Skip to content

Commit c7c1b4a

Browse files
feat: add mempool.space fee market
1 parent 7f80108 commit c7c1b4a

File tree

17 files changed

+276
-61
lines changed

17 files changed

+276
-61
lines changed

apps/browser-extension-wallet/.env.defaults

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,6 @@ E2E_FORCE_TREZOR_PICKED=false
148148

149149
# ADA handle cache lifetime
150150
HANDLE_RESOLUTION_CACHE_LIFETIME=600000
151+
152+
# mempool.space api
153+
MEMPOOLSPACE_URL=https://mempool.live-mainnet.eks.lw.iog.io

apps/browser-extension-wallet/.env.developerpreview

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,6 @@ LACE_EXTENSION_UNINSTALL_REDIRECT_URL=
9999

100100
# Midnight
101101
MIDNIGHT_EVENT_BANNER_REMINDER_TIME=129600000
102+
103+
# mempool.space api
104+
MEMPOOLSPACE_URL=https://mempool.live-mainnet.eks.lw.iog.io

apps/browser-extension-wallet/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,6 @@ MIN_NUMBER_OF_COSIGNERS=2
117117

118118
# POLLING PAUSE AFTER INACTIVITY
119119
SESSION_TIMEOUT=300000
120+
121+
# mempool.space api
122+
MEMPOOLSPACE_URL=https://mempool.live-mainnet.eks.lw.iog.io

