diff --git a/a3p-integration/proposals/p:upgrade-19/mint-payment/send-script-permit.json b/a3p-integration/proposals/p:upgrade-19/mint-payment/send-script-permit.json new file mode 100644 index 00000000000..27ba77ddaf6 --- /dev/null +++ b/a3p-integration/proposals/p:upgrade-19/mint-payment/send-script-permit.json @@ -0,0 +1 @@ +true diff --git a/a3p-integration/proposals/p:upgrade-19/mint-payment/send-script.tjs b/a3p-integration/proposals/p:upgrade-19/mint-payment/send-script.tjs new file mode 100644 index 00000000000..eb1a4815470 --- /dev/null +++ b/a3p-integration/proposals/p:upgrade-19/mint-payment/send-script.tjs @@ -0,0 +1,56 @@ +/* global E */ + +/// +/// + +/** + * The primary purpose of this script is to mint a payment of a certain + * bankAsset and deposit in an user wallet. + * + * The receiverAddress and label placeholders should be replaced with + * the desired address and asset name during the execution of each test case. + * + * See z:acceptance/mintHolder.test.js + * + * @param {BootstrapPowers} powers + */ +const sendBankAsset = async powers => { + const { + consume: { namesByAddress, contractKits: contractKitsP }, + } = powers; + + const receiverAddress = '{{ADDRESS}}'; + const label = '{{LABEL}}'; + const valueStr = '{{VALUE}}'; + const value = BigInt(valueStr) + + console.log(`Start sendBankAsset for ${label}`); + + const contractKits = await contractKitsP; + const mintHolderKit = Array.from(contractKits.values()).filter( + kit => kit.label && kit.label === label, + ); + + const { creatorFacet: mint, publicFacet: issuer } = mintHolderKit[0]; + + /* + * Ensure that publicFacet holds an issuer by verifying that has + * the makeEmptyPurse method. + */ + await E(issuer).makeEmptyPurse() + + const brand = await E(issuer).getBrand(); + const amount = harden({ value, brand }); + const payment = await E(mint).mintPayment(amount); + + const receiverDepositFacet = E(namesByAddress).lookup( + receiverAddress, + 'depositFacet', + ); + + await E(receiverDepositFacet).receive(payment); + + console.log(`Finished sendBankAsset for ${label}`); +}; + +sendBankAsset; diff --git a/a3p-integration/proposals/p:upgrade-19/mintHolder.test.js b/a3p-integration/proposals/p:upgrade-19/mintHolder.test.js new file mode 100644 index 00000000000..b47a499411e --- /dev/null +++ b/a3p-integration/proposals/p:upgrade-19/mintHolder.test.js @@ -0,0 +1,32 @@ +/* eslint-env node */ + +import '@endo/init'; +import test from 'ava'; +import { addUser, provisionSmartWallet } from '@agoric/synthetic-chain'; +import { + mintPayment, + getAssetList, + swap, + getPSMChildren, + upgradeMintHolder, +} from './test-lib/mintHolder-helpers.js'; + +const networkConfig = { + rpcAddrs: ['http://0.0.0.0:26657'], + chainName: 'agoriclocal', +}; + +test('mintHolder contract is upgraded', async t => { + const receiver = await addUser('receiver'); + await provisionSmartWallet(receiver, `20000000ubld`); + + let assetList = await getAssetList(); + t.log('List of mintHolder vats being upgraded: ', assetList); + await upgradeMintHolder(`upgrade-mintHolder`, assetList); + await mintPayment(t, receiver, assetList, 10); + + const psmLabelList = await getPSMChildren(fetch, networkConfig); + assetList = await getAssetList(psmLabelList); + t.log('List of assets being swapped with IST via PSM: ', assetList); + await swap(t, receiver, assetList, 5); +}); diff --git a/a3p-integration/proposals/p:upgrade-19/test-lib/mintHolder-helpers.js b/a3p-integration/proposals/p:upgrade-19/test-lib/mintHolder-helpers.js new file mode 100644 index 00000000000..131de77d30e --- /dev/null +++ b/a3p-integration/proposals/p:upgrade-19/test-lib/mintHolder-helpers.js @@ -0,0 +1,156 @@ +/* eslint-env node */ + +import { + agoric, + evalBundles, + getDetailsMatchingVats, + getISTBalance, +} from '@agoric/synthetic-chain'; +import { makeVstorageKit, retryUntilCondition } from '@agoric/client-utils'; +import { readFile, writeFile } from 'node:fs/promises'; +import { sendOfferAgd, psmSwap, snapshotAgoricNames } from './psm-helpers.js'; + +/** + * @param {string} fileName base file name without .tjs extension + * @param {Record} replacements + */ +export const replaceTemplateValuesInFile = async (fileName, replacements) => { + let script = await readFile(`${fileName}.tjs`, 'utf-8'); + for (const [template, value] of Object.entries(replacements)) { + script = script.replaceAll(`{{${template}}}`, value); + } + await writeFile(`${fileName}.js`, script); +}; + +export const getPSMChildren = async (fetch, networkConfig) => { + const { + vstorage: { keys }, + } = await makeVstorageKit({ fetch }, networkConfig); + + const children = await keys('published.psm.IST'); + + return children; +}; + +export const getAssetList = async labelList => { + const assetList = []; + const { vbankAssets } = await snapshotAgoricNames(); + + // Determine the assets to consider based on labelList + const assetsToConsider = + labelList || Object.values(vbankAssets).map(asset => asset.issuerName); + + for (const label of assetsToConsider) { + if (label === 'IST') { + break; + } + + const vbankAsset = Object.values(vbankAssets).find( + asset => asset.issuerName === label, + ); + assert(vbankAsset, `vbankAsset not found for ${label}`); + + const { denom } = vbankAsset; + const mintHolderVat = `zcf-mintHolder-${label}`; + + assetList.push({ label, denom, mintHolderVat }); + } + + return assetList; +}; + +export const mintPayment = async (t, address, assetList, value) => { + const SUBMISSION_DIR = 'mint-payment'; + + for (const asset of assetList) { + const { label, denom } = asset; + const scaled = BigInt(parseInt(value, 10) * 1_000_000).toString(); + + await replaceTemplateValuesInFile(`${SUBMISSION_DIR}/send-script`, { + ADDRESS: address, + LABEL: label, + VALUE: scaled, + }); + + await evalBundles(SUBMISSION_DIR); + + const balance = await getISTBalance(address, denom); + + // Add to value the BLD provisioned to smart wallet + if (label === 'BLD') { + value += 10; + } + + t.is( + balance, + value, + `receiver ${denom} balance ${balance} is not ${value}`, + ); + } +}; + +export const swap = async (t, address, assetList, want) => { + for (const asset of assetList) { + const { label, denom } = asset; + + // TODO: remove condition after fixing issue #10655 + if (/^DAI/.test(label)) { + break; + } + + const pair = `IST.${label}`; + + const istBalanceBefore = await getISTBalance(address, 'uist'); + const anchorBalanceBefore = await getISTBalance(address, denom); + + await psmSwap(address, ['swap', '--pair', pair, '--wantMinted', want], { + now: Date.now, + follow: agoric.follow, + setTimeout, + sendOffer: sendOfferAgd, + }); + + const istBalanceAfter = await getISTBalance(address, 'uist'); + const anchorBalanceAfter = await getISTBalance(address, denom); + + t.is(istBalanceAfter, istBalanceBefore + want); + t.is(anchorBalanceAfter, anchorBalanceBefore - want); + } +}; + +const getIncarnationForAllVats = async assetList => { + const vatsIncarnation = {}; + + for (const asset of assetList) { + const { label, mintHolderVat } = asset; + const matchingVats = await getDetailsMatchingVats(label); + const expectedVat = matchingVats.find(vat => vat.vatName === mintHolderVat); + vatsIncarnation[label] = expectedVat.incarnation; + } + assert(Object.keys(vatsIncarnation).length === assetList.length); + + return vatsIncarnation; +}; + +const checkVatsUpgraded = (before, current) => { + for (const vatLabel in before) { + if (current[vatLabel] !== before[vatLabel] + 1) { + console.log(`${vatLabel} upgrade failed. `); + return false; + } + } + return true; +}; + +export const upgradeMintHolder = async (submissionPath, assetList) => { + const before = await getIncarnationForAllVats(assetList); + + await evalBundles(submissionPath); + + return retryUntilCondition( + async () => getIncarnationForAllVats(assetList), + current => checkVatsUpgraded(before, current), + `mintHolder upgrade not processed yet`, + { setTimeout, retryIntervalMs: 5000, maxRetries: 15 }, + ); +}; diff --git a/a3p-integration/proposals/p:upgrade-19/test-lib/psm-helpers.js b/a3p-integration/proposals/p:upgrade-19/test-lib/psm-helpers.js new file mode 100644 index 00000000000..954e954941c --- /dev/null +++ b/a3p-integration/proposals/p:upgrade-19/test-lib/psm-helpers.js @@ -0,0 +1,192 @@ +/* eslint-env node */ + +import { execa } from 'execa'; +import { + boardSlottingMarshaller, + makeFromBoard, + waitUntilOfferResult, + fetchEnvNetworkConfig, +} from '@agoric/client-utils'; +import { + agopsLocation, + agoric, + executeCommand, + mkTemp, +} from '@agoric/synthetic-chain'; +import fsp from 'node:fs/promises'; + +/** @import {Result as ExecaResult, ExecaError} from 'execa'; */ +/** + * @typedef {ExecaResult & { all: string } & ( + * | { failed: false } + * | Pick< + * ExecaError & { failed: true }, + * | 'failed' + * | 'shortMessage' + * | 'cause' + * | 'exitCode' + * | 'signal' + * | 'signalDescription' + * > + * )} SendOfferResult + */ + +/** + * @typedef {object} PsmMetrics + * @property {import('@agoric/ertp').Amount<'nat'>} anchorPoolBalance + * @property {import('@agoric/ertp').Amount<'nat'>} feePoolBalance + * @property {import('@agoric/ertp').Amount<'nat'>} mintedPoolBalance + * @property {import('@agoric/ertp').Amount<'nat'>} totalAnchorProvided + * @property {import('@agoric/ertp').Amount<'nat'>} totalMintedProvided + */ + +const fromBoard = makeFromBoard(); +const marshaller = boardSlottingMarshaller(fromBoard.convertSlotToVal); + +/** + * @param {string} path + */ +const objectFromVstorageEntries = async path => { + const rawEntries = await agoric.follow('-lF', `:${path}`, '-o', 'text'); + return Object.fromEntries(marshaller.fromCapData(JSON.parse(rawEntries))); +}; + +export const snapshotAgoricNames = async () => { + const [brands, instances, vbankAssets] = await Promise.all([ + objectFromVstorageEntries('published.agoricNames.brand'), + objectFromVstorageEntries('published.agoricNames.instance'), + objectFromVstorageEntries('published.agoricNames.vbankAsset'), + ]); + return { brands, instances, vbankAssets }; +}; + +/** + * Similar to + * https://github.com/Agoric/agoric-3-proposals/blob/422b163fecfcf025d53431caebf6d476778b5db3/packages/synthetic-chain/src/lib/commonUpgradeHelpers.ts#L123-L139 + * However, for an address that is not provisioned, `agoric wallet send` is + * needed because `agops perf satisfaction` hangs when trying to follow + * nonexistent vstorage path ":published.wallet.${address}". + * + * @param {string} address + * @param {Promise} offerPromise + * @returns {Promise} + */ +export const sendOfferAgoric = async (address, offerPromise) => { + const offerPath = await mkTemp('agops.XXX'); + const offer = await offerPromise; + await fsp.writeFile(offerPath, offer); + + const [settlement] = await Promise.allSettled([ + execa({ + all: true, + })`agoric wallet --keyring-backend=test send --offer ${offerPath} --from ${address} --verbose`, + ]); + return settlement.status === 'fulfilled' + ? settlement.value + : settlement.reason; +}; + +/** + * A variant of {@link sendOfferAgoric} that uses `agd` directly to e.g. + * control gas calculation. + * + * @param {string} address + * @param {Promise} offerPromise + * @returns {Promise} + */ +export const sendOfferAgd = async (address, offerPromise) => { + const offer = await offerPromise; + const networkConfig = await fetchEnvNetworkConfig({ + env: process.env, + fetch, + }); + const { chainName, rpcAddrs } = networkConfig; + const args = /** @type {string[]} */ ( + // @ts-expect-error heterogeneous concat + [].concat( + [`--node=${rpcAddrs[0]}`, `--chain-id=${chainName}`], + [`--keyring-backend=test`, `--from=${address}`], + ['tx', 'swingset', 'wallet-action', '--allow-spend', offer], + '--yes', + '-bblock', + '-ojson', + ) + ); + + const [settlement] = await Promise.allSettled([ + execa('agd', args, { all: true }), + ]); + + // Upon successful exit, verify that the *output* also indicates success. + // cf. https://github.com/Agoric/agoric-sdk/blob/master/packages/agoric-cli/src/lib/wallet.js + if (settlement.status === 'fulfilled') { + const result = settlement.value; + try { + const tx = JSON.parse(result.stdout); + if (tx.code !== 0) { + return { ...result, failed: true, shortMessage: `code ${tx.code}` }; + } + } catch (err) { + return { + ...result, + failed: true, + shortMessage: 'unexpected output', + cause: err, + }; + } + } + + return settlement.status === 'fulfilled' + ? settlement.value + : settlement.reason; +}; + +/** + * @param {string} address + * @param {Array} params + * @param {{ + * follow: (...params: string[]) => Promise; + * sendOffer?: (address: string, offerPromise: Promise) => Promise; + * setTimeout: typeof global.setTimeout; + * now: () => number + * }} io + */ +export const psmSwap = async (address, params, io) => { + const { now, sendOffer = sendOfferAgoric, ...waitIO } = io; + const offerId = `${address}-psm-swap-${now()}`; + const newParams = ['psm', ...params, '--offerId', offerId]; + const offerPromise = executeCommand(agopsLocation, newParams); + const sendResult = await sendOffer(address, offerPromise); + if (sendResult.failed) { + const { + command, + durationMs, + shortMessage, + cause, + exitCode, + signal, + signalDescription, + all: output, + } = sendResult; + const summary = { + command, + durationMs, + shortMessage, + cause, + exitCode, + signal, + signalDescription, + output, + }; + console.error('psmSwap tx send failed', summary); + throw Error( + `psmSwap tx send failed: ${JSON.stringify({ exitCode, signal, signalDescription })}`, + { cause }, + ); + } + console.log('psmSwap tx send results', sendResult.all); + + await waitUntilOfferResult(address, offerId, true, waitIO, { + errorMessage: `${offerId} not succeeded`, + }); +}; diff --git a/a3p-integration/proposals/p:upgrade-19/test.sh b/a3p-integration/proposals/p:upgrade-19/test.sh index aa766db3a28..f42147483ef 100644 --- a/a3p-integration/proposals/p:upgrade-19/test.sh +++ b/a3p-integration/proposals/p:upgrade-19/test.sh @@ -2,6 +2,6 @@ yarn ava replaceFeeDistributor.test.js yarn ava upgradedBoard.test.js - +yarn ava mintHolder.test.js yarn ava provisionPool.test.js yarn ava agoricNames.test.js