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
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
@@ -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"
},
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

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

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

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

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;
};
103 changes: 103 additions & 0 deletions a3p-integration/proposals/z:acceptance/test-lib/priceFeed-lib.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import test from 'ava';
import '@endo/init';
import { ensureGCDeliveryOnly } from './priceFeed-lib.js';

const testConfig = {
before: {
'-scaledPriceAuthority-stATOM': { v58: 13, v74: 119 },
'-scaledPriceAuthority-ATOM': { v46: 77, v73: 178 },
'-stATOM-USD_price_feed': { v57: 40, v72: 192 },
'-ATOM-USD_price_feed': { v29: 100, v70: 247 },
},
after: {
'-scaledPriceAuthority-stATOM': { v58: 15, v74: 119 },
'-scaledPriceAuthority-ATOM': { v46: 79, v73: 178 },
'-stATOM-USD_price_feed': { v57: 42, v72: 192 },
'-ATOM-USD_price_feed': { v29: 102, v70: 247 },
},
};

const makeFakeGetTranscriptItemsForVat = (
deliveryType,
maximumAllowedDeliveries,
) => {
const fakeGetTranscriptItemsForVat = (_, number) => {
const fakeTranscriptItems = [];
for (let i = 0; i < number; i += 1) {
const item = { d: [deliveryType] };
fakeTranscriptItems.push({ item: JSON.stringify(item) });
}
return fakeTranscriptItems;
};

const tooManyTranscriptItemsForVat = () => {
const fakeTranscriptItems = [];
for (let i = 0; i <= maximumAllowedDeliveries; i += 1) {
const item = { d: [deliveryType] };
fakeTranscriptItems.push({ item: JSON.stringify(item) });
}
return fakeTranscriptItems;
};

return { fakeGetTranscriptItemsForVat, tooManyTranscriptItemsForVat };
};

test('should not throw', t => {
const { fakeGetTranscriptItemsForVat } =
makeFakeGetTranscriptItemsForVat('dropExports');

t.notThrows(() =>
ensureGCDeliveryOnly(testConfig, {
getTranscriptItems: fakeGetTranscriptItemsForVat,
}),
);
});

test('should throw for "notify"', t => {
const { fakeGetTranscriptItemsForVat } =
makeFakeGetTranscriptItemsForVat('notify');

t.throws(
() =>
ensureGCDeliveryOnly(testConfig, {
getTranscriptItems: fakeGetTranscriptItemsForVat,
}),
{ message: 'DeliveryType "notify" is not GC delivery' },
);
});

test('should throw for "message"', t => {
const { fakeGetTranscriptItemsForVat } =
makeFakeGetTranscriptItemsForVat('message');

t.throws(
() =>
ensureGCDeliveryOnly(testConfig, {
getTranscriptItems: fakeGetTranscriptItemsForVat,
}),
{ message: 'DeliveryType "message" is not GC delivery' },
);
});

test('should throw too many deliveries', t => {
const { fakeGetTranscriptItemsForVat } = makeFakeGetTranscriptItemsForVat(
'dropExports',
5,
);

const config = {
...testConfig,
after: {
...testConfig.after,
'-scaledPriceAuthority-stATOM': { v58: 20, v74: 119 },
},
};

t.throws(
() =>
ensureGCDeliveryOnly(config, {
getTranscriptItems: fakeGetTranscriptItemsForVat,
}),
{ message: '7 deliveries is greater than maximum allowed: 5' },
);
});
102 changes: 102 additions & 0 deletions a3p-integration/proposals/z:acceptance/test-lib/vat-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import dbOpenAmbient from 'better-sqlite3';
import { HOME, dbTool } from '@agoric/synthetic-chain';

/**
* @typedef {{position: number; item: string; vatID: string; incarnation: number}} TranscriptItem
*/

const swingstorePath = '~/.agoric/data/agoric/swingstore.sqlite';

