diff --git a/mutiny-core/src/auth.rs b/mutiny-core/src/auth.rs index 205bca1c7..59a4e4dca 100644 --- a/mutiny-core/src/auth.rs +++ b/mutiny-core/src/auth.rs @@ -6,6 +6,7 @@ use crate::{ networking::websocket::{SimpleWebSocket, WebSocketImpl}, storage::MutinyStorage, }; +use anyhow::anyhow; use jwt_compact::UntrustedToken; use lightning::util::logger::*; use lightning::{log_error, log_info}; @@ -76,7 +77,11 @@ impl MutinyAuthClient { self.retrieve_new_jwt().await?; self.authenticated_request(method, url, body).await } - _ => Ok(res), + StatusCode::OK | StatusCode::ACCEPTED | StatusCode::CREATED => Ok(res), + code => Err(MutinyError::Other(anyhow!( + "Received unexpected status code: {}", + code + ))), } } diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 4fcd5aa4c..aa11c1112 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -34,6 +34,7 @@ pub mod redshift; pub mod scb; pub mod storage; mod subscription; +mod vss; #[cfg(any(test, feature = "test-utils"))] pub mod test_utils; diff --git a/mutiny-core/src/vss.rs b/mutiny-core/src/vss.rs new file mode 100644 index 000000000..a3d121cca --- /dev/null +++ b/mutiny-core/src/vss.rs @@ -0,0 +1,197 @@ +#![allow(dead_code)] +use crate::auth::MutinyAuthClient; +use crate::{error::MutinyError, logging::MutinyLogger, storage::MutinyStorage}; +use anyhow::anyhow; +use lightning::log_error; +use lightning::util::logger::*; +use reqwest::{Method, Url}; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::sync::Arc; + +pub(crate) struct MutinyVssClient { + auth_client: Arc>, + url: String, + logger: Arc, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct VssKeyValueItem { + pub key: String, + pub value: Option, + pub version: u32, +} + +impl MutinyVssClient { + pub(crate) fn new( + auth_client: Arc>, + url: String, + logger: Arc, + ) -> Self { + Self { + auth_client, + url, + logger, + } + } + + pub async fn put_objects(&self, items: Vec) -> Result<(), MutinyError> { + let url = Url::parse(&format!("{}/putObjects", self.url)).map_err(|e| { + log_error!(self.logger, "Error parsing put objects url: {e}"); + MutinyError::Other(anyhow!("Error parsing put objects url: {e}")) + })?; + + // todo do we need global version here? + let body = json!({ "transaction_items": items }); + + self.auth_client + .request(Method::PUT, url, Some(body)) + .await?; + + Ok(()) + } + + pub async fn get_objects(&self, key: &str) -> Result { + let url = Url::parse(&format!("{}/getObject", self.url)).map_err(|e| { + log_error!(self.logger, "Error parsing get objects url: {e}"); + MutinyError::Other(anyhow!("Error parsing get objects url: {e}")) + })?; + + let body = json!({ "key": key }); + let result = self + .auth_client + .request(Method::POST, url, Some(body)) + .await? + .json() + .await + .map_err(|e| { + log_error!(self.logger, "Error parsing get objects response: {e}"); + MutinyError::Other(anyhow!("Error parsing get objects response: {e}")) + })?; + + Ok(result) + } + + pub async fn list_key_versions( + &self, + key_prefix: Option, + ) -> Result, MutinyError> { + let url = Url::parse(&format!("{}/listKeyVersions", self.url)).map_err(|e| { + log_error!(self.logger, "Error parsing list key versions url: {e}"); + MutinyError::Other(anyhow!("Error parsing list key versions url: {e}")) + })?; + + let body = json!({ "key_prefix": key_prefix }); + let result = self + .auth_client + .request(Method::POST, url, Some(body)) + .await? + .json() + .await + .map_err(|e| { + log_error!(self.logger, "Error parsing list key versions response: {e}"); + MutinyError::Other(anyhow!("Error parsing list key versions response: {e}")) + })?; + + Ok(result) + } +} + +#[cfg(test)] +#[cfg(not(target_arch = "wasm32"))] +mod tests { + use super::*; + use crate::auth::MutinyAuthClient; + use crate::logging::MutinyLogger; + use crate::storage::MemoryStorage; + use crate::test_utils::*; + use std::sync::Arc; + + async fn create_client() -> MutinyVssClient { + // Set up test auth client + let auth_manager = create_manager(); + let lnurl_client = Arc::new( + lnurl::Builder::default() + .build_async() + .expect("failed to make lnurl client"), + ); + let logger = Arc::new(MutinyLogger::default()); + let url = "https://auth-staging.mutinywallet.com"; + + let auth_client = + MutinyAuthClient::new(auth_manager, lnurl_client, logger.clone(), url.to_string()); + + // Test authenticate method + match auth_client.authenticate().await { + Ok(_) => assert!(auth_client.is_authenticated().is_some()), + Err(e) => panic!("Authentication failed with error: {:?}", e), + }; + + MutinyVssClient::new( + Arc::new(auth_client), + "https://storage-staging.mutinywallet.com".to_string(), + logger, + ) + } + + #[tokio::test] + async fn test_vss() { + let client = create_client().await; + + let key = "hello".to_string(); + let value = "world".to_string(); + let obj = VssKeyValueItem { + key: key.clone(), + value: Some(value.clone()), + version: 0, + }; + + client.put_objects(vec![obj.clone()]).await.unwrap(); + + let result = client.get_objects(&key).await.unwrap(); + assert_eq!(obj, result); + + let result = client.list_key_versions(None).await.unwrap(); + let key_version = VssKeyValueItem { + key, + value: None, + version: 0, + }; + + assert_eq!(vec![key_version], result); + assert_eq!(result.len(), 1); + } + + #[tokio::test] + async fn test_vss_versions() { + let client = create_client().await; + + let key = "hello".to_string(); + let value = "world1".to_string(); + let obj = VssKeyValueItem { + key: key.clone(), + value: Some(value.clone()), + version: 0, + }; + + client.put_objects(vec![obj.clone()]).await.unwrap(); + let result = client.get_objects(&key).await.unwrap(); + assert_eq!(obj.clone(), result); + + let value1 = "new world".to_string(); + let obj1 = VssKeyValueItem { + key: key.clone(), + value: Some(value1.clone()), + version: 1, + }; + + client.put_objects(vec![obj1.clone()]).await.unwrap(); + let result = client.get_objects(&key).await.unwrap(); + assert_eq!(obj1, result); + + // check we get version 1 + client.put_objects(vec![obj]).await.unwrap(); + let result = client.get_objects(&key).await.unwrap(); + assert_eq!(obj1, result); + } +}