Blockstream Enterprise Custody SDK
Issuer Guide (SDK)

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.

Sending moves existing tokens from one wallet to one or more recipient wallets. It follows the same PSET lifecycle as issuance: request PSET -> sign -> cosign -> broadcast.

Prerequisites

  • The source wallet holds the asset being sent.
  • The source wallet has LBTC for transaction fees.
  • Recipient wallets are registered in AMP2 (see Wallet Registration).
  • If restrictions are set, all recipient WIDs must be in the source wallet's toWhitelist.

Build the send request

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

const sendReq = AmpSendAssetRequestSchema.parse({
  action: 'add',
  resource: `/amp/assets/${assetId}/send`,
  details: {
    wid: sourceWid,
    recipients: [
      { satoshi: 50_000, wid: investorWid1 },
      { satoshi: 25_000, wid: investorWid2 },
    ],
  },
});

Multi recipient support

A single send request can include multiple recipients. AMP2 constructs a single transaction with one output per recipient, which is more fee efficient than separate transfers.

Request the PSET

const sendResponse = await broadcastRequest<
  typeof AmpActions.send
>(sendReq, broadcaster);

if (sendResponse.status !== 'success') {
  throw new Error(
    `Send PSET request failed: ec=${sendResponse.error_code} message=${sendResponse.message}`
  );
}

const pset = sendResponse.details.pset;

Sign, cosign, and broadcast

The completion flow is identical to issuance:

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

// 1. Sign locally with LWK
const signedPset = signWithLwk(pset);

// 2. Request AMP2 cosigning
const cosignResponse = await broadcastRequest<
  typeof AmpActions.sign
>(
  AmpSignPsetRequestSchema.parse({
    action: 'add',
    resource: '/amp/wallets/sign',
    details: { pset: signedPset.toString() },
  }),
  broadcaster,
);

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

// 3. Broadcast
const broadcastResponse = await broadcastRequest<
  typeof AmpActions.broadcast
>(
  AmpBroadcastRequestSchema.parse({
    action: 'add',
    resource: '/amp/wallets/broadcast',
    details: { pset: cosignResponse.details.pset },
  }),
  broadcaster,
);

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

const txid = broadcastResponse.details.txid;

Restriction enforcement

When useToWhitelist is enabled on the source wallet's restriction group:

  1. AMP2 constructs the PSET with the requested recipients.
  2. At cosigning, the HSM checks every output address against the restriction group's toWhitelist.
  3. If any recipient WID is not in the whitelist, the HSM refuses to cosign and returns an error.

This enforcement is at the cryptographic level. There is no override.

LBTC fees

The source wallet pays the Liquid transaction fee in LBTC. If the wallet does not have enough LBTC, the PSET construction fails. Ensure wallets are funded before sending.

Verify the transfer

After broadcast, confirm the transfer:

const txsResponse = await broadcastRequest<
  typeof AmpActions.listAssetTxs
>(
  {
    action: 'get',
    resource: `/amp/assets/${assetId}/txs`,
    details: {},
  },
  broadcaster,
);

Next steps

On this page