Release Vexta Wallet v0.6.8 hotfix

This commit is contained in:
Vexta Project 2026-09-01 09:00:40 +00:00
commit 45d9b111fb
6 changed files with 348 additions and 57 deletions

View file

@ -37,6 +37,7 @@ object BlockScanner {
val height: Int,
val netSatoshis: Long,
val blockTime: Long,
val isCoinbase: Boolean = false,
val isPending: Boolean = false,
val feeSatoshis: Long? = null,
val recipientAddress: String? = null
@ -263,7 +264,8 @@ object BlockScanner {
.toHex(),
height = height,
netSatoshis = netSatoshis,
blockTime = blockTime
blockTime = blockTime,
isCoinbase = transaction.isCoinbase
)
)
}

View file

@ -136,6 +136,7 @@ object HeaderSync {
var versionInfo: VersionInfo? = null
var gotVerack = false
var requested = false
var totalReceived = 0
val cachedBefore = chain.last().height
repeat(200) {
@ -153,6 +154,8 @@ object HeaderSync {
"headers" -> {
val received = parseHeaders(message.second, chain)
totalReceived += received
val remote = versionInfo
?: throw IllegalStateException("missing node version")
@ -162,8 +165,25 @@ object HeaderSync {
)
}
if (chain.last().height < remote.height) {
if (received == 0) {
throw IllegalStateException(
"peer advertises height ${remote.height} " +
"but returned no additional headers"
)
}
sendMessage(
output,
"getheaders",
createGetHeadersPayload(chain)
)
return@repeat
}
return SyncResult(
received = received,
received = totalReceived,
cachedBefore = cachedBefore,
chainHeight = chain.last().height,
lastHash = chain.last().hashDisplay
@ -175,7 +195,7 @@ object HeaderSync {
sendMessage(
output,
"getheaders",
createGetHeadersPayload(chain.last().hashWire)
createGetHeadersPayload(chain)
)
requested = true
}
@ -201,6 +221,8 @@ object HeaderSync {
)
}
val receivedHeaders = mutableListOf<ByteArray>()
repeat(count) {
if (buffer.remaining() < 81) {
throw IllegalStateException("truncated headers message")
@ -217,7 +239,33 @@ object HeaderSync {
)
}
appendVerifiedHeader(chain, raw)
receivedHeaders.add(raw)
}
if (receivedHeaders.isNotEmpty()) {
val firstPreviousHash =
receivedHeaders.first().copyOfRange(4, 36)
if (!firstPreviousHash.contentEquals(chain.last().hashWire)) {
val ancestorIndex =
chain.indexOfLast {
it.hashWire.contentEquals(firstPreviousHash)
}
if (ancestorIndex < 0) {
throw IllegalStateException(
"peer returned headers with no common ancestor"
)
}
while (chain.lastIndex > ancestorIndex) {
chain.removeAt(chain.lastIndex)
}
}
receivedHeaders.forEach { raw ->
appendVerifiedHeader(chain, raw)
}
}
return count
@ -375,12 +423,38 @@ object HeaderSync {
return compact
}
fun createGetHeadersPayload(locatorHash: ByteArray): ByteArray {
fun createGetHeadersPayload(chain: List<ChainHeader>): ByteArray {
val out = ByteArrayOutputStream()
val locator = mutableListOf<ByteArray>()
var index = chain.lastIndex
var step = 1
while (index >= 0) {
locator.add(chain[index].hashWire)
if (index == 0) {
break
}
index = (index - step).coerceAtLeast(0)
if (locator.size >= 10) {
step *= 2
}
}
if (!locator.last().contentEquals(chain.first().hashWire)) {
locator.add(chain.first().hashWire)
}
writeInt32LE(out, PROTOCOL_VERSION)
writeCompactSize(out, 1)
out.write(locatorHash)
writeCompactSize(out, locator.size)
locator.forEach { hash ->
out.write(hash)
}
out.write(ByteArray(32))
return out.toByteArray()

View file

@ -119,6 +119,7 @@ class MainActivity : Activity() {
)
val results = mutableListOf<String>()
var successfulPeerSyncs = 0
for ((index, peer) in peers.withIndex()) {
try {
@ -128,6 +129,8 @@ class MainActivity : Activity() {
saveChain(chain)
}
successfulPeerSyncs++
results.add(
"$peer:$PORT\n" +
"Handshake: successful\n" +
@ -148,7 +151,10 @@ class MainActivity : Activity() {
runOnUiThread {
if (intent.getBooleanExtra("sync_only", false)) {
if (chain.last().height > 0) {
if (
successfulPeerSyncs > 0 &&
chain.last().height > 0
) {
setResult(
RESULT_OK,
android.content.Intent().putExtra(
@ -271,6 +277,7 @@ class MainActivity : Activity() {
var versionInfo: VersionInfo? = null
var gotVerack = false
var requested = false
var totalReceived = 0
val cachedBefore = chain.last().height
repeat(200) {
@ -288,6 +295,8 @@ class MainActivity : Activity() {
"headers" -> {
val received = parseHeaders(message.second, chain)
totalReceived += received
val remote = versionInfo
?: throw IllegalStateException("missing node version")
@ -297,8 +306,25 @@ class MainActivity : Activity() {
)
}
if (chain.last().height < remote.height) {
if (received == 0) {
throw IllegalStateException(
"peer advertises height ${remote.height} " +
"but returned no additional headers"
)
}
sendMessage(
output,
"getheaders",
createGetHeadersPayload(chain)
)
return@repeat
}
return SyncResult(
received = received,
received = totalReceived,
cachedBefore = cachedBefore,
chainHeight = chain.last().height,
lastHash = chain.last().hashDisplay
@ -310,7 +336,7 @@ class MainActivity : Activity() {
sendMessage(
output,
"getheaders",
createGetHeadersPayload(chain.last().hashWire)
createGetHeadersPayload(chain)
)
requested = true
}
@ -336,6 +362,8 @@ class MainActivity : Activity() {
)
}
val receivedHeaders = mutableListOf<ByteArray>()
repeat(count) {
if (buffer.remaining() < 81) {
throw IllegalStateException("truncated headers message")
@ -352,7 +380,33 @@ class MainActivity : Activity() {
)
}
appendVerifiedHeader(chain, raw)
receivedHeaders.add(raw)
}
if (receivedHeaders.isNotEmpty()) {
val firstPreviousHash =
receivedHeaders.first().copyOfRange(4, 36)
if (!firstPreviousHash.contentEquals(chain.last().hashWire)) {
val ancestorIndex =
chain.indexOfLast {
it.hashWire.contentEquals(firstPreviousHash)
}
if (ancestorIndex < 0) {
throw IllegalStateException(
"peer returned headers with no common ancestor"
)
}
while (chain.lastIndex > ancestorIndex) {
chain.removeAt(chain.lastIndex)
}
}
receivedHeaders.forEach { raw ->
appendVerifiedHeader(chain, raw)
}
}
return count
@ -510,12 +564,38 @@ class MainActivity : Activity() {
return compact
}
private fun createGetHeadersPayload(locatorHash: ByteArray): ByteArray {
private fun createGetHeadersPayload(chain: List<ChainHeader>): ByteArray {
val out = ByteArrayOutputStream()
val locator = mutableListOf<ByteArray>()
var index = chain.lastIndex
var step = 1
while (index >= 0) {
locator.add(chain[index].hashWire)
if (index == 0) {
break
}
index = (index - step).coerceAtLeast(0)
if (locator.size >= 10) {
step *= 2
}
}
if (!locator.last().contentEquals(chain.first().hashWire)) {
locator.add(chain.first().hashWire)
}
writeInt32LE(out, PROTOCOL_VERSION)
writeCompactSize(out, 1)
out.write(locatorHash)
writeCompactSize(out, locator.size)
locator.forEach { hash ->
out.write(hash)
}
out.write(ByteArray(32))
return out.toByteArray()

View file

@ -110,6 +110,11 @@ class WalletActivity : FragmentActivity() {
"background_transaction_history"
private const val PREF_LAST_BACKGROUND_SCAN_HEIGHT =
"last_background_scan_height"
private const val PREF_LAST_HEADER_HEIGHT =
"last_header_height"
private const val PREF_BACKGROUND_TX_HISTORY_VERSION =
"background_transaction_history_version"
private const val BACKGROUND_TX_HISTORY_VERSION = 4
private const val PREF_FIAT_CURRENCY =
"preferred_fiat_currency"
@ -147,6 +152,7 @@ class WalletActivity : FragmentActivity() {
private var latestSpendableUtxos =
emptyList<BlockScanner.SpendableUtxo>()
private var latestChainHeight = 0
private var walletFiatRefresh: ((Long) -> Unit)? = null
private fun spendableUtxos(
utxos: List<BlockScanner.SpendableUtxo>,
@ -193,7 +199,7 @@ class WalletActivity : FragmentActivity() {
latestChainHeight =
getSharedPreferences(PREFS, MODE_PRIVATE)
.getInt(
PREF_LAST_BACKGROUND_SCAN_HEIGHT,
PREF_LAST_HEADER_HEIGHT,
0
)
.coerceAtLeast(0)
@ -217,6 +223,10 @@ class WalletActivity : FragmentActivity() {
100_000_000.0
)
walletFiatRefresh?.invoke(
balanceSatoshis
)
walletHistoryView?.text =
formatTransactionHistory(
cachedTransactions,
@ -429,7 +439,7 @@ class WalletActivity : FragmentActivity() {
mainWalletVisible &&
!blockchainScanRunning
) {
startHeaderSyncAndScan()
startBackgroundHeaderSyncAndScan()
}
}, 400L)
}
@ -870,12 +880,21 @@ class WalletActivity : FragmentActivity() {
}
blockchainScanRunning = true
blockchainScanStatus?.text =
"Synchronizing block headers..."
Thread {
try {
val chain = HeaderSync.loadCachedChain(this)
runOnUiThread {
blockchainScanStatus?.text =
"Synchronizing block headers...\n" +
"Cached height: ${chain.last().height}"
}
val syncErrors = mutableListOf<String>()
var successfulPeerSyncs = 0
for (peer in listOf("87.106.99.23", "74.208.53.160")) {
try {
@ -884,6 +903,14 @@ class WalletActivity : FragmentActivity() {
if (result.received > 0) {
HeaderSync.saveChain(this, chain)
}
successfulPeerSyncs++
runOnUiThread {
blockchainScanStatus?.text =
"Header sync OK: ${result.chainHeight}\n" +
"Peer: $peer"
}
} catch (e: Exception) {
syncErrors.add(
"$peer: ${e.message ?: e.javaClass.simpleName}"
@ -891,7 +918,10 @@ class WalletActivity : FragmentActivity() {
}
}
if (chain.last().height <= 0) {
if (
successfulPeerSyncs == 0 ||
chain.last().height <= 0
) {
runOnUiThread {
blockchainScanRunning = false
blockchainScanStatus?.text =
@ -904,9 +934,12 @@ class WalletActivity : FragmentActivity() {
runOnUiThread {
runBlockchainScan()
}
} catch (_: Exception) {
} catch (e: Exception) {
runOnUiThread {
blockchainScanRunning = false
blockchainScanStatus?.text =
"Background synchronization failed:\n" +
(e.message ?: e.javaClass.simpleName)
}
}
}.start()
@ -947,6 +980,14 @@ class WalletActivity : FragmentActivity() {
-1
) ?: -1
if (height >= 0) {
latestChainHeight = height
getSharedPreferences(PREFS, MODE_PRIVATE)
.edit()
.putInt(PREF_LAST_HEADER_HEIGHT, height)
.apply()
}
blockchainScanStatus?.text = if (height >= 0) {
"Headers synchronized to block $height.\n" +
"Scanning compact filters..."
@ -1073,7 +1114,7 @@ class WalletActivity : FragmentActivity() {
try {
val parts = value.split("|")
if (parts.size != 4) {
if (parts.size != 5) {
return@mapNotNull null
}
@ -1090,6 +1131,7 @@ class WalletActivity : FragmentActivity() {
parts[2].toLong()
},
blockTime = parts[3].toLong(),
isCoinbase = parts[4].toBooleanStrict(),
feeSatoshis = outgoing?.feeSatoshis,
recipientAddress =
outgoing?.recipientAddress
@ -1135,11 +1177,23 @@ class WalletActivity : FragmentActivity() {
}
private fun hasCachedWalletState(): Boolean {
return getSharedPreferences(PREFS, MODE_PRIVATE)
.getInt(
val preferences =
getSharedPreferences(PREFS, MODE_PRIVATE)
val scanHeight =
preferences.getInt(
PREF_LAST_BACKGROUND_SCAN_HEIGHT,
0
) > 0
)
val cacheVersion =
preferences.getInt(
PREF_BACKGROUND_TX_HISTORY_VERSION,
0
)
return scanHeight > 0 &&
cacheVersion == BACKGROUND_TX_HISTORY_VERSION
}
private fun preferredFiatCurrency(): String {
@ -1395,6 +1449,9 @@ class WalletActivity : FragmentActivity() {
}
},
blockTime = newest.blockTime,
isCoinbase = transactions.any {
it.isCoinbase
},
feeSatoshis =
outgoing?.feeSatoshis,
recipientAddress =
@ -1586,8 +1643,22 @@ class WalletActivity : FragmentActivity() {
1
).coerceAtLeast(1)
"Block ${transaction.height}\n" +
"Confirmations: $confirmations"
if (transaction.isCoinbase) {
val maturityStatus =
if (confirmations >= COINBASE_MATURITY) {
"Coinbase reward — Mature"
} else {
"Coinbase reward — Immature"
}
"Block ${transaction.height}\n" +
"$maturityStatus\n" +
"Confirmations: $confirmations / " +
"$COINBASE_MATURITY"
} else {
"Block ${transaction.height}\n" +
"Confirmations: $confirmations"
}
}
"$direction $sign$amount VTX\n" +
@ -1637,7 +1708,14 @@ class WalletActivity : FragmentActivity() {
val details = StringBuilder()
details.append("Type: $direction\n")
details.append(
"Type: " +
if (transaction.isCoinbase) {
"Coinbase reward\n"
} else {
"$direction\n"
}
)
details.append("Amount: $amount VTX\n")
details.append("Date & time: $dateTime\n")
@ -1645,9 +1723,30 @@ class WalletActivity : FragmentActivity() {
details.append("Status: Pending\n")
details.append("Confirmations: 0\n")
} else {
details.append("Status: Confirmed\n")
details.append("Block: ${transaction.height}\n")
details.append("Confirmations: $confirmations\n")
if (transaction.isCoinbase) {
val mature =
confirmations >= COINBASE_MATURITY
details.append(
"Status: " +
if (mature) {
"Mature\n"
} else {
"Immature\n"
}
)
details.append("Block: ${transaction.height}\n")
details.append(
"Confirmations: $confirmations / " +
"$COINBASE_MATURITY\n"
)
} else {
details.append("Status: Confirmed\n")
details.append("Block: ${transaction.height}\n")
details.append(
"Confirmations: $confirmations\n"
)
}
}
transaction.feeSatoshis?.let { fee ->
@ -2144,7 +2243,7 @@ class WalletActivity : FragmentActivity() {
latestChainHeight =
getSharedPreferences(PREFS, MODE_PRIVATE)
.getInt(
PREF_LAST_BACKGROUND_SCAN_HEIGHT,
PREF_LAST_HEADER_HEIGHT,
0
)
.coerceAtLeast(0)
@ -2337,30 +2436,37 @@ class WalletActivity : FragmentActivity() {
addView(alternativesContainer)
loadFiatValues(
cachedBalance
) { values ->
fiatValues = values
walletFiatRefresh = {
balanceSatoshis ->
loadFiatValues(
balanceSatoshis
) { values ->
fiatValues = values
preferredFiatView.text =
preferredFiatText(
alternativesContainer.visibility ==
View.VISIBLE
)
preferredFiatView.text =
preferredFiatText(
alternativesContainer.visibility ==
View.VISIBLE
)
alternativeViews.forEach {
(currency, view) ->
values[currency]?.let {
value ->
view.text =
formatFiatValue(
currency,
value
)
alternativeViews.forEach {
(currency, view) ->
values[currency]?.let {
value ->
view.text =
formatFiatValue(
currency,
value
)
}
}
}
}
walletFiatRefresh?.invoke(
cachedBalance
)
addView(space(8))
addView(
TextView(this@WalletActivity).apply {
@ -2422,7 +2528,7 @@ class WalletActivity : FragmentActivity() {
symbol = "↻",
label = "Scan"
) {
startHeaderSyncAndScan()
startBackgroundHeaderSyncAndScan()
}
)
@ -2527,7 +2633,7 @@ class WalletActivity : FragmentActivity() {
mainWalletVisible &&
!blockchainScanRunning
) {
startHeaderSyncAndScan()
startBackgroundHeaderSyncAndScan()
}
}, 500L)
}

View file

@ -43,13 +43,15 @@ class WalletMonitoringService : Service() {
"tx_notifications_initialized"
private const val PREF_LAST_BACKGROUND_SCAN_HEIGHT =
"last_background_scan_height"
private const val PREF_LAST_HEADER_HEIGHT =
"last_header_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 = 2
private const val BACKGROUND_TX_HISTORY_VERSION = 4
}
private val handler = Handler(Looper.getMainLooper())
@ -97,10 +99,24 @@ class WalletMonitoringService : Service() {
try {
val chain = HeaderSync.loadCachedChain(this)
val cachedTipHeightBeforeSync = chain.last().height
val cachedTipHashBeforeSync = chain.last().hashDisplay
var reorgDetected = false
for (peer in listOf("87.106.99.23", "74.208.53.160")) {
try {
val result = HeaderSync.syncFromPeer(peer, chain)
if (cachedTipHeightBeforeSync > 0) {
val originalTipStillCanonical =
chain.getOrNull(cachedTipHeightBeforeSync)
?.hashDisplay == cachedTipHashBeforeSync
if (!originalTipStillCanonical) {
reorgDetected = true
}
}
if (result.received > 0) {
HeaderSync.saveChain(this, chain)
}
@ -122,6 +138,13 @@ class WalletMonitoringService : Service() {
val localHeight = chain.last().height
preferences.edit()
.putInt(
PREF_LAST_HEADER_HEIGHT,
localHeight
)
.apply()
val previousHeight =
preferences
.getInt(
@ -145,7 +168,8 @@ class WalletMonitoringService : Service() {
) == BACKGROUND_TX_HISTORY_VERSION
val fullScanRequired =
previousHeight <= 0 ||
reorgDetected ||
previousHeight <= 0 ||
previousHeight > localHeight ||
!backgroundCacheAvailable ||
!transactionHistoryCurrent
@ -226,7 +250,10 @@ class WalletMonitoringService : Service() {
netSatoshis = transactions.sumOf {
it.netSatoshis
},
blockTime = newest.blockTime
blockTime = newest.blockTime,
isCoinbase = transactions.any {
it.isCoinbase
}
)
}
.filter { it.netSatoshis != 0L }
@ -308,7 +335,8 @@ class WalletMonitoringService : Service() {
transaction.txid,
transaction.height.toString(),
transaction.netSatoshis.toString(),
transaction.blockTime.toString()
transaction.blockTime.toString(),
transaction.isCoinbase.toString()
).joinToString("|")
}.toSet()
@ -325,7 +353,7 @@ class WalletMonitoringService : Service() {
try {
val parts = value.split("|")
if (parts.size != 4) {
if (parts.size != 5) {
return@mapNotNull null
}
@ -333,7 +361,8 @@ class WalletMonitoringService : Service() {
txid = parts[0],
height = parts[1].toInt(),
netSatoshis = parts[2].toLong(),
blockTime = parts[3].toLong()
blockTime = parts[3].toLong(),
isCoinbase = parts[4].toBooleanStrict()
)
} catch (_: Exception) {
null