Files
breez-sdk-liquid/cli/src/commands.rs
ok300 bcb4743260 Integrate sdk-common (#308)
* Rename Network to LiquidSdkNetwork to avoid naming conflicts with sdk-common

* Integrate sdk-common: re-use parse_invoice

* Fix UDL bindings tests

* Add input parser support

* CLI: Add input parser support

* Integrate lnurl-pay

* Cargo format, re-generate FRB and RN bindings

* Duplicate LnUrlPayError, update FRB/UniFFI bindings

* Move duplicate LnUrlPayError to FRB bindings.rs

* Add support for lnurl-withdraw

* Lnurl-withdraw: convert units, incorporate fees

* Bump sdk-common, rename InputType::LnUrlError

* Bump sdk-common, remove Wrapped prefix from LnUrlPaySuccessData, LnUrlPayResult

* LNURL-withdraw: add missing UDL mappings

* Add support for LNURL-auth

* Move LNURL structs into models.rs

* Rename LiquidSdkNetwork to LiquidNetwork

* Fix binding tests

* Fix tests

* Move FRB mirroring structs to bindings.rs

* Remove TODO

* Bump sdk-common, remove sdk-common dependency on rusqlite

* Propagate LiquidNetwork rename into flutter example app

* CI: install protobuf before running clippy

* CI: add rust cache, protobuf for remaining rust steps
2024-06-20 16:18:50 +00:00

308 lines
9.6 KiB
Rust

use std::borrow::Cow::{self, Owned};
use std::io::Write;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use anyhow::Result;
use breez_liquid_sdk::model::*;
use breez_liquid_sdk::sdk::LiquidSdk;
use breez_liquid_sdk::*;
use clap::{arg, Parser};
use qrcode_rs::render::unicode;
use qrcode_rs::{EcLevel, QrCode};
use rustyline::highlight::Highlighter;
use rustyline::history::DefaultHistory;
use rustyline::Editor;
use rustyline::{hint::HistoryHinter, Completer, Helper, Hinter, Validator};
use serde::Serialize;
use serde_json::to_string_pretty;
#[derive(Parser, Debug, Clone, PartialEq)]
pub(crate) enum Command {
/// Send lbtc and receive btc lightning through a swap
SendPayment {
/// Invoice which has to be paid
bolt11: String,
/// Delay for the send, in seconds
#[arg(short, long)]
delay: Option<u64>,
},
/// Send lbtc and receive btc onchain through a swap
SendOnchainPayment {
/// Btc onchain address to send to
address: String,
/// Amount that will be received, in satoshi
amount_sat: u64,
},
/// Receive lbtc and send btc through a swap
ReceivePayment {
/// Amount the payer will send, in satoshi
payer_amount_sat: u64,
},
/// List incoming and outgoing payments
ListPayments,
/// Get the balance and general info of the current instance
GetInfo,
/// Sync local data with mempool and onchain data
Sync,
/// Empties the encrypted transaction cache
EmptyCache,
/// Backs up the current pending swaps
Backup {
#[arg(short, long)]
backup_path: Option<String>,
},
/// Retrieve a list of backups
Restore {
#[arg(short, long)]
backup_path: Option<String>,
},
/// Shuts down all background threads of this SDK instance
Disconnect,
/// Parse a generic string to get its type and relevant metadata
Parse {
/// Generic input (URL, LNURL, BIP-21 BTC Address, LN invoice, etc)
input: String,
},
/// Pay using LNURL
LnurlPay {
/// LN Address or LNURL-pay endpoint
lnurl: String,
},
LnurlWithdraw {
/// LNURL-withdraw endpoint
lnurl: String,
},
LnurlAuth {
/// LNURL-auth endpoint
lnurl: String,
},
}
#[derive(Helper, Completer, Hinter, Validator)]
pub(crate) struct CliHelper {
#[rustyline(Hinter)]
pub(crate) hinter: HistoryHinter,
}
impl Highlighter for CliHelper {
fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
Owned("\x1b[1m".to_owned() + hint + "\x1b[m")
}
}
#[derive(Serialize)]
pub(crate) struct CommandResult<T: Serialize> {
pub success: bool,
pub message: T,
}
macro_rules! command_result {
($expr:expr) => {{
to_string_pretty(&CommandResult {
success: true,
message: $expr,
})?
}};
}
macro_rules! wait_confirmation {
($prompt:expr,$result:expr) => {
print!("{}", $prompt);
std::io::stdout().flush()?;
let mut buf = String::new();
std::io::stdin().read_line(&mut buf)?;
if !['y', 'Y'].contains(&(buf.as_bytes()[0] as char)) {
return Ok(command_result!($result));
}
};
}
pub(crate) async fn handle_command(
rl: &mut Editor<CliHelper, DefaultHistory>,
sdk: &Arc<LiquidSdk>,
command: Command,
) -> Result<String> {
Ok(match command {
Command::ReceivePayment { payer_amount_sat } => {
let prepare_response = sdk
.prepare_receive_payment(&PrepareReceiveRequest { payer_amount_sat })
.await?;
wait_confirmation!(
format!(
"Fees: {} sat. Are the fees acceptable? (y/N) ",
prepare_response.fees_sat
),
"Payment receive halted"
);
let response = sdk.receive_payment(&prepare_response).await?;
let invoice = response.invoice.clone();
let mut result = command_result!(response);
result.push('\n');
result.push_str(&build_qr_text(&invoice));
result
}
Command::SendPayment { bolt11, delay } => {
let prepare_response = sdk
.prepare_send_payment(&PrepareSendRequest { invoice: bolt11 })
.await?;
wait_confirmation!(
format!(
"Fees: {} sat. Are the fees acceptable? (y/N) ",
prepare_response.fees_sat
),
"Payment send halted"
);
if let Some(delay) = delay {
let sdk_cloned = sdk.clone();
let prepare_cloned = prepare_response.clone();
tokio::spawn(async move {
thread::sleep(Duration::from_secs(delay));
sdk_cloned.send_payment(&prepare_cloned).await.unwrap();
});
command_result!(prepare_response)
} else {
let response = sdk.send_payment(&prepare_response).await?;
command_result!(response)
}
}
Command::SendOnchainPayment {
address,
amount_sat,
} => {
let prepare_res = sdk
.prepare_pay_onchain(&PreparePayOnchainRequest { amount_sat })
.await?;
wait_confirmation!(
format!(
"Fees: {} sat. Are the fees acceptable? (y/N) ",
prepare_res.fees_sat
),
"Payment send halted"
);
let response = sdk
.pay_onchain(&PayOnchainRequest {
address,
prepare_res,
})
.await?;
command_result!(response)
}
Command::GetInfo => {
command_result!(sdk.get_info().await?)
}
Command::ListPayments => {
let payments = sdk.list_payments().await?;
command_result!(payments)
}
Command::Sync => {
sdk.sync().await?;
command_result!("Synced successfully")
}
Command::EmptyCache => {
sdk.empty_wallet_cache()?;
command_result!("Cache emptied successfully")
}
Command::Backup { backup_path } => {
sdk.backup(BackupRequest { backup_path })?;
command_result!("Backup created successfully!")
}
Command::Restore { backup_path } => {
sdk.restore(RestoreRequest { backup_path })?;
command_result!("Backup restored successfully!")
}
Command::Disconnect => {
sdk.disconnect().await?;
command_result!("Liquid SDK instance disconnected")
}
Command::Parse { input } => {
let res = LiquidSdk::parse(&input).await?;
command_result!(res)
}
Command::LnurlPay { lnurl } => {
let input = LiquidSdk::parse(&lnurl).await?;
let res = match input {
InputType::LnUrlPay { data: pd } => {
let prompt = format!(
"Amount to pay in millisatoshi (min {} msat, max {} msat): ",
pd.min_sendable, pd.max_sendable
);
let amount_msat = rl.readline(&prompt)?;
let pay_res = sdk
.lnurl_pay(LnUrlPayRequest {
data: pd,
amount_msat: amount_msat.parse::<u64>()?,
comment: None,
payment_label: None,
})
.await?;
Ok(pay_res)
}
_ => Err(anyhow::anyhow!("Invalid input")),
}?;
command_result!(res)
}
Command::LnurlWithdraw { lnurl } => {
let input = LiquidSdk::parse(&lnurl).await?;
let res = match input {
InputType::LnUrlWithdraw { data: pd } => {
let prompt = format!(
"Amount to withdraw in millisatoshi (min {} msat, max {} msat): ",
pd.min_withdrawable, pd.max_withdrawable
);
let amount_msat = rl.readline(&prompt)?;
let withdraw_res = sdk
.lnurl_withdraw(LnUrlWithdrawRequest {
data: pd,
amount_msat: amount_msat.parse()?,
description: Some("LNURL-withdraw".to_string()),
})
.await?;
Ok(withdraw_res)
}
_ => Err(anyhow::anyhow!("Invalid input")),
}?;
command_result!(res)
}
Command::LnurlAuth { lnurl } => {
let lnurl_endpoint = lnurl.trim();
let res = match parse(lnurl_endpoint).await? {
InputType::LnUrlAuth { data: ad } => {
let auth_res = sdk.lnurl_auth(ad).await?;
serde_json::to_string_pretty(&auth_res).map_err(|e| e.into())
}
_ => Err(anyhow::anyhow!("Unexpected result type")),
}?;
command_result!(res)
}
})
}
fn build_qr_text(text: &str) -> String {
QrCode::with_error_correction_level(text, EcLevel::L)
.unwrap()
.render::<unicode::Dense1x2>()
.dark_color(unicode::Dense1x2::Light)
.light_color(unicode::Dense1x2::Dark)
.build()
}