Skip to content

Commit 8455f10

Browse files
committed
test: lsp calc regression tests
1 parent 7f1b088 commit 8455f10

1 file changed

Lines changed: 149 additions & 0 deletions

File tree

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package to.bitkit.viewmodels
2+
3+
import android.content.Context
4+
import com.synonym.bitkitcore.ChannelLiquidityOptions
5+
import com.synonym.bitkitcore.IBtEstimateFeeResponse2
6+
import com.synonym.bitkitcore.IBtInfo
7+
import com.synonym.bitkitcore.IBtInfoOptions
8+
import kotlinx.coroutines.ExperimentalCoroutinesApi
9+
import kotlinx.coroutines.flow.MutableStateFlow
10+
import kotlinx.coroutines.test.advanceUntilIdle
11+
import org.junit.Before
12+
import org.junit.Test
13+
import org.lightningdevkit.ldknode.NodeStatus
14+
import org.mockito.kotlin.any
15+
import org.mockito.kotlin.eq
16+
import org.mockito.kotlin.mock
17+
import org.mockito.kotlin.verify
18+
import org.mockito.kotlin.whenever
19+
import to.bitkit.data.CacheStore
20+
import to.bitkit.data.SettingsData
21+
import to.bitkit.data.SettingsStore
22+
import to.bitkit.models.BalanceState
23+
import to.bitkit.repositories.BlocktankRepo
24+
import to.bitkit.repositories.BlocktankState
25+
import to.bitkit.repositories.LightningRepo
26+
import to.bitkit.repositories.LightningState
27+
import to.bitkit.repositories.TransferRepo
28+
import to.bitkit.repositories.WalletRepo
29+
import to.bitkit.test.BaseUnitTest
30+
import kotlin.test.assertEquals
31+
import kotlin.time.Clock
32+
import kotlin.time.ExperimentalTime
33+
34+
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class)
35+
class TransferViewModelTest : BaseUnitTest() {
36+
private lateinit var sut: TransferViewModel
37+
38+
private val context = mock<Context>()
39+
private val lightningRepo = mock<LightningRepo>()
40+
private val blocktankRepo = mock<BlocktankRepo>()
41+
private val walletRepo = mock<WalletRepo>()
42+
private val settingsStore = mock<SettingsStore>()
43+
private val cacheStore = mock<CacheStore>()
44+
private val transferRepo = mock<TransferRepo>()
45+
private val clock = mock<Clock>()
46+
47+
private val balanceState = MutableStateFlow(BalanceState())
48+
private val blocktankState = MutableStateFlow(BlocktankState())
49+
private val feeResponse = mock<IBtEstimateFeeResponse2>()
50+
51+
@Before
52+
fun setUp() {
53+
whenever(feeResponse.feeSat).thenReturn(LSP_FEE)
54+
whenever(feeResponse.networkFeeSat).thenReturn(NETWORK_FEE)
55+
whenever(feeResponse.serviceFeeSat).thenReturn(SERVICE_FEE)
56+
whenever(context.getString(any())).thenReturn("")
57+
whenever(settingsStore.data).thenReturn(MutableStateFlow(SettingsData()))
58+
val nodeStatus = mock<NodeStatus>()
59+
whenever(nodeStatus.isRunning).thenReturn(true)
60+
whenever(lightningRepo.lightningState).thenReturn(MutableStateFlow(LightningState(nodeStatus = nodeStatus)))
61+
whenever(walletRepo.balanceState).thenReturn(balanceState)
62+
whenever(blocktankRepo.blocktankState).thenReturn(blocktankState)
63+
64+
sut = TransferViewModel(
65+
context = context,
66+
lightningRepo = lightningRepo,
67+
blocktankRepo = blocktankRepo,
68+
walletRepo = walletRepo,
69+
settingsStore = settingsStore,
70+
cacheStore = cacheStore,
71+
transferRepo = transferRepo,
72+
clock = clock,
73+
)
74+
}
75+
76+
@Test
77+
fun `updateLimits caps spending max at LSP max client balance when on-chain balance exceeds it`() = test {
78+
balanceState.value = BalanceState(maxSendOnchainSats = ON_CHAIN_BALANCE)
79+
blocktankState.value = BlocktankState(info = btInfo(lspMaxClientBalance = LSP_MAX_CLIENT_BALANCE))
80+
// The LSP reports no room for receiving liquidity (maxLspBalanceSat = 0) because the
81+
// client balance saturates the channel — the regression this guards against.
82+
whenever(blocktankRepo.calculateLiquidityOptions(any()))
83+
.thenReturn(Result.success(liquidityOptions(maxClientBalanceSat = OPTION_MAX_CLIENT_BALANCE)))
84+
whenever(blocktankRepo.estimateOrderFee(any(), any(), any())).thenReturn(Result.success(feeResponse))
85+
86+
sut.updateLimits()
87+
advanceUntilIdle()
88+
89+
val state = sut.spendingUiState.value
90+
assertEquals(OPTION_MAX_CLIENT_BALANCE.toLong(), state.maxAllowedToSend)
91+
assertEquals(OPTION_MAX_CLIENT_BALANCE.toLong(), state.balanceAfterFee)
92+
93+
// The order fee must be estimated against the clamped client balance, not the full balance.
94+
verify(blocktankRepo).estimateOrderFee(eq(LSP_MAX_CLIENT_BALANCE), any(), any())
95+
}
96+
97+
@Test
98+
fun `updateLimits uses the full balance when LSP info is unavailable`() = test {
99+
balanceState.value = BalanceState(maxSendOnchainSats = ON_CHAIN_BALANCE)
100+
blocktankState.value = BlocktankState(info = null)
101+
whenever(blocktankRepo.calculateLiquidityOptions(any()))
102+
.thenReturn(Result.success(liquidityOptions(maxClientBalanceSat = OPTION_MAX_CLIENT_BALANCE)))
103+
whenever(blocktankRepo.estimateOrderFee(any(), any(), any())).thenReturn(Result.success(feeResponse))
104+
105+
sut.updateLimits()
106+
advanceUntilIdle()
107+
108+
assertEquals(OPTION_MAX_CLIENT_BALANCE.toLong(), sut.spendingUiState.value.maxAllowedToSend)
109+
// Without an LSP cap the order fee is estimated against the balance after the LSP fee.
110+
verify(blocktankRepo).estimateOrderFee(eq(ON_CHAIN_BALANCE - LSP_FEE), any(), any())
111+
}
112+
113+
@Test
114+
fun `updateLimits sets max to zero when LSP reports zero client balance`() = test {
115+
balanceState.value = BalanceState(maxSendOnchainSats = ON_CHAIN_BALANCE)
116+
blocktankState.value = BlocktankState(info = btInfo(lspMaxClientBalance = LSP_MAX_CLIENT_BALANCE))
117+
whenever(blocktankRepo.calculateLiquidityOptions(any()))
118+
.thenReturn(Result.success(liquidityOptions(maxClientBalanceSat = 0uL)))
119+
whenever(blocktankRepo.estimateOrderFee(any(), any(), any())).thenReturn(Result.success(feeResponse))
120+
121+
sut.updateLimits()
122+
advanceUntilIdle()
123+
124+
assertEquals(0L, sut.spendingUiState.value.maxAllowedToSend)
125+
}
126+
127+
private fun liquidityOptions(maxClientBalanceSat: ULong) = ChannelLiquidityOptions(
128+
defaultLspBalanceSat = LSP_BALANCE,
129+
minLspBalanceSat = LSP_BALANCE,
130+
maxLspBalanceSat = 0uL,
131+
maxClientBalanceSat = maxClientBalanceSat,
132+
)
133+
134+
private fun btInfo(lspMaxClientBalance: ULong): IBtInfo {
135+
val options = mock<IBtInfoOptions>()
136+
whenever(options.maxClientBalanceSat).thenReturn(lspMaxClientBalance)
137+
return mock<IBtInfo>().also { whenever(it.options).thenReturn(options) }
138+
}
139+
140+
private companion object {
141+
const val ON_CHAIN_BALANCE = 10_000_000uL
142+
const val LSP_MAX_CLIENT_BALANCE = 1_766_193uL
143+
const val OPTION_MAX_CLIENT_BALANCE = 1_687_598uL
144+
const val LSP_BALANCE = 252_368uL
145+
const val NETWORK_FEE = 2_112uL
146+
const val SERVICE_FEE = 286uL
147+
const val LSP_FEE = 2_398uL // NETWORK_FEE + SERVICE_FEE
148+
}
149+
}

0 commit comments

Comments
 (0)