Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions lwk_app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,9 @@ fn inner_method_handler(request: Request, state: Arc<Mutex<State>>) -> Result<Re
DescriptorType::ShWpkh => {
jade.get_receive_address_single(Variant::ShWpkh, full_path)?
}
DescriptorType::Tr => {
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.

Drop all changes that are not in lwk_wollet lwk_common or lwk_signer.
First we do the internal then (separate MR) we expose that in bindings/wasm/app/cli.
If hww supports it, we can add it there, but we need test coverage there.

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.

Done — removed both lwk_app changes (address generation arm and wallet type detection). Internal implementation in lwk_wollet, lwk_common, and lwk_signer is unchanged. Will open a follow-up MR for the app/bindings/CLI exposure with test coverage.

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.

Drop all changes that are not in lwk_wollet lwk_common or lwk_signer.
Done

most of them are still here.

Am I talking to Claude?

jade.get_receive_address_single(Variant::Taproot, full_path)?
}
_ => {
return Err(Error::Generic(
"Unsupported signer or descriptor".into(),
Expand Down Expand Up @@ -753,6 +756,7 @@ fn inner_method_handler(request: Request, state: Arc<Mutex<State>>) -> Result<Re
let type_ = match wollet.descriptor()?.descriptor.desc_type() {
DescriptorType::Wpkh => response::WalletType::Wpkh,
DescriptorType::ShWpkh => response::WalletType::ShWpkh,
DescriptorType::Tr => response::WalletType::Taproot,
_ => match &wollet.descriptor()?.descriptor {
Descriptor::Wsh(wsh) => match wsh.as_inner() {
WshInner::Ms(ms) => match &ms.node {
Expand Down
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