Skip to main content

ark_grpc/
client.rs

1use crate::generated;
2use crate::generated::ark::v1::ark_service_client::ArkServiceClient;
3use crate::generated::ark::v1::get_subscription_response;
4use crate::generated::ark::v1::indexer_service_client::IndexerServiceClient;
5use crate::generated::ark::v1::indexer_tx_history_record::Key;
6use crate::generated::ark::v1::ConfirmRegistrationRequest;
7use crate::generated::ark::v1::EstimateIntentFeeRequest;
8use crate::generated::ark::v1::GetEventStreamRequest;
9use crate::generated::ark::v1::GetInfoRequest;
10use crate::generated::ark::v1::GetSubscriptionRequest;
11use crate::generated::ark::v1::GetTransactionsStreamRequest;
12use crate::generated::ark::v1::IndexerChainedTxType;
13use crate::generated::ark::v1::Intent;
14use crate::generated::ark::v1::Outpoint;
15use crate::generated::ark::v1::RegisterIntentRequest;
16use crate::generated::ark::v1::SubmitSignedForfeitTxsRequest;
17use crate::generated::ark::v1::SubmitTreeNoncesRequest;
18use crate::generated::ark::v1::SubmitTreeSignaturesRequest;
19use crate::generated::ark::v1::SubscribeForScriptsRequest;
20use crate::generated::ark::v1::UnsubscribeForScriptsRequest;
21use crate::Error;
22use ark_core::asset::AssetId;
23use ark_core::history;
24use ark_core::server::parse_sequence_number;
25use ark_core::server::ArkTransaction;
26use ark_core::server::AssetInfo;
27use ark_core::server::BatchFailed;
28use ark_core::server::BatchFinalizationEvent;
29use ark_core::server::BatchFinalizedEvent;
30use ark_core::server::BatchStartedEvent;
31use ark_core::server::BatchTreeEventType;
32use ark_core::server::ChainedTxType;
33use ark_core::server::CommitmentTransaction;
34use ark_core::server::FinalizeOffchainTxResponse;
35use ark_core::server::GetVtxosRequest;
36use ark_core::server::GetVtxosRequestFilter;
37use ark_core::server::GetVtxosRequestReference;
38use ark_core::server::IndexerPage;
39use ark_core::server::Info;
40use ark_core::server::NoncePks;
41use ark_core::server::PartialSigTree;
42use ark_core::server::StreamEvent;
43use ark_core::server::StreamStartedEvent;
44use ark_core::server::StreamTransactionData;
45use ark_core::server::SubmitOffchainTxResponse;
46use ark_core::server::SubscriptionEvent;
47use ark_core::server::SubscriptionResponse;
48use ark_core::server::TreeNoncesAggregatedEvent;
49use ark_core::server::TreeNoncesEvent;
50use ark_core::server::TreeSignatureEvent;
51use ark_core::server::TreeSigningStartedEvent;
52use ark_core::server::TreeTxEvent;
53use ark_core::server::TreeTxNoncePks;
54use ark_core::server::VirtualTxOutPoint;
55use ark_core::server::VirtualTxsResponse;
56use ark_core::server::VtxoChain;
57use ark_core::server::VtxoChains;
58use ark_core::ArkAddress;
59use ark_core::TxGraphChunk;
60use async_stream::stream;
61use base64::Engine;
62use bitcoin::hex::FromHex;
63use bitcoin::secp256k1::PublicKey;
64use bitcoin::taproot::Signature;
65use bitcoin::OutPoint;
66use bitcoin::Psbt;
67use bitcoin::ScriptBuf;
68use bitcoin::SignedAmount;
69use bitcoin::Transaction;
70use bitcoin::Txid;
71use futures::Stream;
72use futures::StreamExt;
73use futures::TryStreamExt;
74use std::collections::HashMap;
75use std::str::FromStr;
76
77#[derive(Debug, Clone)]
78pub struct Client {
79    url: String,
80    ark_client: Option<ArkServiceClient<tonic::transport::Channel>>,
81    indexer_client: Option<IndexerServiceClient<tonic::transport::Channel>>,
82}
83
84impl Client {
85    pub fn new(url: String) -> Self {
86        Self {
87            url,
88            ark_client: None,
89            indexer_client: None,
90        }
91    }
92
93    pub async fn connect(&mut self) -> Result<(), Error> {
94        let ark_service_client = ArkServiceClient::connect(self.url.clone())
95            .await
96            .map_err(Error::connect)?;
97        let indexer_client = IndexerServiceClient::connect(self.url.clone())
98            .await
99            .map_err(Error::connect)?;
100
101        self.ark_client = Some(ark_service_client);
102        self.indexer_client = Some(indexer_client);
103        Ok(())
104    }
105
106    pub async fn get_info(&mut self) -> Result<Info, Error> {
107        let mut client = self.ark_client()?;
108
109        let response = client
110            .get_info(GetInfoRequest {})
111            .await
112            .map_err(Error::request)?;
113
114        response.into_inner().try_into()
115    }
116
117    /// List VTXOs with pagination support.
118    /// Returns a single page of results along with pagination info.
119    pub async fn list_vtxos(&self, request: GetVtxosRequest) -> Result<ListVtxosResponse, Error> {
120        if request.reference().is_empty() {
121            return Ok(ListVtxosResponse {
122                vtxos: Vec::new(),
123                page: None,
124            });
125        }
126
127        let mut client = self.indexer_client()?;
128
129        let response = client
130            .get_vtxos(generated::ark::v1::GetVtxosRequest::from(request))
131            .await
132            .map_err(Error::request)?;
133
134        let inner = response.into_inner();
135
136        let vtxos = inner
137            .vtxos
138            .iter()
139            .map(VirtualTxOutPoint::try_from)
140            .collect::<Result<Vec<_>, _>>()?;
141
142        let page = inner
143            .page
144            .map(IndexerPage::try_from)
145            .transpose()
146            .map_err(Error::conversion)?;
147
148        Ok(ListVtxosResponse { vtxos, page })
149    }
150
151    pub async fn register_intent(&self, intent: ark_core::intent::Intent) -> Result<String, Error> {
152        let mut client = self.ark_client()?;
153
154        let intent = intent.try_into()?;
155        let request = RegisterIntentRequest {
156            intent: Some(intent),
157        };
158
159        let response = client
160            .register_intent(request)
161            .await
162            .map_err(Error::request)?;
163
164        let intent_id = response.into_inner().intent_id;
165
166        Ok(intent_id)
167    }
168
169    pub async fn submit_offchain_transaction_request(
170        &self,
171        ark_tx: Psbt,
172        checkpoint_txs: Vec<Psbt>,
173    ) -> Result<SubmitOffchainTxResponse, Error> {
174        let mut client = self.ark_client()?;
175
176        let base64 = base64::engine::GeneralPurpose::new(
177            &base64::alphabet::STANDARD,
178            base64::engine::GeneralPurposeConfig::new(),
179        );
180
181        let ark_tx = base64.encode(ark_tx.serialize());
182
183        let checkpoint_txs = checkpoint_txs
184            .into_iter()
185            .map(|tx| base64.encode(tx.serialize()))
186            .collect();
187
188        let res = client
189            .submit_tx(generated::ark::v1::SubmitTxRequest {
190                signed_ark_tx: ark_tx,
191                checkpoint_txs,
192            })
193            .await
194            .map_err(Error::request)?;
195
196        let res = res.into_inner();
197
198        let signed_ark_tx = res.final_ark_tx;
199        let signed_ark_tx = base64.decode(signed_ark_tx).map_err(Error::conversion)?;
200        let signed_ark_tx = Psbt::deserialize(&signed_ark_tx).map_err(Error::conversion)?;
201
202        let signed_checkpoint_txs = res
203            .signed_checkpoint_txs
204            .into_iter()
205            .map(|tx| {
206                let tx = base64.decode(tx).map_err(Error::conversion)?;
207                let tx = Psbt::deserialize(&tx).map_err(Error::conversion)?;
208
209                Ok(tx)
210            })
211            .collect::<Result<Vec<_>, Error>>()?;
212
213        Ok(SubmitOffchainTxResponse {
214            signed_ark_tx,
215            signed_checkpoint_txs,
216        })
217    }
218
219    pub async fn finalize_offchain_transaction(
220        &self,
221        txid: Txid,
222        checkpoint_txs: Vec<Psbt>,
223    ) -> Result<FinalizeOffchainTxResponse, Error> {
224        let mut client = self.ark_client()?;
225
226        let base64 = base64::engine::GeneralPurpose::new(
227            &base64::alphabet::STANDARD,
228            base64::engine::GeneralPurposeConfig::new(),
229        );
230
231        let checkpoint_txs = checkpoint_txs
232            .into_iter()
233            .map(|tx| base64.encode(tx.serialize()))
234            .collect();
235
236        client
237            .finalize_tx(generated::ark::v1::FinalizeTxRequest {
238                ark_txid: txid.to_string(),
239                final_checkpoint_txs: checkpoint_txs,
240            })
241            .await
242            .map_err(Error::request)?;
243
244        Ok(FinalizeOffchainTxResponse {})
245    }
246
247    pub async fn get_pending_tx(
248        &self,
249        intent: ark_core::intent::Intent,
250    ) -> Result<Vec<ark_core::server::PendingTx>, Error> {
251        let mut client = self.ark_client()?;
252
253        let intent: Intent = intent.try_into()?;
254
255        let res = client
256            .get_pending_tx(generated::ark::v1::GetPendingTxRequest {
257                identifier: Some(
258                    generated::ark::v1::get_pending_tx_request::Identifier::Intent(intent),
259                ),
260            })
261            .await
262            .map_err(Error::request)?;
263
264        let inner = res.into_inner();
265        let base64 = base64::engine::GeneralPurpose::new(
266            &base64::alphabet::STANDARD,
267            base64::engine::GeneralPurposeConfig::new(),
268        );
269
270        inner
271            .pending_txs
272            .into_iter()
273            .map(|tx| {
274                let ark_txid = tx.ark_txid.parse().map_err(Error::conversion)?;
275
276                let signed_ark_tx = base64.decode(&tx.final_ark_tx).map_err(Error::conversion)?;
277                let signed_ark_tx = Psbt::deserialize(&signed_ark_tx).map_err(Error::conversion)?;
278
279                let signed_checkpoint_txs = tx
280                    .signed_checkpoint_txs
281                    .into_iter()
282                    .map(|cp| {
283                        let bytes = base64.decode(cp).map_err(Error::conversion)?;
284                        Psbt::deserialize(&bytes).map_err(Error::conversion)
285                    })
286                    .collect::<Result<Vec<_>, Error>>()?;
287
288                Ok(ark_core::server::PendingTx {
289                    ark_txid,
290                    signed_ark_tx,
291                    signed_checkpoint_txs,
292                })
293            })
294            .collect()
295    }
296
297    pub async fn confirm_registration(&self, intent_id: String) -> Result<(), Error> {
298        let mut client = self.ark_client()?;
299
300        client
301            .confirm_registration(ConfirmRegistrationRequest { intent_id })
302            .await
303            .map_err(Error::request)?;
304
305        Ok(())
306    }
307
308    pub async fn submit_tree_nonces(
309        &self,
310        batch_id: &str,
311        cosigner_pubkey: PublicKey,
312        pub_nonce_tree: NoncePks,
313    ) -> Result<(), Error> {
314        let mut client = self.ark_client()?;
315
316        client
317            .submit_tree_nonces(SubmitTreeNoncesRequest {
318                batch_id: batch_id.to_string(),
319                pubkey: cosigner_pubkey.to_string(),
320                tree_nonces: pub_nonce_tree.encode(),
321            })
322            .await
323            .map_err(Error::request)?;
324
325        Ok(())
326    }
327
328    pub async fn submit_tree_signatures(
329        &self,
330        batch_id: &str,
331        cosigner_pk: PublicKey,
332        partial_sig_tree: PartialSigTree,
333    ) -> Result<(), Error> {
334        let mut client = self.ark_client()?;
335
336        client
337            .submit_tree_signatures(SubmitTreeSignaturesRequest {
338                batch_id: batch_id.to_string(),
339                pubkey: cosigner_pk.to_string(),
340                tree_signatures: partial_sig_tree.encode(),
341            })
342            .await
343            .map_err(Error::request)?;
344
345        Ok(())
346    }
347
348    pub async fn submit_signed_forfeit_txs(
349        &self,
350        signed_forfeit_txs: Vec<Psbt>,
351        signed_commitment_tx: Option<Psbt>,
352    ) -> Result<(), Error> {
353        let mut client = self.ark_client()?;
354
355        let base64 = base64::engine::GeneralPurpose::new(
356            &base64::alphabet::STANDARD,
357            base64::engine::GeneralPurposeConfig::new(),
358        );
359
360        let signed_commitment_tx = signed_commitment_tx
361            .map(|tx| base64.encode(tx.serialize()))
362            .unwrap_or_default();
363
364        client
365            .submit_signed_forfeit_txs(SubmitSignedForfeitTxsRequest {
366                signed_forfeit_txs: signed_forfeit_txs
367                    .iter()
368                    .map(|psbt| base64.encode(psbt.serialize()))
369                    .collect(),
370                signed_commitment_tx,
371            })
372            .await
373            .map_err(Error::request)?;
374
375        Ok(())
376    }
377
378    pub async fn get_event_stream(
379        &self,
380        topics: Vec<String>,
381    ) -> Result<impl Stream<Item = Result<StreamEvent, Error>> + Unpin, Error> {
382        let mut client = self.ark_client()?;
383
384        let response = client
385            .get_event_stream(GetEventStreamRequest { topics })
386            .await
387            .map_err(Error::request)?;
388        let mut stream = response.into_inner();
389
390        let stream = stream! {
391            loop {
392                match stream.try_next().await {
393                    Ok(Some(event)) => match event.event {
394                        None => {
395                            log::debug!("Got empty message");
396                        }
397                        Some(event) => {
398                            yield Ok(StreamEvent::try_from(event)?);
399                        }
400                    },
401                    Ok(None) => {
402                        yield Err(Error::event_stream_disconnect());
403                    }
404                    Err(e) => {
405                        yield Err(Error::event_stream(e));
406                    }
407                }
408            }
409        };
410
411        Ok(stream.boxed())
412    }
413
414    pub async fn get_tx_stream(
415        &self,
416    ) -> Result<impl Stream<Item = Result<StreamTransactionData, Error>> + Unpin, Error> {
417        let mut client = self.ark_client()?;
418
419        let response = client
420            .get_transactions_stream(GetTransactionsStreamRequest {})
421            .await
422            .map_err(Error::request)?;
423
424        let mut stream = response.into_inner();
425
426        let stream = stream! {
427            loop {
428                match stream.try_next().await {
429                    Ok(Some(event)) => match event.data {
430                        None => {
431                            log::debug!("Got empty message");
432                        }
433                        Some(event) => {
434                            yield Ok(StreamTransactionData::try_from(event)?);
435                        }
436                    },
437                    Ok(None) => {
438                        yield Err(Error::event_stream_disconnect());
439                    }
440                    Err(e) => {
441                        yield Err(Error::event_stream(e));
442                    }
443                }
444            }
445        };
446
447        Ok(stream.boxed())
448    }
449
450    pub async fn get_vtxo_chain(
451        &self,
452        outpoint: Option<OutPoint>,
453        size_and_index: Option<(i32, i32)>,
454    ) -> Result<VtxoChainResponse, Error> {
455        let mut client = self.indexer_client()?;
456        let response = client
457            .get_vtxo_chain(generated::ark::v1::GetVtxoChainRequest {
458                outpoint: outpoint.map(|o| generated::ark::v1::IndexerOutpoint {
459                    txid: o.txid.to_string(),
460                    vout: o.vout,
461                }),
462                page: size_and_index
463                    .map(|(size, index)| generated::ark::v1::IndexerPageRequest { size, index }),
464            })
465            .await
466            .map_err(Error::request)?;
467        let response = response.into_inner();
468        let result = response.try_into()?;
469        Ok(result)
470    }
471
472    pub async fn get_virtual_txs(
473        &self,
474        txids: Vec<String>,
475        size_and_index: Option<(i32, i32)>,
476    ) -> Result<VirtualTxsResponse, Error> {
477        let mut client = self.indexer_client()?;
478        let response = client
479            .get_virtual_txs(generated::ark::v1::GetVirtualTxsRequest {
480                txids,
481                page: size_and_index
482                    .map(|(size, index)| generated::ark::v1::IndexerPageRequest { size, index }),
483            })
484            .await
485            .map_err(Error::request)?;
486        let response = response.into_inner();
487        let result = response.try_into()?;
488        Ok(result)
489    }
490
491    /// Allows to subscribe for tx notifications related to the provided
492    /// vtxo scripts.
493    ///
494    /// It can also be used to update an existing subscriptions by adding
495    /// new scripts to it.
496    ///
497    /// Note: for new subscriptions, don't provide a `subscription_id`
498    ///
499    /// Returns the subscription id if successful
500    pub async fn subscribe_to_scripts(
501        &self,
502        scripts: Vec<ArkAddress>,
503        subscription_id: Option<String>,
504    ) -> Result<String, Error> {
505        let mut client = self.indexer_client()?;
506        let scripts = scripts
507            .iter()
508            .map(|address| address.to_p2tr_script_pubkey().to_hex_string())
509            .collect::<Vec<_>>();
510
511        // For new subscription we expect empty string ("") here
512        let subscription_id = subscription_id.unwrap_or_default();
513
514        let response = client
515            .subscribe_for_scripts(SubscribeForScriptsRequest {
516                scripts,
517                subscription_id,
518            })
519            .await
520            .map_err(Error::request)?;
521
522        let response = response.into_inner();
523
524        Ok(response.subscription_id)
525    }
526
527    /// Allows to remove scripts from an existing subscription.
528    pub async fn unsubscribe_from_scripts(
529        &self,
530        scripts: Vec<ArkAddress>,
531        subscription_id: String,
532    ) -> Result<(), Error> {
533        let mut client = self.indexer_client()?;
534        let scripts = scripts
535            .iter()
536            .map(|address| address.to_p2tr_script_pubkey().to_hex_string())
537            .collect::<Vec<_>>();
538
539        let _ = client
540            .unsubscribe_for_scripts(UnsubscribeForScriptsRequest {
541                subscription_id,
542                scripts,
543            })
544            .await
545            .map_err(Error::request)?;
546
547        Ok(())
548    }
549
550    /// Gets a subscription stream that returns subscription responses.
551    pub async fn get_subscription(
552        &self,
553        subscription_id: String,
554    ) -> Result<impl Stream<Item = Result<SubscriptionResponse, Error>> + Unpin, Error> {
555        let mut client = self.indexer_client()?;
556
557        let response = client
558            .get_subscription(GetSubscriptionRequest { subscription_id })
559            .await
560            .map_err(Error::request)?;
561
562        let mut stream = response.into_inner();
563
564        let stream = stream! {
565            loop {
566                match stream.try_next().await {
567                    Ok(Some(response)) => {
568                        match SubscriptionResponse::try_from(response) {
569                            Ok(subscription_response) => {
570                                yield Ok(subscription_response);
571                            }
572                            Err(e) => {
573                                yield Err(e);
574                            }
575                        }
576                    }
577                    Ok(None) => {
578                        break;
579                    }
580                    Err(e) => {
581                        yield Err(Error::event_stream(e));
582                    }
583                }
584            }
585        };
586
587        Ok(stream.boxed())
588    }
589
590    pub async fn estimate_fees(
591        &self,
592        intent: ark_core::intent::Intent,
593    ) -> Result<SignedAmount, Error> {
594        let mut client = self.ark_client()?;
595
596        let intent = intent.try_into()?;
597        let response = client
598            .estimate_intent_fee(EstimateIntentFeeRequest {
599                intent: Some(intent),
600            })
601            .await
602            .map_err(Error::request)?;
603        let response = response.into_inner();
604
605        Ok(SignedAmount::from_sat(response.fee))
606    }
607
608    pub async fn get_asset(&self, asset_id: AssetId) -> Result<AssetInfo, Error> {
609        let mut client = self.indexer_client()?;
610
611        let response = client
612            .get_asset(generated::ark::v1::GetAssetRequest {
613                asset_id: asset_id.to_string(),
614            })
615            .await
616            .map_err(Error::request)?;
617
618        let inner = response.into_inner();
619
620        let supply = inner.supply.parse::<u64>().map_err(Error::conversion)?;
621
622        let asset_id = inner.asset_id.parse().map_err(Error::conversion)?;
623        let control_asset_id = if inner.control_asset.is_empty() {
624            None
625        } else {
626            Some(inner.control_asset.parse().map_err(Error::conversion)?)
627        };
628
629        Ok(AssetInfo {
630            asset_id,
631            control_asset_id,
632            supply,
633            metadata: inner.metadata,
634        })
635    }
636
637    fn ark_client(&self) -> Result<ArkServiceClient<tonic::transport::Channel>, Error> {
638        // Cloning an `ArkServiceClient<Channel>` is cheap.
639        self.ark_client.clone().ok_or(Error::not_connected())
640    }
641    fn indexer_client(&self) -> Result<IndexerServiceClient<tonic::transport::Channel>, Error> {
642        self.indexer_client.clone().ok_or(Error::not_connected())
643    }
644}
645
646impl TryFrom<ark_core::intent::Intent> for Intent {
647    type Error = Error;
648
649    fn try_from(value: ark_core::intent::Intent) -> Result<Self, Self::Error> {
650        Ok(Self {
651            proof: value.serialize_proof(),
652            message: value.serialize_message().map_err(Error::conversion)?,
653        })
654    }
655}
656
657impl TryFrom<generated::ark::v1::BatchStartedEvent> for BatchStartedEvent {
658    type Error = Error;
659
660    fn try_from(value: generated::ark::v1::BatchStartedEvent) -> Result<Self, Self::Error> {
661        let batch_expiry = parse_sequence_number(value.batch_expiry).map_err(Error::conversion)?;
662
663        Ok(BatchStartedEvent {
664            id: value.id,
665            intent_id_hashes: value.intent_id_hashes,
666            batch_expiry,
667        })
668    }
669}
670
671impl TryFrom<generated::ark::v1::StreamStartedEvent> for StreamStartedEvent {
672    type Error = Error;
673
674    fn try_from(value: generated::ark::v1::StreamStartedEvent) -> Result<Self, Self::Error> {
675        Ok(StreamStartedEvent { id: value.id })
676    }
677}
678
679impl TryFrom<generated::ark::v1::BatchFinalizationEvent> for BatchFinalizationEvent {
680    type Error = Error;
681
682    fn try_from(value: generated::ark::v1::BatchFinalizationEvent) -> Result<Self, Self::Error> {
683        let base64 = &base64::engine::GeneralPurpose::new(
684            &base64::alphabet::STANDARD,
685            base64::engine::GeneralPurposeConfig::new(),
686        );
687
688        let commitment_tx = base64
689            .decode(&value.commitment_tx)
690            .map_err(Error::conversion)?;
691        let commitment_tx = Psbt::deserialize(&commitment_tx).map_err(Error::conversion)?;
692
693        Ok(BatchFinalizationEvent {
694            id: value.id,
695            commitment_tx,
696        })
697    }
698}
699
700impl TryFrom<generated::ark::v1::BatchFinalizedEvent> for BatchFinalizedEvent {
701    type Error = Error;
702
703    fn try_from(value: generated::ark::v1::BatchFinalizedEvent) -> Result<Self, Self::Error> {
704        let commitment_txid = value.commitment_txid.parse().map_err(Error::conversion)?;
705
706        Ok(BatchFinalizedEvent {
707            id: value.id,
708            commitment_txid,
709        })
710    }
711}
712
713impl From<generated::ark::v1::BatchFailedEvent> for BatchFailed {
714    fn from(value: generated::ark::v1::BatchFailedEvent) -> Self {
715        BatchFailed {
716            id: value.id,
717            reason: value.reason,
718        }
719    }
720}
721
722impl TryFrom<generated::ark::v1::TreeSigningStartedEvent> for TreeSigningStartedEvent {
723    type Error = Error;
724
725    fn try_from(value: generated::ark::v1::TreeSigningStartedEvent) -> Result<Self, Self::Error> {
726        let unsigned_commitment_tx = base64::engine::GeneralPurpose::new(
727            &base64::alphabet::STANDARD,
728            base64::engine::GeneralPurposeConfig::new(),
729        )
730        .decode(&value.unsigned_commitment_tx)
731        .map_err(Error::conversion)?;
732
733        let unsigned_commitment_tx =
734            Psbt::deserialize(&unsigned_commitment_tx).map_err(Error::conversion)?;
735
736        Ok(TreeSigningStartedEvent {
737            id: value.id,
738            cosigners_pubkeys: value
739                .cosigners_pubkeys
740                .into_iter()
741                .map(|pk| pk.parse().map_err(Error::conversion))
742                .collect::<Result<Vec<_>, Error>>()?,
743            unsigned_commitment_tx,
744        })
745    }
746}
747
748impl TryFrom<generated::ark::v1::TreeNoncesAggregatedEvent> for TreeNoncesAggregatedEvent {
749    type Error = Error;
750
751    fn try_from(value: generated::ark::v1::TreeNoncesAggregatedEvent) -> Result<Self, Self::Error> {
752        let tree_nonces = NoncePks::decode(value.tree_nonces).map_err(Error::conversion)?;
753
754        Ok(TreeNoncesAggregatedEvent {
755            id: value.id,
756            tree_nonces,
757        })
758    }
759}
760
761impl TryFrom<generated::ark::v1::TreeTxEvent> for TreeTxEvent {
762    type Error = Error;
763
764    fn try_from(value: generated::ark::v1::TreeTxEvent) -> Result<Self, Self::Error> {
765        let batch_tree_event_type = match value.batch_index {
766            0 => BatchTreeEventType::Vtxo,
767            1 => BatchTreeEventType::Connector,
768            n => return Err(Error::conversion(format!("unsupported batch index: {n}"))),
769        };
770
771        let txid = if value.txid.is_empty() {
772            None
773        } else {
774            Some(value.txid.parse().map_err(Error::conversion)?)
775        };
776
777        let base64 = &base64::engine::GeneralPurpose::new(
778            &base64::alphabet::STANDARD,
779            base64::engine::GeneralPurposeConfig::new(),
780        );
781
782        let bytes = base64.decode(&value.tx).map_err(Error::conversion)?;
783        let tx = Psbt::deserialize(&bytes).map_err(Error::conversion)?;
784
785        let children = value
786            .children
787            .iter()
788            .map(|(index, txid)| Ok((*index, txid.parse().map_err(Error::conversion)?)))
789            .collect::<Result<HashMap<_, _>, Error>>()?;
790
791        Ok(Self {
792            id: value.id,
793            topic: value.topic,
794            batch_tree_event_type,
795            tx_graph_chunk: TxGraphChunk { txid, tx, children },
796        })
797    }
798}
799
800impl TryFrom<generated::ark::v1::TreeSignatureEvent> for TreeSignatureEvent {
801    type Error = Error;
802
803    fn try_from(value: generated::ark::v1::TreeSignatureEvent) -> Result<Self, Self::Error> {
804        let batch_tree_event_type = match value.batch_index {
805            0 => BatchTreeEventType::Vtxo,
806            1 => BatchTreeEventType::Connector,
807            n => return Err(Error::conversion(format!("unsupported batch index: {n}"))),
808        };
809
810        let txid = value.txid.parse().map_err(Error::conversion)?;
811
812        let signature = Vec::from_hex(&value.signature).map_err(Error::conversion)?;
813        let signature = Signature::from_slice(&signature).map_err(Error::conversion)?;
814
815        Ok(Self {
816            id: value.id,
817            topic: value.topic,
818            batch_tree_event_type,
819            txid,
820            signature,
821        })
822    }
823}
824
825impl TryFrom<generated::ark::v1::TreeNoncesEvent> for TreeNoncesEvent {
826    type Error = Error;
827
828    fn try_from(value: generated::ark::v1::TreeNoncesEvent) -> Result<Self, Self::Error> {
829        let txid = value.txid.parse().map_err(Error::conversion)?;
830
831        let nonces = TreeTxNoncePks::decode(value.nonces).map_err(Error::conversion)?;
832
833        Ok(Self {
834            id: value.id,
835            topic: value.topic,
836            txid,
837            nonces,
838        })
839    }
840}
841
842impl TryFrom<generated::ark::v1::get_event_stream_response::Event> for StreamEvent {
843    type Error = Error;
844
845    fn try_from(
846        value: generated::ark::v1::get_event_stream_response::Event,
847    ) -> Result<Self, Self::Error> {
848        Ok(match value {
849            generated::ark::v1::get_event_stream_response::Event::StreamStarted(e) => {
850                StreamEvent::StreamStarted(e.try_into()?)
851            }
852            generated::ark::v1::get_event_stream_response::Event::BatchStarted(e) => {
853                StreamEvent::BatchStarted(e.try_into()?)
854            }
855            generated::ark::v1::get_event_stream_response::Event::BatchFinalization(e) => {
856                StreamEvent::BatchFinalization(e.try_into()?)
857            }
858            generated::ark::v1::get_event_stream_response::Event::BatchFinalized(e) => {
859                StreamEvent::BatchFinalized(e.try_into()?)
860            }
861            generated::ark::v1::get_event_stream_response::Event::BatchFailed(e) => {
862                StreamEvent::BatchFailed(e.into())
863            }
864            generated::ark::v1::get_event_stream_response::Event::TreeSigningStarted(e) => {
865                StreamEvent::TreeSigningStarted(e.try_into()?)
866            }
867            generated::ark::v1::get_event_stream_response::Event::TreeNoncesAggregated(e) => {
868                StreamEvent::TreeNoncesAggregated(e.try_into()?)
869            }
870            generated::ark::v1::get_event_stream_response::Event::TreeTx(e) => {
871                StreamEvent::TreeTx(e.try_into()?)
872            }
873            generated::ark::v1::get_event_stream_response::Event::TreeSignature(e) => {
874                StreamEvent::TreeSignature(e.try_into()?)
875            }
876            generated::ark::v1::get_event_stream_response::Event::TreeNonces(e) => {
877                StreamEvent::TreeNonces(e.try_into()?)
878            }
879            generated::ark::v1::get_event_stream_response::Event::Heartbeat(_) => {
880                StreamEvent::Heartbeat
881            }
882        })
883    }
884}
885
886impl TryFrom<generated::ark::v1::get_transactions_stream_response::Data> for StreamTransactionData {
887    type Error = Error;
888
889    fn try_from(
890        value: generated::ark::v1::get_transactions_stream_response::Data,
891    ) -> Result<Self, Self::Error> {
892        match value {
893            generated::ark::v1::get_transactions_stream_response::Data::CommitmentTx(
894                commitment_tx,
895            ) => Ok(StreamTransactionData::Commitment(
896                CommitmentTransaction::try_from(commitment_tx)?,
897            )),
898            generated::ark::v1::get_transactions_stream_response::Data::ArkTx(redeem) => Ok(
899                StreamTransactionData::Ark(ArkTransaction::try_from(redeem)?),
900            ),
901            generated::ark::v1::get_transactions_stream_response::Data::Heartbeat(_) => {
902                Ok(StreamTransactionData::Heartbeat)
903            }
904            generated::ark::v1::get_transactions_stream_response::Data::SweepTx(tx) => {
905                Ok(StreamTransactionData::Ark(ArkTransaction::try_from(tx)?))
906            }
907        }
908    }
909}
910
911impl TryFrom<generated::ark::v1::TxNotification> for CommitmentTransaction {
912    type Error = Error;
913
914    fn try_from(value: generated::ark::v1::TxNotification) -> Result<Self, Self::Error> {
915        let spent_vtxos = value
916            .spent_vtxos
917            .iter()
918            .map(VirtualTxOutPoint::try_from)
919            .collect::<Result<Vec<_>, _>>()?;
920
921        let spendable_vtxos = value
922            .spendable_vtxos
923            .iter()
924            .map(VirtualTxOutPoint::try_from)
925            .collect::<Result<Vec<_>, _>>()?;
926
927        Ok(CommitmentTransaction {
928            txid: Txid::from_str(value.txid.as_str()).map_err(Error::conversion)?,
929            spent_vtxos,
930            unspent_vtxos: spendable_vtxos,
931        })
932    }
933}
934
935impl TryFrom<generated::ark::v1::TxNotification> for ArkTransaction {
936    type Error = Error;
937
938    fn try_from(value: generated::ark::v1::TxNotification) -> Result<Self, Self::Error> {
939        let spent_vtxos = value
940            .spent_vtxos
941            .iter()
942            .map(VirtualTxOutPoint::try_from)
943            .collect::<Result<Vec<_>, _>>()?;
944
945        let spendable_vtxos = value
946            .spendable_vtxos
947            .iter()
948            .map(VirtualTxOutPoint::try_from)
949            .collect::<Result<Vec<_>, _>>()?;
950
951        let tx = if value.tx.is_empty() {
952            None
953        } else {
954            let base64 = base64::engine::GeneralPurpose::new(
955                &base64::alphabet::STANDARD,
956                base64::engine::GeneralPurposeConfig::new(),
957            );
958            let bytes = base64.decode(&value.tx).map_err(Error::conversion)?;
959            Some(Psbt::deserialize(&bytes).map_err(Error::conversion)?)
960        };
961
962        let checkpoint_txs = value
963            .checkpoint_txs
964            .into_iter()
965            .map(|(k, v)| {
966                let out_point = OutPoint::from_str(k.as_str()).map_err(Error::conversion)?;
967                let txid = v.txid.parse().map_err(Error::conversion)?;
968                Ok((out_point, txid))
969            })
970            .collect::<Result<HashMap<_, _>, Error>>()?;
971
972        let swept_vtxos = value
973            .swept_vtxos
974            .into_iter()
975            .map(OutPoint::try_from)
976            .collect::<Result<Vec<_>, _>>()?;
977
978        Ok(ArkTransaction {
979            txid: Txid::from_str(value.txid.as_str()).map_err(Error::conversion)?,
980            tx,
981            spent_vtxos,
982            unspent_vtxos: spendable_vtxos,
983            checkpoint_txs,
984            swept_vtxos,
985        })
986    }
987}
988
989impl TryFrom<Outpoint> for OutPoint {
990    type Error = Error;
991
992    fn try_from(value: Outpoint) -> Result<Self, Self::Error> {
993        let point = OutPoint {
994            txid: Txid::from_str(value.txid.as_str()).map_err(Error::conversion)?,
995            vout: value.vout,
996        };
997        Ok(point)
998    }
999}
1000
1001pub struct VtxoChainResponse {
1002    pub chains: VtxoChains,
1003    pub page: Option<IndexerPage>,
1004}
1005
1006pub struct ListVtxosResponse {
1007    pub vtxos: Vec<VirtualTxOutPoint>,
1008    pub page: Option<IndexerPage>,
1009}
1010
1011impl TryFrom<generated::ark::v1::GetVtxoChainResponse> for VtxoChainResponse {
1012    type Error = Error;
1013
1014    fn try_from(value: generated::ark::v1::GetVtxoChainResponse) -> Result<Self, Self::Error> {
1015        let chains = value
1016            .chain
1017            .iter()
1018            .map(VtxoChain::try_from)
1019            .collect::<Result<Vec<_>, Error>>()?;
1020
1021        Ok(VtxoChainResponse {
1022            chains: VtxoChains { inner: chains },
1023            page: value
1024                .page
1025                .map(IndexerPage::try_from)
1026                .transpose()
1027                .map_err(Error::conversion)?,
1028        })
1029    }
1030}
1031
1032impl TryFrom<generated::ark::v1::GetVirtualTxsResponse> for VirtualTxsResponse {
1033    type Error = Error;
1034
1035    fn try_from(value: generated::ark::v1::GetVirtualTxsResponse) -> Result<Self, Self::Error> {
1036        let base64 = &base64::engine::GeneralPurpose::new(
1037            &base64::alphabet::STANDARD,
1038            base64::engine::GeneralPurposeConfig::new(),
1039        );
1040
1041        let txs = value
1042            .txs
1043            .into_iter()
1044            .map(|tx| {
1045                let bytes = base64.decode(&tx).map_err(Error::conversion)?;
1046                let psbt = Psbt::deserialize(&bytes).map_err(Error::conversion)?;
1047
1048                Ok(psbt)
1049            })
1050            .collect::<Result<Vec<_>, _>>()?;
1051
1052        Ok(VirtualTxsResponse {
1053            txs,
1054            page: value
1055                .page
1056                .map(IndexerPage::try_from)
1057                .transpose()
1058                .map_err(Error::conversion)?,
1059        })
1060    }
1061}
1062
1063impl TryFrom<&generated::ark::v1::IndexerChain> for VtxoChain {
1064    type Error = Error;
1065
1066    fn try_from(value: &generated::ark::v1::IndexerChain) -> Result<Self, Self::Error> {
1067        let spends = value
1068            .spends
1069            .iter()
1070            .map(|txid| {
1071                // Handle the case where txid might be 66 bytes long by trimming the last 2 bytes.
1072                let txid_str = if txid.len() == 66 { &txid[..64] } else { txid };
1073                txid_str.parse().map_err(Error::conversion)
1074            })
1075            .collect::<Result<Vec<_>, Error>>()?;
1076
1077        let tx_type = match value.r#type() {
1078            IndexerChainedTxType::Unspecified => ChainedTxType::Unspecified,
1079            IndexerChainedTxType::Commitment => ChainedTxType::Commitment,
1080            IndexerChainedTxType::Ark => ChainedTxType::Ark,
1081            IndexerChainedTxType::Tree => ChainedTxType::Tree,
1082            IndexerChainedTxType::Checkpoint => ChainedTxType::Checkpoint,
1083        };
1084
1085        Ok(VtxoChain {
1086            txid: value.txid.parse().map_err(Error::conversion)?,
1087            tx_type,
1088            spends,
1089            expires_at: value.expires_at,
1090        })
1091    }
1092}
1093
1094impl From<generated::ark::v1::IndexerPageResponse> for IndexerPage {
1095    fn from(value: generated::ark::v1::IndexerPageResponse) -> Self {
1096        IndexerPage {
1097            current: value.current,
1098            next: value.next,
1099            total: value.total,
1100        }
1101    }
1102}
1103
1104impl TryFrom<&generated::ark::v1::IndexerTxHistoryRecord> for history::Transaction {
1105    type Error = Error;
1106
1107    fn try_from(value: &generated::ark::v1::IndexerTxHistoryRecord) -> Result<Self, Self::Error> {
1108        let sign = match value.r#type() {
1109            generated::ark::v1::IndexerTxType::Received => 1,
1110            // Default to sent if unspecified.
1111            generated::ark::v1::IndexerTxType::Sent
1112            | generated::ark::v1::IndexerTxType::Unspecified => -1,
1113        };
1114
1115        let amount = SignedAmount::from_sat(value.amount as i64 * sign);
1116
1117        let tx = match &value.key {
1118            Some(Key::CommitmentTxid(txid)) => history::Transaction::Commitment {
1119                txid: txid.parse().map_err(Error::conversion)?,
1120                amount,
1121                created_at: value.created_at,
1122            },
1123            Some(Key::VirtualTxid(txid)) => history::Transaction::Ark {
1124                txid: txid.parse().map_err(Error::conversion)?,
1125                amount,
1126                is_settled: value.is_settled,
1127                created_at: value.created_at,
1128            },
1129            None => return Err(Error::conversion("invalid transaction without key")),
1130        };
1131
1132        Ok(tx)
1133    }
1134}
1135
1136impl TryFrom<generated::ark::v1::GetSubscriptionResponse> for SubscriptionResponse {
1137    type Error = Error;
1138
1139    fn try_from(value: generated::ark::v1::GetSubscriptionResponse) -> Result<Self, Self::Error> {
1140        let value = match value.data {
1141            Some(get_subscription_response::Data::Heartbeat(_)) => return Ok(Self::Heartbeat),
1142            Some(get_subscription_response::Data::Event(event)) => event,
1143            None => return Err(Error::conversion("empty subscription response")),
1144        };
1145
1146        let txid = value.txid.parse().map_err(Error::conversion)?;
1147
1148        let new_vtxos = value
1149            .new_vtxos
1150            .iter()
1151            .map(VirtualTxOutPoint::try_from)
1152            .collect::<Result<Vec<_>, _>>()?;
1153
1154        let spent_vtxos = value
1155            .spent_vtxos
1156            .iter()
1157            .map(VirtualTxOutPoint::try_from)
1158            .collect::<Result<Vec<_>, _>>()?;
1159
1160        let tx = if value.tx.is_empty() {
1161            None
1162        } else {
1163            match Vec::from_hex(&value.tx)
1164                .ok()
1165                .and_then(|bytes| bitcoin::consensus::deserialize::<Transaction>(&bytes).ok())
1166            {
1167                Some(raw_tx) => Some(raw_tx),
1168                None => {
1169                    let base64 = base64::engine::GeneralPurpose::new(
1170                        &base64::alphabet::STANDARD,
1171                        base64::engine::GeneralPurposeConfig::new(),
1172                    );
1173                    let bytes = base64.decode(&value.tx).map_err(|e| {
1174                        let tx_prefix = value.tx.chars().take(24).collect::<String>();
1175                        Error::conversion(format!(
1176                            "invalid subscription tx payload (not hex tx or base64 psbt) (len={}, tx_prefix='{}'): {e}",
1177                            value.tx.len(),
1178                            tx_prefix
1179                        ))
1180                    })?;
1181                    let psbt = Psbt::deserialize(&bytes).map_err(|e| {
1182                        let tx_prefix = value.tx.chars().take(24).collect::<String>();
1183                        Error::conversion(format!(
1184                            "invalid subscription tx payload (base64 but not psbt) (len={}, tx_prefix='{}'): {e}",
1185                            value.tx.len(),
1186                            tx_prefix
1187                        ))
1188                    })?;
1189                    Some(psbt.unsigned_tx)
1190                }
1191            }
1192        };
1193
1194        let checkpoint_txs = value
1195            .checkpoint_txs
1196            .into_iter()
1197            .map(|(k, v)| {
1198                let out_point = OutPoint::from_str(k.as_str()).map_err(Error::conversion)?;
1199                let txid = v.txid.parse().map_err(Error::conversion)?;
1200                Ok((out_point, txid))
1201            })
1202            .collect::<Result<HashMap<_, _>, Error>>()?;
1203
1204        let scripts = value
1205            .scripts
1206            .iter()
1207            .map(|h| ScriptBuf::from_hex(h).map_err(Error::conversion))
1208            .collect::<Result<Vec<_>, _>>()?;
1209
1210        Ok(Self::Event(Box::new(SubscriptionEvent {
1211            txid,
1212            scripts,
1213            new_vtxos,
1214            spent_vtxos,
1215            tx,
1216            checkpoint_txs,
1217        })))
1218    }
1219}
1220
1221impl From<GetVtxosRequest> for generated::ark::v1::GetVtxosRequest {
1222    fn from(value: GetVtxosRequest) -> Self {
1223        let (spendable_only, spent_only, recoverable_only, pending_only) = match value.filter() {
1224            Some(GetVtxosRequestFilter::Spendable) => (true, false, false, false),
1225            Some(GetVtxosRequestFilter::Spent) => (false, true, false, false),
1226            Some(GetVtxosRequestFilter::Recoverable) => (false, false, true, false),
1227            Some(GetVtxosRequestFilter::PendingOnly) => (false, false, false, true),
1228            None => (false, false, false, false),
1229        };
1230
1231        let page = value
1232            .page()
1233            .map(|p| generated::ark::v1::IndexerPageRequest {
1234                size: p.size,
1235                index: p.index,
1236            });
1237
1238        match value.reference() {
1239            GetVtxosRequestReference::Scripts(script_bufs) => Self {
1240                scripts: script_bufs.iter().map(|s| s.to_hex_string()).collect(),
1241                outpoints: Vec::new(),
1242                spendable_only,
1243                spent_only,
1244                recoverable_only,
1245                page,
1246                pending_only,
1247                after: value.after().unwrap_or(0) as i64,
1248                before: value.before().unwrap_or(0) as i64,
1249            },
1250            GetVtxosRequestReference::OutPoints(outpoints) => Self {
1251                scripts: Vec::new(),
1252                outpoints: outpoints.iter().map(|o| o.to_string()).collect(),
1253                spendable_only,
1254                spent_only,
1255                recoverable_only,
1256                page,
1257                pending_only,
1258                after: value.after().unwrap_or(0) as i64,
1259                before: value.before().unwrap_or(0) as i64,
1260            },
1261        }
1262    }
1263}