Skip to content

Commit e8f4e51

Browse files
authored
docs(bitcoin): improve CLI UX, fix heading structure, reorder Direct API sections (#170)
## Summary ### CLI copy-paste UX The `### Interact with ckBTC using icp-cli` section previously had a single monolithic code block with `YOUR-PRINCIPAL` literals that required manual editing before every command. The section now: - Exports `MY_PRINCIPAL` from the active identity in one setup block (`icp identity get-principal`) so it never needs to be typed or edited. - Splits each operation into its own copy-paste block with `$MY_PRINCIPAL` interpolated via double-quote shell expansion. - The transfer command includes `export RECIPIENT="<paste-recipient-principal>"` at the top of the same block, keeping it a single copy-paste operation while making the placeholder explicit. ### Direct Bitcoin API section reordering Previous order mixed monitoring utilities in with core read operations and placed `### Blockchain info` immediately after `### Available endpoints`, before the developer's first instinct (`get_balance`, `get_utxos`). New order groups sections by purpose: | Before | After | |---|---| | Bitcoin API canister IDs | Bitcoin API canister IDs | | Available endpoints | Available endpoints | | **Blockchain info** | **Read Bitcoin balance** | | Read Bitcoin balance | **Read UTXOs** | | Read UTXOs | **Get fee percentiles** | | Get fee percentiles | **Blockchain info** | | Developer workflow | Developer workflow | | (UTXO selection as h4) | **UTXO selection (h3)** | | Common mistakes | Common mistakes | | Cycle costs | Cycle costs | `Blockchain info` is a monitoring/diagnostics call — it belongs after the core transaction-building reads, not before them. ### Developer workflow heading structure `#### UTXO selection` was the only h4 in the entire file, sitting as a lone child under `### Developer workflow`. This: - Created a structurally awkward section (a heading with a single subheading implies the parent section exists only to contain one thing). - Buried the UTXO selection anchor below the fold of the workflow section. The fix promotes `#### UTXO selection` to `### UTXO selection` as a sibling of `### Developer workflow`. The `### Developer workflow` section now closes with the working-example links (which cover the full flow, not just UTXO selection), and step 3 in the numbered list links directly to `#utxo-selection` for readers who want to jump there. ### Em-dash cleanup The previous PR (#169) introduced several em-dashes in new prose. All instances outside code blocks are replaced with colons, semicolons, or parentheses per the project's banned-terms rule. ## Sync recommendation Informed by `dfinity/examples` — no source changes, documentation improvements only.
1 parent 3789998 commit e8f4e51

1 file changed

Lines changed: 148 additions & 133 deletions

File tree

docs/guides/chain-fusion/bitcoin.mdx

Lines changed: 148 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -390,34 +390,49 @@ async fn withdraw(btc_address: String, amount: u64) -> RetrieveBtcResult {
390390

391391
### Interact with ckBTC using icp-cli
392392

393-
You can call the ckBTC canisters directly from the command line:
393+
First, export your principal from your active identity (every command below reuses it):
394+
395+
```bash
396+
export MY_PRINCIPAL=$(icp identity principal)
397+
```
398+
399+
Get a deposit address:
394400

395401
```bash
396-
# Get a BTC deposit address
397402
icp canister call mqygn-kiaaa-aaaar-qaadq-cai get_btc_address \
398-
'(record { owner = opt principal "YOUR-PRINCIPAL"; subaccount = null })' \
403+
"(record { owner = opt principal \"$MY_PRINCIPAL\"; subaccount = null })" \
399404
-n ic
405+
```
406+
407+
Check for new deposits and mint ckBTC:
400408

401-
# Check for new deposits and mint ckBTC
409+
```bash
402410
icp canister call mqygn-kiaaa-aaaar-qaadq-cai update_balance \
403-
'(record { owner = opt principal "YOUR-PRINCIPAL"; subaccount = null })' \
411+
"(record { owner = opt principal \"$MY_PRINCIPAL\"; subaccount = null })" \
404412
-n ic
413+
```
405414

406-
# Check ckBTC balance (amount in satoshis)
415+
Check ckBTC balance (amount in satoshis):
416+
417+
```bash
407418
icp canister call mxzaz-hqaaa-aaaar-qaada-cai icrc1_balance_of \
408-
'(record { owner = principal "YOUR-PRINCIPAL"; subaccount = null })' \
419+
"(record { owner = principal \"$MY_PRINCIPAL\"; subaccount = null })" \
409420
-n ic
421+
```
422+
423+
Transfer ckBTC (10 satoshi fee, amount in satoshis):
410424

411-
# Transfer ckBTC (10 satoshi fee)
425+
```bash
426+
export RECIPIENT="<paste-recipient-principal>"
412427
icp canister call mxzaz-hqaaa-aaaar-qaada-cai icrc1_transfer \
413-
'(record {
414-
to = record { owner = principal "RECIPIENT"; subaccount = null };
428+
"(record {
429+
to = record { owner = principal \"$RECIPIENT\"; subaccount = null };
415430
amount = 100_000;
416431
fee = opt 10;
417432
memo = null;
418433
from_subaccount = null;
419434
created_at_time = null;
420-
})' -n ic
435+
})" -n ic
421436
```
422437

423438
### Common mistakes
@@ -459,117 +474,7 @@ Signing uses threshold key derivation provided by the management canister:
459474
- `ecdsa_public_key` / `sign_with_ecdsa`: P2PKH, P2SH, and P2WPKH addresses
460475
- `schnorr_public_key` / `sign_with_schnorr`: Taproot (P2TR) addresses
461476

462-
All calls require cycles — see [Cycle costs](#cycle-costs). The `ic-cdk-bitcoin-canister` crate handles them automatically in Rust; in Motoko attach cycles explicitly with `(with cycles = amount)`.
463-
464-
### Blockchain info
465-
466-
`get_blockchain_info()` queries the state of the Bitcoin chain. The response includes:
467-
468-
| Field | Type | Description |
469-
|---|---|---|
470-
| `height` | `nat32` | Current chain tip block height |
471-
| `block_hash` | `blob` | Chain tip block hash |
472-
| `timestamp` | `nat32` | Unix timestamp of the tip block |
473-
| `difficulty` | `nat` | Current mining difficulty target |
474-
| `utxos_length` | `nat64` | Total number of UTXOs in the UTXO set |
475-
476-
<Tabs syncKey="lang">
477-
<TabItem label="Motoko">
478-
479-
```motoko
480-
import Runtime "mo:core/Runtime";
481-
import Text "mo:core/Text";
482-
483-
persistent actor Backend {
484-
public type Network = { #mainnet; #testnet; #regtest };
485-
486-
type BlockchainInfo = {
487-
height : Nat32;
488-
block_hash : Blob;
489-
timestamp : Nat32;
490-
difficulty : Nat;
491-
utxos_length : Nat64;
492-
};
493-
494-
type BitcoinCanister = actor {
495-
get_blockchain_info : shared () -> async BlockchainInfo;
496-
};
497-
498-
// <system> capability is required to access Runtime.envVar (reads environment variables at runtime)
499-
private func getNetwork<system>() : Network {
500-
switch (Runtime.envVar("BITCOIN_NETWORK")) {
501-
case (?value) {
502-
switch (Text.toLower(value)) {
503-
case ("mainnet") #mainnet;
504-
case ("testnet") #testnet;
505-
case _ #regtest;
506-
};
507-
};
508-
case null #regtest;
509-
};
510-
};
511-
512-
private func getBitcoinCanisterId(network : Network) : Text {
513-
switch (network) {
514-
case (#mainnet) "ghsi2-tqaaa-aaaan-aaaca-cai";
515-
case _ "g4xu7-jiaaa-aaaan-aaaaq-cai";
516-
};
517-
};
518-
519-
private func getBlockchainInfoCost(network : Network) : Nat {
520-
switch (network) {
521-
case (#mainnet) 100_000_000;
522-
case _ 40_000_000;
523-
};
524-
};
525-
526-
public func get_blockchain_info() : async BlockchainInfo {
527-
let network = getNetwork();
528-
await (with cycles = getBlockchainInfoCost(network))
529-
(actor (getBitcoinCanisterId(network)) : BitcoinCanister)
530-
.get_blockchain_info();
531-
};
532-
};
533-
```
534-
535-
</TabItem>
536-
<TabItem label="Rust">
537-
538-
```rust
539-
use ic_cdk_bitcoin_canister::{get_blockchain_info, BlockchainInfo, Network};
540-
541-
fn get_network() -> Network {
542-
let network_str = if ic_cdk::api::env_var_name_exists("BITCOIN_NETWORK") {
543-
ic_cdk::api::env_var_value("BITCOIN_NETWORK").to_lowercase()
544-
} else {
545-
"regtest".to_string()
546-
};
547-
548-
match network_str.as_str() {
549-
"mainnet" => Network::Mainnet,
550-
"testnet" => Network::Testnet,
551-
_ => Network::Regtest,
552-
}
553-
}
554-
555-
#[ic_cdk::update]
556-
async fn get_blockchain_info_handler() -> BlockchainInfo {
557-
get_blockchain_info(get_network())
558-
.await
559-
.expect("Failed to get blockchain info")
560-
}
561-
```
562-
563-
Add to `Cargo.toml` alongside `ic-cdk`:
564-
565-
```toml
566-
ic-cdk-bitcoin-canister = "0.2"
567-
```
568-
569-
</TabItem>
570-
</Tabs>
571-
572-
Full implementation: [basic_bitcoin example (Rust)](https://github.com/dfinity/examples/tree/master/rust/basic_bitcoin)`src/service/get_blockchain_info.rs`.
477+
All calls require cycles (see [Cycle costs](#cycle-costs)). The `ic-cdk-bitcoin-canister` crate handles them automatically in Rust; in Motoko attach cycles explicitly with `(with cycles = amount)`.
573478

574479
### Read Bitcoin balance
575480

@@ -763,11 +668,11 @@ async fn get_utxos(address: String) -> GetUtxosResponse {
763668
</TabItem>
764669
</Tabs>
765670

766-
`bitcoin_get_utxos` returns a `next_page` field. If non-null, the address has more UTXOs than fit in one response call again with `filter = ?#page(next_page)` (Motoko) or `filter: Some(UtxosFilter::Page(next_page))` (Rust) until `next_page` is null.
671+
`bitcoin_get_utxos` returns a `next_page` field. If non-null, the address has more UTXOs than fit in one response: call again with `filter = ?#page(next_page)` (Motoko) or `filter: Some(UtxosFilter::Page(next_page))` (Rust) until `next_page` is null.
767672

768673
### Get fee percentiles
769674

770-
Fee percentiles are measured in millisatoshi per vbyte (1,000 msat = 1 satoshi). The 50th percentile gives a reasonable median confirmation target. On regtest there are no transactions, so the response is empty use a fallback.
675+
Fee percentiles are measured in millisatoshi per vbyte (1,000 msat = 1 satoshi). The 50th percentile gives a reasonable median confirmation target. On regtest there are no transactions, so the response is empty: use a fallback.
771676

772677
<Tabs syncKey="lang">
773678
<TabItem label="Motoko">
@@ -853,26 +758,142 @@ async fn get_fee_per_byte(network: Network) -> MillisatoshiPerByte {
853758
</TabItem>
854759
</Tabs>
855760

761+
### Blockchain info
762+
763+
`get_blockchain_info()` queries the state of the Bitcoin chain. The response includes:
764+
765+
| Field | Type | Description |
766+
|---|---|---|
767+
| `height` | `nat32` | Current chain tip block height |
768+
| `block_hash` | `blob` | Chain tip block hash |
769+
| `timestamp` | `nat32` | Unix timestamp of the tip block |
770+
| `difficulty` | `nat` | Current mining difficulty target |
771+
| `utxos_length` | `nat64` | Total number of UTXOs in the UTXO set |
772+
773+
<Tabs syncKey="lang">
774+
<TabItem label="Motoko">
775+
776+
```motoko
777+
import Runtime "mo:core/Runtime";
778+
import Text "mo:core/Text";
779+
780+
persistent actor Backend {
781+
public type Network = { #mainnet; #testnet; #regtest };
782+
783+
type BlockchainInfo = {
784+
height : Nat32;
785+
block_hash : Blob;
786+
timestamp : Nat32;
787+
difficulty : Nat;
788+
utxos_length : Nat64;
789+
};
790+
791+
type BitcoinCanister = actor {
792+
get_blockchain_info : shared () -> async BlockchainInfo;
793+
};
794+
795+
// <system> capability is required to access Runtime.envVar (reads environment variables at runtime)
796+
private func getNetwork<system>() : Network {
797+
switch (Runtime.envVar("BITCOIN_NETWORK")) {
798+
case (?value) {
799+
switch (Text.toLower(value)) {
800+
case ("mainnet") #mainnet;
801+
case ("testnet") #testnet;
802+
case _ #regtest;
803+
};
804+
};
805+
case null #regtest;
806+
};
807+
};
808+
809+
private func getBitcoinCanisterId(network : Network) : Text {
810+
switch (network) {
811+
case (#mainnet) "ghsi2-tqaaa-aaaan-aaaca-cai";
812+
case _ "g4xu7-jiaaa-aaaan-aaaaq-cai";
813+
};
814+
};
815+
816+
private func getBlockchainInfoCost(network : Network) : Nat {
817+
switch (network) {
818+
case (#mainnet) 100_000_000;
819+
case _ 40_000_000;
820+
};
821+
};
822+
823+
public func get_blockchain_info() : async BlockchainInfo {
824+
let network = getNetwork();
825+
await (with cycles = getBlockchainInfoCost(network))
826+
(actor (getBitcoinCanisterId(network)) : BitcoinCanister)
827+
.get_blockchain_info();
828+
};
829+
};
830+
```
831+
832+
</TabItem>
833+
<TabItem label="Rust">
834+
835+
```rust
836+
use ic_cdk_bitcoin_canister::{get_blockchain_info, BlockchainInfo, Network};
837+
838+
fn get_network() -> Network {
839+
let network_str = if ic_cdk::api::env_var_name_exists("BITCOIN_NETWORK") {
840+
ic_cdk::api::env_var_value("BITCOIN_NETWORK").to_lowercase()
841+
} else {
842+
"regtest".to_string()
843+
};
844+
845+
match network_str.as_str() {
846+
"mainnet" => Network::Mainnet,
847+
"testnet" => Network::Testnet,
848+
_ => Network::Regtest,
849+
}
850+
}
851+
852+
#[ic_cdk::update]
853+
async fn get_blockchain_info_handler() -> BlockchainInfo {
854+
get_blockchain_info(get_network())
855+
.await
856+
.expect("Failed to get blockchain info")
857+
}
858+
```
859+
860+
Add to `Cargo.toml` alongside `ic-cdk`:
861+
862+
```toml
863+
ic-cdk-bitcoin-canister = "0.2"
864+
```
865+
866+
</TabItem>
867+
</Tabs>
868+
869+
Full implementation: [basic_bitcoin example (Rust)](https://github.com/dfinity/examples/tree/master/rust/basic_bitcoin), `src/service/get_blockchain_info.rs`.
870+
856871
### Developer workflow
857872

858873
Building a full Bitcoin transaction flow involves these steps:
859874

860875
1. **Generate a Bitcoin address** from a threshold ECDSA or Schnorr public key
861876
2. **Read UTXOs** for the address using `bitcoin_get_utxos`
862-
3. **Select UTXOs and calculate the fee** see below
877+
3. **Select UTXOs and calculate the fee** (see [UTXO selection](#utxo-selection) below)
863878
4. **Build the unsigned transaction** from the selected UTXOs, recipient output, and change output
864879
5. **Sign each input** using `sign_with_ecdsa` or `sign_with_schnorr`
865880
6. **Submit the transaction** using `bitcoin_send_transaction`
866881

867-
#### UTXO selection
882+
Address generation, transaction construction, signing, and submission together exceed 30 lines per language. See these working examples for the full flow:
883+
884+
- [basic_bitcoin (Motoko)](https://github.com/dfinity/examples/tree/master/motoko/basic_bitcoin): full send/receive with ECDSA and Schnorr
885+
- [basic_bitcoin (Rust)](https://github.com/dfinity/examples/tree/master/rust/basic_bitcoin): full send/receive with ECDSA and Schnorr
886+
- [threshold-ecdsa (Motoko)](https://github.com/dfinity/examples/tree/master/motoko/threshold-ecdsa): ECDSA signing
887+
888+
### UTXO selection
868889

869890
Transaction fee depends on transaction size in bytes, which depends on the number of inputs, which depends on which UTXOs are selected. Because fee and input count are mutually dependent, the calculation is iterative: start with fee=0, select UTXOs to cover `amount + 0`, estimate the signed transaction size with a mock signer, recalculate fee, repeat until the fee stabilises.
870891

871892
Two selection strategies cover the common cases:
872893

873894
**Greedy (standard payments):** accumulate UTXOs oldest-first until the total covers `amount + fee`. Consolidates old UTXOs and reduces wallet fragmentation over time.
874895

875-
**Single UTXO (Ordinals, Runes, BRC-20):** find the first UTXO that alone covers `amount + fee`. Required when the asset is inscribed on a specific satoshi spending multiple UTXOs risks accidentally burning the inscription.
896+
**Single UTXO (Ordinals, Runes, BRC-20):** find the first UTXO that alone covers `amount + fee`. Required when the asset is inscribed on a specific satoshi; spending multiple UTXOs risks accidentally burning the inscription.
876897

877898
<Tabs syncKey="lang">
878899
<TabItem label="Motoko">
@@ -954,12 +975,6 @@ fn select_one_utxo<'a>(
954975
</TabItem>
955976
</Tabs>
956977

957-
Address generation, transaction construction, signing, and submission together exceed 30 lines per language. See these working examples for the full flow:
958-
959-
- [basic_bitcoin (Motoko)](https://github.com/dfinity/examples/tree/master/motoko/basic_bitcoin): full send/receive with ECDSA and Schnorr
960-
- [basic_bitcoin (Rust)](https://github.com/dfinity/examples/tree/master/rust/basic_bitcoin): full send/receive with ECDSA and Schnorr
961-
- [threshold-ecdsa (Motoko)](https://github.com/dfinity/examples/tree/master/motoko/threshold-ecdsa): ECDSA signing
962-
963978
### Common mistakes
964979

965980
- **Not paginating `bitcoin_get_utxos`.** The response includes a `next_page` field. For addresses with many transactions a single call may not return all UTXOs. If `next_page` is non-null, call again with `filter = ?#page(next_page)` (Motoko) or `UtxosFilter::Page(next_page)` (Rust) until `next_page` is null.

0 commit comments

Comments
 (0)