Compare commits

..

37 Commits

Author SHA1 Message Date
Steve Myers
80ed21e4c9 Bump version to 0.7.0 2022-06-14 10:46:44 -07:00
Steve Myers
f2efcb6196 Update CHANGELOG for v0.7.0 2022-06-14 10:45:35 -07:00
Steve Myers
cc8a17ef86 Merge bitcoindevkit/bdk-ffi#148: Add Wallet and TxBuilder tests using test funded wallet
e469dcd32c Add test with funded wallet (Steve Myers)

Pull request description:

  This PR adds an example test using the `bdk` test funded wallet.  The example test makes sure the `bdk-ffi` `TxBuilder` is able to drain a single wallet UTXO to a single address.  More tests can be added as we need them in future PRs.

  Required to complete #141

Top commit has no ACKs.

Tree-SHA512: 780e8cf5b3d3091f3322113f017c5b5524b30a3ac9e18910539c51042740d2809535a947b8d56012076ac5e9ad1abcf707ceaf17651457ea327a0b522fcc1002
2022-06-14 10:27:43 -07:00
Steve Myers
5ffe9ff331 Merge bitcoindevkit/bdk-ffi#161: Match bdk API and return a boolean when signing a PSBT
9a3d609826 Match bdk API and return a boolean when signing a PSBT (thunderbiscuit)

Pull request description:

  This is a fix for #160.

  I was looking at the `get_transactions()` method just below and I'm not sure which syntax is best (let me know if you have opinions on this) between

  What I have:
  ```rust
  fn sign(&self, psbt: &PartiallySignedBitcoinTransaction) -> Result<bool, Error> {
      let mut psbt = psbt.internal.lock().unwrap();
      self.get_wallet().sign(&mut psbt, SignOptions::default())
  }
  ```

  If I mirrored `get_transactions()`:
  ```rust
  fn sign(&self, psbt: &PartiallySignedBitcoinTransaction) -> Result<bool, Error> {
      let mut psbt = psbt.internal.lock().unwrap();
      let finalized = self.get_wallet().sign(&mut psbt, SignOptions::default())?;
      Ok(finalized)
  }
  ```

ACKs for top commit:
  notmandatory:
    reACK 9a3d609826

Tree-SHA512: c220929ea9bf7f670c850aebef1c2ebefcbf354f3887e692be36dced30e0e180816426bd58c5a58f61a9759e2f9f451b56e9448f42c23e26f96cf857fd6aa37c
2022-06-13 12:09:03 -07:00
thunderbiscuit
9a3d609826 Match bdk API and return a boolean when signing a PSBT 2022-06-13 15:58:44 -03:00
Steve Myers
bf8fef807d Merge bitcoindevkit/bdk-ffi#162: Update BDK to version 0.19.0
0a3347b85a Update BDK to version 0.19.0 (Steve Myers)

Pull request description:

  Update BDK to version 0.19.0
    - fixes #155, sqlite-db issue causing wrong balance
    - adds experimental taproot descriptor and PSBT support

ACKs for top commit:
  thunderbiscuit:
    Tested ACK [0a3347b](0a3347b85a).

Tree-SHA512: c94ee13f948996350f1079a5fb9be73225e40a930b982dcd7d6826bc19ae3d5ed76d4d3b78ebebf1ce78092570a26a53c93bcc6edf3eeca731142921cf297f83
2022-06-13 11:07:21 -07:00
Steve Myers
e469dcd32c Add test with funded wallet 2022-06-10 15:59:33 -07:00
Steve Myers
0a3347b85a Update BDK to version 0.19.0 2022-06-10 11:25:37 -07:00
Steve Myers
f40ab551b6 Merge bitcoindevkit/bdk-ffi#147: Clean up UDL file
efc475e33f Clean up UDL file (thunderbiscuit)

Pull request description:

  This PR groups related constructs together, fixes indentation inconsistencies (2 spaces is the standard), and adds space between methods in interfaces.

ACKs for top commit:
  notmandatory:
    ACK efc475e33f

Tree-SHA512: 8b37afd1d05f23cb51e04664459b88f3cf415f8616ee0a080294bc27c71c16ed8049ea605b4b41091e4c8276b107da21aff0c1712f2ebfb0dd059f68a4240745
2022-05-17 10:08:55 -07:00
thunderbiscuit
efc475e33f Clean up UDL file 2022-05-17 08:16:17 -04:00
thunderbiscuit
cdea6dc0bf Merge pull request #137 from thunderbiscuit/feat/address-index
Add address index to return type of get_address
2022-05-17 07:31:03 -04:00
thunderbiscuit
6beb98ca4c Incorporate PR review fixes 2022-05-16 15:06:01 -04:00
thunderbiscuit
04d538ad45 Add get_address method on the wallet 2022-05-16 14:50:12 -04:00
thunderbiscuit
c074a92e0c Add address index to return type of get_last_unused_address 2022-05-16 14:45:21 -04:00
Steve Myers
ff260edb3c Merge bitcoindevkit/bdk-ffi#153: Simplify using struct update syntax and clone
e5cd7cb3a2 Simplify using struct update syntax and clone (Sudarsan Balaji)

Pull request description:

