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/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', { 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/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..a90d99462ac 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 @@ -57,7 +60,7 @@ const validateQuestionFromCounter = async (zoe, electorate, voteCounter) => { /** * @typedef {StandardTerms} ContractGovernorTerms - * @property {import('@agoric/time/src/types').TimerService} timer + * @property {ERef} timer * @property {Installation} governedContractInstallation */ @@ -115,19 +118,30 @@ const validateQuestionFromCounter = async (zoe, electorate, voteCounter) => { * governedContractInstallation: Installation, * governed: { * issuerKeywordRecord: IssuerKeywordRecord, - * terms: {governedParams: {[CONTRACT_ELECTORATE]: Amount<'set'>}}, + * terms: {governedParams: {[CONTRACT_ELECTORATE]: InvitationParam}}, * } * }>} */ /** - * @template {() => {creatorFacet: GovernorFacet, publicFacet: GovernedPublicFacetMethods} } SF Start function of governed contract + * @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. + * + * @template {GovernableStartFn} SF Start function of governed contract * @param {ZCF<{ - * timer: import('@agoric/time/src/types').TimerService, + * timer: ERef, * governedContractInstallation: Installation, * governed: { * issuerKeywordRecord: IssuerKeywordRecord, - * terms: {governedParams: {[CONTRACT_ELECTORATE]: Amount<'set'>}}, + * terms: {governedParams: {[CONTRACT_ELECTORATE]: import('./contractGovernance/typedParamManager.js').InvitationParam}}, * } * }>} zcf * @param {{ @@ -135,7 +149,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, @@ -144,6 +160,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`; @@ -152,6 +169,13 @@ const start = async (zcf, privateArgs) => { electionManager: zcf.getInstance(), }); + 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, @@ -168,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, ); @@ -191,9 +216,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( @@ -204,6 +231,7 @@ const start = async (zcf, privateArgs) => { getUpdatedPoserFacet, ); + trace('awaiting setupFilterGovernance'); const { voteOnFilter, createdFilterQuestion } = await setupFilterGovernance( zoe, governedInstance, @@ -296,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/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/src/types-ambient.js b/packages/governance/src/types-ambient.js index 9b8f0889bfc..08b0d31f9c8 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 @@ -581,19 +581,20 @@ /** * @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 - * @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 */ /** @@ -681,16 +682,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/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/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..0c7c85c6f20 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,17 @@ 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(() => positive); + }; const creatorFacet = Far('governor creatorFacet', { changeParams, @@ -94,6 +106,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..a7650c9d162 --- /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 {{ [k: string]: any, governedParams?: Record, governedApis?: string[] }} termsOfGoverned + * @param {{}} privateArgsOfGoverned + */ +export const setUpGovernedContract = async ( + zoe, + governedP, + timer, + termsOfGoverned = {}, + privateArgsOfGoverned = {}, +) => { + 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 = { + ...termsOfGoverned, + governedParams: { + ...termsOfGoverned.governedParams, + [CONTRACT_ELECTORATE]: { + type: ParamTypes.INVITATION, + value: fakeInvitationAmount, + }, + }, + governedApis: termsOfGoverned.governedApis, + }; + const governorTerms = { + timer, + governedContractInstallation: governed, + governed: { + terms: governedTermsWithElectorate, + issuerKeywordRecord: {}, + }, + }; + + const governorFacets = await E(zoe).startInstance( + governor, + {}, + governorTerms, + { + governed: { + ...privateArgsOfGoverned, + initialPoserInvitation: fakeInvitationPayment, + }, + }, + ); + + return { getFakeInvitation, governorFacets, installs }; +}; +harden(setUpGovernedContract); 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/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 diff --git a/packages/inter-protocol/src/econCommitteeCharter.js b/packages/inter-protocol/src/econCommitteeCharter.js index de3b0866cd4..c8fe2aa6e76 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.any()), + TimestampShape, + ).returns(M.promise()), }); const invitationMakers = makeExo('Charter Invitation Makers', MakerI, { VoteOnParamChange: makeParamInvitation, VoteOnPauseOffers: makeOfferFilterInvitation, + VoteOnApiCall: makeApiInvocationInvitation, }); const charterMemberHandler = seat => { 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 new file mode 100644 index 00000000000..9f0954945b5 --- /dev/null +++ b/packages/inter-protocol/src/price/fluxAggregator.contract.js @@ -0,0 +1,119 @@ +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'); +/** + * @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 {{ + * initialPoserInvitation: Invitation, + * marshaller: Marshaller, + * namesByAddressAdmin: ERef, + * quoteMint?: ERef>, + * storageNode: ERef, + * }} privateArgs + * @param {Baggage} baggage + */ +export const start = async (zcf, privateArgs, baggage) => { + trace('start'); + 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 { + initialPoserInvitation, + marshaller, + namesByAddressAdmin, + storageNode: storageNodeP, + } = privateArgs; + assertAllDefined({ initialPoserInvitation, marshaller, storageNodeP }); + + const timer = await timerP; + const storageNode = await storageNodeP; + + trace('awaited args'); + + const fa = provideFluxAggregator( + baggage, + zcf, + timer, + quoteKit, + storageNode, + marshaller, + ); + trace('got fa', fa); + + const { augmentPublicFacet, makeGovernorFacet } = await handleParamGovernance( + // @ts-expect-error FIXME include Governance params + zcf, + initialPoserInvitation, + { + // No governed parameters. Governance just for API methods. + }, + storageNode, + 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[]} oracleIds + * @returns {Promise>>} + */ + addOracles: oracleIds => { + return Promise.allSettled(oracleIds.map(addOracle)); + }, + }; + + const governorFacet = makeGovernorFacet(fa.creatorFacet, governedApis); + return harden({ + creatorFacet: governorFacet, + // 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/price/fluxAggregator.js b/packages/inter-protocol/src/price/fluxAggregator.js index 0d5f8d60fd2..1176922dd7b 100644 --- a/packages/inter-protocol/src/price/fluxAggregator.js +++ b/packages/inter-protocol/src/price/fluxAggregator.js @@ -2,8 +2,8 @@ * 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 { assertAllDefined } from '@agoric/internal'; +import { AmountMath } from '@agoric/ertp'; +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'; /** @@ -63,20 +65,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 +97,6 @@ export const start = async (zcf, privateArgs, baggage) => { minSubmissionValue, restartDelay, timeout, - timer, unitAmountIn = AmountMath.make(brandIn, 1n), } = zcf.getTerms(); @@ -103,27 +110,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 +169,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, }); @@ -212,6 +201,7 @@ export const start = async (zcf, privateArgs, baggage) => { * @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 @@ -258,6 +248,7 @@ export const start = async (zcf, privateArgs, baggage) => { /** @param {string} oracleId */ async initOracle(oracleId) { + trace('initOracle', oracleId); assert.typeof(oracleId, 'string'); const oracleAdmin = makeOracleAdmin( @@ -266,7 +257,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 +274,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 +333,5 @@ export const start = async (zcf, privateArgs, baggage) => { return harden({ creatorFacet, publicFacet }); }; -harden(start); +harden(provideFluxAggregator); +/** @typedef {ReturnType} FluxAggregator */ 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/econ-behaviors.js b/packages/inter-protocol/src/proposals/econ-behaviors.js index 1b1533e3ad2..f0e9cde7a4a 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,12 +214,15 @@ 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, { - electorateCreatorFacet: committeeCreator, + // FIXME unused? + // electorateCreatorFacet: committeeCreator, governed: { initialPoserInvitation: poserInvitation, storageNode, @@ -309,12 +312,14 @@ 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, {}, reserveGovernorTerms, { - electorateCreatorFacet: committeeCreator, + // FIXME unused? + // electorateCreatorFacet: committeeCreator, governed: { feeMintAccess, initialPoserInvitation: poserInvitation, @@ -462,7 +467,8 @@ export const startVaultFactory = async ( undefined, governorTerms, harden({ - electorateCreatorFacet, + // FIXME unused? + // electorateCreatorFacet, governed: { feeMintAccess, initialPoserInvitation, @@ -481,6 +487,7 @@ export const startVaultFactory = async ( ]); vaultFactoryKit.resolve( + // @ts-expect-error XXX governance types for governed contract harden({ creatorFacet: vaultFactoryCreator, governorCreatorFacet, @@ -589,11 +596,13 @@ 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( rewardDistributorDepositFacet, ), + // @ts-expect-error FIXME looks like legit uncovered bug Reserve: E(instanceKit.creatorFacet).makeOfferDestination( 'Collateral', E.get(reserveKit).publicFacet, @@ -623,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( @@ -766,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/proposals/price-feed-proposal.js b/packages/inter-protocol/src/proposals/price-feed-proposal.js index 9b4421cf78e..86d89e0c41d 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,8 @@ export const createPriceFeed = async ( chainStorage, chainTimerService, client, + econCharterKit, + economicCommitteeCreatorFacet, namesByAddressAdmin, priceAuthority, priceAuthorityAdmin, @@ -128,62 +131,115 @@ 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], [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 {{ creatorFacet: import('@agoric/governance/src/contractGovernor.js').GovernedContractFnFacetAccess, publicFacet: GovernorPublic, instance: Instance, adminFacet: AdminFacet }} */ + const aggregatorGovernor = await E(zoe).startInstance( + contractGovernor, undefined, - terms, + governorTerms, { - storageNode: E(storageNode).makeChildNode( - sanitizePathSegment(AGORIC_INSTANCE_NAME), - ), - marshaller, + governed: { + initialPoserInvitation, + marshaller, + namesByAddressAdmin, + storageNode: E(storageNode).makeChildNode( + sanitizePathSegment(AGORIC_INSTANCE_NAME), + ), + }, }, ); - 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, + ); + + E(E.get(econCharterKit).creatorFacet).addInstance( + faInstance, + aggregatorGovernor.creatorFacet, + AGORIC_INSTANCE_NAME, ); + 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. @@ -191,9 +247,7 @@ export const createPriceFeed = async ( * @param {string} addr */ const addOracle = async addr => { - const invitation = await E(aggregator.creatorFacet).makeOracleInvitation( - addr, - ); + const invitation = await E(faCreatorFacet).makeOracleInvitation(addr); await reserveThenDeposit( `${AGORIC_INSTANCE_NAME} member ${addr}`, namesByAddressAdmin, @@ -228,6 +282,8 @@ export const getManifestForPriceFeed = async ( chainStorage: t, chainTimerService: t, client: t, + contractGovernor: t, + economicCommitteeCreatorFacet: t, namesByAddressAdmin: t, priceAuthority: t, priceAuthorityAdmin: t, @@ -331,6 +387,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/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/proposals/utils.js b/packages/inter-protocol/src/proposals/utils.js index 618a95fa117..fbaedd71031 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/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/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/src/vaultFactory/vaultDirector.js b/packages/inter-protocol/src/vaultFactory/vaultDirector.js index 3d9c9d02520..8af6913f7a4 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} */ @@ -214,7 +215,8 @@ 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', { addVaultType: M.call(IssuerShape, M.string(), M.record()).returns( @@ -231,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(), ), @@ -272,7 +273,10 @@ export const prepareVaultDirector = ( return harden({}); }, getGovernedApiNames() { - return harden({}); + return harden([]); + }, + setOfferFilter(_strings) { + Fail`FIXME not yet implemented`; }, }, machine: { @@ -509,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 - */ - getSubscription({ 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] */ - getElectorateSubscription() { + getSubscription(path) { + if (path && path.collateralBrand) { + const { collateralBrand } = path; + return vaultParamManagers.get(collateralBrand).getSubscription(); + } return directorParamManager.getSubscription(); }, /** @@ -535,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 diff --git a/packages/inter-protocol/test/test-priceAggregatorChainlink.js b/packages/inter-protocol/test/price/test-fluxAggregator.contract.js similarity index 94% rename from packages/inter-protocol/test/test-priceAggregatorChainlink.js rename to packages/inter-protocol/test/price/test-fluxAggregator.contract.js index 66f72e3df05..06ee6861d68 100644 --- a/packages/inter-protocol/test/test-priceAggregatorChainlink.js +++ b/packages/inter-protocol/test/price/test-fluxAggregator.contract.js @@ -8,6 +8,7 @@ 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, @@ -18,7 +19,7 @@ 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 { topicPath } from '../supports.js'; /** @type {import('ava').TestFn>>} */ const test = unknownTest; @@ -26,7 +27,11 @@ const test = unknownTest; const filename = new URL(import.meta.url).pathname; const dirname = path.dirname(filename); -const aggregatorPath = `${dirname}/../src/price/fluxAggregator.js`; +// Pack the contracts. +/** @type {EndoZipBase64Bundle} */ +const aggregatorBundle = await bundleSource( + `${dirname}/../../src/price/fluxAggregator.contract.js`, +); const defaultConfig = { maxSubmissionCount: 1000, @@ -43,21 +48,21 @@ const makeContext = async () => { 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} */ + /** @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, @@ -75,9 +80,10 @@ const makeContext = async () => { const timer = buildManualTimer(() => {}); - const aggregator = await E(zoe).startInstance( + const { governorFacets } = await setUpGovernedContract( + zoe, aggregatorInstallation, - undefined, + timer, { timer, brandIn: link.brand, @@ -88,13 +94,25 @@ const makeContext = async () => { timeout, minSubmissionValue, maxSubmissionValue, + governedApis: ['initOracle'], }, { marshaller, storageNode: E(storageNode).makeChildNode('LINK-USD_price_feed'), }, ); - return { ...aggregator, mockStorageRoot }; + + return { + governor: governorFacets.creatorFacet, + /** @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, + }; } return { makeChainlinkAggregator, zoe }; @@ -112,17 +130,17 @@ test('basic', async t => { // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); - const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( - 'agorice1priceOracleA', + const pricePushAdminA = await aggregator.creatorFacet.initOracle( + 'agoric1priceOracleA', ); - const pricePushAdminB = await E(aggregator.creatorFacet).initOracle( - 'agorice1priceOracleB', + const pricePushAdminB = await aggregator.creatorFacet.initOracle( + 'agoric1priceOracleB', ); - const pricePushAdminC = await E(aggregator.creatorFacet).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 }); @@ -133,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 @@ -151,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(); diff --git a/packages/inter-protocol/test/price/test-fluxAggregator.js b/packages/inter-protocol/test/price/test-fluxAggregator.js new file mode 100644 index 00000000000..f8a1733a447 --- /dev/null +++ b/packages/inter-protocol/test/price/test-fluxAggregator.js @@ -0,0 +1,705 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { test as unknownTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import { AssetKind, makeIssuerKit } from '@agoric/ertp'; +import { E } from '@endo/eventual-send'; +import { Far } from '@endo/marshal'; + +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 { 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'; + +/** @type {import('ava').TestFn>>} */ +const test = unknownTest; + +const defaultConfig = { + maxSubmissionCount: 1000, + minSubmissionCount: 2, + restartDelay: 5, + timeout: 10, + minSubmissionValue: 100, + maxSubmissionValue: 10000, +}; + +const makeContext = async () => { + const link = makeIssuerKit('$LINK', AssetKind.NAT); + const usd = makeIssuerKit('$USD', AssetKind.NAT); + + async function makeChainlinkAggregator(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 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, manualTimer, mockStorageRoot }; + } + + return { makeChainlinkAggregator }; +}; + +test.before('setup aggregator and oracles', async t => { + t.context = await makeContext(); +}); + +test('basic', async t => { + const aggregator = await t.context.makeChainlinkAggregator(defaultConfig); + const oracleTimer = aggregator.manualTimer; + + 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: 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 aggregator = await t.context.makeChainlinkAggregator({ + ...defaultConfig, + restartDelay: 2, + timeout: 5, + }); + const oracleTimer = aggregator.manualTimer; + + 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 aggregator = await t.context.makeChainlinkAggregator({ + ...defaultConfig, + restartDelay: 2, + }); + const oracleTimer = aggregator.manualTimer; + + 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 aggregator = await t.context.makeChainlinkAggregator({ + ...defaultConfig, + restartDelay: 1, + }); + const oracleTimer = aggregator.manualTimer; + + 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 aggregator = await t.context.makeChainlinkAggregator({ + ...defaultConfig, + maxSubmissionCount: 3, + minSubmissionCount: 3, // requires ALL the oracles for consensus in this case + restartDelay: 1, + timeout: 5, + }); + const oracleTimer = aggregator.manualTimer; + + 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 aggregator = await t.context.makeChainlinkAggregator({ + ...defaultConfig, + minSubmissionCount: 3, + restartDelay: 1, + timeout: 5, + }); + const oracleTimer = aggregator.manualTimer; + + 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 aggregator = await t.context.makeChainlinkAggregator({ + ...defaultConfig, + minSubmissionCount: 3, + restartDelay: 1, + timeout: 5, + }); + const oracleTimer = aggregator.manualTimer; + + 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 aggregator = await t.context.makeChainlinkAggregator({ + ...defaultConfig, + maxSubmissionCount: 1000, + restartDelay: 1, // have to alternate to start rounds + }); + const oracleTimer = aggregator.manualTimer; + + 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/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, diff --git a/packages/inter-protocol/test/psm/test-governedPsm.js b/packages/inter-protocol/test/psm/test-governedPsm.js index bb3d5143819..6d047282f6c 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, }, @@ -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'); diff --git a/packages/inter-protocol/test/smartWallet/contexts.js b/packages/inter-protocol/test/smartWallet/contexts.js index 1ed95bf5245..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'; @@ -80,10 +82,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); @@ -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-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 diff --git a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js index 3950244d014..aca9530e6bf 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,14 @@ 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 { 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 +25,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 +79,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; @@ -88,13 +95,13 @@ 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( + /** @type {import('@agoric/zoe/src/zoeService/utils.js').Instance} */ + const governedPriceAggregator = await E(agoricNames).lookup( 'instance', 'ATOM-USD price feed', ); - return { oracleWallets, priceAggregator }; + return { oracleWallets, governedPriceAggregator }; }; let acceptInvitationCounter = 0; @@ -105,7 +112,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} */ @@ -153,7 +160,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 @@ -174,15 +183,15 @@ 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, - priceAggregator, + governedPriceAggregator, 'priceAggregator', ); @@ -191,8 +200,8 @@ test.serial('invitations', async t => { /** @type {import('@agoric/smart-wallet/src/invitations.js').PurseInvitationSpec} */ const getInvMakersSpec = { source: 'purse', - instance: priceAggregator, - description: INVITATION_MAKERS_DESC, + instance: governedPriceAggregator, + description: ORACLE_INVITATION_MAKERS_DESC, }; const id = '33'; @@ -210,7 +219,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, ); }); @@ -218,11 +227,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 +249,7 @@ 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 = E(zoe).getPublicFacet(governedPriceAggregator); const latestRoundSubscriber = await E(paPublicFacet).getRoundStartNotifier(); @@ -252,9 +262,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); @@ -306,3 +315,209 @@ 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 {{get: (b: Brand) => Amount | undefined}} balances + * @returns {Promise<[{description: string, instance: Instance}]>} + */ + const getInvitationFor = async (desc, len, balances) => + E(E(zoe).getInvitationIssuer()) + .getBrand() + .then(brand => { + /** @type {any} */ + const invitationsAmount = balances.get(brand); + t.is(invitationsAmount?.value.length, len); + return invitationsAmount.value.filter(i => i.description === desc); + }); + + 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', + ); + + // 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, 'addOracles', [[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: 'addOracles', + methodArgs: [[newOracle]], + }); + t.deepEqual(yesPosition, [ + { apiMethodName: 'addOracles', 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); + + // 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( + E(oracleWallet).getUpdatesSubscriber(), + ); + await eventLoopIteration(); + + const oracleInvitationDetails = await getInvitationFor( + ORACLE_INVITATION_MAKERS_DESC, + 1, + oracleWalletComputedState.balances, + ); + t.log(oracleInvitationDetails); + + t.is(oracleInvitationDetails[0].description, ORACLE_INVITATION_MAKERS_DESC); + t.is(oracleInvitationDetails[0].instance, feed, 'matches feed instance'); +}); 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); diff --git a/packages/inter-protocol/test/vaultFactory/test-storage.js b/packages/inter-protocol/test/vaultFactory/test-storage.js index f18c0516049..8db544bee99 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', ); diff --git a/packages/inter-protocol/test/vaultFactory/test-vaultFactory.js b/packages/inter-protocol/test/vaultFactory/test-vaultFactory.js index ff79a1e5f00..d9aa5ab1c80 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 }, 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/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/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/vats/src/core/types.js b/packages/vats/src/core/types.js index fc1aa5e75e1..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,12 +186,8 @@ * consume: Record>, * }, * installation:{ - * produce: Record>, - * consume: Record>> & { - * interchainPool: Promise>, - * mintHolder: Promise>, - * walletFactory: Promise>, - * }, + * produce: { [K in keyof WellKnownInstallations]: Producer> }, + * consume: { [K in keyof WellKnownInstallations]: Promise> }, * }, * instance:{ * produce: Record>, 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. 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( diff --git a/yarn.lock b/yarn.lock index fd85b3aed11..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== @@ -12658,6 +12713,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"