Skip to content

Commit 2e5fba0

Browse files
authored
Merge branch 'master' into feat/wallet-scoped-core-0.3.9
2 parents 3dfbc32 + a3157d9 commit 2e5fba0

12 files changed

Lines changed: 262 additions & 10 deletions

File tree

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
{
2+
"formatVersion": 1,
3+
"database": {
4+
"version": 7,
5+
"identityHash": "2b1cda225c170b5f1bbfbf76e5fa4cf5",
6+
"entities": [
7+
{
8+
"tableName": "config",
9+
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`walletIndex` INTEGER NOT NULL, PRIMARY KEY(`walletIndex`))",
10+
"fields": [
11+
{
12+
"fieldPath": "walletIndex",
13+
"columnName": "walletIndex",
14+
"affinity": "INTEGER",
15+
"notNull": true
16+
}
17+
],
18+
"primaryKey": {
19+
"autoGenerate": false,
20+
"columnNames": [
21+
"walletIndex"
22+
]
23+
}
24+
},
25+
{
26+
"tableName": "transfers",
27+
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT NOT NULL, `amountSats` INTEGER NOT NULL, `channelId` TEXT, `fundingTxId` TEXT, `lspOrderId` TEXT, `isSettled` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `settledAt` INTEGER, `claimableAtHeight` INTEGER, `txTotalSats` INTEGER, `preTransferOnchainSats` INTEGER, PRIMARY KEY(`id`))",
28+
"fields": [
29+
{
30+
"fieldPath": "id",
31+
"columnName": "id",
32+
"affinity": "TEXT",
33+
"notNull": true
34+
},
35+
{
36+
"fieldPath": "type",
37+
"columnName": "type",
38+
"affinity": "TEXT",
39+
"notNull": true
40+
},
41+
{
42+
"fieldPath": "amountSats",
43+
"columnName": "amountSats",
44+
"affinity": "INTEGER",
45+
"notNull": true
46+
},
47+
{
48+
"fieldPath": "channelId",
49+
"columnName": "channelId",
50+
"affinity": "TEXT"
51+
},
52+
{
53+
"fieldPath": "fundingTxId",
54+
"columnName": "fundingTxId",
55+
"affinity": "TEXT"
56+
},
57+
{
58+
"fieldPath": "lspOrderId",
59+
"columnName": "lspOrderId",
60+
"affinity": "TEXT"
61+
},
62+
{
63+
"fieldPath": "isSettled",
64+
"columnName": "isSettled",
65+
"affinity": "INTEGER",
66+
"notNull": true
67+
},
68+
{
69+
"fieldPath": "createdAt",
70+
"columnName": "createdAt",
71+
"affinity": "INTEGER",
72+
"notNull": true
73+
},
74+
{
75+
"fieldPath": "settledAt",
76+
"columnName": "settledAt",
77+
"affinity": "INTEGER"
78+
},
79+
{
80+
"fieldPath": "claimableAtHeight",
81+
"columnName": "claimableAtHeight",
82+
"affinity": "INTEGER"
83+
},
84+
{
85+
"fieldPath": "txTotalSats",
86+
"columnName": "txTotalSats",
87+
"affinity": "INTEGER"
88+
},
89+
{
90+
"fieldPath": "preTransferOnchainSats",
91+
"columnName": "preTransferOnchainSats",
92+
"affinity": "INTEGER"
93+
}
94+
],
95+
"primaryKey": {
96+
"autoGenerate": false,
97+
"columnNames": [
98+
"id"
99+
]
100+
}
101+
}
102+
],
103+
"setupQueries": [
104+
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
105+
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2b1cda225c170b5f1bbfbf76e5fa4cf5')"
106+
]
107+
}
108+
}

app/src/main/java/to/bitkit/data/AppDb.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import to.bitkit.env.Env
3131
ConfigEntity::class,
3232
TransferEntity::class,
3333
],
34-
version = 6,
34+
version = 7,
3535
)
3636
@TypeConverters(StringListConverter::class)
3737
abstract class AppDb : RoomDatabase() {
@@ -45,6 +45,13 @@ abstract class AppDb : RoomDatabase() {
4545
}
4646
}
4747

