Blockstream Enterprise Custody SDK
Issuer Guide (SDK)

Asset issuance

Asset issuance creates a new Liquid asset from a funded wallet. The page explains issuance request setup, PSET handling, cosigning, broadcast, and verification.

Asset issuance creates a new Liquid asset from a funded wallet. It follows the standard PSET pattern: AMP2 builds an unsigned transaction, you sign locally, AMP2 cosigns via HSM, and AMP2 broadcasts to the Liquid Network.

Prerequisites

Step 1: Build the issuance request

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

const ampIssueReq = AmpIssueRequestSchema.parse({
  action: 'add',
  resource: '/amp/assets/issue',
  details: {
    wid: wid,
    satoshi_asset: 1_000_000,
    satoshi_token: 100,
    contract: {
      name: 'My Security Token',
      ticker: 'MST',
      domain: 'example.com',
      precision: 8,
      version: 0,
      issuer_pubkey: signer.getMasterXpub().compressed_public_key(),
    },
    utxos: [],
  },
});

Parameter reference

ParameterTypeDescription
widstringSource wallet that pays fees and receives the new asset
satoshi_assetnumberTotal supply in satoshi units
satoshi_tokennumberReissuance token amount (0 to disable future supply changes)
contract.namestringHuman readable asset name (immutable after issuance)
contract.tickerstringShort symbol, e.g. "MST"
contract.domainstringIssuer domain for Asset Registry verification
contract.precisionnumberDecimal mapping (0-8). Precision 8: 100M sat = 1 unit
contract.versionnumberContract format version (typically 0)
contract.issuer_pubkeystringCompressed public key of the issuer
utxosarraySpecific UTXOs to use as inputs (empty = auto select)

How precision works

  • precision: 8 and satoshi_asset: 100_000_000 -> 1.00000000 display units (like Bitcoin)
  • precision: 2 and satoshi_asset: 100_000 -> 1,000.00 display units (like USD cents)
  • precision: 0 and satoshi_asset: 1000 -> 1,000 display units (integer tokens)

Step 2: Request the PSET

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

const [issueResponse, error] = await broadcaster.processBroadcastSafe<
  typeof AmpActions.issue
>(ampIssueReq);

if (error) {
  if (error instanceof Error) {
    throw error
  }

  if (error instanceof ProposalCreatedError) {
    throw error
  }

  console.log(error.metadata)
  throw new Error(
    `Issuance PSET request failed: ${error.message}`,
  )
}

if (!issueResponse) {
  throw new Error(
    `unexpected`
  );
}

const psetStr = issueResponse.details.pset;

AMP2 selects UTXOs from the wallet, constructs the issuance transaction, and returns an unsigned PSET.

Step 3: Sign locally

Sign the PSET with your own key using LWK:

use elements::encode as el_encode;
use base64::engine::general_purpose;
use base64::Engine;

pub fn pset_from_b64(s: &str) -> eyre::Result<PartiallySignedTransaction> {
    let bytes = general_purpose::STANDARD.decode(s)?;
    Ok(el_encode::deserialize::<PartiallySignedTransaction>(&bytes)?)
}

let mut pset = pset_from_b64(&pset_str)?;
signer.sign(&mut pset)?;

This adds your half of the 2-of-2 multisig signature. Alternatively, you can use lwk_wasm:

import * as lwk from 'lwk_wasm'

// e.g. construct lwk.Signer using mnemonic
// let signer = new lwk.Signer(mnemonic, lwk.Network.testnet())
const signedPset = signer.sign(new lwk.Pset(pset))

Step 4: Request AMP2 cosigning

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

const cosignReq = AmpSignPsetRequestSchema.parse({
  action: 'add',
  resource: '/amp/wallets/sign',
  details: {
    pset: signedPset.toString(),
  },
});

const cosignResponse = await broadcaster.processBroadcast<
  typeof AmpActions.sign
>(cosignReq);

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

const cosignedPset = cosignResponse.details.pset;

The HSM adds the server's secp256k1 signature, completing the 2-of-2 multisig.

Step 5: Broadcast

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

const broadcastReq = AmpBroadcastRequestSchema.parse({
  action: 'add',
  resource: '/amp/wallets/broadcast',
  details: {
    pset: cosignedPset,
  },
});

const broadcastResponse = await broadcaster.processBroadcast<
  typeof AmpActions.broadcast
>(broadcastReq);

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

const txid = broadcastResponse.details.txid;
console.log('Asset issued, txid:', txid);

What gets created on chain

After broadcast and confirmation (check tx on explorer):

  • A new Liquid asset with the hex asset_id derived from the issuance transaction.
  • A reissuance token (if satoshi_token > 0) that enables future supply increases.
  • An asset record in AMP2's database linked to your issuer account.

Verify issuance

await broadcaster.processBroadcast<
  typeof AmpActions.sync
>(
  {
    action: 'get',
    resource: `/amp/sync`,
  },
  {},
)

const assetsResponse = await broadcaster.processBroadcast<
  typeof AmpActions.listAssets
>(
  {
    action: 'get',
    resource: '/amp/assets',
    details: {},
  },
);

Next steps

On this page