diff --git a/README.md b/README.md index e4e122c10..7672537d8 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,9 @@ CONTRACTS_PATH - path to contracts that will be used in tests NEUTRON_ADDRESS_PREFIX - address prefix for neutron controller network COSMOS_ADDRESS_PREFIX - address prefix for gaia (cosmoshub) host network NODE1_URL - url to the first node +NODE1_WS_URL - url to websocket of the first node NODE2_URL - url to the second node +NODE2_WS_URL - url to websocket of the second node BLOCKS_COUNT_BEFORE_START - how many blocks we wait before start first test NO_DOCKER - do not start cosmopark for tests NO_REBUILD - skip containers rebuilding diff --git a/src/helpers/cosmos.ts b/src/helpers/cosmos.ts index f7ade22ad..dd353d93a 100644 --- a/src/helpers/cosmos.ts +++ b/src/helpers/cosmos.ts @@ -8,17 +8,17 @@ import axios from 'axios'; import { CodeId, Wallet } from '../types'; import Long from 'long'; import path from 'path'; -import { waitBlocks } from './wait'; import { CosmosTxV1beta1GetTxResponse, InlineResponse20075TxResponse, } from '@cosmos-client/core/cjs/openapi/api'; import { cosmos, google } from '@cosmos-client/core/cjs/proto'; import { CosmosSDK } from '@cosmos-client/core/cjs/sdk'; -import { ibc } from '@cosmos-client/ibc/cjs/proto'; -import crypto from 'crypto'; import ICoin = cosmos.base.v1beta1.ICoin; +import { ibc } from '@cosmos-client/ibc/cjs/proto'; import IHeight = ibc.core.client.v1.IHeight; +import crypto from 'crypto'; +import { BlockWaiter } from './wait'; export const NEUTRON_DENOM = process.env.NEUTRON_DENOM || 'stake'; export const COSMOS_DENOM = process.env.COSMOS_DENOM || 'uatom'; @@ -134,12 +134,19 @@ cosmosclient.codec.register( export class CosmosWrapper { sdk: cosmosclient.CosmosSDK; + blockWaiter: BlockWaiter; wallet: Wallet; denom: string; - constructor(sdk: cosmosclient.CosmosSDK, wallet: Wallet, denom: string) { + constructor( + sdk: cosmosclient.CosmosSDK, + blockWaiter: BlockWaiter, + wallet: Wallet, + denom: string, + ) { this.denom = denom; this.sdk = sdk; + this.blockWaiter = blockWaiter; this.wallet = wallet; } @@ -193,7 +200,7 @@ export class CosmosWrapper { const txhash = res.data?.tx_response.txhash; let error = null; while (numAttempts > 0) { - await waitBlocks(this.sdk, 1); + await this.blockWaiter.next(); numAttempts--; const data = await rest.tx .getTx(this.sdk as CosmosSDK, txhash) @@ -303,7 +310,7 @@ export class CosmosWrapper { } numAttempts--; - await waitBlocks(this.sdk, 1); + await this.blockWaiter.next(); } throw new Error('failed to query contract'); diff --git a/src/helpers/env.ts b/src/helpers/env.ts index 5430fd6d2..420ca4646 100644 --- a/src/helpers/env.ts +++ b/src/helpers/env.ts @@ -9,7 +9,7 @@ const BLOCKS_COUNT_BEFORE_START = process.env.BLOCKS_COUNT_BEFORE_START let alreadySetUp = false; -export const setup = async (host: string) => { +export const setup = async (host1: string, host2: string) => { if (alreadySetUp) { console.log('already set up'); return; @@ -32,8 +32,12 @@ export const setup = async (host: string) => { showVersions(); await showContractsHashes(); - await waitForHTTP(host); - await waitForChannel(host); + await waitForHTTP(host1); + await waitForChannel(host1); + await waitForHTTP(host2); + await waitForChannel(host2); + await wait(20); // FIXME: this hardcoded sleep is here to wait until hermes is fully initialized. + // proper fix would be to monitor hermes status events. alreadySetUp = true; }; @@ -53,7 +57,7 @@ export const waitForHTTP = async ( } // eslint-disable-next-line no-empty } catch (e) {} - await wait(10); + await wait(1); } throw new Error('No port opened'); }; @@ -80,7 +84,7 @@ export const waitForChannel = async ( } // eslint-disable-next-line no-empty } catch (e) {} - await wait(10); + await wait(1); } throw new Error('No channel opened'); diff --git a/src/helpers/ica.ts b/src/helpers/ica.ts index 9a67c0823..6b1cfbc96 100644 --- a/src/helpers/ica.ts +++ b/src/helpers/ica.ts @@ -9,7 +9,7 @@ export const getIca = ( numAttempts = 20, ) => getWithAttempts( - cm.sdk, + cm, () => cm.queryContract<{ interchain_account_address: string; diff --git a/src/helpers/icq.ts b/src/helpers/icq.ts index d27f17df3..852b8678b 100644 --- a/src/helpers/icq.ts +++ b/src/helpers/icq.ts @@ -48,7 +48,7 @@ export const waitForICQResultWithRemoteHeight = ( numAttempts = 20, ) => getWithAttempts( - cm.sdk, + cm, () => getRegisteredQuery(cm, contractAddress, queryId), async (query) => query.registered_query.last_submitted_result_remote_height >= @@ -80,7 +80,7 @@ export const waitForTransfersAmount = ( numAttempts = 50, ) => getWithAttempts( - cm.sdk, + cm, async () => (await queryTransfersNumber(cm, contractAddress)).transfers_number, async (amount) => amount == expectedTransfersAmount, diff --git a/src/helpers/wait.ts b/src/helpers/wait.ts index df94e4388..2272974fe 100644 --- a/src/helpers/wait.ts +++ b/src/helpers/wait.ts @@ -1,4 +1,6 @@ -import { rest } from '@cosmos-client/core'; +import { rest, websocket } from '@cosmos-client/core'; + +(global as any).WebSocket = require('ws'); export const wait = async (seconds: number) => new Promise((r) => { @@ -16,23 +18,45 @@ export const getRemoteHeight = async (sdk: any) => { return +block.data.block.header.height; }; -export const waitBlocks = async (sdk: any, n: number) => { - const targetHeight = (await getRemoteHeight(sdk)) + n; - for (;;) { - await wait(1); - const currentHeight = await getRemoteHeight(sdk); - if (currentHeight >= targetHeight) { - break; +export class BlockWaiter { + url; + + constructor(url: string) { + this.url = url; + } + + next() { + return new Promise((r) => { + const ws = websocket.connect(this.url); + ws.next({ + id: '1', + jsonrpc: '2.0', + method: 'subscribe', + params: ["tm.event='NewBlock'"], + }); + ws.subscribe((x) => { + if (Object.entries((x as any).result).length !== 0) { + ws.unsubscribe(); + r(x); + } + }); + }); + } + + async waitBlocks(n: number) { + while (n > 0) { + await this.next(); + n--; } } -}; +} /** * getWithAttempts waits until readyFunc(getFunc()) returns true * and only then returns result of getFunc() */ export const getWithAttempts = async ( - sdk: any, + cm: any, getFunc: () => Promise, readyFunc: (t: T) => Promise, numAttempts = 20, @@ -48,7 +72,7 @@ export const getWithAttempts = async ( } catch (e) { error = e; } - await waitBlocks(sdk, 1); + await cm.blockWaiter.next(); } throw error != null ? error : new Error('getWithAttempts: no attempts left'); }; diff --git a/src/testcases/common_localcosmosnet.ts b/src/testcases/common_localcosmosnet.ts index 489972db8..8d5777a10 100644 --- a/src/testcases/common_localcosmosnet.ts +++ b/src/testcases/common_localcosmosnet.ts @@ -3,6 +3,7 @@ import { cosmosclient } from '@cosmos-client/core'; import { Wallet } from '../types'; import { mnemonicToWallet } from '../helpers/cosmos'; import { setup } from '../helpers/env'; +import { BlockWaiter } from '../helpers/wait'; const config = require('../config.json'); @@ -56,6 +57,8 @@ const walletSet = async ( export class TestStateLocalCosmosTestNet { sdk1: cosmosclient.CosmosSDK; sdk2: cosmosclient.CosmosSDK; + blockWaiter1: BlockWaiter; + blockWaiter2: BlockWaiter; wallets: Record>; icq_web_host: string; init = async () => { @@ -70,7 +73,14 @@ export class TestStateLocalCosmosTestNet { this.icq_web_host = 'http://localhost:9999'; - await setup(host1); + this.blockWaiter1 = new BlockWaiter( + process.env.NODE1_WS_URL || 'ws://localhost:26657', + ); + this.blockWaiter2 = new BlockWaiter( + process.env.NODE2_WS_URL || 'ws://localhost:16657', + ); + + await setup(host1, host2); this.wallets = {}; this.wallets.neutron = await walletSet(this.sdk1, neutron_prefix); diff --git a/src/testcases/interchain_kv_query.test.ts b/src/testcases/interchain_kv_query.test.ts index 18a93a7a7..d7a763c75 100644 --- a/src/testcases/interchain_kv_query.test.ts +++ b/src/testcases/interchain_kv_query.test.ts @@ -1,12 +1,12 @@ import { proto, rest } from '@cosmos-client/core'; import { - CosmosWrapper, COSMOS_DENOM, + CosmosWrapper, NEUTRON_DENOM, NeutronContract, } from '../helpers/cosmos'; import { TestStateLocalCosmosTestNet } from './common_localcosmosnet'; -import { getRemoteHeight, getWithAttempts, waitBlocks } from '../helpers/wait'; +import { getRemoteHeight, getWithAttempts } from '../helpers/wait'; import { AccAddress, ValAddress } from '@cosmos-client/core/cjs/types'; import { CosmosSDK } from '@cosmos-client/core/cjs/sdk'; import { @@ -174,10 +174,10 @@ const acceptInterchainqueriesParamsChangeProposal = async ( const proposalId = parseInt(attribute); expect(proposalId).toBeGreaterThanOrEqual(0); - await waitBlocks(cm.sdk, 1); + await cm[1].blockWaiter.next(); await cm.voteYes(proposalId, wallet.address.toString()); - await waitBlocks(cm.sdk, 1); + await cm[1].blockWaiter.next(); await cm.executeProposal(proposalId, wallet.address.toString()); await getWithAttempts( @@ -271,11 +271,13 @@ describe('Neutron / Interchain KV Query', () => { cm = { 1: new CosmosWrapper( testState.sdk1, + testState.blockWaiter1, testState.wallets.neutron.demo1, NEUTRON_DENOM, ), 2: new CosmosWrapper( testState.sdk2, + testState.blockWaiter2, testState.wallets.cosmos.demo2, COSMOS_DENOM, ), @@ -599,7 +601,7 @@ describe('Neutron / Interchain KV Query', () => { for (const j of res) { expect(j).not.toEqual(0); } - await waitBlocks(cm[1].sdk, 1); + await cm[1].blockWaiter.next(); } const end = await Promise.all( [2, 3, 4].map((i) => getKvCallbackStatus(cm[1], contractAddress, i)), @@ -660,7 +662,7 @@ describe('Neutron / Interchain KV Query', () => { testState.wallets.cosmos.demo2.address, ); - await waitBlocks(cm[1].sdk, 1); + await cm[1].blockWaiter.next(); const queryResult = await getRegisteredQuery( cm[1], @@ -720,7 +722,7 @@ describe('Neutron / Interchain KV Query', () => { testState.wallets.cosmos.demo2.address, ); - await waitBlocks(cm[1].sdk, 1); + await cm[1].blockWaiter.next(); const queryResult = await getRegisteredQuery( cm[1], diff --git a/src/testcases/interchain_tx_query.test.ts b/src/testcases/interchain_tx_query.test.ts index edfa97802..244c87f55 100644 --- a/src/testcases/interchain_tx_query.test.ts +++ b/src/testcases/interchain_tx_query.test.ts @@ -1,12 +1,11 @@ import { - CosmosWrapper, COSMOS_DENOM, + CosmosWrapper, NEUTRON_DENOM, NeutronContract, } from '../helpers/cosmos'; import { proto } from '@cosmos-client/core'; import { TestStateLocalCosmosTestNet } from './common_localcosmosnet'; -import { waitBlocks } from '../helpers/wait'; import Long from 'long'; import { getRegisteredQuery, @@ -28,11 +27,13 @@ describe('Neutron / Interchain TX Query', () => { await testState.init(); cm = new CosmosWrapper( testState.sdk1, + testState.blockWaiter1, testState.wallets.neutron.demo1, NEUTRON_DENOM, ); cm2 = new CosmosWrapper( testState.sdk2, + testState.blockWaiter2, testState.wallets.cosmos.demo2, COSMOS_DENOM, ); @@ -139,7 +140,7 @@ describe('Neutron / Interchain TX Query', () => { expect(balances.balances).toEqual([ { amount: addr2ExpectedBalance.toString(), denom: cm2.denom }, ]); - await waitBlocks(cm.sdk, query1UpdatePeriod * 2); // we are waiting for quite a big time just to be sure + await cm.blockWaiter.waitBlocks(query1UpdatePeriod * 2); // we are waiting for quite a big time just to be sure // the different address is not registered by the contract, so its receivings aren't tracked let deposits = await queryRecipientTxs( @@ -168,7 +169,7 @@ describe('Neutron / Interchain TX Query', () => { expect(balances.balances).toEqual([ { amount: addr1ExpectedBalance.toString(), denom: cm2.denom }, // balance hasn't changed thus tx failed ]); - await waitBlocks(cm.sdk, query1UpdatePeriod * 2 + 1); // we are waiting for quite a big time just to be sure + await cm.blockWaiter.waitBlocks(query1UpdatePeriod * 2 + 1); // we are waiting for quite a big time just to be sure // the watched address receivings are not changed const deposits = await queryRecipientTxs( @@ -471,7 +472,7 @@ describe('Neutron / Interchain TX Query', () => { const watchedAddr5: string = addr5; const query5UpdatePeriod = 12; - // by this checks we ensure the transactions will be processed in the desired order + // by these checks we ensure the transactions will be processed in the desired order test('validate update periods', async () => { expect(query5UpdatePeriod).toBeGreaterThanOrEqual(9); expect(query5UpdatePeriod).toBeGreaterThanOrEqual(query4UpdatePeriod * 3); @@ -494,7 +495,7 @@ describe('Neutron / Interchain TX Query', () => { query5UpdatePeriod, watchedAddr5, ); - await waitBlocks(cm.sdk, 2); // wait for queries handling on init + await cm.blockWaiter.waitBlocks(2); // wait for queries handling on init }); test('make older sending', async () => { @@ -613,7 +614,7 @@ describe('Neutron / Interchain TX Query', () => { }); test('check that transfer has not been recorded', async () => { - await waitBlocks(cm.sdk, query4UpdatePeriod * 2 + 1); // we are waiting for quite a big time just to be sure + await cm.blockWaiter.waitBlocks(query4UpdatePeriod * 2 + 1); // we are waiting for quite a big time just to be sure const deposits = await queryRecipientTxs( cm, contractAddress, diff --git a/src/testcases/interchain_tx_query_resubmit.test.ts b/src/testcases/interchain_tx_query_resubmit.test.ts index 14fe852c9..caa6621ad 100644 --- a/src/testcases/interchain_tx_query_resubmit.test.ts +++ b/src/testcases/interchain_tx_query_resubmit.test.ts @@ -1,6 +1,5 @@ -import { CosmosWrapper, COSMOS_DENOM, NEUTRON_DENOM } from '../helpers/cosmos'; +import { COSMOS_DENOM, CosmosWrapper, NEUTRON_DENOM } from '../helpers/cosmos'; import { TestStateLocalCosmosTestNet } from './common_localcosmosnet'; -import { waitBlocks } from '../helpers/wait'; import { getRegisteredQuery, getUnsuccessfulTxs, @@ -22,11 +21,13 @@ describe('Neutron / Interchain TX Query Resubmit', () => { await testState.init(); cm = new CosmosWrapper( testState.sdk1, + testState.blockWaiter1, testState.wallets.neutron.demo1, NEUTRON_DENOM, ); cm2 = new CosmosWrapper( testState.sdk2, + testState.blockWaiter2, testState.wallets.cosmos.demo2, COSMOS_DENOM, ); @@ -101,7 +102,7 @@ describe('Neutron / Interchain TX Query Resubmit', () => { expect(res.code).toEqual(0); } - await waitBlocks(cm.sdk, 5); + await cm.blockWaiter.waitBlocks(5); const txs = await getUnsuccessfulTxs(testState.icq_web_host); expect(txs.length).toEqual(5); @@ -121,7 +122,7 @@ describe('Neutron / Interchain TX Query Resubmit', () => { const resp = await postResubmitTxs(testState.icq_web_host, resubmit_txs); expect(resp.status).toEqual(200); - await waitBlocks(cm.sdk, 20); + await cm.blockWaiter.waitBlocks(20); await waitForTransfersAmount( cm, diff --git a/src/testcases/interchaintx.test.ts b/src/testcases/interchaintx.test.ts index 7af56f12b..885ae0ca6 100644 --- a/src/testcases/interchaintx.test.ts +++ b/src/testcases/interchaintx.test.ts @@ -11,7 +11,7 @@ import { } from '../helpers/cosmos'; import { AcknowledgementResult } from '../helpers/contract_types'; import { TestStateLocalCosmosTestNet } from './common_localcosmosnet'; -import { getWithAttempts, waitBlocks } from '../helpers/wait'; +import { getWithAttempts } from '../helpers/wait'; import { CosmosSDK } from '@cosmos-client/core/cjs/sdk'; import { getIca } from '../helpers/ica'; @@ -31,11 +31,13 @@ describe('Neutron / Interchain TXs', () => { await testState.init(); cm1 = new CosmosWrapper( testState.sdk1, + testState.blockWaiter1, testState.wallets.neutron.demo1, NEUTRON_DENOM, ); cm2 = new CosmosWrapper( testState.sdk2, + testState.blockWaiter2, testState.wallets.cosmos.demo2, COSMOS_DENOM, ); @@ -88,7 +90,7 @@ describe('Neutron / Interchain TXs', () => { }); test('multiple IBC accounts created', async () => { const channels = await getWithAttempts( - cm1.sdk, + cm1, () => cm1.listIBCChannels(), // Wait until there are 3 channels: // - one exists already, it is open for IBC transfers; @@ -182,7 +184,7 @@ describe('Neutron / Interchain TXs', () => { }); test('check validator state', async () => { const res1 = await getWithAttempts( - cm2.sdk, + cm2, () => rest.staking.delegatorDelegations( cm2.sdk as CosmosSDK, @@ -432,7 +434,7 @@ describe('Neutron / Interchain TXs', () => { ); expect(res.code).toEqual(0); await getWithAttempts( - cm1.sdk, + cm1, async () => cm1.listIBCChannels(), // Wait until there are 4 channels: // - one exists already, it is open for IBC transfers; @@ -441,7 +443,7 @@ describe('Neutron / Interchain TXs', () => { async (channels) => channels.channels.length == 4, ); await getWithAttempts( - cm1.sdk, + cm1, () => cm1.listIBCChannels(), async (channels) => channels.channels.find((c) => c.channel_id == 'channel-3').state == @@ -515,7 +517,7 @@ describe('Neutron / Interchain TXs', () => { }), ); - await waitBlocks(cm1.sdk, 10); + await cm1.blockWaiter.waitBlocks(10); // Testing ACK timeout failure await cm1.executeContract( @@ -532,7 +534,7 @@ describe('Neutron / Interchain TXs', () => { ); const failuresAfterCall = await getWithAttempts( - cm1.sdk, + cm1, async () => cm1.queryAckFailures(contractAddress), // Wait until there 2 failure in the list async (data) => data.failures.length == 2, @@ -583,7 +585,7 @@ const waitForAck = ( numAttempts = 20, ) => getWithAttempts( - cm.sdk, + cm, () => cm.queryContract(contractAddress, { acknowledgement_result: { diff --git a/src/testcases/simple.test.ts b/src/testcases/simple.test.ts index 11d6f5acd..675b308ca 100644 --- a/src/testcases/simple.test.ts +++ b/src/testcases/simple.test.ts @@ -8,7 +8,7 @@ import { NeutronContract, PageRequest, } from '../helpers/cosmos'; -import { getRemoteHeight, getWithAttempts, waitBlocks } from '../helpers/wait'; +import { getRemoteHeight, getWithAttempts } from '../helpers/wait'; import { TestStateLocalCosmosTestNet } from './common_localcosmosnet'; describe('Neutron / Simple', () => { @@ -22,11 +22,13 @@ describe('Neutron / Simple', () => { await testState.init(); cm = new CosmosWrapper( testState.sdk1, + testState.blockWaiter1, testState.wallets.neutron.demo1, NEUTRON_DENOM, ); cm2 = new CosmosWrapper( testState.sdk2, + testState.blockWaiter2, testState.wallets.cosmos.demo2, COSMOS_DENOM, ); @@ -62,7 +64,7 @@ describe('Neutron / Simple', () => { describe('Correct way', () => { let relayerBalance = 0; beforeAll(async () => { - await waitBlocks(cm.sdk, 10); + await cm.blockWaiter.waitBlocks(10); const balances = await cm.queryBalances(IBC_RELAYER_NEUTRON_ADDRESS); relayerBalance = parseInt( balances.balances.find((bal) => bal.denom == NEUTRON_DENOM)?.amount || @@ -91,7 +93,7 @@ describe('Neutron / Simple', () => { expect(res.code).toEqual(0); }); test('check IBC token balance', async () => { - await waitBlocks(cm.sdk, 10); + await cm.blockWaiter.waitBlocks(10); const balances = await cm2.queryBalances( testState.wallets.cosmos.demo2.address.toString(), ); @@ -114,7 +116,7 @@ describe('Neutron / Simple', () => { expect(res.code).toEqual(0); }); test('check uatom token balance transfered via IBC on Neutron', async () => { - await waitBlocks(cm.sdk, 10); + await cm.blockWaiter.waitBlocks(10); const balances = await cm.queryBalances( testState.wallets.neutron.demo1.address.toString(), ); @@ -163,7 +165,7 @@ describe('Neutron / Simple', () => { }); test('check wallet balance', async () => { - await waitBlocks(cm.sdk, 10); + await cm.blockWaiter.waitBlocks(10); const balances = await cm2.queryBalances( testState.wallets.cosmos.demo2.address.toString(), ); @@ -186,7 +188,7 @@ describe('Neutron / Simple', () => { expect(balance - 2333 * 2 - relayerBalance).toBeLessThan(5); // it may differ by about 1-2 because of the gas fee }); test('contract should be refunded', async () => { - await waitBlocks(cm.sdk, 10); + await cm.blockWaiter.waitBlocks(10); const balances = await cm.queryBalances(contractAddress); const balance = parseInt( balances.balances.find((bal) => bal.denom == NEUTRON_DENOM)?.amount || @@ -244,7 +246,7 @@ describe('Neutron / Simple', () => { ); expect(res.code).toEqual(0); - await waitBlocks(cm.sdk, 10); + await cm.blockWaiter.waitBlocks(10); const balances = await cm.queryBalances(contractAddress); expect( balances.balances.find((bal): boolean => bal.denom == uatomIBCDenom) @@ -349,7 +351,7 @@ describe('Neutron / Simple', () => { }), ); - await waitBlocks(cm.sdk, 3); + await cm.blockWaiter.waitBlocks(3); const currentHeight = await getRemoteHeight(cm.sdk); await cm.executeContract( @@ -366,7 +368,7 @@ describe('Neutron / Simple', () => { ); const failuresAfterCall = await getWithAttempts( - cm.sdk, + cm, async () => cm.queryAckFailures(contractAddress), // Wait until there 4 failure in the list async (data) => data.failures.length == 4,