From 8f1f0755e3bb04655d5d4a8907d31ec0aa4eeffc Mon Sep 17 00:00:00 2001 From: Einherjar Date: Mon, 2 Oct 2023 17:41:31 -0300 Subject: [PATCH 1/5] test: add testing to all markdown files --- .github/workflows/test.yaml | 28 ++++++++++++++++++++++++++++ .gitignore | 6 ++++++ justfile | 7 +++++++ tests/Cargo.toml | 12 ++++++++++++ tests/generate.sh | 17 +++++++++++++++++ 5 files changed, 70 insertions(+) create mode 100644 .github/workflows/test.yaml create mode 100644 justfile create mode 100644 tests/Cargo.toml create mode 100755 tests/generate.sh diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..7ddd084 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,28 @@ +name: Tests +on: + push: + branches: [ master, '[0-9]+.[0-9]+.[0-9]+', '[0-9]+.[0-9]+.[0-9]+-[A-Za-z0-9]+' ] + pull_request: + branches: [ master, '[0-9]+.[0-9]+.[0-9]+', '[0-9]+.[0-9]+.[0-9]+-[A-Za-z0-9]+' ] + # runs every monday at 9 am + schedule: + - cron: "0 9 * * 1" + workflow_dispatch: null + +jobs: + code-samples: + name: Test code samples + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + - name: Generate harness + working-directory: ./tests + run: ./generate.sh + - name: Test code samples + working-directory: ./tests + run: cargo test diff --git a/.gitignore b/.gitignore index 062e1f4..04ad25d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ node_modules/ cache/ dist/ + +# test stuff +tests/Cargo.lock +tests/target +tests/src +tests/config.txt diff --git a/justfile b/justfile new file mode 100644 index 0000000..8c70aae --- /dev/null +++ b/justfile @@ -0,0 +1,7 @@ +default: + just --list + +alias t := test + +test: + cd tests && ./generate.sh && cargo test diff --git a/tests/Cargo.toml b/tests/Cargo.toml new file mode 100644 index 0000000..961ee0d --- /dev/null +++ b/tests/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "tests" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +bdk = { git = "https://github.com/notmandatory/bdk.git", branch = "test/esplora_tests", version = "1.0.0-alpha.1" } +bdk_esplora = { git = "https://github.com/notmandatory/bdk.git", branch = "test/esplora_tests", version = "0.3.0" } +bdk_file_store = { git = "https://github.com/notmandatory/bdk.git", branch = "test/esplora_tests", version = "0.2.0" } +doc-comment = "0.3" +tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } diff --git a/tests/generate.sh b/tests/generate.sh new file mode 100755 index 0000000..b497beb --- /dev/null +++ b/tests/generate.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +mkdir -p src +cat << EOF > src/lib.rs +#![allow(non_snake_case)] +#[macro_use] +extern crate doc_comment; +EOF + +for doc in ../book/*.md +do + NAME=$(basename $doc .md) + NAME=${NAME//./_} + NAME=${NAME//-/_} + echo -e "doctest\041(\"../$doc\");" > src/$NAME.rs + echo "mod $NAME;" >> src/lib.rs +done From a5d4d98fa253eb2482135c09f993e15ee49df367 Mon Sep 17 00:00:00 2001 From: Einherjar Date: Mon, 2 Oct 2023 17:42:01 -0300 Subject: [PATCH 2/5] ci: add dependabot to update test/Cargo.toml periodically ci: dependabot --- .github/dependabot.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..6f45c33 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# Set update schedule for GitHub Actions +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" + - package-ecosystem: "cargo" + directory: "/tests" # point this towards a Cargo.toml directory + schedule: + # Check for updates to Rust dependencies every week + interval: "weekly" \ No newline at end of file From ab0b866a886ae9eceb9676e97f46d1f61410a320 Mon Sep 17 00:00:00 2001 From: Einherjar Date: Mon, 2 Oct 2023 17:42:27 -0300 Subject: [PATCH 3/5] ci: test markdown links in book and repo --- .github/workflows/check-links.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/check-links.yml diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml new file mode 100644 index 0000000..773cad5 --- /dev/null +++ b/.github/workflows/check-links.yml @@ -0,0 +1,25 @@ +name: Markdown Links Check +# runs every monday at 9 am +on: + schedule: + - cron: "0 9 * * 1" + workflow_dispatch: null + +jobs: + check-links: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: gaurav-nelson/github-action-markdown-link-check@v1 + # checks all markdown files from /book including all subfolders + with: + use-quiet-mode: 'yes' + use-verbose-mode: 'yes' + folder-path: 'book/' + - uses: actions/checkout@v4 + - uses: gaurav-nelson/github-action-markdown-link-check@v1 + # checks all markdown files from root but ignores subfolders + with: + use-quiet-mode: 'yes' + use-verbose-mode: 'yes' + max-depth: 0 \ No newline at end of file From ba1640f90b63bc28adfd0578219212a720b310b7 Mon Sep 17 00:00:00 2001 From: Einherjar Date: Mon, 2 Oct 2023 17:42:54 -0300 Subject: [PATCH 4/5] fix(getting-started): add example in tip --- book/getting-started.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/book/getting-started.md b/book/getting-started.md index aa792a3..bd4e217 100644 --- a/book/getting-started.md +++ b/book/getting-started.md @@ -1,26 +1,37 @@ # Getting Started + ### Install Rust + See the Rust ["Getting Started"] page to install the Rust development tools. ### Using BDK in a Rust project + Follow these steps to use BDK in your own rust project with the async `esplora` blockchain client. :::tip For now use the latest `master` branch versions of BDK crates. +As an example, for BDK: + +```shell +cargo add bdk --git "https://github.com/notmandatory/bdk.git" +``` ::: 1. Create a new Rust project: + ```shell cargo init my_bdk_app cd my_bdk_app ``` 2. Add `bdk` to your `Cargo.toml` file. Find the latest `bdk@1` release on [`crates.io`](https://crates.io/crates/bdk/versions), for example: + ```shell cargo add bdk@1.0.0-alpha.1 ``` 3. Add other required dependencies: + ```shell cargo add bdk_esplora@0.3.0 cargo add bdk_file_store@0.2.0 From 4343c8498249f30929b1fe7eda7b1228cc84ab4a Mon Sep 17 00:00:00 2001 From: Einherjar Date: Mon, 2 Oct 2023 17:43:18 -0300 Subject: [PATCH 5/5] fix(wallet): add explainable and testable example --- book/wallet.md | 199 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 166 insertions(+), 33 deletions(-) diff --git a/book/wallet.md b/book/wallet.md index 6287e78..9da3809 100644 --- a/book/wallet.md +++ b/book/wallet.md @@ -1,24 +1,31 @@ # Wallet with Async Explora 1. Create a new Rust project: + ```shell cargo init my_bdk_app cd my_bdk_app ``` -2. Add `bdk` to your `Cargo.toml` file. ~~Find the latest `BDK@1.0.0` release on [`crates.io`](https://crates.io/crates/bdk/versions)~~ (use pre-released branch for now): +1. Add `bdk` to your `Cargo.toml` file. + ~~Find the latest `BDK@1.0.0` release on [`crates.io`](https://crates.io/crates/bdk/versions)~~ + (use pre-released branch for now): + ```shell cargo add bdk --git "https://github.com/notmandatory/bdk.git" --branch "test/esplora_tests" ``` -3. Add other required dependencies: +1. Add other required dependencies: + ```shell cargo add bdk_esplora --git "https://github.com/notmandatory/bdk.git" --branch "test/esplora_tests" cargo add bdk_file_store --git "https://github.com/notmandatory/bdk.git" --branch "test/esplora_tests" cargo add tokio@1 --features "rt,rt-multi-thread,macros" ``` -4. Edit `src/main.rs`, replace with below code to load or create and save new descriptors: +1. Edit `src/main.rs`, replace with below code to load or create and save new descriptors: + + ```rust use std::fs::File; use std::io::Read; @@ -116,17 +123,17 @@ async fn main() -> Result<(), Box> { } ``` -5. Add code to create a wallet and get a new address and current wallet balance: -```rust +1. Add code to create a wallet and get a new address and current wallet balance: + + +```rs use bdk::{bitcoin::Network, descriptor, Wallet}; use bdk::wallet::AddressIndex; use bdk_file_store::Store; const CHAIN_DATA_FILE: &str = "chain.dat"; const DB_MAGIC: &[u8] = "TABCONF24".as_bytes(); -``` -```rust // Create a wallet and get a new address and current wallet balance let db = Store::::new_from_path(DB_MAGIC, CHAIN_DATA_FILE)?; @@ -143,20 +150,22 @@ let balance = wallet.get_balance(); println!("Wallet balance before syncing: confirmed {} sats, trusted_pending {} sats, untrusted pending {} sats", balance.confirmed, balance.trusted_pending, balance.untrusted_pending); ``` -6. Add code to create an async esplora client: -```rust +1. Add code to create an async esplora client: + + +```rs use bdk_esplora::esplora_client; -``` - -```rust + // Create an async esplora client let client = esplora_client::Builder::new("http://signet.bitcoindevkit.net").build_async()?; let prev_tip = wallet.latest_checkpoint(); ``` -7. Add code to scans keychain SPKs for transaction histories, stopping after `stop_gap` is reached: -```rust +1. Add code to scans keychain SPKs for transaction histories, stopping after `stop_gap` is reached: + + +```rs use std::collections::BTreeMap; use std::{io, io::Write, str::FromStr}; use bdk::chain::keychain::WalletUpdate; @@ -165,9 +174,7 @@ use bdk_esplora::{esplora_client, EsploraAsyncExt}; const STOP_GAP: usize = 50; const PARALLEL_REQUESTS: usize = 5; -``` - -```rust + // Prepare the `IndexedTxGraph` update based on whether we are scanning or syncing. // Scanning: We are iterating through spks of all keychains and scanning for transactions for @@ -237,12 +244,12 @@ if prompt("Scan wallet") { } ``` -8. Add code to sync wallet by checking for history on all derived SPKs: -```rust +1. Add code to sync wallet by checking for history on all derived SPKs: + + +```rs use bdk::bitcoin::{bip32, Address, OutPoint, ScriptBuf, Txid}; -``` - -```rust + // Syncing: We only check for specified spks, utxos and txids to update their confirmation // status or fetch missing transactions. else { @@ -300,8 +307,10 @@ else { } ``` -9. Add code to sync wallet by checking for history on only unused SPKs: -```rust +1. Add code to sync wallet by checking for history on only unused SPKs: + + +```rs // Sync only unused SPKs else if prompt("Sync only unused SPKs") { // TODO add Wallet::unused_spks() function, gives all unused tracked spks @@ -328,8 +337,10 @@ else { } ``` -10. Add code to sync wallet UTXOs to see if any have been spent: -```rust +1. Add code to sync wallet UTXOs to see if any have been spent: + + +```rs // Sync UTXOs if prompt("Sync UTXOs") { // We want to search for whether the UTXO is spent, and spent by which @@ -350,8 +361,10 @@ else { }; ``` -11. Add code to sync wallet unconfirmed TXs: -```rust +1. Add code to sync wallet unconfirmed TXs: + + +```rs // Sync unconfirmed TX if prompt("Sync unconfirmed TX") { // We want to search for whether the unconfirmed transaction is now confirmed. @@ -369,12 +382,15 @@ else { txids = Box::new(unconfirmed_txids); } ``` -12. Add code to check the new wallet balance and request a deposit if required: -```rust + +1. Add code to check the new wallet balance and request a deposit if required: + + +```rs const SEND_AMOUNT: u64 = 5000; ``` -```rust +```rs // Check balance and request deposit if required if balance.total() < SEND_AMOUNT { println!( @@ -385,8 +401,9 @@ if balance.total() < SEND_AMOUNT { } ``` -13. Add code to create a TX to return sats to the [signet faucet](https://signetfaucet.com/): -```rust +1. Add code to create a TX to return sats to the [signet faucet](https://signetfaucet.com/): + +```rs // Create TX to return sats to signet faucet https://signetfaucet.com/ let faucet_address = Address::from_str("tb1qg3lau83hm9e9tdvzr5k7aqtw3uv0dwkfct4xdn")? .require_network(network)?; @@ -428,3 +445,119 @@ if prompt("Broadcast") { ); } ``` + +### Solution + +The final `src/main.rs` should look like this: + +```rust +use std::fs::File; +use std::io::Read; +use std::string::ToString; +use std::{io::Write, str::FromStr}; +use std::collections::BTreeMap; // Added for use in the scan wallet section + +use bdk::bitcoin::bip32; +use bdk::bitcoin::bip32::ExtendedPrivKey; +use bdk::bitcoin::secp256k1::{rand, rand::RngCore, Secp256k1}; + +use bdk::{bitcoin::Network, descriptor, Wallet}; +use bdk::descriptor::IntoWalletDescriptor; +use bdk::keys::IntoDescriptorKey; + +use bdk::wallet::AddressIndex; +use bdk_file_store::Store; + +use bdk_esplora::{esplora_client, EsploraAsyncExt}; + +const CONFIG_FILE: &str = "config.txt"; + +const CHAIN_DATA_FILE: &str = "chain.dat"; +const DB_MAGIC: &[u8] = "TABCONF24".as_bytes(); + +const STOP_GAP: usize = 50; +const PARALLEL_REQUESTS: usize = 5; + +const SEND_AMOUNT: u64 = 5000; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Create and load or save new descriptors + + let secp = Secp256k1::new(); + let network = Network::Signet; + + // get descriptors from config.txt file, if file is missing create new ones + let descriptors = match File::open(CONFIG_FILE) { + // load descriptors from file + Ok(mut file) => { + let mut config = String::new(); + file.read_to_string(&mut config)?; + let descriptor_strings: [_; 2] = config + .split("|") + .map(|d| d.to_string()) + .collect::>() + .try_into() + .unwrap(); + let external_descriptor = descriptor_strings[0] + .into_wallet_descriptor(&secp, network) + .unwrap(); + let internal_descriptor = descriptor_strings[1] + .into_wallet_descriptor(&secp, network) + .unwrap(); + (external_descriptor, internal_descriptor) + } + Err(_) => { + // create new descriptors and save them to the file + let mut seed = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut seed); + let xprv = ExtendedPrivKey::new_master(network, &seed).unwrap(); + let bip86_external = bip32::DerivationPath::from_str("m/86'/1'/0'/0/0").unwrap(); + let bip86_internal = bip32::DerivationPath::from_str("m/86'/1'/0'/0/1").unwrap(); + let external_key = (xprv, bip86_external).into_descriptor_key().unwrap(); + let internal_key = (xprv, bip86_internal).into_descriptor_key().unwrap(); + let external_descriptor = descriptor!(tr(external_key)) + .unwrap() + .into_wallet_descriptor(&secp, network) + .unwrap(); + let internal_descriptor = descriptor!(tr(internal_key)) + .unwrap() + .into_wallet_descriptor(&secp, network) + .unwrap(); + // save descriptor strings to file + let mut file = File::create(CONFIG_FILE).unwrap(); + println!("Created new descriptor config file: config.txt"); + let config = format!( + "{}|{}", + &external_descriptor + .0 + .to_string_with_secret(&external_descriptor.1), + &internal_descriptor + .0 + .to_string_with_secret(&internal_descriptor.1) + ); + file.write(config.as_bytes()).unwrap(); + (external_descriptor, internal_descriptor) + } + }; + + let external_descriptor = descriptors.0; + let internal_descriptor = descriptors.1; + println!( + "External descriptor: {}", + &external_descriptor + .0 + .to_string_with_secret(&external_descriptor.1) + ); + println!( + "Internal descriptor: {}\n", + &internal_descriptor + .0 + .to_string_with_secret(&internal_descriptor.1) + ); + + // Rest of the code... + + Ok(()) +} +```