Add support for pure integration tests (#458)

* Embed mint_url as a field of HttpClient

* Create pure integration tests

* DirectMintConnection: convert between String and Uuid
This commit is contained in:
ok300
2024-12-08 14:33:58 +00:00
committed by GitHub
parent 9cb684e5db
commit 86c4c2dfeb
18 changed files with 426 additions and 170 deletions

1
Cargo.lock generated
View File

@@ -780,6 +780,7 @@ name = "cdk-integration-tests"
version = "0.5.0" version = "0.5.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait",
"axum", "axum",
"bip39", "bip39",
"cdk", "cdk",

View File

@@ -144,16 +144,17 @@ async fn main() -> Result<()> {
let mints = localstore.get_mints().await?; let mints = localstore.get_mints().await?;
for (mint, _) in mints { for (mint_url, _) in mints {
let mut wallet = Wallet::new( let mut wallet = Wallet::new(
&mint.to_string(), &mint_url.to_string(),
cdk::nuts::CurrencyUnit::Sat, cdk::nuts::CurrencyUnit::Sat,
localstore.clone(), localstore.clone(),
&mnemonic.to_seed_normalized(""), &mnemonic.to_seed_normalized(""),
None, None,
)?; )?;
if let Some(proxy_url) = args.proxy.as_ref() { if let Some(proxy_url) = args.proxy.as_ref() {
wallet.set_client(HttpClient::with_proxy(proxy_url.clone(), None, true)?); let http_client = HttpClient::with_proxy(mint_url, proxy_url.clone(), None, true)?;
wallet.set_client(Arc::from(http_client));
} }
wallets.push(wallet); wallets.push(wallet);

View File

@@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use cdk::mint_url::MintUrl; use cdk::mint_url::MintUrl;
use cdk::wallet::client::HttpClientMethods; use cdk::wallet::client::MintConnector;
use cdk::HttpClient; use cdk::HttpClient;
use clap::Args; use clap::Args;
use url::Url; use url::Url;
@@ -11,14 +11,13 @@ pub struct MintInfoSubcommand {
} }
pub async fn mint_info(proxy: Option<Url>, sub_command_args: &MintInfoSubcommand) -> Result<()> { pub async fn mint_info(proxy: Option<Url>, sub_command_args: &MintInfoSubcommand) -> Result<()> {
let mint_url = sub_command_args.mint_url.clone();
let client = match proxy { let client = match proxy {
Some(proxy) => HttpClient::with_proxy(proxy, None, true)?, Some(proxy) => HttpClient::with_proxy(mint_url, proxy, None, true)?,
None => HttpClient::new(), None => HttpClient::new(mint_url),
}; };
let info = client let info = client.get_mint_info().await?;
.get_mint_info(sub_command_args.mint_url.clone())
.await?;
println!("{:#?}", info); println!("{:#?}", info);

View File

@@ -57,6 +57,7 @@ getrandom = { version = "0.2", features = ["js"] }
instant = { version = "0.1", features = ["wasm-bindgen", "inaccurate"] } instant = { version = "0.1", features = ["wasm-bindgen", "inaccurate"] }
[dev-dependencies] [dev-dependencies]
async-trait = "0.1"
rand = "0.8.5" rand = "0.8.5"
bip39 = { version = "2.0", features = ["rand"] } bip39 = { version = "2.0", features = ["rand"] }
anyhow = "1" anyhow = "1"

View File

@@ -1,4 +1,5 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@@ -10,12 +11,13 @@ use cdk::cdk_database::mint_memory::MintMemoryDatabase;
use cdk::cdk_lightning::MintLightning; use cdk::cdk_lightning::MintLightning;
use cdk::dhke::construct_proofs; use cdk::dhke::construct_proofs;
use cdk::mint::FeeReserve; use cdk::mint::FeeReserve;
use cdk::mint_url::MintUrl;
use cdk::nuts::{ use cdk::nuts::{
CurrencyUnit, Id, KeySet, MintBolt11Request, MintInfo, MintQuoteBolt11Request, MintQuoteState, CurrencyUnit, Id, KeySet, MintBolt11Request, MintInfo, MintQuoteBolt11Request, MintQuoteState,
Nuts, PaymentMethod, PreMintSecrets, Proofs, State, Nuts, PaymentMethod, PreMintSecrets, Proofs, State,
}; };
use cdk::types::{LnKey, QuoteTTL}; use cdk::types::{LnKey, QuoteTTL};
use cdk::wallet::client::{HttpClient, HttpClientMethods}; use cdk::wallet::client::{HttpClient, MintConnector};
use cdk::{Mint, Wallet}; use cdk::{Mint, Wallet};
use cdk_fake_wallet::FakeWallet; use cdk_fake_wallet::FakeWallet;
use init_regtest::{get_mint_addr, get_mint_port, get_mint_url}; use init_regtest::{get_mint_addr, get_mint_port, get_mint_url};
@@ -155,7 +157,7 @@ pub async fn mint_proofs(
println!("Minting for ecash"); println!("Minting for ecash");
println!(); println!();
let wallet_client = HttpClient::new(); let wallet_client = HttpClient::new(MintUrl::from_str(mint_url)?);
let request = MintQuoteBolt11Request { let request = MintQuoteBolt11Request {
amount, amount,
@@ -163,15 +165,13 @@ pub async fn mint_proofs(
description, description,
}; };
let mint_quote = wallet_client let mint_quote = wallet_client.post_mint_quote(request).await?;
.post_mint_quote(mint_url.parse()?, request)
.await?;
println!("Please pay: {}", mint_quote.request); println!("Please pay: {}", mint_quote.request);
loop { loop {
let status = wallet_client let status = wallet_client
.get_mint_quote_status(mint_url.parse()?, &mint_quote.quote) .get_mint_quote_status(&mint_quote.quote)
.await?; .await?;
if status.state == MintQuoteState::Paid { if status.state == MintQuoteState::Paid {
@@ -189,7 +189,7 @@ pub async fn mint_proofs(
outputs: premint_secrets.blinded_messages(), outputs: premint_secrets.blinded_messages(),
}; };
let mint_response = wallet_client.post_mint(mint_url.parse()?, request).await?; let mint_response = wallet_client.post_mint(request).await?;
let pre_swap_proofs = construct_proofs( let pre_swap_proofs = construct_proofs(
mint_response.signatures, mint_response.signatures,

View File

@@ -8,7 +8,7 @@ use cdk::cdk_database::WalletMemoryDatabase;
use cdk::nuts::{ use cdk::nuts::{
CurrencyUnit, MeltBolt11Request, MeltQuoteState, MintQuoteState, PreMintSecrets, State, CurrencyUnit, MeltBolt11Request, MeltQuoteState, MintQuoteState, PreMintSecrets, State,
}; };
use cdk::wallet::client::{HttpClient, HttpClientMethods}; use cdk::wallet::client::{HttpClient, MintConnector};
use cdk::wallet::Wallet; use cdk::wallet::Wallet;
use cdk_fake_wallet::{create_fake_invoice, FakeInvoiceDescription}; use cdk_fake_wallet::{create_fake_invoice, FakeInvoiceDescription};
use cdk_integration_tests::attempt_to_swap_pending; use cdk_integration_tests::attempt_to_swap_pending;
@@ -354,7 +354,7 @@ async fn test_fake_melt_change_in_quote() -> Result<()> {
let premint_secrets = PreMintSecrets::random(keyset.id, 100.into(), &SplitTarget::default())?; let premint_secrets = PreMintSecrets::random(keyset.id, 100.into(), &SplitTarget::default())?;
let client = HttpClient::new(); let client = HttpClient::new(MINT_URL.parse()?);
let melt_request = MeltBolt11Request { let melt_request = MeltBolt11Request {
quote: melt_quote.id.clone(), quote: melt_quote.id.clone(),
@@ -362,7 +362,7 @@ async fn test_fake_melt_change_in_quote() -> Result<()> {
outputs: Some(premint_secrets.blinded_messages()), outputs: Some(premint_secrets.blinded_messages()),
}; };
let melt_response = client.post_melt(MINT_URL.parse()?, melt_request).await?; let melt_response = client.post_melt(melt_request).await?;
assert!(melt_response.change.is_some()); assert!(melt_response.change.is_some());

View File

@@ -0,0 +1,266 @@
#[cfg(test)]
mod integration_tests_pure {
use std::assert_eq;
use std::collections::HashMap;
use std::fmt::{Debug, Formatter};
use std::str::FromStr;
use std::sync::Arc;
use async_trait::async_trait;
use cdk::amount::SplitTarget;
use cdk::cdk_database::mint_memory::MintMemoryDatabase;
use cdk::cdk_database::WalletMemoryDatabase;
use cdk::nuts::{
CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysetResponse,
MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request,
MintBolt11Response, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response,
MintQuoteState, Nuts, RestoreRequest, RestoreResponse, SwapRequest, SwapResponse,
};
use cdk::types::QuoteTTL;
use cdk::util::unix_time;
use cdk::wallet::client::MintConnector;
use cdk::{Amount, Error, Mint, Wallet};
use cdk_integration_tests::create_backends_fake_wallet;
use rand::random;
use tokio::sync::Notify;
use uuid::Uuid;
struct DirectMintConnection {
mint: Arc<Mint>,
}
impl Debug for DirectMintConnection {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"DirectMintConnection {{ mint_info: {:?} }}",
self.mint.mint_info
)
}
}
/// Implements the generic [MintConnector] (i.e. use the interface that expects to communicate
/// to a generic mint, where we don't know that quote ID's are [Uuid]s) for [DirectMintConnection],
/// where we know we're dealing with a mint that uses [Uuid]s for quotes.
/// Convert the requests and responses between the [String] and [Uuid] variants as necessary.
#[async_trait]
impl MintConnector for DirectMintConnection {
async fn get_mint_keys(&self) -> Result<Vec<KeySet>, Error> {
self.mint.pubkeys().await.map(|pks| pks.keysets)
}
async fn get_mint_keyset(&self, keyset_id: Id) -> Result<KeySet, Error> {
self.mint
.keyset(&keyset_id)
.await
.and_then(|res| res.ok_or(Error::UnknownKeySet))
}
async fn get_mint_keysets(&self) -> Result<KeysetResponse, Error> {
self.mint.keysets().await
}
async fn post_mint_quote(
&self,
request: MintQuoteBolt11Request,
) -> Result<MintQuoteBolt11Response<String>, Error> {
self.mint
.get_mint_bolt11_quote(request)
.await
.map(Into::into)
}
async fn get_mint_quote_status(
&self,
quote_id: &str,
) -> Result<MintQuoteBolt11Response<String>, Error> {
let quote_id_uuid = Uuid::from_str(quote_id).unwrap();
self.mint
.check_mint_quote(&quote_id_uuid)
.await
.map(Into::into)
}
async fn post_mint(
&self,
request: MintBolt11Request<String>,
) -> Result<MintBolt11Response, Error> {
let request_uuid = request.try_into().unwrap();
self.mint.process_mint_request(request_uuid).await
}
async fn post_melt_quote(
&self,
request: MeltQuoteBolt11Request,
) -> Result<MeltQuoteBolt11Response<String>, Error> {
self.mint
.get_melt_bolt11_quote(&request)
.await
.map(Into::into)
}
async fn get_melt_quote_status(
&self,
quote_id: &str,
) -> Result<MeltQuoteBolt11Response<String>, Error> {
let quote_id_uuid = Uuid::from_str(quote_id).unwrap();
self.mint
.check_melt_quote(&quote_id_uuid)
.await
.map(Into::into)
}
async fn post_melt(
&self,
request: MeltBolt11Request<String>,
) -> Result<MeltQuoteBolt11Response<String>, Error> {
let request_uuid = request.try_into().unwrap();
self.mint.melt_bolt11(&request_uuid).await.map(Into::into)
}
async fn post_swap(&self, swap_request: SwapRequest) -> Result<SwapResponse, Error> {
self.mint.process_swap_request(swap_request).await
}
async fn get_mint_info(&self) -> Result<MintInfo, Error> {
Ok(self.mint.mint_info().clone().time(unix_time()))
}
async fn post_check_state(
&self,
request: CheckStateRequest,
) -> Result<CheckStateResponse, Error> {
self.mint.check_state(&request).await
}
async fn post_restore(&self, request: RestoreRequest) -> Result<RestoreResponse, Error> {
self.mint.restore(request).await
}
}
fn get_mint_connector(mint: Arc<Mint>) -> DirectMintConnection {
DirectMintConnection { mint }
}
async fn create_and_start_test_mint() -> anyhow::Result<Arc<Mint>> {
let fee: u64 = 0;
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 quote_ttl = QuoteTTL::new(10000, 10000);
let mint_url = "http://aaa";
let seed = random::<[u8; 32]>();
let mint: Mint = Mint::new(
mint_url,
&seed,
mint_info,
quote_ttl,
Arc::new(MintMemoryDatabase::default()),
create_backends_fake_wallet(),
supported_units,
HashMap::new(),
)
.await?;
let mint_arc = Arc::new(mint);
let mint_arc_clone = Arc::clone(&mint_arc);
let shutdown = Arc::new(Notify::new());
tokio::spawn({
let shutdown = Arc::clone(&shutdown);
async move { mint_arc_clone.wait_for_paid_invoices(shutdown).await }
});
Ok(mint_arc)
}
fn create_test_wallet_for_mint(mint: Arc<Mint>) -> anyhow::Result<Arc<Wallet>> {
let connector = get_mint_connector(mint);
let seed = random::<[u8; 32]>();
let mint_url = connector.mint.mint_url.to_string();
let unit = CurrencyUnit::Sat;
let localstore = WalletMemoryDatabase::default();
let mut wallet = Wallet::new(&mint_url, unit, Arc::new(localstore), &seed, None)?;
wallet.set_client(Arc::from(connector));
Ok(Arc::new(wallet))
}
/// Creates a mint quote for the given amount and checks its state in a loop. Returns when
/// amount is minted.
async fn receive(wallet: Arc<Wallet>, amount: u64) -> anyhow::Result<Amount> {
let desired_amount = Amount::from(amount);
let quote = wallet.mint_quote(desired_amount, None).await?;
loop {
let status = wallet.mint_quote_state(&quote.id).await?;
if status.state == MintQuoteState::Paid {
break;
}
}
wallet
.mint(&quote.id, SplitTarget::default(), None)
.await
.map_err(Into::into)
}
mod nut03 {
use cdk::nuts::nut00::ProofsMethods;
use cdk::wallet::SendKind;
use crate::integration_tests_pure::*;
#[tokio::test]
async fn test_swap_to_send() -> anyhow::Result<()> {
let mint_bob = create_and_start_test_mint().await?;
let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())?;
// Alice gets 64 sats
receive(wallet_alice.clone(), 64).await?;
let balance_alice = wallet_alice.total_balance().await?;
assert_eq!(Amount::from(64), balance_alice);
// Alice wants to send 40 sats, which internally swaps
let token = wallet_alice
.send(
Amount::from(40),
None,
None,
&SplitTarget::None,
&SendKind::OnlineExact,
false,
)
.await?;
assert_eq!(Amount::from(40), token.proofs().total_amount()?);
assert_eq!(Amount::from(24), wallet_alice.total_balance().await?);
// Alice sends cashu, Carol receives
let wallet_carol = create_test_wallet_for_mint(mint_bob.clone())?;
let received_amount = wallet_carol
.receive_proofs(token.proofs(), SplitTarget::None, &[], &[])
.await?;
assert_eq!(Amount::from(40), received_amount);
assert_eq!(Amount::from(40), wallet_carol.total_balance().await?);
Ok(())
}
}
}

View File

@@ -11,7 +11,7 @@ use cdk::nuts::{
CurrencyUnit, MeltQuoteState, MintBolt11Request, MintQuoteState, NotificationPayload, CurrencyUnit, MeltQuoteState, MintBolt11Request, MintQuoteState, NotificationPayload,
PreMintSecrets, State, PreMintSecrets, State,
}; };
use cdk::wallet::client::{HttpClient, HttpClientMethods}; use cdk::wallet::client::{HttpClient, MintConnector};
use cdk::wallet::Wallet; use cdk::wallet::Wallet;
use cdk_integration_tests::init_regtest::{ use cdk_integration_tests::init_regtest::{
get_mint_url, get_mint_ws_url, init_cln_client, init_lnd_client, get_mint_url, get_mint_ws_url, init_cln_client, init_lnd_client,
@@ -374,7 +374,7 @@ async fn test_cached_mint() -> Result<()> {
} }
let active_keyset_id = wallet.get_active_mint_keyset().await?.id; let active_keyset_id = wallet.get_active_mint_keyset().await?.id;
let http_client = HttpClient::new(); let http_client = HttpClient::new(get_mint_url().as_str().parse()?);
let premint_secrets = let premint_secrets =
PreMintSecrets::random(active_keyset_id, 31.into(), &SplitTarget::default()).unwrap(); PreMintSecrets::random(active_keyset_id, 31.into(), &SplitTarget::default()).unwrap();
@@ -383,12 +383,8 @@ async fn test_cached_mint() -> Result<()> {
outputs: premint_secrets.blinded_messages(), outputs: premint_secrets.blinded_messages(),
}; };
let response = http_client let response = http_client.post_mint(request.clone()).await?;
.post_mint(get_mint_url().as_str().parse()?, request.clone()) let response1 = http_client.post_mint(request).await?;
.await?;
let response1 = http_client
.post_mint(get_mint_url().as_str().parse()?, request)
.await?;
assert!(response == response1); assert!(response == response1);
Ok(()) Ok(())

View File

@@ -96,6 +96,18 @@ pub struct MintQuoteBolt11Response<Q> {
pub expiry: Option<u64>, pub expiry: Option<u64>,
} }
#[cfg(feature = "mint")]
impl From<MintQuoteBolt11Response<Uuid>> for MintQuoteBolt11Response<String> {
fn from(value: MintQuoteBolt11Response<Uuid>) -> Self {
Self {
quote: value.quote.to_string(),
request: value.request,
state: value.state,
expiry: value.expiry,
}
}
}
#[cfg(feature = "mint")] #[cfg(feature = "mint")]
impl From<crate::mint::MintQuote> for MintQuoteBolt11Response<Uuid> { impl From<crate::mint::MintQuote> for MintQuoteBolt11Response<Uuid> {
fn from(mint_quote: crate::mint::MintQuote) -> MintQuoteBolt11Response<Uuid> { fn from(mint_quote: crate::mint::MintQuote) -> MintQuoteBolt11Response<Uuid> {
@@ -121,6 +133,18 @@ pub struct MintBolt11Request<Q> {
pub outputs: Vec<BlindedMessage>, pub outputs: Vec<BlindedMessage>,
} }
#[cfg(feature = "mint")]
impl TryFrom<MintBolt11Request<String>> for MintBolt11Request<Uuid> {
type Error = uuid::Error;
fn try_from(value: MintBolt11Request<String>) -> Result<Self, Self::Error> {
Ok(Self {
quote: Uuid::from_str(&value.quote)?,
outputs: value.outputs,
})
}
}
impl<Q> MintBolt11Request<Q> { impl<Q> MintBolt11Request<Q> {
/// Total [`Amount`] of outputs /// Total [`Amount`] of outputs
pub fn total_amount(&self) -> Result<Amount, Error> { pub fn total_amount(&self) -> Result<Amount, Error> {

View File

@@ -115,6 +115,22 @@ pub struct MeltQuoteBolt11Response<Q> {
pub change: Option<Vec<BlindSignature>>, pub change: Option<Vec<BlindSignature>>,
} }
#[cfg(feature = "mint")]
impl From<MeltQuoteBolt11Response<Uuid>> for MeltQuoteBolt11Response<String> {
fn from(value: MeltQuoteBolt11Response<Uuid>) -> Self {
Self {
quote: value.quote.to_string(),
amount: value.amount,
fee_reserve: value.fee_reserve,
paid: value.paid,
state: value.state,
expiry: value.expiry,
payment_preimage: value.payment_preimage,
change: value.change,
}
}
}
#[cfg(feature = "mint")] #[cfg(feature = "mint")]
impl From<&MeltQuote> for MeltQuoteBolt11Response<Uuid> { impl From<&MeltQuote> for MeltQuoteBolt11Response<Uuid> {
fn from(melt_quote: &MeltQuote) -> MeltQuoteBolt11Response<Uuid> { fn from(melt_quote: &MeltQuote) -> MeltQuoteBolt11Response<Uuid> {
@@ -247,6 +263,19 @@ pub struct MeltBolt11Request<Q> {
pub outputs: Option<Vec<BlindedMessage>>, pub outputs: Option<Vec<BlindedMessage>>,
} }
#[cfg(feature = "mint")]
impl TryFrom<MeltBolt11Request<String>> for MeltBolt11Request<Uuid> {
type Error = uuid::Error;
fn try_from(value: MeltBolt11Request<String>) -> Result<Self, Self::Error> {
Ok(Self {
quote: Uuid::from_str(&value.quote)?,
inputs: value.inputs,
outputs: value.outputs,
})
}
}
impl<Q: Serialize + DeserializeOwned> MeltBolt11Request<Q> { impl<Q: Serialize + DeserializeOwned> MeltBolt11Request<Q> {
/// Total [`Amount`] of [`Proofs`] /// Total [`Amount`] of [`Proofs`]
pub fn proofs_amount(&self) -> Result<Amount, Error> { pub fn proofs_amount(&self) -> Result<Amount, Error> {

View File

@@ -33,19 +33,15 @@ macro_rules! convert_http_response {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct HttpClient { pub struct HttpClient {
inner: Client, inner: Client,
} mint_url: MintUrl,
impl Default for HttpClient {
fn default() -> Self {
Self::new()
}
} }
impl HttpClient { impl HttpClient {
/// Create new [`HttpClient`] /// Create new [`HttpClient`]
pub fn new() -> Self { pub fn new(mint_url: MintUrl) -> Self {
Self { Self {
inner: Client::new(), inner: Client::new(),
mint_url,
} }
} }
@@ -54,6 +50,7 @@ impl HttpClient {
/// Specifying `None` for `host_matcher` will use the proxy for all /// Specifying `None` for `host_matcher` will use the proxy for all
/// requests. /// requests.
pub fn with_proxy( pub fn with_proxy(
mint_url: MintUrl,
proxy: Url, proxy: Url,
host_matcher: Option<&str>, host_matcher: Option<&str>,
accept_invalid_certs: bool, accept_invalid_certs: bool,
@@ -76,26 +73,31 @@ impl HttpClient {
.danger_accept_invalid_certs(accept_invalid_certs) // Allow self-signed certs .danger_accept_invalid_certs(accept_invalid_certs) // Allow self-signed certs
.build()?; .build()?;
Ok(Self { inner: client }) Ok(Self {
inner: client,
mint_url,
})
} }
} }
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl HttpClientMethods for HttpClient { impl MintConnector for HttpClient {
/// Get Active Mint Keys [NUT-01] /// Get Active Mint Keys [NUT-01]
#[instrument(skip(self), fields(mint_url = %mint_url))] #[instrument(skip(self), fields(mint_url = %self.mint_url))]
async fn get_mint_keys(&self, mint_url: MintUrl) -> Result<Vec<KeySet>, Error> { async fn get_mint_keys(&self) -> Result<Vec<KeySet>, Error> {
let url = mint_url.join_paths(&["v1", "keys"])?; let url = self.mint_url.join_paths(&["v1", "keys"])?;
let keys = self.inner.get(url).send().await?.text().await?; let keys = self.inner.get(url).send().await?.text().await?;
Ok(convert_http_response!(KeysResponse, keys)?.keysets) Ok(convert_http_response!(KeysResponse, keys)?.keysets)
} }
/// Get Keyset Keys [NUT-01] /// Get Keyset Keys [NUT-01]
#[instrument(skip(self), fields(mint_url = %mint_url))] #[instrument(skip(self), fields(mint_url = %self.mint_url))]
async fn get_mint_keyset(&self, mint_url: MintUrl, keyset_id: Id) -> Result<KeySet, Error> { async fn get_mint_keyset(&self, keyset_id: Id) -> Result<KeySet, Error> {
let url = mint_url.join_paths(&["v1", "keys", &keyset_id.to_string()])?; let url = self
.mint_url
.join_paths(&["v1", "keys", &keyset_id.to_string()])?;
let keys = self.inner.get(url).send().await?.text().await?; let keys = self.inner.get(url).send().await?.text().await?;
convert_http_response!(KeysResponse, keys)? convert_http_response!(KeysResponse, keys)?
@@ -106,22 +108,23 @@ impl HttpClientMethods for HttpClient {
} }
/// Get Keysets [NUT-02] /// Get Keysets [NUT-02]
#[instrument(skip(self), fields(mint_url = %mint_url))] #[instrument(skip(self), fields(mint_url = %self.mint_url))]
async fn get_mint_keysets(&self, mint_url: MintUrl) -> Result<KeysetResponse, Error> { async fn get_mint_keysets(&self) -> Result<KeysetResponse, Error> {
let url = mint_url.join_paths(&["v1", "keysets"])?; let url = self.mint_url.join_paths(&["v1", "keysets"])?;
let res = self.inner.get(url).send().await?.text().await?; let res = self.inner.get(url).send().await?.text().await?;
convert_http_response!(KeysetResponse, res) convert_http_response!(KeysetResponse, res)
} }
/// Mint Quote [NUT-04] /// Mint Quote [NUT-04]
#[instrument(skip(self), fields(mint_url = %mint_url))] #[instrument(skip(self), fields(mint_url = %self.mint_url))]
async fn post_mint_quote( async fn post_mint_quote(
&self, &self,
mint_url: MintUrl,
request: MintQuoteBolt11Request, request: MintQuoteBolt11Request,
) -> Result<MintQuoteBolt11Response<String>, Error> { ) -> Result<MintQuoteBolt11Response<String>, Error> {
let url = mint_url.join_paths(&["v1", "mint", "quote", "bolt11"])?; let url = self
.mint_url
.join_paths(&["v1", "mint", "quote", "bolt11"])?;
let res = self let res = self
.inner .inner
@@ -136,13 +139,14 @@ impl HttpClientMethods for HttpClient {
} }
/// Mint Quote status /// Mint Quote status
#[instrument(skip(self), fields(mint_url = %mint_url))] #[instrument(skip(self), fields(mint_url = %self.mint_url))]
async fn get_mint_quote_status( async fn get_mint_quote_status(
&self, &self,
mint_url: MintUrl,
quote_id: &str, quote_id: &str,
) -> Result<MintQuoteBolt11Response<String>, Error> { ) -> Result<MintQuoteBolt11Response<String>, Error> {
let url = mint_url.join_paths(&["v1", "mint", "quote", "bolt11", quote_id])?; let url = self
.mint_url
.join_paths(&["v1", "mint", "quote", "bolt11", quote_id])?;
let res = self.inner.get(url).send().await?.text().await?; let res = self.inner.get(url).send().await?.text().await?;
@@ -150,13 +154,12 @@ impl HttpClientMethods for HttpClient {
} }
/// Mint Tokens [NUT-04] /// Mint Tokens [NUT-04]
#[instrument(skip(self, request), fields(mint_url = %mint_url))] #[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
async fn post_mint( async fn post_mint(
&self, &self,
mint_url: MintUrl,
request: MintBolt11Request<String>, request: MintBolt11Request<String>,
) -> Result<MintBolt11Response, Error> { ) -> Result<MintBolt11Response, Error> {
let url = mint_url.join_paths(&["v1", "mint", "bolt11"])?; let url = self.mint_url.join_paths(&["v1", "mint", "bolt11"])?;
let res = self let res = self
.inner .inner
@@ -171,13 +174,14 @@ impl HttpClientMethods for HttpClient {
} }
/// Melt Quote [NUT-05] /// Melt Quote [NUT-05]
#[instrument(skip(self, request), fields(mint_url = %mint_url))] #[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
async fn post_melt_quote( async fn post_melt_quote(
&self, &self,
mint_url: MintUrl,
request: MeltQuoteBolt11Request, request: MeltQuoteBolt11Request,
) -> Result<MeltQuoteBolt11Response<String>, Error> { ) -> Result<MeltQuoteBolt11Response<String>, Error> {
let url = mint_url.join_paths(&["v1", "melt", "quote", "bolt11"])?; let url = self
.mint_url
.join_paths(&["v1", "melt", "quote", "bolt11"])?;
let res = self let res = self
.inner .inner
@@ -192,13 +196,14 @@ impl HttpClientMethods for HttpClient {
} }
/// Melt Quote Status /// Melt Quote Status
#[instrument(skip(self), fields(mint_url = %mint_url))] #[instrument(skip(self), fields(mint_url = %self.mint_url))]
async fn get_melt_quote_status( async fn get_melt_quote_status(
&self, &self,
mint_url: MintUrl,
quote_id: &str, quote_id: &str,
) -> Result<MeltQuoteBolt11Response<String>, Error> { ) -> Result<MeltQuoteBolt11Response<String>, Error> {
let url = mint_url.join_paths(&["v1", "melt", "quote", "bolt11", quote_id])?; let url = self
.mint_url
.join_paths(&["v1", "melt", "quote", "bolt11", quote_id])?;
let res = self.inner.get(url).send().await?.text().await?; let res = self.inner.get(url).send().await?.text().await?;
@@ -207,13 +212,12 @@ impl HttpClientMethods for HttpClient {
/// Melt [NUT-05] /// Melt [NUT-05]
/// [Nut-08] Lightning fee return if outputs defined /// [Nut-08] Lightning fee return if outputs defined
#[instrument(skip(self, request), fields(mint_url = %mint_url))] #[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
async fn post_melt( async fn post_melt(
&self, &self,
mint_url: MintUrl,
request: MeltBolt11Request<String>, request: MeltBolt11Request<String>,
) -> Result<MeltQuoteBolt11Response<String>, Error> { ) -> Result<MeltQuoteBolt11Response<String>, Error> {
let url = mint_url.join_paths(&["v1", "melt", "bolt11"])?; let url = self.mint_url.join_paths(&["v1", "melt", "bolt11"])?;
let res = self let res = self
.inner .inner
@@ -228,13 +232,9 @@ impl HttpClientMethods for HttpClient {
} }
/// Swap Token [NUT-03] /// Swap Token [NUT-03]
#[instrument(skip(self, swap_request), fields(mint_url = %mint_url))] #[instrument(skip(self, swap_request), fields(mint_url = %self.mint_url))]
async fn post_swap( async fn post_swap(&self, swap_request: SwapRequest) -> Result<SwapResponse, Error> {
&self, let url = self.mint_url.join_paths(&["v1", "swap"])?;
mint_url: MintUrl,
swap_request: SwapRequest,
) -> Result<SwapResponse, Error> {
let url = mint_url.join_paths(&["v1", "swap"])?;
let res = self let res = self
.inner .inner
@@ -249,9 +249,9 @@ impl HttpClientMethods for HttpClient {
} }
/// Get Mint Info [NUT-06] /// Get Mint Info [NUT-06]
#[instrument(skip(self), fields(mint_url = %mint_url))] #[instrument(skip(self), fields(mint_url = %self.mint_url))]
async fn get_mint_info(&self, mint_url: MintUrl) -> Result<MintInfo, Error> { async fn get_mint_info(&self) -> Result<MintInfo, Error> {
let url = mint_url.join_paths(&["v1", "info"])?; let url = self.mint_url.join_paths(&["v1", "info"])?;
let res = self.inner.get(url).send().await?.text().await?; let res = self.inner.get(url).send().await?.text().await?;
@@ -259,13 +259,12 @@ impl HttpClientMethods for HttpClient {
} }
/// Spendable check [NUT-07] /// Spendable check [NUT-07]
#[instrument(skip(self, request), fields(mint_url = %mint_url))] #[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
async fn post_check_state( async fn post_check_state(
&self, &self,
mint_url: MintUrl,
request: CheckStateRequest, request: CheckStateRequest,
) -> Result<CheckStateResponse, Error> { ) -> Result<CheckStateResponse, Error> {
let url = mint_url.join_paths(&["v1", "checkstate"])?; let url = self.mint_url.join_paths(&["v1", "checkstate"])?;
let res = self let res = self
.inner .inner
@@ -280,13 +279,9 @@ impl HttpClientMethods for HttpClient {
} }
/// Restore request [NUT-13] /// Restore request [NUT-13]
#[instrument(skip(self, request), fields(mint_url = %mint_url))] #[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
async fn post_restore( async fn post_restore(&self, request: RestoreRequest) -> Result<RestoreResponse, Error> {
&self, let url = self.mint_url.join_paths(&["v1", "restore"])?;
mint_url: MintUrl,
request: RestoreRequest,
) -> Result<RestoreResponse, Error> {
let url = mint_url.join_paths(&["v1", "restore"])?;
let res = self let res = self
.inner .inner
@@ -301,83 +296,56 @@ impl HttpClientMethods for HttpClient {
} }
} }
/// Http Client Methods /// Interface that connects a wallet to a mint. Typically represents an [HttpClient].
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait HttpClientMethods: Debug { pub trait MintConnector: Debug {
/// Get Active Mint Keys [NUT-01] /// Get Active Mint Keys [NUT-01]
async fn get_mint_keys(&self, mint_url: MintUrl) -> Result<Vec<KeySet>, Error>; async fn get_mint_keys(&self) -> Result<Vec<KeySet>, Error>;
/// Get Keyset Keys [NUT-01] /// Get Keyset Keys [NUT-01]
async fn get_mint_keyset(&self, mint_url: MintUrl, keyset_id: Id) -> Result<KeySet, Error>; async fn get_mint_keyset(&self, keyset_id: Id) -> Result<KeySet, Error>;
/// Get Keysets [NUT-02] /// Get Keysets [NUT-02]
async fn get_mint_keysets(&self, mint_url: MintUrl) -> Result<KeysetResponse, Error>; async fn get_mint_keysets(&self) -> Result<KeysetResponse, Error>;
/// Mint Quote [NUT-04] /// Mint Quote [NUT-04]
async fn post_mint_quote( async fn post_mint_quote(
&self, &self,
mint_url: MintUrl,
request: MintQuoteBolt11Request, request: MintQuoteBolt11Request,
) -> Result<MintQuoteBolt11Response<String>, Error>; ) -> Result<MintQuoteBolt11Response<String>, Error>;
/// Mint Quote status /// Mint Quote status
async fn get_mint_quote_status( async fn get_mint_quote_status(
&self, &self,
mint_url: MintUrl,
quote_id: &str, quote_id: &str,
) -> Result<MintQuoteBolt11Response<String>, Error>; ) -> Result<MintQuoteBolt11Response<String>, Error>;
/// Mint Tokens [NUT-04] /// Mint Tokens [NUT-04]
async fn post_mint( async fn post_mint(
&self, &self,
mint_url: MintUrl,
request: MintBolt11Request<String>, request: MintBolt11Request<String>,
) -> Result<MintBolt11Response, Error>; ) -> Result<MintBolt11Response, Error>;
/// Melt Quote [NUT-05] /// Melt Quote [NUT-05]
async fn post_melt_quote( async fn post_melt_quote(
&self, &self,
mint_url: MintUrl,
request: MeltQuoteBolt11Request, request: MeltQuoteBolt11Request,
) -> Result<MeltQuoteBolt11Response<String>, Error>; ) -> Result<MeltQuoteBolt11Response<String>, Error>;
/// Melt Quote Status /// Melt Quote Status
async fn get_melt_quote_status( async fn get_melt_quote_status(
&self, &self,
mint_url: MintUrl,
quote_id: &str, quote_id: &str,
) -> Result<MeltQuoteBolt11Response<String>, Error>; ) -> Result<MeltQuoteBolt11Response<String>, Error>;
/// Melt [NUT-05] /// Melt [NUT-05]
/// [Nut-08] Lightning fee return if outputs defined /// [Nut-08] Lightning fee return if outputs defined
async fn post_melt( async fn post_melt(
&self, &self,
mint_url: MintUrl,
request: MeltBolt11Request<String>, request: MeltBolt11Request<String>,
) -> Result<MeltQuoteBolt11Response<String>, Error>; ) -> Result<MeltQuoteBolt11Response<String>, Error>;
/// Split Token [NUT-06] /// Split Token [NUT-06]
async fn post_swap( async fn post_swap(&self, request: SwapRequest) -> Result<SwapResponse, Error>;
&self,
mint_url: MintUrl,
request: SwapRequest,
) -> Result<SwapResponse, Error>;
/// Get Mint Info [NUT-06] /// Get Mint Info [NUT-06]
async fn get_mint_info(&self, mint_url: MintUrl) -> Result<MintInfo, Error>; async fn get_mint_info(&self) -> Result<MintInfo, Error>;
/// Spendable check [NUT-07] /// Spendable check [NUT-07]
async fn post_check_state( async fn post_check_state(
&self, &self,
mint_url: MintUrl,
request: CheckStateRequest, request: CheckStateRequest,
) -> Result<CheckStateResponse, Error>; ) -> Result<CheckStateResponse, Error>;
/// Restore request [NUT-13] /// Restore request [NUT-13]
async fn post_restore( async fn post_restore(&self, request: RestoreRequest) -> Result<RestoreResponse, Error>;
&self,
mint_url: MintUrl,
request: RestoreRequest,
) -> Result<RestoreResponse, Error>;
} }

View File

@@ -13,10 +13,7 @@ impl Wallet {
let keys = if let Some(keys) = self.localstore.get_keys(&keyset_id).await? { let keys = if let Some(keys) = self.localstore.get_keys(&keyset_id).await? {
keys keys
} else { } else {
let keys = self let keys = self.client.get_mint_keyset(keyset_id).await?;
.client
.get_mint_keyset(self.mint_url.clone(), keyset_id)
.await?;
keys.verify_id()?; keys.verify_id()?;
@@ -33,7 +30,7 @@ impl Wallet {
/// Queries mint for all keysets /// Queries mint for all keysets
#[instrument(skip(self))] #[instrument(skip(self))]
pub async fn get_mint_keysets(&self) -> Result<Vec<KeySetInfo>, Error> { pub async fn get_mint_keysets(&self) -> Result<Vec<KeySetInfo>, Error> {
let keysets = self.client.get_mint_keysets(self.mint_url.clone()).await?; let keysets = self.client.get_mint_keysets().await?;
self.localstore self.localstore
.add_mint_keysets(self.mint_url.clone(), keysets.keysets.clone()) .add_mint_keysets(self.mint_url.clone(), keysets.keysets.clone())
@@ -48,7 +45,7 @@ impl Wallet {
/// keysets /// keysets
#[instrument(skip(self))] #[instrument(skip(self))]
pub async fn get_active_mint_keysets(&self) -> Result<Vec<KeySetInfo>, Error> { pub async fn get_active_mint_keysets(&self) -> Result<Vec<KeySetInfo>, Error> {
let keysets = self.client.get_mint_keysets(self.mint_url.clone()).await?; let keysets = self.client.get_mint_keysets().await?;
let keysets = keysets.keysets; let keysets = keysets.keysets;
self.localstore self.localstore

View File

@@ -65,10 +65,7 @@ impl Wallet {
options, options,
}; };
let quote_res = self let quote_res = self.client.post_melt_quote(quote_request).await?;
.client
.post_melt_quote(self.mint_url.clone(), quote_request)
.await?;
if quote_res.amount != amount { if quote_res.amount != amount {
return Err(Error::IncorrectQuoteAmount); return Err(Error::IncorrectQuoteAmount);
@@ -96,10 +93,7 @@ impl Wallet {
&self, &self,
quote_id: &str, quote_id: &str,
) -> Result<MeltQuoteBolt11Response<String>, Error> { ) -> Result<MeltQuoteBolt11Response<String>, Error> {
let response = self let response = self.client.get_melt_quote_status(quote_id).await?;
.client
.get_melt_quote_status(self.mint_url.clone(), quote_id)
.await?;
match self.localstore.get_melt_quote(quote_id).await? { match self.localstore.get_melt_quote(quote_id).await? {
Some(quote) => { Some(quote) => {
@@ -160,7 +154,7 @@ impl Wallet {
outputs: Some(premint_secrets.blinded_messages()), outputs: Some(premint_secrets.blinded_messages()),
}; };
let melt_response = self.client.post_melt(self.mint_url.clone(), request).await; let melt_response = self.client.post_melt(request).await;
let melt_response = match melt_response { let melt_response = match melt_response {
Ok(melt_response) => melt_response, Ok(melt_response) => melt_response,

View File

@@ -71,10 +71,7 @@ impl Wallet {
description, description,
}; };
let quote_res = self let quote_res = self.client.post_mint_quote(request).await?;
.client
.post_mint_quote(mint_url.clone(), request)
.await?;
let quote = MintQuote { let quote = MintQuote {
mint_url, mint_url,
@@ -97,10 +94,7 @@ impl Wallet {
&self, &self,
quote_id: &str, quote_id: &str,
) -> Result<MintQuoteBolt11Response<String>, Error> { ) -> Result<MintQuoteBolt11Response<String>, Error> {
let response = self let response = self.client.get_mint_quote_status(quote_id).await?;
.client
.get_mint_quote_status(self.mint_url.clone(), quote_id)
.await?;
match self.localstore.get_mint_quote(quote_id).await? { match self.localstore.get_mint_quote(quote_id).await? {
Some(quote) => { Some(quote) => {
@@ -227,10 +221,7 @@ impl Wallet {
outputs: premint_secrets.blinded_messages(), outputs: premint_secrets.blinded_messages(),
}; };
let mint_res = self let mint_res = self.client.post_mint(request).await?;
.client
.post_mint(self.mint_url.clone(), request)
.await?;
let keys = self.get_keyset_keys(active_keyset_id).await?; let keys = self.get_keyset_keys(active_keyset_id).await?;

View File

@@ -6,7 +6,7 @@ use std::sync::Arc;
use bitcoin::bip32::Xpriv; use bitcoin::bip32::Xpriv;
use bitcoin::Network; use bitcoin::Network;
use client::HttpClientMethods; use client::MintConnector;
use tracing::instrument; use tracing::instrument;
use crate::amount::SplitTarget; use crate::amount::SplitTarget;
@@ -57,7 +57,7 @@ pub struct Wallet {
/// The targeted amount of proofs to have at each size /// The targeted amount of proofs to have at each size
pub target_proof_count: usize, pub target_proof_count: usize,
xpriv: Xpriv, xpriv: Xpriv,
client: Arc<dyn HttpClientMethods + Send + Sync>, client: Arc<dyn MintConnector + Send + Sync>,
} }
impl Wallet { impl Wallet {
@@ -86,11 +86,12 @@ impl Wallet {
target_proof_count: Option<usize>, target_proof_count: Option<usize>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let xpriv = Xpriv::new_master(Network::Bitcoin, seed).expect("Could not create master key"); let xpriv = Xpriv::new_master(Network::Bitcoin, seed).expect("Could not create master key");
let mint_url = MintUrl::from_str(mint_url)?;
Ok(Self { Ok(Self {
mint_url: MintUrl::from_str(mint_url)?, mint_url: mint_url.clone(),
unit, unit,
client: Arc::new(HttpClient::new()), client: Arc::new(HttpClient::new(mint_url)),
localstore, localstore,
xpriv, xpriv,
target_proof_count: target_proof_count.unwrap_or(3), target_proof_count: target_proof_count.unwrap_or(3),
@@ -98,8 +99,8 @@ impl Wallet {
} }
/// Change HTTP client /// Change HTTP client
pub fn set_client<C: HttpClientMethods + 'static + Send + Sync>(&mut self, client: C) { pub fn set_client(&mut self, client: Arc<dyn MintConnector + Send + Sync>) {
self.client = Arc::new(client); self.client = client;
} }
/// Fee required for proof set /// Fee required for proof set
@@ -160,10 +161,10 @@ impl Wallet {
Ok(()) Ok(())
} }
/// Qeury mint for current mint information /// Query mint for current mint information
#[instrument(skip(self))] #[instrument(skip(self))]
pub async fn get_mint_info(&self) -> Result<Option<MintInfo>, Error> { pub async fn get_mint_info(&self) -> Result<Option<MintInfo>, Error> {
let mint_info = match self.client.get_mint_info(self.mint_url.clone()).await { let mint_info = match self.client.get_mint_info().await {
Ok(mint_info) => Some(mint_info), Ok(mint_info) => Some(mint_info),
Err(err) => { Err(err) => {
tracing::warn!("Could not get mint info {}", err); tracing::warn!("Could not get mint info {}", err);
@@ -277,10 +278,7 @@ impl Wallet {
outputs: premint_secrets.blinded_messages(), outputs: premint_secrets.blinded_messages(),
}; };
let response = self let response = self.client.post_restore(restore_request).await?;
.client
.post_restore(self.mint_url.clone(), restore_request)
.await?;
if response.signatures.is_empty() { if response.signatures.is_empty() {
empty_batch += 1; empty_batch += 1;

View File

@@ -65,7 +65,7 @@ impl Wallet {
let spendable = self let spendable = self
.client .client
.post_check_state(self.mint_url.clone(), CheckStateRequest { ys: proof_ys }) .post_check_state(CheckStateRequest { ys: proof_ys })
.await? .await?
.states; .states;
@@ -86,10 +86,7 @@ impl Wallet {
pub async fn check_proofs_spent(&self, proofs: Proofs) -> Result<Vec<ProofState>, Error> { pub async fn check_proofs_spent(&self, proofs: Proofs) -> Result<Vec<ProofState>, Error> {
let spendable = self let spendable = self
.client .client
.post_check_state( .post_check_state(CheckStateRequest { ys: proofs.ys()? })
self.mint_url.clone(),
CheckStateRequest { ys: proofs.ys()? },
)
.await?; .await?;
let spent_ys: Vec<_> = spendable let spent_ys: Vec<_> = spendable
.states .states

View File

@@ -128,10 +128,7 @@ impl Wallet {
} }
} }
let swap_response = self let swap_response = self.client.post_swap(pre_swap.swap_request).await?;
.client
.post_swap(mint_url.clone(), pre_swap.swap_request)
.await?;
// Proof to keep // Proof to keep
let recv_proofs = construct_proofs( let recv_proofs = construct_proofs(

View File

@@ -33,10 +33,7 @@ impl Wallet {
) )
.await?; .await?;
let swap_response = self let swap_response = self.client.post_swap(pre_swap.swap_request).await?;
.client
.post_swap(mint_url.clone(), pre_swap.swap_request)
.await?;
let active_keyset_id = pre_swap.pre_mint_secrets.keyset_id; let active_keyset_id = pre_swap.pre_mint_secrets.keyset_id;