Skip to content

Commit 6efa8fe

Browse files
committed
fix: match cjit entry to channel by funding tx
1 parent cfcb2e6 commit 6efa8fe

3 files changed

Lines changed: 72 additions & 4 deletions

File tree

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,13 @@ class BlocktankRepo @Inject constructor(
125125
}
126126

127127
suspend fun getCjitEntry(channel: ChannelDetails): IcJitEntry? = withContext(bgDispatcher) {
128-
return@withContext _blocktankState.value.cjitEntries.firstOrNull { order ->
129-
order.channelSizeSat == channel.channelValueSats &&
130-
order.lspNode.pubkey == channel.counterpartyNodeId
131-
}
128+
val fundingTxId = channel.fundingTxo?.txid ?: return@withContext null
129+
130+
// Refresh from the server so a freshly opened CJIT channel association is up to date before matching.
131+
val entries = runCatching { coreService.blocktank.cjitEntries(refresh = true) }
132+
.getOrElse { _blocktankState.value.cjitEntries }
133+
134+
return@withContext entries.firstOrNull { it.channel?.fundingTx?.id == fundingTxId }
132135
}
133136

134137
suspend fun refreshInfo() = withContext(bgDispatcher) {

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package to.bitkit.repositories
22

33
import app.cash.turbine.test
4+
import com.synonym.bitkitcore.FundingTx
5+
import com.synonym.bitkitcore.IBtChannel
46
import com.synonym.bitkitcore.IBtInfo
57
import com.synonym.bitkitcore.IBtOrder
8+
import com.synonym.bitkitcore.IcJitEntry
69
import kotlinx.coroutines.flow.MutableStateFlow
710
import kotlinx.coroutines.flow.flowOf
811
import org.junit.Before
912
import org.junit.Test
13+
import org.lightningdevkit.ldknode.ChannelDetails
14+
import org.lightningdevkit.ldknode.OutPoint
1015
import org.mockito.kotlin.doReturn
1116
import org.mockito.kotlin.mock
1217
import org.mockito.kotlin.verify
@@ -192,4 +197,63 @@ class BlocktankRepoTest : BaseUnitTest() {
192197
assertTrue(result.isFailure)
193198
}
194199
}
200+
201+
@Test
202+
fun `getCjitEntry returns null when channel has no funding txo`() = test {
203+
sut = createSut()
204+
val channelDetails = mock<ChannelDetails>()
205+
whenever(channelDetails.fundingTxo).thenReturn(null)
206+
207+
assertNull(sut.getCjitEntry(channelDetails))
208+
}
209+
210+
@Test
211+
fun `getCjitEntry does not match a stale unpaid CJIT entry without an opened channel`() = test {
212+
sut = createSut()
213+
// A leftover CJIT entry that was never paid: same size & LSP as a transfer-flow channel order,
214+
// but it never opened a channel. It must not be mistaken for the freshly opened channel.
215+
val staleEntry = mock<IcJitEntry>()
216+
whenever(staleEntry.channel).thenReturn(null)
217+
whenever(coreService.blocktank.cjitEntries(refresh = true)).thenReturn(listOf(staleEntry))
218+
219+
val channelDetails = mock<ChannelDetails>()
220+
whenever(channelDetails.fundingTxo).thenReturn(OutPoint(txid = "channel-order-funding-tx", vout = 0u))
221+
222+
assertNull(sut.getCjitEntry(channelDetails))
223+
}
224+
225+
@Test
226+
fun `getCjitEntry matches the entry whose channel funding tx matches`() = test {
227+
sut = createSut()
228+
val fundingTxId = "cjit-funding-tx"
229+
val matchingChannel = mock<IBtChannel>()
230+
whenever(matchingChannel.fundingTx).thenReturn(FundingTx(id = fundingTxId, vout = 0u))
231+
val otherChannel = mock<IBtChannel>()
232+
whenever(otherChannel.fundingTx).thenReturn(FundingTx(id = "other-funding-tx", vout = 0u))
233+
val matchingEntry = mock<IcJitEntry>()
234+
whenever(matchingEntry.channel).thenReturn(matchingChannel)
235+
val otherEntry = mock<IcJitEntry>()
236+
whenever(otherEntry.channel).thenReturn(otherChannel)
237+
whenever(coreService.blocktank.cjitEntries(refresh = true)).thenReturn(listOf(otherEntry, matchingEntry))
238+
239+
val channelDetails = mock<ChannelDetails>()
240+
whenever(channelDetails.fundingTxo).thenReturn(OutPoint(txid = fundingTxId, vout = 0u))
241+
242+
assertEquals(matchingEntry, sut.getCjitEntry(channelDetails))
243+
}
244+
245+
@Test
246+
fun `getCjitEntry returns null when no CJIT channel funding tx matches`() = test {
247+
sut = createSut()
248+
val channel = mock<IBtChannel>()
249+
whenever(channel.fundingTx).thenReturn(FundingTx(id = "cjit-funding-tx", vout = 0u))
250+
val entry = mock<IcJitEntry>()
251+
whenever(entry.channel).thenReturn(channel)
252+
whenever(coreService.blocktank.cjitEntries(refresh = true)).thenReturn(listOf(entry))
253+
254+
val channelDetails = mock<ChannelDetails>()
255+
whenever(channelDetails.fundingTxo).thenReturn(OutPoint(txid = "different-funding-tx", vout = 0u))
256+
257+
assertNull(sut.getCjitEntry(channelDetails))
258+
}
195259
}

changelog.d/next/1016.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Transferring to your spending balance now reliably shows the "Spending Balance Ready" confirmation instead of sometimes being mistaken for an incoming payment when an unused instant-payment invoice is still pending.

0 commit comments

Comments
 (0)