From 0dab20fa8edb243a00a0a31a8d301e3157c4d36d Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Mon, 18 Sep 2023 11:42:17 -0700 Subject: [PATCH] feat: install changes on chain --- agoric/Makefile | 25 +- agoric/contract/package.json | 3 + agoric/contract/src/kreadCommitteeCharter.js | 111 ++++++++ .../src/proposal/chain-storage-proposal.js | 191 ++++++++++++- .../src/proposal/kread-committee-proposal.js | 268 ++++++++++++++++++ .../src/proposal/kread-committee-script.js | 45 +++ agoric/contract/src/proposal/powers.json | 5 +- .../src/proposal/start-kread-proposal.js | 251 ++++++++++++++++ .../src/proposal/start-kread-script.js | 21 ++ agoric/contract/test/bootstrap.js | 3 +- agoric/contract/test/test-governance.js | 60 ++++ 11 files changed, 963 insertions(+), 20 deletions(-) create mode 100644 agoric/contract/src/kreadCommitteeCharter.js create mode 100644 agoric/contract/src/proposal/kread-committee-proposal.js create mode 100644 agoric/contract/src/proposal/kread-committee-script.js create mode 100644 agoric/contract/src/proposal/start-kread-proposal.js create mode 100644 agoric/contract/src/proposal/start-kread-script.js create mode 100644 agoric/contract/test/test-governance.js diff --git a/agoric/Makefile b/agoric/Makefile index b11f77c90..df30da36c 100644 --- a/agoric/Makefile +++ b/agoric/Makefile @@ -37,6 +37,25 @@ wallet1: deploy: agoric deploy contract/kread-deploy-contract.js api/kread-deploy-api.js +install-kread: + agoric run contract/src/proposal/kread-committee-script.js + agoric publish --node 127.0.0.1:26657 /Users/chris/.agoric/cache/b1-610ae18f1dcefba0d443196aaf853550d26a69704113ba5cfcea8f811a0c17fa21a98cd485c25b403bf22d629c3e161d4b42e16fa5b1c77aee5457919aad74c0.json \ + --chain-id agoriclocal --home $(COSMIC_SWINGSET_PATH)/t1/8000; + agoric publish --node 127.0.0.1:26657 /Users/chris/.agoric/cache/b1-0c780c20b02a7e3a10126ca84ce76c50bb34744d9621c83e230ec977b85a8d21ab8f88842fe89edf3a219008edb375178a9dc54f4867ad17463bc0a05470e5ee.json \ + --chain-id agoriclocal --home $(COSMIC_SWINGSET_PATH)/t1/8000; +# agd tx swingset install-bundle @/Users/chris/.agoric/cache/b1-.json \ +# --from gov1 --keyring-backend=test --gas=auto --chain-id=agoriclocal -bblock --yes +# agd tx gov submit-proposal swingset-core-eval kread-invite-committee-permit.json kread-invite-committee.js \ +# --title="Install Kread Committee" --description="Evaluate kread-invite-committee.js" --deposit=1000000ubld \ +# --gas=auto --gas-adjustment=1.2 +# agoric run contract/src/proposal/start-kread-script.js +# agd tx gov submit-proposal swingset-core-eval kread-invite-committee-permit.json kread-invite-committee.js \ +# --title="Enable " --description="Evaluate kread-invite-committee.js" --deposit=1000000ubld \ +# --gas=auto --gas-adjustment=1.2 +# agd tx gov submit-proposal swingset-core-eval kread-invite-committee-permit.json kread-invite-committee.js \ +# --title="Enable " --description="Evaluate kread-invite-committee.js" --deposit=1000000ubld \ +# --gas=auto --gas-adjustment=1. + kread-bundle: cd $(VATS_PATH); \ yarn bundle-source --cache-json bundles/ ${KREAD_REPO} kread; \ @@ -58,7 +77,7 @@ provision-account: fund-account: cd $(COSMIC_SWINGSET_PATH); \ make fund-acct ACCT_ADDR=$(KEPLR_ADDRESS) FUNDS=1000000000000uist; \ - make fund-acct ACCT_ADDR=$(KEPLR_ADDRESS) FUNDS=1000000000000ubld; \ + make fund-acct ACCT_ADDR=$(KEPLR_ADDRESS) FUNDS=1000000000000ubld; \ fund-pool: cd $(COSMIC_SWINGSET_PATH); \ @@ -81,10 +100,10 @@ proposal-2: # before running make proposal bootstrap: make provision-account; \ - make fund-account; \ + make fund-account; \ make kread-bundle; \ make proposal; \ fund+provision: make provision-account; \ - make fund-account; \ + make fund-account; \ diff --git a/agoric/contract/package.json b/agoric/contract/package.json index c846c9467..f825266a0 100644 --- a/agoric/contract/package.json +++ b/agoric/contract/package.json @@ -35,14 +35,17 @@ "@agoric/deploy-script-support": "beta", "@agoric/ertp": "beta", "@agoric/governance": "beta", + "@agoric/inter-protocol": "beta", "@agoric/nat": "dev", "@agoric/notifier": "beta", "@agoric/store": "beta", + "@agoric/time": "beta", "@agoric/vat-data": "^0.5.2", "@agoric/zoe": "beta", "@agoric/vats": "beta", "@endo/bundle-source": "^2.1.1", "@endo/eventual-send": "^0.14.8", + "@endo/far": "^0.2.18", "@endo/init": "^0.5.37", "@endo/marshal": "^0.6.9", "@endo/ses-ava": "^0.2.40", diff --git a/agoric/contract/src/kreadCommitteeCharter.js b/agoric/contract/src/kreadCommitteeCharter.js new file mode 100644 index 000000000..4c792d3ad --- /dev/null +++ b/agoric/contract/src/kreadCommitteeCharter.js @@ -0,0 +1,111 @@ +// @jessie-check + +import '@agoric/governance/exported.js'; +import { M } from '@agoric/store'; +import { TimestampShape } from '@agoric/time'; +import { prepareExo, provideDurableMapStore } from '@agoric/vat-data'; +import '@agoric/zoe/exported.js'; +import '@agoric/zoe/src/contracts/exported.js'; +import { + InstallationShape, + InstanceHandleShape, +} from '@agoric/zoe/src/typeGuards.js'; +import { E } from '@endo/far'; + +/** + * @file This contract makes it possible for those who govern the KREAd contract + * to call for votes to pause offers. + */ + +export const INVITATION_MAKERS_DESC = 'charter member invitation'; + +/** @type {ContractMeta} */ +export const meta = { + customTermsShape: { + binaryVoteCounterInstallation: InstallationShape, + }, + upgradability: 'canUpgrade', +}; +harden(meta); + +/** + * @param {ZCF<{ binaryVoteCounterInstallation: Installation }>} zcf + * @param {undefined} privateArgs + * @param {import('@agoric/vat-data').Baggage} baggage + */ +export const start = async (zcf, privateArgs, baggage) => { + const { binaryVoteCounterInstallation: counter } = zcf.getTerms(); + /** @type {MapStore>} */ + const instanceToGovernor = provideDurableMapStore( + baggage, + 'instanceToGovernor', + ); + + const makeOfferFilterInvitation = (instance, strings, deadline) => { + const voteOnOfferFilterHandler = seat => { + seat.exit(); + + const governor = instanceToGovernor.get(instance); + return E(governor).voteOnOfferFilter(counter, deadline, strings); + }; + + return zcf.makeInvitation(voteOnOfferFilterHandler, 'vote on offer filter'); + }; + + const MakerI = M.interface('Charter InvitationMakers', { + VoteOnPauseOffers: M.call( + InstanceHandleShape, + M.arrayOf(M.string()), + TimestampShape, + ).returns(M.promise()), + }); + + // durable so that when this contract is upgraded this ocap held + // by committee members (from their invitations) stay capable + const invitationMakers = prepareExo( + baggage, + 'Charter Invitation Makers', + MakerI, + { + VoteOnPauseOffers: makeOfferFilterInvitation, + }, + ); + + const charterMemberHandler = seat => { + seat.exit(); + return harden({ invitationMakers }); + }; + + const CharterCreatorI = M.interface('Charter creatorFacet', { + addInstance: M.call(InstanceHandleShape, M.any()) + .optional(M.string()) + .returns(), + makeCharterMemberInvitation: M.call().returns(M.promise()), + }); + + const creatorFacet = prepareExo( + baggage, + 'Charter creatorFacet', + CharterCreatorI, + { + /** + * @param {Instance} governedInstance + * @param {GovernorCreatorFacet} governorFacet + * @param {string} [label] for diagnostic use only + */ + addInstance: (governedInstance, governorFacet, label) => { + console.log('charter: adding instance', label); + instanceToGovernor.init(governedInstance, governorFacet); + }, + makeCharterMemberInvitation: () => + zcf.makeInvitation(charterMemberHandler, INVITATION_MAKERS_DESC), + }, + ); + + return harden({ creatorFacet }); +}; +harden(start); + +/** + * @typedef {import('@agoric/zoe/src/zoeService/utils.js').StartedInstanceKit} KreadCharterStartResult + */ diff --git a/agoric/contract/src/proposal/chain-storage-proposal.js b/agoric/contract/src/proposal/chain-storage-proposal.js index 44f209b54..4f7b54fe7 100644 --- a/agoric/contract/src/proposal/chain-storage-proposal.js +++ b/agoric/contract/src/proposal/chain-storage-proposal.js @@ -12,7 +12,7 @@ // uncomment the following line to typecheck, for example, in vs-code. // import { E } from '@endo/far'; -const defaultCharacters = [ +export const defaultCharacters = [ { title: 'character 1', type: 'tempetScavenger', @@ -84,7 +84,8 @@ const defaultCharacters = [ 'https://ipfs.io/ipfs/QmSkCL11goTK7qw1qLjbozUJ1M7mJtSyH1PnL1g8AB96Zg', }, ]; -const defaultItems = { + +export const defaultItems = { noseline: { name: 'AirTox: Fairy Dust Elite', category: 'noseline', @@ -351,6 +352,156 @@ const fail = (reason) => { throw reason; }; +/** + * @template {GovernableStartFn} SF + * @param {{ + * zoe: ERef; + * timer: ERef; + * contractGovernor: ERef; + * }} bootstrapish + * @param {{ + * governedContractInstallation: ERef>; + * issuerKeywordRecord?: IssuerKeywordRecord; + * terms: Record; + * privateArgs: any; // TODO: connect with Installation type + * label: string; + * }} zoeArgs + * @param {{ + * governedParams: Record; + * committeeCreatorFacet: any; + * }} govArgs + * @returns {Promise>} + */ +const startGovernedInstance = async (bootstrapish, { + governedContractInstallation, + issuerKeywordRecord, + terms, + privateArgs, + label, + }, + { governedParams, committeeCreatorFacet }, +) => { + const { zoe, timer, contractGovernor } = bootstrapish; + + const poserInvitationP = E( + committeeCreatorFacet, + ).getPoserInvitation(); + const [initialPoserInvitation, electorateInvitationAmount] = + await Promise.all([ + poserInvitationP, + E(E(zoe).getInvitationIssuer()).getAmountOf(poserInvitationP), + ]); + + const governorTerms = + harden({ + timer, + governedContractInstallation, + governed: { + terms: { + ...terms, + governedParams: { + Electorate: { + type: 'invitation', + value: electorateInvitationAmount, + }, + ...governedParams, + }, + }, + issuerKeywordRecord, + label, + }, + }, + ); + const governorFacets = await E(zoe).startInstance( + contractGovernor, + {}, + governorTerms, + harden({ + committeeCreatorFacet, + governed: { + ...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; +}; + +/** + * Modeled on produceStartGovernedUpgradable in basicBehaviors, but modified for + * a distinct committee. + * + * @param {{ + * chainTimerService: TimerService, + * diagnostics: { + * savePrivateArgs: (instance: Instance, privateArgs: unknown) => void; + * }; + * committeeCreatorFacet, + * zoe: ZoeService, + * contractGovernor, + * }} bootstrapish + * @param {{ + * installation: Installation, + * issuerKeywordRecord: IssuerKeywordRecord, + * governedParams: , + * terms: Record, + * privateArgs: Record, + * label: string, + * contractKits: Map, + * }} params + */ +const startGovernedKread = async (bootstrapish, { + installation, + issuerKeywordRecord, + governedParams, + terms, + privateArgs, + label, + contractKits, +}) => { + const { diagnostics, committeeCreatorFacet } = bootstrapish; + + const facets = await startGovernedInstance(bootstrapish, + { + governedContractInstallation: installation, + issuerKeywordRecord, + terms, + privateArgs, + label, + }, + { + governedParams, + committeeCreatorFacet, + }, + ); + const kit = harden({ ...facets, label }); + contractKits.init(facets.instance, kit); + + await E(diagnostics).savePrivateArgs(kit.instance, privateArgs); + await E(diagnostics).savePrivateArgs(kit.governor, { + economicCommitteeCreatorFacet: await committeeCreatorFacet, + }); + + return facets; +}; + /** * Execute a proposal to start a contract that publishes bake sales. * @@ -363,25 +514,32 @@ const fail = (reason) => { */ const executeProposal = async (powers) => { // Destructure the powers that we use. - // See also bakeSale-permit.json + // See also powers.json const { + zone, consume: { board, chainStorage, zoe, - startUpgradable, chainTimerService, agoricNamesAdmin, agoricNames, + diagnostics, + kreadCommitteeCreatorFacet, }, - // @ts-expect-error bakeSaleKit isn't declared in vats/src/core/types.js - produce: { kreadKit }, + produce, instance: { - // @ts-expect-error bakeSaleKit isn't declared in vats/src/core/types.js + // @ts-expect-error kreadKit isn't declared in vats/src/core/types.js produce: { [contractInfo.instanceName]: kread }, }, + installation: { + consume: { contractGovernor }, + }, } = powers; + const contractKits = zone.mapStore('KreadContractKits'); + produce.governedContractKits.resolve(contractKits); + const chainStorageSettled = (await chainStorage) || fail(Error('no chainStorage - sim chain?')); const storageNode = E(chainStorageSettled).makeChildNode( @@ -407,13 +565,18 @@ const executeProposal = async (powers) => { // TODO: add terms indicating the keywordRecords used within our offers const noTerms = harden({}); - const { instance, creatorFacet, publicFacet } = await E(startUpgradable)({ - installation, - label: 'KREAd', - issuers, - privateArgs, - noTerms, - }); + const bootstrapish = { + diagnostics, + committeeCreatorFacet: kreadCommitteeCreatorFacet, + zoe, + contractGovernor, + chainTimerService, + }; + + const { instance, creatorFacet, publicFacet } = await E(startGovernedKread)( + bootstrapish, + { installation, label: 'KREAd', issuers, privateArgs, terms: noTerms }, + ); // Get board ids for instance and assets const boardId = await E(board).getId(instance); diff --git a/agoric/contract/src/proposal/kread-committee-proposal.js b/agoric/contract/src/proposal/kread-committee-proposal.js new file mode 100644 index 000000000..a511a1346 --- /dev/null +++ b/agoric/contract/src/proposal/kread-committee-proposal.js @@ -0,0 +1,268 @@ +import { E } from '@endo/far'; +import { reserveThenDeposit } from '@agoric/inter-protocol/src/proposals/utils.js'; + +/** @type {(name: string) => string} */ +const sanitizePathSegment = name => { + const candidate = name.replace(/[ ,]/g, '_'); + assertPathSegment(candidate); + return candidate; +}; + +const { Fail } = assert; + +// These should be exported by Agoric, somewhere. +const pathSegmentPattern = /^[a-zA-Z0-9_-]{1,100}$/; +/** @type {(name: string) => void} */ +const assertPathSegment = name => { + pathSegmentPattern.test(name) || + Fail`Path segment names must consist of 1 to 100 characters limited to ASCII alphanumerics, underscores, and/or dashes: ${name}`; +}; + +const { values } = Object; + +/** + * @typedef {object} KreadCommitteeOptions + * @property {string} [committeeName] + * @property {number} [committeeSize] + */ + +/** + * @param {import('@agoric/inter-protocol/src/proposals/econ-behaviors.js').EconomyBootstrapPowers} powers + * @param {object} config + * @param {object} [config.options] + * @param {KreadCommitteeOptions} [config.options.kreadCommitteeOptions] + */ +export const startKreadCommittee = async ( + { + consume: { board, chainStorage, diagnostics, zoe }, + produce: { kreadCommitteeKit, kreadCommitteeCreatorFacet }, + installation: { + consume: { committee }, + }, + instance: { + produce: { kreadCommittee }, + }, + }, + { options }, +) => { + const COMMITTEES_ROOT = 'committees'; + const { + // NB: the electorate (and size) of the committee may change, but the name must not + committeeName = 'KREAd Committee', + voterAddresses, + ...rest + } = options; + const committeeSize = voterAddresses.length; + console.log(`KCP size`, committeeSize, voterAddresses.keys().length()); + + const [storageNode, marshaller] = await Promise.all([ + E(chainStorage).makeChildNode(COMMITTEES_ROOT).makeChildNode( + sanitizePathSegment(committeeName)), + E(board).getPublishingMarshaller(), + ]); + + const privateArgs = { + storageNode, + marshaller, + }; + const startResult = await E(zoe).startInstance( + committee, + {}, + { committeeName, committeeSize, ...rest }, + privateArgs, + 'kreadCommittee', + ); + kreadCommitteeKit.resolve( + harden({ ...startResult, label: 'kreadCommittee' }), + ); + + await E(diagnostics).savePrivateArgs(startResult.instance, privateArgs); + + kreadCommitteeCreatorFacet.resolve(startResult.creatorFacet); + kreadCommittee.resolve(startResult.instance); +}; +harden(startKreadCommittee); + +/** @type {(xs: X[], ys: Y[]) => [X, Y][]} */ +const zip = (xs, ys) => xs.map((x, i) => [x, ys[i]]); + +/** + * @param {import('@agoric/inter-protocol/src/proposals/econ-behaviors').EconomyBootstrapPowers} powers + * @param {{ options: { voterAddresses: Record } }} param1 + */ +export const inviteCommitteeMembers = async ( + { + consume: { namesByAddressAdmin, kreadCommitteeCreatorFacet, ...consume }, + }, + { options: { voterAddresses } }, +) => { + const invitations = await E( + kreadCommitteeCreatorFacet, + ).getVoterInvitations(); + assert.equal(invitations.length, values(voterAddresses).length); + + const highPrioritySendersManager = await consume.highPrioritySendersManager; + + /** @param {[string, Promise][]} addrInvitations */ + const distributeInvitations = async addrInvitations => { + await Promise.all( + addrInvitations.map(async ([addr, invitationP]) => { + const debugName = `kread committee member ${addr}`; + await reserveThenDeposit(debugName, namesByAddressAdmin, addr, [ + invitationP, + ]).catch(err => console.error(`failed deposit to ${debugName}`, err)); + }), + ); + }; + + // This doesn't resolve until the committee members create their smart wallets. + void distributeInvitations(zip(values(voterAddresses), invitations)); +}; +harden(inviteCommitteeMembers); + +/** @param {import('@agoric/inter-protocol/src/proposals/econ-behaviors').EconomyBootstrapPowers} powers */ +export const startKreadCharter = async ({ + consume: { zoe }, + produce: { kreadCharterKit }, + installation: { + consume: { binaryVoteCounter: counterP, kreadCommitteeCharter: installP }, + }, + instance: { + produce: { kreadCommitteeCharter: instanceP }, + }, +}) => { + const [charterInstall, counterInstall] = await Promise.all([ + installP, + counterP, + ]); + const terms = harden({ + binaryVoteCounterInstallation: counterInstall, + }); + + /** @type {Promise} */ + const startResult = E(zoe).startInstance( + charterInstall, + undefined, + terms, + undefined, + 'kreadCommitteeCharter', + ); + instanceP.resolve(E.get(startResult).instance); + kreadCharterKit.resolve(startResult); +}; +harden(startKreadCharter); + +/** + * Introduce charter to governed creator facets. + * + * @param {import('@agoric/inter-protocol/src/proposals/econ-behaviors').EconomyBootstrapPowers} powers + */ +export const addGovernorToKreadCharter = async ({ + consume: { kreadKit }, + instance: { consume: { kread } }, +}) => { + const { creatorFacet } = E.get(kreadCharterKit); + + await Promise.all( + [ + { + label: 'kread', + instanceP: kread, + facetP: E.get(kreadKit).governorCreatorFacet, + }, + ].map(async ({ label, instanceP, facetP }) => { + const [instance, govFacet] = await Promise.all([instanceP, facetP]); + + return E(creatorFacet).addInstance(instance, govFacet, label); + }), + ); +}; +harden(addGovernorToKreadCharter); + +/** + * @param {import('@agoric/inter-protocol/src/proposals/econ-behaviors').EconomyBootstrapPowers} powers + * @param {{ options: { voterAddresses: Record } }} param1 + */ +export const inviteToKreadCharter = async ( + { consume: { namesByAddressAdmin, kreadCharterKit } }, + { options: { voterAddresses } }, +) => { + const { creatorFacet } = E.get(kreadCharterKit); + + // This doesn't resolve until the committee members create their smart wallets. + // Don't block bootstrap on it. + void Promise.all( + values(voterAddresses).map(async addr => { + const debugName = `KREAd charter member ${addr}`; + reserveThenDeposit(debugName, namesByAddressAdmin, addr, [ + E(creatorFacet).makeCharterMemberInvitation(), + ]).catch(err => console.error(`failed deposit to ${debugName}`, err)); + }), + ); +}; +harden(inviteToKreadCharter); + +export const getManifestForInviteCommittee = async ( + { restoreRef }, + { voterAddresses, kreadCommitteeCharterRef, committeeName }, +) => ({ + manifest: { + [startKreadCommittee.name]: { + consume: { + board: true, + chainStorage: true, + diagnostics: true, + zoe: true, + }, + produce: { + kreadCommitteeKit: true, + kreadCommitteeCreatorFacet: 'kreadCommittee', + }, + installation: { + consume: { committee: 'zoe' }, + }, + instance: { + produce: { kreadCommittee: 'kreadCommittee' }, + }, + }, + [inviteCommitteeMembers.name]: { + consume: { + namesByAddressAdmin: true, + kreadCommitteeCreatorFacet: true, + }, + }, + [startKreadCharter.name]: { + consume: { zoe: true }, + produce: { kreadCharterKit: true }, + installation: { + consume: { binaryVoteCounter: true, kreadCommitteeCharter: true }, + }, + instance: { + produce: { kreadCommitteeCharter: true }, + }, + }, + [addGovernorToKreadCharter.name]: { + consume: { + kreadCharterKit: true, + zoe: true, + agoricNames: true, + namesByAddressAdmin: true, + kreadCommitteeCreatorFacet: true, + kreadKit: true, + }, + installation: { + consume: { binaryVoteCounter: true }, + }, + instance: { + consume: { kread: true }, + }, + }, + [inviteToKreadCharter.name]: { + consume: { namesByAddressAdmin: true, kreadCharterKit: true }, + }, + }, + installations: { + kreadCommitteeCharter: restoreRef(kreadCommitteeCharterRef), + }, + options: { voterAddresses, committeeName }, +}); diff --git a/agoric/contract/src/proposal/kread-committee-script.js b/agoric/contract/src/proposal/kread-committee-script.js new file mode 100644 index 000000000..c37a7d73f --- /dev/null +++ b/agoric/contract/src/proposal/kread-committee-script.js @@ -0,0 +1,45 @@ +/* global process */ +import { makeHelpers } from '@agoric/deploy-script-support'; + +import { getManifestForInviteCommittee } from './kread-committee-proposal.js'; + +// Build proposal for sim-chain etc. +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').ProposalBuilder} */ +export const defaultProposalBuilder = async ( + { publishRef, install }, + options = {}, +) => { + const { + KREAD_COMMITTEE_ADDRESSES = process.env.KREAD_COMMITTEE_ADDRESSES, + committeeName = process.env.KREAD_COMMITTEE_NAME, + voterAddresses = JSON.parse(KREAD_COMMITTEE_ADDRESSES), + } = options; + + console.log(`SCRIPT`, voterAddresses, committeeName); + assert(voterAddresses, 'KREAD_COMMITTEE_ADDRESSES is required'); + + return harden({ + sourceSpec: './kread-committee-proposal.js', + getManifestCall: [ + getManifestForInviteCommittee.name, + { + voterAddresses, + committeeName, + kreadCommitteeCharterRef: publishRef( + install( + '../kreadCommitteeCharter.js', + '../bundles/bundle-kreadCommitteeCharter.js.js', + { + persist: true, + }, + ), + ), + }, + ], + }); +}; + +export default async (homeP, endowments) => { + const { writeCoreProposal } = await makeHelpers(homeP, endowments); + await writeCoreProposal('kread-invite-committee', defaultProposalBuilder); +}; diff --git a/agoric/contract/src/proposal/powers.json b/agoric/contract/src/proposal/powers.json index f6b9b64e7..56fe60276 100644 --- a/agoric/contract/src/proposal/powers.json +++ b/agoric/contract/src/proposal/powers.json @@ -4,9 +4,10 @@ "board": true, "chainStorage": true, "zoe": true, - "startUpgradable": true, "agoricNamesAdmin": true, - "agoricNames": true + "agoricNames": true, + "diagnostics": true, + "kreadCommitteeCreatorFacet": true }, "produce": { "kreadKit": true diff --git a/agoric/contract/src/proposal/start-kread-proposal.js b/agoric/contract/src/proposal/start-kread-proposal.js new file mode 100644 index 000000000..475f40ae7 --- /dev/null +++ b/agoric/contract/src/proposal/start-kread-proposal.js @@ -0,0 +1,251 @@ +// @ts-check + +/** @file This is a module for use with swingset.CoreEval. */ + +import {E} from '@endo/far'; +import { defaultCharacters, defaultItems } from './chain-storage-proposal.js'; + +const contractInfo = { + storagePath: 'kread', + instanceName: 'kread', + // see discussion of publish-bundle and bundleID + // from Dec 14 office hours + // https://github.com/Agoric/agoric-sdk/issues/6454#issuecomment-1351949397 + bundleID: + 'b1-eb9ca74ef1c31f74f95ddb0ee117575faf55b854d701033d0a8e6b52af9550b4081891aa5329f88fb877de6354c2f668b5b31f185e8c2613e1b1f572f6494439', +}; + +const fail = (reason) => { + throw reason; +}; + +/** @typedef {import('@agoric/deploy-script-support/src/coreProposalBehavior.js').BootstrapPowers} BootstrapPowers */ +/** @typedef {import('@agoric/governance/src/types-ambient.js').GovernanceFacetKit} GovernanceFacetKit */ + +/** + * Generalized from basic-behaviors.js to take an arbitrary committee. + * + * @template {GovernableStartFn} SF + * @param {BootstrapPowers} powers + * @param {{object}} kreadConfig + * @returns {Promise>} + */ +const startGovernedInstance = async ({ + zone, + consume: { + zoe, + timer, + contractGovernor, + chainStorage, + board, + kreadCommitteeCreatorFacet, + agoricNames, + }, + produce: { kreadKit }, +}, { kreadConfig, }) => { + const poserInvitationP = E(kreadCommitteeCreatorFacet).getPoserInvitation(); + const [initialPoserInvitation, electorateInvitationAmount] = + await Promise.all([ + poserInvitationP, + E(E(zoe).getInvitationIssuer()).getAmountOf(poserInvitationP), + ]); + + const contractKits = zone.mapStore('KreadContractKits'); + + const chainStorageSettled = + (await chainStorage) || fail(Error('no chainStorage - sim chain?')); + const storageNode = E(chainStorageSettled).makeChildNode( + contractInfo.storagePath, + ); + const marshaller = await E(board).getReadonlyMarshaller(); + const kreadPowers = { storageNode, marshaller }; + const clock = await E(chainTimerService).getClock(); + + const istIssuer = await E(agoricNames).lookup('issuer', 'IST'); + const installation = await E(zoe).installBundleID(contractInfo.bundleID); + + const governorTerms = + harden({ + timer, + governedContractInstallation: installation, + governed: { + terms: { + governedParams: { + Electorate: { + type: 'invitation', + value: electorateInvitationAmount, + }, + }, + }, + issuerKeywordRecord: harden({ Money: istIssuer }), + label: 'KREAd', + }, + }, + ); + + const privateArgs = harden({ powers: kreadPowers, ...kreadConfig }); + + const g = await E(zoe).startInstance( + contractGovernor, + {}, + governorTerms, + harden({ + committeeCreatorFacet, + governed: { + ...privateArgs, + initialPoserInvitation, + }, + }), + `${label}-governor`, + ); + + const [instance, publicFacet, creatorFacet, adminFacet] = await Promise.all([ + E(g.creatorFacet).getInstance(), + E(g.creatorFacet).getPublicFacet(), + E(g.creatorFacet).getCreatorFacet(), + E(g.creatorFacet).getAdminFacet(), + ]); + + kreadKit.resolve( + harden({ + label: 'KREAd', + instance, + publicFacet, + creatorFacet, + adminFacet, + + governor: g.instance, + governorCreatorFacet: g.creatorFacet, + governorAdminFacet: g.adminFacet, + privateArgs, + }), + ); + + return { publicFacet, creatorFacet, instance }; +}; + +/** + * Execute a proposal to start a contract that publishes the KREAd dapp. + * + * See also: + * BLDer DAO governance using arbitrary code injection: swingset.CoreEval + * https://community.agoric.com/t/blder-dao-governance-using-arbitrary-code-injection-swingset-coreeval/99 + * + * @param {BootstrapPowers} powers + */ +const executeProposal = async powers => { + const { + consume: { board, agoricNamesAdmin }, + instance: { + produce: { [contractInfo.instanceName]: kread }, + }, + } = powers; + + const kreadConfig = harden({ + defaultCharacters, + defaultItems, + clock, + seed: 303, + }); + + const { publicFacet, creatorFacet, instance } = await startGovernedInstance( + powers, + { kreadConfig }, + ); + + // Get board ids for instance and assets + const boardId = await E(board).getId(instance); + const { + character: { issuer: characterIssuer, brand: characterBrand }, + item: { issuer: itemIssuer, brand: itemBrand }, + payment: { issuer: tokenIssuer, brand: tokenBrand }, + } = await E(publicFacet).getTokenInfo(); + + const [ + CHARACTER_BRAND_BOARD_ID, + CHARACTER_ISSUER_BOARD_ID, + ITEM_BRAND_BOARD_ID, + ITEM_ISSUER_BOARD_ID, + TOKEN_BRAND_BOARD_ID, + TOKEN_ISSUER_BOARD_ID, + ] = await Promise.all([ + E(board).getId(characterBrand), + E(board).getId(characterIssuer), + E(board).getId(itemBrand), + E(board).getId(itemIssuer), + E(board).getId(tokenBrand), + E(board).getId(tokenIssuer), + ]); + + const assetBoardIds = { + character: { + issuer: CHARACTER_ISSUER_BOARD_ID, + brand: CHARACTER_BRAND_BOARD_ID, + }, + item: { issuer: ITEM_ISSUER_BOARD_ID, brand: ITEM_BRAND_BOARD_ID }, + paymentFT: { issuer: TOKEN_ISSUER_BOARD_ID, brand: TOKEN_BRAND_BOARD_ID }, + }; + + await E(creatorFacet).publishKreadInfo( + boardId, + CHARACTER_BRAND_BOARD_ID, + CHARACTER_ISSUER_BOARD_ID, + ITEM_BRAND_BOARD_ID, + ITEM_ISSUER_BOARD_ID, + TOKEN_BRAND_BOARD_ID, + TOKEN_ISSUER_BOARD_ID, + ); + + await E(creatorFacet).initializeMetrics(); + + // TODO Get the most recent state of metrics from the storage node and send it to the contract + // const data = {}; + // const restoreMetricsInvitation = await E( + // creatorFacet, + // ).makeRestoreMetricsInvitation(); + // await E(zoe).offer(restoreMetricsInvitation, {}, {}, data); + + // Log board ids for use in frontend constants + console.log(`KREAD BOARD ID: ${boardId}`); + for (const [key, value] of Object.entries(assetBoardIds)) { + console.log(`${key.toUpperCase()} BRAND BOARD ID: ${value.brand}`); + console.log(`${key.toUpperCase()} ISSUER BOARD ID: ${value.issuer}`); + } + + // Share instance widely via E(agoricNames).lookup('instance', ) + kread.resolve(instance); + + const kindAdmin = (kind) => E(agoricNamesAdmin).lookupAdmin(kind); + + await E(kindAdmin('issuer')).update('KREAdCHARACTER', characterIssuer); + await E(kindAdmin('brand')).update('KREAdCHARACTER', characterBrand); + + await E(kindAdmin('issuer')).update('KREAdITEM', itemIssuer); + await E(kindAdmin('brand')).update('KREAdITEM', itemBrand); + + console.log('ASSETS ADDED TO AGORIC NAMES'); + // Share instance widely via E(agoricNames).lookup('instance', ) +}; +harden(executeProposal); + +export const getManifestForStartKread = async () => ({ + manifest: { + [executeProposal.name]: { + zone: true, + consume: { + chainTimerService: true, + board: true, + chainStorage: true, + zoe: true, + agoricNamesAdmin: true, + agoricNames: true, + diagnostics: true, + kreadCommitteeCreatorFacet: true, + }, + produce: { kreadKit: true }, + instance: { + produce: { kread: true } + }, + } + } +}); diff --git a/agoric/contract/src/proposal/start-kread-script.js b/agoric/contract/src/proposal/start-kread-script.js new file mode 100644 index 000000000..094977a11 --- /dev/null +++ b/agoric/contract/src/proposal/start-kread-script.js @@ -0,0 +1,21 @@ +/* global process */ +import { makeHelpers } from '@agoric/deploy-script-support'; + +import { getManifestForStartKread } from './start-kread-proposal.js'; + +// Build proposal for sim-chain etc. +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').ProposalBuilder} */ +export const defaultProposalBuilder = async ( + { publishRef, install }, + options = {}, +) => { + return harden({ + sourceSpec: './start-kread-proposal.js', + getManifestCall: [getManifestForStartKread.name, {}], + }); +}; + +export default async (homeP, endowments) => { + const { writeCoreProposal } = await makeHelpers(homeP, endowments); + await writeCoreProposal('kread-invite-committee', defaultProposalBuilder); +}; diff --git a/agoric/contract/test/bootstrap.js b/agoric/contract/test/bootstrap.js index 5db5eedf6..13d95ec35 100644 --- a/agoric/contract/test/bootstrap.js +++ b/agoric/contract/test/bootstrap.js @@ -12,7 +12,7 @@ import { setUpGovernedContract } from '@agoric/governance/tools/puppetGovernance * @param {BootstrapConf} [conf] * @returns {Promise} */ -export const bootstrapContext = async (conf) => { +export const bootstrapContext = async (conf = undefined) => { const { zoe } = setupZoe(); // Setup fungible and non-fungible assets @@ -57,6 +57,7 @@ export const bootstrapContext = async (conf) => { publicFacet, purses, zoe, + governorFacets, }; harden(result); diff --git a/agoric/contract/test/test-governance.js b/agoric/contract/test/test-governance.js new file mode 100644 index 000000000..9cfb46b5e --- /dev/null +++ b/agoric/contract/test/test-governance.js @@ -0,0 +1,60 @@ +// eslint-disable-next-line import/order +import { test } from './prepare-test-env-ava.js'; +import { E } from '@endo/eventual-send'; +import { AmountMath } from '@agoric/ertp'; +import { bootstrapContext } from './bootstrap.js'; +import { flow } from './flow.js'; +import { makeCopyBag } from '@agoric/store'; +import {addCharacterToBootstrap} from './setup.js'; + +test.before(async (t) => { + const bootstrap = + await bootstrapContext(); + + const { zoe, contractAssets, assets, purses, publicFacet, governorFacets } = + bootstrap; + + t.context = { + publicFacet, + contractAssets, + assets, + purses, + zoe, + governorFacets, + }; +}); + +test.serial('block methods', async (t) => { + /** @type {Bootstrap} */ + const { + publicFacet, + governorFacets, + contractAssets, + purses, + zoe, + } = t.context; + + await E(governorFacets.creatorFacet).setFilters(['mintCharacterNfts']); + + console.log(`TG `, purses.item.getCurrentAmount().value.payload); + + const { want } = flow.mintCharacter.expected; + + const mintCharacterInvitation = await E( + publicFacet, + ).makeMintCharacterInvitation(); + const copyBagAmount = makeCopyBag(harden([[want, 1n]])); + const proposal = harden({ + want: { + Asset: AmountMath.make( + contractAssets.character.brand, + harden(copyBagAmount), + ), + }, + }); + + await t.throwsAsync( + () => E(zoe).offer(mintCharacterInvitation, proposal), + { message: 'not accepting offer with description "mintCharacterNfts"' }, + ); +});