From 055b32cec409dfb2dfbc43a4d2ccf75c04695afa Mon Sep 17 00:00:00 2001 From: Alex Tsokurov Date: Sun, 13 Jul 2025 14:28:44 +0200 Subject: [PATCH 1/2] nostr sign / verify cleanup --- client/src/bin/space-cli.rs | 140 +++++++----------------------- client/src/rpc.rs | 74 +++++++++------- client/tests/integration_tests.rs | 30 +++++-- wallet/src/lib.rs | 64 +++++--------- wallet/src/nostr.rs | 42 ++++++--- 5 files changed, 151 insertions(+), 199 deletions(-) diff --git a/client/src/bin/space-cli.rs b/client/src/bin/space-cli.rs index 3e89fe6..eda5e1d 100644 --- a/client/src/bin/space-cli.rs +++ b/client/src/bin/space-cli.rs @@ -37,10 +37,7 @@ use spaces_client::{ }; use spaces_protocol::bitcoin::{Amount, FeeRate, OutPoint, Txid}; use spaces_wallet::{ - bitcoin::secp256k1::schnorr::Signature, - export::WalletExport, - nostr::{NostrEvent, NostrTag}, - Listing, + bitcoin::secp256k1::schnorr::Signature, export::WalletExport, nostr::NostrEvent, Listing, }; #[derive(Parser, Debug)] @@ -268,10 +265,6 @@ enum Commands { /// Path to a Nostr event json file (omit for stdin) #[arg(short, long)] input: Option, - - /// Include a space-tag and trust path data - #[arg(short, long)] - anchor: bool, }, /// Verify a signed Nostr event against the space's public key #[command(name = "verifyevent")] @@ -290,9 +283,6 @@ enum Commands { space: String, /// The DNS zone file path (omit for stdin) input: Option, - /// Skip including bundled Merkle proof in the event. - #[arg(long)] - skip_anchor: bool, }, /// Updates the Merkle trust path for space-anchored Nostr events #[command(name = "refreshanchor")] @@ -439,62 +429,15 @@ impl SpaceCli { &self, space: String, event: NostrEvent, - anchor: bool, most_recent: bool, ) -> Result { - let mut result = self + let result = self .client - .wallet_sign_event(&self.wallet, &space, event) + .wallet_sign_event(&self.wallet, &space, event, Some(most_recent)) .await?; - if anchor { - result = self.add_anchor(result, most_recent).await? - } - Ok(result) } - async fn add_anchor( - &self, - mut event: NostrEvent, - most_recent: bool, - ) -> Result { - let space = match event.space() { - None => { - return Err(ClientError::Custom( - "A space tag is required to add an anchor".to_string(), - )) - } - Some(space) => space, - }; - - let spaceout = self - .client - .get_space(&space) - .await - .map_err(|e| ClientError::Custom(e.to_string()))? - .ok_or(ClientError::Custom(format!( - "Space not found \"{}\"", - space - )))?; - - event.proof = Some( - base64::prelude::BASE64_STANDARD.encode( - self.client - .prove_spaceout( - OutPoint { - txid: spaceout.txid, - vout: spaceout.spaceout.n as _, - }, - Some(most_recent), - ) - .await - .map_err(|e| ClientError::Custom(e.to_string()))? - .proof, - ), - ); - - Ok(event) - } async fn send_request( &self, req: Option, @@ -893,42 +836,20 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client cli.client.verify_listing(listing).await?; println!("{} Listing verified", "✓".color(Color::Green)); } - Commands::SignEvent { - mut space, - input, - anchor, - } => { - let mut event = read_event(input) + Commands::SignEvent { space, input } => { + let space = normalize_space(&space); + let event = read_event(input) .map_err(|e| ClientError::Custom(format!("input error: {}", e.to_string())))?; - space = normalize_space(&space); - match event.space() { - None if anchor => event - .tags - .insert(0, NostrTag(vec!["space".to_string(), space.clone()])), - Some(tag) => { - if tag != space { - return Err(ClientError::Custom(format!( - "Expected a space tag with value '{}', got '{}'", - space, tag - ))); - } - } - _ => {} - }; - - let result = cli.sign_event(space, event, anchor, false).await?; + let result = cli.sign_event(space, event, false).await?; println!("{}", serde_json::to_string(&result).expect("result")); } - Commands::SignZone { - space, - input, - skip_anchor, - } => { - let update = encode_dns_update(&space, input) + Commands::SignZone { space, input } => { + let space = normalize_space(&space); + let event = encode_dns_update(input) .map_err(|e| ClientError::Custom(format!("Parse error: {}", e)))?; - let result = cli.sign_event(space, update, !skip_anchor, false).await?; + let result = cli.sign_event(space, event, false).await?; println!("{}", serde_json::to_string(&result).expect("result")); } Commands::RefreshAnchor { @@ -937,34 +858,31 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client } => { let event = read_event(input) .map_err(|e| ClientError::Custom(format!("input error: {}", e.to_string())))?; - let space = match event.space() { - None => { - return Err(ClientError::Custom( - "Not a space-anchored event (no space tag)".to_string(), - )) - } - Some(space) => space, - }; - - let mut event = cli - .client - .verify_event(&space, event) + cli.client + .verify_event(event.clone()) .await .map_err(|e| ClientError::Custom(e.to_string()))?; - event.proof = None; - event = cli.add_anchor(event, prefer_recent).await?; - println!("{}", serde_json::to_string(&event).expect("result")); + let e = event.clone(); + let space = e.get_space_tag().expect("space tag").0; + let result = cli + .client + .wallet_sign_event(&cli.wallet, space, event, Some(prefer_recent)) + .await?; + println!("{}", serde_json::to_string(&result).expect("result")); } Commands::VerifyEvent { space, input } => { let event = read_event(input) .map_err(|e| ClientError::Custom(format!("input error: {}", e.to_string())))?; - let event = cli - .client - .verify_event(&space, event) + cli.client + .verify_event(event.clone()) .await .map_err(|e| ClientError::Custom(e.to_string()))?; - + if event.get_space_tag().expect("space tag").0 != &space { + return Err(ClientError::Custom( + "Space tag does not match specified space".to_string(), + )); + } println!("{}", serde_json::to_string(&event).expect("result")); } } @@ -976,7 +894,7 @@ fn default_rpc_url(chain: &ExtendedNetwork) -> String { format!("http://127.0.0.1:{}", default_spaces_rpc_port(chain)) } -fn encode_dns_update(space: &str, zone_file: Option) -> anyhow::Result { +fn encode_dns_update(zone_file: Option) -> anyhow::Result { // domain crate panics if zone doesn't end in a new line let zone = get_input(zone_file)? + "\n"; @@ -1000,7 +918,7 @@ fn encode_dns_update(space: &str, zone_file: Option) -> anyhow::Result< Ok(NostrEvent::new( 871_222, &base64::prelude::BASE64_STANDARD.encode(msg.as_slice()), - vec![NostrTag(vec!["space".to_string(), space.to_string()])], + vec![], )) } diff --git a/client/src/rpc.rs b/client/src/rpc.rs index cc6a71d..2ac6d43 100644 --- a/client/src/rpc.rs +++ b/client/src/rpc.rs @@ -4,6 +4,7 @@ use std::{ }; use anyhow::{anyhow, Context}; +use base64::Engine; use bdk::{ bitcoin::{Amount, BlockHash, FeeRate, Network, Txid}, chain::BlockId, @@ -49,11 +50,10 @@ use tokio::{ }; use crate::auth::BasicAuthLayer; -use crate::wallets::WalletInfoWithProgress; use crate::{ calc_progress, checker::TxChecker, - client::{BlockMeta, TxEntry, BlockchainInfo}, + client::{BlockMeta, BlockchainInfo, TxEntry}, config::ExtendedNetwork, deserialize_base64, serialize_base64, source::BitcoinRpc, @@ -61,7 +61,7 @@ use crate::{ store::{ChainState, LiveSnapshot, RolloutEntry, Sha256}, wallets::{ AddressKind, ListSpacesResponse, RpcWallet, TxInfo, TxResponse, WalletCommand, - WalletResponse, + WalletInfoWithProgress, WalletResponse, }, }; @@ -147,9 +147,8 @@ pub enum ChainStateCommand { resp: Responder>, }, VerifyEvent { - space: String, event: NostrEvent, - resp: Responder>, + resp: Responder>, }, ProveSpaceout { outpoint: OutPoint, @@ -221,11 +220,7 @@ pub trait Rpc { async fn wallet_import(&self, wallet: WalletExport) -> Result<(), ErrorObjectOwned>; #[method(name = "verifyevent")] - async fn verify_event( - &self, - space: &str, - event: NostrEvent, - ) -> Result; + async fn verify_event(&self, event: NostrEvent) -> Result<(), ErrorObjectOwned>; #[method(name = "walletsignevent")] async fn wallet_sign_event( @@ -233,6 +228,7 @@ pub trait Rpc { wallet: &str, space: &str, event: NostrEvent, + most_recent: Option, ) -> Result; #[method(name = "walletgetinfo")] @@ -507,7 +503,11 @@ impl WalletManager { Ok(export) } - pub async fn create_wallet(&self, client: &reqwest::Client, name: &str) -> anyhow::Result { + pub async fn create_wallet( + &self, + client: &reqwest::Client, + name: &str, + ) -> anyhow::Result { let mnemonic: GeneratedKey<_, Tap> = Mnemonic::generate((WordCount::Words12, Language::English)) .map_err(|_| anyhow!("Mnemonic generation error"))?; @@ -518,7 +518,12 @@ impl WalletManager { Ok(mnemonic.to_string()) } - pub async fn recover_wallet(&self, client: &reqwest::Client, name: &str, mnemonic: &str) -> anyhow::Result<()> { + pub async fn recover_wallet( + &self, + client: &reqwest::Client, + name: &str, + mnemonic: &str, + ) -> anyhow::Result<()> { let start_block = self.get_wallet_start_block(client).await?; self.setup_new_wallet(name.to_string(), mnemonic.to_string(), start_block)?; self.load_wallet(name).await?; @@ -892,13 +897,9 @@ impl RpcServer for RpcServerImpl { }) } - async fn verify_event( - &self, - space: &str, - event: NostrEvent, - ) -> Result { + async fn verify_event(&self, event: NostrEvent) -> Result<(), ErrorObjectOwned> { self.store - .verify_event(space, event) + .verify_event(event) .await .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::)) } @@ -907,8 +908,27 @@ impl RpcServer for RpcServerImpl { &self, wallet: &str, space: &str, - event: NostrEvent, + mut event: NostrEvent, + most_recent: Option, ) -> Result { + let most_recent = most_recent.unwrap_or(false); + let spaceout = self.get_space(space).await?.ok_or(ErrorObjectOwned::owned( + -1, + format!("Space not found \"{}\"", space), + None::, + ))?; + let proof = base64::prelude::BASE64_STANDARD.encode( + self.prove_spaceout( + OutPoint { + txid: spaceout.txid, + vout: spaceout.spaceout.n as _, + }, + Some(most_recent), + ) + .await? + .proof, + ); + event.set_space_tag(space, &proof); self.wallet(&wallet) .await? .send_sign_event(space, event) @@ -1281,12 +1301,8 @@ impl AsyncChainState { SpacesWallet::verify_listing::(chain_state, &listing).map(|_| ()), ); } - ChainStateCommand::VerifyEvent { space, event, resp } => { - _ = resp.send(SpacesWallet::verify_event::( - chain_state, - &space, - event, - )); + ChainStateCommand::VerifyEvent { event, resp } => { + _ = resp.send(SpacesWallet::verify_event::(chain_state, event)); } ChainStateCommand::ProveSpaceout { prefer_recent, @@ -1474,14 +1490,10 @@ impl AsyncChainState { resp_rx.await? } - pub async fn verify_event(&self, space: &str, event: NostrEvent) -> anyhow::Result { + pub async fn verify_event(&self, event: NostrEvent) -> anyhow::Result<()> { let (resp, resp_rx) = oneshot::channel(); self.sender - .send(ChainStateCommand::VerifyEvent { - space: space.to_string(), - event, - resp, - }) + .send(ChainStateCommand::VerifyEvent { event, resp }) .await?; resp_rx.await? } diff --git a/client/tests/integration_tests.rs b/client/tests/integration_tests.rs index e14fa27..983f959 100644 --- a/client/tests/integration_tests.rs +++ b/client/tests/integration_tests.rs @@ -45,12 +45,21 @@ async fn it_should_open_a_space_for_auction(rig: &TestRig) -> anyhow::Result<()> } assert_eq!(response.result.len(), 2, "must be 2 transactions"); let alices_spaces = rig.spaced.client.wallet_list_spaces(ALICE).await?; - assert!(alices_spaces.pending.first().is_some_and(|s| s.to_string() == TEST_SPACE), "must be a pending space"); + assert!( + alices_spaces + .pending + .first() + .is_some_and(|s| s.to_string() == TEST_SPACE), + "must be a pending space" + ); rig.mine_blocks(1, None).await?; rig.wait_until_synced().await?; let alices_spaces = rig.spaced.client.wallet_list_spaces(ALICE).await?; - assert!(alices_spaces.pending.is_empty(), "must have no pending spaces"); + assert!( + alices_spaces.pending.is_empty(), + "must have no pending spaces" + ); let fullspaceout = rig.spaced.client.get_space(TEST_SPACE).await?; let fullspaceout = fullspaceout.expect("a fullspace out"); @@ -102,7 +111,13 @@ async fn it_should_allow_outbidding(rig: &TestRig) -> anyhow::Result<()> { println!("{}", serde_json::to_string_pretty(&result).unwrap()); let bob_spaces_updated = rig.spaced.client.wallet_list_spaces(BOB).await?; - assert!(bob_spaces_updated.pending.first().is_some_and(|s| s.to_string() == TEST_SPACE), "must be a pending space"); + assert!( + bob_spaces_updated + .pending + .first() + .is_some_and(|s| s.to_string() == TEST_SPACE), + "must be a pending space" + ); rig.mine_blocks(1, None).await?; rig.wait_until_synced().await?; @@ -132,7 +147,10 @@ async fn it_should_allow_outbidding(rig: &TestRig) -> anyhow::Result<()> { alices_balance.balance + Amount::from_sat(TEST_INITIAL_BID + 662), "alice must be refunded this exact amount" ); - assert!(bob_spaces_updated.pending.is_empty(), "must have no pending spaces"); + assert!( + bob_spaces_updated.pending.is_empty(), + "must have no pending spaces" + ); let fullspaceout = rig.spaced.client.get_space(TEST_SPACE).await?; let fullspaceout = fullspaceout.expect("a fullspace out"); @@ -1274,7 +1292,7 @@ async fn it_should_allow_sign_verify_messages(rig: &TestRig) -> anyhow::Result<( let signed = rig .spaced .client - .wallet_sign_event(BOB, &space_name, msg.clone()) + .wallet_sign_event(BOB, &space_name, msg.clone(), None) .await .expect("sign"); @@ -1283,7 +1301,7 @@ async fn it_should_allow_sign_verify_messages(rig: &TestRig) -> anyhow::Result<( rig.spaced .client - .verify_event(&space_name, signed.clone()) + .verify_event(signed.clone()) .await .expect("verify"); diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 82ea6b2..80e686d 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -1,5 +1,5 @@ -use std::{collections::BTreeMap, fmt::Debug, fs, ops::Mul, path::PathBuf, str::FromStr}; use anyhow::{anyhow, Context}; +use bdk_wallet::chain::keychain_txout::KeychainTxOutIndex; use bdk_wallet::{ chain, chain::{ @@ -14,7 +14,6 @@ use bdk_wallet::{ AddressInfo, KeychainKind, LocalOutput, PersistedWallet, SignOptions, TxBuilder, Update, Wallet, WalletTx, WeightedUtxo, }; -use bdk_wallet::chain::keychain_txout::KeychainTxOutIndex; use bincode::config; use bitcoin::{ absolute::{Height, LockTime}, @@ -46,6 +45,7 @@ use spaces_protocol::{ slabel::SLabel, Covenant, FullSpaceOut, Space, }; +use std::{collections::BTreeMap, fmt::Debug, fs, ops::Mul, path::PathBuf, str::FromStr}; use crate::{ address::SpaceAddress, @@ -210,11 +210,11 @@ impl SpacesWallet { config.space_descriptors.external.clone(), config.space_descriptors.internal.clone(), ) - .lookahead(50) - .network(config.network) - .genesis_hash(genesis_hash) - .create_wallet(&mut conn) - .context("could not create wallet")? + .lookahead(50) + .network(config.network) + .genesis_hash(genesis_hash) + .create_wallet(&mut conn) + .context("could not create wallet")? }; let tx = conn @@ -281,7 +281,7 @@ impl SpacesWallet { }) } - pub fn transactions(&self) -> impl Iterator + '_ { + pub fn transactions(&self) -> impl Iterator + '_ { self.internal .transactions() .filter(|tx| !is_revert_tx(tx) && self.internal.spk_index().is_tx_relevant(&tx.tx_node)) @@ -379,11 +379,11 @@ impl SpacesWallet { self.internal.is_mine(script) } - pub fn list_unspent(&self) -> impl Iterator + '_ { + pub fn list_unspent(&self) -> impl Iterator + '_ { self.internal.list_unspent() } - pub fn list_output(&self) -> impl Iterator + '_ { + pub fn list_output(&self) -> impl Iterator + '_ { self.internal.list_output() } @@ -398,10 +398,6 @@ impl SpacesWallet { space: &str, mut event: NostrEvent, ) -> anyhow::Result { - 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)? { @@ -423,12 +419,12 @@ impl SpacesWallet { pub fn verify_event( src: &mut impl DataSource, - space: &str, - mut event: NostrEvent, - ) -> anyhow::Result { - if event.space().is_some_and(|s| s != space) { - return Err(anyhow::anyhow!("Space tag does not match specified space")); - } + event: NostrEvent, + ) -> anyhow::Result<()> { + let space = event + .get_space_tag() + .ok_or(anyhow::anyhow!("Space tag not found"))? + .0; let label = SLabel::from_str(&space)?; let space_key = SpaceKey::from(H::hash(label.as_ref())); @@ -448,23 +444,15 @@ impl SpacesWallet { if script_bytes.len() != secp256k1::constants::SCHNORR_PUBLIC_KEY_SIZE + 2 { return Err(anyhow::anyhow!("Expected a schnorr public key")); } - let pubkey = XOnlyPublicKey::from_slice(&script_bytes[2..])?; - match event.pubkey { - None => { - event.pubkey = Some(pubkey); - } - Some(actual) => { - if actual != pubkey { - return Err(anyhow::anyhow!("Event pubkey doesn't match space pubkey")); - } - } + if event.pubkey != Some(XOnlyPublicKey::from_slice(&script_bytes[2..])?) { + return Err(anyhow::anyhow!("Event pubkey doesn't match space pubkey")); } if !event.verify(secp256k1::Secp256k1::new()) { return Err(anyhow::anyhow!("Could not verify signature")); } - Ok(event) + Ok(()) } pub fn list_unspent_with_details( @@ -615,12 +603,8 @@ impl SpacesWallet { Ok(()) } - pub fn apply_update( - &mut self, - update: impl Into, - ) -> Result<(), CannotConnectError> { - self.internal - .apply_update(update) + pub fn apply_update(&mut self, update: impl Into) -> Result<(), CannotConnectError> { + self.internal.apply_update(update) } pub fn apply_unconfirmed_tx(&mut self, tx: Transaction, seen: u64) { @@ -653,7 +637,7 @@ impl SpacesWallet { event.previous_spaceout, event.details, ) - .context("could not insert tx event into wallet db")?; + .context("could not insert tx event into wallet db")?; } db_tx .commit() @@ -755,7 +739,7 @@ impl SpacesWallet { signature: listing.signature, sighash_type: TapSighashType::SinglePlusAnyoneCanPay, } - .to_vec(), + .to_vec(), ); let funded_psbt = { @@ -1205,7 +1189,7 @@ impl SpacesWallet { signature, sighash_type, } - .to_vec(), + .to_vec(), ); witness.push(&signing_info.script); witness.push(&signing_info.control_block.serialize()); diff --git a/wallet/src/nostr.rs b/wallet/src/nostr.rs index e3baf38..73fb5b6 100644 --- a/wallet/src/nostr.rs +++ b/wallet/src/nostr.rs @@ -9,6 +9,24 @@ use serde_json::json; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct NostrTag(pub Vec); +impl NostrTag { + pub fn is_space_tag(&self) -> bool { + self.0.len() >= 3 && self.0[0] == "s" + } + + pub fn get_space_data(&self) -> Option<(&String, &String)> { + if self.is_space_tag() { + Some((&self.0[1], &self.0[2])) + } else { + None + } + } + + pub fn new_space_tag(space: &str, proof: &str) -> Self { + NostrTag(vec!["s".to_string(), space.to_string(), proof.to_string()]) + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NostrEvent { #[serde(skip_serializing_if = "Option::is_none")] @@ -43,17 +61,19 @@ impl NostrEvent { } } - pub fn space(&self) -> Option { - self.tags - .iter() - .find(|tag| { - if tag.0.len() >= 1 { - tag.0[0] == "space" - } else { - false - } - }) - .map(|tag| tag.0[1].clone()) + pub fn get_space_tag(&self) -> Option<(&String, &String)> { + let space_tags: Vec<&NostrTag> = + self.tags.iter().filter(|tag| tag.is_space_tag()).collect(); + if space_tags.len() == 1 { + space_tags[0].get_space_data() + } else { + None + } + } + + pub fn set_space_tag(&mut self, space: &str, proof: &str) { + self.tags.retain(|tag| !tag.is_space_tag()); + self.tags.push(NostrTag::new_space_tag(space, proof)); } pub fn serialize_for_signing(&self) -> Option { From ddef4aeea22b91c9fd4a25c6aad44a156c58dc4f Mon Sep 17 00:00:00 2001 From: Alex Tsokurov Date: Mon, 14 Jul 2025 13:59:04 +0200 Subject: [PATCH 2/2] prefer_recent option --- client/src/bin/space-cli.rs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/client/src/bin/space-cli.rs b/client/src/bin/space-cli.rs index eda5e1d..0e5d353 100644 --- a/client/src/bin/space-cli.rs +++ b/client/src/bin/space-cli.rs @@ -261,17 +261,18 @@ enum Commands { SignEvent { /// Space name (e.g., @example) space: String, - /// Path to a Nostr event json file (omit for stdin) #[arg(short, long)] input: Option, + /// Prefer the most recent trust path (not recommended) + #[arg(long)] + prefer_recent: bool, }, /// Verify a signed Nostr event against the space's public key #[command(name = "verifyevent")] VerifyEvent { /// Space name (e.g., @example) space: String, - /// Path to a signed Nostr event json file (omit for stdin) #[arg(short, long)] input: Option, @@ -283,6 +284,9 @@ enum Commands { space: String, /// The DNS zone file path (omit for stdin) input: Option, + /// Prefer the most recent trust path (not recommended) + #[arg(long)] + prefer_recent: bool, }, /// Updates the Merkle trust path for space-anchored Nostr events #[command(name = "refreshanchor")] @@ -836,20 +840,28 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client cli.client.verify_listing(listing).await?; println!("{} Listing verified", "✓".color(Color::Green)); } - Commands::SignEvent { space, input } => { + Commands::SignEvent { + space, + input, + prefer_recent, + } => { let space = normalize_space(&space); let event = read_event(input) .map_err(|e| ClientError::Custom(format!("input error: {}", e.to_string())))?; - let result = cli.sign_event(space, event, false).await?; + let result = cli.sign_event(space, event, prefer_recent).await?; println!("{}", serde_json::to_string(&result).expect("result")); } - Commands::SignZone { space, input } => { + Commands::SignZone { + space, + input, + prefer_recent, + } => { let space = normalize_space(&space); let event = encode_dns_update(input) .map_err(|e| ClientError::Custom(format!("Parse error: {}", e)))?; - let result = cli.sign_event(space, event, false).await?; + let result = cli.sign_event(space, event, prefer_recent).await?; println!("{}", serde_json::to_string(&result).expect("result")); } Commands::RefreshAnchor {