diff --git a/.travis.yml b/.travis.yml index 6a6263f1..9efcb3f0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ env: - TARGET=x86_64-unknown-linux-gnu FEATURES=cli-utils,esplora NO_DEFAULT_FEATURES=1 - TARGET=x86_64-unknown-linux-gnu FEATURES=compiler NO_DEFAULT_FEATURES=1 RUN_TESTS=1 # Test the `miniscriptc` example - TARGET=x86_64-unknown-linux-gnu FEATURES=test-electrum NO_DEFAULT_FEATURES=1 RUN_TESTS=1 RUN_CORE=1 + - TARGET=x86_64-unknown-linux-gnu FEATURES=test-md-docs NO_DEFAULT_FEATURES=1 RUN_TESTS=1 NIGHTLY=1 - TARGET=wasm32-unknown-unknown FEATURES=cli-utils,esplora NO_DEFAULT_FEATURES=1 before_script: - | @@ -33,6 +34,11 @@ before_script: if [[ $CHECK_FMT -eq 1 ]]; then rustup component add rustfmt fi + - | + if [[ $NIGHTLY -eq 1 ]]; then + rustup toolchain install nightly + rustup default nightly + fi - rustup target add "$TARGET" script: - | diff --git a/Cargo.toml b/Cargo.toml index bb24548f..18ebba9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ async-interface = ["async-trait"] # Debug/Test features debug-proc-macros = ["magical-macros/debug", "testutils-macros/debug"] test-electrum = ["electrum"] +test-md-docs = ["base64", "electrum"] [dev-dependencies] testutils = { path = "./testutils" } diff --git a/README.md b/README.md index 103e8d68..188628ae 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,145 @@ -# Magical Bitcoin Wallet +
+

Magical Bitcoin Library

-A modern, lightweight, descriptor-based wallet written in Rust! + -## Getting Started +

+ A modern, lightweight, descriptor-based wallet library written in Rust! +

-See the documentation at [magicalbitcoin.org](https://magicalbitcoin.org) +

+ Crate Info + API Docs + Rustc Version 1.45+ +

+ +

+ Project Homepage + | + Documentation +

+
+ +## About + +The `magical` library aims to be the core building block for Bitcoin wallets of any kind. + +* It uses [Miniscript](https://github.com/rust-bitcoin/rust-miniscript) to support descriptors with generalized conditions. This exact same library can be used to build + single-sig wallets, multisigs, timelocked contracts and more. +* It supports multiple blockchain backends and databases, allowing developers to choose exactly what's right for their projects. +* It's built to be cross-platform: the core logic works on desktop, mobile, and even WebAssembly. +* It's very easy to extend: developers can implement customized logic for blockchain backends, databases, signers, coin selection, and more, without having to fork and modify this library. + +## Examples + +### Sync the balance of a descriptor + +```no_run +use magical::Wallet; +use magical::database::MemoryDatabase; +use magical::blockchain::{noop_progress, ElectrumBlockchain}; + +use magical::electrum_client::Client; + +fn main() -> Result<(), magical::Error> { + let client = Client::new("ssl://electrum.blockstream.info:60002", None)?; + let wallet = Wallet::new( + "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", + Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), + bitcoin::Network::Testnet, + MemoryDatabase::default(), + ElectrumBlockchain::from(client) + )?; + + wallet.sync(noop_progress(), None)?; + + println!("Descriptor balance: {} SAT", wallet.get_balance()?); + + Ok(()) +} +``` + +### Generate a few addresses + +``` +use magical::{Wallet, OfflineWallet}; +use magical::database::MemoryDatabase; + +fn main() -> Result<(), magical::Error> { + let wallet: OfflineWallet<_> = Wallet::new_offline( + "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", + Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), + bitcoin::Network::Testnet, + MemoryDatabase::default(), + )?; + + println!("Address #0: {}", wallet.get_new_address()?); + println!("Address #1: {}", wallet.get_new_address()?); + println!("Address #2: {}", wallet.get_new_address()?); + + Ok(()) +} +``` + +### Create a transaction + +```no_run +use magical::{FeeRate, TxBuilder, Wallet}; +use magical::database::MemoryDatabase; +use magical::blockchain::{noop_progress, ElectrumBlockchain}; + +use magical::electrum_client::Client; + +use bitcoin::consensus::serialize; + +fn main() -> Result<(), magical::Error> { + let client = Client::new("ssl://electrum.blockstream.info:60002", None)?; + let wallet = Wallet::new( + "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", + Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), + bitcoin::Network::Testnet, + MemoryDatabase::default(), + ElectrumBlockchain::from(client) + )?; + + wallet.sync(noop_progress(), None)?; + + let send_to = wallet.get_new_address()?; + let (psbt, details) = wallet.create_tx( + TxBuilder::with_recipients(vec![(send_to.script_pubkey(), 50_000)]) + .enable_rbf() + .do_not_spend_change() + .fee_rate(FeeRate::from_sat_per_vb(5.0)) + )?; + + println!("Transaction details: {:#?}", details); + println!("Unsigned PSBT: {}", base64::encode(&serialize(&psbt))); + + Ok(()) +} +``` + +### Sign a transaction + +```no_run +use magical::{Wallet, OfflineWallet}; +use magical::database::MemoryDatabase; + +use bitcoin::consensus::deserialize; + +fn main() -> Result<(), magical::Error> { + let wallet: OfflineWallet<_> = Wallet::new_offline( + "wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)", + Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"), + bitcoin::Network::Testnet, + MemoryDatabase::default(), + )?; + + let psbt = "..."; + let psbt = deserialize(&base64::decode(psbt).unwrap())?; + + let (signed_psbt, finalized) = wallet.sign(psbt, None)?; + + Ok(()) +} +``` diff --git a/src/doctest.rs b/src/doctest.rs new file mode 100644 index 00000000..1c59362f --- /dev/null +++ b/src/doctest.rs @@ -0,0 +1,3 @@ +#[doc(include = "../README.md")] +#[cfg(doctest)] +pub struct ReadmeDoctests; diff --git a/src/lib.rs b/src/lib.rs index d215df13..9f2e05ad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,9 @@ // only enables the `doc_cfg` feature when // the `docsrs` configuration attribute is defined #![cfg_attr(docsrs, feature(doc_cfg))] +// only enables the nightly `external_doc` feature when +// `test-md-docs` is enabled +#![cfg_attr(feature = "test-md-docs", feature(external_doc))] pub extern crate bitcoin; extern crate log; @@ -70,6 +73,8 @@ pub(crate) mod error; pub mod blockchain; pub mod database; pub mod descriptor; +#[cfg(feature = "test-md-docs")] +mod doctest; pub(crate) mod psbt; pub(crate) mod types; pub mod wallet; diff --git a/static/wizard.svg b/static/wizard.svg new file mode 100644 index 00000000..5fc405bf --- /dev/null +++ b/static/wizard.svg @@ -0,0 +1,327 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +