Skip to content

Commit

Permalink
Implement Backup client
Browse files Browse the repository at this point in the history
  • Loading branch information
danielgranhao committed Nov 8, 2023
1 parent 2a0885d commit dbe803e
Show file tree
Hide file tree
Showing 9 changed files with 312 additions and 2 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ members = [
"graphql",
"honey-badger",
"mole",
"squirrel",
]
2 changes: 1 addition & 1 deletion graphql/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ edition = "2021"

[dependencies]
chrono = { version = "0.4.24", default-features = false, features = ["std"] }
graphql_client = { version = "0.13.0", features = ["reqwest-blocking"]}
graphql_client = { version = "0.13.0", features = ["reqwest-blocking", "reqwest"]}
log = "0.4.17"
reqwest = { version = "0.11", default-features = false, features = ["json", "blocking", "rustls-tls"]}
serde = { version = "1.0", features = ["derive"]}
Expand Down
14 changes: 14 additions & 0 deletions graphql/schemas/operations.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,17 @@ query MigrationBalance($nodePubKey: String) {
mutation MigrateFunds($invoice: String, $base16InvoiceSignature: String, $ldkNodePubKey: String) {
migrate_funds(invoice: $invoice, base16InvoiceSignature: $base16InvoiceSignature, ldkNodePubKey: $ldkNodePubKey)
}

mutation CreateBackup($encryptedBackup: String!, $schemaName: String!, $schemaVersion: String!) {
create_backup(encryptedBackup: $encryptedBackup, schemaName: $schemaName, schemaVersion: $schemaVersion) {
updatedAt
}
}

mutation RecoverBackup($schemaName: String!) {
recover_backup(schemaName: $schemaName) {
encryptedBackup
schemaVersion
updatedAt
}
}
17 changes: 17 additions & 0 deletions graphql/schemas/schema_wallet_read.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,8 @@ type mutation_root {
): accepted_terms_conditions
accept_wallet_acl_by_pk(pk_columns: AcceptWalletPkRequestInput!): WalletAcl

create_backup(encryptedBackup: String!, schemaName: String!, schemaVersion: String!): CreateBackupResponse

"""
delete data from the table: "channel_manager"
"""
Expand Down Expand Up @@ -895,6 +897,7 @@ type mutation_root {
on_conflict: channel_monitor_on_conflict
): channel_monitor
migrate_funds(base16InvoiceSignature: String, invoice: String, ldkNodePubKey: String): Boolean!
recover_backup(schemaName: String!): RecoverBackupResponse
refresh_session(refreshToken: String!): TokenContainer
refresh_session_v2(refreshToken: String!): SessionPermit
register_email(email: String): WalletEmail
Expand Down Expand Up @@ -1787,3 +1790,17 @@ input wallet_acl_stream_cursor_value_input {
ownerWalletPubKeyId: uuid
role: String
}

type CreateBackupResponse {
walletId: String
schemaName: String
schemaVersion: String
updatedAt: timestamptz
}

type RecoverBackupResponse {
walletId: String
encryptedBackup: String
schemaVersion: String
updatedAt: timestamptz
}
45 changes: 44 additions & 1 deletion graphql/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pub use crate::errors::*;
pub use perro;
pub use reqwest;

use graphql_client::reqwest::post_graphql_blocking;
use graphql_client::reqwest::{post_graphql, post_graphql_blocking};
use graphql_client::Response;
use perro::{permanent_failure, runtime_error, MapToError, OptionToError};
use reqwest::blocking::Client;
Expand Down Expand Up @@ -37,6 +37,25 @@ pub fn build_client(access_token: Option<&str>) -> Result<Client> {
Ok(client)
}

pub fn build_async_client(access_token: Option<&str>) -> Result<reqwest::Client> {
let user_agent = "graphql-rust/0.12.0";
let timeout = Duration::from_secs(20);

let mut builder = reqwest::Client::builder()
.user_agent(user_agent)
.timeout(timeout);
if let Some(access_token) = access_token {
let value = HeaderValue::from_str(&format!("Bearer {access_token}"))
.map_to_permanent_failure("Failed to build header value from str")?;
builder = builder.default_headers(std::iter::once((AUTHORIZATION, value)).collect());
}

let client = builder
.build()
.map_to_permanent_failure("Failed to build a async reqwest client")?;
Ok(client)
}

pub fn post_blocking<Query: graphql_client::GraphQLQuery>(
client: &Client,
backend_url: &String,
Expand All @@ -61,6 +80,30 @@ pub fn post_blocking<Query: graphql_client::GraphQLQuery>(
get_response_data(response, backend_url)
}

pub async fn post<Query: graphql_client::GraphQLQuery>(
client: &reqwest::Client,
backend_url: &String,
variables: Query::Variables,
) -> Result<Query::ResponseData> {
let response = match post_graphql::<Query, _>(client, backend_url, variables).await {
Ok(r) => r,
Err(e) => {
if is_502_status(e.status()) || e.to_string().contains("502") {
// checking for the error containing 502 because reqwest is unexpectedly returning a decode error instead of status error
return Err(runtime_error(
GraphQlRuntimeErrorCode::RemoteServiceUnavailable,
"The remote server returned status 502",
));
}
return Err(runtime_error(
GraphQlRuntimeErrorCode::NetworkError,
"Failed to execute the query",
));
}
};
get_response_data(response, backend_url)
}

pub fn parse_from_rfc3339(rfc3339: &str) -> Result<SystemTime> {
let datetime = chrono::DateTime::parse_from_rfc3339(rfc3339).map_to_runtime_error(
GraphQlRuntimeErrorCode::CorruptData,
Expand Down
16 changes: 16 additions & 0 deletions graphql/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,19 @@ pub struct MigrationBalance;
response_derives = "Debug"
)]
pub struct MigrateFunds;

#[derive(GraphQLQuery)]
#[graphql(
schema_path = "schemas/schema_wallet_read.graphql",
query_path = "schemas/operations.graphql",
response_derives = "Debug"
)]
pub struct CreateBackup;

