Release Vexta Wallet v0.6.3
This commit is contained in:
parent
339b2eed60
commit
207657fe13
3 changed files with 377 additions and 25 deletions
|
|
@ -22,8 +22,8 @@ android {
|
|||
applicationId = "org.vextaproject.wallet"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 32
|
||||
versionName = "0.6.2"
|
||||
versionCode = 33
|
||||
versionName = "0.6.3"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
|
|
|||
|
|
@ -1044,7 +1044,7 @@ class WalletActivity : FragmentActivity() {
|
|||
100_000_000.0
|
||||
)
|
||||
|
||||
updateIncomingTransactionsAndNotify(mergedTransactions)
|
||||
// Incoming notifications are handled by WalletMonitoringService.
|
||||
|
||||
if (restoreAddressDiscovery) {
|
||||
setCurrentReceiveAddressIndex(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package org.vextaproject.wallet
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
|
|
@ -7,48 +8,58 @@ import android.app.PendingIntent
|
|||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import org.bitcoinj.core.ECKey
|
||||
import org.bitcoinj.core.Utils
|
||||
import org.bitcoinj.crypto.ChildNumber
|
||||
import org.bitcoinj.crypto.DeterministicHierarchy
|
||||
import org.bitcoinj.crypto.HDKeyDerivation
|
||||
import org.bitcoinj.crypto.MnemonicCode
|
||||
import java.security.KeyStore
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
|
||||
class WalletMonitoringService : Service() {
|
||||
|
||||
companion object {
|
||||
private const val CHANNEL_ID = "vexta_wallet_monitoring"
|
||||
private const val PAYMENT_CHANNEL_ID = "vexta_incoming_payments"
|
||||
private const val NOTIFICATION_ID = 7001
|
||||
|
||||
private const val PREFS = "vexta_wallet"
|
||||
private const val KEY_ALIAS = "vexta_wallet_seed_key"
|
||||
private const val PREF_CIPHERTEXT = "seed_ciphertext"
|
||||
private const val PREF_IV = "seed_iv"
|
||||
private const val PREF_RECEIVE_ADDRESS_INDEX =
|
||||
"receive_address_index"
|
||||
private const val PREF_KNOWN_INCOMING_TXIDS =
|
||||
"known_incoming_txids"
|
||||
private const val PREF_TX_NOTIFICATIONS_INITIALIZED =
|
||||
"tx_notifications_initialized"
|
||||
}
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private var scanRunning = false
|
||||
|
||||
private val monitorRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
startBackgroundScan()
|
||||
handler.postDelayed(this, 60_000L)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
val manager =
|
||||
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
createChannels()
|
||||
startForeground(NOTIFICATION_ID, monitoringNotification())
|
||||
|
||||
manager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"Wallet monitoring",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
)
|
||||
|
||||
val intent = Intent(this, WalletActivity::class.java)
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val notification = Notification.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(android.R.drawable.stat_notify_sync)
|
||||
.setContentTitle("Vexta Wallet")
|
||||
.setContentText("Monitoring active")
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.setCategory(Notification.CATEGORY_SERVICE)
|
||||
.build()
|
||||
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
handler.postDelayed(monitorRunnable, 5_000L)
|
||||
}
|
||||
|
||||
override fun onStartCommand(
|
||||
|
|
@ -59,5 +70,346 @@ class WalletMonitoringService : Service() {
|
|||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
handler.removeCallbacks(monitorRunnable)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
private fun startBackgroundScan() {
|
||||
if (scanRunning || !walletExists()) {
|
||||
return
|
||||
}
|
||||
|
||||
scanRunning = true
|
||||
|
||||
Thread {
|
||||
try {
|
||||
val chain = HeaderSync.loadCachedChain(this)
|
||||
|
||||
for (peer in listOf("87.106.99.23", "74.208.53.160")) {
|
||||
try {
|
||||
val result = HeaderSync.syncFromPeer(peer, chain)
|
||||
|
||||
if (result.received > 0) {
|
||||
HeaderSync.saveChain(this, chain)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
val words = loadMnemonic()
|
||||
.trim()
|
||||
.split(Regex("\\s+"))
|
||||
|
||||
val highestAddressIndex =
|
||||
getSharedPreferences(PREFS, MODE_PRIVATE)
|
||||
.getInt(PREF_RECEIVE_ADDRESS_INDEX, 0)
|
||||
.coerceAtLeast(0)
|
||||
|
||||
val allTransactions =
|
||||
mutableListOf<BlockScanner.WalletTransaction>()
|
||||
|
||||
for (addressIndex in 0..highestAddressIndex) {
|
||||
val script =
|
||||
deriveWitnessScript(words, addressIndex)
|
||||
|
||||
val filters =
|
||||
CompactFilterClient.scan(
|
||||
this,
|
||||
"87.106.99.23",
|
||||
script
|
||||
) { _, _ -> }
|
||||
|
||||
val blocks =
|
||||
BlockScanner.scan(
|
||||
this,
|
||||
"87.106.99.23",
|
||||
filters.matchingHeights,
|
||||
script,
|
||||
addressIndex
|
||||
) { _, _ -> }
|
||||
|
||||
allTransactions.addAll(blocks.transactions)
|
||||
}
|
||||
|
||||
val mergedTransactions =
|
||||
allTransactions
|
||||
.groupBy { it.txid }
|
||||
.map { (_, transactions) ->
|
||||
BlockScanner.WalletTransaction(
|
||||
txid = transactions.first().txid,
|
||||
height = transactions.maxOf {
|
||||
it.height
|
||||
},
|
||||
netSatoshis = transactions.sumOf {
|
||||
it.netSatoshis
|
||||
}
|
||||
)
|
||||
}
|
||||
.filter { it.netSatoshis != 0L }
|
||||
|
||||
updateIncomingTransactionsAndNotify(
|
||||
mergedTransactions
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
} finally {
|
||||
scanRunning = false
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun updateIncomingTransactionsAndNotify(
|
||||
transactions: List<BlockScanner.WalletTransaction>
|
||||
) {
|
||||
val preferences =
|
||||
getSharedPreferences(PREFS, MODE_PRIVATE)
|
||||
|
||||
val incoming =
|
||||
transactions.filter { it.netSatoshis > 0L }
|
||||
|
||||
val currentTxids =
|
||||
incoming.map { it.txid }.toSet()
|
||||
|
||||
val initialized =
|
||||
preferences.getBoolean(
|
||||
PREF_TX_NOTIFICATIONS_INITIALIZED,
|
||||
false
|
||||
)
|
||||
|
||||
if (!initialized) {
|
||||
preferences.edit()
|
||||
.putBoolean(
|
||||
PREF_TX_NOTIFICATIONS_INITIALIZED,
|
||||
true
|
||||
)
|
||||
.putStringSet(
|
||||
PREF_KNOWN_INCOMING_TXIDS,
|
||||
currentTxids
|
||||
)
|
||||
.apply()
|
||||
return
|
||||
}
|
||||
|
||||
val knownTxids =
|
||||
preferences.getStringSet(
|
||||
PREF_KNOWN_INCOMING_TXIDS,
|
||||
emptySet()
|
||||
)?.toMutableSet() ?: mutableSetOf()
|
||||
|
||||
incoming
|
||||
.filter { it.txid !in knownTxids }
|
||||
.sortedBy { it.height }
|
||||
.forEach { transaction ->
|
||||
showIncomingPaymentNotification(
|
||||
transaction.netSatoshis
|
||||
)
|
||||
knownTxids.add(transaction.txid)
|
||||
}
|
||||
|
||||
preferences.edit()
|
||||
.putStringSet(
|
||||
PREF_KNOWN_INCOMING_TXIDS,
|
||||
knownTxids
|
||||
)
|
||||
.apply()
|
||||
}
|
||||
|
||||
private fun showIncomingPaymentNotification(
|
||||
receivedSatoshis: Long
|
||||
) {
|
||||
if (
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||
checkSelfPermission(
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
val amount = String.format(
|
||||
"%.8f",
|
||||
receivedSatoshis.toDouble() / 100_000_000.0
|
||||
)
|
||||
|
||||
val openWalletIntent = Intent(
|
||||
this,
|
||||
WalletActivity::class.java
|
||||
).apply {
|
||||
flags =
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
openWalletIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val notification = Notification.Builder(
|
||||
this,
|
||||
PAYMENT_CHANNEL_ID
|
||||
)
|
||||
.setSmallIcon(android.R.drawable.stat_notify_more)
|
||||
.setContentTitle("VTX payment received")
|
||||
.setContentText("$amount VTX has been confirmed")
|
||||
.setStyle(
|
||||
Notification.BigTextStyle().bigText(
|
||||
"A payment of $amount VTX was received " +
|
||||
"and confirmed in your Vexta Wallet."
|
||||
)
|
||||
)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setCategory(Notification.CATEGORY_MESSAGE)
|
||||
.build()
|
||||
|
||||
val manager =
|
||||
getSystemService(
|
||||
Context.NOTIFICATION_SERVICE
|
||||
) as NotificationManager
|
||||
|
||||
manager.notify(
|
||||
(System.currentTimeMillis() and 0x7fffffff).toInt(),
|
||||
notification
|
||||
)
|
||||
}
|
||||
|
||||
private fun walletExists(): Boolean {
|
||||
val prefs =
|
||||
getSharedPreferences(PREFS, MODE_PRIVATE)
|
||||
|
||||
return !prefs.getString(PREF_CIPHERTEXT, "")
|
||||
.isNullOrBlank() &&
|
||||
!prefs.getString(PREF_IV, "")
|
||||
.isNullOrBlank()
|
||||
}
|
||||
|
||||
private fun loadMnemonic(): String {
|
||||
val prefs =
|
||||
getSharedPreferences(PREFS, MODE_PRIVATE)
|
||||
|
||||
val ciphertext =
|
||||
android.util.Base64.decode(
|
||||
prefs.getString(PREF_CIPHERTEXT, "") ?: "",
|
||||
android.util.Base64.NO_WRAP
|
||||
)
|
||||
|
||||
val iv =
|
||||
android.util.Base64.decode(
|
||||
prefs.getString(PREF_IV, "") ?: "",
|
||||
android.util.Base64.NO_WRAP
|
||||
)
|
||||
|
||||
val keyStore =
|
||||
KeyStore.getInstance("AndroidKeyStore").apply {
|
||||
load(null)
|
||||
}
|
||||
|
||||
val key =
|
||||
keyStore.getKey(KEY_ALIAS, null) as SecretKey
|
||||
|
||||
val cipher =
|
||||
Cipher.getInstance("AES/GCM/NoPadding")
|
||||
|
||||
cipher.init(
|
||||
Cipher.DECRYPT_MODE,
|
||||
key,
|
||||
GCMParameterSpec(128, iv)
|
||||
)
|
||||
|
||||
return cipher.doFinal(ciphertext)
|
||||
.toString(Charsets.UTF_8)
|
||||
}
|
||||
|
||||
private fun deriveWitnessScript(
|
||||
words: List<String>,
|
||||
addressIndex: Int
|
||||
): ByteArray {
|
||||
MnemonicCode.INSTANCE.check(words)
|
||||
|
||||
val seedBytes =
|
||||
MnemonicCode.toSeed(words, "")
|
||||
|
||||
val masterKey =
|
||||
HDKeyDerivation.createMasterPrivateKey(seedBytes)
|
||||
|
||||
val hierarchy =
|
||||
DeterministicHierarchy(masterKey)
|
||||
|
||||
val path =
|
||||
listOf(
|
||||
ChildNumber(84, true),
|
||||
ChildNumber(0, true),
|
||||
ChildNumber(0, true),
|
||||
ChildNumber.ZERO,
|
||||
ChildNumber(addressIndex, false)
|
||||
)
|
||||
|
||||
val key =
|
||||
hierarchy.get(path, true, true)
|
||||
|
||||
val ecKey =
|
||||
ECKey.fromPrivate(
|
||||
key.privKeyBytes,
|
||||
true
|
||||
)
|
||||
|
||||
val pubKeyHash =
|
||||
Utils.sha256hash160(ecKey.pubKey)
|
||||
|
||||
return byteArrayOf(0x00, 0x14) + pubKeyHash
|
||||
}
|
||||
|
||||
private fun createChannels() {
|
||||
val manager =
|
||||
getSystemService(
|
||||
Context.NOTIFICATION_SERVICE
|
||||
) as NotificationManager
|
||||
|
||||
manager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"Wallet monitoring",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
)
|
||||
|
||||
manager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
PAYMENT_CHANNEL_ID,
|
||||
"Incoming VTX payments",
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun monitoringNotification(): Notification {
|
||||
val intent =
|
||||
Intent(this, WalletActivity::class.java)
|
||||
|
||||
val pendingIntent =
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
return Notification.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(
|
||||
android.R.drawable.stat_notify_sync
|
||||
)
|
||||
.setContentTitle("Vexta Wallet")
|
||||
.setContentText("Monitoring active")
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.setCategory(Notification.CATEGORY_SERVICE)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue