mirror of
https://github.com/aljazceru/cdk.git
synced 2026-02-06 21:56:13 +01:00
feat(NUT05): update with quote state
feat(NUT04): update with quote state feat: db migrations for mint state chore: remove logging
This commit is contained in:
@@ -46,8 +46,8 @@ impl From<MintQuoteBolt11Response> for JsMintQuoteBolt11Response {
|
||||
#[wasm_bindgen(js_class = MintQuoteBolt11Response)]
|
||||
impl JsMintQuoteBolt11Response {
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn paid(&self) -> bool {
|
||||
self.inner.paid
|
||||
pub fn state(&self) -> String {
|
||||
self.inner.state.to_string()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use cdk::nuts::{
|
||||
MeltBolt11Request, MeltBolt11Response, MeltMethodSettings, MeltQuoteBolt11Request,
|
||||
MeltQuoteBolt11Response, NUT05Settings,
|
||||
MeltBolt11Request, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteBolt11Response,
|
||||
NUT05Settings,
|
||||
};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
@@ -60,24 +60,6 @@ impl From<MeltBolt11Request> for JsMeltBolt11Request {
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = PostMeltResponse)]
|
||||
pub struct JsMeltBolt11Response {
|
||||
inner: MeltBolt11Response,
|
||||
}
|
||||
|
||||
impl Deref for JsMeltBolt11Response {
|
||||
type Target = MeltBolt11Response;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MeltBolt11Response> for JsMeltBolt11Response {
|
||||
fn from(inner: MeltBolt11Response) -> JsMeltBolt11Response {
|
||||
JsMeltBolt11Response { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = MeltMethodSettings)]
|
||||
pub struct JsMeltMethodSettings {
|
||||
inner: MeltMethodSettings,
|
||||
|
||||
@@ -52,8 +52,8 @@ impl JsMeltQuote {
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn paid(&self) -> bool {
|
||||
self.inner.paid
|
||||
pub fn state(&self) -> String {
|
||||
self.inner.state.to_string()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
|
||||
@@ -26,8 +26,8 @@ impl From<Melted> for JsMelted {
|
||||
#[wasm_bindgen(js_class = Melted)]
|
||||
impl JsMelted {
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn paid(&self) -> bool {
|
||||
self.inner.paid
|
||||
pub fn paid(&self) -> String {
|
||||
self.inner.state.to_string()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
|
||||
@@ -47,8 +47,8 @@ impl JsMintQuote {
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn paid(&self) -> bool {
|
||||
self.inner.paid
|
||||
pub fn state(&self) -> String {
|
||||
self.inner.state.to_string()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
|
||||
@@ -102,7 +102,7 @@ impl JsWallet {
|
||||
pub async fn mint_quote_status(&self, quote_id: String) -> Result<JsMintQuoteBolt11Response> {
|
||||
let quote = self
|
||||
.inner
|
||||
.mint_quote_status("e_id)
|
||||
.mint_quote_state("e_id)
|
||||
.await
|
||||
.map_err(into_err)?;
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ enum Commands {
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::WARN)
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
.init();
|
||||
|
||||
// Parse input
|
||||
|
||||
@@ -52,12 +52,14 @@ pub async fn melt(
|
||||
}
|
||||
let quote = wallet.melt_quote(bolt11.to_string(), None).await?;
|
||||
|
||||
println!("{:?}", quote);
|
||||
|
||||
let melt = wallet
|
||||
.melt("e.id, SplitTarget::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Paid invoice: {}", melt.paid);
|
||||
println!("Paid invoice: {}", melt.state);
|
||||
if let Some(preimage) = melt.preimage {
|
||||
println!("Payment preimage: {}", preimage);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::time::Duration;
|
||||
use anyhow::Result;
|
||||
use cdk::amount::SplitTarget;
|
||||
use cdk::cdk_database::{Error, WalletDatabase};
|
||||
use cdk::nuts::CurrencyUnit;
|
||||
use cdk::nuts::{CurrencyUnit, MintQuoteState};
|
||||
use cdk::url::UncheckedUrl;
|
||||
use cdk::wallet::Wallet;
|
||||
use cdk::Amount;
|
||||
@@ -43,9 +43,9 @@ pub async fn mint(
|
||||
println!("Please pay: {}", quote.request);
|
||||
|
||||
loop {
|
||||
let status = wallet.mint_quote_status("e.id).await?;
|
||||
let status = wallet.mint_quote_state("e.id).await?;
|
||||
|
||||
if status.paid {
|
||||
if status.state == MintQuoteState::Paid {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -133,8 +133,6 @@ pub async fn send(
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
tracing::debug!("{}", data_pubkey.to_string());
|
||||
|
||||
Some(SpendingConditions::P2PKConditions {
|
||||
data: data_pubkey,
|
||||
conditions: Some(conditions),
|
||||
|
||||
@@ -46,6 +46,9 @@ pub enum Error {
|
||||
/// Unknown Proof Y
|
||||
#[error("Unknown Proof Y")]
|
||||
UnknownY,
|
||||
/// Unknown Database Version
|
||||
#[error("Unknown Database Version")]
|
||||
UnknownDatabaseVersion,
|
||||
}
|
||||
|
||||
impl From<Error> for cdk::cdk_database::Error {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod error;
|
||||
mod migrations;
|
||||
|
||||
#[cfg(feature = "mint")]
|
||||
pub mod mint;
|
||||
|
||||
191
crates/cdk-redb/src/migrations.rs
Normal file
191
crates/cdk-redb/src/migrations.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use cdk::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState};
|
||||
use cdk::types::{MeltQuote, MintQuote};
|
||||
use cdk::{Amount, UncheckedUrl};
|
||||
use redb::{Database, ReadableTable, TableDefinition};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::error::Error;
|
||||
|
||||
const MINT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_quotes");
|
||||
const MELT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("melt_quotes");
|
||||
|
||||
/// Mint Quote Info
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct V0MintQuote {
|
||||
pub id: String,
|
||||
pub mint_url: UncheckedUrl,
|
||||
pub amount: Amount,
|
||||
pub unit: CurrencyUnit,
|
||||
pub request: String,
|
||||
pub paid: bool,
|
||||
pub expiry: u64,
|
||||
}
|
||||
|
||||
impl From<V0MintQuote> for MintQuote {
|
||||
fn from(quote: V0MintQuote) -> MintQuote {
|
||||
let state = match quote.paid {
|
||||
true => MintQuoteState::Paid,
|
||||
false => MintQuoteState::Unpaid,
|
||||
};
|
||||
MintQuote {
|
||||
id: quote.id,
|
||||
mint_url: quote.mint_url,
|
||||
amount: quote.amount,
|
||||
unit: quote.unit,
|
||||
request: quote.request,
|
||||
state,
|
||||
expiry: quote.expiry,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Melt Quote Info
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct V0MeltQuote {
|
||||
pub id: String,
|
||||
pub unit: CurrencyUnit,
|
||||
pub amount: Amount,
|
||||
pub request: String,
|
||||
pub fee_reserve: Amount,
|
||||
pub paid: bool,
|
||||
pub expiry: u64,
|
||||
}
|
||||
|
||||
impl From<V0MeltQuote> for MeltQuote {
|
||||
fn from(quote: V0MeltQuote) -> MeltQuote {
|
||||
let state = match quote.paid {
|
||||
true => MeltQuoteState::Paid,
|
||||
false => MeltQuoteState::Unpaid,
|
||||
};
|
||||
MeltQuote {
|
||||
id: quote.id,
|
||||
amount: quote.amount,
|
||||
unit: quote.unit,
|
||||
request: quote.request,
|
||||
state,
|
||||
expiry: quote.expiry,
|
||||
fee_reserve: quote.fee_reserve,
|
||||
payment_preimage: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn migrate_mint_quotes_00_to_01(db: Arc<Database>) -> Result<(), Error> {
|
||||
let read_txn = db.begin_read().map_err(Error::from)?;
|
||||
let table = read_txn
|
||||
.open_table(MINT_QUOTES_TABLE)
|
||||
.map_err(Error::from)?;
|
||||
|
||||
let mint_quotes: HashMap<String, Option<V0MintQuote>>;
|
||||
{
|
||||
mint_quotes = table
|
||||
.iter()
|
||||
.map_err(Error::from)?
|
||||
.flatten()
|
||||
.map(|(quote_id, mint_quote)| {
|
||||
(
|
||||
quote_id.value().to_string(),
|
||||
serde_json::from_str(mint_quote.value()).ok(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
let migrated_mint_quotes: HashMap<String, Option<MintQuote>> = mint_quotes
|
||||
.into_iter()
|
||||
.map(|(quote_id, quote)| (quote_id, quote.map(|q| q.into())))
|
||||
.collect();
|
||||
|
||||
{
|
||||
let write_txn = db.begin_write()?;
|
||||
|
||||
{
|
||||
let mut table = write_txn
|
||||
.open_table(MINT_QUOTES_TABLE)
|
||||
.map_err(Error::from)?;
|
||||
for (quote_id, quote) in migrated_mint_quotes {
|
||||
match quote {
|
||||
Some(quote) => {
|
||||
let quote_str = serde_json::to_string("e)?;
|
||||
|
||||
table.insert(quote_id.as_str(), quote_str.as_str())?;
|
||||
}
|
||||
None => {
|
||||
table.remove(quote_id.as_str())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn migrate_melt_quotes_00_to_01(db: Arc<Database>) -> Result<(), Error> {
|
||||
let read_txn = db.begin_read().map_err(Error::from)?;
|
||||
let table = read_txn
|
||||
.open_table(MELT_QUOTES_TABLE)
|
||||
.map_err(Error::from)?;
|
||||
|
||||
let melt_quotes: HashMap<String, Option<V0MeltQuote>>;
|
||||
{
|
||||
melt_quotes = table
|
||||
.iter()
|
||||
.map_err(Error::from)?
|
||||
.flatten()
|
||||
.map(|(quote_id, melt_quote)| {
|
||||
(
|
||||
quote_id.value().to_string(),
|
||||
serde_json::from_str(melt_quote.value()).ok(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
let migrated_melt_quotes: HashMap<String, Option<MeltQuote>> = melt_quotes
|
||||
.into_iter()
|
||||
.map(|(quote_id, quote)| (quote_id, quote.map(|q| q.into())))
|
||||
.collect();
|
||||
|
||||
{
|
||||
let write_txn = db.begin_write()?;
|
||||
|
||||
{
|
||||
let mut table = write_txn
|
||||
.open_table(MELT_QUOTES_TABLE)
|
||||
.map_err(Error::from)?;
|
||||
for (quote_id, quote) in migrated_melt_quotes {
|
||||
match quote {
|
||||
Some(quote) => {
|
||||
let quote_str = serde_json::to_string("e)?;
|
||||
|
||||
table.insert(quote_id.as_str(), quote_str.as_str())?;
|
||||
}
|
||||
None => {
|
||||
table.remove(quote_id.as_str())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn migrate_00_to_01(db: Arc<Database>) -> Result<u32, Error> {
|
||||
tracing::info!("Starting Migrations of mint quotes from 00 to 01");
|
||||
migrate_mint_quotes_00_to_01(Arc::clone(&db))?;
|
||||
tracing::info!("Finished Migrations of mint quotes from 00 to 01");
|
||||
|
||||
tracing::info!("Starting Migrations of melt quotes from 00 to 01");
|
||||
migrate_melt_quotes_00_to_01(Arc::clone(&db))?;
|
||||
tracing::info!("Finished Migrations of melt quotes from 00 to 01");
|
||||
Ok(1)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
@@ -8,14 +9,16 @@ use cdk::cdk_database;
|
||||
use cdk::cdk_database::MintDatabase;
|
||||
use cdk::dhke::hash_to_curve;
|
||||
use cdk::mint::MintKeySetInfo;
|
||||
use cdk::nuts::{BlindSignature, CurrencyUnit, Id, Proof, PublicKey};
|
||||
use cdk::nuts::{
|
||||
BlindSignature, CurrencyUnit, Id, MeltQuoteState, MintQuoteState, Proof, PublicKey,
|
||||
};
|
||||
use cdk::secret::Secret;
|
||||
use cdk::types::{MeltQuote, MintQuote};
|
||||
use redb::{Database, ReadableTable, TableDefinition};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::debug;
|
||||
|
||||
use super::error::Error;
|
||||
use crate::migrations::migrate_00_to_01;
|
||||
|
||||
const ACTIVE_KEYSETS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("active_keysets");
|
||||
const KEYSETS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("keysets");
|
||||
@@ -29,7 +32,7 @@ const CONFIG_TABLE: TableDefinition<&str, &str> = TableDefinition::new("config")
|
||||
const BLINDED_SIGNATURES: TableDefinition<[u8; 33], &str> =
|
||||
TableDefinition::new("blinded_signatures");
|
||||
|
||||
const DATABASE_VERSION: u64 = 0;
|
||||
const DATABASE_VERSION: u32 = 0;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MintRedbDatabase {
|
||||
@@ -38,41 +41,78 @@ pub struct MintRedbDatabase {
|
||||
|
||||
impl MintRedbDatabase {
|
||||
pub fn new(path: &Path) -> Result<Self, Error> {
|
||||
let db = Database::create(path)?;
|
||||
|
||||
let write_txn = db.begin_write()?;
|
||||
// Check database version
|
||||
{
|
||||
let _ = write_txn.open_table(CONFIG_TABLE)?;
|
||||
let mut table = write_txn.open_table(CONFIG_TABLE)?;
|
||||
// Check database version
|
||||
|
||||
let db_version = table.get("db_version")?;
|
||||
let db_version = db_version.map(|v| v.value().to_owned());
|
||||
let db = Arc::new(Database::create(path)?);
|
||||
|
||||
// Check database version
|
||||
let read_txn = db.begin_read()?;
|
||||
let table = read_txn.open_table(CONFIG_TABLE);
|
||||
|
||||
let db_version = match table {
|
||||
Ok(table) => table.get("db_version")?.map(|v| v.value().to_owned()),
|
||||
Err(_) => None,
|
||||
};
|
||||
match db_version {
|
||||
Some(db_version) => {
|
||||
let current_file_version = u64::from_str(&db_version)?;
|
||||
if current_file_version.ne(&DATABASE_VERSION) {
|
||||
// Database needs to be upgraded
|
||||
todo!()
|
||||
let mut current_file_version = u32::from_str(&db_version)?;
|
||||
match current_file_version.cmp(&DATABASE_VERSION) {
|
||||
Ordering::Less => {
|
||||
tracing::info!(
|
||||
"Database needs to be upgraded at {} current is {}",
|
||||
current_file_version,
|
||||
DATABASE_VERSION
|
||||
);
|
||||
if current_file_version == 0 {
|
||||
current_file_version = migrate_00_to_01(Arc::clone(&db))?;
|
||||
}
|
||||
|
||||
if current_file_version != DATABASE_VERSION {
|
||||
tracing::warn!(
|
||||
"Database upgrade did not complete at {} current is {}",
|
||||
current_file_version,
|
||||
DATABASE_VERSION
|
||||
);
|
||||
return Err(Error::UnknownDatabaseVersion);
|
||||
}
|
||||
}
|
||||
Ordering::Equal => {
|
||||
tracing::info!("Database is at current version {}", DATABASE_VERSION);
|
||||
}
|
||||
Ordering::Greater => {
|
||||
tracing::warn!(
|
||||
"Database upgrade did not complete at {} current is {}",
|
||||
current_file_version,
|
||||
DATABASE_VERSION
|
||||
);
|
||||
return Err(Error::UnknownDatabaseVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Open all tables to init a new db
|
||||
let _ = write_txn.open_table(ACTIVE_KEYSETS_TABLE)?;
|
||||
let _ = write_txn.open_table(KEYSETS_TABLE)?;
|
||||
let _ = write_txn.open_table(MINT_QUOTES_TABLE)?;
|
||||
let _ = write_txn.open_table(MELT_QUOTES_TABLE)?;
|
||||
let _ = write_txn.open_table(PENDING_PROOFS_TABLE)?;
|
||||
let _ = write_txn.open_table(SPENT_PROOFS_TABLE)?;
|
||||
let _ = write_txn.open_table(BLINDED_SIGNATURES)?;
|
||||
let write_txn = db.begin_write()?;
|
||||
{
|
||||
let mut table = write_txn.open_table(CONFIG_TABLE)?;
|
||||
// Open all tables to init a new db
|
||||
let _ = write_txn.open_table(ACTIVE_KEYSETS_TABLE)?;
|
||||
let _ = write_txn.open_table(KEYSETS_TABLE)?;
|
||||
let _ = write_txn.open_table(MINT_QUOTES_TABLE)?;
|
||||
let _ = write_txn.open_table(MELT_QUOTES_TABLE)?;
|
||||
let _ = write_txn.open_table(PENDING_PROOFS_TABLE)?;
|
||||
let _ = write_txn.open_table(SPENT_PROOFS_TABLE)?;
|
||||
let _ = write_txn.open_table(BLINDED_SIGNATURES)?;
|
||||
|
||||
table.insert("db_version", "0")?;
|
||||
table.insert("db_version", DATABASE_VERSION.to_string().as_str())?;
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
}
|
||||
}
|
||||
drop(db);
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
let db = Database::create(path)?;
|
||||
Ok(Self {
|
||||
db: Arc::new(Mutex::new(db)),
|
||||
})
|
||||
@@ -219,6 +259,53 @@ impl MintDatabase for MintRedbDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_mint_quote_state(
|
||||
&self,
|
||||
quote_id: &str,
|
||||
state: MintQuoteState,
|
||||
) -> Result<MintQuoteState, Self::Err> {
|
||||
let db = self.db.lock().await;
|
||||
|
||||
let mut mint_quote: MintQuote;
|
||||
{
|
||||
let read_txn = db.begin_read().map_err(Error::from)?;
|
||||
let table = read_txn
|
||||
.open_table(MINT_QUOTES_TABLE)
|
||||
.map_err(Error::from)?;
|
||||
|
||||
let quote_guard = table
|
||||
.get(quote_id)
|
||||
.map_err(Error::from)?
|
||||
.ok_or(Error::UnknownMintInfo)?;
|
||||
|
||||
let quote = quote_guard.value();
|
||||
|
||||
mint_quote = serde_json::from_str(quote).map_err(Error::from)?;
|
||||
}
|
||||
|
||||
let current_state = mint_quote.state;
|
||||
mint_quote.state = state;
|
||||
|
||||
let write_txn = db.begin_write().map_err(Error::from)?;
|
||||
{
|
||||
let mut table = write_txn
|
||||
.open_table(MINT_QUOTES_TABLE)
|
||||
.map_err(Error::from)?;
|
||||
|
||||
table
|
||||
.insert(
|
||||
quote_id,
|
||||
serde_json::to_string(&mint_quote)
|
||||
.map_err(Error::from)?
|
||||
.as_str(),
|
||||
)
|
||||
.map_err(Error::from)?;
|
||||
}
|
||||
write_txn.commit().map_err(Error::from)?;
|
||||
|
||||
Ok(current_state)
|
||||
}
|
||||
|
||||
async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> {
|
||||
let db = self.db.lock().await;
|
||||
let read_txn = db.begin_read().map_err(Error::from)?;
|
||||
@@ -286,6 +373,52 @@ impl MintDatabase for MintRedbDatabase {
|
||||
Ok(quote.map(|q| serde_json::from_str(q.value()).unwrap()))
|
||||
}
|
||||
|
||||
async fn update_melt_quote_state(
|
||||
&self,
|
||||
quote_id: &str,
|
||||
state: MeltQuoteState,
|
||||
) -> Result<MeltQuoteState, Self::Err> {
|
||||
let db = self.db.lock().await;
|
||||
let mut melt_quote: MeltQuote;
|
||||
{
|
||||
let read_txn = db.begin_read().map_err(Error::from)?;
|
||||
let table = read_txn
|
||||
.open_table(MELT_QUOTES_TABLE)
|
||||
.map_err(Error::from)?;
|
||||
|
||||
let quote_guard = table
|
||||
.get(quote_id)
|
||||
.map_err(Error::from)?
|
||||
.ok_or(Error::UnknownMintInfo)?;
|
||||
|
||||
let quote = quote_guard.value();
|
||||
|
||||
melt_quote = serde_json::from_str(quote).map_err(Error::from)?;
|
||||
}
|
||||
|
||||
let current_state = melt_quote.state;
|
||||
melt_quote.state = state;
|
||||
|
||||
let write_txn = db.begin_write().map_err(Error::from)?;
|
||||
{
|
||||
let mut table = write_txn
|
||||
.open_table(MELT_QUOTES_TABLE)
|
||||
.map_err(Error::from)?;
|
||||
|
||||
table
|
||||
.insert(
|
||||
quote_id,
|
||||
serde_json::to_string(&melt_quote)
|
||||
.map_err(Error::from)?
|
||||
.as_str(),
|
||||
)
|
||||
.map_err(Error::from)?;
|
||||
}
|
||||
write_txn.commit().map_err(Error::from)?;
|
||||
|
||||
Ok(current_state)
|
||||
}
|
||||
|
||||
async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, Self::Err> {
|
||||
let db = self.db.lock().await;
|
||||
let read_txn = db.begin_read().map_err(Error::from)?;
|
||||
@@ -338,7 +471,6 @@ impl MintDatabase for MintRedbDatabase {
|
||||
.map_err(Error::from)?;
|
||||
}
|
||||
write_txn.commit().map_err(Error::from)?;
|
||||
debug!("Added spend secret: {}", proof.secret.to_string());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
//! Redb Wallet
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
@@ -17,6 +20,7 @@ use tokio::sync::Mutex;
|
||||
use tracing::instrument;
|
||||
|
||||
use super::error::Error;
|
||||
use crate::migrations::migrate_00_to_01;
|
||||
|
||||
const MINTS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mints_table");
|
||||
// <Mint_Url, Keyset_id>
|
||||
@@ -24,7 +28,9 @@ const MINT_KEYSETS_TABLE: MultimapTableDefinition<&str, &[u8]> =
|
||||
MultimapTableDefinition::new("mint_keysets");
|
||||
// <Keyset_id, KeysetInfo>
|
||||
const KEYSETS_TABLE: TableDefinition<&[u8], &str> = TableDefinition::new("keysets");
|
||||
// <Quote_id, quote>
|
||||
const MINT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_quotes");
|
||||
// <Quote_id, quote>
|
||||
const MELT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("melt_quotes");
|
||||
const MINT_KEYS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_keys");
|
||||
// <Y, Proof Info>
|
||||
@@ -33,7 +39,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 = 0;
|
||||
const DATABASE_VERSION: u32 = 1;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RedbWalletDatabase {
|
||||
@@ -42,42 +48,82 @@ pub struct RedbWalletDatabase {
|
||||
|
||||
impl RedbWalletDatabase {
|
||||
pub fn new(path: &Path) -> Result<Self, Error> {
|
||||
let db = Database::create(path)?;
|
||||
|
||||
let write_txn = db.begin_write()?;
|
||||
|
||||
// Check database version
|
||||
{
|
||||
let _ = write_txn.open_table(CONFIG_TABLE)?;
|
||||
let mut table = write_txn.open_table(CONFIG_TABLE)?;
|
||||
let db = Arc::new(Database::create(path)?);
|
||||
|
||||
let db_version = table.get("db_version")?.map(|v| v.value().to_owned());
|
||||
let db_version: Option<String>;
|
||||
{
|
||||
// Check database version
|
||||
let read_txn = db.begin_read()?;
|
||||
let table = read_txn.open_table(CONFIG_TABLE);
|
||||
|
||||
db_version = match table {
|
||||
Ok(table) => table.get("db_version")?.map(|v| v.value().to_string()),
|
||||
Err(_) => None,
|
||||
};
|
||||
}
|
||||
|
||||
match db_version {
|
||||
Some(db_version) => {
|
||||
let current_file_version = u32::from_str(&db_version)?;
|
||||
if current_file_version.ne(&DATABASE_VERSION) {
|
||||
// Database needs to be upgraded
|
||||
todo!()
|
||||
let mut current_file_version = u32::from_str(&db_version)?;
|
||||
|
||||
match current_file_version.cmp(&DATABASE_VERSION) {
|
||||
Ordering::Less => {
|
||||
tracing::info!(
|
||||
"Database needs to be upgraded at {} current is {}",
|
||||
current_file_version,
|
||||
DATABASE_VERSION
|
||||
);
|
||||
if current_file_version == 0 {
|
||||
current_file_version = migrate_00_to_01(Arc::clone(&db))?;
|
||||
}
|
||||
|
||||
if current_file_version != DATABASE_VERSION {
|
||||
tracing::warn!(
|
||||
"Database upgrade did not complete at {} current is {}",
|
||||
current_file_version,
|
||||
DATABASE_VERSION
|
||||
);
|
||||
return Err(Error::UnknownDatabaseVersion);
|
||||
}
|
||||
}
|
||||
Ordering::Equal => {
|
||||
tracing::info!("Database is at current version {}", DATABASE_VERSION);
|
||||
}
|
||||
Ordering::Greater => {
|
||||
tracing::warn!(
|
||||
"Database upgrade did not complete at {} current is {}",
|
||||
current_file_version,
|
||||
DATABASE_VERSION
|
||||
);
|
||||
return Err(Error::UnknownDatabaseVersion);
|
||||
}
|
||||
}
|
||||
let _ = write_txn.open_table(KEYSET_COUNTER)?;
|
||||
}
|
||||
None => {
|
||||
// Open all tables to init a new db
|
||||
let _ = write_txn.open_table(MINTS_TABLE)?;
|
||||
let _ = write_txn.open_multimap_table(MINT_KEYSETS_TABLE)?;
|
||||
let _ = write_txn.open_table(KEYSETS_TABLE)?;
|
||||
let _ = write_txn.open_table(MINT_QUOTES_TABLE)?;
|
||||
let _ = write_txn.open_table(MELT_QUOTES_TABLE)?;
|
||||
let _ = write_txn.open_table(MINT_KEYS_TABLE)?;
|
||||
let _ = write_txn.open_table(PROOFS_TABLE)?;
|
||||
let _ = write_txn.open_table(KEYSET_COUNTER)?;
|
||||
let _ = write_txn.open_table(NOSTR_LAST_CHECKED)?;
|
||||
table.insert("db_version", "0")?;
|
||||
let write_txn = db.begin_write()?;
|
||||
{
|
||||
let mut table = write_txn.open_table(CONFIG_TABLE)?;
|
||||
// Open all tables to init a new db
|
||||
let _ = write_txn.open_table(MINTS_TABLE)?;
|
||||
let _ = write_txn.open_multimap_table(MINT_KEYSETS_TABLE)?;
|
||||
let _ = write_txn.open_table(KEYSETS_TABLE)?;
|
||||
let _ = write_txn.open_table(MINT_QUOTES_TABLE)?;
|
||||
let _ = write_txn.open_table(MELT_QUOTES_TABLE)?;
|
||||
let _ = write_txn.open_table(MINT_KEYS_TABLE)?;
|
||||
let _ = write_txn.open_table(PROOFS_TABLE)?;
|
||||
let _ = write_txn.open_table(KEYSET_COUNTER)?;
|
||||
let _ = write_txn.open_table(NOSTR_LAST_CHECKED)?;
|
||||
table.insert("db_version", DATABASE_VERSION.to_string().as_str())?;
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
}
|
||||
}
|
||||
drop(db);
|
||||
}
|
||||
write_txn.commit()?;
|
||||
|
||||
let db = Database::create(path)?;
|
||||
|
||||
Ok(Self {
|
||||
db: Arc::new(Mutex::new(db)),
|
||||
@@ -5,12 +5,18 @@ 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),
|
||||
/// NUT02 Error
|
||||
#[error(transparent)]
|
||||
CDKNUT02(#[from] cdk::nuts::nut02::Error),
|
||||
/// NUT04 Error
|
||||
#[error(transparent)]
|
||||
CDKNUT04(#[from] cdk::nuts::nut04::Error),
|
||||
/// NUT05 Error
|
||||
#[error(transparent)]
|
||||
CDKNUT05(#[from] cdk::nuts::nut05::Error),
|
||||
/// Secret Error
|
||||
#[error(transparent)]
|
||||
CDKSECRET(#[from] cdk::secret::Error),
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE melt_quote ADD state TEXT CHECK ( state IN ('UNPAID', 'PENDING', 'PAID' ) ) NOT NULL;
|
||||
ALTER TABLE melt_quote ADD payment_preimage TEXT;
|
||||
ALTER TABLE melt_quote DROP COLUMN paid;
|
||||
CREATE INDEX IF NOT EXISTS melt_quote_state_index ON melt_quote(state);
|
||||
DROP INDEX IF EXISTS paid_index;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE mint_quote ADD state TEXT CHECK ( state IN ('UNPAID', 'PENDING', 'PAID', 'ISSUED' ) ) NOT NULL;
|
||||
ALTER TABLE mint_quote DROP COLUMN paid;
|
||||
CREATE INDEX IF NOT EXISTS mint_quote_state_index ON mint_quote(state);
|
||||
@@ -8,7 +8,10 @@ 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::nuts::nut05::QuoteState;
|
||||
use cdk::nuts::{
|
||||
BlindSignature, CurrencyUnit, Id, MeltQuoteState, MintQuoteState, Proof, PublicKey,
|
||||
};
|
||||
use cdk::secret::Secret;
|
||||
use cdk::types::{MeltQuote, MintQuote};
|
||||
use cdk::Amount;
|
||||
@@ -122,7 +125,7 @@ WHERE active = 1
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT OR REPLACE INTO mint_quote
|
||||
(id, mint_url, amount, unit, request, paid, expiry)
|
||||
(id, mint_url, amount, unit, request, state, expiry)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||
"#,
|
||||
)
|
||||
@@ -131,7 +134,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||
.bind(u64::from(quote.amount) as i64)
|
||||
.bind(quote.unit.to_string())
|
||||
.bind(quote.request)
|
||||
.bind(quote.paid)
|
||||
.bind(quote.state.to_string())
|
||||
.bind(quote.expiry as i64)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
@@ -162,6 +165,44 @@ WHERE id=?;
|
||||
|
||||
Ok(Some(sqlite_row_to_mint_quote(rec)?))
|
||||
}
|
||||
|
||||
async fn update_mint_quote_state(
|
||||
&self,
|
||||
quote_id: &str,
|
||||
state: MintQuoteState,
|
||||
) -> Result<MintQuoteState, Self::Err> {
|
||||
let mut transaction = self.pool.begin().await.map_err(Error::from)?;
|
||||
|
||||
let rec = sqlx::query(
|
||||
r#"
|
||||
SELECT *
|
||||
FROM mint_quote
|
||||
WHERE id=?;
|
||||
"#,
|
||||
)
|
||||
.bind(quote_id)
|
||||
.fetch_one(&mut transaction)
|
||||
.await
|
||||
.map_err(Error::from)?;
|
||||
|
||||
let quote = sqlite_row_to_mint_quote(rec)?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE mint_quote SET state = ? WHERE id = ?
|
||||
"#,
|
||||
)
|
||||
.bind(state.to_string())
|
||||
.bind(quote_id)
|
||||
.execute(&mut transaction)
|
||||
.await
|
||||
.map_err(Error::from)?;
|
||||
|
||||
transaction.commit().await.map_err(Error::from)?;
|
||||
|
||||
Ok(quote.state)
|
||||
}
|
||||
|
||||
async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> {
|
||||
let rec = sqlx::query(
|
||||
r#"
|
||||
@@ -196,8 +237,8 @@ WHERE id=?
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT OR REPLACE INTO melt_quote
|
||||
(id, unit, amount, request, fee_reserve, paid, expiry)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||
(id, unit, amount, request, fee_reserve, state, expiry, payment_preimage)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?);
|
||||
"#,
|
||||
)
|
||||
.bind(quote.id.to_string())
|
||||
@@ -205,8 +246,9 @@ VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||
.bind(u64::from(quote.amount) as i64)
|
||||
.bind(quote.request)
|
||||
.bind(u64::from(quote.fee_reserve) as i64)
|
||||
.bind(quote.paid)
|
||||
.bind(quote.state.to_string())
|
||||
.bind(quote.expiry as i64)
|
||||
.bind(quote.payment_preimage)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Error::from)?;
|
||||
@@ -250,6 +292,44 @@ FROM melt_quote
|
||||
|
||||
Ok(melt_quotes)
|
||||
}
|
||||
|
||||
async fn update_melt_quote_state(
|
||||
&self,
|
||||
quote_id: &str,
|
||||
state: MeltQuoteState,
|
||||
) -> Result<MeltQuoteState, Self::Err> {
|
||||
let mut transaction = self.pool.begin().await.map_err(Error::from)?;
|
||||
|
||||
let rec = sqlx::query(
|
||||
r#"
|
||||
SELECT *
|
||||
FROM melt_quote
|
||||
WHERE id=?;
|
||||
"#,
|
||||
)
|
||||
.bind(quote_id)
|
||||
.fetch_one(&mut transaction)
|
||||
.await
|
||||
.map_err(Error::from)?;
|
||||
|
||||
let quote = sqlite_row_to_melt_quote(rec)?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE melt_quote SET state = ? WHERE id = ?
|
||||
"#,
|
||||
)
|
||||
.bind(state.to_string())
|
||||
.bind(quote_id)
|
||||
.execute(&mut transaction)
|
||||
.await
|
||||
.map_err(Error::from)?;
|
||||
|
||||
transaction.commit().await.map_err(Error::from)?;
|
||||
|
||||
Ok(quote.state)
|
||||
}
|
||||
|
||||
async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
@@ -581,7 +661,7 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result<MintQuote, Error> {
|
||||
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_state: String = row.try_get("state").map_err(Error::from)?;
|
||||
let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?;
|
||||
|
||||
Ok(MintQuote {
|
||||
@@ -590,7 +670,7 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result<MintQuote, Error> {
|
||||
amount: Amount::from(row_amount as u64),
|
||||
unit: CurrencyUnit::from(row_unit),
|
||||
request: row_request,
|
||||
paid: row_paid,
|
||||
state: MintQuoteState::from_str(&row_state).map_err(Error::from)?,
|
||||
expiry: row_expiry as u64,
|
||||
})
|
||||
}
|
||||
@@ -601,8 +681,9 @@ fn sqlite_row_to_melt_quote(row: SqliteRow) -> Result<MeltQuote, Error> {
|
||||
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_state: String = row.try_get("state").map_err(Error::from)?;
|
||||
let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?;
|
||||
let row_preimage: Option<String> = row.try_get("payment_preimage").map_err(Error::from)?;
|
||||
|
||||
Ok(MeltQuote {
|
||||
id: row_id,
|
||||
@@ -610,8 +691,9 @@ fn sqlite_row_to_melt_quote(row: SqliteRow) -> Result<MeltQuote, Error> {
|
||||
unit: CurrencyUnit::from(row_unit),
|
||||
request: row_request,
|
||||
fee_reserve: Amount::from(row_fee_reserve as u64),
|
||||
paid: row_paid,
|
||||
state: QuoteState::from_str(&row_state)?,
|
||||
expiry: row_expiry as u64,
|
||||
payment_preimage: row_preimage,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,12 @@ pub enum Error {
|
||||
/// NUT02 Error
|
||||
#[error(transparent)]
|
||||
CDKNUT02(#[from] cdk::nuts::nut02::Error),
|
||||
/// NUT04 Error
|
||||
#[error(transparent)]
|
||||
CDKNUT04(#[from] cdk::nuts::nut04::Error),
|
||||
/// NUT05 Error
|
||||
#[error(transparent)]
|
||||
CDKNUT05(#[from] cdk::nuts::nut05::Error),
|
||||
/// NUT07 Error
|
||||
#[error(transparent)]
|
||||
CDKNUT07(#[from] cdk::nuts::nut07::Error),
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE melt_quote ADD state TEXT CHECK ( state IN ('UNPAID', 'PENDING', 'PAID' ) ) NOT NULL;
|
||||
ALTER TABLE melt_quote ADD payment_preimage TEXT;
|
||||
ALTER TABLE melt_quote DROP COLUMN paid;
|
||||
CREATE INDEX IF NOT EXISTS melt_quote_state_index ON melt_quote(state);
|
||||
DROP INDEX IF EXISTS paid_index;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE mint_quote ADD state TEXT CHECK ( state IN ('UNPAID', 'PENDING', 'PAID', 'ISSUED' ) ) NOT NULL;
|
||||
ALTER TABLE mint_quote DROP COLUMN paid;
|
||||
CREATE INDEX IF NOT EXISTS mint_quote_state_index ON mint_quote(state);
|
||||
@@ -7,8 +7,8 @@ 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,
|
||||
CurrencyUnit, Id, KeySetInfo, Keys, MeltQuoteState, MintInfo, MintQuoteState, Proof, Proofs,
|
||||
PublicKey, SpendingConditions, State,
|
||||
};
|
||||
use cdk::secret::Secret;
|
||||
use cdk::types::{MeltQuote, MintQuote, ProofInfo};
|
||||
@@ -278,7 +278,7 @@ WHERE id=?
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT OR REPLACE INTO mint_quote
|
||||
(id, mint_url, amount, unit, request, paid, expiry)
|
||||
(id, mint_url, amount, unit, request, state, expiry)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||
"#,
|
||||
)
|
||||
@@ -287,7 +287,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||
.bind(u64::from(quote.amount) as i64)
|
||||
.bind(quote.unit.to_string())
|
||||
.bind(quote.request)
|
||||
.bind(quote.paid)
|
||||
.bind(quote.state.to_string())
|
||||
.bind(quote.expiry as i64)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
@@ -351,7 +351,7 @@ WHERE id=?
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT OR REPLACE INTO melt_quote
|
||||
(id, unit, amount, request, fee_reserve, paid, expiry)
|
||||
(id, unit, amount, request, fee_reserve, state, expiry)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||
"#,
|
||||
)
|
||||
@@ -360,7 +360,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||
.bind(u64::from(quote.amount) as i64)
|
||||
.bind(quote.request)
|
||||
.bind(u64::from(quote.fee_reserve) as i64)
|
||||
.bind(quote.paid)
|
||||
.bind(quote.state.to_string())
|
||||
.bind(quote.expiry as i64)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
@@ -502,8 +502,6 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
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 *
|
||||
@@ -719,16 +717,18 @@ fn sqlite_row_to_mint_quote(row: &SqliteRow) -> Result<MintQuote, Error> {
|
||||
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_state: String = row.try_get("state").map_err(Error::from)?;
|
||||
let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?;
|
||||
|
||||
let state = MintQuoteState::from_str(&row_state)?;
|
||||
|
||||
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,
|
||||
state,
|
||||
expiry: row_expiry as u64,
|
||||
})
|
||||
}
|
||||
@@ -739,17 +739,20 @@ fn sqlite_row_to_melt_quote(row: &SqliteRow) -> Result<MeltQuote, Error> {
|
||||
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_state: String = row.try_get("state").map_err(Error::from)?;
|
||||
let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?;
|
||||
let row_preimage: Option<String> = row.try_get("payment_preimage").map_err(Error::from)?;
|
||||
|
||||
let state = MeltQuoteState::from_str(&row_state)?;
|
||||
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,
|
||||
state,
|
||||
expiry: row_expiry as u64,
|
||||
payment_preimage: row_preimage,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::time::Duration;
|
||||
use cdk::amount::SplitTarget;
|
||||
use cdk::cdk_database::WalletMemoryDatabase;
|
||||
use cdk::error::Error;
|
||||
use cdk::nuts::CurrencyUnit;
|
||||
use cdk::nuts::{CurrencyUnit, MintQuoteState};
|
||||
use cdk::wallet::Wallet;
|
||||
use cdk::Amount;
|
||||
use rand::Rng;
|
||||
@@ -26,11 +26,11 @@ async fn main() -> Result<(), Error> {
|
||||
println!("Quote: {:#?}", quote);
|
||||
|
||||
loop {
|
||||
let status = wallet.mint_quote_status("e.id).await.unwrap();
|
||||
let status = wallet.mint_quote_state("e.id).await.unwrap();
|
||||
|
||||
println!("Quote status: {}", status.paid);
|
||||
println!("Quote status: {}", status.state);
|
||||
|
||||
if status.paid {
|
||||
if status.state == MintQuoteState::Paid {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::time::Duration;
|
||||
use cdk::amount::SplitTarget;
|
||||
use cdk::cdk_database::WalletMemoryDatabase;
|
||||
use cdk::error::Error;
|
||||
use cdk::nuts::{CurrencyUnit, SecretKey, SpendingConditions};
|
||||
use cdk::nuts::{CurrencyUnit, MintQuoteState, SecretKey, SpendingConditions};
|
||||
use cdk::wallet::Wallet;
|
||||
use cdk::Amount;
|
||||
use rand::Rng;
|
||||
@@ -26,11 +26,11 @@ async fn main() -> Result<(), Error> {
|
||||
println!("Minting nuts ...");
|
||||
|
||||
loop {
|
||||
let status = wallet.mint_quote_status("e.id).await.unwrap();
|
||||
let status = wallet.mint_quote_state("e.id).await.unwrap();
|
||||
|
||||
println!("Quote status: {}", status.paid);
|
||||
println!("Quote status: {}", status.state);
|
||||
|
||||
if status.paid {
|
||||
if status.state == MintQuoteState::Paid {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ use tokio::sync::RwLock;
|
||||
use super::{Error, MintDatabase};
|
||||
use crate::dhke::hash_to_curve;
|
||||
use crate::mint::MintKeySetInfo;
|
||||
use crate::nuts::{BlindSignature, CurrencyUnit, Id, Proof, Proofs, PublicKey};
|
||||
use crate::nuts::{
|
||||
BlindSignature, CurrencyUnit, Id, MeltQuoteState, MintQuoteState, Proof, Proofs, PublicKey,
|
||||
};
|
||||
use crate::secret::Secret;
|
||||
use crate::types::{MeltQuote, MintQuote};
|
||||
|
||||
@@ -103,6 +105,27 @@ impl MintDatabase for MintMemoryDatabase {
|
||||
Ok(self.mint_quotes.read().await.get(quote_id).cloned())
|
||||
}
|
||||
|
||||
async fn update_mint_quote_state(
|
||||
&self,
|
||||
quote_id: &str,
|
||||
state: MintQuoteState,
|
||||
) -> Result<MintQuoteState, Self::Err> {
|
||||
let mut mint_quotes = self.mint_quotes.write().await;
|
||||
|
||||
let mut quote = mint_quotes
|
||||
.get(quote_id)
|
||||
.cloned()
|
||||
.ok_or(Error::UnknownQuote)?;
|
||||
|
||||
let current_state = quote.state;
|
||||
|
||||
quote.state = state;
|
||||
|
||||
mint_quotes.insert(quote_id.to_string(), quote.clone());
|
||||
|
||||
Ok(current_state)
|
||||
}
|
||||
|
||||
async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> {
|
||||
Ok(self.mint_quotes.read().await.values().cloned().collect())
|
||||
}
|
||||
@@ -125,6 +148,27 @@ impl MintDatabase for MintMemoryDatabase {
|
||||
Ok(self.melt_quotes.read().await.get(quote_id).cloned())
|
||||
}
|
||||
|
||||
async fn update_melt_quote_state(
|
||||
&self,
|
||||
quote_id: &str,
|
||||
state: MeltQuoteState,
|
||||
) -> Result<MeltQuoteState, Self::Err> {
|
||||
let mut melt_quotes = self.melt_quotes.write().await;
|
||||
|
||||
let mut quote = melt_quotes
|
||||
.get(quote_id)
|
||||
.cloned()
|
||||
.ok_or(Error::UnknownQuote)?;
|
||||
|
||||
let current_state = quote.state;
|
||||
|
||||
quote.state = state;
|
||||
|
||||
melt_quotes.insert(quote_id.to_string(), quote.clone());
|
||||
|
||||
Ok(current_state)
|
||||
}
|
||||
|
||||
async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, Self::Err> {
|
||||
Ok(self.melt_quotes.read().await.values().cloned().collect())
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::mint::MintKeySetInfo;
|
||||
#[cfg(feature = "wallet")]
|
||||
use crate::nuts::State;
|
||||
#[cfg(feature = "mint")]
|
||||
use crate::nuts::{BlindSignature, Proof};
|
||||
use crate::nuts::{BlindSignature, MeltQuoteState, MintQuoteState, Proof};
|
||||
#[cfg(any(feature = "wallet", feature = "mint"))]
|
||||
use crate::nuts::{CurrencyUnit, Id, PublicKey};
|
||||
#[cfg(feature = "wallet")]
|
||||
@@ -43,6 +43,8 @@ pub enum Error {
|
||||
Cdk(#[from] crate::error::Error),
|
||||
#[error(transparent)]
|
||||
NUT01(#[from] crate::nuts::nut00::Error),
|
||||
#[error("Unknown Quote")]
|
||||
UnknownQuote,
|
||||
}
|
||||
|
||||
#[cfg(feature = "wallet")]
|
||||
@@ -126,11 +128,21 @@ pub trait MintDatabase {
|
||||
|
||||
async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Self::Err>;
|
||||
async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<MintQuote>, Self::Err>;
|
||||
async fn update_mint_quote_state(
|
||||
&self,
|
||||
quote_id: &str,
|
||||
state: MintQuoteState,
|
||||
) -> Result<MintQuoteState, Self::Err>;
|
||||
async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err>;
|
||||
async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
|
||||
|
||||
async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Self::Err>;
|
||||
async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Self::Err>;
|
||||
async fn update_melt_quote_state(
|
||||
&self,
|
||||
quote_id: &str,
|
||||
state: MeltQuoteState,
|
||||
) -> Result<MeltQuoteState, Self::Err>;
|
||||
async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, Self::Err>;
|
||||
async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
|
||||
|
||||
|
||||
@@ -26,8 +26,14 @@ pub enum Error {
|
||||
TokenPending,
|
||||
#[error("Quote not paid")]
|
||||
UnpaidQuote,
|
||||
#[error("Quote is already paid")]
|
||||
PaidQuote,
|
||||
#[error("Unknown quote")]
|
||||
UnknownQuote,
|
||||
#[error("Quote pending")]
|
||||
PendingQuote,
|
||||
#[error("Quote already issued")]
|
||||
IssuedQuote,
|
||||
#[error("Unknown secret kind")]
|
||||
UnknownSecretKind,
|
||||
#[error("Cannot have multiple units")]
|
||||
|
||||
@@ -9,6 +9,7 @@ use error::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use self::nut05::QuoteState;
|
||||
use crate::cdk_database::{self, MintDatabase};
|
||||
use crate::dhke::{hash_to_curve, sign_message, verify_message};
|
||||
use crate::nuts::nut11::enforce_sig_flag;
|
||||
@@ -130,10 +131,13 @@ impl Mint {
|
||||
.await?
|
||||
.ok_or(Error::UnknownQuote)?;
|
||||
|
||||
let paid = quote.state == MintQuoteState::Paid;
|
||||
|
||||
Ok(MintQuoteBolt11Response {
|
||||
quote: quote.id,
|
||||
request: quote.request,
|
||||
paid: quote.paid,
|
||||
paid: Some(paid),
|
||||
state: quote.state,
|
||||
expiry: Some(quote.expiry),
|
||||
})
|
||||
}
|
||||
@@ -183,10 +187,13 @@ impl Mint {
|
||||
|
||||
Ok(MeltQuoteBolt11Response {
|
||||
quote: quote.id,
|
||||
paid: quote.paid,
|
||||
paid: Some(quote.state == QuoteState::Paid),
|
||||
state: quote.state,
|
||||
expiry: quote.expiry,
|
||||
amount: quote.amount,
|
||||
fee_reserve: quote.fee_reserve,
|
||||
payment_preimage: quote.payment_preimage,
|
||||
change: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -294,6 +301,24 @@ impl Mint {
|
||||
&self,
|
||||
mint_request: nut04::MintBolt11Request,
|
||||
) -> Result<nut04::MintBolt11Response, Error> {
|
||||
let state = self
|
||||
.localstore
|
||||
.update_mint_quote_state(&mint_request.quote, MintQuoteState::Pending)
|
||||
.await?;
|
||||
|
||||
match state {
|
||||
MintQuoteState::Unpaid => {
|
||||
return Err(Error::UnpaidQuote);
|
||||
}
|
||||
MintQuoteState::Pending => {
|
||||
return Err(Error::PendingQuote);
|
||||
}
|
||||
MintQuoteState::Issued => {
|
||||
return Err(Error::IssuedQuote);
|
||||
}
|
||||
MintQuoteState::Paid => (),
|
||||
}
|
||||
|
||||
for blinded_message in &mint_request.outputs {
|
||||
if self
|
||||
.localstore
|
||||
@@ -309,16 +334,6 @@ impl Mint {
|
||||
}
|
||||
}
|
||||
|
||||
let quote = self
|
||||
.localstore
|
||||
.get_mint_quote(&mint_request.quote)
|
||||
.await?
|
||||
.ok_or(Error::UnknownQuote)?;
|
||||
|
||||
if !quote.paid {
|
||||
return Err(Error::UnpaidQuote);
|
||||
}
|
||||
|
||||
let mut blind_signatures = Vec::with_capacity(mint_request.outputs.len());
|
||||
|
||||
for blinded_message in mint_request.outputs.into_iter() {
|
||||
@@ -330,7 +345,7 @@ impl Mint {
|
||||
}
|
||||
|
||||
self.localstore
|
||||
.remove_mint_quote(&mint_request.quote)
|
||||
.update_mint_quote_state(&mint_request.quote, MintQuoteState::Issued)
|
||||
.await?;
|
||||
|
||||
Ok(nut04::MintBolt11Response {
|
||||
@@ -568,6 +583,21 @@ impl Mint {
|
||||
&self,
|
||||
melt_request: &MeltBolt11Request,
|
||||
) -> Result<MeltQuote, Error> {
|
||||
let state = self
|
||||
.localstore
|
||||
.update_melt_quote_state(&melt_request.quote, MeltQuoteState::Pending)
|
||||
.await?;
|
||||
|
||||
match state {
|
||||
MeltQuoteState::Unpaid => (),
|
||||
MeltQuoteState::Pending => {
|
||||
return Err(Error::PendingQuote);
|
||||
}
|
||||
MeltQuoteState::Paid => {
|
||||
return Err(Error::PaidQuote);
|
||||
}
|
||||
}
|
||||
|
||||
let quote = self
|
||||
.localstore
|
||||
.get_melt_quote(&melt_request.quote)
|
||||
@@ -664,8 +694,8 @@ impl Mint {
|
||||
melt_request: &MeltBolt11Request,
|
||||
preimage: &str,
|
||||
total_spent: Amount,
|
||||
) -> Result<MeltBolt11Response, Error> {
|
||||
self.verify_melt_request(melt_request).await?;
|
||||
) -> Result<MeltQuoteBolt11Response, Error> {
|
||||
let quote = self.verify_melt_request(melt_request).await?;
|
||||
|
||||
if let Some(outputs) = &melt_request.outputs {
|
||||
for blinded_message in outputs {
|
||||
@@ -688,10 +718,6 @@ impl Mint {
|
||||
self.localstore.add_spent_proof(input.clone()).await?;
|
||||
}
|
||||
|
||||
self.localstore
|
||||
.remove_melt_quote(&melt_request.quote)
|
||||
.await?;
|
||||
|
||||
let mut change = None;
|
||||
|
||||
if let Some(outputs) = melt_request.outputs.clone() {
|
||||
@@ -734,10 +760,19 @@ impl Mint {
|
||||
);
|
||||
}
|
||||
|
||||
Ok(MeltBolt11Response {
|
||||
paid: true,
|
||||
self.localstore
|
||||
.update_melt_quote_state(&melt_request.quote, MeltQuoteState::Paid)
|
||||
.await?;
|
||||
|
||||
Ok(MeltQuoteBolt11Response {
|
||||
amount: quote.amount,
|
||||
paid: Some(true),
|
||||
payment_preimage: Some(preimage.to_string()),
|
||||
change,
|
||||
quote: quote.id,
|
||||
fee_reserve: quote.fee_reserve,
|
||||
state: QuoteState::Paid,
|
||||
expiry: quote.expiry,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -28,11 +28,11 @@ pub use nut03::PreSwap;
|
||||
pub use nut03::{SwapRequest, SwapResponse};
|
||||
pub use nut04::{
|
||||
MintBolt11Request, MintBolt11Response, MintMethodSettings, MintQuoteBolt11Request,
|
||||
MintQuoteBolt11Response, Settings as NUT04Settings,
|
||||
MintQuoteBolt11Response, QuoteState as MintQuoteState, Settings as NUT04Settings,
|
||||
};
|
||||
pub use nut05::{
|
||||
MeltBolt11Request, MeltBolt11Response, MeltMethodSettings, MeltQuoteBolt11Request,
|
||||
MeltQuoteBolt11Response, Settings as NUT05Settings,
|
||||
MeltBolt11Request, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteBolt11Response,
|
||||
QuoteState as MeltQuoteState, Settings as NUT05Settings,
|
||||
};
|
||||
pub use nut06::{MintInfo, MintVersion, Nuts};
|
||||
pub use nut07::{CheckStateRequest, CheckStateResponse, ProofState, State};
|
||||
|
||||
@@ -2,12 +2,25 @@
|
||||
//!
|
||||
//! <https://github.com/cashubtc/nuts/blob/main/04.md>
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde_json::Value;
|
||||
use thiserror::Error;
|
||||
|
||||
use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod};
|
||||
use super::MintQuoteState;
|
||||
use crate::types::MintQuote;
|
||||
use crate::Amount;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Unknown Quote State
|
||||
#[error("Unknown Quote State")]
|
||||
UnknownState,
|
||||
}
|
||||
|
||||
/// Mint quote request [NUT-04]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MintQuoteBolt11Request {
|
||||
@@ -17,25 +30,132 @@ pub struct MintQuoteBolt11Request {
|
||||
pub unit: CurrencyUnit,
|
||||
}
|
||||
|
||||
/// Possible states of a quote
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum QuoteState {
|
||||
#[default]
|
||||
Unpaid,
|
||||
Paid,
|
||||
Pending,
|
||||
Issued,
|
||||
}
|
||||
|
||||
impl fmt::Display for QuoteState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Unpaid => write!(f, "UNPAID"),
|
||||
Self::Paid => write!(f, "PAID"),
|
||||
Self::Pending => write!(f, "PENDING"),
|
||||
Self::Issued => write!(f, "ISSUED"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for QuoteState {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(state: &str) -> Result<Self, Self::Err> {
|
||||
match state {
|
||||
"PENDING" => Ok(Self::Pending),
|
||||
"PAID" => Ok(Self::Paid),
|
||||
"UNPAID" => Ok(Self::Unpaid),
|
||||
"ISSUED" => Ok(Self::Issued),
|
||||
_ => Err(Error::UnknownState),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mint quote response [NUT-04]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct MintQuoteBolt11Response {
|
||||
/// Quote Id
|
||||
pub quote: String,
|
||||
/// Payment request to fulfil
|
||||
pub request: String,
|
||||
// TODO: To be deprecated
|
||||
/// Whether the the request haas be paid
|
||||
pub paid: bool,
|
||||
/// Deprecated
|
||||
pub paid: Option<bool>,
|
||||
/// Quote State
|
||||
pub state: MintQuoteState,
|
||||
/// Unix timestamp until the quote is valid
|
||||
pub expiry: Option<u64>,
|
||||
}
|
||||
|
||||
// A custom deserializer is needed until all mints
|
||||
// update some will return without the required state.
|
||||
impl<'de> Deserialize<'de> for MintQuoteBolt11Response {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = Value::deserialize(deserializer)?;
|
||||
|
||||
let quote: String = serde_json::from_value(
|
||||
value
|
||||
.get("quote")
|
||||
.ok_or(serde::de::Error::missing_field("quote"))?
|
||||
.clone(),
|
||||
)
|
||||
.map_err(|_| serde::de::Error::custom("Invalid quote id string"))?;
|
||||
|
||||
let request: String = serde_json::from_value(
|
||||
value
|
||||
.get("request")
|
||||
.ok_or(serde::de::Error::missing_field("request"))?
|
||||
.clone(),
|
||||
)
|
||||
.map_err(|_| serde::de::Error::custom("Invalid request string"))?;
|
||||
|
||||
let paid: Option<bool> = value.get("paid").and_then(|p| p.as_bool());
|
||||
|
||||
let state: Option<String> = value
|
||||
.get("state")
|
||||
.and_then(|s| serde_json::from_value(s.clone()).ok());
|
||||
|
||||
let (state, paid) = match (state, paid) {
|
||||
(None, None) => return Err(serde::de::Error::custom("State or paid must be defined")),
|
||||
(Some(state), _) => {
|
||||
let state: QuoteState = QuoteState::from_str(&state)
|
||||
.map_err(|_| serde::de::Error::custom("Unknown state"))?;
|
||||
let paid = state == QuoteState::Paid;
|
||||
|
||||
(state, paid)
|
||||
}
|
||||
(None, Some(paid)) => {
|
||||
let state = if paid {
|
||||
QuoteState::Paid
|
||||
} else {
|
||||
QuoteState::Unpaid
|
||||
};
|
||||
(state, paid)
|
||||
}
|
||||
};
|
||||
|
||||
let expiry = value
|
||||
.get("expiry")
|
||||
.ok_or(serde::de::Error::missing_field("expiry"))?
|
||||
.as_u64();
|
||||
|
||||
Ok(Self {
|
||||
quote,
|
||||
request,
|
||||
paid: Some(paid),
|
||||
state,
|
||||
expiry,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MintQuote> for MintQuoteBolt11Response {
|
||||
fn from(mint_quote: MintQuote) -> MintQuoteBolt11Response {
|
||||
let paid = mint_quote.state == QuoteState::Paid;
|
||||
MintQuoteBolt11Response {
|
||||
quote: mint_quote.id,
|
||||
request: mint_quote.request,
|
||||
paid: mint_quote.paid,
|
||||
paid: Some(paid),
|
||||
state: mint_quote.state,
|
||||
expiry: Some(mint_quote.expiry),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,25 @@
|
||||
//!
|
||||
//! <https://github.com/cashubtc/nuts/blob/main/05.md>
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde_json::Value;
|
||||
use thiserror::Error;
|
||||
|
||||
use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, Proofs};
|
||||
use super::nut15::Mpp;
|
||||
use crate::types::MeltQuote;
|
||||
use crate::{Amount, Bolt11Invoice};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Unknown Quote State
|
||||
#[error("Unknown Quote State")]
|
||||
UnknownState,
|
||||
}
|
||||
|
||||
/// Melt quote request [NUT-05]
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MeltQuoteBolt11Request {
|
||||
@@ -20,8 +32,41 @@ pub struct MeltQuoteBolt11Request {
|
||||
pub options: Option<Mpp>,
|
||||
}
|
||||
|
||||
/// Possible states of a quote
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum QuoteState {
|
||||
#[default]
|
||||
Unpaid,
|
||||
Paid,
|
||||
Pending,
|
||||
}
|
||||
|
||||
impl fmt::Display for QuoteState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Unpaid => write!(f, "UNPAID"),
|
||||
Self::Paid => write!(f, "PAID"),
|
||||
Self::Pending => write!(f, "PENDING"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for QuoteState {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(state: &str) -> Result<Self, Self::Err> {
|
||||
match state {
|
||||
"PENDING" => Ok(Self::Pending),
|
||||
"PAID" => Ok(Self::Paid),
|
||||
"UNPAID" => Ok(Self::Unpaid),
|
||||
_ => Err(Error::UnknownState),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Melt quote response [NUT-05]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct MeltQuoteBolt11Response {
|
||||
/// Quote Id
|
||||
pub quote: String,
|
||||
@@ -30,19 +75,117 @@ pub struct MeltQuoteBolt11Response {
|
||||
/// The fee reserve that is required
|
||||
pub fee_reserve: Amount,
|
||||
/// Whether the the request haas be paid
|
||||
pub paid: bool,
|
||||
// TODO: To be deprecated
|
||||
/// Deprecated
|
||||
pub paid: Option<bool>,
|
||||
/// Quote State
|
||||
pub state: QuoteState,
|
||||
/// Unix timestamp until the quote is valid
|
||||
pub expiry: u64,
|
||||
/// Payment preimage
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub payment_preimage: Option<String>,
|
||||
/// Change
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub change: Option<Vec<BlindSignature>>,
|
||||
}
|
||||
|
||||
// A custom deserializer is needed until all mints
|
||||
// update some will return without the required state.
|
||||
impl<'de> Deserialize<'de> for MeltQuoteBolt11Response {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = Value::deserialize(deserializer)?;
|
||||
|
||||
let quote: String = serde_json::from_value(
|
||||
value
|
||||
.get("quote")
|
||||
.ok_or(serde::de::Error::missing_field("quote"))?
|
||||
.clone(),
|
||||
)
|
||||
.map_err(|_| serde::de::Error::custom("Invalid quote if string"))?;
|
||||
|
||||
let amount = value
|
||||
.get("amount")
|
||||
.ok_or(serde::de::Error::missing_field("amount"))?
|
||||
.as_u64()
|
||||
.ok_or(serde::de::Error::missing_field("amount"))?;
|
||||
let amount = Amount::from(amount);
|
||||
|
||||
let fee_reserve = value
|
||||
.get("fee_reserve")
|
||||
.ok_or(serde::de::Error::missing_field("fee_reserve"))?
|
||||
.as_u64()
|
||||
.ok_or(serde::de::Error::missing_field("fee_reserve"))?;
|
||||
|
||||
let fee_reserve = Amount::from(fee_reserve);
|
||||
|
||||
let paid: Option<bool> = value.get("paid").and_then(|p| p.as_bool());
|
||||
|
||||
let state: Option<String> = value
|
||||
.get("state")
|
||||
.and_then(|s| serde_json::from_value(s.clone()).ok());
|
||||
|
||||
let (state, paid) = match (state, paid) {
|
||||
(None, None) => return Err(serde::de::Error::custom("State or paid must be defined")),
|
||||
(Some(state), _) => {
|
||||
let state: QuoteState = QuoteState::from_str(&state)
|
||||
.map_err(|_| serde::de::Error::custom("Unknown state"))?;
|
||||
let paid = state == QuoteState::Paid;
|
||||
|
||||
(state, paid)
|
||||
}
|
||||
(None, Some(paid)) => {
|
||||
let state = if paid {
|
||||
QuoteState::Paid
|
||||
} else {
|
||||
QuoteState::Unpaid
|
||||
};
|
||||
(state, paid)
|
||||
}
|
||||
};
|
||||
|
||||
let expiry = value
|
||||
.get("expiry")
|
||||
.ok_or(serde::de::Error::missing_field("expiry"))?
|
||||
.as_u64()
|
||||
.ok_or(serde::de::Error::missing_field("expiry"))?;
|
||||
|
||||
let payment_preimage: Option<String> = value
|
||||
.get("payment_preimage")
|
||||
.and_then(|p| serde_json::from_value(p.clone()).ok());
|
||||
|
||||
let change: Option<Vec<BlindSignature>> = value
|
||||
.get("change")
|
||||
.and_then(|b| serde_json::from_value(b.clone()).ok());
|
||||
|
||||
Ok(Self {
|
||||
quote,
|
||||
amount,
|
||||
fee_reserve,
|
||||
paid: Some(paid),
|
||||
state,
|
||||
expiry,
|
||||
payment_preimage,
|
||||
change,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MeltQuote> for MeltQuoteBolt11Response {
|
||||
fn from(melt_quote: MeltQuote) -> MeltQuoteBolt11Response {
|
||||
let paid = melt_quote.state == QuoteState::Paid;
|
||||
MeltQuoteBolt11Response {
|
||||
quote: melt_quote.id,
|
||||
amount: melt_quote.amount,
|
||||
fee_reserve: melt_quote.fee_reserve,
|
||||
paid: melt_quote.paid,
|
||||
paid: Some(paid),
|
||||
state: melt_quote.state,
|
||||
expiry: melt_quote.expiry,
|
||||
payment_preimage: melt_quote.payment_preimage,
|
||||
change: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,6 +208,7 @@ impl MeltBolt11Request {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: to be deprecated
|
||||
/// Melt Response [NUT-05]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MeltBolt11Response {
|
||||
@@ -76,6 +220,16 @@ pub struct MeltBolt11Response {
|
||||
pub change: Option<Vec<BlindSignature>>,
|
||||
}
|
||||
|
||||
impl From<MeltQuoteBolt11Response> for MeltBolt11Response {
|
||||
fn from(quote_response: MeltQuoteBolt11Response) -> MeltBolt11Response {
|
||||
MeltBolt11Response {
|
||||
paid: quote_response.paid.unwrap(),
|
||||
payment_preimage: quote_response.payment_preimage,
|
||||
change: quote_response.change,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Melt Method Settings
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct MeltMethodSettings {
|
||||
|
||||
@@ -30,10 +30,10 @@ pub enum State {
|
||||
impl fmt::Display for State {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let s = match self {
|
||||
State::Spent => "SPENT",
|
||||
State::Unspent => "UNSPENT",
|
||||
State::Pending => "PENDING",
|
||||
State::Reserved => "RESERVED",
|
||||
Self::Spent => "SPENT",
|
||||
Self::Unspent => "UNSPENT",
|
||||
Self::Pending => "PENDING",
|
||||
Self::Reserved => "RESERVED",
|
||||
};
|
||||
|
||||
write!(f, "{}", s)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! <https://github.com/cashubtc/nuts/blob/main/08.md>
|
||||
|
||||
use super::nut05::{MeltBolt11Request, MeltBolt11Response};
|
||||
use super::nut05::{MeltBolt11Request, MeltQuoteBolt11Response};
|
||||
use crate::Amount;
|
||||
|
||||
impl MeltBolt11Request {
|
||||
@@ -13,7 +13,7 @@ impl MeltBolt11Request {
|
||||
}
|
||||
}
|
||||
|
||||
impl MeltBolt11Response {
|
||||
impl MeltQuoteBolt11Response {
|
||||
pub fn change_amount(&self) -> Option<Amount> {
|
||||
self.change
|
||||
.as_ref()
|
||||
|
||||
@@ -4,27 +4,21 @@ use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::nuts::{CurrencyUnit, Proof, Proofs, PublicKey, SpendingConditions, State};
|
||||
use crate::nuts::{
|
||||
CurrencyUnit, MeltQuoteState, MintQuoteState, Proof, Proofs, PublicKey, SpendingConditions,
|
||||
State,
|
||||
};
|
||||
use crate::url::UncheckedUrl;
|
||||
use crate::Amount;
|
||||
|
||||
/// Melt response with proofs
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
pub struct Melted {
|
||||
pub paid: bool,
|
||||
pub state: MeltQuoteState,
|
||||
pub preimage: Option<String>,
|
||||
pub change: Option<Proofs>,
|
||||
}
|
||||
|
||||
/// Possible states of an invoice
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum InvoiceStatus {
|
||||
Unpaid,
|
||||
Paid,
|
||||
Expired,
|
||||
InFlight,
|
||||
}
|
||||
|
||||
/// Mint Quote Info
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MintQuote {
|
||||
@@ -33,7 +27,7 @@ pub struct MintQuote {
|
||||
pub amount: Amount,
|
||||
pub unit: CurrencyUnit,
|
||||
pub request: String,
|
||||
pub paid: bool,
|
||||
pub state: MintQuoteState,
|
||||
pub expiry: u64,
|
||||
}
|
||||
|
||||
@@ -53,7 +47,7 @@ impl MintQuote {
|
||||
amount,
|
||||
unit,
|
||||
request,
|
||||
paid: false,
|
||||
state: MintQuoteState::Unpaid,
|
||||
expiry,
|
||||
}
|
||||
}
|
||||
@@ -67,10 +61,12 @@ pub struct MeltQuote {
|
||||
pub amount: Amount,
|
||||
pub request: String,
|
||||
pub fee_reserve: Amount,
|
||||
pub paid: bool,
|
||||
pub state: MeltQuoteState,
|
||||
pub expiry: u64,
|
||||
pub payment_preimage: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "mint")]
|
||||
impl MeltQuote {
|
||||
pub fn new(
|
||||
request: String,
|
||||
@@ -87,8 +83,9 @@ impl MeltQuote {
|
||||
unit,
|
||||
request,
|
||||
fee_reserve,
|
||||
paid: false,
|
||||
state: MeltQuoteState::Unpaid,
|
||||
expiry,
|
||||
payment_preimage: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,14 @@ use url::Url;
|
||||
|
||||
use super::Error;
|
||||
use crate::error::ErrorResponse;
|
||||
use crate::nuts::nut05::MeltBolt11Response;
|
||||
use crate::nuts::nut15::Mpp;
|
||||
use crate::nuts::{
|
||||
BlindedMessage, CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysResponse,
|
||||
KeysetResponse, MeltBolt11Request, MeltBolt11Response, MeltQuoteBolt11Request,
|
||||
MeltQuoteBolt11Response, MintBolt11Request, MintBolt11Response, MintInfo,
|
||||
MintQuoteBolt11Request, MintQuoteBolt11Response, PreMintSecrets, Proof, PublicKey,
|
||||
RestoreRequest, RestoreResponse, SwapRequest, SwapResponse,
|
||||
KeysetResponse, MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response,
|
||||
MintBolt11Request, MintBolt11Response, MintInfo, MintQuoteBolt11Request,
|
||||
MintQuoteBolt11Response, PreMintSecrets, Proof, PublicKey, RestoreRequest, RestoreResponse,
|
||||
SwapRequest, SwapResponse,
|
||||
};
|
||||
use crate::{Amount, Bolt11Invoice};
|
||||
|
||||
@@ -112,7 +113,10 @@ impl HttpClient {
|
||||
|
||||
match serde_json::from_value::<MintQuoteBolt11Response>(res.clone()) {
|
||||
Ok(mint_quote_response) => Ok(mint_quote_response),
|
||||
Err(_) => Err(ErrorResponse::from_value(res)?.into()),
|
||||
Err(err) => {
|
||||
tracing::warn!("{}", err);
|
||||
Err(ErrorResponse::from_value(res)?.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +133,10 @@ impl HttpClient {
|
||||
|
||||
match serde_json::from_value::<MintQuoteBolt11Response>(res.clone()) {
|
||||
Ok(mint_quote_response) => Ok(mint_quote_response),
|
||||
Err(_) => Err(ErrorResponse::from_value(res)?.into()),
|
||||
Err(err) => {
|
||||
tracing::warn!("{}", err);
|
||||
Err(ErrorResponse::from_value(res)?.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,9 +248,14 @@ impl HttpClient {
|
||||
.json::<Value>()
|
||||
.await?;
|
||||
|
||||
match serde_json::from_value::<MeltBolt11Response>(res.clone()) {
|
||||
Ok(melt_quote_response) => Ok(melt_quote_response),
|
||||
Err(_) => Err(ErrorResponse::from_value(res)?.into()),
|
||||
match serde_json::from_value::<MeltQuoteBolt11Response>(res.clone()) {
|
||||
Ok(melt_quote_response) => Ok(melt_quote_response.into()),
|
||||
Err(_) => {
|
||||
if let Ok(res) = serde_json::from_value::<MeltBolt11Response>(res.clone()) {
|
||||
return Ok(res);
|
||||
}
|
||||
Err(ErrorResponse::from_value(res)?.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ use crate::cdk_database::{self, WalletDatabase};
|
||||
use crate::dhke::{construct_proofs, hash_to_curve};
|
||||
use crate::nuts::{
|
||||
nut10, nut12, Conditions, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, Kind,
|
||||
MeltQuoteBolt11Response, MintInfo, MintQuoteBolt11Response, PreMintSecrets, PreSwap, Proof,
|
||||
ProofState, Proofs, PublicKey, RestoreRequest, SecretKey, SigFlag, SpendingConditions, State,
|
||||
SwapRequest, Token,
|
||||
MeltQuoteBolt11Response, MeltQuoteState, MintInfo, MintQuoteBolt11Response, MintQuoteState,
|
||||
PreMintSecrets, PreSwap, Proof, ProofState, Proofs, PublicKey, RestoreRequest, SecretKey,
|
||||
SigFlag, SpendingConditions, State, SwapRequest, Token,
|
||||
};
|
||||
use crate::types::{MeltQuote, Melted, MintQuote, ProofInfo};
|
||||
use crate::url::UncheckedUrl;
|
||||
@@ -380,7 +380,7 @@ impl Wallet {
|
||||
amount,
|
||||
unit: unit.clone(),
|
||||
request: quote_res.request,
|
||||
paid: quote_res.paid,
|
||||
state: quote_res.state,
|
||||
expiry: quote_res.expiry.unwrap_or(0),
|
||||
};
|
||||
|
||||
@@ -391,10 +391,7 @@ impl Wallet {
|
||||
|
||||
/// Mint quote status
|
||||
#[instrument(skip(self, quote_id))]
|
||||
pub async fn mint_quote_status(
|
||||
&self,
|
||||
quote_id: &str,
|
||||
) -> Result<MintQuoteBolt11Response, Error> {
|
||||
pub async fn mint_quote_state(&self, quote_id: &str) -> Result<MintQuoteBolt11Response, Error> {
|
||||
let response = self
|
||||
.client
|
||||
.get_mint_quote_status(self.mint_url.clone().try_into()?, quote_id)
|
||||
@@ -404,7 +401,7 @@ impl Wallet {
|
||||
Some(quote) => {
|
||||
let mut quote = quote;
|
||||
|
||||
quote.paid = response.paid;
|
||||
quote.state = response.state;
|
||||
self.localstore.add_mint_quote(quote).await?;
|
||||
}
|
||||
None => {
|
||||
@@ -422,9 +419,9 @@ impl Wallet {
|
||||
let mut total_amount = Amount::ZERO;
|
||||
|
||||
for mint_quote in mint_quotes {
|
||||
let mint_quote_response = self.mint_quote_status(&mint_quote.id).await?;
|
||||
let mint_quote_response = self.mint_quote_state(&mint_quote.id).await?;
|
||||
|
||||
if mint_quote_response.paid {
|
||||
if mint_quote_response.state == MintQuoteState::Paid {
|
||||
let amount = self
|
||||
.mint(&mint_quote.id, SplitTarget::default(), None)
|
||||
.await?;
|
||||
@@ -864,8 +861,9 @@ impl Wallet {
|
||||
request,
|
||||
unit: self.unit.clone(),
|
||||
fee_reserve: quote_res.fee_reserve,
|
||||
paid: quote_res.paid,
|
||||
state: quote_res.state,
|
||||
expiry: quote_res.expiry,
|
||||
payment_preimage: quote_res.payment_preimage,
|
||||
};
|
||||
|
||||
self.localstore.add_melt_quote(quote.clone()).await?;
|
||||
@@ -888,7 +886,7 @@ impl Wallet {
|
||||
Some(quote) => {
|
||||
let mut quote = quote;
|
||||
|
||||
quote.paid = response.paid;
|
||||
quote.state = response.state;
|
||||
self.localstore.add_melt_quote(quote).await?;
|
||||
}
|
||||
None => {
|
||||
@@ -976,8 +974,13 @@ impl Wallet {
|
||||
None => None,
|
||||
};
|
||||
|
||||
let state = match melt_response.paid {
|
||||
true => MeltQuoteState::Paid,
|
||||
false => MeltQuoteState::Unpaid,
|
||||
};
|
||||
|
||||
let melted = Melted {
|
||||
paid: true,
|
||||
state,
|
||||
preimage: melt_response.payment_preimage,
|
||||
change: change_proofs.clone(),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user