feat: wallet sqlite

This commit is contained in:
thesimplekid
2024-06-05 17:14:26 +01:00
parent bbc63306db
commit e1506c4e34
7 changed files with 909 additions and 2 deletions

View File

@@ -5,3 +5,5 @@ pub mod wallet;
#[cfg(feature = "mint")]
pub use mint::MintSqliteDatabase;
#[cfg(feature = "wallet")]
pub use wallet::WalletSQLiteDatabase;

View File

@@ -1,3 +1,5 @@
//! SQLite Mint Migration
use const_format::formatcp;
use sqlx::{Executor, Pool, Sqlite};

View File

@@ -1,4 +1,4 @@
//! SQLite
//! SQLite Mint
use std::collections::HashMap;
use std::str::FromStr;

View File

@@ -0,0 +1,38 @@
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
/// SQLX Error
#[error(transparent)]
SQLX(#[from] sqlx::Error),
/// NUT02 Error
#[error(transparent)]
Serde(#[from] serde_json::Error),
/// NUT02 Error
#[error(transparent)]
CDKWallet(#[from] cdk::wallet::error::Error),
/// NUT07 Error
#[error(transparent)]
CDKNUT07(#[from] cdk::nuts::nut07::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))
}
}

View File

@@ -0,0 +1,124 @@
//! SQLite Wallet Migration
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 = {};
-- Mints
CREATE TABLE IF NOT EXISTS mint (
mint_url TEXT PRIMARY KEY,
name TEXT,
pubkey BLOB,
version TEXT,
description TEXT,
description_long TEXT,
contact TEXT,
nuts TEXT,
motd TEXT
);
CREATE TABLE IF NOT EXISTS keyset (
id TEXT PRIMARY KEY,
mint_url TEXT NOT NULL,
unit TEXT NOT NULL,
active BOOL NOT NULL,
counter INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(mint_url) REFERENCES mint(mint_url) ON UPDATE CASCADE ON DELETE CASCADE
);
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 key (
id TEXT PRIMARY KEY,
keys TEXT NOT NULL
);
-- Proof Table
CREATE TABLE IF NOT EXISTS proof (
y BLOB PRIMARY KEY,
mint_url TEXT NOT NULL,
state TEXT CHECK ( state IN ('SPENT', 'UNSPENT', 'PENDING', 'RESERVED' ) ) NOT NULL,
spending_condition TEXT,
unit TEXT NOT NULL,
amount INTEGER NOT NULL,
keyset_id TEXT NOT NULL,
secret TEXT NOT NULL,
c BLOB NOT NULL,
witness TEXT
);
CREATE INDEX IF NOT EXISTS secret_index ON proof(secret);
CREATE INDEX IF NOT EXISTS state_index ON proof(state);
CREATE INDEX IF NOT EXISTS spending_condition_index ON proof(spending_condition);
CREATE INDEX IF NOT EXISTS unit_index ON proof(unit);
CREATE INDEX IF NOT EXISTS amount_index ON proof(amount);
CREATE INDEX IF NOT EXISTS mint_url_index ON proof(mint_url);
CREATE TABLE IF NOT EXISTS nostr_last_checked (
key BLOB PRIMARY KEY,
last_check INTEGER NOT NULL
);
"#,
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)
}

View File

