Skip to content

Commit ecb2fd4

Browse files
rustaceanrobbenalleng
authored andcommitted
Add psbt ffi bindings
This add some basic ffi bindings for psbts including serialization and deserizlization as well as a few methods used in payjoin integration tests that are needed for downstream bindings there.
1 parent 6b1d131 commit ecb2fd4

File tree

4 files changed

+255
-3
lines changed

4 files changed

+255
-3
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ name = "bitcoin_ffi"
1212
default = ["uniffi/cli"]
1313

1414
[dependencies]
15-
bitcoin = { version = "0.32.4" }
15+
bitcoin = { version = "0.32.4", features = ["base64"] }
1616
uniffi = { version = "0.29.1" }
1717
thiserror = "1.0.58"
1818

src/bitcoin.udl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ enum Network {
77
"Testnet4",
88
"Signet",
99
"Regtest"
10-
};
10+
};

src/error.rs

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ pub use bitcoin::address::ParseError as BitcoinParseError;
33
use bitcoin::amount::ParseAmountError as BitcoinParseAmountError;
44
use bitcoin::consensus::encode::Error as BitcoinEncodeError;
55
use bitcoin::hex::DisplayHex;
6+
use bitcoin::psbt::Error as BitcoinPsbtError;
7+
use bitcoin::psbt::ExtractTxError as BitcoinExtractTxError;
8+
use bitcoin::psbt::PsbtParseError as BitcoinPsbtParseError;
69

