Skip to content
Draft
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
17 changes: 14 additions & 3 deletions app/src/main/java/to/bitkit/ext/Activities.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,24 @@ 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
import to.bitkit.models.ActivityWalletType

val DEFAULT_WALLET_ID: String get() = ActivityWalletType.BITKIT.id()

fun Activity.rawId(): String = when (this) {
is Activity.Lightning -> v1.id
is Activity.Onchain -> v1.id
}

fun Activity.walletId(): String = when (this) {
is Activity.Lightning -> v1.walletId
is Activity.Onchain -> v1.walletId
}

fun Activity.scopedId(): String = "${walletId()}:${rawId()}"

fun Activity.isFromHardwareWallet(): Boolean = ActivityWalletType.TREZOR.owns(walletId())

fun Activity.txType(): PaymentType = when (this) {
is Activity.Lightning -> v1.txType
is Activity.Onchain -> v1.txType
Expand Down Expand Up @@ -95,7 +106,6 @@ enum class BoostType { RBF, CPFP }

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

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

import java.util.Locale

enum class ActivityWalletType {
BITKIT, TREZOR;

fun id(): String = name.lowercase(Locale.US)

fun idPrefixed(value: String): String = "${prefix()}$value"

fun owns(walletId: String): Boolean = walletId == id() || walletId.startsWith(prefix())

private fun prefix(): String = "${id()}:"
}
1 change: 1 addition & 0 deletions app/src/main/java/to/bitkit/models/HardwareWallet.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ data class HwWalletBalance(
data class HwWalletReceivedTx(
val txid: String,
val sats: ULong,
val walletId: String,
)

sealed interface HwFundingAccount {
Expand Down
104 changes: 104 additions & 0 deletions app/src/main/java/to/bitkit/models/HwWalletIdentity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package to.bitkit.models

import to.bitkit.utils.sha256d
import java.math.BigInteger

private const val EXTENDED_KEY_VERSION_SIZE = 4
private const val BASE58_CHECKSUM_SIZE = 4
private const val BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
private const val BASE58_ZERO = '1'
private val BASE58_RADIX = BigInteger.valueOf(58L)
private val XPUB_VERSION = byteArrayOf(0x04.toByte(), 0x88.toByte(), 0xB2.toByte(), 0x1E.toByte())
private val TPUB_VERSION = byteArrayOf(0x04.toByte(), 0x35.toByte(), 0x87.toByte(), 0xCF.toByte())

val KnownDevice.identityKey: String
get() = identityKey(xpubs, id)

fun identityKey(xpubs: Map<String, String>, fallback: String): String =
xpubs.values.sorted().joinToString().ifEmpty { fallback }

fun List<KnownDevice>.findWalletId(
deviceId: String,
xpubs: Map<String, String>,
deriveWalletId: (Collection<String>) -> String,
): String {
val targetIdentityKey = identityKey(xpubs, deviceId)
firstOrNull { it.identityKey == targetIdentityKey }?.walletId?.takeIf { it.isNotBlank() }?.let { return it }
if (xpubs.values.any { it.isNotBlank() }) return deriveWalletId(xpubs.values)

return firstOrNull { it.id == deviceId }?.walletId?.takeIf { it.isNotBlank() }.orEmpty()
}

fun List<KnownDevice>.withWalletIds(
deriveWalletId: (Collection<String>) -> String,
): List<KnownDevice> {
val existingByIdentity = filter { it.walletId.isNotBlank() }
.associate { it.identityKey to it.walletId }
val generatedByIdentity = mutableMapOf<String, String>()

return map {
val walletId = existingByIdentity[it.identityKey]
?: generatedByIdentity.getOrPut(it.identityKey) { deriveWalletId(it.xpubs.values) }
if (it.walletId == walletId) it else it.copy(walletId = walletId)
}
}

fun List<KnownDevice>.withStandardExtendedKeys(): List<KnownDevice> = map { device ->
val normalized = device.xpubs.mapValues { it.value.toStandardExtendedKey() }
if (normalized == device.xpubs) device else device.copy(xpubs = normalized)
}

fun String.toStandardExtendedKey(): String {
val version = when (take(EXTENDED_KEY_VERSION_SIZE)) {
"xpub", "tpub" -> return this
"ypub", "zpub" -> XPUB_VERSION
"upub", "vpub" -> TPUB_VERSION
else -> return this
}
val payload = decodeBase58Check().getOrNull() ?: return this
if (payload.size < EXTENDED_KEY_VERSION_SIZE) return this

val normalized = payload.copyOf()
version.copyInto(normalized, endIndex = EXTENDED_KEY_VERSION_SIZE)
return normalized.encodeBase58Check()
}

private fun String.decodeBase58Check(): Result<ByteArray> = runCatching {
val decoded = decodeBase58()
check(decoded.size >= BASE58_CHECKSUM_SIZE)
val payload = decoded.copyOfRange(0, decoded.size - BASE58_CHECKSUM_SIZE)
val checksum = decoded.copyOfRange(decoded.size - BASE58_CHECKSUM_SIZE, decoded.size)
check(sha256d(payload).copyOfRange(0, BASE58_CHECKSUM_SIZE).contentEquals(checksum))
payload
}

private fun String.decodeBase58(): ByteArray {
var value = BigInteger.ZERO
forEach {
val digit = BASE58_ALPHABET.indexOf(it)
require(digit >= 0)
value = value.multiply(BASE58_RADIX).add(BigInteger.valueOf(digit.toLong()))
}

val bytes = value.toByteArray().let {
if (it.size > 1 && it.first() == 0.toByte()) it.copyOfRange(1, it.size) else it
}.takeUnless { value == BigInteger.ZERO } ?: byteArrayOf()
return ByteArray(takeWhile { it == BASE58_ZERO }.length) + bytes
}

private fun ByteArray.encodeBase58Check(): String {
val checksum = sha256d(this).copyOfRange(0, BASE58_CHECKSUM_SIZE)
return (this + checksum).encodeBase58()
}

private fun ByteArray.encodeBase58(): String {
var value = BigInteger(1, this)
val encoded = StringBuilder()
while (value > BigInteger.ZERO) {
val divideAndRemainder = value.divideAndRemainder(BASE58_RADIX)
value = divideAndRemainder[0]
encoded.append(BASE58_ALPHABET[divideAndRemainder[1].toInt()])
}
repeat(takeWhile { it == 0.toByte() }.size) { encoded.append(BASE58_ZERO) }
return encoded.reverse().toString()
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ data class NewTransactionSheetDetails(
val direction: NewTransactionSheetDirection,
val paymentHashOrTxId: String? = null,
val activityId: String? = null,
val activityWalletId: String? = null,
val sats: Long = 0,
val isLoadingDetails: Boolean = false,
) {
Expand Down
Loading
Loading