use anyhow::Result;
use cdk::mint_url::MintUrl;
use cdk::nuts::CurrencyUnit;
use cdk::wallet::{payment_request as pr, NostrWaitInfo, WalletRepository};
use clap::Args;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub(super) struct StoredNostrWaitInfo {
pub(super) secret_key_hex: String,
pub(super) relays: Vec<String>,
pub(super) pubkey_hex: String,
#[serde(default)]
pub(super) mints: Vec<MintUrl>,
#[serde(default)]
pub(super) mint_preferred: Option<bool>,
}
impl StoredNostrWaitInfo {
pub(super) fn accepts_mint(&self, mint_url: &MintUrl) -> bool {
self.mints.is_empty() || self.mint_preferred == Some(true) || self.mints.contains(mint_url)
}
}
impl From<NostrWaitInfo> for StoredNostrWaitInfo {
fn from(info: NostrWaitInfo) -> Self {
Self {
secret_key_hex: info.keys.secret_key().to_secret_hex(),
relays: info.relays,
pubkey_hex: info.pubkey.to_hex(),
mints: info.mints,
mint_preferred: info.mint_preferred,
}
}
}
#[derive(Args)]
pub struct CreateRequestSubCommand {
#[arg(short, long)]
amount: Option<u64>,
description: Option<String>,
#[arg(long, action = clap::ArgAction::Append)]
pubkey: Option<Vec<String>>,
#[arg(long, default_value = "1")]
num_sigs: u64,
#[arg(long, conflicts_with = "preimage")]
hash: Option<String>,
#[arg(long, conflicts_with = "hash")]
preimage: Option<String>,
#[arg(long, default_value = "nostr")]
transport: String,
#[arg(long)]
http_url: Option<String>,
#[arg(long, action = clap::ArgAction::Append)]
nostr_relay: Option<Vec<String>>,
#[arg(long, action = clap::ArgAction::Append)]
mints: Option<Vec<String>>,
#[arg(long)]
mint_preferred: bool,
#[arg(short, long)]
bech32: bool,
}
pub async fn create_request(
wallet_repository: &WalletRepository,
sub_command_args: &CreateRequestSubCommand,
unit: &CurrencyUnit,
) -> Result<()> {
let params = pr::CreateRequestParams {
amount: sub_command_args.amount,
unit: unit.to_string(),
description: sub_command_args.description.clone(),
pubkeys: sub_command_args.pubkey.clone(),
num_sigs: sub_command_args.num_sigs,
hash: sub_command_args.hash.clone(),
preimage: sub_command_args.preimage.clone(),
transport: sub_command_args.transport.to_lowercase(),
http_url: sub_command_args.http_url.clone(),
nostr_relays: sub_command_args.nostr_relay.clone(),
mints: sub_command_args.mints.clone(),
mint_preferred: sub_command_args.mint_preferred.then_some(true),
};
let (req, nostr_wait) = wallet_repository.create_request(params).await?;
if sub_command_args.bech32 {
println!("{}", req.to_bech32_string()?);
} else {
println!("{}", req);
}
if let Some(info) = nostr_wait {
let key = info.pubkey.to_string();
if let Some(wallet) = wallet_repository.get_wallets().await.first() {
let serializable_info = StoredNostrWaitInfo::from(info.clone());
let val = serde_json::to_vec(&serializable_info)?;
wallet
.localstore
.kv_write("cdk_cli", "pending_nostr_requests", &key, &val)
.await?;
}
println!("Listening for payment via Nostr...");
let amount = wallet_repository.wait_for_nostr_payment(info).await?;
println!("Received {}", amount);
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
#[test]
fn stored_nostr_wait_info_enforces_strict_mints() {
let listed_mint = MintUrl::from_str("https://listed.example.com").expect("valid mint");
let unlisted_mint = MintUrl::from_str("https://unlisted.example.com").expect("valid mint");
let info = stored_info(vec![listed_mint.clone()], None);
assert!(info.accepts_mint(&listed_mint));
assert!(!info.accepts_mint(&unlisted_mint));
}
#[test]
fn stored_nostr_wait_info_allows_preferred_or_empty_mints() {
let listed_mint = MintUrl::from_str("https://listed.example.com").expect("valid mint");
let unlisted_mint = MintUrl::from_str("https://unlisted.example.com").expect("valid mint");
assert!(stored_info(vec![listed_mint], Some(true)).accepts_mint(&unlisted_mint));
assert!(stored_info(vec![], None).accepts_mint(&unlisted_mint));
}
#[test]
fn old_stored_nostr_wait_info_deserializes_with_empty_policy() {
let json = r#"{
"secret_key_hex":"secret",
"relays":["wss://relay.example.com"],
"pubkey_hex":"pubkey"
}"#;
let info: StoredNostrWaitInfo = serde_json::from_str(json).expect("old record");
assert!(info.mints.is_empty());
assert!(info.mint_preferred.is_none());
}
fn stored_info(mints: Vec<MintUrl>, mint_preferred: Option<bool>) -> StoredNostrWaitInfo {
StoredNostrWaitInfo {
secret_key_hex: "secret".to_string(),
relays: vec![],
pubkey_hex: "pubkey".to_string(),
mints,
mint_preferred,
}
}
}