Release Vexta Wallet v0.6.6

This commit is contained in:
Vexta Project 2026-08-26 23:06:36 +00:00
commit e1f63b1727
5 changed files with 1610 additions and 105 deletions

View file

@ -22,8 +22,8 @@ android {
applicationId = "org.vextaproject.wallet"
minSdk = 26
targetSdk = 36
versionCode = 35
versionName = "0.6.5"
versionCode = 36
versionName = "0.6.6"
}
signingConfigs {

View file

@ -34,7 +34,11 @@ object BlockScanner {
data class WalletTransaction(
val txid: String,
val height: Int,
val netSatoshis: Long
val netSatoshis: Long,
val blockTime: Long,
val isPending: Boolean = false,
val feeSatoshis: Long? = null,
val recipientAddress: String? = null
)
data class Result(
@ -78,16 +82,26 @@ object BlockScanner {
matchingHeights: List<Int>,
walletScript: ByteArray,
addressIndex: Int,
initialUtxos: List<SpendableUtxo> = emptyList(),
progress: (Int, Int) -> Unit
): Result {
if (matchingHeights.isEmpty()) {
val existing =
initialUtxos
.filter { it.addressIndex == addressIndex }
.sortedWith(
compareBy<SpendableUtxo> { it.height }
.thenBy { it.txid }
.thenBy { it.outputIndex }
)
return Result(
downloadedBlocks = 0,
receivedTransactions = 0,
spentTransactions = 0,
utxoCount = 0,
balanceSatoshis = 0,
utxos = emptyList(),
utxoCount = existing.size,
balanceSatoshis = existing.sumOf { it.value },
utxos = existing,
transactions = emptyList()
)
}
@ -108,6 +122,26 @@ object BlockScanner {
}
val utxos = linkedMapOf<String, Utxo>()
initialUtxos
.filter { it.addressIndex == addressIndex }
.forEach { existing ->
val txidWire =
existing.txid
.chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
.reversedArray()
utxos[outPointKey(txidWire, existing.outputIndex)] =
Utxo(
txidWire = txidWire,
outputIndex = existing.outputIndex,
value = existing.value,
height = existing.height,
addressIndex = existing.addressIndex
)
}
var receivedTransactions = 0
var spentTransactions = 0
var downloadedBlocks = 0
@ -145,6 +179,17 @@ object BlockScanner {
}
val blockHeader = payload.copyOfRange(0, 80)
val blockTime =
ByteBuffer.wrap(
blockHeader,
68,
4
)
.order(ByteOrder.LITTLE_ENDIAN)
.int
.toLong() and 0xffffffffL
val blockHashWire = doubleSha256(blockHeader)
val height = heightByHash[blockHashWire.toHex()]
?: throw IllegalStateException(
@ -212,7 +257,8 @@ object BlockScanner {
.reversedArray()
.toHex(),
height = height,
netSatoshis = netSatoshis
netSatoshis = netSatoshis,
blockTime = blockTime
)
)
}

View file

@ -128,6 +128,7 @@ object CompactFilterClient {
context: Context,
peer: String,
scriptPubKey: ByteArray,
startHeight: Int = 1,
progress: (Int, Int) -> Unit
): ScanResult {
val headerHashes = loadHeaderHashes(context)
@ -151,7 +152,8 @@ object CompactFilterClient {
var compactFiltersAvailable = false
var gotVersion = false
var gotVerack = false
var nextHeight = 1
var nextHeight =
startHeight.coerceIn(1, localHeight)
var requestedEndHeight = 0
var receivedInBatch = 0
var totalScanned = 0
@ -239,7 +241,7 @@ object CompactFilterClient {
requestedEndHeight == 0
) {
requestedEndHeight = minOf(
maxFiltersPerRequest,
nextHeight + maxFiltersPerRequest - 1,
localHeight
)

File diff suppressed because it is too large Load diff

View file

@ -41,6 +41,15 @@ class WalletMonitoringService : Service() {
"known_incoming_txids"
private const val PREF_TX_NOTIFICATIONS_INITIALIZED =
"tx_notifications_initialized"
private const val PREF_LAST_BACKGROUND_SCAN_HEIGHT =
"last_background_scan_height"
private const val PREF_BACKGROUND_UTXO_PREFIX =
"background_utxos_"
private const val PREF_BACKGROUND_TX_HISTORY =
"background_transaction_history"
private const val PREF_BACKGROUND_TX_HISTORY_VERSION =
"background_transaction_history_version"
private const val BACKGROUND_TX_HISTORY_VERSION = 1
}
private val handler = Handler(Looper.getMainLooper())
@ -103,14 +112,64 @@ class WalletMonitoringService : Service() {
.trim()
.split(Regex("\\s+"))
val highestAddressIndex =
val preferences =
getSharedPreferences(PREFS, MODE_PRIVATE)
val highestAddressIndex =
preferences
.getInt(PREF_RECEIVE_ADDRESS_INDEX, 0)
.coerceAtLeast(0)
val localHeight = chain.last().height
val previousHeight =
preferences
.getInt(
PREF_LAST_BACKGROUND_SCAN_HEIGHT,
0
)
.coerceAtLeast(0)
val backgroundCacheAvailable =
(0..highestAddressIndex).all { addressIndex ->
preferences.contains(
PREF_BACKGROUND_UTXO_PREFIX +
addressIndex
)
}
val transactionHistoryCurrent =
preferences.getInt(
PREF_BACKGROUND_TX_HISTORY_VERSION,
0
) == BACKGROUND_TX_HISTORY_VERSION
val fullScanRequired =
previousHeight <= 0 ||
previousHeight > localHeight ||
!backgroundCacheAvailable ||
!transactionHistoryCurrent
if (
!fullScanRequired &&
previousHeight == localHeight
) {
return@Thread
}
val startHeight =
if (fullScanRequired) {
1
} else {
previousHeight + 1
}
val allTransactions =
mutableListOf<BlockScanner.WalletTransaction>()
val updatedUtxos =
linkedMapOf<Int, List<BlockScanner.SpendableUtxo>>()
for (addressIndex in 0..highestAddressIndex) {
val script =
deriveWitnessScript(words, addressIndex)
@ -119,33 +178,52 @@ class WalletMonitoringService : Service() {
CompactFilterClient.scan(
this,
"87.106.99.23",
script
script,
startHeight = startHeight
) { _, _ -> }
val initialUtxos =
if (fullScanRequired) {
emptyList()
} else {
loadBackgroundUtxos(
preferences,
addressIndex
)
}
val blocks =
BlockScanner.scan(
this,
"87.106.99.23",
filters.matchingHeights,
script,
addressIndex
addressIndex,
initialUtxos = initialUtxos
) { _, _ -> }
allTransactions.addAll(blocks.transactions)
updatedUtxos[addressIndex] =
blocks.utxos
}
val mergedTransactions =
allTransactions
.groupBy { it.txid }
.map { (_, transactions) ->
BlockScanner.WalletTransaction(
txid = transactions.first().txid,
height = transactions.maxOf {
val newest =
transactions.maxByOrNull {
it.height
},
} ?: transactions.first()
BlockScanner.WalletTransaction(
txid = newest.txid,
height = newest.height,
netSatoshis = transactions.sumOf {
it.netSatoshis
}
},
blockTime = newest.blockTime
)
}
.filter { it.netSatoshis != 0L }
@ -153,6 +231,59 @@ class WalletMonitoringService : Service() {
updateIncomingTransactionsAndNotify(
mergedTransactions
)
val cachedTransactions =
if (fullScanRequired) {
mergedTransactions
} else {
(
loadBackgroundTransactions(preferences) +
mergedTransactions
)
.groupBy { it.txid }
.map { (_, transactions) ->
transactions.maxByOrNull {
it.height
} ?: transactions.first()
}
}
.sortedByDescending {
it.blockTime
}
val editor = preferences.edit()
editor.putStringSet(
PREF_BACKGROUND_TX_HISTORY,
encodeBackgroundTransactions(
cachedTransactions
)
)
for (
addressIndex in 0..highestAddressIndex
) {
editor.putStringSet(
PREF_BACKGROUND_UTXO_PREFIX +
addressIndex,
encodeBackgroundUtxos(
updatedUtxos[addressIndex]
?: emptyList()
)
)
}
editor.putInt(
PREF_LAST_BACKGROUND_SCAN_HEIGHT,
localHeight
)
editor.putInt(
PREF_BACKGROUND_TX_HISTORY_VERSION,
BACKGROUND_TX_HISTORY_VERSION
)
editor.apply()
} catch (_: Exception) {
} finally {
scanRunning = false
@ -160,6 +291,47 @@ class WalletMonitoringService : Service() {
}.start()
}
private fun encodeBackgroundTransactions(
transactions: List<BlockScanner.WalletTransaction>
): Set<String> =
transactions.map { transaction ->
listOf(
transaction.txid,
transaction.height.toString(),
transaction.netSatoshis.toString(),
transaction.blockTime.toString()
).joinToString("|")
}.toSet()
private fun loadBackgroundTransactions(
preferences: android.content.SharedPreferences
): List<BlockScanner.WalletTransaction> {
val values =
preferences.getStringSet(
PREF_BACKGROUND_TX_HISTORY,
emptySet()
) ?: emptySet()
return values.mapNotNull { value ->
try {
val parts = value.split("|")
if (parts.size != 4) {
return@mapNotNull null
}
BlockScanner.WalletTransaction(
txid = parts[0],
height = parts[1].toInt(),
netSatoshis = parts[2].toLong(),
blockTime = parts[3].toLong()
)
} catch (_: Exception) {
null
}
}
}
private fun updateIncomingTransactionsAndNotify(
transactions: List<BlockScanner.WalletTransaction>
) {
@ -279,6 +451,50 @@ class WalletMonitoringService : Service() {
)
}
private fun encodeBackgroundUtxos(
utxos: List<BlockScanner.SpendableUtxo>
): Set<String> =
utxos.map { utxo ->
listOf(
utxo.txid,
utxo.outputIndex.toString(),
utxo.value.toString(),
utxo.height.toString(),
utxo.addressIndex.toString()
).joinToString("|")
}.toSet()
private fun loadBackgroundUtxos(
preferences: android.content.SharedPreferences,
addressIndex: Int
): List<BlockScanner.SpendableUtxo> {
val values =
preferences.getStringSet(
PREF_BACKGROUND_UTXO_PREFIX + addressIndex,
emptySet()
) ?: emptySet()
return values.mapNotNull { value ->
try {
val parts = value.split("|")
if (parts.size != 5) {
return@mapNotNull null
}
BlockScanner.SpendableUtxo(
txid = parts[0],
outputIndex = parts[1].toLong(),
value = parts[2].toLong(),
height = parts[3].toInt(),
addressIndex = parts[4].toInt()
)
} catch (_: Exception) {
null
}
}
}
private fun walletExists(): Boolean {
val prefs =
getSharedPreferences(PREFS, MODE_PRIVATE)