Creating Bitcoin transactions
Unspent transaction outputs (UTXOs) are used as inputs to build a Bitcoin transaction. Every Bitcoin transaction spends one or more UTXOs and in return creates new UTXOs. A UTXO exists until it is used as input for a future transaction. In order to create a Bitcoin transaction, you need to:
Get the available UTXOs corresponding to a Bitcoin address controlled by your ICP smart contract 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.
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,
}
Get available UTXOs
The following snippet shows how to get the available UTXOs corresponding to own_address
. Note that an ICP smart contract can control multiple addresses and each one can have multiple UTXOs associated with it.
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.
To implement the following code snippets in your dapp, you will need to setup a local development environment and create a project.
- Motoko
- Rust
/// Returns the UTXOs of the given Bitcoin address.
///
/// NOTE: Relies on the `bitcoin_get_utxos` endpoint.
/// See https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-bitcoin_get_utxos
public func get_utxos(network : Network, address : BitcoinAddress) : async GetUtxosResponse {
ExperimentalCycles.add<system>(GET_UTXOS_COST_CYCLES);
await management_canister_actor.bitcoin_get_utxos({
address;
network;
filter = null;
})
};
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()
}
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 on the Bitcoin mainnet. 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
/// Returns the 100 fee percentiles measured in millisatoshi/vbyte.
/// Percentiles are computed from the last 10,000 transactions (if available).
///
/// Relies on the `bitcoin_get_current_fee_percentiles` endpoint.
/// See https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-bitcoin_get_current_fee_percentiles
public func get_current_fee_percentiles(network : Network) : async [MillisatoshiPerVByte] {
ExperimentalCycles.add<system>(GET_CURRENT_FEE_PERCENTILES_COST_CYCLES);
await management_canister_actor.bitcoin_get_current_fee_percentiles({
network;
})
};
use crate::BTC_CONTEXT;
use ic_cdk::{
bitcoin_canister::{
bitcoin_get_current_fee_percentiles, GetCurrentFeePercentilesRequest, MillisatoshiPerByte,
},
update,
};
/// Returns the 100 fee percentiles measured in millisatoshi/byte.
/// Percentiles are computed from the last 10,000 transactions (if available).
#[update]
pub async fn get_current_fee_percentiles() -> Vec<MillisatoshiPerByte> {
let ctx = BTC_CONTEXT.with(|ctx| ctx.get());
bitcoin_get_current_fee_percentiles(&GetCurrentFeePercentilesRequest {
network: ctx.network,
})
.await
.unwrap()
}
Build the transaction
Now the transaction can be built. 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 adds the respective size of the signature. Each selected UTXO is used as an input for the transaction and requires a signature.
The following snippet shows a simplified version of how to build a transaction that will be signed by a P2PKH address:
- Motoko
- Rust
// Builds a transaction to send the given `amount` of satoshis to the
// destination address.
func build_transaction(
ecdsa_canister_actor : EcdsaCanisterActor,
own_public_key : [Nat8],
own_address : BitcoinAddress,
own_utxos : [Utxo],
dst_address : BitcoinAddress,
amount : Satoshi,
fee_per_vbyte : MillisatoshiPerVByte,
) : async [Nat8] {
let dst_address_typed = Utils.get_ok_expect(Address.addressFromText(dst_address), "failed to decode destination address");
// 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_expect(Bitcoin.buildTransaction(2, own_utxos, [(dst_address_typed, 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(
ecdsa_canister_actor,
own_public_key,
own_address,
transaction,
"", // mock key name
[], // mock derivation path
Utils.ecdsa_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;
};
};
};
};
use ic_cdk::{
bitcoin_canister::{MillisatoshiPerByte, Utxo},
trap,
};
use std::convert::TryFrom;
// Builds a transaction to send the given `amount` of satoshis to the
// destination address.
pub async fn build_transaction(
ctx: &BitcoinContext,
own_public_key: &PublicKey,
own_address: &Address,
own_utxos: &[Utxo],
primary_output: &PrimaryOutput,
fee_per_vbyte: MillisatoshiPerByte,
) -> Transaction {
// 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 amount = match primary_output {
PrimaryOutput::Address(_, amt) => *amt, // grab the amount
PrimaryOutput::OpReturn(_) => trap("expected an address output, got OP_RETURN"),
};
let mut fee = 0;
loop {
let utxos_to_spend = select_utxos_greedy(own_utxos, amount, fee).unwrap();
let (transaction, _) =
build_transaction_with_fee(utxos_to_spend, own_address, primary_output, fee).unwrap();
// 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 = sign_transaction(
ctx,
own_public_key,
own_address,
transaction.clone(),
vec![], // mock derivation path
Learn more about constructing Bitcoin transactions with the Rust Bitcoin Cookbook.