From aa417be12781f995854dc2f4fc49da85d23569f2 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Fri, 23 May 2025 12:48:15 +0200 Subject: [PATCH] gui: add parsing for xpub from coldcard format --- liana-gui/src/export.rs | 125 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 114 insertions(+), 11 deletions(-) diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index cf2253e0..88a910f9 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -655,25 +655,52 @@ pub async fn import_xpub( file.read_to_string(&mut xpub_str)?; let xpub_str = xpub_str.trim().to_string(); - if let Ok(DescriptorPublicKey::XPub(key)) = DescriptorPublicKey::from_str(&xpub_str) { - let valid = if network == Network::Bitcoin { - key.xkey.network == Network::Bitcoin.into() + let (descriptor_pubkey, key) = + if let Some(DescriptorPublicKey::XPub(key)) = parse_raw_xpub(&xpub_str) { + (DescriptorPublicKey::XPub(key.clone()), key) + } else if let Some(DescriptorPublicKey::XPub(key)) = parse_coldcard_xpub(&xpub_str) { + (DescriptorPublicKey::XPub(key.clone()), key) } else { - key.xkey.network == Network::Testnet.into() + return Err(Error::ParseXpub); }; - if valid { - send_progress!(sender, Progress(100.0)); - send_progress!(sender, Xpub(xpub_str)); - } else { - return Err(Error::XpubNetwork); - } + let xpub_str = descriptor_pubkey.to_string(); + + let valid = if network == Network::Bitcoin { + key.xkey.network == Network::Bitcoin.into() } else { - return Err(Error::ParseXpub); + key.xkey.network == Network::Testnet.into() + }; + if valid { + send_progress!(sender, Progress(100.0)); + send_progress!(sender, Xpub(xpub_str)); + } else { + return Err(Error::XpubNetwork); } Ok(()) } +pub fn parse_raw_xpub(raw_xpub: &str) -> Option { + DescriptorPublicKey::from_str(raw_xpub).ok() +} + +pub fn parse_coldcard_xpub(coldcard_xpub: &str) -> Option { + if let serde_json::Value::Object(map) = serde_json::from_str(coldcard_xpub).ok()? { + let fg = map.get("xfp")?.to_string().to_lowercase(); + let fg = fg.replace("\"", ""); + if let serde_json::Value::Object(bip48) = map.get("bip48_2")? { + let deriv = bip48.get("deriv")?.to_string(); + let deriv = deriv.replace("\"", ""); + let deriv = deriv.replace("m", ""); + let xpub = bip48.get("xpub")?.to_string(); + let xpub = xpub.replace("\"", ""); + let raw_xpub = format!("[{fg}{deriv}]{xpub}"); + return parse_raw_xpub(&raw_xpub); + } + } + None +} + /// Import a backup in an already existing wallet: /// - Load backup from file /// - check if networks matches @@ -1244,3 +1271,79 @@ pub async fn app_backup_export( .map_err(Error::Backup)?; export_string(sender, path, backup).await } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_coldcard_xpub() { + let raw = r#" + { + "chain": "XTN", + "xfp": "C658B283", + "account": 3, + "xpub": "tpubD6NzVbkrYhZ4XHQ1pLJ7pdpEGWCVbSUEaUakxnrtENzaZaDp4vL6gBgGH7n983ZPgsVe5G2JEAM2oYZkEPCNrfo9XLq8nHFhp9GzFjGc1uQ", + "bip44": { + "name": "p2pkh", + "xfp": "F623F3D0", + "deriv": "m/44h/1h/3h", + "xpub": "tpubDCrmGPwVjNJsUDsh7pxsWgTZ1sjFZtnPPhpgCxM3yXg6RXjfDQ73g6mX6H2Hn69j5S5MJnhEr7mSvTzaz7qcrYzyZK7Aw836Qwkj1brgDh8", + "desc": "pkh([c658b283/44h/1h/3h]tpubDCrmGPwVjNJsUDsh7pxsWgTZ1sjFZtnPPhpgCxM3yXg6RXjfDQ73g6mX6H2Hn69j5S5MJnhEr7mSvTzaz7qcrYzyZK7Aw836Qwkj1brgDh8/<0;1>/*)#2w5s7qf5", + "first": "miu3fgGrAZqtPb6iWwUD1pi7MRqWnkVTeG" + }, + "bip49": { + "name": "p2sh-p2wpkh", + "xfp": "1226F685", + "deriv": "m/49h/1h/3h", + "xpub": "tpubDDMPh7VRRQ7waHhLDskFU63ZY4Pdue8vPLhim4Nf34nX8KpFZ4yPt5wDtwuQQ79jn7AvpGBCVreVdhPvJCqCSi5zznCwZ61YYnLdGBmn3As", + "desc": "sh(wpkh([c658b283/49h/1h/3h]tpubDDMPh7VRRQ7waHhLDskFU63ZY4Pdue8vPLhim4Nf34nX8KpFZ4yPt5wDtwuQQ79jn7AvpGBCVreVdhPvJCqCSi5zznCwZ61YYnLdGBmn3As/<0;1>/*))#3c4vfpj8", + "_pub": "upub5EUyFsezG5Y3kbw8GcQHduRgh2re6PfN6NYm4KdrZ8tzDjhrisdcsTHybHwrstEjmHHbxFseamkoHf4ckFjvAZauLANN7ptr9eZHLRHAtJz", + "first": "2MueqD2UoZZ566mLqTVCVT5Dm7YMbVBwPeq" + }, + "bip84": { + "name": "p2wpkh", + "xfp": "B74B1EF5", + "deriv": "m/84h/1h/3h", + "xpub": "tpubDDKQtgKtTeTVebMfJ6RJ6vL7UMnDjhUfK7scrYiWGMWy8htipN9dCkuHqx9PmJoAUoydwsc9TEoj3A1C1FbPqzxKth8qfn7axA5qHc5YbJz", + "desc": "wpkh([c658b283/84h/1h/3h]tpubDDKQtgKtTeTVebMfJ6RJ6vL7UMnDjhUfK7scrYiWGMWy8htipN9dCkuHqx9PmJoAUoydwsc9TEoj3A1C1FbPqzxKth8qfn7axA5qHc5YbJz/<0;1>/*)#0ac7rpv0", + "_pub": "vpub5ZHFm7ANT1R5gCnaBBrxUpojoJPfs4zbwGEswCsbAS1KHDbZEpyQpBvBZW9SEzY5sdD7qLu9zpGaaQHTAzv8N68q6QzgpRpNpkN8kStaFVA", + "first": "tb1qt8l4mel8c8epzcrqchmrsdsv6e8n0chkynuxzz" + }, + "bip86": { + "name": "p2tr", + "xfp": "99B6CEE8", + "deriv": "m/86h/1h/3h", + "xpub": "tpubDDNzAa2tRWaaiDVf6qnzMYjELyz68DrBzGW6PtsZkWz3tU4QZLUhB9TSxxT4KF4sXncg856etJ1rDM2XHibm21uCxQtLjMd4aR9EXydtEpY", + "desc": "tr([c658b283/86h/1h/3h]tpubDDNzAa2tRWaaiDVf6qnzMYjELyz68DrBzGW6PtsZkWz3tU4QZLUhB9TSxxT4KF4sXncg856etJ1rDM2XHibm21uCxQtLjMd4aR9EXydtEpY/<0;1>/*)#ggndxtk6", + "first": "tb1pcawjnx5krtffagyzvcmqz40z3hds3nycc2vtjj9ngy8hskk5zwzsfh2a3w" + }, + "bip48_1": { + "name": "p2sh-p2wsh", + "xfp": "141AB091", + "deriv": "m/48h/1h/3h/1h", + "xpub": "tpubDFmeRMxr4X7dtY9C9H6gAnVBfzBfjrmJ961ST2STHfQQwrHWMtEU1Zgr2PUfQQL4q9uywHxDJcffmRRpL58RJeSuaVs5CYzrcBrMoobyVRH", + "desc": "sh(wsh(sortedmulti(M,[c658b283/48h/1h/3h/1h]tpubDFmeRMxr4X7dtY9C9H6gAnVBfzBfjrmJ961ST2STHfQQwrHWMtEU1Zgr2PUfQQL4q9uywHxDJcffmRRpL58RJeSuaVs5CYzrcBrMoobyVRH/0/*,...)))", + "_pub": "Upub5ToK7MrrUA67VRYN8gDhAgD7Ykgw8xyLAPW9fYyCBWMHfSk2J6Gy63uXXSUbScdy3o6dwsenGkAUYYiH5MC6Az4UkM8uAhMA6nLtTzhps22" + }, + "bip48_2": { + "name": "p2wsh", + "xfp": "88AD98C4", + "deriv": "m/48h/1h/3h/2h", + "xpub": "tpubDFmeRMxr4X7dxNKxxBKWXu1rskHEQYB8vY5PYPmiB74EjyrE814HHpQzh2XEFpm3z5uJpk7Cjt2hmhcMYmBbot6CmRHn3CKK2K6vzLPBMbH", + "desc": "wsh(sortedmulti(M,[c658b283/48h/1h/3h/2h]tpubDFmeRMxr4X7dxNKxxBKWXu1rskHEQYB8vY5PYPmiB74EjyrE814HHpQzh2XEFpm3z5uJpk7Cjt2hmhcMYmBbot6CmRHn3CKK2K6vzLPBMbH/0/*,...))", + "_pub": "Vpub5ndaR2XmcqdbQYvFmwE9jsqHvUvwkGNfrx6KYKCLSxNzWg7yJsGLzNHpDHUkHwiscNCmaoQLAft4S7WP1jfHUTPNocG2bFV6ndf736mPM9R" + }, + "bip48_3": { + "name": "p2tr", + "xfp": "C3F84B2C", + "deriv": "m/48h/1h/3h/3h", + "xpub": "tpubDFmeRMxr4X7e1LErDJWLDRjrHGfirhnFxk3aZoFe8tjMHUyPjh1mATfLfyC6VKUfuS4uBEhMuow7kQWfNqA7U2uHz7fyT9S6V49MLQmyzjm", + "desc": "tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,sortedmulti_a(M,[c658b283/48h/1h/3h/3h]tpubDFmeRMxr4X7e1LErDJWLDRjrHGfirhnFxk3aZoFe8tjMHUyPjh1mATfLfyC6VKUfuS4uBEhMuow7kQWfNqA7U2uHz7fyT9S6V49MLQmyzjm/0/*,...))" + } + } + "#; + let expected = "[c658b283/48'/1'/3'/2']tpubDFmeRMxr4X7dxNKxxBKWXu1rskHEQYB8vY5PYPmiB74EjyrE814HHpQzh2XEFpm3z5uJpk7Cjt2hmhcMYmBbot6CmRHn3CKK2K6vzLPBMbH".to_string(); + assert_eq!(expected, parse_coldcard_xpub(raw).unwrap().to_string()); + } +}