feat: Sign using NIP-07

Signed-off-by: Awiteb <a@4rs.nl>
This commit is contained in:
Awiteb
2025-08-06 07:58:06 +00:00
parent ad215002e2
commit 904d14064f
17 changed files with 272 additions and 40 deletions

View File

@@ -18,6 +18,8 @@
mod bunker;
/// `config keyring` subcommand
mod keyring;
/// `config nip07` subcommand
mod nip07;
/// `config pow` subcommand
mod pow;
/// `config relays` subcommand
@@ -27,6 +29,7 @@ use clap::Subcommand;
use self::bunker::BunkerArgs;
use self::keyring::KeyringArgs;
use self::nip07::Nip07Args;
use self::pow::PowArgs;
use self::relays::RelaysArgs;
use super::CliOptions;
@@ -45,10 +48,13 @@ pub enum ConfigSubcommands {
/// Managing the secret key keyring, including enabling, disabling, or
/// resetting it.
Keyring(KeyringArgs),
/// Controls the NIP-07 browser signer proxy, turning it on or off, and
/// configures the `ip:port` address.
Nip07(Nip07Args),
}
impl CommandRunner for ConfigSubcommands {
async fn run(self, options: CliOptions) -> N34Result<()> {
crate::run_command!(self, options, & Pow Relays Bunker Keyring)
crate::run_command!(self, options, & Pow Relays Bunker Keyring Nip07)
}
}

View File

@@ -0,0 +1,58 @@
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
// Copyright (C) 2025 Awiteb <a@4rs.nl>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
use std::net::SocketAddr;
use clap::{ArgGroup, Args};
use crate::{
cli::{CliOptions, options_state::DEFAULT_NIP07_PROXY_ADDR, traits::CommandRunner},
error::N34Result,
};
#[derive(Args, Debug)]
#[clap(
group(
ArgGroup::new("options")
.required(true)
)
)]
pub struct Nip07Args {
/// Enable NIP-07 as the default signer.
#[arg(long, group = "options")]
enable: bool,
/// Disable NIP-07 as the default signer.
#[arg(long, group = "options", group = "disable_options")]
disable: bool,
/// Set the default `ip:port` for the browser signer proxy.
#[arg(long, group = "disable_options")]
addr: Option<SocketAddr>,
}
impl CommandRunner for Nip07Args {
const NEED_SIGNER: bool = false;
async fn run(self, mut options: CliOptions) -> N34Result<()> {
if self.enable {
let addr = self.addr.unwrap_or(DEFAULT_NIP07_PROXY_ADDR);
options.config.nip07 = Some(addr)
} else {
options.config.nip07 = None
}
options.config.dump()
}
}

View File

