Skip to main content

ark_rest/
conversions.rs

1//! Type conversions between generated API types and ark-core types
2
3use 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_str = value.cutoff_date.ok_or_else(|| {
121            ConversionError("Missing cutoff_date in deprecated signer".to_string())
122        })?;
123        let cutoff_date = i64::from_str(&cutoff_date_str)
124            .map_err(|e| ConversionError(format!("Could not parse cutoff_date: {e:#}")))?;
125
126        Ok(ark_core::server::DeprecatedSigner { pk, cutoff_date })
127    }
128}
129
130impl TryFrom<GetInfoResponse> for ark_core::server::Info {
131    type Error = ConversionError;
132
133    fn try_from(response: GetInfoResponse) -> Result<Self, Self::Error> {
134        // Parse signer_pk
135        let signer_pubkey_str = response
136            .signer_pubkey
137            .ok_or_else(|| ConversionError("Missing signer_pubkey".to_string()))?;
138        let signer_pk = signer_pubkey_str.parse::<PublicKey>().map_err(|e| {
139            ConversionError(format!("Invalid signer_pubkey '{signer_pubkey_str}': {e}"))
140        })?;
141
142        // Parse forfeit_pk
143        let forfeit_pubkey_str = response
144            .forfeit_pubkey
145            .ok_or_else(|| ConversionError("Missing forfeit_pubkey".to_string()))?;
146        let forfeit_pk = forfeit_pubkey_str.parse::<PublicKey>().map_err(|e| {
147            ConversionError(format!(
148                "Invalid forfeit_pubkey '{forfeit_pubkey_str}': {e}"
149            ))
150        })?;
151
152        // Parse checkpoint_tapscript
153        let checkpoint_tapscript_str = response
154            .checkpoint_tapscript
155            .ok_or_else(|| ConversionError("Missing checkpoint_tapscript".to_string()))?;
156        let checkpoint_tapscript = ScriptBuf::from_hex(&checkpoint_tapscript_str).map_err(|e| {
157            ConversionError(format!(
158                "Invalid checkpoint_tapscript hex '{checkpoint_tapscript_str}': {e}"
159            ))
160        })?;
161
162        // Parse unilateral_exit_delay
163        let unilateral_exit_delay_str = response
164            .unilateral_exit_delay
165            .ok_or_else(|| ConversionError("Missing unilateral_exit_delay".to_string()))?;
166        let unilateral_exit_delay_val = i64::from_str(&unilateral_exit_delay_str).map_err(|e| {
167            ConversionError(format!("Could not parse unilateral_exit_delay: {e:#}"))
168        })?;
169        let unilateral_exit_delay = parse_sequence_number(unilateral_exit_delay_val)?;
170
171        // Parse boarding_exit_delay
172        let boarding_exit_delay_str = response
173            .boarding_exit_delay
174            .ok_or_else(|| ConversionError("Missing boarding_exit_delay".to_string()))?;
175        let boarding_exit_delay_val = i64::from_str(&boarding_exit_delay_str)
176            .map_err(|e| ConversionError(format!("Could not parse boarding_exit_delay: {e:#}")))?;
177        let boarding_exit_delay = parse_sequence_number(boarding_exit_delay_val)?;
178
179        // Parse network
180        let network_str = response
181            .network
182            .ok_or_else(|| ConversionError("Missing network".to_string()))?;
183        let network = ark_core::server::Network::from_str(&network_str)
184            .map_err(|e| ConversionError(format!("Invalid network '{network_str}': {e}")))?;
185        let network = bitcoin::Network::from(network);
186
187        // Parse session_duration
188        let session_duration_str = response
189            .session_duration
190            .ok_or_else(|| ConversionError("Missing session_duration".to_string()))?;
191        let session_duration = i64::from_str(&session_duration_str)
192            .map_err(|e| ConversionError(format!("Could not parse session_duration: {e:#}")))?
193            as u64;
194
195        // Parse dust
196        let dust_str = response
197            .dust
198            .ok_or_else(|| ConversionError("Missing dust".to_string()))?;
199        let dust_val = i64::from_str(&dust_str)
200            .map_err(|e| ConversionError(format!("Could not parse dust: {e:#}")))?;
201        let dust = Amount::from_sat(dust_val as u64);
202
203        // Parse forfeit_address
204        let forfeit_address_str = response
205            .forfeit_address
206            .ok_or_else(|| ConversionError("Missing forfeit_address".to_string()))?;
207        let forfeit_address = forfeit_address_str
208            .parse::<bitcoin::Address<bitcoin::address::NetworkUnchecked>>()
209            .map_err(|e| {
210                ConversionError(format!(
211                    "Invalid forfeit_address '{forfeit_address_str}': {e}"
212                ))
213            })?
214            .require_network(network)
215            .map_err(|e| {
216                ConversionError(format!(
217                    "Address network mismatch for '{forfeit_address_str}': {e}"
218                ))
219            })?;
220
221        // Parse version
222        let version = response
223            .version
224            .ok_or_else(|| ConversionError("Missing version".to_string()))?;
225
226        // Parse digest
227        let digest = response.digest.unwrap_or_default();
228
229        // Parse utxo amount limits
230        let utxo_min_amount = response
231            .utxo_min_amount
232            .and_then(|s| i64::from_str(&s).ok())
233            .and_then(|val| {
234                if val >= 0 {
235                    Some(Amount::from_sat(val as u64))
236                } else {
237                    None
238                }
239            });
240
241        let utxo_max_amount = response
242            .utxo_max_amount
243            .and_then(|s| i64::from_str(&s).ok())
244            .and_then(|val| {
245                if val >= 0 {
246                    Some(Amount::from_sat(val as u64))
247                } else {
248                    None
249                }
250            });
251
252        let vtxo_min_amount = response
253            .vtxo_min_amount
254            .and_then(|s| i64::from_str(&s).ok())
255            .and_then(|val| {
256                if val >= 0 {
257                    Some(Amount::from_sat(val as u64))
258                } else {
259                    None
260                }
261            });
262
263        let vtxo_max_amount = response
264            .vtxo_max_amount
265            .and_then(|s| i64::from_str(&s).ok())
266            .and_then(|val| {
267                if val >= 0 {
268                    Some(Amount::from_sat(val as u64))
269                } else {
270                    None
271                }
272            });
273
274        // Parse fees
275        let fees = response
276            .fees
277            .map(ark_core::server::FeeInfo::try_from)
278            .transpose()?;
279
280        // Parse scheduled_session
281        let scheduled_session = response
282            .scheduled_session
283            .map(ark_core::server::ScheduledSession::try_from)
284            .transpose()?;
285
286        // Parse deprecated_signers
287        let deprecated_signers = response
288            .deprecated_signers
289            .unwrap_or_default()
290            .into_iter()
291            .map(ark_core::server::DeprecatedSigner::try_from)
292            .collect::<Result<Vec<_>, _>>()?;
293
294        // Parse service_status
295        let service_status = response.service_status.unwrap_or_default();
296
297        let max_tx_weight_str = response
298            .max_tx_weight
299            .ok_or_else(|| ConversionError("Missing max_tx_weight".to_string()))?;
300        let max_tx_weight = i64::from_str(&max_tx_weight_str)
301            .map_err(|e| ConversionError(format!("Could not parse max_tx_weight: {e:#}")))?;
302
303        let max_op_return_outputs_str = response
304            .max_op_return_outputs
305            .ok_or_else(|| ConversionError("Missing max_op_return_outputs".to_string()))?;
306        let max_op_return_outputs = i64::from_str(&max_op_return_outputs_str).map_err(|e| {
307            ConversionError(format!("Could not parse max_op_return_outputs: {e:#}"))
308        })?;
309
310        Ok(ark_core::server::Info {
311            version,
312            signer_pk,
313            forfeit_pk,
314            forfeit_address,
315            checkpoint_tapscript,
316            network,
317            session_duration,
318            unilateral_exit_delay,
319            boarding_exit_delay,
320            utxo_min_amount,
321            utxo_max_amount,
322            vtxo_min_amount,
323            vtxo_max_amount,
324            dust,
325            fees,
326            scheduled_session,
327            deprecated_signers,
328            service_status,
329            digest,
330            max_tx_weight,
331            max_op_return_outputs,
332        })
333    }
334}
335
336impl TryFrom<IndexerVtxo> for ark_core::server::VirtualTxOutPoint {
337    type Error = ConversionError;
338
339    fn try_from(value: IndexerVtxo) -> Result<Self, Self::Error> {
340        // Parse outpoint
341        let outpoint_data = value
342            .outpoint
343            .ok_or_else(|| ConversionError("Missing outpoint".to_string()))?;
344
345        let txid_str = outpoint_data
346            .txid
347            .ok_or_else(|| ConversionError("Missing outpoint txid".to_string()))?;
348        let txid = txid_str
349            .parse::<Txid>()
350            .map_err(|e| ConversionError(format!("Invalid outpoint txid '{txid_str}': {e}")))?;
351
352        let vout = outpoint_data
353            .vout
354            .ok_or_else(|| ConversionError("Missing outpoint vout".to_string()))?;
355        let vout = vout as u32; // Convert i64 to u32
356
357        let outpoint = OutPoint { txid, vout };
358
359        // Parse timestamps
360        let created_at_str = value
361            .created_at
362            .ok_or_else(|| ConversionError("Missing created_at".to_string()))?;
363        let created_at = i64::from_str(&created_at_str)
364            .map_err(|e| ConversionError(format!("Could not parse created_at: {e:#}")))?;
365
366        let expires_at_str = value
367            .expires_at
368            .ok_or_else(|| ConversionError("Missing expires_at".to_string()))?;
369        let expires_at = i64::from_str(&expires_at_str)
370            .map_err(|e| ConversionError(format!("Could not parse expires_at: {e:#}")))?;
371
372        // Parse amount
373        let amount_str = value
374            .amount
375            .ok_or_else(|| ConversionError("Missing amount".to_string()))?;
376        let amount_val = u64::from_str(&amount_str)
377            .map_err(|e| ConversionError(format!("Could not parse amount: {e:#}")))?;
378        let amount = Amount::from_sat(amount_val);
379
380        // Parse script
381        let script_str = value
382            .script
383            .ok_or_else(|| ConversionError("Missing script".to_string()))?;
384        let script = ScriptBuf::from_hex(&script_str)
385            .map_err(|e| ConversionError(format!("Invalid script hex '{script_str}': {e}")))?;
386
387        // Parse optional spent_by
388        let spent_by = value
389            .spent_by
390            .filter(|s| !s.is_empty())
391            .map(|s| s.parse::<Txid>())
392            .transpose()
393            .map_err(|e| ConversionError(format!("Invalid spent_by txid: {e}")))?;
394
395        // Parse commitment_txids
396        let commitment_txids = value
397            .commitment_txids
398            .unwrap_or_default()
399            .into_iter()
400            .map(|s| s.parse::<Txid>())
401            .collect::<Result<Vec<_>, _>>()
402            .map_err(|e| ConversionError(format!("Invalid commitment_txid: {e}")))?;
403
404        // Parse optional settled_by
405        let settled_by = value
406            .settled_by
407            .filter(|s| !s.is_empty())
408            .map(|s| s.parse::<Txid>())
409            .transpose()
410            .map_err(|e| ConversionError(format!("Invalid settled_by txid: {e}")))?;
411
412        // Parse optional ark_txid
413        let ark_txid = value
414            .ark_txid
415            .filter(|s| !s.is_empty())
416            .map(|s| s.parse::<Txid>())
417            .transpose()
418            .map_err(|e| ConversionError(format!("Invalid ark_txid: {e}")))?;
419
420        let assets = value
421            .assets
422            .unwrap_or_default()
423            .into_iter()
424            .filter_map(|a| match a {
425                IndexerAsset {
426                    amount: Some(amount),
427                    asset_id: Some(asset_id),
428                } => {
429                    let asset_id = match asset_id.parse() {
430                        Ok(asset_id) => asset_id,
431                        Err(e) => {
432                            return Some(Err(ConversionError(format!("Invalid asset ID: {e}"))));
433                        }
434                    };
435
436                    Some(Ok(ark_core::server::Asset {
437                        asset_id,
438                        amount: amount as u64,
439                    }))
440                }
441                _ => None,
442            })
443            .collect::<Result<Vec<_>, _>>()?;
444
445        Ok(ark_core::server::VirtualTxOutPoint {
446            outpoint,
447            created_at,
448            expires_at,
449            amount,
450            script,
451            is_preconfirmed: value.is_preconfirmed.unwrap_or(false),
452            is_swept: value.is_swept.unwrap_or(false),
453            is_unrolled: value.is_unrolled.unwrap_or(false),
454            is_spent: value.is_spent.unwrap_or(false),
455            spent_by,
456            commitment_txids,
457            settled_by,
458            ark_txid,
459            assets,
460        })
461    }
462}
463
464fn parse_sequence_number(value: i64) -> Result<bitcoin::Sequence, ConversionError> {
465    /// The threshold that determines whether an expiry or exit delay should be parsed as a
466    /// number of blocks or a number of seconds.
467    ///
468    /// - A value below 512 is considered a number of blocks.
469    /// - A value over 512 is considered a number of seconds.
470    const ARBITRARY_SEQUENCE_THRESHOLD: i64 = 512;
471
472    let sequence = if value.is_negative() {
473        return Err(ConversionError(format!("invalid sequence number: {value}")));
474    } else if value < ARBITRARY_SEQUENCE_THRESHOLD {
475        bitcoin::Sequence::from_height(value as u16)
476    } else {
477        bitcoin::Sequence::from_seconds_ceil(value as u32)
478            .map_err(|e| ConversionError(format!("Failed parsing sequence number: {e}")))?
479    };
480
481    Ok(sequence)
482}
483
484impl TryFrom<crate::models::IndexerSubscriptionEvent> for ark_core::server::SubscriptionEvent {
485    type Error = ConversionError;
486
487    fn try_from(event: crate::models::IndexerSubscriptionEvent) -> Result<Self, Self::Error> {
488        // Parse txid
489        let txid_str = event
490            .txid
491            .ok_or_else(|| ConversionError("Missing txid in subscription event".to_string()))?;
492        let txid = txid_str
493            .parse::<Txid>()
494            .map_err(|e| ConversionError(format!("Invalid txid '{txid_str}': {e}")))?;
495
496        // Parse scripts
497        let scripts = event
498            .scripts
499            .unwrap_or_default()
500            .iter()
501            .map(|h| {
502                ScriptBuf::from_hex(h)
503                    .map_err(|e| ConversionError(format!("Invalid script hex: {e}")))
504            })
505            .collect::<Result<Vec<_>, _>>()?;
506
507        // Parse new_vtxos
508        let new_vtxos = event
509            .new_vtxos
510            .unwrap_or_default()
511            .into_iter()
512            .map(ark_core::server::VirtualTxOutPoint::try_from)
513            .collect::<Result<Vec<_>, _>>()
514            .map_err(|e| ConversionError(format!("Invalid new_vtxos: {e}")))?;
515
516        // Parse spent_vtxos
517        let spent_vtxos = event
518            .spent_vtxos
519            .unwrap_or_default()
520            .into_iter()
521            .map(ark_core::server::VirtualTxOutPoint::try_from)
522            .collect::<Result<Vec<_>, _>>()
523            .map_err(|e| ConversionError(format!("Invalid spent_vtxos: {e}")))?;
524
525        // Parse tx (raw tx hex or base64 PSBT)
526        let tx = if let Some(tx_str) = event.tx.filter(|s| !s.is_empty()) {
527            match Vec::from_hex(&tx_str)
528                .ok()
529                .and_then(|bytes| bitcoin::consensus::deserialize::<Transaction>(&bytes).ok())
530            {
531                Some(raw_tx) => Some(raw_tx),
532                None => {
533                    let base64 = base64::engine::GeneralPurpose::new(
534                        &base64::alphabet::STANDARD,
535                        base64::engine::GeneralPurposeConfig::new(),
536                    );
537                    let bytes = base64
538                        .decode(&tx_str)
539                        .map_err(|e| ConversionError(format!("Invalid tx payload: {e}")))?;
540                    let psbt = Psbt::deserialize(&bytes)
541                        .map_err(|e| ConversionError(format!("Invalid tx psbt: {e}")))?;
542                    Some(psbt.unsigned_tx)
543                }
544            }
545        } else {
546            None
547        };
548
549        // Parse checkpoint_txs
550        let checkpoint_txs = event
551            .checkpoint_txs
552            .unwrap_or_default()
553            .into_iter()
554            .map(|(k, v)| {
555                let out_point = OutPoint::from_str(&k)
556                    .map_err(|e| ConversionError(format!("Invalid checkpoint outpoint: {e}")))?;
557                let txid_str = v
558                    .txid
559                    .ok_or_else(|| ConversionError("Missing checkpoint txid".to_string()))?;
560                let txid = txid_str
561                    .parse::<Txid>()
562                    .map_err(|e| ConversionError(format!("Invalid checkpoint txid: {e}")))?;
563                Ok((out_point, txid))
564            })
565            .collect::<Result<HashMap<_, _>, ConversionError>>()?;
566
567        Ok(ark_core::server::SubscriptionEvent {
568            txid,
569            scripts,
570            new_vtxos,
571            spent_vtxos,
572            tx,
573            checkpoint_txs,
574        })
575    }
576}
577
578impl TryFrom<GetSubscriptionResponse> for ark_core::server::SubscriptionResponse {
579    type Error = ConversionError;
580
581    fn try_from(value: GetSubscriptionResponse) -> Result<Self, Self::Error> {
582        // Check if it's a heartbeat or an event
583        if value.heartbeat.is_some() {
584            Ok(ark_core::server::SubscriptionResponse::Heartbeat)
585        } else if let Some(event) = value.event {
586            let subscription_event = ark_core::server::SubscriptionEvent::try_from(event)?;
587            Ok(ark_core::server::SubscriptionResponse::Event(Box::new(
588                subscription_event,
589            )))
590        } else {
591            Err(ConversionError(
592                "GetSubscriptionResponse must have either event or heartbeat".to_string(),
593            ))
594        }
595    }
596}