From eae60bea7ffba9506ca35ab31274ffc534c4b87a Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Tue, 23 Apr 2024 17:33:41 -0700 Subject: [PATCH 1/2] feat: upgrade vaults and add a new auction --- .../scripts/generate-a3p-submissions.sh | 3 +- golang/cosmos/app/app.go | 4 + packages/builders/scripts/vats/add-auction.js | 14 ++ .../builders/scripts/vats/upgradeVaults.js | 23 ++ .../src/proposals/add-auction.js | 177 ++++++++++++++++ .../src/proposals/upgrade-vaults.js | 196 ++++++++++++++++++ 6 files changed, 416 insertions(+), 1 deletion(-) create mode 100644 packages/builders/scripts/vats/add-auction.js create mode 100644 packages/builders/scripts/vats/upgradeVaults.js create mode 100644 packages/inter-protocol/src/proposals/add-auction.js create mode 100644 packages/inter-protocol/src/proposals/upgrade-vaults.js diff --git a/a3p-integration/scripts/generate-a3p-submissions.sh b/a3p-integration/scripts/generate-a3p-submissions.sh index 527e1755429..9e14163bd9e 100755 --- a/a3p-integration/scripts/generate-a3p-submissions.sh +++ b/a3p-integration/scripts/generate-a3p-submissions.sh @@ -1,7 +1,8 @@ #!/bin/bash set -ueo pipefail -SCRIPT_DIR=$( cd ${0%/*} && pwd -P ) +# cd prints its target on some platforms. Without the redirect, we get 2 copies +SCRIPT_DIR=$( cd ${0%/*} > /dev/null && pwd -P ) IFS=$'\n' diff --git a/golang/cosmos/app/app.go b/golang/cosmos/app/app.go index 5f0c0cb70dd..d2794e32717 100644 --- a/golang/cosmos/app/app.go +++ b/golang/cosmos/app/app.go @@ -914,6 +914,10 @@ func unreleasedUpgradeHandler(app *GaiaApp, targetUpgrade string) func(sdk.Conte "@agoric/builders/scripts/vats/updateStOsmoPriceFeed.js", "@agoric/builders/scripts/vats/updateStTiaPriceFeed.js", ), + // Add new auction contract. The old one will be retired shortly. + vm.CoreProposalStepForModules( "@agoric/builders/scripts/vats/add-auction.js"), + // upgrade vaultFactory. + vm.CoreProposalStepForModules( "@agoric/builders/scripts/vats/upgradeVaults.js"), } } diff --git a/packages/builders/scripts/vats/add-auction.js b/packages/builders/scripts/vats/add-auction.js new file mode 100644 index 00000000000..8a248f17d17 --- /dev/null +++ b/packages/builders/scripts/vats/add-auction.js @@ -0,0 +1,14 @@ +import { makeHelpers } from '@agoric/deploy-script-support'; + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').ProposalBuilder} */ +export const defaultProposalBuilder = async () => { + return harden({ + sourceSpec: '@agoric/inter-protocol/src/proposals/add-auction.js', + getManifestCall: ['getManifestForAddAuction'], + }); +}; + +export default async (homeP, endowments) => { + const { writeCoreProposal } = await makeHelpers(homeP, endowments); + await writeCoreProposal('add-auction', defaultProposalBuilder); +}; diff --git a/packages/builders/scripts/vats/upgradeVaults.js b/packages/builders/scripts/vats/upgradeVaults.js new file mode 100644 index 00000000000..ac69a81a37c --- /dev/null +++ b/packages/builders/scripts/vats/upgradeVaults.js @@ -0,0 +1,23 @@ +import { makeHelpers } from '@agoric/deploy-script-support'; + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').ProposalBuilder} */ +export const defaultProposalBuilder = async ({ publishRef, install }) => + harden({ + sourceSpec: '@agoric/inter-protocol/src/proposals/upgrade-vaults.js', + getManifestCall: [ + 'getManifestForUpgradeVaults', + { + vaultsRef: publishRef( + install( + '@agoric/inter-protocol/src/vaultFactory/vaultFactory.js', + '../bundles/bundle-vaultFactory.js', + ), + ), + }, + ], + }); + +export default async (homeP, endowments) => { + const { writeCoreProposal } = await makeHelpers(homeP, endowments); + await writeCoreProposal('upgrade-vaults', defaultProposalBuilder); +}; diff --git a/packages/inter-protocol/src/proposals/add-auction.js b/packages/inter-protocol/src/proposals/add-auction.js new file mode 100644 index 00000000000..1eafb2e747f --- /dev/null +++ b/packages/inter-protocol/src/proposals/add-auction.js @@ -0,0 +1,177 @@ +import { deeplyFulfilledObject, makeTracer } from '@agoric/internal'; +import { makeStorageNodeChild } from '@agoric/internal/src/lib-chainStorage.js'; +import { E } from '@endo/far'; +import { Stable } from '@agoric/internal/src/tokens.js'; +import { makeGovernedTerms as makeGovernedATerms } from '../auction/params.js'; + +const trace = makeTracer('NewAuction', true); + +/** @param {import('./econ-behaviors.js').EconomyBootstrapPowers} powers */ +export const addAuction = async ({ + consume: { + zoe, + board, + chainTimerService, + priceAuthority, + chainStorage, + economicCommitteeCreatorFacet: electorateCreatorFacet, + auctioneerKit: legacyKitP, + }, + produce: { newAuctioneerKit }, + instance: { + consume: { reserve: reserveInstance }, + }, + installation: { + consume: { + auctioneer: auctionInstallation, + contractGovernor: contractGovernorInstallation, + }, + }, + issuer: { + consume: { [Stable.symbol]: stableIssuerP }, + }, +}) => { + trace('addAuction start'); + const STORAGE_PATH = 'auction'; + + const poserInvitationP = E(electorateCreatorFacet).getPoserInvitation(); + + const [ + initialPoserInvitation, + electorateInvitationAmount, + stableIssuer, + legacyKit, + ] = await Promise.all([ + poserInvitationP, + E(E(zoe).getInvitationIssuer()).getAmountOf(poserInvitationP), + stableIssuerP, + legacyKitP, + ]); + + // Each field has an extra layer of type + value: + // AuctionStartDelay: { type: 'relativeTime', value: { relValue: 2n, timerBrand: Object [Alleged: timerBrand] {} } } + /** @type {any} */ + const paramValues = await E(legacyKit.publicFacet).getGovernedParams(); + const params = harden({ + StartFrequency: paramValues.StartFrequency.value, + ClockStep: paramValues.ClockStep.value, + StartingRate: paramValues.StartingRate.value, + LowestRate: paramValues.LowestRate.value, + DiscountStep: paramValues.DiscountStep.value, + AuctionStartDelay: paramValues.AuctionStartDelay.value, + PriceLockPeriod: paramValues.PriceLockPeriod.value, + }); + const timerBrand = await E(chainTimerService).getTimerBrand(); + + const storageNode = await makeStorageNodeChild(chainStorage, STORAGE_PATH); + const marshaller = await E(board).getReadonlyMarshaller(); + + const reservePublicFacet = await E(zoe).getPublicFacet(reserveInstance); + + const auctionTerms = makeGovernedATerms( + { storageNode, marshaller }, + chainTimerService, + priceAuthority, + reservePublicFacet, + { + ...params, + ElectorateInvitationAmount: electorateInvitationAmount, + TimerBrand: timerBrand, + }, + ); + + const governorTerms = await deeplyFulfilledObject( + harden({ + timer: chainTimerService, + governedContractInstallation: auctionInstallation, + governed: { + terms: auctionTerms, + issuerKeywordRecord: { Bid: stableIssuer }, + storageNode, + marshaller, + label: 'auctioneer', + }, + }), + ); + + /** @type {GovernorStartedInstallationKit} */ + const governorStartResult = await E(zoe).startInstance( + contractGovernorInstallation, + undefined, + governorTerms, + harden({ + electorateCreatorFacet, + governed: { + initialPoserInvitation, + storageNode, + marshaller, + }, + }), + 'auctioneer.governor', + ); + + const [governedInstance, governedCreatorFacet, governedPublicFacet] = + await Promise.all([ + E(governorStartResult.creatorFacet).getInstance(), + E(governorStartResult.creatorFacet).getCreatorFacet(), + E(governorStartResult.creatorFacet).getPublicFacet(), + ]); + + const allIssuers = await E(zoe).getIssuers(legacyKit.instance); + const { Bid: _istIssuer, ...auctionIssuers } = allIssuers; + await Promise.all( + Object.keys(auctionIssuers).map(kwd => + E(governedCreatorFacet).addBrand(auctionIssuers[kwd], kwd), + ), + ); + + newAuctioneerKit.resolve( + harden({ + label: 'auctioneer', + creatorFacet: governedCreatorFacet, + adminFacet: governorStartResult.adminFacet, + publicFacet: governedPublicFacet, + instance: governedInstance, + + governor: governorStartResult.instance, + governorCreatorFacet: governorStartResult.creatorFacet, + governorAdminFacet: governorStartResult.adminFacet, + }), + ); + // don't overwrite auctioneerKit or auction instance yet. Wait until + // upgrade-vault.js +}; + +export const ADD_AUCTION_MANIFEST = harden({ + [addAuction.name]: { + consume: { + zoe: true, + board: true, + chainTimerService: true, + priceAuthority: true, + chainStorage: true, + economicCommitteeCreatorFacet: true, + auctioneerKit: true, + }, + produce: { + newAuctioneerKit: true, + }, + instance: { + consume: { reserve: true }, + }, + installation: { + consume: { + auctioneer: true, + contractGovernor: true, + }, + }, + issuer: { + consume: { [Stable.symbol]: true }, + }, + }, +}); + +/* Add a new auction to a chain that already has one. */ +export const getManifestForAddAuction = async () => { + return { manifest: ADD_AUCTION_MANIFEST }; +}; diff --git a/packages/inter-protocol/src/proposals/upgrade-vaults.js b/packages/inter-protocol/src/proposals/upgrade-vaults.js new file mode 100644 index 00000000000..f8b0441e736 --- /dev/null +++ b/packages/inter-protocol/src/proposals/upgrade-vaults.js @@ -0,0 +1,196 @@ +import { E } from '@endo/far'; +import { makeNotifierFromAsyncIterable } from '@agoric/notifier'; +import { AmountMath } from '@agoric/ertp/src/index.js'; +import { makeTracer } from '@agoric/internal/src/index.js'; + +const trace = makeTracer('upgrade Vaults proposal'); + +// stand-in for Promise.any() which isn't available at this point. +const any = promises => + new Promise((resolve, reject) => { + for (const promise of promises) { + promise.then(resolve); + } + void Promise.allSettled(promises).then(results => { + const rejects = results.filter(({ status }) => status === 'rejected'); + if (rejects.length === results.length) { + // @ts-expect-error TypeScript doesn't know enough + const messages = rejects.map(({ message }) => message); + const aggregate = new Error(messages.join(';')); + // @ts-expect-error TypeScript doesn't know enough + aggregate.errors = rejects.map(({ reason }) => reason); + reject(aggregate); + } + }); + }); + +/** + * @param {import('../../src/proposals/econ-behaviors').EconomyBootstrapPowers} powers + * @param {{ options: { vaultsRef: { bundleID: string } } }} options + */ +export const upgradeVaults = async (powers, { options }) => { + const { + consume: { + agoricNamesAdmin, + newAuctioneerKit: auctioneerKitP, + priceAuthority, + vaultFactoryKit, + zoe, + economicCommitteeCreatorFacet: electorateCreatorFacet, + reserveKit, + }, + produce: { + auctioneerKit: auctioneerKitProducer, + newAuctioneerKit: tempAuctioneerKit, + }, + instance: { + produce: { auctioneer: auctioneerProducer }, + }, + } = powers; + const { vaultsRef } = options; + const kit = await vaultFactoryKit; + const auctioneerKit = await auctioneerKitP; + const { instance: directorInstance } = kit; + const allBrands = await E(zoe).getBrands(directorInstance); + const { Minted: istBrand, ...vaultBrands } = allBrands; + + const bundleID = vaultsRef.bundleID; + console.log(`upgradeVaults: bundleId`, bundleID); + let installationP; + await null; + if (vaultsRef) { + if (bundleID) { + installationP = E(zoe).installBundleID(bundleID); + await E.when( + installationP, + installation => + E(E(agoricNamesAdmin).lookupAdmin('installation')).update( + 'vaultFactory', + installation, + ), + err => + console.error(`🚨 failed to update vaultFactory installation`, err), + ); + } + } + + const readManagerParams = async () => { + const { publicFacet: directorPF } = kit; + + await null; + + const params = {}; + for (const kwd of Object.keys(vaultBrands)) { + const collateralBrand = vaultBrands[kwd]; + const subscription = E(directorPF).getSubscription({ + collateralBrand, + }); + const notifier = makeNotifierFromAsyncIterable(subscription); + let { value, updateCount } = await notifier.getUpdateSince(0n); + // @ts-expect-error It's an amount. + while (AmountMath.isEmpty(value.current.DebtLimit.value)) { + ({ value, updateCount } = await notifier.getUpdateSince(updateCount)); + trace(`debtLimit was empty, retried`, value.current.DebtLimit.value); + } + trace(kwd, 'params at', updateCount, 'are', value.current); + params[kwd] = harden({ + brand: collateralBrand, + debtLimit: value.current.DebtLimit.value, + interestRate: value.current.InterestRate.value, + liquidationMargin: value.current.LiquidationMargin.value, + liquidationPadding: value.current.LiquidationPadding.value, + liquidationPenalty: value.current.LiquidationPenalty.value, + mintFee: value.current.MintFee.value, + }); + } + return params; + }; + const managerParamValues = await readManagerParams(); + + // upgrade the vaultFactory + const upgradeVaultFactory = async () => { + // @ts-expect-error cast XXX privateArgs missing from type + const { privateArgs } = kit; + + const shortfallInvitation = await E( + E.get(reserveKit).creatorFacet, + ).makeShortfallReportingInvitation(); + + const poserInvitation = await E( + electorateCreatorFacet, + ).getPoserInvitation(); + /** @type {import('../../src/vaultFactory/vaultFactory').VaultFactoryContract['privateArgs']} */ + const newPrivateArgs = harden({ + ...privateArgs, + // @ts-expect-error It has a value until reset after the upgrade + auctioneerInstance: auctioneerKit.instance, + initialPoserInvitation: poserInvitation, + initialShortfallInvitation: shortfallInvitation, + managerParams: managerParamValues, + }); + + const upgradeResult = await E(kit.adminFacet).upgradeContract( + bundleID, + newPrivateArgs, + ); + + console.log('upgraded vaultFactory.', upgradeResult); + }; + + // Wait for at least one new price feed to be ready before upgrading Vaults + void E.when( + any( + Object.values(vaultBrands).map(brand => + E(priceAuthority).quoteGiven(AmountMath.make(brand, 10n), istBrand), + ), + ), + async price => { + trace(`upgrading after delay`, price); + await upgradeVaultFactory(); + auctioneerKitProducer.reset(); + // @ts-expect-error It has a value until reset just below + auctioneerKitProducer.resolve(auctioneerKit); + auctioneerProducer.reset(); + // @ts-expect-error It has a value until reset just below + auctioneerProducer.resolve(auctioneerKit.instance); + // We wanted it to be valid for only a short while. + tempAuctioneerKit.reset(); + await E(E(agoricNamesAdmin).lookupAdmin('instance')).update( + 'auctioneer', + auctioneerKit.instance, + ); + }, + ); + + console.log(`upgradeVaults scheduled; waiting for priceFeeds`); +}; + +const t = 'upgradeVaults'; +/** + * Return the manifest, installations, and options for upgrading Vaults. + * + * @param {object} _ign + * @param {any} vaultUpgradeOptions + */ +export const getManifestForUpgradeVaults = async ( + _ign, + vaultUpgradeOptions, +) => ({ + manifest: { + [upgradeVaults.name]: { + consume: { + agoricNamesAdmin: t, + newAuctioneerKit: t, + economicCommitteeCreatorFacet: t, + priceAuthority: t, + reserveKit: t, + vaultFactoryKit: t, + board: t, + zoe: t, + }, + produce: { auctioneerKit: t, newAuctioneerKit: t }, + instance: { produce: { auctioneer: t, newAuctioneerKit: t } }, + }, + }, + options: { ...vaultUpgradeOptions }, +}); From a30f7beecc1cd3aaf9f005b92b14636aebcf59e3 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Tue, 23 Apr 2024 17:35:43 -0700 Subject: [PATCH 2/2] test: verify that vaults and auctions upgrade correctly --- a3p-integration/package.json | 3 +- .../proposals/a:upgrade-next/agd-tools.js | 138 ++++++++++++++ .../proposals/a:upgrade-next/post.test.js | 15 ++ .../a:upgrade-next/priceFeed.test.js | 176 +++++++----------- .../proposals/a:upgrade-next/test.sh | 5 +- .../proposals/a:upgrade-next/vatDetails.js | 100 ++++++++++ a3p-integration/yarn.lock | 26 +++ 7 files changed, 352 insertions(+), 111 deletions(-) create mode 100644 a3p-integration/proposals/a:upgrade-next/agd-tools.js create mode 100644 a3p-integration/proposals/a:upgrade-next/post.test.js create mode 100644 a3p-integration/proposals/a:upgrade-next/vatDetails.js diff --git a/a3p-integration/package.json b/a3p-integration/package.json index 3882c279d1f..fe183229c7b 100644 --- a/a3p-integration/package.json +++ b/a3p-integration/package.json @@ -12,7 +12,8 @@ "doctor": "yarn synthetic-chain doctor" }, "dependencies": { - "@agoric/synthetic-chain": "^0.0.10" + "@agoric/synthetic-chain": "^0.0.10", + "@types/better-sqlite3": "^7.6.9" }, "packageManager": "yarn@4.1.1", "license": "Apache-2.0" diff --git a/a3p-integration/proposals/a:upgrade-next/agd-tools.js b/a3p-integration/proposals/a:upgrade-next/agd-tools.js new file mode 100644 index 00000000000..8570763772c --- /dev/null +++ b/a3p-integration/proposals/a:upgrade-next/agd-tools.js @@ -0,0 +1,138 @@ +import { + agd, + agops, + agopsLocation, + executeCommand, + VALIDATORADDR, + executeOffer, + GOV1ADDR, + GOV2ADDR, + GOV3ADDR, + newOfferId, + CHAINID, +} from '@agoric/synthetic-chain'; + +const ORACLE_ADDRESSES = [GOV1ADDR, GOV2ADDR, GOV3ADDR]; + +export const BID_OFFER_ID = 'bid-vaultUpgrade-test3'; + +const agdQuery = path => + agd.query('vstorage', 'data', '--output', 'json', path); + +const getQuoteBody = async path => { + const queryout = await agdQuery(path); + + const body = JSON.parse(JSON.parse(queryout.value).values[0]); + return JSON.parse(body.body.substring(1)); +}; + +export const getOracleInstance = async price => { + const instanceRec = await agdQuery(`published.agoricNames.instance`); + + const value = JSON.parse(instanceRec.value); + const body = JSON.parse(value.values.at(-1)); + + const feeds = JSON.parse(body.body.substring(1)); + const feedName = `${price}-USD price feed`; + + const key = Object.keys(feeds).find(k => feeds[k][0] === feedName); + if (key) { + return body.slots[key]; + } + return null; +}; + +export const checkForOracle = async (t, name) => { + const instance = await getOracleInstance(name); + t.truthy(instance); +}; + +export const addOraclesForBrand = async (brandIn, oraclesByBrand) => { + await null; + const promiseArray = []; + + const oraclesWithID = []; + // newOfferId() waits 1 second + const offerIdBase = await newOfferId(); + for (let i = 0; i < ORACLE_ADDRESSES.length; i += 1) { + const oracleAddress = ORACLE_ADDRESSES[i]; + const offerId = `${offerIdBase}.${i}`; + oraclesWithID.push({ address: oracleAddress, offerId }); + + promiseArray.push( + executeOffer( + oracleAddress, + agops.oracle('accept', '--offerId', offerId, `--pair ${brandIn}.USD`), + ), + ); + } + oraclesByBrand.set(brandIn, oraclesWithID); + + return Promise.all(promiseArray); +}; + +export const pushPrices = (price, brandIn, oraclesByBrand) => { + const promiseArray = []; + + for (const oracle of oraclesByBrand.get(brandIn)) { + promiseArray.push( + executeOffer( + oracle.address, + agops.oracle( + 'pushPriceRound', + '--price', + price, + '--oracleAdminAcceptOfferId', + oracle.offerId, + ), + ), + ); + } + + return Promise.all(promiseArray); +}; + +export const getPriceQuote = async price => { + const path = `published.priceFeed.${price}-USD_price_feed`; + const body = await getQuoteBody(path); + return body.amountOut.value; +}; + +export const agopsInter = (...params) => { + const newParams = ['inter', ...params]; + return executeCommand(agopsLocation, newParams); +}; + +export const createBid = (price, addr, offerId) => { + return agopsInter( + 'bid', + 'by-price', + `--price ${price}`, + `--give 1.0IST`, + '--from', + addr, + '--keyring-backend test', + `--offer-id ${offerId}`, + ); +}; + +export const getLiveOffers = async addr => { + const path = `published.wallet.${addr}.current`; + const body = await getQuoteBody(path); + return body.liveOffers; +}; + +export const getAuctionCollateral = async index => { + const path = `published.auction.book${index}`; + const body = await getQuoteBody(path); + return body.collateralAvailable.value; +}; + +export const bankSend = (addr, wanted) => { + const chain = ['--chain-id', CHAINID]; + const from = ['--from', VALIDATORADDR]; + const testKeyring = ['--keyring-backend', 'test']; + const noise = [...from, ...chain, ...testKeyring, '--yes']; + + return agd.tx('bank', 'send', VALIDATORADDR, addr, wanted, ...noise); +}; diff --git a/a3p-integration/proposals/a:upgrade-next/post.test.js b/a3p-integration/proposals/a:upgrade-next/post.test.js new file mode 100644 index 00000000000..b8fef1f102f --- /dev/null +++ b/a3p-integration/proposals/a:upgrade-next/post.test.js @@ -0,0 +1,15 @@ +import test from 'ava'; + +import { USER1ADDR, waitForBlock } from '@agoric/synthetic-chain'; + +import { getLiveOffers } from './agd-tools.js'; + +// We might have to wait a full cycle for the auction to settle. That's too +// long for a test, so never mind. +test.serial.skip('trigger auction', async t => { + await waitForBlock(2); + + const liveOffer = await getLiveOffers(USER1ADDR); + t.log({ liveOffer }); + t.is(liveOffer.length, 0, 'There should be no liveOffers remaining'); +}); diff --git a/a3p-integration/proposals/a:upgrade-next/priceFeed.test.js b/a3p-integration/proposals/a:upgrade-next/priceFeed.test.js index 5dba0181116..a82933670a6 100644 --- a/a3p-integration/proposals/a:upgrade-next/priceFeed.test.js +++ b/a3p-integration/proposals/a:upgrade-next/priceFeed.test.js @@ -1,48 +1,25 @@ import test from 'ava'; import { - agd, agops, - executeOffer, + ATOM_DENOM, + getISTBalance, getVatDetails, - GOV1ADDR, - GOV2ADDR, - GOV3ADDR, - newOfferId, + openVault, + USER1ADDR, } from '@agoric/synthetic-chain'; -const ORACLE_ADDRESSES = [GOV1ADDR, GOV2ADDR, GOV3ADDR]; - -const getOracleInstance = async price => { - const instanceRec = await agd.query( - 'vstorage', - 'data', - '--output', - 'json', - `published.agoricNames.instance`, - ); - - // agd query -o json vstorage data published.agoricNames.instance - // |& jq '.value | fromjson | .values[-1] | fromjson | .body[1:] - // | fromjson | .[-2] ' - - const value = JSON.parse(instanceRec.value); - const body = JSON.parse(value.values.at(-1)); - - const feeds = JSON.parse(body.body.substring(1)); - const feedName = `${price}-USD price feed`; - - const key = Object.keys(feeds).find(k => feeds[k][0] === feedName); - if (key) { - return body.slots[key]; - } - return null; -}; - -const checkForOracle = async (t, name) => { - const instance = await getOracleInstance(name); - t.truthy(instance); -}; +import { getDetailsMatchingVats } from './vatDetails.js'; +import { + addOraclesForBrand, + bankSend, + BID_OFFER_ID, + checkForOracle, + createBid, + getLiveOffers, + getPriceQuote, + pushPrices, +} from './agd-tools.js'; test.serial('check all priceFeed vats updated', async t => { const atomDetails = await getVatDetails('ATOM-USD_price_feed'); @@ -54,91 +31,34 @@ test.serial('check all priceFeed vats updated', async t => { t.is(stOsmoDetails.incarnation, 0); const stTiaDetails = await getVatDetails('stTIA'); t.is(stTiaDetails.incarnation, 0); - await checkForOracle(t, 'ATOM'); - await checkForOracle(t, 'stATOM'); - await checkForOracle(t, 'stTIA'); - await checkForOracle(t, 'stOSMO'); + await Promise.all([ + checkForOracle(t, 'ATOM'), + checkForOracle(t, 'stATOM'), + checkForOracle(t, 'stTIA'), + checkForOracle(t, 'stOSMO'), + ]); }); const oraclesByBrand = new Map(); -const addOraclesForBrand = async brandIn => { - await null; - const promiseArray = []; - - const oraclesWithID = []; - for (const oracleAddress of ORACLE_ADDRESSES) { - const offerId = await newOfferId(); - oraclesWithID.push({ address: oracleAddress, offerId }); - - promiseArray.push( - executeOffer( - oracleAddress, - agops.oracle('accept', '--offerId', offerId, `--pair ${brandIn}.USD`), - ), - ); - } - oraclesByBrand.set(brandIn, oraclesWithID); - - return Promise.all(promiseArray); -}; - -const pushPrices = (price = 10.0, brandIn) => { - const promiseArray = []; - - for (const oracle of oraclesByBrand.get(brandIn)) { - promiseArray.push( - executeOffer( - oracle.address, - agops.oracle( - 'pushPriceRound', - '--price', - price, - '--oracleAdminAcceptOfferId', - oracle.offerId, - ), - ), - ); - } - - return Promise.all(promiseArray); -}; - -const getPriceQuote = async price => { - const priceQuote = await agd.query( - 'vstorage', - 'data', - '--output', - 'json', - `published.priceFeed.${price}-USD_price_feed`, - ); - - const body = JSON.parse(JSON.parse(priceQuote.value).values[0]); - const bodyTruncated = JSON.parse(body.body.substring(1)); - return bodyTruncated.amountOut.value; -}; - test.serial('push prices', async t => { // There are no old prices for the other currencies. - t.log('awaiting ATOM price pre'); const atomOutPre = await getPriceQuote('ATOM'); t.is(atomOutPre, '+12010000'); t.log('adding oracle for each brand'); - await addOraclesForBrand('ATOM'); - await addOraclesForBrand('stATOM'); - await addOraclesForBrand('stTIA'); - await addOraclesForBrand('stOSMO'); + await addOraclesForBrand('ATOM', oraclesByBrand); + await addOraclesForBrand('stATOM', oraclesByBrand); + await addOraclesForBrand('stTIA', oraclesByBrand); + await addOraclesForBrand('stOSMO', oraclesByBrand); t.log('pushing new prices'); - await pushPrices(11.2, 'ATOM'); - await pushPrices(11.3, 'stTIA'); - await pushPrices(11.4, 'stATOM'); - await pushPrices(11.5, 'stOSMO'); + await pushPrices(11.2, 'ATOM', oraclesByBrand); + await pushPrices(11.3, 'stTIA', oraclesByBrand); + await pushPrices(11.4, 'stATOM', oraclesByBrand); + await pushPrices(11.5, 'stOSMO', oraclesByBrand); t.log('awaiting new quotes'); - // agd query -o json vstorage data published.priceFeed.stOSMO-USD_price_feed |& - // jq '.value | fromjson | .values[0] | fromjson | .body[1:] | fromjson | .amountOut.value' const atomOut = await getPriceQuote('ATOM'); t.is(atomOut, '+11200000'); const tiaOut = await getPriceQuote('stTIA'); @@ -148,3 +68,41 @@ test.serial('push prices', async t => { const osmoOut = await getPriceQuote('stOSMO'); t.is(osmoOut, '+11500000'); }); + +test.serial('create new bid', async t => { + await createBid('20', USER1ADDR, BID_OFFER_ID); + const liveOffer = await getLiveOffers(USER1ADDR); + t.true(liveOffer[0].includes(BID_OFFER_ID)); +}); + +test.serial('open a marginal vault', 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}`, + ); +}); + +test.serial('trigger auction', async t => { + await pushPrices(5.2, 'ATOM', oraclesByBrand); + + const atomOut = await getPriceQuote('ATOM'); + t.is(atomOut, '+5200000'); +}); + +test.serial('new auction vat', 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); +}); diff --git a/a3p-integration/proposals/a:upgrade-next/test.sh b/a3p-integration/proposals/a:upgrade-next/test.sh index 6c8533d07f0..ab23e8a5c24 100755 --- a/a3p-integration/proposals/a:upgrade-next/test.sh +++ b/a3p-integration/proposals/a:upgrade-next/test.sh @@ -1,5 +1,7 @@ #!/bin/bash +GLOBIGNORE=initial.test.js:post.test.js + # Place here any test that should be executed using the executed proposal. # The effects of this step are not persisted in further proposal layers. @@ -7,5 +9,6 @@ yarn ava initial.test.js # test more, in ways that changes system state -GLOBIGNORE=initial.test.js yarn ava ./*.test.js + +yarn ava post.test.js diff --git a/a3p-integration/proposals/a:upgrade-next/vatDetails.js b/a3p-integration/proposals/a:upgrade-next/vatDetails.js new file mode 100644 index 00000000000..ccf24608309 --- /dev/null +++ b/a3p-integration/proposals/a:upgrade-next/vatDetails.js @@ -0,0 +1,100 @@ +import dbOpenAmbient from 'better-sqlite3'; + +const HOME = process.env.HOME; + +/** @type {(val: T | undefined) => T} */ +export const NonNullish = val => { + if (!val) throw Error('required'); + return val; +}; + +/** + * @file look up vat incarnation from kernel DB + * @see {getIncarnation} + */ + +const swingstorePath = `${HOME}/.agoric/data/agoric/swingstore.sqlite`; + +/** + * SQL short-hand + * + * @param {import('better-sqlite3').Database} db + */ +export const dbTool = db => { + const prepare = (strings, ...params) => { + const dml = strings.join('?'); + return { stmt: db.prepare(dml), params }; + }; + const sql = (strings, ...args) => { + const { stmt, params } = prepare(strings, ...args); + return stmt.all(...params); + }; + sql.get = (strings, ...args) => { + const { stmt, params } = prepare(strings, ...args); + return stmt.get(...params); + }; + return sql; +}; + +/** + * @param {import('better-sqlite3').Database} db + */ +const makeSwingstore = db => { + const sql = dbTool(db); + + /** @param {string} key */ + const kvGet = key => sql.get`select * from kvStore where key = ${key}`.value; + /** @param {string} key */ + const kvGetJSON = key => JSON.parse(kvGet(key)); + + /** @param {string} vatID */ + const lookupVat = vatID => { + return Object.freeze({ + source: () => kvGetJSON(`${vatID}.source`), + options: () => kvGetJSON(`${vatID}.options`), + currentSpan: () => + sql.get`select * from transcriptSpans where isCurrent = 1 and vatID = ${vatID}`, + }); + }; + + return Object.freeze({ + /** @param {string} vatName */ + findVat: vatName => { + /** @type {string[]} */ + const dynamicIDs = kvGetJSON('vat.dynamicIDs'); + const targetVat = dynamicIDs.find(vatID => + lookupVat(vatID).options().name.includes(vatName), + ); + if (!targetVat) throw Error(`vat not found: ${vatName}`); + return targetVat; + }, + /** @param {string} vatName */ + findVats: vatName => { + /** @type {string[]} */ + const dynamicIDs = kvGetJSON('vat.dynamicIDs'); + return dynamicIDs.filter(vatID => + lookupVat(vatID).options().name.includes(vatName), + ); + }, + lookupVat, + }); +}; + +/** @param {string} vatName */ +export const getDetailsMatchingVats = async vatName => { + const kStore = makeSwingstore( + dbOpenAmbient(swingstorePath, { readonly: true }), + ); + + const vatIDs = kStore.findVats(vatName); + const infos = []; + for (const vatID of vatIDs) { + const vatInfo = kStore.lookupVat(vatID); + const source = vatInfo.source(); + // @ts-expect-error cast + const { incarnation } = vatInfo.currentSpan(); + infos.push({ vatName, vatID, incarnation, ...source }); + } + + return infos; +}; diff --git a/a3p-integration/yarn.lock b/a3p-integration/yarn.lock index 64be7d6a2ce..02cdd029411 100644 --- a/a3p-integration/yarn.lock +++ b/a3p-integration/yarn.lock @@ -69,6 +69,24 @@ __metadata: languageName: node linkType: hard +"@types/better-sqlite3@npm:^7.6.9": + version: 7.6.10 + resolution: "@types/better-sqlite3@npm:7.6.10" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/11c4da950e0e1a31270e8c7d98ba34fa5a28fbd3280ffa75945983291d2ec5bc87a9b3b378c21c042249a415d557066a0431da568b83ff9e1bac53eddf4f5adc + languageName: node + linkType: hard + +"@types/node@npm:*": + version: 20.12.7 + resolution: "@types/node@npm:20.12.7" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 10c0/dce80d63a3b91892b321af823d624995c61e39c6a223cc0ac481a44d337640cc46931d33efb3beeed75f5c85c3bda1d97cef4c5cd4ec333caf5dee59cff6eca0 + languageName: node + linkType: hard + "abbrev@npm:^2.0.0": version: 2.0.0 resolution: "abbrev@npm:2.0.0" @@ -973,6 +991,7 @@ __metadata: resolution: "root-workspace-0b6124@workspace:." dependencies: "@agoric/synthetic-chain": "npm:^0.0.10" + "@types/better-sqlite3": "npm:^7.6.9" languageName: unknown linkType: soft @@ -1190,6 +1209,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 10c0/bb673d7876c2d411b6eb6c560e0c571eef4a01c1c19925175d16e3a30c4c428181fb8d7ae802a261f283e4166a0ac435e2f505743aa9e45d893f9a3df017b501 + languageName: node + linkType: hard + "unique-filename@npm:^3.0.0": version: 3.0.0 resolution: "unique-filename@npm:3.0.0"