Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
3 changes: 3 additions & 0 deletions crates/cashu/src/nuts/nut10/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ pub enum Error {
/// Spend conditions not met
#[error("Spend conditions are not met")]
SpendConditionsNotMet,
/// Proof does not contain enough signature for lock
#[error("proof does not contain enough proofs to be spendable")]
NotEnoughSignatures,

/// From hex error
#[error(transparent)]
Expand Down
20 changes: 20 additions & 0 deletions crates/cdk-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ enum Commands {
#[command(subcommand)]
command: sub_commands::npubcash::NpubCashSubCommand,
},
/// Generate a public key
GeneratePublicKey(sub_commands::generate_public_key::GeneratePublicKeySubCommand),
/// Get public keys
GetPublicKeys(sub_commands::get_public_keys::GetPublicKeysSubCommand),
}

#[tokio::main]
Expand Down Expand Up @@ -358,5 +362,21 @@ async fn main() -> Result<()> {
)
.await
}
Commands::GeneratePublicKey(sub_command_args) => {
sub_commands::generate_public_key::generate_public_key(
&wallet_repository,
sub_command_args,
&currency_unit,
)
.await
}
Commands::GetPublicKeys(sub_command_args) => {
sub_commands::get_public_keys::get_public_keys(
&wallet_repository,
sub_command_args,
&currency_unit,
)
.await
}
}
}
44 changes: 44 additions & 0 deletions crates/cdk-cli/src/sub_commands/generate_public_key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use std::str::FromStr;

use anyhow::Result;
use cdk::mint_url::MintUrl;
use cdk::nuts::CurrencyUnit;
use cdk::wallet::WalletRepository;
use clap::Args;

use crate::utils::get_or_create_wallet;

#[derive(Args)]
pub struct GeneratePublicKeySubCommand {
/// Mint URL to select wallet context
#[arg(long)]
mint_url: Option<String>,
}

pub async fn generate_public_key(
wallet_repository: &WalletRepository,
sub_command_args: &GeneratePublicKeySubCommand,
unit: &CurrencyUnit,
) -> Result<()> {
let mint_url = match &sub_command_args.mint_url {
Some(url) => MintUrl::from_str(url)?,
None => {
let wallets = wallet_repository.get_wallets().await;
wallets
.iter()
.find(|wallet| &wallet.unit == unit)
.map(|wallet| wallet.mint_url.clone())
.ok_or_else(|| {
anyhow::anyhow!("No wallet found for unit {}. Use --mint-url.", unit)
})?
}
};

let wallet = get_or_create_wallet(wallet_repository, &mint_url, unit).await?;
let public_key = wallet.generate_public_key().await?;

println!("\npublic key generated!\n");
println!("public key: {}", public_key.to_hex());

Ok(())
}
70 changes: 70 additions & 0 deletions crates/cdk-cli/src/sub_commands/get_public_keys.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use std::str::FromStr;

use anyhow::Result;
use cdk::mint_url::MintUrl;
use cdk::nuts::CurrencyUnit;
use cdk::wallet::WalletRepository;
use clap::Args;

use crate::utils::get_or_create_wallet;

#[derive(Args)]
pub struct GetPublicKeysSubCommand {
/// Show the latest public key
#[arg(long)]
pub latest: bool,
/// Mint URL to select wallet context
#[arg(long)]
mint_url: Option<String>,
}

