From 159596b8d44b8cbdaf6e19513cb3e716febfae7b Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Fri, 17 Dec 2021 12:07:54 -0800 Subject: [PATCH] feat: refactor parameter governance support to allow for Invitations (#4121) Invitations didn't fit the old model, so turned it into more polymorphic objects. I didn't get as much type-checking advantage as I expected. Moved parameter governance support into a subdirectory, which touched a bunch of files lightly. Added separate types for Amounts and Ratios whose Brands must match. vaults weren't using the params passed to them, so they were dropped. get parameters by type Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- packages/governance/README.md | 74 +++- packages/governance/src/contractGovernor.js | 5 +- packages/governance/src/contractHelper.js | 33 +- packages/governance/src/index.js | 10 +- .../src/paramGovernance/assertions.js | 64 +++ .../src/{ => paramGovernance}/governParam.js | 5 +- .../src/paramGovernance/paramMakers.js | 62 +++ .../src/paramGovernance/paramManager.js | 301 ++++++++++++++ packages/governance/src/paramMakers.js | 59 --- packages/governance/src/paramManager.js | 147 ------- packages/governance/src/question.js | 5 +- packages/governance/src/types.js | 65 ++- .../contractGovernor/bootstrap.js | 8 +- .../contractGovernor/governedContract.js | 17 +- .../contractGovernor/test-governor.js | 1 + .../contractGovernor/vat-voter.js | 5 +- .../test/unitTests/test-buildParamManager.js | 370 ++++++++++++++++++ .../test/unitTests/test-param-manager.js | 367 ----------------- .../unitTests/test-paramChangePositions.js | 112 ++++++ .../test/unitTests/test-paramGovernance.js | 29 +- packages/treasury/bundles/install-on-chain.js | 11 +- packages/treasury/package.json | 1 + packages/treasury/src/params.js | 22 +- packages/treasury/src/stablecoinMachine.js | 13 +- packages/treasury/src/types.js | 18 +- packages/treasury/src/vault.js | 1 - packages/treasury/src/vaultManager.js | 1 - .../governance/test-governance.js | 4 +- .../swingsetTests/governance/vat-voter.js | 8 +- packages/treasury/test/test-stablecoin.js | 12 +- .../treasury/test/vault-contract-wrapper.js | 4 - packages/zoe/package.json | 1 + .../vpool-xyk-amm/multipoolMarketMaker.js | 23 +- .../zoe/src/contracts/vpool-xyk-amm/params.js | 13 +- .../vpool-xyk-amm/test-amm-governance.js | 28 +- .../vpool-xyk-amm/test-xyk-amm-swap.js | 16 +- packages/zoe/test/unitTests/test-zoe.js | 9 +- .../zoe/test/unitTests/zcf/setupZcfTest.js | 1 + .../test/unitTests/zcf/zcfTesterContract.js | 7 +- 39 files changed, 1183 insertions(+), 749 deletions(-) create mode 100644 packages/governance/src/paramGovernance/assertions.js rename packages/governance/src/{ => paramGovernance}/governParam.js (96%) create mode 100644 packages/governance/src/paramGovernance/paramMakers.js create mode 100644 packages/governance/src/paramGovernance/paramManager.js delete mode 100644 packages/governance/src/paramMakers.js delete mode 100644 packages/governance/src/paramManager.js create mode 100644 packages/governance/test/unitTests/test-buildParamManager.js delete mode 100644 packages/governance/test/unitTests/test-param-manager.js create mode 100644 packages/governance/test/unitTests/test-paramChangePositions.js diff --git a/packages/governance/README.md b/packages/governance/README.md index 327f9779944..d2156d26a1e 100644 --- a/packages/governance/README.md +++ b/packages/governance/README.md @@ -9,18 +9,24 @@ connected to so that voters can verify that their votes mean what they say and will be tabulated as expected. Any occasion of governance starts with the creation of an Electorate. Two kinds -exist currently that represent committees and stakeholders (Stakeholder support -is in review). The electorate may deal with many questions governing many -things, so the electorate has to exist before any questions can be posed. +exist currently that represent committees (elected or appointed) and +stakeholders (Stakeholder support is in review). The electorate may deal with +many questions governing many things, so the electorate has to exist before any +questions can be posed. -The next piece to be created is an ElectionManager. (A Contract Governor, is a -particular example, discussed below). An ElectionManager is tied to a particular +The next piece to be created is an ElectionManager. (Contract Governor, one +implementation, is discussed below). An ElectionManager is tied to a particular Electorate. It supports creation of questions, can manage what happens with the results, and may limit the kinds of questions it can handle. The ElectionManager is also responsible for specifying which VoteCounter will be used with any particular question. Different VoteCounters will handle elections with two -positions or more, with plurality voting, single-transferable-vote, or -instant-runoff-voting. +positions or more. The architecture supports vote counters for +[majority decisions](https://en.wikipedia.org/wiki/Majority_rule), +[approval voting](https://en.wikipedia.org/wiki/Approval_voting), and +[proportional representation](https://en.wikipedia.org/wiki/Proportional_representation), +as well as [quadratic](https://en.wikipedia.org/wiki/Quadratic_voting), +[instant runoff](https://en.wikipedia.org/wiki/Category:Instant-runoff_voting), +and more esoteric approaches. When a question is posed, it is only with respect to a particular Electorate, (which identifies a collection of eligible voters) and a particular vote @@ -66,7 +72,7 @@ voterHandle. We want some contracts to be able to make it visible that their internal parameters are controlled by a public process, and allow observers to see who -has control, and how those changes can happen. To do so, the contract would +has control, and how those changes can happen. To do so, the contract will use a ParamManager to hold its mutable state. ParamManager has facets for accessing the param values and for setting them. The governed contract would use the access facet internally, and make that visible to anyone who should be able @@ -115,21 +121,49 @@ the governed contract's publicFacet, creatorFacet, instance, and ### ParamManager `ContractGovernor` expects to work with contracts that use `ParamManager` to -manage their parameters. `buildParamManager()` is designed to be called within -the managed contract so that internal access to the parameter values is +manage their parameters. `makeParamManagerBuilder()` is designed to be called +within the managed contract so that internal access to the parameter values is synchronous. A separate facet allows visible management of changes to the parameter values. -`buildParamManager()` takes a list of parameter descriptions as its argument. -Descriptions give `{ name, type, value }` for each parameter. The parameter -values are retrieved by name. A separate facet of the paramManager allows the -holder to call `updateFoo()` to change the value. ContractGovernor wraps that -facet up so that usage can be monitored. - -The `type` part of the parameter description is a string. Current supported -values are -`{ AMOUNT, BRAND, INSTANCE, INSTALLATION, NAT, RATIO, STRING, UNKNOWN }`. -The list can be extended as we find more types that contracts want to manage. +`makeParamManagerBuilder(zoe)` makes a builder for the ParamManager. The +parameters that will be managed are specified by a sequence of calls to the +builder, each describing one parameter. For instance, such a squence might look +like this: + +``` javascript +return makeParamManagerBuilder() + .addNat(CHARGING_PERIOD_KEY, loanParams.chargingPeriod) + .addNat(RECORDING_PERIOD_KEY, loanParams.recordingPeriod) + .addRatio(INITIAL_MARGIN_KEY, rates.initialMargin) + .addRatio(LIQUIDATION_MARGIN_KEY, rates.liquidationMargin) + .addRatio(INTEREST_RATE_KEY, rates.interestRate) + .addRatio(LOAN_FEE_KEY, rates.loanFee) + .build(); +``` + +Each `addType()` call returns the builder, so the next call can continue the +call cascade. At the end, `.build()` is called. One of the calls +`addInvitation()`, is `async`, so it can't be cascaded. + +``` javascript + const paramManagerBuilder = makeParamManagerBuilder(zoe) + .addBrand('Currency', drachmaBrand) + .addAmount('Amt', drachmaAmount); + // addInvitation is async, so it can't be part of the cascade. + await paramManagerBuilder.addInvitation('Invite', invitation); + const paramManager = paramManagerBuilder.build(); +``` + +The parameter values are retrieved by name. A separate facet of the paramManager +allows the holder to call `updateFoo()` to change the value. ContractGovernor +wraps that facet up so that usage can be monitored. + +Current supported methods for adding parameters include 'addAmount', 'addBrand', +'addInstance', 'addInstallation', 'addInvitation', 'addNat', 'addRatio', +'addString', and 'addUnknown'. The list can be extended as we find more types +that contracts want to manage. (If you find yourself using 'addUnknown', let us +know, as that's a sign that we should support a new type). There's a contractHelper for the vast majority of expected clients that will have a single set of parameters to manage. A contract only has to define the diff --git a/packages/governance/src/contractGovernor.js b/packages/governance/src/contractGovernor.js index 168e0c533f8..638500fab3f 100644 --- a/packages/governance/src/contractGovernor.js +++ b/packages/governance/src/contractGovernor.js @@ -3,7 +3,10 @@ import { E } from '@agoric/eventual-send'; import { Far } from '@agoric/marshal'; -import { setupGovernance, validateParamChangeQuestion } from './governParam.js'; +import { + setupGovernance, + validateParamChangeQuestion, +} from './paramGovernance/governParam.js'; const { details: X } = assert; diff --git a/packages/governance/src/contractHelper.js b/packages/governance/src/contractHelper.js index 9e692039fb0..3cfb0719b53 100644 --- a/packages/governance/src/contractHelper.js +++ b/packages/governance/src/contractHelper.js @@ -3,8 +3,6 @@ import { Far } from '@agoric/marshal'; import { sameStructure } from '@agoric/same-structure'; -import { buildParamManager } from './paramManager.js'; - const { details: X, quote: q } = assert; /** @@ -21,29 +19,38 @@ const { details: X, quote: q } = assert; * * @type {HandleParamGovernance} */ -const handleParamGovernance = (zcf, governedParamsTemplate) => { +const handleParamGovernance = (zcf, paramManager) => { const terms = zcf.getTerms(); /** @type {ParamDescriptions} */ const governedParams = terms.main; const { electionManager } = terms; assert( - sameStructure(governedParams, governedParamsTemplate), - X`Terms must include ${q(governedParamsTemplate)}, but were ${q( + sameStructure(governedParams, paramManager.getParams()), + X`Terms must include ${q(paramManager.getParams())}, but were ${q( governedParams, )}`, ); - const paramManager = buildParamManager(governedParams); + + const typedAccessors = { + getAmount: paramManager.getAmount, + getBrand: paramManager.getBrand, + getInstance: paramManager.getInstance, + getInstallation: paramManager.getInstallation, + getInvitationAmount: paramManager.getInvitationAmount, + getNat: paramManager.getNat, + getRatio: paramManager.getRatio, + getString: paramManager.getString, + getUnknown: paramManager.getUnknown, + }; const wrapPublicFacet = (originalPublicFacet = {}) => { return Far('publicFacet', { ...originalPublicFacet, getSubscription: () => paramManager.getSubscription(), getContractGovernor: () => electionManager, - getGovernedParams: () => { - return paramManager.getParams(); - }, - getParamValue: name => paramManager.getParam(name).value, + getGovernedParams: () => paramManager.getParams(), + ...typedAccessors, }); }; @@ -56,10 +63,13 @@ const handleParamGovernance = (zcf, governedParamsTemplate) => { const wrapCreatorFacet = (originalCreatorFacet = Far('creatorFacet', {})) => { const limitedCreatorFacet = makeLimitedCreatorFacet(originalCreatorFacet); + + // exclusively for contractGovernor, which only reveals limitedCreatorFacet return Far('creatorFacet', { getParamMgrRetriever: () => { return Far('paramRetriever', { get: () => paramManager }); }, + getInvitation: name => paramManager.getInternalParamValue(name), getLimitedCreatorFacet: () => limitedCreatorFacet, }); }; @@ -67,8 +77,9 @@ const handleParamGovernance = (zcf, governedParamsTemplate) => { return harden({ wrapPublicFacet, wrapCreatorFacet, - getParamValue: name => paramManager.getParam(name).value, + ...typedAccessors, }); }; harden(handleParamGovernance); + export { handleParamGovernance }; diff --git a/packages/governance/src/index.js b/packages/governance/src/index.js index f026fdc57f9..df1f812be91 100644 --- a/packages/governance/src/index.js +++ b/packages/governance/src/index.js @@ -20,12 +20,16 @@ export { export { handleParamGovernance } from './contractHelper.js'; export { + assertBallotConcernsQuestion, makeParamChangePositions, + setupGovernance, validateParamChangeQuestion, - assertBallotConcernsQuestion, -} from './governParam.js'; +} from './paramGovernance/governParam.js'; -export { ParamType, buildParamManager, assertType } from './paramManager.js'; +export { + ParamType, + makeParamManagerBuilder, +} from './paramGovernance/paramManager.js'; export { assertContractGovernance, diff --git a/packages/governance/src/paramGovernance/assertions.js b/packages/governance/src/paramGovernance/assertions.js new file mode 100644 index 00000000000..3409aba0d99 --- /dev/null +++ b/packages/governance/src/paramGovernance/assertions.js @@ -0,0 +1,64 @@ +// @ts-check + +import { isRemotable } from '@agoric/marshal'; +import { assertIsRatio } from '@agoric/zoe/src/contractSupport/ratio.js'; + +const { details: X } = assert; + +const makeLooksLikeBrand = name => { + return brand => { + assert( + // @ts-ignore value is undifferentiated to this point + isRemotable(brand), + X`value for ${name} must be a brand, was ${brand}`, + ); + }; +}; +harden(makeLooksLikeBrand); + +const makeAssertInstallation = name => { + return installation => { + // TODO(3344): add a better assertion once Zoe validates installations + assert( + typeof installation === 'object' && + Object.getOwnPropertyNames(installation).length === 1, + X`value for ${name} must be an Installation, was ${installation}`, + ); + }; +}; +harden(makeAssertInstallation); + +const makeAssertInstance = name => { + return instance => { + // TODO(3344): add a better assertion once Zoe validates instances + assert( + typeof instance === 'object' && + Object.getOwnPropertyNames(instance).length === 0, + X`value for ${name} must be an Instance, was ${instance}`, + ); + }; +}; +harden(makeAssertInstance); + +const makeAssertBrandedRatio = (name, modelRatio) => { + return ratio => { + assertIsRatio(ratio); + assert( + ratio.numerator.brand === modelRatio.numerator.brand, + X`Numerator brand for ${name} must be ${modelRatio.numerator.brand}`, + ); + assert( + ratio.denominator.brand === modelRatio.denominator.brand, + X`Denominator brand for ${name} must be ${modelRatio.denominator.brand}`, + ); + return true; + }; +}; +harden(makeAssertBrandedRatio); + +export { + makeLooksLikeBrand, + makeAssertInstallation, + makeAssertInstance, + makeAssertBrandedRatio, +}; diff --git a/packages/governance/src/governParam.js b/packages/governance/src/paramGovernance/governParam.js similarity index 96% rename from packages/governance/src/governParam.js rename to packages/governance/src/paramGovernance/governParam.js index faa1f7f9a5f..d34fcd5eded 100644 --- a/packages/governance/src/governParam.js +++ b/packages/governance/src/paramGovernance/governParam.js @@ -11,8 +11,7 @@ import { QuorumRule, ElectionType, looksLikeQuestionSpec, -} from './question.js'; -import { assertType } from './paramManager.js'; +} from '../question.js'; const { details: X } = assert; @@ -76,8 +75,6 @@ const setupGovernance = async ( ) => { const paramMgr = E(paramManagerRetriever).get(paramSpec); const paramName = paramSpec.parameterName; - const param = await E(paramMgr).getParam(paramName); - assertType(param.type, proposedValue, paramName); const outcomeOfUpdateP = makePromiseKit(); const { positive, negative } = makeParamChangePositions( diff --git a/packages/governance/src/paramGovernance/paramMakers.js b/packages/governance/src/paramGovernance/paramMakers.js new file mode 100644 index 00000000000..5adfc3f2c5f --- /dev/null +++ b/packages/governance/src/paramGovernance/paramMakers.js @@ -0,0 +1,62 @@ +// ts-check + +import { ParamType } from './paramManager.js'; + +const makeGovernedNat = value => { + return harden({ type: ParamType.NAT, value }); +}; + +const makeGovernedAmount = value => { + return harden({ type: ParamType.AMOUNT, value }); +}; + +const makeGovernedRatio = value => { + return harden({ type: ParamType.RATIO, value }); +}; + +const makeGovernedBrand = value => { + return harden({ type: ParamType.BRAND, value }); +}; + +const makeGovernedInstance = value => { + return harden({ type: ParamType.INSTANCE, value }); +}; + +const makeGovernedInstallation = value => { + return harden({ type: ParamType.INSTALLATION, value }); +}; + +// value is an invitation amount, not an invitation +const makeGovernedInvitation = value => { + return harden({ type: ParamType.INVITATION, value }); +}; + +const makeGovernedString = value => { + return harden({ type: ParamType.STRING, value }); +}; + +const makeGovernedUnknown = value => { + return harden({ type: ParamType.UNKNOWN, value }); +}; + +harden(makeGovernedAmount); +harden(makeGovernedBrand); +harden(makeGovernedInstallation); +harden(makeGovernedInstance); +harden(makeGovernedInvitation); +harden(makeGovernedNat); +harden(makeGovernedRatio); +harden(makeGovernedString); +harden(makeGovernedUnknown); + +export { + makeGovernedAmount, + makeGovernedBrand, + makeGovernedInstallation, + makeGovernedInstance, + makeGovernedInvitation, + makeGovernedNat, + makeGovernedRatio, + makeGovernedString, + makeGovernedUnknown, +}; diff --git a/packages/governance/src/paramGovernance/paramManager.js b/packages/governance/src/paramGovernance/paramManager.js new file mode 100644 index 00000000000..8868fe907bb --- /dev/null +++ b/packages/governance/src/paramGovernance/paramManager.js @@ -0,0 +1,301 @@ +// @ts-check + +import { Far } from '@agoric/marshal'; +import { assertIsRatio } from '@agoric/zoe/src/contractSupport/index.js'; +import { AmountMath } from '@agoric/ertp'; +import { assertKeywordName } from '@agoric/zoe/src/cleanProposal.js'; +import { Nat } from '@agoric/nat'; +import { makeSubscriptionKit } from '@agoric/notifier'; +import { makeStore } from '@agoric/store'; +import { E } from '@agoric/eventual-send'; + +import { + makeLooksLikeBrand, + makeAssertInstallation, + makeAssertInstance, + makeAssertBrandedRatio, +} from './assertions.js'; + +const { details: X } = assert; +/** + * @type {{ + * AMOUNT: 'amount', + * BRAND: 'brand', + * INSTANCE: 'instance', + * INSTALLATION: 'installation', + * INVITATION: 'invitation', + * NAT: 'nat', + * RATIO: 'ratio', + * STRING: 'string', + * UNKNOWN: 'unknown', + * }} + * + * UNKNOWN is an escape hatch for types we haven't added yet. If you are + * developing a new contract and use UNKNOWN, please also file an issue to ask + * us to support the new type. + */ +const ParamType = { + AMOUNT: 'amount', + BRAND: 'brand', + INSTANCE: 'instance', + INSTALLATION: 'installation', + INVITATION: 'invitation', + NAT: 'nat', + RATIO: 'ratio', + STRING: 'string', + UNKNOWN: 'unknown', +}; + +/** @type {(zoe?:ERef) => ParamManagerBuilder} */ +const makeParamManagerBuilder = zoe => { + const namesToParams = makeStore('Parameter Name'); + const { publication, subscription } = makeSubscriptionKit(); + const updateFns = {}; + + // support for parameters that are copy objects + const buildCopyParam = (name, value, assertion, type) => { + let current; + assertKeywordName(name); + + const setParamValue = newValue => { + assertion(newValue); + current = newValue; + publication.updateState({ name, type, value: current }); + return newValue; + }; + setParamValue(value); + + const publicMethods = Far(`Parameter ${name}`, { + getValue: () => current, + assertType: assertion, + makeDescription: () => ({ name, type, value: current }), + makeShortDescription: () => ({ type, value: current }), + getType: () => type, + }); + + // CRUCIAL: here we're creating the update functions that can change the + // values of the governed contract's parameters. We'll return the updateFns + // to our caller. They must handle them carefully to ensure that they end up + // in appropriate hands. + updateFns[`update${name}`] = setParamValue; + namesToParams.init(name, publicMethods); + }; + + // HANDLERS FOR EACH PARAMETER TYPE ///////////////////////////////////////// + + /** @type {(name: string, amount: Amount, builder: ParamManagerBuilder) => ParamManagerBuilder} */ + const addAmount = (name, amount, builder) => { + const assertAmount = a => { + assert(a.brand, `Expected an Amount for ${name}, got "${a}"`); + return AmountMath.coerce(a.brand, a); + }; + buildCopyParam(name, amount, assertAmount, ParamType.AMOUNT); + return builder; + }; + + /** @type {(name: string, amount: Amount, builder: ParamManagerBuilder) => ParamManagerBuilder} */ + const addBrandedAmount = (name, amount, builder) => { + const assertAmount = a => { + assert(a.brand, `Expected an Amount for ${name}, got "${a}"`); + return AmountMath.coerce(amount.brand, a); + }; + buildCopyParam(name, amount, assertAmount, ParamType.AMOUNT); + return builder; + }; + + /** @type {(name: string, value: Brand, builder: ParamManagerBuilder) => ParamManagerBuilder} */ + const addBrand = (name, value, builder) => { + const assertBrand = makeLooksLikeBrand(name); + buildCopyParam(name, value, assertBrand, ParamType.BRAND); + return builder; + }; + + /** @type {(name: string, value: Installation, builder: ParamManagerBuilder) => ParamManagerBuilder} */ + const addInstallation = (name, value, builder) => { + const assertInstallation = makeAssertInstallation(name); + buildCopyParam(name, value, assertInstallation, ParamType.INSTALLATION); + return builder; + }; + + /** @type {(name: string, value: Instance, builder: ParamManagerBuilder) => ParamManagerBuilder} */ + const addInstance = (name, value, builder) => { + const assertInstance = makeAssertInstance(name); + buildCopyParam(name, value, assertInstance, ParamType.INSTANCE); + return builder; + }; + + /** @type {(name: string, value: bigint, builder: ParamManagerBuilder) => ParamManagerBuilder} */ + const addNat = (name, value, builder) => { + const assertNat = v => { + assert.typeof(v, 'bigint'); + Nat(v); + return true; + }; + buildCopyParam(name, value, assertNat, ParamType.NAT); + return builder; + }; + + /** @type {(name: string, value: Ratio, builder: ParamManagerBuilder) => ParamManagerBuilder} */ + const addRatio = (name, value, builder) => { + buildCopyParam(name, value, assertIsRatio, ParamType.RATIO); + return builder; + }; + + /** @type {(name: string, value: Ratio, builder: ParamManagerBuilder) => ParamManagerBuilder} */ + const addBrandedRatio = (name, value, builder) => { + const assertBrandedRatio = makeAssertBrandedRatio(name, value); + buildCopyParam(name, value, assertBrandedRatio, ParamType.RATIO); + return builder; + }; + + /** @type {(name: string, value: string, builder: ParamManagerBuilder) => ParamManagerBuilder} */ + const addString = (name, value, builder) => { + const assertString = v => assert.typeof(v, 'string'); + buildCopyParam(name, value, assertString, ParamType.STRING); + return builder; + }; + + /** @type {(name: string, value: any, builder: ParamManagerBuilder) => ParamManagerBuilder} */ + const addUnknown = (name, value, builder) => { + const assertUnknown = _v => true; + buildCopyParam(name, value, assertUnknown, ParamType.UNKNOWN); + return builder; + }; + + const assertInvitation = async i => { + assert(zoe, `zoe must be provided for governed Invitations ${zoe}`); + const { instance, installation } = await E(zoe).getInvitationDetails(i); + assert(instance && installation, 'must be an invitation'); + }; + + // Invitations are closely held, so we should only reveal the amount publicly. + // getInternalValue() will only be accessible within the contract. + const buildInvitationParam = async (name, invitation) => { + assert(zoe, `zoe must be provided for governed Invitations ${zoe}`); + let currentInvitation; + let currentAmount; + + const setInvitation = async i => { + [currentAmount] = await Promise.all([ + E(E(zoe).getInvitationIssuer()).getAmountOf(invitation), + assertInvitation(invitation), + ]); + currentInvitation = i; + publication.updateState({ + name, + type: ParamType.INVITATION, + value: currentAmount, + }); + }; + await setInvitation(invitation); + + const makeDescription = () => { + return ({ name, type: ParamType.INVITATION, value: currentAmount }); + }; + const makeShortDescription = () => { + return ({ type: ParamType.INVITATION, value: currentAmount }); + }; + + const publicMethods = Far(`Parameter ${name}`, { + getValue: () => currentAmount, + getInternalValue: () => currentInvitation, + assertType: assertInvitation, + makeDescription, + makeShortDescription, + getType: () => ParamType.INVITATION, + }); + + // CRUCIAL: here we're creating the update functions that can change the + // values of the governed contract's parameters. We'll return the updateFns + // to our caller. They must handle them carefully to ensure that they end up + // in appropriate hands. + updateFns[`update${name}`] = setInvitation; + namesToParams.init(name, publicMethods); + return name; + }; + + /** @type {(name: string, value: Invitation, builder: ParamManagerBuilder) => Promise} */ + const addInvitation = async (name, value, builder) => { + assertKeywordName(name); + await Promise.all([ + assertInvitation(value), + buildInvitationParam(name, value), + ]); + + return builder; + }; + + // PARAM MANAGER METHODS //////////////////////////////////////////////////// + + const getParam = name => { + return namesToParams.get(name).makeDescription(); + }; + + const getTypedParam = (type, name) => { + const param = namesToParams.get(name); + assert(type === param.getType(), X`${name} is not ${type}`); + return param.getValue(); + }; + + const getParamList = () => { + return harden(namesToParams.keys().map(k => getParam(k))); + }; + + // should be exposed within contracts, and not externally, for invitations + const getInternalParamValue = name => { + return namesToParams.get(name).getInternalValue(); + }; + + const getParams = () => { + /** @type {Record} */ + const descriptions = {}; + namesToParams.entries().forEach(([name, param]) => { + descriptions[name] = param.makeShortDescription(); + }); + return harden(descriptions); + }; + + const makeParamManager = updateFunctions => { + // CRUCIAL: Contracts that call buildParamManager should only export the + // resulting paramManager to their creatorFacet, where it will be picked up by + // contractGovernor. The getParams method can be shared widely. + return Far('param manager', { + getParams, + getSubscription: () => subscription, + getAmount: name => getTypedParam(ParamType.AMOUNT, name), + getBrand: name => getTypedParam(ParamType.BRAND, name), + getInstance: name => getTypedParam(ParamType.INSTANCE, name), + getInstallation: name => getTypedParam(ParamType.INSTALLATION, name), + getInvitationAmount: name => getTypedParam(ParamType.INVITATION, name), + getNat: name => getTypedParam(ParamType.NAT, name), + getRatio: name => getTypedParam(ParamType.RATIO, name), + getString: name => getTypedParam(ParamType.STRING, name), + getUnknown: name => getTypedParam(ParamType.UNKNOWN, name), + getParamList, + getInternalParamValue, + ...updateFunctions, + }); + }; + + /** @type {ParamManagerBuilder} */ + const builder = { + addAmount: (n, v) => addAmount(n, v, builder), + addBrandedAmount: (n, v) => addBrandedAmount(n, v, builder), + addBrand: (n, v) => addBrand(n, v, builder), + addInstallation: (n, v) => addInstallation(n, v, builder), + addInstance: (n, v) => addInstance(n, v, builder), + addUnknown: (n, v) => addUnknown(n, v, builder), + addInvitation: (n, v) => addInvitation(n, v, builder), + addNat: (n, v) => addNat(n, v, builder), + addRatio: (n, v) => addRatio(n, v, builder), + addBrandedRatio: (n, v) => addBrandedRatio(n, v, builder), + addString: (n, v) => addString(n, v, builder), + build: () => makeParamManager(updateFns), + }; + return builder; +}; + +harden(makeParamManagerBuilder); +harden(ParamType); + +export { ParamType, makeParamManagerBuilder }; diff --git a/packages/governance/src/paramMakers.js b/packages/governance/src/paramMakers.js deleted file mode 100644 index 88f018643f4..00000000000 --- a/packages/governance/src/paramMakers.js +++ /dev/null @@ -1,59 +0,0 @@ -// ts-check - -import { ParamType } from './paramManager.js'; - -// It might be cleaner if these returned objects with behavior that included the -// type assertion from paramManager, and other polymorphism, but this is enough -// to get us a much simpler calling style. - -const makeGovernedNat = (name, value) => { - return harden({ name, type: ParamType.NAT, value }); -}; - -const makeGovernedAmount = (name, value) => { - return harden({ name, type: ParamType.AMOUNT, value }); -}; - -const makeGovernedRatio = (name, value) => { - return harden({ name, type: ParamType.RATIO, value }); -}; - -const makeGovernedBrand = (name, value) => { - return harden({ name, type: ParamType.BRAND, value }); -}; - -const makeGovernedInstance = (name, value) => { - return harden({ name, type: ParamType.INSTANCE, value }); -}; - -const makeGovernedInstallation = (name, value) => { - return harden({ name, type: ParamType.INSTALLATION, value }); -}; - -const makeGovernedString = (name, value) => { - return harden({ name, type: ParamType.STRING, value }); -}; - -const makeGovernedUnknown = (name, value) => { - return harden({ name, type: ParamType.UNKNOWN, value }); -}; - -harden(makeGovernedAmount); -harden(makeGovernedBrand); -harden(makeGovernedInstallation); -harden(makeGovernedInstance); -harden(makeGovernedNat); -harden(makeGovernedRatio); -harden(makeGovernedString); -harden(makeGovernedUnknown); - -export { - makeGovernedAmount, - makeGovernedBrand, - makeGovernedInstallation, - makeGovernedInstance, - makeGovernedNat, - makeGovernedRatio, - makeGovernedString, - makeGovernedUnknown, -}; diff --git a/packages/governance/src/paramManager.js b/packages/governance/src/paramManager.js deleted file mode 100644 index 9fd7fef4a4c..00000000000 --- a/packages/governance/src/paramManager.js +++ /dev/null @@ -1,147 +0,0 @@ -// @ts-check - -import { isRemotable, Far } from '@agoric/marshal'; -import { assertIsRatio } from '@agoric/zoe/src/contractSupport/index.js'; -import { AmountMath } from '@agoric/ertp'; -import { assertKeywordName } from '@agoric/zoe/src/cleanProposal.js'; -import { Nat } from '@agoric/nat'; -import { makeSubscriptionKit } from '@agoric/notifier'; - -const { details: X } = assert; - -/** - * @type {{ - * AMOUNT: 'amount', - * BRAND: 'brand', - * INSTANCE: 'instance', - * INSTALLATION: 'installation', - * NAT: 'nat', - * RATIO: 'ratio', - * STRING: 'string', - * UNKNOWN: 'unknown', - * }} - * - * UNKNOWN is an escape hatch for types we haven't added yet. If you are - * developing a new contract and use UNKNOWN, please also file an issue to ask - * us to support the new type. - */ -const ParamType = { - AMOUNT: 'amount', - BRAND: 'brand', - INSTANCE: 'instance', - INSTALLATION: 'installation', - NAT: 'nat', - RATIO: 'ratio', - STRING: 'string', - UNKNOWN: 'unknown', -}; - -/** @type {AssertParamManagerType} */ -const assertType = (type, value, name) => { - switch (type) { - case ParamType.AMOUNT: - // It would be nice to have a clean way to assert something is an amount. - // @ts-ignore value is undifferentiated to this point - AmountMath.coerce(value.brand, value); - break; - case ParamType.BRAND: - assert( - // @ts-ignore value is undifferentiated to this point - isRemotable(value), - X`value for ${name} must be a brand, was ${value}`, - ); - break; - case ParamType.INSTALLATION: - // TODO(3344): add a better assertion once Zoe validates installations - assert( - typeof value === 'object' && - Object.getOwnPropertyNames(value).length === 1, - X`value for ${name} must be an Installation, was ${value}`, - ); - break; - case ParamType.INSTANCE: - // TODO(3344): add a better assertion once Zoe validates instances - assert( - typeof value === 'object' && !Object.getOwnPropertyNames(value).length, - X`value for ${name} must be an Instance, was ${value}`, - ); - break; - case ParamType.NAT: - assert.typeof(value, 'bigint'); - Nat(value); - break; - case ParamType.RATIO: - assertIsRatio(value); - break; - case ParamType.STRING: - assert.typeof(value, 'string'); - break; - case ParamType.UNKNOWN: - break; - default: - assert.fail(X`unrecognized type ${type}`); - } -}; - -/** @type {BuildParamManager} */ -const buildParamManager = paramDescriptions => { - const typesAndValues = {}; - // updateFns will have updateFoo() for each Foo param. - const updateFns = {}; - const { publication, subscription } = makeSubscriptionKit(); - paramDescriptions.forEach(({ name, value, type }) => { - // we want to create function names like updateFeeRatio(), so we insist that - // the name has Keyword-nature. - assertKeywordName(name); - - assert( - !typesAndValues[name], - X`each parameter name must be unique: ${name} duplicated`, - ); - assertType(type, value, name); - - typesAndValues[name] = { type, value }; - - // CRUCIAL: here we're creating the update functions that can change the - // values of the governed contract's parameters. We'll return the updateFns - // to our caller. They must handle them carefully to ensure that they end up - // in appropriate hands. - updateFns[`update${name}`] = newValue => { - assertType(type, newValue, name); - typesAndValues[name].value = newValue; - - publication.updateState({ name, type, value }); - return newValue; - }; - }); - - const makeDescription = name => ({ - name, - type: typesAndValues[name].type, - value: typesAndValues[name].value, - }); - const getParams = () => { - /** @type {Record} */ - const descriptions = {}; - Object.getOwnPropertyNames(typesAndValues).forEach(name => { - descriptions[name] = makeDescription(name); - }); - return harden(descriptions); - }; - const getParam = name => harden(makeDescription(name)); - - // CRUCIAL: Contracts that call buildParamManager should only export the - // resulting paramManager to their creatorFacet, where it will be picked up by - // contractGovernor. The getParams method can be shared widely. - return Far('param manager', { - getParams, - getSubscription: () => subscription, - getParam, - ...updateFns, - }); -}; - -harden(ParamType); -harden(buildParamManager); -harden(assertType); -export { ParamType, buildParamManager, assertType }; diff --git a/packages/governance/src/question.js b/packages/governance/src/question.js index aa533e82d83..8d30743d346 100644 --- a/packages/governance/src/question.js +++ b/packages/governance/src/question.js @@ -5,7 +5,7 @@ import { sameStructure } from '@agoric/same-structure'; import { makeHandle } from '@agoric/zoe/src/makeHandle.js'; import { Nat } from '@agoric/nat'; -import { assertType, ParamType } from './paramManager.js'; +import { makeAssertInstance } from './paramGovernance/assertions.js'; const { details: X, quote: q } = assert; @@ -76,7 +76,8 @@ const looksLikeParamChangeIssue = issue => { X`Issue ("${issue}") must be a record with paramSpec: anObject`, ); assert(issue && issue.proposedValue); - assertType(ParamType.INSTANCE, issue.contract, 'contract'); + const assertInstance = makeAssertInstance('contract'); + assertInstance(issue.contract); }; /** @type {LooksLikeIssueForType} */ diff --git a/packages/governance/src/types.js b/packages/governance/src/types.js index 2d4ca245d18..368fb4400c3 100644 --- a/packages/governance/src/types.js +++ b/packages/governance/src/types.js @@ -20,8 +20,8 @@ */ /** - * @typedef { 'amount' | 'brand' | 'instance' | 'installation' | 'nat' | - * 'ratio' | 'string' | 'unknown' } ParamType + * @typedef { 'amount' | 'brand' | 'instance' | 'installation' | 'invitation' | + * 'nat' | 'ratio' | 'string' | 'unknown' } ParamType */ /** @@ -40,7 +40,7 @@ /** * @template T - * @typedef {{ type: T, name: string }} ParamRecord + * @typedef {{ type: T }} ParamRecord */ /** @@ -48,6 +48,7 @@ * ParamRecord<'brand'> & { value: Brand } | * ParamRecord<'installation'> & { value: Installation } | * ParamRecord<'instance'> & { value: Instance } | + * ParamRecord<'invitation'> & { value: Amount } | * ParamRecord<'nat'> & { value: bigint } | * ParamRecord<'ratio'> & { value: Ratio } | * ParamRecord<'string'> & { value: string } | @@ -390,17 +391,20 @@ * @returns {Record} */ -/** - * @callback GetParam - getParam() retrieves a description of a parameter under - * governance. - * @param {string} name - * @returns {ParamDescription} - */ - /** * @typedef {Object} ParamManagerBase * @property {GetParams} getParams - * @property {GetParam} getParam + * @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 + * @property {() => ParamDescriptions} getParamList + * @property {(name:string) => Invitation} getInternalParamValue * @property {() => Subscription} getSubscription */ @@ -511,8 +515,15 @@ * @property {VoteOnParamChange} getContractGovernor * @property {GetParams} getGovernedParams - get descriptions of * all the governed parameters - * @property {(name: string) => ParamValue} getParamValue - get the value of a - * named parameter + * @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 */ /** @@ -541,13 +552,21 @@ * @typedef {Object} ParamGovernorBundle * @property {WrapPublicFacet} wrapPublicFacet * @property {WrapCreatorFacet} wrapCreatorFacet - * @property {(name: string) => ParamValue} getParamValue + * @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 */ /** * @callback HandleParamGovernance * @param {ContractFacet} zcf - * @param {ParamDescriptions} governedParamsTemplate + * @param {ParamManagerFull} paramManager * @returns {ParamGovernorBundle} */ @@ -668,3 +687,19 @@ * * @param {ParamChangeIssueDetails} details */ + +/** + * @typedef {Object} ParamManagerBuilder + * @property {(name: string, value: Amount) => ParamManagerBuilder} addAmount + * @property {(name: string, value: Amount) => ParamManagerBuilder} addBrandedAmount + * @property {(name: string, value: Brand) => ParamManagerBuilder} addBrand + * @property {(name: string, value: Installation) => ParamManagerBuilder} addInstallation + * @property {(name: string, value: Instance) => ParamManagerBuilder} addInstance + * @property {(name: string, value: Invitation) => ERef} addInvitation + * @property {(name: string, value: bigint) => ParamManagerBuilder} addNat + * @property {(name: string, value: Ratio) => ParamManagerBuilder} addRatio + * @property {(name: string, value: Ratio) => ParamManagerBuilder} addBrandedRatio + * @property {(name: string, value: string) => ParamManagerBuilder} addString + * @property {(name: string, value: any) => ParamManagerBuilder} addUnknown + * @property {() => ParamManagerFull} build + */ diff --git a/packages/governance/test/swingsetTests/contractGovernor/bootstrap.js b/packages/governance/test/swingsetTests/contractGovernor/bootstrap.js index 72de7598e8f..818f2ca64a9 100644 --- a/packages/governance/test/swingsetTests/contractGovernor/bootstrap.js +++ b/packages/governance/test/swingsetTests/contractGovernor/bootstrap.js @@ -4,7 +4,7 @@ import { E } from '@agoric/eventual-send'; import { Far } from '@agoric/marshal'; import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; import { observeIteration } from '@agoric/notifier'; -import { makeGovernedNat } from '../../../src/paramMakers.js'; +import { makeGovernedNat } from '../../../src/paramGovernance/paramMakers.js'; import { MALLEABLE_NUMBER } from './governedContract.js'; const { quote: q } = assert; @@ -145,7 +145,7 @@ const checkContractState = async (zoe, contractInstanceP, log) => { paramValues = await E(contractPublic).getGovernedParams(); const malleableNumber = paramValues.MalleableNumber; - log(`current value of ${malleableNumber.name} is ${malleableNumber.value}`); + log(`current value of MalleableNumber is ${malleableNumber.value}`); }; const makeBootstrap = (argv, cb, vatPowers) => async (vats, devices) => { @@ -172,7 +172,9 @@ const makeBootstrap = (argv, cb, vatPowers) => async (vats, devices) => { governed: { issuerKeywordRecord: {}, terms: { - main: [makeGovernedNat(MALLEABLE_NUMBER, 602214090000000000000000n)], + main: { + [MALLEABLE_NUMBER]: makeGovernedNat(602214090000000000000000n), + }, }, }, }; diff --git a/packages/governance/test/swingsetTests/contractGovernor/governedContract.js b/packages/governance/test/swingsetTests/contractGovernor/governedContract.js index a8b3127e40e..4e7f0515c96 100644 --- a/packages/governance/test/swingsetTests/contractGovernor/governedContract.js +++ b/packages/governance/test/swingsetTests/contractGovernor/governedContract.js @@ -1,15 +1,28 @@ // @ts-check import { handleParamGovernance } from '../../../src/contractHelper.js'; +import { + ParamType, + makeParamManagerBuilder, +} from '../../../src/paramGovernance/paramManager.js'; const MALLEABLE_NUMBER = 'MalleableNumber'; /** @type {ContractStartFn} */ const start = async zcf => { - const { main: initialValue } = zcf.getTerms(); + const { + main: { [MALLEABLE_NUMBER]: numberParam }, + } = zcf.getTerms(); + assert( + numberParam.type === ParamType.NAT, + `${MALLEABLE_NUMBER} Should be a Nat: ${numberParam.type}`, + ); + const { wrapPublicFacet, wrapCreatorFacet } = handleParamGovernance( zcf, - harden(initialValue), + makeParamManagerBuilder() + .addNat(MALLEABLE_NUMBER, numberParam.value) + .build(), ); return { diff --git a/packages/governance/test/swingsetTests/contractGovernor/test-governor.js b/packages/governance/test/swingsetTests/contractGovernor/test-governor.js index 6acc28ab569..51eaff7a9f4 100644 --- a/packages/governance/test/swingsetTests/contractGovernor/test-governor.js +++ b/packages/governance/test/swingsetTests/contractGovernor/test-governor.js @@ -98,6 +98,7 @@ const expectedcontractGovernorStartLog = [ 'updated to "[299792458n]"', 'MalleableNumber was changed to "[602214090000000000000000n]"', 'current value of MalleableNumber is 299792458', + 'MalleableNumber was changed to "[299792458n]"', 'Voter Alice validated all the things', ]; diff --git a/packages/governance/test/swingsetTests/contractGovernor/vat-voter.js b/packages/governance/test/swingsetTests/contractGovernor/vat-voter.js index bbb3cdc3057..cdf9f1a1629 100644 --- a/packages/governance/test/swingsetTests/contractGovernor/vat-voter.js +++ b/packages/governance/test/swingsetTests/contractGovernor/vat-voter.js @@ -10,6 +10,7 @@ import { validateQuestionDetails, assertBallotConcernsQuestion, } from '../../../src/index.js'; +import { MALLEABLE_NUMBER } from './governedContract'; const { details: X, quote: q } = assert; @@ -45,7 +46,6 @@ const build = async (log, zoe) => { ); const [ - governedParam, questionDetails, electorateInstallation, voteCounterInstallation, @@ -54,7 +54,6 @@ const build = async (log, zoe) => { validatedQuestion, contractGovernance, ] = await Promise.all([ - E.get(E(zoe).getTerms(governedInstance)).main, E(E(zoe).getPublicFacet(counterInstance)).getDetails(), E(zoe).getInstallationForInstance(electorateInstance), E(zoe).getInstallationForInstance(counterInstance), @@ -64,7 +63,7 @@ const build = async (log, zoe) => { contractGovernanceP, ]); - assertBallotConcernsQuestion(governedParam[0].name, questionDetails); + assertBallotConcernsQuestion(MALLEABLE_NUMBER, questionDetails); assert(installations.binaryVoteCounter === voteCounterInstallation); assert(installations.governedContract === governedInstallation); assert(installations.contractGovernor === governorInstallation); diff --git a/packages/governance/test/unitTests/test-buildParamManager.js b/packages/governance/test/unitTests/test-buildParamManager.js new file mode 100644 index 00000000000..d0097980d63 --- /dev/null +++ b/packages/governance/test/unitTests/test-buildParamManager.js @@ -0,0 +1,370 @@ +// @ts-check + +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { makeIssuerKit, AmountMath, AssetKind } from '@agoric/ertp'; +import { makeRatio } from '@agoric/zoe/src/contractSupport/index.js'; +import { setupZCFTest } from '@agoric/zoe/test/unitTests/zcf/setupZcfTest.js'; +import { E } from '@agoric/eventual-send'; +import { Far } from '@agoric/marshal'; + +import { makeHandle } from '@agoric/zoe/src/makeHandle.js'; +import { + ParamType, + makeParamManagerBuilder, +} from '../../src/paramGovernance/paramManager.js'; + +test('two parameters', t => { + const drachmaKit = makeIssuerKit('drachma'); + + const drachmaBrand = drachmaKit.brand; + const paramManager = makeParamManagerBuilder() + .addBrand('Currency', drachmaBrand) + .addAmount('Amt', AmountMath.make(drachmaBrand, 37n)) + .build(); + + t.is(paramManager.getBrand('Currency'), drachmaBrand); + t.deepEqual( + paramManager.getAmount('Amt'), + AmountMath.make(drachmaBrand, 37n), + ); +}); + +test('getParams', t => { + const drachmaKit = makeIssuerKit('drachma'); + + const drachmaBrand = drachmaKit.brand; + const drachmas = AmountMath.make(drachmaBrand, 37n); + const paramManager = makeParamManagerBuilder() + .addBrand('Currency', drachmaBrand) + .addAmount('Amt', drachmas) + .build(); + + t.deepEqual( + paramManager.getParams(), + harden({ + Currency: { + type: ParamType.BRAND, + value: drachmaBrand, + }, + Amt: { + type: ParamType.AMOUNT, + value: drachmas, + }, + }), + ); +}); + +test('params duplicate entry', async t => { + const stuffKey = 'Stuff'; + const { brand: stiltonBrand } = makeIssuerKit('stilton', AssetKind.SET); + t.throws( + () => + makeParamManagerBuilder() + .addNat(stuffKey, 37n) + .addUnknown(stuffKey, stiltonBrand) + .build(), + { + message: `"Parameter Name" already registered: "Stuff"`, + }, + ); +}); + +test('Amount', async t => { + const { brand } = makeIssuerKit('floor wax'); + const { brand: brand2 } = makeIssuerKit('dessertTopping'); + const paramManager = makeParamManagerBuilder() + .addAmount('Shimmer', AmountMath.make(brand, 250n)) + .build(); + t.deepEqual(paramManager.getAmount('Shimmer'), AmountMath.make(brand, 250n)); + + // @ts-ignore updateShimmer is a generated name + paramManager.updateShimmer(AmountMath.make(brand2, 300n)); + t.deepEqual(paramManager.getAmount('Shimmer'), AmountMath.make(brand2, 300n)); + + // @ts-ignore updateShimmer is a generated name + t.throws(() => paramManager.updateShimmer('fear,loathing'), { + message: 'Expected an Amount for Shimmer, got "fear,loathing"', + }); + + t.throws(() => paramManager.getNat('Shimmer'), { + message: '"Shimmer" is not "nat"', + }); +}); + +test('Branded Amount', async t => { + const { brand: floorBrand } = makeIssuerKit('floor wax'); + const { brand: dessertBrand } = makeIssuerKit('dessertTopping'); + const paramManager = makeParamManagerBuilder() + .addBrandedAmount('Shimmer', AmountMath.make(floorBrand, 2n)) + .build(); + t.deepEqual( + paramManager.getAmount('Shimmer'), + AmountMath.make(floorBrand, 2n), + ); + + // @ts-ignore updateShimmer is a generated name + paramManager.updateShimmer(AmountMath.make(floorBrand, 5n)); + t.deepEqual( + paramManager.getAmount('Shimmer'), + AmountMath.make(floorBrand, 5n), + ); + + // @ts-ignore updateShimmer is a generated name + t.throws( + // @ts-ignore updateShimmer is a generated name + () => paramManager.updateShimmer(AmountMath.make(dessertBrand, 20n)), + { + message: + 'The brand in the allegedAmount {"brand":"[Alleged: dessertTopping brand]","value":"[20n]"} in \'coerce\' didn\'t match the specified brand "[Alleged: floor wax brand]".', + }, + ); + + // @ts-ignore updateCandle is a generated name + t.throws(() => paramManager.updateShimmer('fear,loathing'), { + message: 'Expected an Amount for Shimmer, got "fear,loathing"', + }); + + t.throws(() => paramManager.getString('Shimmer'), { + message: '"Shimmer" is not "string"', + }); +}); + +test('params one installation', async t => { + const installationKey = 'Installation'; + // this is sufficient for the current type check. When we add + // isInstallation() (#3344), we'll need to make a mockZoe. + const installationHandle = Far('fake Installation', { + getBundle: () => ({ obfuscated: 42 }), + }); + + const paramManager = makeParamManagerBuilder() + .addInstallation('Installation', installationHandle) + .build(); + + t.deepEqual( + paramManager.getInstallation(installationKey), + installationHandle, + ); + t.throws( + // @ts-ignore updateInstallation is a generated name + () => paramManager.updateInstallation(18.1), + { + message: 'value for "Installation" must be an Installation, was 18.1', + }, + 'value should be an installation', + ); + const handle2 = Far('another fake Installation', { + getBundle: () => ({ condensed: '() => {})' }), + }); + // @ts-ignore updateInstallation is a generated name + paramManager.updateInstallation(handle2); + t.deepEqual(paramManager.getInstallation(installationKey), handle2); + + t.throws(() => paramManager.getNat('Installation'), { + message: '"Installation" is not "nat"', + }); + + t.deepEqual( + paramManager.getParams(), + harden({ + Installation: { + type: ParamType.INSTALLATION, + value: handle2, + }, + }), + ); +}); + +test('params one instance', async t => { + const instanceKey = 'Instance'; + // this is sufficient for the current type check. When we add + // isInstallation() (#3344), we'll need to make a mockZoe. + const instanceHandle = makeHandle(instanceKey); + + const paramManager = makeParamManagerBuilder() + .addInstance(instanceKey, instanceHandle) + .build(); + + t.deepEqual(paramManager.getInstance(instanceKey), instanceHandle); + t.throws( + // @ts-ignore updateInstance is a generated name + () => paramManager.updateInstance(18.1), + { + message: 'value for "Instance" must be an Instance, was 18.1', + }, + 'value should be an instance', + ); + const handle2 = makeHandle(instanceKey); + // @ts-ignore updateInstance is a generated name + paramManager.updateInstance(handle2); + t.deepEqual(paramManager.getInstance(instanceKey), handle2); + + t.deepEqual( + paramManager.getParams(), + harden({ + Instance: { + type: ParamType.INSTANCE, + value: handle2, + }, + }), + ); +}); + +test('Invitation', async t => { + const drachmaKit = makeIssuerKit('drachma'); + const terms = harden({ + mmr: makeRatio(150n, drachmaKit.brand), + }); + + const issuerKeywordRecord = harden({ + Ignore: drachmaKit.issuer, + }); + + const { instance, zoe } = await setupZCFTest(issuerKeywordRecord, terms); + + const invitation = await E(E(zoe).getPublicFacet(instance)).makeInvitation(); + + const invitationAmount = await E(E(zoe).getInvitationIssuer()).getAmountOf( + invitation, + ); + + const drachmaBrand = drachmaKit.brand; + const drachmaAmount = AmountMath.make(drachmaBrand, 37n); + const paramManagerBuilder = makeParamManagerBuilder(zoe) + .addBrand('Currency', drachmaBrand) + .addAmount('Amt', drachmaAmount); + // addInvitation is async, so it can't be part of the cascade. + await paramManagerBuilder.addInvitation('Invite', invitation); + const paramManager = paramManagerBuilder.build(); + + t.is(paramManager.getBrand('Currency'), drachmaBrand); + t.is(paramManager.getAmount('Amt'), drachmaAmount); + const invitationActualAmount = paramManager.getInvitationAmount('Invite') + .value; + t.is(invitationActualAmount, invitationAmount.value); + // @ts-ignore invitationActualAmount's type is unknown + t.is(invitationActualAmount[0].description, 'simple'); + + t.is(await paramManager.getInternalParamValue('Invite'), invitation); + + t.deepEqual( + paramManager.getParams(), + harden({ + Amt: { + type: ParamType.AMOUNT, + value: drachmaAmount, + }, + Currency: { + type: ParamType.BRAND, + value: drachmaBrand, + }, + Invite: { + type: ParamType.INVITATION, + value: invitationAmount, + }, + }), + ); +}); + +test('two Nats', async t => { + const paramManager = makeParamManagerBuilder() + .addNat('Acres', 50n) + .addNat('SpeedLimit', 299_792_458n) + .build(); + + t.is(paramManager.getNat('Acres'), 50n); + t.is(paramManager.getNat('SpeedLimit'), 299_792_458n); + + // @ts-ignore updateSpeedLimit is a generated name + t.throws(() => paramManager.updateSpeedLimit(300000000), { + message: '300000000 must be a bigint', + }); + + // @ts-ignore updateSpeedLimit is a generated name + t.throws(() => paramManager.updateSpeedLimit(-37n), { + message: '-37 is negative', + }); +}); + +test('Ratio', async t => { + const unitlessBrand = makeIssuerKit('unitless').brand; + + const ratio = makeRatio(16180n, unitlessBrand, 10_000n); + const paramManager = makeParamManagerBuilder() + .addNat('Acres', 50n) + .addRatio('GoldenRatio', ratio) + .build(); + t.is(paramManager.getRatio('GoldenRatio'), ratio); + + const morePrecise = makeRatio(1618033n, unitlessBrand, 1_000_000n); + // @ts-ignore updateGoldenRatio is a generated name + paramManager.updateGoldenRatio(morePrecise); + t.is(paramManager.getRatio('GoldenRatio'), morePrecise); + // @ts-ignore updateGoldenRatio is a generated name + t.throws(() => paramManager.updateGoldenRatio(300000000), { + message: '"ratio" 300000000 must be a pass-by-copy record, not "number"', + }); +}); + +test('Branded Ratio', async t => { + const unitlessBrand = makeIssuerKit('unitless').brand; + + const ratio = makeRatio(16180n, unitlessBrand, 10_000n); + const paramManager = makeParamManagerBuilder() + .addBrandedRatio('GoldenRatio', ratio) + .build(); + t.is(paramManager.getRatio('GoldenRatio'), ratio); + + const morePrecise = makeRatio(1618033n, unitlessBrand, 1_000_000n); + // @ts-ignore updateGoldenRatio is a generated name + paramManager.updateGoldenRatio(morePrecise); + t.is(paramManager.getRatio('GoldenRatio'), morePrecise); + + const anotherBrand = makeIssuerKit('arbitrary').brand; + + // @ts-ignore updateGoldenRatio is a generated name + t.throws( + () => + // @ts-ignore updateGoldenRatio is a generated name + paramManager.updateGoldenRatio(makeRatio(16180n, anotherBrand, 10_000n)), + { + message: + 'Numerator brand for "GoldenRatio" must be "[Alleged: unitless brand]"', + }, + ); +}); + +test('Strings', async t => { + const paramManager = makeParamManagerBuilder() + .addNat('Acres', 50n) + .addString('OurWeapons', 'fear') + .build(); + t.is(paramManager.getString('OurWeapons'), 'fear'); + + // @ts-ignore updateOurWeapons is a generated name + paramManager.updateOurWeapons('fear,surprise'); + t.is(paramManager.getString('OurWeapons'), 'fear,surprise'); + // @ts-ignore updateOurWeapons is a generated name + t.throws(() => paramManager.updateOurWeapons(300000000), { + message: '300000000 must be a string', + }); + + t.throws(() => paramManager.getNat('OurWeapons'), { + message: '"OurWeapons" is not "nat"', + }); +}); + +test('Unknown', async t => { + const paramManager = makeParamManagerBuilder() + .addString('Label', 'birthday') + .addUnknown('Surprise', 'party') + .build(); + t.is(paramManager.getUnknown('Surprise'), 'party'); + + // @ts-ignore updateSurprise is a generated name + paramManager.updateSurprise('gift'); + t.is(paramManager.getUnknown('Surprise'), 'gift'); + // @ts-ignore updateSurprise is a generated name + paramManager.updateSurprise(['gift', 'party']); + t.deepEqual(paramManager.getUnknown('Surprise'), ['gift', 'party']); +}); diff --git a/packages/governance/test/unitTests/test-param-manager.js b/packages/governance/test/unitTests/test-param-manager.js deleted file mode 100644 index 0beaaff116b..00000000000 --- a/packages/governance/test/unitTests/test-param-manager.js +++ /dev/null @@ -1,367 +0,0 @@ -// @ts-check - -import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import '@agoric/zoe/exported.js'; -import { AmountMath, AssetKind, makeIssuerKit } from '@agoric/ertp'; -import { makeRatio } from '@agoric/zoe/src/contractSupport/index.js'; - -import { makeHandle } from '@agoric/zoe/src/makeHandle.js'; -import { Far } from '@agoric/marshal'; -import { buildParamManager } from '../../src/paramManager.js'; -import { makeParamChangePositions } from '../../src/governParam.js'; -import { - makeGovernedString, - makeGovernedAmount, - makeGovernedNat, - makeGovernedRatio, - makeGovernedBrand, - makeGovernedInstance, - makeGovernedInstallation, - makeGovernedUnknown, -} from '../../src/paramMakers.js'; - -const BASIS_POINTS = 10_000n; - -test('params one Nat', async t => { - const numberKey = 'Number'; - const numberDescription = makeGovernedNat(numberKey, 13n); - const { getParam, updateNumber } = buildParamManager([numberDescription]); - t.deepEqual(getParam(numberKey), numberDescription); - updateNumber(42n); - t.deepEqual(getParam(numberKey).value, 42n); - - t.throws( - () => updateNumber(18.1), - { - message: '18.1 must be a bigint', - }, - 'value should be a nat', - ); - t.throws( - () => updateNumber(13), - { - message: '13 must be a bigint', - }, - 'must be bigint', - ); -}); - -test('params one String', async t => { - const stringKey = 'String'; - const stringDescription = makeGovernedString(stringKey, 'foo'); - const { getParam, updateString } = buildParamManager([stringDescription]); - t.deepEqual(getParam(stringKey), stringDescription); - updateString('bar'); - t.deepEqual(getParam(stringKey).value, 'bar'); - - t.throws( - () => updateString(18.1), - { - message: '18.1 must be a string', - }, - 'value should be a string', - ); -}); - -test('params one Amount', async t => { - const amountKey = 'Amount'; - const { brand } = makeIssuerKit('roses', AssetKind.SET); - const emptyAmount = AmountMath.makeEmpty(brand); - const amountDescription = makeGovernedAmount(amountKey, emptyAmount); - const { getParam, updateAmount } = buildParamManager([amountDescription]); - t.deepEqual(getParam(amountKey), amountDescription); - updateAmount(AmountMath.make(brand, harden([13]))); - t.deepEqual(getParam(amountKey).value, AmountMath.make(brand, harden([13]))); - - t.throws( - () => updateAmount(18.1), - { - message: '"brand" "[undefined]" must be a remotable, not "undefined"', - }, - 'value should be a amount', - ); -}); - -test('params one BigInt', async t => { - const bigintKey = 'Bigint'; - const bigIntDescription = makeGovernedNat(bigintKey, 314159n); - const { getParam, updateBigint } = buildParamManager([bigIntDescription]); - t.deepEqual(getParam(bigintKey), bigIntDescription); - updateBigint(271828182845904523536n); - t.deepEqual(getParam(bigintKey).value, 271828182845904523536n); - - t.throws( - () => updateBigint(18.1), - { - message: '18.1 must be a bigint', - }, - 'value should be a bigint', - ); - t.throws( - () => updateBigint(-1000n), - { - message: '-1000 is negative', - }, - 'NAT params must be positive', - ); -}); - -test('params one ratio', async t => { - const ratioKey = 'Ratio'; - const { brand } = makeIssuerKit('roses', AssetKind.SET); - const ratioDescription = makeGovernedRatio(ratioKey, makeRatio(7n, brand)); - - const { getParam, getParams, updateRatio } = buildParamManager([ - ratioDescription, - ]); - // t.deepEqual(getParams()[ratioKey], ratioDescription); - t.deepEqual(getParam(ratioKey), ratioDescription); - updateRatio(makeRatio(701n, brand, BASIS_POINTS)); - t.deepEqual( - getParams()[ratioKey].value, - makeRatio(701n, brand, BASIS_POINTS), - ); - - t.throws( - () => updateRatio(18.1), - { - message: '"ratio" 18.1 must be a pass-by-copy record, not "number"', - }, - 'value should be a ratio', - ); -}); - -test('params one brand', async t => { - const brandKey = 'Brand'; - const { brand: roseBrand } = makeIssuerKit('roses', AssetKind.SET); - const { brand: thornBrand } = makeIssuerKit('thorns'); - const brandDescription = makeGovernedBrand(brandKey, roseBrand); - const { getParam, updateBrand } = buildParamManager([brandDescription]); - t.deepEqual(getParam(brandKey), brandDescription); - updateBrand(thornBrand); - t.deepEqual(getParam(brandKey).value, thornBrand); - - t.throws( - () => updateBrand(18.1), - { - message: 'value for "Brand" must be a brand, was 18.1', - }, - 'value should be a brand', - ); -}); - -test('params one unknown', async t => { - const stuffKey = 'Stuff'; - const { brand: stiltonBrand } = makeIssuerKit('stilton', AssetKind.SET); - const stuffDescription = makeGovernedUnknown(stuffKey, stiltonBrand); - const { getParam, updateStuff } = buildParamManager([stuffDescription]); - t.deepEqual(getParam(stuffKey), stuffDescription); - updateStuff(18.1); - t.deepEqual(getParam(stuffKey).value, 18.1); -}); - -test('params one instance', async t => { - const instanceKey = 'Instance'; - // this is sufficient for the current type check. When we add - // isInstance() (#3344), we'll need to make a mockZoe. - const instanceHandle = makeHandle('Instance'); - const instanceDescription = makeGovernedInstance(instanceKey, instanceHandle); - const { getParam, updateInstance } = buildParamManager([instanceDescription]); - t.deepEqual(getParam(instanceKey), instanceDescription); - t.throws( - () => updateInstance(18.1), - { - message: 'value for "Instance" must be an Instance, was 18.1', - }, - 'value should be an Instance', - ); - const handle2 = makeHandle('another Instance'); - updateInstance(handle2); - t.deepEqual(getParam(instanceKey).value, handle2); -}); - -test('params one installation', async t => { - const installationKey = 'Installation'; - // this is sufficient for the current type check. When we add - // isInstallation() (#3344), we'll need to make a mockZoe. - const installationHandle = Far('fake Installation', { - getBundle: () => ({ obfuscated: 42 }), - }); - const installationDescription = makeGovernedInstallation( - installationKey, - installationHandle, - ); - const { getParam, updateInstallation } = buildParamManager([ - installationDescription, - ]); - t.deepEqual(getParam(installationKey), installationDescription); - t.throws( - () => updateInstallation(18.1), - { - message: 'value for "Installation" must be an Installation, was 18.1', - }, - 'value should be an installation', - ); - const handle2 = Far('another fake Installation', { - getBundle: () => ({ condensed: '() => {})' }), - }); - updateInstallation(handle2); - t.deepEqual(getParam(installationKey).value, handle2); -}); - -test('params duplicate entry', async t => { - const stuffKey = 'Stuff'; - const { brand: stiltonBrand } = makeIssuerKit('stilton', AssetKind.SET); - t.throws( - () => - buildParamManager([ - makeGovernedNat(stuffKey, 37n), - makeGovernedUnknown(stuffKey, stiltonBrand), - ]), - { - message: `each parameter name must be unique: "Stuff" duplicated`, - }, - ); -}); - -test('params unrecognized type', async t => { - const stuffKey = 'Stuff'; - const stuffDescription = { - name: stuffKey, - value: 'It was the best of times, it was the worst of times', - type: 'quote', - }; - // @ts-ignore illegal value for testing - t.throws(() => buildParamManager([stuffDescription]), { - message: 'unrecognized type "quote"', - }); -}); - -test('params multiple values', t => { - const stuffKey = 'Stuff'; - const natKey = 'Nat'; - const { brand: parmesanBrand } = makeIssuerKit('parmesan', AssetKind.SET); - const cheeseDescription = makeGovernedUnknown(stuffKey, parmesanBrand); - const constantDescription = makeGovernedNat( - natKey, - 602214076000000000000000n, - ); - const { getParams, getParam, updateNat, updateStuff } = buildParamManager([ - cheeseDescription, - constantDescription, - ]); - t.deepEqual(getParam(stuffKey), cheeseDescription); - updateStuff(18.1); - const floatDescription = makeGovernedUnknown(stuffKey, 18.1); - t.deepEqual(getParam(stuffKey), floatDescription); - t.deepEqual(getParam(natKey), constantDescription); - t.deepEqual(getParams(), { - Nat: constantDescription, - Stuff: floatDescription, - }); - updateNat(299792458n); - t.deepEqual(getParam(natKey).value, 299792458n); -}); - -const positive = (name, val) => { - return { changeParam: name, proposedValue: val }; -}; - -const negative = name => { - return { noChange: name }; -}; - -test('positions amount', t => { - const amountSpec = { parameterName: 'amount', key: 'something' }; - const { brand } = makeIssuerKit('roses', AssetKind.SET); - const amount = AmountMath.makeEmpty(brand); - - const positions = makeParamChangePositions(amountSpec, amount); - t.deepEqual(positions.positive, positive(amountSpec, amount)); - t.deepEqual(positions.negative, negative(amountSpec)); - t.notDeepEqual( - positions.positive, - positive(AmountMath.make(brand, harden([1]))), - ); -}); - -test('positions brand', t => { - const brandSpec = { parameterName: 'brand', key: 'params' }; - const { brand: roseBrand } = makeIssuerKit('roses', AssetKind.SET); - const { brand: thornBrand } = makeIssuerKit('thorns', AssetKind.SET); - - const positions = makeParamChangePositions(brandSpec, roseBrand); - t.deepEqual(positions.positive, positive(brandSpec, roseBrand)); - t.deepEqual(positions.negative, negative(brandSpec)); - t.not(positions.positive, positive(brandSpec, thornBrand)); -}); - -test('positions instance', t => { - const instanceSpec = { parameterName: 'instance', key: 'something' }; - // this is sufficient for the current type check. When we add - // isInstallation() (#3344), we'll need to make a mockZoe. - const instanceHandle = makeHandle('Instance'); - - const positions = makeParamChangePositions(instanceSpec, instanceHandle); - t.deepEqual(positions.positive, positive(instanceSpec, instanceHandle)); - t.deepEqual(positions.negative, negative(instanceSpec)); - t.not(positions.positive, positive(instanceSpec, makeHandle('Instance'))); -}); - -test('positions Installation', t => { - const installationSpec = { parameterName: 'installation', key: 'something' }; - // this is sufficient for the current type check. When we add - // isInstallation() (#3344), we'll need to make a mockZoe. - const installationHandle = makeHandle('Installation'); - - const positions = makeParamChangePositions( - installationSpec, - installationHandle, - ); - t.deepEqual( - positions.positive, - positive(installationSpec, installationHandle), - ); - t.deepEqual(positions.negative, negative(installationSpec)); - t.not( - positions.positive, - positive(installationSpec, makeHandle('Installation')), - ); -}); - -test('positions Nat', t => { - const natSpec = { parameterName: 'nat', key: 'something' }; - const nat = 3n; - - const positions = makeParamChangePositions(natSpec, nat); - t.deepEqual(positions.positive, positive(natSpec, nat)); - t.deepEqual(positions.negative, negative(natSpec)); - t.notDeepEqual(positions.positive, positive(natSpec, 4n)); -}); - -test('positions Ratio', t => { - const ratioSpec = { parameterName: 'ratio', key: 'something' }; - const { brand } = makeIssuerKit('elo', AssetKind.NAT); - const ratio = makeRatio(2500n, brand, 2400n); - - const positions = makeParamChangePositions(ratioSpec, ratio); - t.deepEqual(positions.positive, positive(ratioSpec, ratio)); - t.deepEqual(positions.negative, negative(ratioSpec)); - t.notDeepEqual( - positions.positive, - positive(ratioSpec, makeRatio(2500n, brand, 2200n)), - ); -}); - -test('positions string', t => { - const stringSpec = { parameterName: 'string', key: 'something' }; - const string = 'When in the course'; - - const positions = makeParamChangePositions(stringSpec, string); - t.deepEqual(positions.positive, positive(stringSpec, string)); - t.deepEqual(positions.negative, negative(stringSpec)); - t.notDeepEqual( - positions.positive, - positive(stringSpec, 'We hold these truths'), - ); -}); diff --git a/packages/governance/test/unitTests/test-paramChangePositions.js b/packages/governance/test/unitTests/test-paramChangePositions.js new file mode 100644 index 00000000000..d81c3632f7d --- /dev/null +++ b/packages/governance/test/unitTests/test-paramChangePositions.js @@ -0,0 +1,112 @@ +// @ts-check + +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import '@agoric/zoe/exported.js'; +import { AmountMath, AssetKind, makeIssuerKit } from '@agoric/ertp'; +import { makeRatio } from '@agoric/zoe/src/contractSupport/index.js'; + +import { makeHandle } from '@agoric/zoe/src/makeHandle.js'; +import { makeParamChangePositions } from '../../src/paramGovernance/governParam.js'; + +const positive = (name, val) => { + return { changeParam: name, proposedValue: val }; +}; + +const negative = name => { + return { noChange: name }; +}; + +test('positions amount', t => { + const amountSpec = { parameterName: 'amount', key: 'something' }; + const { brand } = makeIssuerKit('roses', AssetKind.SET); + const amount = AmountMath.makeEmpty(brand); + + const positions = makeParamChangePositions(amountSpec, amount); + t.deepEqual(positions.positive, positive(amountSpec, amount)); + t.deepEqual(positions.negative, negative(amountSpec)); + t.notDeepEqual( + positions.positive, + positive(AmountMath.make(brand, harden([1]))), + ); +}); + +test('positions brand', t => { + const brandSpec = { parameterName: 'brand', key: 'params' }; + const { brand: roseBrand } = makeIssuerKit('roses', AssetKind.SET); + const { brand: thornBrand } = makeIssuerKit('thorns', AssetKind.SET); + + const positions = makeParamChangePositions(brandSpec, roseBrand); + t.deepEqual(positions.positive, positive(brandSpec, roseBrand)); + t.deepEqual(positions.negative, negative(brandSpec)); + t.not(positions.positive, positive(brandSpec, thornBrand)); +}); + +test('positions instance', t => { + const instanceSpec = { parameterName: 'instance', key: 'something' }; + // this is sufficient for the current type check. When we add + // isInstallation() (#3344), we'll need to make a mockZoe. + const instanceHandle = makeHandle('Instance'); + + const positions = makeParamChangePositions(instanceSpec, instanceHandle); + t.deepEqual(positions.positive, positive(instanceSpec, instanceHandle)); + t.deepEqual(positions.negative, negative(instanceSpec)); + t.not(positions.positive, positive(instanceSpec, makeHandle('Instance'))); +}); + +test('positions Installation', t => { + const installationSpec = { parameterName: 'installation', key: 'something' }; + // this is sufficient for the current type check. When we add + // isInstallation() (#3344), we'll need to make a mockZoe. + const installationHandle = makeHandle('Installation'); + + const positions = makeParamChangePositions( + installationSpec, + installationHandle, + ); + t.deepEqual( + positions.positive, + positive(installationSpec, installationHandle), + ); + t.deepEqual(positions.negative, negative(installationSpec)); + t.not( + positions.positive, + positive(installationSpec, makeHandle('Installation')), + ); +}); + +test('positions Nat', t => { + const natSpec = { parameterName: 'nat', key: 'something' }; + const nat = 3n; + + const positions = makeParamChangePositions(natSpec, nat); + t.deepEqual(positions.positive, positive(natSpec, nat)); + t.deepEqual(positions.negative, negative(natSpec)); + t.notDeepEqual(positions.positive, positive(natSpec, 4n)); +}); + +test('positions Ratio', t => { + const ratioSpec = { parameterName: 'ratio', key: 'something' }; + const { brand } = makeIssuerKit('elo', AssetKind.NAT); + const ratio = makeRatio(2500n, brand, 2400n); + + const positions = makeParamChangePositions(ratioSpec, ratio); + t.deepEqual(positions.positive, positive(ratioSpec, ratio)); + t.deepEqual(positions.negative, negative(ratioSpec)); + t.notDeepEqual( + positions.positive, + positive(ratioSpec, makeRatio(2500n, brand, 2200n)), + ); +}); + +test('positions string', t => { + const stringSpec = { parameterName: 'string', key: 'something' }; + const string = 'When in the course'; + + const positions = makeParamChangePositions(stringSpec, string); + t.deepEqual(positions.positive, positive(stringSpec, string)); + t.deepEqual(positions.negative, negative(stringSpec)); + t.notDeepEqual( + positions.positive, + positive(stringSpec, 'We hold these truths'), + ); +}); diff --git a/packages/governance/test/unitTests/test-paramGovernance.js b/packages/governance/test/unitTests/test-paramGovernance.js index 5e2bad4220a..eb11568db3d 100644 --- a/packages/governance/test/unitTests/test-paramGovernance.js +++ b/packages/governance/test/unitTests/test-paramGovernance.js @@ -17,9 +17,9 @@ import path from 'path'; import { setupGovernance, makeParamChangePositions, -} from '../../src/governParam.js'; +} from '../../src/paramGovernance/governParam.js'; import { MALLEABLE_NUMBER } from '../swingsetTests/contractGovernor/governedContract.js'; -import { makeGovernedNat } from '../../src/paramMakers.js'; +import { makeGovernedNat } from '../../src/paramGovernance/paramMakers.js'; const filename = new URL(import.meta.url).pathname; const dirname = path.dirname(filename); @@ -45,7 +45,11 @@ test('governParam happy path with fakes', async t => { const governedFacets = await E(zoe).startInstance( governedInstall, {}, - { main: [makeGovernedNat(MALLEABLE_NUMBER, 602214090000000000000000n)] }, + { + main: { + [MALLEABLE_NUMBER]: makeGovernedNat(602214090000000000000000n), + }, + }, ); const Retriever = governedFacets.creatorFacet.getParamMgrRetriever(); @@ -86,7 +90,6 @@ test('governParam happy path with fakes', async t => { t.deepEqual(governedFacets.publicFacet.getGovernedParams(), { MalleableNumber: { - name: MALLEABLE_NUMBER, type: 'nat', value: 25n, }, @@ -105,7 +108,11 @@ test('governParam no votes', async t => { const governedFacets = await E(zoe).startInstance( governedInstall, {}, - { main: [makeGovernedNat(MALLEABLE_NUMBER, 602214090000000000000000n)] }, + { + main: { + [MALLEABLE_NUMBER]: makeGovernedNat(602214090000000000000000n), + }, + }, ); const Retriever = governedFacets.creatorFacet.getParamMgrRetriever(); @@ -152,7 +159,6 @@ test('governParam no votes', async t => { t.deepEqual(governedFacets.publicFacet.getGovernedParams(), { MalleableNumber: { - name: MALLEABLE_NUMBER, type: 'nat', value: 602214090000000000000000n, }, @@ -171,10 +177,14 @@ test('governParam bad update', async t => { const governedFacets = await E(zoe).startInstance( governedInstall, {}, - { main: [makeGovernedNat(MALLEABLE_NUMBER, 602214090000000000000000n)] }, + { + main: { + [MALLEABLE_NUMBER]: makeGovernedNat(602214090000000000000000n), + }, + }, ); const brokenParamMgr = Far('broken ParamMgr', { - getParam: () => { + getNat: () => { return harden({ type: 'nat' }); }, }); @@ -218,14 +228,13 @@ test('governParam bad update', async t => { await t.throwsAsync( outcomeOfUpdate, { - message: 'target has no method "updateMalleableNumber", has ["getParam"]', + message: 'target has no method "updateMalleableNumber", has ["getNat"]', }, 'Expected a throw', ); t.deepEqual(governedFacets.publicFacet.getGovernedParams(), { MalleableNumber: { - name: MALLEABLE_NUMBER, type: 'nat', value: 602214090000000000000000n, }, diff --git a/packages/treasury/bundles/install-on-chain.js b/packages/treasury/bundles/install-on-chain.js index 76e0580ed45..c2c3c488cd9 100644 --- a/packages/treasury/bundles/install-on-chain.js +++ b/packages/treasury/bundles/install-on-chain.js @@ -3,6 +3,9 @@ import { E } from '@agoric/eventual-send'; import '@agoric/governance/exported.js'; +import { Far } from '@agoric/far'; +import { PROTOCOL_FEE_KEY, POOL_FEE_KEY } from '@agoric/zoe/src/contracts/vpool-xyk-amm/params'; + import liquidateBundle from './bundle-liquidateMinimum.js'; import ammBundle from './bundle-amm.js'; import stablecoinBundle from './bundle-stablecoinMachine.js'; @@ -10,8 +13,7 @@ import contractGovernorBundle from './bundle-contractGovernor.js'; import noActionElectorateBundle from './bundle-noActionElectorate.js'; import binaryVoteCounterBundle from './bundle-binaryVoteCounter.js'; import { governedParameterTerms } from '../src/params'; -import { Far } from '@agoric/marshal'; -import { makeInitialValues } from '@agoric/zoe/src/contracts/vpool-xyk-amm/params'; +import { makeGovernedNat } from '@agoric/governance/src/paramGovernance/paramMakers'; const SECONDS_PER_HOUR = 60n * 60n; const SECONDS_PER_DAY = 24n * SECONDS_PER_HOUR; @@ -33,7 +35,10 @@ async function setupAmm( timer, poolFeeBP: poolFee, protocolFeeBP: protocolFee, - main: makeInitialValues(poolFee, protocolFee), + main: { + [PROTOCOL_FEE_KEY]: makeGovernedNat(protocolFee), + [POOL_FEE_KEY]: makeGovernedNat(poolFee), + }, }; const ammGovernorTerms = { diff --git a/packages/treasury/package.json b/packages/treasury/package.json index c1694ff8bc4..6faf15c82ef 100644 --- a/packages/treasury/package.json +++ b/packages/treasury/package.json @@ -36,6 +36,7 @@ "@agoric/deploy-script-support": "^0.6.0", "@agoric/ertp": "^0.13.0", "@agoric/eventual-send": "^0.14.0", + "@agoric/far": "^0.1.1", "@agoric/governance": "^0.4.0", "@agoric/marshal": "^0.5.0", "@agoric/nat": "^4.1.0", diff --git a/packages/treasury/src/params.js b/packages/treasury/src/params.js index 2eb058d9b56..18e96a3957f 100644 --- a/packages/treasury/src/params.js +++ b/packages/treasury/src/params.js @@ -2,11 +2,7 @@ import './types.js'; -import { buildParamManager } from '@agoric/governance'; -import { - makeGovernedNat, - makeGovernedRatio, -} from '@agoric/governance/src/paramMakers.js'; +import { makeParamManagerBuilder } from '@agoric/governance'; export const CHARGING_PERIOD_KEY = 'ChargingPeriod'; export const RECORDING_PERIOD_KEY = 'RecordingPeriod'; @@ -30,12 +26,12 @@ export const governedParameterTerms = { /** @type {MakeVaultParamManager} */ export const makeVaultParamManager = (loanParams, rates) => { // @ts-ignore buildParamManager doesn't describe all the update methods - return buildParamManager([ - makeGovernedNat(CHARGING_PERIOD_KEY, loanParams.chargingPeriod), - makeGovernedNat(RECORDING_PERIOD_KEY, loanParams.recordingPeriod), - makeGovernedRatio(INITIAL_MARGIN_KEY, rates.initialMargin), - makeGovernedRatio(LIQUIDATION_MARGIN_KEY, rates.liquidationMargin), - makeGovernedRatio(INTEREST_RATE_KEY, rates.interestRate), - makeGovernedRatio(LOAN_FEE_KEY, rates.loanFee), - ]); + return makeParamManagerBuilder() + .addNat(CHARGING_PERIOD_KEY, loanParams.chargingPeriod) + .addNat(RECORDING_PERIOD_KEY, loanParams.recordingPeriod) + .addBrandedRatio(INITIAL_MARGIN_KEY, rates.initialMargin) + .addBrandedRatio(LIQUIDATION_MARGIN_KEY, rates.liquidationMargin) + .addBrandedRatio(INTEREST_RATE_KEY, rates.interestRate) + .addBrandedRatio(LOAN_FEE_KEY, rates.loanFee) + .build(); }; diff --git a/packages/treasury/src/stablecoinMachine.js b/packages/treasury/src/stablecoinMachine.js index e8ac92caa67..1ec7e6fd7c2 100644 --- a/packages/treasury/src/stablecoinMachine.js +++ b/packages/treasury/src/stablecoinMachine.js @@ -234,10 +234,16 @@ export async function start(zcf, privateArgs) { return vaultParamManagers.get(paramDesc.collateralBrand).getParams(); }; - const getParamState = paramDesc => { + const getRatioParamState = paramDesc => { return vaultParamManagers .get(paramDesc.collateralBrand) - .getParam(paramDesc.parameterName); + .getRatio(paramDesc.parameterName); + }; + + const getNatParamState = paramDesc => { + return vaultParamManagers + .get(paramDesc.collateralBrand) + .getNat(paramDesc.parameterName); }; const publicFacet = Far('stablecoin public facet', { @@ -248,7 +254,8 @@ export async function start(zcf, privateArgs) { getRunIssuer() { return runIssuer; }, - getParamState, + getNatParamState, + getRatioParamState, getParams, getContractGovernor: () => governorPublic, }); diff --git a/packages/treasury/src/types.js b/packages/treasury/src/types.js index f79823db4f9..10cae21822a 100644 --- a/packages/treasury/src/types.js +++ b/packages/treasury/src/types.js @@ -183,7 +183,6 @@ * @param {InnerVaultManager} manager * @param {ZCFMint} runMint * @param {ERef} priceAuthority - * @param {GetParams} paramManager * @param {Timestamp} startTimeStamp * @returns {VaultKit} */ @@ -221,18 +220,11 @@ * @returns {CalculatorKit} */ -/** - * @typedef {Object} FeeParamManager - * @property {GetParams} getParams - * @property {GetParam} getParam - * @property {(fee: bigint) => void} updateProtocolFee - * @property {(fee: bigint) => void} updatePoolFee - */ - /** * @typedef {Object} VaultParamManager * @property {GetParams} getParams - * @property {GetParam} getParam + * @property {(name: string) => bigint} getNat + * @property {(name: string) => Ratio} getRatio * @property {(period: bigint) => void} updateChargingPeriod * @property {(period: bigint) => void} updateRecordingPeriod * @property {(margin: Ratio) => void} updateInitialMargin @@ -248,12 +240,6 @@ * @returns {VaultParamManager} */ -/** - * @callback MakeFeeParamManager - * @param {AMMFees} ammFees - * @returns {FeeParamManager} - */ - /** * @callback TreasuryLiquidate * @param {ContractFacet} zcf, diff --git a/packages/treasury/src/vault.js b/packages/treasury/src/vault.js index 3cb5d6e94ed..9f9e8e17f3b 100644 --- a/packages/treasury/src/vault.js +++ b/packages/treasury/src/vault.js @@ -42,7 +42,6 @@ export function makeVaultKit( manager, runMint, priceAuthority, - loanParamManager, startTimeStamp, ) { const { updater: uiUpdater, notifier } = makeNotifierKit(); diff --git a/packages/treasury/src/vaultManager.js b/packages/treasury/src/vaultManager.js index f097f829b47..1d7d701d830 100644 --- a/packages/treasury/src/vaultManager.js +++ b/packages/treasury/src/vaultManager.js @@ -252,7 +252,6 @@ export function makeVaultManager( innerFacet, runMint, priceAuthority, - getLoanParams, startTimeStamp, ); diff --git a/packages/treasury/test/swingsetTests/governance/test-governance.js b/packages/treasury/test/swingsetTests/governance/test-governance.js index ab3f2b3075c..3448f439e60 100644 --- a/packages/treasury/test/swingsetTests/governance/test-governance.js +++ b/packages/treasury/test/swingsetTests/governance/test-governance.js @@ -75,14 +75,14 @@ async function main(t, argv) { const expectedTreasuryLog = [ '=> voter and electorate vats are set up', '=> alice and the treasury are set up', - 'param values before {"ChargingPeriod":{"name":"ChargingPeriod","type":"nat","value":"[86400n]"},"InitialMargin":{"name":"InitialMargin","type":"ratio","value":{"denominator":{"brand":"[Alleged: RUN brand]","value":"[100n]"},"numerator":{"brand":"[Seen]","value":"[120n]"}}},"InterestRate":{"name":"InterestRate","type":"ratio","value":{"denominator":{"brand":"[Seen]","value":"[10000n]"},"numerator":{"brand":"[Seen]","value":"[250n]"}}},"LiquidationMargin":{"name":"LiquidationMargin","type":"ratio","value":{"denominator":{"brand":"[Seen]","value":"[100n]"},"numerator":{"brand":"[Seen]","value":"[105n]"}}},"LoanFee":{"name":"LoanFee","type":"ratio","value":{"denominator":{"brand":"[Seen]","value":"[10000n]"},"numerator":{"brand":"[Seen]","value":"[200n]"}}},"RecordingPeriod":{"name":"RecordingPeriod","type":"nat","value":"[86400n]"}}', + 'param values before {"ChargingPeriod":{"type":"nat","value":"[86400n]"},"InitialMargin":{"type":"ratio","value":{"denominator":{"brand":"[Alleged: RUN brand]","value":"[100n]"},"numerator":{"brand":"[Seen]","value":"[120n]"}}},"InterestRate":{"type":"ratio","value":{"denominator":{"brand":"[Seen]","value":"[10000n]"},"numerator":{"brand":"[Seen]","value":"[250n]"}}},"LiquidationMargin":{"type":"ratio","value":{"denominator":{"brand":"[Seen]","value":"[100n]"},"numerator":{"brand":"[Seen]","value":"[105n]"}}},"LoanFee":{"type":"ratio","value":{"denominator":{"brand":"[Seen]","value":"[10000n]"},"numerator":{"brand":"[Seen]","value":"[200n]"}}},"RecordingPeriod":{"type":"nat","value":"[86400n]"}}', 'Voter Bob cast a ballot for {"changeParam":{"collateralBrand":"[Alleged: moola brand]","parameterName":"InterestRate"},"proposedValue":{"denominator":{"brand":"[Alleged: RUN brand]","value":"[10000n]"},"numerator":{"brand":"[Seen]","value":"[500n]"}}}', 'Voter Carol cast a ballot for {"noChange":{"collateralBrand":"[Alleged: moola brand]","parameterName":"InterestRate"}}', 'Voter Dave cast a ballot for {"noChange":{"collateralBrand":"[Alleged: moola brand]","parameterName":"InterestRate"}}', 'Voter Emma cast a ballot for {"changeParam":{"collateralBrand":"[Alleged: moola brand]","parameterName":"InterestRate"},"proposedValue":{"denominator":{"brand":"[Alleged: RUN brand]","value":"[10000n]"},"numerator":{"brand":"[Seen]","value":"[500n]"}}}', 'Voter Flora cast a ballot for {"changeParam":{"collateralBrand":"[Alleged: moola brand]","parameterName":"InterestRate"},"proposedValue":{"denominator":{"brand":"[Alleged: RUN brand]","value":"[10000n]"},"numerator":{"brand":"[Seen]","value":"[500n]"}}}', '=> alice.oneLoanWithInterest called', - 'param values after vote on (InterestRate) {"ChargingPeriod":{"name":"ChargingPeriod","type":"nat","value":"[86400n]"},"InitialMargin":{"name":"InitialMargin","type":"ratio","value":{"denominator":{"brand":"[Alleged: RUN brand]","value":"[100n]"},"numerator":{"brand":"[Seen]","value":"[120n]"}}},"InterestRate":{"name":"InterestRate","type":"ratio","value":{"denominator":{"brand":"[Seen]","value":"[10000n]"},"numerator":{"brand":"[Seen]","value":"[500n]"}}},"LiquidationMargin":{"name":"LiquidationMargin","type":"ratio","value":{"denominator":{"brand":"[Seen]","value":"[100n]"},"numerator":{"brand":"[Seen]","value":"[105n]"}}},"LoanFee":{"name":"LoanFee","type":"ratio","value":{"denominator":{"brand":"[Seen]","value":"[10000n]"},"numerator":{"brand":"[Seen]","value":"[200n]"}}},"RecordingPeriod":{"name":"RecordingPeriod","type":"nat","value":"[86400n]"}}', + 'param values after vote on (InterestRate) {"ChargingPeriod":{"type":"nat","value":"[86400n]"},"InitialMargin":{"type":"ratio","value":{"denominator":{"brand":"[Alleged: RUN brand]","value":"[100n]"},"numerator":{"brand":"[Seen]","value":"[120n]"}}},"InterestRate":{"type":"ratio","value":{"denominator":{"brand":"[Seen]","value":"[10000n]"},"numerator":{"brand":"[Seen]","value":"[500n]"}}},"LiquidationMargin":{"type":"ratio","value":{"denominator":{"brand":"[Seen]","value":"[100n]"},"numerator":{"brand":"[Seen]","value":"[105n]"}}},"LoanFee":{"type":"ratio","value":{"denominator":{"brand":"[Seen]","value":"[10000n]"},"numerator":{"brand":"[Seen]","value":"[200n]"}}},"RecordingPeriod":{"type":"nat","value":"[86400n]"}}', 'governor from governed matches governor instance', 'Param "InterestRate" is in the question', 'Alice owes {"brand":"[Alleged: RUN brand]","value":"[510000n]"} after borrowing', diff --git a/packages/treasury/test/swingsetTests/governance/vat-voter.js b/packages/treasury/test/swingsetTests/governance/vat-voter.js index 12e3cd23e44..6446517958b 100644 --- a/packages/treasury/test/swingsetTests/governance/vat-voter.js +++ b/packages/treasury/test/swingsetTests/governance/vat-voter.js @@ -4,9 +4,11 @@ import { E } from '@agoric/eventual-send'; import { Far } from '@agoric/marshal'; import { q } from '@agoric/assert'; import { sameStructure } from '@agoric/same-structure'; -import { validateQuestionFromCounter } from '@agoric/governance/src/contractGovernor'; -import { assertContractElectorate } from '@agoric/governance/src/validators'; -import { assertBallotConcernsQuestion } from '@agoric/governance/src/governParam'; +import { + validateQuestionFromCounter, + assertContractElectorate, + assertBallotConcernsQuestion, +} from '@agoric/governance'; const { details: X } = assert; diff --git a/packages/treasury/test/test-stablecoin.js b/packages/treasury/test/test-stablecoin.js index 06142c9fb19..1a723544d23 100644 --- a/packages/treasury/test/test-stablecoin.js +++ b/packages/treasury/test/test-stablecoin.js @@ -24,8 +24,12 @@ import { makePromiseKit } from '@agoric/promise-kit'; import { makeScriptedPriceAuthority } from '@agoric/zoe/tools/scriptedPriceAuthority.js'; import { assertAmountsEqual } from '@agoric/zoe/test/zoeTestHelpers.js'; -import { makeInitialValues } from '@agoric/zoe/src/contracts/vpool-xyk-amm/params.js'; +import { + POOL_FEE_KEY, + PROTOCOL_FEE_KEY, +} from '@agoric/zoe/src/contracts/vpool-xyk-amm/params.js'; +import { makeGovernedNat } from '@agoric/governance/src/paramGovernance/paramMakers.js'; import { makeTracer } from '../src/makeTracer.js'; import { SECONDS_PER_YEAR } from '../src/interest.js'; import { VaultState } from '../src/vault.js'; @@ -162,9 +166,12 @@ async function setupAmm( ) { const ammTerms = { timer, + main: { + [POOL_FEE_KEY]: makeGovernedNat(POOL_FEE_BP), + [PROTOCOL_FEE_KEY]: makeGovernedNat(PROTOCOL_FEE_BP), + }, poolFeeBP: POOL_FEE_BP, protocolFeeBP: PROTOCOL_FEE_BP, - main: makeInitialValues(POOL_FEE_BP, PROTOCOL_FEE_BP), }; const ammGovernorTerms = { @@ -177,6 +184,7 @@ async function setupAmm( privateArgs: {}, }, }; + const { instance: ammGovernorInstance, publicFacet: ammGovernorPublicFacet, diff --git a/packages/treasury/test/vault-contract-wrapper.js b/packages/treasury/test/vault-contract-wrapper.js index 97a056d9f10..c64d2a51b3a 100644 --- a/packages/treasury/test/vault-contract-wrapper.js +++ b/packages/treasury/test/vault-contract-wrapper.js @@ -10,7 +10,6 @@ import { makeFakePriceAuthority } from '@agoric/zoe/tools/fakePriceAuthority.js' import { makeRatio } from '@agoric/zoe/src/contractSupport/ratio.js'; import { Far } from '@agoric/marshal'; -import { buildParamManager } from '@agoric/governance/src/paramManager'; import { makeVaultKit } from '../src/vault.js'; import { paymentFromZCFMint } from '../src/burn.js'; @@ -83,14 +82,11 @@ export async function start(zcf, privateArgs) { }; const priceAuthority = makeFakePriceAuthority(options); - const { publicFacet } = buildParamManager([]); - const { vault, openLoan, accrueInterestAndAddToPool } = await makeVaultKit( zcf, managerMock, runMint, priceAuthority, - publicFacet, timer.getCurrentTimestamp(), ); diff --git a/packages/zoe/package.json b/packages/zoe/package.json index 1a47355664b..56a2e21e1bb 100644 --- a/packages/zoe/package.json +++ b/packages/zoe/package.json @@ -45,6 +45,7 @@ "@agoric/bundle-source": "^2.0.1", "@agoric/ertp": "^0.13.0", "@agoric/eventual-send": "^0.14.0", + "@agoric/far": "^0.1.1", "@agoric/governance": "^0.4.0", "@agoric/import-bundle": "^0.2.32", "@agoric/marshal": "^0.5.0", diff --git a/packages/zoe/src/contracts/vpool-xyk-amm/multipoolMarketMaker.js b/packages/zoe/src/contracts/vpool-xyk-amm/multipoolMarketMaker.js index 8280ff929d6..a2cc15e8b3f 100644 --- a/packages/zoe/src/contracts/vpool-xyk-amm/multipoolMarketMaker.js +++ b/packages/zoe/src/contracts/vpool-xyk-amm/multipoolMarketMaker.js @@ -15,7 +15,7 @@ import '../../../exported.js'; import { makeMakeCollectFeesInvitation } from './collectFees.js'; import { makeMakeSwapInvitation } from './swap.js'; import { makeDoublePool } from './doublePool.js'; -import { makeInitialValues, POOL_FEE_KEY, PROTOCOL_FEE_KEY } from './params.js'; +import { makeParamManager, POOL_FEE_KEY, PROTOCOL_FEE_KEY } from './params.js'; const { details: X } = assert; @@ -76,7 +76,7 @@ const { details: X } = assert; * * The contract gets the initial values for those parameters from its terms, and * thereafter can be seen to only use the values provided by the - * `getParamValue()` method returned by the paramManager. + * `getNat()` method returned by the paramManager. * * `handleParamGovernance()` adds several methods to the publicFacet of the * contract, and bundles the privateFacet to ensure that governance @@ -107,19 +107,20 @@ const start = zcf => { const { brands: { Central: centralBrand }, timer, - poolFeeBP, - protocolFeeBP, + main: { + [POOL_FEE_KEY]: poolFeeParam, + [PROTOCOL_FEE_KEY]: protocolFeeParam, + }, } = /** @type { Terms & AMMTerms } */ (zcf.getTerms()); assertIssuerKeywords(zcf, ['Central']); assert(centralBrand !== undefined, X`centralBrand must be present`); - const { - wrapPublicFacet, - wrapCreatorFacet, - getParamValue, - } = handleParamGovernance(zcf, makeInitialValues(poolFeeBP, protocolFeeBP)); - const getPoolFeeBP = () => getParamValue(POOL_FEE_KEY); - const getProtocolFeeBP = () => getParamValue(PROTOCOL_FEE_KEY); + const { wrapPublicFacet, wrapCreatorFacet, getNat } = handleParamGovernance( + zcf, + makeParamManager(poolFeeParam.value, protocolFeeParam.value), + ); + const getPoolFeeBP = () => getNat(POOL_FEE_KEY); + const getProtocolFeeBP = () => getNat(PROTOCOL_FEE_KEY); /** @type {WeakStore} */ const secondaryBrandToPool = makeWeakStore('secondaryBrand'); diff --git a/packages/zoe/src/contracts/vpool-xyk-amm/params.js b/packages/zoe/src/contracts/vpool-xyk-amm/params.js index c2fdfd6a77c..4d00bc5f6d9 100644 --- a/packages/zoe/src/contracts/vpool-xyk-amm/params.js +++ b/packages/zoe/src/contracts/vpool-xyk-amm/params.js @@ -1,13 +1,14 @@ // @ts-check -import { makeGovernedNat } from '@agoric/governance/src/paramMakers.js'; +import { makeParamManagerBuilder } from '@agoric/governance'; export const POOL_FEE_KEY = 'PoolFee'; export const PROTOCOL_FEE_KEY = 'ProtocolFee'; -export const makeInitialValues = (poolFeeBP, protocolFeeBP) => { - return harden([ - makeGovernedNat(POOL_FEE_KEY, poolFeeBP), - makeGovernedNat(PROTOCOL_FEE_KEY, protocolFeeBP), - ]); +/** @type {(poolFeeBP: bigint, protocolFeeBP: bigint) => ParamManagerFull} */ +export const makeParamManager = (poolFeeBP, protocolFeeBP) => { + return makeParamManagerBuilder() + .addNat(POOL_FEE_KEY, poolFeeBP) + .addNat(PROTOCOL_FEE_KEY, protocolFeeBP) + .build(); }; diff --git a/packages/zoe/test/unitTests/contracts/vpool-xyk-amm/test-amm-governance.js b/packages/zoe/test/unitTests/contracts/vpool-xyk-amm/test-amm-governance.js index fdda890e22a..88af9b6c7d1 100644 --- a/packages/zoe/test/unitTests/contracts/vpool-xyk-amm/test-amm-governance.js +++ b/packages/zoe/test/unitTests/contracts/vpool-xyk-amm/test-amm-governance.js @@ -156,18 +156,10 @@ const setupServices = async ( }; }; -const ammInitialValues = harden([ - { - name: POOL_FEE_KEY, - value: 24n, - type: ParamType.NAT, - }, - { - name: PROTOCOL_FEE_KEY, - value: 6n, - type: ParamType.NAT, - }, -]); +const ammInitialValues = harden({ + [POOL_FEE_KEY]: { value: 24n, type: ParamType.NAT }, + [PROTOCOL_FEE_KEY]: { value: 6n, type: ParamType.NAT }, +}); test('amm change param via Governance', async t => { const centralR = makeIssuerKit('central'); @@ -193,12 +185,10 @@ test('amm change param via Governance', async t => { await E(amm.ammPublicFacet).getGovernedParams(), { PoolFee: { - name: POOL_FEE_KEY, type: 'nat', value: 24n, }, ProtocolFee: { - name: PROTOCOL_FEE_KEY, type: 'nat', value: 6n, }, @@ -227,9 +217,7 @@ test('amm change param via Governance', async t => { await timer.tick(); await timer.tick(); - const paramValue = await E(amm.ammPublicFacet).getParamValue( - PROTOCOL_FEE_KEY, - ); + const paramValue = await E(amm.ammPublicFacet).getNat(PROTOCOL_FEE_KEY); t.deepEqual(paramValue, 20n, 'updated value'); }); @@ -304,12 +292,10 @@ test('price check after Governance param change', async t => { await E(amm.ammPublicFacet).getGovernedParams(), { PoolFee: { - name: POOL_FEE_KEY, type: 'nat', value: 24n, }, ProtocolFee: { - name: PROTOCOL_FEE_KEY, type: 'nat', value: 6n, }, @@ -338,9 +324,7 @@ test('price check after Governance param change', async t => { await timer.tick(); await timer.tick(); - const paramValue = await E(amm.ammPublicFacet).getParamValue( - PROTOCOL_FEE_KEY, - ); + const paramValue = await E(amm.ammPublicFacet).getNat(PROTOCOL_FEE_KEY); t.deepEqual(paramValue, 20n, 'updated value'); const priceAfter = await E(amm.ammPublicFacet).getInputPrice( diff --git a/packages/zoe/test/unitTests/contracts/vpool-xyk-amm/test-xyk-amm-swap.js b/packages/zoe/test/unitTests/contracts/vpool-xyk-amm/test-xyk-amm-swap.js index fe0a58325a0..d710427793a 100644 --- a/packages/zoe/test/unitTests/contracts/vpool-xyk-amm/test-xyk-amm-swap.js +++ b/packages/zoe/test/unitTests/contracts/vpool-xyk-amm/test-xyk-amm-swap.js @@ -177,18 +177,10 @@ const setupServices = async ( }; }; -const ammInitialValues = harden([ - { - name: POOL_FEE_KEY, - value: 24n, - type: ParamType.NAT, - }, - { - name: PROTOCOL_FEE_KEY, - value: 6n, - type: ParamType.NAT, - }, -]); +const ammInitialValues = harden({ + [POOL_FEE_KEY]: { value: 24n, type: ParamType.NAT }, + [PROTOCOL_FEE_KEY]: { value: 6n, type: ParamType.NAT }, +}); test('amm with valid offers', async t => { // Set up central token diff --git a/packages/zoe/test/unitTests/test-zoe.js b/packages/zoe/test/unitTests/test-zoe.js index e23f25d73dd..1d3fddaeb9c 100644 --- a/packages/zoe/test/unitTests/test-zoe.js +++ b/packages/zoe/test/unitTests/test-zoe.js @@ -72,6 +72,11 @@ function isEmptyFacet(t, facet) { t.deepEqual(Object.getOwnPropertyNames(facet), []); } +function facetHasMethods(t, facet, names) { + t.is(passStyleOf(facet), 'remotable'); + t.deepEqual(Object.getOwnPropertyNames(facet), names); +} + test(`E(zoe).startInstance no issuerKeywordRecord, no terms`, async t => { const { zoe, installation } = await setupZCFTest(); const result = await E(zoe).startInstance(installation); @@ -85,7 +90,7 @@ test(`E(zoe).startInstance no issuerKeywordRecord, no terms`, async t => { ]); isEmptyFacet(t, result.creatorFacet); t.deepEqual(result.creatorInvitation, undefined); - isEmptyFacet(t, result.publicFacet); + facetHasMethods(t, result.publicFacet, ['makeInvitation']); t.deepEqual(Object.getOwnPropertyNames(result.adminFacet).sort(), [ 'getVatShutdownPromise', ]); @@ -112,7 +117,7 @@ test(`E(zoe).startInstance promise for installation`, async t => { ]); isEmptyFacet(t, result.creatorFacet); t.deepEqual(result.creatorInvitation, undefined); - isEmptyFacet(t, result.publicFacet); + facetHasMethods(t, result.publicFacet, ['makeInvitation']); t.deepEqual(Object.getOwnPropertyNames(result.adminFacet).sort(), [ 'getVatShutdownPromise', ]); diff --git a/packages/zoe/test/unitTests/zcf/setupZcfTest.js b/packages/zoe/test/unitTests/zcf/setupZcfTest.js index 86ed229306b..f6be3e300f6 100644 --- a/packages/zoe/test/unitTests/zcf/setupZcfTest.js +++ b/packages/zoe/test/unitTests/zcf/setupZcfTest.js @@ -61,6 +61,7 @@ export const setupZCFTest = async (issuerKeywordRecord, terms) => { feeMintAccess, // Additional ZCF + // @ts-ignore zcf2 is accessible before it is set zcf2, creatorFacet2, instance2, diff --git a/packages/zoe/test/unitTests/zcf/zcfTesterContract.js b/packages/zoe/test/unitTests/zcf/zcfTesterContract.js index f447f7a9817..f6aa08b651e 100644 --- a/packages/zoe/test/unitTests/zcf/zcfTesterContract.js +++ b/packages/zoe/test/unitTests/zcf/zcfTesterContract.js @@ -1,6 +1,7 @@ // @ts-check import '../../../exported.js'; +import { Far } from '@agoric/far'; /** * Tests ZCF @@ -12,7 +13,11 @@ const start = async zcf => { const instance = zcf.getInstance(); zcf.setTestJig(() => harden({ instance })); - return {}; + const publicFacet = Far('public facet', { + makeInvitation: () => zcf.makeInvitation(() => 17, 'simple'), + }); + + return { publicFacet }; }; harden(start);