Release Vexta Wallet v0.6.5
This commit is contained in:
parent
b0f32d1a6a
commit
6ee4b78298
5 changed files with 278 additions and 69 deletions
|
|
@ -22,8 +22,8 @@ android {
|
||||||
applicationId = "org.vextaproject.wallet"
|
applicationId = "org.vextaproject.wallet"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 34
|
versionCode = 35
|
||||||
versionName = "0.6.4"
|
versionName = "0.6.5"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
|
|
@ -40,6 +41,17 @@
|
||||||
android:value="Monitors Vexta wallet for incoming blockchain payments while the app is in the background." />
|
android:value="Monitors Vexta wallet for incoming blockchain payments while the app is in the background." />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".BootReceiver"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.mlkit.vision.DEPENDENCIES"
|
android:name="com.google.mlkit.vision.DEPENDENCIES"
|
||||||
android:value="barcode_ui" />
|
android:value="barcode_ui" />
|
||||||
|
|
|
||||||
37
app/src/main/java/org/vextaproject/wallet/BootReceiver.kt
Normal file
37
app/src/main/java/org/vextaproject/wallet/BootReceiver.kt
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
package org.vextaproject.wallet
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
|
||||||
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
if (
|
||||||
|
intent?.action != Intent.ACTION_BOOT_COMPLETED &&
|
||||||
|
intent?.action != Intent.ACTION_MY_PACKAGE_REPLACED
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val walletExists =
|
||||||
|
context.getSharedPreferences(
|
||||||
|
"vexta_wallet",
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
).contains("seed_ciphertext")
|
||||||
|
|
||||||
|
if (!walletExists) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val monitoringIntent =
|
||||||
|
Intent(context, WalletMonitoringService::class.java)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
context.startForegroundService(monitoringIntent)
|
||||||
|
} else {
|
||||||
|
context.startService(monitoringIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -43,6 +43,39 @@ object VextaTransactionSender {
|
||||||
val spentOutpoints: Set<String>
|
val spentOutpoints: Set<String>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class MaxSpend(
|
||||||
|
val amount: Long,
|
||||||
|
val fee: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
fun calculateMaxSpend(
|
||||||
|
spendableUtxos: List<BlockScanner.SpendableUtxo>
|
||||||
|
): MaxSpend {
|
||||||
|
require(spendableUtxos.isNotEmpty()) {
|
||||||
|
"Wallet has no spendable outputs"
|
||||||
|
}
|
||||||
|
|
||||||
|
val total = spendableUtxos.fold(0L) { sum, utxo ->
|
||||||
|
Math.addExact(sum, utxo.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
val fee = estimateFee(
|
||||||
|
inputCount = spendableUtxos.size,
|
||||||
|
outputCount = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
val amount = total - fee
|
||||||
|
|
||||||
|
require(amount > 0L) {
|
||||||
|
"Balance is too small to cover the transaction fee"
|
||||||
|
}
|
||||||
|
|
||||||
|
return MaxSpend(
|
||||||
|
amount = amount,
|
||||||
|
fee = fee
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun createAndSign(
|
fun createAndSign(
|
||||||
spendableUtxos: List<BlockScanner.SpendableUtxo>,
|
spendableUtxos: List<BlockScanner.SpendableUtxo>,
|
||||||
recipientAddress: String,
|
recipientAddress: String,
|
||||||
|
|
@ -72,11 +105,27 @@ object VextaTransactionSender {
|
||||||
.thenBy { it.outputIndex }
|
.thenBy { it.outputIndex }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val maxSpend = calculateMaxSpend(orderedUtxos)
|
||||||
|
val sendingMaximum =
|
||||||
|
amountSatoshis == maxSpend.amount
|
||||||
|
|
||||||
val selected =
|
val selected =
|
||||||
mutableListOf<BlockScanner.SpendableUtxo>()
|
mutableListOf<BlockScanner.SpendableUtxo>()
|
||||||
|
|
||||||
var selectedValue = 0L
|
var selectedValue = 0L
|
||||||
|
var fee: Long
|
||||||
|
var change: Long
|
||||||
|
|
||||||
|
if (sendingMaximum) {
|
||||||
|
selected.addAll(orderedUtxos)
|
||||||
|
|
||||||
|
selectedValue = selected.fold(0L) { sum, utxo ->
|
||||||
|
Math.addExact(sum, utxo.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fee = maxSpend.fee
|
||||||
|
change = 0L
|
||||||
|
} else {
|
||||||
for (utxo in orderedUtxos) {
|
for (utxo in orderedUtxos) {
|
||||||
selected.add(utxo)
|
selected.add(utxo)
|
||||||
selectedValue =
|
selectedValue =
|
||||||
|
|
@ -101,7 +150,7 @@ object VextaTransactionSender {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var fee = estimateFee(
|
fee = estimateFee(
|
||||||
inputCount = selected.size,
|
inputCount = selected.size,
|
||||||
outputCount = 2
|
outputCount = 2
|
||||||
)
|
)
|
||||||
|
|
@ -112,8 +161,9 @@ object VextaTransactionSender {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var change =
|
change =
|
||||||
selectedValue - amountSatoshis - fee
|
selectedValue - amountSatoshis - fee
|
||||||
|
}
|
||||||
|
|
||||||
val outputs = mutableListOf(
|
val outputs = mutableListOf(
|
||||||
TransactionOutputData(
|
TransactionOutputData(
|
||||||
|
|
@ -122,6 +172,7 @@ object VextaTransactionSender {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (!sendingMaximum) {
|
||||||
if (change >= dustLimit) {
|
if (change >= dustLimit) {
|
||||||
outputs.add(
|
outputs.add(
|
||||||
TransactionOutputData(
|
TransactionOutputData(
|
||||||
|
|
@ -133,6 +184,7 @@ object VextaTransactionSender {
|
||||||
fee += change
|
fee += change
|
||||||
change = 0L
|
change = 0L
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Network parameters do not affect transaction serialization or
|
* Network parameters do not affect transaction serialization or
|
||||||
|
|
|
||||||
|
|
@ -267,12 +267,6 @@ class WalletActivity : FragmentActivity() {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
|
|
||||||
if (now - lastExitSwipeAt <= 2_000L) {
|
if (now - lastExitSwipeAt <= 2_000L) {
|
||||||
stopService(
|
|
||||||
Intent(
|
|
||||||
this@WalletActivity,
|
|
||||||
WalletMonitoringService::class.java
|
|
||||||
)
|
|
||||||
)
|
|
||||||
finishAndRemoveTask()
|
finishAndRemoveTask()
|
||||||
} else {
|
} else {
|
||||||
lastExitSwipeAt = now
|
lastExitSwipeAt = now
|
||||||
|
|
@ -372,12 +366,6 @@ class WalletActivity : FragmentActivity() {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
|
|
||||||
if (now - lastExitSwipeAt <= 2_000L) {
|
if (now - lastExitSwipeAt <= 2_000L) {
|
||||||
stopService(
|
|
||||||
Intent(
|
|
||||||
this,
|
|
||||||
WalletMonitoringService::class.java
|
|
||||||
)
|
|
||||||
)
|
|
||||||
finishAndRemoveTask()
|
finishAndRemoveTask()
|
||||||
} else {
|
} else {
|
||||||
lastExitSwipeAt = now
|
lastExitSwipeAt = now
|
||||||
|
|
@ -1404,10 +1392,10 @@ class WalletActivity : FragmentActivity() {
|
||||||
addView(sectionTitle("Get started"))
|
addView(sectionTitle("Get started"))
|
||||||
addView(space(10))
|
addView(space(10))
|
||||||
addView(primaryButton("Create new wallet") {
|
addView(primaryButton("Create new wallet") {
|
||||||
createNewWallet()
|
showSeedLengthDialog()
|
||||||
})
|
})
|
||||||
addView(space(12))
|
addView(space(12))
|
||||||
addView(secondaryButton("Restore from 12 words") {
|
addView(secondaryButton("Restore wallet") {
|
||||||
showRestoreDialog()
|
showRestoreDialog()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -1416,11 +1404,59 @@ class WalletActivity : FragmentActivity() {
|
||||||
setContentView(scroll(content))
|
setContentView(scroll(content))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createNewWallet() {
|
private fun showSeedLengthDialog() {
|
||||||
val entropy = ByteArray(16)
|
val content = LinearLayout(this).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
setPadding(
|
||||||
|
dp(20),
|
||||||
|
dp(8),
|
||||||
|
dp(20),
|
||||||
|
dp(4)
|
||||||
|
)
|
||||||
|
|
||||||
|
addView(
|
||||||
|
primaryButton("12-word recovery phrase") {
|
||||||
|
createNewWallet(12)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
addView(space(12))
|
||||||
|
|
||||||
|
addView(
|
||||||
|
primaryButton("24-word recovery phrase") {
|
||||||
|
createNewWallet(24)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle("Recovery phrase length")
|
||||||
|
.setMessage(
|
||||||
|
"Choose the recovery phrase length for your new wallet."
|
||||||
|
)
|
||||||
|
.setView(content)
|
||||||
|
.setNegativeButton("Cancel", null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNewWallet(wordCount: Int) {
|
||||||
|
val entropyBytes = when (wordCount) {
|
||||||
|
12 -> 16
|
||||||
|
24 -> 32
|
||||||
|
else -> throw IllegalArgumentException(
|
||||||
|
"Unsupported recovery phrase length"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val entropy = ByteArray(entropyBytes)
|
||||||
SecureRandom().nextBytes(entropy)
|
SecureRandom().nextBytes(entropy)
|
||||||
|
|
||||||
val words = MnemonicCode.INSTANCE.toMnemonic(entropy)
|
val words = MnemonicCode.INSTANCE.toMnemonic(entropy)
|
||||||
|
|
||||||
|
require(words.size == wordCount) {
|
||||||
|
"Unexpected recovery phrase length"
|
||||||
|
}
|
||||||
|
|
||||||
saveMnemonic(words.joinToString(" "))
|
saveMnemonic(words.joinToString(" "))
|
||||||
|
|
||||||
getSharedPreferences(PREFS, MODE_PRIVATE)
|
getSharedPreferences(PREFS, MODE_PRIVATE)
|
||||||
|
|
@ -1441,7 +1477,7 @@ class WalletActivity : FragmentActivity() {
|
||||||
content.addView(title("Recovery phrase"))
|
content.addView(title("Recovery phrase"))
|
||||||
content.addView(
|
content.addView(
|
||||||
subtitle(
|
subtitle(
|
||||||
"Write these 12 words down in the correct order.\n" +
|
"Write these ${words.size} words down in the correct order.\n" +
|
||||||
"Anyone with these words can control your VTX."
|
"Anyone with these words can control your VTX."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -1449,7 +1485,7 @@ class WalletActivity : FragmentActivity() {
|
||||||
|
|
||||||
content.addView(
|
content.addView(
|
||||||
card {
|
card {
|
||||||
addView(sectionTitle("Your 12 words"))
|
addView(sectionTitle("Your ${words.size} words"))
|
||||||
addView(space(14))
|
addView(space(14))
|
||||||
|
|
||||||
addView(
|
addView(
|
||||||
|
|
@ -1674,12 +1710,6 @@ class WalletActivity : FragmentActivity() {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
|
|
||||||
if (now - lastExitSwipeAt <= 2_000L) {
|
if (now - lastExitSwipeAt <= 2_000L) {
|
||||||
stopService(
|
|
||||||
Intent(
|
|
||||||
this,
|
|
||||||
WalletMonitoringService::class.java
|
|
||||||
)
|
|
||||||
)
|
|
||||||
finishAndRemoveTask()
|
finishAndRemoveTask()
|
||||||
} else {
|
} else {
|
||||||
lastExitSwipeAt = now
|
lastExitSwipeAt = now
|
||||||
|
|
@ -2005,6 +2035,80 @@ class WalletActivity : FragmentActivity() {
|
||||||
|
|
||||||
addView(space(12))
|
addView(space(12))
|
||||||
addView(amountInput)
|
addView(amountInput)
|
||||||
|
addView(space(10))
|
||||||
|
|
||||||
|
val availableSatoshis =
|
||||||
|
latestSpendableUtxos.sumOf { it.value }
|
||||||
|
|
||||||
|
addView(
|
||||||
|
smallStatus(
|
||||||
|
"Available: " +
|
||||||
|
String.format(
|
||||||
|
java.util.Locale.US,
|
||||||
|
"%.8f VTX",
|
||||||
|
availableSatoshis.toDouble() /
|
||||||
|
100_000_000.0
|
||||||
|
) +
|
||||||
|
"\nNetwork fee is shown before sending."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
addView(space(10))
|
||||||
|
|
||||||
|
addView(
|
||||||
|
secondaryButton("Send Max") {
|
||||||
|
try {
|
||||||
|
val maxSpend =
|
||||||
|
VextaTransactionSender.calculateMaxSpend(
|
||||||
|
latestSpendableUtxos
|
||||||
|
)
|
||||||
|
|
||||||
|
amountInput.setText(
|
||||||
|
String.format(
|
||||||
|
java.util.Locale.US,
|
||||||
|
"%.8f",
|
||||||
|
maxSpend.amount.toDouble() /
|
||||||
|
100_000_000.0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
noteView.text =
|
||||||
|
"Send Max selected\n" +
|
||||||
|
"Available: " +
|
||||||
|
String.format(
|
||||||
|
java.util.Locale.US,
|
||||||
|
"%.8f VTX",
|
||||||
|
(
|
||||||
|
maxSpend.amount +
|
||||||
|
maxSpend.fee
|
||||||
|
).toDouble() /
|
||||||
|
100_000_000.0
|
||||||
|
) +
|
||||||
|
"\nNetwork fee: " +
|
||||||
|
String.format(
|
||||||
|
java.util.Locale.US,
|
||||||
|
"%.8f VTX",
|
||||||
|
maxSpend.fee.toDouble() /
|
||||||
|
100_000_000.0
|
||||||
|
) +
|
||||||
|
"\nSending: " +
|
||||||
|
String.format(
|
||||||
|
java.util.Locale.US,
|
||||||
|
"%.8f VTX",
|
||||||
|
maxSpend.amount.toDouble() /
|
||||||
|
100_000_000.0
|
||||||
|
)
|
||||||
|
} catch (error: Exception) {
|
||||||
|
Toast.makeText(
|
||||||
|
this@WalletActivity,
|
||||||
|
error.message
|
||||||
|
?: "Unable to calculate maximum amount",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
addView(space(14))
|
addView(space(14))
|
||||||
addView(primaryButton("Review payment") {
|
addView(primaryButton("Review payment") {
|
||||||
if (sendInProgress) {
|
if (sendInProgress) {
|
||||||
|
|
@ -2355,7 +2459,7 @@ class WalletActivity : FragmentActivity() {
|
||||||
|
|
||||||
private fun showRestoreDialog() {
|
private fun showRestoreDialog() {
|
||||||
val input = EditText(this).apply {
|
val input = EditText(this).apply {
|
||||||
hint = "Enter the 12 recovery words"
|
hint = "Enter your 12 or 24 recovery words"
|
||||||
minLines = 5
|
minLines = 5
|
||||||
gravity = Gravity.TOP
|
gravity = Gravity.TOP
|
||||||
setTextColor(textPrimary)
|
setTextColor(textPrimary)
|
||||||
|
|
@ -2370,7 +2474,9 @@ class WalletActivity : FragmentActivity() {
|
||||||
|
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setTitle("Restore Vexta Wallet")
|
.setTitle("Restore Vexta Wallet")
|
||||||
.setMessage("Enter all 12 words separated by spaces.")
|
.setMessage(
|
||||||
|
"Enter all 12 or 24 recovery words separated by spaces."
|
||||||
|
)
|
||||||
.setView(input)
|
.setView(input)
|
||||||
.setNegativeButton("Cancel", null)
|
.setNegativeButton("Cancel", null)
|
||||||
.setPositiveButton("Restore") { _, _ ->
|
.setPositiveButton("Restore") { _, _ ->
|
||||||
|
|
@ -2387,8 +2493,10 @@ class WalletActivity : FragmentActivity() {
|
||||||
.split(Regex("\\s+"))
|
.split(Regex("\\s+"))
|
||||||
.filter { it.isNotBlank() }
|
.filter { it.isNotBlank() }
|
||||||
|
|
||||||
if (words.size != 12) {
|
if (words.size != 12 && words.size != 24) {
|
||||||
throw IllegalArgumentException("Exactly 12 words are required")
|
throw IllegalArgumentException(
|
||||||
|
"Recovery phrase must contain 12 or 24 words"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
MnemonicCode.INSTANCE.check(words)
|
MnemonicCode.INSTANCE.check(words)
|
||||||
|
|
@ -2527,7 +2635,7 @@ class WalletActivity : FragmentActivity() {
|
||||||
.setTitle("Delete wallet?")
|
.setTitle("Delete wallet?")
|
||||||
.setMessage(
|
.setMessage(
|
||||||
"This removes the encrypted seed from this phone. " +
|
"This removes the encrypted seed from this phone. " +
|
||||||
"The wallet can only be recovered with its 12 words."
|
"The wallet can only be recovered with its recovery phrase."
|
||||||
)
|
)
|
||||||
.setNegativeButton("Cancel", null)
|
.setNegativeButton("Cancel", null)
|
||||||
.setPositiveButton("Delete") { _, _ ->
|
.setPositiveButton("Delete") { _, _ ->
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue