Skip to content
Open
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
41 changes: 41 additions & 0 deletions bdk-ffi/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,21 @@ pub enum SighashParseError {
Invalid { error_message: String },
}

#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum GetPsbtInputError {
#[error("invalid sighash type: {error_message}")]
InvalidSighash { error_message: String },

#[error("reference to an unknown utxo: {outpoint}")]
UnknownUtxo { outpoint: String },

#[error("miniscript psbt error: {error_message}")]
MiniscriptPsbt { error_message: String },

#[error("create tx error: {error_message}")]
CreateTx { error_message: String },
}

#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum PsbtFinalizeError {
#[error("an input at index {index} is invalid: {reason}")]
Expand Down Expand Up @@ -1066,6 +1081,32 @@ impl From<BdkCreateTxError> for CreateTxError {
}
}

impl GetPsbtInputError {
pub(crate) fn from_bdk_create_tx_error(error: BdkCreateTxError, outpoint: OutPoint) -> Self {
match error {
BdkCreateTxError::UnknownUtxo => GetPsbtInputError::UnknownUtxo {
outpoint: format!("{}:{}", outpoint.txid, outpoint.vout),
},
BdkCreateTxError::MiniscriptPsbt(error) => GetPsbtInputError::MiniscriptPsbt {
error_message: error.to_string(),
},
other => GetPsbtInputError::CreateTx {
error_message: CreateTxError::from(other).to_string(),
},
}
}
}

impl From<SighashParseError> for GetPsbtInputError {
fn from(error: SighashParseError) -> Self {
match error {
SighashParseError::Invalid { error_message } => {
GetPsbtInputError::InvalidSighash { error_message }
}
}
}
}

