diff --git a/src/app/operations/lightning.rs b/src/app/operations/lightning.rs new file mode 100644 index 0000000..ed1c991 --- /dev/null +++ b/src/app/operations/lightning.rs @@ -0,0 +1,62 @@ +use anyhow::Context; +use rust_decimal::Decimal; + +use crate::{app::App, client::types::Wallet}; + +impl App { + pub async fn ln_invoice_create( + &self, + wallet: Wallet, + amount: Decimal, + memo: Option, + ) -> anyhow::Result<()> { + let receiving_wallet_id = match wallet { + Wallet::Btc => self.get_user_btc_wallet_id().await?, + Wallet::Usd => self.get_user_usd_wallet_id().await?, + }; + + match wallet { + Wallet::Btc => { + let data = self + .client + .lightning_invoice_create_btc(receiving_wallet_id, amount, memo) + .await + .context("Error occurred while creating BTC lightning invoice.")?; + + println!("{}", serde_json::to_string_pretty(&data.invoice)?); + } + Wallet::Usd => { + let data = self + .client + .lightning_invoice_create_usd(receiving_wallet_id, amount, memo) + .await + .context("Error occurred while creating USD lightning invoice.")?; + + println!("{}", serde_json::to_string_pretty(&data.invoice)?); + } + } + + Ok(()) + } + + pub async fn send_lightning( + &self, + ln_payment_request: String, + wallet: Wallet, + memo: Option, + ) -> anyhow::Result<()> { + let sender_wallet_id = match wallet { + Wallet::Btc => self.get_user_btc_wallet_id().await?, + Wallet::Usd => self.get_user_usd_wallet_id().await?, + }; + + self.client + .ln_payment_send(sender_wallet_id, ln_payment_request, memo) + .await + .context("Error occurred while sending payment to lightning invoice")?; + + println!("Successfully sent payment to the given lightning invoice",); + + Ok(()) + } +} diff --git a/src/app/operations/mod.rs b/src/app/operations/mod.rs index 7303276..af00de0 100644 --- a/src/app/operations/mod.rs +++ b/src/app/operations/mod.rs @@ -2,6 +2,7 @@ pub mod auth; pub mod batch; pub mod globals; pub mod intraledger; +mod lightning; pub mod onchain; pub mod request_phone_code; pub mod user; diff --git a/src/cli/commands.rs b/src/cli/commands.rs index 833be34..456d3cb 100644 --- a/src/cli/commands.rs +++ b/src/cli/commands.rs @@ -62,13 +62,25 @@ pub enum Command { Pay { #[clap(short, long)] username: Option, - #[clap(short, long, conflicts_with("username"))] + #[clap(short, long, conflicts_with_all(["username", "ln_payment_request"]))] onchain_address: Option, + #[clap(short, long, conflicts_with_all(["username", "onchain_address"]))] + ln_payment_request: Option, #[clap(short, long, value_parser)] wallet: Wallet, - #[clap(short, long, required_if_eq("wallet", "usd"))] + #[clap( + short, + long, + required_if_eq("wallet", "usd"), + required_unless_present("ln_payment_request") + )] cents: Option, - #[clap(short, long, required_if_eq("wallet", "btc"))] + #[clap( + short, + long, + required_if_eq("wallet", "btc"), + required_unless_present("ln_payment_request") + )] sats: Option, #[clap(short, long)] memo: Option, @@ -80,6 +92,15 @@ pub enum Command { #[clap(short, long, value_parser)] via: ReceiveVia, }, + /// Create a lightning invoice + LnInvoice { + #[clap(short, long, value_parser)] + wallet: Wallet, + #[clap(short, long, value_parser)] + amount: Decimal, + #[clap(short, long, value_parser)] + memo: Option, + }, /// execute a batch payment Batch { #[clap(short, long = "csv")] diff --git a/src/cli/runner.rs b/src/cli/runner.rs index c31c232..55dfe87 100644 --- a/src/cli/runner.rs +++ b/src/cli/runner.rs @@ -43,20 +43,31 @@ pub async fn run() -> anyhow::Result<()> { cents, sats, memo, - } => match (username, onchain_address) { - (Some(username), None) => { + ln_payment_request, + } => match (username, onchain_address, ln_payment_request) { + (Some(username), None, None) => { app.intraledger_payment(username, wallet, cents, sats, memo) .await?; } - (None, Some(onchain_address)) => { + (None, Some(onchain_address), None) => { app.send_onchain(onchain_address, wallet, cents, sats, memo) .await?; } + (None, None, Some(ln_payment_request)) => { + app.send_lightning(ln_payment_request, wallet, memo).await?; + } _ => {} }, Command::Receive { wallet, via } => { app.receive(wallet, via).await?; } + Command::LnInvoice { + wallet, + amount, + memo, + } => { + app.ln_invoice_create(wallet, amount, memo).await?; + } Command::Batch { file, skip_confirmation, diff --git a/src/client/gql/mutations/ln_invoice_create.gql b/src/client/gql/mutations/ln_invoice_create.gql new file mode 100644 index 0000000..46e9baf --- /dev/null +++ b/src/client/gql/mutations/ln_invoice_create.gql @@ -0,0 +1,14 @@ +mutation LnInvoiceCreate($input: LnInvoiceCreateInput!) { + lnInvoiceCreate(input: $input) { + errors { + message + __typename + } + invoice { + paymentHash + paymentRequest + paymentSecret + __typename + } + } +} \ No newline at end of file diff --git a/src/client/gql/mutations/ln_invoice_payment_send.gql b/src/client/gql/mutations/ln_invoice_payment_send.gql new file mode 100644 index 0000000..94b0f1c --- /dev/null +++ b/src/client/gql/mutations/ln_invoice_payment_send.gql @@ -0,0 +1,9 @@ +mutation LnInvoicePaymentSend($input: LnInvoicePaymentInput!) { + lnInvoicePaymentSend(input: $input) { + errors { + __typename + message + } + status + } +} \ No newline at end of file diff --git a/src/client/gql/mutations/ln_usd_invoice_create.gql b/src/client/gql/mutations/ln_usd_invoice_create.gql new file mode 100644 index 0000000..581e35b --- /dev/null +++ b/src/client/gql/mutations/ln_usd_invoice_create.gql @@ -0,0 +1,14 @@ +mutation LnUsdInvoiceCreate($input: LnUsdInvoiceCreateInput!) { + lnUsdInvoiceCreate(input: $input) { + errors { + message + __typename + } + invoice { + paymentHash + paymentRequest + paymentSecret + __typename + } + } +} \ No newline at end of file diff --git a/src/client/queries.rs b/src/client/queries.rs index df5787b..055aca5 100644 --- a/src/client/queries.rs +++ b/src/client/queries.rs @@ -13,6 +13,9 @@ type SatAmount = Decimal; type CentAmount = Decimal; type Memo = String; type OnChainAddress = String; +type PaymentHash = String; +type LnPaymentSecret = String; +type LnPaymentRequest = String; #[derive(GraphQLQuery)] #[graphql( @@ -158,6 +161,33 @@ pub(super) struct OnChainAddressCurrent; pub use self::on_chain_address_current::OnChainAddressCurrentInput; pub use self::on_chain_address_current::OnChainAddressCurrentOnChainAddressCurrent; +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/client/gql/schema.gql", + query_path = "src/client/gql/mutations/ln_invoice_create.gql", + response_derives = "Debug, Serialize" +)] +pub(super) struct LnInvoiceCreate; +pub use self::ln_invoice_create::LnInvoiceCreateInput; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/client/gql/schema.gql", + query_path = "src/client/gql/mutations/ln_usd_invoice_create.gql", + response_derives = "Debug, Serialize" +)] +pub(super) struct LnUsdInvoiceCreate; +pub use self::ln_usd_invoice_create::LnUsdInvoiceCreateInput; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/client/gql/schema.gql", + query_path = "src/client/gql/mutations/ln_invoice_payment_send.gql", + response_derives = "Debug, Serialize" +)] +pub(super) struct LnInvoicePaymentSend; +pub use self::ln_invoice_payment_send::LnInvoicePaymentInput; + #[derive(GraphQLQuery)] #[graphql( schema_path = "src/client/gql/schema.gql", diff --git a/src/client/requests/lightning.rs b/src/client/requests/lightning.rs new file mode 100644 index 0000000..68db4b4 --- /dev/null +++ b/src/client/requests/lightning.rs @@ -0,0 +1,103 @@ +use graphql_client::reqwest::post_graphql; +use rust_decimal::Decimal; + +use crate::client::{ + errors::{api_error::ApiError, ClientError}, + queries::{ + ln_invoice_create::{self, LnInvoiceCreateLnInvoiceCreate}, + ln_invoice_payment_send, + ln_usd_invoice_create::{self, LnUsdInvoiceCreateLnUsdInvoiceCreate}, + LnInvoiceCreate, LnInvoicePaymentInput, LnInvoicePaymentSend, LnUsdInvoiceCreate, + }, + GaloyClient, +}; + +impl GaloyClient { + pub async fn lightning_invoice_create_btc( + &self, + receiving_wallet_id: String, + amount: Decimal, + memo: Option, + ) -> Result { + let input = ln_invoice_create::LnInvoiceCreateInput { + wallet_id: receiving_wallet_id, + amount, + memo, + }; + let variables = ln_invoice_create::Variables { input }; + + let response_body = + post_graphql::(&self.graphql_client, &self.api, variables) + .await + .map_err(|err| ApiError::IssueGettingResponse(anyhow::Error::new(err)))?; + + let response_data = response_body.data.ok_or(ApiError::IssueParsingResponse)?; + + let result = response_data.ln_invoice_create; + + Ok(result) + } + + pub async fn lightning_invoice_create_usd( + &self, + receiving_wallet_id: String, + amount: Decimal, + memo: Option, + ) -> Result { + let input = ln_usd_invoice_create::LnUsdInvoiceCreateInput { + wallet_id: receiving_wallet_id, + amount, + memo, + }; + let variables = ln_usd_invoice_create::Variables { input }; + + let response_body = + post_graphql::(&self.graphql_client, &self.api, variables) + .await + .map_err(|err| ApiError::IssueGettingResponse(anyhow::Error::new(err)))?; + + let response_data = response_body.data.ok_or(ApiError::IssueParsingResponse)?; + + let result = response_data.ln_usd_invoice_create; + + Ok(result) + } + + pub async fn ln_payment_send( + &self, + sender_wallet_id: String, + payment_request: String, + memo: Option, + ) -> Result<(), ClientError> { + let input = LnInvoicePaymentInput { + wallet_id: sender_wallet_id, + payment_request, + memo, + }; + + let variables = ln_invoice_payment_send::Variables { input }; + + let response_body = + post_graphql::(&self.graphql_client, &self.api, variables) + .await + .map_err(|err| ApiError::IssueGettingResponse(anyhow::Error::new(err)))?; + + let response_data = response_body.data.ok_or(ApiError::IssueParsingResponse)?; + + if !response_data.ln_invoice_payment_send.errors.is_empty() { + let error_string: String = response_data + .ln_invoice_payment_send + .errors + .iter() + .map(|error| format!("{:?}", error)) + .collect::>() + .join(", "); + + return Err(ClientError::ApiError(ApiError::RequestFailedWithError( + error_string, + ))); + } else { + Ok(()) + } + } +} diff --git a/src/client/requests/mod.rs b/src/client/requests/mod.rs index 75a0c2b..686d048 100644 --- a/src/client/requests/mod.rs +++ b/src/client/requests/mod.rs @@ -2,6 +2,7 @@ mod auth; mod default_wallet; mod globals; mod intraledger; +mod lightning; mod me; mod onchain; mod set_username;