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
xpub
s, 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()
.
use lwk_signer::{bip39::Mnemonic, SwSigner};
let mnemonic = Mnemonic::generate(12)?;
let is_mainnet = false;
let signer = SwSigner::new(&mnemonic.to_string(), is_mainnet)?;
from lwk import *
mnemonic = Mnemonic.from_random(12)
network = Network.testnet()
signer = Signer(mnemonic, network)
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...
.
let bip = lwk_common::Bip::Bip84;
let xpub = signer.keyorigin_xpub(bip, is_mainnet);
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 keyel
the "Elements" prefixwpkh([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 Wollet
s to have a custom persister.
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)?;
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.
let addr = wollet.address(None)?;
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.
let txs = wollet.transactions()?;
let balance = wollet.balance()?;
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).
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)?;
}
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
- Set fee rate with
TxBuilder::fee_rate()
- Manual coin selection
- External UTXOS
- Explicit inputs and outputs
- Send all LBTC
- Issuance, reissuance, burn
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.
let mut pset = wollet
.tx_builder()
.add_recipient(&address, sats, lbtc)?
.finish()?;
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 Wollet
s can work without internet,
so offline Signer
s can have Wollet
s instance to enhance the validation performed before signing.
let details = wollet.get_details(&pset)?;
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.
let sigs_added = signer.sign(&mut pset)?;
assert_eq!(sigs_added, 1);
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()
.
let tx = wollet.finalize(&mut pset)?;
let txid = client.broadcast(&tx)?;
// (optional)
wollet.apply_transaction(tx)?;
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.
LWK | GDK | AMP0 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.
Setup
To use AMP0 with LWK you need to:
- Create a Liquid wallet (backup its mnemonic/seed)
- Create an AMP account (AMP ID)
- 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 websocketconst WebSocket = require('ws'); global.WebSocket = WebSocket; const lwk = require('lwk_node');
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, &_id)?;
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:
- Blockstream App (easiest, GUI, mobile, desktop, Jade support), or
green_cli
(CLI, Jade support), or- 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()
.
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()
.
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()
.
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(&0pset)?;
// Broadcast the transaction
let txid = client.broadcast(&tx)?;
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:
- amp0.py shows how to receive, monitor and send with an AMP0 account
- liquidwebwallet.org integrates AMP0 using WASM
- Rust tests in amp0.rs
LWK Structure
LWK functionalities are split into different component crates that might be useful independently.
lwk_cli
: a CLI tool to use LWK wallets.lwk_wollet
: library for watch-only wallets; specify a CT descriptor, generate new addresses, get balance, create PSETs and other actions.lwk_signer
: interact with Liquid signers to get your PSETs signed.lwk_jade
: unlock Jade, get xpubs, register multisig wallets, sign PSETs and more.lwk_bindings
: use LWK from other languages.lwk_wasm
: use LWK from WebAssembly.- and more:
common or ancillary components (
lwk_common
,lwk_rpc_model
,lwk_tiny_rpc
,lwk_app
), future improvements (lwk_hwi
), testing infrastructure (lwk_test_util
,lwk_containers
)
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):
(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.
Name | Description | Type | Language |
---|---|---|---|
Liquidtestnet.com | Testnet Faucet | Server | Python |
Liquidwebwallet.org | Browser wallet | Wallet | Wasm |
AMP2 | Registered Assets | Server | Closed source |
Breeze SDK Liquid | Lightning swaps | SDK | Rust |
Boltz | Atomic swaps | Server | Go |
Aqua | Atomic swaps | Wallet | Dart |
Bull Bitcoin | Atomic swaps | Wallet | Dart |
Blitz Wallet | Atomic swaps | Wallet | Javascript |
Peerswap | LN Balancing | Server | Go |
AreaLayer FireBolt wallet | Wallet | Wallet | TypeScript |
Banco Libre | Browser wallet | Wallet | Wasm |
Misty Breeze | Lightning wallet | Wallet | Dart |
Onion Mill - StashPay | Lightning wallet | Wallet | |
PSET GUI | PSET Analyzer | Tool | JavaScript |
Satsails | Wallet | Wallet | Dart |
Shopstr | Nostr Marketplace | Server | TypeScript |
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.