diff --git a/.github/workflows/basic_bitcoin.yml b/.github/workflows/basic_bitcoin.yml new file mode 100644 index 000000000..4733e1fa0 --- /dev/null +++ b/.github/workflows/basic_bitcoin.yml @@ -0,0 +1,43 @@ +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: + # 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 + env: + ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - 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 + uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 + with: + context: motoko/basic_bitcoin + push: false + load: true + tags: icp-cli-network-launcher-bitcoin: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 --cycles 30t + 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/Dockerfile b/motoko/basic_bitcoin/Dockerfile new file mode 100644 index 000000000..f63dfeb0a --- /dev/null +++ b/motoko/basic_bitcoin/Dockerfile @@ -0,0 +1,20 @@ +FROM ghcr.io/dfinity/icp-cli-network-launcher:latest + +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 + +ENTRYPOINT ["/app/start.sh"] diff --git a/motoko/basic_bitcoin/Makefile b/motoko/basic_bitcoin/Makefile new file mode 100644 index 000000000..16e6b6dc6 --- /dev/null +++ b/motoko/basic_bitcoin/Makefile @@ -0,0 +1,59 @@ +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) + +.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 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 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 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_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 '"') && \ + 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 + + @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 '[1-9]' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @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 'tip_height = 101' && \ + echo "PASS" || (echo "FAIL" && exit 1) diff --git a/motoko/basic_bitcoin/README.md b/motoko/basic_bitcoin/README.md index e095c567d..1100e1d04 100644 --- a/motoko/basic_bitcoin/README.md +++ b/motoko/basic_bitcoin/README.md @@ -1,154 +1,113 @@ # 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. +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. -## Architecture +For a deeper understanding of the ICP ↔ Bitcoin integration, see the [Bitcoin integration concepts](https://docs.internetcomputer.org/concepts/chain-fusion/bitcoin). -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. +## Build and deploy from the command line -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). +### Prerequisites -## Deploying from ICP Ninja +- Node.js +- icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` +- Docker (required for local testing — bundles the IC network launcher + `bitcoind`) -[![](https://icp.ninja/assets/open.svg)](https://icp.ninja/editor?g=https://github.com/dfinity/examples/tree/master/motoko/basic_bitcoin) +### Install -## Build and deploy from the command-line - -### 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 +### Local deployment -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: +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 -dfx canister --network=ic call basic_bitcoin get_${type}_address +make build-image ``` -* We are generating a Bitcoin testnet address, which can only be -used for sending/receiving Bitcoin on the Bitcoin testnet. - -### Step 2: Receiving bitcoin - -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. +Then deploy and run tests: -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. +```bash +icp network start -d +icp deploy --cycles 30t +make test +icp network stop +``` -### Step 3: Checking your bitcoin balance +> If tests fail with an out-of-cycles error, run `make topup` and retry. -You can check a Bitcoin address's balance by using the `get_balance` endpoint on your canister. +### Staging (IC mainnet, Bitcoin testnet4) -In the Candid UI, paste in your canister's address, and click on "Call". +```bash +icp deploy -e staging +``` -Alternatively, make the call using the command line. Be sure to replace `mheyfRsAQ1XrjtzjfU1cCH2B6G1KmNarNL` with your own generated address: +### Production (IC mainnet, Bitcoin mainnet) ```bash -dfx canister --network=ic call basic_bitcoin get_balance '("mheyfRsAQ1XrjtzjfU1cCH2B6G1KmNarNL")' +icp deploy -e production ``` -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 +## Environments -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]`. +| 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` | -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. +## Available functions -Via the command line, the same call would look like this: +### Address generation ```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_p2pkh_address '()' +icp canister call backend get_p2tr_key_only_address '()' +icp canister call backend get_p2tr_address '()' ``` -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. +### Chain queries -### 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": +```bash +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 '()' +``` -Alternatively, make the call using the command line. Be sure to replace `10` with your desired start height: +### Sending Bitcoin ```bash -dfx canister --network=ic call basic_bitcoin get_block_headers "(10: nat32)" +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 })' ``` -or replace `0` and `11` with your desired start and end height respectively: + +### Local testing: mine blocks and fund an address + ```bash -dfx canister --network=ic call basic_bitcoin get_block_headers "(0: nat32, 11: nat32)" +# Get a P2PKH address +ADDR=$(icp canister call backend get_p2pkh_address '()' | grep -o '"[^"]*"' | tr -d '"') + +# 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, 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 app. diff --git a/motoko/basic_bitcoin/backend/BitcoinApi.mo b/motoko/basic_bitcoin/backend/BitcoinApi.mo new file mode 100644 index 000000000..f1b7ebe49 --- /dev/null +++ b/motoko/basic_bitcoin/backend/BitcoinApi.mo @@ -0,0 +1,122 @@ +import Types "Types"; +import Blob "mo:core/Blob"; + +module { + type Network = Types.Network; + type BitcoinAddress = Types.BitcoinAddress; + type Satoshi = Types.Satoshi; + type MillisatoshiPerByte = Types.MillisatoshiPerByte; + + // 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; + min_confirmations : ?Nat32; + } -> async Satoshi; + + bitcoin_get_utxos : { + address : BitcoinAddress; + network : Network; + filter : ?Types.UtxosFilter; + } -> async Types.GetUtxosResponse; + + bitcoin_get_current_fee_percentiles : { + 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; + }; + }; + + /// 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. + public func get_balance(network : Network, address : BitcoinAddress) : async Satoshi { + await (with cycles = 100_000_000) bitcoinCanister(network).bitcoin_get_balance({ + address; + network; + min_confirmations = null; + }); + }; + + /// Returns the UTXOs of the given Bitcoin address. + public func get_utxos(network : Network, address : BitcoinAddress) : async Types.GetUtxosResponse { + await (with cycles = 10_000_000_000) bitcoinCanister(network).bitcoin_get_utxos({ + address; + network; + filter = null; + }); + }; + + /// Returns the 100 fee percentiles measured in millisatoshi/byte. + /// Percentiles are computed from the last 10,000 transactions (if available). + public func get_current_fee_percentiles(network : Network) : async [MillisatoshiPerByte] { + await (with cycles = 100_000_000) bitcoinCanister(network).bitcoin_get_current_fee_percentiles({ + network; + }); + }; + + /// Returns Bitcoin block headers for the given height range. + 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; + }); + }; + + /// Returns a summary of the current Bitcoin blockchain state. + 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. + 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({ + network; + transaction = Blob.fromArray(transaction); + }); + }; +} diff --git a/motoko/basic_bitcoin/backend/EcdsaApi.mo b/motoko/basic_bitcoin/backend/EcdsaApi.mo new file mode 100644 index 000000000..ef0c571ae --- /dev/null +++ b/motoko/basic_bitcoin/backend/EcdsaApi.mo @@ -0,0 +1,37 @@ +import Types "Types"; +import { ic } "mo:ic"; + +module { + + // The fee for the `sign_with_ecdsa` endpoint using the test key. + 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 { + // Retrieve the public key of this canister at derivation path + // from the ECDSA API. + let res = await ic.ecdsa_public_key({ + canister_id = null; + derivation_path; + key_id = { + curve = #secp256k1; + name = key_name; + }; + }); + + res.public_key; + }; + + 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 = { + curve = #secp256k1; + name = key_name; + }; + }); + + res.signature; + }; +}; diff --git a/motoko/basic_bitcoin/src/basic_bitcoin/src/P2pkh.mo b/motoko/basic_bitcoin/backend/P2pkh.mo similarity index 82% rename from motoko/basic_bitcoin/src/basic_bitcoin/src/P2pkh.mo rename to motoko/basic_bitcoin/backend/P2pkh.mo index d99e8bb53..9e93eb527 100644 --- a/motoko/basic_bitcoin/src/basic_bitcoin/src/P2pkh.mo +++ b/motoko/basic_bitcoin/backend/P2pkh.mo @@ -36,20 +36,19 @@ 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; 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,11 +57,11 @@ 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); - 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) @@ -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,13 +97,12 @@ 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], 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"); @@ -122,16 +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( - ecdsa_canister_actor, 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(); @@ -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. @@ -215,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/src/basic_bitcoin/src/P2tr.mo b/motoko/basic_bitcoin/backend/P2tr.mo similarity index 83% rename from motoko/basic_bitcoin/src/basic_bitcoin/src/P2tr.mo rename to motoko/basic_bitcoin/backend/P2tr.mo index e4d54e965..08999e66f 100644 --- a/motoko/basic_bitcoin/src/basic_bitcoin/src/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"; @@ -46,16 +48,15 @@ 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 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,12 +83,11 @@ 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, 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"); @@ -116,17 +116,16 @@ 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( - schnorr_canister_actor, own_address, transaction, amounts, "", // 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(); @@ -148,13 +147,12 @@ 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], 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 @@ -171,7 +169,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 +189,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 +226,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 +240,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,17 +251,17 @@ 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 : ?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); - 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) @@ -281,7 +278,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 +293,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,11 +307,11 @@ 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); - 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) @@ -325,8 +322,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 +335,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 +352,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 +412,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 71% rename from motoko/basic_bitcoin/src/basic_bitcoin/src/P2trKeyOnly.mo rename to motoko/basic_bitcoin/backend/P2trKeyOnly.mo index 73c98f2d7..6f45fe130 100644 --- a/motoko/basic_bitcoin/src/basic_bitcoin/src/P2trKeyOnly.mo +++ b/motoko/basic_bitcoin/backend/P2trKeyOnly.mo @@ -26,29 +26,28 @@ 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 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/backend/SchnorrApi.mo b/motoko/basic_bitcoin/backend/SchnorrApi.mo new file mode 100644 index 000000000..bbc94e5aa --- /dev/null +++ b/motoko/basic_bitcoin/backend/SchnorrApi.mo @@ -0,0 +1,40 @@ +import Types "Types"; +import { ic } "mo:ic"; +import IC "mo:ic/Types"; + +module { + type SchnorrAux = IC.SchnorrAux; + + // The fee for the `sign_with_schnorr` endpoint using the test key. + 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 { + // Retrieve the public key of this canister at derivation path + // from the Schnorr API. + let res = await ic.schnorr_public_key({ + canister_id = null; + derivation_path; + key_id = { + algorithm = #bip340secp256k1; + name = key_name; + }; + }); + + res.public_key; + }; + + 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 = { + algorithm = #bip340secp256k1; + name = key_name; + }; + aux; + }); + + res.signature; + }; +}; diff --git a/motoko/basic_bitcoin/backend/Types.mo b/motoko/basic_bitcoin/backend/Types.mo new file mode 100644 index 000000000..72cde0c4f --- /dev/null +++ b/motoko/basic_bitcoin/backend/Types.mo @@ -0,0 +1,51 @@ +import Curves "mo:bitcoin/ec/Curves"; +import BitcoinTypes "mo:bitcoin/bitcoin/Types"; +import IC "mo:ic/Types"; + +module Types { + public type SendRequest = { + destination_address : Text; + amount_in_satoshi : Satoshi; + }; + + public type Satoshi = BitcoinTypes.Satoshi; + + /// 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; + + /// 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; + }; + + + public type OutPoint = BitcoinTypes.OutPoint; + public type Utxo = BitcoinTypes.Utxo; + + /// UTXO filter — matches the IC management canister's inline filter type exactly. + public type UtxosFilter = { + #min_confirmations : Nat32; + #page : Blob; + }; + + /// 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; + + public type SchnorrSignFunction = (Text, [Blob], Blob, ?IC.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 75% rename from motoko/basic_bitcoin/src/basic_bitcoin/src/Utils.mo rename to motoko/basic_bitcoin/backend/Utils.mo index d2861300a..b5cb6113c 100644 --- a/motoko/basic_bitcoin/src/basic_bitcoin/src/Utils.mo +++ b/motoko/basic_bitcoin/backend/Utils.mo @@ -1,18 +1,15 @@ 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"; +import IC "mo:ic/Types"; module { type Result = Result.Result; - type EcdsaCanisterActor = Types.EcdsaCanisterActor; - type SchnorrCanisterActor = Types.SchnorrCanisterActor; - 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 { @@ -77,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(_ecdsa_canister_actor : EcdsaCanisterActor, _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(_schnorr_canister_actor : SchnorrCanisterActor, _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 new file mode 100644 index 000000000..84016c1cd --- /dev/null +++ b/motoko/basic_bitcoin/backend/app.mo @@ -0,0 +1,134 @@ +import Array "mo:core/Array"; +import Blob "mo:core/Blob"; + +import BitcoinApi "BitcoinApi"; +import P2pkh "P2pkh"; +import P2trKeyOnly "P2trKeyOnly"; +import P2tr "P2tr"; +import Types "Types"; +import Utils "Utils"; + +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`. + let NETWORK : Types.Network = network; + + /// The derivation path to use for ECDSA secp256k1 or Schnorr BIP340/BIP341 key + /// derivation. + transient let DERIVATION_PATH : [[Nat8]] = []; + + // 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 { + await BitcoinApi.get_balance(NETWORK, address); + }; + + /// Returns the UTXOs of the given Bitcoin address. + 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 [Types.MillisatoshiPerByte] { + 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 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 : 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 Types.BitcoinAddress { + await P2trKeyOnly.get_address_key_only(NETWORK, KEY_NAME, p2trKeyOnlyDerivationPath()); + }; + + 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 Types.BitcoinAddress { + await P2tr.get_address(NETWORK, KEY_NAME, p2trDerivationPaths()); + }; + + 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 : Types.SendRequest) : async Text { + Utils.bytesToText(await P2tr.send_script_path(NETWORK, p2trDerivationPaths(), KEY_NAME, request.destination_address, request.amount_in_satoshi)); + }; + + /// 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; + }); + }; + + /// 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; + timestamp : Nat32; + difficulty : Nat; + utxos_length : Nat64; + } { + await BitcoinApi.get_blockchain_info(NETWORK); + }; + + func p2pkhDerivationPath() : [[Nat8]] { + derivationPathWithSuffix("p2pkh"); + }; + + func p2trKeyOnlyDerivationPath() : [[Nat8]] { + derivationPathWithSuffix("p2tr_key_only"); + }; + + func p2trDerivationPaths() : Types.P2trDerivationPaths { + { + key_path_derivation_path = derivationPathWithSuffix("p2tr_internal_key"); + script_path_derivation_path = derivationPathWithSuffix("p2tr_script_key"); + }; + }; + + func derivationPathWithSuffix(suffix : Blob) : [[Nat8]] { + [DERIVATION_PATH, [suffix.toArray()]].flatten(); + }; +}; 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/docker/start.sh b/motoko/basic_bitcoin/docker/start.sh new file mode 100644 index 000000000..8d83fe257 --- /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=0.0.0.0 -rpcallowip=0.0.0.0/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 new file mode 100644 index 000000000..cf1e2e6fd --- /dev/null +++ b/motoko/basic_bitcoin/icp.yaml @@ -0,0 +1,27 @@ +canisters: + - name: backend + recipe: + type: "@dfinity/motoko@v5.0.0" + +networks: + - name: local + mode: managed + image: icp-cli-network-launcher-bitcoin + port-mapping: + - 0:4943 # IC gateway (dynamic) + +environments: + - name: local + network: local + init_args: + backend: "(variant { regtest })" + + - name: staging + network: ic + init_args: + backend: "(variant { testnet })" + + - name: production + network: ic + init_args: + backend: "(variant { mainnet })" diff --git a/motoko/basic_bitcoin/mops.lock b/motoko/basic_bitcoin/mops.lock new file mode 100644 index 000000000..cf55451fd --- /dev/null +++ b/motoko/basic_bitcoin/mops.lock @@ -0,0 +1,123 @@ +{ + "version": 3, + "mopsTomlDepsHash": "c3303186871a047ea88cfff4faa6666a4310a22fcdefcc52427de77ba048307d", + "deps": { + "core": "2.5.0", + "bitcoin": "0.2.0", + "sha2": "0.1.14", + "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.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" + }, + "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", + "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 diff --git a/motoko/basic_bitcoin/mops.toml b/motoko/basic_bitcoin/mops.toml index 72fcccb25..6015375df 100644 --- a/motoko/basic_bitcoin/mops.toml +++ b/motoko/basic_bitcoin/mops.toml @@ -1,12 +1,13 @@ [toolchain] -moc = "1.5.1" +moc = "1.9.0" [dependencies] -core = "2.4.0" -bitcoin = "0.1.0" +core = "2.5.0" +bitcoin = "0.2.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"] +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/BitcoinApi.mo b/motoko/basic_bitcoin/src/basic_bitcoin/src/BitcoinApi.mo deleted file mode 100644 index f20231ffc..000000000 --- a/motoko/basic_bitcoin/src/basic_bitcoin/src/BitcoinApi.mo +++ /dev/null @@ -1,77 +0,0 @@ -import Types "Types"; - -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; - 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; - 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; - - /// 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 (); - }; - - 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({ - address; - network; - min_confirmations = null; - }) - }; - - /// Returns the UTXOs of the given Bitcoin address. - /// - /// 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({ - address; - network; - filter = null; - }) - }; - - /// Returns the 100 fee percentiles measured in millisatoshi/vbyte. - /// 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({ - network; - }) - }; - - /// 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 - 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; - }) - }; -} diff --git a/motoko/basic_bitcoin/src/basic_bitcoin/src/EcdsaApi.mo b/motoko/basic_bitcoin/src/basic_bitcoin/src/EcdsaApi.mo deleted file mode 100644 index edeeb31b7..000000000 --- a/motoko/basic_bitcoin/src/basic_bitcoin/src/EcdsaApi.mo +++ /dev/null @@ -1,42 +0,0 @@ -import Types "Types"; - -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 { - // Retrieve the public key of this canister at derivation path - // from the ECDSA API. - let res = await ecdsa_canister_actor.ecdsa_public_key({ - canister_id = null; - derivation_path; - key_id = { - curve = #secp256k1; - name = key_name; - }; - }); - - 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({ - message_hash; - derivation_path; - key_id = { - curve = #secp256k1; - name = key_name; - }; - }); - - res.signature; - }; -}; diff --git a/motoko/basic_bitcoin/src/basic_bitcoin/src/Main.mo b/motoko/basic_bitcoin/src/basic_bitcoin/src/Main.mo deleted file mode 100644 index b6cade249..000000000 --- a/motoko/basic_bitcoin/src/basic_bitcoin/src/Main.mo +++ /dev/null @@ -1,112 +0,0 @@ -import Array "mo:core/Array"; -import Blob "mo:core/Blob"; - -import BitcoinApi "BitcoinApi"; -import P2pkh "P2pkh"; -import P2trKeyOnly "P2trKeyOnly"; -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 EcdsaCanisterActor = Types.EcdsaCanisterActor; - type SchnorrCanisterActor = Types.SchnorrCanisterActor; - type P2trDerivationPaths = Types.P2trDerivationPaths; - - /// 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; - - /// 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 dfx. - 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); - }; - - /// Returns the UTXOs of the given Bitcoin address. - public func get_utxos(address : BitcoinAddress) : async 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] { - 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 { - await P2pkh.get_address(ecdsa_canister_actor, 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)); - }; - - public func get_p2tr_key_only_address() : async BitcoinAddress { - await P2trKeyOnly.get_address_key_only(schnorr_canister_actor, 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)); - }; - - public func get_p2tr_address() : async BitcoinAddress { - await P2tr.get_address(schnorr_canister_actor, 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)); - }; - - 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)); - }; - - func p2pkhDerivationPath() : [[Nat8]] { - derivationPathWithSuffix("p2pkh"); - }; - - func p2trKeyOnlyDerivationPath() : [[Nat8]] { - derivationPathWithSuffix("p2tr_key_only"); - }; - - func p2trDerivationPaths() : P2trDerivationPaths { - { - key_path_derivation_path = derivationPathWithSuffix("p2tr_internal_key"); - script_path_derivation_path = derivationPathWithSuffix("p2tr_script_key"); - }; - }; - - func derivationPathWithSuffix(suffix : Blob) : [[Nat8]] { - [DERIVATION_PATH, [suffix.toArray()]].flatten(); - }; -}; diff --git a/motoko/basic_bitcoin/src/basic_bitcoin/src/SchnorrApi.mo b/motoko/basic_bitcoin/src/basic_bitcoin/src/SchnorrApi.mo deleted file mode 100644 index a347ba327..000000000 --- a/motoko/basic_bitcoin/src/basic_bitcoin/src/SchnorrApi.mo +++ /dev/null @@ -1,43 +0,0 @@ -import Types "Types"; - -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; - - // 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 { - // Retrieve the public key of this canister at derivation path - // from the Schnorr API. - let res = await schnorr_canister_actor.schnorr_public_key({ - canister_id = null; - derivation_path; - key_id = { - algorithm = #bip340secp256k1; - name = key_name; - }; - }); - - 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({ - message; - derivation_path; - key_id = { - algorithm = #bip340secp256k1; - name = key_name; - }; - aux; - }); - - res.signature; - }; -}; 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]]; - }; -};