Skip to content
Open
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
6 changes: 5 additions & 1 deletion crates/cashu/src/nuts/nut10.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub(crate) struct SpendingRequirements {
pub refund_path: Option<RefundPath>,
}

/// NUT13 Error
/// NUT10 Error
#[derive(Debug, Error)]
pub enum Error {
/// Secret error
Expand All @@ -48,6 +48,10 @@ pub enum Error {
/// Serde Json error
#[error(transparent)]
SerdeJsonError(#[from] serde_json::Error),

/// Proof does not contain enough signature for lock
#[error("proof does not contain enough proofs to be spendable")]
NotEnoughSignatures,
}

/// NUT10 Secret Kind
Expand Down
2 changes: 0 additions & 2 deletions crates/cashu/src/nuts/nut11/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -488,8 +488,6 @@ pub struct Conditions {
#[serde(skip_serializing_if = "Option::is_none")]
pub refund_keys: Option<Vec<PublicKey>>,
/// Number of signatures required
///
/// Default is 1
#[serde(skip_serializing_if = "Option::is_none")]
pub num_sigs: Option<u64>,
/// Signature flag
Expand Down
15 changes: 15 additions & 0 deletions crates/cdk-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,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 @@ -326,5 +330,16 @@ async fn main() -> Result<()> {
)
.await
}
Commands::GeneratePublicKey(sub_command_args) => {
sub_commands::generate_public_key::generate_public_key(
&multi_mint_wallet,
sub_command_args,
)
.await
}
Commands::GetPublicKeys(sub_command_args) => {
sub_commands::get_public_keys::get_public_keys(&multi_mint_wallet, sub_command_args)
.await
}
}
}
18 changes: 18 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,18 @@
use anyhow::Result;
use cdk::wallet::MultiMintWallet;
use clap::Args;

#[derive(Args)]
pub struct GeneratePublicKeySubCommand {}

pub async fn generate_public_key(
multi_mint_wallet: &MultiMintWallet,
_sub_command_args: &GeneratePublicKeySubCommand,
) -> Result<()> {
let public_key = multi_mint_wallet.generate_public_key().await?;

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

Ok(())
}
44 changes: 44 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,44 @@
use anyhow::Result;
use cdk::wallet::MultiMintWallet;
use clap::Args;

#[derive(Args)]
pub struct GetPublicKeysSubCommand {
/// Show the latest public key
#[arg(long)]
pub latest: bool,
}

pub async fn get_public_keys(
multi_mint_wallet: &MultiMintWallet,
sub_command_args: &GetPublicKeysSubCommand,
) -> Result<()> {
if sub_command_args.latest {
let latest_public_key = multi_mint_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 = multi_mint_wallet.get_public_keys().await?;
if list_public_keys.is_empty() {
println!("\npublic not found!\n");
}
println!("\npublic keys found:\n");
for public_key in list_public_keys {
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 @@ -6,6 +6,8 @@ pub mod check_pending;
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 @@ -168,4 +169,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>;
}
125 changes: 124 additions & 1 deletion crates/cdk-common/src/database/wallet/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use std::str::FromStr;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};

use bitcoin::bip32::DerivationPath;
use cashu::nut00::KnownMethod;
use cashu::secret::Secret;
use cashu::{Amount, CurrencyUnit, SecretKey};
Expand Down Expand Up @@ -1019,6 +1020,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);
}

/// Unit test that is expected to be passed for a correct wallet database implementation
#[macro_export]
Expand Down Expand Up @@ -1059,7 +1177,12 @@ macro_rules! wallet_db_test {
kvstore_list,
kvstore_update,
kvstore_remove,
kvstore_namespace_isolation
kvstore_namespace_isolation,
add_and_get_p2pk_key,
list_p2pk_keys_empty,
list_p2pk_keys_multiple,
latest_p2pk_empty,
latest_p2pk_with_keys
);
};
($make_db_fn:ident, $($name:ident),+ $(,)?) => {
Expand Down
13 changes: 13 additions & 0 deletions crates/cdk-common/src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;

use bitcoin::bip32::DerivationPath;
use bitcoin::hashes::{sha256, Hash, HashEngine};
use cashu::util::hex;
use cashu::{nut00, PaymentMethod, Proofs, PublicKey};
Expand Down Expand Up @@ -374,6 +375,18 @@ impl TryFrom<Proofs> for TransactionId {
}
}

#[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