From 46c4659b510a3b5fce656dee2039f124d920ad4c Mon Sep 17 00:00:00 2001 From: anilhelvaci Date: Mon, 30 Sep 2024 01:00:54 +0300 Subject: [PATCH] chore: implement sync tools for `z:acceptance` Refs: https://github.com/Agoric/BytePitchPartnerEng/issues/10 --- .../z:acceptance/test-lib/sync-tools.js | 313 +++++++++-- .../z:acceptance/test-lib/sync-tools.test.js | 525 ++++++++++++++++++ 2 files changed, 787 insertions(+), 51 deletions(-) create mode 100644 a3p-integration/proposals/z:acceptance/test-lib/sync-tools.test.js diff --git a/a3p-integration/proposals/z:acceptance/test-lib/sync-tools.js b/a3p-integration/proposals/z:acceptance/test-lib/sync-tools.js index 4a0e727c4653..77683a04d8f7 100644 --- a/a3p-integration/proposals/z:acceptance/test-lib/sync-tools.js +++ b/a3p-integration/proposals/z:acceptance/test-lib/sync-tools.js @@ -1,72 +1,283 @@ -/* 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. + * @file The purpose of this file is to bring together a set of tool that + * developers can use to synchronize operations they carry out in their tests. + * + * These operations include; + * - Making sure a core-eval resulted succesfully deploying a contract + * - Making sure a core-eval succesfully sent zoe invitations to committee members for governance + * - Making sure an account is successfully funded with vbank assets like IST, BLD etc. + * - operation: query dest account's balance + * - condition: dest account has a balance >= sent token + * - Making sure an offer is in a specific state, such as; + * - seated + * - successfuly resulted + * - error + * */ /** - * @typedef {object} RetryOptions - * @property {number} [maxRetries] - * @property {number} [retryIntervalMs] + * @typedef {object} RetyrOptions + * @property {number} maxRetries + * @property {number} retryIntervalMs * @property {(...arg0: string[]) => void} log * @property {(object) => void} [setTimeout] * @property {string} [errorMessage=Error] + * + * + * @typedef {object} CosmosBalanceThresold + * @property {string} denom + * @property {number} value */ -const ambientSetTimeout = global.setTimeout; +const ambientSetTimeout = globalThis.setTimeout; /** - * From https://github.com/Agoric/agoric-sdk/blob/442f07c8f0af03281b52b90e90c27131eef6f331/multichain-testing/tools/sleep.ts#L10 - * - * @param {number} ms - * @param {*} sleepOptions + * + * @param {number} ms + * @param {*} sleepOptions + * @returns */ -const sleep = (ms, { log = () => {}, setTimeout = ambientSetTimeout }) => - new Promise(resolve => { - log(`Sleeping for ${ms}ms...`); - setTimeout(resolve, ms); - }); +export const sleep = ( + ms, + { log = () => { }, setTimeout = ambientSetTimeout } +) => + new Promise(resolve => { + // @ts-ignore + 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 + * + * @param {() => Promise} operation + * @param {(result) => boolean} condition + * @param {string} message + * @param {RetyrOptions} options + * @returns */ -export const retryUntilCondition = async ( - operation, - condition, - message, - { maxRetries = 6, retryIntervalMs = 3500, log, setTimeout }, +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)}`); - } + console.log({ maxRetries, retryIntervalMs, message }); + let retries = 0; + + while (retries < maxRetries) { + try { + const result = await operation(); + console.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++; + console.log( + `Retry ${retries}/${maxRetries} - Waiting for ${retryIntervalMs}ms for ${message}...`, + ); + await sleep(retryIntervalMs, { log, setTimeout }); } - retries += 1; - console.log( - `Retry ${retries}/${maxRetries} - Waiting for ${retryIntervalMs}ms for ${message}...`, + throw Error(`${message} condition failed after ${maxRetries} retries.`); +}; + +export const makeRetryUntilCondition = (defaultOptions) => { + /** + * Retry an asynchronous operation until a condition is met. + * Defaults to maxRetries = 6, retryIntervalMs = 3500 + */ + return ( + operation, + condition, + message, + options, + ) => + retryUntilCondition(operation, condition, message, { + ...defaultOptions, + ...options, + }); +}; + +/** + * Making sure a core-eval resulted succesfully deploying a contract + */ +const makeGetInstances = follow => async () => { + const instanceEntries = await follow( + '-lF', + `:published.agoricNames.instance`, + ); + + return Object.fromEntries(instanceEntries); +}; + +/** + * + * @param {string} contractName + * @param {{follow: () => object, setTimeout: (object) => void}} ambientAuthroity + * @param {RetyrOptions} options + * @returns + */ +export const waitUntilContractDeplyed = (contractName, ambientAuthroity, options) => { + const { follow, setTimeout } = ambientAuthroity; + const getInstances = makeGetInstances(follow); + const { maxRetries = 6, retryIntervalMs = 3500, log = console.log, errorMessage = "Error" } = options; + + return retryUntilCondition( + getInstances, + instanceObject => Object.keys(instanceObject).includes(contractName), + errorMessage, + // @ts-ignore + { maxRetries, retryIntervalMs, log, setTimeout } + ) +}; + +/** + * Making sure an account is successfully funded with vbank assets like IST, BLD etc. + * - operation: query dest account's balance + * - condition: dest account has a balance >= sent token + */ + +const makeQueryCosmosBalace = queryCb => async dest => { + const conins = await queryCb('bank', 'balances', dest); + return conins.balances; +}; + +/** + * + * @param {Array} balances + * @param {CosmosBalanceThresold} thresold + * @returns {boolean} + */ +const checkCosmosBalance = (balances, thresold) => { + const balance = [...balances].find(({ denom }) => denom === thresold.denom); + return Number(balance.amount) >= thresold.value; +} + +/** + * @param {string} destAcct + * @param {{query: () => Promise, setTimeout: (object) => void}} ambientAuthroity + * @param {{denom: string, value: number}} threshold + * @param {RetyrOptions} options + * @returns + */ +export const waitUntilAccountFunded = (destAcct, ambientAuthroity, threshold, options) => { + const { query, setTimeout } = ambientAuthroity; + const queryCosmosBalance = makeQueryCosmosBalace(query); + const { maxRetries = 6, retryIntervalMs = 3500, log = console.log, errorMessage = "Error" } = options; + + return retryUntilCondition( + async () => queryCosmosBalance(destAcct), + balances => checkCosmosBalance(balances, threshold), + errorMessage, + // @ts-ignore + { maxRetries, retryIntervalMs, log, setTimeout } + ) +}; + +/** + * - Making sure an offer is resulted; + */ + +const makeQueryWallet = follow => async (/** @type {String} */ addr) => { + const update = await follow( + '-lF', + `:published.wallet.${addr}`, ); - await sleep(retryIntervalMs, { log, setTimeout }); - } - throw Error(`${message} condition failed after ${maxRetries} retries.`); + return update; }; + +/** + * + * @param {object} offerStatus + * @param {boolean} waitForPayouts + * @param {string} offerId + * @returns + */ +const checkOfferState = (offerStatus, waitForPayouts, offerId) => { + const { updated, status } = offerStatus; + + if (updated !== "offerStatus") return false; + if (!status) return false; + if (status.id !== offerId) return false; + if (!status.numWantsSatisfied || status.numWantsSatisfied !== 1) return false; + if (waitForPayouts && status.result && status.payouts) return true; + if (!waitForPayouts && status.result) return true; + + return false; +}; + +/** + * + * @param {string} addr + * @param {string} offerId + * @param {boolean} waitForPayouts + * @param {{follow: () => object, setTimeout: (object) => void}} ambientAuthroity + * @param {RetyrOptions} options + * @returns + */ +export const waitUntilOfferResult = (addr, offerId, waitForPayouts, ambientAuthroity, options) => { + const { follow, setTimeout } = ambientAuthroity; + const queryWallet = makeQueryWallet(follow); + const { maxRetries = 6, retryIntervalMs = 3500, log = console.log, errorMessage = "Error" } = options; + + return retryUntilCondition( + async () => queryWallet(addr), + status => checkOfferState(status, waitForPayouts, offerId), + errorMessage, + // @ts-ignore + { maxRetries, retryIntervalMs, log, setTimeout } + ) +}; + +/** + * Making sure a core-eval succesfully sent zoe invitations to committee members for governance + */ + +/** + * + * @param {{ updated: string, currentAmount: any }} update + * @returns {boolean} + */ +const checkForInvitation = update => { + const { updated, currentAmount } = update; + + if (updated !== 'balance') return false; + if (!currentAmount || !currentAmount.brand) return false; + + return currentAmount.brand.includes('Invitation'); +}; + +/** + * + * @param {string} addr + * @param {{follow: () => object, setTimeout: (object) => void}} ambientAuthroity + * @param {RetyrOptions} options + * @returns + */ +export const waitUntilInvitationReceived = (addr, ambientAuthroity, options) => { + const { follow, setTimeout } = ambientAuthroity; + const queryWallet = makeQueryWallet(follow); + const { maxRetries = 6, retryIntervalMs = 3500, log = console.log, errorMessage = "Error" } = options; + + return retryUntilCondition( + async () => queryWallet(addr), + checkForInvitation, + errorMessage, + // @ts-ignore + { maxRetries, retryIntervalMs, log, setTimeout } + ) +}; \ No newline at end of file diff --git a/a3p-integration/proposals/z:acceptance/test-lib/sync-tools.test.js b/a3p-integration/proposals/z:acceptance/test-lib/sync-tools.test.js new file mode 100644 index 000000000000..922f4a485e11 --- /dev/null +++ b/a3p-integration/proposals/z:acceptance/test-lib/sync-tools.test.js @@ -0,0 +1,525 @@ +// @ts-check +import test from 'ava'; +import '@endo/init/debug.js'; +import { waitUntilAccountFunded, waitUntilContractDeplyed, waitUntilInvitationReceived, waitUntilOfferResult } from './sync-tools.js'; + +const sampleOfferStatus = { + "status": { + "id": 1726389716785, + "invitationSpec": { + "invitationMakerName": "CloseVault", + "previousOffer": "1711355582081", + "source": "continuing" + }, + "numWantsSatisfied": 1, + "payouts": { + "Collateral": { + "brand": "[Alleged: SEVERED: stTIA brand {}]", + "value": "10347167" + }, + "Minted": { + "brand": "[Alleged: SEVERED: IST brand {}]", + "value": "0" + } + }, + "proposal": { + "give": { + "Minted": { + "brand": "[Alleged: SEVERED: IST brand {}]", + "value": "0" + } + }, + "want": { + "Collateral": { + "brand": "[Alleged: SEVERED: stTIA brand {}]", + "value": "10347167" + } + } + }, + "result": "your vault is closed, thank you for your business" + }, + "updated": "offerStatus" +}; + +const sampleBalanceUpdate = { + "currentAmount": { + "brand": "[Alleged: SEVERED: Zoe Invitation brand {}]", + "value": [] + }, + "updated": "balance" +} + +const sampleInstance = [ + [ + "ATOM-USD price feed", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "Crabble", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "CrabbleCommittee", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "CrabbleGovernor", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "VaultFactory", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "VaultFactoryGovernor", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "econCommitteeCharter", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "economicCommittee", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "feeDistributor", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "kread", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "kreadCommittee", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "kreadCommitteeCharter", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "provisionPool", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "psm-IST-DAI_axl", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "psm-IST-DAI_grv", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "psm-IST-USDC", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "psm-IST-USDC_axl", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "psm-IST-USDC_grv", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "psm-IST-USDT", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "psm-IST-USDT_axl", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "psm-IST-USDT_grv", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "reserve", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "reserveGovernor", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "scaledPriceAuthority-stATOM", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "scaledPriceAuthority-stOSMO", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "scaledPriceAuthority-stTIA", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "scaledPriceAuthority-stkATOM", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "stATOM-USD price feed", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "stOSMO-USD price feed", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "stTIA-USD price feed", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "stkATOM-USD price feed", + "[Alleged: SEVERED: InstanceHandle {}]" + ], + [ + "walletFactory", + "[Alleged: SEVERED: InstanceHandle {}]" + ] +] + +const cosmosBalanceSample = { + "balances": [ + { + "denom": "ubld", + "amount": "364095061" + }, + { + "denom": "uist", + "amount": "2257215" + } + ], + "pagination": { + "next_key": null, + "total": "0" + } +} + +const makeFakeFollow = () => { + let value = [[]]; + + const setValue = newValue => value = newValue; + const follow = () => Promise.resolve(value); + + return { setValue, follow }; +} + +const makeFakeBalanceQuery = () => { + let result = { + "balances": [ + { + "denom": "ubld", + "amount": "364095061" + }, + { + "denom": "uist", + "amount": "2257215" + } + ], + "pagination": { + "next_key": null, + "total": "0" + } + } + + const setResult = newValue => result = newValue; + const query = () => Promise.resolve(result); + + return { setResult, query }; +}; + +test.serial('wait until contract is deployed', async t => { + const { setValue, follow } = makeFakeFollow(); + const waitP = waitUntilContractDeplyed('name', { + follow, + setTimeout: globalThis.setTimeout + }, { maxRetries: 5, retryIntervalMs: 1000, log: t.log, errorMessage: "Contact not deplyed yet" }); + + setTimeout(() => setValue([["name", true]]), 3000); // set desired value after third retry + + await t.notThrowsAsync(waitP); +}); + +test.serial('wait until account funded', async t => { + const { setResult, query } = makeFakeBalanceQuery(); + + const waitP = waitUntilAccountFunded( + 'agoric12345', + { query, setTimeout: globalThis.setTimeout }, + { denom: 'ufake', value: 100_000 }, + { maxRetries: 5, retryIntervalMs: 1000, log: t.log, errorMessage: "Account not funded yet" } + ) + + + const desiredResult = { + "balances": [ + { + "denom": "ubld", + "amount": "364095061" + }, + { + "denom": "uist", + "amount": "2257215" + }, + { + "denom": "ufake", + "amount": "100001" + }, + ], + "pagination": { + "next_key": null, + "total": "0" + } + } + setTimeout(() => setResult(desiredResult), 3000); // set desired value after third retry + await t.notThrowsAsync(waitP); +}) + +test.serial('wait until account funded, insufficient balance', async t => { + const { setResult, query } = makeFakeBalanceQuery(); + + const waitP = waitUntilAccountFunded( + 'agoric12345', + { query, setTimeout: globalThis.setTimeout }, + { denom: 'ufake', value: 100_000 }, + { maxRetries: 5, retryIntervalMs: 1000, log: t.log, errorMessage: "Account not funded yet" } + ) + + + const desiredResult = { + "balances": [ + { + "denom": "ubld", + "amount": "364095061" + }, + { + "denom": "uist", + "amount": "2257215" + }, + { + "denom": "ufake", + "amount": "90000" + }, + ], + "pagination": { + "next_key": null, + "total": "0" + } + } + setTimeout(() => setResult(desiredResult), 3000); // set desired value after third retry + await t.throwsAsync(waitP); +}); + +test.serial('wait until offer result, balance update - should throw', async t => { + const { setValue, follow } = makeFakeFollow(); + setValue({ status: {}, updated: "balance" }); + + const waitP = waitUntilOfferResult( + 'agoric12345', + 'my-offer', + false, + { follow, setTimeout: globalThis.setTimeout }, + { + maxRetries: 5, + retryIntervalMs: 1000, + log: t.log, + errorMessage: "Wrong update type" + } + ); + + await t.throwsAsync(waitP); +}); + +test.serial('wait until offer result, wrong id - should throw', async t => { + const { setValue, follow } = makeFakeFollow(); + setValue({ status: { id: 'your-offer' }, updated: "offerStatus" }); + + const waitP = waitUntilOfferResult( + 'agoric12345', + 'my-offer', + false, + { follow, setTimeout: globalThis.setTimeout }, + { + maxRetries: 5, + retryIntervalMs: 1000, + log: t.log, + errorMessage: "Wrong offer id" + } + ); + + await t.throwsAsync(waitP); +}); + +test.serial('wait until offer result, no "status" - should throw', async t => { + const { setValue, follow } = makeFakeFollow(); + setValue({ updated: "offerStatus" }); + + const waitP = waitUntilOfferResult( + 'agoric12345', + 'my-offer', + false, + { follow, setTimeout: globalThis.setTimeout }, + { + maxRetries: 5, + retryIntervalMs: 1000, + log: t.log, + errorMessage: 'No "status" object' + } + ); + + await t.throwsAsync(waitP); +}); + +test.serial('wait until offer result, numWantsSatisfied not equals to 1 - should throw', async t => { + const { setValue, follow } = makeFakeFollow(); + setValue({ status: { id: 'my-offer', numWantsSatisfied: 0 }, updated: "offerStatus" }); + + const waitP = waitUntilOfferResult( + 'agoric12345', + 'my-offer', + false, + { follow, setTimeout: globalThis.setTimeout }, + { + maxRetries: 5, + retryIntervalMs: 1000, + log: t.log, + errorMessage: '"numWantsSatisfied" is not 1' + } + ); + + await t.throwsAsync(waitP); +}); + +test.serial('wait until offer result, do not wait for "payouts"', async t => { + const { setValue, follow } = makeFakeFollow(); + setValue({ status: { id: 'my-offer' }, updated: "offerStatus" }); + + const waitP = waitUntilOfferResult( + 'agoric12345', + 'my-offer', + false, + { follow, setTimeout: globalThis.setTimeout }, + { + maxRetries: 7, + retryIntervalMs: 1000, + log: t.log, + errorMessage: 'offer not resulted on time' + } + ); + + setTimeout(() => setValue({ status: { id: 'my-offer', numWantsSatisfied: 1 }, updated: "offerStatus" }), 1000); // First, offer is seated + setTimeout(() => setValue({ status: { id: 'my-offer', numWantsSatisfied: 1, result: 'thank you' }, updated: "offerStatus" }), 3000); // First, offer is resulted + + await t.notThrowsAsync(waitP); +}); + +test.serial('wait until offer result, wait for "payouts"', async t => { + const { setValue, follow } = makeFakeFollow(); + setValue({ status: { id: 'my-offer' }, updated: "offerStatus" }); + + const waitP = waitUntilOfferResult( + 'agoric12345', + 'my-offer', + true, + { follow, setTimeout: globalThis.setTimeout }, + { + maxRetries: 7, + retryIntervalMs: 1000, + log: t.log, + errorMessage: 'payouts not received on time' + } + ); + + setTimeout( + () => setValue({ status: { id: 'my-offer', numWantsSatisfied: 1 }, updated: "offerStatus" }), + 1000 + ); // First, offer is seated + setTimeout( + () => setValue({ status: { id: 'my-offer', numWantsSatisfied: 1, result: 'thank you' }, updated: "offerStatus" }), + 3000 + ); // Now, offer is resulted + setTimeout( + () => setValue({ status: { id: 'my-offer', numWantsSatisfied: 1, result: 'thank you', payouts: {} }, updated: "offerStatus" }), + 4000 + ); // Payouts are received + + await t.notThrowsAsync(waitP); +}); + +test.serial('wait until invitation recevied, wrong "updated" value', async t => { + const { setValue, follow } = makeFakeFollow(); + setValue({ updated: "offerStatus" }); + + const waitP = waitUntilInvitationReceived( + 'agoric12345', + { follow, setTimeout: globalThis.setTimeout }, + { + maxRetries: 3, + retryIntervalMs: 1000, + log: t.log, + errorMessage: 'wrong "updated" value' + } + ); + + await t.throwsAsync(waitP); +}); + +test.serial('wait until invitation recevied, falty "currentAmount" object', async t => { + const { setValue, follow } = makeFakeFollow(); + setValue({ updated: "balance" }); + + const waitP = waitUntilInvitationReceived( + 'agoric12345', + { follow, setTimeout: globalThis.setTimeout }, + { + maxRetries: 5, + retryIntervalMs: 1000, + log: t.log, + errorMessage: 'falty "currentAmount" object' + } + ); + + setTimeout(() => setValue({ updated: "balance", currentAmount: { foo: true } }), 2000); + + await t.throwsAsync(waitP); +}); + +test.serial('wait until invitation recevied, brand string do not match', async t => { + const { setValue, follow } = makeFakeFollow(); + setValue({ updated: "balance", currentAmount: { brand: 'foo bar foo' } }); + + const waitP = waitUntilInvitationReceived( + 'agoric12345', + { follow, setTimeout: globalThis.setTimeout }, + { + maxRetries: 3, + retryIntervalMs: 1000, + log: t.log, + errorMessage: 'brand string do not match' + } + ); + + await t.throwsAsync(waitP); +}); + +test.only('wait until invitation recevied', async t => { + const { setValue, follow } = makeFakeFollow(); + setValue({}); + + const waitP = waitUntilInvitationReceived( + 'agoric12345', + { follow, setTimeout: globalThis.setTimeout }, + { + maxRetries: 5, + retryIntervalMs: 1000, + log: t.log, + errorMessage: 'brand string do not match' + } + ); + + setTimeout(() => setValue({ updated: 'balance', currentAmount: { brand: '[Alleged: SEVERED: Zoe Invitation brand {}]'}}), 2000); + + await t.notThrowsAsync(waitP); +}); \ No newline at end of file