Эх сурвалжийг харах

feat: new struct 'MintUrl' which trims trailing slashes (#283)

https://github.com/cashubtc/nuts/pull/151/files
Caleb Beery 7 сар өмнө
parent
commit
40554987e1

+ 3 - 3
crates/cdk-cli/src/sub_commands/balance.rs

@@ -1,8 +1,8 @@
 use std::collections::HashMap;
 
 use anyhow::Result;
+use cdk::mint_url::MintUrl;
 use cdk::nuts::CurrencyUnit;
-use cdk::url::UncheckedUrl;
 use cdk::wallet::multi_mint_wallet::MultiMintWallet;
 use cdk::Amount;
 
@@ -14,8 +14,8 @@ pub async fn balance(multi_mint_wallet: &MultiMintWallet) -> Result<()> {
 pub async fn mint_balances(
     multi_mint_wallet: &MultiMintWallet,
     unit: &CurrencyUnit,
-) -> Result<Vec<(UncheckedUrl, Amount)>> {
-    let wallets: HashMap<UncheckedUrl, Amount> = multi_mint_wallet.get_balances(unit).await?;
+) -> Result<Vec<(MintUrl, Amount)>> {
+    let wallets: HashMap<MintUrl, Amount> = multi_mint_wallet.get_balances(unit).await?;
 
     let mut wallets_vec = Vec::with_capacity(wallets.capacity());
 

+ 3 - 2
crates/cdk-cli/src/sub_commands/burn.rs

@@ -1,16 +1,17 @@
 use std::str::FromStr;
 
 use anyhow::Result;
+use cdk::mint_url::MintUrl;
 use cdk::nuts::CurrencyUnit;
 use cdk::wallet::multi_mint_wallet::WalletKey;
 use cdk::wallet::MultiMintWallet;
-use cdk::{Amount, UncheckedUrl};
+use cdk::Amount;
 use clap::Args;
 
 #[derive(Args)]
 pub struct BurnSubCommand {
     /// Mint Url
-    mint_url: Option<UncheckedUrl>,
+    mint_url: Option<MintUrl>,
     /// Currency unit e.g. sat
     #[arg(default_value = "sat")]
     unit: String,

+ 2 - 2
crates/cdk-cli/src/sub_commands/mint.rs

@@ -5,8 +5,8 @@ use std::time::Duration;
 use anyhow::Result;
 use cdk::amount::SplitTarget;
 use cdk::cdk_database::{Error, WalletDatabase};
+use cdk::mint_url::MintUrl;
 use cdk::nuts::{CurrencyUnit, MintQuoteState};
-use cdk::url::UncheckedUrl;
 use cdk::wallet::multi_mint_wallet::WalletKey;
 use cdk::wallet::{MultiMintWallet, Wallet};
 use cdk::Amount;
@@ -16,7 +16,7 @@ use tokio::time::sleep;
 #[derive(Args)]
 pub struct MintSubCommand {
     /// Mint url
-    mint_url: UncheckedUrl,
+    mint_url: MintUrl,
     /// Amount
     amount: u64,
     /// Currency unit e.g. sat

+ 2 - 2
crates/cdk-cli/src/sub_commands/mint_info.rs

@@ -1,5 +1,5 @@
 use anyhow::Result;
-use cdk::url::UncheckedUrl;
+use cdk::mint_url::MintUrl;
 use cdk::HttpClient;
 use clap::Args;
 use url::Url;
@@ -7,7 +7,7 @@ use url::Url;
 #[derive(Args)]
 pub struct MintInfoSubcommand {
     /// Cashu Token
-    mint_url: UncheckedUrl,
+    mint_url: MintUrl,
 }
 
 pub async fn mint_info(proxy: Option<Url>, sub_command_args: &MintInfoSubcommand) -> Result<()> {

+ 2 - 2
crates/cdk-cli/src/sub_commands/restore.rs

@@ -1,8 +1,8 @@
 use std::str::FromStr;
 
 use anyhow::{anyhow, Result};
+use cdk::mint_url::MintUrl;
 use cdk::nuts::CurrencyUnit;
-use cdk::url::UncheckedUrl;
 use cdk::wallet::multi_mint_wallet::WalletKey;
 use cdk::wallet::MultiMintWallet;
 use clap::Args;
@@ -10,7 +10,7 @@ use clap::Args;
 #[derive(Args)]
 pub struct RestoreSubCommand {
     /// Mint Url
-    mint_url: UncheckedUrl,
+    mint_url: MintUrl,
     /// Currency unit e.g. sat
     #[arg(default_value = "sat")]
     unit: String,

+ 3 - 3
crates/cdk-cli/src/sub_commands/update_mint_url.rs

@@ -1,6 +1,6 @@
 use anyhow::{anyhow, Result};
+use cdk::mint_url::MintUrl;
 use cdk::nuts::CurrencyUnit;
-use cdk::url::UncheckedUrl;
 use cdk::wallet::multi_mint_wallet::WalletKey;
 use cdk::wallet::MultiMintWallet;
 use clap::Args;
@@ -8,9 +8,9 @@ use clap::Args;
 #[derive(Args)]
 pub struct UpdateMintUrlSubCommand {
     /// Old Mint Url
-    old_mint_url: UncheckedUrl,
+    old_mint_url: MintUrl,
     /// New Mint Url
-    new_mint_url: UncheckedUrl,
+    new_mint_url: MintUrl,
 }
 
 pub async fn update_mint_url(

+ 3 - 0
crates/cdk-redb/src/error.rs

@@ -34,6 +34,9 @@ pub enum Error {
     /// CDK Database Error
     #[error(transparent)]
     CDKDatabase(#[from] cdk::cdk_database::Error),
+    /// CDK Mint Url Error
+    #[error(transparent)]
+    CDKMintUrl(#[from] cdk::mint_url::Error),
     /// CDK Error
     #[error(transparent)]
     CDK(#[from] cdk::error::Error),

+ 4 - 3
crates/cdk-redb/src/migrations.rs

@@ -1,8 +1,9 @@
 use std::collections::HashMap;
 use std::sync::Arc;
 
+use cdk::mint_url::MintUrl;
 use cdk::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState};
-use cdk::{Amount, UncheckedUrl};
+use cdk::Amount;
 use redb::{Database, ReadableTable, TableDefinition};
 use serde::{Deserialize, Serialize};
 
@@ -15,7 +16,7 @@ const MELT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mel
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub struct V0MintQuote {
     pub id: String,
-    pub mint_url: UncheckedUrl,
+    pub mint_url: MintUrl,
     pub amount: Amount,
     pub unit: CurrencyUnit,
     pub request: String,
@@ -27,7 +28,7 @@ pub struct V0MintQuote {
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub struct V1MintQuote {
     pub id: String,
-    pub mint_url: UncheckedUrl,
+    pub mint_url: MintUrl,
     pub amount: Amount,
     pub unit: CurrencyUnit,
     pub request: String,

+ 4 - 2
crates/cdk-redb/src/mint/migrations.rs

@@ -4,8 +4,9 @@ use std::str::FromStr;
 use std::sync::Arc;
 
 use cdk::mint::MintQuote;
+use cdk::mint_url::MintUrl;
 use cdk::nuts::{CurrencyUnit, MintQuoteState, Proof, State};
-use cdk::{Amount, UncheckedUrl};
+use cdk::Amount;
 use lightning_invoice::Bolt11Invoice;
 use redb::{Database, ReadableTable, TableDefinition};
 use serde::{Deserialize, Serialize};
@@ -26,11 +27,12 @@ pub fn migrate_02_to_03(db: Arc<Database>) -> Result<u32, Error> {
     migrate_mint_proofs_02_to_03(db)?;
     Ok(3)
 }
+
 /// Mint Quote Info
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 struct V1MintQuote {
     pub id: String,
-    pub mint_url: UncheckedUrl,
+    pub mint_url: MintUrl,
     pub amount: Amount,
     pub unit: CurrencyUnit,
     pub request: String,

+ 100 - 0
crates/cdk-redb/src/wallet/migrations.rs

@@ -0,0 +1,100 @@
+//! Wallet Migrations
+use std::ops::Deref;
+use std::str::FromStr;
+use std::sync::Arc;
+
+use cdk::mint_url::MintUrl;
+use redb::{
+    Database, MultimapTableDefinition, ReadableMultimapTable, ReadableTable, TableDefinition,
+};
+
+use super::Error;
+
+// <Mint_url, Info>
+const MINTS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mints_table");
+// <Mint_Url, Keyset_id>
+const MINT_KEYSETS_TABLE: MultimapTableDefinition<&str, &[u8]> =
+    MultimapTableDefinition::new("mint_keysets");
+
+pub fn migrate_01_to_02(db: Arc<Database>) -> Result<u32, Error> {
+    migrate_trim_mint_urls_01_to_02(db)?;
+    Ok(2)
+}
+
+fn migrate_mints_table_01_to_02(db: Arc<Database>) -> Result<(), Error> {
+    let mints: Vec<(String, String)>;
+    {
+        let read_txn = db.begin_read().map_err(Error::from)?;
+        let table = read_txn.open_table(MINTS_TABLE).map_err(Error::from)?;
+
+        mints = table
+            .iter()
+            .map_err(Error::from)?
+            .flatten()
+            .map(|(mint_url, mint_info)| {
+                (mint_url.value().to_string(), mint_info.value().to_string())
+            })
+            .collect();
+    }
+
+    let write_txn = db.begin_write()?;
+    {
+        let mut table = write_txn.open_table(MINTS_TABLE).map_err(Error::from)?;
+        for (mint_url_str, info) in mints {
+            let mint_url = MintUrl::from_str(&mint_url_str).map_err(Error::from)?;
+
+            table.remove(mint_url_str.as_str())?;
+
+            table.insert(mint_url.to_string().as_str(), info.as_str())?;
+        }
+    }
+    write_txn.commit()?;
+
+    Ok(())
+}
+
+fn migrate_mint_keyset_table_01_to_02(db: Arc<Database>) -> Result<(), Error> {
+    let mut mints: Vec<(String, Vec<Vec<u8>>)> = vec![];
+    {
+        let read_txn = db.begin_read().map_err(Error::from)?;
+        let table = read_txn
+            .open_multimap_table(MINT_KEYSETS_TABLE)
+            .map_err(Error::from)?;
+
+        let mint_keysets_range = table.iter().map_err(Error::from)?;
+
+        for (url, keysets) in mint_keysets_range.flatten() {
+            let keysets: Vec<Vec<u8>> = keysets
+                .into_iter()
+                .flatten()
+                .map(|k| k.value().to_vec())
+                .collect();
+
+            mints.push((url.value().to_string(), keysets));
+        }
+    }
+
+    let write_txn = db.begin_write()?;
+    {
+        let mut table = write_txn
+            .open_multimap_table(MINT_KEYSETS_TABLE)
+            .map_err(Error::from)?;
+        for (mint_url_str, keysets) in mints {
+            let mint_url = MintUrl::from_str(&mint_url_str).map_err(Error::from)?;
+
+            table.remove_all(mint_url_str.as_str())?;
+            for keyset in keysets {
+                table.insert(mint_url.to_string().as_str(), keyset.deref())?;
+            }
+        }
+    }
+    write_txn.commit()?;
+
+    Ok(())
+}
+
+fn migrate_trim_mint_urls_01_to_02(db: Arc<Database>) -> Result<(), Error> {
+    migrate_mints_table_01_to_02(Arc::clone(&db))?;
+    migrate_mint_keyset_table_01_to_02(Arc::clone(&db))?;
+    Ok(())
+}

+ 30 - 12
crates/cdk-redb/src/wallet/mod.rs

@@ -8,11 +8,11 @@ use std::sync::Arc;
 
 use async_trait::async_trait;
 use cdk::cdk_database::WalletDatabase;
+use cdk::mint_url::MintUrl;
 use cdk::nuts::{
     CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, SpendingConditions, State,
 };
 use cdk::types::ProofInfo;
-use cdk::url::UncheckedUrl;
 use cdk::util::unix_time;
 use cdk::wallet::MintQuote;
 use cdk::{cdk_database, wallet};
@@ -22,7 +22,11 @@ use tracing::instrument;
 
 use super::error::Error;
 use crate::migrations::migrate_00_to_01;
+use crate::wallet::migrations::migrate_01_to_02;
 
+mod migrations;
+
+// <Mint_url, Info>
 const MINTS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mints_table");
 // <Mint_Url, Keyset_id>
 const MINT_KEYSETS_TABLE: MultimapTableDefinition<&str, &[u8]> =
@@ -40,7 +44,7 @@ const CONFIG_TABLE: TableDefinition<&str, &str> = TableDefinition::new("config")
 const KEYSET_COUNTER: TableDefinition<&str, u32> = TableDefinition::new("keyset_counter");
 const NOSTR_LAST_CHECKED: TableDefinition<&str, u32> = TableDefinition::new("keyset_counter");
 
-const DATABASE_VERSION: u32 = 1;
+const DATABASE_VERSION: u32 = 2;
 
 /// Wallet Redb Database
 #[derive(Debug, Clone)]
@@ -82,6 +86,10 @@ impl WalletRedbDatabase {
                                 current_file_version = migrate_00_to_01(Arc::clone(&db))?;
                             }
 
+                            if current_file_version == 1 {
+                                current_file_version = migrate_01_to_02(Arc::clone(&db))?;
+                            }
+
                             if current_file_version != DATABASE_VERSION {
                                 tracing::warn!(
                                     "Database upgrade did not complete at {} current is {}",
@@ -90,6 +98,16 @@ impl WalletRedbDatabase {
                                 );
                                 return Err(Error::UnknownDatabaseVersion);
                             }
+
+                            let write_txn = db.begin_write()?;
+                            {
+                                let mut table = write_txn.open_table(CONFIG_TABLE)?;
+
+                                table
+                                    .insert("db_version", DATABASE_VERSION.to_string().as_str())?;
+                            }
+
+                            write_txn.commit()?;
                         }
                         Ordering::Equal => {
                             tracing::info!("Database is at current version {}", DATABASE_VERSION);
@@ -142,7 +160,7 @@ impl WalletDatabase for WalletRedbDatabase {
     #[instrument(skip(self))]
     async fn add_mint(
         &self,
-        mint_url: UncheckedUrl,
+        mint_url: MintUrl,
         mint_info: Option<MintInfo>,
     ) -> Result<(), Self::Err> {
         let db = self.db.lock().await;
@@ -166,7 +184,7 @@ impl WalletDatabase for WalletRedbDatabase {
     }
 
     #[instrument(skip(self))]
-    async fn remove_mint(&self, mint_url: UncheckedUrl) -> Result<(), Self::Err> {
+    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), Self::Err> {
         let db = self.db.lock().await;
 
         let write_txn = db.begin_write().map_err(Error::from)?;
@@ -183,7 +201,7 @@ impl WalletDatabase for WalletRedbDatabase {
     }
 
     #[instrument(skip(self))]
-    async fn get_mint(&self, mint_url: UncheckedUrl) -> Result<Option<MintInfo>, Self::Err> {
+    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, Self::Err> {
         let db = self.db.lock().await;
         let read_txn = db.begin_read().map_err(Into::<Error>::into)?;
         let table = read_txn.open_table(MINTS_TABLE).map_err(Error::from)?;
@@ -199,7 +217,7 @@ impl WalletDatabase for WalletRedbDatabase {
     }
 
     #[instrument(skip(self))]
-    async fn get_mints(&self) -> Result<HashMap<UncheckedUrl, Option<MintInfo>>, Self::Err> {
+    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, Self::Err> {
         let db = self.db.lock().await;
         let read_txn = db.begin_read().map_err(Error::from)?;
         let table = read_txn.open_table(MINTS_TABLE).map_err(Error::from)?;
@@ -209,7 +227,7 @@ impl WalletDatabase for WalletRedbDatabase {
             .flatten()
             .map(|(mint, mint_info)| {
                 (
-                    UncheckedUrl::from_str(mint.value()).unwrap(),
+                    MintUrl::from_str(mint.value()).unwrap(),
                     serde_json::from_str(mint_info.value()).ok(),
                 )
             })
@@ -221,8 +239,8 @@ impl WalletDatabase for WalletRedbDatabase {
     #[instrument(skip(self))]
     async fn update_mint_url(
         &self,
-        old_mint_url: UncheckedUrl,
-        new_mint_url: UncheckedUrl,
+        old_mint_url: MintUrl,
+        new_mint_url: MintUrl,
     ) -> Result<(), Self::Err> {
         // Update proofs table
         {
@@ -275,7 +293,7 @@ impl WalletDatabase for WalletRedbDatabase {
     #[instrument(skip(self))]
     async fn add_mint_keysets(
         &self,
-        mint_url: UncheckedUrl,
+        mint_url: MintUrl,
         keysets: Vec<KeySetInfo>,
     ) -> Result<(), Self::Err> {
         let db = self.db.lock().await;
@@ -314,7 +332,7 @@ impl WalletDatabase for WalletRedbDatabase {
     #[instrument(skip(self))]
     async fn get_mint_keysets(
         &self,
-        mint_url: UncheckedUrl,
+        mint_url: MintUrl,
     ) -> Result<Option<Vec<KeySetInfo>>, Self::Err> {
         let db = self.db.lock().await;
         let read_txn = db.begin_read().map_err(Into::<Error>::into)?;
@@ -576,7 +594,7 @@ impl WalletDatabase for WalletRedbDatabase {
     #[instrument(skip_all)]
     async fn get_proofs(
         &self,
-        mint_url: Option<UncheckedUrl>,
+        mint_url: Option<MintUrl>,
         unit: Option<CurrencyUnit>,
         state: Option<Vec<State>>,
         spending_conditions: Option<Vec<SpendingConditions>>,

+ 11 - 11
crates/cdk-rexie/src/wallet.rs

@@ -6,11 +6,11 @@ use std::result::Result;
 
 use async_trait::async_trait;
 use cdk::cdk_database::WalletDatabase;
+use cdk::mint_url::MintUrl;
 use cdk::nuts::{
     CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, SpendingConditions, State,
 };
 use cdk::types::ProofInfo;
-use cdk::url::UncheckedUrl;
 use cdk::util::unix_time;
 use cdk::wallet::{MeltQuote, MintQuote};
 use rexie::*;
@@ -117,7 +117,7 @@ impl WalletDatabase for WalletRexieDatabase {
 
     async fn add_mint(
         &self,
-        mint_url: UncheckedUrl,
+        mint_url: MintUrl,
         mint_info: Option<MintInfo>,
     ) -> Result<(), Self::Err> {
         let rexie = self.db.lock().await;
@@ -141,7 +141,7 @@ impl WalletDatabase for WalletRexieDatabase {
         Ok(())
     }
 
-    async fn remove_mint(&self, mint_url: UncheckedUrl) -> Result<(), Self::Err> {
+    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), Self::Err> {
         let rexie = self.db.lock().await;
 
         let transaction = rexie
@@ -159,7 +159,7 @@ impl WalletDatabase for WalletRexieDatabase {
         Ok(())
     }
 
-    async fn get_mint(&self, mint_url: UncheckedUrl) -> Result<Option<MintInfo>, Self::Err> {
+    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, Self::Err> {
         let rexie = self.db.lock().await;
 
         let transaction = rexie
@@ -178,7 +178,7 @@ impl WalletDatabase for WalletRexieDatabase {
         Ok(mint_info)
     }
 
-    async fn get_mints(&self) -> Result<HashMap<UncheckedUrl, Option<MintInfo>>, Self::Err> {
+    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, Self::Err> {
         let rexie = self.db.lock().await;
 
         let transaction = rexie
@@ -192,7 +192,7 @@ impl WalletDatabase for WalletRexieDatabase {
             .await
             .map_err(Error::from)?;
 
-        let mints: HashMap<UncheckedUrl, Option<MintInfo>> = mints
+        let mints: HashMap<MintUrl, Option<MintInfo>> = mints
             .into_iter()
             .map(|(url, info)| {
                 (
@@ -207,8 +207,8 @@ impl WalletDatabase for WalletRexieDatabase {
 
     async fn update_mint_url(
         &self,
-        old_mint_url: UncheckedUrl,
-        new_mint_url: UncheckedUrl,
+        old_mint_url: MintUrl,
+        new_mint_url: MintUrl,
     ) -> Result<(), Self::Err> {
         let proofs = self
             .get_proofs(Some(old_mint_url), None, None, None)
@@ -255,7 +255,7 @@ impl WalletDatabase for WalletRexieDatabase {
 
     async fn add_mint_keysets(
         &self,
-        mint_url: UncheckedUrl,
+        mint_url: MintUrl,
         keysets: Vec<KeySetInfo>,
     ) -> Result<(), Self::Err> {
         let rexie = self.db.lock().await;
@@ -306,7 +306,7 @@ impl WalletDatabase for WalletRexieDatabase {
 
     async fn get_mint_keysets(
         &self,
-        mint_url: UncheckedUrl,
+        mint_url: MintUrl,
     ) -> Result<Option<Vec<KeySetInfo>>, Self::Err> {
         let rexie = self.db.lock().await;
 
@@ -593,7 +593,7 @@ impl WalletDatabase for WalletRexieDatabase {
 
     async fn get_proofs(
         &self,
-        mint_url: Option<UncheckedUrl>,
+        mint_url: Option<MintUrl>,
         unit: Option<CurrencyUnit>,
         state: Option<Vec<State>>,
         spending_conditions: Option<Vec<SpendingConditions>>,

+ 1 - 0
crates/cdk-sqlite/src/mint/migrations/20240811031111_update_mint_url.sql

@@ -0,0 +1 @@
+UPDATE `mint_quote` SET `mint_url` = RTRIM(`mint_url`, '/');

+ 21 - 0
crates/cdk-sqlite/src/wallet/migrations/20240810233905_update_mint_url.sql

@@ -0,0 +1,21 @@
+-- Delete duplicates from `mint`
+DELETE FROM `mint`
+WHERE `mint_url` IN (
+    SELECT `mint_url`
+    FROM (
+        SELECT RTRIM(`mint_url`, '/') AS trimmed_url, MIN(rowid) AS keep_id
+        FROM `mint`
+        GROUP BY trimmed_url
+        HAVING COUNT(*) > 1
+    )
+)
+AND rowid NOT IN (
+    SELECT MIN(rowid)
+    FROM `mint`
+    GROUP BY RTRIM(`mint_url`, '/')
+);
+
+UPDATE `mint` SET `mint_url` = RTRIM(`mint_url`, '/');
+UPDATE `keyset` SET `mint_url` = RTRIM(`mint_url`, '/');
+UPDATE `mint_quote` SET `mint_url` = RTRIM(`mint_url`, '/');
+UPDATE `proof` SET `mint_url` = RTRIM(`mint_url`, '/');

+ 10 - 10
crates/cdk-sqlite/src/wallet/mod.rs

@@ -7,13 +7,13 @@ use std::str::FromStr;
 use async_trait::async_trait;
 use cdk::amount::Amount;
 use cdk::cdk_database::{self, WalletDatabase};
+use cdk::mint_url::MintUrl;
 use cdk::nuts::{
     CurrencyUnit, Id, KeySetInfo, Keys, MeltQuoteState, MintInfo, MintQuoteState, Proof, Proofs,
     PublicKey, SpendingConditions, State,
 };
 use cdk::secret::Secret;
 use cdk::types::ProofInfo;
-use cdk::url::UncheckedUrl;
 use cdk::wallet;
 use cdk::wallet::MintQuote;
 use error::Error;
@@ -62,7 +62,7 @@ impl WalletDatabase for WalletSqliteDatabase {
     #[instrument(skip(self, mint_info))]
     async fn add_mint(
         &self,
-        mint_url: UncheckedUrl,
+        mint_url: MintUrl,
         mint_info: Option<MintInfo>,
     ) -> Result<(), Self::Err> {
         let (
@@ -129,7 +129,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
     }
 
     #[instrument(skip(self))]
-    async fn remove_mint(&self, mint_url: UncheckedUrl) -> Result<(), Self::Err> {
+    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), Self::Err> {
         sqlx::query(
             r#"
 DELETE FROM mint
@@ -145,7 +145,7 @@ WHERE mint_url=?
     }
 
     #[instrument(skip(self))]
-    async fn get_mint(&self, mint_url: UncheckedUrl) -> Result<Option<MintInfo>, Self::Err> {
+    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, Self::Err> {
         let rec = sqlx::query(
             r#"
 SELECT *
@@ -169,7 +169,7 @@ WHERE mint_url=?;
     }
 
     #[instrument(skip(self))]
-    async fn get_mints(&self) -> Result<HashMap<UncheckedUrl, Option<MintInfo>>, Self::Err> {
+    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, Self::Err> {
         let rec = sqlx::query(
             r#"
 SELECT *
@@ -197,8 +197,8 @@ FROM mint
     #[instrument(skip(self))]
     async fn update_mint_url(
         &self,
-        old_mint_url: UncheckedUrl,
-        new_mint_url: UncheckedUrl,
+        old_mint_url: MintUrl,
+        new_mint_url: MintUrl,
     ) -> Result<(), Self::Err> {
         let tables = ["mint_quote", "proof"];
         for table in &tables {
@@ -224,7 +224,7 @@ FROM mint
     #[instrument(skip(self, keysets))]
     async fn add_mint_keysets(
         &self,
-        mint_url: UncheckedUrl,
+        mint_url: MintUrl,
         keysets: Vec<KeySetInfo>,
     ) -> Result<(), Self::Err> {
         for keyset in keysets {
@@ -251,7 +251,7 @@ VALUES (?, ?, ?, ?, ?);
     #[instrument(skip(self))]
     async fn get_mint_keysets(
         &self,
-        mint_url: UncheckedUrl,
+        mint_url: MintUrl,
     ) -> Result<Option<Vec<KeySetInfo>>, Self::Err> {
         let recs = sqlx::query(
             r#"
@@ -547,7 +547,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
     #[instrument(skip(self, state, spending_conditions))]
     async fn get_proofs(
         &self,
-        mint_url: Option<UncheckedUrl>,
+        mint_url: Option<MintUrl>,
         unit: Option<CurrencyUnit>,
         state: Option<Vec<State>>,
         spending_conditions: Option<Vec<SpendingConditions>>,

+ 11 - 11
crates/cdk/src/cdk_database/mod.rs

@@ -14,6 +14,8 @@ use crate::mint;
 use crate::mint::MintKeySetInfo;
 #[cfg(feature = "mint")]
 use crate::mint::MintQuote as MintMintQuote;
+#[cfg(feature = "wallet")]
+use crate::mint_url::MintUrl;
 #[cfg(feature = "mint")]
 use crate::nuts::{BlindSignature, MeltQuoteState, MintQuoteState, Proof};
 #[cfg(any(feature = "wallet", feature = "mint"))]
@@ -23,8 +25,6 @@ use crate::nuts::{KeySetInfo, Keys, MintInfo, SpendingConditions};
 #[cfg(feature = "wallet")]
 use crate::types::ProofInfo;
 #[cfg(feature = "wallet")]
-use crate::url::UncheckedUrl;
-#[cfg(feature = "wallet")]
 use crate::wallet;
 #[cfg(feature = "wallet")]
 use crate::wallet::MintQuote as WalletMintQuote;
@@ -65,32 +65,32 @@ pub trait WalletDatabase: Debug {
     /// Add Mint to storage
     async fn add_mint(
         &self,
-        mint_url: UncheckedUrl,
+        mint_url: MintUrl,
         mint_info: Option<MintInfo>,
     ) -> Result<(), Self::Err>;
     /// Remove Mint from storage
-    async fn remove_mint(&self, mint_url: UncheckedUrl) -> Result<(), Self::Err>;
+    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), Self::Err>;
     /// Get mint from storage
-    async fn get_mint(&self, mint_url: UncheckedUrl) -> Result<Option<MintInfo>, Self::Err>;
+    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, Self::Err>;
     /// Get all mints from storage
-    async fn get_mints(&self) -> Result<HashMap<UncheckedUrl, Option<MintInfo>>, Self::Err>;
+    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, Self::Err>;
     /// Update mint url
     async fn update_mint_url(
         &self,
-        old_mint_url: UncheckedUrl,
-        new_mint_url: UncheckedUrl,
+        old_mint_url: MintUrl,
+        new_mint_url: MintUrl,
     ) -> Result<(), Self::Err>;
 
     /// Add mint keyset to storage
     async fn add_mint_keysets(
         &self,
-        mint_url: UncheckedUrl,
+        mint_url: MintUrl,
         keysets: Vec<KeySetInfo>,
     ) -> Result<(), Self::Err>;
     /// Get mint keysets for mint url
     async fn get_mint_keysets(
         &self,
-        mint_url: UncheckedUrl,
+        mint_url: MintUrl,
     ) -> Result<Option<Vec<KeySetInfo>>, Self::Err>;
     /// Get mint keyset by id
     async fn get_keyset_by_id(&self, keyset_id: &Id) -> Result<Option<KeySetInfo>, Self::Err>;
@@ -123,7 +123,7 @@ pub trait WalletDatabase: Debug {
     /// Get proofs from storage
     async fn get_proofs(
         &self,
-        mint_url: Option<UncheckedUrl>,
+        mint_url: Option<MintUrl>,
         unit: Option<CurrencyUnit>,
         state: Option<Vec<State>>,
         spending_conditions: Option<Vec<SpendingConditions>>,

+ 12 - 15
crates/cdk/src/cdk_database/wallet_memory.rs

@@ -8,11 +8,11 @@ use tokio::sync::RwLock;
 
 use super::WalletDatabase;
 use crate::cdk_database::Error;
+use crate::mint_url::MintUrl;
 use crate::nuts::{
     CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, SpendingConditions, State,
 };
 use crate::types::ProofInfo;
-use crate::url::UncheckedUrl;
 use crate::util::unix_time;
 use crate::wallet;
 use crate::wallet::types::MintQuote;
@@ -20,8 +20,8 @@ use crate::wallet::types::MintQuote;
 /// Wallet in Memory Database
 #[derive(Default, Debug, Clone)]
 pub struct WalletMemoryDatabase {
-    mints: Arc<RwLock<HashMap<UncheckedUrl, Option<MintInfo>>>>,
-    mint_keysets: Arc<RwLock<HashMap<UncheckedUrl, HashSet<Id>>>>,
+    mints: Arc<RwLock<HashMap<MintUrl, Option<MintInfo>>>>,
+    mint_keysets: Arc<RwLock<HashMap<MintUrl, HashSet<Id>>>>,
     keysets: Arc<RwLock<HashMap<Id, KeySetInfo>>>,
     mint_quotes: Arc<RwLock<HashMap<String, MintQuote>>>,
     melt_quotes: Arc<RwLock<HashMap<String, wallet::MeltQuote>>>,
@@ -67,32 +67,32 @@ impl WalletDatabase for WalletMemoryDatabase {
 
     async fn add_mint(
         &self,
-        mint_url: UncheckedUrl,
+        mint_url: MintUrl,
         mint_info: Option<MintInfo>,
     ) -> Result<(), Self::Err> {
         self.mints.write().await.insert(mint_url, mint_info);
         Ok(())
     }
 
-    async fn remove_mint(&self, mint_url: UncheckedUrl) -> Result<(), Self::Err> {
+    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), Self::Err> {
         let mut mints = self.mints.write().await;
         mints.remove(&mint_url);
 
         Ok(())
     }
 
-    async fn get_mint(&self, mint_url: UncheckedUrl) -> Result<Option<MintInfo>, Self::Err> {
+    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, Self::Err> {
         Ok(self.mints.read().await.get(&mint_url).cloned().flatten())
     }
 
-    async fn get_mints(&self) -> Result<HashMap<UncheckedUrl, Option<MintInfo>>, Error> {
+    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, Error> {
         Ok(self.mints.read().await.clone())
     }
 
     async fn update_mint_url(
         &self,
-        old_mint_url: UncheckedUrl,
-        new_mint_url: UncheckedUrl,
+        old_mint_url: MintUrl,
+        new_mint_url: MintUrl,
     ) -> Result<(), Self::Err> {
         let proofs = self
             .get_proofs(Some(old_mint_url), None, None, None)
@@ -140,7 +140,7 @@ impl WalletDatabase for WalletMemoryDatabase {
 
     async fn add_mint_keysets(
         &self,
-        mint_url: UncheckedUrl,
+        mint_url: MintUrl,
         keysets: Vec<KeySetInfo>,
     ) -> Result<(), Error> {
         let mut current_mint_keysets = self.mint_keysets.write().await;
@@ -160,10 +160,7 @@ impl WalletDatabase for WalletMemoryDatabase {
         Ok(())
     }
 
-    async fn get_mint_keysets(
-        &self,
-        mint_url: UncheckedUrl,
-    ) -> Result<Option<Vec<KeySetInfo>>, Error> {
+    async fn get_mint_keysets(&self, mint_url: MintUrl) -> Result<Option<Vec<KeySetInfo>>, Error> {
         match self.mint_keysets.read().await.get(&mint_url) {
             Some(keyset_ids) => {
                 let mut keysets = vec![];
@@ -253,7 +250,7 @@ impl WalletDatabase for WalletMemoryDatabase {
 
     async fn get_proofs(
         &self,
-        mint_url: Option<UncheckedUrl>,
+        mint_url: Option<MintUrl>,
         unit: Option<CurrencyUnit>,
         state: Option<Vec<State>>,
         spending_conditions: Option<Vec<SpendingConditions>>,

+ 1 - 3
crates/cdk/src/lib.rs

@@ -12,10 +12,10 @@ pub mod dhke;
 pub mod error;
 #[cfg(feature = "mint")]
 pub mod mint;
+pub mod mint_url;
 pub mod nuts;
 pub mod secret;
 pub mod types;
-pub mod url;
 pub mod util;
 #[cfg(feature = "wallet")]
 pub mod wallet;
@@ -34,8 +34,6 @@ pub use wallet::Wallet;
 #[doc(hidden)]
 pub use self::amount::Amount;
 #[doc(hidden)]
-pub use self::url::UncheckedUrl;
-#[doc(hidden)]
 pub use self::util::SECP256K1;
 #[cfg(feature = "wallet")]
 #[doc(hidden)]

+ 6 - 6
crates/cdk/src/mint/mod.rs

@@ -14,9 +14,9 @@ use self::nut05::QuoteState;
 use self::nut11::EnforceSigFlag;
 use crate::cdk_database::{self, MintDatabase};
 use crate::dhke::{hash_to_curve, sign_message, verify_message};
+use crate::mint_url::MintUrl;
 use crate::nuts::nut11::enforce_sig_flag;
 use crate::nuts::*;
-use crate::url::UncheckedUrl;
 use crate::util::unix_time;
 use crate::Amount;
 
@@ -29,7 +29,7 @@ pub use types::{MeltQuote, MintQuote};
 #[derive(Clone)]
 pub struct Mint {
     /// Mint Url
-    pub mint_url: UncheckedUrl,
+    pub mint_url: MintUrl,
     /// Mint Info
     pub mint_info: MintInfo,
     /// Mint Storage backend
@@ -154,7 +154,7 @@ impl Mint {
             }
         }
 
-        let mint_url = UncheckedUrl::from(mint_url);
+        let mint_url = MintUrl::from(mint_url);
 
         Ok(Self {
             mint_url,
@@ -168,13 +168,13 @@ impl Mint {
 
     /// Set Mint Url
     #[instrument(skip_all)]
-    pub fn set_mint_url(&mut self, mint_url: UncheckedUrl) {
+    pub fn set_mint_url(&mut self, mint_url: MintUrl) {
         self.mint_url = mint_url;
     }
 
     /// Get Mint Url
     #[instrument(skip_all)]
-    pub fn get_mint_url(&self) -> &UncheckedUrl {
+    pub fn get_mint_url(&self) -> &MintUrl {
         &self.mint_url
     }
 
@@ -194,7 +194,7 @@ impl Mint {
     #[instrument(skip_all)]
     pub async fn new_mint_quote(
         &self,
-        mint_url: UncheckedUrl,
+        mint_url: MintUrl,
         request: String,
         unit: CurrencyUnit,
         amount: Amount,

+ 4 - 3
crates/cdk/src/mint/types.rs

@@ -4,8 +4,9 @@ use serde::{Deserialize, Serialize};
 use uuid::Uuid;
 
 use super::CurrencyUnit;
+use crate::mint_url::MintUrl;
 use crate::nuts::{MeltQuoteState, MintQuoteState};
-use crate::{Amount, UncheckedUrl};
+use crate::Amount;
 
 /// Mint Quote Info
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
@@ -13,7 +14,7 @@ pub struct MintQuote {
     /// Quote id
     pub id: String,
     /// Mint Url
-    pub mint_url: UncheckedUrl,
+    pub mint_url: MintUrl,
     /// Amount of quote
     pub amount: Amount,
     /// Unit of quote
@@ -31,7 +32,7 @@ pub struct MintQuote {
 impl MintQuote {
     /// Create new [`MintQuote`]
     pub fn new(
-        mint_url: UncheckedUrl,
+        mint_url: MintUrl,
         request: String,
         unit: CurrencyUnit,
         amount: Amount,

+ 120 - 0
crates/cdk/src/mint_url.rs

@@ -0,0 +1,120 @@
+// Copyright (c) 2022-2023 Yuki Kishimoto
+// Distributed under the MIT software license
+
+//! Url
+
+use core::fmt;
+use core::str::FromStr;
+
+use serde::{Deserialize, Deserializer, Serialize};
+use thiserror::Error;
+use url::{ParseError, Url};
+
+/// Url Error
+#[derive(Debug, Error, PartialEq, Eq)]
+pub enum Error {
+    /// Url error
+    #[error(transparent)]
+    Url(#[from] ParseError),
+}
+
+/// MintUrl Url
+#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
+pub struct MintUrl(String);
+
+impl MintUrl {
+    /// New mint url
+    pub fn new<S>(url: S) -> Self
+    where
+        S: Into<String>,
+    {
+        let url: String = url.into();
+        Self(url.trim_end_matches('/').to_string())
+    }
+
+    /// Empty mint url
+    pub fn empty() -> Self {
+        Self(String::new())
+    }
+
+    /// Join onto url
+    pub fn join(&self, path: &str) -> Result<Url, Error> {
+        let url: Url = self.try_into()?;
+        Ok(url.join(path)?)
+    }
+
+    /// Remove trailing slashes from url
+    pub fn trim_trailing_slashes(&self) -> Self {
+        Self(self.to_string().trim_end_matches('/').to_string())
+    }
+}
+impl<'de> Deserialize<'de> for MintUrl {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let s = String::deserialize(deserializer)?;
+        MintUrl::from_str(&s).map_err(serde::de::Error::custom)
+    }
+}
+impl<S> From<S> for MintUrl
+where
+    S: Into<String>,
+{
+    fn from(url: S) -> Self {
+        let url: String = url.into();
+        Self(url.trim_end_matches('/').to_string())
+    }
+}
+
+impl FromStr for MintUrl {
+    type Err = Error;
+
+    fn from_str(url: &str) -> Result<Self, Self::Err> {
+        Ok(Self::from(url))
+    }
+}
+
+impl TryFrom<MintUrl> for Url {
+    type Error = Error;
+
+    fn try_from(mint_url: MintUrl) -> Result<Url, Self::Error> {
+        Ok(Self::parse(&mint_url.0)?)
+    }
+}
+
+impl TryFrom<&MintUrl> for Url {
+    type Error = Error;
+
+    fn try_from(mint_url: &MintUrl) -> Result<Url, Self::Error> {
+        Ok(Self::parse(mint_url.0.as_str())?)
+    }
+}
+
+impl fmt::Display for MintUrl {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.0)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+
+    use super::*;
+
+    #[test]
+    fn test_trim_trailing_slashes() {
+        let very_unformatted_url = "http://url-to-check.com////";
+        let unformatted_url = "http://url-to-check.com/";
+        let formatted_url = "http://url-to-check.com";
+
+        let very_trimmed_url = MintUrl::from_str(very_unformatted_url).unwrap();
+        assert_eq!("http://url-to-check.com", very_trimmed_url.to_string());
+
+        let trimmed_url = MintUrl::from_str(unformatted_url).unwrap();
+        assert_eq!("http://url-to-check.com", trimmed_url.to_string());
+
+        let unchanged_url = MintUrl::from_str(formatted_url).unwrap();
+        assert_eq!("http://url-to-check.com", unchanged_url.to_string());
+    }
+}

+ 14 - 14
crates/cdk/src/nuts/nut00/token.rs

@@ -12,8 +12,8 @@ use serde::{Deserialize, Serialize};
 use url::Url;
 
 use super::{Error, Proof, ProofV4, Proofs};
+use crate::mint_url::MintUrl;
 use crate::nuts::{CurrencyUnit, Id};
-use crate::url::UncheckedUrl;
 use crate::Amount;
 
 /// Token Enum
@@ -40,7 +40,7 @@ impl fmt::Display for Token {
 impl Token {
     /// Create new [`Token`]
     pub fn new(
-        mint_url: UncheckedUrl,
+        mint_url: MintUrl,
         proofs: Proofs,
         memo: Option<String>,
         unit: Option<CurrencyUnit>,
@@ -66,7 +66,7 @@ impl Token {
     }
 
     /// Proofs in [`Token`]
-    pub fn proofs(&self) -> HashMap<UncheckedUrl, Proofs> {
+    pub fn proofs(&self) -> HashMap<MintUrl, Proofs> {
         match self {
             Self::TokenV3(token) => token.proofs(),
             Self::TokenV4(token) => token.proofs(),
@@ -140,14 +140,14 @@ impl FromStr for Token {
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct TokenV3Token {
     /// Url of mint
-    pub mint: UncheckedUrl,
+    pub mint: MintUrl,
     /// [`Proofs`]
     pub proofs: Proofs,
 }
 
 impl TokenV3Token {
     /// Create new [`TokenV3Token`]
-    pub fn new(mint_url: UncheckedUrl, proofs: Proofs) -> Self {
+    pub fn new(mint_url: MintUrl, proofs: Proofs) -> Self {
         Self {
             mint: mint_url,
             proofs,
@@ -171,7 +171,7 @@ pub struct TokenV3 {
 impl TokenV3 {
     /// Create new [`Token`]
     pub fn new(
-        mint_url: UncheckedUrl,
+        mint_url: MintUrl,
         proofs: Proofs,
         memo: Option<String>,
         unit: Option<CurrencyUnit>,
@@ -190,8 +190,8 @@ impl TokenV3 {
         })
     }
 
-    fn proofs(&self) -> HashMap<UncheckedUrl, Proofs> {
-        let mut proofs: HashMap<UncheckedUrl, Proofs> = HashMap::new();
+    fn proofs(&self) -> HashMap<MintUrl, Proofs> {
+        let mut proofs: HashMap<MintUrl, Proofs> = HashMap::new();
 
         for token in self.token.clone() {
             let mint_url = token.mint;
@@ -268,7 +268,7 @@ impl From<TokenV4> for TokenV3 {
 pub struct TokenV4 {
     /// Mint Url
     #[serde(rename = "m")]
-    pub mint_url: UncheckedUrl,
+    pub mint_url: MintUrl,
     /// Token Unit
     #[serde(rename = "u", skip_serializing_if = "Option::is_none")]
     pub unit: Option<CurrencyUnit>,
@@ -284,9 +284,9 @@ pub struct TokenV4 {
 
 impl TokenV4 {
     /// Proofs from token
-    pub fn proofs(&self) -> HashMap<UncheckedUrl, Proofs> {
+    pub fn proofs(&self) -> HashMap<MintUrl, Proofs> {
         let mint_url = &self.mint_url;
-        let mut proofs: HashMap<UncheckedUrl, Proofs> = HashMap::new();
+        let mut proofs: HashMap<MintUrl, Proofs> = HashMap::new();
 
         for token in self.token.clone() {
             let mut mint_proofs = token
@@ -423,7 +423,7 @@ mod tests {
     use std::str::FromStr;
 
     use super::*;
-    use crate::UncheckedUrl;
+    use crate::mint_url::MintUrl;
 
     #[test]
     fn test_token_padding() {
@@ -445,7 +445,7 @@ mod tests {
 
         assert_eq!(
             token.mint_url,
-            UncheckedUrl::from_str("http://localhost:3338").unwrap()
+            MintUrl::from_str("http://localhost:3338").unwrap()
         );
         assert_eq!(
             token.token[0].keyset_id,
@@ -500,7 +500,7 @@ mod tests {
         let token = TokenV3::from_str(token_str).unwrap();
         assert_eq!(
             token.token[0].mint,
-            UncheckedUrl::from_str("https://8333.space:3338").unwrap()
+            MintUrl::from_str("https://8333.space:3338").unwrap()
         );
         assert_eq!(
             token.token[0].proofs[0].clone().keyset_id,

+ 4 - 4
crates/cdk/src/types.rs

@@ -3,10 +3,10 @@
 use serde::{Deserialize, Serialize};
 
 use crate::error::Error;
+use crate::mint_url::MintUrl;
 use crate::nuts::{
     CurrencyUnit, MeltQuoteState, Proof, Proofs, PublicKey, SpendingConditions, State,
 };
-use crate::url::UncheckedUrl;
 
 /// Melt response with proofs
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
@@ -27,7 +27,7 @@ pub struct ProofInfo {
     /// y
     pub y: PublicKey,
     /// Mint Url
-    pub mint_url: UncheckedUrl,
+    pub mint_url: MintUrl,
     /// Proof State
     pub state: State,
     /// Proof Spending Conditions
@@ -40,7 +40,7 @@ impl ProofInfo {
     /// Create new [`ProofInfo`]
     pub fn new(
         proof: Proof,
-        mint_url: UncheckedUrl,
+        mint_url: MintUrl,
         state: State,
         unit: CurrencyUnit,
     ) -> Result<Self, Error> {
@@ -63,7 +63,7 @@ impl ProofInfo {
     /// Check if [`Proof`] matches conditions
     pub fn matches_conditions(
         &self,
-        mint_url: &Option<UncheckedUrl>,
+        mint_url: &Option<MintUrl>,
         unit: &Option<CurrencyUnit>,
         state: &Option<Vec<State>>,
         spending_conditions: &Option<Vec<SpendingConditions>>,

+ 0 - 109
crates/cdk/src/url.rs

@@ -1,109 +0,0 @@
-// Copyright (c) 2022-2023 Yuki Kishimoto
-// Distributed under the MIT software license
-
-//! Url
-
-use core::fmt;
-use core::str::FromStr;
-
-use serde::{Deserialize, Serialize};
-use thiserror::Error;
-use url::{ParseError, Url};
-
-/// Url Error
-#[derive(Debug, Error, PartialEq, Eq)]
-pub enum Error {
-    /// Url error
-    #[error(transparent)]
-    Url(#[from] ParseError),
-}
-
-/// Unchecked Url
-#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
-pub struct UncheckedUrl(String);
-
-impl UncheckedUrl {
-    /// New unchecked url
-    pub fn new<S>(url: S) -> Self
-    where
-        S: Into<String>,
-    {
-        Self(url.into())
-    }
-
-    /// Empty unchecked url
-    pub fn empty() -> Self {
-        Self(String::new())
-    }
-
-    /// Join onto url
-    pub fn join(&self, path: &str) -> Result<Url, Error> {
-        let url: Url = self.try_into()?;
-        Ok(url.join(path)?)
-    }
-}
-
-impl<S> From<S> for UncheckedUrl
-where
-    S: Into<String>,
-{
-    fn from(url: S) -> Self {
-        Self(url.into())
-    }
-}
-
-impl FromStr for UncheckedUrl {
-    type Err = Error;
-
-    fn from_str(url: &str) -> Result<Self, Self::Err> {
-        Ok(Self::from(url))
-    }
-}
-
-impl TryFrom<UncheckedUrl> for Url {
-    type Error = Error;
-
-    fn try_from(unchecked_url: UncheckedUrl) -> Result<Url, Self::Error> {
-        Ok(Self::parse(&unchecked_url.0)?)
-    }
-}
-
-impl TryFrom<&UncheckedUrl> for Url {
-    type Error = Error;
-
-    fn try_from(unchecked_url: &UncheckedUrl) -> Result<Url, Self::Error> {
-        Ok(Self::parse(unchecked_url.0.as_str())?)
-    }
-}
-
-impl fmt::Display for UncheckedUrl {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{}", self.0)
-    }
-}
-
-#[cfg(test)]
-mod tests {
-
-    use super::*;
-
-    #[test]
-    fn test_unchecked_relay_url() {
-        let relay = "wss://relay.damus.io:8333/";
-
-        let unchecked_relay_url = UncheckedUrl::from_str(relay).unwrap();
-
-        assert_eq!(relay, unchecked_relay_url.to_string());
-
-        // assert_eq!(relay, serde_json::to_string(&unchecked_relay_url).unwrap());
-
-        let relay = "wss://relay.damus.io:8333";
-
-        let unchecked_relay_url = UncheckedUrl::from_str(relay).unwrap();
-
-        assert_eq!(relay, unchecked_relay_url.to_string());
-
-        // assert_eq!(relay,
-        // serde_json::to_string(&unchecked_relay_url).unwrap())
-    }
-}

+ 1 - 1
crates/cdk/src/wallet/error.rs

@@ -89,7 +89,7 @@ pub enum Error {
     Cashu(#[from] crate::error::Error),
     /// Cashu Url Error
     #[error(transparent)]
-    CashuUrl(#[from] crate::url::Error),
+    CashuUrl(#[from] crate::mint_url::Error),
     /// Database Error
     #[error(transparent)]
     Database(#[from] crate::cdk_database::Error),

+ 5 - 5
crates/cdk/src/wallet/mod.rs

@@ -17,6 +17,7 @@ use tracing::instrument;
 use crate::amount::SplitTarget;
 use crate::cdk_database::{self, WalletDatabase};
 use crate::dhke::{construct_proofs, hash_to_curve};
+use crate::mint_url::MintUrl;
 use crate::nuts::nut00::token::Token;
 use crate::nuts::{
     nut10, nut12, Conditions, CurrencyUnit, Id, KeySetInfo, Keys, Kind, MeltQuoteBolt11Response,
@@ -25,7 +26,6 @@ use crate::nuts::{
     State, SwapRequest,
 };
 use crate::types::{Melted, ProofInfo};
-use crate::url::UncheckedUrl;
 use crate::util::{hex, unix_time};
 use crate::{Amount, Bolt11Invoice, HttpClient, SECP256K1};
 
@@ -42,7 +42,7 @@ pub use types::{MeltQuote, MintQuote, SendKind};
 #[derive(Debug, Clone)]
 pub struct Wallet {
     /// Mint Url
-    pub mint_url: UncheckedUrl,
+    pub mint_url: MintUrl,
     /// Unit
     pub unit: CurrencyUnit,
     /// Storage backend
@@ -66,7 +66,7 @@ impl Wallet {
             .expect("Could not create master key");
 
         Self {
-            mint_url: UncheckedUrl::from(mint_url),
+            mint_url: MintUrl::from(mint_url),
             unit,
             client: HttpClient::new(),
             localstore,
@@ -176,7 +176,7 @@ impl Wallet {
 
     /// Update Mint information and related entries in the event a mint changes its URL
     #[instrument(skip(self))]
-    pub async fn update_mint_url(&mut self, new_mint_url: UncheckedUrl) -> Result<(), Error> {
+    pub async fn update_mint_url(&mut self, new_mint_url: MintUrl) -> Result<(), Error> {
         self.mint_url = new_mint_url.clone();
         // Where the mint_url is in the database it must be updated
         self.localstore
@@ -1506,7 +1506,7 @@ impl Wallet {
         p2pk_signing_keys: &[SecretKey],
         preimages: &[String],
     ) -> Result<Amount, Error> {
-        let mut received_proofs: HashMap<UncheckedUrl, Proofs> = HashMap::new();
+        let mut received_proofs: HashMap<MintUrl, Proofs> = HashMap::new();
         let mint_url = &self.mint_url;
         // Add mint if it does not exist in the store
         if self

+ 5 - 4
crates/cdk/src/wallet/multi_mint_wallet.rs

@@ -14,10 +14,11 @@ use tracing::instrument;
 use super::types::SendKind;
 use super::Error;
 use crate::amount::SplitTarget;
+use crate::mint_url::MintUrl;
 use crate::nuts::{CurrencyUnit, SecretKey, SpendingConditions, Token};
 use crate::types::Melted;
 use crate::wallet::types::MintQuote;
-use crate::{Amount, UncheckedUrl, Wallet};
+use crate::{Amount, Wallet};
 
 /// Multi Mint Wallet
 #[derive(Debug, Clone)]
@@ -29,7 +30,7 @@ pub struct MultiMintWallet {
 /// Wallet Key
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub struct WalletKey {
-    mint_url: UncheckedUrl,
+    mint_url: MintUrl,
     unit: CurrencyUnit,
 }
 
@@ -41,7 +42,7 @@ impl fmt::Display for WalletKey {
 
 impl WalletKey {
     /// Create new [`WalletKey`]
-    pub fn new(mint_url: UncheckedUrl, unit: CurrencyUnit) -> Self {
+    pub fn new(mint_url: MintUrl, unit: CurrencyUnit) -> Self {
         Self { mint_url, unit }
     }
 }
@@ -102,7 +103,7 @@ impl MultiMintWallet {
     pub async fn get_balances(
         &self,
         unit: &CurrencyUnit,
-    ) -> Result<HashMap<UncheckedUrl, Amount>, Error> {
+    ) -> Result<HashMap<MintUrl, Amount>, Error> {
         let mut balances = HashMap::new();
 
         for (WalletKey { mint_url, unit: u }, wallet) in self.wallets.lock().await.iter() {

+ 3 - 2
crates/cdk/src/wallet/types.rs

@@ -2,8 +2,9 @@
 
 use serde::{Deserialize, Serialize};
 
+use crate::mint_url::MintUrl;
 use crate::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState};
-use crate::{Amount, UncheckedUrl};
+use crate::Amount;
 
 /// Mint Quote Info
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
@@ -11,7 +12,7 @@ pub struct MintQuote {
     /// Quote id
     pub id: String,
     /// Mint Url
-    pub mint_url: UncheckedUrl,
+    pub mint_url: MintUrl,
     /// Amount of quote
     pub amount: Amount,
     /// Unit of quote