Skip to content

Add psbt ffi bindings #36

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ name = "bitcoin_ffi"
default = ["uniffi/cli"]

[dependencies]
bitcoin = { version = "0.32.4" }
bitcoin = { version = "0.32.4", features = ["base64"] }
uniffi = { version = "0.29.1" }
thiserror = "1.0.58"

Expand Down
2 changes: 1 addition & 1 deletion src/bitcoin.udl
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ enum Network {
"Testnet4",
"Signet",
"Regtest"
};
};
196 changes: 196 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ pub use bitcoin::address::ParseError as BitcoinParseError;
use bitcoin::amount::ParseAmountError as BitcoinParseAmountError;
use bitcoin::consensus::encode::Error as BitcoinEncodeError;
use bitcoin::hex::DisplayHex;
use bitcoin::psbt::Error as BitcoinPsbtError;
use bitcoin::psbt::ExtractTxError as BitcoinExtractTxError;
use bitcoin::psbt::PsbtParseError as BitcoinPsbtParseError;

#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum AddressParseError {
Expand Down Expand Up @@ -166,3 +169,196 @@ impl From<BitcoinEncodeError> for EncodeError {
}
}
}

#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum PsbtError {
Comment on lines +173 to +174
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe remote error types can be used with uniffi::remote(Enum). Then I'd guess they don't need to derive thiserror::Error, or require a conversion, or require the explicit display #[error] attributes. Not sure If it has been tried in this repo before. I think an enum still needs to be written with each variant but some of the other boilerplate that might get out of sync can go. I am not certain of this but the documentation suggests it.

https://mozilla.github.io/uniffi-rs/latest/types/remote_ext_types.html

Copy link
Author

@benalleng benalleng May 6, 2025

Choose a reason for hiding this comment

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

With the Psbt Error Enum there are a lot of complex error payload types, wouldn't these all still need to be converted for this to be easily used as a remote(Enum)? I'm just not sure its worth it to avoid error messages getting out of sync unless there is a way to more easily avoid those conversions.

Copy link
Contributor

@DanGould DanGould May 6, 2025

Choose a reason for hiding this comment

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

I think #[uniffi(flat_error)] might be able to keep the upstream payload types but inherit debug/display implementations (perhaps with #[uniffi::export(Debug), uniffi::export(Display)], not sure to what extent these macros can be combined). You may need to rely on the bitcoin/std feature so errors are std::error::Error in order to make sure UniFFI knows the remote PsbtError can be thrown as an error.

Copy link
Member

Choose a reason for hiding this comment

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

Errors have been a pain in our side for a long time, but I'm curious to see if some of the new features in uniffi might fix some of those pain points for us. In particular, simply using the remote error types would be awesome instead of reimplementing errors.

For a comprehensive dive into this from a year ago (some things have changed since then so I still have hope!) see this issue: bitcoindevkit/bdk-ffi#509.

Copy link
Member

@thunderbiscuit thunderbiscuit May 6, 2025

Choose a reason for hiding this comment

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

From what I remember our problem was mostly about having to choose between offering the complex types in the payload vs correct and interesting error messages for users (which they expect in their target languages).

I think if you have complex types in the payload, your Display becomes a stringified version of the type, and that's what you get in Kotlin for your exception.message field (where you expect a good message). Same thing for Swift (I think it's the description field). If you want to instead fix this message, you cannot have fields in your enum (something like that). The issue as far as I remember is that for some errors you'd much rather have the message, and for others you'd rather have the data... 😅

Copy link
Member

Choose a reason for hiding this comment

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

Note that you cannot use #[uniffi::export(Display)] on enums (you get the error unsupported item: only functions and impl blocks may be annotated with this attribute).

Copy link
Contributor

Choose a reason for hiding this comment

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

Are you sure that message is related? I definitely use #[uniffi::export(Display)] on this struct which is neither a function nor an impl block

Copy link
Member

Choose a reason for hiding this comment

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

Ok weird. Maybe I got this wrong. I tried a bunch of stuff 2 weeks ago to play with the errors a bit more and got to the same place we ended up so far.

Let me know how you make out I'm definitely interested in better errors!

