Release Vexta Wallet v0.6.7
This commit is contained in:
parent
e1f63b1727
commit
3e2a7150f9
5 changed files with 443 additions and 54 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
||||||
updateIncomingTransactionsAndNotify(
|
if (fullScanRequired) {
|
||||||
mergedTransactions
|
synchronizeKnownIncomingTransactions(
|
||||||
)
|
mergedTransactions
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
updateIncomingTransactionsAndNotify(
|
||||||
|
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)
|
||||||
}
|
|
||||||
|
|
||||||
preferences.edit()
|
val saved =
|
||||||
.putStringSet(
|
preferences.edit()
|
||||||
PREF_KNOWN_INCOMING_TXIDS,
|
.putStringSet(
|
||||||
knownTxids
|
PREF_KNOWN_INCOMING_TXIDS,
|
||||||
)
|
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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue