spark_sdk/wallet/internal_handlers/implementations/
ssp.rs1use 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 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 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 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 let response = requester
96 .execute_graphql(query, variable_map)
97 .await
98 .map_err(|e| SparkSdkError::GraphQLRequestFailed(e.to_string()))?;
99
100 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 let query = r#"
133 mutation RequestLightningSend($input: RequestLightningSendInput!) {
134 request_lightning_send(input: $input) {
135 payment {
136 id
137 }
138 }
139 }"#;
140
141 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 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 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 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 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 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 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 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 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 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 let coop_exit_txid = connector_tx.input[0].previous_output.txid.clone(); 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 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 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 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 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 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 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}