use crate::gen::fiber as molecule_fiber;
use crate::Hash256;
use crate::{EntityHex, Pubkey, SliceHex};
use ckb_types::prelude::{Pack, Unpack};
use molecule::prelude::{Builder, Byte, Entity};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use std::collections::HashMap;
use strum::{AsRefStr, EnumString};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum PaymentStatus {
Created,
Inflight,
Success,
Failed,
}
impl PaymentStatus {
pub fn is_final(&self) -> bool {
matches!(self, PaymentStatus::Success | PaymentStatus::Failed)
}
}
pub const USER_CUSTOM_RECORDS_MAX_INDEX: u32 = 65535;
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Default)]
pub struct PaymentCustomRecords {
pub data: HashMap<u32, Vec<u8>>,
}
impl From<PaymentCustomRecords> for molecule_fiber::CustomRecords {
fn from(custom_records: PaymentCustomRecords) -> Self {
molecule_fiber::CustomRecords::new_builder()
.data(
custom_records
.data
.into_iter()
.map(|(key, val)| {
molecule_fiber::CustomRecordDataPairBuilder::default()
.key(key.pack())
.value(val.pack())
.build()
})
.collect(),
)
.build()
}
}
impl From<molecule_fiber::CustomRecords> for PaymentCustomRecords {
fn from(custom_records: molecule_fiber::CustomRecords) -> Self {
PaymentCustomRecords {
data: custom_records
.data()
.into_iter()
.map(|pair| (pair.key().unpack(), pair.value().unpack()))
.collect(),
}
}
}
#[derive(Eq, PartialEq, Debug)]
pub struct BasicMppPaymentData {
pub payment_secret: crate::Hash256,
pub total_amount: u128,
}
impl BasicMppPaymentData {
pub const CUSTOM_RECORD_KEY: u32 = USER_CUSTOM_RECORDS_MAX_INDEX + 1;
pub fn new(payment_secret: crate::Hash256, total_amount: u128) -> Self {
Self {
payment_secret,
total_amount,
}
}
fn to_vec(&self) -> Vec<u8> {
let mut vec = Vec::new();
vec.extend_from_slice(self.payment_secret.as_ref());
vec.extend_from_slice(&self.total_amount.to_le_bytes());
vec
}
pub fn write(&self, custom_records: &mut PaymentCustomRecords) {
custom_records
.data
.insert(Self::CUSTOM_RECORD_KEY, self.to_vec());
}
pub fn read(custom_records: &PaymentCustomRecords) -> Option<Self> {
custom_records
.data
.get(&Self::CUSTOM_RECORD_KEY)
.and_then(|data| {
if data.len() != 32 + 16 {
return None;
}
let secret: [u8; 32] = data[..32].try_into().unwrap();
let payment_secret = crate::Hash256::from(secret);
let total_amount = u128::from_le_bytes(data[32..].try_into().unwrap());
Some(Self::new(payment_secret, total_amount))
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum AttemptStatus {
Created,
Inflight,
Retrying,
Success,
Failed,
}
impl AttemptStatus {
pub fn is_final(&self) -> bool {
matches!(self, AttemptStatus::Success | AttemptStatus::Failed)
}
}
use crate::protocol::ChannelUpdate;
use ckb_types::packed::OutPoint;
use std::fmt::Display;
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum TlcErrData {
ChannelFailed {
#[serde_as(as = "EntityHex")]
channel_outpoint: OutPoint,
channel_update: Option<ChannelUpdate>,
node_id: Pubkey,
},
NodeFailed {
node_id: Pubkey,
},
TrampolineFailed {
node_id: Pubkey,
#[serde_as(as = "SliceHex")]
inner_error_packet: Vec<u8>,
},
}
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TlcErr {
pub error_code: TlcErrorCode,
pub extra_data: Option<TlcErrData>,
}
impl Display for TlcErr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.extra_data {
Some(TlcErrData::TrampolineFailed {
node_id,
inner_error_packet,
}) => write!(
f,
"{} (TrampolineFailed node_id={:?} inner_error_packet_len={})",
self.error_code_as_str(),
node_id,
inner_error_packet.len()
),
_ => write!(f, "{}", self.error_code_as_str()),
}
}
}
impl TlcErr {
pub fn new(error_code: TlcErrorCode) -> Self {
TlcErr {
error_code,
extra_data: None,
}
}
pub fn new_node_fail(error_code: TlcErrorCode, node_id: Pubkey) -> Self {
TlcErr {
error_code,
extra_data: Some(TlcErrData::NodeFailed { node_id }),
}
}
pub fn new_channel_fail(
error_code: TlcErrorCode,
node_id: Pubkey,
channel_outpoint: OutPoint,
channel_update: Option<ChannelUpdate>,
) -> Self {
TlcErr {
error_code,
extra_data: Some(TlcErrData::ChannelFailed {
node_id,
channel_outpoint,
channel_update,
}),
}
}
pub fn error_node_id(&self) -> Option<Pubkey> {
match &self.extra_data {
Some(TlcErrData::NodeFailed { node_id }) => Some(*node_id),
Some(TlcErrData::ChannelFailed { node_id, .. }) => Some(*node_id),
Some(TlcErrData::TrampolineFailed { node_id, .. }) => Some(*node_id),
_ => None,
}
}
pub fn error_channel_outpoint(&self) -> Option<OutPoint> {
match &self.extra_data {
Some(TlcErrData::ChannelFailed {
channel_outpoint, ..
}) => Some(channel_outpoint.clone()),
_ => None,
}
}
pub fn error_code(&self) -> TlcErrorCode {
self.error_code
}
pub fn error_code_as_str(&self) -> String {
let error_code: TlcErrorCode = self.error_code;
error_code.as_ref().to_string()
}
pub fn error_code_as_u16(&self) -> u16 {
self.error_code.into()
}
pub fn set_extra_data(&mut self, extra_data: TlcErrData) {
self.extra_data = Some(extra_data);
}
pub fn serialize(&self) -> Vec<u8> {
molecule_fiber::TlcErr::try_from(self.clone())
.expect("TlcErr serialization should not fail for valid TlcErr")
.as_slice()
.to_vec()
}
pub fn deserialize(data: &[u8]) -> Option<Self> {
molecule_fiber::TlcErr::from_slice(data)
.ok()
.and_then(|e| TlcErr::try_from(e).ok())
}
}
impl TryFrom<TlcErrData> for molecule_fiber::TlcErrData {
type Error = anyhow::Error;
fn try_from(tlc_err_data: TlcErrData) -> Result<Self, Self::Error> {
match tlc_err_data {
TlcErrData::ChannelFailed {
channel_outpoint,
channel_update,
node_id,
} => Ok(molecule_fiber::ChannelFailed::new_builder()
.channel_outpoint(channel_outpoint)
.channel_update(
molecule_fiber::ChannelUpdateOpt::new_builder()
.set(channel_update.map(|x| x.into()))
.build(),
)
.node_id(node_id.into())
.build()
.into()),
TlcErrData::NodeFailed { node_id } => Ok(molecule_fiber::NodeFailed::new_builder()
.node_id(node_id.into())
.build()
.into()),
TlcErrData::TrampolineFailed {
node_id,
inner_error_packet,
} => Ok(molecule_fiber::TrampolineFailed::new_builder()
.node_id(node_id.into())
.inner_error_packet(inner_error_packet.pack())
.build()
.into()),
}
}
}
impl TryFrom<molecule_fiber::TlcErrData> for TlcErrData {
type Error = anyhow::Error;
fn try_from(tlc_err_data: molecule_fiber::TlcErrData) -> Result<Self, Self::Error> {
match tlc_err_data.to_enum() {
molecule_fiber::TlcErrDataUnion::ChannelFailed(channel_failed) => {
Ok(TlcErrData::ChannelFailed {
channel_outpoint: channel_failed.channel_outpoint(),
channel_update: channel_failed
.channel_update()
.to_opt()
.map(|x| x.try_into())
.transpose()?,
node_id: channel_failed.node_id().try_into()?,
})
}
molecule_fiber::TlcErrDataUnion::NodeFailed(node_failed) => {
Ok(TlcErrData::NodeFailed {
node_id: node_failed.node_id().try_into()?,
})
}
molecule_fiber::TlcErrDataUnion::TrampolineFailed(trampoline_failed) => {
Ok(TlcErrData::TrampolineFailed {
node_id: trampoline_failed.node_id().try_into()?,
inner_error_packet: trampoline_failed.inner_error_packet().unpack(),
})
}
}
}
}
impl TryFrom<TlcErr> for molecule_fiber::TlcErr {
type Error = anyhow::Error;
fn try_from(tlc_err: TlcErr) -> Result<Self, Self::Error> {
Ok(molecule_fiber::TlcErr::new_builder()
.error_code(tlc_err.error_code_as_u16().into())
.extra_data(
molecule_fiber::TlcErrDataOpt::new_builder()
.set(tlc_err.extra_data.map(|data| data.try_into()).transpose()?)
.build(),
)
.build())
}
}
impl TryFrom<molecule_fiber::TlcErr> for TlcErr {
type Error = anyhow::Error;
fn try_from(tlc_err: molecule_fiber::TlcErr) -> Result<Self, Self::Error> {
let code: u16 = tlc_err.error_code().into();
let error_code = TlcErrorCode::try_from(code)
.map_err(|_| anyhow::anyhow!("Invalid TLC error code: {}", code))?;
let extra_data = tlc_err
.extra_data()
.to_opt()
.map(|data| data.try_into())
.transpose()?;
Ok(TlcErr {
error_code,
extra_data,
})
}
}
use crate::invoice::HashAlgorithm;
#[serde_as]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CurrentPaymentHopData {
pub amount: u128,
pub expiry: u64,
pub payment_preimage: Option<crate::Hash256>,
pub hash_algorithm: HashAlgorithm,
pub funding_tx_hash: crate::Hash256,
pub custom_records: Option<PaymentCustomRecords>,
}
impl From<PaymentHopData> for CurrentPaymentHopData {
fn from(hop: PaymentHopData) -> Self {
CurrentPaymentHopData {
amount: hop.amount,
expiry: hop.expiry,
payment_preimage: hop.payment_preimage,
hash_algorithm: hop.hash_algorithm,
funding_tx_hash: hop.funding_tx_hash,
custom_records: hop.custom_records,
}
}
}
impl From<CurrentPaymentHopData> for PaymentHopData {
fn from(hop: CurrentPaymentHopData) -> Self {
PaymentHopData {
amount: hop.amount,
expiry: hop.expiry,
payment_preimage: hop.payment_preimage,
hash_algorithm: hop.hash_algorithm,
funding_tx_hash: hop.funding_tx_hash,
custom_records: hop.custom_records,
next_hop: None,
}
}
}
impl CurrentPaymentHopData {
pub fn trampoline_onion(&self) -> Option<Vec<u8>> {
self.custom_records
.as_ref()
.and_then(TrampolineOnionData::read)
}
pub fn set_trampoline_onion(&mut self, data: Vec<u8>) {
let cr = self.custom_records.get_or_insert_with(Default::default);
TrampolineOnionData::write(data, cr);
}
}
use crate::U128Hex;
#[serde_as]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SessionRouteNode {
pub pubkey: Pubkey,
#[serde_as(as = "U128Hex")]
pub amount: u128,
#[serde_as(as = "EntityHex")]
pub channel_outpoint: OutPoint,
}
use crate::U64Hex;
#[serde_as]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct RouterHop {
pub target: Pubkey,
#[serde_as(as = "EntityHex")]
pub channel_outpoint: OutPoint,
#[serde_as(as = "U128Hex")]
pub amount_received: u128,
#[serde_as(as = "U64Hex")]
pub incoming_tlc_expiry: u64,
}
#[serde_as]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HopHint {
pub pubkey: Pubkey,
#[serde_as(as = "EntityHex")]
pub channel_outpoint: OutPoint,
pub fee_rate: u64,
pub tlc_expiry_delta: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct SessionRoute {
pub nodes: Vec<SessionRouteNode>,
}
impl SessionRoute {
pub fn new(source: Pubkey, target: Pubkey, payment_hops: &[PaymentHopData]) -> Self {
let nodes = std::iter::once(source)
.chain(
payment_hops
.iter()
.map(|hop| hop.next_hop.unwrap_or(target)),
)
.zip(payment_hops)
.map(|(pubkey, hop)| SessionRouteNode {
pubkey,
channel_outpoint: OutPoint::new(
if hop.funding_tx_hash != Hash256::default() {
hop.funding_tx_hash.into()
} else {
Hash256::default().into()
},
0,
),
amount: hop.amount,
})
.collect();
Self { nodes }
}
pub fn receiver_amount(&self) -> u128 {
self.nodes.last().map_or(0, |s| s.amount)
}
pub fn fee(&self) -> u128 {
let first_amount = self.nodes.first().map_or(0, |s| s.amount);
let last_amount = self.receiver_amount();
debug_assert!(first_amount >= last_amount);
first_amount - last_amount
}
pub fn channel_outpoints(&self) -> impl Iterator<Item = (Pubkey, &OutPoint, u128)> {
self.nodes
.iter()
.map(|x| (x.pubkey, &x.channel_outpoint, x.amount))
}
}
#[serde_as]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct PaymentHopData {
pub amount: u128,
pub expiry: u64,
pub payment_preimage: Option<Hash256>,
pub hash_algorithm: HashAlgorithm,
pub funding_tx_hash: Hash256,
pub next_hop: Option<Pubkey>,
pub custom_records: Option<PaymentCustomRecords>,
}
use crate::onion::TrampolineOnionData;
impl PaymentHopData {
pub fn next_hop(&self) -> Option<Pubkey> {
self.next_hop
}
pub fn trampoline_onion(&self) -> Option<Vec<u8>> {
self.custom_records
.as_ref()
.and_then(TrampolineOnionData::read)
}
pub fn set_trampoline_onion(&mut self, data: Vec<u8>) {
let cr = self.custom_records.get_or_insert_with(Default::default);
TrampolineOnionData::write(data, cr);
}
pub fn serialize(&self) -> Vec<u8> {
molecule_fiber::PaymentHopData::from(self.clone())
.as_bytes()
.to_vec()
}
pub fn deserialize(data: &[u8]) -> Option<Self> {
molecule_fiber::PaymentHopData::from_slice(data)
.ok()
.map(|x| x.into())
}
}
impl From<PaymentHopData> for molecule_fiber::PaymentHopData {
fn from(payment_hop_data: PaymentHopData) -> Self {
molecule_fiber::PaymentHopData::new_builder()
.amount(payment_hop_data.amount.pack())
.expiry(payment_hop_data.expiry.pack())
.payment_preimage(
molecule_fiber::PaymentPreimageOpt::new_builder()
.set(payment_hop_data.payment_preimage.map(|x| x.into()))
.build(),
)
.hash_algorithm(Byte::new(payment_hop_data.hash_algorithm as u8))
.funding_tx_hash(payment_hop_data.funding_tx_hash.into())
.next_hop(
molecule_fiber::PubkeyOpt::new_builder()
.set(payment_hop_data.next_hop.map(|x| x.into()))
.build(),
)
.custom_records(
molecule_fiber::CustomRecordsOpt::new_builder()
.set(payment_hop_data.custom_records.map(|x| x.into()))
.build(),
)
.build()
}
}
impl From<molecule_fiber::PaymentHopData> for PaymentHopData {
fn from(payment_hop_data: molecule_fiber::PaymentHopData) -> Self {
let custom_records: Option<PaymentCustomRecords> =
payment_hop_data.custom_records().to_opt().map(|x| x.into());
PaymentHopData {
amount: payment_hop_data.amount().unpack(),
expiry: payment_hop_data.expiry().unpack(),
payment_preimage: payment_hop_data
.payment_preimage()
.to_opt()
.map(|x| x.into()),
hash_algorithm: payment_hop_data
.hash_algorithm()
.try_into()
.unwrap_or_default(),
funding_tx_hash: payment_hop_data.funding_tx_hash().into(),
next_hop: payment_hop_data
.next_hop()
.to_opt()
.map(|x| x.try_into())
.and_then(Result::ok),
custom_records,
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Attempt {
pub id: u64,
pub try_limit: u32,
pub tried_times: u32,
pub hash: Hash256,
pub status: AttemptStatus,
pub payment_hash: Hash256,
pub route: SessionRoute,
pub route_hops: Vec<PaymentHopData>,
pub session_key: [u8; 32],
pub preimage: Option<Hash256>,
pub created_at: u64,
pub last_updated_at: u64,
pub last_error: Option<String>,
}
impl std::fmt::Debug for Attempt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Attempt")
.field("id", &self.id)
.field("try_limit", &self.try_limit)
.field("tried_times", &self.tried_times)
.field("hash", &self.hash)
.field("status", &self.status)
.field("payment_hash", &self.payment_hash)
.field("route", &self.route)
.field("route_hops", &self.route_hops)
.field("session_key", &"[REDACTED]")
.field("preimage", &self.preimage.as_ref().map(|_| "[REDACTED]"))
.field("created_at", &self.created_at)
.field("last_updated_at", &self.last_updated_at)
.field("last_error", &self.last_error)
.finish()
}
}
impl Attempt {
pub fn set_inflight_status(&mut self) {
self.status = AttemptStatus::Inflight;
self.last_error = None;
}
pub fn set_success_status(&mut self) {
self.status = AttemptStatus::Success;
self.last_error = None;
}
pub fn set_failed_status(&mut self, error: &str, retryable: bool) {
self.last_error = Some(error.to_string());
self.last_updated_at = crate::now_timestamp_as_millis_u64();
if !retryable || self.tried_times > self.try_limit {
self.status = AttemptStatus::Failed;
} else {
self.status = AttemptStatus::Retrying;
self.tried_times += 1;
}
}
pub fn update_route(&mut self, new_route_hops: Vec<PaymentHopData>) {
self.route_hops = new_route_hops;
let sender = self.route.nodes[0].pubkey;
let receiver = self.route.nodes.last().unwrap().pubkey;
self.route = SessionRoute::new(sender, receiver, &self.route_hops);
}
pub fn is_success(&self) -> bool {
self.status == AttemptStatus::Success
}
pub fn is_inflight(&self) -> bool {
self.status == AttemptStatus::Inflight
}
pub fn is_failed(&self) -> bool {
self.status == AttemptStatus::Failed
}
pub fn is_active(&self) -> bool {
self.status != AttemptStatus::Failed
}
pub fn is_retrying(&self) -> bool {
self.status == AttemptStatus::Retrying
}
pub fn first_hop_channel_outpoint_eq(&self, out_point: &OutPoint) -> bool {
self.first_hop_channel_outpoint()
.map(|x| x.eq(out_point))
.unwrap_or_default()
}
pub fn first_hop_channel_outpoint(&self) -> Option<&OutPoint> {
self.route.nodes.first().map(|x| &x.channel_outpoint)
}
pub fn channel_outpoints(&self) -> impl Iterator<Item = (Pubkey, &OutPoint, u128)> {
self.route.channel_outpoints()
}
pub fn hops_public_keys(&self) -> Vec<Pubkey> {
self.route.nodes.iter().skip(1).map(|x| x.pubkey).collect()
}
}
use crate::channel::PrevTlcInfo;
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct TrampolineContext {
pub remaining_trampoline_onion: Vec<u8>,
pub previous_tlcs: Vec<PrevTlcInfo>,
pub hash_algorithm: HashAlgorithm,
}
const BADONION: u16 = 0x8000;
const PERM: u16 = 0x4000;
const NODE: u16 = 0x2000;
const UPDATE: u16 = 0x1000;
#[repr(u16)]
#[derive(
Debug,
Copy,
Clone,
Serialize,
Deserialize,
PartialEq,
Eq,
AsRefStr,
EnumString,
TryFromPrimitive,
IntoPrimitive,
)]
pub enum TlcErrorCode {
TemporaryNodeFailure = NODE | 2,
PermanentNodeFailure = PERM | NODE | 2,
RequiredNodeFeatureMissing = PERM | NODE | 3,
InvalidOnionVersion = BADONION | PERM | 4,
InvalidOnionHmac = BADONION | PERM | 5,
InvalidOnionKey = BADONION | PERM | 6,
TemporaryChannelFailure = UPDATE | 7,
PermanentChannelFailure = PERM | 8,
RequiredChannelFeatureMissing = PERM | 9,
UnknownNextPeer = PERM | 10,
AmountBelowMinimum = UPDATE | 11,
FeeInsufficient = UPDATE | 12,
IncorrectTlcExpiry = UPDATE | 13,
ExpiryTooSoon = PERM | 14,
IncorrectOrUnknownPaymentDetails = PERM | 15,
InvoiceExpired = PERM | 16,
InvoiceCancelled = PERM | 17,
FinalIncorrectExpiryDelta = 18,
FinalIncorrectTlcAmount = 19,
ChannelDisabled = UPDATE | 20,
ExpiryTooFar = PERM | 21,
InvalidOnionPayload = PERM | 22,
HoldTlcTimeout = PERM | 23,
InvalidOnionError = BADONION | PERM | 25,
IncorrectTlcDirection = PERM | 26,
}
impl TlcErrorCode {
pub fn is_node(&self) -> bool {
*self as u16 & NODE != 0
}
pub fn is_bad_onion(&self) -> bool {
*self as u16 & BADONION != 0
}
pub fn is_perm(&self) -> bool {
*self as u16 & PERM != 0
}
pub fn is_update(&self) -> bool {
*self as u16 & UPDATE != 0
}
}
use ckb_types::packed::Script;
#[serde_as]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SendPaymentData {
pub target_pubkey: Pubkey,
pub amount: u128,
pub payment_hash: Hash256,
pub invoice: Option<String>,
pub final_tlc_expiry_delta: u64,
pub tlc_expiry_limit: u64,
pub timeout: Option<u64>,
pub max_fee_amount: Option<u128>,
pub max_parts: Option<u64>,
pub keysend: bool,
#[serde_as(as = "Option<EntityHex>")]
pub udt_type_script: Option<Script>,
pub preimage: Option<Hash256>,
pub custom_records: Option<PaymentCustomRecords>,
pub allow_self_payment: bool,
pub hop_hints: Vec<HopHint>,
pub router: Vec<RouterHop>,
pub allow_mpp: bool,
pub dry_run: bool,
pub trampoline_hops: Option<Vec<Pubkey>>,
#[serde(default)]
pub trampoline_context: Option<TrampolineContext>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PaymentSession {
pub request: SendPaymentData,
pub last_error: Option<String>,
#[serde(default)]
pub last_error_code: Option<TlcErrorCode>,
pub try_limit: u32,
pub status: PaymentStatus,
pub created_at: u64,
pub last_updated_at: u64,
#[serde(skip)]
pub cached_attempts: Vec<Attempt>,
}
pub const DEFAULT_MAX_PARTS: u64 = 12;
pub const DEFAULT_PAYMENT_MPP_ATTEMPT_TRY_LIMIT: u32 = 3;
impl SendPaymentData {
pub fn max_parts(&self) -> usize {
self.max_parts.unwrap_or(DEFAULT_MAX_PARTS) as usize
}
pub fn allow_mpp(&self) -> bool {
self.allow_mpp && self.max_parts() > 1 && !self.keysend
}
pub fn use_trampoline_routing(&self) -> bool {
self.trampoline_hops
.as_ref()
.is_some_and(|hops| !hops.is_empty())
}
}
impl PaymentSession {
pub fn update_with_attempt(&mut self, attempt: Attempt) {
if let Some(a) = self.cached_attempts.iter_mut().find(|a| a.id == attempt.id) {
*a = attempt;
}
self.status = self.calc_payment_status();
}
pub fn retry_times(&self) -> u32 {
self.attempts().map(|a| a.tried_times).sum()
}
pub fn allow_mpp(&self) -> bool {
self.request.allow_mpp()
}
pub fn payment_hash(&self) -> crate::Hash256 {
self.request.payment_hash
}
pub fn is_payment_with_router(&self) -> bool {
!self.request.router.is_empty()
}
pub fn is_dry_run(&self) -> bool {
self.request.dry_run
}
pub fn attempts(&self) -> std::slice::Iter<'_, Attempt> {
self.cached_attempts.iter()
}
#[cfg(test)]
pub fn all_attempts_with_status(&self) -> Vec<(u64, AttemptStatus, Option<String>, u32, u128)> {
self.cached_attempts
.iter()
.map(|a| {
(
a.id,
a.status,
a.last_error.clone(),
a.tried_times,
a.route.receiver_amount(),
)
})
.collect()
}
pub fn attempts_count(&self) -> usize {
self.cached_attempts.len()
}
pub fn max_parts(&self) -> usize {
if self.allow_mpp() && !self.request.use_trampoline_routing() {
self.request.max_parts()
} else {
1
}
}
pub fn active_attempts(&self) -> Vec<&Attempt> {
self.attempts().filter(|a| a.is_active()).collect()
}
pub fn fee_paid(&self) -> u128 {
if self.request.use_trampoline_routing() {
let total_sent: u128 = self
.active_attempts()
.iter()
.map(|a| a.route.nodes.first().map_or(0, |n| n.amount))
.sum();
let total_received = self.request.amount;
total_sent.saturating_sub(total_received)
} else {
self.active_attempts().iter().map(|a| a.route.fee()).sum()
}
}
pub fn remain_fee_amount(&self) -> Option<u128> {
let max_fee_amount = self.request.max_fee_amount?;
let remain_fee = max_fee_amount.saturating_sub(self.fee_paid());
Some(remain_fee)
}
pub fn remain_amount(&self) -> u128 {
let sent_amount = self
.active_attempts()
.iter()
.map(|a| a.route.receiver_amount())
.sum::<u128>();
self.request.amount.saturating_sub(sent_amount)
}
pub fn new_attempt(
&self,
attempt_id: u64,
source: Pubkey,
target: Pubkey,
route_hops: Vec<PaymentHopData>,
) -> Attempt {
let now = crate::now_timestamp_as_millis_u64();
let payment_hash = self.payment_hash();
let hash = payment_hash;
let try_limit = if self.allow_mpp() {
DEFAULT_PAYMENT_MPP_ATTEMPT_TRY_LIMIT
} else {
self.try_limit
};
let route = SessionRoute::new(source, target, &route_hops);
Attempt {
id: attempt_id,
hash,
try_limit,
tried_times: 1,
payment_hash,
route,
route_hops,
session_key: [0; 32],
preimage: None,
created_at: now,
last_updated_at: now,
last_error: None,
status: AttemptStatus::Created,
}
}
pub fn append_attempt(&mut self, attempt: Attempt) {
self.cached_attempts.push(attempt);
}
pub fn allow_more_attempts(&self) -> bool {
if self.status.is_final() {
return false;
}
if self.remain_amount() == 0 {
return false;
}
if self.retry_times() >= self.try_limit {
return false;
}
if self.active_attempts().len() >= self.max_parts() {
return false;
}
true
}
pub fn calc_payment_status(&self) -> PaymentStatus {
if self.cached_attempts.is_empty() || self.status.is_final() {
return self.status;
}
if self.attempts().any(|a| a.is_inflight()) {
return PaymentStatus::Inflight;
}
if self.attempts().all(|a| a.is_failed()) && !self.allow_more_attempts() {
return PaymentStatus::Failed;
}
if self.success_attempts_amount_is_enough() {
return PaymentStatus::Success;
}
PaymentStatus::Created
}
pub fn set_inflight_status(&mut self) {
self.status = PaymentStatus::Inflight;
self.last_updated_at = crate::now_timestamp_as_millis_u64();
}
pub fn set_success_status(&mut self) {
self.status = PaymentStatus::Success;
self.last_updated_at = crate::now_timestamp_as_millis_u64();
self.last_error = None;
self.last_error_code = None;
}
pub fn set_failed_status(&mut self, error: &str) {
self.status = PaymentStatus::Failed;
self.last_updated_at = crate::now_timestamp_as_millis_u64();
self.last_error = Some(error.to_string());
}
fn success_attempts_amount_is_enough(&self) -> bool {
let success_amount: u128 = self
.cached_attempts
.iter()
.filter_map(|a| {
if a.is_success() {
Some(a.route.receiver_amount())
} else {
None
}
})
.sum();
success_amount >= self.request.amount
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TimedResult {
pub fail_time: u64,
pub fail_amount: u128,
pub success_time: u64,
pub success_amount: u128,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum Direction {
Forward,
Backward,
}