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 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 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 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 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 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 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 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 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}