Skip to content

feat: add mempool.space fee market #1927

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/browser-extension-wallet/.env.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,6 @@ E2E_FORCE_TREZOR_PICKED=false

# ADA handle cache lifetime
HANDLE_RESOLUTION_CACHE_LIFETIME=600000

# mempool.space api
MEMPOOLSPACE_URL=https://mempool.lw.iog.io
3 changes: 3 additions & 0 deletions apps/browser-extension-wallet/.env.developerpreview
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,6 @@ LACE_EXTENSION_UNINSTALL_REDIRECT_URL=

# Midnight
MIDNIGHT_EVENT_BANNER_REMINDER_TIME=129600000

# mempool.space api
MEMPOOLSPACE_URL=https://mempool.lw.iog.io
3 changes: 3 additions & 0 deletions apps/browser-extension-wallet/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,6 @@ MIN_NUMBER_OF_COSIGNERS=2

# POLLING PAUSE AFTER INACTIVITY
SESSION_TIMEOUT=300000

# mempool.space api
MEMPOOLSPACE_URL=https://mempool.lw.iog.io
2 changes: 1 addition & 1 deletion apps/browser-extension-wallet/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"permissions": ["webRequest", "storage", "tabs", "unlimitedStorage"],
"host_permissions": ["<all_urls>"],
"content_security_policy": {
"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:;"
"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:;"
},
"content_scripts": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,15 @@ const bitcoinWalletFactory: BitcoinWalletFactory<Wallet.WalletMetadata, Wallet.A
Bitcoin.Network.Mainnet
);

const featureFlags = await getFeatureFlags(
network === Bitcoin.Network.Testnet ? Cardano.NetworkMagics.Preprod : Cardano.NetworkMagics.Mainnet
);
const useMempoolSpaceFeeMarket = isExperimentEnabled(featureFlags, ExperimentName.MEMPOOLSPACE_FEE_MARKET);

const feeMarketProvider = useMempoolSpaceFeeMarket
? new Bitcoin.MempoolSpaceMarketProvider(process.env.MEMPOOLSPACE_URL, logger, network)
: new Bitcoin.MaestroFeeMarketProvider(provider, logger, network);

if (!walletAccount.metadata.bitcoin) {
throw new Error('Bitcoin metadata not found');
}
Expand All @@ -278,6 +287,7 @@ const bitcoinWalletFactory: BitcoinWalletFactory<Wallet.WalletMetadata, Wallet.A
currentBitcoinWalletProvider$.next(provider);
return new Bitcoin.BitcoinWallet(
provider,
feeMarketProvider,
localPollingIntervalConfig,
20,
walletInfo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export enum ExperimentName {
SEND_CONSOLE_ERRORS_TO_SENTRY = 'send-console-errors-to-sentry',
BITCOIN_WALLETS = 'bitcoin-wallets',
NFTPRINTLAB = 'nftprintlab',
GLACIER_DROP = 'glacier-drop'
GLACIER_DROP = 'glacier-drop',
MEMPOOLSPACE_FEE_MARKET = 'bitcoin-mempool-space-fee-market'
}

export type FeatureFlag = `${ExperimentName}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ const defaultFeatureFlags: FeatureFlags = {
[ExperimentName.SEND_CONSOLE_ERRORS_TO_SENTRY]: false,
[ExperimentName.BITCOIN_WALLETS]: false,
[ExperimentName.NFTPRINTLAB]: false,
[ExperimentName.GLACIER_DROP]: false
[ExperimentName.GLACIER_DROP]: false,
[ExperimentName.MEMPOOLSPACE_FEE_MARKET]: false
};

export const featureFlagsByNetworkInitialValue: FeatureFlagsByNetwork = {
Expand Down
1 change: 1 addition & 0 deletions apps/browser-extension-wallet/webpack-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const transformManifest = (content, mode, jsAssets = []) => {
: 'http://localhost:* http://127.0.0.1:*'
)
.replace('$POSTHOG_HOST', process.env.POSTHOG_HOST)
.replace('$MEMPOOLSPACE_URL', process.env.MEMPOOLSPACE_URL)
.replace('$SENTRY_URL', constructSentryConnectSrc(process.env.SENTRY_DSN))
.replace('$DAPP_RADAR_APPI_URL', process.env.DAPP_RADAR_API_URL);

Expand Down
54 changes: 25 additions & 29 deletions packages/bitcoin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,46 +10,44 @@ It exposes RxJS streams (balance$, utxos$, transactionHistory$) so front-ends ca

The package is organized in 5 modules:


<p align="center">
<img align="middle" src="./assets/modules_chart.png"/>
</p>

### **Modules Description**

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

## **High Level Overview**

### Common

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.


### Provider

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, …).
Expand All @@ -75,5 +73,3 @@ This module provides:
- **Input** selection Extension point for input selection, comes with a GreedyInputSelector that implements a simple, deterministic algorithm.
- **Builder** TransactionBuilder composes PSBTs, computes vBytes, and enforces dust rules.
- **Signer** BitcoinSigner wraps @bitcoinerlab/secp256k1, signs each PSBT input, and scrubs the private key from memory.


2 changes: 1 addition & 1 deletion packages/bitcoin/src/wallet/lib/common/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const INPUT_SIZE = 68;
export const OUTPUT_SIZE = 34;
export const OUTPUT_SIZE = 31;
export const TRANSACTION_OVERHEAD = 10;
export const DUST_THRESHOLD = 546;
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@ import {
validateBitcoinAddress,
AddressValidationResult,
UnsignedTransaction,
isP2trAddress
isP2trAddress,
INPUT_SIZE,
OUTPUT_SIZE,
TRANSACTION_OVERHEAD
} from '../common';
import { UTxO } from '../providers';
import { payments, script, Psbt } from 'bitcoinjs-lib';
import * as bitcoin from 'bitcoinjs-lib';

const INPUT_SIZE = 68;
const OUTPUT_SIZE = 34;
const TRANSACTION_OVERHEAD = 10;

/**
* A class to build Bitcoin transactions using a flexible input and output management system.
*/
Expand Down
46 changes: 5 additions & 41 deletions packages/bitcoin/src/wallet/lib/wallet/BitcoinWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
BlockchainDataProvider,
BlockchainInputResolver,
BlockInfo,
FeeEstimationMode,
InputResolver,
TransactionHistoryEntry,
UTxO
Expand All @@ -25,6 +24,7 @@ import * as bitcoin from 'bitcoinjs-lib';
import * as ecc from '@bitcoinerlab/secp256k1';
import isEqual from 'lodash/isEqual';
import { historyEntryFromRawTx } from '../tx-builder/utils';
import { FeeMarketProvider } from './FeeMarketProvider';

bitcoin.initEccLib(ecc);

Expand Down Expand Up @@ -91,6 +91,7 @@ export class BitcoinWallet {
private pollController$: Observable<boolean>;
private logger: Logger;
private inputResolver: InputResolver;
private readonly feeMarketProvider: FeeMarketProvider;

public syncStatus: SyncStatus;

Expand All @@ -107,6 +108,7 @@ export class BitcoinWallet {
// eslint-disable-next-line max-params
constructor(
provider: BlockchainDataProvider,
feeMarketProvider: FeeMarketProvider,
pollInterval = 30_000,
historyDepth = 20,
info: BitcoinWalletInfo,
Expand All @@ -133,6 +135,7 @@ export class BitcoinWallet {

// TODO: Allow this to be injected.
this.inputResolver = new BlockchainInputResolver(provider);
this.feeMarketProvider = feeMarketProvider;

const networkKeys = getNetworkKeys(info, network);
const extendedAccountPubKey = networkKeys.nativeSegWit;
Expand Down Expand Up @@ -175,46 +178,7 @@ export class BitcoinWallet {
* Fetches the current fee market for estimating transaction fees.
*/
public async getCurrentFeeMarket(): Promise<EstimatedFees> {
try {
if (this.network === Network.Testnet) {
return {
fast: {
feeRate: 0.000_025,
targetConfirmationTime: 1
},
standard: {
feeRate: 0.000_015,
targetConfirmationTime: 3
},
slow: {
feeRate: 0.000_01,
targetConfirmationTime: 6
}
};
}

const fastEstimate = await this.provider.estimateFee(1, FeeEstimationMode.Conservative);
const standardEstimate = await this.provider.estimateFee(3, FeeEstimationMode.Conservative);
const slowEstimate = await this.provider.estimateFee(6, FeeEstimationMode.Conservative);

return {
fast: {
feeRate: fastEstimate.feeRate,
targetConfirmationTime: fastEstimate.blocks * 10 * 60
},
standard: {
feeRate: standardEstimate.feeRate,
targetConfirmationTime: standardEstimate.blocks * 10 * 60
},
slow: {
feeRate: slowEstimate.feeRate,
targetConfirmationTime: slowEstimate.blocks * 10 * 60
}
};
} catch (error) {
this.logger.error('Failed to fetch fee market:', error);
throw error;
}
return this.feeMarketProvider.getFeeMarket();
}

/**
Expand Down
16 changes: 16 additions & 0 deletions packages/bitcoin/src/wallet/lib/wallet/FeeMarketProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { EstimatedFees } from '../wallet/BitcoinWallet';

/**
* Interface for providing a fee estimation strategy.
*
* Implementations of this interface are responsible for retrieving
* current fee market data from a source and returning a normalized fee structure.
*/
export interface FeeMarketProvider {
/**
* Retrieves current fee estimates.
*
* @returns A promise resolving to the latest fee estimates.
*/
getFeeMarket(): Promise<EstimatedFees>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* eslint-disable no-magic-numbers */
import { EstimatedFees } from './BitcoinWallet';
import { FeeEstimationMode } from '../providers/BitcoinDataProvider';
import { MaestroBitcoinDataProvider } from '../providers/MaestroBitcoinDataProvider';
import { Network } from '../common/network';
import { Logger } from 'ts-log';
import { FeeMarketProvider } from './FeeMarketProvider';
import { DEFAULT_MARKETS } from './constants';

export class MaestroFeeMarketProvider implements FeeMarketProvider {
constructor(
private readonly provider: MaestroBitcoinDataProvider,
private readonly logger: Logger,
private readonly network: Network = Network.Mainnet
) {}

async getFeeMarket(): Promise<EstimatedFees> {
try {
if (this.network === Network.Testnet) {
return DEFAULT_MARKETS;
}

const fastEstimate = await this.provider.estimateFee(1, FeeEstimationMode.Economical);
const standardEstimate = await this.provider.estimateFee(3, FeeEstimationMode.Economical);
const slowEstimate = await this.provider.estimateFee(6, FeeEstimationMode.Economical);

return {
fast: {
feeRate: fastEstimate.feeRate,
targetConfirmationTime: fastEstimate.blocks * 10 * 60
},
standard: {
feeRate: standardEstimate.feeRate,
targetConfirmationTime: standardEstimate.blocks * 10 * 60
},
slow: {
feeRate: slowEstimate.feeRate,
targetConfirmationTime: slowEstimate.blocks * 10 * 60
}
};
} catch (error) {
this.logger.error('Failed to fetch fee market:', error);
}

return DEFAULT_MARKETS;
}
}
Loading
Loading