@@ -177,6 +177,7 @@ pub struct TxGraph<A = ConfirmationBlockTime> {
177
177
txs : HashMap < Txid , TxNodeInternal > ,
178
178
spends : BTreeMap < OutPoint , HashSet < Txid > > ,
179
179
anchors : HashMap < Txid , BTreeSet < A > > ,
180
+ first_seen : HashMap < Txid , u64 > ,
180
181
last_seen : HashMap < Txid , u64 > ,
181
182
last_evicted : HashMap < Txid , u64 > ,
182
183
@@ -195,6 +196,7 @@ impl<A> Default for TxGraph<A> {
195
196
txs : Default :: default ( ) ,
196
197
spends : Default :: default ( ) ,
197
198
anchors : Default :: default ( ) ,
199
+ first_seen : Default :: default ( ) ,
198
200
last_seen : Default :: default ( ) ,
199
201
last_evicted : Default :: default ( ) ,
200
202
txs_by_highest_conf_heights : Default :: default ( ) ,
@@ -214,8 +216,10 @@ pub struct TxNode<'a, T, A> {
214
216
pub tx : T ,
215
217
/// The blocks that the transaction is "anchored" in.
216
218
pub anchors : & ' a BTreeSet < A > ,
219
+ /// The first-seen unix timestamp of the transaction as unconfirmed.
220
+ pub first_seen : Option < u64 > ,
217
221
/// The last-seen unix timestamp of the transaction as unconfirmed.
218
- pub last_seen_unconfirmed : Option < u64 > ,
222
+ pub last_seen : Option < u64 > ,
219
223
}
220
224
221
225
impl < T , A > Deref for TxNode < ' _ , T , A > {
@@ -337,7 +341,8 @@ impl<A> TxGraph<A> {
337
341
txid,
338
342
tx : tx. clone ( ) ,
339
343
anchors : self . anchors . get ( & txid) . unwrap_or ( & self . empty_anchors ) ,
340
- last_seen_unconfirmed : self . last_seen . get ( & txid) . copied ( ) ,
344
+ first_seen : self . first_seen . get ( & txid) . copied ( ) ,
345
+ last_seen : self . last_seen . get ( & txid) . copied ( ) ,
341
346
} ) ,
342
347
TxNodeInternal :: Partial ( _) => None ,
343
348
} )
@@ -348,7 +353,7 @@ impl<A> TxGraph<A> {
348
353
& self ,
349
354
) -> impl Iterator < Item = TxNode < ' _ , Arc < Transaction > , A > > {
350
355
self . full_txs ( ) . filter_map ( |tx| {
351
- if tx. anchors . is_empty ( ) && tx. last_seen_unconfirmed . is_none ( ) {
356
+ if tx. anchors . is_empty ( ) && tx. last_seen . is_none ( ) {
352
357
Some ( tx)
353
358
} else {
354
359
None
@@ -372,7 +377,8 @@ impl<A> TxGraph<A> {
372
377
txid,
373
378
tx : tx. clone ( ) ,
374
379
anchors : self . anchors . get ( & txid) . unwrap_or ( & self . empty_anchors ) ,
375
- last_seen_unconfirmed : self . last_seen . get ( & txid) . copied ( ) ,
380
+ first_seen : self . first_seen . get ( & txid) . copied ( ) ,
381
+ last_seen : self . last_seen . get ( & txid) . copied ( ) ,
376
382
} ) ,
377
383
_ => None ,
378
384
}
@@ -787,10 +793,48 @@ impl<A: Anchor> TxGraph<A> {
787
793
changeset
788
794
}
789
795
790
- /// Inserts the given `seen_at` for `txid` into [`TxGraph`].
796
+ /// Updates the first-seen and last-seen timestamps for a given `txid` in the [`TxGraph`].
791
797
///
792
- /// Note that [`TxGraph`] only keeps track of the latest `seen_at`.
798
+ /// This method records the time a transaction was observed by updating both:
799
+ /// - the **first-seen** timestamp, which only changes if `seen_at` is earlier than the current value, and
800
+ /// - the **last-seen** timestamp, which only changes if `seen_at` is later than the current value.
801
+ ///
802
+ /// `seen_at` is a UNIX timestamp in seconds.
803
+ ///
804
+ /// Returns a [`ChangeSet`] representing any changes applied.
793
805
pub fn insert_seen_at ( & mut self , txid : Txid , seen_at : u64 ) -> ChangeSet < A > {
806
+ let mut changeset_first_seen = self . update_first_seen ( txid, seen_at) ;
807
+ let changeset_last_seen = self . update_last_seen ( txid, seen_at) ;
808
+ changeset_first_seen. merge ( changeset_last_seen) ;
809
+ changeset_first_seen
810
+ }
811
+
812
+ /// Updates `first_seen` given a new `seen_at`.
813
+ fn update_first_seen ( & mut self , txid : Txid , seen_at : u64 ) -> ChangeSet < A > {
814
+ let is_changed = match self . first_seen . entry ( txid) {
815
+ hash_map:: Entry :: Occupied ( mut e) => {
816
+ let first_seen = e. get_mut ( ) ;
817
+ let change = * first_seen > seen_at;
818
+ if change {
819
+ * first_seen = seen_at;
820
+ }
821
+ change
822
+ }
823
+ hash_map:: Entry :: Vacant ( e) => {
824
+ e. insert ( seen_at) ;
825
+ true
826
+ }
827
+ } ;
828
+
829
+ let mut changeset = ChangeSet :: < A > :: default ( ) ;
830
+ if is_changed {
831
+ changeset. first_seen . insert ( txid, seen_at) ;
832
+ }
833
+ changeset
834
+ }
835
+
836
+ /// Updates `last_seen` given a new `seen_at`.
837
+ fn update_last_seen ( & mut self , txid : Txid , seen_at : u64 ) -> ChangeSet < A > {
794
838
let mut old_last_seen = None ;
795
839
let is_changed = match self . last_seen . entry ( txid) {
796
840
hash_map:: Entry :: Occupied ( mut e) => {
@@ -904,6 +948,7 @@ impl<A: Anchor> TxGraph<A> {
904
948
. iter ( )
905
949
. flat_map ( |( txid, anchors) | anchors. iter ( ) . map ( |a| ( a. clone ( ) , * txid) ) )
906
950
. collect ( ) ,
951
+ first_seen : self . first_seen . iter ( ) . map ( |( & k, & v) | ( k, v) ) . collect ( ) ,
907
952
last_seen : self . last_seen . iter ( ) . map ( |( & k, & v) | ( k, v) ) . collect ( ) ,
908
953
last_evicted : self . last_evicted . iter ( ) . map ( |( & k, & v) | ( k, v) ) . collect ( ) ,
909
954
}
@@ -978,11 +1023,13 @@ impl<A: Anchor> TxGraph<A> {
978
1023
transitively : None ,
979
1024
} ,
980
1025
None => ChainPosition :: Unconfirmed {
981
- last_seen : tx_node. last_seen_unconfirmed ,
1026
+ first_seen : tx_node. first_seen ,
1027
+ last_seen : tx_node. last_seen ,
982
1028
} ,
983
1029
} ,
984
1030
None => ChainPosition :: Unconfirmed {
985
- last_seen : tx_node. last_seen_unconfirmed ,
1031
+ first_seen : tx_node. first_seen ,
1032
+ last_seen : tx_node. last_seen ,
986
1033
} ,
987
1034
} ,
988
1035
CanonicalReason :: Anchor { anchor, descendant } => match descendant {
@@ -1003,9 +1050,13 @@ impl<A: Anchor> TxGraph<A> {
1003
1050
} ,
1004
1051
CanonicalReason :: ObservedIn { observed_in, .. } => match observed_in {
1005
1052
ObservedIn :: Mempool ( last_seen) => ChainPosition :: Unconfirmed {
1053
+ first_seen : tx_node. first_seen ,
1006
1054
last_seen : Some ( last_seen) ,
1007
1055
} ,
1008
- ObservedIn :: Block ( _) => ChainPosition :: Unconfirmed { last_seen : None } ,
1056
+ ObservedIn :: Block ( _) => ChainPosition :: Unconfirmed {
1057
+ first_seen : tx_node. first_seen ,
1058
+ last_seen : None ,
1059
+ } ,
1009
1060
} ,
1010
1061
} ;
1011
1062
Ok ( CanonicalTx {
@@ -1372,6 +1423,9 @@ pub struct ChangeSet<A = ()> {
1372
1423
/// Added timestamps of when a transaction is last evicted from the mempool.
1373
1424
#[ cfg_attr( feature = "serde" , serde( default ) ) ]
1374
1425
pub last_evicted : BTreeMap < Txid , u64 > ,
1426
+ /// Added first-seen unix timestamps of transactions.
1427
+ #[ cfg_attr( feature = "serde" , serde( default ) ) ]
1428
+ pub first_seen : BTreeMap < Txid , u64 > ,
1375
1429
}
1376
1430
1377
1431
impl < A > Default for ChangeSet < A > {
@@ -1380,6 +1434,7 @@ impl<A> Default for ChangeSet<A> {
1380
1434
txs : Default :: default ( ) ,
1381
1435
txouts : Default :: default ( ) ,
1382
1436
anchors : Default :: default ( ) ,
1437
+ first_seen : Default :: default ( ) ,
1383
1438
last_seen : Default :: default ( ) ,
1384
1439
last_evicted : Default :: default ( ) ,
1385
1440
}
@@ -1428,6 +1483,18 @@ impl<A: Ord> Merge for ChangeSet<A> {
1428
1483
self . txouts . extend ( other. txouts ) ;
1429
1484
self . anchors . extend ( other. anchors ) ;
1430
1485
1486
+ // first_seen timestamps should only decrease
1487
+ self . first_seen . extend (
1488
+ other
1489
+ . first_seen
1490
+ . into_iter ( )
1491
+ . filter ( |( txid, update_fs) | match self . first_seen . get ( txid) {
1492
+ Some ( existing) => update_fs < existing,
1493
+ None => true ,
1494
+ } )
1495
+ . collect :: < Vec < _ > > ( ) ,
1496
+ ) ;
1497
+
1431
1498
// last_seen timestamps should only increase
1432
1499
self . last_seen . extend (
1433
1500
other
@@ -1450,6 +1517,7 @@ impl<A: Ord> Merge for ChangeSet<A> {
1450
1517
self . txs . is_empty ( )
1451
1518
&& self . txouts . is_empty ( )
1452
1519
&& self . anchors . is_empty ( )
1520
+ && self . first_seen . is_empty ( )
1453
1521
&& self . last_seen . is_empty ( )
1454
1522
&& self . last_evicted . is_empty ( )
1455
1523
}
@@ -1470,6 +1538,7 @@ impl<A: Ord> ChangeSet<A> {
1470
1538
anchors : BTreeSet :: < ( A2 , Txid ) > :: from_iter (
1471
1539
self . anchors . into_iter ( ) . map ( |( a, txid) | ( f ( a) , txid) ) ,
1472
1540
) ,
1541
+ first_seen : self . first_seen ,
1473
1542
last_seen : self . last_seen ,
1474
1543
last_evicted : self . last_evicted ,
1475
1544
}
0 commit comments