diff --git a/package.json b/package.json index 2bebb3df159..df573416dab 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,7 @@ "@coinbase/cbpay-js": "1.0.2", "@dlc-link/dlc-tools": "1.1.1", "@fungible-systems/zone-file": "2.0.0", - "@hirosystems/token-metadata-api-client": "1.1.0", + "@hirosystems/token-metadata-api-client": "1.2.0", "@ledgerhq/hw-transport-webusb": "6.27.19", "@noble/hashes": "1.3.2", "@noble/secp256k1": "2.0.0", diff --git a/src/app/common/api/fetch-wrapper.ts b/src/app/common/api/fetch-wrapper.ts index 4fa0bda3a5b..11b3a4bc1b9 100644 --- a/src/app/common/api/fetch-wrapper.ts +++ b/src/app/common/api/fetch-wrapper.ts @@ -1,34 +1,36 @@ +import axios from 'axios'; + +import { analytics } from '@shared/utils/analytics'; + const leatherHeaders: HeadersInit = { 'x-leather-version': VERSION, }; +function isErrorCode(statusCode: number) { + return statusCode >= 400; +} + +function trackApiError(url: string, statusCode: number) { + void analytics.track('api_error', { origin: new URL(url).origin, statusCode, url }); +} + /** - * @deprecated Use `axios` directly instead + * @deprecated Use `axios` directly instead. Fetch only needed for interation + * with generated stacks blockchain api library */ -export function wrappedFetch(input: RequestInfo, init: RequestInit = {}) { +export async function wrappedFetch(input: RequestInfo, init: RequestInit = {}) { const initHeaders = init.headers || {}; // eslint-disable-next-line no-restricted-globals - return fetch(input, { - credentials: 'omit', + const resp = await fetch(input, { ...init, + credentials: 'omit', headers: { ...initHeaders, ...leatherHeaders }, }); + if (isErrorCode(resp.status)) trackApiError(resp.url, resp.status); + return resp; } -export async function fetchWithTimeout( - input: RequestInfo, - init: RequestInit & { timeout?: number } = {} -) { - const { timeout = 8000, ...options } = init; - - const controller = new AbortController(); - const id = setTimeout(() => controller.abort(), timeout); - - const response = await wrappedFetch(input, { - ...options, - signal: controller.signal, - }); - clearTimeout(id); - +axios.interceptors.response.use(response => { + if (isErrorCode(response.status)) trackApiError(response.config.url ?? '', response.status); return response; -} +}); diff --git a/src/app/query/bitcoin/bitcoin-client.ts b/src/app/query/bitcoin/bitcoin-client.ts index e11a141655d..0d536727b8e 100644 --- a/src/app/query/bitcoin/bitcoin-client.ts +++ b/src/app/query/bitcoin/bitcoin-client.ts @@ -1,7 +1,5 @@ import axios from 'axios'; -import { fetchData } from '../utils'; - class Configuration { constructor(public baseUrl: string) {} } @@ -26,20 +24,15 @@ class AddressApi { constructor(public configuration: Configuration) {} async getTransactionsByAddress(address: string) { - return fetchData({ - errorMsg: 'No transactions fetched', - url: `${this.configuration.baseUrl}/address/${address}/txs`, - }); + const resp = await axios.get(`${this.configuration.baseUrl}/address/${address}/txs`); + return resp.data; } async getUtxosByAddress(address: string): Promise { - return fetchData({ - errorMsg: 'No UTXOs fetched', - url: `${this.configuration.baseUrl}/address/${address}/utxo`, - }).then((utxos: UtxoResponseItem[]) => - // Sort by vout as blockstream API returns them inconsistently - utxos.sort((a, b) => a.vout - b.vout) + const resp = await axios.get( + `${this.configuration.baseUrl}/address/${address}/utxo` ); + return resp.data.sort((a, b) => a.vout - b.vout); } } @@ -77,32 +70,28 @@ class FeeEstimatesApi { constructor(public configuration: Configuration) {} async getFeeEstimatesFromBlockcypherApi(network: string): Promise { - return fetchData({ - errorMsg: 'No fee estimates fetched', - url: `https://api.blockcypher.com/v1/btc/${network}`, - }).then((resp: FeeEstimateEarnApiResponse) => { - const { low_fee_per_kb, medium_fee_per_kb, high_fee_per_kb } = resp; - // These fees are in satoshis per kb - return { - slow: low_fee_per_kb / 1000, - medium: medium_fee_per_kb / 1000, - fast: high_fee_per_kb / 1000, - }; - }); + const resp = await axios.get( + `https://api.blockcypher.com/v1/btc/${network}` + ); + const { low_fee_per_kb, medium_fee_per_kb, high_fee_per_kb } = resp.data; + // These fees are in satoshis per kb + return { + slow: low_fee_per_kb / 1000, + medium: medium_fee_per_kb / 1000, + fast: high_fee_per_kb / 1000, + }; } async getFeeEstimatesFromMempoolSpaceApi(): Promise { - return fetchData({ - errorMsg: 'No fee estimates fetched', - url: ` https://mempool.space/api/v1/fees/recommended`, - }).then((resp: FeeEstimateMempoolSpaceApiResponse) => { - const { fastestFee, halfHourFee, hourFee } = resp; - return { - slow: hourFee, - medium: halfHourFee, - fast: fastestFee, - }; - }); + const resp = await axios.get( + `https://mempool.space/api/v1/fees/recommended` + ); + const { fastestFee, halfHourFee, hourFee } = resp.data; + return { + slow: hourFee, + medium: halfHourFee, + fast: fastestFee, + }; } } diff --git a/src/app/query/bitcoin/ordinals/inscriptions.query.ts b/src/app/query/bitcoin/ordinals/inscriptions.query.ts index 7d91fdfe325..16b05dd3edf 100644 --- a/src/app/query/bitcoin/ordinals/inscriptions.query.ts +++ b/src/app/query/bitcoin/ordinals/inscriptions.query.ts @@ -1,13 +1,13 @@ import { useCallback, useEffect } from 'react'; import { useInfiniteQuery } from '@tanstack/react-query'; +import axios from 'axios'; import { HIRO_INSCRIPTIONS_API_URL } from '@shared/constants'; import { getTaprootAddress } from '@shared/crypto/bitcoin/bitcoin.utils'; import { InscriptionResponseItem } from '@shared/models/inscription.model'; import { ensureArray } from '@shared/utils'; -import { wrappedFetch } from '@app/common/api/fetch-wrapper'; import { createNumArrayOfRange } from '@app/common/utils'; import { QueryPrefixes } from '@app/query/query-prefixes'; import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; @@ -41,11 +41,10 @@ async function fetchInscriptions(addresses: string | string[], offset = 0, limit ensureArray(addresses).forEach(address => params.append('address', address)); params.append('limit', limit.toString()); params.append('offset', offset.toString()); - - const res = await wrappedFetch(`${HIRO_INSCRIPTIONS_API_URL}?${params.toString()}`); - if (!res.ok) throw new Error('Error retrieving inscription metadata'); - const data = await res.json(); - return data as InscriptionsQueryResponse; + const res = await axios.get( + `${HIRO_INSCRIPTIONS_API_URL}?${params.toString()}` + ); + return res.data; } /** diff --git a/src/app/query/stacks/fees/fees.query.ts b/src/app/query/stacks/fees/fees.query.ts index e0e0d540308..6cda35fc37d 100644 --- a/src/app/query/stacks/fees/fees.query.ts +++ b/src/app/query/stacks/fees/fees.query.ts @@ -1,8 +1,8 @@ import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; import { StacksTxFeeEstimation } from '@shared/models/fees/stacks-fees.model'; -import { wrappedFetch } from '@app/common/api/fetch-wrapper'; import { AppUseQueryConfig } from '@app/query/query-config'; import { useCurrentNetworkState } from '@app/store/networks/networks.hooks'; @@ -11,16 +11,14 @@ import { RateLimiter, useHiroApiRateLimiter } from '../rate-limiter'; function fetchTransactionFeeEstimation(currentNetwork: any, limiter: RateLimiter) { return async (estimatedLen: number | null, transactionPayload: string) => { await limiter.removeTokens(1); - const resp = await wrappedFetch(currentNetwork.chain.stacks.url + '/v2/fees/transaction', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ + const resp = await axios.post( + currentNetwork.chain.stacks.url + '/v2/fees/transaction', + { estimated_len: estimatedLen, transaction_payload: transactionPayload, - }), - }); - const data = await resp.json(); - return data as StacksTxFeeEstimation; + } + ); + return resp.data; }; } diff --git a/src/app/query/stacks/network/network.query.ts b/src/app/query/stacks/network/network.query.ts index 1e0af93804c..405b54edef4 100644 --- a/src/app/query/stacks/network/network.query.ts +++ b/src/app/query/stacks/network/network.query.ts @@ -1,6 +1,5 @@ import { useQuery } from '@tanstack/react-query'; - -import { fetchWithTimeout } from '@app/common/api/fetch-wrapper'; +import axios from 'axios'; import { RateLimiter, useHiroApiRateLimiter } from '../rate-limiter'; @@ -16,7 +15,8 @@ const networkStatusQueryOptions = { async function getNetworkStatusFetcher(url: string, limiter: RateLimiter) { await limiter.removeTokens(1); - return fetchWithTimeout(url, { timeout: 4500 }); + const resp = await axios.get(url, { timeout: 4500 }); + return resp.data; } export function useGetNetworkStatus(url: string) { diff --git a/src/app/query/utils.ts b/src/app/query/utils.ts deleted file mode 100644 index 02827bf384e..00000000000 --- a/src/app/query/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { wrappedFetch } from '@app/common/api/fetch-wrapper'; - -interface FetchDataArgs { - errorMsg: string; - url: string; -} - -export async function fetchData({ errorMsg, url }: FetchDataArgs) { - const response = await wrappedFetch(url); - if (!response.ok) { - throw new Error(errorMsg); - } - return response.json(); -} diff --git a/tests/specs/onboarding/onboarding.spec.ts b/tests/specs/onboarding/onboarding.spec.ts index 0dd20301c1f..036c5bcf109 100644 --- a/tests/specs/onboarding/onboarding.spec.ts +++ b/tests/specs/onboarding/onboarding.spec.ts @@ -47,6 +47,7 @@ test.describe('Onboarding an existing user', () => { await test.expect(error).toBeVisible(); await test.expect(signInButton).toBeDisabled(); }); + test('mnemonic key validation: should not show error for valid mnemonic key words', async ({ extensionId, globalPage, @@ -56,9 +57,7 @@ test.describe('Onboarding an existing user', () => { // enter some key partial const validPartialKey = 'shoulder any pencil'; await onboardingPage.signInMnemonicKey(validPartialKey); - const signInSeedError = await onboardingPage.page.getByTestId( - OnboardingSelectors.SignInSeedError - ); + const signInSeedError = onboardingPage.page.getByTestId(OnboardingSelectors.SignInSeedError); await test.expect(signInSeedError).not.toBeVisible(); }); diff --git a/yarn.lock b/yarn.lock index 88f14d0b5af..c4bb00e5dc4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2313,10 +2313,10 @@ resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== -"@hirosystems/token-metadata-api-client@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@hirosystems/token-metadata-api-client/-/token-metadata-api-client-1.1.0.tgz#ba61900fef6f9d3348b50540341ed3d3d2960ca5" - integrity sha512-1IWfthAvxJzIqpbcBmZlavEVGS/bxh2gOsO+SuSsBOp95CGpKAuTjUv7BS5Y6LBfniKrok/FEKlR+Wh8+zBwhQ== +"@hirosystems/token-metadata-api-client@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@hirosystems/token-metadata-api-client/-/token-metadata-api-client-1.2.0.tgz#a10af1061f1556ca454182471f42af76093e1ede" + integrity sha512-voIhvGV4yCOEE2BWbQeGV4S395OLTKg5VsV4HJBM4Ekf/hiu5fktF8R0T24JcZc06resf94hH6L9ybiLz6tpGQ== dependencies: isomorphic-fetch "^3.0.0"