Creating Bitcoin transactions
A Bitcoin transaction spends a number of unspent transaction outputs (UTXOs) and creates new UTXOs. In order to create a Bitcoin transaction, you need to:
- Get the available UTXOs corresponding to a Bitcoin address controlled by the canister using the
bitcoin_get_utxos
API endpoint. - Calculate an appropriate transaction fee using the
bitcoin_get_current_fee_percentiles
API endpoint. - Select a subset of the available UTXOs to spend that covers the transaction amount and fee.
- Create a transaction that spends the selected UTXOs and creates new UTXOs. You will need at least one for the recipient, and in most cases, one to collect the change.
Get available UTXOs
The following snippet shows how to get the available UTXOs corresponding to own_address
. Note that a canister can control many addresses, and each of them can have multiple UTXOs associated with it.
To test canisters locally that use the following code snippets, you will need to enable local Bitcoin development. To do this, you can either start the local development environment with dfx start --enable-bitcoin
or you can include the following configuration in the project's dfx.json
file:
"defaults": {
"bitcoin": {
"enabled": true,
"nodes": [
"127.0.0.1:18444"
],
"log_level": "info"
},
- Motoko
- Rust
public func get_p2pkh_address(network : Network, key_name : Text, derivation_path : [[Nat8]]) : async BitcoinAddress {
use crate::BTC_CONTEXT;
use ic_cdk::{
bitcoin_canister::{bitcoin_get_utxos, GetUtxosRequest, GetUtxosResponse},
update,
};
/// Returns the UTXOs of the given Bitcoin address.
#[update]
pub async fn get_utxos(address: String) -> GetUtxosResponse {
let ctx = BTC_CONTEXT.with(|ctx| ctx.get());
bitcoin_get_utxos(&GetUtxosRequest {
address,
network: ctx.network,
filter: None,
})
.await
.unwrap()
}
A UTXO has the following structure:
- Motoko
- Rust
/// An unspent transaction output.
public type Utxo = {
outpoint : OutPoint;
value : Satoshi;
height : Nat32;
};
/// A reference to a transaction output.
public type OutPoint = {
txid : Blob;
vout : Nat32;
};
/// Unspent transaction output (UTXO).
pub struct Utxo {
/// See [Outpoint].
pub outpoint: Outpoint,
/// Value in the units of satoshi.
pub value: Satoshi,
/// Height in the chain.
pub height: u32,
}
/// Identifier of [Utxo].
pub struct Outpoint {
/// Transaction Identifier.
pub txid: Vec<u8>,
/// A implicit index number.
pub vout: u32,
}
To create a transaction that sends X
satoshis to a destination address, you need to select a subset of the available UTXOs that cover the amount X
plus the transaction fee.
Calculate transaction fee per byte
The transaction fee of a Bitcoin transaction is calculated based on the size of the transaction in bytes. An appropriate fee per byte can be determined by looking at the fees of recent transactions that were included in the Bitcoin blockchain.
The following snippet shows how to estimate the fee per byte for a transaction using the bitcoin_get_current_fee_percentiles
API endpoint and choosing the 50th percentile.
- Motoko
- Rust
// Get fee percentiles from previous transactions to estimate our own fee.
let fee_percentiles = await BitcoinApi.get_current_fee_percentiles(network);
let fee_per_vbyte : MillisatoshiPerVByte = if(fee_percentiles.size() == 0) {
// There are no fee percentiles. This case can only happen on a regtest
// network where there are no non-coinbase transactions. In this case,
// we use a default of 1000 millisatoshis/vbyte (i.e. 2 satoshi/byte)
2000
} else {
// Choose the 50th percentile for sending fees.
fee_percentiles[50]
};
/// Estimates a reasonable fee rate (in millisatoshis per byte) for sending a Bitcoin transaction.
///
/// This function fetches recent fee percentiles from the Bitcoin API and returns
/// the median (50th percentile) fee rate, which is a reasonable default for timely inclusion.
///
/// - On **regtest** networks (without any coinbase mature transactions), no fee data is available,
/// so the function falls back to a static default of `2000` millisatoshis/byte (i.e., `2 sat/vB`).
///
/// # Returns
/// A fee rate in millisatoshis per byte (1000 msat = 1 satoshi).
pub async fn get_fee_per_byte(ctx: &BitcoinContext) -> u64 {
// Query recent fee percentiles from the Bitcoin API.
let fee_percentiles = bitcoin_get_current_fee_percentiles(&GetCurrentFeePercentilesRequest {
network: ctx.network,
})
.await
.unwrap();
if fee_percentiles.is_empty() {
// If the percentiles list is empty, we're likely on a regtest network
// with no standard transactions. Use a fixed fallback value.
2000 // 2 sat/vB in millisatoshis
} else {
// Use the 50th percentile (median) fee for balanced confirmation time and cost.
fee_percentiles[50]
}
}
Build the transaction
Next, the transaction can be built. The following snippet shows a simplified version of how to build a transaction that sends amount
satoshis to the dst_address
, and returns the change to the own_address
.
Since the fee of a transaction is based on its size, the transaction has to be built iteratively and signed with a mock signer that just adds the respective size of the signature. Each selected UTXO is used as an input for the transaction and requires a signature.
- Motoko
- Rust
public func build_transaction(
own_public_key : [Nat8],
own_address : BitcoinAddress,
own_utxos : [Utxo],
dst_address : BitcoinAddress,
amount : Satoshi,
fee_per_vbyte : MillisatoshiPerVByte,
) : async [Nat8] {
// We have a chicken-and-egg problem where we need to know the length
// of the transaction in order to compute its proper fee, but we need
// to know the proper fee in order to figure out the inputs needed for
// the transaction.
//
// We solve this problem iteratively. We start with a fee of zero, build
// and sign a transaction, see what its size is, and then update the fee,
// rebuild the transaction, until the fee is set to the correct amount.
let fee_per_vbyte_nat = Nat64.toNat(fee_per_vbyte);
Debug.print("Building transaction...");
var total_fee : Nat = 0;
loop {
let transaction =
Utils.get_ok_except(Bitcoin.buildTransaction(2, own_utxos, [(#p2pkh dst_address, amount)], #p2pkh own_address, Nat64.fromNat(total_fee)), "Error building transaction.");
// Sign the transaction. In this case, we only care about the size
// of the signed transaction, so we use a mock signer here for efficiency.
let signed_transaction_bytes = await sign_transaction(
own_public_key,
own_address,
transaction,
"", // mock key name
[], // mock derivation path
mock_signer,
);
let signed_tx_bytes_len : Nat = signed_transaction_bytes.size();
if((signed_tx_bytes_len * fee_per_vbyte_nat) / 1000 == total_fee) {
Debug.print("Transaction built with fee " # debug_show(total_fee));
return transaction.toBytes();
} else {
total_fee := (signed_tx_bytes_len * fee_per_vbyte_nat) / 1000;
}
}
};
/// Constructs a Bitcoin transaction from the given UTXOs, sending the specified `amount`
/// to `dst_address`, subtracting a fixed `fee`, and returning any remaining change
/// to `own_address` (if it's not considered dust (leftover bitcoin that is lower in value than the minimum limit of a valid transaction)).
///
/// Returns the constructed unsigned transaction and the list of previous outputs (`prevouts`)
/// used for signing.
///
/// Assumes that:
/// - Inputs are unspent and valid
/// - Dust threshold is 1_000 satoshis (outputs below this are omitted)
/// - UTXOs are already filtered to be spendable (e.g., confirmed, mature)
pub fn build_transaction_with_fee(
own_utxos: &[Utxo],
own_address: &Address,
dst_address: &Address,
amount: u64,
fee: u64,
) -> Result<(Transaction, Vec<TxOut>), String> {
// Define a dust threshold below which change outputs are discarded.
const DUST_THRESHOLD: u64 = 1_000;
// --- Input Selection ---
// Greedily select UTXOs in reverse order (oldest last) until we cover amount + fee.
let mut utxos_to_spend = vec![];
let mut total_spent = 0;
for utxo in own_utxos.iter().rev() {
total_spent += utxo.value;
utxos_to_spend.push(utxo);
if total_spent >= amount + fee {
break;
}
}
// Abort if we can't cover the payment + fee.
if total_spent < amount + fee {
return Err(format!(
"Insufficient balance: {}, trying to transfer {} satoshi with fee {}",
total_spent, amount, fee
));
}
// --- Build Inputs ---
let inputs: Vec<TxIn> = utxos_to_spend
.iter()
.map(|utxo| TxIn {
previous_output: OutPoint {
txid: Txid::from_raw_hash(Hash::from_slice(&utxo.outpoint.txid).unwrap()),
vout: utxo.outpoint.vout,
},
sequence: Sequence::MAX,
witness: Witness::new(), // Will be filled in during signing
script_sig: ScriptBuf::new(), // Empty for SegWit or Taproot
})
.collect();
// --- Create Previous Outputs ---
// Each TxOut struct represents an output of a previous transaction that is now being spent.
// This information is needed later when signing transactions for P2TR and P2WPKH.
let prevouts = utxos_to_spend
.into_iter()
.map(|utxo| TxOut {
value: Amount::from_sat(utxo.value),
script_pubkey: own_address.script_pubkey(),
})
.collect();
// --- Build Outputs ---
// Primary output: send amount to destination.
let mut outputs = vec![TxOut {
script_pubkey: dst_address.script_pubkey(),
value: Amount::from_sat(amount),
}];
// Add a change output if the remainder is above the dust threshold.
let change = total_spent - amount - fee;
if change >= DUST_THRESHOLD {
outputs.push(TxOut {
script_pubkey: own_address.script_pubkey(),
value: Amount::from_sat(change),
});
}
// --- Assemble Transaction ---
Ok((
Transaction {
input: inputs,
output: outputs,
lock_time: LockTime::ZERO,
version: Version::TWO,
},
prevouts,
))
}
Learn more about constructing Bitcoin transactions with the Rust Bitcoin Cookbook.