Blockstream Enterprise Custody SDK
Issuer Guide (SDK)

Wallet registration

Wallet registration submits a Liquid descriptor to AMP2 and returns a wallet ID. The page explains xpub retrieval, descriptor construction, funding, and edge cases.

Before you can issue or send assets, you need at least one registered wallet. Registration submits a Liquid descriptor to AMP2 and returns a wallet ID (WID) used in all subsequent operations.

Why you need the AMP xpub

AMP2 wallets are 2-of-2 multisig descriptors combining your key and AMP2's server key. To construct the descriptor, you need AMP2's keyorigin xpub:

import { AmpActions } from '@blockstream/amp-registry';

const xpubResponse = await broadcastRequest<
  typeof AmpActions.xpub
>(
  {
    action: 'get',
    resource: '/amp/xpub',
    details: {},
  },
  broadcaster,
);

const ampKeyoriginXpub = xpubResponse.details.keyorigin_xpub;

The returned xpub looks like:

[c67f5991/87'/1'/0']tpubDC4SUtWGWcMQPtwjgQQ4DYnFmAYhiKxw3f3KKCvMGT9sojZNvHsQ4rVW6nQeCPtk4rLAxGKeuAzMmBmH92X3HDgLho3nRWpvuJrpCmYgeQj

Create a signer and get your xpub

Use LWK to create a signer and extract your keyorigin xpub:

use lwk_signer::SwSigner;
use lwk_common::Bip;

let signer = SwSigner::new("your mnemonic words here", false).unwrap();
let keyorigin_xpub = signer.keyorigin_xpub(&Bip::new_bip84()).unwrap();

Or import an existing xpub directly:

let keyorigin_xpub = "[b805d768/87h/1h/0h]tpubDCYEgnLyCH2okSittQNNB8JHLwPgmoEAoKcMrJDHP9dFVamsadPAFJQ77C1htgR8ksie3VksLXoryng9AUaPZSF8FwTwEv6CaHp8j2YCrds";

Build the descriptor

Combine your xpub with AMP2's to form the 2-of-2 multisig descriptor. Using the AMP2 Rust client:

let amp2 = Amp2::new(keyorigin_xpub.to_string(), amp_url.to_string()).unwrap();
let descriptor = amp2.descriptor_from_str(keyorigin_xpub).unwrap();
let descriptor_str = descriptor.descriptor().to_string();

The resulting descriptor has the form:

ct(elip151,elwsh(multi(2,
  [<your_fingerprint>/87'/1'/0']<your_xpub>/<0;1>/*,
  [<amp_fingerprint>/87'/1'/0']<amp_xpub>/<0;1>/*
)))

Register the wallet

Submit the descriptor to AMP2:

import { AmpWalletRequestSchema } from '@blockstream/amp-registry';

const registerResponse = await broadcastRequest(
  AmpWalletRequestSchema.parse({
    action: 'add',
    resource: '/amp/wallets',
    details: {
      descriptor: descriptor_str,
    },
  }),
  broadcaster,
);

if (registerResponse.status !== 'success') {
  throw new Error(
    `Wallet registration failed: ec=${registerResponse.error_code} message=${registerResponse.message}`
  );
}

const wid = registerResponse.details.wid;
console.log('Registered wallet:', wid);

The wid is a deterministic identifier derived from the descriptor. Store it. You will use it in every asset operation.

Verify registration

Check that the wallet is registered by querying its balance:

const balanceResponse = await broadcastRequest<
  typeof AmpActions.walletBalance
>(
  {
    action: 'get',
    resource: `/amp/wallets/${wid}/balance`,
    details: { with_names: true },
  },
  broadcaster,
);

console.log('Wallet balance:', balanceResponse.details);

A newly registered wallet has zero balances.

Fund the wallet with LBTC

Every AMP2 wallet needs LBTC to pay Liquid transaction fees. Use LWK to derive a receive address locally from the descriptor:

use lwk_wollet::Wollet;

let wollet = Wollet::new(network, &descriptor)?;
let address = wollet.address(None)?;
println!("Fund this address with LBTC: {}", address);

Send LBTC to this address from an external source (e.g., peg in from Bitcoin or transfer from another Liquid wallet).

Verify LBTC balance by scanning the chain with LWK:

use lwk_wollet::ElectrumClient;

let client = ElectrumClient::new(&ElectrumUrl::new("blockstream.info:995", true, true))?;
wollet.full_scan(&client)?;
let balance = wollet.balance()?;
println!("LBTC balance: {:?}", balance);

Multiple wallets

You can register multiple wallets for different purposes:

  • Treasury wallet: holds the main asset supply and reissuance tokens.
  • Distribution wallet: used for sending assets to investors.
  • Fee wallet: dedicated LBTC wallet for fee management.

Each wallet has its own WID and can be independently funded, locked, and monitored.

Registering investor wallets

As an issuer, you also register wallets for your investors. When an investor provides their wallet descriptor (through your onboarding channel), register it the same way:

const investorRegResponse = await broadcastRequest(
  AmpWalletRequestSchema.parse({
    action: 'add',
    resource: '/amp/wallets',
    details: {
      descriptor: investorDescriptorStr,
    },
  }),
  broadcaster,
);

const investorWid = investorRegResponse.details.wid;

The investor's WID is then used as a recipient in send operations and as a member of restriction groups.

Registration edge cases

  • Same descriptor, same blinding keys: returns the existing WID without error (idempotent).
  • Same descriptor, different blinding keys: returns 403 Forbidden.
  • Invalid descriptor format: returns a validation error.

Next steps

On this page