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

About LWK

The Liquid Wallet Kit (LWK) is a comprehensive toolkit that empowers developers to build a new generation of wallets and applications for the Liquid Network. Instead of grappling with the intricate, low-level details of Liquid's confidential transactions, asset management, and cryptographic primitives, LWK provides a powerful set of foundational building blocks. These tools are functional and secure, helping you build your projects with confidence.

LWK's primary goal is to abstract away complexity by handling the most challenging aspects of Liquid development, such as:

  • Confidential Transactions handling, which automatically obscures amounts and asset types to maintain user privacy.
  • Asset issuance and management, providing a seamless way to create and interact with new digital assets.
  • Signing Liquid transactions, allowing for interaction with software signers and hardware wallets.

By providing these building blocks, LWK liberates developers from building Liquid functionality from scratch. This allows them to significantly accelerate development time and focus on creating unique, value-added features for their specific use cases, whether it's building a mobile wallet, integrating Liquid in an exchange, or developing a DeFi application. Ultimately, LWK is the definitive, go-to library for anyone committed to innovating on the Liquid Network.

Example: single-sig mobile wallet

This example application showcases how the Liquid Wallet Kit (LWK) simplifies the development of a single-signature mobile wallet. The two diagrams below illustrate the key user flows: Wallet Creation and Transaction Management. LWK handles the complex, low-level interactions with the Liquid blockchain and cryptographic operations, allowing the application to focus on the user interface and experience.

Wallet Creation

