Repository: breez/breez-sdk-greenlight (language: python)

Issue:
Moonpay: Add ability to set custom redirectURL on complete
Allow the caller to set a custom `MoonPayConfig::redirect_url`, which can point to their own custom landing page or in-app view.

https://github.com/breez/breez-sdk/blob/e541135b0f1fa90f964829444cb34d53251660e5/libs/sdk-core/src/moonpay.rs#L24


Discussion / hints:

Fix patch:
diff --git a/libs/Cargo.lock b/libs/Cargo.lock
index be1c35e98..cce201352 100644
--- a/libs/Cargo.lock
+++ b/libs/Cargo.lock
@@ -2808,6 +2808,7 @@ dependencies = [
  "tokio",
  "tonic",
  "tonic-build",
+ "url",
 ]
 
 [[package]]
diff --git a/libs/sdk-bindings/src/breez_sdk.udl b/libs/sdk-bindings/src/breez_sdk.udl
index d47fc198c..b9b9d22de 100644
--- a/libs/sdk-bindings/src/breez_sdk.udl
+++ b/libs/sdk-bindings/src/breez_sdk.udl
@@ -491,6 +491,7 @@ dictionary ReceiveOnchainRequest {
 dictionary BuyBitcoinRequest {
     BuyBitcoinProvider provider;
     OpeningFeeParams? opening_fee_params = null;
+    string? redirect_url = null;
 };
 
 dictionary BuyBitcoinResponse {
diff --git a/libs/sdk-common/Cargo.toml b/libs/sdk-common/Cargo.toml
index 735e28575..d8764454b 100644
--- a/libs/sdk-common/Cargo.toml
+++ b/libs/sdk-common/Cargo.toml
@@ -28,6 +28,7 @@ tonic = { workspace = true, features = [
     "tls-roots",
     "tls-webpki-roots",
 ] }
+url = "2.5.0"
 
 [dev-dependencies]
 bitcoin = { workspace = true, features = ["rand"] }
diff --git a/libs/sdk-common/src/buy/mod.rs b/libs/sdk-common/src/buy/mod.rs
new file mode 100644
index 000000000..00b05997f
--- /dev/null
+++ b/libs/sdk-common/src/buy/mod.rs
@@ -0,0 +1,15 @@
+use anyhow::Result;
+
+pub mod moonpay;
+
+#[tonic::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>,
+        max_amount_sat: Option<u64>,
+        redirect_url: Option<String>,
+    ) -> Result<String>;
+}
diff --git a/libs/sdk-common/src/buy/moonpay.rs b/libs/sdk-common/src/buy/moonpay.rs
new file mode 100644
index 000000000..922d2d0be
--- /dev/null
+++ b/libs/sdk-common/src/buy/moonpay.rs
@@ -0,0 +1,171 @@
+use std::sync::Arc;
+
+use anyhow::Result;
+use url::Url;
+
+use crate::{grpc::SignUrlRequest, prelude::BreezServer};
+
+use super::BuyBitcoinProviderApi;
+
+#[derive(Clone)]
+struct MoonPayConfig {
+    pub base_url: String,
+    pub api_key: String,
+    pub lock_amount: String,
+    pub currency_code: String,
+    pub color_code: String,
+    pub redirect_url: String,
+    pub enabled_payment_methods: String,
+}
+
+fn moonpay_config() -> MoonPayConfig {
+    MoonPayConfig {
+        base_url: String::from("https://buy.moonpay.io"),
+        api_key: String::from("pk_live_Mx5g6bpD6Etd7T0bupthv7smoTNn2Vr"),
+        lock_amount: String::from("true"),
+        currency_code: String::from("btc"),
+        color_code: String::from("#055DEB"),
+        redirect_url: String::from("https://buy.moonpay.io/transaction_receipt?addFunds=true"),
+        enabled_payment_methods: String::from(
+            "credit_debit_card,sepa_bank_transfer,gbp_bank_transfer",
+        ),
+    }
+}
+
+fn create_moonpay_url(
+    wallet_address: String,
+    quote_currency_amount: Option<String>,
+    max_quote_currency_amount: Option<String>,
+    redirect_url: Option<String>,
+) -> Result<Url> {
+    let config = moonpay_config();
+    let mut params = vec![
+        ("apiKey", config.api_key),
+        ("currencyCode", config.currency_code),
+        ("colorCode", config.color_code),
+        ("redirectURL", redirect_url.unwrap_or(config.redirect_url)),
+        ("enabledPaymentMethods", config.enabled_payment_methods),
+        ("walletAddress", wallet_address),
+    ];
+
+    if let Some(quote_currency_amount) = quote_currency_amount {
+        params.extend(vec![
+            ("quoteCurrencyAmount", quote_currency_amount),
+            ("lockAmount", config.lock_amount),
+        ]);
+    }
+
+    if let Some(max_quote_currency_amount) = max_quote_currency_amount {
+        params.extend(vec![("maxQuoteCurrencyAmount", max_quote_currency_amount)]);
+    }
+
+    let url = Url::parse_with_params(&config.base_url, params)?;
+    Ok(url)
+}
+
+pub struct MoonpayProvider {
+    breez_server: Arc<BreezServer>,
+}
+
+impl MoonpayProvider {
+    pub fn new(breez_server: Arc<BreezServer>) -> Self {
+        Self { breez_server }
+    }
+}
+
+#[tonic::async_trait]
+impl BuyBitcoinProviderApi for MoonpayProvider {
+    async fn buy_bitcoin(
+        &self,
+        address: String,
+        locked_amount_sat: Option<u64>,
+        max_amount_sat: Option<u64>,
+        redirect_url: Option<String>,
+    ) -> Result<String> {
+        let config = moonpay_config();
+        let url = create_moonpay_url(
+            address,
+            locked_amount_sat.map(|amount| format!("{:.8}", amount as f64 / 100_000_000.0)),
+            max_amount_sat.map(|amount| format!("{:.8}", amount as f64 / 100_000_000.0)),
+            redirect_url,
+        )?;
+        let mut signer = self.breez_server.get_signer_client().await;
+        let signed_url = signer
+            .sign_url(SignUrlRequest {
+                base_url: config.base_url.clone(),
+                query_string: format!("?{}", url.query().unwrap()),
+            })
+            .await?
+            .into_inner()
+            .full_url;
+        Ok(signed_url)
+    }
+}
+
+#[cfg(test)]
+pub(crate) mod tests {
+    use std::collections::HashMap;
+
+    use crate::prelude::moonpay::{create_moonpay_url, moonpay_config};
+
+    #[tokio::test]
+    async fn test_sign_moonpay_url() -> Result<(), Box<dyn std::error::Error>> {
+        let wallet_address = "a wallet address".to_string();
+        let quote_amount = "a quote amount".to_string();
+        let config = moonpay_config();
+
+        let url = create_moonpay_url(
+            wallet_address.clone(),
+            Some(quote_amount.clone()),
+            None,
+            None,
+        )?;
+
+        let query_pairs = url.query_pairs().into_owned().collect::<HashMap<_, _>>();
+        assert_eq!(url.host_str(), Some("buy.moonpay.io"));
+        assert_eq!(url.path(), "/");
+        assert_eq!(query_pairs.get("apiKey"), Some(&config.api_key));
+        assert_eq!(query_pairs.get("currencyCode"), Some(&config.currency_code));
+        assert_eq!(query_pairs.get("colorCode"), Some(&config.color_code));
+        assert_eq!(query_pairs.get("redirectURL"), Some(&config.redirect_url));
+        assert_eq!(query_pairs.get("lockAmount"), Some(&config.lock_amount));
+        assert_eq!(
+            query_pairs.get("enabledPaymentMethods"),
+            Some(&config.enabled_payment_methods),
+        );
+        assert_eq!(query_pairs.get("walletAddress"), Some(&wallet_address));
+        assert_eq!(query_pairs.get("quoteCurrencyAmount"), Some(&quote_amount),);
+        Ok(())
+    }
+
+    #[tokio::test]
+    async fn test_sign_moonpay_url_with_redirect() -> Result<(), Box<dyn std::error::Error>> {
+        let wallet_address = "a wallet address".to_string();
+        let quote_amount = "a quote amount".to_string();
+        let redirect_url = "https://test.moonpay.url/receipt".to_string();
+        let config = moonpay_config();
+
+        let url = create_moonpay_url(
+            wallet_address.clone(),
+            Some(quote_amount.clone()),
+            None,
+            Some(redirect_url.clone()),
+        )?;
+
+        let query_pairs = url.query_pairs().into_owned().collect::<HashMap<_, _>>();
+        assert_eq!(url.host_str(), Some("buy.moonpay.io"));
+        assert_eq!(url.path(), "/");
+        assert_eq!(query_pairs.get("apiKey"), Some(&config.api_key));
+        assert_eq!(query_pairs.get("currencyCode"), Some(&config.currency_code));
+        assert_eq!(query_pairs.get("colorCode"), Some(&config.color_code));
+        assert_eq!(query_pairs.get("redirectURL"), Some(&redirect_url));
+        assert_eq!(query_pairs.get("lockAmount"), Some(&config.lock_amount));
+        assert_eq!(
+            query_pairs.get("enabledPaymentMethods"),
+            Some(&config.enabled_payment_methods),
+        );
+        assert_eq!(query_pairs.get("walletAddress"), Some(&wallet_address));
+        assert_eq!(query_pairs.get("quoteCurrencyAmount"), Some(&quote_amount),);
+        Ok(())
+    }
+}
diff --git a/libs/sdk-common/src/lib.rs b/libs/sdk-common/src/lib.rs
index 1546d54cd..60051c41a 100644
--- a/libs/sdk-common/src/lib.rs
+++ b/libs/sdk-common/src/lib.rs
@@ -1,4 +1,5 @@
 mod breez_server;
