1use std::collections::HashMap;
4use std::str::FromStr;
5
6use async_trait::async_trait;
7use bitcoin::bip32::DerivationPath;
8use cdk_common::common::IssuerVersion;
9use cdk_common::database::{Error, MintKeyDatabaseTransaction, MintKeysDatabase};
10use cdk_common::mint::MintKeySetInfo;
11use cdk_common::{CurrencyUnit, Id};
12
13use super::{SQLMintDatabase, SQLTransaction};
14use crate::database::{ConnectionWithTransaction, DatabaseExecutor};
15use crate::pool::DatabasePool;
16use crate::stmt::{query, Column};
17use crate::{
18 column_as_nullable_number, column_as_nullable_string, column_as_number, column_as_string,
19 unpack_into,
20};
21
22pub(crate) fn sql_row_to_keyset_info(row: Vec<Column>) -> Result<MintKeySetInfo, Error> {
23 unpack_into!(
24 let (
25 id,
26 unit,
27 active,
28 valid_from,
29 valid_to,
30 derivation_path,
31 derivation_path_index,
32 amounts,
33 row_keyset_ppk,
34 issuer_version
35 ) = row
36 );
37
38 let amounts = column_as_nullable_string!(amounts)
39 .and_then(|str| serde_json::from_str(&str).ok())
40 .ok_or_else(|| Error::Database("amounts field is required".to_string().into()))?;
41
42 Ok(MintKeySetInfo {
43 id: column_as_string!(id, Id::from_str, Id::from_bytes),
44 unit: column_as_string!(unit, CurrencyUnit::from_str),
45 active: matches!(active, Column::Integer(1)),
46 valid_from: column_as_number!(valid_from),
47 derivation_path: column_as_string!(derivation_path, DerivationPath::from_str),
48 derivation_path_index: column_as_nullable_number!(derivation_path_index),
49 amounts,
50 input_fee_ppk: column_as_nullable_number!(row_keyset_ppk).unwrap_or(0),
51 final_expiry: column_as_nullable_number!(valid_to),
52 issuer_version: column_as_nullable_string!(issuer_version).and_then(|v| {
53 match IssuerVersion::from_str(&v) {
54 Ok(ver) => Some(ver),
55 Err(e) => {
56 tracing::warn!(
57 "Failed to parse issuer_version from database: {}. Error: {}",
58 v,
59 e
60 );
61 None
62 }
63 }
64 }),
65 })
66}
67
68const KEYSET_INFO_COLUMNS: &str = r#"
70 id,
71 unit,
72 active,
73 valid_from,
74 valid_to,
75 derivation_path,
76 derivation_path_index,
77 amounts,
78 input_fee_ppk,
79 issuer_version
80"#;
81
82async fn read_active_keysets<C>(conn: &C) -> Result<HashMap<CurrencyUnit, Id>, Error>
87where
88 C: DatabaseExecutor,
89{
90 query(r#"SELECT id, unit FROM keyset WHERE active = :active"#)?
91 .bind("active", true)
92 .fetch_all(conn)
93 .await?
94 .into_iter()
95 .map(|row| {
96 Ok((
97 column_as_string!(&row[1], CurrencyUnit::from_str),
98 column_as_string!(&row[0], Id::from_str, Id::from_bytes),
99 ))
100 })
101 .collect::<Result<HashMap<_, _>, Error>>()
102}
103
104async fn read_keyset_infos<C>(conn: &C) -> Result<Vec<MintKeySetInfo>, Error>
106where
107 C: DatabaseExecutor,
108{
109 query(&format!("SELECT {KEYSET_INFO_COLUMNS} FROM keyset"))?
110 .fetch_all(conn)
111 .await?
112 .into_iter()
113 .map(sql_row_to_keyset_info)
114 .collect::<Result<Vec<_>, _>>()
115}
116
117async fn read_keysets_epoch<C>(conn: &C) -> Result<u64, Error>
120where
121 C: DatabaseExecutor,
122{
123 Ok(
124 match query(r#"SELECT epoch FROM keyset_epoch WHERE id = 0"#)?
125 .pluck(conn)
126 .await?
127 {
128 Some(column) => column_as_number!(column),
129 None => 0,
130 },
131 )
132}
133
134#[async_trait]
135impl<RM> MintKeyDatabaseTransaction<'_, Error> for SQLTransaction<RM>
136where
137 RM: DatabasePool + 'static,
138{
139 async fn add_keyset_info(&mut self, keyset: MintKeySetInfo) -> Result<(), Error> {
140 query(
141 r#"
142 INSERT INTO
143 keyset (
144 id, unit, active, valid_from, valid_to, derivation_path,
145 amounts, input_fee_ppk, derivation_path_index, issuer_version
146 )
147 VALUES (
148 :id, :unit, :active, :valid_from, :valid_to, :derivation_path,
149 :amounts, :input_fee_ppk, :derivation_path_index, :issuer_version
150 )
151 ON CONFLICT(id) DO UPDATE SET
152 unit = excluded.unit,
153 active = excluded.active,
154 valid_from = excluded.valid_from,
155 valid_to = excluded.valid_to,
156 derivation_path = excluded.derivation_path,
157 amounts = excluded.amounts,
158 input_fee_ppk = excluded.input_fee_ppk,
159 derivation_path_index = excluded.derivation_path_index,
160 issuer_version = excluded.issuer_version
161 "#,
162 )?
163 .bind("id", keyset.id.to_string())
164 .bind("unit", keyset.unit.to_string())
165 .bind("active", keyset.active)
166 .bind("valid_from", keyset.valid_from as i64)
167 .bind("valid_to", keyset.final_expiry.map(|v| v as i64))
168 .bind("derivation_path", keyset.derivation_path.to_string())
169 .bind("amounts", serde_json::to_string(&keyset.amounts).ok())
170 .bind("input_fee_ppk", keyset.input_fee_ppk as i64)
171 .bind("derivation_path_index", keyset.derivation_path_index)
172 .bind(
173 "issuer_version",
174 keyset.issuer_version.map(|v| v.to_string()),
175 )
176 .execute(&self.inner)
177 .await?;
178
179 self.bump_keyset_epoch().await?;
180
181 Ok(())
182 }
183
184 async fn set_active_keyset(&mut self, unit: CurrencyUnit, id: Id) -> Result<(), Error> {
185 query(r#"UPDATE keyset SET active=FALSE WHERE unit = :unit"#)?
186 .bind("unit", unit.to_string())
187 .execute(&self.inner)
188 .await?;
189
190 query(r#"UPDATE keyset SET active=TRUE WHERE unit = :unit AND id = :id"#)?
191 .bind("unit", unit.to_string())
192 .bind("id", id.to_string())
193 .execute(&self.inner)
194 .await?;
195
196 self.bump_keyset_epoch().await?;
197
198 Ok(())
199 }
200
201 async fn next_derivation_index(&mut self, unit: &CurrencyUnit) -> Result<u32, Error> {
202 let next = match query(
206 r#"SELECT COALESCE(MAX(derivation_path_index), 0) + 1 FROM keyset WHERE unit = :unit"#,
207 )?
208 .bind("unit", unit.to_string())
209 .pluck(&self.inner)
210 .await?
211 {
212 Some(column) => column_as_number!(column),
213 None => 1,
214 };
215
216 Ok(next)
217 }
218
219 async fn get_keyset_infos_by_unit(
220 &mut self,
221 unit: &CurrencyUnit,
222 ) -> Result<Vec<MintKeySetInfo>, Error> {
223 Ok(query(
227 r#"SELECT
228 id,
229 unit,
230 active,
231 valid_from,
232 valid_to,
233 derivation_path,
234 derivation_path_index,
235 amounts,
236 input_fee_ppk,
237 issuer_version
238 FROM
239 keyset
240 WHERE unit = :unit"#,
241 )?
242 .bind("unit", unit.to_string())
243 .fetch_all(&self.inner)
244 .await?
245 .into_iter()
246 .map(sql_row_to_keyset_info)
247 .collect::<Result<Vec<_>, _>>()?)
248 }
249
250 async fn get_active_keysets(&mut self) -> Result<HashMap<CurrencyUnit, Id>, Error> {
251 read_active_keysets(&self.inner).await
252 }
253
254 async fn get_keyset_infos(&mut self) -> Result<Vec<MintKeySetInfo>, Error> {
255 read_keyset_infos(&self.inner).await
256 }
257
258 async fn keysets_epoch(&mut self) -> Result<u64, Error> {
259 read_keysets_epoch(&self.inner).await
260 }
261}
262
263impl<RM> SQLTransaction<RM>
264where
265 RM: DatabasePool + 'static,
266{
267 async fn lock_keysets(&self) -> Result<(), Error> {
278 if RM::Connection::name() == "postgres" {
279 query(r#"SELECT pg_advisory_xact_lock(hashtext('cdk:keysets'))"#)?
280 .execute(&self.inner)
281 .await?;
282 }
283
284 Ok(())
285 }
286
287 async fn bump_keyset_epoch(&self) -> Result<(), Error> {
296 query(
300 r#"
301 INSERT INTO keyset_epoch (id, epoch) VALUES (0, 1)
302 ON CONFLICT (id) DO UPDATE SET epoch = keyset_epoch.epoch + 1
303 "#,
304 )?
305 .execute(&self.inner)
306 .await?;
307
308 Ok(())
309 }
310}
311
312#[async_trait]
313impl<RM> MintKeysDatabase for SQLMintDatabase<RM>
314where
315 RM: DatabasePool + 'static,
316{
317 type Err = Error;
318
319 async fn begin_transaction<'a>(
320 &'a self,
321 ) -> Result<Box<dyn MintKeyDatabaseTransaction<'a, Error> + Send + Sync + 'a>, Error> {
322 let tx = SQLTransaction {
323 inner: ConnectionWithTransaction::new(
324 self.pool
325 .get()
326 .await
327 .map_err(|e| Error::Database(Box::new(e)))?,
328 )
329 .await?,
330 };
331
332 tx.lock_keysets().await?;
336
337 Ok(Box::new(tx))
338 }
339
340 async fn keysets_epoch(&self) -> Result<u64, Self::Err> {
341 let conn = self
345 .pool
346 .get()
347 .await
348 .map_err(|e| Error::Database(Box::new(e)))?;
349 read_keysets_epoch(&*conn).await
350 }
351}
352
353#[cfg(test)]
354mod test {
355 use super::*;
356
357 mod keyset_amounts_tests {
358 use super::*;
359
360 #[test]
361 fn keyset_with_amounts() {
362 let amounts = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
363 let result = sql_row_to_keyset_info(vec![
364 Column::Text("0083a60439303340".to_owned()),
365 Column::Text("sat".to_owned()),
366 Column::Integer(1),
367 Column::Integer(1749844864),
368 Column::Null,
369 Column::Text("0'/0'/0'".to_owned()),
370 Column::Integer(0),
371 Column::Text(serde_json::to_string(&amounts).expect("valid json")),
372 Column::Integer(0),
373 Column::Text("cdk/0.1.0".to_owned()),
374 ]);
375 assert!(result.is_ok());
376 let keyset = result.unwrap();
377 assert_eq!(keyset.amounts.len(), 32);
378 assert_eq!(
379 keyset.issuer_version,
380 Some(IssuerVersion::from_str("cdk/0.1.0").unwrap())
381 );
382 }
383 }
384}