@@ -13,31 +13,67 @@ use crate::data_store::{StorableObject, StorableObjectUpdate};
1313use crate :: payment:: store:: PaymentDetailsUpdate ;
1414use crate :: payment:: { PaymentDetails , PaymentKind } ;
1515
16+ /// One candidate transaction in an interactive-funding (splice) RBF history, holding this node's
17+ /// share of the funding amount and fee for that candidate. Both are `None` for a candidate this
18+ /// node did not contribute to — e.g. a counterparty-initiated round before our `splice_in` joined
19+ /// it via RBF. Recorded per pending payment so that, on confirmation, the payment reports the
20+ /// figures of the candidate that actually confirmed, which need not be the last one broadcast.
21+ #[ derive( Clone , Debug , PartialEq , Eq ) ]
22+ pub ( crate ) struct FundingTxCandidate {
23+ /// The candidate's broadcast transaction id.
24+ pub txid : Txid ,
25+ /// This node's share of the funding amount for this candidate, in millisatoshis, or `None` if
26+ /// this node did not contribute to it.
27+ pub amount_msat : Option < u64 > ,
28+ /// This node's share of the on-chain fee for this candidate, in millisatoshis, or `None` if
29+ /// this node did not contribute to it.
30+ pub fee_paid_msat : Option < u64 > ,
31+ }
32+
33+ impl_writeable_tlv_based ! ( FundingTxCandidate , {
34+ ( 0 , txid, required) ,
35+ ( 2 , amount_msat, option) ,
36+ ( 4 , fee_paid_msat, option) ,
37+ } ) ;
38+
1639/// Represents a pending payment
1740#[ derive( Clone , Debug , PartialEq , Eq ) ]
1841pub struct PendingPaymentDetails {
1942 /// The full payment details
2043 pub details : PaymentDetails ,
2144 /// Transaction IDs that have replaced or conflict with this payment.
2245 pub conflicting_txids : Vec < Txid > ,
46+ /// For interactive funding (splices), this node's per-candidate funding figures across the
47+ /// RBF history, keyed by each candidate's txid. Empty for non-funding payments and for
48+ /// records written before per-candidate tracking existed.
49+ pub ( crate ) candidates : Vec < FundingTxCandidate > ,
2350}
2451
2552impl PendingPaymentDetails {
26- pub ( crate ) fn new ( details : PaymentDetails , conflicting_txids : Vec < Txid > ) -> Self {
27- Self { details, conflicting_txids }
53+ pub ( crate ) fn new (
54+ details : PaymentDetails , conflicting_txids : Vec < Txid > , candidates : Vec < FundingTxCandidate > ,
55+ ) -> Self {
56+ Self { details, conflicting_txids, candidates }
57+ }
58+
59+ /// Returns this node's recorded funding figures for the candidate with the given txid, if any.
60+ pub ( crate ) fn candidate ( & self , txid : Txid ) -> Option < & FundingTxCandidate > {
61+ self . candidates . iter ( ) . find ( |candidate| candidate. txid == txid)
2862 }
2963}
3064
3165impl_writeable_tlv_based ! ( PendingPaymentDetails , {
3266 ( 0 , details, required) ,
3367 ( 2 , conflicting_txids, optional_vec) ,
68+ ( 4 , candidates, optional_vec) ,
3469} ) ;
3570
3671#[ derive( Clone , Debug , PartialEq , Eq ) ]
3772pub ( crate ) struct PendingPaymentDetailsUpdate {
3873 pub id : PaymentId ,
3974 pub payment_update : Option < PaymentDetailsUpdate > ,
4075 pub conflicting_txids : Option < Vec < Txid > > ,
76+ pub candidates : Vec < FundingTxCandidate > ,
4177}
4278
4379impl StorableObject for PendingPaymentDetails {
@@ -69,6 +105,13 @@ impl StorableObject for PendingPaymentDetails {
69105 updated |= self . conflicting_txids . len ( ) != conflicts_len;
70106 }
71107
108+ // Each classify passes the complete candidate history, so a non-empty update replaces the
109+ // stored list. An empty update (e.g. a non-funding payment) leaves it untouched.
110+ if !update. candidates . is_empty ( ) && self . candidates != update. candidates {
111+ self . candidates = update. candidates ;
112+ updated = true ;
113+ }
114+
72115 updated
73116 }
74117
@@ -90,16 +133,73 @@ impl From<&PendingPaymentDetails> for PendingPaymentDetailsUpdate {
90133 } else {
91134 Some ( value. conflicting_txids . clone ( ) )
92135 } ;
93- Self { id : value. id ( ) , payment_update : Some ( value. details . to_update ( ) ) , conflicting_txids }
136+ Self {
137+ id : value. id ( ) ,
138+ payment_update : Some ( value. details . to_update ( ) ) ,
139+ conflicting_txids,
140+ candidates : value. candidates . clone ( ) ,
141+ }
94142 }
95143}
96144
97145#[ cfg( test) ]
98146mod tests {
147+ use super :: * ;
148+ use crate :: payment:: store:: ConfirmationStatus ;
149+ use crate :: payment:: { PaymentDirection , PaymentKind , PaymentStatus } ;
99150 use bitcoin:: hashes:: Hash ;
100151
101- use super :: * ;
102- use crate :: payment:: { ConfirmationStatus , PaymentDirection , PaymentKind , PaymentStatus } ;
152+ #[ test]
153+ fn pending_payment_candidate_lookup ( ) {
154+ let payment_id = PaymentId ( [ 1u8 ; 32 ] ) ;
155+ let first_txid = Txid :: from_byte_array ( [ 2u8 ; 32 ] ) ;
156+ let rbf_txid = Txid :: from_byte_array ( [ 3u8 ; 32 ] ) ;
157+
158+ // A leading counterparty-initiated round we didn't contribute to (no figures), then our own
159+ // original and RBF candidates.
160+ let counterparty_txid = Txid :: from_byte_array ( [ 4u8 ; 32 ] ) ;
161+ let candidates = vec ! [
162+ FundingTxCandidate { txid: counterparty_txid, amount_msat: None , fee_paid_msat: None } ,
163+ FundingTxCandidate {
164+ txid: first_txid,
165+ amount_msat: Some ( 1_000_000 ) ,
166+ fee_paid_msat: Some ( 1_000 ) ,
167+ } ,
168+ FundingTxCandidate {
169+ txid: rbf_txid,
170+ amount_msat: Some ( 1_000_000 ) ,
171+ fee_paid_msat: Some ( 5_000 ) ,
172+ } ,
173+ ] ;
174+
175+ // The stored details only need to be a valid funding payment; `candidate` resolves figures
176+ // purely from the recorded candidate list.
177+ let details = PaymentDetails :: new (
178+ payment_id,
179+ PaymentKind :: Onchain {
180+ txid : rbf_txid,
181+ status : ConfirmationStatus :: Unconfirmed ,
182+ tx_type : None ,
183+ } ,
184+ Some ( 1_000_000 ) ,
185+ Some ( 5_000 ) ,
186+ PaymentDirection :: Outbound ,
187+ PaymentStatus :: Pending ,
188+ ) ;
189+ let pending =
190+ PendingPaymentDetails :: new ( details, vec ! [ first_txid, counterparty_txid] , candidates) ;
191+
192+ // Each candidate resolves to its own figures, so a non-last candidate that confirms reports
193+ // its own (lower) fee rather than the last-broadcast candidate's.
194+ assert_eq ! ( pending. candidate( first_txid) . and_then( |c| c. fee_paid_msat) , Some ( 1_000 ) ) ;
195+ assert_eq ! ( pending. candidate( rbf_txid) . and_then( |c| c. fee_paid_msat) , Some ( 5_000 ) ) ;
196+ // A candidate we didn't contribute to carries no figures, so the payment reports `None`
197+ // rather than another candidate's stale figures.
198+ let counterparty = pending. candidate ( counterparty_txid) . expect ( "candidate is recorded" ) ;
199+ assert_eq ! ( counterparty. amount_msat, None ) ;
200+ assert_eq ! ( counterparty. fee_paid_msat, None ) ;
201+ assert_eq ! ( pending. candidate( Txid :: from_byte_array( [ 9u8 ; 32 ] ) ) , None ) ;
202+ }
103203
104204 fn test_txid ( byte : u8 ) -> Txid {
105205 Txid :: from_byte_array ( [ byte; 32 ] )
@@ -125,10 +225,12 @@ mod tests {
125225 let mut pending_payment = PendingPaymentDetails :: new (
126226 pending_onchain_payment ( payment_id, replacement_txid) ,
127227 vec ! [ original_txid] ,
228+ Vec :: new ( ) ,
128229 ) ;
129230 let update = PendingPaymentDetails :: new (
130231 pending_onchain_payment ( payment_id, original_txid) ,
131232 Vec :: new ( ) ,
233+ Vec :: new ( ) ,
132234 )
133235 . to_update ( ) ;
134236
0 commit comments