Compare commits
30 Commits
release/0.
...
v0.14.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c175dd2aae | ||
|
|
6b1cbcc4b7 | ||
|
|
afa1ab4ff8 | ||
|
|
632422a3ab | ||
|
|
54f61d17f2 | ||
|
|
5830226216 | ||
|
|
2c77329333 | ||
|
|
3e5bb077ac | ||
|
|
7c06f52a07 | ||
|
|
12e51b3c06 | ||
|
|
2892edf94b | ||
|
|
aa075f0b2f | ||
|
|
8010d692e9 | ||
|
|
b2d7412d6d | ||
|
|
fd51029197 | ||
|
|
711510006b | ||
|
|
d21b6e47ab | ||
|
|
5922c216a1 | ||
|
|
9e29e2d2b1 | ||
|
|
16e832533c | ||
|
|
7f91bcdf1a | ||
|
|
35695d8795 | ||
|
|
756858e882 | ||
|
|
d2ce2714f2 | ||
|
|
3b2b559910 | ||
|
|
3c8416bf31 | ||
|
|
3d8efbf8bf | ||
|
|
d75d221540 | ||
|
|
548e43d928 | ||
|
|
a348dbdcfe |
2
.github/workflows/code_coverage.yml
vendored
2
.github/workflows/code_coverage.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
uses: actions-rs/grcov@v0.1.5
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
file: ${{ steps.coverage.outputs.report }}
|
||||
directory: ./coverage/reports/
|
||||
|
||||
10
CHANGELOG.md
10
CHANGELOG.md
@@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.14.0] - [v0.13.0]
|
||||
|
||||
- BIP39 implementation dependency, in `keys::bip39` changed from tiny-bip39 to rust-bip39.
|
||||
- Add new method on the `TxBuilder` to embed data in the transaction via `OP_RETURN`. To allow that a fix to check the dust only on spendable output has been introduced.
|
||||
- Update the `Database` trait to store the last sync timestamp and block height
|
||||
- Rename `ConfirmationTime` to `BlockTime`
|
||||
|
||||
## [v0.13.0] - [v0.12.0]
|
||||
|
||||
- Exposed `get_tx()` method from `Database` to `Wallet`.
|
||||
@@ -388,4 +395,5 @@ final transaction is created by calling `finish` on the builder.
|
||||
[v0.10.0]: https://github.com/bitcoindevkit/bdk/compare/v0.9.0...v0.10.0
|
||||
[v0.11.0]: https://github.com/bitcoindevkit/bdk/compare/v0.10.0...v0.11.0
|
||||
[v0.12.0]: https://github.com/bitcoindevkit/bdk/compare/v0.11.0...v0.12.0
|
||||
[v0.13.0]: https://github.com/bitcoindevkit/bdk/compare/v0.12.0...v0.13.0
|
||||
[v0.13.0]: https://github.com/bitcoindevkit/bdk/compare/v0.12.0...v0.13.0
|
||||
[v0.14.0]: https://github.com/bitcoindevkit/bdk/compare/v0.13.0...v0.14.0
|
||||
|
||||
15
Cargo.toml
15
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk"
|
||||
version = "0.13.1-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2018"
|
||||
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
@@ -34,14 +34,7 @@ cc = { version = ">=1.0.64", optional = true }
|
||||
socks = { version = "0.3", optional = true }
|
||||
lazy_static = { version = "1.4", optional = true }
|
||||
|
||||
# the latest 0.8 version of tiny-bip39 depends on zeroize_derive 1.2 which has MSRV 1.51 and our
|
||||
# MSRV is 1.46, to fix this until we update our MSRV or replace the tiny-bip39
|
||||
# dependency https://github.com/bitcoindevkit/bdk/issues/399 we can only use an older version
|
||||
tiny-bip39 = { version = "< 0.8", optional = true }
|
||||
# backtrace > 0.3.61 includes object v0.27 which doesn't compile on 1.46. this is used by
|
||||
# tiny-bip39
|
||||
backtrace = { version = "=0.3.61", optional = true }
|
||||
|
||||
bip39 = { version = "1.0.1", optional = true }
|
||||
bitcoinconsensus = { version = "0.19.0-3", optional = true }
|
||||
|
||||
# Needed by bdk_blockchain_tests macro
|
||||
@@ -65,7 +58,7 @@ sqlite = ["rusqlite", "ahash"]
|
||||
compact_filters = ["rocksdb", "socks", "lazy_static", "cc"]
|
||||
key-value-db = ["sled"]
|
||||
all-keys = ["keys-bip39"]
|
||||
keys-bip39 = ["tiny-bip39", "backtrace"]
|
||||
keys-bip39 = ["bip39"]
|
||||
rpc = ["bitcoincore-rpc"]
|
||||
|
||||
# We currently provide mulitple implementations of `Blockchain`, all are
|
||||
@@ -101,7 +94,7 @@ test-md-docs = ["electrum"]
|
||||
lazy_static = "1.4"
|
||||
env_logger = "0.7"
|
||||
clap = "2.33"
|
||||
electrsd = { version= "0.12", features = ["trigger", "bitcoind_0_21_1"] }
|
||||
electrsd = { version= "0.12", features = ["trigger", "bitcoind_22_0"] }
|
||||
|
||||
[[example]]
|
||||
name = "address_validator"
|
||||
|
||||
@@ -71,7 +71,7 @@ use super::{Blockchain, Capability, ConfigurableBlockchain, Progress};
|
||||
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
|
||||
use crate::error::Error;
|
||||
use crate::types::{KeychainKind, LocalUtxo, TransactionDetails};
|
||||
use crate::{ConfirmationTime, FeeRate};
|
||||
use crate::{BlockTime, FeeRate};
|
||||
|
||||
use peer::*;
|
||||
use store::*;
|
||||
@@ -206,7 +206,7 @@ impl CompactFiltersBlockchain {
|
||||
transaction: Some(tx.clone()),
|
||||
received: incoming,
|
||||
sent: outgoing,
|
||||
confirmation_time: ConfirmationTime::new(height, timestamp),
|
||||
confirmation_time: BlockTime::new(height, timestamp),
|
||||
verified: height.is_some(),
|
||||
fee: Some(inputs_sum.saturating_sub(outputs_sum)),
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ use crate::blockchain::{Blockchain, Capability, ConfigurableBlockchain, Progress
|
||||
use crate::database::{BatchDatabase, DatabaseUtils};
|
||||
use crate::descriptor::{get_checksum, IntoWalletDescriptor};
|
||||
use crate::wallet::utils::SecpCtx;
|
||||
use crate::{ConfirmationTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails};
|
||||
use crate::{BlockTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails};
|
||||
use bitcoincore_rpc::json::{
|
||||
GetAddressInfoResultLabel, ImportMultiOptions, ImportMultiRequest,
|
||||
ImportMultiRequestScriptPubkey, ImportMultiRescanSince,
|
||||
@@ -230,7 +230,7 @@ impl Blockchain for RpcBlockchain {
|
||||
list_txs_ids.insert(txid);
|
||||
if let Some(mut known_tx) = known_txs.get_mut(&txid) {
|
||||
let confirmation_time =
|
||||
ConfirmationTime::new(tx_result.info.blockheight, tx_result.info.blocktime);
|
||||
BlockTime::new(tx_result.info.blockheight, tx_result.info.blocktime);
|
||||
if confirmation_time != known_tx.confirmation_time {
|
||||
// reorg may change tx height
|
||||
debug!(
|
||||
@@ -266,7 +266,7 @@ impl Blockchain for RpcBlockchain {
|
||||
let td = TransactionDetails {
|
||||
transaction: Some(tx),
|
||||
txid: tx_result.info.txid,
|
||||
confirmation_time: ConfirmationTime::new(
|
||||
confirmation_time: BlockTime::new(
|
||||
tx_result.info.blockheight,
|
||||
tx_result.info.blocktime,
|
||||
),
|
||||
|
||||
@@ -21,7 +21,7 @@ use bitcoin::{BlockHeader, OutPoint, Script, Transaction, Txid};
|
||||
use super::*;
|
||||
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
|
||||
use crate::error::Error;
|
||||
use crate::types::{ConfirmationTime, KeychainKind, LocalUtxo, TransactionDetails};
|
||||
use crate::types::{BlockTime, KeychainKind, LocalUtxo, TransactionDetails};
|
||||
use crate::wallet::time::Instant;
|
||||
use crate::wallet::utils::ChunksIterator;
|
||||
|
||||
@@ -151,7 +151,7 @@ pub trait ElectrumLikeSync {
|
||||
// check if tx height matches, otherwise updates it. timestamp is not in the if clause
|
||||
// because we are not asking headers for confirmed tx we know about
|
||||
if tx_details.confirmation_time.as_ref().map(|c| c.height) != height {
|
||||
let confirmation_time = ConfirmationTime::new(height, timestamp);
|
||||
let confirmation_time = BlockTime::new(height, timestamp);
|
||||
let mut new_tx_details = tx_details.clone();
|
||||
new_tx_details.confirmation_time = confirmation_time;
|
||||
batch.set_tx(&new_tx_details)?;
|
||||
@@ -359,7 +359,7 @@ fn save_transaction_details_and_utxos<D: BatchDatabase>(
|
||||
transaction: Some(tx),
|
||||
received: incoming,
|
||||
sent: outgoing,
|
||||
confirmation_time: ConfirmationTime::new(height, timestamp),
|
||||
confirmation_time: BlockTime::new(height, timestamp),
|
||||
fee: Some(inputs_sum.saturating_sub(outputs_sum)), /* if the tx is a coinbase, fees would be negative */
|
||||
verified: height.is_some(),
|
||||
};
|
||||
|
||||
@@ -144,6 +144,9 @@ impl BatchOperations for AnyDatabase {
|
||||
fn set_last_index(&mut self, keychain: KeychainKind, value: u32) -> Result<(), Error> {
|
||||
impl_inner_method!(AnyDatabase, self, set_last_index, keychain, value)
|
||||
}
|
||||
fn set_sync_time(&mut self, sync_time: SyncTime) -> Result<(), Error> {
|
||||
impl_inner_method!(AnyDatabase, self, set_sync_time, sync_time)
|
||||
}
|
||||
|
||||
fn del_script_pubkey_from_path(
|
||||
&mut self,
|
||||
@@ -180,6 +183,9 @@ impl BatchOperations for AnyDatabase {
|
||||
fn del_last_index(&mut self, keychain: KeychainKind) -> Result<Option<u32>, Error> {
|
||||
impl_inner_method!(AnyDatabase, self, del_last_index, keychain)
|
||||
}
|
||||
fn del_sync_time(&mut self) -> Result<Option<SyncTime>, Error> {
|
||||
impl_inner_method!(AnyDatabase, self, del_sync_time)
|
||||
}
|
||||
}
|
||||
|
||||
impl Database for AnyDatabase {
|
||||
@@ -241,6 +247,9 @@ impl Database for AnyDatabase {
|
||||
fn get_last_index(&self, keychain: KeychainKind) -> Result<Option<u32>, Error> {
|
||||
impl_inner_method!(AnyDatabase, self, get_last_index, keychain)
|
||||
}
|
||||
fn get_sync_time(&self) -> Result<Option<SyncTime>, Error> {
|
||||
impl_inner_method!(AnyDatabase, self, get_sync_time)
|
||||
}
|
||||
|
||||
fn increment_last_index(&mut self, keychain: KeychainKind) -> Result<u32, Error> {
|
||||
impl_inner_method!(AnyDatabase, self, increment_last_index, keychain)
|
||||
@@ -272,6 +281,9 @@ impl BatchOperations for AnyBatch {
|
||||
fn set_last_index(&mut self, keychain: KeychainKind, value: u32) -> Result<(), Error> {
|
||||
impl_inner_method!(AnyBatch, self, set_last_index, keychain, value)
|
||||
}
|
||||
fn set_sync_time(&mut self, sync_time: SyncTime) -> Result<(), Error> {
|
||||
impl_inner_method!(AnyBatch, self, set_sync_time, sync_time)
|
||||
}
|
||||
|
||||
fn del_script_pubkey_from_path(
|
||||
&mut self,
|
||||
@@ -302,6 +314,9 @@ impl BatchOperations for AnyBatch {
|
||||
fn del_last_index(&mut self, keychain: KeychainKind) -> Result<Option<u32>, Error> {
|
||||
impl_inner_method!(AnyBatch, self, del_last_index, keychain)
|
||||
}
|
||||
fn del_sync_time(&mut self) -> Result<Option<SyncTime>, Error> {
|
||||
impl_inner_method!(AnyBatch, self, del_sync_time)
|
||||
}
|
||||
}
|
||||
|
||||
impl BatchDatabase for AnyDatabase {
|
||||
|
||||
@@ -18,7 +18,7 @@ use bitcoin::hash_types::Txid;
|
||||
use bitcoin::{OutPoint, Script, Transaction};
|
||||
|
||||
use crate::database::memory::MapKey;
|
||||
use crate::database::{BatchDatabase, BatchOperations, Database};
|
||||
use crate::database::{BatchDatabase, BatchOperations, Database, SyncTime};
|
||||
use crate::error::Error;
|
||||
use crate::types::*;
|
||||
|
||||
@@ -82,6 +82,13 @@ macro_rules! impl_batch_operations {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_sync_time(&mut self, data: SyncTime) -> Result<(), Error> {
|
||||
let key = MapKey::SyncTime.as_map_key();
|
||||
self.insert(key, serde_json::to_vec(&data)?)$($after_insert)*;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn del_script_pubkey_from_path(&mut self, keychain: KeychainKind, path: u32) -> Result<Option<Script>, Error> {
|
||||
let key = MapKey::Path((Some(keychain), Some(path))).as_map_key();
|
||||
let res = self.remove(key);
|
||||
@@ -168,6 +175,14 @@ macro_rules! impl_batch_operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn del_sync_time(&mut self) -> Result<Option<SyncTime>, Error> {
|
||||
let key = MapKey::SyncTime.as_map_key();
|
||||
let res = self.remove(key);
|
||||
let res = $process_delete!(res);
|
||||
|
||||
Ok(res.map(|b| serde_json::from_slice(&b)).transpose()?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,6 +357,14 @@ impl Database for Tree {
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn get_sync_time(&self) -> Result<Option<SyncTime>, Error> {
|
||||
let key = MapKey::SyncTime.as_map_key();
|
||||
Ok(self
|
||||
.get(key)?
|
||||
.map(|b| serde_json::from_slice(&b))
|
||||
.transpose()?)
|
||||
}
|
||||
|
||||
// inserts 0 if not present
|
||||
fn increment_last_index(&mut self, keychain: KeychainKind) -> Result<u32, Error> {
|
||||
let key = MapKey::LastIndex(keychain).as_map_key();
|
||||
@@ -470,4 +493,9 @@ mod test {
|
||||
fn test_last_index() {
|
||||
crate::database::test::test_last_index(get_tree());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_time() {
|
||||
crate::database::test::test_sync_time(get_tree());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ use bitcoin::consensus::encode::{deserialize, serialize};
|
||||
use bitcoin::hash_types::Txid;
|
||||
use bitcoin::{OutPoint, Script, Transaction};
|
||||
|
||||
use crate::database::{BatchDatabase, BatchOperations, ConfigurableDatabase, Database};
|
||||
use crate::database::{BatchDatabase, BatchOperations, ConfigurableDatabase, Database, SyncTime};
|
||||
use crate::error::Error;
|
||||
use crate::types::*;
|
||||
|
||||
@@ -33,6 +33,7 @@ use crate::types::*;
|
||||
// transactions t<txid> -> tx details
|
||||
// deriv indexes c{i,e} -> u32
|
||||
// descriptor checksum d{i,e} -> vec<u8>
|
||||
// last sync time l -> { height, timestamp }
|
||||
|
||||
pub(crate) enum MapKey<'a> {
|
||||
Path((Option<KeychainKind>, Option<u32>)),
|
||||
@@ -41,6 +42,7 @@ pub(crate) enum MapKey<'a> {
|
||||
RawTx(Option<&'a Txid>),
|
||||
Transaction(Option<&'a Txid>),
|
||||
LastIndex(KeychainKind),
|
||||
SyncTime,
|
||||
DescriptorChecksum(KeychainKind),
|
||||
}
|
||||
|
||||
@@ -59,6 +61,7 @@ impl MapKey<'_> {
|
||||
MapKey::RawTx(_) => b"r".to_vec(),
|
||||
MapKey::Transaction(_) => b"t".to_vec(),
|
||||
MapKey::LastIndex(st) => [b"c", st.as_ref()].concat(),
|
||||
MapKey::SyncTime => b"l".to_vec(),
|
||||
MapKey::DescriptorChecksum(st) => [b"d", st.as_ref()].concat(),
|
||||
}
|
||||
}
|
||||
@@ -180,6 +183,12 @@ impl BatchOperations for MemoryDatabase {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
fn set_sync_time(&mut self, data: SyncTime) -> Result<(), Error> {
|
||||
let key = MapKey::SyncTime.as_map_key();
|
||||
self.map.insert(key, Box::new(data));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn del_script_pubkey_from_path(
|
||||
&mut self,
|
||||
@@ -270,6 +279,13 @@ impl BatchOperations for MemoryDatabase {
|
||||
Some(b) => Ok(Some(*b.downcast_ref().unwrap())),
|
||||
}
|
||||
}
|
||||
fn del_sync_time(&mut self) -> Result<Option<SyncTime>, Error> {
|
||||
let key = MapKey::SyncTime.as_map_key();
|
||||
let res = self.map.remove(&key);
|
||||
self.deleted_keys.push(key);
|
||||
|
||||
Ok(res.map(|b| b.downcast_ref().cloned().unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Database for MemoryDatabase {
|
||||
@@ -407,6 +423,14 @@ impl Database for MemoryDatabase {
|
||||
Ok(self.map.get(&key).map(|b| *b.downcast_ref().unwrap()))
|
||||
}
|
||||
|
||||
fn get_sync_time(&self) -> Result<Option<SyncTime>, Error> {
|
||||
let key = MapKey::SyncTime.as_map_key();
|
||||
Ok(self
|
||||
.map
|
||||
.get(&key)
|
||||
.map(|b| b.downcast_ref().cloned().unwrap()))
|
||||
}
|
||||
|
||||
// inserts 0 if not present
|
||||
fn increment_last_index(&mut self, keychain: KeychainKind) -> Result<u32, Error> {
|
||||
let key = MapKey::LastIndex(keychain).as_map_key();
|
||||
@@ -479,12 +503,10 @@ macro_rules! populate_test_db {
|
||||
};
|
||||
|
||||
let txid = tx.txid();
|
||||
let confirmation_time = tx_meta
|
||||
.min_confirmations
|
||||
.map(|conf| $crate::ConfirmationTime {
|
||||
height: current_height.unwrap().checked_sub(conf as u32).unwrap(),
|
||||
timestamp: 0,
|
||||
});
|
||||
let confirmation_time = tx_meta.min_confirmations.map(|conf| $crate::BlockTime {
|
||||
height: current_height.unwrap().checked_sub(conf as u32).unwrap(),
|
||||
timestamp: 0,
|
||||
});
|
||||
|
||||
let tx_details = $crate::TransactionDetails {
|
||||
transaction: Some(tx.clone()),
|
||||
@@ -590,4 +612,9 @@ mod test {
|
||||
fn test_last_index() {
|
||||
crate::database::test::test_last_index(get_tree());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_time() {
|
||||
crate::database::test::test_sync_time(get_tree());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
//!
|
||||
//! [`Wallet`]: crate::wallet::Wallet
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use bitcoin::hash_types::Txid;
|
||||
use bitcoin::{OutPoint, Script, Transaction, TxOut};
|
||||
|
||||
@@ -44,6 +46,15 @@ pub use sqlite::SqliteDatabase;
|
||||
pub mod memory;
|
||||
pub use memory::MemoryDatabase;
|
||||
|
||||
/// Blockchain state at the time of syncing
|
||||
///
|
||||
/// Contains only the block time and height at the moment
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SyncTime {
|
||||
/// Block timestamp and height at the time of sync
|
||||
pub block_time: BlockTime,
|
||||
}
|
||||
|
||||
/// Trait for operations that can be batched
|
||||
///
|
||||
/// This trait defines the list of operations that must be implemented on the [`Database`] type and
|
||||
@@ -64,6 +75,8 @@ pub trait BatchOperations {
|
||||
fn set_tx(&mut self, transaction: &TransactionDetails) -> Result<(), Error>;
|
||||
/// Store the last derivation index for a given keychain.
|
||||
fn set_last_index(&mut self, keychain: KeychainKind, value: u32) -> Result<(), Error>;
|
||||
/// Store the sync time
|
||||
fn set_sync_time(&mut self, sync_time: SyncTime) -> Result<(), Error>;
|
||||
|
||||
/// Delete a script_pubkey given the keychain and its child number.
|
||||
fn del_script_pubkey_from_path(
|
||||
@@ -89,6 +102,10 @@ pub trait BatchOperations {
|
||||
) -> Result<Option<TransactionDetails>, Error>;
|
||||
/// Delete the last derivation index for a keychain.
|
||||
fn del_last_index(&mut self, keychain: KeychainKind) -> Result<Option<u32>, Error>;
|
||||
/// Reset the sync time to `None`
|
||||
///
|
||||
/// Returns the removed value
|
||||
fn del_sync_time(&mut self) -> Result<Option<SyncTime>, Error>;
|
||||
}
|
||||
|
||||
/// Trait for reading data from a database
|
||||
@@ -134,6 +151,8 @@ pub trait Database: BatchOperations {
|
||||
fn get_tx(&self, txid: &Txid, include_raw: bool) -> Result<Option<TransactionDetails>, Error>;
|
||||
/// Return the last defivation index for a keychain.
|
||||
fn get_last_index(&self, keychain: KeychainKind) -> Result<Option<u32>, Error>;
|
||||
/// Return the sync time, if present
|
||||
fn get_sync_time(&self) -> Result<Option<SyncTime>, Error>;
|
||||
|
||||
/// Increment the last derivation index for a keychain and return it
|
||||
///
|
||||
@@ -325,7 +344,7 @@ pub mod test {
|
||||
received: 1337,
|
||||
sent: 420420,
|
||||
fee: Some(140),
|
||||
confirmation_time: Some(ConfirmationTime {
|
||||
confirmation_time: Some(BlockTime {
|
||||
timestamp: 123456,
|
||||
height: 1000,
|
||||
}),
|
||||
@@ -377,5 +396,25 @@ pub mod test {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn test_sync_time<D: Database>(mut tree: D) {
|
||||
assert!(tree.get_sync_time().unwrap().is_none());
|
||||
|
||||
tree.set_sync_time(SyncTime {
|
||||
block_time: BlockTime {
|
||||
height: 100,
|
||||
timestamp: 1000,
|
||||
},
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let extracted = tree.get_sync_time().unwrap();
|
||||
assert!(extracted.is_some());
|
||||
assert_eq!(extracted.as_ref().unwrap().block_time.height, 100);
|
||||
assert_eq!(extracted.as_ref().unwrap().block_time.timestamp, 1000);
|
||||
|
||||
tree.del_sync_time().unwrap();
|
||||
assert!(tree.get_sync_time().unwrap().is_none());
|
||||
}
|
||||
|
||||
// TODO: more tests...
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use bitcoin::consensus::encode::{deserialize, serialize};
|
||||
use bitcoin::hash_types::Txid;
|
||||
use bitcoin::{OutPoint, Script, Transaction, TxOut};
|
||||
|
||||
use crate::database::{BatchDatabase, BatchOperations, Database};
|
||||
use crate::database::{BatchDatabase, BatchOperations, Database, SyncTime};
|
||||
use crate::error::Error;
|
||||
use crate::types::*;
|
||||
|
||||
@@ -35,6 +35,7 @@ static MIGRATIONS: &[&str] = &[
|
||||
"CREATE UNIQUE INDEX idx_indices_keychain ON last_derivation_indices(keychain);",
|
||||
"CREATE TABLE checksums (keychain TEXT, checksum BLOB);",
|
||||
"CREATE INDEX idx_checksums_keychain ON checksums(keychain);",
|
||||
"CREATE TABLE sync_time (id INTEGER PRIMARY KEY, height INTEGER, timestamp INTEGER);"
|
||||
];
|
||||
|
||||
/// Sqlite database stored on filesystem
|
||||
@@ -205,6 +206,19 @@ impl SqliteDatabase {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_sync_time(&self, data: SyncTime) -> Result<i64, Error> {
|
||||
let mut statement = self.connection.prepare_cached(
|
||||
"INSERT INTO sync_time (id, height, timestamp) VALUES (0, :height, :timestamp) ON CONFLICT(id) DO UPDATE SET height=:height, timestamp=:timestamp WHERE id = 0",
|
||||
)?;
|
||||
|
||||
statement.execute(named_params! {
|
||||
":height": data.block_time.height,
|
||||
":timestamp": data.block_time.timestamp,
|
||||
})?;
|
||||
|
||||
Ok(self.connection.last_insert_rowid())
|
||||
}
|
||||
|
||||
fn select_script_pubkeys(&self) -> Result<Vec<Script>, Error> {
|
||||
let mut statement = self
|
||||
.connection
|
||||
@@ -375,7 +389,7 @@ impl SqliteDatabase {
|
||||
};
|
||||
|
||||
let confirmation_time = match (height, timestamp) {
|
||||
(Some(height), Some(timestamp)) => Some(ConfirmationTime { height, timestamp }),
|
||||
(Some(height), Some(timestamp)) => Some(BlockTime { height, timestamp }),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
@@ -409,7 +423,7 @@ impl SqliteDatabase {
|
||||
let verified: bool = row.get(6)?;
|
||||
|
||||
let confirmation_time = match (height, timestamp) {
|
||||
(Some(height), Some(timestamp)) => Some(ConfirmationTime { height, timestamp }),
|
||||
(Some(height), Some(timestamp)) => Some(BlockTime { height, timestamp }),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
@@ -452,7 +466,7 @@ impl SqliteDatabase {
|
||||
};
|
||||
|
||||
let confirmation_time = match (height, timestamp) {
|
||||
(Some(height), Some(timestamp)) => Some(ConfirmationTime { height, timestamp }),
|
||||
(Some(height), Some(timestamp)) => Some(BlockTime { height, timestamp }),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
@@ -487,6 +501,24 @@ impl SqliteDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
fn select_sync_time(&self) -> Result<Option<SyncTime>, Error> {
|
||||
let mut statement = self
|
||||
.connection
|
||||
.prepare_cached("SELECT height, timestamp FROM sync_time WHERE id = 0")?;
|
||||
let mut rows = statement.query([])?;
|
||||
|
||||
if let Some(row) = rows.next()? {
|
||||
Ok(Some(SyncTime {
|
||||
block_time: BlockTime {
|
||||
height: row.get(0)?,
|
||||
timestamp: row.get(1)?,
|
||||
},
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn select_checksum_by_keychain(&self, keychain: String) -> Result<Option<Vec<u8>>, Error> {
|
||||
let mut statement = self
|
||||
.connection
|
||||
@@ -563,6 +595,14 @@ impl SqliteDatabase {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_sync_time(&self) -> Result<(), Error> {
|
||||
let mut statement = self
|
||||
.connection
|
||||
.prepare_cached("DELETE FROM sync_time WHERE id = 0")?;
|
||||
statement.execute([])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl BatchOperations for SqliteDatabase {
|
||||
@@ -622,6 +662,11 @@ impl BatchOperations for SqliteDatabase {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_sync_time(&mut self, ct: SyncTime) -> Result<(), Error> {
|
||||
self.update_sync_time(ct)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn del_script_pubkey_from_path(
|
||||
&mut self,
|
||||
keychain: KeychainKind,
|
||||
@@ -707,6 +752,17 @@ impl BatchOperations for SqliteDatabase {
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn del_sync_time(&mut self) -> Result<Option<SyncTime>, Error> {
|
||||
match self.select_sync_time()? {
|
||||
Some(value) => {
|
||||
self.delete_sync_time()?;
|
||||
|
||||
Ok(Some(value))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Database for SqliteDatabase {
|
||||
@@ -818,6 +874,10 @@ impl Database for SqliteDatabase {
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn get_sync_time(&self) -> Result<Option<SyncTime>, Error> {
|
||||
self.select_sync_time()
|
||||
}
|
||||
|
||||
fn increment_last_index(&mut self, keychain: KeychainKind) -> Result<u32, Error> {
|
||||
let keychain_string = serde_json::to_string(&keychain)?;
|
||||
match self.get_last_index(keychain)? {
|
||||
@@ -965,4 +1025,9 @@ pub mod test {
|
||||
fn test_last_index() {
|
||||
crate::database::test::test_last_index(get_database());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_time() {
|
||||
crate::database::test::test_sync_time(get_database());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,23 @@ use bitcoin::Network;
|
||||
|
||||
use miniscript::ScriptContext;
|
||||
|
||||
pub use bip39::{Language, Mnemonic, MnemonicType, Seed};
|
||||
pub use bip39::{Language, Mnemonic};
|
||||
|
||||
type Seed = [u8; 64];
|
||||
|
||||
/// Type describing entropy length (aka word count) in the mnemonic
|
||||
pub enum WordCount {
|
||||
/// 12 words mnemonic (128 bits entropy)
|
||||
Words12 = 128,
|
||||
/// 15 words mnemonic (160 bits entropy)
|
||||
Words15 = 160,
|
||||
/// 18 words mnemonic (192 bits entropy)
|
||||
Words18 = 192,
|
||||
/// 21 words mnemonic (224 bits entropy)
|
||||
Words21 = 224,
|
||||
/// 24 words mnemonic (256 bits entropy)
|
||||
Words24 = 256,
|
||||
}
|
||||
|
||||
use super::{
|
||||
any_network, DerivableKey, DescriptorKey, ExtendedKey, GeneratableKey, GeneratedKey, KeyError,
|
||||
@@ -40,7 +56,7 @@ pub type MnemonicWithPassphrase = (Mnemonic, Option<String>);
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
|
||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for Seed {
|
||||
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||
Ok(bip32::ExtendedPrivKey::new_master(Network::Bitcoin, self.as_bytes())?.into())
|
||||
Ok(bip32::ExtendedPrivKey::new_master(Network::Bitcoin, &self[..])?.into())
|
||||
}
|
||||
|
||||
fn into_descriptor_key(
|
||||
@@ -60,7 +76,7 @@ impl<Ctx: ScriptContext> DerivableKey<Ctx> for Seed {
|
||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for MnemonicWithPassphrase {
|
||||
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||
let (mnemonic, passphrase) = self;
|
||||
let seed = Seed::new(&mnemonic, passphrase.as_deref().unwrap_or(""));
|
||||
let seed: Seed = mnemonic.to_seed(passphrase.as_deref().unwrap_or(""));
|
||||
|
||||
seed.into_extended_key()
|
||||
}
|
||||
@@ -101,15 +117,15 @@ impl<Ctx: ScriptContext> DerivableKey<Ctx> for Mnemonic {
|
||||
impl<Ctx: ScriptContext> GeneratableKey<Ctx> for Mnemonic {
|
||||
type Entropy = [u8; 32];
|
||||
|
||||
type Options = (MnemonicType, Language);
|
||||
type Error = Option<bip39::ErrorKind>;
|
||||
type Options = (WordCount, Language);
|
||||
type Error = Option<bip39::Error>;
|
||||
|
||||
fn generate_with_entropy(
|
||||
(mnemonic_type, language): Self::Options,
|
||||
(word_count, language): Self::Options,
|
||||
entropy: Self::Entropy,
|
||||
) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
|
||||
let entropy = &entropy.as_ref()[..(mnemonic_type.entropy_bits() / 8)];
|
||||
let mnemonic = Mnemonic::from_entropy(entropy, language).map_err(|e| e.downcast().ok())?;
|
||||
let entropy = &entropy.as_ref()[..(word_count as usize / 8)];
|
||||
let mnemonic = Mnemonic::from_entropy_in(language, entropy)?;
|
||||
|
||||
Ok(GeneratedKey::new(mnemonic, any_network()))
|
||||
}
|
||||
@@ -121,15 +137,17 @@ mod test {
|
||||
|
||||
use bitcoin::util::bip32;
|
||||
|
||||
use bip39::{Language, Mnemonic, MnemonicType};
|
||||
use bip39::{Language, Mnemonic};
|
||||
|
||||
use crate::keys::{any_network, GeneratableKey, GeneratedKey};
|
||||
|
||||
use super::WordCount;
|
||||
|
||||
#[test]
|
||||
fn test_keys_bip39_mnemonic() {
|
||||
let mnemonic =
|
||||
"aim bunker wash balance finish force paper analyst cabin spoon stable organ";
|
||||
let mnemonic = Mnemonic::from_phrase(mnemonic, Language::English).unwrap();
|
||||
let mnemonic = Mnemonic::parse_in(Language::English, mnemonic).unwrap();
|
||||
let path = bip32::DerivationPath::from_str("m/44'/0'/0'/0").unwrap();
|
||||
|
||||
let key = (mnemonic, path);
|
||||
@@ -143,7 +161,7 @@ mod test {
|
||||
fn test_keys_bip39_mnemonic_passphrase() {
|
||||
let mnemonic =
|
||||
"aim bunker wash balance finish force paper analyst cabin spoon stable organ";
|
||||
let mnemonic = Mnemonic::from_phrase(mnemonic, Language::English).unwrap();
|
||||
let mnemonic = Mnemonic::parse_in(Language::English, mnemonic).unwrap();
|
||||
let path = bip32::DerivationPath::from_str("m/44'/0'/0'/0").unwrap();
|
||||
|
||||
let key = ((mnemonic, Some("passphrase".into())), path);
|
||||
@@ -157,7 +175,7 @@ mod test {
|
||||
fn test_keys_generate_bip39() {
|
||||
let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> =
|
||||
Mnemonic::generate_with_entropy(
|
||||
(MnemonicType::Words12, Language::English),
|
||||
(WordCount::Words12, Language::English),
|
||||
crate::keys::test::TEST_ENTROPY,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -169,7 +187,7 @@ mod test {
|
||||
|
||||
let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> =
|
||||
Mnemonic::generate_with_entropy(
|
||||
(MnemonicType::Words24, Language::English),
|
||||
(WordCount::Words24, Language::English),
|
||||
crate::keys::test::TEST_ENTROPY,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -180,11 +198,11 @@ mod test {
|
||||
#[test]
|
||||
fn test_keys_generate_bip39_random() {
|
||||
let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> =
|
||||
Mnemonic::generate((MnemonicType::Words12, Language::English)).unwrap();
|
||||
Mnemonic::generate((WordCount::Words12, Language::English)).unwrap();
|
||||
assert_eq!(generated_mnemonic.valid_networks, any_network());
|
||||
|
||||
let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> =
|
||||
Mnemonic::generate((MnemonicType::Words24, Language::English)).unwrap();
|
||||
Mnemonic::generate((WordCount::Words24, Language::English)).unwrap();
|
||||
assert_eq!(generated_mnemonic.valid_networks, any_network());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,9 +460,9 @@ use bdk::keys::bip39::{Mnemonic, Language};
|
||||
|
||||
# fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let xkey: ExtendedKey =
|
||||
Mnemonic::from_phrase(
|
||||
Mnemonic::parse_in(
|
||||
Language::English,
|
||||
"jelly crash boy whisper mouse ecology tuna soccer memory million news short",
|
||||
Language::English
|
||||
)?
|
||||
.into_extended_key()?;
|
||||
let xprv = xkey.into_xprv(Network::Bitcoin).unwrap();
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
//! interact with the bitcoin P2P network.
|
||||
//!
|
||||
//! ```toml
|
||||
//! bdk = "0.13.0"
|
||||
//! bdk = "0.14.0"
|
||||
//! ```
|
||||
#![cfg_attr(
|
||||
feature = "electrum",
|
||||
|
||||
@@ -145,9 +145,7 @@ impl TestClient {
|
||||
|
||||
let bumped: serde_json::Value = self.call("bumpfee", &[txid.to_string().into()]).unwrap();
|
||||
let new_txid = Txid::from_str(&bumped["txid"].as_str().unwrap().to_string()).unwrap();
|
||||
|
||||
let monitor_script =
|
||||
tx.vout[0].script_pub_key.addresses.as_ref().unwrap()[0].script_pubkey();
|
||||
let monitor_script = Script::from_hex(&mut tx.vout[0].script_pub_key.hex.to_hex()).unwrap();
|
||||
self.wait_for_tx(new_txid, &monitor_script);
|
||||
|
||||
debug!("Bumped {}, new txid {}", txid, new_txid);
|
||||
@@ -394,6 +392,9 @@ macro_rules! bdk_blockchain_tests {
|
||||
|
||||
#[test]
|
||||
fn test_sync_simple() {
|
||||
use std::ops::Deref;
|
||||
use crate::database::Database;
|
||||
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
|
||||
let tx = testutils! {
|
||||
@@ -402,7 +403,13 @@ macro_rules! bdk_blockchain_tests {
|
||||
println!("{:?}", tx);
|
||||
let txid = test_client.receive(tx);
|
||||
|
||||
// the RPC blockchain needs to call `sync()` during initialization to import the
|
||||
// addresses (see `init_single_sig()`), so we skip this assertion
|
||||
#[cfg(not(feature = "test-rpc"))]
|
||||
assert!(wallet.database().deref().get_sync_time().unwrap().is_none(), "initial sync_time not none");
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert!(wallet.database().deref().get_sync_time().unwrap().is_some(), "sync_time hasn't been updated");
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
assert_eq!(wallet.list_unspent().unwrap()[0].keychain, KeychainKind::External, "incorrect keychain kind");
|
||||
@@ -594,7 +601,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
let tx = psbt.extract_tx();
|
||||
println!("{}", bitcoin::consensus::encode::serialize_hex(&tx));
|
||||
wallet.broadcast(tx).unwrap();
|
||||
wallet.broadcast(&tx).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after send");
|
||||
|
||||
@@ -645,7 +652,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
let sent_txid = wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
let sent_txid = wallet.broadcast(&psbt.extract_tx()).unwrap();
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after receive");
|
||||
@@ -688,7 +695,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
wallet.broadcast(&psbt.extract_tx()).unwrap();
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
@@ -727,7 +734,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
wallet.broadcast(&psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees");
|
||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance from received");
|
||||
@@ -737,7 +744,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
let (mut new_psbt, new_details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
||||
wallet.broadcast(&new_psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - new_details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees after bump");
|
||||
assert_eq!(wallet.get_balance().unwrap(), new_details.received, "incorrect balance from received after bump");
|
||||
@@ -762,7 +769,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
wallet.broadcast(&psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect received after send");
|
||||
@@ -772,7 +779,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
let (mut new_psbt, new_details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
||||
wallet.broadcast(&new_psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after change removal");
|
||||
assert_eq!(new_details.received, 0, "incorrect received after change removal");
|
||||
@@ -797,7 +804,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
wallet.broadcast(&psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||
assert_eq!(details.received, 1_000 - details.fee.unwrap_or(0), "incorrect received after send");
|
||||
@@ -807,7 +814,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
let (mut new_psbt, new_details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
||||
wallet.broadcast(&new_psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(new_details.sent, 75_000, "incorrect sent");
|
||||
assert_eq!(wallet.get_balance().unwrap(), new_details.received, "incorrect balance after add input");
|
||||
@@ -830,7 +837,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
wallet.broadcast(&psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||
assert_eq!(details.received, 1_000 - details.fee.unwrap_or(0), "incorrect received after send");
|
||||
@@ -842,13 +849,44 @@ macro_rules! bdk_blockchain_tests {
|
||||
|
||||
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
||||
wallet.broadcast(&new_psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(new_details.sent, 75_000, "incorrect sent");
|
||||
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after add input");
|
||||
assert_eq!(new_details.received, 0, "incorrect received after add input");
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_add_data() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
let node_addr = test_client.get_node_address(None);
|
||||
let _ = test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
let data = [42u8;80];
|
||||
builder.add_data(&data);
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
let tx = psbt.extract_tx();
|
||||
let serialized_tx = bitcoin::consensus::encode::serialize(&tx);
|
||||
assert!(serialized_tx.windows(data.len()).any(|e| e==data), "cannot find op_return data in transaction");
|
||||
let sent_txid = wallet.broadcast(&tx).unwrap();
|
||||
test_client.generate(1, Some(node_addr));
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||
|
||||
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
|
||||
let _ = tx_map.get(&sent_txid).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_receive_coinbase() {
|
||||
let (wallet, _, mut test_client) = init_single_sig();
|
||||
@@ -871,6 +909,102 @@ macro_rules! bdk_blockchain_tests {
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert!(wallet.get_balance().unwrap() > 0, "incorrect balance after receiving coinbase");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_send_to_bech32m_addr() {
|
||||
use std::str::FromStr;
|
||||
use serde;
|
||||
use serde_json;
|
||||
use serde::Serialize;
|
||||
use bitcoincore_rpc::jsonrpc::serde_json::Value;
|
||||
use bitcoincore_rpc::{Auth, Client, RpcApi};
|
||||
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
|
||||
// TODO remove once rust-bitcoincore-rpc with PR 199 released
|
||||
// https://github.com/rust-bitcoin/rust-bitcoincore-rpc/pull/199
|
||||
/// Import Descriptor Request
|
||||
#[derive(Serialize, Clone, PartialEq, Eq, Debug)]
|
||||
pub struct ImportDescriptorRequest {
|
||||
pub active: bool,
|
||||
#[serde(rename = "desc")]
|
||||
pub descriptor: String,
|
||||
pub range: [i64; 2],
|
||||
pub next_index: i64,
|
||||
pub timestamp: String,
|
||||
pub internal: bool,
|
||||
}
|
||||
|
||||
// TODO remove once rust-bitcoincore-rpc with PR 199 released
|
||||
impl ImportDescriptorRequest {
|
||||
/// Create a new Import Descriptor request providing just the descriptor and internal flags
|
||||
pub fn new(descriptor: &str, internal: bool) -> Self {
|
||||
ImportDescriptorRequest {
|
||||
descriptor: descriptor.to_string(),
|
||||
internal,
|
||||
active: true,
|
||||
range: [0, 100],
|
||||
next_index: 0,
|
||||
timestamp: "now".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Create and add descriptors to a test bitcoind node taproot wallet
|
||||
|
||||
// TODO replace once rust-bitcoincore-rpc with PR 174 released
|
||||
// https://github.com/rust-bitcoin/rust-bitcoincore-rpc/pull/174
|
||||
let _createwallet_result: Value = test_client.bitcoind.client.call("createwallet", &["taproot_wallet".into(),false.into(),true.into(),serde_json::to_value("").unwrap(), false.into(), true.into()]).unwrap();
|
||||
|
||||
// TODO replace once bitcoind released with support for rust-bitcoincore-rpc PR 174
|
||||
let taproot_wallet_client = Client::new(&test_client.bitcoind.rpc_url_with_wallet("taproot_wallet"), Auth::CookieFile(test_client.bitcoind.params.cookie_file.clone())).unwrap();
|
||||
|
||||
let wallet_descriptor = "tr(tprv8ZgxMBicQKsPdBtxmEMPnNq58KGusNAimQirKFHqX2yk2D8q1v6pNLiKYVAdzDHy2w3vF4chuGfMvNtzsbTTLVXBcdkCA1rje1JG6oksWv8/86h/1h/0h/0/*)#y283ssmn";
|
||||
let change_descriptor = "tr(tprv8ZgxMBicQKsPdBtxmEMPnNq58KGusNAimQirKFHqX2yk2D8q1v6pNLiKYVAdzDHy2w3vF4chuGfMvNtzsbTTLVXBcdkCA1rje1JG6oksWv8/86h/1h/0h/1/*)#47zsd9tt";
|
||||
|
||||
let tr_descriptors = vec![
|
||||
ImportDescriptorRequest::new(wallet_descriptor, false),
|
||||
ImportDescriptorRequest::new(change_descriptor, false),
|
||||
];
|
||||
|
||||
// TODO replace once rust-bitcoincore-rpc with PR 199 released
|
||||
let _import_result: Value = taproot_wallet_client.call("importdescriptors", &[serde_json::to_value(tr_descriptors).unwrap()]).unwrap();
|
||||
|
||||
// 2. Get a new bech32m address from test bitcoind node taproot wallet
|
||||
|
||||
// TODO replace once rust-bitcoincore-rpc with PR 199 released
|
||||
let node_addr: bitcoin::Address = taproot_wallet_client.call("getnewaddress", &["test address".into(), "bech32m".into()]).unwrap();
|
||||
assert_eq!(node_addr, bitcoin::Address::from_str("bcrt1pj5y3f0fu4y7g98k4v63j9n0xvj3lmln0cpwhsjzknm6nt0hr0q7qnzwsy9").unwrap());
|
||||
|
||||
// 3. Send 50_000 sats from test bitcoind node to test BDK wallet
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "wallet has incorrect balance");
|
||||
|
||||
// 4. Send 25_000 sats from test BDK wallet to test bitcoind node taproot wallet
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey(), 25_000);
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "wallet cannot finalize transaction");
|
||||
let tx = psbt.extract_tx();
|
||||
wallet.broadcast(&tx).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "wallet has incorrect balance after send");
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "wallet has incorrect number of txs");
|
||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "wallet has incorrect number of unspents");
|
||||
test_client.generate(1, None);
|
||||
|
||||
// 5. Verify 25_000 sats are received by test bitcoind node taproot wallet
|
||||
|
||||
let taproot_balance = taproot_wallet_client.get_balance(None, None).unwrap();
|
||||
assert_eq!(taproot_balance.as_sat(), 25_000, "node has incorrect taproot wallet balance");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
18
src/types.rs
18
src/types.rs
@@ -210,7 +210,7 @@ pub struct TransactionDetails {
|
||||
pub fee: Option<u64>,
|
||||
/// If the transaction is confirmed, contains height and timestamp of the block containing the
|
||||
/// transaction, unconfirmed transaction contains `None`.
|
||||
pub confirmation_time: Option<ConfirmationTime>,
|
||||
pub confirmation_time: Option<BlockTime>,
|
||||
/// Whether the tx has been verified against the consensus rules
|
||||
///
|
||||
/// Confirmed txs are considered "verified" by default, while unconfirmed txs are checked to
|
||||
@@ -222,20 +222,26 @@ pub struct TransactionDetails {
|
||||
pub verified: bool,
|
||||
}
|
||||
|
||||
/// Block height and timestamp of the block containing the confirmed transaction
|
||||
/// Block height and timestamp of a block
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct ConfirmationTime {
|
||||
pub struct BlockTime {
|
||||
/// confirmation block height
|
||||
pub height: u32,
|
||||
/// confirmation block timestamp
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
impl ConfirmationTime {
|
||||
/// Returns `Some` `ConfirmationTime` if both `height` and `timestamp` are `Some`
|
||||
/// **DEPRECATED**: Confirmation time of a transaction
|
||||
///
|
||||
/// The structure has been renamed to `BlockTime`
|
||||
#[deprecated(note = "This structure has been renamed to `BlockTime`")]
|
||||
pub type ConfirmationTime = BlockTime;
|
||||
|
||||
impl BlockTime {
|
||||
/// Returns `Some` `BlockTime` if both `height` and `timestamp` are `Some`
|
||||
pub fn new(height: Option<u32>, timestamp: Option<u64>) -> Option<Self> {
|
||||
match (height, timestamp) {
|
||||
(Some(height), Some(timestamp)) => Some(ConfirmationTime { height, timestamp }),
|
||||
(Some(height), Some(timestamp)) => Some(BlockTime { height, timestamp }),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ mod test {
|
||||
use crate::database::{memory::MemoryDatabase, BatchOperations};
|
||||
use crate::types::TransactionDetails;
|
||||
use crate::wallet::Wallet;
|
||||
use crate::ConfirmationTime;
|
||||
use crate::BlockTime;
|
||||
|
||||
fn get_test_db() -> MemoryDatabase {
|
||||
let mut db = MemoryDatabase::new();
|
||||
@@ -226,7 +226,7 @@ mod test {
|
||||
received: 100_000,
|
||||
sent: 0,
|
||||
fee: Some(500),
|
||||
confirmation_time: Some(ConfirmationTime {
|
||||
confirmation_time: Some(BlockTime {
|
||||
timestamp: 12345678,
|
||||
height: 5000,
|
||||
}),
|
||||
|
||||
@@ -57,7 +57,7 @@ use utils::{check_nlocktime, check_nsequence_rbf, After, Older, SecpCtx, DUST_LI
|
||||
|
||||
use crate::blockchain::{Blockchain, Progress};
|
||||
use crate::database::memory::MemoryDatabase;
|
||||
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
|
||||
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils, SyncTime};
|
||||
use crate::descriptor::derived::AsDerived;
|
||||
use crate::descriptor::policy::BuildSatisfaction;
|
||||
use crate::descriptor::{
|
||||
@@ -601,7 +601,7 @@ where
|
||||
let recipients = params.recipients.iter().map(|(r, v)| (r, *v));
|
||||
|
||||
for (index, (script_pubkey, value)) in recipients.enumerate() {
|
||||
if value.is_dust() {
|
||||
if value.is_dust() && !script_pubkey.is_provably_unspendable() {
|
||||
return Err(Error::OutputBelowDustLimit(index));
|
||||
}
|
||||
|
||||
@@ -1447,6 +1447,11 @@ where
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return an immutable reference to the internal database
|
||||
pub fn database(&self) -> impl std::ops::Deref<Target = D> + '_ {
|
||||
self.database.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, D> Wallet<B, D>
|
||||
@@ -1549,6 +1554,15 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
let sync_time = SyncTime {
|
||||
block_time: BlockTime {
|
||||
height: maybe_await!(self.client.get_height())?,
|
||||
timestamp: time::get_timestamp(),
|
||||
},
|
||||
};
|
||||
debug!("Saving `sync_time` = {:?}", sync_time);
|
||||
self.database.borrow_mut().set_sync_time(sync_time)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1559,8 +1573,8 @@ where
|
||||
|
||||
/// Broadcast a transaction to the network
|
||||
#[maybe_async]
|
||||
pub fn broadcast(&self, tx: Transaction) -> Result<Txid, Error> {
|
||||
maybe_await!(self.client.broadcast(&tx))?;
|
||||
pub fn broadcast(&self, tx: &Transaction) -> Result<Txid, Error> {
|
||||
maybe_await!(self.client.broadcast(tx))?;
|
||||
|
||||
Ok(tx.txid())
|
||||
}
|
||||
@@ -2778,7 +2792,7 @@ pub(crate) mod test {
|
||||
let txid = tx.txid();
|
||||
// skip saving the utxos, we know they can't be used anyways
|
||||
details.transaction = Some(tx);
|
||||
details.confirmation_time = Some(ConfirmationTime {
|
||||
details.confirmation_time = Some(BlockTime {
|
||||
timestamp: 12345678,
|
||||
height: 42,
|
||||
});
|
||||
@@ -3980,4 +3994,15 @@ pub(crate) mod test {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sending_to_bip350_bech32m_address() {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr =
|
||||
Address::from_str("tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c")
|
||||
.unwrap();
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(addr.script_pubkey(), 45_000);
|
||||
builder.finish().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,6 +560,13 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, B, D,
|
||||
self
|
||||
}
|
||||
|
||||
/// Add data as an output, using OP_RETURN
|
||||
pub fn add_data(&mut self, data: &[u8]) -> &mut Self {
|
||||
let script = Script::new_op_return(data);
|
||||
self.add_recipient(script, 0u64);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the address to *drain* excess coins to.
|
||||
///
|
||||
/// Usually, when there are excess coins they are sent to a change address generated by the
|
||||
|
||||
Reference in New Issue
Block a user