Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/src/main/java/to/bitkit/ext/Activities.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.synonym.bitkitcore.LightningActivity
import com.synonym.bitkitcore.OnchainActivity
import com.synonym.bitkitcore.PaymentState
import com.synonym.bitkitcore.PaymentType
import to.bitkit.models.WalletScope

fun Activity.rawId(): String = when (this) {
is Activity.Lightning -> v1.id
Expand Down Expand Up @@ -94,6 +95,7 @@ enum class BoostType { RBF, CPFP }

@Suppress("LongParameterList")
fun LightningActivity.Companion.create(
walletId: String = WalletScope.default,
id: String,
txType: PaymentType,
status: PaymentState,
Expand All @@ -108,6 +110,7 @@ fun LightningActivity.Companion.create(
updatedAt: ULong? = createdAt,
seenAt: ULong? = null,
) = LightningActivity(
walletId = walletId,
id = id,
txType = txType,
status = status,
Expand All @@ -125,6 +128,7 @@ fun LightningActivity.Companion.create(

@Suppress("LongParameterList")
fun OnchainActivity.Companion.create(
walletId: String = WalletScope.default,
id: String,
txType: PaymentType,
txId: String,
Expand All @@ -146,6 +150,7 @@ fun OnchainActivity.Companion.create(
updatedAt: ULong? = createdAt,
seenAt: ULong? = null,
) = OnchainActivity(
walletId = walletId,
id = id,
txType = txType,
txId = txId,
Expand Down
17 changes: 17 additions & 0 deletions app/src/main/java/to/bitkit/ext/TrezorExceptionExt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package to.bitkit.ext

import com.synonym.bitkitcore.TrezorException

fun Throwable.isTrezorUserCancellation(): Boolean {
var current: Throwable? = this
while (current != null) {
when (current) {
is TrezorException.UserCancelled,
is TrezorException.PinCancelled,
is TrezorException.PassphraseCancelled,
-> return true
}
current = current.cause
}
return false
}
Comment thread
jvsena42 marked this conversation as resolved.
Outdated
10 changes: 10 additions & 0 deletions app/src/main/java/to/bitkit/models/HwWalletId.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package to.bitkit.models

import com.synonym.bitkitcore.deriveWalletId

object HwWalletId {
fun derive(xpubs: Map<String, String>, deviceType: String = "trezor"): String {
require(xpubs.isNotEmpty()) { "xpubs must not be empty" }
return deriveWalletId(deviceType = deviceType, xpubs = xpubs.values.toList())
}
}
14 changes: 14 additions & 0 deletions app/src/main/java/to/bitkit/models/WalletScope.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package to.bitkit.models

import androidx.annotation.VisibleForTesting
import com.synonym.bitkitcore.getDefaultWalletId

object WalletScope {
@VisibleForTesting
internal var testOverride: String? = null
Comment thread
jvsena42 marked this conversation as resolved.
Outdated

val default: String
get() = testOverride ?: lazyDefault

private val lazyDefault: String by lazy { getDefaultWalletId() }
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}
3 changes: 2 additions & 1 deletion app/src/main/java/to/bitkit/repositories/ActivityRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import to.bitkit.di.BgDispatcher
import to.bitkit.di.IoDispatcher
import to.bitkit.ext.amountOnClose
import to.bitkit.ext.contact
import to.bitkit.ext.create
import to.bitkit.ext.isReplacedSentTransaction
import to.bitkit.ext.matchesPaymentId
import to.bitkit.ext.nowMillis
Expand Down Expand Up @@ -653,7 +654,7 @@ class ActivityRepo @Inject constructor(
val now = nowTimestamp().epochSecond.toULong()
insertActivity(
Activity.Lightning(
LightningActivity(
LightningActivity.create(
id = id,
txType = PaymentType.RECEIVED,
status = PaymentState.SUCCEEDED,
Expand Down
109 changes: 37 additions & 72 deletions app/src/main/java/to/bitkit/repositories/HwWalletRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@ import com.synonym.bitkitcore.Activity
import com.synonym.bitkitcore.CoinSelection
import com.synonym.bitkitcore.ComposeOutput
import com.synonym.bitkitcore.ComposeResult
import com.synonym.bitkitcore.HistoryTransaction
import com.synonym.bitkitcore.OnchainActivity
import com.synonym.bitkitcore.PaymentType
import com.synonym.bitkitcore.TrezorDeviceInfo
import com.synonym.bitkitcore.TrezorFeatures
import com.synonym.bitkitcore.TxDirection
import com.synonym.bitkitcore.WatcherEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
Expand Down Expand Up @@ -38,6 +36,7 @@ import to.bitkit.data.SettingsStore
import to.bitkit.di.IoDispatcher
import to.bitkit.env.Env
import to.bitkit.ext.create
import to.bitkit.ext.isTrezorUserCancellation
import to.bitkit.ext.rawId
import to.bitkit.ext.runSuspendCatching
import to.bitkit.models.HwFundingAccount
Expand Down Expand Up @@ -107,6 +106,8 @@ class HwWalletRepo @Inject constructor(

fun onAppForegrounded() = trezorRepo.onAppForegrounded()

fun warmUpKnownDevice(deviceId: String) = trezorRepo.warmUpKnownDevice(deviceId)

suspend fun resetState() = withContext(ioDispatcher) {
activeWatchers.toList().forEach { watcherId ->
trezorRepo.stopWatcher(watcherId)
Expand Down Expand Up @@ -153,6 +154,8 @@ class HwWalletRepo @Inject constructor(

suspend fun ensureConnected(deviceId: String): Result<TrezorFeatures> = trezorRepo.ensureConnected(deviceId)

suspend fun isKnownBluetoothDevice(deviceId: String): Boolean = trezorRepo.isKnownBluetoothDevice(deviceId)

suspend fun getFundingAccount(
deviceId: String,
addressType: HwFundingAddressType = HwFundingAddressType.DEFAULT,
Expand Down Expand Up @@ -225,7 +228,10 @@ class HwWalletRepo @Inject constructor(
).getOrThrow()
}
if (signed.isFailure) {
trezorRepo.disconnectStaleSession(deviceId)
val failure = signed.exceptionOrNull()
if (failure?.isTrezorUserCancellation() != true) {
trezorRepo.disconnectStaleSession(deviceId)
}
}
val txId = trezorRepo.broadcastRawTx(serializedTx = signed.getOrThrow().serializedTx).getOrThrow()
HwFundingBroadcastResult(
Expand Down Expand Up @@ -352,22 +358,19 @@ class HwWalletRepo @Inject constructor(
trezorRepo.watcherEvents.collect { (watcherId, event) ->
if (event !is WatcherEvent.TransactionsChanged) return@collect
val previous = _watcherData.value[watcherId]
val activities = event.transactions
.map { it.toOnchainActivity(clock, previous?.activities.orEmpty()) }
.toImmutableList()
val activities = event.activities.toImmutableList()
val watcher = HwWatcherData(
deviceId = watcherId.toDeviceId(),
addressType = watcherId.toAddressTypeKey(),
balanceSats = event.balance.total,
transactions = event.transactions.toImmutableList(),
activities = activities,
)
val updatedWatcherData = _watcherData.value + (watcherId to watcher)
_watcherData.update { updatedWatcherData }
activities.filterIsInstance<Activity.Onchain>().forEach {
activityRepo.syncHardwareOnchainActivity(it.v1)
}
emitReceivedTxs(previous, event, updatedWatcherData)
emitReceivedTxs(previous, activities, updatedWatcherData)
Comment thread
jvsena42 marked this conversation as resolved.
Outdated
}
}
}
Expand All @@ -378,21 +381,21 @@ class HwWalletRepo @Inject constructor(
*/
private suspend fun emitReceivedTxs(
previous: HwWatcherData?,
event: WatcherEvent.TransactionsChanged,
activities: List<Activity>,
watcherData: Map<String, HwWatcherData>,
) {
if (previous == null) return
val knownTxIds = previous.activities.map { it.rawId() }.toSet()
val knownTxIds = previous.activities.mapNotNull { activity ->
(activity as? Activity.Onchain)?.v1?.txId
}.toSet()
val mergedActivities = watcherData.values.toList().toMergedActivities()
event.transactions
.filter {
it.direction == TxDirection.RECEIVED &&
it.txid !in knownTxIds &&
emittedReceivedTxIds.add(it.txid)
}
.forEach {
val sats = mergedActivities.findOnchain(it.txid)?.v1?.value ?: it.amount
_receivedTxs.emit(HwWalletReceivedTx(txid = it.txid, sats = sats))
activities.filterIsInstance<Activity.Onchain>()
.filter { it.v1.txType == PaymentType.RECEIVED }
.forEach { onchain ->
val txid = onchain.v1.txId
if (txid in knownTxIds || !emittedReceivedTxIds.add(txid)) return@forEach
val sats = mergedActivities.findOnchain(txid)?.v1?.value ?: onchain.v1.value
_receivedTxs.emit(HwWalletReceivedTx(txid = txid, sats = sats))
Comment thread
jvsena42 marked this conversation as resolved.
}
}

Expand Down Expand Up @@ -421,7 +424,13 @@ class HwWalletRepo @Inject constructor(
device.xpubs
.filterKeys { it in watcherSettings.monitoredTypes }
.map { (addressType, xpub) ->
WatcherSpec(device.id, addressType, xpub, watcherSettings.electrumUrl)
WatcherSpec(
deviceId = device.id,
addressType = addressType,
xpub = xpub,
electrumUrl = watcherSettings.electrumUrl,
walletId = device.walletId,
Comment thread
jvsena42 marked this conversation as resolved.
Outdated
)
}
}.distinctBy { it.addressType to it.xpub }
val filteredIds = filtered.map { it.watcherId }.toSet()
Expand All @@ -437,6 +446,7 @@ class HwWalletRepo @Inject constructor(
network = Env.network.toCoreNetwork(),
accountType = spec.addressType.toAddressType()?.toAccountType(),
electrumUrl = spec.electrumUrl,
walletId = spec.walletId,
).onSuccess {
activeWatchers += spec.watcherId
activeWatcherElectrumUrls[spec.watcherId] = spec.electrumUrl
Expand Down Expand Up @@ -473,59 +483,14 @@ class HwWalletRepo @Inject constructor(
}
}

private fun HistoryTransaction.toOnchainActivity(clock: Clock, previousActivities: List<Activity>): Activity {
val activityTimestamp = timestamp ?: previousActivities.findOnchain(txid)?.v1?.timestamp
?: clock.now().epochSeconds.toULong()
return listOf(this).toOnchainActivity(
timestamp = activityTimestamp,
sourceActivities = previousActivities,
)
}

private fun List<HwWatcherData>.toMergedActivities(): List<Activity> {
val sourceActivities = flatMap { it.activities }
return flatMap { it.transactions }
.groupBy { it.txid }
.values
.map { transactions ->
val timestamp = transactions.mapNotNull { it.timestamp }.minOrNull()
?: sourceActivities.findOnchain(transactions.first().txid)?.v1?.timestamp
?: 0uL
transactions.toOnchainActivity(timestamp, sourceActivities)
val byId = linkedMapOf<String, Activity>()
sortedBy { "${it.deviceId}|${it.addressType}" }
.flatMap { it.activities }
.forEach { activity ->
byId[activity.rawId()] = activity
Comment thread
jvsena42 marked this conversation as resolved.
Outdated
}
}

private fun List<HistoryTransaction>.toOnchainActivity(
timestamp: ULong,
sourceActivities: List<Activity>,
): Activity {
val first = first()
val received = fold(0uL) { acc, tx -> acc.safe() + tx.received.safe() }
val sent = fold(0uL) { acc, tx -> acc.safe() + tx.sent.safe() }
val fee = mapNotNull { it.fee }.maxOrNull() ?: 0uL
val type = when {
received > sent -> PaymentType.RECEIVED
else -> PaymentType.SENT
}
val value = when (type) {
PaymentType.RECEIVED -> received.safe() - sent.safe()
PaymentType.SENT -> (sent.safe() - received.safe()).safe() - fee.safe()
}
val confirmations = maxOf { it.confirmations }
val sourceActivity = sourceActivities.findOnchain(first.txid)
return Activity.Onchain(
OnchainActivity.create(
id = first.txid,
txType = type,
txId = first.txid,
value = value,
fee = fee,
address = "",
timestamp = timestamp,
confirmed = confirmations > 0u,
confirmTimestamp = sourceActivity?.v1?.confirmTimestamp,
)
)
return byId.values.toList()
}

private fun List<Activity>.findOnchain(txid: String) = filterIsInstance<Activity.Onchain>()
Expand All @@ -536,6 +501,7 @@ class HwWalletRepo @Inject constructor(
val addressType: String,
val xpub: String,
val electrumUrl: String,
val walletId: String,
) {
val watcherId: String get() = "$deviceId$WATCHER_ID_SEPARATOR$addressType"
}
Expand Down Expand Up @@ -577,6 +543,5 @@ private data class HwWatcherData(
val deviceId: String,
val addressType: String,
val balanceSats: ULong,
val transactions: ImmutableList<HistoryTransaction>,
val activities: ImmutableList<Activity>,
)
2 changes: 2 additions & 0 deletions app/src/main/java/to/bitkit/repositories/LightningRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import to.bitkit.services.LnurlWithdrawResponse
import to.bitkit.services.LspNotificationsService
import to.bitkit.services.NodeEventHandler
import to.bitkit.utils.AppError
import to.bitkit.models.WalletScope
import to.bitkit.utils.Logger
import to.bitkit.utils.ServiceError
import to.bitkit.utils.UrlValidator
Expand Down Expand Up @@ -1178,6 +1179,7 @@ class LightningRepo @Inject constructor(
val txId = lightningService.send(address, sats, satsPerVByte, utxosForSend, isMaxAmount)

val preActivityMetadata = PreActivityMetadata(
walletId = WalletScope.default,
paymentId = txId,
createdAt = nowTimestamp().toEpochMilli().toULong(),
tags = tags,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import to.bitkit.di.IoDispatcher
import to.bitkit.ext.nowMillis
import to.bitkit.ext.nowTimestamp
import to.bitkit.services.CoreService
import to.bitkit.models.WalletScope
import to.bitkit.utils.Logger
import javax.inject.Inject
import javax.inject.Singleton
Expand Down Expand Up @@ -127,11 +128,13 @@ class PreActivityMetadataRepo @Inject constructor(
feeRate: ULong? = null,
isTransfer: Boolean = false,
channelId: String? = null,
walletId: String = WalletScope.default,
): Result<Unit> = withContext(ioDispatcher) {
return@withContext runCatching {
require(tags.isNotEmpty() || isTransfer)

val preActivityMetadata = PreActivityMetadata(
walletId = walletId,
paymentId = id,
createdAt = nowTimestamp().toEpochMilli().toULong(),
tags = tags,
Expand Down
Loading
Loading