Skip to main content

fiber_types/
payment.rs

1//! Payment-related types.
2
3use crate::gen::fiber as molecule_fiber;
4use crate::Hash256;
5use crate::{EntityHex, Pubkey, SliceHex};
6use ckb_types::prelude::{Pack, Unpack};
7use molecule::prelude::{Builder, Byte, Entity};
8use num_enum::{IntoPrimitive, TryFromPrimitive};
9use serde::{Deserialize, Serialize};
10use serde_with::serde_as;
11use std::collections::HashMap;
12use strum::{AsRefStr, EnumString};
13
14/// The status of a payment, will update as the payment progresses.
15/// The transfer path for payment status is `Created -> Inflight -> Success | Failed`.
16///
17/// **MPP Behavior**: A single session may involve multiple attempts (HTLCs) to fulfill the total amount.
18#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
19pub enum PaymentStatus {
20    /// Initial status. A payment session is created, but no HTLC has been dispatched.
21    Created,
22    /// The first hop AddTlc is sent successfully and waiting for the response.
23    ///
24    /// > **MPP Logic**: Status `Inflight` means at least one attempt is still not in `Success`, payment needs more retrying or waiting for HTLC settlement.
25    Inflight,
26    /// The payment is finished. All related HTLCs are successfully settled,
27    /// and the aggregate amount equals the total requested amount.
28    Success,
29    /// The payment session has terminated. HTLCs have failed and the target
30    /// amount cannot be fulfilled after exhausting all retries.
31    Failed,
32}
33
34impl PaymentStatus {
35    pub fn is_final(&self) -> bool {
36        matches!(self, PaymentStatus::Success | PaymentStatus::Failed)
37    }
38}
39
40/// 0 ~ 65535 is reserved for endpoint usage, index above 65535 is reserved for internal usage
41pub const USER_CUSTOM_RECORDS_MAX_INDEX: u32 = 65535;
42
43/// The custom records to be included in the payment.
44/// The key is hex encoded of `u32`, and the value is hex encoded of `Vec<u8>` with `0x` as prefix.
45#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Default)]
46pub struct PaymentCustomRecords {
47    /// The custom records to be included in the payment.
48    pub data: HashMap<u32, Vec<u8>>,
49}
50
51impl From<PaymentCustomRecords> for molecule_fiber::CustomRecords {
52    fn from(custom_records: PaymentCustomRecords) -> Self {
53        molecule_fiber::CustomRecords::new_builder()
54            .data(
55                custom_records
56                    .data
57                    .into_iter()
58                    .map(|(key, val)| {
59                        molecule_fiber::CustomRecordDataPairBuilder::default()
60                            .key(key.pack())
61                            .value(val.pack())
62                            .build()
63                    })
64                    .collect(),
65            )
66            .build()
67    }
68}
69
70impl From<molecule_fiber::CustomRecords> for PaymentCustomRecords {
71    fn from(custom_records: molecule_fiber::CustomRecords) -> Self {
72        PaymentCustomRecords {
73            data: custom_records
74                .data()
75                .into_iter()
76                .map(|pair| (pair.key().unpack(), pair.value().unpack()))
77                .collect(),
78        }
79    }
80}
81
82/// Bolt04 basic MPP payment data record
83#[derive(Eq, PartialEq, Debug)]
84pub struct BasicMppPaymentData {
85    pub payment_secret: crate::Hash256,
86    pub total_amount: u128,
87}
88
89impl BasicMppPaymentData {
90    // record type for payment data record in bolt04
91    // custom records key from 65536 is reserved for internal usage
92    pub const CUSTOM_RECORD_KEY: u32 = USER_CUSTOM_RECORDS_MAX_INDEX + 1;
93
94    pub fn new(payment_secret: crate::Hash256, total_amount: u128) -> Self {
95        Self {
96            payment_secret,
97            total_amount,
98        }
99    }
100
101    fn to_vec(&self) -> Vec<u8> {
102        let mut vec = Vec::new();
103        vec.extend_from_slice(self.payment_secret.as_ref());
104        vec.extend_from_slice(&self.total_amount.to_le_bytes());
105        vec
106    }
107
108    pub fn write(&self, custom_records: &mut PaymentCustomRecords) {
109        custom_records
110            .data
111            .insert(Self::CUSTOM_RECORD_KEY, self.to_vec());
112    }
113
114    pub fn read(custom_records: &PaymentCustomRecords) -> Option<Self> {
115        custom_records
116            .data
117            .get(&Self::CUSTOM_RECORD_KEY)
118            .and_then(|data| {
119                if data.len() != 32 + 16 {
120                    return None;
121                }
122                let secret: [u8; 32] = data[..32].try_into().unwrap();
123                let payment_secret = crate::Hash256::from(secret);
124                let total_amount = u128::from_le_bytes(data[32..].try_into().unwrap());
125                Some(Self::new(payment_secret, total_amount))
126            })
127    }
128}
129
130/// The status of a payment attempt.
131///
132/// State transitions:
133/// ```text
134///     Created -> Inflight -> Success
135///                         -> Failed -> Retrying -> Inflight
136/// ```
137#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
138pub enum AttemptStatus {
139    /// Initial status, a payment attempt is created, no HTLC is sent.
140    Created,
141    /// The first hop AddTlc is sent successfully and waiting for the response.
142    Inflight,
143    /// The attempt is retrying after failed.
144    Retrying,
145    /// Related HTLC is successfully settled.
146    Success,
147    /// Related HTLC is failed.
148    Failed,
149}
150
151impl AttemptStatus {
152    pub fn is_final(&self) -> bool {
153        matches!(self, AttemptStatus::Success | AttemptStatus::Failed)
154    }
155}
156
157use crate::protocol::ChannelUpdate;
158use ckb_types::packed::OutPoint;
159use std::fmt::Display;
160
161/// Extra data attached to a TLC error.
162#[serde_as]
163#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
164pub enum TlcErrData {
165    ChannelFailed {
166        #[serde_as(as = "EntityHex")]
167        channel_outpoint: OutPoint,
168        channel_update: Option<ChannelUpdate>,
169        node_id: Pubkey,
170    },
171    NodeFailed {
172        node_id: Pubkey,
173    },
174    TrampolineFailed {
175        node_id: Pubkey,
176        #[serde_as(as = "SliceHex")]
177        inner_error_packet: Vec<u8>,
178    },
179}
180
181/// Structured TLC error with error code and optional extra data.
182#[serde_as]
183#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
184pub struct TlcErr {
185    pub error_code: TlcErrorCode,
186    pub extra_data: Option<TlcErrData>,
187}
188
189impl Display for TlcErr {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        write!(f, "{}", self.error_code_as_str())
192    }
193}
194
195impl TlcErr {
196    pub fn new(error_code: TlcErrorCode) -> Self {
197        TlcErr {
198            error_code,
199            extra_data: None,
200        }
201    }
202
203    pub fn new_node_fail(error_code: TlcErrorCode, node_id: Pubkey) -> Self {
204        TlcErr {
205            error_code,
206            extra_data: Some(TlcErrData::NodeFailed { node_id }),
207        }
208    }
209
210    pub fn new_channel_fail(
211        error_code: TlcErrorCode,
212        node_id: Pubkey,
213        channel_outpoint: OutPoint,
214        channel_update: Option<ChannelUpdate>,
215    ) -> Self {
216        TlcErr {
217            error_code,
218            extra_data: Some(TlcErrData::ChannelFailed {
219                node_id,
220                channel_outpoint,
221                channel_update,
222            }),
223        }
224    }
225
226    pub fn error_node_id(&self) -> Option<Pubkey> {
227        match &self.extra_data {
228            Some(TlcErrData::NodeFailed { node_id }) => Some(*node_id),
229            Some(TlcErrData::ChannelFailed { node_id, .. }) => Some(*node_id),
230            Some(TlcErrData::TrampolineFailed { node_id, .. }) => Some(*node_id),
231            _ => None,
232        }
233    }
234
235    pub fn error_channel_outpoint(&self) -> Option<OutPoint> {
236        match &self.extra_data {
237            Some(TlcErrData::ChannelFailed {
238                channel_outpoint, ..
239            }) => Some(channel_outpoint.clone()),
240            _ => None,
241        }
242    }
243
244    pub fn error_code(&self) -> TlcErrorCode {
245        self.error_code
246    }
247
248    pub fn error_code_as_str(&self) -> String {
249        let error_code: TlcErrorCode = self.error_code;
250        error_code.as_ref().to_string()
251    }
252
253    pub fn error_code_as_u16(&self) -> u16 {
254        self.error_code.into()
255    }
256
257    pub fn set_extra_data(&mut self, extra_data: TlcErrData) {
258        self.extra_data = Some(extra_data);
259    }
260
261    pub fn serialize(&self) -> Vec<u8> {
262        molecule_fiber::TlcErr::try_from(self.clone())
263            .expect("TlcErr serialization should not fail for valid TlcErr")
264            .as_slice()
265            .to_vec()
266    }
267
268    pub fn deserialize(data: &[u8]) -> Option<Self> {
269        molecule_fiber::TlcErr::from_slice(data)
270            .ok()
271            .and_then(|e| TlcErr::try_from(e).ok())
272    }
273}
274
275impl TryFrom<TlcErrData> for molecule_fiber::TlcErrData {
276    type Error = anyhow::Error;
277
278    fn try_from(tlc_err_data: TlcErrData) -> Result<Self, Self::Error> {
279        match tlc_err_data {
280            TlcErrData::ChannelFailed {
281                channel_outpoint,
282                channel_update,
283                node_id,
284            } => Ok(molecule_fiber::ChannelFailed::new_builder()
285                .channel_outpoint(channel_outpoint)
286                .channel_update(
287                    molecule_fiber::ChannelUpdateOpt::new_builder()
288                        .set(channel_update.map(|x| x.into()))
289                        .build(),
290                )
291                .node_id(node_id.into())
292                .build()
293                .into()),
294            TlcErrData::NodeFailed { node_id } => Ok(molecule_fiber::NodeFailed::new_builder()
295                .node_id(node_id.into())
296                .build()
297                .into()),
298            TlcErrData::TrampolineFailed {
299                node_id,
300                inner_error_packet,
301            } => Ok(molecule_fiber::TrampolineFailed::new_builder()
302                .node_id(node_id.into())
303                .inner_error_packet(inner_error_packet.pack())
304                .build()
305                .into()),
306        }
307    }
308}
309
310impl TryFrom<molecule_fiber::TlcErrData> for TlcErrData {
311    type Error = anyhow::Error;
312
313    fn try_from(tlc_err_data: molecule_fiber::TlcErrData) -> Result<Self, Self::Error> {
314        match tlc_err_data.to_enum() {
315            molecule_fiber::TlcErrDataUnion::ChannelFailed(channel_failed) => {
316                Ok(TlcErrData::ChannelFailed {
317                    channel_outpoint: channel_failed.channel_outpoint(),
318                    channel_update: channel_failed
319                        .channel_update()
320                        .to_opt()
321                        .map(|x| x.try_into())
322                        .transpose()?,
323                    node_id: channel_failed.node_id().try_into()?,
324                })
325            }
326            molecule_fiber::TlcErrDataUnion::NodeFailed(node_failed) => {
327                Ok(TlcErrData::NodeFailed {
328                    node_id: node_failed.node_id().try_into()?,
329                })
330            }
331            molecule_fiber::TlcErrDataUnion::TrampolineFailed(trampoline_failed) => {
332                Ok(TlcErrData::TrampolineFailed {
333                    node_id: trampoline_failed.node_id().try_into()?,
334                    inner_error_packet: trampoline_failed.inner_error_packet().unpack(),
335                })
336            }
337        }
338    }
339}
340
341impl TryFrom<TlcErr> for molecule_fiber::TlcErr {
342    type Error = anyhow::Error;
343
344    fn try_from(tlc_err: TlcErr) -> Result<Self, Self::Error> {
345        Ok(molecule_fiber::TlcErr::new_builder()
346            .error_code(tlc_err.error_code_as_u16().into())
347            .extra_data(
348                molecule_fiber::TlcErrDataOpt::new_builder()
349                    .set(tlc_err.extra_data.map(|data| data.try_into()).transpose()?)
350                    .build(),
351            )
352            .build())
353    }
354}
355
356impl TryFrom<molecule_fiber::TlcErr> for TlcErr {
357    type Error = anyhow::Error;
358
359    fn try_from(tlc_err: molecule_fiber::TlcErr) -> Result<Self, Self::Error> {
360        let code: u16 = tlc_err.error_code().into();
361        let error_code = TlcErrorCode::try_from(code)
362            .map_err(|_| anyhow::anyhow!("Invalid TLC error code: {}", code))?;
363        let extra_data = tlc_err
364            .extra_data()
365            .to_opt()
366            .map(|data| data.try_into())
367            .transpose()?;
368        Ok(TlcErr {
369            error_code,
370            extra_data,
371        })
372    }
373}
374
375use crate::invoice::HashAlgorithm;
376
377/// The decrypted hop data for the current hop, without the `next_hop` field.
378#[serde_as]
379#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
380pub struct CurrentPaymentHopData {
381    pub amount: u128,
382    pub expiry: u64,
383    pub payment_preimage: Option<crate::Hash256>,
384    pub hash_algorithm: HashAlgorithm,
385    pub funding_tx_hash: crate::Hash256,
386    pub custom_records: Option<PaymentCustomRecords>,
387}
388
389impl From<PaymentHopData> for CurrentPaymentHopData {
390    fn from(hop: PaymentHopData) -> Self {
391        CurrentPaymentHopData {
392            amount: hop.amount,
393            expiry: hop.expiry,
394            payment_preimage: hop.payment_preimage,
395            hash_algorithm: hop.hash_algorithm,
396            funding_tx_hash: hop.funding_tx_hash,
397            custom_records: hop.custom_records,
398        }
399    }
400}
401
402impl From<CurrentPaymentHopData> for PaymentHopData {
403    fn from(hop: CurrentPaymentHopData) -> Self {
404        PaymentHopData {
405            amount: hop.amount,
406            expiry: hop.expiry,
407            payment_preimage: hop.payment_preimage,
408            hash_algorithm: hop.hash_algorithm,
409            funding_tx_hash: hop.funding_tx_hash,
410            custom_records: hop.custom_records,
411            next_hop: None,
412        }
413    }
414}
415
416impl CurrentPaymentHopData {
417    /// Returns the trampoline onion bytes embedded in `custom_records`, if present.
418    pub fn trampoline_onion(&self) -> Option<Vec<u8>> {
419        self.custom_records
420            .as_ref()
421            .and_then(TrampolineOnionData::read)
422    }
423
424    /// Embeds a trampoline onion packet into `custom_records`.
425    pub fn set_trampoline_onion(&mut self, data: Vec<u8>) {
426        let cr = self.custom_records.get_or_insert_with(Default::default);
427        TrampolineOnionData::write(data, cr);
428    }
429}
430
431use crate::U128Hex;
432
433/// The node and channel information in a payment route hop
434#[serde_as]
435#[derive(Clone, Debug, Serialize, Deserialize)]
436pub struct SessionRouteNode {
437    /// the public key of the node
438    pub pubkey: Pubkey,
439    /// the amount for this hop
440    #[serde_as(as = "U128Hex")]
441    pub amount: u128,
442    /// the channel outpoint for this hop
443    #[serde_as(as = "EntityHex")]
444    pub channel_outpoint: OutPoint,
445}
446
447use crate::U64Hex;
448
449/// A router hop information for a payment, a paymenter router is an array of RouterHop,
450/// a router hop generally implies hop `target` will receive `amount_received` with `channel_outpoint` of channel.
451/// Improper hop hint may make payment fail, for example the specified channel do not have enough capacity.
452#[serde_as]
453#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
454pub struct RouterHop {
455    /// The node that is sending the TLC to the next node.
456    pub target: Pubkey,
457    /// The channel of this hop used to receive TLC
458    #[serde_as(as = "EntityHex")]
459    pub channel_outpoint: OutPoint,
460    /// The amount that the source node will transfer to the target node.
461    /// We have already added up all the fees along the path, so this amount can be used directly for the TLC.
462    #[serde_as(as = "U128Hex")]
463    pub amount_received: u128,
464    /// The expiry for the TLC that the source node sends to the target node.
465    /// We have already added up all the expiry deltas along the path,
466    /// the only thing missing is current time. So the expiry is the current time plus the expiry delta.
467    #[serde_as(as = "U64Hex")]
468    pub incoming_tlc_expiry: u64,
469}
470
471/// A hop hint is a hint for a node to use a specific channel,
472/// usually used for the last hop to the target node.
473#[serde_as]
474#[derive(Clone, Debug, Serialize, Deserialize)]
475pub struct HopHint {
476    /// The public key of the node.
477    pub pubkey: Pubkey,
478    /// The outpoint for the channel.
479    #[serde_as(as = "EntityHex")]
480    pub channel_outpoint: OutPoint,
481    /// The fee rate to use this hop to forward the payment.
482    pub fee_rate: u64,
483    /// The TLC expiry delta to use this hop to forward the payment.
484    pub tlc_expiry_delta: u64,
485}
486
487/// The router is a list of nodes that the payment will go through.
488/// We store in the payment session and then will use it to track the payment history.
489/// The router is a list of nodes that the payment will go through.
490/// For example:
491///    `A(amount, channel) -> B -> C -> D`
492/// means A will send `amount` with `channel` to B.
493#[derive(Clone, Debug, Serialize, Deserialize, Default)]
494pub struct SessionRoute {
495    /// the nodes in the route
496    pub nodes: Vec<SessionRouteNode>,
497}
498
499impl SessionRoute {
500    // Create a new route from the source to the target with the given payment hops.
501    // The payment hops are the hops that the payment will go through.
502    // for a payment route A -> B -> C -> D
503    // the `payment_hops` is [B, C, D], which is a convenient way for onion routing.
504    // here we need to create a session route with source, which is A -> B -> C -> D
505    pub fn new(source: Pubkey, target: Pubkey, payment_hops: &[PaymentHopData]) -> Self {
506        let nodes = std::iter::once(source)
507            .chain(
508                payment_hops
509                    .iter()
510                    .map(|hop| hop.next_hop.unwrap_or(target)),
511            )
512            .zip(payment_hops)
513            .map(|(pubkey, hop)| SessionRouteNode {
514                pubkey,
515                channel_outpoint: OutPoint::new(
516                    if hop.funding_tx_hash != Hash256::default() {
517                        hop.funding_tx_hash.into()
518                    } else {
519                        Hash256::default().into()
520                    },
521                    0,
522                ),
523                amount: hop.amount,
524            })
525            .collect();
526        Self { nodes }
527    }
528
529    pub fn receiver_amount(&self) -> u128 {
530        self.nodes.last().map_or(0, |s| s.amount)
531    }
532
533    pub fn fee(&self) -> u128 {
534        let first_amount = self.nodes.first().map_or(0, |s| s.amount);
535        let last_amount = self.receiver_amount();
536        debug_assert!(first_amount >= last_amount);
537        first_amount - last_amount
538    }
539
540    pub fn channel_outpoints(&self) -> impl Iterator<Item = (Pubkey, &OutPoint, u128)> {
541        self.nodes
542            .iter()
543            .map(|x| (x.pubkey, &x.channel_outpoint, x.amount))
544    }
545}
546
547/// The data for a single hop in a payment route.
548#[serde_as]
549#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
550pub struct PaymentHopData {
551    /// The amount of the tlc, <= total amount.
552    pub amount: u128,
553    pub expiry: u64,
554    pub payment_preimage: Option<Hash256>,
555    pub hash_algorithm: HashAlgorithm,
556    pub funding_tx_hash: Hash256,
557    pub next_hop: Option<Pubkey>,
558    pub custom_records: Option<PaymentCustomRecords>,
559}
560
561// TrampolineOnionData is in crate::onion.
562use crate::onion::TrampolineOnionData;
563
564impl PaymentHopData {
565    pub fn next_hop(&self) -> Option<Pubkey> {
566        self.next_hop
567    }
568
569    /// Returns the trampoline onion bytes embedded in `custom_records`, if present.
570    pub fn trampoline_onion(&self) -> Option<Vec<u8>> {
571        self.custom_records
572            .as_ref()
573            .and_then(TrampolineOnionData::read)
574    }
575
576    /// Embeds a trampoline onion packet into `custom_records`.
577    pub fn set_trampoline_onion(&mut self, data: Vec<u8>) {
578        let cr = self.custom_records.get_or_insert_with(Default::default);
579        TrampolineOnionData::write(data, cr);
580    }
581
582    pub fn serialize(&self) -> Vec<u8> {
583        molecule_fiber::PaymentHopData::from(self.clone())
584            .as_bytes()
585            .to_vec()
586    }
587
588    pub fn deserialize(data: &[u8]) -> Option<Self> {
589        molecule_fiber::PaymentHopData::from_slice(data)
590            .ok()
591            .map(|x| x.into())
592    }
593}
594
595impl From<PaymentHopData> for molecule_fiber::PaymentHopData {
596    fn from(payment_hop_data: PaymentHopData) -> Self {
597        molecule_fiber::PaymentHopData::new_builder()
598            .amount(payment_hop_data.amount.pack())
599            .expiry(payment_hop_data.expiry.pack())
600            .payment_preimage(
601                molecule_fiber::PaymentPreimageOpt::new_builder()
602                    .set(payment_hop_data.payment_preimage.map(|x| x.into()))
603                    .build(),
604            )
605            .hash_algorithm(Byte::new(payment_hop_data.hash_algorithm as u8))
606            .funding_tx_hash(payment_hop_data.funding_tx_hash.into())
607            .next_hop(
608                molecule_fiber::PubkeyOpt::new_builder()
609                    .set(payment_hop_data.next_hop.map(|x| x.into()))
610                    .build(),
611            )
612            .custom_records(
613                molecule_fiber::CustomRecordsOpt::new_builder()
614                    .set(payment_hop_data.custom_records.map(|x| x.into()))
615                    .build(),
616            )
617            .build()
618    }
619}
620
621impl From<molecule_fiber::PaymentHopData> for PaymentHopData {
622    fn from(payment_hop_data: molecule_fiber::PaymentHopData) -> Self {
623        let custom_records: Option<PaymentCustomRecords> =
624            payment_hop_data.custom_records().to_opt().map(|x| x.into());
625
626        PaymentHopData {
627            amount: payment_hop_data.amount().unpack(),
628            expiry: payment_hop_data.expiry().unpack(),
629            payment_preimage: payment_hop_data
630                .payment_preimage()
631                .to_opt()
632                .map(|x| x.into()),
633            hash_algorithm: payment_hop_data
634                .hash_algorithm()
635                .try_into()
636                .unwrap_or_default(),
637            funding_tx_hash: payment_hop_data.funding_tx_hash().into(),
638            next_hop: payment_hop_data
639                .next_hop()
640                .to_opt()
641                .map(|x| x.try_into())
642                .and_then(Result::ok),
643            custom_records,
644        }
645    }
646}
647
648/// A payment attempt.
649#[derive(Clone, Serialize, Deserialize)]
650pub struct Attempt {
651    pub id: u64,
652    pub try_limit: u32,
653    pub tried_times: u32,
654    pub hash: Hash256,
655    pub status: AttemptStatus,
656    pub payment_hash: Hash256,
657    pub route: SessionRoute,
658    pub route_hops: Vec<PaymentHopData>,
659    pub session_key: [u8; 32],
660    pub preimage: Option<Hash256>,
661    pub created_at: u64,
662    pub last_updated_at: u64,
663    pub last_error: Option<String>,
664}
665
666impl std::fmt::Debug for Attempt {
667    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
668        f.debug_struct("Attempt")
669            .field("id", &self.id)
670            .field("try_limit", &self.try_limit)
671            .field("tried_times", &self.tried_times)
672            .field("hash", &self.hash)
673            .field("status", &self.status)
674            .field("payment_hash", &self.payment_hash)
675            .field("route", &self.route)
676            .field("route_hops", &self.route_hops)
677            .field("session_key", &"[REDACTED]")
678            .field("preimage", &self.preimage.as_ref().map(|_| "[REDACTED]"))
679            .field("created_at", &self.created_at)
680            .field("last_updated_at", &self.last_updated_at)
681            .field("last_error", &self.last_error)
682            .finish()
683    }
684}
685
686impl Attempt {
687    pub fn set_inflight_status(&mut self) {
688        self.status = AttemptStatus::Inflight;
689        self.last_error = None;
690    }
691
692    pub fn set_success_status(&mut self) {
693        self.status = AttemptStatus::Success;
694        self.last_error = None;
695    }
696
697    pub fn set_failed_status(&mut self, error: &str, retryable: bool) {
698        self.last_error = Some(error.to_string());
699        self.last_updated_at = crate::now_timestamp_as_millis_u64();
700        if !retryable || self.tried_times > self.try_limit {
701            self.status = AttemptStatus::Failed;
702        } else {
703            self.status = AttemptStatus::Retrying;
704            // already checked tried_times < try_limit
705            self.tried_times += 1;
706        }
707    }
708
709    pub fn update_route(&mut self, new_route_hops: Vec<PaymentHopData>) {
710        self.route_hops = new_route_hops;
711        let sender = self.route.nodes[0].pubkey;
712        let receiver = self.route.nodes.last().unwrap().pubkey;
713        self.route = SessionRoute::new(sender, receiver, &self.route_hops);
714    }
715
716    pub fn is_success(&self) -> bool {
717        self.status == AttemptStatus::Success
718    }
719
720    pub fn is_inflight(&self) -> bool {
721        self.status == AttemptStatus::Inflight
722    }
723
724    pub fn is_failed(&self) -> bool {
725        self.status == AttemptStatus::Failed
726    }
727
728    pub fn is_active(&self) -> bool {
729        self.status != AttemptStatus::Failed
730    }
731
732    pub fn is_retrying(&self) -> bool {
733        self.status == AttemptStatus::Retrying
734    }
735
736    pub fn first_hop_channel_outpoint_eq(&self, out_point: &OutPoint) -> bool {
737        self.first_hop_channel_outpoint()
738            .map(|x| x.eq(out_point))
739            .unwrap_or_default()
740    }
741
742    pub fn first_hop_channel_outpoint(&self) -> Option<&OutPoint> {
743        self.route.nodes.first().map(|x| &x.channel_outpoint)
744    }
745
746    pub fn channel_outpoints(&self) -> impl Iterator<Item = (Pubkey, &OutPoint, u128)> {
747        self.route.channel_outpoints()
748    }
749
750    pub fn hops_public_keys(&self) -> Vec<Pubkey> {
751        // Skip the first node, which is the sender.
752        self.route.nodes.iter().skip(1).map(|x| x.pubkey).collect()
753    }
754}
755
756use crate::channel::PrevTlcInfo;
757
758/// Context for trampoline routing payments.
759#[derive(Clone, Debug, Serialize, Deserialize, Default)]
760pub struct TrampolineContext {
761    /// Optional final trampoline onion packet.
762    ///
763    /// When provided, this onion packet will be attached to the payload of the next hop
764    /// (which in this context is the next trampoline node).
765    pub remaining_trampoline_onion: Vec<u8>,
766    /// Previous TLCs information for the payment session.
767    /// This is used to associate the outgoing payment with the incoming payment.
768    pub previous_tlcs: Vec<PrevTlcInfo>,
769    /// Hash algorithm used for the payment.
770    pub hash_algorithm: HashAlgorithm,
771    /// Maximum absolute expiry allowed for the outgoing first-hop TLC.
772    ///
773    /// Trampoline forwarding derives this from the upstream TLC expiry minus
774    /// the local forwarding delta, so the forwarded payment cannot outlive the
775    /// upstream claim window.
776    #[serde(default)]
777    pub max_outgoing_tlc_expiry: Option<u64>,
778}
779
780// The onion packet is invalid
781const BADONION: u16 = 0x8000;
782// Permanent errors (otherwise transient)
783const PERM: u16 = 0x4000;
784// Node related errors (otherwise channels)
785const NODE: u16 = 0x2000;
786// Channel forwarding parameter was violated
787const UPDATE: u16 = 0x1000;
788
789/// Error codes for TLC (Time-Locked Contract) failures.
790#[repr(u16)]
791#[derive(
792    Debug,
793    Copy,
794    Clone,
795    Serialize,
796    Deserialize,
797    PartialEq,
798    Eq,
799    AsRefStr,
800    EnumString,
801    TryFromPrimitive,
802    IntoPrimitive,
803)]
804pub enum TlcErrorCode {
805    TemporaryNodeFailure = NODE | 2,
806    PermanentNodeFailure = PERM | NODE | 2,
807    RequiredNodeFeatureMissing = PERM | NODE | 3,
808    InvalidOnionVersion = BADONION | PERM | 4,
809    InvalidOnionHmac = BADONION | PERM | 5,
810    InvalidOnionKey = BADONION | PERM | 6,
811    TemporaryChannelFailure = UPDATE | 7,
812    PermanentChannelFailure = PERM | 8,
813    RequiredChannelFeatureMissing = PERM | 9,
814    UnknownNextPeer = PERM | 10,
815    AmountBelowMinimum = UPDATE | 11,
816    FeeInsufficient = UPDATE | 12,
817    IncorrectTlcExpiry = UPDATE | 13,
818    ExpiryTooSoon = PERM | 14,
819    IncorrectOrUnknownPaymentDetails = PERM | 15,
820    InvoiceExpired = PERM | 16,
821    InvoiceCancelled = PERM | 17,
822    FinalIncorrectExpiryDelta = 18,
823    FinalIncorrectTlcAmount = 19,
824    ChannelDisabled = UPDATE | 20,
825    ExpiryTooFar = PERM | 21,
826    InvalidOnionPayload = PERM | 22,
827    HoldTlcTimeout = PERM | 23,
828    InvalidOnionError = BADONION | PERM | 25,
829    IncorrectTlcDirection = PERM | 26,
830}
831
832impl TlcErrorCode {
833    pub fn is_node(&self) -> bool {
834        *self as u16 & NODE != 0
835    }
836
837    pub fn is_bad_onion(&self) -> bool {
838        *self as u16 & BADONION != 0
839    }
840
841    pub fn is_perm(&self) -> bool {
842        *self as u16 & PERM != 0
843    }
844
845    pub fn is_update(&self) -> bool {
846        *self as u16 & UPDATE != 0
847    }
848}
849
850use ckb_types::packed::Script;
851
852/// Data for sending a payment.
853#[serde_as]
854#[derive(Clone, Debug, Serialize, Deserialize)]
855pub struct SendPaymentData {
856    pub target_pubkey: Pubkey,
857    pub amount: u128,
858    pub payment_hash: Hash256,
859    pub invoice: Option<String>,
860    pub final_tlc_expiry_delta: u64,
861    pub tlc_expiry_limit: u64,
862    pub timeout: Option<u64>,
863    pub max_fee_amount: Option<u128>,
864    /// The number of parts for the payment, only used for multi-part payment
865    pub max_parts: Option<u64>,
866    pub keysend: bool,
867    #[serde_as(as = "Option<EntityHex>")]
868    pub udt_type_script: Option<Script>,
869    pub preimage: Option<Hash256>,
870    pub custom_records: Option<PaymentCustomRecords>,
871    pub allow_self_payment: bool,
872    pub hop_hints: Vec<HopHint>,
873    pub router: Vec<RouterHop>,
874    /// This flag indicates the invoice whether to allow multi-path payment.
875    pub allow_mpp: bool,
876    pub dry_run: bool,
877    /// Optional explicit trampoline hops.
878    ///
879    /// When set to a non-empty list `[t1, t2, ...]`, routing will only find a path from the
880    /// payer to `t1`, and the inner trampoline onion will encode `t1 -> t2 -> ... -> final`.
881    pub trampoline_hops: Option<Vec<Pubkey>>,
882    #[serde(default)]
883    pub trampoline_context: Option<TrampolineContext>,
884}
885
886/// A payment session tracking the state of a payment attempt.
887#[derive(Clone, Debug, Serialize, Deserialize)]
888pub struct PaymentSession {
889    pub request: SendPaymentData,
890    pub last_error: Option<String>,
891    #[serde(default)]
892    pub last_error_code: Option<TlcErrorCode>,
893    /// For non-MPP, this is the maximum number of single attempt retry limit.
894    /// For MPP, this is the sum limit of all parts' retry times.
895    pub try_limit: u32,
896    pub status: PaymentStatus,
897    pub created_at: u64,
898    pub last_updated_at: u64,
899    /// Runtime cache for attempts; not persisted.
900    #[serde(skip)]
901    pub cached_attempts: Vec<Attempt>,
902}
903
904/// Default maximum number of parts for a multi-part payment.
905pub const DEFAULT_MAX_PARTS: u64 = 12;
906
907/// Default retry limit per attempt in MPP mode.
908pub const DEFAULT_PAYMENT_MPP_ATTEMPT_TRY_LIMIT: u32 = 3;
909
910impl SendPaymentData {
911    /// Maximum number of parallel parts for this payment.
912    pub fn max_parts(&self) -> usize {
913        self.max_parts.unwrap_or(DEFAULT_MAX_PARTS) as usize
914    }
915
916    /// Whether this payment allows multi-path payment (MPP).
917    pub fn allow_mpp(&self) -> bool {
918        // only allow mpp if max_parts is greater than 1 and not keysend
919        self.allow_mpp && self.max_parts() > 1 && !self.keysend
920    }
921
922    /// Whether this payment uses trampoline routing.
923    pub fn use_trampoline_routing(&self) -> bool {
924        self.trampoline_hops
925            .as_ref()
926            .is_some_and(|hops| !hops.is_empty())
927    }
928}
929
930impl PaymentSession {
931    pub fn update_with_attempt(&mut self, attempt: Attempt) {
932        if let Some(a) = self.cached_attempts.iter_mut().find(|a| a.id == attempt.id) {
933            *a = attempt;
934        }
935        self.status = self.calc_payment_status();
936    }
937
938    pub fn retry_times(&self) -> u32 {
939        self.attempts().map(|a| a.tried_times).sum()
940    }
941
942    pub fn allow_mpp(&self) -> bool {
943        self.request.allow_mpp()
944    }
945
946    pub fn payment_hash(&self) -> crate::Hash256 {
947        self.request.payment_hash
948    }
949
950    pub fn is_payment_with_router(&self) -> bool {
951        !self.request.router.is_empty()
952    }
953
954    pub fn is_dry_run(&self) -> bool {
955        self.request.dry_run
956    }
957
958    pub fn attempts(&self) -> std::slice::Iter<'_, Attempt> {
959        self.cached_attempts.iter()
960    }
961
962    #[cfg(test)]
963    pub fn all_attempts_with_status(&self) -> Vec<(u64, AttemptStatus, Option<String>, u32, u128)> {
964        self.cached_attempts
965            .iter()
966            .map(|a| {
967                (
968                    a.id,
969                    a.status,
970                    a.last_error.clone(),
971                    a.tried_times,
972                    a.route.receiver_amount(),
973                )
974            })
975            .collect()
976    }
977
978    pub fn attempts_count(&self) -> usize {
979        self.cached_attempts.len()
980    }
981
982    pub fn max_parts(&self) -> usize {
983        if self.allow_mpp() && !self.request.use_trampoline_routing() {
984            self.request.max_parts()
985        } else {
986            1
987        }
988    }
989
990    pub fn active_attempts(&self) -> Vec<&Attempt> {
991        self.attempts().filter(|a| a.is_active()).collect()
992    }
993
994    pub fn fee_paid(&self) -> u128 {
995        if self.request.use_trampoline_routing() {
996            let total_sent: u128 = self
997                .active_attempts()
998                .iter()
999                .map(|a| a.route.nodes.first().map_or(0, |n| n.amount))
1000                .sum();
1001            let total_received = self.request.amount;
1002            total_sent.saturating_sub(total_received)
1003        } else {
1004            self.active_attempts().iter().map(|a| a.route.fee()).sum()
1005        }
1006    }
1007
1008    pub fn remain_fee_amount(&self) -> Option<u128> {
1009        let max_fee_amount = self.request.max_fee_amount?;
1010        let remain_fee = max_fee_amount.saturating_sub(self.fee_paid());
1011        Some(remain_fee)
1012    }
1013
1014    pub fn remain_amount(&self) -> u128 {
1015        let sent_amount = self
1016            .active_attempts()
1017            .iter()
1018            .map(|a| a.route.receiver_amount())
1019            .sum::<u128>();
1020        self.request.amount.saturating_sub(sent_amount)
1021    }
1022
1023    pub fn new_attempt(
1024        &self,
1025        attempt_id: u64,
1026        source: Pubkey,
1027        target: Pubkey,
1028        route_hops: Vec<PaymentHopData>,
1029    ) -> Attempt {
1030        let now = crate::now_timestamp_as_millis_u64();
1031        let payment_hash = self.payment_hash();
1032        let hash = payment_hash;
1033        let try_limit = if self.allow_mpp() {
1034            DEFAULT_PAYMENT_MPP_ATTEMPT_TRY_LIMIT
1035        } else {
1036            self.try_limit
1037        };
1038
1039        let route = SessionRoute::new(source, target, &route_hops);
1040
1041        Attempt {
1042            id: attempt_id,
1043            hash,
1044            try_limit,
1045            tried_times: 1,
1046            payment_hash,
1047            route,
1048            route_hops,
1049            session_key: [0; 32],
1050            preimage: None,
1051            created_at: now,
1052            last_updated_at: now,
1053            last_error: None,
1054            status: AttemptStatus::Created,
1055        }
1056    }
1057
1058    pub fn append_attempt(&mut self, attempt: Attempt) {
1059        self.cached_attempts.push(attempt);
1060    }
1061
1062    pub fn allow_more_attempts(&self) -> bool {
1063        if self.status.is_final() {
1064            return false;
1065        }
1066
1067        if self.remain_amount() == 0 {
1068            return false;
1069        }
1070
1071        if self.retry_times() >= self.try_limit {
1072            return false;
1073        }
1074
1075        if self.active_attempts().len() >= self.max_parts() {
1076            return false;
1077        }
1078
1079        true
1080    }
1081
1082    pub fn calc_payment_status(&self) -> PaymentStatus {
1083        if self.cached_attempts.is_empty() || self.status.is_final() {
1084            return self.status;
1085        }
1086
1087        if self.attempts().any(|a| a.is_inflight()) {
1088            return PaymentStatus::Inflight;
1089        }
1090
1091        if self.attempts().all(|a| a.is_failed()) && !self.allow_more_attempts() {
1092            return PaymentStatus::Failed;
1093        }
1094
1095        if self.success_attempts_amount_is_enough() {
1096            return PaymentStatus::Success;
1097        }
1098
1099        PaymentStatus::Created
1100    }
1101
1102    pub fn set_inflight_status(&mut self) {
1103        self.status = PaymentStatus::Inflight;
1104        self.last_updated_at = crate::now_timestamp_as_millis_u64();
1105    }
1106
1107    pub fn set_success_status(&mut self) {
1108        self.status = PaymentStatus::Success;
1109        self.last_updated_at = crate::now_timestamp_as_millis_u64();
1110        self.last_error = None;
1111        self.last_error_code = None;
1112    }
1113
1114    pub fn set_failed_status(&mut self, error: &str) {
1115        self.status = PaymentStatus::Failed;
1116        self.last_updated_at = crate::now_timestamp_as_millis_u64();
1117        self.last_error = Some(error.to_string());
1118    }
1119
1120    fn success_attempts_amount_is_enough(&self) -> bool {
1121        let success_amount: u128 = self
1122            .cached_attempts
1123            .iter()
1124            .filter_map(|a| {
1125                if a.is_success() {
1126                    Some(a.route.receiver_amount())
1127                } else {
1128                    None
1129                }
1130            })
1131            .sum();
1132        success_amount >= self.request.amount
1133    }
1134}
1135
1136/// A timed payment result for a channel direction, used to track success/failure
1137/// history for probability-based routing.
1138#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
1139pub struct TimedResult {
1140    pub fail_time: u64,
1141    pub fail_amount: u128,
1142    pub success_time: u64,
1143    pub success_amount: u128,
1144}
1145
1146/// The direction of a channel payment.
1147/// Forward means from node_a to node_b;
1148/// Backward means from node_b to node_a.
1149#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1150pub enum Direction {
1151    Forward,
1152    Backward,
1153}