48+
private val MIGRATION_6_7 = object : Migration(6, 7) {
49+
override fun migrate(db: SupportSQLiteDatabase) {
50+
db.execSQL("ALTER TABLE transfers ADD COLUMN txTotalSats INTEGER DEFAULT NULL")
51+
db.execSQL("ALTER TABLE transfers ADD COLUMN preTransferOnchainSats INTEGER DEFAULT NULL")
52+
}
53+
}
54+
4855
private const val DB_NAME = "${BuildConfig.APPLICATION_ID}.sqlite"
4956

5057
@Volatile
@@ -72,7 +79,7 @@ abstract class AppDb : RoomDatabase() {
7279
}
7380
}
7481
})
75-
.addMigrations(MIGRATION_5_6)
82+
.addMigrations(MIGRATION_5_6, MIGRATION_6_7)
7683
.apply {
7784
if (Env.isDebug) fallbackToDestructiveMigration(dropAllTables = true)
7885
}

app/src/main/java/to/bitkit/data/entities/TransferEntity.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,6 @@ data class TransferEntity(
1818
val createdAt: Long,
1919
val settledAt: Long? = null,
2020
val claimableAtHeight: Int? = null,
21+
val txTotalSats: Long? = null,
22+
val preTransferOnchainSats: Long? = null,
2123
)

