Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -932,7 +932,7 @@ private static async Task HandleBuyBitcoin(BreezSdk sdk, Func<string, string?> 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
));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,7 @@ Future<void> _handleBuyBitcoin(BreezSdk sdk, TokenIssuer tokenIssuer, List<Strin
final redirectUrl = results.option('redirect-url');

final result = await sdk.buyBitcoin(
request: BuyBitcoinRequest(lockedAmountSat: lockedAmount, redirectUrl: redirectUrl),
request: BuyBitcoinRequest_Moonpay(lockedAmountSat: lockedAmount, redirectUrl: redirectUrl),
);
print('Open this URL in a browser to complete the purchase:');
print(result.url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -871,7 +871,7 @@ func handleBuyBitcoin(sdk *breez_sdk_spark.BreezSdk, _ *readline.Instance, args
return err
}

req := breez_sdk_spark.BuyBitcoinRequest{}
req := breez_sdk_spark.BuyBitcoinRequestMoonpay{}
if *lockedAmount > 0 {
req.LockedAmountSat = lockedAmount
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -760,7 +760,7 @@ suspend fun handleBuyBitcoin(sdk: BreezSdk, reader: LineReader, args: List<Strin
val redirectUrl = fp.getString("redirect-url")

val result = sdk.buyBitcoin(
BuyBitcoinRequest(
BuyBitcoinRequest.Moonpay(
lockedAmountSat = lockedAmount,
redirectUrl = redirectUrl,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,7 @@ func handleBuyBitcoin(_ sdk: BreezSdk, _ args: [String]) async throws {
let lockedAmount = fp.get("locked-amount-sat").flatMap { UInt64($0) }
let redirectUrl = fp.get("redirect-url")

let result = try await sdk.buyBitcoin(request: BuyBitcoinRequest(
let result = try await sdk.buyBitcoin(request: .moonpay(
lockedAmountSat: lockedAmount,
redirectUrl: redirectUrl
))
Expand Down
29 changes: 19 additions & 10 deletions crates/breez-sdk/cli/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,13 +264,17 @@ pub enum Command {
sat_per_vbyte: Option<u64>,
},
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<u64>,
amount_sat: Option<u64>,

/// Custom redirect URL after purchase completion
/// Custom redirect URL after purchase completion (`MoonPay` only)
#[arg(long)]
redirect_url: Option<String>,
},
Expand Down Expand Up @@ -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)
Expand Down
22 changes: 22 additions & 0 deletions crates/breez-sdk/common/src/buy/cashapp.rs
Original file line number Diff line number Diff line change
@@ -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}"));
}
}
14 changes: 1 addition & 13 deletions crates/breez-sdk/common/src/buy/mod.rs
Original file line number Diff line number Diff line change
@@ -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<u64>,
redirect_url: Option<String>,
) -> Result<String>;
}
6 changes: 1 addition & 5 deletions crates/breez-sdk/common/src/buy/moonpay.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use std::sync::Arc;

use super::BuyBitcoinProviderApi;
use crate::{breez_server::BreezServer, grpc::SignUrlRequest};
use anyhow::Result;
use bitreq::Url;
Expand Down Expand Up @@ -70,11 +69,8 @@ impl MoonpayProvider {
pub fn new(breez_server: Arc<BreezServer>) -> 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<u64>,
Expand Down
42 changes: 31 additions & 11 deletions crates/breez-sdk/core/src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -826,17 +826,37 @@ pub struct ListUnclaimedDepositsResponse {
pub deposits: Vec<DepositInfo>,
}

/// 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<u64>,
/// Optional: Custom redirect URL after purchase completion
#[cfg_attr(feature = "uniffi", uniffi(default=None))]
pub redirect_url: Option<String>,
/// 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<u64>,
/// Custom redirect URL after purchase completion.
redirect_url: Option<String>,
},
/// `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<u64>,
},
}

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
Expand Down
61 changes: 40 additions & 21 deletions crates/breez-sdk/core/src/sdk/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
ListFiatRatesResponse, Network, OptimizationProgress, RegisterWebhookRequest,
RegisterWebhookResponse, SignMessageRequest, SignMessageResponse, UnregisterWebhookRequest,
UpdateUserSettingsRequest, UserSettings, Webhook,
chain::RecommendedFees,
error::SdkError,
events::EventListener,
Expand Down Expand Up @@ -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<BuyBitcoinResponse, SdkError> {
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 })
}
Expand Down
6 changes: 3 additions & 3 deletions crates/breez-sdk/core/src/sdk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -92,7 +92,7 @@ pub struct BreezSdk {
pub(crate) spark_private_mode_initialized: Arc<OnceCell<()>>,
pub(crate) token_converter: Arc<dyn TokenConverter>,
pub(crate) stable_balance: Option<Arc<StableBalance>>,
pub(crate) buy_bitcoin_provider: Arc<dyn BuyBitcoinProviderApi>,
pub(crate) buy_bitcoin_provider: Arc<MoonpayProvider>,
}

pub(crate) struct BreezSdkParams {
Expand All @@ -107,7 +107,7 @@ pub(crate) struct BreezSdkParams {
pub spark_wallet: Arc<SparkWallet>,
pub event_emitter: Arc<EventEmitter>,
pub sync_signing_client: Option<SigningClient>,
pub buy_bitcoin_provider: Arc<dyn BuyBitcoinProviderApi>,
pub buy_bitcoin_provider: Arc<MoonpayProvider>,
}

pub async fn parse_input(
Expand Down
2 changes: 1 addition & 1 deletion crates/breez-sdk/core/src/sdk/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64>,
Expand Down
5 changes: 2 additions & 3 deletions crates/breez-sdk/core/src/sdk_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -590,8 +590,7 @@ impl SdkBuilder {
};

// Create the MoonPay provider for buying Bitcoin
let buy_bitcoin_provider: Arc<dyn BuyBitcoinProviderApi> =
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 {
Expand Down
11 changes: 8 additions & 3 deletions crates/breez-sdk/wasm/src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1332,9 +1332,14 @@ pub struct SparkStatus {
}

#[macros::extern_wasm_bindgen(breez_sdk_spark::BuyBitcoinRequest)]
pub struct BuyBitcoinRequest {
pub locked_amount_sat: Option<u64>,
pub redirect_url: Option<String>,
pub enum BuyBitcoinRequest {
Moonpay {
locked_amount_sat: Option<u64>,
redirect_url: Option<String>,
},
CashApp {
amount_sats: Option<u64>,
},
}

#[macros::extern_wasm_bindgen(breez_sdk_spark::BuyBitcoinResponse)]
Expand Down
Loading
Loading