pub async fn get_public_keys(
wallet_repository: &WalletRepository,
sub_command_args: &GetPublicKeysSubCommand,
unit: &CurrencyUnit,
) -> Result<()> {
let mint_url = match &sub_command_args.mint_url {
Some(url) => MintUrl::from_str(url)?,
None => {
let wallets = wallet_repository.get_wallets().await;
wallets
.iter()
.find(|wallet| &wallet.unit == unit)
.map(|wallet| wallet.mint_url.clone())
.ok_or_else(|| {
anyhow::anyhow!("No wallet found for unit {}. Use --mint-url.", unit)
})?
}
};

let wallet = get_or_create_wallet(wallet_repository, &mint_url, unit).await?;

if sub_command_args.latest {
let latest_public_key = wallet.get_latest_public_key().await?;

match latest_public_key {
Some(key) => {
println!("\npublic key found!\n");

println!("public key: {}", key.pubkey.to_hex());
println!("derivation path: {}", key.derivation_path);
}
None => {
println!("\npublic key not found!\n");
}
}

return Ok(());
}

let list_public_keys = wallet.get_public_keys().await?;
if list_public_keys.is_empty() {
println!("\npublic not found!\n");
Comment thread
thesimplekid marked this conversation as resolved.
Outdated
}
println!("\npublic keys found:\n");
for public_key in list_public_keys {
Comment thread
lescuer97 marked this conversation as resolved.
println!("public key: {}", public_key.pubkey.to_hex());
println!("derivation path: {}", public_key.derivation_path);
}
Ok(())
}
2 changes: 2 additions & 0 deletions crates/cdk-cli/src/sub_commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ pub mod check_requests;
pub mod create_request;
pub mod decode_request;
pub mod decode_token;
pub mod generate_public_key;
pub mod get_public_keys;
pub mod list_mint_proofs;
pub mod melt;
pub mod mint;
Expand Down
21 changes: 21 additions & 0 deletions crates/cdk-common/src/database/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::collections::HashMap;
use std::fmt::Debug;

use async_trait::async_trait;
use bitcoin::bip32::DerivationPath;
use cashu::KeySet;

use super::Error;
Expand Down Expand Up @@ -233,4 +234,24 @@ where
secondary_namespace: &str,
key: &str,
) -> Result<(), Err>;

// P2PK signing key methods

/// Store a P2PK signing key for the wallet
async fn add_p2pk_key(
&self,
pubkey: &PublicKey,
derivation_path: DerivationPath,
derivation_index: u32,
) -> Result<(), Err>;

/// Get a stored P2PK signing key by pubkey.
async fn get_p2pk_key(&self, pubkey: &PublicKey)
-> Result<Option<wallet::P2PKSigningKey>, Err>;

/// List all stored P2PK signing keys.
async fn list_p2pk_keys(&self) -> Result<Vec<wallet::P2PKSigningKey>, Err>;

/// Tries to get the latest p2pk key generated
async fn latest_p2pk(&self) -> Result<Option<wallet::P2PKSigningKey>, Err>;
}
118 changes: 118 additions & 0 deletions crates/cdk-common/src/database/wallet/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::collections::{BTreeMap, HashMap};
use std::str::FromStr;
use std::sync::atomic::{AtomicU64, Ordering};

use bitcoin::bip32::DerivationPath;
use cashu::nut00::KnownMethod;
use cashu::secret::Secret;
use cashu::{Amount, CurrencyUnit, MeltQuoteState, MintQuoteState, SecretKey};
Expand Down Expand Up @@ -1124,6 +1125,123 @@ where
let value3 = db.kv_read("ns1", "sub2", "key").await.unwrap();
assert_eq!(value3, Some(b"value_sub2".to_vec()));
}
/// Test adding and retrieving a P2PK signing key
pub async fn add_and_get_p2pk_key<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let pubkey = SecretKey::generate().public_key();
let derivation_path = DerivationPath::from_str("m/0'/0'/0'").unwrap();
let derivation_index = 0u32;

// Add P2PK key
db.add_p2pk_key(&pubkey, derivation_path.clone(), derivation_index)
.await
.unwrap();

// Retrieve the key
let retrieved = db.get_p2pk_key(&pubkey).await.unwrap();
assert!(retrieved.is_some());
let retrieved_key = retrieved.unwrap();
assert_eq!(retrieved_key.pubkey, pubkey);
assert_eq!(retrieved_key.derivation_path, derivation_path);
assert_eq!(retrieved_key.derivation_index, derivation_index);