app/src/main/java/to/bitkit/models/BalanceState.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@ data class BalanceState(
1515
val balanceInTransferToSpending: ULong = 0uL,
1616
val hardwareWallets: List<HwWalletBalance> = emptyList(),
1717
) {
18-
val totalSats get() = totalOnchainSats + totalLightningSats
18+
val totalSats
19+
get() = totalOnchainSats
20+
.safe()
21+
.plus(totalLightningSats.safe())
22+
.safe()
23+
.plus(balanceInTransferToSavings.safe())
24+
.safe()
25+
.plus(balanceInTransferToSpending.safe())
1926

2027
val totalHardwareSats get() = hardwareWallets.fold(0uL) { acc, wallet -> acc.safe() + wallet.sats.safe() }
2128

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ class TransferRepo @Inject constructor(
6262
fundingTxId: String? = null,
6363
lspOrderId: String? = null,
6464
claimableAtHeight: UInt? = null,
65+
txTotalSats: Long? = null,
66+
preTransferOnchainSats: Long? = null,
6567
): Result<String> = withContext(bgDispatcher) {
6668
runCatching {
6769
val id = UUID.randomUUID().toString()
@@ -76,6 +78,8 @@ class TransferRepo @Inject constructor(
7678
isSettled = false,
7779
createdAt = clock.now().epochSeconds,
7880
claimableAtHeight = claimableAtHeight?.toInt(),
81+
txTotalSats = txTotalSats,
82+
preTransferOnchainSats = preTransferOnchainSats,
7983
)
8084
)
8185
Logger.info("Created transfer: id=$id type=$type channelId=$channelId", context = TAG)

app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ fun ActivityExploreScreen(
164164
val toastMessage = stringResource(R.string.common__copied)
165165
ActivityExploreContent(
166166
item = item,
167+
isHardware = uiState.isHardwareActivity,
167168
txDetails = txDetails,
168169
boostTxDoesExist = boostTxDoesExist,
169170
onCopy = { text ->
@@ -187,6 +188,7 @@ fun ActivityExploreScreen(
187188
@Composable
188189
private fun ActivityExploreContent(
189190
item: Activity,
191+
isHardware: Boolean = false,
190192
txDetails: TransactionDetails? = null,
191193
boostTxDoesExist: Map<String, Boolean> = emptyMap(),
192194
onCopy: (String) -> Unit = {},
@@ -210,7 +212,7 @@ private fun ActivityExploreContent(
210212
showBitcoinSymbol = false,
211213
modifier = Modifier.weight(1f),
212214
)
213-
ActivityIcon(activity = item, size = 48.dp)
215+
ActivityIcon(activity = item, size = 48.dp, isHardware = isHardware)
214216
}
215217

216218
Spacer(modifier = Modifier.height(16.dp))
@@ -219,6 +221,7 @@ private fun ActivityExploreContent(
219221
is Activity.Onchain -> {
220222
OnchainDetails(
221223
onchain = item,
224+
isHardware = isHardware,
222225
onCopy = onCopy,
223226
txDetails = txDetails,
224227
boostTxDoesExist = boostTxDoesExist,
@@ -281,6 +284,7 @@ private fun LightningDetails(
281284
@Composable
282285
private fun ColumnScope.OnchainDetails(
283286
onchain: Activity.Onchain,
287+
isHardware: Boolean,
284288
onCopy: (String) -> Unit,
285289
txDetails: TransactionDetails?,
286290
boostTxDoesExist: Map<String, Boolean> = emptyMap(),
@@ -323,7 +327,7 @@ private fun ColumnScope.OnchainDetails(
323327
}
324328
},
325329
)
326-
} else {
330+
} else if (!isHardware && !onchain.v1.isTransfer) {
327331
CircularProgressIndicator(
328332
strokeWidth = 2.dp,
329333
modifier = Modifier

app/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,12 @@ class DeriveBalanceStateUseCase @Inject constructor(
4747
val coopCloseSavingsSats = getCoopCloseTransferSats(activeTransfers, channels, balanceDetails)
4848
val lingeringCoopCloseSats = getLingeringCoopCloseSats(activeTransfers, channels, balanceDetails)
4949
val toSpendingAmount = paidOrdersSats.safe() + pendingChannelsSats.safe()
50+
val orderPaymentsOnchainToSubtract = getOrderPaymentOnchainToSubtract(
51+
activeTransfers = activeTransfers,
52+
currentOnchainSats = balanceDetails.totalOnchainBalanceSats,
53+
)
5054

51-
val totalOnchainSats = balanceDetails.totalOnchainBalanceSats
55+
val totalOnchainSats = balanceDetails.totalOnchainBalanceSats.safe() - orderPaymentsOnchainToSubtract.safe()
5256
val channelFundableBalance = getMaxChannelFundableAmount(lightningRepo.getChannelFundableBalance())
5357
val afterPendingChannels = balanceDetails.totalLightningBalanceSats.safe() - pendingChannelsSats.safe()
5458
val afterClosingChannels = afterPendingChannels.safe() - toSavingsAmount.safe()
@@ -95,6 +99,27 @@ class DeriveBalanceStateUseCase @Inject constructor(
9599
return amount
96100
}
97101

102+
private fun getOrderPaymentOnchainToSubtract(
103+
activeTransfers: List<TransferEntity>,
104+
currentOnchainSats: ULong,
105+
): ULong {
106+
var amount = 0uL
107+
val orderPayments = activeTransfers.filter {
108+
it.type.isToSpending() && it.fundingTxId != null && it.lspOrderId != null
109+
}
110+
111+
for (transfer in orderPayments) {
112+
val txTotalSats = transfer.txTotalSats?.takeIf { it > 0 }?.toULong()
113+
val preTransferOnchainSats = transfer.preTransferOnchainSats?.takeIf { it > 0 }?.toULong()
114+
115+
if (txTotalSats != null && preTransferOnchainSats != null && currentOnchainSats >= preTransferOnchainSats) {
116+
amount = amount.safe() + txTotalSats.safe()
117+
}
118+
}
119+
120+
return amount
121+
}
122+
98123
private suspend fun getPendingChannelsSats(
99124
transfers: List<TransferEntity>,
100125
channels: List<ChannelDetails>,

app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,8 @@ class TransferViewModel @Inject constructor(
206206
val address = order.payment?.onchain?.address.orEmpty()
207207

208208
// Use live spendableOnchainBalanceSats (not cached) to respect anchor reserves
209-
val spendableBalance =
210-
lightningRepo.getBalancesAsync().getOrNull()?.spendableOnchainBalanceSats ?: 0uL
209+
val balanceDetails = lightningRepo.getBalancesAsync().getOrNull()
210+
val spendableBalance = balanceDetails?.spendableOnchainBalanceSats ?: 0uL
211211
val sendAllFee = lightningRepo.estimateSendAllFee(
212212
address = address,
213213
speed = speed,
@@ -222,6 +222,24 @@ class TransferViewModel @Inject constructor(
222222
val shouldUseSendAll =
223223
expectedChange >= 0 && expectedChange < TRANSFER_SEND_ALL_THRESHOLD_SATS
224224

225+
val miningFee = if (shouldUseSendAll) {
226+
sendAllFee
227+
} else {
228+
lightningRepo.calculateTotalFee(
229+
amountSats = order.feeSat,
230+
address = address,
231+
speed = speed,
232+
).getOrElse {
233+
Logger.warn("Failed to estimate transfer funding fee", it, context = TAG)
234+
0uL
235+
}
236+
}
237+
val txTotalSats = if (shouldUseSendAll) {
238+
spendableBalance
239+
} else {
240+
order.feeSat.safe() + miningFee.safe()
241+
}
242+
225243
Logger.debug(
226244
"BT confirm: spendable=$spendableBalance, feeSat=${order.feeSat}, " +
227245
"sendAllFee=$sendAllFee, expectedChange=$expectedChange, sendAll=$shouldUseSendAll",
@@ -237,7 +255,14 @@ class TransferViewModel @Inject constructor(
237255
channelId = order.channel?.shortChannelId,
238256
isMaxAmount = shouldUseSendAll,
239257
)
240-
.onSuccess { txId -> fundPaidOrder(order, txId) }
258+
.onSuccess { txId ->
259+
fundPaidOrder(
260+
order = order,
261+
txId = txId,
262+
txTotalSats = txTotalSats,
263+
preTransferOnchainSats = balanceDetails?.totalOnchainBalanceSats ?: spendableBalance,
264+
)
265+
}
241266
.onFailure { error ->
242267
ToastEventBus.send(error)
243268
}
@@ -251,13 +276,17 @@ class TransferViewModel @Inject constructor(
251276
createTransferActivity: Boolean = false,
252277
fee: ULong = 0uL,
253278
feeRate: ULong = 0uL,
279+
txTotalSats: ULong? = null,
280+
preTransferOnchainSats: ULong? = null,
254281
) {
255282
cacheStore.addPaidOrder(orderId = order.id, txId = txId)
256283
transferRepo.createTransfer(
257284
type = TransferType.TO_SPENDING,
258285
amountSats = order.clientBalanceSat.toLong(),
259286
fundingTxId = txId,
260287
lspOrderId = order.id,
288+
txTotalSats = txTotalSats?.toLong(),
289+
preTransferOnchainSats = preTransferOnchainSats?.toLong(),
261290
)
262291
if (createTransferActivity) {
263292
transferRepo.createPendingToSpendingActivity(

app/src/test/java/to/bitkit/models/BalanceStateTest.kt

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@ class BalanceStateTest {
1111
assertEquals(150uL, state.totalSats)
1212
}
1313

14+
@Test
15+
fun `totalSats includes balances in transfer`() {
16+
val state = BalanceState(
17+
totalOnchainSats = 100uL,
18+
totalLightningSats = 50uL,
19+
balanceInTransferToSavings = 10uL,
20+
balanceInTransferToSpending = 15uL,
21+
)
22+
assertEquals(175uL, state.totalSats)
23+
}
24+
1425
@Test
1526
fun `totalHardwareSats sums all hardware wallet balances`() {
1627
val state = BalanceState(
@@ -27,9 +38,10 @@ class BalanceStateTest {
2738
val state = BalanceState(
2839
totalOnchainSats = 100uL,
2940
totalLightningSats = 50uL,
41+
balanceInTransferToSpending = 15uL,
3042
hardwareWallets = listOf(HwWalletBalance(id = "dev1", sats = 25uL)),
3143
)
32-
assertEquals(175uL, state.totalWithHardwareSats)
44+
assertEquals(190uL, state.totalWithHardwareSats)
3345
}
3446

3547
@Test
@@ -46,4 +58,13 @@ class BalanceStateTest {
4658
)
4759
assertEquals(ULong.MAX_VALUE, state.totalWithHardwareSats)
4860
}
61+
62+
@Test
63+
fun `totalSats saturates instead of overflowing`() {
64+
val state = BalanceState(
65+
totalOnchainSats = ULong.MAX_VALUE,
66+
balanceInTransferToSpending = 10uL,
67+
)
68+
assertEquals(ULong.MAX_VALUE, state.totalSats)
69+
}
4970
}

0 commit comments

Comments
 (0)