From 7264d150dd55635c0f9c2720ae32ee6d174c7f70 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 26 Sep 2023 07:48:38 -0700 Subject: [PATCH] pausing confirmed to work using steps in README --- .../agoric-upgrade-11/README.md | 13 + .../agoric-upgrade-11/agops-bin | 89 ++++ .../agoric-upgrade-11/gov-cmd | 409 ++++++++++++++++++ 3 files changed, 511 insertions(+) create mode 100644 packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/agops-bin create mode 100644 packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/gov-cmd diff --git a/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/README.md b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/README.md index 57f11bdc7ed..5cd9ac31fb4 100644 --- a/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/README.md +++ b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/README.md @@ -76,4 +76,17 @@ agops gov charter --name kreadCommitteeCharter --send-from krgov1 # now should have two used invitations agoric wallet show --keyring-backend=test --from krgov1 +# propose to pause offers +agops gov proposePauseOffers --instance kread --send-from krgov1 --substring foo + +# verify it's there +agd query vstorage data published.committees.kread-gov.latestQuestion + +agops gov vote --instance kreadCommittee --pathname kread-gov --forPosition 0 --send-from krgov1 + +# after a minute the chain output should report the question resolving in the affirmative +agd query vstorage data published.committees.kread-gov.latestOutcome +# TODO a way to read capdata out of vstorage +# this should say "win" for the strings you specified +agd query vstorage data --output json published.committees.kread-gov.latestOutcome | jq -r .value | jq -r .values[0] | jq ``` diff --git a/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/agops-bin b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/agops-bin new file mode 100644 index 00000000000..bad024b4b7a --- /dev/null +++ b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/agops-bin @@ -0,0 +1,89 @@ +#!/usr/bin/env node +// @ts-check +// @jessie-check +/* eslint @typescript-eslint/no-floating-promises: "warn" */ + +/* global fetch, setTimeout */ + +import '@endo/init/pre.js'; + +import '@agoric/casting/node-fetch-shim.js'; +import '@endo/init'; + +import { E } from '@endo/far'; + +import { execFileSync } from 'child_process'; +import path from 'path'; +import process from 'process'; +import anylogger from 'anylogger'; +import { Command, CommanderError, createCommand } from 'commander'; +import { makeOracleCommand } from './commands/oracle.js'; +import { makeEconomicCommiteeCommand } from './commands/ec.js'; +import { makeGovCommand } from './commands/gov.js'; +import { makePsmCommand } from './commands/psm.js'; +import { makeReserveCommand } from './commands/reserve.js'; +import { makeVaultsCommand } from './commands/vaults.js'; +import { makePerfCommand } from './commands/perf.js'; +import { makeInterCommand } from './commands/inter.js'; +import { makeAuctionCommand } from './commands/auction.js'; +import { makeTestCommand } from './commands/test-upgrade.js'; + +const logger = anylogger('agops'); +const progname = path.basename(process.argv[1]); + +const program = new Command(); +program.name(progname).version('docker-u11'); + +program.addCommand(makeOracleCommand(logger)); +program.addCommand(makeEconomicCommiteeCommand(logger)); +program.addCommand(makeGovCommand(logger)); +program.addCommand(makePerfCommand(logger)); +program.addCommand(makePsmCommand(logger)); +program.addCommand(makeVaultsCommand(logger)); + +/** + * XXX Threading I/O powers has gotten a bit jumbled. + * + * Perhaps a more straightforward approach would be: + * + * - makeTUI({ stdout, stderr, logger }) + * where tui.show(data) prints data as JSON to stdout + * and tui.warn() and tui.error() log ad-hoc to stderr + * - makeQueryClient({ fetch }) + * with q.withConfig(networkConfig) + * and q.vstorage.get('published...') (no un-marshaling) + * and q.pollBlocks(), q.pollTx() + * also, printing the progress message should be done + * in the lookup callback + * - makeBoardClient(queryClient) + * with b.readLatestHead('published...') + * - makeKeyringNames({ execFileSync }) + * with names.lookup('gov1') -> 'agoric1...' + * and names.withBackend('test') + * and names.withHome('~/.agoric') + * - makeSigner({ execFileSync }) + * signer.sendSwingsetTx() + */ +const procIO = { + env: { ...process.env }, + stdout: process.stdout, + stderr: process.stderr, + createCommand, + execFileSync, + now: () => Date.now(), + setTimeout, +}; + +program.addCommand(makeReserveCommand(logger, procIO)); +program.addCommand(makeAuctionCommand(logger, { ...procIO, fetch })); +program.addCommand(makeInterCommand(procIO, { fetch })); +program.addCommand(makeTestCommand(procIO, { fetch })); + +E.when(program.parseAsync(process.argv), undefined, err => { + if (err instanceof CommanderError) { + console.error(err.message); + } else { + console.error(err); // CRASH! show stack trace + } + process.exit(1); +}); diff --git a/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/gov-cmd b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/gov-cmd new file mode 100644 index 00000000000..4796f01d0f6 --- /dev/null +++ b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/gov-cmd @@ -0,0 +1,409 @@ +// TO BE COPIED INTO agoric-cli/src/commands +/* eslint-disable func-names */ +/* global globalThis, process, setTimeout */ +import { execFileSync as execFileSyncAmbient } from 'child_process'; +import { Command, CommanderError } from 'commander'; +import { normalizeAddressWithOptions, pollBlocks } from '../lib/chain.js'; +import { getNetworkConfig, makeRpcUtils } from '../lib/rpc.js'; +import { + findContinuingIds, + getCurrent, + getLastUpdate, + outputActionAndHint, + sendAction, +} from '../lib/wallet.js'; + +/** @typedef {import('@agoric/smart-wallet/src/offers.js').OfferSpec} OfferSpec */ + +function collectValues(val, memo) { + memo.push(val); + return memo; +} + +/** + * @param {import('anylogger').Logger} _logger + * @param {{ + * env?: Record, + * fetch?: typeof window.fetch, + * stdout?: Pick, + * stderr?: Pick, + * execFileSync?: typeof execFileSyncAmbient, + * delay?: (ms: number) => Promise, + * }} [io] + */ +export const makeGovCommand = (_logger, io = {}) => { + const { + // Allow caller to provide access explicitly, but + // default to conventional ambient IO facilities. + env = process.env, + stdout = process.stdout, + stderr = process.stderr, + fetch = globalThis.fetch, + execFileSync = execFileSyncAmbient, + delay = ms => new Promise(resolve => setTimeout(resolve, ms)), + } = io; + + const cmd = new Command('gov').description('Electoral governance commands'); + + /** @param {string} literalOrName */ + const normalizeAddress = literalOrName => + normalizeAddressWithOptions(literalOrName, { keyringBackend: 'test' }); + + /** @type {(info: unknown, indent?: unknown) => boolean } */ + const show = (info, indent) => + stdout.write(`${JSON.stringify(info, null, indent ? 2 : undefined)}\n`); + + const abortIfSeen = (instanceName, found) => { + const done = found.filter(it => it.instanceName === instanceName); + if (done.length > 0) { + console.warn(`invitation to ${instanceName} already accepted`, done); + throw new CommanderError(1, 'EALREADY', `already accepted`); + } + }; + + /** + * Make an offer from agoricNames, wallet status; sign and broadcast it, + * given a sendFrom address; else print it. + * + * @param {{ + * toOffer: (agoricNames: *, current: import('@agoric/smart-wallet/src/smartWallet').CurrentWalletRecord | undefined) => OfferSpec, + * sendFrom?: string | undefined, + * instanceName?: string, + * }} detail + * @param {Awaited>} [optUtils] + */ + const processOffer = async function ({ toOffer, sendFrom }, optUtils) { + const networkConfig = await getNetworkConfig(env); + const utils = await (optUtils || makeRpcUtils({ fetch })); + const { agoricNames, readLatestHead } = utils; + + let current; + if (sendFrom) { + current = await getCurrent(sendFrom, { readLatestHead }); + } + + const offer = toOffer(agoricNames, current); + if (!sendFrom) { + outputActionAndHint( + { method: 'executeOffer', offer }, + { stdout, stderr }, + ); + return; + } + + const result = await sendAction( + { method: 'executeOffer', offer }, + { + keyring: { backend: 'test' }, // XXX + from: sendFrom, + verbose: false, + ...networkConfig, + execFileSync, + stdout, + delay, + }, + ); + assert(result); // not dry-run + const { timestamp, txhash, height } = result; + console.error('wallet action is broadcast:'); + show({ timestamp, height, offerId: offer.id, txhash }); + const checkInWallet = async blockInfo => { + const [state, update] = await Promise.all([ + getCurrent(sendFrom, { readLatestHead }), + getLastUpdate(sendFrom, { readLatestHead }), + readLatestHead(`published.wallet.${sendFrom}`), + ]); + if (update.updated === 'offerStatus' && update.status.id === offer.id) { + return blockInfo; + } + const info = await findContinuingIds(state, agoricNames); + const done = info.filter(it => it.offerId === offer.id); + if (!(done.length > 0)) throw Error('retry'); + return blockInfo; + }; + const blockInfo = await pollBlocks({ + retryMessage: 'offer not yet in block', + ...networkConfig, + execFileSync, + delay, + })(checkInWallet); + console.error('offer accepted in block'); + show(blockInfo); + }; + + cmd + .command('committee') + .description('accept invitation to join a committee') + .requiredOption('--name ', 'Committee instance name') + .option('--voter ', 'Voter number', Number, 0) + .option( + '--offerId ', + 'Offer id', + String, + `ecCommittee-${Date.now()}`, + ) + .option( + '--send-from ', + 'Send from address', + normalizeAddress, + ) + .action(async function (opts) { + const { name: instanceName } = opts; + + /** @type {Parameters[0]['toOffer']} */ + const toOffer = (agoricNames, current) => { + const instance = agoricNames.instance[instanceName]; + assert(instance, `missing ${instanceName}`); + + if (current) { + const found = findContinuingIds(current, agoricNames); + abortIfSeen(instanceName, found); + } + + return { + id: opts.offerId, + invitationSpec: { + source: 'purse', + instance, + description: `Voter${opts.voter}`, + }, + proposal: {}, + }; + }; + + await processOffer({ + toOffer, + instanceName, + ...opts, + }); + }); + + cmd + .command('charter') + .description('accept the charter invitation') + .requiredOption('--name ', 'Charter instance name') + .option('--offerId ', 'Offer id', String, `charter-${Date.now()}`) + .option( + '--send-from ', + 'Send from address', + normalizeAddress, + ) + .action(async function (opts) { + const { name: instanceName } = opts; + + /** @type {Parameters[0]['toOffer']} */ + const toOffer = (agoricNames, current) => { + const instance = agoricNames.instance[instanceName]; + assert(instance, `missing ${instanceName}`); + + if (current) { + const found = findContinuingIds(current, agoricNames); + abortIfSeen(instanceName, found); + } + + return { + id: opts.offerId, + invitationSpec: { + source: 'purse', + instance, + description: 'charter member invitation', + }, + proposal: {}, + }; + }; + + await processOffer({ + toOffer, + instanceName: instanceName, + ...opts, + }); + }); + + cmd + .command('find-continuing-id') + .description('print id of specified voting continuing invitation') + .requiredOption( + '--from ', + 'from address', + normalizeAddress, + ) + .requiredOption('--for ', 'description of the invitation') + .action(async opts => { + const { agoricNames, readLatestHead } = await makeRpcUtils({ fetch }); + const current = await getCurrent(opts.from, { readLatestHead }); + + const known = findContinuingIds(current, agoricNames); + if (!known) { + console.error('No continuing ids found'); + return; + } + const match = known.find(r => r.description === opts.for); + if (!match) { + console.error(`No match found for '${opts.for}'`); + return; + } + + console.log(match.offerId); + }); + + cmd + .command('find-continuing-ids') + .description('print records of voting continuing invitations') + .requiredOption( + '--from ', + 'from address', + normalizeAddress, + ) + .action(async opts => { + const { agoricNames, readLatestHead } = await makeRpcUtils({ fetch }); + const current = await getCurrent(opts.from, { readLatestHead }); + + const found = findContinuingIds(current, agoricNames); + found.forEach(it => show({ ...it, address: opts.from })); + }); + + cmd + .command('vote') + .description('vote on latest question') + .requiredOption( + '--instance ', + 'Committee name under agoricNames.instances', + ) + .requiredOption( + '--pathname ', + 'Committee name under published.committees', + ) + .option('--offerId ', 'Offer id', String, `ecVote-${Date.now()}`) + .requiredOption( + '--forPosition ', + 'index of one position to vote for (within the question description.positions); ', + Number, + ) + .requiredOption( + '--send-from ', + 'Send from address', + normalizeAddress, + ) + .action(async function (opts) { + const utils = await makeRpcUtils({ fetch }); + const { readLatestHead } = utils; + + const info = await readLatestHead( + `published.committees.${opts.pathname}.latestQuestion`, + ).catch(err => { + throw new CommanderError(1, 'VSTORAGE_FAILURE', err.message); + }); + // XXX runtime shape-check + const questionDesc = /** @type {any} */ (info); + + // TODO support multiple position arguments + const chosenPositions = [questionDesc.positions[opts.forPosition]]; + assert(chosenPositions, `undefined position index ${opts.forPosition}`); + + /** @type {Parameters[0]['toOffer']} */ + const toOffer = (agoricNames, current) => { + const cont = current ? findContinuingIds(current, agoricNames) : []; + console.log({ cont }); + const votingRight = cont.find(it => it.instanceName === opts.instance); + if (!votingRight) { + console.debug('continuing ids', cont, 'for', current); + throw new CommanderError( + 1, + 'NO_INVITATION', + 'first, try: agops ec committee ...', + ); + } + return { + id: opts.offerId, + invitationSpec: { + source: 'continuing', + previousOffer: votingRight.offerId, + invitationMakerName: 'makeVoteInvitation', + // (positionList, questionHandle) + invitationArgs: harden([ + chosenPositions, + questionDesc.questionHandle, + ]), + }, + proposal: {}, + }; + }; + + await processOffer({ toOffer, sendFrom: opts.sendFrom }, utils); + }); + + cmd + .command('proposePauseOffers') + .description('propose a vote to pause offers') + .option( + '--send-from ', + 'Send from address', + normalizeAddress, + ) + .option( + '--offerId ', + 'Offer id', + String, + `proposePauseOffers-${Date.now()}`, + ) + .requiredOption( + '--instance ', + 'name of governed instance in agoricNames', + ) + .requiredOption( + '--substring ', + 'an offer string to pause (can be repeated)', + collectValues, + [], + ) + .option( + '--deadline ', + 'minutes from now to close the vote', + Number, + 1, + ) + .action(async function (opts) { + const { instance: instanceName } = opts; + + /** @type {Parameters[0]['toOffer']} */ + const toOffer = (agoricNames, current) => { + const instance = agoricNames.instance[instanceName]; + assert(instance, `missing ${instanceName}`); + + const known = findContinuingIds(current, agoricNames); + console.log({ known }); + + assert(known, 'could not find committee acceptance offer id'); + + // TODO magic string + const match = known.find( + r => r.description === 'charter member invitation', + ); + assert(match, 'no offer found for charter member invitation'); + + return { + id: opts.offerId, + invitationSpec: { + source: 'continuing', + previousOffer: match.offerId, + invitationMakerName: 'VoteOnPauseOffers', + // ( instance, strings list, timer deadline seconds ) + invitationArgs: harden([ + instance, + opts.substring, + BigInt(opts.deadline * 60 + Math.round(Date.now() / 1000)), + ]), + }, + proposal: {}, + }; + }; + + await processOffer({ + toOffer, + instanceName, + ...opts, + }); + }); + + return cmd; +};