ACKs for top commit:
  notmandatory:
    ACK e5cd7cb3a2

Tree-SHA512: 643013f6b0e131a56d9819598c99e7ac4ed008e121bd22d0e3d7dcff61bcb0826c8f1295bf2317580865e2b014f12b56bf4b538f0461a120ad3222b35b7cb933
2022-05-13 10:14:13 -07:00
Steve Myers
15a0795626 Merge bitcoindevkit/bdk-ffi#152: Release version 0.6.0
30e54ac067 Bump version to 0.6.0 (Steve Myers)

Pull request description:

ACKs for top commit:
  thunderbiscuit:
    ACK 30e54ac067.

Tree-SHA512: 235e1f894ba5bfac2fa60330d6e38c6179aaa27b4c6cb8cede17974207c9e04674d4cbbbaaa1705578e0552f09cc78db3ee1015e14cad91281a9d06764495cdd
2022-05-12 19:32:12 -07:00
Sudarsan Balaji
e5cd7cb3a2 Simplify using struct update syntax and clone 2022-05-12 22:50:03 +01:00
Steve Myers
30e54ac067 Bump version to 0.6.0 2022-05-10 22:41:48 -07:00
Steve Myers
71583eca7f Merge bitcoindevkit/bdk-ffi#150: Add new BumpFeeTxBuilder interface
0787d9c446 Fix order of BumpFeeTxBuilder parameters (Steve Myers)
390d12703e Change TxBuilder and BumpFeeTxBuilder build() to finish() (Steve Myers)
9f903932dc Add BumpFeeTxBuilder (Steve Myers)

Pull request description:

  Add BumpFeeTxBuilder to bump the fee on an unconfirmed tx created by the Wallet. The structure of the new interface is:

  ```udl
  interface BumpFeeTxBuilder {
    constructor(string txid, float new_fee_rate);
    BumpFeeTxBuilder allow_shrinking(string address);
    BumpFeeTxBuilder enable_rbf();
    BumpFeeTxBuilder enable_rbf_with_sequence(u32 nsequence);
    [Throws=BdkError]
    PartiallySignedBitcoinTransaction build([ByRef] Wallet wallet);
  };
  ```
  Fixes #150

Top commit has no ACKs.

Tree-SHA512: a24ed41f7b897a0e091ef55d4c1347b3973cbe628b7d1bd69c95e663bc6f049de8f7e3b019115d763cff3fa6d5cb4d31ed474022c3087e8404a2af37f380d56d
2022-05-10 22:35:32 -07:00
Steve Myers
0787d9c446 Fix order of BumpFeeTxBuilder parameters 2022-05-06 09:59:07 -07:00
Steve Myers
390d12703e Change TxBuilder and BumpFeeTxBuilder build() to finish() 2022-05-06 09:52:20 -07:00
Steve Myers
9f903932dc Add BumpFeeTxBuilder 2022-05-05 20:22:55 -07:00
Steve Myers
3b243efefd Merge bitcoindevkit/bdk-ffi#149: Update to bdk 0.18.0
f38f4c6197 Update CHANGELOG (Steve Myers)
11ba16ec1b Move txid getter to PartiallySignedBitcoinTransaction (Steve Myers)
4665c551dd Update EsploraConfig, Blockchain broadcast, Wallet sync (Steve Myers)
907540d214 Update bdk to 0.18.0 with sqlite-bundled feature (Steve Myers)

