diff --git a/README.md b/README.md index b8714af9..98e568fe 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,91 @@ +# eTIMEbank + +` work in progress ` + +This is a **Proof of Concept** fork of the [Cashu Development Kit (cdk)](https://github.com/cashubtc/cdk) + +Concept is the creation of a FOSS ecash based time-banking application that runs both the wallet client and mint logic for offline and offgrid communities. + +This project will be submitted to a [Bitcoin++ Hackathon](https://btcplusplus.dev/) but you are free to fork it and make it your own. PRs welcome. + +This project by design not include any Bitcoin elements and does not use satoshis as a unit, it is an exercise in applying a [Chaumian blinded signatures scheme](https://wikipedia.org/wiki/Blind_signature) to the concept of [Time Banking](https://en.wikipedia.org/wiki/Time_banking) + +To know more about the design choices, what time banking is, and the reason why I made this project please refer to the [context.md document within this repository](https://github.com/0xg4tt0/eTIMEbank/blob/main/context.md) + +## TO DO + +- [x] fork cdk & write project description +- [x] write context.md +- [ ] remove Lightning Network logic: + - [ ] cdk-lnbits + - [ ] cdk-lnd + - [ ] cdk-phoenixd + - [ ] cdk-strike + - [ ] ... +- [ ] remove Bitcoin logic: + - [ ] multi-mint logic + - [ ] channel opening/closing + - [ ] ... +- [ ] replace 'sat' with 'time' + - [ ] define 'time' + - [ ] integrate logic to check real time and 'ecash 'time' + - [ ] melt & mint logic remains the same + - [ ] ... +- [ ] testing +- [ ] deploy on 2 devices +- [ ] test sharing 'time' +- [ ] +- [ ] +- [ ] slap together some slides +- [ ] drink milk +- [ ] profit ? + + +Design choices for project + +- 100% offline use (no http/API) +- should run on smartphones made after 2010 (ideally a PWA) +- access-control to the mint (not open to anyone) +- etime notes represent minutes of real time (time counter mechanism) +- no 'withdrawl' or 'deposit' logic (no exchange of etime for other assets) +- ability to cryptographically query the mint for unique audits (more transparency with verifiability) + +## Specifications + +*meow* +*meow* + + +## Relevnt NUTs + +*meow* +*meow* + + +## Feature Request / Future + +*meow* +*meow* +- multi-mint or federated setup of mints within wallet client (Fedi example) +- LoRA / mesh network / bluetooth connectivity protocol for clients (offline) +- APK build +- reproducible build +- Translate into multiple languages (not just European ones) + +## How to deploy / build + +*meow* +*meow* + + +## License + +Code is under the [MIT License](LICENSE) + +# Archive of the original README.md from cdk fork + +``` + > **Warning** > This project is in early development, it does however work with real sats! Always use amounts you don't mind loosing. @@ -86,6 +174,8 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [08]: https://github.com/cashubtc/nuts/blob/main/08.md [09]: https://github.com/cashubtc/nuts/blob/main/09.md [10]: https://github.com/cashubtc/nuts/blob/main/10.md + +``` [11]: https://github.com/cashubtc/nuts/blob/main/11.md [12]: https://github.com/cashubtc/nuts/blob/main/12.md [13]: https://github.com/cashubtc/nuts/blob/main/13.md diff --git a/bindings/README.md b/bindings/README.md deleted file mode 100644 index d17392ef..00000000 --- a/bindings/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# CDK Bindings - -Javascript: - [cdk-js](./cdk-js/) javascript bindings for [cdk](../crates/cdk/) diff --git a/bindings/cdk-js/.npmignore b/bindings/cdk-js/.npmignore deleted file mode 100644 index 1147df4e..00000000 --- a/bindings/cdk-js/.npmignore +++ /dev/null @@ -1,12 +0,0 @@ -.cargo/ -target/ -src/ -scripts/ -examples/ -pkg/package.json -pkg/*.md -pkg/*.wasm -.gitignore -Cargo.toml -Cargo.lock -*.tgz diff --git a/bindings/cdk-js/Cargo.toml b/bindings/cdk-js/Cargo.toml deleted file mode 100644 index 83626cbc..00000000 --- a/bindings/cdk-js/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "cdk-js" -version = "0.4.0" -edition = "2021" -license = "MIT" -homepage = "https://github.com/cashubtc/cdk" -repository = "https://github.com/cashubtc/cdk.git" -rust-version = "1.63.0" # MSRV -description = "Cashu Development Kit JS Bindings" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[lib] -crate-type = ["lib", "cdylib"] - -[dependencies] -cdk = { path = "../../crates/cdk", features = ["wallet"] } -cdk-rexie = { path = "../../crates/cdk-rexie", features = ["wallet"] } -console_error_panic_hook = "0.1" -js-sys = "0.3" -serde_json = "1" -serde-wasm-bindgen = "0.6.5" -serde = { version = "1", default-features = false, features = ["derive"] } -wasm-bindgen = { version = "0.2.92", features = ["serde-serialize"] } -wasm-bindgen-futures = "0.4.41" -web-sys = { version = "0.3.69", default-features = false, features = ["console"] } diff --git a/bindings/cdk-js/examples/mint_token.js b/bindings/cdk-js/examples/mint_token.js deleted file mode 100644 index 78deb7bd..00000000 --- a/bindings/cdk-js/examples/mint_token.js +++ /dev/null @@ -1,61 +0,0 @@ -const { - loadWasmAsync, - Wallet, - CurrencyUnit -} = require("../"); - -async function main() { - await loadWasmAsync(); - let seed = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - let mint_url = "https://testnut.cashu.space"; - let currency = CurrencyUnit.Sat; - - wallet = await new Wallet(seed, []); - - await wallet.addMint(mint_url); - await wallet.refreshMint(mint_url); - - - - let amount = 10; - - let quote = await wallet?.mintQuote($mint_url, BigInt(amount), currency); - let quote_id = quote?.id; - - let invoice = quote?.request; - if (invoice != undefined) { - data = invoice; - } - - let paid = false; - while (paid == false) { - let check_mint = await wallet?.mintQuoteStatus(mint_url, quote_id); - if (check_mint?.paid == true) { - paid = true; - } else { - await new Promise((r) => setTimeout(r, 2000)); - } - - await wallet?.mint( - mint_url, - quote_id, - undefined, - undefined, - undefined, - ); - - let token = await wallet?.send( - mint_url, - currency, - undefined, - BigInt(amount) undefined, - undefined, - ); - - console.log(token); - - - } -} - -main(); diff --git a/bindings/cdk-js/justfile b/bindings/cdk-js/justfile deleted file mode 100644 index b3e6e575..00000000 --- a/bindings/cdk-js/justfile +++ /dev/null @@ -1,3 +0,0 @@ -pack: - rm -rf ./pkg - wasm-pack build --target web diff --git a/bindings/cdk-js/package.json b/bindings/cdk-js/package.json deleted file mode 100644 index 270644be..00000000 --- a/bindings/cdk-js/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "@cashudevkit/cdk", - "version": "0.0.0", - "description": "Cashu Dev Kit", - "keywords": [ - "cashu" - "rust", - "bindings", - "javascript" - ], - "license": "MIT", - "homepage": "https://github.com/cashubtc/cdk", - "repository": { - "type": "git", - "url": "git+https://github.com/cashubtc/cdk.git" - }, - "bugs": { - "url": "https://github.com/cashubtc/cdk/issues" - }, - "author": { - "name": "thesimplekid", - "email": "tsk@thesimplekid.com", - "url": "https://github.com/thesimplekid" - }, - "main": "pkg/cdk_js.js", - "types": "pkg/cdk_js.d.ts", - "files": [ - "pkg/cdk_js_bg.wasm.js", - "pkg/cdk_js_bg.wasm.d.ts", - "pkg/cdk_js.js", - "pkg/cdk_js.d.ts" - ], - "devDependencies": { - "wasm-pack": "^0.10.2" - }, - "engines": { - "node": ">= 10" - }, - "scripts": { - "build": "wasm-pack build --target web", - } -} diff --git a/bindings/cdk-js/src/error.rs b/bindings/cdk-js/src/error.rs deleted file mode 100644 index d4e1cb0d..00000000 --- a/bindings/cdk-js/src/error.rs +++ /dev/null @@ -1,12 +0,0 @@ -use wasm_bindgen::JsValue; - -pub type Result = std::result::Result; - -/// Helper to replace the `E` to `Error` to `napi::Error` conversion. -#[inline] -pub fn into_err(error: E) -> JsValue -where - E: std::fmt::Display, -{ - JsValue::from_str(&error.to_string()) -} diff --git a/bindings/cdk-js/src/lib.rs b/bindings/cdk-js/src/lib.rs deleted file mode 100644 index 60c66d60..00000000 --- a/bindings/cdk-js/src/lib.rs +++ /dev/null @@ -1,12 +0,0 @@ -use wasm_bindgen::prelude::*; - -pub mod error; -pub mod nuts; -pub mod types; -#[cfg(target_arch = "wasm32")] -pub mod wallet; - -#[wasm_bindgen(start)] -pub fn start() { - console_error_panic_hook::set_once(); -} diff --git a/bindings/cdk-js/src/nuts/mod.rs b/bindings/cdk-js/src/nuts/mod.rs deleted file mode 100644 index 0e3e30f2..00000000 --- a/bindings/cdk-js/src/nuts/mod.rs +++ /dev/null @@ -1,24 +0,0 @@ -pub mod nut00; -pub mod nut01; -pub mod nut02; -pub mod nut03; -pub mod nut04; -pub mod nut05; -pub mod nut06; -pub mod nut07; -pub mod nut09; -pub mod nut10; -pub mod nut11; -pub mod nut12; -pub mod nut14; - -pub use nut00::*; -pub use nut01::{JsKeys, JsPublicKey, JsSecretKey}; -pub use nut02::JsId; -pub use nut03::{JsSwapRequest, JsSwapResponse}; -pub use nut06::{JsMintInfo, JsMintVersion}; -pub use nut07::*; -pub use nut09::{JsRestoreRequest, JsRestoreResponse}; -pub use nut11::*; -pub use nut12::{JsBlindSignatureDleq, JsProofDleq}; -pub use nut14::JsHTLCWitness; diff --git a/bindings/cdk-js/src/nuts/nut00/blind_signature.rs b/bindings/cdk-js/src/nuts/nut00/blind_signature.rs deleted file mode 100644 index 31981a53..00000000 --- a/bindings/cdk-js/src/nuts/nut00/blind_signature.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::ops::Deref; - -use cdk::nuts::BlindSignature; -use wasm_bindgen::prelude::*; - -use crate::nuts::nut01::JsPublicKey; -use crate::nuts::nut02::JsId; -use crate::nuts::JsBlindSignatureDleq; -use crate::types::JsAmount; - -#[wasm_bindgen(js_name = BlindSignature)] -pub struct JsBlindSignature { - inner: BlindSignature, -} - -impl Deref for JsBlindSignature { - type Target = BlindSignature; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -#[wasm_bindgen(js_class = BlindSignature)] -impl JsBlindSignature { - #[allow(clippy::new_without_default)] - #[wasm_bindgen(constructor)] - pub fn new( - keyset_id: JsId, - amount: JsAmount, - c: JsPublicKey, - dleq: Option, - ) -> Self { - Self { - inner: BlindSignature { - keyset_id: *keyset_id.deref(), - amount: *amount.deref(), - c: *c.deref(), - dleq: dleq.map(|b| b.deref().clone()), - }, - } - } -} diff --git a/bindings/cdk-js/src/nuts/nut00/blinded_message.rs b/bindings/cdk-js/src/nuts/nut00/blinded_message.rs deleted file mode 100644 index 92eff5f3..00000000 --- a/bindings/cdk-js/src/nuts/nut00/blinded_message.rs +++ /dev/null @@ -1,71 +0,0 @@ -use std::ops::Deref; - -use cdk::nuts::BlindedMessage; -use wasm_bindgen::prelude::*; - -use super::JsWitness; -use crate::nuts::{JsId, JsPublicKey}; -use crate::types::JsAmount; - -#[wasm_bindgen(js_name = BlindedMessage)] -pub struct JsBlindedMessage { - inner: BlindedMessage, -} - -impl Deref for JsBlindedMessage { - type Target = BlindedMessage; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsBlindedMessage { - fn from(inner: BlindedMessage) -> JsBlindedMessage { - JsBlindedMessage { inner } - } -} - -#[wasm_bindgen(js_class = BlindedMessage)] -impl JsBlindedMessage { - #[allow(clippy::new_without_default)] - #[wasm_bindgen(constructor)] - pub fn new( - keyset_id: JsId, - amount: JsAmount, - blinded_secret: JsPublicKey, - witness: Option, - ) -> Self { - Self { - inner: BlindedMessage { - keyset_id: *keyset_id.deref(), - amount: *amount.deref(), - blinded_secret: *blinded_secret.deref(), - witness: witness.map(|w| w.deref().clone()), - }, - } - } - - /// Keyset Id - #[wasm_bindgen(getter)] - pub fn keyset_id(&self) -> JsId { - self.keyset_id.into() - } - - /// Amount - #[wasm_bindgen(getter)] - pub fn amount(&self) -> JsAmount { - self.inner.amount.into() - } - - /// Blinded Secret - #[wasm_bindgen(getter)] - pub fn blinded_secret(&self) -> JsPublicKey { - self.inner.blinded_secret.into() - } - - /// Witness - #[wasm_bindgen(getter)] - pub fn witness(&self) -> Option { - self.inner.witness.clone().map(|w| w.into()) - } -} diff --git a/bindings/cdk-js/src/nuts/nut00/currency_unit.rs b/bindings/cdk-js/src/nuts/nut00/currency_unit.rs deleted file mode 100644 index 02251de3..00000000 --- a/bindings/cdk-js/src/nuts/nut00/currency_unit.rs +++ /dev/null @@ -1,35 +0,0 @@ -use cdk::nuts::CurrencyUnit; -use wasm_bindgen::prelude::*; - -// use crate::nuts::{JsHTLCWitness, JsP2PKWitness}; - -#[wasm_bindgen(js_name = CurrencyUnit)] -pub enum JsCurrencyUnit { - Sat, - Msat, - Usd, - Eur, -} - -impl From for JsCurrencyUnit { - fn from(inner: CurrencyUnit) -> JsCurrencyUnit { - match inner { - CurrencyUnit::Sat => JsCurrencyUnit::Sat, - CurrencyUnit::Msat => JsCurrencyUnit::Msat, - CurrencyUnit::Usd => JsCurrencyUnit::Usd, - CurrencyUnit::Eur => JsCurrencyUnit::Eur, - _ => panic!("Unsupported unit"), - } - } -} - -impl From for CurrencyUnit { - fn from(inner: JsCurrencyUnit) -> CurrencyUnit { - match inner { - JsCurrencyUnit::Sat => CurrencyUnit::Sat, - JsCurrencyUnit::Msat => CurrencyUnit::Msat, - JsCurrencyUnit::Usd => CurrencyUnit::Usd, - JsCurrencyUnit::Eur => CurrencyUnit::Eur, - } - } -} diff --git a/bindings/cdk-js/src/nuts/nut00/mod.rs b/bindings/cdk-js/src/nuts/nut00/mod.rs deleted file mode 100644 index 17f8d66a..00000000 --- a/bindings/cdk-js/src/nuts/nut00/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub mod blind_signature; -pub mod blinded_message; -pub mod currency_unit; -pub mod premint; -pub mod proof; -pub mod token; -pub mod witness; - -pub use blinded_message::JsBlindedMessage; -pub use currency_unit::JsCurrencyUnit; -pub use premint::{JsPreMint, JsPreMintSecrets}; -pub use proof::JsProof; -pub use token::JsToken; -pub use witness::JsWitness; diff --git a/bindings/cdk-js/src/nuts/nut00/premint.rs b/bindings/cdk-js/src/nuts/nut00/premint.rs deleted file mode 100644 index 9e8f7a82..00000000 --- a/bindings/cdk-js/src/nuts/nut00/premint.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::ops::Deref; - -use cdk::nuts::{PreMint, PreMintSecrets}; -use wasm_bindgen::prelude::*; - -#[wasm_bindgen(js_name = PreMint)] -pub struct JsPreMint { - inner: PreMint, -} - -impl Deref for JsPreMint { - type Target = PreMint; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsPreMint { - fn from(inner: PreMint) -> JsPreMint { - JsPreMint { inner } - } -} - -#[wasm_bindgen(js_name = PreMintSecrets)] -pub struct JsPreMintSecrets { - inner: PreMintSecrets, -} - -impl Deref for JsPreMintSecrets { - type Target = PreMintSecrets; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsPreMintSecrets { - fn from(inner: PreMintSecrets) -> JsPreMintSecrets { - JsPreMintSecrets { inner } - } -} diff --git a/bindings/cdk-js/src/nuts/nut00/proof.rs b/bindings/cdk-js/src/nuts/nut00/proof.rs deleted file mode 100644 index ac474e48..00000000 --- a/bindings/cdk-js/src/nuts/nut00/proof.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::ops::Deref; - -use cdk::nuts::Proof; -use wasm_bindgen::prelude::*; - -use super::JsWitness; -use crate::nuts::nut01::JsPublicKey; -use crate::nuts::nut02::JsId; -use crate::nuts::nut12::JsProofDleq; -use crate::types::{JsAmount, JsSecret}; - -#[wasm_bindgen(js_name = Proof)] -pub struct JsProof { - inner: Proof, -} - -impl Deref for JsProof { - type Target = Proof; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsProof { - fn from(inner: Proof) -> JsProof { - JsProof { inner } - } -} - -#[wasm_bindgen(js_class = Proof)] -impl JsProof { - #[wasm_bindgen(constructor)] - pub fn new( - amount: JsAmount, - secret: JsSecret, - c: JsPublicKey, - keyset_id: JsId, - witness: Option, - dleq: Option, - ) -> Self { - Self { - inner: Proof { - amount: *amount.deref(), - secret: secret.deref().clone(), - c: *c.deref(), - keyset_id: *keyset_id.deref(), - witness: witness.map(|w| w.deref().clone()), - dleq: dleq.map(|d| d.deref().clone()), - }, - } - } - - /// Amount - #[wasm_bindgen(getter)] - pub fn amount(&self) -> JsAmount { - self.inner.amount.into() - } - - /// Secret - #[wasm_bindgen(getter)] - pub fn secret(&self) -> JsSecret { - self.inner.secret.clone().into() - } - - /// C - #[wasm_bindgen(getter)] - pub fn c(&self) -> JsPublicKey { - self.inner.c.into() - } - - /// Keyset Id - #[wasm_bindgen(getter)] - pub fn keyset_id(&self) -> JsId { - self.inner.keyset_id.into() - } -} diff --git a/bindings/cdk-js/src/nuts/nut00/token.rs b/bindings/cdk-js/src/nuts/nut00/token.rs deleted file mode 100644 index 051954b8..00000000 --- a/bindings/cdk-js/src/nuts/nut00/token.rs +++ /dev/null @@ -1,35 +0,0 @@ -use std::ops::Deref; -use std::str::FromStr; - -use cdk::nuts::Token; -use wasm_bindgen::prelude::*; - -use crate::error::{into_err, Result}; - -#[wasm_bindgen(js_name = Token)] -pub struct JsToken { - inner: Token, -} - -impl Deref for JsToken { - type Target = Token; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsToken { - fn from(inner: Token) -> JsToken { - JsToken { inner } - } -} - -#[wasm_bindgen(js_class = Token)] -impl JsToken { - #[wasm_bindgen(constructor)] - pub fn new(token: String) -> Result { - Ok(Self { - inner: Token::from_str(&token).map_err(into_err)?, - }) - } -} diff --git a/bindings/cdk-js/src/nuts/nut00/witness.rs b/bindings/cdk-js/src/nuts/nut00/witness.rs deleted file mode 100644 index 8c16481f..00000000 --- a/bindings/cdk-js/src/nuts/nut00/witness.rs +++ /dev/null @@ -1,52 +0,0 @@ -use std::ops::Deref; - -use cdk::nuts::{HTLCWitness, P2PKWitness, Witness}; -use wasm_bindgen::prelude::*; - -use crate::error::Result; - -#[wasm_bindgen(js_name = Witness)] -pub struct JsWitness { - inner: Witness, -} - -impl Deref for JsWitness { - type Target = Witness; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsWitness { - fn from(inner: Witness) -> JsWitness { - JsWitness { inner } - } -} - -#[wasm_bindgen(js_class = Witness)] -impl JsWitness { - #[wasm_bindgen(constructor)] - pub fn new(preimage: Option, signatures: Option>) -> Result { - match preimage { - Some(preimage) => Ok(Witness::HTLCWitness(HTLCWitness { - preimage, - signatures, - }) - .into()), - None => Ok(Witness::P2PKWitness(P2PKWitness { - signatures: signatures.unwrap(), - }) - .into()), - } - } - - #[wasm_bindgen(getter)] - pub fn signatures(&self) -> Option> { - self.inner.signatures() - } - - #[wasm_bindgen(getter)] - pub fn preimage(&self) -> Option { - self.inner.preimage() - } -} diff --git a/bindings/cdk-js/src/nuts/nut01/keys.rs b/bindings/cdk-js/src/nuts/nut01/keys.rs deleted file mode 100644 index e44db08f..00000000 --- a/bindings/cdk-js/src/nuts/nut01/keys.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::ops::Deref; - -use cdk::nuts::nut01::Keys; -use wasm_bindgen::prelude::*; - -use super::JsPublicKey; -use crate::error::{into_err, Result}; -use crate::types::JsAmount; - -#[wasm_bindgen(js_name = Keys)] -pub struct JsKeys { - inner: Keys, -} - -impl Deref for JsKeys { - type Target = Keys; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsKeys { - fn from(inner: Keys) -> JsKeys { - JsKeys { inner } - } -} - -#[wasm_bindgen(js_class = Keys)] -impl JsKeys { - /// From Hex - #[wasm_bindgen(constructor)] - pub fn new(keys: JsValue) -> Result { - let keys = serde_wasm_bindgen::from_value(keys).map_err(into_err)?; - - Ok(JsKeys { - inner: Keys::new(keys), - }) - } - - /// Keys - #[wasm_bindgen(js_name = keys)] - pub fn keys(&self) -> Result { - serde_wasm_bindgen::to_value(&self.inner.keys()).map_err(into_err) - } - - /// Amount Key - #[wasm_bindgen(js_name = amountKey)] - pub fn amount_key(&self, amount: JsAmount) -> Option { - self.inner.amount_key(*amount.deref()).map(|k| k.into()) - } -} diff --git a/bindings/cdk-js/src/nuts/nut01/mod.rs b/bindings/cdk-js/src/nuts/nut01/mod.rs deleted file mode 100644 index 888a7f89..00000000 --- a/bindings/cdk-js/src/nuts/nut01/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod keys; -pub mod public_key; -pub mod secret_key; - -pub use keys::JsKeys; -pub use public_key::JsPublicKey; -pub use secret_key::JsSecretKey; diff --git a/bindings/cdk-js/src/nuts/nut01/public_key.rs b/bindings/cdk-js/src/nuts/nut01/public_key.rs deleted file mode 100644 index 7fb3440a..00000000 --- a/bindings/cdk-js/src/nuts/nut01/public_key.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::ops::Deref; - -use cdk::nuts::nut01::PublicKey; -use wasm_bindgen::prelude::*; - -use crate::error::{into_err, Result}; - -#[wasm_bindgen(js_name = PublicKey)] -pub struct JsPublicKey { - inner: PublicKey, -} - -impl Deref for JsPublicKey { - type Target = PublicKey; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsPublicKey { - fn from(inner: PublicKey) -> JsPublicKey { - JsPublicKey { inner } - } -} - -#[wasm_bindgen(js_class = PublicKey)] -impl JsPublicKey { - /// From Hex - #[wasm_bindgen(js_name = fromHex)] - pub fn from_hex(hex: String) -> Result { - Ok(Self { - inner: PublicKey::from_hex(hex).map_err(into_err)?, - }) - } - - /// To Hex - #[wasm_bindgen(js_name = toHex)] - pub fn to_hex(&self) -> String { - self.inner.to_hex() - } -} diff --git a/bindings/cdk-js/src/nuts/nut01/secret_key.rs b/bindings/cdk-js/src/nuts/nut01/secret_key.rs deleted file mode 100644 index 6bd7180c..00000000 --- a/bindings/cdk-js/src/nuts/nut01/secret_key.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::ops::Deref; - -use cdk::nuts::nut01::SecretKey; -use wasm_bindgen::prelude::*; - -#[wasm_bindgen(js_name = SecretKey)] -pub struct JsSecretKey { - inner: SecretKey, -} - -impl Deref for JsSecretKey { - type Target = SecretKey; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsSecretKey { - fn from(inner: SecretKey) -> JsSecretKey { - JsSecretKey { inner } - } -} - -#[wasm_bindgen(js_class = SecretKey)] -impl JsSecretKey { - /// To Hex - #[wasm_bindgen(js_name = toHex)] - pub fn to_hex(&self) -> String { - self.inner.to_secret_hex() - } -} diff --git a/bindings/cdk-js/src/nuts/nut02/id.rs b/bindings/cdk-js/src/nuts/nut02/id.rs deleted file mode 100644 index d0044ae2..00000000 --- a/bindings/cdk-js/src/nuts/nut02/id.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::ops::Deref; -use std::str::FromStr; - -use cdk::nuts::Id; -use wasm_bindgen::prelude::*; - -use crate::error::{into_err, Result}; - -#[wasm_bindgen(js_name = Id)] -pub struct JsId { - inner: Id, -} - -impl Deref for JsId { - type Target = Id; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsId { - fn from(inner: Id) -> JsId { - JsId { inner } - } -} - -#[wasm_bindgen(js_class = Id)] -impl JsId { - /// Try From Base 64 String - #[wasm_bindgen(js_name = tryFromBase64)] - pub fn try_from_base64(id: String) -> Result { - Ok(JsId { - inner: Id::from_str(&id).map_err(into_err)?, - }) - } - - /// As String - #[wasm_bindgen(js_name = asString)] - pub fn as_string(&self) -> String { - self.inner.to_string() - } -} diff --git a/bindings/cdk-js/src/nuts/nut02/keyset.rs b/bindings/cdk-js/src/nuts/nut02/keyset.rs deleted file mode 100644 index e5f7550d..00000000 --- a/bindings/cdk-js/src/nuts/nut02/keyset.rs +++ /dev/null @@ -1,118 +0,0 @@ -use std::ops::Deref; -use std::str::FromStr; - -use cdk::nuts::{CurrencyUnit, KeySet, KeysResponse, KeysetResponse}; -use wasm_bindgen::prelude::*; - -use super::JsId; -use crate::error::{into_err, Result}; -use crate::nuts::JsKeys; - -#[wasm_bindgen(js_name = KeySet)] -pub struct JsKeySet { - inner: KeySet, -} - -impl Deref for JsKeySet { - type Target = KeySet; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsKeySet { - fn from(inner: KeySet) -> JsKeySet { - JsKeySet { inner } - } -} - -#[wasm_bindgen(js_class = KeyPair)] -impl JsKeySet { - /// From Hex - #[wasm_bindgen(constructor)] - pub fn new(id: JsId, unit: String, keys: JsKeys) -> JsKeySet { - Self { - inner: KeySet { - id: *id.deref(), - unit: CurrencyUnit::from_str(&unit).unwrap(), - keys: keys.deref().clone(), - }, - } - } - - #[wasm_bindgen(getter)] - pub fn id(&self) -> JsId { - self.inner.id.into() - } - - #[wasm_bindgen(getter)] - pub fn keys(&self) -> JsKeys { - self.inner.keys.clone().into() - } -} - -#[wasm_bindgen(js_name = KeySetsResponse)] -pub struct JsKeySetsResponse { - inner: KeysetResponse, -} - -impl Deref for JsKeySetsResponse { - type Target = KeysetResponse; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsKeySetsResponse { - fn from(inner: KeysetResponse) -> JsKeySetsResponse { - JsKeySetsResponse { inner } - } -} - -#[wasm_bindgen(js_class = KeySetsResponse)] -impl JsKeySetsResponse { - #[wasm_bindgen(constructor)] - pub fn new(keysets: JsValue) -> Result { - let response = serde_wasm_bindgen::from_value(keysets).map_err(into_err)?; - Ok(Self { inner: response }) - } - - /// Get KeySets - #[wasm_bindgen(getter)] - pub fn keys(&self) -> Result { - serde_wasm_bindgen::to_value(&self.inner.keysets).map_err(into_err) - } -} - -#[wasm_bindgen(js_name = KeysResponse)] -pub struct JsKeysResponse { - inner: KeysResponse, -} - -impl Deref for JsKeysResponse { - type Target = KeysResponse; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsKeysResponse { - fn from(inner: KeysResponse) -> JsKeysResponse { - JsKeysResponse { inner } - } -} - -#[wasm_bindgen(js_class = KeysResponse)] -impl JsKeysResponse { - #[wasm_bindgen(constructor)] - pub fn new(keysets: JsValue) -> Result { - let response = serde_wasm_bindgen::from_value(keysets).map_err(into_err)?; - Ok(Self { inner: response }) - } - - /// Get Keys - #[wasm_bindgen(getter)] - pub fn keysets(&self) -> Result { - serde_wasm_bindgen::to_value(&self.inner.keysets).map_err(into_err) - } -} diff --git a/bindings/cdk-js/src/nuts/nut02/mod.rs b/bindings/cdk-js/src/nuts/nut02/mod.rs deleted file mode 100644 index e88193dc..00000000 --- a/bindings/cdk-js/src/nuts/nut02/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod id; -pub mod keyset; - -pub use id::JsId; diff --git a/bindings/cdk-js/src/nuts/nut03.rs b/bindings/cdk-js/src/nuts/nut03.rs deleted file mode 100644 index 166bb72a..00000000 --- a/bindings/cdk-js/src/nuts/nut03.rs +++ /dev/null @@ -1,107 +0,0 @@ -use std::ops::Deref; - -use cdk::nuts::{SwapRequest, SwapResponse}; -use wasm_bindgen::prelude::*; - -use crate::error::{into_err, Result}; -use crate::types::JsAmount; - -#[wasm_bindgen(js_name = SwapRequest)] -pub struct JsSwapRequest { - inner: SwapRequest, -} - -impl Deref for JsSwapRequest { - type Target = SwapRequest; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsSwapRequest { - fn from(inner: SwapRequest) -> JsSwapRequest { - JsSwapRequest { inner } - } -} - -#[wasm_bindgen(js_class = SwapRequest)] -impl JsSwapRequest { - #[wasm_bindgen(constructor)] - pub fn new(inputs: JsValue, outputs: JsValue) -> Result { - let inputs = serde_wasm_bindgen::from_value(inputs).map_err(into_err)?; - let outputs = serde_wasm_bindgen::from_value(outputs).map_err(into_err)?; - - Ok(JsSwapRequest { - inner: SwapRequest { inputs, outputs }, - }) - } - - /// Get Proofs - #[wasm_bindgen(getter)] - pub fn proofs(&self) -> Result { - serde_wasm_bindgen::to_value(&self.inner.inputs).map_err(into_err) - } - - /// Get Outputs - #[wasm_bindgen(getter)] - pub fn outputs(&self) -> Result { - serde_wasm_bindgen::to_value(&self.inner.outputs).map_err(into_err) - } - - /// Proofs Amount - #[wasm_bindgen(js_name = proofsAmount)] - pub fn proofs_amount(&self) -> JsAmount { - self.inner.input_amount().expect("Amount overflow").into() - } - - /// Output Amount - #[wasm_bindgen(js_name = outputAmount)] - pub fn output_amount(&self) -> JsAmount { - self.inner.output_amount().expect("Amount overflow").into() - } -} - -#[wasm_bindgen(js_name = SplitResponse)] -pub struct JsSwapResponse { - inner: SwapResponse, -} - -impl Deref for JsSwapResponse { - type Target = SwapResponse; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsSwapResponse { - fn from(inner: SwapResponse) -> JsSwapResponse { - JsSwapResponse { inner } - } -} - -#[wasm_bindgen(js_class = SplitResponse)] -impl JsSwapResponse { - #[wasm_bindgen(constructor)] - pub fn new(signatures: JsValue) -> Result { - let signatures = serde_wasm_bindgen::from_value(signatures).map_err(into_err)?; - - Ok(JsSwapResponse { - inner: SwapResponse { signatures }, - }) - } - - /// Get Promises - #[wasm_bindgen(getter)] - pub fn signatures(&self) -> Result { - serde_wasm_bindgen::to_value(&self.inner.signatures).map_err(into_err) - } - - /// Promises Amount - #[wasm_bindgen(js_name = promisesAmount)] - pub fn promises_amount(&self) -> JsAmount { - self.inner - .promises_amount() - .expect("Amount overflow") - .into() - } -} diff --git a/bindings/cdk-js/src/nuts/nut04.rs b/bindings/cdk-js/src/nuts/nut04.rs deleted file mode 100644 index 6cbdb33c..00000000 --- a/bindings/cdk-js/src/nuts/nut04.rs +++ /dev/null @@ -1,178 +0,0 @@ -use std::ops::Deref; - -use cdk::nuts::nut04::{MintBolt11Request, MintBolt11Response, MintMethodSettings, Settings}; -use cdk::nuts::{MintQuoteBolt11Request, MintQuoteBolt11Response}; -use wasm_bindgen::prelude::*; - -use crate::error::{into_err, Result}; -use crate::types::JsAmount; - -#[wasm_bindgen(js_name = MintQuoteBolt11Request)] -pub struct JsMintQuoteBolt11Request { - inner: MintQuoteBolt11Request, -} - -impl Deref for JsMintQuoteBolt11Request { - type Target = MintQuoteBolt11Request; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsMintQuoteBolt11Request { - fn from(inner: MintQuoteBolt11Request) -> JsMintQuoteBolt11Request { - JsMintQuoteBolt11Request { inner } - } -} - -#[wasm_bindgen(js_name = MintQuoteBolt11Response)] -pub struct JsMintQuoteBolt11Response { - inner: MintQuoteBolt11Response, -} - -impl Deref for JsMintQuoteBolt11Response { - type Target = MintQuoteBolt11Response; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsMintQuoteBolt11Response { - fn from(inner: MintQuoteBolt11Response) -> JsMintQuoteBolt11Response { - JsMintQuoteBolt11Response { inner } - } -} - -#[wasm_bindgen(js_class = MintQuoteBolt11Response)] -impl JsMintQuoteBolt11Response { - #[wasm_bindgen(getter)] - pub fn state(&self) -> String { - self.inner.state.to_string() - } - - #[wasm_bindgen(getter)] - pub fn quote(&self) -> String { - self.inner.quote.clone() - } - - #[wasm_bindgen(getter)] - pub fn request(&self) -> String { - self.inner.request.clone() - } - - #[wasm_bindgen(getter)] - pub fn expiry(&self) -> Option { - self.inner.expiry - } -} - -#[wasm_bindgen(js_name = MintBolt11Request)] -pub struct JsMintBolt11Request { - inner: MintBolt11Request, -} - -impl Deref for JsMintBolt11Request { - type Target = MintBolt11Request; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsMintBolt11Request { - fn from(inner: MintBolt11Request) -> JsMintBolt11Request { - JsMintBolt11Request { inner } - } -} - -#[wasm_bindgen(js_class = MintBolt11Request)] -impl JsMintBolt11Request { - /// Try From Base 64 String - #[wasm_bindgen(constructor)] - pub fn new(quote: String, outputs: JsValue) -> Result { - let outputs = serde_wasm_bindgen::from_value(outputs).map_err(into_err)?; - Ok(JsMintBolt11Request { - inner: MintBolt11Request { quote, outputs }, - }) - } - - #[wasm_bindgen(getter)] - pub fn outputs(&self) -> Result { - serde_wasm_bindgen::to_value(&self.inner.outputs).map_err(into_err) - } - - #[wasm_bindgen(js_name = totalAmount)] - pub fn total_amount(&self) -> JsAmount { - self.inner.total_amount().expect("Amount overflow").into() - } -} - -#[wasm_bindgen(js_name = PostMintResponse)] -pub struct JsMintBolt11Response { - inner: MintBolt11Response, -} - -impl Deref for JsMintBolt11Response { - type Target = MintBolt11Response; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsMintBolt11Response { - fn from(inner: MintBolt11Response) -> JsMintBolt11Response { - JsMintBolt11Response { inner } - } -} - -#[wasm_bindgen(js_class = PostMintResponse)] -impl JsMintBolt11Response { - /// Try From Base 64 String - #[wasm_bindgen(constructor)] - pub fn new(signatures: JsValue) -> Result { - let signatures = serde_wasm_bindgen::from_value(signatures).map_err(into_err)?; - Ok(JsMintBolt11Response { - inner: MintBolt11Response { signatures }, - }) - } - - #[wasm_bindgen(getter)] - pub fn signatures(&self) -> Result { - serde_wasm_bindgen::to_value(&self.inner.signatures).map_err(into_err) - } -} - -#[wasm_bindgen(js_name = MintMethodSettings)] -pub struct JsMintMethodSettings { - inner: MintMethodSettings, -} - -impl Deref for JsMintMethodSettings { - type Target = MintMethodSettings; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsMintMethodSettings { - fn from(inner: MintMethodSettings) -> JsMintMethodSettings { - JsMintMethodSettings { inner } - } -} - -#[wasm_bindgen(js_name = Settings)] -pub struct JsSettings { - inner: Settings, -} - -impl Deref for JsSettings { - type Target = Settings; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsSettings { - fn from(inner: Settings) -> JsSettings { - JsSettings { inner } - } -} diff --git a/bindings/cdk-js/src/nuts/nut05.rs b/bindings/cdk-js/src/nuts/nut05.rs deleted file mode 100644 index 4767d4a7..00000000 --- a/bindings/cdk-js/src/nuts/nut05.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::ops::Deref; - -use cdk::nuts::{ - MeltBolt11Request, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteBolt11Response, - NUT05Settings, -}; -use wasm_bindgen::prelude::*; - -#[wasm_bindgen(js_name = MeltQuoteBolt11Request)] -pub struct JsMeltQuoteBolt11Request { - inner: MeltQuoteBolt11Request, -} - -impl Deref for JsMeltQuoteBolt11Request { - type Target = MeltQuoteBolt11Request; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsMeltQuoteBolt11Request { - fn from(inner: MeltQuoteBolt11Request) -> JsMeltQuoteBolt11Request { - JsMeltQuoteBolt11Request { inner } - } -} - -#[wasm_bindgen(js_name = MeltQuoteBolt11Response)] -pub struct JsMeltQuoteBolt11Response { - inner: MeltQuoteBolt11Response, -} - -impl Deref for JsMeltQuoteBolt11Response { - type Target = MeltQuoteBolt11Response; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsMeltQuoteBolt11Response { - fn from(inner: MeltQuoteBolt11Response) -> JsMeltQuoteBolt11Response { - JsMeltQuoteBolt11Response { inner } - } -} - -#[wasm_bindgen(js_name = MeltBolt11Request)] -pub struct JsMeltBolt11Request { - inner: MeltBolt11Request, -} - -impl Deref for JsMeltBolt11Request { - type Target = MeltBolt11Request; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsMeltBolt11Request { - fn from(inner: MeltBolt11Request) -> JsMeltBolt11Request { - JsMeltBolt11Request { inner } - } -} - -#[wasm_bindgen(js_name = MeltMethodSettings)] -pub struct JsMeltMethodSettings { - inner: MeltMethodSettings, -} - -impl Deref for JsMeltMethodSettings { - type Target = MeltMethodSettings; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsMeltMethodSettings { - fn from(inner: MeltMethodSettings) -> JsMeltMethodSettings { - JsMeltMethodSettings { inner } - } -} - -#[wasm_bindgen(js_name = Nut05Settings)] -pub struct JsSettings { - inner: NUT05Settings, -} - -impl Deref for JsSettings { - type Target = NUT05Settings; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsSettings { - fn from(inner: NUT05Settings) -> JsSettings { - JsSettings { inner } - } -} diff --git a/bindings/cdk-js/src/nuts/nut06.rs b/bindings/cdk-js/src/nuts/nut06.rs deleted file mode 100644 index dbbb1912..00000000 --- a/bindings/cdk-js/src/nuts/nut06.rs +++ /dev/null @@ -1,202 +0,0 @@ -use std::ops::Deref; - -use cdk::nuts::nut06::{MintInfo, MintVersion}; -use cdk::nuts::ContactInfo; -use wasm_bindgen::prelude::*; - -use super::nut01::JsPublicKey; -use crate::error::{into_err, Result}; - -#[wasm_bindgen(js_name = MintVersion)] -pub struct JsMintVersion { - inner: MintVersion, -} - -impl Deref for JsMintVersion { - type Target = MintVersion; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsMintVersion { - fn from(inner: MintVersion) -> JsMintVersion { - JsMintVersion { inner } - } -} - -#[wasm_bindgen(js_class = MintVersion)] -impl JsMintVersion { - #[wasm_bindgen(constructor)] - pub fn new(name: String, version: String) -> Result { - Ok(JsMintVersion { - inner: MintVersion { name, version }, - }) - } - - /// Get Name - #[wasm_bindgen(getter)] - pub fn name(&self) -> String { - self.inner.name.clone() - } - - /// Get Version - #[wasm_bindgen(getter)] - pub fn version(&self) -> String { - self.inner.version.clone() - } -} - -#[wasm_bindgen(js_name = MintInfo)] -pub struct JsMintInfo { - inner: MintInfo, -} - -impl Deref for JsMintInfo { - type Target = MintInfo; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsMintInfo { - fn from(inner: MintInfo) -> JsMintInfo { - JsMintInfo { inner } - } -} - -#[wasm_bindgen(js_class = MintInfo)] -impl JsMintInfo { - #[wasm_bindgen(constructor)] - #[allow(clippy::too_many_arguments)] - pub fn new( - name: Option, - pubkey: Option, - version: Option, - description: Option, - description_long: Option, - contact: Option>, - nuts: JsValue, - icon_url: Option, - motd: Option, - time: Option, - ) -> Result { - Ok(JsMintInfo { - inner: MintInfo { - name, - pubkey: pubkey.map(|p| *p.deref()), - version: version.map(|v| v.deref().clone()), - description, - description_long, - contact: contact - .map(|contacts| contacts.iter().map(|c| c.deref().clone()).collect()), - nuts: serde_wasm_bindgen::from_value(nuts).map_err(into_err)?, - icon_url, - motd, - time, - }, - }) - } - - /// Get Name - #[wasm_bindgen(getter)] - pub fn name(&self) -> Option { - self.inner.name.clone() - } - - /// Get Pubkey - #[wasm_bindgen(getter)] - pub fn pubkey(&self) -> Option { - self.inner.pubkey.map(|p| p.into()) - } - - /// Get Version - #[wasm_bindgen(getter)] - pub fn version(&self) -> Option { - self.inner.version.clone().map(|v| v.into()) - } - - /// Get description - #[wasm_bindgen(getter)] - pub fn description(&self) -> Option { - self.inner.description.clone() - } - - /// Get description long - #[wasm_bindgen(getter)] - pub fn description_long(&self) -> Option { - self.inner.description_long.clone() - } - - /// Get contact info - #[wasm_bindgen(getter)] - pub fn contact(&self) -> Option> { - self.inner - .contact - .clone() - .map(|c| c.into_iter().map(|c| c.into()).collect()) - } - - /// Get supported nuts - #[wasm_bindgen(getter)] - pub fn nuts(&self) -> Result { - serde_wasm_bindgen::to_value(&self.inner.nuts).map_err(into_err) - } - - /// Get mint icon url - #[wasm_bindgen(getter)] - pub fn icon_url(&self) -> Option { - self.inner.icon_url.clone() - } - - /// Get motd - #[wasm_bindgen(getter)] - pub fn motd(&self) -> Option { - self.inner.motd.clone() - } - - /// Get time - #[wasm_bindgen(getter)] - pub fn time(&self) -> Option { - self.inner.time - } -} - -#[wasm_bindgen(js_name = ContactInfo)] -pub struct JsContactInfo { - inner: ContactInfo, -} - -impl Deref for JsContactInfo { - type Target = ContactInfo; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsContactInfo { - fn from(inner: ContactInfo) -> JsContactInfo { - JsContactInfo { inner } - } -} - -#[wasm_bindgen(js_class = ContactInfo)] -impl JsContactInfo { - #[wasm_bindgen(constructor)] - pub fn new(method: String, info: String) -> Result { - Ok(JsContactInfo { - inner: ContactInfo { method, info }, - }) - } - /// Method - #[wasm_bindgen(getter)] - pub fn method(&self) -> String { - self.inner.method.clone() - } - - /// Info - #[wasm_bindgen(getter)] - pub fn info(&self) -> String { - self.inner.info.clone() - } -} diff --git a/bindings/cdk-js/src/nuts/nut07.rs b/bindings/cdk-js/src/nuts/nut07.rs deleted file mode 100644 index a5129f2d..00000000 --- a/bindings/cdk-js/src/nuts/nut07.rs +++ /dev/null @@ -1,88 +0,0 @@ -use std::ops::Deref; - -use cdk::nuts::{CheckStateRequest, CheckStateResponse, ProofState, State}; -use wasm_bindgen::prelude::*; - -#[wasm_bindgen(js_name = State)] -pub enum JsState { - Spent, - Unspent, - Pending, - Reserved, -} - -impl From for JsState { - fn from(inner: State) -> JsState { - match inner { - State::Spent => JsState::Spent, - State::Unspent => JsState::Unspent, - State::Pending => JsState::Pending, - State::Reserved => JsState::Reserved, - } - } -} - -impl From for State { - fn from(inner: JsState) -> State { - match inner { - JsState::Spent => State::Spent, - JsState::Unspent => State::Unspent, - JsState::Pending => State::Pending, - JsState::Reserved => State::Reserved, - } - } -} - -#[wasm_bindgen(js_name = CheckStateRequest)] -pub struct JsCheckStateRequest { - inner: CheckStateRequest, -} - -impl Deref for JsCheckStateRequest { - type Target = CheckStateRequest; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsCheckStateRequest { - fn from(inner: CheckStateRequest) -> JsCheckStateRequest { - JsCheckStateRequest { inner } - } -} - -#[wasm_bindgen(js_name = ProofState)] -pub struct JsProofState { - inner: ProofState, -} - -impl Deref for JsProofState { - type Target = ProofState; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsProofState { - fn from(inner: ProofState) -> JsProofState { - JsProofState { inner } - } -} - -#[wasm_bindgen(js_name = CheckStateResponse)] -pub struct JsCheckStateResponse { - inner: CheckStateResponse, -} - -impl Deref for JsCheckStateResponse { - type Target = CheckStateResponse; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsCheckStateResponse { - fn from(inner: CheckStateResponse) -> JsCheckStateResponse { - JsCheckStateResponse { inner } - } -} diff --git a/bindings/cdk-js/src/nuts/nut09.rs b/bindings/cdk-js/src/nuts/nut09.rs deleted file mode 100644 index ead4690b..00000000 --- a/bindings/cdk-js/src/nuts/nut09.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::ops::Deref; - -use cdk::nuts::{RestoreRequest, RestoreResponse}; -use wasm_bindgen::prelude::*; - -#[wasm_bindgen(js_name = RestoreRequest)] -pub struct JsRestoreRequest { - inner: RestoreRequest, -} - -impl Deref for JsRestoreRequest { - type Target = RestoreRequest; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsRestoreRequest { - fn from(inner: RestoreRequest) -> JsRestoreRequest { - JsRestoreRequest { inner } - } -} - -#[wasm_bindgen(js_name = RestoreResponse)] -pub struct JsRestoreResponse { - inner: RestoreResponse, -} - -impl Deref for JsRestoreResponse { - type Target = RestoreResponse; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsRestoreResponse { - fn from(inner: RestoreResponse) -> JsRestoreResponse { - JsRestoreResponse { inner } - } -} diff --git a/bindings/cdk-js/src/nuts/nut10.rs b/bindings/cdk-js/src/nuts/nut10.rs deleted file mode 100644 index 06f99aba..00000000 --- a/bindings/cdk-js/src/nuts/nut10.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::ops::Deref; - -use cdk::nuts::{Kind, Nut10Secret, SecretData}; -use wasm_bindgen::prelude::*; - -#[wasm_bindgen(js_name = Kind)] -pub enum JsKind { - P2PK, - HTLC, -} - -impl From for JsKind { - fn from(inner: Kind) -> JsKind { - match inner { - Kind::P2PK => JsKind::P2PK, - Kind::HTLC => JsKind::HTLC, - } - } -} - -impl From for Kind { - fn from(inner: JsKind) -> Kind { - match inner { - JsKind::P2PK => Kind::P2PK, - JsKind::HTLC => Kind::HTLC, - } - } -} - -#[wasm_bindgen(js_name = SecretData)] -pub struct JsSecretData { - inner: SecretData, -} - -impl Deref for JsSecretData { - type Target = SecretData; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsSecretData { - fn from(inner: SecretData) -> JsSecretData { - JsSecretData { inner } - } -} - -#[wasm_bindgen(js_name = Nut10Secret)] -pub struct JsNut10Secret { - inner: Nut10Secret, -} - -impl Deref for JsNut10Secret { - type Target = Nut10Secret; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsNut10Secret { - fn from(inner: Nut10Secret) -> JsNut10Secret { - JsNut10Secret { inner } - } -} diff --git a/bindings/cdk-js/src/nuts/nut11.rs b/bindings/cdk-js/src/nuts/nut11.rs deleted file mode 100644 index ca2cd3df..00000000 --- a/bindings/cdk-js/src/nuts/nut11.rs +++ /dev/null @@ -1,120 +0,0 @@ -use std::ops::Deref; -use std::str::FromStr; - -use cdk::nuts::{Conditions, P2PKWitness, PublicKey, SigFlag, SpendingConditions}; -use wasm_bindgen::prelude::*; - -use crate::error::{into_err, Result}; - -#[wasm_bindgen(js_name = P2PKWitness)] -pub struct JsP2PKWitness { - inner: P2PKWitness, -} - -impl Deref for JsP2PKWitness { - type Target = P2PKWitness; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsP2PKWitness { - fn from(inner: P2PKWitness) -> JsP2PKWitness { - JsP2PKWitness { inner } - } -} - -#[wasm_bindgen(js_name = P2PKSpendingConditions)] -pub struct JsP2PKSpendingConditions { - inner: SpendingConditions, -} - -impl Deref for JsP2PKSpendingConditions { - type Target = SpendingConditions; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -#[wasm_bindgen(js_class = P2PKSpendingConditions)] -impl JsP2PKSpendingConditions { - #[wasm_bindgen(constructor)] - pub fn new( - pubkey: String, - conditions: Option, - ) -> Result { - let pubkey = PublicKey::from_str(&pubkey).map_err(into_err)?; - Ok(Self { - inner: SpendingConditions::new_p2pk(pubkey, conditions.map(|c| c.deref().clone())), - }) - } -} - -#[wasm_bindgen(js_name = Conditions)] -pub struct JsConditions { - inner: Conditions, -} - -impl Deref for JsConditions { - type Target = Conditions; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsConditions { - fn from(inner: Conditions) -> JsConditions { - JsConditions { inner } - } -} - -#[wasm_bindgen(js_class = Conditions)] -impl JsConditions { - #[wasm_bindgen(constructor)] - pub fn new( - locktime: Option, - pubkeys: JsValue, - refund_key: JsValue, - num_sigs: Option, - sig_flag: String, - ) -> Result { - let pubkeys: Result, _> = serde_wasm_bindgen::from_value(pubkeys); - let refund_key: Result, _> = serde_wasm_bindgen::from_value(refund_key); - - Ok(Self { - inner: Conditions::new( - locktime, - pubkeys.ok(), - refund_key.ok(), - num_sigs, - Some(SigFlag::from_str(&sig_flag).unwrap_or_default()), - ) - .map_err(into_err)?, - }) - } - - #[wasm_bindgen(getter)] - pub fn locktime(&self) -> Option { - self.inner.locktime - } - - #[wasm_bindgen(getter)] - pub fn pubkeys(&self) -> Result { - Ok(serde_wasm_bindgen::to_value(&self.inner.pubkeys)?) - } - - #[wasm_bindgen(getter)] - pub fn refund_keys(&self) -> Result { - Ok(serde_wasm_bindgen::to_value(&self.inner.refund_keys)?) - } - - #[wasm_bindgen(getter)] - pub fn num_sigs(&self) -> Option { - self.inner.num_sigs - } - - #[wasm_bindgen(getter)] - pub fn sig_flag(&self) -> String { - self.inner.sig_flag.to_string() - } -} diff --git a/bindings/cdk-js/src/nuts/nut12.rs b/bindings/cdk-js/src/nuts/nut12.rs deleted file mode 100644 index 22961437..00000000 --- a/bindings/cdk-js/src/nuts/nut12.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::ops::Deref; - -use cdk::nuts::{BlindSignatureDleq, ProofDleq}; -use wasm_bindgen::prelude::*; - -use crate::nuts::JsSecretKey; - -#[wasm_bindgen(js_name = BlindSignatureDleq)] -pub struct JsBlindSignatureDleq { - inner: BlindSignatureDleq, -} - -#[wasm_bindgen(js_class = BlindedSignatureDleq)] -impl JsBlindSignatureDleq { - #[wasm_bindgen(constructor)] - pub fn new(e: JsSecretKey, s: JsSecretKey) -> Self { - Self { - inner: BlindSignatureDleq { - e: e.deref().clone(), - s: s.deref().clone(), - }, - } - } - - /// e - #[wasm_bindgen(getter)] - pub fn e(&self) -> JsSecretKey { - self.inner.e.clone().into() - } - - /// s - #[wasm_bindgen(getter)] - pub fn s(&self) -> JsSecretKey { - self.inner.s.clone().into() - } -} - -impl Deref for JsBlindSignatureDleq { - type Target = BlindSignatureDleq; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsBlindSignatureDleq { - fn from(inner: BlindSignatureDleq) -> JsBlindSignatureDleq { - JsBlindSignatureDleq { inner } - } -} - -#[wasm_bindgen(js_name = ProofDleq)] -pub struct JsProofDleq { - inner: ProofDleq, -} - -#[wasm_bindgen(js_class = ProofDleq)] -impl JsProofDleq { - #[wasm_bindgen(constructor)] - pub fn new(e: JsSecretKey, s: JsSecretKey, r: JsSecretKey) -> Self { - Self { - inner: ProofDleq { - e: e.deref().clone(), - s: s.deref().clone(), - r: r.deref().clone(), - }, - } - } - - /// e - #[wasm_bindgen(getter)] - pub fn e(&self) -> JsSecretKey { - self.inner.e.clone().into() - } - - /// s - #[wasm_bindgen(getter)] - pub fn s(&self) -> JsSecretKey { - self.inner.s.clone().into() - } - - /// r - #[wasm_bindgen(getter)] - pub fn r(&self) -> JsSecretKey { - self.inner.r.clone().into() - } -} - -impl Deref for JsProofDleq { - type Target = ProofDleq; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsProofDleq { - fn from(inner: ProofDleq) -> JsProofDleq { - JsProofDleq { inner } - } -} diff --git a/bindings/cdk-js/src/nuts/nut14.rs b/bindings/cdk-js/src/nuts/nut14.rs deleted file mode 100644 index 7ff17b47..00000000 --- a/bindings/cdk-js/src/nuts/nut14.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::ops::Deref; - -use cdk::nuts::{HTLCWitness, SpendingConditions}; -use wasm_bindgen::prelude::*; - -use super::JsConditions; -use crate::error::{into_err, Result}; - -#[wasm_bindgen(js_name = HTLCWitness)] -pub struct JsHTLCWitness { - inner: HTLCWitness, -} - -impl Deref for JsHTLCWitness { - type Target = HTLCWitness; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsHTLCWitness { - fn from(inner: HTLCWitness) -> JsHTLCWitness { - JsHTLCWitness { inner } - } -} - -#[wasm_bindgen(js_name = HTLCSpendingConditions)] -pub struct JsHTLCSpendingConditions { - inner: SpendingConditions, -} - -impl Deref for JsHTLCSpendingConditions { - type Target = SpendingConditions; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -#[wasm_bindgen(js_class = HTLCSpendingConditions)] -impl JsHTLCSpendingConditions { - #[wasm_bindgen(constructor)] - pub fn new( - preimage: String, - conditions: Option, - ) -> Result { - Ok(Self { - inner: SpendingConditions::new_htlc(preimage, conditions.map(|c| c.deref().clone())) - .map_err(into_err)?, - }) - } -} diff --git a/bindings/cdk-js/src/types/amount.rs b/bindings/cdk-js/src/types/amount.rs deleted file mode 100644 index 3a7e17d0..00000000 --- a/bindings/cdk-js/src/types/amount.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::ops::Deref; - -use cdk::Amount; -use wasm_bindgen::prelude::*; - -use crate::error::{into_err, Result}; - -#[wasm_bindgen(js_name = Amount)] -pub struct JsAmount { - inner: Amount, -} - -impl Deref for JsAmount { - type Target = Amount; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsAmount { - fn from(inner: Amount) -> JsAmount { - JsAmount { inner } - } -} - -impl From for JsAmount { - fn from(amount: u64) -> JsAmount { - JsAmount { - inner: Amount::from(amount), - } - } -} - -#[wasm_bindgen(js_class = Amount)] -impl JsAmount { - #[wasm_bindgen(constructor)] - pub fn new(sats: u64) -> Self { - Self { - inner: Amount::from(sats), - } - } - - /// Split amount returns sat vec of sats - #[wasm_bindgen(js_name = split)] - pub fn split(&self) -> Result { - serde_wasm_bindgen::to_value(&self.inner.split()).map_err(into_err) - } - - #[wasm_bindgen(getter)] - pub fn value(&self) -> u64 { - self.inner.into() - } -} diff --git a/bindings/cdk-js/src/types/melt_quote.rs b/bindings/cdk-js/src/types/melt_quote.rs deleted file mode 100644 index d6e6f325..00000000 --- a/bindings/cdk-js/src/types/melt_quote.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::ops::Deref; - -use cdk::wallet::types::MeltQuote; -use wasm_bindgen::prelude::*; - -use crate::nuts::JsCurrencyUnit; -use crate::types::JsAmount; - -#[wasm_bindgen(js_name = MeltQuote)] -pub struct JsMeltQuote { - inner: MeltQuote, -} - -impl Deref for JsMeltQuote { - type Target = MeltQuote; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsMeltQuote { - fn from(inner: MeltQuote) -> JsMeltQuote { - JsMeltQuote { inner } - } -} - -#[wasm_bindgen(js_class = MeltQuote)] -impl JsMeltQuote { - #[wasm_bindgen(getter)] - pub fn id(&self) -> String { - self.inner.id.clone() - } - - #[wasm_bindgen(getter)] - pub fn unit(&self) -> JsCurrencyUnit { - self.inner.unit.into() - } - - #[wasm_bindgen(getter)] - pub fn amount(&self) -> JsAmount { - self.inner.amount.into() - } - - #[wasm_bindgen(getter)] - pub fn request(&self) -> String { - self.inner.request.clone() - } - - #[wasm_bindgen(getter)] - pub fn fee_reserve(&self) -> JsAmount { - self.inner.fee_reserve.into() - } - - #[wasm_bindgen(getter)] - pub fn state(&self) -> String { - self.inner.state.to_string() - } - - #[wasm_bindgen(getter)] - pub fn expiry(&self) -> u64 { - self.inner.expiry - } -} diff --git a/bindings/cdk-js/src/types/melted.rs b/bindings/cdk-js/src/types/melted.rs deleted file mode 100644 index 2cbccb57..00000000 --- a/bindings/cdk-js/src/types/melted.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::ops::Deref; - -use cdk::types::Melted; -use wasm_bindgen::prelude::*; - -use crate::error::Result; - -#[wasm_bindgen(js_name = Melted)] -pub struct JsMelted { - inner: Melted, -} - -impl Deref for JsMelted { - type Target = Melted; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsMelted { - fn from(inner: Melted) -> JsMelted { - JsMelted { inner } - } -} - -#[wasm_bindgen(js_class = Melted)] -impl JsMelted { - #[wasm_bindgen(getter)] - pub fn paid(&self) -> String { - self.inner.state.to_string() - } - - #[wasm_bindgen(getter)] - pub fn preimage(&self) -> Option { - self.inner.preimage.clone() - } - - #[wasm_bindgen(getter)] - pub fn change(&self) -> Result { - Ok(serde_wasm_bindgen::to_value(&self.inner.change)?) - } -} diff --git a/bindings/cdk-js/src/types/mint_quote.rs b/bindings/cdk-js/src/types/mint_quote.rs deleted file mode 100644 index fed36a92..00000000 --- a/bindings/cdk-js/src/types/mint_quote.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::ops::Deref; - -use cdk::wallet::MintQuote; -use wasm_bindgen::prelude::*; - -use crate::nuts::JsCurrencyUnit; -use crate::types::JsAmount; - -#[wasm_bindgen(js_name = MintQuote)] -pub struct JsMintQuote { - inner: MintQuote, -} - -impl Deref for JsMintQuote { - type Target = MintQuote; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsMintQuote { - fn from(inner: MintQuote) -> JsMintQuote { - JsMintQuote { inner } - } -} - -#[wasm_bindgen(js_class = MintQuote)] -impl JsMintQuote { - #[wasm_bindgen(getter)] - pub fn id(&self) -> String { - self.inner.id.clone() - } - - #[wasm_bindgen(getter)] - pub fn unit(&self) -> JsCurrencyUnit { - self.inner.unit.into() - } - - #[wasm_bindgen(getter)] - pub fn amount(&self) -> JsAmount { - self.inner.amount.into() - } - - #[wasm_bindgen(getter)] - pub fn request(&self) -> String { - self.inner.request.clone() - } - - #[wasm_bindgen(getter)] - pub fn state(&self) -> String { - self.inner.state.to_string() - } - - #[wasm_bindgen(getter)] - pub fn expiry(&self) -> u64 { - self.inner.expiry - } -} diff --git a/bindings/cdk-js/src/types/mod.rs b/bindings/cdk-js/src/types/mod.rs deleted file mode 100644 index e27ce7ea..00000000 --- a/bindings/cdk-js/src/types/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod amount; -pub mod melt_quote; -pub mod melted; -pub mod mint_quote; -pub mod secret; - -pub use amount::JsAmount; -pub use melt_quote::JsMeltQuote; -pub use melted::JsMelted; -pub use mint_quote::JsMintQuote; -pub use secret::JsSecret; diff --git a/bindings/cdk-js/src/types/secret.rs b/bindings/cdk-js/src/types/secret.rs deleted file mode 100644 index 683a6578..00000000 --- a/bindings/cdk-js/src/types/secret.rs +++ /dev/null @@ -1,44 +0,0 @@ -use std::ops::Deref; - -use cdk::secret::Secret; -use wasm_bindgen::prelude::*; - -#[wasm_bindgen(js_name = Secret)] -pub struct JsSecret { - inner: Secret, -} - -impl Default for JsSecret { - fn default() -> Self { - Self::new() - } -} - -impl Deref for JsSecret { - type Target = Secret; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsSecret { - fn from(inner: Secret) -> JsSecret { - JsSecret { inner } - } -} - -#[wasm_bindgen(js_class = Secret)] -impl JsSecret { - #[wasm_bindgen(constructor)] - pub fn new() -> JsSecret { - Self { - inner: Secret::generate(), - } - } - - /// As Bytes - #[wasm_bindgen(js_name = asBytes)] - pub fn as_bytes(&self) -> Vec { - self.inner.as_bytes().to_vec() - } -} diff --git a/bindings/cdk-js/src/wallet.rs b/bindings/cdk-js/src/wallet.rs deleted file mode 100644 index 8f4cf405..00000000 --- a/bindings/cdk-js/src/wallet.rs +++ /dev/null @@ -1,285 +0,0 @@ -//! Wallet Js Bindings - -use std::ops::Deref; -use std::sync::Arc; - -use cdk::amount::SplitTarget; -use cdk::nuts::{Proofs, SecretKey}; -use cdk::wallet::{SendKind, Wallet}; -use cdk::Amount; -use cdk_rexie::WalletRexieDatabase; -use wasm_bindgen::prelude::*; - -use crate::error::{into_err, Result}; -use crate::nuts::nut01::JsSecretKey; -use crate::nuts::nut04::JsMintQuoteBolt11Response; -use crate::nuts::nut05::JsMeltQuoteBolt11Response; -use crate::nuts::nut11::JsP2PKSpendingConditions; -use crate::nuts::nut14::JsHTLCSpendingConditions; -use crate::nuts::{JsCurrencyUnit, JsMintInfo, JsProof}; -use crate::types::melt_quote::JsMeltQuote; -use crate::types::{JsAmount, JsMelted, JsMintQuote}; - -#[wasm_bindgen(js_name = Wallet)] -pub struct JsWallet { - inner: Wallet, -} - -impl Deref for JsWallet { - type Target = Wallet; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for JsWallet { - fn from(inner: Wallet) -> JsWallet { - JsWallet { inner } - } -} - -#[wasm_bindgen(js_class = Wallet)] -impl JsWallet { - #[wasm_bindgen(constructor)] - pub async fn new(mints_url: String, unit: JsCurrencyUnit, seed: Vec) -> Result { - let db = WalletRexieDatabase::new().await.unwrap(); - - Ok( - Wallet::new(&mints_url, unit.into(), Arc::new(db), &seed, None) - .map_err(into_err)? - .into(), - ) - } - - #[wasm_bindgen(js_name = totalBalance)] - pub async fn total_balance(&self) -> Result { - Ok(serde_wasm_bindgen::to_value( - &self.inner.total_balance().await.map_err(into_err)?, - )?) - } - - #[wasm_bindgen(js_name = totalPendingBalance)] - pub async fn total_pending_balance(&self) -> Result { - Ok(serde_wasm_bindgen::to_value( - &self.inner.total_pending_balance().await.map_err(into_err)?, - )?) - } - - #[wasm_bindgen(js_name = checkAllPendingProofs)] - pub async fn check_all_pending_proofs(&self) -> Result { - Ok(self - .inner - .check_all_pending_proofs() - .await - .map_err(into_err)? - .into()) - } - - #[wasm_bindgen(js_name = getMintInfo)] - pub async fn get_mint_info(&self) -> Result> { - Ok(self - .inner - .get_mint_info() - .await - .map_err(into_err)? - .map(|i| i.into())) - } - - #[wasm_bindgen(js_name = mintQuote)] - pub async fn mint_quote( - &mut self, - amount: u64, - description: Option, - ) -> Result { - let quote = self - .inner - .mint_quote(amount.into(), description) - .await - .map_err(into_err)?; - - Ok(quote.into()) - } - - #[wasm_bindgen(js_name = mintQuoteStatus)] - pub async fn mint_quote_status(&self, quote_id: String) -> Result { - let quote = self - .inner - .mint_quote_state("e_id) - .await - .map_err(into_err)?; - - Ok(quote.into()) - } - - #[wasm_bindgen(js_name = checkAllMintQuotes)] - pub async fn check_all_mint_quotes(&self) -> Result { - let amount = self.inner.check_all_mint_quotes().await.map_err(into_err)?; - - Ok(amount.into()) - } - - #[wasm_bindgen(js_name = mint)] - pub async fn mint( - &mut self, - quote_id: String, - p2pk_condition: Option, - htlc_condition: Option, - split_target_amount: Option, - ) -> Result { - let target = split_target_amount - .map(|a| SplitTarget::Value(*a.deref())) - .unwrap_or_default(); - let conditions = match (p2pk_condition, htlc_condition) { - (Some(_), Some(_)) => { - return Err(JsValue::from_str( - "Cannot define both p2pk and htlc conditions", - )); - } - (None, Some(htlc_condition)) => Some(htlc_condition.deref().clone()), - (Some(p2pk_condition), None) => Some(p2pk_condition.deref().clone()), - (None, None) => None, - }; - - Ok(self - .inner - .mint("e_id, target, conditions) - .await - .map_err(into_err)? - .into()) - } - - #[wasm_bindgen(js_name = meltQuote)] - pub async fn melt_quote( - &mut self, - request: String, - mpp_amount: Option, - ) -> Result { - let melt_quote = self - .inner - .melt_quote(request, mpp_amount.map(|a| *a.deref())) - .await - .map_err(into_err)?; - - Ok(melt_quote.into()) - } - - #[wasm_bindgen(js_name = meltQuoteStatus)] - pub async fn melt_quote_status(&self, quote_id: String) -> Result { - let quote = self - .inner - .melt_quote_status("e_id) - .await - .map_err(into_err)?; - - Ok(quote.into()) - } - - #[wasm_bindgen(js_name = melt)] - pub async fn melt(&mut self, quote_id: String) -> Result { - let melted = self.inner.melt("e_id).await.map_err(into_err)?; - - Ok(melted.into()) - } - - #[wasm_bindgen(js_name = receive)] - pub async fn receive( - &mut self, - encoded_token: String, - signing_keys: Vec, - preimages: Vec, - ) -> Result { - let signing_keys: Vec = signing_keys.iter().map(|s| s.deref().clone()).collect(); - - Ok(self - .inner - .receive( - &encoded_token, - SplitTarget::default(), - &signing_keys, - &preimages, - ) - .await - .map_err(into_err)? - .into()) - } - - #[allow(clippy::too_many_arguments)] - #[wasm_bindgen(js_name = send)] - pub async fn send( - &mut self, - memo: Option, - amount: u64, - p2pk_condition: Option, - htlc_condition: Option, - split_target_amount: Option, - ) -> Result { - let conditions = match (p2pk_condition, htlc_condition) { - (Some(_), Some(_)) => { - return Err(JsValue::from_str( - "Cannot define both p2pk and htlc conditions", - )); - } - (None, Some(htlc_condition)) => Some(htlc_condition.deref().clone()), - (Some(p2pk_condition), None) => Some(p2pk_condition.deref().clone()), - (None, None) => None, - }; - - let target = split_target_amount - .map(|a| SplitTarget::Value(*a.deref())) - .unwrap_or_default(); - Ok(self - .inner - .send( - Amount::from(amount), - memo, - conditions, - &target, - &SendKind::default(), - false, - ) - .await - .map_err(into_err)? - .to_string()) - } - - #[allow(clippy::too_many_arguments)] - #[wasm_bindgen(js_name = swap)] - pub async fn swap( - &mut self, - amount: u64, - input_proofs: Vec, - p2pk_condition: Option, - htlc_condition: Option, - split_target_amount: Option, - ) -> Result { - let conditions = match (p2pk_condition, htlc_condition) { - (Some(_), Some(_)) => { - return Err(JsValue::from_str( - "Cannot define both p2pk and htlc conditions", - )); - } - (None, Some(htlc_condition)) => Some(htlc_condition.deref().clone()), - (Some(p2pk_condition), None) => Some(p2pk_condition.deref().clone()), - (None, None) => None, - }; - - let proofs: Proofs = input_proofs.iter().map(|p| p.deref()).cloned().collect(); - - let target = split_target_amount - .map(|a| SplitTarget::Value(*a.deref())) - .unwrap_or_default(); - let post_swap_proofs = self - .inner - .swap( - Some(Amount::from(amount)), - target, - proofs, - conditions, - false, - ) - .await - .map_err(into_err)?; - - Ok(serde_wasm_bindgen::to_value(&post_swap_proofs)?) - } -} diff --git a/context.md b/context.md new file mode 100644 index 00000000..a764fec0 --- /dev/null +++ b/context.md @@ -0,0 +1,159 @@ +`work in progress` + +# Context for the Proof of Concept eTIMEbank + + +#### Contents: +- Introduction +- What is time banking? +- Addressing with the 'how do you value one hour' question +- An experiment in new forms of collectively ascibing 'value' +- Why not just use Bitcoin? +- Relevant features of Chaumian ecash for time banking +- Use-case scenarios +- Design choices for project +- FAQ + + +## Introduction + + +Concept is the creation of a FOSS ecash based time-banking application that runs both the wallet client and mint logic for offline and offgrid communities. + +This project by design not include any Bitcoin elements and does not use satoshis as a unit, it is an exercise in applying a Chaumian blinded signatures scheme to the concept of Time Banking + +To know more about the design choices, what time banking is, and the reason why I made this project please refer to the context.md document within this repository + + + +## What is time banking? + +[Wikipedia entry introduction:](https://en.wikipedia.org/wiki/Time_banking) + +``` +"In economics, a time-based currency is an alternative currency or exchange system where the unit of account is the person-hour or some other time unit. + +Some time-based currencies value everyone's contributions equally: one hour equals one service credit. In these systems, one person volunteers to work for an hour for another person; thus, they are credited with one hour, which they can redeem for an hour of service from another volunteer. + +Others use time units that might be fractions of an hour (e.g. minutes, ten minutes – 6 units/hour, or 15 minutes – 4 units/hour). While most time-based exchange systems are service exchanges in that most exchange involves the provision of services that can be measured in a time unit, it is also possible to exchange goods by 'pricing' them in terms of the average national hourly wage rate (e.g. if the average hourly rate is $20/hour, then a commodity valued at $20 in the national currency would be equivalent to 1 hour). " +``` + +Time Banking is an alternative currency system with a long history and [active communities that operate with these systems in 2024](https://en.wikipedia.org/wiki/Time-based_currency#Studies_and_examples), it is an alternative to the fiat banking system which uses a unit of account this abstracted from meatspace/reality. + +Similarly to Bitcoin, Time Banking exists in contrasts to the current social consensus that 'time is money' and that a service or product (both of which can be calculated in person-hours) are attributed a numismatic 'value' to it which can be exchange/traded with another party for other services or products, creating an 'economy' + +Time Banking presents a unique mental and social model for what 'economy' is an reframes the 'numismatic value' of the time that a person contributes to their local community/society/economy - 'time is money, so let us collaborate with minutes and not fiat' + +There has been a plethora of research and books that have been written on the concept of Time Banking, this section is just a small introduction and primer for the understanding of why I chose Time Banking as a alternative type of economy to apply the Cashu protocol and technology within this proof of concept. + +For a more in-depth explanation on 'how it works in practice' please see the following webpages: +- [https://timebanking.org/howitworks/](https://timebanking.org/howitworks/) +- [https://timebanks.org/start-a-timebank](https://timebanks.org/start-a-timebank) +- [Video: Time Banking animation](https://www.youtube.com/watch?v=aB8ifVJ34JU) +- [TEDxDouglas: How timebanking can help rebuilding community spirit - Valerie Miller](https://www.youtube.com/watch?v=VRHvoYas82g) +- [TEDxStPeterPort: Timebanking in the UK: It's About Time - Sarah Bird](https://www.youtube.com/watch?v=k0Flh6cuuWs) + +**Time Banking Educational Resources** +- [Time Banking wiki](https://wikipedia.org/wiki/Time_banking) +- [Numismatics wiki](https://en.wikipedia.org/wiki/Numismatics) +- [Alternative currency wiki](https://en.wikipedia.org/wiki/Alternative_currency) + +**Examples of Time Banks** +- [Custom GoogleMap with Pins for all Time Banking ommunities globally](https://www.google.com/maps/d/viewer?mid=1ZZRA7ombZ7CN_8u8gHIi0wRxq45FaFWs&ll=23.581971987838646%2C2.24820156946123&z=2) +- [Tempo Time Credits](https://wearetempo.org/) +- [Time Banking UK](https://timebanking.org/overview/) +- [bespoke time banking software by Time Banking UK](https://timebanking.org/software/) +- [Time Banking USA](https://timebanks.org/) + +**Other Sources** +- [Huffpost](https://www.huffpost.com/entry/bringing-people-together_b_8916374) +- [Vice Portugese - O banco que quer seu tempo, não seu dinheiro](https://www.vice.com/pt/article/o-banco-que-quer-seu-tempo-nao-seu-dinheiro/) +- [Book: Give and Take: How timebanking is transforming healthcare](https://books.google.com/books?id=LIiSBQAAQBAJ) +- [Timebanking (CCIA – 2015)](https://monneta.org/en/timebanking-ccia-2015/) + +**Academic papers** +- [Introduction to time banking and Time Credits, 2016](https://www.researchgate.net/publication/297696050_Introduction_to_time_banking_and_Time_Credits) +- [Participação em bancos de tempo: utilizando dados sobre transações para avaliar o Banco de Tempo - Florianópolis](https://www.apec.org.br/rce/index.php/rce/article/view/16) +- [Banco de Tempo-Florianópolis: análise das características socioeconômicas de seus membros](https://ojsrevista.furb.br/ojs/index.php/rbdr/article/view/6937) +- [Time banks: rewarding community self-help in the inner city?](https://academic.oup.com/cdj/article-abstract/39/1/62/268434) + + + +## Addressing with the 'how do you value one hour' question + +This is a fundamental question of any economic system and especially of relevance to Time Banking ones, which have been discussed at length within books and discussions within alternative currency and economics fields. This is outside the scope of this current proof of concept or this context document. + +A standard presentation of this issue is how can 1 hour for a skilled Doctor with all the tools versus 1 hour of a gardener cutting the grass? + +This is a social consensus problem and about how the members of a social group decide to operate within *their own sovereign Time Bank* and not something that will be solved with a technological solution such as an ecash wallet and mint. + + + +## An experiment in new forms of collectively ascibing 'value' + +What I will add that this is a question on whether any economic system needs to have an abstact numismatic value attributed to a real world action that is the time a person invests/spends/uses in helping another person, creating a good or service useful in the local economy, volunteer in public goods within society, pay their fine/punishment for sanctioned behaviour within the community. + + +## Why not just use Bitcoin? + +This proof of concept is applying the ecash model to minutes, hours, days - what can be called 'person-hours' to create a blind signature system to respesent their economic interactions as 'etime' + +This 'etime' is not meant to be backed by satoshis/Bitcoin within the mint - that is out of scope, but a system could leverage both and have a satoshi 'value' for the 'etime' within an economic system (Citadel public goods idea) + +Time Banking systems do not require a digital collateral to back the etime, what is at stake is your reputation and power within a closed community group that will punish bad actors and incentivise public goods. + + + + + + +## Relevant features of Chaumian ecash for time banking + +- Offline blind signature exchange +- Feeless blind signature exchange +- Creation of any denomination (melt/mint) +- Low-resource requirement +- self-hostable +- low/no-latency contexts +- asyncronhous mint interaction +- programmable auditability (not just Proof of Liabilities) +- cdk / MIT license +- written in Rust + +## Use-case scenarios + +From my brief research into current Time Banks there upwards of 5 active communities that could implement this today, but the core use-case is for new Time Banking systems starting in remote or off-grid communities that could benefit from the features of ecash within their economic model. + +Ideal community for use-case: coordinated groups of 50 - 200 people living within a 15 km area that meet in person, share resources, assist each other in time of need and have a shared values including the wish to not have a fiat/numismatic economy between members. + +The community would have to seek social consensus on what the 'mint' is, what the unit 'etime' is denominated in (minutes, hours, days), how they would resolve disputes and issues, + +In terms of technology stack, all the members of this community would have and know how to use a stock android smart phone, for the mint it could be a mesh network, a wifi router to a rasperbby pi, a bluetooth gossip protocol. Up to the community, they might have their own intranet or wish to implement a multi-mint model for their etime system where each client is also its own independent mint (more complication and out of scope here) + + + +## Design choices for project + +- 100% offline use (no http/API) +- should run on smartphones made after 2010 (ideally a PWA) +- access-control to the mint (not open to anyone) +- etime notes represent minutes of real time (time counter mechanism) +- no 'withdrawl' or 'deposit' logic (no exchange of etime for other assets) +- ability to cryptographically query the mint for unique audits (more transparency with verifiability) + + +## FAQ + +Q: + +A: + + +Q: + +A: + + +Q: + +A: diff --git a/crates/cdk-cli/Cargo.toml b/crates/cdk-cli/Cargo.toml index bd3b26b0..7372f6b6 100644 --- a/crates/cdk-cli/Cargo.toml +++ b/crates/cdk-cli/Cargo.toml @@ -16,7 +16,6 @@ anyhow = "1" bip39 = "2.0" cdk = { path = "../cdk", version = "0.4.0", default-features = false, features = ["wallet"]} cdk-redb = { path = "../cdk-redb", version = "0.4.0", default-features = false, features = ["wallet"] } -cdk-sqlite = { path = "../cdk-sqlite", version = "0.4.0", default-features = false, features = ["wallet"] } clap = { version = "4.4.8", features = ["derive", "env", "default"] } serde = { version = "1", default-features = false, features = ["derive"] } serde_json = "1" @@ -25,8 +24,4 @@ tracing = { version = "0.1", default-features = false, features = ["attributes", tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } rand = "0.8.5" home = "0.5.5" -nostr-sdk = { version = "0.33.0", default-features = false, features = [ - "nip04", - "nip44" -]} url = "2.3" diff --git a/crates/cdk-cli/src/main.rs b/crates/cdk-cli/src/main.rs index 2da00dc8..3ce842d5 100644 --- a/crates/cdk-cli/src/main.rs +++ b/crates/cdk-cli/src/main.rs @@ -3,14 +3,12 @@ use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; -use anyhow::{bail, Result}; +use anyhow::Result; use bip39::Mnemonic; -use cdk::cdk_database; use cdk::cdk_database::WalletDatabase; use cdk::wallet::client::HttpClient; use cdk::wallet::{MultiMintWallet, Wallet}; use cdk_redb::WalletRedbDatabase; -use cdk_sqlite::WalletSqliteDatabase; use clap::{Parser, Subcommand}; use rand::Rng; use tracing::Level; @@ -28,9 +26,6 @@ const DEFAULT_WORK_DIR: &str = ".cdk-cli"; #[command(version = "0.1.0")] #[command(author, version, about, long_about = None)] struct Cli { - /// Database engine to use (sqlite/redb) - #[arg(short, long, default_value = "sqlite")] - engine: String, /// Path to working dir #[arg(short, long)] work_dir: Option, @@ -94,23 +89,8 @@ async fn main() -> Result<()> { fs::create_dir_all(&work_dir)?; - let localstore: Arc + Send + Sync> = - match args.engine.as_str() { - "sqlite" => { - let sql_path = work_dir.join("cdk-cli.sqlite"); - let sql = WalletSqliteDatabase::new(&sql_path).await?; - - sql.migrate().await; - - Arc::new(sql) - } - "redb" => { - let redb_path = work_dir.join("cdk-cli.redb"); - - Arc::new(WalletRedbDatabase::new(&redb_path)?) - } - _ => bail!("Unknown DB engine"), - }; + let redb_path = work_dir.join("cdk-cli.redb"); + let localstore = Arc::new(WalletRedbDatabase::new(&redb_path)?); let seed_path = work_dir.join("seed"); diff --git a/crates/cdk-cli/src/sub_commands/receive.rs b/crates/cdk-cli/src/sub_commands/receive.rs index 24eea39b..6594bd40 100644 --- a/crates/cdk-cli/src/sub_commands/receive.rs +++ b/crates/cdk-cli/src/sub_commands/receive.rs @@ -1,17 +1,13 @@ -use std::collections::HashSet; use std::str::FromStr; use std::sync::Arc; -use anyhow::{anyhow, Result}; +use anyhow::Result; use cdk::cdk_database::{self, WalletDatabase}; use cdk::nuts::{SecretKey, Token}; -use cdk::util::unix_time; use cdk::wallet::multi_mint_wallet::{MultiMintWallet, WalletKey}; use cdk::wallet::Wallet; use cdk::Amount; use clap::Args; -use nostr_sdk::nips::nip04; -use nostr_sdk::{Filter, Keys, Kind, Timestamp}; #[derive(Args)] pub struct ReceiveSubCommand { @@ -20,18 +16,6 @@ pub struct ReceiveSubCommand { /// Signing Key #[arg(short, long, action = clap::ArgAction::Append)] signing_key: Vec, - /// Nostr key - #[arg(short, long)] - nostr_key: Option, - /// Nostr relay - #[arg(short, long, action = clap::ArgAction::Append)] - relay: Vec, - /// Unix time to to query nostr from - #[arg(long)] - since: Option, - /// Preimage - #[arg(short, long, action = clap::ArgAction::Append)] - preimage: Vec, } pub async fn receive( @@ -40,88 +24,8 @@ pub async fn receive( seed: &[u8], sub_command_args: &ReceiveSubCommand, ) -> Result<()> { - let mut signing_keys = Vec::new(); - - if !sub_command_args.signing_key.is_empty() { - let mut s_keys: Vec = sub_command_args - .signing_key - .iter() - .map(|s| { - if s.starts_with("nsec") { - let nostr_key = nostr_sdk::SecretKey::from_str(s).expect("Invalid secret key"); - - SecretKey::from_str(&nostr_key.to_secret_hex()) - } else { - SecretKey::from_str(s) - } - }) - .collect::, _>>()?; - signing_keys.append(&mut s_keys); - } - - let amount = match &sub_command_args.token { - Some(token_str) => { - receive_token( - multi_mint_wallet, - localstore, - seed, - token_str, - &signing_keys, - &sub_command_args.preimage, - ) - .await? - } - None => { - //wallet.add_p2pk_signing_key(nostr_signing_key).await; - let nostr_key = match sub_command_args.nostr_key.as_ref() { - Some(nostr_key) => { - let secret_key = nostr_sdk::SecretKey::from_str(nostr_key)?; - let secret_key = SecretKey::from_str(&secret_key.to_secret_hex())?; - Some(secret_key) - } - None => None, - }; - - let nostr_key = - nostr_key.ok_or(anyhow!("Nostr key required if token is not provided"))?; - - signing_keys.push(nostr_key.clone()); - - let relays = sub_command_args.relay.clone(); - let since = localstore - .get_nostr_last_checked(&nostr_key.public_key()) - .await?; - - let tokens = nostr_receive(relays, nostr_key.clone(), since).await?; - - let mut total_amount = Amount::ZERO; - for token_str in &tokens { - match receive_token( - multi_mint_wallet, - localstore.clone(), - seed, - token_str, - &signing_keys, - &sub_command_args.preimage, - ) - .await - { - Ok(amount) => { - total_amount += amount; - } - Err(err) => { - println!("{}", err); - } - } - } - - localstore - .add_nostr_last_checked(nostr_key.public_key(), unix_time() as u32) - .await?; - total_amount - } - }; - + let token_str = sub_command_args.token.clone().unwrap(); + let amount = receive_token(multi_mint_wallet, localstore, seed, &token_str, &[], &[]).await?; println!("Received: {}", amount); Ok(()) @@ -157,55 +61,3 @@ async fn receive_token( .await?; Ok(amount) } - -/// Receive tokens sent to nostr pubkey via dm -async fn nostr_receive( - relays: Vec, - nostr_signing_key: SecretKey, - since: Option, -) -> Result> { - let verifying_key = nostr_signing_key.public_key(); - - let x_only_pubkey = verifying_key.x_only_public_key(); - - let nostr_pubkey = nostr_sdk::PublicKey::from_hex(x_only_pubkey.to_string())?; - - let since = since.map(|s| Timestamp::from(s as u64)); - - let filter = match since { - Some(since) => Filter::new() - .pubkey(nostr_pubkey) - .kind(Kind::EncryptedDirectMessage) - .since(since), - None => Filter::new() - .pubkey(nostr_pubkey) - .kind(Kind::EncryptedDirectMessage), - }; - - let client = nostr_sdk::Client::default(); - - client.add_relays(relays).await?; - - client.connect().await; - - let events = client.get_events_of(vec![filter], None).await?; - - let mut tokens: HashSet = HashSet::new(); - - let keys = Keys::from_str(&(nostr_signing_key).to_secret_hex())?; - - for event in events { - if event.kind() == Kind::EncryptedDirectMessage { - if let Ok(msg) = nip04::decrypt(keys.secret_key()?, event.author_ref(), event.content()) - { - if let Some(token) = cdk::wallet::util::token_from_text(&msg) { - tokens.insert(token.to_string()); - } - } else { - tracing::error!("Impossible to decrypt direct message"); - } - } - } - - Ok(tokens) -} diff --git a/crates/cdk-cln/Cargo.toml b/crates/cdk-cln/Cargo.toml deleted file mode 100644 index a5c505ed..00000000 --- a/crates/cdk-cln/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "cdk-cln" -version = "0.4.0" -edition = "2021" -authors = ["CDK Developers"] -license = "MIT" -homepage = "https://github.com/cashubtc/cdk" -repository = "https://github.com/cashubtc/cdk.git" -rust-version = "1.63.0" # MSRV -description = "CDK ln backend for cln" - -[dependencies] -async-trait = "0.1" -bitcoin = { version = "0.32.2", default-features = false } -cdk = { path = "../cdk", version = "0.4.0", default-features = false, features = ["mint"] } -cln-rpc = "0.2.0" -futures = { version = "0.3.28", default-features = false } -tokio = { version = "1", default-features = false } -tokio-util = { version = "0.7.11", default-features = false } -tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] } -thiserror = "1" -uuid = { version = "1", features = ["v4"] } diff --git a/crates/cdk-cln/README.md b/crates/cdk-cln/README.md deleted file mode 100644 index eaf4975a..00000000 --- a/crates/cdk-cln/README.md +++ /dev/null @@ -1,16 +0,0 @@ - -## Minimum Supported Rust Version (MSRV) - -The `cdk-cln` library should always compile with any combination of features on Rust **1.63.0**. - -To build and test with the MSRV you will need to pin the below dependency versions: - -```shell -cargo update -p half --precise 2.2.1 -cargo update -p tokio --precise 1.38.1 -cargo update -p tokio-util --precise 0.7.11 -cargo update -p reqwest --precise 0.12.4 -cargo update -p serde_with --precise 3.1.0 -cargo update -p regex --precise 1.9.6 -cargo update -p backtrace --precise 0.3.58 -``` diff --git a/crates/cdk-cln/src/error.rs b/crates/cdk-cln/src/error.rs deleted file mode 100644 index e97832fc..00000000 --- a/crates/cdk-cln/src/error.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! CLN Errors - -use thiserror::Error; - -/// CLN Error -#[derive(Debug, Error)] -pub enum Error { - /// Invoice amount not defined - #[error("Unknown invoice amount")] - UnknownInvoiceAmount, - /// Wrong CLN response - #[error("Wrong CLN response")] - WrongClnResponse, - /// Unknown invoice - #[error("Unknown invoice")] - UnknownInvoice, - /// Invalid payment hash - #[error("Invalid hash")] - InvalidHash, - /// Cln Error - #[error(transparent)] - Cln(#[from] cln_rpc::Error), - /// Cln Rpc Error - #[error(transparent)] - ClnRpc(#[from] cln_rpc::RpcError), - /// Amount Error - #[error(transparent)] - Amount(#[from] cdk::amount::Error), -} - -impl From for cdk::cdk_lightning::Error { - fn from(e: Error) -> Self { - Self::Lightning(Box::new(e)) - } -} diff --git a/crates/cdk-cln/src/lib.rs b/crates/cdk-cln/src/lib.rs deleted file mode 100644 index 1e8f609c..00000000 --- a/crates/cdk-cln/src/lib.rs +++ /dev/null @@ -1,544 +0,0 @@ -//! CDK lightning backend for CLN - -#![warn(missing_docs)] -#![warn(rustdoc::bare_urls)] - -use std::path::PathBuf; -use std::pin::Pin; -use std::str::FromStr; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; -use std::time::Duration; - -use async_trait::async_trait; -use cdk::amount::{to_unit, Amount}; -use cdk::cdk_lightning::{ - self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, -}; -use cdk::mint::FeeReserve; -use cdk::nuts::{ - CurrencyUnit, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteState, MintMethodSettings, - MintQuoteState, -}; -use cdk::util::{hex, unix_time}; -use cdk::{mint, Bolt11Invoice}; -use cln_rpc::model::requests::{ - InvoiceRequest, ListinvoicesRequest, ListpaysRequest, PayRequest, WaitanyinvoiceRequest, -}; -use cln_rpc::model::responses::{ - ListinvoicesInvoices, ListinvoicesInvoicesStatus, ListpaysPaysStatus, PayStatus, - WaitanyinvoiceResponse, WaitanyinvoiceStatus, -}; -use cln_rpc::model::Request; -use cln_rpc::primitives::{Amount as CLN_Amount, AmountOrAny}; -use error::Error; -use futures::{Stream, StreamExt}; -use tokio::sync::Mutex; -use tokio_util::sync::CancellationToken; -use uuid::Uuid; - -pub mod error; - -/// CLN mint backend -#[derive(Clone)] -pub struct Cln { - rpc_socket: PathBuf, - cln_client: Arc>, - fee_reserve: FeeReserve, - mint_settings: MintMethodSettings, - melt_settings: MeltMethodSettings, - wait_invoice_cancel_token: CancellationToken, - wait_invoice_is_active: Arc, -} - -impl Cln { - /// Create new [`Cln`] - pub async fn new( - rpc_socket: PathBuf, - fee_reserve: FeeReserve, - mint_settings: MintMethodSettings, - melt_settings: MeltMethodSettings, - ) -> Result { - let cln_client = cln_rpc::ClnRpc::new(&rpc_socket).await?; - - Ok(Self { - rpc_socket, - cln_client: Arc::new(Mutex::new(cln_client)), - fee_reserve, - mint_settings, - melt_settings, - wait_invoice_cancel_token: CancellationToken::new(), - wait_invoice_is_active: Arc::new(AtomicBool::new(false)), - }) - } -} - -#[async_trait] -impl MintLightning for Cln { - type Err = cdk_lightning::Error; - - fn get_settings(&self) -> Settings { - Settings { - mpp: true, - unit: CurrencyUnit::Msat, - mint_settings: self.mint_settings, - melt_settings: self.melt_settings, - invoice_description: true, - } - } - - /// Is wait invoice active - fn is_wait_invoice_active(&self) -> bool { - self.wait_invoice_is_active.load(Ordering::SeqCst) - } - - /// Cancel wait invoice - fn cancel_wait_invoice(&self) { - self.wait_invoice_cancel_token.cancel() - } - - #[allow(clippy::incompatible_msrv)] - // Clippy thinks select is not stable but it compiles fine on MSRV (1.63.0) - async fn wait_any_invoice( - &self, - ) -> Result + Send>>, Self::Err> { - let last_pay_index = self.get_last_pay_index().await?; - let cln_client = cln_rpc::ClnRpc::new(&self.rpc_socket).await?; - - let stream = futures::stream::unfold( - ( - cln_client, - last_pay_index, - self.wait_invoice_cancel_token.clone(), - Arc::clone(&self.wait_invoice_is_active), - ), - |(mut cln_client, mut last_pay_idx, cancel_token, is_active)| async move { - // Set the stream as active - is_active.store(true, Ordering::SeqCst); - - loop { - tokio::select! { - _ = cancel_token.cancelled() => { - // Set the stream as inactive - is_active.store(false, Ordering::SeqCst); - // End the stream - return None; - } - result = cln_client.call(cln_rpc::Request::WaitAnyInvoice(WaitanyinvoiceRequest { - timeout: None, - lastpay_index: last_pay_idx, - })) => { - match result { - Ok(invoice) => { - - // Try to convert the invoice to WaitanyinvoiceResponse - let wait_any_response_result: Result = - invoice.try_into(); - - let wait_any_response = match wait_any_response_result { - Ok(response) => response, - Err(e) => { - tracing::warn!( - "Failed to parse WaitAnyInvoice response: {:?}", - e - ); - // Continue to the next iteration without panicking - continue; - } - }; - - // Check the status of the invoice - // We only want to yield invoices that have been paid - match wait_any_response.status { - WaitanyinvoiceStatus::PAID => (), - WaitanyinvoiceStatus::EXPIRED => continue, - } - - last_pay_idx = wait_any_response.pay_index; - - let payment_hash = wait_any_response.payment_hash.to_string(); - - let request_look_up = match wait_any_response.bolt12 { - // If it is a bolt12 payment we need to get the offer_id as this is what we use as the request look up. - // Since this is not returned in the wait any response, - // we need to do a second query for it. - Some(_) => { - match fetch_invoice_by_payment_hash( - &mut cln_client, - &payment_hash, - ) - .await - { - Ok(Some(invoice)) => { - if let Some(local_offer_id) = invoice.local_offer_id { - local_offer_id.to_string() - } else { - continue; - } - } - Ok(None) => continue, - Err(e) => { - tracing::warn!( - "Error fetching invoice by payment hash: {e}" - ); - continue; - } - } - } - None => payment_hash, - }; - - return Some((request_look_up, (cln_client, last_pay_idx, cancel_token, is_active))); - } - Err(e) => { - tracing::warn!("Error fetching invoice: {e}"); - tokio::time::sleep(Duration::from_secs(1)).await; - continue; - } - } - } - } - } - }, - ) - .boxed(); - - Ok(stream) - } - - async fn get_payment_quote( - &self, - melt_quote_request: &MeltQuoteBolt11Request, - ) -> Result { - let invoice_amount_msat = melt_quote_request - .request - .amount_milli_satoshis() - .ok_or(Error::UnknownInvoiceAmount)?; - - let amount = to_unit( - invoice_amount_msat, - &CurrencyUnit::Msat, - &melt_quote_request.unit, - )?; - - let relative_fee_reserve = - (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64; - - let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into(); - - let fee = match relative_fee_reserve > absolute_fee_reserve { - true => relative_fee_reserve, - false => absolute_fee_reserve, - }; - - Ok(PaymentQuoteResponse { - request_lookup_id: melt_quote_request.request.payment_hash().to_string(), - amount, - fee: fee.into(), - state: MeltQuoteState::Unpaid, - }) - } - - async fn pay_invoice( - &self, - melt_quote: mint::MeltQuote, - partial_amount: Option, - max_fee: Option, - ) -> Result { - let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?; - let pay_state = self - .check_outgoing_payment(&bolt11.payment_hash().to_string()) - .await?; - - match pay_state.status { - MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => (), - MeltQuoteState::Paid => { - tracing::debug!("Melt attempted on invoice already paid"); - return Err(Self::Err::InvoiceAlreadyPaid); - } - MeltQuoteState::Pending => { - tracing::debug!("Melt attempted on invoice already pending"); - return Err(Self::Err::InvoicePaymentPending); - } - } - - let mut cln_client = self.cln_client.lock().await; - let cln_response = cln_client - .call(Request::Pay(PayRequest { - bolt11: melt_quote.request.to_string(), - amount_msat: None, - label: None, - riskfactor: None, - maxfeepercent: None, - retry_for: None, - maxdelay: None, - exemptfee: None, - localinvreqid: None, - exclude: None, - maxfee: max_fee - .map(|a| { - let msat = to_unit(a, &melt_quote.unit, &CurrencyUnit::Msat)?; - Ok::(CLN_Amount::from_msat( - msat.into(), - )) - }) - .transpose()?, - description: None, - partial_msat: partial_amount - .map(|a| { - let msat = to_unit(a, &melt_quote.unit, &CurrencyUnit::Msat)?; - - Ok::(CLN_Amount::from_msat( - msat.into(), - )) - }) - .transpose()?, - })) - .await; - - let response = match cln_response { - Ok(cln_rpc::Response::Pay(pay_response)) => { - let status = match pay_response.status { - PayStatus::COMPLETE => MeltQuoteState::Paid, - PayStatus::PENDING => MeltQuoteState::Pending, - PayStatus::FAILED => MeltQuoteState::Failed, - }; - PayInvoiceResponse { - payment_preimage: Some(hex::encode(pay_response.payment_preimage.to_vec())), - payment_lookup_id: pay_response.payment_hash.to_string(), - status, - total_spent: to_unit( - pay_response.amount_sent_msat.msat(), - &CurrencyUnit::Msat, - &melt_quote.unit, - )?, - unit: melt_quote.unit, - } - } - _ => { - tracing::error!( - "Error attempting to pay invoice: {}", - bolt11.payment_hash().to_string() - ); - return Err(Error::WrongClnResponse.into()); - } - }; - - Ok(response) - } - - async fn create_invoice( - &self, - amount: Amount, - unit: &CurrencyUnit, - description: String, - unix_expiry: u64, - ) -> Result { - let time_now = unix_time(); - assert!(unix_expiry > time_now); - - let mut cln_client = self.cln_client.lock().await; - - let label = Uuid::new_v4().to_string(); - - let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?; - let amount_msat = AmountOrAny::Amount(CLN_Amount::from_msat(amount.into())); - - let cln_response = cln_client - .call(cln_rpc::Request::Invoice(InvoiceRequest { - amount_msat, - description, - label: label.clone(), - expiry: Some(unix_expiry - time_now), - fallbacks: None, - preimage: None, - cltv: None, - deschashonly: None, - exposeprivatechannels: None, - })) - .await - .map_err(Error::from)?; - - match cln_response { - cln_rpc::Response::Invoice(invoice_res) => { - let request = Bolt11Invoice::from_str(&invoice_res.bolt11)?; - let expiry = request.expires_at().map(|t| t.as_secs()); - let payment_hash = request.payment_hash(); - - Ok(CreateInvoiceResponse { - request_lookup_id: payment_hash.to_string(), - request, - expiry, - }) - } - _ => { - tracing::warn!("CLN returned wrong response kind"); - Err(Error::WrongClnResponse.into()) - } - } - } - - async fn check_incoming_invoice_status( - &self, - payment_hash: &str, - ) -> Result { - let mut cln_client = self.cln_client.lock().await; - - let cln_response = cln_client - .call(Request::ListInvoices(ListinvoicesRequest { - payment_hash: Some(payment_hash.to_string()), - label: None, - invstring: None, - offer_id: None, - index: None, - limit: None, - start: None, - })) - .await - .map_err(Error::from)?; - - let status = match cln_response { - cln_rpc::Response::ListInvoices(invoice_response) => { - match invoice_response.invoices.first() { - Some(invoice_response) => { - cln_invoice_status_to_mint_state(invoice_response.status) - } - None => { - tracing::info!( - "Check invoice called on unknown look up id: {}", - payment_hash - ); - return Err(Error::WrongClnResponse.into()); - } - } - } - _ => { - tracing::warn!("CLN returned wrong response kind"); - return Err(Error::WrongClnResponse.into()); - } - }; - - Ok(status) - } - - async fn check_outgoing_payment( - &self, - payment_hash: &str, - ) -> Result { - let mut cln_client = self.cln_client.lock().await; - - let cln_response = cln_client - .call(Request::ListPays(ListpaysRequest { - payment_hash: Some(payment_hash.parse().map_err(|_| Error::InvalidHash)?), - bolt11: None, - status: None, - })) - .await - .map_err(Error::from)?; - - match cln_response { - cln_rpc::Response::ListPays(pays_response) => match pays_response.pays.first() { - Some(pays_response) => { - let status = cln_pays_status_to_mint_state(pays_response.status); - - Ok(PayInvoiceResponse { - payment_lookup_id: pays_response.payment_hash.to_string(), - payment_preimage: pays_response.preimage.map(|p| hex::encode(p.to_vec())), - status, - total_spent: pays_response - .amount_sent_msat - .map_or(Amount::ZERO, |a| a.msat().into()), - unit: CurrencyUnit::Msat, - }) - } - None => Ok(PayInvoiceResponse { - payment_lookup_id: payment_hash.to_string(), - payment_preimage: None, - status: MeltQuoteState::Unknown, - total_spent: Amount::ZERO, - unit: CurrencyUnit::Msat, - }), - }, - _ => { - tracing::warn!("CLN returned wrong response kind"); - Err(Error::WrongClnResponse.into()) - } - } - } -} - -impl Cln { - /// Get last pay index for cln - async fn get_last_pay_index(&self) -> Result, Error> { - let mut cln_client = self.cln_client.lock().await; - let cln_response = cln_client - .call(cln_rpc::Request::ListInvoices(ListinvoicesRequest { - index: None, - invstring: None, - label: None, - limit: None, - offer_id: None, - payment_hash: None, - start: None, - })) - .await - .map_err(Error::from)?; - - match cln_response { - cln_rpc::Response::ListInvoices(invoice_res) => match invoice_res.invoices.last() { - Some(last_invoice) => Ok(last_invoice.pay_index), - None => Ok(None), - }, - _ => { - tracing::warn!("CLN returned wrong response kind"); - Err(Error::WrongClnResponse) - } - } - } -} - -fn cln_invoice_status_to_mint_state(status: ListinvoicesInvoicesStatus) -> MintQuoteState { - match status { - ListinvoicesInvoicesStatus::UNPAID => MintQuoteState::Unpaid, - ListinvoicesInvoicesStatus::PAID => MintQuoteState::Paid, - ListinvoicesInvoicesStatus::EXPIRED => MintQuoteState::Unpaid, - } -} - -fn cln_pays_status_to_mint_state(status: ListpaysPaysStatus) -> MeltQuoteState { - match status { - ListpaysPaysStatus::PENDING => MeltQuoteState::Pending, - ListpaysPaysStatus::COMPLETE => MeltQuoteState::Paid, - ListpaysPaysStatus::FAILED => MeltQuoteState::Failed, - } -} - -async fn fetch_invoice_by_payment_hash( - cln_client: &mut cln_rpc::ClnRpc, - payment_hash: &str, -) -> Result, Error> { - match cln_client - .call(cln_rpc::Request::ListInvoices(ListinvoicesRequest { - payment_hash: Some(payment_hash.to_string()), - index: None, - invstring: None, - label: None, - limit: None, - offer_id: None, - start: None, - })) - .await - { - Ok(cln_rpc::Response::ListInvoices(invoice_response)) => { - Ok(invoice_response.invoices.first().cloned()) - } - Ok(_) => { - tracing::warn!("CLN returned an unexpected response type"); - Err(Error::WrongClnResponse) - } - Err(e) => { - tracing::warn!("Error fetching invoice: {e}"); - Err(Error::from(e)) - } - } -} diff --git a/crates/cdk-integration-tests/Cargo.toml b/crates/cdk-integration-tests/Cargo.toml deleted file mode 100644 index 7291eba9..00000000 --- a/crates/cdk-integration-tests/Cargo.toml +++ /dev/null @@ -1,60 +0,0 @@ -[package] -name = "cdk-integration-tests" -version = "0.4.0" -edition = "2021" -authors = ["CDK Developers"] -description = "Core Cashu Development Kit library implementing the Cashu protocol" -license = "MIT" -homepage = "https://github.com/cashubtc/cdk" -repository = "https://github.com/cashubtc/cdk.git" -rust-version = "1.63.0" # MSRV - - -[features] - - -[dependencies] -axum = "0.6.20" -rand = "0.8.5" -bip39 = { version = "2.0", features = ["rand"] } -anyhow = "1" -cdk = { path = "../cdk", version = "0.4.0", features = ["mint", "wallet"] } -cdk-cln = { path = "../cdk-cln", version = "0.4.0" } -cdk-axum = { path = "../cdk-axum"} -cdk-sqlite = { path = "../cdk-sqlite"} -cdk-redb = { path = "../cdk-redb"} -cdk-fake-wallet = { path = "../cdk-fake-wallet" } -tower-http = { version = "0.4.4", features = ["cors"] } -futures = { version = "0.3.28", default-features = false, features = ["executor"] } -once_cell = "1.19.0" -uuid = { version = "1", features = ["v4"] } -serde = "1" -serde_json = "1" -# ln-regtest-rs = { path = "../../../../ln-regtest-rs" } -ln-regtest-rs = { git = "https://github.com/thesimplekid/ln-regtest-rs", rev = "1d88d3d0b" } -lightning-invoice = { version = "0.32.0", features = ["serde", "std"] } -tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] } -tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } -tower-service = "0.3.3" - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -tokio = { version = "1", features = [ - "rt-multi-thread", - "time", - "macros", - "sync", -] } - -[target.'cfg(target_arch = "wasm32")'.dependencies] -tokio = { version = "1", features = ["rt", "macros", "sync", "time"] } -getrandom = { version = "0.2", features = ["js"] } -instant = { version = "0.1", features = ["wasm-bindgen", "inaccurate"] } - -[dev-dependencies] -rand = "0.8.5" -bip39 = { version= "2.0", features = ["rand"] } -anyhow = "1" -cdk = { path = "../cdk", features = ["mint", "wallet"] } -cdk-axum = { path = "../cdk-axum" } -cdk-fake-wallet = { path = "../cdk-fake-wallet" } -tower-http = { version = "0.4.4", features = ["cors"] } diff --git a/crates/cdk-integration-tests/src/bin/fake_wallet.rs b/crates/cdk-integration-tests/src/bin/fake_wallet.rs deleted file mode 100644 index 162dc7ee..00000000 --- a/crates/cdk-integration-tests/src/bin/fake_wallet.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::env; - -use anyhow::Result; -use cdk::cdk_database::mint_memory::MintMemoryDatabase; -use cdk_integration_tests::{init_fake_wallet::start_fake_mint, init_regtest::get_temp_dir}; -use cdk_redb::MintRedbDatabase; -use cdk_sqlite::MintSqliteDatabase; - -#[tokio::main] -async fn main() -> Result<()> { - let addr = "127.0.0.1"; - let port = 8086; - - let mint_db_kind = env::var("MINT_DATABASE")?; - - match mint_db_kind.as_str() { - "MEMORY" => { - start_fake_mint(addr, port, MintMemoryDatabase::default()).await?; - } - "SQLITE" => { - let sqlite_db = MintSqliteDatabase::new(&get_temp_dir().join("mint")).await?; - sqlite_db.migrate().await; - start_fake_mint(addr, port, sqlite_db).await?; - } - "REDB" => { - let redb_db = MintRedbDatabase::new(&get_temp_dir().join("mint")).unwrap(); - start_fake_mint(addr, port, redb_db).await?; - } - _ => panic!("Unknown mint db type: {}", mint_db_kind), - }; - Ok(()) -} diff --git a/crates/cdk-integration-tests/src/init_fake_wallet.rs b/crates/cdk-integration-tests/src/init_fake_wallet.rs deleted file mode 100644 index 3eeaa37f..00000000 --- a/crates/cdk-integration-tests/src/init_fake_wallet.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - sync::Arc, -}; - -use anyhow::Result; -use axum::Router; -use cdk::{ - cdk_database::{self, MintDatabase}, - cdk_lightning::MintLightning, - mint::FeeReserve, - nuts::{CurrencyUnit, MeltMethodSettings, MintMethodSettings}, - types::LnKey, -}; -use cdk_fake_wallet::FakeWallet; -use tokio::sync::Notify; -use tower_http::cors::CorsLayer; -use tracing_subscriber::EnvFilter; - -use crate::init_regtest::create_mint; - -pub async fn start_fake_mint(addr: &str, port: u16, database: D) -> Result<()> -where - D: MintDatabase + Send + Sync + 'static, -{ - let default_filter = "debug"; - - let sqlx_filter = "sqlx=warn"; - let hyper_filter = "hyper=warn"; - - let env_filter = EnvFilter::new(format!( - "{},{},{}", - default_filter, sqlx_filter, hyper_filter - )); - - // Parse input - tracing_subscriber::fmt().with_env_filter(env_filter).init(); - - let mut ln_backends: HashMap< - LnKey, - Arc + Sync + Send>, - > = HashMap::new(); - - let fee_reserve = FeeReserve { - min_fee_reserve: 1.into(), - percent_fee_reserve: 1.0, - }; - - let fake_wallet = FakeWallet::new( - fee_reserve, - MintMethodSettings::default(), - MeltMethodSettings::default(), - HashMap::default(), - HashSet::default(), - 0, - ); - - ln_backends.insert( - LnKey::new(CurrencyUnit::Sat, cdk::nuts::PaymentMethod::Bolt11), - Arc::new(fake_wallet), - ); - - let mint = create_mint(database, ln_backends.clone()).await?; - let cache_ttl = 3600; - let cache_tti = 3600; - let mint_arc = Arc::new(mint); - - let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint_arc), cache_ttl, cache_tti) - .await - .unwrap(); - - let mint_service = Router::new() - .merge(v1_service) - .layer(CorsLayer::permissive()); - - let mint = Arc::clone(&mint_arc); - - let shutdown = Arc::new(Notify::new()); - - tokio::spawn({ - let shutdown = Arc::clone(&shutdown); - async move { mint.wait_for_paid_invoices(shutdown).await } - }); - - println!("Staring Axum server"); - axum::Server::bind(&format!("{}:{}", addr, port).as_str().parse().unwrap()) - .serve(mint_service.into_make_service()) - .await?; - - Ok(()) -} diff --git a/crates/cdk-integration-tests/src/init_regtest.rs b/crates/cdk-integration-tests/src/init_regtest.rs deleted file mode 100644 index 769a3350..00000000 --- a/crates/cdk-integration-tests/src/init_regtest.rs +++ /dev/null @@ -1,302 +0,0 @@ -use std::{collections::HashMap, env, path::PathBuf, sync::Arc}; - -use anyhow::Result; -use axum::Router; -use bip39::Mnemonic; -use cdk::{ - cdk_database::{self, MintDatabase}, - cdk_lightning::MintLightning, - mint::{FeeReserve, Mint}, - nuts::{CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings}, - types::{LnKey, QuoteTTL}, -}; -use cdk_cln::Cln as CdkCln; -use ln_regtest_rs::{ - bitcoin_client::BitcoinClient, bitcoind::Bitcoind, cln::Clnd, cln_client::ClnClient, lnd::Lnd, - lnd_client::LndClient, -}; -use tokio::sync::Notify; -use tower_http::cors::CorsLayer; -use tracing_subscriber::EnvFilter; - -const BITCOIND_ADDR: &str = "127.0.0.1:18443"; -const ZMQ_RAW_BLOCK: &str = "tcp://127.0.0.1:28332"; -const ZMQ_RAW_TX: &str = "tcp://127.0.0.1:28333"; -const BITCOIN_RPC_USER: &str = "testuser"; -const BITCOIN_RPC_PASS: &str = "testpass"; -const CLN_ADDR: &str = "127.0.0.1:19846"; -const LND_ADDR: &str = "0.0.0.0:18444"; -const LND_RPC_ADDR: &str = "https://127.0.0.1:10009"; - -const BITCOIN_DIR: &str = "bitcoin"; -const CLN_DIR: &str = "cln"; -const LND_DIR: &str = "lnd"; - -pub fn get_mint_addr() -> String { - env::var("cdk_itests_mint_addr").expect("Temp dir set") -} - -pub fn get_mint_port() -> u16 { - let dir = env::var("cdk_itests_mint_port").expect("Temp dir set"); - dir.parse().unwrap() -} - -pub fn get_mint_url() -> String { - format!("http://{}:{}", get_mint_addr(), get_mint_port()) -} - -pub fn get_temp_dir() -> PathBuf { - let dir = env::var("cdk_itests").expect("Temp dir set"); - std::fs::create_dir_all(&dir).unwrap(); - dir.parse().expect("Valid path buf") -} - -pub fn get_bitcoin_dir() -> PathBuf { - let dir = get_temp_dir().join(BITCOIN_DIR); - std::fs::create_dir_all(&dir).unwrap(); - dir -} - -pub fn init_bitcoind() -> Bitcoind { - Bitcoind::new( - get_bitcoin_dir(), - BITCOIND_ADDR.parse().unwrap(), - BITCOIN_RPC_USER.to_string(), - BITCOIN_RPC_PASS.to_string(), - ZMQ_RAW_BLOCK.to_string(), - ZMQ_RAW_TX.to_string(), - ) -} - -pub fn init_bitcoin_client() -> Result { - BitcoinClient::new( - "wallet".to_string(), - BITCOIND_ADDR.into(), - None, - Some(BITCOIN_RPC_USER.to_string()), - Some(BITCOIN_RPC_PASS.to_string()), - ) -} - -pub fn get_cln_dir() -> PathBuf { - let dir = get_temp_dir().join(CLN_DIR); - std::fs::create_dir_all(&dir).unwrap(); - dir -} - -pub fn init_cln() -> Clnd { - Clnd::new( - get_bitcoin_dir(), - get_cln_dir(), - CLN_ADDR.to_string().parse().unwrap(), - BITCOIN_RPC_USER.to_string(), - BITCOIN_RPC_PASS.to_string(), - ) -} - -pub async fn init_cln_client() -> Result { - ClnClient::new(get_cln_dir(), None).await -} - -pub fn get_lnd_dir() -> PathBuf { - let dir = get_temp_dir().join(LND_DIR); - std::fs::create_dir_all(&dir).unwrap(); - dir -} - -pub async fn init_lnd() -> Lnd { - Lnd::new( - get_bitcoin_dir(), - get_lnd_dir(), - LND_ADDR.parse().unwrap(), - BITCOIN_RPC_USER.to_string(), - BITCOIN_RPC_PASS.to_string(), - ZMQ_RAW_BLOCK.to_string(), - ZMQ_RAW_TX.to_string(), - ) -} - -pub async fn init_lnd_client() -> Result { - let lnd_dir = get_lnd_dir(); - let cert_file = lnd_dir.join("tls.cert"); - let macaroon_file = lnd_dir.join("data/chain/bitcoin/regtest/admin.macaroon"); - LndClient::new(LND_RPC_ADDR.parse().unwrap(), cert_file, macaroon_file).await -} - -pub async fn create_cln_backend(cln_client: &ClnClient) -> Result { - let rpc_path = cln_client.rpc_path.clone(); - - let fee_reserve = FeeReserve { - min_fee_reserve: 1.into(), - percent_fee_reserve: 1.0, - }; - - Ok(CdkCln::new( - rpc_path, - fee_reserve, - MintMethodSettings::default(), - MeltMethodSettings::default(), - ) - .await?) -} - -pub async fn create_mint( - database: D, - ln_backends: HashMap< - LnKey, - Arc + Sync + Send>, - >, -) -> Result -where - D: MintDatabase + Send + Sync + 'static, -{ - let nuts = cdk::nuts::Nuts::new() - .nut07(true) - .nut08(true) - .nut09(true) - .nut10(true) - .nut11(true) - .nut12(true) - .nut14(true); - - let mint_info = MintInfo::new().nuts(nuts); - - let mnemonic = Mnemonic::generate(12)?; - - let mut supported_units: HashMap = HashMap::new(); - supported_units.insert(CurrencyUnit::Sat, (0, 32)); - - let quote_ttl = QuoteTTL::new(10000, 10000); - - let mint = Mint::new( - &get_mint_url(), - &mnemonic.to_seed_normalized(""), - mint_info, - quote_ttl, - Arc::new(database), - ln_backends, - supported_units, - ) - .await?; - - Ok(mint) -} - -pub async fn start_cln_mint(addr: &str, port: u16, database: D) -> Result<()> -where - D: MintDatabase + Send + Sync + 'static, -{ - let default_filter = "debug"; - - let sqlx_filter = "sqlx=warn"; - let hyper_filter = "hyper=warn"; - - let env_filter = EnvFilter::new(format!( - "{},{},{}", - default_filter, sqlx_filter, hyper_filter - )); - - // Parse input - tracing_subscriber::fmt().with_env_filter(env_filter).init(); - - let cln_client = init_cln_client().await?; - - let cln_backend = create_cln_backend(&cln_client).await?; - - let mut ln_backends: HashMap< - LnKey, - Arc + Sync + Send>, - > = HashMap::new(); - - ln_backends.insert( - LnKey::new(CurrencyUnit::Sat, cdk::nuts::PaymentMethod::Bolt11), - Arc::new(cln_backend), - ); - - let mint = create_mint(database, ln_backends.clone()).await?; - let cache_time_to_live = 3600; - let cache_time_to_idle = 3600; - let mint_arc = Arc::new(mint); - - let v1_service = cdk_axum::create_mint_router( - Arc::clone(&mint_arc), - cache_time_to_live, - cache_time_to_idle, - ) - .await - .unwrap(); - - let mint_service = Router::new() - .merge(v1_service) - .layer(CorsLayer::permissive()); - - let mint = Arc::clone(&mint_arc); - - let shutdown = Arc::new(Notify::new()); - - tokio::spawn({ - let shutdown = Arc::clone(&shutdown); - async move { mint.wait_for_paid_invoices(shutdown).await } - }); - - println!("Staring Axum server"); - axum::Server::bind(&format!("{}:{}", addr, port).as_str().parse().unwrap()) - .serve(mint_service.into_make_service()) - .await?; - - Ok(()) -} - -pub async fn fund_ln( - bitcoin_client: &BitcoinClient, - cln_client: &ClnClient, - lnd_client: &LndClient, -) -> Result<()> { - let lnd_address = lnd_client.get_new_address().await?; - - bitcoin_client.send_to_address(&lnd_address, 2_000_000)?; - - let cln_address = cln_client.get_new_address().await?; - bitcoin_client.send_to_address(&cln_address, 2_000_000)?; - - let mining_address = bitcoin_client.get_new_address()?; - bitcoin_client.generate_blocks(&mining_address, 200)?; - - cln_client.wait_chain_sync().await?; - lnd_client.wait_chain_sync().await?; - - Ok(()) -} - -pub async fn open_channel( - bitcoin_client: &BitcoinClient, - cln_client: &ClnClient, - lnd_client: &LndClient, -) -> Result<()> { - let cln_info = cln_client.get_info().await?; - - let cln_pubkey = cln_info.id; - let cln_address = "127.0.0.1"; - let cln_port = 19846; - - lnd_client - .connect(cln_pubkey.to_string(), cln_address.to_string(), cln_port) - .await - .unwrap(); - - lnd_client - .open_channel(1_500_000, &cln_pubkey.to_string(), Some(750_000)) - .await - .unwrap(); - - let mine_to_address = bitcoin_client.get_new_address()?; - bitcoin_client.generate_blocks(&mine_to_address, 10)?; - - cln_client.wait_chain_sync().await?; - lnd_client.wait_chain_sync().await?; - - cln_client.wait_channels_active().await?; - lnd_client.wait_channels_active().await?; - - Ok(()) -} diff --git a/crates/cdk-integration-tests/src/lib.rs b/crates/cdk-integration-tests/src/lib.rs deleted file mode 100644 index eacc3b3a..00000000 --- a/crates/cdk-integration-tests/src/lib.rs +++ /dev/null @@ -1,235 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::sync::Arc; -use std::time::Duration; - -use anyhow::{bail, Result}; -use axum::Router; -use bip39::Mnemonic; -use cdk::amount::{Amount, SplitTarget}; -use cdk::cdk_database::mint_memory::MintMemoryDatabase; -use cdk::cdk_lightning::MintLightning; -use cdk::dhke::construct_proofs; -use cdk::mint::FeeReserve; -use cdk::nuts::{ - CurrencyUnit, Id, KeySet, MeltMethodSettings, MintInfo, MintMethodSettings, MintQuoteState, - Nuts, PaymentMethod, PreMintSecrets, Proofs, State, -}; -use cdk::types::{LnKey, QuoteTTL}; -use cdk::wallet::client::HttpClient; -use cdk::{Mint, Wallet}; -use cdk_fake_wallet::FakeWallet; -use init_regtest::{get_mint_addr, get_mint_port, get_mint_url}; -use tokio::sync::Notify; -use tokio::time::sleep; -use tower_http::cors::CorsLayer; - -pub mod init_fake_wallet; -pub mod init_regtest; - -pub fn create_backends_fake_wallet( -) -> HashMap + Sync + Send>> { - let fee_reserve = FeeReserve { - min_fee_reserve: 1.into(), - percent_fee_reserve: 1.0, - }; - let mut ln_backends: HashMap< - LnKey, - Arc + Sync + Send>, - > = HashMap::new(); - let ln_key = LnKey::new(CurrencyUnit::Sat, PaymentMethod::Bolt11); - - let wallet = Arc::new(FakeWallet::new( - fee_reserve.clone(), - MintMethodSettings::default(), - MeltMethodSettings::default(), - HashMap::default(), - HashSet::default(), - 0, - )); - - ln_backends.insert(ln_key, wallet.clone()); - - ln_backends -} - -pub async fn start_mint( - ln_backends: HashMap< - LnKey, - Arc + Sync + Send>, - >, - supported_units: HashMap, -) -> Result<()> { - let nuts = Nuts::new() - .nut07(true) - .nut08(true) - .nut09(true) - .nut10(true) - .nut11(true) - .nut12(true) - .nut14(true); - - let mint_info = MintInfo::new().nuts(nuts); - - let mnemonic = Mnemonic::generate(12)?; - - let quote_ttl = QuoteTTL::new(10000, 10000); - - let mint = Mint::new( - &get_mint_url(), - &mnemonic.to_seed_normalized(""), - mint_info, - quote_ttl, - Arc::new(MintMemoryDatabase::default()), - ln_backends.clone(), - supported_units, - ) - .await?; - let cache_time_to_live = 3600; - let cache_time_to_idle = 3600; - - let mint_arc = Arc::new(mint); - - let v1_service = cdk_axum::create_mint_router( - Arc::clone(&mint_arc), - cache_time_to_live, - cache_time_to_idle, - ) - .await?; - - let mint_service = Router::new() - .merge(v1_service) - .layer(CorsLayer::permissive()); - - let mint = Arc::clone(&mint_arc); - - let shutdown = Arc::new(Notify::new()); - - tokio::spawn({ - let shutdown = Arc::clone(&shutdown); - async move { mint.wait_for_paid_invoices(shutdown).await } - }); - - axum::Server::bind( - &format!("{}:{}", get_mint_addr(), get_mint_port()) - .as_str() - .parse()?, - ) - .serve(mint_service.into_make_service()) - .await?; - - Ok(()) -} - -pub async fn wallet_mint( - wallet: Arc, - amount: Amount, - split_target: SplitTarget, - description: Option, -) -> Result<()> { - let quote = wallet.mint_quote(amount, description).await?; - - loop { - let status = wallet.mint_quote_state("e.id).await?; - - if status.state == MintQuoteState::Paid { - break; - } - println!("{:?}", status); - - sleep(Duration::from_secs(2)).await; - } - - let receive_amount = wallet.mint("e.id, split_target, None).await?; - - println!("Minted: {}", receive_amount); - - Ok(()) -} - -pub async fn mint_proofs( - mint_url: &str, - amount: Amount, - keyset_id: Id, - mint_keys: &KeySet, - description: Option, -) -> anyhow::Result { - println!("Minting for ecash"); - println!(); - - let wallet_client = HttpClient::new(); - - let mint_quote = wallet_client - .post_mint_quote(mint_url.parse()?, 1.into(), CurrencyUnit::Sat, description) - .await?; - - println!("Please pay: {}", mint_quote.request); - - loop { - let status = wallet_client - .get_mint_quote_status(mint_url.parse()?, &mint_quote.quote) - .await?; - - if status.state == MintQuoteState::Paid { - break; - } - println!("{:?}", status.state); - - sleep(Duration::from_secs(2)).await; - } - - let premint_secrets = PreMintSecrets::random(keyset_id, amount, &SplitTarget::default())?; - - let mint_response = wallet_client - .post_mint( - mint_url.parse()?, - &mint_quote.quote, - premint_secrets.clone(), - ) - .await?; - - let pre_swap_proofs = construct_proofs( - mint_response.signatures, - premint_secrets.rs(), - premint_secrets.secrets(), - &mint_keys.clone().keys, - )?; - - Ok(pre_swap_proofs) -} - -// Get all pending from wallet and attempt to swap -// Will panic if there are no pending -// Will return Ok if swap fails as expected -pub async fn attempt_to_swap_pending(wallet: &Wallet) -> Result<()> { - let pending = wallet - .localstore - .get_proofs(None, None, Some(vec![State::Pending]), None) - .await?; - - assert!(!pending.is_empty()); - - let swap = wallet - .swap( - None, - SplitTarget::None, - pending.into_iter().map(|p| p.proof).collect(), - None, - false, - ) - .await; - - match swap { - Ok(_swap) => { - bail!("These proofs should be pending") - } - Err(err) => match err { - cdk::error::Error::TokenPending => (), - _ => { - println!("{:?}", err); - bail!("Wrong error") - } - }, - } - - Ok(()) -} diff --git a/crates/cdk-integration-tests/src/main.rs b/crates/cdk-integration-tests/src/main.rs deleted file mode 100644 index 5cf76c4d..00000000 --- a/crates/cdk-integration-tests/src/main.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::env; - -use anyhow::Result; -use cdk::cdk_database::mint_memory::MintMemoryDatabase; -use cdk_integration_tests::init_regtest::{ - fund_ln, get_temp_dir, init_bitcoin_client, init_bitcoind, init_cln, init_cln_client, init_lnd, - init_lnd_client, open_channel, start_cln_mint, -}; -use cdk_redb::MintRedbDatabase; -use cdk_sqlite::MintSqliteDatabase; - -#[tokio::main] -async fn main() -> Result<()> { - let mut bitcoind = init_bitcoind(); - bitcoind.start_bitcoind()?; - - let bitcoin_client = init_bitcoin_client()?; - bitcoin_client.create_wallet().ok(); - bitcoin_client.load_wallet()?; - - let new_add = bitcoin_client.get_new_address()?; - bitcoin_client.generate_blocks(&new_add, 200).unwrap(); - - let mut clnd = init_cln(); - clnd.start_clnd()?; - - let cln_client = init_cln_client().await?; - - let mut lnd = init_lnd().await; - lnd.start_lnd().unwrap(); - - let lnd_client = init_lnd_client().await.unwrap(); - - fund_ln(&bitcoin_client, &cln_client, &lnd_client) - .await - .unwrap(); - - open_channel(&bitcoin_client, &cln_client, &lnd_client) - .await - .unwrap(); - - let addr = "127.0.0.1"; - let port = 8085; - - let mint_db_kind = env::var("MINT_DATABASE")?; - - match mint_db_kind.as_str() { - "MEMORY" => { - start_cln_mint(addr, port, MintMemoryDatabase::default()).await?; - } - "SQLITE" => { - let sqlite_db = MintSqliteDatabase::new(&get_temp_dir().join("mint")).await?; - sqlite_db.migrate().await; - start_cln_mint(addr, port, sqlite_db).await?; - } - "REDB" => { - let redb_db = MintRedbDatabase::new(&get_temp_dir().join("mint")).unwrap(); - start_cln_mint(addr, port, redb_db).await?; - } - _ => panic!("Unknown mint db type: {}", mint_db_kind), - }; - - Ok(()) -} diff --git a/crates/cdk-integration-tests/tests/fake_wallet.rs b/crates/cdk-integration-tests/tests/fake_wallet.rs deleted file mode 100644 index e120a4a2..00000000 --- a/crates/cdk-integration-tests/tests/fake_wallet.rs +++ /dev/null @@ -1,381 +0,0 @@ -use std::{sync::Arc, time::Duration}; - -use anyhow::Result; -use bip39::Mnemonic; -use cdk::{ - amount::SplitTarget, - cdk_database::WalletMemoryDatabase, - nuts::{CurrencyUnit, MeltQuoteState, PreMintSecrets, State}, - wallet::{client::HttpClient, Wallet}, -}; -use cdk_fake_wallet::{create_fake_invoice, FakeInvoiceDescription}; -use cdk_integration_tests::attempt_to_swap_pending; -use tokio::time::sleep; - -const MINT_URL: &str = "http://127.0.0.1:8086"; - -// If both pay and check return pending input proofs should remain pending -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_fake_tokens_pending() -> Result<()> { - let wallet = Wallet::new( - MINT_URL, - CurrencyUnit::Sat, - Arc::new(WalletMemoryDatabase::default()), - &Mnemonic::generate(12)?.to_seed_normalized(""), - None, - )?; - - let mint_quote = wallet.mint_quote(100.into(), None).await?; - - sleep(Duration::from_millis(5)).await; - - let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) - .await?; - - let fake_description = FakeInvoiceDescription { - pay_invoice_state: MeltQuoteState::Pending, - check_payment_state: MeltQuoteState::Pending, - pay_err: false, - check_err: false, - }; - - let invoice = create_fake_invoice(1000, serde_json::to_string(&fake_description).unwrap()); - - let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?; - - let melt = wallet.melt(&melt_quote.id).await; - - assert!(melt.is_err()); - - attempt_to_swap_pending(&wallet).await?; - - Ok(()) -} - -// If the pay error fails and the check returns unknown or failed -// The inputs proofs should be unset as spending -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_fake_melt_payment_fail() -> Result<()> { - let wallet = Wallet::new( - MINT_URL, - CurrencyUnit::Sat, - Arc::new(WalletMemoryDatabase::default()), - &Mnemonic::generate(12)?.to_seed_normalized(""), - None, - )?; - - let mint_quote = wallet.mint_quote(100.into(), None).await?; - - sleep(Duration::from_millis(5)).await; - - let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) - .await?; - - let fake_description = FakeInvoiceDescription { - pay_invoice_state: MeltQuoteState::Unknown, - check_payment_state: MeltQuoteState::Unknown, - pay_err: true, - check_err: false, - }; - - let invoice = create_fake_invoice(1000, serde_json::to_string(&fake_description).unwrap()); - - let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?; - - // The melt should error at the payment invoice command - let melt = wallet.melt(&melt_quote.id).await; - assert!(melt.is_err()); - - let fake_description = FakeInvoiceDescription { - pay_invoice_state: MeltQuoteState::Failed, - check_payment_state: MeltQuoteState::Failed, - pay_err: true, - check_err: false, - }; - - let invoice = create_fake_invoice(1000, serde_json::to_string(&fake_description).unwrap()); - - let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?; - - // The melt should error at the payment invoice command - let melt = wallet.melt(&melt_quote.id).await; - assert!(melt.is_err()); - - // The mint should have unset proofs from pending since payment failed - let all_proof = wallet.get_proofs().await?; - let states = wallet.check_proofs_spent(all_proof).await?; - for state in states { - assert!(state.state == State::Unspent); - } - - let wallet_bal = wallet.total_balance().await?; - assert!(wallet_bal == 100.into()); - - Ok(()) -} - -// When both the pay_invoice and check_invoice both fail -// the proofs should remain as pending -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_fake_melt_payment_fail_and_check() -> Result<()> { - let wallet = Wallet::new( - MINT_URL, - CurrencyUnit::Sat, - Arc::new(WalletMemoryDatabase::default()), - &Mnemonic::generate(12)?.to_seed_normalized(""), - None, - )?; - - let mint_quote = wallet.mint_quote(100.into(), None).await?; - - sleep(Duration::from_millis(5)).await; - - let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) - .await?; - - let fake_description = FakeInvoiceDescription { - pay_invoice_state: MeltQuoteState::Unknown, - check_payment_state: MeltQuoteState::Unknown, - pay_err: true, - check_err: true, - }; - - let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap()); - - let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?; - - // The melt should error at the payment invoice command - let melt = wallet.melt(&melt_quote.id).await; - assert!(melt.is_err()); - - let pending = wallet - .localstore - .get_proofs(None, None, Some(vec![State::Pending]), None) - .await?; - - assert!(!pending.is_empty()); - - Ok(()) -} - -// In the case that the ln backend returns a failed status but does not error -// The mint should do a second check, then remove proofs from pending -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_fake_melt_payment_return_fail_status() -> Result<()> { - let wallet = Wallet::new( - MINT_URL, - CurrencyUnit::Sat, - Arc::new(WalletMemoryDatabase::default()), - &Mnemonic::generate(12)?.to_seed_normalized(""), - None, - )?; - - let mint_quote = wallet.mint_quote(100.into(), None).await?; - - sleep(Duration::from_millis(5)).await; - - let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) - .await?; - - let fake_description = FakeInvoiceDescription { - pay_invoice_state: MeltQuoteState::Failed, - check_payment_state: MeltQuoteState::Failed, - pay_err: false, - check_err: false, - }; - - let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap()); - - let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?; - - // The melt should error at the payment invoice command - let melt = wallet.melt(&melt_quote.id).await; - assert!(melt.is_err()); - - let fake_description = FakeInvoiceDescription { - pay_invoice_state: MeltQuoteState::Unknown, - check_payment_state: MeltQuoteState::Unknown, - pay_err: false, - check_err: false, - }; - - let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap()); - - let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?; - - // The melt should error at the payment invoice command - let melt = wallet.melt(&melt_quote.id).await; - assert!(melt.is_err()); - - let pending = wallet - .localstore - .get_proofs(None, None, Some(vec![State::Pending]), None) - .await?; - - assert!(pending.is_empty()); - - Ok(()) -} - -// In the case that the ln backend returns a failed status but does not error -// The mint should do a second check, then remove proofs from pending -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_fake_melt_payment_error_unknown() -> Result<()> { - let wallet = Wallet::new( - MINT_URL, - CurrencyUnit::Sat, - Arc::new(WalletMemoryDatabase::default()), - &Mnemonic::generate(12)?.to_seed_normalized(""), - None, - )?; - - let mint_quote = wallet.mint_quote(100.into(), None).await?; - - sleep(Duration::from_millis(5)).await; - - let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) - .await?; - - let fake_description = FakeInvoiceDescription { - pay_invoice_state: MeltQuoteState::Failed, - check_payment_state: MeltQuoteState::Unknown, - pay_err: true, - check_err: false, - }; - - let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap()); - - let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?; - - // The melt should error at the payment invoice command - let melt = wallet.melt(&melt_quote.id).await; - assert!(melt.is_err()); - - let fake_description = FakeInvoiceDescription { - pay_invoice_state: MeltQuoteState::Unknown, - check_payment_state: MeltQuoteState::Unknown, - pay_err: true, - check_err: false, - }; - - let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap()); - - let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?; - - // The melt should error at the payment invoice command - let melt = wallet.melt(&melt_quote.id).await; - assert!(melt.is_err()); - - let pending = wallet - .localstore - .get_proofs(None, None, Some(vec![State::Pending]), None) - .await?; - - assert!(pending.is_empty()); - - Ok(()) -} - -// In the case that the ln backend returns an err -// The mint should do a second check, that returns paid -// Proofs should remain pending -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_fake_melt_payment_err_paid() -> Result<()> { - let wallet = Wallet::new( - MINT_URL, - CurrencyUnit::Sat, - Arc::new(WalletMemoryDatabase::default()), - &Mnemonic::generate(12)?.to_seed_normalized(""), - None, - )?; - - let mint_quote = wallet.mint_quote(100.into(), None).await?; - - sleep(Duration::from_millis(5)).await; - - let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) - .await?; - - let fake_description = FakeInvoiceDescription { - pay_invoice_state: MeltQuoteState::Failed, - check_payment_state: MeltQuoteState::Paid, - pay_err: true, - check_err: false, - }; - - let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap()); - - let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?; - - // The melt should error at the payment invoice command - let melt = wallet.melt(&melt_quote.id).await; - assert!(melt.is_err()); - - attempt_to_swap_pending(&wallet).await?; - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_fake_melt_change_in_quote() -> Result<()> { - let wallet = Wallet::new( - MINT_URL, - CurrencyUnit::Sat, - Arc::new(WalletMemoryDatabase::default()), - &Mnemonic::generate(12)?.to_seed_normalized(""), - None, - )?; - - let mint_quote = wallet.mint_quote(100.into(), None).await?; - - sleep(Duration::from_millis(5)).await; - - let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) - .await?; - - let fake_description = FakeInvoiceDescription::default(); - - let invoice = create_fake_invoice(9000, serde_json::to_string(&fake_description).unwrap()); - - let proofs = wallet.get_proofs().await?; - - let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?; - - let keyset = wallet.get_active_mint_keyset().await?; - - let premint_secrets = PreMintSecrets::random(keyset.id, 100.into(), &SplitTarget::default())?; - - let client = HttpClient::new(); - - let melt_response = client - .post_melt( - MINT_URL.parse()?, - melt_quote.id.clone(), - proofs.clone(), - Some(premint_secrets.blinded_messages()), - ) - .await?; - - assert!(melt_response.change.is_some()); - - let check = wallet.melt_quote_status(&melt_quote.id).await?; - - assert_eq!( - melt_response - .change - .unwrap() - .sort_by(|a, b| a.amount.cmp(&b.amount)), - check - .change - .unwrap() - .sort_by(|a, b| a.amount.cmp(&b.amount)) - ); - Ok(()) -} diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs deleted file mode 100644 index c86e2dd3..00000000 --- a/crates/cdk-integration-tests/tests/mint.rs +++ /dev/null @@ -1,373 +0,0 @@ -//! Mint tests - -use anyhow::{bail, Result}; -use bip39::Mnemonic; -use cdk::amount::{Amount, SplitTarget}; -use cdk::cdk_database::mint_memory::MintMemoryDatabase; -use cdk::dhke::construct_proofs; -use cdk::mint::MintQuote; -use cdk::nuts::{ - CurrencyUnit, Id, MintBolt11Request, MintInfo, Nuts, PreMintSecrets, Proofs, SecretKey, - SpendingConditions, SwapRequest, -}; -use cdk::types::QuoteTTL; -use cdk::util::unix_time; -use cdk::Mint; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::OnceCell; - -pub const MINT_URL: &str = "http://127.0.0.1:8088"; - -static INSTANCE: OnceCell = OnceCell::const_new(); - -async fn new_mint(fee: u64) -> Mint { - let mut supported_units = HashMap::new(); - supported_units.insert(CurrencyUnit::Sat, (fee, 32)); - - let nuts = Nuts::new() - .nut07(true) - .nut08(true) - .nut09(true) - .nut10(true) - .nut11(true) - .nut12(true) - .nut14(true); - - let mint_info = MintInfo::new().nuts(nuts); - - let mnemonic = Mnemonic::generate(12).unwrap(); - - let quote_ttl = QuoteTTL::new(10000, 10000); - - Mint::new( - MINT_URL, - &mnemonic.to_seed_normalized(""), - mint_info, - quote_ttl, - Arc::new(MintMemoryDatabase::default()), - HashMap::new(), - supported_units, - ) - .await - .unwrap() -} - -async fn initialize() -> &'static Mint { - INSTANCE.get_or_init(|| new_mint(0)).await -} - -async fn mint_proofs( - mint: &Mint, - amount: Amount, - split_target: &SplitTarget, - keys: cdk::nuts::Keys, -) -> Result { - let request_lookup = uuid::Uuid::new_v4().to_string(); - - let quote = MintQuote::new( - mint.mint_url.clone(), - "".to_string(), - CurrencyUnit::Sat, - amount, - unix_time() + 36000, - request_lookup.to_string(), - ); - - mint.localstore.add_mint_quote(quote.clone()).await?; - - mint.pay_mint_quote_for_request_id(&request_lookup).await?; - let keyset_id = Id::from(&keys); - - let premint = PreMintSecrets::random(keyset_id, amount, split_target)?; - - let mint_request = MintBolt11Request { - quote: quote.id, - outputs: premint.blinded_messages(), - }; - - let after_mint = mint.process_mint_request(mint_request).await?; - - let proofs = construct_proofs( - after_mint.signatures, - premint.rs(), - premint.secrets(), - &keys, - )?; - - Ok(proofs) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_mint_double_spend() -> Result<()> { - let mint = initialize().await; - - let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys; - let keyset_id = Id::from(&keys); - - let proofs = mint_proofs(mint, 100.into(), &SplitTarget::default(), keys).await?; - - let preswap = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default())?; - - let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages()); - - let swap = mint.process_swap_request(swap_request).await; - - assert!(swap.is_ok()); - - let preswap_two = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default())?; - - let swap_two_request = SwapRequest::new(proofs, preswap_two.blinded_messages()); - - match mint.process_swap_request(swap_two_request).await { - Ok(_) => bail!("Proofs double spent"), - Err(err) => match err { - cdk::Error::TokenAlreadySpent => (), - _ => bail!("Wrong error returned"), - }, - } - - Ok(()) -} - -/// This attempts to swap for more outputs then inputs. -/// This will work if the mint does not check for outputs amounts overflowing -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_attempt_to_swap_by_overflowing() -> Result<()> { - let mint = initialize().await; - - let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys; - let keyset_id = Id::from(&keys); - - let proofs = mint_proofs(mint, 100.into(), &SplitTarget::default(), keys).await?; - - let amount = 2_u64.pow(63); - - let pre_mint_amount = - PreMintSecrets::random(keyset_id, amount.into(), &SplitTarget::default())?; - let pre_mint_amount_two = - PreMintSecrets::random(keyset_id, amount.into(), &SplitTarget::default())?; - - let mut pre_mint = PreMintSecrets::random(keyset_id, 1.into(), &SplitTarget::default())?; - - pre_mint.combine(pre_mint_amount); - pre_mint.combine(pre_mint_amount_two); - - let swap_request = SwapRequest::new(proofs.clone(), pre_mint.blinded_messages()); - - match mint.process_swap_request(swap_request).await { - Ok(_) => bail!("Swap occurred with overflow"), - Err(err) => match err { - cdk::Error::NUT03(cdk::nuts::nut03::Error::Amount(_)) => (), - _ => { - println!("{:?}", err); - bail!("Wrong error returned in swap overflow") - } - }, - } - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -pub async fn test_p2pk_swap() -> Result<()> { - let mint = initialize().await; - - let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys; - let keyset_id = Id::from(&keys); - - let proofs = mint_proofs(mint, 100.into(), &SplitTarget::default(), keys).await?; - - let secret = SecretKey::generate(); - - let spending_conditions = SpendingConditions::new_p2pk(secret.public_key(), None); - - let pre_swap = PreMintSecrets::with_conditions( - keyset_id, - 100.into(), - &SplitTarget::default(), - &spending_conditions, - )?; - - let swap_request = SwapRequest::new(proofs.clone(), pre_swap.blinded_messages()); - - let keys = mint.pubkeys().await?.keysets.first().cloned().unwrap().keys; - - let post_swap = mint.process_swap_request(swap_request).await?; - - let mut proofs = construct_proofs( - post_swap.signatures, - pre_swap.rs(), - pre_swap.secrets(), - &keys, - )?; - - let pre_swap = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default())?; - - let swap_request = SwapRequest::new(proofs.clone(), pre_swap.blinded_messages()); - - match mint.process_swap_request(swap_request).await { - Ok(_) => bail!("Proofs spent without sig"), - Err(err) => match err { - cdk::Error::NUT11(cdk::nuts::nut11::Error::SignaturesNotProvided) => (), - _ => { - println!("{:?}", err); - bail!("Wrong error returned") - } - }, - } - - for proof in &mut proofs { - proof.sign_p2pk(secret.clone())?; - } - - let swap_request = SwapRequest::new(proofs.clone(), pre_swap.blinded_messages()); - - let attempt_swap = mint.process_swap_request(swap_request).await; - - assert!(attempt_swap.is_ok()); - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_swap_unbalanced() -> Result<()> { - let mint = initialize().await; - - let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys; - let keyset_id = Id::from(&keys); - - let proofs = mint_proofs(mint, 100.into(), &SplitTarget::default(), keys).await?; - - let preswap = PreMintSecrets::random(keyset_id, 95.into(), &SplitTarget::default())?; - - let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages()); - - match mint.process_swap_request(swap_request).await { - Ok(_) => bail!("Swap was allowed unbalanced"), - Err(err) => match err { - cdk::Error::TransactionUnbalanced(_, _, _) => (), - _ => bail!("Wrong error returned"), - }, - } - - let preswap = PreMintSecrets::random(keyset_id, 101.into(), &SplitTarget::default())?; - - let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages()); - - match mint.process_swap_request(swap_request).await { - Ok(_) => bail!("Swap was allowed unbalanced"), - Err(err) => match err { - cdk::Error::TransactionUnbalanced(_, _, _) => (), - _ => bail!("Wrong error returned"), - }, - } - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_swap_overpay_underpay_fee() -> Result<()> { - let mint = new_mint(1).await; - - mint.rotate_keyset(CurrencyUnit::Sat, 1, 32, 1).await?; - - let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys; - let keyset_id = Id::from(&keys); - - let proofs = mint_proofs(&mint, 1000.into(), &SplitTarget::default(), keys).await?; - - let preswap = PreMintSecrets::random(keyset_id, 9998.into(), &SplitTarget::default())?; - - let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages()); - - // Attempt to swap overpaying fee - match mint.process_swap_request(swap_request).await { - Ok(_) => bail!("Swap was allowed unbalanced"), - Err(err) => match err { - cdk::Error::TransactionUnbalanced(_, _, _) => (), - _ => { - println!("{:?}", err); - bail!("Wrong error returned") - } - }, - } - - let preswap = PreMintSecrets::random(keyset_id, 1000.into(), &SplitTarget::default())?; - - let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages()); - - // Attempt to swap underpaying fee - match mint.process_swap_request(swap_request).await { - Ok(_) => bail!("Swap was allowed unbalanced"), - Err(err) => match err { - cdk::Error::TransactionUnbalanced(_, _, _) => (), - _ => { - println!("{:?}", err); - bail!("Wrong error returned") - } - }, - } - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_mint_enforce_fee() -> Result<()> { - let mint = new_mint(1).await; - - let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys; - let keyset_id = Id::from(&keys); - - let mut proofs = mint_proofs(&mint, 1010.into(), &SplitTarget::Value(1.into()), keys).await?; - - let five_proofs: Vec<_> = proofs.drain(..5).collect(); - - let preswap = PreMintSecrets::random(keyset_id, 5.into(), &SplitTarget::default())?; - - let swap_request = SwapRequest::new(five_proofs.clone(), preswap.blinded_messages()); - - // Attempt to swap underpaying fee - match mint.process_swap_request(swap_request).await { - Ok(_) => bail!("Swap was allowed unbalanced"), - Err(err) => match err { - cdk::Error::TransactionUnbalanced(_, _, _) => (), - _ => { - println!("{:?}", err); - bail!("Wrong error returned") - } - }, - } - - let preswap = PreMintSecrets::random(keyset_id, 4.into(), &SplitTarget::default())?; - - let swap_request = SwapRequest::new(five_proofs.clone(), preswap.blinded_messages()); - - let _ = mint.process_swap_request(swap_request).await?; - - let thousnad_proofs: Vec<_> = proofs.drain(..1001).collect(); - - let preswap = PreMintSecrets::random(keyset_id, 1000.into(), &SplitTarget::default())?; - - let swap_request = SwapRequest::new(thousnad_proofs.clone(), preswap.blinded_messages()); - - // Attempt to swap underpaying fee - match mint.process_swap_request(swap_request).await { - Ok(_) => bail!("Swap was allowed unbalanced"), - Err(err) => match err { - cdk::Error::TransactionUnbalanced(_, _, _) => (), - _ => { - println!("{:?}", err); - bail!("Wrong error returned") - } - }, - } - - let preswap = PreMintSecrets::random(keyset_id, 999.into(), &SplitTarget::default())?; - - let swap_request = SwapRequest::new(thousnad_proofs.clone(), preswap.blinded_messages()); - - let _ = mint.process_swap_request(swap_request).await?; - - Ok(()) -} diff --git a/crates/cdk-integration-tests/tests/regtest.rs b/crates/cdk-integration-tests/tests/regtest.rs deleted file mode 100644 index 5658fb57..00000000 --- a/crates/cdk-integration-tests/tests/regtest.rs +++ /dev/null @@ -1,305 +0,0 @@ -use std::{str::FromStr, sync::Arc, time::Duration}; - -use anyhow::{bail, Result}; -use bip39::Mnemonic; -use cdk::{ - amount::{Amount, SplitTarget}, - cdk_database::WalletMemoryDatabase, - nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState, PreMintSecrets, State}, - wallet::{client::HttpClient, Wallet}, -}; -use cdk_integration_tests::init_regtest::{get_mint_url, init_cln_client, init_lnd_client}; -use lightning_invoice::Bolt11Invoice; -use ln_regtest_rs::InvoiceStatus; -use tokio::time::sleep; - -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_regtest_mint_melt_round_trip() -> Result<()> { - let lnd_client = init_lnd_client().await.unwrap(); - - let wallet = Wallet::new( - &get_mint_url(), - CurrencyUnit::Sat, - Arc::new(WalletMemoryDatabase::default()), - &Mnemonic::generate(12)?.to_seed_normalized(""), - None, - )?; - - let mint_quote = wallet.mint_quote(100.into(), None).await?; - - lnd_client.pay_invoice(mint_quote.request).await?; - - let mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) - .await?; - - assert!(mint_amount == 100.into()); - - let invoice = lnd_client.create_invoice(50).await?; - - let melt = wallet.melt_quote(invoice, None).await?; - - let melt = wallet.melt(&melt.id).await.unwrap(); - - assert!(melt.preimage.is_some()); - - assert!(melt.state == MeltQuoteState::Paid); - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_regtest_mint_melt() -> Result<()> { - let lnd_client = init_lnd_client().await?; - - let wallet = Wallet::new( - &get_mint_url(), - CurrencyUnit::Sat, - Arc::new(WalletMemoryDatabase::default()), - &Mnemonic::generate(12)?.to_seed_normalized(""), - None, - )?; - - let mint_amount = Amount::from(100); - - let mint_quote = wallet.mint_quote(mint_amount, None).await?; - - assert_eq!(mint_quote.amount, mint_amount); - - lnd_client.pay_invoice(mint_quote.request).await?; - - let mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) - .await?; - - assert!(mint_amount == 100.into()); - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_restore() -> Result<()> { - let lnd_client = init_lnd_client().await?; - - let seed = Mnemonic::generate(12)?.to_seed_normalized(""); - let wallet = Wallet::new( - &get_mint_url(), - CurrencyUnit::Sat, - Arc::new(WalletMemoryDatabase::default()), - &seed, - None, - )?; - - let mint_quote = wallet.mint_quote(100.into(), None).await?; - - lnd_client.pay_invoice(mint_quote.request).await?; - - let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) - .await?; - - assert!(wallet.total_balance().await? == 100.into()); - - let wallet_2 = Wallet::new( - &get_mint_url(), - CurrencyUnit::Sat, - Arc::new(WalletMemoryDatabase::default()), - &seed, - None, - )?; - - assert!(wallet_2.total_balance().await? == 0.into()); - - let restored = wallet_2.restore().await?; - let proofs = wallet_2.get_proofs().await?; - - wallet_2 - .swap(None, SplitTarget::default(), proofs, None, false) - .await?; - - assert!(restored == 100.into()); - - assert!(wallet_2.total_balance().await? == 100.into()); - - let proofs = wallet.get_proofs().await?; - - let states = wallet.check_proofs_spent(proofs).await?; - - for state in states { - if state.state != State::Spent { - bail!("All proofs should be spent"); - } - } - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_pay_invoice_twice() -> Result<()> { - let lnd_client = init_lnd_client().await?; - let seed = Mnemonic::generate(12)?.to_seed_normalized(""); - let wallet = Wallet::new( - &get_mint_url(), - CurrencyUnit::Sat, - Arc::new(WalletMemoryDatabase::default()), - &seed, - None, - )?; - - let mint_quote = wallet.mint_quote(100.into(), None).await?; - - lnd_client.pay_invoice(mint_quote.request).await?; - - let mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) - .await?; - - assert_eq!(mint_amount, 100.into()); - - let invoice = lnd_client.create_invoice(10).await?; - - let melt_quote = wallet.melt_quote(invoice.clone(), None).await?; - - let melt = wallet.melt(&melt_quote.id).await.unwrap(); - - let melt_two = wallet.melt_quote(invoice, None).await?; - - let melt_two = wallet.melt(&melt_two.id).await; - - match melt_two { - Err(err) => match err { - cdk::Error::RequestAlreadyPaid => (), - _ => { - bail!("Wrong invoice already paid"); - } - }, - Ok(_) => { - bail!("Should not have allowed second payment"); - } - } - - let balance = wallet.total_balance().await?; - - assert_eq!(balance, (Amount::from(100) - melt.fee_paid - melt.amount)); - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_internal_payment() -> Result<()> { - let lnd_client = init_lnd_client().await?; - - let seed = Mnemonic::generate(12)?.to_seed_normalized(""); - let wallet = Wallet::new( - &get_mint_url(), - CurrencyUnit::Sat, - Arc::new(WalletMemoryDatabase::default()), - &seed, - None, - )?; - - let mint_quote = wallet.mint_quote(100.into(), None).await?; - - lnd_client.pay_invoice(mint_quote.request).await?; - - let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) - .await?; - - assert!(wallet.total_balance().await? == 100.into()); - - let seed = Mnemonic::generate(12)?.to_seed_normalized(""); - - let wallet_2 = Wallet::new( - &get_mint_url(), - CurrencyUnit::Sat, - Arc::new(WalletMemoryDatabase::default()), - &seed, - None, - )?; - - let mint_quote = wallet_2.mint_quote(10.into(), None).await?; - - let melt = wallet.melt_quote(mint_quote.request.clone(), None).await?; - - assert_eq!(melt.amount, 10.into()); - - let _melted = wallet.melt(&melt.id).await.unwrap(); - - let _wallet_2_mint = wallet_2 - .mint(&mint_quote.id, SplitTarget::default(), None) - .await - .unwrap(); - - let cln_client = init_cln_client().await?; - let payment_hash = Bolt11Invoice::from_str(&mint_quote.request)?; - let check_paid = cln_client - .check_incoming_invoice(payment_hash.payment_hash().to_string()) - .await?; - - match check_paid { - InvoiceStatus::Unpaid => (), - _ => { - bail!("Invoice has incorrect status: {:?}", check_paid); - } - } - - let wallet_2_balance = wallet_2.total_balance().await?; - - assert!(wallet_2_balance == 10.into()); - - let wallet_1_balance = wallet.total_balance().await?; - - assert!(wallet_1_balance == 90.into()); - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_cached_mint() -> Result<()> { - let lnd_client = init_lnd_client().await.unwrap(); - - let wallet = Wallet::new( - &get_mint_url(), - CurrencyUnit::Sat, - Arc::new(WalletMemoryDatabase::default()), - &Mnemonic::generate(12)?.to_seed_normalized(""), - None, - )?; - - let mint_amount = Amount::from(100); - - let quote = wallet.mint_quote(mint_amount, None).await?; - lnd_client.pay_invoice(quote.request).await?; - - loop { - let status = wallet.mint_quote_state("e.id).await.unwrap(); - - println!("Quote status: {}", status.state); - - if status.state == MintQuoteState::Paid { - break; - } - - sleep(Duration::from_secs(5)).await; - } - - let active_keyset_id = wallet.get_active_mint_keyset().await?.id; - let http_client = HttpClient::new(); - let premint_secrets = - PreMintSecrets::random(active_keyset_id, 31.into(), &SplitTarget::default()).unwrap(); - - let response = http_client - .post_mint( - get_mint_url().as_str().parse()?, - "e.id, - premint_secrets.clone(), - ) - .await?; - let response1 = http_client - .post_mint(get_mint_url().as_str().parse()?, "e.id, premint_secrets) - .await?; - - assert!(response == response1); - Ok(()) -} diff --git a/crates/cdk-lnbits/Cargo.toml b/crates/cdk-lnbits/Cargo.toml deleted file mode 100644 index ee9ba6d8..00000000 --- a/crates/cdk-lnbits/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "cdk-lnbits" -version = "0.4.0" -edition = "2021" -authors = ["CDK Developers"] -license = "MIT" -homepage = "https://github.com/cashubtc/cdk" -repository = "https://github.com/cashubtc/cdk.git" -rust-version = "1.63.0" # MSRV -description = "CDK ln backend for lnbits" - -[dependencies] -async-trait = "0.1" -anyhow = "1" -axum = "0.6.20" -bitcoin = { version = "0.32.2", default-features = false } -cdk = { path = "../cdk", version = "0.4.0", default-features = false, features = ["mint"] } -futures = { version = "0.3.28", default-features = false } -tokio = { version = "1", default-features = false } -tokio-util = { version = "0.7.11", default-features = false } -tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] } -thiserror = "1" -# lnbits-rs = "0.2.0" -# lnbits-rs = { path = "../../../../lnbits-rs" } -lnbits-rs = { git = "https://github.com/thesimplekid/lnbits-rs.git", rev = "9fff4d" } diff --git a/crates/cdk-lnbits/src/error.rs b/crates/cdk-lnbits/src/error.rs deleted file mode 100644 index c968376d..00000000 --- a/crates/cdk-lnbits/src/error.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Error for LNbits ln backend - -use thiserror::Error; - -/// LNbits Error -#[derive(Debug, Error)] -pub enum Error { - /// Invoice amount not defined - #[error("Unknown invoice amount")] - UnknownInvoiceAmount, - /// Unknown invoice - #[error("Unknown invoice")] - UnknownInvoice, - /// Anyhow error - #[error(transparent)] - Anyhow(#[from] anyhow::Error), -} - -impl From for cdk::cdk_lightning::Error { - fn from(e: Error) -> Self { - Self::Lightning(Box::new(e)) - } -} diff --git a/crates/cdk-lnbits/src/lib.rs b/crates/cdk-lnbits/src/lib.rs deleted file mode 100644 index 64e7bef7..00000000 --- a/crates/cdk-lnbits/src/lib.rs +++ /dev/null @@ -1,354 +0,0 @@ -//! CDK lightning backend for lnbits - -#![warn(missing_docs)] -#![warn(rustdoc::bare_urls)] - -use std::pin::Pin; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; - -use anyhow::anyhow; -use async_trait::async_trait; -use axum::Router; -use cdk::amount::{to_unit, Amount, MSAT_IN_SAT}; -use cdk::cdk_lightning::{ - self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, -}; -use cdk::mint::FeeReserve; -use cdk::nuts::{ - CurrencyUnit, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteState, MintMethodSettings, - MintQuoteState, -}; -use cdk::util::unix_time; -use cdk::{mint, Bolt11Invoice}; -use error::Error; -use futures::stream::StreamExt; -use futures::Stream; -use lnbits_rs::api::invoice::CreateInvoiceRequest; -use lnbits_rs::LNBitsClient; -use tokio::sync::Mutex; -use tokio_util::sync::CancellationToken; - -pub mod error; - -/// LNbits -#[derive(Clone)] -pub struct LNbits { - lnbits_api: LNBitsClient, - mint_settings: MintMethodSettings, - melt_settings: MeltMethodSettings, - fee_reserve: FeeReserve, - receiver: Arc>>>, - webhook_url: String, - wait_invoice_cancel_token: CancellationToken, - wait_invoice_is_active: Arc, -} - -impl LNbits { - /// Create new [`LNbits`] wallet - #[allow(clippy::too_many_arguments)] - pub async fn new( - admin_api_key: String, - invoice_api_key: String, - api_url: String, - mint_settings: MintMethodSettings, - melt_settings: MeltMethodSettings, - fee_reserve: FeeReserve, - receiver: Arc>>>, - webhook_url: String, - ) -> Result { - let lnbits_api = LNBitsClient::new("", &admin_api_key, &invoice_api_key, &api_url, None)?; - - Ok(Self { - lnbits_api, - mint_settings, - melt_settings, - receiver, - fee_reserve, - webhook_url, - wait_invoice_cancel_token: CancellationToken::new(), - wait_invoice_is_active: Arc::new(AtomicBool::new(false)), - }) - } -} - -#[async_trait] -impl MintLightning for LNbits { - type Err = cdk_lightning::Error; - - fn get_settings(&self) -> Settings { - Settings { - mpp: false, - unit: CurrencyUnit::Sat, - mint_settings: self.mint_settings, - melt_settings: self.melt_settings, - invoice_description: true, - } - } - - fn is_wait_invoice_active(&self) -> bool { - self.wait_invoice_is_active.load(Ordering::SeqCst) - } - - fn cancel_wait_invoice(&self) { - self.wait_invoice_cancel_token.cancel() - } - - #[allow(clippy::incompatible_msrv)] - async fn wait_any_invoice( - &self, - ) -> Result + Send>>, Self::Err> { - let receiver = self - .receiver - .lock() - .await - .take() - .ok_or(anyhow!("No receiver"))?; - - let lnbits_api = self.lnbits_api.clone(); - - let cancel_token = self.wait_invoice_cancel_token.clone(); - - Ok(futures::stream::unfold( - ( - receiver, - lnbits_api, - cancel_token, - Arc::clone(&self.wait_invoice_is_active), - ), - |(mut receiver, lnbits_api, cancel_token, is_active)| async move { - is_active.store(true, Ordering::SeqCst); - - tokio::select! { - _ = cancel_token.cancelled() => { - // Stream is cancelled - is_active.store(false, Ordering::SeqCst); - tracing::info!("Waiting for phonixd invoice ending"); - None - } - msg_option = receiver.recv() => { - match msg_option { - Some(msg) => { - let check = lnbits_api.is_invoice_paid(&msg).await; - - match check { - Ok(state) => { - if state { - Some((msg, (receiver, lnbits_api, cancel_token, is_active))) - } else { - None - } - } - _ => None, - } - } - None => { - is_active.store(true, Ordering::SeqCst); - None - }, - } - - } - } - }, - ) - .boxed()) - } - - async fn get_payment_quote( - &self, - melt_quote_request: &MeltQuoteBolt11Request, - ) -> Result { - if melt_quote_request.unit != CurrencyUnit::Sat { - return Err(Self::Err::Anyhow(anyhow!("Unsupported unit"))); - } - - let invoice_amount_msat = melt_quote_request - .request - .amount_milli_satoshis() - .ok_or(Error::UnknownInvoiceAmount)?; - - let amount = to_unit( - invoice_amount_msat, - &CurrencyUnit::Msat, - &melt_quote_request.unit, - )?; - - let relative_fee_reserve = - (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64; - - let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into(); - - let fee = match relative_fee_reserve > absolute_fee_reserve { - true => relative_fee_reserve, - false => absolute_fee_reserve, - }; - - Ok(PaymentQuoteResponse { - request_lookup_id: melt_quote_request.request.payment_hash().to_string(), - amount, - fee: fee.into(), - state: MeltQuoteState::Unpaid, - }) - } - - async fn pay_invoice( - &self, - melt_quote: mint::MeltQuote, - _partial_msats: Option, - _max_fee_msats: Option, - ) -> Result { - let pay_response = self - .lnbits_api - .pay_invoice(&melt_quote.request) - .await - .map_err(|err| { - tracing::error!("Could not pay invoice"); - tracing::error!("{}", err.to_string()); - Self::Err::Anyhow(anyhow!("Could not pay invoice")) - })?; - - let invoice_info = self - .lnbits_api - .find_invoice(&pay_response.payment_hash) - .await - .map_err(|err| { - tracing::error!("Could not find invoice"); - tracing::error!("{}", err.to_string()); - Self::Err::Anyhow(anyhow!("Could not find invoice")) - })?; - - let status = match invoice_info.pending { - true => MeltQuoteState::Unpaid, - false => MeltQuoteState::Paid, - }; - - let total_spent = Amount::from((invoice_info.amount + invoice_info.fee).unsigned_abs()); - - Ok(PayInvoiceResponse { - payment_lookup_id: pay_response.payment_hash, - payment_preimage: Some(invoice_info.payment_hash), - status, - total_spent, - unit: CurrencyUnit::Sat, - }) - } - - async fn create_invoice( - &self, - amount: Amount, - unit: &CurrencyUnit, - description: String, - unix_expiry: u64, - ) -> Result { - if unit != &CurrencyUnit::Sat { - return Err(Self::Err::Anyhow(anyhow!("Unsupported unit"))); - } - - let time_now = unix_time(); - assert!(unix_expiry > time_now); - - let expiry = unix_expiry - time_now; - - let invoice_request = CreateInvoiceRequest { - amount: to_unit(amount, unit, &CurrencyUnit::Sat)?.into(), - memo: Some(description), - unit: unit.to_string(), - expiry: Some(expiry), - webhook: Some(self.webhook_url.clone()), - internal: None, - out: false, - }; - - let create_invoice_response = self - .lnbits_api - .create_invoice(&invoice_request) - .await - .map_err(|err| { - tracing::error!("Could not create invoice"); - tracing::error!("{}", err.to_string()); - Self::Err::Anyhow(anyhow!("Could not create invoice")) - })?; - - let request: Bolt11Invoice = create_invoice_response.payment_request.parse()?; - let expiry = request.expires_at().map(|t| t.as_secs()); - - Ok(CreateInvoiceResponse { - request_lookup_id: create_invoice_response.payment_hash, - request, - expiry, - }) - } - - async fn check_incoming_invoice_status( - &self, - payment_hash: &str, - ) -> Result { - let paid = self - .lnbits_api - .is_invoice_paid(payment_hash) - .await - .map_err(|err| { - tracing::error!("Could not check invoice status"); - tracing::error!("{}", err.to_string()); - Self::Err::Anyhow(anyhow!("Could not check invoice status")) - })?; - - let state = match paid { - true => MintQuoteState::Paid, - false => MintQuoteState::Unpaid, - }; - - Ok(state) - } - - async fn check_outgoing_payment( - &self, - payment_hash: &str, - ) -> Result { - let payment = self - .lnbits_api - .get_payment_info(payment_hash) - .await - .map_err(|err| { - tracing::error!("Could not check invoice status"); - tracing::error!("{}", err.to_string()); - Self::Err::Anyhow(anyhow!("Could not check invoice status")) - })?; - - let pay_response = PayInvoiceResponse { - payment_lookup_id: payment.details.payment_hash, - payment_preimage: Some(payment.preimage), - status: lnbits_to_melt_status(&payment.details.status, payment.details.pending), - total_spent: Amount::from( - payment.details.amount.unsigned_abs() - + payment.details.fee.unsigned_abs() / MSAT_IN_SAT, - ), - unit: self.get_settings().unit, - }; - - Ok(pay_response) - } -} - -fn lnbits_to_melt_status(status: &str, pending: bool) -> MeltQuoteState { - match (status, pending) { - ("success", false) => MeltQuoteState::Paid, - ("failed", false) => MeltQuoteState::Unpaid, - (_, false) => MeltQuoteState::Unknown, - (_, true) => MeltQuoteState::Pending, - } -} - -impl LNbits { - /// Create invoice webhook - pub async fn create_invoice_webhook_router( - &self, - webhook_endpoint: &str, - sender: tokio::sync::mpsc::Sender, - ) -> anyhow::Result { - self.lnbits_api - .create_invoice_webhook_router(webhook_endpoint, sender) - .await - } -} diff --git a/crates/cdk-lnd/Cargo.toml b/crates/cdk-lnd/Cargo.toml deleted file mode 100644 index e0e99329..00000000 --- a/crates/cdk-lnd/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "cdk-lnd" -version = "0.4.0" -edition = "2021" -authors = ["CDK Developers"] -license = "MIT" -homepage = "https://github.com/cashubtc/cdk" -repository = "https://github.com/cashubtc/cdk.git" -description = "CDK ln backend for lnd" - -[dependencies] -async-trait = "0.1" -anyhow = "1" -cdk = { path = "../cdk", version= "0.4.0", default-features = false, features = ["mint"] } -fedimint-tonic-lnd = "0.2.0" -futures = { version = "0.3.28", default-features = false } -tokio = { version = "1", default-features = false } -tokio-util = { version = "0.7.11", default-features = false } -tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] } -thiserror = "1" diff --git a/crates/cdk-lnd/README.md b/crates/cdk-lnd/README.md deleted file mode 100644 index 010e5eec..00000000 --- a/crates/cdk-lnd/README.md +++ /dev/null @@ -1,14 +0,0 @@ - -## Minimum Supported Rust Version (MSRV) - -The `cdk` library should always compile with any combination of features on Rust **1.66.0**. - -To build and test with the MSRV you will need to pin the below dependency versions: - -```shell -cargo update -p home --precise 0.5.5 -cargo update -p prost --precise 0.12.3 -cargo update -p prost-types --precise 0.12.3 -cargo update -p prost-build --precise 0.12.3 -cargo update -p prost-derive --precise 0.12.3 -``` diff --git a/crates/cdk-lnd/src/error.rs b/crates/cdk-lnd/src/error.rs deleted file mode 100644 index 3b6f427b..00000000 --- a/crates/cdk-lnd/src/error.rs +++ /dev/null @@ -1,32 +0,0 @@ -//! LND Errors - -use thiserror::Error; - -/// LND Error -#[derive(Debug, Error)] -pub enum Error { - /// Invoice amount not defined - #[error("Unknown invoice amount")] - UnknownInvoiceAmount, - /// Unknown invoice - #[error("Unknown invoice")] - UnknownInvoice, - /// Connection error - #[error("LND connection error")] - Connection, - /// Invalid hash - #[error("Invalid hash")] - InvalidHash, - /// Payment failed - #[error("LND payment failed")] - PaymentFailed, - /// Unknown payment status - #[error("LND unknown payment status")] - UnknownPaymentStatus, -} - -impl From for cdk::cdk_lightning::Error { - fn from(e: Error) -> Self { - Self::Lightning(Box::new(e)) - } -} diff --git a/crates/cdk-lnd/src/lib.rs b/crates/cdk-lnd/src/lib.rs deleted file mode 100644 index c5ecbeb8..00000000 --- a/crates/cdk-lnd/src/lib.rs +++ /dev/null @@ -1,396 +0,0 @@ -//! CDK lightning backend for LND - -// Copyright (c) 2023 Steffen (MIT) - -#![warn(missing_docs)] -#![warn(rustdoc::bare_urls)] - -use std::path::PathBuf; -use std::pin::Pin; -use std::str::FromStr; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; - -use anyhow::anyhow; -use async_trait::async_trait; -use cdk::amount::{to_unit, Amount, MSAT_IN_SAT}; -use cdk::cdk_lightning::{ - self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, -}; -use cdk::mint::FeeReserve; -use cdk::nuts::{ - CurrencyUnit, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteState, MintMethodSettings, - MintQuoteState, -}; -use cdk::util::{hex, unix_time}; -use cdk::{mint, Bolt11Invoice}; -use error::Error; -use fedimint_tonic_lnd::lnrpc::fee_limit::Limit; -use fedimint_tonic_lnd::lnrpc::payment::PaymentStatus; -use fedimint_tonic_lnd::lnrpc::FeeLimit; -use fedimint_tonic_lnd::Client; -use futures::{Stream, StreamExt}; -use tokio::sync::Mutex; -use tokio_util::sync::CancellationToken; - -pub mod error; - -/// Lnd mint backend -#[derive(Clone)] -pub struct Lnd { - address: String, - cert_file: PathBuf, - macaroon_file: PathBuf, - client: Arc>, - fee_reserve: FeeReserve, - mint_settings: MintMethodSettings, - melt_settings: MeltMethodSettings, - wait_invoice_cancel_token: CancellationToken, - wait_invoice_is_active: Arc, -} - -impl Lnd { - /// Create new [`Lnd`] - pub async fn new( - address: String, - cert_file: PathBuf, - macaroon_file: PathBuf, - fee_reserve: FeeReserve, - mint_settings: MintMethodSettings, - melt_settings: MeltMethodSettings, - ) -> Result { - let client = fedimint_tonic_lnd::connect(address.to_string(), &cert_file, &macaroon_file) - .await - .map_err(|err| { - tracing::error!("Connection error: {}", err.to_string()); - Error::Connection - })?; - - Ok(Self { - address, - cert_file, - macaroon_file, - client: Arc::new(Mutex::new(client)), - fee_reserve, - mint_settings, - melt_settings, - wait_invoice_cancel_token: CancellationToken::new(), - wait_invoice_is_active: Arc::new(AtomicBool::new(false)), - }) - } -} - -#[async_trait] -impl MintLightning for Lnd { - type Err = cdk_lightning::Error; - - fn get_settings(&self) -> Settings { - Settings { - mpp: true, - unit: CurrencyUnit::Msat, - mint_settings: self.mint_settings, - melt_settings: self.melt_settings, - invoice_description: true, - } - } - - fn is_wait_invoice_active(&self) -> bool { - self.wait_invoice_is_active.load(Ordering::SeqCst) - } - - fn cancel_wait_invoice(&self) { - self.wait_invoice_cancel_token.cancel() - } - - async fn wait_any_invoice( - &self, - ) -> Result + Send>>, Self::Err> { - let mut client = - fedimint_tonic_lnd::connect(self.address.clone(), &self.cert_file, &self.macaroon_file) - .await - .map_err(|_| Error::Connection)?; - - let stream_req = fedimint_tonic_lnd::lnrpc::InvoiceSubscription { - add_index: 0, - settle_index: 0, - }; - - let stream = client - .lightning() - .subscribe_invoices(stream_req) - .await - .unwrap() - .into_inner(); - - let cancel_token = self.wait_invoice_cancel_token.clone(); - - Ok(futures::stream::unfold( - ( - stream, - cancel_token, - Arc::clone(&self.wait_invoice_is_active), - ), - |(mut stream, cancel_token, is_active)| async move { - is_active.store(true, Ordering::SeqCst); - - tokio::select! { - _ = cancel_token.cancelled() => { - // Stream is cancelled - is_active.store(false, Ordering::SeqCst); - tracing::info!("Waiting for lnd invoice ending"); - None - - } - msg = stream.message() => { - - match msg { - Ok(Some(msg)) => { - if msg.state == 1 { - Some((hex::encode(msg.r_hash), (stream, cancel_token, is_active))) - } else { - None - } - } - Ok(None) => { - is_active.store(false, Ordering::SeqCst); - tracing::info!("LND invoice stream ended."); - None - }, // End of stream - Err(err) => { - is_active.store(false, Ordering::SeqCst); - tracing::warn!("Encounrdered error in LND invoice stream. Stream ending"); - tracing::error!("{:?}", err); - None - - }, // Handle errors gracefully, ends the stream on error - } - } - } - }, - ) - .boxed()) - } - - async fn get_payment_quote( - &self, - melt_quote_request: &MeltQuoteBolt11Request, - ) -> Result { - let invoice_amount_msat = melt_quote_request - .request - .amount_milli_satoshis() - .ok_or(Error::UnknownInvoiceAmount)?; - - let amount = to_unit( - invoice_amount_msat, - &CurrencyUnit::Msat, - &melt_quote_request.unit, - )?; - - let relative_fee_reserve = - (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64; - - let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into(); - - let fee = match relative_fee_reserve > absolute_fee_reserve { - true => relative_fee_reserve, - false => absolute_fee_reserve, - }; - - Ok(PaymentQuoteResponse { - request_lookup_id: melt_quote_request.request.payment_hash().to_string(), - amount, - fee: fee.into(), - state: MeltQuoteState::Unpaid, - }) - } - - async fn pay_invoice( - &self, - melt_quote: mint::MeltQuote, - partial_amount: Option, - max_fee: Option, - ) -> Result { - let payment_request = melt_quote.request; - - let pay_req = fedimint_tonic_lnd::lnrpc::SendRequest { - payment_request, - fee_limit: max_fee.map(|f| { - let limit = Limit::Fixed(u64::from(f) as i64); - - FeeLimit { limit: Some(limit) } - }), - amt_msat: partial_amount - .map(|a| { - let msat = to_unit(a, &melt_quote.unit, &CurrencyUnit::Msat).unwrap(); - - u64::from(msat) as i64 - }) - .unwrap_or_default(), - ..Default::default() - }; - - let payment_response = self - .client - .lock() - .await - .lightning() - .send_payment_sync(fedimint_tonic_lnd::tonic::Request::new(pay_req)) - .await - .map_err(|_| Error::PaymentFailed)? - .into_inner(); - - let total_amount = payment_response - .payment_route - .map_or(0, |route| route.total_amt_msat / MSAT_IN_SAT as i64) - as u64; - - let (status, payment_preimage) = match total_amount == 0 { - true => (MeltQuoteState::Unpaid, None), - false => ( - MeltQuoteState::Paid, - Some(hex::encode(payment_response.payment_preimage)), - ), - }; - - Ok(PayInvoiceResponse { - payment_lookup_id: hex::encode(payment_response.payment_hash), - payment_preimage, - status, - total_spent: total_amount.into(), - unit: CurrencyUnit::Sat, - }) - } - - async fn create_invoice( - &self, - amount: Amount, - unit: &CurrencyUnit, - description: String, - unix_expiry: u64, - ) -> Result { - let time_now = unix_time(); - assert!(unix_expiry > time_now); - - let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?; - - let invoice_request = fedimint_tonic_lnd::lnrpc::Invoice { - value_msat: u64::from(amount) as i64, - memo: description, - ..Default::default() - }; - - let invoice = self - .client - .lock() - .await - .lightning() - .add_invoice(fedimint_tonic_lnd::tonic::Request::new(invoice_request)) - .await - .unwrap() - .into_inner(); - - let bolt11 = Bolt11Invoice::from_str(&invoice.payment_request)?; - - Ok(CreateInvoiceResponse { - request_lookup_id: bolt11.payment_hash().to_string(), - request: bolt11, - expiry: Some(unix_expiry), - }) - } - - async fn check_incoming_invoice_status( - &self, - request_lookup_id: &str, - ) -> Result { - let invoice_request = fedimint_tonic_lnd::lnrpc::PaymentHash { - r_hash: hex::decode(request_lookup_id).unwrap(), - ..Default::default() - }; - - let invoice = self - .client - .lock() - .await - .lightning() - .lookup_invoice(fedimint_tonic_lnd::tonic::Request::new(invoice_request)) - .await - .unwrap() - .into_inner(); - - match invoice.state { - // Open - 0 => Ok(MintQuoteState::Unpaid), - // Settled - 1 => Ok(MintQuoteState::Paid), - // Canceled - 2 => Ok(MintQuoteState::Unpaid), - // Accepted - 3 => Ok(MintQuoteState::Unpaid), - _ => Err(Self::Err::Anyhow(anyhow!("Invalid status"))), - } - } - - async fn check_outgoing_payment( - &self, - payment_hash: &str, - ) -> Result { - let track_request = fedimint_tonic_lnd::routerrpc::TrackPaymentRequest { - payment_hash: hex::decode(payment_hash).map_err(|_| Error::InvalidHash)?, - no_inflight_updates: true, - }; - let mut payment_stream = self - .client - .lock() - .await - .router() - .track_payment_v2(track_request) - .await - .unwrap() - .into_inner(); - - while let Some(update_result) = payment_stream.next().await { - match update_result { - Ok(update) => { - let status = update.status(); - - let response = match status { - PaymentStatus::Unknown => PayInvoiceResponse { - payment_lookup_id: payment_hash.to_string(), - payment_preimage: Some(update.payment_preimage), - status: MeltQuoteState::Unknown, - total_spent: Amount::ZERO, - unit: self.get_settings().unit, - }, - PaymentStatus::InFlight => { - // Continue waiting for the next update - continue; - } - PaymentStatus::Succeeded => PayInvoiceResponse { - payment_lookup_id: payment_hash.to_string(), - payment_preimage: Some(update.payment_preimage), - status: MeltQuoteState::Paid, - total_spent: Amount::from((update.value_sat + update.fee_sat) as u64), - unit: CurrencyUnit::Sat, - }, - PaymentStatus::Failed => PayInvoiceResponse { - payment_lookup_id: payment_hash.to_string(), - payment_preimage: Some(update.payment_preimage), - status: MeltQuoteState::Failed, - total_spent: Amount::ZERO, - unit: self.get_settings().unit, - }, - }; - - return Ok(response); - } - Err(_) => { - // Handle the case where the update itself is an error (e.g., stream failure) - return Err(Error::UnknownPaymentStatus.into()); - } - } - } - - // If the stream is exhausted without a final status - Err(Error::UnknownPaymentStatus.into()) - } -} diff --git a/crates/cdk-mintd/Cargo.toml b/crates/cdk-mintd/Cargo.toml index c61db669..41b1555e 100644 --- a/crates/cdk-mintd/Cargo.toml +++ b/crates/cdk-mintd/Cargo.toml @@ -14,13 +14,7 @@ anyhow = "1" axum = "0.6.20" cdk = { path = "../cdk", version = "0.4.0", default-features = false, features = ["mint"] } cdk-redb = { path = "../cdk-redb", version = "0.4.0", default-features = false, features = ["mint"] } -cdk-sqlite = { path = "../cdk-sqlite", version = "0.4.0", default-features = false, features = ["mint"] } -cdk-cln = { path = "../cdk-cln", version = "0.4.0", default-features = false } -cdk-lnbits = { path = "../cdk-lnbits", version = "0.4.0", default-features = false } -cdk-phoenixd = { path = "../cdk-phoenixd", version = "0.4.0", default-features = false } -cdk-lnd = { path = "../cdk-lnd", version = "0.4.0", default-features = false } cdk-fake-wallet = { path = "../cdk-fake-wallet", version = "0.4.0", default-features = false } -cdk-strike = { path = "../cdk-strike", version = "0.4.0" } cdk-axum = { path = "../cdk-axum", version = "0.4.0", default-features = false } config = { version = "0.13.3", features = ["toml"] } clap = { version = "4.4.8", features = ["derive", "env", "default"] } diff --git a/crates/cdk-mintd/src/config.rs b/crates/cdk-mintd/src/config.rs index 756c2d14..fc3e38b4 100644 --- a/crates/cdk-mintd/src/config.rs +++ b/crates/cdk-mintd/src/config.rs @@ -21,12 +21,7 @@ pub struct Info { #[serde(rename_all = "lowercase")] pub enum LnBackend { #[default] - Cln, - Strike, - LNbits, FakeWallet, - Phoenixd, - Lnd, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] @@ -37,37 +32,6 @@ pub struct Ln { pub reserve_fee_min: Amount, } -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct Strike { - pub api_key: String, - pub supported_units: Option>, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct LNbits { - pub admin_api_key: String, - pub invoice_api_key: String, - pub lnbits_api: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct Cln { - pub rpc_path: PathBuf, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct Lnd { - pub address: String, - pub cert_file: PathBuf, - pub macaroon_file: PathBuf, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct Phoenixd { - pub api_password: String, - pub api_url: String, -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FakeWallet { pub supported_units: Vec, @@ -99,11 +63,6 @@ pub struct Settings { pub info: Info, pub mint_info: MintInfo, pub ln: Ln, - pub cln: Option, - pub strike: Option, - pub lnbits: Option, - pub phoenixd: Option, - pub lnd: Option, pub fake_wallet: Option, pub database: Database, } @@ -165,15 +124,6 @@ impl Settings { .build()?; let settings: Settings = config.try_deserialize()?; - match settings.ln.ln_backend { - LnBackend::Cln => assert!(settings.cln.is_some()), - LnBackend::Strike => assert!(settings.strike.is_some()), - LnBackend::LNbits => assert!(settings.lnbits.is_some()), - LnBackend::Phoenixd => assert!(settings.phoenixd.is_some()), - LnBackend::Lnd => assert!(settings.lnd.is_some()), - LnBackend::FakeWallet => (), - } - Ok(settings) } } diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index 6462236b..2c73858a 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -11,31 +11,22 @@ use std::sync::Arc; use anyhow::{anyhow, bail, Result}; use axum::Router; use bip39::Mnemonic; -use cdk::cdk_database::{self, MintDatabase}; use cdk::cdk_lightning; use cdk::cdk_lightning::MintLightning; use cdk::mint::{FeeReserve, MeltQuote, Mint}; use cdk::mint_url::MintUrl; use cdk::nuts::{ - nut04, nut05, ContactInfo, CurrencyUnit, MeltMethodSettings, MeltQuoteState, MintInfo, - MintMethodSettings, MintVersion, MppMethodSettings, Nuts, PaymentMethod, + nut04, nut05, ContactInfo, MeltMethodSettings, MeltQuoteState, MintInfo, MintMethodSettings, + MintVersion, MppMethodSettings, Nuts, PaymentMethod, }; use cdk::types::{LnKey, QuoteTTL}; -use cdk_cln::Cln; use cdk_fake_wallet::FakeWallet; -use cdk_lnbits::LNbits; -use cdk_lnd::Lnd; -use cdk_phoenixd::Phoenixd; use cdk_redb::MintRedbDatabase; -use cdk_sqlite::MintSqliteDatabase; -use cdk_strike::Strike; use clap::Parser; use cli::CLIArgs; -use config::{DatabaseEngine, LnBackend}; -use tokio::sync::{Mutex, Notify}; +use tokio::sync::Notify; use tower_http::cors::CorsLayer; use tracing_subscriber::EnvFilter; -use url::Url; mod cli; mod config; @@ -74,21 +65,8 @@ async fn main() -> anyhow::Result<()> { let settings = config::Settings::new(&Some(config_file_arg)); - let localstore: Arc + Send + Sync> = - match settings.database.engine { - DatabaseEngine::Sqlite => { - let sql_db_path = work_dir.join("cdk-mintd.sqlite"); - let sqlite_db = MintSqliteDatabase::new(&sql_db_path).await?; - - sqlite_db.migrate().await; - - Arc::new(sqlite_db) - } - DatabaseEngine::Redb => { - let redb_path = work_dir.join("cdk-mintd.redb"); - Arc::new(MintRedbDatabase::new(&redb_path)?) - } - }; + let redb_path = work_dir.join("cdk-mintd.redb"); + let localstore = Arc::new(MintRedbDatabase::new(&redb_path)?); let mut contact_info: Option> = None; @@ -138,211 +116,26 @@ async fn main() -> anyhow::Result<()> { let mut supported_units = HashMap::new(); let input_fee_ppk = settings.info.input_fee_ppk.unwrap_or(0); - let mint_url: MintUrl = settings.info.url.parse()?; - - let ln_routers: Vec = match settings.ln.ln_backend { - LnBackend::Cln => { - let cln_socket = expand_path( - settings - .cln - .expect("Config checked at load that cln is some") - .rpc_path - .to_str() - .ok_or(anyhow!("cln socket not defined"))?, - ) - .ok_or(anyhow!("cln socket not defined"))?; - let cln = Arc::new( - Cln::new( - cln_socket, - fee_reserve, - MintMethodSettings::default(), - MeltMethodSettings::default(), - ) - .await?, - ); - - ln_backends.insert(LnKey::new(CurrencyUnit::Sat, PaymentMethod::Bolt11), cln); - supported_units.insert(CurrencyUnit::Sat, (input_fee_ppk, 64)); - vec![] - } - LnBackend::Strike => { - let strike_settings = settings.strike.expect("Checked on config load"); - let api_key = strike_settings.api_key; - - let units = strike_settings - .supported_units - .unwrap_or(vec![CurrencyUnit::Sat]); - - let mut routers = vec![]; - - for unit in units { - // Channel used for strike web hook - let (sender, receiver) = tokio::sync::mpsc::channel(8); - let webhook_endpoint = format!("/webhook/{}/invoice", unit); - - let webhook_url = mint_url.join(&webhook_endpoint)?; - - let strike = Strike::new( - api_key.clone(), - MintMethodSettings::default(), - MeltMethodSettings::default(), - unit, - Arc::new(Mutex::new(Some(receiver))), - webhook_url.to_string(), - ) - .await?; - - let router = strike - .create_invoice_webhook(&webhook_endpoint, sender) - .await?; - routers.push(router); - - let ln_key = LnKey::new(unit, PaymentMethod::Bolt11); - - ln_backends.insert(ln_key, Arc::new(strike)); - - supported_units.insert(unit, (input_fee_ppk, 64)); - } - - routers - } - LnBackend::LNbits => { - let lnbits_settings = settings.lnbits.expect("Checked on config load"); - let admin_api_key = lnbits_settings.admin_api_key; - let invoice_api_key = lnbits_settings.invoice_api_key; - - // Channel used for lnbits web hook - let (sender, receiver) = tokio::sync::mpsc::channel(8); - let webhook_endpoint = "/webhook/lnbits/sat/invoice"; - - let webhook_url = mint_url.join(webhook_endpoint)?; - - let lnbits = LNbits::new( - admin_api_key, - invoice_api_key, - lnbits_settings.lnbits_api, - MintMethodSettings::default(), - MeltMethodSettings::default(), - fee_reserve, - Arc::new(Mutex::new(Some(receiver))), - webhook_url.to_string(), - ) - .await?; - - let router = lnbits - .create_invoice_webhook_router(webhook_endpoint, sender) - .await?; - - let unit = CurrencyUnit::Sat; - - let ln_key = LnKey::new(unit, PaymentMethod::Bolt11); + let _mint_url: MintUrl = settings.info.url.parse()?; + // Consider: we probably need only one unit, so this element might be redundant + let units = settings.fake_wallet.unwrap_or_default().supported_units; - ln_backends.insert(ln_key, Arc::new(lnbits)); - - supported_units.insert(unit, (input_fee_ppk, 64)); - vec![router] - } - LnBackend::Phoenixd => { - let api_password = settings - .clone() - .phoenixd - .expect("Checked at config load") - .api_password; - - let api_url = settings - .clone() - .phoenixd - .expect("Checked at config load") - .api_url; - - if fee_reserve.percent_fee_reserve < 0.04 { - bail!("Fee reserve is too low needs to be at least 0.02"); - } + for unit in units { + let ln_key = LnKey::new(unit, PaymentMethod::Bolt11); - let webhook_endpoint = "/webhook/phoenixd"; + let wallet = Arc::new(FakeWallet::new( + fee_reserve.clone(), + MintMethodSettings::default(), + MeltMethodSettings::default(), + HashMap::default(), + HashSet::default(), + 0, + )); - let mint_url = Url::parse(&settings.info.url)?; + ln_backends.insert(ln_key, wallet); - let webhook_url = mint_url.join(webhook_endpoint)?.to_string(); - - let (sender, receiver) = tokio::sync::mpsc::channel(8); - - let phoenixd = Phoenixd::new( - api_password.to_string(), - api_url.to_string(), - MintMethodSettings::default(), - MeltMethodSettings::default(), - fee_reserve, - Arc::new(Mutex::new(Some(receiver))), - webhook_url, - )?; - - let router = phoenixd - .create_invoice_webhook(webhook_endpoint, sender) - .await?; - - supported_units.insert(CurrencyUnit::Sat, (input_fee_ppk, 64)); - ln_backends.insert( - LnKey { - unit: CurrencyUnit::Sat, - method: PaymentMethod::Bolt11, - }, - Arc::new(phoenixd), - ); - - vec![router] - } - LnBackend::Lnd => { - let lnd_settings = settings.lnd.expect("Checked at config load"); - - let address = lnd_settings.address; - let cert_file = lnd_settings.cert_file; - let macaroon_file = lnd_settings.macaroon_file; - - let lnd = Lnd::new( - address, - cert_file, - macaroon_file, - fee_reserve, - MintMethodSettings::default(), - MeltMethodSettings::default(), - ) - .await?; - - supported_units.insert(CurrencyUnit::Sat, (input_fee_ppk, 64)); - ln_backends.insert( - LnKey { - unit: CurrencyUnit::Sat, - method: PaymentMethod::Bolt11, - }, - Arc::new(lnd), - ); - - vec![] - } - LnBackend::FakeWallet => { - let units = settings.fake_wallet.unwrap_or_default().supported_units; - - for unit in units { - let ln_key = LnKey::new(unit, PaymentMethod::Bolt11); - - let wallet = Arc::new(FakeWallet::new( - fee_reserve.clone(), - MintMethodSettings::default(), - MeltMethodSettings::default(), - HashMap::default(), - HashSet::default(), - 0, - )); - - ln_backends.insert(ln_key, wallet); - - supported_units.insert(unit, (input_fee_ppk, 64)); - } - - vec![] - } - }; + supported_units.insert(unit, (input_fee_ppk, 64)); + } let (nut04_settings, nut05_settings, mpp_settings): ( nut04::Settings, @@ -471,14 +264,10 @@ async fn main() -> anyhow::Result<()> { let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint), cache_ttl, cache_tti).await?; - let mut mint_service = Router::new() + let mint_service = Router::new() .merge(v1_service) .layer(CorsLayer::permissive()); - for router in ln_routers { - mint_service = mint_service.merge(router); - } - let shutdown = Arc::new(Notify::new()); tokio::spawn({ @@ -633,21 +422,6 @@ async fn check_pending_melt_quotes( Ok(()) } -fn expand_path(path: &str) -> Option { - if path.starts_with('~') { - if let Some(home_dir) = home::home_dir().as_mut() { - let remainder = &path[2..]; - home_dir.push(remainder); - let expanded_path = home_dir; - Some(expanded_path.clone()) - } else { - None - } - } else { - Some(PathBuf::from(path)) - } -} - fn work_dir() -> Result { let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?; diff --git a/crates/cdk-phoenixd/Cargo.toml b/crates/cdk-phoenixd/Cargo.toml deleted file mode 100644 index cfeb98b8..00000000 --- a/crates/cdk-phoenixd/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "cdk-phoenixd" -version = "0.4.0" -edition = "2021" -authors = ["CDK Developers"] -license = "MIT" -homepage = "https://github.com/cashubtc/cdk" -repository = "https://github.com/cashubtc/cdk.git" -rust-version = "1.63.0" # MSRV -description = "CDK ln backend for phoenixd" - -[dependencies] -async-trait = "0.1" -anyhow = "1" -axum = "0.6.20" -bitcoin = { version = "0.32.2", default-features = false } -cdk = { path = "../cdk", version = "0.4.0", default-features = false, features = ["mint"] } -futures = { version = "0.3.28", default-features = false } -tokio = { version = "1", default-features = false } -tokio-util = { version = "0.7.11", default-features = false } -tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] } -thiserror = "1" -# phoenixd-rs = "0.3.0" -phoenixd-rs = { git = "https://github.com/thesimplekid/phoenixd-rs", rev = "22a44f0"} -uuid = { version = "1", features = ["v4"] } diff --git a/crates/cdk-phoenixd/README.md b/crates/cdk-phoenixd/README.md deleted file mode 100644 index 046566f9..00000000 --- a/crates/cdk-phoenixd/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# cdk-phoenixd - -## Run phoenixd - -The `phoenixd` node is included in the cdk and needs to be run separately. -Get started here: [Phoenixd Server Documentation](https://phoenix.acinq.co/server/get-started) - -## Start Phoenixd - -By default, `phoenixd` will run with auto-liquidity enabled. While this simplifies channel management, it makes fees non-deterministic, which is not recommended for most scenarios. However, it is necessary to start with auto-liquidity enabled in order to open a channel and get started. - -Start the node with auto-liquidity enabled as documented by [Phoenixd](https://phoenix.acinq.co/server/get-started): -```sh -./phoenixd -``` - -> **Note:** By default the `auto-liquidity` will open a channel of 2m sats depending on the size of mint you plan to run you may want to increase this by setting the `--auto-liquidity` flag to `5m` or `10m`. - -## Open Channel - -Once the node is running, create an invoice using the phoenixd-cli to fund your node. A portion of this deposit will go to ACINQ as a fee for the provided liquidity, and a portion will cover the mining fee. These two fees cannot be refunded or withdrawn from the node. More on fees can be found [here](https://phoenix.acinq.co/server/auto-liquidity#fees). The remainder will stay as the node balance and can be withdrawn later. -```sh -./phoenix-cli createinvoice \ - --description "Fund Node" \ - --amountSat xxxxx -``` - -> **Note:** The amount above should be set depending on the size of the mint you would like to run as it will determine the size of the channel and amount of liquidity. - -## Check Channel state - -After paying the invoice view that a channal has been opened. -```sh -./phoenix-cli listchannels -``` - -## Restart Phoenixd without `auto-liquidity` - -Now that the node has a channel, it is recommended to stop the node and restart it without auto-liquidity. This will prevent phoenixd from opening new channels and incurring additional fees. -```sh -./phoenixd --auto-liquidity off -``` - -## Start cashu-mintd - -Once the node is running following the [cashu-mintd](../cdk-mintd/README.md) to start the mint. by default the `api_url` will be `http://127.0.0.1:9740` and the `api_password` can be found in `~/.phoenix/phoenix.conf` these will need to be set in the `cdk-mintd` config file. diff --git a/crates/cdk-phoenixd/src/error.rs b/crates/cdk-phoenixd/src/error.rs deleted file mode 100644 index 85e56c4e..00000000 --- a/crates/cdk-phoenixd/src/error.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Error for phoenixd ln backend - -use thiserror::Error; - -/// Phoenixd Error -#[derive(Debug, Error)] -pub enum Error { - /// Invoice amount not defined - #[error("Unknown invoice amount")] - UnknownInvoiceAmount, - /// Unknown invoice - #[error("Unknown invoice")] - UnknownInvoice, - /// Unsupported unit - #[error("Unit Unsupported")] - UnsupportedUnit, - /// phd error - #[error(transparent)] - Phd(#[from] phoenixd_rs::Error), - /// Anyhow error - #[error(transparent)] - Anyhow(#[from] anyhow::Error), -} - -impl From for cdk::cdk_lightning::Error { - fn from(e: Error) -> Self { - Self::Lightning(Box::new(e)) - } -} diff --git a/crates/cdk-phoenixd/src/lib.rs b/crates/cdk-phoenixd/src/lib.rs deleted file mode 100644 index 4aecfac6..00000000 --- a/crates/cdk-phoenixd/src/lib.rs +++ /dev/null @@ -1,317 +0,0 @@ -//! CDK lightning backend for Phoenixd - -#![warn(missing_docs)] -#![warn(rustdoc::bare_urls)] - -use std::pin::Pin; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; - -use anyhow::anyhow; -use async_trait::async_trait; -use axum::Router; -use cdk::amount::{to_unit, Amount, MSAT_IN_SAT}; -use cdk::cdk_lightning::{ - self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, -}; -use cdk::mint::FeeReserve; -use cdk::nuts::{ - CurrencyUnit, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteState, MintMethodSettings, - MintQuoteState, -}; -use cdk::{mint, Bolt11Invoice}; -use error::Error; -use futures::{Stream, StreamExt}; -use phoenixd_rs::webhooks::WebhookResponse; -use phoenixd_rs::{InvoiceRequest, Phoenixd as PhoenixdApi}; -use tokio::sync::Mutex; -use tokio_util::sync::CancellationToken; - -pub mod error; - -/// Phoenixd -#[derive(Clone)] -pub struct Phoenixd { - mint_settings: MintMethodSettings, - melt_settings: MeltMethodSettings, - phoenixd_api: PhoenixdApi, - fee_reserve: FeeReserve, - receiver: Arc>>>, - webhook_url: String, - wait_invoice_cancel_token: CancellationToken, - wait_invoice_is_active: Arc, -} - -impl Phoenixd { - /// Create new [`Phoenixd`] wallet - pub fn new( - api_password: String, - api_url: String, - mint_settings: MintMethodSettings, - melt_settings: MeltMethodSettings, - fee_reserve: FeeReserve, - receiver: Arc>>>, - webhook_url: String, - ) -> Result { - let phoenixd = PhoenixdApi::new(&api_password, &api_url)?; - Ok(Self { - mint_settings, - melt_settings, - phoenixd_api: phoenixd, - fee_reserve, - receiver, - webhook_url, - wait_invoice_cancel_token: CancellationToken::new(), - wait_invoice_is_active: Arc::new(AtomicBool::new(false)), - }) - } - - /// Create invoice webhook - pub async fn create_invoice_webhook( - &self, - webhook_endpoint: &str, - sender: tokio::sync::mpsc::Sender, - ) -> anyhow::Result { - self.phoenixd_api - .create_invoice_webhook_router(webhook_endpoint, sender) - .await - } -} - -#[async_trait] -impl MintLightning for Phoenixd { - type Err = cdk_lightning::Error; - - fn get_settings(&self) -> Settings { - Settings { - mpp: false, - unit: CurrencyUnit::Sat, - mint_settings: self.mint_settings, - melt_settings: self.melt_settings, - invoice_description: true, - } - } - - fn is_wait_invoice_active(&self) -> bool { - self.wait_invoice_is_active.load(Ordering::SeqCst) - } - - fn cancel_wait_invoice(&self) { - self.wait_invoice_cancel_token.cancel() - } - - #[allow(clippy::incompatible_msrv)] - async fn wait_any_invoice( - &self, - ) -> Result + Send>>, Self::Err> { - let receiver = self - .receiver - .lock() - .await - .take() - .ok_or(anyhow!("No receiver"))?; - - let phoenixd_api = self.phoenixd_api.clone(); - - let cancel_token = self.wait_invoice_cancel_token.clone(); - - Ok(futures::stream::unfold( - (receiver, phoenixd_api, cancel_token, - Arc::clone(&self.wait_invoice_is_active), - ), - |(mut receiver, phoenixd_api, cancel_token, is_active)| async move { - - is_active.store(true, Ordering::SeqCst); - tokio::select! { - _ = cancel_token.cancelled() => { - // Stream is cancelled - is_active.store(false, Ordering::SeqCst); - tracing::info!("Waiting for phonixd invoice ending"); - None - } - msg_option = receiver.recv() => { - match msg_option { - Some(msg) => { - let check = phoenixd_api.get_incoming_invoice(&msg.payment_hash).await; - - match check { - Ok(state) => { - if state.is_paid { - // Yield the payment hash and continue the stream - Some((msg.payment_hash, (receiver, phoenixd_api, cancel_token, is_active))) - } else { - // Invoice not paid yet, continue waiting - // We need to continue the stream, so we return the same state - None - } - } - Err(e) => { - // Log the error and continue - tracing::warn!("Error checking invoice state: {:?}", e); - None - } - } - } - None => { - // The receiver stream has ended - None - } - } - } - } - }, - ) - .boxed()) - } - - async fn get_payment_quote( - &self, - melt_quote_request: &MeltQuoteBolt11Request, - ) -> Result { - if CurrencyUnit::Sat != melt_quote_request.unit { - return Err(Error::UnsupportedUnit.into()); - } - - let invoice_amount_msat = melt_quote_request - .request - .amount_milli_satoshis() - .ok_or(Error::UnknownInvoiceAmount)?; - - let amount = to_unit( - invoice_amount_msat, - &CurrencyUnit::Msat, - &melt_quote_request.unit, - )?; - - let relative_fee_reserve = - (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64; - - let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into(); - - let mut fee = match relative_fee_reserve > absolute_fee_reserve { - true => relative_fee_reserve, - false => absolute_fee_reserve, - }; - - // Fee in phoenixd is always 0.04 + 4 sat - fee += 4; - - Ok(PaymentQuoteResponse { - request_lookup_id: melt_quote_request.request.payment_hash().to_string(), - amount, - fee: fee.into(), - state: MeltQuoteState::Unpaid, - }) - } - - async fn pay_invoice( - &self, - melt_quote: mint::MeltQuote, - partial_amount: Option, - _max_fee_msats: Option, - ) -> Result { - let pay_response = self - .phoenixd_api - .pay_bolt11_invoice(&melt_quote.request, partial_amount.map(|a| a.into())) - .await?; - - // The pay invoice response does not give the needed fee info so we have to check. - let check_outgoing_response = self - .check_outgoing_payment(&pay_response.payment_id) - .await?; - - let bolt11: Bolt11Invoice = melt_quote.request.parse()?; - - Ok(PayInvoiceResponse { - payment_lookup_id: bolt11.payment_hash().to_string(), - payment_preimage: Some(pay_response.payment_preimage), - status: MeltQuoteState::Paid, - total_spent: check_outgoing_response.total_spent, - unit: CurrencyUnit::Sat, - }) - } - - async fn create_invoice( - &self, - amount: Amount, - unit: &CurrencyUnit, - description: String, - _unix_expiry: u64, - ) -> Result { - let amount_sat = to_unit(amount, unit, &CurrencyUnit::Sat)?; - - let invoice_request = InvoiceRequest { - external_id: None, - description: Some(description), - description_hash: None, - amount_sat: amount_sat.into(), - webhook_url: Some(self.webhook_url.clone()), - }; - - let create_invoice_response = self.phoenixd_api.create_invoice(invoice_request).await?; - - let bolt11: Bolt11Invoice = create_invoice_response.serialized.parse()?; - let expiry = bolt11.expires_at().map(|t| t.as_secs()); - - Ok(CreateInvoiceResponse { - request_lookup_id: create_invoice_response.payment_hash, - request: bolt11.clone(), - expiry, - }) - } - - async fn check_incoming_invoice_status( - &self, - payment_hash: &str, - ) -> Result { - let invoice = self.phoenixd_api.get_incoming_invoice(payment_hash).await?; - - let state = match invoice.is_paid { - true => MintQuoteState::Paid, - false => MintQuoteState::Unpaid, - }; - - Ok(state) - } - - /// Check the status of an outgoing invoice - async fn check_outgoing_payment( - &self, - payment_id: &str, - ) -> Result { - let res = self.phoenixd_api.get_outgoing_invoice(payment_id).await; - - let state = match res { - Ok(res) => { - let status = match res.is_paid { - true => MeltQuoteState::Paid, - false => MeltQuoteState::Unpaid, - }; - - let total_spent = res.sent + (res.fees + 999) / MSAT_IN_SAT; - - PayInvoiceResponse { - payment_lookup_id: res.payment_hash, - payment_preimage: Some(res.preimage), - status, - total_spent: total_spent.into(), - unit: CurrencyUnit::Sat, - } - } - Err(err) => match err { - phoenixd_rs::Error::NotFound => PayInvoiceResponse { - payment_lookup_id: payment_id.to_string(), - payment_preimage: None, - status: MeltQuoteState::Unknown, - total_spent: Amount::ZERO, - unit: CurrencyUnit::Sat, - }, - _ => { - return Err(Error::from(err).into()); - } - }, - }; - - Ok(state) - } -} diff --git a/crates/cdk-rexie/Cargo.toml b/crates/cdk-rexie/Cargo.toml deleted file mode 100644 index 671ea7cf..00000000 --- a/crates/cdk-rexie/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "cdk-rexie" -version = "0.4.0" -edition = "2021" -authors = ["CDK Developers"] -description = "Indexdb storage backend for CDK in the browser" -license = "MIT" -homepage = "https://github.com/cashubtc/cdk" -repository = "https://github.com/cashubtc/cdk.git" -rust-version = "1.63.0" # MSRV - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[features] -default = ["wallet"] -wallet = ["cdk/wallet"] - -[dependencies] -rexie = "0.6.0" -cdk = { path = "../cdk", version = "0.4.0", default-features = false } -async-trait = "0.1.74" -tokio = { version = "1", default-features = false } -serde = { version = "1", default-features = false, features = ["derive"] } -serde_json = "1" -thiserror = "1" -serde-wasm-bindgen = "0.6.5" -web-sys = { version = "0.3.69", default-features = false, features = ["console"] } diff --git a/crates/cdk-rexie/README.md b/crates/cdk-rexie/README.md deleted file mode 100644 index 9203ce14..00000000 --- a/crates/cdk-rexie/README.md +++ /dev/null @@ -1,22 +0,0 @@ - -# Cashu Development Kit Rexie Storage Backend - -**ALPHA** This library is in early development, the api will change and should be used with caution. - -cdk-rexie is the [rexie](https://docs.rs/rexie/latest/rexie/) storage backend for wasm cdk wallets in the browser. - -## Crate Feature Flags - -The following crate feature flags are available: - -| Feature | Default | Description | -|-------------|:-------:|------------------------------------| -| `wallet` | Yes | Enable cashu wallet features | - -## Implemented [NUTs](https://github.com/cashubtc/nuts/): - -See - -## License - -This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details. diff --git a/crates/cdk-rexie/src/lib.rs b/crates/cdk-rexie/src/lib.rs deleted file mode 100644 index fca5caad..00000000 --- a/crates/cdk-rexie/src/lib.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Rexie Indexdb database - -#![warn(missing_docs)] -#![warn(rustdoc::bare_urls)] - -#[cfg(all(feature = "wallet", target_arch = "wasm32"))] -pub mod wallet; - -#[cfg(all(feature = "wallet", target_arch = "wasm32"))] -pub use wallet::WalletRexieDatabase; diff --git a/crates/cdk-rexie/src/wallet.rs b/crates/cdk-rexie/src/wallet.rs deleted file mode 100644 index 436ad327..00000000 --- a/crates/cdk-rexie/src/wallet.rs +++ /dev/null @@ -1,800 +0,0 @@ -//! Rexie Browser Database - -use std::collections::{HashMap, HashSet}; -use std::rc::Rc; -use std::result::Result; - -use async_trait::async_trait; -use cdk::cdk_database::{self, WalletDatabase}; -use cdk::mint_url::MintUrl; -use cdk::nuts::{ - CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PublicKey, SpendingConditions, State, -}; -use cdk::types::ProofInfo; -use cdk::util::unix_time; -use cdk::wallet::{MeltQuote, MintQuote}; -use rexie::*; -use thiserror::Error; -use tokio::sync::Mutex; - -// Tables -const MINTS: &str = "mints"; -const MINT_KEYSETS: &str = "keysets_by_mint"; -const KEYSETS: &str = "keysets"; -const MINT_KEYS: &str = "mint_keys"; -const MINT_QUOTES: &str = "mint_quotes"; -const MELT_QUOTES: &str = "melt_quotes"; -const PROOFS: &str = "proofs"; -const CONFIG: &str = "config"; -const KEYSET_COUNTER: &str = "keyset_counter"; -const NOSTR_LAST_CHECKED: &str = "nostr_last_check"; - -const DATABASE_VERSION: u32 = 3; - -/// Rexie Database Error -#[derive(Debug, Error)] -pub enum Error { - /// CDK Database Error - #[error(transparent)] - CDKDatabase(#[from] cdk::cdk_database::Error), - /// Rexie Error - #[error(transparent)] - Rexie(#[from] rexie::Error), - /// Serde Wasm Error - #[error(transparent)] - SerdeBindgen(#[from] serde_wasm_bindgen::Error), - /// NUT00 Error - #[error(transparent)] - NUT00(cdk::nuts::nut00::Error), - #[error("Not found")] - /// Not Found - NotFound, -} -impl From for cdk::cdk_database::Error { - fn from(e: Error) -> Self { - Self::Database(Box::new(e)) - } -} - -// These are okay because we never actually send across threads in the browser -unsafe impl Send for Error {} -unsafe impl Sync for Error {} - -/// Wallet Rexie Database -#[derive(Debug, Clone)] -pub struct WalletRexieDatabase { - db: Rc>, -} - -// These are okay because we never actually send across threads in the browser -unsafe impl Send for WalletRexieDatabase {} -unsafe impl Sync for WalletRexieDatabase {} - -impl WalletRexieDatabase { - /// Create new [`WalletRexieDatabase`] - pub async fn new() -> Result { - let rexie = Rexie::builder("cdk") - .version(DATABASE_VERSION) - .add_object_store( - ObjectStore::new(PROOFS) - .add_index(Index::new("y", "y").unique(true)) - .add_index(Index::new("mint_url", "mint_url")) - .add_index(Index::new("state", "state")) - .add_index(Index::new("unit", "unit")), - ) - .add_object_store( - ObjectStore::new(MINTS).add_index(Index::new("mint_url", "mint_url").unique(true)), - ) - .add_object_store(ObjectStore::new(MINT_KEYSETS)) - .add_object_store( - ObjectStore::new(KEYSETS) - .add_index(Index::new("keyset_id", "keyset_id").unique(true)), - ) - .add_object_store( - ObjectStore::new(MINT_KEYS) - .add_index(Index::new("keyset_id", "keyset_id").unique(true)), - ) - .add_object_store(ObjectStore::new(MINT_QUOTES)) - .add_object_store(ObjectStore::new(MELT_QUOTES)) - .add_object_store(ObjectStore::new(CONFIG)) - .add_object_store(ObjectStore::new(KEYSET_COUNTER)) - .add_object_store(ObjectStore::new(NOSTR_LAST_CHECKED)) - // Build the database - .build() - .await - .unwrap(); - - Ok(Self { - db: Rc::new(Mutex::new(rexie)), - }) - } - - async fn set_proof_states( - &self, - ys: Vec, - state: State, - ) -> Result<(), cdk_database::Error> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[PROOFS], TransactionMode::ReadWrite) - .map_err(Error::from)?; - - let proofs_store = transaction.store(PROOFS).map_err(Error::from)?; - - for y in ys { - let y = serde_wasm_bindgen::to_value(&y).map_err(Error::from)?; - - let mut proof: ProofInfo = proofs_store - .get(y.clone()) - .await - .map_err(Error::from)? - .and_then(|p| serde_wasm_bindgen::from_value(p).ok()) - .ok_or(Error::NotFound)?; - - proof.state = state; - - let proof = serde_wasm_bindgen::to_value(&proof).map_err(Error::from)?; - - proofs_store - .put(&proof, Some(&y)) - .await - .map_err(Error::from)?; - } - - transaction.done().await.map_err(Error::from)?; - - Ok(()) - } -} - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl WalletDatabase for WalletRexieDatabase { - type Err = cdk::cdk_database::Error; - - async fn add_mint( - &self, - mint_url: MintUrl, - mint_info: Option, - ) -> Result<(), Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[MINTS], TransactionMode::ReadWrite) - .map_err(Error::from)?; - - let mints_store = transaction.store(MINTS).map_err(Error::from)?; - - let mint_url = serde_wasm_bindgen::to_value(&mint_url).map_err(Error::from)?; - let mint_info = serde_wasm_bindgen::to_value(&mint_info).map_err(Error::from)?; - - mints_store - .put(&mint_info, Some(&mint_url)) - .await - .map_err(Error::from)?; - - transaction.done().await.map_err(Error::from)?; - - Ok(()) - } - - async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[MINTS], TransactionMode::ReadWrite) - .map_err(Error::from)?; - - let mints_store = transaction.store(MINTS).map_err(Error::from)?; - - let mint_url = serde_wasm_bindgen::to_value(&mint_url).map_err(Error::from)?; - - mints_store.delete(mint_url).await.map_err(Error::from)?; - - transaction.done().await.map_err(Error::from)?; - - Ok(()) - } - - async fn get_mint(&self, mint_url: MintUrl) -> Result, Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[MINTS], TransactionMode::ReadOnly) - .map_err(Error::from)?; - - let mints_store = transaction.store(MINTS).map_err(Error::from)?; - - let mint_url = serde_wasm_bindgen::to_value(&mint_url).map_err(Error::from)?; - let mint_info = mints_store - .get(mint_url) - .await - .map_err(Error::from)? - .and_then(|m| serde_wasm_bindgen::from_value(m).ok()); - - Ok(mint_info) - } - - async fn get_mints(&self) -> Result>, Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[MINTS], TransactionMode::ReadOnly) - .map_err(Error::from)?; - - let mints_store = transaction.store(MINTS).map_err(Error::from)?; - - let mints = mints_store - .scan(None, None, None, None) - .await - .map_err(Error::from)?; - - let mints: HashMap> = mints - .into_iter() - .map(|(url, info)| { - ( - serde_wasm_bindgen::from_value(url).unwrap(), - serde_wasm_bindgen::from_value(info).unwrap(), - ) - }) - .collect(); - - Ok(mints) - } - - async fn update_mint_url( - &self, - old_mint_url: MintUrl, - new_mint_url: MintUrl, - ) -> Result<(), Self::Err> { - let proofs = self - .get_proofs(Some(old_mint_url), None, None, None) - .await - .map_err(Error::from)?; - - let updated_proofs: Vec = proofs - .into_iter() - .map(|mut p| { - p.mint_url = new_mint_url.clone(); - p - }) - .collect(); - - if !updated_proofs.is_empty() { - self.update_proofs(updated_proofs, vec![]).await?; - } - - // Update mint quotes - { - let quotes = self.get_mint_quotes().await?; - - let unix_time = unix_time(); - - let quotes: Vec = quotes - .into_iter() - .filter_map(|mut q| { - if q.expiry < unix_time { - q.mint_url = new_mint_url.clone(); - Some(q) - } else { - None - } - }) - .collect(); - - for quote in quotes { - self.add_mint_quote(quote).await?; - } - } - - Ok(()) - } - - async fn add_mint_keysets( - &self, - mint_url: MintUrl, - keysets: Vec, - ) -> Result<(), Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[MINT_KEYSETS, KEYSETS], TransactionMode::ReadWrite) - .map_err(Error::from)?; - - let mint_keysets_store = transaction.store(MINT_KEYSETS).map_err(Error::from)?; - let keysets_store = transaction.store(KEYSETS).map_err(Error::from)?; - - let mint_url = serde_wasm_bindgen::to_value(&mint_url).map_err(Error::from)?; - - let mut mint_keysets = mint_keysets_store - .get(mint_url.clone()) - .await - .map_err(Error::from)? - .and_then(|m| serde_wasm_bindgen::from_value(m).ok()); - - let new_keyset_ids: Vec = keysets.iter().map(|k| k.id).collect(); - - mint_keysets - .as_mut() - .unwrap_or(&mut HashSet::new()) - .extend(new_keyset_ids); - - let mint_keysets = serde_wasm_bindgen::to_value(&mint_keysets).map_err(Error::from)?; - - mint_keysets_store - .put(&mint_keysets, Some(&mint_url)) - .await - .map_err(Error::from)?; - - for keyset in keysets { - let id = serde_wasm_bindgen::to_value(&keyset.id).map_err(Error::from)?; - let keyset = serde_wasm_bindgen::to_value(&keyset).map_err(Error::from)?; - - keysets_store - .put(&keyset, Some(&id)) - .await - .map_err(Error::from)?; - } - - transaction.done().await.map_err(Error::from)?; - - Ok(()) - } - - async fn get_mint_keysets( - &self, - mint_url: MintUrl, - ) -> Result>, Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[MINT_KEYSETS, KEYSETS], TransactionMode::ReadOnly) - .map_err(Error::from)?; - - let mints_store = transaction.store(MINT_KEYSETS).map_err(Error::from)?; - - let mint_url = serde_wasm_bindgen::to_value(&mint_url).map_err(Error::from)?; - let mint_keysets: Option> = mints_store - .get(mint_url) - .await - .map_err(Error::from)? - .and_then(|m| serde_wasm_bindgen::from_value(m).ok()); - - let keysets_store = transaction.store(KEYSETS).map_err(Error::from)?; - - let keysets = match mint_keysets { - Some(mint_keysets) => { - let mut keysets = vec![]; - - for mint_keyset in mint_keysets { - let id = serde_wasm_bindgen::to_value(&mint_keyset).map_err(Error::from)?; - - let keyset = keysets_store - .get(id) - .await - .map_err(Error::from)? - .and_then(|k| serde_wasm_bindgen::from_value(k).ok()); - - keysets.push(keyset); - } - - let keysets = keysets.iter().flatten().cloned().collect(); - - Some(keysets) - } - None => None, - }; - - transaction.done().await.map_err(Error::from)?; - - Ok(keysets) - } - - async fn get_keyset_by_id(&self, keyset_id: &Id) -> Result, Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[KEYSETS], TransactionMode::ReadOnly) - .map_err(Error::from)?; - let keysets_store = transaction.store(KEYSETS).map_err(Error::from)?; - - let keyset_id = serde_wasm_bindgen::to_value(keyset_id).map_err(Error::from)?; - - let keyset = keysets_store - .get(keyset_id) - .await - .map_err(Error::from)? - .and_then(|k| serde_wasm_bindgen::from_value(k).ok()); - - Ok(keyset) - } - - async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[MINT_QUOTES], TransactionMode::ReadWrite) - .map_err(Error::from)?; - - let quotes_store = transaction.store(MINT_QUOTES).map_err(Error::from)?; - - let quote_id = serde_wasm_bindgen::to_value("e.id).map_err(Error::from)?; - let quote = serde_wasm_bindgen::to_value("e).map_err(Error::from)?; - - quotes_store - .put("e, Some("e_id)) - .await - .map_err(Error::from)?; - - transaction.done().await.map_err(Error::from)?; - - Ok(()) - } - - async fn get_mint_quote(&self, quote_id: &str) -> Result, Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[MINT_QUOTES], TransactionMode::ReadOnly) - .map_err(Error::from)?; - - let quotes_store = transaction.store(MINT_QUOTES).map_err(Error::from)?; - - let quote_id = serde_wasm_bindgen::to_value("e_id).map_err(Error::from)?; - let quote = quotes_store - .get(quote_id) - .await - .map_err(Error::from)? - .and_then(|q| serde_wasm_bindgen::from_value(q).ok()); - - Ok(quote) - } - - async fn get_mint_quotes(&self) -> Result, Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[MINT_QUOTES], TransactionMode::ReadOnly) - .map_err(Error::from)?; - - let quotes_store = transaction.store(MINT_QUOTES).map_err(Error::from)?; - - let quotes = quotes_store - .scan(None, None, None, None) - .await - .map_err(Error::from)?; - - Ok(quotes - .into_iter() - .map(|(_id, q)| serde_wasm_bindgen::from_value(q)) - .collect::, serde_wasm_bindgen::Error>>() - .map_err(>::into)?) - } - - async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[MINT_QUOTES], TransactionMode::ReadWrite) - .map_err(Error::from)?; - - let quotes_store = transaction.store(MINT_QUOTES).map_err(Error::from)?; - - let quote_id = serde_wasm_bindgen::to_value("e_id).map_err(Error::from)?; - - quotes_store.delete(quote_id).await.map_err(Error::from)?; - - transaction.done().await.map_err(Error::from)?; - - Ok(()) - } - - async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[MELT_QUOTES], TransactionMode::ReadWrite) - .map_err(Error::from)?; - - let quotes_store = transaction.store(MELT_QUOTES).map_err(Error::from)?; - - let quote_id = serde_wasm_bindgen::to_value("e.id).map_err(Error::from)?; - let quote = serde_wasm_bindgen::to_value("e).map_err(Error::from)?; - - quotes_store - .put("e, Some("e_id)) - .await - .map_err(Error::from)?; - - transaction.done().await.map_err(Error::from)?; - - Ok(()) - } - - async fn get_melt_quote(&self, quote_id: &str) -> Result, Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[MELT_QUOTES], TransactionMode::ReadOnly) - .map_err(Error::from)?; - - let quotes_store = transaction.store(MELT_QUOTES).map_err(Error::from)?; - - let quote_id = serde_wasm_bindgen::to_value("e_id).map_err(Error::from)?; - let quote = quotes_store - .get(quote_id) - .await - .map_err(Error::from)? - .and_then(|q| serde_wasm_bindgen::from_value(q).ok()); - - Ok(quote) - } - - async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[MELT_QUOTES], TransactionMode::ReadWrite) - .map_err(Error::from)?; - - let quotes_store = transaction.store(MELT_QUOTES).map_err(Error::from)?; - - let quote_id = serde_wasm_bindgen::to_value("e_id).map_err(Error::from)?; - - quotes_store.delete(quote_id).await.map_err(Error::from)?; - - transaction.done().await.map_err(Error::from)?; - - Ok(()) - } - - async fn add_keys(&self, keys: Keys) -> Result<(), Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[MINT_KEYS], TransactionMode::ReadWrite) - .map_err(Error::from)?; - - let keys_store = transaction.store(MINT_KEYS).map_err(Error::from)?; - - let keyset_id = serde_wasm_bindgen::to_value(&Id::from(&keys)).map_err(Error::from)?; - let keys = serde_wasm_bindgen::to_value(&keys).map_err(Error::from)?; - - keys_store - .put(&keys, Some(&keyset_id)) - .await - .map_err(Error::from)?; - - transaction.done().await.map_err(Error::from)?; - - Ok(()) - } - - async fn get_keys(&self, id: &Id) -> Result, Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[MINT_KEYS], TransactionMode::ReadOnly) - .map_err(Error::from)?; - - let keys_store = transaction.store(MINT_KEYS).map_err(Error::from)?; - - let keyset_id = serde_wasm_bindgen::to_value(id).map_err(Error::from)?; - let keys = keys_store - .get(keyset_id) - .await - .map_err(Error::from)? - .and_then(|k| serde_wasm_bindgen::from_value(k).ok()); - - Ok(keys) - } - - async fn remove_keys(&self, id: &Id) -> Result<(), Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[MINT_KEYS], TransactionMode::ReadWrite) - .map_err(Error::from)?; - - let keys_store = transaction.store(MINT_KEYS).map_err(Error::from)?; - - let keyset_id = serde_wasm_bindgen::to_value(id).map_err(Error::from)?; - keys_store.delete(keyset_id).await.map_err(Error::from)?; - - Ok(()) - } - - async fn update_proofs( - &self, - added: Vec, - removed_ys: Vec, - ) -> Result<(), Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[PROOFS], TransactionMode::ReadWrite) - .map_err(Error::from)?; - - let proofs_store = transaction.store(PROOFS).map_err(Error::from)?; - - for proof in added { - let y = serde_wasm_bindgen::to_value(&proof.y).map_err(Error::from)?; - let proof = serde_wasm_bindgen::to_value(&proof).map_err(Error::from)?; - - proofs_store - .put(&proof, Some(&y)) - .await - .map_err(Error::from)?; - } - - for y in removed_ys { - let y = serde_wasm_bindgen::to_value(&y).map_err(Error::from)?; - - proofs_store.delete(y).await.map_err(Error::from)?; - } - - transaction.done().await.map_err(Error::from)?; - - Ok(()) - } - - async fn set_pending_proofs(&self, ys: Vec) -> Result<(), Self::Err> { - self.set_proof_states(ys, State::Pending).await - } - - async fn reserve_proofs(&self, ys: Vec) -> Result<(), Self::Err> { - self.set_proof_states(ys, State::Reserved).await - } - - async fn set_unspent_proofs(&self, ys: Vec) -> Result<(), Self::Err> { - self.set_proof_states(ys, State::Unspent).await - } - - async fn get_proofs( - &self, - mint_url: Option, - unit: Option, - state: Option>, - spending_conditions: Option>, - ) -> Result, Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[PROOFS], TransactionMode::ReadOnly) - .map_err(Error::from)?; - - let proofs_store = transaction.store(PROOFS).map_err(Error::from)?; - - let proofs = proofs_store - .scan(None, None, None, None) - .await - .map_err(Error::from)?; - - let proofs: Vec = proofs - .into_iter() - .filter_map(|(_k, v)| { - let mut proof = None; - - if let Ok(proof_info) = serde_wasm_bindgen::from_value::(v) { - proof = match proof_info.matches_conditions( - &mint_url, - &unit, - &state, - &spending_conditions, - ) { - true => Some(proof_info), - false => None, - }; - } - - proof - }) - .collect(); - - transaction.done().await.map_err(Error::from)?; - - Ok(proofs) - } - - async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[KEYSET_COUNTER], TransactionMode::ReadWrite) - .map_err(Error::from)?; - - let counter_store = transaction.store(KEYSET_COUNTER).map_err(Error::from)?; - - let keyset_id = serde_wasm_bindgen::to_value(keyset_id).map_err(Error::from)?; - - let current_count: Option = counter_store - .get(keyset_id.clone()) - .await - .map_err(Error::from)? - .and_then(|c| serde_wasm_bindgen::from_value(c).ok()); - - let new_count = current_count.unwrap_or_default() + count; - - let new_count = serde_wasm_bindgen::to_value(&new_count).map_err(Error::from)?; - - counter_store - .put(&new_count, Some(&keyset_id)) - .await - .map_err(Error::from)?; - - transaction.done().await.map_err(Error::from)?; - - Ok(()) - } - - async fn get_keyset_counter(&self, keyset_id: &Id) -> Result, Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[KEYSET_COUNTER], TransactionMode::ReadWrite) - .map_err(Error::from)?; - - let counter_store = transaction.store(KEYSET_COUNTER).map_err(Error::from)?; - - let keyset_id = serde_wasm_bindgen::to_value(keyset_id).map_err(Error::from)?; - - let current_count = counter_store - .get(keyset_id) - .await - .map_err(Error::from)? - .and_then(|c| serde_wasm_bindgen::from_value(c).ok()); - - Ok(current_count) - } - - async fn add_nostr_last_checked( - &self, - verifying_key: PublicKey, - last_checked: u32, - ) -> Result<(), Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[NOSTR_LAST_CHECKED], TransactionMode::ReadWrite) - .map_err(Error::from)?; - - let counter_store = transaction.store(NOSTR_LAST_CHECKED).map_err(Error::from)?; - - let verifying_key = serde_wasm_bindgen::to_value(&verifying_key).map_err(Error::from)?; - - let last_checked = serde_wasm_bindgen::to_value(&last_checked).map_err(Error::from)?; - - counter_store - .put(&last_checked, Some(&verifying_key)) - .await - .map_err(Error::from)?; - - transaction.done().await.map_err(Error::from)?; - - Ok(()) - } - - async fn get_nostr_last_checked( - &self, - verifying_key: &PublicKey, - ) -> Result, Self::Err> { - let rexie = self.db.lock().await; - - let transaction = rexie - .transaction(&[NOSTR_LAST_CHECKED], TransactionMode::ReadOnly) - .map_err(Error::from)?; - - let nostr_last_check_store = transaction.store(NOSTR_LAST_CHECKED).map_err(Error::from)?; - - let verifying_key = serde_wasm_bindgen::to_value(verifying_key).map_err(Error::from)?; - - let last_checked = nostr_last_check_store - .get(verifying_key) - .await - .map_err(Error::from)? - .and_then(|l| serde_wasm_bindgen::from_value(l).ok()); - - Ok(last_checked) - } -} diff --git a/crates/cdk-sqlite/Cargo.toml b/crates/cdk-sqlite/Cargo.toml deleted file mode 100644 index e281ba48..00000000 --- a/crates/cdk-sqlite/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "cdk-sqlite" -version = "0.4.0" -edition = "2021" -authors = ["CDK Developers"] -description = "SQLite storage backend for CDK" -license = "MIT" -homepage = "https://github.com/cashubtc/cdk" -repository = "https://github.com/cashubtc/cdk.git" -rust-version = "1.66.0" # MSRV - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[features] -default = ["mint", "wallet"] -mint = ["cdk/mint"] -wallet = ["cdk/wallet"] - -[dependencies] -async-trait = "0.1" -cdk = { path = "../cdk", version = "0.4.0", default-features = false } -bitcoin = { version = "0.32.2", default-features = false } -sqlx = { version = "0.6.3", default-features = false, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } -thiserror = "1" -tokio = { version = "1", features = [ - "time", - "macros", - "sync", -] } -tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] } -serde_json = "1" -lightning-invoice = { version = "0.32.0", features = ["serde", "std"] } diff --git a/crates/cdk-sqlite/README.md b/crates/cdk-sqlite/README.md deleted file mode 100644 index ce1b2700..00000000 --- a/crates/cdk-sqlite/README.md +++ /dev/null @@ -1,38 +0,0 @@ - -# Cashu Development Kit SQLite Storage Backend - -**ALPHA** This library is in early development, the api will change and should be used with caution. - -cdk-sqlite is the sqlite storage backend for cdk. - -## Crate Feature Flags - -The following crate feature flags are available: - -| Feature | Default | Description | -|-------------|:-------:|------------------------------------| -| `wallet` | Yes | Enable cashu wallet features | -| `mint` | Yes | Enable cashu mint wallet features | - -## Implemented [NUTs](https://github.com/cashubtc/nuts/): - -See - - -## Minimum Supported Rust Version (MSRV) - -The `cdk` library should always compile with any combination of features on Rust **1.63.0**. - -To build and test with the MSRV you will need to pin the below dependency versions: - -```shell -cargo update -p half --precise 2.2.1 -cargo update -p home --precise 0.5.5 -cargo update -p tokio --precise 1.38.1 -cargo update -p serde_with --precise 3.1.0 -cargo update -p reqwest --precise 0.12.4 -``` - -## License - -This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details. diff --git a/crates/cdk-sqlite/src/lib.rs b/crates/cdk-sqlite/src/lib.rs deleted file mode 100644 index ea1e51df..00000000 --- a/crates/cdk-sqlite/src/lib.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! SQLite storage backend for cdk - -#![warn(missing_docs)] -#![warn(rustdoc::bare_urls)] - -#[cfg(feature = "mint")] -pub mod mint; -#[cfg(feature = "wallet")] -pub mod wallet; - -#[cfg(feature = "mint")] -pub use mint::MintSqliteDatabase; -#[cfg(feature = "wallet")] -pub use wallet::WalletSqliteDatabase; diff --git a/crates/cdk-sqlite/src/mint/error.rs b/crates/cdk-sqlite/src/mint/error.rs deleted file mode 100644 index 5f2be090..00000000 --- a/crates/cdk-sqlite/src/mint/error.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! SQLite Database Error - -use thiserror::Error; - -/// SQLite Database Error -#[derive(Debug, Error)] -pub enum Error { - /// SQLX Error - #[error(transparent)] - SQLX(#[from] sqlx::Error), - /// NUT00 Error - #[error(transparent)] - CDKNUT00(#[from] cdk::nuts::nut00::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), - /// NUT07 Error - #[error(transparent)] - CDKNUT07(#[from] cdk::nuts::nut07::Error), - /// Secret Error - #[error(transparent)] - CDKSECRET(#[from] cdk::secret::Error), - /// BIP32 Error - #[error(transparent)] - BIP32(#[from] bitcoin::bip32::Error), - /// Mint Url Error - #[error(transparent)] - MintUrl(#[from] cdk::mint_url::Error), - /// Could Not Initialize Database - #[error("Could not initialize database")] - CouldNotInitialize, - /// Invalid Database Path - #[error("Invalid database path")] - InvalidDbPath, - /// Serde Error - #[error(transparent)] - Serde(#[from] serde_json::Error), -} - -impl From for cdk::cdk_database::Error { - fn from(e: Error) -> Self { - Self::Database(Box::new(e)) - } -} diff --git a/crates/cdk-sqlite/src/mint/migrations/20240612124932_init.sql b/crates/cdk-sqlite/src/mint/migrations/20240612124932_init.sql deleted file mode 100644 index 0bc12730..00000000 --- a/crates/cdk-sqlite/src/mint/migrations/20240612124932_init.sql +++ /dev/null @@ -1,68 +0,0 @@ - --- Proof Table -CREATE TABLE IF NOT EXISTS proof ( -y BLOB PRIMARY KEY, -amount INTEGER NOT NULL, -keyset_id TEXT NOT NULL, -secret TEXT NOT NULL, -c BLOB NOT NULL, -witness TEXT, -state TEXT CHECK ( state IN ('SPENT', 'PENDING' ) ) NOT NULL -); - -CREATE INDEX IF NOT EXISTS state_index ON proof(state); -CREATE INDEX IF NOT EXISTS secret_index ON proof(secret); - --- Keysets Table - -CREATE TABLE IF NOT EXISTS keyset ( - id TEXT PRIMARY KEY, - unit TEXT NOT NULL, - active BOOL NOT NULL, - valid_from INTEGER NOT NULL, - valid_to INTEGER, - derivation_path TEXT NOT NULL, - max_order INTEGER NOT NULL -); - -CREATE INDEX IF NOT EXISTS unit_index ON keyset(unit); -CREATE INDEX IF NOT EXISTS active_index ON keyset(active); - - -CREATE TABLE IF NOT EXISTS mint_quote ( - id TEXT PRIMARY KEY, - mint_url TEXT NOT NULL, - amount INTEGER NOT NULL, - unit TEXT NOT NULL, - request TEXT NOT NULL, - paid BOOL NOT NULL DEFAULT FALSE, - expiry INTEGER NOT NULL -); - - -CREATE INDEX IF NOT EXISTS paid_index ON mint_quote(paid); -CREATE INDEX IF NOT EXISTS request_index ON mint_quote(request); -CREATE INDEX IF NOT EXISTS expiry_index ON mint_quote(expiry); - -CREATE TABLE IF NOT EXISTS melt_quote ( - id TEXT PRIMARY KEY, - unit TEXT NOT NULL, - amount INTEGER NOT NULL, - request TEXT NOT NULL, - fee_reserve INTEGER NOT NULL, - paid BOOL NOT NULL DEFAULT FALSE, - expiry INTEGER NOT NULL -); - -CREATE INDEX IF NOT EXISTS paid_index ON melt_quote(paid); -CREATE INDEX IF NOT EXISTS request_index ON melt_quote(request); -CREATE INDEX IF NOT EXISTS expiry_index ON melt_quote(expiry); - -CREATE TABLE IF NOT EXISTS blind_signature ( - y BLOB PRIMARY KEY, - amount INTEGER NOT NULL, - keyset_id TEXT NOT NULL, - c BLOB NOT NULL -); - -CREATE INDEX IF NOT EXISTS keyset_id_index ON blind_signature(keyset_id); diff --git a/crates/cdk-sqlite/src/mint/migrations/20240618195700_quote_state.sql b/crates/cdk-sqlite/src/mint/migrations/20240618195700_quote_state.sql deleted file mode 100644 index 82d8d479..00000000 --- a/crates/cdk-sqlite/src/mint/migrations/20240618195700_quote_state.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE melt_quote ADD state TEXT CHECK ( state IN ('UNPAID', 'PENDING', 'PAID' ) ) NOT NULL DEFAULT 'UNPAID'; -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; diff --git a/crates/cdk-sqlite/src/mint/migrations/20240626092101_nut04_state.sql b/crates/cdk-sqlite/src/mint/migrations/20240626092101_nut04_state.sql deleted file mode 100644 index ec3918e2..00000000 --- a/crates/cdk-sqlite/src/mint/migrations/20240626092101_nut04_state.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE mint_quote ADD state TEXT CHECK ( state IN ('UNPAID', 'PENDING', 'PAID', 'ISSUED' ) ) NOT NULL DEFAULT 'UNPAID'; -ALTER TABLE mint_quote DROP COLUMN paid; -CREATE INDEX IF NOT EXISTS mint_quote_state_index ON mint_quote(state); diff --git a/crates/cdk-sqlite/src/mint/migrations/20240703122347_request_lookup_id.sql b/crates/cdk-sqlite/src/mint/migrations/20240703122347_request_lookup_id.sql deleted file mode 100644 index ff28b8a1..00000000 --- a/crates/cdk-sqlite/src/mint/migrations/20240703122347_request_lookup_id.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE mint_quote ADD COLUMN request_lookup_id TEXT; -ALTER TABLE melt_quote ADD COLUMN request_lookup_id TEXT; - -CREATE UNIQUE INDEX unique_request_lookup_id_mint ON mint_quote(request_lookup_id); -CREATE UNIQUE INDEX unique_request_lookup_id_melt ON melt_quote(request_lookup_id); diff --git a/crates/cdk-sqlite/src/mint/migrations/20240710145043_input_fee.sql b/crates/cdk-sqlite/src/mint/migrations/20240710145043_input_fee.sql deleted file mode 100644 index 6bdd8cd0..00000000 --- a/crates/cdk-sqlite/src/mint/migrations/20240710145043_input_fee.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE keyset ADD input_fee_ppk INTEGER; diff --git a/crates/cdk-sqlite/src/mint/migrations/20240711183109_derivation_path_index.sql b/crates/cdk-sqlite/src/mint/migrations/20240711183109_derivation_path_index.sql deleted file mode 100644 index 862eebd8..00000000 --- a/crates/cdk-sqlite/src/mint/migrations/20240711183109_derivation_path_index.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE keyset ADD derivation_path_index INTEGER; diff --git a/crates/cdk-sqlite/src/mint/migrations/20240718203721_allow_unspent.sql b/crates/cdk-sqlite/src/mint/migrations/20240718203721_allow_unspent.sql deleted file mode 100644 index 81a27629..00000000 --- a/crates/cdk-sqlite/src/mint/migrations/20240718203721_allow_unspent.sql +++ /dev/null @@ -1,21 +0,0 @@ --- Create a new table with the updated CHECK constraint -CREATE TABLE proof_new ( - y BLOB PRIMARY KEY, - amount INTEGER NOT NULL, - keyset_id TEXT NOT NULL, - secret TEXT NOT NULL, - c BLOB NOT NULL, - witness TEXT, - state TEXT CHECK (state IN ('SPENT', 'PENDING', 'UNSPENT')) NOT NULL -); - --- Copy the data from the old table to the new table -INSERT INTO proof_new (y, amount, keyset_id, secret, c, witness, state) -SELECT y, amount, keyset_id, secret, c, witness, state -FROM proof; - --- Drop the old table -DROP TABLE proof; - --- Rename the new table to the original table name -ALTER TABLE proof_new RENAME TO proof; diff --git a/crates/cdk-sqlite/src/mint/migrations/20240811031111_update_mint_url.sql b/crates/cdk-sqlite/src/mint/migrations/20240811031111_update_mint_url.sql deleted file mode 100644 index db70cb1e..00000000 --- a/crates/cdk-sqlite/src/mint/migrations/20240811031111_update_mint_url.sql +++ /dev/null @@ -1 +0,0 @@ -UPDATE `mint_quote` SET `mint_url` = RTRIM(`mint_url`, '/'); diff --git a/crates/cdk-sqlite/src/mint/migrations/20240919103407_proofs_quote_id.sql b/crates/cdk-sqlite/src/mint/migrations/20240919103407_proofs_quote_id.sql deleted file mode 100644 index 34f33fdc..00000000 --- a/crates/cdk-sqlite/src/mint/migrations/20240919103407_proofs_quote_id.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE proof ADD COLUMN quote_id TEXT; -ALTER TABLE blind_signature ADD COLUMN quote_id TEXT; diff --git a/crates/cdk-sqlite/src/mint/migrations/20240923153640_melt_requests.sql b/crates/cdk-sqlite/src/mint/migrations/20240923153640_melt_requests.sql deleted file mode 100644 index 3299bff1..00000000 --- a/crates/cdk-sqlite/src/mint/migrations/20240923153640_melt_requests.sql +++ /dev/null @@ -1,8 +0,0 @@ --- Melt Request Table -CREATE TABLE IF NOT EXISTS melt_request ( -id TEXT PRIMARY KEY, -inputs TEXT NOT NULL, -outputs TEXT, -method TEXT NOT NULL, -unit TEXT NOT NULL -); diff --git a/crates/cdk-sqlite/src/mint/migrations/20240930101140_dleq_for_sigs.sql b/crates/cdk-sqlite/src/mint/migrations/20240930101140_dleq_for_sigs.sql deleted file mode 100644 index 27b0ade4..00000000 --- a/crates/cdk-sqlite/src/mint/migrations/20240930101140_dleq_for_sigs.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE blind_signature ADD COLUMN dleq_e TEXT; -ALTER TABLE blind_signature ADD COLUMN dleq_s TEXT; diff --git a/crates/cdk-sqlite/src/mint/mod.rs b/crates/cdk-sqlite/src/mint/mod.rs deleted file mode 100644 index 20fc70bb..00000000 --- a/crates/cdk-sqlite/src/mint/mod.rs +++ /dev/null @@ -1,1413 +0,0 @@ -//! SQLite Mint - -use std::collections::HashMap; -use std::path::Path; -use std::str::FromStr; -use std::time::Duration; - -use async_trait::async_trait; -use bitcoin::bip32::DerivationPath; -use cdk::cdk_database::{self, MintDatabase}; -use cdk::mint::{MintKeySetInfo, MintQuote}; -use cdk::mint_url::MintUrl; -use cdk::nuts::nut05::QuoteState; -use cdk::nuts::{ - BlindSignature, BlindSignatureDleq, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, - MintQuoteState, PaymentMethod, Proof, Proofs, PublicKey, SecretKey, State, -}; -use cdk::secret::Secret; -use cdk::types::LnKey; -use cdk::{mint, Amount}; -use error::Error; -use lightning_invoice::Bolt11Invoice; -use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions, SqliteRow}; -use sqlx::Row; - -pub mod error; - -/// Mint SQLite Database -#[derive(Debug, Clone)] -pub struct MintSqliteDatabase { - pool: SqlitePool, -} - -impl MintSqliteDatabase { - /// Create new [`MintSqliteDatabase`] - pub async fn new(path: &Path) -> Result { - let path = path.to_str().ok_or(Error::InvalidDbPath)?; - let db_options = SqliteConnectOptions::from_str(path)? - .busy_timeout(Duration::from_secs(5)) - .read_only(false) - .create_if_missing(true) - .auto_vacuum(sqlx::sqlite::SqliteAutoVacuum::Full); - - let pool = SqlitePoolOptions::new() - .max_connections(1) - .connect_with(db_options) - .await?; - - Ok(Self { pool }) - } - - /// Migrate [`MintSqliteDatabase`] - pub async fn migrate(&self) { - sqlx::migrate!("./src/mint/migrations") - .run(&self.pool) - .await - .expect("Could not run migrations"); - } -} - -#[async_trait] -impl MintDatabase for MintSqliteDatabase { - type Err = cdk_database::Error; - - async fn set_active_keyset(&self, unit: CurrencyUnit, id: Id) -> Result<(), Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - - let update_res = sqlx::query( - r#" -UPDATE keyset -SET active=FALSE -WHERE unit IS ?; - "#, - ) - .bind(unit.to_string()) - .execute(&mut transaction) - .await; - - match update_res { - Ok(_) => (), - Err(err) => { - tracing::error!("SQLite Could not update keyset"); - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - - return Err(Error::from(err).into()); - } - }; - - let update_res = sqlx::query( - r#" -UPDATE keyset -SET active=TRUE -WHERE unit IS ? -AND id IS ?; - "#, - ) - .bind(unit.to_string()) - .bind(id.to_string()) - .execute(&mut transaction) - .await; - - match update_res { - Ok(_) => (), - Err(err) => { - tracing::error!("SQLite Could not update keyset"); - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - - return Err(Error::from(err).into()); - } - }; - - transaction.commit().await.map_err(Error::from)?; - - Ok(()) - } - - async fn get_active_keyset_id(&self, unit: &CurrencyUnit) -> Result, Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - - let rec = sqlx::query( - r#" -SELECT id -FROM keyset -WHERE active = 1 -AND unit IS ? - "#, - ) - .bind(unit.to_string()) - .fetch_one(&mut transaction) - .await; - - let rec = match rec { - Ok(rec) => { - transaction.commit().await.map_err(Error::from)?; - rec - } - Err(err) => match err { - sqlx::Error::RowNotFound => { - transaction.commit().await.map_err(Error::from)?; - return Ok(None); - } - _ => { - return { - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - Err(Error::SQLX(err).into()) - } - } - }, - }; - - Ok(Some( - Id::from_str(rec.try_get("id").map_err(Error::from)?).map_err(Error::from)?, - )) - } - - async fn get_active_keysets(&self) -> Result, Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - - let recs = sqlx::query( - r#" -SELECT id, unit -FROM keyset -WHERE active = 1 - "#, - ) - .fetch_all(&mut transaction) - .await; - - match recs { - Ok(recs) => { - transaction.commit().await.map_err(Error::from)?; - - let keysets = recs - .iter() - .filter_map(|r| match Id::from_str(r.get("id")) { - Ok(id) => Some(( - CurrencyUnit::from_str(r.get::<'_, &str, &str>("unit")).unwrap(), - id, - )), - Err(_) => None, - }) - .collect(); - Ok(keysets) - } - Err(err) => { - tracing::error!("SQLite could not get active keyset"); - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - Err(Error::from(err).into()) - } - } - } - - async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - - let res = sqlx::query( - r#" -INSERT OR REPLACE INTO mint_quote -(id, mint_url, amount, unit, request, state, expiry, request_lookup_id) -VALUES (?, ?, ?, ?, ?, ?, ?, ?); - "#, - ) - .bind(quote.id.to_string()) - .bind(quote.mint_url.to_string()) - .bind(u64::from(quote.amount) as i64) - .bind(quote.unit.to_string()) - .bind(quote.request) - .bind(quote.state.to_string()) - .bind(quote.expiry as i64) - .bind(quote.request_lookup_id) - .execute(&mut transaction) - .await; - - match res { - Ok(_) => { - transaction.commit().await.map_err(Error::from)?; - Ok(()) - } - Err(err) => { - tracing::error!("SQLite Could not update keyset"); - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - - Err(Error::from(err).into()) - } - } - } - - async fn get_mint_quote(&self, quote_id: &str) -> Result, 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; - - match rec { - Ok(rec) => { - transaction.commit().await.map_err(Error::from)?; - Ok(Some(sqlite_row_to_mint_quote(rec)?)) - } - Err(err) => match err { - sqlx::Error::RowNotFound => { - transaction.commit().await.map_err(Error::from)?; - Ok(None) - } - _ => { - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - Err(Error::SQLX(err).into()) - } - }, - } - } - - async fn get_mint_quote_by_request( - &self, - request: &str, - ) -> Result, Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - let rec = sqlx::query( - r#" -SELECT * -FROM mint_quote -WHERE request=?; - "#, - ) - .bind(request) - .fetch_one(&mut transaction) - .await; - - match rec { - Ok(rec) => { - transaction.commit().await.map_err(Error::from)?; - Ok(Some(sqlite_row_to_mint_quote(rec)?)) - } - Err(err) => match err { - sqlx::Error::RowNotFound => { - transaction.commit().await.map_err(Error::from)?; - Ok(None) - } - _ => { - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - Err(Error::SQLX(err).into()) - } - }, - } - } - - async fn get_mint_quote_by_request_lookup_id( - &self, - request_lookup_id: &str, - ) -> Result, Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - - let rec = sqlx::query( - r#" -SELECT * -FROM mint_quote -WHERE request_lookup_id=?; - "#, - ) - .bind(request_lookup_id) - .fetch_one(&mut transaction) - .await; - - match rec { - Ok(rec) => { - transaction.commit().await.map_err(Error::from)?; - - Ok(Some(sqlite_row_to_mint_quote(rec)?)) - } - Err(err) => match err { - sqlx::Error::RowNotFound => { - transaction.commit().await.map_err(Error::from)?; - Ok(None) - } - _ => { - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - Err(Error::SQLX(err).into()) - } - }, - } - } - - async fn update_mint_quote_state( - &self, - quote_id: &str, - state: MintQuoteState, - ) -> Result { - 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; - let quote = match rec { - Ok(row) => sqlite_row_to_mint_quote(row)?, - Err(err) => { - tracing::error!("SQLite Could not update keyset"); - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - - return Err(Error::from(err).into()); - } - }; - - let update = sqlx::query( - r#" - UPDATE mint_quote SET state = ? WHERE id = ? - "#, - ) - .bind(state.to_string()) - .bind(quote_id) - .execute(&mut transaction) - .await; - - match update { - Ok(_) => { - transaction.commit().await.map_err(Error::from)?; - Ok(quote.state) - } - Err(err) => { - tracing::error!("SQLite Could not update keyset"); - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - - return Err(Error::from(err).into()); - } - } - } - - async fn get_mint_quotes(&self) -> Result, Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - let rec = sqlx::query( - r#" -SELECT * -FROM mint_quote - "#, - ) - .fetch_all(&mut transaction) - .await; - - match rec { - Ok(rows) => { - transaction.commit().await.map_err(Error::from)?; - let mint_quotes = rows - .into_iter() - .map(sqlite_row_to_mint_quote) - .collect::, _>>()?; - - Ok(mint_quotes) - } - Err(err) => { - tracing::error!("SQLite get mint quotes"); - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - - return Err(Error::from(err).into()); - } - } - } - - async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - - let res = sqlx::query( - r#" -DELETE FROM mint_quote -WHERE id=? - "#, - ) - .bind(quote_id) - .execute(&mut transaction) - .await; - - match res { - Ok(_) => { - transaction.commit().await.map_err(Error::from)?; - - Ok(()) - } - Err(err) => { - tracing::error!("SQLite Could not remove mint quote"); - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - - Err(Error::from(err).into()) - } - } - } - - async fn add_melt_quote(&self, quote: mint::MeltQuote) -> Result<(), Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - let res = sqlx::query( - r#" -INSERT OR REPLACE INTO melt_quote -(id, unit, amount, request, fee_reserve, state, expiry, payment_preimage, request_lookup_id) -VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); - "#, - ) - .bind(quote.id.to_string()) - .bind(quote.unit.to_string()) - .bind(u64::from(quote.amount) as i64) - .bind(quote.request) - .bind(u64::from(quote.fee_reserve) as i64) - .bind(quote.state.to_string()) - .bind(quote.expiry as i64) - .bind(quote.payment_preimage) - .bind(quote.request_lookup_id) - .execute(&mut transaction) - .await; - - match res { - Ok(_) => { - transaction.commit().await.map_err(Error::from)?; - - Ok(()) - } - Err(err) => { - tracing::error!("SQLite Could not remove mint quote"); - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - - Err(Error::from(err).into()) - } - } - } - async fn get_melt_quote(&self, quote_id: &str) -> Result, 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; - - match rec { - Ok(rec) => { - transaction.commit().await.map_err(Error::from)?; - - Ok(Some(sqlite_row_to_melt_quote(rec)?)) - } - Err(err) => match err { - sqlx::Error::RowNotFound => { - transaction.commit().await.map_err(Error::from)?; - Ok(None) - } - _ => { - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - - Err(Error::SQLX(err).into()) - } - }, - } - } - - async fn get_melt_quotes(&self) -> Result, Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - let rec = sqlx::query( - r#" -SELECT * -FROM melt_quote - "#, - ) - .fetch_all(&mut transaction) - .await - .map_err(Error::from); - - match rec { - Ok(rec) => { - let melt_quotes = rec - .into_iter() - .map(sqlite_row_to_melt_quote) - .collect::, _>>()?; - Ok(melt_quotes) - } - Err(err) => { - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - - Err(err.into()) - } - } - } - - async fn update_melt_quote_state( - &self, - quote_id: &str, - state: MeltQuoteState, - ) -> Result { - 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; - - let quote = match rec { - Ok(rec) => sqlite_row_to_melt_quote(rec)?, - Err(err) => { - tracing::error!("SQLite Could not update keyset"); - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - - return Err(Error::from(err).into()); - } - }; - - let rec = sqlx::query( - r#" - UPDATE melt_quote SET state = ? WHERE id = ? - "#, - ) - .bind(state.to_string()) - .bind(quote_id) - .execute(&mut transaction) - .await; - - match rec { - Ok(_) => { - transaction.commit().await.map_err(Error::from)?; - } - Err(err) => { - tracing::error!("SQLite Could not update melt quote"); - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - - return Err(Error::from(err).into()); - } - }; - - Ok(quote.state) - } - - async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - let res = sqlx::query( - r#" -DELETE FROM melt_quote -WHERE id=? - "#, - ) - .bind(quote_id) - .execute(&mut transaction) - .await; - - match res { - Ok(_) => { - transaction.commit().await.map_err(Error::from)?; - Ok(()) - } - Err(err) => { - tracing::error!("SQLite Could not update melt quote"); - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - - Err(Error::from(err).into()) - } - } - } - - async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - let res = sqlx::query( - r#" -INSERT OR REPLACE INTO keyset -(id, unit, active, valid_from, valid_to, derivation_path, max_order, input_fee_ppk, derivation_path_index) -VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); - "#, - ) - .bind(keyset.id.to_string()) - .bind(keyset.unit.to_string()) - .bind(keyset.active) - .bind(keyset.valid_from as i64) - .bind(keyset.valid_to.map(|v| v as i64)) - .bind(keyset.derivation_path.to_string()) - .bind(keyset.max_order) - .bind(keyset.input_fee_ppk as i64) - .bind(keyset.derivation_path_index) - .execute(&mut transaction) - .await; - - match res { - Ok(_) => { - transaction.commit().await.map_err(Error::from)?; - Ok(()) - } - Err(err) => { - tracing::error!("SQLite could not add keyset info"); - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - - Err(Error::from(err).into()) - } - } - } - - async fn get_keyset_info(&self, id: &Id) -> Result, Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - let rec = sqlx::query( - r#" -SELECT * -FROM keyset -WHERE id=?; - "#, - ) - .bind(id.to_string()) - .fetch_one(&mut transaction) - .await; - - match rec { - Ok(rec) => { - transaction.commit().await.map_err(Error::from)?; - Ok(Some(sqlite_row_to_keyset_info(rec)?)) - } - Err(err) => match err { - sqlx::Error::RowNotFound => { - transaction.commit().await.map_err(Error::from)?; - return Ok(None); - } - _ => { - tracing::error!("SQLite could not get keyset info"); - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - return Err(Error::SQLX(err).into()); - } - }, - } - } - - async fn get_keyset_infos(&self) -> Result, Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - let recs = sqlx::query( - r#" -SELECT * -FROM keyset; - "#, - ) - .fetch_all(&mut transaction) - .await - .map_err(Error::from); - - match recs { - Ok(recs) => { - transaction.commit().await.map_err(Error::from)?; - Ok(recs - .into_iter() - .map(sqlite_row_to_keyset_info) - .collect::>()?) - } - Err(err) => { - tracing::error!("SQLite could not get keyset info"); - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - Err(err.into()) - } - } - } - - async fn add_proofs(&self, proofs: Proofs, quote_id: Option) -> Result<(), Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - for proof in proofs { - if let Err(err) = sqlx::query( - r#" -INSERT INTO proof -(y, amount, keyset_id, secret, c, witness, state, quote_id) -VALUES (?, ?, ?, ?, ?, ?, ?, ?); - "#, - ) - .bind(proof.y()?.to_bytes().to_vec()) - .bind(u64::from(proof.amount) as i64) - .bind(proof.keyset_id.to_string()) - .bind(proof.secret.to_string()) - .bind(proof.c.to_bytes().to_vec()) - .bind(proof.witness.map(|w| serde_json::to_string(&w).unwrap())) - .bind("UNSPENT") - .bind(quote_id.clone()) - .execute(&mut transaction) - .await - .map_err(Error::from) - { - tracing::debug!("Attempting to add known proof. Skipping.... {:?}", err); - } - } - transaction.commit().await.map_err(Error::from)?; - - Ok(()) - } - - async fn get_proofs_by_ys(&self, ys: &[PublicKey]) -> Result>, Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - - let mut proofs = Vec::with_capacity(ys.len()); - - for y in ys { - let rec = sqlx::query( - r#" -SELECT * -FROM proof -WHERE y=?; - "#, - ) - .bind(y.to_bytes().to_vec()) - .fetch_one(&mut transaction) - .await; - - match rec { - Ok(rec) => { - proofs.push(Some(sqlite_row_to_proof(rec)?)); - } - Err(err) => match err { - sqlx::Error::RowNotFound => proofs.push(None), - _ => { - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - return Err(Error::SQLX(err).into()); - } - }, - }; - } - - transaction.commit().await.map_err(Error::from)?; - - Ok(proofs) - } - - async fn get_proof_ys_by_quote_id(&self, quote_id: &str) -> Result, Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - - let rec = sqlx::query( - r#" -SELECT * -FROM proof -WHERE quote_id=?; - "#, - ) - .bind(quote_id) - .fetch_all(&mut transaction) - .await; - - let ys = match rec { - Ok(rec) => { - transaction.commit().await.map_err(Error::from)?; - - let proofs = rec - .into_iter() - .map(sqlite_row_to_proof) - .collect::, _>>()?; - - proofs - .iter() - .map(|p| p.y()) - .collect::, _>>()? - } - Err(err) => match err { - sqlx::Error::RowNotFound => { - transaction.commit().await.map_err(Error::from)?; - - vec![] - } - _ => { - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - return Err(Error::SQLX(err).into()); - } - }, - }; - - Ok(ys) - } - - async fn get_proofs_states(&self, ys: &[PublicKey]) -> Result>, Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - - let mut states = Vec::with_capacity(ys.len()); - - for y in ys { - let rec = sqlx::query( - r#" -SELECT state -FROM proof -WHERE y=?; - "#, - ) - .bind(y.to_bytes().to_vec()) - .fetch_one(&mut transaction) - .await; - - match rec { - Ok(rec) => { - let state: String = rec.get("state"); - let state = State::from_str(&state).map_err(Error::from)?; - states.push(Some(state)); - } - Err(err) => match err { - sqlx::Error::RowNotFound => states.push(None), - _ => { - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - return Err(Error::SQLX(err).into()); - } - }, - }; - } - - transaction.commit().await.map_err(Error::from)?; - - Ok(states) - } - - async fn get_proofs_by_keyset_id( - &self, - keyset_id: &Id, - ) -> Result<(Proofs, Vec>), Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - let rec = sqlx::query( - r#" -SELECT * -FROM proof -WHERE keyset_id=?; - "#, - ) - .bind(keyset_id.to_string()) - .fetch_all(&mut transaction) - .await; - - match rec { - Ok(rec) => { - transaction.commit().await.map_err(Error::from)?; - let mut proofs_for_id = vec![]; - let mut states = vec![]; - - for row in rec { - let (proof, state) = sqlite_row_to_proof_with_state(row)?; - - proofs_for_id.push(proof); - states.push(state); - } - - Ok((proofs_for_id, states)) - } - Err(err) => { - tracing::error!("SQLite could not get proofs by keysets id"); - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - - return Err(Error::from(err).into()); - } - } - } - - async fn update_proofs_states( - &self, - ys: &[PublicKey], - proofs_state: State, - ) -> Result>, Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - - let mut states = Vec::with_capacity(ys.len()); - - let proofs_state = proofs_state.to_string(); - for y in ys { - let current_state; - let y = y.to_bytes().to_vec(); - let rec = sqlx::query( - r#" -SELECT state -FROM proof -WHERE y=?; - "#, - ) - .bind(&y) - .fetch_one(&mut transaction) - .await; - - match rec { - Ok(rec) => { - let state: String = rec.get("state"); - current_state = Some(State::from_str(&state).map_err(Error::from)?); - } - Err(err) => match err { - sqlx::Error::RowNotFound => { - current_state = None; - } - _ => { - tracing::error!("SQLite could not get state of proof"); - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - return Err(Error::SQLX(err).into()); - } - }, - }; - - states.push(current_state); - - if current_state != Some(State::Spent) { - let res = sqlx::query( - r#" - UPDATE proof SET state = ? WHERE y = ? - "#, - ) - .bind(&proofs_state) - .bind(y) - .execute(&mut transaction) - .await; - - if let Err(err) = res { - tracing::error!("SQLite could not update proof state"); - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - return Err(Error::SQLX(err).into()); - } - } - } - - transaction.commit().await.map_err(Error::from)?; - - Ok(states) - } - - async fn add_blind_signatures( - &self, - blinded_messages: &[PublicKey], - blinded_signatures: &[BlindSignature], - quote_id: Option, - ) -> Result<(), Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - for (message, signature) in blinded_messages.iter().zip(blinded_signatures) { - let res = sqlx::query( - r#" -INSERT INTO blind_signature -(y, amount, keyset_id, c, quote_id, dleq_e, dleq_s) -VALUES (?, ?, ?, ?, ?, ?, ?); - "#, - ) - .bind(message.to_bytes().to_vec()) - .bind(u64::from(signature.amount) as i64) - .bind(signature.keyset_id.to_string()) - .bind(signature.c.to_bytes().to_vec()) - .bind(quote_id.clone()) - .bind(signature.dleq.as_ref().map(|dleq| dleq.e.to_secret_hex())) - .bind(signature.dleq.as_ref().map(|dleq| dleq.s.to_secret_hex())) - .execute(&mut transaction) - .await; - - if let Err(err) = res { - tracing::error!("SQLite could not add blind signature"); - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - return Err(Error::SQLX(err).into()); - } - } - - transaction.commit().await.map_err(Error::from)?; - - Ok(()) - } - - async fn get_blind_signatures( - &self, - blinded_messages: &[PublicKey], - ) -> Result>, Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - - let mut signatures = Vec::with_capacity(blinded_messages.len()); - - for message in blinded_messages { - let rec = sqlx::query( - r#" -SELECT * -FROM blind_signature -WHERE y=?; - "#, - ) - .bind(message.to_bytes().to_vec()) - .fetch_one(&mut transaction) - .await; - - if let Ok(row) = rec { - let blinded = sqlite_row_to_blind_signature(row)?; - - signatures.push(Some(blinded)); - } else { - signatures.push(None); - } - } - - transaction.commit().await.map_err(Error::from)?; - - Ok(signatures) - } - - async fn get_blind_signatures_for_keyset( - &self, - keyset_id: &Id, - ) -> Result, Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - - let rec = sqlx::query( - r#" -SELECT * -FROM blind_signature -WHERE keyset_id=?; - "#, - ) - .bind(keyset_id.to_string()) - .fetch_all(&mut transaction) - .await; - - match rec { - Ok(rec) => { - transaction.commit().await.map_err(Error::from)?; - let sigs = rec - .into_iter() - .map(sqlite_row_to_blind_signature) - .collect::, _>>()?; - - Ok(sigs) - } - Err(err) => { - tracing::error!("SQLite could not get vlinf signatures for keyset"); - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - - return Err(Error::from(err).into()); - } - } - } - - async fn add_melt_request( - &self, - melt_request: MeltBolt11Request, - ln_key: LnKey, - ) -> Result<(), Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - - let res = sqlx::query( - r#" -INSERT OR REPLACE INTO melt_request -(id, inputs, outputs, method, unit) -VALUES (?, ?, ?, ?, ?); - "#, - ) - .bind(melt_request.quote) - .bind(serde_json::to_string(&melt_request.inputs)?) - .bind(serde_json::to_string(&melt_request.outputs)?) - .bind(ln_key.method.to_string()) - .bind(ln_key.unit.to_string()) - .execute(&mut transaction) - .await; - - match res { - Ok(_) => { - transaction.commit().await.map_err(Error::from)?; - Ok(()) - } - Err(err) => { - tracing::error!("SQLite Could not update keyset"); - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - - Err(Error::from(err).into()) - } - } - } - - async fn get_melt_request( - &self, - quote_id: &str, - ) -> Result, Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - - let rec = sqlx::query( - r#" -SELECT * -FROM melt_request -WHERE id=?; - "#, - ) - .bind(quote_id) - .fetch_one(&mut transaction) - .await; - - match rec { - Ok(rec) => { - transaction.commit().await.map_err(Error::from)?; - - let (request, key) = sqlite_row_to_melt_request(rec)?; - - Ok(Some((request, key))) - } - Err(err) => match err { - sqlx::Error::RowNotFound => { - transaction.commit().await.map_err(Error::from)?; - return Ok(None); - } - _ => { - return { - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - Err(Error::SQLX(err).into()) - } - } - }, - } - } - - /// Get [`BlindSignature`]s for quote - async fn get_blind_signatures_for_quote( - &self, - quote_id: &str, - ) -> Result, Self::Err> { - let mut transaction = self.pool.begin().await.map_err(Error::from)?; - - let recs = sqlx::query( - r#" -SELECT * -FROM blind_signature -WHERE quote_id=?; - "#, - ) - .bind(quote_id) - .fetch_all(&mut transaction) - .await; - - match recs { - Ok(recs) => { - transaction.commit().await.map_err(Error::from)?; - - let keysets = recs - .into_iter() - .map(sqlite_row_to_blind_signature) - .collect::, _>>()?; - Ok(keysets) - } - Err(err) => { - tracing::error!("SQLite could not get active keyset"); - if let Err(err) = transaction.rollback().await { - tracing::error!("Could not rollback sql transaction: {}", err); - } - Err(Error::from(err).into()) - } - } - } -} - -fn sqlite_row_to_keyset_info(row: SqliteRow) -> Result { - let row_id: String = row.try_get("id").map_err(Error::from)?; - let row_unit: String = row.try_get("unit").map_err(Error::from)?; - let row_active: bool = row.try_get("active").map_err(Error::from)?; - let row_valid_from: i64 = row.try_get("valid_from").map_err(Error::from)?; - let row_valid_to: Option = row.try_get("valid_to").map_err(Error::from)?; - let row_derivation_path: String = row.try_get("derivation_path").map_err(Error::from)?; - let row_max_order: u8 = row.try_get("max_order").map_err(Error::from)?; - let row_keyset_ppk: Option = row.try_get("input_fee_ppk").map_err(Error::from)?; - let row_derivation_path_index: Option = - row.try_get("derivation_path_index").map_err(Error::from)?; - - Ok(MintKeySetInfo { - id: Id::from_str(&row_id).map_err(Error::from)?, - unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?, - active: row_active, - valid_from: row_valid_from as u64, - valid_to: row_valid_to.map(|v| v as u64), - derivation_path: DerivationPath::from_str(&row_derivation_path).map_err(Error::from)?, - derivation_path_index: row_derivation_path_index.map(|d| d as u32), - max_order: row_max_order, - input_fee_ppk: row_keyset_ppk.unwrap_or(0) as u64, - }) -} - -fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result { - let row_id: String = row.try_get("id").map_err(Error::from)?; - let row_mint_url: String = row.try_get("mint_url").map_err(Error::from)?; - let row_amount: i64 = row.try_get("amount").map_err(Error::from)?; - let row_unit: String = row.try_get("unit").map_err(Error::from)?; - let row_request: String = row.try_get("request").map_err(Error::from)?; - let row_state: String = row.try_get("state").map_err(Error::from)?; - let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?; - let row_request_lookup_id: Option = - row.try_get("request_lookup_id").map_err(Error::from)?; - - let request_lookup_id = match row_request_lookup_id { - Some(id) => id, - None => match Bolt11Invoice::from_str(&row_request) { - Ok(invoice) => invoice.payment_hash().to_string(), - Err(_) => row_request.clone(), - }, - }; - - Ok(MintQuote { - id: row_id, - mint_url: MintUrl::from_str(&row_mint_url)?, - amount: Amount::from(row_amount as u64), - unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?, - request: row_request, - state: MintQuoteState::from_str(&row_state).map_err(Error::from)?, - expiry: row_expiry as u64, - request_lookup_id, - }) -} - -fn sqlite_row_to_melt_quote(row: SqliteRow) -> Result { - let row_id: String = row.try_get("id").map_err(Error::from)?; - let row_unit: String = row.try_get("unit").map_err(Error::from)?; - let row_amount: i64 = row.try_get("amount").map_err(Error::from)?; - let row_request: String = row.try_get("request").map_err(Error::from)?; - let row_fee_reserve: i64 = row.try_get("fee_reserve").map_err(Error::from)?; - let row_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 = row.try_get("payment_preimage").map_err(Error::from)?; - let row_request_lookup: Option = - row.try_get("request_lookup_id").map_err(Error::from)?; - - let request_lookup_id = row_request_lookup.unwrap_or(row_request.clone()); - - Ok(mint::MeltQuote { - id: row_id, - amount: Amount::from(row_amount as u64), - unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?, - request: row_request, - fee_reserve: Amount::from(row_fee_reserve as u64), - state: QuoteState::from_str(&row_state)?, - expiry: row_expiry as u64, - payment_preimage: row_preimage, - request_lookup_id, - }) -} - -fn sqlite_row_to_proof(row: SqliteRow) -> Result { - let row_amount: i64 = row.try_get("amount").map_err(Error::from)?; - let keyset_id: String = row.try_get("keyset_id").map_err(Error::from)?; - let row_secret: String = row.try_get("secret").map_err(Error::from)?; - let row_c: Vec = row.try_get("c").map_err(Error::from)?; - let row_witness: Option = row.try_get("witness").map_err(Error::from)?; - - Ok(Proof { - amount: Amount::from(row_amount as u64), - keyset_id: Id::from_str(&keyset_id)?, - secret: Secret::from_str(&row_secret)?, - c: PublicKey::from_slice(&row_c)?, - witness: row_witness.and_then(|w| serde_json::from_str(&w).ok()), - dleq: None, - }) -} - -fn sqlite_row_to_proof_with_state(row: SqliteRow) -> Result<(Proof, Option), Error> { - let row_amount: i64 = row.try_get("amount").map_err(Error::from)?; - let keyset_id: String = row.try_get("keyset_id").map_err(Error::from)?; - let row_secret: String = row.try_get("secret").map_err(Error::from)?; - let row_c: Vec = row.try_get("c").map_err(Error::from)?; - let row_witness: Option = row.try_get("witness").map_err(Error::from)?; - - let row_state: Option = row.try_get("state").map_err(Error::from)?; - - let state = row_state.and_then(|s| State::from_str(&s).ok()); - - Ok(( - Proof { - amount: Amount::from(row_amount as u64), - keyset_id: Id::from_str(&keyset_id)?, - secret: Secret::from_str(&row_secret)?, - c: PublicKey::from_slice(&row_c)?, - witness: row_witness.and_then(|w| serde_json::from_str(&w).ok()), - dleq: None, - }, - state, - )) -} - -fn sqlite_row_to_blind_signature(row: SqliteRow) -> Result { - let row_amount: i64 = row.try_get("amount").map_err(Error::from)?; - let keyset_id: String = row.try_get("keyset_id").map_err(Error::from)?; - let row_c: Vec = row.try_get("c").map_err(Error::from)?; - let row_dleq_e: Option = row.try_get("dleq_e").map_err(Error::from)?; - let row_dleq_s: Option = row.try_get("dleq_s").map_err(Error::from)?; - - let dleq = match (row_dleq_e, row_dleq_s) { - (Some(e), Some(s)) => Some(BlindSignatureDleq { - e: SecretKey::from_hex(e)?, - s: SecretKey::from_hex(s)?, - }), - _ => None, - }; - - Ok(BlindSignature { - amount: Amount::from(row_amount as u64), - keyset_id: Id::from_str(&keyset_id)?, - c: PublicKey::from_slice(&row_c)?, - dleq, - }) -} - -fn sqlite_row_to_melt_request(row: SqliteRow) -> Result<(MeltBolt11Request, LnKey), Error> { - let quote_id: String = row.try_get("id").map_err(Error::from)?; - let row_inputs: String = row.try_get("inputs").map_err(Error::from)?; - let row_outputs: Option = row.try_get("outputs").map_err(Error::from)?; - let row_method: String = row.try_get("method").map_err(Error::from)?; - let row_unit: String = row.try_get("unit").map_err(Error::from)?; - - let melt_request = MeltBolt11Request { - quote: quote_id, - inputs: serde_json::from_str(&row_inputs)?, - outputs: row_outputs.and_then(|o| serde_json::from_str(&o).ok()), - }; - - let ln_key = LnKey { - unit: CurrencyUnit::from_str(&row_unit)?, - method: PaymentMethod::from_str(&row_method)?, - }; - - Ok((melt_request, ln_key)) -} diff --git a/crates/cdk-sqlite/src/wallet/error.rs b/crates/cdk-sqlite/src/wallet/error.rs deleted file mode 100644 index bfebdf26..00000000 --- a/crates/cdk-sqlite/src/wallet/error.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! SQLite Wallet Error - -use thiserror::Error; - -/// SQLite Wallet Error -#[derive(Debug, Error)] -pub enum Error { - /// SQLX Error - #[error(transparent)] - SQLX(#[from] sqlx::Error), - /// Serde Error - #[error(transparent)] - Serde(#[from] serde_json::Error), - /// NUT00 Error - #[error(transparent)] - CDKNUT00(#[from] cdk::nuts::nut00::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), - /// NUT07 Error - #[error(transparent)] - CDKNUT07(#[from] cdk::nuts::nut07::Error), - /// Secret Error - #[error(transparent)] - CDKSECRET(#[from] cdk::secret::Error), - /// Mint Url - #[error(transparent)] - MintUrl(#[from] cdk::mint_url::Error), - /// BIP32 Error - #[error(transparent)] - BIP32(#[from] bitcoin::bip32::Error), - /// Could Not Initialize Database - #[error("Could not initialize database")] - CouldNotInitialize, - /// Invalid Database Path - #[error("Invalid database path")] - InvalidDbPath, -} - -impl From for cdk::cdk_database::Error { - fn from(e: Error) -> Self { - Self::Database(Box::new(e)) - } -} diff --git a/crates/cdk-sqlite/src/wallet/migrations/20240612132920_init.sql b/crates/cdk-sqlite/src/wallet/migrations/20240612132920_init.sql deleted file mode 100644 index 070d10b8..00000000 --- a/crates/cdk-sqlite/src/wallet/migrations/20240612132920_init.sql +++ /dev/null @@ -1,81 +0,0 @@ --- Mints -CREATE TABLE IF NOT EXISTS mint ( - mint_url TEXT PRIMARY KEY, - name TEXT, - pubkey BLOB, - version TEXT, - description TEXT, - description_long TEXT, - contact TEXT, - nuts TEXT, - motd TEXT -); - - -CREATE TABLE IF NOT EXISTS keyset ( - id TEXT PRIMARY KEY, - mint_url TEXT NOT NULL, - unit TEXT NOT NULL, - active BOOL NOT NULL, - counter INTEGER NOT NULL DEFAULT 0, - FOREIGN KEY(mint_url) REFERENCES mint(mint_url) ON UPDATE CASCADE ON DELETE CASCADE -); - -CREATE TABLE IF NOT EXISTS mint_quote ( - id TEXT PRIMARY KEY, - mint_url TEXT NOT NULL, - amount INTEGER NOT NULL, - unit TEXT NOT NULL, - request TEXT NOT NULL, - paid BOOL NOT NULL DEFAULT FALSE, - expiry INTEGER NOT NULL -); - - -CREATE INDEX IF NOT EXISTS paid_index ON mint_quote(paid); -CREATE INDEX IF NOT EXISTS request_index ON mint_quote(request); - -CREATE TABLE IF NOT EXISTS melt_quote ( - id TEXT PRIMARY KEY, - unit TEXT NOT NULL, - amount INTEGER NOT NULL, - request TEXT NOT NULL, - fee_reserve INTEGER NOT NULL, - paid BOOL NOT NULL DEFAULT FALSE, - expiry INTEGER NOT NULL -); - -CREATE INDEX IF NOT EXISTS paid_index ON melt_quote(paid); -CREATE INDEX IF NOT EXISTS request_index ON melt_quote(request); - -CREATE TABLE IF NOT EXISTS key ( - id TEXT PRIMARY KEY, - keys TEXT NOT NULL -); - - --- Proof Table -CREATE TABLE IF NOT EXISTS proof ( -y BLOB PRIMARY KEY, -mint_url TEXT NOT NULL, -state TEXT CHECK ( state IN ('SPENT', 'UNSPENT', 'PENDING', 'RESERVED' ) ) NOT NULL, -spending_condition TEXT, -unit TEXT NOT NULL, -amount INTEGER NOT NULL, -keyset_id TEXT NOT NULL, -secret TEXT NOT NULL, -c BLOB NOT NULL, -witness TEXT -); - -CREATE INDEX IF NOT EXISTS secret_index ON proof(secret); -CREATE INDEX IF NOT EXISTS state_index ON proof(state); -CREATE INDEX IF NOT EXISTS spending_condition_index ON proof(spending_condition); -CREATE INDEX IF NOT EXISTS unit_index ON proof(unit); -CREATE INDEX IF NOT EXISTS amount_index ON proof(amount); -CREATE INDEX IF NOT EXISTS mint_url_index ON proof(mint_url); - -CREATE TABLE IF NOT EXISTS nostr_last_checked ( - key BLOB PRIMARY KEY, - last_check INTEGER NOT NULL -); diff --git a/crates/cdk-sqlite/src/wallet/migrations/20240618200350_quote_state.sql b/crates/cdk-sqlite/src/wallet/migrations/20240618200350_quote_state.sql deleted file mode 100644 index 82d8d479..00000000 --- a/crates/cdk-sqlite/src/wallet/migrations/20240618200350_quote_state.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE melt_quote ADD state TEXT CHECK ( state IN ('UNPAID', 'PENDING', 'PAID' ) ) NOT NULL DEFAULT 'UNPAID'; -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; diff --git a/crates/cdk-sqlite/src/wallet/migrations/20240626091921_nut04_state.sql b/crates/cdk-sqlite/src/wallet/migrations/20240626091921_nut04_state.sql deleted file mode 100644 index ec3918e2..00000000 --- a/crates/cdk-sqlite/src/wallet/migrations/20240626091921_nut04_state.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE mint_quote ADD state TEXT CHECK ( state IN ('UNPAID', 'PENDING', 'PAID', 'ISSUED' ) ) NOT NULL DEFAULT 'UNPAID'; -ALTER TABLE mint_quote DROP COLUMN paid; -CREATE INDEX IF NOT EXISTS mint_quote_state_index ON mint_quote(state); diff --git a/crates/cdk-sqlite/src/wallet/migrations/20240710144711_input_fee.sql b/crates/cdk-sqlite/src/wallet/migrations/20240710144711_input_fee.sql deleted file mode 100644 index 6bdd8cd0..00000000 --- a/crates/cdk-sqlite/src/wallet/migrations/20240710144711_input_fee.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE keyset ADD input_fee_ppk INTEGER; diff --git a/crates/cdk-sqlite/src/wallet/migrations/20240810214105_mint_icon_url.sql b/crates/cdk-sqlite/src/wallet/migrations/20240810214105_mint_icon_url.sql deleted file mode 100644 index 4b9983d1..00000000 --- a/crates/cdk-sqlite/src/wallet/migrations/20240810214105_mint_icon_url.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE mint ADD mint_icon_url TEXT; diff --git a/crates/cdk-sqlite/src/wallet/migrations/20240810233905_update_mint_url.sql b/crates/cdk-sqlite/src/wallet/migrations/20240810233905_update_mint_url.sql deleted file mode 100644 index be0e2d22..00000000 --- a/crates/cdk-sqlite/src/wallet/migrations/20240810233905_update_mint_url.sql +++ /dev/null @@ -1,21 +0,0 @@ --- Delete duplicates from `mint` -DELETE FROM `mint` -WHERE `mint_url` IN ( - SELECT `mint_url` - FROM ( - SELECT RTRIM(`mint_url`, '/') AS trimmed_url, MIN(rowid) AS keep_id - FROM `mint` - GROUP BY trimmed_url - HAVING COUNT(*) > 1 - ) -) -AND rowid NOT IN ( - SELECT MIN(rowid) - FROM `mint` - GROUP BY RTRIM(`mint_url`, '/') -); - -UPDATE `mint` SET `mint_url` = RTRIM(`mint_url`, '/'); -UPDATE `keyset` SET `mint_url` = RTRIM(`mint_url`, '/'); -UPDATE `mint_quote` SET `mint_url` = RTRIM(`mint_url`, '/'); -UPDATE `proof` SET `mint_url` = RTRIM(`mint_url`, '/'); diff --git a/crates/cdk-sqlite/src/wallet/migrations/20240902151515_icon_url.sql b/crates/cdk-sqlite/src/wallet/migrations/20240902151515_icon_url.sql deleted file mode 100644 index bd672d4b..00000000 --- a/crates/cdk-sqlite/src/wallet/migrations/20240902151515_icon_url.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE mint RENAME COLUMN mint_icon_url TO icon_url; diff --git a/crates/cdk-sqlite/src/wallet/migrations/20240902210905_mint_time.sql b/crates/cdk-sqlite/src/wallet/migrations/20240902210905_mint_time.sql deleted file mode 100644 index 7768dd30..00000000 --- a/crates/cdk-sqlite/src/wallet/migrations/20240902210905_mint_time.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE mint ADD mint_time INTEGER; diff --git a/crates/cdk-sqlite/src/wallet/mod.rs b/crates/cdk-sqlite/src/wallet/mod.rs deleted file mode 100644 index f45c6bb4..00000000 --- a/crates/cdk-sqlite/src/wallet/mod.rs +++ /dev/null @@ -1,886 +0,0 @@ -//! SQLite Wallet Database - -use std::collections::HashMap; -use std::path::Path; -use std::str::FromStr; - -use async_trait::async_trait; -use cdk::amount::Amount; -use cdk::cdk_database::{self, WalletDatabase}; -use cdk::mint_url::MintUrl; -use cdk::nuts::{ - CurrencyUnit, Id, KeySetInfo, Keys, MeltQuoteState, MintInfo, MintQuoteState, Proof, PublicKey, - SpendingConditions, State, -}; -use cdk::secret::Secret; -use cdk::types::ProofInfo; -use cdk::wallet; -use cdk::wallet::MintQuote; -use error::Error; -use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqliteRow}; -use sqlx::{ConnectOptions, Row}; -use tracing::instrument; - -pub mod error; - -/// Wallet SQLite Database -#[derive(Debug, Clone)] -pub struct WalletSqliteDatabase { - pool: SqlitePool, -} - -impl WalletSqliteDatabase { - /// Create new [`WalletSqliteDatabase`] - pub async fn new(path: &Path) -> Result { - let path = path.to_str().ok_or(Error::InvalidDbPath)?; - let _conn = SqliteConnectOptions::from_str(path)? - .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) - .read_only(false) - .create_if_missing(true) - .auto_vacuum(sqlx::sqlite::SqliteAutoVacuum::Full) - .connect() - .await?; - - let pool = SqlitePool::connect(path).await?; - - Ok(Self { pool }) - } - - /// Migrate [`WalletSqliteDatabase`] - pub async fn migrate(&self) { - sqlx::migrate!("./src/wallet/migrations") - .run(&self.pool) - .await - .expect("Could not run migrations"); - } - - async fn set_proof_state(&self, y: PublicKey, state: State) -> Result<(), cdk_database::Error> { - sqlx::query( - r#" - UPDATE proof - SET state=? - WHERE y IS ?; - "#, - ) - .bind(state.to_string()) - .bind(y.to_bytes().to_vec()) - .execute(&self.pool) - .await - .map_err(Error::from)?; - - Ok(()) - } -} - -#[async_trait] -impl WalletDatabase for WalletSqliteDatabase { - type Err = cdk_database::Error; - - #[instrument(skip(self, mint_info))] - async fn add_mint( - &self, - mint_url: MintUrl, - mint_info: Option, - ) -> Result<(), Self::Err> { - let ( - name, - pubkey, - version, - description, - description_long, - contact, - nuts, - icon_url, - motd, - time, - ) = match mint_info { - Some(mint_info) => { - let MintInfo { - name, - pubkey, - version, - description, - description_long, - contact, - nuts, - icon_url, - motd, - time, - } = mint_info; - - ( - name, - pubkey.map(|p| p.to_bytes().to_vec()), - version.map(|v| serde_json::to_string(&v).ok()), - description, - description_long, - contact.map(|c| serde_json::to_string(&c).ok()), - serde_json::to_string(&nuts).ok(), - icon_url, - motd, - time, - ) - } - None => (None, None, None, None, None, None, None, None, None, None), - }; - - sqlx::query( - r#" -INSERT OR REPLACE INTO mint -(mint_url, name, pubkey, version, description, description_long, contact, nuts, icon_url, motd, mint_time) -VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); - "#, - ) - .bind(mint_url.to_string()) - .bind(name) - .bind(pubkey) - .bind(version) - .bind(description) - .bind(description_long) - .bind(contact) - .bind(nuts) - .bind(icon_url) - .bind(motd) - .bind(time.map(|v| v as i64)) - .execute(&self.pool) - .await - .map_err(Error::from)?; - - Ok(()) - } - - #[instrument(skip(self))] - async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), Self::Err> { - sqlx::query( - r#" -DELETE FROM mint -WHERE mint_url=? - "#, - ) - .bind(mint_url.to_string()) - .execute(&self.pool) - .await - .map_err(Error::from)?; - - Ok(()) - } - - #[instrument(skip(self))] - async fn get_mint(&self, mint_url: MintUrl) -> Result, Self::Err> { - let rec = sqlx::query( - r#" -SELECT * -FROM mint -WHERE mint_url=?; - "#, - ) - .bind(mint_url.to_string()) - .fetch_one(&self.pool) - .await; - - let rec = match rec { - Ok(rec) => rec, - Err(err) => match err { - sqlx::Error::RowNotFound => return Ok(None), - _ => return Err(Error::SQLX(err).into()), - }, - }; - - Ok(Some(sqlite_row_to_mint_info(&rec)?)) - } - - #[instrument(skip(self))] - async fn get_mints(&self) -> Result>, Self::Err> { - let rec = sqlx::query( - r#" -SELECT * -FROM mint - "#, - ) - .fetch_all(&self.pool) - .await - .map_err(Error::from)?; - - let mints = rec - .into_iter() - .flat_map(|row| { - let mint_url: String = row.get("mint_url"); - - // Attempt to parse mint_url and convert mint_info - let mint_result = MintUrl::from_str(&mint_url).ok(); - let mint_info = sqlite_row_to_mint_info(&row).ok(); - - // Combine mint_result and mint_info into an Option tuple - mint_result.map(|mint| (mint, mint_info)) - }) - .collect(); - - Ok(mints) - } - - #[instrument(skip(self))] - async fn update_mint_url( - &self, - old_mint_url: MintUrl, - new_mint_url: MintUrl, - ) -> Result<(), Self::Err> { - let tables = ["mint_quote", "proof"]; - for table in &tables { - let query = format!( - r#" - UPDATE {} - SET mint_url = ? - WHERE mint_url = ?; - "#, - table - ); - - sqlx::query(&query) - .bind(new_mint_url.to_string()) - .bind(old_mint_url.to_string()) - .execute(&self.pool) - .await - .map_err(Error::from)?; - } - Ok(()) - } - - #[instrument(skip(self, keysets))] - async fn add_mint_keysets( - &self, - mint_url: MintUrl, - keysets: Vec, - ) -> Result<(), Self::Err> { - for keyset in keysets { - sqlx::query( - r#" -INSERT OR REPLACE INTO keyset -(mint_url, id, unit, active, input_fee_ppk) -VALUES (?, ?, ?, ?, ?); - "#, - ) - .bind(mint_url.to_string()) - .bind(keyset.id.to_string()) - .bind(keyset.unit.to_string()) - .bind(keyset.active) - .bind(keyset.input_fee_ppk as i64) - .execute(&self.pool) - .await - .map_err(Error::from)?; - } - - Ok(()) - } - - #[instrument(skip(self))] - async fn get_mint_keysets( - &self, - mint_url: MintUrl, - ) -> Result>, Self::Err> { - let recs = sqlx::query( - r#" -SELECT * -FROM keyset -WHERE mint_url=? - "#, - ) - .bind(mint_url.to_string()) - .fetch_all(&self.pool) - .await; - - let recs = match recs { - Ok(recs) => recs, - Err(err) => match err { - sqlx::Error::RowNotFound => return Ok(None), - _ => return Err(Error::SQLX(err).into()), - }, - }; - - let keysets = recs - .iter() - .map(sqlite_row_to_keyset) - .collect::, _>>()?; - - match keysets.is_empty() { - false => Ok(Some(keysets)), - true => Ok(None), - } - } - - #[instrument(skip(self), fields(keyset_id = %keyset_id))] - async fn get_keyset_by_id(&self, keyset_id: &Id) -> Result, Self::Err> { - let rec = sqlx::query( - r#" -SELECT * -FROM keyset -WHERE id=? - "#, - ) - .bind(keyset_id.to_string()) - .fetch_one(&self.pool) - .await; - - let rec = match rec { - Ok(recs) => recs, - Err(err) => match err { - sqlx::Error::RowNotFound => return Ok(None), - _ => return Err(Error::SQLX(err).into()), - }, - }; - - Ok(Some(sqlite_row_to_keyset(&rec)?)) - } - - #[instrument(skip_all)] - async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Self::Err> { - sqlx::query( - r#" -INSERT OR REPLACE INTO mint_quote -(id, mint_url, amount, unit, request, state, expiry) -VALUES (?, ?, ?, ?, ?, ?, ?); - "#, - ) - .bind(quote.id.to_string()) - .bind(quote.mint_url.to_string()) - .bind(u64::from(quote.amount) as i64) - .bind(quote.unit.to_string()) - .bind(quote.request) - .bind(quote.state.to_string()) - .bind(quote.expiry as i64) - .execute(&self.pool) - .await - .map_err(Error::from)?; - - Ok(()) - } - - #[instrument(skip(self))] - async fn get_mint_quote(&self, quote_id: &str) -> Result, Self::Err> { - let rec = sqlx::query( - r#" -SELECT * -FROM mint_quote -WHERE id=?; - "#, - ) - .bind(quote_id) - .fetch_one(&self.pool) - .await; - - let rec = match rec { - Ok(rec) => rec, - Err(err) => match err { - sqlx::Error::RowNotFound => return Ok(None), - _ => return Err(Error::SQLX(err).into()), - }, - }; - - Ok(Some(sqlite_row_to_mint_quote(&rec)?)) - } - - #[instrument(skip(self))] - async fn get_mint_quotes(&self) -> Result, Self::Err> { - let rec = sqlx::query( - r#" -SELECT * -FROM mint_quote - "#, - ) - .fetch_all(&self.pool) - .await - .map_err(Error::from)?; - - let mint_quotes = rec - .iter() - .map(sqlite_row_to_mint_quote) - .collect::>()?; - - Ok(mint_quotes) - } - - #[instrument(skip(self))] - async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err> { - sqlx::query( - r#" -DELETE FROM mint_quote -WHERE id=? - "#, - ) - .bind(quote_id) - .execute(&self.pool) - .await - .map_err(Error::from)?; - - Ok(()) - } - - #[instrument(skip_all)] - async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), Self::Err> { - sqlx::query( - r#" -INSERT OR REPLACE INTO melt_quote -(id, unit, amount, request, fee_reserve, state, expiry) -VALUES (?, ?, ?, ?, ?, ?, ?); - "#, - ) - .bind(quote.id.to_string()) - .bind(quote.unit.to_string()) - .bind(u64::from(quote.amount) as i64) - .bind(quote.request) - .bind(u64::from(quote.fee_reserve) as i64) - .bind(quote.state.to_string()) - .bind(quote.expiry as i64) - .execute(&self.pool) - .await - .map_err(Error::from)?; - - Ok(()) - } - - #[instrument(skip(self))] - async fn get_melt_quote(&self, quote_id: &str) -> Result, Self::Err> { - let rec = sqlx::query( - r#" -SELECT * -FROM melt_quote -WHERE id=?; - "#, - ) - .bind(quote_id) - .fetch_one(&self.pool) - .await; - - let rec = match rec { - Ok(rec) => rec, - Err(err) => match err { - sqlx::Error::RowNotFound => return Ok(None), - _ => return Err(Error::SQLX(err).into()), - }, - }; - - Ok(Some(sqlite_row_to_melt_quote(&rec)?)) - } - - #[instrument(skip(self))] - async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err> { - sqlx::query( - r#" -DELETE FROM melt_quote -WHERE id=? - "#, - ) - .bind(quote_id) - .execute(&self.pool) - .await - .map_err(Error::from)?; - - Ok(()) - } - - #[instrument(skip_all)] - async fn add_keys(&self, keys: Keys) -> Result<(), Self::Err> { - sqlx::query( - r#" -INSERT OR REPLACE INTO key -(id, keys) -VALUES (?, ?); - "#, - ) - .bind(Id::from(&keys).to_string()) - .bind(serde_json::to_string(&keys).map_err(Error::from)?) - .execute(&self.pool) - .await - .map_err(Error::from)?; - - Ok(()) - } - - #[instrument(skip(self), fields(keyset_id = %keyset_id))] - async fn get_keys(&self, keyset_id: &Id) -> Result, Self::Err> { - let rec = sqlx::query( - r#" -SELECT * -FROM key -WHERE id=?; - "#, - ) - .bind(keyset_id.to_string()) - .fetch_one(&self.pool) - .await; - - let rec = match rec { - Ok(rec) => rec, - Err(err) => match err { - sqlx::Error::RowNotFound => return Ok(None), - _ => return Err(Error::SQLX(err).into()), - }, - }; - - let keys: String = rec.get("keys"); - - Ok(serde_json::from_str(&keys).map_err(Error::from)?) - } - - #[instrument(skip(self))] - async fn remove_keys(&self, id: &Id) -> Result<(), Self::Err> { - sqlx::query( - r#" -DELETE FROM key -WHERE id=? - "#, - ) - .bind(id.to_string()) - .execute(&self.pool) - .await - .map_err(Error::from)?; - - Ok(()) - } - - async fn update_proofs( - &self, - added: Vec, - removed_ys: Vec, - ) -> Result<(), Self::Err> { - for proof in added { - sqlx::query( - r#" - INSERT OR REPLACE INTO proof - (y, mint_url, state, spending_condition, unit, amount, keyset_id, secret, c, witness) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); - "#, - ) - .bind(proof.y.to_bytes().to_vec()) - .bind(proof.mint_url.to_string()) - .bind(proof.state.to_string()) - .bind( - proof - .spending_condition - .map(|s| serde_json::to_string(&s).ok()), - ) - .bind(proof.unit.to_string()) - .bind(u64::from(proof.proof.amount) as i64) - .bind(proof.proof.keyset_id.to_string()) - .bind(proof.proof.secret.to_string()) - .bind(proof.proof.c.to_bytes().to_vec()) - .bind( - proof - .proof - .witness - .map(|w| serde_json::to_string(&w).unwrap()), - ) - .execute(&self.pool) - .await - .map_err(Error::from)?; - } - - // TODO: Generate a IN clause - for y in removed_ys { - sqlx::query( - r#" - DELETE FROM proof - WHERE y = ? - "#, - ) - .bind(y.to_bytes().to_vec()) - .execute(&self.pool) - .await - .map_err(Error::from)?; - } - - Ok(()) - } - - async fn set_pending_proofs(&self, ys: Vec) -> Result<(), Self::Err> { - for y in ys { - self.set_proof_state(y, State::Pending).await?; - } - - Ok(()) - } - - async fn reserve_proofs(&self, ys: Vec) -> Result<(), Self::Err> { - for y in ys { - self.set_proof_state(y, State::Reserved).await?; - } - - Ok(()) - } - - async fn set_unspent_proofs(&self, ys: Vec) -> Result<(), Self::Err> { - for y in ys { - self.set_proof_state(y, State::Unspent).await?; - } - - Ok(()) - } - - #[instrument(skip(self, state, spending_conditions))] - async fn get_proofs( - &self, - mint_url: Option, - unit: Option, - state: Option>, - spending_conditions: Option>, - ) -> Result, Self::Err> { - let recs = sqlx::query( - r#" -SELECT * -FROM proof; - "#, - ) - .fetch_all(&self.pool) - .await; - - let recs = match recs { - Ok(rec) => rec, - Err(err) => match err { - sqlx::Error::RowNotFound => return Ok(vec![]), - _ => return Err(Error::SQLX(err).into()), - }, - }; - - let proofs: Vec = recs - .iter() - .filter_map(|p| match sqlite_row_to_proof_info(p) { - Ok(proof_info) => { - match proof_info.matches_conditions( - &mint_url, - &unit, - &state, - &spending_conditions, - ) { - true => Some(proof_info), - false => None, - } - } - Err(err) => { - tracing::error!("Could not deserialize proof row: {}", err); - None - } - }) - .collect(); - - match proofs.is_empty() { - false => Ok(proofs), - true => return Ok(vec![]), - } - } - - #[instrument(skip(self), fields(keyset_id = %keyset_id))] - async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> { - sqlx::query( - r#" -UPDATE keyset -SET counter = counter + ? -WHERE id IS ?; - "#, - ) - .bind(count) - .bind(keyset_id.to_string()) - .execute(&self.pool) - .await - .map_err(Error::from)?; - - Ok(()) - } - - #[instrument(skip(self), fields(keyset_id = %keyset_id))] - async fn get_keyset_counter(&self, keyset_id: &Id) -> Result, Self::Err> { - let rec = sqlx::query( - r#" -SELECT counter -FROM keyset -WHERE id=?; - "#, - ) - .bind(keyset_id.to_string()) - .fetch_one(&self.pool) - .await; - - let count = match rec { - Ok(rec) => { - let count: Option = rec.try_get("counter").map_err(Error::from)?; - count - } - Err(err) => match err { - sqlx::Error::RowNotFound => return Ok(None), - _ => return Err(Error::SQLX(err).into()), - }, - }; - - Ok(count) - } - - #[instrument(skip_all)] - async fn get_nostr_last_checked( - &self, - verifying_key: &PublicKey, - ) -> Result, Self::Err> { - let rec = sqlx::query( - r#" -SELECT last_check -FROM nostr_last_checked -WHERE key=?; - "#, - ) - .bind(verifying_key.to_bytes().to_vec()) - .fetch_one(&self.pool) - .await; - - let count = match rec { - Ok(rec) => { - let count: Option = rec.try_get("last_check").map_err(Error::from)?; - count - } - Err(err) => match err { - sqlx::Error::RowNotFound => return Ok(None), - _ => return Err(Error::SQLX(err).into()), - }, - }; - - Ok(count) - } - - #[instrument(skip_all)] - async fn add_nostr_last_checked( - &self, - verifying_key: PublicKey, - last_checked: u32, - ) -> Result<(), Self::Err> { - sqlx::query( - r#" -INSERT OR REPLACE INTO nostr_last_checked -(key, last_check) -VALUES (?, ?); - "#, - ) - .bind(verifying_key.to_bytes().to_vec()) - .bind(last_checked) - .execute(&self.pool) - .await - .map_err(Error::from)?; - - Ok(()) - } -} - -fn sqlite_row_to_mint_info(row: &SqliteRow) -> Result { - let name: Option = row.try_get("name").map_err(Error::from)?; - let row_pubkey: Option> = row.try_get("pubkey").map_err(Error::from)?; - let row_version: Option = row.try_get("version").map_err(Error::from)?; - let description: Option = row.try_get("description").map_err(Error::from)?; - let description_long: Option = row.try_get("description_long").map_err(Error::from)?; - let row_contact: Option = row.try_get("contact").map_err(Error::from)?; - let row_nuts: Option = row.try_get("nuts").map_err(Error::from)?; - let icon_url: Option = row.try_get("icon_url").map_err(Error::from)?; - let motd: Option = row.try_get("motd").map_err(Error::from)?; - let time: Option = row.try_get("mint_time").map_err(Error::from)?; - - Ok(MintInfo { - name, - pubkey: row_pubkey.and_then(|p| PublicKey::from_slice(&p).ok()), - version: row_version.and_then(|v| serde_json::from_str(&v).ok()), - description, - description_long, - contact: row_contact.and_then(|c| serde_json::from_str(&c).ok()), - nuts: row_nuts - .and_then(|n| serde_json::from_str(&n).ok()) - .unwrap_or_default(), - icon_url, - motd, - time: time.map(|t| t as u64), - }) -} - -fn sqlite_row_to_keyset(row: &SqliteRow) -> Result { - let row_id: String = row.try_get("id").map_err(Error::from)?; - let row_unit: String = row.try_get("unit").map_err(Error::from)?; - let active: bool = row.try_get("active").map_err(Error::from)?; - let row_keyset_ppk: Option = row.try_get("input_fee_ppk").map_err(Error::from)?; - - Ok(KeySetInfo { - id: Id::from_str(&row_id)?, - unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?, - active, - input_fee_ppk: row_keyset_ppk.unwrap_or(0) as u64, - }) -} - -fn sqlite_row_to_mint_quote(row: &SqliteRow) -> Result { - let row_id: String = row.try_get("id").map_err(Error::from)?; - let row_mint_url: String = row.try_get("mint_url").map_err(Error::from)?; - let row_amount: i64 = row.try_get("amount").map_err(Error::from)?; - let row_unit: String = row.try_get("unit").map_err(Error::from)?; - let row_request: String = row.try_get("request").map_err(Error::from)?; - let row_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: MintUrl::from_str(&row_mint_url)?, - amount: Amount::from(row_amount as u64), - unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?, - request: row_request, - state, - expiry: row_expiry as u64, - }) -} - -fn sqlite_row_to_melt_quote(row: &SqliteRow) -> Result { - let row_id: String = row.try_get("id").map_err(Error::from)?; - let row_unit: String = row.try_get("unit").map_err(Error::from)?; - let row_amount: i64 = row.try_get("amount").map_err(Error::from)?; - let row_request: String = row.try_get("request").map_err(Error::from)?; - let row_fee_reserve: i64 = row.try_get("fee_reserve").map_err(Error::from)?; - let row_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 = row.try_get("payment_preimage").map_err(Error::from)?; - - let state = MeltQuoteState::from_str(&row_state)?; - Ok(wallet::MeltQuote { - id: row_id, - amount: Amount::from(row_amount as u64), - unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?, - request: row_request, - fee_reserve: Amount::from(row_fee_reserve as u64), - state, - expiry: row_expiry as u64, - payment_preimage: row_preimage, - }) -} - -fn sqlite_row_to_proof_info(row: &SqliteRow) -> Result { - let row_amount: i64 = row.try_get("amount").map_err(Error::from)?; - let keyset_id: String = row.try_get("keyset_id").map_err(Error::from)?; - let row_secret: String = row.try_get("secret").map_err(Error::from)?; - let row_c: Vec = row.try_get("c").map_err(Error::from)?; - let row_witness: Option = row.try_get("witness").map_err(Error::from)?; - - let y: Vec = row.try_get("y").map_err(Error::from)?; - let row_mint_url: String = row.try_get("mint_url").map_err(Error::from)?; - let row_state: String = row.try_get("state").map_err(Error::from)?; - let row_spending_condition: Option = - row.try_get("spending_condition").map_err(Error::from)?; - let row_unit: String = row.try_get("unit").map_err(Error::from)?; - - let proof = Proof { - amount: Amount::from(row_amount as u64), - keyset_id: Id::from_str(&keyset_id)?, - secret: Secret::from_str(&row_secret)?, - c: PublicKey::from_slice(&row_c)?, - witness: row_witness.and_then(|w| serde_json::from_str(&w).ok()), - dleq: None, - }; - - Ok(ProofInfo { - proof, - y: PublicKey::from_slice(&y)?, - mint_url: MintUrl::from_str(&row_mint_url)?, - state: State::from_str(&row_state)?, - spending_condition: row_spending_condition.and_then(|r| serde_json::from_str(&r).ok()), - unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?, - }) -} diff --git a/crates/cdk-strike/Cargo.toml b/crates/cdk-strike/Cargo.toml deleted file mode 100644 index c1ccf737..00000000 --- a/crates/cdk-strike/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "cdk-strike" -version = "0.4.0" -edition = "2021" -authors = ["CDK Developers"] -license = "MIT" -homepage = "https://github.com/cashubtc/cdk" -repository = "https://github.com/cashubtc/cdk.git" -rust-version = "1.63.0" # MSRV -description = "CDK ln backend for Strike api" - -[dependencies] -async-trait = "0.1" -anyhow = "1" -axum = "0.6.20" -bitcoin = { version = "0.32.2", default-features = false } -cdk = { path = "../cdk", version = "0.4.0", default-features = false, features = ["mint"] } -futures = { version = "0.3.28", default-features = false } -tokio = { version = "1", default-features = false } -tokio-util = { version = "0.7.11", default-features = false } -tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] } -thiserror = "1" -uuid = { version = "1", features = ["v4"] } -# strike-rs = "0.3.0" -# strike-rs = { path = "../../../../strike-rs" } -strike-rs = { git = "https://github.com/thesimplekid/strike-rs.git", rev = "c6167ce" } diff --git a/crates/cdk-strike/src/error.rs b/crates/cdk-strike/src/error.rs deleted file mode 100644 index b9915d8d..00000000 --- a/crates/cdk-strike/src/error.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Error for Strike ln backend - -use thiserror::Error; - -/// Strike Error -#[derive(Debug, Error)] -pub enum Error { - /// Invoice amount not defined - #[error("Unknown invoice amount")] - UnknownInvoiceAmount, - /// Unknown invoice - #[error("Unknown invoice")] - UnknownInvoice, - /// Strikers error - #[error(transparent)] - StrikeRs(#[from] strike_rs::Error), - /// Anyhow error - #[error(transparent)] - Anyhow(#[from] anyhow::Error), -} - -impl From for cdk::cdk_lightning::Error { - fn from(e: Error) -> Self { - Self::Lightning(Box::new(e)) - } -} diff --git a/crates/cdk-strike/src/lib.rs b/crates/cdk-strike/src/lib.rs deleted file mode 100644 index 76eb0f25..00000000 --- a/crates/cdk-strike/src/lib.rs +++ /dev/null @@ -1,390 +0,0 @@ -//! CDK lightning backend for Strike - -#![warn(missing_docs)] -#![warn(rustdoc::bare_urls)] - -use std::pin::Pin; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; - -use anyhow::{anyhow, bail}; -use async_trait::async_trait; -use axum::Router; -use cdk::amount::Amount; -use cdk::cdk_lightning::{ - self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, -}; -use cdk::nuts::{ - CurrencyUnit, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteState, MintMethodSettings, - MintQuoteState, -}; -use cdk::util::unix_time; -use cdk::{mint, Bolt11Invoice}; -use error::Error; -use futures::stream::StreamExt; -use futures::Stream; -use strike_rs::{ - Amount as StrikeAmount, Currency as StrikeCurrencyUnit, InvoiceRequest, InvoiceState, - PayInvoiceQuoteRequest, Strike as StrikeApi, -}; -use tokio::sync::Mutex; -use tokio_util::sync::CancellationToken; -use uuid::Uuid; - -pub mod error; - -/// Strike -#[derive(Clone)] -pub struct Strike { - strike_api: StrikeApi, - mint_settings: MintMethodSettings, - melt_settings: MeltMethodSettings, - unit: CurrencyUnit, - receiver: Arc>>>, - webhook_url: String, - wait_invoice_cancel_token: CancellationToken, - wait_invoice_is_active: Arc, -} - -impl Strike { - /// Create new [`Strike`] wallet - pub async fn new( - api_key: String, - mint_settings: MintMethodSettings, - melt_settings: MeltMethodSettings, - unit: CurrencyUnit, - receiver: Arc>>>, - webhook_url: String, - ) -> Result { - let strike = StrikeApi::new(&api_key, None)?; - Ok(Self { - strike_api: strike, - mint_settings, - melt_settings, - receiver, - unit, - webhook_url, - wait_invoice_cancel_token: CancellationToken::new(), - wait_invoice_is_active: Arc::new(AtomicBool::new(false)), - }) - } -} - -#[async_trait] -impl MintLightning for Strike { - type Err = cdk_lightning::Error; - - fn get_settings(&self) -> Settings { - Settings { - mpp: false, - unit: self.unit, - mint_settings: self.mint_settings, - melt_settings: self.melt_settings, - invoice_description: true, - } - } - - fn is_wait_invoice_active(&self) -> bool { - self.wait_invoice_is_active.load(Ordering::SeqCst) - } - - fn cancel_wait_invoice(&self) { - self.wait_invoice_cancel_token.cancel() - } - - #[allow(clippy::incompatible_msrv)] - async fn wait_any_invoice( - &self, - ) -> Result + Send>>, Self::Err> { - self.strike_api - .subscribe_to_invoice_webhook(self.webhook_url.clone()) - .await?; - - let receiver = self - .receiver - .lock() - .await - .take() - .ok_or(anyhow!("No receiver"))?; - - let strike_api = self.strike_api.clone(); - let cancel_token = self.wait_invoice_cancel_token.clone(); - - Ok(futures::stream::unfold( - ( - receiver, - strike_api, - cancel_token, - Arc::clone(&self.wait_invoice_is_active), - ), - |(mut receiver, strike_api, cancel_token, is_active)| async move { - tokio::select! { - - _ = cancel_token.cancelled() => { - // Stream is cancelled - is_active.store(false, Ordering::SeqCst); - tracing::info!("Waiting for phonixd invoice ending"); - None - } - - msg_option = receiver.recv() => { - match msg_option { - Some(msg) => { - let check = strike_api.get_incoming_invoice(&msg).await; - - match check { - Ok(state) => { - if state.state == InvoiceState::Paid { - Some((msg, (receiver, strike_api, cancel_token, is_active))) - } else { - None - } - } - _ => None, - } - } - None => None, - } - - } - } - }, - ) - .boxed()) - } - - async fn get_payment_quote( - &self, - melt_quote_request: &MeltQuoteBolt11Request, - ) -> Result { - if melt_quote_request.unit != self.unit { - return Err(Self::Err::Anyhow(anyhow!("Unsupported unit"))); - } - - let source_currency = match melt_quote_request.unit { - CurrencyUnit::Sat => StrikeCurrencyUnit::BTC, - CurrencyUnit::Msat => StrikeCurrencyUnit::BTC, - CurrencyUnit::Usd => StrikeCurrencyUnit::USD, - CurrencyUnit::Eur => StrikeCurrencyUnit::EUR, - _ => return Err(Self::Err::UnsupportedUnit), - }; - - let payment_quote_request = PayInvoiceQuoteRequest { - ln_invoice: melt_quote_request.request.to_string(), - source_currency, - }; - - let quote = self.strike_api.payment_quote(payment_quote_request).await?; - - let fee = from_strike_amount(quote.lightning_network_fee, &melt_quote_request.unit)?; - - Ok(PaymentQuoteResponse { - request_lookup_id: quote.payment_quote_id, - amount: from_strike_amount(quote.amount, &melt_quote_request.unit)?.into(), - fee: fee.into(), - state: MeltQuoteState::Unpaid, - }) - } - - async fn pay_invoice( - &self, - melt_quote: mint::MeltQuote, - _partial_msats: Option, - _max_fee_msats: Option, - ) -> Result { - let pay_response = self - .strike_api - .pay_quote(&melt_quote.request_lookup_id) - .await?; - - let state = match pay_response.state { - InvoiceState::Paid => MeltQuoteState::Paid, - InvoiceState::Unpaid => MeltQuoteState::Unpaid, - InvoiceState::Completed => MeltQuoteState::Paid, - InvoiceState::Pending => MeltQuoteState::Pending, - }; - - let total_spent = from_strike_amount(pay_response.total_amount, &melt_quote.unit)?.into(); - - Ok(PayInvoiceResponse { - payment_lookup_id: pay_response.payment_id, - payment_preimage: None, - status: state, - total_spent, - unit: melt_quote.unit, - }) - } - - async fn create_invoice( - &self, - amount: Amount, - _unit: &CurrencyUnit, - description: String, - unix_expiry: u64, - ) -> Result { - let time_now = unix_time(); - assert!(unix_expiry > time_now); - let request_lookup_id = Uuid::new_v4(); - - let invoice_request = InvoiceRequest { - correlation_id: Some(request_lookup_id.to_string()), - amount: to_strike_unit(amount, &self.unit)?, - description: Some(description), - }; - - let create_invoice_response = self.strike_api.create_invoice(invoice_request).await?; - - let quote = self - .strike_api - .invoice_quote(&create_invoice_response.invoice_id) - .await?; - - let request: Bolt11Invoice = quote.ln_invoice.parse()?; - let expiry = request.expires_at().map(|t| t.as_secs()); - - Ok(CreateInvoiceResponse { - request_lookup_id: create_invoice_response.invoice_id, - request: quote.ln_invoice.parse()?, - expiry, - }) - } - - async fn check_incoming_invoice_status( - &self, - request_lookup_id: &str, - ) -> Result { - let invoice = self - .strike_api - .get_incoming_invoice(request_lookup_id) - .await?; - - let state = match invoice.state { - InvoiceState::Paid => MintQuoteState::Paid, - InvoiceState::Unpaid => MintQuoteState::Unpaid, - InvoiceState::Completed => MintQuoteState::Paid, - InvoiceState::Pending => MintQuoteState::Pending, - }; - - Ok(state) - } - - async fn check_outgoing_payment( - &self, - payment_id: &str, - ) -> Result { - let invoice = self.strike_api.get_outgoing_payment(payment_id).await; - - let pay_invoice_response = match invoice { - Ok(invoice) => { - let state = match invoice.state { - InvoiceState::Paid => MeltQuoteState::Paid, - InvoiceState::Unpaid => MeltQuoteState::Unpaid, - InvoiceState::Completed => MeltQuoteState::Paid, - InvoiceState::Pending => MeltQuoteState::Pending, - }; - - PayInvoiceResponse { - payment_lookup_id: invoice.payment_id, - payment_preimage: None, - status: state, - total_spent: from_strike_amount(invoice.total_amount, &self.unit)?.into(), - unit: self.unit, - } - } - Err(err) => match err { - strike_rs::Error::NotFound => PayInvoiceResponse { - payment_lookup_id: payment_id.to_string(), - payment_preimage: None, - status: MeltQuoteState::Unknown, - total_spent: Amount::ZERO, - unit: self.unit, - }, - _ => { - return Err(Error::from(err).into()); - } - }, - }; - - Ok(pay_invoice_response) - } -} - -impl Strike { - /// Create invoice webhook - pub async fn create_invoice_webhook( - &self, - webhook_endpoint: &str, - sender: tokio::sync::mpsc::Sender, - ) -> anyhow::Result { - let subs = self.strike_api.get_current_subscriptions().await?; - - tracing::debug!("Got {} current subscriptions", subs.len()); - - for sub in subs { - tracing::info!("Deleting webhook: {}", &sub.id); - if let Err(err) = self.strike_api.delete_subscription(&sub.id).await { - tracing::error!("Error deleting webhook subscription: {} {}", sub.id, err); - } - } - - self.strike_api - .create_invoice_webhook_router(webhook_endpoint, sender) - .await - } -} - -pub(crate) fn from_strike_amount( - strike_amount: StrikeAmount, - target_unit: &CurrencyUnit, -) -> anyhow::Result { - match target_unit { - CurrencyUnit::Sat => strike_amount.to_sats(), - CurrencyUnit::Msat => Ok(strike_amount.to_sats()? * 1000), - CurrencyUnit::Usd => { - if strike_amount.currency == StrikeCurrencyUnit::USD { - Ok((strike_amount.amount * 100.0).round() as u64) - } else { - bail!("Could not convert strike USD"); - } - } - CurrencyUnit::Eur => { - if strike_amount.currency == StrikeCurrencyUnit::EUR { - Ok((strike_amount.amount * 100.0).round() as u64) - } else { - bail!("Could not convert to EUR"); - } - } - _ => bail!("Unsupported unit"), - } -} - -pub(crate) fn to_strike_unit( - amount: T, - current_unit: &CurrencyUnit, -) -> anyhow::Result -where - T: Into, -{ - let amount = amount.into(); - match current_unit { - CurrencyUnit::Sat => Ok(StrikeAmount::from_sats(amount)), - CurrencyUnit::Msat => Ok(StrikeAmount::from_sats(amount / 1000)), - CurrencyUnit::Usd => { - let dollars = (amount as f64 / 100_f64) * 100.0; - - Ok(StrikeAmount { - currency: StrikeCurrencyUnit::USD, - amount: dollars.round() / 100.0, - }) - } - CurrencyUnit::Eur => { - let euro = (amount as f64 / 100_f64) * 100.0; - - Ok(StrikeAmount { - currency: StrikeCurrencyUnit::EUR, - amount: euro.round() / 100.0, - }) - } - _ => bail!("Unsupported unit"), - } -}