impl From<PushBytesError> for CreateTxError {
fn from(_: PushBytesError) -> Self {
CreateTxError::PushBytesError
Expand Down
89 changes: 88 additions & 1 deletion bdk-ffi/src/tests/wallet.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
use crate::bitcoin::{Network, NetworkKind};
use crate::bitcoin::{Network, NetworkKind, Transaction};
use crate::descriptor::Descriptor;
use crate::error::GetPsbtInputError;
use crate::store::Persister;
use crate::types::{LocalOutput, UnconfirmedTx};
use crate::wallet::Wallet;

use bdk_wallet::bitcoin::absolute::LockTime;
use bdk_wallet::bitcoin::transaction::Version;
use bdk_wallet::bitcoin::{
Amount as BdkAmount, OutPoint as BdkOutPoint, ScriptBuf, Sequence,
Transaction as BdkTransaction, TxIn as BdkTxIn, TxOut as BdkTxOut, Txid as BdkTxid, Witness,
};
use bdk_wallet::KeychainKind;

use std::str::FromStr;
use std::sync::Arc;

const EXTERNAL_DESCRIPTOR: &str = "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)";
Expand Down Expand Up @@ -35,6 +44,40 @@ fn build_wallet() -> Wallet {
.unwrap()
}

fn fund_wallet(wallet: &Wallet) -> LocalOutput {
let address_info = wallet.reveal_next_address(KeychainKind::External);
let script_pubkey = ScriptBuf::from_bytes(address_info.address.script_pubkey().to_bytes());
let tx = BdkTransaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input: vec![BdkTxIn {
previous_output: BdkOutPoint {
txid: BdkTxid::from_str(
"0101010101010101010101010101010101010101010101010101010101010101",
)
.unwrap(),
vout: 0,
},
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::default(),
}],
output: vec![BdkTxOut {
value: BdkAmount::from_sat(50_000),
script_pubkey,
}],
};

wallet.apply_unconfirmed_txs(vec![UnconfirmedTx {
tx: Arc::new(Transaction::from(tx)),
last_seen: 1,
}]);

let utxos = wallet.list_unspent();
assert_eq!(utxos.len(), 1);
utxos.into_iter().next().unwrap()
}

#[test]
fn test_create_wallet() {
let wallet = build_wallet();
Expand Down Expand Up @@ -150,3 +193,47 @@ fn test_create_two_path_wallet() {
assert_eq!(wallet.derivation_index(KeychainKind::External), Some(0));
assert_eq!(wallet.derivation_index(KeychainKind::Internal), Some(0));
}

#[test]
fn test_get_psbt_input_for_local_utxo() {
let wallet = build_wallet();
let utxo = fund_wallet(&wallet);

let psbt_input = wallet.get_psbt_input(utxo, None, false).unwrap();

assert!(psbt_input.witness_utxo.is_some() || psbt_input.non_witness_utxo.is_some());
assert_eq!(psbt_input.sighash_type, None);
assert!(!psbt_input.bip32_derivation.is_empty());
}

#[test]
fn test_get_psbt_input_parses_sighash() {
let wallet = build_wallet();
let utxo = fund_wallet(&wallet);

let psbt_input = wallet
.get_psbt_input(
utxo,
Some("SIGHASH_SINGLE|SIGHASH_ANYONECANPAY".to_string()),
false,
)
.unwrap();

assert_eq!(
psbt_input.sighash_type.as_deref(),
Some("SIGHASH_SINGLE|SIGHASH_ANYONECANPAY")
);
}

#[test]
fn test_get_psbt_input_invalid_sighash_returns_error() {
let wallet = build_wallet();
let utxo = fund_wallet(&wallet);

let result = wallet.get_psbt_input(utxo, Some("not-a-sighash".to_string()), false);

assert!(matches!(
result,
Err(GetPsbtInputError::InvalidSighash { .. })
));
}
31 changes: 31 additions & 0 deletions bdk-ffi/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,24 @@ impl From<BdkChainPosition<BdkConfirmationBlockTime>> for ChainPosition {
}
}

impl From<ChainPosition> for BdkChainPosition<BdkConfirmationBlockTime> {
fn from(chain_position: ChainPosition) -> Self {
match chain_position {
ChainPosition::Confirmed {
confirmation_block_time,
transitively,
} => BdkChainPosition::Confirmed {
anchor: confirmation_block_time.into(),
transitively: transitively.map(|txid| txid.0),
},
ChainPosition::Unconfirmed { timestamp } => BdkChainPosition::Unconfirmed {
first_seen: timestamp,

@ItoroD ItoroD Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I see we leave out first_seen in our ChainPosition type definition but now we have to use same timestamp when passing back.

Maybe not related to this PR; Asking in general... Do we really not need first seen?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good question, cuz we had already exposed first_seen on TxGraphChangeSet, but ChainPosition was still using the older single timestamp field which maps to upstream last_seen. I opened a followup PR to expose both first_seen and last_seen there too: #1039

last_seen: timestamp,
},
}
}
}

/// Represents the confirmation block and time of a transaction.
#[derive(Debug, Clone, PartialEq, Eq, std::hash::Hash, uniffi::Record)]
pub struct ConfirmationBlockTime {
Expand Down Expand Up @@ -294,6 +312,19 @@ impl From<BdkLocalOutput> for LocalOutput {
}
}

impl From<LocalOutput> for BdkLocalOutput {
fn from(local_utxo: LocalOutput) -> Self {
BdkLocalOutput {
outpoint: local_utxo.outpoint.into(),
txout: local_utxo.txout.into(),
keychain: local_utxo.keychain,
is_spent: local_utxo.is_spent,
derivation_index: local_utxo.derivation_index,
chain_position: local_utxo.chain_position.into(),
}
}
}

// Callback for the FullScanRequest
#[uniffi::export(with_foreign)]
pub trait FullScanScriptInspector: Sync + Send {
Expand Down
33 changes: 31 additions & 2 deletions bdk-ffi/src/wallet.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::bitcoin::{Amount, FeeRate, OutPoint, Psbt, Script, Transaction, TxOut, Txid};
use crate::bitcoin::{Amount, FeeRate, Input, OutPoint, Psbt, Script, Transaction, TxOut, Txid};
use crate::descriptor::Descriptor;
use crate::error::{
CalculateFeeError, CannotConnectError, CreateWithPersistError, DescriptorError,
LoadWithPersistError, PersistenceError, SignerError, TxidParseError,
GetPsbtInputError, LoadWithPersistError, PersistenceError, SighashParseError, SignerError,
TxidParseError,
};
use crate::store::{PersistenceType, Persister};
use crate::types::{
Expand All @@ -11,13 +12,15 @@ use crate::types::{
SyncRequestBuilder, UnconfirmedTx, Update, WalletEvent, WalletKeychain,
};

use bdk_wallet::bitcoin::psbt::PsbtSighashType as BdkPsbtSighashType;
use bdk_wallet::bitcoin::Network;
use bdk_wallet::keys::KeyMap;
#[allow(deprecated)]
use bdk_wallet::signer::SignOptions as BdkSignOptions;
use bdk_wallet::{PersistedWallet, Wallet as BdkWallet};

use std::ops::DerefMut;
use std::str::FromStr;
use std::sync::{Arc, Mutex, MutexGuard};

/// A Bitcoin wallet.
Expand Down Expand Up @@ -214,6 +217,26 @@ impl Wallet {
.map(|local_output| local_output.into())
}

/// Get the corresponding PSBT Input for a `LocalOutput`.
#[uniffi::method(default(sighash_type = None, only_witness_utxo = false))]
pub fn get_psbt_input(
&self,
utxo: LocalOutput,
sighash_type: Option<String>,
only_witness_utxo: bool,
) -> Result<Input, GetPsbtInputError> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nice! Took me a while to see why we are returning GetPsbtInputError here when we have CreateTxError. This is good as we need to handle for string parsing, the 2 possible errors from bdk-wallet (miniscript and unknown), and all others.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks, yup exactly

let outpoint = utxo.outpoint.clone();
let sighash_type = sighash_type
.map(|sighash_type| parse_sighash_type(&sighash_type))
.transpose()?;
let psbt_input = self
.get_wallet()
.get_psbt_input(utxo.into(), sighash_type, only_witness_utxo)
.map_err(|error| GetPsbtInputError::from_bdk_create_tx_error(error, outpoint))?;

Ok(Input::from(&psbt_input))
}

/// Attempt to reveal the next address of the given `keychain`.
///
/// This will increment the keychain's derivation index. If the keychain's descriptor doesn't
Expand Down Expand Up @@ -744,3 +767,9 @@ impl Wallet {
self.inner_mutex.lock().expect("wallet")
}
}

fn parse_sighash_type(sighash: &str) -> Result<BdkPsbtSighashType, SighashParseError> {
BdkPsbtSighashType::from_str(sighash).map_err(|error| SighashParseError::Invalid {
error_message: error.to_string(),
})
}
Loading