@@ -6,7 +6,7 @@ import com.google.iot.cbor.CborByteString
66import com.google.iot.cbor.CborInteger
77import com.google.iot.cbor.CborMap
88import com.google.iot.cbor.CborReader
9- import io.newm.chain.config.Config
9+ import io.newm.chain.util. config.Config
1010import io.newm.chain.database.entity.LedgerAsset
1111import io.newm.chain.database.entity.LedgerAssetMetadata
1212import io.newm.chain.database.entity.LedgerUtxoHistory
@@ -42,10 +42,12 @@ import io.newm.chain.util.Constants.UTXO_SCRIPT_REF_INDEX
4242import io.newm.chain.util.elementToByteArray
4343import io.newm.chain.util.elementToInt
4444import io.newm.chain.util.extractCredentials
45+ import io.newm.chain.util.getInstantAtSlot
4546import io.newm.chain.util.hexToByteArray
4647import io.newm.chain.util.toHexString
4748import java.math.BigInteger
4849import java.time.Duration
50+ import java.time.Instant
4951import kotlin.math.max
5052import kotlinx.coroutines.runBlocking
5153import kotlinx.coroutines.sync.Mutex
@@ -89,6 +91,9 @@ class LedgerRepositoryImpl : LedgerRepository {
8991 */
9092 private val liveUtxoMap = mutableMapOf<String , Set <Utxo >>()
9193
94+ // Track validity end slot (TTL) for pending transactions that produced liveUtxos
95+ private val pendingTxValidityEndSlots = mutableMapOf<String , Long >()
96+
9297 override fun queryUtxos (address : String ): Set <Utxo > =
9398 transaction {
9499 warnLongQueriesDuration = 1000L
@@ -350,6 +355,7 @@ class LedgerRepositoryImpl : LedgerRepository {
350355 chainUtxos : MutableSet <Utxo >
351356 ): Set <Utxo > {
352357 utxoMutex.withLock {
358+ cleanupExpiredPendingTransactions()
353359 return chainUtxos
354360 .apply {
355361 // add in any liveUtxos from pending transactions
@@ -386,9 +392,14 @@ class LedgerRepositoryImpl : LedgerRepository {
386392 cborByteArray : ByteArray
387393 ) {
388394 utxoMutex.withLock {
395+ cleanupExpiredPendingTransactions()
389396 val tx = CborReader .createFromByteArray(cborByteArray).readDataItem() as CborArray
390397 val txBody = tx.elementAt(0 ) as CborMap
391398 val witnessSet = tx.elementAt(1 ) as CborMap
399+ // Record TTL (validityEndSlot) if present so we can expire pending UTxOs later
400+ (txBody[CborInteger .create(3 )] as ? CborInteger )?.longValue()?.let { ttl ->
401+ pendingTxValidityEndSlots[transactionId] = ttl
402+ }
392403 val utxosInArray = txBody[TX_SPENT_UTXOS_INDEX ] as CborArray
393404 utxosInArray.forEach { utxo ->
394405 var hash = " "
@@ -554,6 +565,37 @@ class LedgerRepositoryImpl : LedgerRepository {
554565 }
555566 )
556567 }
568+ // After adding new pending utxos, perform another cleanup in case they are already expired
569+ cleanupExpiredPendingTransactions()
570+ }
571+ }
572+
573+ // Purge any pending transactions whose TTL has passed
574+ private fun cleanupExpiredPendingTransactions () {
575+ if (pendingTxValidityEndSlots.isEmpty()) return
576+ val now = Instant .now()
577+ val expiredTxIds = pendingTxValidityEndSlots.filter { (_, slot) -> now.isAfter(getInstantAtSlot(slot)) }.keys
578+ if (expiredTxIds.isEmpty()) return
579+ expiredTxIds.forEach { pendingTxValidityEndSlots.remove(it) }
580+ // Unmark spent UTXOs from expired transactions
581+ if (spentUtxoSet.isNotEmpty()) {
582+ spentUtxoSet.removeIf { it.transactionSpent in expiredTxIds }
583+ }
584+ // Remove created UTXOs from expired transactions
585+ if (liveUtxoMap.isNotEmpty()) {
586+ val removeAddresses = mutableListOf<String >()
587+ liveUtxoMap.forEach { (address, utxos) ->
588+ val newSet = utxos.filterNot { it.hash in expiredTxIds }.toSet()
589+ if (newSet.isEmpty()) {
590+ if (utxos.isNotEmpty()) removeAddresses.add(address)
591+ } else if (newSet.size != utxos.size) {
592+ liveUtxoMap[address] = newSet
593+ }
594+ }
595+ removeAddresses.forEach { liveUtxoMap.remove(it) }
596+ }
597+ if (log.isDebugEnabled) {
598+ log.debug(" cleanupExpiredPendingTransactions: expiredTxIds=$expiredTxIds " )
557599 }
558600 }
559601
0 commit comments