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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ target
.idea
.DS_store
.zone

NOTES.md
temp/
50 changes: 49 additions & 1 deletion client/src/bin/space-cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,14 @@ enum Commands {
#[arg(long)]
skip_anchor: bool,
},
/// Export a space's Nostr nsec key
#[command(name = "exportspacensec")]
ExportSpaceNsec {
/// The space to use for exporting the Nostr nsec key
space: String,
/// Destination path to export nsec key file
path: PathBuf,
},
/// Updates the Merkle trust path for space-anchored Nostr events
#[command(name = "refreshanchor")]
RefreshAnchor {
Expand Down Expand Up @@ -453,6 +461,35 @@ impl SpaceCli {

Ok(result)
}

async fn export_space_nsec(
&self,
space: String,
event: NostrEvent,
anchor: bool,
most_recent: bool,
) -> Result<NostrEvent, ClientError> {
let mut result = self
.client
.wallet_sign_event(&self.wallet, &space, event)
.await?;

if anchor {
result = self.add_anchor(result, most_recent).await?
}

Ok(result)
}

async fn get_space_nsec_keys(&self, space: String) -> Result<(String, String), ClientError> {
let result = self
.client
.wallet_get_space_nsec_keys(&self.wallet, &space)
.await?;

Ok(result)
}

async fn add_anchor(
&self,
mut event: NostrEvent,
Expand Down Expand Up @@ -927,10 +964,21 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client
} => {
let update = encode_dns_update(&space, input)
.map_err(|e| ClientError::Custom(format!("Parse error: {}", e)))?;
let result = cli.sign_event(space, update, !skip_anchor, false).await?;
let result = cli.export_space_nsec(space, update, !skip_anchor, false).await?;

println!("{}", serde_json::to_string(&result).expect("result"));
}
Commands::ExportSpaceNsec {
space,
path,
} => {
let (secret_hex, nsec) = cli.get_space_nsec_keys(space).await?;

let content = format!("secret_hex: {}\nnsec: {}", secret_hex, nsec);
fs::write(path, content).map_err(|e| {
ClientError::Custom(format!("Could not save to path: {}", e.to_string()))
})?;
}
Commands::RefreshAnchor {
input,
prefer_recent,
Expand Down
40 changes: 40 additions & 0 deletions client/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,21 @@ pub trait Rpc {
event: NostrEvent,
) -> Result<NostrEvent, ErrorObjectOwned>;

#[method(name = "walletexportnsec")]
async fn wallet_export_nsec(
&self,
wallet: &str,
space: &str,
event: NostrEvent,
) -> Result<NostrEvent, ErrorObjectOwned>;

#[method(name = "walletgetspacenseckeys")]
async fn wallet_get_space_nsec_keys(
&self,
wallet: &str,
space: &str,
) -> Result<(String, String), ErrorObjectOwned>;

#[method(name = "walletgetinfo")]
async fn wallet_get_info(&self, name: &str)
-> Result<WalletInfoWithProgress, ErrorObjectOwned>;
Expand Down Expand Up @@ -916,6 +931,31 @@ impl RpcServer for RpcServerImpl {
.map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::<String>))
}

async fn wallet_export_nsec(
&self,
wallet: &str,
space: &str,
event: NostrEvent,
) -> Result<NostrEvent, ErrorObjectOwned> {
self.wallet(&wallet)
.await?
.send_sign_event(space, event)
.await
.map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::<String>))
}

async fn wallet_get_space_nsec_keys(
&self,
wallet: &str,
space: &str,
) -> Result<(String, String), ErrorObjectOwned> {
self.wallet(&wallet)
.await?
.send_get_space_nsec_keys(space)
.await
.map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::<String>))
}

async fn wallet_get_info(
&self,
wallet: &str,
Expand Down
29 changes: 29 additions & 0 deletions client/src/wallets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,15 @@ pub enum WalletCommand {
event: NostrEvent,
resp: crate::rpc::Responder<anyhow::Result<NostrEvent>>,
},
ExportSpaceNsec {
space: String,
event: NostrEvent,
resp: crate::rpc::Responder<anyhow::Result<NostrEvent>>,
},
GetSpaceNsecKeys {
space: String,
resp: crate::rpc::Responder<anyhow::Result<(String, String)>>,
},
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, ValueEnum)]
Expand Down Expand Up @@ -517,6 +526,12 @@ impl RpcWallet {
WalletCommand::SignEvent { space, event, resp } => {
_ = resp.send(wallet.sign_event::<Sha256>(state, &space, event));
}
WalletCommand::ExportSpaceNsec { space, event, resp } => {
_ = resp.send(wallet.export_space_nsec::<Sha256>(state, &space, event));
}
WalletCommand::GetSpaceNsecKeys { space, resp } => {
_ = resp.send(wallet.get_space_nsec_keys::<Sha256>(state, &space));
}
}
Ok(())
}
Expand Down Expand Up @@ -1474,6 +1489,20 @@ impl RpcWallet {
resp_rx.await?
}

