From ca662eea6a40919e1f51275ef1915c892f13e7ce Mon Sep 17 00:00:00 2001 From: edouardparis Date: Tue, 28 May 2024 15:31:35 +0200 Subject: [PATCH 1/3] Add lianalite module --- gui/Cargo.toml | 2 +- gui/src/lianalite/client/auth.rs | 176 ++++ gui/src/lianalite/client/backend/api.rs | 416 +++++++++ gui/src/lianalite/client/backend/mod.rs | 1042 +++++++++++++++++++++++ gui/src/lianalite/client/mod.rs | 32 + gui/src/lianalite/mod.rs | 86 ++ gui/src/lib.rs | 1 + 7 files changed, 1754 insertions(+), 1 deletion(-) create mode 100644 gui/src/lianalite/client/auth.rs create mode 100644 gui/src/lianalite/client/backend/api.rs create mode 100644 gui/src/lianalite/client/backend/mod.rs create mode 100644 gui/src/lianalite/client/mod.rs create mode 100644 gui/src/lianalite/mod.rs diff --git a/gui/Cargo.toml b/gui/Cargo.toml index 92201240..bce27ac0 100644 --- a/gui/Cargo.toml +++ b/gui/Cargo.toml @@ -44,7 +44,7 @@ chrono = "0.4.38" # Used for managing internal bitcoind base64 = "0.21" bitcoin_hashes = "0.12" -reqwest = { version = "0.11", default-features=false, features = ["rustls-tls"] } +reqwest = { version = "0.11", default-features=false, features = ["json", "rustls-tls"] } rust-ini = "0.19.0" diff --git a/gui/src/lianalite/client/auth.rs b/gui/src/lianalite/client/auth.rs new file mode 100644 index 00000000..0d0412a9 --- /dev/null +++ b/gui/src/lianalite/client/auth.rs @@ -0,0 +1,176 @@ +use reqwest::{Error, IntoUrl, Method, RequestBuilder, Response}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct SignInOtp<'a> { + email: &'a str, + create_user: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct VerifyOtp<'a, 'b> { + email: &'a str, + token: &'b str, + #[serde(rename = "type")] + kind: &'static str, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ResendOtp<'a> { + email: &'a str, + #[serde(rename = "type")] + kind: &'static str, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RefreshToken<'a> { + refresh_token: &'a str, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AccessTokenResponse { + pub access_token: String, + pub expires_at: i64, + pub refresh_token: String, +} + +#[derive(Debug, Clone)] +pub struct AuthClient { + http: reqwest::Client, + url: String, + api_public_key: String, +} + +#[derive(Debug, Clone)] +pub struct AuthError { + pub http_status: Option, + pub error: String, +} + +impl std::fmt::Display for AuthError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + if let Some(status) = self.http_status { + write!(f, "{}: {}", status, self.error) + } else { + write!(f, "{}", self.error) + } + } +} + +impl From for AuthError { + fn from(value: Error) -> Self { + AuthError { + http_status: None, + error: value.to_string(), + } + } +} + +impl AuthClient { + pub fn new(url: String, api_public_key: String) -> Self { + AuthClient { + http: reqwest::Client::new(), + url, + api_public_key, + } + } + + fn request(&self, method: Method, url: U) -> RequestBuilder { + let req = self + .http + .request(method, url) + .header("apikey", &self.api_public_key) + .header("Content-Type", "application/json"); + tracing::debug!("Sending http request: {:?}", req); + req + } + + pub async fn sign_in_otp(&self, email: &str) -> Result<(), AuthError> { + let response: Response = self + .request(Method::POST, &format!("{}/auth/v1/otp", self.url)) + .json(&SignInOtp { + email, + create_user: true, + }) + .send() + .await?; + + if !response.status().is_success() { + return Err(AuthError { + http_status: Some(response.status().into()), + error: response.text().await?, + }); + } + + Ok(()) + } + + pub async fn resend_otp(&self, email: &str) -> Result { + let response: Response = self + .request(Method::POST, &format!("{}/auth/v1/resend", self.url)) + .json(&ResendOtp { + email, + kind: "email", + }) + .send() + .await?; + if !response.status().is_success() { + return Err(AuthError { + http_status: Some(response.status().into()), + error: response.text().await?, + }); + } + Ok(response) + } + + pub async fn verify_otp( + &self, + email: &str, + token: &str, + ) -> Result { + let response: Response = self + .http + .post(&format!("{}/auth/v1/verify", self.url)) + .header("apikey", &self.api_public_key) + .header("Content-Type", "application/json") + .json(&VerifyOtp { + email, + token, + kind: "email", + }) + .send() + .await?; + if !response.status().is_success() { + return Err(AuthError { + http_status: Some(response.status().into()), + error: response.text().await?, + }); + } + + Ok(response.json().await?) + } + + pub async fn refresh_token( + &self, + refresh_token: &str, + ) -> Result { + let response: Response = self + .http + .post(&format!( + "{}/auth/v1/token?grant_type=refresh_token", + self.url + )) + .header("apikey", &self.api_public_key) + .header("Content-Type", "application/json") + .json(&RefreshToken { refresh_token }) + .send() + .await?; + if !response.status().is_success() { + return Err(AuthError { + http_status: Some(response.status().into()), + error: response.text().await?, + }); + } + Ok(response.json().await?) + } +} diff --git a/gui/src/lianalite/client/backend/api.rs b/gui/src/lianalite/client/backend/api.rs new file mode 100644 index 00000000..6649418a --- /dev/null +++ b/gui/src/lianalite/client/backend/api.rs @@ -0,0 +1,416 @@ +use std::collections::HashMap; +use std::str::FromStr; + +use liana::{ + descriptors::LianaDescriptor, + miniscript::bitcoin::{self, bip32, consensus, hashes::hex::FromHex, Amount, OutPoint, Txid}, +}; +use serde::{de, Deserialize, Deserializer}; + +pub fn deser_fromstr<'de, D, T>(deserializer: D) -> Result +where + D: Deserializer<'de>, + T: FromStr, + ::Err: std::fmt::Display, +{ + let string = String::deserialize(deserializer)?; + T::from_str(&string).map_err(de::Error::custom) +} + +/// Deserialize an address from string, assuming the network was checked. +pub fn deser_addr_assume_checked<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let string = String::deserialize(deserializer)?; + bitcoin::Address::from_str(&string) + .map(|addr| addr.assume_checked()) + .map_err(de::Error::custom) +} + +/// Deserialize an amount from sats +pub fn deser_amount_from_sats<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let a = u64::deserialize(deserializer)?; + Ok(bitcoin::Amount::from_sat(a)) +} + +pub fn deser_hex<'de, D, T>(d: D) -> Result +where + D: Deserializer<'de>, + T: consensus::Decodable, +{ + let s = String::deserialize(d)?; + let s = Vec::from_hex(&s).map_err(de::Error::custom)?; + consensus::deserialize(&s).map_err(de::Error::custom) +} + +/// The maximum number of item to return. +pub const DEFAULT_LIMIT: usize = 20; +/// The maximum number of outpoints that can be provided as a filter. +pub const DEFAULT_OUTPOINTS_LIMIT: usize = 50; +/// The maximum number of items that can be provided as a filter. +pub const DEFAULT_LABEL_ITEMS_LIMIT: usize = 50; + +#[derive(Deserialize)] +pub struct Claims { + pub sub: String, +} + +#[derive(Deserialize)] +pub struct NetworkInfo { + pub feerate: Feerate, + pub rates: HashMap, +} + +#[derive(Deserialize)] +pub struct Feerate { + pub low: Option, + pub high: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct WalletBalance { + /// Total of funds that present in a block. + pub confirmed: u64, + /// Total of funds that is not yet in a block. + pub unconfirmed: u64, + /// Total of funds that are mined but not yet available + pub immature: u64, + /// Total of funds that are unconfirmed but are coming from + /// the wallet + pub unconfirmed_change: u64, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum WalletStatus { + Normal, + Recovering, + Recovered, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct RecoveryPath { + pub sequence: u16, + pub available_balance: u64, + pub total_coins: usize, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Wallet { + pub id: String, + pub name: String, + #[serde(deserialize_with = "deser_fromstr")] + pub descriptor: LianaDescriptor, + pub recovery_paths: Vec, + pub biggest_remaining_sequence: Option, + pub smallest_remaining_sequence: Option, + pub metadata: WalletMetadata, + pub created_at: i64, + pub balance: WalletBalance, + pub status: WalletStatus, + pub tip_height: Option, +} + +#[derive(Deserialize)] +pub struct ListWallets { + pub wallets: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct WalletMetadata { + pub ledger_hmacs: Vec, + pub fingerprint_aliases: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct LedgerHmac { + #[serde(deserialize_with = "deser_fromstr")] + pub fingerprint: bip32::Fingerprint, + pub user_id: String, + pub hmac: String, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub struct FingerprintAlias { + #[serde(deserialize_with = "deser_fromstr")] + pub fingerprint: bip32::Fingerprint, + pub user_id: String, + pub alias: String, +} + +#[derive(Deserialize)] +pub struct WalletLabels { + pub labels: HashMap, +} + +#[derive(serde::Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum PaymentKind { + Outgoing, + Incoming, +} + +#[derive(Deserialize)] +pub struct Payment { + pub txuuid: String, + pub txid: String, + pub vout: u32, + pub amount: u64, + pub block_height: Option, + pub confirmed_at: Option, + pub label: Option, + pub address_label: Option, + pub transaction_label: Option, + pub kind: PaymentKind, + pub is_single: bool, +} + +#[derive(Deserialize)] +pub struct ListPayments { + pub payments: Vec, +} + +#[derive(Clone, Deserialize)] +pub struct Coin { + #[serde(deserialize_with = "deser_addr_assume_checked")] + pub address: bitcoin::Address, + #[serde(deserialize_with = "deser_amount_from_sats")] + pub amount: Amount, + pub derivation_index: bip32::ChildNumber, + pub outpoint: OutPoint, + pub block_height: Option, + pub spend_info: Option, + pub is_immature: bool, + pub is_change_address: bool, +} + +#[derive(Clone, Deserialize)] +pub struct CoinSpendInfo { + pub txid: Txid, + pub height: Option, +} + +#[derive(Deserialize)] +pub struct ListCoins { + pub coins: Vec, +} + +#[derive(Clone, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum UTXOKind { + Deposit, + Change, + External, +} + +#[derive(Clone, Deserialize)] +pub struct Transaction { + pub uuid: String, + pub txid: String, + pub fee: u64, + pub fee_rate: u64, + pub block_height: Option, + pub confirmed_at: Option, + pub label: Option, + #[serde(deserialize_with = "deser_hex")] + pub raw: bitcoin::Transaction, + pub inputs: Vec, + pub outputs: Vec, + /// If the transaction has multiple incoming or ougoing payment. + pub is_batch: bool, +} + +#[derive(Deserialize)] +pub struct ListTransactions { + pub transactions: Vec, +} + +#[derive(Clone, Deserialize)] +pub struct Output { + pub address: Option, + pub label: Option, + pub address_label: Option, + pub amount: u64, + pub kind: UTXOKind, + pub coin: Option, +} + +#[derive(Clone, Deserialize)] +pub struct Input { + pub txid: String, + pub vout: usize, + pub amount: Option, + pub label: Option, + pub kind: UTXOKind, + pub coin: Option, +} + +#[derive(Clone, Deserialize)] +pub struct Psbt { + pub uuid: String, + pub txid: Txid, + pub fee: Option, + pub fee_rate: Option, + pub label: Option, + #[serde(deserialize_with = "deser_fromstr")] + pub raw: bitcoin::Psbt, + pub inputs: Vec, + pub outputs: Vec, + pub is_batch: bool, + pub updated_at: i64, +} + +#[derive(Clone, Deserialize)] +#[serde(untagged)] +pub enum DraftPsbtResult { + Success(DraftPsbt), + InsufficientFunds(InsufficientFundsInfo), +} + +#[derive(Clone, Deserialize)] +pub struct InsufficientFundsInfo { + pub missing: u64, +} + +#[derive(Clone, Deserialize)] +pub struct DraftPsbt { + pub uuid: Option, + pub txid: Txid, + pub fee: u64, + pub fee_rate: u64, + pub label: Option, + #[serde(deserialize_with = "deser_fromstr")] + pub raw: bitcoin::Psbt, + pub inputs: Vec, + pub outputs: Vec, + pub warnings: Vec, +} + +#[derive(Deserialize)] +pub struct ListPsbts { + pub psbts: Vec, +} + +#[derive(Deserialize)] +pub struct Address { + #[serde(deserialize_with = "deser_addr_assume_checked")] + pub address: bitcoin::Address, + pub derivation_index: bip32::ChildNumber, +} + +pub mod payload { + use liana::miniscript::bitcoin; + use serde::{Serialize, Serializer}; + + pub fn ser_to_string( + field: T, + s: S, + ) -> Result { + s.serialize_str(&field.to_string()) + } + + #[derive(Serialize)] + pub struct ImportPsbt { + pub psbt: String, + } + + #[derive(Serialize)] + pub struct Recipient { + /// Recipient cannot have an empty amount and is_max set to false + /// Amount cannot be less that the DUST limit. + pub amount: Option, + pub address: bitcoin::Address, + /// If is_max is set to true, API will calculate the remaining funds and + /// use it for psbt output amount. + /// Only one recipient can have is_max set to true + pub is_max: bool, + } + + #[derive(Serialize)] + pub struct GeneratePsbt<'a> { + pub recipients: Vec, + /// The outpoints of coins to use as transaction inputs. If empty, + /// coins will be selected automatically from the set of confirmed coins + /// and those unconfirmed coins at a change address, excluding immature + /// coins. + pub inputs: &'a [bitcoin::OutPoint], + // The feerate to use for this transaction. + pub feerate: u64, + /// If save is set to true, API will save in database the generated psbt + /// and store the generated change address. + pub save: bool, + } + + #[derive(Serialize)] + pub struct GenerateRecoveryPsbt { + /// The address to sweep funds to. + pub address: bitcoin::Address, + // The feerate to use for this transaction. + pub feerate: u64, + /// Timelock of the recovery path to use. + pub timelock: u16, + /// If save is set to true, API will save in database the generated psbt + /// and store the generated change address. + pub save: bool, + } + + #[derive(Serialize)] + pub struct Labels { + pub labels: Vec