Skip to content

Commit d60a1ad

Browse files
committed
-x importwallet
1 parent 86bd507 commit d60a1ad

File tree

3 files changed

+158
-16
lines changed

3 files changed

+158
-16
lines changed

client/src/bin/space-cli.rs

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ use spaces_wallet::{
4242
nostr::{NostrEvent, NostrTag},
4343
Listing,
4444
};
45+
use hex;
4546

4647
#[derive(Parser, Debug)]
4748
#[command(version, about, long_about = None)]
@@ -107,6 +108,9 @@ enum Commands {
107108
ImportWallet {
108109
// Wallet json file to import
109110
path: PathBuf,
111+
/// Use hex-encoded private key from json to generate taproot descriptor
112+
#[arg(long, short = 'x')]
113+
hex_secret: bool,
110114
},
111115
/// Export a wallet
112116
#[command(name = "getwalletinfo")]
@@ -539,6 +543,26 @@ fn normalize_space(space: &str) -> String {
539543
}
540544
}
541545

546+
fn generate_taproot_descriptor_from_hex(hex_secret: &str, _network: ExtendedNetwork) -> Result<String, ClientError> {
547+
// Parse hex secret
548+
let secret_bytes = hex::decode(hex_secret)
549+
.map_err(|e| ClientError::Custom(format!("Invalid hex secret: {}", e)))?;
550+
551+
if secret_bytes.len() != 32 {
552+
return Err(ClientError::Custom("Hex secret must be 32 bytes (64 hex characters)".to_string()));
553+
}
554+
555+
// For now, create a simple descriptor that indicates this is a hex-based import
556+
// The actual xprv conversion would need to be done properly in the wallet creation
557+
// This is a placeholder that will need to be handled by the wallet creation process
558+
let descriptor = format!(
559+
"tr([hex:{}]/86'/0'/0'/0/*)",
560+
hex_secret
561+
);
562+
563+
Ok(descriptor)
564+
}
565+
542566
#[tokio::main]
543567
async fn main() -> anyhow::Result<()> {
544568
let (cli, args) = SpaceCli::configure().await?;
@@ -632,10 +656,28 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client
632656
Commands::LoadWallet => {
633657
cli.client.wallet_load(&cli.wallet).await?;
634658
}
635-
Commands::ImportWallet { path } => {
659+
Commands::ImportWallet { path, hex_secret } => {
636660
let content =
637661
fs::read_to_string(path).map_err(|e| ClientError::Custom(e.to_string()))?;
638-
let wallet: WalletExport = serde_json::from_str(&content)?;
662+
let mut wallet: WalletExport = serde_json::from_str(&content)?;
663+
664+
// If hex_secret is requested, generate taproot descriptor from hex secret
665+
if hex_secret {
666+
if let Some(hex_secret_value) = &wallet.hex_secret {
667+
// For now, we'll create a simple descriptor format
668+
// The actual conversion to xprv should be done in the wallet creation process
669+
let taproot_descriptor = format!("tr([hex:{}]/86'/0'/0'/0/*)", hex_secret_value);
670+
wallet.descriptor = Some(taproot_descriptor);
671+
} else {
672+
return Err(ClientError::Custom("hex-secret option specified but no hex_secret found in wallet json".to_string()));
673+
}
674+
} else {
675+
// If not using hex_secret, ensure descriptor is present
676+
if wallet.descriptor.is_none() {
677+
return Err(ClientError::Custom("descriptor field is required when not using hex-secret option".to_string()));
678+
}
679+
}
680+
639681
cli.client.wallet_import(wallet).await?;
640682
}
641683
Commands::ExportWallet { path, hex_secret } => {

client/src/rpc.rs

Lines changed: 102 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -509,8 +509,10 @@ impl WalletManager {
509509
// If hex_secret is requested, we need to extract it from the wallet descriptor
510510
if hex_secret {
511511
// Parse the descriptor to extract the xprv and get the hex secret
512-
if let Some(hex_secret_value) = self.extract_hex_secret_from_descriptor(&export.descriptor)? {
513-
export.hex_secret = Some(hex_secret_value);
512+
if let Some(descriptor) = &export.descriptor {
513+
if let Some(hex_secret_value) = self.extract_hex_secret_from_descriptor(descriptor)? {
514+
export.hex_secret = Some(hex_secret_value);
515+
}
514516
}
515517
}
516518

@@ -650,11 +652,55 @@ impl WalletManager {
650652
name: name.to_string(),
651653
network,
652654
genesis_hash,
653-
space_descriptors: WalletDescriptors {
654-
external: export.descriptor(),
655-
internal: export
656-
.change_descriptor()
657-
.expect("expected a change descriptor"),
655+
space_descriptors: {
656+
let external_descriptor = export.descriptor().expect("expected a descriptor");
657+
let internal_descriptor = export.change_descriptor().expect("expected a change descriptor");
658+
659+
// Check if this is a hex-based descriptor and convert it
660+
let (external, internal) = if external_descriptor.starts_with("tr([hex:") {
661+
// Extract hex secret from descriptor
662+
if let Some(hex_secret) = export.hex_secret.as_ref() {
663+
// Convert hex secret to proper xprv and create descriptors
664+
let xpriv = Self::xpriv_from_hex_secret(network, hex_secret)?;
665+
let (external_desc, internal_desc) = Self::default_descriptors(xpriv);
666+
667+
// Create a temporary wallet to get the descriptor strings
668+
let temp_wallet = bdk::Wallet::create(external_desc, internal_desc)
669+
.network(network)
670+
.create_wallet_no_persist()?;
671+
672+
// Get descriptor strings using the same method as WalletExport
673+
let external_str = temp_wallet
674+
.public_descriptor(KeychainKind::External)
675+
.to_string_with_secret(
676+
&temp_wallet
677+
.get_signers(KeychainKind::External)
678+
.as_key_map(temp_wallet.secp_ctx()),
679+
);
680+
let internal_str = temp_wallet
681+
.public_descriptor(KeychainKind::Internal)
682+
.to_string_with_secret(
683+
&temp_wallet
684+
.get_signers(KeychainKind::Internal)
685+
.as_key_map(temp_wallet.secp_ctx()),
686+
);
687+
688+
// Remove checksums (same as WalletExport does)
689+
let external_str = Self::remove_checksum(external_str);
690+
let internal_str = Self::remove_checksum(internal_str);
691+
692+
(external_str, internal_str)
693+
} else {
694+
return Err(anyhow!("Hex-based descriptor found but no hex_secret in export"));
695+
}
696+
} else {
697+
(external_descriptor, internal_descriptor)
698+
};
699+
700+
WalletDescriptors {
701+
external,
702+
internal,
703+
}
658704
},
659705
};
660706

@@ -692,13 +738,57 @@ impl WalletManager {
692738
Ok(xkey.into_xprv(network).expect("xpriv"))
693739
}
694740

741+
fn xpriv_from_hex_secret(network: Network, hex_secret: &str) -> anyhow::Result<Xpriv> {
742+
use spaces_protocol::bitcoin::bip32::Xpriv;
743+
use spaces_protocol::bitcoin::key::Secp256k1;
744+
use spaces_protocol::bitcoin::secp256k1::SecretKey;
745+
746+
// Parse hex secret
747+
let secret_bytes = hex::decode(hex_secret)
748+
.map_err(|e| anyhow!("Invalid hex secret: {}", e))?;
749+
750+
if secret_bytes.len() != 32 {
751+
return Err(anyhow!("Hex secret must be 32 bytes (64 hex characters)"));
752+
}
753+
754+
// Convert to SecretKey
755+
let secret_key = SecretKey::from_slice(&secret_bytes)
756+
.map_err(|e| anyhow!("Invalid secret key: {}", e))?;
757+
758+
// Create Xpriv from secret key
759+
let secp = Secp256k1::new();
760+
let xpriv = Xpriv::new_master(network, &secret_key.secret_bytes())
761+
.map_err(|e| anyhow!("Failed to create xpriv: {}", e))?;
762+
763+
Ok(xpriv)
764+
}
765+
766+
fn descriptor_from_hex_secret(network: Network, hex_secret: &str) -> anyhow::Result<String> {
767+
// Parse hex secret
768+
let secret_bytes = hex::decode(hex_secret)
769+
.map_err(|e| anyhow!("Invalid hex secret: {}", e))?;
770+
771+
if secret_bytes.len() != 32 {
772+
return Err(anyhow!("Hex secret must be 32 bytes (64 hex characters)"));
773+
}
774+
775+
// For now, create a simple descriptor that can be processed by the wallet creation
776+
// The actual xprv conversion will need to be handled in the wallet creation process
777+
// This is a placeholder format that indicates this is a hex-based import
778+
Ok(format!("tr([hex:{}]/86'/0'/0'/0/*)", hex_secret))
779+
}
780+
695781
fn default_descriptors(x: Xpriv) -> (Bip86<Xpriv>, Bip86<Xpriv>) {
696782
(
697783
Bip86(x, KeychainKind::External),
698784
Bip86(x, KeychainKind::Internal),
699785
)
700786
}
701787

788+
fn remove_checksum(s: String) -> String {
789+
s.split_once('#').map(|(a, _)| String::from(a)).unwrap_or(s)
790+
}
791+
702792
fn extract_hex_secret_from_descriptor(&self, descriptor: &str) -> anyhow::Result<Option<String>> {
703793
// Parse the descriptor to extract the xprv
704794
// The descriptor format is typically: tr([xprv...]/path)...
@@ -722,6 +812,11 @@ impl WalletManager {
722812

723813
Ok(None)
724814
}
815+
816+
pub fn create_taproot_descriptor_from_hex(&self, hex_secret: &str) -> anyhow::Result<String> {
817+
let (network, _) = self.fallback_network();
818+
Self::descriptor_from_hex_secret(network, hex_secret)
819+
}
725820
}
726821

727822
impl RpcServerImpl {

wallet/src/export.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ use hex;
2121
/// For a usage example see [this module](crate::wallet::export)'s documentation.
2222
#[derive(Clone, Debug, Serialize, Deserialize)]
2323
pub struct WalletExport {
24-
pub descriptor: String,
24+
/// Wallet descriptor (optional when using hex-secret import)
25+
#[serde(skip_serializing_if = "Option::is_none")]
26+
pub descriptor: Option<String>,
2527
/// Earliest block to rescan when looking for the wallet's transactions
2628
pub blockheight: u32,
2729
/// Arbitrary label for the wallet
@@ -95,7 +97,7 @@ impl WalletExport {
9597
};
9698

9799
let export = WalletExport {
98-
descriptor,
100+
descriptor: Some(descriptor),
99101
label: label.into(),
100102
blockheight,
101103
hex_secret,
@@ -120,16 +122,19 @@ impl WalletExport {
120122
}
121123

122124
/// Return the external descriptor
123-
pub fn descriptor(&self) -> String {
125+
pub fn descriptor(&self) -> Option<String> {
124126
self.descriptor.clone()
125127
}
126128

127129
/// Return the internal descriptor, if present
128130
pub fn change_descriptor(&self) -> Option<String> {
129-
let replaced = self.descriptor.replace("/0/*", "/1/*");
130-
131-
if replaced != self.descriptor {
132-
Some(replaced)
131+
if let Some(descriptor) = &self.descriptor {
132+
let replaced = descriptor.replace("/0/*", "/1/*");
133+
if replaced != *descriptor {
134+
Some(replaced)
135+
} else {
136+
None
137+
}
133138
} else {
134139
None
135140
}

0 commit comments

Comments
 (0)