Skip to content
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: 2 additions & 0 deletions lwk_bindings/src/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ use std::sync::Arc;
pub enum Singlesig {
Wpkh,
ShWpkh,
Taproot,
}

impl From<Singlesig> for lwk_common::Singlesig {
fn from(singlesig: Singlesig) -> Self {
match singlesig {
Singlesig::Wpkh => lwk_common::Singlesig::Wpkh,
Singlesig::ShWpkh => lwk_common::Singlesig::ShWpkh,
Singlesig::Taproot => lwk_common::Singlesig::Taproot,
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions lwk_cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,13 +408,15 @@ impl Display for BlindingKeyKind {
pub enum SinglesigKind {
Wpkh,
Shwpkh,
Taproot,
}

impl Display for SinglesigKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SinglesigKind::Wpkh => write!(f, "wpkh"),
SinglesigKind::Shwpkh => write!(f, "shwpkh"),
SinglesigKind::Taproot => write!(f, "taproot"),
}
}
}
Expand Down
22 changes: 21 additions & 1 deletion lwk_common/src/descriptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub fn singlesig_desc<S: Signer + ?Sized>(
let (prefix, path, suffix) = match script_variant {
Singlesig::Wpkh => ("elwpkh", format!("84h/{coin_type}h/0h"), ""),
Singlesig::ShWpkh => ("elsh(wpkh", format!("49h/{coin_type}h/0h"), ")"),
Singlesig::Taproot => ("eltr", format!("86h/{coin_type}h/0h"), ""),
};

let fingerprint = signer.fingerprint().map_err(|e| format!("{e:?}"))?;
Expand Down Expand Up @@ -108,11 +109,14 @@ pub enum Singlesig {

/// Witness public key hash wrapped in script hash as defined by bip49
ShWpkh,

/// Taproot as defined by bip86
Taproot,
}

/// The error type returned by Singlesig::from_str
#[derive(Error, Debug)]
#[error("Invalid singlesig variant '{0}' supported variant are: 'wpkh', 'shwpkh'")]
#[error("Invalid singlesig variant '{0}', supported variants are: 'wpkh', 'shwpkh', 'taproot'")]
pub struct InvalidSinglesigVariant(String);

impl FromStr for Singlesig {
Expand All @@ -122,6 +126,7 @@ impl FromStr for Singlesig {
Ok(match s {
"wpkh" => Singlesig::Wpkh,
"shwpkh" => Singlesig::ShWpkh,
"taproot" => Singlesig::Taproot,
v => return Err(InvalidSinglesigVariant(v.to_string())),
})
}
Expand Down Expand Up @@ -248,4 +253,19 @@ mod test {
}
Bip::from_str("vattelapesca").unwrap_err();
}

#[test]
fn singlesig_from_str() {
use super::Singlesig;
assert!(matches!(Singlesig::from_str("wpkh"), Ok(Singlesig::Wpkh)));
assert!(matches!(
Singlesig::from_str("shwpkh"),
Ok(Singlesig::ShWpkh)
));
assert!(matches!(
Singlesig::from_str("taproot"),
Ok(Singlesig::Taproot)
));
Singlesig::from_str("invalid").unwrap_err();
}
}
4 changes: 4 additions & 0 deletions lwk_jade/src/get_receive_address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ pub enum Variant {
/// Script hash, Witness public key hash AKA nested segwit, BIP49
#[serde(rename = "sh(wpkh(k))")]
ShWpkh,

/// Taproot, BIP86
#[serde(rename = "tr(k)")]
Taproot,
}

#[derive(Debug, PartialEq, Eq)]
Expand Down
13 changes: 12 additions & 1 deletion lwk_ledger/src/asyncr/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ impl<T: Transport> LiquidClient<T> {
let purpose = match variant {
lwk_common::Singlesig::Wpkh => 84,
lwk_common::Singlesig::ShWpkh => 49,
lwk_common::Singlesig::Taproot => 86,
};
let path = format!("m/{purpose}h/{coin_type}h/0h")
.parse()
Expand All @@ -281,7 +282,17 @@ impl<T: Transport> LiquidClient<T> {
.map_err(|e| map_str_err(e, "Failed to get master blinding key"))?;
let wpk0 = WalletPubKey::from(((fingerprint, path), xpub));
let ss_keys = vec![wpk0];
let desc = format!("ct(slip77({master_blinding_key}),wpkh(@0/**))");
let desc = match variant {
lwk_common::Singlesig::Wpkh => {
format!("ct(slip77({master_blinding_key}),wpkh(@0/**))")
}
lwk_common::Singlesig::ShWpkh => {
format!("ct(slip77({master_blinding_key}),sh(wpkh(@0/**)))")
}
lwk_common::Singlesig::Taproot => {
format!("ct(slip77({master_blinding_key}),tr(@0/**))")
}
};
let ss = WalletPolicy::new("".to_string(), version, desc, ss_keys.clone());
let address = self
.get_wallet_address(
Expand Down
4 changes: 4 additions & 0 deletions lwk_rpc_model/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,9 @@ pub enum WalletType {
/// Script hash Witness pay to public key hash (nested segwit)
ShWpkh,

/// Taproot
Taproot,

/// Witnes script hash, multisig N of M
WshMulti(usize, usize),
}
Expand Down Expand Up @@ -480,6 +483,7 @@ impl std::fmt::Display for WalletType {
WalletType::Unknown => write!(f, "unknown"),
WalletType::Wpkh => write!(f, "wpkh"),
WalletType::ShWpkh => write!(f, "sh_wpkh"),
WalletType::Taproot => write!(f, "taproot"),
WalletType::WshMulti(threshold, num_pubkeys) => {
write!(f, "wsh_multi_{threshold}of{num_pubkeys}")
}
Expand Down
8 changes: 6 additions & 2 deletions lwk_wasm/src/jade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ impl Jade {
let network = self.inner.network();
let mut paths = HashMap::new();

for purpose in [49, 84, 87] {
for purpose in [49, 84, 86, 87] {
for coin_type in [1, 1776] {
let derivation_path_str = format!("m/{purpose}h/{coin_type}h/0h");
let derivation_path = DerivationPath::from_str(&derivation_path_str)?;
Expand Down Expand Up @@ -245,6 +245,7 @@ impl From<Singlesig> for Variant {
match v.inner {
lwk_common::Singlesig::Wpkh => Variant::Wpkh,
lwk_common::Singlesig::ShWpkh => Variant::ShWpkh,
lwk_common::Singlesig::Taproot => Variant::Taproot,
}
}
}
Expand All @@ -259,8 +260,11 @@ impl Singlesig {
"ShWpkh" => Ok(Singlesig {
inner: lwk_common::Singlesig::ShWpkh,
}),
"Taproot" => Ok(Singlesig {
inner: lwk_common::Singlesig::Taproot,
}),
_ => Err(Error::Generic(
"Unsupported variant, possible values are: Wpkh and ShWpkh".to_string(),
"Unsupported variant, possible values are: Wpkh, ShWpkh and Taproot".to_string(),
)),
}
}
Expand Down
2 changes: 1 addition & 1 deletion lwk_wasm/src/jade_websocket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ impl JadeWebSocket {
let network = self.inner.network();
let mut paths = HashMap::new();

for purpose in [49, 84, 87] {
for purpose in [49, 84, 86, 87] {
for coin_type in [1, 1776] {
let derivation_path_str = format!("m/{purpose}h/{coin_type}h/0h");
let derivation_path = DerivationPath::from_str(&derivation_path_str)?;
Expand Down
25 changes: 25 additions & 0 deletions lwk_wollet/src/wollet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1723,6 +1723,31 @@ mod tests {
}
}

#[test]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Write a e2e test (in lwk_wollet/src) that receives, check balance and sends using that descriptor.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added e2e test in lwk_wollet/tests/tr.rstest_taproot_singlesig_receive_balance_send covers receive (1M sats, exact balance assert), and send (via send_btc, balance decreases by fee only). Follows the same TestEnvBuilder/TestWollet pattern as existing integration tests.

fn taproot_singlesig_desc_format() {
let mnemonic = lwk_test_util::TEST_MNEMONIC;

for is_mainnet in [false, true] {
let signer = SwSigner::new(mnemonic, is_mainnet).unwrap();
for blinding_variant in [
DescriptorBlindingKey::Slip77,
DescriptorBlindingKey::Elip151,
] {
let desc_str =
singlesig_desc(&signer, Singlesig::Taproot, blinding_variant).unwrap();
assert!(desc_str.contains("eltr("), "desc: {desc_str}");
assert!(desc_str.contains("86h/"), "desc: {desc_str}");
assert!(desc_str.contains("/<0;1>/*)"), "desc: {desc_str}");

let expected_coin = if is_mainnet { "1776h" } else { "1h" };
assert!(desc_str.contains(expected_coin), "desc: {desc_str}");

// Verify the descriptor parses into a valid WolletDescriptor
let _: WolletDescriptor = desc_str.parse().unwrap();
}
}
}

#[test]
fn test_wollet_status() {
let bytes = lwk_test_util::update_test_vector_bytes();
Expand Down
37 changes: 36 additions & 1 deletion lwk_wollet/tests/tr.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::test_wollet::*;
use lwk_common::Signer;
use lwk_common::{singlesig_desc, DescriptorBlindingKey, Signer, Singlesig};
use lwk_signer::{AnySigner, SwSigner};
use lwk_test_util::*;
use lwk_wollet::{ElementsNetwork, WolletBuilder, WolletDescriptor};

Expand Down Expand Up @@ -79,3 +80,37 @@ async fn test_single_address_tr_async() {
let utxos = wollet.utxos().unwrap();
assert_eq!(utxos.len(), 1);
}

#[test]
fn test_taproot_singlesig_receive_balance_send() {
let env = TestEnvBuilder::from_env().with_electrum().build();

let signer = SwSigner::new(TEST_MNEMONIC, false).unwrap();
let desc_str =
singlesig_desc(&signer, Singlesig::Taproot, DescriptorBlindingKey::Slip77).unwrap();
let client = test_client_electrum(&env.electrum_url());
let mut wallet = TestWollet::new(client, &desc_str);

// 1. Receive
let fund_sat = 1_000_000;
wallet.fund(
&env,
fund_sat,
Some(wallet.address()),
None,
);

// 2. Check balance
let balance = wallet.balance_btc();
assert_eq!(balance, fund_sat);

// 3. Send
let signers: [&AnySigner; 1] = [&AnySigner::Software(signer)];
wallet.send_btc(&signers, None, None);

// send_btc(None, None) sends 10_000 sat to self, so balance drops by fee only
let balance_after = wallet.balance_btc();
assert!(balance_after < balance, "balance should decrease by fee");
// 1_000 sat is a generous upper bound for a regtest fee
assert!(balance_after > balance - 1_000, "fee should be reasonable");
}
Loading