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
23pub const TARGET_ARKD_VERSION: &str = "0.9.9";
32
33pub const SDK_VERSION: &str = concat!("rust-sdk/", env!("CARGO_PKG_VERSION"));
40
41#[derive(Debug, Clone)]
43pub struct NoncePks(HashMap<Txid, musig::PublicNonce>);
44
45impl NoncePks {
46 pub fn new(nonce_pks: HashMap<Txid, musig::PublicNonce>) -> Self {
47 Self(nonce_pks)
48 }
49
50 pub fn get(&self, txid: &Txid) -> Option<musig::PublicNonce> {
52 self.0.get(txid).copied()
53 }
54
55 pub fn encode(&self) -> HashMap<String, String> {
56 self.0
57 .iter()
58 .map(|(k, v)| (k.to_string(), v.serialize().to_lower_hex_string()))
59 .collect()
60 }
61
62 pub fn decode(map: HashMap<String, String>) -> Result<Self, Error> {
63 let map = map
64 .into_iter()
65 .map(|(k, v)| {
66 let key = k
67 .parse()
68 .map_err(Error::ad_hoc)
69 .context("failed to parse TXID")?;
70
71 let value = {
72 let nonce_bytes = bitcoin::hex::FromHex::from_hex(&v)
73 .map_err(Error::ad_hoc)
74 .context("failed to decode public nonce from hex")?;
75 musig::PublicNonce::from_byte_array(&nonce_bytes)
76 .map_err(Error::ad_hoc)
77 .context("failed to decode public nonce from bytes")?
78 };
79
80 Ok((key, value))
81 })
82 .collect::<Result<HashMap<Txid, musig::PublicNonce>, Error>>()?;
83
84 Ok(Self(map))
85 }
86}
87
88#[derive(Debug, Clone)]
91pub struct TreeTxNoncePks(pub HashMap<XOnlyPublicKey, musig::PublicNonce>);
92
93impl TreeTxNoncePks {
94 pub fn new(tree_nonce_pks: HashMap<XOnlyPublicKey, musig::PublicNonce>) -> Self {
95 Self(tree_nonce_pks)
96 }
97
98 pub fn to_pks(&self) -> Vec<musig::PublicNonce> {
99 self.0.values().copied().collect()
100 }
101
102 pub fn encode(&self) -> HashMap<String, String> {
103 self.0
104 .iter()
105 .map(|(k, v)| (k.to_string(), v.serialize().to_lower_hex_string()))
106 .collect()
107 }
108
109 pub fn decode(map: HashMap<String, String>) -> Result<Self, Error> {
110 let map = map
111 .into_iter()
112 .map(|(k, v)| {
113 let key = k
114 .parse()
115 .map_err(Error::ad_hoc)
116 .context("failed to parse PK")?;
117
118 let value = {
119 let nonce_bytes = bitcoin::hex::FromHex::from_hex(&v)
120 .map_err(Error::ad_hoc)
121 .context("failed to decode public nonce from hex")?;
122 musig::PublicNonce::from_byte_array(&nonce_bytes)
123 .map_err(Error::ad_hoc)
124 .context("failed to decode public nonce from bytes")?
125 };
126
127 Ok((key, value))
128 })
129 .collect::<Result<HashMap<XOnlyPublicKey, musig::PublicNonce>, Error>>()?;
130
131 Ok(Self(map))
132 }
133}
134
135#[derive(Debug, Clone, Default)]
137pub struct PartialSigTree(pub HashMap<Txid, musig::PartialSignature>);
138
139impl PartialSigTree {
140 pub fn encode(&self) -> HashMap<String, String> {
141 self.0
142 .iter()
143 .map(|(k, v)| (k.to_string(), v.serialize().to_lower_hex_string()))
144 .collect()
145 }
146
147 pub fn decode(map: HashMap<String, String>) -> Result<Self, Error> {
148 let map = map
149 .into_iter()
150 .map(|(k, v)| {
151 let key = k
152 .parse()
153 .map_err(Error::ad_hoc)
154 .context("failed to parse TXID")?;
155
156 let value = {
157 let sig_bytes = bitcoin::hex::FromHex::from_hex(&v)
158 .map_err(Error::ad_hoc)
159 .context("failed to decode partial signature from hex")?;
160 musig::PartialSignature::from_byte_array(&sig_bytes)
161 .map_err(Error::ad_hoc)
162 .context("failed to decode partial signature from bytes")?
163 };
164
165 Ok((key, value))
166 })
167 .collect::<Result<HashMap<Txid, musig::PartialSignature>, Error>>()?;
168
169 Ok(Self(map))
170 }
171}
172
173#[derive(Debug, Clone, Default)]
174pub struct TxTree {
175 pub nodes: BTreeMap<(usize, usize), TxTreeNode>,
176}
177
178impl TxTree {
179 pub fn new() -> Self {
180 Self {
181 nodes: BTreeMap::new(),
182 }
183 }
184
185 pub fn get_mut(&mut self, level: usize, index: usize) -> Result<&mut TxTreeNode, Error> {
186 self.nodes
187 .get_mut(&(level, index))
188 .ok_or_else(|| Error::ad_hoc("TxTreeNode not found at ({level}, {index})"))
189 }
190
191 pub fn insert(&mut self, node: TxTreeNode, level: usize, index: usize) {
192 self.nodes.insert((level, index), node);
193 }
194
195 pub fn txs(&self) -> impl Iterator<Item = &Transaction> {
196 self.nodes.values().map(|node| &node.tx.unsigned_tx)
197 }
198
199 pub fn get_level(&self, level: usize) -> Vec<&TxTreeNode> {
201 self.nodes
202 .range((level, 0)..(level + 1, 0))
203 .map(|(_, node)| node)
204 .collect()
205 }
206
207 pub fn iter_levels(&self) -> impl Iterator<Item = (usize, Vec<&TxTreeNode>)> {
209 let max_level = self
210 .nodes
211 .keys()
212 .map(|(level, _)| *level)
213 .max()
214 .unwrap_or(0);
215
216 (0..=max_level).map(move |level| {
217 let nodes = self.get_level(level);
218 (level, nodes)
219 })
220 }
221}
222
223#[derive(Debug, Clone)]
224pub struct TxTreeNode {
225 pub txid: Txid,
226 pub tx: Psbt,
227 pub parent_txid: Txid,
228 pub level: i32,
229 pub level_index: i32,
230 pub leaf: bool,
231}
232
233#[derive(Clone)]
234pub struct GetVtxosRequest {
235 reference: GetVtxosRequestReference,
236 filter: Option<GetVtxosRequestFilter>,
237 page: Option<PageRequest>,
238 before: Option<u64>,
239 after: Option<u64>,
240}
241
242#[derive(Debug, Clone, Copy)]
244pub struct PageRequest {
245 pub size: i32,
247 pub index: i32,
249}
250
251impl GetVtxosRequest {
252 pub fn new_for_addresses(addresses: impl Iterator<Item = ArkAddress>) -> Self {
253 let scripts = addresses
254 .flat_map(|a| [a.to_p2tr_script_pubkey()])
255 .collect();
256
257 Self {
258 reference: GetVtxosRequestReference::Scripts(scripts),
259 filter: None,
260 page: None,
261 before: None,
262 after: None,
263 }
264 }
265
266 pub fn new_for_outpoints(outpoints: &[OutPoint]) -> Self {
267 Self {
268 reference: GetVtxosRequestReference::OutPoints(outpoints.to_vec()),
269 filter: None,
270 page: None,
271 before: None,
272 after: None,
273 }
274 }
275
276 pub fn spendable_only(self) -> Result<Self, Error> {
277 if self.filter.is_some() {
278 return Err(Error::ad_hoc("GetVtxosRequest filter already set"));
279 }
280
281 Ok(Self {
282 filter: Some(GetVtxosRequestFilter::Spendable),
283 ..self
284 })
285 }
286
287 pub fn spent_only(self) -> Result<Self, Error> {
288 if self.filter.is_some() {
289 return Err(Error::ad_hoc("GetVtxosRequest filter already set"));
290 }
291
292 Ok(Self {
293 filter: Some(GetVtxosRequestFilter::Spent),
294 ..self
295 })
296 }
297
298 pub fn recoverable_only(self) -> Result<Self, Error> {
299 if self.filter.is_some() {
300 return Err(Error::ad_hoc("GetVtxosRequest filter already set"));
301 }
302
303 Ok(Self {
304 filter: Some(GetVtxosRequestFilter::Recoverable),
305 ..self
306 })
307 }
308
309 pub fn pending_only(self) -> Result<Self, Error> {
310 if self.filter.is_some() {
311 return Err(Error::ad_hoc("GetVtxosRequest filter already set"));
312 }
313
314 Ok(Self {
315 filter: Some(GetVtxosRequestFilter::PendingOnly),
316 ..self
317 })
318 }
319
320 pub fn reference(&self) -> &GetVtxosRequestReference {
321 &self.reference
322 }
323
324 pub fn filter(&self) -> Option<&GetVtxosRequestFilter> {
325 self.filter.as_ref()
326 }
327
328 pub fn with_page(self, size: i32, index: i32) -> Self {
329 Self {
330 page: Some(PageRequest { size, index }),
331 ..self
332 }
333 }
334
335 pub fn page(&self) -> Option<PageRequest> {
336 self.page
337 }
338
339 pub fn with_before(self, before: u64) -> Self {
340 Self {
341 before: Some(before),
342 ..self
343 }
344 }
345
346 pub fn with_after(self, after: u64) -> Self {
347 Self {
348 after: Some(after),
349 ..self
350 }
351 }
352
353 pub fn before(&self) -> Option<u64> {
354 self.before
355 }
356 pub fn after(&self) -> Option<u64> {
357 self.after
358 }
359}
360
361#[derive(Clone)]
362pub enum GetVtxosRequestReference {
363 Scripts(Vec<ScriptBuf>),
364 OutPoints(Vec<OutPoint>),
365}
366
367impl GetVtxosRequestReference {
368 pub fn is_empty(&self) -> bool {
369 match self {
370 GetVtxosRequestReference::Scripts(script_bufs) => script_bufs.is_empty(),
371 GetVtxosRequestReference::OutPoints(outpoints) => outpoints.is_empty(),
372 }
373 }
374}
375
376#[derive(Clone, Copy)]
377pub enum GetVtxosRequestFilter {
378 Spendable,
379 Spent,
380 Recoverable,
381 PendingOnly,
382}
383
384#[derive(Clone, Debug, PartialEq)]
385pub struct VirtualTxOutPoint {
386 pub outpoint: OutPoint,
387 pub created_at: i64,
388 pub expires_at: i64,
389 pub amount: Amount,
390 pub script: ScriptBuf,
391 pub is_preconfirmed: bool,
393 pub is_swept: bool,
394 pub is_unrolled: bool,
395 pub is_spent: bool,
396 pub spent_by: Option<Txid>,
401 pub commitment_txids: Vec<Txid>,
403 pub settled_by: Option<Txid>,
405 pub ark_txid: Option<Txid>,
407 pub assets: Vec<Asset>,
409}
410
411impl VirtualTxOutPoint {
412 pub fn is_recoverable(&self, dust: Amount) -> bool {
418 if self.is_spent {
419 return false;
420 }
421
422 self.amount < dust || self.is_swept || self.is_expired()
423 }
424
425 pub fn is_unspent(&self, dust: Amount) -> bool {
427 self.is_recoverable(dust) || (!self.is_unrolled && !self.is_spent && !self.is_swept)
428 }
429
430 pub fn is_spendable_offchain(&self, dust: Amount) -> bool {
432 !self.is_recoverable(dust) && !self.is_unrolled && !self.is_spent && !self.is_swept
433 }
434
435 pub fn is_pre_confirmed_spendable(&self, dust: Amount) -> bool {
436 self.is_spendable_offchain(dust) && self.is_preconfirmed
437 }
438
439 pub fn is_confirmed_spendable(&self, dust: Amount) -> bool {
440 self.is_spendable_offchain(dust) && !self.is_preconfirmed
441 }
442
443 pub fn is_spent_status(&self, dust: Amount) -> bool {
445 !self.is_recoverable(dust) && (self.is_unrolled || self.is_spent || self.is_swept)
446 }
447
448 pub fn is_expired(&self) -> bool {
456 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
457 let current_timestamp = std::time::SystemTime::now()
458 .duration_since(std::time::UNIX_EPOCH)
459 .expect("valid duration")
460 .as_secs() as i64;
461
462 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
463 let current_timestamp = (js_sys::Date::now() / 1000.0) as i64;
464
465 current_timestamp > self.expires_at && !self.is_swept && !self.is_spent
466 }
467}
468
469#[derive(Clone, Debug)]
470pub struct Info {
471 pub version: String,
472 pub signer_pk: PublicKey,
473 pub forfeit_pk: PublicKey,
474 pub forfeit_address: bitcoin::Address,
475 pub checkpoint_tapscript: ScriptBuf,
476 pub network: bitcoin::Network,
477 pub session_duration: u64,
478 pub unilateral_exit_delay: bitcoin::Sequence,
479 pub boarding_exit_delay: bitcoin::Sequence,
480 pub utxo_min_amount: Option<Amount>,
481 pub utxo_max_amount: Option<Amount>,
482 pub vtxo_min_amount: Option<Amount>,
483 pub vtxo_max_amount: Option<Amount>,
484 pub dust: Amount,
485 pub fees: Option<FeeInfo>,
486 pub scheduled_session: Option<ScheduledSession>,
487 pub deprecated_signers: Vec<DeprecatedSigner>,
488 pub service_status: HashMap<String, String>,
489 pub digest: String,
490 pub max_tx_weight: i64,
491 pub max_op_return_outputs: i64,
492}
493
494#[derive(Clone, Debug)]
496pub struct FeeInfo {
497 pub intent_fee: IntentFeeInfo,
498 pub tx_fee_rate: String,
499}
500
501#[derive(Clone, Debug, Default)]
506pub struct IntentFeeInfo {
507 pub offchain_input: Option<String>,
508 pub offchain_output: Option<String>,
509 pub onchain_input: Option<String>,
510 pub onchain_output: Option<String>,
511}
512
513#[derive(Clone, Debug)]
514pub struct ScheduledSession {
515 pub next_start_time: i64,
516 pub next_end_time: i64,
517 pub period: i64,
518 pub duration: i64,
519 pub fees: Option<FeeInfo>,
520}
521
522#[derive(Clone, Debug)]
523pub struct DeprecatedSigner {
524 pub pk: PublicKey,
525 pub cutoff_date: i64,
526}
527
528#[derive(Debug, Clone, Copy, PartialEq, Eq)]
534pub enum DeprecatedSignerStatus {
535 Migratable,
537 DueNow,
539 Expired,
541}
542
543impl DeprecatedSignerStatus {
544 pub fn from_cutoff(cutoff_date: i64, now_unix_secs: i64) -> Self {
546 if cutoff_date == 0 {
547 Self::DueNow
548 } else if cutoff_date > now_unix_secs {
549 Self::Migratable
550 } else {
551 Self::Expired
552 }
553 }
554
555 pub fn seconds_until_cutoff(self, cutoff_date: i64, now_unix_secs: i64) -> Option<i64> {
557 match self {
558 Self::Migratable => Some(cutoff_date - now_unix_secs),
559 Self::DueNow | Self::Expired => None,
560 }
561 }
562
563 pub fn is_cooperatively_migratable(self) -> bool {
565 matches!(self, Self::Migratable | Self::DueNow)
566 }
567}
568
569#[derive(Debug, Clone, Copy, PartialEq, Eq)]
571pub enum ServerSignerStatus {
572 Current,
574 Deprecated(DeprecatedSignerStatus),
576 Unknown,
578}
579
580impl ServerSignerStatus {
581 pub fn requires_recovery(self) -> bool {
583 matches!(self, Self::Deprecated(DeprecatedSignerStatus::Expired))
584 }
585
586 pub fn is_pre_cutoff_deprecated(self) -> bool {
588 matches!(
589 self,
590 Self::Deprecated(DeprecatedSignerStatus::Migratable | DeprecatedSignerStatus::DueNow)
591 )
592 }
593}
594
595impl Info {
596 pub fn all_server_keys(&self) -> impl Iterator<Item = XOnlyPublicKey> + '_ {
598 std::iter::once(self.signer_pk.x_only_public_key().0).chain(
599 self.deprecated_signers
600 .iter()
601 .map(|ds| ds.pk.x_only_public_key().0),
602 )
603 }
604
605 pub fn signer_status_at(
607 &self,
608 server_pk: XOnlyPublicKey,
609 now_unix_secs: i64,
610 ) -> ServerSignerStatus {
611 if self.signer_pk.x_only_public_key().0 == server_pk {
612 return ServerSignerStatus::Current;
613 }
614
615 self.deprecated_signers
616 .iter()
617 .find(|ds| ds.pk.x_only_public_key().0 == server_pk)
618 .map(|ds| {
619 ServerSignerStatus::Deprecated(DeprecatedSignerStatus::from_cutoff(
620 ds.cutoff_date,
621 now_unix_secs,
622 ))
623 })
624 .unwrap_or(ServerSignerStatus::Unknown)
625 }
626
627 pub fn deprecated_signer_status_at(
629 &self,
630 server_pk: XOnlyPublicKey,
631 now_unix_secs: i64,
632 ) -> Option<DeprecatedSignerStatus> {
633 match self.signer_status_at(server_pk, now_unix_secs) {
634 ServerSignerStatus::Deprecated(status) => Some(status),
635 ServerSignerStatus::Current | ServerSignerStatus::Unknown => None,
636 }
637 }
638
639 pub fn signer_requires_recovery_at(
643 &self,
644 server_pk: XOnlyPublicKey,
645 now_unix_secs: i64,
646 ) -> bool {
647 self.signer_status_at(server_pk, now_unix_secs)
648 .requires_recovery()
649 }
650
651 pub fn is_signer_past_cutoff_at(&self, server_pk: XOnlyPublicKey, now_unix_secs: i64) -> bool {
653 self.signer_requires_recovery_at(server_pk, now_unix_secs)
654 }
655}
656
657#[derive(Debug, Clone)]
658pub struct StreamStartedEvent {
659 pub id: String,
660}
661
662#[derive(Debug, Clone)]
663pub struct BatchStartedEvent {
664 pub id: String,
665 pub intent_id_hashes: Vec<String>,
666 pub batch_expiry: bitcoin::Sequence,
667}
668
669#[derive(Debug, Clone)]
670pub struct BatchFinalizationEvent {
671 pub id: String,
672 pub commitment_tx: Psbt,
673}
674
675#[derive(Debug, Clone)]
676pub struct BatchFinalizedEvent {
677 pub id: String,
678 pub commitment_txid: Txid,
679}
680
681#[derive(Debug, Clone)]
682pub struct BatchFailed {
683 pub id: String,
684 pub reason: String,
685}
686
687#[derive(Debug, Clone)]
688pub struct TreeSigningStartedEvent {
689 pub id: String,
690 pub cosigners_pubkeys: Vec<PublicKey>,
691 pub unsigned_commitment_tx: Psbt,
692}
693
694#[derive(Debug, Clone)]
695pub struct TreeNoncesAggregatedEvent {
696 pub id: String,
697 pub tree_nonces: NoncePks,
698}
699
700#[derive(Debug, Clone)]
701pub struct TreeTxEvent {
702 pub id: String,
703 pub topic: Vec<String>,
704 pub batch_tree_event_type: BatchTreeEventType,
705 pub tx_graph_chunk: TxGraphChunk,
706}
707
708#[derive(Debug, Clone)]
709pub struct TreeSignatureEvent {
710 pub id: String,
711 pub topic: Vec<String>,
712 pub batch_tree_event_type: BatchTreeEventType,
713 pub txid: Txid,
714 pub signature: Signature,
715}
716
717#[derive(Debug, Clone)]
718pub struct TreeNoncesEvent {
719 pub id: String,
720 pub topic: Vec<String>,
721 pub txid: Txid,
722 pub nonces: TreeTxNoncePks,
723}
724
725#[derive(Debug, Clone)]
726pub enum BatchTreeEventType {
727 Vtxo,
728 Connector,
729}
730
731#[derive(Debug, Clone)]
732pub enum StreamEvent {
733 StreamStarted(StreamStartedEvent),
734 BatchStarted(BatchStartedEvent),
735 BatchFinalization(BatchFinalizationEvent),
736 BatchFinalized(BatchFinalizedEvent),
737 BatchFailed(BatchFailed),
738 TreeSigningStarted(TreeSigningStartedEvent),
739 TreeNoncesAggregated(TreeNoncesAggregatedEvent),
740 TreeTx(TreeTxEvent),
741 TreeSignature(TreeSignatureEvent),
742 TreeNonces(TreeNoncesEvent),
743 Heartbeat,
744}
745
746impl StreamEvent {
747 pub fn name(&self) -> String {
748 let s = match self {
749 StreamEvent::StreamStarted(_) => "StreamStarted",
750 StreamEvent::BatchStarted(_) => "BatchStarted",
751 StreamEvent::BatchFinalization(_) => "BatchFinalization",
752 StreamEvent::BatchFinalized(_) => "BatchFinalized",
753 StreamEvent::BatchFailed(_) => "BatchFailed",
754 StreamEvent::TreeSigningStarted(_) => "TreeSigningStarted",
755 StreamEvent::TreeNoncesAggregated(_) => "TreeNoncesAggregated",
756 StreamEvent::TreeTx(_) => "TreeTx",
757 StreamEvent::TreeSignature(_) => "TreeSignature",
758 StreamEvent::TreeNonces(_) => "TreeNoncesEvent",
759 StreamEvent::Heartbeat => "Heartbeat",
760 };
761
762 s.to_string()
763 }
764}
765
766pub enum StreamTransactionData {
767 Commitment(CommitmentTransaction),
768 Ark(ArkTransaction),
769 Heartbeat,
770}
771
772pub struct ArkTransaction {
773 pub txid: Txid,
774 pub tx: Option<Psbt>,
775 pub spent_vtxos: Vec<VirtualTxOutPoint>,
776 pub unspent_vtxos: Vec<VirtualTxOutPoint>,
777 pub checkpoint_txs: HashMap<OutPoint, Txid>,
779 pub swept_vtxos: Vec<OutPoint>,
780}
781
782pub struct CommitmentTransaction {
783 pub txid: Txid,
784 pub spent_vtxos: Vec<VirtualTxOutPoint>,
785 pub unspent_vtxos: Vec<VirtualTxOutPoint>,
786}
787
788#[derive(Clone, Debug)]
789pub enum SubscriptionResponse {
790 Event(Box<SubscriptionEvent>),
791 Heartbeat,
792}
793
794#[derive(Clone, Debug)]
795pub struct SubscriptionEvent {
796 pub txid: Txid,
797 pub scripts: Vec<ScriptBuf>,
798 pub new_vtxos: Vec<VirtualTxOutPoint>,
799 pub spent_vtxos: Vec<VirtualTxOutPoint>,
800 pub tx: Option<Transaction>,
801 pub checkpoint_txs: HashMap<OutPoint, Txid>,
802}
803
804pub struct VtxoChains {
805 pub inner: Vec<VtxoChain>,
806}
807
808pub struct VtxoChain {
809 pub txid: Txid,
810 pub tx_type: ChainedTxType,
811 pub spends: Vec<Txid>,
812 pub expires_at: i64,
813}
814
815#[derive(Debug)]
816pub enum ChainedTxType {
817 Commitment,
818 Tree,
819 Checkpoint,
820 Ark,
821 Unspecified,
822}
823
824pub struct SubmitOffchainTxResponse {
825 pub signed_ark_tx: Psbt,
826 pub signed_checkpoint_txs: Vec<Psbt>,
827}
828
829#[derive(Debug, Clone)]
830pub struct PendingTx {
831 pub ark_txid: Txid,
832 pub signed_ark_tx: Psbt,
833 pub signed_checkpoint_txs: Vec<Psbt>,
834}
835
836#[derive(Debug, Clone)]
837pub struct FinalizeOffchainTxResponse {}
838
839#[derive(Debug)]
840pub struct VirtualTxsResponse {
841 pub txs: Vec<Psbt>,
842 pub page: Option<IndexerPage>,
843}
844
845#[derive(Debug)]
846pub struct IndexerPage {
847 pub current: i32,
848 pub next: i32,
849 pub total: i32,
850}
851
852#[derive(Clone, Debug)]
853pub enum Network {
854 Bitcoin,
855 Testnet,
856 Testnet4,
857 Signet,
858 Regtest,
859 Mutinynet,
860}
861
862#[derive(Clone, Debug, PartialEq, Eq, Hash)]
864pub struct Asset {
865 pub asset_id: AssetId,
866 pub amount: u64,
867}
868
869#[derive(Clone, Debug, PartialEq, Eq)]
871pub struct AssetInfo {
872 pub asset_id: AssetId,
873 pub control_asset_id: Option<AssetId>,
874 pub supply: u64,
875 pub metadata: String,
876}
877
878impl AssetInfo {
879 pub fn can_be_reissued(&self) -> bool {
880 self.control_asset_id.is_some()
881 }
882}
883
884impl From<Network> for bitcoin::Network {
885 fn from(value: Network) -> Self {
886 match value {
887 Network::Bitcoin => bitcoin::Network::Bitcoin,
888 Network::Testnet => bitcoin::Network::Testnet,
889 Network::Testnet4 => bitcoin::Network::Testnet4,
890 Network::Signet => bitcoin::Network::Signet,
891 Network::Regtest => bitcoin::Network::Regtest,
892 Network::Mutinynet => bitcoin::Network::Signet,
893 }
894 }
895}
896
897impl FromStr for Network {
898 type Err = String;
899
900 #[inline]
901 fn from_str(s: &str) -> Result<Self, Self::Err> {
902 match s {
903 "bitcoin" => Ok(Network::Bitcoin),
904 "testnet" => Ok(Network::Testnet),
905 "testnet4" => Ok(Network::Testnet4),
906 "signet" => Ok(Network::Signet),
907 "regtest" => Ok(Network::Regtest),
908 "mutinynet" => Ok(Network::Mutinynet),
909 _ => Err(format!("Unsupported network {}", s.to_owned())),
910 }
911 }
912}
913
914pub fn parse_sequence_number(value: i64) -> Result<bitcoin::Sequence, Error> {
915 const ARBITRARY_SEQUENCE_THRESHOLD: i64 = 512;
921
922 let sequence = if value.is_negative() {
923 return Err(Error::ad_hoc(format!("invalid sequence number: {value}")));
924 } else if value < ARBITRARY_SEQUENCE_THRESHOLD {
925 bitcoin::Sequence::from_height(value as u16)
926 } else {
927 let secs = u32::try_from(value)
928 .map_err(|_| Error::ad_hoc(format!("sequence seconds overflow: {value}")))?;
929
930 bitcoin::Sequence::from_seconds_ceil(secs).map_err(Error::ad_hoc)?
931 };
932
933 Ok(sequence)
934}
935
936pub fn parse_fee_amount(amount_str: Option<String>) -> Amount {
938 amount_str
939 .and_then(|s| {
940 if s.is_empty() {
941 None
942 } else {
943 s.parse::<u64>().ok()
944 }
945 })
946 .map(Amount::from_sat)
947 .unwrap_or(Amount::ZERO)
948}
949
950#[cfg(test)]
951mod tests {
952 use super::*;
953 use bitcoin::address::NetworkUnchecked;
954 use bitcoin::secp256k1::PublicKey;
955 use std::collections::HashMap;
956 use std::str::FromStr;
957
958 const PK_A: &str = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
960 const PK_B: &str = "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5";
961 const PK_C: &str = "02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9";
962 const PK_UNRELATED: &str = "030192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4";
963
964 fn pk(hex: &str) -> PublicKey {
965 PublicKey::from_str(hex).unwrap()
966 }
967
968 fn xonly(hex: &str) -> XOnlyPublicKey {
969 pk(hex).x_only_public_key().0
970 }
971
972 fn make_info(current_hex: &str, deprecated: Vec<(&str, i64)>) -> Info {
973 let dummy_address: bitcoin::Address<NetworkUnchecked> =
974 "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx"
975 .parse()
976 .unwrap();
977 Info {
978 version: "1".into(),
979 signer_pk: pk(current_hex),
980 forfeit_pk: pk(current_hex),
981 forfeit_address: dummy_address.assume_checked(),
982 checkpoint_tapscript: ScriptBuf::new(),
983 network: bitcoin::Network::Testnet,
984 session_duration: 0,
985 unilateral_exit_delay: bitcoin::Sequence::ZERO,
986 boarding_exit_delay: bitcoin::Sequence::ZERO,
987 utxo_min_amount: None,
988 utxo_max_amount: None,
989 vtxo_min_amount: None,
990 vtxo_max_amount: None,
991 dust: Amount::ZERO,
992 fees: None,
993 scheduled_session: None,
994 deprecated_signers: deprecated
995 .into_iter()
996 .map(|(key, cutoff)| DeprecatedSigner {
997 pk: pk(key),
998 cutoff_date: cutoff,
999 })
1000 .collect(),
1001 service_status: HashMap::new(),
1002 digest: String::new(),
1003 max_tx_weight: 0,
1004 max_op_return_outputs: 0,
1005 }
1006 }
1007
1008 #[test]
1011 fn all_server_keys_no_deprecated() {
1012 let info = make_info(PK_A, vec![]);
1013 let keys: Vec<_> = info.all_server_keys().collect();
1014 assert_eq!(keys, vec![xonly(PK_A)]);
1015 }
1016
1017 #[test]
1018 fn all_server_keys_includes_deprecated_in_order() {
1019 let info = make_info(PK_A, vec![(PK_B, 1000), (PK_C, 2000)]);
1020 let keys: Vec<_> = info.all_server_keys().collect();
1021 assert_eq!(keys, vec![xonly(PK_A), xonly(PK_B), xonly(PK_C)]);
1022 }
1023
1024 #[test]
1025 fn all_server_keys_current_is_always_first() {
1026 let info = make_info(PK_C, vec![(PK_A, 500), (PK_B, 600)]);
1027 let keys: Vec<_> = info.all_server_keys().collect();
1028 assert_eq!(keys[0], xonly(PK_C));
1029 }
1030
1031 #[test]
1034 fn signer_status_classifies_current_deprecated_and_unknown() {
1035 let now = 1_000_000i64;
1036 let info = make_info(PK_A, vec![(PK_B, 0), (PK_C, now + 10)]);
1037
1038 assert_eq!(
1039 info.signer_status_at(xonly(PK_A), now),
1040 ServerSignerStatus::Current
1041 );
1042 assert_eq!(
1043 info.signer_status_at(xonly(PK_B), now),
1044 ServerSignerStatus::Deprecated(DeprecatedSignerStatus::DueNow)
1045 );
1046 assert_eq!(
1047 info.signer_status_at(xonly(PK_C), now),
1048 ServerSignerStatus::Deprecated(DeprecatedSignerStatus::Migratable)
1049 );
1050 assert_eq!(
1051 info.signer_status_at(xonly(PK_UNRELATED), now),
1052 ServerSignerStatus::Unknown
1053 );
1054 }
1055
1056 #[test]
1057 fn signer_status_expired_requires_recovery() {
1058 let now = 1_000_000i64;
1059 let info = make_info(PK_A, vec![(PK_B, now)]);
1060
1061 let status = info.signer_status_at(xonly(PK_B), now);
1062 assert_eq!(
1063 status,
1064 ServerSignerStatus::Deprecated(DeprecatedSignerStatus::Expired)
1065 );
1066 assert!(status.requires_recovery());
1067 }
1068
1069 #[test]
1072 fn current_signer_key_is_never_past_cutoff() {
1073 let info = make_info(PK_A, vec![]);
1074 assert!(!info.is_signer_past_cutoff_at(xonly(PK_A), i64::MAX));
1075 }
1076
1077 #[test]
1078 fn unknown_key_is_not_past_cutoff() {
1079 let info = make_info(PK_A, vec![(PK_B, 100)]);
1080 assert!(!info.is_signer_past_cutoff_at(xonly(PK_UNRELATED), 200));
1081 }
1082
1083 #[test]
1084 fn cutoff_zero_means_rotate_immediately_not_past_cutoff() {
1085 let info = make_info(PK_A, vec![(PK_B, 0)]);
1088 assert!(!info.is_signer_past_cutoff_at(xonly(PK_B), 9_999_999));
1089 }
1090
1091 #[test]
1092 fn future_cutoff_is_not_past() {
1093 let now = 1_000_000i64;
1094 let info = make_info(PK_A, vec![(PK_B, now + 1)]);
1095 assert!(!info.is_signer_past_cutoff_at(xonly(PK_B), now));
1096 }
1097
1098 #[test]
1099 fn exact_cutoff_boundary_is_past() {
1100 let now = 1_000_000i64;
1101 let info = make_info(PK_A, vec![(PK_B, now)]);
1102 assert!(info.is_signer_past_cutoff_at(xonly(PK_B), now));
1103 }
1104
1105 #[test]
1106 fn past_cutoff_is_past() {
1107 let now = 1_000_000i64;
1108 let info = make_info(PK_A, vec![(PK_B, now - 1)]);
1109 assert!(info.is_signer_past_cutoff_at(xonly(PK_B), now));
1110 }
1111
1112 #[test]
1113 fn multiple_deprecated_only_past_key_is_flagged() {
1114 let now = 1_000_000i64;
1115 let info = make_info(PK_A, vec![(PK_B, now + 100), (PK_C, now - 100)]);
1117 assert!(!info.is_signer_past_cutoff_at(xonly(PK_B), now));
1118 assert!(info.is_signer_past_cutoff_at(xonly(PK_C), now));
1119 }
1120}