Skip to content
Closed
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
5 changes: 5 additions & 0 deletions crates/cdk-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ enum Commands {
Resolve(sub_commands::resolve::ResolveSubCommand),
/// Create Payment request
CreateRequest(sub_commands::create_request::CreateRequestSubCommand),
/// Manage P2PK signing keys (generate, store, list, remove)
P2pk(sub_commands::p2pk::P2pkSubCommand),
/// Mint blind auth proofs
MintBlindAuth(sub_commands::mint_blind_auth::MintBlindAuthSubCommand),
/// Cat login with username/password
Expand Down Expand Up @@ -327,6 +329,9 @@ async fn main() -> Result<()> {
)
.await
}
Commands::P2pk(sub_command_args) => {
sub_commands::p2pk::p2pk(&wallet_repository, sub_command_args, &currency_unit).await
}
Commands::MintBlindAuth(sub_command_args) => {
sub_commands::mint_blind_auth::mint_blind_auth(
&wallet_repository,
Expand Down
1 change: 1 addition & 0 deletions crates/cdk-cli/src/sub_commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub mod mint_blind_auth;
pub mod mint_info;
#[cfg(feature = "npubcash")]
pub mod npubcash;
pub mod p2pk;
pub mod pay_request;
pub mod pending_mints;
pub mod receive;
Expand Down
77 changes: 77 additions & 0 deletions crates/cdk-cli/src/sub_commands/p2pk.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use std::str::FromStr;

use anyhow::{anyhow, Result};
use cdk::nuts::{CurrencyUnit, PublicKey, SecretKey};
use cdk::wallet::WalletRepository;
use clap::{Args, Subcommand};

use crate::utils::get_or_create_wallet;

#[derive(Args)]
pub struct P2pkSubCommand {
/// Mint URL (required to obtain a wallet context)
#[arg(short, long)]
mint_url: String,
#[command(subcommand)]
command: P2pkCommands,
}

#[derive(Subcommand)]
pub enum P2pkCommands {
/// Generate a new P2PK signing key and store it in the wallet
Generate,
/// Store an existing P2PK signing key
Store {
/// Secret key in hex format
secret_key: String,
},
/// List all stored P2PK signing keys (shows public keys)
List,
/// Remove a stored P2PK signing key by its public key
Remove {
/// Public key in hex format
pubkey: String,
},
}

pub async fn p2pk(
wallet_repository: &WalletRepository,
sub_command_args: &P2pkSubCommand,
unit: &CurrencyUnit,
) -> Result<()> {
let mint_url = cdk::mint_url::MintUrl::from_str(&sub_command_args.mint_url)?;
let wallet = get_or_create_wallet(wallet_repository, &mint_url, unit).await?;

match &sub_command_args.command {
P2pkCommands::Generate => {
let pubkey = wallet.generate_p2pk_key().await?;
println!("Generated P2PK key:");
println!(" Public key: {}", pubkey.to_hex());
}
P2pkCommands::Store { secret_key } => {
let sk =
SecretKey::from_hex(secret_key).map_err(|e| anyhow!("Invalid secret key: {e}"))?;
let pubkey = wallet.store_p2pk_key(sk).await?;
println!("Stored P2PK key:");
println!(" Public key: {}", pubkey.to_hex());
}
P2pkCommands::List => {
let keys = wallet.get_p2pk_signing_keys().await?;
if keys.is_empty() {
println!("No P2PK signing keys stored.");
} else {
println!("Stored P2PK signing keys ({}):", keys.len());
for xonly in keys.keys() {
println!(" {xonly}");
}
}
}
P2pkCommands::Remove { pubkey } => {
let pk = PublicKey::from_hex(pubkey).map_err(|e| anyhow!("Invalid public key: {e}"))?;
wallet.remove_p2pk_key(&pk).await?;
println!("Removed P2PK key: {pubkey}");
}
}

Ok(())
}
101 changes: 101 additions & 0 deletions crates/cdk-integration-tests/tests/test_p2pk_autosign.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//! P2PK Auto-Sign Integration Tests
//!
//! Tests that stored P2PK signing keys are automatically used when receiving
//! tokens locked to those keys, without requiring manual key provision.

use anyhow::Result;
use cdk::nuts::SpendingConditions;
use cdk::wallet::{ReceiveOptions, SendOptions};
use cdk::Amount;
use cdk_integration_tests::init_pure_tests::*;

/// Tests the full P2PK auto-sign flow:
/// 1. Receiver generates and stores a P2PK key
/// 2. Sender sends tokens locked to that key
/// 3. Receiver receives tokens WITHOUT providing signing keys manually
/// 4. Verifies balance increased (auto-sign worked)
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_p2pk_autosign_receive() -> Result<()> {
setup_tracing();
let mint = create_and_start_test_mint().await?;

let sender = create_test_wallet_for_mint(mint.clone()).await?;
let receiver = create_test_wallet_for_mint(mint.clone()).await?;

// Fund sender
let fund_amount = 1000_u64;
fund_wallet(sender.clone(), fund_amount, None).await?;

// Receiver generates and stores a P2PK key
let receiver_pubkey = receiver.generate_p2pk_key().await?;

// Sender sends tokens locked to receiver's pubkey
let send_amount = Amount::from(500_u64);
let conditions = SpendingConditions::new_p2pk(receiver_pubkey, None);
let prepared = sender
.prepare_send(
send_amount,
SendOptions {
conditions: Some(conditions),
..Default::default()
},
)
.await?;
let token = prepared.confirm(None).await?;

// Receiver receives WITHOUT providing signing keys — should auto-sign
let received = receiver
.receive(&token.to_string(), ReceiveOptions::default())
.await?;

assert!(received > Amount::ZERO, "Should have received some amount");

// Verify receiver balance
let balance = receiver.total_balance().await?;
assert!(
balance > Amount::ZERO,
"Receiver balance should be positive"
);

Ok(())
}

/// Tests that receive fails when token is locked to an unknown key
/// and no signing keys are provided (negative case)
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_p2pk_receive_fails_without_stored_key() -> Result<()> {
setup_tracing();
let mint = create_and_start_test_mint().await?;

let sender = create_test_wallet_for_mint(mint.clone()).await?;
let receiver = create_test_wallet_for_mint(mint.clone()).await?;

// Fund sender
fund_wallet(sender.clone(), 1000, None).await?;

// Generate a key but DON'T store it in the receiver wallet
let secret_key = cdk::nuts::SecretKey::generate();
let pubkey = secret_key.public_key();

// Send locked to that key
let conditions = SpendingConditions::new_p2pk(pubkey, None);
let prepared = sender
.prepare_send(
Amount::from(500_u64),
SendOptions {
conditions: Some(conditions),
..Default::default()
},
)
.await?;
let token = prepared.confirm(None).await?;

// Receiver tries to receive without any signing key — should fail
let result = receiver
.receive(&token.to_string(), ReceiveOptions::default())
.await;

assert!(result.is_err(), "Should fail without signing key");

Ok(())
}
1 change: 1 addition & 0 deletions crates/cdk/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ mod mint_connector;
mod mint_metadata_cache;
#[cfg(feature = "npubcash")]
mod npubcash;
mod p2pk_storage;
pub mod payment_request;
mod proofs;
mod receive;
Expand Down
Loading
Loading