Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ icp new <project-name> --subfolder <template-name>

| Template | Description |
| --- | --- |
| [hello-world](./hello-world/) | Full-stack dapp with a frontend and backend canister (Rust or Motoko) |
| [motoko](./motoko/) | A basic Motoko canister |
| [rust](./rust/) | A basic Rust canister |
| [hello-world](./hello-world/) | Full-stack dapp with a frontend and backend canister (Rust or Motoko) |
| [static-website](./static-website/) | A static website deployed to an asset canister |
| [bitcoin-starter](./bitcoin-starter/) | Bitcoin integration with balance reading (Rust or Motoko) |
| [proxy](./proxy/) | A pre-built proxy canister for use with `icp canister call --proxy` |
| [static-website](./static-website/) | A static website deployed to an asset canister |

## Contributing

Expand Down
17 changes: 17 additions & 0 deletions bitcoin-starter/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Various IDEs and Editors
.vscode/
.idea/
**/*~

# Mac OSX temporary files
.DS_Store
**/.DS_Store

# environment variables
.env

# rust
target/

# icp-cli
.icp/cache/
114 changes: 114 additions & 0 deletions bitcoin-starter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Bitcoin Starter Template

Demonstrates reading Bitcoin balance from a canister on the Internet Computer using the [Bitcoin canister API](https://github.com/dfinity/bitcoin-canister/blob/master/INTERFACE_SPECIFICATION.md).

## Bitcoin Canister IDs

| IC Network | Bitcoin Network | Bitcoin Canister ID |
|------------|----------------|---------------------|
| Local (PocketIC) | regtest | `g4xu7-jiaaa-aaaan-aaaaq-cai` |
| IC mainnet | testnet | `g4xu7-jiaaa-aaaan-aaaaq-cai` |
| IC mainnet | mainnet | `ghsi2-tqaaa-aaaan-aaaca-cai` |

The `BITCOIN_NETWORK` environment variable controls which network and canister to use. It is configured per environment in `icp.yaml`.

## Prerequisites

- [icp-cli](https://github.com/dfinity/icp-cli) installed
- [Docker](https://docs.docker.com/get-docker/) installed and running (optional, but recommended)

> **Note:** Docker is used in this guide to run `bitcoind` for simplicity. If you prefer, you can install and run `bitcoind` natively instead — just make sure it is listening on the same ports (`18443` for RPC, `18444` for P2P) with the same credentials.

## Getting Started

Start a Bitcoin regtest node:

```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 IC network and deploy:

```bash
icp network start -d
icp deploy
```

## Usage

Verify the configured network and Bitcoin canister ID:

```bash
icp canister call backend get_config '()'
```

Create a wallet and get a Bitcoin address:

```bash
docker exec bitcoind bitcoin-cli -regtest \
-rpcuser=ic-btc-integration -rpcpassword=ic-btc-integration \
createwallet "default"

ADDR=$(docker exec bitcoind bitcoin-cli -regtest \
-rpcuser=ic-btc-integration -rpcpassword=ic-btc-integration \
getnewaddress)
```

Check the balance (should be 0):

```bash
icp canister call backend get_balance "(\"$ADDR\")"
```

Mine a block to the address (each block rewards 50 BTC):

```bash
docker exec bitcoind bitcoin-cli -regtest \
-rpcuser=ic-btc-integration -rpcpassword=ic-btc-integration \
generatetoaddress 1 "$ADDR"
```

Check the balance again (should be 5,000,000,000 satoshis = 50 BTC):

> **Note:** Coinbase rewards require 100 confirmations before they can be spent. If you extend this example to send transactions, mine at least 101 blocks so the first block's reward becomes spendable.

```bash
icp canister call backend get_balance "(\"$ADDR\")"
```

## Cleanup

```bash
icp network stop
docker stop bitcoind && docker rm bitcoind
```

## Environments

| Environment | IC Network | Bitcoin Network | Usage |
|-------------|-----------|----------------|-------|
| `local` | Local (PocketIC) | regtest | `icp deploy` |
| `staging` | IC mainnet | testnet | `icp deploy --env staging` |
| `production` | IC mainnet | mainnet | `icp deploy --env production` |

## Cycle Costs

Bitcoin canister API calls require cycles. The canister must attach cycles when calling the Bitcoin canister — the Rust CDK handles this automatically, while the Motoko backend attaches them explicitly via `(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 |

See [Bitcoin API costs](https://docs.internetcomputer.org/references/bitcoin-how-it-works#api-fees-and-pricing) for the full reference.

## Learn More

- [Bitcoin Canister API Specification](https://github.com/dfinity/bitcoin-canister/blob/master/INTERFACE_SPECIFICATION.md) — full API reference (get_utxos, send_transaction, fee percentiles, etc.)
- [Developer Docs](https://docs.internetcomputer.org/build-on-btc) - How to build on Bitcoin
15 changes: 15 additions & 0 deletions bitcoin-starter/cargo-generate.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[template]
description = "ICP project with local Bitcoin integration"

[placeholders]
backend_type = { type = "string", prompt = "Choose your backend language:", choices = ["rust", "motoko"], default = "rust" }
network_type = { type = "string", prompt = "Use the default network or a dockerized one?", choices = ["Default", "Docker"], default = "Default" }

[conditional.'backend_type == "rust"']
ignore = [ "motoko-backend" ]

[conditional.'backend_type == "motoko"']
ignore = [ "rust-backend" ]

[hooks]
pre = ["rename-backend-dir.rhai"]
42 changes: 42 additions & 0 deletions bitcoin-starter/icp.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# yaml-language-server: $schema=https://github.com/dfinity/icp-cli/raw/refs/tags/v0.2.0/docs/schemas/icp-yaml-schema.json

canisters:
- backend

{% if network_type == "Docker" -%}
networks:
- name: local
mode: managed
image: ghcr.io/dfinity/icp-cli-network-launcher
port-mapping:
- 0:4943
args:
- "--bitcoind-addr=host.docker.internal:18444"
{% else -%}
networks:
- name: local
mode: managed
bitcoind-addr:
- "127.0.0.1:18444"
{% endif %}
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"
2 changes: 2 additions & 0 deletions bitcoin-starter/motoko-backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.vessel
.mops
13 changes: 13 additions & 0 deletions bitcoin-starter/motoko-backend/backend.did
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
type satoshi = nat64;

type bitcoin_address = text;

type bitcoin_config = record {
network : text;
bitcoin_canister_id : text;
};

service : {
"get_balance" : (bitcoin_address) -> (satoshi);
"get_config" : () -> (bitcoin_config) query;
}
8 changes: 8 additions & 0 deletions bitcoin-starter/motoko-backend/canister.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# yaml-language-server: $schema=https://github.com/dfinity/icp-cli/raw/refs/tags/v0.2.0/docs/schemas/canister-yaml-schema.json

name: backend
recipe:
type: "@dfinity/motoko@v4.1.0"
configuration:
main: src/main.mo
candid: backend.did
5 changes: 5 additions & 0 deletions bitcoin-starter/motoko-backend/mops.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[dependencies]
core = "2.1.0"

[toolchain]
moc = "1.3.0"
88 changes: 88 additions & 0 deletions bitcoin-starter/motoko-backend/src/main.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/// Minimal Bitcoin integration example for the Internet Computer.
///
/// Demonstrates reading Bitcoin balance via the Bitcoin canister API.

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;
};

public type BitcoinConfig = {
network : Text;
bitcoin_canister_id : Text;
};

type BitcoinCanister = actor {
bitcoin_get_balance : shared {
address : BitcoinAddress;
network : Network;
min_confirmations : ?Nat32;
} -> async Satoshi;
};

// The BITCOIN_NETWORK env var (and thus the targeted Bitcoin network)
// can be changed at runtime without redeploying the canister.
private func getNetwork<system>() : 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";
};
};

// Minimum cycles required for bitcoin_get_balance
// (100M for mainnet, 40M for testnet/regtest).
// See https://docs.internetcomputer.org/references/bitcoin-how-it-works#api-fees-and-pricing
private func getBalanceCost(network : Network) : Nat {
switch (network) {
case (#mainnet) 100_000_000;
case _ 40_000_000;
};
};

/// Get the balance of a Bitcoin address in satoshis.
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;
});
};

/// Get the canister's Bitcoin configuration.
// Note: Runtime.envVar requires `system` capability which is not
// available in Motoko query functions, so we use a cached value here.
transient let cachedNetwork : Network = getNetwork();

public query func get_config() : async BitcoinConfig {
{
network = switch (cachedNetwork) {
case (#mainnet) "mainnet";
case (#testnet) "testnet";
case (#regtest) "regtest";
};
bitcoin_canister_id = getBitcoinCanisterId(cachedNetwork);
};
};
};
15 changes: 15 additions & 0 deletions bitcoin-starter/rename-backend-dir.rhai
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// This script renames the `<language>-backend` directory to `backend`
// By the time this script is called the conditionals would have filtered
// all the unselected backend folders

let backend_type = variable::get("backend_type");
debug(`backend_type: ${backend_type}`);

switch backend_type {
"rust" => {
file::rename("rust-backend", "backend");
}
"motoko" => {
file::rename("motoko-backend", "backend");
}
}
2 changes: 2 additions & 0 deletions bitcoin-starter/rust-backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target
Cargo.lock
11 changes: 11 additions & 0 deletions bitcoin-starter/rust-backend/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "backend"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
candid = "0.10"
ic-cdk = "0.19"
13 changes: 13 additions & 0 deletions bitcoin-starter/rust-backend/backend.did
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
type satoshi = nat64;

type bitcoin_address = text;

type bitcoin_config = record {
network : text;
bitcoin_canister_id : text;
};

service : {
"get_balance" : (bitcoin_address) -> (satoshi);
"get_config" : () -> (bitcoin_config) query;
}
12 changes: 12 additions & 0 deletions bitcoin-starter/rust-backend/canister.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# yaml-language-server: $schema=https://github.com/dfinity/icp-cli/raw/refs/tags/v0.2.0/docs/schemas/canister-yaml-schema.json

name: backend
recipe:
type: "@dfinity/rust@v3.2.0"
configuration:
package: backend
shrink: true
candid: backend.did
metadata:
- name: "crate:version"
value: 1.0.0
Loading