// Test getting a non-existent key
let non_existent_pubkey = SecretKey::generate().public_key();
let result = db.get_p2pk_key(&non_existent_pubkey).await.unwrap();
assert!(result.is_none());
}

/// Test that list_p2pk_keys returns empty vector on fresh database
pub async fn list_p2pk_keys_empty<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let keys = db.list_p2pk_keys().await.unwrap();
assert!(keys.is_empty());
}

/// Test listing multiple P2PK signing keys
pub async fn list_p2pk_keys_multiple<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
// Add multiple keys with different derivation indices
let pubkey1 = SecretKey::generate().public_key();
let pubkey2 = SecretKey::generate().public_key();
let pubkey3 = SecretKey::generate().public_key();

db.add_p2pk_key(&pubkey1, DerivationPath::from_str("m/0'/0'/0'").unwrap(), 0)
.await
.unwrap();

db.add_p2pk_key(&pubkey2, DerivationPath::from_str("m/0'/0'/1'").unwrap(), 1)
.await
.unwrap();

db.add_p2pk_key(&pubkey3, DerivationPath::from_str("m/0'/0'/2'").unwrap(), 2)
.await
.unwrap();

// List all keys
let keys = db.list_p2pk_keys().await.unwrap();
assert_eq!(keys.len(), 3);

// Verify all keys are present
let pubkeys: Vec<_> = keys.iter().map(|k| k.pubkey).collect();
assert!(pubkeys.contains(&pubkey1));
assert!(pubkeys.contains(&pubkey2));
assert!(pubkeys.contains(&pubkey3));

// Verify derivation indices are correct
let derivation_indices: Vec<_> = keys.iter().map(|k| k.derivation_index).collect();
assert!(derivation_indices.contains(&0));
assert!(derivation_indices.contains(&1));
assert!(derivation_indices.contains(&2));
}

/// Test that latest_p2pk returns None on fresh database
pub async fn latest_p2pk_empty<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let latest = db.latest_p2pk().await.unwrap();
assert!(latest.is_none());
}

/// Test getting the latest P2PK signing key
pub async fn latest_p2pk_with_keys<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
// Add multiple keys with delays to ensure different timestamps
let pubkey1 = SecretKey::generate().public_key();
let pubkey2 = SecretKey::generate().public_key();
let pubkey3 = SecretKey::generate().public_key();

db.add_p2pk_key(&pubkey1, DerivationPath::from_str("m/0'/0'/0'").unwrap(), 0)
.await
.unwrap();

db.add_p2pk_key(&pubkey2, DerivationPath::from_str("m/0'/0'/1'").unwrap(), 1)
.await
.unwrap();

// Wait 1 second to ensure the last key has a different (newer) timestamp
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;

db.add_p2pk_key(&pubkey3, DerivationPath::from_str("m/0'/0'/2'").unwrap(), 2)
.await
.unwrap();

// Get latest key - should be the most recently created (pubkey3)
let latest = db.latest_p2pk().await.unwrap();
assert!(latest.is_some());
let latest_key = latest.unwrap();
assert_eq!(latest_key.pubkey, pubkey3);
assert_eq!(latest_key.derivation_index, 2);
}

// =============================================================================
// Wallet Saga Tests
Expand Down
14 changes: 14 additions & 0 deletions crates/cdk-common/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::fmt;
use std::str::FromStr;

use async_trait::async_trait;
use bitcoin::bip32::DerivationPath;
use bitcoin::hashes::{sha256, Hash, HashEngine};
use cashu::amount::SplitTarget;
use cashu::nuts::nut07::ProofState;
Expand Down Expand Up @@ -952,6 +953,19 @@ pub trait Wallet: Send + Sync {
async fn get_proofs_by_states(&self, states: Vec<State>) -> Result<Proofs, Self::Error>;
}

/// Public key generated for proof signing
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct P2PKSigningKey {
/// Public key
pub pubkey: PublicKey,
/// Derivation path
pub derivation_path: DerivationPath,
/// Derivation index
pub derivation_index: u32,
/// Created time
pub created_time: u64,
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading
Loading