From b13f85fbda7ba9b3035e790c1f138f91c7b9c6ff Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 9 Feb 2023 20:05:44 -0800 Subject: [PATCH 01/33] chore: remove empty amm integration test --- .../inter-protocol/test/smartWallet/test-amm-integration.js | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 packages/inter-protocol/test/smartWallet/test-amm-integration.js diff --git a/packages/inter-protocol/test/smartWallet/test-amm-integration.js b/packages/inter-protocol/test/smartWallet/test-amm-integration.js deleted file mode 100644 index ef25b243a5f..00000000000 --- a/packages/inter-protocol/test/smartWallet/test-amm-integration.js +++ /dev/null @@ -1,6 +0,0 @@ -import test from 'ava'; - -// defer to after ps0 -test.todo('trade amm'); -// make a smart wallet -// suggestIssuer From e2207ff4257f890fd265b98c7f71d9ebd2a3b2df Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 9 Feb 2023 11:27:36 -0800 Subject: [PATCH 02/33] refactor(fluxAggregator): split singleton / contract --- .../inter-protocol/scripts/build-bundles.js | 5 +- .../inter-protocol/scripts/price-feed-core.js | 2 +- .../src/price/fluxAggregator.contract.js | 63 +++++++++++++++++++ .../src/price/fluxAggregator.js | 49 ++++++--------- .../inter-protocol/src/price/roundsManager.js | 6 +- .../src/proposals/price-feed-proposal.js | 2 +- .../test/smartWallet/contexts.js | 4 +- .../smartWallet/test-oracle-integration.js | 2 +- .../test/test-priceAggregatorChainlink.js | 4 +- 9 files changed, 97 insertions(+), 40 deletions(-) create mode 100644 packages/inter-protocol/src/price/fluxAggregator.contract.js diff --git a/packages/inter-protocol/scripts/build-bundles.js b/packages/inter-protocol/scripts/build-bundles.js index 76b5339cc8a..0c970c702cf 100644 --- a/packages/inter-protocol/scripts/build-bundles.js +++ b/packages/inter-protocol/scripts/build-bundles.js @@ -29,7 +29,10 @@ await createBundles( '../src/econCommitteeCharter.js', '../bundles/bundle-econCommitteeCharter.js', ], - ['../src/price/fluxAggregator.js', '../bundles/bundle-fluxAggregator.js'], + [ + '../src/price/fluxAggregator.contract.js', + '../bundles/bundle-fluxAggregator.js', + ], ], dirname, ); diff --git a/packages/inter-protocol/scripts/price-feed-core.js b/packages/inter-protocol/scripts/price-feed-core.js index ee5c0ff1f3b..18ac04e9102 100644 --- a/packages/inter-protocol/scripts/price-feed-core.js +++ b/packages/inter-protocol/scripts/price-feed-core.js @@ -57,7 +57,7 @@ export const defaultProposalBuilder = async ( brandOutRef: brandOut && publishRef(brandOut), priceAggregatorRef: publishRef( install( - '@agoric/inter-protocol/src/price/fluxAggregator.js', + '@agoric/inter-protocol/src/price/fluxAggregator.contract.js', '../bundles/bundle-fluxAggregator.js', ), ), diff --git a/packages/inter-protocol/src/price/fluxAggregator.contract.js b/packages/inter-protocol/src/price/fluxAggregator.contract.js new file mode 100644 index 00000000000..52a73a0132c --- /dev/null +++ b/packages/inter-protocol/src/price/fluxAggregator.contract.js @@ -0,0 +1,63 @@ +import { AssetKind, makeIssuerKit } from '@agoric/ertp'; +import { assertAllDefined } from '@agoric/internal'; +import { E } from '@endo/eventual-send'; +import { provideFluxAggregator } from './fluxAggregator.js'; + +/** + * @typedef {import('@agoric/vat-data').Baggage} Baggage + * @typedef {import('@agoric/time/src/types').TimerService} TimerService + */ + +/** + * 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, + * brandOut: Brand<'nat'>, + * unitAmountIn?: Amount<'nat'>, + * }>} zcf + * @param {{ + * marshaller: Marshaller, + * quoteMint?: ERef>, + * storageNode: ERef, + * }} privateArgs + * @param {Baggage} baggage + */ +export const start = async (zcf, privateArgs, baggage) => { + const { timer: timerP } = zcf.getTerms(); + + const quoteMintP = + privateArgs.quoteMint || makeIssuerKit('quote', AssetKind.SET).mint; + const [quoteMint, quoteIssuerRecord] = await Promise.all([ + quoteMintP, + zcf.saveIssuer(E(quoteMintP).getIssuer(), 'Quote'), + ]); + const quoteKit = { + ...quoteIssuerRecord, + mint: quoteMint, + }; + + const { marshaller, storageNode: storageNodeP } = privateArgs; + assertAllDefined({ marshaller, storageNodeP }); + + const timer = await timerP; + const storageNode = await storageNodeP; + + const fa = provideFluxAggregator( + baggage, + zcf, + timer, + quoteKit, + storageNode, + marshaller, + ); + + return harden({ + creatorFacet: fa.creatorFacet, + publicFacet: fa.publicFacet, + }); +}; +harden(start); diff --git a/packages/inter-protocol/src/price/fluxAggregator.js b/packages/inter-protocol/src/price/fluxAggregator.js index 0d5f8d60fd2..ef18f43f51f 100644 --- a/packages/inter-protocol/src/price/fluxAggregator.js +++ b/packages/inter-protocol/src/price/fluxAggregator.js @@ -2,7 +2,7 @@ * 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 { AmountMath } from '@agoric/ertp'; import { assertAllDefined } from '@agoric/internal'; import { makeNotifierFromSubscriber, @@ -63,20 +63,26 @@ const priceDescriptionFromQuote = quote => quote.quoteAmount.value[0]; * the *Node Operator Aggregation* logic of [Chainlink price * feeds](https://blog.chain.link/levels-of-data-aggregation-in-chainlink-price-feeds/). * + * @param {Baggage} baggage * @param {ZCF, * brandOut: Brand<'nat'>, * unitAmountIn?: Amount<'nat'>, * }>} zcf - * @param {{ - * marshaller: Marshaller, - * quoteMint?: ERef>, - * storageNode: ERef, - * }} privateArgs - * @param {Baggage} baggage + * @param {TimerService} timerPresence + * @param {IssuerRecord<'set'> & { mint: Mint<'set'> }} quoteKit + * @param {StorageNode} storageNode + * @param {Marshaller} marshaller */ -export const start = async (zcf, privateArgs, baggage) => { +export const provideFluxAggregator = ( + baggage, + zcf, + timerPresence, + quoteKit, + storageNode, + marshaller, +) => { // 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). @@ -89,7 +95,6 @@ export const start = async (zcf, privateArgs, baggage) => { minSubmissionValue, restartDelay, timeout, - timer, unitAmountIn = AmountMath.make(brandIn, 1n), } = zcf.getTerms(); @@ -103,27 +108,9 @@ export const start = async (zcf, privateArgs, baggage) => { minSubmissionValue, restartDelay, timeout, - timer, unitAmountIn, }); - // 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', - ); - const quoteKit = { - ...quoteIssuerRecord, - mint: quoteMint, - }; - - const { marshaller, storageNode } = privateArgs; - assertAllDefined({ marshaller, storageNode }); - const makeDurablePublishKit = prepareDurablePublishKit( baggage, 'Price Aggregator publish kit', @@ -180,7 +167,7 @@ export const start = async (zcf, privateArgs, baggage) => { createQuote: roundsManagerKit.contract.makeCreateQuote(), notifier: makeNotifierFromSubscriber(answerSubscriber), quoteIssuer: quoteKit.issuer, - timer, + timer: timerPresence, actualBrandIn: brandIn, actualBrandOut: brandOut, }); @@ -266,7 +253,7 @@ export const start = async (zcf, privateArgs, baggage) => { maxSubmissionValue, oracleId, // must be unique per vat roundPowers: roundsManagerKit.oracle, - timer, + timer: timerPresence, }), ); oracles.init(oracleId, oracleAdmin); @@ -283,7 +270,7 @@ export const start = async (zcf, privateArgs, baggage) => { * @returns {Promise} */ async oracleRoundState(oracleId, queriedRoundId) { - const blockTimestamp = await E(timer).getCurrentTimestamp(); + const blockTimestamp = await E(timerPresence).getCurrentTimestamp(); const status = await E(oracles.get(oracleId)).getStatus(); const oracleCount = oracles.getSize(); @@ -342,4 +329,4 @@ export const start = async (zcf, privateArgs, baggage) => { return harden({ creatorFacet, publicFacet }); }; -harden(start); +harden(provideFluxAggregator); diff --git a/packages/inter-protocol/src/price/roundsManager.js b/packages/inter-protocol/src/price/roundsManager.js index 0aedbd5b434..106fddfe271 100644 --- a/packages/inter-protocol/src/price/roundsManager.js +++ b/packages/inter-protocol/src/price/roundsManager.js @@ -75,9 +75,13 @@ const validRoundId = roundId => { * @property {number} roundTimeout */ +/** + * @typedef {IssuerRecord<'set'> & { mint: Mint<'set'> }} QuoteKit + */ + /** * @typedef {Readonly & { mint: ERef> }, + * quoteKit: QuoteKit, * answerPublisher: Publisher, * brandIn: Brand<'nat'>, * brandOut: Brand<'nat'>, diff --git a/packages/inter-protocol/src/proposals/price-feed-proposal.js b/packages/inter-protocol/src/proposals/price-feed-proposal.js index 9b4421cf78e..a44df843576 100644 --- a/packages/inter-protocol/src/proposals/price-feed-proposal.js +++ b/packages/inter-protocol/src/proposals/price-feed-proposal.js @@ -128,7 +128,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'), [ diff --git a/packages/inter-protocol/test/smartWallet/contexts.js b/packages/inter-protocol/test/smartWallet/contexts.js index 1ed95bf5245..ec7a7471507 100644 --- a/packages/inter-protocol/test/smartWallet/contexts.js +++ b/packages/inter-protocol/test/smartWallet/contexts.js @@ -80,10 +80,10 @@ export const makeDefaultTestContext = async (t, makeSpace) => { 'installation', ); const paBundle = await bundleCache.load( - '../inter-protocol/src/price/fluxAggregator.js', + '../inter-protocol/src/price/fluxAggregator.contract.js', 'priceAggregator', ); - /** @type {Promise>} */ + /** @type {Promise>} */ const paInstallation = E(zoe).install(paBundle); await E(installAdmin).update('priceAggregator', paInstallation); diff --git a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js index 3950244d014..706e7df76f4 100644 --- a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js +++ b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js @@ -88,7 +88,7 @@ const setupFeedWithWallets = async (t, oracleAddresses) => { await t.context.simpleCreatePriceFeed(oracleAddresses, 'ATOM', 'USD'); - /** @type {import('@agoric/zoe/src/zoeService/utils.js').Instance} */ + /** @type {import('@agoric/zoe/src/zoeService/utils.js').Instance} */ const priceAggregator = await E(agoricNames).lookup( 'instance', 'ATOM-USD price feed', diff --git a/packages/inter-protocol/test/test-priceAggregatorChainlink.js b/packages/inter-protocol/test/test-priceAggregatorChainlink.js index 66f72e3df05..4be2e86d448 100644 --- a/packages/inter-protocol/test/test-priceAggregatorChainlink.js +++ b/packages/inter-protocol/test/test-priceAggregatorChainlink.js @@ -26,7 +26,7 @@ const test = unknownTest; const filename = new URL(import.meta.url).pathname; const dirname = path.dirname(filename); -const aggregatorPath = `${dirname}/../src/price/fluxAggregator.js`; +const aggregatorPath = `${dirname}/../src/price/fluxAggregator.contract.js`; const defaultConfig = { maxSubmissionCount: 1000, @@ -52,7 +52,7 @@ const makeContext = async () => { // else, and they can use it to create a new contract instance // using the same code. vatAdminState.installBundle('b1-aggregator', aggregatorBundle); - /** @type {Installation} */ + /** @type {Installation} */ const aggregatorInstallation = await E(zoe).installBundleID('b1-aggregator'); const link = makeIssuerKit('$LINK', AssetKind.NAT); From 727f40c0f0ce25d0c2e31a34798829806358ac41 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 9 Feb 2023 15:06:35 -0800 Subject: [PATCH 03/33] chore(types): BundleCap --- packages/SwingSet/src/types-external.js | 2 +- packages/zoe/src/zoeService/startInstance.js | 2 +- packages/zoe/tools/fakeVatAdmin.js | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/SwingSet/src/types-external.js b/packages/SwingSet/src/types-external.js index bd26030b502..309add57b83 100644 --- a/packages/SwingSet/src/types-external.js +++ b/packages/SwingSet/src/types-external.js @@ -303,7 +303,7 @@ export {}; * Vat Creation and Management * * @typedef { string } BundleID - * @typedef {*} BundleCap + * @typedef {'BundleCap' & import("@endo/marshal").Remotable<'BundleCap'>} BundleCap * @typedef { { moduleFormat: 'endoZipBase64', endoZipBase64: string, endoZipBase64Sha512: string } } EndoZipBase64Bundle * * @typedef { unknown } Meter diff --git a/packages/zoe/src/zoeService/startInstance.js b/packages/zoe/src/zoeService/startInstance.js index d66f7f0e1ef..5b3f7e56e45 100644 --- a/packages/zoe/src/zoeService/startInstance.js +++ b/packages/zoe/src/zoeService/startInstance.js @@ -20,7 +20,7 @@ const { Fail, quote: q } = assert; /** * @param {any} startInstanceAccess * @param {() => ERef} getZcfBundleCapP - * @param {(id: string) => BundleCap} getBundleCapByIdNow + * @param {(id: string) => ERef} getBundleCapByIdNow * @param {Baggage} [zoeBaggage] * @returns {import('./utils').StartInstance} */ diff --git a/packages/zoe/tools/fakeVatAdmin.js b/packages/zoe/tools/fakeVatAdmin.js index 065bcd275d3..59a46d4995a 100644 --- a/packages/zoe/tools/fakeVatAdmin.js +++ b/packages/zoe/tools/fakeVatAdmin.js @@ -15,7 +15,10 @@ import zcfBundle from '../bundles/bundle-contractFacet.js'; // this simulates a bundlecap, which is normally a swingset "device node" /** @typedef { import('@agoric/swingset-vat').BundleCap } BundleCap */ /** @type {() => BundleCap} */ +// @ts-expect-error cast mock const fakeBundleCap = () => makeHandle('FakeBundleCap'); +/** @type {() => BundleCap} */ +// @ts-expect-error cast mock const bogusBundleCap = () => makeHandle('BogusBundleCap'); export const zcfBundleCap = fakeBundleCap(); @@ -125,6 +128,11 @@ function makeFakeVatAdmin(testContextSetter = undefined, makeRemote = x => x) { getExitMessage: () => exitMessage, getHasExited: () => hasExited, getExitWithFailure: () => exitWithFailure, + /** + * + * @param {string} id + * @param {EndoZipBase64Bundle} bundle + */ installBundle: (id, bundle) => { if (idToBundleCap.has(id)) { assert.equal( From cd4b9474be9553bf7f0092d4a028c63cce745cf2 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 9 Feb 2023 15:23:41 -0800 Subject: [PATCH 04/33] feat: puppetGovernance setup tools --- .../contractGovernance/typedParamManager.js | 4 + packages/governance/src/contractGovernor.js | 6 +- packages/governance/src/contractHelper.js | 3 +- .../unitTests/test-puppetContractGovernor.js | 120 +++++------------- .../tools/puppetContractGovernor.js | 24 +++- packages/governance/tools/puppetGovernance.js | 108 ++++++++++++++++ 6 files changed, 168 insertions(+), 97 deletions(-) create mode 100644 packages/governance/tools/puppetGovernance.js diff --git a/packages/governance/src/contractGovernance/typedParamManager.js b/packages/governance/src/contractGovernance/typedParamManager.js index ab3cfa737e5..0234ba06871 100644 --- a/packages/governance/src/contractGovernance/typedParamManager.js +++ b/packages/governance/src/contractGovernance/typedParamManager.js @@ -51,6 +51,10 @@ const isAsync = { * @typedef {[type: T, value: ParamValueForType]} ST param spec tuple */ +/** + * @typedef {{ type: 'invitation', value: Amount<'set'> }} InvitationParam + */ + // XXX better to use the manifest constant ParamTypes // but importing that here turns this file into a module, // breaking the ambient typing diff --git a/packages/governance/src/contractGovernor.js b/packages/governance/src/contractGovernor.js index df86aba698b..b19c042f1f3 100644 --- a/packages/governance/src/contractGovernor.js +++ b/packages/governance/src/contractGovernor.js @@ -121,7 +121,11 @@ const validateQuestionFromCounter = async (zoe, electorate, voteCounter) => { */ /** - * @template {() => {creatorFacet: GovernorFacet, publicFacet: GovernedPublicFacetMethods} } SF Start function of governed contract + * @typedef {() => {creatorFacet: GovernorFacet, publicFacet: GovernedPublicFacetMethods}} GovernableStartFn + */ + +/** + * @template {GovernableStartFn} SF Start function of governed contract * @param {ZCF<{ * timer: import('@agoric/time/src/types').TimerService, * governedContractInstallation: Installation, diff --git a/packages/governance/src/contractHelper.js b/packages/governance/src/contractHelper.js index ca7ece63e30..62b2a4d3f25 100644 --- a/packages/governance/src/contractHelper.js +++ b/packages/governance/src/contractHelper.js @@ -115,7 +115,7 @@ const facetHelpers = (zcf, paramManager) => { /** * @template {{}} CF * @param {CF} limitedCreatorFacet - * @param {{}} [governedApis] + * @param {Record unknown>} [governedApis] * @returns {GovernorFacet} */ const makeFarGovernorFacet = (limitedCreatorFacet, governedApis = {}) => { @@ -127,7 +127,6 @@ const facetHelpers = (zcf, paramManager) => { // The contract provides a facet with the APIs that can be invoked by // governance /** @type {() => GovernedApis} */ - // @ts-expect-error TS think this is a RemotableBrand?? getGovernedApis: () => Far('governedAPIs', governedApis), // The facet returned by getGovernedApis is Far, so we can't see what // methods it has. There's no clean way to have contracts specify the APIs diff --git a/packages/governance/test/unitTests/test-puppetContractGovernor.js b/packages/governance/test/unitTests/test-puppetContractGovernor.js index e799edb4c71..76dad49d8dd 100644 --- a/packages/governance/test/unitTests/test-puppetContractGovernor.js +++ b/packages/governance/test/unitTests/test-puppetContractGovernor.js @@ -1,20 +1,16 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import '@agoric/zoe/exported.js'; import { makeNotifierFromAsyncIterable } from '@agoric/notifier'; import { makeZoeKit } from '@agoric/zoe'; -import bundleSource from '@endo/bundle-source'; -import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; import { makeFakeVatAdmin } from '@agoric/zoe/tools/fakeVatAdmin.js'; +import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; +import bundleSource from '@endo/bundle-source'; import { E } from '@endo/eventual-send'; - import { resolve as importMetaResolve } from 'import-meta-resolve'; -import { MALLEABLE_NUMBER } from '../swingsetTests/contractGovernor/governedContract.js'; -import { CONTRACT_ELECTORATE, ParamTypes } from '../../src/index.js'; -const governedRoot = '../swingsetTests/contractGovernor/governedContract.js'; -const contractGovernorRoot = '../../tools/puppetContractGovernor.js'; -const autoRefundRoot = '@agoric/zoe/src/contracts/automaticRefund.js'; +import { CONTRACT_ELECTORATE, ParamTypes } from '../../src/index.js'; +import { setUpGovernedContract } from '../../tools/puppetGovernance.js'; +import { MALLEABLE_NUMBER } from '../swingsetTests/contractGovernor/governedContract.js'; const makeBundle = async sourceRoot => { const url = await importMetaResolve(sourceRoot, import.meta.url); @@ -22,12 +18,10 @@ const makeBundle = async sourceRoot => { const contractBundle = await bundleSource(path); return contractBundle; }; - // makeBundle is a slow step, so we do it once for all the tests. -const contractGovernorBundleP = makeBundle(contractGovernorRoot); -const governedBundleP = makeBundle(governedRoot); -// could be called fakeCommittee. It's used as a source of invitations only -const autoRefundBundleP = makeBundle(autoRefundRoot); +const governedBundleP = await makeBundle( + '../swingsetTests/contractGovernor/governedContract.js', +); const setUpZoeForTest = async setJig => { const makeFar = o => o; @@ -50,72 +44,13 @@ const setUpZoeForTest = async setJig => { }; }; -const installBundle = (zoe, contractBundle) => E(zoe).install(contractBundle); - -// contract governor wants a committee invitation. give it a random invitation -async function getInvitation(zoe, autoRefundInstance) { - const autoRefundFacets = await E(zoe).startInstance(autoRefundInstance); - const invitationP = E(autoRefundFacets.publicFacet).makeInvitation(); - const [fakeInvitationPayment, fakeInvitationAmount] = await Promise.all([ - invitationP, - E(E(zoe).getInvitationIssuer()).getAmountOf(invitationP), - ]); - return { fakeInvitationPayment, fakeInvitationAmount }; -} - -const setUpGovernedContract = async (zoe, electorateTerms, timer) => { - const [contractGovernorBundle, autoRefundBundle, governedBundle] = - await Promise.all([ - contractGovernorBundleP, - autoRefundBundleP, - governedBundleP, - ]); - - const [governor, autoRefund, governed] = await Promise.all([ - installBundle(zoe, contractGovernorBundle), - installBundle(zoe, autoRefundBundle), - installBundle(zoe, governedBundle), - ]); - const installs = { governor, autoRefund, governed }; - const { fakeInvitationPayment, fakeInvitationAmount } = await getInvitation( - zoe, - autoRefund, - ); - - const governedTerms = { - governedParams: { - [MALLEABLE_NUMBER]: { - type: ParamTypes.NAT, - value: 602214090000000000000000n, - }, - [CONTRACT_ELECTORATE]: { - type: ParamTypes.INVITATION, - value: fakeInvitationAmount, - }, - }, - governedApis: ['governanceApi'], - }; - const governorTerms = { - timer, - governedContractInstallation: governed, - governed: { - terms: governedTerms, - issuerKeywordRecord: {}, - }, - }; - - const governorFacets = await E(zoe).startInstance( - governor, - {}, - governorTerms, - { - governed: { - initialPoserInvitation: fakeInvitationPayment, - }, +const governedTerms = { + governedParams: { + [MALLEABLE_NUMBER]: { + type: ParamTypes.NAT, + value: 602214090000000000000000n, }, - ); - - return { governorFacets, installs }; + }, }; test('multiple params bad change', async t => { @@ -123,8 +58,9 @@ test('multiple params bad change', async t => { const timer = buildManualTimer(t.log); const { governorFacets } = await setUpGovernedContract( zoe, - { committeeName: 'Demos', committeeSize: 1 }, + E(zoe).install(governedBundleP), timer, + governedTerms, ); const paramChangesSpec = harden({ @@ -147,10 +83,11 @@ test('multiple params bad change', async t => { test('change a param', async t => { const { zoe } = await setUpZoeForTest(() => {}); const timer = buildManualTimer(t.log); - const { governorFacets, installs } = await setUpGovernedContract( + const { governorFacets, getFakeInvitation } = await setUpGovernedContract( zoe, - { committeeName: 'Demos', committeeSize: 1 }, + E(zoe).install(governedBundleP), timer, + governedTerms, ); /** @type {GovernedPublicFacet} */ @@ -173,10 +110,8 @@ test('change a param', async t => { }); // This is the wrong kind of invitation, but governance can't tell - const { fakeInvitationPayment, fakeInvitationAmount } = await getInvitation( - zoe, - installs.autoRefund, - ); + const { fakeInvitationPayment, fakeInvitationAmount } = + await getFakeInvitation(); const paramChangesSpec = harden({ paramPath: { key: 'governedParams' }, @@ -207,8 +142,9 @@ test('set offer Filter directly', async t => { const timer = buildManualTimer(t.log); const { governorFacets } = await setUpGovernedContract( zoe, - { committeeName: 'Demos', committeeSize: 1 }, + E(zoe).install(governedBundleP), timer, + governedTerms, ); await E(governorFacets.creatorFacet).setFilters(['whatever']); @@ -223,12 +159,18 @@ test('call API directly', async t => { const timer = buildManualTimer(t.log); const { governorFacets } = await setUpGovernedContract( zoe, - { committeeName: 'Demos', committeeSize: 1 }, + E(zoe).install(governedBundleP), timer, + governedTerms, ); - await E(governorFacets.creatorFacet).invokeAPI('governanceApi', []); + const result = await E(governorFacets.creatorFacet).invokeAPI( + 'governanceApi', + [], + ); + t.deepEqual(result, { apiMethodName: 'governanceApi', methodArgs: [] }); t.deepEqual( + // @ts-expect-error FIXME type the puppet extensions await E(E(governorFacets.creatorFacet).getPublicFacet()).getApiCalled(), 1, ); diff --git a/packages/governance/tools/puppetContractGovernor.js b/packages/governance/tools/puppetContractGovernor.js index fadc4e2f1b1..e3a69a2465b 100644 --- a/packages/governance/tools/puppetContractGovernor.js +++ b/packages/governance/tools/puppetContractGovernor.js @@ -4,7 +4,10 @@ import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; // eslint-disable-next-line no-unused-vars +import { Fail } from '@agoric/assert'; +// eslint-disable-next-line no-unused-vars -- used by typedef import { CONTRACT_ELECTORATE } from '../src/contractGovernance/governParam.js'; +import { makeApiInvocationPositions } from '../src/contractGovernance/governApi.js'; // @file a version of the contractGovernor.js contract simplified for testing. // It removes the electorate and doesn't try to support legibility. @@ -12,13 +15,13 @@ import { CONTRACT_ELECTORATE } from '../src/contractGovernance/governParam.js'; // It adds the ability for tests to update parameters directly. /** - * @template {() => {creatorFacet: GovernorFacet, publicFacet: unknown} } SF Start function of governed contract + * @template {import('../src/contractGovernor.js').GovernableStartFn} SF Start function of governed contract * @param {ZCF<{ * timer: import('@agoric/time/src/types').TimerService, * governedContractInstallation: Installation, * governed: { * issuerKeywordRecord: IssuerKeywordRecord, - * terms: {governedParams: {[CONTRACT_ELECTORATE]: Amount<'set'>}}, + * terms: {governedParams: {[CONTRACT_ELECTORATE]: import('../src/contractGovernance/typedParamManager.js').InvitationParam }}, * } * }>} zcf * @param {{ @@ -71,8 +74,19 @@ export const start = async (zcf, privateArgs) => { * @param {string} apiMethodName * @param {unknown[]} methodArgs */ - const invokeAPI = (apiMethodName, methodArgs) => - E(E(governedCF).getGovernedApis())[apiMethodName](...methodArgs); + const invokeAPI = async (apiMethodName, methodArgs) => { + const governedNames = await E(governedCF).getGovernedApiNames(); + governedNames.includes(apiMethodName) || + Fail`${apiMethodName} is not a governed API.`; + + const { positive } = makeApiInvocationPositions(apiMethodName, methodArgs); + + return E(E(governedCF).getGovernedApis()) + [apiMethodName](...methodArgs) + .then(() => { + return positive; + }); + }; const creatorFacet = Far('governor creatorFacet', { changeParams, @@ -94,6 +108,6 @@ export const start = async (zcf, privateArgs) => { }; harden(start); /** - * @template {() => {creatorFacet: GovernorFacet, publicFacet: unknown} } SF Start function of governed contract + * @template {import('../src/contractGovernor.js').GovernableStartFn} SF Start function of governed contract * @typedef {Awaited>>} PuppetContractGovernorKit */ diff --git a/packages/governance/tools/puppetGovernance.js b/packages/governance/tools/puppetGovernance.js new file mode 100644 index 00000000000..90c5065293f --- /dev/null +++ b/packages/governance/tools/puppetGovernance.js @@ -0,0 +1,108 @@ +/* eslint-disable import/no-extraneous-dependencies */ + +import bundleSource from '@endo/bundle-source'; +import { E } from '@endo/eventual-send'; +import { resolve as importMetaResolve } from 'import-meta-resolve'; +import { CONTRACT_ELECTORATE, ParamTypes } from '../src/index.js'; + +const makeBundle = async sourceRoot => { + const url = await importMetaResolve(sourceRoot, import.meta.url); + const path = new URL(url).pathname; + const contractBundle = await bundleSource(path); + return contractBundle; +}; + +// makeBundle is a slow step, so we do it once for all the tests. +const contractGovernorBundleP = makeBundle('./puppetContractGovernor.js'); +// could be called fakeCommittee. It's used as a source of invitations only +const autoRefundBundleP = makeBundle( + '@agoric/zoe/src/contracts/automaticRefund.js', +); + +/** */ + +/** + * @template {import('../src/contractGovernor.js').GovernableStartFn} T governed contract startfn + * @param {ERef} zoe + * @param {ERef>} governedP + * @param {import('@agoric/swingset-vat/src/vats/timer/vat-timer.js').TimerService} timer + * @param {{ governedParams?: Record, governedApis?: string[] }} governedTerms + * @param {{}} governedPrivateArgs + */ +export const setUpGovernedContract = async ( + zoe, + governedP, + timer, + governedTerms = {}, + governedPrivateArgs = {}, +) => { + const [contractGovernorBundle, autoRefundBundle] = await Promise.all([ + contractGovernorBundleP, + autoRefundBundleP, + ]); + + /** + * @type {[ + * Installation, + * Installation, + * Installation, + * ]} + */ + const [governor, autoRefund, governed] = await Promise.all([ + E(zoe).install(contractGovernorBundle), + E(zoe).install(autoRefundBundle), + governedP, + ]); + const installs = { governor, autoRefund, governed }; + + /** + * Contract governor wants a committee invitation. Give it a random invitation. + */ + async function getFakeInvitation() { + const autoRefundFacets = await E(zoe).startInstance(autoRefund); + const invitationP = E(autoRefundFacets.publicFacet).makeInvitation(); + const [fakeInvitationPayment, fakeInvitationAmount] = await Promise.all([ + invitationP, + E(E(zoe).getInvitationIssuer()).getAmountOf(invitationP), + ]); + return { fakeInvitationPayment, fakeInvitationAmount }; + } + + const { fakeInvitationAmount, fakeInvitationPayment } = + await getFakeInvitation(); + + const governedTermsWithElectorate = { + ...governedTerms, + governedParams: { + ...governedTerms.governedParams, + [CONTRACT_ELECTORATE]: { + type: ParamTypes.INVITATION, + value: fakeInvitationAmount, + }, + }, + governedApis: governedTerms.governedApis, + }; + const governorTerms = { + timer, + governedContractInstallation: governed, + governed: { + terms: governedTermsWithElectorate, + issuerKeywordRecord: {}, + }, + }; + + const governorFacets = await E(zoe).startInstance( + governor, + {}, + governorTerms, + { + governed: { + ...governedPrivateArgs, + initialPoserInvitation: fakeInvitationPayment, + }, + }, + ); + + return { getFakeInvitation, governorFacets, installs }; +}; +harden(setUpGovernedContract); From fbe5136fa85433ad3b2a62d3d9d202ef39d96887 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 9 Feb 2023 19:37:18 -0800 Subject: [PATCH 05/33] test(price): rename test-fluxAggregator --- .../test-fluxAggregator.js} | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename packages/inter-protocol/test/{test-priceAggregatorChainlink.js => price/test-fluxAggregator.js} (99%) diff --git a/packages/inter-protocol/test/test-priceAggregatorChainlink.js b/packages/inter-protocol/test/price/test-fluxAggregator.js similarity index 99% rename from packages/inter-protocol/test/test-priceAggregatorChainlink.js rename to packages/inter-protocol/test/price/test-fluxAggregator.js index 4be2e86d448..ab9fc15a3b1 100644 --- a/packages/inter-protocol/test/test-priceAggregatorChainlink.js +++ b/packages/inter-protocol/test/price/test-fluxAggregator.js @@ -18,7 +18,8 @@ import { subscribeEach } from '@agoric/notifier'; import { makeFakeVatAdmin } from '@agoric/zoe/tools/fakeVatAdmin.js'; import { makeZoeKit } from '@agoric/zoe/src/zoeService/zoe.js'; import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; -import { topicPath } from './supports.js'; +import { provideFluxAggregator } from '../../src/price/fluxAggregator.js'; +import { topicPath } from '../supports.js'; /** @type {import('ava').TestFn>>} */ const test = unknownTest; From 2add6955d11e49a62ef2c75e53a1df2b9a610b72 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 9 Feb 2023 19:36:33 -0800 Subject: [PATCH 06/33] test(price): simplify fluxAggregator test --- .../test/price/test-fluxAggregator.js | 131 ++++-------------- 1 file changed, 30 insertions(+), 101 deletions(-) diff --git a/packages/inter-protocol/test/price/test-fluxAggregator.js b/packages/inter-protocol/test/price/test-fluxAggregator.js index ab9fc15a3b1..f8a1733a447 100644 --- a/packages/inter-protocol/test/price/test-fluxAggregator.js +++ b/packages/inter-protocol/test/price/test-fluxAggregator.js @@ -1,22 +1,18 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { test as unknownTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import path from 'path'; - -import bundleSource from '@endo/bundle-source'; - +import { AssetKind, makeIssuerKit } from '@agoric/ertp'; import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; -import { makeIssuerKit, AssetKind } from '@agoric/ertp'; +import { makeMockChainStorageRoot } from '@agoric/internal/src/storage-test-utils.js'; +import { subscribeEach } from '@agoric/notifier'; import { eventLoopIteration, makeFakeMarshaller, } from '@agoric/notifier/tools/testSupports.js'; -import { makeMockChainStorageRoot } from '@agoric/internal/src/storage-test-utils.js'; -import { subscribeEach } from '@agoric/notifier'; -import { makeFakeVatAdmin } from '@agoric/zoe/tools/fakeVatAdmin.js'; -import { makeZoeKit } from '@agoric/zoe/src/zoeService/zoe.js'; +import { makeScalarBigMapStore } from '@agoric/vat-data'; +import { setupZCFTest } from '@agoric/zoe/test/unitTests/zcf/setupZcfTest.js'; import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; import { provideFluxAggregator } from '../../src/price/fluxAggregator.js'; import { topicPath } from '../supports.js'; @@ -24,11 +20,6 @@ import { topicPath } from '../supports.js'; /** @type {import('ava').TestFn>>} */ const test = unknownTest; -const filename = new URL(import.meta.url).pathname; -const dirname = path.dirname(filename); - -const aggregatorPath = `${dirname}/../src/price/fluxAggregator.contract.js`; - const defaultConfig = { maxSubmissionCount: 1000, minSubmissionCount: 2, @@ -39,66 +30,36 @@ const defaultConfig = { }; 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 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-aggregator', aggregatorBundle); - /** @type {Installation} */ - const aggregatorInstallation = await E(zoe).installBundleID('b1-aggregator'); - const link = makeIssuerKit('$LINK', AssetKind.NAT); const usd = makeIssuerKit('$USD', AssetKind.NAT); async function makeChainlinkAggregator(config) { - const { - maxSubmissionCount, - maxSubmissionValue, - minSubmissionCount, - minSubmissionValue, - restartDelay, - timeout, - } = config; + const terms = { ...config, brandIn: link.brand, brandOut: usd.brand }; + const zcfTestKit = await setupZCFTest(undefined, terms); // ??? 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( - aggregatorInstallation, - undefined, - { - timer, - brandIn: link.brand, - brandOut: usd.brand, - maxSubmissionCount, - minSubmissionCount, - restartDelay, - timeout, - minSubmissionValue, - maxSubmissionValue, - }, - { - marshaller, - storageNode: E(storageNode).makeChildNode('LINK-USD_price_feed'), - }, + const manualTimer = buildManualTimer(() => {}); + + const baggage = makeScalarBigMapStore('test baggage'); + const quoteIssuerKit = makeIssuerKit('quote', AssetKind.SET); + + const aggregator = provideFluxAggregator( + baggage, + zcfTestKit.zcf, + manualTimer, + { ...quoteIssuerKit, assetKind: 'set', displayInfo: undefined }, + await E(storageNode).makeChildNode('LINK-USD_price_feed'), + marshaller, ); - return { ...aggregator, mockStorageRoot }; + + return { ...aggregator, manualTimer, mockStorageRoot }; } - return { makeChainlinkAggregator, zoe }; + return { makeChainlinkAggregator }; }; test.before('setup aggregator and oracles', async t => { @@ -106,12 +67,8 @@ test.before('setup aggregator and oracles', async t => { }); test('basic', async t => { - 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 oracleTimer = aggregator.manualTimer; const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( 'agorice1priceOracleA', @@ -168,16 +125,12 @@ test('basic', async t => { }); test('timeout', async t => { - const { zoe } = t.context; - 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); + const oracleTimer = aggregator.manualTimer; const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( 'agorice1priceOracleA', @@ -224,15 +177,11 @@ test('timeout', async t => { }); test('issue check', async t => { - const { zoe } = t.context; - 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); + const oracleTimer = aggregator.manualTimer; const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( 'agorice1priceOracleA', @@ -275,15 +224,11 @@ test('issue check', async t => { }); test('supersede', async t => { - const { zoe } = t.context; - 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); + const oracleTimer = aggregator.manualTimer; const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( 'agorice1priceOracleA', @@ -334,8 +279,6 @@ test('supersede', async t => { }); test('interleaved', async t => { - const { zoe } = t.context; - const aggregator = await t.context.makeChainlinkAggregator({ ...defaultConfig, maxSubmissionCount: 3, @@ -343,9 +286,7 @@ test('interleaved', async t => { restartDelay: 1, timeout: 5, }); - /** @type {{ timer: ManualTimer }} */ - // @ts-expect-error cast - const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); + const oracleTimer = aggregator.manualTimer; const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( 'agorice1priceOracleA', @@ -477,17 +418,13 @@ test('interleaved', async t => { }); test('larger', async t => { - const { zoe } = t.context; - 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); + const oracleTimer = aggregator.manualTimer; const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( 'agorice1priceOracleA', @@ -558,17 +495,13 @@ test('larger', async t => { }); test('suggest', async t => { - const { zoe } = t.context; - 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); + const oracleTimer = aggregator.manualTimer; const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( 'agorice1priceOracleA', @@ -660,16 +593,12 @@ test('suggest', async t => { }); test('notifications', async t => { - const { zoe } = t.context; - 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); + const oracleTimer = aggregator.manualTimer; const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( 'agorice1priceOracleA', From 9e65ecf5d09ccb512e05b4512b6c91fa2cb9dcd1 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 08:20:05 -0800 Subject: [PATCH 07/33] chore(types): cleanup --- .../governance/src/contractGovernance/governParam.js | 9 ++++++++- packages/governance/src/contractGovernor.js | 4 ++-- packages/governance/src/types-ambient.js | 10 ---------- .../inter-protocol/src/proposals/econ-behaviors.js | 9 ++++++--- packages/internal/package.json | 3 ++- packages/vats/src/core/types.js | 2 ++ yarn.lock | 5 +++++ 7 files changed, 25 insertions(+), 17 deletions(-) diff --git a/packages/governance/src/contractGovernance/governParam.js b/packages/governance/src/contractGovernance/governParam.js index aafa3fa7749..a9434f1be4d 100644 --- a/packages/governance/src/contractGovernance/governParam.js +++ b/packages/governance/src/contractGovernance/governParam.js @@ -51,7 +51,14 @@ const assertBallotConcernsParam = (paramSpec, questionSpec) => { Fail`Question path (${issue.spec.paramPath}) doesn't match request (${paramPath})`; }; -/** @type {SetupGovernance} */ +/** + * @param {ERef} zoe + * @param {ERef} paramManagerRetriever + * @param {Instance} contractInstance + * @param {ERef} timer + * @param {() => Promise} getUpdatedPoserFacet + * @returns {Promise} + */ const setupParamGovernance = async ( zoe, paramManagerRetriever, diff --git a/packages/governance/src/contractGovernor.js b/packages/governance/src/contractGovernor.js index b19c042f1f3..fb0868055d7 100644 --- a/packages/governance/src/contractGovernor.js +++ b/packages/governance/src/contractGovernor.js @@ -57,7 +57,7 @@ const validateQuestionFromCounter = async (zoe, electorate, voteCounter) => { /** * @typedef {StandardTerms} ContractGovernorTerms - * @property {import('@agoric/time/src/types').TimerService} timer + * @property {ERef} timer * @property {Installation} governedContractInstallation */ @@ -127,7 +127,7 @@ const validateQuestionFromCounter = async (zoe, electorate, voteCounter) => { /** * @template {GovernableStartFn} SF Start function of governed contract * @param {ZCF<{ - * timer: import('@agoric/time/src/types').TimerService, + * timer: ERef, * governedContractInstallation: Installation, * governed: { * issuerKeywordRecord: IssuerKeywordRecord, diff --git a/packages/governance/src/types-ambient.js b/packages/governance/src/types-ambient.js index 9b8f0889bfc..8a5755333ab 100644 --- a/packages/governance/src/types-ambient.js +++ b/packages/governance/src/types-ambient.js @@ -681,16 +681,6 @@ * @property {CreatedQuestion} createdFilterQuestion */ -/** - * @callback SetupGovernance - * @param {ERef} zoe - * @param {ERef} paramManagerRetriever - * @param {Instance} contractInstance - * @param {import('@agoric/time/src/types').TimerService} timer - * @param {() => Promise} getUpdatedPoserFacet - * @returns {ParamGovernor} - */ - /** * @callback CreatedQuestion * Was this question created by this ContractGovernor? diff --git a/packages/inter-protocol/src/proposals/econ-behaviors.js b/packages/inter-protocol/src/proposals/econ-behaviors.js index 1b1533e3ad2..ffff2901fd0 100644 --- a/packages/inter-protocol/src/proposals/econ-behaviors.js +++ b/packages/inter-protocol/src/proposals/econ-behaviors.js @@ -219,7 +219,8 @@ export const setupAmm = async ( {}, ammGovernorTerms, { - electorateCreatorFacet: committeeCreator, + // FIXME unused? + // electorateCreatorFacet: committeeCreator, governed: { initialPoserInvitation: poserInvitation, storageNode, @@ -314,7 +315,8 @@ export const setupReserve = async ({ {}, reserveGovernorTerms, { - electorateCreatorFacet: committeeCreator, + // FIXME unused? + // electorateCreatorFacet: committeeCreator, governed: { feeMintAccess, initialPoserInvitation: poserInvitation, @@ -462,7 +464,8 @@ export const startVaultFactory = async ( undefined, governorTerms, harden({ - electorateCreatorFacet, + // FIXME unused? + // electorateCreatorFacet, governed: { feeMintAccess, initialPoserInvitation, diff --git a/packages/internal/package.json b/packages/internal/package.json index f5a496bc9c5..854dfdfe9e4 100755 --- a/packages/internal/package.json +++ b/packages/internal/package.json @@ -27,7 +27,8 @@ }, "devDependencies": { "@endo/init": "^0.5.52", - "ava": "^5.1.0" + "ava": "^5.1.0", + "type-fest": "^3.5.7" }, "author": "Agoric", "license": "Apache-2.0", diff --git a/packages/vats/src/core/types.js b/packages/vats/src/core/types.js index fc1aa5e75e1..742bf855385 100644 --- a/packages/vats/src/core/types.js +++ b/packages/vats/src/core/types.js @@ -172,8 +172,10 @@ * installation:{ * produce: Record>, * consume: Record>> & { + * contractGovernor: Promise>, * interchainPool: Promise>, * mintHolder: Promise>, + * psm: Promise>, * walletFactory: Promise>, * }, * }, diff --git a/yarn.lock b/yarn.lock index fd85b3aed11..deb7014428a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12658,6 +12658,11 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^3.5.7: + version "3.5.7" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.5.7.tgz#1ee9efc9a172f4002c40b896689928a7bba537f2" + integrity sha512-6J4bYzb4sdkcLBty4XW7F18VPI66M4boXNE+CY40532oq2OJe6AVMB5NmjOp6skt/jw5mRjz/hLRpuglz0U+FA== + type-is@^1.6.16, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" From acd263b6e7eb08f0490a9b8a5e21fb1f37ff5137 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 09:07:11 -0800 Subject: [PATCH 08/33] chore(types): fix DeeplyAwaited --- packages/internal/src/utils.js | 4 +-- yarn.lock | 59 ++++++++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/packages/internal/src/utils.js b/packages/internal/src/utils.js index abd8d23e8ef..48cd3fec90c 100644 --- a/packages/internal/src/utils.js +++ b/packages/internal/src/utils.js @@ -250,7 +250,7 @@ harden(bindAllMethods); /** * @template {{}} T - * @typedef {{ [K in keyof T]: DeeplyAwaited }} DeeplyAwaitedObject + * @typedef {{ [K in keyof T]: T[K] extends Function ? T[K] : DeeplyAwaited }} DeeplyAwaitedObject */ /** @@ -266,7 +266,7 @@ harden(bindAllMethods); * A more constrained version of {deeplyFulfilled} for type safety until https://github.com/endojs/endo/issues/1257 * Useful in starting contracts that need all terms to be fulfilled in order to be durable. * - * @type {(unfulfilledTerms: T) => import('@endo/far').ERef>} + * @type {(unfulfilledTerms: T) => import('@endo/far').ERef>>} */ export const deeplyFulfilledObject = obj => { assert(isObject(obj), 'param must be an object'); diff --git a/yarn.lock b/yarn.lock index deb7014428a..ced9947613d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3310,6 +3310,14 @@ "@typescript-eslint/types" "5.49.0" "@typescript-eslint/visitor-keys" "5.49.0" +"@typescript-eslint/scope-manager@5.51.0": + version "5.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.51.0.tgz#ad3e3c2ecf762d9a4196c0fbfe19b142ac498990" + integrity sha512-gNpxRdlx5qw3yaHA0SFuTjW4rxeYhpHxt491PEcKF8Z6zpq0kMhe0Tolxt0qjlojS+/wArSDlj/LtE69xUJphQ== + dependencies: + "@typescript-eslint/types" "5.51.0" + "@typescript-eslint/visitor-keys" "5.51.0" + "@typescript-eslint/type-utils@5.33.0": version "5.33.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.33.0.tgz#92ad1fba973c078d23767ce2d8d5a601baaa9338" @@ -3329,6 +3337,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.49.0.tgz#ad66766cb36ca1c89fcb6ac8b87ec2e6dac435c3" integrity sha512-7If46kusG+sSnEpu0yOz2xFv5nRz158nzEXnJFCGVEHWnuzolXKwrH5Bsf9zsNlOQkyZuk0BZKKoJQI+1JPBBg== +"@typescript-eslint/types@5.51.0": + version "5.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.51.0.tgz#e7c1622f46c7eea7e12bbf1edfb496d4dec37c90" + integrity sha512-SqOn0ANn/v6hFn0kjvLwiDi4AzR++CBZz0NV5AnusT2/3y32jdc0G4woXPWHCumWtUXZKPAS27/9vziSsC9jnw== + "@typescript-eslint/typescript-estree@5.33.0": version "5.33.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.33.0.tgz#02d9c9ade6f4897c09e3508c27de53ad6bfa54cf" @@ -3355,6 +3368,19 @@ semver "^7.3.7" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@5.51.0": + version "5.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.51.0.tgz#0ec8170d7247a892c2b21845b06c11eb0718f8de" + integrity sha512-TSkNupHvNRkoH9FMA3w7TazVFcBPveAAmb7Sz+kArY6sLT86PA5Vx80cKlYmd8m3Ha2SwofM1KwraF24lM9FvA== + dependencies: + "@typescript-eslint/types" "5.51.0" + "@typescript-eslint/visitor-keys" "5.51.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + "@typescript-eslint/utils@5.33.0": version "5.33.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.33.0.tgz#46797461ce3146e21c095d79518cc0f8ec574038" @@ -3367,7 +3393,21 @@ eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@^5.10.2": +"@typescript-eslint/utils@^5.10.0": + version "5.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.51.0.tgz#074f4fabd5b12afe9c8aa6fdee881c050f8b4d47" + integrity sha512-76qs+5KWcaatmwtwsDJvBk4H76RJQBFe+Gext0EfJdC3Vd2kpY2Pf//OHHzHp84Ciw0/rYoGTDnIAr3uWhhJYw== + dependencies: + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.51.0" + "@typescript-eslint/types" "5.51.0" + "@typescript-eslint/typescript-estree" "5.51.0" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + semver "^7.3.7" + +"@typescript-eslint/utils@^5.10.2": version "5.49.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.49.0.tgz#1c07923bc55ff7834dfcde487fff8d8624a87b32" integrity sha512-cPJue/4Si25FViIb74sHCLtM4nTSBXtLx1d3/QT6mirQ/c65bV8arBEebBJJizfq8W2YyMoPI/WWPFWitmNqnQ== @@ -3397,6 +3437,14 @@ "@typescript-eslint/types" "5.49.0" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@5.51.0": + version "5.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.51.0.tgz#c0147dd9a36c0de758aaebd5b48cae1ec59eba87" + integrity sha512-Oh2+eTdjHjOFjKA27sxESlA87YPSOJafGCR0md5oeMdh1ZcCfAGCIOL216uTBAkAIptvLIfKQhl7lHxMJet4GQ== + dependencies: + "@typescript-eslint/types" "5.51.0" + eslint-visitor-keys "^3.3.0" + "@web/browser-logs@^0.2.1", "@web/browser-logs@^0.2.2": version "0.2.5" resolved "https://registry.yarnpkg.com/@web/browser-logs/-/browser-logs-0.2.5.tgz#0895efb641eacb0fbc1138c6092bd18c01df2734" @@ -9674,7 +9722,14 @@ node-fetch-npm@^2.0.2: json-parse-better-errors "^1.0.0" safe-buffer "^5.1.1" -node-fetch@2.6.5, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.5: +node-fetch@2.6.5, node-fetch@^2.6.5: + version "2.6.9" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" + integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== + dependencies: + whatwg-url "^5.0.0" + +node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0, node-fetch@^2.6.1: version "2.6.8" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.8.tgz#a68d30b162bc1d8fd71a367e81b997e1f4d4937e" integrity sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg== From d4ac24e4940131c3f110c714c4db13b35ae332d5 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 09:13:28 -0800 Subject: [PATCH 09/33] chore(types): fix allValues --- packages/inter-protocol/src/collect.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/inter-protocol/src/collect.js b/packages/inter-protocol/src/collect.js index 5408fb2242a..c05c4dc780f 100644 --- a/packages/inter-protocol/src/collect.js +++ b/packages/inter-protocol/src/collect.js @@ -3,7 +3,7 @@ const { fromEntries, keys, values } = Object; /** @type { (xs: X[], ys: Y[]) => [X, Y][]} */ export const zip = (xs, ys) => harden(xs.map((x, i) => [x, ys[+i]])); -/** @type { (obj: Record>) => Promise> } */ +/** @type { >>(obj: T) => Promise<{ [K in keyof T]: Awaited}> } */ export const allValues = async obj => { const resolved = await Promise.all(values(obj)); // @ts-expect-error cast From 8010fb2820f12cf6301ba7f0bb3b965adfac7dcf Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 12:56:17 -0800 Subject: [PATCH 10/33] fix(vaultDirector): add setOfferFilter --- packages/inter-protocol/src/vaultFactory/vaultDirector.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/inter-protocol/src/vaultFactory/vaultDirector.js b/packages/inter-protocol/src/vaultFactory/vaultDirector.js index 3d9c9d02520..8750c82a45a 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultDirector.js +++ b/packages/inter-protocol/src/vaultFactory/vaultDirector.js @@ -215,6 +215,7 @@ export const prepareVaultDirector = ( getLimitedCreatorFacet: M.call().returns(M.remotable()), getGovernedApis: M.call().returns(M.record()), getGovernedApiNames: M.call().returns(M.record()), + setOfferFilter: M.call(M.arrayOf(M.string())).returns(), }), machine: M.interface('machine', { addVaultType: M.call(IssuerShape, M.string(), M.record()).returns( From a9c03a513be23963cc0f356bc9ba7222104b8a53 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 12:36:56 -0800 Subject: [PATCH 11/33] fix(vaultDirector): getContractGovernor return --- packages/inter-protocol/src/vaultFactory/vaultDirector.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/inter-protocol/src/vaultFactory/vaultDirector.js b/packages/inter-protocol/src/vaultFactory/vaultDirector.js index 8750c82a45a..78eec0f0c00 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultDirector.js +++ b/packages/inter-protocol/src/vaultFactory/vaultDirector.js @@ -536,14 +536,8 @@ export const prepareVaultDirector = ( // TODO use named getters of TypedParamManager return vaultParamManagers.get(collateralBrand).getParams(); }, - /** - * @returns {Promise} - */ getContractGovernor() { - // PERF consider caching - return E(zcf.getZoeService()).getPublicFacet( - zcf.getTerms().electionManager, - ); + return zcf.getTerms().electionManager; }, /** * @param {string} name From b255ca3508e3174cd6e99b5157d2c33d01b9551c Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 09:02:12 -0800 Subject: [PATCH 12/33] chore(types): installations in WellKnownSpaces --- packages/vats/src/core/types.js | 40 ++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/packages/vats/src/core/types.js b/packages/vats/src/core/types.js index 742bf855385..06131df2423 100644 --- a/packages/vats/src/core/types.js +++ b/packages/vats/src/core/types.js @@ -129,19 +129,35 @@ * @property {(nickname: string, clientAddress: string, powerFlags: string[]) => Promise} createClientFacet */ +/** + * @typedef {{ + * amm: import('@agoric/inter-protocol/src/vpool-xyk-amm/multipoolMarketMaker.js').start, + * binaryVoteCounter: import('@agoric/governance/src/binaryVoteCounter.js').start, + * centralSupply: import('@agoric/vats/src/centralSupply.js').start, + * committee: import('@agoric/governance/src/committee.js').start, + * contractGovernor: import('@agoric/governance/src/contractGovernor.js').start, + * econCommitteeCharter: import('@agoric/inter-protocol/src/econCommitteeCharter.js').start, + * feeDistributor: import('@agoric/inter-protocol/src/feeDistributor.js').start, + * interchainPool: import('@agoric/inter-protocol/src/interchainPool.js').start, + * liquidate: import('@agoric/inter-protocol/src/vaultFactory/liquidateIncrementally.js').start, + * mintHolder: import('@agoric/vats/src/mintHolder.js').prepare, + * noActionElectorate: unknown, + * Pegasus: unknown, + * psm: import('@agoric/inter-protocol/src/psm/psm.js').start, + * priceAggregator: import('@agoric/inter-protocol/src/price/fluxAggregator.contract.js').start, + * provisionPool: import('@agoric/vats/src/provisionPool.js').start, + * reserve: import('@agoric/inter-protocol/src/reserve/assetReserve.js').start, + * stakeFactory: import('@agoric/inter-protocol/src/stakeFactory/stakeFactory.js').start, + * walletFactory: import('@agoric/smart-wallet/src/walletFactory.js').start, + * VaultFactory: import('@agoric/inter-protocol/src/vaultFactory/vaultFactory.js').start, + * }} WellKnownInstallations */ + /** * @typedef {import('../tokens.js').TokenKeyword} TokenKeyword * * @typedef {{ * issuer: | * TokenKeyword | 'Invitation' | 'Attestation' | 'AUSD', - * installation: | - * 'centralSupply' | 'mintHolder' | - * 'walletFactory' | 'provisionPool' | - * 'feeDistributor' | - * 'contractGovernor' | 'committee' | 'noActionElectorate' | 'binaryVoteCounter' | - * 'amm' | 'VaultFactory' | 'liquidate' | 'stakeFactory' | - * 'Pegasus' | 'reserve' | 'psm' | 'econCommitteeCharter' | 'interchainPool' | 'priceAggregator', * instance: | * 'economicCommittee' | 'feeDistributor' | * 'amm' | 'ammGovernor' | 'VaultFactory' | 'VaultFactoryGovernor' | @@ -170,14 +186,8 @@ * consume: Record>, * }, * installation:{ - * produce: Record>, - * consume: Record>> & { - * contractGovernor: Promise>, - * interchainPool: Promise>, - * mintHolder: Promise>, - * psm: Promise>, - * walletFactory: Promise>, - * }, + * produce: { [K in keyof WellKnownInstallations]: Producer> }, + * consume: { [K in keyof WellKnownInstallations]: Promise> }, * }, * instance:{ * produce: Record>, From dbc015983003a39d0a6dea44223fb7955258b4f4 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 9 Feb 2023 19:42:29 -0800 Subject: [PATCH 13/33] WIP feat(price): fluxAggregator governance --- packages/governance/src/contractGovernor.js | 12 + .../src/price/fluxAggregator.contract.js | 37 +- .../src/proposals/price-feed-proposal.js | 91 +- .../price/test-fluxAggregator.contract.js | 794 ++++++++++++++++++ .../smartWallet/test-oracle-integration.js | 30 +- 5 files changed, 922 insertions(+), 42 deletions(-) create mode 100644 packages/inter-protocol/test/price/test-fluxAggregator.contract.js diff --git a/packages/governance/src/contractGovernor.js b/packages/governance/src/contractGovernor.js index fb0868055d7..188e2021db1 100644 --- a/packages/governance/src/contractGovernor.js +++ b/packages/governance/src/contractGovernor.js @@ -2,6 +2,7 @@ import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; import { mustMatch } from '@agoric/store'; +import { makeTracer } from '@agoric/internal'; import { CONTRACT_ELECTORATE, setupParamGovernance, @@ -12,6 +13,8 @@ import { ParamChangesQuestionDetailsShape } from './typeGuards.js'; const { Fail } = assert; +const trace = makeTracer('CGov'); + /** * Validate that the question details correspond to a parameter change question * that the electorate hosts, and that the voteCounter and other details are @@ -125,6 +128,8 @@ const validateQuestionFromCounter = async (zoe, electorate, voteCounter) => { */ /** + * Start an instance of a governor, governing a "governed" contract specified in terms. + * * @template {GovernableStartFn} SF Start function of governed contract * @param {ZCF<{ * timer: ERef, @@ -139,7 +144,9 @@ const validateQuestionFromCounter = async (zoe, electorate, voteCounter) => { * }} privateArgs */ const start = async (zcf, privateArgs) => { + trace('start'); const zoe = zcf.getZoeService(); + trace('getTerms', zcf.getTerms()); const { timer, governedContractInstallation, @@ -148,6 +155,7 @@ const start = async (zcf, privateArgs) => { terms: contractTerms, }, } = zcf.getTerms(); + trace('contractTerms', contractTerms); contractTerms.governedParams[CONTRACT_ELECTORATE] || Fail`Contract must declare ${CONTRACT_ELECTORATE} as a governed parameter`; @@ -156,6 +164,7 @@ const start = async (zcf, privateArgs) => { electionManager: zcf.getInstance(), }); + trace('starting governedContractInstallation'); const { creatorFacet: governedCF, instance: governedInstance, @@ -195,9 +204,11 @@ const start = async (zcf, privateArgs) => { } return poserFacet; }; + trace('awaiting getUpdatedPoserFacet'); await getUpdatedPoserFacet(); assert(poserFacet, 'question poser facet must be initialized'); + trace('awaiting setupParamGovernance'); // All governed contracts have at least a governed electorate const { voteOnParamChanges, createdQuestion: createdParamQuestion } = await setupParamGovernance( @@ -208,6 +219,7 @@ const start = async (zcf, privateArgs) => { getUpdatedPoserFacet, ); + trace('awaiting setupFilterGovernance'); const { voteOnFilter, createdFilterQuestion } = await setupFilterGovernance( zoe, governedInstance, diff --git a/packages/inter-protocol/src/price/fluxAggregator.contract.js b/packages/inter-protocol/src/price/fluxAggregator.contract.js index 52a73a0132c..f7183fe991e 100644 --- a/packages/inter-protocol/src/price/fluxAggregator.contract.js +++ b/packages/inter-protocol/src/price/fluxAggregator.contract.js @@ -1,8 +1,10 @@ import { AssetKind, makeIssuerKit } from '@agoric/ertp'; -import { assertAllDefined } from '@agoric/internal'; +import { handleParamGovernance } from '@agoric/governance'; +import { assertAllDefined, makeTracer } from '@agoric/internal'; import { E } from '@endo/eventual-send'; import { provideFluxAggregator } from './fluxAggregator.js'; +const trace = makeTracer('FluxAgg'); /** * @typedef {import('@agoric/vat-data').Baggage} Baggage * @typedef {import('@agoric/time/src/types').TimerService} TimerService @@ -20,6 +22,7 @@ import { provideFluxAggregator } from './fluxAggregator.js'; * unitAmountIn?: Amount<'nat'>, * }>} zcf * @param {{ + * initialPoserInvitation: Invitation, * marshaller: Marshaller, * quoteMint?: ERef>, * storageNode: ERef, @@ -27,6 +30,7 @@ import { provideFluxAggregator } from './fluxAggregator.js'; * @param {Baggage} baggage */ export const start = async (zcf, privateArgs, baggage) => { + trace('start'); const { timer: timerP } = zcf.getTerms(); const quoteMintP = @@ -40,12 +44,18 @@ export const start = async (zcf, privateArgs, baggage) => { mint: quoteMint, }; - const { marshaller, storageNode: storageNodeP } = privateArgs; - assertAllDefined({ marshaller, storageNodeP }); + const { + initialPoserInvitation, + marshaller, + storageNode: storageNodeP, + } = privateArgs; + assertAllDefined({ initialPoserInvitation, marshaller, storageNodeP }); const timer = await timerP; const storageNode = await storageNodeP; + trace('awaited args'); + const fa = provideFluxAggregator( baggage, zcf, @@ -54,9 +64,28 @@ export const start = async (zcf, privateArgs, baggage) => { storageNode, marshaller, ); + trace('got fa', fa); + + const { makeGovernorFacet } = await handleParamGovernance( + // @ts-expect-error FIXME include Governance params + zcf, + initialPoserInvitation, + { + // No governed parameters. Governance just for API methods. + }, + storageNode, + marshaller, + ); + + trace('got param governance'); + + const governedApis = { + initOracle: fa.creatorFacet.initOracle, + }; + const governorFacet = makeGovernorFacet(fa.creatorFacet, governedApis); return harden({ - creatorFacet: fa.creatorFacet, + creatorFacet: governorFacet, publicFacet: fa.publicFacet, }); }; diff --git a/packages/inter-protocol/src/proposals/price-feed-proposal.js b/packages/inter-protocol/src/proposals/price-feed-proposal.js index a44df843576..c3be5191165 100644 --- a/packages/inter-protocol/src/proposals/price-feed-proposal.js +++ b/packages/inter-protocol/src/proposals/price-feed-proposal.js @@ -8,6 +8,7 @@ import { import { deeplyFulfilledObject, makeTracer } from '@agoric/internal'; import { unitAmount } from '@agoric/zoe/src/contractSupport/priceQuote.js'; +import { CONTRACT_ELECTORATE, ParamTypes } from '@agoric/governance'; import { reserveThenDeposit, reserveThenGetNames } from './utils.js'; const trace = makeTracer('RunPriceFeed'); @@ -97,6 +98,7 @@ export const createPriceFeed = async ( chainStorage, chainTimerService, client, + economicCommitteeCreatorFacet, namesByAddressAdmin, priceAuthority, priceAuthorityAdmin, @@ -130,43 +132,80 @@ export const createPriceFeed = async ( * * @type {[[Brand<'nat'>, Brand<'nat'>], [Installation]]} */ - const [[brandIn, brandOut], [priceAggregator]] = await Promise.all([ - reserveThenGetNames(E(agoricNamesAdmin).lookupAdmin('oracleBrand'), [ - IN_BRAND_NAME, - OUT_BRAND_NAME, - ]), - reserveThenGetNames(E(agoricNamesAdmin).lookupAdmin('installation'), [ - 'priceAggregator', - ]), - ]); + const [[brandIn, brandOut], [contractGovernor, priceAggregator]] = + await Promise.all([ + reserveThenGetNames(E(agoricNamesAdmin).lookupAdmin('oracleBrand'), [ + IN_BRAND_NAME, + OUT_BRAND_NAME, + ]), + reserveThenGetNames(E(agoricNamesAdmin).lookupAdmin('installation'), [ + 'contractGovernor', + 'priceAggregator', + ]), + ]); + + trace('getPoserInvitation'); + const poserInvitationP = E( + economicCommitteeCreatorFacet, + ).getPoserInvitation(); + const [initialPoserInvitation, electorateInvitationAmount] = + await Promise.all([ + poserInvitationP, + E(E(zoe).getInvitationIssuer()).getAmountOf(poserInvitationP), + ]); + trace('got initialPoserInvitation'); const unitAmountIn = await unitAmount(brandIn); - const terms = await deeplyFulfilledObject( + const terms = harden({ + ...contractTerms, + description: AGORIC_INSTANCE_NAME, + brandIn, + brandOut, + timer, + unitAmountIn, + governedParams: { + [CONTRACT_ELECTORATE]: { + type: ParamTypes.INVITATION, + value: electorateInvitationAmount, + }, + }, + }); + trace('got terms'); + + const governorTerms = await deeplyFulfilledObject( harden({ - ...contractTerms, - description: AGORIC_INSTANCE_NAME, - brandIn, - brandOut, - timer, - unitAmountIn, + timer: chainTimerService, + governedContractInstallation: priceAggregator, + governed: { + terms, + }, }), ); + trace('got governorTerms', governorTerms); const storageNode = await makeStorageNodeChild(chainStorage, STORAGE_PATH); const marshaller = E(board).getReadonlyMarshaller(); + trace('got contractGovernor', contractGovernor); + + trace('awaiting startInstance'); // Create the price feed. const aggregator = await E(zoe).startInstance( - priceAggregator, + /** @type {Installation} */ + (contractGovernor), undefined, - terms, + governorTerms, { - storageNode: E(storageNode).makeChildNode( - sanitizePathSegment(AGORIC_INSTANCE_NAME), - ), - marshaller, + governed: { + initialPoserInvitation, + marshaller, + storageNode: E(storageNode).makeChildNode( + sanitizePathSegment(AGORIC_INSTANCE_NAME), + ), + }, }, ); + trace('got aggregator'); await E(aggregators).set(terms, { aggregator }); E(E(agoricNamesAdmin).lookupAdmin('instance')).update( @@ -191,9 +230,9 @@ export const createPriceFeed = async ( * @param {string} addr */ const addOracle = async addr => { - const invitation = await E(aggregator.creatorFacet).makeOracleInvitation( - addr, - ); + // FIXME different facet that peeks under governance + const aggregatorFacet = E(aggregator.creatorFacet).getCreatorFacet(); + const invitation = await E(aggregatorFacet).makeOracleInvitation(addr); await reserveThenDeposit( `${AGORIC_INSTANCE_NAME} member ${addr}`, namesByAddressAdmin, @@ -228,6 +267,8 @@ export const getManifestForPriceFeed = async ( chainStorage: t, chainTimerService: t, client: t, + contractGovernor: t, + economicCommitteeCreatorFacet: t, namesByAddressAdmin: t, priceAuthority: t, priceAuthorityAdmin: t, diff --git a/packages/inter-protocol/test/price/test-fluxAggregator.contract.js b/packages/inter-protocol/test/price/test-fluxAggregator.contract.js new file mode 100644 index 00000000000..da6efc6b5c3 --- /dev/null +++ b/packages/inter-protocol/test/price/test-fluxAggregator.contract.js @@ -0,0 +1,794 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { test as unknownTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import path from 'path'; + +import bundleSource from '@endo/bundle-source'; + +import { E } from '@endo/eventual-send'; +import { Far } from '@endo/marshal'; +import { makeIssuerKit, AssetKind } from '@agoric/ertp'; +import { setUpGovernedContract } from '@agoric/governance/tools/puppetGovernance.js'; + +import { + eventLoopIteration, + makeFakeMarshaller, +} from '@agoric/notifier/tools/testSupports.js'; +import { makeMockChainStorageRoot } from '@agoric/internal/src/storage-test-utils.js'; +import { subscribeEach } from '@agoric/notifier'; +import { makeFakeVatAdmin } from '@agoric/zoe/tools/fakeVatAdmin.js'; +import { makeZoeKit } from '@agoric/zoe/src/zoeService/zoe.js'; +import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; +import { topicPath } from '../supports.js'; + +/** @type {import('ava').TestFn>>} */ +const test = unknownTest; + +const filename = new URL(import.meta.url).pathname; +const dirname = path.dirname(filename); + +// Pack the contracts. +/** @type {EndoZipBase64Bundle} */ +const aggregatorBundle = await bundleSource( + `${dirname}/../src/price/fluxAggregator.contract.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. + const { admin, vatAdminState } = makeFakeVatAdmin(); + const { zoeService: zoe } = makeZoeKit(admin); + + // 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-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 {Record} config + */ + 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(); + const storageNode = E(mockStorageRoot).makeChildNode('priceAggregator'); + + const timer = buildManualTimer(() => {}); + + const { governorFacets } = await setUpGovernedContract( + zoe, + aggregatorInstallation, + timer, + { + timer, + brandIn: link.brand, + brandOut: usd.brand, + maxSubmissionCount, + minSubmissionCount, + restartDelay, + timeout, + minSubmissionValue, + maxSubmissionValue, + governedApis: ['initOracle'], + }, + { + marshaller, + storageNode: E(storageNode).makeChildNode('LINK-USD_price_feed'), + }, + ); + + return { + governor: governorFacets.creatorFacet, + public: governorFacets.publicFacet, + instance: governorFacets.instance, + mockStorageRoot, + }; + } + + return { makeChainlinkAggregator, zoe }; +}; + +test.before('setup aggregator and oracles', async t => { + t.context = await makeContext(); +}); + +test('basic', async t => { + 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); + + /** @type {import('../../src/price/priceOracleAdmin.js').OracleAdmin} */ + // @ts-expect-error cast + const pricePushAdminA = await E( + aggregator.governor.invokeAPI('initOracle', ['agorice1priceOracleA']), + ); + /** @type {import('../../src/price/priceOracleAdmin.js').OracleAdmin} */ + // @ts-expect-error cast + const pricePushAdminB = await E( + aggregator.governor.invokeAPI('initOracle', ['agorice1priceOracleB']), + ); + /** @type {import('../../src/price/priceOracleAdmin.js').OracleAdmin} */ + // @ts-expect-error cast + const pricePushAdminC = await E( + aggregator.governor.invokeAPI('initOracle', ['agorice1priceOracleC']), + ); + + // ----- round 1: basic consensus + await oracleTimer.tick(); + await E(pricePushAdminA).pushPrice({ roundId: 1, unitPrice: 100n }); + await E(pricePushAdminB).pushPrice({ roundId: 1, unitPrice: 200n }); + await E(pricePushAdminC).pushPrice({ roundId: 1, unitPrice: 300n }); + await oracleTimer.tick(); + + const round1Attempt1 = await E(aggregator.creatorFacet).getRoundData(1); + t.is(round1Attempt1.roundId, 1n); + t.is(round1Attempt1.answer, 200n); + + // ----- round 2: check restartDelay implementation + // since oracle A initialized the last round, it CANNOT start another round before + // 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 t.throwsAsync( + E(pricePushAdminA).pushPrice({ roundId: 2, unitPrice: 1000n }), + { message: 'round not accepting submissions' }, + ); + await E(pricePushAdminB).pushPrice({ roundId: 2, unitPrice: 2000n }); + await E(pricePushAdminC).pushPrice({ roundId: 2, unitPrice: 3000n }); + await oracleTimer.tick(); + + const round1Attempt2 = await E(aggregator.creatorFacet).getRoundData(1); + t.is(round1Attempt2.answer, 200n); + const round2Attempt1 = await E(aggregator.creatorFacet).getRoundData(2); + t.is(round2Attempt1.answer, 2500n); + + // ----- round 3: check oracle submission order + // 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).pushPrice({ roundId: 3, unitPrice: 5000n }); + await E(pricePushAdminA).pushPrice({ roundId: 3, unitPrice: 4000n }); + await E(pricePushAdminB).pushPrice({ roundId: 3, unitPrice: 6000n }); + await oracleTimer.tick(); + + const round1Attempt3 = await E(aggregator.creatorFacet).getRoundData(1); + t.is(round1Attempt3.answer, 200n); + const round3Attempt1 = await E(aggregator.creatorFacet).getRoundData(3); + t.is(round3Attempt1.answer, 5000n); +}); + +test('timeout', async t => { + const { zoe } = t.context; + + 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); + + const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( + 'agorice1priceOracleA', + ); + const pricePushAdminB = await E(aggregator.creatorFacet).initOracle( + 'agorice1priceOracleB', + ); + const pricePushAdminC = await E(aggregator.creatorFacet).initOracle( + 'agorice1priceOracleC', + ); + + // ----- round 1: basic consensus w/ ticking: should work EXACTLY the same + await oracleTimer.tick(); + await E(pricePushAdminA).pushPrice({ roundId: 1, unitPrice: 100n }); + await oracleTimer.tick(); + await E(pricePushAdminB).pushPrice({ roundId: 1, unitPrice: 200n }); + await oracleTimer.tick(); + await E(pricePushAdminC).pushPrice({ roundId: 1, unitPrice: 300n }); + + const round1Attempt1 = await E(aggregator.creatorFacet).getRoundData(1); + t.is(round1Attempt1.roundId, 1n); + t.is(round1Attempt1.answer, 200n); + + // ----- round 2: check restartDelay implementation + // 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).pushPrice({ 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).pushPrice({ roundId: 3, unitPrice: 1000n }); + await E(pricePushAdminA).pushPrice({ roundId: 3, unitPrice: 3000n }); + + const round1Attempt2 = await E(aggregator.creatorFacet).getRoundData(1); + t.is(round1Attempt2.answer, 200n); + const round2Attempt1 = await E(aggregator.creatorFacet).getRoundData(2); + t.is(round2Attempt1.answer, 200n); + const round3Attempt1 = await E(aggregator.creatorFacet).getRoundData(3); + t.is(round3Attempt1.answer, 2000n); +}); + +test('issue check', async t => { + const { zoe } = t.context; + + 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); + + const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( + 'agorice1priceOracleA', + ); + const pricePushAdminB = await E(aggregator.creatorFacet).initOracle( + 'agorice1priceOracleB', + ); + const pricePushAdminC = await E(aggregator.creatorFacet).initOracle( + 'agorice1priceOracleC', + ); + + // ----- round 1: ignore too low values + await oracleTimer.tick(); + await t.throwsAsync( + E(pricePushAdminA).pushPrice({ roundId: 1, unitPrice: 50n }), + { + message: 'value below minSubmissionValue 100', + }, + ); + await oracleTimer.tick(); + await E(pricePushAdminB).pushPrice({ roundId: 1, unitPrice: 200n }); + await oracleTimer.tick(); + await E(pricePushAdminC).pushPrice({ roundId: 1, unitPrice: 300n }); + + const round1Attempt1 = await E(aggregator.creatorFacet).getRoundData(1); + t.is(round1Attempt1.answer, 250n); + + // ----- round 2: ignore too high values + await oracleTimer.tick(); + await t.throwsAsync( + E(pricePushAdminB).pushPrice({ roundId: 2, unitPrice: 20000n }), + { message: 'value above maxSubmissionValue 10000' }, + ); + await E(pricePushAdminC).pushPrice({ roundId: 2, unitPrice: 1000n }); + await E(pricePushAdminA).pushPrice({ roundId: 2, unitPrice: 3000n }); + await oracleTimer.tick(); + + const round2Attempt1 = await E(aggregator.creatorFacet).getRoundData(2); + t.is(round2Attempt1.answer, 2000n); +}); + +test('supersede', async t => { + const { zoe } = t.context; + + 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); + + const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( + 'agorice1priceOracleA', + ); + const pricePushAdminB = await E(aggregator.creatorFacet).initOracle( + 'agorice1priceOracleB', + ); + const pricePushAdminC = await E(aggregator.creatorFacet).initOracle( + 'agorice1priceOracleC', + ); + + // ----- round 1: round 1 is NOT supersedable when 3 submits, meaning it will be ignored + await oracleTimer.tick(); + await E(pricePushAdminA).pushPrice({ roundId: 1, unitPrice: 100n }); + await t.throwsAsync( + E(pricePushAdminC).pushPrice({ roundId: 2, unitPrice: 300n }), + { + message: 'previous round not supersedable', + }, + ); + await E(pricePushAdminB).pushPrice({ roundId: 1, unitPrice: 200n }); + await oracleTimer.tick(); + + const round1Attempt1 = await E(aggregator.creatorFacet).getRoundData(1); + t.is(round1Attempt1.answer, 150n); + + // ----- round 2: oracle C's value from before should have been IGNORED + await oracleTimer.tick(); + await E(pricePushAdminB).pushPrice({ roundId: 2, unitPrice: 2000n }); + await E(pricePushAdminA).pushPrice({ roundId: 2, unitPrice: 1000n }); + await oracleTimer.tick(); + + const round2Attempt1 = await E(aggregator.creatorFacet).getRoundData(2); + t.is(round2Attempt1.answer, 1500n); + + // ----- round 3: oracle C should NOT be able to supersede round 3 + await oracleTimer.tick(); + await t.throwsAsync( + E(pricePushAdminC).pushPrice({ roundId: 4, unitPrice: 1000n }), + { message: 'invalid round to report' }, + ); + + try { + await E(aggregator.creatorFacet).getRoundData(4); + } catch (error) { + t.is(error.message, 'No data present'); + } +}); + +test('interleaved', async t => { + const { zoe } = t.context; + + 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); + + const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( + 'agorice1priceOracleA', + ); + const pricePushAdminB = await E(aggregator.creatorFacet).initOracle( + 'agorice1priceOracleB', + ); + const pricePushAdminC = await E(aggregator.creatorFacet).initOracle( + 'agorice1priceOracleC', + ); + + // ----- round 1: we now need unanimous submission for a round for it to have consensus + await oracleTimer.tick(); + await E(pricePushAdminA).pushPrice({ roundId: 1, unitPrice: 100n }); + await t.throwsAsync( + E(pricePushAdminC).pushPrice({ roundId: 2, unitPrice: 300n }), + { + message: 'previous round not supersedable', + }, + ); + await E(pricePushAdminB).pushPrice({ roundId: 1, unitPrice: 200n }); + await oracleTimer.tick(); + + try { + await E(aggregator.creatorFacet).getRoundData(1); + } catch (error) { + t.is(error.message, 'No data present'); + } + + // ----- round 2: interleaved round submission -- just making sure this works + await oracleTimer.tick(); + await E(pricePushAdminC).pushPrice({ roundId: 1, unitPrice: 300n }); + await oracleTimer.tick(); + await E(pricePushAdminB).pushPrice({ roundId: 2, unitPrice: 2000n }); + await E(pricePushAdminA).pushPrice({ roundId: 2, unitPrice: 1000n }); + await oracleTimer.tick(); + await t.throwsAsync( + E(pricePushAdminC).pushPrice({ roundId: 3, unitPrice: 9000n }), + { message: 'previous round not supersedable' }, + ); + await oracleTimer.tick(); + await E(pricePushAdminC).pushPrice({ roundId: 2, unitPrice: 3000n }); // assumes oracle C is going for a resubmission + await oracleTimer.tick(); + await oracleTimer.tick(); + await E(pricePushAdminA).pushPrice({ roundId: 3, unitPrice: 5000n }); + await oracleTimer.tick(); + + const round1Attempt2 = await E(aggregator.creatorFacet).getRoundData(1); + const round2Attempt1 = await E(aggregator.creatorFacet).getRoundData(2); + + t.is(round1Attempt2.answer, 200n); + t.is(round2Attempt1.answer, 2000n); + + try { + await E(aggregator.creatorFacet).getRoundData(3); + } catch (error) { + t.is(error.message, 'No data present'); + } + + // ----- round 3/4: complicated supersedable case + await oracleTimer.tick(); + await oracleTimer.tick(); + await oracleTimer.tick(); + await oracleTimer.tick(); + 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 t.throwsAsync( + E(pricePushAdminA).pushPrice({ roundId: 4, unitPrice: 4000n }), + { message: 'round not accepting submissions' }, + ); + await E(pricePushAdminB).pushPrice({ roundId: 4, unitPrice: 5000n }); + await E(pricePushAdminC).pushPrice({ roundId: 4, unitPrice: 6000n }); + await oracleTimer.tick(); // --- round 3 has NOW timed out, meaning it is now supersedable + + try { + await E(aggregator.creatorFacet).getRoundData(3); + } catch (error) { + t.is(error.message, 'No data present'); + } + + try { + await E(aggregator.creatorFacet).getRoundData(4); + } catch (error) { + t.is(error.message, 'No data present'); + } + + // so NOW we should be able to submit round 4, and round 3 should just be copied from round 2 + await E(pricePushAdminA).pushPrice({ roundId: 4, unitPrice: 4000n }); + await t.throwsAsync( + E(pricePushAdminB).pushPrice({ roundId: 4, unitPrice: 5000n }), + { message: /cannot report on previous rounds/ }, + ); + await t.throwsAsync( + E(pricePushAdminC).pushPrice({ roundId: 4, unitPrice: 6000n }), + { message: /cannot report on previous rounds/ }, + ); + await oracleTimer.tick(); + + const round3Attempt3 = await E(aggregator.creatorFacet).getRoundData(3); + const round4Attempt2 = await E(aggregator.creatorFacet).getRoundData(4); + + t.is(round3Attempt3.answer, 2000n); + t.is(round4Attempt2.answer, 5000n); + + // ----- round 5: ping-ponging should be possible (although this is an unlikely pernicious case) + await E(pricePushAdminC).pushPrice({ roundId: 5, unitPrice: 1000n }); + await oracleTimer.tick(); + await oracleTimer.tick(); + await oracleTimer.tick(); + await oracleTimer.tick(); + await oracleTimer.tick(); + await oracleTimer.tick(); + await oracleTimer.tick(); + await E(pricePushAdminA).pushPrice({ 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).pushPrice({ roundId: 7, unitPrice: 1000n }); + + const round5Attempt1 = await E(aggregator.creatorFacet).getRoundData(5); + const round6Attempt1 = await E(aggregator.creatorFacet).getRoundData(6); + + t.is(round5Attempt1.answer, 5000n); + t.is(round6Attempt1.answer, 5000n); +}); + +test('larger', async t => { + const { zoe } = t.context; + + 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); + + const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( + 'agorice1priceOracleA', + ); + const pricePushAdminB = await E(aggregator.creatorFacet).initOracle( + 'agorice1priceOracleB', + ); + const pricePushAdminC = await E(aggregator.creatorFacet).initOracle( + 'agorice1priceOracleC', + ); + const pricePushAdminD = await E(aggregator.creatorFacet).initOracle( + 'agorice1priceOracleD', + ); + const pricePushAdminE = await E(aggregator.creatorFacet).initOracle( + 'agorice1priceOracleE', + ); + + // ----- round 1: usual case + await oracleTimer.tick(); + await E(pricePushAdminA).pushPrice({ roundId: 1, unitPrice: 100n }); + await E(pricePushAdminB).pushPrice({ roundId: 1, unitPrice: 200n }); + await oracleTimer.tick(); + await oracleTimer.tick(); + await t.throwsAsync( + E(pricePushAdminC).pushPrice({ roundId: 2, unitPrice: 1000n }), + { message: 'previous round not supersedable' }, + ); + await oracleTimer.tick(); + await t.throwsAsync( + E(pricePushAdminD).pushPrice({ roundId: 3, unitPrice: 3000n }), + { message: 'invalid round to report' }, + ); + await oracleTimer.tick(); + await oracleTimer.tick(); + await oracleTimer.tick(); + await E(pricePushAdminE).pushPrice({ roundId: 1, unitPrice: 300n }); + + const round1Attempt1 = await E(aggregator.creatorFacet).getRoundData(1); + t.is(round1Attempt1.answer, 200n); + + // ----- round 2: ignore late arrival + await oracleTimer.tick(); + await E(pricePushAdminB).pushPrice({ roundId: 2, unitPrice: 600n }); + await oracleTimer.tick(); + await E(pricePushAdminA).pushPrice({ roundId: 2, unitPrice: 500n }); + await oracleTimer.tick(); + await t.throwsAsync( + E(pricePushAdminC).pushPrice({ roundId: 3, unitPrice: 1000n }), + { message: 'previous round not supersedable' }, + ); + await oracleTimer.tick(); + await E(pricePushAdminD).pushPrice({ roundId: 1, unitPrice: 500n }); + await oracleTimer.tick(); + await oracleTimer.tick(); + await oracleTimer.tick(); + await E(pricePushAdminC).pushPrice({ roundId: 2, unitPrice: 1000n }); + await oracleTimer.tick(); + await t.throwsAsync( + E(pricePushAdminC).pushPrice({ roundId: 1, unitPrice: 700n }), + // oracle C has already sent round 2 + { message: 'cannot report on previous rounds' }, + ); + + const round1Attempt2 = await E(aggregator.creatorFacet).getRoundData(1); + const round2Attempt1 = await E(aggregator.creatorFacet).getRoundData(2); + t.is(round1Attempt2.answer, 250n); + t.is(round2Attempt1.answer, 600n); +}); + +test('suggest', async t => { + const { zoe } = t.context; + + 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); + + const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( + 'agorice1priceOracleA', + ); + const pricePushAdminB = await E(aggregator.creatorFacet).initOracle( + 'agorice1priceOracleB', + ); + const pricePushAdminC = await E(aggregator.creatorFacet).initOracle( + 'agorice1priceOracleC', + ); + + // ----- round 1: basic consensus + await oracleTimer.tick(); + await E(pricePushAdminA).pushPrice({ roundId: 1, unitPrice: 100n }); + await E(pricePushAdminB).pushPrice({ roundId: 1, unitPrice: 200n }); + await E(pricePushAdminC).pushPrice({ roundId: 1, unitPrice: 300n }); + await oracleTimer.tick(); + + const round1Attempt1 = await E(aggregator.creatorFacet).getRoundData(1); + t.is(round1Attempt1.roundId, 1n); + t.is(round1Attempt1.answer, 200n); + + // ----- round 2: add a new oracle and confirm the suggested round is correct + await oracleTimer.tick(); + await E(pricePushAdminB).pushPrice({ roundId: 2, unitPrice: 1000n }); + + t.deepEqual( + await E(aggregator.creatorFacet).oracleRoundState( + 'agorice1priceOracleC', + 1n, + ), + { + eligibleForSpecificRound: false, + oracleCount: 3, + latestSubmission: 300n, + queriedRoundId: 1n, + roundTimeout: 5, + startedAt: 1n, + }, + ); + + t.deepEqual( + await E(aggregator.creatorFacet).oracleRoundState( + 'agorice1priceOracleB', + 0n, + ), + { + eligibleForSpecificRound: false, + oracleCount: 3, + latestSubmission: 1000n, + queriedRoundId: 2n, + roundTimeout: 5, + startedAt: 3n, + }, + ); + + await oracleTimer.tick(); + await E(pricePushAdminA).pushPrice({ roundId: 2, unitPrice: 2000n }); + await oracleTimer.tick(); + await oracleTimer.tick(); + await E(pricePushAdminC).pushPrice({ roundId: 2, unitPrice: 3000n }); + + t.deepEqual( + await E(aggregator.creatorFacet).oracleRoundState( + 'agorice1priceOracleA', + 0n, + ), + { + eligibleForSpecificRound: true, + oracleCount: 3, + latestSubmission: 2000n, + queriedRoundId: 3n, + roundTimeout: 0, + startedAt: 0n, // round 3 hasn't yet started, so it should be zeroed + }, + ); + + // ----- round 3: try using suggested round + await E(pricePushAdminC).pushPrice({ roundId: 3, unitPrice: 100n }); + await oracleTimer.tick(); + await E(pricePushAdminA).pushPrice({ roundId: undefined, unitPrice: 200n }); + await oracleTimer.tick(); + await oracleTimer.tick(); + await E(pricePushAdminB).pushPrice({ roundId: undefined, unitPrice: 300n }); + + const round3Attempt1 = await E(aggregator.creatorFacet).getRoundData(3); + t.is(round3Attempt1.roundId, 3n); + t.is(round3Attempt1.answer, 200n); +}); + +test('notifications', async t => { + const { zoe } = t.context; + + 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); + + const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( + 'agorice1priceOracleA', + ); + const pricePushAdminB = await E(aggregator.creatorFacet).initOracle( + 'agorice1priceOracleB', + ); + + const latestRoundSubscriber = await E( + aggregator.publicFacet, + ).getRoundStartNotifier(); + const eachLatestRound = subscribeEach(latestRoundSubscriber)[ + Symbol.asyncIterator + ](); + + await oracleTimer.tick(); + await E(pricePushAdminA).pushPrice({ roundId: 1, unitPrice: 100n }); + t.deepEqual((await eachLatestRound.next()).value, { + roundId: 1n, + startedAt: 1n, + }); + await E(pricePushAdminB).pushPrice({ roundId: 1, unitPrice: 200n }); + + 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 t.throwsAsync( + E(pricePushAdminA).pushPrice({ roundId: 2, unitPrice: 1000n }), + { message: 'round not accepting submissions' }, + ); + // A started last round so fails to start next round + 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).pushPrice({ 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).pushPrice({ roundId: 2, unitPrice: 1000n }); + // writes to storage + t.deepEqual( + aggregator.mockStorageRoot.getBody( + 'mockChainStorageRoot.priceAggregator.LINK-USD_price_feed.latestRound', + ), + { roundId: 2n, startedAt: 1n }, + ); + + 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: 1000n, // AVG(1000, 1000) + }, + timer: { iface: 'Alleged: ManualTimer' }, + timestamp: 1n, + }, + ); + + // A can start again + await E(pricePushAdminA).pushPrice({ roundId: 3, unitPrice: 1000n }); + t.deepEqual((await eachLatestRound.next()).value, { + roundId: 3n, + startedAt: 1n, + }); + // no new price yet publishable +}); + +test('storage keys', async t => { + const { publicFacet } = await t.context.makeChainlinkAggregator( + defaultConfig, + ); + + t.is( + await topicPath(publicFacet, 'quotes'), + 'mockChainStorageRoot.priceAggregator.LINK-USD_price_feed', + ); +}); diff --git a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js index 706e7df76f4..d2defd54b44 100644 --- a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js +++ b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js @@ -89,12 +89,12 @@ const setupFeedWithWallets = async (t, oracleAddresses) => { await t.context.simpleCreatePriceFeed(oracleAddresses, 'ATOM', 'USD'); /** @type {import('@agoric/zoe/src/zoeService/utils.js').Instance} */ - const priceAggregator = await E(agoricNames).lookup( + const governedPriceAggregator = await E(agoricNames).lookup( 'instance', 'ATOM-USD price feed', ); - return { oracleWallets, priceAggregator }; + return { oracleWallets, governedPriceAggregator }; }; let acceptInvitationCounter = 0; @@ -153,7 +153,9 @@ test.serial('invitations', async t => { // this returns wallets, but we need the updates subscriber to start before the price feed starts // so we provision the wallet earlier above - const { priceAggregator } = await setupFeedWithWallets(t, [operatorAddress]); + const { governedPriceAggregator } = await setupFeedWithWallets(t, [ + operatorAddress, + ]); /** * get invitation details the way a user would @@ -182,7 +184,7 @@ test.serial('invitations', async t => { t.is(proposeInvitationDetails[0].description, INVITATION_MAKERS_DESC); t.is( proposeInvitationDetails[0].instance, - priceAggregator, + governedPriceAggregator, 'priceAggregator', ); @@ -191,7 +193,7 @@ test.serial('invitations', async t => { /** @type {import('@agoric/smart-wallet/src/invitations.js').PurseInvitationSpec} */ const getInvMakersSpec = { source: 'purse', - instance: priceAggregator, + instance: governedPriceAggregator, description: INVITATION_MAKERS_DESC, }; @@ -218,11 +220,12 @@ test.serial('admin price', async t => { const operatorAddress = 'adminPriceAddress'; const { zoe } = t.context.consume; - const { oracleWallets, priceAggregator } = await setupFeedWithWallets(t, [ - operatorAddress, - ]); + const { oracleWallets, governedPriceAggregator } = await setupFeedWithWallets( + t, + [operatorAddress], + ); const wallet = oracleWallets[operatorAddress]; - const adminOfferId = await acceptInvitation(wallet, priceAggregator); + const adminOfferId = await acceptInvitation(wallet, governedPriceAggregator); // Push a new price result ///////////////////////// @@ -239,7 +242,9 @@ test.serial('admin price', async t => { // trigger an aggregation (POLL_INTERVAL=1n in context) await E(manualTimer).tickN(1); - const paPublicFacet = await E(zoe).getPublicFacet(priceAggregator); + const paPublicFacet = await E( + E(zoe).getPublicFacet(governedPriceAggregator), + ).getGovernedContract(); const latestRoundSubscriber = await E(paPublicFacet).getRoundStartNotifier(); @@ -252,9 +257,8 @@ test.serial('admin price', async t => { test.serial('errors', async t => { const operatorAddress = 'badInputsAddress'; - const { oracleWallets, priceAggregator } = await setupFeedWithWallets(t, [ - operatorAddress, - ]); + const { oracleWallets, governedPriceAggregator: priceAggregator } = + await setupFeedWithWallets(t, [operatorAddress]); const wallet = oracleWallets[operatorAddress]; const adminOfferId = await acceptInvitation(wallet, priceAggregator); From dfd334a0cc424acb69d255f64596f9b4281b4462 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 9 Feb 2023 22:09:29 -0800 Subject: [PATCH 14/33] WIP: instances don't match --- packages/governance/src/contractGovernor.js | 2 ++ .../test/smartWallet/test-oracle-integration.js | 4 +--- packages/smart-wallet/src/invitations.js | 7 +++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/governance/src/contractGovernor.js b/packages/governance/src/contractGovernor.js index 188e2021db1..b1921c3c0c3 100644 --- a/packages/governance/src/contractGovernor.js +++ b/packages/governance/src/contractGovernor.js @@ -317,6 +317,8 @@ const start = async (zcf, privateArgs) => { }); const publicFacet = Far('contract governor public', { + // BEFOREMERGE is this okay? why not? + ...governedPF, getElectorate: getElectorateInstance, getGovernedContract: () => governedInstance, validateVoteCounter, diff --git a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js index d2defd54b44..e39795c6282 100644 --- a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js +++ b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js @@ -242,9 +242,7 @@ test.serial('admin price', async t => { // trigger an aggregation (POLL_INTERVAL=1n in context) await E(manualTimer).tickN(1); - const paPublicFacet = await E( - E(zoe).getPublicFacet(governedPriceAggregator), - ).getGovernedContract(); + const paPublicFacet = E(zoe).getPublicFacet(governedPriceAggregator); const latestRoundSubscriber = await E(paPublicFacet).getRoundStartNotifier(); diff --git a/packages/smart-wallet/src/invitations.js b/packages/smart-wallet/src/invitations.js index db06c1bf3a0..24a00aec8b8 100644 --- a/packages/smart-wallet/src/invitations.js +++ b/packages/smart-wallet/src/invitations.js @@ -72,9 +72,12 @@ export const makeInvitationsHelper = ( const purseAmount = await E(invitationsPurse).getCurrentAmount(); const invitations = AmountMath.getValue(invitationBrand, purseAmount); + console.log('DEBUG searching invitations', invitations, 'for', instance); + const matches = invitations.filter( - details => - description === details.description && instance === details.instance, + details => description === details.description, + // FIXME DONOTMERGE instance isn't matching + // && instance === details.instance, ); if (matches.length === 0) { // look up diagnostic info From dcafc14b3153aaa6ce6490b41fde4e517f59a211 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 10:04:50 -0800 Subject: [PATCH 15/33] chore(types): fixup governance --- packages/governance/src/contractGovernor.js | 6 +++--- packages/governance/src/types-ambient.js | 4 ++-- packages/inter-protocol/src/proposals/econ-behaviors.js | 1 + packages/inter-protocol/test/psm/test-governedPsm.js | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/governance/src/contractGovernor.js b/packages/governance/src/contractGovernor.js index b1921c3c0c3..c83136a9d52 100644 --- a/packages/governance/src/contractGovernor.js +++ b/packages/governance/src/contractGovernor.js @@ -118,13 +118,13 @@ const validateQuestionFromCounter = async (zoe, electorate, voteCounter) => { * governedContractInstallation: Installation, * governed: { * issuerKeywordRecord: IssuerKeywordRecord, - * terms: {governedParams: {[CONTRACT_ELECTORATE]: Amount<'set'>}}, + * terms: {governedParams: {[CONTRACT_ELECTORATE]: InvitationParam}}, * } * }>} */ /** - * @typedef {() => {creatorFacet: GovernorFacet, publicFacet: GovernedPublicFacetMethods}} GovernableStartFn + * @typedef {(zcf?: any, privateArgs?: any, baggage?: any) => import('type-fest').Promisable<{creatorFacet: GovernorFacet, publicFacet: GovernedPublicFacetMethods}>} GovernableStartFn */ /** @@ -136,7 +136,7 @@ const validateQuestionFromCounter = async (zoe, electorate, voteCounter) => { * governedContractInstallation: Installation, * governed: { * issuerKeywordRecord: IssuerKeywordRecord, - * terms: {governedParams: {[CONTRACT_ELECTORATE]: Amount<'set'>}}, + * terms: {governedParams: {[CONTRACT_ELECTORATE]: import('./contractGovernance/typedParamManager.js').InvitationParam}}, * } * }>} zcf * @param {{ diff --git a/packages/governance/src/types-ambient.js b/packages/governance/src/types-ambient.js index 8a5755333ab..baa2c427463 100644 --- a/packages/governance/src/types-ambient.js +++ b/packages/governance/src/types-ambient.js @@ -569,11 +569,11 @@ * @property {VoteOnParamChanges} voteOnParamChanges * @property {VoteOnApiInvocation} voteOnApiInvocation * @property {VoteOnOfferFilter} voteOnOfferFilter - * @property {() => Promise>} getCreatorFacet - creator + * @property {() => ERef>} getCreatorFacet - creator * facet of the governed contract, without the tightly held ability to change * param values. * @property {(poserInvitation: Invitation) => Promise} replaceElectorate - * @property {() => Promise} getAdminFacet + * @property {() => ERef} getAdminFacet * @property {() => GovernedPublicFacet} getPublicFacet - public facet of the governed contract * @property {() => Instance} getInstance - instance of the governed * contract diff --git a/packages/inter-protocol/src/proposals/econ-behaviors.js b/packages/inter-protocol/src/proposals/econ-behaviors.js index ffff2901fd0..40c1f36f2b7 100644 --- a/packages/inter-protocol/src/proposals/econ-behaviors.js +++ b/packages/inter-protocol/src/proposals/econ-behaviors.js @@ -597,6 +597,7 @@ export const startRewardDistributor = async ({ E(instanceKit.creatorFacet).makeDepositFacetDestination( rewardDistributorDepositFacet, ), + // @ts-expect-error FIXME looks like legit uncovered bug Reserve: E(instanceKit.creatorFacet).makeOfferDestination( 'Collateral', E.get(reserveKit).publicFacet, diff --git a/packages/inter-protocol/test/psm/test-governedPsm.js b/packages/inter-protocol/test/psm/test-governedPsm.js index bb3d5143819..45ad43a6858 100644 --- a/packages/inter-protocol/test/psm/test-governedPsm.js +++ b/packages/inter-protocol/test/psm/test-governedPsm.js @@ -178,7 +178,7 @@ test('replace electorate of Economic Committee', async t => { harden({}), electorateTerms, { - // mocks + // @ts-expect-error mock marshaller: {}, storageNode: makeFakeStorageKit('governedPsmTest').rootNode, }, From 9931e4f9a569991d88280af2a35d7e18150981e4 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 11:07:23 -0800 Subject: [PATCH 16/33] refactor: WellKnownInstallations --- packages/vats/src/core/types.js | 26 ++------------ packages/vats/src/core/wellKnown.d.ts | 51 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 23 deletions(-) create mode 100644 packages/vats/src/core/wellKnown.d.ts diff --git a/packages/vats/src/core/types.js b/packages/vats/src/core/types.js index 06131df2423..43a703b36ac 100644 --- a/packages/vats/src/core/types.js +++ b/packages/vats/src/core/types.js @@ -130,27 +130,7 @@ */ /** - * @typedef {{ - * amm: import('@agoric/inter-protocol/src/vpool-xyk-amm/multipoolMarketMaker.js').start, - * binaryVoteCounter: import('@agoric/governance/src/binaryVoteCounter.js').start, - * centralSupply: import('@agoric/vats/src/centralSupply.js').start, - * committee: import('@agoric/governance/src/committee.js').start, - * contractGovernor: import('@agoric/governance/src/contractGovernor.js').start, - * econCommitteeCharter: import('@agoric/inter-protocol/src/econCommitteeCharter.js').start, - * feeDistributor: import('@agoric/inter-protocol/src/feeDistributor.js').start, - * interchainPool: import('@agoric/inter-protocol/src/interchainPool.js').start, - * liquidate: import('@agoric/inter-protocol/src/vaultFactory/liquidateIncrementally.js').start, - * mintHolder: import('@agoric/vats/src/mintHolder.js').prepare, - * noActionElectorate: unknown, - * Pegasus: unknown, - * psm: import('@agoric/inter-protocol/src/psm/psm.js').start, - * priceAggregator: import('@agoric/inter-protocol/src/price/fluxAggregator.contract.js').start, - * provisionPool: import('@agoric/vats/src/provisionPool.js').start, - * reserve: import('@agoric/inter-protocol/src/reserve/assetReserve.js').start, - * stakeFactory: import('@agoric/inter-protocol/src/stakeFactory/stakeFactory.js').start, - * walletFactory: import('@agoric/smart-wallet/src/walletFactory.js').start, - * VaultFactory: import('@agoric/inter-protocol/src/vaultFactory/vaultFactory.js').start, - * }} WellKnownInstallations */ + * @typedef {import('./wellKnown').WellKnownInstallations} WellKnownInstallations */ /** * @typedef {import('../tokens.js').TokenKeyword} TokenKeyword @@ -186,8 +166,8 @@ * consume: Record>, * }, * installation:{ - * produce: { [K in keyof WellKnownInstallations]: Producer> }, - * consume: { [K in keyof WellKnownInstallations]: Promise> }, + * produce: { [K in keyof WellKnownInstallations]: Producer }, + * consume: { [K in keyof WellKnownInstallations]: Promise }, * }, * instance:{ * produce: Record>, diff --git a/packages/vats/src/core/wellKnown.d.ts b/packages/vats/src/core/wellKnown.d.ts new file mode 100644 index 00000000000..2bb5464271a --- /dev/null +++ b/packages/vats/src/core/wellKnown.d.ts @@ -0,0 +1,51 @@ +/** + * @file Type exports for WellKnown names. TS so it doesn't affect runtime dependency graph. + */ +// 'start' fns +import type { start as amm } from '@agoric/inter-protocol/src/vpool-xyk-amm/multipoolMarketMaker.js'; +import type { start as binaryVoteCounter } from '@agoric/governance/src/binaryVoteCounter.js'; +import type { start as committee } from '@agoric/governance/src/committee.js'; +import type { start as contractGovernor } from '@agoric/governance/src/contractGovernor.js'; +import type { start as econCommitteeCharter } from '@agoric/inter-protocol/src/econCommitteeCharter.js'; +import type { start as feeDistributor } from '@agoric/inter-protocol/src/feeDistributor.js'; +import type { start as interchainPool } from '@agoric/inter-protocol/src/interchainPool.js'; +import type { start as liquidate } from '@agoric/inter-protocol/src/vaultFactory/liquidateIncrementally.js'; +import type { start as noActionElectorate } from '@agoric/governance/src/noActionElectorate.js'; +import type { start as priceAggregator } from '@agoric/inter-protocol/src/price/fluxAggregator.contract.js'; +import type { start as psm } from '@agoric/inter-protocol/src/psm/psm.js'; +import type { start as reserve } from '@agoric/inter-protocol/src/reserve/assetReserve.js'; +import type { start as stakeFactory } from '@agoric/inter-protocol/src/stakeFactory/stakeFactory.js'; +import type { start as VaultFactory } from '@agoric/inter-protocol/src/vaultFactory/vaultFactory.js'; +import type { start as walletFactory } from '@agoric/smart-wallet/src/walletFactory.js'; +import type { start as Pegasus } from '@agoric/pegasus/src/pegasus.js'; +import type { start as provisionPool } from '../provisionPool.js'; +import type { start as centralSupply } from '../centralSupply.js'; + +// 'prepare' fns +import type { prepare as mintHolder } from '../mintHolder.js'; + +const contractFns = { + amm, + binaryVoteCounter, + centralSupply, + committee, + contractGovernor, + econCommitteeCharter, + feeDistributor, + interchainPool, + liquidate, + mintHolder, + noActionElectorate, + Pegasus, + priceAggregator, + provisionPool, + psm, + reserve, + stakeFactory, + VaultFactory, + walletFactory, +}; + +export type WellKnownInstallations = { + [K in keyof typeof contractFns]: Installation<(typeof contractFns)[K]>; +}; From 50b38a8763cbc34fafa079b244883596b888bb64 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 11:34:33 -0800 Subject: [PATCH 17/33] Revert "refactor: WellKnownInstallations" This reverts commit 24f23f01aae3c589b62950370ae52165dc0f8eb4. --- packages/vats/src/core/types.js | 26 ++++++++++++-- packages/vats/src/core/wellKnown.d.ts | 51 --------------------------- 2 files changed, 23 insertions(+), 54 deletions(-) delete mode 100644 packages/vats/src/core/wellKnown.d.ts diff --git a/packages/vats/src/core/types.js b/packages/vats/src/core/types.js index 43a703b36ac..06131df2423 100644 --- a/packages/vats/src/core/types.js +++ b/packages/vats/src/core/types.js @@ -130,7 +130,27 @@ */ /** - * @typedef {import('./wellKnown').WellKnownInstallations} WellKnownInstallations */ + * @typedef {{ + * amm: import('@agoric/inter-protocol/src/vpool-xyk-amm/multipoolMarketMaker.js').start, + * binaryVoteCounter: import('@agoric/governance/src/binaryVoteCounter.js').start, + * centralSupply: import('@agoric/vats/src/centralSupply.js').start, + * committee: import('@agoric/governance/src/committee.js').start, + * contractGovernor: import('@agoric/governance/src/contractGovernor.js').start, + * econCommitteeCharter: import('@agoric/inter-protocol/src/econCommitteeCharter.js').start, + * feeDistributor: import('@agoric/inter-protocol/src/feeDistributor.js').start, + * interchainPool: import('@agoric/inter-protocol/src/interchainPool.js').start, + * liquidate: import('@agoric/inter-protocol/src/vaultFactory/liquidateIncrementally.js').start, + * mintHolder: import('@agoric/vats/src/mintHolder.js').prepare, + * noActionElectorate: unknown, + * Pegasus: unknown, + * psm: import('@agoric/inter-protocol/src/psm/psm.js').start, + * priceAggregator: import('@agoric/inter-protocol/src/price/fluxAggregator.contract.js').start, + * provisionPool: import('@agoric/vats/src/provisionPool.js').start, + * reserve: import('@agoric/inter-protocol/src/reserve/assetReserve.js').start, + * stakeFactory: import('@agoric/inter-protocol/src/stakeFactory/stakeFactory.js').start, + * walletFactory: import('@agoric/smart-wallet/src/walletFactory.js').start, + * VaultFactory: import('@agoric/inter-protocol/src/vaultFactory/vaultFactory.js').start, + * }} WellKnownInstallations */ /** * @typedef {import('../tokens.js').TokenKeyword} TokenKeyword @@ -166,8 +186,8 @@ * consume: Record>, * }, * installation:{ - * produce: { [K in keyof WellKnownInstallations]: Producer }, - * consume: { [K in keyof WellKnownInstallations]: Promise }, + * produce: { [K in keyof WellKnownInstallations]: Producer> }, + * consume: { [K in keyof WellKnownInstallations]: Promise> }, * }, * instance:{ * produce: Record>, diff --git a/packages/vats/src/core/wellKnown.d.ts b/packages/vats/src/core/wellKnown.d.ts deleted file mode 100644 index 2bb5464271a..00000000000 --- a/packages/vats/src/core/wellKnown.d.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * @file Type exports for WellKnown names. TS so it doesn't affect runtime dependency graph. - */ -// 'start' fns -import type { start as amm } from '@agoric/inter-protocol/src/vpool-xyk-amm/multipoolMarketMaker.js'; -import type { start as binaryVoteCounter } from '@agoric/governance/src/binaryVoteCounter.js'; -import type { start as committee } from '@agoric/governance/src/committee.js'; -import type { start as contractGovernor } from '@agoric/governance/src/contractGovernor.js'; -import type { start as econCommitteeCharter } from '@agoric/inter-protocol/src/econCommitteeCharter.js'; -import type { start as feeDistributor } from '@agoric/inter-protocol/src/feeDistributor.js'; -import type { start as interchainPool } from '@agoric/inter-protocol/src/interchainPool.js'; -import type { start as liquidate } from '@agoric/inter-protocol/src/vaultFactory/liquidateIncrementally.js'; -import type { start as noActionElectorate } from '@agoric/governance/src/noActionElectorate.js'; -import type { start as priceAggregator } from '@agoric/inter-protocol/src/price/fluxAggregator.contract.js'; -import type { start as psm } from '@agoric/inter-protocol/src/psm/psm.js'; -import type { start as reserve } from '@agoric/inter-protocol/src/reserve/assetReserve.js'; -import type { start as stakeFactory } from '@agoric/inter-protocol/src/stakeFactory/stakeFactory.js'; -import type { start as VaultFactory } from '@agoric/inter-protocol/src/vaultFactory/vaultFactory.js'; -import type { start as walletFactory } from '@agoric/smart-wallet/src/walletFactory.js'; -import type { start as Pegasus } from '@agoric/pegasus/src/pegasus.js'; -import type { start as provisionPool } from '../provisionPool.js'; -import type { start as centralSupply } from '../centralSupply.js'; - -// 'prepare' fns -import type { prepare as mintHolder } from '../mintHolder.js'; - -const contractFns = { - amm, - binaryVoteCounter, - centralSupply, - committee, - contractGovernor, - econCommitteeCharter, - feeDistributor, - interchainPool, - liquidate, - mintHolder, - noActionElectorate, - Pegasus, - priceAggregator, - provisionPool, - psm, - reserve, - stakeFactory, - VaultFactory, - walletFactory, -}; - -export type WellKnownInstallations = { - [K in keyof typeof contractFns]: Installation<(typeof contractFns)[K]>; -}; From 8bc48704655b8f6a98d24cde1fcc401f8dd70667 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 12:28:39 -0800 Subject: [PATCH 18/33] WIP --- packages/governance/src/contractGovernor.js | 14 +++++++++++-- packages/governance/src/types-ambient.js | 21 ++++++++++--------- .../test/unitTests/test-paramGovernance.js | 8 +++++++ packages/governance/tools/puppetGovernance.js | 16 +++++++------- packages/inter-protocol/src/interchainPool.js | 1 + .../src/price/fluxAggregator.contract.js | 5 +++-- .../src/proposals/econ-behaviors.js | 9 +++++--- .../inter-protocol/src/proposals/startPSM.js | 16 +++++++------- .../src/stakeFactory/stakeFactory.js | 4 ++-- .../src/vaultFactory/vaultDirector.js | 9 +++++--- packages/inter-protocol/test/psm/setupPsm.js | 3 +-- 11 files changed, 67 insertions(+), 39 deletions(-) diff --git a/packages/governance/src/contractGovernor.js b/packages/governance/src/contractGovernor.js index c83136a9d52..11860acb4ac 100644 --- a/packages/governance/src/contractGovernor.js +++ b/packages/governance/src/contractGovernor.js @@ -126,6 +126,11 @@ const validateQuestionFromCounter = async (zoe, electorate, voteCounter) => { /** * @typedef {(zcf?: any, privateArgs?: any, baggage?: any) => import('type-fest').Promisable<{creatorFacet: GovernorFacet, publicFacet: GovernedPublicFacetMethods}>} GovernableStartFn */ +/** + * @template {GovernableStartFn} SF Start function of governed contract + * @typedef {GovernedContractFacetAccess>['publicFacet'], Awaited>['creatorFacet']>} GovernedContractFnFacetAccess + * Like GovernedContractFacetAccess but templated by the start function + */ /** * Start an instance of a governor, governing a "governed" contract specified in terms. @@ -165,6 +170,12 @@ const start = async (zcf, privateArgs) => { }); trace('starting governedContractInstallation'); + /** @type {{ + * creatorFacet: GovernorFacet>['creatorFacet']>, + * instance: import('@agoric/zoe/src/zoeService/utils').Instance, + * publicFacet: Awaited>['publicFacet'], + * adminFacet: AdminFacet, + * }} */ const { creatorFacet: governedCF, instance: governedInstance, @@ -181,6 +192,7 @@ const start = async (zcf, privateArgs) => { /** @type {() => Promise} */ const getElectorateInstance = async () => { + // @ts-expect-error FIXME use getGovernedParams() and possibly a helper const invitationAmount = await E(governedPF).getInvitationAmount( CONTRACT_ELECTORATE, ); @@ -317,8 +329,6 @@ const start = async (zcf, privateArgs) => { }); const publicFacet = Far('contract governor public', { - // BEFOREMERGE is this okay? why not? - ...governedPF, getElectorate: getElectorateInstance, getGovernedContract: () => governedInstance, validateVoteCounter, diff --git a/packages/governance/src/types-ambient.js b/packages/governance/src/types-ambient.js index baa2c427463..5620b4a8a2c 100644 --- a/packages/governance/src/types-ambient.js +++ b/packages/governance/src/types-ambient.js @@ -585,15 +585,16 @@ * @property {() => Instance} getContractGovernor * @property {() => ParamStateRecord} getGovernedParams - get descriptions of * all the governed parameters - * @property {(name: string) => Amount} getAmount - * @property {(name: string) => Brand} getBrand - * @property {(name: string) => Instance} getInstance - * @property {(name: string) => Installation} getInstallation - * @property {(name: string) => Amount} getInvitationAmount - * @property {(name: string) => bigint} getNat - * @property {(name: string) => Ratio} getRatio - * @property {(name: string) => string} getString - * @property {(name: string) => any} getUnknown +// FIXME these accessors are onerous for the contract. A facade for them can be made from getGovernedParams() +// property {(name: string) => Amount} getAmount +// property {(name: string) => Brand} getBrand +// property {(name: string) => Instance} getInstance +// property {(name: string) => Installation} getInstallation +// property {(name: string) => Amount} getInvitationAmount +// property {(name: string) => bigint} getNat +// property {(name: string) => Ratio} getRatio +// property {(name: string) => string} getString +// property {(name: string) => any} getUnknown */ /** @@ -633,7 +634,7 @@ /** * @typedef {object} ParamManagerRetriever - * @property {(paramKey?: ParamKey) => AnyParamManager} get + * @property {(paramKey: unknown) => AnyParamManager} get */ /** diff --git a/packages/governance/test/unitTests/test-paramGovernance.js b/packages/governance/test/unitTests/test-paramGovernance.js index c2377151bbd..95f270f14ef 100644 --- a/packages/governance/test/unitTests/test-paramGovernance.js +++ b/packages/governance/test/unitTests/test-paramGovernance.js @@ -76,6 +76,14 @@ const setUpGovernedContract = async (zoe, electorateTerms, timer) => { installBundle(zoe, voteCounterBundle), installBundle(zoe, governedBundle), ]); + /** + * @type {{ + * governor: Installation, + * electorate: Installation, + * counter: Installation, + * governed: Installation, + * }} + */ const installs = { governor, electorate, counter, governed }; const { creatorFacet: committeeCreator } = await E(zoe).startInstance( diff --git a/packages/governance/tools/puppetGovernance.js b/packages/governance/tools/puppetGovernance.js index 90c5065293f..a7650c9d162 100644 --- a/packages/governance/tools/puppetGovernance.js +++ b/packages/governance/tools/puppetGovernance.js @@ -26,15 +26,15 @@ const autoRefundBundleP = makeBundle( * @param {ERef} zoe * @param {ERef>} governedP * @param {import('@agoric/swingset-vat/src/vats/timer/vat-timer.js').TimerService} timer - * @param {{ governedParams?: Record, governedApis?: string[] }} governedTerms - * @param {{}} governedPrivateArgs + * @param {{ [k: string]: any, governedParams?: Record, governedApis?: string[] }} termsOfGoverned + * @param {{}} privateArgsOfGoverned */ export const setUpGovernedContract = async ( zoe, governedP, timer, - governedTerms = {}, - governedPrivateArgs = {}, + termsOfGoverned = {}, + privateArgsOfGoverned = {}, ) => { const [contractGovernorBundle, autoRefundBundle] = await Promise.all([ contractGovernorBundleP, @@ -72,15 +72,15 @@ export const setUpGovernedContract = async ( await getFakeInvitation(); const governedTermsWithElectorate = { - ...governedTerms, + ...termsOfGoverned, governedParams: { - ...governedTerms.governedParams, + ...termsOfGoverned.governedParams, [CONTRACT_ELECTORATE]: { type: ParamTypes.INVITATION, value: fakeInvitationAmount, }, }, - governedApis: governedTerms.governedApis, + governedApis: termsOfGoverned.governedApis, }; const governorTerms = { timer, @@ -97,7 +97,7 @@ export const setUpGovernedContract = async ( governorTerms, { governed: { - ...governedPrivateArgs, + ...privateArgsOfGoverned, initialPoserInvitation: fakeInvitationPayment, }, }, diff --git a/packages/inter-protocol/src/interchainPool.js b/packages/inter-protocol/src/interchainPool.js index e49f8f181bf..dc22fcec9ab 100644 --- a/packages/inter-protocol/src/interchainPool.js +++ b/packages/inter-protocol/src/interchainPool.js @@ -51,6 +51,7 @@ export const start = (zcf, { bankManager }) => { assert.typeof(denom, 'string'); assert.typeof(decimalPlaces, 'number'); + // @ts-expect-error FIXME use getGovernedParams() and possibly a helper const minimumCentral = await E(ammPub).getAmount( MIN_INITIAL_POOL_LIQUIDITY_KEY, ); diff --git a/packages/inter-protocol/src/price/fluxAggregator.contract.js b/packages/inter-protocol/src/price/fluxAggregator.contract.js index f7183fe991e..faf8151c95a 100644 --- a/packages/inter-protocol/src/price/fluxAggregator.contract.js +++ b/packages/inter-protocol/src/price/fluxAggregator.contract.js @@ -66,7 +66,7 @@ export const start = async (zcf, privateArgs, baggage) => { ); trace('got fa', fa); - const { makeGovernorFacet } = await handleParamGovernance( + const { augmentPublicFacet, makeGovernorFacet } = await handleParamGovernance( // @ts-expect-error FIXME include Governance params zcf, initialPoserInvitation, @@ -86,7 +86,8 @@ export const start = async (zcf, privateArgs, baggage) => { const governorFacet = makeGovernorFacet(fa.creatorFacet, governedApis); return harden({ creatorFacet: governorFacet, - publicFacet: fa.publicFacet, + // XXX this is a lot of API to put on every public facet + publicFacet: augmentPublicFacet(fa.publicFacet), }); }; harden(start); diff --git a/packages/inter-protocol/src/proposals/econ-behaviors.js b/packages/inter-protocol/src/proposals/econ-behaviors.js index 40c1f36f2b7..f1add1f1f7f 100644 --- a/packages/inter-protocol/src/proposals/econ-behaviors.js +++ b/packages/inter-protocol/src/proposals/econ-behaviors.js @@ -35,10 +35,10 @@ const MILLI = 1_000_000n; /** * @typedef {object} PSMKit - * @property {Instance} psm - * @property {Instance} psmGovernor + * @property {import('@agoric/zoe/src/zoeService/utils').Instance} psm + * @property {import('@agoric/zoe/src/zoeService/utils').Instance} psmGovernor * @property {Awaited>['creatorFacet']} psmCreatorFacet - * @property {GovernedContractFacetAccess<{},{}>} psmGovernorCreatorFacet + * @property {import('@agoric/governance/src/contractGovernor.js').GovernedContractFnFacetAccess} psmGovernorCreatorFacet * @property {AdminFacet} psmAdminFacet */ @@ -214,9 +214,11 @@ export const setupAmm = async ( ); /** @type {{ creatorFacet: GovernedContractFacetAccess, publicFacet: GovernorPublic, instance: Instance, adminFacet: AdminFacet }} */ + // @ts-expect-error XXX governance or AMM types const g = await E(zoe).startInstance( governorInstallation, {}, + // @ts-expect-error XXX governance or AMM types ammGovernorTerms, { // FIXME unused? @@ -592,6 +594,7 @@ export const startRewardDistributor = async ({ feeDistributorTerms, ); await E(instanceKit.creatorFacet).setDestinations({ + // @ts-expect-error FIXME looks like legit uncovered bug RewardDistributor: rewardDistributorDepositFacet && E(instanceKit.creatorFacet).makeDepositFacetDestination( diff --git a/packages/inter-protocol/src/proposals/startPSM.js b/packages/inter-protocol/src/proposals/startPSM.js index 1f6c604e781..3fe3cf44380 100644 --- a/packages/inter-protocol/src/proposals/startPSM.js +++ b/packages/inter-protocol/src/proposals/startPSM.js @@ -117,10 +117,11 @@ export const startPSM = async ( }, MintLimit: { type: ParamTypes.AMOUNT, value: mintLimit }, }, - [CONTRACT_ELECTORATE]: { - type: ParamTypes.INVITATION, - value: electorateInvitationAmount, - }, + // FIXME dupe and wrong place (or the other is) + // [CONTRACT_ELECTORATE]: { + // type: ParamTypes.INVITATION, + // value: electorateInvitationAmount, + // }, }), ); @@ -162,9 +163,10 @@ export const startPSM = async ( E(governorFacets.creatorFacet).getAdminFacet(), ]); - /** @typedef {import('./econ-behaviors.js').PSMKit} psmKit */ - /** @type {psmKit} */ + /** @typedef {import('./econ-behaviors.js').PSMKit} PSMKit */ + /** @type {PSMKit} */ const newpsmKit = { + // @ts-expect-error XXX with Promisable psm, psmGovernor: governorFacets.instance, psmCreatorFacet, @@ -175,7 +177,7 @@ export const startPSM = async ( // Provide pattern with a promise. producepsmKit.resolve(makeScalarMapStore()); - /** @type {MapStore} */ + /** @type {MapStore} */ const psmKitMap = await psmKit; psmKitMap.init(anchorBrand, newpsmKit); diff --git a/packages/inter-protocol/src/stakeFactory/stakeFactory.js b/packages/inter-protocol/src/stakeFactory/stakeFactory.js index 3a0364cc7f1..6cd2d410dba 100644 --- a/packages/inter-protocol/src/stakeFactory/stakeFactory.js +++ b/packages/inter-protocol/src/stakeFactory/stakeFactory.js @@ -76,8 +76,8 @@ const { values } = Object; * makeCollectFeesInvitation: () => Promise, * }} StakeFactoryCreator * - * @type {ContractStartFn>, - * StakeFactoryTerms, StakeFactoryPrivateArgs>} + * @param {ZCF} zcf + * @param {StakeFactoryPrivateArgs} privateArgs */ export const start = async ( zcf, diff --git a/packages/inter-protocol/src/vaultFactory/vaultDirector.js b/packages/inter-protocol/src/vaultFactory/vaultDirector.js index 78eec0f0c00..01eaee7555c 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultDirector.js +++ b/packages/inter-protocol/src/vaultFactory/vaultDirector.js @@ -246,7 +246,7 @@ export const prepareVaultDirector = ( }, initState, { - creator: { + creator: /** @type {GovernorFacet<{}>} */ ({ getParamMgrRetriever: () => Far('paramManagerRetriever', { /** @param {VaultFactoryParamPath} paramPath */ @@ -273,9 +273,12 @@ export const prepareVaultDirector = ( return harden({}); }, getGovernedApiNames() { - return harden({}); + return harden([]); }, - }, + setOfferFilter(_strings) { + Fail`FIXME not yet implemented`; + }, + }), machine: { // TODO move this under governance #3924 /** diff --git a/packages/inter-protocol/test/psm/setupPsm.js b/packages/inter-protocol/test/psm/setupPsm.js index 415e5647b85..f83518db2a1 100644 --- a/packages/inter-protocol/test/psm/setupPsm.js +++ b/packages/inter-protocol/test/psm/setupPsm.js @@ -161,9 +161,8 @@ export const setupPsm = async ( governorPublicFacet, governorCreatorFacet, }; - const governedInstance = E(governorPublicFacet).getGovernedContract(); + const governedInstance = await E(governorPublicFacet).getGovernedContract(); - /** @type { GovernedPublicFacet } */ const psmPublicFacet = await E(governorCreatorFacet).getPublicFacet(); const psm = { psmCreatorFacet: psmKit.psmCreatorFacet, From ae3d5d2936b63d9aa34f1f842ef04b40bbb5b4a7 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 12:33:33 -0800 Subject: [PATCH 19/33] chore(vaultFactory)!: subscription method compatible with governance --- packages/inter-protocol/src/vaultFactory/vaultDirector.js | 5 +++-- packages/inter-protocol/test/vaultFactory/test-storage.js | 6 +++--- .../inter-protocol/test/vaultFactory/test-vaultFactory.js | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/inter-protocol/src/vaultFactory/vaultDirector.js b/packages/inter-protocol/src/vaultFactory/vaultDirector.js index 01eaee7555c..35ae9b60b31 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultDirector.js +++ b/packages/inter-protocol/src/vaultFactory/vaultDirector.js @@ -102,6 +102,7 @@ export const prepareVaultDirector = ( // Non-durable map because param managers aren't durable. // In the event they're needed they can be reconstructed from contract terms and off-chain data. + /** @type {MapStore>} */ const vaultParamManagers = makeScalarMapStore('vaultParamManagers'); /** @type {PublishKit} */ @@ -520,7 +521,7 @@ export const prepareVaultDirector = ( * * @param {{ collateralBrand: Brand }} selector */ - getSubscription({ collateralBrand }) { + getCollateralManagerGovernanceSubscription({ collateralBrand }) { return vaultParamManagers.get(collateralBrand).getSubscription(); }, getPublicTopics() { @@ -529,7 +530,7 @@ export const prepareVaultDirector = ( /** * subscription for the paramManager for the vaultFactory's electorate */ - getElectorateSubscription() { + getSubscription() { return directorParamManager.getSubscription(); }, /** diff --git a/packages/inter-protocol/test/vaultFactory/test-storage.js b/packages/inter-protocol/test/vaultFactory/test-storage.js index f18c0516049..e195c73c17c 100644 --- a/packages/inter-protocol/test/vaultFactory/test-storage.js +++ b/packages/inter-protocol/test/vaultFactory/test-storage.js @@ -33,7 +33,7 @@ test('storage keys', async t => { ['collaterals', 'rewardPoolAllocation'], ); t.is( - await subscriptionKey(E(vdp).getElectorateSubscription()), + await subscriptionKey(E(vdp).getSubscription()), 'mockChainStorageRoot.vaultFactory.governance', ); @@ -71,7 +71,7 @@ test('storage keys', async t => { ); t.is( await subscriptionKey( - E(vdp).getSubscription({ + E(vdp).getCollateralManagerGovernanceSubscription({ collateralBrand: aeth.brand, }), ), @@ -94,7 +94,7 @@ test('storage keys', async t => { ); t.is( await subscriptionKey( - E(vdp).getSubscription({ + E(vdp).getCollateralManagerGovernanceSubscription({ collateralBrand: chit.brand, }), ), diff --git a/packages/inter-protocol/test/vaultFactory/test-vaultFactory.js b/packages/inter-protocol/test/vaultFactory/test-vaultFactory.js index ff79a1e5f00..4998f448c28 100644 --- a/packages/inter-protocol/test/vaultFactory/test-vaultFactory.js +++ b/packages/inter-protocol/test/vaultFactory/test-vaultFactory.js @@ -2861,7 +2861,7 @@ test('governance publisher', async t => { ); const { lender } = services.vaultFactory; const directorGovNotifier = makeNotifierFromAsyncIterable( - E(lender).getElectorateSubscription(), + E(lender).getSubscription(), ); let { value: { current }, @@ -2875,7 +2875,7 @@ test('governance publisher', async t => { t.is(current.EndorsedUI.type, 'string'); const managerGovNotifier = makeNotifierFromAsyncIterable( - E(lender).getSubscription({ + E(lender).getCollateralManagerGovernanceSubscription({ collateralBrand: aeth.brand, }), ); From 96a7589d46fc6df7cd9b1972e34c13625f62aa0b Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 12:40:28 -0800 Subject: [PATCH 20/33] feat!: allow paths in governance methods --- packages/governance/src/types-ambient.js | 4 +-- .../src/vaultFactory/vaultDirector.js | 28 ++++++++----------- .../test/vaultFactory/test-storage.js | 4 +-- .../test/vaultFactory/test-vaultFactory.js | 2 +- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/governance/src/types-ambient.js b/packages/governance/src/types-ambient.js index 5620b4a8a2c..08b0d31f9c8 100644 --- a/packages/governance/src/types-ambient.js +++ b/packages/governance/src/types-ambient.js @@ -581,9 +581,9 @@ /** * @typedef GovernedPublicFacetMethods - * @property {() => StoredSubscription} getSubscription + * @property {(path?: any) => StoredSubscription} getSubscription * @property {() => Instance} getContractGovernor - * @property {() => ParamStateRecord} getGovernedParams - get descriptions of + * @property {(path?: any) => ParamStateRecord} getGovernedParams - get descriptions of * all the governed parameters // FIXME these accessors are onerous for the contract. A facade for them can be made from getGovernedParams() // property {(name: string) => Amount} getAmount diff --git a/packages/inter-protocol/src/vaultFactory/vaultDirector.js b/packages/inter-protocol/src/vaultFactory/vaultDirector.js index 35ae9b60b31..5acac29cd67 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultDirector.js +++ b/packages/inter-protocol/src/vaultFactory/vaultDirector.js @@ -233,10 +233,9 @@ export const prepareVaultDirector = ( getMetrics: M.call().returns(SubscriberShape), makeVaultInvitation: M.call().returns(M.promise()), getRunIssuer: M.call().returns(IssuerShape), - getSubscription: M.call({ collateralBrand: BrandShape }).returns( - SubscriberShape, - ), - getElectorateSubscription: M.call().returns(SubscriberShape), + getSubscription: M.call() + .optional({ collateralBrand: BrandShape }) + .returns(SubscriberShape), getGovernedParams: M.call({ collateralBrand: BrandShape }).returns( M.record(), ), @@ -514,23 +513,20 @@ export const prepareVaultDirector = ( getRunIssuer() { return debtMint.getIssuerRecord().issuer; }, - /** - * @deprecated get from the CollateralManager directly - * - * subscription for the paramManager for a particular vaultManager - * - * @param {{ collateralBrand: Brand }} selector - */ - getCollateralManagerGovernanceSubscription({ collateralBrand }) { - return vaultParamManagers.get(collateralBrand).getSubscription(); - }, getPublicTopics() { return topics; }, /** - * subscription for the paramManager for the vaultFactory's electorate + * subscription for the paramManager for the vaultFactory's electorate or a particular collateral manager + * + * @param {object} [path] + * @param {Brand} [path.collateralBrand] */ - getSubscription() { + getSubscription(path) { + if (path && path.collateralBrand) { + const { collateralBrand } = path; + return vaultParamManagers.get(collateralBrand).getSubscription(); + } return directorParamManager.getSubscription(); }, /** diff --git a/packages/inter-protocol/test/vaultFactory/test-storage.js b/packages/inter-protocol/test/vaultFactory/test-storage.js index e195c73c17c..8db544bee99 100644 --- a/packages/inter-protocol/test/vaultFactory/test-storage.js +++ b/packages/inter-protocol/test/vaultFactory/test-storage.js @@ -71,7 +71,7 @@ test('storage keys', async t => { ); t.is( await subscriptionKey( - E(vdp).getCollateralManagerGovernanceSubscription({ + E(vdp).getSubscription({ collateralBrand: aeth.brand, }), ), @@ -94,7 +94,7 @@ test('storage keys', async t => { ); t.is( await subscriptionKey( - E(vdp).getCollateralManagerGovernanceSubscription({ + E(vdp).getSubscription({ collateralBrand: chit.brand, }), ), diff --git a/packages/inter-protocol/test/vaultFactory/test-vaultFactory.js b/packages/inter-protocol/test/vaultFactory/test-vaultFactory.js index 4998f448c28..d9aa5ab1c80 100644 --- a/packages/inter-protocol/test/vaultFactory/test-vaultFactory.js +++ b/packages/inter-protocol/test/vaultFactory/test-vaultFactory.js @@ -2875,7 +2875,7 @@ test('governance publisher', async t => { t.is(current.EndorsedUI.type, 'string'); const managerGovNotifier = makeNotifierFromAsyncIterable( - E(lender).getCollateralManagerGovernanceSubscription({ + E(lender).getSubscription({ collateralBrand: aeth.brand, }), ); From d2fc0efc52d22ef18912b02c81f84bbf3e85d5d8 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 12:42:02 -0800 Subject: [PATCH 21/33] fixup something --- packages/inter-protocol/src/vaultFactory/vaultDirector.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/inter-protocol/src/vaultFactory/vaultDirector.js b/packages/inter-protocol/src/vaultFactory/vaultDirector.js index 5acac29cd67..1f414122720 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultDirector.js +++ b/packages/inter-protocol/src/vaultFactory/vaultDirector.js @@ -246,7 +246,7 @@ export const prepareVaultDirector = ( }, initState, { - creator: /** @type {GovernorFacet<{}>} */ ({ + creator: { getParamMgrRetriever: () => Far('paramManagerRetriever', { /** @param {VaultFactoryParamPath} paramPath */ @@ -278,7 +278,7 @@ export const prepareVaultDirector = ( setOfferFilter(_strings) { Fail`FIXME not yet implemented`; }, - }), + }, machine: { // TODO move this under governance #3924 /** From 5b137a9741ca643aa7b0a435ef889ab4e95ed1b2 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 12:52:57 -0800 Subject: [PATCH 22/33] give up --- .../inter-protocol/src/proposals/econ-behaviors.js | 4 ++++ packages/inter-protocol/src/vaultFactory/params.js | 12 ++++++------ packages/inter-protocol/test/psm/test-governedPsm.js | 2 ++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/inter-protocol/src/proposals/econ-behaviors.js b/packages/inter-protocol/src/proposals/econ-behaviors.js index f1add1f1f7f..f0e9cde7a4a 100644 --- a/packages/inter-protocol/src/proposals/econ-behaviors.js +++ b/packages/inter-protocol/src/proposals/econ-behaviors.js @@ -312,6 +312,7 @@ export const setupReserve = async ({ }), ); /** @type {{ creatorFacet: GovernedAssetReserveFacetAccess, publicFacet: GovernorPublic, instance: Instance, adminFacet: AdminFacet }} */ + // @ts-expect-error XXX governance types for governed contract const g = await E(zoe).startInstance( governorInstallation, {}, @@ -486,6 +487,7 @@ export const startVaultFactory = async ( ]); vaultFactoryKit.resolve( + // @ts-expect-error XXX governance types for governed contract harden({ creatorFacet: vaultFactoryCreator, governorCreatorFacet, @@ -630,6 +632,7 @@ export const startRewardDistributor = async ({ Object.entries(collectorKit).map(async ([debugName, collectorFacet]) => { const collector = E(instanceKit.creatorFacet).makeContractFeeCollector( zoe, + // @ts-expect-error FIXME seems like a bug collectorFacet, ); const periodicCollector = await E( @@ -773,6 +776,7 @@ export const startStakeFactory = async ( ); /** @type {{ publicFacet: GovernorPublic, creatorFacet: GovernedContractFacetAccess, adminFacet: AdminFacet}} */ + // @ts-expect-error XXX governance types for governed contract const governorStartResult = await E(zoe).startInstance( contractGovernorInstallation, {}, diff --git a/packages/inter-protocol/src/vaultFactory/params.js b/packages/inter-protocol/src/vaultFactory/params.js index 492f38e80d6..2aae73e9e41 100644 --- a/packages/inter-protocol/src/vaultFactory/params.js +++ b/packages/inter-protocol/src/vaultFactory/params.js @@ -28,11 +28,11 @@ export const SHORTFALL_INVITATION_KEY = 'ShortfallInvitation'; export const ENDORSED_UI_KEY = 'EndorsedUI'; /** - * @param {Amount} electorateInvitationAmount + * @param {Amount<'set'>} electorateInvitationAmount * @param {Installation} liquidationInstall * @param {import('./liquidation.js').LiquidationTerms} liquidationTerms - * @param {Amount} minInitialDebt - * @param {Amount} shortfallInvitationAmount + * @param {Amount<'nat'>} minInitialDebt + * @param {Amount<'set'>} shortfallInvitationAmount * @param {string} endorsedUi */ const makeVaultDirectorParams = ( @@ -152,8 +152,8 @@ harden(makeVaultDirectorParamManager); /** * @param {{storageNode: ERef, marshaller: ERef}} caps * @param {{ - * electorateInvitationAmount: Amount, - * minInitialDebt: Amount, + * electorateInvitationAmount: Amount<'set'>, + * minInitialDebt: Amount<'nat'>, * bootstrapPaymentValue: bigint, * priceAuthority: ERef, * timer: ERef, @@ -162,7 +162,7 @@ harden(makeVaultDirectorParamManager); * loanTiming: LoanTiming, * liquidationTerms: import('./liquidation.js').LiquidationTerms, * ammPublicFacet: XYKAMMPublicFacet, - * shortfallInvitationAmount: Amount, + * shortfallInvitationAmount: Amount<'set'>, * endorsedUi?: string, * }} opts */ diff --git a/packages/inter-protocol/test/psm/test-governedPsm.js b/packages/inter-protocol/test/psm/test-governedPsm.js index 45ad43a6858..6d047282f6c 100644 --- a/packages/inter-protocol/test/psm/test-governedPsm.js +++ b/packages/inter-protocol/test/psm/test-governedPsm.js @@ -192,6 +192,8 @@ test('replace electorate of Economic Committee', async t => { const { governorCreatorFacet } = governor; await E(governorCreatorFacet).replaceElectorate(newPoserInvitation); + /** @type {GovernedPublicFacet} */ + // TODO this should come from the call const pf = await E(governorCreatorFacet).getPublicFacet(); const { Electorate: newElectorate } = await E(pf).getGovernedParams(); t.is(newElectorate.type, 'invitation'); From e8c9eb053dd7c9fc25ab5a58a67fb630c6b6e01f Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 14:03:08 -0800 Subject: [PATCH 23/33] fixup getGovernedApiNames --- packages/inter-protocol/src/vaultFactory/vaultDirector.js | 2 +- packages/smart-wallet/src/invitations.js | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/inter-protocol/src/vaultFactory/vaultDirector.js b/packages/inter-protocol/src/vaultFactory/vaultDirector.js index 1f414122720..8af6913f7a4 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultDirector.js +++ b/packages/inter-protocol/src/vaultFactory/vaultDirector.js @@ -215,7 +215,7 @@ export const prepareVaultDirector = ( getInvitation: M.call(M.string()).returns(M.promise()), getLimitedCreatorFacet: M.call().returns(M.remotable()), getGovernedApis: M.call().returns(M.record()), - getGovernedApiNames: M.call().returns(M.record()), + getGovernedApiNames: M.call().returns(M.arrayOf(M.string())), setOfferFilter: M.call(M.arrayOf(M.string())).returns(), }), machine: M.interface('machine', { diff --git a/packages/smart-wallet/src/invitations.js b/packages/smart-wallet/src/invitations.js index 24a00aec8b8..0566d68b74d 100644 --- a/packages/smart-wallet/src/invitations.js +++ b/packages/smart-wallet/src/invitations.js @@ -75,9 +75,8 @@ export const makeInvitationsHelper = ( console.log('DEBUG searching invitations', invitations, 'for', instance); const matches = invitations.filter( - details => description === details.description, - // FIXME DONOTMERGE instance isn't matching - // && instance === details.instance, + details => + description === details.description && instance === details.instance, ); if (matches.length === 0) { // look up diagnostic info From f6377d95fdd6cb52e776bc39b48bda8b2ac8ecbd Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 14:49:15 -0800 Subject: [PATCH 24/33] oracle test passing --- packages/governance/src/contractGovernor.js | 4 ++ .../src/proposals/price-feed-proposal.js | 44 +++++++++++-------- .../smartWallet/test-oracle-integration.js | 2 + 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/packages/governance/src/contractGovernor.js b/packages/governance/src/contractGovernor.js index 11860acb4ac..a90d99462ac 100644 --- a/packages/governance/src/contractGovernor.js +++ b/packages/governance/src/contractGovernor.js @@ -324,7 +324,11 @@ const start = async (zcf, privateArgs) => { voteOnOfferFilter: voteOnFilter, getCreatorFacet: () => limitedCreatorFacet, getAdminFacet: () => adminFacet, + // TODO use distinct names. "governed" could apply to governance aspects or the contract subject to governance + // "governee" could be used to name the contract that the governor instantiated + // TODO rename to getGoverneeInstance for clarity getInstance: () => governedInstance, + // TODO rename to getGoverneePublicFacet for clarity getPublicFacet: () => governedPF, }); diff --git a/packages/inter-protocol/src/proposals/price-feed-proposal.js b/packages/inter-protocol/src/proposals/price-feed-proposal.js index c3be5191165..8d9b08310d2 100644 --- a/packages/inter-protocol/src/proposals/price-feed-proposal.js +++ b/packages/inter-protocol/src/proposals/price-feed-proposal.js @@ -130,7 +130,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, Installation]]} */ const [[brandIn, brandOut], [contractGovernor, priceAggregator]] = await Promise.all([ @@ -190,9 +190,9 @@ export const createPriceFeed = async ( trace('awaiting startInstance'); // Create the price feed. - const aggregator = await E(zoe).startInstance( - /** @type {Installation} */ - (contractGovernor), + /** @type {{ creatorFacet: import('@agoric/governance/src/contractGovernor.js').GovernedContractFnFacetAccess, publicFacet: GovernorPublic, instance: Instance, adminFacet: AdminFacet }} */ + const aggregatorGovernor = await E(zoe).startInstance( + contractGovernor, undefined, governorTerms, { @@ -205,24 +205,34 @@ export const createPriceFeed = async ( }, }, ); - trace('got aggregator'); - await E(aggregators).set(terms, { aggregator }); + const faCreatorFacet = await E( + aggregatorGovernor.creatorFacet, + ).getCreatorFacet(); + trace('got aggregator', faCreatorFacet); + // FIXME come back to this, might be wrong in master + // await E(aggregators).set(terms, { aggregator }); + + const faPublic = await E(aggregatorGovernor.creatorFacet).getPublicFacet(); + const faInstance = await E(aggregatorGovernor.creatorFacet).getInstance(); + trace('got', { faInstance, faPublic }); E(E(agoricNamesAdmin).lookupAdmin('instance')).update( AGORIC_INSTANCE_NAME, - aggregator.instance, + faInstance, ); + trace('registered', AGORIC_INSTANCE_NAME, faInstance); + // Publish price feed in home.priceAuthority. const forceReplace = true; - void E(priceAuthorityAdmin) - .registerPriceAuthority( - E(aggregator.publicFacet).getPriceAuthority(), - brandIn, - brandOut, - forceReplace, - ) - .then(deleter => E(aggregators).set(terms, { aggregator, deleter })); + void E(priceAuthorityAdmin).registerPriceAuthority( + E(faPublic).getPriceAuthority(), + brandIn, + brandOut, + forceReplace, + ); + // FIXME come back to this, might be wrong in master + // .then(deleter => E(aggregators).set(terms, { aggregator, deleter })); /** * Initialize a new oracle and send an invitation to administer it. @@ -230,9 +240,7 @@ export const createPriceFeed = async ( * @param {string} addr */ const addOracle = async addr => { - // FIXME different facet that peeks under governance - const aggregatorFacet = E(aggregator.creatorFacet).getCreatorFacet(); - const invitation = await E(aggregatorFacet).makeOracleInvitation(addr); + const invitation = await E(faCreatorFacet).makeOracleInvitation(addr); await reserveThenDeposit( `${AGORIC_INSTANCE_NAME} member ${addr}`, namesByAddressAdmin, diff --git a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js index e39795c6282..206b084a291 100644 --- a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js +++ b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js @@ -94,6 +94,8 @@ const setupFeedWithWallets = async (t, oracleAddresses) => { 'ATOM-USD price feed', ); + console.log('DEBUG governedPriceAggregator', governedPriceAggregator); + return { oracleWallets, governedPriceAggregator }; }; From 8ec7e313d7b7b18d608fb477795b87eff245af83 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 15:17:52 -0800 Subject: [PATCH 25/33] fixup puppet gov --- packages/governance/tools/puppetContractGovernor.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/governance/tools/puppetContractGovernor.js b/packages/governance/tools/puppetContractGovernor.js index e3a69a2465b..0c7c85c6f20 100644 --- a/packages/governance/tools/puppetContractGovernor.js +++ b/packages/governance/tools/puppetContractGovernor.js @@ -83,9 +83,7 @@ export const start = async (zcf, privateArgs) => { return E(E(governedCF).getGovernedApis()) [apiMethodName](...methodArgs) - .then(() => { - return positive; - }); + .then(() => positive); }; const creatorFacet = Far('governor creatorFacet', { From 7410bb78cf85630a0038b75c4b6b6db80bf6b94a Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 15:18:09 -0800 Subject: [PATCH 26/33] clean up debugging --- .../inter-protocol/test/smartWallet/test-oracle-integration.js | 2 -- packages/smart-wallet/src/invitations.js | 2 -- 2 files changed, 4 deletions(-) diff --git a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js index 206b084a291..e39795c6282 100644 --- a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js +++ b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js @@ -94,8 +94,6 @@ const setupFeedWithWallets = async (t, oracleAddresses) => { 'ATOM-USD price feed', ); - console.log('DEBUG governedPriceAggregator', governedPriceAggregator); - return { oracleWallets, governedPriceAggregator }; }; diff --git a/packages/smart-wallet/src/invitations.js b/packages/smart-wallet/src/invitations.js index 0566d68b74d..db06c1bf3a0 100644 --- a/packages/smart-wallet/src/invitations.js +++ b/packages/smart-wallet/src/invitations.js @@ -72,8 +72,6 @@ export const makeInvitationsHelper = ( const purseAmount = await E(invitationsPurse).getCurrentAmount(); const invitations = AmountMath.getValue(invitationBrand, purseAmount); - console.log('DEBUG searching invitations', invitations, 'for', instance); - const matches = invitations.filter( details => description === details.description && instance === details.instance, From be64930074de4a150c8a30d857255cc35ab4b073 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 15:18:28 -0800 Subject: [PATCH 27/33] fixup splitof fluxAggregator.contract --- .../src/price/fluxAggregator.js | 1 + .../price/test-fluxAggregator.contract.js | 35 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/inter-protocol/src/price/fluxAggregator.js b/packages/inter-protocol/src/price/fluxAggregator.js index ef18f43f51f..bd1f277cc0c 100644 --- a/packages/inter-protocol/src/price/fluxAggregator.js +++ b/packages/inter-protocol/src/price/fluxAggregator.js @@ -330,3 +330,4 @@ export const provideFluxAggregator = ( return harden({ creatorFacet, publicFacet }); }; harden(provideFluxAggregator); +/** @typedef {ReturnType} FluxAggregator */ diff --git a/packages/inter-protocol/test/price/test-fluxAggregator.contract.js b/packages/inter-protocol/test/price/test-fluxAggregator.contract.js index da6efc6b5c3..06ee6861d68 100644 --- a/packages/inter-protocol/test/price/test-fluxAggregator.contract.js +++ b/packages/inter-protocol/test/price/test-fluxAggregator.contract.js @@ -30,7 +30,7 @@ const dirname = path.dirname(filename); // Pack the contracts. /** @type {EndoZipBase64Bundle} */ const aggregatorBundle = await bundleSource( - `${dirname}/../src/price/fluxAggregator.contract.js`, + `${dirname}/../../src/price/fluxAggregator.contract.js`, ); const defaultConfig = { @@ -104,8 +104,13 @@ const makeContext = async () => { return { governor: governorFacets.creatorFacet, - public: governorFacets.publicFacet, - instance: governorFacets.instance, + /** @type {import('../../src/price/fluxAggregator.js').FluxAggregator['creatorFacet']} */ + // @ts-expect-error XXX types + creatorFacet: await E(governorFacets.creatorFacet).getCreatorFacet(), + /** @type {import('../../src/price/fluxAggregator.js').FluxAggregator['publicFacet']} */ + // @ts-expect-error XXX types + publicFacet: await E(governorFacets.creatorFacet).getPublicFacet(), + instance: E(governorFacets.creatorFacet).getInstance(), mockStorageRoot, }; } @@ -125,23 +130,17 @@ test('basic', async t => { // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); - /** @type {import('../../src/price/priceOracleAdmin.js').OracleAdmin} */ - // @ts-expect-error cast - const pricePushAdminA = await E( - aggregator.governor.invokeAPI('initOracle', ['agorice1priceOracleA']), + const pricePushAdminA = await aggregator.creatorFacet.initOracle( + 'agoric1priceOracleA', ); - /** @type {import('../../src/price/priceOracleAdmin.js').OracleAdmin} */ - // @ts-expect-error cast - const pricePushAdminB = await E( - aggregator.governor.invokeAPI('initOracle', ['agorice1priceOracleB']), + const pricePushAdminB = await aggregator.creatorFacet.initOracle( + 'agoric1priceOracleB', ); - /** @type {import('../../src/price/priceOracleAdmin.js').OracleAdmin} */ - // @ts-expect-error cast - const pricePushAdminC = await E( - aggregator.governor.invokeAPI('initOracle', ['agorice1priceOracleC']), + const pricePushAdminC = await aggregator.creatorFacet.initOracle( + 'agoric1priceOracleC', ); - // ----- round 1: basic consensus + t.log('----- round 1: basic consensus'); await oracleTimer.tick(); await E(pricePushAdminA).pushPrice({ roundId: 1, unitPrice: 100n }); await E(pricePushAdminB).pushPrice({ roundId: 1, unitPrice: 200n }); @@ -152,7 +151,7 @@ test('basic', async t => { t.is(round1Attempt1.roundId, 1n); t.is(round1Attempt1.answer, 200n); - // ----- round 2: check restartDelay implementation + t.log('----- round 2: check restartDelay implementation'); // since oracle A initialized the last round, it CANNOT start another round before // 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 @@ -170,7 +169,7 @@ test('basic', async t => { const round2Attempt1 = await E(aggregator.creatorFacet).getRoundData(2); t.is(round2Attempt1.answer, 2500n); - // ----- round 3: check oracle submission order + t.log('----- round 3: check oracle submission order'); // 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(); From b0f4e4a0b7fce88d82fbf439f5e78f28c51a8382 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 19:57:22 -0800 Subject: [PATCH 28/33] feat(ec-charter): VoteOnApiCall --- .../src/econCommitteeCharter.js | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/inter-protocol/src/econCommitteeCharter.js b/packages/inter-protocol/src/econCommitteeCharter.js index de3b0866cd4..f165e01b1ad 100644 --- a/packages/inter-protocol/src/econCommitteeCharter.js +++ b/packages/inter-protocol/src/econCommitteeCharter.js @@ -79,6 +79,32 @@ export const start = async zcf => { return zcf.makeInvitation(voteOnOfferFilterHandler, 'vote on offer filter'); }; + /** + * @param {Instance} instance + * @param {string} methodName + * @param {string[]} methodArgs + * @param {import('@agoric/time').TimestampValue} deadline + */ + const makeApiInvocationInvitation = ( + instance, + methodName, + methodArgs, + deadline, + ) => { + const handler = seat => { + seat.exit(); + + const governor = instanceToGovernor.get(instance); + return E(governor).voteOnApiInvocation( + methodName, + methodArgs, + counter, + deadline, + ); + }; + return zcf.makeInvitation(handler, 'vote on API invocation'); + }; + const MakerI = M.interface('Charter InvitationMakers', { VoteOnParamChange: M.call().returns(M.promise()), VoteOnPauseOffers: M.call( @@ -86,10 +112,17 @@ export const start = async zcf => { M.arrayOf(M.string()), TimestampShape, ).returns(M.promise()), + VoteOnApiCall: M.call( + InstanceHandleShape, + M.string(), + M.arrayOf(M.string()), + TimestampShape, + ).returns(M.promise()), }); const invitationMakers = makeExo('Charter Invitation Makers', MakerI, { VoteOnParamChange: makeParamInvitation, VoteOnPauseOffers: makeOfferFilterInvitation, + VoteOnApiCall: makeApiInvocationInvitation, }); const charterMemberHandler = seat => { From 6c7a922206a0b092cf3f51fbb63e61c4ac36af31 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 20:28:36 -0800 Subject: [PATCH 29/33] fix(governApi): harden returns --- packages/governance/src/contractGovernance/governApi.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/governance/src/contractGovernance/governApi.js b/packages/governance/src/contractGovernance/governApi.js index f2fd625d767..c3fb25bd4d3 100644 --- a/packages/governance/src/contractGovernance/governApi.js +++ b/packages/governance/src/contractGovernance/governApi.js @@ -22,7 +22,7 @@ const { Fail, quote: q } = assert; const makeApiInvocationPositions = (apiMethodName, methodArgs) => { const positive = harden({ apiMethodName, methodArgs }); const negative = harden({ dontInvoke: apiMethodName }); - return { positive, negative }; + return harden({ positive, negative }); }; /** @@ -113,11 +113,11 @@ const setupApiGovernance = async ( }, ); - return { + return harden({ outcomeOfUpdate, instance: voteCounter, details: E(counterPublicFacet).getDetails(), - }; + }); }; return Far('paramGovernor', { From edc7d2cc2eb8be45f3c9807e15c454a36b77a61f Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 20:29:05 -0800 Subject: [PATCH 30/33] test(smartWallet): extract helpers from psm integration --- .../test/smartWallet/contexts.js | 31 ++++++++++++++++- .../test/smartWallet/test-psm-integration.js | 33 +++---------------- 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/packages/inter-protocol/test/smartWallet/contexts.js b/packages/inter-protocol/test/smartWallet/contexts.js index ec7a7471507..57ca3e78e73 100644 --- a/packages/inter-protocol/test/smartWallet/contexts.js +++ b/packages/inter-protocol/test/smartWallet/contexts.js @@ -1,6 +1,8 @@ import { BridgeId, deeplyFulfilledObject } from '@agoric/internal'; -import { unsafeMakeBundleCache } from '@agoric/swingset-vat/tools/bundleTool.js'; import { makeStorageNodeChild } from '@agoric/internal/src/lib-chainStorage.js'; +// eslint-disable-next-line no-unused-vars -- used by TS +import { coalesceUpdates } from '@agoric/smart-wallet/src/utils.js'; +import { unsafeMakeBundleCache } from '@agoric/swingset-vat/tools/bundleTool.js'; import { E } from '@endo/far'; import path from 'path'; import { createPriceFeed } from '../../src/proposals/price-feed-proposal.js'; @@ -125,3 +127,30 @@ export const makeDefaultTestContext = async (t, makeSpace) => { simpleCreatePriceFeed, }; }; + +/** + * @param {Awaited>} state + * @param {Brand<'nat'>} brand + */ +export const purseBalance = (state, brand) => { + const balances = Array.from(state.balances.values()); + const match = balances.find(b => b.brand === brand); + if (!match) { + console.debug('balances', ...balances); + assert.fail(`${brand} not found in record`); + } + return match.value; +}; +/** + * @param {import('@agoric/smart-wallet/src/smartWallet.js').CurrentWalletRecord} record + * @param {Brand<'nat'>} brand + */ +export const currentPurseBalance = (record, brand) => { + const purses = Array.from(record.purses.values()); + const match = purses.find(b => b.brand === brand); + if (!match) { + console.debug('purses', ...purses); + assert.fail(`${brand} not found in record`); + } + return match.balance.value; +}; diff --git a/packages/inter-protocol/test/smartWallet/test-psm-integration.js b/packages/inter-protocol/test/smartWallet/test-psm-integration.js index d47df0f9032..ca5ea304022 100644 --- a/packages/inter-protocol/test/smartWallet/test-psm-integration.js +++ b/packages/inter-protocol/test/smartWallet/test-psm-integration.js @@ -14,7 +14,11 @@ import { NonNullish } from '@agoric/assert'; import { coalesceUpdates } from '@agoric/smart-wallet/src/utils.js'; import { INVITATION_MAKERS_DESC } from '../../src/econCommitteeCharter.js'; -import { makeDefaultTestContext } from './contexts.js'; +import { + currentPurseBalance, + makeDefaultTestContext, + purseBalance, +} from './contexts.js'; import { headValue, withAmountUtils } from '../supports.js'; /** @@ -51,33 +55,6 @@ test.before(async t => { t.context = await makeDefaultTestContext(t, makePsmTestSpace); }); -/** - * @param {Awaited>} state - * @param {Brand<'nat'>} brand - */ -const purseBalance = (state, brand) => { - const balances = Array.from(state.balances.values()); - const match = balances.find(b => b.brand === brand); - if (!match) { - console.debug('balances', ...balances); - assert.fail(`${brand} not found in record`); - } - return match.value; -}; -/** - * @param {import('@agoric/smart-wallet/src/smartWallet.js').CurrentWalletRecord} record - * @param {Brand<'nat'>} brand - */ -const currentPurseBalance = (record, brand) => { - const purses = Array.from(record.purses.values()); - const match = purses.find(b => b.brand === brand); - if (!match) { - console.debug('purses', ...purses); - assert.fail(`${brand} not found in record`); - } - return match.balance.value; -}; - test('null swap', async t => { const { anchor } = t.context; const { agoricNames } = await E.get(t.context.consume); From 49ec3eef61021d94c383662e11dcdcdadffca79f Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 10 Feb 2023 20:45:33 -0800 Subject: [PATCH 31/33] fixup! WIP feat(price): fluxAggregator governance --- .../src/price/fluxAggregator.contract.js | 13 +- .../src/proposals/price-feed-proposal.js | 7 + .../smartWallet/test-oracle-integration.js | 236 +++++++++++++++++- 3 files changed, 241 insertions(+), 15 deletions(-) diff --git a/packages/inter-protocol/src/price/fluxAggregator.contract.js b/packages/inter-protocol/src/price/fluxAggregator.contract.js index faf8151c95a..2159bf80cc9 100644 --- a/packages/inter-protocol/src/price/fluxAggregator.contract.js +++ b/packages/inter-protocol/src/price/fluxAggregator.contract.js @@ -77,10 +77,17 @@ export const start = async (zcf, privateArgs, baggage) => { marshaller, ); - trace('got param governance'); - const governedApis = { - initOracle: fa.creatorFacet.initOracle, + /** + * + * @param {string} oracleId + */ + addOracle: oracleId => fa.creatorFacet.initOracle(oracleId), + /** + * + * @param {string} oracleId + */ + removeOracle: oracleId => fa.creatorFacet.deleteOracle(oracleId), }; const governorFacet = makeGovernorFacet(fa.creatorFacet, governedApis); diff --git a/packages/inter-protocol/src/proposals/price-feed-proposal.js b/packages/inter-protocol/src/proposals/price-feed-proposal.js index 8d9b08310d2..88873574cd8 100644 --- a/packages/inter-protocol/src/proposals/price-feed-proposal.js +++ b/packages/inter-protocol/src/proposals/price-feed-proposal.js @@ -98,6 +98,7 @@ export const createPriceFeed = async ( chainStorage, chainTimerService, client, + econCharterKit, economicCommitteeCreatorFacet, namesByAddressAdmin, priceAuthority, @@ -221,6 +222,11 @@ export const createPriceFeed = async ( faInstance, ); + E(E.get(econCharterKit).creatorFacet).addInstance( + faInstance, + aggregatorGovernor.creatorFacet, + AGORIC_INSTANCE_NAME, + ); trace('registered', AGORIC_INSTANCE_NAME, faInstance); // Publish price feed in home.priceAuthority. @@ -380,6 +386,7 @@ export const PRICE_FEEDS_MANIFEST = harden({ chainStorage: true, chainTimerService: true, client: true, + econCharterKit: true, namesByAddressAdmin: true, priceAuthority: true, priceAuthorityAdmin: true, diff --git a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js index e39795c6282..0bf06a78a1f 100644 --- a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js +++ b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js @@ -1,6 +1,7 @@ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { NonNullish } from '@agoric/assert'; +import { coalesceUpdates } from '@agoric/smart-wallet/src/utils.js'; import { buildRootObject } from '@agoric/vats/src/core/boot-psm.js'; import '@agoric/vats/src/core/types.js'; import { @@ -8,15 +9,15 @@ import { mockPsmBootstrapArgs, } from '@agoric/vats/tools/boot-test-utils.js'; import { eventLoopIteration } from '@agoric/zoe/tools/eventLoopIteration.js'; -import { E } from '@endo/far'; - -import { coalesceUpdates } from '@agoric/smart-wallet/src/utils.js'; import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; -import { INVITATION_MAKERS_DESC } from '../../src/price/fluxAggregator.js'; +import { E } from '@endo/far'; +import util from 'util'; +import { zip } from '../../src/collect.js'; +import { INVITATION_MAKERS_DESC as EC_INVITATION_MAKERS_DESC } from '../../src/econCommitteeCharter.js'; +import { INVITATION_MAKERS_DESC as ORACLE_INVITATION_MAKERS_DESC } from '../../src/price/fluxAggregator.js'; import { ensureOracleBrands } from '../../src/proposals/price-feed-proposal.js'; import { headValue } from '../supports.js'; -import { makeDefaultTestContext } from './contexts.js'; -import { zip } from '../../src/collect.js'; +import { currentPurseBalance, makeDefaultTestContext } from './contexts.js'; /** * @type {import('ava').TestFn> @@ -25,11 +26,13 @@ import { zip } from '../../src/collect.js'; */ const test = anyTest; +const committeeAddress = 'econCommitteeMemberA'; + const makeTestSpace = async log => { const psmParams = { anchorAssets: [{ denom: 'ibc/usdc1234', keyword: 'AUSD' }], economicCommitteeAddresses: { - /* empty */ + aMember: committeeAddress, }, argv: { bootMsg: {} }, }; @@ -77,6 +80,11 @@ test.before(async t => { t.context = await makeDefaultTestContext(t, makeTestSpace); }); +/** + * + * @param {import('ava').ExecutionContext<*>} t + * @param {string[]} oracleAddresses + */ const setupFeedWithWallets = async (t, oracleAddresses) => { const { agoricNames } = t.context.consume; @@ -105,7 +113,7 @@ const acceptInvitation = async (wallet, priceAggregator) => { const getInvMakersSpec = { source: 'purse', instance: priceAggregator, - description: INVITATION_MAKERS_DESC, + description: ORACLE_INVITATION_MAKERS_DESC, }; /** @type {import('@agoric/smart-wallet/src/offers').OfferSpec} */ @@ -176,12 +184,12 @@ test.serial('invitations', async t => { }; const proposeInvitationDetails = await getInvitationFor( - INVITATION_MAKERS_DESC, + ORACLE_INVITATION_MAKERS_DESC, 1, computedState.balances, ); - t.is(proposeInvitationDetails[0].description, INVITATION_MAKERS_DESC); + t.is(proposeInvitationDetails[0].description, ORACLE_INVITATION_MAKERS_DESC); t.is( proposeInvitationDetails[0].instance, governedPriceAggregator, @@ -194,7 +202,7 @@ test.serial('invitations', async t => { const getInvMakersSpec = { source: 'purse', instance: governedPriceAggregator, - description: INVITATION_MAKERS_DESC, + description: ORACLE_INVITATION_MAKERS_DESC, }; const id = '33'; @@ -212,7 +220,7 @@ test.serial('invitations', async t => { t.deepEqual(Object.keys(currentState.offerToUsedInvitation), [id]); t.is( currentState.offerToUsedInvitation[id].value[0].description, - INVITATION_MAKERS_DESC, + ORACLE_INVITATION_MAKERS_DESC, ); }); @@ -308,3 +316,207 @@ test.serial('errors', async t => { }, ); }); + +test.serial('govern addOracle', async t => { + const { invitationBrand } = t.context; + + const newOracle = 'agoric1OracleB'; + + const { agoricNames, zoe } = await E.get(t.context.consume); + const wallet = await t.context.simpleProvideWallet(committeeAddress); + const computedState = coalesceUpdates(E(wallet).getUpdatesSubscriber()); + const currentSub = E(wallet).getCurrentSubscriber(); + + const offersFacet = wallet.getOffersFacet(); + + const econCharter = await E(agoricNames).lookup( + 'instance', + 'econCommitteeCharter', + ); + const economicCommittee = await E(agoricNames).lookup( + 'instance', + 'economicCommittee', + ); + await eventLoopIteration(); + + /** + * get invitation details the way a user would + * + * @param {string} desc + * @param {number} len + * @param {any} balances XXX please improve this + * @returns {Promise<[{description: string, instance: Instance}]>} + */ + const getInvitationFor = async (desc, len, balances) => + // @ts-expect-error TS can't tell that it's going to satisfy the @returns. + E(E(zoe).getInvitationIssuer()) + .getBrand() + .then(brand => { + /** @type {Amount<'set'>} */ + const invitationsAmount = NonNullish(balances.get(brand)); + console.log('DEBUG invitationsAmount', invitationsAmount); + t.is(invitationsAmount?.value.length, len); + return invitationsAmount.value.filter(i => i.description === desc); + }); + + console.log('DEBUG computedState.balances', computedState.balances); + + const proposeInvitationDetails = await getInvitationFor( + EC_INVITATION_MAKERS_DESC, + 2, + computedState.balances, + ); + + t.is(proposeInvitationDetails[0].description, EC_INVITATION_MAKERS_DESC); + t.is(proposeInvitationDetails[0].instance, econCharter, 'econCharter'); + t.is( + // @ts-expect-error cast amount kind + currentPurseBalance(await headValue(currentSub), invitationBrand).length, + 2, + 'two invitations deposited', + ); + + // The purse has the invitation to get the makers /////////// + + /** @type {import('@agoric/smart-wallet/src/invitations').PurseInvitationSpec} */ + const getInvMakersSpec = { + source: 'purse', + instance: econCharter, + description: EC_INVITATION_MAKERS_DESC, + }; + + /** @type {import('@agoric/smart-wallet/src/offers').OfferSpec} */ + const invMakersOffer = { + id: 44, + invitationSpec: getInvMakersSpec, + proposal: {}, + }; + + await offersFacet.executeOffer(invMakersOffer); + + /** @type {import('@agoric/smart-wallet/src/smartWallet.js').CurrentWalletRecord} */ + let currentState = await headValue(currentSub); + t.is( + // @ts-expect-error cast amount kind + currentPurseBalance(currentState, invitationBrand).length, + 1, + 'one invitation consumed, one left', + ); + t.deepEqual(Object.keys(currentState.offerToUsedInvitation), ['44']); + t.is( + currentState.offerToUsedInvitation[44].value[0].description, + 'charter member invitation', + ); + + console.log('DEBUG purses', util.inspect(currentState.purses, false, 8)); + console.log( + 'DEBUG offerToUsedInvitation', + util.inspect(currentState.offerToUsedInvitation, false, 8), + ); + // Call for a vote //////////////////////////////// + + const feed = await E(agoricNames).lookup('instance', 'ATOM-USD price feed'); + t.assert(feed); + + /** @type {import('@agoric/smart-wallet/src/invitations').ContinuingInvitationSpec} */ + const proposeInvitationSpec = { + source: 'continuing', + previousOffer: 44, + invitationMakerName: 'VoteOnApiCall', + invitationArgs: harden([feed, 'addOracle', [newOracle], 2n]), + }; + + /** @type {import('@agoric/smart-wallet/src/offers').OfferSpec} */ + const proposalOfferSpec = { + id: 45, + invitationSpec: proposeInvitationSpec, + proposal: {}, + }; + + await offersFacet.executeOffer(proposalOfferSpec); + await eventLoopIteration(); + + // vote ///////////////////////// + + const committeePublic = E(zoe).getPublicFacet(economicCommittee); + const questions = await E(committeePublic).getOpenQuestions(); + const question = E(committeePublic).getQuestion(questions[0]); + const { positions, issue, electionType, questionHandle } = await E( + question, + ).getDetails(); + t.is(electionType, 'api_invocation'); + const yesPosition = harden([positions[0]]); + t.deepEqual(issue, { apiMethodName: 'addOracle', methodArgs: [newOracle] }); + t.deepEqual(yesPosition, [ + { apiMethodName: 'addOracle', methodArgs: [newOracle] }, + ]); + + const voteInvitationDetails = await getInvitationFor( + 'Voter0', + 1, + computedState.balances, + ); + t.is(voteInvitationDetails.length, 1); + const voteInvitationDetail = voteInvitationDetails[0]; + t.is(voteInvitationDetail.description, 'Voter0'); + t.is(voteInvitationDetail.instance, economicCommittee); + + /** @type {import('@agoric/smart-wallet/src/invitations').PurseInvitationSpec} */ + const getCommitteeInvMakersSpec = { + source: 'purse', + instance: economicCommittee, + description: 'Voter0', + }; + + /** @type {import('@agoric/smart-wallet/src/offers').OfferSpec} */ + const committeeInvMakersOffer = { + id: 46, + invitationSpec: getCommitteeInvMakersSpec, + proposal: {}, + }; + + await offersFacet.executeOffer(committeeInvMakersOffer); + currentState = await headValue(currentSub); + t.is( + // @ts-expect-error cast amount kind + currentPurseBalance(currentState, invitationBrand).length, + 0, + 'last invitation consumed, none left', + ); + t.deepEqual(Object.keys(currentState.offerToUsedInvitation), ['44', '46']); + // 44 tested above + t.is(currentState.offerToUsedInvitation[46].value[0].description, 'Voter0'); + + /** @type {import('@agoric/smart-wallet/src/invitations').ContinuingInvitationSpec} */ + const getVoteSpec = { + source: 'continuing', + previousOffer: 46, + invitationMakerName: 'makeVoteInvitation', + invitationArgs: harden([yesPosition, questionHandle]), + }; + + /** @type {import('@agoric/smart-wallet/src/offers').OfferSpec} */ + const voteOffer = { + id: 47, + invitationSpec: getVoteSpec, + proposal: {}, + }; + + await offersFacet.executeOffer(voteOffer); + await eventLoopIteration(); + + // FIXME now go into the newOracle wallet and make sure it has the invitation + + const oracleWallet = await t.context.simpleProvideWallet(newOracle); + const oracleWalletComputedState = coalesceUpdates( + E(oracleWallet).getUpdatesSubscriber(), + ); + await eventLoopIteration(); + + /** @type {ERef} */ + // @ts-expect-error cast mock + const timer = t.context.consume.chainTimerService; + await E(timer).tickN(1000); + + console.log(util.inspect(oracleWalletComputedState, false, 9)); +}); From cd8211aa3f6014e08dd6af456181fbf7775453e7 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Sat, 11 Feb 2023 07:18:32 -0800 Subject: [PATCH 32/33] refactor: DRY WalletName.depositFacet --- packages/inter-protocol/src/proposals/utils.js | 12 ++++++++---- packages/pegasus/src/courier.js | 5 ++++- packages/wallet/api/src/lib-wallet.js | 9 ++++++--- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/inter-protocol/src/proposals/utils.js b/packages/inter-protocol/src/proposals/utils.js index 618a95fa117..666f9ce214f 100644 --- a/packages/inter-protocol/src/proposals/utils.js +++ b/packages/inter-protocol/src/proposals/utils.js @@ -1,9 +1,7 @@ +import { WalletName } from '@agoric/internal'; import { getCopyMapEntries, makeCopyMap } from '@agoric/store'; import { E } from '@endo/far'; -// must match packages/wallet/api/src/lib-wallet.js -export const DEPOSIT_FACET = 'depositFacet'; - const { Fail } = assert; /** @@ -52,6 +50,12 @@ export const reserveThenGetNames = async (nameAdmin, names) => names.map(name => [name]), ); +/** + * @param debugName + * @param {ERef} namesByAddressAdmin + * @param {string} addr + * @param {Payment[]} payments + */ export const reserveThenDeposit = async ( debugName, namesByAddressAdmin, @@ -60,7 +64,7 @@ export const reserveThenDeposit = async ( ) => { console.info('awaiting depositFacet for', debugName); const [depositFacet] = await reserveThenGetNamePaths(namesByAddressAdmin, [ - [addr, DEPOSIT_FACET], + [addr, WalletName.depositFacet], ]); console.info('depositing to', debugName); await Promise.allSettled( diff --git a/packages/pegasus/src/courier.js b/packages/pegasus/src/courier.js index e6a7f272632..c422b512f5f 100644 --- a/packages/pegasus/src/courier.js +++ b/packages/pegasus/src/courier.js @@ -2,6 +2,7 @@ import { details as X } from '@agoric/assert'; import { AmountMath } from '@agoric/ertp'; +import { WalletName } from '@agoric/internal'; import { E, Far } from '@endo/far'; import { makeOncePromiseKit } from './once-promise-kit.js'; @@ -95,7 +96,9 @@ export const makeCourierMaker = /** @type {DepositFacet} */ const depositFacet = await E(board) .getValue(depositAddress) - .catch(_ => E(namesByAddress).lookup(depositAddress, 'depositFacet')); + .catch(_ => + E(namesByAddress).lookup(depositAddress, WalletName.depositFacet), + ); const { userSeat, zcfSeat } = zcf.makeEmptySeatKit(); diff --git a/packages/wallet/api/src/lib-wallet.js b/packages/wallet/api/src/lib-wallet.js index c0892394be5..a63b33b0516 100644 --- a/packages/wallet/api/src/lib-wallet.js +++ b/packages/wallet/api/src/lib-wallet.js @@ -13,7 +13,7 @@ import { assert, q, Fail } from '@agoric/assert'; import { makeScalarStoreCoordinator } from '@agoric/cache'; -import { objectMap } from '@agoric/internal'; +import { objectMap, WalletName } from '@agoric/internal'; import { makeLegacyMap, makeScalarMapStore, @@ -863,7 +863,7 @@ export function makeWalletRoot({ if (already) { depositFacet = actions; } else { - depositFacet = Far('depositFacet', { + depositFacet = Far(WalletName.depositFacet, { receive(paymentP) { return E(actions).receive(paymentP); }, @@ -1987,7 +1987,10 @@ export function makeWalletRoot({ .then(addInviteDepositFacet); zoeInvitePurse = wallet.getPurse(ZOE_INVITE_PURSE_PETNAME); - await E(myAddressNameAdmin).update('depositFacet', selfDepositFacet); + await E(myAddressNameAdmin).update( + WalletName.depositFacet, + selfDepositFacet, + ); }; // Importing assets as virtual purses from the bank is a highly-trusted path. From d9204e4531eb53c4717c034dc3d39dc1cdf97e20 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Sat, 11 Feb 2023 07:53:59 -0800 Subject: [PATCH 33/33] feat(price): addOracles by EC --- .../src/econCommitteeCharter.js | 2 +- .../src/price/fluxAggregator.contract.js | 33 ++++++++++--- .../src/price/fluxAggregator.js | 6 ++- .../src/proposals/price-feed-proposal.js | 1 + .../inter-protocol/src/proposals/utils.js | 2 +- .../smartWallet/test-oracle-integration.js | 47 ++++++++++--------- 6 files changed, 58 insertions(+), 33 deletions(-) diff --git a/packages/inter-protocol/src/econCommitteeCharter.js b/packages/inter-protocol/src/econCommitteeCharter.js index f165e01b1ad..c8fe2aa6e76 100644 --- a/packages/inter-protocol/src/econCommitteeCharter.js +++ b/packages/inter-protocol/src/econCommitteeCharter.js @@ -115,7 +115,7 @@ export const start = async zcf => { VoteOnApiCall: M.call( InstanceHandleShape, M.string(), - M.arrayOf(M.string()), + M.arrayOf(M.any()), TimestampShape, ).returns(M.promise()), }); diff --git a/packages/inter-protocol/src/price/fluxAggregator.contract.js b/packages/inter-protocol/src/price/fluxAggregator.contract.js index 2159bf80cc9..9f0954945b5 100644 --- a/packages/inter-protocol/src/price/fluxAggregator.contract.js +++ b/packages/inter-protocol/src/price/fluxAggregator.contract.js @@ -2,6 +2,7 @@ import { AssetKind, makeIssuerKit } from '@agoric/ertp'; import { handleParamGovernance } from '@agoric/governance'; import { assertAllDefined, makeTracer } from '@agoric/internal'; import { E } from '@endo/eventual-send'; +import { reserveThenDeposit } from '../proposals/utils.js'; import { provideFluxAggregator } from './fluxAggregator.js'; const trace = makeTracer('FluxAgg'); @@ -24,6 +25,7 @@ const trace = makeTracer('FluxAgg'); * @param {{ * initialPoserInvitation: Invitation, * marshaller: Marshaller, + * namesByAddressAdmin: ERef, * quoteMint?: ERef>, * storageNode: ERef, * }} privateArgs @@ -47,6 +49,7 @@ export const start = async (zcf, privateArgs, baggage) => { const { initialPoserInvitation, marshaller, + namesByAddressAdmin, storageNode: storageNodeP, } = privateArgs; assertAllDefined({ initialPoserInvitation, marshaller, storageNodeP }); @@ -77,17 +80,33 @@ export const start = async (zcf, privateArgs, baggage) => { marshaller, ); + /** + * Initialize a new oracle and send an invitation to administer it. + * + * @param {string} addr + */ + const addOracle = async addr => { + const invitation = await E(fa.creatorFacet).makeOracleInvitation(addr); + // XXX imported from 'proposals' path + await reserveThenDeposit( + `fluxAggregator oracle ${addr}`, + namesByAddressAdmin, + addr, + [invitation], + ); + return `added ${addr}`; + }; + const governedApis = { /** + * Add the specified oracles. May partially fail, such that some oracles are added and others aren't. * - * @param {string} oracleId - */ - addOracle: oracleId => fa.creatorFacet.initOracle(oracleId), - /** - * - * @param {string} oracleId + * @param {string[]} oracleIds + * @returns {Promise>>} */ - removeOracle: oracleId => fa.creatorFacet.deleteOracle(oracleId), + addOracles: oracleIds => { + return Promise.allSettled(oracleIds.map(addOracle)); + }, }; const governorFacet = makeGovernorFacet(fa.creatorFacet, governedApis); diff --git a/packages/inter-protocol/src/price/fluxAggregator.js b/packages/inter-protocol/src/price/fluxAggregator.js index bd1f277cc0c..1176922dd7b 100644 --- a/packages/inter-protocol/src/price/fluxAggregator.js +++ b/packages/inter-protocol/src/price/fluxAggregator.js @@ -3,7 +3,7 @@ * Modeled on https://github.com/smartcontractkit/chainlink/blob/master/contracts/src/v0.6/FluxAggregator.sol (version?) */ import { AmountMath } from '@agoric/ertp'; -import { assertAllDefined } from '@agoric/internal'; +import { assertAllDefined, makeTracer } from '@agoric/internal'; import { makeNotifierFromSubscriber, observeNotifier, @@ -20,6 +20,8 @@ import { Far } from '@endo/marshal'; import { makeOracleAdmin } from './priceOracleAdmin.js'; import { makeRoundsManagerKit } from './roundsManager.js'; +const trace = makeTracer('FlxAgg'); + export const INVITATION_MAKERS_DESC = 'oracle invitation'; /** @@ -199,6 +201,7 @@ export const provideFluxAggregator = ( * @param {string} oracleId unique per contract instance */ makeOracleInvitation: async oracleId => { + trace('makeOracleInvitation', oracleId); /** * If custom arguments are supplied to the `zoe.offer` call, they can * indicate an OraclePriceSubmission notifier and a corresponding @@ -245,6 +248,7 @@ export const provideFluxAggregator = ( /** @param {string} oracleId */ async initOracle(oracleId) { + trace('initOracle', oracleId); assert.typeof(oracleId, 'string'); const oracleAdmin = makeOracleAdmin( diff --git a/packages/inter-protocol/src/proposals/price-feed-proposal.js b/packages/inter-protocol/src/proposals/price-feed-proposal.js index 88873574cd8..86d89e0c41d 100644 --- a/packages/inter-protocol/src/proposals/price-feed-proposal.js +++ b/packages/inter-protocol/src/proposals/price-feed-proposal.js @@ -200,6 +200,7 @@ export const createPriceFeed = async ( governed: { initialPoserInvitation, marshaller, + namesByAddressAdmin, storageNode: E(storageNode).makeChildNode( sanitizePathSegment(AGORIC_INSTANCE_NAME), ), diff --git a/packages/inter-protocol/src/proposals/utils.js b/packages/inter-protocol/src/proposals/utils.js index 666f9ce214f..fbaedd71031 100644 --- a/packages/inter-protocol/src/proposals/utils.js +++ b/packages/inter-protocol/src/proposals/utils.js @@ -52,7 +52,7 @@ export const reserveThenGetNames = async (nameAdmin, names) => /** * @param debugName - * @param {ERef} namesByAddressAdmin + * @param {ERef} namesByAddressAdmin * @param {string} addr * @param {Payment[]} payments */ diff --git a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js index 0bf06a78a1f..aca9530e6bf 100644 --- a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js +++ b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js @@ -11,7 +11,6 @@ import { import { eventLoopIteration } from '@agoric/zoe/tools/eventLoopIteration.js'; import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; import { E } from '@endo/far'; -import util from 'util'; import { zip } from '../../src/collect.js'; import { INVITATION_MAKERS_DESC as EC_INVITATION_MAKERS_DESC } from '../../src/econCommitteeCharter.js'; import { INVITATION_MAKERS_DESC as ORACLE_INVITATION_MAKERS_DESC } from '../../src/price/fluxAggregator.js'; @@ -344,23 +343,19 @@ test.serial('govern addOracle', async t => { * * @param {string} desc * @param {number} len - * @param {any} balances XXX please improve this + * @param {{get: (b: Brand) => Amount | undefined}} balances * @returns {Promise<[{description: string, instance: Instance}]>} */ const getInvitationFor = async (desc, len, balances) => - // @ts-expect-error TS can't tell that it's going to satisfy the @returns. E(E(zoe).getInvitationIssuer()) .getBrand() .then(brand => { - /** @type {Amount<'set'>} */ - const invitationsAmount = NonNullish(balances.get(brand)); - console.log('DEBUG invitationsAmount', invitationsAmount); + /** @type {any} */ + const invitationsAmount = balances.get(brand); t.is(invitationsAmount?.value.length, len); return invitationsAmount.value.filter(i => i.description === desc); }); - console.log('DEBUG computedState.balances', computedState.balances); - const proposeInvitationDetails = await getInvitationFor( EC_INVITATION_MAKERS_DESC, 2, @@ -408,11 +403,6 @@ test.serial('govern addOracle', async t => { 'charter member invitation', ); - console.log('DEBUG purses', util.inspect(currentState.purses, false, 8)); - console.log( - 'DEBUG offerToUsedInvitation', - util.inspect(currentState.offerToUsedInvitation, false, 8), - ); // Call for a vote //////////////////////////////// const feed = await E(agoricNames).lookup('instance', 'ATOM-USD price feed'); @@ -423,7 +413,7 @@ test.serial('govern addOracle', async t => { source: 'continuing', previousOffer: 44, invitationMakerName: 'VoteOnApiCall', - invitationArgs: harden([feed, 'addOracle', [newOracle], 2n]), + invitationArgs: harden([feed, 'addOracles', [[newOracle]], 2n]), }; /** @type {import('@agoric/smart-wallet/src/offers').OfferSpec} */ @@ -446,9 +436,12 @@ test.serial('govern addOracle', async t => { ).getDetails(); t.is(electionType, 'api_invocation'); const yesPosition = harden([positions[0]]); - t.deepEqual(issue, { apiMethodName: 'addOracle', methodArgs: [newOracle] }); + t.deepEqual(issue, { + apiMethodName: 'addOracles', + methodArgs: [[newOracle]], + }); t.deepEqual(yesPosition, [ - { apiMethodName: 'addOracle', methodArgs: [newOracle] }, + { apiMethodName: 'addOracles', methodArgs: [[newOracle]] }, ]); const voteInvitationDetails = await getInvitationFor( @@ -503,9 +496,14 @@ test.serial('govern addOracle', async t => { }; await offersFacet.executeOffer(voteOffer); - await eventLoopIteration(); - // FIXME now go into the newOracle wallet and make sure it has the invitation + // pass time to exceed the voting deadline + /** @type {ERef} */ + // @ts-expect-error cast mock + const timer = t.context.consume.chainTimerService; + await E(timer).tickN(10); + + // confirm deposit ///////////////////////// const oracleWallet = await t.context.simpleProvideWallet(newOracle); const oracleWalletComputedState = coalesceUpdates( @@ -513,10 +511,13 @@ test.serial('govern addOracle', async t => { ); await eventLoopIteration(); - /** @type {ERef} */ - // @ts-expect-error cast mock - const timer = t.context.consume.chainTimerService; - await E(timer).tickN(1000); + const oracleInvitationDetails = await getInvitationFor( + ORACLE_INVITATION_MAKERS_DESC, + 1, + oracleWalletComputedState.balances, + ); + t.log(oracleInvitationDetails); - console.log(util.inspect(oracleWalletComputedState, false, 9)); + t.is(oracleInvitationDetails[0].description, ORACLE_INVITATION_MAKERS_DESC); + t.is(oracleInvitationDetails[0].instance, feed, 'matches feed instance'); });