mirror of
https://github.com/zhigang1992/wallet.git
synced 2026-01-12 22:53:27 +08:00
refactor: improve fee estimation
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user