77
88use std:: time:: { Duration , SystemTime , UNIX_EPOCH } ;
99
10+ use bitcoin:: secp256k1:: PublicKey ;
1011use bitcoin:: { BlockHash , Txid } ;
12+ use lightning:: chain:: chaininterface:: TransactionType as LdkTransactionType ;
1113use lightning:: ln:: channelmanager:: PaymentId ;
1214use lightning:: ln:: msgs:: DecodeError ;
15+ use lightning:: ln:: types:: ChannelId ;
1316use lightning:: offers:: offer:: OfferId ;
1417use lightning:: util:: ser:: { Readable , Writeable } ;
1518use lightning:: {
@@ -282,6 +285,15 @@ impl StorableObject for PaymentDetails {
282285 }
283286 }
284287
288+ if let Some ( tx_type_update) = update. tx_type {
289+ match self . kind {
290+ PaymentKind :: Onchain { ref mut tx_type, .. } => {
291+ update_if_necessary ! ( * tx_type, tx_type_update) ;
292+ } ,
293+ _ => { } ,
294+ }
295+ }
296+
285297 if updated {
286298 self . latest_update_timestamp = SystemTime :: now ( )
287299 . duration_since ( UNIX_EPOCH )
@@ -330,6 +342,156 @@ impl_writeable_tlv_based_enum!(PaymentStatus,
330342 ( 4 , Failed ) => { }
331343) ;
332344
345+ /// A channel referenced by a [`TransactionType`].
346+ #[ derive( Clone , Debug , PartialEq , Eq ) ]
347+ #[ cfg_attr( feature = "uniffi" , derive( uniffi:: Record ) ) ]
348+ pub struct Channel {
349+ /// The `node_id` of the channel counterparty.
350+ pub counterparty_node_id : PublicKey ,
351+ /// The ID of the channel.
352+ pub channel_id : ChannelId ,
353+ }
354+
355+ impl_writeable_tlv_based ! ( Channel , {
356+ ( 0 , counterparty_node_id, required) ,
357+ ( 2 , channel_id, required) ,
358+ } ) ;
359+
360+ /// The classification of a [`PaymentKind::Onchain`] transaction, as reported by LDK when the
361+ /// transaction was broadcast.
362+ ///
363+ /// Mirrors [`lightning::chain::chaininterface::TransactionType`], retaining the channel references
364+ /// but dropping the broadcast-time contribution data; a transaction's amount and fee are tracked on
365+ /// the [`PaymentDetails`] itself.
366+ #[ derive( Clone , Debug , PartialEq , Eq ) ]
367+ #[ cfg_attr( feature = "uniffi" , derive( uniffi:: Enum ) ) ]
368+ pub enum TransactionType {
369+ /// A funding transaction establishing one or more new channels.
370+ Funding {
371+ /// The channels being funded.
372+ channels : Vec < Channel > ,
373+ } ,
374+ /// A transaction cooperatively closing a channel.
375+ CooperativeClose {
376+ /// The `node_id` of the channel counterparty.
377+ counterparty_node_id : PublicKey ,
378+ /// The ID of the channel being closed.
379+ channel_id : ChannelId ,
380+ } ,
381+ /// A transaction force-closing a channel.
382+ UnilateralClose {
383+ /// The `node_id` of the channel counterparty.
384+ counterparty_node_id : PublicKey ,
385+ /// The ID of the channel being force-closed.
386+ channel_id : ChannelId ,
387+ } ,
388+ /// An anchor transaction CPFP fee-bumping a closing transaction.
389+ AnchorBump {
390+ /// The `node_id` of the channel counterparty.
391+ counterparty_node_id : PublicKey ,
392+ /// The ID of the channel whose closing transaction is being fee-bumped.
393+ channel_id : ChannelId ,
394+ } ,
395+ /// A transaction resolving an output spendable by both us and our counterparty.
396+ Claim {
397+ /// The `node_id` of the channel counterparty.
398+ counterparty_node_id : PublicKey ,
399+ /// The ID of the channel from which outputs are being claimed.
400+ channel_id : ChannelId ,
401+ } ,
402+ /// A transaction sweeping spendable outputs to the on-chain wallet.
403+ Sweep {
404+ /// The channels from which outputs are being swept, if known.
405+ channels : Vec < Channel > ,
406+ } ,
407+ /// An interactively-negotiated funding transaction: a splice, or (once supported) a V2
408+ /// dual-funded channel open.
409+ InteractiveFunding {
410+ /// The channels participating in the negotiation.
411+ channels : Vec < Channel > ,
412+ } ,
413+ }
414+
415+ impl_writeable_tlv_based_enum ! ( TransactionType ,
416+ ( 0 , Funding ) => {
417+ ( 0 , channels, optional_vec) ,
418+ } ,
419+ ( 2 , CooperativeClose ) => {
420+ ( 0 , counterparty_node_id, required) ,
421+ ( 2 , channel_id, required) ,
422+ } ,
423+ ( 4 , UnilateralClose ) => {
424+ ( 0 , counterparty_node_id, required) ,
425+ ( 2 , channel_id, required) ,
426+ } ,
427+ ( 6 , AnchorBump ) => {
428+ ( 0 , counterparty_node_id, required) ,
429+ ( 2 , channel_id, required) ,
430+ } ,
431+ ( 8 , Claim ) => {
432+ ( 0 , counterparty_node_id, required) ,
433+ ( 2 , channel_id, required) ,
434+ } ,
435+ ( 10 , Sweep ) => {
436+ ( 0 , channels, optional_vec) ,
437+ } ,
438+ ( 12 , InteractiveFunding ) => {
439+ ( 0 , channels, optional_vec) ,
440+ }
441+ ) ;
442+
443+ impl From < LdkTransactionType > for TransactionType {
444+ fn from ( tx_type : LdkTransactionType ) -> Self {
445+ let to_channels = |channels : Vec < ( PublicKey , ChannelId ) > | -> Vec < Channel > {
446+ channels
447+ . into_iter ( )
448+ . map ( |( counterparty_node_id, channel_id) | Channel {
449+ counterparty_node_id,
450+ channel_id,
451+ } )
452+ . collect ( )
453+ } ;
454+ match tx_type {
455+ LdkTransactionType :: Funding { channels } => {
456+ TransactionType :: Funding { channels : to_channels ( channels) }
457+ } ,
458+ LdkTransactionType :: CooperativeClose { counterparty_node_id, channel_id } => {
459+ TransactionType :: CooperativeClose { counterparty_node_id, channel_id }
460+ } ,
461+ LdkTransactionType :: UnilateralClose { counterparty_node_id, channel_id } => {
462+ TransactionType :: UnilateralClose { counterparty_node_id, channel_id }
463+ } ,
464+ LdkTransactionType :: AnchorBump { counterparty_node_id, channel_id } => {
465+ TransactionType :: AnchorBump { counterparty_node_id, channel_id }
466+ } ,
467+ LdkTransactionType :: Claim { counterparty_node_id, channel_id } => {
468+ TransactionType :: Claim { counterparty_node_id, channel_id }
469+ } ,
470+ LdkTransactionType :: Sweep { channels } => {
471+ TransactionType :: Sweep { channels : to_channels ( channels) }
472+ } ,
473+ LdkTransactionType :: InteractiveFunding { candidates } => {
474+ // Every candidate (the original negotiation plus any RBF replacements) references
475+ // the same channel(s); take the active (last) candidate's channel references.
476+ let channels = candidates
477+ . last ( )
478+ . map ( |candidate| {
479+ candidate
480+ . channels
481+ . iter ( )
482+ . map ( |cf| Channel {
483+ counterparty_node_id : cf. counterparty_node_id ,
484+ channel_id : cf. channel_id ,
485+ } )
486+ . collect ( )
487+ } )
488+ . unwrap_or_default ( ) ;
489+ TransactionType :: InteractiveFunding { channels }
490+ } ,
491+ }
492+ }
493+ }
494+
333495/// Represents the kind of a payment.
334496#[ derive( Clone , Debug , PartialEq , Eq ) ]
335497#[ cfg_attr( feature = "uniffi" , derive( uniffi:: Enum ) ) ]
@@ -345,6 +507,11 @@ pub enum PaymentKind {
345507 txid : Txid ,
346508 /// The confirmation status of this payment.
347509 status : ConfirmationStatus ,
510+ /// The classification of this transaction, if known.
511+ ///
512+ /// `None` for plain on-chain sends, and for records written by versions of LDK Node that
513+ /// predate on-chain transaction classification.
514+ tx_type : Option < TransactionType > ,
348515 } ,
349516 /// A [BOLT 11] payment.
350517 ///
@@ -423,6 +590,7 @@ pub enum PaymentKind {
423590impl_writeable_tlv_based_enum ! ( PaymentKind ,
424591 ( 0 , Onchain ) => {
425592 ( 0 , txid, required) ,
593+ ( 1 , tx_type, option) ,
426594 ( 2 , status, required) ,
427595 } ,
428596 ( 2 , Bolt11 ) => {
@@ -522,6 +690,7 @@ pub(crate) struct PaymentDetailsUpdate {
522690 pub status : Option < PaymentStatus > ,
523691 pub confirmation_status : Option < ConfirmationStatus > ,
524692 pub txid : Option < Txid > ,
693+ pub tx_type : Option < Option < TransactionType > > ,
525694}
526695
527696impl PaymentDetailsUpdate {
@@ -538,6 +707,7 @@ impl PaymentDetailsUpdate {
538707 status : None ,
539708 confirmation_status : None ,
540709 txid : None ,
710+ tx_type : None ,
541711 }
542712 }
543713}
@@ -552,9 +722,11 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate {
552722 _ => ( None , None , None ) ,
553723 } ;
554724
555- let ( confirmation_status, txid) = match & value. kind {
556- PaymentKind :: Onchain { status, txid, .. } => ( Some ( * status) , Some ( * txid) ) ,
557- _ => ( None , None ) ,
725+ let ( confirmation_status, txid, tx_type) = match & value. kind {
726+ PaymentKind :: Onchain { status, txid, tx_type } => {
727+ ( Some ( * status) , Some ( * txid) , Some ( tx_type. clone ( ) ) )
728+ } ,
729+ _ => ( None , None , None ) ,
558730 } ;
559731
560732 let counterparty_skimmed_fee_msat = match value. kind {
@@ -576,6 +748,7 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate {
576748 status : Some ( value. status ) ,
577749 confirmation_status,
578750 txid,
751+ tx_type,
579752 }
580753 }
581754}
@@ -697,6 +870,57 @@ mod tests {
697870 }
698871 }
699872
873+ #[ derive( Clone , Debug , PartialEq , Eq ) ]
874+ struct OldOnchainKind {
875+ txid : Txid ,
876+ status : ConfirmationStatus ,
877+ }
878+
879+ impl_writeable_tlv_based ! ( OldOnchainKind , {
880+ ( 0 , txid, required) ,
881+ ( 2 , status, required) ,
882+ } ) ;
883+
884+ #[ test]
885+ fn onchain_tx_type_deser_compat ( ) {
886+ use bitcoin:: hashes:: Hash ;
887+ use std:: str:: FromStr ;
888+
889+ let txid = Txid :: from_byte_array ( [ 7u8 ; 32 ] ) ;
890+ let status = ConfirmationStatus :: Unconfirmed ;
891+
892+ // An `Onchain` record written before `tx_type` existed (only txid + status) must read back
893+ // with `tx_type: None`.
894+ let old = OldOnchainKind { txid, status } ;
895+ let mut on_disk = Vec :: new ( ) ;
896+ 0u8 . write ( & mut on_disk) . unwrap ( ) ; // the `Onchain` enum discriminant
897+ on_disk. extend_from_slice ( & old. encode ( ) ) ;
898+ match PaymentKind :: read ( & mut & * on_disk) . unwrap ( ) {
899+ PaymentKind :: Onchain { txid : t, status : s, tx_type } => {
900+ assert_eq ! ( t, txid) ;
901+ assert_eq ! ( s, status) ;
902+ assert_eq ! ( tx_type, None ) ;
903+ } ,
904+ other => panic ! ( "Unexpected kind: {:?}" , other) ,
905+ }
906+
907+ // A populated `tx_type` round-trips.
908+ let kind = PaymentKind :: Onchain {
909+ txid,
910+ status,
911+ tx_type : Some ( TransactionType :: InteractiveFunding {
912+ channels : vec ! [ Channel {
913+ counterparty_node_id: PublicKey :: from_str(
914+ "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" ,
915+ )
916+ . unwrap( ) ,
917+ channel_id: ChannelId ( [ 3u8 ; 32 ] ) ,
918+ } ] ,
919+ } ) ,
920+ } ;
921+ assert_eq ! ( kind, PaymentKind :: read( & mut & * kind. encode( ) ) . unwrap( ) ) ;
922+ }
923+
700924 #[ derive( Clone , Debug , PartialEq , Eq ) ]
701925 struct LegacyBolt11JitKind {
702926 hash : PaymentHash ,
0 commit comments