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 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 let response = requester
77 .execute_graphql(REQUEST_LIGHTNING_RECEIVE_MUTATION, variable_map)
78 .await?;
79
80 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("user_leaves".to_string(), json!(user_leaves));
120
121 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 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 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 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 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 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 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 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 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 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 let response_value = serde_json::to_value(response)
348 .map_err(|_| SparkSdkError::from(NetworkError::InvalidResponse))?;
349
350 let response_struct: SspLightningSendResponse = serde_json::from_value(response_value)
352 .map_err(|_| SparkSdkError::from(NetworkError::InvalidResponse))?;
353
354 let request_id = response_struct.request.request.id;
356
357 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 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 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 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 let mut variable_map = HashMap::new();
419 variable_map.insert("encoded_invoice".to_string(), Value::String(invoice));
420
421 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 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 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 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 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 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}