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
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(&_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(&_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:
- Validates that the PSET structure matches the expected operation.
- Checks restriction policies for the source wallet and asset.
- Verifies that destination wallets are whitelisted (if
useToWhitelistis enabled). - 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
| Error | Cause | Action |
|---|---|---|
TxBuilder fails | Insufficient balance, no valid UTXOs | Check wallet balance, wait for confirmation |
| Cosign refused | Restriction policy violation | Verify whitelist and lock state |
| Signing fails | Wrong signer key | Verify signer matches descriptor key |
| Broadcast rejected | Duplicate tx or invalid PSET | Check 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
- Proxy Integration: abstract PSET flow for wallet clients
- Receiving Assets: detect incoming transfers
- PSET Lifecycle: conceptual overview
Asset receipt
Receiving AMP2-managed assets does not require a transaction-building step. The page explains receipt flow, transfer detection, address derivation, and confirmation states.
Proxy integration
A proxy service translates plain JSON requests into COSE-wrapped AMP2 calls. The page explains proxy purpose, operations, authentication, deployment models, and security guidance.