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:
thesimplekid
2024-06-18 21:09:49 +01:00
parent bfb59f6bec
commit 7223c5bda8
37 changed files with 1050 additions and 199 deletions

View File

@@ -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)]

View File

@@ -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,

View File

@@ -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)]

View File

@@ -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)]

View File

@@ -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)]

View File

@@ -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(&quote_id)
.mint_quote_state(&quote_id)
.await
.map_err(into_err)?;

View File

@@ -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

View File

@@ -52,12 +52,14 @@ pub async fn melt(
}
let quote = wallet.melt_quote(bolt11.to_string(), None).await?;
println!("{:?}", quote);
let melt = wallet
.melt(&quote.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);
}

View File

@@ -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(&quote.id).await?;
let status = wallet.mint_quote_state(&quote.id).await?;
if status.paid {
if status.state == MintQuoteState::Paid {
break;
}

View File

@@ -133,8 +133,6 @@ pub async fn send(
)
.unwrap();
tracing::debug!("{}", data_pubkey.to_string());
Some(SpendingConditions::P2PKConditions {
data: data_pubkey,
conditions: Some(conditions),

View File

@@ -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 {

View File

@@ -1,4 +1,5 @@
pub mod error;
mod migrations;
#[cfg(feature = "mint")]
pub mod mint;

View 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(&quote)?;
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(&quote)?;
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)
}

View File

@@ -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(())
}

View File

@@ -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)),

View File

@@ -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),

View File

@@ -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;

View File

@@ -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);

View File

@@ -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,
})
}

View File

@@ -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),

View File

@@ -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;

View File

@@ -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);

View File

@@ -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,
})
}

View File

@@ -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(&quote.id).await.unwrap();
let status = wallet.mint_quote_state(&quote.id).await.unwrap();
println!("Quote status: {}", status.paid);
println!("Quote status: {}", status.state);
if status.paid {
if status.state == MintQuoteState::Paid {
break;
}

View File

@@ -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(&quote.id).await.unwrap();
let status = wallet.mint_quote_state(&quote.id).await.unwrap();
println!("Quote status: {}", status.paid);
println!("Quote status: {}", status.state);
if status.paid {
if status.state == MintQuoteState::Paid {
break;
}

View File

@@ -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())
}

View File

@@ -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>;

View File

@@ -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")]

View File

@@ -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,
})
}

View File

@@ -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};

View File

@@ -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),
}
}

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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()

View File

@@ -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,
}
}
}

View File

@@ -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())
}
}
}

View File

@@ -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(),
};