Skip to content

Commit bc2bdb0

Browse files
committed
feat: Expose psbt output
1 parent 110703d commit bc2bdb0

File tree

2 files changed

+204
-0
lines changed

2 files changed

+204
-0
lines changed

bdk-ffi/src/bitcoin.rs

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ use bdk_wallet::bitcoin::hashes::sha256::Hash as BitcoinSha256Hash;
1919
use bdk_wallet::bitcoin::hashes::sha256d::Hash as BitcoinDoubleSha256Hash;
2020
use bdk_wallet::bitcoin::io::Cursor;
2121
use bdk_wallet::bitcoin::psbt::Input as BdkInput;
22+
use bdk_wallet::bitcoin::psbt::Output as BdkOutput;
2223
use bdk_wallet::bitcoin::secp256k1::Secp256k1;
24+
use bdk_wallet::bitcoin::taproot::TapLeaf as BdkTapLeaf;
2325
use bdk_wallet::bitcoin::Amount as BdkAmount;
2426
use bdk_wallet::bitcoin::BlockHash as BitcoinBlockHash;
2527
use bdk_wallet::bitcoin::FeeRate as BdkFeeRate;
@@ -767,6 +769,172 @@ impl From<&BdkInput> for Input {
767769
}
768770
}
769771

772+
#[derive(Clone, Debug, uniffi::Enum)]
773+
pub enum TapLeaf {
774+
/// A known script
775+
Script,
776+
/// Hidden node
777+
Hidden,
778+
}
779+
780+
#[derive(Clone, Debug, uniffi::Record)]
781+
pub struct LeafNode {
782+
/// The leaf type (script or hidden)
783+
pub leaf_type: TapLeaf,
784+
/// The script if this is a Script leaf (None if Hidden)
785+
pub script: Option<Arc<Script>>,
786+
/// The version if this is a Script leaf (None if Hidden)
787+
pub version: Option<u8>,
788+
/// The hash if this is a Hidden leaf (None if Script)
789+
pub hash: Option<String>,
790+
/// The merkle proof (hashing partners) to get this node.
791+
pub merkle_branch: Vec<String>,
792+
}
793+
794+
#[derive(Clone, Debug, uniffi::Record)]
795+
pub struct TapTree {
796+
/// Merkle hash for this node.
797+
pub hash: String,
798+
/// Tracks information on hidden nodes below this node.
799+
pub has_hidden_nodes: bool,
800+
/// Information about leaves inside this node.
801+
pub leaves: Vec<LeafNode>,
802+
}
803+
804+
#[derive(Clone, Debug, uniffi::Record)]
805+
pub struct Output {
806+
/// The redeem script for this output.
807+
pub redeem_script: Option<Arc<Script>>,
808+
/// The witness script for this output.
809+
pub witness_script: Option<Arc<Script>>,
810+
/// Map of public keys needed to spend this output to their corresponding
811+
/// master key fingerprints and derivation paths.
812+
pub bip32_derivation: HashMap<String, KeySource>,
813+
/// Taproot Internal key.
814+
pub tap_internal_key: Option<String>,
815+
/// Taproot Output tree (structured record).
816+
pub tap_tree: Option<TapTree>,
817+
/// Map of tap root x only keys to origin info and leaf hashes contained in it.
818+
pub tap_key_origins: HashMap<String, TapKeyOrigin>,
819+
/// Proprietary key-value pairs for this output.
820+
pub proprietary: HashMap<ProprietaryKey, Vec<u8>>,
821+
/// Unknown key-value pairs for this output.
822+
pub unknown: HashMap<Key, Vec<u8>>,
823+
}
824+
825+
impl From<&BdkOutput> for Output {
826+
fn from(output: &BdkOutput) -> Self {
827+
Output {
828+
redeem_script: output
829+
.redeem_script
830+
.as_ref()
831+
.map(|s| Arc::new(Script(s.clone()))),
832+
witness_script: output
833+
.witness_script
834+
.as_ref()
835+
.map(|s| Arc::new(Script(s.clone()))),
836+
bip32_derivation: output
837+
.bip32_derivation
838+
.iter()
839+
.map(|(pk, (fingerprint, deriv_path))| {
840+
(
841+
pk.to_string(),
842+
KeySource {
843+
fingerprint: fingerprint.to_string(),
844+
path: Arc::new(deriv_path.clone().into()),
845+
},
846+
)
847+
})
848+
.collect(),
849+
tap_internal_key: output.tap_internal_key.as_ref().map(|k| k.to_string()),
850+
tap_tree: output.tap_tree.as_ref().map(|t| {
851+
let node_info = t.node_info();
852+
let mut has_hidden_nodes = false;
853+
let leaves: Vec<LeafNode> = node_info
854+
.leaf_nodes()
855+
.map(|leaf_node| {
856+
let (leaf_type, script, version, hash) = match leaf_node.leaf() {
857+
BdkTapLeaf::Script(script, ver) => (
858+
TapLeaf::Script,
859+
Some(Arc::new(Script(script.clone()))),
860+
Some(ver.to_consensus()),
861+
None,
862+
),
863+
BdkTapLeaf::Hidden(hash) => {
864+
has_hidden_nodes = true;
865+
(TapLeaf::Hidden, None, None, Some(hash.to_string()))
866+
}
867+
};
868+
869+
LeafNode {
870+
leaf_type,
871+
script,
872+
version,
873+
hash,
874+
merkle_branch: leaf_node
875+
.merkle_branch()
876+
.iter()
877+
.map(|h| h.to_string())
878+
.collect(),
879+
}
880+
})
881+
.collect();
882+
883+
TapTree {
884+
hash: node_info.node_hash().to_string(),
885+
has_hidden_nodes,
886+
leaves,
887+
}
888+
}),
889+
tap_key_origins: output
890+
.tap_key_origins
891+
.iter()
892+
.map(|(k, v)| {
893+
let key = k.to_string();
894+
let value = TapKeyOrigin {
895+
tap_leaf_hashes: v.0.iter().map(|h| h.to_string()).collect(),
896+
key_source: KeySource {
897+
// Unnecessary spaces being added by fmt. We use #[rustfmt::skip] to avoid them for now.
898+
#[rustfmt::skip]
899+
fingerprint: v.1.0.to_string(),
900+
#[rustfmt::skip]
901+
path: Arc::new(v.1.1.clone().into()),
902+
},
903+
};
904+
(key, value)
905+
})
906+
.collect(),
907+
proprietary: output
908+
.proprietary
909+
.iter()
910+
.map(|(k, v)| {
911+
(
912+
ProprietaryKey {
913+
prefix: k.prefix.clone(),
914+
subtype: k.subtype,
915+
key: k.key.clone(),
916+
},
917+
v.to_vec(),
918+
)
919+
})
920+
.collect(),
921+
unknown: output
922+
.unknown
923+
.iter()
924+
.map(|(k, v)| {
925+
(
926+
Key {
927+
key: k.key.clone(),
928+
type_value: k.type_value,
929+
},
930+
v.to_vec(),
931+
)
932+
})
933+
.collect(),
934+
}
935+
}
936+
}
937+
770938
/// A Partially Signed Transaction.
771939
#[derive(uniffi::Object)]
772940
pub struct Psbt(pub(crate) Mutex<BdkPsbt>);
@@ -898,6 +1066,12 @@ impl Psbt {
8981066
let psbt = self.0.lock().unwrap();
8991067
psbt.inputs.iter().map(|input| input.into()).collect()
9001068
}
1069+
1070+
/// The corresponding key-value map for each output in the unsigned transaction.
1071+
pub fn output(&self) -> Vec<Output> {
1072+
let psbt = self.0.lock().unwrap();
1073+
psbt.outputs.iter().map(|o| o.into()).collect()
1074+
}
9011075
}
9021076