710
#[derive(Debug, thiserror::Error, uniffi::Error)]
811
pub enum AddressParseError {
@@ -166,3 +169,196 @@ impl From<BitcoinEncodeError> for EncodeError {
166169
}
167170
}
168171
}
172+
173+
#[derive(Debug, thiserror::Error, uniffi::Error)]
174+
pub enum PsbtError {
175+
#[error("invalid magic")]
176+
InvalidMagic,
177+
#[error("UTXO information is not present in PSBT")]
178+
MissingUtxo,
179+
#[error("invalid separator")]
180+
InvalidSeparator,
181+
#[error("output index is out of bounds of non witness script output array")]
182+
PsbtUtxoOutOfBounds,
183+
#[error("invalid key: {key}")]
184+
InvalidKey { key: String },
185+
#[error("non-proprietary key type found when proprietary key was expected")]
186+
InvalidProprietaryKey,
187+
#[error("duplicate key: {key}")]
188+
DuplicateKey { key: String },
189+
#[error("the unsigned transaction has script sigs")]
190+
UnsignedTxHasScriptSigs,
191+
#[error("the unsigned transaction has script witnesses")]
192+
UnsignedTxHasScriptWitnesses,
193+
#[error("partially signed transactions must have an unsigned transaction")]
194+
MustHaveUnsignedTx,
195+
#[error("no more key-value pairs for this psbt map")]
196+
NoMorePairs,
197+
#[error("different unsigned transaction")]
198+
UnexpectedUnsignedTx,
199+
#[error("non-standard sighash type: {sighash}")]
200+
NonStandardSighashType { sighash: u32 },
201+
#[error("invalid hash when parsing slice: {hash}")]
202+
InvalidHash { hash: String },
203+
#[error("preimage does not match")]
204+
InvalidPreimageHashPair,
205+
#[error("combine conflict: {xpub}")]
206+
CombineInconsistentKeySources { xpub: String },
207+
#[error("bitcoin consensus encoding error: {encoding_error}")]
208+
ConsensusEncoding { encoding_error: String },
209+
#[error("PSBT has a negative fee which is not allowed")]
210+
NegativeFee,
211+
#[error("integer overflow in fee calculation")]
212+
FeeOverflow,
213+
#[error("invalid public key {error_message}")]
214+
InvalidPublicKey { error_message: String },
215+
#[error("invalid secp256k1 public key: {secp256k1_error}")]
216+
InvalidSecp256k1PublicKey { secp256k1_error: String },
217+
#[error("invalid xonly public key")]
218+
InvalidXOnlyPublicKey,
219+
#[error("invalid ECDSA signature: {error_message}")]
220+
InvalidEcdsaSignature { error_message: String },
221+
#[error("invalid taproot signature: {error_message}")]
222+
InvalidTaprootSignature { error_message: String },
223+
#[error("invalid control block")]
224+
InvalidControlBlock,
225+
#[error("invalid leaf version")]
226+
InvalidLeafVersion,
227+
#[error("taproot error")]
228+
Taproot,
229+
#[error("taproot tree error: {error_message}")]
230+
TapTree { error_message: String },
231+
#[error("xpub key error")]
232+
XPubKey,
233+
#[error("version error: {error_message}")]
234+
Version { error_message: String },
235+
#[error("data not consumed entirely when explicitly deserializing")]
236+
PartialDataConsumption,
237+
#[error("I/O error: {error_message}")]
238+
Io { error_message: String },
239+
#[error("other PSBT error")]
240+
OtherPsbtErr,
241+
}
242+
243+
impl From<BitcoinPsbtError> for PsbtError {
244+
fn from(error: BitcoinPsbtError) -> Self {
245+
match error {
246+
BitcoinPsbtError::InvalidMagic => PsbtError::InvalidMagic,
247+
BitcoinPsbtError::MissingUtxo => PsbtError::MissingUtxo,
248+
BitcoinPsbtError::InvalidSeparator => PsbtError::InvalidSeparator,
249+
BitcoinPsbtError::PsbtUtxoOutOfbounds => PsbtError::PsbtUtxoOutOfBounds,
250+
BitcoinPsbtError::InvalidKey(key) => PsbtError::InvalidKey {
251+
key: key.to_string(),
252+
},
253+
BitcoinPsbtError::InvalidProprietaryKey => PsbtError::InvalidProprietaryKey,
254+
BitcoinPsbtError::DuplicateKey(key) => PsbtError::DuplicateKey {
255+
key: key.to_string(),
256+
},
257+
BitcoinPsbtError::UnsignedTxHasScriptSigs => PsbtError::UnsignedTxHasScriptSigs,
258+
BitcoinPsbtError::UnsignedTxHasScriptWitnesses => {
259+
PsbtError::UnsignedTxHasScriptWitnesses
260+
}
261+
BitcoinPsbtError::MustHaveUnsignedTx => PsbtError::MustHaveUnsignedTx,
262+
BitcoinPsbtError::NoMorePairs => PsbtError::NoMorePairs,
263+
BitcoinPsbtError::UnexpectedUnsignedTx { .. } => PsbtError::UnexpectedUnsignedTx,
264+
BitcoinPsbtError::NonStandardSighashType(sighash) => {
265+
PsbtError::NonStandardSighashType { sighash }
266+
}
267+
BitcoinPsbtError::InvalidHash(hash) => PsbtError::InvalidHash {
268+
hash: hash.to_string(),
269+
},
270+
BitcoinPsbtError::InvalidPreimageHashPair { .. } => PsbtError::InvalidPreimageHashPair,
271+
BitcoinPsbtError::CombineInconsistentKeySources(xpub) => {
272+
PsbtError::CombineInconsistentKeySources {
273+
xpub: xpub.to_string(),
274+
}
275+
}
276+
BitcoinPsbtError::ConsensusEncoding(encoding_error) => PsbtError::ConsensusEncoding {
277+
encoding_error: encoding_error.to_string(),
278+
},
279+
BitcoinPsbtError::NegativeFee => PsbtError::NegativeFee,
280+
BitcoinPsbtError::FeeOverflow => PsbtError::FeeOverflow,
281+
BitcoinPsbtError::InvalidPublicKey(e) => PsbtError::InvalidPublicKey {
282+
error_message: e.to_string(),
283+
},
284+
BitcoinPsbtError::InvalidSecp256k1PublicKey(e) => {
285+
PsbtError::InvalidSecp256k1PublicKey {
286+
secp256k1_error: e.to_string(),
287+
}
288+
}
289+
BitcoinPsbtError::InvalidXOnlyPublicKey => PsbtError::InvalidXOnlyPublicKey,
290+
BitcoinPsbtError::InvalidEcdsaSignature(e) => PsbtError::InvalidEcdsaSignature {
291+
error_message: e.to_string(),
292+
},
293+
BitcoinPsbtError::InvalidTaprootSignature(e) => PsbtError::InvalidTaprootSignature {
294+
error_message: e.to_string(),
295+
},
296+
BitcoinPsbtError::InvalidControlBlock => PsbtError::InvalidControlBlock,
297+
BitcoinPsbtError::InvalidLeafVersion => PsbtError::InvalidLeafVersion,
298+
BitcoinPsbtError::Taproot(_) => PsbtError::Taproot,
299+
BitcoinPsbtError::TapTree(e) => PsbtError::TapTree {
300+
error_message: e.to_string(),
301+
},
302+
BitcoinPsbtError::XPubKey(_) => PsbtError::XPubKey,
303+
BitcoinPsbtError::Version(e) => PsbtError::Version {
304+
error_message: e.to_string(),
305+
},
306+
BitcoinPsbtError::PartialDataConsumption => PsbtError::PartialDataConsumption,
307+
BitcoinPsbtError::Io(e) => PsbtError::Io {
308+
error_message: e.to_string(),
309+
},
310+
_ => PsbtError::OtherPsbtErr,
311+
}
312+
}
313+
}
314+
315+
#[derive(Debug, thiserror::Error, uniffi::Error)]
316+
pub enum PsbtParseError {
317+
#[error("error in internal psbt data structure: {error_message}")]
318+
PsbtEncoding { error_message: String },
319+
#[error("error in psbt base64 encoding: {error_message}")]
320+
Base64Encoding { error_message: String },
321+
}
322+
323+
impl From<BitcoinPsbtParseError> for PsbtParseError {
324+
fn from(error: BitcoinPsbtParseError) -> Self {
325+
match error {
326+
BitcoinPsbtParseError::PsbtEncoding(e) => PsbtParseError::PsbtEncoding {
327+
error_message: e.to_string(),
328+
},
329+
BitcoinPsbtParseError::Base64Encoding(e) => PsbtParseError::Base64Encoding {
330+
error_message: e.to_string(),
331+
},
332+
_ => {
333+
unreachable!("this is required because of the non-exhaustive enum in rust-bitcoin")
334+
}
335+
}
336+
}
337+
}
338+
339+
#[derive(Debug, thiserror::Error, uniffi::Error)]
340+
pub enum ExtractTxError {
341+
#[error("feerate is too high {fee_rate}")]
342+
AbsurdFeeRate { fee_rate: String },
343+
#[error("input[s] are missing information")]
344+
MissingInputValue,
345+
#[error("input is less than the output value")]
346+
SendingTooMuch,
347+
#[error("other extract tx error")]
348+
OtherExtractTxErr,
349+
}
350+
351+
impl From<BitcoinExtractTxError> for ExtractTxError {
352+
fn from(error: BitcoinExtractTxError) -> Self {
353+
match error {
354+
BitcoinExtractTxError::AbsurdFeeRate { fee_rate, .. } => {
355+
ExtractTxError::AbsurdFeeRate {
356+
fee_rate: fee_rate.to_string(),
357+
}
358+
}
359+
BitcoinExtractTxError::MissingInputValue { .. } => ExtractTxError::MissingInputValue,
360+
BitcoinExtractTxError::SendingTooMuch { .. } => ExtractTxError::SendingTooMuch,
361+
_ => ExtractTxError::OtherExtractTxErr,
362+
}
363+
}
364+
}