The mobile app starts by creating a new software signer and helps the user back up the corresponding BIP39 mnemonic. From this signer, the app extracts the xpub to derive a single-signature CT descriptor (e.g., ct(slip77(...),elwpkh([...]xpub/<0;1>/*))).

This CT descriptor is then used to initialize a wollet, which is LWK's watch-only wallet. The wollet allows the app to fetch addresses, transactions, and the current balance to display in the user interface.

When the app is opened, it uses a client to sync the wollet with the latest blockchain information. This ensures the wallet data is up-to-date.

flowchart TD
    Signer(Signer 🔑)
    Wollet("Wollet 👀<br>(descriptor)")
    Client(Client 🌐)
    App((📱))
    Signer -->|Xpub| Wollet 
    Client -->|Blockchain Update| Wollet
    Wollet -->|Addresses, Txs, Balance| App

Transaction Management

The mobile app enables users to send funds by allowing them to specify the amount, asset, and destination address. The wollet then takes this information to create an unsigned transaction, which is encoded in the PSET format.

The PSET is passed to the signer, which uses its private keys to sign the transaction. Once the PSET is signed, it's finalized into a complete transaction, which the client then broadcasts to the Liquid Network.

flowchart TD
    Signer(Signer 🔑)
    Wollet("Wollet 👀<br>(descriptor)")
    Client(Client 🌐)
    App((📱))
    App -->|Create TX| Wollet
    Wollet -->|Unsigned PSET| Signer 
    Signer -->|Signed PSET| Wollet 
    Wollet -->|Broadcast TX| Client

Remarks

This simple example highlights the core responsibilities of each LWK component:

  • Signer 🔑: Manages private keys and handles all signing operations.
  • Wollet 👀: Provides a watch-only view of the wallet, deriving addresses and tracking transactions and balances without holding any private keys.
  • Client 🌐: Fetch blockchain data from the Liquid Network to update the wollet.

Key Features

LWK allows to build more complex applications and prodcuts by leveraging its wide range of features:

  • Send and receive LBTC
  • Send and receive Liquid Issued Assets (e.g. USDT)
  • Send and receive AMP assets (e.g. BMN)
  • Software signers
  • Hardware wallets support (Jade)
  • Watch-Only view with CT descriptors
  • Single-sig
  • Generic Multisig
  • Multi-language support (Swift, Kotlin, Javascript, Typescript, Wasm, React Native, Go, C#, Rust, Flutter/Dart, Python)

For a more complete and detailed list of LWK features see here.

Get started

Install LWK and go through our tutorial.

Features

  • Watch-Only wallet support: using Liquid descriptors, better known as CT descriptors.
  • PSET based: transactions are shared and processed using the Partially Signed Elements Transaction format.
  • Electrum, Esplora and Waterfalls: no need to run and sync a full Liquid node or rely on closed source servers.
  • Asset issuance, reissuance and burn support: manage the lifecycle of your Issued Assets with a lightweight client.
  • Generic multisig wallets: create a wallet controlled by any combination of hardware or software signers, with a user specified quorum.
  • Hardware signer support: receive, issue, reissue and burn L-BTC and Issued Assets with your hardware signer, using singlesig or multisig wallets (currently Jade only, with more coming soon).
  • Multi Language support: Swift, Kotlin, Javascript, Typescript, WASM, React Native, Go, C#, Rust, Flutter/Dart and Python.
  • Liquid Atomic Swaps: using LiquiDEX.
  • Blockstream AMP support: send and receive asset issued with the Blockstream Asset Management Platform.
  • ...and more!

Installing LWK

LWK is available for several languages.

Rust

You can use the crates released on crates.io

[dependencies]
lwk_wollet = "0.11.0"
lwk_signer = "0.11.0"
lwk_common = "0.11.0"

Python

You can use the official python package: lwk

pip install lwk

Javascript/Typescript (Wasm)

Wasm module

Install LWK

npm install lwk_wasm

Import LWK

const lwk = require('lwk_wasm');

Node module

Install LWK

npm install lwk_node

Import LWK

const lwk = require('lwk_node');

iOS/Swift

Android/Kotlin

React Native

Go

C#

dotnet add package LiquidWalletKit --version 0.8.2

Please open an issue if you need a more recent version

Flutter/Dart

LWK Basics

LWK is a versatile library designed for a wide range of Liquid applications, from server integrations and mobile wallets to secure, standalone offline signers. Its flexibility allows developers to find the perfect balance between security, performance, and user experience for their specific needs.

How LWK Works: A Step-by-Step Walkthrough

This guide will walk you through the core components of LWK and show how they interact to manage and sign Liquid transactions. We'll cover the following steps in detail:

  • Create a Signer: First you will see how LWK manages private keys.
  • Create a Wallet: Next, you'll create a wallet to track your funds and handle your addresses.
  • Update the Wallet: You'll learn how to sync your wallet with blockchain data to get an accurate view of your balances.
  • Create a Transaction: This step covers how to build a new transaction, specifying inputs and outputs.
  • Sign a Transaction: Here, we'll demonstrate how to use the private keys from your signer to sign a transaction.
  • Broadcast a Transaction: Finally, you'll learn how to send your signed transaction to the Liquid network.

Next: LWK signers

Signers

In LWK, the management of private keys is delegated to a specialized component called Signer.

The primary tasks of a signer are:

  • provide xpubs, which are used to create wallets
  • sign transactions

Types of Signers

LWK has two signer types:

  • Software Signers: store the private keys in memory. This is the simplest signer to integrate and interact with.
  • Hardware Signers: LWK provides specific integrations for hardware wallets, such as the Blockstream Jade. These signers keep the private keys completely isolated from the computer.

While hardware signers are inherently more secure, LWK's design allows you to enhance the security of software signers as well. For example, a software signer can be run on an isolated machine or a mobile app might store the mnemonic (seed) encrypted, only decrypting it when a signature is required.

This guide will focus on software signers. For more details on hardware signers, please see the Jade documentation.

Create Signer

To create a signer you need a mnemonic. You can generate a new one with bip39::Mnemonic::generate(). Then you can create a software signer with SwSigner::new().

Rust
use lwk_signer::{bip39::Mnemonic, SwSigner};

let mnemonic = Mnemonic::generate(12)?;
let is_mainnet = false;

let signer = SwSigner::new(&mnemonic.to_string(), is_mainnet)?;
Python
from lwk import *

mnemonic = Mnemonic.from_random(12)
network = Network.testnet()

signer = Signer(mnemonic, network)
Javascript
const mnemonic = lwk.Mnemonic.fromRandom(12);
const network = lwk.Network.testnet();
const signer = new lwk.Signer(mnemonic, network);

Get Xpub

Once you have a signer you need to get some an extended public key (xpub), which can be used to create a wallet that requires signature from the signer.

The xpub is obtained with Signer::keyorigin_xpub(), which also includes the keyorigin information: signer fingerprint and derivation path from master key to the returned xpub, e.g. [ffffffff/84h/1h/0h]xpub....

Rust
let bip = lwk_common::Bip::Bip84;
let xpub = signer.keyorigin_xpub(bip, is_mainnet);
Python
Javascript
const xpub = signer.keyoriginXpub(lwk.Bip.bip84());

For particularly simple cases, such as single sig, you can get the CT descriptor directly from the signer, for instance using Signer::wpkh_slip77_descriptor().


Next: Watch-Only Wallets

Watch-Only Wallets

In LWK, the core functions of a wallet are split between two components for enhanced security: Signers manage private keys, while the Wollet handles everything else.

The term "Wollet" is not a typo; it stands for "Watch-Only wallet." A wollet provides view-only access, allowing you to generate addresses and see your balance without ever handling private keys. This design is crucial for security, as it keeps your private keys isolated.

A LWK wollet can perform the following operations:

  • Generate addresses
  • List transactions
  • Get balance
  • Create transactions (but not sign them)

CT descriptors

A Wollet is defined by a CT descriptor, which consists in a Bitcoin descriptor plus the descriptor blinding key.

In the previous section, we saw how to generate a single sig CT descriptor from a signer with Signer::wpkh_slip77_descriptor(), which returns something like:

ct(slip77(...),elwpkh([ffffffff/84h/1h/0h]xpub...))
  • ct(...,...)
  • slip77(...) the descriptor blinding key
  • el the "Elements" prefix
  • wpkh([ffffffff/84h/1h/0h]xpub...) the "Bitcoin descriptor", with

The CT descriptors defines the wallet spending conditions. In this case it requires a single signature from a specific signer.

LWK supports more complex spending conditions, such as multisig.

Create a Wollet

From the CT descriptor, you need to generate a WolletDescriptor. Calling WolletDescriptor::from_str() will perform some basic validation of the descriptor, and fails if the descriptor is not supported by LWK.

Once you have a WolletDescriptor you can create a Wollet using either Wollet::without_persist() (keeps wallet data in memory) or Wollet::with_fs_persist() (stores wallet data on filesystem).

LWK also allows Wollets to have a custom persister.

Rust
use lwk_wollet::{ElementsNetwork, Wollet, WolletDescriptor};

let desc = signer.wpkh_slip77_descriptor()?;
let wd = WolletDescriptor::from_str(&desc)?;
let network = ElementsNetwork::LiquidTestnet;
let mut wollet = Wollet::without_persist(network, wd)?;
Python
Javascript
const desc = signer.wpkhSlip77Descriptor();
const wollet = new lwk.Wollet(network, desc);

Generate Addresses

You can generate a wallet confidential address with Wollet::address().

This address can receive any Liquid asset or amount.

Rust
let addr = wollet.address(None)?;
Python
Javascript
const addr = wollet.address(null).address().toString();

Get Transactions and Balance

It's possibile to get the list of wallet transactions with Wollet::transactions() and the balance Wollet::balance().

Note: Liquid transactions are confidential, meaining that only sender and receiver can see their asset and amount. Wollet unblinds the transactions and returns unblinded data that can be shown to the user.

Wollet however does not have internet access. To fetch (new) wallet data, you need to use a "client" that fetches wallet transactions from some server. In the next section we explain how (new) blockchain data can be obtained and added to the wallet.

Rust
let txs = wollet.transactions()?;
let balance = wollet.balance()?;
Python
Javascript
const txs = wollet.transactions();
const balance = wollet.balance();

Next: Update the Wallet

Update the Wallet

The fact that Wollet does have access to internet is a deliberate choice. This allows Wollet to work offline, where they can generate addresses.

The connection is handled by a specific component, a Blockchain Client. Blockchain clients connect to the specified server a fetch the wallet data from the blockchain.

LWK currently support 3 types of servers:

  • Electrum Servers
  • Esplora Servers
  • Waterfalls Servers

To delve into their differences and strength points see our dedicated section.

Create a Client

In this guide we will use an EsploraClient.

You can create a new client with EsploraClient::new(), specifying the URL of the service.

Scan the Blockchain

Given a Wollet you can call EsploraClient::full_scan(), which performs a series of network calls that scan the blockchain to find transactions relevant for the wallet.

EsploraClient::full_scan() has a stopping mechanisms that relies on BIP44 GAP LIMIT. This might not fit every use cases. In case you have large sequences of consecutive unused addresses you can use EsploraClient::full_scan_to_index().

Apply the Update

EsploraClient::full_scan() fetches, parses, (locally) unblind and serialized the fetched data in returned value, an Update. The Update can be applied to the Wollet using Wollet::apply_update().

After applying the update the wollet data will include the new transaction downloaded, for instance more transactions can be returned and balance can increase (or decrease).

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

// let url = "https://blockstream.info/liquidtestnet/api";
// let url = "https://blockstream.info/liquid/api";

let mut client = EsploraClient::new(&url, network)?;

if let Some(update) = client.full_scan(&wollet)? {
    wollet.apply_update(update)?;
}
Python
Javascript
const url = "https://waterfalls.liquidwebwallet.org/liquidtestnet/api";
const client = new lwk.EsploraClient(network, url, true, 4, false);

const update = await client.fullScan(wollet);
if (update) {
    wollet.applyUpdate(update);
}

Next: Create a transaction

Transaction Creation

With a Wollet you can generate an address, which can be used to receive some funds. You can fetch the transactions receiving the funds using a "client", and apply them to the wallet. Now that the Wollet has a balance, it is able to craft transactions sending funds to desired destination.

The first step is construction a TxBuilder (or WolletTxBuilder), using TxBuilder::new() or Wollet::tx_builder(). You can now specify how to build the transaction using the methods exposed by the TxBuilder.

Add a Recipient

If you want to send some funds you need this information:

  • Address: destination (confidential) address provided by the receiver
  • Amount: number of units of the asset (satoshi) to be sent.
  • Asset: identifier of the asset that should be sent

Then you can call TxBuilder::add_recipient() to create an output which sends the amount of the asset, to the specified address.

You can add multiple recipients to the same transaction.

Advanced Options

LWK allows to construct complex transactions, here there are few examples

Construct the Transaction (PSET)

Once you set all the desired options to the TxBuilder. You can construct the transaction calling TxBuilder::finish(). This will return a Partially Signed Elements Transaction (PSET), a transaction encoded in a format that facilitates sharing the transaction with signers.

Rust
let mut pset = wollet
    .tx_builder()
    .add_recipient(&address, sats, lbtc)?
    .finish()?;
Python
Javascript
const sats = BigInt(1000);
const address = new lwk.Address("<address>");
const asset = new lwk.AssetId("<asset>");

var builder = new lwk.TxBuilder(network)
builder = builder.addRecipient(address, sats, asset)
var pset = builder.finish(wollet)

Next: Sign Transaction

Transaction Signing

Once you have created a PSET now you need to add some signatures to it. This is done by the Signer, however the signer might be isolated, so we need some mechanisms to allow the signer to understand what is signing.

Get the PSET details

This is done with Wollet::get_details(), which returns:

  • missing signatures and the respective signers' fingerprints
  • net balance, the effect that transaction has on wallet (e.g. how much funds are sent out of the wallet)

If the Signer fingeprint is included in the missing signatures, then a Signer with that fingeprint expected to sign.

The balance can be shown to the user or validated against the Signer expectations.

It's worth noticing that Wollets can work without internet, so offline Signers can have Wollets instance to enhance the validation performed before signing.

Rust
let details = wollet.get_details(&pset)?;
Python
Javascript
const details = wollet.psetDetails(pset);

Sign the PSET

Once you have performed enough validation, you can call Signer::sign. Which adds signatures from Signer to the PSET.

Once the PSET has enough signatures, you can broadcast to the Liquid Network.

Rust
let sigs_added = signer.sign(&mut pset)?;
assert_eq!(sigs_added, 1);
Python
Javascript
pset = signer.sign(pset)

Next: Broadcast a Transaction

Transaction Broadcast

When a PSET has enough signatures, it's ready to be broadcasted to the Liquid Network.

Finalize the PSET

First you need to call Wollet::finalize() to finalize the PSET and extract the signed transaction.

Broadcast the Transaction

The transaction returned by the previous step can be sent to the Liquid Network with EsploraClient::broadcast().

Apply the Transaction

If you send a transaction you might want to see the balance decrease immediately. With LWK this does not happens automatically, you can do a "full scan" and apply the returned update. However this requires network calls and it might be slow, if you want your balance to be updated immediately, you can call Wollet::apply_tx().

Rust
let tx = wollet.finalize(&mut pset)?;
let txid = client.broadcast(&tx)?;

// (optional)
wollet.apply_transaction(tx)?;
Python
Javascript
pset = wollet.finalize(pset)
const tx = pset.extractTx();
const txid = await client.broadcastTx(tx)

// (optional)
wollet.applyTransaction(tx);

Next: Advanced Features

AMP0 in LWK

AMP0 (Asset Management Platform version 0) is a service for issuers that allows to enforce specific rules on certain Liquid assets (AMP0 assets).

AMP0 is based on a legacy system and it does not fit the LWK model perfectly. That is reflected in the LWK AMP0 interface which could be a bit cumbersome to use.

Limitations

LWK has partial support for AMP0. For instance it does not allow to issue AMP0 asset, or use accounts with 2FA.

LWKGDKAMP0 API
Create AMP0 accounts
Receive on AMP0 accounts
Monitor AMP0 accounts
Send from AMP0 accounts
Account with 2FA
issue, reissue, burn AMP0 assets
set restriction for AMP0 assets

If you need full support for AMP0, use GDK and the AMP0 issuer API.

Overview

To use AMP0 with LWK you need:

  • 👀 some Green Watch-Only credentials (username and password) for a Green Wallet with an AMP account
  • 🔑 the corresponding signer available (e.g. Jade or software with the BIP39 mnemonic)

Then you can:

  • get addresses for the AMP0 account (👀)
  • monitor the AMP0 account (get balance and transactions) (👀)
  • create AMP0 transactions (👀)
  • sign AMP0 transactions (🔑)
  • ask AMP0 to cosign transactions (👀)
  • broadcast AMP0 transactions (👀)

Using AMP0 with LWK you can keep the signer separated and operate it accoriding to the desired degree of security and isolation.

⚠️ AMP0 is based on a legacy system and it has some pitfalls. We put some mechanism in order to make it harder to do the wrong thing, anyway callers should be careful when getting new addresses and syncing the wallet.

Setup

To use AMP0 with LWK you need to:

  1. Create a Liquid wallet (backup its mnemonic/seed)
  2. Create an AMP account (AMP ID)
  3. Create a Liquid Watch-Only (username and password)

1. Create Liquid wallet

Create a Signer and backup it's mnemonic/seed. From the signer get its signer_data using Signer::amp0_signer_data().

Create a Amp0Connected::new() passing the signer_data. You now need to authenticate with AMP0 server. First get the server challenge with Amp0Connected::get_challenge(). Sign the challenge with Signer::amp0_sign_challenge(). You can now call Amp0Connected::login() passing the signature. This function returns a Amp0LoggedIn instance, which can be used to create new AMP0 accounts and watch-only entries.

2. Create an AMP account

Obtain the number of the next account using Amp0LoggedIn::next_account(). Use the signer to get the corresponding xpub Signer::amp0_accont_xpub(). Now you can create a new AMP0 account with Amp0LoggedIn::create_amp0_account(), which returns the AMP ID.

3. Create a Liquid Watch-Only

Choose your your AMP0 Watch-Only credentials username and password and call Amp0LoggedIn::create_watch_only().

Now that you have mnemonic/seed (or Jade), AMP ID and Watch-Only credentials (username and password), you're ready to use AMP0 with LWK.

If you're using lwk_node, polyfill the websocket

const WebSocket = require('ws');
global.WebSocket = WebSocket;
const lwk = require('lwk_node');
Rust
use lwk_common::{Amp0Signer, Network};
use lwk_signer::SwSigner;
use lwk_wollet::amp0::blocking::{Amp0, Amp0Connected};

// Create signer and watch only credentials
let network = Network::TestnetLiquid;
let is_mainnet = false;
let (signer, mnemonic) = SwSigner::random(is_mainnet)?;
let username = "<username>";
let password = "<password>";

// Collect signer data
let signer_data = signer.amp0_signer_data()?;

// Connect to AMP0
let amp0 = Amp0Connected::new(network, signer_data)?;

// Obtain and sign the authentication challenge
let challenge = amp0.get_challenge()?;
let sig = signer.amp0_sign_challenge(&challenge)?;

// Login
let mut amp0 = amp0.login(&sig)?;

// Create a new AMP0 account
let pointer = amp0.next_account()?;
let account_xpub = signer.amp0_account_xpub(pointer)?;
let amp_id = amp0.create_amp0_account(pointer, &account_xpub)?;

// Create watch only entries
amp0.create_watch_only(&username, &password)?;

// Use watch only credentials to interact with AMP0
let amp0 = Amp0::new(network, &username, &password, &amp_id)?;
Python
Javascript
const mnemonic = "<mnemonic>";
const m = new lwk.Mnemonic(mnemonic);
const network = lwk.Network.testnet();
const signer = new lwk.Signer(m, network);
const username = "<username>";
const password = "<password>";

// Collect signer data
const signer_data = signer.amp0SignerData();
// Connect to AMP0
const amp0connected = await new lwk.Amp0Connected(network, signer_data);
// Obtain and sign the authentication challenge
const challenge = await amp0connected.getChallenge();
const sig = signer.amp0SignChallenge(challenge);
// Login
const amp0loggedin = await amp0connected.login(sig);
// Create a new AMP0 account
const pointer = amp0loggedin.nextAccount();
const account_xpub = signer.amp0AccountXpub(pointer);
const amp_id = await amp0loggedin.createAmp0Account(pointer, account_xpub);
// Create watch only entries
await amp0loggedin.createWatchOnly(username, password);
// Use watch only credentials to interact with AMP0
const amp0 = await new lwk.Amp0(network, username, password, amp_id);

Alternative setup

It's possible to setup an AMP0 account using GDK based apps:

  1. Blockstream App (easiest, GUI, mobile, desktop, Jade support), or
  2. green_cli (CLI, Jade support), or
  3. GDK directly (fastest, example)

AMP0 daily operations

LWK allows to manage created AMP0 accounts. You can receive funds, monitor transactions and send to other wallets.

Receive

To receive funds you need an address, you can get addresses with Amp0::address().

⚠️ For AMP0 wallets, do not use Wollet::address() or WolletDescriptor::address(), using them can lead to loss of funds. AMP0 server only monitors addresses that have been returned by the server. If you send funds to an address that was not returned by the server, the AMP0 server will not cosign transactions spending that inputs. Which means that those funds are lost (!), since AMP0 accounts are 2of2.

Monitor

LWK allows to monitor Liquid wallets, including AMP0 accounts.

First you get the AMP0 descriptor with Amp0::wollet_descriptor(). You then create a wallet with Wollet::new().

Once you have the AMP0 Wollet, you can get Wollet::transactions(), Wollet::balance() and other information.

LWK wallets needs to be updated with new data from the Liquid blockchain. First create a blockchain client, for insance EsploraClient::new(). Then get an update with BlockchainBackend::full_scan_to_index() passing the value returned by Amp0::last_index(). Finally update the wallet with Wollet::apply_update().

⚠️ For AMP0 wallets, do not sync the wallet with BlockchainBackend::full_scan(), otherwise some funds might not show up. AMP0 accounts do not have the concept of GAP_LIMIT and they can have several unused address in a row. The default scanning mechanism when it sees enough unused addresses in a row it stops. So it can happen that some transactions are not returned, and the wallet balance could be incorrect.

Send

For AMP0 you can follow the standard LWK transaction flow, with few small differences.

Use the TxBuilder, add recipients TxBuilder::add_recipient(), and use the other available methods if needed.

Then instead of using TxBuilder::finish(), use TxBuilder::finish_for_amp0(). This creates an Amp0Pset which contains the PSET and the blinding_nonces, some extra data needed by the AMP0 cosigner.

Now you need to interact with secret key material (🔑) corresponding to this AMP0 account. Create a signer, using SWSigner or Jade and sign the PSET with the signer, using Signer::sign().

Once the PSET is signed, you need to have it cosigned by AMP0. Construct an Amp0Pset using the signed PSET and the blinding_nonces obtained before. Call Amp0::sign() passing the signed Amp0Pset.

If all the AMP0 rules are respected, the transaction is cosigned by AMP0 and can be broadcast, e.g. with EsploraClient::broadcast().

Rust
use lwk_common::{Network, Signer};
use lwk_signer::SwSigner;
use lwk_wollet::amp0::{blocking::Amp0, Amp0Pset};
use lwk_wollet::{clients::blocking::EsploraClient, ElementsNetwork, Wollet};

// Signer
let mnemonic = "<mnemonic>";
// AMP0 Watch-Only credentials
let username = "<username>";
let password = "<password>";
// AMP ID (optional)
let amp_id = "";

// Create AMP0 context
let network = Network::TestnetLiquid;

let mut amp0 = Amp0::new(network, username, password, amp_id)?;

// Create AMP0 Wollet
let wd = amp0.wollet_descriptor();
let mut wollet = Wollet::without_persist(ElementsNetwork::LiquidTestnet, wd)?;

// Get a new address
let addr = amp0.address(None);

// Update the wallet with (new) blockchain data
let url = "https://blockstream.info/liquidtestnet/api";
let mut client = EsploraClient::new(url, ElementsNetwork::LiquidTestnet)?;
if let Some(update) = client.full_scan_to_index(&wollet, amp0.last_index())? {
    wollet.apply_update(update)?;
}

// Get balance
let balance = wollet.balance()?;

// Construct a PSET sending LBTC back to the wallet
let amp0pset = wollet
    .tx_builder()
    .drain_lbtc_wallet()
    .finish_for_amp0()?;
let mut pset = amp0pset.pset().clone();
let blinding_nonces = amp0pset.blinding_nonces();

// User signs the PSET
let is_mainnet = false;
let signer = SwSigner::new(mnemonic, is_mainnet)?;
let sigs = signer.sign(&mut pset)?;
assert!(sigs > 0);

// Reconstruct the Amp0 PSET with the PSET signed by the user
let amp0pset = Amp0Pset::new(pset, blinding_nonces.to_vec())?;

// AMP0 signs
let tx = amp0.sign(&amp0pset)?;

// Broadcast the transaction
let txid = client.broadcast(&tx)?;
Python
Javascript
const mnemonic = "<mnemonic>";
const m = new lwk.Mnemonic(mnemonic);
const network = lwk.Network.testnet();
const signer = new lwk.Signer(m, network);
const username = "<username>";
const password = "<password>";
const amp_id = "";

// Create AMP0 object
const amp0 = await lwk.Amp0.newTestnet(username, password, amp_id);

// Get an address
const addrResult = await amp0.address(1);

// Create wollet
const wollet = amp0.wollet();

// Sync the wallet
const url = "https://waterfalls.liquidwebwallet.org/liquidtestnet/api";
const client = new lwk.EsploraClient(network, url, true, 4, false);

const last_index = amp0.lastIndex();
const update = await client.fullScanToIndex(wollet, last_index);
if (update) {
    wollet.applyUpdate(update);
}

// Get the wallet transactions
const txs = wollet.transactions();

// Get the balance
const balance = wollet.balance();

// Create a (redeposit) transaction
var b = network.txBuilder();
b = b.drainLbtcWallet();
const amp0pset = b.finishForAmp0(wollet);

// Sign with the user key
const pset = amp0pset.pset();
const signed_pset = signer.sign(pset);

// Ask AMP0 to cosign
const amp0pset_signed = new lwk.Amp0Pset(signed_pset, amp0pset.blindingNonces());
const tx = await amp0.sign(amp0pset_signed);

// Broadcast
const txid = await client.broadcastTx(tx);

Examples

We provide a few examples on how to integrate use AMP0 with LWK:

LWK Structure

LWK functionalities are split into different component crates that might be useful independently.

For instance, mobile app devs might be interested mainly in lwk_bindings, lwk_wollet and lwk_signer. While backend developers might want to directly use lwk_cli in their systems.

Internal crate dependencies are shown in this diagram: an arrow indicates "depends on" (when dotted the dependency is feature-activated, when blue is a dev-dependency):

Dep tree

(generated with cargo depgraph --workspace-only --dev-deps)

Users of LWK

This section showcases a series of projects that are built on LWK or its components.

NameDescriptionTypeLanguage
Liquidtestnet.comTestnet FaucetServerPython
Liquidwebwallet.orgBrowser walletWalletWasm
AMP2Registered AssetsServerClosed source
Breeze SDK LiquidLightning swapsSDKRust
BoltzAtomic swapsServerGo
AquaAtomic swapsWalletDart
Bull BitcoinAtomic swapsWalletDart
Blitz WalletAtomic swapsWalletJavascript
PeerswapLN BalancingServerGo
AreaLayer FireBolt walletWalletWalletTypeScript
Banco LibreBrowser walletWalletWasm
Misty BreezeLightning walletWalletDart
Onion Mill - StashPayLightning walletWallet
PSET GUIPSET AnalyzerToolJavaScript
SatsailsWalletWalletDart
ShopstrNostr MarketplaceServerTypeScript

Feel free to open a PR to add or remove your product.

Liquidtestnet.com

Liquidtestnet.com provides a several utilities to interact with Liquid testnet. It also has a testnet faucet which distributes testnet assets (LBTC, standard assets and AMP0, AMP2 assets).

The faucet is built using LWK python wheels.

Liquidtestnet.com is open source, source available in the Github repository.

Liquidwebwallet.org

Liquidwebwallet.org is a companion app for Liquid running in the browser. The website allows the use of read-only wallets or hardware wallets such as Jade or Ledger to access one's wallet, view the balance, create transactions, create issuance, reissuance, and burn transactions.

The wallet is built using LWK_wasm.

Liquidwebwallet.org is open source, source available in the Github repository

AMP2

AMP2 is a platform able to issue and manage digital assets on the Liquid Network with flexible API. The platform allows for the management of the entire token lifecycle, enabling the control and authorization of each individual operation.

AMP2 uses LWK internally.

AMP2 is closed source.

Breeze SDK Liquid

The Breeze SDK Liquid provides developers with a end-to-end solution for integrating self-custodial Lightning payments into their apps and services.

The SDK use LWK as internal liquid wallet and signer.

Breeze SDK Liquid is open source, source available in the Github repository

Boltz

Boltz the Non-Custodial Bitcoin Bridge able to swap between different Bitcoin layers.

The SDK use LWK as internal liquid wallet and signer.

Boltz is open source, source available in the Github repository

Aqua

Aqua AQUA is a free, open-source wallet for iOS and Android.

The app use LWK in the backend.

Aqua is open source, source available in the Github repository

Bull Bitcoin

Bull Bitcoin is a mobile wallet with Bitcoin, Liquid and lightning support.

Bull Bitcoin uses LWK for its Liquid wallet.

Bull Bitcoin is open source, source available in the Github repository

Blitz Wallet

Blitz Wallet is a react native wallet.

The app is based on Breeze SDK Liquid.

Blitz Wallet is open source, source available in the Github repository

Peerswap

Peerswap Atomic swaps for rebalancing Lightning channels.

The app use LWK as internal liquid wallet and signer.

Peerswap is open source, source available in the Github repository

AreaLayer FireBolt wallet

|AreaLayer FireBolt wallet is a react native wallet.

The app is based on Breeze SDK Liquid.

AreaLayer FireBolt wallet is open source, source available in the Github repository

Banco Libre

Banco Libre is a web wallet using LWK_wasm.

The app use LWK_wasm as internal liquid wallet and signer.

Banco Libre is open source, source available in the Github repository

Misty Breeze

Misty Breeze is a flutter app based on Breez SDK Liquid.

The app is based on Breeze SDK Liquid.

Misty Breeze is open source, source available in the Github repository

Onion Mill - StashPay

Onion Mill - StashPay is a minimalist Bitcoin wallet based on Breeze SDK Liquid.

The app is based on Breeze SDK Liquid.

Onion Mill - StashPay is open source, source available in the Github repository

PSET GUI

PSET GUI is a user-friendly application designed for analyzing and signing PSETs.

The app use LWK_wasm as internal liquid wallet and signer.

PSET GUI is open source, source available in the Github repository

Satsails

Satsails is a self-custodial Bitcoin and Liquid wallet with support for stablecoins.

The app use LWK_wasm as internal liquid wallet and signer.

Satsails is open source, source available in the Github repository

Shopstr

Shopstr is a global, permissionless Nostr marketplace for Bitcoin commerce.

WIP for Liquid integration

Shopstr is open source, source available in the Github repository

Unknown users

There may be many other users of the libraries who are currently unknown or not publicly disclosed

The libraries' license allows their integration into both open-source and closed-source solutions.These users can leverage lwk to:

  • Develop wallets and online services for asset management,
  • Create swap services,
  • Issue, reissue, and burn assets.

LWK History

BEWallet was originally an Elements/Liquid wallet library written in Rust to develop prototypes and experiments.

BEWallet was based on Blockstream's GDK. Essentially some GDK Rust pieces were moved to this project.

This was used as the starting point for the Liquid Wallet Kit project. Parts that were not necessary have been dropped, many things have been polished, and new features have been added.

The codebase has been entirely re-written, and now it has almost no similarity with the original code.