Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Anil/price feed tests #4

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions a3p-integration/proposals/z:acceptance/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@endo/far": "^1.1.5",
"@endo/init": "^1.1.4",
"ava": "^6.1.2",
"better-sqlite3": "11.5.0",
"execa": "^9.3.1",
"tsx": "^4.17.0"
},
Expand Down
210 changes: 210 additions & 0 deletions a3p-integration/proposals/z:acceptance/priceFeed.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/* eslint-env node */

/**
* @file The purpose of this test is to make sure;
* - Old priceFeed and scaledPriceAuthority vats that are replaced with new ones are truly quiescent.
* The method we use for this is to check if those vats received any deliveries from swingset that
* are of type "message" or "notify" (We give delivery types related to GC a pass since GC cycles
* aren't in our control).
* - Make sure new price feeds can produce quotes
* - Make sure vaults receive quotes
*/

import test from 'ava';
import '@endo/init';
import {
agd,
agoric,
generateOracleMap,
getPriceQuote,
GOV1ADDR,
GOV2ADDR,
GOV3ADDR,
pushPrices,
registerOraclesForBrand,
} from '@agoric/synthetic-chain';
import { snapshotVat } from './test-lib/vat-helpers.js';
import {
bankSend,
ensureGCDeliveryOnly,
getQuoteFromVault,
pollRoundIdAndPushPrice,
scale6,
} from './test-lib/priceFeed-lib.js';
import {
retryUntilCondition,
waitUntilOfferResult,
} from './test-lib/sync-tools.js';

const ambientAuthority = {
query: agd.query,
follow: agoric.follow,
setTimeout,
};

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',
},
},
};

/**
* https://github.com/Agoric/agoric-sdk/pull/10074 introduced new price feeds to the system.
* However, `f:replace-price-feeds` does not activate oracles for future layers of the build.
* Meaning, proposals running after `f:replace-price-feeds` will not have oracles that received
* invitationMakers for new price feeds and there will not be quotes published by new
* price feeds. There are conflicting work to fix this issue, see;
* - https://github.com/Agoric/agoric-sdk/pull/10296
* - https://github.com/Agoric/agoric-sdk/pull/10317
* - https://github.com/Agoric/agoric-sdk/pull/10296#pullrequestreview-2389390624
*
* The purpose of init() is to unblock testing new price feeds from the situation above. We can remove
* this when it resolves.
*
* @param {Map<string,Array<{address: string; offerId: string}>>} oraclesByBrand
*/
const init = async oraclesByBrand => {
const retryOptions = {
log: console.log,
maxRetries: 5,
retryIntervalMs: 3000,
};

const atomInviteOffers = [];
registerOraclesForBrand('ATOM', oraclesByBrand);
// @ts-expect-error we expect oraclesByBrand.get('ATOM') will not return undefined
for (const { address, offerId } of oraclesByBrand.get('ATOM')) {
const offerP = waitUntilOfferResult(
address,
offerId,
false,
ambientAuthority,
{
errorMessage: `ERROR: ${address} could not accept invite, offerID: ${offerId}`,
...retryOptions,
},
);
atomInviteOffers.push(offerP);
}
await Promise.all(atomInviteOffers);

const stAtomInviteOffers = [];
registerOraclesForBrand('stATOM', oraclesByBrand);
// @ts-expect-error we expect oraclesByBrand.get('ATOM') will not return undefined
for (const { address, offerId } of oraclesByBrand.get('stATOM')) {
const offerP = waitUntilOfferResult(
address,
offerId,
false,
ambientAuthority,
{
errorMessage: `ERROR: ${address} could not accept invite, offerID: ${offerId}`,
...retryOptions,
},
);

stAtomInviteOffers.push(offerP);
}
await Promise.all(stAtomInviteOffers);

await pushPrices(1, 'ATOM', oraclesByBrand, 1);
// await waitForBlock(3);
await retryUntilCondition(
() => getPriceQuote('ATOM'),
res => res === '+1000000',
'ATOM quote not received',
{ ...retryOptions, setTimeout },
);
await pushPrices(1, 'stATOM', oraclesByBrand, 1);
await retryUntilCondition(
() => getPriceQuote('stATOM'),
res => res === '+1000000',
'stATOM quote not received',
{ ...retryOptions, setTimeout },
);
};