src/lib.rs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ use bitcoin::consensus::{deserialize, serialize};
44
pub use bitcoin::BlockHash;
55
pub use bitcoin::Txid;
66

7-
use error::AddressParseError;
87
use error::EncodeError;
8+
use error::ExtractTxError;
99
use error::FeeRateError;
1010
use error::FromScriptError;
1111
use error::ParseAmountError;
12+
use error::PsbtError;
13+
use error::{AddressParseError, PsbtParseError};
1214

1315
use std::fmt::Display;
1416
use std::str::FromStr;
@@ -297,6 +299,60 @@ impl Transaction {
297299
impl_from_core_type!(Transaction, bitcoin::Transaction);
298300
impl_from_ffi_type!(Transaction, bitcoin::Transaction);
299301

302+
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Object)]
303+
pub struct Psbt(bitcoin::Psbt);
304+
305+
#[uniffi::export]
306+
impl Psbt {
307+
#[uniffi::constructor]
308+
pub fn from_unsigned_tx(tx: Arc<Transaction>) -> Result<Self, PsbtError> {
309+
let psbt = bitcoin::Psbt::from_unsigned_tx(tx.0.clone().into())?;
310+
Ok(Psbt(psbt))
311+
}
312+
313+
#[uniffi::constructor]
314+
pub fn deserialize(psbt_bytes: &[u8]) -> Result<Self, PsbtError> {
315+
let psbt = bitcoin::Psbt::deserialize(psbt_bytes)?;
316+
Ok(psbt.into())
317+
}
318+
319+
#[uniffi::constructor]
320+
pub fn deserialize_base64(psbt_base64: String) -> Result<Self, PsbtParseError> {
321+
let psbt = bitcoin::Psbt::from_str(&psbt_base64)?;
322+
Ok(psbt.into())
323+
}
324+
325+
pub fn serialize(&self) -> Vec<u8> {
326+
self.0.serialize()
327+
}
328+
329+
pub fn serialize_hex(&self) -> String {
330+
self.0.serialize_hex()
331+
}
332+
333+
pub fn serialize_base64(&self) -> String {
334+
self.0.to_string()
335+
}
336+
337+
pub fn extract_tx(&self) -> Result<Arc<Transaction>, ExtractTxError> {
338+
Ok(Arc::new(self.0.clone().extract_tx()?.into()))
339+
}
340+
341+
pub fn combine(&self, other: Arc<Self>) -> Result<Psbt, PsbtError> {
342+
let mut psbt = self.0.clone();
343+
let other_psbt = other.0.clone();
344+
psbt.combine(other_psbt)?;
345+
Ok(psbt.into())
346+
}
347+
348+
pub fn fee(&self) -> Result<Arc<Amount>, PsbtError> {
349+
Ok(Arc::new(self.0.clone().fee()?.into()))
350+
}
351+
}
352+
353+
impl_from_core_type!(Psbt, bitcoin::Psbt);
354+
impl_from_ffi_type!(Psbt, bitcoin::Psbt);
355+
300356
#[derive(Clone, Default, uniffi::Enum)]
301357
#[non_exhaustive]
302358
pub enum Network {

0 commit comments

Comments
 (0)