mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-19 21:55:03 +01:00
feat: mint sqlite
This commit is contained in:
@@ -1,9 +1,7 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"bindings/cdk-js",
|
"bindings/cdk-js",
|
||||||
"crates/cdk",
|
"crates/*",
|
||||||
"crates/cdk-redb",
|
|
||||||
"crates/cdk-rexie",
|
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
@@ -35,6 +33,11 @@ serde_json = "1"
|
|||||||
serde-wasm-bindgen = { version = "0.6.5", default-features = false }
|
serde-wasm-bindgen = { version = "0.6.5", default-features = false }
|
||||||
web-sys = { version = "0.3.68", default-features = false, features = ["console"] }
|
web-sys = { version = "0.3.68", default-features = false, features = ["console"] }
|
||||||
uniffi = { version = "0.27.1", default-features = false }
|
uniffi = { version = "0.27.1", default-features = false }
|
||||||
|
bitcoin = { version = "0.30", features = [
|
||||||
|
"serde",
|
||||||
|
"rand",
|
||||||
|
"rand-std",
|
||||||
|
] } # lightning-invoice uses v0.30
|
||||||
|
|
||||||
[profile]
|
[profile]
|
||||||
|
|
||||||
|
|||||||
30
crates/cdk-sqlite/Cargo.toml
Normal file
30
crates/cdk-sqlite/Cargo.toml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
[package]
|
||||||
|
name = "cdk-sqlite"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
license.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
[features]
|
||||||
|
default = ["mint", "wallet"]
|
||||||
|
mint = ["cdk/mint"]
|
||||||
|
wallet = ["cdk/wallet"]
|
||||||
|
nostr = ["cdk/nostr"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bitcoin.workspace = true
|
||||||
|
const_format = "0.2.32"
|
||||||
|
sqlx = { version = "0.6.3", default-features = false, features = ["runtime-tokio-rustls", "chrono", "sqlite"] }
|
||||||
|
cdk = { workspace = true, default-features = false }
|
||||||
|
thiserror.workspace = true
|
||||||
|
tokio = { workspace = true, features = [
|
||||||
|
"time",
|
||||||
|
"macros",
|
||||||
|
"sync",
|
||||||
|
] }
|
||||||
|
tracing.workspace = true
|
||||||
|
async-trait.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
7
crates/cdk-sqlite/src/lib.rs
Normal file
7
crates/cdk-sqlite/src/lib.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#[cfg(feature = "mint")]
|
||||||
|
pub mod mint;
|
||||||
|
#[cfg(feature = "wallet")]
|
||||||
|
pub mod wallet;
|
||||||
|
|
||||||
|
#[cfg(feature = "mint")]
|
||||||
|
pub use mint::MintSqliteDatabase;
|
||||||
29
crates/cdk-sqlite/src/mint/error.rs
Normal file
29
crates/cdk-sqlite/src/mint/error.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum Error {
|
||||||
|
/// SQLX Error
|
||||||
|
#[error(transparent)]
|
||||||
|
SQLX(#[from] sqlx::Error),
|
||||||
|
/// NUT02 Error
|
||||||
|
#[error(transparent)]
|
||||||
|
CDKNUT02(#[from] cdk::nuts::nut02::Error),
|
||||||
|
/// NUT01 Error
|
||||||
|
#[error(transparent)]
|
||||||
|
CDKNUT01(#[from] cdk::nuts::nut01::Error),
|
||||||
|
/// Secret Error
|
||||||
|
#[error(transparent)]
|
||||||
|
CDKSECRET(#[from] cdk::secret::Error),
|
||||||
|
/// BIP32 Error
|
||||||
|
#[error(transparent)]
|
||||||
|
BIP32(#[from] bitcoin::bip32::Error),
|
||||||
|
/// Could Not Initialize Db
|
||||||
|
#[error("Could not initialize Db")]
|
||||||
|
CouldNotInitialize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Error> for cdk::cdk_database::Error {
|
||||||
|
fn from(e: Error) -> Self {
|
||||||
|
Self::Database(Box::new(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
106
crates/cdk-sqlite/src/mint/migration.rs
Normal file
106
crates/cdk-sqlite/src/mint/migration.rs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
use const_format::formatcp;
|
||||||
|
use sqlx::{Executor, Pool, Sqlite};
|
||||||
|
|
||||||
|
use super::error::Error;
|
||||||
|
|
||||||
|
/// Latest database version
|
||||||
|
pub const DB_VERSION: usize = 0;
|
||||||
|
|
||||||
|
/// Schema definition
|
||||||
|
const INIT_SQL: &str = formatcp!(
|
||||||
|
r#"
|
||||||
|
-- Database settings
|
||||||
|
PRAGMA encoding = "UTF-8";
|
||||||
|
PRAGMA journal_mode = WAL;
|
||||||
|
PRAGMA auto_vacuum = FULL;
|
||||||
|
PRAGMA main.synchronous=NORMAL;
|
||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
PRAGMA user_version = {};
|
||||||
|
|
||||||
|
-- Proof Table
|
||||||
|
CREATE TABLE IF NOT EXISTS proof (
|
||||||
|
y BLOB PRIMARY KEY,
|
||||||
|
amount INTEGER NOT NULL,
|
||||||
|
keyset_id TEXT NOT NULL,
|
||||||
|
secret TEXT NOT NULL,
|
||||||
|
c BLOB NOT NULL,
|
||||||
|
witness TEXT,
|
||||||
|
state TEXT CHECK ( state IN ('SPENT', 'PENDING' ) ) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS state_index ON proof(state);
|
||||||
|
CREATE INDEX IF NOT EXISTS secret_index ON proof(secret);
|
||||||
|
|
||||||
|
-- Keysets Table
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS keyset (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
unit TEXT NOT NULL,
|
||||||
|
active BOOL NOT NULL,
|
||||||
|
valid_from INTEGER NOT NULL,
|
||||||
|
valid_to INTEGER,
|
||||||
|
derivation_path TEXT NOT NULL,
|
||||||
|
max_order INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS unit_index ON keyset(unit);
|
||||||
|
CREATE INDEX IF NOT EXISTS active_index ON keyset(active);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS mint_quote (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
mint_url TEXT NOT NULL,
|
||||||
|
amount INTEGER NOT NULL,
|
||||||
|
unit TEXT NOT NULL,
|
||||||
|
request TEXT NOT NULL,
|
||||||
|
paid BOOL NOT NULL DEFAULT FALSE,
|
||||||
|
expiry INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS paid_index ON mint_quote(paid);
|
||||||
|
CREATE INDEX IF NOT EXISTS request_index ON mint_quote(request);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS melt_quote (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
unit TEXT NOT NULL,
|
||||||
|
amount INTEGER NOT NULL,
|
||||||
|
request TEXT NOT NULL,
|
||||||
|
fee_reserve INTEGER NOT NULL,
|
||||||
|
paid BOOL NOT NULL DEFAULT FALSE,
|
||||||
|
expiry INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS paid_index ON melt_quote(paid);
|
||||||
|
CREATE INDEX IF NOT EXISTS request_index ON melt_quote(request);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS blind_signature (
|
||||||
|
y BLOB PRIMARY KEY,
|
||||||
|
amount INTEGER NOT NULL,
|
||||||
|
keyset_id TEXT NOT NULL,
|
||||||
|
c BLOB NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS keyset_id_index ON blind_signature(keyset_id);
|
||||||
|
|
||||||
|
"#,
|
||||||
|
DB_VERSION
|
||||||
|
);
|
||||||
|
|
||||||
|
pub(crate) async fn init_migration(pool: &Pool<Sqlite>) -> Result<usize, Error> {
|
||||||
|
let mut conn = pool.acquire().await?;
|
||||||
|
|
||||||
|
match conn.execute(INIT_SQL).await {
|
||||||
|
Ok(_) => {
|
||||||
|
tracing::info!(
|
||||||
|
"database pragma/schema initialized to v{}, and ready",
|
||||||
|
DB_VERSION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("update (init) failed: {}", err);
|
||||||
|
return Err(Error::CouldNotInitialize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(DB_VERSION)
|
||||||
|
}
|
||||||
641
crates/cdk-sqlite/src/mint/mod.rs
Normal file
641
crates/cdk-sqlite/src/mint/mod.rs
Normal file
@@ -0,0 +1,641 @@
|
|||||||
|
//! SQLite
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use bitcoin::bip32::DerivationPath;
|
||||||
|
use cdk::cdk_database::{self, MintDatabase};
|
||||||
|
use cdk::mint::MintKeySetInfo;
|
||||||
|
use cdk::nuts::{BlindSignature, CurrencyUnit, Id, Proof, PublicKey};
|
||||||
|
use cdk::secret::Secret;
|
||||||
|
use cdk::types::{MeltQuote, MintQuote};
|
||||||
|
use cdk::Amount;
|
||||||
|
use error::Error;
|
||||||
|
use migration::init_migration;
|
||||||
|
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqliteRow};
|
||||||
|
use sqlx::{ConnectOptions, Row};
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
|
mod migration;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MintSqliteDatabase {
|
||||||
|
pool: SqlitePool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MintSqliteDatabase {
|
||||||
|
pub async fn new(path: &str) -> Result<Self, Error> {
|
||||||
|
let _conn = SqliteConnectOptions::from_str(path)?
|
||||||
|
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
||||||
|
.read_only(false)
|
||||||
|
.create_if_missing(true)
|
||||||
|
.auto_vacuum(sqlx::sqlite::SqliteAutoVacuum::Full)
|
||||||
|
.connect()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let pool = SqlitePool::connect(path).await?;
|
||||||
|
|
||||||
|
init_migration(&pool).await?;
|
||||||
|
|
||||||
|
Ok(Self { pool })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl MintDatabase for MintSqliteDatabase {
|
||||||
|
type Err = cdk_database::Error;
|
||||||
|
|
||||||
|
async fn add_active_keyset(&self, unit: CurrencyUnit, id: Id) -> Result<(), Self::Err> {
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
UPDATE keyset
|
||||||
|
SET active=TRUE
|
||||||
|
WHERE unit IS ?
|
||||||
|
AND id IS ?;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(unit.to_string())
|
||||||
|
.bind(id.to_string())
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
// TODO: should check if error is not found and return none
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn get_active_keyset_id(&self, unit: &CurrencyUnit) -> Result<Option<Id>, Self::Err> {
|
||||||
|
let rec = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT id
|
||||||
|
FROM keyset
|
||||||
|
WHERE active = 1
|
||||||
|
AND unit IS ?
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(unit.to_string())
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let rec = match rec {
|
||||||
|
Ok(rec) => rec,
|
||||||
|
Err(err) => match err {
|
||||||
|
sqlx::Error::RowNotFound => return Ok(None),
|
||||||
|
_ => return Err(Error::SQLX(err).into()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(
|
||||||
|
Id::from_str(rec.try_get("id").map_err(Error::from)?).map_err(Error::from)?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
async fn get_active_keysets(&self) -> Result<HashMap<CurrencyUnit, Id>, Self::Err> {
|
||||||
|
let recs = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT id, unit
|
||||||
|
FROM keyset
|
||||||
|
WHERE active = 1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
// TODO: should check if error is not found and return none
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
|
||||||
|
let keysets = recs
|
||||||
|
.iter()
|
||||||
|
.filter_map(|r| match Id::from_str(r.get("id")) {
|
||||||
|
Ok(id) => Some((CurrencyUnit::from(r.get::<'_, &str, &str>("unit")), id)),
|
||||||
|
Err(_) => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(keysets)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Self::Err> {
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT OR REPLACE INTO mint_quote
|
||||||
|
(id, mint_url, amount, unit, request, paid, expiry)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(quote.id.to_string())
|
||||||
|
.bind(quote.mint_url.to_string())
|
||||||
|
.bind(u64::from(quote.amount) as i64)
|
||||||
|
.bind(quote.unit.to_string())
|
||||||
|
.bind(quote.request)
|
||||||
|
.bind(quote.paid)
|
||||||
|
.bind(quote.expiry as i64)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
// TODO: should check if error is not found and return none
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<MintQuote>, Self::Err> {
|
||||||
|
let rec = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT *
|
||||||
|
FROM mint_quote
|
||||||
|
WHERE id=?;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(quote_id)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let rec = match rec {
|
||||||
|
Ok(rec) => rec,
|
||||||
|
Err(err) => match err {
|
||||||
|
sqlx::Error::RowNotFound => return Ok(None),
|
||||||
|
_ => return Err(Error::SQLX(err).into()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(sqlite_row_to_mint_quote(rec)?))
|
||||||
|
}
|
||||||
|
async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> {
|
||||||
|
let rec = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT *
|
||||||
|
FROM mint_quote
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
|
||||||
|
let mint_quotes = rec.into_iter().flat_map(sqlite_row_to_mint_quote).collect();
|
||||||
|
|
||||||
|
Ok(mint_quotes)
|
||||||
|
}
|
||||||
|
async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err> {
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
DELETE FROM mint_quote
|
||||||
|
WHERE id=?
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(quote_id)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Self::Err> {
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT OR REPLACE INTO melt_quote
|
||||||
|
(id, unit, amount, request, fee_reserve, paid, expiry)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(quote.id.to_string())
|
||||||
|
.bind(quote.unit.to_string())
|
||||||
|
.bind(u64::from(quote.amount) as i64)
|
||||||
|
.bind(quote.request)
|
||||||
|
.bind(u64::from(quote.fee_reserve) as i64)
|
||||||
|
.bind(quote.paid)
|
||||||
|
.bind(quote.expiry as i64)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Self::Err> {
|
||||||
|
let rec = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT *
|
||||||
|
FROM melt_quote
|
||||||
|
WHERE id=?;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(quote_id)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let rec = match rec {
|
||||||
|
Ok(rec) => rec,
|
||||||
|
Err(err) => match err {
|
||||||
|
sqlx::Error::RowNotFound => return Ok(None),
|
||||||
|
_ => return Err(Error::SQLX(err).into()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(sqlite_row_to_melt_quote(rec)?))
|
||||||
|
}
|
||||||
|
async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, Self::Err> {
|
||||||
|
let rec = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT *
|
||||||
|
FROM melt_quote
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
|
||||||
|
let melt_quotes = rec.into_iter().flat_map(sqlite_row_to_melt_quote).collect();
|
||||||
|
|
||||||
|
Ok(melt_quotes)
|
||||||
|
}
|
||||||
|
async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err> {
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
DELETE FROM melt_quote
|
||||||
|
WHERE id=?
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(quote_id)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err> {
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO keyset
|
||||||
|
(id, unit, active, valid_from, valid_to, derivation_path, max_order)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(keyset.id.to_string())
|
||||||
|
.bind(keyset.unit.to_string())
|
||||||
|
.bind(keyset.active)
|
||||||
|
.bind(keyset.valid_from as i64)
|
||||||
|
.bind(keyset.valid_to.map(|v| v as i64))
|
||||||
|
.bind(keyset.derivation_path.to_string())
|
||||||
|
.bind(keyset.max_order)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn get_keyset_info(&self, id: &Id) -> Result<Option<MintKeySetInfo>, Self::Err> {
|
||||||
|
let rec = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT *
|
||||||
|
FROM keyset
|
||||||
|
WHERE id=?;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id.to_string())
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let rec = match rec {
|
||||||
|
Ok(rec) => rec,
|
||||||
|
Err(err) => match err {
|
||||||
|
sqlx::Error::RowNotFound => return Ok(None),
|
||||||
|
_ => return Err(Error::SQLX(err).into()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(sqlite_row_to_keyset_info(rec)?))
|
||||||
|
}
|
||||||
|
async fn get_keyset_infos(&self) -> Result<Vec<MintKeySetInfo>, Self::Err> {
|
||||||
|
let recs = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT *
|
||||||
|
FROM keyset;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(recs
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(sqlite_row_to_keyset_info)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_spent_proof(&self, proof: Proof) -> Result<(), Self::Err> {
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT OR REPLACE INTO proof
|
||||||
|
(y, amount, keyset_id, secret, c, witness, state)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(proof.y()?.to_bytes().to_vec())
|
||||||
|
.bind(u64::from(proof.amount) as i64)
|
||||||
|
.bind(proof.keyset_id.to_string())
|
||||||
|
.bind(proof.secret.to_string())
|
||||||
|
.bind(proof.c.to_bytes().to_vec())
|
||||||
|
.bind(proof.witness.map(|w| serde_json::to_string(&w).unwrap()))
|
||||||
|
.bind("SPENT")
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn get_spent_proof_by_secret(&self, secret: &Secret) -> Result<Option<Proof>, Self::Err> {
|
||||||
|
let rec = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT *
|
||||||
|
FROM proof
|
||||||
|
WHERE secret=?
|
||||||
|
AND state="SPENT";
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(secret.to_string())
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let rec = match rec {
|
||||||
|
Ok(rec) => rec,
|
||||||
|
Err(err) => match err {
|
||||||
|
sqlx::Error::RowNotFound => return Ok(None),
|
||||||
|
_ => return Err(Error::SQLX(err).into()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(sqlite_row_to_proof(rec)?))
|
||||||
|
}
|
||||||
|
async fn get_spent_proof_by_y(&self, y: &PublicKey) -> Result<Option<Proof>, Self::Err> {
|
||||||
|
let rec = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT *
|
||||||
|
FROM proof
|
||||||
|
WHERE y=?
|
||||||
|
AND state="SPENT";
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(y.to_bytes().to_vec())
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let rec = match rec {
|
||||||
|
Ok(rec) => rec,
|
||||||
|
Err(err) => match err {
|
||||||
|
sqlx::Error::RowNotFound => return Ok(None),
|
||||||
|
_ => return Err(Error::SQLX(err).into()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(sqlite_row_to_proof(rec)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_pending_proof(&self, proof: Proof) -> Result<(), Self::Err> {
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT OR REPLACE INTO proof
|
||||||
|
(y, amount, keyset_id, secret, c, witness, spent, pending)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(proof.y()?.to_bytes().to_vec())
|
||||||
|
.bind(u64::from(proof.amount) as i64)
|
||||||
|
.bind(proof.keyset_id.to_string())
|
||||||
|
.bind(proof.secret.to_string())
|
||||||
|
.bind(proof.c.to_bytes().to_vec())
|
||||||
|
.bind(proof.witness.map(|w| serde_json::to_string(&w).unwrap()))
|
||||||
|
.bind("PENDING")
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn get_pending_proof_by_secret(
|
||||||
|
&self,
|
||||||
|
secret: &Secret,
|
||||||
|
) -> Result<Option<Proof>, Self::Err> {
|
||||||
|
let rec = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT *
|
||||||
|
FROM proof
|
||||||
|
WHERE secret=?
|
||||||
|
AND state="PENDING";
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(secret.to_string())
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let rec = match rec {
|
||||||
|
Ok(rec) => rec,
|
||||||
|
Err(err) => match err {
|
||||||
|
sqlx::Error::RowNotFound => return Ok(None),
|
||||||
|
_ => return Err(Error::SQLX(err).into()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(sqlite_row_to_proof(rec)?))
|
||||||
|
}
|
||||||
|
async fn get_pending_proof_by_y(&self, y: &PublicKey) -> Result<Option<Proof>, Self::Err> {
|
||||||
|
let rec = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT *
|
||||||
|
FROM proof
|
||||||
|
WHERE y=?
|
||||||
|
AND state="PENDING";
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(y.to_bytes().to_vec())
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let rec = match rec {
|
||||||
|
Ok(rec) => rec,
|
||||||
|
Err(err) => match err {
|
||||||
|
sqlx::Error::RowNotFound => return Ok(None),
|
||||||
|
_ => return Err(Error::SQLX(err).into()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Ok(Some(sqlite_row_to_proof(rec)?))
|
||||||
|
}
|
||||||
|
async fn remove_pending_proof(&self, secret: &Secret) -> Result<(), Self::Err> {
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
DELETE FROM proof
|
||||||
|
WHERE secret=?
|
||||||
|
AND state="PENDING";
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(secret.to_string())
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_blinded_signature(
|
||||||
|
&self,
|
||||||
|
blinded_message: PublicKey,
|
||||||
|
blinded_signature: BlindSignature,
|
||||||
|
) -> Result<(), Self::Err> {
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO blind_signature
|
||||||
|
(y, amount, keyset_id, c)
|
||||||
|
VALUES (?, ?, ?, ?);
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(blinded_message.to_bytes().to_vec())
|
||||||
|
.bind(u64::from(blinded_signature.amount) as i64)
|
||||||
|
.bind(blinded_signature.keyset_id.to_string())
|
||||||
|
.bind(blinded_signature.c.to_bytes().to_vec())
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn get_blinded_signature(
|
||||||
|
&self,
|
||||||
|
blinded_message: &PublicKey,
|
||||||
|
) -> Result<Option<BlindSignature>, Self::Err> {
|
||||||
|
let rec = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT *
|
||||||
|
FROM blind_signature
|
||||||
|
WHERE y=?;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(blinded_message.to_bytes().to_vec())
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let rec = match rec {
|
||||||
|
Ok(rec) => rec,
|
||||||
|
Err(err) => match err {
|
||||||
|
sqlx::Error::RowNotFound => return Ok(None),
|
||||||
|
_ => return Err(Error::SQLX(err).into()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(sqlite_row_to_blind_signature(rec)?))
|
||||||
|
}
|
||||||
|
async fn get_blinded_signatures(
|
||||||
|
&self,
|
||||||
|
blinded_messages: Vec<PublicKey>,
|
||||||
|
) -> Result<Vec<Option<BlindSignature>>, Self::Err> {
|
||||||
|
let mut signatures = Vec::with_capacity(blinded_messages.len());
|
||||||
|
for message in blinded_messages {
|
||||||
|
let rec = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT *
|
||||||
|
FROM blind_signature
|
||||||
|
WHERE y=?;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(message.to_bytes().to_vec())
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Ok(row) = rec {
|
||||||
|
let blinded = sqlite_row_to_blind_signature(row)?;
|
||||||
|
|
||||||
|
signatures.push(Some(blinded));
|
||||||
|
} else {
|
||||||
|
signatures.push(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(signatures)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sqlite_row_to_keyset_info(row: SqliteRow) -> Result<MintKeySetInfo, Error> {
|
||||||
|
let row_id: String = row.try_get("id").map_err(Error::from)?;
|
||||||
|
let row_unit: String = row.try_get("unit").map_err(Error::from)?;
|
||||||
|
let row_active: bool = row.try_get("active").map_err(Error::from)?;
|
||||||
|
let row_valid_from: i64 = row.try_get("valid_from").map_err(Error::from)?;
|
||||||
|
let row_valid_to: Option<i64> = row.try_get("valid_to").map_err(Error::from)?;
|
||||||
|
let row_derivation_path: String = row.try_get("derivation_path").map_err(Error::from)?;
|
||||||
|
let row_max_order: u8 = row.try_get("max_order").map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(MintKeySetInfo {
|
||||||
|
id: Id::from_str(&row_id).map_err(Error::from)?,
|
||||||
|
unit: CurrencyUnit::from(&row_unit),
|
||||||
|
active: row_active,
|
||||||
|
valid_from: row_valid_from as u64,
|
||||||
|
valid_to: row_valid_to.map(|v| v as u64),
|
||||||
|
derivation_path: DerivationPath::from_str(&row_derivation_path).map_err(Error::from)?,
|
||||||
|
max_order: row_max_order,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result<MintQuote, Error> {
|
||||||
|
let row_id: String = row.try_get("id").map_err(Error::from)?;
|
||||||
|
let row_mint_url: String = row.try_get("mint_url").map_err(Error::from)?;
|
||||||
|
let row_amount: i64 = row.try_get("amount").map_err(Error::from)?;
|
||||||
|
let row_unit: String = row.try_get("unit").map_err(Error::from)?;
|
||||||
|
let row_request: String = row.try_get("request").map_err(Error::from)?;
|
||||||
|
let row_paid: bool = row.try_get("paid").map_err(Error::from)?;
|
||||||
|
let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(MintQuote {
|
||||||
|
id: row_id,
|
||||||
|
mint_url: row_mint_url.into(),
|
||||||
|
amount: Amount::from(row_amount as u64),
|
||||||
|
unit: CurrencyUnit::from(row_unit),
|
||||||
|
request: row_request,
|
||||||
|
paid: row_paid,
|
||||||
|
expiry: row_expiry as u64,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sqlite_row_to_melt_quote(row: SqliteRow) -> Result<MeltQuote, Error> {
|
||||||
|
let row_id: String = row.try_get("id").map_err(Error::from)?;
|
||||||
|
let row_unit: String = row.try_get("unit").map_err(Error::from)?;
|
||||||
|
let row_amount: i64 = row.try_get("amount").map_err(Error::from)?;
|
||||||
|
let row_request: String = row.try_get("request").map_err(Error::from)?;
|
||||||
|
let row_fee_reserve: i64 = row.try_get("fee_reserve").map_err(Error::from)?;
|
||||||
|
let row_paid: bool = row.try_get("paid").map_err(Error::from)?;
|
||||||
|
let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(MeltQuote {
|
||||||
|
id: row_id,
|
||||||
|
amount: Amount::from(row_amount as u64),
|
||||||
|
unit: CurrencyUnit::from(row_unit),
|
||||||
|
request: row_request,
|
||||||
|
fee_reserve: Amount::from(row_fee_reserve as u64),
|
||||||
|
paid: row_paid,
|
||||||
|
expiry: row_expiry as u64,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sqlite_row_to_proof(row: SqliteRow) -> Result<Proof, Error> {
|
||||||
|
let row_amount: i64 = row.try_get("amount").map_err(Error::from)?;
|
||||||
|
let keyset_id: String = row.try_get("keyset_id").map_err(Error::from)?;
|
||||||
|
let row_secret: String = row.try_get("secret").map_err(Error::from)?;
|
||||||
|
let row_c: Vec<u8> = row.try_get("c").map_err(Error::from)?;
|
||||||
|
let row_witness: Option<String> = row.try_get("witness").map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(Proof {
|
||||||
|
amount: Amount::from(row_amount as u64),
|
||||||
|
keyset_id: Id::from_str(&keyset_id)?,
|
||||||
|
secret: Secret::from_str(&row_secret)?,
|
||||||
|
c: PublicKey::from_slice(&row_c)?,
|
||||||
|
witness: row_witness.and_then(|w| serde_json::from_str(&w).ok()),
|
||||||
|
dleq: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sqlite_row_to_blind_signature(row: SqliteRow) -> Result<BlindSignature, Error> {
|
||||||
|
let row_amount: i64 = row.try_get("amount").map_err(Error::from)?;
|
||||||
|
let keyset_id: String = row.try_get("keyset_id").map_err(Error::from)?;
|
||||||
|
let row_c: Vec<u8> = row.try_get("c").map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(BlindSignature {
|
||||||
|
amount: Amount::from(row_amount as u64),
|
||||||
|
keyset_id: Id::from_str(&keyset_id)?,
|
||||||
|
c: PublicKey::from_slice(&row_c)?,
|
||||||
|
dleq: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
1
crates/cdk-sqlite/src/wallet/mod.rs
Normal file
1
crates/cdk-sqlite/src/wallet/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -17,15 +17,15 @@ nostr = ["dep:nostr-sdk"]
|
|||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = "0.1"
|
async-trait.workspace = true
|
||||||
base64 = "0.22" # bitcoin uses v0.13 (optional dep)
|
base64 = "0.22" # bitcoin uses v0.13 (optional dep)
|
||||||
bip39 = "2.0"
|
bip39 = "2.0"
|
||||||
bitcoin = { version = "0.30", features = [
|
http = "1.0"
|
||||||
|
bitcoin = { workspace = true, features = [
|
||||||
"serde",
|
"serde",
|
||||||
"rand",
|
"rand",
|
||||||
"rand-std",
|
"rand-std",
|
||||||
] } # lightning-invoice uses v0.30
|
] }
|
||||||
http = "1.0"
|
|
||||||
lightning-invoice = { version = "0.31", features = ["serde"] }
|
lightning-invoice = { version = "0.31", features = ["serde"] }
|
||||||
once_cell = "1.19"
|
once_cell = "1.19"
|
||||||
reqwest = { version = "0.12", default-features = false, features = [
|
reqwest = { version = "0.12", default-features = false, features = [
|
||||||
|
|||||||
6
flake.lock
generated
6
flake.lock
generated
@@ -175,11 +175,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1716633019,
|
"lastModified": 1717159533,
|
||||||
"narHash": "sha256-xim1b5/HZYbWaZKyI7cn9TJCM6ewNVZnesRr00mXeS4=",
|
"narHash": "sha256-oamiKNfr2MS6yH64rUn99mIZjc45nGJlj9eGth/3Xuw=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "9d29cd266cebf80234c98dd0b87256b6be0af44e",
|
"rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ buildargs=(
|
|||||||
"-p cdk-redb --no-default-features --features wallet"
|
"-p cdk-redb --no-default-features --features wallet"
|
||||||
"-p cdk-redb --no-default-features --features wallet --features nostr"
|
"-p cdk-redb --no-default-features --features wallet --features nostr"
|
||||||
"-p cdk-redb --no-default-features --features mint"
|
"-p cdk-redb --no-default-features --features mint"
|
||||||
|
"-p cdk-sqlite --no-default-features --features mint"
|
||||||
|
"-p cdk-sqlite --no-default-features --features wallet"
|
||||||
"--examples"
|
"--examples"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user