spark_sdk/wallet/internal_handlers/implementations/
ssp.rs

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