1use crate::models::GetInfoResponse;
4use crate::models::GetSubscriptionResponse;
5use crate::models::IndexerAsset;
6use crate::models::IndexerVtxo;
7use bitcoin::base64;
8use bitcoin::base64::Engine;
9use bitcoin::hex::FromHex;
10use bitcoin::secp256k1::PublicKey;
11use bitcoin::Amount;
12use bitcoin::OutPoint;
13use bitcoin::Psbt;
14use bitcoin::ScriptBuf;
15use bitcoin::Transaction;
16use bitcoin::Txid;
17use std::collections::HashMap;
18use std::error::Error as StdError;
19use std::str::FromStr;
20
21pub mod stream;
22
23#[derive(Debug)]
24pub struct ConversionError(pub String);
25
26impl std::fmt::Display for ConversionError {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 write!(f, "Conversion error: {}", self.0)
29 }
30}
31
32impl StdError for ConversionError {}
33
34impl TryFrom<crate::models::IntentFeeInfo> for ark_core::server::IntentFeeInfo {
35 type Error = ConversionError;
36
37 fn try_from(value: crate::models::IntentFeeInfo) -> Result<Self, Self::Error> {
38 Ok(ark_core::server::IntentFeeInfo {
39 offchain_input: value.offchain_input,
40 offchain_output: value.offchain_output,
41 onchain_input: value.onchain_input,
42 onchain_output: value.onchain_output,
43 })
44 }
45}
46
47impl TryFrom<crate::models::FeeInfo> for ark_core::server::FeeInfo {
48 type Error = ConversionError;
49
50 fn try_from(value: crate::models::FeeInfo) -> Result<Self, Self::Error> {
51 let intent_fee = value
52 .intent_fee
53 .map(ark_core::server::IntentFeeInfo::try_from)
54 .transpose()?
55 .unwrap_or_default();
56
57 let tx_fee_rate = value.tx_fee_rate.unwrap_or_default();
58
59 Ok(ark_core::server::FeeInfo {
60 intent_fee,
61 tx_fee_rate,
62 })
63 }
64}
65
66impl TryFrom<crate::models::ScheduledSession> for ark_core::server::ScheduledSession {
67 type Error = ConversionError;
68
69 fn try_from(value: crate::models::ScheduledSession) -> Result<Self, Self::Error> {
70 let next_start_time_str = value
71 .next_start_time
72 .ok_or_else(|| ConversionError("Missing next_start_time".to_string()))?;
73 let next_start_time = i64::from_str(&next_start_time_str)
74 .map_err(|e| ConversionError(format!("Could not parse next_start_time: {e:#}")))?;
75
76 let next_end_time_str = value
77 .next_end_time
78 .ok_or_else(|| ConversionError("Missing next_end_time".to_string()))?;
79 let next_end_time = i64::from_str(&next_end_time_str)
80 .map_err(|e| ConversionError(format!("Could not parse next_end_time: {e:#}")))?;
81
82 let period_str = value
83 .period
84 .ok_or_else(|| ConversionError("Missing period".to_string()))?;
85 let period = i64::from_str(&period_str)
86 .map_err(|e| ConversionError(format!("Could not parse period: {e:#}")))?;
87
88 let duration_str = value
89 .duration
90 .ok_or_else(|| ConversionError("Missing duration".to_string()))?;
91 let duration = i64::from_str(&duration_str)
92 .map_err(|e| ConversionError(format!("Could not parse duration: {e:#}")))?;
93
94 let fees = value
95 .fees
96 .map(ark_core::server::FeeInfo::try_from)
97 .transpose()?;
98
99 Ok(ark_core::server::ScheduledSession {
100 next_start_time,
101 next_end_time,
102 period,
103 duration,
104 fees,
105 })
106 }
107}
108
109impl TryFrom<crate::models::DeprecatedSigner> for ark_core::server::DeprecatedSigner {
110 type Error = ConversionError;
111
112 fn try_from(value: crate::models::DeprecatedSigner) -> Result<Self, Self::Error> {
113 let pubkey_str = value
114 .pubkey
115 .ok_or_else(|| ConversionError("Missing pubkey in deprecated signer".to_string()))?;
116 let pk = pubkey_str
117 .parse::<PublicKey>()
118 .map_err(|e| ConversionError(format!("Invalid pubkey '{pubkey_str}': {e}")))?;
119
120 let cutoff_date = match value.cutoff_date {
122 Some(s) => i64::from_str(&s)
123 .map_err(|e| ConversionError(format!("Could not parse cutoff_date: {e:#}")))?,
124 None => 0,
125 };
126
127 Ok(ark_core::server::DeprecatedSigner { pk, cutoff_date })
128 }
129}
130
131impl TryFrom<GetInfoResponse> for ark_core::server::Info {
132 type Error = ConversionError;
133
134 fn try_from(response: GetInfoResponse) -> Result<Self, Self::Error> {
135 let signer_pubkey_str = response
137 .signer_pubkey
138 .ok_or_else(|| ConversionError("Missing signer_pubkey".to_string()))?;
139 let signer_pk = signer_pubkey_str.parse::<PublicKey>().map_err(|e| {
140 ConversionError(format!("Invalid signer_pubkey '{signer_pubkey_str}': {e}"))
141 })?;
142
143 let forfeit_pubkey_str = response
145 .forfeit_pubkey
146 .ok_or_else(|| ConversionError("Missing forfeit_pubkey".to_string()))?;
147 let forfeit_pk = forfeit_pubkey_str.parse::<PublicKey>().map_err(|e| {
148 ConversionError(format!(
149 "Invalid forfeit_pubkey '{forfeit_pubkey_str}': {e}"
150 ))
151 })?;
152
153 let checkpoint_tapscript_str = response
155 .checkpoint_tapscript
156 .ok_or_else(|| ConversionError("Missing checkpoint_tapscript".to_string()))?;
157 let checkpoint_tapscript = ScriptBuf::from_hex(&checkpoint_tapscript_str).map_err(|e| {
158 ConversionError(format!(
159 "Invalid checkpoint_tapscript hex '{checkpoint_tapscript_str}': {e}"
160 ))
161 })?;
162
163 let unilateral_exit_delay_str = response
165 .unilateral_exit_delay
166 .ok_or_else(|| ConversionError("Missing unilateral_exit_delay".to_string()))?;
167 let unilateral_exit_delay_val = i64::from_str(&unilateral_exit_delay_str).map_err(|e| {
168 ConversionError(format!("Could not parse unilateral_exit_delay: {e:#}"))
169 })?;
170 let unilateral_exit_delay = parse_sequence_number(unilateral_exit_delay_val)?;
171
172 let boarding_exit_delay_str = response
174 .boarding_exit_delay
175 .ok_or_else(|| ConversionError("Missing boarding_exit_delay".to_string()))?;
176 let boarding_exit_delay_val = i64::from_str(&boarding_exit_delay_str)
177 .map_err(|e| ConversionError(format!("Could not parse boarding_exit_delay: {e:#}")))?;
178 let boarding_exit_delay = parse_sequence_number(boarding_exit_delay_val)?;
179
180 let network_str = response
182 .network
183 .ok_or_else(|| ConversionError("Missing network".to_string()))?;
184 let network = ark_core::server::Network::from_str(&network_str)
185 .map_err(|e| ConversionError(format!("Invalid network '{network_str}': {e}")))?;
186 let network = bitcoin::Network::from(network);
187
188 let session_duration_str = response
190 .session_duration
191 .ok_or_else(|| ConversionError("Missing session_duration".to_string()))?;
192 let session_duration = i64::from_str(&session_duration_str)
193 .map_err(|e| ConversionError(format!("Could not parse session_duration: {e:#}")))?
194 as u64;
195
196 let dust_str = response
198 .dust
199 .ok_or_else(|| ConversionError("Missing dust".to_string()))?;
200 let dust_val = i64::from_str(&dust_str)
201 .map_err(|e| ConversionError(format!("Could not parse dust: {e:#}")))?;
202 let dust = Amount::from_sat(dust_val as u64);
203
204 let forfeit_address_str = response
206 .forfeit_address
207 .ok_or_else(|| ConversionError("Missing forfeit_address".to_string()))?;
208 let forfeit_address = forfeit_address_str
209 .parse::<bitcoin::Address<bitcoin::address::NetworkUnchecked>>()
210 .map_err(|e| {
211 ConversionError(format!(
212 "Invalid forfeit_address '{forfeit_address_str}': {e}"
213 ))
214 })?
215 .require_network(network)
216 .map_err(|e| {
217 ConversionError(format!(
218 "Address network mismatch for '{forfeit_address_str}': {e}"
219 ))
220 })?;
221
222 let version = response
224 .version
225 .ok_or_else(|| ConversionError("Missing version".to_string()))?;
226
227 let digest = response.digest.unwrap_or_default();
229
230 let utxo_min_amount = response
232 .utxo_min_amount
233 .and_then(|s| i64::from_str(&s).ok())
234 .and_then(|val| {
235 if val >= 0 {
236 Some(Amount::from_sat(val as u64))
237 } else {
238 None
239 }
240 });
241
242 let utxo_max_amount = response
243 .utxo_max_amount
244 .and_then(|s| i64::from_str(&s).ok())
245 .and_then(|val| {
246 if val >= 0 {
247 Some(Amount::from_sat(val as u64))
248 } else {
249 None
250 }
251 });
252
253 let vtxo_min_amount = response
254 .vtxo_min_amount
255 .and_then(|s| i64::from_str(&s).ok())
256 .and_then(|val| {
257 if val >= 0 {
258 Some(Amount::from_sat(val as u64))
259 } else {
260 None
261 }
262 });
263
264 let vtxo_max_amount = response
265 .vtxo_max_amount
266 .and_then(|s| i64::from_str(&s).ok())
267 .and_then(|val| {
268 if val >= 0 {
269 Some(Amount::from_sat(val as u64))
270 } else {
271 None
272 }
273 });
274
275 let fees = response
277 .fees
278 .map(ark_core::server::FeeInfo::try_from)
279 .transpose()?;
280
281 let scheduled_session = response
283 .scheduled_session
284 .map(ark_core::server::ScheduledSession::try_from)
285 .transpose()?;
286
287 let deprecated_signers = response
289 .deprecated_signers
290 .unwrap_or_default()
291 .into_iter()
292 .map(ark_core::server::DeprecatedSigner::try_from)
293 .collect::<Result<Vec<_>, _>>()?;
294
295 let service_status = response.service_status.unwrap_or_default();
297
298 let max_tx_weight_str = response
299 .max_tx_weight
300 .ok_or_else(|| ConversionError("Missing max_tx_weight".to_string()))?;
301 let max_tx_weight = i64::from_str(&max_tx_weight_str)
302 .map_err(|e| ConversionError(format!("Could not parse max_tx_weight: {e:#}")))?;
303
304 let max_op_return_outputs_str = response
305 .max_op_return_outputs
306 .ok_or_else(|| ConversionError("Missing max_op_return_outputs".to_string()))?;
307 let max_op_return_outputs = i64::from_str(&max_op_return_outputs_str).map_err(|e| {
308 ConversionError(format!("Could not parse max_op_return_outputs: {e:#}"))
309 })?;
310
311 Ok(ark_core::server::Info {
312 version,
313 signer_pk,
314 forfeit_pk,
315 forfeit_address,
316 checkpoint_tapscript,
317 network,
318 session_duration,
319 unilateral_exit_delay,
320 boarding_exit_delay,
321 utxo_min_amount,
322 utxo_max_amount,
323 vtxo_min_amount,
324 vtxo_max_amount,
325 dust,
326 fees,
327 scheduled_session,
328 deprecated_signers,
329 service_status,
330 digest,
331 max_tx_weight,
332 max_op_return_outputs,
333 })
334 }
335}
336
337impl TryFrom<IndexerVtxo> for ark_core::server::VirtualTxOutPoint {
338 type Error = ConversionError;
339
340 fn try_from(value: IndexerVtxo) -> Result<Self, Self::Error> {
341 let outpoint_data = value
343 .outpoint
344 .ok_or_else(|| ConversionError("Missing outpoint".to_string()))?;
345
346 let txid_str = outpoint_data
347 .txid
348 .ok_or_else(|| ConversionError("Missing outpoint txid".to_string()))?;
349 let txid = txid_str
350 .parse::<Txid>()
351 .map_err(|e| ConversionError(format!("Invalid outpoint txid '{txid_str}': {e}")))?;
352
353 let vout = outpoint_data
354 .vout
355 .ok_or_else(|| ConversionError("Missing outpoint vout".to_string()))?;
356 let vout = vout as u32; let outpoint = OutPoint { txid, vout };
359
360 let created_at_str = value
362 .created_at
363 .ok_or_else(|| ConversionError("Missing created_at".to_string()))?;
364 let created_at = i64::from_str(&created_at_str)
365 .map_err(|e| ConversionError(format!("Could not parse created_at: {e:#}")))?;
366
367 let expires_at_str = value
368 .expires_at
369 .ok_or_else(|| ConversionError("Missing expires_at".to_string()))?;
370 let expires_at = i64::from_str(&expires_at_str)
371 .map_err(|e| ConversionError(format!("Could not parse expires_at: {e:#}")))?;
372
373 let amount_str = value
375 .amount
376 .ok_or_else(|| ConversionError("Missing amount".to_string()))?;
377 let amount_val = u64::from_str(&amount_str)
378 .map_err(|e| ConversionError(format!("Could not parse amount: {e:#}")))?;
379 let amount = Amount::from_sat(amount_val);
380
381 let script_str = value
383 .script
384 .ok_or_else(|| ConversionError("Missing script".to_string()))?;
385 let script = ScriptBuf::from_hex(&script_str)
386 .map_err(|e| ConversionError(format!("Invalid script hex '{script_str}': {e}")))?;
387
388 let spent_by = value
390 .spent_by
391 .filter(|s| !s.is_empty())
392 .map(|s| s.parse::<Txid>())
393 .transpose()
394 .map_err(|e| ConversionError(format!("Invalid spent_by txid: {e}")))?;
395
396 let commitment_txids = value
398 .commitment_txids
399 .unwrap_or_default()
400 .into_iter()
401 .map(|s| s.parse::<Txid>())
402 .collect::<Result<Vec<_>, _>>()
403 .map_err(|e| ConversionError(format!("Invalid commitment_txid: {e}")))?;
404
405 let settled_by = value
407 .settled_by
408 .filter(|s| !s.is_empty())
409 .map(|s| s.parse::<Txid>())
410 .transpose()
411 .map_err(|e| ConversionError(format!("Invalid settled_by txid: {e}")))?;
412
413 let ark_txid = value
415 .ark_txid
416 .filter(|s| !s.is_empty())
417 .map(|s| s.parse::<Txid>())
418 .transpose()
419 .map_err(|e| ConversionError(format!("Invalid ark_txid: {e}")))?;
420
421 let assets = value
422 .assets
423 .unwrap_or_default()
424 .into_iter()
425 .filter_map(|a| match a {
426 IndexerAsset {
427 amount: Some(amount),
428 asset_id: Some(asset_id),
429 } => {
430 let asset_id = match asset_id.parse() {
431 Ok(asset_id) => asset_id,
432 Err(e) => {
433 return Some(Err(ConversionError(format!("Invalid asset ID: {e}"))));
434 }
435 };
436
437 Some(Ok(ark_core::server::Asset {
438 asset_id,
439 amount: amount as u64,
440 }))
441 }
442 _ => None,
443 })
444 .collect::<Result<Vec<_>, _>>()?;
445
446 Ok(ark_core::server::VirtualTxOutPoint {
447 outpoint,
448 created_at,
449 expires_at,
450 amount,
451 script,
452 is_preconfirmed: value.is_preconfirmed.unwrap_or(false),
453 is_swept: value.is_swept.unwrap_or(false),
454 is_unrolled: value.is_unrolled.unwrap_or(false),
455 is_spent: value.is_spent.unwrap_or(false),
456 spent_by,
457 commitment_txids,
458 settled_by,
459 ark_txid,
460 assets,
461 })
462 }
463}
464
465fn parse_sequence_number(value: i64) -> Result<bitcoin::Sequence, ConversionError> {
466 const ARBITRARY_SEQUENCE_THRESHOLD: i64 = 512;
472
473 let sequence = if value.is_negative() {
474 return Err(ConversionError(format!("invalid sequence number: {value}")));
475 } else if value < ARBITRARY_SEQUENCE_THRESHOLD {
476 bitcoin::Sequence::from_height(value as u16)
477 } else {
478 bitcoin::Sequence::from_seconds_ceil(value as u32)
479 .map_err(|e| ConversionError(format!("Failed parsing sequence number: {e}")))?
480 };
481
482 Ok(sequence)
483}
484
485impl TryFrom<crate::models::IndexerSubscriptionEvent> for ark_core::server::SubscriptionEvent {
486 type Error = ConversionError;
487
488 fn try_from(event: crate::models::IndexerSubscriptionEvent) -> Result<Self, Self::Error> {
489 let txid_str = event
491 .txid
492 .ok_or_else(|| ConversionError("Missing txid in subscription event".to_string()))?;
493 let txid = txid_str
494 .parse::<Txid>()
495 .map_err(|e| ConversionError(format!("Invalid txid '{txid_str}': {e}")))?;
496
497 let scripts = event
499 .scripts
500 .unwrap_or_default()
501 .iter()
502 .map(|h| {
503 ScriptBuf::from_hex(h)
504 .map_err(|e| ConversionError(format!("Invalid script hex: {e}")))
505 })
506 .collect::<Result<Vec<_>, _>>()?;
507
508 let new_vtxos = event
510 .new_vtxos
511 .unwrap_or_default()
512 .into_iter()
513 .map(ark_core::server::VirtualTxOutPoint::try_from)
514 .collect::<Result<Vec<_>, _>>()
515 .map_err(|e| ConversionError(format!("Invalid new_vtxos: {e}")))?;
516
517 let spent_vtxos = event
519 .spent_vtxos
520 .unwrap_or_default()
521 .into_iter()
522 .map(ark_core::server::VirtualTxOutPoint::try_from)
523 .collect::<Result<Vec<_>, _>>()
524 .map_err(|e| ConversionError(format!("Invalid spent_vtxos: {e}")))?;
525
526 let tx = if let Some(tx_str) = event.tx.filter(|s| !s.is_empty()) {
528 match Vec::from_hex(&tx_str)
529 .ok()
530 .and_then(|bytes| bitcoin::consensus::deserialize::<Transaction>(&bytes).ok())
531 {
532 Some(raw_tx) => Some(raw_tx),
533 None => {
534 let base64 = base64::engine::GeneralPurpose::new(
535 &base64::alphabet::STANDARD,
536 base64::engine::GeneralPurposeConfig::new(),
537 );
538 let bytes = base64
539 .decode(&tx_str)
540 .map_err(|e| ConversionError(format!("Invalid tx payload: {e}")))?;
541 let psbt = Psbt::deserialize(&bytes)
542 .map_err(|e| ConversionError(format!("Invalid tx psbt: {e}")))?;
543 Some(psbt.unsigned_tx)
544 }
545 }
546 } else {
547 None
548 };
549
550 let checkpoint_txs = event
552 .checkpoint_txs
553 .unwrap_or_default()
554 .into_iter()
555 .map(|(k, v)| {
556 let out_point = OutPoint::from_str(&k)
557 .map_err(|e| ConversionError(format!("Invalid checkpoint outpoint: {e}")))?;
558 let txid_str = v
559 .txid
560 .ok_or_else(|| ConversionError("Missing checkpoint txid".to_string()))?;
561 let txid = txid_str
562 .parse::<Txid>()
563 .map_err(|e| ConversionError(format!("Invalid checkpoint txid: {e}")))?;
564 Ok((out_point, txid))
565 })
566 .collect::<Result<HashMap<_, _>, ConversionError>>()?;
567
568 Ok(ark_core::server::SubscriptionEvent {
569 txid,
570 scripts,
571 new_vtxos,
572 spent_vtxos,
573 tx,
574 checkpoint_txs,
575 })
576 }
577}
578
579impl TryFrom<GetSubscriptionResponse> for ark_core::server::SubscriptionResponse {
580 type Error = ConversionError;
581
582 fn try_from(value: GetSubscriptionResponse) -> Result<Self, Self::Error> {
583 if value.heartbeat.is_some() {
585 Ok(ark_core::server::SubscriptionResponse::Heartbeat)
586 } else if let Some(event) = value.event {
587 let subscription_event = ark_core::server::SubscriptionEvent::try_from(event)?;
588 Ok(ark_core::server::SubscriptionResponse::Event(Box::new(
589 subscription_event,
590 )))
591 } else {
592 Err(ConversionError(
593 "GetSubscriptionResponse must have either event or heartbeat".to_string(),
594 ))
595 }
596 }
597}
598
599#[cfg(test)]
600mod tests {
601 use crate::models::DeprecatedSigner as ModelDeprecatedSigner;
602
603 fn model(pubkey: Option<&str>, cutoff_date: Option<&str>) -> ModelDeprecatedSigner {
604 ModelDeprecatedSigner {
605 pubkey: pubkey.map(str::to_string),
606 cutoff_date: cutoff_date.map(str::to_string),
607 }
608 }
609
610 const PK_A: &str = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
611
612 #[test]
615 fn deprecated_signer_parses_cutoff_date() {
616 let result = ark_core::server::DeprecatedSigner::try_from(model(Some(PK_A), Some("12345")));
617 let ds = result.expect("should succeed");
618 assert_eq!(ds.cutoff_date, 12345);
619 assert_eq!(ds.pk.to_string(), PK_A);
620 }
621
622 #[test]
623 fn deprecated_signer_missing_cutoff_defaults_to_zero() {
624 let result = ark_core::server::DeprecatedSigner::try_from(model(Some(PK_A), None));
626 let ds = result.expect("should succeed");
627 assert_eq!(ds.cutoff_date, 0);
628 }
629
630 #[test]
631 fn deprecated_signer_missing_pubkey_returns_error() {
632 let result = ark_core::server::DeprecatedSigner::try_from(model(None, Some("100")));
633 assert!(result.is_err());
634 let msg = result.unwrap_err().to_string();
635 assert!(msg.contains("Missing pubkey"), "unexpected: {msg}");
636 }
637
638 #[test]
639 fn deprecated_signer_invalid_pubkey_returns_error() {
640 let result =
641 ark_core::server::DeprecatedSigner::try_from(model(Some("notahex"), Some("100")));
642 assert!(result.is_err());
643 let msg = result.unwrap_err().to_string();
644 assert!(msg.contains("Invalid pubkey"), "unexpected: {msg}");
645 }
646
647 #[test]
648 fn deprecated_signer_invalid_cutoff_date_returns_error() {
649 let result =
650 ark_core::server::DeprecatedSigner::try_from(model(Some(PK_A), Some("not-a-number")));
651 assert!(result.is_err());
652 let msg = result.unwrap_err().to_string();
653 assert!(msg.contains("cutoff_date"), "unexpected: {msg}");
654 }
655}