diff --git a/a3p-integration/proposals/p:upgrade-19/.gitignore b/a3p-integration/proposals/p:upgrade-19/.gitignore index 8c6dbb7f6af..514675739d6 100644 --- a/a3p-integration/proposals/p:upgrade-19/.gitignore +++ b/a3p-integration/proposals/p:upgrade-19/.gitignore @@ -1 +1,2 @@ replaceFeeDistributor/ +addUsdLemons/ diff --git a/a3p-integration/proposals/p:upgrade-19/depositUSD-LEMONS/deposit-usd-lemons.js b/a3p-integration/proposals/p:upgrade-19/depositUSD-LEMONS/deposit-usd-lemons.js index 2d9a6f0650f..4a4a3c0313c 100644 --- a/a3p-integration/proposals/p:upgrade-19/depositUSD-LEMONS/deposit-usd-lemons.js +++ b/a3p-integration/proposals/p:upgrade-19/depositUSD-LEMONS/deposit-usd-lemons.js @@ -1,29 +1,29 @@ +/* eslint-disable no-undef */ const PROVISIONING_POOL_ADDR = 'agoric1megzytg65cyrgzs6fvzxgrcqvwwl7ugpt62346'; const depositUsdLemons = async powers => { const { - consume: { contractKits: contractKitsP, namesByAddressAdmin: namesByAddressAdminP , agoricNames }, - // instance: { consume: { ['psm-IST-USD_LEMONS']: usdLemonsPsmInstanceP }} + consume: { + contractKits: contractKitsP, + namesByAddressAdmin: namesByAddressAdminP, + agoricNames, + }, } = powers; const namesByAddressAdmin = await namesByAddressAdminP; const getDepositFacet = async address => { - const admin = await E(namesByAddressAdmin).lookupAdmin(address); - console.log('ADMIN', admin) - - const nameHub = await E(admin).readonly(); - console.log('NAME_HUB', nameHub); const hub = E(E(namesByAddressAdmin).lookupAdmin(address)).readonly(); return E(hub).lookup('depositFacet'); }; - const [contractKits, usdLemonsIssuer, usdLemonsBrand, ppDepositFacet] = await Promise.all([ - contractKitsP, - E(agoricNames).lookup('issuer', 'USD_LEMONS'), - E(agoricNames).lookup('brand', 'USD_LEMONS'), - getDepositFacet(PROVISIONING_POOL_ADDR), - ]); + const [contractKits, usdLemonsIssuer, usdLemonsBrand, ppDepositFacet] = + await Promise.all([ + contractKitsP, + E(agoricNames).lookup('issuer', 'USD_LEMONS'), + E(agoricNames).lookup('brand', 'USD_LEMONS'), + getDepositFacet(PROVISIONING_POOL_ADDR), + ]); console.log('[CONTRACT_KITS]', contractKits); console.log('[ISSUER]', usdLemonsIssuer); @@ -32,13 +32,15 @@ const depositUsdLemons = async powers => { for (const { publicFacet, creatorFacet: mint } of contractKits.values()) { if (publicFacet === usdLemonsIssuer) { usdLemonsMint = mint; - console.log('BINGO', mint) + console.log('BINGO', mint); break; } } console.log('Minting USD_LEMONS'); - const helloPayment = await E(usdLemonsMint).mintPayment(harden({ brand: usdLemonsBrand, value: 500000n })); + const helloPayment = await E(usdLemonsMint).mintPayment( + harden({ brand: usdLemonsBrand, value: 500000n }), + ); console.log('Funding provision pool...'); await E(ppDepositFacet).receive(helloPayment); diff --git a/a3p-integration/proposals/p:upgrade-19/package.json b/a3p-integration/proposals/p:upgrade-19/package.json index e2f705ad798..78b01e2acae 100644 --- a/a3p-integration/proposals/p:upgrade-19/package.json +++ b/a3p-integration/proposals/p:upgrade-19/package.json @@ -2,7 +2,8 @@ "agoricProposal": { "type": "/agoric.swingset.CoreEvalProposal", "sdk-generate": [ - "testing/replace-feeDistributor-short.js replaceFeeDistributor" + "testing/replace-feeDistributor-short.js replaceFeeDistributor", + "testing/add-USD-LEMONS.js addUsdLemons" ] }, "type": "module", diff --git a/a3p-integration/proposals/p:upgrade-19/provisionPool.test.js b/a3p-integration/proposals/p:upgrade-19/provisionPool.test.js new file mode 100644 index 00000000000..df8511305d9 --- /dev/null +++ b/a3p-integration/proposals/p:upgrade-19/provisionPool.test.js @@ -0,0 +1,129 @@ +/* eslint-env node */ +/** + * @file The goal of this file is to make sure v28-provisionPool and v14-bank can be successfully + * upgraded. These vats are related because of the issues below; + * - https://github.com/Agoric/agoric-sdk/issues/8722 + * - https://github.com/Agoric/agoric-sdk/issues/8724 + * + * The test scenario is as follows; + * - Prerequisite: provisionPool and bank are already upgraded. See `upgrade.go` + * 1. Add a new account and successfully provision it + * - Observe new account's address under `published.wallet.${address}` + * 2. Send some USDC_axl to provisionPoolAddress and observe its IST balances increases accordingly + * 3. Introduce a new asset to the chain and start a PSM instance for the new asset + * 3a. Deposit some of that asset to provisionPoolAddress + * 3b. Observe provisionPoolAddress' IST balance increase by the amount deposited in step 3a + */ + +import '@endo/init'; +import test from 'ava'; +import { execFileSync } from 'node:child_process'; +import { + addUser, + makeAgd, + evalBundles, + agd as agdAmbient, + agoric, + bankSend, + getISTBalance, +} from '@agoric/synthetic-chain'; +import { NonNullish } from './test-lib/errors.js'; +import { + retryUntilCondition, + waitUntilAccountFunded, + waitUntilContractDeployed, +} from './test-lib/sync-tools.js'; + +const PROVISIONING_POOL_ADDR = 'agoric1megzytg65cyrgzs6fvzxgrcqvwwl7ugpt62346'; + +const ADD_PSM_DIR = 'addUsdLemons'; +const DEPOSIT_USD_LEMONS_DIR = 'depositUSD-LEMONS'; + +const USDC_DENOM = NonNullish(process.env.USDC_DENOM); + +const agd = makeAgd({ execFileSync }).withOpts({ keyringBackend: 'test' }); + +const ambientAuthority = { + query: agdAmbient.query, + follow: agoric.follow, + setTimeout, +}; + +const provision = (name, address) => + agd.tx(['swingset', 'provision-one', name, address, 'SMART_WALLET'], { + chainId: 'agoriclocal', + from: 'validator', + yes: true, + }); + +const introduceAndProvision = async name => { + const address = await addUser(name); + console.log('ADDR', name, address); + + const provisionP = provision(name, address); + + return { provisionP, address }; +}; + +const getProvisionedAddresses = async () => { + const { children } = await agd.query([ + 'vstorage', + 'children', + 'published.wallet', + ]); + return children; +}; + +const checkUserProvisioned = addr => + retryUntilCondition( + getProvisionedAddresses, + children => children.includes(addr), + 'Account not provisioned', + { maxRetries: 5, retryIntervalMs: 1000, log: console.log, setTimeout }, + ); + +test(`upgrade provision pool`, async t => { + // Introduce new user then provision + const { address } = await introduceAndProvision('provisionTester'); + await checkUserProvisioned(address); + + // Send USDC_axl to pp + const istBalanceBefore = await getISTBalance(PROVISIONING_POOL_ADDR); + await bankSend(PROVISIONING_POOL_ADDR, `500000${USDC_DENOM}`); + + // Check IST balance + await waitUntilAccountFunded( + PROVISIONING_POOL_ADDR, + ambientAuthority, + { denom: 'uist', value: istBalanceBefore + 500000 }, + { errorMessage: 'Provision pool not able to swap USDC_axl for IST.' }, + ); + + // Introduce USD_LEMONS + await evalBundles(ADD_PSM_DIR); + await waitUntilContractDeployed('psm-IST-USD_LEMONS', ambientAuthority, { + errorMessage: 'psm-IST-USD_LEMONS instance not observed.', + }); + + // Provision the provisionPoolAddress. This is a workaround of provisionPoolAddress + // not having a depositFacet published to namesByAddress. Shouldn't be a problem since + // vat-bank keeps track of virtual purses per address basis. We need there to be + // depositFacet for provisionPoolAddress since we'll fund it with USD_LEMONS + await provision('provisionPoolAddress', PROVISIONING_POOL_ADDR); + await checkUserProvisioned(PROVISIONING_POOL_ADDR); + + // Send USD_LEMONS to provisionPoolAddress + const istBalanceBeforeLemonsSent = await getISTBalance( + PROVISIONING_POOL_ADDR, + ); + await evalBundles(DEPOSIT_USD_LEMONS_DIR); + + // Check balance again + await waitUntilAccountFunded( + PROVISIONING_POOL_ADDR, + ambientAuthority, + { denom: 'uist', value: istBalanceBeforeLemonsSent + 500000 }, + { errorMessage: 'Provision pool not bale swap USDC_axl for IST.' }, + ); + t.pass(); +}); diff --git a/a3p-integration/proposals/p:upgrade-19/test-lib/errors.js b/a3p-integration/proposals/p:upgrade-19/test-lib/errors.js new file mode 100644 index 00000000000..57dc771e6a5 --- /dev/null +++ b/a3p-integration/proposals/p:upgrade-19/test-lib/errors.js @@ -0,0 +1,20 @@ +/** + * @file Copied from "@agoric/internal" + */ + +import { q } from '@endo/errors'; + +/** + * @template T + * @param {T | null | undefined} val + * @param {string} [optDetails] + * @returns {T} + */ +export const NonNullish = (val, optDetails = `unexpected ${q(val)}`) => { + if (val != null) { + // This `!= null` idiom checks that `val` is neither `null` nor `undefined`. + return val; + } + assert.fail(optDetails); +}; +harden(NonNullish); diff --git a/a3p-integration/proposals/p:upgrade-19/test-lib/sync-tools.js b/a3p-integration/proposals/p:upgrade-19/test-lib/sync-tools.js new file mode 100644 index 00000000000..dac2ba7e04f --- /dev/null +++ b/a3p-integration/proposals/p:upgrade-19/test-lib/sync-tools.js @@ -0,0 +1,272 @@ +/* eslint-env node */ + +/** + * @file The purpose of this file is to bring together a set of tools that + * developers can use to synchronize operations they carry out in their tests. + * + * These operations include; + * - Making sure a core-eval resulted in successfully deploying a contract + * - Making sure a core-eval successfully sent zoe invitations to committee members for governance + * - Making sure an account is successfully funded with vbank assets like IST, BLD etc. + * - operation: query dest account's balance + * - condition: dest account has a balance >= sent token + * - Making sure an offer resulted successfully + * + */ + +/** + * @typedef {object} RetryOptions + * @property {number} [maxRetries] + * @property {number} [retryIntervalMs] + * @property {(...arg0: string[]) => void} [log] + * @property {(callback: Function, delay: number) => void} [setTimeout] + * + * @typedef {RetryOptions & {errorMessage: string}} WaitUntilOptions + * + * @typedef {object} CosmosBalanceThreshold + * @property {string} denom + * @property {number} value + */ + +const ambientSetTimeout = global.setTimeout; + +/** + * From https://github.com/Agoric/agoric-sdk/blob/442f07c8f0af03281b52b90e90c27131eef6f331/multichain-testing/tools/sleep.ts#L10 + * + * @param {number} ms + * @param {*} sleepOptions + */ +export const sleep = (ms, { log = () => {}, setTimeout = ambientSetTimeout }) => + new Promise(resolve => { + log(`Sleeping for ${ms}ms...`); + setTimeout(resolve, ms); + }); + +/** + * From https://github.com/Agoric/agoric-sdk/blob/442f07c8f0af03281b52b90e90c27131eef6f331/multichain-testing/tools/sleep.ts#L24 + * + * @param {() => Promise} operation + * @param {(result: any) => boolean} condition + * @param {string} message + * @param {RetryOptions} options + */ +export const retryUntilCondition = async ( + operation, + condition, + message, + { maxRetries = 6, retryIntervalMs = 3500, log = console.log, setTimeout }, +) => { + console.log({ maxRetries, retryIntervalMs, message }); + let retries = 0; + + while (retries < maxRetries) { + try { + const result = await operation(); + log('RESULT', result); + if (condition(result)) { + return result; + } + } catch (error) { + if (error instanceof Error) { + log(`Error: ${error.message}`); + } else { + log(`Unknown error: ${String(error)}`); + } + } + + retries += 1; + console.log( + `Retry ${retries}/${maxRetries} - Waiting for ${retryIntervalMs}ms for ${message}...`, + ); + await sleep(retryIntervalMs, { log, setTimeout }); + } + + throw Error(`${message} condition failed after ${maxRetries} retries.`); +}; + +/** + * @param {WaitUntilOptions} options + */ +const overrideDefaultOptions = options => { + const defaultValues = { + maxRetries: 6, + retryIntervalMs: 3500, + log: console.log, + errorMessage: 'Error', + }; + + return { ...defaultValues, ...options }; +}; + +/// ////////// Making sure a core-eval resulted successfully deploying a contract ///////////// + +const makeGetInstances = follow => async () => { + const instanceEntries = await follow( + '-lF', + `:published.agoricNames.instance`, + ); + + return Object.fromEntries(instanceEntries); +}; + +/** + * + * @param {string} contractName + * @param {{follow: () => object, setTimeout: (object) => void}} ambientAuthority + * @param {WaitUntilOptions} options + */ +export const waitUntilContractDeployed = ( + contractName, + ambientAuthority, + options, +) => { + const { follow, setTimeout } = ambientAuthority; + const getInstances = makeGetInstances(follow); + const { maxRetries, retryIntervalMs, errorMessage, log } = + overrideDefaultOptions(options); + + return retryUntilCondition( + getInstances, + instanceObject => Object.keys(instanceObject).includes(contractName), + errorMessage, + { maxRetries, retryIntervalMs, log, setTimeout }, + ); +}; + +/// ////////// Making sure an account is successfully funded with vbank assets like IST, BLD etc. /////////////// + +const makeQueryCosmosBalance = queryCb => async dest => { + const coins = await queryCb('bank', 'balances', dest); + return coins.balances; +}; + +/** + * + * @param {Array} balances + * @param {CosmosBalanceThreshold} threshold + * @returns {boolean} + */ +const checkCosmosBalance = (balances, threshold) => { + const balance = [...balances].find(({ denom }) => denom === threshold.denom); + return Number(balance.amount) >= threshold.value; +}; + +/** + * @param {string} destAcct + * @param {{query: () => Promise, setTimeout: (object) => void}} ambientAuthority + * @param {{denom: string, value: number}} threshold + * @param {WaitUntilOptions} options + */ +export const waitUntilAccountFunded = ( + destAcct, + ambientAuthority, + threshold, + options, +) => { + const { query, setTimeout } = ambientAuthority; + const queryCosmosBalance = makeQueryCosmosBalance(query); + const { maxRetries, retryIntervalMs, errorMessage, log } = + overrideDefaultOptions(options); + + return retryUntilCondition( + async () => queryCosmosBalance(destAcct), + balances => checkCosmosBalance(balances, threshold), + errorMessage, + { maxRetries, retryIntervalMs, log, setTimeout }, + ); +}; + +/// ////////// Making sure an offers get results ///////////// + +const makeQueryWallet = follow => async (/** @type {string} */ addr) => { + const update = await follow('-lF', `:published.wallet.${addr}`); + + return update; +}; + +/** + * + * @param {object} offerStatus + * @param {boolean} waitForPayouts + * @param {string} offerId + */ +const checkOfferState = (offerStatus, waitForPayouts, offerId) => { + const { updated, status } = offerStatus; + + if (updated !== 'offerStatus') return false; + if (!status) return false; + if (status.id !== offerId) return false; + if (!status.numWantsSatisfied || status.numWantsSatisfied !== 1) return false; + if (waitForPayouts && status.payouts) return true; + if (!waitForPayouts && status.result) return true; + + return false; +}; + +/** + * + * @param {string} addr + * @param {string} offerId + * @param {boolean} waitForPayouts + * @param {{follow: () => object, setTimeout: (callback: Function, delay: number) => void}} ambientAuthority + * @param {WaitUntilOptions} options + */ +export const waitUntilOfferResult = ( + addr, + offerId, + waitForPayouts, + ambientAuthority, + options, +) => { + const { follow, setTimeout } = ambientAuthority; + const queryWallet = makeQueryWallet(follow); + const { maxRetries, retryIntervalMs, errorMessage, log } = + overrideDefaultOptions(options); + + return retryUntilCondition( + async () => queryWallet(addr), + status => checkOfferState(status, waitForPayouts, offerId), + errorMessage, + { maxRetries, retryIntervalMs, log, setTimeout }, + ); +}; + +/// ////////// Making sure a core-eval successfully sent zoe invitations to committee members for governance ///////////// + +/** + * + * @param {{ updated: string, currentAmount: any }} update + * @returns {boolean} + */ +const checkForInvitation = update => { + const { updated, currentAmount } = update; + + if (updated !== 'balance') return false; + if (!currentAmount || !currentAmount.brand) return false; + + return currentAmount.brand.includes('Invitation'); +}; + +/** + * + * @param {string} addr + * @param {{follow: () => object, setTimeout: (object) => void}} ambientAuthority + * @param {WaitUntilOptions} options + */ +export const waitUntilInvitationReceived = ( + addr, + ambientAuthority, + options, +) => { + const { follow, setTimeout } = ambientAuthority; + const queryWallet = makeQueryWallet(follow); + const { maxRetries, retryIntervalMs, errorMessage, log } = + overrideDefaultOptions(options); + + return retryUntilCondition( + async () => queryWallet(addr), + checkForInvitation, + errorMessage, + { maxRetries, retryIntervalMs, log, setTimeout }, + ); +}; diff --git a/a3p-integration/proposals/p:upgrade-19/test.sh b/a3p-integration/proposals/p:upgrade-19/test.sh index 7b9ea0090c5..6f9a617e18e 100644 --- a/a3p-integration/proposals/p:upgrade-19/test.sh +++ b/a3p-integration/proposals/p:upgrade-19/test.sh @@ -1,3 +1,6 @@ #!/bin/bash yarn ava replaceFeeDistributor.test.js +#!/bin/bash + +yarn ava provisionPool.test.js diff --git a/packages/builders/scripts/testing/add-USD-LEMONS.js b/packages/builders/scripts/testing/add-USD-LEMONS.js index 89afc1279cb..ed548b5561c 100644 --- a/packages/builders/scripts/testing/add-USD-LEMONS.js +++ b/packages/builders/scripts/testing/add-USD-LEMONS.js @@ -2,18 +2,18 @@ import { makeHelpers } from '@agoric/deploy-script-support'; import { psmProposalBuilder } from '../inter-protocol/add-collateral-core.js'; const addUsdLemonsProposalBuilder = async powers => { - return psmProposalBuilder(powers, { - anchorOptions: { - denom: 'ibc/000C0AAAEECAFE000', - keyword: 'USD_LEMONS', - decimalPlaces: 6, - proposedName: 'USD_LEMONS', - } - }); + return psmProposalBuilder(powers, { + anchorOptions: { + denom: 'ibc/000C0AAAEECAFE000', + keyword: 'USD_LEMONS', + decimalPlaces: 6, + proposedName: 'USD_LEMONS', + }, + }); }; /** @type {import('@agoric/deploy-script-support/src/externalTypes.js').DeployScriptFunction} */ export default async (homeP, endowments) => { - const { writeCoreEval } = await makeHelpers(homeP, endowments); - await writeCoreEval('add-LEMONS-PSM', addUsdLemonsProposalBuilder); - }; \ No newline at end of file + const { writeCoreEval } = await makeHelpers(homeP, endowments); + await writeCoreEval('add-LEMONS-PSM', addUsdLemonsProposalBuilder); +};