#[error("invalid magic")]
InvalidMagic,
#[error("UTXO information is not present in PSBT")]
MissingUtxo,
#[error("invalid separator")]
InvalidSeparator,
#[error("output index is out of bounds of non witness script output array")]
PsbtUtxoOutOfBounds,
#[error("invalid key: {key}")]
InvalidKey { key: String },
#[error("non-proprietary key type found when proprietary key was expected")]
InvalidProprietaryKey,
#[error("duplicate key: {key}")]
DuplicateKey { key: String },
#[error("the unsigned transaction has script sigs")]
UnsignedTxHasScriptSigs,
#[error("the unsigned transaction has script witnesses")]
UnsignedTxHasScriptWitnesses,
#[error("partially signed transactions must have an unsigned transaction")]
MustHaveUnsignedTx,
#[error("no more key-value pairs for this psbt map")]
NoMorePairs,
#[error("different unsigned transaction")]
UnexpectedUnsignedTx,
#[error("non-standard sighash type: {sighash}")]
NonStandardSighashType { sighash: u32 },
#[error("invalid hash when parsing slice: {hash}")]
InvalidHash { hash: String },
#[error("preimage does not match")]
InvalidPreimageHashPair,
#[error("combine conflict: {xpub}")]
CombineInconsistentKeySources { xpub: String },
#[error("bitcoin consensus encoding error: {encoding_error}")]
ConsensusEncoding { encoding_error: String },
#[error("PSBT has a negative fee which is not allowed")]
NegativeFee,
#[error("integer overflow in fee calculation")]
FeeOverflow,
#[error("invalid public key {error_message}")]
InvalidPublicKey { error_message: String },
#[error("invalid secp256k1 public key: {secp256k1_error}")]
InvalidSecp256k1PublicKey { secp256k1_error: String },
#[error("invalid xonly public key")]
InvalidXOnlyPublicKey,
#[error("invalid ECDSA signature: {error_message}")]
InvalidEcdsaSignature { error_message: String },
#[error("invalid taproot signature: {error_message}")]
InvalidTaprootSignature { error_message: String },
#[error("invalid control block")]
InvalidControlBlock,
#[error("invalid leaf version")]
InvalidLeafVersion,
#[error("taproot error")]
Taproot,
#[error("taproot tree error: {error_message}")]
TapTree { error_message: String },
#[error("xpub key error")]
XPubKey,
#[error("version error: {error_message}")]
Version { error_message: String },
#[error("data not consumed entirely when explicitly deserializing")]
PartialDataConsumption,
#[error("I/O error: {error_message}")]
Io { error_message: String },
#[error("other PSBT error")]
OtherPsbtErr,
}

impl From<BitcoinPsbtError> for PsbtError {
fn from(error: BitcoinPsbtError) -> Self {
match error {
BitcoinPsbtError::InvalidMagic => PsbtError::InvalidMagic,
BitcoinPsbtError::MissingUtxo => PsbtError::MissingUtxo,
BitcoinPsbtError::InvalidSeparator => PsbtError::InvalidSeparator,
BitcoinPsbtError::PsbtUtxoOutOfbounds => PsbtError::PsbtUtxoOutOfBounds,
BitcoinPsbtError::InvalidKey(key) => PsbtError::InvalidKey {
key: key.to_string(),
},
BitcoinPsbtError::InvalidProprietaryKey => PsbtError::InvalidProprietaryKey,
BitcoinPsbtError::DuplicateKey(key) => PsbtError::DuplicateKey {
key: key.to_string(),
},
BitcoinPsbtError::UnsignedTxHasScriptSigs => PsbtError::UnsignedTxHasScriptSigs,
BitcoinPsbtError::UnsignedTxHasScriptWitnesses => {
PsbtError::UnsignedTxHasScriptWitnesses
}
BitcoinPsbtError::MustHaveUnsignedTx => PsbtError::MustHaveUnsignedTx,
BitcoinPsbtError::NoMorePairs => PsbtError::NoMorePairs,
BitcoinPsbtError::UnexpectedUnsignedTx { .. } => PsbtError::UnexpectedUnsignedTx,
BitcoinPsbtError::NonStandardSighashType(sighash) => {
PsbtError::NonStandardSighashType { sighash }
}
BitcoinPsbtError::InvalidHash(hash) => PsbtError::InvalidHash {
hash: hash.to_string(),
},
BitcoinPsbtError::InvalidPreimageHashPair { .. } => PsbtError::InvalidPreimageHashPair,
BitcoinPsbtError::CombineInconsistentKeySources(xpub) => {
PsbtError::CombineInconsistentKeySources {
xpub: xpub.to_string(),
}
}
BitcoinPsbtError::ConsensusEncoding(encoding_error) => PsbtError::ConsensusEncoding {
encoding_error: encoding_error.to_string(),
},
BitcoinPsbtError::NegativeFee => PsbtError::NegativeFee,
BitcoinPsbtError::FeeOverflow => PsbtError::FeeOverflow,
BitcoinPsbtError::InvalidPublicKey(e) => PsbtError::InvalidPublicKey {
error_message: e.to_string(),
},
BitcoinPsbtError::InvalidSecp256k1PublicKey(e) => {
PsbtError::InvalidSecp256k1PublicKey {
secp256k1_error: e.to_string(),
}
}
BitcoinPsbtError::InvalidXOnlyPublicKey => PsbtError::InvalidXOnlyPublicKey,
BitcoinPsbtError::InvalidEcdsaSignature(e) => PsbtError::InvalidEcdsaSignature {
error_message: e.to_string(),
},
BitcoinPsbtError::InvalidTaprootSignature(e) => PsbtError::InvalidTaprootSignature {
error_message: e.to_string(),
},
BitcoinPsbtError::InvalidControlBlock => PsbtError::InvalidControlBlock,
BitcoinPsbtError::InvalidLeafVersion => PsbtError::InvalidLeafVersion,
BitcoinPsbtError::Taproot(_) => PsbtError::Taproot,
BitcoinPsbtError::TapTree(e) => PsbtError::TapTree {
error_message: e.to_string(),
},
BitcoinPsbtError::XPubKey(_) => PsbtError::XPubKey,
BitcoinPsbtError::Version(e) => PsbtError::Version {
error_message: e.to_string(),
},
BitcoinPsbtError::PartialDataConsumption => PsbtError::PartialDataConsumption,
BitcoinPsbtError::Io(e) => PsbtError::Io {
error_message: e.to_string(),
},
_ => PsbtError::OtherPsbtErr,
}
}
}

