Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
24 changes: 24 additions & 0 deletions app/src/main/java/to/bitkit/ext/TrezorExceptionExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,27 @@ fun Throwable.isTrezorFirmwareError(): Boolean =
val message = it.message.orEmpty()
"Device error (code $FIRMWARE_ERROR_CODE)" in message && "Firmware error" in message
}

fun Throwable.isTrezorSessionFailure(): Boolean =
generateSequence(this) { it.cause }.any { error ->
when (error) {
is TrezorException.TransportException,
is TrezorException.DeviceDisconnected,
is TrezorException.ConnectionException,
is TrezorException.Timeout,
is TrezorException.NotConnected,
is TrezorException.SessionException,
is TrezorException.IoException,
-> true

is TrezorException.ProtocolException -> error.errorDetails.lowercase().let { details ->
"thp decryption" in details ||
"thp encryption" in details ||
"thp ack" in details ||
"thp invalid sync" in details ||
"thp state missing" in details
}

else -> false
}
}
17 changes: 17 additions & 0 deletions app/src/main/java/to/bitkit/models/HardwareWallet.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.compose.runtime.Stable
import com.synonym.bitkitcore.AccountType
import com.synonym.bitkitcore.Activity
import com.synonym.bitkitcore.AddressType
import com.synonym.bitkitcore.TrezorScriptType
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
Expand Down Expand Up @@ -41,6 +42,14 @@ data class HwWalletReceivedTx(
val walletId: String,
)

/** An unused external address derived from a paired hardware wallet's stored account xpub. */
@Immutable
data class HwReceiveAddress(
val address: String,
val path: String,
val addressType: HwFundingAddressType,
)

sealed interface HwFundingAccount {
val vendor: HwWalletVendor
val xpub: String
Expand Down Expand Up @@ -99,6 +108,14 @@ enum class HwFundingAddressType(
val accountType: AccountType
get() = addressType.toAccountType()

val trezorScriptType: TrezorScriptType
get() = when (this) {
LEGACY -> TrezorScriptType.SPEND_ADDRESS
NESTED_SEGWIT -> TrezorScriptType.SPEND_P2SH_WITNESS
NATIVE_SEGWIT -> TrezorScriptType.SPEND_WITNESS
TAPROOT -> TrezorScriptType.SPEND_TAPROOT
}

companion object {
val DEFAULT: HwFundingAddressType = entries.first { it.addressType == DEFAULT_ADDRESS_TYPE }
}
Expand Down
73 changes: 53 additions & 20 deletions app/src/main/java/to/bitkit/repositories/ActivityRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -347,11 +347,30 @@ class ActivityRepo @Inject constructor(
type: ActivityFilter,
txType: PaymentType?,
retry: Boolean = true,
): Result<Activity> = findActivityByPaymentId(
paymentHashOrTxId = paymentHashOrTxId,
type = type,
txType = txType,
retry = retry,
walletId = WalletScope.default,
)

suspend fun findActivityByPaymentId(
paymentHashOrTxId: String,
type: ActivityFilter,
txType: PaymentType?,
retry: Boolean,
walletId: String,
): Result<Activity> = withContext(bgDispatcher) {
runCatching {
require(paymentHashOrTxId.isNotEmpty()) { "paymentHashOrTxId is empty" }

suspend fun findActivity(): Activity? = getActivities(filter = type, txType = txType, limit = 10u)
suspend fun findActivity(): Activity? = getActivities(
walletId = walletId,
filter = type,
txType = txType,
limit = 10u,
)
.getOrNull()
?.firstOrNull { it.matchesPaymentId(paymentHashOrTxId) }

Expand All @@ -362,21 +381,25 @@ class ActivityRepo @Inject constructor(
context = TAG
)

lightningRepo.sync().onSuccess { Logger.debug("Syncing LN node SUCCESS", context = TAG) }

syncActivities().onSuccess {
Logger.debug(
"Sync success, searching again the activity with paymentHashOrTxId:'$paymentHashOrTxId'",
context = TAG,
)
if (walletId == WalletScope.default) {
lightningRepo.sync().onSuccess { Logger.debug("Syncing LN node SUCCESS", context = TAG) }
syncActivities().onSuccess {
Logger.debug(
"Sync success, searching again the activity with paymentHashOrTxId:'$paymentHashOrTxId'",
context = TAG,
)
activity = findActivity()
}
} else {
activity = findActivity()
}
}

checkNotNull(activity) { "Activity not found" }
}.onFailure {
Logger.error(
"findActivityByPaymentId error (paymentHashOrTxId:'$paymentHashOrTxId' type:'$type' txType:'$txType')",
"findActivityByPaymentId error " +
"(paymentHashOrTxId:'$paymentHashOrTxId' type:'$type' txType:'$txType' walletId:'$walletId')",
context = TAG,
)
}
Expand Down Expand Up @@ -438,6 +461,7 @@ class ActivityRepo @Inject constructor(
val normalizedKey = PubkyPublicKeyFormat.normalized(publicKey) ?: publicKey
val txIdsInBoostTxIds = getTxIdsInBoostTxIds()
getActivities(
walletId = null,
filter = ActivityFilter.ALL,
sortDirection = SortDirection.DESC,
).getOrThrow()
Expand All @@ -452,6 +476,7 @@ class ActivityRepo @Inject constructor(
contactPublicKey: String,
forPaymentId: String,
syncLdkPayments: Boolean = true,
walletId: String = WalletScope.default,
): Result<Unit> = withContext(ioDispatcher) {
runCatching {
if (syncLdkPayments) {
Expand All @@ -461,7 +486,7 @@ class ActivityRepo @Inject constructor(
}

val normalizedKey = PubkyPublicKeyFormat.normalized(contactPublicKey) ?: contactPublicKey
val activity = findActivityForPaymentId(forPaymentId, syncLdkPayments)
val activity = findActivityForPaymentId(forPaymentId, syncLdkPayments, walletId)
if (activity == null) {
Logger.warn(
"Skipped setting contact for payment '$forPaymentId' because activity was not found",
Expand All @@ -476,7 +501,7 @@ class ActivityRepo @Inject constructor(
val updatedAt = nowTimestamp().epochSecond.toULong()
val updatedActivity = activity.withContact(normalizedKey, updatedAt)
updateActivity(updatedActivity.rawId(), updatedActivity).getOrThrow()
updateReplacementContactIfNeeded(updatedActivity, normalizedKey, updatedAt)
updateReplacementContactIfNeeded(updatedActivity, normalizedKey, updatedAt, walletId)
}.onFailure {
Logger.error("Failed to set contact for payment '$forPaymentId'", it, context = TAG)
}
Expand All @@ -485,6 +510,7 @@ class ActivityRepo @Inject constructor(
suspend fun clearContact(
forPaymentId: String,
syncLdkPayments: Boolean = true,
walletId: String = WalletScope.default,
): Result<Unit> = withContext(ioDispatcher) {
runCatching {
if (syncLdkPayments) {
Expand All @@ -493,7 +519,7 @@ class ActivityRepo @Inject constructor(
}.getOrThrow()
}

val activity = findActivityForPaymentId(forPaymentId, syncLdkPayments)
val activity = findActivityForPaymentId(forPaymentId, syncLdkPayments, walletId)
if (activity == null) {
Logger.warn(
"Skipped clearing contact for payment '$forPaymentId' because activity was not found",
Expand All @@ -506,7 +532,7 @@ class ActivityRepo @Inject constructor(
val updatedAt = nowTimestamp().epochSecond.toULong()
val updatedActivity = activity.withContact(null, updatedAt)
updateActivity(updatedActivity.rawId(), updatedActivity).getOrThrow()
updateReplacementContactIfNeeded(updatedActivity, null, updatedAt)
updateReplacementContactIfNeeded(updatedActivity, null, updatedAt, walletId)
}.onFailure {
Logger.error("Failed to clear contact for payment '$forPaymentId'", it, context = TAG)
}
Expand All @@ -516,10 +542,11 @@ class ActivityRepo @Inject constructor(
activity: Activity,
normalizedKey: String?,
updatedAt: ULong,
walletId: String = WalletScope.default,
) {
if (activity !is Activity.Onchain || activity.v1.doesExist || activity.v1.txType != PaymentType.SENT) return

getActivities(filter = ActivityFilter.ONCHAIN).getOrThrow()
getActivities(walletId = walletId, filter = ActivityFilter.ONCHAIN).getOrThrow()
.filterIsInstance<Activity.Onchain>()
.filter { activity.v1.txId in it.v1.boostTxIds }
.filterNot { PubkyPublicKeyFormat.matches(it.v1.contact, normalizedKey) }
Expand All @@ -529,18 +556,24 @@ class ActivityRepo @Inject constructor(
}
}

private suspend fun findActivityForPaymentId(forPaymentId: String, syncLdkPayments: Boolean): Activity? {
val activity = getActivityByPaymentId(forPaymentId)
private suspend fun findActivityForPaymentId(
forPaymentId: String,
syncLdkPayments: Boolean,
walletId: String = WalletScope.default,
): Activity? {
val activity = getActivityByPaymentId(forPaymentId, walletId)
if (activity != null) return activity
if (!syncLdkPayments) return null

syncActivities().getOrThrow()
return getActivityByPaymentId(forPaymentId)
return getActivityByPaymentId(forPaymentId, walletId)
}

private suspend fun getActivityByPaymentId(forPaymentId: String): Activity? =
coreService.activity.getActivity(forPaymentId, WalletScope.default)
?: getOnchainActivityByTxId(forPaymentId)?.let { Activity.Onchain(it) }
private suspend fun getActivityByPaymentId(
forPaymentId: String,
walletId: String = WalletScope.default,
): Activity? = coreService.activity.getActivity(forPaymentId, walletId)
?: getOnchainActivityByTxId(forPaymentId, walletId)?.let { Activity.Onchain(it) }

private fun Activity.withContact(normalizedKey: String?, updatedAt: ULong): Activity = when (this) {
is Activity.Lightning -> Activity.Lightning(v1.copy(contact = normalizedKey, updatedAt = updatedAt))
Expand Down
Loading
Loading