Release Vexta Wallet v0.6.6
This commit is contained in:
parent
6ee4b78298
commit
e1f63b1727
5 changed files with 1610 additions and 105 deletions
|
|
@ -22,8 +22,8 @@ android {
|
||||||
applicationId = "org.vextaproject.wallet"
|
applicationId = "org.vextaproject.wallet"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 35
|
versionCode = 36
|
||||||
versionName = "0.6.5"
|
versionName = "0.6.6"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,11 @@ object BlockScanner {
|
||||||
data class WalletTransaction(
|
data class WalletTransaction(
|
||||||
val txid: String,
|
val txid: String,
|
||||||
val height: Int,
|
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(
|
data class Result(
|
||||||
|
|
@ -78,16 +82,26 @@ object BlockScanner {
|
||||||
matchingHeights: List<Int>,
|
matchingHeights: List<Int>,
|
||||||
walletScript: ByteArray,
|
walletScript: ByteArray,
|
||||||
addressIndex: Int,
|
addressIndex: Int,
|
||||||
|
initialUtxos: List<SpendableUtxo> = emptyList(),
|
||||||
progress: (Int, Int) -> Unit
|
progress: (Int, Int) -> Unit
|
||||||
): Result {
|
): Result {
|
||||||
if (matchingHeights.isEmpty()) {
|
if (matchingHeights.isEmpty()) {
|
||||||
|
val existing =
|
||||||
|
initialUtxos
|
||||||
|
.filter { it.addressIndex == addressIndex }
|
||||||
|
.sortedWith(
|
||||||
|
compareBy<SpendableUtxo> { it.height }
|
||||||
|
.thenBy { it.txid }
|
||||||
|
.thenBy { it.outputIndex }
|
||||||
|
)
|
||||||
|
|
||||||
return Result(
|
return Result(
|
||||||
downloadedBlocks = 0,
|
downloadedBlocks = 0,
|
||||||
receivedTransactions = 0,
|
receivedTransactions = 0,
|
||||||
spentTransactions = 0,
|
spentTransactions = 0,
|
||||||
utxoCount = 0,
|
utxoCount = existing.size,
|
||||||
balanceSatoshis = 0,
|
balanceSatoshis = existing.sumOf { it.value },
|
||||||
utxos = emptyList(),
|
utxos = existing,
|
||||||
transactions = emptyList()
|
transactions = emptyList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -108,6 +122,26 @@ object BlockScanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
val utxos = linkedMapOf<String, Utxo>()
|
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 receivedTransactions = 0
|
||||||
var spentTransactions = 0
|
var spentTransactions = 0
|
||||||
var downloadedBlocks = 0
|
var downloadedBlocks = 0
|
||||||
|
|
@ -145,6 +179,17 @@ object BlockScanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
val blockHeader = payload.copyOfRange(0, 80)
|
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 blockHashWire = doubleSha256(blockHeader)
|
||||||
val height = heightByHash[blockHashWire.toHex()]
|
val height = heightByHash[blockHashWire.toHex()]
|
||||||
?: throw IllegalStateException(
|
?: throw IllegalStateException(
|
||||||
|
|
@ -212,7 +257,8 @@ object BlockScanner {
|
||||||
.reversedArray()
|
.reversedArray()
|
||||||
.toHex(),
|
.toHex(),
|
||||||
height = height,
|
height = height,
|
||||||
netSatoshis = netSatoshis
|
netSatoshis = netSatoshis,
|
||||||
|
blockTime = blockTime
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,7 @@ object CompactFilterClient {
|
||||||
context: Context,
|
context: Context,
|
||||||
peer: String,
|
peer: String,
|
||||||
scriptPubKey: ByteArray,
|
scriptPubKey: ByteArray,
|
||||||
|
startHeight: Int = 1,
|
||||||
progress: (Int, Int) -> Unit
|
progress: (Int, Int) -> Unit
|
||||||
): ScanResult {
|
): ScanResult {
|
||||||
val headerHashes = loadHeaderHashes(context)
|
val headerHashes = loadHeaderHashes(context)
|
||||||
|
|
@ -151,7 +152,8 @@ object CompactFilterClient {
|
||||||
var compactFiltersAvailable = false
|
var compactFiltersAvailable = false
|
||||||
var gotVersion = false
|
var gotVersion = false
|
||||||
var gotVerack = false
|
var gotVerack = false
|
||||||
var nextHeight = 1
|
var nextHeight =
|
||||||
|
startHeight.coerceIn(1, localHeight)
|
||||||
var requestedEndHeight = 0
|
var requestedEndHeight = 0
|
||||||
var receivedInBatch = 0
|
var receivedInBatch = 0
|
||||||
var totalScanned = 0
|
var totalScanned = 0
|
||||||
|
|
@ -239,7 +241,7 @@ object CompactFilterClient {
|
||||||
requestedEndHeight == 0
|
requestedEndHeight == 0
|
||||||
) {
|
) {
|
||||||
requestedEndHeight = minOf(
|
requestedEndHeight = minOf(
|
||||||
maxFiltersPerRequest,
|
nextHeight + maxFiltersPerRequest - 1,
|
||||||
localHeight
|
localHeight
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -41,6 +41,15 @@ class WalletMonitoringService : Service() {
|
||||||
"known_incoming_txids"
|
"known_incoming_txids"
|
||||||
private const val PREF_TX_NOTIFICATIONS_INITIALIZED =
|
private const val PREF_TX_NOTIFICATIONS_INITIALIZED =
|
||||||
"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())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
@ -103,14 +112,64 @@ class WalletMonitoringService : Service() {
|
||||||
.trim()
|
.trim()
|
||||||
.split(Regex("\\s+"))
|
.split(Regex("\\s+"))
|
||||||
|
|
||||||
val highestAddressIndex =
|
val preferences =
|
||||||
getSharedPreferences(PREFS, MODE_PRIVATE)
|
getSharedPreferences(PREFS, MODE_PRIVATE)
|
||||||
|
|
||||||
|
val highestAddressIndex =
|
||||||
|
preferences
|
||||||
.getInt(PREF_RECEIVE_ADDRESS_INDEX, 0)
|
.getInt(PREF_RECEIVE_ADDRESS_INDEX, 0)
|
||||||
.coerceAtLeast(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 =
|
val allTransactions =
|
||||||
mutableListOf<BlockScanner.WalletTransaction>()
|
mutableListOf<BlockScanner.WalletTransaction>()
|
||||||
|
|
||||||
|
val updatedUtxos =
|
||||||
|
linkedMapOf<Int, List<BlockScanner.SpendableUtxo>>()
|
||||||
|
|
||||||
for (addressIndex in 0..highestAddressIndex) {
|
for (addressIndex in 0..highestAddressIndex) {
|
||||||
val script =
|
val script =
|
||||||
deriveWitnessScript(words, addressIndex)
|
deriveWitnessScript(words, addressIndex)
|
||||||
|
|
@ -119,33 +178,52 @@ class WalletMonitoringService : Service() {
|
||||||
CompactFilterClient.scan(
|
CompactFilterClient.scan(
|
||||||
this,
|
this,
|
||||||
"87.106.99.23",
|
"87.106.99.23",
|
||||||
script
|
script,
|
||||||
|
startHeight = startHeight
|
||||||
) { _, _ -> }
|
) { _, _ -> }
|
||||||
|
|
||||||
|
val initialUtxos =
|
||||||
|
if (fullScanRequired) {
|
||||||
|
emptyList()
|
||||||
|
} else {
|
||||||
|
loadBackgroundUtxos(
|
||||||
|
preferences,
|
||||||
|
addressIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val blocks =
|
val blocks =
|
||||||
BlockScanner.scan(
|
BlockScanner.scan(
|
||||||
this,
|
this,
|
||||||
"87.106.99.23",
|
"87.106.99.23",
|
||||||
filters.matchingHeights,
|
filters.matchingHeights,
|
||||||
script,
|
script,
|
||||||
addressIndex
|
addressIndex,
|
||||||
|
initialUtxos = initialUtxos
|
||||||
) { _, _ -> }
|
) { _, _ -> }
|
||||||
|
|
||||||
allTransactions.addAll(blocks.transactions)
|
allTransactions.addAll(blocks.transactions)
|
||||||
|
|
||||||
|
updatedUtxos[addressIndex] =
|
||||||
|
blocks.utxos
|
||||||
}
|
}
|
||||||
|
|
||||||
val mergedTransactions =
|
val mergedTransactions =
|
||||||
allTransactions
|
allTransactions
|
||||||
.groupBy { it.txid }
|
.groupBy { it.txid }
|
||||||
.map { (_, transactions) ->
|
.map { (_, transactions) ->
|
||||||
BlockScanner.WalletTransaction(
|
val newest =
|
||||||
txid = transactions.first().txid,
|
transactions.maxByOrNull {
|
||||||
height = transactions.maxOf {
|
|
||||||
it.height
|
it.height
|
||||||
},
|
} ?: transactions.first()
|
||||||
|
|
||||||
|
BlockScanner.WalletTransaction(
|
||||||
|
txid = newest.txid,
|
||||||
|
height = newest.height,
|
||||||
netSatoshis = transactions.sumOf {
|
netSatoshis = transactions.sumOf {
|
||||||
it.netSatoshis
|
it.netSatoshis
|
||||||
}
|
},
|
||||||
|
blockTime = newest.blockTime
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.filter { it.netSatoshis != 0L }
|
.filter { it.netSatoshis != 0L }
|
||||||
|
|
@ -153,6 +231,59 @@ class WalletMonitoringService : Service() {
|
||||||
updateIncomingTransactionsAndNotify(
|
updateIncomingTransactionsAndNotify(
|
||||||
mergedTransactions
|
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) {
|
} catch (_: Exception) {
|
||||||
} finally {
|
} finally {
|
||||||
scanRunning = false
|
scanRunning = false
|
||||||
|
|
@ -160,6 +291,47 @@ class WalletMonitoringService : Service() {
|
||||||
}.start()
|
}.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(
|
private fun updateIncomingTransactionsAndNotify(
|
||||||
transactions: List<BlockScanner.WalletTransaction>
|
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 {
|
private fun walletExists(): Boolean {
|
||||||
val prefs =
|
val prefs =
|
||||||
getSharedPreferences(PREFS, MODE_PRIVATE)
|
getSharedPreferences(PREFS, MODE_PRIVATE)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue