feat: Support signing using NIP-46 bunker

Suggested-by: DanConwayDev <DanConwayDev@protonmail.com>
Signed-off-by: Awiteb <a@4rs.nl>
This commit is contained in:
Awiteb
2025-07-18 21:18:26 +00:00
parent f0c20c3677
commit 4e0ecdceaf
11 changed files with 211 additions and 23 deletions

View File

@@ -28,9 +28,14 @@ pub mod repo;
pub mod sets;
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use clap::{ArgGroup, Args, Parser};
use nostr::key::{Keys, PublicKey, SecretKey};
use nostr::nips::nip46::NostrConnectURI;
use nostr::signer::{IntoNostrSigner, NostrSigner};
use nostr_connect::client::NostrConnect;
use self::config::ConfigSubcommands;
use self::issue::IssueSubcommands;
@@ -41,6 +46,8 @@ use self::sets::SetsSubcommands;
use super::CliConfig;
use super::types::RelayOrSet;
use super::{parsers, traits::CommandRunner};
use crate::cli::Cli;
use crate::cli::types::EchoAuthUrl;
use crate::error::{N34Error, N34Result};
/// Default path used when no path is provided via command line arguments.
@@ -48,12 +55,15 @@ use crate::error::{N34Error, N34Result};
/// This is a workaround since Clap doesn't support lazy evaluation of defaults.
pub const DEFAULT_FALLBACK_PATH: &str = "I_DO_NOT_KNOW_WHY_CLAP_DO_NOT_SUPPORT_LAZY_DEFAULT";
/// How long to wait for bunker response (3 minutes).
const BUNKER_TIMEOUT: Duration = Duration::from_secs(60 * 3);
/// The command-line interface options
#[derive(Args, Clone)]
#[clap(
group(
ArgGroup::new("signer")
.args(&["secret_key"])
.args(&["secret_key", "bunker_url"])
.required(false)
)
)]
@@ -61,6 +71,9 @@ pub struct CliOptions {
/// Your Nostr secret key
#[arg(short, long)]
pub secret_key: Option<SecretKey>,
/// NIP-46 bunker url used for signing events
#[arg(short, long, value_parser=parsers::parse_bunker_url)]
pub bunker_url: Option<NostrConnectURI>,
/// Fallbacks relay to write and read from it. Multiple relays can be
/// passed.
#[arg(short, long)]
@@ -111,22 +124,35 @@ pub enum Commands {
impl CliOptions {
/// Gets the public key of the user.
pub async fn pubkey(&self) -> N34Result<PublicKey> {
if let Some(sk) = &self.secret_key {
return Ok(Keys::new(sk.clone()).public_key());
}
unreachable!(
"This method should only be called when a signer is required. If this panic occurs, \
it indicates a bug where the command failed to properly require a signer (which is \
the default behavior)"
)
let Some(signer) = self.signer()? else {
unreachable!(
"This method should only be called when a signer is required. If this panic \
occurs, it indicates a bug where the command failed to properly require a signer \
(which is the default behavior)"
)
};
signer.get_public_key().await.map_err(N34Error::SignerError)
}
/// Returns the signer
pub fn signer(&self) -> Option<impl nostr::signer::IntoNostrSigner> {
pub fn signer(&self) -> N34Result<Option<Arc<dyn NostrSigner + 'static>>> {
if let Some(sk) = &self.secret_key {
return Some(Keys::new(sk.clone()));
return Ok(Some(Keys::new(sk.clone()).into_nostr_signer()));
}
None
if let Some(ref bunker_url) = self.bunker_url {
let mut nostrconnect = NostrConnect::new(
bunker_url.clone(),
Cli::n34_keypair()?,
BUNKER_TIMEOUT,
None,
)
.expect("It's a bunker url and not a client");
nostrconnect.auth_url_handler(EchoAuthUrl);
return Ok(Some(nostrconnect.into_nostr_signer()));
}
Ok(None)
}
/// Returns an error if there are no relays.
@@ -139,11 +165,10 @@ impl CliOptions {
/// Returns an error if there are no signers
pub fn ensure_signer(&self) -> N34Result<()> {
if self.secret_key.is_none() {
Err(N34Error::SignerRequired)
} else {
Ok(())
if self.secret_key.is_none() && self.bunker_url.is_none() {
return Err(N34Error::SignerRequired);
}
Ok(())
}
}
@@ -151,6 +176,7 @@ impl fmt::Debug for CliOptions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CliOptions")
.field("secret_key", &self.secret_key.as_ref().map(|_| "*******"))
.field("bunker_url", &self.bunker_url.as_ref().map(|_| "*******"))
.field("relays", &self.relays)
.field("pow", &self.pow)
.field("config", &self.config)