Skip to main content

ark_core/
server.rs

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