Skip to content

Commit

Permalink
fix: track error codes
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranjamie committed Jan 16, 2024
1 parent e67b61d commit 227f6ee
Show file tree
Hide file tree
Showing 9 changed files with 68 additions and 95 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
42 changes: 22 additions & 20 deletions src/app/common/api/fetch-wrapper.ts
Original file line number Diff line number Diff line change
@@ -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;
}
});
59 changes: 24 additions & 35 deletions src/app/query/bitcoin/bitcoin-client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import axios from 'axios';

import { fetchData } from '../utils';

class Configuration {
constructor(public baseUrl: string) {}
}
Expand All @@ -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<UtxoResponseItem[]> {
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<UtxoResponseItem[]>(
`${this.configuration.baseUrl}/address/${address}/utxo`
);
return resp.data.sort((a, b) => a.vout - b.vout);
}
}

Expand Down Expand Up @@ -77,32 +70,28 @@ class FeeEstimatesApi {
constructor(public configuration: Configuration) {}

async getFeeEstimatesFromBlockcypherApi(network: string): Promise<FeeResult> {
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<FeeEstimateEarnApiResponse>(
`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<FeeResult> {
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<FeeEstimateMempoolSpaceApiResponse>(
`https://mempool.space/api/v1/fees/recommended`
);
const { fastestFee, halfHourFee, hourFee } = resp.data;
return {
slow: hourFee,
medium: halfHourFee,
fast: fastestFee,
};
}
}

Expand Down
11 changes: 5 additions & 6 deletions src/app/query/bitcoin/ordinals/inscriptions.query.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<InscriptionsQueryResponse>(
`${HIRO_INSCRIPTIONS_API_URL}?${params.toString()}`
);
return res.data;
}

/**
Expand Down
16 changes: 7 additions & 9 deletions src/app/query/stacks/fees/fees.query.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<StacksTxFeeEstimation>(
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;
};
}

Expand Down
6 changes: 3 additions & 3 deletions src/app/query/stacks/network/network.query.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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) {
Expand Down
14 changes: 0 additions & 14 deletions src/app/query/utils.ts

This file was deleted.

5 changes: 2 additions & 3 deletions tests/specs/onboarding/onboarding.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
});

Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down

0 comments on commit 227f6ee

Please sign in to comment.