+mod buy;
 mod error;
 mod fiat;
 pub mod grpc;
@@ -24,6 +25,8 @@ pub mod prelude {
     pub use crate::breez_server::*;
     pub use crate::error::*;
     pub use crate::fiat::*;
+    pub use crate::buy::*;
+    pub use crate::buy::moonpay::*;
     pub use crate::input_parser::*;
     pub use crate::invoice::*;
     pub use crate::lnurl::error::*;
diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs
index 1b86867b7..ad021ae71 100644
--- a/libs/sdk-core/src/breez_services.rs
+++ b/libs/sdk-core/src/breez_services.rs
@@ -21,6 +21,7 @@ use tokio::sync::{mpsc, watch, Mutex};
 use tokio::time::{sleep, MissedTickBehavior};
 
 use crate::backup::{BackupRequest, BackupTransport, BackupWatcher};
+use crate::buy::{BuyBitcoinApi, BuyBitcoinService};
 use crate::chain::{
     ChainService, Outspend, RecommendedFees, RedundantChainService, RedundantChainServiceTrait,
     DEFAULT_MEMPOOL_SPACE_URL,
@@ -37,13 +38,11 @@ use crate::models::{
     LspAPI, NodeState, Payment, PaymentDetails, PaymentType, ReverseSwapPairInfo,
     ReverseSwapServiceAPI, SwapInfo, SwapperAPI, INVOICE_PAYMENT_FEE_EXPIRY_SECONDS,
 };
-use crate::moonpay::MoonPayApi;
 use crate::node_api::{CreateInvoiceRequest, NodeAPI};
 use crate::persist::db::SqliteStorage;
 use crate::swap_in::swap::BTCReceiveSwap;
 use crate::swap_out::boltzswap::BoltzApi;
 use crate::swap_out::reverseswap::{BTCSendSwap, CreateReverseSwapArg};
-use crate::BuyBitcoinProvider::Moonpay;
 use crate::*;
 
 pub type BreezServicesResult<T, E = ConnectError> = Result<T, E>;
@@ -148,7 +147,7 @@ pub struct BreezServices {
     node_api: Arc<dyn NodeAPI>,
     lsp_api: Arc<dyn LspAPI>,
     fiat_api: Arc<dyn FiatAPI>,
-    moonpay_api: Arc<dyn MoonPayApi>,
+    buy_bitcoin_api: Arc<dyn BuyBitcoinApi>,
     support_api: Arc<dyn SupportAPI>,
     chain_service: Arc<dyn ChainService>,
     persister: Arc<SqliteStorage>,
@@ -1343,9 +1342,10 @@ impl BreezServices {
                 opening_fee_params: req.opening_fee_params,
             })
             .await?;
-        let url = match req.provider {
-            Moonpay => self.moonpay_api.buy_bitcoin_url(&swap_info).await?,
-        };
+        let url = self
+            .buy_bitcoin_api
+            .buy_bitcoin(req.provider, &swap_info, req.redirect_url)
+            .await?;
 
         Ok(BuyBitcoinResponse {
             url,
@@ -2117,7 +2117,7 @@ struct BreezServicesBuilder {
     reverse_swapper_api: Option<Arc<dyn ReverseSwapperRoutingAPI>>,
     /// Reverse swap functionality on the 3rd party reverse swap service
     reverse_swap_service_api: Option<Arc<dyn ReverseSwapServiceAPI>>,
-    moonpay_api: Option<Arc<dyn MoonPayApi>>,
+    buy_bitcoin_api: Option<Arc<dyn BuyBitcoinApi>>,
 }
 
 #[allow(dead_code)]
@@ -2134,7 +2134,7 @@ impl BreezServicesBuilder {
             swapper_api: None,
             reverse_swapper_api: None,
             reverse_swap_service_api: None,
-            moonpay_api: None,
+            buy_bitcoin_api: None,
             backup_transport: None,
         }
     }
@@ -2154,8 +2154,8 @@ impl BreezServicesBuilder {
         self
     }
 
-    pub fn moonpay_api(&mut self, moonpay_api: Arc<dyn MoonPayApi>) -> &mut Self {
-        self.moonpay_api = Some(moonpay_api.clone());
+    pub fn buy_bitcoin_api(&mut self, buy_bitcoin_api: Arc<dyn BuyBitcoinApi>) -> &mut Self {
+        self.buy_bitcoin_api = Some(buy_bitcoin_api.clone());
         self
     }
 
@@ -2341,6 +2341,11 @@ impl BreezServicesBuilder {
         // create a shutdown channel (sender and receiver)
         let (shutdown_sender, shutdown_receiver) = watch::channel::<()>(());
 
+        let buy_bitcoin_api = self
+            .buy_bitcoin_api
+            .clone()
+            .unwrap_or_else(|| Arc::new(BuyBitcoinService::new(breez_server.clone())));
+
         // Create the node services and it them statically
         let breez_services = Arc::new(BreezServices {
             config: self.config.clone(),
@@ -2355,10 +2360,7 @@ impl BreezServicesBuilder {
                 .support_api
                 .clone()
                 .unwrap_or_else(|| breez_server.clone()),
-            moonpay_api: self
-                .moonpay_api
-                .clone()
-                .unwrap_or_else(|| breez_server.clone()),
+            buy_bitcoin_api,
             chain_service,
             persister: persister.clone(),
             btc_receive_swapper,
@@ -3172,6 +3174,7 @@ pub(crate) mod tests {
             .buy_bitcoin(BuyBitcoinRequest {
                 provider: BuyBitcoinProvider::Moonpay,
                 opening_fee_params: None,
+                redirect_url: None,
             })
             .await?
             .url;
@@ -3214,7 +3217,7 @@ pub(crate) mod tests {
             .lsp_api(Arc::new(MockBreezServer {}))
             .fiat_api(Arc::new(MockBreezServer {}))
             .reverse_swap_service_api(Arc::new(MockReverseSwapperAPI {}))
-            .moonpay_api(Arc::new(MockBreezServer {}))
+            .buy_bitcoin_api(Arc::new(MockBuyBitcoinService {}))
             .persister(persister)
             .node_api(node_api)
             .backup_transport(Arc::new(MockBackupTransport::new()))
diff --git a/libs/sdk-core/src/bridge_generated.io.rs b/libs/sdk-core/src/bridge_generated.io.rs
index 05c3c5579..c11b00b63 100644
--- a/libs/sdk-core/src/bridge_generated.io.rs
+++ b/libs/sdk-core/src/bridge_generated.io.rs
@@ -749,6 +749,7 @@ impl Wire2Api<BuyBitcoinRequest> for wire_BuyBitcoinRequest {
         BuyBitcoinRequest {
             provider: self.provider.wire2api(),
             opening_fee_params: self.opening_fee_params.wire2api(),
+            redirect_url: self.redirect_url.wire2api(),
         }
     }
 }
@@ -1135,6 +1136,7 @@ impl Wire2Api<Vec<u8>> for *mut wire_uint_8_list {
 pub struct wire_BuyBitcoinRequest {
     provider: i32,
     opening_fee_params: *mut wire_OpeningFeeParams,
+    redirect_url: *mut wire_uint_8_list,
 }
 
 #[repr(C)]
@@ -1492,6 +1494,7 @@ impl NewWithNullPtr for wire_BuyBitcoinRequest {
         Self {
             provider: Default::default(),
             opening_fee_params: core::ptr::null_mut(),
+            redirect_url: core::ptr::null_mut(),
         }
     }
 }
diff --git a/libs/sdk-core/src/buy.rs b/libs/sdk-core/src/buy.rs
new file mode 100644
index 000000000..335a04c57
--- /dev/null
+++ b/libs/sdk-core/src/buy.rs
@@ -0,0 +1,51 @@
+use std::sync::Arc;
+
+use anyhow::Result;
+use sdk_common::prelude::{BreezServer, BuyBitcoinProviderApi, MoonpayProvider};
+
+use crate::{BuyBitcoinProvider, SwapInfo};
+
+#[tonic::async_trait]
+pub(crate) trait BuyBitcoinApi: Send + Sync {
+    /// Initiate buying Bitcoin and return a URL to the selected third party provider
+    async fn buy_bitcoin(
+        &self,
+        provider: BuyBitcoinProvider,
+        swap_info: &SwapInfo,
+        redirect_url: Option<String>,
+    ) -> Result<String>;
+}
+
+pub(crate) struct BuyBitcoinService {
+    moonpay_provider: Arc<dyn BuyBitcoinProviderApi>,
+}
+
+impl BuyBitcoinService {
+    pub fn new(breez_server: Arc<BreezServer>) -> Self {
+        let moonpay_provider = Arc::new(MoonpayProvider::new(breez_server));
+        Self { moonpay_provider }
+    }
+}
+
+#[tonic::async_trait]
+impl BuyBitcoinApi for BuyBitcoinService {
+    async fn buy_bitcoin(
+        &self,
+        provider: BuyBitcoinProvider,
+        swap_info: &SwapInfo,
+        redirect_url: Option<String>,
+    ) -> Result<String> {
+        match provider {
+            BuyBitcoinProvider::Moonpay => {
+                self.moonpay_provider
+                    .buy_bitcoin(
+                        swap_info.bitcoin_address.clone(),
+                        None,
+                        Some(swap_info.max_allowed_deposit as u64),
+                        redirect_url,
+                    )
+                    .await
+            }
+        }
+    }
+}
diff --git a/libs/sdk-core/src/lib.rs b/libs/sdk-core/src/lib.rs
index 989768fc7..d51f17fcd 100644
--- a/libs/sdk-core/src/lib.rs
+++ b/libs/sdk-core/src/lib.rs
@@ -172,11 +172,11 @@ mod node_api; // flutter_rust_bridge_codegen: has to be defined before greenligh
 mod greenlight;
 #[rustfmt::skip]
 pub mod lnurl;
+mod buy;
 mod lsp;
 mod lsps0;
 mod lsps2;
 mod models;
-mod moonpay;
 mod persist;
 mod support;
 mod swap_in;
diff --git a/libs/sdk-core/src/models.rs b/libs/sdk-core/src/models.rs
index 153604733..e71d87815 100644
--- a/libs/sdk-core/src/models.rs
+++ b/libs/sdk-core/src/models.rs
@@ -952,6 +952,10 @@ pub struct ReceiveOnchainRequest {
 pub struct BuyBitcoinRequest {
     pub provider: BuyBitcoinProvider,
     pub opening_fee_params: Option<OpeningFeeParams>,
+    /// The optional URL to redirect to after completing the buy.
+    ///
+    /// For Moonpay, see <https://dev.moonpay.com/docs/on-ramp-configure-user-journey-params>
+    pub redirect_url: Option<String>,
 }
 
 #[derive(Clone, Debug, Serialize, Deserialize)]
diff --git a/libs/sdk-core/src/moonpay.rs b/libs/sdk-core/src/moonpay.rs
deleted file mode 100644
index e8213004e..000000000
--- a/libs/sdk-core/src/moonpay.rs
+++ /dev/null
@@ -1,110 +0,0 @@
-use anyhow::Result;
-use reqwest::Url;
-use sdk_common::grpc::SignUrlRequest;
-use sdk_common::prelude::BreezServer;
-
-use crate::SwapInfo;
-
-#[derive(Clone)]
-pub(crate) struct MoonPayConfig {
-    pub base_url: String,
-    pub api_key: String,
-    pub currency_code: String,
-    pub color_code: String,
-    pub redirect_url: String,
-    pub enabled_payment_methods: String,
-}
-
-pub(crate) fn moonpay_config() -> MoonPayConfig {
-    MoonPayConfig {
-        base_url: String::from("https://buy.moonpay.io"),
-        api_key: String::from("pk_live_Mx5g6bpD6Etd7T0bupthv7smoTNn2Vr"),
-        currency_code: String::from("btc"),
-        color_code: String::from("#055DEB"),
-        redirect_url: String::from("https://buy.moonpay.io/transaction_receipt?addFunds=true"),
-        enabled_payment_methods: String::from(
-            "credit_debit_card,sepa_bank_transfer,gbp_bank_transfer",
-        ),
-    }
-}
-
-async fn create_moonpay_url(wallet_address: &str, max_amount: &str) -> Result<Url> {
-    let config = moonpay_config();
-    let url = Url::parse_with_params(
-        &config.base_url,
-        &[
-            ("apiKey", &config.api_key),
-            ("currencyCode", &config.currency_code),
-            ("colorCode", &config.color_code),
-            ("redirectURL", &config.redirect_url),
-            ("enabledPaymentMethods", &config.enabled_payment_methods),
-            ("walletAddress", &wallet_address.to_string()),
-            ("maxQuoteCurrencyAmount", &max_amount.to_string()),
-        ],
-    )?;
-    Ok(url)
-}
-
-#[tonic::async_trait]
-pub(crate) trait MoonPayApi: Send + Sync {
-    async fn buy_bitcoin_url(&self, swap_info: &SwapInfo) -> Result<String>;
-}
-
-#[tonic::async_trait]
-impl MoonPayApi for BreezServer {
-    async fn buy_bitcoin_url(&self, swap_info: &SwapInfo) -> Result<String> {
-        let config = moonpay_config();
-        let url = create_moonpay_url(
-            swap_info.bitcoin_address.as_str(),
-            format!("{:.8}", swap_info.max_allowed_deposit as f64 / 100000000.0).as_str(),
-        )
-        .await?;
-        let mut signer = self.get_signer_client().await;
-        let signed_url = signer
-            .sign_url(SignUrlRequest {
-                base_url: config.base_url.clone(),
-                query_string: format!("?{}", url.query().unwrap()),
-            })
-            .await?
-            .into_inner()
-            .full_url;
-        Ok(signed_url)
-    }
-}
-
-#[cfg(test)]
-pub(crate) mod tests {
-    use std::collections::HashMap;
-
-    use crate::moonpay::{create_moonpay_url, moonpay_config};
-
-    #[tokio::test]
-    async fn test_sign_moonpay_url() -> Result<(), Box<dyn std::error::Error>> {
-        let wallet_address = "a wallet address";
-        let max_amount = "a max amount";
-        let config = moonpay_config();
-
-        let url = create_moonpay_url(wallet_address, max_amount).await?;
-
-        let query_pairs = url.query_pairs().into_owned().collect::<HashMap<_, _>>();
-        assert_eq!(url.host_str(), Some("buy.moonpay.io"));
-        assert_eq!(url.path(), "/");
-        assert_eq!(query_pairs.get("apiKey"), Some(&config.api_key));
-        assert_eq!(query_pairs.get("currencyCode"), Some(&config.currency_code));
-        assert_eq!(query_pairs.get("colorCode"), Some(&config.color_code));
-        assert_eq!(query_pairs.get("redirectURL"), Some(&config.redirect_url));
-        assert_eq!(
-            query_pairs.get("enabledPaymentMethods"),
-            Some(&config.enabled_payment_methods),
-        );
-        assert_eq!(
-            query_pairs.get("walletAddress"),
-            Some(&String::from(wallet_address))
-        );
-        assert_eq!(
-            query_pairs.get("maxQuoteCurrencyAmount"),
-            Some(&String::from(max_amount)),
-        );
-        Ok(())
-    }
-}
diff --git a/libs/sdk-flutter/ios/Classes/bridge_generated.h b/libs/sdk-flutter/ios/Classes/bridge_generated.h
index b022e0a7d..bd79e19cd 100644
--- a/libs/sdk-flutter/ios/Classes/bridge_generated.h
+++ b/libs/sdk-flutter/ios/Classes/bridge_generated.h
@@ -242,6 +242,7 @@ typedef struct wire_ReceiveOnchainRequest {
 typedef struct wire_BuyBitcoinRequest {
   int32_t provider;
   struct wire_OpeningFeeParams *opening_fee_params;
+  struct wire_uint_8_list *redirect_url;
 } wire_BuyBitcoinRequest;
 
 typedef struct wire_RedeemOnchainFundsRequest {
diff --git a/libs/sdk-flutter/lib/bridge_generated.dart b/libs/sdk-flutter/lib/bridge_generated.dart
index 4e3ed0739..f08065468 100644
--- a/libs/sdk-flutter/lib/bridge_generated.dart
+++ b/libs/sdk-flutter/lib/bridge_generated.dart
@@ -443,9 +443,15 @@ class BuyBitcoinRequest {
   final BuyBitcoinProvider provider;
   final OpeningFeeParams? openingFeeParams;
 
+  /// The optional URL to redirect to after completing the buy.
+  ///
+  /// For Moonpay, see <https://dev.moonpay.com/docs/on-ramp-configure-user-journey-params>
+  final String? redirectUrl;
+
   const BuyBitcoinRequest({
     required this.provider,
     this.openingFeeParams,
+    this.redirectUrl,
   });
 }
 
@@ -4760,6 +4766,7 @@ class BreezSdkCorePlatform extends FlutterRustBridgeBase<BreezSdkCoreWire> {
   void _api_fill_to_wire_buy_bitcoin_request(BuyBitcoinRequest apiObj, wire_BuyBitcoinRequest wireObj) {
     wireObj.provider = api2wire_buy_bitcoin_provider(apiObj.provider);
     wireObj.opening_fee_params = api2wire_opt_box_autoadd_opening_fee_params(apiObj.openingFeeParams);
+    wireObj.redirect_url = api2wire_opt_String(apiObj.redirectUrl);
   }
 
   void _api_fill_to_wire_check_message_request(CheckMessageRequest apiObj, wire_CheckMessageRequest wireObj) {
@@ -6767,6 +6774,8 @@ final class wire_BuyBitcoinRequest extends ffi.Struct {
   external int provider;
 
   external ffi.Pointer<wire_OpeningFeeParams> opening_fee_params;
+
+  external ffi.Pointer<wire_uint_8_list> redirect_url;
 }
 
 final class wire_RedeemOnchainFundsRequest extends ffi.Struct {
diff --git a/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt b/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt
index b294890c5..b54bc27d2 100644
--- a/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt
+++ b/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt
@@ -170,9 +170,11 @@ fun asBuyBitcoinRequest(buyBitcoinRequest: ReadableMap): BuyBitcoinRequest? {
         } else {
             null
         }
+    val redirectUrl = if (hasNonNullKey(buyBitcoinRequest, "redirectUrl")) buyBitcoinRequest.getString("redirectUrl") else null
     return BuyBitcoinRequest(
         provider,
         openingFeeParams,
+        redirectUrl,
     )
 }
 
@@ -180,6 +182,7 @@ fun readableMapOf(buyBitcoinRequest: BuyBitcoinRequest): ReadableMap =
     readableMapOf(
         "provider" to buyBitcoinRequest.provider.name.lowercase(),
         "openingFeeParams" to buyBitcoinRequest.openingFeeParams?.let { readableMapOf(it) },
+        "redirectUrl" to buyBitcoinRequest.redirectUrl,
     )
 
 fun asBuyBitcoinRequestList(arr: ReadableArray): List<BuyBitcoinRequest> {
diff --git a/libs/sdk-react-native/ios/BreezSDKMapper.swift b/libs/sdk-react-native/ios/BreezSDKMapper.swift
index aff067630..2d5811806 100644
--- a/libs/sdk-react-native/ios/BreezSDKMapper.swift
+++ b/libs/sdk-react-native/ios/BreezSDKMapper.swift
@@ -192,9 +192,18 @@ enum BreezSDKMapper {
             openingFeeParams = try asOpeningFeeParams(openingFeeParams: openingFeeParamsTmp)
         }
 
+        var redirectUrl: String?
+        if hasNonNilKey(data: buyBitcoinRequest, key: "redirectUrl") {
+            guard let redirectUrlTmp = buyBitcoinRequest["redirectUrl"] as? String else {
+                throw SdkError.Generic(message: errUnexpectedValue(fieldName: "redirectUrl"))
+            }
+            redirectUrl = redirectUrlTmp
+        }
+
         return BuyBitcoinRequest(
             provider: provider,
-            openingFeeParams: openingFeeParams
+            openingFeeParams: openingFeeParams,
+            redirectUrl: redirectUrl
         )
     }
 
@@ -202,6 +211,7 @@ enum BreezSDKMapper {
         return [
             "provider": valueOf(buyBitcoinProvider: buyBitcoinRequest.provider),
             "openingFeeParams": buyBitcoinRequest.openingFeeParams == nil ? nil : dictionaryOf(openingFeeParams: buyBitcoinRequest.openingFeeParams!),
+            "redirectUrl": buyBitcoinRequest.redirectUrl == nil ? nil : buyBitcoinRequest.redirectUrl,
         ]
     }
 
diff --git a/libs/sdk-react-native/src/index.ts b/libs/sdk-react-native/src/index.ts
index 26b4b479c..35b03855b 100644
--- a/libs/sdk-react-native/src/index.ts
+++ b/libs/sdk-react-native/src/index.ts
@@ -44,6 +44,7 @@ export interface BitcoinAddressData {
 export interface BuyBitcoinRequest {
     provider: BuyBitcoinProvider
     openingFeeParams?: OpeningFeeParams
+    redirectUrl?: string
 }
 
 export interface BuyBitcoinResponse {
diff --git a/tools/sdk-cli/Cargo.lock b/tools/sdk-cli/Cargo.lock
index 677ab785b..4871b16cc 100644
--- a/tools/sdk-cli/Cargo.lock
+++ b/tools/sdk-cli/Cargo.lock
@@ -2689,6 +2689,7 @@ dependencies = [
  "thiserror",
  "tonic",
  "tonic-build",
+ "url",
 ]
 
 [[package]]
diff --git a/tools/sdk-cli/src/command_handlers.rs b/tools/sdk-cli/src/command_handlers.rs
index 53175bd07..19a4e4a71 100644
--- a/tools/sdk-cli/src/command_handlers.rs
+++ b/tools/sdk-cli/src/command_handlers.rs
@@ -574,6 +574,7 @@ pub(crate) async fn handle_command(
                 .buy_bitcoin(BuyBitcoinRequest {
                     provider: provider.clone(),
                     opening_fee_params: None,
+                    redirect_url: None,
                 })
                 .await?;
             Ok(format!("Here your {provider:?} url: {}", res.url))


Test patch:
diff --git a/libs/sdk-core/src/test_utils.rs b/libs/sdk-core/src/test_utils.rs
index d5b408816..5d3ae7ab5 100644
--- a/libs/sdk-core/src/test_utils.rs
+++ b/libs/sdk-core/src/test_utils.rs
@@ -28,6 +28,7 @@ use crate::bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, SecretKe
 use crate::bitcoin::util::bip32::{ChildNumber, ExtendedPrivKey};
 use crate::bitcoin::Network;
 use crate::breez_services::{OpenChannelParams, Receiver};
+use crate::buy::BuyBitcoinApi;
 use crate::chain::{ChainService, OnchainTx, Outspend, RecommendedFees, TxStatus};
 use crate::error::{ReceivePaymentError, SdkError, SdkResult};
 use crate::invoice::{InvoiceError, InvoiceResult};
@@ -37,15 +38,14 @@ use crate::lsp::LspInformation;
 use crate::models::{
     LspAPI, NodeState, Payment, ReverseSwapServiceAPI, Swap, SwapperAPI, SyncResponse, TlvEntry,
 };
-use crate::moonpay::MoonPayApi;
 use crate::node_api::{CreateInvoiceRequest, FetchBolt11Result, NodeAPI, NodeError, NodeResult};
 use crate::swap_in::error::SwapResult;
 use crate::swap_in::swap::create_submarine_swap_script;
 use crate::swap_out::boltzswap::{BoltzApiCreateReverseSwapResponse, BoltzApiReverseSwapStatus};
 use crate::swap_out::error::{ReverseSwapError, ReverseSwapResult};
 use crate::{
-    parse_invoice, Config, CustomMessage, LNInvoice, MaxChannelAmount, NodeCredentials,
-    OpeningFeeParamsMenu, PaymentResponse, PrepareRedeemOnchainFundsRequest,
+    parse_invoice, BuyBitcoinProvider, Config, CustomMessage, LNInvoice, MaxChannelAmount,
+    NodeCredentials, OpeningFeeParamsMenu, PaymentResponse, PrepareRedeemOnchainFundsRequest,
     PrepareRedeemOnchainFundsResponse, ReceivePaymentRequest, ReverseSwapPairInfo, RouteHint,
     RouteHintHop, SwapInfo,
 };
@@ -709,9 +709,16 @@ impl FiatAPI for MockBreezServer {
     }
 }
 
+pub struct MockBuyBitcoinService {}
+
 #[tonic::async_trait]
-impl MoonPayApi for MockBreezServer {
-    async fn buy_bitcoin_url(&self, swap_info: &SwapInfo) -> Result<String> {
+impl BuyBitcoinApi for MockBuyBitcoinService {
+    async fn buy_bitcoin(
+        &self,
+        _provider: BuyBitcoinProvider,
+        swap_info: &SwapInfo,
+        _redirect_url: Option<String>,
+    ) -> Result<String> {
         Ok(format!(
             "https://mock.moonpay?wa={}&ma={}",
             swap_info.bitcoin_address.as_str(),