#[derive(GraphQLQuery)]
#[graphql(
schema_path = "schemas/schema_wallet_read.graphql",
query_path = "schemas/operations.graphql",
response_derives = "Debug"
)]
pub struct RecoverBackup;
19 changes: 19 additions & 0 deletions squirrel/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "squirrel"
version = "0.1.0"
edition = "2021"

[dependencies]
hex = "0.4.3"
log = "0.4.17"

graphql = { path = "../graphql" }
honey-badger = { path = "../honey-badger" }

[dev-dependencies]
bitcoin = { version = "0.29.2" }
ctor = "0.2.0"
rand = "0.8.5"
simplelog = { version ="0.12.0", features = ["test"] }
serial_test = "2.0.0"
tokio = "1.32.0"
69 changes: 69 additions & 0 deletions squirrel/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use graphql::errors::*;
use graphql::perro::{runtime_error, MapToError};
use graphql::schema::*;
use graphql::{build_async_client, post};
use honey_badger::Auth;
use std::sync::Arc;

#[derive(Debug, PartialEq)]
pub struct Backup {
pub encrypted_backup: Vec<u8>,
pub schema_name: String,
pub schema_version: String,
}

pub struct RemoteBackupClient {
backend_url: String,
auth: Arc<Auth>,
}

impl RemoteBackupClient {
pub fn new(backend_url: String, auth: Arc<Auth>) -> Self {
Self { backend_url, auth }
}

pub async fn create_backup(&self, backup: &Backup) -> Result<()> {
let token = self.auth.query_token()?;
let client = build_async_client(Some(&token))?;
let variables = create_backup::Variables {
encrypted_backup: graphql_hex_encode(&backup.encrypted_backup),
schema_name: backup.schema_name.clone(),
schema_version: backup.schema_version.clone(),
};
post::<CreateBackup>(&client, &self.backend_url, variables).await?;

Ok(())
}

pub async fn recover_backup(&self, schema_name: &str) -> Result<Backup> {
let token = self.auth.query_token()?;
let client = build_async_client(Some(&token))?;
let variables = recover_backup::Variables {
schema_name: schema_name.to_string(),
};
let data = post::<RecoverBackup>(&client, &self.backend_url, variables).await?;

match data.recover_backup {
None => Err(runtime_error(
GraphQlRuntimeErrorCode::ObjectNotFound,
"No backup found",
)),
Some(d) => Ok(Backup {
encrypted_backup: graphql_hex_decode(&d.encrypted_backup.unwrap()).unwrap(),
schema_name: schema_name.to_string(),
schema_version: d.schema_version.unwrap(),
}),
}
}
}

