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 VTXO 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 VTXO 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 VTXO 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 the original VTXO tree
374    /// in a batch.
375    pub is_preconfirmed: bool,
376    pub is_swept: bool,
377    pub is_unrolled: bool,
378    pub is_spent: bool,
379    /// If the VTXO is spent, this field references the _checkpoint transaction_ that actually
380    /// spends it. The corresponding Ark transaction is in the `ark_txid` field.
381    ///
382    /// If the VTXO is renewed, this field references the corresponding _forfeit transaction_.
383    pub spent_by: Option<Txid>,
384    /// The list of commitment transactions that are ancestors to this VTXO.
385    pub commitment_txids: Vec<Txid>,
386    /// The commitment TXID onto which this VTXO was forfeited.
387    pub settled_by: Option<Txid>,
388    /// The Ark transaction that _spends_ this VTXO (if we omit the checkpoint transaction).
389    pub ark_txid: Option<Txid>,
390    /// Assets carried by this VTXO.
391    pub assets: Vec<Asset>,
392}
393
394impl VirtualTxOutPoint {
395    /// Check if a VTXO is recoverable.
396    ///
397    /// Recoverable VTXOs can be settled, but they cannot be sent in an offchain transaction. To
398    /// settle them, the original VTXO does not need to be forfeited, as the Arkade server already
399    /// controls it.
400    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    /// Check if a VTXO has expired.
409    ///
410    /// Expired VTXOs can be settled, but they cannot be sent in an offchain transaction. To settle
411    /// them, the original VTXO must be forfeited.
412    ///
413    /// NOTE: The server's concept of now may differ from the client's, so client and server may
414    /// sometimes disagree on whether a VTXO has expired or not.
415    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/// Fee information from the server.
455#[derive(Clone, Debug)]
456pub struct FeeInfo {
457    pub intent_fee: IntentFeeInfo,
458    pub tx_fee_rate: String,
459}
460
461/// Intent fee information.
462///
463/// These are CEL like programs which need to be evaluated during runtime. See [`ark-fees`] module
464/// for details.
465#[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    /// key: outpoint, value: checkpoint txid. Only set for offchain txs.
609    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/// An asset carried by a VTXO.
694#[derive(Clone, Debug, PartialEq, Eq, Hash)]
695pub struct Asset {
696    pub asset_id: AssetId,
697    pub amount: u64,
698}
699
700/// Metadata about an issued asset, including its control asset reference.
701#[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    /// The threshold that determines whether an expiry or exit delay should be parsed as a
747    /// number of blocks or a number of seconds.
748    ///
749    /// - A value below 512 is considered a number of blocks.
750    /// - A value of 512 or more is considered a number of seconds.
751    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
767/// Parse a fee amount string as satoshis. Returns Amount::ZERO for empty or missing strings.
768pub 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}