From 2e2bcfa94eb0084d986b073cdd353b6c5405dc66 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Thu, 18 Jan 2024 11:18:22 +0100 Subject: [PATCH] refactor: remove axios (#1237) --- packages/ethereum-storage/package.json | 5 +- .../ethereum-storage/src/gas-price-definer.ts | 106 -------- .../etherchain-provider.ts | 76 ------ .../gas-price-providers/etherscan-provider.ts | 84 ------- .../ethgasstation-provider.ts | 75 ------ .../xdai-fixed-provider.ts | 23 -- packages/ethereum-storage/src/index.ts | 1 - packages/ethereum-storage/src/ipfs-manager.ts | 90 ++++--- packages/ethereum-storage/src/ipfs-storage.ts | 14 -- .../test/gas-price-definer.test.ts | 134 ---------- .../etherchain-provider.test.ts | 110 --------- .../etherscan-provider.test.ts | 139 ----------- .../ethgasstation-provider.test.ts | 109 -------- .../xdai-fixed-provider.test.ts | 24 -- .../test/ipfs-manager.test.ts | 39 ++- .../integration-test/test/node-client.test.ts | 7 +- packages/payment-detection/package.json | 1 - .../btc/default-providers/blockchain-info.ts | 7 +- .../btc/default-providers/blockcypher-com.ts | 7 +- .../btc/default-providers/blockstream-info.ts | 7 +- .../src/btc/default-providers/chain-so.ts | 7 +- packages/request-client.js/README.md | 2 - packages/request-client.js/package.json | 3 +- .../request-client.js/src/http-data-access.ts | 136 ++++++---- .../src/http-metamask-data-access.ts | 45 ++-- .../src/http-request-network.ts | 8 +- packages/request-client.js/test/data-test.ts | 24 +- .../test/declarative-payments.test.ts | 172 +++++++------ .../test/http-data-access.test.ts | 46 ++-- .../test/http-request-network.test.ts | 74 +++--- packages/request-client.js/test/index.test.ts | 232 ++++++++---------- packages/request-node/package.json | 1 + .../test/persistTransaction.test.ts | 26 +- packages/toolbox/src/create-request.ts | 6 +- packages/types/src/storage-types.ts | 1 - yarn.lock | 165 +++++++++++-- 36 files changed, 627 insertions(+), 1379 deletions(-) delete mode 100644 packages/ethereum-storage/src/gas-price-definer.ts delete mode 100644 packages/ethereum-storage/src/gas-price-providers/etherchain-provider.ts delete mode 100644 packages/ethereum-storage/src/gas-price-providers/etherscan-provider.ts delete mode 100644 packages/ethereum-storage/src/gas-price-providers/ethgasstation-provider.ts delete mode 100644 packages/ethereum-storage/src/gas-price-providers/xdai-fixed-provider.ts delete mode 100644 packages/ethereum-storage/test/gas-price-definer.test.ts delete mode 100644 packages/ethereum-storage/test/gas-price-providers/etherchain-provider.test.ts delete mode 100644 packages/ethereum-storage/test/gas-price-providers/etherscan-provider.test.ts delete mode 100644 packages/ethereum-storage/test/gas-price-providers/ethgasstation-provider.test.ts delete mode 100644 packages/ethereum-storage/test/gas-price-providers/xdai-fixed-provider.test.ts diff --git a/packages/ethereum-storage/package.json b/packages/ethereum-storage/package.json index d173400b5e..d3943ee091 100644 --- a/packages/ethereum-storage/package.json +++ b/packages/ethereum-storage/package.json @@ -44,11 +44,10 @@ "@requestnetwork/smart-contracts": "0.32.0", "@requestnetwork/types": "0.39.0", "@requestnetwork/utils": "0.39.0", - "axios": "0.27.2", "ethers": "5.5.1", "form-data": "3.0.0", "ipfs-unixfs": "6.0.7", - "qs": "6.10.3", + "qs": "6.11.2", "shelljs": "0.8.5", "tslib": "2.5.0", "yargs": "17.6.2" @@ -56,9 +55,9 @@ "devDependencies": { "@types/jest": "29.5.6", "@types/node": "18.11.9", - "axios-mock-adapter": "1.19.0", "jest": "29.5.0", "jest-junit": "16.0.0", + "msw": "2.0.6", "nyc": "15.1.0", "shx": "0.3.2", "solium": "1.2.5", diff --git a/packages/ethereum-storage/src/gas-price-definer.ts b/packages/ethereum-storage/src/gas-price-definer.ts deleted file mode 100644 index 2a9c20ae5b..0000000000 --- a/packages/ethereum-storage/src/gas-price-definer.ts +++ /dev/null @@ -1,106 +0,0 @@ -import * as config from './config'; -import EtherchainProvider from './gas-price-providers/etherchain-provider'; -import EtherscanProvider from './gas-price-providers/etherscan-provider'; -import EthGasStationProvider from './gas-price-providers/ethgasstation-provider'; - -import { LogTypes, StorageTypes } from '@requestnetwork/types'; - -import { BigNumber } from 'ethers'; -import XDaiFixedProvider from './gas-price-providers/xdai-fixed-provider'; -import { SimpleLogger } from '@requestnetwork/utils'; -import { getEthereumStorageNetworkIdFromName } from './ethereum-utils'; - -/** - * Determines the gas price to use depending on the used network - * Polls gas price API providers if necessary - */ -export class GasPriceDefiner { - private defaultProviders = [ - new EtherchainProvider(), - new EthGasStationProvider(), - new EtherscanProvider(), - ]; - /** - * List of gas price api provider to call to determine the used gas price - * This array is left public for mocking purpose - */ - public gasPriceProviderList: Partial< - Record - > = { - [StorageTypes.EthereumNetwork.MAINNET]: this.defaultProviders, - [StorageTypes.EthereumNetwork.XDAI]: [new XDaiFixedProvider()], - }; - - /** - * Logger instance - */ - private logger: LogTypes.ILogger; - - private readonly gasPriceMin: BigNumber | undefined; - - /** - * Constructor - * @param logger Logger instance - * @param gasPriceMin Minimum gas price to return - */ - public constructor({ - logger, - gasPriceMin, - }: { gasPriceMin?: BigNumber; logger?: LogTypes.ILogger } = {}) { - this.logger = logger || new SimpleLogger(); - this.gasPriceMin = gasPriceMin; - } - - /** - * Get the gas price to use for transaction sending - * - * @param type Gas price type (fast, standard or safe low) - * @param networkName Name of the Ethereum network used that can influence the way to get the gas price - * @returns Big number representing the gas price to use - */ - public async getGasPrice( - type: StorageTypes.GasPriceType, - networkName: string, - ): Promise { - const network = getEthereumStorageNetworkIdFromName(networkName); - if (network) { - const gasPriceArray = await this.pollProviders(type, network); - if (gasPriceArray.length > 0) { - // Get the highest gas price from the providers - const gasPrice = gasPriceArray.reduce( - (currentMax, gasPrice) => (currentMax.gt(gasPrice) ? currentMax : gasPrice), - BigNumber.from(0), - ); - return this.gasPriceMin && gasPrice.lt(this.gasPriceMin) ? this.gasPriceMin : gasPrice; - } else { - this.logger.warn('Cannot determine gas price: There is no available gas price provider', [ - 'ethereum', - ]); - } - } - - return config.getDefaultEthereumGasPrice(); - } - - /** - * Get all gas prices from the APIs - * If request to the API fails, no value is added to the array - * - * @param type Gas price type (fast, standard or safe low) - * @returns Array containing each gas price - */ - public async pollProviders( - type: StorageTypes.GasPriceType, - network: StorageTypes.EthereumNetwork, - ): Promise> { - const providers = this.gasPriceProviderList[network] || []; - const results = await Promise.all( - providers.map((provider) => - provider.getGasPrice(type).catch((err) => this.logger.warn(err, ['ethereum', 'gas'])), - ), - ); - // use a type predicate to make typescript understand that the array cannot contain null - const notNull = (val: T | void | null): val is T => val !== null && val !== undefined; - return results.filter(notNull); - } -} diff --git a/packages/ethereum-storage/src/gas-price-providers/etherchain-provider.ts b/packages/ethereum-storage/src/gas-price-providers/etherchain-provider.ts deleted file mode 100644 index 135612fc41..0000000000 --- a/packages/ethereum-storage/src/gas-price-providers/etherchain-provider.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { StorageTypes } from '@requestnetwork/types'; - -import Axios from 'axios'; - -import { BigNumber } from 'ethers'; -import { retry } from '@requestnetwork/utils'; -import { isGasPriceSafe } from '../ethereum-utils'; - -// Maximum number of api requests to retry when an error is encountered (ECONNRESET, EPIPE, ENOTFOUND) -const ETHERCHAIN_REQUEST_MAX_RETRY = 3; - -// Delay between retries in ms -const ETHERCHAIN_REQUEST_RETRY_DELAY = 100; - -// Multiplier to use to convert the gas price in wei -const API_MULTIPLIER = 1000000000; - -/** - * Retrieves and processes the gas price returned by etherchain.org API - */ -export default class EtherchainProvider implements StorageTypes.IGasPriceProvider { - /** - * Url to connect to the provider API - */ - public providerUrl = 'https://www.etherchain.org/api/gasPriceOracle'; - - /** - * Gets gas price from etherchain.org API - * - * @param type Type of the gas price (fast, standard or safe low) - * @returns Requested gas price - */ - public async getGasPrice(type: StorageTypes.GasPriceType): Promise { - const res = await retry(async () => Axios.get(this.providerUrl), { - maxRetries: ETHERCHAIN_REQUEST_MAX_RETRY, - retryDelay: ETHERCHAIN_REQUEST_RETRY_DELAY, - })(); - - // eslint-disable-next-line no-magic-numbers - if (res.status >= 400) { - throw new Error( - `Etherchain error ${res.status}. Bad response from server ${this.providerUrl}`, - ); - } - const apiResponse = res.data; - - // Check if the API response has the correct format - if ( - !apiResponse.fast || - !apiResponse.standard || - !apiResponse.safeLow || - isNaN(apiResponse.fast) || - isNaN(apiResponse.standard) || - isNaN(apiResponse.safeLow) - ) { - throw new Error(`Etherchain API response doesn't contain the correct format`); - } - - // Retrieve the gas price from the provided gas price type and the format of the API response - const apiGasPrice = BigNumber.from( - parseFloat( - { - [StorageTypes.GasPriceType.FAST]: apiResponse.fast, - [StorageTypes.GasPriceType.STANDARD]: apiResponse.standard, - [StorageTypes.GasPriceType.SAFELOW]: apiResponse.safeLow, - }[type], - ) * API_MULTIPLIER, - ); - - if (!isGasPriceSafe(apiGasPrice)) { - throw Error(`Etherchain provided gas price not safe to use: ${apiGasPrice}`); - } - - return apiGasPrice; - } -} diff --git a/packages/ethereum-storage/src/gas-price-providers/etherscan-provider.ts b/packages/ethereum-storage/src/gas-price-providers/etherscan-provider.ts deleted file mode 100644 index 40272bdeef..0000000000 --- a/packages/ethereum-storage/src/gas-price-providers/etherscan-provider.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { StorageTypes } from '@requestnetwork/types'; - -import Axios from 'axios'; - -import { BigNumber } from 'ethers'; -import { retry } from '@requestnetwork/utils'; -import { isGasPriceSafe } from '../ethereum-utils'; - -// Maximum number of api requests to retry when an error is encountered (ECONNRESET, EPIPE, ENOTFOUND) -const ETHERSCAN_REQUEST_MAX_RETRY = 3; - -// Delay between retries in ms -const ETHERSCAN_REQUEST_RETRY_DELAY = 100; - -// Multiplier to use to convert the gas price in wei -const API_MULTIPLIER = 1000000000; - -/** - * Retrieves and processes the gas price returned by etherscan.io API - */ -export default class EtherscanProvider implements StorageTypes.IGasPriceProvider { - /** - * Url to connect to the provider API - */ - public providerUrl = 'https://api.etherscan.io/api?module=gastracker&action=gasoracle'; - - /** - * Gets gas price from etherscan.io API - * - * @param type Type of the gas price (fast, standard or safe low) - * @returns Requested gas price - */ - public async getGasPrice(type: StorageTypes.GasPriceType): Promise { - const res = await retry(async () => Axios.get(this.providerUrl), { - maxRetries: ETHERSCAN_REQUEST_MAX_RETRY, - retryDelay: ETHERSCAN_REQUEST_RETRY_DELAY, - })(); - - // eslint-disable-next-line no-magic-numbers - if (res.status >= 400) { - throw new Error( - `Etherscan error ${res.status}. Bad response from server ${this.providerUrl}`, - ); - } - const apiResponse = res.data; - - if (apiResponse.status && apiResponse.status !== '1') { - throw new Error(`Etherscan error: ${apiResponse.message} ${apiResponse.result}`); - } - - const { result } = apiResponse; - - // Check if the API response has the correct format - if ( - !result || - !result.FastGasPrice || - !result.ProposeGasPrice || - !result.SafeGasPrice || - isNaN(result.FastGasPrice) || - isNaN(result.ProposeGasPrice) || - isNaN(result.SafeGasPrice) - ) { - throw new Error(`Etherscan API response doesn't contain the correct format`); - } - - // Retrieve the gas price from the provided gas price type and the format of the API response - const apiGasPrice = BigNumber.from( - parseInt( - { - [StorageTypes.GasPriceType.FAST]: result.FastGasPrice, - [StorageTypes.GasPriceType.STANDARD]: result.ProposeGasPrice, - [StorageTypes.GasPriceType.SAFELOW]: result.SafeGasPrice, - }[type], - 10, - ) * API_MULTIPLIER, - ); - - if (!isGasPriceSafe(apiGasPrice)) { - throw Error(`Etherscan provided gas price not safe to use: ${apiGasPrice}`); - } - - return apiGasPrice; - } -} diff --git a/packages/ethereum-storage/src/gas-price-providers/ethgasstation-provider.ts b/packages/ethereum-storage/src/gas-price-providers/ethgasstation-provider.ts deleted file mode 100644 index 65c5146061..0000000000 --- a/packages/ethereum-storage/src/gas-price-providers/ethgasstation-provider.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { StorageTypes } from '@requestnetwork/types'; -import Axios from 'axios'; - -import { BigNumber } from 'ethers'; -import { retry } from '@requestnetwork/utils'; -import { isGasPriceSafe } from '../ethereum-utils'; - -// Maximum number of api requests to retry when an error is encountered (ECONNRESET, EPIPE, ENOTFOUND) -const ETHGASSTATION_REQUEST_MAX_RETRY = 3; - -// Delay between retries in ms -const ETHGASSTATION_RETRY_DELAY = 100; - -// Multiplier to use to convert the gas price in wei -const API_MULTIPLIER = 100000000; - -/** - * Retrieve and process the gas price returned by ethgasstation.org API - */ -export default class EthGasStationProvider implements StorageTypes.IGasPriceProvider { - /** - * Url to connect to the provider API - */ - public providerUrl = 'https://ethgasstation.info/json/ethgasAPI.json'; - - /** - * Gets gas price from ethgasstation.org API - * - * @param type Type of the gas price (fast, standard or safe low) - * @returns Requested gas price - */ - public async getGasPrice(type: StorageTypes.GasPriceType): Promise { - const res = await retry(async () => Axios.get(this.providerUrl), { - maxRetries: ETHGASSTATION_REQUEST_MAX_RETRY, - retryDelay: ETHGASSTATION_RETRY_DELAY, - })(); - - // eslint-disable-next-line no-magic-numbers - if (res.status >= 400) { - throw new Error( - `EthGasStation error ${res.status}. Bad response from server ${this.providerUrl}`, - ); - } - const apiResponse = res.data; - - // Check if the API response has the correct format - if ( - !apiResponse.fast || - !apiResponse.average || - !apiResponse.safeLow || - isNaN(apiResponse.fast) || - isNaN(apiResponse.average) || - isNaN(apiResponse.safeLow) - ) { - throw new Error(`EthGasStation API response doesn't contain the correct format`); - } - - // Retrieve the gas price from the provided gas price type and the format of the API response - const apiGasPrice = BigNumber.from( - parseFloat( - { - [StorageTypes.GasPriceType.FAST]: apiResponse.fast, - [StorageTypes.GasPriceType.STANDARD]: apiResponse.average, - [StorageTypes.GasPriceType.SAFELOW]: apiResponse.safeLow, - }[type], - ) * API_MULTIPLIER, - ); - - if (!isGasPriceSafe(apiGasPrice)) { - throw Error(`EthGasStation provided gas price not safe to use: ${apiGasPrice}`); - } - - return apiGasPrice; - } -} diff --git a/packages/ethereum-storage/src/gas-price-providers/xdai-fixed-provider.ts b/packages/ethereum-storage/src/gas-price-providers/xdai-fixed-provider.ts deleted file mode 100644 index bb7569cdac..0000000000 --- a/packages/ethereum-storage/src/gas-price-providers/xdai-fixed-provider.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { StorageTypes } from '@requestnetwork/types'; - -import { BigNumber, utils } from 'ethers'; - -/** - * Provide a fixed gas price for xDAI. - */ -export class XDaiFixedProvider implements StorageTypes.IGasPriceProvider { - /** - * @param type Type of the gas price (fast, standard or safe low) - * @returns Requested gas price - */ - public async getGasPrice(type: StorageTypes.GasPriceType): Promise { - const basePrice = { - [StorageTypes.GasPriceType.FAST]: 20, - [StorageTypes.GasPriceType.STANDARD]: 10, - [StorageTypes.GasPriceType.SAFELOW]: 2, - }[type]; - return BigNumber.from(utils.parseUnits(basePrice.toString(), 'gwei')); - } -} - -export default XDaiFixedProvider; diff --git a/packages/ethereum-storage/src/index.ts b/packages/ethereum-storage/src/index.ts index 12e3633945..40dbc32440 100644 --- a/packages/ethereum-storage/src/index.ts +++ b/packages/ethereum-storage/src/index.ts @@ -4,6 +4,5 @@ export { } from './ethereum-utils'; export { EthereumStorage } from './ethereum-storage'; export { EthereumTransactionSubmitter } from './ethereum-tx-submitter'; -export { GasPriceDefiner } from './gas-price-definer'; export { GasFeeDefiner } from './gas-fee-definer'; export { IpfsStorage } from './ipfs-storage'; diff --git a/packages/ethereum-storage/src/ipfs-manager.ts b/packages/ethereum-storage/src/ipfs-manager.ts index 8bb7a9a41a..6693fdfa97 100644 --- a/packages/ethereum-storage/src/ipfs-manager.ts +++ b/packages/ethereum-storage/src/ipfs-manager.ts @@ -1,10 +1,8 @@ import { UnixFS } from 'ipfs-unixfs'; import * as qs from 'qs'; -import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios'; import { LogTypes, StorageTypes } from '@requestnetwork/types'; import { getDefaultIpfsTimeout, getDefaultIpfsUrl, getIpfsErrorHandlingConfig } from './config'; -import * as FormData from 'form-data'; import { retry, SimpleLogger } from '@requestnetwork/utils'; /** A mapping between IPFS Paths and the response type */ @@ -30,7 +28,7 @@ export type IpfsOptions = { */ export default class IpfsManager { private readonly logger: LogTypes.ILogger; - private readonly axiosInstance: AxiosInstance; + private readonly httpOptions: { baseURL: string; timeout: number }; private readonly ipfsErrorHandling: StorageTypes.IIpfsErrorHandlingConfiguration; public readonly BASE_PATH: string = 'api/v0'; @@ -47,29 +45,62 @@ export default class IpfsManager { this.ipfsErrorHandling = options?.ipfsErrorHandling || getIpfsErrorHandlingConfig(); this.logger = options?.logger || new SimpleLogger(); - this.axiosInstance = axios.create({ + this.httpOptions = { baseURL: `${ipfsUrl}/${this.BASE_PATH}/`, timeout: ipfsTimeout, - paramsSerializer: function (params) { - return qs.stringify(params, { arrayFormat: 'repeat' }); - }, - }); + }; } - private async ipfs(path: T, config?: AxiosRequestConfig) { - const _post = retry(this.axiosInstance.post, { - context: this.axiosInstance, + private async ipfs( + path: T, + config?: { + data?: unknown; + params?: Record; + timeout?: number; + headers?: Record; + }, + ): Promise { + const url = new URL(path, this.httpOptions.baseURL); + if (config?.params) { + url.search = qs.stringify(config.params, { arrayFormat: 'repeat' }); + } + const timeout = config?.timeout || this.httpOptions.timeout; + + const fetchWithRetry = retry(fetch, { maxRetries: this.ipfsErrorHandling.maxRetries, retryDelay: this.ipfsErrorHandling.delayBetweenRetries, }); try { - const { data, ...rest } = config || {}; - const response = await _post(path, data, rest); - return response.data; + const response = await fetchWithRetry(url.toString(), { + method: 'POST', + body: config?.data + ? config.data instanceof FormData + ? config.data + : JSON.stringify(config?.data) + : undefined, + headers: (config?.headers as Record) || {}, + signal: AbortSignal.timeout(timeout), + }); + + if (!response.ok) { + try { + const json = await response.json(); + if (json?.Message) { + throw new Error(json.Message); + } + } catch { + throw new Error(await response.text()); + } + throw new Error(`${response.status} ${response.statusText}`); + } + + return await response.json(); } catch (e) { - const axiosError = e as AxiosError<{ Message?: string }>; - if (axiosError.isAxiosError && axiosError.response?.data?.Message) { - throw new Error(axiosError.response.data.Message); + if (e.name === 'TimeoutError') { + throw new Error(`timeout of ${timeout}ms exceeded`); + } + if (e.message === 'fetch failed' && e.cause) { + throw e.cause; } throw e; } @@ -96,10 +127,9 @@ export default class IpfsManager { public async add(content: string): Promise { try { const addForm = new FormData(); - addForm.append('file', Buffer.from(content)); + addForm.append('file', new Blob([content])); const response = await this.ipfs('add', { data: addForm, - headers: addForm.getHeaders(), }); // Return the hash of the response const hash = response.Hash; @@ -116,28 +146,12 @@ export default class IpfsManager { /** * Retrieve content from ipfs from its hash * @param hash Hash of the content - * @param maxSize The maximum size of the file to read, in bytes. This is the unixfs file size, as represented on IPFS. * @returns Promise resolving retrieved ipfs object */ - public async read( - hash: string, - maxSize: number = Number.POSITIVE_INFINITY, - ): Promise { + public async read(hash: string): Promise { try { - if (maxSize !== Number.POSITIVE_INFINITY) { - // In order to prevent downloading a file that is too big, we can set maxContentLength in axios options. - // maxContentLength defines the maximum allowed size of the HTTP response in bytes. - // The IPFS HTTP RPC API returns a JSON with some metadata around the actual file itself, so we need to take that into consideration. - const jsonMetadataSize = 500; - // We ask the IPFS node to return a file encoded in base64 to avoid JSON in JSON, and in case of binary data. - // Let's transform the max file size in bytes, to the max length of the base64 string that represents the file. - // This will be our max content length in bytes, since each base64 string characters is encoded as one byte in UTF-8. - const base64StringMaxLength = ((4 * maxSize) / 3 + 3) & ~3; // https://stackoverflow.com/a/32140193/16270345 - maxSize = base64StringMaxLength + jsonMetadataSize; - } const response = await this.ipfs('object/get', { params: { arg: hash, 'data-encoding': 'base64' }, - maxContentLength: maxSize, }); if (response.Type === 'error') { throw new Error(response.Message); @@ -221,8 +235,8 @@ export default class IpfsManager { public async getConfig(): Promise { return { delayBetweenRetries: this.ipfsErrorHandling.delayBetweenRetries, - url: this.axiosInstance.defaults.baseURL || '', - timeout: this.axiosInstance.defaults.timeout, + url: this.httpOptions.baseURL || '', + timeout: this.httpOptions.timeout, id: await this.getIpfsNodeId(), maxRetries: this.ipfsErrorHandling.maxRetries, }; diff --git a/packages/ethereum-storage/src/ipfs-storage.ts b/packages/ethereum-storage/src/ipfs-storage.ts index 8ad9c7a64b..e1fa532775 100644 --- a/packages/ethereum-storage/src/ipfs-storage.ts +++ b/packages/ethereum-storage/src/ipfs-storage.ts @@ -65,20 +65,6 @@ export class IpfsStorage implements StorageTypes.IIpfsStorage { return ipfsSize; } - /** - * Retrieve content from ipfs from its hash - * @param hash Hash of the content - * @param maxSize The maximum size of the file to read - * @returns Promise resolving retrieved ipfs object - */ - public async read(hash: string, maxSize?: number): Promise { - try { - return this.ipfsManager.read(hash, maxSize); - } catch (error) { - throw Error(`Ipfs read request error: ${error}`); - } - } - /** * Gets current configuration */ diff --git a/packages/ethereum-storage/test/gas-price-definer.test.ts b/packages/ethereum-storage/test/gas-price-definer.test.ts deleted file mode 100644 index 8f19e3e317..0000000000 --- a/packages/ethereum-storage/test/gas-price-definer.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* eslint-disable no-magic-numbers */ - -import { StorageTypes } from '@requestnetwork/types'; -import { GasPriceDefiner, getEthereumStorageNetworkNameFromId } from '../src'; - -import * as config from '../src/config'; - -import { BigNumber } from 'ethers'; - -let gasPriceDefiner: GasPriceDefiner; - -describe('GasPriceDefiner', () => { - beforeEach(() => { - gasPriceDefiner = new GasPriceDefiner(); - }); - - describe('getGasPrice', () => { - it('returns default gas price from config if network has no provider', async () => { - gasPriceDefiner.gasPriceProviderList = { - [StorageTypes.EthereumNetwork.MAINNET]: [ - { getGasPrice: () => Promise.resolve(BigNumber.from(1)) }, - ], - }; - const gasPrice = await gasPriceDefiner.getGasPrice( - StorageTypes.GasPriceType.STANDARD, - getEthereumStorageNetworkNameFromId(StorageTypes.EthereumNetwork.RINKEBY), - ); - - expect(gasPrice).toEqual(config.getDefaultEthereumGasPrice()); - }); - - it('returns gas price from appropriate gas provider', async () => { - gasPriceDefiner.gasPriceProviderList = { - [StorageTypes.EthereumNetwork.MAINNET]: [ - { getGasPrice: () => Promise.resolve(BigNumber.from(1)) }, - ], - [StorageTypes.EthereumNetwork.XDAI]: [ - { getGasPrice: () => Promise.resolve(BigNumber.from(2)) }, - ], - }; - const gasPrice = await gasPriceDefiner.getGasPrice( - StorageTypes.GasPriceType.STANDARD, - getEthereumStorageNetworkNameFromId(StorageTypes.EthereumNetwork.XDAI), - ); - - expect(gasPrice).toEqual(BigNumber.from(2)); - }); - - it('returns default gas price from config if no provider is available', async () => { - gasPriceDefiner.pollProviders = async ( - _type: StorageTypes.GasPriceType, - ): Promise => Promise.resolve([]); - const gasPrice = await gasPriceDefiner.getGasPrice( - StorageTypes.GasPriceType.STANDARD, - getEthereumStorageNetworkNameFromId(StorageTypes.EthereumNetwork.MAINNET), - ); - - expect(gasPrice).toEqual(config.getDefaultEthereumGasPrice()); - }); - - it('returns the max of values returned by providers', async () => { - gasPriceDefiner.gasPriceProviderList = { - [StorageTypes.EthereumNetwork.MAINNET]: [ - { getGasPrice: () => Promise.resolve(BigNumber.from(100)) }, - { getGasPrice: () => Promise.resolve(BigNumber.from(200)) }, - { getGasPrice: () => Promise.resolve(BigNumber.from(300)) }, - { getGasPrice: () => Promise.resolve(BigNumber.from(40)) }, - { getGasPrice: () => Promise.resolve(BigNumber.from(300)) }, - ], - }; - - const gasPrice = await gasPriceDefiner.getGasPrice( - StorageTypes.GasPriceType.STANDARD, - getEthereumStorageNetworkNameFromId(StorageTypes.EthereumNetwork.MAINNET), - ); - - expect(gasPrice).toEqual(BigNumber.from(300)); - }); - }); - - describe('pollProviders', () => { - it('returns an array containing value from each provider', async () => { - gasPriceDefiner.gasPriceProviderList = { - [StorageTypes.EthereumNetwork.MAINNET]: [ - { getGasPrice: () => Promise.resolve(BigNumber.from(100)) }, - { getGasPrice: () => Promise.resolve(BigNumber.from(500)) }, - { getGasPrice: () => Promise.resolve(BigNumber.from(200)) }, - { getGasPrice: () => Promise.resolve(BigNumber.from(300000)) }, - ], - }; - - await expect( - gasPriceDefiner.pollProviders( - StorageTypes.GasPriceType.STANDARD, - StorageTypes.EthereumNetwork.MAINNET, - ), - ).resolves.toEqual([ - BigNumber.from(100), - BigNumber.from(500), - BigNumber.from(200), - BigNumber.from(300000), - ]); - }); - - it('returns empty array if there is no provider', async () => { - gasPriceDefiner.gasPriceProviderList = { [StorageTypes.EthereumNetwork.MAINNET]: [] }; - - await expect( - gasPriceDefiner.pollProviders( - StorageTypes.GasPriceType.STANDARD, - StorageTypes.EthereumNetwork.MAINNET, - ), - ).resolves.toHaveLength(0); - }); - - it('handles failures and invalid returns', async () => { - gasPriceDefiner.gasPriceProviderList = { - [StorageTypes.EthereumNetwork.MAINNET]: [ - { getGasPrice: () => Promise.resolve(BigNumber.from(100)) }, - { getGasPrice: () => Promise.reject(new Error('oops!')) }, - { getGasPrice: () => Promise.resolve(BigNumber.from(500)) }, - { getGasPrice: () => Promise.resolve(null) }, - ], - }; - - await expect( - gasPriceDefiner.pollProviders( - StorageTypes.GasPriceType.STANDARD, - StorageTypes.EthereumNetwork.MAINNET, - ), - ).resolves.toEqual([BigNumber.from(100), BigNumber.from(500)]); - }); - }); -}); diff --git a/packages/ethereum-storage/test/gas-price-providers/etherchain-provider.test.ts b/packages/ethereum-storage/test/gas-price-providers/etherchain-provider.test.ts deleted file mode 100644 index 083a27e85e..0000000000 --- a/packages/ethereum-storage/test/gas-price-providers/etherchain-provider.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* eslint-disable no-magic-numbers */ - -import { StorageTypes } from '@requestnetwork/types'; -import EtherchainProvider from '../../src/gas-price-providers/etherchain-provider'; - -import axios from 'axios'; - -import { BigNumber } from 'ethers'; - -let etherchainProvider: EtherchainProvider; - -const apiCorrectResponse = { - fast: '7.0', - safeLow: '1.0', - standard: '3.5', -}; - -const apiIncorrectResponse = { - incorrect: 'response', -}; - -const apiUncompleteResponse = { - fast: '7.0', - safeLow: '1.0', -}; - -const apiNotANumber = { - fast: '7.0', - safeLow: '1.0', - standard: 'not a number', -}; - -const apiNotSafeGasPriceResponse = { - fast: '0', - safeLow: '1.0', - standard: '10000', -}; - -describe('EtherchainProvider', () => { - beforeEach(() => { - etherchainProvider = new EtherchainProvider(); - jest.clearAllMocks(); - }); - - describe('getGasPrice', () => { - it('allows to get the requested gas price', async () => { - jest.spyOn(axios, 'get').mockResolvedValue({ status: 200, data: apiCorrectResponse }); - - // Test with each gas price type - await expect( - etherchainProvider.getGasPrice(StorageTypes.GasPriceType.SAFELOW), - ).resolves.toEqual(BigNumber.from(1000000000)); - - await expect( - etherchainProvider.getGasPrice(StorageTypes.GasPriceType.STANDARD), - ).resolves.toEqual(BigNumber.from(3500000000)); - - await expect(etherchainProvider.getGasPrice(StorageTypes.GasPriceType.FAST)).resolves.toEqual( - BigNumber.from(7000000000), - ); - }); - - it('throws when API is not available', async () => { - jest.spyOn(axios, 'get').mockResolvedValue({ status: 400 }); - - await expect( - etherchainProvider.getGasPrice(StorageTypes.GasPriceType.STANDARD), - ).rejects.toThrowError( - `Etherchain error 400. Bad response from server ${etherchainProvider.providerUrl}`, - ); - }); - - it('throws when API returns a response with the incorrect format', async () => { - jest - .spyOn(axios, 'get') - .mockResolvedValueOnce({ status: 200, data: apiIncorrectResponse }) - .mockResolvedValueOnce({ status: 200, data: apiUncompleteResponse }) - .mockResolvedValueOnce({ status: 200, data: apiNotANumber }); - - // When format is incorrect - await expect( - etherchainProvider.getGasPrice(StorageTypes.GasPriceType.STANDARD), - ).rejects.toThrowError(`Etherchain API response doesn't contain the correct format`); - - // When a field is missing - await expect( - etherchainProvider.getGasPrice(StorageTypes.GasPriceType.STANDARD), - ).rejects.toThrowError(`Etherchain API response doesn't contain the correct format`); - - // When a field is not a number - await expect( - etherchainProvider.getGasPrice(StorageTypes.GasPriceType.STANDARD), - ).rejects.toThrowError(`Etherchain API response doesn't contain the correct format`); - }); - - it('throws when API returns a response with a gas price not safe to use', async () => { - jest.spyOn(axios, 'get').mockResolvedValue({ status: 200, data: apiNotSafeGasPriceResponse }); - - // When over the limit - await expect( - etherchainProvider.getGasPrice(StorageTypes.GasPriceType.STANDARD), - ).rejects.toThrowError(`Etherchain provided gas price not safe to use`); - - // When 0 - await expect( - etherchainProvider.getGasPrice(StorageTypes.GasPriceType.FAST), - ).rejects.toThrowError(`Etherchain provided gas price not safe to use`); - }); - }); -}); diff --git a/packages/ethereum-storage/test/gas-price-providers/etherscan-provider.test.ts b/packages/ethereum-storage/test/gas-price-providers/etherscan-provider.test.ts deleted file mode 100644 index b707c6cb72..0000000000 --- a/packages/ethereum-storage/test/gas-price-providers/etherscan-provider.test.ts +++ /dev/null @@ -1,139 +0,0 @@ -/* eslint-disable no-magic-numbers */ -import { StorageTypes } from '@requestnetwork/types'; -import EtherscanProvider from '../../src/gas-price-providers/etherscan-provider'; - -import axios from 'axios'; - -import { BigNumber } from 'ethers'; - -let etherscanProvider: EtherscanProvider; - -const apiCorrectResponse = { - status: '1', - message: 'OK', - result: { - LastBlock: '10785932', - SafeGasPrice: '10', - ProposeGasPrice: '35', - FastGasPrice: '70', - }, -}; - -const apiIncorrectResponse = { - incorrect: 'response', -}; - -const apiIncompleteResponse = { - status: '1', - message: 'OK', - result: { - FastGasPrice: '70', - SafeGasPrice: '10', - }, -}; - -const apiNotANumber = { - status: '1', - message: 'OK', - result: { - FastGasPrice: '70', - ProposeGasPrice: 'not a number', - SafeGasPrice: '10', - }, -}; - -const apiNotSafeGasPriceResponse = { - status: '1', - message: 'OK', - result: { - FastGasPrice: '0', - ProposeGasPrice: '10000', - SafeGasPrice: '10', - }, -}; - -const apiRateLimitResponse = { - message: 'NOTOK', - result: 'Max rate limit reached, please use API Key for higher rate limit', - status: '0', -}; - -describe('EtherscanProvider', () => { - beforeEach(() => { - etherscanProvider = new EtherscanProvider(); - }); - - describe('getGasPrice', () => { - it('allows to get the requested gas price', async () => { - jest.spyOn(axios, 'get').mockResolvedValue({ status: 200, data: apiCorrectResponse }); - - // Test with each gas price type - await expect( - etherscanProvider.getGasPrice(StorageTypes.GasPriceType.SAFELOW), - ).resolves.toEqual(BigNumber.from(10000000000)); - - await expect( - etherscanProvider.getGasPrice(StorageTypes.GasPriceType.STANDARD), - ).resolves.toEqual(BigNumber.from(35000000000)); - - await expect(etherscanProvider.getGasPrice(StorageTypes.GasPriceType.FAST)).resolves.toEqual( - BigNumber.from(70000000000), - ); - }); - - it('throws when API is not available', async () => { - jest.spyOn(axios, 'get').mockResolvedValue({ status: 400 }); - - await expect( - etherscanProvider.getGasPrice(StorageTypes.GasPriceType.STANDARD), - ).rejects.toThrowError( - `Etherscan error 400. Bad response from server ${etherscanProvider.providerUrl}`, - ); - }); - - it('throws when API returns a response with the incorrect format', async () => { - jest - .spyOn(axios, 'get') - .mockResolvedValueOnce({ status: 200, data: apiIncorrectResponse }) - .mockResolvedValueOnce({ status: 200, data: apiIncompleteResponse }) - .mockResolvedValueOnce({ status: 200, data: apiNotANumber }) - .mockResolvedValueOnce({ status: 200, data: apiRateLimitResponse }); - - // When format is incorrect - await expect( - etherscanProvider.getGasPrice(StorageTypes.GasPriceType.STANDARD), - ).rejects.toThrowError(`Etherscan API response doesn't contain the correct format`); - - // When a field is missing - await expect( - etherscanProvider.getGasPrice(StorageTypes.GasPriceType.STANDARD), - ).rejects.toThrowError(`Etherscan API response doesn't contain the correct format`); - - // When a field is not a number - await expect( - etherscanProvider.getGasPrice(StorageTypes.GasPriceType.STANDARD), - ).rejects.toThrowError(`Etherscan API response doesn't contain the correct format`); - - // When status is not 1 - await expect( - etherscanProvider.getGasPrice(StorageTypes.GasPriceType.STANDARD), - ).rejects.toThrowError( - `Etherscan error: NOTOK Max rate limit reached, please use API Key for higher rate limit`, - ); - }); - - it('throws when API returns a response with a gas price not safe to use', async () => { - jest.spyOn(axios, 'get').mockResolvedValue({ status: 200, data: apiNotSafeGasPriceResponse }); - - // When over the limit - await expect( - etherscanProvider.getGasPrice(StorageTypes.GasPriceType.STANDARD), - ).rejects.toThrowError(`Etherscan provided gas price not safe to use: 10000`); - - // When 0 - await expect( - etherscanProvider.getGasPrice(StorageTypes.GasPriceType.FAST), - ).rejects.toThrowError(`Etherscan provided gas price not safe to use: 0`); - }); - }); -}); diff --git a/packages/ethereum-storage/test/gas-price-providers/ethgasstation-provider.test.ts b/packages/ethereum-storage/test/gas-price-providers/ethgasstation-provider.test.ts deleted file mode 100644 index 52051d1d79..0000000000 --- a/packages/ethereum-storage/test/gas-price-providers/ethgasstation-provider.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* eslint-disable no-magic-numbers */ - -import { StorageTypes } from '@requestnetwork/types'; -import EthGasStationProvider from '../../src/gas-price-providers/ethgasstation-provider'; - -import Axios from 'axios'; - -import { BigNumber } from 'ethers'; - -let ethGasStationProvider: EthGasStationProvider; - -const apiCorrectResponse = { - average: '30.5', - fast: '70', - safeLow: '10', -}; - -const apiIncorrectResponse = { - incorrect: 'response', -}; - -const apiUncompleteResponse = { - fast: '70', - safeLow: '10', -}; - -const apiNotANumber = { - average: 'not a number', - fast: '70', - safeLow: '10', -}; - -const apiNotSafeGasPriceResponse = { - average: '100000', - fast: '0', - safeLow: '10', -}; - -describe('EtherchainProvider', () => { - beforeEach(() => { - ethGasStationProvider = new EthGasStationProvider(); - }); - - describe('getGasPrice', () => { - it('allows to get the requested gas price', async () => { - jest.spyOn(Axios, 'get').mockResolvedValue({ status: 200, data: apiCorrectResponse }); - - // Test with each gas price type - await expect( - ethGasStationProvider.getGasPrice(StorageTypes.GasPriceType.SAFELOW), - ).resolves.toEqual(BigNumber.from(1000000000)); - - await expect( - ethGasStationProvider.getGasPrice(StorageTypes.GasPriceType.STANDARD), - ).resolves.toEqual(BigNumber.from(3050000000)); - - await expect( - ethGasStationProvider.getGasPrice(StorageTypes.GasPriceType.FAST), - ).resolves.toEqual(BigNumber.from(7000000000)); - }); - - it('throws when API is not available', async () => { - jest.spyOn(Axios, 'get').mockResolvedValue({ status: 400 }); - - await expect( - ethGasStationProvider.getGasPrice(StorageTypes.GasPriceType.STANDARD), - ).rejects.toThrowError( - `EthGasStation error 400. Bad response from server ${ethGasStationProvider.providerUrl}`, - ); - }); - - it('throws when API returns a response with the incorrect format', async () => { - jest - .spyOn(Axios, 'get') - .mockResolvedValueOnce({ status: 200, data: apiIncorrectResponse }) - .mockResolvedValueOnce({ status: 200, data: apiUncompleteResponse }) - .mockResolvedValueOnce({ status: 200, data: apiNotANumber }); - - // When format is incorrect - await expect( - ethGasStationProvider.getGasPrice(StorageTypes.GasPriceType.STANDARD), - ).rejects.toThrowError(`EthGasStation API response doesn't contain the correct format`); - - // When a field is missing - await expect( - ethGasStationProvider.getGasPrice(StorageTypes.GasPriceType.STANDARD), - ).rejects.toThrowError(`EthGasStation API response doesn't contain the correct format`); - - // When a field is not a number - await expect( - ethGasStationProvider.getGasPrice(StorageTypes.GasPriceType.STANDARD), - ).rejects.toThrowError(`EthGasStation API response doesn't contain the correct format`); - }); - - it('throws when API returns a response with a gas price not safe to use', async () => { - jest.spyOn(Axios, 'get').mockResolvedValue({ status: 200, data: apiNotSafeGasPriceResponse }); - - // When over the limit - await expect( - ethGasStationProvider.getGasPrice(StorageTypes.GasPriceType.STANDARD), - ).rejects.toThrowError(`EthGasStation provided gas price not safe to use`); - - // When 0 - await expect( - ethGasStationProvider.getGasPrice(StorageTypes.GasPriceType.FAST), - ).rejects.toThrowError(`EthGasStation provided gas price not safe to use`); - }); - }); -}); diff --git a/packages/ethereum-storage/test/gas-price-providers/xdai-fixed-provider.test.ts b/packages/ethereum-storage/test/gas-price-providers/xdai-fixed-provider.test.ts deleted file mode 100644 index eddb5de230..0000000000 --- a/packages/ethereum-storage/test/gas-price-providers/xdai-fixed-provider.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { StorageTypes } from '@requestnetwork/types'; -import { BigNumber } from 'ethers'; -import XDaiFixedProvider from '../../src/gas-price-providers/xdai-fixed-provider'; - -describe('XDaiFixedProvider', () => { - describe('getGasPrice', () => { - it('allows to get the requested gas price', async () => { - const provider = new XDaiFixedProvider(); - - // Test with each gas price type - await expect(provider.getGasPrice(StorageTypes.GasPriceType.SAFELOW)).resolves.toEqual( - BigNumber.from(2000000000), - ); - - await expect(provider.getGasPrice(StorageTypes.GasPriceType.STANDARD)).resolves.toEqual( - BigNumber.from(10000000000), - ); - - await expect(provider.getGasPrice(StorageTypes.GasPriceType.FAST)).resolves.toEqual( - BigNumber.from(20000000000), - ); - }); - }); -}); diff --git a/packages/ethereum-storage/test/ipfs-manager.test.ts b/packages/ethereum-storage/test/ipfs-manager.test.ts index 3b02a03e17..993ae68f95 100644 --- a/packages/ethereum-storage/test/ipfs-manager.test.ts +++ b/packages/ethereum-storage/test/ipfs-manager.test.ts @@ -1,7 +1,7 @@ import { StorageTypes } from '@requestnetwork/types'; import IpfsManager from '../src/ipfs-manager'; -import { AxiosInstance } from 'axios'; -import MockAdapter from 'axios-mock-adapter'; +import { setupServer } from 'msw/node'; +import { HttpResponse, delay, http } from 'msw'; const testErrorHandling: StorageTypes.IIpfsErrorHandlingConfiguration = { delayBetweenRetries: 0, @@ -74,7 +74,7 @@ describe('Ipfs manager', () => { it('allows to read files from ipfs', async () => { await ipfsManager.add(content); - let contentReturned = await ipfsManager.read(hash, 36); + let contentReturned = await ipfsManager.read(hash); expect(contentReturned.content).toBe(content); await ipfsManager.add(content2); @@ -82,15 +82,6 @@ describe('Ipfs manager', () => { expect(contentReturned.content).toBe(content2); }); - it('must throw if max size reached', async () => { - const bigContent = '#'.repeat(550); - const maxSize = 10; - const hash = await ipfsManager.add(bigContent); - await expect(ipfsManager.read(hash, maxSize)).rejects.toThrowError( - `maxContentLength size of 516 exceeded`, - ); - }); - it('allows to get file size from ipfs', async () => { await ipfsManager.add(content); let sizeReturned = await ipfsManager.getContentLength(hash); @@ -128,11 +119,18 @@ describe('Ipfs manager', () => { ipfsManager = new IpfsManager({ ipfsErrorHandling: retryTestErrorHandling, }); - const axiosInstance: AxiosInstance = (ipfsManager as any).axiosInstance; - const axiosInstanceMock = new MockAdapter(axiosInstance); - axiosInstanceMock.onAny().networkError(); - await expect(ipfsManager.read(hash)).rejects.toThrowError('Network Error'); - expect(axiosInstanceMock.history.post.length).toBe(retryTestErrorHandling.maxRetries + 1); + const mock = jest.fn(); + const mockServer = setupServer( + http.all('*', () => { + mock(); + return HttpResponse.error(); + }), + ); + mockServer.listen(); + + await expect(ipfsManager.read(hash)).rejects.toThrowError('Failed to fetch'); + expect(mock).toHaveBeenCalledTimes(retryTestErrorHandling.maxRetries + 1); + mockServer.close(); }); it('timeout errors should generate retry', async () => { @@ -140,11 +138,8 @@ describe('Ipfs manager', () => { ipfsTimeout: 1, ipfsErrorHandling: retryTestErrorHandling, }); - const axiosInstance: AxiosInstance = (ipfsManager as any).axiosInstance; - const axiosInstanceMock = new MockAdapter(axiosInstance); - axiosInstanceMock.onAny().timeout(); + await expect(ipfsManager.add('test')).rejects.toThrowError('timeout of 1ms exceeded'); - expect(axiosInstanceMock.history.post.length).toBe(retryTestErrorHandling.maxRetries + 1); }); it('added and read files should have the same size and content', async () => { @@ -158,7 +153,7 @@ describe('Ipfs manager', () => { const contentSize = Buffer.from(content, 'utf-8').length; const hash = await ipfsManager.add(content); const contentSizeOnIPFS = await ipfsManager.getContentLength(hash); - const contentRead = await ipfsManager.read(hash, contentSizeOnIPFS); + const contentRead = await ipfsManager.read(hash); expect(contentRead.ipfsSize).toEqual(contentSizeOnIPFS); const contentReadSize = Buffer.from(contentRead.content, 'utf-8').length; expect(contentReadSize).toBe(contentSize); diff --git a/packages/integration-test/test/node-client.test.ts b/packages/integration-test/test/node-client.test.ts index f5729bc2c1..03f41e1fbf 100644 --- a/packages/integration-test/test/node-client.test.ts +++ b/packages/integration-test/test/node-client.test.ts @@ -203,7 +203,9 @@ describe('Request client using a request node', () => { signer: payeeIdentity, topics: topicsRequest1and2, }); - await waitForConfirmation(request1); + const confirmedRequest = await request1.waitForConfirmation(); + expect(confirmedRequest.state).toBe('created'); + const timestampBeforeReduce = getCurrentTimestampInSecond(); // make sure that request 2 timestamp is greater than request 1 timestamp @@ -225,7 +227,8 @@ describe('Request client using a request node', () => { topics: topicsRequest1and2, }); - await waitForConfirmation(request2); + const confirmedRequest2 = await waitForConfirmation(request2); + expect(confirmedRequest2.state).toBe('created'); // reduce request 1 const requestDataReduce = await request1.reduceExpectedAmountRequest('10000000', payeeIdentity); diff --git a/packages/payment-detection/package.json b/packages/payment-detection/package.json index 3f25d7cf7c..299c1f34e5 100644 --- a/packages/payment-detection/package.json +++ b/packages/payment-detection/package.json @@ -45,7 +45,6 @@ "@requestnetwork/smart-contracts": "0.32.0", "@requestnetwork/types": "0.39.0", "@requestnetwork/utils": "0.39.0", - "axios": "0.27.2", "ethers": "5.5.1", "graphql": "16.8.1", "graphql-request": "6.1.0", diff --git a/packages/payment-detection/src/btc/default-providers/blockchain-info.ts b/packages/payment-detection/src/btc/default-providers/blockchain-info.ts index 9419633018..450d38f537 100644 --- a/packages/payment-detection/src/btc/default-providers/blockchain-info.ts +++ b/packages/payment-detection/src/btc/default-providers/blockchain-info.ts @@ -1,5 +1,4 @@ import { PaymentTypes } from '@requestnetwork/types'; -import Axios from 'axios'; import { BigNumber } from 'ethers'; import { retry } from '@requestnetwork/utils'; @@ -34,16 +33,16 @@ export class BlockchainInfoProvider implements PaymentTypes.IBitcoinDetectionPro const queryUrl = `${blockchainInfoUrl}/rawaddr/${address}?cors=true`; try { - const res = await retry(async () => Axios.get(queryUrl), { + const res = await retry(fetch, { maxRetries: BLOCKCHAININFO_REQUEST_MAX_RETRY, retryDelay: BLOCKCHAININFO_REQUEST_RETRY_DELAY, - })(); + })(queryUrl); // eslint-disable-next-line no-magic-numbers if (res.status >= 400) { throw new Error(`Error ${res.status}. Bad response from server ${queryUrl}`); } - const addressInfo = res.data; + const addressInfo = await res.json(); // count the number of extra pages to retrieve const numberOfExtraPages = Math.floor(addressInfo.n_tx / (TXS_PER_PAGE + 1)); diff --git a/packages/payment-detection/src/btc/default-providers/blockcypher-com.ts b/packages/payment-detection/src/btc/default-providers/blockcypher-com.ts index e9914b2d0b..0880ba4809 100644 --- a/packages/payment-detection/src/btc/default-providers/blockcypher-com.ts +++ b/packages/payment-detection/src/btc/default-providers/blockcypher-com.ts @@ -1,5 +1,4 @@ import { PaymentTypes } from '@requestnetwork/types'; -import Axios from 'axios'; import { BigNumber } from 'ethers'; import { retry } from '@requestnetwork/utils'; @@ -29,16 +28,16 @@ export class BlockcypherComProvider implements PaymentTypes.IBitcoinDetectionPro const baseUrl = this.getBaseUrl(bitcoinNetworkId); const queryUrl = `${baseUrl}/addrs/${address}`; try { - const res = await retry(async () => Axios.get(queryUrl), { + const res = await retry(fetch, { maxRetries: BLOCKCYPHER_REQUEST_MAX_RETRY, retryDelay: BLOCKCYPHER_REQUEST_RETRY_DELAY, - })(); + })(queryUrl); // eslint-disable-next-line no-magic-numbers if (res.status >= 400) { throw new Error(`Error ${res.status}. Bad response from server ${queryUrl}`); } - const addressInfo = await res.data; + const addressInfo = await res.json(); return this.parse(addressInfo, eventName); } catch (err) { diff --git a/packages/payment-detection/src/btc/default-providers/blockstream-info.ts b/packages/payment-detection/src/btc/default-providers/blockstream-info.ts index 83ec48fa74..9dd03e7a17 100644 --- a/packages/payment-detection/src/btc/default-providers/blockstream-info.ts +++ b/packages/payment-detection/src/btc/default-providers/blockstream-info.ts @@ -1,5 +1,4 @@ import { PaymentTypes } from '@requestnetwork/types'; -import Axios from 'axios'; import { BigNumber } from 'ethers'; import { retry } from '@requestnetwork/utils'; @@ -32,16 +31,16 @@ export class BlockStreamInfoProvider implements PaymentTypes.IBitcoinDetectionPr const baseUrl = this.getBaseUrl(bitcoinNetworkId); const queryUrl = `${baseUrl}/address/${address}/txs`; try { - const res = await retry(async () => Axios.get(queryUrl), { + const res = await retry(fetch, { maxRetries: BLOCKSTREAMINFO_REQUEST_MAX_RETRY, retryDelay: BLOCKSTREAMINFO_REQUEST_RETRY_DELAY, - })(); + })(queryUrl); // eslint-disable-next-line no-magic-numbers if (res.status >= 400) { throw new Error(`Error ${res.status}. Bad response from server ${queryUrl}`); } - let txs: any[] = res.data; + let txs: any[] = await res.json(); let checkForMoreTransactions = txs.length === TXS_PER_PAGE; // if there are 'TXS_PER_PAGE' transactions, need to check the pagination diff --git a/packages/payment-detection/src/btc/default-providers/chain-so.ts b/packages/payment-detection/src/btc/default-providers/chain-so.ts index 8258ce457f..74e00dc3a7 100644 --- a/packages/payment-detection/src/btc/default-providers/chain-so.ts +++ b/packages/payment-detection/src/btc/default-providers/chain-so.ts @@ -1,5 +1,4 @@ import { PaymentTypes } from '@requestnetwork/types'; -import Axios from 'axios'; import * as converterBTC from 'satoshi-bitcoin'; import { BigNumber } from 'ethers'; import { retry } from '@requestnetwork/utils'; @@ -31,16 +30,16 @@ export class ChainSoProvider implements PaymentTypes.IBitcoinDetectionProvider { const queryUrl = `${baseUrl}${address}`; try { - const res = await retry(async () => Axios.get(queryUrl), { + const res = await retry(fetch, { maxRetries: CHAINSO_REQUEST_MAX_RETRY, retryDelay: CHAINSO_REQUEST_RETRY_DELAY, - })(); + })(queryUrl); // eslint-disable-next-line no-magic-numbers if (res.status >= 400) { throw new Error(`Error ${res.status}. Bad response from server ${queryUrl}`); } - const data = res.data; + const data = await res.json(); if (data.status === 'fail') { throw new Error(`Error bad response from ${baseUrl}: ${data.message}`); diff --git a/packages/request-client.js/README.md b/packages/request-client.js/README.md index 2f681081c8..76a07bf96d 100644 --- a/packages/request-client.js/README.md +++ b/packages/request-client.js/README.md @@ -56,8 +56,6 @@ const requestNetwork = new RequestNetwork({ }); ``` -It can be further configured with option from [Axios](https://github.com/axios/axios#request-config). - By default, it uses a local node, on http://localhost:3000. ### Use in development, without a node diff --git a/packages/request-client.js/package.json b/packages/request-client.js/package.json index 3125e31712..c5830400a5 100644 --- a/packages/request-client.js/package.json +++ b/packages/request-client.js/package.json @@ -55,15 +55,14 @@ "@requestnetwork/transaction-manager": "0.30.0", "@requestnetwork/types": "0.39.0", "@requestnetwork/utils": "0.39.0", - "axios": "0.27.2", "ethers": "5.5.1", + "qs": "6.11.2", "tslib": "2.5.0" }, "devDependencies": { "@compodoc/compodoc": "1.1.11", "@types/jest": "29.5.6", "amd-loader": "0.0.8", - "axios-mock-adapter": "1.19.0", "crypto-browserify": "3.12.0", "duplicate-package-checker-webpack-plugin": "3.0.0", "jest": "29.5.0", diff --git a/packages/request-client.js/src/http-data-access.ts b/packages/request-client.js/src/http-data-access.ts index 429c73b457..b277058f11 100644 --- a/packages/request-client.js/src/http-data-access.ts +++ b/packages/request-client.js/src/http-data-access.ts @@ -1,13 +1,15 @@ import { ClientTypes, DataAccessTypes } from '@requestnetwork/types'; -import axios, { AxiosRequestConfig } from 'axios'; import { EventEmitter } from 'events'; import httpConfigDefaults from './http-config-defaults'; import { normalizeKeccak256Hash, retry } from '@requestnetwork/utils'; +import { stringify } from 'qs'; // eslint-disable-next-line @typescript-eslint/no-var-requires const packageJson = require('../package.json'); +export type NodeConnectionConfig = { baseURL: string; headers: Record }; + /** * Exposes a Data-Access module over HTTP */ @@ -19,15 +21,14 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { protected httpConfig: ClientTypes.IHttpDataAccessConfig; /** - * Configuration that will be sent to axios for each request. - * We can also create a AxiosInstance with axios.create() but it dramatically complicates testing. + * Configuration that will be sent at each request. */ - protected axiosConfig: AxiosRequestConfig; + protected nodeConnectionConfig: NodeConnectionConfig; /** * Creates an instance of HttpDataAccess. * @param httpConfig @see ClientTypes.IHttpDataAccessConfig for available options. - * @param nodeConnectionConfig Configuration options to connect to the node. Follows Axios configuration format. + * @param nodeConnectionConfig Configuration options to connect to the node. */ constructor( { @@ -35,7 +36,7 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { nodeConnectionConfig, }: { httpConfig?: Partial; - nodeConnectionConfig?: AxiosRequestConfig; + nodeConnectionConfig?: Partial; } = { httpConfig: {}, nodeConnectionConfig: {}, @@ -47,7 +48,7 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { ...httpConfigDefaults, ...httpConfig, }; - this.axiosConfig = { + this.nodeConnectionConfig = { baseURL: 'http://localhost:3000', headers: { [this.httpConfig.requestClientVersionHeader]: requestClientVersion, @@ -90,53 +91,51 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { // We don't retry this request since it may fail because of a slow Storage // For example, if the Ethereum network is slow and we retry the request three times // three data will be persisted at the end - const { data } = await axios.post( + const data = await this.fetch( + 'POST', '/persistTransaction', - { - channelId, - topics, - transactionData, - }, - this.axiosConfig, + undefined, + { channelId, topics, transactionData }, ); const transactionHash: string = normalizeKeccak256Hash(transactionData).value; // Create the return result with EventEmitter const result: DataAccessTypes.IReturnPersistTransaction = Object.assign( - new EventEmitter(), + new EventEmitter() as DataAccessTypes.PersistTransactionEmitter, data, ); // Try to get the confirmation new Promise((r) => setTimeout(r, this.httpConfig.getConfirmationDeferDelay)) .then(async () => { - const confirmedData = await this.fetchAndRetry( - '/getConfirmedTransaction', - { - transactionHash, - }, - { - maxRetries: this.httpConfig.getConfirmationMaxRetry, - retryDelay: this.httpConfig.getConfirmationRetryDelay, - exponentialBackoffDelay: this.httpConfig.getConfirmationExponentialBackoffDelay, - maxExponentialBackoffDelay: this.httpConfig.getConfirmationMaxExponentialBackoffDelay, - }, - ); + const confirmedData = + await this.fetchAndRetry( + '/getConfirmedTransaction', + { + transactionHash, + }, + { + maxRetries: this.httpConfig.getConfirmationMaxRetry, + retryDelay: this.httpConfig.getConfirmationRetryDelay, + exponentialBackoffDelay: this.httpConfig.getConfirmationExponentialBackoffDelay, + maxExponentialBackoffDelay: this.httpConfig.getConfirmationMaxExponentialBackoffDelay, + }, + ); // when found, emit the event 'confirmed' result.emit('confirmed', confirmedData); }) .catch((e) => { let error: Error = e; - if (e.response.status === 404) { + if (e.status === 404) { error = new Error( `Transaction confirmation not received. Try polling - getTransactionsByChannelId() until the transaction is confirmed. - deferDelay: ${this.httpConfig.getConfirmationDeferDelay}ms, - maxRetries: ${this.httpConfig.getConfirmationMaxRetry}, - retryDelay: ${this.httpConfig.getConfirmationRetryDelay}ms, - exponentialBackoffDelay: ${this.httpConfig.getConfirmationExponentialBackoffDelay}ms, - maxExponentialBackoffDelay: ${this.httpConfig.getConfirmationMaxExponentialBackoffDelay}ms`, + getTransactionsByChannelId() until the transaction is confirmed. + deferDelay: ${this.httpConfig.getConfirmationDeferDelay}ms, + maxRetries: ${this.httpConfig.getConfirmationMaxRetry}, + retryDelay: ${this.httpConfig.getConfirmationRetryDelay}ms, + exponentialBackoffDelay: ${this.httpConfig.getConfirmationExponentialBackoffDelay}ms, + maxExponentialBackoffDelay: ${this.httpConfig.getConfirmationMaxExponentialBackoffDelay}ms`, ); } result.emit('error', error); @@ -155,7 +154,13 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { channelId: string, timestampBoundaries?: DataAccessTypes.ITimestampBoundaries, ): Promise { - return this.fetchAndRetry('/getTransactionsByChannelId', { channelId, timestampBoundaries }); + return await this.fetchAndRetry( + '/getTransactionsByChannelId', + { + channelId, + timestampBoundaries, + }, + ); } /** @@ -168,7 +173,10 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { topic: string, updatedBetween?: DataAccessTypes.ITimestampBoundaries, ): Promise { - return this.fetchAndRetry('/getChannelsByTopic', { topic, updatedBetween }); + return await this.fetchAndRetry('/getChannelsByTopic', { + topic, + updatedBetween, + }); } /** @@ -181,7 +189,10 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { topics: string[], updatedBetween?: DataAccessTypes.ITimestampBoundaries, ): Promise { - return this.fetchAndRetry('/getChannelsByMultipleTopics', { topics, updatedBetween }); + return await this.fetchAndRetry('/getChannelsByMultipleTopics', { + topics, + updatedBetween, + }); } /** @@ -189,7 +200,7 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { * */ public async _getStatus(): Promise { - return this.fetchAndRetry('/information', {}); + return await this.fetchAndRetry('/information', {}); } /** @@ -200,16 +211,16 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { * @param params HTTP GET request parameters * @param retryConfig Maximum retry count, delay between retries, exponential backoff delay, and maximum exponential backoff delay */ - protected async fetchAndRetry( - url: string, - params: any, + protected async fetchAndRetry( + path: string, + params: Record, retryConfig: { maxRetries?: number; retryDelay?: number; exponentialBackoffDelay?: number; maxExponentialBackoffDelay?: number; } = {}, - ): Promise { + ): Promise { retryConfig.maxRetries = retryConfig.maxRetries ?? this.httpConfig.httpRequestMaxRetry; retryConfig.retryDelay = retryConfig.retryDelay ?? this.httpConfig.httpRequestRetryDelay; retryConfig.exponentialBackoffDelay = @@ -217,11 +228,42 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { retryConfig.maxExponentialBackoffDelay = retryConfig.maxExponentialBackoffDelay ?? this.httpConfig.httpRequestMaxExponentialBackoffDelay; - const { data } = await retry( - async () => axios.get(url, { ...this.axiosConfig, params }), - retryConfig, - )(); + return await retry(async () => await this.fetch('GET', path, params), retryConfig)(); + } + + protected async fetch( + method: 'GET' | 'POST', + path: string, + params: Record | undefined, + body?: Record, + ): Promise { + const { baseURL, headers, ...options } = this.nodeConnectionConfig; + const url = new URL(path, baseURL); + if (params) { + // qs.parse doesn't handle well mixes of string and object params + for (const [key, value] of Object.entries(params)) { + if (typeof value === 'object') { + params[key] = JSON.stringify(value); + } + } + url.search = stringify(params); + } + const r = await fetch(url, { + method, + body: body ? JSON.stringify(body) : undefined, + headers: { + 'Content-Type': 'application/json', + ...headers, + }, + ...options, + }); + if (r.ok) { + return await r.json(); + } - return data; + throw Object.assign(new Error(r.statusText), { + status: r.status, + statusText: r.statusText, + }); } } diff --git a/packages/request-client.js/src/http-metamask-data-access.ts b/packages/request-client.js/src/http-metamask-data-access.ts index 9c2de334e6..f3c7ab2777 100644 --- a/packages/request-client.js/src/http-metamask-data-access.ts +++ b/packages/request-client.js/src/http-metamask-data-access.ts @@ -1,11 +1,9 @@ import { Block } from '@requestnetwork/data-access'; import { requestHashSubmitterArtifact } from '@requestnetwork/smart-contracts'; import { ClientTypes, CurrencyTypes, DataAccessTypes, StorageTypes } from '@requestnetwork/types'; -import axios, { AxiosRequestConfig } from 'axios'; import { ethers } from 'ethers'; import { EventEmitter } from 'events'; -import HttpDataAccess from './http-data-access'; -import { retry } from '@requestnetwork/utils'; +import HttpDataAccess, { NodeConnectionConfig } from './http-data-access'; /** * Exposes a Data-Access module over HTTP @@ -27,7 +25,7 @@ export default class HttpMetaMaskDataAccess extends HttpDataAccess { /** * Creates an instance of HttpDataAccess. * @param httpConfig Http config that will be used by the underlying http-data-access. @see ClientTypes.IHttpDataAccessConfig - * @param nodeConnectionConfig Configuration options to connect to the node. Follows Axios configuration format. + * @param nodeConnectionConfig Configuration options to connect to the node. */ constructor( { @@ -37,12 +35,11 @@ export default class HttpMetaMaskDataAccess extends HttpDataAccess { ethereumProviderUrl, }: { httpConfig?: Partial; - nodeConnectionConfig?: AxiosRequestConfig; + nodeConnectionConfig?: NodeConnectionConfig; web3?: any; ethereumProviderUrl?: string; } = { httpConfig: {}, - nodeConnectionConfig: {}, }, ) { super({ httpConfig, nodeConnectionConfig }); @@ -98,9 +95,11 @@ export default class HttpMetaMaskDataAccess extends HttpDataAccess { ); // store the block on ipfs and get the the ipfs hash and size - const { - data: { ipfsHash, ipfsSize }, - } = await axios.post('/ipfsAdd', { data: block }, this.axiosConfig); + const { ipfsHash, ipfsSize } = await this.fetch<{ ipfsHash: string; ipfsSize: number }>( + 'POST', + '/ipfsAdd', + { data: block }, + ); // get the fee required to submit the hash const fee = await submitterContract.getFeesAmount(ipfsSize); @@ -185,32 +184,28 @@ export default class HttpMetaMaskDataAccess extends HttpDataAccess { channelId: string, timestampBoundaries?: DataAccessTypes.ITimestampBoundaries, ): Promise { - const { data } = await retry( - async () => - axios.get( - '/getTransactionsByChannelId', - Object.assign(this.axiosConfig, { - params: { channelId, timestampBoundaries }, - }), - ), + const data = await this.fetchAndRetry( + '/getTransactionsByChannelId', + { + params: { channelId, timestampBoundaries }, + }, { maxRetries: this.httpConfig.httpRequestMaxRetry, retryDelay: this.httpConfig.httpRequestRetryDelay, }, - )(); + ); // get the transactions from the cache - const transactionsCached: DataAccessTypes.IReturnGetTransactions = - this.getCachedTransactionsAndCleanCache( - channelId, - data.meta.transactionsStorageLocation, - timestampBoundaries, - ); + const transactionsCached = this.getCachedTransactionsAndCleanCache( + channelId, + data.meta.transactionsStorageLocation, + timestampBoundaries, + ); // merge cache and data from the node return { meta: { - storageMeta: data.meta.storageMeta.concat(transactionsCached.meta.storageMeta), + storageMeta: data.meta.storageMeta?.concat(transactionsCached.meta.storageMeta ?? []) ?? [], transactionsStorageLocation: data.meta.transactionsStorageLocation.concat( transactionsCached.meta.transactionsStorageLocation, ), diff --git a/packages/request-client.js/src/http-request-network.ts b/packages/request-client.js/src/http-request-network.ts index d66cf24d42..6762f1343d 100644 --- a/packages/request-client.js/src/http-request-network.ts +++ b/packages/request-client.js/src/http-request-network.ts @@ -5,10 +5,9 @@ import { DecryptionProviderTypes, SignatureProviderTypes, } from '@requestnetwork/types'; -import { AxiosRequestConfig } from 'axios'; import { PaymentNetworkOptions } from '@requestnetwork/payment-detection'; import RequestNetwork from './api/request-network'; -import HttpDataAccess from './http-data-access'; +import HttpDataAccess, { NodeConnectionConfig } from './http-data-access'; import { MockDataAccess } from '@requestnetwork/data-access'; import { MockStorage } from './mock-storage'; @@ -20,7 +19,7 @@ export default class HttpRequestNetwork extends RequestNetwork { * Creates an instance of HttpRequestNetwork. * * @param options.httpConfig Http config that will be used by the underlying data-access. @see ClientTypes.IHttpDataAccessConfig for available options. - * @param options.nodeConnectionConfig Configuration options to connect to the node. Follows Axios configuration format. + * @param options.nodeConnectionConfig Configuration options to connect to the node. * @param options.useMockStorage When true, will use a mock storage in memory. Meant to simplify local development and should never be used in production. * @param options.signatureProvider Module to handle the signature. If not given it will be impossible to create new transaction (it requires to sign). * @param options.currencies custom currency list @@ -39,7 +38,7 @@ export default class HttpRequestNetwork extends RequestNetwork { }: { decryptionProvider?: DecryptionProviderTypes.IDecryptionProvider; httpConfig?: Partial; - nodeConnectionConfig?: AxiosRequestConfig; + nodeConnectionConfig?: Partial; signatureProvider?: SignatureProviderTypes.ISignatureProvider; useMockStorage?: boolean; currencies?: CurrencyInput[]; @@ -47,7 +46,6 @@ export default class HttpRequestNetwork extends RequestNetwork { paymentOptions?: Partial; } = { httpConfig: {}, - nodeConnectionConfig: {}, useMockStorage: false, }, ) { diff --git a/packages/request-client.js/test/data-test.ts b/packages/request-client.js/test/data-test.ts index 338a050c7c..53d0f94fb9 100644 --- a/packages/request-client.js/test/data-test.ts +++ b/packages/request-client.js/test/data-test.ts @@ -9,8 +9,8 @@ import { TransactionTypes, } from '@requestnetwork/types'; import { normalizeKeccak256Hash, sign } from '@requestnetwork/utils'; -import AxiosMockAdapter from 'axios-mock-adapter'; -import axios from 'axios'; +import { http, HttpResponse } from 'msw'; +import { setupServer, SetupServer } from 'msw/node'; import { Types } from '../src'; export const arbitraryTimestamp = 1549953337; @@ -270,12 +270,16 @@ export const fakeSignatureProvider: SignatureProviderTypes.ISignatureProvider = supportedMethods: [SignatureTypes.METHOD.ECDSA], }; -export const mockAxiosRequestNode = (): AxiosMockAdapter => { - const mockAxios = new AxiosMockAdapter(axios); - mockAxios.onPost('/persistTransaction').reply(200, { result: {} }); - mockAxios.onGet('/getTransactionsByChannelId').reply(200, { - result: { transactions: [timestampedTransactionWithoutExtensionsData] }, - }); - mockAxios.onGet('/getConfirmedTransaction').reply(200, { result: {} }); - return mockAxios; +export const mockRequestNode = (): SetupServer => { + const mockServer = setupServer( + http.post('*/persistTransaction', () => HttpResponse.json({ result: {} })), + http.get('*/getTransactionsByChannelId', () => + HttpResponse.json({ + result: { transactions: [timestampedTransactionWithoutExtensionsData] }, + }), + ), + http.get('*/getConfirmedTransaction', () => HttpResponse.json({ result: {} })), + ); + mockServer.listen({ onUnhandledRequest: 'bypass' }); + return mockServer; }; diff --git a/packages/request-client.js/test/declarative-payments.test.ts b/packages/request-client.js/test/declarative-payments.test.ts index 828ee8cabe..b06ac6e33b 100644 --- a/packages/request-client.js/test/declarative-payments.test.ts +++ b/packages/request-client.js/test/declarative-payments.test.ts @@ -1,5 +1,3 @@ -import axios from 'axios'; - import { ClientTypes, ExtensionTypes, @@ -8,8 +6,9 @@ import { TransactionTypes, } from '@requestnetwork/types'; import { ethers } from 'ethers'; +import { http, HttpResponse } from 'msw'; +import { setupServer, SetupServer } from 'msw/node'; -import AxiosMockAdapter from 'axios-mock-adapter'; import { RequestNetwork } from '../src/index'; import * as TestData from './data-test'; @@ -36,6 +35,21 @@ const waitForConfirmation = async ( }); }; +const countHttpCalls = (mockServer: SetupServer) => { + const hits: Record = { get: 0, post: 0 }; + + const method = ({ request }: any) => { + if (request.url.startsWith('http://localhost:3000')) { + hits[request.method.toLowerCase()]++; + } + }; + mockServer.events.on('request:start', method); + return { + hits, + unsubscribe: () => mockServer.events.removeListener('request:start', method), + }; +}; + // Integration tests /* eslint-disable @typescript-eslint/no-unused-expressions */ describe('request-client.js: declarative payments', () => { @@ -44,26 +58,24 @@ describe('request-client.js: declarative payments', () => { requestInfo: TestData.parametersWithoutExtensionsData, signer: TestData.payee.identity, }; - let mock: AxiosMockAdapter; + let mockServer: SetupServer; afterEach(() => { jest.clearAllMocks(); - mock.reset(); + mockServer.close(); }); describe(`with simple creation`, () => { beforeEach(() => { - mock = new AxiosMockAdapter(axios); - - const callback = (config: any): any => { - expect(config.baseURL).toBe('http://localhost:3000'); - return [200, {}]; - }; - const spy = jest.fn(callback); - mock.onPost('/persistTransaction').reply(spy); - mock.onGet('/getTransactionsByChannelId').reply(200, { - result: { transactions: [TestData.timestampedTransactionWithoutPaymentInfo] }, - }); - mock.onGet('/getConfirmedTransaction').reply(200, { result: {} }); + mockServer = setupServer( + http.post('*/persistTransaction', () => HttpResponse.json({})), + http.get('*/getTransactionsByChannelId', () => + HttpResponse.json({ + result: { transactions: [TestData.timestampedTransactionWithoutPaymentInfo] }, + }), + ), + http.get('*/getConfirmedTransaction', () => HttpResponse.json({ result: {} })), + ); + mockServer.listen({ onUnhandledRequest: 'bypass' }); }); it('allows to declare a sent payment', async () => { @@ -75,14 +87,15 @@ describe('request-client.js: declarative payments', () => { const request = await requestNetwork.createRequest(requestCreationParams); await request.waitForConfirmation(); - mock.resetHistory(); + const { hits, unsubscribe } = countHttpCalls(mockServer); await waitForConfirmation( request.declareSentPayment('10', 'sent payment', TestData.payer.identity), ); + unsubscribe(); - expect(mock.history.get).toHaveLength(5); - expect(mock.history.post).toHaveLength(1); + expect(hits.get).toBe(5); + expect(hits.post).toBe(1); }); it('allows to declare a received payment', async () => { @@ -94,14 +107,14 @@ describe('request-client.js: declarative payments', () => { const request = await requestNetwork.createRequest(requestCreationParams); await request.waitForConfirmation(); - mock.resetHistory(); - + const { hits, unsubscribe } = countHttpCalls(mockServer); await waitForConfirmation( request.declareReceivedPayment('10', 'received payment', TestData.payee.identity), ); + unsubscribe(); - expect(mock.history.get).toHaveLength(5); - expect(mock.history.post).toHaveLength(1); + expect(hits.get).toBe(5); + expect(hits.post).toBe(1); }); it('allows to create a request with delegate', async () => { @@ -142,8 +155,7 @@ describe('request-client.js: declarative payments', () => { const request = await requestNetwork.createRequest(requestCreationParams); await request.waitForConfirmation(); - mock.resetHistory(); - + const { hits, unsubscribe } = countHttpCalls(mockServer); await waitForConfirmation( request.declareReceivedPayment( '10', @@ -153,9 +165,10 @@ describe('request-client.js: declarative payments', () => { 'mainnet', ), ); + unsubscribe(); - expect(mock.history.get).toHaveLength(5); - expect(mock.history.post).toHaveLength(1); + expect(hits.get).toBe(5); + expect(hits.post).toBe(1); }); it('allows to declare a received payment from delegate', async () => { @@ -186,14 +199,14 @@ describe('request-client.js: declarative payments', () => { const request = await requestNetwork.createRequest(requestCreationParams); await request.waitForConfirmation(); - mock.resetHistory(); - + const { hits, unsubscribe } = countHttpCalls(mockServer); await waitForConfirmation( request.declareSentRefund('10', 'sent refund', TestData.payee.identity), ); + unsubscribe(); - expect(mock.history.get).toHaveLength(5); - expect(mock.history.post).toHaveLength(1); + expect(hits.get).toBe(5); + expect(hits.post).toBe(1); }); it('allows to declare a received refund', async () => { @@ -205,14 +218,14 @@ describe('request-client.js: declarative payments', () => { const request = await requestNetwork.createRequest(requestCreationParams); await request.waitForConfirmation(); - mock.resetHistory(); - + const { hits, unsubscribe } = countHttpCalls(mockServer); await waitForConfirmation( request.declareReceivedRefund('10', 'received refund', TestData.payer.identity), ); + unsubscribe(); - expect(mock.history.get).toHaveLength(5); - expect(mock.history.post).toHaveLength(1); + expect(hits.get).toBe(5); + expect(hits.post).toBe(1); }); it('allows to declare a received refund from delegate', async () => { @@ -243,8 +256,7 @@ describe('request-client.js: declarative payments', () => { const request = await requestNetwork.createRequest(requestCreationParams); await request.waitForConfirmation(); - mock.resetHistory(); - + const { hits, unsubscribe } = countHttpCalls(mockServer); await waitForConfirmation( request.declareReceivedRefund( '10', @@ -253,9 +265,10 @@ describe('request-client.js: declarative payments', () => { '0x123456789', ), ); + unsubscribe(); - expect(mock.history.get).toHaveLength(5); - expect(mock.history.post).toHaveLength(1); + expect(hits.get).toBe(5); + expect(hits.post).toBe(1); }); it('allows to get the right balance', async () => { @@ -397,53 +410,50 @@ describe('request-client.js: declarative payments', () => { describe('other creations', () => { it('can have a payment reference on a declarative payment network', async () => { const salt = 'ea3bc7caf64110ca'; - mock = new AxiosMockAdapter(axios); + const extensionParams = { salt, paymentInfo: { anyInfo: 'anyValue' } }; - const callback = (config: any): any => { - expect(config.baseURL).toBe('http://localhost:3000'); - return [200, {}]; - }; - const spy = jest.fn(callback); - mock.onPost('/persistTransaction').reply(spy); - mock.onGet('/getTransactionsByChannelId').reply(200, { - result: { - transactions: [ - { - ...TestData.timestampedTransactionWithoutPaymentInfo, - transaction: { - data: JSON.stringify({ - state: TransactionTypes.TransactionState.CONFIRMED, - timestamp: TestData.arbitraryTimestamp, + mockServer.use( + http.get('*/getTransactionsByChannelId', () => + HttpResponse.json({ + result: { + transactions: [ + { + ...TestData.timestampedTransactionWithoutPaymentInfo, transaction: { - data: JSON.stringify( - sign( - { - name: RequestLogicTypes.ACTION_NAME.CREATE, - parameters: { - ...TestData.parametersWithDeclarative, - extensionsData: [ - { - action: 'create', - id: 'pn-any-declarative', - parameters: extensionParams, - version: '0.1.0', + data: JSON.stringify({ + state: TransactionTypes.TransactionState.CONFIRMED, + timestamp: TestData.arbitraryTimestamp, + transaction: { + data: JSON.stringify( + sign( + { + name: RequestLogicTypes.ACTION_NAME.CREATE, + parameters: { + ...TestData.parametersWithDeclarative, + extensionsData: [ + { + action: 'create', + id: 'pn-any-declarative', + parameters: extensionParams, + version: '0.1.0', + }, + ], }, - ], - }, - version: '2.0.3', - }, - TestData.payee.signatureParams, - ), - ), + version: '2.0.3', + }, + TestData.payee.signatureParams, + ), + ), + }, + }), }, - }), - }, + }, + ], }, - ], - }, - }); - mock.onGet('/getConfirmedTransaction').reply(200, { result: {} }); + }), + ), + ); const requestNetwork = new RequestNetwork({ httpConfig, diff --git a/packages/request-client.js/test/http-data-access.test.ts b/packages/request-client.js/test/http-data-access.test.ts index f29929f9bd..1f681a8946 100644 --- a/packages/request-client.js/test/http-data-access.test.ts +++ b/packages/request-client.js/test/http-data-access.test.ts @@ -1,40 +1,50 @@ import HttpDataAccess from '../src/http-data-access'; -import MockAdapter from 'axios-mock-adapter'; import * as TestData from './data-test'; +import { SetupServer } from 'msw/node'; +import { http, HttpResponse } from 'msw'; -let mockAxios: MockAdapter; +let mockServer: SetupServer; beforeAll(() => { - mockAxios = TestData.mockAxiosRequestNode(); + mockServer = TestData.mockRequestNode(); }); afterAll(() => { - mockAxios.restore(); + mockServer.close(); + mockServer.resetHandlers(); jest.restoreAllMocks(); }); describe('HttpDataAccess', () => { describe('persistTransaction()', () => { - it('should emmit error', (done) => { - mockAxios.onGet('/getConfirmedTransaction').reply(404, { result: {} }); + it('should emit error', async () => { + mockServer.use( + http.get('*/getConfirmedTransaction', () => + HttpResponse.json({ result: {} }, { status: 404 }), + ), + ); const httpDataAccess = new HttpDataAccess({ httpConfig: { getConfirmationDeferDelay: 0, getConfirmationMaxRetry: 0, }, }); - void httpDataAccess.persistTransaction({}, '', []).then((returnPersistTransaction) => { - returnPersistTransaction.on('error', (e: any) => { - expect(e.message).toBe(`Transaction confirmation not received. Try polling - getTransactionsByChannelId() until the transaction is confirmed. - deferDelay: 0ms, - maxRetries: 0, - retryDelay: 1000ms, - exponentialBackoffDelay: 0ms, - maxExponentialBackoffDelay: 30000ms`); - done(); - }); - }); + await expect( + new Promise((resolve, reject) => + httpDataAccess.persistTransaction({}, '', []).then((returnPersistTransaction) => { + returnPersistTransaction.on('confirmed', resolve); + returnPersistTransaction.on('error', reject); + }), + ), + ).rejects.toThrow( + new Error(`Transaction confirmation not received. Try polling + getTransactionsByChannelId() until the transaction is confirmed. + deferDelay: 0ms, + maxRetries: 0, + retryDelay: 1000ms, + exponentialBackoffDelay: 0ms, + maxExponentialBackoffDelay: 30000ms`), + ); }); }); }); diff --git a/packages/request-client.js/test/http-request-network.test.ts b/packages/request-client.js/test/http-request-network.test.ts index 1a775a8117..5f4c7bb51d 100644 --- a/packages/request-client.js/test/http-request-network.test.ts +++ b/packages/request-client.js/test/http-request-network.test.ts @@ -1,37 +1,49 @@ -import MockAdapter from 'axios-mock-adapter'; +import { http, HttpResponse } from 'msw'; +import { SetupServer } from 'msw/node'; import * as TestData from './data-test'; import HttpRequestNetwork from '../src/http-request-network'; -let mockAxios: MockAdapter; +jest.setTimeout(10_000); + +let mockServer: SetupServer; beforeAll(() => { - mockAxios = TestData.mockAxiosRequestNode(); + mockServer = TestData.mockRequestNode(); }); afterAll(() => { - mockAxios.restore(); + mockServer.close(); + mockServer.resetHandlers(); jest.restoreAllMocks(); }); +afterEach(() => { + mockServer.restoreHandlers(); +}); + describe('HttpRequestNetwork', () => { - describe('should emmit errors throwing on refresh after the confirmation happened', () => { + describe('should emit errors throwing on refresh after the confirmation happened', () => { const failAtCall = (call: number, skipPaymentInfo = false) => { let requestCount = 0; - mockAxios.onGet('/getTransactionsByChannelId').reply(() => { - requestCount++; - return [ - requestCount >= call ? 500 : 200, - { - result: { - transactions: [ - skipPaymentInfo - ? TestData.timestampedTransactionWithoutPaymentInfo - : TestData.timestampedTransaction, - ], + mockServer.use( + http.get('*/getTransactionsByChannelId', () => { + requestCount++; + return HttpResponse.json( + { + result: { + transactions: [ + skipPaymentInfo + ? TestData.timestampedTransactionWithoutPaymentInfo + : TestData.timestampedTransaction, + ], + }, }, - }, - ]; - }); + { + status: requestCount >= call ? 500 : 200, + }, + ); + }), + ); }; const createRequest = async (skipPaymentInfo = false) => { @@ -53,7 +65,7 @@ describe('HttpRequestNetwork', () => { r(e); }); }); - expect(error.message).toBe('Request failed with status code 500'); + expect(error.message).toBe('Internal Server Error'); }; it('create', async () => { @@ -68,7 +80,7 @@ describe('HttpRequestNetwork', () => { await request.waitForConfirmation(); await request.accept(TestData.payer.identity); await checkForError(request); - }, 10000); + }); it('cancel', async () => { failAtCall(6); @@ -76,7 +88,7 @@ describe('HttpRequestNetwork', () => { await request.waitForConfirmation(); await request.cancel(TestData.payee.identity); await checkForError(request); - }, 10000); + }); it('increase the expected amount', async () => { failAtCall(6); @@ -84,7 +96,7 @@ describe('HttpRequestNetwork', () => { await request.waitForConfirmation(); await request.increaseExpectedAmountRequest(3, TestData.payer.identity); await checkForError(request); - }, 10000); + }); it('reduce the expected amount', async () => { failAtCall(6); @@ -92,7 +104,7 @@ describe('HttpRequestNetwork', () => { await request.waitForConfirmation(); await request.reduceExpectedAmountRequest(3, TestData.payee.identity); await checkForError(request); - }, 10000); + }); it('add payment information', async () => { // Skipping payment information at creation and faked storage @@ -101,7 +113,7 @@ describe('HttpRequestNetwork', () => { await request.waitForConfirmation(); await request.addPaymentInformation('payment info added', TestData.payee.identity); await checkForError(request); - }, 10000); + }); it('add refund information', async () => { failAtCall(6); @@ -109,7 +121,7 @@ describe('HttpRequestNetwork', () => { await request.waitForConfirmation(); await request.addRefundInformation('refund info added', TestData.payer.identity); await checkForError(request); - }, 10000); + }); it('declare sent payment', async () => { failAtCall(6); @@ -117,7 +129,7 @@ describe('HttpRequestNetwork', () => { await request.waitForConfirmation(); await request.declareSentPayment('10', 'sent payment', TestData.payer.identity); await checkForError(request); - }, 10000); + }); it('declare sent refund', async () => { failAtCall(6); @@ -125,7 +137,7 @@ describe('HttpRequestNetwork', () => { await request.waitForConfirmation(); await request.declareSentRefund('10', 'sent refund', TestData.payee.identity); await checkForError(request); - }, 10000); + }); it('declare received payment', async () => { failAtCall(6); @@ -133,7 +145,7 @@ describe('HttpRequestNetwork', () => { await request.waitForConfirmation(); await request.declareReceivedPayment('10', 'received payment', TestData.payee.identity); await checkForError(request); - }, 10000); + }); it('declare received refund', async () => { failAtCall(6); @@ -141,7 +153,7 @@ describe('HttpRequestNetwork', () => { await request.waitForConfirmation(); await request.declareReceivedRefund('10', 'received refund', TestData.payer.identity); await checkForError(request); - }, 10000); + }); it('add declarative delegate', async () => { failAtCall(6); @@ -149,6 +161,6 @@ describe('HttpRequestNetwork', () => { await request.waitForConfirmation(); await request.addDeclarativeDelegate(TestData.delegate.identity, TestData.payer.identity); await checkForError(request); - }, 10000); + }); }); }); diff --git a/packages/request-client.js/test/index.test.ts b/packages/request-client.js/test/index.test.ts index 6daea08399..183ad15a98 100644 --- a/packages/request-client.js/test/index.test.ts +++ b/packages/request-client.js/test/index.test.ts @@ -1,5 +1,3 @@ -import axios from 'axios'; - import { ClientTypes, DecryptionProviderTypes, @@ -12,22 +10,21 @@ import { import { decrypt, random32Bytes } from '@requestnetwork/utils'; import { BigNumber, ethers } from 'ethers'; -import AxiosMockAdapter from 'axios-mock-adapter'; import { Request, RequestNetwork, RequestNetworkBase } from '../src/index'; import * as TestData from './data-test'; import * as TestDataRealBTC from './data-test-real-btc'; import { PaymentReferenceCalculator } from '@requestnetwork/payment-detection'; import EtherscanProviderMock from './etherscan-mock'; -import httpConfigDefaults from '../src/http-config-defaults'; import { IRequestDataWithEvents } from '../src/types'; import HttpMetaMaskDataAccess from '../src/http-metamask-data-access'; import { MockDataAccess } from '@requestnetwork/data-access'; import { CurrencyManager } from '@requestnetwork/currency'; import { MockStorage } from '../src/mock-storage'; import * as RequestLogic from '@requestnetwork/types/src/request-logic-types'; - -const packageJson = require('../package.json'); +import { http, HttpResponse } from 'msw'; +import { setupServer, SetupServer } from 'msw/node'; +import config from '../src/http-config-defaults'; const httpConfig: Partial = { getConfirmationDeferDelay: 0, @@ -169,10 +166,16 @@ const waitForConfirmation = async ( /* eslint-disable @typescript-eslint/no-unused-expressions */ describe('request-client.js', () => { afterEach(() => { - jest.clearAllMocks(); + jest.resetAllMocks(); }); describe('API', () => { + const spyPersistTransaction = jest.fn(); + const spyIpfsAdd = jest.fn(); + const spyGetTransactionsByChannelId = jest.fn(); + + let mockServer: SetupServer; + const requestCreationParams: ClientTypes.ICreateRequestParameters = { paymentNetwork: TestData.declarativePaymentNetworkNoPaymentInfo, requestInfo: TestData.parametersWithoutExtensionsData, @@ -181,48 +184,32 @@ describe('request-client.js', () => { const mockedTransactions = { transactions: [TestData.timestampedTransactionWithoutPaymentInfo], }; - it('specify the Request Client version in the header', async () => { - const mock = new AxiosMockAdapter(axios); - - const callback = (config: any): any => { - expect(config.headers[httpConfigDefaults.requestClientVersionHeader]).toBe( - packageJson.version, - ); - return [200, {}]; - }; - const spy = jest.fn(callback); - mock.onPost('/persistTransaction').reply(spy); - mock.onGet('/getTransactionsByChannelId').reply(200, { - result: mockedTransactions, - }); - mock.onGet('/getConfirmedTransaction').reply(200, { result: {} }); - - const requestNetwork = new RequestNetwork({ - httpConfig, - signatureProvider: TestData.fakeSignatureProvider, - paymentOptions: { - bitcoinDetectionProvider: mockBTCProvider, - }, - }); - - const request = await requestNetwork.createRequest(requestCreationParams); - expect(spy).toHaveBeenCalledTimes(1); - await request.waitForConfirmation(); + beforeAll(() => { + mockServer = setupServer( + http.post('*/persistTransaction', ({ request }) => { + if (!request.headers.get(config.requestClientVersionHeader)) { + throw new Error('Missing version header'); + } + return HttpResponse.json(spyPersistTransaction()); + }), + http.get('*/getTransactionsByChannelId', () => + HttpResponse.json(spyGetTransactionsByChannelId()), + ), + http.post('*/ipfsAdd', () => HttpResponse.json(spyIpfsAdd())), + http.get('*/getConfirmedTransaction', () => HttpResponse.json({ result: {} })), + ); + mockServer.listen({ onUnhandledRequest: 'bypass' }); + }); + beforeEach(() => { + spyPersistTransaction.mockReturnValue({}); + spyGetTransactionsByChannelId.mockReturnValue({ result: mockedTransactions }); + }); + afterAll(() => { + mockServer.close(); }); it('uses http://localhost:3000 with signatureProvider and paymentNetwork', async () => { - const mock = new AxiosMockAdapter(axios); - - const callback = (config: any): any => { - expect(config.baseURL).toBe('http://localhost:3000'); - return [200, {}]; - }; - const spy = jest.fn(callback); - mock.onPost('/persistTransaction').reply(spy); - mock.onGet('/getTransactionsByChannelId').reply(200, { result: mockedTransactions }); - mock.onGet('/getConfirmedTransaction').reply(200, { result: {} }); - const requestNetwork = new RequestNetwork({ httpConfig, signatureProvider: TestData.fakeSignatureProvider, @@ -232,25 +219,20 @@ describe('request-client.js', () => { }); const request = await requestNetwork.createRequest(requestCreationParams); - expect(spy).toHaveBeenCalledTimes(1); + expect(spyPersistTransaction).toHaveBeenCalledTimes(1); await request.waitForConfirmation(); }); it('uses http://localhost:3000 with persist from local', async () => { - const mock = new AxiosMockAdapter(axios); - const callback = (): any => { - return [200, { ipfsSize: 100, ipfsHash: 'QmZLqH4EsjmB79gjvyzXWBcihbNBZkw8YuELco84PxGzQY' }]; - }; - // const spyPersistTransaction = jest.fn(); - const spyIpfsAdd = jest.fn(callback); - // mock.onPost('/persistTransaction').reply(spyPersistTransaction); - mock.onPost('/persistTransaction').reply(200, { meta: {}, result: {} }); - mock.onPost('/ipfsAdd').reply(spyIpfsAdd); - mock.onGet('/getTransactionsByChannelId').reply(200, { + spyGetTransactionsByChannelId.mockReturnValue({ meta: { storageMeta: [], transactionsStorageLocation: [] }, result: { transactions: [] }, }); + spyIpfsAdd.mockReturnValue({ + ipfsSize: 100, + ipfsHash: 'QmZLqH4EsjmB79gjvyzXWBcihbNBZkw8YuELco84PxGzQY', + }); const requestNetwork = new RequestNetworkBase({ dataAccess: new HttpMetaMaskDataAccess({ @@ -281,18 +263,11 @@ describe('request-client.js', () => { }); it('uses http://localhost:3000 with signatureProvider and paymentNetwork real btc', async () => { - const mock = new AxiosMockAdapter(axios); - - const callback = (config: any): any => { - expect(config.baseURL).toBe('http://localhost:3000'); - return [200, {}]; - }; - const spy = jest.fn(callback); - mock.onPost('/persistTransaction').reply(spy); - mock.onGet('/getTransactionsByChannelId').reply(200, { - result: { transactions: [TestDataRealBTC.timestampedTransaction] }, + spyGetTransactionsByChannelId.mockReturnValue({ + result: { + transactions: [TestDataRealBTC.timestampedTransaction], + }, }); - mock.onGet('/getConfirmedTransaction').reply(200, { result: {} }); const requestNetwork = new RequestNetwork({ httpConfig, @@ -314,24 +289,17 @@ describe('request-client.js', () => { requestInfo: requestParameters, signer: TestData.payee.identity, }); - expect(spy).toHaveBeenCalledTimes(1); + expect(spyPersistTransaction).toHaveBeenCalledTimes(1); await request.waitForConfirmation(); }); it('uses http://localhost:3000 with signatureProvider', async () => { - const mock = new AxiosMockAdapter(axios); - - const callback = (config: any): any => { - expect(config.baseURL).toBe('http://localhost:3000'); - return [200, {}]; - }; - const spy = jest.fn(callback); - mock.onPost('/persistTransaction').reply(spy); - mock.onGet('/getTransactionsByChannelId').reply(200, { - result: { transactions: [TestData.timestampedTransactionWithoutExtensionsData] }, + spyGetTransactionsByChannelId.mockReturnValue({ + result: { + transactions: [TestData.timestampedTransactionWithoutExtensionsData], + }, }); - mock.onGet('/getConfirmedTransaction').reply(200, { result: {} }); const requestNetwork = new RequestNetwork({ httpConfig, @@ -342,23 +310,16 @@ describe('request-client.js', () => { requestInfo: TestData.parametersWithoutExtensionsData, signer: TestData.payee.identity, }); - expect(spy).toHaveBeenCalledTimes(1); + expect(spyPersistTransaction).toHaveBeenCalledTimes(1); }); it('uses baseUrl given in parameter', async () => { const baseURL = 'http://request.network/api'; - const mock = new AxiosMockAdapter(axios); - - const callback = (config: any): any => { - expect(config.baseURL).toBe(baseURL); - return [200, {}]; - }; - const spy = jest.fn(callback); - mock.onPost('/persistTransaction').reply(spy); - mock.onGet('/getTransactionsByChannelId').reply(200, { - result: { transactions: [TestData.timestampedTransactionWithoutExtensionsData] }, + spyGetTransactionsByChannelId.mockReturnValue({ + result: { + transactions: [TestData.timestampedTransactionWithoutExtensionsData], + }, }); - mock.onGet('/getConfirmedTransaction').reply(200, { result: {} }); const requestNetwork = new RequestNetwork({ httpConfig, @@ -369,15 +330,31 @@ describe('request-client.js', () => { requestInfo: TestData.parametersWithoutExtensionsData, signer: TestData.payee.identity, }); - expect(spy).toHaveBeenCalledTimes(1); + expect(spyPersistTransaction).toHaveBeenCalledTimes(1); await request.waitForConfirmation(); }); }); describe('Request Logic without encryption', () => { + let mockServer: SetupServer; + let hits: Record = {}; + + beforeAll(() => { + mockServer = TestData.mockRequestNode(); + mockServer.events.on('request:start', ({ request }) => { + hits[request.method.toLowerCase()]++; + }); + }); + afterAll(() => { + mockServer.events.removeAllListeners(); + mockServer.resetHandlers(); + mockServer.close(); + }); + beforeEach(() => { + hits = { get: 0, post: 0 }; + }); it('allows to create a request', async () => { - const mock = TestData.mockAxiosRequestNode(); const requestNetwork = new RequestNetwork({ httpConfig, signatureProvider: TestData.fakeSignatureProvider, @@ -391,8 +368,8 @@ describe('request-client.js', () => { expect(request).toBeInstanceOf(Request); expect(request.requestId).toBeDefined(); - expect(mock.history.get).toHaveLength(3); - expect(mock.history.post).toHaveLength(1); + expect(hits.get).toBe(3); + expect(hits.post).toBe(1); // Assert on the length to avoid unnecessary maintenance of the test. 66 = 64 char + '0x' const requestIdLength = 66; @@ -400,21 +377,18 @@ describe('request-client.js', () => { }); it('allows to compute a request id', async () => { - const mock = TestData.mockAxiosRequestNode(); const requestNetwork = new RequestNetwork({ httpConfig, signatureProvider: TestData.fakeSignatureProvider, }); - mock.resetHistory(); - const requestId = await requestNetwork.computeRequestId({ requestInfo: TestData.parametersWithoutExtensionsData, signer: TestData.payee.identity, }); - expect(mock.history.get).toHaveLength(0); - expect(mock.history.post).toHaveLength(0); + expect(hits.get).toBe(0); + expect(hits.post).toBe(0); // Assert on the length to avoid unnecessary maintenance of the test. 66 = 64 char + '0x' const requestIdLength = 66; @@ -422,7 +396,6 @@ describe('request-client.js', () => { }); it('allows to compute a request id, then generate the request with the same id', async () => { - const mock = TestData.mockAxiosRequestNode(); const requestNetwork = new RequestNetwork({ httpConfig, signatureProvider: TestData.fakeSignatureProvider, @@ -445,12 +418,11 @@ describe('request-client.js', () => { expect(request).toBeInstanceOf(Request); expect(request.requestId).toBe(requestId); - expect(mock.history.get).toHaveLength(3); - expect(mock.history.post).toHaveLength(1); + expect(hits.get).toBe(3); + expect(hits.post).toBe(1); }); it('allows to get a request from its ID', async () => { - TestData.mockAxiosRequestNode(); const requestNetwork = new RequestNetwork({ httpConfig, signatureProvider: TestData.fakeSignatureProvider, @@ -503,13 +475,6 @@ describe('request-client.js', () => { }); it('allows to refresh a request', async () => { - const mock = new AxiosMockAdapter(axios); - mock.onPost('/persistTransaction').reply(200, { result: {} }); - mock.onGet('/getTransactionsByChannelId').reply(200, { - result: { transactions: [TestData.timestampedTransactionWithoutExtensionsData] }, - }); - mock.onGet('/getConfirmedTransaction').reply(200, { result: {} }); - const requestNetwork = new RequestNetwork({ httpConfig, signatureProvider: TestData.fakeSignatureProvider, @@ -520,15 +485,16 @@ describe('request-client.js', () => { }); await request.waitForConfirmation(); - mock.resetHistory(); + // reset hits + hits = { get: 0, post: 0 }; const data = await request.refresh(); expect(data).toBeDefined(); expect(data.balance).toBeNull(); expect(data.meta).toBeDefined(); - expect(mock.history.get).toHaveLength(1); - expect(mock.history.post).toHaveLength(0); + expect(hits.get).toBe(1); + expect(hits.post).toBe(0); }); it('works with mocked storage', async () => { @@ -708,7 +674,6 @@ describe('request-client.js', () => { }); it('allows to accept a request', async () => { - const mock = TestData.mockAxiosRequestNode(); const requestNetwork = new RequestNetwork({ httpConfig, signatureProvider: TestData.fakeSignatureProvider, @@ -719,13 +684,13 @@ describe('request-client.js', () => { }); await request.waitForConfirmation(); - mock.resetHistory(); - + // reset hits + hits = { get: 0, post: 0 }; const requestDataWithEvents = await request.accept(TestData.payer.identity); await waitForConfirmation(requestDataWithEvents); - expect(mock.history.get).toHaveLength(5); - expect(mock.history.post).toHaveLength(1); + expect(hits.get).toBe(5); + expect(hits.post).toBe(1); }); it('works with mocked storage emitting error when append an accept', async () => { @@ -771,7 +736,6 @@ describe('request-client.js', () => { }); it('allows to cancel a request', async () => { - const mock = TestData.mockAxiosRequestNode(); const requestNetwork = new RequestNetwork({ httpConfig, signatureProvider: TestData.fakeSignatureProvider, @@ -781,17 +745,15 @@ describe('request-client.js', () => { signer: TestData.payee.identity, }); await request.waitForConfirmation(); - - mock.resetHistory(); - + // reset hits + hits = { get: 0, post: 0 }; await waitForConfirmation(request.cancel(TestData.payee.identity)); - expect(mock.history.get).toHaveLength(5); - expect(mock.history.post).toHaveLength(1); + expect(hits.get).toBe(5); + expect(hits.post).toBe(1); }); it('allows to increase the expected amount a request', async () => { - const mock = TestData.mockAxiosRequestNode(); const requestNetwork = new RequestNetwork({ httpConfig, signatureProvider: TestData.fakeSignatureProvider, @@ -801,17 +763,15 @@ describe('request-client.js', () => { signer: TestData.payee.identity, }); await request.waitForConfirmation(); - - mock.resetHistory(); - + // reset hits + hits = { get: 0, post: 0 }; await waitForConfirmation(request.increaseExpectedAmountRequest(3, TestData.payer.identity)); - expect(mock.history.get).toHaveLength(5); - expect(mock.history.post).toHaveLength(1); + expect(hits.get).toBe(5); + expect(hits.post).toBe(1); }); it('allows to reduce the expected amount a request', async () => { - const mock = TestData.mockAxiosRequestNode(); const requestNetwork = new RequestNetwork({ httpConfig, signatureProvider: TestData.fakeSignatureProvider, @@ -822,12 +782,12 @@ describe('request-client.js', () => { }); await request.waitForConfirmation(); - mock.resetHistory(); - + // reset hits + hits = { get: 0, post: 0 }; await waitForConfirmation(request.reduceExpectedAmountRequest(3, TestData.payee.identity)); - expect(mock.history.get).toHaveLength(5); - expect(mock.history.post).toHaveLength(1); + expect(hits.get).toBe(5); + expect(hits.post).toBe(1); }); }); diff --git a/packages/request-node/package.json b/packages/request-node/package.json index 8a47d7475a..f7ad981d87 100644 --- a/packages/request-node/package.json +++ b/packages/request-node/package.json @@ -71,6 +71,7 @@ "@types/yargs": "17.0.14", "jest": "29.5.0", "jest-junit": "16.0.0", + "msw": "2.0.6", "shx": "0.3.2", "source-map-support": "0.5.19", "supertest": "5.0.0", diff --git a/packages/request-node/test/persistTransaction.test.ts b/packages/request-node/test/persistTransaction.test.ts index bbc0617fbb..e86469f830 100644 --- a/packages/request-node/test/persistTransaction.test.ts +++ b/packages/request-node/test/persistTransaction.test.ts @@ -1,10 +1,9 @@ -import axios from 'axios'; import request from 'supertest'; import { StatusCodes } from 'http-status-codes'; -import MockAdapter from 'axios-mock-adapter'; import { getRequestNode } from '../src/server'; import { RequestNode } from '../src/requestNode'; +import { setupServer } from 'msw/node'; const channelId = '010aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; const anotherChannelId = '010bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'; @@ -19,14 +18,12 @@ const badlyFormattedTransactionData = { not: 'a transaction' }; let requestNodeInstance: RequestNode; let server: any; -const axiosMock = new MockAdapter(axios); +const mockServer = setupServer(); /* eslint-disable no-magic-numbers */ /* eslint-disable @typescript-eslint/no-unused-expressions */ describe('persistTransaction', () => { beforeAll(async () => { - axiosMock.onAny().passThrough(); - requestNodeInstance = getRequestNode(); await requestNodeInstance.initialize(); @@ -36,7 +33,7 @@ describe('persistTransaction', () => { afterAll(async () => { await requestNodeInstance.close(); jest.restoreAllMocks(); - axiosMock.reset(); + mockServer.restoreHandlers(); }); it('responds with status 200 to requests with correct values', async () => { @@ -76,21 +73,4 @@ describe('persistTransaction', () => { .set('Accept', 'application/json') .expect(StatusCodes.INTERNAL_SERVER_ERROR); }); - - it('should catch IPFS timeout error', async () => { - axiosMock.reset(); - axiosMock.onAny().timeout(); - const assertionsNb = 10; - const assertions = []; - for (let i = 0; i < assertionsNb; i++) { - assertions.push( - request(server) - .post('/persistTransaction') - .send({ channelId, topics, transactionData }) - .set('Accept', 'application/json') - .expect(StatusCodes.INTERNAL_SERVER_ERROR), - ); - } - await Promise.all(assertions); - }); }); diff --git a/packages/toolbox/src/create-request.ts b/packages/toolbox/src/create-request.ts index a8ae1d2b46..41dbc89fc9 100644 --- a/packages/toolbox/src/create-request.ts +++ b/packages/toolbox/src/create-request.ts @@ -1,6 +1,5 @@ import { EthereumPrivateKeySignatureProvider } from '@requestnetwork/epk-signature'; import { Request, RequestNetwork, Types } from '@requestnetwork/request-client.js'; -import { AxiosRequestConfig } from 'axios'; export default { createTestRequest, @@ -15,10 +14,7 @@ export default { * * @returns The created request */ -function createTestRequest( - expectedAmount = '1000', - nodeConnectionConfig: AxiosRequestConfig = {}, -): Promise { +function createTestRequest(expectedAmount = '1000', nodeConnectionConfig = {}): Promise { const signatureProvider = new EthereumPrivateKeySignatureProvider({ method: Types.Signature.METHOD.ECDSA, privateKey: '0xc87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3', diff --git a/packages/types/src/storage-types.ts b/packages/types/src/storage-types.ts index 8e71657463..8b312cbaf1 100644 --- a/packages/types/src/storage-types.ts +++ b/packages/types/src/storage-types.ts @@ -67,7 +67,6 @@ export type IIpfsConfig = { export interface IIpfsStorage { initialize: () => Promise; ipfsAdd: (data: string) => Promise; - read(hash: string, maxSize?: number, retries?: number): Promise; getConfig(): Promise; } diff --git a/yarn.lock b/yarn.lock index 3b16b13676..1dedb32393 100644 --- a/yarn.lock +++ b/yarn.lock @@ -891,6 +891,27 @@ resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@bundled-es-modules/cookie@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@bundled-es-modules/cookie/-/cookie-2.0.0.tgz#c3b82703969a61cf6a46e959a012b2c257f6b164" + integrity sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw== + dependencies: + cookie "^0.5.0" + +"@bundled-es-modules/js-levenshtein@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@bundled-es-modules/js-levenshtein/-/js-levenshtein-2.0.1.tgz#b02bbbd546358ab77080a430f0911cfc2b3779c4" + integrity sha512-DERMS3yfbAljKsQc0U2wcqGKUWpdFjwqWuoMugEJlqBnKO180/n+4SR/J8MRDt1AN48X1ovgoD9KrdVXcaa3Rg== + dependencies: + js-levenshtein "^1.1.6" + +"@bundled-es-modules/statuses@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz#761d10f44e51a94902c4da48675b71a76cc98872" + integrity sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg== + dependencies: + statuses "^2.0.1" + "@compodoc/compodoc@1.1.11": version "1.1.11" resolved "https://registry.npmjs.org/@compodoc/compodoc/-/compodoc-1.1.11.tgz" @@ -4213,6 +4234,23 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" +"@mswjs/cookies@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@mswjs/cookies/-/cookies-1.1.0.tgz#1528eb43630caf83a1d75d5332b30e75e9bb1b5b" + integrity sha512-0ZcCVQxifZmhwNBoQIrystCb+2sWBY2Zw8lpfJBPCHGCA/HWqehITeCRVIv4VMy8MPlaHo2w2pTHFV2pFfqKPw== + +"@mswjs/interceptors@^0.25.11": + version "0.25.11" + resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.25.11.tgz#65014cd0951cc4c6916f52d80bdfefe439a62b21" + integrity sha512-27aonWAjdeoZN4j4j6QvePOSOacQUucFRUESAU8FUXsmmagDjmyOi4/NHizxzY/NHSk/HAyqF/IMhl+9puhqNw== + dependencies: + "@open-draft/deferred-promise" "^2.2.0" + "@open-draft/logger" "^0.3.0" + "@open-draft/until" "^2.0.0" + is-node-process "^1.2.0" + outvariant "^1.2.1" + strict-event-emitter "^0.5.1" + "@near-js/accounts@0.1.4": version "0.1.4" resolved "https://registry.yarnpkg.com/@near-js/accounts/-/accounts-0.1.4.tgz#ff557dc65c5064ee4ac2dbfdd39aa3e35ae4d222" @@ -4765,6 +4803,24 @@ dependencies: "@octokit/openapi-types" "^5.3.2" +"@open-draft/deferred-promise@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz#4a822d10f6f0e316be4d67b4d4f8c9a124b073bd" + integrity sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA== + +"@open-draft/logger@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@open-draft/logger/-/logger-0.3.0.tgz#2b3ab1242b360aa0adb28b85f5d7da1c133a0954" + integrity sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ== + dependencies: + is-node-process "^1.2.0" + outvariant "^1.4.0" + +"@open-draft/until@^2.0.0", "@open-draft/until@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-2.1.0.tgz#0acf32f470af2ceaf47f095cdecd40d68666efda" + integrity sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg== + "@openzeppelin/contracts@4.5.0": version "4.5.0" resolved "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.5.0.tgz" @@ -5514,6 +5570,11 @@ dependencies: "@types/node" "*" +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== + "@types/cookiejar@*": version "2.1.2" resolved "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz" @@ -5619,6 +5680,11 @@ expect "^29.0.0" pretty-format "^29.0.0" +"@types/js-levenshtein@^1.1.1": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@types/js-levenshtein/-/js-levenshtein-1.1.3.tgz#a6fd0bdc8255b274e5438e0bfb25f154492d1106" + integrity sha512-jd+Q+sD20Qfu9e2aEXogiO3vpOC1PYJOUdyN9gvs4Qrvkg4wF43L5OhqrPeokdv8TL0/mXoYfpkcoGZMNN2pkQ== + "@types/js-yaml@^4.0.0": version "4.0.1" resolved "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.1.tgz" @@ -5822,6 +5888,11 @@ resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz" integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw== +"@types/statuses@^2.0.1": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/statuses/-/statuses-2.0.4.tgz#041143ba4a918e8f080f8b0ffbe3d4cb514e2315" + integrity sha512-eqNDvZsCNY49OAXB0Firg/Sc2BgoWsntsLUdybGFOhAfCD6QJ2n9HXUIHGqt5qjrxmMv4wS8WLAw43ZkKcJ8Pw== + "@types/strip-bom@^3.0.0": version "3.0.0" resolved "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz" @@ -6932,14 +7003,6 @@ aws4@^1.8.0: resolved "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== -axios-mock-adapter@1.19.0: - version "1.19.0" - resolved "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.19.0.tgz" - integrity sha512-D+0U4LNPr7WroiBDvWilzTMYPYTuZlbo6BI8YHZtj7wYQS8NkARlP9KBt8IWWHTQJ0q/8oZ0ClPBtKCCkx8cQg== - dependencies: - fast-deep-equal "^3.1.3" - is-buffer "^2.0.3" - axios@0.27.2: version "0.27.2" resolved "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz" @@ -8580,7 +8643,7 @@ chalk@4.1.0, chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@4.1.2, chalk@^4.1.1: +chalk@4.1.2, chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -8752,7 +8815,7 @@ cheerio@^1.0.0-rc.3: parse5 "^6.0.0" parse5-htmlparser2-tree-adapter "^6.0.0" -chokidar@3.5.3: +chokidar@3.5.3, chokidar@^3.4.2: version "3.5.3" resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -9403,7 +9466,7 @@ cookie-signature@1.0.6: resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= -cookie@0.5.0: +cookie@0.5.0, cookie@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== @@ -12007,7 +12070,7 @@ fast-deep-equal@^1.0.0: resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz" integrity sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ= -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: +fast-deep-equal@^3.1.1: version "3.1.3" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== @@ -13207,7 +13270,7 @@ graphql-ws@^5.14.0: resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-5.14.2.tgz#7db6f6138717a544d9480f0213f65f2841ed1c52" integrity sha512-LycmCwhZ+Op2GlHz4BZDsUYHKRiiUz+3r9wbhBATMETNlORQJAaFlAgTFoeRh6xQoQegwYwIylVD1Qns9/DA3w== -graphql@16.8.1: +graphql@16.8.1, graphql@^16.8.1: version "16.8.1" resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07" integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw== @@ -13461,6 +13524,11 @@ header-case@^2.0.4: capital-case "^1.0.4" tslib "^2.0.3" +headers-polyfill@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-4.0.2.tgz#9115a76eee3ce8fbf95b6e3c6bf82d936785b44a" + integrity sha512-EWGTfnTqAO2L/j5HZgoM/3z82L7necsJ0pO9Tp0X1wil3PDLrkypTBRgVO2ExehEEvUycejZD3FuRaXpZZc3kw== + heap@0.2.6: version "0.2.6" resolved "https://registry.npmjs.org/heap/-/heap-0.2.6.tgz" @@ -13964,7 +14032,7 @@ inquirer@^6.2.0, inquirer@^6.2.2: strip-ansi "^5.1.0" through "^2.3.6" -inquirer@^8.0.0: +inquirer@^8.0.0, inquirer@^8.2.0: version "8.2.6" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.6.tgz#733b74888195d8d400a67ac332011b5fae5ea562" integrity sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg== @@ -14123,7 +14191,7 @@ is-buffer@^1.1.0, is-buffer@^1.1.5: resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-buffer@^2.0.3, is-buffer@^2.0.5: +is-buffer@^2.0.5: version "2.0.5" resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== @@ -14339,6 +14407,11 @@ is-negative-zero@^2.0.1: resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz" integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== +is-node-process@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-node-process/-/is-node-process-1.2.0.tgz#ea02a1b90ddb3934a19aea414e88edef7e11d134" + integrity sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw== + is-number-object@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz" @@ -15109,6 +15182,11 @@ jose@^5.0.0: resolved "https://registry.yarnpkg.com/jose/-/jose-5.1.3.tgz#303959d85c51b5cb14725f930270b72be56abdca" integrity sha512-GPExOkcMsCLBTi1YetY2LmkoY559fss0+0KVa6kOfb2YFe84nAM7Nm/XzuZozah4iHgmBGrCOHL5/cy670SBRw== +js-levenshtein@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" + integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g== + js-sha256@^0.9.0: version "0.9.0" resolved "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz" @@ -16868,6 +16946,34 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1: resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +msw@2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/msw/-/msw-2.0.6.tgz#635ddfc0908c481f10a5b0c696c3732bc693d615" + integrity sha512-74nN9ADQxHsVx8YGOq5yeDE4bye1mVMAYXojKl0qWraRunTK8UjAVk3wSmOOvVVIDJsyySyjDZ4oYvRuzuMO2g== + dependencies: + "@bundled-es-modules/cookie" "^2.0.0" + "@bundled-es-modules/js-levenshtein" "^2.0.1" + "@bundled-es-modules/statuses" "^1.0.1" + "@mswjs/cookies" "^1.1.0" + "@mswjs/interceptors" "^0.25.11" + "@open-draft/until" "^2.1.0" + "@types/cookie" "^0.4.1" + "@types/js-levenshtein" "^1.1.1" + "@types/statuses" "^2.0.1" + chalk "^4.1.2" + chokidar "^3.4.2" + graphql "^16.8.1" + headers-polyfill "^4.0.1" + inquirer "^8.2.0" + is-node-process "^1.2.0" + js-levenshtein "^1.1.6" + node-fetch "^2.6.7" + outvariant "^1.4.0" + path-to-regexp "^6.2.0" + strict-event-emitter "^0.5.0" + type-fest "^2.19.0" + yargs "^17.3.1" + multibase@^0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/multibase/-/multibase-0.7.0.tgz" @@ -17723,6 +17829,11 @@ osenv@^0.1.4, osenv@^0.1.5: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +outvariant@^1.2.1, outvariant@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/outvariant/-/outvariant-1.4.0.tgz#e742e4bda77692da3eca698ef5bfac62d9fba06e" + integrity sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw== + p-cancelable@^0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz" @@ -18191,6 +18302,11 @@ path-to-regexp@0.1.7: resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= +path-to-regexp@^6.2.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5" + integrity sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw== + path-type@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz" @@ -18767,6 +18883,13 @@ qs@6.10.3, qs@^6.7.0, qs@^6.9.4: dependencies: side-channel "^1.0.4" +qs@6.11.2: + version "6.11.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" + integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== + dependencies: + side-channel "^1.0.4" + qs@~6.5.2: version "6.5.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" @@ -20509,7 +20632,7 @@ static-module@^3.0.2: static-eval "^2.0.5" through2 "~2.0.3" -statuses@2.0.1: +statuses@2.0.1, statuses@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== @@ -20586,6 +20709,11 @@ streamsearch@^1.1.0: resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== +strict-event-emitter@^0.5.0, strict-event-emitter@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz#1602ece81c51574ca39c6815e09f1a3e8550bd93" + integrity sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ== + strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz" @@ -21717,6 +21845,11 @@ type-fest@^0.8.0, type-fest@^0.8.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + type-is@~1.6.18: version "1.6.18" resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz"