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
| Feature | Electrum | Esplora | Waterfalls |
|---|---|---|---|
| Protocol | TCP | HTTP/HTTPS | HTTP/HTTPS |
| Browser Support | ❌ No | ✅ Yes | ✅ Yes |
| Mobile Support | ✅ Yes | ✅ Yes | ✅ Yes |
| Sync Speed | 🏃 Average | 🐢 Slower | 🚀 Fastest |
| Roundtrips | Many but batched | Many | Few |
| 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.
use lwk_wollet::{ElectrumClient, ElectrumUrl};
let electrum_url = ElectrumUrl::new("blockstream.info:995", true, true)?;
let mut client = ElectrumClient::new(&electrum_url)?;
# 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()
// 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.
use lwk_wollet::clients::blocking::EsploraClient;
let esplora_url = "https://blockstream.info/liquid/api";
let mut client = EsploraClient::new(esplora_url, Network::Liquid)?;
url = "https://blockstream.info/liquid/api"
client = EsploraClient(url, Network.mainnet())
const url_esplora = "https://blockstream.info/liquid/api";
const esplora_client = new lwk.EsploraClient(lwk.Network.mainnet(), url_esplora, true, 4, false);
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.
let waterfalls_url = "https://waterfalls.liquidwebwallet.org/liquid/api";
let mut client = WaterfallsClientBuilder::new(waterfalls_url, Network::Liquid).build()?;
url = "https://waterfalls.liquidwebwallet.org/liquid/api"
client = WaterfallsClient(url, Network.mainnet())
const url_waterfalls = "https://waterfalls.liquidwebwallet.org/liquid/api";
const waterfalls_client = new lwk.WaterfallsClient(lwk.Network.mainnet(), url_waterfalls);
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.
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)?;
}
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)
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);
}
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:
| API | Endpoint | Transport |
|---|---|---|
| Esplora (REST) | https://enterprise.blockstream.info/liquid/api | HTTPS |
| Waterfalls | https://enterprise.blockstream.info/liquid/api/waterfalls | HTTPS |
| Electrum RPC | ssl://elements-mainnet.enterprise.blockstream.info:50002 | TLS |
| OAuth2 token | https://login.blockstream.com/realms/blockstream-public/protocol/openid-connect/token | HTTPS |
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_oidccargo feature (a default feature oflwk_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_tokenin the builder, or--auth-allow-plaintext-with-tokeninlwk_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
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();
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)
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
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
// 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();
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()