From a9061dc70908bee258f388f336277ebb7f1ef705 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 7 Mar 2024 17:30:25 -0600 Subject: [PATCH 1/9] build: governance resolutions --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 6a630e90..45236c52 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "ses": "1.3.0", "@agoric/assert": "0.6.1-u11wf.0", "@agoric/ertp": "0.16.3-u14.0", + "@agoric/governance": "0.10.4-u14.0", "@agoric/store": "0.9.3-u14.0", "@agoric/xsnap": "0.14.3-u14.0", "@agoric/vat-data": "0.5.3-u14.0", From 1fce4543e54c415de610285688c109722837947a Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Mon, 18 Mar 2024 00:19:05 -0500 Subject: [PATCH 2/9] build: inter-protocol dep for econCommitteeCharter --- contract/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/contract/package.json b/contract/package.json index 4b28a62f..44373463 100644 --- a/contract/package.json +++ b/contract/package.json @@ -52,6 +52,7 @@ "dependencies": { "@agoric/ertp": "^0.16.3-u14.0", "@agoric/governance": "^0.10.4-u14.0", + "@agoric/inter-protocol": "0.16.2-u14.1", "@agoric/vats": "0.15.2-u14.0", "@agoric/zoe": "^0.26.3-u14.0", "@endo/bundle-source": "^2.8.0", From 33d792b8a3aede34697332eea00665473705ec68 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Fri, 8 Mar 2024 00:50:23 -0600 Subject: [PATCH 3/9] feat(swaparoo.contract): governed Fee param - fix: get namesByAddressAdmin from privateArgs - refactor: subordinate electorate param while writing docs, it became clear that Fee is more interesting --- contract/src/swaparoo.contract.js | 88 ++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 8 deletions(-) diff --git a/contract/src/swaparoo.contract.js b/contract/src/swaparoo.contract.js index b8164903..cf393b2e 100644 --- a/contract/src/swaparoo.contract.js +++ b/contract/src/swaparoo.contract.js @@ -6,9 +6,20 @@ import { E, Far } from '@endo/far'; import '@agoric/zoe/exported.js'; import { atomicRearrange } from '@agoric/zoe/src/contractSupport/atomicTransfer.js'; import '@agoric/zoe/src/contracts/exported.js'; +import { AmountShape } from '@agoric/ertp/src/typeGuards.js'; +import { + InstanceHandleShape, + InvitationShape, +} from '@agoric/zoe/src/typeGuards.js'; +// deep imports to avoid bloating our bundle +import { ParamTypes } from '@agoric/governance/src/constants.js'; +import { CONTRACT_ELECTORATE } from '@agoric/governance/src/contractGovernance/governParam.js'; +import { handleParamGovernance } from '@agoric/governance/src/contractHelper.js'; import { makeCollectFeesInvitation } from './collectFees.js'; import { fixHub } from './fixHub.js'; +/** @template [Slot=unknown] @typedef {import('@endo/marshal').Marshal} Marshaller */ + const { quote: q } = assert; const makeNatAmountShape = (brand, min) => @@ -40,19 +51,76 @@ export const swapWithFee = (zcf, firstSeat, secondSeat, feeSeat, feeAmount) => { let issuerNumber = 1; const IssuerShape = M.remotable('Issuer'); +const paramTypes = harden( + /** @type {const} */ ({ + Fee: ParamTypes.AMOUNT, + }), +); + +export const meta = harden({ + customTermsShape: { + electionManager: InstanceHandleShape, + governedParams: { + Fee: { + type: ParamTypes.AMOUNT, + value: AmountShape, + }, + [CONTRACT_ELECTORATE]: { + type: ParamTypes.INVITATION, + value: AmountShape, + }, + }, + }, + privateArgsShape: M.splitRecord( + { + marshaller: M.remotable('Marshaller'), + storageNode: M.remotable('StorageNode'), + namesByAddressAdmin: M.remotable('namesByAddressAdmin'), + }, + { + // only necessary on first invocation, not subsequent + initialPoserInvitation: InvitationShape, + }, + ), +}); +export const customTermsShape = meta.customTermsShape; +export const privateArgsShape = meta.privateArgsShape; + /** - * @param {ZCF<{feeAmount: Amount<'nat'>, namesByAddressAdmin: import('@agoric/vats').NamesByAddressAdmin}>} zcf + * @param {ZCF>} zcf + * + * @typedef {{ + * initialPoserInvitation: Invitation; + * storageNode: StorageNode; + * marshaller: Marshaller; + * }} GovPrivateArgs + * + * @param {GovPrivateArgs & { + * namesByAddressAdmin: import('@agoric/vats').NamesByAddressAdmin + * }} privateArgs + * @param {import('@agoric/vat-data').Baggage} baggage */ -export const start = async zcf => { +export const start = async (zcf, privateArgs, baggage) => { // set up fee handling - const { feeAmount, namesByAddressAdmin } = zcf.getTerms(); + const { namesByAddressAdmin } = privateArgs; /** @type { ERef> } */ const stableIssuer = await E(zcf.getZoeService()).getFeeIssuer(); const feeBrand = await E(stableIssuer).getBrand(); const { zcfSeat: feeSeat } = zcf.makeEmptySeatKit(); - const feeShape = makeNatAmountShape(feeBrand, feeAmount.value); const depositFacetFromAddr = fixHub(namesByAddressAdmin); + const { publicMixin, makeDurableGovernorFacet, params } = + await handleParamGovernance( + zcf, + privateArgs.initialPoserInvitation, + paramTypes, + privateArgs.storageNode, + privateArgs.marshaller, + ); + + // TODO: update with Fee param + const feeShape = makeNatAmountShape(feeBrand, params.getFee().value); + /** * @param { ZCFSeat } firstSeat * @param {{ addr: string }} offerArgs @@ -87,7 +155,7 @@ export const start = async zcf => { return; } - return swapWithFee(zcf, firstSeat, secondSeat, feeSeat, feeAmount); + return swapWithFee(zcf, firstSeat, secondSeat, feeSeat, params.getFee()); }; const secondSeatInvitation = await zcf.makeInvitation( @@ -132,13 +200,17 @@ export const start = async zcf => { const publicFacet = Far('Public', { makeFirstInvitation, + ...publicMixin, }); - const creatorFacet = Far('Creator', { + const limitedCreatorFacet = Far('Creator', { makeCollectFeesInvitation() { return makeCollectFeesInvitation(zcf, feeSeat, feeBrand, 'Fee'); }, }); - - return harden({ publicFacet, creatorFacet }); + const { governorFacet } = makeDurableGovernorFacet( + baggage, + limitedCreatorFacet, + ); + return harden({ publicFacet, creatorFacet: governorFacet }); }; harden(start); From 1e4c1675705bfd3e6a77001c655f5dce50c62b85 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 7 Mar 2024 17:31:16 -0600 Subject: [PATCH 4/9] feat(start-governed-contract): core eval support for governance - feat: startMyCommittee - platform-goals/types: governance, vats, Zoe, ERTP types make them available ambiently --- .../platform-goals/start-governed-contract.js | 280 ++++++++++++++++++ contract/src/platform-goals/types.js | 4 + 2 files changed, 284 insertions(+) create mode 100644 contract/src/platform-goals/start-governed-contract.js create mode 100644 contract/src/platform-goals/types.js diff --git a/contract/src/platform-goals/start-governed-contract.js b/contract/src/platform-goals/start-governed-contract.js new file mode 100644 index 00000000..299b9661 --- /dev/null +++ b/contract/src/platform-goals/start-governed-contract.js @@ -0,0 +1,280 @@ +// @ts-check +import { E } from '@endo/far'; + +import { allValues, zip } from '../objectTools.js'; +import { sanitizePathSegment } from './start-contract.js'; + +const { values } = Object; + +/** + * @template SF + * @typedef {import('@agoric/zoe/src/zoeService/utils').StartResult} StartResult + */ + +/** + * @template SF + * @typedef {import('@agoric/zoe/src/zoeService/utils').StartParams} StartParams + */ + +/** + * @typedef {StartResult< + * typeof import('@agoric/governance/src/committee.js').prepare + * >} CommitteeStart + */ + +/** + * cf. packages/inter-protocol/src/econCommitteeCharter.js + */ +export const INVITATION_MAKERS_DESC = 'charter member invitation'; + +export const COMMITTEES_ROOT = 'committees'; // cf startEconCommittee.js in agoric-sdk + +export const CONTRACT_ELECTORATE = 'Electorate'; + +export const ParamTypes = /** @type {const} */ ({ + AMOUNT: 'amount', + BRAND: 'brand', + INSTALLATION: 'installation', + INSTANCE: 'instance', + INVITATION: 'invitation', + NAT: 'nat', + RATIO: 'ratio', + STRING: 'string', + PASSABLE_RECORD: 'record', + TIMESTAMP: 'timestamp', + RELATIVE_TIME: 'relativeTime', + UNKNOWN: 'unknown', +}); + +/** + * Like startGovernedInstance but with parameterized committeeCreatorFacet + * + * @template {GovernableStartFn} SF + * @param {{ + * zoe: ERef; + * governedContractInstallation: ERef>; + * issuerKeywordRecord?: IssuerKeywordRecord; + * terms: Record; + * privateArgs: StartParams['privateArgs']; + * label: string; + * }} zoeArgs + * @param {{ + * governedParams: Record; + * timer: ERef; + * contractGovernor: ERef; + * governorTerms: Record; + * committeeCreatorFacet: CommitteeStart['creatorFacet']; + * }} govArgs + * @returns {Promise>} + */ +export const startMyGovernedInstance = async ( + { + zoe, + governedContractInstallation, + issuerKeywordRecord, + terms, + privateArgs, + label, + }, + { + governedParams, + timer, + contractGovernor, + governorTerms, + committeeCreatorFacet, + }, +) => { + console.log('Getting poser invitation...'); + + const poserInvitationP = E(committeeCreatorFacet).getPoserInvitation(); + const [initialPoserInvitation, electorateInvitationAmount] = + await Promise.all([ + poserInvitationP, + E(E(zoe).getInvitationIssuer()).getAmountOf(poserInvitationP), + ]); + + const fullGovernorTerms = await allValues({ + timer, + governedContractInstallation, + governed: { + terms: { + ...terms, + governedParams: { + [CONTRACT_ELECTORATE]: { + type: ParamTypes.INVITATION, + value: electorateInvitationAmount, + }, + ...governedParams, + }, + }, + issuerKeywordRecord, + label, + }, + ...governorTerms, + }); + const governorFacets = await E(zoe).startInstance( + contractGovernor, + {}, + fullGovernorTerms, + harden({ + governed: await allValues({ + ...privateArgs, + initialPoserInvitation, + }), + }), + `${label}-governor`, + ); + const [instance, publicFacet, creatorFacet, adminFacet] = await Promise.all([ + E(governorFacets.creatorFacet).getInstance(), + E(governorFacets.creatorFacet).getPublicFacet(), + E(governorFacets.creatorFacet).getCreatorFacet(), + E(governorFacets.creatorFacet).getAdminFacet(), + ]); + /** @type {GovernanceFacetKit} */ + const facets = harden({ + instance, + publicFacet, + governor: governorFacets.instance, + creatorFacet, + adminFacet, + governorCreatorFacet: governorFacets.creatorFacet, + governorAdminFacet: governorFacets.adminFacet, + }); + return facets; +}; + +/** + * @param {*} creatorFacet + * @param {ERef} namesByAddress + * @param {Record} voterAddresses + */ +export const inviteToMyCharter = async ( + creatorFacet, + namesByAddress, + voterAddresses, +) => + Promise.all( + values(voterAddresses).map(async addr => { + const debugName = `econ charter member ${addr}`; + const depositFacet = E(namesByAddress).lookup(addr, 'depositFacet'); + const invitation = await E(creatorFacet).makeCharterMemberInvitation(); + return E(depositFacet) + .receive(invitation) + .catch(err => console.error(`failed deposit to ${debugName}`, err)); + }), + ); +harden(inviteToMyCharter); + +/** + * @param {string} contractName + * @param {BootstrapPowers} powers + * @param {*} config + */ +export const startMyCharter = async (contractName, powers, config) => { + const committeeName = `${contractName}Committee`; + const charterName = `${contractName}Charter`; + + const { + consume: { namesByAddress, zoe }, + produce: { [`${charterName}Kit`]: produceKit }, + installation: { + consume: { binaryVoteCounter: counterP, econCommitteeCharter: installP }, + }, + instance: { + produce: { [charterName]: instanceP }, + }, + } = powers; + const { + [committeeName]: { voterAddresses }, + } = config?.options || {}; + assert(voterAddresses, 'no voter addresses???'); + + const [charterInstall, counterInstall] = await Promise.all([ + installP, + counterP, + ]); + const terms = await allValues({ + binaryVoteCounterInstallation: counterInstall, + }); + + const startResult = await E(zoe).startInstance( + charterInstall, + undefined, + terms, + undefined, + 'econCommitteeCharter', + ); + instanceP.resolve(startResult.instance); + produceKit.resolve(startResult); + + await inviteToMyCharter( + startResult.creatorFacet, + namesByAddress, + voterAddresses, + ); + return startResult; +}; +harden(startMyCharter); + +/** + * @param {string} contractName + * @param {BootstrapPowers} powers + * @param {*} config + */ +export const startMyCommittee = async (contractName, powers, config) => { + const committeeName = `${contractName}Committee`; + const { + consume: { board, chainStorage, namesByAddress, startUpgradable }, + produce: { [`${committeeName}Kit`]: produceKit }, + installation: { + consume: { committee }, + }, + instance: { + produce: { [committeeName]: produceInstance }, + }, + } = powers; + const { + [committeeName]: { voterAddresses }, + } = config?.options || {}; + assert(voterAddresses, 'no voter addresses???'); + + const committeesNode = await E(chainStorage).makeChildNode(COMMITTEES_ROOT); + const storageNode = await E(committeesNode).makeChildNode( + sanitizePathSegment(committeeName), + ); + const marshaller = await E(board).getPublishingMarshaller(); + + const privateArgs = { + storageNode, + marshaller, + }; + + const started = await E(startUpgradable)({ + label: committeeName, + installation: await committee, + terms: { committeeName, committeeSize: values(voterAddresses).length }, + privateArgs, + }); + produceKit.resolve(started); + produceInstance.resolve(started.instance); + console.log(committeeName, 'started'); + + /** @param {[string, Promise][]} addrInvitations */ + const distributeInvitations = async addrInvitations => { + await Promise.all( + addrInvitations.map(async ([addr, invitationP]) => { + const debugName = `econ committee member ${addr}`; + const depositFacet = E(namesByAddress).lookup(addr, 'depositFacet'); + await E.when(invitationP, invitation => + E(depositFacet).receive(invitation), + ).catch(err => console.error(`failed deposit to ${debugName}`, err)); + }), + ); + }; + + /** @type {Promise[]} */ + const invitations = await E(started.creatorFacet).getVoterInvitations(); + + await distributeInvitations(zip(values(voterAddresses), invitations)); + return started; +}; diff --git a/contract/src/platform-goals/types.js b/contract/src/platform-goals/types.js new file mode 100644 index 00000000..293e5124 --- /dev/null +++ b/contract/src/platform-goals/types.js @@ -0,0 +1,4 @@ +import '@agoric/governance/src/types-ambient'; +import '@agoric/vats/src/core/types'; +import '@agoric/ertp/exported'; +import '@agoric/zoe/exported'; From 79eab0109c08a171ef379f181859cf06939ebf4a Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Fri, 8 Mar 2024 00:49:40 -0600 Subject: [PATCH 5/9] feat(swaparoo.proposal): start governed - avoid linking to other packages - start committee - test-deploy-tools: test local work-alikes against sdk --- contract/src/swaparoo.proposal.js | 163 +++++++++++++++++++++++++---- contract/test/test-deploy-tools.js | 53 ++++++++++ 2 files changed, 193 insertions(+), 23 deletions(-) create mode 100644 contract/test/test-deploy-tools.js diff --git a/contract/src/swaparoo.proposal.js b/contract/src/swaparoo.proposal.js index 43e4438c..da31aa5e 100644 --- a/contract/src/swaparoo.proposal.js +++ b/contract/src/swaparoo.proposal.js @@ -1,34 +1,65 @@ // @ts-check +import { E } from '@endo/far'; import { AmountMath, installContract, - startContract, } from './platform-goals/start-contract.js'; +import { + ParamTypes, + startMyCharter, + startMyCommittee, + startMyGovernedInstance, +} from './platform-goals/start-governed-contract.js'; +import { allValues } from './objectTools.js'; const { Fail } = assert; const contractName = 'swaparoo'; /** - * @typedef {{ + * @template SF + * @typedef {import('@agoric/zoe/src/zoeService/utils').StartResult} StartResult + */ + +/** + * @typedef {PromiseSpaceOf<{ + * swaparooKit: GovernanceFacetKit; + * swaparooCommitteeKit: StartResult<*>; + * swaparooCharterKit: StartResult<*>; + * }> & { * installation: PromiseSpaceOf<{ swaparoo: Installation }>; * instance: PromiseSpaceOf<{ swaparoo: Instance }>; * }} SwaparooSpace */ /** - * Core eval script to install contract - * + * @param {BootstrapPowers} powers + * @param {*} config + */ +export const startSwaparooCharter = (powers, config) => + startMyCharter(contractName, powers, config); + +/** + * @param {BootstrapPowers} powers + * @param {*} config + */ +export const startSwaparooCommittee = (powers, config) => + startMyCommittee(contractName, powers, config); + +/** * @param {BootstrapPowers} powers * @param {*} config */ export const installSwapContract = async (powers, config) => { const { - // must be supplied at runtime or replaced in template fashion - bundleID = Fail`missing bundleID`, - } = config.options?.[contractName] || {}; + // must be supplied by caller or template-replaced + bundleID = Fail`no bundleID`, + } = config?.options?.[contractName] ?? {}; - await installContract(powers, { name: contractName, bundleID }); + return installContract(powers, { + name: contractName, + bundleID, + }); }; /** @@ -37,46 +68,132 @@ export const installSwapContract = async (powers, config) => { * @param {BootstrapPowers} powers */ export const startSwapContract = async powers => { - console.error('startContract()...'); + console.error(contractName, 'startContract()...'); /** @type { BootstrapPowers & SwaparooSpace} */ // @ts-expect-error bootstrap powers evolve with BLD staker governance const swapPowers = powers; const { - consume: { namesByAddressAdmin: namesByAddressAdminP }, + consume: { + board, + chainTimerService, + namesByAddressAdmin: namesByAddressAdminP, + zoe, + [`${contractName}CommitteeKit`]: committeeKitP, + [`${contractName}CharterKit`]: charterKitP, + }, + produce: { [`${contractName}Kit`]: produceContractKit }, brand: { consume: { IST: istBrandP }, }, + installation: { + consume: { [contractName]: installationP, contractGovernor }, + }, + instance: { + produce: { [contractName]: produceInstance }, + }, } = swapPowers; + /** @type {import('./types').NonNullChainStorage['consume']} */ + // @ts-expect-error + const { chainStorage } = powers.consume; + const istBrand = await istBrandP; const oneIST = AmountMath.make(istBrand, 1n); const namesByAddressAdmin = await namesByAddressAdminP; - const terms = { feeAmount: oneIST, namesByAddressAdmin }; - return startContract(powers, { name: contractName, startArgs: { terms } }); -}; + const governedParams = { + Fee: { + type: ParamTypes.AMOUNT, + value: oneIST, + }, + }; + + // TODO: push more of the formulaic stuff down to startMyGovernedInstance + const marshaller = await E(board).getPublishingMarshaller(); + const storageNode = await E(chainStorage).makeChildNode(contractName); + const it = await startMyGovernedInstance( + { + zoe, + governedContractInstallation: installationP, + label: contractName, + terms: {}, + privateArgs: { + storageNode, + marshaller, + namesByAddressAdmin, + }, + }, + { + governedParams, + timer: chainTimerService, + contractGovernor, + governorTerms: {}, + committeeCreatorFacet: E.get(committeeKitP).creatorFacet, + }, + ); + produceContractKit.resolve(it); + await E(E.get(charterKitP).creatorFacet).addInstance( + it.instance, + it.governorCreatorFacet, + ); + + console.log('CoreEval script: started contract', contractName, it.instance); + + console.log('CoreEval script: share via agoricNames: none'); -export const main = async (powers, config = {}) => { - await installSwapContract(powers, config); - await startSwapContract(powers); + produceInstance.reset(); + produceInstance.resolve(it.instance); + + console.log(`${contractName} (re)started`); }; +export const main = (permittedPowers, config) => + allValues({ + installation: installSwapContract(permittedPowers, config), + committeeFacets: startSwaparooCommittee(permittedPowers, config), + contractFacets: startSwapContract(permittedPowers), + charterFacets: startSwaparooCharter(permittedPowers, config), + }); + /** @type { import("@agoric/vats/src/core/lib-boot").BootstrapManifestPermit } */ -export const permit = { +export const permit = harden({ consume: { - startUpgradable: true, + namesByAddress: true, namesByAddressAdmin: true, // to convert string addresses to depositFacets - zoe: true, // to install the contract + startUpgradable: true, + swaparooCharterKit: true, + + swaparooCommitteeKit: true, + board: true, // for to marshal governance parameter values + chainStorage: true, // to publish governance parameter values + chainTimerService: true, // to manage vote durations + zoe: true, // to start governed contract (TODO: use startUpgradable?) + }, + produce: { + swaparooKit: true, + swaparooCommitteeKit: true, + swaparooCharterKit: true, }, installation: { + consume: { + [contractName]: true, + contractGovernor: true, + committee: true, + binaryVoteCounter: true, + econCommitteeCharter: true, + }, produce: { [contractName]: true }, - consume: { [contractName]: true }, }, - instance: { produce: { [contractName]: true } }, + instance: { + produce: { + [contractName]: true, + [`${contractName}Charter`]: true, + [`${contractName}Committee`]: true, + }, + }, brand: { consume: { IST: true, // for use in contract terms }, }, -}; -harden(permit); +}); diff --git a/contract/test/test-deploy-tools.js b/contract/test/test-deploy-tools.js new file mode 100644 index 00000000..cb05e1ed --- /dev/null +++ b/contract/test/test-deploy-tools.js @@ -0,0 +1,53 @@ +// @ts-check + +/* eslint-disable import/order -- https://github.com/endojs/endo/issues/1235 */ +import { test } from './prepare-test-env-ava.js'; + +import * as agoricGovernance from '@agoric/governance'; +import * as charter from '@agoric/inter-protocol/src/econCommitteeCharter.js'; +import { DisplayInfoShape } from '@agoric/ertp/src/typeGuards.js'; +import * as agoricERTP from '@agoric/ertp'; +import { mustMatch, keyEQ } from '@endo/patterns'; +import { makeMarshal } from '@endo/marshal'; +import { + ParamTypes, + CONTRACT_ELECTORATE, + INVITATION_MAKERS_DESC, +} from '../src/platform-goals/start-governed-contract.js'; +import { marshalData } from '../src/platform-goals/board-aux.core.js'; +import { AmountMath } from '../src/platform-goals/start-contract.js'; + +test('local copy of ParamTypes matches @agoric/governance', async t => { + t.deepEqual(ParamTypes, agoricGovernance.ParamTypes); +}); + +test('local copy of ELECTORATE matches @agoric/governance', async t => { + t.deepEqual(CONTRACT_ELECTORATE, agoricGovernance.CONTRACT_ELECTORATE); +}); + +test('local copy of INVITATION_MAKERS_DESC matches @agoric/iner-protocol', async t => { + t.deepEqual(INVITATION_MAKERS_DESC, charter.INVITATION_MAKERS_DESC); +}); + +test('boardAux marshal short-cut matches @endo/marshal', async t => { + const displayInfo = harden({ + assetKind: 'nat', + decimalPlaces: 6, + }); + mustMatch(displayInfo, DisplayInfoShape); + const m0 = makeMarshal(undefined, undefined, { + serializeBodyFormat: 'smallcaps', + }); + const actual = marshalData.toCapData(displayInfo); + const expected = m0.toCapData(displayInfo); + t.deepEqual(actual, expected); + t.true(keyEQ(actual, expected)); +}); + +test('AmountMath.make work-alike matches @agoric/ertp', async t => { + const kit1 = agoricERTP.makeIssuerKit('KW1'); + const actual = AmountMath.make(kit1.brand, 123n); + const expected = agoricERTP.AmountMath.make(kit1.brand, 123n); + t.deepEqual(actual, expected); + t.true(keyEQ(actual, expected)); +}); From eb054461e378b26fc3b2cf76f4e3b3a2fd0b605c Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Mon, 11 Mar 2024 23:38:19 -0500 Subject: [PATCH 6/9] fix: patch @agoric/governance for testing - harden - supply puppet electionManager --- patches/@agoric+governance+0.10.4-u14.0.patch | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 patches/@agoric+governance+0.10.4-u14.0.patch diff --git a/patches/@agoric+governance+0.10.4-u14.0.patch b/patches/@agoric+governance+0.10.4-u14.0.patch new file mode 100644 index 00000000..250ef5a7 --- /dev/null +++ b/patches/@agoric+governance+0.10.4-u14.0.patch @@ -0,0 +1,35 @@ +diff --git a/node_modules/@agoric/governance/src/contractGovernance/governParam.js b/node_modules/@agoric/governance/src/contractGovernance/governParam.js +index c27c6d8..95af1b5 100644 +--- a/node_modules/@agoric/governance/src/contractGovernance/governParam.js ++++ b/node_modules/@agoric/governance/src/contractGovernance/governParam.js +@@ -133,11 +133,11 @@ const setupParamGovernance = ( + return negative; + }); + +- return { ++ return harden({ + outcomeOfUpdate, + instance: voteCounter, + details: E(counterPublicFacet).getDetails(), +- }; ++ }); + }; + + return Far('paramGovernor', { +diff --git a/node_modules/@agoric/governance/tools/puppetContractGovernor.js b/node_modules/@agoric/governance/tools/puppetContractGovernor.js +index 65fdda0..650f702 100644 +--- a/node_modules/@agoric/governance/tools/puppetContractGovernor.js ++++ b/node_modules/@agoric/governance/tools/puppetContractGovernor.js +@@ -38,8 +38,10 @@ export const start = async (zcf, privateArgs) => { + }, + } = zcf.getTerms(); + +- // in the fake there's no electionManager to augment the terms +- const augmentedTerms = contractTerms; ++ const augmentedTerms = { ++ ...contractTerms, ++ electionManager: zcf.getInstance(), ++ }; + + const { + creatorFacet: governedCF, From ccd224d458372ee36316c5673da68c78d25daeaf Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 7 Mar 2024 17:31:31 -0600 Subject: [PATCH 7/9] test: swap fee governance - puppet-gov: test support - test-swap-wallet: restore to working order with gov - test-vote-fee-change: start governed; change Fee - test-vote-by-committee: vote by committee members with snapshots of offer details - initialize chainTimerService to modern time --- contract/test/boot-tools.js | 6 +- contract/test/lib-gov-test/puppet-gov.js | 76 ++++++ contract/test/test-swap-wallet.js | 100 ++++++-- contract/test/test-vote-by-committee.js | 284 +++++++++++++++++++++++ contract/test/test-vote-fee-change.js | 136 +++++++++++ 5 files changed, 576 insertions(+), 26 deletions(-) create mode 100644 contract/test/lib-gov-test/puppet-gov.js create mode 100644 contract/test/test-vote-by-committee.js create mode 100644 contract/test/test-vote-fee-change.js diff --git a/contract/test/boot-tools.js b/contract/test/boot-tools.js index b077fc1a..ed8973b5 100644 --- a/contract/test/boot-tools.js +++ b/contract/test/boot-tools.js @@ -51,7 +51,11 @@ export const mockBootstrapPowers = async ( const { nameHub: namesByAddress, nameAdmin: namesByAddressAdmin } = makeNameHubKit(); - const chainTimerService = buildManualTimer(); + const noop = () => {}; + const modernTime = BigInt(new Date(2024, 6, 1, 9).valueOf() / 1000); + const chainTimerService = buildManualTimer(noop, modernTime, { + timeStep: 60n, + }); const timerBrand = await E(chainTimerService).getTimerBrand(); const chainStorage = makeMockChainStorageRoot(); diff --git a/contract/test/lib-gov-test/puppet-gov.js b/contract/test/lib-gov-test/puppet-gov.js new file mode 100644 index 00000000..5cd6b091 --- /dev/null +++ b/contract/test/lib-gov-test/puppet-gov.js @@ -0,0 +1,76 @@ +// @ts-check +// borrowed from https://github.com/Agoric/agoric-sdk/blob/master/packages/inter-protocol/test/supports.js +import { E, Far } from '@endo/far'; +import { createRequire } from 'node:module'; + +const nodeRequire = createRequire(import.meta.url); +export const assets = { + binaryVoteCounter: nodeRequire.resolve( + '@agoric/governance/src/binaryVoteCounter.js', + ), + committee: nodeRequire.resolve('@agoric/governance/src/committee.js'), + contractGovernor: nodeRequire.resolve( + '@agoric/governance/src/contractGovernor.js', + ), + committeeCharter: nodeRequire.resolve( + '@agoric/inter-protocol/src/econCommitteeCharter.js', + ), + puppetContractGovernor: nodeRequire.resolve( + '@agoric/governance/tools/puppetContractGovernor.js', + ), + invitationMakerContract: nodeRequire.resolve( + '@agoric/zoe/src/contracts/automaticRefund.js', + ), +}; + +export const installCommitteeContract = async (zoe, produce, bundleCache) => { + const committeeBundle = await bundleCache.load(assets.committee, 'committee'); + + produce.committee.resolve(E(zoe).install(committeeBundle)); +}; + +/** + * Install governance contracts, with a "puppet" governor for use in tests. + * + * @param {ERef} zoe + * @param {BootstrapPowers['installation']['produce']} produce + * @param {BundleCache} bundleCache + * + * @typedef {Awaited>} BundleCache + */ +export const installPuppetGovernance = async (zoe, produce, bundleCache) => { + await installCommitteeContract(zoe, produce, bundleCache); + produce.contractGovernor.resolve( + E(zoe).install(await bundleCache.load(assets.puppetContractGovernor)), + ); + // ignored by puppetContractGovernor but expected by something + produce.binaryVoteCounter.resolve( + E(zoe).install(await bundleCache.load(assets.binaryVoteCounter)), + ); +}; +// a source of arbitrary invitations +export const mockElectorate = async (zoe, bundleCache) => { + const installation = await E(zoe).install( + await bundleCache.load(assets.invitationMakerContract), + ); + const arbInstance = await E(zoe).startInstance(installation); + const committeeCreatorFacet = Far('Electorate CF', { + getPoserInvitation: async () => E(arbInstance.publicFacet).makeInvitation(), + }); + return { creatorFacet: committeeCreatorFacet }; +}; + +export const installGovContracts = async (t, powers, bundleCache) => { + const { zoe } = powers.consume; + for await (const [name, asset] of Object.entries({ + contractGovernor: assets.contractGovernor, + binaryVoteCounter: assets.binaryVoteCounter, + committee: assets.committee, + econCommitteeCharter: assets.committeeCharter, + })) { + t.log('installation:', name); + powers.installation.produce[name].resolve( + E(zoe).install(await bundleCache.load(asset)), + ); + } +}; diff --git a/contract/test/test-swap-wallet.js b/contract/test/test-swap-wallet.js index 0d4bb9ee..675c84d4 100644 --- a/contract/test/test-swap-wallet.js +++ b/contract/test/test-swap-wallet.js @@ -3,21 +3,27 @@ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { createRequire } from 'node:module'; import { E } from '@endo/far'; import { AmountMath } from '@agoric/ertp'; +import { extractPowers } from '@agoric/vats/src/core/utils.js'; -import { extract } from '@agoric/vats/src/core/utils.js'; import { mockBootstrapPowers } from './boot-tools.js'; import { installSwapContract, permit, startSwapContract, + startSwaparooCharter, } from '../src/swaparoo.proposal.js'; import { makeStableFaucet } from './mintStable.js'; import { mockWalletFactory, seatLike } from './wallet-tools.js'; import { getBundleId, makeBundleCacheContext } from '../tools/bundle-tools.js'; +import { + installPuppetGovernance, + mockElectorate, + assets as govAssets, +} from './lib-gov-test/puppet-gov.js'; /** @typedef {import('./wallet-tools.js').MockWallet} MockWallet */ -/** @type {import('ava').TestFn>>} */ +/** @type {import('ava').TestFn>>} */ const test = anyTest; const nodeRequire = createRequire(import.meta.url); @@ -27,30 +33,77 @@ const assets = { [contractName]: nodeRequire.resolve(`../src/${contractName}.contract.js`), }; -test.before(async t => (t.context = await makeBundleCacheContext(t))); - -test.serial('bootstrap and start contract', async t => { +const makeTestContext = async t => { + const bc = await makeBundleCacheContext(t); t.log('bootstrap'); const { powers, vatAdminState } = await mockBootstrapPowers(t.log); - const { bundleCache } = t.context; + const { zoe } = powers.consume; + for await (const [name, asset] of Object.entries({ + econCommitteeCharter: govAssets.committeeCharter, + })) { + powers.installation.produce[name].resolve( + E(zoe).install(await bc.bundleCache.load(asset)), + ); + } + + return { ...bc, powers, vatAdminState }; +}; + +test.before(async t => (t.context = await makeTestContext(t))); + +test.serial('install bundle; make zoe Installation', async t => { + const { bundleCache, powers, vatAdminState } = t.context; + const bundle = await bundleCache.load(assets.swaparoo, contractName); const bundleID = getBundleId(bundle); t.log('publish bundle', bundleID.slice(0, 8)); vatAdminState.installBundle(bundleID, bundle); - t.log('install contract'); const config = { options: { [contractName]: { bundleID } } }; - const swapPowers = extract(permit, powers); - await installSwapContract(swapPowers, config); // `agoric run` style proposal does this for us - t.log('start contract'); - await startSwapContract(swapPowers); + const installation = await installSwapContract(powers, config); + t.log(installation); + t.is(typeof installation, 'object'); +}); + +test.serial('install puppet governor; mock getPoserInvitation', async t => { + const { bundleCache, powers } = t.context; + const { zoe } = powers.consume; + await installPuppetGovernance(zoe, powers.installation.produce, bundleCache); + + powers.produce[`${contractName}CommitteeKit`].resolve( + mockElectorate(zoe, bundleCache), + ); + + const invitation = await E( + E.get(powers.consume[`${contractName}CommitteeKit`]).creatorFacet, + ).getPoserInvitation(); + t.log(invitation); + t.is(typeof invitation, 'object'); +}); + +test.serial('start contract', async t => { + t.log('install, start contract'); + const { powers } = t.context; + t.log('start contract, checking permit'); + const permittedPowers = extractPowers(permit, powers); + + const config = { + options: { + [`${contractName}Committee`]: { + voterAddresses: {}, + }, + }, + }; + + await Promise.all([ + startSwaparooCharter(permittedPowers, config), + startSwapContract(permittedPowers), + ]); const instance = await powers.instance.consume[contractName]; t.log(instance); t.is(typeof instance, 'object'); - - Object.assign(t.context.shared, { powers }); }); /** @@ -73,9 +126,8 @@ const startAlice = async ( ) => { const instance = wellKnown.instance[contractName]; - // Let's presume the terms are in vstorage somewhere... say... boardAux - const terms = wellKnown.terms.get(instance); - const { feeAmount } = terms; + // Governed terms are in vstorage + const feeAmount = await wellKnown.getGovernedParam(instance, 'Fee'); const proposal = { give: { MagicBeans: beansAmount, Fee: feeAmount }, @@ -120,8 +172,7 @@ const startJack = async ( jackPays = false, ) => { const instance = wellKnown.instance[contractName]; - const terms = wellKnown.terms.get(instance); - const { feeAmount } = terms; + const feeAmount = await wellKnown.getGovernedParam(instance, 'Fee'); const proposal = { want: { MagicBeans: beansAmount }, @@ -153,15 +204,10 @@ test.serial('basic swap', async t => { jack: 'agoric1jack', }; - const { - shared: { powers }, - bundleCache, - } = t.context; + const { powers, bundleCache } = t.context; const { zoe, feeMintAccess, bldIssuerKit } = powers.consume; const instance = await powers.instance.consume[contractName]; - // TODO: we presume terms are available... perhaps in boardAux - const terms = await E(zoe).getTerms(instance); // A higher fidelity test would get these from vstorage const wellKnown = { @@ -177,7 +223,11 @@ test.serial('basic swap', async t => { instance: { [contractName]: instance, }, - terms: new Map([[instance, terms]]), + getGovernedParam: async (i, n) => { + const pf = await E(zoe).getPublicFacet(i); + const params = await E(pf).getGovernedParams(); + return params[n].value; + }, }; const beans = x => AmountMath.make(wellKnown.brand.IST, x); diff --git a/contract/test/test-vote-by-committee.js b/contract/test/test-vote-by-committee.js new file mode 100644 index 00000000..48f36efe --- /dev/null +++ b/contract/test/test-vote-by-committee.js @@ -0,0 +1,284 @@ +// @ts-check +import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { createRequire } from 'node:module'; +import { E } from '@endo/far'; + +import { extractPowers } from '@agoric/vats/src/core/utils.js'; +import { AmountMath } from '@agoric/ertp/src/amountMath.js'; + +import { main, permit } from '../src/swaparoo.proposal.js'; + +import { mockBootstrapPowers } from './boot-tools.js'; +import { makeBundleCacheContext } from '../tools/bundle-tools.js'; +import { NonNullish } from '../src/objectTools.js'; +import { mockWalletFactory, seatLike } from './wallet-tools.js'; +import { INVITATION_MAKERS_DESC } from '../src/platform-goals/start-governed-contract.js'; +import { installGovContracts } from './lib-gov-test/puppet-gov.js'; + +/** @typedef {import('./wallet-tools.js').MockWallet} MockWallet */ + +/** @type {import('ava').TestFn>>} */ +const test = anyTest; + +const nodeRequire = createRequire(import.meta.url); + +const contractName = 'swaparoo'; +export const assets = { + [contractName]: nodeRequire.resolve(`../src/${contractName}.contract.js`), +}; + +const makeTestContext = async t => { + const bc = await makeBundleCacheContext(t); + t.log('bootstrap'); + const { powers, vatAdminState } = await mockBootstrapPowers(t.log); + + await installGovContracts(t, powers, bc.bundleCache); + + return { ...bc, powers, vatAdminState }; +}; + +test.before(async t => (t.context = await makeTestContext(t))); + +/** + * @param {import('ava').ExecutionContext} t + * @param {MockWallet} wallet + * @param {{ instance: BootstrapPowers['instance']['consume']}} wellKnown + */ +const makeVoter = (t, wallet, wellKnown) => { + let charterAcceptOfferId; + let committeeOfferId; + + const doOffer = async offer => { + t.snapshot(offer, `voter offer: ${offer.id}`); + const updates = wallet.offers.executeOffer(offer); + const seat = seatLike(updates); + const result = await seat.getOfferResult(); + await seatLike(updates).getPayoutAmounts(); + return result; + }; + + const acceptInvitation = async (offerId, { instance, description }) => { + /** @type {import('./wallet-tools.js').OfferSpec} */ + const offer = { + id: offerId, + invitationSpec: { + source: 'purse', + description, + instance, + }, + proposal: {}, + }; + const result = await doOffer(offer); + charterAcceptOfferId = offerId; + return result; + }; + + const acceptCharterInvitation = async offerId => { + const instance = await wellKnown.instance[`${contractName}Charter`]; + const description = INVITATION_MAKERS_DESC; + const result = await acceptInvitation(offerId, { instance, description }); + charterAcceptOfferId = offerId; + return result; + }; + + const acceptCommitteeInvitation = async (offerId, index) => { + const instance = await wellKnown.instance[`${contractName}Committee`]; + const description = `Voter${index}`; + const result = await acceptInvitation(offerId, { instance, description }); + committeeOfferId = offerId; + return result; + }; + + const putQuestion = async (offerId, params, deadline) => { + const instance = await wellKnown.instance[contractName]; + const path = { paramPath: { key: 'governedParams' } }; + + /** @type {import('@agoric/inter-protocol/src/econCommitteeCharter.js').ParamChangesOfferArgs} */ + const offerArgs = harden({ deadline, params, instance, path }); + + /** @type {import('@agoric/smart-wallet/src/offers.js').OfferSpec} */ + const offer = { + id: offerId, + invitationSpec: { + source: 'continuing', + previousOffer: NonNullish(charterAcceptOfferId), + invitationMakerName: 'VoteOnParamChange', + }, + offerArgs, + proposal: {}, + }; + return doOffer(offer); + }; + + /** + * @param {string | number} offerId + * @param {QuestionDetails} details - TODO: get from vstorage + * @param {number} position + */ + const vote = async (offerId, details, position) => { + const chosenPositions = [details.positions[position]]; + + /** @type {import('./wallet-tools.js').OfferSpec} */ + const offer = { + id: offerId, + invitationSpec: { + source: 'continuing', + previousOffer: NonNullish(committeeOfferId), + invitationMakerName: 'makeVoteInvitation', + invitationArgs: harden([chosenPositions, details.questionHandle]), + }, + proposal: {}, + }; + return doOffer(offer); + }; + + return harden({ + acceptCharterInvitation, + acceptCommitteeInvitation, + putQuestion, + vote, + }); +}; + +const voterAddresses = { + mem1: 'agoric18jr9nlvp300feu726y3v4n07ykfjwup3twnlyn', +}; + +test.serial('provision Voter1 account', async t => { + const { powers } = t.context; + const { zoe, namesByAddressAdmin } = powers.consume; + + await null; + const walletFactory = mockWalletFactory( + { zoe, namesByAddressAdmin }, + { Invitation: await powers.issuer.consume.Invitation }, + ); + + const victor = makeVoter( + t, + await walletFactory.makeSmartWallet(voterAddresses.mem1), + { instance: powers.instance.consume }, + ); + t.pass(); + + Object.assign(t.context.shared, { victor }); +}); + +test.serial('install bundle', async t => { + const { bundleCache, vatAdminState } = t.context; + const bundle = await bundleCache.load(assets.swaparoo, contractName); + const bundleID = `b1-${bundle.endoZipBase64Sha512}`; + t.log('publish bundle', bundleID.slice(0, 8)); + vatAdminState.installBundle(bundleID, bundle); + Object.assign(t.context.shared, { bundleID }); + t.pass(); +}); + +test.serial('core eval: start swap committee, charter, contract', async t => { + const { powers, shared } = t.context; + + const permittedPowers = extractPowers(permit, powers); + const { bundleID } = shared; + const config = { + options: { + [contractName]: { bundleID }, + [`${contractName}Committee`]: { + voterAddresses, + }, + }, + }; + + t.log('run core eval', config); + + const { installation, committeeFacets } = await main(permittedPowers, config); + + const kit = await powers.consume[`${contractName}Kit`]; + t.log(`${contractName}Kit`, 'facets', Object.keys(kit)); + t.is(typeof kit.governorCreatorFacet, 'object'); + t.is(typeof committeeFacets.instance, 'object'); + t.is(typeof installation, 'object'); + t.is(typeof (await powers.instance.consume[contractName]), 'object'); + t.is( + typeof (await powers.instance.consume[`${contractName}Committee`]), + 'object', + ); + t.is( + typeof (await powers.instance.consume[`${contractName}Charter`]), + 'object', + ); +}); + +test.serial('Voter0 accepts charter, committee invitations', async t => { + /** @type {ReturnType} */ + const victor = t.context.shared.victor; + await victor.acceptCommitteeInvitation('v0-join-committee', 0); + await victor.acceptCharterInvitation('v0-accept-charter'); + t.pass(); +}); + +/** @param {Promise} brandP */ +const makeAmountMaker = async brandP => { + const brand = await brandP; + const { decimalPlaces } = await E(brand).getDisplayInfo(); + const unit = BigInt(10 ** NonNullish(decimalPlaces)); + /** + * @param {bigint} num + * @param {bigint} [denom] + */ + return (num, denom = 1n) => AmountMath.make(brand, (num * unit) / denom); +}; + +test.serial('vote to change swap fee', async t => { + const { powers } = t.context; + const IST = await makeAmountMaker(powers.brand.consume.IST); + const targetFee = IST(50n, 100n); + const changes = { Fee: targetFee }; + + const { zoe } = powers.consume; + + /** @type {ReturnType} */ + const victor = t.context.shared.victor; + + /** @type {BootstrapPowers & import('../src/swaparoo.proposal.js').SwaparooSpace */ + // @ts-expect-error cast + const swapPowers = powers; + const swapPub = E(zoe).getPublicFacet( + swapPowers.instance.consume[contractName], + ); + const cmtePub = E(zoe).getPublicFacet( + swapPowers.instance.consume[`${contractName}Committee`], + ); + + const before = await E(swapPub).getAmount('Fee'); + t.deepEqual(before, IST(1n, 1_000_000n)); + + const deadline = BigInt(new Date(2024, 6, 1, 9, 10).valueOf() / 1000); + const result = await victor.putQuestion('proposeToSetFee', changes, deadline); + t.log('question is posed', result); + + // TODO: .latestQuestion from vstorage + const qSub = await E(cmtePub).getQuestionSubscriber(); + const { value: details } = await E(qSub).getUpdateSince(); + t.is(details.electionType, 'param_change'); + const voteResult = await victor.vote('voteToSetFee', details, 0); + t.log('victor voted:', voteResult); + + const timer = details.closingRule.timer; + await E(timer).tickN(11); + + // TODO: .latestOutcome from vstorage + const counterHandle = await E( + E(cmtePub).getQuestion(details.questionHandle), + ).getVoteCounter(); + const counterPub = await E(zoe).getPublicFacet(counterHandle); + const outcome = await E(counterPub).getOutcome(); + t.deepEqual(outcome, { changes }); + t.log('question carried'); + + const after = await E(swapPub).getAmount('Fee'); + t.deepEqual(after, targetFee); +}); + +test.todo('wallet-based voting'); +test.todo('swap after changing fee'); +test.todo('e2e swap after changing fee with voters'); diff --git a/contract/test/test-vote-fee-change.js b/contract/test/test-vote-fee-change.js new file mode 100644 index 00000000..3554c838 --- /dev/null +++ b/contract/test/test-vote-fee-change.js @@ -0,0 +1,136 @@ +// @ts-check +import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { createRequire } from 'node:module'; +import { E } from '@endo/far'; + +import { extractPowers } from '@agoric/vats/src/core/utils.js'; +import { AmountMath } from '@agoric/ertp/src/amountMath.js'; +import { mockBootstrapPowers } from './boot-tools.js'; +import { + installSwapContract, + permit, + startSwapContract, + startSwaparooCharter, +} from '../src/swaparoo.proposal.js'; +import { makeBundleCacheContext } from '../tools/bundle-tools.js'; +import { + installPuppetGovernance, + mockElectorate, + assets as govAssets, +} from './lib-gov-test/puppet-gov.js'; +import { NonNullish } from '../src/objectTools.js'; + +/** @typedef {import('./wallet-tools.js').MockWallet} MockWallet */ + +/** @type {import('ava').TestFn>>} */ +const test = anyTest; + +const nodeRequire = createRequire(import.meta.url); + +const contractName = 'swaparoo'; +export const assets = { + [contractName]: nodeRequire.resolve(`../src/${contractName}.contract.js`), +}; + +const makeTestContext = async t => { + const bc = await makeBundleCacheContext(t); + t.log('bootstrap'); + const { powers, vatAdminState } = await mockBootstrapPowers(t.log); + + const { zoe } = powers.consume; + for await (const [name, asset] of Object.entries({ + binaryVoteCounter: govAssets.binaryVoteCounter, + econCommitteeCharter: govAssets.committeeCharter, + })) { + t.log('installation:', name); + powers.installation.produce[name].resolve( + E(zoe).install(await bc.bundleCache.load(asset)), + ); + } + + return { ...bc, powers, vatAdminState }; +}; + +test.before(async t => (t.context = await makeTestContext(t))); + +test.serial('install puppet governor; mock getPoserInvitation', async t => { + const { bundleCache, powers } = t.context; + const { zoe } = powers.consume; + await installPuppetGovernance(zoe, powers.installation.produce, bundleCache); + + powers.produce[`${contractName}CommitteeKit`].resolve( + mockElectorate(zoe, bundleCache), + ); + + const invitation = await E( + E.get(powers.consume[`${contractName}CommitteeKit`]).creatorFacet, + ).getPoserInvitation(); + t.log(invitation); + t.is(typeof invitation, 'object'); +}); + +test.serial('install bundle; make zoe Installation', async t => { + const { bundleCache, powers, vatAdminState } = t.context; + + const bundle = await bundleCache.load(assets.swaparoo, contractName); + const bundleID = `b1-${bundle.endoZipBase64Sha512}`; + t.log('publish bundle', bundleID.slice(0, 8)); + vatAdminState.installBundle(bundleID, bundle); + t.log('install contract'); + const config = { options: { [contractName]: { bundleID } } }; + const installation = await installSwapContract(powers, config); + t.log(installation); + t.is(typeof installation, 'object'); +}); + +test.serial('start governed swap contract', async t => { + const { powers } = t.context; + + t.log('start contract, checking permit'); + const permittedPowers = extractPowers(permit, powers); + const config = { + options: { [`${contractName}Committee`]: { voterAddresses: {} } }, + }; + await startSwaparooCharter(powers, config); + await startSwapContract(permittedPowers); + + const instance = await powers.instance.consume[contractName]; + t.log(instance); + t.is(typeof instance, 'object'); + + const puppetGovernors = { + [contractName]: E.get(powers.consume[`${contractName}Kit`]) + .governorCreatorFacet, + }; + + Object.assign(t.context.shared, { powers, puppetGovernors }); +}); + +test.serial('vote to change swap fee', async t => { + const { powers, shared } = t.context; + const { puppetGovernors } = shared; + const { zoe } = powers.consume; + + const istBrand = await powers.brand.consume.IST; + const { decimalPlaces } = await E(istBrand).getDisplayInfo(); + + const instance = await powers.instance.consume[contractName]; + const swapPub = E(zoe).getPublicFacet(instance); + + const before = await E(swapPub).getAmount('Fee'); + t.deepEqual(before, AmountMath.make(istBrand, 1n)); + + const UNIT = 10n ** BigInt(NonNullish(decimalPlaces)); + const CENT = UNIT / 100n; + const targetFee = AmountMath.make(istBrand, 50n * CENT); + const changes = { Fee: targetFee }; + t.log('changeParams', changes); + const swapGov = NonNullish(puppetGovernors[contractName]); + await E(swapGov).changeParams(harden({ changes })); + const after = await E(swapPub).getAmount('Fee'); + t.deepEqual(after, targetFee); +}); + +test.todo('wallet-based voting'); +test.todo('swap after changing fee'); +test.todo('e2e swap after changing fee with voters'); From 441d062e06db0bbbfb33dd78cb0c1e4389578e4e Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 12 Mar 2024 00:25:24 -0500 Subject: [PATCH 8/9] test: vote-by-committee generated snapshots --- .../snapshots/test-vote-by-committee.js.md | 83 ++++++++++++++++++ .../snapshots/test-vote-by-committee.js.snap | Bin 0 -> 956 bytes 2 files changed, 83 insertions(+) create mode 100644 contract/test/snapshots/test-vote-by-committee.js.md create mode 100644 contract/test/snapshots/test-vote-by-committee.js.snap diff --git a/contract/test/snapshots/test-vote-by-committee.js.md b/contract/test/snapshots/test-vote-by-committee.js.md new file mode 100644 index 00000000..0632d9ca --- /dev/null +++ b/contract/test/snapshots/test-vote-by-committee.js.md @@ -0,0 +1,83 @@ +# Snapshot report for `test/test-vote-by-committee.js` + +The actual snapshot is saved in `test-vote-by-committee.js.snap`. + +Generated by [AVA](https://avajs.dev). + +## provision Voter1 account + +> voter offer: v0-join-committee + + { + id: 'v0-join-committee', + invitationSpec: { + description: 'Voter0', + instance: Object @Alleged: InstanceHandle {}, + source: 'purse', + }, + proposal: {}, + } + +> voter offer: v0-accept-charter + + { + id: 'v0-accept-charter', + invitationSpec: { + description: 'charter member invitation', + instance: Object @Alleged: InstanceHandle {}, + source: 'purse', + }, + proposal: {}, + } + +> voter offer: proposeToSetFee + + { + id: 'proposeToSetFee', + invitationSpec: { + invitationMakerName: 'VoteOnParamChange', + previousOffer: 'v0-accept-charter', + source: 'continuing', + }, + offerArgs: { + deadline: 2n, + instance: Object @Alleged: InstanceHandle {}, + params: { + Fee: { + brand: Object @Alleged: ZDEFAULT brand {}, + value: 500000n, + }, + }, + path: { + paramPath: { + key: 'governedParams', + }, + }, + }, + proposal: {}, + } + +> voter offer: voteToSetFee + + { + id: 'voteToSetFee', + invitationSpec: { + invitationArgs: [ + [ + { + changes: { + Fee: { + brand: Object @Alleged: ZDEFAULT brand {}, + value: 500000n, + }, + }, + }, + ], + Object @Alleged: QuestionHandle {}, + ], + invitationMakerName: 'makeVoteInvitation', + previousOffer: 'v0-join-committee', + source: 'continuing', + }, + proposal: {}, + } diff --git a/contract/test/snapshots/test-vote-by-committee.js.snap b/contract/test/snapshots/test-vote-by-committee.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..d8956b5e55ee1b9ae16699c744a430c00f46776a GIT binary patch literal 956 zcmV;t14H~lRzVhxSC^N%PE<4`Y$1P-NP4!?tr#0=s! z)f;B2LxanvW(+E3xvaS^AspI<&;~5(P1&XyRn5gNz?`#)!t$?tc^OV&nGV(&v`>)v z3b7Ay+z}Ll#s{Iv!E7QHc(+O%#nvo}#d{{P(hV_Ki^vB{GaMHi3JI+tIcCLHi1$;p zDz-x+ScDAPnbUPrBq?N#~_v=Za_SU zXEf2C(R7UtGH=ciccL!ECoz0}^yRx4_?%sISZY6NNA*$2j6+<4cpOJH5;tMWJBSp| zaR(sAcrH@l4&9N8?T-HD!A|29Vo&38gVPKZ_>7UqHZC78VWSxCfnLidRn4q8GxYSG zs2d6sY4rI1$<}j-WLfw=rzzTZRl4p_vk7>jt>&hkZS z9$#3N<7OeQ|D5edg`Hp?FOOI-h)m*1;acad_NI*Uyi7t*>-0t#G%La zNj}9^hD{dudQ0)tU#`YjKtsTHeWsICn2kDYm8Xz+7G6bav&tqPb_l3fFI6 Date: Thu, 7 Mar 2024 17:30:36 -0600 Subject: [PATCH 9/9] build: lock packages with governance, zone --- yarn.lock | 111 +++++++++++++++++++++++------------------------------- 1 file changed, 48 insertions(+), 63 deletions(-) diff --git a/yarn.lock b/yarn.lock index 113d1d32..0a62b6fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -156,32 +156,10 @@ resolved "https://registry.yarnpkg.com/@agoric/eventual-send/-/eventual-send-0.14.1.tgz#b414888bed67cf003a61bd22da30a70f79b8f9dc" integrity sha512-E1gr1LhkN/imwiXIdjTQZ7pOao82avOIDsoSgjPwoFU1jrysRE8UIUGBogBn7GGFUEPogo16rrsfKzb35ZayOw== -"@agoric/governance@^0.10.3": - version "0.10.3" - resolved "https://registry.yarnpkg.com/@agoric/governance/-/governance-0.10.3.tgz#d930b3da8c8e6362ddcbd8c7ac2ec3f090c57f1d" - integrity sha512-DmaZAJ7u5Tpsh3GscqLkxkZfVl5qg6fD905gdKx/b5xahzF65KIcOz48qUVFqZm/l4N4n1TzIazeAApsLpD7Pw== - dependencies: - "@agoric/assert" "^0.6.0" - "@agoric/ertp" "^0.16.2" - "@agoric/internal" "^0.3.2" - "@agoric/notifier" "^0.6.2" - "@agoric/store" "^0.9.2" - "@agoric/swingset-vat" "^0.32.2" - "@agoric/time" "^0.3.2" - "@agoric/vat-data" "^0.5.2" - "@agoric/vats" "^0.15.1" - "@agoric/zoe" "^0.26.2" - "@endo/captp" "^3.1.1" - "@endo/eventual-send" "^0.17.2" - "@endo/far" "^0.2.18" - "@endo/marshal" "^0.8.5" - "@endo/nat" "^4.1.27" - "@endo/promise-kit" "^0.2.56" - -"@agoric/governance@^0.10.4-u14.0", "@agoric/governance@^0.10.4-u14.1": - version "0.10.4-u14.1" - resolved "https://registry.yarnpkg.com/@agoric/governance/-/governance-0.10.4-u14.1.tgz#b98877a7622a62a66cd87fc0196c3eaae4b294c7" - integrity sha512-OJX1Sj7wCiyPYkDQuYil3NbmgH0TkP3XypcajwKFzS0F3nqR8TRYBM2jtxNWDenSLGvxXIuE65Zp04YVMEv+Bw== +"@agoric/governance@0.10.4-u14.0", "@agoric/governance@^0.10.3", "@agoric/governance@^0.10.4-u14.0", "@agoric/governance@^0.10.4-u14.1": + version "0.10.4-u14.0" + resolved "https://registry.yarnpkg.com/@agoric/governance/-/governance-0.10.4-u14.0.tgz#251ee1bb934610bcb689958fba9020b0ad1ba594" + integrity sha512-PRXatFskI4rGLc/iWBurYY7iXnwzqtKUDEL+VXuyf5roJrBCh0FohottbvJRBffIk9z3jfb/Nq2DcpBAwaOfBA== dependencies: "@agoric/assert" "^0.6.1-u11wf.0" "@agoric/ertp" "^0.16.3-u14.0" @@ -191,7 +169,7 @@ "@agoric/swingset-vat" "^0.32.3-u14.0" "@agoric/time" "^0.3.3-u14.0" "@agoric/vat-data" "^0.5.3-u14.0" - "@agoric/vats" "^0.15.2-u14.1" + "@agoric/vats" "^0.15.2-u14.0" "@agoric/zoe" "^0.26.3-u14.0" "@endo/captp" "3.1.1" "@endo/eventual-send" "0.17.2" @@ -205,6 +183,28 @@ resolved "https://registry.yarnpkg.com/@agoric/import-manager/-/import-manager-0.3.12-u14.0.tgz#6d3368e3a373692d1e632c71d7228f2260089d4e" integrity sha512-x/YeuweYncYLVUkDaydbaejqLSzGP1+yaeeKPsz4ioHIp+0T4ARNAzJInvUGyomlwUSXZlE5VQdr3ODZ8gc65w== +"@agoric/inter-protocol@0.16.2-u14.1", "@agoric/inter-protocol@^0.16.2-u12.0", "@agoric/inter-protocol@^0.16.2-u14.0", "@agoric/inter-protocol@^0.16.2-u14.1": + version "0.16.2-u14.1" + resolved "https://registry.yarnpkg.com/@agoric/inter-protocol/-/inter-protocol-0.16.2-u14.1.tgz#30b55cf6ff79e1025eb9be3ed649904b42047800" + integrity sha512-eSZnzIF0K+3YoFdBm0J/XYHkTQOBGjEAHVbAEAxEluMXC6p7c7sL3n16zt34Qu1bEd734a1w3S+UOIJ/1UJBpw== + dependencies: + "@agoric/assert" "^0.6.1-u11wf.0" + "@agoric/ertp" "^0.16.3-u14.0" + "@agoric/governance" "^0.10.4-u14.1" + "@agoric/internal" "^0.4.0-u14.0" + "@agoric/notifier" "^0.6.3-u14.0" + "@agoric/store" "^0.9.3-u14.0" + "@agoric/time" "^0.3.3-u14.0" + "@agoric/vat-data" "^0.5.3-u14.0" + "@agoric/vats" "^0.15.2-u14.1" + "@agoric/zoe" "^0.26.3-u14.0" + "@endo/captp" "3.1.1" + "@endo/eventual-send" "0.17.2" + "@endo/far" "0.2.18" + "@endo/marshal" "0.8.5" + "@endo/nat" "4.1.27" + jessie.js "^0.3.2" + "@agoric/inter-protocol@^0.16.1": version "0.16.1" resolved "https://registry.yarnpkg.com/@agoric/inter-protocol/-/inter-protocol-0.16.1.tgz#ace188e046bb63b0afd2d79065671d26dab53868" @@ -228,28 +228,6 @@ agoric "^0.21.1" jessie.js "^0.3.2" -"@agoric/inter-protocol@^0.16.2-u12.0", "@agoric/inter-protocol@^0.16.2-u14.0", "@agoric/inter-protocol@^0.16.2-u14.1": - version "0.16.2-u14.1" - resolved "https://registry.yarnpkg.com/@agoric/inter-protocol/-/inter-protocol-0.16.2-u14.1.tgz#30b55cf6ff79e1025eb9be3ed649904b42047800" - integrity sha512-eSZnzIF0K+3YoFdBm0J/XYHkTQOBGjEAHVbAEAxEluMXC6p7c7sL3n16zt34Qu1bEd734a1w3S+UOIJ/1UJBpw== - dependencies: - "@agoric/assert" "^0.6.1-u11wf.0" - "@agoric/ertp" "^0.16.3-u14.0" - "@agoric/governance" "^0.10.4-u14.1" - "@agoric/internal" "^0.4.0-u14.0" - "@agoric/notifier" "^0.6.3-u14.0" - "@agoric/store" "^0.9.3-u14.0" - "@agoric/time" "^0.3.3-u14.0" - "@agoric/vat-data" "^0.5.3-u14.0" - "@agoric/vats" "^0.15.2-u14.1" - "@agoric/zoe" "^0.26.3-u14.0" - "@endo/captp" "3.1.1" - "@endo/eventual-send" "0.17.2" - "@endo/far" "0.2.18" - "@endo/marshal" "0.8.5" - "@endo/nat" "4.1.27" - jessie.js "^0.3.2" - "@agoric/internal@^0.3.2": version "0.3.2" resolved "https://registry.yarnpkg.com/@agoric/internal/-/internal-0.3.2.tgz#a1242947083ab46cbd34613add8bacbd0c9dc443" @@ -807,6 +785,15 @@ "@endo/patterns" "0.2.2" "@endo/promise-kit" "0.2.56" +"@agoric/zone@0.2.3-u14.0", "@agoric/zone@^0.2.3-u12.0", "@agoric/zone@^0.2.3-u14.0": + version "0.2.3-u14.0" + resolved "https://registry.yarnpkg.com/@agoric/zone/-/zone-0.2.3-u14.0.tgz#d83adefec94734a62271d6155b68b0ba05362bfa" + integrity sha512-+2c8NhNkm5XwJKyiooQ78N9qlVPMJ5K/kYOysC+BRz+vtWPInocehlFJsVuqisOmPajvnykr8QC+lPN1xHBsmg== + dependencies: + "@agoric/store" "^0.9.3-u14.0" + "@agoric/vat-data" "^0.5.3-u14.0" + "@endo/far" "0.2.18" + "@agoric/zone@^0.2.2": version "0.2.2" resolved "https://registry.yarnpkg.com/@agoric/zone/-/zone-0.2.2.tgz#df5cc091d4a83842b87888e74159a723a424a82e" @@ -816,15 +803,6 @@ "@agoric/vat-data" "^0.5.2" "@endo/far" "^0.2.18" -"@agoric/zone@^0.2.3-u12.0", "@agoric/zone@^0.2.3-u14.0": - version "0.2.3-u14.0" - resolved "https://registry.yarnpkg.com/@agoric/zone/-/zone-0.2.3-u14.0.tgz#d83adefec94734a62271d6155b68b0ba05362bfa" - integrity sha512-+2c8NhNkm5XwJKyiooQ78N9qlVPMJ5K/kYOysC+BRz+vtWPInocehlFJsVuqisOmPajvnykr8QC+lPN1xHBsmg== - dependencies: - "@agoric/store" "^0.9.3-u14.0" - "@agoric/vat-data" "^0.5.3-u14.0" - "@endo/far" "0.2.18" - "@ampproject/remapping@^2.2.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" @@ -1331,6 +1309,11 @@ "@endo/zip" "^0.2.31" ses "^0.18.4" +"@endo/env-options@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@endo/env-options/-/env-options-1.1.1.tgz#eee630f8eff01580ec49e0dedcb1b6cef05d89a4" + integrity sha512-uCwlJ8Vkndx/VBBo36BdYHdxSoQPy7ZZpwyJNfv86Rh4B1IZfqzCRPf0u0mPgJdzOr7lShQey60SuYwoMSZ9Xg== + "@endo/eslint-plugin@^0.5.2": version "0.5.2" resolved "https://registry.yarnpkg.com/@endo/eslint-plugin/-/eslint-plugin-0.5.2.tgz#835d22e9ff17d9935f7f565e50a21ef07aa92ca2" @@ -4260,7 +4243,7 @@ eslint@^8.45.0, eslint@^8.47.0: strip-ansi "^6.0.1" text-table "^0.2.0" -"esm@github:agoric-labs/esm#Agoric-built": +esm@agoric-labs/esm#Agoric-built: version "3.2.25" resolved "https://codeload.github.com/agoric-labs/esm/tar.gz/3603726ad4636b2f865f463188fcaade6375638e" @@ -6982,10 +6965,12 @@ serialize-error@^7.0.1: dependencies: type-fest "^0.13.1" -ses@0.18.4, ses@^0.18.4, ses@^0.18.5, ses@^1.3.0: - version "0.18.4" - resolved "https://registry.yarnpkg.com/ses/-/ses-0.18.4.tgz#28781719870262afc6928b7d6d94dc16318dbd86" - integrity sha512-Ph0PC38Q7uutHmMM9XPqA7rp/2taiRwW6pIZJwTr4gz90DtrBvy/x7AmNPH2uqNPhKriZpYKvPi1xKWjM9xJuQ== +ses@1.3.0, ses@^0.18.4, ses@^0.18.5, ses@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ses/-/ses-1.3.0.tgz#4de8a2e740e5ff9e3cdbc4fd4a3574075c493f40" + integrity sha512-TURVgXm/fs38N4iJfhU9NjUiNvnU7Z/G7gVjM17jD+nrChRzMmR57fbvAzbQeGCS8Cm0m1fBs0jYCqmU6GZ7Tg== + dependencies: + "@endo/env-options" "^1.1.1" set-function-length@^1.2.1: version "1.2.2"