#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum PsbtParseError {
#[error("error in internal psbt data structure: {error_message}")]
PsbtEncoding { error_message: String },
#[error("error in psbt base64 encoding: {error_message}")]
Base64Encoding { error_message: String },
}

impl From<BitcoinPsbtParseError> for PsbtParseError {
fn from(error: BitcoinPsbtParseError) -> Self {
match error {
BitcoinPsbtParseError::PsbtEncoding(e) => PsbtParseError::PsbtEncoding {
error_message: e.to_string(),
},
BitcoinPsbtParseError::Base64Encoding(e) => PsbtParseError::Base64Encoding {
error_message: e.to_string(),
},
_ => {
unreachable!("this is required because of the non-exhaustive enum in rust-bitcoin")
}
}
}
}

#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum ExtractTxError {
#[error("feerate is too high {fee_rate}")]
AbsurdFeeRate { fee_rate: String },
#[error("input[s] are missing information")]
MissingInputValue,
#[error("input is less than the output value")]
SendingTooMuch,
#[error("other extract tx error")]
OtherExtractTxErr,
}

impl From<BitcoinExtractTxError> for ExtractTxError {
fn from(error: BitcoinExtractTxError) -> Self {
match error {
BitcoinExtractTxError::AbsurdFeeRate { fee_rate, .. } => {
ExtractTxError::AbsurdFeeRate {
fee_rate: fee_rate.to_string(),
}
}
BitcoinExtractTxError::MissingInputValue { .. } => ExtractTxError::MissingInputValue,
BitcoinExtractTxError::SendingTooMuch { .. } => ExtractTxError::SendingTooMuch,
_ => ExtractTxError::OtherExtractTxErr,
}
}
}
58 changes: 57 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ use bitcoin::consensus::{deserialize, serialize};
pub use bitcoin::BlockHash;
pub use bitcoin::Txid;

use error::AddressParseError;
use error::EncodeError;
use error::ExtractTxError;
use error::FeeRateError;
use error::FromScriptError;
use error::ParseAmountError;
use error::PsbtError;
use error::{AddressParseError, PsbtParseError};

use std::fmt::Display;
use std::str::FromStr;
Expand Down Expand Up @@ -297,6 +299,60 @@ impl Transaction {
impl_from_core_type!(Transaction, bitcoin::Transaction);
impl_from_ffi_type!(Transaction, bitcoin::Transaction);

#[derive(Debug, Clone, PartialEq, Eq, uniffi::Object)]
pub struct Psbt(bitcoin::Psbt);

#[uniffi::export]
impl Psbt {
#[uniffi::constructor]
pub fn from_unsigned_tx(tx: Arc<Transaction>) -> Result<Self, PsbtError> {
let psbt = bitcoin::Psbt::from_unsigned_tx(tx.0.clone().into())?;
Ok(Psbt(psbt))
}

#[uniffi::constructor]
pub fn deserialize(psbt_bytes: &[u8]) -> Result<Self, PsbtError> {
let psbt = bitcoin::Psbt::deserialize(psbt_bytes)?;
Ok(psbt.into())
}

#[uniffi::constructor]
pub fn deserialize_base64(psbt_base64: String) -> Result<Self, PsbtParseError> {
let psbt = bitcoin::Psbt::from_str(&psbt_base64)?;
Ok(psbt.into())
}

pub fn serialize(&self) -> Vec<u8> {
self.0.serialize()
}

pub fn serialize_hex(&self) -> String {
self.0.serialize_hex()
}

pub fn serialize_base64(&self) -> String {
self.0.to_string()
}

pub fn extract_tx(&self) -> Result<Arc<Transaction>, ExtractTxError> {
Ok(Arc::new(self.0.clone().extract_tx()?.into()))
}

pub fn combine(&self, other: Arc<Self>) -> Result<Arc<Psbt>, PsbtError> {
let mut psbt = self.0.clone();
let other_psbt = other.0.clone();
psbt.combine(other_psbt)?;
Ok(Arc::new(psbt.into()))
}

pub fn fee(&self) -> Result<Arc<Amount>, PsbtError> {
Ok(Arc::new(self.0.clone().fee()?.into()))
}
}

impl_from_core_type!(Psbt, bitcoin::Psbt);
impl_from_ffi_type!(Psbt, bitcoin::Psbt);

#[derive(Clone, Default, uniffi::Enum)]
#[non_exhaustive]
pub enum Network {
Expand Down