Release Vexta Wallet v0.6.7

This commit is contained in:
Vexta Project 2026-08-31 23:35:19 +00:00
commit 3e2a7150f9
5 changed files with 443 additions and 54 deletions

View file

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

View file

@ -28,7 +28,8 @@ object BlockScanner {
val outputIndex: Long, val outputIndex: Long,
val value: Long, val value: Long,
val height: Int, val height: Int,
val addressIndex: Int val addressIndex: Int,
val isCoinbase: Boolean
) )
data class WalletTransaction( data class WalletTransaction(
@ -56,13 +57,15 @@ object BlockScanner {
val outputIndex: Long, val outputIndex: Long,
val value: Long, val value: Long,
val height: Int, val height: Int,
val addressIndex: Int val addressIndex: Int,
val isCoinbase: Boolean
) )
private data class ParsedTransaction( private data class ParsedTransaction(
val txidWire: ByteArray, val txidWire: ByteArray,
val inputs: List<OutPoint>, val inputs: List<OutPoint>,
val outputs: List<TransactionOutput> val outputs: List<TransactionOutput>,
val isCoinbase: Boolean
) )
private data class OutPoint( private data class OutPoint(
@ -139,7 +142,8 @@ object BlockScanner {
outputIndex = existing.outputIndex, outputIndex = existing.outputIndex,
value = existing.value, value = existing.value,
height = existing.height, height = existing.height,
addressIndex = existing.addressIndex addressIndex = existing.addressIndex,
isCoinbase = existing.isCoinbase
) )
} }
var receivedTransactions = 0 var receivedTransactions = 0
@ -235,7 +239,8 @@ object BlockScanner {
transactionOutput.index.toLong(), transactionOutput.index.toLong(),
value = transactionOutput.value, value = transactionOutput.value,
height = height, height = height,
addressIndex = addressIndex addressIndex = addressIndex,
isCoinbase = transaction.isCoinbase
) )
receivedWalletValue += receivedWalletValue +=
@ -282,7 +287,8 @@ object BlockScanner {
outputIndex = utxo.outputIndex, outputIndex = utxo.outputIndex,
value = utxo.value, value = utxo.value,
height = utxo.height, height = utxo.height,
addressIndex = utxo.addressIndex addressIndex = utxo.addressIndex,
isCoinbase = utxo.isCoinbase
) )
} }
@ -461,10 +467,16 @@ object BlockScanner {
val lockTime = cursor.readBytes(4) val lockTime = cursor.readBytes(4)
stripped.write(lockTime) stripped.write(lockTime)
val isCoinbase =
inputs.size == 1 &&
inputs[0].index == 0xffffffffL &&
inputs[0].txidWire.all { it == 0.toByte() }
return ParsedTransaction( return ParsedTransaction(
txidWire = doubleSha256(stripped.toByteArray()), txidWire = doubleSha256(stripped.toByteArray()),
inputs = inputs, inputs = inputs,
outputs = outputs outputs = outputs,
isCoinbase = isCoinbase
) )
} }

View file

