diff --git a/a3p-integration/proposals/a:upgrade-next/agd-tools.js b/a3p-integration/proposals/a:upgrade-next/agd-tools.js index 90cf71587ab..f9da630c785 100644 --- a/a3p-integration/proposals/a:upgrade-next/agd-tools.js +++ b/a3p-integration/proposals/a:upgrade-next/agd-tools.js @@ -2,14 +2,14 @@ import { agd, agops, agopsLocation, + CHAINID, executeCommand, - VALIDATORADDR, executeOffer, GOV1ADDR, GOV2ADDR, GOV3ADDR, newOfferId, - CHAINID, + VALIDATORADDR, } from '@agoric/synthetic-chain'; const ORACLE_ADDRESSES = [GOV1ADDR, GOV2ADDR, GOV3ADDR]; @@ -137,3 +137,8 @@ export const bankSend = (addr, wanted) => { return agd.tx('bank', 'send', VALIDATORADDR, addr, wanted, ...noise); }; + +export const getProvisionPoolMetrics = async () => { + const path = `published.provisionPool.metrics`; + return getQuoteBody(path); +}; diff --git a/a3p-integration/proposals/a:upgrade-next/localchain.test.js b/a3p-integration/proposals/a:upgrade-next/localchain.test.js deleted file mode 100644 index b7145c64d6e..00000000000 --- a/a3p-integration/proposals/a:upgrade-next/localchain.test.js +++ /dev/null @@ -1,33 +0,0 @@ -import test from 'ava'; - -import { agd, evalBundles, waitForBlock } from '@agoric/synthetic-chain'; - -const SUBMISSION_DIR = 'localchaintest-submission'; - -const readPublished = async path => { - const { value } = await agd.query( - 'vstorage', - 'data', - '--output', - 'json', - `published.${path}`, - ); - if (value === '') { - return undefined; - } - const obj = JSON.parse(value); - return obj.values[0]; -}; - -// The testing assertions are in the submission that runs in the core-eval. -// The test here runs that and confirms the eval made it through all the assertions. -test(`localchain passes tests`, async t => { - await evalBundles(SUBMISSION_DIR); - - const nodePath = 'test.localchain'; - const nodeValue = JSON.stringify({ success: true }); - - await waitForBlock(2); // enough time for core eval to execute ? - - t.is(await readPublished(nodePath), nodeValue); -}); diff --git a/a3p-integration/proposals/a:upgrade-next/package.json b/a3p-integration/proposals/a:upgrade-next/package.json index 441024424bd..c56bf1e6cec 100644 --- a/a3p-integration/proposals/a:upgrade-next/package.json +++ b/a3p-integration/proposals/a:upgrade-next/package.json @@ -7,8 +7,10 @@ "coreProposals": [] }, "sdk-generate": [ - "vats/probe-zcf-bundle.js probe-submission", - "vats/test-localchain.js localchaintest-submission" + "vats/upgrade-bank.js upgrade-bank", + "vats/upgrade-provisionPool.js upgrade-provisionPool", + "testing/add-LEMONS.js add-LEMONS", + "testing/add-OLIVES.js add-OLIVES" ], "type": "Software Upgrade Proposal" }, diff --git a/a3p-integration/proposals/a:upgrade-next/priceFeed-follower-auction.test.js b/a3p-integration/proposals/a:upgrade-next/priceFeed-follower-auction.test.js deleted file mode 100644 index 30ae903b075..00000000000 --- a/a3p-integration/proposals/a:upgrade-next/priceFeed-follower-auction.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import test from 'ava'; -import { getDetailsMatchingVats } from './vatDetails.js'; - -test('new auction vat', async t => { - const details = await getDetailsMatchingVats('auctioneer'); - // This query matches both the auction and its governor, so 2*2 - t.is(Object.keys(details).length, 4); -}); diff --git a/a3p-integration/proposals/a:upgrade-next/probeZcfBundleCap.test.js b/a3p-integration/proposals/a:upgrade-next/probeZcfBundleCap.test.js deleted file mode 100644 index ef27a73f649..00000000000 --- a/a3p-integration/proposals/a:upgrade-next/probeZcfBundleCap.test.js +++ /dev/null @@ -1,27 +0,0 @@ -import test from 'ava'; - -import { - evalBundles, - getIncarnation, - getVatDetails, -} from '@agoric/synthetic-chain'; - -const SUBMISSION_DIR = 'probe-submission'; - -test('upgrade Zoe to verify ZcfBundleCap endures', async t => { - await null; - t.assert((await getIncarnation('zoe')) === 1, 'zoe incarnation must be one'); - - // Before the test, the Wallet Factory should be using the legacy ZCF - const detailsBefore = await getVatDetails('walletFactory'); - t.true(detailsBefore.incarnation >= 2, 'wf incarnation must be >= 2'); - - await evalBundles(SUBMISSION_DIR); - - const detailsAfter = await getVatDetails('walletFactory'); - t.is( - detailsAfter.incarnation, - detailsBefore.incarnation + 2, - 'wf incarnation must increase by 2', - ); -}); diff --git a/a3p-integration/proposals/a:upgrade-next/provisionPool.test.js b/a3p-integration/proposals/a:upgrade-next/provisionPool.test.js new file mode 100644 index 00000000000..6e0bbcca1f1 --- /dev/null +++ b/a3p-integration/proposals/a:upgrade-next/provisionPool.test.js @@ -0,0 +1,102 @@ +// @ts-check + +import test from 'ava'; + +import { + evalBundles, + getIncarnation, + waitForBlock, +} from '@agoric/synthetic-chain'; + +import { bankSend, getProvisionPoolMetrics } from './agd-tools.js'; + +const NULL_UPGRADE_BANK_DIR = 'upgrade-bank'; +const UPGRADE_PP_DIR = 'upgrade-provisionPool'; +const ADD_LEMONS_DIR = 'add-LEMONS'; +const ADD_OLIVES_DIR = 'add-OLIVES'; + +const USDC_DENOM = + 'ibc/295548A78785A1007F232DE286149A6FF512F180AF5657780FC89C009E2C348F'; +const PROVISIONING_POOL_ADDR = 'agoric1megzytg65cyrgzs6fvzxgrcqvwwl7ugpt62346'; + +/** + * @file + * The problem to be addressed is that provisionPool won't correctly handle + * (#8722) deposit of assets after it (provisionPool) is upgraded or (#8724) + * new asset kinds after vat-bank is upgraded. + * + * To test this, we will + * + * 1. See that we can add USDC. + * + * 2. Null upgrade vat-bank, and see that we can add a new collateal. + * + * 2a. Not null upgrade provisionPool, since it would fail. If it had succeeded, + * we would have been able to observe the effect of #8724, which would have + * caused addition of new currencies to be ignored. + * + * 3. Do a full upgrade of provisionPool; then deposit USDC, and see IST + * incremented in totalMintedConverted. + * + * 4. Null upgrade vat-bank again, and then see (in logs) that adding a new + * asset type gives the ability to make deposits. We don't actually add it + * because it would be too much work to add a faucet or other ability to mint + * the new collateral. + */ + +const contributeToPool = async (t, asset, expectedToGrow) => { + const metricsBefore = await getProvisionPoolMetrics(); + console.log('PPT pre', metricsBefore); + + await bankSend(PROVISIONING_POOL_ADDR, asset); + + const metricsAfter = await getProvisionPoolMetrics(); + console.log('PPT post', metricsAfter); + t.is( + metricsAfter.totalMintedConverted.brand, + metricsBefore.totalMintedConverted.brand, + 'brands match', + ); + if (expectedToGrow) { + // I couldn't import AmountMath. dunno why. + t.true( + metricsAfter.totalMintedConverted.value > + metricsBefore.totalMintedConverted.value, + 'brands match', + ); + } else { + t.equal( + metricsAfter.totalMintedConverted.value, + metricsBefore.totalMintedConverted.value, + ); + } +}; + +test('upgrading provisionPool and vat-bank', async t => { + t.log('add assets before'); + await contributeToPool(t, `10000${USDC_DENOM}`, true); + + t.log(`upgrade Bank`); + await evalBundles(NULL_UPGRADE_BANK_DIR); + + const firstIncarnation = await getIncarnation('bank'); + t.is(firstIncarnation, 1); + + await evalBundles(ADD_LEMONS_DIR); + + t.log('full upgrade ProvisionPool'); + await evalBundles(UPGRADE_PP_DIR); + const ppIncarnation = await getIncarnation('db93f-provisionPool'); + t.is(ppIncarnation, 1); + + await contributeToPool(t, `30000${USDC_DENOM}`, true); + + t.log(`Add assets after bank upgrade`); + await evalBundles(NULL_UPGRADE_BANK_DIR); + await waitForBlock(2); + + const secondIncarnation = await getIncarnation('bank'); + t.is(secondIncarnation, 2); + + await evalBundles(ADD_OLIVES_DIR); +}); diff --git a/a3p-integration/proposals/a:upgrade-next/provisioning.test.js b/a3p-integration/proposals/a:upgrade-next/provisioning.test.js deleted file mode 100644 index e31ffbb89a8..00000000000 --- a/a3p-integration/proposals/a:upgrade-next/provisioning.test.js +++ /dev/null @@ -1,51 +0,0 @@ -// @ts-check - -import test from 'ava'; -import { readFile, writeFile } from 'node:fs/promises'; - -import { - getIncarnation, - getUser, - evalBundles, - waitForBlock, - agoric, -} from '@agoric/synthetic-chain'; - -const SUBMISSION_DIR = 'provisioning-test-submission'; - -/** - * @param {string} fileName base file name without .tjs extension - * @param {Record} replacements - */ -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); -}; - -test.serial(`provisioning vat was upgraded`, async t => { - const incarnation = await getIncarnation('provisioning'); - - t.is(incarnation, 1); -}); - -test.serial(`send invitation via namesByAddress`, async t => { - const addr = await getUser('gov1'); - - await replaceTemplateValuesInFile(`${SUBMISSION_DIR}/send-script`, { - ADDRESS: addr, - }); - - await evalBundles(SUBMISSION_DIR); - - await waitForBlock(2); // enough time for invitation to arrive? - const update = await agoric.follow('-lF', `:published.wallet.${addr}`); - t.is(update.updated, 'balance'); - t.notDeepEqual(update.currentAmount.value, []); - t.log('balance value', update.currentAmount.value); - t.log('balance brand', update.currentAmount.brand); - // XXX agoric follow returns brands as strings - t.regex(update.currentAmount.brand, /Invitation/); -}); diff --git a/a3p-integration/proposals/a:upgrade-next/upgradeVaults.test.js b/a3p-integration/proposals/a:upgrade-next/upgradeVaults.test.js deleted file mode 100644 index 1dc1c662232..00000000000 --- a/a3p-integration/proposals/a:upgrade-next/upgradeVaults.test.js +++ /dev/null @@ -1,134 +0,0 @@ -import test from 'ava'; - -import { - agops, - ATOM_DENOM, - getISTBalance, - getVatDetails, - openVault, - USER1ADDR, -} from '@agoric/synthetic-chain'; - -import { - addOraclesForBrand, - bankSend, - BID_OFFER_ID, - checkForOracle, - createBid, - getLiveOffers, - getPriceQuote, - pushPrices, -} from './agd-tools.js'; -import { getDetailsMatchingVats } from './vatDetails.js'; - -const checkPriceFeedVatsUpdated = async t => { - const atomDetails = await getVatDetails('ATOM-USD_price_feed'); - // both the original and the new ATOM vault are incarnation 0 - t.is(atomDetails.incarnation, 0); - const stAtomDetails = await getVatDetails('stATOM'); - t.is(stAtomDetails.incarnation, 0); - const stOsmoDetails = await getVatDetails('stOSMO'); - t.is(stOsmoDetails.incarnation, 0); - const stTiaDetails = await getVatDetails('stTIA'); - t.is(stTiaDetails.incarnation, 0); - await Promise.all([ - checkForOracle(t, 'ATOM'), - checkForOracle(t, 'stATOM'), - checkForOracle(t, 'stTIA'), - checkForOracle(t, 'stOSMO'), - checkForOracle(t, 'stkATOM'), - ]); -}; - -const oraclesByBrand = new Map(); - -const tryPushPrices = async t => { - // There are no old prices for the other currencies. - const atomOutPre = await getPriceQuote('ATOM'); - t.is(atomOutPre, '+12010000'); - - t.log('adding oracle for each brand'); - await addOraclesForBrand('ATOM', oraclesByBrand); - await addOraclesForBrand('stATOM', oraclesByBrand); - await addOraclesForBrand('stTIA', oraclesByBrand); - await addOraclesForBrand('stOSMO', oraclesByBrand); - await addOraclesForBrand('stkATOM', oraclesByBrand); - - t.log('pushing new prices'); - await pushPrices(11.2, 'ATOM', oraclesByBrand); - await pushPrices(11.3, 'stTIA', oraclesByBrand); - await pushPrices(11.4, 'stATOM', oraclesByBrand); - await pushPrices(11.5, 'stOSMO', oraclesByBrand); - await pushPrices(11.6, 'stkATOM', oraclesByBrand); - - t.log('awaiting new quotes'); - const atomOut = await getPriceQuote('ATOM'); - t.is(atomOut, '+11200000'); - const tiaOut = await getPriceQuote('stTIA'); - t.is(tiaOut, '+11300000'); - const stAtomOut = await getPriceQuote('stATOM'); - t.is(stAtomOut, '+11400000'); - const osmoOut = await getPriceQuote('stOSMO'); - t.is(osmoOut, '+11500000'); - const stkAtomOut = await getPriceQuote('stkATOM'); - t.is(stkAtomOut, '+11600000'); -}; - -const createNewBid = async t => { - await createBid('20', USER1ADDR, BID_OFFER_ID); - const liveOffer = await getLiveOffers(USER1ADDR); - t.true(liveOffer[0].includes(BID_OFFER_ID)); -}; - -const openMarginalVault = async t => { - let user1IST = await getISTBalance(USER1ADDR); - await bankSend(USER1ADDR, `20000000${ATOM_DENOM}`); - const currentVaults = await agops.vaults('list', '--from', USER1ADDR); - - t.log('opening a vault'); - await openVault(USER1ADDR, 5, 10); - user1IST += 5; - const istBalanceAfterVaultOpen = await getISTBalance(USER1ADDR); - t.is(istBalanceAfterVaultOpen, user1IST); - - const activeVaultsAfter = await agops.vaults('list', '--from', USER1ADDR); - t.log(currentVaults, activeVaultsAfter); - t.true( - activeVaultsAfter.length > currentVaults.length, - `vaults count should increase, ${activeVaultsAfter.length}, ${currentVaults.length}`, - ); -}; - -const triggerAuction = async t => { - await pushPrices(5.2, 'ATOM', oraclesByBrand); - - const atomOut = await getPriceQuote('ATOM'); - t.is(atomOut, '+5200000'); -}; - -const makeNewAuctionVat = async t => { - const details = await getDetailsMatchingVats('auctioneer'); - // This query matches both the auction and its governor, so double the count - t.true(Object.keys(details).length > 2); -}; - -// test.serial() isn't guaranteed to run tests in order, so we run the intended tests here -test('liquidation post upgrade', async t => { - t.log('starting upgrade vaults test'); - await checkPriceFeedVatsUpdated(t); - - t.log('starting pushPrices'); - await tryPushPrices(t); - - t.log('create a new Bid for the auction'); - await createNewBid(t); - - t.log('open a marginal vault'); - await openMarginalVault(t); - - t.log('trigger Auction'); - await triggerAuction(t); - - t.log('make new auction'); - await makeNewAuctionVat(t); -}); diff --git a/packages/builders/scripts/testing/add-LEMONS.js b/packages/builders/scripts/testing/add-LEMONS.js new file mode 100644 index 00000000000..f0fce8380f1 --- /dev/null +++ b/packages/builders/scripts/testing/add-LEMONS.js @@ -0,0 +1,23 @@ +import { makeHelpers } from '@agoric/deploy-script-support'; +import { defaultProposalBuilder as vaultProposalBuilder } from '../inter-protocol/add-collateral-core.js'; + +/** @file This is for use in tests in a3p-integration */ + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */ +export const starsVaultProposalBuilder = async powers => { + return vaultProposalBuilder(powers, { + interchainAssetOptions: { + denom: 'ibc/000C0FFEECAFE000', + decimalPlaces: 6, + keyword: 'LEMONS', + + oracleBrand: 'LEMONS', + proposedName: 'LEMONS', + }, + }); +}; + +export default async (homeP, endowments) => { + const { writeCoreEval } = await makeHelpers(homeP, endowments); + await writeCoreEval('add-STARS-collateral', starsVaultProposalBuilder); +}; diff --git a/packages/builders/scripts/testing/add-OLIVES.js b/packages/builders/scripts/testing/add-OLIVES.js new file mode 100644 index 00000000000..9b82d9242b7 --- /dev/null +++ b/packages/builders/scripts/testing/add-OLIVES.js @@ -0,0 +1,22 @@ +import { makeHelpers } from '@agoric/deploy-script-support'; +import { defaultProposalBuilder as vaultProposalBuilder } from '../inter-protocol/add-collateral-core.js'; + +/** @file This is for use in tests in a3p-integration */ + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */ +export const stars2VaultProposalBuilder = async powers => { + return vaultProposalBuilder(powers, { + interchainAssetOptions: { + denom: 'ibc/111C0FFEECAFE111', + decimalPlaces: 6, + keyword: 'OLIVES', + oracleBrand: 'OLIVES', + proposedName: 'OLIVES', + }, + }); +}; + +export default async (homeP, endowments) => { + const { writeCoreEval } = await makeHelpers(homeP, endowments); + await writeCoreEval('add-STARS2-collateral', stars2VaultProposalBuilder); +}; diff --git a/packages/builders/scripts/vats/upgrade-bank.js b/packages/builders/scripts/vats/upgrade-bank.js new file mode 100644 index 00000000000..ce3d5a9b5b6 --- /dev/null +++ b/packages/builders/scripts/vats/upgrade-bank.js @@ -0,0 +1,18 @@ +import { makeHelpers } from '@agoric/deploy-script-support'; + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */ +export const defaultProposalBuilder = async ({ publishRef, install }) => + harden({ + sourceSpec: '@agoric/vats/src/proposals/upgrade-bank-proposal.js', + getManifestCall: [ + 'getManifestForUpgradingBank', + { + bankRef: publishRef(install('@agoric/vats/src/vat-bank.js')), + }, + ], + }); + +export default async (homeP, endowments) => { + const { writeCoreProposal } = await makeHelpers(homeP, endowments); + await writeCoreProposal('upgrade-bank', defaultProposalBuilder); +}; diff --git a/packages/builders/scripts/vats/upgrade-provisionPool.js b/packages/builders/scripts/vats/upgrade-provisionPool.js new file mode 100644 index 00000000000..9ffce9b822b --- /dev/null +++ b/packages/builders/scripts/vats/upgrade-provisionPool.js @@ -0,0 +1,20 @@ +import { makeHelpers } from '@agoric/deploy-script-support'; + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */ +export const defaultProposalBuilder = async ({ publishRef, install }) => + harden({ + sourceSpec: '@agoric/vats/src/proposals/upgrade-provisionPool-proposal.js', + getManifestCall: [ + 'getManifestForUpgradingProvisionPool', + { + provisionPoolRef: publishRef( + install('@agoric/inter-protocol/src/provisionPool.js'), + ), + }, + ], + }); + +export default async (homeP, endowments) => { + const { writeCoreProposal } = await makeHelpers(homeP, endowments); + await writeCoreProposal('upgrade-provision-pool', defaultProposalBuilder); +}; diff --git a/packages/inter-protocol/src/provisionPoolKit.js b/packages/inter-protocol/src/provisionPoolKit.js index 59b2b0ca424..fb8136e2359 100644 --- a/packages/inter-protocol/src/provisionPoolKit.js +++ b/packages/inter-protocol/src/provisionPoolKit.js @@ -1,6 +1,6 @@ // @ts-check import { AmountMath, BrandShape } from '@agoric/ertp'; -import { deeplyFulfilledObject } from '@agoric/internal'; +import { deeplyFulfilledObject, makeTracer } from '@agoric/internal'; import { UnguardedHelperI } from '@agoric/internal/src/typeGuards.js'; import { observeIteration, @@ -21,9 +21,16 @@ import { import { InstanceHandleShape } from '@agoric/zoe/src/typeGuards.js'; import { E } from '@endo/far'; import { Far } from '@endo/marshal'; +import { isUpgradeDisconnection } from '@agoric/internal/src/upgrade-api.js'; const { details: X, quote: q, Fail } = assert; +const trace = makeTracer('ProvPool'); + +const FIRST_UPPER_KEYWORD = /^[A-Z][a-zA-Z0-9_$]*$/; +// see https://github.com/Agoric/agoric-sdk/issues/8238 +const FIRST_LOWER_NEAR_KEYWORD = /^[a-z][a-zA-Z0-9_$]*$/; + /** * @import {ERef} from '@endo/far' * @import {Amount} from '@agoric/ertp/src/types.js' @@ -73,7 +80,7 @@ export const makeBridgeProvisionTool = (sendInitialPayment, onProvisioned) => { fromBridge: async obj => { obj.type === 'PLEASE_PROVISION' || Fail`Unrecognized request ${obj.type}`; - console.info('PLEASE_PROVISION', obj); + trace('PLEASE_PROVISION', obj); const { address, powerFlags } = obj; powerFlags.includes(PowerFlags.SMART_WALLET) || Fail`missing SMART_WALLET in powerFlags`; @@ -90,7 +97,7 @@ export const makeBridgeProvisionTool = (sendInitialPayment, onProvisioned) => { if (created) { onProvisioned(); } - console.info(created ? 'provisioned' : 're-provisioned', address); + trace(created ? 'provisioned' : 're-provisioned', address); }, }); return makeBridgeHandler; @@ -149,7 +156,7 @@ export const prepareProvisionPoolKit = ( /** @type {import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit} */ const metricsRecorderKit = makeRecorderKit(metricsNode); - /** @type {MapStore} */ + /** @type {MapStore, PsmInstance>} */ const brandToPSM = makeScalarBigMapStore('brandToPSM', { durable: true }); const revivableAddresses = makeScalarBigSetStore('revivableAddresses', { durable: true, @@ -184,7 +191,7 @@ export const prepareProvisionPoolKit = ( machine: { /** @param {string[]} oldAddresses */ addRevivableAddresses(oldAddresses) { - console.log('revivableAddresses count', oldAddresses.length); + trace('revivableAddresses count', oldAddresses.length); this.state.revivableAddresses.addAll(oldAddresses); }, getWalletReviver() { @@ -318,7 +325,7 @@ export const prepareProvisionPoolKit = ( .deposit(initialPmt) .then(amt => { helper.onSendFunds(perAccountInitialAmount); - console.log('provisionPool sent', amt); + trace('provisionPool sent', amt); }) .catch(reason => { console.error(X`initial deposit failed: ${q(reason)}`); @@ -327,62 +334,114 @@ export const prepareProvisionPoolKit = ( }); }, /** - * @param {object} [options] - * @param {MetricsNotification} [options.metrics] + * @param {ERef} exchangePurse + * @param {ERef} brand */ - start({ metrics } = {}) { + watchCurrentAmount(exchangePurse, brand) { const { state: { brandToPSM, poolBrand }, facets: { helper }, } = this; - // Must match. poolBrand is from durable state and the param is from - // the contract, so it technically can change between incarnations. - // That would be a severe bug. - AmountMath.coerce(poolBrand, params.getPerAccountInitialAmount()); + void observeNotifier(E(exchangePurse).getCurrentAmountNotifier(), { + updateState: async amount => { + trace('provisionPool balance update', amount); + if (AmountMath.isEmpty(amount) || amount.brand === poolBrand) { + return; + } + if (!brandToPSM.has(brand)) { + console.error('funds arrived but no PSM instance', brand); + return; + } + const instance = brandToPSM.get(brand); + const payment = E(exchangePurse).withdraw(amount); + await helper + .swap(payment, amount, instance) + .catch(async reason => { + console.error(X`swap failed: ${reason}`); + const resolvedPayment = await payment; + return E(exchangePurse).deposit(resolvedPayment); + }); + }, + fail: reason => { + if (isUpgradeDisconnection(reason)) { + void helper.watchCurrentAmount(exchangePurse, brand); + } else { + console.error(reason); + } + }, + }); + }, + watchAssetSubscription() { + const { facets } = this; + const { helper } = facets; - void observeIteration( + /** @param {import('@agoric/vats/src/vat-bank.js').AssetDescriptor} desc */ + const repairDesc = desc => { + if (desc.issuerName.match(FIRST_UPPER_KEYWORD)) { + trace(`Saving Issuer ${desc.issuerName}`); + return desc; + } else if (desc.issuerName.match(FIRST_LOWER_NEAR_KEYWORD)) { + const bad = desc.issuerName; + const goodName = bad.replace(bad[0], bad[0].toUpperCase()); + + trace( + `Saving Issuer ${desc.issuerName} with repaired keyword ${goodName}`, + ); + return { ...desc, issuerName: goodName }; + } else { + console.error( + `unable to save issuer with illegal keyword: ${desc.issuerName}`, + ); + return undefined; + } + }; + + return observeIteration( subscribeEach(E(poolBank).getAssetSubscription()), { updateState: async desc => { - console.log('provisionPool notified of new asset', desc.brand); - await zcf.saveIssuer(desc.issuer, desc.issuerName); + await null; + const issuer = zcf.getTerms().issuers[desc.issuerName]; + if (issuer === desc.issuer) { + trace('provisionPool re-notified of known asset', desc.brand); + } else { + const goodDesc = repairDesc(desc); + if (goodDesc) { + await zcf.saveIssuer(goodDesc.issuer, goodDesc.issuerName); + } else { + console.error( + `unable to save issuer with illegal keyword: ${desc.issuerName}`, + ); + } + } + /** @type {ERef} */ const exchangePurse = E(poolBank).getPurse(desc.brand); - void observeNotifier( - E(exchangePurse).getCurrentAmountNotifier(), - { - updateState: async amount => { - console.log('provisionPool balance update', amount); - if ( - AmountMath.isEmpty(amount) || - amount.brand === poolBrand - ) { - return; - } - if (!brandToPSM.has(desc.brand)) { - console.error( - 'funds arrived but no PSM instance', - desc.brand, - ); - return; - } - const instance = brandToPSM.get(desc.brand); - const payment = E(exchangePurse).withdraw(amount); - await helper - .swap(payment, amount, instance) - .catch(async reason => { - console.error(X`swap failed: ${reason}`); - const resolvedPayment = await payment; - return E(exchangePurse).deposit(resolvedPayment); - }); - }, - fail: reason => console.error(reason), - }, - ); + helper.watchCurrentAmount(exchangePurse, desc.brand); + }, + fail: _reason => { + void helper.watchAssetSubscription(); }, }, ); + }, + /** + * @param {object} [options] + * @param {MetricsNotification} [options.metrics] + */ + start({ metrics } = {}) { + const { + state: { poolBrand }, + facets: { helper }, + } = this; + + // Must match. poolBrand is from durable state and the param is from + // the contract, so it technically can change between incarnations. + // That would be a severe bug. + AmountMath.coerce(poolBrand, params.getPerAccountInitialAmount()); + + void helper.watchAssetSubscription(); if (metrics) { // Restore state. diff --git a/packages/vats/src/proposals/upgrade-bank-proposal.js b/packages/vats/src/proposals/upgrade-bank-proposal.js new file mode 100644 index 00000000000..46467475c9e --- /dev/null +++ b/packages/vats/src/proposals/upgrade-bank-proposal.js @@ -0,0 +1,42 @@ +import { E } from '@endo/far'; + +/** + * @param {BootstrapPowers & { + * consume: { + * vatAdminSvc: VatAdminSvc; + * vatStore: MapStore< + * string, + * import('@agoric/swingset-vat').CreateVatResults + * >; + * }; + * }} powers + * @param {object} options + * @param {{ bankRef: VatSourceRef }} options.options + */ +export const upgradeBank = async ( + { consume: { vatAdminSvc, vatStore } }, + options, +) => { + const { bankRef } = options.options; + + assert(bankRef.bundleID); + const bankBundleCap = await E(vatAdminSvc).getBundleCap(bankRef.bundleID); + console.log(`BANK BUNDLE ID: `, bankRef.bundleID); + + const { adminNode } = await E(vatStore).get('bank'); + + await E(adminNode).upgrade(bankBundleCap, {}); +}; + +export const getManifestForUpgradingBank = (_powers, { bankRef }) => ({ + manifest: { + [upgradeBank.name]: { + consume: { + vatAdminSvc: 'vatAdminSvc', + vatStore: 'vatStore', + }, + produce: {}, + }, + }, + options: { bankRef }, +}); diff --git a/packages/vats/src/proposals/upgrade-provisionPool-proposal.js b/packages/vats/src/proposals/upgrade-provisionPool-proposal.js new file mode 100644 index 00000000000..aea588dece8 --- /dev/null +++ b/packages/vats/src/proposals/upgrade-provisionPool-proposal.js @@ -0,0 +1,67 @@ +import { E } from '@endo/far'; +import { deeplyFulfilled } from '@endo/marshal'; + +/** + * @param {BootstrapPowers & { + * consume: { + * economicCommitteeCreatorFacet: any; + * }; + * }} powers + * @param {object} options + * @param {{ provisionPoolRef: VatSourceRef }} options.options + */ +export const upgradeProvisionPool = async ( + { + consume: { + economicCommitteeCreatorFacet: electorateCreatorFacet, + instancePrivateArgs: instancePrivateArgsP, + provisionPoolStartResult: provisionPoolStartResultP, + }, + }, + options, +) => { + const { provisionPoolRef } = options.options; + + assert(provisionPoolRef.bundleID); + console.log(`PROVISION POOL BUNDLE ID: `, provisionPoolRef.bundleID); + + const [provisionPoolStartResult, instancePrivateArgs] = await Promise.all([ + provisionPoolStartResultP, + instancePrivateArgsP, + ]); + const { adminFacet, instance } = provisionPoolStartResult; + + const [originalPrivateArgs, poserInvitation] = await Promise.all([ + deeplyFulfilled(instancePrivateArgs.get(instance)), + E(electorateCreatorFacet).getPoserInvitation(), + ]); + + const newPrivateArgs = harden({ + ...originalPrivateArgs, + initialPoserInvitation: poserInvitation, + }); + + const upgradeResult = await E(adminFacet).upgradeContract( + provisionPoolRef.bundleID, + newPrivateArgs, + ); + + console.log('ProvisionPool upgraded: ', upgradeResult); +}; + +export const getManifestForUpgradingProvisionPool = ( + _powers, + { provisionPoolRef }, +) => ({ + manifest: { + [upgradeProvisionPool.name]: { + consume: { + economicCommitteeCreatorFacet: true, + instancePrivateArgs: true, + provisionPoolStartResult: true, + }, + produce: {}, + }, + }, + options: { provisionPoolRef }, +}); diff --git a/packages/vats/src/proposals/upgrade-zoe-proposal.js b/packages/vats/src/proposals/upgrade-zoe-proposal.js index 6ae75e32088..8d9e9210b9e 100644 --- a/packages/vats/src/proposals/upgrade-zoe-proposal.js +++ b/packages/vats/src/proposals/upgrade-zoe-proposal.js @@ -11,7 +11,7 @@ import { E } from '@endo/far'; * }; * }} powers * @param {object} options - * @param {{ zoeRef: VatSourceRef; zcfRef: VatSourceRef }} options.options + * @param {{ zoeRef: VatSourceRef }} options.options */ export const upgradeZoe = async ( { consume: { vatAdminSvc, vatStore } },