1use crate::asset::AssetId;
4use crate::tx_graph::TxGraphChunk;
5use crate::ArkAddress;
6use crate::Error;
7use crate::ErrorContext;
8use bitcoin::hex::DisplayHex;
9use bitcoin::secp256k1::PublicKey;
10use bitcoin::taproot::Signature;
11use bitcoin::Amount;
12use bitcoin::OutPoint;
13use bitcoin::Psbt;
14use bitcoin::ScriptBuf;
15use bitcoin::Transaction;
16use bitcoin::Txid;
17use bitcoin::XOnlyPublicKey;
18use musig::musig;
19use std::collections::BTreeMap;
20use std::collections::HashMap;
21use std::str::FromStr;
22
23#[derive(Debug, Clone)]
25pub struct NoncePks(HashMap<Txid, musig::PublicNonce>);
26
27impl NoncePks {
28 pub fn new(nonce_pks: HashMap<Txid, musig::PublicNonce>) -> Self {
29 Self(nonce_pks)
30 }
31
32 pub fn get(&self, txid: &Txid) -> Option<musig::PublicNonce> {
34 self.0.get(txid).copied()
35 }
36
37 pub fn encode(&self) -> HashMap<String, String> {
38 self.0
39 .iter()
40 .map(|(k, v)| (k.to_string(), v.serialize().to_lower_hex_string()))
41 .collect()
42 }
43
44 pub fn decode(map: HashMap<String, String>) -> Result<Self, Error> {
45 let map = map
46 .into_iter()
47 .map(|(k, v)| {
48 let key = k
49 .parse()
50 .map_err(Error::ad_hoc)
51 .context("failed to parse TXID")?;
52
53 let value = {
54 let nonce_bytes = bitcoin::hex::FromHex::from_hex(&v)
55 .map_err(Error::ad_hoc)
56 .context("failed to decode public nonce from hex")?;
57 musig::PublicNonce::from_byte_array(&nonce_bytes)
58 .map_err(Error::ad_hoc)
59 .context("failed to decode public nonce from bytes")?
60 };
61
62 Ok((key, value))
63 })
64 .collect::<Result<HashMap<Txid, musig::PublicNonce>, Error>>()?;
65
66 Ok(Self(map))
67 }
68}
69
70#[derive(Debug, Clone)]
73pub struct TreeTxNoncePks(pub HashMap<XOnlyPublicKey, musig::PublicNonce>);
74
75impl TreeTxNoncePks {
76 pub fn new(tree_nonce_pks: HashMap<XOnlyPublicKey, musig::PublicNonce>) -> Self {
77 Self(tree_nonce_pks)
78 }
79
80 pub fn to_pks(&self) -> Vec<musig::PublicNonce> {
81 self.0.values().copied().collect()
82 }
83
84 pub fn encode(&self) -> HashMap<String, String> {
85 self.0
86 .iter()
87 .map(|(k, v)| (k.to_string(), v.serialize().to_lower_hex_string()))
88 .collect()
89 }
90
91 pub fn decode(map: HashMap<String, String>) -> Result<Self, Error> {
92 let map = map
93 .into_iter()
94 .map(|(k, v)| {
95 let key = k
96 .parse()
97 .map_err(Error::ad_hoc)
98 .context("failed to parse PK")?;
99
100 let value = {
101 let nonce_bytes = bitcoin::hex::FromHex::from_hex(&v)
102 .map_err(Error::ad_hoc)
103 .context("failed to decode public nonce from hex")?;
104 musig::PublicNonce::from_byte_array(&nonce_bytes)
105 .map_err(Error::ad_hoc)
106 .context("failed to decode public nonce from bytes")?
107 };
108
109 Ok((key, value))
110 })
111 .collect::<Result<HashMap<XOnlyPublicKey, musig::PublicNonce>, Error>>()?;
112
113 Ok(Self(map))
114 }
115}
116
117#[derive(Debug, Clone, Default)]
119pub struct PartialSigTree(pub HashMap<Txid, musig::PartialSignature>);
120
121impl PartialSigTree {
122 pub fn encode(&self) -> HashMap<String, String> {
123 self.0
124 .iter()
125 .map(|(k, v)| (k.to_string(), v.serialize().to_lower_hex_string()))
126 .collect()
127 }
128
129 pub fn decode(map: HashMap<String, String>) -> Result<Self, Error> {
130 let map = map
131 .into_iter()
132 .map(|(k, v)| {
133 let key = k
134 .parse()
135 .map_err(Error::ad_hoc)
136 .context("failed to parse TXID")?;
137
138 let value = {
139 let sig_bytes = bitcoin::hex::FromHex::from_hex(&v)
140 .map_err(Error::ad_hoc)
141 .context("failed to decode partial signature from hex")?;
142 musig::PartialSignature::from_byte_array(&sig_bytes)
143 .map_err(Error::ad_hoc)
144 .context("failed to decode partial signature from bytes")?
145 };
146
147 Ok((key, value))
148 })
149 .collect::<Result<HashMap<Txid, musig::PartialSignature>, Error>>()?;
150
151 Ok(Self(map))
152 }
153}
154
155#[derive(Debug, Clone, Default)]
156pub struct TxTree {
157 pub nodes: BTreeMap<(usize, usize), TxTreeNode>,
158}
159
160impl TxTree {
161 pub fn new() -> Self {
162 Self {
163 nodes: BTreeMap::new(),
164 }
165 }
166
167 pub fn get_mut(&mut self, level: usize, index: usize) -> Result<&mut TxTreeNode, Error> {
168 self.nodes
169 .get_mut(&(level, index))
170 .ok_or_else(|| Error::ad_hoc("TxTreeNode not found at ({level}, {index})"))
171 }
172
173 pub fn insert(&mut self, node: TxTreeNode, level: usize, index: usize) {
174 self.nodes.insert((level, index), node);
175 }
176
177 pub fn txs(&self) -> impl Iterator<Item = &Transaction> {
178 self.nodes.values().map(|node| &node.tx.unsigned_tx)
179 }
180
181 pub fn get_level(&self, level: usize) -> Vec<&TxTreeNode> {
183 self.nodes
184 .range((level, 0)..(level + 1, 0))
185 .map(|(_, node)| node)
186 .collect()
187 }
188
189 pub fn iter_levels(&self) -> impl Iterator<Item = (usize, Vec<&TxTreeNode>)> {
191 let max_level = self
192 .nodes
193 .keys()
194 .map(|(level, _)| *level)
195 .max()
196 .unwrap_or(0);
197
198 (0..=max_level).map(move |level| {
199 let nodes = self.get_level(level);
200 (level, nodes)
201 })
202 }
203}
204
205#[derive(Debug, Clone)]
206pub struct TxTreeNode {
207 pub txid: Txid,
208 pub tx: Psbt,
209 pub parent_txid: Txid,
210 pub level: i32,
211 pub level_index: i32,
212 pub leaf: bool,
213}
214
215#[derive(Clone)]
216pub struct GetVtxosRequest {
217 reference: GetVtxosRequestReference,
218 filter: Option<GetVtxosRequestFilter>,
219 page: Option<PageRequest>,
220 before: Option<u64>,
221 after: Option<u64>,
222}
223
224#[derive(Debug, Clone, Copy)]
226pub struct PageRequest {
227 pub size: i32,
229 pub index: i32,
231}
232
233impl GetVtxosRequest {
234 pub fn new_for_addresses(addresses: impl Iterator<Item = ArkAddress>) -> Self {
235 let scripts = addresses
236 .flat_map(|a| [a.to_p2tr_script_pubkey()])
237 .collect();
238
239 Self {
240 reference: GetVtxosRequestReference::Scripts(scripts),
241 filter: None,
242 page: None,
243 before: None,
244 after: None,
245 }
246 }
247
248 pub fn new_for_outpoints(outpoints: &[OutPoint]) -> Self {
249 Self {
250 reference: GetVtxosRequestReference::OutPoints(outpoints.to_vec()),
251 filter: None,
252 page: None,
253 before: None,
254 after: None,
255 }
256 }
257
258 pub fn spendable_only(self) -> Result<Self, Error> {
259 if self.filter.is_some() {
260 return Err(Error::ad_hoc("GetVtxosRequest filter already set"));
261 }
262
263 Ok(Self {
264 filter: Some(GetVtxosRequestFilter::Spendable),
265 ..self
266 })
267 }
268
269 pub fn spent_only(self) -> Result<Self, Error> {
270 if self.filter.is_some() {
271 return Err(Error::ad_hoc("GetVtxosRequest filter already set"));
272 }
273
274 Ok(Self {
275 filter: Some(GetVtxosRequestFilter::Spent),
276 ..self
277 })
278 }
279
280 pub fn recoverable_only(self) -> Result<Self, Error> {
281 if self.filter.is_some() {
282 return Err(Error::ad_hoc("GetVtxosRequest filter already set"));
283 }
284
285 Ok(Self {
286 filter: Some(GetVtxosRequestFilter::Recoverable),
287 ..self
288 })
289 }
290
291 pub fn pending_only(self) -> Result<Self, Error> {
292 if self.filter.is_some() {
293 return Err(Error::ad_hoc("GetVtxosRequest filter already set"));
294 }
295
296 Ok(Self {
297 filter: Some(GetVtxosRequestFilter::PendingOnly),
298 ..self
299 })
300 }
301
302 pub fn reference(&self) -> &GetVtxosRequestReference {
303 &self.reference
304 }
305
306 pub fn filter(&self) -> Option<&GetVtxosRequestFilter> {
307 self.filter.as_ref()
308 }
309
310 pub fn with_page(self, size: i32, index: i32) -> Self {
311 Self {
312 page: Some(PageRequest { size, index }),
313 ..self
314 }
315 }
316
317 pub fn page(&self) -> Option<PageRequest> {
318 self.page
319 }
320
321 pub fn with_before(self, before: u64) -> Self {
322 Self {
323 before: Some(before),
324 ..self
325 }
326 }
327
328 pub fn with_after(self, after: u64) -> Self {
329 Self {
330 after: Some(after),
331 ..self
332 }
333 }
334
335 pub fn before(&self) -> Option<u64> {
336 self.before
337 }
338 pub fn after(&self) -> Option<u64> {
339 self.after
340 }
341}
342
343#[derive(Clone)]
344pub enum GetVtxosRequestReference {
345 Scripts(Vec<ScriptBuf>),
346 OutPoints(Vec<OutPoint>),
347}
348
349impl GetVtxosRequestReference {
350 pub fn is_empty(&self) -> bool {
351 match self {
352 GetVtxosRequestReference::Scripts(script_bufs) => script_bufs.is_empty(),
353 GetVtxosRequestReference::OutPoints(outpoints) => outpoints.is_empty(),
354 }
355 }
356}
357
358#[derive(Clone, Copy)]
359pub enum GetVtxosRequestFilter {
360 Spendable,
361 Spent,
362 Recoverable,
363 PendingOnly,
364}
365
366#[derive(Clone, Debug, PartialEq)]
367pub struct VirtualTxOutPoint {
368 pub outpoint: OutPoint,
369 pub created_at: i64,
370 pub expires_at: i64,
371 pub amount: Amount,
372 pub script: ScriptBuf,
373 pub is_preconfirmed: bool,
375 pub is_swept: bool,
376 pub is_unrolled: bool,
377 pub is_spent: bool,
378 pub spent_by: Option<Txid>,
383 pub commitment_txids: Vec<Txid>,
385 pub settled_by: Option<Txid>,
387 pub ark_txid: Option<Txid>,
389 pub assets: Vec<Asset>,
391}
392
393impl VirtualTxOutPoint {
394 pub fn is_recoverable(&self, dust: Amount) -> bool {
400 if self.is_spent {
401 return false;
402 }
403
404 self.amount < dust || self.is_swept || self.is_expired()
405 }
406
407 pub fn is_expired(&self) -> bool {
415 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
416 let current_timestamp = std::time::SystemTime::now()
417 .duration_since(std::time::UNIX_EPOCH)
418 .expect("valid duration")
419 .as_secs() as i64;
420
421 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
422 let current_timestamp = (js_sys::Date::now() / 1000.0) as i64;
423
424 current_timestamp > self.expires_at && !self.is_swept && !self.is_spent
425 }
426}
427
428#[derive(Clone, Debug)]
429pub struct Info {
430 pub version: String,
431 pub signer_pk: PublicKey,
432 pub forfeit_pk: PublicKey,
433 pub forfeit_address: bitcoin::Address,
434 pub checkpoint_tapscript: ScriptBuf,
435 pub network: bitcoin::Network,
436 pub session_duration: u64,
437 pub unilateral_exit_delay: bitcoin::Sequence,
438 pub boarding_exit_delay: bitcoin::Sequence,
439 pub utxo_min_amount: Option<Amount>,
440 pub utxo_max_amount: Option<Amount>,
441 pub vtxo_min_amount: Option<Amount>,
442 pub vtxo_max_amount: Option<Amount>,
443 pub dust: Amount,
444 pub fees: Option<FeeInfo>,
445 pub scheduled_session: Option<ScheduledSession>,
446 pub deprecated_signers: Vec<DeprecatedSigner>,
447 pub service_status: HashMap<String, String>,
448 pub digest: String,
449 pub max_tx_weight: i64,
450 pub max_op_return_outputs: i64,
451}
452
453#[derive(Clone, Debug)]
455pub struct FeeInfo {
456 pub intent_fee: IntentFeeInfo,
457 pub tx_fee_rate: String,
458}
459
460#[derive(Clone, Debug, Default)]
465pub struct IntentFeeInfo {
466 pub offchain_input: Option<String>,
467 pub offchain_output: Option<String>,
468 pub onchain_input: Option<String>,
469 pub onchain_output: Option<String>,
470}
471
472#[derive(Clone, Debug)]
473pub struct ScheduledSession {
474 pub next_start_time: i64,
475 pub next_end_time: i64,
476 pub period: i64,
477 pub duration: i64,
478 pub fees: Option<FeeInfo>,
479}
480
481#[derive(Clone, Debug)]
482pub struct DeprecatedSigner {
483 pub pk: PublicKey,
484 pub cutoff_date: i64,
485}
486
487#[derive(Debug, Clone)]
488pub struct StreamStartedEvent {
489 pub id: String,
490}
491
492#[derive(Debug, Clone)]
493pub struct BatchStartedEvent {
494 pub id: String,
495 pub intent_id_hashes: Vec<String>,
496 pub batch_expiry: bitcoin::Sequence,
497}
498
499#[derive(Debug, Clone)]
500pub struct BatchFinalizationEvent {
501 pub id: String,
502 pub commitment_tx: Psbt,
503}
504
505#[derive(Debug, Clone)]
506pub struct BatchFinalizedEvent {
507 pub id: String,
508 pub commitment_txid: Txid,
509}
510
511#[derive(Debug, Clone)]
512pub struct BatchFailed {
513 pub id: String,
514 pub reason: String,
515}
516
517#[derive(Debug, Clone)]
518pub struct TreeSigningStartedEvent {
519 pub id: String,
520 pub cosigners_pubkeys: Vec<PublicKey>,
521 pub unsigned_commitment_tx: Psbt,
522}
523
524#[derive(Debug, Clone)]
525pub struct TreeNoncesAggregatedEvent {
526 pub id: String,
527 pub tree_nonces: NoncePks,
528}
529
530#[derive(Debug, Clone)]
531pub struct TreeTxEvent {
532 pub id: String,
533 pub topic: Vec<String>,
534 pub batch_tree_event_type: BatchTreeEventType,
535 pub tx_graph_chunk: TxGraphChunk,
536}
537
538#[derive(Debug, Clone)]
539pub struct TreeSignatureEvent {
540 pub id: String,
541 pub topic: Vec<String>,
542 pub batch_tree_event_type: BatchTreeEventType,
543 pub txid: Txid,
544 pub signature: Signature,
545}
546
547#[derive(Debug, Clone)]
548pub struct TreeNoncesEvent {
549 pub id: String,
550 pub topic: Vec<String>,
551 pub txid: Txid,
552 pub nonces: TreeTxNoncePks,
553}
554
555#[derive(Debug, Clone)]
556pub enum BatchTreeEventType {
557 Vtxo,
558 Connector,
559}
560
561#[derive(Debug, Clone)]
562pub enum StreamEvent {
563 StreamStarted(StreamStartedEvent),
564 BatchStarted(BatchStartedEvent),
565 BatchFinalization(BatchFinalizationEvent),
566 BatchFinalized(BatchFinalizedEvent),
567 BatchFailed(BatchFailed),
568 TreeSigningStarted(TreeSigningStartedEvent),
569 TreeNoncesAggregated(TreeNoncesAggregatedEvent),
570 TreeTx(TreeTxEvent),
571 TreeSignature(TreeSignatureEvent),
572 TreeNonces(TreeNoncesEvent),
573 Heartbeat,
574}
575
576impl StreamEvent {
577 pub fn name(&self) -> String {
578 let s = match self {
579 StreamEvent::StreamStarted(_) => "StreamStarted",
580 StreamEvent::BatchStarted(_) => "BatchStarted",
581 StreamEvent::BatchFinalization(_) => "BatchFinalization",
582 StreamEvent::BatchFinalized(_) => "BatchFinalized",
583 StreamEvent::BatchFailed(_) => "BatchFailed",
584 StreamEvent::TreeSigningStarted(_) => "TreeSigningStarted",
585 StreamEvent::TreeNoncesAggregated(_) => "TreeNoncesAggregated",
586 StreamEvent::TreeTx(_) => "TreeTx",
587 StreamEvent::TreeSignature(_) => "TreeSignature",
588 StreamEvent::TreeNonces(_) => "TreeNoncesEvent",
589 StreamEvent::Heartbeat => "Heartbeat",
590 };
591
592 s.to_string()
593 }
594}
595
596pub enum StreamTransactionData {
597 Commitment(CommitmentTransaction),
598 Ark(ArkTransaction),
599 Heartbeat,
600}
601
602pub struct ArkTransaction {
603 pub txid: Txid,
604 pub tx: Option<Psbt>,
605 pub spent_vtxos: Vec<VirtualTxOutPoint>,
606 pub unspent_vtxos: Vec<VirtualTxOutPoint>,
607 pub checkpoint_txs: HashMap<OutPoint, Txid>,
609 pub swept_vtxos: Vec<OutPoint>,
610}
611
612pub struct CommitmentTransaction {
613 pub txid: Txid,
614 pub spent_vtxos: Vec<VirtualTxOutPoint>,
615 pub unspent_vtxos: Vec<VirtualTxOutPoint>,
616}
617
618#[derive(Clone, Debug)]
619pub enum SubscriptionResponse {
620 Event(Box<SubscriptionEvent>),
621 Heartbeat,
622}
623
624#[derive(Clone, Debug)]
625pub struct SubscriptionEvent {
626 pub txid: Txid,
627 pub scripts: Vec<ScriptBuf>,
628 pub new_vtxos: Vec<VirtualTxOutPoint>,
629 pub spent_vtxos: Vec<VirtualTxOutPoint>,
630 pub tx: Option<Transaction>,
631 pub checkpoint_txs: HashMap<OutPoint, Txid>,
632}
633
634pub struct VtxoChains {
635 pub inner: Vec<VtxoChain>,
636}
637
638pub struct VtxoChain {
639 pub txid: Txid,
640 pub tx_type: ChainedTxType,
641 pub spends: Vec<Txid>,
642 pub expires_at: i64,
643}
644
645#[derive(Debug)]
646pub enum ChainedTxType {
647 Commitment,
648 Tree,
649 Checkpoint,
650 Ark,
651 Unspecified,
652}
653
654pub struct SubmitOffchainTxResponse {
655 pub signed_ark_tx: Psbt,
656 pub signed_checkpoint_txs: Vec<Psbt>,
657}
658
659#[derive(Debug, Clone)]
660pub struct PendingTx {
661 pub ark_txid: Txid,
662 pub signed_ark_tx: Psbt,
663 pub signed_checkpoint_txs: Vec<Psbt>,
664}
665
666#[derive(Debug, Clone)]
667pub struct FinalizeOffchainTxResponse {}
668
669#[derive(Debug)]
670pub struct VirtualTxsResponse {
671 pub txs: Vec<Psbt>,
672 pub page: Option<IndexerPage>,
673}
674
675#[derive(Debug)]
676pub struct IndexerPage {
677 pub current: i32,
678 pub next: i32,
679 pub total: i32,
680}
681
682#[derive(Clone, Debug)]
683pub enum Network {
684 Bitcoin,
685 Testnet,
686 Testnet4,
687 Signet,
688 Regtest,
689 Mutinynet,
690}
691
692#[derive(Clone, Debug, PartialEq, Eq, Hash)]
694pub struct Asset {
695 pub asset_id: AssetId,
696 pub amount: u64,
697}
698
699#[derive(Clone, Debug, PartialEq, Eq)]
701pub struct AssetInfo {
702 pub asset_id: AssetId,
703 pub control_asset_id: Option<AssetId>,
704 pub supply: u64,
705 pub metadata: String,
706}
707
708impl AssetInfo {
709 pub fn can_be_reissued(&self) -> bool {
710 self.control_asset_id.is_some()
711 }
712}
713
714impl From<Network> for bitcoin::Network {
715 fn from(value: Network) -> Self {
716 match value {
717 Network::Bitcoin => bitcoin::Network::Bitcoin,
718 Network::Testnet => bitcoin::Network::Testnet,
719 Network::Testnet4 => bitcoin::Network::Testnet4,
720 Network::Signet => bitcoin::Network::Signet,
721 Network::Regtest => bitcoin::Network::Regtest,
722 Network::Mutinynet => bitcoin::Network::Signet,
723 }
724 }
725}
726
727impl FromStr for Network {
728 type Err = String;
729
730 #[inline]
731 fn from_str(s: &str) -> Result<Self, Self::Err> {
732 match s {
733 "bitcoin" => Ok(Network::Bitcoin),
734 "testnet" => Ok(Network::Testnet),
735 "testnet4" => Ok(Network::Testnet4),
736 "signet" => Ok(Network::Signet),
737 "regtest" => Ok(Network::Regtest),
738 "mutinynet" => Ok(Network::Mutinynet),
739 _ => Err(format!("Unsupported network {}", s.to_owned())),
740 }
741 }
742}
743
744pub fn parse_sequence_number(value: i64) -> Result<bitcoin::Sequence, Error> {
745 const ARBITRARY_SEQUENCE_THRESHOLD: i64 = 512;
751
752 let sequence = if value.is_negative() {
753 return Err(Error::ad_hoc(format!("invalid sequence number: {value}")));
754 } else if value < ARBITRARY_SEQUENCE_THRESHOLD {
755 bitcoin::Sequence::from_height(value as u16)
756 } else {
757 let secs = u32::try_from(value)
758 .map_err(|_| Error::ad_hoc(format!("sequence seconds overflow: {value}")))?;
759
760 bitcoin::Sequence::from_seconds_ceil(secs).map_err(Error::ad_hoc)?
761 };
762
763 Ok(sequence)
764}
765
766pub fn parse_fee_amount(amount_str: Option<String>) -> Amount {
768 amount_str
769 .and_then(|s| {
770 if s.is_empty() {
771 None
772 } else {
773 s.parse::<u64>().ok()
774 }
775 })
776 .map(Amount::from_sat)
777 .unwrap_or(Amount::ZERO)
778}