🥷 Try it live — no local setup. ICP Ninja is a web-based IDE that builds and deploys this project to the mainnet for free, right in your browser. Click the badge above, or hit Deploy if you're already in Ninja. To build and run it locally instead, follow the steps below.
This example demonstrates how a canister can receive and send bitcoin on the Internet Computer, including support for legacy (P2PKH), SegWit (P2WPKH), and Taproot (P2TR) address types.
For a deeper understanding of the ICP ↔ Bitcoin integration, see the Bitcoin integration concepts.
This example integrates with the Internet Computer's built-in:
- Threshold ECDSA (
ecdsa_public_key,sign_with_ecdsa) — derives P2PKH addresses and signs transactions spending from them - Threshold Schnorr (
schnorr_public_key,sign_with_schnorr) — derives P2TR addresses (BIP340/341) and signs Taproot transactions - Bitcoin canister — queries balances, UTXOs, fee percentiles, and block data; submits signed transactions to the Bitcoin network
- Node.js v18+
- icp-cli:
npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm - Rust v1.85+ with
wasm32-unknown-unknowntarget:rustup target add wasm32-unknown-unknown - Docker (required to run the custom network launcher image that bundles bitcoind)
- On macOS, a
clangwith WASM support is required to compile thesecp256k1-sysC library for thewasm32-unknown-unknowntarget. Xcode's bundled clang does not include the WASM backend. Install the Homebrew LLVM and add it to your PATH:Add thebrew install llvm export PATH="$(brew --prefix llvm)/bin:$PATH"
exportline to your shell profile (~/.zshrcor~/.bashrc) to make it permanent.
git clone https://github.com/dfinity/examples
cd examples/rust/basic_bitcoinThe local network bundles bitcoind inside a custom Docker image. Build it once before starting the network:
./build-image.shicp network start -d
icp deploy --cycles 30t
bash test.sh
icp network stopIf tests fail with an out-of-cycles error, top up the canister and retry:
icp canister top-up --amount 30t backend
The ic environment deploys to IC mainnet connected to Bitcoin testnet4, using test_key_1:
icp deploy -e ic --cycles 30tTo deploy to Bitcoin mainnet, change the
init_argsfor theicenvironment inicp.yamlfromtestnettomainnet. The canister automatically selectskey_1(the production threshold signing key) when initialized with themainnetvariant.
The example demonstrates how to generate and use the following address types:
- P2PKH (Legacy) using ECDSA and
sign_with_ecdsa - P2WPKH (SegWit v0) using ECDSA and
sign_with_ecdsa - P2TR (Taproot, key-path-only) using Schnorr keys and
sign_with_schnorr - P2TR (Taproot, script-path-enabled) commits to a script allowing both key path and script path spending
icp canister call backend get_p2pkh_address '()'
# or: get_p2wpkh_address, get_p2tr_key_path_only_address, get_p2tr_script_path_enabled_addressThis walkthrough shows how to fund an address, check its balance, send bitcoin to another address, and confirm the transfer — using the bundled bitcoind in regtest mode.
Coinbase maturity: In Bitcoin, newly mined block rewards (coinbase UTXOs) cannot be spent until 100 more blocks have been mined on top. Mine at least 101 blocks upfront so the first reward is immediately spendable.
CONTAINER=$(docker ps --filter "ancestor=icp-cli-network-launcher-bitcoin" --format "{{.ID}}" | head -1)
ADDR=$(icp canister call backend get_p2pkh_address '()' | grep -o '"[^"]*"' | tr -d '"')
echo "Address: $ADDR"Mining 101 blocks ensures the first block reward (50 BTC) is past the coinbase maturity threshold and immediately spendable.
docker exec $CONTAINER bitcoin-cli -regtest \
-rpcuser=ic-btc-integration -rpcpassword=ic-btc-integration \
generatetoaddress 101 "$ADDR"The IC Bitcoin integration syncs new blocks continuously. If the balance shows 0, wait a few seconds and retry.
icp canister call backend get_balance "(\"$ADDR\")"
# Expected: (505_000_000_000 : nat64) — 101 blocks × 50 BTC eachDEST="bcrt1qg8qknn6f3txqg97gt8ca0ctya0vw7ep6d02qmt"
icp canister call backend send_from_p2pkh_address "(record {
destination_address = \"$DEST\";
amount_in_satoshi = 4321;
})"
# Returns the transaction IDThe transaction is now broadcast to bitcoind's mempool. The destination balance will remain 0 until it is confirmed in a block.
docker exec $CONTAINER bitcoin-cli -regtest \
-rpcuser=ic-btc-integration -rpcpassword=ic-btc-integration \
generatetoaddress 1 "$ADDR"icp canister call backend get_balance "(\"$DEST\")"
# Expected: (4_321 : nat64)The same pattern (fund → send → mine confirmation block → verify) applies to the other address types:
send_from_p2wpkh_addresssend_from_p2tr_key_path_only_addresssend_from_p2tr_script_path_enabled_address_key_spendsend_from_p2tr_script_path_enabled_address_script_spend
Each endpoint internally estimates fees, selects UTXOs, builds a transaction, signs it using ECDSA or Schnorr, and broadcasts it via bitcoin_send_transaction.
When the canister is deployed on IC mainnet, you can track testnet transactions on mempool.space.
You can inspect the UTXOs held at any Bitcoin address:
icp canister call backend get_utxos "(\"$ADDR\")"This returns all unspent outputs at the address — useful for verifying that funds arrived or for debugging balance issues. The response includes each outpoint (txid + vout index), value in satoshis, and confirmation height.
You can query the current state of the Bitcoin blockchain:
icp canister call backend get_blockchain_info '()'This calls get_blockchain_info on the Bitcoin canister and returns the tip height, block hash, timestamp, difficulty, and total UTXO count. It is useful for monitoring the state of the Bitcoin network from your canister.
You can query historical block headers:
icp canister call backend get_block_headers '(10: nat32, null)'
# or a range:
icp canister call backend get_block_headers '(10: nat32, opt (11: nat32))'This calls bitcoin_get_block_headers, which is useful for blockchain validation or light client logic.
This example implements several important patterns for Bitcoin integration:
- Derivation paths: Keys are derived using structured derivation paths according to BIP-32, ensuring reproducible key generation.
- Key caching: Optimization is used to avoid repeated calls to
get_ecdsa_public_keyandget_schnorr_public_key. - Manual transaction construction: Transactions are assembled and signed manually, ensuring maximum flexibility in construction and fee estimation.
- Cost optimization: When testing on mainnet, the chain-key testing canister can be used to save on costs for calling the threshold signing APIs.
This example is provided for educational purposes and is not production-ready. It is important to consider security implications when developing applications that interact with Bitcoin or other cryptocurrencies. The code has not been audited and may contain vulnerabilities or security issues.
If you base your application on this example, we recommend you familiarize yourself with and adhere to the security best practices for developing on the Internet Computer. This example may not implement all the best practices.
For example, the following aspects are particularly relevant for this app:
- Certify query responses if they are relevant for security, since the app offers a method to read balances.
- Use a decentralized governance system like SNS to give a canister a decentralized controller, since decentralized control may be essential for canisters holding bitcoins on behalf of users.