@@ -31,7 +31,7 @@ use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use clap::{ArgGroup, Args, Parser};
use clap::{Args, Parser};
use nostr::key::{Keys, PublicKey, SecretKey};
use nostr::nips::nip46::NostrConnectURI;
use nostr::signer::{IntoNostrSigner, NostrSigner};
@@ -44,6 +44,7 @@ use self::reply::ReplyArgs;
use self::repo::RepoSubcommands;
use self::sets::SetsSubcommands;
use super::CliConfig;
use super::options_state::OptionsState;
use super::types::RelayOrSet;
use super::{parsers, traits::CommandRunner};
use crate::cli::Cli;
@@ -59,21 +60,18 @@ pub const DEFAULT_FALLBACK_PATH: &str = "I_DO_NOT_KNOW_WHY_CLAP_DO_NOT_SUPPORT_L
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", "bunker_url"])
.required(false)
)
)]
#[derive(Args)]
pub struct CliOptions {
/// Your Nostr secret key
#[arg(short, long)]
#[arg(short, long, group = "signer")]
pub secret_key: Option<SecretKey>,
/// NIP-46 bunker url used for signing events
#[arg(short, long, value_parser=parsers::parse_bunker_url)]
#[arg(short, long, group = "signer", value_parser = parsers::parse_bunker_url)]
pub bunker_url: Option<NostrConnectURI>,
/// Enables signing events using the browser's NIP-07 extension. Listens on
/// `127.0.0.1:51034`.
#[arg(short = '7', long, group = "signer")]
pub nip07: bool,
/// Fallbacks relay to write and read from it. Multiple relays can be
/// passed.
#[arg(short, long)]
@@ -86,6 +84,10 @@ pub struct CliOptions {
hide_default_value = true, value_parser = parsers::parse_config_path
)]
pub config: CliConfig,
/// The state of options. Some values that are used by them but should not
/// be entered via the CLI
#[arg(skip)]
pub state: OptionsState,
}
/// N34 commands
@@ -124,7 +126,7 @@ pub enum Commands {
impl CliOptions {
/// Gets the public key of the user.
pub async fn pubkey(&self) -> N34Result<PublicKey> {
let Some(signer) = self.signer()? else {
let Some(signer) = self.signer().await? 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 \
@@ -136,10 +138,21 @@ impl CliOptions {
}
/// Returns the signer
pub fn signer(&self) -> N34Result<Option<Arc<dyn NostrSigner + 'static>>> {
if self.config.keyring_secret_key {
pub async fn signer(&self) -> N34Result<Option<Arc<dyn NostrSigner + 'static>>> {
if self.nip07 {
self.state.browser_signer_proxy.start().await?;
println!(
"Browser signer proxy started at: {}",
self.state.browser_signer_proxy.url()
);
// FIXME: Use `BrowserSignerProxy::is_session_active` after it release
// nostr@0.44.0
tokio::time::sleep(Duration::from_secs(10)).await;
return Ok(Some(
Cli::user_keypair(self.secret_key.clone())?.into_nostr_signer(),
self.state.browser_signer_proxy.clone().into_nostr_signer(),
));
}

View File

@@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
use std::{collections::HashSet, fs, path::PathBuf};
use std::{collections::HashSet, fs, net::SocketAddr, path::PathBuf};
use nostr::{
nips::{nip19::Nip19Coordinate, nip46::NostrConnectURI},
@@ -76,6 +76,9 @@ pub struct CliConfig {
/// Whether to use the system keyring to store the secret key.
#[serde(default)]
pub keyring_secret_key: bool,
/// Signs events using the browser's NIP-07 extension.
#[serde(default)]
pub nip07: Option<SocketAddr>,
}
/// A named group of repositories and relays.

View File

@@ -24,6 +24,8 @@ pub mod config;
pub mod defaults;
/// Macros for CLI application.
pub mod macros;
/// Represents the state used for CLI options.
pub mod options_state;
/// CLI arguments parsers
pub mod parsers;
/// CLI traits
@@ -31,10 +33,13 @@ pub mod traits;
/// Common helper types used throughout the CLI.
pub mod types;
use clap::Parser;
use clap_verbosity_flag::Verbosity;
use nostr::key::Keys;
use nostr::key::SecretKey;
use nostr_browser_signer_proxy::BrowserSignerProxy;
use nostr_browser_signer_proxy::BrowserSignerProxyOptions;
use nostr_keyring::KeyringError;
use nostr_keyring::NostrKeyring;
use types::RelayOrSet;
@@ -42,6 +47,7 @@ use types::RelayOrSet;
pub use self::commands::*;
pub use self::config::*;
use self::traits::CommandRunner;
use crate::cli::options_state::BROWSER_SIGNER_PROXY_TIMEOUT;
use crate::error::N34Error;
use crate::error::N34Result;
use crate::nostr_utils::traits::NostrKeyringErrorUtils;
@@ -143,10 +149,29 @@ pub fn post_cli(mut cli: Cli) -> N34Result<Cli> {
cli.options.relays = relays.iter().cloned().map(RelayOrSet::Relay).collect();
}
if cli.options.bunker_url.is_none()
&& let Some(bunker_url) = &cli.options.config.bunker_url
// Automatically sets the signer based on the configuration if no signer
// is provided.
if !cli.options.nip07
&& cli.options.bunker_url.is_none()
&& (cli.options.secret_key.is_none() || cli.options.config.keyring_secret_key)
{
cli.options.bunker_url = Some(bunker_url.clone());
if let Some(addr) = cli.options.config.nip07 {
cli.options.nip07 = true;
cli.options.state.browser_signer_proxy = BrowserSignerProxy::new(
BrowserSignerProxyOptions::default()
.timeout(BROWSER_SIGNER_PROXY_TIMEOUT)
.ip_addr(addr.ip())
.port(addr.port()),
);
} else if let Some(bunker_url) = &cli.options.config.bunker_url {
cli.options.bunker_url = Some(bunker_url.clone());
} else if cli.options.config.keyring_secret_key {
cli.options.secret_key = Some(
Cli::user_keypair(cli.options.secret_key)?
.secret_key()
.clone(),
);
}
}
Ok(cli)

56
src/cli/options_state.rs Normal file
View File

@@ -0,0 +1,56 @@
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
// Copyright (C) 2025 Awiteb <a@4rs.nl>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
use std::{
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
time::Duration,
};
use nostr_browser_signer_proxy::{BrowserSignerProxy, BrowserSignerProxyOptions};
/// The default socket address used for the NIP-07 signer proxy, set to
/// localhost on port 51034.
pub const DEFAULT_NIP07_PROXY_ADDR: SocketAddr =
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 51034));
/// How long to wait for the proxy response (3 minutes).
pub const BROWSER_SIGNER_PROXY_TIMEOUT: Duration = Duration::from_secs(60 * 3);
/// Represents the state used for CLI options.
pub struct OptionsState {
/// The browser signer proxy, will be used if `--nip07` is enabled
pub browser_signer_proxy: BrowserSignerProxy,
}
impl Default for OptionsState {
fn default() -> Self {
Self {
browser_signer_proxy: default_browser_signer_proxy(),
}
}
}
/// Build the default browser signer proxy
#[inline]
fn default_browser_signer_proxy() -> BrowserSignerProxy {
BrowserSignerProxy::new(
BrowserSignerProxyOptions::default()
.timeout(BROWSER_SIGNER_PROXY_TIMEOUT)
.ip_addr(DEFAULT_NIP07_PROXY_ADDR.ip())
.port(DEFAULT_NIP07_PROXY_ADDR.port()),
)
}

View File

@@ -75,8 +75,8 @@ pub fn parse_config_path(config_path: &str) -> N34Result<CliConfig> {
CliConfig::load(path)
}
// Parses a bunker URL and checks if it's a valid Nostr Connect URI.
// Returns an error if the URL is not a valid bunker URL.
/// Parses a bunker URL and checks if it's a valid Nostr Connect URI.
/// Returns an error if the URL is not a valid bunker URL.
pub fn parse_bunker_url(bunker_url: &str) -> N34Result<NostrConnectURI> {
match NostrConnectURI::parse(bunker_url) {
Ok(url) if url.is_bunker() => Ok(url),