diff --git a/Cargo.toml b/Cargo.toml index e56b997..9271f95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/bitcoin.udl b/src/bitcoin.udl index cbd7d68..7284a63 100644 --- a/src/bitcoin.udl +++ b/src/bitcoin.udl @@ -7,4 +7,4 @@ enum Network { "Testnet4", "Signet", "Regtest" -}; \ No newline at end of file +}; diff --git a/src/error.rs b/src/error.rs index ded9785..adc3eb5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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 { @@ -166,3 +169,196 @@ impl From for EncodeError { } } } + +#[derive(Debug, thiserror::Error, uniffi::Error)] +pub enum PsbtError { + #[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 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 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 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, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index cd68c0c..638eef6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; @@ -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) -> Result { + let psbt = bitcoin::Psbt::from_unsigned_tx(tx.0.clone().into())?; + Ok(Psbt(psbt)) + } + + #[uniffi::constructor] + pub fn deserialize(psbt_bytes: &[u8]) -> Result { + let psbt = bitcoin::Psbt::deserialize(psbt_bytes)?; + Ok(psbt.into()) + } + + #[uniffi::constructor] + pub fn deserialize_base64(psbt_base64: String) -> Result { + let psbt = bitcoin::Psbt::from_str(&psbt_base64)?; + Ok(psbt.into()) + } + + pub fn serialize(&self) -> Vec { + 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, ExtractTxError> { + Ok(Arc::new(self.0.clone().extract_tx()?.into())) + } + + pub fn combine(&self, other: Arc) -> Result, 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, 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 {