@ -48,9 +48,40 @@ object VextaTransactionSender {
val fee: Long val fee: Long
) )
private const val COINBASE_MATURITY = 100
private fun validateCoinbaseMaturity(
utxos: List<BlockScanner.SpendableUtxo>,
chainHeight: Int
) {
require(chainHeight >= 0) {
"Invalid blockchain height"
}
val immatureCoinbase =
utxos.firstOrNull { utxo ->
utxo.isCoinbase &&
(
chainHeight < utxo.height ||
chainHeight - utxo.height + 1 <
COINBASE_MATURITY
)
}
require(immatureCoinbase == null) {
"Immature mining reward cannot be spent before 100 confirmations"
}
}
fun calculateMaxSpend( fun calculateMaxSpend(
spendableUtxos: List<BlockScanner.SpendableUtxo> spendableUtxos: List<BlockScanner.SpendableUtxo>,
chainHeight: Int
): MaxSpend { ): MaxSpend {
validateCoinbaseMaturity(
spendableUtxos,
chainHeight
)
require(spendableUtxos.isNotEmpty()) { require(spendableUtxos.isNotEmpty()) {
"Wallet has no spendable outputs" "Wallet has no spendable outputs"
} }
@ -81,8 +112,13 @@ object VextaTransactionSender {
recipientAddress: String, recipientAddress: String,
amountSatoshis: Long, amountSatoshis: Long,
privateKeysByIndex: Map<Int, ECKey>, privateKeysByIndex: Map<Int, ECKey>,
changePubKeyHash: ByteArray changePubKeyHash: ByteArray,
chainHeight: Int
): CreatedTransaction { ): CreatedTransaction {
validateCoinbaseMaturity(
spendableUtxos,
chainHeight
)
require(amountSatoshis > 0L) { require(amountSatoshis > 0L) {
"Amount must be greater than zero" "Amount must be greater than zero"
} }
@ -105,7 +141,11 @@ object VextaTransactionSender {
.thenBy { it.outputIndex } .thenBy { it.outputIndex }
) )
val maxSpend = calculateMaxSpend(orderedUtxos) val maxSpend =
calculateMaxSpend(
orderedUtxos,
chainHeight
)
val sendingMaximum = val sendingMaximum =
amountSatoshis == maxSpend.amount amountSatoshis == maxSpend.amount

View file

@ -74,6 +74,8 @@ import javax.crypto.spec.GCMParameterSpec
class WalletActivity : FragmentActivity() { class WalletActivity : FragmentActivity() {
companion object { companion object {
const val COINBASE_MATURITY = 100
private const val PREFS = "vexta_wallet" private const val PREFS = "vexta_wallet"
private const val KEY_ALIAS = "vexta_wallet_seed_key" private const val KEY_ALIAS = "vexta_wallet_seed_key"
private const val PREF_CIPHERTEXT = "seed_ciphertext" private const val PREF_CIPHERTEXT = "seed_ciphertext"
@ -108,6 +110,8 @@ class WalletActivity : FragmentActivity() {
"background_transaction_history" "background_transaction_history"
private const val PREF_LAST_BACKGROUND_SCAN_HEIGHT = private const val PREF_LAST_BACKGROUND_SCAN_HEIGHT =
"last_background_scan_height" "last_background_scan_height"
private const val PREF_FIAT_CURRENCY =
"preferred_fiat_currency"
private const val CONTACT_PREFS = private const val CONTACT_PREFS =
"vexta_wallet_contacts" "vexta_wallet_contacts"
@ -144,6 +148,18 @@ class WalletActivity : FragmentActivity() {
emptyList<BlockScanner.SpendableUtxo>() emptyList<BlockScanner.SpendableUtxo>()
private var latestChainHeight = 0 private var latestChainHeight = 0
private fun spendableUtxos(
utxos: List<BlockScanner.SpendableUtxo>,
chainHeight: Int
): List<BlockScanner.SpendableUtxo> =
utxos.filter { utxo ->
!utxo.isCoinbase ||
(
chainHeight >= utxo.height &&
chainHeight - utxo.height + 1 >= COINBASE_MATURITY
)
}
private data class OutgoingTransactionRecord( private data class OutgoingTransactionRecord(
val txid: String, val txid: String,
val amountSatoshis: Long, val amountSatoshis: Long,
@ -174,7 +190,20 @@ class WalletActivity : FragmentActivity() {
val cachedTransactions = val cachedTransactions =
loadCachedWalletTransactions() loadCachedWalletTransactions()
latestSpendableUtxos = cachedUtxos latestChainHeight =
getSharedPreferences(PREFS, MODE_PRIVATE)
.getInt(
PREF_LAST_BACKGROUND_SCAN_HEIGHT,
0
)
.coerceAtLeast(0)
latestSpendableUtxos =
spendableUtxos(
cachedUtxos,
latestChainHeight
)
latestWalletTransactions = latestWalletTransactions =
cachedTransactions cachedTransactions
@ -1001,7 +1030,7 @@ class WalletActivity : FragmentActivity() {
try { try {
val parts = value.split("|") val parts = value.split("|")
if (parts.size != 5) { if (parts.size != 6) {
return@mapNotNull null return@mapNotNull null
} }
@ -1010,7 +1039,8 @@ class WalletActivity : FragmentActivity() {
outputIndex = parts[1].toLong(), outputIndex = parts[1].toLong(),
value = parts[2].toLong(), value = parts[2].toLong(),
height = parts[3].toInt(), height = parts[3].toInt(),
addressIndex = parts[4].toInt() addressIndex = parts[4].toInt(),
isCoinbase = parts[5].toBooleanStrict()
) )
} catch (_: Exception) { } catch (_: Exception) {
null null
@ -1112,6 +1142,109 @@ class WalletActivity : FragmentActivity() {
) > 0 ) > 0
} }
private fun preferredFiatCurrency(): String {
return getSharedPreferences(PREFS, MODE_PRIVATE)
.getString(
PREF_FIAT_CURRENCY,
"GBP"
)
?.takeIf {
it in setOf("GBP", "EUR", "USD")
}
?: "GBP"
}
private fun setPreferredFiatCurrency(
currency: String
) {
if (currency !in setOf("GBP", "EUR", "USD")) {
return
}
getSharedPreferences(PREFS, MODE_PRIVATE)
.edit()
.putString(
PREF_FIAT_CURRENCY,
currency
)
.apply()
}
private fun fiatSymbol(currency: String): String =
when (currency) {
"GBP" -> "£"
"EUR" -> "€"
"USD" -> "$"
else -> ""
}
private fun loadFiatValues(
balanceSatoshis: Long,
onLoaded: (Map<String, Double>) -> Unit
) {
Thread {
try {
val nestExJson =
org.json.JSONObject(
java.net.URL(
"https://trade.nestex.one/api/cg/tickers/VTX_USDT"
).readText()
)
val vtxUsdt =
nestExJson.getString("last_price")
.toDouble()
val fxJson =
org.json.JSONObject(
java.net.URL(
"https://api.frankfurter.app/latest?from=USD&to=GBP,EUR"
).readText()
)
val rates =
fxJson.getJSONObject("rates")
val gbpRate =
rates.getDouble("GBP")
val eurRate =
rates.getDouble("EUR")
val vtxBalance =
balanceSatoshis.toDouble() /
100_000_000.0
val usdValue =
vtxBalance * vtxUsdt
val values =
mapOf(
"USD" to usdValue,
"GBP" to usdValue * gbpRate,
"EUR" to usdValue * eurRate
)
runOnUiThread {
onLoaded(values)
}
} catch (_: Exception) {
}
}.start()
}
private fun formatFiatValue(
currency: String,
value: Double
): String {
return String.format(
java.util.Locale.getDefault(),
"%s%,.2f",
fiatSymbol(currency),
value
)
}
private fun currentReceiveAddressIndex(): Int { private fun currentReceiveAddressIndex(): Int {
return getSharedPreferences(PREFS, MODE_PRIVATE) return getSharedPreferences(PREFS, MODE_PRIVATE)
.getInt(PREF_RECEIVE_ADDRESS_INDEX, 0) .getInt(PREF_RECEIVE_ADDRESS_INDEX, 0)
@ -1349,7 +1482,12 @@ class WalletActivity : FragmentActivity() {
} }
runOnUiThread { runOnUiThread {
latestSpendableUtxos = mergedUtxos latestSpendableUtxos =
spendableUtxos(
mergedUtxos,
latestChainHeight
)
latestWalletTransactions = latestWalletTransactions =
mergedTransactions mergedTransactions
@ -2014,7 +2152,12 @@ class WalletActivity : FragmentActivity() {
val cachedTransactions = val cachedTransactions =
loadCachedWalletTransactions() loadCachedWalletTransactions()
latestSpendableUtxos = cachedUtxos latestSpendableUtxos =
spendableUtxos(
cachedUtxos,
latestChainHeight
)
latestWalletTransactions = cachedTransactions latestWalletTransactions = cachedTransactions
val cachedBalance = val cachedBalance =
@ -2075,6 +2218,149 @@ class WalletActivity : FragmentActivity() {
) )
addView(space(12)) addView(space(12))
addView(balanceView) addView(balanceView)
addView(space(10))
val preferredCurrency =
preferredFiatCurrency()
val alternativeCurrencies =
listOf("GBP", "EUR", "USD")
.filter {
it != preferredCurrency
}
var fiatValues =
emptyMap<String, Double>()
val alternativesContainer =
LinearLayout(
this@WalletActivity
).apply {
orientation =
LinearLayout.VERTICAL
gravity = Gravity.CENTER
visibility = View.GONE
}
val alternativeViews =
mutableMapOf<String, TextView>()
fun preferredFiatText(
expanded: Boolean
): String {
val value =
fiatValues[preferredCurrency]
val formatted =
if (value != null) {
formatFiatValue(
preferredCurrency,
value
)
} else {
"${fiatSymbol(preferredCurrency)}—"
}
return "≈ $formatted " +
if (expanded) "▴" else "▾"
}
val preferredFiatView =
TextView(
this@WalletActivity
).apply {
text = preferredFiatText(false)
textSize = 16f
setTextColor(textPrimary)
gravity = Gravity.CENTER
typeface =
Typeface.DEFAULT_BOLD
isClickable = true
isFocusable = true
setOnClickListener {
val expanding =
alternativesContainer.visibility !=
View.VISIBLE
alternativesContainer.visibility =
if (expanding) {
View.VISIBLE
} else {
View.GONE
}
text =
preferredFiatText(expanding)
}
}
addView(preferredFiatView)
alternativeCurrencies.forEach {
currency ->
val fiatView =
TextView(
this@WalletActivity
).apply {
text =
"${fiatSymbol(currency)}—"
textSize = 15f
setTextColor(textSecondary)
gravity = Gravity.CENTER
setPadding(
0,
dp(8),
0,
dp(8)
)
isClickable = true
isFocusable = true
setOnClickListener {
setPreferredFiatCurrency(
currency
)
showMainWallet()
}
}
alternativeViews[currency] =
fiatView
alternativesContainer.addView(
fiatView
)
}
addView(alternativesContainer)
loadFiatValues(
cachedBalance
) { values ->
fiatValues = values
preferredFiatView.text =
preferredFiatText(
alternativesContainer.visibility ==
View.VISIBLE
)
alternativeViews.forEach {
(currency, view) ->
values[currency]?.let {
value ->
view.text =
formatFiatValue(
currency,
value
)
}
}
}
addView(space(8)) addView(space(8))
addView( addView(
TextView(this@WalletActivity).apply { TextView(this@WalletActivity).apply {
@ -2824,7 +3110,7 @@ class WalletActivity : FragmentActivity() {
val nameInput = val nameInput =
inputField( inputField(
hint = "Contact name", hint = "Saved address name",
singleLine = true singleLine = true
).apply { ).apply {
setText(existing?.name ?: "") setText(existing?.name ?: "")
@ -2846,9 +3132,9 @@ class WalletActivity : FragmentActivity() {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setTitle( .setTitle(
if (existing == null) { if (existing == null) {
"Add contact" "Add saved address"
} else { } else {
"Edit contact" "Edit saved address"
} }
) )
.setView(content) .setView(content)
@ -2873,7 +3159,7 @@ class WalletActivity : FragmentActivity() {
if (name.isBlank()) { if (name.isBlank()) {
Toast.makeText( Toast.makeText(
this, this,
"Enter a contact name", "Enter a name for this saved address",
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
@ -2918,7 +3204,7 @@ class WalletActivity : FragmentActivity() {
if (duplicate) { if (duplicate) {
Toast.makeText( Toast.makeText(
this, this,
"This address is already in Contacts", "This address is already saved",
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
@ -2959,9 +3245,9 @@ class WalletActivity : FragmentActivity() {
Toast.makeText( Toast.makeText(
this, this,
if (existing == null) { if (existing == null) {
"Contact added" "Saved address added"
} else { } else {
"Contact updated" "Saved address updated"
}, },
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
@ -2982,9 +3268,9 @@ class WalletActivity : FragmentActivity() {
contact: WalletContact contact: WalletContact
) { ) {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setTitle("Delete contact") .setTitle("Delete saved address")
.setMessage( .setMessage(
"Delete ${contact.name} from Contacts?" "Delete ${contact.name} from Saved addresses?"
) )
.setNegativeButton("Cancel", null) .setNegativeButton("Cancel", null)
.setPositiveButton("Delete") { _, _ -> .setPositiveButton("Delete") { _, _ ->
@ -2998,7 +3284,7 @@ class WalletActivity : FragmentActivity() {
Toast.makeText( Toast.makeText(
this, this,
"Contact deleted", "Saved address deleted",
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
@ -3071,7 +3357,7 @@ class WalletActivity : FragmentActivity() {
content.addView(space(22)) content.addView(space(22))
content.addView( content.addView(
primaryButton("Edit contact") { primaryButton("Edit saved address") {
showContactEditor( showContactEditor(
recipientInput, recipientInput,
contact contact
@ -3082,7 +3368,7 @@ class WalletActivity : FragmentActivity() {
content.addView(space(12)) content.addView(space(12))
content.addView( content.addView(
primaryButton("Delete contact") { primaryButton("Delete saved address") {
deleteContact( deleteContact(
recipientInput, recipientInput,
contact contact
@ -3118,7 +3404,7 @@ class WalletActivity : FragmentActivity() {
if (contacts.isEmpty()) { if (contacts.isEmpty()) {
content.addView( content.addView(
smallStatus( smallStatus(
"No contacts saved yet." "No saved addresses yet."
) )
) )
} else { } else {
@ -3149,7 +3435,7 @@ class WalletActivity : FragmentActivity() {
content.addView(space(16)) content.addView(space(16))
content.addView( content.addView(
primaryButton("Add contact") { primaryButton("Add saved address") {
contactsDialog.dismiss() contactsDialog.dismiss()
showContactEditor( showContactEditor(
@ -3165,7 +3451,7 @@ class WalletActivity : FragmentActivity() {
contactsDialog = contactsDialog =
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setTitle("Contacts") .setTitle("Saved addresses")
.setView(scroll) .setView(scroll)
.setNegativeButton("Close", null) .setNegativeButton("Close", null)
.create() .create()
@ -3252,7 +3538,7 @@ class WalletActivity : FragmentActivity() {
addView(space(10)) addView(space(10))
addView( addView(
secondaryButton("Contacts") { secondaryButton("Saved addresses") {
showContactsDialog( showContactsDialog(
recipientInput recipientInput
) )
@ -3286,7 +3572,8 @@ class WalletActivity : FragmentActivity() {
try { try {
val maxSpend = val maxSpend =
VextaTransactionSender.calculateMaxSpend( VextaTransactionSender.calculateMaxSpend(
latestSpendableUtxos latestSpendableUtxos,
latestChainHeight
) )
amountInput.setText( amountInput.setText(
@ -3433,7 +3720,9 @@ class WalletActivity : FragmentActivity() {
privateKeysByIndex = privateKeysByIndex =
privateKeysByIndex, privateKeysByIndex,
changePubKeyHash = changePubKeyHash =
changePubKeyHash changePubKeyHash,
chainHeight =
latestChainHeight
) )
val amountDisplay = String.format( val amountDisplay = String.format(

View file

@ -49,7 +49,7 @@ class WalletMonitoringService : Service() {
"background_transaction_history" "background_transaction_history"
private const val PREF_BACKGROUND_TX_HISTORY_VERSION = private const val PREF_BACKGROUND_TX_HISTORY_VERSION =
"background_transaction_history_version" "background_transaction_history_version"
private const val BACKGROUND_TX_HISTORY_VERSION = 1 private const val BACKGROUND_TX_HISTORY_VERSION = 2
} }
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
@ -210,6 +210,9 @@ class WalletMonitoringService : Service() {
val mergedTransactions = val mergedTransactions =
allTransactions allTransactions
.distinctBy {
"${it.txid}:${it.height}:${it.netSatoshis}:${it.blockTime}"
}
.groupBy { it.txid } .groupBy { it.txid }
.map { (_, transactions) -> .map { (_, transactions) ->
val newest = val newest =
@ -228,9 +231,15 @@ class WalletMonitoringService : Service() {
} }
.filter { it.netSatoshis != 0L } .filter { it.netSatoshis != 0L }
if (fullScanRequired) {
synchronizeKnownIncomingTransactions(
mergedTransactions
)
} else {
updateIncomingTransactionsAndNotify( updateIncomingTransactionsAndNotify(
mergedTransactions mergedTransactions
) )
}
val cachedTransactions = val cachedTransactions =
if (fullScanRequired) { if (fullScanRequired) {
@ -332,6 +341,36 @@ class WalletMonitoringService : Service() {
} }
} }
private fun synchronizeKnownIncomingTransactions(
transactions: List<BlockScanner.WalletTransaction>
) {
val preferences =
getSharedPreferences(PREFS, MODE_PRIVATE)
val knownTxids =
preferences.getStringSet(
PREF_KNOWN_INCOMING_TXIDS,
emptySet()
)?.toMutableSet() ?: mutableSetOf()
knownTxids.addAll(
transactions
.filter { it.netSatoshis > 0L }
.map { it.txid }
)
preferences.edit()
.putBoolean(
PREF_TX_NOTIFICATIONS_INITIALIZED,
true
)
.putStringSet(
PREF_KNOWN_INCOMING_TXIDS,
knownTxids
)
.apply()
}
private fun updateIncomingTransactionsAndNotify( private fun updateIncomingTransactionsAndNotify(
transactions: List<BlockScanner.WalletTransaction> transactions: List<BlockScanner.WalletTransaction>
) { ) {
@ -371,25 +410,32 @@ class WalletMonitoringService : Service() {
)?.toMutableSet() ?: mutableSetOf() )?.toMutableSet() ?: mutableSetOf()
incoming incoming
.distinctBy { it.txid }
.filter { it.txid !in knownTxids } .filter { it.txid !in knownTxids }
.sortedBy { it.height } .sortedBy { it.height }
.forEach { transaction -> .forEach { transaction ->
showIncomingPaymentNotification(
transaction.netSatoshis
)
knownTxids.add(transaction.txid) knownTxids.add(transaction.txid)
}
val saved =
preferences.edit() preferences.edit()
.putStringSet( .putStringSet(
PREF_KNOWN_INCOMING_TXIDS, PREF_KNOWN_INCOMING_TXIDS,
knownTxids knownTxids.toSet()
) )
.apply() .commit()
if (saved) {
showIncomingPaymentNotification(
transaction.netSatoshis,
transaction.txid
)
}
}
} }
private fun showIncomingPaymentNotification( private fun showIncomingPaymentNotification(
receivedSatoshis: Long receivedSatoshis: Long,
txid: String
) { ) {
if ( if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
@ -446,7 +492,7 @@ class WalletMonitoringService : Service() {
) as NotificationManager ) as NotificationManager
manager.notify( manager.notify(
(System.currentTimeMillis() and 0x7fffffff).toInt(), txid.hashCode(),
notification notification
) )
} }
@ -460,7 +506,8 @@ class WalletMonitoringService : Service() {
utxo.outputIndex.toString(), utxo.outputIndex.toString(),
utxo.value.toString(), utxo.value.toString(),
utxo.height.toString(), utxo.height.toString(),
utxo.addressIndex.toString() utxo.addressIndex.toString(),
utxo.isCoinbase.toString()
).joinToString("|") ).joinToString("|")
}.toSet() }.toSet()
@ -478,7 +525,7 @@ class WalletMonitoringService : Service() {
try { try {
val parts = value.split("|") val parts = value.split("|")
if (parts.size != 5) { if (parts.size != 6) {
return@mapNotNull null return@mapNotNull null
} }
@ -487,7 +534,8 @@ class WalletMonitoringService : Service() {
outputIndex = parts[1].toLong(), outputIndex = parts[1].toLong(),
value = parts[2].toLong(), value = parts[2].toLong(),
height = parts[3].toInt(), height = parts[3].toInt(),
addressIndex = parts[4].toInt() addressIndex = parts[4].toInt(),
isCoinbase = parts[5].toBooleanStrict()
) )
} catch (_: Exception) { } catch (_: Exception) {
null null