refactor: improve fee estimation

This commit is contained in:
kyranjamie
2023-06-06 14:01:44 +02:00
parent d4264575a9
commit 11ce4b0195
12 changed files with 79 additions and 60 deletions

View File

@@ -1,7 +1,10 @@
import BigNumber from 'bignumber.js';
export function calculateMeanAverage(numbers: BigNumber[]) {
import { initBigNumber } from './helpers';
export function calculateMeanAverage(numbers: BigNumber[] | number[]) {
return numbers
.map(initBigNumber)
.reduce((acc, price) => acc.plus(price), new BigNumber(0))
.dividedBy(numbers.length);
}

View File

@@ -311,3 +311,11 @@ export const parseIfValidPunycode = (s: string) => {
export function capitalize(val: string) {
return val.charAt(0).toUpperCase() + val.slice(1);
}
export function isFulfilled<T>(p: PromiseSettledResult<T>): p is PromiseFulfilledResult<T> {
return p.status === 'fulfilled';
}
export function isRejected<T>(p: PromiseSettledResult<T>): p is PromiseRejectedResult {
return p.status === 'rejected';
}

View File

@@ -38,7 +38,7 @@ export function useBitcoinFeesList({ amount, isSendingMax, recipient }: UseBitco
const { data: utxos } = useSpendableNativeSegwitUtxos(currentAccountBtcAddress);
const btcMarketData = useCryptoCurrencyMarketData('BTC');
const { avgApiFeeRates: feeRates, isLoading } = useAverageBitcoinFeeRates();
const { data: feeRates, isLoading } = useAverageBitcoinFeeRates();
const feesList: FeesListItem[] = useMemo(() => {
function getFiatFeeValue(fee: number) {

View File

@@ -16,7 +16,7 @@ export function useGenerateRetrieveTaprootFundsTx() {
const networkMode = useBitcoinScureLibNetworkConfig();
const uninscribedUtxos = useCurrentTaprootAccountUninscribedUtxos();
const createSigner = useCurrentAccountTaprootSigner();
const { avgApiFeeRates: feeRates } = useAverageBitcoinFeeRates();
const { data: feeRates } = useAverageBitcoinFeeRates();
const fee = useMemo(() => {
if (!feeRates) return createMoney(0, 'BTC');

View File

@@ -13,7 +13,7 @@ interface SendInscriptionLoaderProps {
}
export function SendInscriptionLoader({ children }: SendInscriptionLoaderProps) {
const { inscription } = useSendInscriptionRouteState();
const { avgApiFeeRates: feeRates } = useAverageBitcoinFeeRates();
const { data: feeRates } = useAverageBitcoinFeeRates();
if (!feeRates) return null;

View File

@@ -23,7 +23,7 @@ export function useSendInscriptionFeesList({ recipient, utxo }: UseSendInscripti
const { data: nativeSegwitUtxos } = useCurrentNativeSegwitUtxos();
const btcMarketData = useCryptoCurrencyMarketData('BTC');
const { avgApiFeeRates: feeRates, isLoading } = useAverageBitcoinFeeRates();
const { data: feeRates, isLoading } = useAverageBitcoinFeeRates();
const feesList: FeesListItem[] = useMemo(() => {
function getFiatFeeValue(fee: number) {

View File

@@ -11,7 +11,7 @@ export function SendCryptoAssetFormLayout({ children }: SendCryptoAssetFormLayou
data-testid={SendCryptoAssetSelectors.SendForm}
flexDirection="column"
maxHeight={['calc(100vh - 116px)', 'calc(85vh - 116px)']}
overflowY="scroll"
overflowY="auto"
pb={['120px', '48px']}
pt={['base', '48px']}
px="loose"

View File

@@ -16,7 +16,7 @@ export function useCalculateMaxBitcoinSpend() {
const currentAccountBtcAddress = useCurrentAccountNativeSegwitAddressIndexZero();
const balance = useCurrentNativeSegwitAddressBalance();
const { data: utxos } = useSpendableNativeSegwitUtxos(currentAccountBtcAddress);
const { avgApiFeeRates: feeRates } = useAverageBitcoinFeeRates();
const { data: feeRates } = useAverageBitcoinFeeRates();
return useCallback(
(address = '', feeRate?: number) => {

View File

@@ -38,9 +38,20 @@ class AddressApi {
}
interface FeeEstimateEarnApiResponse {
fastestFee: number;
halfHourFee: number;
hourFee: number;
name: string;
height: number;
hash: string;
time: string;
latest_url: string;
previous_hash: string;
previous_url: string;
peer_count: number;
unconfirmed_count: number;
high_fee_per_kb: number;
medium_fee_per_kb: number;
low_fee_per_kb: number;
last_fork_height: number;
last_fork_hash: string;
}
interface FeeEstimateMempoolSpaceApiResponse {
fastestFee: number;
@@ -50,20 +61,41 @@ interface FeeEstimateMempoolSpaceApiResponse {
minimumFee: number;
}
interface FeeResult {
fast: number;
medium: number;
slow: number;
}
class FeeEstimatesApi {
constructor(public configuration: Configuration) {}
async getFeeEstimatesFromEarnApi(): Promise<FeeEstimateEarnApiResponse> {
async getFeeEstimatesFromBlockcypherApi(): Promise<FeeResult> {
return fetchData({
errorMsg: 'No fee estimates fetched',
url: `https://bitcoinfees.earn.com/api/v1/fees/recommended`,
url: `https://api.blockcypher.com/v1/btc/main`,
}).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,
};
});
}
async getFeeEstimatesFromMempoolSpaceApi(): Promise<FeeEstimateMempoolSpaceApiResponse> {
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,
};
});
}
}

View File

@@ -1,56 +1,33 @@
import BigNumber from 'bignumber.js';
import { logger } from '@shared/logger';
import { AverageBitcoinFeeRates } from '@shared/models/fees/bitcoin-fees.model';
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { calculateMeanAverage } from '@app/common/math/calculate-averages';
import { initBigNumber } from '@app/common/math/helpers';
import { isFulfilled, isRejected } from '@app/common/utils';
import { useGetAllBitcoinFeeEstimatesQuery } from './fee-estimates.query';
export function useAverageBitcoinFeeRates() {
const { data: avgApiFeeRates, isLoading } = useGetAllBitcoinFeeEstimatesQuery({
const analytics = useAnalytics();
return useGetAllBitcoinFeeEstimatesQuery({
onError: err => logger.error('Error getting all apis bitcoin fee estimates', { err }),
select: (resp): AverageBitcoinFeeRates | null => {
if (resp[0].status === 'rejected' && resp[1].status === 'rejected') {
return null;
select(feeEstimates) {
if (feeEstimates.every(isRejected)) {
void analytics.track('error_using_fallback_bitcoin_fees');
return {
fastestFee: initBigNumber(15),
halfHourFee: initBigNumber(10),
hourFee: initBigNumber(5),
};
}
let mempoolApiFeeRates = null;
if (resp[0].status === 'fulfilled') {
mempoolApiFeeRates = resp[0].value;
}
let earnApiFeeRates = null;
if (resp[1].status === 'fulfilled') {
earnApiFeeRates = resp[1].value;
}
// zero values for cases when one api is down
const fastestFees = [
new BigNumber(mempoolApiFeeRates?.fastestFee ?? 0),
new BigNumber(earnApiFeeRates?.fastestFee ?? 0),
].filter(fee => fee.isGreaterThan(0));
const halfHourFees = [
new BigNumber(mempoolApiFeeRates?.halfHourFee ?? 0),
new BigNumber(earnApiFeeRates?.halfHourFee ?? 0),
].filter(fee => fee.isGreaterThan(0));
const hourFees = [
new BigNumber(mempoolApiFeeRates?.hourFee ?? 0),
new BigNumber(earnApiFeeRates?.hourFee ?? 0),
].filter(fee => fee.isGreaterThan(0));
// use the highest fee rate for fastest fee
const fastestFee = fastestFees.reduce((p, v) => (p.isGreaterThan(v) ? p : v));
const fees = feeEstimates.filter(isFulfilled).map(result => result.value);
return {
fastestFee,
halfHourFee: calculateMeanAverage(halfHourFees),
hourFee: calculateMeanAverage(hourFees),
fastestFee: calculateMeanAverage(fees.map(fee => fee.fast)),
halfHourFee: calculateMeanAverage(fees.map(fee => fee.medium)),
hourFee: calculateMeanAverage(fees.map(fee => fee.slow)),
};
},
});
return { isLoading, avgApiFeeRates };
}

View File

@@ -6,12 +6,11 @@ import { useBitcoinClient } from '@app/store/common/api-clients.hooks';
import { BitcoinClient } from '../bitcoin-client';
function fetchAllBitcoinFeeEstimates(client: BitcoinClient) {
return async () => {
return Promise.allSettled([
return async () =>
Promise.allSettled([
client.feeEstimatesApi.getFeeEstimatesFromMempoolSpaceApi(),
client.feeEstimatesApi.getFeeEstimatesFromEarnApi(),
client.feeEstimatesApi.getFeeEstimatesFromBlockcypherApi(),
]);
};
}
type FetchAllBitcoinFeeEstimatesResp = Awaited<
@@ -23,7 +22,7 @@ export function useGetAllBitcoinFeeEstimatesQuery<
>(options?: AppUseQueryConfig<FetchAllBitcoinFeeEstimatesResp, T>) {
const client = useBitcoinClient();
return useQuery({
queryKey: ['all-bitcoin-fee-estimates'],
queryKey: ['average-bitcoin-fee-estimates'],
queryFn: fetchAllBitcoinFeeEstimates(client),
staleTime: 1000 * 60,
refetchOnWindowFocus: false,

View File

@@ -35,7 +35,7 @@ export function useBrc20Transfers() {
const currentAccountIndex = useCurrentAccountIndex();
const ordinalsbotClient = useOrdinalsbotClient();
const { address } = useCurrentAccountTaprootAddressIndexZeroPayment();
const bitcoinFees = useAverageBitcoinFeeRates();
const { data: fees } = useAverageBitcoinFeeRates();
return {
async initiateTransfer(tick: string, amount: string) {
@@ -47,7 +47,7 @@ export function useBrc20Transfers() {
file: payload,
size,
name: `${tick}-${amount}.txt`,
fee: bitcoinFees.avgApiFeeRates?.halfHourFee.toNumber() ?? 10,
fee: fees?.halfHourFee.toNumber() ?? 10,
});
if (order.data.status !== 'ok') throw new Error('Failed to initiate transfer');