From 836801a4d4b35b007a1a6d36602534ee4741d87b Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 20 Mar 2026 09:18:11 +0100 Subject: [PATCH 1/3] docs: Bitcoin integration guide --- docs/guides/chain-fusion/bitcoin.md | 24 - docs/guides/chain-fusion/bitcoin.mdx | 716 +++++++++++++++++++++++++++ 2 files changed, 716 insertions(+), 24 deletions(-) delete mode 100644 docs/guides/chain-fusion/bitcoin.md create mode 100644 docs/guides/chain-fusion/bitcoin.mdx diff --git a/docs/guides/chain-fusion/bitcoin.md b/docs/guides/chain-fusion/bitcoin.md deleted file mode 100644 index 009a612d..00000000 --- a/docs/guides/chain-fusion/bitcoin.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: "Bitcoin Integration" -description: "Send and receive BTC directly from ICP canisters using chain-key signatures" -sidebar: - order: 1 ---- - -TODO: Write content for this page. - - -Integrate Bitcoin with ICP canisters. Cover the full workflow: generating Bitcoin addresses using threshold ECDSA and Schnorr signatures, creating transactions, signing with chain-key, submitting to the Bitcoin network, and UTXO management. Include local regtest setup, the bitcoin-starter template, and multi-environment configuration (regtest/testnet/mainnet). Mention ckBTC as the tokenized alternative. - - -- Portal: build-on-btc/ (14 files: index, btc-api, btc-dev-env, btc-dev-workflow, btc-transactions/*, read-state, using-regtest, brc-20, ordinals, runes) -- icskills: ckbtc -- Examples: basic_bitcoin (both), threshold-ecdsa (both), threshold-schnorr (both) -- Template: bitcoin-starter (multi-environment icp.yaml) -- Learn Hub: [Bitcoin Integration](https://learn.internetcomputer.org/hc/en-us/articles/34211154520084), [Chain-key Bitcoin](https://learn.internetcomputer.org/hc/en-us/articles/44598021228564), [Bitcoin Checker Canister](https://learn.internetcomputer.org/hc/en-us/articles/45033984570516) - - -- concepts/chain-fusion -- chain fusion overview -- concepts/chain-key-cryptography -- how threshold signatures work -- guides/defi/chain-key-tokens -- ckBTC -- guides/chain-fusion/ethereum -- similar patterns for ETH diff --git a/docs/guides/chain-fusion/bitcoin.mdx b/docs/guides/chain-fusion/bitcoin.mdx new file mode 100644 index 00000000..0e5de266 --- /dev/null +++ b/docs/guides/chain-fusion/bitcoin.mdx @@ -0,0 +1,716 @@ +--- +title: "Bitcoin Integration" +description: "Send and receive BTC directly from ICP canisters using chain-key signatures" +sidebar: + order: 1 +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +ICP provides a protocol-level integration with the Bitcoin network. Canisters can hold BTC, generate Bitcoin addresses, build transactions, sign them with threshold ECDSA or Schnorr signatures, and submit them to the Bitcoin network — all without bridges or oracles. + +There are two approaches to working with Bitcoin on ICP: + +- **ckBTC (chain-key Bitcoin)** — a 1:1 BTC-backed token native to ICP. Transfers settle in 1-2 seconds with a 10 satoshi fee. Best for most applications that need to accept, hold, or transfer Bitcoin value. +- **Direct Bitcoin API** — call the management canister to read UTXOs, get balances, and submit raw Bitcoin transactions. Best for advanced use cases that need full control over Bitcoin transactions (custom scripts, Ordinals, Runes, BRC-20). + +This guide covers both approaches. + +## ckBTC integration + +ckBTC is the recommended path for most developers. The ckBTC minter canister holds real BTC and mints/burns ckBTC tokens. Your canister interacts with the minter and ledger canisters using standard ICRC-1/ICRC-2 interfaces. + +### Canister IDs + +| Canister | Mainnet | Testnet4 | +|---|---|---| +| ckBTC Ledger | `mxzaz-hqaaa-aaaar-qaada-cai` | `mc6ru-gyaaa-aaaar-qaaaq-cai` | +| ckBTC Minter | `mqygn-kiaaa-aaaar-qaadq-cai` | `ml52i-qqaaa-aaaar-qaaba-cai` | +| ckBTC Index | `n5wcd-faaaa-aaaar-qaaea-cai` | `mm444-5iaaa-aaaar-qaabq-cai` | + +### Deposit flow (BTC to ckBTC) + +1. Call `get_btc_address` on the minter with the user's principal and subaccount. This returns a unique Bitcoin address controlled by the minter. +2. The user sends BTC to that address using any Bitcoin wallet. +3. Wait for Bitcoin confirmations (the minter requires confirmations before minting). +4. Call `update_balance` on the minter with the same principal and subaccount. The minter checks for new UTXOs and mints equivalent ckBTC to the user's ICRC-1 account. + + + + +```motoko +import Principal "mo:core/Principal"; +import Blob "mo:core/Blob"; +import Nat8 "mo:core/Nat8"; +import Array "mo:core/Array"; +import Runtime "mo:core/Runtime"; + +persistent actor Self { + + type Account = { owner : Principal; subaccount : ?Blob }; + type UpdateBalanceResult = { #Ok : [UtxoStatus]; #Err : UpdateBalanceError }; + // See the full example for UtxoStatus and UpdateBalanceError definitions + + transient let ckbtcMinter : actor { + get_btc_address : shared ({ owner : ?Principal; subaccount : ?Blob }) -> async Text; + update_balance : shared ({ owner : ?Principal; subaccount : ?Blob }) -> async UpdateBalanceResult; + } = actor "mqygn-kiaaa-aaaar-qaadq-cai"; + + func principalToSubaccount(p : Principal) : Blob { + let bytes = Blob.toArray(Principal.toBlob(p)); + let size = bytes.size(); + let sub = Array.tabulate(32, func(i : Nat) : Nat8 { + if (i == 0) { Nat8.fromNat(size) } + else if (i <= size) { bytes[i - 1] } + else { 0 } + }); + Blob.fromArray(sub) + }; + + public shared ({ caller }) func getDepositAddress() : async Text { + if (Principal.isAnonymous(caller)) { Runtime.trap("Authentication required") }; + let subaccount = principalToSubaccount(caller); + await ckbtcMinter.get_btc_address({ + owner = ?Principal.fromActor(Self); + subaccount = ?subaccount; + }) + }; + + public shared ({ caller }) func updateBalance() : async UpdateBalanceResult { + if (Principal.isAnonymous(caller)) { Runtime.trap("Authentication required") }; + let subaccount = principalToSubaccount(caller); + await ckbtcMinter.update_balance({ + owner = ?Principal.fromActor(Self); + subaccount = ?subaccount; + }) + }; +}; +``` + + + + +```rust +use candid::{CandidType, Deserialize, Principal}; +use ic_cdk::update; +use ic_cdk::call::Call; + +const CKBTC_MINTER: &str = "mqygn-kiaaa-aaaar-qaadq-cai"; + +#[derive(CandidType, Deserialize)] +struct GetBtcAddressArgs { + owner: Option, + subaccount: Option>, +} + +fn principal_to_subaccount(principal: &Principal) -> [u8; 32] { + let mut subaccount = [0u8; 32]; + let principal_bytes = principal.as_slice(); + subaccount[0] = principal_bytes.len() as u8; + subaccount[1..1 + principal_bytes.len()].copy_from_slice(principal_bytes); + subaccount +} + +fn minter_id() -> Principal { + Principal::from_text(CKBTC_MINTER).unwrap() +} + +#[update] +async fn get_deposit_address() -> String { + let caller = ic_cdk::api::msg_caller(); + assert_ne!(caller, Principal::anonymous(), "Authentication required"); + + let subaccount = principal_to_subaccount(&caller); + let args = GetBtcAddressArgs { + owner: Some(ic_cdk::api::canister_self()), + subaccount: Some(subaccount.to_vec()), + }; + + let (address,): (String,) = Call::unbounded_wait(minter_id(), "get_btc_address") + .with_arg(args) + .await + .expect("Failed to get BTC address") + .candid_tuple() + .expect("Failed to decode response"); + + address +} +``` + + + + +### Transfer ckBTC + +Call `icrc1_transfer` on the ckBTC ledger. The fee is 10 satoshis and transfers settle in 1-2 seconds. + + + + +```motoko +// Inside your persistent actor: + +type TransferArgs = { + from_subaccount : ?Blob; + to : Account; + amount : Nat; + fee : ?Nat; + memo : ?Blob; + created_at_time : ?Nat64; +}; + +type TransferResult = { #Ok : Nat; #Err : TransferError }; +// See the full example for TransferError definition + +transient let ckbtcLedger : actor { + icrc1_transfer : shared (TransferArgs) -> async TransferResult; +} = actor "mxzaz-hqaaa-aaaar-qaada-cai"; + +public shared ({ caller }) func transfer(to : Principal, amount : Nat) : async TransferResult { + if (Principal.isAnonymous(caller)) { Runtime.trap("Authentication required") }; + let fromSubaccount = principalToSubaccount(caller); + await ckbtcLedger.icrc1_transfer({ + from_subaccount = ?fromSubaccount; + to = { owner = to; subaccount = null }; + amount = amount; + fee = ?10; + memo = null; + created_at_time = null; + }) +}; +``` + + + + +```rust +use icrc_ledger_types::icrc1::account::Account; +use icrc_ledger_types::icrc1::transfer::{TransferArg, TransferError}; +use candid::Nat; + +const CKBTC_LEDGER: &str = "mxzaz-hqaaa-aaaar-qaada-cai"; + +fn ledger_id() -> Principal { + Principal::from_text(CKBTC_LEDGER).unwrap() +} + +#[update] +async fn transfer(to: Principal, amount: Nat) -> Result { + let caller = ic_cdk::api::msg_caller(); + assert_ne!(caller, Principal::anonymous(), "Authentication required"); + + let from_subaccount = principal_to_subaccount(&caller); + let args = TransferArg { + from_subaccount: Some(from_subaccount), + to: Account { owner: to, subaccount: None }, + amount, + fee: Some(Nat::from(10u64)), + memo: None, + created_at_time: None, + }; + + let (result,): (Result,) = + Call::unbounded_wait(ledger_id(), "icrc1_transfer") + .with_arg(args) + .await + .expect("Failed to call icrc1_transfer") + .candid_tuple() + .expect("Failed to decode response"); + + result +} +``` + + + + +### Withdraw (ckBTC to BTC) + +Withdrawal is a two-step process: approve the minter to spend your ckBTC, then call `retrieve_btc_with_approval`. The minimum withdrawal is 50,000 satoshis (0.0005 BTC). + + + + +```motoko +// Inside your persistent actor: + +type ApproveArgs = { + from_subaccount : ?Blob; + spender : Account; + amount : Nat; + expected_allowance : ?Nat; + expires_at : ?Nat64; + fee : ?Nat; + memo : ?Blob; + created_at_time : ?Nat64; +}; + +type ApproveError = { + #BadFee : { expected_fee : Nat }; + #InsufficientFunds : { balance : Nat }; + #AllowanceChanged : { current_allowance : Nat }; + #Expired : { ledger_time : Nat64 }; + #TooOld; + #CreatedInFuture : { ledger_time : Nat64 }; + #Duplicate : { duplicate_of : Nat }; + #TemporarilyUnavailable; + #GenericError : { error_code : Nat; message : Text }; +}; + +type RetrieveBtcWithApprovalArgs = { + address : Text; + amount : Nat64; + from_subaccount : ?Blob; +}; + +type RetrieveBtcResult = { + #Ok : { block_index : Nat64 }; + #Err : RetrieveBtcError; +}; + +// See the full example for RetrieveBtcError definition + +public shared ({ caller }) func withdraw(btcAddress : Text, amount : Nat64) : async RetrieveBtcResult { + if (Principal.isAnonymous(caller)) { Runtime.trap("Authentication required") }; + + let fromSubaccount = principalToSubaccount(caller); + let approveResult = await ckbtcLedger.icrc2_approve({ + from_subaccount = ?fromSubaccount; + spender = { + owner = Principal.fromText("mqygn-kiaaa-aaaar-qaadq-cai"); + subaccount = null; + }; + amount = Nat64.toNat(amount) + 10; + expected_allowance = null; + expires_at = null; + fee = ?10; + memo = null; + created_at_time = null; + }); + + switch (approveResult) { + case (#Err(_)) { + return #Err(#GenericError({ error_code = 0; error_message = "Approve failed" })) + }; + case (#Ok(_)) {}; + }; + + await ckbtcMinter.retrieve_btc_with_approval({ + address = btcAddress; + amount = amount; + from_subaccount = ?fromSubaccount; + }) +}; +``` + + + + +```rust +use icrc_ledger_types::icrc2::approve::{ApproveArgs, ApproveError}; + +#[derive(CandidType, Deserialize)] +struct RetrieveBtcWithApprovalArgs { + address: String, + amount: u64, + from_subaccount: Option>, +} + +#[derive(CandidType, Deserialize)] +struct RetrieveBtcOk { block_index: u64 } + +#[derive(CandidType, Deserialize, Debug)] +enum RetrieveBtcError { + MalformedAddress(String), + AlreadyProcessing, + AmountTooLow(u64), + InsufficientFunds { balance: u64 }, + InsufficientAllowance { allowance: u64 }, + TemporarilyUnavailable(String), + GenericError { error_code: u64, error_message: String }, +} + +type RetrieveBtcResult = Result; + +#[update] +async fn withdraw(btc_address: String, amount: u64) -> RetrieveBtcResult { + let caller = ic_cdk::api::msg_caller(); + assert_ne!(caller, Principal::anonymous(), "Authentication required"); + + // Step 1: Approve the minter to spend ckBTC + let from_subaccount = principal_to_subaccount(&caller); + let approve_args = ApproveArgs { + from_subaccount: Some(from_subaccount), + spender: Account { owner: minter_id(), subaccount: None }, + amount: Nat::from(amount) + Nat::from(10u64), + expected_allowance: None, + expires_at: None, + fee: Some(Nat::from(10u64)), + memo: None, + created_at_time: None, + }; + + let (approve_result,): (Result,) = + Call::unbounded_wait(ledger_id(), "icrc2_approve") + .with_arg(approve_args) + .await + .expect("Failed to call icrc2_approve") + .candid_tuple() + .expect("Failed to decode response"); + + if let Err(e) = approve_result { + return Err(RetrieveBtcError::GenericError { + error_code: 0, + error_message: format!("Approve failed: {:?}", e), + }); + } + + // Step 2: Request BTC withdrawal + let args = RetrieveBtcWithApprovalArgs { + address: btc_address, + amount, + from_subaccount: Some(from_subaccount.to_vec()), + }; + + let (result,): (RetrieveBtcResult,) = + Call::unbounded_wait(minter_id(), "retrieve_btc_with_approval") + .with_arg(args) + .await + .expect("Failed to call retrieve_btc_with_approval") + .candid_tuple() + .expect("Failed to decode response"); + + result +} +``` + + + + +### Interact with ckBTC using icp-cli + +You can call the ckBTC canisters directly from the command line: + +```bash +# Get a BTC deposit address +icp canister call mqygn-kiaaa-aaaar-qaadq-cai get_btc_address \ + '(record { owner = opt principal "YOUR-PRINCIPAL"; subaccount = null })' \ + -e ic + +# Check for new deposits and mint ckBTC +icp canister call mqygn-kiaaa-aaaar-qaadq-cai update_balance \ + '(record { owner = opt principal "YOUR-PRINCIPAL"; subaccount = null })' \ + -e ic + +# Check ckBTC balance (amount in satoshis) +icp canister call mxzaz-hqaaa-aaaar-qaada-cai icrc1_balance_of \ + '(record { owner = principal "YOUR-PRINCIPAL"; subaccount = null })' \ + -e ic + +# Transfer ckBTC (10 satoshi fee) +icp canister call mxzaz-hqaaa-aaaar-qaada-cai icrc1_transfer \ + '(record { + to = record { owner = principal "RECIPIENT"; subaccount = null }; + amount = 100_000; + fee = opt 10; + memo = null; + from_subaccount = null; + created_at_time = null; + })' -e ic +``` + +### Common mistakes + +- **Not calling `update_balance` after a BTC deposit.** The minter does not auto-detect deposits. Your application must call `update_balance` to trigger minting. +- **Forgetting the 10 satoshi transfer fee.** If a user has exactly 1000 satoshis and you transfer 1000, it fails with `InsufficientFunds`. Transfer `balance - 10` instead. +- **Using AccountIdentifier instead of ICRC-1 Account.** ckBTC uses the ICRC-1 standard: `{ owner: Principal, subaccount: ?Blob }`. Do not use the legacy `AccountIdentifier` (hex string) from the ICP ledger. +- **Subaccount must be exactly 32 bytes or null.** A subaccount shorter or longer than 32 bytes causes a trap. +- **Minimum withdrawal is 50,000 satoshis.** Amounts below this return `AmountTooLow`. +- **Omitting `owner` in `get_btc_address`.** Without `owner`, the minter returns the deposit address of the calling canister instead of the intended user. + +For a complete working example with all type definitions and error handling, see the [ckBTC skill](https://skills.internetcomputer.org) or the full code in the [basic_bitcoin Motoko example](https://github.com/dfinity/examples/tree/master/motoko/basic_bitcoin) and [basic_bitcoin Rust example](https://github.com/dfinity/examples/tree/master/rust/basic_bitcoin). + +## Direct Bitcoin API + +For use cases that require full control over Bitcoin transactions (custom scripts, Ordinals, Runes, BRC-20), you can call the Bitcoin API directly through the management canister. This involves generating addresses with threshold ECDSA or Schnorr signatures, building raw transactions, and submitting them to the Bitcoin network. + +### Bitcoin API canister IDs + +| IC network | Bitcoin network | Canister ID | +|---|---|---| +| Local (PocketIC) | regtest | `g4xu7-jiaaa-aaaan-aaaaq-cai` | +| IC mainnet | testnet4 | `g4xu7-jiaaa-aaaan-aaaaq-cai` | +| IC mainnet | mainnet | `ghsi2-tqaaa-aaaan-aaaca-cai` | + +### Available endpoints + +The Bitcoin canister exposes these methods: + +- `bitcoin_get_balance` — returns the balance of a Bitcoin address in satoshis +- `bitcoin_get_utxos` — returns unspent transaction outputs for an address +- `bitcoin_get_current_fee_percentiles` — returns fee percentiles from recent transactions +- `bitcoin_get_block_headers` — returns raw block headers for a height range +- `bitcoin_send_transaction` — submits a signed transaction to the Bitcoin network + +The threshold signature system provides: + +- `ecdsa_public_key` / `sign_with_ecdsa` — for standard Bitcoin (P2PKH, P2SH) addresses +- `schnorr_public_key` / `sign_with_schnorr` — for Taproot (P2TR) addresses + +All Bitcoin API calls require cycles. The Rust CDK's `ic_cdk::bitcoin_canister` module handles cycle calculation and attachment automatically. + +### Read Bitcoin balance + + + + +```motoko +import Runtime "mo:core/Runtime"; +import Text "mo:core/Text"; + +persistent actor Backend { + public type Satoshi = Nat64; + public type BitcoinAddress = Text; + public type Network = { #mainnet; #testnet; #regtest }; + + type BitcoinCanister = actor { + bitcoin_get_balance : shared { + address : BitcoinAddress; + network : Network; + min_confirmations : ?Nat32; + } -> async Satoshi; + }; + + private func getNetwork() : Network { + switch (Runtime.envVar("BITCOIN_NETWORK")) { + case (?value) { + switch (Text.toLower(value)) { + case ("mainnet") #mainnet; + case ("testnet") #testnet; + case _ #regtest; + }; + }; + case null #regtest; + }; + }; + + private func getBitcoinCanisterId(network : Network) : Text { + switch (network) { + case (#mainnet) "ghsi2-tqaaa-aaaan-aaaca-cai"; + case _ "g4xu7-jiaaa-aaaan-aaaaq-cai"; + }; + }; + + private func getBalanceCost(network : Network) : Nat { + switch (network) { + case (#mainnet) 100_000_000; + case _ 40_000_000; + }; + }; + + public func get_balance(address : BitcoinAddress) : async Satoshi { + let network = getNetwork(); + await (with cycles = getBalanceCost(network)) + (actor (getBitcoinCanisterId(network)) : BitcoinCanister) + .bitcoin_get_balance({ + address; + network; + min_confirmations = null; + }); + }; +}; +``` + + + + +```rust +use ic_cdk::bitcoin_canister::{ + bitcoin_get_balance, GetBalanceRequest, Network, Satoshi, +}; + +fn get_network() -> Network { + let network_str = if ic_cdk::api::env_var_name_exists("BITCOIN_NETWORK") { + ic_cdk::api::env_var_value("BITCOIN_NETWORK").to_lowercase() + } else { + "regtest".to_string() + }; + + match network_str.as_str() { + "mainnet" => Network::Mainnet, + "testnet" => Network::Testnet, + _ => Network::Regtest, + } +} + +#[ic_cdk::update] +async fn get_balance(address: String) -> Satoshi { + bitcoin_get_balance(&GetBalanceRequest { + address, + network: get_network(), + min_confirmations: None, + }) + .await + .expect("Failed to get balance") +} +``` + + + + +### Developer workflow + +Building a full Bitcoin transaction flow involves these steps: + +1. **Generate a Bitcoin address** from a threshold ECDSA or Schnorr public key +2. **Read UTXOs** for the address using `bitcoin_get_utxos` +3. **Build the transaction** selecting UTXOs as inputs, calculating fees, and setting outputs +4. **Sign each input** using `sign_with_ecdsa` or `sign_with_schnorr` +5. **Submit the transaction** using `bitcoin_send_transaction` + +The complete implementation for all steps (address generation, transaction construction, signing, and submission) is more than 30 lines per language. See these working examples: + +- [basic_bitcoin (Motoko)](https://github.com/dfinity/examples/tree/master/motoko/basic_bitcoin) — full send/receive with ECDSA and Schnorr +- [basic_bitcoin (Rust)](https://github.com/dfinity/examples/tree/master/rust/basic_bitcoin) — full send/receive with ECDSA and Schnorr +- [threshold-ecdsa (Motoko)](https://github.com/dfinity/examples/tree/master/motoko/threshold-ecdsa) — ECDSA signing + +### Cycle costs + +All Bitcoin API calls require cycles attached to the call. The Rust CDK handles this automatically through the `ic_cdk::bitcoin_canister` module. In Motoko, attach cycles explicitly with `(with cycles = amount)`. + +| API call | Testnet / Regtest | Mainnet | +|---|---|---| +| `bitcoin_get_balance` | 40,000,000 | 100,000,000 | +| `bitcoin_get_utxos` | 4,000,000,000 | 10,000,000,000 | +| `bitcoin_send_transaction` | 2,000,000,000 | 5,000,000,000 | +| `bitcoin_get_current_fee_percentiles` | 40,000,000 | 100,000,000 | +| `bitcoin_get_block_headers` | 4,000,000,000 | 10,000,000,000 | + +## Development setup + +### Quick start with the bitcoin-starter template + +The fastest way to get started is with the bitcoin-starter template: + +```bash +icp new my-bitcoin-app --template bitcoin-starter +cd my-bitcoin-app +``` + +This sets up a project with multi-environment configuration already in place. + +### Local development with regtest + +For local testing, run a Bitcoin regtest node alongside your local ICP network. The `icp.yaml` configuration connects the two: + +```yaml +canisters: + - backend + +networks: + - name: local + mode: managed + bitcoind-addr: + - "127.0.0.1:18444" + +environments: + - name: local + network: local + settings: + backend: + environment_variables: + BITCOIN_NETWORK: "regtest" + + - name: staging + network: ic + settings: + backend: + environment_variables: + BITCOIN_NETWORK: "testnet" + + - name: production + network: ic + settings: + backend: + environment_variables: + BITCOIN_NETWORK: "mainnet" +``` + +Start the Bitcoin regtest node (using Docker): + +```bash +docker run -d --name bitcoind \ + -p 18443:18443 -p 18444:18444 \ + lncm/bitcoind:v27.2 \ + -regtest -server -rpcbind=0.0.0.0 -rpcallowip=0.0.0.0/0 \ + -rpcuser=ic-btc-integration -rpcpassword=ic-btc-integration \ + -fallbackfee=0.00001 -txindex=1 +``` + +Start the local ICP network and deploy: + +```bash +icp network start -d +icp deploy +``` + +### Test with regtest + +Create a wallet and mine some blocks: + +```bash +# Create a regtest wallet +docker exec bitcoind bitcoin-cli -regtest \ + -rpcuser=ic-btc-integration -rpcpassword=ic-btc-integration \ + createwallet "default" + +# Get a new address +ADDR=$(docker exec bitcoind bitcoin-cli -regtest \ + -rpcuser=ic-btc-integration -rpcpassword=ic-btc-integration \ + getnewaddress) + +# Mine a block (rewards 50 BTC = 5,000,000,000 satoshis) +docker exec bitcoind bitcoin-cli -regtest \ + -rpcuser=ic-btc-integration -rpcpassword=ic-btc-integration \ + generatetoaddress 1 "$ADDR" + +# Check balance through your canister +icp canister call backend get_balance "(\"$ADDR\")" +``` + +Coinbase rewards require 100 confirmations before they can be spent. If you extend this to send transactions, mine at least 101 blocks so the first block's reward becomes spendable. + +### Deploy to testnet and mainnet + +Deploy to testnet (Bitcoin testnet4 via the IC mainnet): + +```bash +icp deploy --env staging +``` + +Deploy to production (Bitcoin mainnet via the IC mainnet): + +```bash +icp deploy --env production +``` + +The `BITCOIN_NETWORK` environment variable controls which Bitcoin network and Bitcoin API canister your code targets, without requiring any code changes. + +### Cleanup + +```bash +icp network stop +docker stop bitcoind && docker rm bitcoind +``` + +## Next steps + +- [Chain fusion overview](../../concepts/chain-fusion.md) — understand how ICP integrates with external blockchains +- [Chain-key cryptography](../../concepts/chain-key-cryptography.md) — learn how threshold ECDSA and Schnorr signatures work +- [Chain-key tokens](../defi/chain-key-tokens.md) — explore ckBTC, ckETH, and other chain-key tokens +- [Ethereum integration](ethereum.md) — apply similar patterns for Ethereum +- [Management canister reference](../../reference/management-canister.md) — full API reference for `bitcoin_get_utxos`, `sign_with_ecdsa`, and other management canister methods +- [Bitcoin canister API specification](https://github.com/dfinity/bitcoin-canister/blob/master/INTERFACE_SPECIFICATION.md) — detailed API documentation +- [Bitcoin integration (Learn Hub)](https://learn.internetcomputer.org/hc/en-us/articles/34211154520084) — protocol-level details of how ICP connects to Bitcoin + +{/* Upstream: informed by dfinity/portal — docs/build-on-btc/*; dfinity/icskills — skills/ckbtc/SKILL.md; dfinity/icp-cli-templates — bitcoin-starter/ */} From 672b9c668250ad6bbfb426ca9aa3089f1cbbc27c Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 15 Apr 2026 13:46:42 +0200 Subject: [PATCH 2/3] fix: address PR feedback on Bitcoin integration guide --- docs/guides/chain-fusion/bitcoin.mdx | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/docs/guides/chain-fusion/bitcoin.mdx b/docs/guides/chain-fusion/bitcoin.mdx index 0e5de266..5c620686 100644 --- a/docs/guides/chain-fusion/bitcoin.mdx +++ b/docs/guides/chain-fusion/bitcoin.mdx @@ -27,6 +27,7 @@ ckBTC is the recommended path for most developers. The ckBTC minter canister hol | ckBTC Ledger | `mxzaz-hqaaa-aaaar-qaada-cai` | `mc6ru-gyaaa-aaaar-qaaaq-cai` | | ckBTC Minter | `mqygn-kiaaa-aaaar-qaadq-cai` | `ml52i-qqaaa-aaaar-qaaba-cai` | | ckBTC Index | `n5wcd-faaaa-aaaar-qaaea-cai` | `mm444-5iaaa-aaaar-qaabq-cai` | +| ckBTC Checker | `oltsj-fqaaa-aaaar-qal5q-cai` | — | ### Deposit flow (BTC to ckBTC) @@ -395,17 +396,17 @@ You can call the ckBTC canisters directly from the command line: # Get a BTC deposit address icp canister call mqygn-kiaaa-aaaar-qaadq-cai get_btc_address \ '(record { owner = opt principal "YOUR-PRINCIPAL"; subaccount = null })' \ - -e ic + -n ic # Check for new deposits and mint ckBTC icp canister call mqygn-kiaaa-aaaar-qaadq-cai update_balance \ '(record { owner = opt principal "YOUR-PRINCIPAL"; subaccount = null })' \ - -e ic + -n ic # Check ckBTC balance (amount in satoshis) icp canister call mxzaz-hqaaa-aaaar-qaada-cai icrc1_balance_of \ '(record { owner = principal "YOUR-PRINCIPAL"; subaccount = null })' \ - -e ic + -n ic # Transfer ckBTC (10 satoshi fee) icp canister call mxzaz-hqaaa-aaaar-qaada-cai icrc1_transfer \ @@ -416,7 +417,7 @@ icp canister call mxzaz-hqaaa-aaaar-qaada-cai icrc1_transfer \ memo = null; from_subaccount = null; created_at_time = null; - })' -e ic + })' -n ic ``` ### Common mistakes @@ -452,6 +453,13 @@ The Bitcoin canister exposes these methods: - `bitcoin_get_block_headers` — returns raw block headers for a height range - `bitcoin_send_transaction` — submits a signed transaction to the Bitcoin network +The Rust CDK's `ic_cdk::bitcoin_canister` module also provides `get_blockchain_info()` for querying blockchain state (current height, chain tip hash, etc.): + +```rust +// Query blockchain state (height, chain tip, etc.) +// let info = ic_cdk::bitcoin_canister::get_blockchain_info().await; +``` + The threshold signature system provides: - `ecdsa_public_key` / `sign_with_ecdsa` — for standard Bitcoin (P2PKH, P2SH) addresses @@ -481,6 +489,7 @@ persistent actor Backend { } -> async Satoshi; }; + // capability is required to access Runtime.envVar (reads environment variables at runtime) private func getNetwork() : Network { switch (Runtime.envVar("BITCOIN_NETWORK")) { case (?value) { @@ -593,7 +602,7 @@ All Bitcoin API calls require cycles attached to the call. The Rust CDK handles The fastest way to get started is with the bitcoin-starter template: ```bash -icp new my-bitcoin-app --template bitcoin-starter +icp new my-bitcoin-app --subfolder bitcoin-starter cd my-bitcoin-app ``` @@ -685,13 +694,13 @@ Coinbase rewards require 100 confirmations before they can be spent. If you exte Deploy to testnet (Bitcoin testnet4 via the IC mainnet): ```bash -icp deploy --env staging +icp deploy -e staging ``` Deploy to production (Bitcoin mainnet via the IC mainnet): ```bash -icp deploy --env production +icp deploy -e production ``` The `BITCOIN_NETWORK` environment variable controls which Bitcoin network and Bitcoin API canister your code targets, without requiring any code changes. From b56b35ba514fe2c57a15e04059babed5f569cfe9 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 15 Apr 2026 16:21:13 +0200 Subject: [PATCH 3/3] docs(bitcoin): update to ic-cdk-bitcoin-canister crate, fix costs and links - Replace ic_cdk::bitcoin_canister imports with ic_cdk_bitcoin_canister (new 0.2 crate) - Uncomment and fix get_blockchain_info example using the new crate - Add bitcoin_send_transaction per-byte cycle costs (8M/20M per byte for testnet/mainnet) - Fix ckBTC skill URL to direct deep link - Add inline comment explaining amount+10 in Rust withdraw function - Update Upstream comment to include cdk-rs and portal cost reference pages --- docs/guides/chain-fusion/bitcoin.mdx | 33 +++++++++++++++++++--------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/docs/guides/chain-fusion/bitcoin.mdx b/docs/guides/chain-fusion/bitcoin.mdx index 5c620686..c78bd9b5 100644 --- a/docs/guides/chain-fusion/bitcoin.mdx +++ b/docs/guides/chain-fusion/bitcoin.mdx @@ -343,7 +343,7 @@ async fn withdraw(btc_address: String, amount: u64) -> RetrieveBtcResult { let approve_args = ApproveArgs { from_subaccount: Some(from_subaccount), spender: Account { owner: minter_id(), subaccount: None }, - amount: Nat::from(amount) + Nat::from(10u64), + amount: Nat::from(amount) + Nat::from(10u64), // +10 covers the ICRC-2 transfer fee the minter charges when moving your ckBTC expected_allowance: None, expires_at: None, fee: Some(Nat::from(10u64)), @@ -429,7 +429,7 @@ icp canister call mxzaz-hqaaa-aaaar-qaada-cai icrc1_transfer \ - **Minimum withdrawal is 50,000 satoshis.** Amounts below this return `AmountTooLow`. - **Omitting `owner` in `get_btc_address`.** Without `owner`, the minter returns the deposit address of the calling canister instead of the intended user. -For a complete working example with all type definitions and error handling, see the [ckBTC skill](https://skills.internetcomputer.org) or the full code in the [basic_bitcoin Motoko example](https://github.com/dfinity/examples/tree/master/motoko/basic_bitcoin) and [basic_bitcoin Rust example](https://github.com/dfinity/examples/tree/master/rust/basic_bitcoin). +For a complete working example with all type definitions and error handling, see the [ckBTC skill](https://skills.internetcomputer.org/skills/ckbtc/) or the full code in the [basic_bitcoin Motoko example](https://github.com/dfinity/examples/tree/master/motoko/basic_bitcoin) and [basic_bitcoin Rust example](https://github.com/dfinity/examples/tree/master/rust/basic_bitcoin). ## Direct Bitcoin API @@ -453,11 +453,23 @@ The Bitcoin canister exposes these methods: - `bitcoin_get_block_headers` — returns raw block headers for a height range - `bitcoin_send_transaction` — submits a signed transaction to the Bitcoin network -The Rust CDK's `ic_cdk::bitcoin_canister` module also provides `get_blockchain_info()` for querying blockchain state (current height, chain tip hash, etc.): +The `ic-cdk-bitcoin-canister` crate provides `get_blockchain_info()` for querying blockchain state (current height, chain tip hash, timestamp, etc.): ```rust -// Query blockchain state (height, chain tip, etc.) -// let info = ic_cdk::bitcoin_canister::get_blockchain_info().await; +use ic_cdk_bitcoin_canister::get_blockchain_info; + +let info = get_blockchain_info(get_network()) + .await + .expect("Failed to get blockchain info"); +// info.height — current chain height +// info.block_hash — chain tip block hash (hex) +// info.timestamp — tip block timestamp +``` + +Add to `Cargo.toml` alongside `ic-cdk`: + +```toml +ic-cdk-bitcoin-canister = "0.2" ``` The threshold signature system provides: @@ -465,7 +477,7 @@ The threshold signature system provides: - `ecdsa_public_key` / `sign_with_ecdsa` — for standard Bitcoin (P2PKH, P2SH) addresses - `schnorr_public_key` / `sign_with_schnorr` — for Taproot (P2TR) addresses -All Bitcoin API calls require cycles. The Rust CDK's `ic_cdk::bitcoin_canister` module handles cycle calculation and attachment automatically. +All Bitcoin API calls require cycles. The `ic-cdk-bitcoin-canister` crate handles cycle calculation and attachment automatically. ### Read Bitcoin balance @@ -534,7 +546,7 @@ persistent actor Backend { ```rust -use ic_cdk::bitcoin_canister::{ +use ic_cdk_bitcoin_canister::{ bitcoin_get_balance, GetBalanceRequest, Network, Satoshi, }; @@ -585,13 +597,14 @@ The complete implementation for all steps (address generation, transaction const ### Cycle costs -All Bitcoin API calls require cycles attached to the call. The Rust CDK handles this automatically through the `ic_cdk::bitcoin_canister` module. In Motoko, attach cycles explicitly with `(with cycles = amount)`. +All Bitcoin API calls require cycles attached to the call. In Rust, the `ic-cdk-bitcoin-canister` crate handles this automatically. In Motoko, attach cycles explicitly with `(with cycles = amount)`. | API call | Testnet / Regtest | Mainnet | |---|---|---| | `bitcoin_get_balance` | 40,000,000 | 100,000,000 | | `bitcoin_get_utxos` | 4,000,000,000 | 10,000,000,000 | -| `bitcoin_send_transaction` | 2,000,000,000 | 5,000,000,000 | +| `bitcoin_send_transaction` (base) | 2,000,000,000 | 5,000,000,000 | +| `bitcoin_send_transaction` (per byte) | 8,000,000 | 20,000,000 | | `bitcoin_get_current_fee_percentiles` | 40,000,000 | 100,000,000 | | `bitcoin_get_block_headers` | 4,000,000,000 | 10,000,000,000 | @@ -722,4 +735,4 @@ docker stop bitcoind && docker rm bitcoind - [Bitcoin canister API specification](https://github.com/dfinity/bitcoin-canister/blob/master/INTERFACE_SPECIFICATION.md) — detailed API documentation - [Bitcoin integration (Learn Hub)](https://learn.internetcomputer.org/hc/en-us/articles/34211154520084) — protocol-level details of how ICP connects to Bitcoin -{/* Upstream: informed by dfinity/portal — docs/build-on-btc/*; dfinity/icskills — skills/ckbtc/SKILL.md; dfinity/icp-cli-templates — bitcoin-starter/ */} +{/* Upstream: informed by dfinity/portal — docs/build-on-btc/*, docs/references/bitcoin-how-it-works.mdx, docs/references/cycles-cost-formulas.mdx; dfinity/icskills — skills/ckbtc/SKILL.md; dfinity/icp-cli-templates — bitcoin-starter/; dfinity/cdk-rs — ic-cdk-bitcoin-canister 0.2 */}