@@ -30,21 +30,20 @@ use bdk_wallet::bitcoin::{
3030} ;
3131use bdk_wallet:: chain:: ChainPosition ;
3232use bdk_wallet:: descriptor:: Segwitv0 ;
33+ use bdk_wallet:: keys:: {
34+ DerivableKey , DescriptorKey , DescriptorKey :: Secret , ExtendedKey , GeneratableKey , GeneratedKey ,
35+ bip39:: WordCount ,
36+ } ;
37+ use bdk_wallet:: miniscript:: miniscript;
3338#[ cfg( feature = "sqlite" ) ]
3439use bdk_wallet:: rusqlite:: Connection ;
3540use bdk_wallet:: { KeychainKind , SignOptions , Wallet } ;
3641#[ cfg( feature = "compiler" ) ]
3742use bdk_wallet:: {
3843 descriptor:: { Descriptor , Legacy , Miniscript } ,
39- miniscript:: policy:: Concrete ,
44+ miniscript:: { Tap , descriptor :: TapTree , policy:: Concrete } ,
4045} ;
4146use cli_table:: { Cell , CellStruct , Style , Table , format:: Justify } ;
42-
43- use bdk_wallet:: keys:: {
44- DerivableKey , DescriptorKey , DescriptorKey :: Secret , ExtendedKey , GeneratableKey , GeneratedKey ,
45- bip39:: WordCount ,
46- } ;
47- use bdk_wallet:: miniscript:: miniscript;
4847use serde_json:: json;
4948use std:: collections:: BTreeMap ;
5049#[ cfg( any( feature = "electrum" , feature = "esplora" ) ) ]
@@ -53,14 +52,16 @@ use std::convert::TryFrom;
5352#[ cfg( any( feature = "repl" , feature = "electrum" , feature = "esplora" ) ) ]
5453use std:: io:: Write ;
5554use std:: str:: FromStr ;
55+ #[ cfg( any( feature = "redb" , feature = "compiler" ) ) ]
56+ use std:: sync:: Arc ;
5657
5758#[ cfg( feature = "electrum" ) ]
5859use crate :: utils:: BlockchainClient :: Electrum ;
5960#[ cfg( feature = "cbf" ) ]
6061use bdk_kyoto:: { Info , LightClient } ;
62+ #[ cfg( feature = "compiler" ) ]
63+ use bdk_wallet:: bitcoin:: XOnlyPublicKey ;
6164use bdk_wallet:: bitcoin:: base64:: prelude:: * ;
62- #[ cfg( feature = "redb" ) ]
63- use std:: sync:: Arc ;
6465#[ cfg( feature = "cbf" ) ]
6566use tokio:: select;
6667#[ cfg( any(
8283 bdk_wallet:: chain:: { BlockId , CanonicalizationParams , CheckPoint } ,
8384} ;
8485
86+ #[ cfg( feature = "compiler" ) ]
87+ const NUMS_UNSPENDABLE_KEY_HEX : & str =
88+ "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" ;
89+
8590/// Execute an offline wallet sub-command
8691///
8792/// Offline wallet sub-commands are described in [`OfflineWalletSubCommand`].
@@ -606,9 +611,9 @@ pub(crate) async fn handle_online_wallet_subcommand(
606611 let mut once = HashSet :: < KeychainKind > :: new ( ) ;
607612 move |k, spk_i, _| {
608613 if once. insert ( k) {
609- print ! ( "\n Scanning keychain [{:?}]" , k ) ;
614+ print ! ( "\n Scanning keychain [{k :?}]" ) ;
610615 }
611- print ! ( " {:<3}" , spk_i ) ;
616+ print ! ( " {spk_i :<3}" ) ;
612617 stdout. flush ( ) . expect ( "must flush" ) ;
613618 }
614619 } ) ;
@@ -687,7 +692,7 @@ pub(crate) async fn handle_online_wallet_subcommand(
687692 . start_sync_with_revealed_spks ( )
688693 . inspect ( |item, progress| {
689694 let pc = ( 100 * progress. consumed ( ) ) as f32 / progress. total ( ) as f32 ;
690- eprintln ! ( "[ SCANNING {:03.0}% ] {}" , pc , item ) ;
695+ eprintln ! ( "[ SCANNING {pc :03.0}% ] {item}" ) ;
691696 } ) ;
692697 match client {
693698 #[ cfg( feature = "electrum" ) ]
@@ -813,7 +818,7 @@ pub(crate) async fn handle_online_wallet_subcommand(
813818
814819 let subscriber = tracing_subscriber:: FmtSubscriber :: new ( ) ;
815820 tracing:: subscriber:: set_global_default ( subscriber)
816- . map_err ( |e| Error :: Generic ( format ! ( "SetGlobalDefault error: {}" , e ) ) ) ?;
821+ . map_err ( |e| Error :: Generic ( format ! ( "SetGlobalDefault error: {e}" ) ) ) ?;
817822
818823 tokio:: task:: spawn ( async move { node. run ( ) . await } ) ;
819824 tokio:: task:: spawn ( async move {
@@ -833,7 +838,7 @@ pub(crate) async fn handle_online_wallet_subcommand(
833838 let txid = tx. compute_txid ( ) ;
834839 requester
835840 . broadcast_random ( tx. clone ( ) )
836- . map_err ( |e| Error :: Generic ( format ! ( "{}" , e ) ) ) ?;
841+ . map_err ( |e| Error :: Generic ( format ! ( "{e}" ) ) ) ?;
837842 tokio:: time:: timeout ( tokio:: time:: Duration :: from_secs ( 30 ) , async move {
838843 while let Some ( info) = info_subscriber. recv ( ) . await {
839844 match info {
@@ -874,8 +879,7 @@ pub(crate) fn is_final(psbt: &Psbt) -> Result<(), Error> {
874879 let psbt_inputs = psbt. inputs . len ( ) ;
875880 if unsigned_tx_inputs != psbt_inputs {
876881 return Err ( Error :: Generic ( format ! (
877- "Malformed PSBT, {} unsigned tx inputs and {} psbt inputs." ,
878- unsigned_tx_inputs, psbt_inputs
882+ "Malformed PSBT, {unsigned_tx_inputs} unsigned tx inputs and {psbt_inputs} psbt inputs."
879883 ) ) ) ;
880884 }
881885 let sig_count = psbt. inputs . iter ( ) . fold ( 0 , |count, input| {
@@ -1022,12 +1026,30 @@ pub(crate) fn handle_compile_subcommand(
10221026 let segwit_policy: Miniscript < String , Segwitv0 > = policy
10231027 . compile ( )
10241028 . map_err ( |e| Error :: Generic ( e. to_string ( ) ) ) ?;
1029+ let taproot_policy: Miniscript < String , Tap > = policy
1030+ . compile ( )
1031+ . map_err ( |e| Error :: Generic ( e. to_string ( ) ) ) ?;
10251032
10261033 let descriptor = match script_type. as_str ( ) {
10271034 "sh" => Descriptor :: new_sh ( legacy_policy) ,
10281035 "wsh" => Descriptor :: new_wsh ( segwit_policy) ,
10291036 "sh-wsh" => Descriptor :: new_sh_wsh ( segwit_policy) ,
1030- _ => panic ! ( "Invalid type" ) ,
1037+ "tr" => {
1038+ // For tr descriptors, we use a well-known unspendable key (NUMS point).
1039+ // This ensures the key path is effectively disabled and only script path can be used.
1040+ // See https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs
1041+
1042+ let xonly_public_key = XOnlyPublicKey :: from_str ( NUMS_UNSPENDABLE_KEY_HEX )
1043+ . map_err ( |e| Error :: Generic ( format ! ( "Invalid NUMS key: {e}" ) ) ) ?;
1044+
1045+ let tree = TapTree :: Leaf ( Arc :: new ( taproot_policy) ) ;
1046+ Descriptor :: new_tr ( xonly_public_key. to_string ( ) , Some ( tree) )
1047+ }
1048+ _ => {
1049+ return Err ( Error :: Generic (
1050+ "Invalid script type. Supported types: sh, wsh, sh-wsh, tr" . to_string ( ) ,
1051+ ) ) ;
1052+ }
10311053 } ?;
10321054 if pretty {
10331055 let table = vec ! [ vec![
@@ -1331,21 +1353,20 @@ fn readline() -> Result<String, Error> {
13311353 Ok ( buffer)
13321354}
13331355
1334- #[ cfg( any(
1335- feature = "electrum" ,
1336- feature = "esplora" ,
1337- feature = "cbf" ,
1338- feature = "rpc"
1339- ) ) ]
13401356#[ cfg( test) ]
13411357mod test {
1342- use bdk_wallet:: bitcoin:: Psbt ;
1343-
1344- use super :: is_final;
1345- use std:: str:: FromStr ;
1346-
1358+ #[ cfg( any(
1359+ feature = "electrum" ,
1360+ feature = "esplora" ,
1361+ feature = "cbf" ,
1362+ feature = "rpc"
1363+ ) ) ]
13471364 #[ test]
13481365 fn test_psbt_is_final ( ) {
1366+ use super :: is_final;
1367+ use bdk_wallet:: bitcoin:: Psbt ;
1368+ use std:: str:: FromStr ;
1369+
13491370 let unsigned_psbt = Psbt :: from_str ( "cHNidP8BAIkBAAAAASWJHzxzyVORV/C3lAynKHVVL7+Rw7/Jj8U9fuvD24olAAAAAAD+////AiBOAAAAAAAAIgAgLzY9yE4jzTFJnHtTjkc+rFAtJ9NB7ENFQ1xLYoKsI1cfqgKVAAAAACIAIFsbWgDeLGU8EA+RGwBDIbcv4gaGG0tbEIhDvwXXa/E7LwEAAAABALUCAAAAAAEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////BALLAAD/////AgD5ApUAAAAAIgAgWxtaAN4sZTwQD5EbAEMhty/iBoYbS1sQiEO/Bddr8TsAAAAAAAAAACZqJKohqe3i9hw/cdHe/T+pmd+jaVN1XGkGiXmZYrSL69g2l06M+QEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQErAPkClQAAAAAiACBbG1oA3ixlPBAPkRsAQyG3L+IGhhtLWxCIQ78F12vxOwEFR1IhA/JV2U/0pXW+iP49QcsYilEvkZEd4phmDM8nV8wC+MeDIQLKhV/gEZYmlsQXnsL5/Uqv5Y8O31tmWW1LQqIBkiqzCVKuIgYCyoVf4BGWJpbEF57C+f1Kr+WPDt9bZlltS0KiAZIqswkEboH3lCIGA/JV2U/0pXW+iP49QcsYilEvkZEd4phmDM8nV8wC+MeDBDS6ZSEAACICAsqFX+ARliaWxBeewvn9Sq/ljw7fW2ZZbUtCogGSKrMJBG6B95QiAgPyVdlP9KV1voj+PUHLGIpRL5GRHeKYZgzPJ1fMAvjHgwQ0umUhAA==" ) . unwrap ( ) ;
13501371 assert ! ( is_final( & unsigned_psbt) . is_err( ) ) ;
13511372
@@ -1355,4 +1376,92 @@ mod test {
13551376 let full_signed_psbt = Psbt :: from_str ( "cHNidP8BAIkBAAAAASWJHzxzyVORV/C3lAynKHVVL7+Rw7/Jj8U9fuvD24olAAAAAAD+////AiBOAAAAAAAAIgAgLzY9yE4jzTFJnHtTjkc+rFAtJ9NB7ENFQ1xLYoKsI1cfqgKVAAAAACIAIFsbWgDeLGU8EA+RGwBDIbcv4gaGG0tbEIhDvwXXa/E7LwEAAAABALUCAAAAAAEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////BALLAAD/////AgD5ApUAAAAAIgAgWxtaAN4sZTwQD5EbAEMhty/iBoYbS1sQiEO/Bddr8TsAAAAAAAAAACZqJKohqe3i9hw/cdHe/T+pmd+jaVN1XGkGiXmZYrSL69g2l06M+QEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQErAPkClQAAAAAiACBbG1oA3ixlPBAPkRsAQyG3L+IGhhtLWxCIQ78F12vxOwEFR1IhA/JV2U/0pXW+iP49QcsYilEvkZEd4phmDM8nV8wC+MeDIQLKhV/gEZYmlsQXnsL5/Uqv5Y8O31tmWW1LQqIBkiqzCVKuIgYCyoVf4BGWJpbEF57C+f1Kr+WPDt9bZlltS0KiAZIqswkEboH3lCIGA/JV2U/0pXW+iP49QcsYilEvkZEd4phmDM8nV8wC+MeDBDS6ZSEBBwABCNsEAEgwRQIhAJzT6busDV9h12M/LNquZ17oOHFn7whg90kh9gjSpvshAiBEDu/1EYVD7BqJJzExPhq2CX/Vsap/ULLjfRRo99nEKQFHMEQCIGoFCvJ2zPB7PCpznh4+1jsY03kMie49KPoPDdr7/T9TAiB3jV7wzR9BH11FSbi+8U8gSX95PrBlnp1lOBgTUIUw3QFHUiED8lXZT/Sldb6I/j1ByxiKUS+RkR3imGYMzydXzAL4x4MhAsqFX+ARliaWxBeewvn9Sq/ljw7fW2ZZbUtCogGSKrMJUq4AACICAsqFX+ARliaWxBeewvn9Sq/ljw7fW2ZZbUtCogGSKrMJBG6B95QiAgPyVdlP9KV1voj+PUHLGIpRL5GRHeKYZgzPJ1fMAvjHgwQ0umUhAA==" ) . unwrap ( ) ;
13561377 assert ! ( is_final( & full_signed_psbt) . is_ok( ) ) ;
13571378 }
1379+
1380+ #[ cfg( feature = "compiler" ) ]
1381+ #[ test]
1382+ fn test_compile_taproot ( ) {
1383+ use super :: { NUMS_UNSPENDABLE_KEY_HEX , handle_compile_subcommand} ;
1384+ use bdk_wallet:: bitcoin:: Network ;
1385+
1386+ // Expected taproot descriptors with checksums (using NUMS key from constant)
1387+ let expected_pk_a = format ! ( "tr({},pk(A))#a2mlskt0" , NUMS_UNSPENDABLE_KEY_HEX ) ;
1388+ let expected_and_ab = format ! (
1389+ "tr({},and_v(v:pk(A),pk(B)))#sfplm6kv" ,
1390+ NUMS_UNSPENDABLE_KEY_HEX
1391+ ) ;
1392+
1393+ // Test simple pk policy compilation to taproot
1394+ let result = handle_compile_subcommand (
1395+ Network :: Testnet ,
1396+ "pk(A)" . to_string ( ) ,
1397+ "tr" . to_string ( ) ,
1398+ false ,
1399+ ) ;
1400+ assert ! ( result. is_ok( ) ) ;
1401+ let json_string = result. unwrap ( ) ;
1402+ let json_result: serde_json:: Value = serde_json:: from_str ( & json_string) . unwrap ( ) ;
1403+ let descriptor = json_result. get ( "descriptor" ) . unwrap ( ) . as_str ( ) . unwrap ( ) ;
1404+ assert_eq ! ( descriptor, expected_pk_a) ;
1405+
1406+ // Test more complex policy
1407+ let result = handle_compile_subcommand (
1408+ Network :: Testnet ,
1409+ "and(pk(A),pk(B))" . to_string ( ) ,
1410+ "tr" . to_string ( ) ,
1411+ false ,
1412+ ) ;
1413+ assert ! ( result. is_ok( ) ) ;
1414+ let json_string = result. unwrap ( ) ;
1415+ let json_result: serde_json:: Value = serde_json:: from_str ( & json_string) . unwrap ( ) ;
1416+ let descriptor = json_result. get ( "descriptor" ) . unwrap ( ) . as_str ( ) . unwrap ( ) ;
1417+ assert_eq ! ( descriptor, expected_and_ab) ;
1418+ }
1419+
1420+ #[ cfg( feature = "compiler" ) ]
1421+ #[ test]
1422+ fn test_compile_invalid_cases ( ) {
1423+ use super :: handle_compile_subcommand;
1424+ use bdk_wallet:: bitcoin:: Network ;
1425+
1426+ // Test invalid policy syntax
1427+ let result = handle_compile_subcommand (
1428+ Network :: Testnet ,
1429+ "invalid_policy" . to_string ( ) ,
1430+ "tr" . to_string ( ) ,
1431+ false ,
1432+ ) ;
1433+ assert ! ( result. is_err( ) ) ;
1434+
1435+ // Test invalid script type
1436+ let result = handle_compile_subcommand (
1437+ Network :: Testnet ,
1438+ "pk(A)" . to_string ( ) ,
1439+ "invalid_type" . to_string ( ) ,
1440+ false ,
1441+ ) ;
1442+ assert ! ( result. is_err( ) ) ;
1443+
1444+ // Test empty policy
1445+ let result =
1446+ handle_compile_subcommand ( Network :: Testnet , "" . to_string ( ) , "tr" . to_string ( ) , false ) ;
1447+ assert ! ( result. is_err( ) ) ;
1448+
1449+ // Test malformed policy with unmatched parentheses
1450+ let result = handle_compile_subcommand (
1451+ Network :: Testnet ,
1452+ "pk(A" . to_string ( ) ,
1453+ "tr" . to_string ( ) ,
1454+ false ,
1455+ ) ;
1456+ assert ! ( result. is_err( ) ) ;
1457+
1458+ // Test policy with unknown function
1459+ let result = handle_compile_subcommand (
1460+ Network :: Testnet ,
1461+ "unknown_func(A)" . to_string ( ) ,
1462+ "tr" . to_string ( ) ,
1463+ false ,
1464+ ) ;
1465+ assert ! ( result. is_err( ) ) ;
1466+ }
13581467}
0 commit comments