fn graphql_hex_encode(data: &Vec<u8>) -> String {
format!("\\x{}", hex::encode(data))
}

fn graphql_hex_decode(data: &str) -> Result<Vec<u8>> {
hex::decode(data.replacen("\\x", "", 1)).map_to_runtime_error(
GraphQlRuntimeErrorCode::CorruptData,
"Could not decode hex encoded binary",
)
}
131 changes: 131 additions & 0 deletions squirrel/tests/integration_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
use bitcoin::Network;
use graphql::perro::Error::RuntimeError;
use graphql::GraphQlRuntimeErrorCode;
use honey_badger::secrets::{derive_keys, generate_keypair, generate_mnemonic};
use honey_badger::{Auth, AuthLevel};
use rand::random;
use simplelog::TestLogger;
use squirrel::{Backup, RemoteBackupClient};
use std::env;
use std::sync::{Arc, Once};

static INIT_LOGGER_ONCE: Once = Once::new();

#[cfg(test)]
#[ctor::ctor]
fn init() {
INIT_LOGGER_ONCE.call_once(|| {
TestLogger::init(simplelog::LevelFilter::Info, simplelog::Config::default()).unwrap();
});
}

#[tokio::test]
async fn test_recovering_backup_when_there_is_none() {
let client = build_backup_client();

let result = client.recover_backup("a").await;

assert!(result.is_err());
assert!(matches!(
result,
Err(RuntimeError {
code: GraphQlRuntimeErrorCode::ObjectNotFound,
..
})
));
}

#[tokio::test]
async fn test_backup_persistence() {
let client = build_backup_client();
let dummy_backup_schema_a_version_1 = Backup {
encrypted_backup: random::<[u8; 32]>().to_vec(),
schema_name: "a".to_string(),
schema_version: "1".to_string(),
};
let dummy_backup_schema_a_version_2 = Backup {
encrypted_backup: random::<[u8; 32]>().to_vec(),
schema_name: "a".to_string(),
schema_version: "2".to_string(),
};
let dummy_backup_schema_b_version_1 = Backup {
encrypted_backup: random::<[u8; 32]>().to_vec(),
schema_name: "b".to_string(),
schema_version: "1".to_string(),
};
let dummy_backup_schema_b_version_2 = Backup {
encrypted_backup: random::<[u8; 32]>().to_vec(),
schema_name: "b".to_string(),
schema_version: "2".to_string(),
};

client
.create_backup(&dummy_backup_schema_a_version_1)
.await
.unwrap();
client
.create_backup(&dummy_backup_schema_b_version_1)
.await
.unwrap();

assert_eq!(
client.recover_backup("a").await.unwrap(),
dummy_backup_schema_a_version_1
);
assert_eq!(
client.recover_backup("b").await.unwrap(),
dummy_backup_schema_b_version_1
);

client
.create_backup(&dummy_backup_schema_a_version_2)
.await
.unwrap();
client
.create_backup(&dummy_backup_schema_b_version_2)
.await
.unwrap();

assert_eq!(
client.recover_backup("a").await.unwrap(),
dummy_backup_schema_a_version_2
);
assert_eq!(
client.recover_backup("b").await.unwrap(),
dummy_backup_schema_b_version_2
);

// non-existing schema name
let result = client.recover_backup("c").await;

assert!(result.is_err());
assert!(matches!(
result,
Err(RuntimeError {
code: GraphQlRuntimeErrorCode::ObjectNotFound,
..
})
));
}

fn build_backup_client() -> RemoteBackupClient {
println!("Generating keys ...");
let mnemonic = generate_mnemonic();
println!("mnemonic: {mnemonic:?}");
let wallet_keys = derive_keys(Network::Testnet, mnemonic).wallet_keypair;
let auth_keys = generate_keypair();

let auth = Auth::new(
get_backend_url(),
AuthLevel::Pseudonymous,
wallet_keys,
auth_keys,
)
.unwrap();

RemoteBackupClient::new(get_backend_url(), Arc::new(auth))
}

fn get_backend_url() -> String {
env::var("GRAPHQL_API_URL").expect("GRAPHQL_API_URL environment variable is not set")
}

0 comments on commit dbe803e

Please sign in to comment.