pub async fn send_get_space_nsec_keys(
&self,
space: &str,
) -> anyhow::Result<(String, String)> {
let (resp, resp_rx) = oneshot::channel();
self.sender
.send(WalletCommand::GetSpaceNsecKeys {
space: space.to_string(),
resp,
})
.await?;
resp_rx.await?
}

pub async fn send_list_transactions(
&self,
count: usize,
Expand Down
79 changes: 79 additions & 0 deletions wallet/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{collections::BTreeMap, fmt::Debug, fs, ops::Mul, path::PathBuf, str::FromStr};
use anyhow::{anyhow, Context};
use bech32::{Bech32, Hrp, encode};
use bdk_wallet::{
chain,
chain::{
Expand Down Expand Up @@ -421,6 +422,84 @@ impl SpacesWallet {
Ok(event)
}

pub fn export_space_nsec<H: KeyHasher>(
&mut self,
src: &mut impl DataSource,
space: &str,
mut event: NostrEvent,
) -> anyhow::Result<NostrEvent> {
if event.space().is_some_and(|s| s != space) {
return Err(anyhow::anyhow!("Space tag does not match specified space"));
}

let label = SLabel::from_str(space)?;
let space_key = SpaceKey::from(H::hash(label.as_ref()));
let outpoint = match src.get_space_outpoint(&space_key)? {
None => return Err(anyhow::anyhow!("Space not found")),
Some(outpoint) => outpoint,
};
let utxo = match self.get_utxo(outpoint) {
None => return Err(anyhow::anyhow!("Space not owned by wallet")),
Some(utxo) => utxo,
};

// derive taproot keypair for space XXX
let keypair = self
.get_taproot_keypair(utxo.keychain, utxo.derivation_index) // derive taproot keypair for space
.context("Could not derive taproot keypair to sign message")?; // propagate derivation errors

// WARNING: printing private keys is insecure; do this only for debugging
let inner = keypair.to_inner();
let secret = inner.secret_key();
let secret_bytes = secret.secret_bytes();
let secret_hex = hex::encode(secret_bytes);

// Convert to nsec format (nostr bech32 encoding)
let hrp = Hrp::parse("nsec").unwrap_or_else(|_| Hrp::parse("nsec").unwrap());
let nsec = encode::<Bech32>(hrp, &secret_bytes)
.unwrap_or_else(|_| "encoding_failed".to_string());

println!("Signing with private key (hex): {}", secret_hex);
println!("Signing with private key (nsec): {}", nsec);

event.sign(secp256k1::Secp256k1::new(), &inner)?; // perform Schnorr signature with taproot key
Ok(event) // return the now-signed event
}

pub fn get_space_nsec_keys<H: KeyHasher>(
&mut self,
src: &mut impl DataSource,
space: &str,
) -> anyhow::Result<(String, String)> {
let label = SLabel::from_str(space)?;
let space_key = SpaceKey::from(H::hash(label.as_ref()));
let outpoint = match src.get_space_outpoint(&space_key)? {
None => return Err(anyhow::anyhow!("Space not found")),
Some(outpoint) => outpoint,
};
let utxo = match self.get_utxo(outpoint) {
None => return Err(anyhow::anyhow!("Space not owned by wallet")),
Some(utxo) => utxo,
};

// derive taproot keypair for space
let keypair = self
.get_taproot_keypair(utxo.keychain, utxo.derivation_index)
.context("Could not derive taproot keypair")?;

let inner = keypair.to_inner();
let secret = inner.secret_key();
let secret_bytes = secret.secret_bytes();
let secret_hex = hex::encode(secret_bytes);

// Convert to nsec format (nostr bech32 encoding)
let hrp = Hrp::parse("nsec").unwrap_or_else(|_| Hrp::parse("nsec").unwrap());
let nsec = encode::<Bech32>(hrp, &secret_bytes)
.unwrap_or_else(|_| "encoding_failed".to_string());

Ok((secret_hex, nsec))
}

pub fn verify_event<H: KeyHasher>(
src: &mut impl DataSource,
space: &str,
Expand Down