Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BYO network transport when minting tokens #605

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
10 changes: 10 additions & 0 deletions crates/cashu/src/amount.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,16 @@ impl AmountStr {
pub(crate) fn from(amt: Amount) -> Self {
Self(amt)
}

pub fn inner(&self) -> Amount {
self.0
}
}

impl From<Amount> for AmountStr {
fn from(amt: Amount) -> Self {
Self(amt)
}
}

impl PartialOrd<Self> for AmountStr {
Expand Down
14 changes: 14 additions & 0 deletions crates/cdk-common/src/database/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::Debug;

use async_trait::async_trait;
use cashu::PreMintSecrets;

use super::Error;
use crate::common::ProofInfo;
Expand Down Expand Up @@ -117,4 +118,17 @@ pub trait Database: Debug {
verifying_key: PublicKey,
last_checked: u32,
) -> Result<(), Self::Err>;

/// Store premint secrets
async fn add_premint_secrets(
&self,
quote_id: &str,
premint_secrets: &PreMintSecrets,
) -> Result<(), Self::Err>;

/// Retrieve premint secrets
async fn get_premint_secrets(
&self,
quote_id: &str,
) -> Result<Option<PreMintSecrets>, Self::Err>;
}
18 changes: 17 additions & 1 deletion crates/cdk-redb/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ use cdk_common::mint_url::MintUrl;
use cdk_common::util::unix_time;
use cdk_common::wallet::{self, MintQuote};
use cdk_common::{
database, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PublicKey, SpendingConditions, State,
database, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PreMintSecrets, PublicKey,
SpendingConditions, State,
};
use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition};
use tracing::instrument;
Expand Down Expand Up @@ -740,4 +741,19 @@ impl WalletDatabase for WalletRedbDatabase {

Ok(())
}

async fn add_premint_secrets(
&self,
quote_id: &str,
premint_secrets: &PreMintSecrets,
) -> Result<(), Self::Err> {
todo!()
}

async fn get_premint_secrets(
&self,
quote_id: &str,
) -> Result<Option<PreMintSecrets>, Self::Err> {
todo!()
}
}
19 changes: 17 additions & 2 deletions crates/cdk-sqlite/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use cdk_common::nuts::{MeltQuoteState, MintQuoteState};
use cdk_common::secret::Secret;
use cdk_common::wallet::{self, MintQuote};
use cdk_common::{
database, Amount, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proof, PublicKey, SecretKey,
SpendingConditions, State,
database, Amount, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PreMintSecrets, Proof,
PublicKey, SecretKey, SpendingConditions, State,
};
use error::Error;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqliteRow};
Expand Down Expand Up @@ -778,6 +778,21 @@ VALUES (?, ?);

Ok(())
}

async fn add_premint_secrets(
&self,
quote_id: &str,
premint_secrets: &PreMintSecrets,
) -> Result<(), Self::Err> {
todo!()
}

async fn get_premint_secrets(
&self,
quote_id: &str,
) -> Result<Option<PreMintSecrets>, Self::Err> {
todo!()
}
}

