diff --git a/a3p-integration/proposals/z:acceptance/test-lib/index.js b/a3p-integration/proposals/z:acceptance/test-lib/index.js index 0c6fffa980a..2f2e7c02d3c 100644 --- a/a3p-integration/proposals/z:acceptance/test-lib/index.js +++ b/a3p-integration/proposals/z:acceptance/test-lib/index.js @@ -15,7 +15,6 @@ export const networkConfig = { */ const delay = ms => new Promise(resolve => setTimeout(() => resolve(), ms)); -// eslint-disable-next-line @jessie.js/safe-await-separator -- buggy version export const walletUtils = await makeWalletUtils( { delay, execFileSync, fetch }, networkConfig, 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 4a0e727c465..f0daeddabde 100644 --- a/a3p-integration/proposals/z:acceptance/test-lib/sync-tools.js +++ b/a3p-integration/proposals/z:acceptance/test-lib/sync-tools.js @@ -1,17 +1,31 @@ /* 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 tools that + * developers can use to synchronize operations they carry out in their tests. + * + * These operations include; + * - Making sure a core-eval resulted in successfully deploying a contract + * - Making sure a core-eval successfully 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 resulted successfully + * */ /** * @typedef {object} RetryOptions - * @property {number} [maxRetries] - * @property {number} [retryIntervalMs] + * @property {number} maxRetries + * @property {number} retryIntervalMs * @property {(...arg0: string[]) => void} log * @property {(object) => void} [setTimeout] * @property {string} [errorMessage=Error] + * + * + * @typedef {object} CosmosBalanceThreshold + * @property {string} denom + * @property {number} value */ const ambientSetTimeout = global.setTimeout; @@ -45,7 +59,6 @@ export const retryUntilCondition = async ( console.log({ maxRetries, retryIntervalMs, message }); let retries = 0; - await null; while (retries < maxRetries) { try { const result = await operation(); @@ -70,3 +83,190 @@ export const retryUntilCondition = async ( throw Error(`${message} condition failed after ${maxRetries} retries.`); }; + +/** + * @param {RetryOptions} options + */ +const overrideDefaultOptions = options => { + const defaultValues = { + maxRetries: 6, + retryIntervalMs: 3500, + log: console.log, + errorMessage: 'Error', + }; + + return { ...defaultValues, ...options }; +}; + +/// ////////// Making sure a core-eval resulted successfully 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}} ambientAuthority + * @param {RetryOptions} options + */ +export const waitUntilContractDeployed = ( + contractName, + ambientAuthority, + options, +) => { + const { follow, setTimeout } = ambientAuthority; + const getInstances = makeGetInstances(follow); + const { maxRetries, retryIntervalMs, errorMessage, log } = + overrideDefaultOptions(options); + + return retryUntilCondition( + getInstances, + instanceObject => Object.keys(instanceObject).includes(contractName), + errorMessage, + { maxRetries, retryIntervalMs, log, setTimeout }, + ); +}; + +/// ////////// Making sure an account is successfully funded with vbank assets like IST, BLD etc. /////////////// + +const makeQueryCosmosBalance = queryCb => async dest => { + const coins = await queryCb('bank', 'balances', dest); + return coins.balances; +}; + +/** + * + * @param {Array} balances + * @param {CosmosBalanceThreshold} threshold + * @returns {boolean} + */ +const checkCosmosBalance = (balances, threshold) => { + const balance = [...balances].find(({ denom }) => denom === threshold.denom); + return Number(balance.amount) >= threshold.value; +}; + +/** + * @param {string} destAcct + * @param {{query: () => Promise, setTimeout: (object) => void}} ambientAuthority + * @param {{denom: string, value: number}} threshold + * @param {RetryOptions} options + */ +export const waitUntilAccountFunded = ( + destAcct, + ambientAuthority, + threshold, + options, +) => { + const { query, setTimeout } = ambientAuthority; + const queryCosmosBalance = makeQueryCosmosBalance(query); + const { maxRetries, retryIntervalMs, errorMessage, log } = + overrideDefaultOptions(options); + + return retryUntilCondition( + async () => queryCosmosBalance(destAcct), + balances => checkCosmosBalance(balances, threshold), + errorMessage, + { maxRetries, retryIntervalMs, log, setTimeout }, + ); +}; + +/// ////////// Making sure an offers get results ///////////// + +const makeQueryWallet = follow => async (/** @type {string} */ addr) => { + const update = await follow('-lF', `:published.wallet.${addr}`); + + return update; +}; + +/** + * + * @param {object} offerStatus + * @param {boolean} waitForPayouts + * @param {string} offerId + */ +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}} ambientAuthority + * @param {RetryOptions} options + */ +export const waitUntilOfferResult = ( + addr, + offerId, + waitForPayouts, + ambientAuthority, + options, +) => { + const { follow, setTimeout } = ambientAuthority; + const queryWallet = makeQueryWallet(follow); + const { maxRetries, retryIntervalMs, errorMessage, log } = + overrideDefaultOptions(options); + + return retryUntilCondition( + async () => queryWallet(addr), + status => checkOfferState(status, waitForPayouts, offerId), + errorMessage, + { maxRetries, retryIntervalMs, log, setTimeout }, + ); +}; + +/// ////////// Making sure a core-eval successfully 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}} ambientAuthority + * @param {RetryOptions} options + */ +export const waitUntilInvitationReceived = ( + addr, + ambientAuthority, + options, +) => { + const { follow, setTimeout } = ambientAuthority; + const queryWallet = makeQueryWallet(follow); + const { maxRetries, retryIntervalMs, errorMessage, log } = + overrideDefaultOptions(options); + + return retryUntilCondition( + async () => queryWallet(addr), + checkForInvitation, + errorMessage, + { maxRetries, retryIntervalMs, log, setTimeout }, + ); +}; 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 00000000000..2b0d3603c4b --- /dev/null +++ b/a3p-integration/proposals/z:acceptance/test-lib/sync-tools.test.js @@ -0,0 +1,413 @@ +/* eslint-env node */ +// @ts-check +import test from 'ava'; +import '@endo/init/debug.js'; +import { + waitUntilAccountFunded, + waitUntilContractDeployed, + waitUntilInvitationReceived, + waitUntilOfferResult, +} from './sync-tools.js'; + +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 = waitUntilContractDeployed( + 'name', + { + follow, + setTimeout, + }, + { + maxRetries: 5, + retryIntervalMs: 1000, + log: t.log, + errorMessage: 'Contract not deployed 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 }, + { 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 }, + { 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, { message: /Account not funded yet/ }); +}); + +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 }, + { + maxRetries: 5, + retryIntervalMs: 1000, + log: t.log, + errorMessage: 'Wrong update type', + }, + ); + + await t.throwsAsync(waitP, { message: /Wrong update type/ }); + }, +); + +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 }, + { + maxRetries: 5, + retryIntervalMs: 1000, + log: t.log, + errorMessage: 'Wrong offer id', + }, + ); + + await t.throwsAsync(waitP, { message: /Wrong offer id/ }); +}); + +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 }, + { + maxRetries: 5, + retryIntervalMs: 1000, + log: t.log, + errorMessage: 'No "status" object', + }, + ); + + await t.throwsAsync(waitP, { message: /No "status" object/ }); +}); + +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 }, + { + maxRetries: 5, + retryIntervalMs: 1000, + log: t.log, + errorMessage: '"numWantsSatisfied" is not 1', + }, + ); + + await t.throwsAsync(waitP, { message: /"numWantsSatisfied" is not 1/ }); + }, +); + +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 }, + { + 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, + ); // Then offer got results + + 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 }, + { + 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 got results + 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 }, + { + maxRetries: 3, + retryIntervalMs: 1000, + log: t.log, + errorMessage: 'wrong "updated" value', + }, + ); + + await t.throwsAsync(waitP, { message: /wrong "updated" value/ }); + }, +); + +test.serial( + 'wait until invitation recevied, falty "currentAmount" object', + async t => { + const { setValue, follow } = makeFakeFollow(); + setValue({ updated: 'balance' }); + + const waitP = waitUntilInvitationReceived( + 'agoric12345', + { follow, setTimeout }, + { + maxRetries: 5, + retryIntervalMs: 1000, + log: t.log, + errorMessage: 'faulty "currentAmount" object', + }, + ); + + setTimeout( + () => setValue({ updated: 'balance', currentAmount: { foo: true } }), + 2000, + ); + + await t.throwsAsync(waitP, { message: /faulty "currentAmount" object/ }); + }, +); + +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 }, + { + maxRetries: 3, + retryIntervalMs: 1000, + log: t.log, + errorMessage: 'brand string do not match', + }, + ); + + await t.throwsAsync(waitP, { message: /brand string do not match/ }); + }, +); + +test.serial('wait until invitation recevied', async t => { + const { setValue, follow } = makeFakeFollow(); + setValue({}); + + const waitP = waitUntilInvitationReceived( + 'agoric12345', + { follow, 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); +}); diff --git a/a3p-integration/proposals/z:acceptance/tsconfig.json b/a3p-integration/proposals/z:acceptance/tsconfig.json index 795ebdd04fc..bd1c45190fe 100644 --- a/a3p-integration/proposals/z:acceptance/tsconfig.json +++ b/a3p-integration/proposals/z:acceptance/tsconfig.json @@ -10,6 +10,6 @@ "strictNullChecks": true, "noImplicitThis": true, // XXX synthetic-chain has some errors - "skipLibCheck": true, + "skipLibCheck": true } }