diff --git a/a3p-integration/package.json b/a3p-integration/package.json index 3882c279d1ff..fe183229c7bd 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 000000000000..8570763772cf --- /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 000000000000..47ce49871714 --- /dev/null +++ b/a3p-integration/proposals/a:upgrade-next/post.test.js @@ -0,0 +1,17 @@ +import test from 'ava'; + +import { getAuctionCollateral } from './agd-tools.js'; + +test.serial('trigger auction', async t => { + const collatBefore = await getAuctionCollateral('0'); + t.log({ collatBefore }); + + // We might have to wait a full cycle for the auction to settle. That's too + // long for a test, so never mind. + + // 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 5dba01811162..a82933670a6b 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 6c8533d07f04..ab23e8a5c249 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 000000000000..ccf246083095 --- /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 64be7d6a2ce8..02cdd029411b 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"