fn sqlite_row_to_mint_info(row: &SqliteRow) -> Result<MintInfo, Error> {
Expand Down
22 changes: 22 additions & 0 deletions crates/cdk/src/cdk_database/wallet_memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::sync::Arc;

use async_trait::async_trait;
use cdk_common::database::{Error, WalletDatabase};
use cdk_common::PreMintSecrets;
use tokio::sync::RwLock;

use crate::mint_url::MintUrl;
Expand All @@ -28,6 +29,7 @@ pub struct WalletMemoryDatabase {
proofs: Arc<RwLock<HashMap<PublicKey, ProofInfo>>>,
keyset_counter: Arc<RwLock<HashMap<Id, u32>>>,
nostr_last_checked: Arc<RwLock<HashMap<PublicKey, u32>>>,
premint_secrets: Arc<RwLock<HashMap<String, PreMintSecrets>>>,

Choose a reason for hiding this comment

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

Why String for the HashMap key here?

Copy link
Contributor Author

@vnprc vnprc Feb 18, 2025

Choose a reason for hiding this comment

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

Honestly, I don't know if this is necessary. It would be great if I could get rid of the string entirely.

It works this way because I copied some code from somewhere else and it used a string as the index. This code comment seems to indicate using a string helps with sorting these items in a BTreeMap.

/// String wrapper for an [Amount].
///
/// It ser-/deserializes the inner [Amount] to a string, while at the same time using the [u64]
/// value of the [Amount] for comparison and ordering. This helps automatically sort the keys of
/// a [BTreeMap] when [AmountStr] is used as key.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AmountStr(Amount);

Copy link
Collaborator

@thesimplekid thesimplekid Feb 18, 2025

Choose a reason for hiding this comment

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

We're planning to remove this memory db so i wouldn't worry about this.

#607

AmountStr is used because the amount in the keyset response is a string not an int. https://github.com/cashubtc/nuts/blob/main/02.md#requesting-public-keys-for-a-specific-keyset

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I use AmountStr in hashpool in the same way. Is this the recommended way to put pubkeys in a BTreeMap?

https://github.com/vnprc/hashpool/blob/master/protocols/v2/subprotocols/mining/src/cashu.rs#L270

Copy link
Collaborator

@thesimplekid thesimplekid Feb 18, 2025

Choose a reason for hiding this comment

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

You can just use the Amount there no need to wrap it in the AmoutnStr. The reason we have to use AmountStr is for when the Keys response is json serialized it needs to be as a string and not an int, so that's really the only place we should use AmountStr everywhere else is Amount.

EDIT: I see you're creating a Keys which currently does expect the AmountStr

pub fn new(keys: BTreeMap<AmountStr, PublicKey>) -> Self {
. But since whats needed is as i described above we should change this and that can be an amount and we can just create a custom serialize deserilization, that maybe a better way to handle this and then we don't need the AmountStr type?

cc @crodas

}

impl WalletMemoryDatabase {
Expand All @@ -38,6 +40,7 @@ impl WalletMemoryDatabase {
mint_keys: Vec<Keys>,
keyset_counter: HashMap<Id, u32>,
nostr_last_checked: HashMap<PublicKey, u32>,
premint_secrets: HashMap<String, PreMintSecrets>,
) -> Self {
Self {
mints: Arc::new(RwLock::new(HashMap::new())),
Expand All @@ -55,6 +58,7 @@ impl WalletMemoryDatabase {
proofs: Arc::new(RwLock::new(HashMap::new())),
keyset_counter: Arc::new(RwLock::new(keyset_counter)),
nostr_last_checked: Arc::new(RwLock::new(nostr_last_checked)),
premint_secrets: Arc::new(RwLock::new(premint_secrets)),
}
}
}
Expand Down Expand Up @@ -353,4 +357,22 @@ impl WalletDatabase for WalletMemoryDatabase {

Ok(())
}

async fn add_premint_secrets(
&self,
quote_id: &str,
premint_secrets: &PreMintSecrets,
) -> Result<(), Self::Err> {
self.premint_secrets
.write()
.await
.insert(quote_id.to_string(), premint_secrets.clone());
Ok(())
}
async fn get_premint_secrets(
&self,
quote_id: &str,
) -> Result<Option<PreMintSecrets>, Self::Err> {
Ok(self.premint_secrets.read().await.get(quote_id).cloned())
}
}
128 changes: 128 additions & 0 deletions crates/cdk/src/wallet/keysets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,29 @@ impl Wallet {
Ok(keys)
}

/// Add a keyset to the local database and update keyset info
pub async fn add_keyset(
&self,
keys: Keys,
active: bool,
input_fee_ppk: u64,
) -> Result<(), Error> {
self.localstore.add_keys(keys.clone()).await?;

let keyset_info = KeySetInfo {
id: Id::from(&keys),
active,
unit: self.unit.clone(),
input_fee_ppk,
};

self.localstore
.add_mint_keysets(self.mint_url.clone(), vec![keyset_info])
.await?;

Ok(())
}

/// Get keysets for mint
///
/// Queries mint for all keysets
Expand Down Expand Up @@ -97,4 +120,109 @@ impl Wallet {
.ok_or(Error::NoActiveKeyset)?;
Ok(keyset_with_lowest_fee)
}

/// Get active keyset for mint from local without querying the mint
#[instrument(skip(self))]
pub async fn get_active_mint_keyset_local(&self) -> Result<KeySetInfo, Error> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Needs a better function name that indicates this function doesn't make any network calls.

let active_keysets = match self
.localstore
.get_mint_keysets(self.mint_url.clone())
.await?
{
Some(keysets) => keysets
.into_iter()
.filter(|k| k.active && k.unit == self.unit)
.collect::<Vec<KeySetInfo>>(),
None => {
vec![]
}
};

let keyset_with_lowest_fee = active_keysets
.into_iter()
.min_by_key(|key| key.input_fee_ppk)
.ok_or(Error::NoActiveKeyset)?;

Ok(keyset_with_lowest_fee)
}
}

#[cfg(test)]
mod test {
use crate::cdk_database::WalletMemoryDatabase;
use crate::nuts;
use crate::Wallet;
use bitcoin::bip32::DerivationPath;
use bitcoin::bip32::Xpriv;
use bitcoin::key::Secp256k1;
use cdk_common::KeySet;
use cdk_common::KeySetInfo;
use cdk_common::MintKeySet;
use nuts::CurrencyUnit;
use std::sync::Arc;

fn create_new_keyset() -> (KeySet, KeySetInfo) {
let secp = Secp256k1::new();
let seed = [0u8; 32]; // Default seed for testing
let xpriv = Xpriv::new_master(bitcoin::Network::Bitcoin, &seed).expect("RNG busted");

let derivation_path = DerivationPath::default();
let unit = CurrencyUnit::Custom("HASH".to_string());
let max_order = 64;

let keyset: KeySet = MintKeySet::generate(
&secp,
xpriv
.derive_priv(&secp, &derivation_path)
.expect("RNG busted"),
unit.clone(),
max_order,
)
.into();

let keyset_info = KeySetInfo {
id: keyset.id,
unit: keyset.unit.clone(),
active: true,
input_fee_ppk: 0,
};

(keyset, keyset_info)
}

fn create_wallet() -> Wallet {
use rand::Rng;

let seed = rand::thread_rng().gen::<[u8; 32]>();
let mint_url = "https://testnut.cashu.space";

let localstore = WalletMemoryDatabase::default();
Wallet::new(
mint_url,
CurrencyUnit::Custom("HASH".to_string()),
Arc::new(localstore),
&seed,
None,
)
.unwrap()
}

#[tokio::test]
async fn test_add_and_get_active_mint_keysets_local() {
let (keyset, keyset_info) = create_new_keyset();

let wallet = create_wallet();

// Add the keyset
wallet.add_keyset(keyset.keys, true, 0).await.unwrap();

// Retrieve the keysets locally
let active_keyset = wallet.get_active_mint_keyset_local().await.unwrap();

// Validate the retrieved keyset
assert_eq!(active_keyset.id, keyset_info.id);
assert_eq!(active_keyset.active, keyset_info.active);
assert_eq!(active_keyset.unit, keyset_info.unit);
assert_eq!(active_keyset.input_fee_ppk, keyset_info.input_fee_ppk);
}
}
Loading
Loading