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,
376 pub is_swept: bool,
377 pub is_unrolled: bool,
378 pub is_spent: bool,
379 pub spent_by: Option<Txid>,
384 pub commitment_txids: Vec<Txid>,
386 pub settled_by: Option<Txid>,
388 pub ark_txid: Option<Txid>,
390 pub assets: Vec<Asset>,
392}
393
394impl VirtualTxOutPoint {
395 pub fn is_recoverable(&self, dust: Amount) -> bool {
401 if self.is_spent {
402 return false;
403 }
404
405 self.amount < dust || self.is_swept || self.is_expired()
406 }
407
408 pub fn is_expired(&self) -> bool {
416 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
417 let current_timestamp = std::time::SystemTime::now()
418 .duration_since(std::time::UNIX_EPOCH)
419 .expect("valid duration")
420 .as_secs() as i64;
421
422 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
423 let current_timestamp = (js_sys::Date::now() / 1000.0) as i64;
424
425 current_timestamp > self.expires_at && !self.is_swept && !self.is_spent
426 }
427}
428
429#[derive(Clone, Debug)]
430pub struct Info {
431 pub version: String,
432 pub signer_pk: PublicKey,
433 pub forfeit_pk: PublicKey,
434 pub forfeit_address: bitcoin::Address,
435 pub checkpoint_tapscript: ScriptBuf,
436 pub network: bitcoin::Network,
437 pub session_duration: u64,
438 pub unilateral_exit_delay: bitcoin::Sequence,
439 pub boarding_exit_delay: bitcoin::Sequence,
440 pub utxo_min_amount: Option<Amount>,
441 pub utxo_max_amount: Option<Amount>,
442 pub vtxo_min_amount: Option<Amount>,
443 pub vtxo_max_amount: Option<Amount>,
444 pub dust: Amount,
445 pub fees: Option<FeeInfo>,
446 pub scheduled_session: Option<ScheduledSession>,
447 pub deprecated_signers: Vec<DeprecatedSigner>,
448 pub service_status: HashMap<String, String>,
449 pub digest: String,
450 pub max_tx_weight: i64,
451 pub max_op_return_outputs: i64,
452}
453
454#[derive(Clone, Debug)]
456pub struct FeeInfo {
457 pub intent_fee: IntentFeeInfo,
458 pub tx_fee_rate: String,
459}
460
461#[derive(Clone, Debug, Default)]
466pub struct IntentFeeInfo {
467 pub offchain_input: Option<String>,
468 pub offchain_output: Option<String>,
469 pub onchain_input: Option<String>,
470 pub onchain_output: Option<String>,
471}
472
473#[derive(Clone, Debug)]
474pub struct ScheduledSession {
475 pub next_start_time: i64,
476 pub next_end_time: i64,
477 pub period: i64,
478 pub duration: i64,
479 pub fees: Option<FeeInfo>,
480}
481
482#[derive(Clone, Debug)]
483pub struct DeprecatedSigner {
484 pub pk: PublicKey,
485 pub cutoff_date: i64,
486}
487
488#[derive(Debug, Clone)]
489pub struct StreamStartedEvent {
490 pub id: String,
491}
492
493#[derive(Debug, Clone)]
494pub struct BatchStartedEvent {
495 pub id: String,
496 pub intent_id_hashes: Vec<String>,
497 pub batch_expiry: bitcoin::Sequence,
498}
499
500#[derive(Debug, Clone)]
501pub struct BatchFinalizationEvent {
502 pub id: String,
503 pub commitment_tx: Psbt,
504}
505
506#[derive(Debug, Clone)]
507pub struct BatchFinalizedEvent {
508 pub id: String,
509 pub commitment_txid: Txid,
510}
511
512#[derive(Debug, Clone)]
513pub struct BatchFailed {
514 pub id: String,
515 pub reason: String,
516}
517
518#[derive(Debug, Clone)]
519pub struct TreeSigningStartedEvent {
520 pub id: String,
521 pub cosigners_pubkeys: Vec<PublicKey>,
522 pub unsigned_commitment_tx: Psbt,
523}
524
525#[derive(Debug, Clone)]
526pub struct TreeNoncesAggregatedEvent {
527 pub id: String,
528 pub tree_nonces: NoncePks,
529}
530
531#[derive(Debug, Clone)]
532pub struct TreeTxEvent {
533 pub id: String,
534 pub topic: Vec<String>,
535 pub batch_tree_event_type: BatchTreeEventType,
536 pub tx_graph_chunk: TxGraphChunk,
537}
538
539#[derive(Debug, Clone)]
540pub struct TreeSignatureEvent {
541 pub id: String,
542 pub topic: Vec<String>,
543 pub batch_tree_event_type: BatchTreeEventType,
544 pub txid: Txid,
545 pub signature: Signature,
546}
547
548#[derive(Debug, Clone)]
549pub struct TreeNoncesEvent {
550 pub id: String,
551 pub topic: Vec<String>,
552 pub txid: Txid,
553 pub nonces: TreeTxNoncePks,
554}
555
556#[derive(Debug, Clone)]
557pub enum BatchTreeEventType {
558 Vtxo,
559 Connector,
560}
561
562#[derive(Debug, Clone)]
563pub enum StreamEvent {
564 StreamStarted(StreamStartedEvent),
565 BatchStarted(BatchStartedEvent),
566 BatchFinalization(BatchFinalizationEvent),
567 BatchFinalized(BatchFinalizedEvent),
568 BatchFailed(BatchFailed),
569 TreeSigningStarted(TreeSigningStartedEvent),
570 TreeNoncesAggregated(TreeNoncesAggregatedEvent),
571 TreeTx(TreeTxEvent),
572 TreeSignature(TreeSignatureEvent),
573 TreeNonces(TreeNoncesEvent),
574 Heartbeat,
575}
576
577impl StreamEvent {
578 pub fn name(&self) -> String {
579 let s = match self {
580 StreamEvent::StreamStarted(_) => "StreamStarted",
581 StreamEvent::BatchStarted(_) => "BatchStarted",
582 StreamEvent::BatchFinalization(_) => "BatchFinalization",
583 StreamEvent::BatchFinalized(_) => "BatchFinalized",
584 StreamEvent::BatchFailed(_) => "BatchFailed",
585 StreamEvent::TreeSigningStarted(_) => "TreeSigningStarted",
586 StreamEvent::TreeNoncesAggregated(_) => "TreeNoncesAggregated",
587 StreamEvent::TreeTx(_) => "TreeTx",
588 StreamEvent::TreeSignature(_) => "TreeSignature",
589 StreamEvent::TreeNonces(_) => "TreeNoncesEvent",
590 StreamEvent::Heartbeat => "Heartbeat",
591 };
592
593 s.to_string()
594 }
595}
596
597pub enum StreamTransactionData {
598 Commitment(CommitmentTransaction),
599 Ark(ArkTransaction),
600 Heartbeat,
601}
602
603pub struct ArkTransaction {
604 pub txid: Txid,
605 pub tx: Option<Psbt>,
606 pub spent_vtxos: Vec<VirtualTxOutPoint>,
607 pub unspent_vtxos: Vec<VirtualTxOutPoint>,
608 pub checkpoint_txs: HashMap<OutPoint, Txid>,
610 pub swept_vtxos: Vec<OutPoint>,
611}
612
613pub struct CommitmentTransaction {
614 pub txid: Txid,
615 pub spent_vtxos: Vec<VirtualTxOutPoint>,
616 pub unspent_vtxos: Vec<VirtualTxOutPoint>,
617}
618
619#[derive(Clone, Debug)]
620pub enum SubscriptionResponse {
621 Event(Box<SubscriptionEvent>),
622 Heartbeat,
623}
624
625#[derive(Clone, Debug)]
626pub struct SubscriptionEvent {
627 pub txid: Txid,
628 pub scripts: Vec<ScriptBuf>,
629 pub new_vtxos: Vec<VirtualTxOutPoint>,
630 pub spent_vtxos: Vec<VirtualTxOutPoint>,
631 pub tx: Option<Transaction>,
632 pub checkpoint_txs: HashMap<OutPoint, Txid>,
633}
634
635pub struct VtxoChains {
636 pub inner: Vec<VtxoChain>,
637}
638
639pub struct VtxoChain {
640 pub txid: Txid,
641 pub tx_type: ChainedTxType,
642 pub spends: Vec<Txid>,
643 pub expires_at: i64,
644}
645
646#[derive(Debug)]
647pub enum ChainedTxType {
648 Commitment,
649 Tree,
650 Checkpoint,
651 Ark,
652 Unspecified,
653}
654
655pub struct SubmitOffchainTxResponse {
656 pub signed_ark_tx: Psbt,
657 pub signed_checkpoint_txs: Vec<Psbt>,
658}
659
660#[derive(Debug, Clone)]
661pub struct PendingTx {
662 pub ark_txid: Txid,
663 pub signed_ark_tx: Psbt,
664 pub signed_checkpoint_txs: Vec<Psbt>,
665}
666
667#[derive(Debug, Clone)]
668pub struct FinalizeOffchainTxResponse {}
669
670#[derive(Debug)]
671pub struct VirtualTxsResponse {
672 pub txs: Vec<Psbt>,
673 pub page: Option<IndexerPage>,
674}
675
676#[derive(Debug)]
677pub struct IndexerPage {
678 pub current: i32,
679 pub next: i32,
680 pub total: i32,
681}
682
683#[derive(Clone, Debug)]
684pub enum Network {
685 Bitcoin,
686 Testnet,
687 Testnet4,
688 Signet,
689 Regtest,
690 Mutinynet,
691}
692
693#[derive(Clone, Debug, PartialEq, Eq, Hash)]
695pub struct Asset {
696 pub asset_id: AssetId,
697 pub amount: u64,
698}
699
700#[derive(Clone, Debug, PartialEq, Eq)]
702pub struct AssetInfo {
703 pub asset_id: AssetId,
704 pub control_asset_id: Option<AssetId>,
705 pub supply: u64,
706 pub metadata: String,
707}
708
709impl AssetInfo {
710 pub fn can_be_reissued(&self) -> bool {
711 self.control_asset_id.is_some()
712 }
713}
714
715impl From<Network> for bitcoin::Network {
716 fn from(value: Network) -> Self {
717 match value {
718 Network::Bitcoin => bitcoin::Network::Bitcoin,
719 Network::Testnet => bitcoin::Network::Testnet,
720 Network::Testnet4 => bitcoin::Network::Testnet4,
721 Network::Signet => bitcoin::Network::Signet,
722 Network::Regtest => bitcoin::Network::Regtest,
723 Network::Mutinynet => bitcoin::Network::Signet,
724 }
725 }
726}
727
728impl FromStr for Network {
729 type Err = String;
730
731 #[inline]
732 fn from_str(s: &str) -> Result<Self, Self::Err> {
733 match s {
734 "bitcoin" => Ok(Network::Bitcoin),
735 "testnet" => Ok(Network::Testnet),
736 "testnet4" => Ok(Network::Testnet4),
737 "signet" => Ok(Network::Signet),
738 "regtest" => Ok(Network::Regtest),
739 "mutinynet" => Ok(Network::Mutinynet),
740 _ => Err(format!("Unsupported network {}", s.to_owned())),
741 }
742 }
743}
744
745pub fn parse_sequence_number(value: i64) -> Result<bitcoin::Sequence, Error> {
746 const ARBITRARY_SEQUENCE_THRESHOLD: i64 = 512;
752
753 let sequence = if value.is_negative() {
754 return Err(Error::ad_hoc(format!("invalid sequence number: {value}")));
755 } else if value < ARBITRARY_SEQUENCE_THRESHOLD {
756 bitcoin::Sequence::from_height(value as u16)
757 } else {
758 let secs = u32::try_from(value)
759 .map_err(|_| Error::ad_hoc(format!("sequence seconds overflow: {value}")))?;
760
761 bitcoin::Sequence::from_seconds_ceil(secs).map_err(Error::ad_hoc)?
762 };
763
764 Ok(sequence)
765}
766
767pub fn parse_fee_amount(amount_str: Option<String>) -> Amount {
769 amount_str
770 .and_then(|s| {
771 if s.is_empty() {
772 None
773 } else {
774 s.parse::<u64>().ok()
775 }
776 })
777 .map(Amount::from_sat)
778 .unwrap_or(Amount::ZERO)
779}