Release Vexta Wallet v0.6.6
This commit is contained in:
parent
6ee4b78298
commit
e1f63b1727
5 changed files with 1610 additions and 105 deletions
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue