From 641020d2de34e111516ea6dbcf172bf9fd4bdc1e Mon Sep 17 00:00:00 2001 From: Jorge-Lopes Date: Fri, 18 Oct 2024 17:01:53 +0100 Subject: [PATCH 1/2] chore(a3p): setup oracle and push price at use phase rel: https://github.com/Agoric/BytePitchPartnerEng/issues/26 --- .../proposals/f:replace-price-feeds/use.sh | 9 ++++++++ .../verifyPushedPrice.js | 21 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 a3p-integration/proposals/f:replace-price-feeds/use.sh create mode 100644 a3p-integration/proposals/f:replace-price-feeds/verifyPushedPrice.js diff --git a/a3p-integration/proposals/f:replace-price-feeds/use.sh b/a3p-integration/proposals/f:replace-price-feeds/use.sh new file mode 100644 index 000000000000..53304b84e272 --- /dev/null +++ b/a3p-integration/proposals/f:replace-price-feeds/use.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Exit when any command fails +set -e + +source /usr/src/upgrade-test-scripts/env_setup.sh + +./verifyPushedPrice.js 'ATOM' 12.01 +./verifyPushedPrice.js 'stATOM' 12.01 diff --git a/a3p-integration/proposals/f:replace-price-feeds/verifyPushedPrice.js b/a3p-integration/proposals/f:replace-price-feeds/verifyPushedPrice.js new file mode 100644 index 000000000000..e08c5ffcc4eb --- /dev/null +++ b/a3p-integration/proposals/f:replace-price-feeds/verifyPushedPrice.js @@ -0,0 +1,21 @@ +#!/usr/bin/env node + +import { + registerOraclesForBrand, + generateOracleMap, +} from '@agoric/synthetic-chain'; +import { argv } from 'node:process'; +import { verifyPushedPrice } from './test-lib/price-feed.js'; + +const brand = argv[2]; +const price = argv[3]; + +const BASE_ID = 'f-priceFeeds'; +const ROUND_ID = 1; + +const oraclesByBrand = generateOracleMap(BASE_ID, [brand]); +await registerOraclesForBrand(brand, oraclesByBrand); +console.log(`Registering Oracle for ${brand}`); + +await verifyPushedPrice(price, brand, oraclesByBrand, ROUND_ID); +console.log(`Price pushed for ${brand}`); From 9080f35cc4c9caede3f1e85e41eb9d2a7355a234 Mon Sep 17 00:00:00 2001 From: Jorge-Lopes Date: Fri, 18 Oct 2024 17:02:27 +0100 Subject: [PATCH 2/2] test(a3p): update test with new helper functions rel: https://github.com/Agoric/BytePitchPartnerEng/issues/26 --- .../priceFeedUpdate.test.js | 27 ++++--- .../test-lib/price-feed.js | 62 ++++++++++++++++ .../test-lib/sync-tools.js | 72 +++++++++++++++++++ 3 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 a3p-integration/proposals/f:replace-price-feeds/test-lib/price-feed.js create mode 100644 a3p-integration/proposals/f:replace-price-feeds/test-lib/sync-tools.js diff --git a/a3p-integration/proposals/f:replace-price-feeds/priceFeedUpdate.test.js b/a3p-integration/proposals/f:replace-price-feeds/priceFeedUpdate.test.js index 5cbacd019add..b9fbb265fe99 100644 --- a/a3p-integration/proposals/f:replace-price-feeds/priceFeedUpdate.test.js +++ b/a3p-integration/proposals/f:replace-price-feeds/priceFeedUpdate.test.js @@ -14,10 +14,12 @@ import { getVaultPrices, getVatDetails, openVault, - pushPrices, - registerOraclesForBrand, USER1ADDR, } from '@agoric/synthetic-chain'; +import { + getPriceFeedRoundId, + verifyPushedPrice, +} from './test-lib/price-feed.js'; import { BID_OFFER_ID } from './agd-tools.js'; @@ -37,12 +39,16 @@ const checkPriceFeedVatsUpdated = async t => { await checkForOracle(t, 'stATOM'); }; -console.log('adding oracle for each brand'); +/* + * The Oracle for ATOM and stATOM brands are being registered in the offer made at file: + * a3p-integration/proposals/f:replace-price-feeds/verifyPushedPrice.js + */ const oraclesByBrand = generateOracleMap('f-priceFeeds', ['ATOM', 'stATOM']); -await registerOraclesForBrand('ATOM', oraclesByBrand); -await registerOraclesForBrand('stATOM', oraclesByBrand); -let roundId = 1; +const latestAtomRoundId = await getPriceFeedRoundId('ATOM'); +const latestStAtomRoundId = await getPriceFeedRoundId('stATOM'); +let atomRoundId = latestAtomRoundId + 1; +let stAtomRoundId = latestStAtomRoundId + 1; const tryPushPrices = async t => { // There are no old prices for the other currencies. @@ -52,9 +58,10 @@ const tryPushPrices = async t => { // t.is(stAtomOutPre, '+12010000'); t.log('pushing new prices'); - await pushPrices(13.4, 'ATOM', oraclesByBrand, roundId); - await pushPrices(13.7, 'stATOM', oraclesByBrand, roundId); - roundId += 1; + await verifyPushedPrice(13.4, 'ATOM', oraclesByBrand, atomRoundId); + await verifyPushedPrice(13.7, 'stATOM', oraclesByBrand, stAtomRoundId); + atomRoundId += 1; + stAtomRoundId += 1; t.log('awaiting new quotes'); const atomOut = await getPriceQuote('ATOM'); @@ -89,7 +96,7 @@ const openMarginalVault = async t => { }; const triggerAuction = async t => { - await pushPrices(5.2, 'ATOM', oraclesByBrand, roundId); + await verifyPushedPrice(5.2, 'ATOM', oraclesByBrand, atomRoundId); const atomOut = await getPriceQuote('ATOM'); t.is(atomOut, '+5200000'); diff --git a/a3p-integration/proposals/f:replace-price-feeds/test-lib/price-feed.js b/a3p-integration/proposals/f:replace-price-feeds/test-lib/price-feed.js new file mode 100644 index 000000000000..c0e2acd311d9 --- /dev/null +++ b/a3p-integration/proposals/f:replace-price-feeds/test-lib/price-feed.js @@ -0,0 +1,62 @@ +/* eslint-env node */ + +import { + agoric, + getContractInfo, + pushPrices, + getPriceQuote, +} from '@agoric/synthetic-chain'; +import { retryUntilCondition } from './sync-tools.js'; + +export const scale6 = x => BigInt(x * 1_000_000); + +/** + * + * @param {number} price + * @param {string} brand + * @param {Map} oraclesByBrand + * @param {number} roundId + * @returns {Promise} + */ +export const verifyPushedPrice = async ( + price, + brand, + oraclesByBrand, + roundId, +) => { + const pushPriceRetryOpts = { + maxRetries: 5, // arbitrary + retryIntervalMs: 5000, // in ms + }; + + await pushPrices(price, brand, oraclesByBrand, roundId); + console.log(`Pushing price ${price} for ${brand}`); + + await retryUntilCondition( + () => getPriceQuote(brand), + res => res === `+${scale6(price).toString()}`, + 'price not pushed yet', + { + log: console.log, + setTimeout: global.setTimeout, + ...pushPriceRetryOpts, + }, + ); + console.log(`Price ${price} pushed for ${brand}`); +}; + +/** + * + * @param {string} brand + * @returns {Promise} + */ +export const getPriceFeedRoundId = async brand => { + const latestRoundPath = `published.priceFeed.${brand}-USD_price_feed.latestRound`; + const latestRound = await getContractInfo(latestRoundPath, { + agoric, + prefix: '', + }); + + console.log('latestRound: ', latestRound); + return Number(latestRound.roundId); +}; diff --git a/a3p-integration/proposals/f:replace-price-feeds/test-lib/sync-tools.js b/a3p-integration/proposals/f:replace-price-feeds/test-lib/sync-tools.js new file mode 100644 index 000000000000..4a0e727c4653 --- /dev/null +++ b/a3p-integration/proposals/f:replace-price-feeds/test-lib/sync-tools.js @@ -0,0 +1,72 @@ +/* eslint-env node */ + +/** + * @file These tools mostly duplicate code that will be added in other PRs + * and eventually migrated to synthetic-chain. Sorry for the duplication. + */ + +/** + * @typedef {object} RetryOptions + * @property {number} [maxRetries] + * @property {number} [retryIntervalMs] + * @property {(...arg0: string[]) => void} log + * @property {(object) => void} [setTimeout] + * @property {string} [errorMessage=Error] + */ + +const ambientSetTimeout = global.setTimeout; + +/** + * From https://github.com/Agoric/agoric-sdk/blob/442f07c8f0af03281b52b90e90c27131eef6f331/multichain-testing/tools/sleep.ts#L10 + * + * @param {number} ms + * @param {*} sleepOptions + */ +const sleep = (ms, { log = () => {}, setTimeout = ambientSetTimeout }) => + new Promise(resolve => { + log(`Sleeping for ${ms}ms...`); + setTimeout(resolve, ms); + }); + +/** + * From https://github.com/Agoric/agoric-sdk/blob/442f07c8f0af03281b52b90e90c27131eef6f331/multichain-testing/tools/sleep.ts#L24 + * + * @param {() => Promise} operation + * @param {(result: any) => boolean} condition + * @param {string} message + * @param {RetryOptions} options + */ +export const retryUntilCondition = async ( + operation, + condition, + message, + { maxRetries = 6, retryIntervalMs = 3500, log, setTimeout }, +) => { + console.log({ maxRetries, retryIntervalMs, message }); + let retries = 0; + + await null; + while (retries < maxRetries) { + try { + const result = await operation(); + log('RESULT', result); + if (condition(result)) { + return result; + } + } catch (error) { + if (error instanceof Error) { + log(`Error: ${error.message}`); + } else { + log(`Unknown error: ${String(error)}`); + } + } + + retries += 1; + console.log( + `Retry ${retries}/${maxRetries} - Waiting for ${retryIntervalMs}ms for ${message}...`, + ); + await sleep(retryIntervalMs, { log, setTimeout }); + } + + throw Error(`${message} condition failed after ${maxRetries} retries.`); +};