/**
* @typedef {Map<string, Array<{ address: string; offerId: string }>>} OraclesByBrand
*/

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);

const oraclesByBrand = generateOracleMap('z-acc', ['ATOM', 'stATOM']);
t.log(oraclesByBrand);

await init(oraclesByBrand);
t.context = {
oraclesByBrand,
};
});

test.serial('snapshot state', t => {
config.vatNames.forEach(name => {

Check warning on line 162 in a3p-integration/proposals/z:acceptance/priceFeed.test.js

View workflow job for this annotation

GitHub Actions / lint-rest

Prefer for...of instead of Array.forEach
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.name, ATOM.price, oraclesByBrand);
await pollRoundIdAndPushPrice(stATOM.name, stATOM.price, oraclesByBrand);

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('snapshot state after price pushed', t => {
config.vatNames.forEach(name => {

Check warning on line 187 in a3p-integration/proposals/z:acceptance/priceFeed.test.js

View workflow job for this annotation

GitHub Actions / lint-rest

Prefer for...of instead of Array.forEach
config.snapshots.after[name] = snapshotVat(name);
});
console.dir(config.snapshots, { depth: null });
t.pass();
});

test.serial('ensure only gc', t => {
ensureGCDeliveryOnly(config.snapshots);
t.pass();
});

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.is(atomVaultQuote, scale6(ATOM.price).toString());
t.is(stAtomVaultQuote, scale6(stATOM.price).toString());
});
143 changes: 143 additions & 0 deletions a3p-integration/proposals/z:acceptance/test-lib/priceFeed-lib.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import {
agd,
CHAINID,
VALIDATORADDR,
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<string, number>
* >} SnapshotItem
*
* @typedef {Record<string, SnapshotItem>} Snapshots
*/

/**
* Import from synthetic-chain once it is updated
*
* @param {string} addr
* @param {string} wanted
* @param {string} [from]
*/
export const bankSend = (addr, wanted, from = VALIDATORADDR) => {
const chain = ['--chain-id', CHAINID];
const fromArg = ['--from', from];
const testKeyring = ['--keyring-backend', 'test'];
const noise = [...fromArg, ...chain, ...testKeyring, '--yes'];

return agd.tx('bank', 'send', from, addr, wanted, ...noise);
};

/**
* Import from synthetic-chain when https://github.com/Agoric/agoric-3-proposals/pull/183 is in
*
* @param {string} price
* @param {{
* agoric?: { follow: () => Promise<object>};
* prefix?: string
* }} io
* @returns

Check warning on line 56 in a3p-integration/proposals/z:acceptance/test-lib/priceFeed-lib.js

View workflow job for this annotation

GitHub Actions / lint-rest

Missing JSDoc @returns type
*/
export const getRoundId = async (price, io = {}) => {
const { agoric = { follow: agoricAmbient.follow }, prefix = 'published.' } =
io;
const path = `:${prefix}priceFeed.${price}-USD_price_feed.latestRound`;
const round = await agoric.follow('-lF', path);
return parseInt(round.roundId, 10);
};

/**
*
* @param {string} brandIn
* @param {number} price
* @param {import('../priceFeed.test.js').OraclesByBrand} oraclesByBrand
*/
export const pollRoundIdAndPushPrice = async (
brandIn,
price,
oraclesByBrand,
) => {
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 => {

Check warning on line 86 in a3p-integration/proposals/z:acceptance/test-lib/priceFeed-lib.js

View workflow job for this annotation

GitHub Actions / lint-rest

Prefer for...of instead of Array.forEach
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;
};
Loading
Loading