Skip to content

Commit b2a24bc

Browse files
feat: add mempool.space fee market
1 parent 7cd8c0c commit b2a24bc

File tree

21 files changed

+312
-94
lines changed

21 files changed

+312
-94
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.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.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.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: 2 additions & 1 deletion
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}`;

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

packages/bitcoin/README.md

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,46 +10,44 @@ It exposes RxJS streams (balance$, utxos$, transactionHistory$) so front-ends ca
1010

1111
The package is organized in 5 modules:
1212

13-
1413
<p align="center">
1514
<img align="middle" src="./assets/modules_chart.png"/>
1615
</p>
1716

1817
### **Modules Description**
1918

20-
| Module | Purpose |
21-
|--------------------------------------------|-----------------------------------------------------------------------------------------------------------|
22-
| **common/** | Pure, stateless helpers that can be reused by every other module. No network or side-effects. |
23-
| `common/address.ts` | Convert compressed pubkeys to P2PKH, P2SH-P2WPKH, P2WPKH and P2TR addresses; validate addresses. |
24-
| `common/constants.ts` | Shared byte-sizes (input/output/overhead), dust threshold, etc. |
25-
| `common/info.ts` | Type that groups the five account-level XPUBs produced for each address type. |
26-
| `common/keyDerivation.ts` | BIP-39/BIP-32/Electrum seed derivation, account & child key derivation, Taproot tweaks. |
27-
| `common/network.ts` | Enum for Mainnet / Testnet and related helpers. |
28-
| `common/taproot.ts` | Low-level functions to compute BIP-341 tagged hashes and tweak x-only pub/priv keys. |
29-
| `common/transaction.ts` | UnsignedTransaction / SignedTransaction type definitions. |
30-
| **input-selection/** | Pluggable coin-selection strategies. |
31-
| `input-selection/InputSelector.ts` | Interface describing the contract for any selector implementation. |
32-
| `input-selection/GreedyInputSelector.ts` | Deterministic greedy algorithm that selects largest UTXOs until target + fee is met. |
33-
| **providers/** | Abstract access to the Bitcoin network and on-chain data. |
34-
| `providers/BlockchainDataProvider.ts` | Interface for reading chain state, mempool, fee rates, and broadcasting TXs. |
35-
| `providers/MaestroBitcoinDataProvider.ts` | Concrete HTTP client against Maestro’s REST API. |
36-
| `providers/StubBitcoinDataProvider.ts` | In-memory mock that returns deterministic data for unit tests. |
37-
| `providers/BlockchainInputResolver.ts` | Helper that maps a (txid, vout) pair to a full UTxO by calling the provider. |
38-
| **tx-builder/** | Everything needed to construct a PSBT ready for signing. |
39-
| `tx-builder/TransactionBuilder.ts` | Accepts outputs, change address, selector, builds PSBT, estimates vBytes & fee. |
40-
| `tx-builder/utils.ts` | Constants & helpers specific to size/fee calculation inside the builder. |
41-
| **wallet/** | Stateful, observable wallet core and signer. |
42-
| `wallet/BitcoinWallet.ts` | Polls provider, maintains `balance$`, `utxos$`, history & pending TXs via RxJS. |
43-
| `wallet/BitcoinSigner.ts` | Thin wrapper around `@bitcoinerlab/secp256k1` for PSBT input signing + key zeroisation. |
44-
| `index.ts` | Package entry point. |
19+
| Module | Purpose |
20+
| ----------------------------------------- | ------------------------------------------------------------------------------------------------ |
21+
| **common/** | Pure, stateless helpers that can be reused by every other module. No network or side-effects. |
22+
| `common/address.ts` | Convert compressed pubkeys to P2PKH, P2SH-P2WPKH, P2WPKH and P2TR addresses; validate addresses. |
23+
| `common/constants.ts` | Shared byte-sizes (input/output/overhead), dust threshold, etc. |
24+
| `common/info.ts` | Type that groups the five account-level XPUBs produced for each address type. |
25+
| `common/keyDerivation.ts` | BIP-39/BIP-32/Electrum seed derivation, account & child key derivation, Taproot tweaks. |
26+
| `common/network.ts` | Enum for Mainnet / Testnet and related helpers. |
27+
| `common/taproot.ts` | Low-level functions to compute BIP-341 tagged hashes and tweak x-only pub/priv keys. |
28+
| `common/transaction.ts` | UnsignedTransaction / SignedTransaction type definitions. |
29+
| **input-selection/** | Pluggable coin-selection strategies. |
30+
| `input-selection/InputSelector.ts` | Interface describing the contract for any selector implementation. |
31+
| `input-selection/GreedyInputSelector.ts` | Deterministic greedy algorithm that selects largest UTXOs until target + fee is met. |
32+
| **providers/** | Abstract access to the Bitcoin network and on-chain data. |
33+
| `providers/BlockchainDataProvider.ts` | Interface for reading chain state, mempool, fee rates, and broadcasting TXs. |
34+
| `providers/MaestroBitcoinDataProvider.ts` | Concrete HTTP client against Maestro’s REST API. |
35+
| `providers/StubBitcoinDataProvider.ts` | In-memory mock that returns deterministic data for unit tests. |
36+
| `providers/BlockchainInputResolver.ts` | Helper that maps a (txid, vout) pair to a full UTxO by calling the provider. |
37+
| **tx-builder/** | Everything needed to construct a PSBT ready for signing. |
38+
| `tx-builder/TransactionBuilder.ts` | Accepts outputs, change address, selector, builds PSBT, estimates vBytes & fee. |
39+
| `tx-builder/utils.ts` | Constants & helpers specific to size/fee calculation inside the builder. |
40+
| **wallet/** | Stateful, observable wallet core and signer. |
41+
| `wallet/BitcoinWallet.ts` | Polls provider, maintains `balance$`, `utxos$`, history & pending TXs via RxJS. |
42+
| `wallet/BitcoinSigner.ts` | Thin wrapper around `@bitcoinerlab/secp256k1` for PSBT input signing + key zeroisation. |
43+
| `index.ts` | Package entry point. |
4544

4645
## **High Level Overview**
4746

4847
### Common
4948

5049
Handles all deterministic key derivation (BIP-32, Electrum), address transformation, and Taproot tweaking. It is strictly pure code with no network or disk I/O. It contains some common types shared across all other modules.
5150

52-
5351
### Provider
5452

5553
A thin abstraction (BlockchainDataProvider) encodes everything the wallet needs from a chain service: block tip, UTXOs, mempool, fee estimates, and broadcast. The provider isolates all network I/O and normalises raw REST/RPC responses into the package’s own types (BlockInfo, UTxO, TransactionHistoryEntry, …).
@@ -75,5 +73,3 @@ This module provides:
7573
- **Input** selection Extension point for input selection, comes with a GreedyInputSelector that implements a simple, deterministic algorithm.
7674
- **Builder** TransactionBuilder composes PSBTs, computes vBytes, and enforces dust rules.
7775
- **Signer** BitcoinSigner wraps @bitcoinerlab/secp256k1, signs each PSBT input, and scrubs the private key from memory.
78-
79-
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 './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/BitcoinWallet';
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: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/* eslint-disable no-magic-numbers */
2+
import { EstimatedFees } from './BitcoinWallet';
3+
import { FeeEstimationMode } from '../providers/BitcoinDataProvider';
4+
import { MaestroBitcoinDataProvider } from '../providers/MaestroBitcoinDataProvider';
5+
import { Network } from '../common/network';
6+
import { Logger } from 'ts-log';
7+
import { FeeMarketProvider } from './FeeMarketProvider';
8+
import { DEFAULT_MARKETS } from './constants';
9+
10+
export class MaestroFeeMarketProvider implements FeeMarketProvider {
11+
constructor(
12+
private readonly provider: MaestroBitcoinDataProvider,
13+
private readonly logger: Logger,
14+
private readonly network: Network = Network.Mainnet
15+
) {}
16+
17+
async getFeeMarket(): Promise<EstimatedFees> {
18+
try {
19+
if (this.network === Network.Testnet) {
20+
return DEFAULT_MARKETS;
21+
}
22+
23+
const fastEstimate = await this.provider.estimateFee(1, FeeEstimationMode.Economical);
24+
const standardEstimate = await this.provider.estimateFee(3, FeeEstimationMode.Economical);
25+
const slowEstimate = await this.provider.estimateFee(6, FeeEstimationMode.Economical);
26+
27+
return {
28+
fast: {
29+
feeRate: fastEstimate.feeRate,
30+
targetConfirmationTime: fastEstimate.blocks * 10 * 60
31+
},
32+
standard: {
33+
feeRate: standardEstimate.feeRate,
34+
targetConfirmationTime: standardEstimate.blocks * 10 * 60
35+
},
36+
slow: {
37+
feeRate: slowEstimate.feeRate,
38+
targetConfirmationTime: slowEstimate.blocks * 10 * 60
39+
}
40+
};
41+
} catch (error) {
42+
this.logger.error('Failed to fetch fee market:', error);
43+
}
44+
45+
return DEFAULT_MARKETS;
46+
}
47+
}

0 commit comments

Comments
 (0)