diff --git a/CHANGELOG.md b/CHANGELOG.md index 1417e327ea2..f2e1c219adc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -958,3 +958,21 @@ should use 4.0.1-alpha.0 for testing. - Parameters decoding error for nested components (#5714) ## [Unreleased] + +### Changed + +#### web3-providers-ipc + +- Refactor to use common SocketProvider class (#5683) + +#### web3-providers-ws + +- Refactor to use common SocketProvider class (#5683) + +#### web3-utils + +- Add SocketProvider class and Eip1193Provider abstract class (#5683) + +#### web3-types + +- These types were added: ProviderRpcError, EthSubscription, ProviderMessage, ProviderConnectInfo (#5683) diff --git a/packages/web3-eth-ens/test/integration/ens.test.ts b/packages/web3-eth-ens/test/integration/ens.test.ts index 85c1d7dce2b..82433eeb602 100644 --- a/packages/web3-eth-ens/test/integration/ens.test.ts +++ b/packages/web3-eth-ens/test/integration/ens.test.ts @@ -30,6 +30,7 @@ import { isWs, isIpc, closeOpenConnection, + isSocket, } from '../fixtures/system_tests_utils'; import { ENSRegistryAbi } from '../../src/abi/ens/ENSRegistry'; @@ -132,9 +133,10 @@ describe('ens', () => { }); afterAll(async () => { - if (isWs || isIpc) { + if (isSocket) { await closeOpenConnection(ens); - await closeOpenConnection(ens['_registry']['contract']); + // @ts-expect-error @typescript-eslint/ban-ts-comment + await closeOpenConnection(ens?._registry?.contract); await closeOpenConnection(getEnsResolver); await closeOpenConnection(setEnsResolver); await closeOpenConnection(registry); diff --git a/packages/web3-eth-ens/test/integration/resolver.test.ts b/packages/web3-eth-ens/test/integration/resolver.test.ts index 63b494458db..096df74ac6c 100644 --- a/packages/web3-eth-ens/test/integration/resolver.test.ts +++ b/packages/web3-eth-ens/test/integration/resolver.test.ts @@ -30,6 +30,7 @@ import { isWs, isIpc, closeOpenConnection, + isSocket, } from '../fixtures/system_tests_utils'; import { ENSRegistryAbi } from '../../src/abi/ens/ENSRegistry'; @@ -130,9 +131,10 @@ describe('ens', () => { }); afterAll(async () => { - if (isWs || isIpc) { + if (isSocket) { await closeOpenConnection(ens); - await closeOpenConnection(ens['_registry']['contract']); + // @ts-expect-error @typescript-eslint/ban-ts-comment + await closeOpenConnection(ens?._registry?.contract); await closeOpenConnection(registry); await closeOpenConnection(resolver); await closeOpenConnection(nameWrapper); diff --git a/packages/web3-eth/test/integration/batch.test.ts b/packages/web3-eth/test/integration/batch.test.ts index 5e00bdd3688..1e167740b6f 100644 --- a/packages/web3-eth/test/integration/batch.test.ts +++ b/packages/web3-eth/test/integration/batch.test.ts @@ -14,7 +14,6 @@ GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ -import WebSocketProvider from 'web3-providers-ws'; // eslint-disable-next-line import/no-extraneous-dependencies import { hexToNumber } from 'web3-utils'; import { Web3Eth } from '../../src'; @@ -23,7 +22,7 @@ import { closeOpenConnection, createTempAccount, getSystemTestProvider, - isWs, + waitForOpenConnection, } from '../fixtures/system_test_utils'; describe('eth', () => { @@ -33,18 +32,8 @@ describe('eth', () => { beforeAll(async () => { clientUrl = getSystemTestProvider(); - - if (isWs) { - web3Eth = new Web3Eth( - new WebSocketProvider( - clientUrl, - {}, - { delay: 1, autoReconnect: false, maxAttempts: 1 }, - ), - ); - } else { - web3Eth = new Web3Eth(clientUrl); - } + web3Eth = new Web3Eth(clientUrl); + await waitForOpenConnection(web3Eth); }); afterAll(async () => { await closeOpenConnection(web3Eth); @@ -54,6 +43,7 @@ describe('eth', () => { it('BatchRequest', async () => { const acc1 = await createTempAccount(); const acc2 = await createTempAccount(); + const batch = new web3Eth.BatchRequest(); const request1 = { id: 10, diff --git a/packages/web3-eth/test/integration/block/rpc.getBlock.test.ts b/packages/web3-eth/test/integration/block/rpc.getBlock.test.ts new file mode 100644 index 00000000000..9fd0693b3fc --- /dev/null +++ b/packages/web3-eth/test/integration/block/rpc.getBlock.test.ts @@ -0,0 +1,130 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ +import { FMT_BYTES, FMT_NUMBER } from 'web3-utils'; +import { TransactionReceipt, Transaction } from 'web3-types'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { Contract } from 'web3-eth-contract'; +import { validator } from 'web3-validator'; +import { Web3Eth } from '../../../src'; +import { + getSystemTestProvider, + createTempAccount, + closeOpenConnection, +} from '../../fixtures/system_test_utils'; +import { BasicAbi, BasicBytecode } from '../../shared_fixtures/build/Basic'; +import { toAllVariants } from '../../shared_fixtures/utils'; +import { sendFewTxes } from '../helper'; +import { blockSchema } from '../../../src/schemas'; + +describe('rpc with block', () => { + let web3Eth: Web3Eth; + let clientUrl: string; + + let contract: Contract; + let deployOptions: Record; + let sendOptions: Record; + + let blockData: { + earliest: 'earliest'; + latest: 'latest'; + pending: 'pending'; + blockNumber: number | bigint; + blockHash: string; + transactionHash: string; + transactionIndex: number | bigint; + }; + let tempAcc: { address: string; privateKey: string }; + let tempAcc2: { address: string; privateKey: string }; + + beforeAll(() => { + clientUrl = getSystemTestProvider(); + web3Eth = new Web3Eth({ + provider: clientUrl, + config: { + transactionPollingTimeout: 2000, + }, + }); + + contract = new Contract(BasicAbi, undefined, { + provider: clientUrl, + }); + + deployOptions = { + data: BasicBytecode, + arguments: [10, 'string init value'], + }; + }); + beforeAll(async () => { + tempAcc = await createTempAccount(); + tempAcc2 = await createTempAccount(); + sendOptions = { from: tempAcc.address, gas: '1000000' }; + + await contract.deploy(deployOptions).send(sendOptions); + const [receipt]: TransactionReceipt[] = await sendFewTxes({ + web3Eth, + from: tempAcc.address, + to: tempAcc2.address, + value: '0x1', + times: 1, + }); + blockData = { + pending: 'pending', + latest: 'latest', + earliest: 'earliest', + blockNumber: Number(receipt.blockNumber), + blockHash: String(receipt.blockHash), + transactionHash: String(receipt.transactionHash), + transactionIndex: Number(receipt.transactionIndex), + }; + }); + afterAll(async () => { + await closeOpenConnection(web3Eth); + await closeOpenConnection(contract); + }); + + describe('methods', () => { + it.each( + toAllVariants<{ + block: 'earliest' | 'latest' | 'pending' | 'blockHash' | 'blockNumber'; + hydrated: boolean; + format: string; + }>({ + block: ['earliest', 'latest', 'blockHash', 'blockNumber'], + hydrated: [true, false], + format: Object.values(FMT_NUMBER), + }), + )('getBlock', async ({ hydrated, block, format }) => { + const b = { + ...(await web3Eth.getBlock(blockData[block], hydrated, { + number: format as FMT_NUMBER, + bytes: FMT_BYTES.HEX, + })), + }; + if (blockData[block] === 'pending') { + b.nonce = '0x0'; + b.miner = '0x0000000000000000000000000000000000000000'; + b.totalDifficulty = '0x0'; + } + expect(validator.validateJSONSchema(blockSchema, b)).toBeUndefined(); + + if (hydrated && b.transactions?.length > 0) { + // eslint-disable-next-line jest/no-conditional-expect + expect(b.transactions).toBeInstanceOf(Array); + } + }); + }); +}); diff --git a/packages/web3-eth/test/integration/block/rpc.getBlockTransactionCount.test.ts b/packages/web3-eth/test/integration/block/rpc.getBlockTransactionCount.test.ts new file mode 100644 index 00000000000..d64d2a43794 --- /dev/null +++ b/packages/web3-eth/test/integration/block/rpc.getBlockTransactionCount.test.ts @@ -0,0 +1,115 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ +import { TransactionReceipt } from 'web3-types'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { Contract } from 'web3-eth-contract'; +import { Web3Eth } from '../../../src'; +import { + getSystemTestBackend, + getSystemTestProvider, + createTempAccount, + closeOpenConnection, +} from '../../fixtures/system_test_utils'; +import { BasicAbi, BasicBytecode } from '../../shared_fixtures/build/Basic'; +import { toAllVariants } from '../../shared_fixtures/utils'; +import { sendFewTxes } from '../helper'; + +describe('rpc with block', () => { + let web3Eth: Web3Eth; + let clientUrl: string; + + let contract: Contract; + let deployOptions: Record; + let sendOptions: Record; + + let blockData: { + earliest: 'earliest'; + latest: 'latest'; + pending: 'pending'; + blockNumber: number | bigint; + blockHash: string; + transactionHash: string; + transactionIndex: number | bigint; + }; + let tempAcc: { address: string; privateKey: string }; + let tempAcc2: { address: string; privateKey: string }; + + beforeAll(() => { + clientUrl = getSystemTestProvider(); + web3Eth = new Web3Eth({ + provider: clientUrl, + config: { + transactionPollingTimeout: 2000, + }, + }); + + contract = new Contract(BasicAbi, undefined, { + provider: clientUrl, + }); + + deployOptions = { + data: BasicBytecode, + arguments: [10, 'string init value'], + }; + }); + beforeAll(async () => { + tempAcc = await createTempAccount(); + tempAcc2 = await createTempAccount(); + sendOptions = { from: tempAcc.address, gas: '1000000' }; + + await contract.deploy(deployOptions).send(sendOptions); + const [receipt]: TransactionReceipt[] = await sendFewTxes({ + web3Eth, + from: tempAcc.address, + to: tempAcc2.address, + value: '0x1', + times: 1, + }); + blockData = { + pending: 'pending', + latest: 'latest', + earliest: 'earliest', + blockNumber: Number(receipt.blockNumber), + blockHash: String(receipt.blockHash), + transactionHash: String(receipt.transactionHash), + transactionIndex: Number(receipt.transactionIndex), + }; + }); + afterAll(async () => { + await closeOpenConnection(web3Eth); + await closeOpenConnection(contract); + }); + + describe('methods', () => { + it.each( + toAllVariants<{ + block: 'earliest' | 'latest' | 'pending' | 'blockHash' | 'blockNumber'; + }>({ + block: ['earliest', 'latest', 'pending', 'blockHash', 'blockNumber'], + }), + )('getBlockTransactionCount', async ({ block }) => { + const res = await web3Eth.getBlockTransactionCount(blockData[block]); + let shouldBe: number; + if (getSystemTestBackend() === 'ganache') { + shouldBe = blockData[block] === 'earliest' ? 0 : 1; + } else { + shouldBe = ['earliest', 'pending'].includes(String(blockData[block])) ? 0 : 1; + } + expect(Number(res)).toBe(shouldBe); + }); + }); +}); diff --git a/packages/web3-eth/test/integration/block/rpc.getBlockUncleCount.test.ts b/packages/web3-eth/test/integration/block/rpc.getBlockUncleCount.test.ts new file mode 100644 index 00000000000..3c4d6ef911b --- /dev/null +++ b/packages/web3-eth/test/integration/block/rpc.getBlockUncleCount.test.ts @@ -0,0 +1,108 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ +import { TransactionReceipt } from 'web3-types'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { Contract } from 'web3-eth-contract'; +import { Web3Eth } from '../../../src'; +import { + getSystemTestProvider, + createTempAccount, + closeOpenConnection, +} from '../../fixtures/system_test_utils'; +import { BasicAbi, BasicBytecode } from '../../shared_fixtures/build/Basic'; +import { toAllVariants } from '../../shared_fixtures/utils'; +import { sendFewTxes } from '../helper'; + +describe('rpc with block', () => { + let web3Eth: Web3Eth; + let clientUrl: string; + + let contract: Contract; + let deployOptions: Record; + let sendOptions: Record; + + let blockData: { + earliest: 'earliest'; + latest: 'latest'; + pending: 'pending'; + blockNumber: number | bigint; + blockHash: string; + transactionHash: string; + transactionIndex: number | bigint; + }; + let tempAcc: { address: string; privateKey: string }; + let tempAcc2: { address: string; privateKey: string }; + + beforeAll(() => { + clientUrl = getSystemTestProvider(); + web3Eth = new Web3Eth({ + provider: clientUrl, + config: { + transactionPollingTimeout: 2000, + }, + }); + + contract = new Contract(BasicAbi, undefined, { + provider: clientUrl, + }); + + deployOptions = { + data: BasicBytecode, + arguments: [10, 'string init value'], + }; + }); + beforeAll(async () => { + tempAcc = await createTempAccount(); + tempAcc2 = await createTempAccount(); + sendOptions = { from: tempAcc.address, gas: '1000000' }; + + await contract.deploy(deployOptions).send(sendOptions); + const [receipt]: TransactionReceipt[] = await sendFewTxes({ + web3Eth, + from: tempAcc.address, + to: tempAcc2.address, + value: '0x1', + times: 1, + }); + blockData = { + pending: 'pending', + latest: 'latest', + earliest: 'earliest', + blockNumber: Number(receipt.blockNumber), + blockHash: String(receipt.blockHash), + transactionHash: String(receipt.transactionHash), + transactionIndex: Number(receipt.transactionIndex), + }; + }); + afterAll(async () => { + await closeOpenConnection(web3Eth); + await closeOpenConnection(contract); + }); + + describe('methods', () => { + it.each( + toAllVariants<{ + block: 'earliest' | 'latest' | 'pending' | 'blockHash' | 'blockNumber'; + }>({ + block: ['earliest', 'latest', 'pending', 'blockHash', 'blockNumber'], + }), + )('getBlockUncleCount', async ({ block }) => { + const res = await web3Eth.getBlockUncleCount(blockData[block]); + expect(Number(res)).toBe(0); + }); + }); +}); diff --git a/packages/web3-eth/test/integration/rpc.block.test.ts b/packages/web3-eth/test/integration/block/rpc.getTransactionCount.test.ts similarity index 52% rename from packages/web3-eth/test/integration/rpc.block.test.ts rename to packages/web3-eth/test/integration/block/rpc.getTransactionCount.test.ts index b8ebf18f99b..6466d0c78a2 100644 --- a/packages/web3-eth/test/integration/rpc.block.test.ts +++ b/packages/web3-eth/test/integration/block/rpc.getTransactionCount.test.ts @@ -15,27 +15,19 @@ You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ import { FMT_BYTES, FMT_NUMBER } from 'web3-utils'; -import { TransactionInfo, TransactionReceipt, Transaction } from 'web3-types'; +import { TransactionReceipt } from 'web3-types'; // eslint-disable-next-line import/no-extraneous-dependencies import { Contract } from 'web3-eth-contract'; -// eslint-disable-next-line import/no-extraneous-dependencies -import IpcProvider from 'web3-providers-ipc'; -import { validator } from 'web3-validator'; - -import { Web3Eth } from '../../src'; - +import { Web3Eth } from '../../../src'; import { - getSystemTestBackend, getSystemTestProvider, createNewAccount, - isIpc, createTempAccount, closeOpenConnection, -} from '../fixtures/system_test_utils'; -import { BasicAbi, BasicBytecode } from '../shared_fixtures/build/Basic'; -import { toAllVariants } from '../shared_fixtures/utils'; -import { sendFewTxes, validateTransaction } from './helper'; -import { blockSchema } from '../../src/schemas'; +} from '../../fixtures/system_test_utils'; +import { BasicAbi, BasicBytecode } from '../../shared_fixtures/build/Basic'; +import { toAllVariants } from '../../shared_fixtures/utils'; +import { sendFewTxes } from '../helper'; describe('rpc with block', () => { let web3Eth: Web3Eth; @@ -57,7 +49,7 @@ describe('rpc with block', () => { let tempAcc: { address: string; privateKey: string }; let tempAcc2: { address: string; privateKey: string }; - beforeAll(async () => { + beforeAll(() => { clientUrl = getSystemTestProvider(); web3Eth = new Web3Eth({ provider: clientUrl, @@ -74,10 +66,6 @@ describe('rpc with block', () => { data: BasicBytecode, arguments: [10, 'string init value'], }; - if (isIpc) { - await (contract.provider as IpcProvider).waitForConnection(); - await (web3Eth.provider as IpcProvider).waitForConnection(); - } }); beforeAll(async () => { tempAcc = await createTempAccount(); @@ -108,60 +96,6 @@ describe('rpc with block', () => { }); describe('methods', () => { - beforeAll(async () => { - tempAcc = await createTempAccount(); - tempAcc2 = await createTempAccount(); - sendOptions = { from: tempAcc.address, gas: '1000000' }; - - await contract.deploy(deployOptions).send(sendOptions); - const [receipt]: TransactionReceipt[] = await sendFewTxes({ - web3Eth, - from: tempAcc.address, - to: tempAcc2.address, - value: '0x1', - times: 1, - }); - blockData = { - pending: 'pending', - latest: 'latest', - earliest: 'earliest', - blockNumber: Number(receipt.blockNumber), - blockHash: String(receipt.blockHash), - transactionHash: String(receipt.transactionHash), - transactionIndex: Number(receipt.transactionIndex), - }; - }); - - it.each( - toAllVariants<{ - block: 'earliest' | 'latest' | 'pending' | 'blockHash' | 'blockNumber'; - hydrated: boolean; - format: string; - }>({ - block: ['earliest', 'latest', 'blockHash', 'blockNumber'], - hydrated: [true, false], - format: Object.values(FMT_NUMBER), - }), - )('getBlock', async ({ hydrated, block, format }) => { - const b = { - ...(await web3Eth.getBlock(blockData[block], hydrated, { - number: format as FMT_NUMBER, - bytes: FMT_BYTES.HEX, - })), - }; - if (blockData[block] === 'pending') { - b.nonce = '0x0'; - b.miner = '0x0000000000000000000000000000000000000000'; - b.totalDifficulty = '0x0'; - } - expect(validator.validateJSONSchema(blockSchema, b)).toBeUndefined(); - - if (hydrated && b.transactions?.length > 0) { - // eslint-disable-next-line jest/no-conditional-expect - expect(b.transactions).toBeInstanceOf(Array); - } - }); - it.each( toAllVariants<{ block: 'earliest' | 'latest' | 'pending' | 'blockHash' | 'blockNumber'; @@ -219,54 +153,5 @@ describe('rpc with block', () => { blockData[block] === 'earliest' ? 0 : count, ); }); - - it.each( - toAllVariants<{ - block: 'earliest' | 'latest' | 'pending' | 'blockHash' | 'blockNumber'; - }>({ - block: ['earliest', 'latest', 'pending', 'blockHash', 'blockNumber'], - }), - )('getBlockTransactionCount', async ({ block }) => { - const res = await web3Eth.getBlockTransactionCount(blockData[block]); - let shouldBe: number; - if (getSystemTestBackend() === 'ganache') { - shouldBe = blockData[block] === 'earliest' ? 0 : 1; - } else { - shouldBe = ['earliest', 'pending'].includes(String(blockData[block])) ? 0 : 1; - } - expect(Number(res)).toBe(shouldBe); - }); - - it.each( - toAllVariants<{ - block: 'earliest' | 'latest' | 'pending' | 'blockHash' | 'blockNumber'; - }>({ - block: ['earliest', 'latest', 'pending', 'blockHash', 'blockNumber'], - }), - )('getBlockUncleCount', async ({ block }) => { - const res = await web3Eth.getBlockUncleCount(blockData[block]); - expect(Number(res)).toBe(0); - }); - - it.each( - toAllVariants<{ - block: 'earliest' | 'latest' | 'pending' | 'blockNumber'; - }>({ - block: ['earliest', 'latest', 'pending', 'blockNumber'], - }), - )('getUncle', async ({ block }) => { - const res = await web3Eth.getUncle(blockData[block], 0); - // eslint-disable-next-line jest/no-standalone-expect - expect(res).toBeNull(); - }); - it('getTransactionFromBlock', async () => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const tx = (await web3Eth.getTransactionFromBlock( - blockData.blockNumber, - blockData.transactionIndex, - ))!; - validateTransaction(tx as TransactionInfo); - expect(tx?.hash).toBe(blockData.transactionHash); - }); }); }); diff --git a/packages/web3-eth/test/integration/block/rpc.getTransactionFromBlock.test.ts b/packages/web3-eth/test/integration/block/rpc.getTransactionFromBlock.test.ts new file mode 100644 index 00000000000..16317373c1a --- /dev/null +++ b/packages/web3-eth/test/integration/block/rpc.getTransactionFromBlock.test.ts @@ -0,0 +1,106 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ +import { TransactionInfo, TransactionReceipt } from 'web3-types'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { Contract } from 'web3-eth-contract'; +import { Web3Eth } from '../../../src'; +import { + getSystemTestProvider, + createTempAccount, + closeOpenConnection, +} from '../../fixtures/system_test_utils'; +import { BasicAbi, BasicBytecode } from '../../shared_fixtures/build/Basic'; +import { sendFewTxes, validateTransaction } from '../helper'; + +describe('rpc with block', () => { + let web3Eth: Web3Eth; + let clientUrl: string; + + let contract: Contract; + let deployOptions: Record; + let sendOptions: Record; + + let blockData: { + earliest: 'earliest'; + latest: 'latest'; + pending: 'pending'; + blockNumber: number | bigint; + blockHash: string; + transactionHash: string; + transactionIndex: number | bigint; + }; + let tempAcc: { address: string; privateKey: string }; + let tempAcc2: { address: string; privateKey: string }; + + beforeAll(() => { + clientUrl = getSystemTestProvider(); + web3Eth = new Web3Eth({ + provider: clientUrl, + config: { + transactionPollingTimeout: 2000, + }, + }); + + contract = new Contract(BasicAbi, undefined, { + provider: clientUrl, + }); + + deployOptions = { + data: BasicBytecode, + arguments: [10, 'string init value'], + }; + }); + beforeAll(async () => { + tempAcc = await createTempAccount(); + tempAcc2 = await createTempAccount(); + sendOptions = { from: tempAcc.address, gas: '1000000' }; + + await contract.deploy(deployOptions).send(sendOptions); + const [receipt]: TransactionReceipt[] = await sendFewTxes({ + web3Eth, + from: tempAcc.address, + to: tempAcc2.address, + value: '0x1', + times: 1, + }); + blockData = { + pending: 'pending', + latest: 'latest', + earliest: 'earliest', + blockNumber: Number(receipt.blockNumber), + blockHash: String(receipt.blockHash), + transactionHash: String(receipt.transactionHash), + transactionIndex: Number(receipt.transactionIndex), + }; + }); + afterAll(async () => { + await closeOpenConnection(web3Eth); + await closeOpenConnection(contract); + }); + + describe('methods', () => { + it('getTransactionFromBlock', async () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const tx = (await web3Eth.getTransactionFromBlock( + blockData.blockNumber, + blockData.transactionIndex, + ))!; + validateTransaction(tx as TransactionInfo); + expect(tx?.hash).toBe(blockData.transactionHash); + }); + }); +}); diff --git a/packages/web3-eth/test/integration/block/rpc.getUncle.test.ts b/packages/web3-eth/test/integration/block/rpc.getUncle.test.ts new file mode 100644 index 00000000000..89dd0e0b3ca --- /dev/null +++ b/packages/web3-eth/test/integration/block/rpc.getUncle.test.ts @@ -0,0 +1,111 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ +import { TransactionReceipt } from 'web3-types'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { Contract } from 'web3-eth-contract'; + +import { Web3Eth } from '../../../src'; + +import { + getSystemTestProvider, + createTempAccount, + closeOpenConnection, +} from '../../fixtures/system_test_utils'; +import { BasicAbi, BasicBytecode } from '../../shared_fixtures/build/Basic'; +import { toAllVariants } from '../../shared_fixtures/utils'; +import { sendFewTxes } from '../helper'; + +describe('rpc with block', () => { + let web3Eth: Web3Eth; + let clientUrl: string; + + let contract: Contract; + let deployOptions: Record; + let sendOptions: Record; + + let blockData: { + earliest: 'earliest'; + latest: 'latest'; + pending: 'pending'; + blockNumber: number | bigint; + blockHash: string; + transactionHash: string; + transactionIndex: number | bigint; + }; + let tempAcc: { address: string; privateKey: string }; + let tempAcc2: { address: string; privateKey: string }; + + beforeAll(() => { + clientUrl = getSystemTestProvider(); + web3Eth = new Web3Eth({ + provider: clientUrl, + config: { + transactionPollingTimeout: 2000, + }, + }); + + contract = new Contract(BasicAbi, undefined, { + provider: clientUrl, + }); + + deployOptions = { + data: BasicBytecode, + arguments: [10, 'string init value'], + }; + }); + beforeAll(async () => { + tempAcc = await createTempAccount(); + tempAcc2 = await createTempAccount(); + sendOptions = { from: tempAcc.address, gas: '1000000' }; + + await contract.deploy(deployOptions).send(sendOptions); + const [receipt]: TransactionReceipt[] = await sendFewTxes({ + web3Eth, + from: tempAcc.address, + to: tempAcc2.address, + value: '0x1', + times: 1, + }); + blockData = { + pending: 'pending', + latest: 'latest', + earliest: 'earliest', + blockNumber: Number(receipt.blockNumber), + blockHash: String(receipt.blockHash), + transactionHash: String(receipt.transactionHash), + transactionIndex: Number(receipt.transactionIndex), + }; + }); + afterAll(async () => { + await closeOpenConnection(web3Eth); + await closeOpenConnection(contract); + }); + + describe('methods', () => { + it.each( + toAllVariants<{ + block: 'earliest' | 'latest' | 'pending' | 'blockNumber'; + }>({ + block: ['earliest', 'latest', 'pending', 'blockNumber'], + }), + )('getUncle', async ({ block }) => { + const res = await web3Eth.getUncle(blockData[block], 0); + // eslint-disable-next-line jest/no-standalone-expect + expect(res).toBeNull(); + }); + }); +}); diff --git a/packages/web3-eth/test/integration/defaults.test.ts b/packages/web3-eth/test/integration/defaults.test.ts index 4320c977298..b750d2d9e1a 100644 --- a/packages/web3-eth/test/integration/defaults.test.ts +++ b/packages/web3-eth/test/integration/defaults.test.ts @@ -20,11 +20,7 @@ import { hexToNumber, numberToHex, DEFAULT_RETURN_FORMAT } from 'web3-utils'; import { TransactionBuilder, TransactionTypeParser, Web3Context, Web3PromiEvent } from 'web3-core'; import { Hardfork, TransactionReceipt, ValidChains, Web3BaseProvider } from 'web3-types'; import { - TransactionBlockTimeoutError, - TransactionPollingTimeoutError, - TransactionSendTimeoutError, -} from 'web3-errors'; -import { + detectTransactionType, prepareTransactionForSigning, SendTransactionEvents, transactionBuilder, @@ -36,9 +32,6 @@ import { createNewAccount, createTempAccount, getSystemTestProvider, - isIpc, - isWs, - itIf, } from '../fixtures/system_test_utils'; import { @@ -48,11 +41,8 @@ import { } from '../../src/utils'; import { BasicAbi, BasicBytecode } from '../shared_fixtures/build/Basic'; import { MsgSenderAbi, MsgSenderBytecode } from '../shared_fixtures/build/MsgSender'; -import { detectTransactionType } from '../../dist'; import { getTransactionGasPricing } from '../../src/utils/get_transaction_gas_pricing'; -import { Resolve, sendFewTxes } from './helper'; - -const MAX_32_SIGNED_INTEGER = 2147483647; +import { Resolve, sendFewTxes, sendFewTxesWithoutReceipt } from './helper'; describe('defaults', () => { let web3Eth: Web3Eth; @@ -63,16 +53,9 @@ describe('defaults', () => { let sendOptions: Record; let tempAcc: { address: string; privateKey: string }; - beforeAll(() => { + beforeEach(async () => { clientUrl = getSystemTestProvider(); web3Eth = new Web3Eth(clientUrl); - }); - - afterAll(async () => { - await closeOpenConnection(web3Eth); - await closeOpenConnection(eth2); - }); - beforeEach(async () => { tempAcc = await createTempAccount(); contract = new Contract(BasicAbi, web3Eth.getContextObject() as any); deployOptions = { @@ -82,6 +65,11 @@ describe('defaults', () => { sendOptions = { from: tempAcc.address, gas: '1000000' }; }); + afterEach(async () => { + await closeOpenConnection(web3Eth); + await closeOpenConnection(eth2); + }); + describe('defaults', () => { it('defaultAccount', async () => { const tempAcc2 = await createTempAccount(); @@ -156,7 +144,6 @@ describe('defaults', () => { tempAcc2.address.toLowerCase(), ); }); - it('handleRevert', () => { /* //TO DO: after handleRevert implementation https://github.com/ChainSafe/web3.js/issues/5069 add following tests in future release @@ -283,7 +270,6 @@ describe('defaults', () => { }); expect(eth2.transactionSendTimeout).toBe(120); }); - it('transactionBlockTimeout', () => { // default expect(web3Eth.transactionBlockTimeout).toBe(50); @@ -323,9 +309,7 @@ describe('defaults', () => { // eslint-disable-next-line jest/no-standalone-expect expect(eth2.transactionConfirmationBlocks).toBe(4); }); - - // TODO: remove itIf when finish #5144 - itIf(!isIpc)('transactionConfirmationBlocks implementation', async () => { + it('transactionConfirmationBlocks implementation', async () => { const tempAcc2 = await createTempAccount(); const waitConfirmations = 1; const eth = new Web3Eth(web3Eth.provider); @@ -365,10 +349,16 @@ describe('defaults', () => { }); await sentTx; await receiptPromise; - await sendFewTxes({ web3Eth: eth, from, to, value, times: waitConfirmations }); + await sendFewTxesWithoutReceipt({ + web3Eth: eth, + from, + to, + value, + times: waitConfirmations, + }); await confirmationPromise; + await closeOpenConnection(eth); }); - it('transactionPollingInterval and transactionPollingTimeout', () => { // default expect(web3Eth.transactionPollingInterval).toBe(1000); @@ -478,7 +468,6 @@ describe('defaults', () => { }); expect(eth2.blockHeaderTimeout).toBe(4); }); - it('enableExperimentalFeatures', () => { // default expect(web3Eth.enableExperimentalFeatures.useSubscriptionWhenCheckingBlockTimeout).toBe( @@ -503,7 +492,6 @@ describe('defaults', () => { true, ); }); - it('should fallback to polling if provider support `on` but `newBlockHeaders` does not arrive in `blockHeaderTimeout` seconds', async () => { const tempAcc2 = await createTempAccount(); @@ -574,172 +562,8 @@ describe('defaults', () => { // Ensure the promise the get the confirmations resolves with no error const status = await confirmationPromise; expect(status).toBe(BigInt(1)); + await closeOpenConnection(tempEth); }); - - it('should fail if Ethereum Node did not respond because of a high nonce', async () => { - const eth = new Web3Eth(clientUrl); - - // Make the test run faster by causing the timeout to happen after 0.2 second - eth.transactionSendTimeout = 200; - eth.transactionPollingTimeout = 200; - - const from = tempAcc.address; - const to = (await createTempAccount()).address; - const value = `0x1`; - - try { - // Setting a high `nonce` when sending a transaction, to cause the RPC call to stuck at the Node - await eth.sendTransaction({ - to, - value, - from, - // Give a high nonce so the transaction stuck forever. - // However, make this random to be able to run the test many times without receiving an error that indicate submitting the same transaction twice. - nonce: Number.MAX_SAFE_INTEGER - Math.floor(Math.random() * 100000000), - }); - expect(true).toBe(false); // the test should fail if there is no exception - } catch (error) { - // Some providers would not respond to the RPC request when sending a transaction (like Ganache v7.4.0) - if (error instanceof TransactionSendTimeoutError) { - // eslint-disable-next-line jest/no-conditional-expect - expect(error.message).toContain( - `connected Ethereum Node did not respond within ${ - eth.transactionSendTimeout / 1000 - } seconds`, - ); - } - // Some other providers would not respond when trying to get the transaction receipt (like Geth v1.10.22-unstable) - else if (error instanceof TransactionPollingTimeoutError) { - // eslint-disable-next-line jest/no-conditional-expect - expect(error.message).toContain( - `Transaction was not mined within ${ - eth.transactionPollingTimeout / 1000 - } seconds`, - ); - } else { - throw error; - } - } - }); - - it('should fail if transaction was not mined within `transactionBlockTimeout` blocks', async () => { - const eth = new Web3Eth(clientUrl); - const tempAcc1 = await createTempAccount(); - const tempAcc2 = await createTempAccount(); - - // Make the test run faster by casing the polling to start after 2 blocks - eth.transactionBlockTimeout = 2; - - // Increase other timeouts so only `transactionBlockTimeout` would be reached - eth.transactionSendTimeout = MAX_32_SIGNED_INTEGER; - eth.transactionPollingTimeout = MAX_32_SIGNED_INTEGER; - eth.blockHeaderTimeout = MAX_32_SIGNED_INTEGER / 1000; - - const from = tempAcc1.address; - const to = tempAcc2.address; - const value = `0x0`; - - // Setting a high `nonce` when sending a transaction, to cause the RPC call to stuck at the Node - const sentTx: Web3PromiEvent< - TransactionReceipt, - SendTransactionEvents - > = eth.sendTransaction({ - to, - value, - from, - // Give a high nonce so the transaction stuck forever. - // However, make this random to be able to run the test many times without receiving an error that indicate submitting the same transaction twice. - nonce: Number.MAX_SAFE_INTEGER - Math.floor(Math.random() * 100000000), - }); - - // Some providers (mostly used for development) will make blocks only when there are new transactions - // So, send 2 transactions, one after another, because in this test `transactionBlockTimeout = 2`. - // eslint-disable-next-line no-void - void sendFewTxes({ - web3Eth: eth, - from: tempAcc2.address, - to: tempAcc1.address, - times: 2, - value: '0x1', - }); - - try { - await sentTx; - throw new Error( - 'The test should fail if there is no exception when sending a transaction that could not be mined within transactionBlockTimeout', - ); - } catch (error) { - // eslint-disable-next-line jest/no-conditional-expect - expect(error).toBeInstanceOf(TransactionBlockTimeoutError); - // eslint-disable-next-line jest/no-conditional-expect - expect((error as Error).message).toMatch(/was not mined within [0-9]+ blocks/); - } - }); - - // The code of this test case is identical to the pervious one except for `eth.enableExperimentalFeatures = true` - // And this test case will be removed once https://github.com/web3/web3.js/issues/5521 is implemented. - itIf(isWs)( - 'should fail if transaction was not mined within `transactionBlockTimeout` blocks - when subscription is used', - async () => { - const eth = new Web3Eth(clientUrl); - - // using subscription to get the new blocks and fire `TransactionBlockTimeoutError` is currently supported only - // with `enableExperimentalFeatures.useSubscriptionWhenCheckingBlockTimeout` equal true. - eth.enableExperimentalFeatures.useSubscriptionWhenCheckingBlockTimeout = true; - - const tempAcc1 = await createTempAccount(); - const tempAcc2 = await createTempAccount(); - - // Make the test run faster by casing the polling to start after 2 blocks - eth.transactionBlockTimeout = 2; - - // Increase other timeouts so only `transactionBlockTimeout` would be reached - eth.transactionSendTimeout = MAX_32_SIGNED_INTEGER; - eth.transactionPollingTimeout = MAX_32_SIGNED_INTEGER; - eth.blockHeaderTimeout = MAX_32_SIGNED_INTEGER / 1000; - - const from = tempAcc1.address; - const to = tempAcc2.address; - const value = `0x0`; - - // Setting a high `nonce` when sending a transaction, to cause the RPC call to stuck at the Node - const sentTx: Web3PromiEvent< - TransactionReceipt, - SendTransactionEvents - > = eth.sendTransaction({ - to, - value, - from, - // Give a high nonce so the transaction stuck forever. - // However, make this random to be able to run the test many times without receiving an error that indicate submitting the same transaction twice. - nonce: Number.MAX_SAFE_INTEGER - Math.floor(Math.random() * 100000000), - }); - - // Some providers (mostly used for development) will make blocks only when there are new transactions - // So, send 2 transactions, one after another, because in this test `transactionBlockTimeout = 2`. - // eslint-disable-next-line no-void - void sendFewTxes({ - web3Eth: eth, - from: tempAcc2.address, - to: tempAcc1.address, - times: 2, - value: '0x1', - }); - - try { - await sentTx; - throw new Error( - 'The test should fail if there is no exception when sending a transaction that could not be mined within transactionBlockTimeout', - ); - } catch (error) { - // eslint-disable-next-line jest/no-conditional-expect, jest/no-standalone-expect - expect(error).toBeInstanceOf(TransactionBlockTimeoutError); - // eslint-disable-next-line jest/no-conditional-expect, jest/no-standalone-expect - expect((error as Error).message).toMatch(/was not mined within [0-9]+ blocks/); - } - }, - ); - it('maxListenersWarningThreshold test default config', () => { // default expect(web3Eth.maxListenersWarningThreshold).toBe(100); diff --git a/packages/web3-eth/test/integration/defaults.transactionBlockTimeout.test.ts b/packages/web3-eth/test/integration/defaults.transactionBlockTimeout.test.ts new file mode 100644 index 00000000000..6649c4cb85b --- /dev/null +++ b/packages/web3-eth/test/integration/defaults.transactionBlockTimeout.test.ts @@ -0,0 +1,173 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ +// eslint-disable-next-line import/no-extraneous-dependencies +import { DEFAULT_RETURN_FORMAT } from 'web3-utils'; +import { Web3PromiEvent } from 'web3-core'; +import { TransactionReceipt } from 'web3-types'; +import { TransactionBlockTimeoutError } from 'web3-errors'; +import { SendTransactionEvents, Web3Eth } from '../../src'; + +import { + closeOpenConnection, + createTempAccount, + getSystemTestProvider, + isSocket, + itIf, + waitForOpenConnection, +} from '../fixtures/system_test_utils'; + +import { sendFewTxesWithoutReceipt } from './helper'; + +const MAX_32_SIGNED_INTEGER = 2147483647; +/* eslint-disable jest/no-standalone-expect */ +describe('defaults', () => { + let web3Eth: Web3Eth; + let eth2: Web3Eth; + let clientUrl: string; + + beforeEach(() => { + clientUrl = getSystemTestProvider(); + web3Eth = new Web3Eth(clientUrl); + }); + + afterEach(async () => { + await closeOpenConnection(web3Eth); + await closeOpenConnection(eth2); + }); + + describe('defaults', () => { + it('should fail if transaction was not mined within `transactionBlockTimeout` blocks', async () => { + const eth = new Web3Eth(clientUrl); + const tempAcc1 = await createTempAccount(); + const tempAcc2 = await createTempAccount(); + + // Make the test run faster by casing the polling to start after 2 blocks + eth.transactionBlockTimeout = 2; + + // Increase other timeouts so only `transactionBlockTimeout` would be reached + eth.transactionSendTimeout = MAX_32_SIGNED_INTEGER; + eth.transactionPollingTimeout = MAX_32_SIGNED_INTEGER; + eth.blockHeaderTimeout = MAX_32_SIGNED_INTEGER / 1000; + + const from = tempAcc1.address; + const to = tempAcc2.address; + const value = `0x0`; + + // Setting a high `nonce` when sending a transaction, to cause the RPC call to stuck at the Node + const sentTx: Web3PromiEvent< + TransactionReceipt, + SendTransactionEvents + > = eth.sendTransaction({ + to, + value, + from, + // Give a high nonce so the transaction stuck forever. + // However, make this random to be able to run the test many times without receiving an error that indicate submitting the same transaction twice. + nonce: Number.MAX_SAFE_INTEGER - Math.floor(Math.random() * 100000000), + }); + + // Some providers (mostly used for development) will make blocks only when there are new transactions + // So, send 2 transactions, one after another, because in this test `transactionBlockTimeout = 2`. + // eslint-disable-next-line no-void + void sendFewTxesWithoutReceipt({ + web3Eth: eth, + from: tempAcc2.address, + to: tempAcc1.address, + times: 2, + value: '0x1', + }); + + try { + await sentTx; + throw new Error( + 'The test should fail if there is no exception when sending a transaction that could not be mined within transactionBlockTimeout', + ); + } catch (error) { + // eslint-disable-next-line jest/no-conditional-expect + expect(error).toBeInstanceOf(TransactionBlockTimeoutError); + // eslint-disable-next-line jest/no-conditional-expect + expect((error as Error).message).toMatch(/was not mined within [0-9]+ blocks/); + } + await closeOpenConnection(eth); + }); + + // The code of this test case is identical to the pervious one except for `eth.enableExperimentalFeatures = true` + // And this test case will be removed once https://github.com/web3/web3.js/issues/5521 is implemented. + itIf(isSocket)( + 'should fail if transaction was not mined within `transactionBlockTimeout` blocks - when subscription is used', + async () => { + const eth = new Web3Eth(clientUrl); + await waitForOpenConnection(eth); + // using subscription to get the new blocks and fire `TransactionBlockTimeoutError` is currently supported only + // with `enableExperimentalFeatures.useSubscriptionWhenCheckingBlockTimeout` equal true. + eth.enableExperimentalFeatures.useSubscriptionWhenCheckingBlockTimeout = true; + + const tempAcc1 = await createTempAccount(); + const tempAcc2 = await createTempAccount(); + + // Make the test run faster by casing the polling to start after 2 blocks + eth.transactionBlockTimeout = 2; + + // Increase other timeouts so only `transactionBlockTimeout` would be reached + eth.transactionSendTimeout = MAX_32_SIGNED_INTEGER; + eth.transactionPollingTimeout = MAX_32_SIGNED_INTEGER; + eth.blockHeaderTimeout = MAX_32_SIGNED_INTEGER / 1000; + + const from = tempAcc1.address; + const to = tempAcc2.address; + const value = `0x0`; + + // Setting a high `nonce` when sending a transaction, to cause the RPC call to stuck at the Node + const sentTx: Web3PromiEvent< + TransactionReceipt, + SendTransactionEvents + > = eth.sendTransaction({ + to, + value, + from, + // Give a high nonce so the transaction stuck forever. + // However, make this random to be able to run the test many times without receiving an error that indicate submitting the same transaction twice. + nonce: Number.MAX_SAFE_INTEGER - Math.floor(Math.random() * 100000000), + }); + + // Some providers (mostly used for development) will make blocks only when there are new transactions + // So, send 2 transactions, one after another, because in this test `transactionBlockTimeout = 2`. + // eslint-disable-next-line no-void + void sendFewTxesWithoutReceipt({ + web3Eth: eth, + from: tempAcc2.address, + to: tempAcc1.address, + times: 2, + value: '0x1', + }); + + try { + await sentTx; + throw new Error( + 'The test should fail if there is no exception when sending a transaction that could not be mined within transactionBlockTimeout', + ); + } catch (error) { + // eslint-disable-next-line jest/no-conditional-expect, jest/no-standalone-expect + expect(error).toBeInstanceOf(TransactionBlockTimeoutError); + // eslint-disable-next-line jest/no-conditional-expect, jest/no-standalone-expect + expect((error as Error).message).toMatch(/was not mined within [0-9]+ blocks/); + } + await closeOpenConnection(eth); + }, + ); + }); +}); diff --git a/packages/web3-eth/test/integration/eth.test.ts b/packages/web3-eth/test/integration/eth.test.ts index 7e9fc400672..f047ed5829c 100644 --- a/packages/web3-eth/test/integration/eth.test.ts +++ b/packages/web3-eth/test/integration/eth.test.ts @@ -42,7 +42,7 @@ describe('eth', () => { let sendOptions: Record; let tempAcc: { address: string; privateKey: string }; - beforeAll(async () => { + beforeAll(() => { clientUrl = getSystemTestProvider(); web3Eth = new Web3Eth(clientUrl); contract = new Contract(BasicAbi, { diff --git a/packages/web3-eth/test/integration/helper.ts b/packages/web3-eth/test/integration/helper.ts index b0c9877504f..72754f7ca6c 100644 --- a/packages/web3-eth/test/integration/helper.ts +++ b/packages/web3-eth/test/integration/helper.ts @@ -27,6 +27,7 @@ type SendFewTxParams = { from: string; value: string; times?: number; + waitReceipt?: boolean; }; export type Resolve = (value?: TransactionReceipt) => void; export const sendFewTxes = async ({ @@ -38,10 +39,12 @@ export const sendFewTxes = async ({ }: SendFewTxParams): Promise => { const res: TransactionReceipt[] = []; for (let i = 0; i < times; i += 1) { + // @TODO: Investigate why we need timeout here #5730 // eslint-disable-next-line no-await-in-loop await new Promise(resolve => { setTimeout(resolve, 500); }); + const tx: Web3PromiEvent< TransactionReceipt, SendTransactionEvents @@ -70,6 +73,38 @@ export const sendFewTxes = async ({ return res; }; +export const sendFewTxesWithoutReceipt = async ({ + web3Eth, + to, + value, + from, + times = 3, +}: SendFewTxParams): Promise< + Web3PromiEvent>[] +> => { + const res: Web3PromiEvent< + TransactionReceipt, + SendTransactionEvents + >[] = []; + for (let i = 0; i < times; i += 1) { + // @TODO: Investigate why we need timeout here #5730 + // eslint-disable-next-line no-await-in-loop + await new Promise(resolve => { + setTimeout(resolve, 500); + }); + + res.push( + web3Eth.sendTransaction({ + to, + value, + from, + }), + ); + } + + return res; +}; + const regexHex20 = /0[xX][0-9a-fA-F]{40}/i; const regexHex32 = /0[xX][0-9a-fA-F]{64}/i; diff --git a/packages/web3-eth/test/integration/nonce.test.ts b/packages/web3-eth/test/integration/nonce.test.ts new file mode 100644 index 00000000000..6d94717bda3 --- /dev/null +++ b/packages/web3-eth/test/integration/nonce.test.ts @@ -0,0 +1,94 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ +// eslint-disable-next-line import/no-extraneous-dependencies +import { TransactionPollingTimeoutError, TransactionSendTimeoutError } from 'web3-errors'; +import { Web3Eth } from '../../src'; + +import { + closeOpenConnection, + createTempAccount, + getSystemTestProvider, +} from '../fixtures/system_test_utils'; + +describe('defaults', () => { + let web3Eth: Web3Eth; + let eth2: Web3Eth; + let clientUrl: string; + let tempAcc: { address: string; privateKey: string }; + + beforeEach(() => { + clientUrl = getSystemTestProvider(); + web3Eth = new Web3Eth(clientUrl); + }); + + afterEach(async () => { + await closeOpenConnection(web3Eth); + await closeOpenConnection(eth2); + }); + beforeEach(async () => { + tempAcc = await createTempAccount(); + }); + + describe('defaults', () => { + it('should fail if Ethereum Node did not respond because of a high nonce', async () => { + const eth = new Web3Eth(clientUrl); + + // Make the test run faster by causing the timeout to happen after 0.2 second + eth.transactionSendTimeout = 200; + eth.transactionPollingTimeout = 200; + + const from = tempAcc.address; + const to = (await createTempAccount()).address; + const value = `0x1`; + + try { + // Setting a high `nonce` when sending a transaction, to cause the RPC call to stuck at the Node + await eth.sendTransaction({ + to, + value, + from, + // Give a high nonce so the transaction stuck forever. + // However, make this random to be able to run the test many times without receiving an error that indicate submitting the same transaction twice. + nonce: Number.MAX_SAFE_INTEGER - Math.floor(Math.random() * 100000000), + }); + expect(true).toBe(false); // the test should fail if there is no exception + } catch (error) { + // Some providers would not respond to the RPC request when sending a transaction (like Ganache v7.4.0) + if (error instanceof TransactionSendTimeoutError) { + // eslint-disable-next-line jest/no-conditional-expect + expect(error.message).toContain( + `connected Ethereum Node did not respond within ${ + eth.transactionSendTimeout / 1000 + } seconds`, + ); + } + // Some other providers would not respond when trying to get the transaction receipt (like Geth v1.10.22-unstable) + else if (error instanceof TransactionPollingTimeoutError) { + // eslint-disable-next-line jest/no-conditional-expect + expect(error.message).toContain( + `Transaction was not mined within ${ + eth.transactionPollingTimeout / 1000 + } seconds`, + ); + } else { + throw error; + } + } + await closeOpenConnection(eth); + }); + }); +}); diff --git a/packages/web3-eth/test/integration/rpc.test.ts b/packages/web3-eth/test/integration/rpc.test.ts index 572b07dc14a..4f3cf431354 100644 --- a/packages/web3-eth/test/integration/rpc.test.ts +++ b/packages/web3-eth/test/integration/rpc.test.ts @@ -23,7 +23,6 @@ import { hexToNumber, hexToString, numberToHex, FMT_BYTES, FMT_NUMBER } from 'we import { AbiEventFragment } from 'web3-eth-abi'; import { getStorageSlotNumForLongString } from 'web3-utils/src'; // eslint-disable-next-line import/no-extraneous-dependencies -import IpcProvider from 'web3-providers-ipc'; import { Web3Eth } from '../../src'; import { @@ -32,7 +31,6 @@ import { getSystemTestProvider, createNewAccount, itIf, - isIpc, createTempAccount, } from '../fixtures/system_test_utils'; import { BasicAbi, BasicBytecode } from '../shared_fixtures/build/Basic'; @@ -61,7 +59,6 @@ describe('rpc', () => { transactionPollingTimeout: 2000, }, }); - contract = new Contract(BasicAbi, undefined, { provider: clientUrl, }); @@ -75,10 +72,6 @@ describe('rpc', () => { sendOptions = { from: tempAcc.address, gas: '1000000' }; contractDeployed = await contract.deploy(deployOptions).send(sendOptions); - if (isIpc) { - await (contract.provider as IpcProvider).waitForConnection(); - await (web3Eth.provider as IpcProvider).waitForConnection(); - } }); afterAll(async () => { diff --git a/packages/web3-eth/test/integration/setup.js b/packages/web3-eth/test/integration/setup.js index 5be1bccf7cc..976511e0e5f 100644 --- a/packages/web3-eth/test/integration/setup.js +++ b/packages/web3-eth/test/integration/setup.js @@ -19,6 +19,6 @@ along with web3.js. If not, see . // eslint-disable-next-line @typescript-eslint/no-require-imports require('../config/setup'); -const jestTimeout = 15000; +const jestTimeout = process.env.WEB3_SYSTEM_TEST_PROVIDER.includes('ipc') ? 30000 : 15000; jest.setTimeout(jestTimeout); diff --git a/packages/web3-eth/test/integration/subscribe.test.ts b/packages/web3-eth/test/integration/subscribe.test.ts index 533a4670657..a41ba49b147 100644 --- a/packages/web3-eth/test/integration/subscribe.test.ts +++ b/packages/web3-eth/test/integration/subscribe.test.ts @@ -17,30 +17,35 @@ along with web3.js. If not, see . import WebSocketProvider from 'web3-providers-ws'; import { Web3BaseProvider } from 'web3-types'; /* eslint-disable import/no-named-as-default */ -import Web3Eth from '../../src/index'; -import { +// eslint-disable-next-line import/no-extraneous-dependencies +import IpcProvider from 'web3-providers-ipc'; +import Web3Eth, { NewHeadsSubscription, SyncingSubscription, NewPendingTransactionsSubscription, LogsSubscription, -} from '../../src'; +} from '../../src/index'; import { getSystemTestProvider, describeIf, + isSocket, isWs, createTempAccount, + closeOpenConnection, } from '../fixtures/system_test_utils'; -describeIf(isWs)('subscribe', () => { +describeIf(isSocket)('subscribe', () => { let web3Eth: Web3Eth; - let provider: WebSocketProvider; + let provider: WebSocketProvider | IpcProvider; - beforeAll(async () => { - provider = new WebSocketProvider(getSystemTestProvider()); + beforeAll(() => { + provider = isWs + ? new WebSocketProvider(getSystemTestProvider()) + : new IpcProvider(getSystemTestProvider()); }); - afterAll(() => { - provider.disconnect(); + afterAll(async () => { + await closeOpenConnection(web3Eth); }); afterEach(async () => { diff --git a/packages/web3-eth/test/integration/subscription_heads.test.ts b/packages/web3-eth/test/integration/subscription_heads.test.ts index aa499962fc7..f3d03269ca8 100644 --- a/packages/web3-eth/test/integration/subscription_heads.test.ts +++ b/packages/web3-eth/test/integration/subscription_heads.test.ts @@ -14,48 +14,39 @@ GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ -import WebSocketProvider from 'web3-providers-ws'; -import { BlockHeaderOutput, Web3BaseProvider } from 'web3-types'; -import { Web3Eth } from '../../src'; -import { sendFewTxes, Resolve } from './helper'; -import { NewHeadsSubscription } from '../../src/web3_subscriptions'; +import { BlockHeaderOutput } from 'web3-types'; +import { Web3Eth, NewHeadsSubscription } from '../../src'; +import { Resolve, sendFewTxesWithoutReceipt } from './helper'; import { + closeOpenConnection, createTempAccount, describeIf, getSystemTestProvider, - isWs, + isSocket, + waitForOpenConnection, } from '../fixtures/system_test_utils'; -const checkTxCount = 5; -type SubName = 'newHeads' | 'newBlockHeaders'; -const subNames: Array = ['newHeads', 'newBlockHeaders']; +const checkTxCount = 3; -describeIf(isWs)('subscription', () => { - let web3Eth: Web3Eth; +describeIf(isSocket)('subscription', () => { let clientUrl: string; - let providerWs: WebSocketProvider; - let tempAcc: { address: string; privateKey: string }; let tempAcc2: { address: string; privateKey: string }; beforeEach(async () => { - tempAcc = await createTempAccount(); tempAcc2 = await createTempAccount(); }); beforeAll(() => { clientUrl = getSystemTestProvider(); - providerWs = new WebSocketProvider(clientUrl); - web3Eth = new Web3Eth(providerWs as Web3BaseProvider); - }); - afterAll(() => { - providerWs.disconnect(); }); describe('heads', () => { - it.each(subNames)(`wait for ${checkTxCount} newHeads`, async (subName: SubName) => { - const sub: NewHeadsSubscription = await web3Eth.subscribe(subName); - const from = tempAcc.address; + it(`wait for ${checkTxCount} newHeads`, async () => { + const web3Eth = new Web3Eth(clientUrl); + const sub: NewHeadsSubscription = await web3Eth.subscribe('newHeads'); + const tempAccForEachTest = await createTempAccount(); + const from = tempAccForEachTest.address; const to = tempAcc2.address; const value = `0x1`; - + await waitForOpenConnection(web3Eth); let times = 0; const pr = new Promise((resolve: Resolve, reject) => { sub.on('data', (data: BlockHeaderOutput) => { @@ -64,9 +55,9 @@ describeIf(isWs)('subscription', () => { } expect(times).toBeGreaterThanOrEqual(times); if (times >= checkTxCount) { - sub.off('data', () => { - // no need to do anything - }); + // sub.off('data', () => { + // no need to do anything + // }); resolve(); } }); @@ -74,16 +65,26 @@ describeIf(isWs)('subscription', () => { reject(error); }); }); + await sendFewTxesWithoutReceipt({ + web3Eth, + from, + to, + value, + times: checkTxCount, + }); - await sendFewTxes({ web3Eth, from, to, value, times: checkTxCount }); await pr; await web3Eth.subscriptionManager?.removeSubscription(sub); + await closeOpenConnection(web3Eth); }); - it.each(subNames)(`clear`, async (subName: SubName) => { - const sub: NewHeadsSubscription = await web3Eth.subscribe(subName); + it(`clear`, async () => { + const web3Eth = new Web3Eth(clientUrl); + await waitForOpenConnection(web3Eth); + const sub: NewHeadsSubscription = await web3Eth.subscribe('newHeads'); expect(sub.id).toBeDefined(); await web3Eth.subscriptionManager?.removeSubscription(sub); expect(sub.id).toBeUndefined(); + await closeOpenConnection(web3Eth); }); }); }); diff --git a/packages/web3-eth/test/integration/subscription_logs.test.ts b/packages/web3-eth/test/integration/subscription_logs.test.ts index 4f2fd5d8367..78a75f4d838 100644 --- a/packages/web3-eth/test/integration/subscription_logs.test.ts +++ b/packages/web3-eth/test/integration/subscription_logs.test.ts @@ -20,6 +20,8 @@ import { Contract, decodeEventABI } from 'web3-eth-contract'; // eslint-disable-next-line import/no-extraneous-dependencies import { AbiEventFragment } from 'web3-eth-abi'; import { Web3BaseProvider } from 'web3-types'; +// eslint-disable-next-line import/no-extraneous-dependencies +import IpcProvider from 'web3-providers-ipc'; import { Web3Eth } from '../../src'; import { BasicAbi, BasicBytecode } from '../shared_fixtures/build/Basic'; import { eventAbi, Resolve } from './helper'; @@ -28,7 +30,9 @@ import { describeIf, getSystemTestProvider, isWs, + isSocket, createTempAccount, + closeOpenConnection, } from '../fixtures/system_test_utils'; const checkEventCount = 2; @@ -49,10 +53,10 @@ const makeFewTxToContract = async ({ prs.push(await contract.methods?.firesStringEvent(testDataString).send(sendOptions)); } }; -describeIf(isWs)('subscription', () => { +describeIf(isSocket)('subscription', () => { let clientUrl: string; let web3Eth: Web3Eth; - let providerWs: WebSocketProvider; + let provider: WebSocketProvider | IpcProvider; let contract: Contract; let contractDeployed: Contract; let deployOptions: Record; @@ -65,19 +69,19 @@ describeIf(isWs)('subscription', () => { }); beforeAll(() => { clientUrl = getSystemTestProvider(); - providerWs = new WebSocketProvider(clientUrl); + provider = isWs ? new WebSocketProvider(clientUrl) : new IpcProvider(clientUrl); contract = new Contract(BasicAbi, undefined, { provider: clientUrl, }); }); - afterAll(() => { - providerWs.disconnect(); - (contract.provider as WebSocketProvider).disconnect(); + afterAll(async () => { + provider.disconnect(); + await closeOpenConnection(web3Eth); }); describe('logs', () => { it(`wait for ${checkEventCount} logs`, async () => { - web3Eth = new Web3Eth(providerWs as Web3BaseProvider); + web3Eth = new Web3Eth(provider as Web3BaseProvider); const from = tempAcc.address; deployOptions = { data: BasicBytecode, diff --git a/packages/web3-eth/test/integration/subscription_logs_block.test.ts b/packages/web3-eth/test/integration/subscription_logs_block.test.ts index 585ec625030..82c8a40e079 100644 --- a/packages/web3-eth/test/integration/subscription_logs_block.test.ts +++ b/packages/web3-eth/test/integration/subscription_logs_block.test.ts @@ -21,6 +21,8 @@ import { Contract, decodeEventABI } from 'web3-eth-contract'; import { AbiEventFragment } from 'web3-eth-abi'; import { Web3BaseProvider } from 'web3-types'; import { numberToHex } from 'web3-utils'; +// eslint-disable-next-line import/no-extraneous-dependencies +import IpcProvider from 'web3-providers-ipc'; import { Web3Eth } from '../../src'; import { BasicAbi, BasicBytecode } from '../shared_fixtures/build/Basic'; import { eventAbi, Resolve } from './helper'; @@ -30,6 +32,8 @@ import { describeIf, getSystemTestProvider, isWs, + isSocket, + closeOpenConnection, } from '../fixtures/system_test_utils'; const checkEventCount = 2; @@ -50,22 +54,22 @@ const makeFewTxToContract = async ({ prs.push(await contract.methods?.firesStringEvent(testDataString).send(sendOptions)); } }; -describeIf(isWs)('subscription', () => { +describeIf(isSocket)('subscription', () => { let clientUrl: string; - let providerWs: WebSocketProvider; + let providerWs: WebSocketProvider | IpcProvider; let contract: Contract; const testDataString = 'someTestString'; beforeAll(() => { clientUrl = getSystemTestProvider(); - providerWs = new WebSocketProvider(clientUrl); + providerWs = isWs ? new WebSocketProvider(clientUrl) : new IpcProvider(clientUrl); contract = new Contract(BasicAbi, undefined, { provider: clientUrl, }); }); - afterAll(() => { + afterAll(async () => { providerWs.disconnect(); - (contract.provider as WebSocketProvider).disconnect(); + await closeOpenConnection(contract); }); describe('logs', () => { diff --git a/packages/web3-eth/test/integration/subscription_logs_clear.test.ts b/packages/web3-eth/test/integration/subscription_logs_clear.test.ts index 991f28def61..262889a339f 100644 --- a/packages/web3-eth/test/integration/subscription_logs_clear.test.ts +++ b/packages/web3-eth/test/integration/subscription_logs_clear.test.ts @@ -14,29 +14,28 @@ GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ -import WebSocketProvider from 'web3-providers-ws'; -// eslint-disable-next-line import/no-extraneous-dependencies -// eslint-disable-next-line import/no-extraneous-dependencies -import { Web3BaseProvider } from 'web3-types'; import { Web3Eth } from '../../src'; +import { + closeOpenConnection, + describeIf, + getSystemTestProvider, + isSocket, +} from '../fixtures/system_test_utils'; import { LogsSubscription } from '../../src/web3_subscriptions'; -import { describeIf, getSystemTestProvider, isWs } from '../fixtures/system_test_utils'; +// eslint-disable-next-line import/no-extraneous-dependencies +// eslint-disable-next-line import/no-extraneous-dependencies -describeIf(isWs)('subscription', () => { - let clientUrl: string; +describeIf(isSocket)('subscription', () => { let web3Eth: Web3Eth; - let providerWs: WebSocketProvider; - beforeAll(async () => { - clientUrl = getSystemTestProvider(); - providerWs = new WebSocketProvider(clientUrl); + beforeAll(() => { + web3Eth = new Web3Eth(getSystemTestProvider()); }); - afterAll(() => { - providerWs.disconnect(); + afterAll(async () => { + await closeOpenConnection(web3Eth); }); describe('logs', () => { it(`clear`, async () => { - web3Eth = new Web3Eth(providerWs as Web3BaseProvider); const sub: LogsSubscription = await web3Eth.subscribe('logs'); expect(sub.id).toBeDefined(); await web3Eth.clearSubscriptions(); diff --git a/packages/web3-eth/test/integration/subscription_new_pending_tx.test.ts b/packages/web3-eth/test/integration/subscription_new_pending_tx.test.ts index 55bc55d4c45..fcf7d9ea138 100644 --- a/packages/web3-eth/test/integration/subscription_new_pending_tx.test.ts +++ b/packages/web3-eth/test/integration/subscription_new_pending_tx.test.ts @@ -14,43 +14,33 @@ GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ -import WebSocketProvider from 'web3-providers-ws'; -import { Web3BaseProvider } from 'web3-types'; import { Web3Eth, NewPendingTransactionsSubscription } from '../../src'; import { sendFewTxes } from './helper'; import { + closeOpenConnection, createTempAccount, describeIf, getSystemTestProvider, - isWs, + isIpc, + isSocket, + waitForOpenConnection, } from '../fixtures/system_test_utils'; const checkTxCount = 2; -type SubName = 'pendingTransactions' | 'newPendingTransactions'; -const subNames: SubName[] = ['pendingTransactions', 'newPendingTransactions']; - -describeIf(isWs)('subscription', () => { - let web3Eth: Web3Eth; - let providerWs: WebSocketProvider; - let clientUrl: string; - beforeAll(() => { - clientUrl = getSystemTestProvider(); - providerWs = new WebSocketProvider(clientUrl); - }); - afterAll(() => { - web3Eth.subscriptionManager?.clear(); - providerWs.disconnect(); - }); - +describeIf(isSocket && !isIpc)('subscription', () => { describe('new pending transaction', () => { - it.each(subNames)(`wait ${checkTxCount} transaction - %s`, async subName => { + it(`wait ${checkTxCount} transaction - %s`, async () => { + const web3Eth = new Web3Eth(getSystemTestProvider()); const [tempAcc, tempAcc2] = await Promise.all([ createTempAccount(), createTempAccount(), ]); - web3Eth = new Web3Eth(providerWs as Web3BaseProvider); - const sub: NewPendingTransactionsSubscription = await web3Eth.subscribe(subName); + await waitForOpenConnection(web3Eth); + + const sub: NewPendingTransactionsSubscription = await web3Eth.subscribe( + 'pendingTransactions', + ); const from = tempAcc.address; const to = tempAcc2.address; const value = `0x1`; @@ -89,7 +79,7 @@ describeIf(isWs)('subscription', () => { from, to, value, - times: checkTxCount, + times: isIpc ? checkTxCount * 3 : checkTxCount, }) ).map(r => String(r?.transactionHash)); if (receipts.length > 0 && waitList.length > 0) { @@ -113,13 +103,18 @@ describeIf(isWs)('subscription', () => { for (const hash of txHashes) { expect(receipts).toContain(hash); } + await closeOpenConnection(web3Eth); }); - it.each(subNames)(`clear`, async (subName: SubName) => { - web3Eth = new Web3Eth(providerWs as Web3BaseProvider); - const sub: NewPendingTransactionsSubscription = await web3Eth.subscribe(subName); + it(`clear`, async () => { + const web3Eth = new Web3Eth(getSystemTestProvider()); + await waitForOpenConnection(web3Eth); + const sub: NewPendingTransactionsSubscription = await web3Eth.subscribe( + 'pendingTransactions', + ); expect(sub.id).toBeDefined(); await web3Eth.subscriptionManager?.removeSubscription(sub); expect(sub.id).toBeUndefined(); + await closeOpenConnection(web3Eth); }); }); }); diff --git a/packages/web3-eth/test/integration/unsubscribe.test.ts b/packages/web3-eth/test/integration/unsubscribe.test.ts index 552d0f0be47..6f60bf16c56 100644 --- a/packages/web3-eth/test/integration/unsubscribe.test.ts +++ b/packages/web3-eth/test/integration/unsubscribe.test.ts @@ -16,19 +16,28 @@ along with web3.js. If not, see . */ import WebSocketProvider from 'web3-providers-ws/dist'; import { Web3BaseProvider } from 'web3-types'; -/* eslint-disable import/no-named-as-default */ +/* eslint-disable import/no-extraneous-dependencies */ +import IpcProvider from 'web3-providers-ipc'; import Web3Eth from '../../src/index'; import { NewHeadsSubscription, SyncingSubscription } from '../../src/web3_subscriptions'; -import { getSystemTestProvider, describeIf, isWs } from '../fixtures/system_test_utils'; +import { + getSystemTestProvider, + describeIf, + isWs, + isSocket, + closeOpenConnection, +} from '../fixtures/system_test_utils'; -describeIf(isWs)('unsubscribe', () => { +describeIf(isSocket)('unsubscribe', () => { let web3Eth: Web3Eth; - let provider: WebSocketProvider; + let provider: WebSocketProvider | IpcProvider; beforeAll(() => { - provider = new WebSocketProvider(getSystemTestProvider()); + provider = isWs + ? new WebSocketProvider(getSystemTestProvider()) + : new IpcProvider(getSystemTestProvider()); }); - afterAll(() => { - provider.disconnect(); + afterAll(async () => { + await closeOpenConnection(web3Eth); }); describe('unsubscribe from', () => { diff --git a/packages/web3-eth/test/integration/watch_transaction.test.ts b/packages/web3-eth/test/integration/watch_transaction.test.ts index 3707a236bfb..e50a15f2dce 100644 --- a/packages/web3-eth/test/integration/watch_transaction.test.ts +++ b/packages/web3-eth/test/integration/watch_transaction.test.ts @@ -14,44 +14,33 @@ GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ -import WebSocketProvider from 'web3-providers-ws'; import { DEFAULT_RETURN_FORMAT } from 'web3-utils'; -import { Web3BaseProvider, TransactionReceipt } from 'web3-types'; +import { TransactionReceipt } from 'web3-types'; import { Web3PromiEvent } from 'web3-core'; import { Web3Eth, SendTransactionEvents } from '../../src'; -import { sendFewTxes } from './helper'; +import { sendFewTxesWithoutReceipt } from './helper'; import { getSystemTestProvider, describeIf, - isWs, createTempAccount, + closeOpenConnection, + isSocket, + waitForOpenConnection, // eslint-disable-next-line import/no-relative-packages } from '../fixtures/system_test_utils'; -const waitConfirmations = 5; +const waitConfirmations = 2; type Resolve = (value?: unknown) => void; -describeIf(isWs)('watch subscription transaction', () => { - let web3Eth: Web3Eth; - let providerWs: WebSocketProvider; - let clientUrl: string; - - beforeAll(() => { - clientUrl = getSystemTestProvider(); - - providerWs = new WebSocketProvider(clientUrl); - }); - afterAll(() => { - providerWs.disconnect(); - }); - +describeIf(isSocket)('watch subscription transaction', () => { describe('wait for confirmation subscription', () => { it('subscription to heads', async () => { + const web3Eth = new Web3Eth(getSystemTestProvider()); + await waitForOpenConnection(web3Eth); const tempAccount = await createTempAccount(); const tempAccount2 = await createTempAccount(); - web3Eth = new Web3Eth(providerWs as Web3BaseProvider); web3Eth.setConfig({ transactionConfirmationBlocks: waitConfirmations }); @@ -88,8 +77,15 @@ describeIf(isWs)('watch subscription transaction', () => { }); }); await receiptPromise; - await sendFewTxes({ web3Eth, from, to, value, times: waitConfirmations }); + await sendFewTxesWithoutReceipt({ + web3Eth, + from, + to, + value, + times: waitConfirmations, + }); await confirmationPromise; + await closeOpenConnection(web3Eth); }); }); }); diff --git a/packages/web3-eth/test/integration/watch_transaction_polling.test.ts b/packages/web3-eth/test/integration/watch_transaction_polling.test.ts index f61c41fe4e7..ef67ce5bc70 100644 --- a/packages/web3-eth/test/integration/watch_transaction_polling.test.ts +++ b/packages/web3-eth/test/integration/watch_transaction_polling.test.ts @@ -25,15 +25,13 @@ import { describeIf, getSystemTestProvider, isHttp, - isIpc, } from '../fixtures/system_test_utils'; -const waitConfirmations = 5; +const waitConfirmations = 3; type Resolve = (value?: unknown) => void; -describeIf(isHttp || isIpc)('watch polling transaction', () => { - let web3Eth: Web3Eth; +describeIf(isHttp)('watch polling transaction', () => { let clientUrl: string; let tempAcc: { address: string; privateKey: string }; let tempAcc2: { address: string; privateKey: string }; @@ -42,16 +40,13 @@ describeIf(isHttp || isIpc)('watch polling transaction', () => { tempAcc = await createTempAccount(); tempAcc2 = await createTempAccount(); }); - beforeAll(async () => { + beforeAll(() => { clientUrl = getSystemTestProvider(); }); - afterAll(async () => { - await closeOpenConnection(web3Eth); - }); describe('wait for confirmation polling', () => { it('polling', async () => { - web3Eth = new Web3Eth(clientUrl); + const web3Eth = new Web3Eth(clientUrl); web3Eth.setConfig({ transactionConfirmationBlocks: waitConfirmations }); const from = tempAcc.address; @@ -97,6 +92,7 @@ describeIf(isHttp || isIpc)('watch polling transaction', () => { await sentTx; await confirmationPromise; sentTx.removeAllListeners(); + await closeOpenConnection(web3Eth); }); }); }); diff --git a/packages/web3-eth/test/integration/web3_eth/getFeeHistory.test.ts b/packages/web3-eth/test/integration/web3_eth/getFeeHistory.test.ts index 56571fb7659..1d664d8555f 100644 --- a/packages/web3-eth/test/integration/web3_eth/getFeeHistory.test.ts +++ b/packages/web3-eth/test/integration/web3_eth/getFeeHistory.test.ts @@ -29,7 +29,7 @@ describeIf(getSystemTestBackend().includes('geth'))('Web3Eth.getFeeHistory', () let web3Eth: Web3Eth; let systemProvider: string; - beforeAll(async () => { + beforeAll(() => { systemProvider = getSystemTestProvider(); web3Eth = new Web3Eth(systemProvider); }); diff --git a/packages/web3-providers-ipc/CHANGELOG.md b/packages/web3-providers-ipc/CHANGELOG.md index bd093a66550..0cf612b53d3 100644 --- a/packages/web3-providers-ipc/CHANGELOG.md +++ b/packages/web3-providers-ipc/CHANGELOG.md @@ -54,3 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated dependencies (#5725) ## [Unreleased] + +### Changed + +- Refactor to use common SocketProvider class (#5683) diff --git a/packages/web3-providers-ipc/src/index.ts b/packages/web3-providers-ipc/src/index.ts index c278d670232..1d80046dc58 100644 --- a/packages/web3-providers-ipc/src/index.ts +++ b/packages/web3-providers-ipc/src/index.ts @@ -15,265 +15,156 @@ You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ -import { EventEmitter } from 'events'; -import { existsSync } from 'fs'; import { Socket } from 'net'; +import { InvalidConnectionError, ConnectionNotOpenError, InvalidClientError } from 'web3-errors'; +import { SocketProvider } from 'web3-utils'; import { EthExecutionAPI, JsonRpcId, - JsonRpcNotification, - JsonRpcResponseWithResult, - JsonRpcResult, + SocketRequestItem, Web3APIMethod, Web3APIPayload, - Web3APIReturnType, Web3APISpec, - Web3BaseProvider, - Web3ProviderEventCallback, Web3ProviderStatus, } from 'web3-types'; -import { - ConnectionNotOpenError, - InvalidClientError, - InvalidConnectionError, - ResponseError, -} from 'web3-errors'; -import { isNullish, Web3DeferredPromise, jsonRpc, ChunkResponseParser } from 'web3-utils'; - -type WaitOptions = { - timeOutTime: number; - maxNumberOfAttempts: number; -}; -export default class IpcProvider< - API extends Web3APISpec = EthExecutionAPI, -> extends Web3BaseProvider { - private readonly _emitter: EventEmitter = new EventEmitter(); - private readonly _socketPath: string; - private readonly _socket: Socket; - private readonly chunkResponseParser: ChunkResponseParser; - private waitOptions: WaitOptions; - private _connectionStatus: Web3ProviderStatus; +import { existsSync } from 'fs'; - private readonly _requestQueue: Map>; +// todo had to ignore, introduce error in doc generation,see why/better solution +/** @ignore */ +export default class IpcProvider extends SocketProvider< + Buffer | string, + CloseEvent, + Error, + API +> { + private _connectionStatus: Web3ProviderStatus; + // Message handlers. Due to bounding of `this` and removing the listeners we have to keep it's reference. + protected _socketConnection?: Socket; public constructor(socketPath: string) { - super(); - - this._connectionStatus = 'disconnected'; - this._socketPath = socketPath; - this._socket = new Socket(); - - this._requestQueue = new Map>(); - - this.connect(); - this.waitOptions = { - timeOutTime: 5000, - maxNumberOfAttempts: 10, - }; - this.chunkResponseParser = new ChunkResponseParser(); - this.chunkResponseParser.onError(() => { - this._clearQueues(); - }); + super(socketPath); + this._connectionStatus = 'connecting'; } public getStatus(): Web3ProviderStatus { + if (this._socketConnection?.connecting) { + return 'connecting'; + } return this._connectionStatus; } - - /* eslint-disable class-methods-use-this */ - public supportsSubscriptions(): boolean { - return true; - } - - public on( - type: 'message' | 'connect' | 'disconnect' | string, - callback: Web3ProviderEventCallback, - ): void { - this._emitter.on(type, callback); - } - - public once(type: string, callback: Web3ProviderEventCallback): void { - this._emitter.once(type, callback); - } - - public removeListener(type: string, callback: Web3ProviderEventCallback): void { - this._emitter.removeListener(type, callback); - } - public connect(): void { if (!existsSync(this._socketPath)) { throw new InvalidClientError(this._socketPath); } - + if (!this._socketConnection || this.getStatus() === 'disconnected') { + this._socketConnection = new Socket(); + } try { + this._connectionStatus = 'connecting'; this._addSocketListeners(); - this._socket.connect({ path: this._socketPath }); + this._socketConnection.connect({ path: this._socketPath }); } catch (e) { throw new InvalidConnectionError(this._socketPath); } } - public disconnect(): void { - this._requestQueue.clear(); - this._removeSocketListeners(); - this._socket.end(); - } - - public reset(): void { - this._requestQueue.clear(); - - this._removeSocketListeners(); - this._addSocketListeners(); + protected _closeSocketConnection(code?: number, data?: string) { + this._socketConnection?.end(() => { + this._onDisconnect(code, data); + }); } - public get waitTimeOut(): number { - return this.waitOptions.timeOutTime; + protected _sendToSocket>( + payload: Web3APIPayload, + ): void { + if (this.getStatus() === 'disconnected') { + throw new ConnectionNotOpenError(); + } + this._socketConnection?.write(JSON.stringify(payload)); } - public set waitTimeOut(timeOut: number) { - this.waitOptions.timeOutTime = timeOut; - } + protected _onCloseEvent(event: CloseEvent): void { + if ( + this._reconnectOptions.autoReconnect && + (![1000, 1001].includes(event.code) || !event.wasClean) + ) { + this._reconnect(); + return; + } - public get waitMaxNumberOfAttempts(): number { - return this.waitOptions.maxNumberOfAttempts; + this._clearQueues(event); + this._removeSocketListeners(); + this._onDisconnect(event.code, event.reason); } - public set waitMaxNumberOfAttempts(maxNumberOfAttempts: number) { - this.waitOptions.maxNumberOfAttempts = maxNumberOfAttempts; + protected _parseResponses(e: Buffer | string) { + return this.chunkResponseParser.parseResponse( + typeof e === 'string' ? e : e.toString('utf8'), + ); } - public async waitForConnection(): Promise { - return new Promise((resolve, reject) => { - let currentAttempt = 0; - const interval = setInterval(() => { - if (currentAttempt > this.waitMaxNumberOfAttempts - 1) { - clearInterval(interval); - reject(new ConnectionNotOpenError()); - } else if (this.getStatus() === 'connected') { - clearInterval(interval); - resolve(); - } - currentAttempt += 1; - }, this.waitTimeOut / this.waitMaxNumberOfAttempts); - }); + protected _onClose(event: CloseEvent): void { + this._clearQueues(event); + this._removeSocketListeners(); } - // eslint-disable-next-line @typescript-eslint/require-await - public async request< - Method extends Web3APIMethod, - ResultType = Web3APIReturnType, - >(request: Web3APIPayload): Promise> { - if (isNullish(this._socket)) throw new Error('IPC connection is undefined'); - - const requestId = jsonRpc.isBatchRequest(request) ? request[0].id : request.id; - - if (isNullish(requestId)) throw new Error('Request Id not defined'); - - if (this.getStatus() !== 'connected') { - await this.waitForConnection(); - } - - // TODO: once https://github.com/web3/web3.js/issues/5460 is implemented, remove this block. - // And catch the error by listening to the error event. - // Additionally, after both https://github.com/web3/web3.js/issues/5466 and https://github.com/web3/web3.js/issues/5467 - // are implemented. There should be no case in the tests that cause a request to the provider after closing the connection. - if (!this._socket.writable) { - console.error( - 'Can not send a request. The internal socket is not `writable`. Request data: ', - request, - ); - const dummyPromise = new Web3DeferredPromise>(); - return dummyPromise; - } - + protected _addSocketListeners(): void { + this._socketConnection?.on('data', this._onMessageHandler); + this._socketConnection?.on('connect', this._onOpenHandler); + this._socketConnection?.on('close', this._onClose.bind(this)); + this._socketConnection?.on('end', this._onCloseHandler); + let errorListeners: unknown[] | undefined; try { - const defPromise = new Web3DeferredPromise>(); - this._requestQueue.set(requestId, defPromise); - this._socket.write(JSON.stringify(request)); - - return defPromise; + errorListeners = (this._socketConnection as Socket)?.listeners('error'); } catch (error) { - this._requestQueue.delete(requestId); - throw error; + // At some cases (at GitHub pipeline) there is an error raised when trying to access the listeners + // However, no need to do take any specific action in this case beside try adding the event listener for `error` + this._socketConnection?.on('error', this._onErrorHandler); + return; + } + // The error event listener may be already there because we do not remove it like the others + // So we add it only if it was not already added + if (!errorListeners || errorListeners.length === 0) { + this._socketConnection?.on('error', this._onErrorHandler); } } - public removeAllListeners(type: string): void { - this._emitter.removeAllListeners(type); + protected _removeSocketListeners(): void { + this._socketConnection?.removeAllListeners('connect'); + this._socketConnection?.removeAllListeners('end'); + this._socketConnection?.removeAllListeners('close'); + this._socketConnection?.removeAllListeners('data'); } - private _onMessage(e: Buffer | string): void { - const responses = this.chunkResponseParser.parseResponse( - typeof e === 'string' ? e : e.toString('utf8'), - ); - if (!responses) { - return; - } - for (const response of responses) { - if ( - jsonRpc.isResponseWithNotification(response as JsonRpcNotification) && - (response as JsonRpcNotification).method.endsWith('_subscription') - ) { - this._emitter.emit('message', undefined, response); - return; - } - - const requestId = jsonRpc.isBatchResponse(response) ? response[0].id : response.id; - const requestItem = this._requestQueue.get(requestId); - - if (!requestItem) { - return; - } - - if (jsonRpc.isBatchResponse(response) || jsonRpc.isResponseWithResult(response)) { - this._emitter.emit('message', undefined, response); - requestItem.resolve(response); - } else { - this._emitter.emit('message', response, undefined); - requestItem?.reject(new ResponseError(response)); - } - - this._requestQueue.delete(requestId); + protected _clearQueues(event?: CloseEvent) { + if (this._pendingRequestsQueue.size > 0) { + this._pendingRequestsQueue.forEach( + (request: SocketRequestItem, key: JsonRpcId) => { + request.deferredPromise.reject(new ConnectionNotOpenError(event)); + this._pendingRequestsQueue.delete(key); + }, + ); } - } - private _clearQueues(event?: CloseEvent) { - if (this._requestQueue.size > 0) { - this._requestQueue.forEach((request: Web3DeferredPromise, key: JsonRpcId) => { - request.reject(new ConnectionNotOpenError(event)); - this._requestQueue.delete(key); - }); + if (this._sentRequestsQueue.size > 0) { + this._sentRequestsQueue.forEach( + (request: SocketRequestItem, key: JsonRpcId) => { + request.deferredPromise.reject(new ConnectionNotOpenError(event)); + this._sentRequestsQueue.delete(key); + }, + ); } this._removeSocketListeners(); } - private _onConnect() { + protected _onConnect() { this._connectionStatus = 'connected'; - this._emitter.emit('connect'); + super._onConnect(); } - private _onDisconnect(): void { + protected _onDisconnect(code?: number, data?: string): void { this._connectionStatus = 'disconnected'; - this._emitter.emit('disconnect'); - } - - private _onClose(event: CloseEvent): void { - this._clearQueues(event); - this._removeSocketListeners(); - } - - private _addSocketListeners(): void { - this._socket.on('connect', this._onConnect.bind(this)); - this._socket.on('end', this._onDisconnect.bind(this)); - this._socket.on('close', this._onClose.bind(this)); - this._socket.on('data', this._onMessage.bind(this)); - } - - private _removeSocketListeners(): void { - this._socket?.removeAllListeners('connect'); - this._socket?.removeAllListeners('end'); - this._socket?.removeAllListeners('close'); - this._socket?.removeAllListeners('data'); + super._onDisconnect(code, data); } } diff --git a/packages/web3-providers-ipc/test/fixtures/accounts.json b/packages/web3-providers-ipc/test/fixtures/accounts.json new file mode 120000 index 00000000000..7dbcddb60a2 --- /dev/null +++ b/packages/web3-providers-ipc/test/fixtures/accounts.json @@ -0,0 +1 @@ +../../../../scripts/accounts.json \ No newline at end of file diff --git a/packages/web3-providers-ipc/test/fixtures/system_test_utils.ts b/packages/web3-providers-ipc/test/fixtures/system_test_utils.ts new file mode 120000 index 00000000000..2ab08a83752 --- /dev/null +++ b/packages/web3-providers-ipc/test/fixtures/system_test_utils.ts @@ -0,0 +1 @@ +../../../../scripts/system_tests_utils.ts \ No newline at end of file diff --git a/packages/web3-providers-ipc/test/fixtures/test_data.ts b/packages/web3-providers-ipc/test/fixtures/test_data.ts new file mode 100644 index 00000000000..c1bddff0444 --- /dev/null +++ b/packages/web3-providers-ipc/test/fixtures/test_data.ts @@ -0,0 +1,45 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +export const validConnectionStrings = [ + 'ws://localhost:8545', + 'ws://www.localhost', + 'ws://localhost', + 'wss://foo.com', + 'ws://foo.ninja', + 'wss://foo.com', + 'ws://foo.com:8545', +]; + +export const invalidConnectionStrings = [ + 'htt://localhost:8545', + 'http//localhost:8545', + 'ipc://localhost:8545', + '', + // Using "null" value intentionally for validation + // eslint-disable-next-line no-null/no-null + null, + undefined, + 42, +]; + +export const wsProviderOptions = { + followRedirects: true, + handshakeTimeout: 1500, + maxRedirects: 3, + perMessageDeflate: true, +}; diff --git a/packages/web3-providers-ipc/test/integration/eip1193.test.ts b/packages/web3-providers-ipc/test/integration/eip1193.test.ts new file mode 100644 index 00000000000..96c2480ee04 --- /dev/null +++ b/packages/web3-providers-ipc/test/integration/eip1193.test.ts @@ -0,0 +1,109 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +import { hexToNumber } from 'web3-utils'; +import { + HexString, + ProviderConnectInfo, + ProviderRpcError, + Web3ProviderEventCallback, +} from 'web3-types'; +import IpcProvider from '../../src/index'; + +import { + getSystemTestProvider, + describeIf, + isIpc, + waitForSocketConnect, + waitForSocketDisconnect, +} from '../fixtures/system_test_utils'; + +describeIf(isIpc)('IpcProvider - eip1193', () => { + let socketPath: string; + let socketProvider: IpcProvider; + + beforeAll(() => { + socketPath = getSystemTestProvider(); + }); + beforeEach(() => { + socketProvider = new IpcProvider(socketPath); + }); + afterEach(async () => { + socketProvider.disconnect(1000); + await waitForSocketDisconnect(socketProvider); + }); + + describe('check events', () => { + it('should send connect event', async () => { + const { chainId } = await new Promise(resolve => { + socketProvider.on('connect', ((_error: unknown, data) => { + resolve(data as unknown as ProviderConnectInfo); + }) as Web3ProviderEventCallback); + }); + expect(hexToNumber(chainId)).toBeGreaterThan(0); + }); + + it('should send disconnect event', async () => { + await waitForSocketConnect(socketProvider); + const disconnectPromise = new Promise(resolve => { + socketProvider.on('disconnect', ((error: ProviderRpcError) => { + resolve(error); + }) as Web3ProviderEventCallback); + }); + socketProvider.disconnect(1000, 'Some extra data'); + + const err = await disconnectPromise; + expect(err.code).toBe(1000); + expect(err.data).toBe('Some extra data'); + }); + it('should send chainChanged event', async () => { + await waitForSocketConnect(socketProvider); + // @ts-expect-error set private variable + socketProvider._chainId = '0x1'; + socketProvider.disconnect(1000); + await waitForSocketDisconnect(socketProvider); + const chainChangedPromise = new Promise(resolve => { + socketProvider.on('chainChanged', ((_error, data) => { + resolve(data as unknown as ProviderConnectInfo); + }) as Web3ProviderEventCallback); + }); + socketProvider.connect(); + await waitForSocketConnect(socketProvider); + const changedData = await chainChangedPromise; + expect(changedData.chainId).not.toBe('0x1'); + expect(hexToNumber(changedData.chainId)).toBeGreaterThan(0); + }); + it('should send accountsChanged event', async () => { + await waitForSocketConnect(socketProvider); + + // @ts-expect-error set private variable + socketProvider._accounts = ['1', '2']; + socketProvider.disconnect(1000); + await waitForSocketDisconnect(socketProvider); + const chainChangedPromise = new Promise<{ accounts: HexString[] }>(resolve => { + socketProvider.on('accountsChanged', ((_error, data) => { + resolve(data as unknown as { accounts: HexString[] }); + }) as Web3ProviderEventCallback<{ accounts: HexString[] }>); + }); + socketProvider.connect(); + await waitForSocketConnect(socketProvider); + const changedData = await chainChangedPromise; + + expect(JSON.stringify(changedData.accounts)).not.toBe(JSON.stringify(['1', '2'])); + }); + }); +}); diff --git a/packages/web3-providers-ipc/test/unit/ipc_provider.test.ts b/packages/web3-providers-ipc/test/unit/ipc_provider.test.ts index 9f66de3e8f6..f1a2d492ca6 100644 --- a/packages/web3-providers-ipc/test/unit/ipc_provider.test.ts +++ b/packages/web3-providers-ipc/test/unit/ipc_provider.test.ts @@ -62,29 +62,39 @@ describe('IpcProvider', () => { it('should add listeners to socket', () => { const provider = new IpcProvider(socketPath); - jest.spyOn(provider['_socket'], 'on'); - - expect(provider['_socket'].on).toHaveBeenCalledWith('connect', expect.any(Function)); - expect(provider['_socket'].on).toHaveBeenCalledWith('end', expect.any(Function)); - expect(provider['_socket'].on).toHaveBeenCalledWith('close', expect.any(Function)); - expect(provider['_socket'].on).toHaveBeenCalledWith('data', expect.any(Function)); + // @ts-expect-error-next-line + jest.spyOn(provider._socketConnection, 'on'); + + // @ts-expect-error-next-line + expect(provider._socketConnection.on).toHaveBeenCalledWith( + 'connect', + expect.any(Function), + ); + // @ts-expect-error-next-line + expect(provider._socketConnection.on).toHaveBeenCalledWith('end', expect.any(Function)); + // @ts-expect-error-next-line + expect(provider._socketConnection.on).toHaveBeenCalledWith( + 'close', + expect.any(Function), + ); + // @ts-expect-error-next-line + expect(provider._socketConnection.on).toHaveBeenCalledWith( + 'data', + expect.any(Function), + ); }); it('should connect to socket path', () => { const provider = new IpcProvider(socketPath); - jest.spyOn(provider['_socket'], 'connect'); - - expect(provider['_socket'].connect).toHaveBeenCalledTimes(1); - expect(provider['_socket'].connect).toHaveBeenCalledWith({ path: socketPath }); - }); - it('check wait params', () => { - const provider = new IpcProvider(socketPath); - expect(provider.waitTimeOut).toBe(5000); - expect(provider.waitMaxNumberOfAttempts).toBe(10); - provider.waitTimeOut = 300; - provider.waitMaxNumberOfAttempts = 20; - expect(provider.waitTimeOut).toBe(300); - expect(provider.waitMaxNumberOfAttempts).toBe(20); + // @ts-expect-error-next-line + jest.spyOn(provider._socketConnection, 'connect'); + + // @ts-expect-error-next-line + expect(provider._socketConnection.connect).toHaveBeenCalledTimes(1); + // @ts-expect-error-next-line + expect(provider._socketConnection.connect).toHaveBeenCalledWith({ + path: socketPath, + }); }); }); }); diff --git a/packages/web3-providers-ws/CHANGELOG.md b/packages/web3-providers-ws/CHANGELOG.md index 5e211c3684f..6f1e0884a71 100644 --- a/packages/web3-providers-ws/CHANGELOG.md +++ b/packages/web3-providers-ws/CHANGELOG.md @@ -48,3 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated dependencies (#5725) ## [Unreleased] + +### Changed + +- Refactor to use common SocketProvider class (#5683) diff --git a/packages/web3-providers-ws/src/index.ts b/packages/web3-providers-ws/src/index.ts index 7d7f5832efc..8d2425728f8 100644 --- a/packages/web3-providers-ws/src/index.ts +++ b/packages/web3-providers-ws/src/index.ts @@ -15,334 +15,108 @@ You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ -import { EventEmitter } from 'events'; import { ClientRequestArgs } from 'http'; -import WebSocket, { ClientOptions, CloseEvent, ErrorEvent, MessageEvent } from 'isomorphic-ws'; +import WebSocket, { ClientOptions, CloseEvent } from 'isomorphic-ws'; import { + ConnectionEvent, EthExecutionAPI, JsonRpcId, - JsonRpcNotification, - JsonRpcResult, + SocketRequestItem, Web3APIMethod, Web3APIPayload, - Web3APIReturnType, Web3APISpec, - Web3BaseProvider, - Web3ProviderEventCallback, Web3ProviderStatus, - JsonRpcResponseWithResult, } from 'web3-types'; -import { jsonRpc, isNullish, Web3DeferredPromise, ChunkResponseParser } from 'web3-utils'; -import { - InvalidClientError, - InvalidConnectionError, - ConnectionNotOpenError, - PendingRequestsOnReconnectingError, - Web3WSProviderError, - RequestAlreadySentError, - ResponseError, -} from 'web3-errors'; -import { EventEmittedCallback, OnCloseEvent, ReconnectOptions, WSRequestItem } from './types'; +import { isNullish } from 'web3-utils'; +import { InvalidConnectionError, ConnectionNotOpenError } from 'web3-errors'; +import { SocketProvider } from 'web3-utils'; export { ClientRequestArgs } from 'http'; // todo had to ignore, introduce error in doc generation,see why/better solution /** @ignore */ export { ClientOptions } from 'isomorphic-ws'; -export { ReconnectOptions } from './types'; + export default class WebSocketProvider< API extends Web3APISpec = EthExecutionAPI, -> extends Web3BaseProvider { - private readonly _wsEventEmitter: EventEmitter = new EventEmitter(); - - private readonly _clientUrl: string; - private readonly _wsProviderOptions?: ClientOptions | ClientRequestArgs; - - private _webSocketConnection?: WebSocket; - private readonly chunkResponseParser: ChunkResponseParser; - - /* eslint-disable @typescript-eslint/no-explicit-any */ - protected readonly _pendingRequestsQueue: Map>; - /* eslint-disable @typescript-eslint/no-explicit-any */ - protected readonly _sentRequestsQueue: Map>; - - private _reconnectAttempts!: number; - private readonly _reconnectOptions: ReconnectOptions; - - // Message handlers. Due to bounding of `this` and removing the listeners we have to keep it's reference. - private readonly _onMessageHandler: (event: MessageEvent) => void; - private readonly _onOpenHandler: () => void; - private readonly _onCloseHandler: (event: CloseEvent) => void; - private readonly _onErrorHandler: (event: ErrorEvent) => void; - - public constructor( - clientUrl: string, - wsProviderOptions?: ClientOptions | ClientRequestArgs, - reconnectOptions?: ReconnectOptions, - ) { - super(); - if (!WebSocketProvider._validateProviderUrl(clientUrl)) - throw new InvalidClientError(clientUrl); - - this._clientUrl = clientUrl; - this._wsProviderOptions = wsProviderOptions; - - const DEFAULT_PROVIDER_RECONNECTION_OPTIONS = { - autoReconnect: true, - delay: 5000, - maxAttempts: 5, - }; - - this._reconnectOptions = { - ...DEFAULT_PROVIDER_RECONNECTION_OPTIONS, - ...reconnectOptions, - }; - - this._pendingRequestsQueue = new Map>(); - this._sentRequestsQueue = new Map>(); +> extends SocketProvider { + protected readonly _providerOptions?: ClientOptions | ClientRequestArgs; + protected _socketConnection?: WebSocket; - this._onMessageHandler = this._onMessage.bind(this); - this._onOpenHandler = this._onConnect.bind(this); - this._onCloseHandler = this._onClose.bind(this); - this._onErrorHandler = this._onError.bind(this); - - this._init(); - this.connect(); - this.chunkResponseParser = new ChunkResponseParser(); - this.chunkResponseParser.onError(() => { - this._clearQueues(); - }); - } - - private static _validateProviderUrl(providerUrl: string): boolean { + // eslint-disable-next-line class-methods-use-this + protected _validateProviderPath(providerUrl: string): boolean { return typeof providerUrl === 'string' ? /^ws(s)?:\/\//i.test(providerUrl) : false; } public getStatus(): Web3ProviderStatus { - if (isNullish(this._webSocketConnection)) return 'disconnected'; - - switch (this._webSocketConnection.readyState) { - case this._webSocketConnection.CONNECTING: { - return 'connecting'; - } - case this._webSocketConnection.OPEN: { - return 'connected'; - } - default: { - return 'disconnected'; + if (this._socketConnection && !isNullish(this._socketConnection)) { + switch (this._socketConnection.readyState) { + case this._socketConnection.CONNECTING: { + return 'connecting'; + } + case this._socketConnection.OPEN: { + return 'connected'; + } + default: { + return 'disconnected'; + } } } - } - - /* eslint-disable class-methods-use-this */ - public supportsSubscriptions(): boolean { - return true; - } - - public on( - type: 'message' | string, - callback: Web3ProviderEventCallback | EventEmittedCallback, - ): void { - this._wsEventEmitter.on(type, callback); - } - - public once(type: string, callback: Web3ProviderEventCallback): void { - this._wsEventEmitter.once(type, callback); - } - - public removeListener(type: string, callback: Web3ProviderEventCallback): void { - this._wsEventEmitter.removeListener(type, callback); + return 'disconnected'; } public connect(): void { try { - this._webSocketConnection = new WebSocket( - this._clientUrl, + this._socketConnection = new WebSocket( + this._socketPath, undefined, - this._wsProviderOptions && Object.keys(this._wsProviderOptions).length === 0 + this._providerOptions && Object.keys(this._providerOptions).length === 0 ? undefined - : this._wsProviderOptions, + : this._providerOptions, ); this._addSocketListeners(); - - // TODO: Debug why this is needed - // if (this.getStatus() === 'connecting') { - // // Rejecting promises if provider is not connected even after reattempts - // setTimeout(() => { - // if (this.getStatus() === 'disconnected') { - // this._clearQueues(undefined); - // } - // }, this._reconnectOptions.delay * (this._reconnectOptions.maxAttempts + 1)); - // } } catch (e) { - throw new InvalidConnectionError(this._clientUrl); + throw new InvalidConnectionError(this._socketPath); } } - public disconnect(code?: number, reason?: string): void { - this._emitCloseEvent(code, reason); - this._removeSocketListeners(); - this._webSocketConnection?.close(code, reason); + protected _closeSocketConnection(code?: number, data?: string) { + this._socketConnection?.close(code, data); } - - public reset(): void { - this._sentRequestsQueue.clear(); - this._pendingRequestsQueue.clear(); - - this._init(); - this._removeSocketListeners(); - this._addSocketListeners(); - } - - public async request< - Method extends Web3APIMethod, - ResultType = Web3APIReturnType, - >(request: Web3APIPayload): Promise> { - const requestId = jsonRpc.isBatchRequest(request) ? request[0].id : request.id; - - if (!requestId) { - throw new Web3WSProviderError('Request Id not defined'); - } - - if (this._sentRequestsQueue.has(requestId)) { - throw new RequestAlreadySentError(requestId); - } - - const deferredPromise = new Web3DeferredPromise>(); - - const reqItem: WSRequestItem> = { - payload: request, - deferredPromise, - }; - - if (this.getStatus() === 'connecting') { - this._pendingRequestsQueue.set(requestId, reqItem); - - return reqItem.deferredPromise; - } - - this._sentRequestsQueue.set(requestId, reqItem); - - try { - this._sendToSocket(reqItem.payload); - } catch (error) { - this._sentRequestsQueue.delete(requestId); - throw error; - } - - return deferredPromise; + protected _parseResponses(event: WebSocket.MessageEvent) { + return this.chunkResponseParser.parseResponse(event.data as string); } - private _sendToSocket>( + protected _sendToSocket>( payload: Web3APIPayload, ): void { - if (!this._webSocketConnection) { - throw new Web3WSProviderError('WebSocket connection is not created'); - } - if (this.getStatus() === 'disconnected') { throw new ConnectionNotOpenError(); } - - this._webSocketConnection.send(JSON.stringify(payload)); - } - - public removeAllListeners(type: string): void { - this._wsEventEmitter.removeAllListeners(type); + this._socketConnection?.send(JSON.stringify(payload)); } - private _init() { - this._reconnectAttempts = 0; - } - - private _addSocketListeners(): void { - this._webSocketConnection?.addEventListener('message', this._onMessageHandler); - this._webSocketConnection?.addEventListener('open', this._onOpenHandler); - this._webSocketConnection?.addEventListener('close', this._onCloseHandler); - + protected _addSocketListeners(): void { + this._socketConnection?.addEventListener('message', this._onMessageHandler); + this._socketConnection?.addEventListener('open', this._onOpenHandler); + this._socketConnection?.addEventListener('close', e => this._onCloseHandler(e)); let errorListeners: unknown[] | undefined; try { - errorListeners = this._webSocketConnection?.listeners('error'); + errorListeners = this._socketConnection?.listeners('error'); } catch (error) { // At some cases (at GitHub pipeline) there is an error raised when trying to access the listeners // However, no need to do take any specific action in this case beside try adding the event listener for `error` - this._webSocketConnection?.addEventListener('error', this._onErrorHandler); + this._socketConnection?.addEventListener('error', this._onErrorHandler); return; } // The error event listener may be already there because we do not remove it like the others // So we add it only if it was not already added if (!errorListeners || errorListeners.length === 0) { - this._webSocketConnection?.addEventListener('error', this._onErrorHandler); + this._socketConnection?.addEventListener('error', this._onErrorHandler); } } - private _reconnect(): void { - if (this._sentRequestsQueue.size > 0) { - this._sentRequestsQueue.forEach( - (request: WSRequestItem, key: JsonRpcId) => { - request.deferredPromise.reject(new PendingRequestsOnReconnectingError()); - this._sentRequestsQueue.delete(key); - }, - ); - } - - if (this._reconnectAttempts < this._reconnectOptions.maxAttempts) { - setTimeout(() => { - this._reconnectAttempts += 1; - this._removeSocketListeners(); - this.connect(); - }, this._reconnectOptions.delay); - } - } - - private _onMessage(event: MessageEvent): void { - const responses = this.chunkResponseParser.parseResponse(event.data as string); - if (!responses) { - return; - } - for (const response of responses) { - if ( - jsonRpc.isResponseWithNotification(response as JsonRpcNotification) && - (response as JsonRpcNotification).method.endsWith('_subscription') - ) { - this._wsEventEmitter.emit('message', undefined, response); - return; - } - - const requestId = jsonRpc.isBatchResponse(response) ? response[0].id : response.id; - const requestItem = this._sentRequestsQueue.get(requestId); - - if (!requestItem) { - return; - } - - if (jsonRpc.isBatchResponse(response) || jsonRpc.isResponseWithResult(response)) { - this._wsEventEmitter.emit('message', undefined, response); - requestItem.deferredPromise.resolve(response); - } else { - this._wsEventEmitter.emit('message', response, undefined); - requestItem?.deferredPromise.reject(new ResponseError(response)); - } - - this._sentRequestsQueue.delete(requestId); - } - } - - private _onConnect() { - this._reconnectAttempts = 0; - this._wsEventEmitter.emit('open'); - this._sendPendingRequests(); - } - - private _sendPendingRequests() { - for (const [id, value] of this._pendingRequestsQueue.entries()) { - this._sendToSocket(value.payload as Web3APIPayload); - this._pendingRequestsQueue.delete(id); - this._sentRequestsQueue.set(id, value); - } - } - - private _onClose(event: CloseEvent): void { + protected _onCloseEvent(event: CloseEvent): void { if ( this._reconnectOptions.autoReconnect && (![1000, 1001].includes(event.code) || !event.wasClean) @@ -351,19 +125,22 @@ export default class WebSocketProvider< return; } - this._emitCloseEvent(event.code, event.reason); this._clearQueues(event); this._removeSocketListeners(); + this._onDisconnect(event.code, event.reason); } - private _onError(event: ErrorEvent): void { - this._wsEventEmitter.emit('error', event); + protected _removeSocketListeners(): void { + this._socketConnection?.removeEventListener('message', this._onMessageHandler); + this._socketConnection?.removeEventListener('open', this._onOpenHandler); + this._socketConnection?.removeEventListener('close', this._onCloseHandler); + // note: we intentionally keep the error event listener to be able to emit it in case an error happens when closing the connection } - private _clearQueues(event?: CloseEvent) { + protected _clearQueues(event?: ConnectionEvent) { if (this._pendingRequestsQueue.size > 0) { this._pendingRequestsQueue.forEach( - (request: WSRequestItem, key: JsonRpcId) => { + (request: SocketRequestItem, key: JsonRpcId) => { request.deferredPromise.reject(new ConnectionNotOpenError(event)); this._pendingRequestsQueue.delete(key); }, @@ -372,7 +149,7 @@ export default class WebSocketProvider< if (this._sentRequestsQueue.size > 0) { this._sentRequestsQueue.forEach( - (request: WSRequestItem, key: JsonRpcId) => { + (request: SocketRequestItem, key: JsonRpcId) => { request.deferredPromise.reject(new ConnectionNotOpenError(event)); this._sentRequestsQueue.delete(key); }, @@ -381,18 +158,4 @@ export default class WebSocketProvider< this._removeSocketListeners(); } - - private _removeSocketListeners(): void { - this._webSocketConnection?.removeEventListener('message', this._onMessageHandler); - this._webSocketConnection?.removeEventListener('open', this._onOpenHandler); - this._webSocketConnection?.removeEventListener('close', this._onCloseHandler); - // note: we intentionally keep the error event listener to be able to emit it in case an error happens when closing the connection - } - - private _emitCloseEvent(code?: number, reason?: string): void { - this._wsEventEmitter.emit('close', undefined, { - code, - reason, - } as OnCloseEvent); - } } diff --git a/packages/web3-providers-ws/src/types.ts b/packages/web3-providers-ws/src/types.ts index ad6cd2b0745..f416303d3ba 100644 --- a/packages/web3-providers-ws/src/types.ts +++ b/packages/web3-providers-ws/src/types.ts @@ -1,4 +1,4 @@ -/* +/* This file is part of web3.js. web3.js is free software: you can redistribute it and/or modify @@ -15,29 +15,8 @@ You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ -import { Web3APIMethod, Web3APIPayload, Web3APISpec, Web3DeferredPromise } from 'web3-types'; - export type ReconnectOptions = { autoReconnect: boolean; delay: number; maxAttempts: number; }; - -export interface WSRequestItem< - API extends Web3APISpec, - Method extends Web3APIMethod, - ResponseType, -> { - payload: Web3APIPayload; - deferredPromise: Web3DeferredPromise; -} - -export type OnCloseEvent = { - code: number; - reason: string; -}; - -export type EventEmittedCallback = ( - error: Error | undefined, - event?: OnCloseEvent | undefined, -) => void; diff --git a/packages/web3-providers-ws/test/fixtures/helpers.ts b/packages/web3-providers-ws/test/fixtures/helpers.ts index 6ce37e9b4f7..fd90c7aa722 100644 --- a/packages/web3-providers-ws/test/fixtures/helpers.ts +++ b/packages/web3-providers-ws/test/fixtures/helpers.ts @@ -15,27 +15,21 @@ You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ +import { ProviderConnectInfo, ProviderRpcError, Web3ProviderEventCallback } from 'web3-types'; import WebSocketProvider from '../../src/index'; -export const waitForOpenConnection = async ( - provider: WebSocketProvider, - currentAttempt: number, - status = 'connected', -) => { - return new Promise((resolve, reject) => { - const maxNumberOfAttempts = 10; - const intervalTime = 2000; // ms +export const waitForOpenConnection = async (provider: WebSocketProvider) => { + return new Promise(resolve => { + provider.on('connect', ((_error, data) => { + resolve(data as unknown as ProviderConnectInfo); + }) as Web3ProviderEventCallback); + }); +}; - const interval = setInterval(() => { - if (currentAttempt > maxNumberOfAttempts - 1) { - clearInterval(interval); - reject(new Error('Maximum number of attempts exceeded')); - } else if (provider.getStatus() === status) { - clearInterval(interval); - resolve(); - } - // eslint-disable-next-line no-plusplus, no-param-reassign - currentAttempt++; - }, intervalTime); +export const waitForCloseConnection = async (provider: WebSocketProvider) => { + return new Promise(resolve => { + provider.on('disconnect', ((_error, data) => { + resolve(data as unknown as ProviderRpcError); + }) as Web3ProviderEventCallback); }); }; diff --git a/packages/web3-providers-ws/test/integration/basic_auth.test.ts b/packages/web3-providers-ws/test/integration/basic_auth.test.ts index a569ac778ca..a3e69a4bf09 100644 --- a/packages/web3-providers-ws/test/integration/basic_auth.test.ts +++ b/packages/web3-providers-ws/test/integration/basic_auth.test.ts @@ -18,7 +18,7 @@ import express from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; import { Server } from 'http'; -import { waitForOpenConnection } from '../fixtures/helpers'; +import { waitForCloseConnection, waitForOpenConnection } from '../fixtures/helpers'; import WebSocketProvider from '../../src/index'; import { getSystemTestProvider, describeIf, isWs } from '../fixtures/system_test_utils'; @@ -26,7 +26,6 @@ describeIf(isWs)('Support of Basic Auth', () => { let server: Server; let clientWsUrl: string; let webSocketProvider: WebSocketProvider; - let currentAttempt = 0; beforeAll(() => { clientWsUrl = getSystemTestProvider(); @@ -74,18 +73,18 @@ describeIf(isWs)('Support of Basic Auth', () => { {}, { delay: 1, autoReconnect: false, maxAttempts: 1 }, ); - currentAttempt = 0; }); afterEach(async () => { // make sure we try to close the connection after it is established if (webSocketProvider.getStatus() === 'connecting') { - await waitForOpenConnection(webSocketProvider, currentAttempt); + await waitForOpenConnection(webSocketProvider); } webSocketProvider.disconnect(); + await waitForCloseConnection(webSocketProvider); }); // eslint-disable-next-line jest/expect-expect test('should connect with basic auth', async () => { - await waitForOpenConnection(webSocketProvider, currentAttempt); - webSocketProvider.disconnect(); + await waitForOpenConnection(webSocketProvider); + expect(webSocketProvider.getStatus()).toBe('connected'); }); }); diff --git a/packages/web3-providers-ws/test/integration/eip1193.test.ts b/packages/web3-providers-ws/test/integration/eip1193.test.ts new file mode 100644 index 00000000000..b92e8182578 --- /dev/null +++ b/packages/web3-providers-ws/test/integration/eip1193.test.ts @@ -0,0 +1,108 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +import { hexToNumber } from 'web3-utils'; +import { + HexString, + ProviderConnectInfo, + ProviderRpcError, + Web3ProviderEventCallback, +} from 'web3-types'; +import WebSocketProvider from '../../src/index'; + +import { + getSystemTestProvider, + describeIf, + isWs, + waitForSocketConnect, + waitForSocketDisconnect, +} from '../fixtures/system_test_utils'; + +describeIf(isWs)('WebSocketProvider - eip1193', () => { + let socketPath: string; + let socketProvider: WebSocketProvider; + + beforeAll(() => { + socketPath = getSystemTestProvider(); + }); + beforeEach(() => { + socketProvider = new WebSocketProvider(socketPath); + }); + afterEach(async () => { + socketProvider.disconnect(1000); + await waitForSocketDisconnect(socketProvider); + }); + + describe('check events', () => { + it('should send connect event', async () => { + const { chainId } = await new Promise(resolve => { + socketProvider.on('connect', ((_error: unknown, data) => { + resolve(data as unknown as ProviderConnectInfo); + }) as Web3ProviderEventCallback); + }); + expect(hexToNumber(chainId)).toBeGreaterThan(0); + }); + it('should send disconnect event', async () => { + await waitForSocketConnect(socketProvider); + const disconnectPromise = new Promise(resolve => { + socketProvider.on('disconnect', ((error: ProviderRpcError) => { + resolve(error); + }) as Web3ProviderEventCallback); + }); + socketProvider.disconnect(1000, 'Some extra data'); + + const err = await disconnectPromise; + expect(err.code).toBe(1000); + expect(err.data).toBe('Some extra data'); + }); + it('should send chainChanged event', async () => { + await waitForSocketConnect(socketProvider); + // @ts-expect-error set private variable + socketProvider._chainId = '0x1'; + socketProvider.disconnect(1000); + await waitForSocketDisconnect(socketProvider); + const chainChangedPromise = new Promise(resolve => { + socketProvider.on('chainChanged', ((_error, data) => { + resolve(data as unknown as ProviderConnectInfo); + }) as Web3ProviderEventCallback); + }); + socketProvider.connect(); + await waitForSocketConnect(socketProvider); + const changedData = await chainChangedPromise; + expect(changedData.chainId).not.toBe('0x1'); + expect(hexToNumber(changedData.chainId)).toBeGreaterThan(0); + }); + it('should send accountsChanged event', async () => { + await waitForSocketConnect(socketProvider); + + // @ts-expect-error set private variable + socketProvider._accounts = ['1', '2']; + socketProvider.disconnect(1000); + await waitForSocketDisconnect(socketProvider); + const chainChangedPromise = new Promise<{ accounts: HexString[] }>(resolve => { + socketProvider.on('accountsChanged', ((_error, data) => { + resolve(data as unknown as { accounts: HexString[] }); + }) as Web3ProviderEventCallback<{ accounts: HexString[] }>); + }); + socketProvider.connect(); + await waitForSocketConnect(socketProvider); + const changedData = await chainChangedPromise; + + expect(JSON.stringify(changedData.accounts)).not.toBe(JSON.stringify(['1', '2'])); + }); + }); +}); diff --git a/packages/web3-providers-ws/test/integration/web_socket_provider_integration.test.ts b/packages/web3-providers-ws/test/integration/web_socket_provider_integration.test.ts index d28bb590274..9828a768af6 100644 --- a/packages/web3-providers-ws/test/integration/web_socket_provider_integration.test.ts +++ b/packages/web3-providers-ws/test/integration/web_socket_provider_integration.test.ts @@ -22,13 +22,15 @@ import { JsonRpcSubscriptionResult, JsonRpcId, JsonRpcResponse, + ProviderRpcError, + Web3ProviderEventCallback, + SocketRequestItem, } from 'web3-types'; import { Web3DeferredPromise } from 'web3-utils'; import { Web3WSProviderError } from 'web3-errors'; import WebSocketProvider from '../../src/index'; -import { OnCloseEvent, WSRequestItem } from '../../src/types'; -import { waitForOpenConnection } from '../fixtures/helpers'; +import { waitForCloseConnection, waitForOpenConnection } from '../fixtures/helpers'; import { getSystemTestProvider, @@ -45,7 +47,6 @@ describeIf(isWs)('WebSocketProvider - implemented methods', () => { let webSocketProvider: WebSocketProvider; let jsonRpcPayload: Web3APIPayload; // helper function - let currentAttempt = 0; beforeAll(async () => { clientWsUrl = getSystemTestProvider(); @@ -63,20 +64,18 @@ describeIf(isWs)('WebSocketProvider - implemented methods', () => { {}, { delay: 1, autoReconnect: false, maxAttempts: 1 }, ); - currentAttempt = 0; }); afterEach(async () => { // make sure we try to close the connection after it is established if (webSocketProvider.getStatus() === 'connecting') { - await waitForOpenConnection(webSocketProvider, currentAttempt); + await waitForOpenConnection(webSocketProvider); } - webSocketProvider.disconnect(1000); }); describe('websocker provider tests', () => { it('should connect', async () => { - await waitForOpenConnection(webSocketProvider, currentAttempt); + await waitForOpenConnection(webSocketProvider); expect(webSocketProvider).toBeInstanceOf(WebSocketProvider); expect(webSocketProvider.getStatus()).toBe('connected'); }); @@ -94,6 +93,9 @@ describeIf(isWs)('WebSocketProvider - implemented methods', () => { if (error) { throw new Error(error.message); } + if (result?.id !== jsonRpcPayload.id) { + return; + } expect(result?.id).toBe(jsonRpcPayload.id); resolve(); }, @@ -104,6 +106,7 @@ describeIf(isWs)('WebSocketProvider - implemented methods', () => { }); it('should subscribe to `error` event that could happen at the underlying WebSocket connection', async () => { + await waitForOpenConnection(webSocketProvider); const errorMsg = 'Custom WebSocket error occurred'; const errorPromise = new Promise((resolve: Resolve) => { @@ -113,7 +116,7 @@ describeIf(isWs)('WebSocketProvider - implemented methods', () => { }); }); - webSocketProvider['_webSocketConnection']?.emit( + webSocketProvider['_socketConnection']?.emit( 'error', new Web3WSProviderError(errorMsg), ); @@ -122,27 +125,23 @@ describeIf(isWs)('WebSocketProvider - implemented methods', () => { it('should subscribe to `connect` event', async () => { const openPromise = new Promise((resolve: Resolve) => { - webSocketProvider.on('open', () => { + webSocketProvider.on('connect', () => { resolve('resolved'); }); }); await expect(openPromise).resolves.toBe('resolved'); }); - it('should subscribe to `close` event', async () => { + it('should subscribe to `disconnect` event', async () => { const code = 1000; - const closePromise = new Promise((resolve: Resolve) => { - webSocketProvider.on('close', (err?: Error, event?: OnCloseEvent) => { - if (err) { - throw new Error(err.message); - } - expect(event?.code).toEqual(code); - resolve(); - }); + const closePromise = new Promise(resolve => { + webSocketProvider.on('disconnect', ((error: ProviderRpcError) => { + expect(error?.code).toEqual(code); + resolve(error); + }) as Web3ProviderEventCallback); }); - currentAttempt = 0; - await waitForOpenConnection(webSocketProvider, currentAttempt); + await waitForOpenConnection(webSocketProvider); webSocketProvider.disconnect(code); await closePromise; }); @@ -150,20 +149,22 @@ describeIf(isWs)('WebSocketProvider - implemented methods', () => { describe('disconnect and reset test', () => { it('should disconnect', async () => { + await waitForOpenConnection(webSocketProvider); const provider = new WebSocketProvider( clientWsUrl, {}, { delay: 1, autoReconnect: false, maxAttempts: 1 }, ); - await waitForOpenConnection(provider, currentAttempt); + await waitForOpenConnection(provider); provider.disconnect(1000); - await waitForOpenConnection(provider, currentAttempt, 'disconnected'); + await waitForCloseConnection(provider); expect(provider.getStatus()).toBe('disconnected'); }); it('should reset', async () => { + await waitForOpenConnection(webSocketProvider); class TestReset extends WebSocketProvider { - public pendigRequestsSize() { + public pendingRequestsSize() { return this._pendingRequestsQueue.size; } @@ -171,11 +172,11 @@ describeIf(isWs)('WebSocketProvider - implemented methods', () => { return this._pendingRequestsQueue.size; } - public setPendingRequest(id: JsonRpcId, reqItem: WSRequestItem) { + public setPendingRequest(id: JsonRpcId, reqItem: SocketRequestItem) { this._pendingRequestsQueue.set(id, reqItem); } - public setSentRequest(id: JsonRpcId, reqItem: WSRequestItem) { + public setSentRequest(id: JsonRpcId, reqItem: SocketRequestItem) { this._sentRequestsQueue.set(id, reqItem); } } @@ -185,23 +186,23 @@ describeIf(isWs)('WebSocketProvider - implemented methods', () => { { delay: 1, autoReconnect: false, maxAttempts: 1 }, ); - await waitForOpenConnection(testResetProvider, currentAttempt); + await waitForOpenConnection(testResetProvider); const defPromise = new Web3DeferredPromise>(); - const reqItem: WSRequestItem = { + const reqItem: SocketRequestItem = { payload: jsonRpcPayload, deferredPromise: defPromise, }; testResetProvider.setPendingRequest(jsonRpcPayload.id, reqItem); - expect(testResetProvider.pendigRequestsSize()).toBe(1); + expect(testResetProvider.pendingRequestsSize()).toBe(1); testResetProvider.setSentRequest(jsonRpcPayload.id, reqItem); expect(testResetProvider.sentRequestsSize()).toBe(1); testResetProvider.reset(); - expect(testResetProvider.pendigRequestsSize()).toBe(0); + expect(testResetProvider.pendingRequestsSize()).toBe(0); expect(testResetProvider.sentRequestsSize()).toBe(0); testResetProvider.disconnect(1000); @@ -209,17 +210,19 @@ describeIf(isWs)('WebSocketProvider - implemented methods', () => { }); describe('getStatus get and validate all status tests', () => { - it('should getStatus `connecting`', () => { + it('should getStatus `connecting`', async () => { expect(webSocketProvider.getStatus()).toBe('connecting'); + await waitForOpenConnection(webSocketProvider); }); it('should getStatus `connected`', async () => { - await waitForOpenConnection(webSocketProvider, currentAttempt); + await waitForOpenConnection(webSocketProvider); expect(webSocketProvider.getStatus()).toBe('connected'); }); it('should getStatus `disconnected`', async () => { - await waitForOpenConnection(webSocketProvider, currentAttempt); + await waitForOpenConnection(webSocketProvider); webSocketProvider.disconnect(); + await waitForCloseConnection(webSocketProvider); expect(webSocketProvider.getStatus()).toBe('disconnected'); }); }); diff --git a/packages/web3-types/CHANGELOG.md b/packages/web3-types/CHANGELOG.md index a9d00e08735..0ce4a80b3dc 100644 --- a/packages/web3-types/CHANGELOG.md +++ b/packages/web3-types/CHANGELOG.md @@ -58,3 +58,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Make the `request` method of `EIP1193Provider` class, compatible with EIP 1193 (#5591) ## [Unreleased] + +### Changed + +- These types were added: ProviderRpcError, EthSubscription, ProviderMessage, ProviderConnectInfo (#5683) diff --git a/packages/web3-types/src/web3_api_types.ts b/packages/web3-types/src/web3_api_types.ts index 472b60ed91d..1cb7bcf5a4f 100644 --- a/packages/web3-types/src/web3_api_types.ts +++ b/packages/web3-types/src/web3_api_types.ts @@ -17,6 +17,24 @@ along with web3.js. If not, see . import { JsonRpcId, JsonRpcIdentifier } from './json_rpc_types'; +export interface ProviderMessage { + readonly type: string; + readonly data: unknown; +} +export interface EthSubscription extends ProviderMessage { + readonly type: 'eth_subscription'; + readonly data: { + readonly subscription: string; + readonly result: unknown; + }; +} +export interface ProviderRpcError extends Error { + code: number; + data?: unknown; +} +export interface ProviderConnectInfo { + readonly chainId: string; +} export type Web3APISpec = Record any> | unknown; export type Web3APIMethod = string & keyof Exclude; export type Web3APIParams< diff --git a/packages/web3-types/src/web3_base_provider.ts b/packages/web3-types/src/web3_base_provider.ts index 067cae9cfea..c602cff5f32 100644 --- a/packages/web3-types/src/web3_base_provider.ts +++ b/packages/web3-types/src/web3_base_provider.ts @@ -27,16 +27,33 @@ import { JsonRpcResult, JsonRpcSubscriptionResult, } from './json_rpc_types'; -import { Web3APISpec, Web3APIMethod, Web3APIReturnType, Web3APIPayload } from './web3_api_types'; +import { + Web3APISpec, + Web3APIMethod, + Web3APIReturnType, + Web3APIPayload, + ProviderConnectInfo, + ProviderRpcError, +} from './web3_api_types'; import { Web3EthExecutionAPI } from './apis/web3_eth_execution_api'; +import { Web3DeferredPromise } from './web3_deferred_promise_type'; const symbol = Symbol.for('web3/base-provider'); +export interface SocketRequestItem< + API extends Web3APISpec, + Method extends Web3APIMethod, + ResponseType, +> { + payload: Web3APIPayload; + deferredPromise: Web3DeferredPromise; +} + // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1193.md#connectivity export type Web3ProviderStatus = 'connecting' | 'connected' | 'disconnected'; export type Web3ProviderEventCallback = ( - error: Error | undefined, + error: Error | ProviderRpcError | undefined, result?: JsonRpcSubscriptionResult | JsonRpcNotification, ) => void; @@ -137,22 +154,23 @@ export abstract class Web3BaseProvider(args: Web3APIPayload): Promise>; // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1193.md#events + + public abstract on( + type: 'disconnect', + callback: Web3ProviderEventCallback, + ): void; public abstract on( - type: 'message' | 'disconnect' | string, + type: 'message' | string, callback: Web3ProviderEventCallback, ): void; public abstract on( type: 'connect' | 'chainChanged', - callback: Web3ProviderEventCallback<{ - readonly [key: string]: unknown; - readonly chainId: string; - }>, + callback: Web3ProviderEventCallback, ): void; public abstract on( type: 'accountsChanged', callback: Web3ProviderEventCallback<{ - readonly [key: string]: unknown; - readonly accountsChanged: string[]; + readonly accounts: string[]; }>, ): void; public abstract removeListener(type: string, callback: Web3ProviderEventCallback): void; @@ -163,7 +181,7 @@ export abstract class Web3BaseProvider. +*/ +import { + EthExecutionAPI, + JsonRpcBatchRequest, + JsonRpcBatchResponse, + JsonRpcId, + JsonRpcNotification, + JsonRpcRequest, + JsonRpcResponse, + JsonRpcResponseWithResult, + JsonRpcResult, + SocketRequestItem, + Web3APIMethod, + Web3APIPayload, + Web3APIReturnType, + Web3APISpec, + Web3ProviderEventCallback, +} from 'web3-types'; +import { + InvalidClientError, + PendingRequestsOnReconnectingError, + RequestAlreadySentError, + ResponseError, + Web3WSProviderError, +} from 'web3-errors'; +import { Eip1193Provider } from './web3_eip1193_provider'; +import { ChunkResponseParser } from './chunk_response_parser'; +import { isNullish } from './validation'; +import { Web3DeferredPromise } from './web3_deferred_promise'; +import * as jsonRpc from './json_rpc'; + +type ReconnectOptions = { + autoReconnect: boolean; + delay: number; + maxAttempts: number; +}; + +type EventType = 'message' | 'connect' | 'disconnect' | 'chainChanged' | 'accountsChanged' | string; + +export abstract class SocketProvider< + MessageEvent, + CloseEvent, + ErrorEvent, + API extends Web3APISpec = EthExecutionAPI, +> extends Eip1193Provider { + protected readonly _socketPath: string; + protected readonly chunkResponseParser: ChunkResponseParser; + /* eslint-disable @typescript-eslint/no-explicit-any */ + protected readonly _pendingRequestsQueue: Map>; + /* eslint-disable @typescript-eslint/no-explicit-any */ + protected readonly _sentRequestsQueue: Map>; + protected _reconnectAttempts!: number; + protected readonly _providerOptions?: object; + protected readonly _reconnectOptions: ReconnectOptions; + protected _socketConnection?: unknown; + protected readonly _onMessageHandler: (event: MessageEvent) => void; + protected readonly _onOpenHandler: () => void; + protected readonly _onCloseHandler: (event: CloseEvent) => void; + protected readonly _onErrorHandler: (event: ErrorEvent) => void; + public constructor(socketPath: string, options?: object, reconnectOptions?: object) { + super(); + this._onMessageHandler = this._onMessage.bind(this); + this._onOpenHandler = this._onConnect.bind(this); + this._onCloseHandler = this._onCloseEvent.bind(this); + this._onErrorHandler = this._onError.bind(this); + if (!this._validateProviderPath(socketPath)) throw new InvalidClientError(socketPath); + + this._socketPath = socketPath; + this._providerOptions = options; + + const DEFAULT_PROVIDER_RECONNECTION_OPTIONS = { + autoReconnect: true, + delay: 5000, + maxAttempts: 5, + }; + + this._reconnectOptions = { + ...DEFAULT_PROVIDER_RECONNECTION_OPTIONS, + ...(reconnectOptions ?? {}), + }; + + this._pendingRequestsQueue = new Map>(); + this._sentRequestsQueue = new Map>(); + + this._init(); + this.connect(); + this.chunkResponseParser = new ChunkResponseParser(); + this.chunkResponseParser.onError(() => { + this._clearQueues(); + }); + } + protected _init() { + this._reconnectAttempts = 0; + } + public abstract connect(): void; + protected abstract _addSocketListeners(): void; + protected abstract _removeSocketListeners(): void; + protected abstract _onCloseEvent(_event: unknown): void; + protected abstract _sendToSocket(_payload: Web3APIPayload): void; + protected abstract _parseResponses(_event: MessageEvent): JsonRpcResponse[]; + protected abstract _clearQueues(_event?: unknown): void; + protected abstract _closeSocketConnection(_code?: number, _data?: string): void; + // eslint-disable-next-line class-methods-use-this + protected _validateProviderPath(path: string): boolean { + return !!path; + } + // eslint-disable-next-line class-methods-use-this + public supportsSubscriptions(): boolean { + return true; + } + public on(type: EventType, callback: Web3ProviderEventCallback): void { + this._eventEmitter.on(type, callback); + } + public once(type: EventType, callback: Web3ProviderEventCallback): void { + this._eventEmitter.once(type, callback); + } + public removeListener(type: EventType, callback: Web3ProviderEventCallback): void { + this._eventEmitter.removeListener(type, callback); + } + + protected _onDisconnect(code?: number, data?: string) { + super._onDisconnect(code, data); + } + public disconnect(code?: number, data?: string): void { + this._removeSocketListeners(); + if (this.getStatus() !== 'disconnected') { + this._closeSocketConnection(code, data); + } + this._onDisconnect(code, data); + } + public removeAllListeners(type: string): void { + this._eventEmitter.removeAllListeners(type); + } + + protected _onError(event: ErrorEvent): void { + this._eventEmitter.emit('error', event); + } + + public reset(): void { + this._sentRequestsQueue.clear(); + this._pendingRequestsQueue.clear(); + + this._init(); + this._removeSocketListeners(); + this._addSocketListeners(); + } + protected _reconnect(): void { + if (this._sentRequestsQueue.size > 0) { + this._sentRequestsQueue.forEach( + (request: SocketRequestItem, key: JsonRpcId) => { + request.deferredPromise.reject(new PendingRequestsOnReconnectingError()); + this._sentRequestsQueue.delete(key); + }, + ); + } + + if (this._reconnectAttempts < this._reconnectOptions.maxAttempts) { + setTimeout(() => { + this._reconnectAttempts += 1; + this._removeSocketListeners(); + this.connect(); + }, this._reconnectOptions.delay); + } + } + + public async request< + Method extends Web3APIMethod, + ResultType = Web3APIReturnType, + >(request: Web3APIPayload): Promise> { + if (isNullish(this._socketConnection)) { + throw new Error('Connection is undefined'); + } + // if socket disconnected - open connection + if (this.getStatus() === 'disconnected') { + this.connect(); + } + + const requestId = jsonRpc.isBatchRequest(request) + ? (request as unknown as JsonRpcBatchRequest)[0].id + : (request as unknown as JsonRpcRequest).id; + + if (!requestId) { + throw new Web3WSProviderError('Request Id not defined'); + } + + if (this._sentRequestsQueue.has(requestId)) { + throw new RequestAlreadySentError(requestId); + } + + const deferredPromise = new Web3DeferredPromise>(); + + const reqItem: SocketRequestItem> = { + payload: request, + deferredPromise, + }; + + if (this.getStatus() === 'connecting') { + this._pendingRequestsQueue.set(requestId, reqItem); + + return reqItem.deferredPromise; + } + + this._sentRequestsQueue.set(requestId, reqItem); + + try { + this._sendToSocket(reqItem.payload); + } catch (error) { + this._sentRequestsQueue.delete(requestId); + throw error; + } + + return deferredPromise; + } + + protected _onConnect() { + this._reconnectAttempts = 0; + super._onConnect(); + this._sendPendingRequests(); + } + + private _sendPendingRequests() { + for (const [id, value] of this._pendingRequestsQueue.entries()) { + this._sendToSocket(value.payload as Web3APIPayload); + this._pendingRequestsQueue.delete(id); + this._sentRequestsQueue.set(id, value); + } + } + + protected _onMessage(event: MessageEvent): void { + const responses = this._parseResponses(event); + if (!responses) { + return; + } + for (const response of responses) { + if ( + jsonRpc.isResponseWithNotification(response as JsonRpcNotification) && + (response as JsonRpcNotification).method.endsWith('_subscription') + ) { + this._eventEmitter.emit('message', undefined, response); + return; + } + + const requestId = jsonRpc.isBatchResponse(response) + ? (response as unknown as JsonRpcBatchResponse)[0].id + : (response as unknown as JsonRpcResponseWithResult).id; + + const requestItem = this._sentRequestsQueue.get(requestId); + + if (!requestItem) { + return; + } + + if (jsonRpc.isBatchResponse(response) || jsonRpc.isResponseWithResult(response)) { + this._eventEmitter.emit('message', undefined, response); + requestItem.deferredPromise.resolve(response); + } else { + this._eventEmitter.emit('message', response, undefined); + requestItem?.deferredPromise.reject(new ResponseError(response)); + } + + this._sentRequestsQueue.delete(requestId); + } + } +} diff --git a/packages/web3-utils/src/web3_eip1193_provider.ts b/packages/web3-utils/src/web3_eip1193_provider.ts new file mode 100644 index 00000000000..74585e7e03a --- /dev/null +++ b/packages/web3-utils/src/web3_eip1193_provider.ts @@ -0,0 +1,118 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ +import { + EthExecutionAPI, + HexString, + Web3APIMethod, + Web3APIPayload, + Web3APISpec, + Web3BaseProvider, +} from 'web3-types'; +import { EventEmitter } from 'events'; +import { toPayload } from './json_rpc'; + +export abstract class Eip1193Provider< + API extends Web3APISpec = EthExecutionAPI, +> extends Web3BaseProvider { + protected readonly _eventEmitter: EventEmitter = new EventEmitter(); + private _chainId: HexString = ''; + private _accounts: HexString[] = []; + + private async _getChainId(): Promise { + const data = await (this as Web3BaseProvider).request< + Web3APIMethod, + ResponseType + >( + toPayload({ + method: 'eth_chainId', + params: [], + }) as Web3APIPayload>, + ); + return data?.result ?? ''; + } + + private async _getAccounts(): Promise { + const data = await (this as Web3BaseProvider).request, HexString[]>( + toPayload({ + method: 'eth_accounts', + params: [], + }) as Web3APIPayload>, + ); + return data?.result ?? []; + } + + protected _onConnect() { + Promise.all([ + this._getChainId() + .then(chainId => { + if (chainId !== this._chainId) { + this._chainId = chainId; + this._eventEmitter.emit('chainChanged', undefined, { + chainId: this._chainId, + }); + } + }) + .catch(err => { + // todo: add error handler + // eslint-disable-next-line no-console + console.error(err); + }), + + this._getAccounts() + .then(accounts => { + if ( + !( + this._accounts.length === accounts.length && + accounts.every(v => accounts.includes(v)) + ) + ) { + this._accounts = accounts; + this._onAccountsChanged(); + } + }) + .catch(err => { + // todo: add error handler + // eslint-disable-next-line no-console + console.error(err); + }), + ]) + .then(() => + this._eventEmitter.emit('connect', undefined, { + chainId: this._chainId, + }), + ) + .catch(err => { + // todo: add error handler + // eslint-disable-next-line no-console + console.error(err); + }); + } + + protected _onDisconnect(code?: number, data?: unknown) { + this._eventEmitter.emit('disconnect', { + code, + data, + }); + } + + private _onAccountsChanged() { + // get chainId and safe to local + this._eventEmitter.emit('accountsChanged', undefined, { + accounts: this._accounts, + }); + } +} diff --git a/packages/web3/test/integration/ipc.test.ts b/packages/web3/test/integration/ipc.test.ts index 8de98d503b4..93da73f5681 100644 --- a/packages/web3/test/integration/ipc.test.ts +++ b/packages/web3/test/integration/ipc.test.ts @@ -21,6 +21,7 @@ import { describeIf, isIpc, closeOpenConnection, + waitForSocketConnect, } from '../shared_fixtures/system_tests_utils'; import Web3 from '../../src/index'; @@ -43,7 +44,7 @@ describe('Web3 instance', () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars expect(web3).toBeInstanceOf(Web3); - await (web3.provider as IpcProvider).waitForConnection(); + await waitForSocketConnect(web3.provider as IpcProvider); expect((web3.provider as IpcProvider).getStatus()).toBe('connected'); }); }); diff --git a/scripts/geth_binary.sh b/scripts/geth_binary.sh index 74cda36b273..33dbdefc373 100755 --- a/scripts/geth_binary.sh +++ b/scripts/geth_binary.sh @@ -21,11 +21,11 @@ getOS(){ getDownloadLink(){ case "$OS" in SOLARIS*) LINK="-" ;; - OSX*) LINK="https://gethstore.blob.core.windows.net/builds/geth-darwin-amd64-1.10.19-23bee162.tar.gz" ;; - LINUX*) LINK="https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.19-23bee162.tar.gz" ;; - BSD*) LINK="https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.19-23bee162.tar.gz" ;; - WINDOWS*) LINK="-" ;; - "ALSO WINDOWS"*) LINK="-" ;; + OSX*) LINK="https://gethstore.blob.core.windows.net/builds/geth-darwin-amd64-1.10.26-e5eb32ac.tar.gz" ;; + LINUX*) LINK="https://gethstore.blob.core.windows.net/builds/geth-linux-386-1.10.26-e5eb32ac.tar.gz" ;; + BSD*) LINK="https://gethstore.blob.core.windows.net/builds/geth-darwin-amd64-1.10.26-e5eb32ac.tar.gz" ;; + WINDOWS*) LINK="https://gethstore.blob.core.windows.net/builds/geth-windows-386-1.10.26-e5eb32ac.exe" ;; + "ALSO WINDOWS"*) LINK="https://gethstore.blob.core.windows.net/builds/geth-windows-386-1.10.26-e5eb32ac.exe" ;; *) LINK="-" ;; esac } diff --git a/scripts/system_tests_utils.ts b/scripts/system_tests_utils.ts index 7eee73e0228..2004a2561ec 100644 --- a/scripts/system_tests_utils.ts +++ b/scripts/system_tests_utils.ts @@ -16,7 +16,7 @@ along with web3.js. If not, see . */ // eslint-disable-next-line import/no-extraneous-dependencies -import { ETH_DATA_FORMAT, format } from 'web3-utils'; +import { ETH_DATA_FORMAT, format, SocketProvider } from 'web3-utils'; // eslint-disable-next-line import/no-extraneous-dependencies import { create, @@ -39,6 +39,11 @@ import { Transaction, Receipt, KeyStore, + ProviderConnectInfo, + Web3ProviderEventCallback, + ProviderRpcError, + JsonRpcSubscriptionResult, + JsonRpcNotification, } from 'web3-types'; // eslint-disable-next-line import/no-extraneous-dependencies import { Personal } from 'web3-eth-personal'; @@ -47,6 +52,7 @@ import Web3 from 'web3'; // eslint-disable-next-line import/no-extraneous-dependencies import { NonPayableMethodObject } from 'web3-eth-contract'; +// eslint-disable-next-line import/no-extraneous-dependencies import accountsString from './accounts.json'; /** @@ -74,6 +80,7 @@ export const isChrome: boolean = getSystemTestEngine() === 'chrome'; export const isFirefox: boolean = getSystemTestEngine() === 'firefox'; export const isElectron: boolean = getSystemTestEngine() === 'electron'; export const isNode: boolean = getSystemTestEngine() === 'isNode'; +export const isSocket: boolean = isWs || isIpc; export const isBrowser: boolean = ['chrome', 'firefox'].includes(getSystemTestEngine()); export const getSystemTestMnemonic = (): string => getEnvVar('WEB3_SYSTEM_TEST_MNEMONIC') ?? ''; @@ -88,8 +95,8 @@ export const itIf = (condition: (() => boolean) | boolean) => export const describeIf = (condition: (() => boolean) | boolean) => (typeof condition === 'function' ? condition() : condition) ? describe : describe.skip; -const maxNumberOfAttempts = 10; -const intervalTime = 5000; // ms +const maxNumberOfAttempts = 100; +const intervalTime = 500; // ms export const waitForOpenConnection = async ( web3Context: Web3Context, @@ -97,7 +104,7 @@ export const waitForOpenConnection = async ( status = 'connected', ) => new Promise((resolve, reject) => { - if (!getSystemTestProvider().startsWith('ws')) { + if (!isSocket) { resolve(); return; } @@ -118,7 +125,7 @@ export const waitForOpenConnection = async ( }); export const closeOpenConnection = async (web3Context: Web3Context) => { - if (!isWs && !isIpc) { + if (!isSocket) { return; } @@ -390,3 +397,28 @@ export const createLocalAccount = async (web3: Web3) => { web3.eth.accounts.wallet.add(account); return account; }; +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +// eslint-disable-next-line arrow-body-style +export const waitForSocketConnect = async (provider: SocketProvider) => { + return new Promise(resolve => { + provider.on('connect', (( + _error: Error | ProviderRpcError | undefined, + data: JsonRpcSubscriptionResult | JsonRpcNotification | undefined, + ) => { + resolve(data as unknown as ProviderConnectInfo); + }) as Web3ProviderEventCallback); + }); +}; + +// eslint-disable-next-line arrow-body-style +export const waitForSocketDisconnect = async (provider: SocketProvider) => { + return new Promise(resolve => { + provider.on('disconnect', (( + _error: ProviderRpcError | Error | undefined, + data: JsonRpcSubscriptionResult | JsonRpcNotification | undefined, + ) => { + resolve(data as unknown as ProviderRpcError); + }) as Web3ProviderEventCallback); + }); +};