/**
* Initially from https://github.com/Agoric/agoric-3-proposals/blob/93bb953db209433499db08ae563942d1bf7eeb46/proposals/76%3Avaults-auctions/vatDetails.js#L36
* but with small modifications
*
* @param {import('better-sqlite3').Database} db
*/
const makeSwingstore = db => {
const sql = dbTool(db);

/** @param {string} key */
// @ts-expect-error sqlite typedefs
const kvGet = key => sql.get`select * from kvStore where key = ${key}`.value;
/** @param {string} key */
const kvGetJSON = key => JSON.parse(kvGet(key));

/** @param {string} vatID */
const lookupVat = vatID => {
return Object.freeze({
source: () => kvGetJSON(`${vatID}.source`),
options: () => kvGetJSON(`${vatID}.options`),
currentSpan: () =>
sql.get`select * from transcriptSpans where isCurrent = 1 and vatID = ${vatID}`,
});
};

return Object.freeze({
/** @param {string} vatName */
findVat: vatName => {
/** @type {string[]} */
const dynamicIDs = kvGetJSON('vat.dynamicIDs');
const targetVat = dynamicIDs.find(vatID =>
lookupVat(vatID).options().name.includes(vatName),
);
if (!targetVat) throw Error(`vat not found: ${vatName}`);
return targetVat;
},
/** @param {string} string a substring to search for within the vat name. */
findVatsExact: string => {
/** @type {string[]} */
const dynamicIDs = kvGetJSON('vat.dynamicIDs');
return dynamicIDs.filter(vatID =>
lookupVat(vatID).options().name.endsWith(string),
);
},
findVatsAll: string => {
/** @type {string[]} */
const dynamicIDs = kvGetJSON('vat.dynamicIDs');
return dynamicIDs.filter(vatID =>
lookupVat(vatID).options().name.includes(string),
);
},
lookupVat,
db,
});
};

const initSwingstore = () => {
const fullPath = swingstorePath.replace(/^~/, HOME);
return makeSwingstore(dbOpenAmbient(fullPath, { readonly: true }));
};

/**
*
* @param {string} vatId
* @param {number} n
* @returns {Array<TranscriptItem>}
*/
export const getTranscriptItemsForVat = (vatId, n = 10) => {
const { db } = initSwingstore();

const items = db
.prepare(
'select * from transcriptItems where vatId = ? order by position desc limit ?',
)
.all(vatId, n);

// @ts-expect-error casting problem when assigning values coming from db
return items;
};

export const snapshotVat = vatName => {
const { findVatsExact } = initSwingstore();

const snapshots = {};
const vatIdsWithExactName = findVatsExact(vatName);
vatIdsWithExactName.forEach(id => {
const element = getTranscriptItemsForVat(id, 1)[0];

snapshots[id] = element.position;
});

return snapshots;
};
3 changes: 3 additions & 0 deletions a3p-integration/proposals/z:acceptance/test.sh
Original file line number Diff line number Diff line change
@@ -26,5 +26,8 @@ echo ACCEPTANCE TESTING state sync
echo ACCEPTANCE TESTING wallet
yarn ava wallet.test.js

echo ACCEPTANCE TESTING replaced price feeds
yarn ava priceFeed.test.js

