diff --git a/packages/ERTP/src/typeGuards.js b/packages/ERTP/src/typeGuards.js index d2751edb501f..78fd4e30475c 100644 --- a/packages/ERTP/src/typeGuards.js +++ b/packages/ERTP/src/typeGuards.js @@ -64,7 +64,7 @@ const SetValueShape = M.arrayOf(M.key()); */ const CopyBagValueShape = M.bag(); -const AmountValueShape = M.or( +export const AmountValueShape = M.or( NatValueShape, CopySetValueShape, SetValueShape, @@ -228,3 +228,9 @@ export const makeIssuerInterfaces = ( }); }; harden(makeIssuerInterfaces); + +/** @param {Amount} amount */ +export const makeBrandedAmountPattern = amount => { + return { brand: amount.brand, value: M.nat() }; +}; +harden(makeBrandedAmountPattern); diff --git a/packages/agoric-cli/src/commands/auction.js b/packages/agoric-cli/src/commands/auction.js index fb10b58dc2c6..d82bdf320f0b 100644 --- a/packages/agoric-cli/src/commands/auction.js +++ b/packages/agoric-cli/src/commands/auction.js @@ -6,11 +6,11 @@ import { outputActionAndHint } from '../lib/wallet.js'; const { Fail } = assert; -/** @typedef {import('@agoric/governance/src/contractGovernance/typedParamManager.js').ParamTypesMap} ParamTypesMap */ +/** @typedef {import('@agoric/governance/src/contractGovernance/paramManager.js').ParamTypesMap} ParamTypesMap */ /** * @template {ParamStateRecord} M - * @typedef {import('@agoric/governance/src/contractGovernance/typedParamManager.js').ParamTypesMapFromRecord} ParamTypesMapFromRecord + * @typedef {import('@agoric/governance/src/contractGovernance/paramManager.js').ParamTypesMapFromRecord} ParamTypesMapFromRecord */ /** diff --git a/packages/boot/test/bootstrapTests/test-vaults-upgrade.js b/packages/boot/test/bootstrapTests/test-vaults-upgrade.js index 76d834b3959e..acfcf6ef182e 100644 --- a/packages/boot/test/bootstrapTests/test-vaults-upgrade.js +++ b/packages/boot/test/bootstrapTests/test-vaults-upgrade.js @@ -12,6 +12,7 @@ import { Offers } from '@agoric/inter-protocol/src/clientSupport.js'; import { Far, makeMarshal } from '@endo/marshal'; import { SECONDS_PER_YEAR } from '@agoric/inter-protocol/src/interest.js'; import { makeAgoricNamesRemotesFromFakeStorage } from '@agoric/vats/tools/board-utils.js'; + import { makeSwingsetTestKit } from './supports.js'; import { makeWalletFactoryDriver } from './drivers.js'; diff --git a/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/actions.sh b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/actions.sh index d10fcbee0d3a..8bca38d3f4e7 100755 --- a/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/actions.sh +++ b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/actions.sh @@ -9,40 +9,48 @@ SDK=${SDK:-/usr/src/agoric-sdk} # Enable debugging set -x -# hacky restore of pruned artifacts -killAgd -EXPORT_DIR=$(mktemp -t -d swing-store-export-upgrade-11-XXX) -WITHOUT_GENESIS_EXPORT=1 make_swing_store_snapshot $EXPORT_DIR --artifact-mode debug || fail "Couldn't make swing-store snapshot" -HISTORICAL_ARTIFACTS="$(cd $HOME/.agoric/data/agoric/swing-store-historical-artifacts/; for i in *; do echo -n "[\"$i\",\"$i\"],"; done)" -mv -n $HOME/.agoric/data/agoric/swing-store-historical-artifacts/* $EXPORT_DIR || fail "some historical artifacts not pruned" -mv $EXPORT_DIR/export-manifest.json $EXPORT_DIR/export-manifest-original.json -cat $EXPORT_DIR/export-manifest-original.json | jq -r ".artifacts = .artifacts + [${HISTORICAL_ARTIFACTS%%,}] | del(.artifactMode)" > $EXPORT_DIR/export-manifest.json -restore_swing_store_snapshot $EXPORT_DIR || fail "Couldn't restore swing-store snapshot" -startAgd -rm -rf $EXPORT_DIR - -test_not_val "$(agops vaults list --from $GOV1ADDR)" "" "gov1 has no vaults" - -# open up a vault -OFFER=$(mktemp -t agops.XXX) -agops vaults open --wantMinted 7.00 --giveCollateral 11.0 >|"$OFFER" -agops perf satisfaction --from "$GOV1ADDR" --executeOffer "$OFFER" --keyring-backend=test - -# put some IST in -OFFER=$(mktemp -t agops.XXX) -agops vaults adjust --vaultId vault3 --giveMinted 1.5 --from $GOV1ADDR --keyring-backend=test >|"$OFFER" -agops perf satisfaction --from "$GOV1ADDR" --executeOffer "$OFFER" --keyring-backend=test - -# add some collateral -OFFER=$(mktemp -t agops.XXX) -agops vaults adjust --vaultId vault3 --giveCollateral 2.0 --from $GOV1ADDR --keyring-backend="test" >|"$OFFER" -agops perf satisfaction --from "$GOV1ADDR" --executeOffer "$OFFER" --keyring-backend=test - -# close out -OFFER=$(mktemp -t agops.XXX) -agops vaults close --vaultId vault3 --giveMinted 5.75 --from $GOV1ADDR --keyring-backend="test" >|"$OFFER" -agops perf satisfaction --from "$GOV1ADDR" --executeOffer "$OFFER" --keyring-backend=test - -test_val $(agoric follow -l -F :published.vaultFactory.managers.manager0.vaults.vault3 -o jsonlines | jq -r '.vaultState') "closed" "vault3 is closed" -test_val $(agoric follow -l -F :published.vaultFactory.managers.manager0.vaults.vault3 -o jsonlines | jq -r '.locked.value') "0" "vault3 contains no collateral" -test_val $(agoric follow -l -F :published.vaultFactory.managers.manager0.vaults.vault3 -o jsonlines | jq -r '.debtSnapshot.debt.value') "0" "vault3 has no debt" +## hacky restore of pruned artifacts +#killAgd +#EXPORT_DIR=$(mktemp -t -d swing-store-export-upgrade-11-XXX) +#WITHOUT_GENESIS_EXPORT=1 make_swing_store_snapshot $EXPORT_DIR --artifact-mode debug || fail "Couldn't make swing-store snapshot" +#HISTORICAL_ARTIFACTS="$(cd $HOME/.agoric/data/agoric/swing-store-historical-artifacts/; for i in *; do echo -n "[\"$i\",\"$i\"],"; done)" +#mv -n $HOME/.agoric/data/agoric/swing-store-historical-artifacts/* $EXPORT_DIR || fail "some historical artifacts not pruned" +#mv $EXPORT_DIR/export-manifest.json $EXPORT_DIR/export-manifest-original.json +#cat $EXPORT_DIR/export-manifest-original.json | jq -r ".artifacts = .artifacts + [${HISTORICAL_ARTIFACTS%%,}] | del(.artifactMode)" > $EXPORT_DIR/export-manifest.json +#restore_swing_store_snapshot $EXPORT_DIR || fail "Couldn't restore swing-store snapshot" +#startAgd +#rm -rf $EXPORT_DIR +# +#test_not_val "$(agops vaults list --from $GOV1ADDR)" "" "gov1 has no vaults" +# +## open up a vault +#OFFER=$(mktemp -t agops.XXX) +#agops vaults open --wantMinted 7.00 --giveCollateral 11.0 >|"$OFFER" +#agops perf satisfaction --from "$GOV1ADDR" --executeOffer "$OFFER" --keyring-backend=test +# +## put some IST in +#OFFER=$(mktemp -t agops.XXX) +#agops vaults adjust --vaultId vault3 --giveMinted 1.5 --from $GOV1ADDR --keyring-backend=test >|"$OFFER" +#agops perf satisfaction --from "$GOV1ADDR" --executeOffer "$OFFER" --keyring-backend=test +# +## add some collateral +#OFFER=$(mktemp -t agops.XXX) +#agops vaults adjust --vaultId vault3 --giveCollateral 2.0 --from $GOV1ADDR --keyring-backend="test" >|"$OFFER" +#agops perf satisfaction --from "$GOV1ADDR" --executeOffer "$OFFER" --keyring-backend=test +# +## close out +#OFFER=$(mktemp -t agops.XXX) +#agops vaults close --vaultId vault3 --giveMinted 5.75 --from $GOV1ADDR --keyring-backend="test" >|"$OFFER" +#agops perf satisfaction --from "$GOV1ADDR" --executeOffer "$OFFER" --keyring-backend=test +# +#test_val $(agoric follow -l -F :published.vaultFactory.managers.manager0.vaults.vault3 -o jsonlines | jq -r '.vaultState') "closed" "vault3 is closed" +#test_val $(agoric follow -l -F :published.vaultFactory.managers.manager0.vaults.vault3 -o jsonlines | jq -r '.locked.value') "0" "vault3 contains no collateral" +#test_val $(agoric follow -l -F :published.vaultFactory.managers.manager0.vaults.vault3 -o jsonlines | jq -r '.debtSnapshot.debt.value') "0" "vault3 has no debt" + +# CWD is agoric-sdk +upgrade11=./upgrade-test-scripts/agoric-upgrade-11 + + +echo $upgrade11 + +$upgrade11/more_actions.sh diff --git a/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/contract-upgrade/vault-upgrade-permit.json b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/contract-upgrade/vault-upgrade-permit.json new file mode 100644 index 000000000000..5e5d0c42ee93 --- /dev/null +++ b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/contract-upgrade/vault-upgrade-permit.json @@ -0,0 +1,7 @@ +{ + "consume": { + "vatAdminSvc": true, + "vaultFactoryKit": true, + "chainStorage": true + } +} diff --git a/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/contract-upgrade/vaults-upgrade-script.js b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/contract-upgrade/vaults-upgrade-script.js new file mode 100644 index 000000000000..9e9ed938fc95 --- /dev/null +++ b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/contract-upgrade/vaults-upgrade-script.js @@ -0,0 +1,42 @@ +// to turn on ts-check: +/* global E */ + +// import { E } from "@endo/far"; + +const GOV_BUNDLE_ID = 'b1-'; +const VAULTS_BUNDLE_ID = 'b1-'; + +console.info('Vaults upgrade: evaluating script'); + +/* + * Test an upgrade of the VaultFactory and its governing contract. + */ +const upgradeVaultFactory = async powers => { + console.info('upgrade vaultFactory'); + const { + consume: { + chainStorage, + vatAdminSvc, + vaultFactoryKit: { + governorAdminFacet, + adminFacet: vaultsAdminFacet, + instance, + privateArgs, + }, + }, + } = powers; + + const newGovernorBundleCap = await E(vatAdminSvc).getBundleCap(GOV_BUNDLE_ID); + const newVaultsBundleCap = await E(vatAdminSvc).getBundleCap( + VAULTS_BUNDLE_ID, + ); + + // consider modifying privateArgs. + await E(governorAdminFacet).upgrade(newGovernorBundleCap, {}); + + // not write, but perhaps visible? + const storageNode = await E(chainStorage).makeChildNode('vaults'); + await E(vaultsAdminFacet).upgrade(newVaultsBundleCap, { storageNode }); +}; + +upgradeVaultFactory; diff --git a/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/more_actions.sh b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/more_actions.sh new file mode 100755 index 000000000000..934e4c17e700 --- /dev/null +++ b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/more_actions.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +. ./upgrade-test-scripts/env_setup.sh + +# Enable debugging +set -x + +# CWD is agoric-sdk +upgrade11=./upgrade-test-scripts/agoric-upgrade-11 + +yarn --silent bundle-source --cache-json /tmp packages/governance/src/contractGovernor.js contractGovernor-upgrade +yarn --silent bundle-source --cache-json /tmp packages/inter-protocol/src/psm/psm.js psm-upgrade +yarn --silent bundle-source --cache-json /tmp packages/inter-protocol/src/reserve/reserve.js reserve-upgrade +yarn --silent bundle-source --cache-json /tmp packages/inter-protocol/src/auction/auctioneer.js auctioneer-upgrade +yarn --silent bundle-source --cache-json /tmp packages/inter-protocol/src/vaultFactory/vaultFactory.js vaultFactory-upgrade + +# fluxAggregator, smartWallet + +# Start by upgrading the governance facet, which will do a null upgrad on the +# contract, and then upgrade the contract itself. + + +GOV_HASH=`jq -r .endoZipBase64Sha512 /tmp/bundle-contractGovernor-upgrade.json +echo bundle-contractGovernor-upgrade.json $GOV_HASH +PSM_HASH=`jq -r .endoZipBase64Sha512 /tmp/bundle-psm-upgrade.json +echo bundle-psm-upgrade.json $PSM_HASH +RESERVE_HASH=`jq -r .endoZipBase64Sha512 /tmp/bundle-reserve-upgrade.json +echo bundle-reserve-upgrade.json $RESERVE_HASH +AUCTIONEER_HASH=`jq -r .endoZipBase64Sha512 /tmp/bundle-auctioneer-upgrade.json +echo bundle-auctioneer-upgrade.json $AUCTIONEER_HASH +VAULTS_HASH=`jq -r .endozipbase64sha512 /tmp/bundle-vaultfactory-upgrade.json +echo bundle-vaultfactory-upgrade.json $vaults_hash +# grep for hashes in upgrade scripts + + + +echo +++++ install bundles +++++ +for f in /tmp/bundle-[a-v]*-upgrade.json; do + echo installing $f + agd tx swingset install-bundle "@$f" \ + --from gov1 --keyring-backend=test --gas=auto \ + --chain-id=agoriclocal -bblock --yes +done + + + +# upgrade the vaultFactory's governing contract, then upgrade the vaultFactory +# itself. +echo +++++ upgrade VaultFactory +++++ +$upgrade11/zoe-full-upgrade/zcf-upgrade-driver.sh + diff --git a/packages/governance/README.md b/packages/governance/README.md index 98511dab1cb4..1d2db3fa548f 100644 --- a/packages/governance/README.md +++ b/packages/governance/README.md @@ -155,7 +155,7 @@ the Electorate is a required parameter in all governed contracts. Invitations are an unusual kind of managed parameter. Most parameters are copy-objects that don't carry any power. Since invitations convey rights, only the invitation's amount appears in `terms`. The actual invitation must -be passed to the contract using `privateArg`. This combination makes it +be passed to the contract using `privateArgs`. This combination makes it possible for clients to see what the invitation is for, but only the contract has the ability to exercise it. Similarly, when there will be a vote to change the Electorate (or any other Invitation-valued parameter), observers can see the @@ -166,36 +166,46 @@ exercised if/when the vote is successful. ### ParamManager `ContractGovernor` expects to work with contracts that use `ParamManager` to -manage their parameters. `makeParamManager()` is designed to be called -within the managed contract so that internal access to the parameter values is +manage their parameters. In order to support upgrade, all governed contracts +will be durable, upgradeable contracts. When using the `contractHelper`, the way +to create a paramManager is to call `handleParamGovernance` from within the +governed contract so that internal access to the parameter values is synchronous. A separate facet allows visible management of changes to the parameter values. -`makeParamManager(zoe)` makes a ParamManager: +`handleParamGovernance(zcf, invitation, paramType, ...)` makes a ParamManager: ```javascript - const paramManager = await makeParamManager( + const facetHelpers = await handleParamGovernance( + zcf, + invitation, { 'MyChangeableNumber': ['nat', startingValue], 'ContractElectorate': ['invitation', initialPoserInvitation], }, - zcf.getZoeService(), + makeRecorderKit, + storageNode, ); - paramManager.getMyChangeableNumber() === startingValue; - paramManager.updatetMyChangeableNumber((newValue); - paramManager.getMyChangeableNumber() === newValue; + const { publicMixin, publicMixinGuards } = facetHelpers; + const { augmentPublicFacet, makeGovernorFacet, params } = facetHelpers; ``` -If you don't need any parameters that depend on the Zoe service, there's -an alternative function that returns synchronously: -```javascript - const paramManager = await makeParamManagerSync( - { - 'Collateral': ['brand', drachmaBrand], - }, - ); -``` +`augmentPublicFacet` is a function that can be applied to a `publicFacet` to +produce a publicFacet that also includes accessors for all the defined +parameters as well as `getParamDescriptions` and `getPublicTopics`. + +Similarly, `makeGovernorFacet` can be applied to the `creatorFacet` to create +the facet that the contractGovernor will use as well as the +`limitedCreatorFacet` that can be handed out to those outside of governance who +should have access to the creator functionality of the governed contract. + +`makeRecorderKit` and `storageNode` are provided to paramGovernance so it can +publish the original values and any updates to governed values to the off-chain +storage. `makeRecorderKit` is a function that creates a durable recorderKit. +Since durable constructors must be defined exactly once per vat, and +recorderKits will be needed elsewhere in the contract, it has to be passed in. +`storageNode` is the node where governance will be able to write updates. See [ParamTypes definition](./src/constants.js) for all supported types. More types will be supported as we learn what contracts need to manage. (If you find @@ -212,7 +222,7 @@ the methods to be called. ### Governed Contracts `contractHelper` provides support for the vast majority of expected clients that -will have a single set of parameters to manage. A contract only has to define +will have a single set of parameters to manage. A contract only has to declare the parameters (including `CONTRACT_ELECTORATE`) in a call to `handleParamGovernance()`, and add any needed methods to the public and creator facets. This will @@ -222,16 +232,10 @@ facets. This will It's convenient for the contract to export a function (e.g. `makeParamTerms`) for the use of those starting up the contract to insert in the `terms`. They would otherwise need to write boilerplate functions to declare all the required -parameters. - -When a governed contract starts up, it should get the parameter declarations -from `terms`, use them to create a paramManager, and pass that to -`handleParamGovernance`. `handleParamGovernance()` returns functions -(`augmentPublicFacet()` and `makeGovernorFacet()`) that add -required methods to the public and creator facets. Since the governed contract -uses the values passed in `terms` to create the paramManager, reviewers of the -contract can verify that all and only the declared parameters are under the -control of the paramManager and made visible to the contract's clients. +parameters. Since the governed contract uses the values passed in `terms` to +create the paramManager, reviewers of the contract can verify that all and only +the declared parameters are under the control of the paramManager and made +visible to the contract's clients. Governed methods and parameters must be included in terms. @@ -249,10 +253,9 @@ Governed methods and parameters must be included in terms. ``` When a contract is written without benefit of `contractHelper`, it is -responsible for adding `getSubscription`, and -`getGovernedParams` to its `PublicFacet`, and for adding -`getParamMgrRetriever`, `getInvitation` and `getLimitedCreatorFacet` to its -`CreatorFacet`. +responsible for adding `getParamDescriptions`, and `getPublicTopics` to its +`PublicFacet`, and for adding `getParamMgrRetriever`, `getInvitation` and +`getLimitedCreatorFacet` to its `CreatorFacet`. ## Scenarios diff --git a/packages/governance/src/contractGovernance/paramManager.js b/packages/governance/src/contractGovernance/paramManager.js index feb03aca466b..b0ca30053080 100644 --- a/packages/governance/src/contractGovernance/paramManager.js +++ b/packages/governance/src/contractGovernance/paramManager.js @@ -1,86 +1,463 @@ /* eslint @typescript-eslint/no-floating-promises: "warn" */ -import { Far, passStyleOf } from '@endo/marshal'; -import { AmountMath } from '@agoric/ertp'; +import { + AmountMath, + AmountShape, + AmountValueShape, + BrandShape, +} from '@agoric/ertp'; +import { + InstallationShape, + InstanceHandleShape, + KeywordShape, +} from '@agoric/zoe/src/typeGuards.js'; import { assertKeywordName } from '@agoric/zoe/src/cleanProposal.js'; -import { Nat } from '@endo/nat'; -import { keyEQ, makeScalarMapStore } from '@agoric/store'; +import { keyEQ, M, mustMatch } from '@agoric/store'; import { E } from '@endo/eventual-send'; import { assertAllDefined } from '@agoric/internal'; -import { ParamTypes } from '../constants.js'; - import { - assertTimestamp, - assertRelativeTime, - makeAssertBrandedRatio, - makeAssertInstallation, - makeAssertInstance, - makeLooksLikeBrand, -} from './assertions.js'; + makeScalarBigMapStore, + prepareExoClass, + provide, +} from '@agoric/vat-data'; +import { RelativeTimeShape, TimestampShape } from '@agoric/time'; +import { ToFarFunction } from '@endo/captp'; +import { makeBrandedRatioPattern } from '@agoric/zoe/src/contractSupport/ratio.js'; + +import { ParamTypes } from '../constants.js'; import { CONTRACT_ELECTORATE } from './governParam.js'; +import { InvitationShape } from '../typeGuards.js'; const { Fail, quote: q } = assert; /** - * @param {ParamManagerBase} paramManager + * @file + * The terminology here is that a ParamHolder holds the value of a particular + * parameter, while a ParamManager manages a collection of parameters. + */ + +/** + * @typedef {Record} ParamTypesMap + */ +/** + * @template {ParamStateRecord} M + * @typedef {{ [R in keyof M]: M[R]['type']}} ParamTypesMapFromRecord + */ +/** + * @template {ParamTypesMap} M + * @typedef {{ [T in keyof M]: { type: M[T], value: ParamValueForType } }} ParamRecordsFromTypes + */ + +/** + * @template {ParamTypesMap} M + * @typedef {{ + * [K in keyof M as `get${string & K}`]: () => ParamValueForType + * }} Getters + */ + +/** + * @template {ParamTypesMap} T + * @typedef {{ + * [K in keyof T as `update${string & K}`]: (value: ParamValueForType) => void + * }} Updaters + */ + +/** + * @template {ParamTypesMap} M + * @typedef {ParamManagerBase & {updateParams: UpdateParams}} ParamManager + */ + +/** + * @param {ParamType} type + */ +const builderMethodName = type => + `add${type[0].toUpperCase() + type.substring(1)}`; + +/** + * @template {ParamType} T + * @typedef {[type: T, value: ParamValueForType]} ST param spec tuple + */ + +/** + * @typedef {{ type: 'invitation', value: Amount<'set'> }} InvitationParam + */ + +// XXX better to use the manifest constant ParamTypes +// but importing that here turns this file into a module, +// breaking the ambient typing +/** + * @typedef {ST<'amount'> + * | ST<'brand'> + * | ST<'installation'> + * | ST<'instance'> + * | ST<'nat'> + * | ST<'ratio'> + * | ST<'string'> + * | ST<'timestamp'> + * | ST<'relativeTime'> + * | ST<'unknown'>} SyncSpecTuple + * + * @typedef {['invitation', [Invitation, Amount]]} AsyncSpecTuple + */ + +export const buildParamGovernanceExoMakers = (zoe, baggage) => { + const ParamHolderCommon = { + getValue: M.call().returns(M.any()), + makeDescription: M.call().returns({ type: M.string(), value: M.any() }), + getVisibleValue: M.call(M.any()).returns(M.any()), + prepareToUpdate: M.call(M.any()).returns(M.any()), + update: M.call(M.any()).returns(M.recordOf(KeywordShape, M.any())), + getGetterElement: M.call().returns([M.string(), M.any()]), + getFarGetterElement: M.call().returns([M.string(), M.any(), M.pattern()]), + }; + + const CopyParamHolderI = M.interface('CopyParamHolder', ParamHolderCommon); + + const makeCopyParamHolder = prepareExoClass( + baggage, + 'CopyParamHolder', + CopyParamHolderI, + (name, current, type, pattern) => { + return { current, type, name, pattern }; + }, + { + makeDescription() { + const { state } = this; + return { + type: state.type, + value: state.current, + }; + }, + getValue() { + return this.state.current; + }, + getVisibleValue(proposed) { + const { + state: { name, pattern }, + } = this; + mustMatch( + proposed, + pattern, + `Proposed value ${proposed} for ${name} must match ${pattern}`, + ); + return proposed; + }, + prepareToUpdate(proposed) { + return proposed; + }, + update(proposed) { + const { state } = this; + mustMatch( + proposed, + state.pattern, + `${state.name} must match ${state.pattern}, was ${proposed}`, + ); + state.current = proposed; + return harden({ [state.name]: proposed }); + }, + getGetterElement() { + const { state } = this; + // names are keywords so they will necessarily be TitleCase + const name = `get${state.name}`; + const getter = () => state.current; + return [name, getter]; + }, + getFarGetterElement() { + const { state } = this; + // names are keywords so they will necessarily be TitleCase + const name = `get${state.name}`; + const getter = ToFarFunction(name, () => state.current); + const guard = M.call().returns(state.pattern); + return [name, getter, guard]; + }, + }, + { + stateShape: { + name: KeywordShape, + current: M.any(), + // XXX how to say it must be one of ParamTypes? + type: M.string(), + pattern: M.pattern(), + }, + }, + ); + + const assertInvitation = async i => { + if (!zoe) { + throw Fail`zoe must be provided for governed Invitations`; + } + const { instance, installation } = await E(zoe).getInvitationDetails(i); + // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error -- the build config doesn't expect an error here + // @ts-ignore typedefs say they're guaranteed truthy but just to be safe + assert(instance && installation, 'must be an invitation'); + }; + + const InvitationParamHolderI = M.interface('InvitationParamHolder', { + ...ParamHolderCommon, + getValue: M.call().returns(InvitationShape), + }); + + const makeInvitationParamHolder = prepareExoClass( + baggage, + 'InvitationParamHolder', + InvitationParamHolderI, + (name, current, amount) => { + return { name, current, amount }; + }, + { + makeDescription() { + const { state } = this; + return { + type: ParamTypes.INVITATION, + value: state.amount, + }; + }, + async prepareToUpdate(invite) { + const { state } = this; + const [preparedAmount] = await Promise.all([ + E(E(zoe).getInvitationIssuer()).getAmountOf(invite), + assertInvitation(invite), + ]); + + state.amount = preparedAmount; + return [invite, preparedAmount]; + }, + update(inviteAndAmount) { + const [invite, amount] = inviteAndAmount; + this.state.amount = amount; + this.state.current = invite; + return harden({ [this.state.name]: amount }); + }, + + getValue() { + return this.state.current; + }, + /** + * Called when preparing to change value to validate the new value. + * + * @param {any} proposed + */ + getVisibleValue(proposed) { + const { + state: { name, amount }, + } = this; + mustMatch( + proposed, + InvitationShape, + `Proposed value ${proposed} for ${name} must match InvitationShape`, + ); + + return amount; + }, + getGetterElement() { + const { state } = this; + + // names are keywords so they will necessarily be TitleCase + const name = `get${state.name}`; + const fn = () => state.amount; + return [name, fn]; + }, + getFarGetterElement() { + const { state } = this; + + // names are keywords so they will necessarily be TitleCase + const name = `get${state.name}`; + const fn = ToFarFunction(name, () => state.amount); + const guard = M.call().returns(AmountShape); + return [name, fn, guard]; + }, + }, + { + stateShape: { + name: KeywordShape, + current: M.or(M.undefined(), M.eref(InvitationShape)), + // XXX is there a tighter declaration for InvitationAmountShape? + amount: M.or(AmountShape, M.undefined()), + }, + }, + ); + + const GovernedParamManagerI = M.interface('GovernedParamManager', { + getInternalParamValue: M.call(M.string()).returns(M.any()), + getters: M.call().returns(M.recordOf(M.string(), M.any())), + getterFunctions: M.call().returns(M.recordOf(M.string(), M.any())), + accessors: M.call().returns({ + behavior: M.recordOf(M.string(), M.any()), + guards: M.recordOf(M.string(), M.pattern()), + }), + getPublicTopics: M.call().optional(BrandShape).returns(M.promise()), + updateParams: M.call(M.any()).returns(M.promise()), + getParamDescriptions: M.call().returns( + M.recordOf(M.string(), { type: M.string(), value: M.any() }), + ), + getGovernedParams: M.call().returns( + M.recordOf(M.string(), { type: M.string(), value: M.any() }), + ), + getVisibleValue: M.call().returns(M.any()), + publish: M.call().returns(M.promise()), + }); + + const makeParamManagerExo = prepareExoClass( + baggage, + 'Param manager', + GovernedParamManagerI, + (namesToParams, recorderKit) => { + const { subscriber, recorder } = recorderKit; + return { + namesToParams, + subscriber, + recorder, + }; + }, + { + // arguably should be a separate facet, but this object must be held + // tightly because of updateParams, and publish() is much less sensitive. + publish() { + const { state } = this; + /** @type {ParamStateRecord} */ + const current = Object.fromEntries( + [...state.namesToParams.entries()].map(([k, paramHolder]) => [ + k, + paramHolder.makeDescription(), + ]), + ); + return E(state.recorder).write({ current }); + }, + getParamDescriptions() { + const { state } = this; + + /** @type {ParamStateRecord} */ + const descriptions = {}; + for (const [name, paramHolder] of state.namesToParams.entries()) { + descriptions[name] = paramHolder.makeDescription(); + } + + return harden(descriptions); + }, + getGovernedParams() { + const { self } = this; + return self.getParamDescriptions(); + }, + getInternalParamValue(name) { + const { state } = this; + return state.namesToParams.get(name).getValue(); + }, + getterFunctions() { + const { state } = this; + const behavior = {}; + for (const holder of state.namesToParams.values()) { + const [name, fn] = holder.getGetterElement(); + behavior[name] = fn; + } + + return harden(behavior); + }, + getPublicTopics() { + // deconstructed from makeRecorderTopic() + return E.when(E(this.state.recorder).getStoragePath(), storagePath => + harden({ + governance: { + description: 'parameters', + subscriber: this.state.subscriber, + storagePath, + }, + }), + ); + }, + accessors() { + const { state } = this; + /** @type {Record} */ + const behavior = {}; + /** @type {Record} */ + const guards = {}; + for (const holder of state.namesToParams.values()) { + const [name, fn, clause] = holder.getFarGetterElement(); + behavior[name] = fn; + guards[name] = clause; + } + + return harden({ behavior, guards }); + }, + getters() { + const { state } = this; + const behavior = {}; + for (const holder of state.namesToParams.values()) { + const [name, fn] = holder.getFarGetterElement(); + behavior[name] = fn; + } + + return behavior; + }, + async updateParams(paramChanges) { + const { self, state } = this; + const paramNames = Object.keys(paramChanges); + + // promises to prepare every update + const asyncResults = paramNames.map(name => { + const paramHolder = state.namesToParams.get(name); + return paramHolder.prepareToUpdate(paramChanges[name]); + }); + + // if any update doesn't succeed, fail the request + const prepared = await Promise.all(asyncResults); + + // actually update + for (const [i, name] of paramNames.entries()) { + const paramHolder = state.namesToParams.get(name); + paramHolder.update(prepared[i]); + } + self.publish(); + }, + getVisibleValue(name, proposed) { + const { state } = this; + const paramHolder = state.namesToParams.get(name); + return paramHolder.getVisibleValue(proposed); + }, + }, + { + finish: ({ self }) => { + self.publish(); + }, + }, + ); + + return { + makeCopyParamHolder, + makeInvitationParamHolder, + makeParamManagerExo, + }; +}; +harden(buildParamGovernanceExoMakers); + +/** @typedef {ReturnType} ParamGovernanceExoMakers */ + +/** + * @param {ParamManager<*>} paramManager * @param {{[CONTRACT_ELECTORATE]: ParamValueTyped<'invitation'>}} governedParams */ -const assertElectorateMatches = (paramManager, governedParams) => { - const managerElectorate = - paramManager.getInvitationAmount(CONTRACT_ELECTORATE); +export const assertElectorateMatches = async (paramManager, governedParams) => { + const { behavior } = paramManager.accessors(); + const managerElectorate = behavior.getElectorate(); const { - [CONTRACT_ELECTORATE]: { value: paramElectorate }, + Electorate: { value: paramElectorate }, } = governedParams; - paramElectorate || - Fail`Missing ${q(CONTRACT_ELECTORATE)} term in ${q(governedParams)}`; + paramElectorate || Fail`Missing Electorate term in ${q(governedParams)}`; keyEQ(managerElectorate, paramElectorate) || Fail`Electorate in manager (${managerElectorate})} incompatible with terms (${paramElectorate}`; }; +harden(assertElectorateMatches); /** - * @typedef {object} ParamManagerBuilder - * @property {(name: string, value: Amount) => ParamManagerBuilder} addAmount - * @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) => ParamManagerBuilder} addInvitation - * @property {(name: string, value: bigint) => ParamManagerBuilder} addNat - * @property {(name: string, value: Ratio) => ParamManagerBuilder} addRatio - * @property {(name: string, value: import('@endo/marshal').CopyRecord) => ParamManagerBuilder} addRecord - * @property {(name: string, value: string) => ParamManagerBuilder} addString - * @property {(name: string, value: import('@agoric/time/src/types').Timestamp) => ParamManagerBuilder} addTimestamp - * @property {(name: string, value: import('@agoric/time/src/types').RelativeTime) => ParamManagerBuilder} addRelativeTime - * @property {(name: string, value: any) => ParamManagerBuilder} addUnknown - * @property {() => AnyParamManager} build - */ - -/** - * @param {import('@agoric/notifier').StoredPublisherKit} publisherKit - * @param {ERef} [zoe] + * @param {import('@agoric/vat-data').Baggage} baggage + * @param {import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit} recorderKit + * @param {ParamGovernanceExoMakers} makers + * @param {ZCF} [zcf] */ -const makeParamManagerBuilder = (publisherKit, zoe) => { +export const makeParamManagerBuilder = (baggage, recorderKit, makers, zcf) => { /** @type {MapStore} */ - const namesToParams = makeScalarMapStore('Parameter Name'); - const { publisher, subscriber } = publisherKit; - assertAllDefined({ publisher, subscriber }); - - const getters = {}; - const setters = {}; - const unfinishedParams = []; - - // XXX let params be finished async. A concession to upgradability - // UNTIL https://github.com/Agoric/agoric-sdk/issues/4343 - const finishBuilding = async () => { - await Promise.all(unfinishedParams); - unfinishedParams.length = 0; - }; - - const publish = () => { - /** @type {ParamStateRecord} */ - const current = Object.fromEntries( - [...namesToParams.entries()].map(([k, v]) => [k, v.makeDescription()]), - ); - publisher.updateState({ current }); - }; + const namesToHolders = makeScalarBigMapStore('Parameter Holders', { + durable: true, + }); + assertAllDefined(recorderKit); /** * Support for parameters that are copy objects @@ -89,327 +466,146 @@ const makeParamManagerBuilder = (publisherKit, zoe) => { * * @param {Keyword} name * @param {unknown} value - * @param {(val) => void} assertion + * @param {import('@endo/patterns').Pattern} pattern * @param {ParamType} type */ - const buildCopyParam = (name, value, assertion, type) => { - let current; + const buildCopyParam = (name, value, pattern, type) => { assertKeywordName(name); value !== undefined || Fail`param ${q(name)} must be defined`; - const setParamValue = newValue => { - assertion(newValue); - current = newValue; - return harden({ [name]: newValue }); - }; - setParamValue(value); - - const getVisibleValue = proposed => { - assertion(proposed); - return proposed; - }; - - const publicMethods = Far(`Parameter ${name}`, { - getValue: () => current, - assertType: assertion, - makeDescription: () => ({ type, value: current }), - getVisibleValue, - getType: () => type, - }); - - // names are keywords so they will necessarily be TitleCase - // eslint-disable-next-line no-use-before-define - getters[`get${name}`] = () => getTypedParam(type, name); - // 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. - setters[`update${name}`] = setParamValue; - setters[`prepareToUpdate${name}`] = proposedValue => proposedValue; - namesToParams.init(name, publicMethods); + const holder = makers.makeCopyParamHolder(name, value, type, pattern); + namesToHolders.init(name, holder); }; // HANDLERS FOR EACH PARAMETER TYPE ///////////////////////////////////////// /** @type {(name: string, value: Amount, builder: ParamManagerBuilder) => ParamManagerBuilder} */ const addAmount = (name, value, builder) => { - const assertAmount = a => { - a.brand || Fail`Expected an Amount for ${q(name)}, got: ${a}`; - return AmountMath.coerce(value.brand, a); - }; - buildCopyParam(name, value, assertAmount, ParamTypes.AMOUNT); + const brandedAmountShape = harden({ + brand: value.brand, + value: AmountValueShape, + }); + buildCopyParam(name, value, brandedAmountShape, ParamTypes.AMOUNT); return builder; }; /** @type {(name: string, value: Brand, builder: ParamManagerBuilder) => ParamManagerBuilder} */ const addBrand = (name, value, builder) => { - const assertBrand = makeLooksLikeBrand(name); - buildCopyParam(name, value, assertBrand, ParamTypes.BRAND); + buildCopyParam(name, value, BrandShape, ParamTypes.BRAND); return builder; }; /** @type {(name: string, value: Installation, builder: ParamManagerBuilder) => ParamManagerBuilder} */ const addInstallation = (name, value, builder) => { - const assertInstallation = makeAssertInstallation(name); - buildCopyParam(name, value, assertInstallation, ParamTypes.INSTALLATION); + buildCopyParam(name, value, InstallationShape, ParamTypes.INSTALLATION); return builder; }; /** @type {(name: string, value: Instance, builder: ParamManagerBuilder) => ParamManagerBuilder} */ const addInstance = (name, value, builder) => { - const assertInstance = makeAssertInstance(name); - buildCopyParam(name, value, assertInstance, ParamTypes.INSTANCE); + buildCopyParam(name, value, InstanceHandleShape, ParamTypes.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, ParamTypes.NAT); + buildCopyParam(name, value, M.nat(), ParamTypes.NAT); return builder; }; /** @type {(name: string, value: Ratio, builder: ParamManagerBuilder) => ParamManagerBuilder} */ const addRatio = (name, value, builder) => { - const assertBrandedRatio = makeAssertBrandedRatio(name, value); - buildCopyParam(name, value, assertBrandedRatio, ParamTypes.RATIO); + const shape = makeBrandedRatioPattern(value); + buildCopyParam(name, value, shape, ParamTypes.RATIO); return builder; }; /** @type {(name: string, value: import('@endo/marshal').CopyRecord, builder: ParamManagerBuilder) => ParamManagerBuilder} */ const addRecord = (name, value, builder) => { - const assertRecord = v => { - passStyleOf(v); - assert.typeof(v, 'object'); - }; - buildCopyParam(name, value, assertRecord, ParamTypes.PASSABLE_RECORD); + buildCopyParam(name, value, M.record(), ParamTypes.PASSABLE_RECORD); 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, ParamTypes.STRING); + buildCopyParam(name, value, M.string(), ParamTypes.STRING); return builder; }; /** @type {(name: string, value: import('@agoric/time/src/types').Timestamp, builder: ParamManagerBuilder) => ParamManagerBuilder} */ const addTimestamp = (name, value, builder) => { - buildCopyParam(name, value, assertTimestamp, ParamTypes.TIMESTAMP); + buildCopyParam(name, value, TimestampShape, ParamTypes.TIMESTAMP); return builder; }; /** @type {(name: string, value: import('@agoric/time/src/types').RelativeTime, builder: ParamManagerBuilder) => ParamManagerBuilder} */ const addRelativeTime = (name, value, builder) => { - buildCopyParam(name, value, assertRelativeTime, ParamTypes.RELATIVE_TIME); + buildCopyParam(name, value, RelativeTimeShape, ParamTypes.RELATIVE_TIME); return builder; }; /** @type {(name: string, value: any, builder: ParamManagerBuilder) => ParamManagerBuilder} */ const addUnknown = (name, value, builder) => { - const assertUnknown = _v => true; - buildCopyParam(name, value, assertUnknown, ParamTypes.UNKNOWN); + buildCopyParam(name, value, M.any(), ParamTypes.UNKNOWN); return builder; }; - const assertInvitation = async i => { - if (!zoe) { - throw Fail`zoe must be provided for governed Invitations ${zoe}`; - } - const { instance, installation } = await E(zoe).getInvitationDetails(i); - // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error -- the build config doesn't expect an error here - // @ts-ignore typedefs say they're guaranteed truthy but just to be safe - assert(instance && installation, 'must be an invitation'); - }; - /** * Invitations are closely held, so we should publicly reveal only the amount. * The approach here makes it possible for contracts to get the actual * invitation privately, and legibly assure clients that it matches the * publicly visible invitation amount. Contract reviewers still have to * manually verify that the actual invitation is handled carefully. - * `getInternalValue()` will only be accessible within the contract. + * `getValue()` will only be accessible within the contract. * * @param {string} name * @param {Invitation} invitation + * @param {Amount} amount */ - const buildInvitationParam = (name, invitation) => { - if (!zoe) { - throw Fail`zoe must be provided for governed Invitations ${zoe}`; - } - let currentInvitation; - let currentAmount; - - /** - * @typedef {[Invitation, Amount]} SetInvitationParam - */ - - /** - * Async phase to prepare for synchronous setting - * - * @param {Invitation} invite - * @returns {Promise} - */ - const prepareToSetInvitation = async invite => { - const [preparedAmount] = await Promise.all([ - E(E(zoe).getInvitationIssuer()).getAmountOf(invite), - assertInvitation(invite), - ]); - - return [invite, preparedAmount]; - }; - - /** - * Synchronous phase of value setting - * - * @param {SetInvitationParam} param0 - */ - const setInvitation = ([newInvitation, amount]) => { - currentAmount = amount; - currentInvitation = newInvitation; - return harden({ [name]: currentAmount }); - }; - - const makeDescription = () => { - return { type: ParamTypes.INVITATION, value: currentAmount }; - }; - - const getVisibleValue = async allegedInvitation => - E(E(zoe).getInvitationIssuer()).getAmountOf(allegedInvitation); - - const publicMethods = Far(`Parameter ${name}`, { - getValue: () => currentAmount, - getInternalValue: () => currentInvitation, - assertType: assertInvitation, - makeDescription, - getType: () => ParamTypes.INVITATION, - getVisibleValue, - }); - - // eslint-disable-next-line no-use-before-define - getters[`get${name}`] = () => getTypedParam(ParamTypes.INVITATION, name); - // CRUCIAL: here we're creating the update functions that can change the - // values of the governed contract's parameters. We'll return updateParams - // (which can invoke all of them) to our caller. They must handle it - // carefully to ensure that they end up in appropriate hands. - setters[`prepareToUpdate${name}`] = prepareToSetInvitation; - setters[`update${name}`] = setInvitation; - - // XXX let the value be set async. A concession to upgradability - // UNTIL https://github.com/Agoric/agoric-sdk/issues/4343 - const finishInvitationParam = E.when( - prepareToSetInvitation(invitation), - invitationAndAmount => { - setInvitation(invitationAndAmount); - // delay until currentAmount is defined because readers expect a valid value - namesToParams.init(name, publicMethods); - }, - ); - unfinishedParams.push(finishInvitationParam); - + const buildInvitationParam = (name, invitation, amount) => { + const holder = makers.makeInvitationParamHolder(name, invitation, amount); + namesToHolders.init(name, holder); return name; }; - /** @type {(name: string, value: Invitation, builder: ParamManagerBuilder) => ParamManagerBuilder} */ - const addInvitation = (name, value, builder) => { - assertKeywordName(name); - value !== null || Fail`param ${q(name)} must be defined`; - - buildInvitationParam(name, value); - - return builder; - }; - - // PARAM MANAGER METHODS //////////////////////////////////////////////////// - - const getTypedParam = (type, name) => { - const param = namesToParams.get(name); - type === param.getType() || Fail`${name} is not ${type}`; - return param.getValue(); - }; - - const getVisibleValue = (name, proposed) => { - const param = namesToParams.get(name); - return param.getVisibleValue(proposed); - }; + /** @type {(name: string, value: [Invitation, Amount], builder: ParamManagerBuilder) => ParamManagerBuilder} */ + const addInvitation = (name, [invitation, amount], builder) => { + if (!zcf) { + throw Fail`zcf must be provided for governed Invitations ${zcf}`; + } + const zoe = zcf?.getZoeService(); - // should be exposed within contracts, and not externally, for invitations - /** @type {(name: string) => Promise} */ - const getInternalParamValue = async name => { - await finishBuilding(); - return namesToParams.get(name).getInternalValue(); - }; + assertKeywordName(name); + amount !== null || Fail`param ${q(name)} must be defined`; - const getParams = async () => { - await finishBuilding(); - /** @type {ParamStateRecord} */ - const descriptions = {}; - for (const [name, param] of namesToParams.entries()) { - descriptions[name] = param.makeDescription(); + if (!zoe) { + throw Fail`zoe must be provided for governed Invitations ${zoe}`; } - return harden(descriptions); - }; - - /** @type {UpdateParams} */ - const updateParams = async paramChanges => { - await finishBuilding(); - const paramNames = Object.keys(paramChanges); - // promises to prepare every update - const asyncResults = paramNames.map(name => - setters[`prepareToUpdate${name}`](paramChanges[name]), + void E.when( + E(E(zoe).getInvitationIssuer()).getAmountOf(invitation), + actualAmount => { + AmountMath.isEqual(actualAmount, amount) || + zcf.shutdownWithFailure( + Error(`mismatch between ${name} invitation and amount.`), + ); + }, ); - // if any update doesn't succeed, fail the request - const prepared = await Promise.all(asyncResults); - // actually update - for (const [i, name] of paramNames.entries()) { - const setFn = setters[`update${name}`]; - setFn(prepared[i]); - } - publish(); + buildInvitationParam(name, invitation, amount); + + return builder; }; // Called after all params have been added with their initial values const build = () => { - // XXX let params be finished async. A concession to upgradability - // UNTIL https://github.com/Agoric/agoric-sdk/issues/4343 - E.when(finishBuilding(), () => publish()); - - // 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: () => subscriber, - getAmount: name => getTypedParam(ParamTypes.AMOUNT, name), - getBrand: name => getTypedParam(ParamTypes.BRAND, name), - getInstance: name => getTypedParam(ParamTypes.INSTANCE, name), - getInstallation: name => getTypedParam(ParamTypes.INSTALLATION, name), - getInvitationAmount: name => getTypedParam(ParamTypes.INVITATION, name), - getNat: name => getTypedParam(ParamTypes.NAT, name), - getRatio: name => getTypedParam(ParamTypes.RATIO, name), - getRecord: name => getTypedParam(ParamTypes.PASSABLE_RECORD, name), - getString: name => getTypedParam(ParamTypes.STRING, name), - getTimestamp: name => getTypedParam(ParamTypes.TIMESTAMP, name), - getRelativeTime: name => getTypedParam(ParamTypes.RELATIVE_TIME, name), - getUnknown: name => getTypedParam(ParamTypes.UNKNOWN, name), - getVisibleValue, - getInternalParamValue, - // Getters and setters for each param value - ...getters, - updateParams, - // Collection of all getters for passing to read-only contexts - readonly: () => harden(getters), - }); + // CRUCIAL: Contracts should only export the paramManager to a + // contractGovernor, which should not expose updateParams(). + return makers.makeParamManagerExo(namesToHolders, recorderKit); }; /** @type {ParamManagerBuilder} */ - const builder = { + const builder = harden({ addAmount: (n, v) => addAmount(n, v, builder), addBrand: (n, v) => addBrand(n, v, builder), addInstallation: (n, v) => addInstallation(n, v, builder), @@ -423,11 +619,144 @@ const makeParamManagerBuilder = (publisherKit, zoe) => { addRelativeTime: (n, v) => addRelativeTime(n, v, builder), addTimestamp: (n, v) => addTimestamp(n, v, builder), build, - }; + }); return builder; }; - -harden(assertElectorateMatches); harden(makeParamManagerBuilder); -export { assertElectorateMatches, makeParamManagerBuilder }; +/** + * @typedef {object} ParamManagerBuilder + * @property {(name: string, value: Amount) => ParamManagerBuilder} addAmount + * @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, Amount]) => ParamManagerBuilder} addInvitation + * @property {(name: string, value: bigint) => ParamManagerBuilder} addNat + * @property {(name: string, value: Ratio) => ParamManagerBuilder} addRatio + * @property {(name: string, value: import('@endo/marshal').CopyRecord) => ParamManagerBuilder} addRecord + * @property {(name: string, value: string) => ParamManagerBuilder} addString + * @property {(name: string, value: import('@agoric/time').Timestamp) => ParamManagerBuilder} addTimestamp + * @property {(name: string, value: import('@agoric/time').RelativeTime) => ParamManagerBuilder} addRelativeTime + * @property {(name: string, value: any) => ParamManagerBuilder} addUnknown + * @property {() => AnyParamManager} build + */ +// * @property {() => AnyParamManager} buildSync + +/** + * Most clients should use the contractHelper's handleParamGovernance(). That + * calls makeParamManagerFromTerms(), which uses this makeParamManger() to make + * a single paramManager (PM) for the contract. When the contract needs multiple + * PMs (e.g. one per collateral type as the VaultFactory does), then the + * contract should use makeParamManager for the top-level PM, and any others + * that have async needs. The rest can use makeParamManagerSync. + * + * @see makeParamManagerSync + * @template {Record} T + * @param {import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit} recorderKit + * @param {import('@agoric/vat-data').Baggage} baggage + * @param {T} spec + * @param {ParamGovernanceExoMakers} makers + * @param {ZCF} [zcf] + * @returns {ParamManager<{[K in keyof T]: T[K][0]}>} + */ +export const makeParamManager = (recorderKit, baggage, spec, makers, zcf) => { + const builder = makeParamManagerBuilder(baggage, recorderKit, makers, zcf); + + for (const [name, [type, value]] of Object.entries(spec)) { + const add = builder[builderMethodName(type)]; + add || Fail`No builder method for param type ${q(type)}`; + add(name, value); + } + + return builder.build(); +}; +harden(makeParamManager); + +/** + * @template {Record & {Electorate: Invitation}} I Private invitation values + * @template {ParamTypesMap} M Map of types of custom governed terms + * @param {import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit} recorderKit + * @param {ZCF>} zcf + * @param {import('@agoric/vat-data').Baggage} baggage + * @param {I} invitations invitation objects, which must come from privateArgs + * @param {M} paramTypesMap + * @param {ParamGovernanceExoMakers} makers + * @returns {ParamManager} + */ +export const makeParamManagerFromTermsAndMakers = ( + recorderKit, + zcf, + baggage, + invitations, + paramTypesMap, + makers, +) => { + const { governedParams } = zcf.getTerms(); + /** @type {Array<[Keyword, SyncSpecTuple | AsyncSpecTuple]>} */ + const makerSpecEntries = Object.entries(paramTypesMap).map( + ([paramKey, paramType]) => [ + paramKey, + /** @type {SyncSpecTuple} */ ([ + paramType, + governedParams[paramKey].value, + ]), + ], + ); + + // Every governed contract has an Electorate param that starts as `initialPoserInvitation` private arg + for (const [name, invitation] of Object.entries(invitations)) { + /** @type {Amount<'set'>} */ + // @ts-expect-error It's an invitation amount + const invitationAmount = governedParams[name].value; + /** @type {AsyncSpecTuple} */ + const invitationTuple = [ + ParamTypes.INVITATION, + [invitation, invitationAmount], + ]; + makerSpecEntries.push([name, invitationTuple]); + } + const makerSpec = Object.fromEntries(makerSpecEntries); + makerSpec[CONTRACT_ELECTORATE] || + Fail`missing Electorate invitation param value`; + + return makeParamManager(recorderKit, baggage, makerSpec, makers, zcf); +}; +harden(makeParamManagerFromTermsAndMakers); + +/** + * @template {Record & {Electorate: Invitation}} I Private invitation values + * @template {ParamTypesMap} M Map of types of custom governed terms + * @param {import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit} recorderKit + * @param {ZCF>} zcf + * @param {import('@agoric/vat-data').Baggage} baggage + * @param {I} invitations invitation objects, which must come from privateArgs + * @param {M} paramTypesMap + * @param {string } paramManagerKey + * @returns {ParamManager} + */ +export const makeParamManagerFromTerms = ( + recorderKit, + zcf, + baggage, + invitations, + paramTypesMap, + paramManagerKey, +) => { + // defines kinds. No top-level awaits before this finishes + const paramMakerKit = buildParamGovernanceExoMakers( + zcf.getZoeService(), + baggage, + ); + + return provide(baggage, paramManagerKey, () => + makeParamManagerFromTermsAndMakers( + recorderKit, + zcf, + baggage, + invitations, + paramTypesMap, + paramMakerKit, + ), + ); +}; +harden(makeParamManagerFromTerms); diff --git a/packages/governance/src/contractGovernance/typedParamManager.js b/packages/governance/src/contractGovernance/typedParamManager.js deleted file mode 100644 index 5ec693ce7a68..000000000000 --- a/packages/governance/src/contractGovernance/typedParamManager.js +++ /dev/null @@ -1,170 +0,0 @@ -import { E } from '@endo/eventual-send'; -import { ParamTypes } from '../constants.js'; -import { CONTRACT_ELECTORATE } from './governParam.js'; -import { makeParamManagerBuilder } from './paramManager.js'; - -const { Fail, quote: q } = assert; - -/** - * @typedef {Record} ParamTypesMap - */ -/** - * @template {ParamStateRecord} M - * @typedef {{ [R in keyof M]: M[R]['type']}} ParamTypesMapFromRecord - */ -/** - * @template {ParamTypesMap} M - * @typedef {{ [T in keyof M]: { type: M[T], value: ParamValueForType } }} ParamRecordsFromTypes - */ - -/** - * @template {ParamTypesMap} M - * @typedef {{ - * [K in keyof M as `get${string & K}`]: () => ParamValueForType - * }} Getters - */ - -/** - * @template {ParamTypesMap} T - * @typedef {{ - * [K in keyof T as `update${string & K}`]: (value: ParamValueForType) => void - * }} Updaters - */ - -/** - * @template {ParamTypesMap} M - * @typedef {ParamManagerBase & Getters & Updaters & {readonly: () => Getters} & {updateParams: UpdateParams}} TypedParamManager - */ - -/** - * @param {ParamType} type - */ -const builderMethodName = type => - `add${type[0].toUpperCase() + type.substring(1)}`; - -/** @type {Partial>} */ -const isAsync = { - invitation: true, -}; - -/** - * @template {ParamType} T - * @typedef {[type: T, value: ParamValueForType]} ST param spec tuple - */ - -/** - * @typedef {{ type: 'invitation', value: Amount<'set'> }} InvitationParam - */ - -// XXX better to use the manifest constant ParamTypes -// but importing that here turns this file into a module, -// breaking the ambient typing -/** - * @typedef {ST<'amount'> - * | ST<'brand'> - * | ST<'installation'> - * | ST<'instance'> - * | ST<'nat'> - * | ST<'ratio'> - * | ST<'string'> - * | ST<'timestamp'> - * | ST<'relativeTime'> - * | ST<'unknown'>} SyncSpecTuple - * - * @typedef {['invitation', Invitation]} AsyncSpecTuple - */ - -/** - * @see makeParamManagerSync - * @template {Record} T - * @param {import('@agoric/notifier').StoredPublisherKit} publisherKit - * @param {T} spec - * @param {ZCF} zcf - * @returns {TypedParamManager<{[K in keyof T]: T[K][0]}>} - */ -export const makeParamManager = (publisherKit, spec, zcf) => { - const builder = makeParamManagerBuilder(publisherKit, zcf.getZoeService()); - - const promises = []; - for (const [name, [type, value]] of Object.entries(spec)) { - const add = builder[builderMethodName(type)]; - if (isAsync[type]) { - promises.push(add(name, value)); - } else { - add(name, value); - } - } - // XXX kick off promises but don't block. This is a concession to contract reincarnation - // which cannot block on a remote call. - // UNTIL https://github.com/Agoric/agoric-sdk/issues/4343 - void E.when(Promise.all(promises), undefined, reason => - zcf.shutdownWithFailure(reason), - ); - - // @ts-expect-error cast - return builder.build(); -}; -harden(makeParamManager); - -/** - * Used only when the contract has multiple param managers. - * Exactly one must manage the electorate, which requires the async version. - * - * @see makeParamManager - * @template {Record} T - * @param {import('@agoric/notifier').StoredPublisherKit} publisherKit - * @param {T} spec - * @returns {TypedParamManager<{[K in keyof T]: T[K][0]}>} - */ -export const makeParamManagerSync = (publisherKit, spec) => { - const builder = makeParamManagerBuilder(publisherKit); - - for (const [name, [type, value]] of Object.entries(spec)) { - const add = builder[builderMethodName(type)]; - add || Fail`No builder method for param type ${q(type)}`; - add(name, value); - } - - // @ts-expect-error cast - return builder.build(); -}; -harden(makeParamManagerSync); - -/** - * @template {Record & {Electorate: Invitation}} I Private invitation values - * @template {ParamTypesMap} M Map of types of custom governed terms - * @param {import('@agoric/notifier').StoredPublisherKit} publisherKit - * @param {ZCF>} zcf - * @param {I} invitations invitation objects, which must come from privateArgs - * @param {M} paramTypesMap - * @returns {TypedParamManager} - */ -export const makeParamManagerFromTerms = ( - publisherKit, - zcf, - invitations, - paramTypesMap, -) => { - const { governedParams } = zcf.getTerms(); - /** @type {Array<[Keyword, SyncSpecTuple | AsyncSpecTuple]>} */ - const makerSpecEntries = Object.entries(paramTypesMap).map( - ([paramKey, paramType]) => [ - paramKey, - /** @type {SyncSpecTuple} */ ([ - paramType, - governedParams[paramKey].value, - ]), - ], - ); - // Every governed contract has an Electorate param that starts as `initialPoserInvitation` private arg - for (const [name, invitation] of Object.entries(invitations)) { - makerSpecEntries.push([name, [ParamTypes.INVITATION, invitation]]); - } - const makerSpec = Object.fromEntries(makerSpecEntries); - makerSpec[CONTRACT_ELECTORATE] || - Fail`missing Electorate invitation param value`; - - // @ts-expect-error cast - return makeParamManager(publisherKit, makerSpec, zcf); -}; -harden(makeParamManagerFromTerms); diff --git a/packages/governance/src/contractGovernor.js b/packages/governance/src/contractGovernor.js index bd18551d16c5..e9fe7cb8378e 100644 --- a/packages/governance/src/contractGovernor.js +++ b/packages/governance/src/contractGovernor.js @@ -140,7 +140,7 @@ harden(validateQuestionFromCounter); * governedContractInstallation: Installation, * governed: { * issuerKeywordRecord: IssuerKeywordRecord, - * terms: {governedParams: {[CONTRACT_ELECTORATE]: import('./contractGovernance/typedParamManager.js').InvitationParam}}, + * terms: {governedParams: {[CONTRACT_ELECTORATE]: import('./contractGovernance/paramManager.js').InvitationParam}}, * label?: string, * } * }>} zcf diff --git a/packages/governance/src/contractGovernorKit.js b/packages/governance/src/contractGovernorKit.js index 542d3e2450fc..7aad0703e7f6 100644 --- a/packages/governance/src/contractGovernorKit.js +++ b/packages/governance/src/contractGovernorKit.js @@ -53,9 +53,8 @@ export const prepareContractGovernorKit = (baggage, powers) => { /** @type {() => Promise} */ async getElectorateInstance() { const { publicFacet } = this.state; - const invitationAmount = await E(publicFacet).getInvitationAmount( - CONTRACT_ELECTORATE, - ); + // @ts-expect-error All governed contracts have an Electorate + const invitationAmount = await E(publicFacet).getElectorate(); return invitationAmount.value[0].instance; }, /** @type {() => Promise} */ @@ -131,7 +130,7 @@ export const prepareContractGovernorKit = (baggage, powers) => { */ replaceElectorate(poserInvitation) { const { creatorFacet } = this.state; - /** @type {Promise>} */ + /** @type {Promise>} */ // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error -- the build config doesn't expect an error here // @ts-ignore cast const paramMgr = E(E(creatorFacet).getParamMgrRetriever()).get({ diff --git a/packages/governance/src/contractHelper.js b/packages/governance/src/contractHelper.js index 1f42b7e72910..1938302e6e69 100644 --- a/packages/governance/src/contractHelper.js +++ b/packages/governance/src/contractHelper.js @@ -1,48 +1,32 @@ import { Far } from '@endo/marshal'; -import { makeStoredPublisherKit } from '@agoric/notifier'; -import { getMethodNames, objectMap } from '@agoric/internal'; -import { ignoreContext, prepareExo } from '@agoric/vat-data'; +import { objectMap } from '@agoric/internal'; +import { ignoreContext, prepareExo, provide } from '@agoric/vat-data'; import { keyEQ, M } from '@agoric/store'; -import { AmountShape, BrandShape } from '@agoric/ertp'; -import { RelativeTimeRecordShape, TimestampRecordShape } from '@agoric/time'; import { E } from '@endo/eventual-send'; -import { assertElectorateMatches } from './contractGovernance/paramManager.js'; -import { makeParamManagerFromTerms } from './contractGovernance/typedParamManager.js'; -import { GovernorFacetShape } from './typeGuards.js'; + +import { + assertElectorateMatches, + makeParamManagerFromTerms, +} from './contractGovernance/paramManager.js'; +import { GovernorFacetI } from './typeGuards.js'; const { Fail, quote: q } = assert; export const GOVERNANCE_STORAGE_KEY = 'governance'; -const publicMixinAPI = harden({ - getSubscription: M.call().returns(M.remotable('StoredSubscription')), - getGovernedParams: M.call().returns(M.or(M.record(), M.promise())), - getAmount: M.call().returns(AmountShape), - getBrand: M.call().returns(BrandShape), - getInstance: M.call().returns(M.remotable('Instance')), - getInstallation: M.call().returns(M.remotable('Installation')), - getInvitationAmount: M.call().returns(M.promise()), - getNat: M.call().returns(M.bigint()), - getRatio: M.call().returns(M.record()), - getString: M.call().returns(M.string()), - getTimestamp: M.call().returns(TimestampRecordShape), - getRelativeTime: M.call().returns(RelativeTimeRecordShape), - getUnknown: M.call().returns(M.any()), -}); - /** * @param {ZCF & {}>} zcf - * @param {import('./contractGovernance/typedParamManager').TypedParamManager} paramManager + * @param {import('./contractGovernance/paramManager').ParamManager} paramManager */ export const validateElectorate = (zcf, paramManager) => { const { governedParams } = zcf.getTerms(); - return E.when(paramManager.getParams(), finishedParams => { + return E.when(paramManager.getParamDescriptions(), async finishedParams => { try { keyEQ(governedParams, finishedParams) || Fail`The 'governedParams' term must be an object like ${q( - finishedParams, - )}, but was ${q(governedParams)}`; - assertElectorateMatches(paramManager, governedParams); + governedParams, + )}, but was ${q(finishedParams)}`; + return assertElectorateMatches(paramManager, governedParams); } catch (err) { zcf.shutdownWithFailure(err); } @@ -53,61 +37,44 @@ harden(validateElectorate); /** * Utility function for `makeParamGovernance`. * - * @template {import('./contractGovernance/typedParamManager.js').ParamTypesMap} T + * @template {import('./contractGovernance/paramManager.js').ParamTypesMap} T + * @param {import('@agoric/vat-data').Baggage} baggage * @param {ZCF & {}>} zcf - * @param {import('./contractGovernance/typedParamManager').TypedParamManager} paramManager + * @param {import('./contractGovernance/paramManager').ParamManager} paramManager */ -const facetHelpers = (zcf, paramManager) => { - // validate async to wait for params to be finished - // UNTIL https://github.com/Agoric/agoric-sdk/issues/4343 - void validateElectorate(zcf, paramManager); - - 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, - getTimestamp: paramManager.getTimestamp, - getRelativeTime: paramManager.getRelativeTime, - getUnknown: paramManager.getUnknown, - }; - +const facetHelpers = (baggage, zcf, paramManager) => { const commonPublicMethods = { - getSubscription: () => paramManager.getSubscription(), - getGovernedParams: () => paramManager.getParams(), + getParamDescriptions: () => paramManager.getParamDescriptions(), + getPublicTopics: () => paramManager.getPublicTopics(), + getGovernedParams: () => paramManager.getParamDescriptions(), }; - /** - * Add required methods to publicFacet - * - * @template {{}} PF public facet - * @param {PF} originalPublicFacet - * @returns {GovernedPublicFacet} - */ - const augmentPublicFacet = originalPublicFacet => { - return Far('publicFacet', { - ...originalPublicFacet, - ...commonPublicMethods, - ...typedAccessors, - }); - }; + const { behavior, guards } = paramManager.accessors(); /** - * Add required methods to publicFacet, for a virtual/durable contract + * Add required methods to publicFacet, for a durable contract * - * @param {OPF} originalPublicFacet * @template {{}} OPF + * @param {OPF} originalPublicFacet + * @param {Record} [publicFacetGuards] */ - const augmentVirtualPublicFacet = originalPublicFacet => { - return Far('publicFacet', { - ...originalPublicFacet, - ...commonPublicMethods, - ...objectMap(typedAccessors, ignoreContext), - }); + const augmentPublicFacet = ( + originalPublicFacet, + publicFacetGuards = undefined, + ) => { + const compiledGuards = publicFacetGuards + ? harden({ ...publicFacetGuards, ...guards }) + : undefined; + return prepareExo( + baggage, + 'publicFacet', + compiledGuards, + harden({ + ...originalPublicFacet, + ...commonPublicMethods, + ...objectMap(behavior, ignoreContext), + }), + ); }; /** @@ -116,8 +83,8 @@ const facetHelpers = (zcf, paramManager) => { * @param {Record unknown>} [governedApis] * @returns {GovernedCreatorFacet} */ - const makeFarGovernorFacet = (limitedCreatorFacet, governedApis = {}) => { - const governorFacet = Far('governorFacet', { + const makeGovernorFacet = (limitedCreatorFacet, governedApis = {}) => { + const governorFacet = prepareExo(baggage, 'governorFacet', GovernorFacetI, { getParamMgrRetriever: () => Far('paramRetriever', { get: () => paramManager }), getInvitation: name => paramManager.getInternalParamValue(name), @@ -137,101 +104,39 @@ const facetHelpers = (zcf, paramManager) => { return governorFacet; }; - /** - * @template {{}} CF - * @param {CF} originalCreatorFacet - * @param {{}} [governedApis] - * @returns {GovernedCreatorFacet} - */ - const makeGovernorFacet = (originalCreatorFacet, governedApis = {}) => { - return makeFarGovernorFacet(originalCreatorFacet, governedApis); - }; - - /** - * Add required methods to a creatorFacet for a durable contract. - * - * @see {makeDurableGovernorFacet} - * - * @param {{ [methodName: string]: (context?: unknown, ...rest: unknown[]) => unknown}} limitedCreatorFacet - */ - const makeVirtualGovernorFacet = limitedCreatorFacet => { - /** @type {import('@agoric/swingset-liveslots').FunctionsPlusContext>} */ - const governorFacet = harden({ - getParamMgrRetriever: () => - Far('paramRetriever', { get: () => paramManager }), - getInvitation: (_context, /** @type {string} */ name) => - paramManager.getInternalParamValue(name), - getLimitedCreatorFacet: ({ facets }) => facets.limitedCreatorFacet, - // The contract provides a facet with the APIs that can be invoked by - // governance - getGovernedApis: ({ facets }) => facets.governedApis, - // The facet returned by getGovernedApis is Far, so we can't see what - // methods it has. There's no clean way to have contracts specify the APIs - // without also separately providing their names. - getGovernedApiNames: ({ facets }) => - getMethodNames(facets.governedApis || {}), - setOfferFilter: (_context, strings) => zcf.setOfferFilter(strings), - }); - - return { governorFacet, limitedCreatorFacet }; - }; - - /** - * Add required methods to a creatorFacet for a durable contract. - * - * @see {makeVirtualGovernorFacet} - * - * @template CF - * @param {import('@agoric/vat-data').Baggage} baggage - * @param {CF} limitedCreatorFacet - * @param {Record unknown>} [governedApis] - */ - const makeDurableGovernorFacet = ( - baggage, - limitedCreatorFacet, - governedApis = {}, - ) => { - const governorFacet = prepareExo( + const makeParamReaderFacet = () => { + return prepareExo( baggage, - 'governorFacet', - M.interface('governorFacet', GovernorFacetShape), - { - getParamMgrRetriever: () => - Far('paramRetriever', { get: () => paramManager }), - getInvitation: name => paramManager.getInternalParamValue(name), - getLimitedCreatorFacet: () => limitedCreatorFacet, - // The contract provides a facet with the APIs that can be invoked by - // governance - /** @type {() => GovernedApis} */ - getGovernedApis: () => Far('governedAPIs', governedApis), - // The facet returned by getGovernedApis is Far, so we can't see what - // methods it has. There's no clean way to have contracts specify the APIs - // without also separately providing their names. - getGovernedApiNames: () => Object.keys(governedApis || {}), - setOfferFilter: strings => zcf.setOfferFilter(strings), - }, + 'ParamReader', + M.interface('ParamReader', guards), + behavior, ); - - return { governorFacet, limitedCreatorFacet }; }; return harden({ publicMixin: { ...commonPublicMethods, - ...typedAccessors, + ...behavior, + }, + publicMixinGuards: { + getParamDescriptions: M.call().returns(M.any()), + getGovernedParams: M.call().returns(M.any()), + getPublicTopics: M.call().returns(M.promise()), + ...guards, }, augmentPublicFacet, - augmentVirtualPublicFacet, makeGovernorFacet, - makeFarGovernorFacet, - makeVirtualGovernorFacet, - makeDurableGovernorFacet, - - params: paramManager.readonly(), + params: makeParamReaderFacet(), }); }; +/** + * @typedef {object} TypesAndValues + * @property {string} type + * @property {any} value + */ + /** * Helper for the 90% of contracts that will have only a single set of * parameters. Using this for managed parameters, a contract only has to @@ -244,37 +149,32 @@ const facetHelpers = (zcf, paramManager) => { * parameter values, and the governance guarantees only hold if they're not used * directly by the governed contract. * - * @template {import('./contractGovernance/typedParamManager').ParamTypesMap} M + * @template {import('./contractGovernance/paramManager').ParamTypesMap} M * Map of types of custom governed terms * @param {ZCF>} zcf + * @param {import('@agoric/vat-data').Baggage} baggage * @param {Invitation} initialPoserInvitation * @param {M} paramTypesMap - * @param {ERef} [storageNode] - * @param {ERef} [marshaller] + * @param {import('@agoric/zoe/src/contractSupport/recorder.js').MakeRecorderKit} makeRecorderKit + * @param {StorageNode} storageNode */ -const handleParamGovernance = ( +export const handleParamGovernance = ( zcf, + baggage, initialPoserInvitation, paramTypesMap, + makeRecorderKit, storageNode, - marshaller, ) => { - /** @type {import('@agoric/notifier').StoredPublisherKit} */ - const publisherKit = makeStoredPublisherKit( - storageNode, - marshaller, - GOVERNANCE_STORAGE_KEY, - ); const paramManager = makeParamManagerFromTerms( - publisherKit, + makeRecorderKit(storageNode), zcf, + baggage, { Electorate: initialPoserInvitation }, paramTypesMap, + 'Contract paramManager', ); - return facetHelpers(zcf, paramManager); + return facetHelpers(baggage, zcf, paramManager); }; - harden(handleParamGovernance); - -export { handleParamGovernance, publicMixinAPI }; diff --git a/packages/governance/src/index.js b/packages/governance/src/index.js index 6943c34c9c4d..0b7082819d7a 100644 --- a/packages/governance/src/index.js +++ b/packages/governance/src/index.js @@ -19,7 +19,7 @@ export { validateQuestionFromCounter, } from './contractGovernor.js'; -export { handleParamGovernance, publicMixinAPI } from './contractHelper.js'; +export { handleParamGovernance } from './contractHelper.js'; export { assertBallotConcernsParam, @@ -29,18 +29,22 @@ export { } from './contractGovernance/governParam.js'; export { - assertElectorateMatches, - makeParamManagerBuilder, + makeParamManagerFromTerms, + buildParamGovernanceExoMakers, + makeParamManagerFromTermsAndMakers, } from './contractGovernance/paramManager.js'; export { + assertElectorateMatches, + makeParamManagerBuilder, makeParamManager, - makeParamManagerSync, -} from './contractGovernance/typedParamManager.js'; +} from './contractGovernance/paramManager.js'; export { assertContractGovernance, assertContractElectorate, } from './validators.js'; +export { GovernorFacetI } from './typeGuards.js'; + export { ParamTypes } from './constants.js'; diff --git a/packages/governance/src/typeGuards.js b/packages/governance/src/typeGuards.js index 6e41726003d5..93dac9ec671d 100644 --- a/packages/governance/src/typeGuards.js +++ b/packages/governance/src/typeGuards.js @@ -223,7 +223,7 @@ export const BinaryVoteCounterPublicI = M.interface( }, ); -export const VoterHandle = M.remotable(); +export const VoterHandle = M.remotable('Voter handle'); export const BinaryVoteCounterAdminI = M.interface( 'BinaryVoteCounter AdminFacet', { @@ -259,12 +259,11 @@ export const VoteCounterCloseI = M.interface('VoteCounter CloseFacet', { closeVoting: M.call().returns(), }); -export const GovernorFacetShape = { +export const GovernorFacetI = M.interface('governedFacet', { getParamMgrRetriever: M.call().returns(M.remotable('paramRetriever')), getInvitation: M.call().returns(InvitationShape), - getLimitedCreatorFacet: M.call().returns(M.remotable()), + getLimitedCreatorFacet: M.call().returns(M.any()), getGovernedApis: M.call().returns(M.remotable('governedAPIs')), getGovernedApiNames: M.call().returns(M.arrayOf(M.string())), setOfferFilter: M.call(M.arrayOf(M.string())).returns(M.promise()), -}; -harden(GovernorFacetShape); +}); diff --git a/packages/governance/src/types-ambient.js b/packages/governance/src/types-ambient.js index c94c98b41ee4..25523e7edd8e 100644 --- a/packages/governance/src/types-ambient.js +++ b/packages/governance/src/types-ambient.js @@ -63,10 +63,10 @@ /** * Terms a contract must provide in order to be governed. * - * @template {import('./contractGovernance/typedParamManager.js').ParamTypesMap} T Governed parameters of contract + * @template {import('./contractGovernance/paramManager.js').ParamTypesMap} T Governed parameters of contract * @typedef {{ * electionManager: import('@agoric/zoe/src/zoeService/utils.js').Instance, - * governedParams: import('./contractGovernance/typedParamManager.js').ParamRecordsFromTypes * }} GovernanceTerms @@ -421,23 +421,20 @@ /** * @typedef {object} ParamManagerBase The base paramManager with typed getters - * @property {() => ERef} getParams - * @property {(name: string) => Amount} getAmount - * @property {(name: string) => Brand} getBrand - * @property {(name: string) => Instance} getInstance - * @property {(name: string) => Installation} getInstallation - * @property {(name: string) => Amount<'set'>} getInvitationAmount - * @property {(name: string) => bigint} getNat - * @property {(name: string) => Ratio} getRatio - * @property {(name: string) => string} getString - * @property {(name: string) => import('@agoric/time/src/types').TimestampRecord} getTimestamp - * @property {(name: string) => import('@agoric/time/src/types').RelativeTimeRecord} getRelativeTime - * @property {(name: string) => any} getUnknown + * @property {() => any} accessors return an array of arrays giving the + * names, functions, and guards for generated getters. They can be used to + * add methods to Exo objects. * @property {(name: string, proposedValue: ParamValue) => ParamValue} getVisibleValue - for * most types, the visible value is the same as proposedValue. For Invitations * the visible value is the amount of the invitation. * @property {(name: string) => Promise} getInternalParamValue - * @property {() => StoredSubscription} getSubscription + * @property {() => Record} getGovernedParams deprecated; use getParamDescriptions instead. + * @property {() => Record} getParamDescriptions provide a record describing + * the values of all governed parameters. + * @property {() => Record} getPublicTopics + * @property {() => Record} publish + * @property {() => Record} getterFunctions + * @property {() => Record} getters */ /** @@ -567,10 +564,10 @@ /** * @typedef GovernedPublicFacetMethods - * @property {(key?: any) => StoredSubscription} getSubscription - * @property {(key?: any) => ERef} getGovernedParams - get descriptions of + * @property {(key?: any) => import('@agoric/zoe/src/contractSupport/topics.js').TopicsRecord} getPublicTopics + * @property {(key?: any) => ERef} getGovernedParams - deprecated. use getParamDescriptions + * @property {(key?: any) => ERef} getParamDescriptions - get descriptions of * all the governed parameters - * @property {(name: string) => Amount} getInvitationAmount */ /** diff --git a/packages/governance/test/swingsetTests/contractGovernor/bootstrap.js b/packages/governance/test/swingsetTests/contractGovernor/bootstrap.js index 4d7dae1ae9ff..3a53aa8d4ffc 100644 --- a/packages/governance/test/swingsetTests/contractGovernor/bootstrap.js +++ b/packages/governance/test/swingsetTests/contractGovernor/bootstrap.js @@ -145,7 +145,8 @@ const watchParams = async (zoe, contractInstanceP, log) => { const contractInstance = await contractInstanceP; /** @type {GovernedPublicFacetMethods} */ const contractPublic = E(zoe).getPublicFacet(contractInstance); - const subscription = await E(contractPublic).getSubscription(); + const topic = await E.get(E(contractPublic).getPublicTopics()).governance; + const { subscriber } = topic; /** @type {any} */ let prev = await E(contractPublic).getGovernedParams(); const paramChangeObserver = Far('param observer', { @@ -167,7 +168,7 @@ const watchParams = async (zoe, contractInstanceP, log) => { prev = current; }, }); - void observeIteration(subscribeEach(subscription), paramChangeObserver); + void observeIteration(subscribeEach(subscriber), paramChangeObserver); }; const setupParameterChanges = async ( @@ -312,6 +313,8 @@ const makeBootstrap = (argv, cb, vatPowers) => async (vats, devices) => { ).startInstance(installations.contractGovernor, {}, governedContractTerms, { governed: { initialPoserInvitation, + storageNode: makeMockChainStorageRoot().makeChildNode('governed'), + marshaller: remoteNullMarshaller, }, }); const governedInstance = E(governor).getInstance(); diff --git a/packages/governance/test/swingsetTests/contractGovernor/governedContract.js b/packages/governance/test/swingsetTests/contractGovernor/governedContract.js index a317f6552a3b..85a16e69a426 100644 --- a/packages/governance/test/swingsetTests/contractGovernor/governedContract.js +++ b/packages/governance/test/swingsetTests/contractGovernor/governedContract.js @@ -1,3 +1,5 @@ +import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js'; + import { handleParamGovernance } from '../../../src/contractHelper.js'; import { ParamTypes } from '../../../src/index.js'; import { CONTRACT_ELECTORATE } from '../../../src/contractGovernance/governParam.js'; @@ -17,19 +19,32 @@ const makeTerms = (number, invitationAmount) => { }; /** - * @type ContractStartFn< - * GovernedPublicFacet<{}>, - * GovernedCreatorFacet, - * GovernanceTerms<{ - * MalleableNumber: 'nat', - * }>, - * {initialPoserInvitation: Invitation}> + * @param {ZCF>} zcf + * @param {{ + * governed: Record, + * marshaller: Marshaller, + * initialPoserInvitation: Invitation, + * storageNode: StorageNode, + * }} privateArgs + * @param {import('@agoric/vat-data').Baggage} baggage */ -const start = async (zcf, privateArgs) => { +const start = async (zcf, privateArgs, baggage) => { + const { makeRecorderKit } = prepareRecorderKitMakers( + baggage, + privateArgs.marshaller, + ); + const { augmentPublicFacet, makeGovernorFacet, params } = - await handleParamGovernance(zcf, privateArgs.initialPoserInvitation, { - [MALLEABLE_NUMBER]: ParamTypes.NAT, - }); + await handleParamGovernance( + zcf, + baggage, + privateArgs.initialPoserInvitation, + { + [MALLEABLE_NUMBER]: ParamTypes.NAT, + }, + makeRecorderKit, + privateArgs.storageNode, + ); let governanceAPICalled = 0; const governanceApi = () => (governanceAPICalled += 1); diff --git a/packages/governance/test/swingsetTests/contractGovernor/test-governor.js b/packages/governance/test/swingsetTests/contractGovernor/test-governor.js index 944f419e7f1f..ee8ef17a36c8 100644 --- a/packages/governance/test/swingsetTests/contractGovernor/test-governor.js +++ b/packages/governance/test/swingsetTests/contractGovernor/test-governor.js @@ -97,10 +97,10 @@ test.serial('contract governance', async t => { '@@ tick:2 @@', '@@ tick:3 @@', 'vote outcome: {"changes":{"MalleableNumber":"[299792458n]"}}', - 'params update: MalleableNumber', - 'current value of MalleableNumber is 299792458', 'updated to {"changes":{"MalleableNumber":"[299792458n]"}}', 'Number after: 299792458', + 'params update: MalleableNumber', + 'current value of MalleableNumber is 299792458', ]); }); @@ -119,9 +119,9 @@ test.serial('change electorate', async t => { '@@ tick:1 @@', '@@ tick:2 @@', 'vote outcome: {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: BundleInstallation]","instance":"[Alleged: InstanceHandle]"}]}}}', + 'updated to ({"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: BundleInstallation]","instance":"[Alleged: InstanceHandle]"}]}}})', 'params update: Electorate', 'current value of MalleableNumber is 602214090000000000000000', - 'updated to ({"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: BundleInstallation]","instance":"[Alleged: InstanceHandle]"}]}}})', 'Validation complete', '@@ schedule task for:4, currently: 2 @@', 'Voter Alice voted for {"changes":{"MalleableNumber":"[299792458n]"}}', @@ -132,9 +132,9 @@ test.serial('change electorate', async t => { '@@ tick:3 @@', '@@ tick:4 @@', 'vote outcome: {"changes":{"MalleableNumber":"[299792458n]"}}', + 'updated to {"changes":{"MalleableNumber":"[299792458n]"}}', 'params update: MalleableNumber', 'current value of MalleableNumber is 299792458', - 'updated to {"changes":{"MalleableNumber":"[299792458n]"}}', ]); }); @@ -162,8 +162,8 @@ test.serial('brokenUpdateStart', async t => { // TODO: allow either message // 'vote rejected outcome: Error: (an object) was not a live payment for brand (an object). It could be a used-up payment, a payment for another brand, or it might not be a payment at all.', // 'update failed: Error: (an object) was not a live payment for brand (an object). It could be a used-up payment, a payment for another brand, or it might not be a payment at all.', - 'vote rejected outcome: Error: A Zoe invitation is required, not (an object)', - 'update failed: Error: A Zoe invitation is required, not (an object)', + 'vote rejected outcome: Error: (an object) was not a live payment for brand "[Alleged: Zoe Invitation brand]". It could be a used-up payment, a payment for another brand, or it might not be a payment at all.', + 'update failed: Error: (an object) was not a live payment for brand "[Alleged: Zoe Invitation brand]". It could be a used-up payment, a payment for another brand, or it might not be a payment at all.', ]); }); @@ -182,10 +182,10 @@ test.serial('changeTwoParams', async t => { '@@ tick:1 @@', '@@ tick:2 @@', 'vote outcome: {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: BundleInstallation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}}', - 'params update: Electorate,MalleableNumber', - 'current value of MalleableNumber is 42', 'updated to ({"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: BundleInstallation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}})', 'successful outcome: {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: BundleInstallation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}} ', + 'params update: Electorate,MalleableNumber', + 'current value of MalleableNumber is 42', 'Validation complete', ]); }); diff --git a/packages/governance/test/unitTests/test-buildParamManager.js b/packages/governance/test/unitTests/test-buildParamManager.js index 3585026d3613..0b13fb40eeca 100644 --- a/packages/governance/test/unitTests/test-buildParamManager.js +++ b/packages/governance/test/unitTests/test-buildParamManager.js @@ -2,45 +2,69 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { AmountMath, AssetKind, makeIssuerKit } from '@agoric/ertp'; -import { makeStoredPublisherKit } from '@agoric/notifier'; import { keyEQ } from '@agoric/store'; import { makeRatio } from '@agoric/zoe/src/contractSupport/index.js'; import { makeHandle } from '@agoric/zoe/src/makeHandle.js'; import { setupZCFTest } from '@agoric/zoe/test/unitTests/zcf/setupZcfTest.js'; import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; -import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; -import { makeParamManagerBuilder, ParamTypes } from '../../src/index.js'; +import { makeScalarBigMapStore } from '@agoric/vat-data'; +import { prepareMockRecorderKitMakers } from '@agoric/zoe/tools/mockRecorderKit.js'; -test('two parameters', t => { - const drachmaKit = makeIssuerKit('drachma'); +import { + makeParamManagerBuilder, + ParamTypes, + buildParamGovernanceExoMakers, +} from '../../src/index.js'; - const drachmaBrand = drachmaKit.brand; - const paramManager = makeParamManagerBuilder(makeStoredPublisherKit()) +const drachmaKit = makeIssuerKit('drachma'); +const drachmaBrand = drachmaKit.brand; + +export const makeZoeKit = issuerKit => { + const terms = harden({ + mmr: makeRatio(150n, issuerKit.brand), + }); + const issuerKeywordRecord = harden({ + Ignore: issuerKit.issuer, + }); + return setupZCFTest(issuerKeywordRecord, terms); +}; + +export const makeBuilder = zcf => { + const { makeRecorderKit, storageNode } = prepareMockRecorderKitMakers(); + const recorderKit = makeRecorderKit(storageNode); + const baggage = makeScalarBigMapStore('baggage'); + const paramMakerKit = buildParamGovernanceExoMakers( + zcf.getZoeService(), + baggage, + ); + return makeParamManagerBuilder(baggage, recorderKit, paramMakerKit, zcf); +}; + +test('two parameters', async t => { + const { zcf } = await makeZoeKit(drachmaKit); + const paramManager = await makeBuilder(zcf) .addBrand('Collateral', drachmaBrand) .addAmount('Amt', AmountMath.make(drachmaBrand, 37n)) .build(); - t.is(paramManager.getBrand('Collateral'), drachmaBrand); - t.is(paramManager.getCollateral(), drachmaBrand); - t.deepEqual( - paramManager.getAmount('Amt'), - AmountMath.make(drachmaBrand, 37n), - ); + const { behavior: getters } = await paramManager.accessors(); + + t.is(getters.getCollateral(), drachmaBrand); + t.deepEqual(getters.getAmt(), AmountMath.make(drachmaBrand, 37n)); }); test('getParams', async t => { - const drachmaKit = makeIssuerKit('drachma'); - - const drachmaBrand = drachmaKit.brand; + const { zcf } = await makeZoeKit(drachmaKit); const drachmas = AmountMath.make(drachmaBrand, 37n); - const paramManager = makeParamManagerBuilder(makeStoredPublisherKit()) + + const paramManager = await makeBuilder(zcf) .addBrand('Collateral', drachmaBrand) .addAmount('Amt', drachmas) .build(); t.deepEqual( - await paramManager.getParams(), + await paramManager.getParamDescriptions(), harden({ Collateral: { type: ParamTypes.BRAND, @@ -57,34 +81,35 @@ test('getParams', async t => { test('params duplicate entry', async t => { const stuffKey = 'Stuff'; const { brand: stiltonBrand } = makeIssuerKit('stilton', AssetKind.SET); + const { zcf } = await makeZoeKit(drachmaKit); + t.throws( () => - makeParamManagerBuilder(makeStoredPublisherKit()) + makeBuilder(zcf) .addNat(stuffKey, 37n) .addUnknown(stuffKey, stiltonBrand) .build(), { - message: `"Parameter Name" already registered: "Stuff"`, + message: + 'key "Stuff" already registered in collection "Parameter Holders"', }, ); }); test('Amount', async t => { - const { brand: floorBrand } = makeIssuerKit('floor wax'); + const waxKit = makeIssuerKit('floor wax'); + const { brand: floorBrand } = waxKit; const { brand: dessertBrand } = makeIssuerKit('dessertTopping'); - const paramManager = makeParamManagerBuilder(makeStoredPublisherKit()) + const { zcf } = await makeZoeKit(waxKit); + const paramManager = await makeBuilder(zcf) .addAmount('Shimmer', AmountMath.make(floorBrand, 2n)) .build(); - t.deepEqual( - paramManager.getAmount('Shimmer'), - AmountMath.make(floorBrand, 2n), - ); + const { behavior: getters } = await paramManager.accessors(); + + t.deepEqual(getters.getShimmer(), AmountMath.make(floorBrand, 2n)); await paramManager.updateParams({ Shimmer: AmountMath.make(floorBrand, 5n) }); - t.deepEqual( - paramManager.getAmount('Shimmer'), - AmountMath.make(floorBrand, 5n), - ); + t.deepEqual(getters.getShimmer(), AmountMath.make(floorBrand, 5n)); await t.throwsAsync( () => @@ -93,20 +118,17 @@ test('Amount', async t => { }), { message: - 'The brand in the allegedAmount {"brand":"[Alleged: dessertTopping brand]","value":"[20n]"} in \'coerce\' didn\'t match the specified brand "[Alleged: floor wax brand]".', + /Shimmer must match \[object Object], was \[object Object]: brand: "\[Alleged: dessertTopping brand]" - Must be: "\[Alleged: floor wax brand]"/, }, ); await t.throwsAsync( () => paramManager.updateParams({ Shimmer: 'fear,loathing' }), { - message: 'Expected an Amount for "Shimmer", got: "fear,loathing"', + message: + /Shimmer must match \[object Object], was fear,loathing: "fear,loathing" - Must be a copyRecord to match a copyRecord pattern: {"brand":"\[Alleged: floor wax brand]","value":"\[match:or]"}/, }, ); - - t.throws(() => paramManager.getString('Shimmer'), { - message: '"Shimmer" is not "string"', - }); }); test('params one installation', async t => { @@ -118,15 +140,18 @@ test('params one installation', async t => { getBundle: () => ({ obfuscated: 42 }), }); - const paramManager = makeParamManagerBuilder(makeStoredPublisherKit()) + const { zcf } = await makeZoeKit(drachmaKit); + const paramManager = await makeBuilder(zcf) .addInstallation('PName', installationHandle) .build(); + const { behavior: getters } = await paramManager.accessors(); - t.deepEqual(paramManager.getInstallation('PName'), installationHandle); + t.deepEqual(getters.getPName(), installationHandle); await t.throwsAsync( () => paramManager.updateParams({ PName: 18.1 }), { - message: 'value for "PName" must be an Installation, was 18.1', + message: + /PName must match .*, was 18.1: 18.1 - Must be a remotable Installation, not number/, }, 'value should be an installation', ); @@ -136,14 +161,10 @@ test('params one installation', async t => { getBundle: () => ({ condensed: '() => {})' }), }); await paramManager.updateParams({ PName: handle2 }); - t.deepEqual(paramManager.getInstallation('PName'), handle2); - - t.throws(() => paramManager.getNat('PName'), { - message: '"PName" is not "nat"', - }); + t.deepEqual(getters.getPName(), handle2); t.deepEqual( - await paramManager.getParams(), + await paramManager.getParamDescriptions(), harden({ PName: { type: ParamTypes.INSTALLATION, @@ -159,24 +180,27 @@ test('params one instance', async t => { // isInstallation() (#3344), we'll need to make a mockZoe. const instanceHandle = makeHandle(handleType); - const paramManager = makeParamManagerBuilder(makeStoredPublisherKit()) + const { zcf } = await makeZoeKit(drachmaKit); + const paramManager = await makeBuilder(zcf) .addInstance('PName', instanceHandle) .build(); + const { behavior: getters } = await paramManager.accessors(); - t.deepEqual(paramManager.getInstance('PName'), instanceHandle); + t.deepEqual(getters.getPName(), instanceHandle); await t.throwsAsync( () => paramManager.updateParams({ PName: 18.1 }), { - message: 'value for "PName" must be an Instance, was 18.1', + message: + /PName must match \[object match:remotable], was 18.1: 18.1 - Must be a remotable InstanceHandle, not number/, }, 'value should be an instance', ); const handle2 = makeHandle(handleType); await paramManager.updateParams({ PName: handle2 }); - t.deepEqual(paramManager.getInstance('PName'), handle2); + t.deepEqual(getters.getPName(), handle2); t.deepEqual( - await paramManager.getParams(), + await paramManager.getParamDescriptions(), harden({ PName: { type: ParamTypes.INSTANCE, @@ -187,7 +211,6 @@ test('params one instance', async t => { }); test('Invitation', async t => { - const drachmaKit = makeIssuerKit('drachma'); const terms = harden({ mmr: makeRatio(150n, drachmaKit.brand), }); @@ -196,7 +219,7 @@ test('Invitation', async t => { Ignore: drachmaKit.issuer, }); - const { instance, zoe } = await setupZCFTest(issuerKeywordRecord, terms); + const { instance, zoe, zcf } = await setupZCFTest(issuerKeywordRecord, terms); const invitation = await E(E(zoe).getPublicFacet(instance)).makeInvitation(); @@ -204,24 +227,17 @@ test('Invitation', async t => { invitation, ); - const drachmaBrand = drachmaKit.brand; const drachmaAmount = AmountMath.make(drachmaBrand, 37n); - const paramManagerBuilder = makeParamManagerBuilder( - makeStoredPublisherKit(), - zoe, - ) + const paramManagerBuilder = await makeBuilder(zcf) .addBrand('Collateral', 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('Collateral'), drachmaBrand); - t.is(paramManager.getAmount('Amt'), drachmaAmount); - // XXX UNTIL https://github.com/Agoric/agoric-sdk/issues/4343 - await eventLoopIteration(); - const invitationActualAmount = - paramManager.getInvitationAmount('Invite').value; + .addAmount('Amt', drachmaAmount) + .addInvitation('Invite', [invitation, invitationAmount]); + const paramManager = await paramManagerBuilder.build(); + const { behavior: getters } = await paramManager.accessors(); + + t.is(getters.getCollateral(), drachmaBrand); + t.is(getters.getAmt(), drachmaAmount); + const invitationActualAmount = getters.getInvite().value; t.deepEqual(invitationActualAmount, invitationAmount.value); t.assert(keyEQ(invitationActualAmount, invitationAmount.value)); t.is(invitationActualAmount[0].description, 'simple'); @@ -229,7 +245,7 @@ test('Invitation', async t => { t.is(await paramManager.getInternalParamValue('Invite'), invitation); t.deepEqual( - await paramManager.getParams(), + await paramManager.getParamDescriptions(), harden({ Amt: { type: ParamTypes.AMOUNT, @@ -248,23 +264,27 @@ test('Invitation', async t => { }); test('two Nats', async t => { - const paramManager = makeParamManagerBuilder(makeStoredPublisherKit()) + const { zcf } = await makeZoeKit(drachmaKit); + const paramManager = await makeBuilder(zcf) .addNat('Acres', 50n) .addNat('SpeedLimit', 299_792_458n) .build(); + const { behavior: getters } = await paramManager.accessors(); - t.is(paramManager.getNat('Acres'), 50n); - t.is(paramManager.getNat('SpeedLimit'), 299_792_458n); + t.is(getters.getAcres(), 50n); + t.is(getters.getSpeedLimit(), 299_792_458n); await t.throwsAsync( () => paramManager.updateParams({ SpeedLimit: 300000000 }), { - message: '300000000 must be a bigint', + message: + /SpeedLimit must match \[object match:nat], was 300000000: number 300000000 - Must be a bigint/, }, ); await t.throwsAsync(() => paramManager.updateParams({ SpeedLimit: -37n }), { - message: '-37 is negative', + message: + /SpeedLimit must match \[object match:nat], was -37: "\[-37n]" - Must be non-negative/, }); }); @@ -272,21 +292,25 @@ test('Ratio', async t => { const unitlessBrand = makeIssuerKit('unitless').brand; const ratio = makeRatio(16180n, unitlessBrand, 10_000n); - const paramManager = makeParamManagerBuilder(makeStoredPublisherKit()) + const { zcf } = await makeZoeKit(drachmaKit); + const paramManager = await makeBuilder(zcf) .addRatio('GoldenRatio', ratio) .build(); - t.is(paramManager.getRatio('GoldenRatio'), ratio); + const { behavior: getters } = await paramManager.accessors(); + + t.is(getters.getGoldenRatio(), ratio); const morePrecise = makeRatio(1618033n, unitlessBrand, 1_000_000n); await paramManager.updateParams({ GoldenRatio: morePrecise }); - t.is(paramManager.getRatio('GoldenRatio'), morePrecise); + t.is(getters.getGoldenRatio(), morePrecise); const anotherBrand = makeIssuerKit('arbitrary').brand; await t.throwsAsync( () => paramManager.updateParams({ GoldenRatio: 300000000 }), { - message: '"ratio" 300000000 must be a pass-by-copy record, not "number"', + message: + /GoldenRatio must match .*, was 300000000: 300000000 - Must be a copyRecord to match a copyRecord pattern: {"numerator":{"brand":"\[Alleged: unitless brand]","value":"\[match:nat]"}/, }, ); @@ -297,7 +321,7 @@ test('Ratio', async t => { }), { message: - 'Numerator brand for "GoldenRatio" must be "[Alleged: unitless brand]"', + /GoldenRatio must match \[object Object], was \[object Object]: numerator: brand: "\[Alleged: arbitrary brand]"/, }, ); }); @@ -311,10 +335,13 @@ test('Record', async t => { C2: 'Flying', D: 'Blue Jay Way', }); - const paramManager = makeParamManagerBuilder(makeStoredPublisherKit()) + const { zcf } = await makeZoeKit(drachmaKit); + const paramManager = await makeBuilder(zcf) .addRecord('BestEP', epRecord) .build(); - t.is(paramManager.getRecord('BestEP'), epRecord); + const { behavior: getters } = await paramManager.accessors(); + + t.is(getters.getBestEP(), epRecord); const replacement = harden({ A1: "Wouldn't It Be Nice", @@ -323,7 +350,7 @@ test('Record', async t => { B2: "I Know There's an Answer", }); await paramManager.updateParams({ BestEP: replacement }); - t.is(paramManager.getRecord('BestEP'), replacement); + t.is(getters.getBestEP(), replacement); const brokenRecord = { A1: 'Long Tall Sally', @@ -331,13 +358,9 @@ test('Record', async t => { B1: 'Slow Down', B2: 'Matchbox', }; - await t.throwsAsync( - () => paramManager.updateParams({ BestEP: brokenRecord }), - { - message: - 'Cannot pass non-frozen objects like {"A1":"Long Tall Sally","A2":"I Call Your Name","B1":"Slow Down","B2":"Matchbox"}. Use harden()', - }, - ); + + await paramManager.updateParams({ BestEP: brokenRecord }); + t.is(getters.getBestEP(), brokenRecord); await t.throwsAsync( () => @@ -345,7 +368,7 @@ test('Record', async t => { duration: '2:37', }), { - message: 'setters[name] is not a function', + message: 'key "duration" not found in collection "Parameter Holders"', }, ); @@ -355,44 +378,47 @@ test('Record', async t => { BestEP: '2:37', }), { - message: '"2:37" must be an object', + message: + /BestEP must match \[object match:recordOf], was 2:37: string "2:37" - Must be a copyRecord/, }, ); }); test('Strings', async t => { - const paramManager = makeParamManagerBuilder(makeStoredPublisherKit()) + const { zcf } = await makeZoeKit(drachmaKit); + const paramManager = await makeBuilder(zcf) .addNat('Acres', 50n) .addString('OurWeapons', 'fear') .build(); - t.is(paramManager.getString('OurWeapons'), 'fear'); + const { behavior: getters } = await paramManager.accessors(); + t.is(getters.getOurWeapons(), 'fear'); await paramManager.updateParams({ OurWeapons: 'fear,surprise' }); - t.is(paramManager.getString('OurWeapons'), 'fear,surprise'); + t.is(getters.getOurWeapons(), 'fear,surprise'); await t.throwsAsync( () => paramManager.updateParams({ OurWeapons: 300000000, }), { - message: '300000000 must be a string', + message: + /OurWeapons must match \[object match:string], was 300000000: number 300000000 - Must be a string/, }, ); - - t.throws(() => paramManager.getNat('OurWeapons'), { - message: '"OurWeapons" is not "nat"', - }); }); test('Unknown', async t => { - const paramManager = makeParamManagerBuilder(makeStoredPublisherKit()) + const { zcf } = await makeZoeKit(drachmaKit); + const paramManager = await makeBuilder(zcf) .addString('Label', 'birthday') .addUnknown('Surprise', 'party') .build(); - t.is(paramManager.getUnknown('Surprise'), 'party'); + const { behavior: getters } = await paramManager.accessors(); + + t.is(getters.getSurprise(), 'party'); await paramManager.updateParams({ Surprise: 'gift' }); - t.is(paramManager.getUnknown('Surprise'), 'gift'); + t.is(getters.getSurprise(), 'gift'); await paramManager.updateParams({ Surprise: ['gift', 'party'] }); - t.deepEqual(paramManager.getUnknown('Surprise'), ['gift', 'party']); + t.deepEqual(getters.getSurprise(), ['gift', 'party']); }); diff --git a/packages/governance/test/unitTests/test-paramGovernance.js b/packages/governance/test/unitTests/test-paramGovernance.js index b096c7066f70..466d3cb76e80 100644 --- a/packages/governance/test/unitTests/test-paramGovernance.js +++ b/packages/governance/test/unitTests/test-paramGovernance.js @@ -2,16 +2,17 @@ import '@agoric/zoe/exported.js'; import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { makeMockChainStorageRoot } from '@agoric/internal/src/storage-test-utils.js'; -import { makeNotifierFromAsyncIterable } from '@agoric/notifier'; import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; +import { makeNotifierFromSubscriber } from '@agoric/notifier'; import { makeZoeForTest } from '@agoric/zoe/tools/setup-zoe.js'; import bundleSource from '@endo/bundle-source'; import { E } from '@endo/eventual-send'; import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; + import { resolve as importMetaResolve } from 'import-meta-resolve'; -import { CONTRACT_ELECTORATE, ParamTypes } from '../../src/index.js'; import { MALLEABLE_NUMBER } from '../swingsetTests/contractGovernor/governedContract.js'; +import { CONTRACT_ELECTORATE, ParamTypes } from '../../src/index.js'; import { remoteNullMarshaller } from '../swingsetTests/utils.js'; const voteCounterRoot = '../../src/binaryVoteCounter.js'; @@ -22,8 +23,7 @@ const committeeRoot = '../../src/committee.js'; const makeBundle = async sourceRoot => { const url = await importMetaResolve(sourceRoot, import.meta.url); const path = new URL(url).pathname; - const contractBundle = await bundleSource(path); - return contractBundle; + return bundleSource(path); }; // makeBundle is a slow step, so we do it once for all the tests. @@ -98,6 +98,8 @@ const setUpGovernedContract = async (zoe, electorateTerms, timer) => { governorTerms, { governed: { + storageNode: makeMockChainStorageRoot().makeChildNode('governed'), + marshaller: remoteNullMarshaller, initialPoserInvitation: poserInvitation, }, }, @@ -126,9 +128,8 @@ test('governParam no votes', async t => { /** @type {GovernedPublicFacet<{}>} */ const publicFacet = E(governorFacets.creatorFacet).getPublicFacet(); - const notifier = makeNotifierFromAsyncIterable( - await E(publicFacet).getSubscription(), - ); + const topic = await E.get(E(publicFacet).getPublicTopics()).governance; + const notifier = makeNotifierFromSubscriber(topic.subscriber); const update1 = await notifier.getUpdateSince(); t.like(update1, { value: { @@ -156,7 +157,7 @@ test('governParam no votes', async t => { // no update2 because the value didn't change - t.deepEqual(await E(publicFacet).getGovernedParams(), { + t.deepEqual(await E(publicFacet).getParamDescriptions(), { Electorate: { type: 'invitation', value: invitationAmount, @@ -194,7 +195,7 @@ test('multiple params bad change', async t => { ), { message: - /In "getAmountOf" method of \(Zoe Invitation issuer\): arg 0: .*"\[13n\]" - Must be a remotable/, + /Proposed value 13 for Electorate must match InvitationShape: "\[13n]" - Must be a remotable Invitation, not bigint/, }, ); }); @@ -211,16 +212,12 @@ test('change multiple params', async t => { /** @type {GovernedPublicFacet<{}>} */ const publicFacet = await E(governorFacets.creatorFacet).getPublicFacet(); - const notifier = makeNotifierFromAsyncIterable( - await E(publicFacet).getSubscription(), - ); + const topic = await E.get(E(publicFacet).getPublicTopics()).governance; + const notifier = makeNotifierFromSubscriber(topic.subscriber); await eventLoopIteration(); const update1 = await notifier.getUpdateSince(); - // This value isn't available synchronously - // XXX UNTIL https://github.com/Agoric/agoric-sdk/issues/4343 // constructing the fixture to deepEqual would complicate this with insufficient benefit t.is( - // @ts-expect-error reaching into {} values update1.value.current.Electorate.value.value[0].description, 'questionPoser', ); @@ -279,7 +276,7 @@ test('change multiple params', async t => { }, }); - const paramsAfter = await E(publicFacet).getGovernedParams(); + const paramsAfter = await E(publicFacet).getParamDescriptions(); t.deepEqual(paramsAfter.Electorate.value, invitationAmount); t.is(paramsAfter.MalleableNumber.value, 42n); }); @@ -296,9 +293,8 @@ test('change multiple params used invitation', async t => { /** @type {GovernedPublicFacet<{}>} */ const publicFacet = E(governorFacets.creatorFacet).getPublicFacet(); - const notifier = makeNotifierFromAsyncIterable( - await E(publicFacet).getSubscription(), - ); + const topic = await E.get(E(publicFacet).getPublicTopics()).governance; + const notifier = makeNotifierFromSubscriber(topic.subscriber); const update1 = await notifier.getUpdateSince(); t.like(update1, { value: { @@ -349,7 +345,7 @@ test('change multiple params used invitation', async t => { // no update2 because the value didn't change - const paramsAfter = await E(publicFacet).getGovernedParams(); + const paramsAfter = await E(publicFacet).getParamDescriptions(); t.deepEqual(paramsAfter.Electorate.value, invitationAmount); // original value t.is(paramsAfter.MalleableNumber.value, 602214090000000000000000n); @@ -367,9 +363,8 @@ test('change param continuing invitation', async t => { /** @type {GovernedPublicFacet<{}>} */ const publicFacet = E(governorFacets.creatorFacet).getPublicFacet(); - const notifier = makeNotifierFromAsyncIterable( - await E(publicFacet).getSubscription(), - ); + const topic = await E.get(E(publicFacet).getPublicTopics()).governance; + const notifier = makeNotifierFromSubscriber(topic.subscriber); const update1 = await notifier.getUpdateSince(); t.like(update1, { value: { @@ -408,7 +403,7 @@ test('change param continuing invitation', async t => { await E(timer).tick(); await eventLoopIteration(); - const paramsAfter = await E(publicFacet).getGovernedParams(); + const paramsAfter = await E(publicFacet).getParamDescriptions(); t.is(paramsAfter.MalleableNumber.value, 42n); const update2 = await notifier.getUpdateSince(update1.updateCount); diff --git a/packages/governance/test/unitTests/test-puppetContractGovernor.js b/packages/governance/test/unitTests/test-puppetContractGovernor.js index 64d1faea5ff8..645c5074148b 100644 --- a/packages/governance/test/unitTests/test-puppetContractGovernor.js +++ b/packages/governance/test/unitTests/test-puppetContractGovernor.js @@ -1,7 +1,7 @@ /* eslint @typescript-eslint/no-floating-promises: "warn" */ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import { makeNotifierFromAsyncIterable } from '@agoric/notifier'; +import { makeNotifierFromSubscriber } from '@agoric/notifier'; import { makeZoeForTest } from '@agoric/zoe/tools/setup-zoe.js'; import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; import bundleSource from '@endo/bundle-source'; @@ -54,7 +54,7 @@ test('multiple params bad change', async t => { () => E(governorFacets.creatorFacet).changeParams(paramChangesSpec), { message: - 'In "getInvitationDetails" method of (ZoeService): arg 0: "[13n]" - Must match one of ["[match:remotable]","[match:kind]"]', + /In "getAmountOf" method of \(Zoe Invitation issuer\): arg 0: "\[13n]" - Must be a remotable Payment, not bigint/, }, ); }); @@ -71,25 +71,21 @@ test('change a param', async t => { /** @type {GovernedPublicFacet<{}>} */ const publicFacet = await E(governorFacets.creatorFacet).getPublicFacet(); - const notifier = makeNotifierFromAsyncIterable( - await E(publicFacet).getSubscription(), - ); + const topic = await E.get(E(publicFacet).getPublicTopics()).governance; + const notifier = makeNotifierFromSubscriber(topic.subscriber); const update1 = await notifier.getUpdateSince(); - publicFacet.getGovernedParams(); - // This value isn't available synchronously and we don't have access here to the param manager to await its finish - // XXX UNTIL https://github.com/Agoric/agoric-sdk/issues/4343 - // t.is( - // // @ts-expect-error reaching into unknown values - // update1.value.current.Electorate.value.value[0].description, - // 'getRefund', - // ); - // t.like(update1, { - // value: { - // current: { - // MalleableNumber: { type: 'nat', value: 602214090000000000000000n }, - // }, - // }, - // }); + publicFacet.getParamDescriptions(); + t.is( + update1.value.current.Electorate.value.value[0].description, + 'getRefund', + ); + t.like(update1, { + value: { + current: { + MalleableNumber: { type: 'nat', value: 602214090000000000000000n }, + }, + }, + }); // This is the wrong kind of invitation, but governance can't tell const { fakeInvitationPayment, fakeInvitationAmount } = @@ -114,7 +110,7 @@ test('change a param', async t => { }, }); - const paramsAfter = await E(publicFacet).getGovernedParams(); + const paramsAfter = await E(publicFacet).getParamDescriptions(); t.deepEqual(paramsAfter.Electorate.value, fakeInvitationAmount); t.is(paramsAfter.MalleableNumber.value, 42n); }); diff --git a/packages/governance/test/unitTests/test-typedParamManager.js b/packages/governance/test/unitTests/test-typedParamManager.js index 505d826acfa8..694d7af5b720 100644 --- a/packages/governance/test/unitTests/test-typedParamManager.js +++ b/packages/governance/test/unitTests/test-typedParamManager.js @@ -1,44 +1,77 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import { makeIssuerKit, AmountMath } from '@agoric/ertp'; -import { makeStoredPublisherKit } from '@agoric/notifier'; +import { AmountMath, makeIssuerKit } 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 '@endo/eventual-send'; import { Far } from '@endo/marshal'; +import { makeScalarBigMapStore } from '@agoric/vat-data'; +import { prepareMockRecorderKitMakers } from '@agoric/zoe/tools/mockRecorderKit.js'; import { makeHandle } from '@agoric/zoe/src/makeHandle.js'; -import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; -import { ParamTypes } from '../../src/index.js'; import { + buildParamGovernanceExoMakers, makeParamManager, - makeParamManagerFromTerms, - makeParamManagerSync, -} from '../../src/contractGovernance/typedParamManager.js'; + makeParamManagerFromTermsAndMakers, + ParamTypes, +} from '../../src/index.js'; const drachmaKit = makeIssuerKit('drachma'); const drachmaBrand = drachmaKit.brand; +const baggage = makeScalarBigMapStore('baggage'); -test('types', async t => { +async function makeKits() { + const terms = harden({ + mmr: makeRatio(150n, drachmaKit.brand), + }); + const issuerKeywordRecord = harden({ + Ignore: drachmaKit.issuer, + }); + const { zcf } = await setupZCFTest(issuerKeywordRecord, terms); + + const { makeRecorderKit, storageNode } = prepareMockRecorderKitMakers(); + const recorderKit = makeRecorderKit(storageNode); + const paramMakerKit = buildParamGovernanceExoMakers( + zcf.getZoeService(), + baggage, + ); + return { recorderKit, paramMakerKit }; +} +const { recorderKit, paramMakerKit } = await makeKits(); + +test('types: bad invitation', async t => { t.throws(() => - makeParamManagerSync(makeStoredPublisherKit(), { - // @ts-expect-error invalid value for the declared type - BrokenBrand: [ParamTypes.BRAND, 'not a brand'], + makeParamManager( + recorderKit, + baggage, + { + // @ts-expect-error invalid value for the declared type + BrokenBrand: [ParamTypes.BRAND, 'not a brand'], - BrokenInvitation: [ // @ts-expect-error not supported in makeParamManagerSync - 'invitation', - undefined, - ], - }), + BrokenInvitation: ['invitation', undefined], + }, + paramMakerKit, + ), ); - const mgr = makeParamManagerSync(makeStoredPublisherKit(), { - Working: [ParamTypes.NAT, 0n], - }); - mgr.getWorking().valueOf(); +}); + +test('types: working', async t => { + const mgr = makeParamManager( + recorderKit, + baggage, + { + Working: [ParamTypes.NAT, 0n], + }, + paramMakerKit, + ); + const { behavior: getters } = await mgr.accessors(); + + getters.getWorking().valueOf(); await t.throwsAsync(() => mgr.updateParams({ Working: 'not a bigint' })); }); -test('makeParamManagerFromTerms', async t => { +// I don't see a way to make zcf with terms containing the Electorate before I have the invitationIssuer. +test.skip('makeParamManagerFromTermsAndMakers', async t => { const terms = harden({ governedParams: { Mmr: { type: 'nat', value: makeRatio(150n, drachmaKit.brand) }, @@ -49,41 +82,40 @@ test('makeParamManagerFromTerms', async t => { }); const { zcf } = await setupZCFTest(issuerKeywordRecord, terms); - const paramManager = await makeParamManagerFromTerms( - makeStoredPublisherKit(), + const invitation = zcf.makeInvitation(() => null, 'mock poser invitation'); + const paramManager = await makeParamManagerFromTermsAndMakers( + recorderKit, // @ts-expect-error missing governance terms zcf, - { Electorate: zcf.makeInvitation(() => null, 'mock poser invitation') }, + baggage, + { Electorate: [invitation] }, { Mmr: 'ratio', }, + paramMakerKit, ); - t.deepEqual(paramManager.getMmr(), terms.governedParams.Mmr.value); -}); - -test('readonly', async t => { - const drachmas = AmountMath.make(drachmaBrand, 37n); - const paramManager = makeParamManagerSync(makeStoredPublisherKit(), { - Collateral: [ParamTypes.BRAND, drachmaBrand], - Amt: [ParamTypes.AMOUNT, drachmas], - }); - const getters = paramManager.readonly(); - t.is(paramManager.getCollateral, getters.getCollateral); - t.is(paramManager.getAmt, getters.getAmt); + const { behavior: getters } = await paramManager.accessors(); + t.deepEqual(getters.getMmr(), terms.governedParams.Mmr.value); }); test('two parameters', async t => { const drachmas = AmountMath.make(drachmaBrand, 37n); - const paramManager = makeParamManagerSync(makeStoredPublisherKit(), { - Collateral: [ParamTypes.BRAND, drachmaBrand], - Amt: [ParamTypes.AMOUNT, drachmas], - }); + const paramManager = makeParamManager( + recorderKit, + baggage, + { + Collateral: [ParamTypes.BRAND, drachmaBrand], + Amt: [ParamTypes.AMOUNT, drachmas], + }, + paramMakerKit, + ); - t.is(paramManager.getCollateral(), drachmaBrand); - t.is(paramManager.readonly().getCollateral(), drachmaBrand); - t.deepEqual(paramManager.getAmt(), drachmas); + const { behavior: getters } = await paramManager.accessors(); + + t.is(getters.getCollateral(), drachmaBrand); + t.deepEqual(getters.getAmt(), drachmas); t.deepEqual( - await paramManager.getParams(), + await paramManager.getParamDescriptions(), harden({ Collateral: { type: ParamTypes.BRAND, @@ -100,18 +132,26 @@ test('two parameters', async t => { test('Amount', async t => { const { brand: floorBrand } = makeIssuerKit('floor wax'); const { brand: dessertBrand } = makeIssuerKit('dessertTopping'); - const paramManager = makeParamManagerSync(makeStoredPublisherKit(), { - Shimmer: [ParamTypes.AMOUNT, AmountMath.make(floorBrand, 2n)], - }); - t.deepEqual(paramManager.getShimmer(), AmountMath.make(floorBrand, 2n)); + const paramManager = makeParamManager( + recorderKit, + baggage, + { + Shimmer: [ParamTypes.AMOUNT, AmountMath.make(floorBrand, 2n)], + }, + paramMakerKit, + ); + + const { behavior: params } = await paramManager.accessors(); + t.deepEqual(params.getShimmer(), AmountMath.make(floorBrand, 2n)); await paramManager.updateParams({ Shimmer: AmountMath.make(floorBrand, 5n) }); - t.deepEqual(paramManager.getShimmer(), AmountMath.make(floorBrand, 5n)); + t.deepEqual(params.getShimmer(), AmountMath.make(floorBrand, 5n)); await t.throwsAsync( () => paramManager.updateParams({ Shimmer: 'fear,loathing' }), { - message: 'Expected an Amount for "Shimmer", got: "fear,loathing"', + message: + /Shimmer must match .*, was fear,loathing: "fear,loathing" - Must be a copyRecord to match a copyRecord pattern:/, }, ); @@ -122,14 +162,15 @@ test('Amount', async t => { }), { message: - 'The brand in the allegedAmount {"brand":"[Alleged: dessertTopping brand]","value":"[20n]"} in \'coerce\' didn\'t match the specified brand "[Alleged: floor wax brand]".', + /Shimmer must match .*: brand: "\[Alleged: dessertTopping brand]" - Must be: "\[Alleged: floor wax brand]"/, }, ); await t.throwsAsync( () => paramManager.updateParams({ Shimmer: 'fear,loathing' }), { - message: 'Expected an Amount for "Shimmer", got: "fear,loathing"', + message: + /Shimmer must match .*: "fear,loathing" - Must be a copyRecord to match a copyRecord pattern:/, }, ); }); @@ -150,18 +191,22 @@ test('params one installation', async t => { }); const paramManager = await makeParamManager( - makeStoredPublisherKit(), + recorderKit, + baggage, { PName: ['installation', installationHandle], }, + paramMakerKit, zcf, ); - t.deepEqual(paramManager.getPName(), installationHandle); + const { behavior: getters } = await paramManager.accessors(); + t.deepEqual(getters.getPName(), installationHandle); await t.throwsAsync( () => paramManager.updateParams({ PName: 18.1 }), { - message: 'value for "PName" must be an Installation, was 18.1', + message: + /PName must match \[object match:remotable], was 18.1: 18.1 - Must be a remotable Installation, not number/, }, 'value should be an installation', ); @@ -171,10 +216,10 @@ test('params one installation', async t => { getBundle: () => ({ condensed: '() => {})' }), }); await paramManager.updateParams({ PName: handle2 }); - t.deepEqual(paramManager.getPName(), handle2); + t.deepEqual(getters.getPName(), handle2); t.deepEqual( - await paramManager.getParams(), + await paramManager.getParamDescriptions(), harden({ PName: { type: ParamTypes.INSTALLATION, @@ -190,24 +235,31 @@ test('params one instance', async t => { // isInstallation() (#3344), we'll need to make a mockZoe. const instanceHandle = makeHandle(instanceKey); - const paramManager = makeParamManagerSync(makeStoredPublisherKit(), { - PName: ['instance', instanceHandle], - }); + const paramManager = makeParamManager( + recorderKit, + baggage, + { + PName: ['instance', instanceHandle], + }, + paramMakerKit, + ); + const { behavior: getters } = await paramManager.accessors(); - t.deepEqual(paramManager.getPName(), instanceHandle); + t.deepEqual(getters.getPName(), instanceHandle); await t.throwsAsync( () => paramManager.updateParams({ PName: 18.1 }), { - message: 'value for "PName" must be an Instance, was 18.1', + message: + /ame must match .*, was 18.1: 18.1 - Must be a remotable InstanceHandle, not number/, }, 'value should be an instance', ); const handle2 = makeHandle(instanceKey); await paramManager.updateParams({ PName: handle2 }); - t.deepEqual(paramManager.getPName(), handle2); + t.deepEqual(getters.getPName(), handle2); t.deepEqual( - await paramManager.getParams(), + await paramManager.getParamDescriptions(), harden({ PName: { type: ParamTypes.INSTANCE, @@ -236,27 +288,28 @@ test('Invitation', async t => { const drachmaAmount = AmountMath.make(drachmaBrand, 37n); const paramManager = await makeParamManager( - makeStoredPublisherKit(), + recorderKit, + baggage, { Collateral: [ParamTypes.BRAND, drachmaBrand], Amt: [ParamTypes.AMOUNT, drachmaAmount], - Invite: ['invitation', invitation], + Invite: [ParamTypes.INVITATION, [invitation, invitationAmount]], }, + paramMakerKit, zcf, ); + const { behavior: getters } = await paramManager.accessors(); - t.is(paramManager.getCollateral(), drachmaBrand); - t.is(paramManager.getAmt(), drachmaAmount); - // XXX UNTIL https://github.com/Agoric/agoric-sdk/issues/4343 - await eventLoopIteration(); - const invitationActualAmount = paramManager.getInvite().value; + t.is(getters.getCollateral(), drachmaBrand); + t.is(getters.getAmt(), drachmaAmount); + const invitationActualAmount = getters.getInvite().value; t.deepEqual(invitationActualAmount, invitationAmount.value); t.is(invitationActualAmount[0].description, 'simple'); t.is(await paramManager.getInternalParamValue('Invite'), invitation); t.deepEqual( - await paramManager.getParams(), + await paramManager.getParamDescriptions(), harden({ Amt: { type: ParamTypes.AMOUNT, @@ -275,23 +328,31 @@ test('Invitation', async t => { }); test('two Nats', async t => { - const paramManager = makeParamManagerSync(makeStoredPublisherKit(), { - Acres: [ParamTypes.NAT, 50n], - SpeedLimit: [ParamTypes.NAT, 299_792_458n], - }); + const paramManager = makeParamManager( + recorderKit, + baggage, + { + Acres: [ParamTypes.NAT, 50n], + SpeedLimit: [ParamTypes.NAT, 299_792_458n], + }, + paramMakerKit, + ); - t.is(paramManager.getAcres(), 50n); - t.is(paramManager.getSpeedLimit(), 299_792_458n); + const { behavior: getters } = await paramManager.accessors(); + t.is(getters.getAcres(), 50n); + t.is(getters.getSpeedLimit(), 299_792_458n); await t.throwsAsync( () => paramManager.updateParams({ SpeedLimit: 300000000 }), { - message: '300000000 must be a bigint', + message: + /SpeedLimit must match \[object match:nat], was 300000000: number 300000000 - Must be a bigint/, }, ); await t.throwsAsync(() => paramManager.updateParams({ SpeedLimit: -37n }), { - message: '-37 is negative', + message: + /SpeedLimit must match \[object match:nat], was -37: "\[-37n]" - Must be non-negative/, }); }); @@ -299,20 +360,27 @@ test('Ratio', async t => { const unitlessBrand = makeIssuerKit('unitless').brand; const ratio = makeRatio(16180n, unitlessBrand, 10_000n); - const paramManager = makeParamManagerSync(makeStoredPublisherKit(), { - Acres: [ParamTypes.NAT, 50n], - GoldenRatio: ['ratio', ratio], - }); - t.is(paramManager.getGoldenRatio(), ratio); + const paramManager = makeParamManager( + recorderKit, + baggage, + { + Acres: [ParamTypes.NAT, 50n], + GoldenRatio: ['ratio', ratio], + }, + paramMakerKit, + ); + const { behavior: getters } = await paramManager.accessors(); + t.is(getters.getGoldenRatio(), ratio); const morePrecise = makeRatio(1618033n, unitlessBrand, 1_000_000n); await paramManager.updateParams({ GoldenRatio: morePrecise }); - t.is(paramManager.getGoldenRatio(), morePrecise); + t.is(getters.getGoldenRatio(), morePrecise); await t.throwsAsync( () => paramManager.updateParams({ GoldenRatio: 300000000 }), { - message: '"ratio" 300000000 must be a pass-by-copy record, not "number"', + message: + /GoldenRatio must match \[object Object], was 300000000: 300000000 - Must be a copyRecord to match a copyRecord pattern: {"numerator":{"brand":"\[Alleged: unitless brand]","value":"\[match:nat]"},"denominator":{"brand":"\[Seen]","value":"\[Seen]"}}/, }, ); @@ -325,37 +393,50 @@ test('Ratio', async t => { }), { message: - 'Numerator brand for "GoldenRatio" must be "[Alleged: unitless brand]"', + /GoldenRatio must match \[object Object], was \[object Object]: numerator: brand: "\[Alleged: arbitrary brand]" - Must be: "\[Alleged: unitless brand]"/, }, ); }); test('Strings', async t => { - const paramManager = makeParamManagerSync(makeStoredPublisherKit(), { - Acres: [ParamTypes.NAT, 50n], - OurWeapons: ['string', 'fear'], - }); - t.is(paramManager.getOurWeapons(), 'fear'); + const paramManager = makeParamManager( + recorderKit, + baggage, + { + Acres: [ParamTypes.NAT, 50n], + OurWeapons: ['string', 'fear'], + }, + paramMakerKit, + ); + const { behavior: getters } = await paramManager.accessors(); + t.is(getters.getOurWeapons(), 'fear'); await paramManager.updateParams({ OurWeapons: 'fear,surprise' }); - t.is(paramManager.getOurWeapons(), 'fear,surprise'); + t.is(getters.getOurWeapons(), 'fear,surprise'); await t.throwsAsync( () => paramManager.updateParams({ OurWeapons: 300000000 }), { - message: '300000000 must be a string', + message: + /OurWeapons must match \[object match:string], was 300000000: number 300000000 - Must be a string/, }, ); }); test('Unknown', async t => { - const paramManager = makeParamManagerSync(makeStoredPublisherKit(), { - Label: ['string', 'birthday'], - Surprise: ['unknown', 'party'], - }); - t.is(paramManager.getSurprise(), 'party'); + const paramManager = makeParamManager( + recorderKit, + baggage, + { + Label: ['string', 'birthday'], + Surprise: ['unknown', 'party'], + }, + paramMakerKit, + ); + const { behavior: getters } = await paramManager.accessors(); + t.is(getters.getSurprise(), 'party'); await paramManager.updateParams({ Surprise: 'gift' }); - t.is(paramManager.getSurprise(), 'gift'); + t.is(getters.getSurprise(), 'gift'); await paramManager.updateParams({ Surprise: ['gift', 'party'] }); - t.deepEqual(paramManager.getSurprise(), ['gift', 'party']); + t.deepEqual(getters.getSurprise(), ['gift', 'party']); }); diff --git a/packages/governance/tools/puppetContractGovernor.js b/packages/governance/tools/puppetContractGovernor.js index 65fdda0126f5..9ee8db89fd8a 100644 --- a/packages/governance/tools/puppetContractGovernor.js +++ b/packages/governance/tools/puppetContractGovernor.js @@ -21,7 +21,7 @@ import { makeApiInvocationPositions } from '../src/contractGovernance/governApi. * governedContractInstallation: Installation, * governed: { * issuerKeywordRecord?: IssuerKeywordRecord, - * terms: {governedParams: {[CONTRACT_ELECTORATE]: import('../src/contractGovernance/typedParamManager.js').InvitationParam }}, + * terms: {governedParams: {[CONTRACT_ELECTORATE]: import('../src/contractGovernance/paramManager.js').InvitationParam }}, * } * }>} zcf * @param {{ diff --git a/packages/governance/tools/puppetGovernance.js b/packages/governance/tools/puppetGovernance.js index d425e3af2905..b3769943e7c6 100644 --- a/packages/governance/tools/puppetGovernance.js +++ b/packages/governance/tools/puppetGovernance.js @@ -1,7 +1,10 @@ import bundleSource from '@endo/bundle-source'; import { E } from '@endo/eventual-send'; import { resolve as importMetaResolve } from 'import-meta-resolve'; +import { makeMockChainStorageRoot } from '@agoric/internal/src/storage-test-utils.js'; + import { CONTRACT_ELECTORATE, ParamTypes } from '../src/index.js'; +import { remoteNullMarshaller } from '../test/swingsetTests/utils.js'; const makeBundle = async sourceRoot => { const url = await importMetaResolve(sourceRoot, import.meta.url); @@ -17,8 +20,6 @@ const autoRefundBundleP = makeBundle( '@agoric/zoe/src/contracts/automaticRefund.js', ); -/** */ - /** * @template {GovernableStartFn} T governed contract startfn * @param {ERef} zoe @@ -56,7 +57,7 @@ export const setUpGovernedContract = async ( /** * Contract governor wants a committee invitation. Give it a random invitation. */ - async function getFakeInvitation() { + const getFakeInvitation = async () => { const autoRefundFacets = await E(zoe).startInstance(autoRefund); const invitationP = E(autoRefundFacets.publicFacet).makeInvitation(); const [fakeInvitationPayment, fakeInvitationAmount] = await Promise.all([ @@ -64,7 +65,7 @@ export const setUpGovernedContract = async ( E(E(zoe).getInvitationIssuer()).getAmountOf(invitationP), ]); return { fakeInvitationPayment, fakeInvitationAmount }; - } + }; const { fakeInvitationAmount, fakeInvitationPayment } = await getFakeInvitation(); @@ -97,6 +98,8 @@ export const setUpGovernedContract = async ( governed: { ...privateArgsOfGoverned, initialPoserInvitation: fakeInvitationPayment, + storageNode: makeMockChainStorageRoot().makeChildNode('governed'), + marshaller: remoteNullMarshaller, }, }, ); diff --git a/packages/inter-protocol/src/auction/auctioneer.js b/packages/inter-protocol/src/auction/auctioneer.js index 7a705398dc33..d644368ee9d6 100644 --- a/packages/inter-protocol/src/auction/auctioneer.js +++ b/packages/inter-protocol/src/auction/auctioneer.js @@ -13,7 +13,6 @@ import { atomicRearrange, ceilDivideBy, ceilMultiplyBy, - defineERecorderKit, defineRecorderKit, floorDivideBy, floorMultiplyBy, @@ -431,17 +430,14 @@ export const start = async (zcf, privateArgs, baggage) => { const makeAuctionBook = prepareAuctionBook(baggage, zcf, makeRecorderKit); - const makeERecorderKit = defineERecorderKit({ - makeRecorder, - makeDurablePublishKit, - }); - const scheduleKit = makeERecorderKit( - E(privateArgs.storageNode).makeChildNode('schedule'), + const scheduleKit = makeRecorderKit( + privateArgs.storageNode, /** * @type {import('@agoric/zoe/src/contractSupport/recorder.js').TypedMatcher< * import('./scheduler.js').ScheduleNotification * >} */ (M.any()), + 'schedule', ); /** @@ -525,13 +521,15 @@ export const start = async (zcf, privateArgs, baggage) => { } }; - const { augmentPublicFacet, makeFarGovernorFacet, params } = + const govNode = await E(privateArgs.storageNode).makeChildNode('governance'); + const { augmentPublicFacet, makeGovernorFacet, params } = await handleParamGovernance( zcf, + baggage, privateArgs.initialPoserInvitation, auctioneerParamTypes, - privateArgs.storageNode, - privateArgs.marshaller, + makeRecorderKit, + govNode, ); const tradeEveryBook = () => { @@ -675,19 +673,18 @@ export const start = async (zcf, privateArgs, baggage) => { }), ); - const scheduler = await E.when(scheduleKit.recorderP, scheduleRecorder => + const scheduler = await E.when(scheduleKit.recorder, scheduleRecorder => makeScheduler( driver, timer, - // @ts-expect-error types are correct. How to convince TS? params, timerBrand, scheduleRecorder, - publicFacet.getSubscription(), + E.get(E.get(publicFacet.getPublicTopics()).governance).subscriber, ), ); - const creatorFacet = makeFarGovernorFacet( + const creatorFacet = makeGovernorFacet( Far('Auctioneer creatorFacet', { /** * @param {Issuer} issuer diff --git a/packages/inter-protocol/src/auction/params.js b/packages/inter-protocol/src/auction/params.js index 3e6ab5e581c8..2eeba307a98e 100644 --- a/packages/inter-protocol/src/auction/params.js +++ b/packages/inter-protocol/src/auction/params.js @@ -2,12 +2,13 @@ import { CONTRACT_ELECTORATE, makeParamManager, ParamTypes, + buildParamGovernanceExoMakers, } from '@agoric/governance'; import { TimeMath, RelativeTimeRecordShape } from '@agoric/time'; import { M } from '@agoric/store'; -/** @typedef {import('@agoric/governance/src/contractGovernance/typedParamManager.js').AsyncSpecTuple} AsyncSpecTuple */ -/** @typedef {import('@agoric/governance/src/contractGovernance/typedParamManager.js').SyncSpecTuple} SyncSpecTuple */ +/** @typedef {import('@agoric/governance/src/contractGovernance/paramManager.js').AsyncSpecTuple} AsyncSpecTuple */ +/** @typedef {import('@agoric/governance/src/contractGovernance/paramManager.js').SyncSpecTuple} SyncSpecTuple */ // TODO duplicated with zoe/src/TypeGuards.js export const InvitationShape = M.remotable('Invitation'); @@ -114,13 +115,25 @@ export const makeAuctioneerParams = ({ harden(makeAuctioneerParams); /** - * @param {import('@agoric/notifier').StoredPublisherKit} publisherKit + * @param {import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit} recorderKit * @param {ZCF} zcf + * @param {import('@agoric/vat-data').Baggage} baggage * @param {AuctionParams} initial */ -export const makeAuctioneerParamManager = (publisherKit, zcf, initial) => { +export const makeAuctioneerParamManager = ( + recorderKit, + zcf, + baggage, + initial, +) => { + const paramMakerKit = buildParamGovernanceExoMakers( + zcf.getZoeService(), + baggage, + ); + return makeParamManager( - publisherKit, + recorderKit, + baggage, { [CONTRACT_ELECTORATE]: [ ParamTypes.INVITATION, @@ -140,6 +153,7 @@ export const makeAuctioneerParamManager = (publisherKit, zcf, initial) => { initial[PRICE_LOCK_PERIOD], ], }, + paramMakerKit, zcf, ); }; diff --git a/packages/inter-protocol/src/auction/scheduleMath.js b/packages/inter-protocol/src/auction/scheduleMath.js index f6e53602c473..1cbe0e143537 100644 --- a/packages/inter-protocol/src/auction/scheduleMath.js +++ b/packages/inter-protocol/src/auction/scheduleMath.js @@ -39,12 +39,19 @@ const subtract1 = relTime => * @returns {import('./scheduler.js').Schedule} */ export const computeRoundTiming = (params, baseTime) => { + // @ts-expect-error Params types not inferred? const freq = params.getStartFrequency(); + // @ts-expect-error Params types not inferred? const clockStep = params.getClockStep(); + // @ts-expect-error Params types not inferred? const startingRate = params.getStartingRate(); + // @ts-expect-error Params types not inferred? const discountStep = params.getDiscountStep(); + // @ts-expect-error Params types not inferred? const lockPeriod = params.getPriceLockPeriod(); + // @ts-expect-error Params types not inferred? const lowestRate = params.getLowestRate(); + // @ts-expect-error Params types not inferred? const startDelay = params.getAuctionStartDelay(); TimeMath.compareRel(freq, startDelay) > 0 || diff --git a/packages/inter-protocol/src/auction/scheduler.js b/packages/inter-protocol/src/auction/scheduler.js index c1d05b18b5f4..16b118c9aded 100644 --- a/packages/inter-protocol/src/auction/scheduler.js +++ b/packages/inter-protocol/src/auction/scheduler.js @@ -75,7 +75,7 @@ const nominalStartTime = nextSchedule => * @param {Awaited} params * @param {import('@agoric/time/src/types').TimerBrand} timerBrand * @param {import('@agoric/zoe/src/contractSupport/recorder.js').Recorder} scheduleRecorder - * @param {StoredSubscription} paramUpdateSubscription + * @param {Subscriber} paramUpdateSubscriber */ export const makeScheduler = async ( auctionDriver, @@ -83,7 +83,7 @@ export const makeScheduler = async ( params, timerBrand, scheduleRecorder, - paramUpdateSubscription, + paramUpdateSubscriber, ) => { /** * live version is defined when an auction is active. @@ -317,7 +317,7 @@ export const makeScheduler = async ( // already scheduled. // NB: what is already scheduled (live or next) is unaffected by param changes void observeIteration( - subscribeEach(paramUpdateSubscription), + subscribeEach(paramUpdateSubscriber), harden({ // NB: may be fired with the initial params as well async updateState(_newState) { diff --git a/packages/inter-protocol/src/price/fluxAggregatorContract.js b/packages/inter-protocol/src/price/fluxAggregatorContract.js index 2ac33c6482df..dee9a351396b 100644 --- a/packages/inter-protocol/src/price/fluxAggregatorContract.js +++ b/packages/inter-protocol/src/price/fluxAggregatorContract.js @@ -10,7 +10,7 @@ import { makeTracer, StorageNodeShape } from '@agoric/internal'; import { prepareDurablePublishKit } from '@agoric/notifier'; import { M } from '@agoric/store'; import { provideAll } from '@agoric/zoe/src/contractSupport/durability.js'; -import { prepareRecorder } from '@agoric/zoe/src/contractSupport/recorder.js'; +import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js'; import { E } from '@endo/eventual-send'; import { reserveThenDeposit } from '../proposals/utils.js'; import { prepareFluxAggregatorKit } from './fluxAggregatorKit.js'; @@ -64,6 +64,7 @@ harden(meta); * marshaller: ERef; * namesByAddressAdmin: ERef; * storageNode: StorageNode; + * recorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit; * }} privateArgs * @param {Baggage} baggage */ @@ -92,7 +93,10 @@ export const start = async (zcf, privateArgs, baggage) => { baggage, 'Price Aggregator publish kit', ); - const makeRecorder = prepareRecorder(baggage, marshaller); + const { makeRecorder, makeRecorderKit } = prepareRecorderKitMakers( + baggage, + marshaller, + ); const makeFluxAggregatorKit = await prepareFluxAggregatorKit( baggage, @@ -100,6 +104,7 @@ export const start = async (zcf, privateArgs, baggage) => { timer, quoteIssuerKit, storageNode, + makeRecorderKit(storageNode), makeDurablePublishKit, makeRecorder, ); @@ -109,20 +114,20 @@ export const start = async (zcf, privateArgs, baggage) => { }); trace('got faKit', faKit); - // cannot be stored in baggage because not durable - // UNTIL https://github.com/Agoric/agoric-sdk/issues/4343 - const { makeDurableGovernorFacet } = handleParamGovernance( - // @ts-expect-error FIXME include Governance params + const { makeGovernorFacet } = await handleParamGovernance( + // TODO(turadg): Type decl help needed here. + // @ts-expect-error parameterized type confusion? zcf, + baggage, initialPoserInvitation, { - // No governed parameters. Governance just for API methods. + /* Only governed parameter is electorate. */ }, + makeRecorderKit, storageNode, - marshaller, ); - trace('got makeDurableGovernorFacet', makeDurableGovernorFacet); + trace('got makeGovernorFacet', makeGovernorFacet); /** * Initialize a new oracle and send an invitation to control it. @@ -181,11 +186,7 @@ export const start = async (zcf, privateArgs, baggage) => { }, }; - const { governorFacet } = makeDurableGovernorFacet( - baggage, - faKit.creator, - governedApis, - ); + const governorFacet = makeGovernorFacet(faKit.creator, governedApis); trace('made governorFacet', governorFacet); return harden({ diff --git a/packages/inter-protocol/src/price/fluxAggregatorKit.js b/packages/inter-protocol/src/price/fluxAggregatorKit.js index 038816a59f92..6599a9dd7a60 100644 --- a/packages/inter-protocol/src/price/fluxAggregatorKit.js +++ b/packages/inter-protocol/src/price/fluxAggregatorKit.js @@ -12,6 +12,7 @@ import { makeOnewayPriceAuthorityKit, makeRecorderTopic, provideAll, + PublicTopicShape, } from '@agoric/zoe/src/contractSupport/index.js'; import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; @@ -85,6 +86,7 @@ const priceDescriptionFromQuote = quote => quote.quoteAmount.value[0]; * @param {TimerService} timerPresence * @param {import('./roundsManager.js').QuoteKit} quoteKit * @param {StorageNode} storageNode + * @param {import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit} recorderKit * @param {() => PublishKit} makeDurablePublishKit * @param {import('@agoric/zoe/src/contractSupport/recorder.js').MakeRecorder} makeRecorder */ @@ -94,6 +96,7 @@ export const prepareFluxAggregatorKit = async ( timerPresence, quoteKit, storageNode, + recorderKit, makeDurablePublishKit, makeRecorder, ) => { @@ -143,24 +146,18 @@ export const prepareFluxAggregatorKit = async ( */ answerKit: () => makeDurablePublishKit(), /** For publishing priceAuthority values to off-chain storage */ - priceKit: () => - makeRecorderKit( + priceKit: () => recorderKit, + latestRoundKit: () => { + return makeRecorderKit( storageNode, - /** @type {import('@agoric/zoe/src/contractSupport/recorder.js').TypedMatcher} */ ( - M.any() - ), - ), - latestRoundKit: () => - E.when(E(storageNode).makeChildNode('latestRound'), node => - makeRecorderKit( - node, - /** - * @type {import('@agoric/zoe/src/contractSupport/recorder.js').TypedMatcher< - * import('./roundsManager.js').LatestRound - * >} - */ (M.any()), - ), - ), + /** + * @type {import('@agoric/zoe/src/contractSupport/recorder.js').TypedMatcher< + * import('./roundsManager.js').LatestRound + * >} + */ (M.any()), + 'latestRound', + ); + }, }); const { roundsManagerKit } = await provideAll(baggage, { @@ -219,8 +216,8 @@ export const prepareFluxAggregatorKit = async ( public: M.interface('fluxAggregator publicFacet', { getPriceAuthority: M.call().returns(M.any()), getPublicTopics: M.call().returns({ - quotes: M.any(), - latestRound: M.any(), + quotes: PublicTopicShape, + latestRound: PublicTopicShape, }), }), }, diff --git a/packages/inter-protocol/src/provisionPool.js b/packages/inter-protocol/src/provisionPool.js index b192b2fcb543..363427fedfcb 100644 --- a/packages/inter-protocol/src/provisionPool.js +++ b/packages/inter-protocol/src/provisionPool.js @@ -1,11 +1,7 @@ // @jessie-check // @ts-check -import { - handleParamGovernance, - ParamTypes, - publicMixinAPI, -} from '@agoric/governance'; +import { handleParamGovernance, ParamTypes } from '@agoric/governance'; import { InvitationShape } from '@agoric/governance/src/typeGuards.js'; import { M } from '@agoric/store'; import { prepareExo } from '@agoric/vat-data'; @@ -59,15 +55,16 @@ export const start = async (zcf, privateArgs, baggage) => { ); // Governance - const { publicMixin, makeDurableGovernorFacet, params } = - await handleParamGovernance( + const { publicMixin, makeGovernorFacet, params, publicMixinGuards } = + handleParamGovernance( zcf, + baggage, privateArgs.initialPoserInvitation, { PerAccountInitialAmount: ParamTypes.AMOUNT, }, + makeRecorderKit, privateArgs.storageNode, - privateArgs.marshaller, ); const makeProvisionPoolKit = prepareProvisionPoolKit(baggage, { @@ -95,23 +92,25 @@ export const start = async (zcf, privateArgs, baggage) => { 'Provisioning Pool public', M.interface('ProvisionPool', { getMetrics: M.call().returns(M.remotable('MetricsSubscriber')), + ...publicMixinGuards, getPublicTopics: M.call().returns(TopicsRecordShape), - ...publicMixinAPI, }), { getMetrics() { return provisionPoolKit.public.getPublicTopics().metrics.subscriber; }, + ...publicMixin, getPublicTopics() { - return provisionPoolKit.public.getPublicTopics(); + return harden({ + ...provisionPoolKit.public.getPublicTopics(), + ...publicMixin.getPublicTopics(), + }); }, - ...publicMixin, }, ); return harden({ - creatorFacet: makeDurableGovernorFacet(baggage, provisionPoolKit.machine) - .governorFacet, + creatorFacet: makeGovernorFacet(provisionPoolKit.machine), publicFacet, }); }; diff --git a/packages/inter-protocol/src/provisionPoolKit.js b/packages/inter-protocol/src/provisionPoolKit.js index b2a416ccda02..21241bd32a9b 100644 --- a/packages/inter-protocol/src/provisionPoolKit.js +++ b/packages/inter-protocol/src/provisionPoolKit.js @@ -266,7 +266,7 @@ export const prepareProvisionPoolKit = ( totalMintedConverted, totalMintedProvided, } = this.state; - void metricsRecorderKit.recorder.write( + void E(metricsRecorderKit.recorder).write( harden({ walletsProvisioned, totalMintedProvided, @@ -341,7 +341,12 @@ export const prepareProvisionPoolKit = ( { updateState: async desc => { console.log('provisionPool notified of new asset', desc.brand); - await zcf.saveIssuer(desc.issuer, desc.issuerName); + await null; + + // After upgrade, we can be re-notified. + if (!zcf.getTerms().issuers[desc.issuerName]) { + await zcf.saveIssuer(desc.issuer, desc.issuerName); + } /** @type {ERef} */ // @ts-expect-error vbank purse is close enough for our use. const exchangePurse = E(poolBank).getPurse(desc.brand); diff --git a/packages/inter-protocol/src/psm/psm.js b/packages/inter-protocol/src/psm/psm.js index be2bf6afb0ab..02968bae9f20 100644 --- a/packages/inter-protocol/src/psm/psm.js +++ b/packages/inter-protocol/src/psm/psm.js @@ -9,7 +9,6 @@ import { CONTRACT_ELECTORATE, handleParamGovernance, ParamTypes, - publicMixinAPI, } from '@agoric/governance'; import { StorageNodeShape } from '@agoric/internal'; import { M, prepareExo, provide } from '@agoric/vat-data'; @@ -23,7 +22,6 @@ import { prepareRecorderKitMakers, provideAll, provideEmptySeat, - TopicsRecordShape, } from '@agoric/zoe/src/contractSupport/index.js'; import { AmountKeywordRecordShape, @@ -143,17 +141,23 @@ export const start = async (zcf, privateArgs, baggage) => { const emptyStable = AmountMath.makeEmpty(stableBrand); const emptyAnchor = AmountMath.makeEmpty(anchorBrand); - const { publicMixin, makeDurableGovernorFacet, params } = + const { governanceStorageNode } = await provideAll(baggage, { + governanceStorageNode: () => + E(privateArgs.storageNode).makeChildNode('governance'), + }); + + const { publicMixin, publicMixinGuards, makeGovernorFacet, params } = await handleParamGovernance( zcf, + baggage, privateArgs.initialPoserInvitation, { GiveMintedFee: ParamTypes.RATIO, MintLimit: ParamTypes.AMOUNT, WantMintedFee: ParamTypes.RATIO, }, - privateArgs.storageNode, - privateArgs.marshaller, + makeRecorderKit, + governanceStorageNode, ); const anchorPool = provideEmptySeat(zcf, baggage, 'anchorPoolSeat'); @@ -174,13 +178,12 @@ export const start = async (zcf, privateArgs, baggage) => { const { metricsKit } = await provideAll(baggage, { metricsKit: () => - E.when(E(privateArgs.storageNode).makeChildNode('metrics'), node => - makeRecorderKit( - node, - /** @type {import('@agoric/zoe/src/contractSupport/recorder.js').TypedMatcher} */ ( - M.any() - ), + makeRecorderKit( + privateArgs.storageNode, + /** @type {import('@agoric/zoe/src/contractSupport/recorder.js').TypedMatcher} */ ( + M.any() ), + 'metrics', ), }); const topics = harden({ @@ -361,10 +364,10 @@ export const start = async (zcf, privateArgs, baggage) => { M.interface('PSM', { getMetrics: M.call().returns(M.remotable('MetricsSubscriber')), getPoolBalance: M.call().returns(anchorAmountShape), - getPublicTopics: M.call().returns(TopicsRecordShape), makeWantMintedInvitation: M.call().returns(M.promise()), makeGiveMintedInvitation: M.call().returns(M.promise()), - ...publicMixinAPI, + ...publicMixinGuards, + getPublicTopics: M.call().returns(M.promise()), }), { getMetrics() { @@ -373,9 +376,6 @@ export const start = async (zcf, privateArgs, baggage) => { getPoolBalance() { return anchorPool.getAmountAllocated('Anchor', anchorBrand); }, - getPublicTopics() { - return topics; - }, makeWantMintedInvitation() { return zcf.makeInvitation( wantMintedHook, @@ -399,6 +399,14 @@ export const start = async (zcf, privateArgs, baggage) => { ); }, ...publicMixin, + getPublicTopics() { + return E.when(publicMixin.getPublicTopics(), publicTopics => + harden({ + ...topics, + ...publicTopics, + }), + ); + }, }, ); @@ -436,10 +444,7 @@ export const start = async (zcf, privateArgs, baggage) => { }, ); - const { governorFacet } = makeDurableGovernorFacet( - baggage, - limitedCreatorFacet, - ); + const governorFacet = makeGovernorFacet(limitedCreatorFacet); return harden({ creatorFacet: governorFacet, publicFacet, diff --git a/packages/inter-protocol/src/reserve/assetReserve.js b/packages/inter-protocol/src/reserve/assetReserve.js index 9791502eb39e..189c9ff4332f 100644 --- a/packages/inter-protocol/src/reserve/assetReserve.js +++ b/packages/inter-protocol/src/reserve/assetReserve.js @@ -2,6 +2,7 @@ import { handleParamGovernance } from '@agoric/governance'; import { makeTracer } from '@agoric/internal'; +import { E } from '@endo/eventual-send'; import { prepareRecorderKitMakers, provideAll, @@ -57,44 +58,35 @@ export const start = async (zcf, privateArgs, baggage) => { privateArgs.marshaller, ); - /** @type {() => Promise>} */ - const takeFeeMint = async () => { - if (baggage.has('feeMint')) { - return baggage.get('feeMint'); - } - - const feeMintTemp = await zcf.registerFeeMint( - 'Fee', - privateArgs.feeMintAccess, - ); - baggage.init('feeMint', feeMintTemp); - return feeMintTemp; - }; - trace('awaiting takeFeeMint'); - const feeMint = await takeFeeMint(); - const storageNode = await privateArgs.storageNode; - const makeAssetReserveKit = await prepareAssetReserveKit(baggage, { + const { feeMint, storageNode, governanceNode } = await provideAll(baggage, { + feeMint: () => zcf.registerFeeMint('Fee', privateArgs.feeMintAccess), + storageNode: () => privateArgs.storageNode, + governanceNode: () => + E(privateArgs.storageNode).makeChildNode('governance'), + }); + + const { makeGovernorFacet, publicMixin } = await handleParamGovernance( + zcf, + baggage, + privateArgs.initialPoserInvitation, + {}, + makeRecorderKit, + governanceNode, + ); + + const makeAssetReserveKit = prepareAssetReserveKit(baggage, { feeMint, makeRecorderKit, storageNode, zcf, + publicMixin, }); const { assetReserveKit } = await provideAll(baggage, { assetReserveKit: makeAssetReserveKit, }); - trace('awaiting handleParamGovernance'); - const { makeDurableGovernorFacet } = await handleParamGovernance( - zcf, - privateArgs.initialPoserInvitation, - {}, - privateArgs.storageNode, - privateArgs.marshaller, - ); - - const { governorFacet } = makeDurableGovernorFacet( - baggage, + const governorFacet = makeGovernorFacet( assetReserveKit.machine, // reconstruct facet so that the keys are enumerable and that the client can't depend on object identity { @@ -107,9 +99,7 @@ export const start = async (zcf, privateArgs, baggage) => { /** @type {GovernedCreatorFacet} */ creatorFacet: governorFacet, /** @type {GovernedPublicFacet} */ - // cast due to missing governance mixins - // XXX https://github.com/Agoric/agoric-sdk/issues/5200 - publicFacet: /** @type {any} */ (assetReserveKit.public), + publicFacet: assetReserveKit.public, }; }; harden(start); diff --git a/packages/inter-protocol/src/reserve/assetReserveKit.js b/packages/inter-protocol/src/reserve/assetReserveKit.js index 3610027a726f..d4652e9f519d 100644 --- a/packages/inter-protocol/src/reserve/assetReserveKit.js +++ b/packages/inter-protocol/src/reserve/assetReserveKit.js @@ -3,10 +3,7 @@ import { AmountMath, AmountShape, IssuerShape } from '@agoric/ertp'; import { makeTracer } from '@agoric/internal'; import { M, makeScalarBigMapStore, prepareExoClassKit } from '@agoric/vat-data'; import { atomicTransfer } from '@agoric/zoe/src/contractSupport/atomicTransfer.js'; -import { - makeRecorderTopic, - TopicsRecordShape, -} from '@agoric/zoe/src/contractSupport/topics.js'; +import { makeRecorderTopic } from '@agoric/zoe/src/contractSupport/topics.js'; import { AmountKeywordRecordShape } from '@agoric/zoe/src/typeGuards.js'; import { E } from '@endo/eventual-send'; import { UnguardedHelperI } from '@agoric/internal/src/typeGuards.js'; @@ -31,11 +28,12 @@ const trace = makeTracer('ReserveKit', true); * makeRecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').MakeRecorderKit; * storageNode: StorageNode; * zcf: ZCF; + * publicMixin: any; * }} powers */ -export const prepareAssetReserveKit = async ( +export const prepareAssetReserveKit = ( baggage, - { feeMint, makeRecorderKit, storageNode, zcf }, + { feeMint, makeRecorderKit, storageNode, zcf, publicMixin }, ) => { trace('prepareAssetReserveKit', [...baggage.keys()]); const feeKit = feeMint.getIssuerRecord(); @@ -56,7 +54,7 @@ export const prepareAssetReserveKit = async ( }), public: M.interface('AssetReserve public', { makeAddCollateralInvitation: M.call().returns(M.promise()), - getPublicTopics: M.call().returns(TopicsRecordShape), + getPublicTopics: M.call().returns(M.promise()), }), shortfallReportingFacet: M.interface('AssetReserve shortfall reporter', { increaseLiquidationShortfall: M.call(AmountShape).returns(), @@ -201,10 +199,6 @@ export const prepareAssetReserveKit = async ( ); }, }, - /** - * XXX missing governance public methods - * https://github.com/Agoric/agoric-sdk/issues/5200 - */ public: { /** Anyone can deposit any assets to the reserve */ makeAddCollateralInvitation() { @@ -233,13 +227,18 @@ export const prepareAssetReserveKit = async ( }; return zcf.makeInvitation(handler, 'Add Collateral'); }, + + ...publicMixin, getPublicTopics() { - return { - metrics: makeRecorderTopic( - 'Asset Reserve metrics', - this.state.metricsKit, - ), - }; + return E.when(publicMixin.getPublicTopics(), publicTopics => + harden({ + metrics: makeRecorderTopic( + 'Asset Reserve metrics', + this.state.metricsKit, + ), + ...publicTopics, + }), + ); }, }, shortfallReportingFacet: { diff --git a/packages/inter-protocol/src/vaultFactory/liquidation.js b/packages/inter-protocol/src/vaultFactory/liquidation.js index cf0241696dc4..8f61cdf02bbe 100644 --- a/packages/inter-protocol/src/vaultFactory/liquidation.js +++ b/packages/inter-protocol/src/vaultFactory/liquidation.js @@ -218,7 +218,9 @@ export const watchForGovernanceChange = ( void E.when(E(timer).getCurrentTimestamp(), now => // make one observer that will usually ignore the update. observeIteration( - subscribeEach(E(auctioneerPublicFacet).getSubscription()), + subscribeEach( + E.get(E(auctioneerPublicFacet).getPublicTopics()).subscriber, + ), harden({ async updateState(_newState) { if (!cancelToken) { diff --git a/packages/inter-protocol/src/vaultFactory/params.js b/packages/inter-protocol/src/vaultFactory/params.js index ce883dffd32b..3003b6263b8e 100644 --- a/packages/inter-protocol/src/vaultFactory/params.js +++ b/packages/inter-protocol/src/vaultFactory/params.js @@ -4,14 +4,15 @@ import './types.js'; import { CONTRACT_ELECTORATE, - makeParamManagerSync, + makeParamManager, ParamTypes, } from '@agoric/governance'; -import { makeStoredPublisherKit } from '@agoric/notifier'; -import { M, makeScalarMapStore } from '@agoric/store'; +import { subtractRatios } from '@agoric/zoe/src/contractSupport/index.js'; +import { M } from '@agoric/store'; import { TimeMath } from '@agoric/time'; -import { provideDurableMapStore } from '@agoric/vat-data'; -import { subtractRatios } from '@agoric/zoe/src/contractSupport/ratio.js'; +import { provide, provideDurableMapStore } from '@agoric/vat-data'; +import { makeTracer } from '@agoric/internal/src/index.js'; + import { amountPattern, ratioPattern } from '../contractSupport.js'; export const CHARGING_PERIOD_KEY = 'ChargingPeriod'; @@ -35,6 +36,8 @@ export const vaultDirectorParamTypes = { }; harden(vaultDirectorParamTypes); +const trace = makeTracer('VaultFactory Params'); + /** * @param {Amount<'set'>} electorateInvitationAmount * @param {Amount<'nat'>} minInitialDebt @@ -76,7 +79,7 @@ const makeVaultDirectorParams = ( harden(makeVaultDirectorParams); /** - * @typedef {import('@agoric/governance/src/contractGovernance/typedParamManager').ParamTypesMapFromRecord< + * @typedef {import('@agoric/governance/src/contractGovernance/paramManager').ParamTypesMapFromRecord< * ReturnType * >} VaultDirectorParams */ @@ -85,12 +88,17 @@ harden(makeVaultDirectorParams); const zeroRatio = liquidationMargin => subtractRatios(liquidationMargin, liquidationMargin); +/** @typedef {import('@agoric/governance/src/contractGovernance/paramManager.js').ParamGovernanceExoMakers} ParamGovernanceExoMakers */ + /** - * @param {import('@agoric/notifier').StoredPublisherKit} publisherKit + * @param {import('@agoric/vat-data').Baggage} baggage + * @param {import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit} recorderKit * @param {VaultManagerParamValues} initial + * @param {ParamGovernanceExoMakers} paramMakerKit */ export const makeVaultParamManager = ( - publisherKit, + baggage, + recorderKit, { debtLimit, interestRate, @@ -99,15 +107,22 @@ export const makeVaultParamManager = ( liquidationPenalty, mintFee, }, -) => - makeParamManagerSync(publisherKit, { - [DEBT_LIMIT_KEY]: [ParamTypes.AMOUNT, debtLimit], - [INTEREST_RATE_KEY]: [ParamTypes.RATIO, interestRate], - [LIQUIDATION_PADDING_KEY]: [ParamTypes.RATIO, liquidationPadding], - [LIQUIDATION_MARGIN_KEY]: [ParamTypes.RATIO, liquidationMargin], - [LIQUIDATION_PENALTY_KEY]: [ParamTypes.RATIO, liquidationPenalty], - [MINT_FEE_KEY]: [ParamTypes.RATIO, mintFee], - }); + paramMakerKit, +) => { + return makeParamManager( + recorderKit, + baggage, + { + [DEBT_LIMIT_KEY]: [ParamTypes.AMOUNT, debtLimit], + [INTEREST_RATE_KEY]: [ParamTypes.RATIO, interestRate], + [LIQUIDATION_PADDING_KEY]: [ParamTypes.RATIO, liquidationPadding], + [LIQUIDATION_MARGIN_KEY]: [ParamTypes.RATIO, liquidationMargin], + [LIQUIDATION_PENALTY_KEY]: [ParamTypes.RATIO, liquidationPenalty], + [MINT_FEE_KEY]: [ParamTypes.RATIO, mintFee], + }, + paramMakerKit, + ); +}; /** @typedef {ReturnType} VaultParamManager */ export const vaultParamPattern = M.splitRecord( @@ -166,24 +181,32 @@ export const makeGovernedTerms = ({ }); }; harden(makeGovernedTerms); + /** - * Stop-gap which restores initial param values UNTIL - * https://github.com/Agoric/agoric-sdk/issues/5200 - * - * NB: changes from initial values will be lost upon restart - * * @param {import('@agoric/vat-data').Baggage} baggage - * @param {ERef} marshaller + * @param {import('@agoric/zoe/src/contractSupport/recorder.js').MakeRecorderKit} makeRecorderKit */ -export const provideVaultParamManagers = (baggage, marshaller) => { +export const provideVaultParamManagers = (baggage, makeRecorderKit) => { /** @type {MapStore} */ - const managers = makeScalarMapStore(); + const managers = provideDurableMapStore(baggage, 'vaultsParamManagers'); + provide(baggage, 'paramManagerCount', () => 0); - // the managers aren't durable but their arguments are + // the param managers weren't originally durable, so we stored the initial + // values of the parameters. Now that we have durable PMs, we'll be extracting + // the initial values from this store, and can drop the store later. /** * @type {MapStore< * Brand, - * { storageNode: StorageNode; initialParamValues: VaultManagerParamValues } + * { + * storageNode: StorageNode; + * govStorageNode: StorageNode; + * initialParamValues: VaultManagerParamValues; + * makers: ParamGovernanceExoMakers; + * directorAccessors: { + * behavior: Record; + * guards: Record; + * }; + * } * >} */ const managerArgs = provideDurableMapStore( @@ -191,33 +214,76 @@ export const provideVaultParamManagers = (baggage, marshaller) => { 'vault param manager parts', ); - const makeManager = (brand, { storageNode, initialParamValues }) => { - const manager = makeVaultParamManager( - makeStoredPublisherKit(storageNode, marshaller, 'governance'), - initialParamValues, + const makeManager = ( + brand, + { govStorageNode, initialParamValues, makers }, + ) => { + const paramManagerCount = baggage.get('paramManagerCount') + 1; + baggage.set('paramManagerCount', paramManagerCount); + + const manager = provide( + baggage, + `vaultManager-${paramManagerCount} paramManager`, + () => + makeVaultParamManager( + baggage, + makeRecorderKit(govStorageNode), + initialParamValues, + makers, + ), ); + managers.init(brand, manager); return manager; }; - // restore from baggage - [...managerArgs.entries()].map(([brand, args]) => makeManager(brand, args)); + // To convert to durable paramManagers, we will extract the values in baggage, + // and use them to build durable PMs then delete the values so we don't + // try to do it again. This will NOT restore the most recent values; The EC + // will have to restore the values they want before enabling trading. + // [...managerArgs.entries()].map(([brand, args]) => makeManager(brand, args)); + trace('extracting paramManagers from baggage', managerArgs.keys()); + for (const [brand, args] of managerArgs.entries()) { + makeManager(brand, args); + managerArgs.delete(brand); + } return { /** * @param {Brand} brand * @param {StorageNode} storageNode + * @param {StorageNode} govStorageNode * @param {VaultManagerParamValues} initialParamValues + * @param {ParamGovernanceExoMakers} makers */ - addParamManager(brand, storageNode, initialParamValues) { - const args = harden({ storageNode, initialParamValues }); - managerArgs.init(brand, args); + addParamManager( + brand, + storageNode, + govStorageNode, + initialParamValues, + makers, + ) { + const args = harden({ + storageNode, + govStorageNode, + initialParamValues, + makers, + }); return makeManager(brand, args); }, /** @param {Brand} brand */ get(brand) { return managers.get(brand); }, + + /** + * @param {Brand} brand + * @returns {import('./vaultManager.js').GovernedParamGetters} + */ + getParamReader(brand) { + // @ts-expect-error override. + return managers.get(brand).getters(); + }, }; }; harden(provideVaultParamManagers); diff --git a/packages/inter-protocol/src/vaultFactory/vault.js b/packages/inter-protocol/src/vaultFactory/vault.js index 951e5ccde97d..6b2eac8a19f2 100644 --- a/packages/inter-protocol/src/vaultFactory/vault.js +++ b/packages/inter-protocol/src/vaultFactory/vault.js @@ -101,6 +101,7 @@ const validTransitions = { * vault: Vault, * ) => void} handleBalanceChange * @property {() => import('./vaultManager.js').GovernedParamGetters} getGovernedParams + * @property {() => import('./vaultManager.js').DirectorParamGetters} getDirectorParams */ /** @@ -626,7 +627,7 @@ export const prepareVault = (baggage, makeRecorderKit, zcf) => { } = seat.getProposal(); const minInitialDebt = state.manager - .getGovernedParams() + .getDirectorParams() .getMinInitialDebt(); AmountMath.isGTE(wantMinted, minInitialDebt) || Fail`Vault creation requires a minInitialDebt of ${q( diff --git a/packages/inter-protocol/src/vaultFactory/vaultDirector.js b/packages/inter-protocol/src/vaultFactory/vaultDirector.js index c60b8b2d5442..4df42a03cd5d 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultDirector.js +++ b/packages/inter-protocol/src/vaultFactory/vaultDirector.js @@ -4,9 +4,9 @@ import '@agoric/zoe/src/contracts/exported.js'; import '@agoric/governance/exported.js'; import { AmountMath, AmountShape, BrandShape, IssuerShape } from '@agoric/ertp'; -import { GovernorFacetShape } from '@agoric/governance/src/typeGuards.js'; +import { GovernorFacetI } from '@agoric/governance'; import { makeTracer } from '@agoric/internal'; -import { M, mustMatch } from '@agoric/store'; +import { initEmpty, M, mustMatch } from '@agoric/store'; import { prepareExoClassKit, provide, @@ -16,13 +16,13 @@ import { assertKeywordName } from '@agoric/zoe/src/cleanProposal.js'; import { atomicRearrange, makeRecorderTopic, + provideAll, provideEmptySeat, - SubscriberShape, - TopicsRecordShape, unitAmount, } from '@agoric/zoe/src/contractSupport/index.js'; import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; + import { makeCollectFeesInvitation } from '../collectFees.js'; import { setWakeupsForNextAuction, @@ -59,6 +59,7 @@ const trace = makeTracer('VD', true); * getGovernedParams: ( * collateralBrand: Brand, * ) => import('./vaultManager.js').GovernedParamGetters; + * getDirectorParams: () => import('./vaultManager.js').DirectorParamGetters; * mintAndTransfer: MintAndTransfer; * getShortfallReporter: () => Promise< * import('../reserve/assetReserve.js').ShortfallReporter @@ -69,7 +70,7 @@ const trace = makeTracer('VD', true); * state: State; * }>} MethodContext * - * @typedef {import('@agoric/governance/src/contractGovernance/typedParamManager').TypedParamManager< + * @typedef {import('@agoric/governance/src/contractGovernance/paramManager').ParamManager< * import('./params.js').VaultDirectorParams * >} VaultDirectorParamManager */ @@ -86,7 +87,7 @@ const shortfallInvitationKey = 'shortfallInvitation'; * @param {ERef} storageNode * @param {ERef} marshaller * @param {import('@agoric/zoe/src/contractSupport/recorder.js').MakeRecorderKit} makeRecorderKit - * @param {import('@agoric/zoe/src/contractSupport/recorder.js').MakeERecorderKit} makeERecorderKit + * @param {import('@agoric/governance/src/contractGovernance/paramManager.js').ParamGovernanceExoMakers} paramMakerKit */ const prepareVaultDirector = ( baggage, @@ -98,7 +99,7 @@ const prepareVaultDirector = ( storageNode, marshaller, makeRecorderKit, - makeERecorderKit, + paramMakerKit, ) => { /** @type {import('../reserve/assetReserve.js').ShortfallReporter} */ let shortfallReporter; @@ -114,18 +115,27 @@ const prepareVaultDirector = ( 'collateralManagers', ); - // Non-durable map because param managers aren't durable. - // In the event they're needed they can be reconstructed from contract terms and off-chain data. - /** a powerful object; can modify parameters */ - const vaultParamManagers = provideVaultParamManagers(baggage, marshaller); - - const metricsNode = E(storageNode).makeChildNode('metrics'); + /** + * A powerful object; it carries the ability to modify parameters. This is + * mitigated by ensuring that vaultManagers only have access to a read facet. + * Notice that only creator.getParamManagerRetriever() provides access to + * powerful facets of the paramManagers. + * + * vaultManagers get access to factoryPowers, which has getGovernedParams(), + * which only provides access to the ability to read parameters. Also notice + * that the VaultDirector's creator facet isn't accessed outside this file. + */ + const vaultParamManagers = provideVaultParamManagers( + baggage, + makeRecorderKit, + ); - const metricsKit = makeERecorderKit( - metricsNode, + const metricsKit = makeRecorderKit( + storageNode, /** @type {import('@agoric/zoe/src/contractSupport/recorder.js').TypedMatcher} */ ( M.any() ), + 'metrics', ); const managersNode = E(storageNode).makeChildNode('managers'); @@ -137,7 +147,7 @@ const prepareVaultDirector = ( rewardPoolAllocation: rewardPoolSeat.getCurrentAllocation(), }); }; - const writeMetrics = () => E(metricsKit.recorderP).write(sampleMetrics()); + const writeMetrics = () => E(metricsKit.recorder).write(sampleMetrics()); const updateShortfallReporter = async () => { const oldInvitation = baggage.has(shortfallInvitationKey) @@ -147,16 +157,12 @@ const prepareVaultDirector = ( SHORTFALL_INVITATION_KEY, ); - if (newInvitation === oldInvitation) { - shortfallReporter || - Fail`updateShortFallReported called with repeat invitation and no prior shortfallReporter`; - return; - } - // Update the values const zoe = zcf.getZoeService(); - // @ts-expect-error cast - shortfallReporter = E(E(zoe).offer(newInvitation)).getOfferResult(); + ({ shortfallReporter } = await provideAll(baggage, { + shortfallReporter: () => E(E(zoe).offer(newInvitation)).getOfferResult(), + })); + if (oldInvitation === undefined) { baggage.init(shortfallInvitationKey, newInvitation); } else { @@ -164,30 +170,17 @@ const prepareVaultDirector = ( } }; + /** @type {FactoryPowersFacet} */ const factoryPowers = Far('vault factory powers', { /** - * Get read-only params for this manager and its director. This grants all - * managers access to params from all managers. It's not POLA but it's a - * public authority and it reduces the number of distinct power objects to - * create. + * Get read-only params for this manager and its director. * * @param {Brand} brand */ getGovernedParams: brand => { - const vaultParamManager = vaultParamManagers.get(brand); - return Far('vault manager param manager', { - // merge director and manager params - ...directorParamManager.readonly(), - ...vaultParamManager.readonly(), - // redeclare these getters as to specify the kind of the Amount - getMinInitialDebt: /** @type {() => Amount<'nat'>} */ ( - directorParamManager.readonly().getMinInitialDebt - ), - getDebtLimit: /** @type {() => Amount<'nat'>} */ ( - vaultParamManager.readonly().getDebtLimit - ), - }); + return vaultParamManagers.getParamReader(brand); }, + getDirectorParams: () => directorParamManager.accessors().behavior, /** * Let the manager add rewards to the rewardPoolSeat without exposing the @@ -229,8 +222,8 @@ const prepareVaultDirector = ( }, }); + // defines kinds. No top-level awaits before this finishes const makeVaultManagerKit = prepareVaultManagerKit(baggage, { - makeERecorderKit, makeRecorderKit, marshaller, factoryPowers, @@ -266,11 +259,6 @@ const prepareVaultDirector = ( }); }; - /** @returns {State} */ - const initState = () => { - return {}; - }; - /** * "Director" of the vault factory, overseeing "vault managers". * @@ -282,9 +270,7 @@ const prepareVaultDirector = ( baggage, 'VaultDirector', { - creator: M.interface('creator', { - ...GovernorFacetShape, - }), + creator: GovernorFacetI, machine: M.interface('machine', { addVaultType: M.call(IssuerShape, M.string(), M.record()).returns( M.promise(), @@ -296,24 +282,25 @@ const prepareVaultDirector = ( makeReschedulerWaker: M.call().returns(M.remotable('TimerWaker')), }), public: M.interface('public', { - getCollateralManager: M.call(BrandShape).returns(M.remotable()), - getDebtIssuer: M.call().returns(IssuerShape), - getSubscription: M.call({ collateralBrand: BrandShape }).returns( - SubscriberShape, + getCollateralManager: M.call(BrandShape).returns( + M.remotable('vaultManager'), ), - getElectorateSubscription: M.call().returns(SubscriberShape), + getDebtIssuer: M.call().returns(IssuerShape), + getPublicTopics: M.call().returns(M.promise()), getGovernedParams: M.call({ collateralBrand: BrandShape }).returns( M.record(), ), - getInvitationAmount: M.call(M.string()).returns(AmountShape), - getPublicTopics: M.call().returns(TopicsRecordShape), + getParamDescriptions: M.call({ collateralBrand: BrandShape }).returns( + M.record(), + ), }), helper: M.interface('helper', { resetWakeupsForNextAuction: M.call().returns(M.promise()), start: M.call().returns(M.promise()), + getters: M.call().returns(M.any()), }), }, - initState, + initEmpty, { creator: { getParamMgrRetriever: () => @@ -361,7 +348,11 @@ const prepareVaultDirector = ( initialParamValues, ) { trace('addVaultType', collateralKeyword, initialParamValues); - mustMatch(collateralIssuer, M.remotable(), 'collateralIssuer'); + mustMatch( + collateralIssuer, + M.remotable('Issuer'), + 'collateralIssuer', + ); assertKeywordName(collateralKeyword); mustMatch( initialParamValues, @@ -380,11 +371,16 @@ const prepareVaultDirector = ( const managerStorageNode = await E(managersNode).makeChildNode( managerId, ); + const govStorageNode = await E(managerStorageNode).makeChildNode( + 'governance', + ); vaultParamManagers.addParamManager( collateralBrand, managerStorageNode, + govStorageNode, initialParamValues, + paramMakerKit, ); const startTimeStamp = await E(timer).getCurrentTimestamp(); @@ -444,35 +440,29 @@ const prepareVaultDirector = ( getCollateralManager(brandIn) { collateralManagers.has(brandIn) || Fail`Not a supported collateral type ${brandIn}`; - /** @type {VaultManager} */ return managerForCollateral(brandIn).getPublicFacet(); }, getDebtIssuer() { return debtMint.getIssuerRecord().issuer; }, - /** - * subscription for the paramManager for a particular vaultManager - * - * @param {{ collateralBrand: Brand }} selector - */ - getSubscription({ collateralBrand }) { - return vaultParamManagers.get(collateralBrand).getSubscription(); - }, - getPublicTopics() { - return topics; - }, - /** subscription for the paramManager for the vaultFactory's electorate */ - getElectorateSubscription() { - return directorParamManager.getSubscription(); + getPublicTopics(collateralBrand) { + if (collateralBrand) { + return vaultParamManagers.get(collateralBrand).getPublicTopics(); + } + return E.when(directorParamManager.getPublicTopics(), publicTopics => + harden({ + metrics: topics.metrics, + ...publicTopics, + }), + ); }, /** @param {{ collateralBrand: Brand }} selector */ getGovernedParams({ collateralBrand }) { - // TODO use named getters of TypedParamManager - return vaultParamManagers.get(collateralBrand).getParams(); + return vaultParamManagers.get(collateralBrand).getParamDescriptions(); }, - /** @param {string} name */ - getInvitationAmount(name) { - return directorParamManager.getInvitationAmount(name); + /** @param {{ collateralBrand: Brand }} selector */ + getParamDescriptions({ collateralBrand }) { + return vaultParamManagers.get(collateralBrand).getParamDescriptions(); }, }, helper: { @@ -490,6 +480,9 @@ const prepareVaultDirector = ( rescheduleWaker, ); }, + getters(collateralBrand) { + return vaultParamManagers.getParamReader(collateralBrand); + }, /** Start non-durable processes (or restart if needed after vat restart) */ async start() { const { helper, machine } = this.facets; @@ -520,6 +513,7 @@ harden(prepareVaultDirector); * ) => ReturnType>} */ export const provideDirector = (...args) => { + // defines kinds. No top-level awaits before this finishes const makeVaultDirector = prepareVaultDirector(...args); const [baggage] = args; diff --git a/packages/inter-protocol/src/vaultFactory/vaultFactory.js b/packages/inter-protocol/src/vaultFactory/vaultFactory.js index a2af199ff3e6..9e15e5f3a931 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultFactory.js +++ b/packages/inter-protocol/src/vaultFactory/vaultFactory.js @@ -17,16 +17,20 @@ import '@agoric/zoe/src/contracts/exported.js'; // contractHelper to satisfy contractGovernor. It needs to return a creatorFacet // with { getParamMgrRetriever, getInvitation, getLimitedCreatorFacet }. -import { CONTRACT_ELECTORATE } from '@agoric/governance'; -import { makeParamManagerFromTerms } from '@agoric/governance/src/contractGovernance/typedParamManager.js'; +import { + CONTRACT_ELECTORATE, + buildParamGovernanceExoMakers, + makeParamManagerFromTermsAndMakers, +} from '@agoric/governance'; import { validateElectorate } from '@agoric/governance/src/contractHelper.js'; import { makeTracer, StorageNodeShape } from '@agoric/internal'; -import { makeStoredSubscription, makeSubscriptionKit } from '@agoric/notifier'; import { M } from '@agoric/store'; import { provideAll } from '@agoric/zoe/src/contractSupport/durability.js'; import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js'; import { E } from '@endo/eventual-send'; import { FeeMintAccessShape } from '@agoric/zoe/src/typeGuards.js'; +import { provide } from '@agoric/vat-data'; + import { InvitationShape } from '../auction/params.js'; import { SHORTFALL_INVITATION_KEY, vaultDirectorParamTypes } from './params.js'; import { provideDirector } from './vaultDirector.js'; @@ -83,8 +87,10 @@ export const start = async (zcf, privateArgs, baggage) => { } = privateArgs; trace('awaiting debtMint'); - const { debtMint } = await provideAll(baggage, { + // on upgrade, this will resolve promptly, start() can complete in one crank + const { debtMint, govStorageNode } = await provideAll(baggage, { debtMint: () => zcf.registerFeeMint('Minted', privateArgs.feeMintAccess), + govStorageNode: () => E(storageNode).makeChildNode('governance'), }); zcf.setTestJig(() => ({ @@ -93,35 +99,41 @@ export const start = async (zcf, privateArgs, baggage) => { const { timerService, auctioneerPublicFacet } = zcf.getTerms(); - const { makeRecorderKit, makeERecorderKit } = prepareRecorderKitMakers( + // defines kinds. No top-level awaits before this finishes + const { makeRecorderKit } = prepareRecorderKitMakers(baggage, marshaller); + + const paramMakerKit = buildParamGovernanceExoMakers( + zcf.getZoeService(), baggage, - marshaller, ); - trace('making non-durable publishers'); - // XXX non-durable, will sever upon vat restart - const governanceSubscriptionKit = makeSubscriptionKit(); - const governanceNode = E(storageNode).makeChildNode('governance'); - const governanceSubscriber = makeStoredSubscription( - governanceSubscriptionKit.subscription, - governanceNode, - marshaller, + const governanceRecorderKit = provide(baggage, 'govRecorderKit', () => + makeRecorderKit(govStorageNode), ); - /** a powerful object; can modify the invitation */ - trace('awaiting makeParamManagerFromTerms'); - const vaultDirectorParamManager = await makeParamManagerFromTerms( - { - publisher: governanceSubscriptionKit.publication, - subscriber: governanceSubscriber, - }, - zcf, - { - [CONTRACT_ELECTORATE]: initialPoserInvitation, - [SHORTFALL_INVITATION_KEY]: initialShortfallInvitation, - }, - vaultDirectorParamTypes, + + /** + * A powerful object; it can modify parameters. including the invitation. + * Notice that the only uncontrolled access to it is in the vaultDirector's + * creator facet. + */ + const vaultDirectorParamManager = provide( + baggage, + 'vaultDirector ParamManager', + () => + makeParamManagerFromTermsAndMakers( + governanceRecorderKit, + zcf, + baggage, + { + [CONTRACT_ELECTORATE]: initialPoserInvitation, + [SHORTFALL_INVITATION_KEY]: initialShortfallInvitation, + }, + vaultDirectorParamTypes, + paramMakerKit, + ), ); + // defines kinds. No top-level awaits before this finishes const director = provideDirector( baggage, zcf, @@ -133,7 +145,7 @@ export const start = async (zcf, privateArgs, baggage) => { // XXX remove Recorder makers; remove once we excise deprecated kits for governance marshaller, makeRecorderKit, - makeERecorderKit, + paramMakerKit, ); // cannot await because it would make remote calls during vat restart @@ -142,9 +154,7 @@ export const start = async (zcf, privateArgs, baggage) => { zcf.shutdownWithFailure(err); }); - // validate async to wait for params to be finished - // UNTIL https://github.com/Agoric/agoric-sdk/issues/4343 - void validateElectorate(zcf, vaultDirectorParamManager); + await validateElectorate(zcf, vaultDirectorParamManager); return harden({ creatorFacet: director.creator, diff --git a/packages/inter-protocol/src/vaultFactory/vaultManager.js b/packages/inter-protocol/src/vaultFactory/vaultManager.js index dd9daf9efee3..72669f6e2cae 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultManager.js +++ b/packages/inter-protocol/src/vaultFactory/vaultManager.js @@ -114,13 +114,16 @@ const trace = makeTracer('VM'); * @typedef {{ * getChargingPeriod: () => RelativeTime; * getRecordingPeriod: () => RelativeTime; + * getMinInitialDebt: () => Amount<'nat'>; + * }} DirectorParamGetters + * + * @typedef {{ * getDebtLimit: () => Amount<'nat'>; * getInterestRate: () => Ratio; * getLiquidationPadding: () => Ratio; * getLiquidationMargin: () => Ratio; * getLiquidationPenalty: () => Ratio; * getMintFee: () => Ratio; - * getMinInitialDebt: () => Amount<'nat'>; * }} GovernedParamGetters */ @@ -182,7 +185,6 @@ const collateralEphemera = makeEphemeraProvider(() => /** @type {any} */ ({})); * zcf: import('./vaultFactory.js').VaultFactoryZCF; * marshaller: ERef; * makeRecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').MakeRecorderKit; - * makeERecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').MakeERecorderKit; * factoryPowers: import('./vaultDirector.js').FactoryPowersFacet; * }} powers */ @@ -276,7 +278,8 @@ export const prepareVaultManagerKit = ( { sloppy: true }, ), manager: M.interface('manager', { - getGovernedParams: M.call().returns(M.remotable('governedParams')), + getGovernedParams: M.call().returns(M.record()), + getDirectorParams: M.call().returns(M.record()), maxDebtFor: M.call(AmountShape).returns(AmountShape), mintAndTransfer: M.call( SeatShape, @@ -299,7 +302,7 @@ export const prepareVaultManagerKit = ( ).returns(), }), self: M.interface('self', { - getGovernedParams: M.call().returns(M.remotable('governedParams')), + getGovernedParams: M.call().returns(M.record()), makeVaultKit: M.call(SeatShape).returns(M.promise()), getCollateralQuote: M.call().returns(PriceQuoteShape), getPublicFacet: M.call().returns(M.remotable('publicFacet')), @@ -369,19 +372,20 @@ export const prepareVaultManagerKit = ( trace('helper.start() making periodNotifier'); const periodNotifier = E(timerService).makeNotifier( 0n, - factoryPowers - .getGovernedParams(collateralBrand) - .getChargingPeriod(), + factoryPowers.getDirectorParams().getChargingPeriod(), ); trace('helper.start() starting observe periodNotifier'); void observeNotifier(periodNotifier, { - updateState: updateTime => - facets.helper + updateState: updateTime => { + trace('charge interest', updateTime); + + return facets.helper .chargeAllVaults(updateTime) .catch(e => console.error('🚨 vaultManager failed to charge interest', e), - ), + ); + }, fail: reason => { zcf.shutdownWithFailure( assert.error(X`Unable to continue without a timer: ${reason}`), @@ -443,10 +447,10 @@ export const prepareVaultManagerKit = ( { interestRate, chargingPeriod: factoryPowers - .getGovernedParams(collateralBrand) + .getDirectorParams() .getChargingPeriod(), recordingPeriod: factoryPowers - .getGovernedParams(collateralBrand) + .getDirectorParams() .getRecordingPeriod(), }, { @@ -777,6 +781,9 @@ export const prepareVaultManagerKit = ( const { collateralBrand } = this.state; return factoryPowers.getGovernedParams(collateralBrand); }, + getDirectorParams() { + return factoryPowers.getDirectorParams(); + }, /** * Look up the most recent price authority price to determine the max @@ -956,7 +963,6 @@ export const prepareVaultManagerKit = ( } = this; trace(state.collateralBrand, 'makeVaultKit'); const { storageNode } = this.state; - assert(marshaller, 'makeVaultKit missing marshaller'); assert(storageNode, 'makeVaultKit missing storageNode'); assert(zcf, 'makeVaultKit missing zcf'); diff --git a/packages/inter-protocol/test/auction/test-auctionBook.js b/packages/inter-protocol/test/auction/test-auctionBook.js index b288460dc817..5e01c184f7e1 100644 --- a/packages/inter-protocol/test/auction/test-auctionBook.js +++ b/packages/inter-protocol/test/auction/test-auctionBook.js @@ -36,10 +36,7 @@ const setupBasics = async () => { const marshaller = makeFakeBoard().getReadonlyMarshaller(); - const { makeERecorderKit, makeRecorderKit } = prepareRecorderKitMakers( - baggage, - marshaller, - ); + const { makeRecorderKit } = prepareRecorderKitMakers(baggage, marshaller); return { moolaKit, moola, @@ -48,7 +45,6 @@ const setupBasics = async () => { zoe, zcf, baggage, - makeERecorderKit, makeRecorderKit, }; }; diff --git a/packages/inter-protocol/test/auction/test-scheduler.js b/packages/inter-protocol/test/auction/test-scheduler.js index e33d5bddf7f5..23de251272f6 100644 --- a/packages/inter-protocol/test/auction/test-scheduler.js +++ b/packages/inter-protocol/test/auction/test-scheduler.js @@ -3,10 +3,12 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { subscribeEach, makePublishKit } from '@agoric/notifier'; import { buildManualTimer } from '@agoric/swingset-vat/tools/manual-timer.js'; import { TimeMath } from '@agoric/time'; -import { prepareMockRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js'; import { setupZCFTest } from '@agoric/zoe/test/unitTests/zcf/setupZcfTest.js'; import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; import { objectMap } from '@agoric/internal'; +import { makeScalarBigMapStore } from '@agoric/vat-data'; +import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/index.js'; +import { makeFakeBoard } from '@agoric/vats/tools/board-utils.js'; import { makeAuctioneerParamManager, @@ -18,67 +20,107 @@ import { getInvitation, makeDefaultParams, makeFakeAuctioneer, - makeGovernancePublisherFromFakes, setUpInstallations, } from './tools.js'; - -/** @typedef {import('@agoric/time/src/types').TimerService} TimerService */ - -test('schedule start to finish', async t => { +import { makeMockChainStorageRoot } from '../supports.js'; + +const setupScheduleTest = async ( + t, + customParams, + startTime = 0n, + child = '', +) => { const { zcf, zoe } = await setupZCFTest(); + const installations = await setUpInstallations(zoe); + // @ts-expect-error This used to work. What's wrong now? /** @type {TimerService & { advanceTo: (when: Timestamp) => bigint }} */ const timer = buildManualTimer(); const timerBrand = await timer.getTimerBrand(); const fakeAuctioneer = makeFakeAuctioneer(); - const { fakeInvitationPayment } = await getInvitation(zoe, installations); + const { fakeInvitationPayment, fakeInvitationAmount } = await getInvitation( + zoe, + installations, + ); + + const marshaller = makeFakeBoard().getReadonlyMarshaller(); + const baggage = makeScalarBigMapStore('baggage'); + const { makeRecorderKit } = prepareRecorderKitMakers(baggage, marshaller); + const storageNode = makeMockChainStorageRoot(); - const { makeRecorderKit, storageNode } = prepareMockRecorderKitMakers(); - const recorderKit = makeRecorderKit(storageNode); + let recorderKit; + if (child) { + recorderKit = makeRecorderKit(storageNode.makeChildNode(child)); + } else { + recorderKit = makeRecorderKit(storageNode); + } const scheduleTracker = await subscriptionTracker( t, subscribeEach(recorderKit.subscriber), ); - let defaultParams = makeDefaultParams(fakeInvitationPayment, timerBrand); - // at 0: capturePrice, at 1: 1st step, at 3: 2nd step, - // at 5: 3rd step, reset price, set final - defaultParams = { - ...defaultParams, - AuctionStartDelay: 1n, - StartFrequency: 10n, - PriceLockPeriod: 5n, - }; + + const defaultParams = makeDefaultParams( + [fakeInvitationPayment, fakeInvitationAmount], + timerBrand, + ); /** @type {import('../../src/auction/params.js').AuctionParams} */ // @ts-expect-error ignore missing values for test const paramValues = objectMap( - makeAuctioneerParams(defaultParams), + makeAuctioneerParams({ ...defaultParams, ...customParams }), r => r.value, ); - /** @type {bigint} */ - let now = await timer.advanceTo(127n); + if (startTime) { + await timer.advanceTo(startTime); + } - const { publisher } = makeGovernancePublisherFromFakes(); const paramManager = await makeAuctioneerParamManager( - // @ts-expect-error test fakes - { publisher, subscriber: null }, + recorderKit, zcf, + makeScalarBigMapStore('baggage'), paramValues, ); const { subscriber } = makePublishKit(); + const { behavior: params } = await paramManager.accessors(); const scheduler = await makeScheduler( fakeAuctioneer, timer, - paramManager, + params, timer.getTimerBrand(), recorderKit.recorder, - // @ts-expect-error Oops. Wrong kind of subscriber. subscriber, ); + return { + zoe, + zcf, + timer, + fakeAuctioneer, + scheduleTracker, + scheduler, + paramManager, + installations, + storageNode, + makeRecorderKit, + }; +}; + +test('schedule start to finish', async t => { + // at 0: capturePrice, at 1: 1st step, at 3: 2nd step, + // at 5: 3rd step, reset price, set final + const customParams = { + AuctionStartDelay: 1n, + StartFrequency: 10n, + PriceLockPeriod: 5n, + }; + const { timer, fakeAuctioneer, scheduleTracker, scheduler } = + await setupScheduleTest(t, customParams, 127n); + const timerBrand = await timer.getTimerBrand(); const schedule = scheduler.getSchedule(); + let now = timer.getCurrentTimestamp(); + t.deepEqual(schedule.liveAuctionSchedule, null); const firstSchedule = { startTime: TimeMath.coerceTimestampRecord(131n, timerBrand), @@ -91,19 +133,38 @@ test('schedule start to finish', async t => { }; t.deepEqual(schedule.nextAuctionSchedule, firstSchedule); + const relative = time => TimeMath.coerceRelativeTimeRecord(time, timerBrand); + t.false(fakeAuctioneer.getState().final); t.is(fakeAuctioneer.getState().step, 0); t.false(fakeAuctioneer.getState().capturedPrices); // :08 - now = await timer.advanceTo(now + 1n); + now = await timer.advanceTo(TimeMath.addAbsRel(now, 1n)); t.is(fakeAuctioneer.getState().step, 0); t.false(fakeAuctioneer.getState().final); t.false(fakeAuctioneer.getState().capturedPrices); - await scheduleTracker.assertInitial({ + await scheduleTracker.assertLike({ + current: { + AuctionStartDelay: { + type: 'relativeTime', + value: relative(customParams.AuctionStartDelay), + }, + PriceLockPeriod: { + type: 'relativeTime', + value: relative(customParams.PriceLockPeriod), + }, + StartFrequency: { + type: 'relativeTime', + value: relative(customParams.StartFrequency), + }, + }, + }); + await scheduleTracker.assertChange({ activeStartTime: null, + current: undefined, nextDescendingStepTime: TimeMath.coerceTimestampRecord(131n, timerBrand), nextStartTime: TimeMath.coerceTimestampRecord(131n, timerBrand), }); @@ -120,7 +181,7 @@ test('schedule start to finish', async t => { }); // XX:01 - now = await timer.advanceTo(now + 1n); + now = await timer.advanceTo(TimeMath.addAbsRel(now, 1n)); await scheduleTracker.assertChange({ nextDescendingStepTime: { absValue: 133n }, }); @@ -256,355 +317,80 @@ test('schedule start to finish', async t => { }); test('lowest >= starting', async t => { - const { zcf, zoe } = await setupZCFTest(); - const installations = await setUpInstallations(zoe); - /** @type {TimerService & { advanceTo: (when: Timestamp) => void }} */ - const timer = buildManualTimer(); - const timerBrand = await timer.getTimerBrand(); - - const fakeAuctioneer = makeFakeAuctioneer(); - const { fakeInvitationPayment } = await getInvitation(zoe, installations); - const { makeRecorderKit, storageNode } = prepareMockRecorderKitMakers(); - const recorderKit = makeRecorderKit(storageNode); - - let defaultParams = makeDefaultParams(fakeInvitationPayment, timerBrand); - defaultParams = { - ...defaultParams, + const customParams = { LowestRate: 110n, StartingRate: 105n, }; - /** @type {import('../../src/auction/params.js').AuctionParams} */ - // @ts-expect-error ignore missing values for test - const paramValues = objectMap( - makeAuctioneerParams(defaultParams), - r => r.value, - ); - - await timer.advanceTo(127n); - - const { publisher } = makeGovernancePublisherFromFakes(); - const paramManager = await makeAuctioneerParamManager( - // @ts-expect-error test fakes - { publisher, subscriber: null }, - zcf, - paramValues, - ); + const { scheduler } = await setupScheduleTest(t, customParams, 127n); - const { subscriber } = makePublishKit(); - const scheduler = await makeScheduler( - fakeAuctioneer, - timer, - paramManager, - timer.getTimerBrand(), - recorderKit.recorder, - // @ts-expect-error Oops. wrong kind of subscriber. - subscriber, - ); t.is(scheduler.getSchedule().nextAuctionSchedule, null); }); test('zero time for auction', async t => { - const { zcf, zoe } = await setupZCFTest(); - const installations = await setUpInstallations(zoe); - /** @type {TimerService & { advanceTo: (when: Timestamp) => void }} */ - const timer = buildManualTimer(); - const timerBrand = await timer.getTimerBrand(); - - const fakeAuctioneer = makeFakeAuctioneer(); - const { fakeInvitationPayment } = await getInvitation(zoe, installations); - const publisherKit = makeGovernancePublisherFromFakes(); - const { makeRecorderKit, storageNode } = prepareMockRecorderKitMakers(); - const recorderKit = makeRecorderKit(storageNode); - - let defaultParams = makeDefaultParams(fakeInvitationPayment, timerBrand); - defaultParams = { - ...defaultParams, + const customParams = { StartFrequency: 2n, ClockStep: 3n, AuctionStartDelay: 1n, PriceLockPeriod: 1n, }; - /** @type {import('../../src/auction/params.js').AuctionParams} */ - // @ts-expect-error ignore missing values for test - const paramValues = objectMap( - makeAuctioneerParams(defaultParams), - r => r.value, - ); - - await timer.advanceTo(127n); + const { scheduler } = await setupScheduleTest(t, customParams, 127n); - const paramManager = await makeAuctioneerParamManager( - publisherKit, - zcf, - paramValues, - ); - - const { subscriber } = makePublishKit(); - const scheduler = await makeScheduler( - fakeAuctioneer, - timer, - paramManager, - timer.getTimerBrand(), - recorderKit.recorder, - // @ts-expect-error Oops. wrong kind of subscriber. - subscriber, - ); t.is(scheduler.getSchedule().nextAuctionSchedule, null); }); test('discountStep 0', async t => { - const { zcf, zoe } = await setupZCFTest(); - const installations = await setUpInstallations(zoe); - /** @type {TimerService & { advanceTo: (when: Timestamp) => void }} */ - const timer = buildManualTimer(); - const timerBrand = await timer.getTimerBrand(); - - const fakeAuctioneer = makeFakeAuctioneer(); - const { fakeInvitationPayment } = await getInvitation(zoe, installations); - const publisherKit = makeGovernancePublisherFromFakes(); - - const { makeRecorderKit, storageNode } = prepareMockRecorderKitMakers(); - const recorderKit = makeRecorderKit(storageNode); - let defaultParams = makeDefaultParams(fakeInvitationPayment, timerBrand); - defaultParams = { - ...defaultParams, + const customParams = { DiscountStep: 0n, }; - /** @type {import('../../src/auction/params.js').AuctionParams} */ - // @ts-expect-error ignore missing values for test - const paramValues = objectMap( - makeAuctioneerParams(defaultParams), - r => r.value, - ); - - await timer.advanceTo(127n); + const { scheduler } = await setupScheduleTest(t, customParams, 127n); - const paramManager = await makeAuctioneerParamManager( - publisherKit, - zcf, - paramValues, - ); - - const { subscriber } = makePublishKit(); - const scheduler = await makeScheduler( - fakeAuctioneer, - timer, - paramManager, - timer.getTimerBrand(), - recorderKit.recorder, - // @ts-expect-error Oops. wrong kind of subscriber. - subscriber, - ); t.is(scheduler.getSchedule().nextAuctionSchedule, null); }); test('discountStep larger than starting rate', async t => { - const { zcf, zoe } = await setupZCFTest(); - const installations = await setUpInstallations(zoe); - /** @type {TimerService & { advanceTo: (when: Timestamp) => void }} */ - const timer = buildManualTimer(); - const timerBrand = await timer.getTimerBrand(); - - const fakeAuctioneer = makeFakeAuctioneer(); - const { fakeInvitationPayment } = await getInvitation(zoe, installations); - const publisherKit = makeGovernancePublisherFromFakes(); - - const { makeRecorderKit, storageNode } = prepareMockRecorderKitMakers(); - const recorderKit = makeRecorderKit(storageNode); - let defaultParams = makeDefaultParams(fakeInvitationPayment, timerBrand); - defaultParams = { - ...defaultParams, + const customParams = { StartingRate: 10100n, DiscountStep: 10500n, }; - /** @type {import('../../src/auction/params.js').AuctionParams} */ - // @ts-expect-error ignore missing values for test - const paramValues = objectMap( - makeAuctioneerParams(defaultParams), - r => r.value, - ); - - await timer.advanceTo(127n); + const { scheduler } = await setupScheduleTest(t, customParams, 127n); - const paramManager = await makeAuctioneerParamManager( - publisherKit, - zcf, - paramValues, - ); - - const { subscriber } = makePublishKit(); - const scheduler = await makeScheduler( - fakeAuctioneer, - timer, - paramManager, - timer.getTimerBrand(), - recorderKit.recorder, - // @ts-expect-error Oops. wrong kind of subscriber. - subscriber, - ); t.is(scheduler.getSchedule().nextAuctionSchedule, null); }); test('start Freq 0', async t => { - const { zcf, zoe } = await setupZCFTest(); - const installations = await setUpInstallations(zoe); - /** @type {TimerService & { advanceTo: (when: Timestamp) => void }} */ - const timer = buildManualTimer(); - const timerBrand = await timer.getTimerBrand(); - - const fakeAuctioneer = makeFakeAuctioneer(); - const { fakeInvitationPayment } = await getInvitation(zoe, installations); - const publisherKit = makeGovernancePublisherFromFakes(); - - const { makeRecorderKit, storageNode } = prepareMockRecorderKitMakers(); - const recorderKit = makeRecorderKit(storageNode); - let defaultParams = makeDefaultParams(fakeInvitationPayment, timerBrand); - defaultParams = { - ...defaultParams, + const customParams = { StartFrequency: 0n, }; - /** @type {import('../../src/auction/params.js').AuctionParams} */ - // @ts-expect-error ignore missing values for test - const paramValues = objectMap( - makeAuctioneerParams(defaultParams), - r => r.value, - ); - - await timer.advanceTo(127n); + const { scheduler } = await setupScheduleTest(t, customParams, 127n); - const paramManager = await makeAuctioneerParamManager( - publisherKit, - zcf, - paramValues, - ); - - const { subscriber } = makePublishKit(); - const scheduler = await makeScheduler( - fakeAuctioneer, - timer, - paramManager, - timer.getTimerBrand(), - recorderKit.recorder, - // @ts-expect-error Oops. wrong kind of subscriber. - subscriber, - ); t.is(scheduler.getSchedule().nextAuctionSchedule, null); }); test('delay > freq', async t => { - const { zcf, zoe } = await setupZCFTest(); - const installations = await setUpInstallations(zoe); - /** @type {TimerService & { advanceTo: (when: Timestamp) => void }} */ - const timer = buildManualTimer(); - const timerBrand = await timer.getTimerBrand(); - - const fakeAuctioneer = makeFakeAuctioneer(); - const { fakeInvitationPayment } = await getInvitation(zoe, installations); - const publisherKit = makeGovernancePublisherFromFakes(); - - const { makeRecorderKit, storageNode } = prepareMockRecorderKitMakers(); - const recorderKit = makeRecorderKit(storageNode); - let defaultParams = makeDefaultParams(fakeInvitationPayment, timerBrand); - defaultParams = { - ...defaultParams, + const customParams = { AuctionStartDelay: 40n, StartFrequency: 20n, }; - /** @type {import('../../src/auction/params.js').AuctionParams} */ - // @ts-expect-error ignore missing values for test - const paramValues = objectMap( - makeAuctioneerParams(defaultParams), - r => r.value, - ); + const { scheduler } = await setupScheduleTest(t, customParams, 127n); - await timer.advanceTo(127n); - - const paramManager = await makeAuctioneerParamManager( - publisherKit, - zcf, - paramValues, - ); - - const { subscriber } = makePublishKit(); - const scheduler = await makeScheduler( - fakeAuctioneer, - timer, - paramManager, - timer.getTimerBrand(), - recorderKit.recorder, - // @ts-expect-error Oops. wrong kind of subscriber. - subscriber, - ); t.is(scheduler.getSchedule().nextAuctionSchedule, null); }); test('lockPeriod > freq', async t => { - const { zcf, zoe } = await setupZCFTest(); - const installations = await setUpInstallations(zoe); - /** @type {TimerService & { advanceTo: (when: Timestamp) => void }} */ - const timer = buildManualTimer(); - const timerBrand = await timer.getTimerBrand(); - - const fakeAuctioneer = makeFakeAuctioneer(); - const { fakeInvitationPayment } = await getInvitation(zoe, installations); - const publisherKit = makeGovernancePublisherFromFakes(); - - const { makeRecorderKit, storageNode } = prepareMockRecorderKitMakers(); - const recorderKit = makeRecorderKit(storageNode); - let defaultParams = makeDefaultParams(fakeInvitationPayment, timerBrand); - defaultParams = { - ...defaultParams, + const customParams = { PriceLockPeriod: 7200n, StartFrequency: 3600n, AuctionStartDelay: 500n, }; - /** @type {import('../../src/auction/params.js').AuctionParams} */ - // @ts-expect-error ignore missing values for test - const paramValues = objectMap( - makeAuctioneerParams(defaultParams), - r => r.value, - ); - - await timer.advanceTo(127n); - - const paramManager = await makeAuctioneerParamManager( - publisherKit, - zcf, - paramValues, - ); - - const { subscriber } = makePublishKit(); - const scheduler = await makeScheduler( - fakeAuctioneer, - timer, - paramManager, - timer.getTimerBrand(), - recorderKit.recorder, - // @ts-expect-error Oops. wrong kind of subscriber. - subscriber, - ); + const { scheduler } = await setupScheduleTest(t, customParams, 127n); t.is(scheduler.getSchedule().nextAuctionSchedule, null); }); // if duration = frequency, we'll cut the duration short to fit. test('duration = freq', async t => { - const { zcf, zoe } = await setupZCFTest(); - const installations = await setUpInstallations(zoe); - /** @type {TimerService & { advanceTo: (when: Timestamp) => void }} */ - const timer = buildManualTimer(); - const timerBrand = await timer.getTimerBrand(); - - const fakeAuctioneer = makeFakeAuctioneer(); - const { fakeInvitationPayment } = await getInvitation(zoe, installations); - const publisherKit = makeGovernancePublisherFromFakes(); - - const { makeRecorderKit, storageNode } = prepareMockRecorderKitMakers(); - const recorderKit = makeRecorderKit(storageNode); - let defaultParams = makeDefaultParams(fakeInvitationPayment, timerBrand); // start hourly, request 6 steps down every 10 minutes, so duration would be // 1 hour. Instead, cut the auction short. - defaultParams = { - ...defaultParams, + const customParams = { PriceLockPeriod: 20n, StartFrequency: 360n, AuctionStartDelay: 5n, @@ -613,32 +399,10 @@ test('duration = freq', async t => { LowestRate: 40n, DiscountStep: 10n, }; - /** @type {import('../../src/auction/params.js').AuctionParams} */ - // @ts-expect-error ignore missing values for test - const paramValues = objectMap( - makeAuctioneerParams(defaultParams), - r => r.value, - ); - - await timer.advanceTo(127n); - - const paramManager = await makeAuctioneerParamManager( - publisherKit, - zcf, - paramValues, - ); - const { subscriber } = makePublishKit(); - const scheduler = await makeScheduler( - fakeAuctioneer, - timer, - paramManager, - timer.getTimerBrand(), - recorderKit.recorder, - // @ts-expect-error Oops. wrong kind of subscriber. - subscriber, - ); - + const { timer, scheduler } = await setupScheduleTest(t, customParams, 127n); + const timerBrand = await timer.getTimerBrand(); let schedule = scheduler.getSchedule(); + t.deepEqual(schedule.liveAuctionSchedule, null); const firstSchedule = { startTime: TimeMath.coerceTimestampRecord(365n, timerBrand), @@ -669,32 +433,14 @@ test('duration = freq', async t => { }); test('change Schedule', async t => { - const { zcf, zoe } = await setupZCFTest(); - const installations = await setUpInstallations(zoe); - /** @type {TimerService & { advanceTo: (when: Timestamp) => void }} */ - const timer = buildManualTimer(); - const timerBrand = await timer.getTimerBrand(); - const startFreq = 360n; const lockPeriodT = 20n; - const lockPeriod = TimeMath.coerceRelativeTimeRecord(lockPeriodT, timerBrand); const startDelayT = 5n; - const startDelay = TimeMath.coerceRelativeTimeRecord(startDelayT, timerBrand); const clockStep = 60n; - const fakeAuctioneer = makeFakeAuctioneer(); - const { fakeInvitationPayment } = await getInvitation(zoe, installations); - const publisherKit = makeGovernancePublisherFromFakes(); - - const { makeRecorderKit, storageNode } = prepareMockRecorderKitMakers(); - const recorderKit = makeRecorderKit(storageNode); - let defaultParams = makeDefaultParams(fakeInvitationPayment, timerBrand); // start hourly, request 6 steps down every 10 minutes, so duration would be // 1 hour. Instead, cut the auction short. - - /** @type {import('../../src/auction/params.js').AuctionParams} */ - defaultParams = { - ...defaultParams, + const customParams = { PriceLockPeriod: lockPeriodT, StartFrequency: startFreq, AuctionStartDelay: startDelayT, @@ -703,34 +449,15 @@ test('change Schedule', async t => { LowestRate: 40n, DiscountStep: 10n, }; - - /** @type {import('../../src/auction/params.js').AuctionParams} */ - // @ts-expect-error ignore missing values for test - const paramValues = objectMap( - makeAuctioneerParams(defaultParams), - r => r.value, - ); - await timer.advanceTo(127n); - - const paramManager = await makeAuctioneerParamManager( - publisherKit, - zcf, - paramValues, + const { timer, scheduler, paramManager } = await setupScheduleTest( + t, + customParams, + 127n, ); - // XXX let the value be set async. A concession to upgradability - // UNTIL https://github.com/Agoric/agoric-sdk/issues/4343 - await paramManager.getParams(); + const timerBrand = await timer.getTimerBrand(); + const lockPeriod = TimeMath.coerceRelativeTimeRecord(lockPeriodT, timerBrand); + const startDelay = TimeMath.coerceRelativeTimeRecord(startDelayT, timerBrand); - const { subscriber } = makePublishKit(); - const scheduler = await makeScheduler( - fakeAuctioneer, - timer, - paramManager, - timer.getTimerBrand(), - recorderKit.recorder, - // @ts-expect-error Oops. wrong kind of subscriber. - subscriber, - ); let schedule = scheduler.getSchedule(); t.is(schedule.liveAuctionSchedule, null); @@ -779,9 +506,6 @@ test('change Schedule', async t => { StartFrequency: TimeMath.coerceRelativeTimeRecord(newFreq, timerBrand), ClockStep: TimeMath.coerceRelativeTimeRecord(newStep, timerBrand), }); - // XXX let the value be set async. A concession to upgradability - // UNTIL https://github.com/Agoric/agoric-sdk/issues/4343 - await paramManager.getParams(); await timer.advanceTo(expected2ndSchedule.lockTime); schedule = scheduler.getSchedule(); @@ -862,32 +586,13 @@ test('change Schedule', async t => { }); test('change Schedule late', async t => { - const { zcf, zoe } = await setupZCFTest(); - const installations = await setUpInstallations(zoe); - /** @type {TimerService & { advanceTo: (when: Timestamp) => void }} */ - const timer = buildManualTimer(); - const timerBrand = await timer.getTimerBrand(); - + // start hourly, request 6 steps down every 10 minutes, so duration would be + // 1 hour. Instead, cut the auction short. const startFreq = 360n; const lockPeriodT = 20n; - const lockPeriod = TimeMath.coerceRelativeTimeRecord(lockPeriodT, timerBrand); const startDelayT = 5n; - const startDelay = TimeMath.coerceRelativeTimeRecord(startDelayT, timerBrand); const clockStep = 60n; - - const fakeAuctioneer = makeFakeAuctioneer(); - const { fakeInvitationPayment } = await getInvitation(zoe, installations); - const publisherKit = makeGovernancePublisherFromFakes(); - - const { makeRecorderKit, storageNode } = prepareMockRecorderKitMakers(); - const recorderKit = makeRecorderKit(storageNode); - let defaultParams = makeDefaultParams(fakeInvitationPayment, timerBrand); - // start hourly, request 6 steps down every 10 minutes, so duration would be - // 1 hour. Instead, cut the auction short. - - /** @type {import('../../src/auction/params.js').AuctionParams} */ - defaultParams = { - ...defaultParams, + const customParams = { PriceLockPeriod: lockPeriodT, StartFrequency: startFreq, AuctionStartDelay: startDelayT, @@ -896,36 +601,19 @@ test('change Schedule late', async t => { LowestRate: 40n, DiscountStep: 10n, }; - - /** @type {import('../../src/auction/params.js').AuctionParams} */ - // @ts-expect-error ignore missing values for test - const paramValues = objectMap( - makeAuctioneerParams(defaultParams), - r => r.value, + const { timer, paramManager, scheduler } = await setupScheduleTest( + t, + customParams, + 127n, ); - await timer.advanceTo(127n); - await eventLoopIteration(); + const timerBrand = await timer.getTimerBrand(); + let schedule = scheduler.getSchedule(); - const paramManager = await makeAuctioneerParamManager( - publisherKit, - zcf, - paramValues, - ); - // XXX let the value be set async. A concession to upgradability - // UNTIL https://github.com/Agoric/agoric-sdk/issues/4343 - await paramManager.getParams(); + const lockPeriod = TimeMath.coerceRelativeTimeRecord(lockPeriodT, timerBrand); + const startDelay = TimeMath.coerceRelativeTimeRecord(startDelayT, timerBrand); + + await eventLoopIteration(); - const { subscriber } = makePublishKit(); - const scheduler = await makeScheduler( - fakeAuctioneer, - timer, - paramManager, - timer.getTimerBrand(), - recorderKit.recorder, - // @ts-expect-error Oops. wrong kind of subscriber. - subscriber, - ); - let schedule = scheduler.getSchedule(); t.is(schedule.liveAuctionSchedule, null); const lockTime = 345n; @@ -1022,9 +710,6 @@ test('change Schedule late', async t => { StartFrequency: TimeMath.coerceRelativeTimeRecord(newFreq, timerBrand), ClockStep: TimeMath.coerceRelativeTimeRecord(newStep, timerBrand), }); - // XXX let the value be set async. A concession to upgradability - // UNTIL https://github.com/Agoric/agoric-sdk/issues/4343 - await paramManager.getParams(); schedule = scheduler.getSchedule(); t.deepEqual(schedule.nextAuctionSchedule, expected3rdSchedule); @@ -1085,71 +770,52 @@ test('change Schedule late', async t => { }); test('schedule anomalies', async t => { - const { zcf, zoe } = await setupZCFTest(); - const installations = await setUpInstallations(zoe); - /** @type {TimerService & { advanceTo: (when: Timestamp) => bigint }} */ - const timer = buildManualTimer(); - const timerBrand = await timer.getTimerBrand(); - const timestamp = time => TimeMath.coerceTimestampRecord(time, timerBrand); - const relative = time => TimeMath.coerceRelativeTimeRecord(time, timerBrand); - - const fakeAuctioneer = makeFakeAuctioneer(); - const { fakeInvitationPayment } = await getInvitation(zoe, installations); - - const { makeRecorderKit, storageNode } = prepareMockRecorderKitMakers(); - const recorderKit = makeRecorderKit(storageNode); - - const scheduleTracker = await subscriptionTracker( - t, - subscribeEach(recorderKit.subscriber), - ); - let defaultParams = makeDefaultParams(fakeInvitationPayment, timerBrand); const oneCycle = 3600n; const delay = 30n; const lock = 300n; const step = 240n; const duration = 480n + delay; - defaultParams = { - ...defaultParams, + const customParams = { StartFrequency: oneCycle, ClockStep: step, AuctionStartDelay: delay, PriceLockPeriod: lock, }; - /** @type {import('../../src/auction/params.js').AuctionParams} */ - // @ts-expect-error ignore missing values for test - const paramValues = objectMap( - makeAuctioneerParams(defaultParams), - r => r.value, - ); - // sometime in November 2023, so we're not using times near zero const baseTime = 1700002800n; - /** @type {bigint} */ + const { timer, fakeAuctioneer, scheduler, scheduleTracker } = + await setupScheduleTest(t, customParams, baseTime - (lock + 1n), 'current'); + const timerBrand = await timer.getTimerBrand(); // ////////////// BEFORE LOCKING /////////// - let now = await timer.advanceTo(baseTime - (lock + 1n)); + let now = await timer.getCurrentTimestamp(); - const { publisher } = makeGovernancePublisherFromFakes(); - const paramManager = await makeAuctioneerParamManager( - // @ts-expect-error test fakes - { publisher, subscriber: null }, - zcf, - paramValues, - ); + const timestamp = time => TimeMath.coerceTimestampRecord(time, timerBrand); + const relative = time => TimeMath.coerceRelativeTimeRecord(time, timerBrand); - const { subscriber } = makePublishKit(); - const scheduler = await makeScheduler( - fakeAuctioneer, - timer, - paramManager, - timer.getTimerBrand(), - recorderKit.recorder, - // @ts-expect-error Oops. Wrong kind of subscriber. - subscriber, - ); const firstStart = baseTime + delay; - await scheduleTracker.assertInitial({ + await scheduleTracker.assertLike({ + current: { + AuctionStartDelay: { + type: 'relativeTime', + value: relative(customParams.AuctionStartDelay), + }, + PriceLockPeriod: { + type: 'relativeTime', + value: relative(customParams.PriceLockPeriod), + }, + StartFrequency: { + type: 'relativeTime', + value: relative(customParams.StartFrequency), + }, + ClockStep: { + type: 'relativeTime', + value: relative(customParams.ClockStep), + }, + }, + }); + await scheduleTracker.assertChange({ activeStartTime: null, + current: undefined, nextDescendingStepTime: timestamp(firstStart), nextStartTime: TimeMath.coerceTimestampRecord(baseTime + delay, timerBrand), }); diff --git a/packages/inter-protocol/test/metrics.js b/packages/inter-protocol/test/metrics.js index 8f9d8520357b..578084e3ac1e 100644 --- a/packages/inter-protocol/test/metrics.js +++ b/packages/inter-protocol/test/metrics.js @@ -142,9 +142,10 @@ export const vaultManagerMetricsTracker = async (t, publicFacet) => { assertFullyLiquidated, }); }; -export const reserveInitialState = emptyRun => ({ + +export const reserveState = runAmount => ({ allocations: {}, - shortfallBalance: emptyRun, - totalFeeBurned: emptyRun, - totalFeeMinted: emptyRun, + shortfallBalance: runAmount, + totalFeeBurned: runAmount, + totalFeeMinted: runAmount, }); diff --git a/packages/inter-protocol/test/price/test-fluxAggregatorKit.js b/packages/inter-protocol/test/price/test-fluxAggregatorKit.js index 2070c0790787..d12dc51f1875 100644 --- a/packages/inter-protocol/test/price/test-fluxAggregatorKit.js +++ b/packages/inter-protocol/test/price/test-fluxAggregatorKit.js @@ -51,17 +51,17 @@ const makeContext = async () => { const baggage = makeScalarBigMapStore('test baggage'); const quoteIssuerKit = makeIssuerKit('quote', AssetKind.SET); - const { makeDurablePublishKit, makeRecorder } = prepareRecorderKitMakers( - baggage, - marshaller, - ); + const { makeDurablePublishKit, makeRecorderKit, makeRecorder } = + prepareRecorderKitMakers(baggage, marshaller); + const childNode = await E(storageNode).makeChildNode('LINK-USD_price_feed'); const makeFluxAggregator = await prepareFluxAggregatorKit( baggage, zcfTestKit.zcf, manualTimer, { ...quoteIssuerKit, displayInfo: { assetKind: 'set' } }, - await E(storageNode).makeChildNode('LINK-USD_price_feed'), + childNode, + makeRecorderKit(childNode), makeDurablePublishKit, makeRecorder, ); @@ -695,6 +695,10 @@ test('notifications', async t => { // no new price yet publishable }); +// This test spuriously throws an exception because the setup creates a single +// price authority which only reports on “aeth” but the second vault manager +// expects quotes for “chit”. It doesn't seem worth adding a second collateral +// type to the price authority, since the test's focus is storage keys. test('storage keys', async t => { const { public: publicFacet } = await t.context.makeTestFluxAggregator( defaultConfig, diff --git a/packages/inter-protocol/test/psm/test-governedPsm.js b/packages/inter-protocol/test/psm/test-governedPsm.js index 3a10c7a345f2..c80bbfe8691e 100644 --- a/packages/inter-protocol/test/psm/test-governedPsm.js +++ b/packages/inter-protocol/test/psm/test-governedPsm.js @@ -196,6 +196,5 @@ test('replace electorate of Economic Committee', async t => { const pf = await E(governorCreatorFacet).getPublicFacet(); const { Electorate: newElectorate } = await E(pf).getGovernedParams(); t.is(newElectorate.type, 'invitation'); - // @ts-expect-error unknonwn t.is(newElectorate.value.value[0].instance, secondElectorateInstance); }); diff --git a/packages/inter-protocol/test/psm/test-psm.js b/packages/inter-protocol/test/psm/test-psm.js index 39e677e6f90c..6c16ddc28812 100644 --- a/packages/inter-protocol/test/psm/test-psm.js +++ b/packages/inter-protocol/test/psm/test-psm.js @@ -38,7 +38,6 @@ import { mintRunPayment, scale6, setUpZoeForTest, - subscriptionKey, withAmountUtils, } from '../supports.js'; import { anchorAssets, chainStorageEntries } from './psm-storage-fixture.js'; @@ -184,7 +183,7 @@ test.before(async t => { * >} t * @param {{}} [customTerms] */ -async function makePsmDriver(t, customTerms) { +const makePsmDriver = async (t, customTerms) => { const { zoe, feeMintAccess, @@ -307,7 +306,7 @@ async function makePsmDriver(t, customTerms) { }, swapMintedForAnchorSeat, }; -} +}; test('simple trades', async t => { const { terms, minted, anchor } = t.context; @@ -537,12 +536,11 @@ test('anchor is 2x minted', async t => { test('governance', async t => { const driver = await makePsmDriver(t); - t.is( - await subscriptionKey(E(driver.publicFacet).getSubscription()), - 'mockChainStorageRoot.psm.IST.AUSD.governance', - ); + const topics = await E(driver.publicFacet).getPublicTopics(); + const governancePath = `mockChainStorageRoot.psm.IST.AUSD.governance`; + t.is(await topics.governance.storagePath, governancePath); - t.like(driver.getStorageChildBody('governance'), { + t.like(driver.mockChainStorage.getBody(governancePath), { current: { Electorate: { type: 'invitation' }, GiveMintedFee: { type: 'ratio' }, diff --git a/packages/inter-protocol/test/reserve/test-reserve.js b/packages/inter-protocol/test/reserve/test-reserve.js index c66918b6e9ff..37f355b96b7b 100644 --- a/packages/inter-protocol/test/reserve/test-reserve.js +++ b/packages/inter-protocol/test/reserve/test-reserve.js @@ -6,7 +6,7 @@ import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; import { E } from '@endo/eventual-send'; import { documentStorageSchema } from '@agoric/governance/tools/storageDoc.js'; -import { reserveInitialState, subscriptionTracker } from '../metrics.js'; +import { reserveState, subscriptionTracker } from '../metrics.js'; import { setupReserveServices } from './setup.js'; /** @@ -96,7 +96,7 @@ test('check allocations', async t => { E(reserve.reservePublicFacet).getPublicTopics(), ).metrics; const m = await subscriptionTracker(t, metricsTopic); - await m.assertInitial(reserveInitialState(AmountMath.makeEmpty(stableBrand))); + await m.assertInitial(reserveState(AmountMath.makeEmpty(stableBrand))); const invitation = await E( reserve.reservePublicFacet, @@ -144,16 +144,17 @@ test('reserve track shortfall', async t => { ); const reporterFacet = await E(shortfallReporterSeat).getOfferResult(); + const metricsTopic = await E.get( + E(reserve.reservePublicFacet).getPublicTopics(), + ).metrics; + const m = await subscriptionTracker(t, metricsTopic); + await m.assertInitial(reserveState(AmountMath.makeEmpty(stableBrand))); + await E(reporterFacet).increaseLiquidationShortfall( AmountMath.make(stableBrand, 1000n), ); let runningShortfall = 1000n; - const metricsTopic = await E.get( - E(reserve.reservePublicFacet).getPublicTopics(), - ).metrics; - const m = await subscriptionTracker(t, metricsTopic); - await m.assertInitial(reserveInitialState(AmountMath.makeEmpty(stableBrand))); await m.assertChange({ shortfallBalance: { value: runningShortfall }, }); @@ -206,14 +207,14 @@ test('reserve burn IST, with snapshot', async t => { ); const reporterFacet = await E(shortfallReporterSeat).getOfferResult(); - const oneK = AmountMath.make(stableBrand, 1000n); - await E(reporterFacet).increaseLiquidationShortfall(oneK); - const metricsTopic = await E.get( E(reserve.reservePublicFacet).getPublicTopics(), ).metrics; const m = await subscriptionTracker(t, metricsTopic); - await m.assertInitial(reserveInitialState(AmountMath.makeEmpty(stableBrand))); + await m.assertInitial(reserveState(AmountMath.makeEmpty(stableBrand))); + const oneK = AmountMath.make(stableBrand, 1000n); + await E(reporterFacet).increaseLiquidationShortfall(oneK); + await m.assertChange({ shortfallBalance: { value: oneK.value }, }); @@ -274,15 +275,13 @@ test('storage keys', async t => { const { reserve } = await setupReserveServices(t, electorateTerms, timer); - // TODO restore governance public mixin - // t.is( - // await subscriptionKey(E(reserve.reservePublicFacet).getSubscription()), - // 'mockChainStorageRoot.reserve.governance', - // ); - const publicTopics = await E(reserve.reservePublicFacet).getPublicTopics(); t.is( - await publicTopics.metrics.storagePath, + await E.get(E.get(publicTopics).governance).storagePath, + 'mockChainStorageRoot.reserve.governance', + ); + t.is( + await E.get(E.get(publicTopics).metrics).storagePath, 'mockChainStorageRoot.reserve.metrics', ); }); diff --git a/packages/inter-protocol/test/supports.js b/packages/inter-protocol/test/supports.js index 33db77e4a49f..f49d6abde282 100644 --- a/packages/inter-protocol/test/supports.js +++ b/packages/inter-protocol/test/supports.js @@ -187,7 +187,7 @@ export const subscriptionKey = subscription => { /** * @param {ERef<{ - * getPublicTopics: () => import('@agoric/zoe/src/contractSupport').TopicsRecord; + * getPublicTopics: () => import('@agoric/zoe/src/contractSupport').TopicsRecord; * }>} hasTopics * @param {string} subscriberName */ @@ -222,7 +222,7 @@ export const headValueLegacy = async subscription => { /** * @param {import('ava').ExecutionContext} t * @param {ERef<{ - * getPublicTopics: () => import('@agoric/zoe/src/contractSupport').TopicsRecord; + * getPublicTopics: () => import('@agoric/zoe/src/contractSupport').TopicsRecord; * }>} hasTopics * @param {string} topicName * @param {string} path diff --git a/packages/inter-protocol/test/swingsetTests/reserve/bootstrap-assetReserve-upgrade.js b/packages/inter-protocol/test/swingsetTests/reserve/bootstrap-assetReserve-upgrade.js index d6da05f78326..1d883762cc61 100644 --- a/packages/inter-protocol/test/swingsetTests/reserve/bootstrap-assetReserve-upgrade.js +++ b/packages/inter-protocol/test/swingsetTests/reserve/bootstrap-assetReserve-upgrade.js @@ -11,6 +11,7 @@ import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; import { makePromiseKit } from '@endo/promise-kit'; + import { withAmountUtils } from '../../supports.js'; const trace = makeTracer('BootFAUpg'); @@ -21,6 +22,7 @@ const moola = withAmountUtils(makeIssuerKit('moola')); export const buildRootObject = async () => { const storageKit = makeFakeStorageKit('assetReserveUpgradeTest'); + const { nameAdmin: namesByAddressAdmin } = makeNameHubKit(); const timer = buildManualTimer(); const marshaller = makeFakeBoard().getReadonlyMarshaller(); @@ -142,6 +144,7 @@ export const buildRootObject = async () => { installations.committee = await E(zoeService).installBundleID( await E(vatAdmin).getBundleIDByName('committee'), ); + const ccStartResult = await E(zoeService).startInstance( installations.committee, harden({}), diff --git a/packages/inter-protocol/test/test-provisionPool.js b/packages/inter-protocol/test/test-provisionPool.js index 60ac910bb73f..87e2788728e5 100644 --- a/packages/inter-protocol/test/test-provisionPool.js +++ b/packages/inter-protocol/test/test-provisionPool.js @@ -13,11 +13,12 @@ import { makeNameHubKit } from '@agoric/vats/src/nameHub.js'; import { buildRootObject as buildBankRoot } from '@agoric/vats/src/vat-bank.js'; import { PowerFlags } from '@agoric/vats/src/walletFlags.js'; import { makeFakeBankKit } from '@agoric/vats/tools/bank-utils.js'; -import { makeFakeBoard } from '@agoric/vats/tools/board-utils.js'; import { makeRatio } from '@agoric/zoe/src/contractSupport/ratio.js'; import { E, Far } from '@endo/far'; import path from 'path'; import { publishDepositFacet } from '@agoric/smart-wallet/src/walletFactory.js'; +import { makeMarshal } from '@endo/marshal'; + import { makeBridgeProvisionTool } from '../src/provisionPoolKit.js'; import { makeMockChainStorageRoot, @@ -68,8 +69,7 @@ const makeTestContext = async () => { const mintLimit = AmountMath.make(mintedBrand, MINT_LIMIT); - const marshaller = makeFakeBoard().getReadonlyMarshaller(); - + const marshaller = Far('Marshaller', { ...makeMarshal() }); const storageRoot = makeMockChainStorageRoot(); const { creatorFacet: committeeCreator } = await E(zoe).startInstance( committeeInstall, @@ -155,7 +155,7 @@ const tools = context => { feeMintAccess, initialPoserInvitation, storageNode: storageRoot.makeChildNode(name), - marshaller: makeFakeBoard().getReadonlyMarshaller(), + marshaller: Far('Marshaller', { ...makeMarshal() }), }, ); }; @@ -213,7 +213,7 @@ test('provisionPool trades provided assets for IST', async t => { poolBank, initialPoserInvitation, storageNode: storageRoot.makeChildNode('provisionPool'), - marshaller: makeFakeBoard().getReadonlyMarshaller(), + marshaller: Far('Marshaller', { ...makeMarshal() }), }, ); @@ -459,7 +459,7 @@ test('provisionPool revives old wallets', async t => { poolBank, initialPoserInvitation, storageNode: storageRoot.makeChildNode('provisionPool'), - marshaller: makeFakeBoard().getReadonlyMarshaller(), + marshaller: Far('Marshaller', { ...makeMarshal() }), }, ); const creatorFacet = E(facets.creatorFacet).getLimitedCreatorFacet(); @@ -568,7 +568,7 @@ test('provisionPool publishes metricsOverride promptly', async t => { poolBank, initialPoserInvitation, storageNode: storageRoot.makeChildNode('provisionPool'), - marshaller: makeFakeBoard().getReadonlyMarshaller(), + marshaller: Far('Marshaller', { ...makeMarshal() }), metricsOverride: { totalMintedConverted: minted.make(20_000_000n), totalMintedProvided: minted.make(750_000n), @@ -577,6 +577,7 @@ test('provisionPool publishes metricsOverride promptly', async t => { }, ); + await eventLoopIteration(); const metrics = E(facets.publicFacet).getMetrics(); const { diff --git a/packages/inter-protocol/test/vaultFactory/driver.js b/packages/inter-protocol/test/vaultFactory/driver.js index d35a87e2bf8f..881c84a0c131 100644 --- a/packages/inter-protocol/test/vaultFactory/driver.js +++ b/packages/inter-protocol/test/vaultFactory/driver.js @@ -289,6 +289,7 @@ const setupServices = async (t, initialPrice, priceBase) => { aethVaultManager, }, priceAuthority, + timer, }; }; @@ -368,7 +369,8 @@ export const makeManagerDriver = async ( Collateral: collUtils.mint.mintPayment(amount), }), ); - return E(seat).getOfferResult(); + + return seat; }, /** * @param {bigint} mintedValue @@ -391,7 +393,7 @@ export const makeManagerDriver = async ( Minted: getRunFromFaucet(t, mintedAmount), }), ); - return E(seat).getOfferResult(); + return seat; }, close: async () => { currentSeat = await E(zoe).offer(E(vault).makeCloseInvitation()); @@ -459,7 +461,6 @@ export const makeManagerDriver = async ( }, currentSeat: () => currentSeat, lastOfferResult: () => currentOfferResult, - timer: () => timer, tick: async (ticks = 1) => { await timer.tickN(ticks, 'test driver'); }, diff --git a/packages/inter-protocol/test/vaultFactory/test-storage.js b/packages/inter-protocol/test/vaultFactory/test-storage.js index 35d318f68121..84a77c7cf87f 100644 --- a/packages/inter-protocol/test/vaultFactory/test-storage.js +++ b/packages/inter-protocol/test/vaultFactory/test-storage.js @@ -2,12 +2,13 @@ import '@agoric/zoe/exported.js'; import { test as unknownTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { makeTracer } from '@agoric/internal'; -import { makeNotifierFromAsyncIterable } from '@agoric/notifier'; import { E } from '@endo/eventual-send'; -import { assertTopicPathData, subscriptionKey } from '../supports.js'; -import { makeDriverContext, makeManagerDriver } from './driver.js'; +import { subscribeEach } from '@agoric/notifier'; +import { assertTopicPathData } from '../supports.js'; +import { makeDriverContext, makeManagerDriver } from './driver.js'; import '../../src/vaultFactory/types.js'; +import { subscriptionTracker } from '../metrics.js'; /** @typedef {import('./driver.js').DriverContext & {}} Context */ /** @type {import('ava').TestFn} */ @@ -28,15 +29,12 @@ test('storage keys', async t => { const vdp = d.getVaultDirectorPublic(); await assertTopicPathData( t, + // @ts-expect-error VaultDirector has a compatible getPublicTopics(). vdp, 'metrics', 'mockChainStorageRoot.vaultFactory.metrics', ['collaterals', 'rewardPoolAllocation'], ); - t.is( - await subscriptionKey(E(vdp).getElectorateSubscription()), - 'mockChainStorageRoot.vaultFactory.governance', - ); // First manager const managerA = await E(vdp).getCollateralManager(aeth.brand); @@ -69,17 +67,9 @@ test('storage keys', async t => { 'totalShortfallReceived', ], ); - t.is( - await subscriptionKey( - E(vdp).getSubscription({ - collateralBrand: aeth.brand, - }), - ), - 'mockChainStorageRoot.vaultFactory.managers.manager0.governance', - ); // Second manager - const [managerC, chit] = await d.addVaultType('Chit'); + const [managerC] = await d.addVaultType('Chit'); await assertTopicPathData( t, E(managerC).getPublicFacet(), @@ -92,14 +82,6 @@ test('storage keys', async t => { 'metrics', 'mockChainStorageRoot.vaultFactory.managers.manager1.metrics', ); - t.is( - await subscriptionKey( - E(vdp).getSubscription({ - collateralBrand: chit.brand, - }), - ), - 'mockChainStorageRoot.vaultFactory.managers.manager1.governance', - ); // First aeth vault const vda1 = await d.makeVaultDriver(aeth.make(1000n), run.make(50n)); @@ -154,27 +136,27 @@ test('quotes storage', async t => { test('governance params', async t => { const md = await makeManagerDriver(t); const vdp = md.getVaultDirectorPublic(); - // TODO make governance work with publicTopics / assertTopicPathData - const governanceSubscription = E(vdp).getElectorateSubscription(); - t.is( - await subscriptionKey(governanceSubscription), - 'mockChainStorageRoot.vaultFactory.governance', - ); - - const notifier = makeNotifierFromAsyncIterable(governanceSubscription); - - const before = await notifier.getUpdateSince(); - t.like(before.value.current, { - ChargingPeriod: { type: 'nat', value: 2n }, - Electorate: { type: 'invitation' }, - ReferencedUI: { type: 'string', value: 'NO REFERENCE' }, - MinInitialDebt: { type: 'amount' }, - RecordingPeriod: { type: 'nat', value: 6n }, - ShortfallInvitation: { type: 'invitation' }, + // @ts-expect-error VDP's getPublicTopics() has governance. + const g = await E.get(E(vdp).getPublicTopics()).governance; + t.is(await g.storagePath, 'mockChainStorageRoot.vaultFactory.governance'); + + const gSubscriber = g.subscriber; + + const gTrack = await subscriptionTracker(t, subscribeEach(gSubscriber)); + await gTrack.assertLike({ + current: { + ChargingPeriod: { type: 'nat', value: 2n }, + Electorate: { type: 'invitation' }, + ReferencedUI: { type: 'string', value: 'NO REFERENCE' }, + MinInitialDebt: { type: 'amount' }, + RecordingPeriod: { type: 'nat', value: 6n }, + ShortfallInvitation: { type: 'invitation' }, + }, }); await md.setGovernedParam('ChargingPeriod', 99n); - const after = await notifier.getUpdateSince(before.updateCount); - t.like(after.value.current, { ChargingPeriod: { value: 99n } }); + await gTrack.assertChange({ + current: { ChargingPeriod: { value: 99n } }, + }); }); diff --git a/packages/inter-protocol/test/vaultFactory/test-vault-collateralization.js b/packages/inter-protocol/test/vaultFactory/test-vault-collateralization.js index 7157de316821..cba6a2006eb0 100644 --- a/packages/inter-protocol/test/vaultFactory/test-vault-collateralization.js +++ b/packages/inter-protocol/test/vaultFactory/test-vault-collateralization.js @@ -32,7 +32,7 @@ test('excessive loan', async t => { ); }); -test('add debt to vault under LiquidationMarging + LiquidationPadding', async t => { +test('add debt to vault under LiquidationMargin + LiquidationPadding', async t => { const { aeth, run } = t.context; const md = await makeManagerDriver(t); @@ -51,8 +51,9 @@ test('add debt to vault under LiquidationMarging + LiquidationPadding', async t key: { collateralBrand: aeth.brand }, }); // ...so we can't take the same loan out + const seat = vd.giveCollateral(100n, aeth, MARGIN_HOP); await t.throwsAsync( - vd.giveCollateral(100n, aeth, MARGIN_HOP), + E(seat).getOfferResult(), { message: /Proposed debt.*exceeds max/ }, 'adjustment still under water', ); @@ -77,8 +78,9 @@ test('add debt to vault under LiquidationMargin', async t => { }); // taking with the give fails + const seat = vd.giveCollateral(100n, aeth, 10n); await t.throwsAsync( - vd.giveCollateral(100n, aeth, 10n), + E(seat).getOfferResult(), { message: /Proposed debt.*exceeds max/ }, 'adjustment still under water', ); diff --git a/packages/inter-protocol/test/vaultFactory/test-vault-interest.js b/packages/inter-protocol/test/vaultFactory/test-vault-interest.js deleted file mode 100644 index ce2c938f430d..000000000000 --- a/packages/inter-protocol/test/vaultFactory/test-vault-interest.js +++ /dev/null @@ -1,156 +0,0 @@ -import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import '@agoric/zoe/exported.js'; - -import { E } from '@endo/eventual-send'; -import { setUpZoeForTest } from '@agoric/zoe/tools/setup-zoe.js'; -import bundleSource from '@endo/bundle-source'; -import { resolve as importMetaResolve } from 'import-meta-resolve'; - -import { AmountMath } from '@agoric/ertp'; - -import { assert } from '@agoric/assert'; -import { makeTracer } from '@agoric/internal'; - -const vaultRoot = './vault-contract-wrapper.js'; -const trace = makeTracer('TestVaultInterest', false); - -/** - * The properties will be asssigned by `setTestJig` in the contract. - * - * @typedef {object} TestContext - * @property {ZCF} zcf - * @property {ZCFMint} stableMint - * @property {IssuerKit} collateralKit - * @property {Vault} vault - * @property {Function} advanceRecordingPeriod - * @property {Function} setInterestRate - */ -let testJig; -/** @param {TestContext} jig */ -const setJig = jig => { - testJig = jig; -}; - -const { zoe, feeMintAccessP: feeMintAccess } = await setUpZoeForTest({ - setJig, - useNearRemote: true, -}); - -/** - * @param {ERef} zoeP - * @param {string} sourceRoot - */ -async function launch(zoeP, sourceRoot) { - const contractUrl = await importMetaResolve(sourceRoot, import.meta.url); - const contractPath = new URL(contractUrl).pathname; - const contractBundle = await bundleSource(contractPath); - const installation = await E(zoeP).install(contractBundle); - const { creatorInvitation, creatorFacet, instance } = await E( - zoeP, - ).startInstance( - installation, - undefined, - undefined, - harden({ feeMintAccess }), - ); - const { - stableMint, - collateralKit: { mint: collateralMint, brand: collaterlBrand }, - } = testJig; - const { brand: stableBrand } = stableMint.getIssuerRecord(); - - const collateral50 = AmountMath.make(collaterlBrand, 50n); - const proposal = harden({ - give: { Collateral: collateral50 }, - want: { Minted: AmountMath.make(stableBrand, 70n) }, - }); - const payments = harden({ - Collateral: collateralMint.mintPayment(collateral50), - }); - assert(creatorInvitation); - return { - creatorSeat: E(zoeP).offer(creatorInvitation, proposal, payments), - creatorFacet, - instance, - }; -} - -test('charges', async t => { - const { creatorSeat, creatorFacet } = await launch(zoe, vaultRoot); - - // Our wrapper gives us a Vault which holds 50 Collateral, has lent out 70 - // Minted (charging 3 Minted fee), which uses an automatic market maker that - // presents a fixed price of 4 Minted per Collateral. - await E(creatorSeat).getOfferResult(); - const { stableMint, collateralKit, vault } = testJig; - const { brand: stableBrand } = stableMint.getIssuerRecord(); - - const { brand: cBrand } = collateralKit; - - const startingDebt = 74n; - t.deepEqual( - vault.getCurrentDebt(), - AmountMath.make(stableBrand, startingDebt), - 'borrower owes 74 Minted', - ); - t.deepEqual( - vault.getCollateralAmount(), - AmountMath.make(cBrand, 50n), - 'vault holds 50 Collateral', - ); - t.deepEqual(vault.getNormalizedDebt().value, startingDebt); - - let interest = 0n; - for (const [i, charge] of [4n, 4n, 4n, 4n].entries()) { - // XXX https://github.com/Agoric/agoric-sdk/issues/5527 - await testJig.advanceRecordingPeriod(); - interest += charge; - t.is( - vault.getCurrentDebt().value, - startingDebt + interest, - `interest charge ${i} should have been ${charge}`, - ); - t.is(vault.getNormalizedDebt().value, startingDebt); - } - - trace('partially payback'); - const paybackValue = 3n; - const collateralWanted = AmountMath.make(cBrand, 1n); - const paybackAmount = AmountMath.make(stableBrand, paybackValue); - const payback = await E(creatorFacet).mintRun(paybackAmount); - const paybackSeat = E(zoe).offer( - vault.makeAdjustBalancesInvitation(), - harden({ - give: { Minted: paybackAmount }, - want: { Collateral: collateralWanted }, - }), - harden({ Minted: payback }), - ); - await E(paybackSeat).getOfferResult(); - t.deepEqual( - vault.getCurrentDebt(), - AmountMath.make(stableBrand, startingDebt + interest - paybackValue), - ); - const normalizedPaybackValue = paybackValue - 1n; - t.deepEqual( - vault.getNormalizedDebt(), - AmountMath.make(stableBrand, startingDebt - normalizedPaybackValue), - ); - - testJig.setInterestRate(25n); - - for (const [i, charge] of [22n, 27n, 34n].entries()) { - // XXX https://github.com/Agoric/agoric-sdk/issues/5527 - await testJig.advanceRecordingPeriod(); - interest += charge; - t.is( - vault.getCurrentDebt().value, - startingDebt + interest - paybackValue, - `interest charge ${i} should have been ${charge}`, - ); - t.is( - vault.getNormalizedDebt().value, - startingDebt - normalizedPaybackValue, - ); - } -}); diff --git a/packages/inter-protocol/test/vaultFactory/test-vault.js b/packages/inter-protocol/test/vaultFactory/test-vault.js index 9cdeb7fae509..fe85f12812bf 100644 --- a/packages/inter-protocol/test/vaultFactory/test-vault.js +++ b/packages/inter-protocol/test/vaultFactory/test-vault.js @@ -1,196 +1,111 @@ import '@agoric/zoe/exported.js'; -import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { test as unknownTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import { setUpZoeForTest } from '@agoric/zoe/tools/setup-zoe.js'; -import bundleSource from '@endo/bundle-source'; import { E } from '@endo/eventual-send'; -import { resolve as importMetaResolve } from 'import-meta-resolve'; import { AmountMath, makeIssuerKit } from '@agoric/ertp'; - -import { assert } from '@agoric/assert'; import { makeTracer } from '@agoric/internal'; +import { makeRatio } from '@agoric/zoe/src/contractSupport/index.js'; +import { assertPayoutAmount } from '@agoric/zoe/test/zoeTestHelpers.js'; + +import { makeDriverContext, makeManagerDriver } from './driver.js'; + +/** @typedef {import('./driver.js').DriverContext & {}} Context */ +/** @type {import('ava').TestFn} */ +const test = unknownTest; -const vaultRoot = './vault-contract-wrapper.js'; const trace = makeTracer('TestVault', false); -/** - * The properties will be asssigned by `setTestJig` in the contract. - * - * @typedef {object} TestContext - * @property {ZCF} zcf - * @property {ZCFMint} stableMint - * @property {IssuerKit} collateralKit - * @property {Vault} vault - * @property {Function} advanceRecordingPeriod - * @property {Function} setInterestRate - */ -let testJig; -/** @param {TestContext} jig */ -const setJig = jig => { - testJig = jig; -}; - -const { zoe, feeMintAccessP: feeMintAccess } = await setUpZoeForTest({ - setJig, - useNearRemote: true, +test.before(async t => { + t.context = await makeDriverContext(); + trace(t, 'CONTEXT'); }); -trace('makeZoe'); - -/** - * @param {ERef} zoeP - * @param {string} sourceRoot - */ -async function launch(zoeP, sourceRoot) { - const contractUrl = await importMetaResolve(sourceRoot, import.meta.url); - const contractPath = new URL(contractUrl).pathname; - const contractBundle = await bundleSource(contractPath); - const installation = await E(zoeP).install(contractBundle); - const { creatorInvitation, creatorFacet, instance } = await E( - zoeP, - ).startInstance( - installation, - undefined, - undefined, - harden({ feeMintAccess }), - ); - const { - stableMint, - collateralKit: { mint: collateralMint, brand: collateralBrand }, - } = testJig; - const { brand: stableBrand } = stableMint.getIssuerRecord(); - - const collateral50 = AmountMath.make(collateralBrand, 50n); - const proposal = harden({ - give: { Collateral: collateral50 }, - want: { Minted: AmountMath.make(stableBrand, 70n) }, - }); - const payments = harden({ - Collateral: collateralMint.mintPayment(collateral50), - }); - assert(creatorInvitation); - return { - creatorSeat: E(zoeP).offer(creatorInvitation, proposal, payments), - creatorFacet, - instance, - }; -} - -const helperContract = launch(zoe, vaultRoot); test('first', async t => { - const { creatorSeat, creatorFacet } = await helperContract; + const { aeth, run } = t.context; + const cBrand = aeth.brand; + const stableBrand = run.brand; - // Our wrapper gives us a Vault which holds 50 Collateral, has lent out 70 - // Minted (charging 3 Minted fee), which uses an automatic market maker that - // presents a fixed price of 4 Minted per Collateral. - await E(creatorSeat).getOfferResult(); - const { stableMint, collateralKit, vault } = testJig; - const { brand: stableBrand } = stableMint.getIssuerRecord(); + // Open a Vault with 50 Collateral, and take out 70 + // Minted (charging 3 Minted fee). - const { issuer: cIssuer, mint: cMint, brand: cBrand } = collateralKit; + const md = await makeManagerDriver(t); + const withdraw = run.make(70n); + let debt = run.make(74n); + let collateral = aeth.make(50n); + const vd = await md.makeVaultDriver(collateral, withdraw); - t.deepEqual( - vault.getCurrentDebt(), - AmountMath.make(stableBrand, 74n), - 'borrower owes 74 Minted', - ); - t.deepEqual( - vault.getCollateralAmount(), - AmountMath.make(cBrand, 50n), - 'vault holds 50 Collateral', - ); + const mintFee = makeRatio(5n, run.brand, 100n); + await vd.checkBorrowed(withdraw, mintFee); + await vd.checkBalance(debt, collateral); // Add more collateral to an existing loan. We get nothing back but a warm // fuzzy feeling. - const collateralAmount = AmountMath.make(cBrand, 20n); - const invite = await E(creatorFacet).makeAdjustBalancesInvitation(); - const giveCollateralSeat = await E(zoe).offer( - invite, - harden({ - give: { Collateral: collateralAmount }, - want: {}, // Minted: AmountMath.make(stableBrand, 2n) }, - }), - harden({ - // TODO - Collateral: cMint.mintPayment(collateralAmount), - }), - ); + const addedC = 20n; + const addedCollateral = AmountMath.make(cBrand, addedC); + collateral = AmountMath.add(collateral, addedCollateral); + + await vd.giveCollateral(addedC, aeth); + await vd.checkBalance(debt, collateral); - await E(giveCollateralSeat).getOfferResult(); - t.deepEqual( - vault.getCollateralAmount(), - AmountMath.make(cBrand, 70n), - 'vault holds 70 Collateral', - ); trace('addCollateral'); // partially payback const collateralWanted = AmountMath.make(cBrand, 1n); const paybackAmount = AmountMath.make(stableBrand, 3n); - const payback = await E(creatorFacet).mintRun(paybackAmount); - const paybackSeat = E(zoe).offer( - vault.makeAdjustBalancesInvitation(), - harden({ - give: { Minted: paybackAmount }, - want: { Collateral: collateralWanted }, - }), - harden({ Minted: payback }), - ); - await E(paybackSeat).getOfferResult(); - const returnedCollateral = await E(paybackSeat).getPayout('Collateral'); - trace('returnedCollateral', returnedCollateral, cIssuer); - const returnedAmount = await cIssuer.getAmountOf(returnedCollateral); + const seat = await vd.giveMinted(3n, aeth, 1n); + collateral = AmountMath.subtract(collateral, collateralWanted); + debt = AmountMath.subtract(debt, paybackAmount); + await vd.checkBalance(debt, collateral); + t.deepEqual( - vault.getCurrentDebt(), - AmountMath.make(stableBrand, 71n), - 'debt reduced to 71 Minted', + await E(seat).getOfferResult(), + 'We have adjusted your balances, thank you for your business', ); - t.deepEqual( - vault.getCollateralAmount(), - AmountMath.make(cBrand, 69n), - 'vault holds 69 Collateral', + const payouts = await E(seat).getPayouts(); + await assertPayoutAmount( + t, + aeth.issuer, + payouts.Collateral, + aeth.make(1n), + 'aeth', ); - t.deepEqual( - returnedAmount, - AmountMath.make(cBrand, 1n), - 'withdrew 1 collateral', + await assertPayoutAmount( + t, + run.issuer, + payouts.Minted, + run.makeEmpty(), + 'run', ); - t.is(returnedAmount.value, 1n, 'withdrew 1 collateral'); }); test('bad collateral', async t => { - const { creatorSeat: offerKit } = await helperContract; + const { aeth, run, zoe } = t.context; + const cBrand = aeth.brand; - const { stableMint, collateralKit, vault } = testJig; + // Open a Vault with 50 Collateral, and take out 70 + // Minted (charging 3 Minted fee). - // Our wrapper gives us a Vault which holds 50 Collateral, has lent out 70 - // Minted (charging 3 Minted fee), which uses an automatic market maker that - // presents a fixed price of 4 Minted per Collateral. - await E(offerKit).getOfferResult(); - const { brand: collateralBrand } = collateralKit; - const { brand: stableBrand } = stableMint.getIssuerRecord(); + const md = await makeManagerDriver(t); + const withdraw = run.make(70n); + const debt = run.make(74n); + const collateral = aeth.make(50n); + const vd = await md.makeVaultDriver(collateral, withdraw); - t.deepEqual( - vault.getCollateralAmount(), - AmountMath.make(collateralBrand, 50n), - 'vault should hold 50 Collateral', - ); - t.deepEqual( - vault.getCurrentDebt(), - AmountMath.make(stableBrand, 74n), - 'borrower owes 74 Minted', - ); + const mintFee = makeRatio(5n, run.brand, 100n); + await vd.checkBorrowed(withdraw, mintFee); + await vd.checkBalance(debt, collateral); - const collateralAmount = AmountMath.make(collateralBrand, 2n); + const collateralAmount = AmountMath.make(cBrand, 2n); // adding the wrong kind of collateral should be rejected const { mint: wrongMint, brand: wrongBrand } = makeIssuerKit('wrong'); const wrongAmount = AmountMath.make(wrongBrand, 2n); + const p = E(zoe).offer( - vault.makeAdjustBalancesInvitation(), + E(vd.vault()).makeAdjustBalancesInvitation(), harden({ give: { Collateral: collateralAmount }, want: {}, @@ -205,8 +120,4 @@ test('bad collateral', async t => { } catch (e) { t.truthy(true, 'yay rejection'); } - // p.then(_ => console.log('oops passed'), - // rej => console.log('reg', rej)); - // t.rejects(p, / /, 'addCollateral requires the right kind', {}); - // t.throws(async () => { await p; }, /was not a live payment/); }); diff --git a/packages/inter-protocol/test/vaultFactory/test-vaultFactory.js b/packages/inter-protocol/test/vaultFactory/test-vaultFactory.js index 80bc0cb63dc5..de82fa283c13 100644 --- a/packages/inter-protocol/test/vaultFactory/test-vaultFactory.js +++ b/packages/inter-protocol/test/vaultFactory/test-vaultFactory.js @@ -5,12 +5,13 @@ import { AmountMath, AssetKind, makeIssuerKit } from '@agoric/ertp'; import { combine, split } from '@agoric/ertp/src/legacy-payment-helpers.js'; import { allValues, makeTracer, objectMap } from '@agoric/internal'; import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; -import { makeNotifierFromAsyncIterable } from '@agoric/notifier'; +import { subscribeEach } from '@agoric/notifier'; import { M, matches } from '@agoric/store'; import { unsafeMakeBundleCache } from '@agoric/swingset-vat/tools/bundleTool.js'; import { ceilMultiplyBy, makeRatio, + makeRatioFromAmounts, } from '@agoric/zoe/src/contractSupport/index.js'; import { assertAmountsEqual, @@ -29,7 +30,7 @@ import { startVaultFactory } from '../../src/proposals/econ-behaviors.js'; import '../../src/vaultFactory/types.js'; import { metricsTracker, - reserveInitialState, + reserveState, subscriptionTracker, vaultManagerMetricsTracker, } from '../metrics.js'; @@ -1245,10 +1246,12 @@ test('collect fees from vault', async t => { reserveKit: { reservePublicFacet }, } = services; - const metricsTopic = await E.get(E(reservePublicFacet).getPublicTopics()) - .metrics; - const m = await subscriptionTracker(t, metricsTopic); - await m.assertInitial(reserveInitialState(run.makeEmpty())); + const topics = await E.get(E(reservePublicFacet).getPublicTopics()).metrics; + const m = await subscriptionTracker( + t, + subscribeEach(await topics.subscriber), + ); + await m.assertInitial(reserveState(run.makeEmpty())); // initial loans ///////////////////////////////////// @@ -1648,6 +1651,7 @@ test('director notifiers', async t => { const { vfPublic, vaultFactory } = services.vaultFactory; + // @ts-expect-error and yet the signatures are compatible. const m = await metricsTracker(t, vfPublic); await m.assertInitial({ @@ -1963,7 +1967,7 @@ test('manager notifiers, with snapshot', async t => { }); test('governance publisher', async t => { - const { aeth } = t.context; + const { aeth, run } = t.context; t.context.interestTiming = { chargingPeriod: 2n, recordingPeriod: 10n, @@ -1979,53 +1983,49 @@ test('governance publisher', async t => { 500n, ); const { vfPublic } = services.vaultFactory; - const directorGovNotifier = makeNotifierFromAsyncIterable( - E(vfPublic).getElectorateSubscription(), - ); - let { - value: { current }, - } = await directorGovNotifier.getUpdateSince(); - // can't deepEqual because of non-literal objects so check keys and then partial shapes - t.deepEqual(Object.keys(current), [ - 'ChargingPeriod', - 'Electorate', - 'MinInitialDebt', - 'RecordingPeriod', - 'ReferencedUI', - 'ShortfallInvitation', - ]); - t.like(current, { - ChargingPeriod: { type: 'nat', value: 2n }, - Electorate: { type: 'invitation' }, - ReferencedUI: { type: 'string', value: 'abracadabra' }, - MinInitialDebt: { type: 'amount' }, - RecordingPeriod: { type: 'nat', value: 10n }, - ShortfallInvitation: { type: 'invitation' }, + // @ts-expect-error governance is there + const { subscriber } = await E.get(E(vfPublic).getPublicTopics()).governance; + const gTrack = await subscriptionTracker(t, subscribeEach(subscriber)); + await gTrack.assertLike({ + current: { + ChargingPeriod: { type: 'nat', value: 2n }, + Electorate: { type: 'invitation' }, + ReferencedUI: { type: 'string', value: 'abracadabra' }, + MinInitialDebt: { type: 'amount' }, + RecordingPeriod: { type: 'nat', value: 10n }, + ShortfallInvitation: { type: 'invitation' }, + }, }); - const managerGovNotifier = makeNotifierFromAsyncIterable( - E(vfPublic).getSubscription({ - collateralBrand: aeth.brand, - }), - ); - ({ - value: { current }, - } = await managerGovNotifier.getUpdateSince()); - // can't deepEqual because of non-literal objects so check keys and then partial shapes - t.deepEqual(Object.keys(current), [ - 'DebtLimit', - 'InterestRate', - 'LiquidationMargin', - 'LiquidationPadding', - 'LiquidationPenalty', - 'MintFee', - ]); - t.like(current, { - DebtLimit: { type: 'amount' }, - InterestRate: { type: 'ratio' }, - LiquidationMargin: { type: 'ratio' }, - LiquidationPadding: { type: 'ratio' }, - LiquidationPenalty: { type: 'ratio' }, - MintFee: { type: 'ratio' }, + // @ts-expect-error governance is there + const topic = await E.get(E(vfPublic).getPublicTopics(aeth.brand)).governance; + const aethTracker = await subscriptionTracker( + t, + subscribeEach(topic.subscriber), + ); + await aethTracker.assertInitial({ + current: { + DebtLimit: { type: 'amount', value: run.make(1_000_000n) }, + InterestRate: { + type: 'ratio', + value: makeRatioFromAmounts(run.make(100n), run.make(10000n)), + }, + LiquidationMargin: { + type: 'ratio', + value: makeRatioFromAmounts(run.make(105n), run.make(100n)), + }, + LiquidationPadding: { + type: 'ratio', + value: makeRatioFromAmounts(run.make(0n), run.make(10000n)), + }, + LiquidationPenalty: { + type: 'ratio', + value: makeRatioFromAmounts(run.make(10n), run.make(100n)), + }, + MintFee: { + type: 'ratio', + value: makeRatioFromAmounts(run.make(500n), run.make(10_000n)), + }, + }, }); }); diff --git a/packages/inter-protocol/test/vaultFactory/test-vaultLiquidation.js b/packages/inter-protocol/test/vaultFactory/test-vaultLiquidation.js index 42cbc9cbc81d..5a894d722a2a 100644 --- a/packages/inter-protocol/test/vaultFactory/test-vaultLiquidation.js +++ b/packages/inter-protocol/test/vaultFactory/test-vaultLiquidation.js @@ -27,7 +27,7 @@ import { } from '../../src/proposals/econ-behaviors.js'; import '../../src/vaultFactory/types.js'; import { - reserveInitialState, + reserveState, subscriptionTracker, vaultManagerMetricsTracker, } from '../metrics.js'; @@ -382,7 +382,7 @@ test('price drop', async t => { .metrics; const m = await subscriptionTracker(t, metricsTopic); - await m.assertInitial(reserveInitialState(run.makeEmpty())); + await m.assertInitial(reserveState(run.makeEmpty())); await E(reserveCreatorFacet).addIssuer(aeth.issuer, 'Aeth'); @@ -603,7 +603,7 @@ test('price falls precipitously', async t => { const metricsTopic = await E.get(E(reservePublicFacet).getPublicTopics()) .metrics; const m = await subscriptionTracker(t, metricsTopic); - await m.assertInitial(reserveInitialState(run.makeEmpty())); + await m.assertInitial(reserveState(run.makeEmpty())); await assertDebtIs(debtAmount.value); await setClockAndAdvanceNTimes(manualTimer, 2, startTime, 2n); @@ -696,7 +696,7 @@ test('liquidate two loans', async t => { const metricsTopic = await E.get(E(reservePublicFacet).getPublicTopics()) .metrics; const m = await subscriptionTracker(t, metricsTopic); - await m.assertInitial(reserveInitialState(run.makeEmpty())); + await m.assertInitial(reserveState(run.makeEmpty())); let shortfallBalance = 0n; const cm = await E(aethVaultManager).getPublicFacet(); @@ -1224,7 +1224,7 @@ test('collect fees from loan', async t => { const metricsTopic = await E.get(E(reservePublicFacet).getPublicTopics()) .metrics; const reserveMetrics = await subscriptionTracker(t, metricsTopic); - await reserveMetrics.assertInitial(reserveInitialState(run.makeEmpty())); + await reserveMetrics.assertInitial(reserveState(run.makeEmpty())); const cm = await E(aethVaultManager).getPublicFacet(); const aethVaultMetrics = await vaultManagerMetricsTracker(t, cm); @@ -1475,7 +1475,7 @@ test('Auction sells all collateral w/shortfall', async t => { const metricsTopic = await E.get(E(reservePublicFacet).getPublicTopics()) .metrics; const m = await subscriptionTracker(t, metricsTopic); - await m.assertInitial(reserveInitialState(run.makeEmpty())); + await m.assertInitial(reserveState(run.makeEmpty())); let shortfallBalance = 0n; const cm = await E(aethVaultManager).getPublicFacet(); @@ -2269,7 +2269,7 @@ test('Bug 7422 vault reinstated with no assets', async t => { const m = await subscriptionTracker(t, metricsTopic); await m.assertState({ - ...reserveInitialState(run.makeEmpty()), + ...reserveState(run.makeEmpty()), shortfallBalance: run.make(shortfall), }); }); @@ -2539,7 +2539,7 @@ test('Bug 7346 excess collateral to holder', async t => { const m = await subscriptionTracker(t, metricsTopic); await m.assertState({ - ...reserveInitialState(run.makeEmpty()), + ...reserveState(run.makeEmpty()), shortfallBalance: run.makeEmpty(), allocations: { Aeth: aeth.make(309_852n), @@ -2951,7 +2951,7 @@ test('Bug 7784 reconstitute both', async t => { const m = await subscriptionTracker(t, metricsTopic); await m.assertState({ - ...reserveInitialState(run.makeEmpty()), + ...reserveState(run.makeEmpty()), shortfallBalance: run.make(5_525n), allocations: { Aeth: aeth.make(1_620n), @@ -3235,7 +3235,7 @@ test('Bug 7796 missing lockedPrice', async t => { const m = await subscriptionTracker(t, metricsTopic); await m.assertState({ - ...reserveInitialState(run.makeEmpty()), + ...reserveState(run.makeEmpty()), shortfallBalance: run.makeEmpty(), allocations: { Aeth: aeth.make(309_852n), @@ -3392,7 +3392,7 @@ test('Bug 7851 & no bidders', async t => { const m = await subscriptionTracker(t, metricsTopic); await m.assertState({ - ...reserveInitialState(run.makeEmpty()), + ...reserveState(run.makeEmpty()), shortfallBalance: run.make(0n), allocations: { Aeth: aeth.make(penalty), diff --git a/packages/inter-protocol/test/vaultFactory/vault-contract-wrapper.js b/packages/inter-protocol/test/vaultFactory/vault-contract-wrapper.js deleted file mode 100644 index e403f71029fa..000000000000 --- a/packages/inter-protocol/test/vaultFactory/vault-contract-wrapper.js +++ /dev/null @@ -1,241 +0,0 @@ -/** @file DEPRECATED use the vault test driver instead */ -import { AmountMath, makeIssuerKit } from '@agoric/ertp'; - -import { makePublishKit, observeNotifier } from '@agoric/notifier'; -import { - makeFakeMarshaller, - makeFakeStorage, -} from '@agoric/notifier/tools/testSupports.js'; -import { - prepareRecorderKit, - unitAmount, -} from '@agoric/zoe/src/contractSupport/index.js'; -import { - floorDivideBy, - makeRatio, - multiplyBy, - multiplyRatios, -} from '@agoric/zoe/src/contractSupport/ratio.js'; -import { makeFakePriceAuthority } from '@agoric/zoe/tools/fakePriceAuthority.js'; -import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; -import { E } from '@endo/eventual-send'; -import { Far } from '@endo/marshal'; - -import { priceFrom } from '../../src/auction/util.js'; -import { paymentFromZCFMint } from '../../src/vaultFactory/burn.js'; -import { prepareVault } from '../../src/vaultFactory/vault.js'; - -const BASIS_POINTS = 10000n; -const SECONDS_PER_HOUR = 60n * 60n; -const DAY = SECONDS_PER_HOUR * 24n; - -const marshaller = makeFakeMarshaller(); - -/** - * @param {ZCF} zcf - * @param {{ feeMintAccess: FeeMintAccess }} privateArgs - * @param {import('@agoric/ertp').Baggage} baggage - */ -export async function start(zcf, privateArgs, baggage) { - console.log(`contract started`); - assert.typeof(privateArgs.feeMintAccess, 'object'); - - const collateralKit = makeIssuerKit('Collateral'); - const { brand: collateralBrand } = collateralKit; - await zcf.saveIssuer(collateralKit.issuer, 'Collateral'); // todo: CollateralETH, etc - - const stableMint = await zcf.registerFeeMint( - 'Minted', - privateArgs.feeMintAccess, - ); - const { brand: stableBrand } = stableMint.getIssuerRecord(); - - const LIQUIDATION_MARGIN = makeRatio(105n, stableBrand); - - const { zcfSeat: vaultFactorySeat } = zcf.makeEmptySeatKit(); - - let vaultCounter = 0; - - let currentInterest = makeRatio(5n, stableBrand); // 5% - let compoundedInterest = makeRatio(100n, stableBrand); // starts at 1.0, no interest - - const { zcfSeat: mintSeat } = zcf.makeEmptySeatKit(); - - const { subscriber: assetSubscriber } = makePublishKit(); - - const timer = buildManualTimer(console.log, 0n, { timeStep: DAY }); - const options = { - actualBrandIn: collateralBrand, - actualBrandOut: stableBrand, - priceList: [80], - tradeList: undefined, - timer, - }; - const priceAuthority = await makeFakePriceAuthority(options); - const collateralUnit = await unitAmount(collateralBrand); - const quoteNotifier = E(priceAuthority).makeQuoteNotifier( - collateralUnit, - stableBrand, - ); - let storedCollateralQuote; - void observeNotifier(quoteNotifier, { - updateState(value) { - storedCollateralQuote = value; - }, - fail(reason) { - console.error('quoteNotifier failed to iterate', reason); - }, - }); - - const maxDebtFor = collateralAmount => { - // floorDivide because we want the debt ceiling lower - return floorDivideBy( - multiplyBy(collateralAmount, priceFrom(storedCollateralQuote)), - LIQUIDATION_MARGIN, - ); - }; - - /** @type {MintAndTransfer} */ - const mintAndTransfer = (mintReceiver, toMint, fee, nonMintTransfers) => { - const kept = AmountMath.subtract(toMint, fee); - stableMint.mintGains(harden({ Minted: toMint }), mintSeat); - /** @type {TransferPart[]} */ - const transfers = [ - ...nonMintTransfers, - [mintSeat, vaultFactorySeat, { Minted: fee }], - [mintSeat, mintReceiver, { Minted: kept }], - ]; - try { - zcf.atomicRearrange(harden(transfers)); - } catch (e) { - console.error('mintAndTransfer caught', e); - stableMint.burnLosses(harden({ Minted: toMint }), mintSeat); - throw e; - } - }; - - const burn = (toBurn, seat) => { - stableMint.burnLosses(harden({ Minted: toBurn }), seat); - }; - - /** @type {Parameters[0]} */ - const managerMock = Far('vault manager mock', { - getGovernedParams() { - return { - getDebtLimit() { - throw Error('not implemented'); - }, - getLiquidationMargin() { - return LIQUIDATION_MARGIN; - }, - getLiquidationPenalty() { - throw Error('not implemented'); - }, - getMintFee() { - return makeRatio(500n, stableBrand, BASIS_POINTS); - }, - getInterestRate() { - return currentInterest; - }, - getChargingPeriod() { - return DAY; - }, - getLiquidationPadding() { - // XXX re-use - return LIQUIDATION_MARGIN; - }, - getMinInitialDebt() { - return AmountMath.makeEmpty(stableBrand); - }, - getRecordingPeriod() { - return DAY; - }, - }; - }, - getCollateralBrand() { - return collateralBrand; - }, - getDebtBrand: () => stableBrand, - - getAssetSubscriber: () => assetSubscriber, - maxDebtFor, - mintAndTransfer, - burn, - getCollateralQuote() { - return Promise.reject(Error('Not implemented')); - }, - getCompoundedInterest: () => compoundedInterest, - scopeDescription: base => `VCW: ${base}`, - handleBalanceChange: () => { - console.warn('mock handleBalanceChange does nothing'); - }, - mintforVault: async amount => { - stableMint.mintGains({ Minted: amount }); - }, - }); - - const makeRecorderKit = prepareRecorderKit(baggage, marshaller); - - const makeVault = prepareVault(baggage, makeRecorderKit, zcf); - - const { self: vault } = await makeVault( - managerMock, - // eslint-disable-next-line no-plusplus - String(vaultCounter++), - makeFakeStorage('test.vaultContractWrapper'), - ); - - const advanceRecordingPeriod = async () => { - await timer.tick(); - - // skip the debt calculation for this mock manager - const currentInterestAsMultiplicand = makeRatio( - 100n + currentInterest.numerator.value, - currentInterest.numerator.brand, - ); - compoundedInterest = multiplyRatios( - compoundedInterest, - currentInterestAsMultiplicand, - ); - }; - - const setInterestRate = percent => { - currentInterest = makeRatio(percent, stableBrand); - }; - - zcf.setTestJig(() => ({ - advanceRecordingPeriod, - collateralKit, - stableMint, - setInterestRate, - vault, - })); - - async function makeHook(seat) { - const vaultKit = await vault.initVaultKit(seat, makeFakeStorage('test')); - return { - vault, - stableMint, - collateralKit, - actions: Far('vault actions', { - add() { - return vaultKit.invitationMakers.AdjustBalances(); - }, - }), - }; - } - - console.log(`makeContract returning`); - - const vaultAPI = Far('vaultAPI', { - makeAdjustBalancesInvitation() { - return vault.makeAdjustBalancesInvitation(); - }, - mintRun(amount) { - return paymentFromZCFMint(zcf, stableMint, amount); - }, - }); - - const testInvitation = zcf.makeInvitation(makeHook, 'foo'); - return harden({ creatorInvitation: testInvitation, creatorFacet: vaultAPI }); -} diff --git a/packages/smart-wallet/src/offers.js b/packages/smart-wallet/src/offers.js index 92e66597e5ff..f67d783c393d 100644 --- a/packages/smart-wallet/src/offers.js +++ b/packages/smart-wallet/src/offers.js @@ -38,7 +38,7 @@ export const UNPUBLISHED_RESULT = 'UNPUBLISHED'; * @param {(spec: import('./invitations').InvitationSpec) => ERef} opts.powers.invitationFromSpec * @param {(brand: Brand) => Promise} opts.powers.purseForBrand * @param {(status: OfferStatus) => void} opts.onStatusChange - * @param {(offerId: string, invitationAmount: Amount<'set'>, invitationMakers: import('./types').InvitationMakers, publicSubscribers: import('./types').PublicSubscribers | import('@agoric/zoe/src/contractSupport').TopicsRecord ) => Promise} opts.onNewContinuingOffer + * @param {(offerId: string, invitationAmount: Amount<'set'>, invitationMakers: import('./types').InvitationMakers, publicSubscribers: import('./types').PublicSubscribers | import('@agoric/zoe/src/contractSupport').TopicsRecord ) => Promise} opts.onNewContinuingOffer */ export const makeOfferExecutor = ({ zoe, diff --git a/packages/smart-wallet/src/smartWallet.js b/packages/smart-wallet/src/smartWallet.js index 4af9031866d3..2504afe1f0ad 100644 --- a/packages/smart-wallet/src/smartWallet.js +++ b/packages/smart-wallet/src/smartWallet.js @@ -621,7 +621,7 @@ export const prepareSmartWallet = (baggage, shared) => { } } }, - /** @type {(offerId: string, invitationAmount: Amount<'set'>, invitationMakers: import('./types').InvitationMakers, publicSubscribers?: import('./types').PublicSubscribers | import('@agoric/zoe/src/contractSupport').TopicsRecord) => Promise} */ + /** @type {(offerId: string, invitationAmount: Amount<'set'>, invitationMakers: import('./types').InvitationMakers, publicSubscribers?: import('./types').PublicSubscribers | import('@agoric/zoe/src/contractSupport').TopicsRecord) => Promise} */ onNewContinuingOffer: async ( offerId, invitationAmount, diff --git a/packages/smart-wallet/src/utils.js b/packages/smart-wallet/src/utils.js index de4758f5bd94..0ea0a13a32d3 100644 --- a/packages/smart-wallet/src/utils.js +++ b/packages/smart-wallet/src/utils.js @@ -125,7 +125,7 @@ export const assertHasData = async follower => { /** * Handles the case of falsy argument so the caller can consistently await. * - * @param {import('./types.js').PublicSubscribers | import('@agoric/zoe/src/contractSupport').TopicsRecord} [subscribers] + * @param {import('./types.js').PublicSubscribers | import('@agoric/zoe/src/contractSupport').TopicsRecord} [subscribers] * @returns {ERef> | null} */ export const objectMapStoragePath = subscribers => { diff --git a/packages/smart-wallet/test/supports.js b/packages/smart-wallet/test/supports.js index dd51dc9c75e3..1c332885cc67 100644 --- a/packages/smart-wallet/test/supports.js +++ b/packages/smart-wallet/test/supports.js @@ -152,7 +152,7 @@ export const makeMockTestSpace = async log => { }; /** - * @param {ERef<{getPublicTopics: () => import('@agoric/zoe/src/contractSupport').TopicsRecord}>} hasTopics + * @param {ERef<{getPublicTopics: () => import('@agoric/zoe/src/contractSupport').TopicsRecord}>} hasTopics * @param {string} subscriberName */ export const topicPath = (hasTopics, subscriberName) => { diff --git a/packages/vats/src/core/startWalletFactory.js b/packages/vats/src/core/startWalletFactory.js index 430c2ed03674..834d39f8d305 100644 --- a/packages/vats/src/core/startWalletFactory.js +++ b/packages/vats/src/core/startWalletFactory.js @@ -7,6 +7,7 @@ import { AmountMath } from '@agoric/ertp'; import { ParamTypes } from '@agoric/governance'; import { makeStorageNodeChild } from '@agoric/internal/src/lib-chainStorage.js'; import { Stable } from '@agoric/internal/src/tokens.js'; + import { makeHistoryReviver, makeBoardRemote, diff --git a/packages/vats/src/proposals/restart-vats-proposal.js b/packages/vats/src/proposals/restart-vats-proposal.js index 7ea3d6a2c139..4bb62ac0831c 100644 --- a/packages/vats/src/proposals/restart-vats-proposal.js +++ b/packages/vats/src/proposals/restart-vats-proposal.js @@ -60,7 +60,7 @@ export const restartVats = async ({ consume }, { options }) => { const tryRestartContract = async (debugName, instance, adminFacet) => { // TODO document that privateArgs cannot contain promises // TODO try making all the contract starts take resolved values - const privateArgs = await deeplyFulfilledObject( + let privateArgs = await deeplyFulfilledObject( harden(instancePrivateArgs.get(instance) || {}), ); @@ -74,6 +74,16 @@ export const restartVats = async ({ consume }, { options }) => { trace('SKIPPED', debugName); return; } + + // HACK: is there a better way? + // @ts-expect-error fishing... + if (privateArgs?.walletBridgeManager) { + // @ts-expect-error Now we know it's there. + const { walletBridgeManager: _wbm, ...newPrivateArgs } = privateArgs; + privateArgs = newPrivateArgs; + trace(`RVProp dropped "walletBridgeManager" from`, privateArgs); + } + try { await E(adminFacet).restartContract(privateArgs); trace('RESTARTED', debugName); @@ -88,7 +98,7 @@ export const restartVats = async ({ consume }, { options }) => { const debugName = kit.label || getInterfaceOf(kit.publicFacet) || 'UNLABELED'; if (debugName !== kit.label) { - console.warn('MISSING LABEL:', kit); + console.warn('MISSING CONTRACT LABEL:', kit); } await tryRestartContract(debugName, kit.instance, kit.adminFacet); } @@ -98,7 +108,7 @@ export const restartVats = async ({ consume }, { options }) => { const debugName = kit.label || getInterfaceOf(kit.publicFacet) || 'UNLABELED'; if (debugName !== kit.label) { - console.warn('MISSING LABEL:', kit); + console.warn('MISSING GOVERNED LABEL:', kit); } trace('restarting governed', debugName); diff --git a/packages/zoe/README.md b/packages/zoe/README.md index ba6747cbda5e..48d0a2fb6913 100644 --- a/packages/zoe/README.md +++ b/packages/zoe/README.md @@ -143,15 +143,15 @@ The contract defines the kinds that are held in durable storage. Thus the functi # Crank -For the first incarnation, `prepare` is allowed to return a promise that takes more than one crank to settle +For the first incarnation, `start` is allowed to return a promise that takes more than one crank to settle (e.g., because it depends upon the results of remote calls). -But in later incarnations, `prepare` must settle in one crank. +But in later incarnations, `start` must settle in one crank. Therefore such necessary values should be stashed in the baggage by earlier incarnations. The `provideAll` function in contract support is designed to support this. The reason is that all vats must be able to finish their upgrade without contacting other vats. There might be messages queued inbound to the vat being -upgraded, and the kernel safely deliver those messages until the upgrade is +upgraded, and the kernel can't safely deliver those messages until the upgrade is complete. The kernel can't tell which external messages are needed for upgrade, vs which are new work that need to be delayed until upgrade is finished, so the rule is that buildRootObject() must be standalone. diff --git a/packages/zoe/src/contractFacet/types.js b/packages/zoe/src/contractFacet/types.js index 942536c21c70..442d50dc48e3 100644 --- a/packages/zoe/src/contractFacet/types.js +++ b/packages/zoe/src/contractFacet/types.js @@ -254,7 +254,7 @@ * @callback ContractStartFn * @param {ZCF} zcf * @param {PA} privateArgs - * @param {MapStore} [baggage] + * @param {import('@agoric/vat-data').Baggage} [baggage] * @returns {ContractStartFnResult} */ diff --git a/packages/zoe/src/contractSupport/ratio.js b/packages/zoe/src/contractSupport/ratio.js index 74fb7e97f884..cfdcc66a4094 100644 --- a/packages/zoe/src/contractSupport/ratio.js +++ b/packages/zoe/src/contractSupport/ratio.js @@ -1,6 +1,6 @@ import './types.js'; -import { q, Fail } from '@agoric/assert'; -import { AmountMath } from '@agoric/ertp'; +import { Fail, q } from '@agoric/assert'; +import { AmountMath, makeBrandedAmountPattern } from '@agoric/ertp'; import { assertRecord } from '@endo/marshal'; import { isNat } from '@endo/nat'; @@ -399,3 +399,14 @@ export const ratioToNumber = ratio => { const d = Number(ratio.denominator.value); return n / d; }; + +/** @param {Ratio} ratio */ +export const makeBrandedRatioPattern = ratio => { + const numeratorAmountShape = makeBrandedAmountPattern(ratio.numerator); + const denominatorAmountShape = makeBrandedAmountPattern(ratio.denominator); + return harden({ + numerator: numeratorAmountShape, + denominator: denominatorAmountShape, + }); +}; +harden(makeBrandedRatioPattern); diff --git a/packages/zoe/src/contractSupport/recorder.js b/packages/zoe/src/contractSupport/recorder.js index cbb6d58eeb69..59ecf81781a8 100644 --- a/packages/zoe/src/contractSupport/recorder.js +++ b/packages/zoe/src/contractSupport/recorder.js @@ -1,29 +1,27 @@ import { Fail } from '@agoric/assert'; import { StorageNodeShape } from '@agoric/internal'; import { prepareDurablePublishKit } from '@agoric/notifier'; -import { - makeFakeMarshaller, - makeFakeStorage, -} from '@agoric/notifier/tools/testSupports.js'; import { mustMatch } from '@agoric/store'; -import { M, makeScalarBigMapStore, prepareExoClass } from '@agoric/vat-data'; +import { M, prepareExoClass } from '@agoric/vat-data'; import { E } from '@endo/eventual-send'; /** * Recorders support publishing data to vstorage. * - * `Recorder` is similar to `Publisher` (in that they send out data) but has different signatures: + * `Recorder` is similar to `Publisher` (in that they send out data) but has + * different signatures: * - methods are async because they await remote calls to Marshaller and StorageNode * - method names convey the durability * - omits fail() * - adds getStorageNode() from its durable state * - * Other names such as StoredPublisher or ChainStoragePublisher were considered, but found to be sometimes confused with *durability*, another trait of this class. + * Other names such as StoredPublisher or ChainStoragePublisher were considered, + * but found to be sometimes confused with *durability*, another trait of this class. */ /** * @template T - * @typedef {{ getStorageNode(): StorageNode, getStoragePath(): Promise, write(value: T): Promise, writeFinal(value: T): Promise }} Recorder + * @typedef {{ getStorageNode(): ERef, getStoragePath(): Promise, write(value: T): Promise, writeFinal(value: T): Promise }} Recorder */ /** @@ -36,8 +34,22 @@ import { E } from '@endo/eventual-send'; * @typedef {Pick, 'subscriber'> & { recorderP: ERef> }} EventualRecorderKit */ +const serializeToStorageIfOpen = async (state, value, marshaller, node) => { + const { closed, valueShape } = state; + !closed || Fail`cannot write to closed recorder`; + mustMatch(value, valueShape); + const encoded = await E(marshaller).toCapData(value); + const serialized = JSON.stringify(encoded); + await E(node).setValue(serialized); +}; + /** - * Wrap a Publisher to record all the values to chain storage. + * Wrap a Publisher to record all the values to chain storage at a child of the + * given storage node. + * + * We need to be able to create durable recorders synchronously for child + * storageNodes. Since creating the child node is async, we'll record the + * address and make the child node lazily, so the maker can return immediately. * * @param {import('@agoric/zoe').Baggage} baggage * @param {ERef} marshaller @@ -47,7 +59,7 @@ export const prepareRecorder = (baggage, marshaller) => { baggage, 'Recorder', M.interface('Recorder', { - getStorageNode: M.call().returns(StorageNodeShape), + getStorageNode: M.call().returns(M.eref(StorageNodeShape)), getStoragePath: M.call().returns(M.promise(/* string */)), write: M.call(M.any()).returns(M.promise()), writeFinal: M.call(M.any()).returns(M.promise()), @@ -55,25 +67,42 @@ export const prepareRecorder = (baggage, marshaller) => { /** * @template T * @param {PublishKit['publisher']} publisher - * @param {StorageNode} storageNode + * @param {ERef} storageNode * @param {TypedMatcher} [valueShape] + * @param {string} [childName] */ ( publisher, storageNode, valueShape = /** @type {TypedMatcher} */ (M.any()), + childName, ) => { + const childNode = childName ? undefined : storageNode; + return { closed: false, publisher, - storageNode, + parentStorageNode: storageNode, + storageNode: /** @type {ERef}*/ (childNode), + childName, storagePath: /** @type {string | undefined} */ (undefined), valueShape, }; }, { getStorageNode() { - return this.state.storageNode; + const { + state: { storageNode, childName, parentStorageNode }, + } = this; + if (!childName) { + return parentStorageNode; + } + + if (storageNode) { + return storageNode; + } + + return E(parentStorageNode).makeChildNode(childName); }, /** * Memoizes the remote call to the storage node @@ -81,14 +110,15 @@ export const prepareRecorder = (baggage, marshaller) => { * @returns {Promise} */ async getStoragePath() { - const { storagePath: heldPath } = this.state; + const { state, self } = this; + const { storagePath: heldPath } = state; // end synchronous prelude await null; if (heldPath !== undefined) { return heldPath; } - const path = await E(this.state.storageNode).getPath(); - this.state.storagePath = path; + const path = await E(self.getStorageNode()).getPath(); + state.storagePath = path; return path; }, /** @@ -98,15 +128,11 @@ export const prepareRecorder = (baggage, marshaller) => { * @returns {Promise} */ async write(value) { - const { closed, publisher, storageNode, valueShape } = this.state; - !closed || Fail`cannot write to closed recorder`; - mustMatch(value, valueShape); - const encoded = await E(marshaller).toCapData(value); - const serialized = JSON.stringify(encoded); - await E(storageNode).setValue(serialized); + const { state, self } = this; - // below here differs from writeFinal() - return publisher.publish(value); + const storageNode = self.getStorageNode(); + await serializeToStorageIfOpen(state, value, marshaller, storageNode); + return state.publisher.publish(value); }, /** * Like `write` but prevents future writes and terminates the publisher. @@ -115,16 +141,28 @@ export const prepareRecorder = (baggage, marshaller) => { * @returns {Promise} */ async writeFinal(value) { - const { closed, publisher, storageNode, valueShape } = this.state; - !closed || Fail`cannot write to closed recorder`; - mustMatch(value, valueShape); - const encoded = await E(marshaller).toCapData(value); - const serialized = JSON.stringify(encoded); - await E(storageNode).setValue(serialized); - - // below here differs from writeFinal() + const { self, state } = this; + const storageNode = self.getStorageNode(); + await serializeToStorageIfOpen(state, value, marshaller, storageNode); this.state.closed = true; - return publisher.finish(value); + return state.publisher.finish(value); + }, + }, + { + finish: ({ state }) => { + if (state.childName) { + // XXX Doesn't the catch clause satisfy our requirement? + void E.when( + E(state.parentStorageNode).makeChildNode(state.childName), + childNode => { + state.storageNode = childNode; + }, + e => + Fail`Unable ${e} to create child node for ${ + state.childName + } on ${E(state.parentStorageNode).getPath()}`, + ); + } }, }, ); @@ -144,50 +182,31 @@ harden(prepareRecorder); export const defineRecorderKit = ({ makeRecorder, makeDurablePublishKit }) => { /** * @template T - * @param {StorageNode} storageNode + * @param {ERef} storageNode * @param {TypedMatcher} [valueShape] + * @param {string} [childName] * @returns {RecorderKit} */ - const makeRecorderKit = (storageNode, valueShape) => { + const makeRecorderKit = (storageNode, valueShape, childName = undefined) => { const { subscriber, publisher } = makeDurablePublishKit(); - const recorder = makeRecorder(publisher, storageNode, valueShape); + const recorder = makeRecorder( + publisher, + storageNode, + valueShape, + childName, + ); return harden({ subscriber, recorder }); }; return makeRecorderKit; }; /** @typedef {ReturnType} MakeRecorderKit */ -/** - * `makeERecorderKit` is for closures that must return a `subscriber` synchronously but can defer the `recorder`. - * - * @see {defineRecorderKit} - * - * @param {{makeRecorder: MakeRecorder, makeDurablePublishKit: ReturnType}} makers - */ -export const defineERecorderKit = ({ makeRecorder, makeDurablePublishKit }) => { - /** - * @template T - * @param {ERef} storageNodeP - * @param {TypedMatcher} [valueShape] - * @returns {EventualRecorderKit} - */ - const makeERecorderKit = (storageNodeP, valueShape) => { - const { publisher, subscriber } = makeDurablePublishKit(); - const recorderP = E.when(storageNodeP, storageNode => - makeRecorder(publisher, storageNode, valueShape), - ); - return { subscriber, recorderP }; - }; - return makeERecorderKit; -}; -harden(defineERecorderKit); -/** @typedef {ReturnType} MakeERecorderKit */ - /** * Convenience wrapper to prepare the DurablePublishKit and Recorder kinds. - * Note that because prepareRecorder() can only be called once per baggage, + * Note that because prepareRecorder() can only be called once per vat instance, * this should only be used when there is no need for an EventualRecorderKit. - * When there is, prepare the kinds separately and pass to the kit definers. + * When EventualRecorderKits are needed, prepare the kinds separately and pass + * them to the kit definers. * * @param {import('@agoric/vat-data').Baggage} baggage * @param {ERef} marshaller @@ -204,10 +223,9 @@ export const prepareRecorderKit = (baggage, marshaller) => { /** * Convenience wrapper for DurablePublishKit and Recorder kinds. * - * NB: this defines two durable kinds. Must be called at most once per baggage. + * NB: this defines two durable kinds. Must be called at most once per vat instance. * * `makeRecorderKit` is suitable for making a durable `RecorderKit` which can be held in Exo state. - * `makeERecorderKit` is for closures that must return a `subscriber` synchronously but can defer the `recorder`. * * @param {import('@agoric/vat-data').Baggage} baggage * @param {ERef} marshaller @@ -223,29 +241,12 @@ export const prepareRecorderKitMakers = (baggage, marshaller) => { makeRecorder, makeDurablePublishKit, }); - const makeERecorderKit = defineERecorderKit({ - makeRecorder, - makeDurablePublishKit, - }); - return { + return harden({ makeDurablePublishKit, makeRecorder, makeRecorderKit, - makeERecorderKit, - }; -}; - -/** - * For use in tests - */ -export const prepareMockRecorderKitMakers = () => { - const baggage = makeScalarBigMapStore('mock recorder baggage'); - const marshaller = makeFakeMarshaller(); - return { - ...prepareRecorderKitMakers(baggage, marshaller), - storageNode: makeFakeStorage('mock recorder storage'), - }; + }); }; /** diff --git a/packages/zoe/src/contractSupport/topics.js b/packages/zoe/src/contractSupport/topics.js index 350b378e1a1b..f135c93136f3 100644 --- a/packages/zoe/src/contractSupport/topics.js +++ b/packages/zoe/src/contractSupport/topics.js @@ -24,8 +24,9 @@ export const PublicTopicShape = M.splitRecord( export const TopicsRecordShape = M.recordOf(M.string(), PublicTopicShape); /** + * @template {object} T topic value * @typedef {{ - * [topicName: string]: PublicTopic, + * [topicName: string]: PublicTopic, * }} TopicsRecord */ diff --git a/packages/zoe/tools/mockRecorderKit.js b/packages/zoe/tools/mockRecorderKit.js new file mode 100644 index 000000000000..ad6da25ed09b --- /dev/null +++ b/packages/zoe/tools/mockRecorderKit.js @@ -0,0 +1,18 @@ +import { + makeFakeMarshaller, + makeFakeStorage, +} from '@agoric/notifier/tools/testSupports.js'; +import { makeScalarBigMapStore } from '@agoric/vat-data'; + +import { prepareRecorderKitMakers } from '../src/contractSupport/recorder.js'; + +/** For use in tests */ +export const prepareMockRecorderKitMakers = () => { + const baggage = makeScalarBigMapStore('mock recorder baggage'); + const marshaller = makeFakeMarshaller(); + return { + ...prepareRecorderKitMakers(baggage, marshaller), + storageNode: makeFakeStorage('mock recorder storage'), + }; +}; +harden(prepareMockRecorderKitMakers);