Skip to content

Commit 85d27e6

Browse files
committed
fix: guard cjit refresh to non-terminal pending entries
1 parent a884da4 commit 85d27e6

2 files changed

Lines changed: 78 additions & 9 deletions

File tree

app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package to.bitkit.repositories
22

33
import androidx.compose.runtime.Stable
44
import com.synonym.bitkitcore.BtOrderState2
5+
import com.synonym.bitkitcore.CJitStateEnum
56
import com.synonym.bitkitcore.ChannelLiquidityOptions
67
import com.synonym.bitkitcore.ChannelLiquidityParams
78
import com.synonym.bitkitcore.CreateCjitOptions
@@ -133,11 +134,17 @@ class BlocktankRepo @Inject constructor(
133134
fun cachedMatch(): IcJitEntry? = _blocktankState.value.cjitEntries
134135
.firstOrNull { it.channel?.fundingTx?.id == fundingTxId }
135136

136-
// Use cached state first; only refresh from the server when the freshly opened channel isn't associated yet.
137-
return@withContext cachedMatch() ?: run {
138-
refreshCjitEntries()
139-
cachedMatch()
137+
cachedMatch()?.let { return@withContext it }
138+
139+
// A ChannelReady can only be a CJIT if a live cached entry is still awaiting its channel; otherwise skip
140+
// the server round-trip so a non-CJIT transfer confirmation isn't delayed by a slow Blocktank API.
141+
val hasPendingCjit = _blocktankState.value.cjitEntries.any {
142+
it.channel == null && it.state != CJitStateEnum.EXPIRED && it.state != CJitStateEnum.FAILED
140143
}
144+
if (!hasPendingCjit) return@withContext null
145+
146+
refreshCjitEntries()
147+
return@withContext cachedMatch()
141148
}
142149

143150
private suspend fun refreshCjitEntries() {

app/src/test/java/to/bitkit/repositories/BlocktankRepoTest.kt

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package to.bitkit.repositories
22

33
import app.cash.turbine.test
4+
import com.synonym.bitkitcore.CJitStateEnum
45
import com.synonym.bitkitcore.FundingTx
56
import com.synonym.bitkitcore.IBtChannel
67
import com.synonym.bitkitcore.IBtInfo
@@ -215,6 +216,8 @@ class BlocktankRepoTest : BaseUnitTest() {
215216
// but it never opened a channel. It must not be mistaken for the freshly opened channel.
216217
val staleEntry = mock<IcJitEntry>()
217218
whenever(staleEntry.channel).thenReturn(null)
219+
whenever(staleEntry.state).thenReturn(CJitStateEnum.CREATED)
220+
seedCjitEntries(staleEntry)
218221
whenever(coreService.blocktank.cjitEntries(refresh = true)).thenReturn(listOf(staleEntry))
219222

220223
val channelDetails = mock<ChannelDetails>()
@@ -226,6 +229,7 @@ class BlocktankRepoTest : BaseUnitTest() {
226229
@Test
227230
fun `getCjitEntry matches the entry whose channel funding tx matches`() = test {
228231
sut = createSut()
232+
seedCjitEntries(pendingCjitEntry())
229233
val fundingTxId = "cjit-funding-tx"
230234
val matchingChannel = mock<IBtChannel>()
231235
whenever(matchingChannel.fundingTx).thenReturn(FundingTx(id = fundingTxId, vout = 0u))
@@ -246,6 +250,7 @@ class BlocktankRepoTest : BaseUnitTest() {
246250
@Test
247251
fun `getCjitEntry returns null when no CJIT channel funding tx matches`() = test {
248252
sut = createSut()
253+
seedCjitEntries(pendingCjitEntry())
249254
val channel = mock<IBtChannel>()
250255
whenever(channel.fundingTx).thenReturn(FundingTx(id = "cjit-funding-tx", vout = 0u))
251256
val entry = mock<IcJitEntry>()
@@ -258,6 +263,53 @@ class BlocktankRepoTest : BaseUnitTest() {
258263
assertNull(sut.getCjitEntry(channelDetails))
259264
}
260265

266+
@Test
267+
fun `getCjitEntry does not refresh when no cached CJIT entry is awaiting a channel`() = test {
268+
sut = createSut()
269+
// Only an already-associated entry is cached (none awaiting a channel), so this ChannelReady cannot be a
270+
// CJIT: the server must not be hit even though a refresh would return a matching entry.
271+
val associatedChannel = mock<IBtChannel>()
272+
whenever(associatedChannel.fundingTx).thenReturn(FundingTx(id = "other-funding-tx", vout = 0u))
273+
val associatedEntry = mock<IcJitEntry>()
274+
whenever(associatedEntry.channel).thenReturn(associatedChannel)
275+
seedCjitEntries(associatedEntry)
276+
277+
val fundingTxId = "channel-order-funding-tx"
278+
val matchingChannel = mock<IBtChannel>()
279+
whenever(matchingChannel.fundingTx).thenReturn(FundingTx(id = fundingTxId, vout = 0u))
280+
val matchingEntry = mock<IcJitEntry>()
281+
whenever(matchingEntry.channel).thenReturn(matchingChannel)
282+
whenever(coreService.blocktank.cjitEntries(refresh = true)).thenReturn(listOf(matchingEntry))
283+
284+
val channelDetails = mock<ChannelDetails>()
285+
whenever(channelDetails.fundingTxo).thenReturn(OutPoint(txid = fundingTxId, vout = 0u))
286+
287+
assertNull(sut.getCjitEntry(channelDetails))
288+
}
289+
290+
@Test
291+
fun `getCjitEntry does not refresh for an expired CJIT entry awaiting no channel`() = test {
292+
sut = createSut()
293+
// An expired entry has no channel but can never open one, so it must not trigger a server refresh
294+
// even though a refresh would surface a matching entry.
295+
val expiredEntry = mock<IcJitEntry>()
296+
whenever(expiredEntry.channel).thenReturn(null)
297+
whenever(expiredEntry.state).thenReturn(CJitStateEnum.EXPIRED)
298+
seedCjitEntries(expiredEntry)
299+
300+
val fundingTxId = "channel-order-funding-tx"
301+
val matchingChannel = mock<IBtChannel>()
302+
whenever(matchingChannel.fundingTx).thenReturn(FundingTx(id = fundingTxId, vout = 0u))
303+
val matchingEntry = mock<IcJitEntry>()
304+
whenever(matchingEntry.channel).thenReturn(matchingChannel)
305+
whenever(coreService.blocktank.cjitEntries(refresh = true)).thenReturn(listOf(matchingEntry))
306+
307+
val channelDetails = mock<ChannelDetails>()
308+
whenever(channelDetails.fundingTxo).thenReturn(OutPoint(txid = fundingTxId, vout = 0u))
309+
310+
assertNull(sut.getCjitEntry(channelDetails))
311+
}
312+
261313
@Test
262314
fun `getCjitEntry returns cached entry without refreshing when already associated`() = test {
263315
sut = createSut()
@@ -266,10 +318,7 @@ class BlocktankRepoTest : BaseUnitTest() {
266318
whenever(channel.fundingTx).thenReturn(FundingTx(id = fundingTxId, vout = 0u))
267319
val cachedEntry = mock<IcJitEntry>()
268320
whenever(cachedEntry.channel).thenReturn(channel)
269-
270-
sut.restoreFromBackup(
271-
BlocktankBackupV1(createdAt = 0L, orders = emptyList(), cjitEntries = listOf(cachedEntry)),
272-
)
321+
seedCjitEntries(cachedEntry)
273322

274323
val channelDetails = mock<ChannelDetails>()
275324
whenever(channelDetails.fundingTxo).thenReturn(OutPoint(txid = fundingTxId, vout = 0u))
@@ -280,8 +329,9 @@ class BlocktankRepoTest : BaseUnitTest() {
280329
}
281330

282331
@Test
283-
fun `getCjitEntry returns null when cache misses and refresh fails`() = test {
332+
fun `getCjitEntry returns null when a pending CJIT is awaiting but refresh fails`() = test {
284333
sut = createSut()
334+
seedCjitEntries(pendingCjitEntry())
285335
whenever(coreService.blocktank.cjitEntries(refresh = true)).thenThrow(RuntimeException("Network error"))
286336

287337
val channelDetails = mock<ChannelDetails>()
@@ -293,6 +343,7 @@ class BlocktankRepoTest : BaseUnitTest() {
293343
@Test
294344
fun `getCjitEntry retries the refresh and matches after a transient failure`() = test {
295345
sut = createSut()
346+
seedCjitEntries(pendingCjitEntry())
296347
val fundingTxId = "cjit-funding-tx"
297348
val channel = mock<IBtChannel>()
298349
whenever(channel.fundingTx).thenReturn(FundingTx(id = fundingTxId, vout = 0u))
@@ -307,4 +358,15 @@ class BlocktankRepoTest : BaseUnitTest() {
307358

308359
assertEquals(entry, sut.getCjitEntry(channelDetails))
309360
}
361+
362+
private fun pendingCjitEntry(): IcJitEntry = mock<IcJitEntry>().apply {
363+
whenever(channel).thenReturn(null)
364+
whenever(state).thenReturn(CJitStateEnum.CREATED)
365+
}
366+
367+
private suspend fun seedCjitEntries(vararg entries: IcJitEntry) {
368+
sut.restoreFromBackup(
369+
BlocktankBackupV1(createdAt = 0L, orders = emptyList(), cjitEntries = entries.toList()),
370+
)
371+
}
310372
}

0 commit comments

Comments
 (0)