Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: full wallet persisted #80

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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ dist/
drafts/
target/
*Cargo.lock
*.sqlite3

# MacOS
*.DS_Store
Expand Down
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"examples/rust/quickstart/Cargo.toml",
"examples/rust/descriptors/Cargo.toml",
"examples/rust/seed-phrase/Cargo.toml",
"examples/rust/transaction/Cargo.toml"
"examples/rust/full-wallet/Cargo.toml"
]
}
64 changes: 53 additions & 11 deletions docs/cookbook/full-wallet.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
This page illustrates core wallet functionality, including:

- Generating descriptors
- Recovering a wallet with descriptors
- Wallet creation, persistence, and loading
- Full scan and light weight sync
- Creating and broadcasting a transaction

!!! tip
The logic for this page is broken down in in 3 separate examples in the [examples source code](https://github.com/bitcoindevkit/book-of-bdk/tree/master/examples/rust). If you are following along with the code examples you will need to copy and paste descriptors between them.
The logic for this page is split between 2 separate examples in the [examples source code](https://github.com/bitcoindevkit/book-of-bdk/tree/master/examples/rust). One to create descriptors and a second for everything else.If you are following along with the code examples you will need to copy and paste your private descriptors you get from the first example into the second. We leave descriptor creation in a separate example because bdk does not handle private descriptor (or private key) storage, that is up to the wallet developer.

## Generating Descriptors

Expand All @@ -19,24 +20,65 @@ First we [create signet descriptors](keys-descriptors/descriptors.md) for our wa

Notice we are creating private descriptors here in order to sign transactions later on.

## Recovering a Wallet with Descriptors
## Full Scan and Address Generation (First Run)

Next, lets use those descriptors to load up our wallet. Replace the descriptors in the [quickstart](./quickstart.md) example with either your private or publickey descriptors (either will work here) and run it to sync a wallet, check the balance, and generate a new address:
Next, lets use those descriptors to load up our wallet. Replace the placeholder descriptors in the `full-wallet` example with your private descriptors:

```rust title="examples/rust/quickstart/src/main.rs"
--8<-- "examples/rust/quickstart/src/main.rs:address"
```rust title="examples/rust/full-wallet/src/main.rs"
--8<-- "examples/rust/full-wallet/src/main.rs:descriptors"
```

We are going to run this example twice. On the first run it will do a full scan for your wallet, persist that chain data, generate a new address for you, and display your current wallet balance, it will then attempt to build a transaction, but will fail becuase we don't have any funds yet. We will use the new address (from the first run) to request funds from the [Mutinynet faucet](https://faucet.mutinynet.com/) so we can build a transaction on the second run. On the second run it will load the data from the previous run, do a light weight sync to check for updates (no need to repeat the full scan), and then build and broadcast a transaction. Let's go through this step by step.

```rust title="examples/rust/full-wallet/src/main.rs"
--8<-- "examples/rust/full-wallet/src/main.rs:persist"
```

In the quickstart example we simply used an in-memory wallet, with no persistence. But here we are saving wallet data to a file. Notice that we are providing our private descriptors during wallet load. This is because bdk never stores private keys, that responsibility is on the wallet developer (you). The data we are loading here does not include the private keys, but we want our wallet to have signing capabilities, so we need to provide our private descriptors during wallet load. If we get a wallet back from the load attempt, we'll use that, otherwise we'll create a new one. Since this is our first run nothing will be loaded and a new wallet will be created.

```rust title="examples/rust/full-wallet/src/main.rs"
--8<-- "examples/rust/full-wallet/src/main.rs:scan"
```

Next we'll fetch data from our blockchain client. On the first run, we don't yet have any data, so we need to do a full scan. We then persist the data from the scan.
Finally, we'll print out an address that we can use to request funds. You should also see the current balance printed out, it should be 0 since this is a brand new wallet. Note that we persist the wallet after generating the new address; this is to avoid re-using the same address as that would compromise our privacy (on subsequent runs you'll notice the address index incremented).

```rust title="examples/rust/full-wallet/src/main.rs"
--8<-- "examples/rust/full-wallet/src/main.rs:address"
```

The process will then error out, indicating we don't have enough funds to send a transaction.

### Request satoshis from the Mutinynet faucet

We can now use our new address to request some sats from the [Mutinynet faucet](https://faucet.mutinynet.com/). After requesting sats, you can view the transaction in their [Mempool Explorer instance](https://mutinynet.com/) (click the link on the faucet confirmation page or put the txid in the search bar of the mempool explorer). After a minute or so you should see the transaction confirmed. We can also re-run the `quickstart` example and see that our wallet now has some funds!
We can now use our new address to request some sats from the [Mutinynet faucet](https://faucet.mutinynet.com/). After requesting sats, you can view the transaction in their [Mempool Explorer instance](https://mutinynet.com/) (click the link on the faucet confirmation page or put the txid in the search bar of the mempool explorer). After a minute or so you should see the transaction confirmed. We can also re-run the `full-wallet` example and see that our wallet now has some funds!

## Load, Sync, and Send a Transaction (Second Run)

Now that we have some funds, we can re-run the `full-wallet` example. Since we persisted data from the previous run, this time our wallet will be loaded. You do not need to provide descriptors to load wallet data, however, if you don't you will not have signing capabilities, so here we do provide our private descriptors in the loading process:

```rust title="examples/rust/full-wallet/src/main.rs"
--8<-- "examples/rust/full-wallet/src/main.rs:persist"
```

Since we already have some data from the previous run, it will not do a full scan, but only a sync which is faster and less data intensive.

```rust title="examples/rust/full-wallet/src/main.rs"
--8<-- "examples/rust/full-wallet/src/main.rs:scan"
```

Now that we have funds, let's prepare to send a transaction. We need to decide where to send the funds and how much to send.We will send funds back to the mutiny faucet return address. It's good practice to send test sats back to the faucet when you're done using them.

```rust title="examples/rust/full-wallet/src/main.rs"
--8<-- "examples/rust/full-wallet/src/main.rs:faucet"
```

## Creating and Broadcasting a Transaction
Here we are preparing to send 5000 sats back to the mutiny faucet (it's good practice to send test sats back to the faucet when you're done using them).

Finally we use the wallet to send some satoshis. Put your private descriptors into the `transaction` example and run it to create, sign, and broadcast a transaction.
Finally we are ready to build, sign, and broadcast the transaction:

```rust title="examples/rust/transaction/src/main.rs"
--8<-- "examples/rust/transaction/src/main.rs:main"
--8<-- "examples/rust/full-wallet/src/main.rs:transaction"
```

Again we can view the transaction in the Mutinynet explorer or re-run the `quickstart` example to see that our wallet has less funds.
Again we can view the transaction in the Mutinynet explorer or re-run the `full-wallet` example to see that our wallet has less funds.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[package]
name = "transaction"
name = "full-wallet"
version = "0.1.0"
edition = "2021"

[dependencies]
bdk_wallet = { version = "=1.0.0-beta.5", features = ["keys-bip39"] }
bdk_wallet = { version = "=1.0.0-beta.5", features = ["keys-bip39", "rusqlite"] }
bdk_esplora = { version = "=0.19.0", features = ["blocking"] }
anyhow = "1"
100 changes: 100 additions & 0 deletions examples/rust/full-wallet/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use bdk_wallet::bitcoin::Address;
use bdk_wallet::AddressInfo;
use bdk_wallet::KeychainKind;
use bdk_wallet::bitcoin::{Network, Amount};
use bdk_wallet::SignOptions;
use bdk_wallet::Wallet;
use bdk_esplora::EsploraExt;
use bdk_esplora::esplora_client::Builder;
use bdk_esplora::esplora_client;
use bdk_wallet::chain::spk_client::{FullScanRequestBuilder, FullScanResult, SyncRequestBuilder, SyncResult};
use std::str::FromStr;
use bdk_wallet::rusqlite::Connection;

const DB_PATH: &str = "full-wallet.sqlite3";
const STOP_GAP: usize = 50;
const PARALLEL_REQUESTS: usize = 1;

// --8<-- [start:descriptors]
const DESCRIPTOR_PRIVATE_EXTERNAL: &str = "[your private external descriptor here ...]";
const DESCRIPTOR_PRIVATE_INTERNAL: &str = "[your private internal descriptor here ...]";
// Example private descriptors
// const DESCRIPTOR_PRIVATE_EXTERNAL: &str = "tr(tprv8ZgxMBicQKsPdJuLWWArdBsWjqDA3W5WoREnfdgKEcCQB1FMKfSoaFz9JHZU71HwXAqTsjHripkLM62kUQar14SDD8brsmhFKqVUPXGrZLc/86'/1'/0'/0/*)#fv8tutn2";
// const DESCRIPTOR_PRIVATE_INTERNAL: &str = "tr(tprv8ZgxMBicQKsPdJuLWWArdBsWjqDA3W5WoREnfdgKEcCQB1FMKfSoaFz9JHZU71HwXAqTsjHripkLM62kUQar14SDD8brsmhFKqVUPXGrZLc/86'/1'/0'/1/*)#ccz2p7rj";
// --8<-- [end:descriptors]

fn main() -> Result<(), anyhow::Error> {
// --8<-- [start:persist]
let mut conn = Connection::open(DB_PATH)?;

let wallet_opt = Wallet::load()
.descriptor(KeychainKind::External, Some(DESCRIPTOR_PRIVATE_EXTERNAL))
.descriptor(KeychainKind::Internal, Some(DESCRIPTOR_PRIVATE_INTERNAL))
.extract_keys()
.check_network(Network::Signet)
.load_wallet(&mut conn)?;

let (mut wallet, is_new_wallet) = if let Some(loaded_wallet) = wallet_opt {
(loaded_wallet, false)
} else {
(Wallet::create(DESCRIPTOR_PRIVATE_EXTERNAL, DESCRIPTOR_PRIVATE_INTERNAL)
.network(Network::Signet)
.create_wallet(&mut conn)?, true)
};
// --8<-- [end:persist]

// --8<-- [start:scan]
let client: esplora_client::BlockingClient = Builder::new("https://mutinynet.com/api").build_blocking();
// Sync the wallet
if is_new_wallet {
// Perform a full scan
println!("Performing full scan...");
let full_scan_request: FullScanRequestBuilder<KeychainKind> = wallet.start_full_scan();
let update: FullScanResult<KeychainKind> = client.full_scan(full_scan_request, STOP_GAP, PARALLEL_REQUESTS)?;
wallet.apply_update(update).unwrap();
} else {
// Perform a regular sync
println!("Performing regular sync...");
let sync_request: SyncRequestBuilder<(KeychainKind, u32)> = wallet.start_sync_with_revealed_spks();
let update: SyncResult = client.sync(sync_request, PARALLEL_REQUESTS)?;
wallet.apply_update(update).unwrap();
};
wallet.persist(&mut conn)?;
// --8<-- [end:scan]

// --8<-- [start:address]
// Reveal a new address from your external keychain
let address: AddressInfo = wallet.reveal_next_address(KeychainKind::External);
println!("Generated address {} at index {}", address.address, address.index);
wallet.persist(&mut conn)?;
// --8<-- [end:address]

let balance = wallet.balance();
println!("Wallet balance: {} sat", balance.total().to_sat());

// --8<-- [start:faucet]
// Use the Mutinynet faucet return address
let address = Address::from_str("tb1qd28npep0s8frcm3y7dxqajkcy2m40eysplyr9v")
.unwrap()
.require_network(Network::Signet)
.unwrap();

let send_amount: Amount = Amount::from_sat(5000);
// --8<-- [end:faucet]

// --8<-- [start:transaction]
// Transaction Logic
let mut tx_builder = wallet.build_tx();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you feel about adding a check on the balance here instead of letting it error out?

Something like

if balance.total().to_sat() == 0 {
    println!("No available funds to send. Send some sats to {}", address.address);
    return Ok(());
}

Copy link
Contributor Author

@riverKanies riverKanies Oct 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the error it throws already says "Error: Insufficient funds: 0 sat available of 5042 sat needed", it seems quite good, so I saw no reason to add extra code to the example

tx_builder.add_recipient(address.script_pubkey(), send_amount);

let mut psbt = tx_builder.finish()?;
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
assert!(finalized);

let tx = psbt.extract_tx()?;
client.broadcast(&tx)?;
println!("Tx broadcasted! Txid: {}", tx.compute_txid());
// --8<-- [end:transaction]

Ok(())
}
63 changes: 0 additions & 63 deletions examples/rust/transaction/src/main.rs

This file was deleted.