Make the Esplora API base URL configurable

Hardcoded MEMPOOL_API constant in src/lib/bitcoin.ts becomes a baseUrl
parameter on every fetch helper, sourced from a new `esploraBaseUrl`
field on AppConfig (default `https://mempool.space/api`). The wallet,
zap dialogs, on-chain zap verification, and NIP-73 Bitcoin tx/address
pages now read the URL from useAppContext and pass it through, so
self-hosted Esplora deployments (or Blockstream's) work without code
changes. The mempool.space-specific `/v1/prices` extension is still
appended by fetchBtcPrice.
This commit is contained in:
Alex Gleason
2026-05-13 11:23:36 -05:00
parent 0022b86299
commit 474ec6cc99
15 changed files with 168 additions and 85 deletions
+13 -13
View File
@@ -91,15 +91,15 @@ bitcoin.initEccLib(ecc);
## Balance & Transaction APIs
All Bitcoin data is fetched from the public [mempool.space](https://mempool.space) Esplora-compatible API:
All Bitcoin data is fetched from an [Esplora](https://github.com/Blockstream/esplora/blob/master/API.md)-compatible REST API. The base URL is read from `AppConfig.esploraBaseUrl` (default: `https://mempool.space/api`) and can be overridden in `ditto.json` at build time or in Settings at runtime. Any Esplora-compatible backend works (mempool.space, Blockstream, self-hosted).
| Endpoint | Purpose |
| Endpoint (relative to `esploraBaseUrl`) | Purpose |
|---|---|
| `GET https://mempool.space/api/address/{address}` | Balance stats (funded/spent sums, tx counts) |
| `GET https://mempool.space/api/address/{address}/txs` | Transaction history for an address |
| `GET https://mempool.space/api/tx/{txid}` | Full transaction detail (inputs, outputs, fee, block) |
| `GET /address/{address}` | Balance stats (funded/spent sums, tx counts) |
| `GET /address/{address}/txs` | Transaction history for an address |
| `GET /tx/{txid}` | Full transaction detail (inputs, outputs, fee, block) |
The wallet page polls balance and transaction data every 30 seconds. BTC/USD price is fetched from mempool.space every 60 seconds.
The wallet page polls balance and transaction data every 30 seconds. BTC/USD price is fetched every 60 seconds via `GET /v1/prices` — a mempool.space-specific extension that is not part of the standard Esplora REST surface.
## NIP-73 Integration
@@ -140,9 +140,9 @@ The signer classes (`NSecSignerBtc`, `NBrowserSignerBtc`, `NConnectSignerBtc`) e
The send flow constructs a standard Taproot (P2TR) key-path spend:
1. **Fetch UTXOs** -- All unspent outputs for the sender's address are retrieved from `mempool.space/api/address/{address}/utxo`.
1. **Fetch UTXOs** -- All unspent outputs for the sender's address are retrieved from `GET {esploraBaseUrl}/address/{address}/utxo`.
2. **Fetch fee rates** -- Recommended fee rates (sat/vB) for four confirmation targets are retrieved from `mempool.space/api/fee-estimates`:
2. **Fetch fee rates** -- Recommended fee rates (sat/vB) for four confirmation targets are retrieved from `GET {esploraBaseUrl}/fee-estimates`:
| Speed | Block target | Typical wait |
|---|---|---|
@@ -170,7 +170,7 @@ The send flow constructs a standard Taproot (P2TR) key-path spend:
For extension and bunker signers, the tweaking is handled by the external signer.
8. **Finalize and broadcast** -- The signed PSBT is finalized, the raw transaction hex is extracted, and POSTed to `mempool.space/api/tx`, which returns the txid on success.
8. **Finalize and broadcast** -- The signed PSBT is finalized, the raw transaction hex is extracted, and POSTed to `{esploraBaseUrl}/tx`, which returns the txid on success.
### User Flow
@@ -182,11 +182,11 @@ The send dialog has three steps:
### Additional API Endpoints
| Endpoint | Purpose |
| Endpoint (relative to `esploraBaseUrl`) | Purpose |
|---|---|
| `GET https://mempool.space/api/address/{address}/utxo` | Unspent transaction outputs |
| `GET https://mempool.space/api/fee-estimates` | Recommended fee rates by block target |
| `POST https://mempool.space/api/tx` | Broadcast signed transaction (raw hex body) |
| `GET /address/{address}/utxo` | Unspent transaction outputs |
| `GET /fee-estimates` | Recommended fee rates by block target |
| `POST /tx` | Broadcast signed transaction (raw hex body) |
### Dependencies (sending-specific)
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "ditto",
"version": "2.14.3",
"version": "2.14.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ditto",
"version": "2.14.3",
"version": "2.14.4",
"dependencies": {
"@bitcoinerlab/secp256k1": "^1.2.0",
"@capacitor/app": "^8.0.0",
+1
View File
@@ -153,6 +153,7 @@ const hardcodedConfig: AppConfig = {
imageQuality: 'compressed',
curatorPubkey: '932614571afcbad4d17a191ee281e39eebbb41b93fac8fd87829622aeb112f4d',
sandboxDomain: 'iframe.diy',
esploraBaseUrl: 'https://mempool.space/api',
sidebarWidgets: [
{ id: 'trends' },
{ id: 'hot-posts' },
+9 -6
View File
@@ -17,6 +17,7 @@ import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useBitcoinSigner } from '@/hooks/useBitcoinSigner';
import { useOnchainZap, type OnchainFeeSpeed } from '@/hooks/useOnchainZap';
import { useToast } from '@/hooks/useToast';
import { useAppContext } from '@/hooks/useAppContext';
import { useNostrLogin } from '@nostrify/react/login';
import {
nostrPubkeyToBitcoinAddress,
@@ -94,6 +95,8 @@ export function OnchainZapContent({ target, onSuccess, onClose }: OnchainZapCont
const { user } = useCurrentUser();
const { capability } = useBitcoinSigner();
const { logins } = useNostrLogin();
const { config } = useAppContext();
const { esploraBaseUrl } = config;
const loginType = logins[0]?.type;
const [usdAmount, setUsdAmount] = useState<number | string>(5);
@@ -114,21 +117,21 @@ export function OnchainZapContent({ target, onSuccess, onClose }: OnchainZapCont
: '';
const { data: btcPrice } = useQuery({
queryKey: ['btc-price'],
queryFn: fetchBtcPrice,
queryKey: ['btc-price', esploraBaseUrl],
queryFn: () => fetchBtcPrice(esploraBaseUrl),
staleTime: 30_000,
});
const { data: utxos } = useQuery({
queryKey: ['bitcoin-utxos', senderAddress],
queryFn: () => fetchUTXOs(senderAddress),
queryKey: ['bitcoin-utxos', esploraBaseUrl, senderAddress],
queryFn: () => fetchUTXOs(senderAddress, esploraBaseUrl),
enabled: !!senderAddress && capability !== 'unsupported',
staleTime: 30_000,
});
const { data: feeRates } = useQuery({
queryKey: ['bitcoin-fee-rates'],
queryFn: getFeeRates,
queryKey: ['bitcoin-fee-rates', esploraBaseUrl],
queryFn: () => getFeeRates(esploraBaseUrl),
enabled: capability !== 'unsupported',
staleTime: 30_000,
});
+8 -5
View File
@@ -33,6 +33,7 @@ import { Skeleton } from '@/components/ui/skeleton';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useBitcoinSigner } from '@/hooks/useBitcoinSigner';
import { useToast } from '@/hooks/useToast';
import { useAppContext } from '@/hooks/useAppContext';
import {
nostrPubkeyToBitcoinAddress,
npubToBitcoinAddress,
@@ -108,6 +109,8 @@ export function SendBitcoinDialog({ isOpen, onClose, btcPrice }: SendBitcoinDial
const { user } = useCurrentUser();
const { canSignPsbt, signPsbt } = useBitcoinSigner();
const { toast } = useToast();
const { config } = useAppContext();
const { esploraBaseUrl } = config;
const queryClient = useQueryClient();
// Form state
@@ -126,15 +129,15 @@ export function SendBitcoinDialog({ isOpen, onClose, btcPrice }: SendBitcoinDial
// ── Data fetching ──────────────────────────────────────────────
const { data: utxos, isLoading: isLoadingUtxos } = useQuery({
queryKey: ['bitcoin-utxos', senderAddress],
queryFn: () => fetchUTXOs(senderAddress),
queryKey: ['bitcoin-utxos', esploraBaseUrl, senderAddress],
queryFn: () => fetchUTXOs(senderAddress, esploraBaseUrl),
enabled: !!senderAddress && isOpen,
staleTime: 30_000,
});
const { data: feeRates, isLoading: isLoadingFees } = useQuery({
queryKey: ['bitcoin-fee-rates'],
queryFn: getFeeRates,
queryKey: ['bitcoin-fee-rates', esploraBaseUrl],
queryFn: () => getFeeRates(esploraBaseUrl),
enabled: isOpen,
staleTime: 30_000,
});
@@ -202,7 +205,7 @@ export function SendBitcoinDialog({ isOpen, onClose, btcPrice }: SendBitcoinDial
// 3. Finalize and extract raw tx
const txHex = finalizePsbt(signedHex);
const id = await broadcastTransaction(txHex);
const id = await broadcastTransaction(txHex, esploraBaseUrl);
return { txId: id, fee };
},
onSuccess: ({ txId: id, fee }) => {
+5 -2
View File
@@ -23,6 +23,7 @@ import { useBitcoinSigner } from '@/hooks/useBitcoinSigner';
import { useToast } from '@/hooks/useToast';
import { useZaps } from '@/hooks/useZaps';
import { useWallet } from '@/hooks/useWallet';
import { useAppContext } from '@/hooks/useAppContext';
import { canZap } from '@/lib/canZap';
import {
fetchBtcPrice,
@@ -287,6 +288,8 @@ export function ZapDialog({ target, children, className }: ZapDialogProps) {
const { data: author } = useAuthor(target.pubkey);
const { toast } = useToast();
const { webln, activeNWC } = useWallet();
const { config } = useAppContext();
const { esploraBaseUrl } = config;
// Success state: populated by either zap rail's onSuccess callback.
// When set, we replace the tab UI with <ZapSuccessScreen />.
@@ -320,8 +323,8 @@ export function ZapDialog({ target, children, className }: ZapDialogProps) {
const amountInputRef = useRef<HTMLInputElement>(null);
const { data: btcPrice } = useQuery({
queryKey: ['btc-price'],
queryFn: fetchBtcPrice,
queryKey: ['btc-price', esploraBaseUrl],
queryFn: () => fetchBtcPrice(esploraBaseUrl),
staleTime: 30_000,
});
+7
View File
@@ -278,6 +278,13 @@ export interface AppConfig {
curatorPubkey?: string;
/** Wildcard domain used for iframe sandboxing (e.g. "iframe.diy"). Default: "iframe.diy". */
sandboxDomain: string;
/**
* Base URL for the Esplora-compatible Bitcoin REST API. Used by the wallet,
* on-chain zap flows, and NIP-73 Bitcoin tx/address pages. The standard
* Esplora REST root (no version segment). The mempool.space `/v1/prices`
* extension is appended by the price call. Default: "https://mempool.space/api".
*/
esploraBaseUrl: string;
/** Ordered list of right sidebar widget configs. Each entry is a widget type ID with optional display settings. */
sidebarWidgets: WidgetConfig[];
}
+11 -6
View File
@@ -1,22 +1,27 @@
import { useQuery } from '@tanstack/react-query';
import { fetchAddressDetail, fetchBtcPrice } from '@/lib/bitcoin';
import { useAppContext } from '@/hooks/useAppContext';
/**
* Fetch full address details (balance + recent txs) via the mempool.space API.
* Also fetches the current BTC/USD price for display.
* Fetch full address details (balance + recent txs) via the configured
* Esplora-compatible API (default: mempool.space). Also fetches the
* current BTC/USD price for display.
*/
export function useBitcoinAddress(address: string) {
const { config } = useAppContext();
const { esploraBaseUrl } = config;
const { data: addressDetail, isLoading, error, refetch } = useQuery({
queryKey: ['bitcoin-address-detail', address],
queryFn: () => fetchAddressDetail(address),
queryKey: ['bitcoin-address-detail', esploraBaseUrl, address],
queryFn: () => fetchAddressDetail(address, esploraBaseUrl),
enabled: !!address,
refetchInterval: 30_000,
});
const { data: btcPrice } = useQuery({
queryKey: ['btc-price'],
queryFn: fetchBtcPrice,
queryKey: ['btc-price', esploraBaseUrl],
queryFn: () => fetchBtcPrice(esploraBaseUrl),
refetchInterval: 60_000,
staleTime: 30_000,
});
+11 -6
View File
@@ -1,22 +1,27 @@
import { useQuery } from '@tanstack/react-query';
import { fetchTxDetail, fetchBtcPrice } from '@/lib/bitcoin';
import { useAppContext } from '@/hooks/useAppContext';
/**
* Fetch full transaction details for a Bitcoin txid via the mempool.space API.
* Also fetches the current BTC/USD price for display.
* Fetch full transaction details for a Bitcoin txid via the configured
* Esplora-compatible API (default: mempool.space). Also fetches the
* current BTC/USD price for display.
*/
export function useBitcoinTx(txid: string) {
const { config } = useAppContext();
const { esploraBaseUrl } = config;
const { data: tx, isLoading, error } = useQuery({
queryKey: ['bitcoin-tx-detail', txid],
queryFn: () => fetchTxDetail(txid),
queryKey: ['bitcoin-tx-detail', esploraBaseUrl, txid],
queryFn: () => fetchTxDetail(txid, esploraBaseUrl),
enabled: !!txid,
staleTime: 60_000,
});
const { data: btcPrice } = useQuery({
queryKey: ['btc-price'],
queryFn: fetchBtcPrice,
queryKey: ['btc-price', esploraBaseUrl],
queryFn: () => fetchBtcPrice(esploraBaseUrl),
refetchInterval: 60_000,
staleTime: 30_000,
});
+11 -7
View File
@@ -2,17 +2,21 @@ import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useAppContext } from '@/hooks/useAppContext';
import { nostrPubkeyToBitcoinAddress, fetchAddressData, fetchBtcPrice, fetchTransactions } from '@/lib/bitcoin';
/**
* Hook that derives a Bitcoin Taproot address from the current user's Nostr
* pubkey and fetches the on-chain balance from the Blockstream API.
* pubkey and fetches the on-chain balance from the configured Esplora-compatible
* API (default: mempool.space).
*
* Balance auto-refreshes every 30 seconds while the component is mounted.
* BTC/USD price refreshes every 60 seconds.
*/
export function useBitcoinWallet() {
const { user } = useCurrentUser();
const { config } = useAppContext();
const { esploraBaseUrl } = config;
const bitcoinAddress = useMemo(() => {
if (!user) return '';
@@ -25,15 +29,15 @@ export function useBitcoinWallet() {
error,
refetch,
} = useQuery({
queryKey: ['bitcoin-balance', bitcoinAddress],
queryFn: () => fetchAddressData(bitcoinAddress),
queryKey: ['bitcoin-balance', esploraBaseUrl, bitcoinAddress],
queryFn: () => fetchAddressData(bitcoinAddress, esploraBaseUrl),
enabled: !!bitcoinAddress,
refetchInterval: 30_000,
});
const { data: btcPrice } = useQuery({
queryKey: ['btc-price'],
queryFn: fetchBtcPrice,
queryKey: ['btc-price', esploraBaseUrl],
queryFn: () => fetchBtcPrice(esploraBaseUrl),
refetchInterval: 60_000,
staleTime: 30_000,
});
@@ -42,8 +46,8 @@ export function useBitcoinWallet() {
data: transactions,
isLoading: isLoadingTxs,
} = useQuery({
queryKey: ['bitcoin-txs', bitcoinAddress],
queryFn: () => fetchTransactions(bitcoinAddress),
queryKey: ['bitcoin-txs', esploraBaseUrl, bitcoinAddress],
queryFn: () => fetchTransactions(bitcoinAddress, esploraBaseUrl),
enabled: !!bitcoinAddress,
refetchInterval: 30_000,
});
+6 -3
View File
@@ -6,6 +6,7 @@ import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useBitcoinSigner, isSignerCapabilityError, reportSignerUnsupported } from '@/hooks/useBitcoinSigner';
import { useNostrPublish } from '@/hooks/useNostrPublish';
import { useToast } from '@/hooks/useToast';
import { useAppContext } from '@/hooks/useAppContext';
import { notificationSuccess } from '@/lib/haptics';
import {
nostrPubkeyToBitcoinAddress,
@@ -72,6 +73,8 @@ export function useOnchainZap(
const { canSignPsbt, signPsbt } = useBitcoinSigner();
const { mutateAsync: publishEvent } = useNostrPublish();
const { toast } = useToast();
const { config } = useAppContext();
const { esploraBaseUrl } = config;
const queryClient = useQueryClient();
const [isZapping, setIsZapping] = useState(false);
@@ -101,8 +104,8 @@ export function useOnchainZap(
// Fetch UTXOs and fee rates
const [utxos, rates] = await Promise.all([
fetchUTXOs(senderAddress),
getFeeRates(),
fetchUTXOs(senderAddress, esploraBaseUrl),
getFeeRates(esploraBaseUrl),
]);
if (utxos.length === 0) {
@@ -134,7 +137,7 @@ export function useOnchainZap(
// Broadcast
setProgress('broadcasting');
const txid = await broadcastTransaction(txHex);
const txid = await broadcastTransaction(txHex, esploraBaseUrl);
// Publish kind 8333 event
setProgress('publishing');
+17 -6
View File
@@ -3,6 +3,7 @@ import { useNostr } from '@nostrify/react';
import type { NostrEvent } from '@nostrify/nostrify';
import { fetchTxDetail, nostrPubkeyToBitcoinAddress } from '@/lib/bitcoin';
import { useAppContext } from '@/hooks/useAppContext';
/** A single verified on-chain zap, with the amount that actually paid the recipient on-chain. */
export interface OnchainZapEntry {
/** The kind 8333 event. */
@@ -57,8 +58,14 @@ export function extractOnchainZapRecipient(event: NostrEvent): string {
*
* A verified amount of 0 means the transaction exists but does not pay
* the claimed recipient — callers should discard such events.
*
* @param event The kind 8333 event to verify.
* @param esploraBaseUrl Esplora REST root used to fetch the tx detail.
*/
export async function verifyOnchainZap(event: NostrEvent): Promise<OnchainZapEntry | null> {
export async function verifyOnchainZap(
event: NostrEvent,
esploraBaseUrl: string,
): Promise<OnchainZapEntry | null> {
const txid = extractOnchainZapTxid(event);
const recipientPubkey = extractOnchainZapRecipient(event);
if (!txid || !recipientPubkey) return null;
@@ -73,7 +80,7 @@ export async function verifyOnchainZap(event: NostrEvent): Promise<OnchainZapEnt
let detail;
try {
detail = await fetchTxDetail(txid);
detail = await fetchTxDetail(txid, esploraBaseUrl);
} catch {
return null;
}
@@ -107,6 +114,8 @@ export async function verifyOnchainZap(event: NostrEvent): Promise<OnchainZapEnt
*/
export function useOnchainZaps(target: NostrEvent | undefined) {
const { nostr } = useNostr();
const { config } = useAppContext();
const { esploraBaseUrl } = config;
const isAddressable = target && target.kind >= 30000 && target.kind < 40000;
const dTag = isAddressable
? target.tags.find(([n]) => n === 'd')?.[1] ?? ''
@@ -155,8 +164,8 @@ export function useOnchainZaps(target: NostrEvent | undefined) {
const events = eventsQuery.data ?? [];
const verifications = useQueries({
queries: events.map((event) => ({
queryKey: ['onchain-zaps', 'verify', extractOnchainZapTxid(event), extractOnchainZapRecipient(event)],
queryFn: () => verifyOnchainZap(event),
queryKey: ['onchain-zaps', 'verify', esploraBaseUrl, extractOnchainZapTxid(event), extractOnchainZapRecipient(event)],
queryFn: () => verifyOnchainZap(event, esploraBaseUrl),
staleTime: 60_000,
})),
});
@@ -189,12 +198,14 @@ export function useOnchainZaps(target: NostrEvent | undefined) {
* (invalid tx, wrong recipient, self-zap, etc.), or the entry.
*/
export function useVerifiedOnchainZap(event: NostrEvent | undefined): OnchainZapEntry | null | undefined {
const { config } = useAppContext();
const { esploraBaseUrl } = config;
const txid = event ? extractOnchainZapTxid(event) : null;
const recipient = event ? extractOnchainZapRecipient(event) : '';
const { data } = useQuery({
queryKey: ['onchain-zaps', 'verify', txid, recipient],
queryFn: () => verifyOnchainZap(event!),
queryKey: ['onchain-zaps', 'verify', esploraBaseUrl, txid, recipient],
queryFn: () => verifyOnchainZap(event!, esploraBaseUrl),
enabled: !!event && !!txid && !!recipient,
staleTime: 60_000,
});
+65 -29
View File
@@ -8,9 +8,6 @@ import { ECPairFactory, type ECPairAPI } from 'ecpair';
// Constants
// ---------------------------------------------------------------------------
/** Base URL for the mempool.space Esplora-compatible REST API. */
const MEMPOOL_API = 'https://mempool.space/api';
/** Standard Bitcoin dust limit in satoshis. */
const DUST_LIMIT = 546;
@@ -108,11 +105,14 @@ export interface AddressData {
}
/**
* Fetch balance and transaction stats for a Bitcoin address from the
* mempool.space Esplora API.
* Fetch balance and transaction stats for a Bitcoin address from an
* Esplora-compatible REST API (e.g. mempool.space, Blockstream).
*
* @param address The Bitcoin address to look up.
* @param baseUrl Esplora REST root, no trailing slash (e.g. `https://mempool.space/api`).
*/
export async function fetchAddressData(address: string): Promise<AddressData> {
const response = await fetch(`${MEMPOOL_API}/address/${address}`);
export async function fetchAddressData(address: string, baseUrl: string): Promise<AddressData> {
const response = await fetch(`${baseUrl}/address/${address}`);
if (!response.ok) {
throw new Error('Failed to fetch balance');
@@ -156,9 +156,17 @@ export function formatSats(sats: number): string {
return sats.toLocaleString();
}
/** Fetch the current BTC price in USD from the mempool.space API. */
export async function fetchBtcPrice(): Promise<number> {
const response = await fetch(`${MEMPOOL_API}/v1/prices`);
/**
* Fetch the current BTC price in USD from a mempool.space-compatible API.
*
* Note: the `/v1/prices` endpoint is a mempool.space extension to the
* standard Esplora REST surface. Backends like Blockstream's Esplora do
* not expose it.
*
* @param baseUrl Esplora REST root, no trailing slash (e.g. `https://mempool.space/api`).
*/
export async function fetchBtcPrice(baseUrl: string): Promise<number> {
const response = await fetch(`${baseUrl}/v1/prices`);
if (!response.ok) {
throw new Error('Failed to fetch BTC price');
@@ -222,11 +230,14 @@ export interface Transaction {
}
/**
* Fetch transactions for a Bitcoin address from the mempool.space Esplora API.
* Fetch transactions for a Bitcoin address from an Esplora-compatible API.
* Returns simplified transactions with net amount relative to the address.
*
* @param address The Bitcoin address to look up.
* @param baseUrl Esplora REST root, no trailing slash.
*/
export async function fetchTransactions(address: string): Promise<Transaction[]> {
const response = await fetch(`${MEMPOOL_API}/address/${address}/txs`);
export async function fetchTransactions(address: string, baseUrl: string): Promise<Transaction[]> {
const response = await fetch(`${baseUrl}/address/${address}/txs`);
if (!response.ok) {
throw new Error('Failed to fetch transactions');
@@ -309,9 +320,14 @@ export interface TxDetail {
totalOutput: number;
}
/** Fetch full transaction details from mempool.space. */
export async function fetchTxDetail(txid: string): Promise<TxDetail> {
const response = await fetch(`${MEMPOOL_API}/tx/${txid}`);
/**
* Fetch full transaction details from an Esplora-compatible API.
*
* @param txid The transaction ID (hex).
* @param baseUrl Esplora REST root, no trailing slash.
*/
export async function fetchTxDetail(txid: string, baseUrl: string): Promise<TxDetail> {
const response = await fetch(`${baseUrl}/tx/${txid}`);
if (!response.ok) throw new Error('Failed to fetch transaction');
const tx = await response.json();
@@ -383,11 +399,16 @@ export interface AddressDetail {
recentTxs: Transaction[];
}
/** Fetch full address details (balance + recent txs) from mempool.space. */
export async function fetchAddressDetail(address: string): Promise<AddressDetail> {
/**
* Fetch full address details (balance + recent txs) from an Esplora-compatible API.
*
* @param address The Bitcoin address to look up.
* @param baseUrl Esplora REST root, no trailing slash.
*/
export async function fetchAddressDetail(address: string, baseUrl: string): Promise<AddressDetail> {
const [addrData, txs] = await Promise.all([
fetchAddressData(address),
fetchTransactions(address),
fetchAddressData(address, baseUrl),
fetchTransactions(address, baseUrl),
]);
return {
@@ -415,9 +436,14 @@ export interface UTXO {
};
}
/** Fetch UTXOs for a Bitcoin address from mempool.space. */
export async function fetchUTXOs(address: string): Promise<UTXO[]> {
const response = await fetch(`${MEMPOOL_API}/address/${address}/utxo`);
/**
* Fetch UTXOs for a Bitcoin address from an Esplora-compatible API.
*
* @param address The Bitcoin address to look up.
* @param baseUrl Esplora REST root, no trailing slash.
*/
export async function fetchUTXOs(address: string, baseUrl: string): Promise<UTXO[]> {
const response = await fetch(`${baseUrl}/address/${address}/utxo`);
if (!response.ok) throw new Error('Failed to fetch UTXOs');
return response.json();
}
@@ -436,9 +462,13 @@ export interface FeeRates {
minimumFee: number;
}
/** Fetch recommended fee rates (sat/vB) from mempool.space. */
export async function getFeeRates(): Promise<FeeRates> {
const response = await fetch(`${MEMPOOL_API}/fee-estimates`);
/**
* Fetch recommended fee rates (sat/vB) from an Esplora-compatible API.
*
* @param baseUrl Esplora REST root, no trailing slash.
*/
export async function getFeeRates(baseUrl: string): Promise<FeeRates> {
const response = await fetch(`${baseUrl}/fee-estimates`);
if (!response.ok) throw new Error('Failed to fetch fee estimates');
const data = await response.json();
@@ -477,9 +507,15 @@ export function validateBitcoinAddress(address: string): boolean {
}
}
/** Broadcast a signed transaction hex to the Bitcoin network via mempool.space. Returns the txid. */
export async function broadcastTransaction(txHex: string): Promise<string> {
const response = await fetch(`${MEMPOOL_API}/tx`, {
/**
* Broadcast a signed transaction hex to the Bitcoin network via an
* Esplora-compatible API. Returns the txid.
*
* @param txHex The signed transaction hex.
* @param baseUrl Esplora REST root, no trailing slash.
*/
export async function broadcastTransaction(txHex: string, baseUrl: string): Promise<string> {
const response = await fetch(`${baseUrl}/tx`, {
method: 'POST',
body: txHex,
});
+1
View File
@@ -261,6 +261,7 @@ export const AppConfigSchema = z.object({
imageQuality: z.enum(['compressed', 'original']),
curatorPubkey: z.string().regex(/^[0-9a-f]{64}$/i).optional(),
sandboxDomain: z.string().optional(),
esploraBaseUrl: z.string().url(),
sidebarWidgets: z.array(z.object({
id: z.string(),
height: z.number().optional(),
+1
View File
@@ -122,6 +122,7 @@ export function TestApp({ children }: TestAppProps) {
autoplayVideos: false,
imageQuality: 'compressed',
sandboxDomain: 'iframe.diy',
esploraBaseUrl: 'https://mempool.space/api',
sidebarWidgets: [],
};