diff --git a/packages/governance/src/committee.js b/packages/governance/src/committee.js index a82352dfac19..95a144823019 100644 --- a/packages/governance/src/committee.js +++ b/packages/governance/src/committee.js @@ -1,7 +1,7 @@ // @ts-check import { makeStoredPublishKit } from '@agoric/notifier'; -import { makeStore } from '@agoric/store'; +import { defineHeapFarClassKit, initEmpty, makeStore } from '@agoric/store'; import { natSafeMath } from '@agoric/zoe/src/contractSupport/index.js'; import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; @@ -14,9 +14,7 @@ import { startCounter, } from './electorateTools.js'; import { QuorumRule } from './question.js'; -import { defineHeapFarClass } from '../../store/src/patterns/interface-tools'; -import { CommitteeAdminI, CommitteeIKit } from './typeGuards'; -import { initEmpty } from '../../store/src'; +import { CommitteeIKit } from './typeGuards.js'; const { ceilDivide } = natSafeMath; @@ -93,58 +91,94 @@ const start = (zcf, privateArgs) => { [...Array(committeeSize).keys()].map(makeCommitteeVoterInvitation), ); - /** @type {AddQuestion} */ - const addQuestion = async (voteCounter, questionSpec) => { - const quorumThreshold = quorumRule => { - switch (quorumRule) { - case QuorumRule.MAJORITY: - return ceilDivide(committeeSize, 2); - case QuorumRule.ALL: - return committeeSize; - case QuorumRule.NO_QUORUM: - return 0; - default: - throw Error(`${quorumRule} is not a recognized quorum rule`); - } - }; - - return startCounter( - zcf, - questionSpec, - quorumThreshold(questionSpec.quorumRule), - voteCounter, - allQuestions, - questionsPublisher, - ); - }; - - /** @type {CommitteeElectoratePublic} */ - const publicFacet = { - getQuestionSubscriber: () => questionsSubscriber, - getOpenQuestions: () => getOpenQuestions(allQuestions), - getName: () => committeeName, - getInstance: zcf.getInstance, - getQuestion: handleP => getQuestion(handleP, allQuestions), - }; - - // /** @ type {CommitteeElectorateCreatorFacet} */ - const adminFacet = { - getPoserInvitation: () => getPoserInvitation(zcf, addQuestion), - addQuestion, - getVoterInvitations: () => invitations, - getQuestionSubscriber: () => questionsSubscriber, - getPublicFacet: () => publicFacet, - }; - - const makeCommitteeFacets = defineHeapFarClass( - // TODO(CTH) a Kit! - // const makeCommitteeFacets = defineHeapFarClassKit( + const makeCommitteeFacets = defineHeapFarClassKit( 'Committee Facets', CommitteeIKit, initEmpty, - { adminFacet, publicFacet }, + { + creatorFacet: { + getPoserInvitation() { + return getPoserInvitation(zcf, async (voteCounter, questionSpec) => { + const quorumThreshold = quorumRule => { + switch (quorumRule) { + case QuorumRule.MAJORITY: + return ceilDivide(committeeSize, 2); + case QuorumRule.ALL: + return committeeSize; + case QuorumRule.NO_QUORUM: + return 0; + default: + throw Error(`${quorumRule} is not a recognized quorum rule`); + } + }; + + return startCounter( + zcf, + questionSpec, + quorumThreshold(questionSpec.quorumRule), + voteCounter, + allQuestions, + questionsPublisher, + ); + }); + }, + /** @type {AddQuestion} */ + async addQuestion(voteCounter, questionSpec) { + const quorumThreshold = quorumRule => { + switch (quorumRule) { + case QuorumRule.MAJORITY: + return ceilDivide(committeeSize, 2); + case QuorumRule.ALL: + return committeeSize; + case QuorumRule.NO_QUORUM: + return 0; + default: + throw Error(`${quorumRule} is not a recognized quorum rule`); + } + }; + + return startCounter( + zcf, + questionSpec, + quorumThreshold(questionSpec.quorumRule), + voteCounter, + allQuestions, + questionsPublisher, + ); + }, + getVoterInvitations() { + return invitations; + }, + getQuestionSubscriber() { + return questionsSubscriber; + }, + + getPublicFacet() { + return this.facets.publicFacet; + }, + }, + + publicFacet: { + getQuestionSubscriber() { + return questionsSubscriber; + }, + getOpenQuestions() { + return getOpenQuestions(allQuestions); + }, + getName() { + return committeeName; + }, + getInstance() { + return zcf.getInstance(); + }, + getQuestion(handleP) { + return getQuestion(handleP, allQuestions); + }, + }, + }, ); + // @ts-expect-error How to type farClasses? return makeCommitteeFacets(); }; diff --git a/packages/governance/src/contractGovernance/governParam.js b/packages/governance/src/contractGovernance/governParam.js index b251ae938701..fe6358b25ba9 100644 --- a/packages/governance/src/contractGovernance/governParam.js +++ b/packages/governance/src/contractGovernance/governParam.js @@ -10,7 +10,7 @@ import { ElectionType, QuorumRule, } from '../question.js'; -import { ParamChangesQuestionSpecShape } from '../typeGuards.js'; +import { ParamChangesQuestionDetailsShape } from '../typeGuards.js'; const { details: X } = assert; @@ -43,7 +43,7 @@ const makeParamChangePositions = changes => { * @param {QuestionSpec>} questionSpec */ const assertBallotConcernsParam = (paramSpec, questionSpec) => { - fit(questionSpec, ParamChangesQuestionSpecShape); + fit(questionSpec, ParamChangesQuestionDetailsShape); const { parameterName, paramPath } = paramSpec; const { issue } = questionSpec; diff --git a/packages/governance/src/contractGovernor.js b/packages/governance/src/contractGovernor.js index c33f9662e30a..00838e17589e 100644 --- a/packages/governance/src/contractGovernor.js +++ b/packages/governance/src/contractGovernor.js @@ -10,7 +10,7 @@ import { } from './contractGovernance/governParam.js'; import { setupApiGovernance } from './contractGovernance/governApi.js'; import { setupFilterGovernance } from './contractGovernance/governFilter.js'; -import { ParamChangesQuestionSpecShape } from './typeGuards.js'; +import { ParamChangesQuestionDetailsShape } from './typeGuards.js'; const { details: X } = assert; @@ -28,7 +28,7 @@ const validateQuestionDetails = async (zoe, electorate, details) => { counterInstance, issue: { contract: governedInstance }, } = details; - fit(details, ParamChangesQuestionSpecShape); + fit(details, ParamChangesQuestionDetailsShape); const governorInstance = await E.get(E(zoe).getTerms(governedInstance)) .electionManager; diff --git a/packages/governance/src/typeGuards.js b/packages/governance/src/typeGuards.js index fb32fc92a18e..3a2947d3cc3d 100644 --- a/packages/governance/src/typeGuards.js +++ b/packages/governance/src/typeGuards.js @@ -47,6 +47,18 @@ export const OfferFilterQuestionSpecShape = harden({ quorumRule: QuorumRuleShape, tieOutcome: NoOfferFilterPositionShape, }); +export const OfferFilterQuestionDetailsShape = harden({ + method: ChoiceMethodShape, + issue: OfferFilterIssueShape, + positions: OfferFilterPositionsShape, + electionType: 'offer_filter', + maxChoices: M.eq(1), + closingRule: ClosingRuleShape, + quorumRule: QuorumRuleShape, + tieOutcome: NoOfferFilterPositionShape, + questionHandle: makeHandleShape('Question'), + counterInstance: InstanceShape, +}); // keys are parameter names, values are proposed values export const ParamChangesSpecShape = M.recordOf(M.string(), M.any()); @@ -79,6 +91,19 @@ export const ParamChangesQuestionSpecShape = harden({ tieOutcome: NoParamChangesPositionShape, }); +export const ParamChangesQuestionDetailsShape = harden({ + method: 'unranked', + issue: ParamChangesIssueShape, + positions: ParamChangesPositionsShape, + electionType: 'param_change', + maxChoices: M.eq(1), + closingRule: ClosingRuleShape, + quorumRule: 'majority', + tieOutcome: NoParamChangesPositionShape, + questionHandle: makeHandleShape('Question'), + counterInstance: InstanceShape, +}); + const ApiInvocationSpecShape = harden({ apiMethodName: M.string(), methodArgs: M.arrayOf(M.string()), @@ -101,6 +126,18 @@ export const ApiInvocationQuestionSpecShape = harden({ quorumRule: QuorumRuleShape, tieOutcome: NoApiInvocationPositionShape, }); +export const ApiInvocationQuestionDetailsShape = harden({ + method: 'unranked', + issue: ApiInvocationSpecShape, + positions: ApiInvocationPositionsShape, + electionType: 'api_invocation', + maxChoices: M.eq(1), + closingRule: ClosingRuleShape, + quorumRule: QuorumRuleShape, + tieOutcome: NoApiInvocationPositionShape, + questionHandle: makeHandleShape('Question'), + counterInstance: InstanceShape, +}); const SimpleSpecShape = harden({ text: M.string(), @@ -122,6 +159,18 @@ export const SimpleQuestionSpecShape = harden({ quorumRule: QuorumRuleShape, tieOutcome: NoSimplePositionShape, }); +export const SimpleQuestionDetailsShape = harden({ + method: ChoiceMethodShape, + issue: SimpleIssueShape, + positions: SimplePositionsShape, + electionType: M.or('election', 'survey'), + maxChoices: M.gte(1), + closingRule: ClosingRuleShape, + quorumRule: QuorumRuleShape, + tieOutcome: NoSimplePositionShape, + questionHandle: makeHandleShape('Question'), + counterInstance: InstanceShape, +}); export const SimplePositionsShapeA = [ harden({ text: 'yes' }), @@ -154,30 +203,38 @@ export const PositionShape = M.or( export const QuestionHandleShape = makeHandleShape('question'); // TODO(hibbert): add details; move to a more appropriate location -export const SubscriberShape = M.remotable(); -export const InvitationShape = M.remotable(); +export const SubscriberShape = M.remotable('Subscriber'); +export const InvitationShape = M.remotable('Invitation'); + +// XXX I want to add questionHandle and counterInstance to +// ParamChangesQuestionSpecShape. I don't see any alternative to adding the +// methods to each member separately +export const QuestionDetailsShape = M.or( + ParamChangesQuestionDetailsShape, + ApiInvocationQuestionDetailsShape, + OfferFilterQuestionDetailsShape, + SimpleQuestionDetailsShape, +); export const CommitteePublicI = M.interface('Committee PublicFacet', { getQuestionSubscriber: M.call().returns(SubscriberShape), - getOpenQuestions: M.call().returns(M.arrayOf(QuestionSpecShape)), + getOpenQuestions: M.call().returns(M.promise()), getName: M.call().returns(M.string()), getInstance: M.call().returns(InstanceShape), - getQuestion: M.call(QuestionHandleShape).returns(QuestionSpecShape), + getQuestion: M.call(QuestionHandleShape).returns(M.promise()), }); export const CommitteeAdminI = M.interface('Committee AdminFacet', { - getPoserInvitation: M.call().returns(InvitationShape), - addQuestion: M.call(InstanceShape, QuestionSpecShape).return(InstanceShape), - getVoterInvitations: M.call().returns(InvitationShape), + getPoserInvitation: M.call().returns(M.promise()), + addQuestion: M.call(InstanceShape, QuestionSpecShape).returns(M.promise()), + getVoterInvitations: M.call().returns(M.arrayOf(M.promise())), getQuestionSubscriber: M.call().returns(SubscriberShape), getPublicFacet: M.call().returns(CommitteePublicI), }); -export const CommitteeIKit = harden({ CommitteePublicI, CommitteeAdminI }); - -export const QuestionDetailsShape = M.and(QuestionSpecShape, { - questionHandle: M.remotable(), - counterInstance: InstanceShape, +export const CommitteeIKit = harden({ + publicFacet: CommitteePublicI, + creatorFacet: CommitteeAdminI, }); export const QuestionStatsShape = harden({ @@ -217,4 +274,5 @@ export const BinaryVoteCounterCloseI = M.interface( export const BinaryVoteCounterIKit = harden({ BinaryVoteCounterPublicI, BinaryVoteCounterAdminI, + BinaryVoteCounterCloseI, });