From 91ea3247f25d51120b632dce348b7dfc693cbdc4 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 26 Oct 2023 16:26:59 -0500 Subject: [PATCH 1/7] WIP: port 8177-mcs-by-ref branch port of diffs from b9a5cb442..72d1398e8 2023-10-04 15:37 b9a5cb442 Merge pull request #8429 from Agoric/mhofman/8423-crank-ensure-transaction 2023-10-13 13:34 72d1398e8 chore: testing beta1 release working pretty well https://github.com/Agoric/agoric-sdk/tree/8177-mcs-by-ref Diffs outside upgrade-test (agd, agops) are omitted. --- Dockerfile | 3 + Makefile | 1 + README.md | 6 +- .../agoric-upgrade-10/actions.js | 61 +-- .../agoric-upgrade-10/actions.test.js | 12 +- .../agoric-upgrade-10/post.test.js | 6 +- .../agoric-upgrade-10/pre.test.js | 8 +- .../agoric-upgrade-10/upgradeHelpers.js | 6 +- .../agoric-upgrade-11/README.md | 92 ++++ .../agoric-upgrade-11/actions.test.js | 6 +- .../agoric-upgrade-11/add-collateral.test.js | 344 ++++++++++++ .../agoric-upgrade-11/agops-bin | 89 +++ .../agoric-upgrade-11/core-eval-support.js | 148 +++++ .../agoric-upgrade-11/gov-cmd | 409 ++++++++++++++ .../agoric-upgrade-11/mn2-start.test.js | 516 ++++++++++++++++++ .../agoric-upgrade-11/pre.test.js | 4 +- .../agoric-upgrade-12/actions.js | 6 +- .../agoric-upgrade-12/actions.test.js | 11 +- .../agoric-upgrade-12/post.test.js | 2 +- .../agoric-upgrade-12/pre.test.js | 4 +- upgrade-test-scripts/lib/agd-lib.js | 119 ++++ upgrade-test-scripts/lib/assert.js | 21 + upgrade-test-scripts/{ => lib}/cliHelper.js | 51 ++ .../{ => lib}/commonUpgradeHelpers.js | 23 +- upgrade-test-scripts/{ => lib}/constants.js | 0 upgrade-test-scripts/{ => lib}/econHelpers.js | 4 +- upgrade-test-scripts/lib/unmarshal.js | 143 +++++ .../tools => lib}/vat-status.js | 4 +- upgrade-test-scripts/lib/vstorage.js | 42 ++ upgrade-test-scripts/lib/webAsset.js | 200 +++++++ upgrade-test-scripts/package.json | 4 +- 31 files changed, 2255 insertions(+), 90 deletions(-) create mode 100644 upgrade-test-scripts/agoric-upgrade-11/README.md create mode 100644 upgrade-test-scripts/agoric-upgrade-11/add-collateral.test.js create mode 100644 upgrade-test-scripts/agoric-upgrade-11/agops-bin create mode 100644 upgrade-test-scripts/agoric-upgrade-11/core-eval-support.js create mode 100644 upgrade-test-scripts/agoric-upgrade-11/gov-cmd create mode 100644 upgrade-test-scripts/agoric-upgrade-11/mn2-start.test.js create mode 100644 upgrade-test-scripts/lib/agd-lib.js create mode 100644 upgrade-test-scripts/lib/assert.js rename upgrade-test-scripts/{ => lib}/cliHelper.js (70%) rename upgrade-test-scripts/{ => lib}/commonUpgradeHelpers.js (94%) rename upgrade-test-scripts/{ => lib}/constants.js (100%) rename upgrade-test-scripts/{ => lib}/econHelpers.js (92%) create mode 100644 upgrade-test-scripts/lib/unmarshal.js rename upgrade-test-scripts/{agoric-upgrade-12/tools => lib}/vat-status.js (97%) create mode 100644 upgrade-test-scripts/lib/vstorage.js create mode 100644 upgrade-test-scripts/lib/webAsset.js diff --git a/Dockerfile b/Dockerfile index 944619dd..86b2284f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -71,6 +71,7 @@ ENV THIS_NAME=agoric-upgrade-10 USE_JS=1 WORKDIR /usr/src/agoric-sdk/ COPY ./env_setup.sh ./start_to_to.sh ./package.json ./*.js ./upgrade-test-scripts/ +COPY ./lib/ ./upgrade-test-scripts/lib/ RUN cd upgrade-test-scripts && yarn RUN echo '. /usr/src/agoric-sdk/upgrade-test-scripts/env_setup.sh' >> ~/.bashrc @@ -104,6 +105,7 @@ ENV THIS_NAME=agoric-upgrade-11 USE_JS=1 # start-chain boilerplate WORKDIR /usr/src/agoric-sdk/ COPY ./env_setup.sh ./start_to_to.sh ./package.json ./*.js ./upgrade-test-scripts/ +COPY ./lib/ ./upgrade-test-scripts/lib/ RUN cd upgrade-test-scripts && yarn RUN echo '. /usr/src/agoric-sdk/upgrade-test-scripts/env_setup.sh' >> ~/.bashrc @@ -138,6 +140,7 @@ COPY --from=propose-agoric-upgrade-12 /root/.agoric /root/.agoric # start-chain boilerplate WORKDIR /usr/src/agoric-sdk/ COPY ./env_setup.sh ./start_to_to.sh ./package.json ./*.js ./upgrade-test-scripts/ +COPY ./lib/ ./upgrade-test-scripts/lib/ RUN cd upgrade-test-scripts && yarn RUN echo '. /usr/src/agoric-sdk/upgrade-test-scripts/env_setup.sh' >> ~/.bashrc diff --git a/Makefile b/Makefile index d9f1e3ef..ef7baf4d 100644 --- a/Makefile +++ b/Makefile @@ -41,6 +41,7 @@ DEBUG ?= SwingSet:ls,SwingSet:vat RUN = docker run --rm -it \ -p 26656:26656 -p 26657:26657 -p 1317:1317 \ -v "$${PWD}:/workspace" \ + -v "$${MN2}:/mn2" \ -e "DEBUG=$(DEBUG)" run: diff --git a/README.md b/README.md index 61f8e544..60b4705f 100644 --- a/README.md +++ b/README.md @@ -98,9 +98,9 @@ To make the wallet ui talk to your local chain, set the network config to 2. Duplicate the last pair of UPGRADE and TEST blocks 3. Update their number from the UPGRADE / DEST block at the end 4. Make directory for tests (e.g. `agoric-upgrade-12`) -4. Make directory for ugprade (e.g. `propose-agoric-upgrade-12` with a `.keep`) -5. Update the UPGRADE/DEST pair to be your new upgrade (THIS_NAME matching the upgrade handler string in app.go) -6. Update the `Makefile` +5. Make directory for ugprade (e.g. `propose-agoric-upgrade-12` with a `.keep`) +6. Update the UPGRADE/DEST pair to be your new upgrade (THIS_NAME matching the upgrade handler string in app.go) +7. Update the `Makefile` - the two targets to `Makefile` (e.g. `propose-agoric-upgrade-12` and `agoric-upgrade-12`) - set the default TARGET (e.g. `agoric-upgrade-12`) - add the DEST target to the `.phony` in `Makefile` diff --git a/upgrade-test-scripts/agoric-upgrade-10/actions.js b/upgrade-test-scripts/agoric-upgrade-10/actions.js index 569fdde2..32998de4 100644 --- a/upgrade-test-scripts/agoric-upgrade-10/actions.js +++ b/upgrade-test-scripts/agoric-upgrade-10/actions.js @@ -3,24 +3,25 @@ import assert from 'assert'; import { agd, - agoric, agops, agopsLocation, executeCommand, -} from '../cliHelper.js'; + smallCapsContext, + wellKnownIdentities, +} from '../lib/cliHelper.js'; import { HOME, ATOM_DENOM, GOV1ADDR, GOV2ADDR, GOV3ADDR, -} from '../constants.js'; +} from '../lib/constants.js'; import { waitForBlock, executeOffer, getUser, provisionSmartWallet, -} from '../commonUpgradeHelpers.js'; +} from '../lib/commonUpgradeHelpers.js'; const govAccounts = [GOV1ADDR, GOV2ADDR, GOV3ADDR]; @@ -122,50 +123,23 @@ const paramChangeOfferGeneration = async ( previousOfferId, voteDur, debtLimit, + io = {}, ) => { + const { now = Date.now, agoricNames = await wellKnownIdentities(io) } = io; + const { brand, instance } = agoricNames; + assert(instance.VaultFactory); + assert(brand.IST); + assert(brand.ATOM); + const ISTunit = 1_000_000n; // aka displayInfo: { decimalPlaces: 6 } const voteDurSec = BigInt(voteDur); const debtLimitValue = BigInt(debtLimit) * ISTunit; const toSec = ms => BigInt(Math.round(ms / 1000)); - const id = `propose-${Date.now()}`; - const deadline = toSec(Date.now()) + voteDurSec; + const id = `propose-${now()}`; + const deadline = toSec(now()) + voteDurSec; - const zip = (xs, ys) => xs.map((x, i) => [x, ys[i]]); - const fromSmallCapsEntries = txt => { - const { body, slots } = JSON.parse(txt); - const theEntries = zip(JSON.parse(body.slice(1)), slots).map( - ([[name, ref], boardID]) => { - const iface = ref.replace(/^\$\d+\./, ''); - return [name, { iface, boardID }]; - }, - ); - return Object.fromEntries(theEntries); - }; - - const slots = []; // XXX global mutable state - const smallCaps = { - Nat: n => `+${n}`, - // XXX mutates obj - ref: obj => { - if (obj.ix) return obj.ix; - const ix = slots.length; - slots.push(obj.boardID); - obj.ix = `$${ix}.Alleged: ${obj.iface}`; - return obj.ix; - }, - }; - - const instance = fromSmallCapsEntries( - await agoric.follow('-lF', ':published.agoricNames.instance', '-o', 'text'), - ); - assert(instance.VaultFactory); - - const brand = fromSmallCapsEntries( - await agoric.follow('-lF', ':published.agoricNames.brand', '-o', 'text'), - ); - assert(brand.IST); - assert(brand.ATOM); + const { smallCaps, toCapData } = smallCapsContext(); const body = { method: 'executeOffer', @@ -197,8 +171,7 @@ const paramChangeOfferGeneration = async ( }, }; - const capData = { body: `#${JSON.stringify(body)}`, slots }; - return JSON.stringify(capData); + return toCapData(body); }; export const provisionWallet = async user => { @@ -251,7 +224,7 @@ export const proposeNewDebtCeiling = async address => { return executeOffer( address, - paramChangeOfferGeneration(charterAcceptOfferId, 30, 123000000), + paramChangeOfferGeneration(charterAcceptOfferId, 30, 123_000_000), ); }; diff --git a/upgrade-test-scripts/agoric-upgrade-10/actions.test.js b/upgrade-test-scripts/agoric-upgrade-10/actions.test.js index a11e28ff..e2265817 100644 --- a/upgrade-test-scripts/agoric-upgrade-10/actions.test.js +++ b/upgrade-test-scripts/agoric-upgrade-10/actions.test.js @@ -6,11 +6,15 @@ import { raiseDebtCeiling, pushPrice, } from './actions.js'; -import { agd, agoric, agops } from '../cliHelper.js'; -import { GOV1ADDR, GOV2ADDR } from '../constants.js'; -import { getUser, newOfferId, waitForBlock } from '../commonUpgradeHelpers.js'; +import { agd, agoric, agops } from '../lib/cliHelper.js'; +import { GOV1ADDR, GOV2ADDR } from '../lib/constants.js'; +import { + getUser, + newOfferId, + waitForBlock, +} from '../lib/commonUpgradeHelpers.js'; import { submitDeliverInbound } from './upgradeHelpers.js'; -import { openVault, adjustVault, closeVault } from '../econHelpers.js'; +import { openVault, adjustVault, closeVault } from '../lib/econHelpers.js'; const START_FREQUENCY = 600; // StartFrequency: 600s (auction runs every 10m) const CLOCK_STEP = 20; // ClockStep: 20s (ensures auction completes in time) diff --git a/upgrade-test-scripts/agoric-upgrade-10/post.test.js b/upgrade-test-scripts/agoric-upgrade-10/post.test.js index 9e6ac511..b18b67b9 100644 --- a/upgrade-test-scripts/agoric-upgrade-10/post.test.js +++ b/upgrade-test-scripts/agoric-upgrade-10/post.test.js @@ -1,8 +1,8 @@ import test from 'ava'; -import { agd, agoric } from '../cliHelper.js'; -import { GOV1ADDR, GOV2ADDR, GOV3ADDR, USER1ADDR } from '../constants.js'; -import { calculateWalletState } from '../commonUpgradeHelpers.js'; +import { agd, agoric } from '../lib/cliHelper.js'; +import { GOV1ADDR, GOV2ADDR, GOV3ADDR, USER1ADDR } from '../lib/constants.js'; +import { calculateWalletState } from '../lib/commonUpgradeHelpers.js'; test('DeliverInbound from un-provisioned account is discarded', async t => { const result = await agd.query('swingset', 'mailbox', USER1ADDR); diff --git a/upgrade-test-scripts/agoric-upgrade-10/pre.test.js b/upgrade-test-scripts/agoric-upgrade-10/pre.test.js index f5b1c96d..4a2fd42b 100644 --- a/upgrade-test-scripts/agoric-upgrade-10/pre.test.js +++ b/upgrade-test-scripts/agoric-upgrade-10/pre.test.js @@ -2,16 +2,16 @@ import test from 'ava'; import { promises as fs } from 'fs'; -import { agd, agoric, agops } from '../cliHelper.js'; +import { agd, agoric, agops } from '../lib/cliHelper.js'; import { GOV1ADDR, GOV2ADDR, GOV3ADDR, PSM_PAIR, -} from '../constants.js'; -import { openVault } from '../econHelpers.js'; -import { getUser, waitForBlock } from '../commonUpgradeHelpers.js'; +} from '../lib/constants.js'; +import { openVault } from '../lib/econHelpers.js'; +import { getUser, waitForBlock } from '../lib/commonUpgradeHelpers.js'; test.before(async () => { console.log('Wait for upgrade to settle'); diff --git a/upgrade-test-scripts/agoric-upgrade-10/upgradeHelpers.js b/upgrade-test-scripts/agoric-upgrade-10/upgradeHelpers.js index 5a81b731..d15295cd 100644 --- a/upgrade-test-scripts/agoric-upgrade-10/upgradeHelpers.js +++ b/upgrade-test-scripts/agoric-upgrade-10/upgradeHelpers.js @@ -8,10 +8,10 @@ import { VALIDATORADDR, USER1ADDR, CHAINID, -} from '../constants.js'; +} from '../lib/constants.js'; -import { agd } from '../cliHelper.js'; -import { getUser } from '../commonUpgradeHelpers.js'; +import { agd } from '../lib/cliHelper.js'; +import { getUser } from '../lib/commonUpgradeHelpers.js'; export const printKeys = async () => { console.log('========== GOVERNANCE KEYS =========='); diff --git a/upgrade-test-scripts/agoric-upgrade-11/README.md b/upgrade-test-scripts/agoric-upgrade-11/README.md new file mode 100644 index 00000000..5cd9ac31 --- /dev/null +++ b/upgrade-test-scripts/agoric-upgrade-11/README.md @@ -0,0 +1,92 @@ +# Upgrade 11 + +1. upgrade-handler +2. 11wf with walletFactory +3. 11kr with kreadKit + +# Testing 11kr + +Related: https://github.com/agoric-labs/KREAd/releases/tag/gryo-rc0 + +It has to be built with these specific addresses, using this one really long command: +```sh +KREAD_COMMITTEE_ADDRESSES='{"krgov1": "agoric1890064p6j3xhzzdf8daknd6kpvhw766ds8flgw", "krgov2": "agoric1vqm5x5sj4lxmj2kem7x92tuhaum0k2yzyj6mgu"}' \ +KREAD_COMMITTEE_NAME=kread-gov \ +KREAD_ROYALTY_ADDRESS=agoric1yjc8llu3fugm7tgqye4rd5n92l9x2dhe30dazp \ +KREAD_PLATFORM_ADDRESS=agoric1enwuyn2hzyyvt39x87tk9rhlkpqtyv9haj7mgs \ +make clean build-proposals +``` + +Build the images if you haven't lately, +```sh +make build +``` + + +Then with MN2 set to the repo you build the above from: +```sh +TARGET=agoric-upgrade-11 MN2=/opt/agoric/KREAd/ make run +``` + +That will run: +```sh +docker run --rm -it -p 26656:26656 -p 26657:26657 -p 1317:1317 -v "${PWD}:/workspace" -v "${MN2}:/mn2" -e "DEBUG=SwingSet:ls,SwingSet:vat" -e "DEST=1" -e "TMUX_USE_CC=0" \ + --entrypoint /usr/src/agoric-sdk/upgrade-test-scripts/start_to_to.sh \ + agoric/upgrade-test:agoric-upgrade-11 +``` + +Then in a new shell do the following. +(TODO integrate these into the layers) +```sh +# Replace its `upgrade-test-scripts` with a symlink: +rm -rf upgrade-test-scripts +ln -s /workspace/upgrade-test-scripts + +# Patch agoric-cli +cp upgrade-test-scripts/agoric-upgrade-11/gov-cmd packages/agoric-cli/src/commands/gov.js +cp upgrade-test-scripts/agoric-upgrade-11/agops-bin packages/agoric-cli/src/bin-agops.js +## this should be docker-u11 +agops --version + +# patch agd so it doesn't try to rebuild +#sed --in-place=.backup '93,$ d' /usr/src/agoric-sdk/bin/agd +#- find `$BUILD_ONLY || exec 1>&2` (line 94?) +#- put this in above it: `${NO_BUILD:-false} && exit 0` +vi /usr/src/agoric-sdk/bin/agd + + +# Run the test using the `/mn2` that `make run` mounted: +cd /usr/src/agoric-sdk/upgrade-test-scripts + +NO_BUILD=true \ +MN2_PROPOSAL_INFO=/mn2/agoric/dist/ \ +MN2_INSTANCE=kread \ +yarn ava /usr/src/agoric-sdk/upgrade-test-scripts/agoric-upgrade-11/mn2-start.test.js +``` + +Test governance +TODO put into JS +```sh +# should have two invitations in balances +agoric wallet show --keyring-backend=test --from krgov1 + +agops gov committee --name kreadCommittee --send-from krgov1 +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/upgrade-test-scripts/agoric-upgrade-11/actions.test.js b/upgrade-test-scripts/agoric-upgrade-11/actions.test.js index 58063c1c..5c1cd44a 100644 --- a/upgrade-test-scripts/agoric-upgrade-11/actions.test.js +++ b/upgrade-test-scripts/agoric-upgrade-11/actions.test.js @@ -1,8 +1,8 @@ import test from 'ava'; -import { agoric, agops } from '../cliHelper.js'; -import { GOV1ADDR } from '../constants.js'; -import { openVault, adjustVault, closeVault } from '../econHelpers.js'; +import { agoric, agops } from '../lib/cliHelper.js'; +import { GOV1ADDR } from '../lib/constants.js'; +import { openVault, adjustVault, closeVault } from '../lib/econHelpers.js'; test.serial('Open Vaults', async t => { const currentVaults = await agops.vaults('list', '--from', GOV1ADDR); diff --git a/upgrade-test-scripts/agoric-upgrade-11/add-collateral.test.js b/upgrade-test-scripts/agoric-upgrade-11/add-collateral.test.js new file mode 100644 index 00000000..8b1688b6 --- /dev/null +++ b/upgrade-test-scripts/agoric-upgrade-11/add-collateral.test.js @@ -0,0 +1,344 @@ +// @ts-check +import * as fspAmbient from 'fs/promises'; +import * as pathAmbient from 'path'; +import * as processAmbient from 'process'; +import * as cpAmbient from 'child_process'; // TODO: use execa + +import anyTest from 'ava'; +import dbOpenAmbient from 'better-sqlite3'; +import { tmpName as tmpNameAmbient } from 'tmp'; +import { ZipReader } from '@endo/zip'; + +import { makeFileRW, makeWebCache, makeWebRd } from '../lib/webAsset.js'; +import { makeAgd } from '../lib/agd-lib.js'; +import { dbTool } from '../lib/vat-status.js'; +import { voteLatestProposalAndWait } from '../lib/commonUpgradeHelpers.js'; +import { + bundleDetail, + ensureISTForInstall, + flags, + getContractInfo, + loadedBundleIds, + testIncludes, + txAbbr, +} from './core-eval-support.js'; +import { agoric, wellKnownIdentities } from '../lib/cliHelper.js'; + +/** @typedef {Awaited>} TestContext */ +/** @type {import('ava').TestFn}} */ +const test = anyTest; + +/** + * URLs of assets, including bundle hashes (to be) agreed by BLD stakers + */ +const assetInfo = { + repo: { + release: + 'https://github.com/dckc/agoric-vault-collateral-proposal/releases/tag/v0.13.0-beta1', + url: 'https://github.com/0xpatrickdev/agoric-vault-collateral-proposal', + name: 'agoric-vault-collateral-proposal', + description: + 'CoreEval Proposal and Permits for Inter Vault Collateral Type', + }, + branch: 'auction-update', + /** @type {Record} */ + buildAssets: { + 'add-stATOM': { + evals: [{ permit: 'add-stATOM-permit.json', script: 'add-stATOM.js' }], + bundles: [ + // addAssetToVault.js + 'b1-903e41a7c448a41b456298404a1c32c69302574209c6a5228723ed19e2dd99f2a693641196445bc27a90e19e1dfadfe6b3d9c9a93f080ffa33a70908e5af4fff.json', + ], + }, + 'add-stATOM-oracle': { + evals: [ + { + permit: 'add-stATOM-oracles-permit.json', + script: 'add-stATOM-oracles.js', + }, + ], + bundles: [ + // price-feed-proposal.js + 'b1-80e6fe68b299c82c2d26802c312bc37966a559f7b28f87d058887a79a9db48ad97da2240e71e3f98986071da8fc3c5d02358bec577b17a89cee2b1cb3cd23958.json', + ], + }, + }, +}; + +const staticConfig = { + deposit: '10000000ubld', // 10 BLD + installer: 'gov1', // as in: agd keys show gov1 + proposer: 'validator', + collateralPrice: 6, // conservatively low price. TODO: look up + swingstorePath: '~/.agoric/data/agoric/swingstore.sqlite', + assetBase: `${assetInfo.repo.url}/raw/${assetInfo.branch}/`, // alternative + releaseAssets: assetInfo.repo.release.replace('/tag/', '/download/') + '/', + title: assetInfo.repo.name, + description: assetInfo.repo.description, + buildInfo: Object.values(assetInfo.buildAssets), +}; + +/** + * Provide access to the outside world via t.context. + * + * TODO: refactor overlap with mn2-start.test.js + * + * @param {*} t + * @param {object} io + */ +const makeTestContext = async (t, io = {}) => { + const { + process: { env } = processAmbient, + child_process: { execFileSync } = cpAmbient, + dbOpen = dbOpenAmbient, + fsp = fspAmbient, + path = pathAmbient, + tmpName = tmpNameAmbient, + } = io; + + // @@ const src = makeWebRd(staticConfig.assetBase, { fetch }); + const src = makeWebRd(staticConfig.releaseAssets, { fetch }); + + const td = await new Promise((resolve, reject) => + tmpName({ prefix: 'assets' }, (err, x) => (err ? reject(err) : resolve(x))), + ); + const dest = makeFileRW(td, { fsp, path }); + // @@ Error: `t.teardown()` is not allowed in hooks + // t.teardown(() => assets.remove()); + const assets = makeWebCache(src, dest); + // assume filenames don't overlap + //@@alt const bundleAssets = makeWebCache(src.join('bundles/'), dest); + const bundleAssets = makeWebCache(src, dest); + console.log(`bundleAssets: ${bundleAssets}`); + + const config = { + assets, + bundleAssets, + chainId: 'agoriclocal', + ...staticConfig, + }; + + const agd = makeAgd({ execFileSync: execFileSync }).withOpts({ + keyringBackend: 'test', + }); + + const dbPath = staticConfig.swingstorePath.replace(/^~/, env.HOME); + const swingstore = dbTool(dbOpen(dbPath, { readonly: true })); + + const before = new Map(); + return { agd, agoric, swingstore, config, before, fetch }; +}; + +test.before(async t => (t.context = await makeTestContext(t))); + +test.serial('bundles not yet installed', async t => { + // TODO: also check that scaledPrice..., fluxAgg bundles match mainnet + // select bundleId from bundles where (bundleId like 'b1-4522b%' or bundleId like 'b1-0b217%')" + const { swingstore } = t.context; + const loaded = loadedBundleIds(swingstore); + const info = staticConfig.buildInfo; + for (const { bundles, evals } of info) { + t.log(evals[0].script, evals.length, 'eval', bundles.length, 'bundles'); + for (const bundle of bundles) { + const { id } = bundleDetail(bundle); + testIncludes(t, id, loaded, 'loaded bundles', false); + } + } +}); + +/** + * @param {{endoZipBase64:string}} bundle + * @param {{fetch: typeof fetch}} io - using fetch for base64 decoding is a bit of over-kill + */ +const bundleEntry = async (bundle, { fetch }) => { + const getZipReader = async () => { + const { endoZipBase64 } = bundle; + const toBlob = (base64, type = 'application/octet-stream') => + fetch(`data:${type};base64,${base64}`).then(res => res.blob()); + const zipBlob = await toBlob(endoZipBase64); + // https://github.com/endojs/endo/issues/1811#issuecomment-1751499626 + const buffer = await zipBlob.arrayBuffer(); + const bytes = new Uint8Array(buffer); + return new ZipReader(bytes); + }; + + const getCompartmentMap = zipRd => { + const { content } = zipRd.files.get('compartment-map.json'); + const td = new TextDecoder(); + const cmap = JSON.parse(td.decode(content)); + return cmap; + }; + + const zipRd = await getZipReader(); + const cmap = getCompartmentMap(zipRd); + return cmap.entry; +}; + +test.serial('bundle names: compartmentMap.entry', async t => { + const { + config: { bundleAssets }, + fetch, + } = t.context; + const info = staticConfig.buildInfo; + for (const { bundles, evals } of info) { + for (const bundleRef of bundles) { + const { fileName } = bundleDetail(bundleRef); + const bundle = JSON.parse(await bundleAssets.getText(fileName)); + const entry = await bundleEntry(bundle, { fetch }); + t.log(entry, fileName.slice(0, 'b1-12345'.length)); + t.truthy(entry.compartment); + t.truthy(entry.module); + } + } +}); + +/** @param {number[]} xs */ +const sum = xs => xs.reduce((a, b) => a + b, 0); + +/** @param {import('../lib/webAsset.js').WebCache} assets */ +const readBundleSizes = async assets => { + const info = staticConfig.buildInfo; + const bundleSizes = await Promise.all( + info + .map(({ bundles }) => + bundles.map(b => assets.size(bundleDetail(b).fileName)), + ) + .flat(), + ); + const totalSize = sum(bundleSizes); + return { bundleSizes, totalSize }; +}; + +test.serial('core eval not permitted to add/replace installations', async t => { + const { + config: { assets }, + } = t.context; + const { buildInfo } = staticConfig; + + for (const { permit: permitRef } of buildInfo.map(x => x.evals).flat()) { + const permit = JSON.parse(await assets.getText(permitRef)); + t.log('installation.produce', permit?.installation?.produce, permitRef); + t.falsy(permit?.installation?.produce); + } +}); + +test.serial('save installations before the poposal', async t => { + const { agoric, before } = t.context; + const { installation } = await wellKnownIdentities({ agoric }); + t.log(installation.priceAggregator); + t.truthy(installation.priceAggregator); + before.set('installation', installation); +}); + +test.serial('ensure enough IST to install bundles', async t => { + const { agd, config } = t.context; + const { totalSize } = await readBundleSizes(config.bundleAssets); + + await ensureISTForInstall(agd, config, totalSize, { + log: t.log, + }); + t.pass(); +}); + +test.serial('ensure bundles installed', async t => { + const { agd, swingstore, agoric, config } = t.context; + const { chainId, bundleAssets } = config; + const loaded = loadedBundleIds(swingstore); + const from = agd.lookup(config.installer); + + let todo = 0; + let done = 0; + for (const { bundles } of staticConfig.buildInfo) { + todo += bundles.length; + for (const bundle of bundles) { + const { id, fileName, endoZipBase64Sha512 } = bundleDetail(bundle); + if (loaded.includes(id)) { + t.log('bundle already installed', id); + done += 1; + continue; + } + + const bundleRd = await bundleAssets.storedPath(fileName); + const result = await agd.tx( + ['swingset', 'install-bundle', `@${bundleRd}`, '--gas', 'auto'], + { from, chainId, yes: true }, + ); + t.log(txAbbr(result)); + t.is(result.code, 0); + + const info = await getContractInfo('bundles', { agoric, prefix: '' }); + t.log(info); + done += 1; + t.deepEqual(info, { + endoZipBase64Sha512, + error: null, + installed: true, + }); + } + } + t.is(todo, done); +}); + +test.serial('core eval proposal passes', async t => { + const { agd, swingstore, config } = t.context; + const from = agd.lookup(config.proposer); + const { chainId, deposit, assets } = config; + const info = { title: config.title, description: config.description }; + t.log('submit proposal', config.title); + + // double-check that bundles are loaded + const loaded = loadedBundleIds(swingstore); + const { buildInfo } = staticConfig; + for (const { bundles } of buildInfo) { + for (const bundle of bundles) { + const { id } = bundleDetail(bundle); + if (!loaded.includes(id)) { + t.fail(`bundle ${id} not loaded`); + return; + } + } + } + + const evalNames = buildInfo + .map(({ evals }) => evals) + .flat() + .map(e => [e.permit, e.script]) + .flat(); + const evalPaths = await Promise.all(evalNames.map(e => assets.storedPath(e))); + const result = await agd.tx( + [ + 'gov', + 'submit-proposal', + 'swingset-core-eval', + ...evalPaths, + ...flags({ ...info, deposit }), + ...flags({ gas: 'auto', 'gas-adjustment': '1.2' }), + ], + { from, chainId, yes: true }, + ); + t.log(txAbbr(result)); + t.is(result.code, 0); + + const detail = await voteLatestProposalAndWait(); + t.log(detail.proposal_id, detail.voting_end_time, detail.status); + t.is(detail.status, 'PROPOSAL_STATUS_PASSED'); +}); + +test.serial('priceAuthority installation was not changed', async t => { + const { agoric, before } = t.context; + const { installation } = await wellKnownIdentities({ agoric }); + const actual = installation.priceAggregator; + const expected = before.get('installation').priceAggregator; + t.log({ expected, actual }); + t.deepEqual(actual, expected); +}); + +test('stATOM-USD price feed instance in agoricNames', async t => { + const { agoric } = t.context; + const { instance } = await wellKnownIdentities({ agoric }); + testIncludes(t, 'stATOM-USD price feed', Object.keys(instance), 'instance'); +}); + +test.todo('manager in vstorage'); +test.todo('price feed in vstorage - after setting prices'); +test.todo('create a vault as dapp-inter does'); diff --git a/upgrade-test-scripts/agoric-upgrade-11/agops-bin b/upgrade-test-scripts/agoric-upgrade-11/agops-bin new file mode 100644 index 00000000..bad024b4 --- /dev/null +++ b/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/upgrade-test-scripts/agoric-upgrade-11/core-eval-support.js b/upgrade-test-scripts/agoric-upgrade-11/core-eval-support.js new file mode 100644 index 00000000..48ebac20 --- /dev/null +++ b/upgrade-test-scripts/agoric-upgrade-11/core-eval-support.js @@ -0,0 +1,148 @@ +// @ts-check +import { Far, makeMarshal, makeTranslationTable } from '../lib/unmarshal.js'; +import { Fail, NonNullish } from '../lib/assert.js'; + +// TODO: factor out ambient authority from these +// or at least allow caller to supply authority. +import { mintIST } from '../lib/econHelpers.js'; +import { agoric } from '../lib/cliHelper.js'; + +// move to unmarshal.js? +const makeBoardUnmarshal = () => { + const synthesizeRemotable = (_slot, iface) => + Far(iface.replace(/^Alleged: /, ''), {}); + + const { convertValToSlot, convertSlotToVal } = makeTranslationTable( + slot => Fail`unknown id: ${slot}`, + synthesizeRemotable, + ); + + return makeMarshal(convertValToSlot, convertSlotToVal); +}; + +export const getContractInfo = async (path, io = {}) => { + const m = makeBoardUnmarshal(); + const { + agoric: { follow = agoric.follow }, + prefix = 'published.', + } = io; + console.log('@@TODO: prevent agoric follow hang', prefix, path); + const txt = await follow('-lF', `:${prefix}${path}`, '-o', 'text'); + const { body, slots } = JSON.parse(txt); + return m.fromCapData({ body, slots }); +}; + +// not really core-eval related +export const testIncludes = (t, needle, haystack, label, sense = true) => { + t.log(needle, sense ? 'in' : 'not in', haystack.length, label, '?'); + const check = sense ? t.deepEqual : t.notDeepEqual; + if (sense) { + t.deepEqual( + haystack.filter(c => c === needle), + [needle], + ); + } else { + t.deepEqual( + haystack.filter(c => c === needle), + [], + ); + } +}; + +/** + * @param {Record} record - e.g. { color: 'blue' } + * @returns {string[]} - e.g. ['--color', 'blue'] + */ +export const flags = record => { + return Object.entries(record) + .map(([k, v]) => [`--${k}`, v]) + .flat(); +}; + +export const txAbbr = tx => { + const { txhash, code, height, gas_used } = tx; + return { txhash, code, height, gas_used }; +}; + +export const loadedBundleIds = swingstore => { + const ids = swingstore`SELECT bundleID FROM bundles`.map(r => r.bundleID); + return ids; +}; + +/** + * @param {string} cacheFn - e.g. /home/me.agoric/cache/b1-DEADBEEF.json + */ +export const bundleDetail = cacheFn => { + const fileName = NonNullish(cacheFn.split('/').at(-1)); + const id = fileName.replace(/\.json$/, ''); + const hash = id.replace(/^b1-/, ''); + return { fileName, endoZipBase64Sha512: hash, id }; +}; + +const importBundleCost = (bytes, price = 0.002) => { + return bytes * price; +}; + +/** + * @typedef {{ + * bundles: string[], + * evals: { permit: string; script: string }[], + * }} ProposalInfo + */ + +const myISTBalance = async (agd, addr, denom = 'uist', unit = 1_000_000) => { + const coins = await agd.query(['bank', 'balances', addr]); + const coin = coins.balances.find(a => a.denom === denom); + return Number(coin.amount) / unit; +}; + +/** + * @param {number} myIST + * @param {number} cost + * @param {{ + * unit?: number, padding?: number, minInitialDebt?: number, + * collateralPrice: number, + * }} opts + * @returns + */ +const mintCalc = (myIST, cost, opts) => { + const { + unit = 1_000_000, + padding = 1, + minInitialDebt = 6, + collateralPrice, + } = opts; + const { round, max } = Math; + const wantMinted = max(round(cost - myIST + padding), minInitialDebt); + const giveCollateral = round(wantMinted / collateralPrice) + 1; + const sendValue = round(giveCollateral * unit); + return { wantMinted, giveCollateral, sendValue }; +}; + +/** + * + * @param {ReturnType} agd + * @param {*} config + * @param {number} bytes total bytes + * @param {{ log: (...args: any[]) => void }} io + * @returns + */ +export const ensureISTForInstall = async (agd, config, bytes, { log }) => { + const cost = importBundleCost(bytes); + log({ totalSize: bytes, cost }); + const { installer } = config; + const addr = agd.lookup(installer); + const istBalance = await myISTBalance(agd, addr); + + if (istBalance > cost) { + log('balance sufficient', { istBalance, cost }); + return; + } + const { sendValue, wantMinted, giveCollateral } = mintCalc( + istBalance, + cost, + config, + ); + log({ wantMinted }); + await mintIST(addr, sendValue, wantMinted, giveCollateral); +}; diff --git a/upgrade-test-scripts/agoric-upgrade-11/gov-cmd b/upgrade-test-scripts/agoric-upgrade-11/gov-cmd new file mode 100644 index 00000000..4796f01d --- /dev/null +++ b/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; +}; diff --git a/upgrade-test-scripts/agoric-upgrade-11/mn2-start.test.js b/upgrade-test-scripts/agoric-upgrade-11/mn2-start.test.js new file mode 100644 index 00000000..4dd831da --- /dev/null +++ b/upgrade-test-scripts/agoric-upgrade-11/mn2-start.test.js @@ -0,0 +1,516 @@ +// @ts-check + +/** + * @file mainnet-2 contract start test + * + * Expects several environment variables: + * If $MN2_PROPOSAL_INFO is not set, no tests are run. + * + * $MN2_PROPOSAL_INFO is a directory with the results of + * running `agoric run xyz-script.js` one or more times. + * Note: include the trailing / in the $MN_PROPOSAL_INFO. + * + * Each time, in addition to any *-permit.json and *.js files, + * stdout is parsed with `parseProposals.js` to produce + * a *-info.json file containing... + * + * @typedef {{ + * bundles: string[], + * evals: { permit: string; script: string }[], + * }} ProposalInfo + * + * Only the last path segment of ProposalInfo.bundles is used. + * All bundles must be available in $MN2_PROPOSAL_INFO/bundles/ . + * + * $MN2_INSTANCE is a key in agoricNames.instance + * where the mainnet-2 contract instance is expected to be installed. + * A vstorage node under published by this name is also expected. + */ + +import anyTest from 'ava'; +import * as cpAmbient from 'child_process'; // TODO: use execa +import * as fspAmbient from 'fs/promises'; +import { tmpName as tmpNameAmbient } from 'tmp'; +import * as pathAmbient from 'path'; +import * as processAmbient from 'process'; +import dbOpenAmbient from 'better-sqlite3'; + +// TODO: factor out ambient authority from these +// or at least allow caller to supply authority. +import { mintIST } from '../lib/econHelpers.js'; +import { agoric, wellKnownIdentities } from '../lib/cliHelper.js'; +import { + provisionSmartWallet, + voteLatestProposalAndWait, +} from '../lib/commonUpgradeHelpers.js'; + +import { makeAgd } from '../lib/agd-lib.js'; +import { Far, makeMarshal, makeTranslationTable } from '../lib/unmarshal.js'; +import { Fail, NonNullish } from '../lib/assert.js'; +import { dbTool } from '../lib/vat-status.js'; +import { makeFileRW, makeWebCache, makeWebRd } from '../lib/webAsset.js'; + +/** @typedef {Awaited>} TestContext */ +/** @type {import('ava').TestFn}} */ +const test = anyTest; + +/** + * URLs of release assets, including bundle hashes agreed by BLD stakers + * + * TODO: get permits, scripts from blockchain? + * TODO: verify bundle contents against hashes? + * + * KREAd-rc1 to Mainnet + * voting 2023-09-28 to 2023-10-01 + * https://agoric.explorers.guru/proposal/53 + */ +const releaseInfo = { + url: 'https://github.com/Kryha/KREAd/releases/tag/KREAd-rc1', + /** @type {Record} */ + buildAssets: { + 'kread-committee-info.json': { + evals: [ + { + permit: 'kread-invite-committee-permit.json', + script: 'kread-invite-committee.js', + }, + ], + bundles: [ + '/Users/wietzes/.agoric/cache/b1-51085a4ad4ac3448ccf039c0b54b41bd11e9367dfbd641deda38e614a7f647d7f1c0d34e55ba354d0331b1bf54c999fca911e6a796c90c30869f7fb8887b3024.json', + '/Users/wietzes/.agoric/cache/b1-a724453e7bfcaae1843be4532e18c1236c3d6d33bf6c44011f2966e155bc7149b904573014e583fdcde2b9cf2913cb8b337fc9daf79c59a38a37c99030fcf7dc.json', + ], + }, + 'start-kread-info.json': { + evals: [{ permit: 'start-kread-permit.json', script: 'start-kread.js' }], + bundles: [ + '/Users/wietzes/.agoric/cache/b1-853acd6ba3993f0f19d6c5b0a88c9a722c9b41da17cf7f98ff7705e131860c4737d7faa758ca2120773632dbaf949e4bcce2a2cbf2db224fa09cd165678f64ac.json', + '/Users/wietzes/.agoric/cache/b1-0c3363b8737677076e141a84b84c8499012f6ba79c0871fc906c8be1bb6d11312a7d14d5a3356828a1de6baa4bee818a37b7cb1ca2064f6eecbabc0a40d28136.json', + ], + }, + }, +}; + +const dappAPI = { + instance: 'kread', // agoricNames.instance key + vstorageNode: 'kread', +}; + +const staticConfig = { + deposit: '10000000ubld', // 10 BLD + installer: 'gov1', // as in: agd keys show gov1 + proposer: 'validator', + collateralPrice: 6, // conservatively low price. TODO: look up + swingstorePath: '~/.agoric/data/agoric/swingstore.sqlite', + releaseAssets: releaseInfo.url.replace('/tag/', '/download/') + '/', + buildInfo: Object.values(releaseInfo.buildAssets), + initialCoins: `20000000ubld`, // enough to provision a smartWallet + accounts: { + krgov1: { + address: 'agoric1890064p6j3xhzzdf8daknd6kpvhw766ds8flgw', + mnemonic: + 'loop clump life tattoo action wish loop garbage room custom tooth lunar increase major draw wage bind vanish order behind bounce unknown cry practice', + }, + krgov2: { + address: 'agoric1vqm5x5sj4lxmj2kem7x92tuhaum0k2yzyj6mgu', + mnemonic: + 'expect wheel safe ankle caution vote reduce sell night pencil suit scrap tumble divorce element become result front hurt begin deputy liberty develop next', + }, + kRoyalties: { + address: 'agoric1yjc8llu3fugm7tgqye4rd5n92l9x2dhe30dazp', + mnemonic: + 'talent approve render pool chief inch nuclear minor rhythm laundry praise swift clog neck shoot elder rely junior rule basket energy payment giggle torch', + }, + kPlatform: { + address: 'agoric1enwuyn2hzyyvt39x87tk9rhlkpqtyv9haj7mgs', + mnemonic: + 'magic enrich village office myth depth upper pair april dad visit memory resemble castle lab surface globe debate chair upper army pony moon tone', + }, + }, + ...dappAPI, +}; + +/** + * Provide access to the outside world via t.context. + */ +const makeTestContext = async (io = {}) => { + const { + process: { env } = processAmbient, + child_process: { execFileSync } = cpAmbient, + dbOpen = dbOpenAmbient, + fsp = fspAmbient, + path = pathAmbient, + tmpName = tmpNameAmbient, + } = io; + + const src = makeWebRd(staticConfig.releaseAssets, { fetch }); + const td = await new Promise((resolve, reject) => + tmpName({ prefix: 'assets' }, (err, x) => (err ? reject(err) : resolve(x))), + ); + const dest = makeFileRW(td, { fsp, path }); + td.teardown(() => assets.remove()); + const assets = makeWebCache(src, dest); + + const config = { + assets, + chainId: 'agoriclocal', + ...staticConfig, + }; + + // This agd API is based on experience "productizing" + // the inter bid CLI in #7939 + const agd = makeAgd({ execFileSync: execFileSync }).withOpts({ + keyringBackend: 'test', + }); + + const dbPath = staticConfig.swingstorePath.replace(/^~/, env.HOME); + const swingstore = dbTool(dbOpen(dbPath, { readonly: true })); + + return { agd, agoric, swingstore, config }; +}; + +test.before(async t => (t.context = await makeTestContext())); + +const testIncludes = (t, needle, haystack, label, sense = true) => { + t.log(needle, sense ? 'in' : 'not in', haystack.length, label, '?'); + const check = sense ? t.deepEqual : t.notDeepEqual; + if (sense) { + t.deepEqual( + haystack.filter(c => c === needle), + [needle], + ); + } else { + t.deepEqual( + haystack.filter(c => c === needle), + [], + ); + } +}; + +test.serial(`pre-flight: not in agoricNames.instance`, async t => { + const { config, agoric } = t.context; + const { instance: target } = config; + const { instance } = await wellKnownIdentities({ agoric }); + testIncludes(t, target, Object.keys(instance), 'instance keys', false); +}); + +const makeBoardUnmarshal = () => { + const synthesizeRemotable = (_slot, iface) => + Far(iface.replace(/^Alleged: /, ''), {}); + + const { convertValToSlot, convertSlotToVal } = makeTranslationTable( + slot => Fail`unknown id: ${slot}`, + synthesizeRemotable, + ); + + return makeMarshal(convertValToSlot, convertSlotToVal); +}; + +export const getContractInfo = async (path, io = {}) => { + const m = makeBoardUnmarshal(); + const { + agoric: { follow = agoric.follow }, + prefix = 'published.', + } = io; + console.log('@@TODO: prevent agoric follow hang', prefix, path); + const txt = await follow('-lF', `:${prefix}${path}`, '-o', 'text'); + const { body, slots } = JSON.parse(txt); + return m.fromCapData({ body, slots }); +}; + +// XXX dead code - worth keeping somewhere? +const ensureMintLimit = async (targetNum, manager = 0, unit = 1_000_000) => { + const io = { agoric }; + const [{ current }, metrics] = await Promise.all([ + getContractInfo(`vaultFactory.managers.manager${manager}.governance`, io), + getContractInfo(`vaultFactory.managers.manager${manager}.metrics`, io), + ]); + const { totalDebt } = metrics; + const { + DebtLimit: { value: limit }, + } = current; + const nums = { + total: Number(totalDebt.value) / unit, + limit: Number(limit.value) / unit, + target: targetNum, + }; + nums.target += nums.total; + console.log( + nums, + nums.limit >= nums.target ? 'limit > target' : 'LIMIT TOO LOW', + ); + if (nums.limit >= nums.target) return; + throw Error('raising mint limit not impl'); +}; + +const myISTBalance = async (agd, addr, denom = 'uist', unit = 1_000_000) => { + const coins = await agd.query(['bank', 'balances', addr]); + const coin = coins.balances.find(a => a.denom === denom); + return Number(coin.amount) / unit; +}; + +const importBundleCost = (bytes, price = 0.002) => { + return bytes * price; +}; + +/** + * @param {number} myIST + * @param {number} cost + * @param {{ + * unit?: number, padding?: number, minInitialDebt?: number, + * collateralPrice: number, + * }} opts + * @returns + */ +const mintCalc = (myIST, cost, opts) => { + const { + unit = 1_000_000, + padding = 1, + minInitialDebt = 6, + collateralPrice, + } = opts; + const { round, max } = Math; + const wantMinted = max(round(cost - myIST + padding), minInitialDebt); + const giveCollateral = round(wantMinted / collateralPrice) + 1; + const sendValue = round(giveCollateral * unit); + return { wantMinted, giveCollateral, sendValue }; +}; + +const loadedBundleIds = swingstore => { + const ids = swingstore`SELECT bundleID FROM bundles`.map(r => r.bundleID); + return ids; +}; + +/** + * @param {string} cacheFn - e.g. /home/me.agoric/cache/b1-DEADBEEF.json + */ +const bundleDetail = cacheFn => { + const fileName = NonNullish(cacheFn.split('/').at(-1)); + const id = fileName.replace(/\.json$/, ''); + const hash = id.replace(/^b1-/, ''); + return { fileName, endoZipBase64Sha512: hash, id }; +}; + +test.serial('bundles not yet installed', async t => { + const { swingstore, config } = t.context; + const loaded = loadedBundleIds(swingstore); + const info = staticConfig.buildInfo; + for (const { bundles, evals } of info) { + t.log(evals[0].script, evals.length, 'eval', bundles.length, 'bundles'); + for (const bundle of bundles) { + const { id } = bundleDetail(bundle); + testIncludes(t, id, loaded, 'loaded bundles', false); + } + } +}); + +/** @param {number[]} xs */ +const sum = xs => xs.reduce((a, b) => a + b, 0); + +/** @param {import('../lib/webAsset.js').WebCache} assets */ +const readBundleSizes = async assets => { + const info = staticConfig.buildInfo; + const bundleSizes = await Promise.all( + info + .map(({ bundles }) => + bundles.map(b => assets.size(bundleDetail(b).fileName)), + ) + .flat(), + ); + const totalSize = sum(bundleSizes); + return { bundleSizes, totalSize }; +}; + +const ensureISTForInstall = async (agd, config, { log }) => { + const { proposalDir, installer } = config; + const { bundleSizes, totalSize } = await readBundleSizes(proposalDir); + const cost = importBundleCost(sum(bundleSizes)); + log({ bundleSizes, totalSize, cost }); + + const addr = agd.lookup(installer); + const istBalance = await myISTBalance(agd, addr); + + if (istBalance > cost) { + log('balance sufficient', { istBalance, cost }); + return; + } + const { sendValue, wantMinted, giveCollateral } = mintCalc( + istBalance, + cost, + config, + ); + log({ wantMinted }); + await mintIST(addr, sendValue, wantMinted, giveCollateral); +}; + +test.serial('ensure enough IST to install bundles', async t => { + const { agd, config } = t.context; + await ensureISTForInstall(agd, config, { log: t.log }); + t.pass(); +}); + +const txAbbr = tx => { + const { txhash, code, height, gas_used } = tx; + return { txhash, code, height, gas_used }; +}; + +test.serial('ensure bundles installed', async t => { + const { agd, swingstore, agoric, config, io } = t.context; + const { chainId, assets } = config; + const loaded = loadedBundleIds(swingstore); + const from = agd.lookup(config.installer); + + let todo = 0; + let done = 0; + for (const { bundles } of staticConfig.buildInfo) { + todo += bundles.length; + for (const bundle of bundles) { + const { id, fileName, endoZipBase64Sha512 } = bundleDetail(bundle); + if (loaded.includes(id)) { + t.log('bundle already installed', id); + done += 1; + continue; + } + + const bundleRd = await assets.storedPath(fileName); + const result = await agd.tx( + ['swingset', 'install-bundle', `@${bundleRd}`, '--gas', 'auto'], + { from, chainId, yes: true }, + ); + t.log(txAbbr(result)); + t.is(result.code, 0); + + const info = await getContractInfo('bundles', { agoric, prefix: '' }); + t.log(info); + done += 1; + t.deepEqual(info, { + endoZipBase64Sha512, + error: null, + installed: true, + }); + } + } + t.is(todo, done); +}); + +/** + * @param {Record} record - e.g. { color: 'blue' } + * @returns {string[]} - e.g. ['--color', 'blue'] + */ +const flags = record => { + return Object.entries(record) + .map(([k, v]) => [`--${k}`, v]) + .flat(); +}; + +test.serial('core eval prereqs: provision royalty, gov, ...', async t => { + const { agd, config } = t.context; + const { entries } = Object; + + for (const [name, { address, mnemonic }] of entries(config.accounts)) { + try { + agd.lookup(address); + t.log(name, 'key already added'); + continue; + } catch (_e) {} + t.log('add key', name); + agd.keys.add(name, mnemonic); + } + + for (const [name, { address }] of entries(config.accounts)) { + const walletPath = `published.wallet.${address}`; + const data = await agd.query(['vstorage', 'data', walletPath]); + if (data.value.length > 0) { + t.log(name, 'wallet already provisioned'); + continue; + } + await provisionSmartWallet(address, config.initialCoins); + } + + t.pass(); +}); + +test.serial('core eval proposal passes', async t => { + const { agd, swingstore, config } = t.context; + const from = agd.lookup(config.proposer); + const { chainId, deposit, assets, instance } = config; + const info = { title: instance, description: `start ${instance}` }; + t.log('submit proposal', instance); + + // double-check that bundles are loaded + const loaded = loadedBundleIds(swingstore); + const { buildInfo } = staticConfig; + for (const { bundles } of buildInfo) { + for (const bundle of bundles) { + const { id } = bundleDetail(bundle); + testIncludes(t, id, loaded, 'loaded bundles'); + } + } + + const evalNames = buildInfo + .map(({ evals }) => evals) + .flat() + .map(e => [e.permit, e.script]) + .flat(); + const evalPaths = await Promise.all(evalNames.map(e => assets.storedPath(e))); + t.log(evalPaths); + console.debug('await tx', evalPaths); + const result = await agd.tx( + [ + 'gov', + 'submit-proposal', + 'swingset-core-eval', + ...evalPaths, + ...flags({ ...info, deposit }), + ...flags({ gas: 'auto', 'gas-adjustment': '1.2' }), + ], + { from, chainId, yes: true }, + ); + t.log(txAbbr(result)); + t.is(result.code, 0); + + console.debug('await voteLatestProposalAndWait', evalPaths); + const detail = await voteLatestProposalAndWait(); + t.log(detail.proposal_id, detail.voting_end_time, detail.status); + t.is(detail.status, 'PROPOSAL_STATUS_PASSED'); +}); + +test.serial(`agoricNames.instance is populated`, async t => { + const { config, agoric } = t.context; + const { instance: target } = config; + const { instance, brand } = await wellKnownIdentities({ agoric }); + const present = Object.keys(instance); + testIncludes(t, target, present, 'instance keys'); +}); + +// needs 2 brand names +test.todo(`agoricNames.brand is populated`); +test.todo('boardAux is populated'); + +test.serial('vstorage published.CHILD is present', async t => { + const { agd, config } = t.context; + const { vstorageNode } = config; + const { children } = await agd.query(['vstorage', 'children', 'published']); + testIncludes(t, vstorageNode, children, 'published children'); +}); + +// KREAd specific below here +// TODO refactor this test for re-use across MN2 scripts + +// TODO test this more robustly with the pausing feature +// This doesn't work with mainline KREAd becaues they don't have anything +// to write upon contract start. The pausing test will ensure there's +// a latestQuestion node published. +test.serial('kread commmittee is present', async t => { + const { agd, config } = t.context; + const { vstorageNode } = config; + const { children } = await agd.query([ + 'vstorage', + 'children', + 'published.committees', + ]); + testIncludes(t, 'kread-gov', children, 'published children'); +}); + +test.todo('test contract features- mint character'); +test.todo('test contract governance - pause'); +test.todo('test contract governance - API'); diff --git a/upgrade-test-scripts/agoric-upgrade-11/pre.test.js b/upgrade-test-scripts/agoric-upgrade-11/pre.test.js index 818a0ea5..0cd83e1e 100644 --- a/upgrade-test-scripts/agoric-upgrade-11/pre.test.js +++ b/upgrade-test-scripts/agoric-upgrade-11/pre.test.js @@ -1,7 +1,7 @@ import test from 'ava'; -import { agd, agoric } from '../cliHelper.js'; -import { waitForBlock } from '../commonUpgradeHelpers.js'; +import { agd, agoric } from '../lib/cliHelper.js'; +import { waitForBlock } from '../lib/commonUpgradeHelpers.js'; test.before(async () => { console.log('Wait for upgrade to settle'); diff --git a/upgrade-test-scripts/agoric-upgrade-12/actions.js b/upgrade-test-scripts/agoric-upgrade-12/actions.js index 6ce8bbfc..32bd3517 100644 --- a/upgrade-test-scripts/agoric-upgrade-12/actions.js +++ b/upgrade-test-scripts/agoric-upgrade-12/actions.js @@ -2,9 +2,9 @@ import { promises as fs } from 'fs'; import * as path from 'path'; import { dirname } from 'path'; import { fileURLToPath } from 'url'; -import { voteLatestProposalAndWait } from '../commonUpgradeHelpers.js'; -import { CHAINID, GOV1ADDR, VALIDATORADDR } from '../constants.js'; -import { agd, bundleSource } from '../cliHelper.js'; +import { voteLatestProposalAndWait } from '../lib/commonUpgradeHelpers.js'; +import { CHAINID, GOV1ADDR, VALIDATORADDR } from '../lib/constants.js'; +import { agd, bundleSource } from '../lib/cliHelper.js'; const directoryName = dirname(fileURLToPath(import.meta.url)); diff --git a/upgrade-test-scripts/agoric-upgrade-12/actions.test.js b/upgrade-test-scripts/agoric-upgrade-12/actions.test.js index aa985cf6..b6a6d162 100644 --- a/upgrade-test-scripts/agoric-upgrade-12/actions.test.js +++ b/upgrade-test-scripts/agoric-upgrade-12/actions.test.js @@ -1,9 +1,14 @@ import test from 'ava'; -import { agd, agoric, agops } from '../cliHelper.js'; -import { GOV1ADDR, SDK_ROOT } from '../constants.js'; +import { agd, agoric, agops } from '../lib/cliHelper.js'; +import { GOV1ADDR, SDK_ROOT } from '../lib/constants.js'; import { installBundles, runZcfUpgrade, runProber } from './actions.js'; -import { adjustVault, closeVault, mintIST, openVault } from '../econHelpers.js'; +import { + adjustVault, + closeVault, + mintIST, + openVault, +} from '../lib/econHelpers.js'; test.before(async t => { await mintIST(GOV1ADDR, 12340000000, 10000, 2000); diff --git a/upgrade-test-scripts/agoric-upgrade-12/post.test.js b/upgrade-test-scripts/agoric-upgrade-12/post.test.js index 3ac4cd61..8ba9f3d0 100644 --- a/upgrade-test-scripts/agoric-upgrade-12/post.test.js +++ b/upgrade-test-scripts/agoric-upgrade-12/post.test.js @@ -1,5 +1,5 @@ import test from 'ava'; -import { getIncarnation } from './tools/vat-status.js'; +import { getIncarnation } from '../lib/vat-status.js'; test(`verify Zoe vat incarnation`, async t => { const incarantion = await getIncarnation('zoe'); diff --git a/upgrade-test-scripts/agoric-upgrade-12/pre.test.js b/upgrade-test-scripts/agoric-upgrade-12/pre.test.js index 2e93afd2..85c2f280 100644 --- a/upgrade-test-scripts/agoric-upgrade-12/pre.test.js +++ b/upgrade-test-scripts/agoric-upgrade-12/pre.test.js @@ -1,7 +1,7 @@ import test from 'ava'; -import { waitForBlock } from '../commonUpgradeHelpers.js'; -import { getIncarnation } from './tools/vat-status.js'; +import { waitForBlock } from '../lib/commonUpgradeHelpers.js'; +import { getIncarnation } from '../lib/vat-status.js'; test.before(async () => { console.log('Wait for upgrade to settle'); diff --git a/upgrade-test-scripts/lib/agd-lib.js b/upgrade-test-scripts/lib/agd-lib.js new file mode 100644 index 00000000..8d2220e4 --- /dev/null +++ b/upgrade-test-scripts/lib/agd-lib.js @@ -0,0 +1,119 @@ +// @ts-check +// @jessie-check + +const { freeze } = Object; + +const agdBinary = 'agd'; + +/** @param {{ execFileSync: typeof import('child_process').execFileSync }} io */ +export const makeAgd = ({ execFileSync }) => { + console.warn('XXX is sync IO essential?'); + + /** @param {{ home?: string, keyringBackend?: string, rpcAddrs?: string[] }} keyringOpts */ + const make = ({ home, keyringBackend, rpcAddrs } = {}) => { + const keyringArgs = [ + ...(home ? ['--home', home] : []), + ...(keyringBackend ? [`--keyring-backend`, keyringBackend] : []), + ]; + console.warn('XXX: rpcAddrs after [0] are ignored'); + const nodeArgs = [...(rpcAddrs ? [`--node`, rpcAddrs[0]] : [])]; + + // TODO: verbose option + const l = a => { + console.log(a); // XXX unilateral logging by a library... iffy + return a; + }; + + /** + * @param {string[]} args + * @param {*} [opts] + */ + const exec = (args, opts) => execFileSync(agdBinary, args, opts).toString(); + + const outJson = ['--output', 'json']; + + const ro = freeze({ + status: async () => JSON.parse(exec([...nodeArgs, 'status'])), + /** + * @param { + * | [kind: 'tx', txhash: string] + * | [mod: 'vstorage', kind: 'data' | 'children', path: string] + * } qArgs + */ + query: async qArgs => { + const out = await exec(['query', ...qArgs, ...nodeArgs, ...outJson], { + stdio: ['ignore', 'pipe', 'ignore'], + }); + try { + return JSON.parse(out); + } catch (e) { + console.error(e); + console.info('output:', out); + } + }, + }); + const nameHub = freeze({ + /** + * @param {string[]} path + * NOTE: synchronous I/O + */ + lookup: (...path) => { + if (!Array.isArray(path)) { + // TODO: use COND || Fail`` + throw TypeError(); + } + if (path.length !== 1) { + throw Error(`path length limited to 1: ${path.length}`); + } + const [name] = path; + const txt = exec(['keys', 'show', `--address`, name, ...keyringArgs]); + return txt.trim(); + }, + }); + const rw = freeze({ + /** + * TODO: gas + * + * @param {string[]} txArgs + * @param {{ chainId: string, from: string, yes?: boolean }} opts + */ + tx: async (txArgs, { chainId, from, yes }) => { + const yesArg = yes ? ['--yes'] : []; + const args = [ + ...nodeArgs, + ...[`--chain-id`, chainId], + ...keyringArgs, + ...[`--from`, from], + 'tx', + ...txArgs, + ...['--broadcast-mode', 'block'], + ...yesArg, + ...outJson, + ]; + const out = exec(args); + try { + return JSON.parse(out); + } catch (e) { + console.error(e); + console.info('output:', out); + } + }, + ...ro, + ...nameHub, + readOnly: () => ro, + nameHub: () => nameHub, + keys: { + add: (name, mnemonic) => { + return execFileSync( + agdBinary, + [...keyringArgs, 'keys', 'add', name, '--recover'], + { input: mnemonic }, + ).toString(); + }, + }, + withOpts: opts => make({ home, keyringBackend, rpcAddrs, ...opts }), + }); + return rw; + }; + return make(); +}; diff --git a/upgrade-test-scripts/lib/assert.js b/upgrade-test-scripts/lib/assert.js new file mode 100644 index 00000000..27599462 --- /dev/null +++ b/upgrade-test-scripts/lib/assert.js @@ -0,0 +1,21 @@ +export const Fail = (template, ...args) => { + throw Error(String.raw(template, ...args.map(val => String(val)))); +}; + +export const assert = (cond, msg = 'check failed') => { + if (!cond) { + throw Error(msg); + } +}; + +assert.typeof = (val, type) => { + if (typeof val !== type) { + throw Error(`expected ${type}, got ${typeof val}`); + } +}; + +/** @type {(val: T | undefined) => T} */ +export const NonNullish = val => { + if (!val) throw Error('required'); + return val; +}; diff --git a/upgrade-test-scripts/cliHelper.js b/upgrade-test-scripts/lib/cliHelper.js similarity index 70% rename from upgrade-test-scripts/cliHelper.js rename to upgrade-test-scripts/lib/cliHelper.js index 4c3e0f4b..cb3c3adf 100644 --- a/upgrade-test-scripts/cliHelper.js +++ b/upgrade-test-scripts/lib/cliHelper.js @@ -129,3 +129,54 @@ export const bundleSource = async (filePath, bundleName) => { console.log(output.stderr); return `/tmp/bundle-${bundleName}.json`; }; + +export const wellKnownIdentities = async (io = {}) => { + const { agoric: { follow = agoric.follow } = {} } = io; + const zip = (xs, ys) => xs.map((x, i) => [x, ys[i]]); + const fromSmallCapsEntries = txt => { + const { body, slots } = JSON.parse(txt); + const theEntries = zip(JSON.parse(body.slice(1)), slots).map( + ([[name, ref], boardID]) => { + const iface = ref.replace(/^\$\d+\./, ''); + return [name, { iface, boardID }]; + }, + ); + return Object.fromEntries(theEntries); + }; + + const installation = fromSmallCapsEntries( + await follow('-lF', ':published.agoricNames.installation', '-o', 'text'), + ); + + const instance = fromSmallCapsEntries( + await follow('-lF', ':published.agoricNames.instance', '-o', 'text'), + ); + + const brand = fromSmallCapsEntries( + await follow('-lF', ':published.agoricNames.brand', '-o', 'text'), + ); + + return { brand, installation, instance }; +}; + +export const smallCapsContext = () => { + const slots = []; // XXX global mutable state + const smallCaps = { + Nat: n => `+${n}`, + // XXX mutates obj + ref: obj => { + if (obj.ix) return obj.ix; + const ix = slots.length; + slots.push(obj.boardID); + obj.ix = `$${ix}.Alleged: ${obj.iface}`; + return obj.ix; + }, + }; + + const toCapData = body => { + const capData = { body: `#${JSON.stringify(body)}`, slots }; + return JSON.stringify(capData); + }; + + return { smallCaps, toCapData }; +}; diff --git a/upgrade-test-scripts/commonUpgradeHelpers.js b/upgrade-test-scripts/lib/commonUpgradeHelpers.js similarity index 94% rename from upgrade-test-scripts/commonUpgradeHelpers.js rename to upgrade-test-scripts/lib/commonUpgradeHelpers.js index dd631468..2c7945d5 100644 --- a/upgrade-test-scripts/commonUpgradeHelpers.js +++ b/upgrade-test-scripts/lib/commonUpgradeHelpers.js @@ -177,16 +177,19 @@ export const voteLatestProposalAndWait = async () => { 'test', ); - let status = ''; - - do { - const proposalData = await agd.query('gov', 'proposal', lastProposalId); - status = proposalData.status; - console.log(`Waiting for proposal to pass (status=${status})`); - } while ( - status !== 'PROPOSAL_STATUS_REJECTED' && - status !== 'PROPOSAL_STATUS_PASSED' - ); + let info = {}; + for ( + ; + info.status !== 'PROPOSAL_STATUS_REJECTED' && + info.status !== 'PROPOSAL_STATUS_PASSED'; + await waitForBlock() + ) { + info = await agd.query('gov', 'proposal', lastProposalId); + console.log( + `Waiting for proposal ${lastProposalId} to pass (status=${info.status})`, + ); + } + return info; }; const Fail = (template, ...args) => { diff --git a/upgrade-test-scripts/constants.js b/upgrade-test-scripts/lib/constants.js similarity index 100% rename from upgrade-test-scripts/constants.js rename to upgrade-test-scripts/lib/constants.js diff --git a/upgrade-test-scripts/econHelpers.js b/upgrade-test-scripts/lib/econHelpers.js similarity index 92% rename from upgrade-test-scripts/econHelpers.js rename to upgrade-test-scripts/lib/econHelpers.js index ced1dff0..fb7f604d 100644 --- a/upgrade-test-scripts/econHelpers.js +++ b/upgrade-test-scripts/lib/econHelpers.js @@ -54,7 +54,7 @@ export const closeVault = (address, vaultId, mint) => { ); }; -export const mintIST = async (addr, sendValue, giveCollateral, wantMinted) => { +export const mintIST = async (addr, sendValue, wantMinted, giveCollateral) => { await agd.tx( 'bank', 'send', @@ -69,5 +69,5 @@ export const mintIST = async (addr, sendValue, giveCollateral, wantMinted) => { 'test', '--yes', ); - await openVault(addr, giveCollateral, wantMinted); + await openVault(addr, wantMinted, giveCollateral); }; diff --git a/upgrade-test-scripts/lib/unmarshal.js b/upgrade-test-scripts/lib/unmarshal.js new file mode 100644 index 00000000..f4f56b61 --- /dev/null +++ b/upgrade-test-scripts/lib/unmarshal.js @@ -0,0 +1,143 @@ +// @ts-check +'use strict'; + +const { + create, + entries, + fromEntries, + freeze, + keys, + setPrototypeOf, + prototype: objectPrototype, +} = Object; +const { isArray } = Array; + +const sigilDoc = { + '!': 'escaped string', + '+': `non-negative bigint`, + '-': `negative bigint`, + '#': `manifest constant`, + '%': `symbol`, + $: `remotable`, + '&': `promise`, +}; +const sigils = keys(sigilDoc).join(''); + +/** @type {(obj: Record, f: (v: V) => U) => Record} */ +const objMap = (obj, f) => + fromEntries(entries(obj).map(([p, v]) => [f(p), f(v)])); + +const { freeze: harden } = Object; // XXX + +const makeMarshal = (_v2s, convertSlotToVal = (s, _i) => s) => { + const fromCapData = ({ body, slots }) => { + const recur = v => { + switch (typeof v) { + case 'boolean': + case 'number': + return v; + case 'string': + if (v === '') return v; + const sigil = v.slice(0, 1); + if (!sigils.includes(sigil)) return v; + switch (sigil) { + case '!': + return v.slice(1); + case '+': + return BigInt(v.slice(1)); + case '-': + return -BigInt(v.slice(1)); + case '$': { + const [ix, iface] = v.slice(1).split('.'); + return convertSlotToVal(slots[Number(ix)], iface); + } + case '#': + switch (v) { + case '#undefined': + return undefined; + case '#Infinity': + return Infinity; + case '#NaN': + return Infinity; + default: + throw RangeError(`Unexpected constant ${v}`); + } + case '%': + // TODO: @@asyncIterator + return Symbol.for(v.slice(1)); + default: + throw RangeError(`Unexpected sigil ${sigil}`); + } + case 'object': + if (v === null) return v; + if (isArray(v)) { + return freeze(v.map(recur)); + } + return freeze(objMap(v, recur)); + default: + throw RangeError(`Unexpected value type ${typeof v}`); + } + }; + const encoding = JSON.parse(body.replace(/^#/, '')); + return recur(encoding); + }; + + const toCapData = () => { + throw Error('not implemented'); + }; + + return harden({ + fromCapData, + unserialize: fromCapData, + toCapData, + serialize: toCapData, + }); +}; + +const PASS_STYLE = Symbol.for('passStyle'); +export const Far = (iface, methods) => { + const proto = freeze( + create(objectPrototype, { + [PASS_STYLE]: { value: 'remotable' }, + [Symbol.toStringTag]: { value: iface }, + }), + ); + setPrototypeOf(methods, proto); + freeze(methods); + return methods; +}; + +// #region marshal-table +const makeSlot1 = (val, serial) => { + const prefix = Promise.resolve(val) === val ? 'promise' : 'object'; + return `${prefix}${serial}`; +}; + +const makeTranslationTable = (makeSlot, makeVal) => { + const valToSlot = new Map(); + const slotToVal = new Map(); + + const convertValToSlot = val => { + if (valToSlot.has(val)) return valToSlot.get(val); + const slot = makeSlot(val, valToSlot.size); + valToSlot.set(val, slot); + slotToVal.set(slot, val); + return slot; + }; + + const convertSlotToVal = (slot, iface) => { + if (slotToVal.has(slot)) return slotToVal.get(slot); + if (makeVal) { + const val = makeVal(slot, iface); + valToSlot.set(val, slot); + slotToVal.set(slot, val); + return val; + } + throw Error(`no such ${iface}: ${slot}`); + }; + + return harden({ convertValToSlot, convertSlotToVal }); +}; +// #endregion marshal-table + +export { makeMarshal, makeTranslationTable }; diff --git a/upgrade-test-scripts/agoric-upgrade-12/tools/vat-status.js b/upgrade-test-scripts/lib/vat-status.js similarity index 97% rename from upgrade-test-scripts/agoric-upgrade-12/tools/vat-status.js rename to upgrade-test-scripts/lib/vat-status.js index 81518a53..cd968911 100644 --- a/upgrade-test-scripts/agoric-upgrade-12/tools/vat-status.js +++ b/upgrade-test-scripts/lib/vat-status.js @@ -1,6 +1,6 @@ // @ts-check import dbOpenAmbient from 'better-sqlite3'; -import { HOME } from '../../constants.js'; +import { HOME } from './constants.js'; /** * @file look up vat incarnation from kernel DB @@ -14,7 +14,7 @@ const swingstorePath = '~/.agoric/data/agoric/swingstore.sqlite'; * * @param {import('better-sqlite3').Database} db */ -const dbTool = db => { +export const dbTool = db => { const prepare = (strings, ...params) => { const dml = strings.join('?'); return { stmt: db.prepare(dml), params }; diff --git a/upgrade-test-scripts/lib/vstorage.js b/upgrade-test-scripts/lib/vstorage.js new file mode 100644 index 00000000..cd1437d4 --- /dev/null +++ b/upgrade-test-scripts/lib/vstorage.js @@ -0,0 +1,42 @@ +// @ts-check +/* global Buffer */ +import { assert, Fail } from './assert.js'; + +const { freeze: harden } = Object; // XXX + +// from '@agoric/internal/src/lib-chainStorage.js'; +const isStreamCell = cell => + cell && + typeof cell === 'object' && + Array.isArray(cell.values) && + typeof cell.blockHeight === 'string' && + /^0$|^[1-9][0-9]*$/.test(cell.blockHeight); +harden(isStreamCell); + +/** + * Extract one value from a the vstorage stream cell in a QueryDataResponse + * + * @param {object} data + * @param {number} [index] index of the desired value in a deserialized stream cell + * + * XXX import('@agoric/cosmic-proto/vstorage/query').QueryDataResponse doesn't worksomehow + * @typedef {Awaited>} QueryDataResponseT + */ +export const extractStreamCellValue = (data, index = -1) => { + const { value: serialized } = data; + + serialized.length > 0 || Fail`no StreamCell values: ${data}`; + + const streamCell = JSON.parse(serialized); + if (!isStreamCell(streamCell)) { + throw Fail`not a StreamCell: ${streamCell}`; + } + + const { values } = streamCell; + values.length > 0 || Fail`no StreamCell values: ${streamCell}`; + + const value = values.at(index); + assert.typeof(value, 'string'); + return value; +}; +harden(extractStreamCellValue); diff --git a/upgrade-test-scripts/lib/webAsset.js b/upgrade-test-scripts/lib/webAsset.js new file mode 100644 index 00000000..bb9514d7 --- /dev/null +++ b/upgrade-test-scripts/lib/webAsset.js @@ -0,0 +1,200 @@ +// @ts-check +import { tmpName } from 'tmp'; + +const dbg = label => x => { + label; + // console.log(label, x); + return x; +}; + +/** + * + * @param {string} root + * @param {{ fetch: typeof fetch }} io + * + * @typedef {ReturnType} TextRd + */ +export const makeWebRd = (root, { fetch }) => { + /** @param {string} there */ + const make = there => { + const join = (...segments) => { + dbg('web.join')({ there, segments }); + let out = there; + for (const segment of segments) { + out = `${new URL(segment, out)}`; + } + return out; + }; + const self = { + toString: () => there, + /** @param {string[]} segments */ + join: (...segments) => make(join(...segments)), + readText: async () => { + console.log('WebRd fetch:', there); + const res = await fetch(there); + if (!res.ok) { + throw Error(`${res.statusText} @ ${there}`); + } + return res.text(); + }, + }; + return self; + }; + return make(root); +}; + +/** + * Reify file read access as an object. + * + * @param {string} root + * @param {object} io + * @param {Pick} io.fsp + * @param {Pick} io.path + * + * @typedef {ReturnType} FileRd + */ +export const makeFileRd = (root, { fsp, path }) => { + /** @param {string} there */ + const make = there => { + const self = { + toString: () => there, + /** @param {string[]} segments */ + join: (...segments) => make(path.join(there, ...segments)), + stat: () => fsp.stat(there), + readText: () => fsp.readFile(there, 'utf8'), + }; + return self; + }; + return make(root); +}; + +/** + * Reify file read/write access as an object. + * + * @param {string} root + * @param {object} io + * @param {Pick} io.fsp + * @param {Pick} io.path + * + * @typedef {ReturnType} FileRW + */ +export const makeFileRW = (root, { fsp, path }) => { + /** @param {string} there */ + const make = there => { + const ro = makeFileRd(there, { fsp, path }); + const self = { + toString: () => there, + readOnly: () => ro, + /** @param {string[]} segments */ + join: (...segments) => + make(dbg('FileRW join')(path.join(there, ...segments))), + writeText: text => fsp.writeFile(there, text, 'utf8'), + unlink: () => fsp.unlink(there), + mkdir: () => fsp.mkdir(there, { recursive: true }), + rmdir: () => fsp.rmdir(there), + }; + return self; + }; + return make(root); +}; + +/** + * @param {TextRd} src + * @param {FileRW} dest + * + * @typedef {ReturnType} WebCache + */ +export const makeWebCache = (src, dest) => { + /** @type {Map>} */ + const saved = new Map(); + + /** @param {string} segment */ + const getFileP = segment => { + const target = src.join(segment); + const addr = `${target}`; + const cached = saved.get(addr); + if (cached) return cached; + + const f = dest.join(segment); + /** @type {Promise} */ + const p = new Promise((resolve, reject) => + target + .readText() + .then(txt => + dest + .mkdir() + .then(() => f.writeText(txt).then(_ => resolve(f.readOnly()))), + ) + .catch(reject), + ); + saved.set(addr, p); + return p; + }; + + const remove = async () => { + await Promise.all([...saved.values()].map(p => p.then(f => f.unlink()))); + await dest.rmdir(); + }; + + const self = { + toString: () => `${src} -> ${dest}`, + /** @param {string} segment */ + getText: async segment => { + const fr = await getFileP(segment); + return fr.readText(); + }, + /** @param {string} segment */ + storedPath: segment => getFileP(segment).then(f => f.toString()), + /** @param {string} segment */ + size: async segment => { + const fr = await getFileP(segment); + const info = await fr.stat(); + return info.size; + }, + remove, + }; + return self; +}; + +const buildInfo = [ + { + evals: [ + { + permit: 'kread-invite-committee-permit.json', + script: 'kread-invite-committee.js', + }, + ], + bundles: [ + 'b1-51085a4ad4ac3448ccf039c0b54b41bd11e9367dfbd641deda38e614a7f647d7f1c0d34e55ba354d0331b1bf54c999fca911e6a796c90c30869f7fb8887b3024.json', + 'b1-a724453e7bfcaae1843be4532e18c1236c3d6d33bf6c44011f2966e155bc7149b904573014e583fdcde2b9cf2913cb8b337fc9daf79c59a38a37c99030fcf7dc.json', + ], + }, + { + evals: [{ permit: 'start-kread-permit.json', script: 'start-kread.js' }], + bundles: [ + '/Users/wietzes/.agoric/cache/b1-853acd6ba3993f0f19d6c5b0a88c9a722c9b41da17cf7f98ff7705e131860c4737d7faa758ca2120773632dbaf949e4bcce2a2cbf2db224fa09cd165678f64ac.json', + '/Users/wietzes/.agoric/cache/b1-0c3363b8737677076e141a84b84c8499012f6ba79c0871fc906c8be1bb6d11312a7d14d5a3356828a1de6baa4bee818a37b7cb1ca2064f6eecbabc0a40d28136.json', + ], + }, +]; + +const main = async () => { + const td = await new Promise((resolve, reject) => + tmpName({ prefix: 'assets' }, (err, x) => (err ? reject(err) : resolve(x))), + ); + const src = makeWebRd( + 'https://github.com/Kryha/KREAd/releases/download/KREAd-rc1/', + { fetch }, + ); + const fsp = await import('fs/promises'); + const path = await import('path'); + const dest = makeFileRW(td, { fsp, path }); + const assets = makeWebCache(src, dest); + const segment = buildInfo[0].bundles[0]; + const info = await assets.size(segment); + console.log(`${segment}:`, info); +}; + +// main().catch(err => console.error(err)); diff --git a/upgrade-test-scripts/package.json b/upgrade-test-scripts/package.json index 90044fe9..f252c62e 100644 --- a/upgrade-test-scripts/package.json +++ b/upgrade-test-scripts/package.json @@ -2,9 +2,11 @@ "type": "module", "devDependencies": {}, "dependencies": { + "@endo/zip": "^0.2.35", "ava": "^5.3.1", "better-sqlite3": "^8.5.1", - "execa": "^7.2.0" + "execa": "^7.2.0", + "tmp": "^0.2.1" }, "scripts": { "agops": "yarn --cwd /usr/src/agoric-sdk/ --silent agops", From a14bec28f46c2a9a2808dc6cd4f70c9e8aebe711 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 26 Oct 2023 17:25:17 -0500 Subject: [PATCH 2/7] chore: punt on MN2= volume --- Makefile | 1 - upgrade-test-scripts/agoric-upgrade-11/README.md | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index ef7baf4d..d9f1e3ef 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,6 @@ DEBUG ?= SwingSet:ls,SwingSet:vat RUN = docker run --rm -it \ -p 26656:26656 -p 26657:26657 -p 1317:1317 \ -v "$${PWD}:/workspace" \ - -v "$${MN2}:/mn2" \ -e "DEBUG=$(DEBUG)" run: diff --git a/upgrade-test-scripts/agoric-upgrade-11/README.md b/upgrade-test-scripts/agoric-upgrade-11/README.md index 5cd9ac31..99f9a2f0 100644 --- a/upgrade-test-scripts/agoric-upgrade-11/README.md +++ b/upgrade-test-scripts/agoric-upgrade-11/README.md @@ -4,7 +4,9 @@ 2. 11wf with walletFactory 3. 11kr with kreadKit -# Testing 11kr +# Testing 11kr - WIP + +*Note: MN2= volume vs. fetching from github releases is in flux.* Related: https://github.com/agoric-labs/KREAd/releases/tag/gryo-rc0 From 158d7dd9db10cd00aad71d275125367e665690f4 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 26 Oct 2023 17:26:27 -0500 Subject: [PATCH 3/7] chore: agops-bin, gov-cmd not needed --- .../agoric-upgrade-11/README.md | 2 +- .../agoric-upgrade-11/agops-bin | 89 ---- .../agoric-upgrade-11/gov-cmd | 409 ------------------ 3 files changed, 1 insertion(+), 499 deletions(-) delete mode 100644 upgrade-test-scripts/agoric-upgrade-11/agops-bin delete mode 100644 upgrade-test-scripts/agoric-upgrade-11/gov-cmd diff --git a/upgrade-test-scripts/agoric-upgrade-11/README.md b/upgrade-test-scripts/agoric-upgrade-11/README.md index 99f9a2f0..1353d666 100644 --- a/upgrade-test-scripts/agoric-upgrade-11/README.md +++ b/upgrade-test-scripts/agoric-upgrade-11/README.md @@ -44,7 +44,7 @@ Then in a new shell do the following. rm -rf upgrade-test-scripts ln -s /workspace/upgrade-test-scripts -# Patch agoric-cli +# Patch agoric-cli - WIP - outdated??? cp upgrade-test-scripts/agoric-upgrade-11/gov-cmd packages/agoric-cli/src/commands/gov.js cp upgrade-test-scripts/agoric-upgrade-11/agops-bin packages/agoric-cli/src/bin-agops.js ## this should be docker-u11 diff --git a/upgrade-test-scripts/agoric-upgrade-11/agops-bin b/upgrade-test-scripts/agoric-upgrade-11/agops-bin deleted file mode 100644 index bad024b4..00000000 --- a/upgrade-test-scripts/agoric-upgrade-11/agops-bin +++ /dev/null @@ -1,89 +0,0 @@ -#!/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/upgrade-test-scripts/agoric-upgrade-11/gov-cmd b/upgrade-test-scripts/agoric-upgrade-11/gov-cmd deleted file mode 100644 index 4796f01d..00000000 --- a/upgrade-test-scripts/agoric-upgrade-11/gov-cmd +++ /dev/null @@ -1,409 +0,0 @@ -// 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; -}; From b6bb47e769b1d5da968a7a6d4521af4fee87d2d6 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Fri, 27 Oct 2023 11:51:41 -0500 Subject: [PATCH 4/7] chore: run KREAd, stATOM tests as well --- upgrade-test-scripts/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/upgrade-test-scripts/package.json b/upgrade-test-scripts/package.json index f252c62e..85116d7a 100644 --- a/upgrade-test-scripts/package.json +++ b/upgrade-test-scripts/package.json @@ -10,7 +10,9 @@ }, "scripts": { "agops": "yarn --cwd /usr/src/agoric-sdk/ --silent agops", - "upgrade-tests": "ava --serial agoric-upgrade*/**/pre.test.js agoric-upgrade*/**/actions.test.js agoric-upgrade*/**/post.test.js" + "upgrade-tests-1": "ava --serial agoric-upgrade*/**/pre.test.js agoric-upgrade*/**/actions.test.js agoric-upgrade*/**/post.test.js", + "upgrade-tests-2": "if [ -d agoric-upgrade-11 ]; then ava --serial agoric-upgrade-11/mn2.test.js agoric-upgrade-11/add-collateral.test.js; fi", + "upgrade-tests": "yarn run upgrade-tests-1 && yarn run upgrade-tests-2" }, "ava": { "files": [ From 6ceba04a3db830567e1ac436fcaddfd0aa7f7975 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 31 Oct 2023 10:28:57 -0500 Subject: [PATCH 5/7] test(Open Vaults): accomodate extra vault for minting IST for uploading bundles --- upgrade-test-scripts/agoric-upgrade-12/actions.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/upgrade-test-scripts/agoric-upgrade-12/actions.test.js b/upgrade-test-scripts/agoric-upgrade-12/actions.test.js index b6a6d162..8f3424ae 100644 --- a/upgrade-test-scripts/agoric-upgrade-12/actions.test.js +++ b/upgrade-test-scripts/agoric-upgrade-12/actions.test.js @@ -34,7 +34,7 @@ test.before(async t => { test.serial('Open Vaults', async t => { const currentVaults = await agops.vaults('list', '--from', GOV1ADDR); - t.is(currentVaults.length, 4); + t.true(currentVaults.length >= 4); await openVault(GOV1ADDR, 7, 11); await adjustVault(GOV1ADDR, 'vault5', { giveMinted: 1.5 }); From 81a8b4cc73f0d77f15f610e1d746aae37a5732de Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 31 Oct 2023 10:29:59 -0500 Subject: [PATCH 6/7] test(add-collateral): postpone price feed test (WIP) pending synchronization --- upgrade-test-scripts/agoric-upgrade-11/add-collateral.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/upgrade-test-scripts/agoric-upgrade-11/add-collateral.test.js b/upgrade-test-scripts/agoric-upgrade-11/add-collateral.test.js index 8b1688b6..06225faa 100644 --- a/upgrade-test-scripts/agoric-upgrade-11/add-collateral.test.js +++ b/upgrade-test-scripts/agoric-upgrade-11/add-collateral.test.js @@ -333,7 +333,8 @@ test.serial('priceAuthority installation was not changed', async t => { t.deepEqual(actual, expected); }); -test('stATOM-USD price feed instance in agoricNames', async t => { +// needs synchronization +test.skip('stATOM-USD price feed instance in agoricNames', async t => { const { agoric } = t.context; const { instance } = await wellKnownIdentities({ agoric }); testIncludes(t, 'stATOM-USD price feed', Object.keys(instance), 'instance'); From 7fd48416af82d460b270d65e6a16f3582451e5b4 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Wed, 1 Nov 2023 16:36:20 -0500 Subject: [PATCH 7/7] chore: use separate accounts for installing bundles from vaults testing --- upgrade-test-scripts/agoric-upgrade-11/add-collateral.test.js | 2 +- upgrade-test-scripts/agoric-upgrade-11/mn2-start.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/upgrade-test-scripts/agoric-upgrade-11/add-collateral.test.js b/upgrade-test-scripts/agoric-upgrade-11/add-collateral.test.js index 06225faa..ef250c1c 100644 --- a/upgrade-test-scripts/agoric-upgrade-11/add-collateral.test.js +++ b/upgrade-test-scripts/agoric-upgrade-11/add-collateral.test.js @@ -67,7 +67,7 @@ const assetInfo = { const staticConfig = { deposit: '10000000ubld', // 10 BLD - installer: 'gov1', // as in: agd keys show gov1 + installer: 'gov2', // as in: agd keys show gov2 proposer: 'validator', collateralPrice: 6, // conservatively low price. TODO: look up swingstorePath: '~/.agoric/data/agoric/swingstore.sqlite', diff --git a/upgrade-test-scripts/agoric-upgrade-11/mn2-start.test.js b/upgrade-test-scripts/agoric-upgrade-11/mn2-start.test.js index 4dd831da..e844cb9a 100644 --- a/upgrade-test-scripts/agoric-upgrade-11/mn2-start.test.js +++ b/upgrade-test-scripts/agoric-upgrade-11/mn2-start.test.js @@ -97,7 +97,7 @@ const dappAPI = { const staticConfig = { deposit: '10000000ubld', // 10 BLD - installer: 'gov1', // as in: agd keys show gov1 + installer: 'gov2', // as in: agd keys show gov2 proposer: 'validator', collateralPrice: 6, // conservatively low price. TODO: look up swingstorePath: '~/.agoric/data/agoric/swingstore.sqlite',