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