Skip to content
Merged
Show file tree
Hide file tree
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
4 changes: 2 additions & 2 deletions config/config.e2e.mainnet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ api:
websocket: true
cron:
cacheWarmer: true
fastWarm: false
fastWarm: true
queueWorker: true
elasticUpdater: false
flags:
Expand Down Expand Up @@ -39,7 +39,7 @@ features:
transactionPool:
enabled: true
transactionPoolWarmer:
enabled: true
enabled: false
cronExpression: '*/1 * * * * *'
ttlInSeconds: 10
updateCollectionExtraDetails:
Expand Down
12 changes: 10 additions & 2 deletions src/test/chain-simulator/accounts.cs-e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { config } from "./config/env.config";
import { NftType } from "src/endpoints/nfts/entities/nft.type";
import { NftSubType } from "src/endpoints/nfts/entities/nft.sub.type";
import { transferNftFromTo } from "./utils/chain.simulator.operations";
import { ChainSimulatorUtils } from "./utils/test.utils";

describe('Accounts e2e tests with chain simulator', () => {
describe('GET /accounts with query parameters', () => {
Expand Down Expand Up @@ -1993,10 +1994,17 @@ describe('Accounts e2e tests with chain simulator', () => {
const sendNftTx = await transferNftFromTo(config.chainSimulatorUrl, config.aliceAddress, config.bobAddress, nft.collection, nft.nonce);

const transaction = await axios.get(`${config.apiServiceUrl}/transactions/${sendNftTx}`);
await new Promise(resolve => setTimeout(resolve, 2000));

expect(transaction.status).toBe(200);

// the transfer is processed on chain, but bob's nfts are read from the index, which catches up later
await ChainSimulatorUtils.waitForApi(
'NFT received by Bob',
`${config.apiServiceUrl}/accounts/${config.bobAddress}/nfts?withReceivedAt=true`,
nfts => nfts.length >= 1 && nfts[0].receivedAt !== undefined,
60000,
1000,
);

const checkBobNft = await axios.get(`${config.apiServiceUrl}/accounts/${config.bobAddress}/nfts?withReceivedAt=true`);
const bobNft = checkBobNft.data;

Expand Down
1 change: 1 addition & 0 deletions src/test/chain-simulator/config/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
CHAIN_SIMULATOR_URL=http://localhost:8085
API_SERVICE_URL=http://localhost:3001
SUBSCRIPTIONS_SERIVCE_URL=http://localhost:6002
ELASTIC_URL=http://localhost:9200
ALICE_ADDRESS=erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th
BOB_ADDRESS=erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx
1 change: 1 addition & 0 deletions src/test/chain-simulator/config/env.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const config = {
chainSimulatorUrl: process.env.CHAIN_SIMULATOR_URL || 'http://localhost:8085',
apiServiceUrl: process.env.API_SERVICE_URL || 'http://localhost:3001',
subscriptionsServiceUrl: process.env.SUBSCRIPTIONS_SERVICE_URL || 'http://localhost:6002',
elasticUrl: process.env.ELASTIC_URL || 'http://localhost:9200',
aliceAddress: process.env.ALICE_ADDRESS || 'erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th',
bobAddress: process.env.BOB_ADDRESS || 'erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx',
};
39 changes: 34 additions & 5 deletions src/test/chain-simulator/utils/chain.simulator.operations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axios from 'axios';
import { AddressUtils } from "@multiversx/sdk-nestjs-common";
import { config } from '../config/env.config';

axios.defaults.adapter = 'fetch';
axios.defaults.headers.common['Connection'] = 'close';
Expand Down Expand Up @@ -32,7 +33,7 @@ export async function getNonce(
return currentNonceResponse.data.data.nonce;
} catch (e) {
console.error(e);
return 0;
throw e;
}
}

Expand All @@ -57,16 +58,39 @@ export async function deploySc(args: DeployScArgs): Promise<string> {
const scDeployLog = txResponse?.data?.data?.transaction?.logs?.events?.find(
(event: { identifier: string }) => event.identifier === 'SCDeploy',
);
if (!scDeployLog) {
throw new Error(`SC deploy ${txHash} produced no SCDeploy event`);
}

console.log(
`Deployed SC. tx hash: ${txHash}. address: ${scDeployLog?.address}`,
`Deployed SC. tx hash: ${txHash}. address: ${scDeployLog.address}`,
);
return scDeployLog?.address;
return scDeployLog.address;
} catch (e) {
console.error(e);
return 'n/a';
throw e;
}
}

// the api resolves a token through its document in the tokens index, and caches the answer, including
// the answer that the token does not exist. anything that makes the api look the token up before it is
// indexed leaves the token unresolved for as long as that answer is cached, so an issued token is not
// handed to the tests before it can be found there, through the same query the api runs
export async function waitForTokenIndexed(identifier: string, timeoutMs: number = 60000) {
const deadline = Date.now() + timeoutMs;

while (Date.now() < deadline) {
const response = await axios.get(`${config.elasticUrl}/tokens/_search?q=_id:${identifier}`);
if (response.data?.hits?.hits?.length > 0) {
return;
}

await new Promise(resolve => setTimeout(resolve, 500));
}

throw new Error(`Token ${identifier} was not indexed within ${timeoutMs}ms`);
}

export async function issueEsdt(args: IssueEsdtArgs) {
const txHash = await sendTransaction(
new SendTransactionArgs({
Expand Down Expand Up @@ -95,6 +119,8 @@ export async function issueEsdt(args: IssueEsdtArgs) {
console.log(
`Issued token with ticker ${args.tokenTicker}. tx hash: ${txHash}. identifier: ${tokenIdentifier}`,
);

await waitForTokenIndexed(tokenIdentifier);
return tokenIdentifier;
}

Expand Down Expand Up @@ -160,8 +186,9 @@ export async function sendTransaction(
);
return txHash;
} catch (e) {
// rethrown: a placeholder result only moves the failure to some later, unrelated assertion
console.error(e);
return 'n/a';
throw e;
}
}

Expand Down Expand Up @@ -287,6 +314,7 @@ export async function issueCollection(args: IssueNftArgs, type: 'NonFungible' |
`Issued ${type} collection with ticker ${args.tokenTicker}. tx hash: ${txHash}. identifier: ${tokenIdentifier}`
);

await waitForTokenIndexed(tokenIdentifier);
return tokenIdentifier;
}

Expand Down Expand Up @@ -496,6 +524,7 @@ export async function issueMultipleMetaESDTCollections(
).toString();

metaEsdtCollectionIdentifiers.push({ identifier: tokenIdentifier });
await waitForTokenIndexed(tokenIdentifier);

console.log(
`Issued MetaESDT collection ${tokenName}. tx hash: ${txHash}. identifier: ${tokenIdentifier}`,
Expand Down
12 changes: 11 additions & 1 deletion src/test/chain-simulator/utils/prepare-test-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,17 @@ async function prepareTestData() {
await ChainSimulatorUtils.deployPingPongSc(config.aliceAddress);
console.log('✓ Deployed PingPong smart contract');

await new Promise((resolve) => setTimeout(resolve, 30000));
await ChainSimulatorUtils.waitForApi('Tokens listed by the API', `${config.apiServiceUrl}/tokens/count`, count => count >= 5);
await ChainSimulatorUtils.waitForApi('Tokens listed on the issuer account', `${config.apiServiceUrl}/accounts/${config.aliceAddress}/tokens/count`, count => count >= 5);
// 2 NFT + 2 SFT collections, five items each; the meta-esdt ones come on top of that
await ChainSimulatorUtils.waitForApi('Collections listed by the API', `${config.apiServiceUrl}/collections/count`, count => count >= 4);
await ChainSimulatorUtils.waitForApi('NFTs listed by the API', `${config.apiServiceUrl}/nfts/count`, count => count >= 20);

// node and validator statistics are filled in by warmers on a one minute cron, and shards are
// derived from them. until those have run at least once, /shards is empty and nodes come back
// with a null rating and no status
await ChainSimulatorUtils.waitForApi('Shards reported by the API', `${config.apiServiceUrl}/shards`, shards => shards.length >= 4);
await ChainSimulatorUtils.waitForApi('Node ratings filled in by the API', `${config.apiServiceUrl}/nodes?size=1`, nodes => nodes.length > 0 && nodes[0].status !== undefined && nodes[0].tempRating !== null);

console.log('Test data preparation completed successfully!');
} catch (error) {
Expand Down
25 changes: 25 additions & 0 deletions src/test/chain-simulator/utils/test.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,31 @@ export class ChainSimulatorUtils {
}
}

// the api serves most of its data from caches the cache warmer fills on its own crons, and from an
// index that is refreshed on its own interval, so what the chain reports says nothing about what the
// api will return. wait on the api's own view instead of on a fixed interval that has to be long
// enough for the slowest of them
static async waitForApi(description: string, url: string, isReady: (data: any) => boolean, timeoutMs: number = 180000, intervalMs: number = 5000) {
const deadline = Date.now() + timeoutMs;
let last: any = 'no response yet';

while (Date.now() < deadline) {
try {
last = (await axios.get(url)).data;
if (isReady(last)) {
console.log(`✓ ${description}`);
return;
}
} catch (error: any) {
last = error.message;
}

await new Promise((resolve) => setTimeout(resolve, intervalMs));
}

throw new Error(`${description}: still not ready after ${timeoutMs}ms. ${url} last returned ${JSON.stringify(last).slice(0, 300)}`);
}

private static async checkSimulatorHealth(maxRetries: number = 50): Promise<boolean> {
let retries = 0;

Expand Down
6 changes: 4 additions & 2 deletions src/test/chain-simulator/utils/testSequencer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class CustomSequencer extends Sequencer {
'delegation-legacy.cs-e2e.ts',
'accounts.cs-e2e.ts',
'stake.cs-e2e.ts',
'round.cs-e2e.ts',
'rounds.cs-e2e.ts',
'results.cs-e2e.ts',
'miniblocks.cs-e2e.ts',
];
Expand All @@ -31,7 +31,9 @@ class CustomSequencer extends Sequencer {
if (indexB !== -1) {
return 1;
}
return 0;
// the files not listed above share one chain, so the order they run in decides the state each
// of them sees. leaving it to the order jest discovered them in makes that differ between runs
return testA.path.localeCompare(testB.path);
});
}
}
Expand Down
Loading
Loading