spark_rust/wallet/internal_handlers/implementations/
ssp.rs

1use crate::{
2    error::{io_error::IoError, network::NetworkError, SparkSdkError},
3    signer::traits::SparkSigner,
4    wallet::{
5        graphql::GraphqlClient,
6        handlers::{cooperative_exit::CoopExitRequestId, fees::SparkFeeEstimate},
7        internal_handlers::traits::ssp::{
8            InitiateCooperativeExitResponse, SspInternalHandlers, SwapLeaf,
9        },
10        utils::{
11            bitcoin::bitcoin_tx_from_bytes,
12            mutations::{
13                COMPLETE_COOP_EXIT_MUTATION, COMPLETE_LEAVES_SWAP_MUTATION,
14                GET_COOP_EXIT_FEE_ESTIMATE_QUERY, GET_LEAVES_SWAP_FEE_ESTIMATE_QUERY,
15                GET_LIGHTNING_RECEIVE_FEE_ESTIMATE_QUERY, GET_LIGHTNING_SEND_FEE_ESTIMATE_QUERY,
16                REQUEST_COOP_EXIT_MUTATION, REQUEST_LEAVES_SWAP_MUTATION,
17                REQUEST_LIGHTNING_RECEIVE_MUTATION, REQUEST_LIGHTNING_SEND_MUTATION,
18            },
19        },
20    },
21    SparkSdk,
22};
23use bitcoin::{Address, Network};
24use serde::Deserialize;
25use serde_json::{json, Value};
26use std::collections::HashMap;
27use tonic::async_trait;
28
29#[derive(Debug, Deserialize)]
30struct SspLightningSendResponse {
31    #[serde(rename = "request_lightning_send")]
32    request: SspLightningSendRequest,
33}
34
35#[derive(Debug, Deserialize)]
36struct SspLightningSendRequest {
37    request: SspLightningSendRequestDetails,
38}
39
40#[derive(Debug, Deserialize)]
41struct SspLightningSendRequestDetails {
42    id: String,
43}
44
45#[async_trait]
46impl<S: SparkSigner + Send + Sync + Clone + 'static> SspInternalHandlers<S> for SparkSdk<S> {
47    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
48    async fn create_invoice_with_ssp(
49        &self,
50        amount_sats: u64,
51        payment_hash: String,
52        expiry_secs: i32,
53        memo: Option<String>,
54        network: Network,
55    ) -> Result<(String, i64), SparkSdkError> {
56        let mut variable_map = HashMap::new();
57        variable_map.insert(
58            "network".to_string(),
59            json!(network.to_string().to_uppercase()),
60        );
61        variable_map.insert("amount_sats".to_string(), json!(amount_sats));
62        variable_map.insert("payment_hash".to_string(), json!(payment_hash));
63        if let Some(memo) = memo {
64            variable_map.insert("memo".to_string(), json!(memo));
65        }
66        variable_map.insert("expiry_secs".to_string(), json!(expiry_secs));
67
68        // Use the custom Requester with the SSP endpoint from config
69        let requester = GraphqlClient::with_base_url(
70            self.get_spark_address()?,
71            Some(self.config.spark_config.ssp_endpoint.clone()),
72        )
73        .map_err(|e| SparkSdkError::from(NetworkError::GraphQL(e.to_string())))?;
74
75        // Execute the GraphQL request
76        let response = requester
77            .execute_graphql(REQUEST_LIGHTNING_RECEIVE_MUTATION, variable_map)
78            .await?;
79
80        // Extract the encoded_payment_request
81        let encoded_invoice = response
82            .get("request_lightning_receive")
83            .and_then(|v| v.get("request"))
84            .and_then(|v| v.get("invoice"))
85            .and_then(|v| v.get("encoded_envoice"))
86            .and_then(|v| v.as_str())
87            .ok_or(SparkSdkError::from(NetworkError::InvalidResponse))?
88            .to_string();
89
90        let fees = response
91            .get("request_lightning_receive")
92            .and_then(|v| v.get("request"))
93            .and_then(|v| v.get("fee"))
94            .and_then(|v| v.get("original_value"))
95            .and_then(|v| v.as_f64())
96            .ok_or(SparkSdkError::from(NetworkError::InvalidResponse))?;
97
98        Ok((encoded_invoice, fees as i64))
99    }
100
101    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
102    async fn request_swap_leaves_with_ssp(
103        &self,
104        adaptor_pubkey: String,
105        total_amount_sats: u64,
106        target_amount_sats: u64,
107        fee_sats: u64,
108        user_leaves: Vec<SwapLeaf>,
109    ) -> Result<(String, Vec<SwapLeaf>), SparkSdkError> {
110        let mut variable_map = HashMap::new();
111        variable_map.insert("adaptor_pubkey".to_string(), json!(adaptor_pubkey));
112        variable_map.insert("total_amount_sats".to_string(), json!(total_amount_sats));
113        variable_map.insert("target_amount_sats".to_string(), json!(target_amount_sats));
114        variable_map.insert("fee_sats".to_string(), json!(fee_sats));
115        // variable_map.insert(
116        //     "network".to_string(),
117        //     json!(network.to_string().to_uppercase()),
118        // );
119        variable_map.insert("user_leaves".to_string(), json!(user_leaves));
120
121        // Use the custom Requester with the SSP endpoint from config
122        let requester = GraphqlClient::with_base_url(
123            self.get_spark_address()?,
124            Some(self.config.spark_config.ssp_endpoint.clone()),
125        )
126        .map_err(|e| SparkSdkError::from(NetworkError::GraphQL(e.to_string())))?;
127
128        let response = requester
129            .execute_graphql(REQUEST_LEAVES_SWAP_MUTATION, variable_map)
130            .await?;
131
132        // Extract the response data
133        let request = response
134            .get("request_leaves_swap")
135            .and_then(|v| v.get("request"))
136            .ok_or(SparkSdkError::from(NetworkError::InvalidResponse))?;
137
138        let request_id = request
139            .get("id")
140            .and_then(|v| v.as_str())
141            .ok_or(SparkSdkError::from(NetworkError::InvalidResponse))?;
142
143        let swap_leaves = request
144            .get("swap_leaves")
145            .and_then(|v| v.as_array())
146            .ok_or(SparkSdkError::from(NetworkError::InvalidResponse))?;
147
148        let mut leaves = Vec::new();
149        for leaf in swap_leaves {
150            let leaf_map = leaf
151                .as_object()
152                .ok_or(SparkSdkError::from(NetworkError::InvalidResponse))?;
153
154            leaves.push(SwapLeaf {
155                leaf_id: leaf_map
156                    .get("leaf_id")
157                    .and_then(|v| v.as_str())
158                    .ok_or(SparkSdkError::from(NetworkError::InvalidResponse))?
159                    .to_string(),
160                raw_unsigned_refund_transaction: leaf_map
161                    .get("raw_unsigned_refund_transaction")
162                    .and_then(|v| v.as_str())
163                    .ok_or(SparkSdkError::from(NetworkError::InvalidResponse))?
164                    .to_string(),
165                adaptor_added_signature: leaf_map
166                    .get("adaptor_signed_signature")
167                    .and_then(|v| v.as_str())
168                    .ok_or(SparkSdkError::from(NetworkError::InvalidResponse))?
169                    .to_string(),
170            });
171        }
172
173        Ok((request_id.to_string(), leaves))
174    }
175
176    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
177    async fn complete_leaves_swap_with_ssp(
178        &self,
179        adaptor_secret_key: String,
180        user_outbound_transfer_external_id: String,
181        leaves_swap_request_id: String,
182    ) -> Result<String, SparkSdkError> {
183        let mut variable_map = HashMap::new();
184        variable_map.insert(
185            "adaptor_secret_key".to_string(),
186            Value::String(adaptor_secret_key),
187        );
188        variable_map.insert(
189            "user_outbound_transfer_external_id".to_string(),
190            Value::String(user_outbound_transfer_external_id),
191        );
192        variable_map.insert(
193            "leaves_swap_request_id".to_string(),
194            Value::String(leaves_swap_request_id),
195        );
196
197        // Use the custom Requester with the SSP endpoint from config
198        let requester = GraphqlClient::with_base_url(
199            self.get_spark_address()?,
200            Some(self.config.spark_config.ssp_endpoint.clone()),
201        )
202        .map_err(|e| SparkSdkError::from(NetworkError::GraphQL(e.to_string())))?;
203
204        let response = requester
205            .execute_graphql(COMPLETE_LEAVES_SWAP_MUTATION, variable_map)
206            .await?;
207
208        // Extract the response data
209        let request = response
210            .get("complete_leaves_swap")
211            .and_then(|v| v.get("request"))
212            .ok_or(SparkSdkError::from(NetworkError::InvalidResponse))?;
213
214        let request_id = request
215            .get("id")
216            .and_then(|v| v.as_str())
217            .ok_or(SparkSdkError::from(NetworkError::InvalidResponse))?;
218
219        Ok(request_id.to_string())
220    }
221
222    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
223    async fn initiate_cooperative_exit_with_ssp(
224        &self,
225        leaf_external_ids: Vec<String>,
226        address: &Address,
227    ) -> Result<InitiateCooperativeExitResponse, SparkSdkError> {
228        let mut variable_map = HashMap::new();
229        variable_map.insert(
230            "leaf_external_ids".to_string(),
231            Value::Array(leaf_external_ids.into_iter().map(Value::String).collect()),
232        );
233        variable_map.insert(
234            "withdrawal_address".to_string(),
235            Value::String(address.to_string()),
236        );
237
238        // Use the custom Requester with the SSP endpoint from config
239        let requester = GraphqlClient::with_base_url(
240            self.get_spark_address()?,
241            Some(self.config.spark_config.ssp_endpoint.clone()),
242        )
243        .map_err(|e| SparkSdkError::from(NetworkError::GraphQL(e.to_string())))?;
244
245        let response = requester
246            .execute_graphql(REQUEST_COOP_EXIT_MUTATION, variable_map)
247            .await?;
248
249        // Extract the response data
250        let request = response
251            .get("request_coop_exit")
252            .and_then(|v| v.get("request"))
253            .ok_or(SparkSdkError::from(NetworkError::InvalidResponse))?;
254
255        let request_id = request
256            .get("id")
257            .and_then(|v| v.as_str())
258            .ok_or(SparkSdkError::from(NetworkError::InvalidResponse))?;
259
260        let raw_connector_transaction = request
261            .get("raw_connector_transaction")
262            .and_then(|v| v.as_str())
263            .ok_or(SparkSdkError::from(NetworkError::InvalidResponse))?;
264
265        // Decode and deserialize the connector transaction
266        let connector_tx = bitcoin_tx_from_bytes(
267            &hex::decode(raw_connector_transaction)
268                .map_err(|err| SparkSdkError::from(IoError::Decoding(err)))?,
269        )?;
270
271        let response = InitiateCooperativeExitResponse {
272            request_id: CoopExitRequestId(
273                request_id
274                    .split(':')
275                    .nth(1)
276                    .unwrap_or(request_id)
277                    .to_string(),
278            ),
279            connector_tx,
280        };
281
282        #[cfg(feature = "telemetry")]
283        tracing::info!("Initiated cooperative exit with SSP");
284
285        Ok(response)
286    }
287
288    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
289    async fn complete_cooperative_exit_with_ssp(
290        &self,
291        user_outbound_transfer_external_id: String,
292        coop_exit_request_id: CoopExitRequestId,
293    ) -> Result<CoopExitRequestId, SparkSdkError> {
294        let mut variable_map = HashMap::new();
295        variable_map.insert(
296            "coop_exit_request_id".to_string(),
297            Value::String(coop_exit_request_id.0),
298        );
299        variable_map.insert(
300            "user_outbound_transfer_external_id".to_string(),
301            Value::String(user_outbound_transfer_external_id),
302        );
303
304        // Use the custom Requester with the SSP endpoint from config
305        let requester = GraphqlClient::with_base_url(
306            self.get_spark_address()?,
307            Some(self.config.spark_config.ssp_endpoint.clone()),
308        )
309        .map_err(|e| SparkSdkError::from(NetworkError::GraphQL(e.to_string())))?;
310        let response = requester
311            .execute_graphql(COMPLETE_COOP_EXIT_MUTATION, variable_map)
312            .await?;
313
314        // Extract the request ID from the response
315        let request_id = response
316            .get("complete_coop_exit")
317            .and_then(|v| v.get("request"))
318            .and_then(|v| v.get("id"))
319            .and_then(|v| v.as_str())
320            .ok_or(SparkSdkError::from(NetworkError::InvalidResponse))?;
321
322        Ok(CoopExitRequestId(request_id.to_string()))
323    }
324
325    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
326    async fn request_lightning_send_with_ssp(
327        &self,
328        encoded_invoice: String,
329        idempotency_key: String,
330    ) -> Result<String, SparkSdkError> {
331        let mut variable_map = HashMap::new();
332        variable_map.insert("encoded_invoice".to_string(), json!(encoded_invoice));
333        variable_map.insert("idempotency_key".to_string(), json!(idempotency_key));
334
335        // Use the custom Requester with the SSP endpoint from config
336        let requester = GraphqlClient::with_base_url(
337            self.get_spark_address()?,
338            Some(self.config.spark_config.ssp_endpoint.clone()),
339        )
340        .map_err(|e| SparkSdkError::from(NetworkError::GraphQL(e.to_string())))?;
341
342        let response = requester
343            .execute_graphql(REQUEST_LIGHTNING_SEND_MUTATION, variable_map)
344            .await?;
345
346        // Convert the HashMap to a Value before deserializing
347        let response_value = serde_json::to_value(response)
348            .map_err(|_| SparkSdkError::from(NetworkError::InvalidResponse))?;
349
350        // Parse the response into the SspLightningSendResponse struct
351        let response_struct: SspLightningSendResponse = serde_json::from_value(response_value)
352            .map_err(|_| SparkSdkError::from(NetworkError::InvalidResponse))?;
353
354        // Extract the request ID from the response struct
355        let request_id = response_struct.request.request.id;
356
357        // Extract just the UUID part if it has the prefix
358        let uuid_only = if request_id.contains(":") {
359            request_id
360                .split(':')
361                .next_back()
362                .unwrap_or(&request_id)
363                .to_string()
364        } else {
365            request_id
366        };
367
368        Ok(uuid_only)
369    }
370
371    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
372    async fn get_lightning_receive_fee_estimate_with_ssp(
373        &self,
374        amount_sats: u64,
375    ) -> Result<SparkFeeEstimate, SparkSdkError> {
376        let network = self
377            .get_network()
378            .to_bitcoin_network()
379            .to_string()
380            .to_uppercase();
381
382        // Create variables map with the network and amount
383        let mut variable_map = HashMap::new();
384        variable_map.insert("network".to_string(), Value::String(network));
385        variable_map.insert(
386            "amount_sats".to_string(),
387            Value::Number(serde_json::Number::from(amount_sats)),
388        );
389
390        // Use the custom Requester with the SSP endpoint from config
391        let requester = GraphqlClient::with_base_url(
392            self.get_spark_address()?,
393            Some(self.config.spark_config.ssp_endpoint.clone()),
394        )
395        .map_err(|e| SparkSdkError::from(NetworkError::GraphQL(e.to_string())))?;
396
397        let response = requester
398            .execute_graphql(GET_LIGHTNING_RECEIVE_FEE_ESTIMATE_QUERY, variable_map)
399            .await?;
400
401        // Extract the fee estimate from the response
402        let fee_value = response
403            .get("lightning_receive_fee_estimate")
404            .and_then(|v| v.get("fee_estimate"))
405            .and_then(|v| v.get("original_value"))
406            .and_then(|v| v.as_u64())
407            .ok_or(SparkSdkError::from(NetworkError::InvalidResponse))?;
408
409        Ok(SparkFeeEstimate { fees: fee_value })
410    }
411
412    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
413    async fn get_lightning_send_fee_estimate_with_ssp(
414        &self,
415        invoice: String,
416    ) -> Result<SparkFeeEstimate, SparkSdkError> {
417        // Create variables map with the invoice
418        let mut variable_map = HashMap::new();
419        variable_map.insert("encoded_invoice".to_string(), Value::String(invoice));
420
421        // Use the custom Requester with the SSP endpoint from config
422        let requester = GraphqlClient::with_base_url(
423            self.get_spark_address()?,
424            Some(self.config.spark_config.ssp_endpoint.clone()),
425        )
426        .map_err(|e| SparkSdkError::from(NetworkError::GraphQL(e.to_string())))?;
427
428        let response = requester
429            .execute_graphql(GET_LIGHTNING_SEND_FEE_ESTIMATE_QUERY, variable_map)
430            .await?;
431
432        // Extract the fee estimate from the response
433        let fee_value = response
434            .get("lightning_send_fee_estimate")
435            .and_then(|v| v.get("fee_estimate"))
436            .and_then(|v| v.get("original_value"))
437            .and_then(|v| v.as_u64())
438            .ok_or(SparkSdkError::from(NetworkError::InvalidResponse))?;
439
440        Ok(SparkFeeEstimate { fees: fee_value })
441    }
442
443    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
444    async fn get_cooperative_exit_fee_estimate_with_ssp(
445        &self,
446        leaf_external_ids: Vec<String>,
447        on_chain_address: String,
448    ) -> Result<SparkFeeEstimate, SparkSdkError> {
449        let mut variable_map = HashMap::new();
450        variable_map.insert(
451            "leaf_external_ids".to_string(),
452            Value::Array(leaf_external_ids.into_iter().map(Value::String).collect()),
453        );
454        variable_map.insert(
455            "withdrawal_address".to_string(),
456            Value::String(on_chain_address),
457        );
458
459        // Use the custom Requester with the SSP endpoint from config
460        let requester = GraphqlClient::with_base_url(
461            self.get_spark_address()?,
462            Some(self.config.spark_config.ssp_endpoint.clone()),
463        )
464        .map_err(|e| SparkSdkError::from(NetworkError::GraphQL(e.to_string())))?;
465
466        let response = requester
467            .execute_graphql(GET_COOP_EXIT_FEE_ESTIMATE_QUERY, variable_map)
468            .await?;
469
470        // Extract the fee estimate from the response
471        let fee_value = response
472            .get("coop_exit_fee_estimate")
473            .and_then(|v| v.get("fee_estimate"))
474            .and_then(|v| v.get("original_value"))
475            .and_then(|v| v.as_u64())
476            .ok_or(SparkSdkError::from(NetworkError::InvalidResponse))?;
477
478        Ok(SparkFeeEstimate { fees: fee_value })
479    }
480
481    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
482    async fn get_leaves_swap_fee_estimate_with_ssp(
483        &self,
484        total_amount_sats: u64,
485    ) -> Result<SparkFeeEstimate, SparkSdkError> {
486        let mut variable_map = HashMap::new();
487        variable_map.insert(
488            "total_amount_sats".to_string(),
489            Value::Number(total_amount_sats.into()),
490        );
491
492        // Use the custom Requester with the SSP endpoint from config
493        let requester = GraphqlClient::with_base_url(
494            self.get_spark_address()?,
495            Some(self.config.spark_config.ssp_endpoint.clone()),
496        )
497        .map_err(|e| SparkSdkError::from(NetworkError::GraphQL(e.to_string())))?;
498
499        let response = requester
500            .execute_graphql(GET_LEAVES_SWAP_FEE_ESTIMATE_QUERY, variable_map)
501            .await?;
502
503        // Extract the fee estimate from the response
504        let fee_value = response
505            .get("leaves_swap_fee_estimate")
506            .and_then(|v| v.get("fee_estimate"))
507            .and_then(|v| v.get("original_value"))
508            .and_then(|v| v.as_u64())
509            .ok_or(SparkSdkError::from(NetworkError::InvalidResponse))?;
510
511        Ok(SparkFeeEstimate { fees: fee_value })
512    }
513}