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
- A registered wallet with a WID (see Wallet Registration)
- LBTC in the wallet to pay transaction fees
- An authenticated broadcaster (see Authentication)
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
| Parameter | Type | Description |
|---|---|---|
wid | string | Source wallet that pays fees and receives the new asset |
satoshi_asset | number | Total supply in satoshi units |
satoshi_token | number | Reissuance token amount (0 to disable future supply changes) |
contract.name | string | Human readable asset name (immutable after issuance) |
contract.ticker | string | Short symbol, e.g. "MST" |
contract.domain | string | Issuer domain for Asset Registry verification |
contract.precision | number | Decimal mapping (0-8). Precision 8: 100M sat = 1 unit |
contract.version | number | Contract format version (typically 0) |
contract.issuer_pubkey | string | Compressed public key of the issuer |
utxos | array | Specific UTXOs to use as inputs (empty = auto select) |
How precision works
precision: 8andsatoshi_asset: 100_000_000-> 1.00000000 display units (like Bitcoin)precision: 2andsatoshi_asset: 100_000-> 1,000.00 display units (like USD cents)precision: 0andsatoshi_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_idderived 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
- Sending Assets: transfer assets to investor wallets
- Managing Restrictions: control who can receive the asset
- PSET Lifecycle: understand the signing pattern
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.
Sending assets
Asset send operations move tokens from one wallet to one or more recipient wallets. The page explains request setup, PSET handling, restrictions, fees, and verification.