echo ACCEPTANCE TESTING vaults
yarn ava vaults.test.js
4 changes: 1 addition & 3 deletions a3p-integration/proposals/z:acceptance/vaults.test.js
Original file line number Diff line number Diff line change
@@ -14,7 +14,6 @@ import {
ATOM_DENOM,
USER1ADDR,
waitForBlock,
registerOraclesForBrand,
generateOracleMap,
} from '@agoric/synthetic-chain';
import { getBalances, agopsVaults } from './test-lib/utils.js';
@@ -30,13 +29,12 @@ test.before(async t => {
retryIntervalMs: 5000, // in ms
};
t.context = {
roundId: 1,
roundId: 3,
retryOpts: {
pushPriceRetryOpts,
},
};
const oraclesByBrand = generateOracleMap('z-acc', ['ATOM']);
await registerOraclesForBrand('ATOM', oraclesByBrand);

const price = 15.2;
// @ts-expect-error t.context is fine
12 changes: 12 additions & 0 deletions a3p-integration/proposals/z:acceptance/yarn.lock
Original file line number Diff line number Diff line change
@@ -784,6 +784,17 @@ __metadata:
languageName: node
linkType: hard

"better-sqlite3@npm:11.5.0":
version: 11.5.0
resolution: "better-sqlite3@npm:11.5.0"
dependencies:
bindings: "npm:^1.5.0"
node-gyp: "npm:latest"
prebuild-install: "npm:^7.1.1"
checksum: 10c0/c24200972e11f6f99c4e6538122bd7ec8b31b92b2fa095f4b595cc39fedf924cb0a93fd326f0900415eccdf634367f7bba2ba4eaa4d164edd7352f4cfaaaec51
languageName: node
linkType: hard

"better-sqlite3@npm:^9.6.0":
version: 9.6.0
resolution: "better-sqlite3@npm:9.6.0"
@@ -2517,6 +2528,7 @@ __metadata:
"@endo/far": "npm:^1.1.5"
"@endo/init": "npm:^1.1.4"
ava: "npm:^6.1.2"
better-sqlite3: "npm:11.5.0"
execa: "npm:^9.3.1"
tsx: "npm:^4.17.0"
typescript: "npm:^5.5.4"

Unchanged files with check annotations Beta

capName := host.ChannelCapabilityPath(portID, channelID)
chanCap, ok := k.vibcKeeper.GetCapability(ctx, capName)
if !ok {
err := sdkerrors.Wrapf(channeltypes.ErrChannelCapabilityNotFound, "could not retrieve channel capability at: %s", capName)

Check failure on line 120 in golang/cosmos/x/vtransfer/keeper/keeper.go

GitHub Actions / golangci-lint (no-failure)

SA1019: sdkerrors.Wrapf is deprecated: functionality of this package has been moved to it's own module: (staticcheck)
return channeltypes.NewErrorAcknowledgement(err)
}
// Give the VM a chance to write (or override) the ack.
case "BRIDGE_TARGET_UNREGISTER":
prefixStore.Delete([]byte(msg.Target))
default:
return "", sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown action type: %s", msg.Type)

Check failure on line 278 in golang/cosmos/x/vtransfer/keeper/keeper.go

GitHub Actions / golangci-lint (no-failure)

SA1019: sdkerrors.Wrapf is deprecated: functionality of this package has been moved to it's own module: (staticcheck)
}
return "true", nil
}
switch msg := msg.(type) {
default:
errMsg := fmt.Sprintf("Unrecognized vtransfer Msg type: %T", msg)
return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, errMsg)

Check failure on line 17 in golang/cosmos/x/vtransfer/handler.go

GitHub Actions / golangci-lint (no-failure)

SA1019: sdkerrors.Wrap is deprecated: functionality of this package has been moved to it's own module: (staticcheck)
}
}
}
return startResult;
};
/**

Check warning on line 308 in packages/inter-protocol/src/proposals/replaceElectorate.js

GitHub Actions / lint-primary

Missing JSDoc @param "root0" declaration

Check warning on line 308 in packages/inter-protocol/src/proposals/replaceElectorate.js

GitHub Actions / lint-primary

Missing JSDoc @param "root0.options" declaration

Check warning on line 308 in packages/inter-protocol/src/proposals/replaceElectorate.js

GitHub Actions / lint-primary

Missing JSDoc @param "root0.options.econCharterKit" declaration
* Adds governors to an existing Economic Committee Charter
*
* - @param {EconomyBootstrapPowers} powers - The resources and capabilities
const waitForBootstrap = async () => {
const endpoint = 'localhost';
while (true) {

Check warning on line 10 in a3p-integration/proposals/n:upgrade-next/synthetic-chain-excerpt.js

GitHub Actions / lint-rest

Unexpected constant condition
const { stdout: json } = await $({
reject: false,
})`curl -s --fail -m 15 ${`${endpoint}:26657/status`}`;
let time = 0;
while (time < times) {
const block1 = await waitForBootstrap();
while (true) {

Check warning on line 40 in a3p-integration/proposals/n:upgrade-next/synthetic-chain-excerpt.js

GitHub Actions / lint-rest

Unexpected constant condition
const block2 = await waitForBootstrap();
if (block1 !== block2) {
/**
* @param {{execFileSync: typeof import('child_process').execFileSync }} io
* @returns

Check warning on line 60 in a3p-integration/proposals/n:upgrade-next/synthetic-chain-excerpt.js

GitHub Actions / lint-rest

Missing JSDoc @returns type
*/
export const makeAgd = ({ execFileSync }) => {
/**
/**
* @template T
* @param follower

Check warning on line 19 in a3p-integration/proposals/s:stake-bld/test-lib/wallet.js

GitHub Actions / lint-rest

Missing JSDoc @param "follower" type
* @param [options]

Check warning on line 20 in a3p-integration/proposals/s:stake-bld/test-lib/wallet.js

GitHub Actions / lint-rest

Missing JSDoc @param "options" type
*/
export const iterateReverse = (follower, options) =>
// For now, just pass through the iterable.
import test from 'ava';
test.todo('initial test');

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

GitHub Actions / lint-rest

`test.todo()` should not be used