Skip to content

Commit

Permalink
test: tools for sync operations (#10171)
Browse files Browse the repository at this point in the history
refs: Agoric/BytePitchPartnerEng#10
refs: Agoric/agoric-3-proposals#181

## Description

Currently the way to make sure a transaction sent to agoric chain has been executed is to use a method from `@agoric/synthetic-chain` called `waitForBlock` which stops the execution flow of a given ava test until N number of blocks are produced. This is not as deterministic as we'd like it to be but so far it worked fine. However, it could still end up in a race condition in some extreme cases. In this PR we introduce a set of tools to sync operations carried out in a test. Currently this PR supports operations like;
 * Making sure a core-eval resulted 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.
 * Making sure an offer resulted successfully

See Agoric/agoric-3-proposals#181 for further discussion.

### Security Considerations

This is merely a testing tool, so no security considerations.

### Scaling Considerations

This is merely a testing tool, so no scaling considerations.

### Documentation Considerations

If this ends up getting in to `@agoric/synthetic-chain` then we might need more extensive explanation of what we do. But for now, I don't think any documentation is needed.

### Testing Considerations

Have confirmed that this works fine with existing a3p tests locally but to keep the scope tighter I will update those tests one this PR lands. The coverage of the unit tests in `sync-tools.test.js` seems fine but I'm open to suggestions, of course.
  • Loading branch information
mergify[bot] authored Oct 22, 2024
2 parents 7f90751 + dfd6be1 commit 4a33acc
Show file tree
Hide file tree
Showing 4 changed files with 619 additions and 7 deletions.
1 change: 0 additions & 1 deletion a3p-integration/proposals/z:acceptance/test-lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
210 changes: 205 additions & 5 deletions a3p-integration/proposals/z:acceptance/test-lib/sync-tools.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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<object>, 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 },
);
};
Loading

0 comments on commit 4a33acc

Please sign in to comment.