Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Blockchain Clients

LWK supports different ways to retrieve wallet data from the Liquid blockchain:

  • Electrum - TCP-based protocol, widely supported
  • Esplora - HTTP-based REST API, browser-compatible
  • Waterfalls - Optimized HTTP-based protocol with reduced roundtrips

Some clients also come in different flavors: blocking or async.

For production, all three clients can connect to Blockstream Enterprise authenticated, paid instances, see Authenticated connections.

Quick Comparison

FeatureElectrumEsploraWaterfalls
ProtocolTCPHTTP/HTTPSHTTP/HTTPS
Browser Support❌ No✅ Yes✅ Yes
Mobile Support✅ Yes✅ Yes✅ Yes
Sync Speed🏃 Average🐢 Slower🚀 Fastest
RoundtripsMany but batchedManyFew
Async Support❌ No✅ Yes✅ Yes
Authentication❌ No✅ OAuth2✅ OAuth2
Maturity⭐⭐⭐ Mature⭐⭐⭐ Mature⭐⭐ New

Electrum

The Electrum protocol is the most widely used light-client syncing mechanism for Bitcoin and Liquid wallets.

Key characteristics:

  • Protocol: TCP-based
  • Performance: Good
  • Availability: Only blocking variant
  • Platform support: Desktop, mobile, and server applications
  • Browser support: ❌ No (TCP not available in browsers)
  • Default servers: Blockstream public Electrum servers

This client is recommended for desktop, mobile, and server applications where interoperability is critical. By default, Blockstream public Electrum servers are used, but you can also specify custom URLs for private or local deployments.

Rust
use lwk_wollet::{ElectrumClient, ElectrumUrl};

let electrum_url = ElectrumUrl::new("blockstream.info:995", true, true)?;
let mut client = ElectrumClient::new(&electrum_url)?;
Python
# Create electrum client with custom URL
client = ElectrumClient("blockstream.info:995", tls=True, validate_domain=True)

# Or use the default electrum client for the network
default_client = Network.mainnet().default_electrum_client()
Javascript
Go
// Create electrum client with custom URL
electrumClient, err := lwk.NewElectrumClient("blockstream.info:995", true, true)
if err != nil {
    log.Fatal(err)
}

// Or use the default electrum client for the network
defaultClient, err := lwk.NetworkMainnet().DefaultElectrumClient()
if err != nil {
    log.Fatal(err)
}

Esplora

The Esplora client is based on the Esplora API, a popular HTTP-based blockchain explorer API.

Key characteristics:

  • Protocol: HTTP/HTTPS REST API
  • Performance: Multiple roundtrips required for wallet sync
  • Availability: Both blocking and async variants
  • Browser support: ✅ Yes, works in web browsers
  • Authentication: Supports OAuth2 for enterprise deployments

This client is ideal for web applications and scenarios where HTTP-based communication is required. While it requires more roundtrips than Electrum, it's the only option for browser-based applications and offers broad compatibility.

Rust
use lwk_wollet::clients::blocking::EsploraClient;

let esplora_url = "https://blockstream.info/liquid/api";
let mut client = EsploraClient::new(esplora_url, Network::Liquid)?;
Python
url = "https://blockstream.info/liquid/api"
client = EsploraClient(url, Network.mainnet())
Javascript
const url_esplora = "https://blockstream.info/liquid/api";
const esplora_client = new lwk.EsploraClient(lwk.Network.mainnet(), url_esplora, true, 4, false);
Go
esploraClient, err := lwk.NewEsploraClient("https://blockstream.info/liquid/api", lwk.NetworkMainnet())
if err != nil {
    log.Fatal(err)
}

Waterfalls

Waterfalls is an optimized blockchain indexer designed to significantly reduce the number of roundtrips required for wallet synchronization compared to traditional Esplora.

Key characteristics:

  • Protocol: HTTP/HTTPS REST API (Esplora-compatible with extensions)
  • Performance: Fewer roundtrips than standard Esplora, faster sync times
  • Availability: Both blocking and async variants
  • Browser support: ✅ Yes, works in web browsers
  • Maturity: Newer technology, still evolving

Important: The public Waterfalls instance shown in the examples (waterfalls.liquidwebwallet.org) is provided for testing and development only.

Rust
let waterfalls_url = "https://waterfalls.liquidwebwallet.org/liquid/api";
let mut client = WaterfallsClientBuilder::new(waterfalls_url, Network::Liquid).build()?;
Python
url = "https://waterfalls.liquidwebwallet.org/liquid/api"
client = WaterfallsClient(url, Network.mainnet())
Javascript
const url_waterfalls = "https://waterfalls.liquidwebwallet.org/liquid/api";
const waterfalls_client = new lwk.WaterfallsClient(lwk.Network.mainnet(), url_waterfalls);
Go
waterfallsClient, err := lwk.NewWaterfallsClient("https://waterfalls.liquidwebwallet.org/liquid/api", lwk.NetworkMainnet())
if err != nil {
    log.Fatal(err)
}

Fallback Client

For improved resilience, implement a fallback strategy to handle transient errors. This pattern is useful when dealing with unreliable network conditions or temporary server issues.

When a primary request fails, manually evaluate the error to determine if a retry is appropriate with a different client.

Rust
let mut client = ElectrumClient::new(&primary_url)?;

let update = match client.full_scan(&wollet) {
    Ok(x) => Ok(x),
    Err(_e) => {
        // Falling into a retryable error, making a request with the fallback client
        let mut fallback_client = ElectrumClient::new(&fallback_url)?;
        fallback_client.full_scan(&wollet)
    }
}?;

if let Some(update) = update {
    wollet.apply_update(update)?;
}
Python
client = ElectrumClient.from_url(primary_url)

try:
    update = client.full_scan(wollet)

except Exception:
    # Falling into a retryable error, making a request with the fallback client
    fallback_client = ElectrumClient.from_url(fallback_url)
    update = fallback_client.full_scan(wollet)

wollet.apply_update(update)
Javascript
const client = new lwk.EsploraClient(network, primary_url, waterfalls, concurrency, utxo_only);

let update;

try {
    update = await client.fullScan(wollet);
} catch (error) {
    // Falling into a retryable error, making a request with the fallback client
    const fallbackClient = new lwk.EsploraClient(network, fallback_url, waterfalls, concurrency, utxo_only);
    update = await fallbackClient.fullScan(wollet);
}

if (update) {
    wollet.applyUpdate(update);
}
Go
client, err := lwk.ElectrumClientFromUrl(primaryUrl)
if err != nil {
    log.Fatal(err)
}

update, err := client.FullScan(wollet)
if err != nil {
    // Falling into a retryable error, making a request with the fallback client
    fallbackClient, err := lwk.ElectrumClientFromUrl(fallbackUrl)
    if err != nil {
        log.Fatal(err)
    }

    update, err = fallbackClient.FullScan(wollet)
    if err != nil {
        log.Fatal(err)
    }
}

if update != nil {
    if err := wollet.ApplyUpdate(*update); err != nil {
        log.Fatal(err)
    }
}

Authenticated connections

Blockstream runs paid, authenticated instances of these APIs for production use, Blockstream Enterprise: dedicated infrastructure with guaranteed rate limits, higher quotas, and greater privacy than the shared public servers. If you are shipping a product on Liquid, these are the endpoints to build against.

All three clients authenticate the same way. Point the client at your enterprise endpoint and add an OAuth2 token provider; the client fetches a token with your credentials and refreshes it automatically, so the rest of your code is unchanged from the public client.

Endpoints

Mainnet Liquid enterprise endpoints:

APIEndpointTransport
Esplora (REST)https://enterprise.blockstream.info/liquid/apiHTTPS
Waterfallshttps://enterprise.blockstream.info/liquid/api/waterfallsHTTPS
Electrum RPCssl://elements-mainnet.enterprise.blockstream.info:50002TLS
OAuth2 tokenhttps://login.blockstream.com/realms/blockstream-public/protocol/openid-connect/tokenHTTPS

The table lists mainnet Liquid. For Liquid testnet, swap the host prefix and path to elements-testnet and liquidtestnet (for example ssl://elements-testnet.enterprise.blockstream.info:50002 and https://enterprise.blockstream.info/liquidtestnet/api); the OAuth2 token endpoint is unchanged.

Token providers

  • TokenProvider::Blockstream { url, client_id, client_secret } fetches a token from the OAuth2 endpoint and refreshes it automatically.
  • TokenProvider::Static(token) uses a token you already hold (no refresh).

Notes:

  • Electrum needs the electrum_oidc cargo feature (a default feature of lwk_wollet).
  • The token is only sent over an encrypted connection. On a plaintext tcp:// Electrum url it is refused unless explicitly allowed (for a localhost or already-tunneled proxy: allow_plaintext_with_token in the builder, or --auth-allow-plaintext-with-token in lwk_cli).
  • Esplora and Waterfalls address the enterprise load balancer by path (/liquid/api, /liquid/api/waterfalls); Electrum uses a network-prefixed host.
  • In the browser (wasm), authenticated Esplora/Waterfalls is not yet available, and Electrum has no browser path.

The snippets below show the client wiring; take the endpoint urls from the table above.

Esplora

Rust
let mut client = clients::asyncr::EsploraClientBuilder::new(base_url, network)
    .token_provider(clients::TokenProvider::Blockstream {
        url: login_url.to_string(),
        client_id: client_id.to_string(),
        client_secret: client_secret.to_string(),
    })
    .build()
    .unwrap();
Python
builder = EsploraClientBuilder(
    base_url=base_url,
    network=network,
    token_provider=TokenProvider.BLOCKSTREAM(
        url=login_url,
        client_id=client_id,
        client_secret=client_secret,
    ),
)
client = EsploraClient.from_builder(builder)
Go
var tpBlockstream lwk.TokenProvider = lwk.TokenProviderBlockstream{
    Url:          loginUrl,
    ClientId:     clientId,
    ClientSecret: clientSecret,
}

builder := lwk.EsploraClientBuilder{
    BaseUrl:       baseUrl,
    Network:       network,
    TokenProvider: &tpBlockstream,
}
client, err := lwk.EsploraClientFromBuilder(builder)

Waterfalls

Rust
let client = clients::WaterfallsClientBuilder::new(base_url, network)
    .token_provider(clients::TokenProvider::Blockstream {
        url: login_url.to_string(),
        client_id: client_id.to_string(),
        client_secret: client_secret.to_string(),
    })
    .build_blocking()
    .unwrap();

Electrum

Rust
// The token provider needs the `electrum_oidc` cargo feature.
let mut client = ElectrumClientBuilder::new(url)
    .token_provider(clients::TokenProvider::Blockstream {
        url: login_url.to_string(),
        client_id: client_id.to_string(),
        client_secret: client_secret.to_string(),
    })
    .build()
    .unwrap();
Python
builder = ElectrumClientBuilder(
    url=url,
    token_provider=TokenProvider.BLOCKSTREAM(
        url=login_url, client_id=client_id, client_secret=client_secret
    ),
    timeout=30,
)
# Building connects to the proxy and mints the OAuth token (the connection's first
# message carries it); the client is then ready for authenticated calls.
client = ElectrumClient.from_builder(builder)
tip = client.tip()