apps/browser-extension-wallet/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"permissions": ["webRequest", "storage", "tabs", "unlimitedStorage"],
1919
"host_permissions": ["<all_urls>"],
2020
"content_security_policy": {
21-
"extension_pages": "default-src 'self' $LOCALHOST_DEFAULT_SRC; frame-src https://connect.trezor.io/ https://www.youtube-nocookie.com; script-src 'self' 'wasm-unsafe-eval'; font-src 'self' data: https://use.typekit.net; object-src 'self'; connect-src $BLOCKFROST_URLS $MAESTRO_URLS $CARDANO_SERVICES_URLS $CARDANO_WS_SERVER_URLS $SENTRY_URL $DAPP_RADAR_APPI_URL https://coingecko.live-mainnet.eks.lw.iog.io https://coingecko.live-mainnet.eks.lw.iog.io https://muesliswap.live-mainnet.eks.lw.iog.io $LOCALHOST_CONNECT_SRC $POSTHOG_HOST https://use.typekit.net https://api.handle.me/ https://*.api.handle.me/ data:; style-src * 'unsafe-inline'; img-src * data: blob:;"
21+
"extension_pages": "default-src 'self' $LOCALHOST_DEFAULT_SRC; frame-src https://connect.trezor.io/ https://www.youtube-nocookie.com; script-src 'self' 'wasm-unsafe-eval'; font-src 'self' data: https://use.typekit.net; object-src 'self'; connect-src $MEMPOOLSPACE_URL $BLOCKFROST_URLS $MAESTRO_URLS $CARDANO_SERVICES_URLS $CARDANO_WS_SERVER_URLS $SENTRY_URL $DAPP_RADAR_APPI_URL https://coingecko.live-mainnet.eks.lw.iog.io https://coingecko.live-mainnet.eks.lw.iog.io https://muesliswap.live-mainnet.eks.lw.iog.io $LOCALHOST_CONNECT_SRC $POSTHOG_HOST https://use.typekit.net https://api.handle.me/ https://*.api.handle.me/ data:; style-src * 'unsafe-inline'; img-src * data: blob:;"
2222
},
2323
"content_scripts": [
2424
{

apps/browser-extension-wallet/src/lib/scripts/background/wallet.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,15 @@ const bitcoinWalletFactory: BitcoinWalletFactory<Wallet.WalletMetadata, Wallet.A
254254
Bitcoin.Network.Mainnet
255255
);
256256

257+
const featureFlags = await getFeatureFlags(
258+
network === Bitcoin.Network.Testnet ? Cardano.NetworkMagics.Preprod : Cardano.NetworkMagics.Mainnet
259+
);
260+
const useMempoolSpaceFeeMarket = isExperimentEnabled(featureFlags, ExperimentName.MEMPOOLSPACE_FEE_MARKET);
261+
262+
const feeMarketProvider = useMempoolSpaceFeeMarket
263+
? new Bitcoin.MempoolSpaceMarketProvider(process.env.MEMPOOLSPACE_URL, logger, network)
264+
: new Bitcoin.MaestroFeeMarketProvider(provider, logger, network);
265+
257266
if (!walletAccount.metadata.bitcoin) {
258267
throw new Error('Bitcoin metadata not found');
259268
}
@@ -278,6 +287,7 @@ const bitcoinWalletFactory: BitcoinWalletFactory<Wallet.WalletMetadata, Wallet.A
278287
currentBitcoinWalletProvider$.next(provider);
279288
return new Bitcoin.BitcoinWallet(
280289
provider,
290+
feeMarketProvider,
281291
localPollingIntervalConfig,
282292
20,
283293
walletInfo,

apps/browser-extension-wallet/src/lib/scripts/types/feature-flags.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ export enum ExperimentName {
1414
SEND_CONSOLE_ERRORS_TO_SENTRY = 'send-console-errors-to-sentry',
1515
BITCOIN_WALLETS = 'bitcoin-wallets',
1616
NFTPRINTLAB = 'nftprintlab',
17-
GLACIER_DROP = 'glacier-drop'
17+
GLACIER_DROP = 'glacier-drop',
18+
MEMPOOLSPACE_FEE_MARKET = 'bitcoin-mempool-space-fee-market'
1819
}
1920

2021
export type FeatureFlag = `${ExperimentName}`;
@@ -43,7 +44,6 @@ type FeatureFlagCustomPayloads = {
4344

4445
export type FeatureFlagPayloads = {
4546
[key in FeatureFlag]: FeatureFlagPayload;
46-
} &
47-
FeatureFlagCustomPayloads;
47+
} & FeatureFlagCustomPayloads;
4848

4949
export type RawFeatureFlagPayloads = Record<ExperimentName, JsonType>;

apps/browser-extension-wallet/src/providers/PostHogClientProvider/client/config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ const defaultFeatureFlags: FeatureFlags = {
3838
[ExperimentName.SEND_CONSOLE_ERRORS_TO_SENTRY]: false,
3939
[ExperimentName.BITCOIN_WALLETS]: false,
4040
[ExperimentName.NFTPRINTLAB]: false,
41-
[ExperimentName.GLACIER_DROP]: false
41+
[ExperimentName.GLACIER_DROP]: false,
42+
[ExperimentName.MEMPOOLSPACE_FEE_MARKET]: false
4243
};
4344

4445
export const featureFlagsByNetworkInitialValue: FeatureFlagsByNetwork = {

apps/browser-extension-wallet/webpack-utils.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const transformManifest = (content, mode, jsAssets = []) => {
5454
: 'http://localhost:* http://127.0.0.1:*'
5555
)
5656
.replace('$POSTHOG_HOST', process.env.POSTHOG_HOST)
57+
.replace('$MEMPOOLSPACE_URL', process.env.MEMPOOLSPACE_URL)
5758
.replace('$SENTRY_URL', constructSentryConnectSrc(process.env.SENTRY_DSN))
5859
.replace('$DAPP_RADAR_APPI_URL', process.env.DAPP_RADAR_API_URL);
5960

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export const INPUT_SIZE = 68;
2-
export const OUTPUT_SIZE = 34;
2+
export const OUTPUT_SIZE = 31;
33
export const TRANSACTION_OVERHEAD = 10;
44
export const DUST_THRESHOLD = 546;

packages/bitcoin/src/wallet/lib/tx-builder/TransactionBuilder.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@ import {
66
validateBitcoinAddress,
77
AddressValidationResult,
88
UnsignedTransaction,
9-
isP2trAddress
9+
isP2trAddress,
10+
INPUT_SIZE,
11+
OUTPUT_SIZE,
12+
TRANSACTION_OVERHEAD
1013
} from '../common';
1114
import { UTxO } from '../providers';
1215
import { payments, script, Psbt } from 'bitcoinjs-lib';
1316
import * as bitcoin from 'bitcoinjs-lib';
1417

15-
const INPUT_SIZE = 68;
16-
const OUTPUT_SIZE = 34;
17-
const TRANSACTION_OVERHEAD = 10;
18-
1918
/**
2019
* A class to build Bitcoin transactions using a flexible input and output management system.
2120
*/

packages/bitcoin/src/wallet/lib/wallet/BitcoinWallet.ts

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
BlockchainDataProvider,
44
BlockchainInputResolver,
55
BlockInfo,
6-
FeeEstimationMode,
76
InputResolver,
87
TransactionHistoryEntry,
98
UTxO
@@ -25,6 +24,7 @@ import * as bitcoin from 'bitcoinjs-lib';
2524
import * as ecc from '@bitcoinerlab/secp256k1';
2625
import isEqual from 'lodash/isEqual';
2726
import { historyEntryFromRawTx } from '../tx-builder/utils';
27+
import { FeeMarketProvider } from '@wallet/lib/wallet/FeeMarketProvider';
2828

2929
bitcoin.initEccLib(ecc);
3030

@@ -91,6 +91,7 @@ export class BitcoinWallet {
9191
private pollController$: Observable<boolean>;
9292
private logger: Logger;
9393
private inputResolver: InputResolver;
94+
private readonly feeMarketProvider: FeeMarketProvider;
9495

9596
public syncStatus: SyncStatus;
9697

@@ -107,6 +108,7 @@ export class BitcoinWallet {
107108
// eslint-disable-next-line max-params
108109
constructor(
109110
provider: BlockchainDataProvider,
111+
feeMarketProvider: FeeMarketProvider,
110112
pollInterval = 30_000,
111113
historyDepth = 20,
112114
info: BitcoinWalletInfo,
@@ -133,6 +135,7 @@ export class BitcoinWallet {
133135

134136
// TODO: Allow this to be injected.
135137
this.inputResolver = new BlockchainInputResolver(provider);
138+
this.feeMarketProvider = feeMarketProvider;
136139

137140
const networkKeys = getNetworkKeys(info, network);
138141
const extendedAccountPubKey = networkKeys.nativeSegWit;
@@ -175,46 +178,7 @@ export class BitcoinWallet {
175178
* Fetches the current fee market for estimating transaction fees.
176179
*/
177180
public async getCurrentFeeMarket(): Promise<EstimatedFees> {
178-
try {
179-
if (this.network === Network.Testnet) {
180-
return {
181-
fast: {
182-
feeRate: 0.000_025,
183-
targetConfirmationTime: 1
184-
},
185-
standard: {
186-
feeRate: 0.000_015,
187-
targetConfirmationTime: 3
188-
},
189-
slow: {
190-
feeRate: 0.000_01,
191-
targetConfirmationTime: 6
192-
}
193-
};
194-
}
195-
196-
const fastEstimate = await this.provider.estimateFee(1, FeeEstimationMode.Conservative);
197-
const standardEstimate = await this.provider.estimateFee(3, FeeEstimationMode.Conservative);
198-
const slowEstimate = await this.provider.estimateFee(6, FeeEstimationMode.Conservative);
199-
200-
return {
201-
fast: {
202-
feeRate: fastEstimate.feeRate,
203-
targetConfirmationTime: fastEstimate.blocks * 10 * 60
204-
},
205-
standard: {
206-
feeRate: standardEstimate.feeRate,
207-
targetConfirmationTime: standardEstimate.blocks * 10 * 60
208-
},
209-
slow: {
210-
feeRate: slowEstimate.feeRate,
211-
targetConfirmationTime: slowEstimate.blocks * 10 * 60
212-
}
213-
};
214-
} catch (error) {
215-
this.logger.error('Failed to fetch fee market:', error);
216-
throw error;
217-
}
181+
return this.feeMarketProvider.getFeeMarket();
218182
}
219183

220184
/**
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { EstimatedFees } from '@wallet';
2+
3+
/**
4+
* Interface for providing a fee estimation strategy.
5+
*
6+
* Implementations of this interface are responsible for retrieving
7+
* current fee market data from a source and returning a normalized fee structure.
8+
*/
9+
export interface FeeMarketProvider {
10+
/**
11+
* Retrieves current fee estimates.
12+
*
13+
* @returns A promise resolving to the latest fee estimates.
14+
*/
15+
getFeeMarket(): Promise<EstimatedFees>;
16+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/* eslint-disable no-magic-numbers */
2+
import { EstimatedFees, FeeEstimationMode, MaestroBitcoinDataProvider, Network } from '@wallet';
3+
import { Logger } from 'ts-log';
4+
import { FeeMarketProvider } from './FeeMarketProvider';
5+
import { DEFAULT_MARKETS } from './constants';
6+
7+
export class MaestroFeeMarketProvider implements FeeMarketProvider {
8+
constructor(
9+
private readonly provider: MaestroBitcoinDataProvider,
10+
private readonly logger: Logger,
11+
private readonly network: Network = Network.Mainnet
12+
) {}
13+
14+
async getFeeMarket(): Promise<EstimatedFees> {
15+
try {
16+
if (this.network === Network.Testnet) {
17+
return DEFAULT_MARKETS;
18+
}
19+
20+
const fastEstimate = await this.provider.estimateFee(1, FeeEstimationMode.Economical);
21+
const standardEstimate = await this.provider.estimateFee(3, FeeEstimationMode.Economical);
22+
const slowEstimate = await this.provider.estimateFee(6, FeeEstimationMode.Economical);
23+
24+
return {
25+
fast: {
26+
feeRate: fastEstimate.feeRate,
27+
targetConfirmationTime: fastEstimate.blocks * 10 * 60
28+
},
29+
standard: {
30+
feeRate: standardEstimate.feeRate,
31+
targetConfirmationTime: standardEstimate.blocks * 10 * 60
32+
},
33+
slow: {
34+
feeRate: slowEstimate.feeRate,
35+
targetConfirmationTime: slowEstimate.blocks * 10 * 60
36+
}
37+
};
38+
} catch (error) {
39+
this.logger.error('Failed to fetch fee market:', error);
40+
}
41+
42+
return DEFAULT_MARKETS;
43+
}
44+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/* eslint-disable no-magic-numbers */
2+
import { EstimatedFees, Network } from '@wallet';
3+
import { FeeMarketProvider } from './FeeMarketProvider';
4+
import axios, { AxiosInstance } from 'axios';
5+
import { Logger } from 'ts-log';
6+
import { DEFAULT_MARKETS } from './constants';
7+
8+
const satsPerVByteToBtcPerKB = (satsPerVByte: number): number => (satsPerVByte * 1000) / 100_000_000;
9+
10+
export class MempoolSpaceMarketProvider implements FeeMarketProvider {
11+
private readonly api: AxiosInstance;
12+
13+
constructor(
14+
url: string,
15+
private readonly logger: Logger,
16+
private readonly network: Network = Network.Mainnet
17+
) {
18+
this.api = axios.create({
19+
baseURL: url
20+
});
21+
}
22+
23+
async getFeeMarket(): Promise<EstimatedFees> {
24+
try {
25+
if (this.network === Network.Testnet) {
26+
return DEFAULT_MARKETS;
27+
}
28+
29+
const response = await this.api.get('/api/v1/fees/recommended');
30+
31+
const fastEstimate = response.data.fastestFee;
32+
const standardEstimate = response.data.halfHourFee;
33+
const slowEstimate = response.data.hourFee;
34+
35+
return {
36+
fast: {
37+
feeRate: satsPerVByteToBtcPerKB(fastEstimate),
38+
targetConfirmationTime: 600 // 10 minutes
39+
},
40+
standard: {
41+
feeRate: satsPerVByteToBtcPerKB(standardEstimate),
42+
targetConfirmationTime: 1800 // 30 minutes
43+
},
44+
slow: {
45+
feeRate: satsPerVByteToBtcPerKB(slowEstimate),
46+
targetConfirmationTime: 3600 // 60 minutes
47+
}
48+
};
49+
} catch (error) {
50+
this.logger.error('Failed to fetch fee market:', error);
51+
}
52+
53+
return DEFAULT_MARKETS;
54+
}
55+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export const DEFAULT_MARKETS = {
2+
fast: {
3+
feeRate: 0.000_025,
4+
targetConfirmationTime: 1
5+
},
6+
standard: {
7+
feeRate: 0.000_015,
8+
targetConfirmationTime: 3
9+
},
10+
slow: {
11+
feeRate: 0.000_01,
12+
targetConfirmationTime: 6
13+
}
14+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
export * from './BitcoinWallet';
22
export * from './BitcoinSigner';
3+
export * from './FeeMarketProvider';
4+
export * from './MaestroFeeMarketProvider';
5+
export * from './MempoolSpaceMarketProvider';

0 commit comments

Comments
 (0)