Pull request description:

  Changes that were needed to match updated bdk APIs:
  * new bdk-ffi `Blockchain`  interface with `constructo(BlockchainConfig)` and `broadcast()` functions
  * `Blockchain.broadcast()` function is now void (doesn't return txid)
  * added `PartiallySignedBitcoinTransaction.txid()` getter function
  * `Wallet.sync()` function now takes a `Blockchain` and optional `Progress` callback interface

  Tests performed:
  * from `bdk-kotlin/bdk-ffi` added local remote to this branch, ran build.sh script
  * fixed broken `bdk-kotlin` jvm and android tests and confirmed tests all pass

Top commit has no ACKs.

Tree-SHA512: d50633bbc8fd8a0d141597b30122c72957d2a0d64fc1537b649eeb8a5df1b1fb9a78ee1f03b9c606f47dee3952c9a91ae09eb47eb7a66d5f0fcb5545c86d906b
2022-05-05 09:29:37 -07:00
Steve Myers
f38f4c6197 Update CHANGELOG 2022-05-02 16:17:27 -07:00
Steve Myers
11ba16ec1b Move txid getter to PartiallySignedBitcoinTransaction 2022-04-25 21:31:40 -07:00
Steve Myers
4665c551dd Update EsploraConfig, Blockchain broadcast, Wallet sync 2022-04-25 21:31:38 -07:00
Steve Myers
907540d214 Update bdk to 0.18.0 with sqlite-bundled feature 2022-04-25 21:31:36 -07:00
Steve Myers
e6a6be5b60 Merge bitcoindevkit/bdk-ffi#145: Fix fee parameter typo in TransactionDetails
c722223b49 Fix fee parameter typo in TransactionDetails (dhruvbaliyan)

Pull request description:

  Solves issue #136
  Generated Kotlin file now have "fee" in TransactionDetails as parameter instead of "fees"
  ```
  data class TransactionDetails (
      var fee: ULong?,
      var received: ULong,
      var sent: ULong,
      var txid: String
  ) {
      // ...
  }
  ```

ACKs for top commit:
  thunderbiscuit:
    Tested ACK c722223. Works as expected in my apps. Thanks for the quick fix!

Tree-SHA512: c55a6e77ca5a0cd19758fc628fc48ed997b3c86247a1eadf5be77771818e3aa5f4db10025e7aa30d05be573e94d7439b15c7fc1f3d6dad752487f7f1ad455367
2022-04-25 21:30:17 -07:00
dhruvbaliyan
c722223b49 Fix fee parameter typo in TransactionDetails 2022-04-20 06:13:49 +05:30
Steve Myers
236360e8c4 Merge bitcoindevkit/bdk-ffi#140: Add RBF methods to TxBuilder
220835cffd Add RBF to TxBuilder (Sudarsan Balaji)
b3c93b0435 Expose functions (Sudarsan Balaji)

Pull request description:

  Fix #133

  We need to create another `RbfValue` enum because the actual type is only visible within the crate in `bdk`.

ACKs for top commit:
  notmandatory:
    tACK 220835cffd

Tree-SHA512: 648ea26a9742c8a395876f38c7299ff0dabb4ccad64e2f6a47d29ceecf44d9e54d845410fa68665e4d4d03d8eda1e51f680d0b89df307b003de49cf7b98e8701
2022-04-16 22:36:34 -07:00
Sudarsan Balaji
220835cffd Add RBF to TxBuilder 2022-04-15 21:04:21 +01:00
Sudarsan Balaji
b3c93b0435 Expose functions 2022-04-15 21:04:04 +01:00
Sudarsan Balaji
a12e5ed396 Use Path instead of PathBuf 2022-04-04 11:07:51 +01:00
Sudarsan Balaji
fc00d0d38c Use unwrap_or_else panic instead of expect 2022-04-04 11:01:50 +01:00
Sudarsan Balaji
7ea5e75bc4 Use write_all when not writing partially 2022-04-04 10:59:19 +01:00
Sudarsan Balaji
a5bd16db4d Enforce rust naming conventions 2022-04-04 10:58:01 +01:00
Steve Myers
d72905168b Merge bitcoindevkit/bdk-ffi#128: Release/0.5
8a556d0ba0 Bump bdk-ffi version to 0.5.0 (Steve Myers)
d7c5f24fe8 Bump bdk-ffi-bindgen version to 0.2.0 (Steve Myers)
f1431c3073 Update CHANGELOG.md (Steve Myers)

Pull request description:

Top commit has no ACKs.

Tree-SHA512: 2d612936740b93148c90acf512005c82e1fe38b4708710952abeac03d361e7dca6c6bdea4e82981a87dbba1cb6d37c0bd48b4ab252467798aa60aca463af5e5e
2022-04-01 19:24:48 -07:00
6 changed files with 453 additions and 176 deletions

1
.gitignore vendored
View File

@@ -14,3 +14,4 @@ testdb
xcuserdata
.lsp
.clj-kondo
.idea/

View File

@@ -6,6 +6,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [v0.7.0]
- Update BDK to version 0.19.0
- fixes sqlite-db issue causing wrong balance
- adds experimental taproot descriptor and PSBT support
- APIs Removed
- `Wallet.get_new_address()`, returned String, [#137]
- `Wallet.get_last_unused_address()`, returned String [#137]
- APIs Added
- `Wallet.get_address(AddressIndex)`, returns `AddressInfo` [#137]
- APIs Changed
- `Wallet.sign(PartiallySignedBitcoinTransaction)` now returns a bool, true if finalized [#161]
[#137]: https://github.com/bitcoindevkit/bdk-ffi/pull/137
[#161]: https://github.com/bitcoindevkit/bdk-ffi/pull/161
## [v0.6.0]
- Update BDK to version 0.18.0
- Add BumpFeeTxBuilder to bump the fee on an unconfirmed tx created by the Wallet
- Change TxBuilder.build() to TxBuilder.finish() to align with bdk function name
## [v0.5.0]
- Fix Wallet.broadcast function, now returns a tx id as a hex string
@@ -37,7 +59,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [v0.2.0]
[unreleased]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.5.0...HEAD
[unreleased]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.7.0...HEAD
[v0.7.0]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.6.0...v0.7.0
[v0.6.0]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.5.0...v0.6.0
[v0.5.0]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.4.0...v0.5.0
[v0.4.0]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.3.1...v0.4.0
[v0.3.1]: https://github.com/bitcoindevkit/bdk-ffi/compare/v0.3.0...v0.3.1

View File

@@ -1,6 +1,6 @@
[package]
name = "bdk-ffi"
version = "0.5.0"
version = "0.7.0"
authors = ["Steve Myers <steve@notmandatory.org>", "Sudarsan Balaji <sudarsan.balaji@artfuldev.com>"]
edition = "2018"
@@ -14,10 +14,7 @@ crate-type = ["staticlib", "cdylib"]
name = "bdkffi"
[dependencies]
bdk = { version = "0.14", features = ["all-keys", "use-esplora-ureq", "sqlite"] }
# TODO remove when bdk "sqlite-bundled" feature added
rusqlite = { version = "0.25.3", features = ["bundled"] }
bdk = { version = "0.19", features = ["all-keys", "use-esplora-ureq", "sqlite-bundled"] }
uniffi_macros = { version = "0.16.0", features = ["builtin-bindgen"] }
uniffi = { version = "0.16.0", features = ["builtin-bindgen"] }

View File

@@ -1,22 +1,21 @@
use std::fmt;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use structopt::StructOpt;
use uniffi_bindgen;
#[derive(Debug, PartialEq)]
pub enum Language {
KOTLIN,
PYTHON,
SWIFT,
Kotlin,
Python,
Swift,
}
impl fmt::Display for Language {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Language::KOTLIN => write!(f, "kotlin"),
Language::SWIFT => write!(f, "swift"),
Language::PYTHON => write!(f, "python"),
Language::Kotlin => write!(f, "kotlin"),
Language::Swift => write!(f, "swift"),
Language::Python => write!(f, "python"),
}
}
}
@@ -36,9 +35,9 @@ impl FromStr for Language {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"kotlin" => Ok(Language::KOTLIN),
"python" => Ok(Language::PYTHON),
"swift" => Ok(Language::SWIFT),
"kotlin" => Ok(Language::Kotlin),
"python" => Ok(Language::Python),
"swift" => Ok(Language::Swift),
_ => Err(Error::UnsupportedLanguage),
}
}
@@ -57,8 +56,8 @@ fn generate_bindings(opt: &Opt) -> anyhow::Result<(), anyhow::Error> {
}
fn fixup_python_lib_path(
out_dir: &PathBuf,
lib_name: &PathBuf,
out_dir: &Path,
lib_name: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
use std::fs;
use std::io::Write;
@@ -68,10 +67,9 @@ fn fixup_python_lib_path(
let bindings_file = out_dir.join("bdk.py");
let mut data = fs::read_to_string(&bindings_file)?;
let pos = data.find(LOAD_INDIRECT_DEF).expect(&format!(
"loadIndirect not found in `{}`",
bindings_file.display()
));
let pos = data
.find(LOAD_INDIRECT_DEF)
.unwrap_or_else(|| panic!("loadIndirect not found in `{}`", bindings_file.display()));
let range = pos..pos + LOAD_INDIRECT_DEF.len();
let replacement = format!(
@@ -89,7 +87,7 @@ def _loadIndirectOld():"#,
.write(true)
.truncate(true)
.open(&bindings_file)?;
file.write(data.as_bytes())?;
file.write_all(data.as_bytes())?;
Ok(())
}
@@ -126,7 +124,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
generate_bindings(&opt)?;
if opt.language == Language::PYTHON {
if opt.language == Language::Python {
if let Some(path) = opt.python_fixup_path {
println!("Fixing up python lib path, {:?}", &path);
fixup_python_lib_path(&opt.out_dir, &path)?;

View File

@@ -1,6 +1,7 @@
namespace bdk {
[Throws=BdkError]
ExtendedKeyInfo generate_extended_key(Network network, WordCount word_count, string? password);
[Throws=BdkError]
ExtendedKeyInfo restore_extended_key(Network network, string mnemonic, string? password);
};
@@ -49,6 +50,16 @@ enum BdkError {
"Rusqlite",
};
dictionary AddressInfo {
u32 index;
string address;
};
enum AddressIndex {
"New",
"LastUnused",
};
enum Network {
"Bitcoin",
"Testnet",
@@ -73,15 +84,15 @@ interface DatabaseConfig {
};
dictionary TransactionDetails {
u64? fees;
u64 received;
u64 sent;
string txid;
u64? fee;
u64 received;
u64 sent;
string txid;
};
dictionary BlockTime {
u32 height;
u64 timestamp;
u32 height;
u64 timestamp;
};
[Enum]
@@ -90,66 +101,6 @@ interface Transaction {
Confirmed(TransactionDetails details, BlockTime confirmation);
};
dictionary ElectrumConfig {
string url;
string? socks5;
u8 retry;
u8? timeout;
u64 stop_gap;
};
dictionary EsploraConfig {
string base_url;
string? proxy;
u64 timeout_read;
u64 timeout_write;
u64 stop_gap;
};
[Enum]
interface BlockchainConfig {
Electrum(ElectrumConfig config);
Esplora(EsploraConfig config);
};
callback interface BdkProgress {
void update(f32 progress, string? message);
};
interface Wallet {
[Throws=BdkError]
constructor(string descriptor, string? change_descriptor, Network network, DatabaseConfig database_config, BlockchainConfig blockchain_config);
string get_new_address();
string get_last_unused_address();
[Throws=BdkError]
u64 get_balance();
[Throws=BdkError]
void sign([ByRef] PartiallySignedBitcoinTransaction psbt);
[Throws=BdkError]
sequence<Transaction> get_transactions();
Network get_network();
[Throws=BdkError]
void sync(BdkProgress progress_update, u32? max_address_param);
[Throws=BdkError]
string broadcast([ByRef] PartiallySignedBitcoinTransaction psbt);
};
interface PartiallySignedBitcoinTransaction {
[Throws=BdkError]
constructor(string psbt_base64);
string serialize();
};
interface TxBuilder {
constructor();
TxBuilder add_recipient(string address, u64 amount);
TxBuilder fee_rate(float sat_per_vbyte);
TxBuilder drain_wallet();
TxBuilder drain_to(string address);
[Throws=BdkError]
PartiallySignedBitcoinTransaction build([ByRef] Wallet wallet);
};
dictionary ExtendedKeyInfo {
string mnemonic;
string xprv;
@@ -163,3 +114,100 @@ enum WordCount {
"Words21",
"Words24",
};
dictionary ElectrumConfig {
string url;
string? socks5;
u8 retry;
u8? timeout;
u64 stop_gap;
};
dictionary EsploraConfig {
string base_url;
string? proxy;
u8? concurrency;
u64 stop_gap;
u64? timeout;
};
[Enum]
interface BlockchainConfig {
Electrum(ElectrumConfig config);
Esplora(EsploraConfig config);
};
interface Blockchain {
[Throws=BdkError]
constructor(BlockchainConfig config);
[Throws=BdkError]
void broadcast([ByRef] PartiallySignedBitcoinTransaction psbt);
};
callback interface Progress {
void update(f32 progress, string? message);
};
interface Wallet {
[Throws=BdkError]
constructor(string descriptor, string? change_descriptor, Network network, DatabaseConfig database_config);
[Throws=BdkError]
AddressInfo get_address(AddressIndex address_index);
[Throws=BdkError]
u64 get_balance();
[Throws=BdkError]
boolean sign([ByRef] PartiallySignedBitcoinTransaction psbt);
[Throws=BdkError]
sequence<Transaction> get_transactions();
Network get_network();
[Throws=BdkError]
void sync([ByRef] Blockchain blockchain, Progress? progress);
};
interface PartiallySignedBitcoinTransaction {
[Throws=BdkError]
constructor(string psbt_base64);
string serialize();
string txid();
};
interface TxBuilder {
constructor();
TxBuilder add_recipient(string address, u64 amount);
TxBuilder fee_rate(float sat_per_vbyte);
TxBuilder drain_wallet();
TxBuilder drain_to(string address);
TxBuilder enable_rbf();
TxBuilder enable_rbf_with_sequence(u32 nsequence);
[Throws=BdkError]
PartiallySignedBitcoinTransaction finish([ByRef] Wallet wallet);
};
interface BumpFeeTxBuilder {
constructor(string txid, float new_fee_rate);
BumpFeeTxBuilder allow_shrinking(string address);
BumpFeeTxBuilder enable_rbf();
BumpFeeTxBuilder enable_rbf_with_sequence(u32 nsequence);
[Throws=BdkError]
PartiallySignedBitcoinTransaction finish([ByRef] Wallet wallet);
};

View File

@@ -1,20 +1,25 @@
use bdk::bitcoin::hashes::hex::ToHex;
use bdk::bitcoin::secp256k1::Secp256k1;
use bdk::bitcoin::util::psbt::PartiallySignedTransaction;
use bdk::bitcoin::{Address, Network, Script};
use bdk::bitcoin::{Address, Network, Script, Txid};
use bdk::blockchain::any::{AnyBlockchain, AnyBlockchainConfig};
use bdk::blockchain::Progress;
use bdk::blockchain::{
electrum::ElectrumBlockchainConfig, esplora::EsploraBlockchainConfig, ConfigurableBlockchain,
};
use bdk::blockchain::{Blockchain as BdkBlockchain, Progress as BdkProgress};
use bdk::database::any::{AnyDatabase, SledDbConfiguration, SqliteDbConfiguration};
use bdk::database::{AnyDatabaseConfig, ConfigurableDatabase};
use bdk::keys::bip39::{Language, Mnemonic, WordCount};
use bdk::keys::{DerivableKey, ExtendedKey, GeneratableKey, GeneratedKey};
use bdk::miniscript::BareCtx;
use bdk::wallet::AddressIndex;
use bdk::{BlockTime, Error, FeeRate, SignOptions, Wallet as BdkWallet};
use std::convert::TryFrom;
use bdk::wallet::AddressIndex as BdkAddressIndex;
use bdk::wallet::AddressInfo as BdkAddressInfo;
use bdk::{
BlockTime, Error, FeeRate, SignOptions, SyncOptions as BdkSyncOptions, Wallet as BdkWallet,
};
use std::convert::{From, TryFrom};
use std::fmt;
use std::ops::Deref;
use std::str::FromStr;
use std::sync::{Arc, Mutex, MutexGuard};
@@ -22,6 +27,34 @@ uniffi_macros::include_scaffolding!("bdk");
type BdkError = Error;
pub struct AddressInfo {
pub index: u32,
pub address: String,
}
impl From<BdkAddressInfo> for AddressInfo {
fn from(x: bdk::wallet::AddressInfo) -> AddressInfo {
AddressInfo {
index: x.index,
address: x.address.to_string(),
}
}
}
pub enum AddressIndex {
New,
LastUnused,
}
impl From<AddressIndex> for BdkAddressIndex {
fn from(x: AddressIndex) -> BdkAddressIndex {
match x {
AddressIndex::New => BdkAddressIndex::New,
AddressIndex::LastUnused => BdkAddressIndex::LastUnused,
}
}
}
pub enum DatabaseConfig {
Memory,
Sled { config: SledDbConfiguration },
@@ -39,9 +72,9 @@ pub struct ElectrumConfig {
pub struct EsploraConfig {
pub base_url: String,
pub proxy: Option<String>,
pub timeout_read: u64,
pub timeout_write: u64,
pub concurrency: Option<u8>,
pub stop_gap: u64,
pub timeout: Option<u64>,
}
pub enum BlockchainConfig {
@@ -51,7 +84,7 @@ pub enum BlockchainConfig {
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TransactionDetails {
pub fees: Option<u64>,
pub fee: Option<u64>,
pub received: u64,
pub sent: u64,
pub txid: String,
@@ -71,7 +104,7 @@ pub enum Transaction {
impl From<&bdk::TransactionDetails> for TransactionDetails {
fn from(x: &bdk::TransactionDetails) -> TransactionDetails {
TransactionDetails {
fees: x.fee,
fee: x.fee,
txid: x.txid.to_string(),
received: x.received,
sent: x.sent,
@@ -93,25 +126,73 @@ impl From<&bdk::TransactionDetails> for Transaction {
}
}
struct Wallet {
wallet_mutex: Mutex<BdkWallet<AnyBlockchain, AnyDatabase>>,
struct Blockchain {
blockchain_mutex: Mutex<AnyBlockchain>,
}
pub trait BdkProgress: Send + Sync {
impl Blockchain {
fn new(blockchain_config: BlockchainConfig) -> Result<Self, BdkError> {
let any_blockchain_config = match blockchain_config {
BlockchainConfig::Electrum { config } => {
AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig {
retry: config.retry,
socks5: config.socks5,
timeout: config.timeout,
url: config.url,
stop_gap: usize::try_from(config.stop_gap).unwrap(),
})
}
BlockchainConfig::Esplora { config } => {
AnyBlockchainConfig::Esplora(EsploraBlockchainConfig {
base_url: config.base_url,
proxy: config.proxy,
concurrency: config.concurrency,
stop_gap: usize::try_from(config.stop_gap).unwrap(),
timeout: config.timeout,
})
}
};
let blockchain = AnyBlockchain::from_config(&any_blockchain_config)?;
Ok(Self {
blockchain_mutex: Mutex::new(blockchain),
})
}
fn get_blockchain(&self) -> MutexGuard<AnyBlockchain> {
self.blockchain_mutex.lock().expect("blockchain")
}
fn broadcast(&self, psbt: &PartiallySignedBitcoinTransaction) -> Result<(), Error> {
let tx = psbt.internal.lock().unwrap().clone().extract_tx();
self.get_blockchain().broadcast(&tx)
}
}
struct Wallet {
wallet_mutex: Mutex<BdkWallet<AnyDatabase>>,
}
pub trait Progress: Send + Sync + 'static {
fn update(&self, progress: f32, message: Option<String>);
}
struct BdkProgressHolder {
progress_update: Box<dyn BdkProgress>,
struct ProgressHolder {
progress: Box<dyn Progress>,
}
impl Progress for BdkProgressHolder {
impl BdkProgress for ProgressHolder {
fn update(&self, progress: f32, message: Option<String>) -> Result<(), Error> {
self.progress_update.update(progress, message);
self.progress.update(progress, message);
Ok(())
}
}
impl fmt::Debug for ProgressHolder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ProgressHolder").finish_non_exhaustive()
}
}
#[derive(Debug)]
struct PartiallySignedBitcoinTransaction {
internal: Mutex<PartiallySignedTransaction>,
@@ -129,6 +210,12 @@ impl PartiallySignedBitcoinTransaction {
let psbt = self.internal.lock().unwrap().clone();
psbt.to_string()
}
fn txid(&self) -> String {
let tx = self.internal.lock().unwrap().clone().extract_tx();
let txid = tx.txid();
txid.to_hex()
}
}
impl Wallet {
@@ -137,46 +224,23 @@ impl Wallet {
change_descriptor: Option<String>,
network: Network,
database_config: DatabaseConfig,
blockchain_config: BlockchainConfig,
) -> Result<Self, BdkError> {
let any_database_config = match database_config {
DatabaseConfig::Memory => AnyDatabaseConfig::Memory(()),
DatabaseConfig::Sled { config } => AnyDatabaseConfig::Sled(config),
DatabaseConfig::Sqlite { config } => AnyDatabaseConfig::Sqlite(config),
};
let any_blockchain_config = match blockchain_config {
BlockchainConfig::Electrum { config } => {
AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig {
retry: config.retry,
socks5: config.socks5,
timeout: config.timeout,
url: config.url,
stop_gap: usize::try_from(config.stop_gap).unwrap(),
})
}
BlockchainConfig::Esplora { config } => {
AnyBlockchainConfig::Esplora(EsploraBlockchainConfig {
base_url: config.base_url,
proxy: config.proxy,
timeout_read: config.timeout_read,
timeout_write: config.timeout_write,
stop_gap: usize::try_from(config.stop_gap).unwrap(),
})
}
};
let database = AnyDatabase::from_config(&any_database_config)?;
let blockchain = AnyBlockchain::from_config(&any_blockchain_config)?;
let wallet_mutex = Mutex::new(BdkWallet::new(
&descriptor,
change_descriptor.as_ref(),
network,
database,
blockchain,
)?);
Ok(Wallet { wallet_mutex })
}
fn get_wallet(&self) -> MutexGuard<BdkWallet<AnyBlockchain, AnyDatabase>> {
fn get_wallet(&self) -> MutexGuard<BdkWallet<AnyDatabase>> {
self.wallet_mutex.lock().expect("wallet")
}
@@ -186,55 +250,39 @@ impl Wallet {
fn sync(
&self,
progress_update: Box<dyn BdkProgress>,
max_address_param: Option<u32>,
blockchain: &Blockchain,
progress: Option<Box<dyn Progress>>,
) -> Result<(), BdkError> {
self.get_wallet()
.sync(BdkProgressHolder { progress_update }, max_address_param)
let bdk_sync_opts = BdkSyncOptions {
progress: progress.map(|p| {
Box::new(ProgressHolder { progress: p })
as Box<(dyn bdk::blockchain::Progress + 'static)>
}),
};
let blockchain = blockchain.get_blockchain();
self.get_wallet().sync(blockchain.deref(), bdk_sync_opts)
}
fn get_new_address(&self) -> String {
fn get_address(&self, address_index: AddressIndex) -> Result<AddressInfo, BdkError> {
self.get_wallet()
.get_address(AddressIndex::New)
.unwrap()
.address
.to_string()
}
fn get_last_unused_address(&self) -> String {
self.get_wallet()
.get_address(AddressIndex::LastUnused)
.unwrap()
.address
.to_string()
.get_address(address_index.into())
.map(AddressInfo::from)
}
fn get_balance(&self) -> Result<u64, Error> {
self.get_wallet().get_balance()
}
fn sign(&self, psbt: &PartiallySignedBitcoinTransaction) -> Result<(), Error> {
fn sign(&self, psbt: &PartiallySignedBitcoinTransaction) -> Result<bool, Error> {
let mut psbt = psbt.internal.lock().unwrap();
let finalized = self.get_wallet().sign(&mut psbt, SignOptions::default())?;
match finalized {
true => Ok(()),
false => Err(BdkError::Generic(format!(
"transaction signing not finalized {:?}",
psbt
))),
}
self.get_wallet().sign(&mut psbt, SignOptions::default())
}
fn get_transactions(&self) -> Result<Vec<Transaction>, Error> {
let transactions = self.get_wallet().list_transactions(true)?;
Ok(transactions.iter().map(Transaction::from).collect())
}
fn broadcast(&self, psbt: &PartiallySignedBitcoinTransaction) -> Result<String, Error> {
let tx = psbt.internal.lock().unwrap().clone().extract_tx();
let txid = self.get_wallet().broadcast(&tx)?;
Ok(txid.to_hex())
}
}
pub struct ExtendedKeyInfo {
@@ -283,11 +331,19 @@ fn to_script_pubkey(address: &str) -> Result<Script, BdkError> {
.map_err(|e| BdkError::Generic(e.to_string()))
}
#[derive(Clone, Debug)]
enum RbfValue {
Default,
Value(u32),
}
#[derive(Clone, Debug)]
struct TxBuilder {
recipients: Vec<(String, u64)>,
fee_rate: Option<f32>,
drain_wallet: bool,
drain_to: Option<String>,
rbf: Option<RbfValue>,
}
impl TxBuilder {
@@ -297,6 +353,7 @@ impl TxBuilder {
fee_rate: None,
drain_wallet: false,
drain_to: None,
rbf: None,
}
}
@@ -305,40 +362,46 @@ impl TxBuilder {
recipients.append(&mut vec![(recipient, amount)]);
Arc::new(TxBuilder {
recipients,
fee_rate: self.fee_rate,
drain_wallet: self.drain_wallet,
drain_to: self.drain_to.clone(),
..self.clone()
})
}
fn fee_rate(&self, sat_per_vb: f32) -> Arc<Self> {
Arc::new(TxBuilder {
recipients: self.recipients.to_vec(),
fee_rate: Some(sat_per_vb),
drain_wallet: self.drain_wallet,
drain_to: self.drain_to.clone(),
..self.clone()
})
}
fn drain_wallet(&self) -> Arc<Self> {
Arc::new(TxBuilder {
recipients: self.recipients.to_vec(),
fee_rate: self.fee_rate,
drain_wallet: true,
drain_to: self.drain_to.clone(),
..self.clone()
})
}
fn drain_to(&self, address: String) -> Arc<Self> {
Arc::new(TxBuilder {
recipients: self.recipients.to_vec(),
fee_rate: self.fee_rate,
drain_wallet: self.drain_wallet,
drain_to: Some(address),
..self.clone()
})
}
fn build(&self, wallet: &Wallet) -> Result<Arc<PartiallySignedBitcoinTransaction>, Error> {
fn enable_rbf(&self) -> Arc<Self> {
Arc::new(TxBuilder {
rbf: Some(RbfValue::Default),
..self.clone()
})
}
fn enable_rbf_with_sequence(&self, nsequence: u32) -> Arc<Self> {
Arc::new(TxBuilder {
rbf: Some(RbfValue::Value(nsequence)),
..self.clone()
})
}
fn finish(&self, wallet: &Wallet) -> Result<Arc<PartiallySignedBitcoinTransaction>, Error> {
let wallet = wallet.get_wallet();
let mut tx_builder = wallet.build_tx();
for (address, amount) in &self.recipients {
@@ -353,6 +416,85 @@ impl TxBuilder {
if let Some(address) = &self.drain_to {
tx_builder.drain_to(to_script_pubkey(address)?);
}
if let Some(rbf) = &self.rbf {
match *rbf {
RbfValue::Default => {
tx_builder.enable_rbf();
}
RbfValue::Value(nsequence) => {
tx_builder.enable_rbf_with_sequence(nsequence);
}
}
}
tx_builder
.finish()
.map(|(psbt, _)| PartiallySignedBitcoinTransaction {
internal: Mutex::new(psbt),
})
.map(Arc::new)
}
}
#[derive(Clone)]
struct BumpFeeTxBuilder {
txid: String,
fee_rate: f32,
allow_shrinking: Option<String>,
rbf: Option<RbfValue>,
}
impl BumpFeeTxBuilder {
fn new(txid: String, fee_rate: f32) -> Self {
Self {
txid,
fee_rate,
allow_shrinking: None,
rbf: None,
}
}
fn allow_shrinking(&self, address: String) -> Arc<Self> {
Arc::new(Self {
allow_shrinking: Some(address),
..self.clone()
})
}
fn enable_rbf(&self) -> Arc<Self> {
Arc::new(Self {
rbf: Some(RbfValue::Default),
..self.clone()
})
}
fn enable_rbf_with_sequence(&self, nsequence: u32) -> Arc<Self> {
Arc::new(Self {
rbf: Some(RbfValue::Value(nsequence)),
..self.clone()
})
}
fn finish(&self, wallet: &Wallet) -> Result<Arc<PartiallySignedBitcoinTransaction>, Error> {
let wallet = wallet.get_wallet();
let txid = Txid::from_str(self.txid.as_str())?;
let mut tx_builder = wallet.build_fee_bump(txid)?;
tx_builder.fee_rate(FeeRate::from_sat_per_vb(self.fee_rate));
if let Some(allow_shrinking) = &self.allow_shrinking {
let address =
Address::from_str(allow_shrinking).map_err(|e| Error::Generic(e.to_string()))?;
let script = address.script_pubkey();
tx_builder.allow_shrinking(script)?;
}
if let Some(rbf) = &self.rbf {
match *rbf {
RbfValue::Default => {
tx_builder.enable_rbf();
}
RbfValue::Value(nsequence) => {
tx_builder.enable_rbf_with_sequence(nsequence);
}
}
}
tx_builder
.finish()
.map(|(psbt, _)| PartiallySignedBitcoinTransaction {
@@ -363,3 +505,70 @@ impl TxBuilder {
}
uniffi::deps::static_assertions::assert_impl_all!(Wallet: Sync, Send);
// The goal of these tests to to ensure `bdk-ffi` intermediate code correctly calls `bdk` APIs.
// These tests should not be used to verify `bdk` behavior that is already tested in the `bdk`
// crate.
#[cfg(test)]
mod test {
use crate::{TxBuilder, Wallet};
use bdk::bitcoin::Address;
use bdk::bitcoin::Network::Testnet;
use bdk::wallet::get_funded_wallet;
use std::str::FromStr;
use std::sync::Mutex;
#[test]
fn test_drain_wallet() {
let test_wpkh = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)";
let (funded_wallet, _, _) = get_funded_wallet(test_wpkh);
let test_wallet = Wallet {
wallet_mutex: Mutex::new(funded_wallet),
};
let drain_to_address = "tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt".to_string();
let tx_builder = TxBuilder::new()
.drain_wallet()
.drain_to(drain_to_address.clone());
//dbg!(&tx_builder);
assert_eq!(tx_builder.drain_wallet, true);
assert_eq!(tx_builder.drain_to, Some(drain_to_address));
let psbt = tx_builder.finish(&test_wallet).unwrap();
let psbt = psbt.internal.lock().unwrap().clone();
// confirm one input with 50,000 sats
assert_eq!(psbt.inputs.len(), 1);
let input_value = psbt
.inputs
.get(0)
.cloned()
.unwrap()
.non_witness_utxo
.unwrap()
.output
.get(0)
.unwrap()
.value;
assert_eq!(input_value, 50_000 as u64);
// confirm one output to correct address with all sats - fee
assert_eq!(psbt.outputs.len(), 1);
let output_address = Address::from_script(
&psbt
.unsigned_tx
.output
.get(0)
.cloned()
.unwrap()
.script_pubkey,
Testnet,
)
.unwrap();
assert_eq!(
output_address,
Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt").unwrap()
);
let output_value = psbt.unsigned_tx.output.get(0).cloned().unwrap().value;
assert_eq!(output_value, 49_890 as u64); // input - fee
}
}