From 6a40344d9873d2c7cb6e1e53be2c7e67afd923aa Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 10 Jun 2026 17:29:02 +0200 Subject: [PATCH 01/21] chore: migrate motoko/basic_bitcoin to icp-cli Replace dfx.json with icp.yaml, restructure sources under backend/, and migrate management canister calls to mo:ic@4.0.0. Actor class with network parameter replaced by persistent actor with hardcoded regtest network. ECDSA/Schnorr APIs updated to call ic.ecdsa_public_key, ic.sign_with_ecdsa, ic.schnorr_public_key, and ic.sign_with_schnorr directly instead of through typed actor handles. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/basic_bitcoin.yml | 28 +++ .../.devcontainer/devcontainer.json | 20 -- motoko/basic_bitcoin/BUILD.md | 113 ----------- motoko/basic_bitcoin/Makefile | 32 +++ motoko/basic_bitcoin/README.md | 158 +++------------ .../src => backend}/BitcoinApi.mo | 60 +++--- .../basic_bitcoin/src => backend}/EcdsaApi.mo | 14 +- .../basic_bitcoin/src => backend}/P2pkh.mo | 18 +- .../basic_bitcoin/src => backend}/P2tr.mo | 42 ++-- .../src => backend}/P2trKeyOnly.mo | 13 +- .../src => backend}/SchnorrApi.mo | 15 +- motoko/basic_bitcoin/backend/Types.mo | 95 +++++++++ .../basic_bitcoin/src => backend}/Utils.mo | 6 +- .../src/Main.mo => backend/app.mo} | 27 +-- motoko/basic_bitcoin/dfx.json | 28 --- motoko/basic_bitcoin/icp.yaml | 4 + motoko/basic_bitcoin/mops.toml | 16 +- .../src/basic_bitcoin/basic_bitcoin.did | 62 ------ .../src/basic_bitcoin/src/Types.mo | 185 ------------------ 19 files changed, 291 insertions(+), 645 deletions(-) create mode 100644 .github/workflows/basic_bitcoin.yml delete mode 100644 motoko/basic_bitcoin/.devcontainer/devcontainer.json delete mode 100644 motoko/basic_bitcoin/BUILD.md create mode 100644 motoko/basic_bitcoin/Makefile rename motoko/basic_bitcoin/{src/basic_bitcoin/src => backend}/BitcoinApi.mo (58%) rename motoko/basic_bitcoin/{src/basic_bitcoin/src => backend}/EcdsaApi.mo (54%) rename motoko/basic_bitcoin/{src/basic_bitcoin/src => backend}/P2pkh.mo (87%) rename motoko/basic_bitcoin/{src/basic_bitcoin/src => backend}/P2tr.mo (86%) rename motoko/basic_bitcoin/{src/basic_bitcoin/src => backend}/P2trKeyOnly.mo (73%) rename motoko/basic_bitcoin/{src/basic_bitcoin/src => backend}/SchnorrApi.mo (52%) create mode 100644 motoko/basic_bitcoin/backend/Types.mo rename motoko/basic_bitcoin/{src/basic_bitcoin/src => backend}/Utils.mo (85%) rename motoko/basic_bitcoin/{src/basic_bitcoin/src/Main.mo => backend/app.mo} (66%) delete mode 100644 motoko/basic_bitcoin/dfx.json create mode 100644 motoko/basic_bitcoin/icp.yaml delete mode 100644 motoko/basic_bitcoin/src/basic_bitcoin/basic_bitcoin.did delete mode 100644 motoko/basic_bitcoin/src/basic_bitcoin/src/Types.mo diff --git a/.github/workflows/basic_bitcoin.yml b/.github/workflows/basic_bitcoin.yml new file mode 100644 index 000000000..570284910 --- /dev/null +++ b/.github/workflows/basic_bitcoin.yml @@ -0,0 +1,28 @@ +name: basic_bitcoin + +on: + push: + branches: [master] + pull_request: + paths: + - motoko/basic_bitcoin/** + - .github/workflows/basic_bitcoin.yml + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + motoko-basic_bitcoin: + runs-on: ubuntu-24.04 + container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.1 + env: + ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Deploy and test + working-directory: motoko/basic_bitcoin + run: | + icp network start -d + icp deploy + make test diff --git a/motoko/basic_bitcoin/.devcontainer/devcontainer.json b/motoko/basic_bitcoin/.devcontainer/devcontainer.json deleted file mode 100644 index ebb0b8bcc..000000000 --- a/motoko/basic_bitcoin/.devcontainer/devcontainer.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "ICP Dev Environment", - "image": "ghcr.io/dfinity/icp-dev-env-slim:22", - "forwardPorts": [4943, 5173], - "portsAttributes": { - "4943": { - "label": "dfx", - "onAutoForward": "ignore" - }, - "5173": { - "label": "vite", - "onAutoForward": "openBrowser" - } - }, - "customizations": { - "vscode": { - "extensions": ["dfinity-foundation.vscode-motoko"] - } - } -} diff --git a/motoko/basic_bitcoin/BUILD.md b/motoko/basic_bitcoin/BUILD.md deleted file mode 100644 index 24cfcb754..000000000 --- a/motoko/basic_bitcoin/BUILD.md +++ /dev/null @@ -1,113 +0,0 @@ -# Continue building locally - -Projects deployed through ICP Ninja are temporary; they will only be live for 20 minutes before they are removed. The command-line tool `dfx` can be used to continue building your ICP Ninja project locally and deploy it to the mainnet. - -To migrate your ICP Ninja project off of the web browser and develop it locally, follow these steps. - -### 1. Install developer tools. - -You can install the developer tools natively or use Dev Containers. - -#### Option 1: Natively install developer tools - -> Installing `dfx` natively is currently only supported on macOS and Linux systems. On Windows, it is recommended to use the Dev Containers option. - -1. Install `dfx` with the following command: - -``` - -sh -ci "$(curl -fsSL https://internetcomputer.org/install.sh)" - -``` - -> On Apple Silicon (e.g., Apple M1 chip), make sure you have Rosetta installed (`softwareupdate --install-rosetta`). - -2. [Install NodeJS](https://nodejs.org/en/download/package-manager). - -3. For Rust projects, you will also need to: - -- Install [Rust](https://doc.rust-lang.org/cargo/getting-started/installation.html#install-rust-and-cargo): `curl https://sh.rustup.rs -sSf | sh` - -- Install [candid-extractor](https://crates.io/crates/candid-extractor): `cargo install candid-extractor` - -4. For Motoko projects, you will also need to: - -- Install the Motoko package manager [Mops](https://docs.mops.one/quick-start#2-install-mops-cli): `npm i -g ic-mops` - -Lastly, navigate into your project's directory that you downloaded from ICP Ninja. - -#### Option 2: Dev Containers - -Continue building your projects locally by installing the [Dev Container extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) for VS Code and [Docker](https://docs.docker.com/engine/install/). - -Make sure Docker is running, then navigate into your project's directory that you downloaded from ICP Ninja and start the Dev Container by selecting `Dev-Containers: Reopen in Container` in VS Code's command palette (F1 or Ctrl/Cmd+Shift+P). - -> Note that local development ports (e.g. the ports used by `dfx` or `vite`) are forwarded from the Dev Container to your local machine. In the VS code terminal, use Ctrl/Cmd+Click on the displayed local URLs to open them in your browser. To view the current port mappings, click the "Ports" tab in the VS Code terminal window. - -### 2. Start the local development environment. - -``` -dfx start --background -``` - -### 3. Create a local developer identity. - -To manage your project's canisters, it is recommended that you create a local [developer identity](https://internetcomputer.org/docs/building-apps/getting-started/identities) rather than use the `dfx` default identity that is not stored securely. - -To create a new identity, run the commands: - -``` - -dfx identity new IDENTITY_NAME - -dfx identity use IDENTITY_NAME - -``` - -Replace `IDENTITY_NAME` with your preferred identity name. The first command `dfx start --background` starts the local `dfx` processes, then `dfx identity new` will create a new identity and return your identity's seed phase. Be sure to save this in a safe, secure location. - -The third command `dfx identity use` will tell `dfx` to use your new identity as the active identity. Any canister smart contracts created after running `dfx identity use` will be owned and controlled by the active identity. - -Your identity will have a principal ID associated with it. Principal IDs are used to identify different entities on ICP, such as users and canisters. - -[Learn more about ICP developer identities](https://internetcomputer.org/docs/building-apps/getting-started/identities). - -### 4. Deploy the project locally. - -Deploy your project to your local developer environment with: - -``` -npm install -dfx deploy - -``` - -Your project will be hosted on your local machine. The local canister URLs for your project will be shown in the terminal window as output of the `dfx deploy` command. You can open these URLs in your web browser to view the local instance of your project. - -### 5. Obtain cycles. - -To deploy your project to the mainnet for long-term public accessibility, first you will need [cycles](https://internetcomputer.org/docs/building-apps/getting-started/tokens-and-cycles). Cycles are used to pay for the resources your project uses on the mainnet, such as storage and compute. - -> This cost model is known as ICP's [reverse gas model](https://internetcomputer.org/docs/building-apps/essentials/gas-cost), where developers pay for their project's gas fees rather than users pay for their own gas fees. This model provides an enhanced end user experience since they do not need to hold tokens or sign transactions when using a dapp deployed on ICP. - -> Learn how much a project may cost by using the [pricing calculator](https://internetcomputer.org/docs/building-apps/essentials/cost-estimations-and-examples). - -Cycles can be obtained through [converting ICP tokens into cycles using `dfx`](https://internetcomputer.org/docs/building-apps/developer-tools/dfx/dfx-cycles#dfx-cycles-convert). - -### 6. Deploy to the mainnet. - -Once you have cycles, run the command: - -``` - -dfx deploy --network ic - -``` - -After your project has been deployed to the mainnet, it will continuously require cycles to pay for the resources it uses. You will need to [top up](https://internetcomputer.org/docs/building-apps/canister-management/topping-up) your project's canisters or set up automatic cycles management through a service such as [CycleOps](https://cycleops.dev/). - -> If your project's canisters run out of cycles, they will be removed from the network. - -## Additional examples - -Additional code examples and sample applications can be found in the [DFINITY examples repo](https://github.com/dfinity/examples). diff --git a/motoko/basic_bitcoin/Makefile b/motoko/basic_bitcoin/Makefile new file mode 100644 index 000000000..1ae767b31 --- /dev/null +++ b/motoko/basic_bitcoin/Makefile @@ -0,0 +1,32 @@ +.PHONY: test + +test: + @echo "=== Test 1: get_p2pkh_address returns a valid address ===" + @result=$$(icp canister call backend get_p2pkh_address '()') && \ + echo "$$result" && \ + echo "$$result" | grep -q '"' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 2: get_p2tr_key_only_address returns a valid address ===" + @result=$$(icp canister call backend get_p2tr_key_only_address '()') && \ + echo "$$result" && \ + echo "$$result" | grep -q '"' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 3: get_p2tr_address returns a valid address ===" + @result=$$(icp canister call backend get_p2tr_address '()') && \ + echo "$$result" && \ + echo "$$result" | grep -q '"' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 4: get_balance returns a nat64 (may be 0 on local replica) ===" + @addr=$$(icp canister call backend get_p2pkh_address '()' | tr -d '()"\n ') && \ + result=$$(icp canister call backend get_balance "(\"$$addr\")") && \ + echo "$$result" && \ + echo "$$result" | grep -qE '[0-9]' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 5: get_current_fee_percentiles returns a vec ===" + @result=$$(icp canister call backend get_current_fee_percentiles '()') && \ + echo "$$result" && \ + echo "PASS" || (echo "FAIL" && exit 1) diff --git a/motoko/basic_bitcoin/README.md b/motoko/basic_bitcoin/README.md index e095c567d..530272354 100644 --- a/motoko/basic_bitcoin/README.md +++ b/motoko/basic_bitcoin/README.md @@ -1,154 +1,56 @@ # Basic Bitcoin -This tutorial will walk you through how to deploy a sample [canister smart contract](https://wiki.internetcomputer.org/wiki/Canister_smart_contract) **that can send and receive Bitcoin** on the Internet Computer. +[View this sample's code on GitHub](https://github.com/dfinity/examples/tree/master/motoko/basic_bitcoin) -## Architecture +## Overview -This example internally leverages the [ECDSA -API](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-ecdsa_public_key), -[Schnorr API](https://internetcomputer.org/docs/current/references/ic-interface-spec#ic-sign_with_schnorr), and [Bitcoin -API](https://github.com/dfinity/bitcoin-canister/blob/master/INTERFACE_SPECIFICATION.md) -of the Internet Computer. +This example demonstrates how a canister smart contract can send and receive Bitcoin on the Internet Computer. It showcases the ECDSA API, Schnorr API (BIP340/BIP341), and Bitcoin API, supporting three address types: P2PKH, P2TR key-only spend, and P2TR with script path. -For a deeper understanding of the ICP < > BTC integration, see the [Bitcoin integration documentation](https://internetcomputer.org/docs/current/developer-docs/multi-chain/bitcoin/overview). +For a deeper understanding of the ICP <> BTC integration, see the [Bitcoin integration documentation](https://internetcomputer.org/docs/current/developer-docs/multi-chain/bitcoin/overview). -## Deploying from ICP Ninja +## Build and deploy from the command line -[![](https://icp.ninja/assets/open.svg)](https://icp.ninja/editor?g=https://github.com/dfinity/examples/tree/master/motoko/basic_bitcoin) +### Prerequisites +- Node.js +- icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` -## Build and deploy from the command-line +### Install -### 1. [Download and install the IC SDK.](https://internetcomputer.org/docs/building-apps/getting-started/install) - -### 2. Download your project from ICP Ninja using the 'Download files' button on the upper left corner, or [clone the GitHub examples repository.](https://github.com/dfinity/examples/) - -### 3. Navigate into the project's directory. - -### 4. Deploy the project to your local environment: - -``` -dfx start --background --clean && dfx deploy +```bash +git clone https://github.com/dfinity/examples +cd examples/motoko/basic_bitcoin ``` -## Security considerations and best practices - -If you base your application on this example, it is recommended that you familiarize yourself with and adhere to the [security best practices](https://internetcomputer.org/docs/building-apps/security/overview) for developing on ICP. This example may not implement all the best practices. - -## Using the basic_bitcoin example - -### Step 1: Generating a Bitcoin address - -Bitcoin has different types of addresses (e.g. P2PKH, P2SH, P2TR). You may want -to check [this -article](https://bitcoinmagazine.com/technical/bitcoin-address-types-compared-p2pkh-p2sh-p2wpkh-and-more) -if you are interested in a high-level comparison of different address types. -These addresses can be generated from an ECDSA public key or a Schnorr -([BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki), -[BIP341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki)) public -key. The example code showcases how your canister can generate and spend from -three types of addresses: -1. A [P2PKH address](https://en.bitcoin.it/wiki/Transaction#Pay-to-PubkeyHash) - using the - [ecdsa_public_key](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-method-ecdsa_public_key) - API. -2. A [P2TR - address](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) - where the funds can be spent using the internal key only ([P2TR key path - spend with unspendable script - tree](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-23)). -3. A [P2TR - address](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) - where the funds can be spent using either 1) the internal key or 2) the - provided public key with the script path, where the Merkelized Alternative - Script Tree (MAST) consists of a single script allowing to spend funds by - exactly one key. - -On the Candid UI of your canister, click the "Call" button under -`get_${type}_address` to generate a `${type}` Bitcoin address, where `${type}` -is one of `[p2pkh, p2tr_key_only, p2tr]` (corresponding to the three types of -addresses described above, in the same order). - -Or, if you prefer the command line: +### Deploy and test ```bash -dfx canister --network=ic call basic_bitcoin get_${type}_address +icp network start -d +icp deploy +make test +icp network stop ``` -* We are generating a Bitcoin testnet address, which can only be -used for sending/receiving Bitcoin on the Bitcoin testnet. +## Generating Bitcoin addresses -### Step 2: Receiving bitcoin +Bitcoin has different types of addresses (e.g. P2PKH, P2TR). These addresses can be generated from an ECDSA public key or a Schnorr ([BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki), [BIP341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki)) public key. The example showcases three address types: -Now that the canister is deployed and you have a Bitcoin address, it's time to receive -some testnet bitcoin. You can use one of the Bitcoin faucets, such as [coinfaucet.eu](https://coinfaucet.eu), -to receive some bitcoin. **Make sure to choose Bitcoin testnet4!** - -Enter your address and click on "Get bitcoins!". In the example below we will use Bitcoin address `n31eU1K11m1r58aJMgTyxGonu7wSMoUYe7`, but you will use your address. The Bitcoin address you see will be different from the one above -because the ECDSA/Schnorr public key your canister retrieves is unique. - -Once the transaction has at least one confirmation, which takes ten minutes on average, -you'll be able to see it in your canister's balance. - -### Step 3: Checking your bitcoin balance - -You can check a Bitcoin address's balance by using the `get_balance` endpoint on your canister. - -In the Candid UI, paste in your canister's address, and click on "Call". - -Alternatively, make the call using the command line. Be sure to replace `mheyfRsAQ1XrjtzjfU1cCH2B6G1KmNarNL` with your own generated address: +1. A [P2PKH address](https://en.bitcoin.it/wiki/Transaction#Pay-to-PubkeyHash) using the ECDSA API. +2. A [P2TR address](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) where funds can be spent using the internal key only (P2TR key path spend with unspendable script tree). +3. A [P2TR address](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) where funds can be spent using either the internal key or a script path key. ```bash -dfx canister --network=ic call basic_bitcoin get_balance '("mheyfRsAQ1XrjtzjfU1cCH2B6G1KmNarNL")' +icp canister call backend get_p2pkh_address '()' +icp canister call backend get_p2tr_key_only_address '()' +icp canister call backend get_p2tr_address '()' ``` -Checking the balance of a Bitcoin address relies on the [bitcoin_get_balance](https://github.com/dfinity/bitcoin-canister/blob/master/INTERFACE_SPECIFICATION.md#bitcoin_get_balance) API. - -### Step 4: Sending bitcoin - -You can send bitcoin using the `send_from_${type}` endpoint on your canister, where -`${type}` is one of -`[p2pkh_address, p2tr_key_only_address, p2tr_address_key_path, p2tr_address_script_path]`. - -In the Candid UI, add a destination address and an amount to send. In the example -below, we're sending 4'321 Satoshi (0.00004321 BTC) back to the testnet faucet. - -Via the command line, the same call would look like this: +## Checking balance and sending Bitcoin ```bash -dfx canister --network=ic call basic_bitcoin send_from_p2pkh_address '(record { destination_address = "tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt"; amount_in_satoshi = 4321; })' +icp canister call backend get_balance '("YOUR_BITCOIN_ADDRESS")' +icp canister call backend send_from_p2pkh_address '(record { destination_address = "DEST_ADDRESS"; amount_in_satoshi = 4321 })' ``` -The `send_from_${type}` endpoint can send bitcoin by: - -1. Getting the percentiles of the most recent fees on the Bitcoin network using the [bitcoin_get_current_fee_percentiles API](https://github.com/dfinity/bitcoin-canister/blob/master/INTERFACE_SPECIFICATION.md#bitcoin_get_current_fee_percentiles). -2. Fetching your unspent transaction outputs (UTXOs), using the [bitcoin_get_utxos API](https://github.com/dfinity/bitcoin-canister/blob/master/INTERFACE_SPECIFICATION.md#bitcoin_get_utxos). -3. Building a transaction, using some of the UTXOs from step 2 as input and the destination address and amount to send as output. - The fee percentiles obtained from step 1 are used to set an appropriate fee. -4. Signing the inputs of the transaction using the - [sign_with_ecdsa - API](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-method-sign_with_ecdsa)/\ - [sign_with_schnorr](https://org5p-7iaaa-aaaak-qckna-cai.icp0.io/docs#ic-sign_with_schnorr). -5. Sending the signed transaction to the Bitcoin network using the [bitcoin_send_transaction API](https://github.com/dfinity/bitcoin-canister/blob/master/INTERFACE_SPECIFICATION.md#bitcoin_send_transaction). - -This canister's `send_from_${type}` endpoint returns the ID of the transaction -it sent to the network. You can track the status of this transaction using a -[block explorer](https://en.bitcoin.it/wiki/Block_chain_browser). Once the -transaction has at least one confirmation, you should be able to see it -reflected in your current balance. - -### Step 5: Retrieving block headers - -You can also get a range of Bitcoin block headers by using the `get_block_headers` -endpoint on your canister. - -In the Candid UI, write the desired start height and optionally end height, and click on "Call": - -Alternatively, make the call using the command line. Be sure to replace `10` with your desired start height: +## Security considerations and best practices -```bash -dfx canister --network=ic call basic_bitcoin get_block_headers "(10: nat32)" -``` -or replace `0` and `11` with your desired start and end height respectively: -```bash -dfx canister --network=ic call basic_bitcoin get_block_headers "(0: nat32, 11: nat32)" -``` +Refer to the [security best practices](https://docs.internetcomputer.org/guides/security/overview) for information on security and best practices for your ICP dapp. diff --git a/motoko/basic_bitcoin/src/basic_bitcoin/src/BitcoinApi.mo b/motoko/basic_bitcoin/backend/BitcoinApi.mo similarity index 58% rename from motoko/basic_bitcoin/src/basic_bitcoin/src/BitcoinApi.mo rename to motoko/basic_bitcoin/backend/BitcoinApi.mo index f20231ffc..fbf3684ea 100644 --- a/motoko/basic_bitcoin/src/basic_bitcoin/src/BitcoinApi.mo +++ b/motoko/basic_bitcoin/backend/BitcoinApi.mo @@ -1,4 +1,6 @@ import Types "Types"; +import Blob "mo:core/Blob"; +import { ic } "mo:ic"; module { type Cycles = Types.Cycles; @@ -7,10 +9,6 @@ module { type BitcoinAddress = Types.BitcoinAddress; type GetUtxosResponse = Types.GetUtxosResponse; type MillisatoshiPerVByte = Types.MillisatoshiPerVByte; - type GetBalanceRequest = Types.GetBalanceRequest; - type GetUtxosRequest = Types.GetUtxosRequest; - type GetCurrentFeePercentilesRequest = Types.GetCurrentFeePercentilesRequest; - type SendTransactionRequest = Types.SendTransactionRequest; // The fees for the various Bitcoin endpoints. let GET_BALANCE_COST_CYCLES : Cycles = 100_000_000; @@ -19,24 +17,25 @@ module { let SEND_TRANSACTION_BASE_COST_CYCLES : Cycles = 5_000_000_000; let SEND_TRANSACTION_COST_CYCLES_PER_BYTE : Cycles = 20_000_000; - /// Actor definition to handle interactions with the management canister. - type ManagementCanisterActor = actor { - bitcoin_get_balance : GetBalanceRequest -> async Satoshi; - bitcoin_get_utxos : GetUtxosRequest -> async GetUtxosResponse; - bitcoin_get_current_fee_percentiles : GetCurrentFeePercentilesRequest -> async [MillisatoshiPerVByte]; - bitcoin_send_transaction : SendTransactionRequest -> async (); + // Maps our local Network type (which includes #regtest) to the mo:ic BitcoinNetwork + // type (which only has #mainnet and #testnet). Regtest is treated as testnet for + // API calls since the local replica's bitcoin integration is equivalent. + func toIcNetwork(network : Network) : { #mainnet; #testnet } { + switch network { + case (#mainnet) #mainnet; + case (#testnet) #testnet; + case (#regtest) #testnet; + }; }; - let management_canister_actor : ManagementCanisterActor = actor("aaaaa-aa"); - /// Returns the balance of the given Bitcoin address. /// /// Relies on the `bitcoin_get_balance` endpoint. /// See https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-bitcoin_get_balance public func get_balance(network : Network, address : BitcoinAddress) : async Satoshi { - await (with cycles = GET_BALANCE_COST_CYCLES) management_canister_actor.bitcoin_get_balance({ + await (with cycles = GET_BALANCE_COST_CYCLES) ic.bitcoin_get_balance({ address; - network; + network = toIcNetwork(network); min_confirmations = null; }) }; @@ -46,11 +45,28 @@ module { /// 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 { - await (with cycles = GET_UTXOS_COST_CYCLES) management_canister_actor.bitcoin_get_utxos({ + let result = await (with cycles = GET_UTXOS_COST_CYCLES) ic.bitcoin_get_utxos({ address; - network; + network = toIcNetwork(network); filter = null; - }) + }); + // Convert mo:ic result types to our local types. + // Blobs are converted to [Nat8] arrays, and Outpoint is mapped to OutPoint. + { + utxos = result.utxos.map(func(u : { height : Nat32; value : Satoshi; outpoint : { txid : Blob; vout : Nat32 } }) : Types.Utxo { + { + outpoint = { txid = u.outpoint.txid; vout = u.outpoint.vout }; + value = u.value; + height = u.height; + } + }); + tip_block_hash = result.tip_block_hash.toArray(); + tip_height = result.tip_height; + next_page = switch (result.next_page) { + case null null; + case (?p) ?p.toArray(); + }; + } }; /// Returns the 100 fee percentiles measured in millisatoshi/vbyte. @@ -59,8 +75,8 @@ module { /// 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] { - await (with cycles = GET_CURRENT_FEE_PERCENTILES_COST_CYCLES) management_canister_actor.bitcoin_get_current_fee_percentiles({ - network; + await (with cycles = GET_CURRENT_FEE_PERCENTILES_COST_CYCLES) ic.bitcoin_get_current_fee_percentiles({ + network = toIcNetwork(network); }) }; @@ -69,9 +85,9 @@ module { /// Relies on the `bitcoin_send_transaction` endpoint. /// See https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-bitcoin_send_transaction public func send_transaction(network : Network, transaction : [Nat8]) : async () { - await (with cycles = SEND_TRANSACTION_BASE_COST_CYCLES + transaction.size() * SEND_TRANSACTION_COST_CYCLES_PER_BYTE) management_canister_actor.bitcoin_send_transaction({ - network; - transaction; + await (with cycles = SEND_TRANSACTION_BASE_COST_CYCLES + transaction.size() * SEND_TRANSACTION_COST_CYCLES_PER_BYTE) ic.bitcoin_send_transaction({ + network = toIcNetwork(network); + transaction = Blob.fromArray(transaction); }) }; } diff --git a/motoko/basic_bitcoin/src/basic_bitcoin/src/EcdsaApi.mo b/motoko/basic_bitcoin/backend/EcdsaApi.mo similarity index 54% rename from motoko/basic_bitcoin/src/basic_bitcoin/src/EcdsaApi.mo rename to motoko/basic_bitcoin/backend/EcdsaApi.mo index edeeb31b7..3dbd6c229 100644 --- a/motoko/basic_bitcoin/src/basic_bitcoin/src/EcdsaApi.mo +++ b/motoko/basic_bitcoin/backend/EcdsaApi.mo @@ -1,21 +1,17 @@ import Types "Types"; +import { ic } "mo:ic"; module { - type ECDSAPublicKey = Types.ECDSAPublicKey; - type ECDSAPublicKeyReply = Types.ECDSAPublicKeyReply; - type SignWithECDSA = Types.SignWithECDSA; - type SignWithECDSAReply = Types.SignWithECDSAReply; type Cycles = Types.Cycles; - type EcdsaCanisterActor = Types.EcdsaCanisterActor; // The fee for the `sign_with_ecdsa` endpoint using the test key. let SIGN_WITH_ECDSA_COST_CYCLES : Cycles = 10_000_000_000; /// Returns the ECDSA public key of this canister at the given derivation path. - public func ecdsa_public_key(ecdsa_canister_actor: EcdsaCanisterActor, key_name : Text, derivation_path : [Blob]) : async Blob { + public func ecdsa_public_key(key_name : Text, derivation_path : [Blob]) : async Blob { // Retrieve the public key of this canister at derivation path // from the ECDSA API. - let res = await ecdsa_canister_actor.ecdsa_public_key({ + let res = await ic.ecdsa_public_key({ canister_id = null; derivation_path; key_id = { @@ -27,8 +23,8 @@ module { res.public_key; }; - public func sign_with_ecdsa(ecdsa_canister_actor: EcdsaCanisterActor, key_name : Text, derivation_path : [Blob], message_hash : Blob) : async Blob { - let res = await (with cycles = SIGN_WITH_ECDSA_COST_CYCLES) ecdsa_canister_actor.sign_with_ecdsa({ + public func sign_with_ecdsa(key_name : Text, derivation_path : [Blob], message_hash : Blob) : async Blob { + let res = await (with cycles = SIGN_WITH_ECDSA_COST_CYCLES) ic.sign_with_ecdsa({ message_hash; derivation_path; key_id = { diff --git a/motoko/basic_bitcoin/src/basic_bitcoin/src/P2pkh.mo b/motoko/basic_bitcoin/backend/P2pkh.mo similarity index 87% rename from motoko/basic_bitcoin/src/basic_bitcoin/src/P2pkh.mo rename to motoko/basic_bitcoin/backend/P2pkh.mo index d99e8bb53..1106cbb65 100644 --- a/motoko/basic_bitcoin/src/basic_bitcoin/src/P2pkh.mo +++ b/motoko/basic_bitcoin/backend/P2pkh.mo @@ -42,14 +42,13 @@ module { type Transaction = Transaction.Transaction; type Script = Script.Script; type SighashType = Nat32; - type EcdsaCanisterActor = Types.EcdsaCanisterActor; let SIGHASH_ALL : SighashType = 0x01; /// Returns the P2PKH address of this canister at the given derivation path. - public func get_address(ecdsa_canister_actor : EcdsaCanisterActor, network : Network, key_name : Text, derivation_path : [[Nat8]]) : async BitcoinAddress { + public func get_address(network : Network, key_name : Text, derivation_path : [[Nat8]]) : async BitcoinAddress { // Fetch the public key of the given derivation path. - let public_key = await EcdsaApi.ecdsa_public_key(ecdsa_canister_actor, key_name, derivation_path.map(Blob.fromArray)); + let public_key = await EcdsaApi.ecdsa_public_key(key_name, derivation_path.map(Blob.fromArray)); // Compute the address. public_key_to_p2pkh_address(network, public_key.toArray()); @@ -58,7 +57,7 @@ module { /// Sends a transaction to the network that transfers the given amount to the /// given destination, where the source of the funds is the canister itself /// at the given derivation path. - public func send(ecdsa_canister_actor : EcdsaCanisterActor, network : Network, derivation_path : [[Nat8]], key_name : Text, dst_address : BitcoinAddress, amount : Satoshi) : async [Nat8] { + public func send(network : Network, derivation_path : [[Nat8]], key_name : Text, dst_address : BitcoinAddress, amount : Satoshi) : async [Nat8] { // Get fee percentiles from previous transactions to estimate our own fee. let fee_percentiles = await BitcoinApi.get_current_fee_percentiles(network); @@ -73,7 +72,7 @@ module { }; // Fetch our public key, P2PKH address, and UTXOs. - let own_public_key = (await EcdsaApi.ecdsa_public_key(ecdsa_canister_actor, key_name, derivation_path.map(Blob.fromArray))).toArray(); + let own_public_key = (await EcdsaApi.ecdsa_public_key(key_name, derivation_path.map(Blob.fromArray))).toArray(); let own_address = public_key_to_p2pkh_address(network, own_public_key); // Note that pagination may have to be used to get all UTXOs for the given address. @@ -82,11 +81,11 @@ module { let own_utxos = (await BitcoinApi.get_utxos(network, own_address)).utxos; // Build the transaction that sends `amount` to the destination address. - let tx_bytes = await build_transaction(ecdsa_canister_actor, own_public_key, own_address, own_utxos, dst_address, amount, fee_per_vbyte); + let tx_bytes = await build_transaction(own_public_key, own_address, own_utxos, dst_address, amount, fee_per_vbyte); let transaction = Utils.get_ok(Transaction.fromBytes(tx_bytes.vals())); // Sign the transaction. - let signed_transaction_bytes = await sign_transaction(ecdsa_canister_actor, own_public_key, own_address, transaction, key_name, derivation_path.map(Blob.fromArray), EcdsaApi.sign_with_ecdsa); + let signed_transaction_bytes = await sign_transaction(own_public_key, own_address, transaction, key_name, derivation_path.map(Blob.fromArray), EcdsaApi.sign_with_ecdsa); let signed_transaction = Utils.get_ok(Transaction.fromBytes(signed_transaction_bytes.vals())); Debug.print("Sending transaction"); @@ -98,7 +97,6 @@ module { // 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], @@ -125,7 +123,6 @@ module { // 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, @@ -153,7 +150,6 @@ module { // 1. All the inputs are referencing outpoints that are owned by `own_address`. // 2. `own_address` is a P2PKH address. func sign_transaction( - ecdsa_canister_actor : EcdsaCanisterActor, own_public_key : [Nat8], own_address : BitcoinAddress, transaction : Transaction, @@ -175,7 +171,7 @@ module { SIGHASH_ALL, ); - let signature_sec = await signer(ecdsa_canister_actor, key_name, derivation_path, Blob.fromArray(sighash)); + let signature_sec = await signer(key_name, derivation_path, Blob.fromArray(sighash)); let signature_der = Der.encodeSignature(signature_sec).toArray(); // Append the sighash type. diff --git a/motoko/basic_bitcoin/src/basic_bitcoin/src/P2tr.mo b/motoko/basic_bitcoin/backend/P2tr.mo similarity index 86% rename from motoko/basic_bitcoin/src/basic_bitcoin/src/P2tr.mo rename to motoko/basic_bitcoin/backend/P2tr.mo index e4d54e965..2119578af 100644 --- a/motoko/basic_bitcoin/src/basic_bitcoin/src/P2tr.mo +++ b/motoko/basic_bitcoin/backend/P2tr.mo @@ -49,13 +49,12 @@ module { type MillisatoshiPerVByte = Types.MillisatoshiPerVByte; type Transaction = Transaction.Transaction; type Script = Script.Script; - type SchnorrCanisterActor = Types.SchnorrCanisterActor; type P2trDerivationPaths = Types.P2trDerivationPaths; /// Returns the P2TR address that allows for key as well as script spends. - public func get_address(schnorr_canister_actor : SchnorrCanisterActor, network : Network, key_name : Text, derivation_paths : P2trDerivationPaths) : async BitcoinAddress { - let internal_bip340_public_key = await fetch_bip340_public_key(schnorr_canister_actor, key_name, derivation_paths.key_path_derivation_path); - let script_bip340_public_key = await fetch_bip340_public_key(schnorr_canister_actor, key_name, derivation_paths.script_path_derivation_path); + public func get_address(network : Network, key_name : Text, derivation_paths : P2trDerivationPaths) : async BitcoinAddress { + let internal_bip340_public_key = await fetch_bip340_public_key(key_name, derivation_paths.key_path_derivation_path); + let script_bip340_public_key = await fetch_bip340_public_key(key_name, derivation_paths.script_path_derivation_path); let { tweaked_address; is_even = _ } = internal_key_and_script_key_to_p2tr_address(internal_bip340_public_key, script_bip340_public_key, network); tweaked_address; @@ -82,7 +81,6 @@ module { // Builds a transaction to send the given `amount` of satoshis to the // destination address. public func build_transaction( - schnorr_canister_actor : SchnorrCanisterActor, own_address : BitcoinAddress, own_utxos : [Utxo], dst_address : BitcoinAddress, @@ -119,7 +117,6 @@ module { // 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_key_spend_transaction( - schnorr_canister_actor, own_address, transaction, amounts, @@ -148,7 +145,6 @@ module { // 1. All the inputs are referencing outpoints that are owned by `own_address`. // 2. `own_address` is a P2TR address. public func sign_key_spend_transaction( - schnorr_canister_actor : SchnorrCanisterActor, own_address : BitcoinAddress, transaction : Transaction, amounts : [Nat64], @@ -171,7 +167,7 @@ module { Nat32.fromIntWrap(i), ); - let signature = (await signer(schnorr_canister_actor, key_name, derivation_path, Blob.fromArray(sighash), aux)).toArray(); + let signature = (await signer(key_name, derivation_path, Blob.fromArray(sighash), aux)).toArray(); transaction.witnesses[i] := [signature]; }; }; @@ -191,7 +187,6 @@ module { /// 2. `own_address` is a P2TR address with a single leaf in MAST that just /// allows one key to be used for spending. func sign_script_spend_transaction( - schnorr_canister_actor : SchnorrCanisterActor, own_address : BitcoinAddress, leaf_script : Script.Script, internal_public_key : [Nat8], @@ -229,7 +224,7 @@ module { Debug.print("Signing sighash: " # debug_show (sighash)); - let signature = (await signer(schnorr_canister_actor, key_name, derivation_path, Blob.fromArray(sighash), null)).toArray(); + let signature = (await signer(key_name, derivation_path, Blob.fromArray(sighash), null)).toArray(); transaction.witnesses[i] := [signature, script_bytes, control_block_bytes]; }; }; @@ -243,9 +238,9 @@ module { /// Sends a key spend transaction with a non-empty MAST to the network that /// transfers the given amount to the given destination, where the source /// of the funds is the canister itself at the given derivation path. - public func send_key_path(schnorr_canister_actor : SchnorrCanisterActor, network : Network, derivation_paths : P2trDerivationPaths, key_name : Text, dst_address : BitcoinAddress, amount : Satoshi) : async [Nat8] { - let internal_bip340_public_key = await fetch_bip340_public_key(schnorr_canister_actor, key_name, derivation_paths.key_path_derivation_path); - let script_bip340_public_key = await fetch_bip340_public_key(schnorr_canister_actor, key_name, derivation_paths.script_path_derivation_path); + public func send_key_path(network : Network, derivation_paths : P2trDerivationPaths, key_name : Text, dst_address : BitcoinAddress, amount : Satoshi) : async [Nat8] { + let internal_bip340_public_key = await fetch_bip340_public_key(key_name, derivation_paths.key_path_derivation_path); + let script_bip340_public_key = await fetch_bip340_public_key(key_name, derivation_paths.script_path_derivation_path); let { tweaked_address = own_tweaked_address } = internal_key_and_script_key_to_p2tr_address(internal_bip340_public_key, script_bip340_public_key, network); let leaf_script = Utils.get_ok(leafScript(script_bip340_public_key)); @@ -254,13 +249,13 @@ module { merkle_root_hash = Blob.fromArray(leafHash(leaf_script)); }); - await send_key_path_generic(schnorr_canister_actor, own_tweaked_address, network, derivation_paths.key_path_derivation_path, key_name, ?aux, dst_address, amount); + await send_key_path_generic(own_tweaked_address, network, derivation_paths.key_path_derivation_path, key_name, ?aux, dst_address, amount); }; /// Sends a key spend transaction to the network that transfers the given amount to the /// given destination, where the source of the funds is the canister itself /// at the given derivation path. - public func send_key_path_generic(schnorr_canister_actor : SchnorrCanisterActor, own_address : BitcoinAddress, network : Network, signer_derivation_path : [[Nat8]], key_name : Text, aux : ?Types.SchnorrAux, dst_address : BitcoinAddress, amount : Satoshi) : async [Nat8] { + public func send_key_path_generic(own_address : BitcoinAddress, network : Network, signer_derivation_path : [[Nat8]], key_name : Text, aux : ?Types.SchnorrAux, dst_address : BitcoinAddress, amount : Satoshi) : async [Nat8] { // Get fee percentiles from previous transactions to estimate our own fee. let fee_percentiles = await BitcoinApi.get_current_fee_percentiles(network); @@ -281,7 +276,7 @@ module { let own_utxos = (await BitcoinApi.get_utxos(network, own_address)).utxos; // Build the transaction that sends `amount` to the destination address. - let tx_bytes = await build_transaction(schnorr_canister_actor, own_address, own_utxos, dst_address, amount, fee_per_vbyte); + let tx_bytes = await build_transaction(own_address, own_utxos, dst_address, amount, fee_per_vbyte); let transaction = Utils.get_ok(Transaction.fromBytes(tx_bytes.vals())); let tx_in_outpoints = transaction.txInputs.map(func(txin : TxInput.TxInput) : Types.OutPoint { txin.prevOutput }); @@ -296,7 +291,7 @@ module { }, ); - let signed_transaction_bytes = await sign_key_spend_transaction(schnorr_canister_actor, own_address, transaction, amounts, key_name, signer_derivation_path.map(Blob.fromArray), aux, SchnorrApi.sign_with_schnorr); + let signed_transaction_bytes = await sign_key_spend_transaction(own_address, transaction, amounts, key_name, signer_derivation_path.map(Blob.fromArray), aux, SchnorrApi.sign_with_schnorr); Debug.print("Sending transaction : " # debug_show (signed_transaction_bytes)); let signed_transaction = Utils.get_ok(Transaction.fromBytes(signed_transaction_bytes.vals())); @@ -310,7 +305,7 @@ module { /// Sends a script spend transaction to the network that transfers the given amount to the /// given destination, where the source of the funds is the canister itself /// at the given derivation path. - public func send_script_path(schnorr_canister_actor : SchnorrCanisterActor, network : Network, derivation_paths : P2trDerivationPaths, key_name : Text, dst_address : BitcoinAddress, amount : Satoshi) : async [Nat8] { + public func send_script_path(network : Network, derivation_paths : P2trDerivationPaths, key_name : Text, dst_address : BitcoinAddress, amount : Satoshi) : async [Nat8] { // Get fee percentiles from previous transactions to estimate our own fee. let fee_percentiles = await BitcoinApi.get_current_fee_percentiles(network); @@ -325,8 +320,8 @@ module { }; // Fetch our public keys, P2TR script spend address, and UTXOs. - let internal_bip340_public_key = await fetch_bip340_public_key(schnorr_canister_actor, key_name, derivation_paths.key_path_derivation_path); - let script_bip340_public_key = await fetch_bip340_public_key(schnorr_canister_actor, key_name, derivation_paths.script_path_derivation_path); + let internal_bip340_public_key = await fetch_bip340_public_key(key_name, derivation_paths.key_path_derivation_path); + let script_bip340_public_key = await fetch_bip340_public_key(key_name, derivation_paths.script_path_derivation_path); let { tweaked_address = own_tweaked_address; is_even } = internal_key_and_script_key_to_p2tr_address(internal_bip340_public_key, script_bip340_public_key, network); let own_leaf_script = Utils.get_ok(leafScript(script_bip340_public_key)); @@ -338,7 +333,7 @@ module { let own_utxos = (await BitcoinApi.get_utxos(network, own_tweaked_address)).utxos; // Build the transaction that sends `amount` to the destination address. - let tx_bytes = await build_transaction(schnorr_canister_actor, own_tweaked_address, own_utxos, dst_address, amount, fee_per_vbyte); + let tx_bytes = await build_transaction(own_tweaked_address, own_utxos, dst_address, amount, fee_per_vbyte); let transaction = Utils.get_ok(Transaction.fromBytes(tx_bytes.vals())); let tx_in_outpoints = transaction.txInputs.map(func(txin : TxInput.TxInput) : Types.OutPoint { txin.prevOutput }); @@ -355,7 +350,6 @@ module { // Sign the transaction. let signed_transaction_bytes = await sign_script_spend_transaction( - schnorr_canister_actor, own_tweaked_address, own_leaf_script, internal_bip340_public_key, @@ -416,8 +410,8 @@ module { Hash.taggedHash(untweaked_bip340_public_key, "TapTweak"); }; - public func fetch_bip340_public_key(schnorr_canister_actor : SchnorrCanisterActor, key_name : Text, derivation_path : [[Nat8]]) : async [Nat8] { - let sec1_public_key = (await SchnorrApi.schnorr_public_key(schnorr_canister_actor, key_name, derivation_path.map(Blob.fromArray))).toArray(); + public func fetch_bip340_public_key(key_name : Text, derivation_path : [[Nat8]]) : async [Nat8] { + let sec1_public_key = (await SchnorrApi.schnorr_public_key(key_name, derivation_path.map(Blob.fromArray))).toArray(); sec1_public_key.sliceToArray(1, 33); }; }; diff --git a/motoko/basic_bitcoin/src/basic_bitcoin/src/P2trKeyOnly.mo b/motoko/basic_bitcoin/backend/P2trKeyOnly.mo similarity index 73% rename from motoko/basic_bitcoin/src/basic_bitcoin/src/P2trKeyOnly.mo rename to motoko/basic_bitcoin/backend/P2trKeyOnly.mo index 73c98f2d7..af37974df 100644 --- a/motoko/basic_bitcoin/src/basic_bitcoin/src/P2trKeyOnly.mo +++ b/motoko/basic_bitcoin/backend/P2trKeyOnly.mo @@ -29,26 +29,25 @@ module { type MillisatoshiPerVByte = Types.MillisatoshiPerVByte; type Transaction = Transaction.Transaction; type Script = Script.Script; - type SchnorrCanisterActor = Types.SchnorrCanisterActor; /// Sends a transaction to the network that transfers the given amount to the /// given destination, where the source of the funds is the canister itself /// at the given derivation path. - public func send(schnorr_canister_actor : SchnorrCanisterActor, network : Network, derivation_path : [[Nat8]], key_name : Text, dst_address : BitcoinAddress, amount : Satoshi) : async [Nat8] { - let own_address = await get_address_key_only(schnorr_canister_actor, network, key_name, derivation_path); - let untweaked_bip340_public_key_bytes = await P2tr.fetch_bip340_public_key(schnorr_canister_actor, key_name, derivation_path); + public func send(network : Network, derivation_path : [[Nat8]], key_name : Text, dst_address : BitcoinAddress, amount : Satoshi) : async [Nat8] { + let own_address = await get_address_key_only(network, key_name, derivation_path); + let untweaked_bip340_public_key_bytes = await P2tr.fetch_bip340_public_key(key_name, derivation_path); let aux = #bip341({ merkle_root_hash = Blob.fromArray(P2tr.unspendableMerkleRoot(untweaked_bip340_public_key_bytes)); }); - await P2tr.send_key_path_generic(schnorr_canister_actor, own_address, network, derivation_path, key_name, ?aux, dst_address, amount); + await P2tr.send_key_path_generic(own_address, network, derivation_path, key_name, ?aux, dst_address, amount); }; /// Returns the P2TR key-only address of this canister at a specific /// derivation path. The Merkle tree root is computed as /// `taggedHash(bip340_public_key_bytes, "TapTweak")` and is unspendable. - public func get_address_key_only(schnorr_canister_actor : SchnorrCanisterActor, network : Network, key_name : Text, derivation_path : [[Nat8]]) : async BitcoinAddress { - let bip340_public_key_bytes = await P2tr.fetch_bip340_public_key(schnorr_canister_actor, key_name, derivation_path); + public func get_address_key_only(network : Network, key_name : Text, derivation_path : [[Nat8]]) : async BitcoinAddress { + let bip340_public_key_bytes = await P2tr.fetch_bip340_public_key(key_name, derivation_path); let merkleRoot = P2tr.unspendableMerkleRoot(bip340_public_key_bytes); let tweak = Utils.get_ok(tweakFromKeyAndHash(bip340_public_key_bytes, merkleRoot)); diff --git a/motoko/basic_bitcoin/src/basic_bitcoin/src/SchnorrApi.mo b/motoko/basic_bitcoin/backend/SchnorrApi.mo similarity index 52% rename from motoko/basic_bitcoin/src/basic_bitcoin/src/SchnorrApi.mo rename to motoko/basic_bitcoin/backend/SchnorrApi.mo index a347ba327..4adebdc13 100644 --- a/motoko/basic_bitcoin/src/basic_bitcoin/src/SchnorrApi.mo +++ b/motoko/basic_bitcoin/backend/SchnorrApi.mo @@ -1,21 +1,18 @@ import Types "Types"; +import { ic } "mo:ic"; module { - type SchnorrPublicKeyArgs = Types.SchnorrPublicKeyArgs; - type SchnorrPublicKeyReply = Types.SchnorrPublicKeyReply; - type SignWithSchnorrArgs = Types.SignWithSchnorrArgs; - type SignWithSchnorrReply = Types.SignWithSchnorrReply; type Cycles = Types.Cycles; - type SchnorrCanisterActor = Types.SchnorrCanisterActor; + type SchnorrAux = Types.SchnorrAux; // The fee for the `sign_with_schnorr` endpoint using the test key. let SIGN_WITH_SCHNORR_COST_CYCLES : Cycles = 10_000_000_000; /// Returns the Schnorr public key of this canister at the given derivation path. - public func schnorr_public_key(schnorr_canister_actor : SchnorrCanisterActor, key_name : Text, derivation_path : [Blob]) : async Blob { + public func schnorr_public_key(key_name : Text, derivation_path : [Blob]) : async Blob { // Retrieve the public key of this canister at derivation path // from the Schnorr API. - let res = await schnorr_canister_actor.schnorr_public_key({ + let res = await ic.schnorr_public_key({ canister_id = null; derivation_path; key_id = { @@ -27,8 +24,8 @@ module { res.public_key; }; - public func sign_with_schnorr(schnorr_canister_actor : SchnorrCanisterActor, key_name : Text, derivation_path : [Blob], message : Blob, aux : ?Types.SchnorrAux) : async Blob { - let res = await (with cycles = SIGN_WITH_SCHNORR_COST_CYCLES) schnorr_canister_actor.sign_with_schnorr({ + public func sign_with_schnorr(key_name : Text, derivation_path : [Blob], message : Blob, aux : ?SchnorrAux) : async Blob { + let res = await (with cycles = SIGN_WITH_SCHNORR_COST_CYCLES) ic.sign_with_schnorr({ message; derivation_path; key_id = { diff --git a/motoko/basic_bitcoin/backend/Types.mo b/motoko/basic_bitcoin/backend/Types.mo new file mode 100644 index 000000000..1519d09e2 --- /dev/null +++ b/motoko/basic_bitcoin/backend/Types.mo @@ -0,0 +1,95 @@ +import Curves "mo:bitcoin/ec/Curves"; + +module Types { + public type SendRequest = { + destination_address : Text; + amount_in_satoshi : Satoshi; + }; + + public type SchnorrAux = { + #bip341 : { + merkle_root_hash : Blob; + }; + }; + + public type Satoshi = Nat64; + public type MillisatoshiPerVByte = Nat64; + public type Cycles = Nat; + public type BitcoinAddress = Text; + public type BlockHash = [Nat8]; + public type Page = [Nat8]; + + public let CURVE = Curves.secp256k1; + + /// The type of Bitcoin network the dapp will be interacting with. + public type Network = { + #mainnet; + #testnet; + #regtest; + }; + + /// The type of Bitcoin network as defined by the Bitcoin Motoko library + /// (Note the difference in casing compared to `Network`) + public type NetworkCamelCase = { + #Mainnet; + #Testnet; + #Regtest; + }; + + public func network_to_network_camel_case(network : Network) : NetworkCamelCase { + switch (network) { + case (#regtest) { + #Regtest; + }; + case (#testnet) { + #Testnet; + }; + case (#mainnet) { + #Mainnet; + }; + }; + }; + + /// A reference to a transaction output. + public type OutPoint = { + txid : Blob; + vout : Nat32; + }; + + /// An unspent transaction output. + public type Utxo = { + outpoint : OutPoint; + value : Satoshi; + height : Nat32; + }; + + /// A filter used when requesting UTXOs. + public type UtxosFilter = { + #MinConfirmations : Nat32; + #Page : Page; + }; + + /// A request for getting the UTXOs for a given address. + public type GetUtxosRequest = { + address : BitcoinAddress; + network : Network; + filter : ?UtxosFilter; + }; + + /// The response returned for a request to get the UTXOs of a given address. + public type GetUtxosResponse = { + utxos : [Utxo]; + tip_block_hash : BlockHash; + tip_height : Nat32; + next_page : ?Page; + }; + + public type EcdsaSignFunction = (Text, [Blob], Blob) -> async Blob; + + public type SchnorrSignFunction = (Text, [Blob], Blob, ?SchnorrAux) -> async Blob; + + public type P2trDerivationPaths = { + key_path_derivation_path : [[Nat8]]; + script_path_derivation_path : [[Nat8]]; + }; +}; diff --git a/motoko/basic_bitcoin/src/basic_bitcoin/src/Utils.mo b/motoko/basic_bitcoin/backend/Utils.mo similarity index 85% rename from motoko/basic_bitcoin/src/basic_bitcoin/src/Utils.mo rename to motoko/basic_bitcoin/backend/Utils.mo index d2861300a..4b0618699 100644 --- a/motoko/basic_bitcoin/src/basic_bitcoin/src/Utils.mo +++ b/motoko/basic_bitcoin/backend/Utils.mo @@ -10,8 +10,6 @@ import Types "Types"; module { type Result = Result.Result; - type EcdsaCanisterActor = Types.EcdsaCanisterActor; - type SchnorrCanisterActor = Types.SchnorrCanisterActor; type SchnorrAux = Types.SchnorrAux; /// Returns the value of the result and traps if there isn't any value to return. @@ -78,12 +76,12 @@ module { }; /// A mock for rubber-stamping 64B ECDSA signatures. - public func ecdsa_mock_signer(_ecdsa_canister_actor : EcdsaCanisterActor, _key_name : Text, _derivation_path : [Blob], _message_hash : Blob) : async Blob { + public func ecdsa_mock_signer(_key_name : Text, _derivation_path : [Blob], _message_hash : Blob) : async Blob { Blob.fromArray(Array.repeat(255 : Nat8, 64)); }; /// A mock for rubber-stamping 64B Schnorr signatures. - public func schnorr_mock_signer(_schnorr_canister_actor : SchnorrCanisterActor, _key_name : Text, _derivation_path : [Blob], _message_hash : Blob, _aux : ?SchnorrAux) : async Blob { + public func schnorr_mock_signer(_key_name : Text, _derivation_path : [Blob], _message_hash : Blob, _aux : ?SchnorrAux) : async Blob { Blob.fromArray(Array.repeat(255 : Nat8, 64)); }; }; diff --git a/motoko/basic_bitcoin/src/basic_bitcoin/src/Main.mo b/motoko/basic_bitcoin/backend/app.mo similarity index 66% rename from motoko/basic_bitcoin/src/basic_bitcoin/src/Main.mo rename to motoko/basic_bitcoin/backend/app.mo index b6cade249..be7ee1758 100644 --- a/motoko/basic_bitcoin/src/basic_bitcoin/src/Main.mo +++ b/motoko/basic_bitcoin/backend/app.mo @@ -8,7 +8,7 @@ import P2tr "P2tr"; import Types "Types"; import Utils "Utils"; -persistent actor class BasicBitcoin(network : Types.Network) { +persistent actor BasicBitcoin { type GetUtxosResponse = Types.GetUtxosResponse; type MillisatoshiPerVByte = Types.MillisatoshiPerVByte; type SendRequest = Types.SendRequest; @@ -16,8 +16,6 @@ persistent actor class BasicBitcoin(network : Types.Network) { type BitcoinAddress = Types.BitcoinAddress; type Satoshi = Types.Satoshi; type TransactionId = Text; - type EcdsaCanisterActor = Types.EcdsaCanisterActor; - type SchnorrCanisterActor = Types.SchnorrCanisterActor; type P2trDerivationPaths = Types.P2trDerivationPaths; /// The Bitcoin network to connect to. @@ -25,7 +23,7 @@ persistent actor class BasicBitcoin(network : Types.Network) { /// When developing locally this should be `regtest`. /// When deploying to the IC this should be `testnet`. /// `mainnet` is currently unsupported. - let NETWORK : Network = network; + let NETWORK : Network = #regtest; /// The derivation path to use for ECDSA secp256k1 or Schnorr BIP340/BIP341 key /// derivation. @@ -33,17 +31,12 @@ persistent actor class BasicBitcoin(network : Types.Network) { // The ECDSA key name. transient let KEY_NAME : Text = switch NETWORK { - // For local development, we use a special test key with dfx. + // For local development, we use a special test key with icp-cli. case (#regtest) "dfx_test_key"; // On the IC we're using a test ECDSA key. case _ "test_key_1"; }; - // Threshold signing APIs instantiated with the management canister ID. Can be - // replaced for cheaper testing. - transient let ecdsa_canister_actor : EcdsaCanisterActor = actor ("aaaaa-aa"); - transient let schnorr_canister_actor : SchnorrCanisterActor = actor ("aaaaa-aa"); - /// Returns the balance of the given Bitcoin address. public func get_balance(address : BitcoinAddress) : async Satoshi { await BitcoinApi.get_balance(NETWORK, address); @@ -62,33 +55,33 @@ persistent actor class BasicBitcoin(network : Types.Network) { /// Returns the P2PKH address of this canister at a specific derivation path. public func get_p2pkh_address() : async BitcoinAddress { - await P2pkh.get_address(ecdsa_canister_actor, NETWORK, KEY_NAME, p2pkhDerivationPath()); + await P2pkh.get_address(NETWORK, KEY_NAME, p2pkhDerivationPath()); }; /// Sends the given amount of bitcoin from this canister to the given address. /// Returns the transaction ID. public func send_from_p2pkh_address(request : SendRequest) : async TransactionId { - Utils.bytesToText(await P2pkh.send(ecdsa_canister_actor, NETWORK, p2pkhDerivationPath(), KEY_NAME, request.destination_address, request.amount_in_satoshi)); + Utils.bytesToText(await P2pkh.send(NETWORK, p2pkhDerivationPath(), KEY_NAME, request.destination_address, request.amount_in_satoshi)); }; public func get_p2tr_key_only_address() : async BitcoinAddress { - await P2trKeyOnly.get_address_key_only(schnorr_canister_actor, NETWORK, KEY_NAME, p2trKeyOnlyDerivationPath()); + await P2trKeyOnly.get_address_key_only(NETWORK, KEY_NAME, p2trKeyOnlyDerivationPath()); }; public func send_from_p2tr_key_only_address(request : SendRequest) : async TransactionId { - Utils.bytesToText(await P2trKeyOnly.send(schnorr_canister_actor, NETWORK, p2trKeyOnlyDerivationPath(), KEY_NAME, request.destination_address, request.amount_in_satoshi)); + Utils.bytesToText(await P2trKeyOnly.send(NETWORK, p2trKeyOnlyDerivationPath(), KEY_NAME, request.destination_address, request.amount_in_satoshi)); }; public func get_p2tr_address() : async BitcoinAddress { - await P2tr.get_address(schnorr_canister_actor, NETWORK, KEY_NAME, p2trDerivationPaths()); + await P2tr.get_address(NETWORK, KEY_NAME, p2trDerivationPaths()); }; public func send_from_p2tr_address_key_path(request : SendRequest) : async TransactionId { - Utils.bytesToText(await P2tr.send_key_path(schnorr_canister_actor, NETWORK, p2trDerivationPaths(), KEY_NAME, request.destination_address, request.amount_in_satoshi)); + Utils.bytesToText(await P2tr.send_key_path(NETWORK, p2trDerivationPaths(), KEY_NAME, request.destination_address, request.amount_in_satoshi)); }; public func send_from_p2tr_address_script_path(request : SendRequest) : async TransactionId { - Utils.bytesToText(await P2tr.send_script_path(schnorr_canister_actor, NETWORK, p2trDerivationPaths(), KEY_NAME, request.destination_address, request.amount_in_satoshi)); + Utils.bytesToText(await P2tr.send_script_path(NETWORK, p2trDerivationPaths(), KEY_NAME, request.destination_address, request.amount_in_satoshi)); }; func p2pkhDerivationPath() : [[Nat8]] { diff --git a/motoko/basic_bitcoin/dfx.json b/motoko/basic_bitcoin/dfx.json deleted file mode 100644 index 9abbb558a..000000000 --- a/motoko/basic_bitcoin/dfx.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "version": 1, - "canisters": { - "basic_bitcoin": { - "main": "src/basic_bitcoin/src/Main.mo", - "type": "motoko", - "init_arg": "(variant { testnet })" - } - }, - "defaults": { - "bitcoin": { - "enabled": true, - "nodes": [ - "127.0.0.1:18444" - ], - "log_level": "info" - }, - "build": { - "packtool": "mops sources", - "args": "" - } - }, - "networks": { - "local": { - "bind": "127.0.0.1:4943" - } - } -} \ No newline at end of file diff --git a/motoko/basic_bitcoin/icp.yaml b/motoko/basic_bitcoin/icp.yaml new file mode 100644 index 000000000..fb741fade --- /dev/null +++ b/motoko/basic_bitcoin/icp.yaml @@ -0,0 +1,4 @@ +canisters: + - name: backend + recipe: + type: "@dfinity/motoko@v5.0.0" diff --git a/motoko/basic_bitcoin/mops.toml b/motoko/basic_bitcoin/mops.toml index 72fcccb25..f52183529 100644 --- a/motoko/basic_bitcoin/mops.toml +++ b/motoko/basic_bitcoin/mops.toml @@ -1,12 +1,16 @@ [toolchain] -moc = "1.5.1" +moc = "1.9.0" [dependencies] -core = "2.4.0" +core = "2.5.0" bitcoin = "0.1.0" +ic = "4.0.0" [moc] -# M0236: use context dot notation (e.g. x.toText() instead of Nat.toText(x)) -# M0237: redundant explicit implicit arguments (e.g. Nat.compare is inferred automatically) -# M0223: redundant type instantiation (e.g. Array.tabulate instead of Array.tabulate) -args = ["-W=M0236,M0237,M0223"] +# M0236: use context dot notation +# M0237: redundant explicit implicit arguments +# M0223: redundant type instantiation +args = ["--default-persistent-actors", "-W=M0236,M0237,M0223"] + +[canisters.backend] +main = "backend/app.mo" diff --git a/motoko/basic_bitcoin/src/basic_bitcoin/basic_bitcoin.did b/motoko/basic_bitcoin/src/basic_bitcoin/basic_bitcoin.did deleted file mode 100644 index e589413f5..000000000 --- a/motoko/basic_bitcoin/src/basic_bitcoin/basic_bitcoin.did +++ /dev/null @@ -1,62 +0,0 @@ -type satoshi = nat64; - -type millisatoshi_per_vbyte = nat64; - -type bitcoin_address = text; - -type transaction_id = text; - -type block_hash = blob; - -type network = variant { - regtest; - testnet; - mainnet; -}; - -type outpoint = record { - txid : blob; - vout : nat32 -}; - -type utxo = record { - outpoint: outpoint; - value: satoshi; - height: nat32; -}; - -type get_utxos_response = record { - utxos: vec utxo; - tip_block_hash: block_hash; - tip_height: nat32; - next_page: opt blob; -}; - -service : (network) -> { - "get_balance": (address: bitcoin_address) -> (satoshi); - - "get_utxos": (bitcoin_address) -> (get_utxos_response); - - "get_current_fee_percentiles": () -> (vec millisatoshi_per_vbyte); - - "get_p2pkh_address": () -> (bitcoin_address); - - "send_from_p2pkh_address": (record { - destination_address: bitcoin_address; - amount_in_satoshi: satoshi; - }) -> (transaction_id); - - "get_p2tr_raw_key_spend_address": () -> (bitcoin_address); - - "send_from_p2tr_raw_key_spend_address": (record { - destination_address: bitcoin_address; - amount_in_satoshi: satoshi; - }) -> (transaction_id); - - "get_p2tr_script_spend_address": () -> (bitcoin_address); - - "send_from_p2tr_script_spend_address": (record { - destination_address: bitcoin_address; - amount_in_satoshi: satoshi; - }) -> (transaction_id); -} diff --git a/motoko/basic_bitcoin/src/basic_bitcoin/src/Types.mo b/motoko/basic_bitcoin/src/basic_bitcoin/src/Types.mo deleted file mode 100644 index 6ac5e4085..000000000 --- a/motoko/basic_bitcoin/src/basic_bitcoin/src/Types.mo +++ /dev/null @@ -1,185 +0,0 @@ -import Curves "mo:bitcoin/ec/Curves"; - -module Types { - public type SendRequest = { - destination_address : Text; - amount_in_satoshi : Satoshi; - }; - - public type ECDSAPublicKeyReply = { - public_key : Blob; - chain_code : Blob; - }; - - public type EcdsaKeyId = { - curve : EcdsaCurve; - name : Text; - }; - - public type EcdsaCurve = { - #secp256k1; - }; - - public type SignWithECDSAReply = { - signature : Blob; - }; - - public type ECDSAPublicKey = { - canister_id : ?Principal; - derivation_path : [Blob]; - key_id : EcdsaKeyId; - }; - - public type SignWithECDSA = { - message_hash : Blob; - derivation_path : [Blob]; - key_id : EcdsaKeyId; - }; - - public type SchnorrKeyId = { - algorithm : SchnorrAlgorithm; - name : Text; - }; - - public type SchnorrAlgorithm = { - #bip340secp256k1; - }; - - public type SchnorrPublicKeyArgs = { - canister_id : ?Principal; - derivation_path : [Blob]; - key_id : SchnorrKeyId; - }; - - public type SchnorrPublicKeyReply = { - public_key : Blob; - chain_code : Blob; - }; - - public type SignWithSchnorrArgs = { - message : Blob; - derivation_path : [Blob]; - key_id : SchnorrKeyId; - aux : ?SchnorrAux; - }; - - public type SchnorrAux = { - #bip341 : { - merkle_root_hash : Blob; - }; - }; - - public type SignWithSchnorrReply = { - signature : Blob; - }; - - public type Satoshi = Nat64; - public type MillisatoshiPerVByte = Nat64; - public type Cycles = Nat; - public type BitcoinAddress = Text; - public type BlockHash = [Nat8]; - public type Page = [Nat8]; - - public let CURVE = Curves.secp256k1; - - /// The type of Bitcoin network the dapp will be interacting with. - public type Network = { - #mainnet; - #testnet; - #regtest; - }; - - /// The type of Bitcoin network as defined by the Bitcoin Motoko library - /// (Note the difference in casing compared to `Network`) - public type NetworkCamelCase = { - #Mainnet; - #Testnet; - #Regtest; - }; - - public func network_to_network_camel_case(network : Network) : NetworkCamelCase { - switch (network) { - case (#regtest) { - #Regtest; - }; - case (#testnet) { - #Testnet; - }; - case (#mainnet) { - #Mainnet; - }; - }; - }; - - /// A reference to a transaction output. - public type OutPoint = { - txid : Blob; - vout : Nat32; - }; - - /// An unspent transaction output. - public type Utxo = { - outpoint : OutPoint; - value : Satoshi; - height : Nat32; - }; - - /// A request for getting the balance for a given address. - public type GetBalanceRequest = { - address : BitcoinAddress; - network : Network; - min_confirmations : ?Nat32; - }; - - /// A filter used when requesting UTXOs. - public type UtxosFilter = { - #MinConfirmations : Nat32; - #Page : Page; - }; - - /// A request for getting the UTXOs for a given address. - public type GetUtxosRequest = { - address : BitcoinAddress; - network : Network; - filter : ?UtxosFilter; - }; - - /// The response returned for a request to get the UTXOs of a given address. - public type GetUtxosResponse = { - utxos : [Utxo]; - tip_block_hash : BlockHash; - tip_height : Nat32; - next_page : ?Page; - }; - - /// A request for getting the current fee percentiles. - public type GetCurrentFeePercentilesRequest = { - network : Network; - }; - - public type SendTransactionRequest = { - transaction : [Nat8]; - network : Network; - }; - - public type EcdsaSignFunction = (EcdsaCanisterActor, Text, [Blob], Blob) -> async Blob; - - /// Actor definition to handle interactions with the ECDSA canister. - public type EcdsaCanisterActor = actor { - ecdsa_public_key : ECDSAPublicKey -> async ECDSAPublicKeyReply; - sign_with_ecdsa : SignWithECDSA -> async SignWithECDSAReply; - }; - - public type SchnorrSignFunction = (SchnorrCanisterActor, Text, [Blob], Blob, ?SchnorrAux) -> async Blob; - - /// Actor definition to handle interactions with the Schnorr canister. - public type SchnorrCanisterActor = actor { - schnorr_public_key : SchnorrPublicKeyArgs -> async SchnorrPublicKeyReply; - sign_with_schnorr : SignWithSchnorrArgs -> async SignWithSchnorrReply; - }; - - public type P2trDerivationPaths = { - key_path_derivation_path : [[Nat8]]; - script_path_derivation_path : [[Nat8]]; - }; -}; From 4b26bae328c2459b47307387a356ff3f7104861a Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 10:43:03 +0200 Subject: [PATCH 02/21] chore: complete basic_bitcoin icp-cli migration with Docker/bitcoind setup - Convert actor to persistent actor class with network init arg - Use test_key_1 for all networks (available in PocketIC) - Add Dockerfile + docker/start.sh for self-contained bitcoind regtest - Update icp.yaml with networks/environments and init_arg per env - Update CI workflow with Docker build steps, version 0.3.2, --cycles 30t - Expand Makefile with mining tests, topup target, BTC JSON-RPC tests - Fix BitcoinApi.mo and Utils.mo for mops check compatibility - Update README with Docker setup and multi-environment docs Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/basic_bitcoin.yml | 15 +- motoko/basic_bitcoin/Dockerfile | 8 + motoko/basic_bitcoin/Makefile | 43 +++-- motoko/basic_bitcoin/README.md | 62 ++++++- motoko/basic_bitcoin/backend/BitcoinApi.mo | 1 + motoko/basic_bitcoin/backend/Utils.mo | 3 +- motoko/basic_bitcoin/backend/app.mo | 12 +- motoko/basic_bitcoin/docker/start.sh | 31 ++++ motoko/basic_bitcoin/icp.yaml | 27 ++++ motoko/basic_bitcoin/mops.lock | 178 +++++++++++++++++++++ 10 files changed, 352 insertions(+), 28 deletions(-) create mode 100644 motoko/basic_bitcoin/Dockerfile create mode 100644 motoko/basic_bitcoin/docker/start.sh create mode 100644 motoko/basic_bitcoin/mops.lock diff --git a/.github/workflows/basic_bitcoin.yml b/.github/workflows/basic_bitcoin.yml index 570284910..97d28e6b6 100644 --- a/.github/workflows/basic_bitcoin.yml +++ b/.github/workflows/basic_bitcoin.yml @@ -15,14 +15,25 @@ concurrency: jobs: motoko-basic_bitcoin: runs-on: ubuntu-24.04 - container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.1 + container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.2 env: ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + - name: Build network launcher image + uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 + with: + context: motoko/basic_bitcoin + push: false + load: true + tags: basic-bitcoin-launcher:latest + cache-from: type=gha + cache-to: type=gha,mode=max - name: Deploy and test working-directory: motoko/basic_bitcoin run: | icp network start -d - icp deploy + icp deploy --cycles 30t make test diff --git a/motoko/basic_bitcoin/Dockerfile b/motoko/basic_bitcoin/Dockerfile new file mode 100644 index 000000000..2531a4aec --- /dev/null +++ b/motoko/basic_bitcoin/Dockerfile @@ -0,0 +1,8 @@ +FROM ghcr.io/dfinity/icp-cli-network-launcher:0.3.2 + +RUN apt-get update && apt-get install -y bitcoind && rm -rf /var/lib/apt/lists/* + +COPY docker/start.sh /app/start.sh +RUN chmod +x /app/start.sh + +ENTRYPOINT ["/app/start.sh"] diff --git a/motoko/basic_bitcoin/Makefile b/motoko/basic_bitcoin/Makefile index 1ae767b31..b2b4541f0 100644 --- a/motoko/basic_bitcoin/Makefile +++ b/motoko/basic_bitcoin/Makefile @@ -1,32 +1,57 @@ -.PHONY: test +IMAGE_NAME = basic-bitcoin-launcher +BTC_RPC = http://ic-btc-integration:ic-btc-integration@localhost:18443 + +.PHONY: build-image test topup + +build-image: + docker build -t $(IMAGE_NAME) . + +topup: + icp canister top-up --amount 30t backend test: - @echo "=== Test 1: get_p2pkh_address returns a valid address ===" + @echo "=== Test 1: get_p2pkh_address returns a valid Bitcoin address ===" @result=$$(icp canister call backend get_p2pkh_address '()') && \ echo "$$result" && \ echo "$$result" | grep -q '"' && \ echo "PASS" || (echo "FAIL" && exit 1) - @echo "=== Test 2: get_p2tr_key_only_address returns a valid address ===" + @echo "=== Test 2: get_p2tr_key_only_address returns a valid Bitcoin address ===" @result=$$(icp canister call backend get_p2tr_key_only_address '()') && \ echo "$$result" && \ echo "$$result" | grep -q '"' && \ echo "PASS" || (echo "FAIL" && exit 1) - @echo "=== Test 3: get_p2tr_address returns a valid address ===" + @echo "=== Test 3: get_p2tr_address returns a valid Bitcoin address ===" @result=$$(icp canister call backend get_p2tr_address '()') && \ echo "$$result" && \ echo "$$result" | grep -q '"' && \ echo "PASS" || (echo "FAIL" && exit 1) - @echo "=== Test 4: get_balance returns a nat64 (may be 0 on local replica) ===" - @addr=$$(icp canister call backend get_p2pkh_address '()' | tr -d '()"\n ') && \ + @echo "=== Test 4: get_current_fee_percentiles returns a vec ===" + @result=$$(icp canister call backend get_current_fee_percentiles '()') && \ + echo "$$result" && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Mining 101 blocks to fund test address ===" + @addr=$$(icp canister call backend get_p2pkh_address '()' | grep -o '"[^"]*"' | tr -d '"') && \ + curl -s -X POST $(BTC_RPC) -H 'Content-Type: application/json' \ + -d "{\"jsonrpc\":\"1.0\",\"method\":\"generatetoaddress\",\"params\":[101,\"$$addr\"]}" \ + > /dev/null && echo "mined 101 blocks to $$addr" + + @echo "=== Waiting for IC to sync Bitcoin blocks ===" + @sleep 5 + + @echo "=== Test 5: get_balance returns non-zero after mining ===" + @addr=$$(icp canister call backend get_p2pkh_address '()' | grep -o '"[^"]*"' | tr -d '"') && \ result=$$(icp canister call backend get_balance "(\"$$addr\")") && \ echo "$$result" && \ - echo "$$result" | grep -qE '[0-9]' && \ + echo "$$result" | grep -qE '[1-9]' && \ echo "PASS" || (echo "FAIL" && exit 1) - @echo "=== Test 5: get_current_fee_percentiles returns a vec ===" - @result=$$(icp canister call backend get_current_fee_percentiles '()') && \ + @echo "=== Test 6: get_utxos returns utxos after mining ===" + @addr=$$(icp canister call backend get_p2pkh_address '()' | grep -o '"[^"]*"' | tr -d '"') && \ + result=$$(icp canister call backend get_utxos "(\"$$addr\")") && \ echo "$$result" && \ + echo "$$result" | grep -q 'utxos' && \ echo "PASS" || (echo "FAIL" && exit 1) diff --git a/motoko/basic_bitcoin/README.md b/motoko/basic_bitcoin/README.md index 530272354..27c9091dd 100644 --- a/motoko/basic_bitcoin/README.md +++ b/motoko/basic_bitcoin/README.md @@ -1,9 +1,5 @@ # Basic Bitcoin -[View this sample's code on GitHub](https://github.com/dfinity/examples/tree/master/motoko/basic_bitcoin) - -## Overview - This example demonstrates how a canister smart contract can send and receive Bitcoin on the Internet Computer. It showcases the ECDSA API, Schnorr API (BIP340/BIP341), and Bitcoin API, supporting three address types: P2PKH, P2TR key-only spend, and P2TR with script path. For a deeper understanding of the ICP <> BTC integration, see the [Bitcoin integration documentation](https://internetcomputer.org/docs/current/developer-docs/multi-chain/bitcoin/overview). @@ -11,8 +7,10 @@ For a deeper understanding of the ICP <> BTC integration, see the [Bitcoin integ ## Build and deploy from the command line ### Prerequisites + - Node.js - icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` +- Docker (for local testing with bitcoind) ### Install @@ -21,15 +19,37 @@ git clone https://github.com/dfinity/examples cd examples/motoko/basic_bitcoin ``` -### Deploy and test +### Local deployment (with bitcoind) + +The local environment uses a self-contained Docker image that bundles `bitcoind` in regtest mode alongside the IC network launcher. Build the image first: + +```bash +make build-image +``` + +Then deploy and test: ```bash icp network start -d -icp deploy +icp deploy --cycles 30t make test icp network stop ``` +> If tests fail with an out-of-cycles error, run `make topup` to add 30 trillion cycles to the backend canister and retry. + +### Staging deployment (IC testnet) + +```bash +icp deploy -e staging +``` + +### Production deployment (IC mainnet) + +```bash +icp deploy -e production +``` + ## Generating Bitcoin addresses Bitcoin has different types of addresses (e.g. P2PKH, P2TR). These addresses can be generated from an ECDSA public key or a Schnorr ([BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki), [BIP341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki)) public key. The example showcases three address types: @@ -48,9 +68,37 @@ icp canister call backend get_p2tr_address '()' ```bash icp canister call backend get_balance '("YOUR_BITCOIN_ADDRESS")' +icp canister call backend get_utxos '("YOUR_BITCOIN_ADDRESS")' +icp canister call backend get_current_fee_percentiles '()' +``` + +### Sending Bitcoin + +```bash icp canister call backend send_from_p2pkh_address '(record { destination_address = "DEST_ADDRESS"; amount_in_satoshi = 4321 })' +icp canister call backend send_from_p2tr_key_only_address '(record { destination_address = "DEST_ADDRESS"; amount_in_satoshi = 4321 })' +icp canister call backend send_from_p2tr_address_key_path '(record { destination_address = "DEST_ADDRESS"; amount_in_satoshi = 4321 })' +icp canister call backend send_from_p2tr_address_script_path '(record { destination_address = "DEST_ADDRESS"; amount_in_satoshi = 4321 })' +``` + +### Local testing with bitcoind JSON-RPC + +For local testing, the Docker-based network launcher exposes the bitcoind JSON-RPC on port 18443. Mine blocks to a canister address using `curl`: + +```bash +# Get the P2PKH address +ADDR=$(icp canister call backend get_p2pkh_address '()' | grep -o '"[^"]*"' | tr -d '"') + +# Mine 101 blocks to that address (provides spendable funds) +curl -s -X POST http://ic-btc-integration:ic-btc-integration@localhost:18443 \ + -H 'Content-Type: application/json' \ + -d "{\"jsonrpc\":\"1.0\",\"method\":\"generatetoaddress\",\"params\":[101,\"$ADDR\"]}" + +# Wait for the IC to sync the blocks, then check balance +sleep 5 +icp canister call backend get_balance "(\"$ADDR\")" ``` ## Security considerations and best practices -Refer to the [security best practices](https://docs.internetcomputer.org/guides/security/overview) for information on security and best practices for your ICP dapp. +Refer to the [security best practices](https://docs.internetcomputer.org/guides/security/overview) for information on security and best practices for your ICP app. diff --git a/motoko/basic_bitcoin/backend/BitcoinApi.mo b/motoko/basic_bitcoin/backend/BitcoinApi.mo index fbf3684ea..45b45ba81 100644 --- a/motoko/basic_bitcoin/backend/BitcoinApi.mo +++ b/motoko/basic_bitcoin/backend/BitcoinApi.mo @@ -1,4 +1,5 @@ import Types "Types"; +import Array "mo:core/Array"; import Blob "mo:core/Blob"; import { ic } "mo:ic"; diff --git a/motoko/basic_bitcoin/backend/Utils.mo b/motoko/basic_bitcoin/backend/Utils.mo index 4b0618699..01a4f2450 100644 --- a/motoko/basic_bitcoin/backend/Utils.mo +++ b/motoko/basic_bitcoin/backend/Utils.mo @@ -1,9 +1,8 @@ import Result "mo:core/Result"; -import Debug "mo:core/Debug"; -import Iter "mo:core/Iter"; import Nat8 "mo:core/Nat8"; import Runtime "mo:core/Runtime"; import Text "mo:core/Text"; +import Iter "mo:core/Iter"; import Blob "mo:core/Blob"; import Array "mo:core/Array"; import Types "Types"; diff --git a/motoko/basic_bitcoin/backend/app.mo b/motoko/basic_bitcoin/backend/app.mo index be7ee1758..8e9e3817a 100644 --- a/motoko/basic_bitcoin/backend/app.mo +++ b/motoko/basic_bitcoin/backend/app.mo @@ -8,7 +8,7 @@ import P2tr "P2tr"; import Types "Types"; import Utils "Utils"; -persistent actor BasicBitcoin { +persistent actor class BasicBitcoin(network : Types.Network) { type GetUtxosResponse = Types.GetUtxosResponse; type MillisatoshiPerVByte = Types.MillisatoshiPerVByte; type SendRequest = Types.SendRequest; @@ -23,19 +23,15 @@ persistent actor BasicBitcoin { /// When developing locally this should be `regtest`. /// When deploying to the IC this should be `testnet`. /// `mainnet` is currently unsupported. - let NETWORK : Network = #regtest; + let NETWORK : Network = network; /// The derivation path to use for ECDSA secp256k1 or Schnorr BIP340/BIP341 key /// derivation. transient let DERIVATION_PATH : [[Nat8]] = []; // The ECDSA key name. - transient let KEY_NAME : Text = switch NETWORK { - // For local development, we use a special test key with icp-cli. - case (#regtest) "dfx_test_key"; - // On the IC we're using a test ECDSA key. - case _ "test_key_1"; - }; + // `test_key_1` is available on both the local PocketIC network and the IC testnet. + transient let KEY_NAME : Text = "test_key_1"; /// Returns the balance of the given Bitcoin address. public func get_balance(address : BitcoinAddress) : async Satoshi { diff --git a/motoko/basic_bitcoin/docker/start.sh b/motoko/basic_bitcoin/docker/start.sh new file mode 100644 index 000000000..f18613c15 --- /dev/null +++ b/motoko/basic_bitcoin/docker/start.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# Start bitcoind in regtest mode, then hand off to the IC network launcher. +# bitcoind runs in the background; the launcher becomes PID 1 via exec. + +bitcoind \ + -regtest -server \ + -rpcbind=127.0.0.1 -rpcallowip=127.0.0.1/0 \ + -rpcuser=ic-btc-integration -rpcpassword=ic-btc-integration \ + -fallbackfee=0.00001 -txindex=1 & + +# Wait for bitcoind to accept RPC connections +until bitcoin-cli -regtest \ + -rpcuser=ic-btc-integration -rpcpassword=ic-btc-integration \ + getblockcount >/dev/null 2>&1; do + sleep 0.5 +done + +echo "bitcoind ready on regtest" + +# Hand off to the IC network launcher. +# --bitcoind-addr wires the IC Bitcoin subnet to our local bitcoind. +# Port 18443 is the RPC port (used in Makefile via curl JSON-RPC). +# Port 18444 is the P2P port (used by the launcher for block discovery). +exec /app/icp-cli-network-launcher \ + --status-dir=/app/status \ + --config-port 4942 \ + --gateway-port 4943 \ + --bind 0.0.0.0 \ + --pocketic-config-bind 0.0.0.0 \ + --bitcoind-addr=127.0.0.1:18444 \ + "$@" diff --git a/motoko/basic_bitcoin/icp.yaml b/motoko/basic_bitcoin/icp.yaml index fb741fade..35933030e 100644 --- a/motoko/basic_bitcoin/icp.yaml +++ b/motoko/basic_bitcoin/icp.yaml @@ -2,3 +2,30 @@ canisters: - name: backend recipe: type: "@dfinity/motoko@v5.0.0" + +networks: + - name: local + mode: managed + image: basic-bitcoin-launcher + port-mapping: + - 0:4943 # IC gateway (dynamic) + - 18443:18443 # bitcoind JSON-RPC (fixed, used by Makefile for mining) + +environments: + - name: local + network: local + settings: + backend: + init_arg: "(variant { regtest })" + + - name: staging + network: ic + settings: + backend: + init_arg: "(variant { testnet })" + + - name: production + network: ic + settings: + backend: + init_arg: "(variant { mainnet })" diff --git a/motoko/basic_bitcoin/mops.lock b/motoko/basic_bitcoin/mops.lock new file mode 100644 index 000000000..a81311db1 --- /dev/null +++ b/motoko/basic_bitcoin/mops.lock @@ -0,0 +1,178 @@ +{ + "version": 3, + "mopsTomlDepsHash": "bbea2f0a867714d2e59626a02fe05b37f6c0e4b02db9003c94f2f090fa565246", + "deps": { + "core": "2.5.0", + "bitcoin": "0.1.0", + "base": "0.12.1", + "sha2": "0.1.0", + "ic": "4.0.0" + }, + "hashes": { + "core@2.5.0": { + "core@2.5.0/src/Blob.mo": "25d7c07e9786f11ceb8611a32a83b8491769580024054086e4548607a67afbe4", + "core@2.5.0/LICENSE": "840e3d57a38a8061f55d04470fbd58b9345326fa04ea10ee42add5c6e3b2aa08", + "core@2.5.0/README.md": "9fce230bdc675d2493d65493c295ffce47bda7b56a80d23a389ca95d117dcb39", + "core@2.5.0/NOTICE": "d62ca4ef9e2c9df064115c339013593cf0769e2b9dc78fa4b3211222541c7f83", + "core@2.5.0/mops.toml": "5d34e80a4636956c22bedeeb1c00de33eb1ebfc32a2c2bdd260ce5045fce84ef", + "core@2.5.0/src/Array.mo": "7400fd0e6d2550ddb6d01833f67a69109c617032ac445275aabc7c7d188caf8d", + "core@2.5.0/src/Base64.mo": "34bedbad18db06770823e86668a5968893124bfa6d60d8df6b8d9a9a8e15e441", + "core@2.5.0/src/Bool.mo": "3b606a6631f28c774e3055bc860201e56260fd32fdc5e8dc00d901506be95db5", + "core@2.5.0/src/Error.mo": "2112280ea5300ae94ff9188c3bbd75a7db87f0c6eb65c2ac5bf04bc27c851d98", + "core@2.5.0/src/CallerAttributes.mo": "7ac5ea306f7e4393a7830a1dd522583d3388b8dd4667db132ee77ba9ae131f66", + "core@2.5.0/src/Debug.mo": "526c3f9d94e4a682917febb7b0021831a7cc345a2aa63e2499ddeda33cf63b3e", + "core@2.5.0/src/Cycles.mo": "9f5c1fceea47f05c43f22a1c25e8753fb7e552e36106d27bceef24b1310f4cbc", + "core@2.5.0/src/Char.mo": "ecc2550a6c70ccbb204d216bd18a422d2fcfbb4668a89ac49aab059972902041", + "core@2.5.0/src/Float.mo": "d8b0afa56911ce8da99e26c8b0f2f149cfdea04da864805ee1af44b67ce047b7", + "core@2.5.0/src/CertifiedData.mo": "5af3182d77ab7aab2384d1f663a8cd992bbd6652aacff29c443314cf2186328f", + "core@2.5.0/src/Int32.mo": "41d42155e753ef9fc1301c8823f255134bf2202d4a74ef33e7b7b54a8fc53af4", + "core@2.5.0/src/Int16.mo": "78c72aafbc039e2c0becc824caa4181158206759d6859e83077db360bc8568c3", + "core@2.5.0/src/Float32.mo": "8716c0504acb010c2f358a22896f917835377ebf16e4977e1776a26dae1d615c", + "core@2.5.0/src/Func.mo": "535991ccca8d551d48129a0b7df03ea7bd6ebf3d74b1b572259d10ce709d7d46", + "core@2.5.0/src/Int.mo": "03886964690ff75ecd4170decd67653c517dadc65d9957fc644d03e9c1cf9abb", + "core@2.5.0/src/InternetComputer.mo": "969951411beb5caf7d60ba6d5e906a5c70a3b466cee42e1135d1cd3d791cb3a1", + "core@2.5.0/src/Int64.mo": "f1fda85af9f20b47bcbfda6eaef08c6eee11cab6e5fe3eadb2d9b1c2bcef2e65", + "core@2.5.0/src/Int8.mo": "361af007cfc333937c6bb8e238304db1a7f07a310ec3321a85b89d627f16990f", + "core@2.5.0/src/Map.mo": "71c1dd738868479cc9770470706f6f222ac9d7b4a2743f59b33b16b31fd98ac7", + "core@2.5.0/src/List.mo": "263c61ad591bc94f5adfc27517122ce060b9f50425b52e4e01c2d0ed1ced029d", + "core@2.5.0/src/Nat16.mo": "2b04622ab82be11147130dce8968195c0d4a09b37de8f57d16580673ca780f78", + "core@2.5.0/src/Nat64.mo": "b7ea9f45a35f82c81771c1c36d99d73fdbd9e21f5b0c10e832685f7327246c5e", + "core@2.5.0/src/Nat32.mo": "e3630af6551ae2a80f3809870f55d137c07f68092940c8d9255395504e4e81c8", + "core@2.5.0/src/Iter.mo": "3627fbf52a392bfa76c6d6803c584b1556add077d0ad214d55802e17acf4d773", + "core@2.5.0/src/Nat.mo": "78dab3e33310b766ee9066095a3e6acb1884a8dac32ae9a7cd560637d1e4ea4a", + "core@2.5.0/src/Nat8.mo": "e45a6ae2a55ee6ff890d556dc289816b1bdf352d52d4b3ce54e57ae795deffc2", + "core@2.5.0/src/Option.mo": "aeec94cd6954e7b8b5463a5b15abddfb56c5322cb9b9e7ac782092d5c33a8b38", + "core@2.5.0/src/Region.mo": "741a6fae4f61e42fa0a938b00646515edfc96527341a3a2c3936d436c973798b", + "core@2.5.0/src/Result.mo": "0932722ab0187524c3c11539ccf2a1b9453438a9f3a042dde94e45d9c56a32c5", + "core@2.5.0/src/Principal.mo": "2246c14f55c5bc6968d97b551d9083522bb7a35bfac6f10f4dbcaa7ba53fbb3f", + "core@2.5.0/src/Random.mo": "f5d48c30297b579a068af8cd8c6f4f986605e492b1b65424329a5171d1480e97", + "core@2.5.0/src/Order.mo": "6c21e905e1e8c2b40cd50438218230c207d0bbec0d2514779edd76f9f84a812c", + "core@2.5.0/src/PriorityQueue.mo": "37b9906d1d5a61e520027fd2e48a77facb28ddc760d8129447d6902e037ef7ce", + "core@2.5.0/src/Queue.mo": "807ae8b4bdcaef6e37c57b65a93a04c0c5ed72f77e315e9916f5a25f62ce424f", + "core@2.5.0/src/Runtime.mo": "d221bd16b45cb717cb4ef4fd698fa8632a415aecbbb5a6fdc8b5cd4bc50a9e56", + "core@2.5.0/src/Stack.mo": "ed3c98fae4164ab4f934752f9adc2a92098af8f6d9fc71b16a54867cdb8787f8", + "core@2.5.0/src/Text.mo": "c23d3f2970234a9553d33aaed7907684d16b220e2cea95ebce02b25c3215e787", + "core@2.5.0/src/Types.mo": "00dc0132996b3bb3eaf5cc4413be78ca860137ef26f5b472b0e7b3ef68f6652f", + "core@2.5.0/src/Tuples.mo": "0156c1fc21fffedd71d173e0d1b1e7a9a19726cbc49ec1cf49029d6d93bc7955", + "core@2.5.0/src/Timer.mo": "389c247eed600fc8195ad1d9872f98fcc92faad96335c9734cf5265d573612e2", + "core@2.5.0/src/Set.mo": "d64fed0989da152d0a4baa81719bef548ad3f461367d5d434d14e98cb18281f0", + "core@2.5.0/src/Time.mo": "fcffd56cbeb21c3d8613973f3f1ea382babcf122ea93c9689149f0101c5f3e96", + "core@2.5.0/src/WeakReference.mo": "06e58a6d67bf67c4b31ff871416d0cf6d6e34bfb409ce73d83372ef6b74de3cf", + "core@2.5.0/src/VarArray.mo": "289e68fe216caa39d7d4829b73ccc03ed6e5e2e1d07079e3baa83b55651d1dd8", + "core@2.5.0/src/internal/BTreeHelper.mo": "20a475baf18f924ee0297fc234469fa0985cdaa31c85ce002b5a921383b0534d", + "core@2.5.0/src/pure/List.mo": "b4fb45cf75410461fb1501a316d2384d1be63fee1fccf11ed1d29677e5e2393c", + "core@2.5.0/src/internal/PRNG.mo": "8a93e87baa2b12f613c82394a4a05b425e44a695bbace4ca36f9a1f78be26442", + "core@2.5.0/src/internal/SortHelper.mo": "fff4f2eec5c59a3db13e46eeb69152bb1b98931f45cb58ac481439a0cd0d10bb", + "core@2.5.0/src/pure/Map.mo": "e9868fc4dd5e8e6272382e33241c59f8664e3de2e618cbf62fa8b20beabecfab", + "core@2.5.0/src/pure/Queue.mo": "ed0bd0968457ea3bb24c5367f0dbdb159cfe950d0e2666224985e57a784fae3b", + "core@2.5.0/src/pure/RealTimeQueue.mo": "af133ae61c69ce4e00db5e9db53ddeccad84373fef82a156c85360c2426349dd", + "core@2.5.0/src/pure/Set.mo": "f0155b1d548cbc8889fcff75ef52f0cab7998b034621e4faac8d3b79d7bbac07" + }, + "bitcoin@0.1.0": { + "bitcoin@0.1.0/NOTICE": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "bitcoin@0.1.0/mops.toml": "682b5879b8f7598261ea1112babc3fe88a7cc93e364f5578eb06e877af85e21b", + "bitcoin@0.1.0/LICENSE": "c40ad81ae283a698516dbff4959219d6ba96cbec1c545647af1f11f715a73d2a", + "bitcoin@0.1.0/README.md": "d32949323e6d041db192350683e55df0da1831e2766762a11403d0e56203e418", + "bitcoin@0.1.0/src/Bech32.mo": "61c21fd8a155cdce6309fe0631c18caa62925410a85b2c14acbea54a868c2d94", + "bitcoin@0.1.0/src/Base58.mo": "e75ffca71bc273f2273f7b495e6935ba247ab39e889f419a7051267b32edc9a3", + "bitcoin@0.1.0/src/Base58Check.mo": "2514513f3792052614261b2323136269713ac50b0185956010f4973564cbf4b3", + "bitcoin@0.1.0/src/Bip32.mo": "75299c2574918cdac0cf0e175950dc0407f967d5150cc64527ed96c0ca332556", + "bitcoin@0.1.0/src/Common.mo": "9bdad7d03703a9486131567222b3f68571afda5eb9ab5897193496fc86085f1f", + "bitcoin@0.1.0/src/ByteUtils.mo": "e3c1568fb8fa5d0eaa5476742d1a0666d3209444aa5b057929a4707975987d36", + "bitcoin@0.1.0/src/Hash.mo": "2beead1ba39f99d3701df642eadf7f84fe4c4d09ddd44d35b28dc0f67b2dcde9", + "bitcoin@0.1.0/src/Hmac.mo": "a9205f0e6e02e76a99c68dada8cada8bd88a02dd3989e7e61985a1da83dba811", + "bitcoin@0.1.0/src/Ripemd160.mo": "b4bceb913683032d3620ba2452fbf7ffa78b4b2c9f6fbaa1b7ff0b1c2cce7dd9", + "bitcoin@0.1.0/src/Segwit.mo": "423b2c8c26bd9f043b6a71f2c558f7ebdd2806cb5bc77557466e71dc4a3ed165", + "bitcoin@0.1.0/src/bitcoin/Address.mo": "dee618f43508553eb167f5df4301e89d74d6fa5a1fec1fdaa8c2923d42242430", + "bitcoin@0.1.0/src/bitcoin/Bitcoin.mo": "5d143e84f58e9f3d17dcd2f090789bb3da4b42f8d59acbdff2169d48d2d120b1", + "bitcoin@0.1.0/src/bitcoin/P2tr.mo": "6d101f25d494eb654896e53eb23708810ddb3a3f57e887e0aa5b6c7afd836a01", + "bitcoin@0.1.0/src/bitcoin/P2pkh.mo": "58747fc30885094df81cd1f69c668e8192da23ce46f42195de4c5fcee9004694", + "bitcoin@0.1.0/src/bitcoin/Transaction.mo": "92cbda65c07e1316e6c0d352bfde28c00009636033e20f52a4263ced89236fed", + "bitcoin@0.1.0/src/bitcoin/Script.mo": "d719f847263e7dc1f17cef81a65dac022d47bd4ec45a98b2e3eaa32ef5798ebb", + "bitcoin@0.1.0/src/bitcoin/TxInput.mo": "bcacaf667bc9fd6c4c337c54b3f132fb281a1701f7b3950230214839d91d7aeb", + "bitcoin@0.1.0/src/bitcoin/Types.mo": "093cdd2376e381907aa2d76b6222d2a50b323a1f01b6b549030ae3fcb1208904", + "bitcoin@0.1.0/src/bitcoin/TxOutput.mo": "83317ecfb44b656dc8e2d96d223732bfbb8facc91e8932f3699d07a5d180ba41", + "bitcoin@0.1.0/src/bitcoin/Wif.mo": "9beea06a70ef7837afbc43fe19496d116bdf5c4244b02b2c4c12bd3f82a1c444", + "bitcoin@0.1.0/src/bitcoin/Witness.mo": "53716b0e99d035f1324e9328dc26d232a300a0085770953f186dde76dee5c853", + "bitcoin@0.1.0/src/ec/Curves.mo": "d3cc1b65a81a0e9397fa3e1545b5ee94de6a9ebaf794b260524b8edef63577d8", + "bitcoin@0.1.0/src/ec/Affine.mo": "a45a89f4f770c1e0dc58cc625cece12c3008e4810d11b99b82d03a1be47f1992", + "bitcoin@0.1.0/src/ec/Field.mo": "42f95c74571ef0b112ee21a282db6953da7ab7a834d6149bc33359ae4e3c706a", + "bitcoin@0.1.0/src/ec/Fp.mo": "246c531b1e7119ed9363fb21a1b4a468132a048ecab2ebc0654bd401ebd52b4a", + "bitcoin@0.1.0/src/ec/Numbers.mo": "df5edb25185cc54331d6fff4cade036be95f17b7c63c8028fc2ca79ce664e1c3", + "bitcoin@0.1.0/src/ec/Jacobi.mo": "bd5606620e43855bd04a624909a4cba7bf3d376fc74d83924f2035e32537d837", + "bitcoin@0.1.0/src/ecdsa/Der.mo": "faf90b16c7ac785f030e27e0abd5da9d92ca1de619b0e113c1b750be50d1886c", + "bitcoin@0.1.0/src/ecdsa/Publickey.mo": "a07e0d1d39b8f44b2a91e90ced1dbccd5189f112f45cd39db091c8cf4e775e7a", + "bitcoin@0.1.0/src/ecdsa/Ecdsa.mo": "0247bc2b2dac95c1b97d8bf56fbed47e5954269c925def118103f0f85f3a71d1", + "bitcoin@0.1.0/src/ecdsa/Types.mo": "72fd67985eb93f53ee136c06e7fc24042929378955c52088294744e29c39b0c9" + }, + "base@0.12.1": { + "base@0.12.1/mops.toml": "118b3be79becd448f8e448734d8319c162f2de3ab86995fa7fe40afce207f145", + "base@0.12.1/NOTICE": "3960a8d25fa5fc909325817b08b36c1146970930ca15b6352f8ea6db803cab47", + "base@0.12.1/src/Blob.mo": "9d4b10667080778bca66cb534f1e0a1477e5247a970170f4ba19e7c58cc4939d", + "base@0.12.1/README.md": "f45244a47229456939321014b550e3360b4f8c4d0fd60aa60bbba7e9eacbc8de", + "base@0.12.1/src/AssocList.mo": "07a92db12c36ba96baf3fb45ac76662407ca5d6cee7aedb01fa8ed6d9ee98cf4", + "base@0.12.1/LICENSE": "166bd8e8cf7790087d1fd18a9fa4d060cc0d0b3e5ab30689aa5f3a59a93386bf", + "base@0.12.1/src/Array.mo": "b8b182ec7522daf79160a00e34a7ea558369c32c2b67a6ed339fa2e56ce60835", + "base@0.12.1/src/Char.mo": "dc34cfe3482b92134e5604629377b4236367f2f31d8a82b81084ca5cadf31c2e", + "base@0.12.1/src/Bool.mo": "65faf01dc6e3609ed2f729b813811506951adcce71c081fc6edcdbae4c8b1169", + "base@0.12.1/src/CertifiedData.mo": "51a205989529f6b4fe788f78be0de2da1cb96f6532aac8accccbf4421cce3d06", + "base@0.12.1/src/Buffer.mo": "7ad75dec25ea37e964c536d6bb86ed90cc48047feedb57c9ded359bea49045f7", + "base@0.12.1/src/Debug.mo": "3bc1b92c7e849ae09bd5bc43397d625c07ce80d1089cfe12cde6e6fee6a6ab7b", + "base@0.12.1/src/Deque.mo": "39da68fb8f19fb497aa3db769ffe2cb740cab54e6d82264052de505edac9b17b", + "base@0.12.1/src/Error.mo": "988e300efcdf69d08338a506b47da2536a376187542a731dcc83b427c711a23d", + "base@0.12.1/src/ExperimentalCycles.mo": "914a997e981431b104ec8ef6c263908738ce88a3858d93cff7d1b3290cd6b68f", + "base@0.12.1/src/ExperimentalStableMemory.mo": "70330ae4d113e15d1d211c8ea229bc0a6fc6c014f5fece58aea0de90c82e4fff", + "base@0.12.1/src/ExperimentalInternetComputer.mo": "4aa908a6d33be0b0ee053056890c10dfc040df327283087e7c2a923ab19b6ca9", + "base@0.12.1/src/HashMap.mo": "639c3377687ef59c22ac8df7ef5b26cf6c9689a356ea1ac9fbe215819db59adf", + "base@0.12.1/src/Func.mo": "bea8bcb92707a255814e0a7d79c238e490860c38001438612948704027f05bec", + "base@0.12.1/src/Float.mo": "499f0b2e4b3b1bd4322eaa6aa34ed3a99b31f837c6b032e5000e2f574bffa908", + "base@0.12.1/src/Hash.mo": "99d7add2ed6e82cf482593ac85edd231d507ef542608cd42fec1cc6d764906e9", + "base@0.12.1/src/Heap.mo": "517b746ca16d80b9b7cf3fc6c2883854d4cb240bd494a888fb65ac02441f769b", + "base@0.12.1/src/Int.mo": "3dc2fe075ca553d92cb8178f3216a591233376ffb591d42a8f08bd86c7b5cea6", + "base@0.12.1/src/Int16.mo": "586617af6cef687bd05cd29cd0c785b366ad5ad2b871151f909de76671b755c3", + "base@0.12.1/src/Int32.mo": "bcb4775ef68e0b2b5b872771abcda8eaef4e0a126562b0877e91b3a1ebaaffd6", + "base@0.12.1/src/Int64.mo": "7cfa15c6a8b777fb5f67e841da3d0960a5ef43c66d46acdf354e8a7dfac57d2d", + "base@0.12.1/src/IterType.mo": "1b6361f1b7ee81d1974719c8464c8c574aaee992c37c3c7b6c39f107a58af01c", + "base@0.12.1/src/Int8.mo": "dfdaf280fe2a1b7524cf82116ccdeea5c70cb27046b8addd3c3d9f387b17f594", + "base@0.12.1/src/Iter.mo": "140291f243dfcfdbedb54a264cce76b7481eeb8767224a41fa9951beaf067b67", + "base@0.12.1/src/List.mo": "d13e77eb05b4edcca55e7cc5ee7ce5018c5a137a7a67708e8eb54967c38673fb", + "base@0.12.1/src/Nat.mo": "d2d24f2b42365b3a54c8bb3cf467b94d1ec7f3271a7d0f671986c04a39a8b844", + "base@0.12.1/src/Nat16.mo": "42483fe26ec90fcfc5d90e82df945ea128d10145dada4ee8f68ee85e62f17841", + "base@0.12.1/src/Nat32.mo": "7919cf0598707ae3628d70caa3bb44c04e3bd940752b85481f4f071cde89b66a", + "base@0.12.1/src/Nat64.mo": "dce2861f9674d6301fb2b98475460e85b3d245537c52e3c7a90fac257e5c96f0", + "base@0.12.1/src/Nat8.mo": "b9266d0b5e284288beeca990963cb7a8c9492d69771c656d0501507a613802ba", + "base@0.12.1/src/None.mo": "b204a4964519117e6fa8ee82da5653376988cf618fc397205dfc8ea58f60d2d1", + "base@0.12.1/src/Option.mo": "72c9ddc183b6248375e0f96efd33881a130a465f97e6a3a553ffe8f16bb5fba7", + "base@0.12.1/src/Order.mo": "d8c76128271612469d052520f10af726556df78688729f9856b62ea292e0f43f", + "base@0.12.1/src/Prelude.mo": "7b4ea84cb683203c68d6135b524ca5587a1b23436d6733e0aaf723d36be4aa85", + "base@0.12.1/src/Principal.mo": "5e50eba952b7de62450264a2ece0af6edadd0b6dd4c8cfd2e432ca66022f70ab", + "base@0.12.1/src/RBTree.mo": "fe2f775b84552e97ab4749b48d8aff6da675c8541b927f697055f34eb4decd2c", + "base@0.12.1/src/Random.mo": "15f6fd50d10a9eb401c3f18f19874fb334fec309e94499a46a0a876037dca266", + "base@0.12.1/src/Region.mo": "6ca3019aed15ad442a3a794000ac245b2bcbd846033966e446dc1a29986dbf0b", + "base@0.12.1/src/Result.mo": "4aa00198ee36ea3b60519f9c350e1c17b14b6ffa005b6d4edcff36a0a206f3af", + "base@0.12.1/src/Stack.mo": "4b2681e9ed562b02840b7a46b58e66b695a725f31b94f6c9952e95e81b703b84", + "base@0.12.1/src/Text.mo": "08c5c0328da359144a6d50552b5c3ca9ccb63c6b859e2b2403abbfd112e09708", + "base@0.12.1/src/Time.mo": "4e2a6126d0a2b0b862735adbde93620a70cb388af25a87c7b853650abd0743ab", + "base@0.12.1/src/Timer.mo": "6d1c5203670163e689484231c1c34add09938621f9f21ce1390d0d8c4e3ec31d", + "base@0.12.1/src/Trie.mo": "63c0fbe6d48808630c7edf51d66140b8b2a43a734e425ab854234126e242cb07", + "base@0.12.1/src/TrieMap.mo": "6d69d887006cd1dff09331473f258141bccb251a04a3a8221b65720a4bf237ac", + "base@0.12.1/src/TrieSet.mo": "29b3314f43fad714fc7b68cc3b0dde00a7d8b02b6431afd7fe12304dc14f9612" + }, + "sha2@0.1.0": { + "sha2@0.1.0/LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4", + "sha2@0.1.0/src/Sha512.mo": "4fbae3f3a1e8f8cbb4816ae093fab814882184cf37fe642de478a8c1d9e0bd5e", + "sha2@0.1.0/NOTICE": "71e037856ba594c2b26f5d97a1f77a907ab2013af8cbb124857ca8b53022f83f", + "sha2@0.1.0/mops.toml": "36a54c05e17a29c276490836732acc5bf8ce89f5f1df4723a2c0837c7c52576f", + "sha2@0.1.0/README.md": "a0ae47017fa3565b3ba7727515673d007a95c54beb7a933588ffb1b1532f9013", + "sha2@0.1.0/src/Sha256.mo": "393a8bd68ee648ee36dfe1d84d36985c2ed1f13e5f40119b00ddb0575a81137b" + }, + "ic@4.0.0": { + "ic@4.0.0/README.md": "7686aefce0c535d18ee2768e98660575563daa53b00d2c1041170032c232784c", + "ic@4.0.0/mops.toml": "7a6bb3d74b1ec5539b9bb88e0a0a0ba09e8c46e00b38cb5feda009d6b80723df", + "ic@4.0.0/src/Call.mo": "b4a3572e5617d0b1eb696450344b65e99504f844794e7ddf1cff664fcfeb2994", + "ic@4.0.0/src/lib.mo": "9d9ec84a85eb25d191c1cae99cb841d9b6e0cf0f9bc27c3f3805ac9673d7bef1", + "ic@4.0.0/LICENSE": "ec415b57f8143746b567c94bfad243f5342f3edf110c94641a1a60e02722db55", + "ic@4.0.0/src/Types.mo": "373c3a18e072058aecaaec99385a08f95746c7e86cc9694f9f23caa982c2ad6c" + } + } +} \ No newline at end of file From e12ef51ad526939b07420e5bcef8f51b551e174d Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 10:50:51 +0200 Subject: [PATCH 03/21] fix: correct network-launcher image tag; remove redundant persistent keyword; drop type aliases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dockerfile: icp-cli-network-launcher:0.3.2 → v14.0.0-2026-06-04-04-52 (correct tag) - app.mo: remove 'persistent' from actor class (redundant with --default-persistent-actors, M0217) - app.mo: remove redundant type aliases inside actor body; use Types.X directly Co-Authored-By: Claude Sonnet 4.6 --- motoko/basic_bitcoin/Dockerfile | 2 +- motoko/basic_bitcoin/backend/app.mo | 35 +++++++++++------------------ 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/motoko/basic_bitcoin/Dockerfile b/motoko/basic_bitcoin/Dockerfile index 2531a4aec..970c1242b 100644 --- a/motoko/basic_bitcoin/Dockerfile +++ b/motoko/basic_bitcoin/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/dfinity/icp-cli-network-launcher:0.3.2 +FROM ghcr.io/dfinity/icp-cli-network-launcher:v14.0.0-2026-06-04-04-52 RUN apt-get update && apt-get install -y bitcoind && rm -rf /var/lib/apt/lists/* diff --git a/motoko/basic_bitcoin/backend/app.mo b/motoko/basic_bitcoin/backend/app.mo index 8e9e3817a..f19eb89de 100644 --- a/motoko/basic_bitcoin/backend/app.mo +++ b/motoko/basic_bitcoin/backend/app.mo @@ -8,22 +8,13 @@ import P2tr "P2tr"; import Types "Types"; import Utils "Utils"; -persistent actor class BasicBitcoin(network : Types.Network) { - type GetUtxosResponse = Types.GetUtxosResponse; - type MillisatoshiPerVByte = Types.MillisatoshiPerVByte; - type SendRequest = Types.SendRequest; - type Network = Types.Network; - type BitcoinAddress = Types.BitcoinAddress; - type Satoshi = Types.Satoshi; - type TransactionId = Text; - type P2trDerivationPaths = Types.P2trDerivationPaths; +actor class BasicBitcoin(network : Types.Network) { /// The Bitcoin network to connect to. /// /// When developing locally this should be `regtest`. /// When deploying to the IC this should be `testnet`. - /// `mainnet` is currently unsupported. - let NETWORK : Network = network; + let NETWORK : Types.Network = network; /// The derivation path to use for ECDSA secp256k1 or Schnorr BIP340/BIP341 key /// derivation. @@ -34,49 +25,49 @@ persistent actor class BasicBitcoin(network : Types.Network) { transient let KEY_NAME : Text = "test_key_1"; /// Returns the balance of the given Bitcoin address. - public func get_balance(address : BitcoinAddress) : async Satoshi { + public func get_balance(address : Types.BitcoinAddress) : async Types.Satoshi { await BitcoinApi.get_balance(NETWORK, address); }; /// Returns the UTXOs of the given Bitcoin address. - public func get_utxos(address : BitcoinAddress) : async GetUtxosResponse { + public func get_utxos(address : Types.BitcoinAddress) : async Types.GetUtxosResponse { await BitcoinApi.get_utxos(NETWORK, address); }; /// Returns the 100 fee percentiles measured in millisatoshi/vbyte. /// Percentiles are computed from the last 10,000 transactions (if available). - public func get_current_fee_percentiles() : async [MillisatoshiPerVByte] { + public func get_current_fee_percentiles() : async [Types.MillisatoshiPerVByte] { await BitcoinApi.get_current_fee_percentiles(NETWORK); }; /// Returns the P2PKH address of this canister at a specific derivation path. - public func get_p2pkh_address() : async BitcoinAddress { + public func get_p2pkh_address() : async Types.BitcoinAddress { await P2pkh.get_address(NETWORK, KEY_NAME, p2pkhDerivationPath()); }; /// Sends the given amount of bitcoin from this canister to the given address. /// Returns the transaction ID. - public func send_from_p2pkh_address(request : SendRequest) : async TransactionId { + public func send_from_p2pkh_address(request : Types.SendRequest) : async Text { Utils.bytesToText(await P2pkh.send(NETWORK, p2pkhDerivationPath(), KEY_NAME, request.destination_address, request.amount_in_satoshi)); }; - public func get_p2tr_key_only_address() : async BitcoinAddress { + public func get_p2tr_key_only_address() : async Types.BitcoinAddress { await P2trKeyOnly.get_address_key_only(NETWORK, KEY_NAME, p2trKeyOnlyDerivationPath()); }; - public func send_from_p2tr_key_only_address(request : SendRequest) : async TransactionId { + public func send_from_p2tr_key_only_address(request : Types.SendRequest) : async Text { Utils.bytesToText(await P2trKeyOnly.send(NETWORK, p2trKeyOnlyDerivationPath(), KEY_NAME, request.destination_address, request.amount_in_satoshi)); }; - public func get_p2tr_address() : async BitcoinAddress { + public func get_p2tr_address() : async Types.BitcoinAddress { await P2tr.get_address(NETWORK, KEY_NAME, p2trDerivationPaths()); }; - public func send_from_p2tr_address_key_path(request : SendRequest) : async TransactionId { + public func send_from_p2tr_address_key_path(request : Types.SendRequest) : async Text { Utils.bytesToText(await P2tr.send_key_path(NETWORK, p2trDerivationPaths(), KEY_NAME, request.destination_address, request.amount_in_satoshi)); }; - public func send_from_p2tr_address_script_path(request : SendRequest) : async TransactionId { + public func send_from_p2tr_address_script_path(request : Types.SendRequest) : async Text { Utils.bytesToText(await P2tr.send_script_path(NETWORK, p2trDerivationPaths(), KEY_NAME, request.destination_address, request.amount_in_satoshi)); }; @@ -88,7 +79,7 @@ persistent actor class BasicBitcoin(network : Types.Network) { derivationPathWithSuffix("p2tr_key_only"); }; - func p2trDerivationPaths() : P2trDerivationPaths { + func p2trDerivationPaths() : Types.P2trDerivationPaths { { key_path_derivation_path = derivationPathWithSuffix("p2tr_internal_key"); script_path_derivation_path = derivationPathWithSuffix("p2tr_script_key"); From 63c37b980fb3149848281a4cc0a6cff574c069df Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 11:07:58 +0200 Subject: [PATCH 04/21] fix: correct network-launcher tag (no v prefix) Co-Authored-By: Claude Sonnet 4.6 --- motoko/basic_bitcoin/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/motoko/basic_bitcoin/Dockerfile b/motoko/basic_bitcoin/Dockerfile index 970c1242b..d196d9701 100644 --- a/motoko/basic_bitcoin/Dockerfile +++ b/motoko/basic_bitcoin/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/dfinity/icp-cli-network-launcher:v14.0.0-2026-06-04-04-52 +FROM ghcr.io/dfinity/icp-cli-network-launcher:14.0.0-2026-06-04-04-52 RUN apt-get update && apt-get install -y bitcoind && rm -rf /var/lib/apt/lists/* From 1cffa4d97653ec56e499dac7544a4c59e51a77c1 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 11:09:29 +0200 Subject: [PATCH 05/21] chore: use latest tag for network-launcher base image Co-Authored-By: Claude Sonnet 4.6 --- motoko/basic_bitcoin/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/motoko/basic_bitcoin/Dockerfile b/motoko/basic_bitcoin/Dockerfile index d196d9701..929f2d131 100644 --- a/motoko/basic_bitcoin/Dockerfile +++ b/motoko/basic_bitcoin/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/dfinity/icp-cli-network-launcher:14.0.0-2026-06-04-04-52 +FROM ghcr.io/dfinity/icp-cli-network-launcher:latest RUN apt-get update && apt-get install -y bitcoind && rm -rf /var/lib/apt/lists/* From da1a9d139d940e2d44ce8ed9454f5f6c52555ffa Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 11:14:47 +0200 Subject: [PATCH 06/21] fix: install Bitcoin Core from official release with SHA256 verification bitcoind is not in Debian trixie's apt repos. Download the official bitcoin-27.2 tarball from bitcoincore.org, verify the SHA256, extract only the two needed binaries, then purge curl to keep the image clean. Co-Authored-By: Claude Sonnet 4.6 --- motoko/basic_bitcoin/Dockerfile | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/motoko/basic_bitcoin/Dockerfile b/motoko/basic_bitcoin/Dockerfile index 929f2d131..f63dfeb0a 100644 --- a/motoko/basic_bitcoin/Dockerfile +++ b/motoko/basic_bitcoin/Dockerfile @@ -1,6 +1,18 @@ FROM ghcr.io/dfinity/icp-cli-network-launcher:latest -RUN apt-get update && apt-get install -y bitcoind && rm -rf /var/lib/apt/lists/* +ARG BITCOIN_VERSION=27.2 +ARG BITCOIN_SHA256=acc223af46c178064c132b235392476f66d486453ddbd6bca6f1f8411547da78 + +RUN apt-get update && apt-get install -y --no-install-recommends curl && \ + curl -fsSL "https://bitcoincore.org/bin/bitcoin-core-${BITCOIN_VERSION}/bitcoin-${BITCOIN_VERSION}-x86_64-linux-gnu.tar.gz" \ + -o /tmp/bitcoin.tar.gz && \ + echo "${BITCOIN_SHA256} /tmp/bitcoin.tar.gz" | sha256sum -c && \ + tar xzf /tmp/bitcoin.tar.gz --strip-components=2 \ + -C /usr/local/bin \ + "bitcoin-${BITCOIN_VERSION}/bin/bitcoind" \ + "bitcoin-${BITCOIN_VERSION}/bin/bitcoin-cli" && \ + rm /tmp/bitcoin.tar.gz && \ + apt-get purge -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/* COPY docker/start.sh /app/start.sh RUN chmod +x /app/start.sh From 3d8c560122f90fec3d23711c0f7671a05422879d Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 11:19:38 +0200 Subject: [PATCH 07/21] fix: run basic_bitcoin CI on host runner, not in container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When icp-cli runs inside a container (icp-dev-env-motoko), it creates a temp status dir inside the container filesystem, then asks the host Docker daemon to bind-mount that path — which the daemon can't see. Run the job directly on ubuntu-24.04 (no container: wrapper) and install icp-cli + ic-wasm via npm. Paths created on the host are visible to Docker. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/basic_bitcoin.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/basic_bitcoin.yml b/.github/workflows/basic_bitcoin.yml index 97d28e6b6..fc2949101 100644 --- a/.github/workflows/basic_bitcoin.yml +++ b/.github/workflows/basic_bitcoin.yml @@ -14,12 +14,16 @@ concurrency: jobs: motoko-basic_bitcoin: + # Run directly on the host (no container:) so that icp-cli can bind-mount + # the status directory into our custom Docker image. When icp-cli runs inside + # a container, the tmpdir it creates is invisible to the host Docker daemon. runs-on: ubuntu-24.04 - container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.2 env: ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Install icp-cli and ic-wasm + run: npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm - name: Set up Docker Buildx uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - name: Build network launcher image From 26147c2a73394e248507d9664c4fc204a3d58fe5 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 11:20:58 +0200 Subject: [PATCH 08/21] =?UTF-8?q?fix:=20install=20ic-mops=20=E2=80=94=20re?= =?UTF-8?q?quired=20by=20@dfinity/motoko=20recipe=20for=20mops=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/basic_bitcoin.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/basic_bitcoin.yml b/.github/workflows/basic_bitcoin.yml index fc2949101..81bb57aeb 100644 --- a/.github/workflows/basic_bitcoin.yml +++ b/.github/workflows/basic_bitcoin.yml @@ -22,8 +22,8 @@ jobs: ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - name: Install icp-cli and ic-wasm - run: npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm + - name: Install icp-cli, ic-wasm and mops + run: npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm ic-mops - name: Set up Docker Buildx uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - name: Build network launcher image From fc2d90bed5a47cfc134f3a30b15b342fc9d54a5c Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 11:24:49 +0200 Subject: [PATCH 09/21] fix: use init_args (plural) at environment level, not init_arg in settings icp-cli uses 'init_args' as a top-level environment field with canister names as keys, not 'init_arg' nested inside 'settings'. Co-Authored-By: Claude Sonnet 4.6 --- motoko/basic_bitcoin/icp.yaml | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/motoko/basic_bitcoin/icp.yaml b/motoko/basic_bitcoin/icp.yaml index 35933030e..9ac54281b 100644 --- a/motoko/basic_bitcoin/icp.yaml +++ b/motoko/basic_bitcoin/icp.yaml @@ -14,18 +14,15 @@ networks: environments: - name: local network: local - settings: - backend: - init_arg: "(variant { regtest })" + init_args: + backend: "(variant { regtest })" - name: staging network: ic - settings: - backend: - init_arg: "(variant { testnet })" + init_args: + backend: "(variant { testnet })" - name: production network: ic - settings: - backend: - init_arg: "(variant { mainnet })" + init_args: + backend: "(variant { mainnet })" From 125409ec5fe30f3eb828d43df3988ff4b505cc3f Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 11:30:50 +0200 Subject: [PATCH 10/21] fix: use inline actor type for Bitcoin API to support #regtest network MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mo:ic@4.0.0 defines BitcoinNetwork = { #mainnet; #testnet } — no #regtest. The local PocketIC Bitcoin subnet runs in regtest mode and rejects calls using #testnet. Define an inline management canister actor type that uses our own Network type (which includes #regtest) for the Bitcoin API calls. mo:ic is still used for ECDSA and Schnorr signing. Co-Authored-By: Claude Sonnet 4.6 --- motoko/basic_bitcoin/backend/BitcoinApi.mo | 85 +++++++++++++--------- 1 file changed, 49 insertions(+), 36 deletions(-) diff --git a/motoko/basic_bitcoin/backend/BitcoinApi.mo b/motoko/basic_bitcoin/backend/BitcoinApi.mo index 45b45ba81..327ff7c19 100644 --- a/motoko/basic_bitcoin/backend/BitcoinApi.mo +++ b/motoko/basic_bitcoin/backend/BitcoinApi.mo @@ -1,7 +1,5 @@ import Types "Types"; -import Array "mo:core/Array"; import Blob "mo:core/Blob"; -import { ic } "mo:ic"; module { type Cycles = Types.Cycles; @@ -18,27 +16,50 @@ module { let SEND_TRANSACTION_BASE_COST_CYCLES : Cycles = 5_000_000_000; let SEND_TRANSACTION_COST_CYCLES_PER_BYTE : Cycles = 20_000_000; - // Maps our local Network type (which includes #regtest) to the mo:ic BitcoinNetwork - // type (which only has #mainnet and #testnet). Regtest is treated as testnet for - // API calls since the local replica's bitcoin integration is equivalent. - func toIcNetwork(network : Network) : { #mainnet; #testnet } { - switch network { - case (#mainnet) #mainnet; - case (#testnet) #testnet; - case (#regtest) #testnet; + // Use an inline actor type with the full Network variant (including #regtest). + // mo:ic@4.0.0 defines BitcoinNetwork as { #mainnet; #testnet } only — passing + // #regtest through it would be a type error, and the local Bitcoin subnet + // rejects calls made with the wrong network mode. + type ManagementCanisterActor = actor { + bitcoin_get_balance : { + address : BitcoinAddress; + network : Network; + min_confirmations : ?Nat32; + } -> async Satoshi; + + bitcoin_get_utxos : { + address : BitcoinAddress; + network : Network; + filter : ?Types.UtxosFilter; + } -> async { + utxos : [Types.Utxo]; + tip_block_hash : Blob; // Blob from mgmt canister; converted to [Nat8] below + tip_height : Nat32; + next_page : ?Blob; // Blob from mgmt canister; converted to [Nat8] below }; + + bitcoin_get_current_fee_percentiles : { + network : Network; + } -> async [MillisatoshiPerVByte]; + + bitcoin_send_transaction : { + network : Network; + transaction : Blob; + } -> async (); }; + let management_canister_actor : ManagementCanisterActor = actor ("aaaaa-aa"); + /// Returns the balance of the given Bitcoin address. /// /// Relies on the `bitcoin_get_balance` endpoint. /// See https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-bitcoin_get_balance public func get_balance(network : Network, address : BitcoinAddress) : async Satoshi { - await (with cycles = GET_BALANCE_COST_CYCLES) ic.bitcoin_get_balance({ - address; - network = toIcNetwork(network); - min_confirmations = null; - }) + await (with cycles = GET_BALANCE_COST_CYCLES) management_canister_actor.bitcoin_get_balance({ + address; + network; + min_confirmations = null; + }); }; /// Returns the UTXOs of the given Bitcoin address. @@ -46,28 +67,20 @@ module { /// 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 { - let result = await (with cycles = GET_UTXOS_COST_CYCLES) ic.bitcoin_get_utxos({ - address; - network = toIcNetwork(network); - filter = null; + let result = await (with cycles = GET_UTXOS_COST_CYCLES) management_canister_actor.bitcoin_get_utxos({ + address; + network; + filter = null; }); - // Convert mo:ic result types to our local types. - // Blobs are converted to [Nat8] arrays, and Outpoint is mapped to OutPoint. { - utxos = result.utxos.map(func(u : { height : Nat32; value : Satoshi; outpoint : { txid : Blob; vout : Nat32 } }) : Types.Utxo { - { - outpoint = { txid = u.outpoint.txid; vout = u.outpoint.vout }; - value = u.value; - height = u.height; - } - }); + utxos = result.utxos; tip_block_hash = result.tip_block_hash.toArray(); tip_height = result.tip_height; next_page = switch (result.next_page) { case null null; case (?p) ?p.toArray(); }; - } + }; }; /// Returns the 100 fee percentiles measured in millisatoshi/vbyte. @@ -76,9 +89,9 @@ module { /// 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] { - await (with cycles = GET_CURRENT_FEE_PERCENTILES_COST_CYCLES) ic.bitcoin_get_current_fee_percentiles({ - network = toIcNetwork(network); - }) + await (with cycles = GET_CURRENT_FEE_PERCENTILES_COST_CYCLES) management_canister_actor.bitcoin_get_current_fee_percentiles({ + network; + }); }; /// Sends a (signed) transaction to the Bitcoin network. @@ -86,9 +99,9 @@ module { /// Relies on the `bitcoin_send_transaction` endpoint. /// See https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-bitcoin_send_transaction public func send_transaction(network : Network, transaction : [Nat8]) : async () { - await (with cycles = SEND_TRANSACTION_BASE_COST_CYCLES + transaction.size() * SEND_TRANSACTION_COST_CYCLES_PER_BYTE) ic.bitcoin_send_transaction({ - network = toIcNetwork(network); - transaction = Blob.fromArray(transaction); - }) + await (with cycles = SEND_TRANSACTION_BASE_COST_CYCLES + transaction.size() * SEND_TRANSACTION_COST_CYCLES_PER_BYTE) management_canister_actor.bitcoin_send_transaction({ + network; + transaction = Blob.fromArray(transaction); + }); }; } From 6391a586d0d11b516fbb17684a5fdb9e54f03119 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 11:38:13 +0200 Subject: [PATCH 11/21] refactor: use mo:bitcoin and mo:ic types; reduce custom type definitions Co-Authored-By: Claude Sonnet 4.6 --- motoko/basic_bitcoin/backend/P2tr.mo | 6 ++-- motoko/basic_bitcoin/backend/SchnorrApi.mo | 3 +- motoko/basic_bitcoin/backend/Types.mo | 36 +++++----------------- motoko/basic_bitcoin/backend/Utils.mo | 4 +-- 4 files changed, 15 insertions(+), 34 deletions(-) diff --git a/motoko/basic_bitcoin/backend/P2tr.mo b/motoko/basic_bitcoin/backend/P2tr.mo index 2119578af..d14696056 100644 --- a/motoko/basic_bitcoin/backend/P2tr.mo +++ b/motoko/basic_bitcoin/backend/P2tr.mo @@ -36,6 +36,8 @@ import Script "mo:bitcoin/bitcoin/Script"; import Segwit "mo:bitcoin/Segwit"; import Hash "mo:bitcoin/Hash"; +import IC "mo:ic/Types"; + import BitcoinApi "BitcoinApi"; import SchnorrApi "SchnorrApi"; import Types "Types"; @@ -150,7 +152,7 @@ module { amounts : [Nat64], key_name : Text, derivation_path : [Blob], - aux : ?Types.SchnorrAux, + aux : ?IC.SchnorrAux, signer : Types.SchnorrSignFunction, ) : async [Nat8] { // Obtain the scriptPubKey of the source address which is also the @@ -255,7 +257,7 @@ module { /// Sends a key spend transaction to the network that transfers the given amount to the /// given destination, where the source of the funds is the canister itself /// at the given derivation path. - public func send_key_path_generic(own_address : BitcoinAddress, network : Network, signer_derivation_path : [[Nat8]], key_name : Text, aux : ?Types.SchnorrAux, dst_address : BitcoinAddress, amount : Satoshi) : async [Nat8] { + public func send_key_path_generic(own_address : BitcoinAddress, network : Network, signer_derivation_path : [[Nat8]], key_name : Text, aux : ?IC.SchnorrAux, dst_address : BitcoinAddress, amount : Satoshi) : async [Nat8] { // Get fee percentiles from previous transactions to estimate our own fee. let fee_percentiles = await BitcoinApi.get_current_fee_percentiles(network); diff --git a/motoko/basic_bitcoin/backend/SchnorrApi.mo b/motoko/basic_bitcoin/backend/SchnorrApi.mo index 4adebdc13..fd423e656 100644 --- a/motoko/basic_bitcoin/backend/SchnorrApi.mo +++ b/motoko/basic_bitcoin/backend/SchnorrApi.mo @@ -1,9 +1,10 @@ import Types "Types"; import { ic } "mo:ic"; +import IC "mo:ic/Types"; module { type Cycles = Types.Cycles; - type SchnorrAux = Types.SchnorrAux; + type SchnorrAux = IC.SchnorrAux; // The fee for the `sign_with_schnorr` endpoint using the test key. let SIGN_WITH_SCHNORR_COST_CYCLES : Cycles = 10_000_000_000; diff --git a/motoko/basic_bitcoin/backend/Types.mo b/motoko/basic_bitcoin/backend/Types.mo index 1519d09e2..820e580c9 100644 --- a/motoko/basic_bitcoin/backend/Types.mo +++ b/motoko/basic_bitcoin/backend/Types.mo @@ -1,4 +1,6 @@ import Curves "mo:bitcoin/ec/Curves"; +import BitcoinTypes "mo:bitcoin/bitcoin/Types"; +import IC "mo:ic/Types"; module Types { public type SendRequest = { @@ -6,13 +8,7 @@ module Types { amount_in_satoshi : Satoshi; }; - public type SchnorrAux = { - #bip341 : { - merkle_root_hash : Blob; - }; - }; - - public type Satoshi = Nat64; + public type Satoshi = BitcoinTypes.Satoshi; public type MillisatoshiPerVByte = Nat64; public type Cycles = Nat; public type BitcoinAddress = Text; @@ -30,11 +26,7 @@ module Types { /// The type of Bitcoin network as defined by the Bitcoin Motoko library /// (Note the difference in casing compared to `Network`) - public type NetworkCamelCase = { - #Mainnet; - #Testnet; - #Regtest; - }; + public type NetworkCamelCase = BitcoinTypes.Network; public func network_to_network_camel_case(network : Network) : NetworkCamelCase { switch (network) { @@ -51,17 +43,10 @@ module Types { }; /// A reference to a transaction output. - public type OutPoint = { - txid : Blob; - vout : Nat32; - }; + public type OutPoint = BitcoinTypes.OutPoint; /// An unspent transaction output. - public type Utxo = { - outpoint : OutPoint; - value : Satoshi; - height : Nat32; - }; + public type Utxo = BitcoinTypes.Utxo; /// A filter used when requesting UTXOs. public type UtxosFilter = { @@ -69,13 +54,6 @@ module Types { #Page : Page; }; - /// A request for getting the UTXOs for a given address. - public type GetUtxosRequest = { - address : BitcoinAddress; - network : Network; - filter : ?UtxosFilter; - }; - /// The response returned for a request to get the UTXOs of a given address. public type GetUtxosResponse = { utxos : [Utxo]; @@ -86,7 +64,7 @@ module Types { public type EcdsaSignFunction = (Text, [Blob], Blob) -> async Blob; - public type SchnorrSignFunction = (Text, [Blob], Blob, ?SchnorrAux) -> async Blob; + public type SchnorrSignFunction = (Text, [Blob], Blob, ?IC.SchnorrAux) -> async Blob; public type P2trDerivationPaths = { key_path_derivation_path : [[Nat8]]; diff --git a/motoko/basic_bitcoin/backend/Utils.mo b/motoko/basic_bitcoin/backend/Utils.mo index 01a4f2450..79732739d 100644 --- a/motoko/basic_bitcoin/backend/Utils.mo +++ b/motoko/basic_bitcoin/backend/Utils.mo @@ -5,11 +5,11 @@ import Text "mo:core/Text"; import Iter "mo:core/Iter"; import Blob "mo:core/Blob"; import Array "mo:core/Array"; -import Types "Types"; +import IC "mo:ic/Types"; module { type Result = Result.Result; - type SchnorrAux = Types.SchnorrAux; + type SchnorrAux = IC.SchnorrAux; /// Returns the value of the result and traps if there isn't any value to return. public func get_ok(result : Result) : T { From b042c524d38b89e615fc8c1b68e20878bcf995e5 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 12:00:39 +0200 Subject: [PATCH 12/21] =?UTF-8?q?refactor:=20clean=20up=20Types.mo=20?= =?UTF-8?q?=E2=80=94=20use=20package=20types,=20Blob,=20accurate=20naming?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GetUtxosResponse → re-export IC.BitcoinGetUtxosResult (Blob for hashes) - BlockHash / Page type aliases removed (use Blob directly) - MillisatoshiPerVByte → MillisatoshiPerByte (matches IC spec naming) - BitcoinAddress → alias IC.BitcoinAddress - UtxosFilter: lowercase variants (#min_confirmations/#page) + Blob, matching IC - Cycles alias removed from all files (inlined as Nat) - BitcoinApi.mo: no more [Nat8] conversion for tip_block_hash/next_page - Types kept custom: Network (lowercase+regtest), NetworkCamelCase bridge, SendRequest, P2trDerivationPaths, EcdsaSignFunction, SchnorrSignFunction Co-Authored-By: Claude Sonnet 4.6 --- motoko/basic_bitcoin/backend/BitcoinApi.mo | 48 ++++++--------------- motoko/basic_bitcoin/backend/EcdsaApi.mo | 3 +- motoko/basic_bitcoin/backend/P2pkh.mo | 6 +-- motoko/basic_bitcoin/backend/P2tr.mo | 8 ++-- motoko/basic_bitcoin/backend/P2trKeyOnly.mo | 2 +- motoko/basic_bitcoin/backend/SchnorrApi.mo | 3 +- motoko/basic_bitcoin/backend/Types.mo | 48 ++++++++------------- motoko/basic_bitcoin/backend/app.mo | 2 +- 8 files changed, 42 insertions(+), 78 deletions(-) diff --git a/motoko/basic_bitcoin/backend/BitcoinApi.mo b/motoko/basic_bitcoin/backend/BitcoinApi.mo index 327ff7c19..d084c2061 100644 --- a/motoko/basic_bitcoin/backend/BitcoinApi.mo +++ b/motoko/basic_bitcoin/backend/BitcoinApi.mo @@ -2,19 +2,10 @@ import Types "Types"; import Blob "mo:core/Blob"; module { - type Cycles = Types.Cycles; - type Satoshi = Types.Satoshi; type Network = Types.Network; type BitcoinAddress = Types.BitcoinAddress; - type GetUtxosResponse = Types.GetUtxosResponse; - type MillisatoshiPerVByte = Types.MillisatoshiPerVByte; - - // The fees for the various Bitcoin endpoints. - let GET_BALANCE_COST_CYCLES : Cycles = 100_000_000; - let GET_UTXOS_COST_CYCLES : Cycles = 10_000_000_000; - let GET_CURRENT_FEE_PERCENTILES_COST_CYCLES : Cycles = 100_000_000; - let SEND_TRANSACTION_BASE_COST_CYCLES : Cycles = 5_000_000_000; - let SEND_TRANSACTION_COST_CYCLES_PER_BYTE : Cycles = 20_000_000; + type Satoshi = Types.Satoshi; + type MillisatoshiPerByte = Types.MillisatoshiPerByte; // Use an inline actor type with the full Network variant (including #regtest). // mo:ic@4.0.0 defines BitcoinNetwork as { #mainnet; #testnet } only — passing @@ -31,16 +22,11 @@ module { address : BitcoinAddress; network : Network; filter : ?Types.UtxosFilter; - } -> async { - utxos : [Types.Utxo]; - tip_block_hash : Blob; // Blob from mgmt canister; converted to [Nat8] below - tip_height : Nat32; - next_page : ?Blob; // Blob from mgmt canister; converted to [Nat8] below - }; + } -> async Types.GetUtxosResponse; bitcoin_get_current_fee_percentiles : { network : Network; - } -> async [MillisatoshiPerVByte]; + } -> async [MillisatoshiPerByte]; bitcoin_send_transaction : { network : Network; @@ -55,7 +41,7 @@ module { /// Relies on the `bitcoin_get_balance` endpoint. /// See https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-bitcoin_get_balance public func get_balance(network : Network, address : BitcoinAddress) : async Satoshi { - await (with cycles = GET_BALANCE_COST_CYCLES) management_canister_actor.bitcoin_get_balance({ + await (with cycles = 100_000_000) management_canister_actor.bitcoin_get_balance({ address; network; min_confirmations = null; @@ -64,32 +50,23 @@ module { /// Returns the UTXOs of the given Bitcoin address. /// - /// NOTE: Relies on the `bitcoin_get_utxos` endpoint. + /// 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 { - let result = await (with cycles = GET_UTXOS_COST_CYCLES) management_canister_actor.bitcoin_get_utxos({ + public func get_utxos(network : Network, address : BitcoinAddress) : async Types.GetUtxosResponse { + await (with cycles = 10_000_000_000) management_canister_actor.bitcoin_get_utxos({ address; network; filter = null; }); - { - utxos = result.utxos; - tip_block_hash = result.tip_block_hash.toArray(); - tip_height = result.tip_height; - next_page = switch (result.next_page) { - case null null; - case (?p) ?p.toArray(); - }; - }; }; - /// Returns the 100 fee percentiles measured in millisatoshi/vbyte. + /// Returns the 100 fee percentiles measured in millisatoshi/byte. /// 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] { - await (with cycles = GET_CURRENT_FEE_PERCENTILES_COST_CYCLES) management_canister_actor.bitcoin_get_current_fee_percentiles({ + public func get_current_fee_percentiles(network : Network) : async [MillisatoshiPerByte] { + await (with cycles = 100_000_000) management_canister_actor.bitcoin_get_current_fee_percentiles({ network; }); }; @@ -99,7 +76,8 @@ module { /// Relies on the `bitcoin_send_transaction` endpoint. /// See https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-bitcoin_send_transaction public func send_transaction(network : Network, transaction : [Nat8]) : async () { - await (with cycles = SEND_TRANSACTION_BASE_COST_CYCLES + transaction.size() * SEND_TRANSACTION_COST_CYCLES_PER_BYTE) management_canister_actor.bitcoin_send_transaction({ + let cost = 5_000_000_000 + transaction.size() * 20_000_000; + await (with cycles = cost) management_canister_actor.bitcoin_send_transaction({ network; transaction = Blob.fromArray(transaction); }); diff --git a/motoko/basic_bitcoin/backend/EcdsaApi.mo b/motoko/basic_bitcoin/backend/EcdsaApi.mo index 3dbd6c229..ef0c571ae 100644 --- a/motoko/basic_bitcoin/backend/EcdsaApi.mo +++ b/motoko/basic_bitcoin/backend/EcdsaApi.mo @@ -2,10 +2,9 @@ import Types "Types"; import { ic } "mo:ic"; module { - type Cycles = Types.Cycles; // The fee for the `sign_with_ecdsa` endpoint using the test key. - let SIGN_WITH_ECDSA_COST_CYCLES : Cycles = 10_000_000_000; + let SIGN_WITH_ECDSA_COST_CYCLES : Nat = 10_000_000_000; /// Returns the ECDSA public key of this canister at the given derivation path. public func ecdsa_public_key(key_name : Text, derivation_path : [Blob]) : async Blob { diff --git a/motoko/basic_bitcoin/backend/P2pkh.mo b/motoko/basic_bitcoin/backend/P2pkh.mo index 1106cbb65..fefd2c36b 100644 --- a/motoko/basic_bitcoin/backend/P2pkh.mo +++ b/motoko/basic_bitcoin/backend/P2pkh.mo @@ -36,7 +36,7 @@ module { type BitcoinAddress = Types.BitcoinAddress; type Satoshi = Types.Satoshi; type Utxo = Types.Utxo; - type MillisatoshiPerVByte = Types.MillisatoshiPerVByte; + type MillisatoshiPerByte = Types.MillisatoshiPerByte; let CURVE = Types.CURVE; type PublicKey = EcdsaTypes.PublicKey; type Transaction = Transaction.Transaction; @@ -61,7 +61,7 @@ module { // Get fee percentiles from previous transactions to estimate our own fee. let fee_percentiles = await BitcoinApi.get_current_fee_percentiles(network); - let fee_per_vbyte : MillisatoshiPerVByte = if (fee_percentiles.size() == 0) { + let fee_per_vbyte : MillisatoshiPerByte = if (fee_percentiles.size() == 0) { // There are no fee percentiles. This case can only happen on a regtest // network where there are no non-coinbase transactions. In this case, // we use a default of 1000 millisatoshis/vbyte (i.e. 2 satoshi/byte) @@ -102,7 +102,7 @@ module { own_utxos : [Utxo], dst_address : BitcoinAddress, amount : Satoshi, - fee_per_vbyte : MillisatoshiPerVByte, + fee_per_vbyte : MillisatoshiPerByte, ) : async [Nat8] { let dst_address_typed = Utils.get_ok_expect(Address.addressFromText(dst_address), "failed to decode destination address"); diff --git a/motoko/basic_bitcoin/backend/P2tr.mo b/motoko/basic_bitcoin/backend/P2tr.mo index d14696056..54ac70ee5 100644 --- a/motoko/basic_bitcoin/backend/P2tr.mo +++ b/motoko/basic_bitcoin/backend/P2tr.mo @@ -48,7 +48,7 @@ module { type BitcoinAddress = Types.BitcoinAddress; type Satoshi = Types.Satoshi; type Utxo = Types.Utxo; - type MillisatoshiPerVByte = Types.MillisatoshiPerVByte; + type MillisatoshiPerByte = Types.MillisatoshiPerByte; type Transaction = Transaction.Transaction; type Script = Script.Script; type P2trDerivationPaths = Types.P2trDerivationPaths; @@ -87,7 +87,7 @@ module { own_utxos : [Utxo], dst_address : BitcoinAddress, amount : Satoshi, - fee_per_vbyte : MillisatoshiPerVByte, + fee_per_vbyte : MillisatoshiPerByte, ) : async [Nat8] { let dst_address_typed = Utils.get_ok_expect(Address.addressFromText(dst_address), "failed to decode destination address"); @@ -261,7 +261,7 @@ module { // Get fee percentiles from previous transactions to estimate our own fee. let fee_percentiles = await BitcoinApi.get_current_fee_percentiles(network); - let fee_per_vbyte : MillisatoshiPerVByte = if (fee_percentiles.size() == 0) { + let fee_per_vbyte : MillisatoshiPerByte = if (fee_percentiles.size() == 0) { // There are no fee percentiles. This case can only happen on a regtest // network where there are no non-coinbase transactions. In this case, // we use a default of 1000 millisatoshis/vbyte (i.e. 2 satoshi/byte) @@ -311,7 +311,7 @@ module { // Get fee percentiles from previous transactions to estimate our own fee. let fee_percentiles = await BitcoinApi.get_current_fee_percentiles(network); - let fee_per_vbyte : MillisatoshiPerVByte = if (fee_percentiles.size() == 0) { + let fee_per_vbyte : MillisatoshiPerByte = if (fee_percentiles.size() == 0) { // There are no fee percentiles. This case can only happen on a regtest // network where there are no non-coinbase transactions. In this case, // we use a default of 1000 millisatoshis/vbyte (i.e. 2 satoshi/byte) diff --git a/motoko/basic_bitcoin/backend/P2trKeyOnly.mo b/motoko/basic_bitcoin/backend/P2trKeyOnly.mo index af37974df..6f45fe130 100644 --- a/motoko/basic_bitcoin/backend/P2trKeyOnly.mo +++ b/motoko/basic_bitcoin/backend/P2trKeyOnly.mo @@ -26,7 +26,7 @@ module { type BitcoinAddress = Types.BitcoinAddress; type Satoshi = Types.Satoshi; type Utxo = Types.Utxo; - type MillisatoshiPerVByte = Types.MillisatoshiPerVByte; + type MillisatoshiPerByte = Types.MillisatoshiPerByte; type Transaction = Transaction.Transaction; type Script = Script.Script; diff --git a/motoko/basic_bitcoin/backend/SchnorrApi.mo b/motoko/basic_bitcoin/backend/SchnorrApi.mo index fd423e656..bbc94e5aa 100644 --- a/motoko/basic_bitcoin/backend/SchnorrApi.mo +++ b/motoko/basic_bitcoin/backend/SchnorrApi.mo @@ -3,11 +3,10 @@ import { ic } "mo:ic"; import IC "mo:ic/Types"; module { - type Cycles = Types.Cycles; type SchnorrAux = IC.SchnorrAux; // The fee for the `sign_with_schnorr` endpoint using the test key. - let SIGN_WITH_SCHNORR_COST_CYCLES : Cycles = 10_000_000_000; + let SIGN_WITH_SCHNORR_COST_CYCLES : Nat = 10_000_000_000; /// Returns the Schnorr public key of this canister at the given derivation path. public func schnorr_public_key(key_name : Text, derivation_path : [Blob]) : async Blob { diff --git a/motoko/basic_bitcoin/backend/Types.mo b/motoko/basic_bitcoin/backend/Types.mo index 820e580c9..1be14cc9c 100644 --- a/motoko/basic_bitcoin/backend/Types.mo +++ b/motoko/basic_bitcoin/backend/Types.mo @@ -9,58 +9,46 @@ module Types { }; public type Satoshi = BitcoinTypes.Satoshi; - public type MillisatoshiPerVByte = Nat64; - public type Cycles = Nat; - public type BitcoinAddress = Text; - public type BlockHash = [Nat8]; - public type Page = [Nat8]; + + /// millisatoshi per byte — matches the IC management canister's fee percentile unit + public type MillisatoshiPerByte = IC.MillisatoshiPerByte; + + public type BitcoinAddress = IC.BitcoinAddress; public let CURVE = Curves.secp256k1; - /// The type of Bitcoin network the dapp will be interacting with. + /// Lowercase variant Network type including #regtest for local development. + /// Note: mo:bitcoin uses PascalCase (#Mainnet etc.) — see + /// https://github.com/caffeinelabs/motoko-bitcoin/issues/22 public type Network = { #mainnet; #testnet; #regtest; }; - /// The type of Bitcoin network as defined by the Bitcoin Motoko library - /// (Note the difference in casing compared to `Network`) + /// Bridge type for mo:bitcoin address-generation functions (which use PascalCase). + /// Can be removed once mo:bitcoin#22 is fixed. public type NetworkCamelCase = BitcoinTypes.Network; public func network_to_network_camel_case(network : Network) : NetworkCamelCase { switch (network) { - case (#regtest) { - #Regtest; - }; - case (#testnet) { - #Testnet; - }; - case (#mainnet) { - #Mainnet; - }; + case (#regtest) #Regtest; + case (#testnet) #Testnet; + case (#mainnet) #Mainnet; }; }; - /// A reference to a transaction output. public type OutPoint = BitcoinTypes.OutPoint; - - /// An unspent transaction output. public type Utxo = BitcoinTypes.Utxo; - /// A filter used when requesting UTXOs. + /// UTXO filter — matches the IC management canister's inline filter type exactly. public type UtxosFilter = { - #MinConfirmations : Nat32; - #Page : Page; + #min_confirmations : Nat32; + #page : Blob; }; - /// The response returned for a request to get the UTXOs of a given address. - public type GetUtxosResponse = { - utxos : [Utxo]; - tip_block_hash : BlockHash; - tip_height : Nat32; - next_page : ?Page; - }; + /// Re-exported from mo:ic — uses Blob for tip_block_hash and next_page. + public type GetUtxosResponse = IC.BitcoinGetUtxosResult; public type EcdsaSignFunction = (Text, [Blob], Blob) -> async Blob; diff --git a/motoko/basic_bitcoin/backend/app.mo b/motoko/basic_bitcoin/backend/app.mo index f19eb89de..05ce5e863 100644 --- a/motoko/basic_bitcoin/backend/app.mo +++ b/motoko/basic_bitcoin/backend/app.mo @@ -36,7 +36,7 @@ actor class BasicBitcoin(network : Types.Network) { /// Returns the 100 fee percentiles measured in millisatoshi/vbyte. /// Percentiles are computed from the last 10,000 transactions (if available). - public func get_current_fee_percentiles() : async [Types.MillisatoshiPerVByte] { + public func get_current_fee_percentiles() : async [Types.MillisatoshiPerByte] { await BitcoinApi.get_current_fee_percentiles(NETWORK); }; From 619e928dc780d2266ad16af7101c2349f6441f03 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 12:04:45 +0200 Subject: [PATCH 13/21] fix: bind bitcoind RPC to 0.0.0.0 so host can reach it via mapped port -rpcbind=127.0.0.1 bound RPC only to localhost inside the container, blocking connections from outside even with the port mapped in icp.yaml. Co-Authored-By: Claude Sonnet 4.6 --- motoko/basic_bitcoin/docker/start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/motoko/basic_bitcoin/docker/start.sh b/motoko/basic_bitcoin/docker/start.sh index f18613c15..8d83fe257 100644 --- a/motoko/basic_bitcoin/docker/start.sh +++ b/motoko/basic_bitcoin/docker/start.sh @@ -4,7 +4,7 @@ bitcoind \ -regtest -server \ - -rpcbind=127.0.0.1 -rpcallowip=127.0.0.1/0 \ + -rpcbind=0.0.0.0 -rpcallowip=0.0.0.0/0 \ -rpcuser=ic-btc-integration -rpcpassword=ic-btc-integration \ -fallbackfee=0.00001 -txindex=1 & From 2f785e842c10b10e6b37999d5eba7b7c242a299f Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 12:24:01 +0200 Subject: [PATCH 14/21] fix: use docker exec for bitcoin mining instead of curl over mapped port MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit icp-cli applies port mappings correctly, but relying on curl over a mapped port introduces fragile network assumptions (port availability, NAT, IPv4/IPv6 resolution). docker exec runs bitcoin-cli directly inside the already-running container — zero network configuration, always reliable. - Makefile: mine blocks via docker exec (find container by ancestor image) - icp.yaml: remove unused 18443:18443 port mapping Co-Authored-By: Claude Sonnet 4.6 --- motoko/basic_bitcoin/Makefile | 10 ++++++---- motoko/basic_bitcoin/icp.yaml | 1 - 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/motoko/basic_bitcoin/Makefile b/motoko/basic_bitcoin/Makefile index b2b4541f0..c2c95db80 100644 --- a/motoko/basic_bitcoin/Makefile +++ b/motoko/basic_bitcoin/Makefile @@ -1,5 +1,6 @@ IMAGE_NAME = basic-bitcoin-launcher -BTC_RPC = http://ic-btc-integration:ic-btc-integration@localhost:18443 +# Find the running container built from our custom image +BITCOIN_CONTAINER = $(shell docker ps --filter "ancestor=$(IMAGE_NAME)" --format "{{.ID}}" | head -1) .PHONY: build-image test topup @@ -35,9 +36,10 @@ test: @echo "=== Mining 101 blocks to fund test address ===" @addr=$$(icp canister call backend get_p2pkh_address '()' | grep -o '"[^"]*"' | tr -d '"') && \ - curl -s -X POST $(BTC_RPC) -H 'Content-Type: application/json' \ - -d "{\"jsonrpc\":\"1.0\",\"method\":\"generatetoaddress\",\"params\":[101,\"$$addr\"]}" \ - > /dev/null && echo "mined 101 blocks to $$addr" + docker exec $(BITCOIN_CONTAINER) bitcoin-cli -regtest \ + -rpcuser=ic-btc-integration -rpcpassword=ic-btc-integration \ + generatetoaddress 101 "$$addr" > /dev/null && \ + echo "mined 101 blocks to $$addr" @echo "=== Waiting for IC to sync Bitcoin blocks ===" @sleep 5 diff --git a/motoko/basic_bitcoin/icp.yaml b/motoko/basic_bitcoin/icp.yaml index 9ac54281b..3ee4f02cb 100644 --- a/motoko/basic_bitcoin/icp.yaml +++ b/motoko/basic_bitcoin/icp.yaml @@ -9,7 +9,6 @@ networks: image: basic-bitcoin-launcher port-mapping: - 0:4943 # IC gateway (dynamic) - - 18443:18443 # bitcoind JSON-RPC (fixed, used by Makefile for mining) environments: - name: local From dd429a8d16af829dc9520d12c5495116083be623 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 12:43:00 +0200 Subject: [PATCH 15/21] fix: rename image to icp-cli-network-launcher-bitcoin; fix test 6 grep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename Docker image basic-bitcoin-launcher → icp-cli-network-launcher-bitcoin to make clear this is a network launcher variant - Test 6: grep for 'tip_height = 101' instead of 'utxos' — the blob field in the Candid output contains binary data that interferes with grep; tip_height = 101 unambiguously confirms the Bitcoin blockchain synced Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/basic_bitcoin.yml | 2 +- motoko/basic_bitcoin/Makefile | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/basic_bitcoin.yml b/.github/workflows/basic_bitcoin.yml index 81bb57aeb..4733e1fa0 100644 --- a/.github/workflows/basic_bitcoin.yml +++ b/.github/workflows/basic_bitcoin.yml @@ -32,7 +32,7 @@ jobs: context: motoko/basic_bitcoin push: false load: true - tags: basic-bitcoin-launcher:latest + tags: icp-cli-network-launcher-bitcoin:latest cache-from: type=gha cache-to: type=gha,mode=max - name: Deploy and test diff --git a/motoko/basic_bitcoin/Makefile b/motoko/basic_bitcoin/Makefile index c2c95db80..16e6b6dc6 100644 --- a/motoko/basic_bitcoin/Makefile +++ b/motoko/basic_bitcoin/Makefile @@ -1,4 +1,4 @@ -IMAGE_NAME = basic-bitcoin-launcher +IMAGE_NAME = icp-cli-network-launcher-bitcoin # Find the running container built from our custom image BITCOIN_CONTAINER = $(shell docker ps --filter "ancestor=$(IMAGE_NAME)" --format "{{.ID}}" | head -1) @@ -51,9 +51,9 @@ test: echo "$$result" | grep -qE '[1-9]' && \ echo "PASS" || (echo "FAIL" && exit 1) - @echo "=== Test 6: get_utxos returns utxos after mining ===" + @echo "=== Test 6: get_utxos returns synced chain state after mining ===" @addr=$$(icp canister call backend get_p2pkh_address '()' | grep -o '"[^"]*"' | tr -d '"') && \ result=$$(icp canister call backend get_utxos "(\"$$addr\")") && \ echo "$$result" && \ - echo "$$result" | grep -q 'utxos' && \ + echo "$$result" | grep -q 'tip_height = 101' && \ echo "PASS" || (echo "FAIL" && exit 1) From ff7ce572dc9cb8ad51f1b88973129a038555c740 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 12:45:48 +0200 Subject: [PATCH 16/21] fix: update image name in icp.yaml Co-Authored-By: Claude Sonnet 4.6 --- motoko/basic_bitcoin/icp.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/motoko/basic_bitcoin/icp.yaml b/motoko/basic_bitcoin/icp.yaml index 3ee4f02cb..cf1e2e6fd 100644 --- a/motoko/basic_bitcoin/icp.yaml +++ b/motoko/basic_bitcoin/icp.yaml @@ -6,7 +6,7 @@ canisters: networks: - name: local mode: managed - image: basic-bitcoin-launcher + image: icp-cli-network-launcher-bitcoin port-mapping: - 0:4943 # IC gateway (dynamic) From 13119ca32e5a25f53930e3438a9501d4f2ec0183 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 13:22:10 +0200 Subject: [PATCH 17/21] feat: add P2WPKH address, get_block_headers; rename mock signers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - P2WPKH address generation (adapted from mo:bitcoin PR#9) Send function pending BIP143 sighash support in mo:bitcoin - get_block_headers via IC management canister - ecdsa_mock_signer → mock_sign_with_ecdsa (matches Rust naming) - schnorr_mock_signer → mock_sign_with_schnorr - Improve fee estimation comments to match Rust clarity Co-Authored-By: Claude Sonnet 4.6 --- motoko/basic_bitcoin/backend/P2pkh.mo | 6 +- motoko/basic_bitcoin/backend/P2tr.mo | 6 +- motoko/basic_bitcoin/backend/P2wpkh.mo | 62 +++++++++++ motoko/basic_bitcoin/backend/Types.mo | 2 + motoko/basic_bitcoin/backend/Utils.mo | 12 +- motoko/basic_bitcoin/backend/app.mo | 38 +++++++ motoko/basic_bitcoin/mops.lock | 145 ++++++++----------------- motoko/basic_bitcoin/mops.toml | 7 +- 8 files changed, 163 insertions(+), 115 deletions(-) create mode 100644 motoko/basic_bitcoin/backend/P2wpkh.mo diff --git a/motoko/basic_bitcoin/backend/P2pkh.mo b/motoko/basic_bitcoin/backend/P2pkh.mo index fefd2c36b..36f51765b 100644 --- a/motoko/basic_bitcoin/backend/P2pkh.mo +++ b/motoko/basic_bitcoin/backend/P2pkh.mo @@ -120,15 +120,15 @@ module { 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. + // Sign the transaction. We only care about the size of the signed + // transaction for fee estimation, so we use a mock signer here for efficiency. let signed_transaction_bytes = await sign_transaction( own_public_key, own_address, transaction, "", // mock key name [], // mock derivation path - Utils.ecdsa_mock_signer, + Utils.mock_sign_with_ecdsa, ); let signed_tx_bytes_len : Nat = signed_transaction_bytes.size(); diff --git a/motoko/basic_bitcoin/backend/P2tr.mo b/motoko/basic_bitcoin/backend/P2tr.mo index 54ac70ee5..08999e66f 100644 --- a/motoko/basic_bitcoin/backend/P2tr.mo +++ b/motoko/basic_bitcoin/backend/P2tr.mo @@ -116,8 +116,8 @@ module { }, ); - // 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. + // Sign the transaction. We only care about the size of the signed + // transaction for fee estimation, so we use a mock signer here for efficiency. let signed_transaction_bytes = await sign_key_spend_transaction( own_address, transaction, @@ -125,7 +125,7 @@ module { "", // mock key name [], // mock derivation path null, // mock aux - Utils.schnorr_mock_signer, + Utils.mock_sign_with_schnorr, ); let signed_tx_bytes_len : Nat = signed_transaction_bytes.size(); diff --git a/motoko/basic_bitcoin/backend/P2wpkh.mo b/motoko/basic_bitcoin/backend/P2wpkh.mo new file mode 100644 index 000000000..7d3b26ac8 --- /dev/null +++ b/motoko/basic_bitcoin/backend/P2wpkh.mo @@ -0,0 +1,62 @@ +//! P2WPKH (Pay-to-Witness-Public-Key-Hash) address generation. +//! +//! Adapted from https://github.com/caffeinelabs/motoko-bitcoin/pull/9 +//! +//! Note: send_from_p2wpkh_address is not yet implemented — it requires BIP143 +//! sighash support in mo:bitcoin (see https://github.com/caffeinelabs/motoko-bitcoin/pull/9). + +import Array "mo:core/Array"; +import Blob "mo:core/Blob"; +import Nat "mo:core/Nat"; +import Nat8 "mo:core/Nat8"; +import Runtime "mo:core/Runtime"; + +import Segwit "mo:bitcoin/Segwit"; +import Hash "mo:bitcoin/Hash"; + +import EcdsaApi "EcdsaApi"; +import Types "Types"; + +module { + type Network = Types.Network; + type BitcoinAddress = Types.BitcoinAddress; + + /// Returns the P2WPKH (SegWit v0) address for the given derivation path. + public func get_address(network : Network, key_name : Text, derivation_path : [[Nat8]]) : async BitcoinAddress { + // Fetch the compressed SEC1 public key for the given derivation path. + let public_key_bytes = (await EcdsaApi.ecdsa_public_key(key_name, derivation_path.map(Blob.fromArray))).toArray(); + + // Derive the P2WPKH address from the compressed public key. + public_key_to_p2wpkh_address(network, public_key_bytes); + }; + + /// Derives a P2WPKH Bech32 address from a compressed SEC1 public key (33 bytes). + func public_key_to_p2wpkh_address(network : Network, public_key_bytes : [Nat8]) : BitcoinAddress { + if (public_key_bytes.size() != 33) { + Runtime.trap("P2WPKH requires a compressed public key (33 bytes), got " # public_key_bytes.size().toText()); + }; + + // Compute HASH160(pubkey) = RIPEMD160(SHA256(pubkey)) + let pub_key_hash : [Nat8] = Hash.hash160(public_key_bytes); + if (pub_key_hash.size() != 20) { + Runtime.trap("Internal error: HASH160 result is not 20 bytes"); + }; + + let hrp = switch (network) { + case (#mainnet) "bc"; + case (#testnet) "tb"; + case (#regtest) "bcrt"; + }; + + let witness_program : Segwit.WitnessProgram = { + version = 0; + program = pub_key_hash; + }; + + switch (Segwit.encode(hrp, witness_program)) { + case (#ok address) address; + case (#err msg) Runtime.trap("Error encoding P2WPKH segwit address: " # msg); + }; + }; + +}; diff --git a/motoko/basic_bitcoin/backend/Types.mo b/motoko/basic_bitcoin/backend/Types.mo index 1be14cc9c..4732b5c17 100644 --- a/motoko/basic_bitcoin/backend/Types.mo +++ b/motoko/basic_bitcoin/backend/Types.mo @@ -15,6 +15,8 @@ module Types { public type BitcoinAddress = IC.BitcoinAddress; + public type P2WpkhAddress = Text; + public let CURVE = Curves.secp256k1; /// Lowercase variant Network type including #regtest for local development. diff --git a/motoko/basic_bitcoin/backend/Utils.mo b/motoko/basic_bitcoin/backend/Utils.mo index 79732739d..b5cb6113c 100644 --- a/motoko/basic_bitcoin/backend/Utils.mo +++ b/motoko/basic_bitcoin/backend/Utils.mo @@ -74,13 +74,17 @@ module { bytes.vals().map(func(n : Nat8) : Text { nat8ToText(n) }).join(""); }; - /// A mock for rubber-stamping 64B ECDSA signatures. - public func ecdsa_mock_signer(_key_name : Text, _derivation_path : [Blob], _message_hash : Blob) : async Blob { + /// Mock ECDSA signer that returns a 64-byte placeholder signature. + /// Used only during fee estimation to get the correct transaction size + /// before invoking the real (expensive) threshold signing operation. + public func mock_sign_with_ecdsa(_key_name : Text, _derivation_path : [Blob], _message_hash : Blob) : async Blob { Blob.fromArray(Array.repeat(255 : Nat8, 64)); }; - /// A mock for rubber-stamping 64B Schnorr signatures. - public func schnorr_mock_signer(_key_name : Text, _derivation_path : [Blob], _message_hash : Blob, _aux : ?SchnorrAux) : async Blob { + /// Mock Schnorr signer that returns a 64-byte placeholder signature. + /// Used only during fee estimation to get the correct transaction size + /// before invoking the real (expensive) threshold signing operation. + public func mock_sign_with_schnorr(_key_name : Text, _derivation_path : [Blob], _message_hash : Blob, _aux : ?SchnorrAux) : async Blob { Blob.fromArray(Array.repeat(255 : Nat8, 64)); }; }; diff --git a/motoko/basic_bitcoin/backend/app.mo b/motoko/basic_bitcoin/backend/app.mo index 05ce5e863..02789701f 100644 --- a/motoko/basic_bitcoin/backend/app.mo +++ b/motoko/basic_bitcoin/backend/app.mo @@ -5,6 +5,7 @@ import BitcoinApi "BitcoinApi"; import P2pkh "P2pkh"; import P2trKeyOnly "P2trKeyOnly"; import P2tr "P2tr"; +import P2wpkh "P2wpkh"; import Types "Types"; import Utils "Utils"; @@ -71,10 +72,47 @@ actor class BasicBitcoin(network : Types.Network) { Utils.bytesToText(await P2tr.send_script_path(NETWORK, p2trDerivationPaths(), KEY_NAME, request.destination_address, request.amount_in_satoshi)); }; + /// Returns the P2WPKH (SegWit v0) address of this canister. + /// Note: send_from_p2wpkh_address is not yet implemented — it requires BIP143 + /// sighash support in mo:bitcoin (see https://github.com/caffeinelabs/motoko-bitcoin/pull/9). + public func get_p2wpkh_address() : async Types.BitcoinAddress { + await P2wpkh.get_address(NETWORK, KEY_NAME, p2wpkhDerivationPath()); + }; + + /// Returns Bitcoin block headers starting at `start_height`. + /// Optionally limit to `end_height` (inclusive). + public func get_block_headers(start_height : Nat32, end_height : ?Nat32) : async { + tip_height : Nat32; + block_headers : [Blob]; + } { + // Inline actor to pass full Network type including #regtest. + // mo:ic@4.0.0 defines BitcoinNetwork as { #mainnet; #testnet } only — passing + // #regtest through it would be a type error. + let management_actor : actor { + bitcoin_get_block_headers : { + network : Types.Network; + start_height : Nat32; + end_height : ?Nat32; + } -> async { + tip_height : Nat32; + block_headers : [Blob]; + }; + } = actor ("aaaaa-aa"); + await management_actor.bitcoin_get_block_headers({ + network = NETWORK; + start_height; + end_height; + }); + }; + func p2pkhDerivationPath() : [[Nat8]] { derivationPathWithSuffix("p2pkh"); }; + func p2wpkhDerivationPath() : [[Nat8]] { + derivationPathWithSuffix("p2wpkh"); + }; + func p2trKeyOnlyDerivationPath() : [[Nat8]] { derivationPathWithSuffix("p2tr_key_only"); }; diff --git a/motoko/basic_bitcoin/mops.lock b/motoko/basic_bitcoin/mops.lock index a81311db1..cf55451fd 100644 --- a/motoko/basic_bitcoin/mops.lock +++ b/motoko/basic_bitcoin/mops.lock @@ -1,11 +1,10 @@ { "version": 3, - "mopsTomlDepsHash": "bbea2f0a867714d2e59626a02fe05b37f6c0e4b02db9003c94f2f090fa565246", + "mopsTomlDepsHash": "c3303186871a047ea88cfff4faa6666a4310a22fcdefcc52427de77ba048307d", "deps": { "core": "2.5.0", - "bitcoin": "0.1.0", - "base": "0.12.1", - "sha2": "0.1.0", + "bitcoin": "0.2.0", + "sha2": "0.1.14", "ic": "4.0.0" }, "hashes": { @@ -68,103 +67,49 @@ "core@2.5.0/src/pure/RealTimeQueue.mo": "af133ae61c69ce4e00db5e9db53ddeccad84373fef82a156c85360c2426349dd", "core@2.5.0/src/pure/Set.mo": "f0155b1d548cbc8889fcff75ef52f0cab7998b034621e4faac8d3b79d7bbac07" }, - "bitcoin@0.1.0": { - "bitcoin@0.1.0/NOTICE": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "bitcoin@0.1.0/mops.toml": "682b5879b8f7598261ea1112babc3fe88a7cc93e364f5578eb06e877af85e21b", - "bitcoin@0.1.0/LICENSE": "c40ad81ae283a698516dbff4959219d6ba96cbec1c545647af1f11f715a73d2a", - "bitcoin@0.1.0/README.md": "d32949323e6d041db192350683e55df0da1831e2766762a11403d0e56203e418", - "bitcoin@0.1.0/src/Bech32.mo": "61c21fd8a155cdce6309fe0631c18caa62925410a85b2c14acbea54a868c2d94", - "bitcoin@0.1.0/src/Base58.mo": "e75ffca71bc273f2273f7b495e6935ba247ab39e889f419a7051267b32edc9a3", - "bitcoin@0.1.0/src/Base58Check.mo": "2514513f3792052614261b2323136269713ac50b0185956010f4973564cbf4b3", - "bitcoin@0.1.0/src/Bip32.mo": "75299c2574918cdac0cf0e175950dc0407f967d5150cc64527ed96c0ca332556", - "bitcoin@0.1.0/src/Common.mo": "9bdad7d03703a9486131567222b3f68571afda5eb9ab5897193496fc86085f1f", - "bitcoin@0.1.0/src/ByteUtils.mo": "e3c1568fb8fa5d0eaa5476742d1a0666d3209444aa5b057929a4707975987d36", - "bitcoin@0.1.0/src/Hash.mo": "2beead1ba39f99d3701df642eadf7f84fe4c4d09ddd44d35b28dc0f67b2dcde9", - "bitcoin@0.1.0/src/Hmac.mo": "a9205f0e6e02e76a99c68dada8cada8bd88a02dd3989e7e61985a1da83dba811", - "bitcoin@0.1.0/src/Ripemd160.mo": "b4bceb913683032d3620ba2452fbf7ffa78b4b2c9f6fbaa1b7ff0b1c2cce7dd9", - "bitcoin@0.1.0/src/Segwit.mo": "423b2c8c26bd9f043b6a71f2c558f7ebdd2806cb5bc77557466e71dc4a3ed165", - "bitcoin@0.1.0/src/bitcoin/Address.mo": "dee618f43508553eb167f5df4301e89d74d6fa5a1fec1fdaa8c2923d42242430", - "bitcoin@0.1.0/src/bitcoin/Bitcoin.mo": "5d143e84f58e9f3d17dcd2f090789bb3da4b42f8d59acbdff2169d48d2d120b1", - "bitcoin@0.1.0/src/bitcoin/P2tr.mo": "6d101f25d494eb654896e53eb23708810ddb3a3f57e887e0aa5b6c7afd836a01", - "bitcoin@0.1.0/src/bitcoin/P2pkh.mo": "58747fc30885094df81cd1f69c668e8192da23ce46f42195de4c5fcee9004694", - "bitcoin@0.1.0/src/bitcoin/Transaction.mo": "92cbda65c07e1316e6c0d352bfde28c00009636033e20f52a4263ced89236fed", - "bitcoin@0.1.0/src/bitcoin/Script.mo": "d719f847263e7dc1f17cef81a65dac022d47bd4ec45a98b2e3eaa32ef5798ebb", - "bitcoin@0.1.0/src/bitcoin/TxInput.mo": "bcacaf667bc9fd6c4c337c54b3f132fb281a1701f7b3950230214839d91d7aeb", - "bitcoin@0.1.0/src/bitcoin/Types.mo": "093cdd2376e381907aa2d76b6222d2a50b323a1f01b6b549030ae3fcb1208904", - "bitcoin@0.1.0/src/bitcoin/TxOutput.mo": "83317ecfb44b656dc8e2d96d223732bfbb8facc91e8932f3699d07a5d180ba41", - "bitcoin@0.1.0/src/bitcoin/Wif.mo": "9beea06a70ef7837afbc43fe19496d116bdf5c4244b02b2c4c12bd3f82a1c444", - "bitcoin@0.1.0/src/bitcoin/Witness.mo": "53716b0e99d035f1324e9328dc26d232a300a0085770953f186dde76dee5c853", - "bitcoin@0.1.0/src/ec/Curves.mo": "d3cc1b65a81a0e9397fa3e1545b5ee94de6a9ebaf794b260524b8edef63577d8", - "bitcoin@0.1.0/src/ec/Affine.mo": "a45a89f4f770c1e0dc58cc625cece12c3008e4810d11b99b82d03a1be47f1992", - "bitcoin@0.1.0/src/ec/Field.mo": "42f95c74571ef0b112ee21a282db6953da7ab7a834d6149bc33359ae4e3c706a", - "bitcoin@0.1.0/src/ec/Fp.mo": "246c531b1e7119ed9363fb21a1b4a468132a048ecab2ebc0654bd401ebd52b4a", - "bitcoin@0.1.0/src/ec/Numbers.mo": "df5edb25185cc54331d6fff4cade036be95f17b7c63c8028fc2ca79ce664e1c3", - "bitcoin@0.1.0/src/ec/Jacobi.mo": "bd5606620e43855bd04a624909a4cba7bf3d376fc74d83924f2035e32537d837", - "bitcoin@0.1.0/src/ecdsa/Der.mo": "faf90b16c7ac785f030e27e0abd5da9d92ca1de619b0e113c1b750be50d1886c", - "bitcoin@0.1.0/src/ecdsa/Publickey.mo": "a07e0d1d39b8f44b2a91e90ced1dbccd5189f112f45cd39db091c8cf4e775e7a", - "bitcoin@0.1.0/src/ecdsa/Ecdsa.mo": "0247bc2b2dac95c1b97d8bf56fbed47e5954269c925def118103f0f85f3a71d1", - "bitcoin@0.1.0/src/ecdsa/Types.mo": "72fd67985eb93f53ee136c06e7fc24042929378955c52088294744e29c39b0c9" + "bitcoin@0.2.0": { + "bitcoin@0.2.0/src/Bech32.mo": "7a10cc7ebac1acff0ab90398e47950d10b3d3ee5fdd0b57cd63fdd79730769de", + "bitcoin@0.2.0/README.md": "c0f4ed402a85115747fae4bc1f0e754473f2e659e1c4c62a54c9b8f70d8ee46b", + "bitcoin@0.2.0/src/Bip32.mo": "a1e40eef536223e4e2230d92e7022adb0a9fb2124e67aace4694fc77149c614e", + "bitcoin@0.2.0/mops.toml": "6a02223072abf7a78a50f597adb4c68e1e677da8b2c726176bb7d47555ea3998", + "bitcoin@0.2.0/src/Base58Check.mo": "aead16dbdfcf5d4666fe761378972631bdb1a25ae03294d716a6224ae3bc5739", + "bitcoin@0.2.0/src/Base58.mo": "37f1909f82405ef545039f99f22d0991913e7cd587baaf8e979d2bee94d1277a", + "bitcoin@0.2.0/LICENSE": "c40ad81ae283a698516dbff4959219d6ba96cbec1c545647af1f11f715a73d2a", + "bitcoin@0.2.0/src/Hmac.mo": "ea7d70defbb7e5631abb81ef8df184362eda8281985448713095baff7f20cb2a", + "bitcoin@0.2.0/src/Hash.mo": "3fb04f66fc1e379e3c6f68eb48f18eae287dbf15e72ff8a45a1aed0a1f8e4908", + "bitcoin@0.2.0/src/ByteUtils.mo": "930ea851001cc588a34c22ce2fe52f04e1604691bea4f624565fb83df853173c", + "bitcoin@0.2.0/src/Ripemd160.mo": "fadc5c8864bca6acea8a5849ce6de595f33c3a8c7aee4763688afef4d36c571c", + "bitcoin@0.2.0/src/Common.mo": "c77221da08103dd6ba593a7c9c00996a8ce61f5514b7868926e72f541a57ce6d", + "bitcoin@0.2.0/src/Segwit.mo": "48119a682fa7b909f8fb0c291e460267f7753ce3c75a48f51cc77f21e109d2c4", + "bitcoin@0.2.0/src/bitcoin/Address.mo": "41f5269c035e63a1009959636e9382d157010de81e89e56446c79e3cfa55fc26", + "bitcoin@0.2.0/src/bitcoin/Bitcoin.mo": "97cd6b7e3c84661ce30706ec7d3f598a5197a3c0f276526a0551505cd6a40e9a", + "bitcoin@0.2.0/src/bitcoin/TxInput.mo": "229305f926e29689da2c790886532d1cfe6c721baeec647c298eef00ea1816f7", + "bitcoin@0.2.0/src/bitcoin/P2tr.mo": "ab896315c399414f08ac6e0ecf7e534afa2cfbc8657476c915cc5d2a77fd044f", + "bitcoin@0.2.0/src/bitcoin/TxOutput.mo": "1a5d405a0d7d69d1cdb8f592a1ff7bcc3e0728b3bf443bda5ffe9bea6e7bc3d4", + "bitcoin@0.2.0/src/bitcoin/Script.mo": "63bc20d19b3ac10e4563c50fa2aea5a3fe78160168da2123571cb03c2d7a94a5", + "bitcoin@0.2.0/src/bitcoin/P2pkh.mo": "38e4914a83b02d67aeb4ef2412fcf6bc9949d79ad22a59258d773719fad056bb", + "bitcoin@0.2.0/src/bitcoin/Wif.mo": "a64c0f625f1febde197a70e8c89c2d5e04bd5f84c1cc90281b963b8b4adea129", + "bitcoin@0.2.0/src/bitcoin/Types.mo": "093cdd2376e381907aa2d76b6222d2a50b323a1f01b6b549030ae3fcb1208904", + "bitcoin@0.2.0/src/bitcoin/Transaction.mo": "3f50861d8cb2b1d026205c235a6fb6026e903cd98b8184bf7685cf43f08ba899", + "bitcoin@0.2.0/src/ec/Jacobi.mo": "b86245b857f6ec8690a22e2af3f37b5d1780fced1e1a00e7636527c6fda07f6a", + "bitcoin@0.2.0/src/ec/Curves.mo": "d851c340c03626916eee160d51bf1a9e43c29ab687f3868fb581a6113c89f2d2", + "bitcoin@0.2.0/src/ec/Numbers.mo": "707971332d4fd91d3d7b8cce9123440f1a91ed1574b87b976343404e8723e830", + "bitcoin@0.2.0/src/ec/Affine.mo": "43b89261a260ae6ef0d4f3d38fd7812fd37d44530d939f5bf1bb7bea242d2508", + "bitcoin@0.2.0/src/bitcoin/Witness.mo": "7f2121c689a6874b7aa0cb9400817ded5387e1834d691998c89762a0a03a0280", + "bitcoin@0.2.0/src/ec/Field.mo": "9baef5e998431d8a58ef10814c2428ac22c51b9f26344d458649edabf65a5609", + "bitcoin@0.2.0/src/ec/Fp.mo": "c23a7d831863601f2de0d7e96d9785adb82a26c81f47a90ec3b7f339eaba3f87", + "bitcoin@0.2.0/src/ecdsa/Der.mo": "2b0733d8e8f7a891f805a6cd535958e3401211fb0b1c4c51ba5476824653e5ee", + "bitcoin@0.2.0/src/ecdsa/Publickey.mo": "4c3decb9e1ceb918e5256d971094e35c16c42f50d1ce2f845aefe4b1eff0a206", + "bitcoin@0.2.0/src/ecdsa/Types.mo": "c51b6e2f437399b634782bba5533a8825636b0ca68c776b62a1b1e40ffa85a31", + "bitcoin@0.2.0/src/ecdsa/Ecdsa.mo": "6707059ca40a1de8c5d94be023b63807fe1fe7cb0b9b79c702b11ec2288efcf6" }, - "base@0.12.1": { - "base@0.12.1/mops.toml": "118b3be79becd448f8e448734d8319c162f2de3ab86995fa7fe40afce207f145", - "base@0.12.1/NOTICE": "3960a8d25fa5fc909325817b08b36c1146970930ca15b6352f8ea6db803cab47", - "base@0.12.1/src/Blob.mo": "9d4b10667080778bca66cb534f1e0a1477e5247a970170f4ba19e7c58cc4939d", - "base@0.12.1/README.md": "f45244a47229456939321014b550e3360b4f8c4d0fd60aa60bbba7e9eacbc8de", - "base@0.12.1/src/AssocList.mo": "07a92db12c36ba96baf3fb45ac76662407ca5d6cee7aedb01fa8ed6d9ee98cf4", - "base@0.12.1/LICENSE": "166bd8e8cf7790087d1fd18a9fa4d060cc0d0b3e5ab30689aa5f3a59a93386bf", - "base@0.12.1/src/Array.mo": "b8b182ec7522daf79160a00e34a7ea558369c32c2b67a6ed339fa2e56ce60835", - "base@0.12.1/src/Char.mo": "dc34cfe3482b92134e5604629377b4236367f2f31d8a82b81084ca5cadf31c2e", - "base@0.12.1/src/Bool.mo": "65faf01dc6e3609ed2f729b813811506951adcce71c081fc6edcdbae4c8b1169", - "base@0.12.1/src/CertifiedData.mo": "51a205989529f6b4fe788f78be0de2da1cb96f6532aac8accccbf4421cce3d06", - "base@0.12.1/src/Buffer.mo": "7ad75dec25ea37e964c536d6bb86ed90cc48047feedb57c9ded359bea49045f7", - "base@0.12.1/src/Debug.mo": "3bc1b92c7e849ae09bd5bc43397d625c07ce80d1089cfe12cde6e6fee6a6ab7b", - "base@0.12.1/src/Deque.mo": "39da68fb8f19fb497aa3db769ffe2cb740cab54e6d82264052de505edac9b17b", - "base@0.12.1/src/Error.mo": "988e300efcdf69d08338a506b47da2536a376187542a731dcc83b427c711a23d", - "base@0.12.1/src/ExperimentalCycles.mo": "914a997e981431b104ec8ef6c263908738ce88a3858d93cff7d1b3290cd6b68f", - "base@0.12.1/src/ExperimentalStableMemory.mo": "70330ae4d113e15d1d211c8ea229bc0a6fc6c014f5fece58aea0de90c82e4fff", - "base@0.12.1/src/ExperimentalInternetComputer.mo": "4aa908a6d33be0b0ee053056890c10dfc040df327283087e7c2a923ab19b6ca9", - "base@0.12.1/src/HashMap.mo": "639c3377687ef59c22ac8df7ef5b26cf6c9689a356ea1ac9fbe215819db59adf", - "base@0.12.1/src/Func.mo": "bea8bcb92707a255814e0a7d79c238e490860c38001438612948704027f05bec", - "base@0.12.1/src/Float.mo": "499f0b2e4b3b1bd4322eaa6aa34ed3a99b31f837c6b032e5000e2f574bffa908", - "base@0.12.1/src/Hash.mo": "99d7add2ed6e82cf482593ac85edd231d507ef542608cd42fec1cc6d764906e9", - "base@0.12.1/src/Heap.mo": "517b746ca16d80b9b7cf3fc6c2883854d4cb240bd494a888fb65ac02441f769b", - "base@0.12.1/src/Int.mo": "3dc2fe075ca553d92cb8178f3216a591233376ffb591d42a8f08bd86c7b5cea6", - "base@0.12.1/src/Int16.mo": "586617af6cef687bd05cd29cd0c785b366ad5ad2b871151f909de76671b755c3", - "base@0.12.1/src/Int32.mo": "bcb4775ef68e0b2b5b872771abcda8eaef4e0a126562b0877e91b3a1ebaaffd6", - "base@0.12.1/src/Int64.mo": "7cfa15c6a8b777fb5f67e841da3d0960a5ef43c66d46acdf354e8a7dfac57d2d", - "base@0.12.1/src/IterType.mo": "1b6361f1b7ee81d1974719c8464c8c574aaee992c37c3c7b6c39f107a58af01c", - "base@0.12.1/src/Int8.mo": "dfdaf280fe2a1b7524cf82116ccdeea5c70cb27046b8addd3c3d9f387b17f594", - "base@0.12.1/src/Iter.mo": "140291f243dfcfdbedb54a264cce76b7481eeb8767224a41fa9951beaf067b67", - "base@0.12.1/src/List.mo": "d13e77eb05b4edcca55e7cc5ee7ce5018c5a137a7a67708e8eb54967c38673fb", - "base@0.12.1/src/Nat.mo": "d2d24f2b42365b3a54c8bb3cf467b94d1ec7f3271a7d0f671986c04a39a8b844", - "base@0.12.1/src/Nat16.mo": "42483fe26ec90fcfc5d90e82df945ea128d10145dada4ee8f68ee85e62f17841", - "base@0.12.1/src/Nat32.mo": "7919cf0598707ae3628d70caa3bb44c04e3bd940752b85481f4f071cde89b66a", - "base@0.12.1/src/Nat64.mo": "dce2861f9674d6301fb2b98475460e85b3d245537c52e3c7a90fac257e5c96f0", - "base@0.12.1/src/Nat8.mo": "b9266d0b5e284288beeca990963cb7a8c9492d69771c656d0501507a613802ba", - "base@0.12.1/src/None.mo": "b204a4964519117e6fa8ee82da5653376988cf618fc397205dfc8ea58f60d2d1", - "base@0.12.1/src/Option.mo": "72c9ddc183b6248375e0f96efd33881a130a465f97e6a3a553ffe8f16bb5fba7", - "base@0.12.1/src/Order.mo": "d8c76128271612469d052520f10af726556df78688729f9856b62ea292e0f43f", - "base@0.12.1/src/Prelude.mo": "7b4ea84cb683203c68d6135b524ca5587a1b23436d6733e0aaf723d36be4aa85", - "base@0.12.1/src/Principal.mo": "5e50eba952b7de62450264a2ece0af6edadd0b6dd4c8cfd2e432ca66022f70ab", - "base@0.12.1/src/RBTree.mo": "fe2f775b84552e97ab4749b48d8aff6da675c8541b927f697055f34eb4decd2c", - "base@0.12.1/src/Random.mo": "15f6fd50d10a9eb401c3f18f19874fb334fec309e94499a46a0a876037dca266", - "base@0.12.1/src/Region.mo": "6ca3019aed15ad442a3a794000ac245b2bcbd846033966e446dc1a29986dbf0b", - "base@0.12.1/src/Result.mo": "4aa00198ee36ea3b60519f9c350e1c17b14b6ffa005b6d4edcff36a0a206f3af", - "base@0.12.1/src/Stack.mo": "4b2681e9ed562b02840b7a46b58e66b695a725f31b94f6c9952e95e81b703b84", - "base@0.12.1/src/Text.mo": "08c5c0328da359144a6d50552b5c3ca9ccb63c6b859e2b2403abbfd112e09708", - "base@0.12.1/src/Time.mo": "4e2a6126d0a2b0b862735adbde93620a70cb388af25a87c7b853650abd0743ab", - "base@0.12.1/src/Timer.mo": "6d1c5203670163e689484231c1c34add09938621f9f21ce1390d0d8c4e3ec31d", - "base@0.12.1/src/Trie.mo": "63c0fbe6d48808630c7edf51d66140b8b2a43a734e425ab854234126e242cb07", - "base@0.12.1/src/TrieMap.mo": "6d69d887006cd1dff09331473f258141bccb251a04a3a8221b65720a4bf237ac", - "base@0.12.1/src/TrieSet.mo": "29b3314f43fad714fc7b68cc3b0dde00a7d8b02b6431afd7fe12304dc14f9612" - }, - "sha2@0.1.0": { - "sha2@0.1.0/LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4", - "sha2@0.1.0/src/Sha512.mo": "4fbae3f3a1e8f8cbb4816ae093fab814882184cf37fe642de478a8c1d9e0bd5e", - "sha2@0.1.0/NOTICE": "71e037856ba594c2b26f5d97a1f77a907ab2013af8cbb124857ca8b53022f83f", - "sha2@0.1.0/mops.toml": "36a54c05e17a29c276490836732acc5bf8ce89f5f1df4723a2c0837c7c52576f", - "sha2@0.1.0/README.md": "a0ae47017fa3565b3ba7727515673d007a95c54beb7a933588ffb1b1532f9013", - "sha2@0.1.0/src/Sha256.mo": "393a8bd68ee648ee36dfe1d84d36985c2ed1f13e5f40119b00ddb0575a81137b" + "sha2@0.1.14": { + "sha2@0.1.14/mops.toml": "ba73b13715683357925fb004b66e43a0f3fabb5fdce3028f13d12d4a297b3170", + "sha2@0.1.14/README.md": "f06ce7acdfabd793202505e744591c6692b54eeb97f97689bc889fd1926a1de0", + "sha2@0.1.14/src/Sha512.mo": "8b9f809dd637e4490ca7c33a0c6911e4ab853a3c69a8b54304567ff502068559", + "sha2@0.1.14/LICENSE": "0351aa17e901e2f426698e6199237119a18c8ea1674877e0eb4d694ee9c4b938", + "sha2@0.1.14/NOTICE": "2a2979e2cb91a7d903c3377ed8031a92e7fd467fd9ddb005f2ae2042ead1b4f1", + "sha2@0.1.14/src/Sha256.mo": "d7c858eb1c097e9336ace80e754fb45b3c7b8e11884bebce3a228af5b4494c26" }, "ic@4.0.0": { "ic@4.0.0/README.md": "7686aefce0c535d18ee2768e98660575563daa53b00d2c1041170032c232784c", diff --git a/motoko/basic_bitcoin/mops.toml b/motoko/basic_bitcoin/mops.toml index f52183529..6015375df 100644 --- a/motoko/basic_bitcoin/mops.toml +++ b/motoko/basic_bitcoin/mops.toml @@ -3,14 +3,11 @@ moc = "1.9.0" [dependencies] core = "2.5.0" -bitcoin = "0.1.0" +bitcoin = "0.2.0" ic = "4.0.0" [moc] -# M0236: use context dot notation -# M0237: redundant explicit implicit arguments -# M0223: redundant type instantiation -args = ["--default-persistent-actors", "-W=M0236,M0237,M0223"] +args = [ "--default-persistent-actors", "-W=M0236,M0237,M0223" ] [canisters.backend] main = "backend/app.mo" From bf09e6cbe126a7d3b5362bb53176248897e62a13 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 17:07:12 +0200 Subject: [PATCH 18/21] feat: add get_blockchain_info; remove P2WPKH (send not yet implementable) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove get_p2wpkh_address and P2wpkh.mo — send_from_p2wpkh_address requires BIP143 sighash support not yet in mo:bitcoin; commented on PR#9 with details - Add get_blockchain_info via inline management canister actor — returns chain height, block hash, timestamp, difficulty, and UTXO set size Co-Authored-By: Claude Sonnet 4.6 --- motoko/basic_bitcoin/backend/P2wpkh.mo | 62 -------------------------- motoko/basic_bitcoin/backend/app.mo | 36 ++++++++++----- 2 files changed, 24 insertions(+), 74 deletions(-) delete mode 100644 motoko/basic_bitcoin/backend/P2wpkh.mo diff --git a/motoko/basic_bitcoin/backend/P2wpkh.mo b/motoko/basic_bitcoin/backend/P2wpkh.mo deleted file mode 100644 index 7d3b26ac8..000000000 --- a/motoko/basic_bitcoin/backend/P2wpkh.mo +++ /dev/null @@ -1,62 +0,0 @@ -//! P2WPKH (Pay-to-Witness-Public-Key-Hash) address generation. -//! -//! Adapted from https://github.com/caffeinelabs/motoko-bitcoin/pull/9 -//! -//! Note: send_from_p2wpkh_address is not yet implemented — it requires BIP143 -//! sighash support in mo:bitcoin (see https://github.com/caffeinelabs/motoko-bitcoin/pull/9). - -import Array "mo:core/Array"; -import Blob "mo:core/Blob"; -import Nat "mo:core/Nat"; -import Nat8 "mo:core/Nat8"; -import Runtime "mo:core/Runtime"; - -import Segwit "mo:bitcoin/Segwit"; -import Hash "mo:bitcoin/Hash"; - -import EcdsaApi "EcdsaApi"; -import Types "Types"; - -module { - type Network = Types.Network; - type BitcoinAddress = Types.BitcoinAddress; - - /// Returns the P2WPKH (SegWit v0) address for the given derivation path. - public func get_address(network : Network, key_name : Text, derivation_path : [[Nat8]]) : async BitcoinAddress { - // Fetch the compressed SEC1 public key for the given derivation path. - let public_key_bytes = (await EcdsaApi.ecdsa_public_key(key_name, derivation_path.map(Blob.fromArray))).toArray(); - - // Derive the P2WPKH address from the compressed public key. - public_key_to_p2wpkh_address(network, public_key_bytes); - }; - - /// Derives a P2WPKH Bech32 address from a compressed SEC1 public key (33 bytes). - func public_key_to_p2wpkh_address(network : Network, public_key_bytes : [Nat8]) : BitcoinAddress { - if (public_key_bytes.size() != 33) { - Runtime.trap("P2WPKH requires a compressed public key (33 bytes), got " # public_key_bytes.size().toText()); - }; - - // Compute HASH160(pubkey) = RIPEMD160(SHA256(pubkey)) - let pub_key_hash : [Nat8] = Hash.hash160(public_key_bytes); - if (pub_key_hash.size() != 20) { - Runtime.trap("Internal error: HASH160 result is not 20 bytes"); - }; - - let hrp = switch (network) { - case (#mainnet) "bc"; - case (#testnet) "tb"; - case (#regtest) "bcrt"; - }; - - let witness_program : Segwit.WitnessProgram = { - version = 0; - program = pub_key_hash; - }; - - switch (Segwit.encode(hrp, witness_program)) { - case (#ok address) address; - case (#err msg) Runtime.trap("Error encoding P2WPKH segwit address: " # msg); - }; - }; - -}; diff --git a/motoko/basic_bitcoin/backend/app.mo b/motoko/basic_bitcoin/backend/app.mo index 02789701f..0a22d02e0 100644 --- a/motoko/basic_bitcoin/backend/app.mo +++ b/motoko/basic_bitcoin/backend/app.mo @@ -5,7 +5,6 @@ import BitcoinApi "BitcoinApi"; import P2pkh "P2pkh"; import P2trKeyOnly "P2trKeyOnly"; import P2tr "P2tr"; -import P2wpkh "P2wpkh"; import Types "Types"; import Utils "Utils"; @@ -72,13 +71,6 @@ actor class BasicBitcoin(network : Types.Network) { Utils.bytesToText(await P2tr.send_script_path(NETWORK, p2trDerivationPaths(), KEY_NAME, request.destination_address, request.amount_in_satoshi)); }; - /// Returns the P2WPKH (SegWit v0) address of this canister. - /// Note: send_from_p2wpkh_address is not yet implemented — it requires BIP143 - /// sighash support in mo:bitcoin (see https://github.com/caffeinelabs/motoko-bitcoin/pull/9). - public func get_p2wpkh_address() : async Types.BitcoinAddress { - await P2wpkh.get_address(NETWORK, KEY_NAME, p2wpkhDerivationPath()); - }; - /// Returns Bitcoin block headers starting at `start_height`. /// Optionally limit to `end_height` (inclusive). public func get_block_headers(start_height : Nat32, end_height : ?Nat32) : async { @@ -105,12 +97,32 @@ actor class BasicBitcoin(network : Types.Network) { }); }; - func p2pkhDerivationPath() : [[Nat8]] { - derivationPathWithSuffix("p2pkh"); + /// Returns a summary of the current Bitcoin blockchain state tracked by the + /// Bitcoin canister: tip height, tip block hash, timestamp, difficulty, and + /// UTXO set size. + public func get_blockchain_info() : async { + height : Nat32; + block_hash : Blob; + timestamp : Nat32; + difficulty : Nat; + utxos_length : Nat64; + } { + // Inline actor type matches the Bitcoin canister Candid directly. + // The Bitcoin canister is accessed via the management canister principal. + let bitcoin_actor : actor { + get_blockchain_info : () -> async { + height : Nat32; + block_hash : Blob; + timestamp : Nat32; + difficulty : Nat; + utxos_length : Nat64; + }; + } = actor ("aaaaa-aa"); + await bitcoin_actor.get_blockchain_info(); }; - func p2wpkhDerivationPath() : [[Nat8]] { - derivationPathWithSuffix("p2wpkh"); + func p2pkhDerivationPath() : [[Nat8]] { + derivationPathWithSuffix("p2pkh"); }; func p2trKeyOnlyDerivationPath() : [[Nat8]] { From 1e83dde25177d04dcb8477734e800ddeda2bc718 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 17:51:42 +0200 Subject: [PATCH 19/21] refactor: call Bitcoin canister directly; derive KEY_NAME from network MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BitcoinApi.mo: replace management canister proxy with direct Bitcoin canister calls. bitcoinCanister(network) selects the right principal at runtime: #mainnet → ghsi2-tqaaa-aaaan-aaaca-cai #testnet/#regtest → g4xu7-jiaaa-aaaan-aaaaq-cai Actor type mirrors the official Bitcoin canister Candid: https://github.com/dfinity/bitcoin-canister/blob/master/canister/candid.did - get_block_headers and get_blockchain_info now go through BitcoinApi (no inline actor workarounds needed) - app.mo: derive KEY_NAME from NETWORK: #mainnet → 'key_1' (ICP mainnet Bitcoin production key) #testnet/#regtest → 'test_key_1' (ICP mainnet staging + local) - Only ECDSA/Schnorr threshold signing still goes through aaaaa-aa (management canister, as intended) Co-Authored-By: Claude Sonnet 4.6 --- motoko/basic_bitcoin/backend/BitcoinApi.mo | 89 ++++++++++++++++------ motoko/basic_bitcoin/backend/app.mo | 28 +++---- 2 files changed, 76 insertions(+), 41 deletions(-) diff --git a/motoko/basic_bitcoin/backend/BitcoinApi.mo b/motoko/basic_bitcoin/backend/BitcoinApi.mo index d084c2061..a0da5809e 100644 --- a/motoko/basic_bitcoin/backend/BitcoinApi.mo +++ b/motoko/basic_bitcoin/backend/BitcoinApi.mo @@ -7,11 +7,13 @@ module { type Satoshi = Types.Satoshi; type MillisatoshiPerByte = Types.MillisatoshiPerByte; - // Use an inline actor type with the full Network variant (including #regtest). - // mo:ic@4.0.0 defines BitcoinNetwork as { #mainnet; #testnet } only — passing - // #regtest through it would be a type error, and the local Bitcoin subnet - // rejects calls made with the wrong network mode. - type ManagementCanisterActor = actor { + // Actor type matching the official Bitcoin canister Candid interface. + // See: https://github.com/dfinity/bitcoin-canister/blob/master/canister/candid.did + // + // The Bitcoin canister is deployed at two well-known principals: + // - Testnet/Regtest: g4xu7-jiaaa-aaaan-aaaaq-cai + // - Mainnet: ghsi2-tqaaa-aaaan-aaaca-cai + type BitcoinCanister = actor { bitcoin_get_balance : { address : BitcoinAddress; network : Network; @@ -28,20 +30,42 @@ module { network : Network; } -> async [MillisatoshiPerByte]; + bitcoin_get_block_headers : { + network : Network; + start_height : Nat32; + end_height : ?Nat32; + } -> async { + tip_height : Nat32; + block_headers : [Blob]; + }; + bitcoin_send_transaction : { network : Network; transaction : Blob; } -> async (); + + get_blockchain_info : () -> async { + height : Nat32; + block_hash : Blob; + timestamp : Nat32; + difficulty : Nat; + utxos_length : Nat64; + }; }; - let management_canister_actor : ManagementCanisterActor = actor ("aaaaa-aa"); + /// Returns the Bitcoin canister actor for the given network. + func bitcoinCanister(network : Network) : BitcoinCanister { + let id = switch network { + case (#mainnet) "ghsi2-tqaaa-aaaan-aaaca-cai"; + case (#testnet or #regtest) "g4xu7-jiaaa-aaaan-aaaaq-cai"; + }; + actor (id) : BitcoinCanister; + }; /// Returns the balance of the given Bitcoin address. - /// - /// Relies on the `bitcoin_get_balance` endpoint. - /// See https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-bitcoin_get_balance + /// See https://docs.internetcomputer.org/guides/chain-fusion/bitcoin public func get_balance(network : Network, address : BitcoinAddress) : async Satoshi { - await (with cycles = 100_000_000) management_canister_actor.bitcoin_get_balance({ + await (with cycles = 100_000_000) bitcoinCanister(network).bitcoin_get_balance({ address; network; min_confirmations = null; @@ -49,11 +73,9 @@ module { }; /// Returns the UTXOs of the given Bitcoin address. - /// - /// Relies on the `bitcoin_get_utxos` endpoint. - /// See https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-bitcoin_get_utxos + /// See https://docs.internetcomputer.org/guides/chain-fusion/bitcoin public func get_utxos(network : Network, address : BitcoinAddress) : async Types.GetUtxosResponse { - await (with cycles = 10_000_000_000) management_canister_actor.bitcoin_get_utxos({ + await (with cycles = 10_000_000_000) bitcoinCanister(network).bitcoin_get_utxos({ address; network; filter = null; @@ -62,22 +84,43 @@ module { /// Returns the 100 fee percentiles measured in millisatoshi/byte. /// 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 + /// See https://docs.internetcomputer.org/guides/chain-fusion/bitcoin public func get_current_fee_percentiles(network : Network) : async [MillisatoshiPerByte] { - await (with cycles = 100_000_000) management_canister_actor.bitcoin_get_current_fee_percentiles({ + await (with cycles = 100_000_000) bitcoinCanister(network).bitcoin_get_current_fee_percentiles({ + network; + }); + }; + + /// Returns Bitcoin block headers for the given height range. + /// See https://docs.internetcomputer.org/guides/chain-fusion/bitcoin + public func get_block_headers(network : Network, start_height : Nat32, end_height : ?Nat32) : async { + tip_height : Nat32; + block_headers : [Blob]; + } { + await (with cycles = 100_000_000) bitcoinCanister(network).bitcoin_get_block_headers({ network; + start_height; + end_height; }); }; - /// Sends a (signed) transaction to the Bitcoin network. - /// - /// Relies on the `bitcoin_send_transaction` endpoint. - /// See https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-bitcoin_send_transaction + /// Returns a summary of the current Bitcoin blockchain state. + /// See https://docs.internetcomputer.org/guides/chain-fusion/bitcoin + public func get_blockchain_info(network : Network) : async { + height : Nat32; + block_hash : Blob; + timestamp : Nat32; + difficulty : Nat; + utxos_length : Nat64; + } { + await (with cycles = 100_000_000) bitcoinCanister(network).get_blockchain_info(); + }; + + /// Sends a signed Bitcoin transaction to the network. + /// See https://docs.internetcomputer.org/guides/chain-fusion/bitcoin public func send_transaction(network : Network, transaction : [Nat8]) : async () { let cost = 5_000_000_000 + transaction.size() * 20_000_000; - await (with cycles = cost) management_canister_actor.bitcoin_send_transaction({ + await (with cycles = cost) bitcoinCanister(network).bitcoin_send_transaction({ network; transaction = Blob.fromArray(transaction); }); diff --git a/motoko/basic_bitcoin/backend/app.mo b/motoko/basic_bitcoin/backend/app.mo index 0a22d02e0..84016c1cd 100644 --- a/motoko/basic_bitcoin/backend/app.mo +++ b/motoko/basic_bitcoin/backend/app.mo @@ -20,9 +20,13 @@ actor class BasicBitcoin(network : Types.Network) { /// derivation. transient let DERIVATION_PATH : [[Nat8]] = []; - // The ECDSA key name. - // `test_key_1` is available on both the local PocketIC network and the IC testnet. - transient let KEY_NAME : Text = "test_key_1"; + // The ECDSA/Schnorr key name depends on which Bitcoin network this canister targets: + // - "key_1" — Bitcoin mainnet on ICP mainnet + // - "test_key_1" — Bitcoin testnet4 on ICP mainnet (staging) OR local regtest + transient let KEY_NAME : Text = switch NETWORK { + case (#mainnet) "key_1"; + case (#testnet or #regtest) "test_key_1"; + }; /// Returns the balance of the given Bitcoin address. public func get_balance(address : Types.BitcoinAddress) : async Types.Satoshi { @@ -97,9 +101,8 @@ actor class BasicBitcoin(network : Types.Network) { }); }; - /// Returns a summary of the current Bitcoin blockchain state tracked by the - /// Bitcoin canister: tip height, tip block hash, timestamp, difficulty, and - /// UTXO set size. + /// Returns a summary of the current Bitcoin blockchain state: tip height, + /// tip block hash, timestamp, difficulty, and UTXO set size. public func get_blockchain_info() : async { height : Nat32; block_hash : Blob; @@ -107,18 +110,7 @@ actor class BasicBitcoin(network : Types.Network) { difficulty : Nat; utxos_length : Nat64; } { - // Inline actor type matches the Bitcoin canister Candid directly. - // The Bitcoin canister is accessed via the management canister principal. - let bitcoin_actor : actor { - get_blockchain_info : () -> async { - height : Nat32; - block_hash : Blob; - timestamp : Nat32; - difficulty : Nat; - utxos_length : Nat64; - }; - } = actor ("aaaaa-aa"); - await bitcoin_actor.get_blockchain_info(); + await BitcoinApi.get_blockchain_info(NETWORK); }; func p2pkhDerivationPath() : [[Nat8]] { From 287b9da334c32c380094a17e1767f0ebbf4e8d09 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 18:16:59 +0200 Subject: [PATCH 20/21] docs: rewrite README with correct docs URL and current architecture Co-Authored-By: Claude Sonnet 4.6 --- motoko/basic_bitcoin/README.md | 69 +++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/motoko/basic_bitcoin/README.md b/motoko/basic_bitcoin/README.md index 27c9091dd..1100e1d04 100644 --- a/motoko/basic_bitcoin/README.md +++ b/motoko/basic_bitcoin/README.md @@ -1,8 +1,8 @@ # Basic Bitcoin -This example demonstrates how a canister smart contract can send and receive Bitcoin on the Internet Computer. It showcases the ECDSA API, Schnorr API (BIP340/BIP341), and Bitcoin API, supporting three address types: P2PKH, P2TR key-only spend, and P2TR with script path. +This example demonstrates how a canister can send and receive Bitcoin on the Internet Computer using threshold ECDSA and Schnorr signatures. It covers three address types (P2PKH, P2TR key-path, P2TR script-path), querying balances and UTXOs, reading chain state, and sending transactions. -For a deeper understanding of the ICP <> BTC integration, see the [Bitcoin integration documentation](https://internetcomputer.org/docs/current/developer-docs/multi-chain/bitcoin/overview). +For a deeper understanding of the ICP ↔ Bitcoin integration, see the [Bitcoin integration concepts](https://docs.internetcomputer.org/concepts/chain-fusion/bitcoin). ## Build and deploy from the command line @@ -10,7 +10,7 @@ For a deeper understanding of the ICP <> BTC integration, see the [Bitcoin integ - Node.js - icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` -- Docker (for local testing with bitcoind) +- Docker (required for local testing — bundles the IC network launcher + `bitcoind`) ### Install @@ -19,15 +19,15 @@ git clone https://github.com/dfinity/examples cd examples/motoko/basic_bitcoin ``` -### Local deployment (with bitcoind) +### Local deployment -The local environment uses a self-contained Docker image that bundles `bitcoind` in regtest mode alongside the IC network launcher. Build the image first: +The local environment uses a self-contained Docker image (`icp-cli-network-launcher-bitcoin`) that runs `bitcoind` in regtest mode alongside the IC network. Build it once: ```bash make build-image ``` -Then deploy and test: +Then deploy and run tests: ```bash icp network start -d @@ -36,27 +36,31 @@ make test icp network stop ``` -> If tests fail with an out-of-cycles error, run `make topup` to add 30 trillion cycles to the backend canister and retry. +> If tests fail with an out-of-cycles error, run `make topup` and retry. -### Staging deployment (IC testnet) +### Staging (IC mainnet, Bitcoin testnet4) ```bash icp deploy -e staging ``` -### Production deployment (IC mainnet) +### Production (IC mainnet, Bitcoin mainnet) ```bash icp deploy -e production ``` -## Generating Bitcoin addresses +## Environments -Bitcoin has different types of addresses (e.g. P2PKH, P2TR). These addresses can be generated from an ECDSA public key or a Schnorr ([BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki), [BIP341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki)) public key. The example showcases three address types: +| Environment | IC network | Bitcoin network | Key | +|-------------|-----------|----------------|-----| +| `local` | local (PocketIC) | regtest | `test_key_1` | +| `staging` | IC mainnet | testnet4 | `test_key_1` | +| `production` | IC mainnet | mainnet | `key_1` | -1. A [P2PKH address](https://en.bitcoin.it/wiki/Transaction#Pay-to-PubkeyHash) using the ECDSA API. -2. A [P2TR address](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) where funds can be spent using the internal key only (P2TR key path spend with unspendable script tree). -3. A [P2TR address](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) where funds can be spent using either the internal key or a script path key. +## Available functions + +### Address generation ```bash icp canister call backend get_p2pkh_address '()' @@ -64,37 +68,42 @@ icp canister call backend get_p2tr_key_only_address '()' icp canister call backend get_p2tr_address '()' ``` -## Checking balance and sending Bitcoin +### Chain queries ```bash -icp canister call backend get_balance '("YOUR_BITCOIN_ADDRESS")' -icp canister call backend get_utxos '("YOUR_BITCOIN_ADDRESS")' +icp canister call backend get_balance '("YOUR_ADDRESS")' +icp canister call backend get_utxos '("YOUR_ADDRESS")' icp canister call backend get_current_fee_percentiles '()' +icp canister call backend get_block_headers '(0 : nat32, null)' +icp canister call backend get_blockchain_info '()' ``` ### Sending Bitcoin ```bash -icp canister call backend send_from_p2pkh_address '(record { destination_address = "DEST_ADDRESS"; amount_in_satoshi = 4321 })' -icp canister call backend send_from_p2tr_key_only_address '(record { destination_address = "DEST_ADDRESS"; amount_in_satoshi = 4321 })' -icp canister call backend send_from_p2tr_address_key_path '(record { destination_address = "DEST_ADDRESS"; amount_in_satoshi = 4321 })' -icp canister call backend send_from_p2tr_address_script_path '(record { destination_address = "DEST_ADDRESS"; amount_in_satoshi = 4321 })' +icp canister call backend send_from_p2pkh_address \ + '(record { destination_address = "DEST"; amount_in_satoshi = 4321 })' +icp canister call backend send_from_p2tr_key_only_address \ + '(record { destination_address = "DEST"; amount_in_satoshi = 4321 })' +icp canister call backend send_from_p2tr_address_key_path \ + '(record { destination_address = "DEST"; amount_in_satoshi = 4321 })' +icp canister call backend send_from_p2tr_address_script_path \ + '(record { destination_address = "DEST"; amount_in_satoshi = 4321 })' ``` -### Local testing with bitcoind JSON-RPC - -For local testing, the Docker-based network launcher exposes the bitcoind JSON-RPC on port 18443. Mine blocks to a canister address using `curl`: +### Local testing: mine blocks and fund an address ```bash -# Get the P2PKH address +# Get a P2PKH address ADDR=$(icp canister call backend get_p2pkh_address '()' | grep -o '"[^"]*"' | tr -d '"') -# Mine 101 blocks to that address (provides spendable funds) -curl -s -X POST http://ic-btc-integration:ic-btc-integration@localhost:18443 \ - -H 'Content-Type: application/json' \ - -d "{\"jsonrpc\":\"1.0\",\"method\":\"generatetoaddress\",\"params\":[101,\"$ADDR\"]}" +# Mine 101 blocks to that address via the bundled bitcoind +CONTAINER=$(docker ps --filter "ancestor=icp-cli-network-launcher-bitcoin" --format "{{.ID}}" | head -1) +docker exec "$CONTAINER" bitcoin-cli -regtest \ + -rpcuser=ic-btc-integration -rpcpassword=ic-btc-integration \ + generatetoaddress 101 "$ADDR" -# Wait for the IC to sync the blocks, then check balance +# Wait for the IC to sync, then check balance sleep 5 icp canister call backend get_balance "(\"$ADDR\")" ``` From bea4339fa2c343963af6f0c147026043c693c306 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 18:21:22 +0200 Subject: [PATCH 21/21] refactor: clean up Types.mo and comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove NetworkCamelCase and network_to_network_camel_case from Types.mo (implementation detail, not part of public API); inline the PascalCase conversion directly in P2pkh.mo where it is actually used - Remove P2WpkhAddress type (P2WPKH was removed from the example) - Drop PR#22 reference from Network type comment — that issue is closed; simplify to state the factual reason (mo:bitcoin uses PascalCase) - Remove repeated 'See docs URL' doc comments from BitcoinApi.mo — one link in the README is sufficient; per-function links add noise Co-Authored-By: Claude Sonnet 4.6 --- motoko/basic_bitcoin/backend/BitcoinApi.mo | 6 ------ motoko/basic_bitcoin/backend/P2pkh.mo | 8 +++++++- motoko/basic_bitcoin/backend/Types.mo | 18 +++--------------- 3 files changed, 10 insertions(+), 22 deletions(-) diff --git a/motoko/basic_bitcoin/backend/BitcoinApi.mo b/motoko/basic_bitcoin/backend/BitcoinApi.mo index a0da5809e..f1b7ebe49 100644 --- a/motoko/basic_bitcoin/backend/BitcoinApi.mo +++ b/motoko/basic_bitcoin/backend/BitcoinApi.mo @@ -63,7 +63,6 @@ module { }; /// Returns the balance of the given Bitcoin address. - /// See https://docs.internetcomputer.org/guides/chain-fusion/bitcoin public func get_balance(network : Network, address : BitcoinAddress) : async Satoshi { await (with cycles = 100_000_000) bitcoinCanister(network).bitcoin_get_balance({ address; @@ -73,7 +72,6 @@ module { }; /// Returns the UTXOs of the given Bitcoin address. - /// See https://docs.internetcomputer.org/guides/chain-fusion/bitcoin public func get_utxos(network : Network, address : BitcoinAddress) : async Types.GetUtxosResponse { await (with cycles = 10_000_000_000) bitcoinCanister(network).bitcoin_get_utxos({ address; @@ -84,7 +82,6 @@ module { /// Returns the 100 fee percentiles measured in millisatoshi/byte. /// Percentiles are computed from the last 10,000 transactions (if available). - /// See https://docs.internetcomputer.org/guides/chain-fusion/bitcoin public func get_current_fee_percentiles(network : Network) : async [MillisatoshiPerByte] { await (with cycles = 100_000_000) bitcoinCanister(network).bitcoin_get_current_fee_percentiles({ network; @@ -92,7 +89,6 @@ module { }; /// Returns Bitcoin block headers for the given height range. - /// See https://docs.internetcomputer.org/guides/chain-fusion/bitcoin public func get_block_headers(network : Network, start_height : Nat32, end_height : ?Nat32) : async { tip_height : Nat32; block_headers : [Blob]; @@ -105,7 +101,6 @@ module { }; /// Returns a summary of the current Bitcoin blockchain state. - /// See https://docs.internetcomputer.org/guides/chain-fusion/bitcoin public func get_blockchain_info(network : Network) : async { height : Nat32; block_hash : Blob; @@ -117,7 +112,6 @@ module { }; /// Sends a signed Bitcoin transaction to the network. - /// See https://docs.internetcomputer.org/guides/chain-fusion/bitcoin public func send_transaction(network : Network, transaction : [Nat8]) : async () { let cost = 5_000_000_000 + transaction.size() * 20_000_000; await (with cycles = cost) bitcoinCanister(network).bitcoin_send_transaction({ diff --git a/motoko/basic_bitcoin/backend/P2pkh.mo b/motoko/basic_bitcoin/backend/P2pkh.mo index 36f51765b..9e93eb527 100644 --- a/motoko/basic_bitcoin/backend/P2pkh.mo +++ b/motoko/basic_bitcoin/backend/P2pkh.mo @@ -211,7 +211,13 @@ module { let public_key = public_key_bytes_to_public_key(public_key_bytes); // Compute the P2PKH address from our public key. - P2pkh.deriveAddress(Types.network_to_network_camel_case(network), Publickey.toSec1(public_key, true)); + // mo:bitcoin uses PascalCase Network variants internally + let bitcoinNetwork = switch network { + case (#mainnet) #Mainnet; + case (#testnet) #Testnet; + case (#regtest) #Regtest; + }; + P2pkh.deriveAddress(bitcoinNetwork, Publickey.toSec1(public_key, true)); }; func public_key_bytes_to_public_key(public_key_bytes : [Nat8]) : PublicKey { diff --git a/motoko/basic_bitcoin/backend/Types.mo b/motoko/basic_bitcoin/backend/Types.mo index 4732b5c17..72cde0c4f 100644 --- a/motoko/basic_bitcoin/backend/Types.mo +++ b/motoko/basic_bitcoin/backend/Types.mo @@ -15,30 +15,18 @@ module Types { public type BitcoinAddress = IC.BitcoinAddress; - public type P2WpkhAddress = Text; public let CURVE = Curves.secp256k1; - /// Lowercase variant Network type including #regtest for local development. - /// Note: mo:bitcoin uses PascalCase (#Mainnet etc.) — see - /// https://github.com/caffeinelabs/motoko-bitcoin/issues/22 + /// Network type using lowercase variants, matching the Bitcoin canister Candid. + /// mo:bitcoin uses PascalCase internally — a conversion is applied when calling + /// mo:bitcoin address-generation functions. public type Network = { #mainnet; #testnet; #regtest; }; - /// Bridge type for mo:bitcoin address-generation functions (which use PascalCase). - /// Can be removed once mo:bitcoin#22 is fixed. - public type NetworkCamelCase = BitcoinTypes.Network; - - public func network_to_network_camel_case(network : Network) : NetworkCamelCase { - switch (network) { - case (#regtest) #Regtest; - case (#testnet) #Testnet; - case (#mainnet) #Mainnet; - }; - }; public type OutPoint = BitcoinTypes.OutPoint; public type Utxo = BitcoinTypes.Utxo;