Blockstream Enterprise Custody SDK
Investor & Venue Guide

PSET signature

PSET signature supports outgoing asset transfers. The page explains transaction construction, AMP2 cosigning, local signing, broadcast, errors, and serialization helpers.

When a venue or investor needs to transfer assets, the flow involves building a PSET locally, requesting AMP2 cosigning, signing with venue keys, and broadcasting.

Full transaction flow

Build PSET with TxBuilder POST cosign {pset} Validate + cosign Cosigned PSET Cosigned PSET Sign with SwSigner Broadcast Venue AMP2 Server HSM Liquid Network

Step 1: Build the PSET

Use LWK's TxBuilder to construct a transaction:

let mut builder = TxBuilder::new(amp2.elements_network());
builder.add_recipient(recipient_address, satoshi, asset_id).unwrap();
let pset = builder.finish(&amp_wallet.inner);

Multiple recipients

Add multiple outputs to the same transaction:

let mut builder = TxBuilder::new(amp2.elements_network());
builder.add_recipient(address_1, 50_000, asset_id).unwrap();
builder.add_recipient(address_2, 25_000, asset_id).unwrap();
let pset = builder.finish(&amp_wallet.inner);

This creates a single transaction with two outputs, which is more fee efficient than separate transfers.

Step 2: Request AMP2 cosigning

Submit the PSET to AMP2 for policy validation and cosigning:

let response = amp2.cosign(pset).await.unwrap();
let cosigned_pset = response.pset;

During cosigning, the HSM:

  1. Validates that the PSET structure matches the expected operation.
  2. Checks restriction policies for the source wallet and asset.
  3. Verifies that destination wallets are whitelisted (if useToWhitelist is enabled).
  4. Adds the server's secp256k1 signature if all checks pass.

If any check fails, cosigning is refused and an error is returned.

Step 3: Sign with venue keys

After AMP2 cosigns, add your half of the 2-of-2 multisig:

use lwk_signer::SwSigner;

let mut pset = cosigned_pset;
signer.sign(&mut pset).unwrap();

The PSET is now fully signed (both venue + AMP2 signatures present).

Step 4: Broadcast

Broadcast the finalized transaction to the Liquid Network. Use your standard Liquid broadcast method:

// Via Esplora
let client = ElectrumClient::new(&ElectrumUrl::new("blockstream.info:995", true, true))?;
let txid = client.broadcast(&pset)?;

// Or via Elements RPC
// let txid = elements_rpc.sendrawtransaction(&pset_hex)?;

Error handling

ErrorCauseAction
TxBuilder failsInsufficient balance, no valid UTXOsCheck wallet balance, wait for confirmation
Cosign refusedRestriction policy violationVerify whitelist and lock state
Signing failsWrong signer keyVerify signer matches descriptor key
Broadcast rejectedDuplicate tx or invalid PSETCheck for existing txid, rebuild PSET

PSET serialization helpers

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

pub fn pset_to_b64(pset: &PartiallySignedTransaction) -> String {
    let bytes = el_encode::serialize(pset);
    general_purpose::STANDARD.encode(&bytes)
}

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

Next steps

On this page