Asset issuance & NFTs
Problem. Mint a brand-new Liquid asset — including a single-unit NFT — as part of an action, and use its derived asset ID in the same transaction and in later ones.
Every recipe so far has moved assets that already existed (L-BTC, a covenant's
collateral). This one creates them. On Liquid, an asset is issued as a property
of a transaction input: you point at a wallet UTXO, attach an issuance block, and
the transaction mints a new asset whose ID is derived from that input. Two ideas do
most of the work:
- The new asset's ID comes from the outpoint of the issuing input — so it's unique and unforgeable, but unknown until the input is chosen.
- You capture that ID with an
on_resolvedhook so the rest of the action (and the instance file) can refer to it.
The only example manifest that issues assets is the lending protocol, so the
snippets below are drawn from its IssueUtilityNFTs constructor — reduced to one
asset at a time. The lending walkthrough
shows all four issuances together.
The issuance block
Add issuance to any wallet input to mint an asset as that input is spent:
{
"id": "nft_issuance_input",
"utxo_source": "wallet",
"asset": "lbtc",
"issuance": { "kind": "new", "asset_amount_sat": 1, "inflation_amount_sat": 0 }
}
| Field | Required | Purpose |
|---|---|---|
kind | yes | "new" for a first issuance, "reissue" to mint more of an existing asset. |
asset_amount_sat | yes | How many units to issue, in the asset's base denomination. A literal or a formula. |
inflation_amount_sat | new only | How many reissuance tokens to mint. 0 fixes the supply forever — nobody can ever issue more. |
entropy | reissue only | The issuance entropy of the original mint. See below. |
issued_asset | no | On a reissue, the asset ID you expect. A check, not an input. |
Either amount may be 0: an issuance of reissuance tokens alone, or of a
fixed-supply asset with no reissuance rights.
The input is still an ordinary input: it's a wallet UTXO you also spend for its
L-BTC (here, to pay the fee). The issuance block just rides along on it.
Why the asset ID comes from the outpoint
A Liquid asset ID is computed from the outpoint (txid + vout) of the input that issues it. That makes the ID globally unique without a registry — no two inputs can ever share an outpoint — but it has a practical consequence:
One issuance per input, and the ID isn't known up front. To mint N distinct assets in one transaction you need N distinct input UTXOs. And because the ID depends on which UTXO the wallet picks, you can't hard-code it — you compute it during the build and capture it (next section).
This is why the lending protocol ships a Prepare helper action that splits one
wallet UTXO into four before IssueUtilityNFTs runs: four NFTs need four separate
inputs to issue from.
Capturing the new asset ID with on_resolved
Once the build picks the input's UTXO, its outpoint — and therefore the new asset
ID — is fixed. An on_resolved hook on the input fires at that moment and lets you
stash the ID into a compile param:
{
"id": "nft_issuance_input",
"utxo_source": "wallet",
"asset": "lbtc",
"issuance": { "kind": "new", "asset_amount_sat": 1, "inflation_amount_sat": 0 },
"on_resolved": { "set": { "instance.BORROWER_NFT_ASSET_ID": "asset" } }
}
Inside an input's own on_resolved, the bare word asset means this input's
resolved asset ID — the freshly minted one. Elsewhere in the action you refer to it
by the input's id, as nft_issuance_input.asset (the general
formula reference form). From here on,
instance.BORROWER_NFT_ASSET_ID is a normal param: you can lock outputs to
it, feed it into a covenant's compile params, or write it into the instance file.
Hooks recap.
on_resolvedruns per-input as soon as that input's UTXO is known;on_pre_broadcastruns once per action just before building. Both runsetassignments. See Hooks & tapleaf compute.
Minting more later: reissue
Issuing more of an existing asset means spending its reissuance token, and that
needs a value you can only capture at the original mint: the issuance
entropy, fast_merkle_root([sha256d(defining outpoint), contract_hash]). The
asset ID is derived from it.
It cannot be recovered from the chain. The reissuance-token UTXO carries no
trace of the outpoint that created it, so if the constructor doesn't record the
entropy, the asset can never be reissued. Capture it in create_instance, where
$inputs.<input_id>.issuance_entropy reaches it:
"create_instance": {
"fields": {
"TOKEN_ISSUANCE_ENTROPY": "$inputs.defining_in.issuance_entropy",
"TOKEN_ASSET": "$inputs.defining_in.issued_asset"
}
}
The reissuing action then hands it back:
{
"id": "reissue_in",
"utxo_source": "wallet",
"asset": "instance.TOKEN_REISSUANCE_TOKEN",
"issuance": {
"kind": "reissue",
"asset_amount_sat": "params.EXTRA_UNITS",
"entropy": "instance.TOKEN_ISSUANCE_ENTROPY",
"issued_asset": "instance.TOKEN_ASSET"
}
}
issued_asset is optional and is worth supplying. The engine re-derives the
asset ID from the entropy and refuses to build if the two disagree — and since an
entropy is opaque, and block explorers print it in the reverse byte order from
the one used here, a transposed value would otherwise build a perfectly
broadcastable transaction that reissues the wrong asset.
Run it
Issuance has no standalone example manifest — it's exercised by the lending
constructor. IssueUtilityNFTs needs four separate L-BTC UTXOs (one per
issuance), which Prepare carves out first:
txw run examples/lending/txmanifest.json Prepare \
--wallet borrower.json
txw run examples/lending/txmanifest.json IssueUtilityNFTs \
--wallet borrower.json
txw sync --wallet borrower.json
txw get-balance --wallet borrower.json # four new single-asset balances
After it broadcasts, the wallet holds four newly minted assets and the instance
file records their IDs. To see the transaction's outputs (including the issuance
outputs) before broadcasting, add --export-pset issue.pset.json and decode it.
Try next
You've minted assets and captured their derived IDs. The full four-NFT construction — plus packing loan terms into amounts and computing the covenants those NFTs get locked to — is the first phase of the lending walkthrough: Issuing the NFTs & encoding the terms. The hooks that capture and derive these values get their own recipe: Hooks & tapleaf compute.