Skip to content
Merged
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
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(())
}
71 changes: 71 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,71 @@
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!("\n public not found! \n");
return Ok(());
}
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
33 changes: 33 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 @@ -950,6 +951,38 @@ pub trait Wallet: Send + Sync {
/// The `Spent` state is typically excluded since spent proofs are removed
/// from the database.
async fn get_proofs_by_states(&self, states: Vec<State>) -> Result<Proofs, Self::Error>;

// P2PK proofs
/// generates and stores public key in database
async fn generate_public_key(&self) -> Result<PublicKey, Self::Error>;

/// gets public key by it's hex value
async fn get_public_key(
&self,
pubkey: &PublicKey,
) -> Result<Option<P2PKSigningKey>, Self::Error>;

/// gets list of stored public keys in database
async fn get_public_keys(&self) -> Result<Vec<P2PKSigningKey>, Self::Error>;

/// Gets the latest generated P2PK signing key (most recently created)
async fn get_latest_public_key(&self) -> Result<Option<P2PKSigningKey>, Self::Error>;

/// try to get secret key from p2pk signing key in localstore
async fn get_signing_key(&self, pubkey: &PublicKey) -> Result<Option<SecretKey>, Self::Error>;
Comment on lines +955 to +972

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought. And thinking about how we want to reduce our public api surface, maybe only generate and get latest should be in the public api and the other fns should be on the wallet as pub(crate) this way they can be removed from the trait and ffi?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the 2 that we could remove from the public are get_signing_key and get_public_keys. the others do need to there

}

/// 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)]
Expand Down
Loading
Loading