BSV.Tokens.Bundle.Stas3Bundle (bsv_sdk v2.0.0)

Copy Markdown View Source

STAS3 Bundle Factory — automatic merge/split/transfer transaction planning.

Given callback functions for UTXO retrieval, transaction lookup, and script construction, the factory automatically plans sequences of merge/split/transfer transactions to fulfill multi-recipient payouts.

How It Works

  1. UTXO Selection — selects the minimum set of STAS UTXOs that cover the required amount (exact match preferred, else smallest-first accumulation)
  2. Merge Tree — if multiple UTXOs selected, builds pairwise merge transactions to consolidate into a single UTXO. Inserts "transfer refresh" levels every 3 merge levels to prevent excessive script depth.
  3. Transfer Planning — splits the consolidated UTXO across recipients, up to 3 per intermediate tx (with STAS change), up to 4 in the final tx.
  4. Fee Chaining — a single funding UTXO is chained through all transactions; each tx's fee change output feeds the next tx's fee input.

Callback Functions

The struct holds five callback functions:

  • get_stas_utxo_set(min_satoshis) -> [utxo] returns available STAS UTXOs
  • get_funding_utxo(request) -> utxo returns a fee-paying UTXO
  • get_transactions([txid_hex]) -> %{txid_hex => Transaction.t()} lookups
  • build_locking_params(args) -> Stas3OutputParams.t() constructs output params
  • build_unlocking_script(args) -> {:ok, Script.t()} signs/unlocks inputs

UTXO Map Structure

Each UTXO in callbacks is a map with keys:

  • :txid — 32-byte binary txid (internal byte order)
  • :txid_hex — hex string txid (display order, for transaction lookups)
  • :vout — output index
  • :satoshis — satoshi amount
  • :locking_scriptScript.t() locking script

Summary

Functions

Generic bundle creation for spend types that bypass transfer/2 validation (swap, confiscation). These use get_stas_utxo_set directly.

Create a freeze bundle (sets spend type to :freeze).

Create a single-recipient transfer bundle.

Create an unfreeze bundle (sets spend type to :unfreeze).

Plan and build a multi-recipient transfer bundle.

Types

bundle_result()

@type bundle_result() ::
  {:ok, %{transactions: [String.t()], fee_satoshis: non_neg_integer()}}
  | {:ok, %{message: String.t(), fee_satoshis: 0}}

funding_request()

@type funding_request() :: %{
  utxo_ids_to_spend: [String.t()],
  estimated_fee_satoshis: non_neg_integer(),
  transactions_count: non_neg_integer()
}

locking_params_args()

@type locking_params_args() :: %{
  from_utxo: utxo(),
  recipient: recipient(),
  spend_type: spend_type(),
  is_freeze_like: boolean(),
  output_index: non_neg_integer(),
  output_count: non_neg_integer(),
  is_change: boolean()
}

recipient()

@type recipient() :: %{m: pos_integer(), addresses: [String.t()]}

spend_type()

@type spend_type() :: :transfer | :freeze | :unfreeze | :swap | :confiscation

t()

@type t() :: %BSV.Tokens.Bundle.Stas3Bundle{
  build_locking_params: (locking_params_args() ->
                           BSV.Tokens.Stas3OutputParams.t()),
  build_unlocking_script: (unlocking_args() -> {:ok, BSV.Script.t()}),
  fee_rate: non_neg_integer(),
  fee_wallet: map(),
  get_funding_utxo: (funding_request() -> utxo()),
  get_stas_utxo_set: (non_neg_integer() -> [utxo()]),
  get_transactions: ([String.t()] ->
                       %{required(String.t()) => BSV.Transaction.t()}),
  stas_wallet: map()
}

transfer_output()

@type transfer_output() :: %{recipient: recipient(), satoshis: non_neg_integer()}

unlocking_args()

@type unlocking_args() :: %{
  tx: BSV.Transaction.t(),
  input_index: non_neg_integer(),
  utxo: utxo(),
  spend_type: spend_type(),
  is_freeze_like: boolean(),
  is_merge: boolean()
}

utxo()

@type utxo() :: %{
  txid: binary(),
  txid_hex: String.t(),
  vout: non_neg_integer(),
  satoshis: non_neg_integer(),
  locking_script: BSV.Script.t()
}

Functions

create_bundle(bundle, amount, recipient, spend_type, note \\ nil)

@spec create_bundle(
  t(),
  non_neg_integer(),
  recipient(),
  spend_type(),
  [binary()] | nil
) ::
  bundle_result()

Generic bundle creation for spend types that bypass transfer/2 validation (swap, confiscation). These use get_stas_utxo_set directly.

create_confiscation_bundle(bundle, amount, recipient, note \\ nil)

@spec create_confiscation_bundle(
  t(),
  non_neg_integer(),
  recipient(),
  [binary()] | nil
) ::
  bundle_result()

Create a confiscation bundle.

create_freeze_bundle(bundle, amount, recipient, note \\ nil)

@spec create_freeze_bundle(t(), non_neg_integer(), recipient(), [binary()] | nil) ::
  bundle_result()

Create a freeze bundle (sets spend type to :freeze).

create_swap_bundle(bundle, amount, recipient, note \\ nil)

@spec create_swap_bundle(t(), non_neg_integer(), recipient(), [binary()] | nil) ::
  bundle_result()

Create a swap bundle.

create_transfer_bundle(bundle, amount, recipient, note \\ nil)

@spec create_transfer_bundle(t(), non_neg_integer(), recipient(), [binary()] | nil) ::
  bundle_result()

Create a single-recipient transfer bundle.

create_unfreeze_bundle(bundle, amount, recipient, note \\ nil)

@spec create_unfreeze_bundle(t(), non_neg_integer(), recipient(), [binary()] | nil) ::
  bundle_result()

Create an unfreeze bundle (sets spend type to :unfreeze).

transfer(bundle, request)

@spec transfer(t(), %{
  outputs: [transfer_output()],
  spend_type: spend_type(),
  note: [binary()] | nil
}) ::
  bundle_result()

Plan and build a multi-recipient transfer bundle.

Takes a list of outputs (each with :recipient and :satoshis), an optional :spend_type (default :transfer), and optional :note (list of binaries for OP_RETURN, attached only to the final transaction).

Returns {:ok, %{transactions: [hex], fee_satoshis: int}} on success, or {:ok, %{message: "Insufficient ...", fee_satoshis: 0}} if balance is too low. Raises on invalid inputs (empty outputs, zero satoshis).