Skip to main content

ark_core/
server.rs

1//! Messages exchanged between the client and the Ark server.
2
3use 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/// An aggregate public nonce per shared internal (non-leaf) node in the batch-tree.
24#[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    /// Get the [`MusigPubNonce`] for the transaction identified by `txid`.
33    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/// A public nonce per public key, where each public key corresponds to a party signing a
71/// transaction in the batch-tree.
72#[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/// A Musig partial signature per shared internal (non-leaf) node in the batch-tree.
118#[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    /// Get all nodes at a specific level.
182    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    /// Iterate over levels in order.
190    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/// Page request for paginated queries.
225#[derive(Debug, Clone, Copy)]
226pub struct PageRequest {
227    /// Number of items per page.
228    pub size: i32,
229    /// Page index (0-based).
230    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    /// A pre-confirmed VTXO spends from another VTXO and is not a leaf of a batch-tree.
374    pub is_preconfirmed: bool,
375    pub is_swept: bool,
376    pub is_unrolled: bool,
377    pub is_spent: bool,
378    /// If the VTXO is spent, this field references the _checkpoint transaction_ that actually
379    /// spends it. The corresponding Ark transaction is in the `ark_txid` field.
380    ///
381    /// If the VTXO is renewed, this field references the corresponding _forfeit transaction_.
382    pub spent_by: Option<Txid>,
383    /// The list of commitment transactions that are ancestors to this VTXO.
384    pub commitment_txids: Vec<Txid>,
385    /// The commitment TXID onto which this VTXO was forfeited.
386    pub settled_by: Option<Txid>,
387    /// The Ark transaction that _spends_ this VTXO (if we omit the checkpoint transaction).
388    pub ark_txid: Option<Txid>,
389    /// Assets carried by this VTXO.
390    pub assets: Vec<Asset>,
391}
392
393impl VirtualTxOutPoint {
394    /// Check if a VTXO is recoverable.
395    ///
396    /// Recoverable VTXOs can be settled, but they cannot be sent in an offchain transaction. To
397    /// settle them, the original VTXO does not need to be forfeited, as the Arkade server already
398    /// controls it.
399    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    /// Check if a VTXO has expired.
408    ///
409    /// Expired VTXOs can be settled, but they cannot be sent in an offchain transaction. To settle
410    /// them, the original VTXO must be forfeited.
411    ///
412    /// NOTE: The server's concept of now may differ from the client's, so client and server may
413    /// sometimes disagree on whether a VTXO has expired or not.
414    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/// Fee information from the server.
454#[derive(Clone, Debug)]
455pub struct FeeInfo {
456    pub intent_fee: IntentFeeInfo,
457    pub tx_fee_rate: String,
458}
459
460/// Intent fee information.
461///
462/// These are CEL like programs which need to be evaluated during runtime. See [`ark-fees`] module
463/// for details.
464#[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    /// key: outpoint, value: checkpoint txid. Only set for offchain txs.
608    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/// An asset carried by a VTXO.
693#[derive(Clone, Debug, PartialEq, Eq, Hash)]
694pub struct Asset {
695    pub asset_id: AssetId,
696    pub amount: u64,
697}
698
699/// Metadata about an issued asset, including its control asset reference.
700#[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    /// The threshold that determines whether an expiry or exit delay should be parsed as a
746    /// number of blocks or a number of seconds.
747    ///
748    /// - A value below 512 is considered a number of blocks.
749    /// - A value of 512 or more is considered a number of seconds.
750    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
766/// Parse a fee amount string as satoshis. Returns Amount::ZERO for empty or missing strings.
767pub 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}