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_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 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 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 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 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 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 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 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 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 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 let version = response
223 .version
224 .ok_or_else(|| ConversionError("Missing version".to_string()))?;
225
226 let digest = response.digest.unwrap_or_default();
228
229 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 let fees = response
276 .fees
277 .map(ark_core::server::FeeInfo::try_from)
278 .transpose()?;
279
280 let scheduled_session = response
282 .scheduled_session
283 .map(ark_core::server::ScheduledSession::try_from)
284 .transpose()?;
285
286 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 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 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; let outpoint = OutPoint { txid, vout };
358
359 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}