diff --git a/a3p-integration/proposals/z:acceptance/package.json b/a3p-integration/proposals/z:acceptance/package.json index a5910e5ded6..e5941bd1a8d 100644 --- a/a3p-integration/proposals/z:acceptance/package.json +++ b/a3p-integration/proposals/z:acceptance/package.json @@ -16,7 +16,8 @@ "@endo/init": "^1.1.4", "ava": "^6.1.2", "execa": "^9.3.1", - "tsx": "^4.17.0" + "tsx": "^4.17.0", + "better-sqlite3": "11.5.0" }, "ava": { "concurrency": 1, diff --git a/a3p-integration/proposals/z:acceptance/priceFeed.test.js b/a3p-integration/proposals/z:acceptance/priceFeed.test.js index ba1037c47ef..94be7d731ac 100644 --- a/a3p-integration/proposals/z:acceptance/priceFeed.test.js +++ b/a3p-integration/proposals/z:acceptance/priceFeed.test.js @@ -1,11 +1,6 @@ import test from 'ava'; +import '@endo/init'; import { - getTranscriptItemsForVat, - getVatsWithSameName, - swingStore, -} from './test-lib/vat-helpers.js'; -import { - addPreexistingOracles, generateOracleMap, getPriceQuote, GOV1ADDR, @@ -15,8 +10,38 @@ import { registerOraclesForBrand, waitForBlock, } from '@agoric/synthetic-chain'; -import { bankSend, pollRoundIdAndPushPrice } from './test-lib/priceFeed-lib.js'; +import { snapshotVat } from './test-lib/vat-helpers.js'; +import { + bankSend, + ensureGCDeliveryOnly, + getQuoteFromVault, + pollRoundIdAndPushPrice, + scale6, +} from './test-lib/priceFeed-lib.js'; + +const config = { + vatNames: [ + '-scaledPriceAuthority-stATOM', + '-scaledPriceAuthority-ATOM', + '-stATOM-USD_price_feed', + '-ATOM-USD_price_feed', + ], + snapshots: { before: {}, after: {} }, // Will be filled in the runtime + priceFeeds: { + ATOM: { + price: 29, + managerIndex: 0, + name: 'ATOM', + }, + stATOM: { + price: 25, + managerIndex: 1, + name: 'stATOM', + }, + }, +}; +// Remove this one when #10296 goes in const init = async oraclesByBrand => { await registerOraclesForBrand('ATOM', oraclesByBrand); await waitForBlock(3); @@ -26,13 +51,13 @@ const init = async oraclesByBrand => { await pushPrices(1, 'ATOM', oraclesByBrand, 1); await waitForBlock(3); await pushPrices(1, 'stATOM', oraclesByBrand, 1); -} +}; /** * @typedef {Map>} OraclesByBrand */ -test.before.skip(async t => { +test.before(async t => { // Fund each oracle members with 10IST incase we hit batch limit here https://github.com/Agoric/agoric-sdk/issues/6525 await bankSend(GOV2ADDR, '10000000uist', GOV1ADDR); await bankSend(GOV3ADDR, '10000000uist', GOV1ADDR); @@ -46,33 +71,53 @@ test.before.skip(async t => { }; }); -test.skip('push-price', async t => { +test.serial('snapshot state', t => { + config.vatNames.forEach(name => { + config.snapshots.before[name] = snapshotVat(name); + }); + console.dir(config.snapshots, { depth: null }); + t.pass(); +}); + +test.serial('push-price', async t => { // @ts-expect-error casting const { oraclesByBrand } = t.context; + const { + priceFeeds: { ATOM, stATOM }, + } = config; - await pollRoundIdAndPushPrice('ATOM', 25, oraclesByBrand); - await pollRoundIdAndPushPrice('stATOM', 21, oraclesByBrand); + await pollRoundIdAndPushPrice(ATOM.name, ATOM.price, oraclesByBrand); + await pollRoundIdAndPushPrice(stATOM.name, stATOM.price, oraclesByBrand); - const atomOut = await getPriceQuote('ATOM'); - t.is(atomOut, '+25000000'); - const stAtomOut = await getPriceQuote('stATOM'); - t.is(stAtomOut, '+21000000'); + const atomOut = await getPriceQuote(ATOM.name); + t.is(atomOut, `+${scale6(ATOM.price)}`); + const stAtomOut = await getPriceQuote(stATOM.name); + t.is(stAtomOut, `+${scale6(stATOM.price)}`); t.pass(); }); -test.serial('dum', async t => { - // const stATOM = await getVatsWithSameName('-stATOM-USD_price_feed'); - // const scaledStATOM = await getVatsWithSameName( - // '-scaledPriceAuthority-stATOM', - // ); - // t.log(scaledStATOM); - const { findVatsAll, lookupVat, findVatsExact } = swingStore; +test.serial('snapshot state after price pushed', t => { + config.vatNames.forEach(name => { + config.snapshots.after[name] = snapshotVat(name); + }); + console.dir(config.snapshots, { depth: null }); + t.pass(); +}); - // const items = findVatsAll('scaledPriceAuthority'); - // items.forEach(id => console.log(lookupVat(id).options(), id)); +test.serial('ensure only gc', t => { + ensureGCDeliveryOnly(config.snapshots); + t.pass(); +}); - const stATOMScaledPAs = findVatsExact('-scaledPriceAuthority-stATOM'); - stATOMScaledPAs.forEach(id => console.log(getTranscriptItemsForVat(id, 1).map((element) =>( JSON.parse(element.item).d[0])))); +test.serial('make sure vaults got the prices', async t => { + const { + priceFeeds: { ATOM, stATOM }, + } = config; + const [atomVaultQuote, stAtomVaultQuote] = await Promise.all([ + getQuoteFromVault(ATOM.managerIndex), + getQuoteFromVault(stATOM.managerIndex), + ]); - t.pass(); + t.is(atomVaultQuote, scale6(ATOM.price).toString()); + t.is(stAtomVaultQuote, scale6(stATOM.price).toString()); }); diff --git a/a3p-integration/proposals/z:acceptance/test-lib/priceFeed-lib.js b/a3p-integration/proposals/z:acceptance/test-lib/priceFeed-lib.js index c01518d08fe..a7f98d26b8e 100644 --- a/a3p-integration/proposals/z:acceptance/test-lib/priceFeed-lib.js +++ b/a3p-integration/proposals/z:acceptance/test-lib/priceFeed-lib.js @@ -5,6 +5,29 @@ import { agoric as agoricAmbient, pushPrices, } from '@agoric/synthetic-chain'; +import { Fail, q } from '@endo/errors'; +import { getTranscriptItemsForVat } from './vat-helpers.js'; + +/** + * By the time we push prices to the new price feed vat, the old one might receive + * some deliveries related to GC events. These delivery types might be; 'dropExports', + * 'retireExports', 'retireImports', 'bringOutYourDead'. + * + * Even though we don't expect to receive all these types of deliveries at once; + * choosing MAX_DELIVERIES_ALLOWED = 5 seems reasonable. + */ +const MAX_DELIVERIES_ALLOWED = 5; + +export const scale6 = x => BigInt(x * 1000000); + +/** + * @typedef {Record< + * string, + * Record + * >} SnapshotItem + * + * @typedef {Record} Snapshots + */ /** * Import from synthetic-chain once it is updated @@ -37,7 +60,7 @@ export const getRoundId = async (price, io = {}) => { io; const path = `:${prefix}priceFeed.${price}-USD_price_feed.latestRound`; const round = await agoric.follow('-lF', path); - return parseInt(round.roundId); + return parseInt(round.roundId, 10); }; /** @@ -54,3 +77,67 @@ export const pollRoundIdAndPushPrice = async ( const roundId = await getRoundId(brandIn); await pushPrices(price, brandIn, oraclesByBrand, roundId + 1); }; + +/** + * @param {SnapshotItem} snapShotItem + */ +export const getQuiescentVats = snapShotItem => { + const quiescentVats = {}; + [...Object.values(snapShotItem)].forEach(vats => { + const keyOne = Object.keys(vats)[0]; + const keyTwo = Object.keys(vats)[1]; + + return parseInt(keyOne.substring(1), 10) > parseInt(keyTwo.substring(1), 10) + ? (quiescentVats[keyTwo] = vats[keyTwo]) + : (quiescentVats[keyOne] = vats[keyOne]); + }); + + return quiescentVats; +}; + +/** + * + * @param {Snapshots} snapshots + * @param {{ getTranscriptItems?: () => Array}} io + */ +export const ensureGCDeliveryOnly = (snapshots, io = {}) => { + const { getTranscriptItems = getTranscriptItemsForVat } = io; + + const { after, before } = snapshots; + const quiescentVatsBefore = getQuiescentVats(before); + const quiescentVatsAfter = getQuiescentVats(after); + + console.dir(quiescentVatsBefore, { depth: null }); + console.dir(quiescentVatsAfter, { depth: null }); + + [...Object.entries(quiescentVatsBefore)].forEach(([vatId, position]) => { + const afterPosition = quiescentVatsAfter[vatId]; + const messageDiff = afterPosition - position; + console.log(vatId, messageDiff); + + if (messageDiff > MAX_DELIVERIES_ALLOWED) + Fail`${q(messageDiff)} deliveries is greater than maximum allowed: ${q(MAX_DELIVERIES_ALLOWED)}`; + else if (messageDiff === 0) return; + + const transcripts = getTranscriptItems(vatId, messageDiff); + console.log('TRANSCRIPTS', transcripts); + + transcripts.forEach(({ item }) => { + const deliveryType = JSON.parse(item).d[0]; + console.log('DELIVERY TYPE', deliveryType); + if (deliveryType === 'notify' || deliveryType === 'message') + Fail`DeliveryType ${q(deliveryType)} is not GC delivery`; + }); + }); +}; + +/** + * @param {number} managerIndex + */ +export const getQuoteFromVault = async managerIndex => { + const res = await agoricAmbient.follow( + '-lF', + `:published.vaultFactory.managers.manager${managerIndex}.quotes`, + ); + return res.quoteAmount.value[0].amountOut.value; +}; diff --git a/a3p-integration/proposals/z:acceptance/test-lib/priceFeed-lib.test.js b/a3p-integration/proposals/z:acceptance/test-lib/priceFeed-lib.test.js new file mode 100644 index 00000000000..5d2adad73ee --- /dev/null +++ b/a3p-integration/proposals/z:acceptance/test-lib/priceFeed-lib.test.js @@ -0,0 +1,103 @@ +import test from 'ava'; +import '@endo/init'; +import { ensureGCDeliveryOnly } from './priceFeed-lib.js'; + +const testConfig = { + before: { + '-scaledPriceAuthority-stATOM': { v58: 13, v74: 119 }, + '-scaledPriceAuthority-ATOM': { v46: 77, v73: 178 }, + '-stATOM-USD_price_feed': { v57: 40, v72: 192 }, + '-ATOM-USD_price_feed': { v29: 100, v70: 247 }, + }, + after: { + '-scaledPriceAuthority-stATOM': { v58: 15, v74: 119 }, + '-scaledPriceAuthority-ATOM': { v46: 79, v73: 178 }, + '-stATOM-USD_price_feed': { v57: 42, v72: 192 }, + '-ATOM-USD_price_feed': { v29: 102, v70: 247 }, + }, +}; + +const makeFakeGetTranscriptItemsForVat = ( + deliveryType, + maximumAllowedDeliveries, +) => { + const fakeGetTranscriptItemsForVat = (_, number) => { + const fakeTranscriptItems = []; + for (let i = 0; i < number; i += 1) { + const item = { d: [deliveryType] }; + fakeTranscriptItems.push({ item: JSON.stringify(item) }); + } + return fakeTranscriptItems; + }; + + const tooManyTranscriptItemsForVat = () => { + const fakeTranscriptItems = []; + for (let i = 0; i <= maximumAllowedDeliveries; i += 1) { + const item = { d: [deliveryType] }; + fakeTranscriptItems.push({ item: JSON.stringify(item) }); + } + return fakeTranscriptItems; + }; + + return { fakeGetTranscriptItemsForVat, tooManyTranscriptItemsForVat }; +}; + +test('should not throw', t => { + const { fakeGetTranscriptItemsForVat } = + makeFakeGetTranscriptItemsForVat('dropExports'); + + t.notThrows(() => + ensureGCDeliveryOnly(testConfig, { + getTranscriptItems: fakeGetTranscriptItemsForVat, + }), + ); +}); + +test('should throw for "notify"', t => { + const { fakeGetTranscriptItemsForVat } = + makeFakeGetTranscriptItemsForVat('notify'); + + t.throws( + () => + ensureGCDeliveryOnly(testConfig, { + getTranscriptItems: fakeGetTranscriptItemsForVat, + }), + { message: 'DeliveryType "notify" is not GC delivery' }, + ); +}); + +test('should throw for "message"', t => { + const { fakeGetTranscriptItemsForVat } = + makeFakeGetTranscriptItemsForVat('message'); + + t.throws( + () => + ensureGCDeliveryOnly(testConfig, { + getTranscriptItems: fakeGetTranscriptItemsForVat, + }), + { message: 'DeliveryType "message" is not GC delivery' }, + ); +}); + +test('should throw too many deliveries', t => { + const { fakeGetTranscriptItemsForVat } = makeFakeGetTranscriptItemsForVat( + 'dropExports', + 5, + ); + + const config = { + ...testConfig, + after: { + ...testConfig.after, + '-scaledPriceAuthority-stATOM': { v58: 20, v74: 119 }, + }, + }; + + t.throws( + () => + ensureGCDeliveryOnly(config, { + getTranscriptItems: fakeGetTranscriptItemsForVat, + }), + { message: '7 deliveries is greater than maximum allowed: 5' }, + ); +}); diff --git a/a3p-integration/proposals/z:acceptance/test-lib/vat-helpers.js b/a3p-integration/proposals/z:acceptance/test-lib/vat-helpers.js index a9d0d07878c..f4e42d8a3c6 100644 --- a/a3p-integration/proposals/z:acceptance/test-lib/vat-helpers.js +++ b/a3p-integration/proposals/z:acceptance/test-lib/vat-helpers.js @@ -2,8 +2,7 @@ import dbOpenAmbient from 'better-sqlite3'; import { HOME, dbTool } from '@agoric/synthetic-chain'; /** - * @file look up vat incarnation from kernel DB - * @see {getIncarnation} + * @typedef {{position: number; item: string; vatID: string; incarnation: number}} TranscriptItem */ const swingstorePath = '~/.agoric/data/agoric/swingstore.sqlite'; @@ -57,18 +56,20 @@ export const makeSwingstore = db => { ); }, lookupVat, - kvGetJSON, - sql, db, }); }; +const initSwingstore = () => { + const fullPath = swingstorePath.replace(/^~/, HOME); + return makeSwingstore(dbOpenAmbient(fullPath, { readonly: true })); +}; + /** * @param {string} vatName */ export const getVatsWithSameName = async vatName => { - const fullPath = swingstorePath.replace(/^~/, HOME); - const kStore = makeSwingstore(dbOpenAmbient(fullPath, { readonly: true })); + const kStore = initSwingstore(); const vatIDs = kStore.findVatsExact(vatName); const vats = vatIDs.map(id => { @@ -81,22 +82,39 @@ export const getVatsWithSameName = async vatName => { return vats; }; +/** + * + * @param {string} vatId + * @param {number} n + * @returns {Array} + */ export const getTranscriptItemsForVat = (vatId, n = 10) => { - const fullPath = swingstorePath.replace(/^~/, HOME); - const { sql, db } = makeSwingstore( - dbOpenAmbient(fullPath, { readonly: true }), - ); + const { db } = initSwingstore(); - // const items = sql.get`select * from transcriptItems where vatId = ${vatId} order by position desc limit ${n}`; const items = db .prepare( 'select * from transcriptItems where vatId = ? order by position desc limit ?', ) .all(vatId, n); - // console.log(items); + + // @ts-expect-error casting problem when assigning values coming from db return items; }; +export const snapshotVat = vatName => { + const { findVatsExact } = initSwingstore(); + + const snapshots = {}; + const vatIdsWithExactName = findVatsExact(vatName); + vatIdsWithExactName.forEach(id => { + const element = getTranscriptItemsForVat(id, 1)[0]; + + snapshots[id] = element.position; + }); + + return snapshots; +}; + export const swingStore = makeSwingstore( dbOpenAmbient(swingstorePath.replace(/^~/, HOME), { readonly: true }), );