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