Add lianalite module

This commit is contained in:
edouardparis 2024-05-28 15:31:35 +02:00
parent ccc88421b3
commit ca662eea6a
7 changed files with 1754 additions and 1 deletions

View File

@ -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"

View File

@ -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<u16>,
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<Error> 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<U: IntoUrl>(&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<Response, AuthError> {
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<AccessTokenResponse, AuthError> {
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<AccessTokenResponse, AuthError> {
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?)
}
}

View File

@ -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<T, D::Error>
where
D: Deserializer<'de>,
T: FromStr,
<T as 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<bitcoin::Address, D::Error>
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<bitcoin::Amount, D::Error>
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<T, D::Error>
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<String, f32>,
}
#[derive(Deserialize)]
pub struct Feerate {
pub low: Option<i32>,
pub high: Option<i32>,
}
#[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<RecoveryPath>,
pub biggest_remaining_sequence: Option<u32>,
pub smallest_remaining_sequence: Option<u32>,
pub metadata: WalletMetadata,
pub created_at: i64,
pub balance: WalletBalance,
pub status: WalletStatus,
pub tip_height: Option<i32>,
}
#[derive(Deserialize)]
pub struct ListWallets {
pub wallets: Vec<Wallet>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WalletMetadata {
pub ledger_hmacs: Vec<LedgerHmac>,
pub fingerprint_aliases: Vec<FingerprintAlias>,
}
#[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<String, String>,
}
#[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<i32>,
pub confirmed_at: Option<i64>,
pub label: Option<String>,
pub address_label: Option<String>,
pub transaction_label: Option<String>,
pub kind: PaymentKind,
pub is_single: bool,
}
#[derive(Deserialize)]
pub struct ListPayments {
pub payments: Vec<Payment>,
}
#[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<i32>,
pub spend_info: Option<CoinSpendInfo>,
pub is_immature: bool,
pub is_change_address: bool,
}
#[derive(Clone, Deserialize)]
pub struct CoinSpendInfo {
pub txid: Txid,
pub height: Option<i32>,
}
#[derive(Deserialize)]
pub struct ListCoins {
pub coins: Vec<Coin>,
}
#[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<i32>,
pub confirmed_at: Option<i64>,
pub label: Option<String>,
#[serde(deserialize_with = "deser_hex")]
pub raw: bitcoin::Transaction,
pub inputs: Vec<Input>,
pub outputs: Vec<Output>,
/// If the transaction has multiple incoming or ougoing payment.
pub is_batch: bool,
}
#[derive(Deserialize)]
pub struct ListTransactions {
pub transactions: Vec<Transaction>,
}
#[derive(Clone, Deserialize)]
pub struct Output {
pub address: Option<String>,
pub label: Option<String>,
pub address_label: Option<String>,
pub amount: u64,
pub kind: UTXOKind,
pub coin: Option<Coin>,
}
#[derive(Clone, Deserialize)]
pub struct Input {
pub txid: String,
pub vout: usize,
pub amount: Option<u64>,
pub label: Option<String>,
pub kind: UTXOKind,
pub coin: Option<Coin>,
}
#[derive(Clone, Deserialize)]
pub struct Psbt {
pub uuid: String,
pub txid: Txid,
pub fee: Option<u64>,
pub fee_rate: Option<u64>,
pub label: Option<String>,
#[serde(deserialize_with = "deser_fromstr")]
pub raw: bitcoin::Psbt,
pub inputs: Vec<Input>,
pub outputs: Vec<Output>,
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<String>,
pub txid: Txid,
pub fee: u64,
pub fee_rate: u64,
pub label: Option<String>,
#[serde(deserialize_with = "deser_fromstr")]
pub raw: bitcoin::Psbt,
pub inputs: Vec<Input>,
pub outputs: Vec<Output>,
pub warnings: Vec<String>,
}
#[derive(Deserialize)]
pub struct ListPsbts {
pub psbts: Vec<Psbt>,
}
#[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<T: std::fmt::Display, S: Serializer>(
field: T,
s: S,
) -> Result<S::Ok, S::Error> {
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<u64>,
pub address: bitcoin::Address<bitcoin::address::NetworkUnchecked>,
/// 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<Recipient>,
/// 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<bitcoin::address::NetworkUnchecked>,
// 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<Label>,
}
#[derive(Serialize)]
pub struct Label {
pub item: String,
pub value: Option<String>,
}
#[derive(Serialize)]
pub struct GenerateRbfPsbt {
/// ID of the transaction to be replaced.
#[serde(serialize_with = "ser_to_string")]
pub txid: bitcoin::Txid,
/// The target feerate (sat/vb) to use for the replacement transaction
/// in order to bump the fee of the transaction being replaced.
///
/// Must be provided if and only if `is_cancel` is `false`.
pub feerate: Option<u64>,
/// Whether to cancel the transaction.
///
/// If `true`, the feerate of the replacement transaction will be set
/// automatically to the lowest possible feerate that satisfies all
/// RBF policies.
///
/// If `false`, the transaction will be replaced by another at the target
/// `feerate` in order to bump its fee.
pub is_cancel: bool,
/// If save is set to true, API will save in database the generated psbt
/// and, if a new change address is generated for the replacement, store
/// this also. Note that if the transaction being replaced has a change
/// output, then its corresponding change address will be reused in the
/// replacement.
pub save: bool,
}
#[derive(Serialize)]
pub struct UpdateWallet {
pub ledger_hmac: Option<UpdateLedgerHmac>,
pub fingerprint_aliases: Option<Vec<UpdateFingerprintAlias>>,
}
#[derive(Serialize)]
pub struct UpdateLedgerHmac {
pub fingerprint: String,
pub hmac: String,
}
#[derive(Serialize)]
pub struct UpdateFingerprintAlias {
pub fingerprint: String,
pub alias: String,
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
pub mod auth;
pub mod backend;
use liana::miniscript::bitcoin;
use serde::Deserialize;
const LIANALITE_SIGNET_URL: &str = "https://signet.lianalite.com";
const LIANALITE_MAINNET_URL: &str = "https://lianalite.com";
#[derive(Debug, Clone, Deserialize)]
pub struct ServiceConfig {
pub auth_api_url: String,
pub auth_api_public_key: String,
pub backend_api_url: String,
}
pub async fn get_service_config(
network: bitcoin::Network,
) -> Result<ServiceConfig, reqwest::Error> {
reqwest::get(format!(
"{}/api/env",
if network == bitcoin::Network::Bitcoin {
LIANALITE_MAINNET_URL
} else {
LIANALITE_SIGNET_URL
}
))
.await?
.json()
.await
}

86
gui/src/lianalite/mod.rs Normal file
View File

@ -0,0 +1,86 @@
pub mod client;
use std::collections::HashMap;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use liana::miniscript::bitcoin::Network;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Config {
auth: HashMap<String, NetworkAuthConfig>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct NetworkAuthConfig {
email: String,
access_token: String,
expires_at: i64,
refresh_token: String,
}
pub const DEFAULT_FILE_NAME: &str = "lite.json";
impl Config {
pub fn file_path(datadir: PathBuf, network: Network) -> PathBuf {
let mut path = datadir;
path.push(network.to_string());
path.push(DEFAULT_FILE_NAME);
path
}
pub fn from_file(datadir: PathBuf, network: Network) -> Result<Self, ConfigError> {
let path = Self::file_path(datadir, network);
let config = std::fs::read(path)
.map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => ConfigError::NotFound,
_ => ConfigError::ReadingFile(format!("Reading settings file: {}", e)),
})
.and_then(|file_content| {
serde_json::from_slice::<Config>(&file_content)
.map_err(|e| ConfigError::ReadingFile(format!("Parsing settings file: {}", e)))
})?;
Ok(config)
}
pub fn to_file(&self, datadir: PathBuf, network: Network) -> Result<(), ConfigError> {
let path = Self::file_path(datadir, network);
let content = serde_json::to_string_pretty(&self).map_err(|e| {
ConfigError::WritingFile(format!("Failed to serialize settings: {}", e))
})?;
let mut settings_file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
.map_err(|e| ConfigError::WritingFile(e.to_string()))?;
settings_file.write_all(content.as_bytes()).map_err(|e| {
tracing::warn!("failed to write to file: {:?}", e);
ConfigError::WritingFile(e.to_string())
})
}
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum ConfigError {
NotFound,
ReadingFile(String),
WritingFile(String),
Unexpected(String),
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::NotFound => write!(f, "Settings file not found"),
Self::ReadingFile(e) => write!(f, "Error while reading file: {}", e),
Self::WritingFile(e) => write!(f, "Error while writing file: {}", e),
Self::Unexpected(e) => write!(f, "Unexpected error: {}", e),
}
}
}

View File

@ -5,6 +5,7 @@ pub mod download;
pub mod hw;
pub mod installer;
pub mod launcher;
pub mod lianalite;
pub mod loader;
pub mod logger;
pub mod signer;