9031077
impl From<BdkPsbt> for Psbt {

bdk-ffi/src/tests/bitcoin.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,36 @@ fn test_psbt_input_proprietary() {
524524
);
525525
}
526526

527+
#[test]
528+
fn test_psbt_output_length() {
529+
let psbt = sample_psbt();
530+
let psbt_outputs = psbt.output();
531+
println!("Psbt Output: {:?}", psbt_outputs);
532+
533+
assert_eq!(psbt_outputs.len(), 2);
534+
}
535+
536+
#[test]
537+
fn test_psbt_output_witness_script() {
538+
let psbt = sample_psbt();
539+
println!("Psbt: {:?}", psbt.json_serialize());
540+
let psbt_outputs = psbt.output();
541+
assert!(!psbt_outputs.is_empty(), "Output should not be empty");
542+
println!("Psbt Output: {:?}", psbt_outputs);
543+
let output = &psbt_outputs[0];
544+
let witness_script = output
545+
.witness_script
546+
.as_ref()
547+
.expect("Witness script should be present");
548+
let byte_witness = witness_script.to_bytes();
549+
let witness_hex = DisplayHex::to_lower_hex_string(&byte_witness);
550+
let expected_witness_hex = "522103b72bf1f4c738fb44fadd3333789626fa5f3efb0d695c90d126abea721ef6d417210326ee4ece63eabe2ec81eddb5400ae49af6bd7d26cfa536e4ed1217a15a4a5ed621027a51e6ce68730ec4130e702921c9d6473de8151ebc517d5a83c8df93f48aba8a53ae";
551+
assert_eq!(
552+
witness_hex, expected_witness_hex,
553+
"Witness script hex does not match the expected value"
554+
);
555+
}
556+
527557
fn sample_psbt() -> Psbt {
528558
Psbt::new("cHNidP8BAH0CAAAAAXHl8cCbj84lm1v42e54IGI6CQru/nBXwrPE3q2fiGO4AAAAAAD9////Ar4DAAAAAAAAIgAgYw/rnGd4Bifj8s7TaMgR2tal/lq+L1jVv2Sqd1mxMbJEEQAAAAAAABYAFNVpt8vHYUPZNSF6Hu07uP1YeHts4QsAAAABALUCAAAAAAEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////BAJ+CwD/////AkAlAAAAAAAAIgAgQyrnn86L9D3vDiH959KJbPudDHc/bp6nI9E5EBLQD1YAAAAAAAAAACZqJKohqe3i9hw/cdHe/T+pmd+jaVN1XGkGiXmZYrSL69g2l06M+QEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQErQCUAAAAAAAAiACBDKuefzov0Pe8OIf3n0ols+50Mdz9unqcj0TkQEtAPViICAy4V+d/Qff71zzPXxK4FWG5x+wL/Ku93y/LG5p+0rI2xSDBFAiEA9b0OdASAs0P2uhQinjN7QGP5jX/b32LcShBmny8U0RUCIBebxvCDbpchCjqLAhOMjydT80DAzokaalGzV7XVTsbiASICA1tMY+46EgxIHU18bgHnUvAAlAkMq5LfwkpOGZ97sDKRRzBEAiBpmlZwJocNEiKLxexEX0Par6UgG8a89AklTG3/z9AHlAIgQH/ybCvfKJzr2dq0+IyueDebm7FamKIJdzBYWMXRr/wBIgID+aCzK9nclwhbbN7KbIVGUQGLWZsjcaqWPxk9gFeG+FxIMEUCIQDRPBzb0i9vaUmxCcs1yz8uq4tq1mdDAYvvYn3isKEhFAIgfmeTLLzMo0mmQ23ooMnyx6iPceE8xV5CvARuJsd88tEBAQVpUiEDW0xj7joSDEgdTXxuAedS8ACUCQyrkt/CSk4Zn3uwMpEhAy4V+d/Qff71zzPXxK4FWG5x+wL/Ku93y/LG5p+0rI2xIQP5oLMr2dyXCFts3spshUZRAYtZmyNxqpY/GT2AV4b4XFOuIgYDLhX539B9/vXPM9fErgVYbnH7Av8q73fL8sbmn7SsjbEYCapBE1QAAIABAACAAAAAgAAAAAAAAAAAIgYDW0xj7joSDEgdTXxuAedS8ACUCQyrkt/CSk4Zn3uwMpEY2bvrelQAAIABAACAAAAAgAAAAAAAAAAAIgYD+aCzK9nclwhbbN7KbIVGUQGLWZsjcaqWPxk9gFeG+FwYAKVFVFQAAIABAACAAAAAgAAAAAAAAAAAAAEBaVIhA7cr8fTHOPtE+t0zM3iWJvpfPvsNaVyQ0Sar6nIe9tQXIQMm7k7OY+q+Lsge3bVACuSa9r19Js+lNuTtEhehWkpe1iECelHmzmhzDsQTDnApIcnWRz3oFR68UX1ag8jfk/SKuopTriICAnpR5s5ocw7EEw5wKSHJ1kc96BUevFF9WoPI35P0irqKGAClRVRUAACAAQAAgAAAAIABAAAAAAAAACICAybuTs5j6r4uyB7dtUAK5Jr2vX0mz6U25O0SF6FaSl7WGAmqQRNUAACAAQAAgAAAAIABAAAAAAAAACICA7cr8fTHOPtE+t0zM3iWJvpfPvsNaVyQ0Sar6nIe9tQXGNm763pUAACAAQAAgAAAAIABAAAAAAAAAAAA".to_string())
529559
.unwrap()

0 commit comments

Comments
 (0)