@@ -1 +1,743 @@
//! SQLite Wallet Database
use std::collections::HashMap;
use std::str::FromStr;
use async_trait::async_trait;
use cdk::cdk_database::{self, WalletDatabase};
use cdk::nuts::{
CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proof, Proofs, PublicKey, SpendingConditions,
State,
};
use cdk::secret::Secret;
use cdk::types::{MeltQuote, MintQuote, ProofInfo};
use cdk::url::UncheckedUrl;
use cdk::Amount;
use error::Error;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqliteRow};
use sqlx::{ConnectOptions, Row};
use self::migration::init_migration;
pub mod error;
mod migration;
#[derive(Debug, Clone)]
pub struct WalletSQLiteDatabase {
pool: SqlitePool,
}
impl WalletSQLiteDatabase {
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 WalletDatabase for WalletSQLiteDatabase {
type Err = cdk_database::Error;
async fn add_mint(
&self,
mint_url: UncheckedUrl,
mint_info: Option<MintInfo>,
) -> Result<(), Self::Err> {
let (name, pubkey, version, description, description_long, contact, nuts, motd) =
match mint_info {
Some(mint_info) => {
let MintInfo {
name,
pubkey,
version,
description,
description_long,
contact,
nuts,
motd,
} = mint_info;
(
name,
pubkey.map(|p| p.to_bytes().to_vec()),
version.map(|v| serde_json::to_string(&v).ok()),
description,
description_long,
contact.map(|c| serde_json::to_string(&c).ok()),
serde_json::to_string(&nuts).ok(),
motd,
)
}
None => (None, None, None, None, None, None, None, None),
};
sqlx::query(
r#"
INSERT OR REPLACE INTO mint
(mint_url, name, pubkey, version, description, description_long, contact, nuts, motd)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
"#,
)
.bind(mint_url.to_string())
.bind(name)
.bind(pubkey)
.bind(version)
.bind(description)
.bind(description_long)
.bind(contact)
.bind(nuts)
.bind(motd)
.execute(&self.pool)
.await
.map_err(Error::from)?;
Ok(())
}
async fn get_mint(&self, mint_url: UncheckedUrl) -> Result<Option<MintInfo>, Self::Err> {
let rec = sqlx::query(
r#"
SELECT *
FROM mint
WHERE mint_url=?;
"#,
)
.bind(mint_url.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_mint_info(&rec)?))
}
async fn get_mints(&self) -> Result<HashMap<UncheckedUrl, Option<MintInfo>>, Self::Err> {
let rec = sqlx::query(
r#"
SELECT *
FROM mint
"#,
)
.fetch_all(&self.pool)
.await
.map_err(Error::from)?;
let mints = rec
.into_iter()
.map(|row| {
let mint_url: String = row.get("mint_url");
let mint_info = sqlite_row_to_mint_info(&row).ok();
(mint_url.into(), mint_info)
})
.collect();
Ok(mints)
}
async fn add_mint_keysets(
&self,
mint_url: UncheckedUrl,
keysets: Vec<KeySetInfo>,
) -> Result<(), Self::Err> {
for keyset in keysets {
sqlx::query(
r#"
INSERT OR REPLACE INTO keyset
(mint_url, id, unit, active)
VALUES (?, ?, ?, ?);
"#,
)
.bind(mint_url.to_string())
.bind(keyset.id.to_string())
.bind(keyset.unit.to_string())
.bind(keyset.active)
.execute(&self.pool)
.await
.map_err(Error::from)?;
}
Ok(())
}
async fn get_mint_keysets(
&self,
mint_url: UncheckedUrl,
) -> Result<Option<Vec<KeySetInfo>>, Self::Err> {
let recs = sqlx::query(
r#"
SELECT *
FROM keyset
WHERE mint_url=?
"#,
)
.bind(mint_url.to_string())
.fetch_all(&self.pool)
.await;
let recs = match recs {
Ok(recs) => recs,
Err(err) => match err {
sqlx::Error::RowNotFound => return Ok(None),
_ => return Err(Error::SQLX(err).into()),
},
};
let keysets: Vec<KeySetInfo> = recs.iter().flat_map(sqlite_row_to_keyset).collect();
match keysets.is_empty() {
false => Ok(Some(keysets)),
true => Ok(None),
}
}
async fn get_keyset_by_id(&self, keyset_id: &Id) -> Result<Option<KeySetInfo>, Self::Err> {
let rec = sqlx::query(
r#"
SELECT *
FROM keyset
WHERE id=?
"#,
)
.bind(keyset_id.to_string())
.fetch_one(&self.pool)
.await;
let rec = match rec {
Ok(recs) => recs,
Err(err) => match err {
sqlx::Error::RowNotFound => return Ok(None),
_ => return Err(Error::SQLX(err).into()),
},
};
Ok(Some(sqlite_row_to_keyset(&rec)?))
}
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
.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.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 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_keys(&self, keys: Keys) -> Result<(), Self::Err> {
sqlx::query(
r#"
INSERT OR REPLACE INTO key
(id, keys)
VALUES (?, ?);
"#,
)
.bind(Id::from(&keys).to_string())
.bind(serde_json::to_string(&keys).map_err(Error::from)?)
.execute(&self.pool)
.await
.map_err(Error::from)?;
Ok(())
}
async fn get_keys(&self, id: &Id) -> Result<Option<Keys>, Self::Err> {
let rec = sqlx::query(
r#"
SELECT *
FROM key
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()),
},
};
let keys: String = rec.get("keys");
Ok(serde_json::from_str(&keys).map_err(Error::from)?)
}
async fn remove_keys(&self, id: &Id) -> Result<(), Self::Err> {
sqlx::query(
r#"
DELETE FROM key
WHERE id=?
"#,
)
.bind(id.to_string())
.execute(&self.pool)
.await
.map_err(Error::from)?;
Ok(())
}
async fn add_proofs(&self, proof_info: Vec<ProofInfo>) -> Result<(), Self::Err> {
for proof in proof_info {
sqlx::query(
r#"
INSERT OR REPLACE INTO proof
(y, mint_url, state, spending_condition, unit, amount, keyset_id, secret, c, witness)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
"#,
)
.bind(proof.y.to_bytes().to_vec())
.bind(proof.mint_url.to_string())
.bind(proof.state.to_string())
.bind(
proof
.spending_condition
.map(|s| serde_json::to_string(&s).ok()),
)
.bind(proof.unit.to_string())
.bind(u64::from(proof.proof.amount) as i64)
.bind(proof.proof.keyset_id.to_string())
.bind(proof.proof.secret.to_string())
.bind(proof.proof.c.to_bytes().to_vec())
.bind(
proof
.proof
.witness
.map(|w| serde_json::to_string(&w).unwrap()),
)
.execute(&self.pool)
.await
.map_err(Error::from)?;
}
Ok(())
}
async fn get_proofs(
&self,
mint_url: Option<UncheckedUrl>,
unit: Option<CurrencyUnit>,
state: Option<Vec<State>>,
spending_conditions: Option<Vec<SpendingConditions>>,
) -> Result<Option<Vec<ProofInfo>>, Self::Err> {
tracing::debug!("{:?}", mint_url);
tracing::debug!("{:?}", unit);
let recs = sqlx::query(
r#"
SELECT *
FROM proof;
"#,
)
.fetch_all(&self.pool)
.await;
let recs = match recs {
Ok(rec) => rec,
Err(err) => match err {
sqlx::Error::RowNotFound => return Ok(None),
_ => return Err(Error::SQLX(err).into()),
},
};
tracing::debug!("{}", recs.len());
let proofs: Vec<ProofInfo> = recs
.iter()
.filter_map(|p| match sqlite_row_to_proof_info(p) {
Ok(proof_info) => {
match proof_info.matches_conditions(
&mint_url,
&unit,
&state,
&spending_conditions,
) {
true => Some(proof_info),
false => None,
}
}
Err(err) => {
tracing::error!("Could not deserialize proof row: {}", err);
None
}
})
.collect();
tracing::debug!("{}", proofs.len());
match proofs.is_empty() {
false => Ok(Some(proofs)),
true => return Ok(None),
}
}
async fn remove_proofs(&self, proofs: &Proofs) -> Result<(), Self::Err> {
// TODO: Generate a IN clause
for proof in proofs {
sqlx::query(
r#"
DELETE FROM proof
WHERE y = ?
"#,
)
.bind(proof.y()?.to_bytes().to_vec())
.execute(&self.pool)
.await
.map_err(Error::from)?;
}
Ok(())
}
async fn set_proof_state(&self, y: PublicKey, state: State) -> Result<(), Self::Err> {
sqlx::query(
r#"
UPDATE proof
SET state=?
WHERE y IS ?;
"#,
)
.bind(state.to_string())
.bind(y.to_bytes().to_vec())
.execute(&self.pool)
.await
.map_err(Error::from)?;
Ok(())
}
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> {
sqlx::query(
r#"
UPDATE keyset
SET counter = counter + ?
WHERE id IS ?;
"#,
)
.bind(count)
.bind(keyset_id.to_string())
.execute(&self.pool)
.await
.map_err(Error::from)?;
Ok(())
}
async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u32>, Self::Err> {
let rec = sqlx::query(
r#"
SELECT counter
FROM keyset
WHERE id=?;
"#,
)
.bind(keyset_id.to_string())
.fetch_one(&self.pool)
.await;
let count = match rec {
Ok(rec) => {
let count: Option<u32> = rec.try_get("counter").map_err(Error::from)?;
count
}
Err(err) => match err {
sqlx::Error::RowNotFound => return Ok(None),
_ => return Err(Error::SQLX(err).into()),
},
};
Ok(count)
}
#[cfg(feature = "nostr")]
async fn get_nostr_last_checked(
&self,
verifying_key: &PublicKey,
) -> Result<Option<u32>, Self::Err> {
let rec = sqlx::query(
r#"
SELECT last_check
FROM nostr_last_checked
WHERE key=?;
"#,
)
.bind(verifying_key.to_bytes().to_vec())
.fetch_one(&self.pool)
.await;
let count = match rec {
Ok(rec) => {
let count: Option<u32> = rec.try_get("last_check").map_err(Error::from)?;
count
}
Err(err) => match err {
sqlx::Error::RowNotFound => return Ok(None),
_ => return Err(Error::SQLX(err).into()),
},
};
Ok(count)
}
#[cfg(feature = "nostr")]
async fn add_nostr_last_checked(
&self,
verifying_key: PublicKey,
last_checked: u32,
) -> Result<(), Self::Err> {
sqlx::query(
r#"
INSERT OR REPLACE INTO nostr_last_checked
(key, last_check)
VALUES (?, ?);
"#,
)
.bind(verifying_key.to_bytes().to_vec())
.bind(last_checked)
.execute(&self.pool)
.await
.map_err(Error::from)?;
Ok(())
}
}
fn sqlite_row_to_mint_info(row: &SqliteRow) -> Result<MintInfo, Error> {
let name: Option<String> = row.try_get("name").map_err(Error::from)?;
let row_pubkey: Option<Vec<u8>> = row.try_get("pubkey").map_err(Error::from)?;
let row_version: Option<String> = row.try_get("version").map_err(Error::from)?;
let description: Option<String> = row.try_get("description").map_err(Error::from)?;
let description_long: Option<String> = row.try_get("description_long").map_err(Error::from)?;
let row_contact: Option<String> = row.try_get("contact").map_err(Error::from)?;
let row_nuts: Option<String> = row.try_get("nuts").map_err(Error::from)?;
let motd: Option<String> = row.try_get("motd").map_err(Error::from)?;
Ok(MintInfo {
name,
pubkey: row_pubkey.and_then(|p| PublicKey::from_slice(&p).ok()),
version: row_version.and_then(|v| serde_json::from_str(&v).ok()),
description,
description_long,
contact: row_contact.and_then(|c| serde_json::from_str(&c).ok()),
nuts: row_nuts
.and_then(|n| serde_json::from_str(&n).ok())
.unwrap_or_default(),
motd,
})
}
fn sqlite_row_to_keyset(row: &SqliteRow) -> Result<KeySetInfo, 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 active: bool = row.try_get("active").map_err(Error::from)?;
Ok(KeySetInfo {
id: Id::from_str(&row_id)?,
unit: CurrencyUnit::from(row_unit),
active,
})
}
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_info(row: &SqliteRow) -> Result<ProofInfo, 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)?;
let y: Vec<u8> = row.try_get("y").map_err(Error::from)?;
let row_mint_url: String = row.try_get("mint_url").map_err(Error::from)?;
let row_state: String = row.try_get("state").map_err(Error::from)?;
let row_spending_condition: Option<String> =
row.try_get("spending_condition").map_err(Error::from)?;
let row_unit: String = row.try_get("unit").map_err(Error::from)?;
let proof = 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,
};
Ok(ProofInfo {
proof,
y: PublicKey::from_slice(&y)?,
mint_url: row_mint_url.into(),
state: State::from_str(&row_state)?,
spending_condition: row_spending_condition.and_then(|r| serde_json::from_str(&r).ok()),
unit: CurrencyUnit::from(row_unit),
})
}