From f52d2ead57473043e463ef37c7b39c1fd958d4fd Mon Sep 17 00:00:00 2001 From: Erdem Yerebasmaz Date: Thu, 26 Mar 2026 22:04:47 +0300 Subject: [PATCH 1/7] Add CashApp buy-bitcoin provider Introduce a CashApp provider that builds a cash.app/launch/lightning deep link from a bolt11 invoice. Remove the BuyBitcoinProviderApi trait (single implementor) and use MoonpayProvider directly. The CashApp flow is a plain function since it has no server-side dependencies. --- crates/breez-sdk/common/src/buy/cashapp.rs | 22 ++++++++++++++++++++++ crates/breez-sdk/common/src/buy/mod.rs | 14 +------------- crates/breez-sdk/common/src/buy/moonpay.rs | 6 +----- crates/breez-sdk/core/src/sdk/mod.rs | 6 +++--- crates/breez-sdk/core/src/sdk_builder.rs | 5 ++--- 5 files changed, 29 insertions(+), 24 deletions(-) create mode 100644 crates/breez-sdk/common/src/buy/cashapp.rs diff --git a/crates/breez-sdk/common/src/buy/cashapp.rs b/crates/breez-sdk/common/src/buy/cashapp.rs new file mode 100644 index 000000000..c904e1146 --- /dev/null +++ b/crates/breez-sdk/common/src/buy/cashapp.rs @@ -0,0 +1,22 @@ +const CASHAPP_LIGHTNING_BASE_URL: &str = "https://cash.app/launch/lightning/"; + +pub struct CashAppProvider; + +impl CashAppProvider { + /// Build a CashApp deep link URL from a bolt11 Lightning invoice. + pub fn build_url(invoice: &str) -> String { + format!("{CASHAPP_LIGHTNING_BASE_URL}{invoice}") + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + + #[test] + fn test_cashapp_url_construction() { + let invoice = "lnbc100n1p0abcde"; + let url = CashAppProvider::build_url(invoice); + assert_eq!(url, format!("https://cash.app/launch/lightning/{invoice}")); + } +} diff --git a/crates/breez-sdk/common/src/buy/mod.rs b/crates/breez-sdk/common/src/buy/mod.rs index 9424c500a..efdc99100 100644 --- a/crates/breez-sdk/common/src/buy/mod.rs +++ b/crates/breez-sdk/common/src/buy/mod.rs @@ -1,14 +1,2 @@ -use anyhow::Result; - +pub mod cashapp; pub mod moonpay; - -#[macros::async_trait] -pub trait BuyBitcoinProviderApi: Send + Sync { - /// Configure buying Bitcoin and return a URL to continue - async fn buy_bitcoin( - &self, - address: String, - locked_amount_sat: Option, - redirect_url: Option, - ) -> Result; -} diff --git a/crates/breez-sdk/common/src/buy/moonpay.rs b/crates/breez-sdk/common/src/buy/moonpay.rs index 5e645effe..e2748acc9 100644 --- a/crates/breez-sdk/common/src/buy/moonpay.rs +++ b/crates/breez-sdk/common/src/buy/moonpay.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use super::BuyBitcoinProviderApi; use crate::{breez_server::BreezServer, grpc::SignUrlRequest}; use anyhow::Result; use bitreq::Url; @@ -70,11 +69,8 @@ impl MoonpayProvider { pub fn new(breez_server: Arc) -> Self { Self { breez_server } } -} -#[macros::async_trait] -impl BuyBitcoinProviderApi for MoonpayProvider { - async fn buy_bitcoin( + pub async fn buy_bitcoin( &self, address: String, locked_amount_sat: Option, diff --git a/crates/breez-sdk/core/src/sdk/mod.rs b/crates/breez-sdk/core/src/sdk/mod.rs index eb26660aa..e4ae28725 100644 --- a/crates/breez-sdk/core/src/sdk/mod.rs +++ b/crates/breez-sdk/core/src/sdk/mod.rs @@ -12,7 +12,7 @@ mod sync_coordinator; pub(crate) use sync_coordinator::SyncCoordinator; use bitflags::bitflags; -use breez_sdk_common::{buy::BuyBitcoinProviderApi, fiat::FiatService, sync::SigningClient}; +use breez_sdk_common::{buy::moonpay::MoonpayProvider, fiat::FiatService, sync::SigningClient}; use platform_utils::HttpClient; use platform_utils::tokio; use spark_wallet::SparkWallet; @@ -92,7 +92,7 @@ pub struct BreezSdk { pub(crate) spark_private_mode_initialized: Arc>, pub(crate) token_converter: Arc, pub(crate) stable_balance: Option>, - pub(crate) buy_bitcoin_provider: Arc, + pub(crate) buy_bitcoin_provider: Arc, } pub(crate) struct BreezSdkParams { @@ -107,7 +107,7 @@ pub(crate) struct BreezSdkParams { pub spark_wallet: Arc, pub event_emitter: Arc, pub sync_signing_client: Option, - pub buy_bitcoin_provider: Arc, + pub buy_bitcoin_provider: Arc, } pub async fn parse_input( diff --git a/crates/breez-sdk/core/src/sdk_builder.rs b/crates/breez-sdk/core/src/sdk_builder.rs index 20d6b1781..bd2a118bc 100644 --- a/crates/breez-sdk/core/src/sdk_builder.rs +++ b/crates/breez-sdk/core/src/sdk_builder.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use breez_sdk_common::{ breez_server::{BreezServer, PRODUCTION_BREEZSERVER_URL}, - buy::{BuyBitcoinProviderApi, moonpay::MoonpayProvider}, + buy::moonpay::MoonpayProvider, }; use platform_utils::DefaultHttpClient; @@ -590,8 +590,7 @@ impl SdkBuilder { }; // Create the MoonPay provider for buying Bitcoin - let buy_bitcoin_provider: Arc = - Arc::new(MoonpayProvider::new(breez_server.clone())); + let buy_bitcoin_provider = Arc::new(MoonpayProvider::new(breez_server.clone())); // Create the SDK instance let sdk = BreezSdk::init_and_start(BreezSdkParams { From a4926cdaceb03dd4c5851b843c1ffcbbed056732 Mon Sep 17 00:00:00 2001 From: Erdem Yerebasmaz Date: Thu, 26 Mar 2026 22:05:01 +0300 Subject: [PATCH 2/7] Refactor BuyBitcoinRequest to a tagged union Replace the flat struct with a tagged enum so each provider carries only its own parameters: BuyBitcoinRequest::Moonpay { locked_amount_sat, redirect_url } BuyBitcoinRequest::CashApp { amount_sats } Route buy_bitcoin through the selected variant: MoonPay uses an on-chain deposit address, CashApp generates a bolt11 invoice and wraps it in a deep link. CashApp is restricted to mainnet. --- crates/breez-sdk/core/src/models/mod.rs | 42 +++++++++++---- crates/breez-sdk/core/src/sdk/api.rs | 65 +++++++++++++++-------- crates/breez-sdk/core/src/sdk/payments.rs | 2 +- 3 files changed, 74 insertions(+), 35 deletions(-) diff --git a/crates/breez-sdk/core/src/models/mod.rs b/crates/breez-sdk/core/src/models/mod.rs index cf25d6e17..1b625540c 100644 --- a/crates/breez-sdk/core/src/models/mod.rs +++ b/crates/breez-sdk/core/src/models/mod.rs @@ -826,17 +826,37 @@ pub struct ListUnclaimedDepositsResponse { pub deposits: Vec, } -/// Request to buy Bitcoin using an external provider (`MoonPay`) -#[derive(Debug, Clone, Default)] -#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] -pub struct BuyBitcoinRequest { - /// Optional: Lock the purchase to a specific amount in satoshis. - /// When provided, the user cannot change the amount in the purchase flow. - #[cfg_attr(feature = "uniffi", uniffi(default=None))] - pub locked_amount_sat: Option, - /// Optional: Custom redirect URL after purchase completion - #[cfg_attr(feature = "uniffi", uniffi(default=None))] - pub redirect_url: Option, +/// The available providers for buying Bitcoin +/// Request to buy Bitcoin using an external provider. +/// +/// Each variant carries only the parameters relevant to that provider. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] +pub enum BuyBitcoinRequest { + /// `MoonPay`: Fiat-to-Bitcoin via credit card, Apple Pay, etc. + /// Uses an on-chain deposit address. + Moonpay { + /// Lock the purchase to a specific amount in satoshis. + locked_amount_sat: Option, + /// Custom redirect URL after purchase completion. + redirect_url: Option, + }, + /// `CashApp`: Pay via the Lightning Network. + /// Generates a bolt11 invoice and returns a `cash.app` deep link. + /// Only available on mainnet. + CashApp { + /// Amount in satoshis for the Lightning invoice. + amount_sats: Option, + }, +} + +impl Default for BuyBitcoinRequest { + fn default() -> Self { + Self::Moonpay { + locked_amount_sat: None, + redirect_url: None, + } + } } /// Response containing a URL to complete the Bitcoin purchase diff --git a/crates/breez-sdk/core/src/sdk/api.rs b/crates/breez-sdk/core/src/sdk/api.rs index 8a0a68213..a13e23748 100644 --- a/crates/breez-sdk/core/src/sdk/api.rs +++ b/crates/breez-sdk/core/src/sdk/api.rs @@ -2,12 +2,14 @@ use bitcoin::secp256k1::{PublicKey, ecdsa::Signature}; use std::str::FromStr; use tracing::info; +use breez_sdk_common::buy::cashapp::CashAppProvider; + use crate::{ - BuyBitcoinRequest, BuyBitcoinResponse, CheckMessageRequest, CheckMessageResponse, - GetTokensMetadataRequest, GetTokensMetadataResponse, InputType, ListFiatCurrenciesResponse, - ListFiatRatesResponse, OptimizationProgress, RegisterWebhookRequest, RegisterWebhookResponse, - SignMessageRequest, SignMessageResponse, UnregisterWebhookRequest, UpdateUserSettingsRequest, - UserSettings, Webhook, + BuyBitcoinRequest, BuyBitcoinResponse, CheckMessageRequest, + CheckMessageResponse, GetTokensMetadataRequest, GetTokensMetadataResponse, InputType, + ListFiatCurrenciesResponse, ListFiatRatesResponse, Network, OptimizationProgress, + RegisterWebhookRequest, RegisterWebhookResponse, SignMessageRequest, SignMessageResponse, + UnregisterWebhookRequest, UpdateUserSettingsRequest, UserSettings, Webhook, chain::RecommendedFees, error::SdkError, events::EventListener, @@ -320,30 +322,47 @@ impl BreezSdk { Ok(webhooks.into_iter().map(Into::into).collect()) } - /// Initiates a Bitcoin purchase flow via an external provider (`MoonPay`). - /// - /// This method generates a URL that the user can open in a browser to complete - /// the Bitcoin purchase. The purchased Bitcoin will be sent to an automatically - /// generated deposit address. + /// Initiates a Bitcoin purchase flow via an external provider. /// - /// # Arguments + /// Returns a URL the user should open to complete the purchase. + /// The request variant determines the provider and its parameters: /// - /// * `request` - The purchase request containing optional amount and redirect URL - /// - /// # Returns - /// - /// A response containing the URL to open in a browser to complete the purchase + /// - [`BuyBitcoinRequest::Moonpay`]: Fiat-to-Bitcoin via on-chain deposit. + /// - [`BuyBitcoinRequest::CashApp`]: Lightning invoice + `cash.app` deep link (mainnet only). pub async fn buy_bitcoin( &self, request: BuyBitcoinRequest, ) -> Result { - let address = get_deposit_address(&self.spark_wallet, true).await?; - - let url = self - .buy_bitcoin_provider - .buy_bitcoin(address, request.locked_amount_sat, request.redirect_url) - .await - .map_err(|e| SdkError::Generic(format!("Failed to create buy bitcoin URL: {e}")))?; + let url = match request { + BuyBitcoinRequest::Moonpay { + locked_amount_sat, + redirect_url, + } => { + let address = get_deposit_address(&self.spark_wallet, true).await?; + self.buy_bitcoin_provider + .buy_bitcoin(address, locked_amount_sat, redirect_url) + .await + .map_err(|e| { + SdkError::Generic(format!("Failed to create buy bitcoin URL: {e}")) + })? + } + BuyBitcoinRequest::CashApp { amount_sats } => { + if !matches!(self.config.network, Network::Mainnet) { + return Err(SdkError::Generic( + "CashApp is only available on mainnet".to_string(), + )); + } + let receive_response = self + .receive_bolt11_invoice( + "Buy Bitcoin via CashApp".to_string(), + amount_sats, + None, + None, + ) + .await?; + CashAppProvider::build_url(&receive_response.payment_request) + } + }; Ok(BuyBitcoinResponse { url }) } diff --git a/crates/breez-sdk/core/src/sdk/payments.rs b/crates/breez-sdk/core/src/sdk/payments.rs index e0f7eba25..1d2b24386 100644 --- a/crates/breez-sdk/core/src/sdk/payments.rs +++ b/crates/breez-sdk/core/src/sdk/payments.rs @@ -459,7 +459,7 @@ impl BreezSdk { // Private payment methods impl BreezSdk { - async fn receive_bolt11_invoice( + pub(crate) async fn receive_bolt11_invoice( &self, description: String, amount_sats: Option, From e07d73611d8dec25d3274c172715d51b79dc2b2e Mon Sep 17 00:00:00 2001 From: Erdem Yerebasmaz Date: Thu, 26 Mar 2026 22:05:16 +0300 Subject: [PATCH 3/7] Update WASM and Flutter bindings for tagged union BuyBitcoinRequest Expose the new enum variants to JavaScript/TypeScript (wasm-bindgen) and Flutter (flutter_rust_bridge). --- crates/breez-sdk/wasm/src/models/mod.rs | 11 ++++++++--- packages/flutter/rust/src/models.rs | 11 ++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/crates/breez-sdk/wasm/src/models/mod.rs b/crates/breez-sdk/wasm/src/models/mod.rs index 66babfa7d..c9403bb2a 100644 --- a/crates/breez-sdk/wasm/src/models/mod.rs +++ b/crates/breez-sdk/wasm/src/models/mod.rs @@ -1332,9 +1332,14 @@ pub struct SparkStatus { } #[macros::extern_wasm_bindgen(breez_sdk_spark::BuyBitcoinRequest)] -pub struct BuyBitcoinRequest { - pub locked_amount_sat: Option, - pub redirect_url: Option, +pub enum BuyBitcoinRequest { + Moonpay { + locked_amount_sat: Option, + redirect_url: Option, + }, + CashApp { + amount_sats: Option, + }, } #[macros::extern_wasm_bindgen(breez_sdk_spark::BuyBitcoinResponse)] diff --git a/packages/flutter/rust/src/models.rs b/packages/flutter/rust/src/models.rs index 42508e7ff..432e9b6c0 100644 --- a/packages/flutter/rust/src/models.rs +++ b/packages/flutter/rust/src/models.rs @@ -1160,9 +1160,14 @@ pub struct _FetchConversionLimitsResponse { } #[frb(mirror(BuyBitcoinRequest))] -pub struct _BuyBitcoinRequest { - pub locked_amount_sat: Option, - pub redirect_url: Option, +pub enum _BuyBitcoinRequest { + Moonpay { + locked_amount_sat: Option, + redirect_url: Option, + }, + CashApp { + amount_sats: Option, + }, } #[frb(mirror(BuyBitcoinResponse))] From db7a2da2d09d779ef24333cf2cc76a6e394e1a69 Mon Sep 17 00:00:00 2001 From: Erdem Yerebasmaz Date: Thu, 26 Mar 2026 22:05:31 +0300 Subject: [PATCH 4/7] Update Rust CLI for tagged union BuyBitcoinRequest Add --provider flag (moonpay/cashapp) and construct the matching enum variant. --- crates/breez-sdk/cli/src/command/mod.rs | 49 +++++++++++++++---------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/crates/breez-sdk/cli/src/command/mod.rs b/crates/breez-sdk/cli/src/command/mod.rs index 3c6a5e846..ac78f1d4a 100644 --- a/crates/breez-sdk/cli/src/command/mod.rs +++ b/crates/breez-sdk/cli/src/command/mod.rs @@ -4,16 +4,16 @@ mod webhooks; use bitcoin::hashes::{Hash, sha256}; use breez_sdk_spark::{ - AssetFilter, BreezSdk, BuyBitcoinRequest, CheckLightningAddressRequest, ClaimDepositRequest, - ClaimHtlcPaymentRequest, ConversionOptions, ConversionType, Fee, FeePolicy, - FetchConversionLimitsRequest, GetInfoRequest, GetPaymentRequest, GetTokensMetadataRequest, - InputType, LightningAddressDetails, ListPaymentsRequest, ListUnclaimedDepositsRequest, - LnurlPayRequest, LnurlWithdrawRequest, MaxFee, OnchainConfirmationSpeed, PaymentDetailsFilter, - PaymentStatus, PaymentType, PrepareLnurlPayRequest, PrepareSendPaymentRequest, - ReceivePaymentMethod, ReceivePaymentRequest, RefundDepositRequest, - RegisterLightningAddressRequest, SendPaymentMethod, SendPaymentOptions, SendPaymentRequest, - SparkHtlcOptions, SparkHtlcStatus, SyncWalletRequest, TokenIssuer, TokenTransactionType, - UpdateUserSettingsRequest, + AssetFilter, BreezSdk, BuyBitcoinRequest, CheckLightningAddressRequest, + ClaimDepositRequest, ClaimHtlcPaymentRequest, ConversionOptions, ConversionType, Fee, + FeePolicy, FetchConversionLimitsRequest, GetInfoRequest, GetPaymentRequest, + GetTokensMetadataRequest, InputType, LightningAddressDetails, ListPaymentsRequest, + ListUnclaimedDepositsRequest, LnurlPayRequest, LnurlWithdrawRequest, MaxFee, + OnchainConfirmationSpeed, PaymentDetailsFilter, PaymentStatus, PaymentType, + PrepareLnurlPayRequest, PrepareSendPaymentRequest, ReceivePaymentMethod, ReceivePaymentRequest, + RefundDepositRequest, RegisterLightningAddressRequest, SendPaymentMethod, SendPaymentOptions, + SendPaymentRequest, SparkHtlcOptions, SparkHtlcStatus, SyncWalletRequest, TokenIssuer, + TokenTransactionType, UpdateUserSettingsRequest, }; use clap::Parser; use rand::RngCore; @@ -264,13 +264,17 @@ pub enum Command { sat_per_vbyte: Option, }, ListUnclaimedDeposits, - /// Buy Bitcoin using an external provider (`MoonPay`) + /// Buy Bitcoin using an external provider BuyBitcoin { - /// Lock the purchase to a specific amount in satoshis. When provided, the user cannot change the amount in the purchase flow. + /// Provider to use: "moonpay" (default) or "cashapp" + #[arg(long, default_value = "moonpay")] + provider: String, + + /// Amount in satoshis (meaning depends on provider) #[arg(long)] - locked_amount_sat: Option, + amount_sat: Option, - /// Custom redirect URL after purchase completion + /// Custom redirect URL after purchase completion (MoonPay only) #[arg(long)] redirect_url: Option, }, @@ -505,15 +509,20 @@ pub(crate) async fn execute_command( Ok(true) } Command::BuyBitcoin { - locked_amount_sat, + provider, + amount_sat, redirect_url, } => { - let value = sdk - .buy_bitcoin(BuyBitcoinRequest { - locked_amount_sat, + let request = match provider.to_lowercase().as_str() { + "cashapp" | "cash_app" | "cash-app" => BuyBitcoinRequest::CashApp { + amount_sats: amount_sat, + }, + _ => BuyBitcoinRequest::Moonpay { + locked_amount_sat: amount_sat, redirect_url, - }) - .await?; + }, + }; + let value = sdk.buy_bitcoin(request).await?; println!("Open this URL in a browser to complete the purchase:"); println!("{}", value.url); Ok(true) From 5194ae7cf4f58362bf155beb541b023b578a40ad Mon Sep 17 00:00:00 2001 From: Erdem Yerebasmaz Date: Thu, 26 Mar 2026 22:05:38 +0300 Subject: [PATCH 5/7] Document CashApp provider with guide and code snippets Add a CashApp section to the buy-bitcoin guide with platform-specific navigation guidance (pre-open pattern for PWA/mobile). Add CashApp code examples across all nine supported languages. --- .../snippets/csharp/BuyingBitcoin.cs | 15 ++++- .../snippets/flutter/lib/buying_bitcoin.dart | 13 +++- docs/breez-sdk/snippets/go/buying_bitcoin.go | 21 ++++++- .../com/example/kotlinmpplib/BuyingBitcoin.kt | 12 +++- .../snippets/python/src/buying_bitcoin.py | 18 +++++- .../snippets/react-native/buying_bitcoin.ts | 18 +++++- .../snippets/rust/src/buying_bitcoin.rs | 15 ++++- .../Sources/BuyingBitcoin.swift | 12 +++- .../breez-sdk/snippets/wasm/buying_bitcoin.ts | 21 +++++-- docs/breez-sdk/src/guide/buy_bitcoin.md | 59 ++++++++++++++++++- 10 files changed, 184 insertions(+), 20 deletions(-) diff --git a/docs/breez-sdk/snippets/csharp/BuyingBitcoin.cs b/docs/breez-sdk/snippets/csharp/BuyingBitcoin.cs index e67115c64..6c37cc96d 100644 --- a/docs/breez-sdk/snippets/csharp/BuyingBitcoin.cs +++ b/docs/breez-sdk/snippets/csharp/BuyingBitcoin.cs @@ -12,7 +12,7 @@ async Task BuyBitcoin(BreezSdk sdk) // Optionally, set a redirect URL for after the purchase is completed var optionalRedirectUrl = "https://example.com/purchase-complete"; - var request = new BuyBitcoinRequest( + var request = new BuyBitcoinRequest.Moonpay( lockedAmountSat: optionalLockedAmountSat, redirectUrl: optionalRedirectUrl ); @@ -22,5 +22,18 @@ async Task BuyBitcoin(BreezSdk sdk) Console.WriteLine($"{response.url}"); // ANCHOR_END: buy-bitcoin } + + async Task BuyBitcoinViaCashapp(BreezSdk sdk) + { + // ANCHOR: buy-bitcoin-cashapp + var request = new BuyBitcoinRequest.CashApp( + amountSats: null + ); + + var response = await sdk.BuyBitcoin(request: request); + Console.WriteLine("Open this URL in Cash App to complete the purchase:"); + Console.WriteLine($"{response.url}"); + // ANCHOR_END: buy-bitcoin-cashapp + } } } diff --git a/docs/breez-sdk/snippets/flutter/lib/buying_bitcoin.dart b/docs/breez-sdk/snippets/flutter/lib/buying_bitcoin.dart index e212d5b80..cfafb2009 100644 --- a/docs/breez-sdk/snippets/flutter/lib/buying_bitcoin.dart +++ b/docs/breez-sdk/snippets/flutter/lib/buying_bitcoin.dart @@ -7,7 +7,7 @@ Future buyBitcoin(BreezSdk sdk) async { // Optionally, set a redirect URL for after the purchase is completed final optionalRedirectUrl = "https://example.com/purchase-complete"; - final request = BuyBitcoinRequest( + final request = BuyBitcoinRequest_Moonpay( lockedAmountSat: optionalLockedAmountSat, redirectUrl: optionalRedirectUrl); @@ -16,3 +16,14 @@ Future buyBitcoin(BreezSdk sdk) async { print(response.url); // ANCHOR_END: buy-bitcoin } + +Future buyBitcoinViaCashapp(BreezSdk sdk) async { + // ANCHOR: buy-bitcoin-cashapp + final request = BuyBitcoinRequest_CashApp( + amountSats: null); + + final response = await sdk.buyBitcoin(request: request); + print("Open this URL in Cash App to complete the purchase:"); + print(response.url); + // ANCHOR_END: buy-bitcoin-cashapp +} diff --git a/docs/breez-sdk/snippets/go/buying_bitcoin.go b/docs/breez-sdk/snippets/go/buying_bitcoin.go index b19b53957..526fd5307 100644 --- a/docs/breez-sdk/snippets/go/buying_bitcoin.go +++ b/docs/breez-sdk/snippets/go/buying_bitcoin.go @@ -8,12 +8,10 @@ import ( func BuyBitcoin(sdk *breez_sdk_spark.BreezSdk) error { // ANCHOR: buy-bitcoin - // Optionally, lock the purchase to a specific amount optionalLockedAmountSat := uint64(100_000) - // Optionally, set a redirect URL for after the purchase is completed optionalRedirectUrl := "https://example.com/purchase-complete" - request := breez_sdk_spark.BuyBitcoinRequest{ + request := breez_sdk_spark.BuyBitcoinRequestMoonpay{ LockedAmountSat: &optionalLockedAmountSat, RedirectUrl: &optionalRedirectUrl, } @@ -28,3 +26,20 @@ func BuyBitcoin(sdk *breez_sdk_spark.BreezSdk) error { // ANCHOR_END: buy-bitcoin return nil } + +func BuyBitcoinViaCashapp(sdk *breez_sdk_spark.BreezSdk) error { + // ANCHOR: buy-bitcoin-cashapp + request := breez_sdk_spark.BuyBitcoinRequestCashApp{ + AmountSats: nil, + } + + response, err := sdk.BuyBitcoin(request) + if err != nil { + return err + } + + log.Printf("Open this URL in Cash App to complete the purchase:") + log.Printf("%v", response.Url) + // ANCHOR_END: buy-bitcoin-cashapp + return nil +} diff --git a/docs/breez-sdk/snippets/kotlin_mpp_lib/shared/src/commonMain/kotlin/com/example/kotlinmpplib/BuyingBitcoin.kt b/docs/breez-sdk/snippets/kotlin_mpp_lib/shared/src/commonMain/kotlin/com/example/kotlinmpplib/BuyingBitcoin.kt index ab259c88a..8b713e4d2 100644 --- a/docs/breez-sdk/snippets/kotlin_mpp_lib/shared/src/commonMain/kotlin/com/example/kotlinmpplib/BuyingBitcoin.kt +++ b/docs/breez-sdk/snippets/kotlin_mpp_lib/shared/src/commonMain/kotlin/com/example/kotlinmpplib/BuyingBitcoin.kt @@ -10,7 +10,7 @@ class BuyingBitcoin { // Optionally, set a redirect URL for after the purchase is completed val optionalRedirectUrl = "https://example.com/purchase-complete" - val request = BuyBitcoinRequest( + val request = BuyBitcoinRequest.Moonpay( lockedAmountSat = optionalLockedAmountSat, redirectUrl = optionalRedirectUrl ) @@ -20,4 +20,14 @@ class BuyingBitcoin { // Log.v("Breez", "${response.url}") // ANCHOR_END: buy-bitcoin } + + suspend fun buyBitcoinViaCashapp(sdk: BreezSdk) { + // ANCHOR: buy-bitcoin-cashapp + val request = BuyBitcoinRequest.CashApp(amountSats = null) + + val response = sdk.buyBitcoin(request) + // Log.v("Breez", "Open this URL in Cash App to complete the purchase:") + // Log.v("Breez", "${response.url}") + // ANCHOR_END: buy-bitcoin-cashapp + } } diff --git a/docs/breez-sdk/snippets/python/src/buying_bitcoin.py b/docs/breez-sdk/snippets/python/src/buying_bitcoin.py index 309fbb2a2..e7f82ee18 100644 --- a/docs/breez-sdk/snippets/python/src/buying_bitcoin.py +++ b/docs/breez-sdk/snippets/python/src/buying_bitcoin.py @@ -14,7 +14,7 @@ async def buy_bitcoin(sdk: BreezSdk): optional_redirect_url = "https://example.com/purchase-complete" try: - request = BuyBitcoinRequest( + request = BuyBitcoinRequest.MOONPAY( locked_amount_sat=optional_locked_amount_sat, redirect_url=optional_redirect_url, ) @@ -26,3 +26,19 @@ async def buy_bitcoin(sdk: BreezSdk): logging.error(error) raise # ANCHOR_END: buy-bitcoin + + +async def buy_bitcoin_via_cashapp(sdk: BreezSdk): + # ANCHOR: buy-bitcoin-cashapp + try: + request = BuyBitcoinRequest.CASH_APP( + amount_sats=None, + ) + + response = await sdk.buy_bitcoin(request=request) + logging.debug("Open this URL in Cash App to complete the purchase:") + logging.debug(response.url) + except Exception as error: + logging.error(error) + raise + # ANCHOR_END: buy-bitcoin-cashapp diff --git a/docs/breez-sdk/snippets/react-native/buying_bitcoin.ts b/docs/breez-sdk/snippets/react-native/buying_bitcoin.ts index e1c730c02..56d0029aa 100644 --- a/docs/breez-sdk/snippets/react-native/buying_bitcoin.ts +++ b/docs/breez-sdk/snippets/react-native/buying_bitcoin.ts @@ -1,6 +1,6 @@ import { type BreezSdk, - type BuyBitcoinRequest + BuyBitcoinRequest } from '@breeztech/breez-sdk-spark-react-native' const buyBitcoin = async (sdk: BreezSdk) => { @@ -10,13 +10,25 @@ const buyBitcoin = async (sdk: BreezSdk) => { // Optionally, set a redirect URL for after the purchase is completed const optionalRedirectUrl = 'https://example.com/purchase-complete' - const request: BuyBitcoinRequest = { + const request = new BuyBitcoinRequest.Moonpay({ lockedAmountSat: optionalLockedAmountSat, redirectUrl: optionalRedirectUrl - } + }) const response = await sdk.buyBitcoin(request) console.log('Open this URL in a browser to complete the purchase:') console.log(response.url) // ANCHOR_END: buy-bitcoin } + +const buyBitcoinViaCashapp = async (sdk: BreezSdk) => { + // ANCHOR: buy-bitcoin-cashapp + const request = new BuyBitcoinRequest.CashApp({ + amountSats: undefined + }) + + const response = await sdk.buyBitcoin(request) + console.log('Open this URL in Cash App to complete the purchase:') + console.log(response.url) + // ANCHOR_END: buy-bitcoin-cashapp +} diff --git a/docs/breez-sdk/snippets/rust/src/buying_bitcoin.rs b/docs/breez-sdk/snippets/rust/src/buying_bitcoin.rs index 245010e83..1f165b138 100644 --- a/docs/breez-sdk/snippets/rust/src/buying_bitcoin.rs +++ b/docs/breez-sdk/snippets/rust/src/buying_bitcoin.rs @@ -9,7 +9,7 @@ async fn buy_bitcoin(sdk: &BreezSdk) -> Result<()> { // Optionally, set a redirect URL for after the purchase is completed let optional_redirect_url = Some("https://example.com/purchase-complete".to_string()); - let request = BuyBitcoinRequest { + let request = BuyBitcoinRequest::Moonpay { locked_amount_sat: optional_locked_amount_sat, redirect_url: optional_redirect_url, }; @@ -20,3 +20,16 @@ async fn buy_bitcoin(sdk: &BreezSdk) -> Result<()> { // ANCHOR_END: buy-bitcoin Ok(()) } + +async fn buy_bitcoin_via_cashapp(sdk: &BreezSdk) -> Result<()> { + // ANCHOR: buy-bitcoin-cashapp + let request = BuyBitcoinRequest::CashApp { + amount_sats: None, + }; + + let response = sdk.buy_bitcoin(request).await?; + info!("Open this URL in Cash App to complete the purchase:"); + info!("{}", response.url); + // ANCHOR_END: buy-bitcoin-cashapp + Ok(()) +} diff --git a/docs/breez-sdk/snippets/swift/BreezSdkSnippets/Sources/BuyingBitcoin.swift b/docs/breez-sdk/snippets/swift/BreezSdkSnippets/Sources/BuyingBitcoin.swift index bea11487f..f98686c9c 100644 --- a/docs/breez-sdk/snippets/swift/BreezSdkSnippets/Sources/BuyingBitcoin.swift +++ b/docs/breez-sdk/snippets/swift/BreezSdkSnippets/Sources/BuyingBitcoin.swift @@ -7,7 +7,7 @@ func buyBitcoin(sdk: BreezSdk) async throws { // Optionally, set a redirect URL for after the purchase is completed let optionalRedirectUrl = "https://example.com/purchase-complete" - let request = BuyBitcoinRequest( + let request = BuyBitcoinRequest.moonpay( lockedAmountSat: optionalLockedAmountSat, redirectUrl: optionalRedirectUrl ) @@ -17,3 +17,13 @@ func buyBitcoin(sdk: BreezSdk) async throws { print("\(response.url)") // ANCHOR_END: buy-bitcoin } + +func buyBitcoinViaCashapp(sdk: BreezSdk) async throws { + // ANCHOR: buy-bitcoin-cashapp + let request = BuyBitcoinRequest.cashApp(amountSats: nil) + + let response = try await sdk.buyBitcoin(request: request) + print("Open this URL in Cash App to complete the purchase:") + print("\(response.url)") + // ANCHOR_END: buy-bitcoin-cashapp +} diff --git a/docs/breez-sdk/snippets/wasm/buying_bitcoin.ts b/docs/breez-sdk/snippets/wasm/buying_bitcoin.ts index d352e92e5..f0b49bd25 100644 --- a/docs/breez-sdk/snippets/wasm/buying_bitcoin.ts +++ b/docs/breez-sdk/snippets/wasm/buying_bitcoin.ts @@ -1,6 +1,5 @@ import { - type BreezSdk, - type BuyBitcoinRequest + type BreezSdk } from '@breeztech/breez-sdk-spark' const buyBitcoin = async (sdk: BreezSdk) => { @@ -10,13 +9,23 @@ const buyBitcoin = async (sdk: BreezSdk) => { // Optionally, set a redirect URL for after the purchase is completed const optionalRedirectUrl = 'https://example.com/purchase-complete' - const request: BuyBitcoinRequest = { + const response = await sdk.buyBitcoin({ + type: 'moonpay', lockedAmountSat: optionalLockedAmountSat, redirectUrl: optionalRedirectUrl - } - - const response = await sdk.buyBitcoin(request) + }) console.log('Open this URL in a browser to complete the purchase:') console.log(response.url) // ANCHOR_END: buy-bitcoin } + +const buyBitcoinViaCashapp = async (sdk: BreezSdk) => { + // ANCHOR: buy-bitcoin-cashapp + const response = await sdk.buyBitcoin({ + type: 'cashApp', + amountSats: undefined + }) + console.log('Open this URL in Cash App to complete the purchase:') + console.log(response.url) + // ANCHOR_END: buy-bitcoin-cashapp +} diff --git a/docs/breez-sdk/src/guide/buy_bitcoin.md b/docs/breez-sdk/src/guide/buy_bitcoin.md index ffd638b6c..96bcf1454 100644 --- a/docs/breez-sdk/src/guide/buy_bitcoin.md +++ b/docs/breez-sdk/src/guide/buy_bitcoin.md @@ -1,8 +1,12 @@ # Buying Bitcoin -The Breez SDK allows users to purchase Bitcoin through external providers, such as Moonpay. The user is directed to a provider URL in their browser to complete the purchase, and the funds are deposited directly into their wallet. +The Breez SDK allows users to purchase Bitcoin through external providers. Two providers are currently supported: **MoonPay** and **CashApp**. The user is directed to a provider URL in their browser (or the CashApp app) to complete the purchase, and the funds are deposited directly into their wallet. -To initiate a Bitcoin purchase: +## MoonPay + +MoonPay uses **on-chain Bitcoin deposit addresses** to receive purchased funds. It supports fiat-to-Bitcoin purchases via credit card, Apple Pay, Google Pay, and other payment methods. + +To initiate a Bitcoin purchase via MoonPay: {{#tabs buying_bitcoin:buy-bitcoin}} @@ -16,3 +20,54 @@ MoonPay supports Apple Pay and Google Pay, but these payment methods will not wo - **Android**: Open the URL using Chrome Custom Tabs. - **Desktop**: Apple Pay requires Safari; Google Pay requires Chrome. + +## CashApp + +CashApp uses **Lightning (bolt11 invoices)** to receive purchased funds. The SDK generates a Lightning invoice and returns a CashApp deep link (`cash.app/launch/lightning/...`) that opens the CashApp for the user to complete payment. + +
+

Developer note

+CashApp is only available on mainnet. Attempting to use CashApp on testnet or regtest will return an error. +
+ +To initiate a Bitcoin purchase via CashApp: + +{{#tabs buying_bitcoin:buy-bitcoin-cashapp}} + +The returned URL is a CashApp universal link (`https://cash.app/launch/lightning/`). On devices with CashApp installed it opens the app directly; otherwise it falls back to the CashApp website. + +
+

Developer note

+ +The URL is obtained after an async SDK call, which means window.open() will be blocked by popup blockers on most mobile browsers and PWAs. + +**Recommended approach: pre-open a blank tab before the async call:** + +```javascript +// 1. Open blank tab synchronously during the user gesture (click handler) +const newTab = window.open('', '_blank'); + +// 2. Async SDK call +const response = await sdk.buyBitcoin({ provider: 'cashApp' }); + +// 3. Navigate the pre-opened tab (or fall back to same-tab) +if (newTab) { + newTab.location.href = response.url; +} else { + // Mobile/PWA: popup was blocked, navigate in same tab + window.location.href = response.url; +} +``` + +**Platform-specific guidance:** + +| Platform | Behavior | Recommendation | +|----------|----------|----------------| +| **Desktop browsers** | Pre-opened tab works reliably | Use the pattern above | +| **Mobile browsers** | `window.open` may be blocked after async | Falls back to `location.href` automatically | +| **PWA (standalone)** | `window.open` is almost always blocked | Same-tab redirect via `location.href`; opens system browser | +| **iOS (native)** | Use `SFSafariViewController` or `UIApplication.open()` | Universal link triggers CashApp if installed | +| **Android (native)** | Use Chrome Custom Tabs or `Intent` | Universal link triggers CashApp if installed | + +**CashApp availability:** US and UK only (excluding New York State for Bitcoin/Lightning features). CashApp handles region restrictions on their end, so no client-side gating is needed. +
From cb95d14711876debe7b451f7eb15db93eb6fcd3f Mon Sep 17 00:00:00 2001 From: Erdem Yerebasmaz Date: Thu, 26 Mar 2026 22:14:25 +0300 Subject: [PATCH 6/7] Fix formatting and clippy warnings --- crates/breez-sdk/cli/src/command/mod.rs | 22 +++++++++++----------- crates/breez-sdk/common/src/buy/cashapp.rs | 2 +- crates/breez-sdk/core/src/sdk/api.rs | 10 +++++----- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/crates/breez-sdk/cli/src/command/mod.rs b/crates/breez-sdk/cli/src/command/mod.rs index ac78f1d4a..548ac12a8 100644 --- a/crates/breez-sdk/cli/src/command/mod.rs +++ b/crates/breez-sdk/cli/src/command/mod.rs @@ -4,16 +4,16 @@ mod webhooks; use bitcoin::hashes::{Hash, sha256}; use breez_sdk_spark::{ - AssetFilter, BreezSdk, BuyBitcoinRequest, CheckLightningAddressRequest, - ClaimDepositRequest, ClaimHtlcPaymentRequest, ConversionOptions, ConversionType, Fee, - FeePolicy, FetchConversionLimitsRequest, GetInfoRequest, GetPaymentRequest, - GetTokensMetadataRequest, InputType, LightningAddressDetails, ListPaymentsRequest, - ListUnclaimedDepositsRequest, LnurlPayRequest, LnurlWithdrawRequest, MaxFee, - OnchainConfirmationSpeed, PaymentDetailsFilter, PaymentStatus, PaymentType, - PrepareLnurlPayRequest, PrepareSendPaymentRequest, ReceivePaymentMethod, ReceivePaymentRequest, - RefundDepositRequest, RegisterLightningAddressRequest, SendPaymentMethod, SendPaymentOptions, - SendPaymentRequest, SparkHtlcOptions, SparkHtlcStatus, SyncWalletRequest, TokenIssuer, - TokenTransactionType, UpdateUserSettingsRequest, + AssetFilter, BreezSdk, BuyBitcoinRequest, CheckLightningAddressRequest, ClaimDepositRequest, + ClaimHtlcPaymentRequest, ConversionOptions, ConversionType, Fee, FeePolicy, + FetchConversionLimitsRequest, GetInfoRequest, GetPaymentRequest, GetTokensMetadataRequest, + InputType, LightningAddressDetails, ListPaymentsRequest, ListUnclaimedDepositsRequest, + LnurlPayRequest, LnurlWithdrawRequest, MaxFee, OnchainConfirmationSpeed, PaymentDetailsFilter, + PaymentStatus, PaymentType, PrepareLnurlPayRequest, PrepareSendPaymentRequest, + ReceivePaymentMethod, ReceivePaymentRequest, RefundDepositRequest, + RegisterLightningAddressRequest, SendPaymentMethod, SendPaymentOptions, SendPaymentRequest, + SparkHtlcOptions, SparkHtlcStatus, SyncWalletRequest, TokenIssuer, TokenTransactionType, + UpdateUserSettingsRequest, }; use clap::Parser; use rand::RngCore; @@ -274,7 +274,7 @@ pub enum Command { #[arg(long)] amount_sat: Option, - /// Custom redirect URL after purchase completion (MoonPay only) + /// Custom redirect URL after purchase completion (`MoonPay` only) #[arg(long)] redirect_url: Option, }, diff --git a/crates/breez-sdk/common/src/buy/cashapp.rs b/crates/breez-sdk/common/src/buy/cashapp.rs index c904e1146..a66dfd3d9 100644 --- a/crates/breez-sdk/common/src/buy/cashapp.rs +++ b/crates/breez-sdk/common/src/buy/cashapp.rs @@ -3,7 +3,7 @@ const CASHAPP_LIGHTNING_BASE_URL: &str = "https://cash.app/launch/lightning/"; pub struct CashAppProvider; impl CashAppProvider { - /// Build a CashApp deep link URL from a bolt11 Lightning invoice. + /// Build a `CashApp` deep link URL from a bolt11 Lightning invoice. pub fn build_url(invoice: &str) -> String { format!("{CASHAPP_LIGHTNING_BASE_URL}{invoice}") } diff --git a/crates/breez-sdk/core/src/sdk/api.rs b/crates/breez-sdk/core/src/sdk/api.rs index a13e23748..024ced836 100644 --- a/crates/breez-sdk/core/src/sdk/api.rs +++ b/crates/breez-sdk/core/src/sdk/api.rs @@ -5,11 +5,11 @@ use tracing::info; use breez_sdk_common::buy::cashapp::CashAppProvider; use crate::{ - BuyBitcoinRequest, BuyBitcoinResponse, CheckMessageRequest, - CheckMessageResponse, GetTokensMetadataRequest, GetTokensMetadataResponse, InputType, - ListFiatCurrenciesResponse, ListFiatRatesResponse, Network, OptimizationProgress, - RegisterWebhookRequest, RegisterWebhookResponse, SignMessageRequest, SignMessageResponse, - UnregisterWebhookRequest, UpdateUserSettingsRequest, UserSettings, Webhook, + BuyBitcoinRequest, BuyBitcoinResponse, CheckMessageRequest, CheckMessageResponse, + GetTokensMetadataRequest, GetTokensMetadataResponse, InputType, ListFiatCurrenciesResponse, + ListFiatRatesResponse, Network, OptimizationProgress, RegisterWebhookRequest, + RegisterWebhookResponse, SignMessageRequest, SignMessageResponse, UnregisterWebhookRequest, + UpdateUserSettingsRequest, UserSettings, Webhook, chain::RecommendedFees, error::SdkError, events::EventListener, From a041accd3dbf7ec899b45feaa4ed47222c402d25 Mon Sep 17 00:00:00 2001 From: Erdem Yerebasmaz Date: Thu, 26 Mar 2026 22:44:53 +0300 Subject: [PATCH 7/7] Update CLI modification policy and fix language CLI build failures Update CLAUDE.md CLI modification policy to require fixing CI failures with minimal changes. Fix language CLIs to use the new tagged union BuyBitcoinRequest.Moonpay constructor. --- CLAUDE.md | 8 ++++---- .../bindings/examples/cli/langs/csharp/Commands.cs | 2 +- .../bindings/examples/cli/langs/flutter/lib/commands.dart | 2 +- .../bindings/examples/cli/langs/golang/commands.go | 2 +- .../kotlin-multiplatform/src/main/kotlin/Commands.kt | 2 +- .../cli/langs/swift/Sources/BreezCLI/Commands.swift | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e27b8e922..4c74520d2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -138,12 +138,12 @@ See [snippets-processor/src/main.rs](docs/breez-sdk/snippets-processor/src/main. ## CLI Modification Policy **Do not modify language-specific CLIs** (`crates/breez-sdk/bindings/examples/cli/langs/`) unless: -- Fixing issues caught by static analysis (clippy, linting, build errors) in those CLIs -- Explicitly requested by the user (e.g. porting a new feature for testing) +- Fixing failures in the **CLI matrix** or **Flutter** (which includes Flutter/Dart CLI static analysis) CI jobs. Always fix those CI failures, as this gives the Sync CLI Languages workflow (`sync-cli.yml`) better context when propagating future Rust CLI changes. Keep fixes minimal: only make changes needed to pass the build. Do not add new features, flags, or update descriptions; leave full feature propagation to the sync workflow. +- Explicitly requested by the user (e.g. porting a new feature for testing). -The **Sync CLI Languages** workflow (`sync-cli.yml`) handles propagating Rust CLI changes to all language CLIs automatically. Modifying language CLIs directly on PRs creates noise and conflicts with the sync agent. +The **Sync CLI Languages** workflow (`sync-cli.yml`) automatically propagates Rust CLI changes to all language CLIs. Unnecessary modifications to language CLIs create PR noise. -The **Rust CLI** (`crates/breez-sdk/cli/`) can be modified freely — it is the source of truth that the sync workflow reads from. +The **Rust CLI** (`crates/breez-sdk/cli/`) can be modified freely as it is the source of truth that the sync workflow reads from. ## Workspace Configuration diff --git a/crates/breez-sdk/bindings/examples/cli/langs/csharp/Commands.cs b/crates/breez-sdk/bindings/examples/cli/langs/csharp/Commands.cs index 0fbea7de0..883c8536a 100644 --- a/crates/breez-sdk/bindings/examples/cli/langs/csharp/Commands.cs +++ b/crates/breez-sdk/bindings/examples/cli/langs/csharp/Commands.cs @@ -932,7 +932,7 @@ private static async Task HandleBuyBitcoin(BreezSdk sdk, Func r var lockedAmountStr = GetFlag(args, "--locked-amount-sat"); var redirectUrl = GetFlag(args, "--redirect-url"); - var result = await sdk.BuyBitcoin(new BuyBitcoinRequest( + var result = await sdk.BuyBitcoin(new BuyBitcoinRequest.Moonpay( lockedAmountSat: ParseOptionalUlong(lockedAmountStr), redirectUrl: redirectUrl )); diff --git a/crates/breez-sdk/bindings/examples/cli/langs/flutter/lib/commands.dart b/crates/breez-sdk/bindings/examples/cli/langs/flutter/lib/commands.dart index fed10191b..d5e8861fa 100644 --- a/crates/breez-sdk/bindings/examples/cli/langs/flutter/lib/commands.dart +++ b/crates/breez-sdk/bindings/examples/cli/langs/flutter/lib/commands.dart @@ -726,7 +726,7 @@ Future _handleBuyBitcoin(BreezSdk sdk, TokenIssuer tokenIssuer, List 0 { req.LockedAmountSat = lockedAmount } diff --git a/crates/breez-sdk/bindings/examples/cli/langs/kotlin-multiplatform/src/main/kotlin/Commands.kt b/crates/breez-sdk/bindings/examples/cli/langs/kotlin-multiplatform/src/main/kotlin/Commands.kt index 1d0a77189..9f8b44186 100644 --- a/crates/breez-sdk/bindings/examples/cli/langs/kotlin-multiplatform/src/main/kotlin/Commands.kt +++ b/crates/breez-sdk/bindings/examples/cli/langs/kotlin-multiplatform/src/main/kotlin/Commands.kt @@ -760,7 +760,7 @@ suspend fun handleBuyBitcoin(sdk: BreezSdk, reader: LineReader, args: List