Release Vexta Wallet v0.6.5
This commit is contained in:
parent
b0f32d1a6a
commit
6ee4b78298
5 changed files with 278 additions and 69 deletions
|
|
@ -7,6 +7,7 @@
|
|||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
|
|
@ -40,6 +41,17 @@
|
|||
android:value="Monitors Vexta wallet for incoming blockchain payments while the app is in the background." />
|
||||
</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
|
||||
android:name="com.google.mlkit.vision.DEPENDENCIES"
|
||||
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>
|
||||
)
|
||||
|
||||
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(
|
||||
spendableUtxos: List<BlockScanner.SpendableUtxo>,
|
||||
recipientAddress: String,
|
||||
|
|
@ -72,49 +105,66 @@ object VextaTransactionSender {
|
|||
.thenBy { it.outputIndex }
|
||||
)
|
||||
|
||||
val maxSpend = calculateMaxSpend(orderedUtxos)
|
||||
val sendingMaximum =
|
||||
amountSatoshis == maxSpend.amount
|
||||
|
||||
val selected =
|
||||
mutableListOf<BlockScanner.SpendableUtxo>()
|
||||
|
||||
var selectedValue = 0L
|
||||
var fee: Long
|
||||
var change: Long
|
||||
|
||||
for (utxo in orderedUtxos) {
|
||||
selected.add(utxo)
|
||||
selectedValue =
|
||||
Math.addExact(selectedValue, utxo.value)
|
||||
if (sendingMaximum) {
|
||||
selected.addAll(orderedUtxos)
|
||||
|
||||
val estimatedFee = estimateFee(
|
||||
selectedValue = selected.fold(0L) { sum, utxo ->
|
||||
Math.addExact(sum, utxo.value)
|
||||
}
|
||||
|
||||
fee = maxSpend.fee
|
||||
change = 0L
|
||||
} else {
|
||||
for (utxo in orderedUtxos) {
|
||||
selected.add(utxo)
|
||||
selectedValue =
|
||||
Math.addExact(selectedValue, utxo.value)
|
||||
|
||||
val estimatedFee = estimateFee(
|
||||
inputCount = selected.size,
|
||||
outputCount = 2
|
||||
)
|
||||
|
||||
if (
|
||||
selectedValue >=
|
||||
amountSatoshis + estimatedFee
|
||||
) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (selected.isEmpty()) {
|
||||
throw IllegalStateException(
|
||||
"Wallet has no spendable outputs"
|
||||
)
|
||||
}
|
||||
|
||||
fee = estimateFee(
|
||||
inputCount = selected.size,
|
||||
outputCount = 2
|
||||
)
|
||||
|
||||
if (
|
||||
selectedValue >=
|
||||
amountSatoshis + estimatedFee
|
||||
) {
|
||||
break
|
||||
if (selectedValue < amountSatoshis + fee) {
|
||||
throw IllegalStateException(
|
||||
"Insufficient balance including transaction fee"
|
||||
)
|
||||
}
|
||||
|
||||
change =
|
||||
selectedValue - amountSatoshis - fee
|
||||
}
|
||||
|
||||
if (selected.isEmpty()) {
|
||||
throw IllegalStateException(
|
||||
"Wallet has no spendable outputs"
|
||||
)
|
||||
}
|
||||
|
||||
var fee = estimateFee(
|
||||
inputCount = selected.size,
|
||||
outputCount = 2
|
||||
)
|
||||
|
||||
if (selectedValue < amountSatoshis + fee) {
|
||||
throw IllegalStateException(
|
||||
"Insufficient balance including transaction fee"
|
||||
)
|
||||
}
|
||||
|
||||
var change =
|
||||
selectedValue - amountSatoshis - fee
|
||||
|
||||
val outputs = mutableListOf(
|
||||
TransactionOutputData(
|
||||
value = amountSatoshis,
|
||||
|
|
@ -122,16 +172,18 @@ object VextaTransactionSender {
|
|||
)
|
||||
)
|
||||
|
||||
if (change >= dustLimit) {
|
||||
outputs.add(
|
||||
TransactionOutputData(
|
||||
value = change,
|
||||
script = changeScript
|
||||
if (!sendingMaximum) {
|
||||
if (change >= dustLimit) {
|
||||
outputs.add(
|
||||
TransactionOutputData(
|
||||
value = change,
|
||||
script = changeScript
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
fee += change
|
||||
change = 0L
|
||||
} else {
|
||||
fee += change
|
||||
change = 0L
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -267,12 +267,6 @@ class WalletActivity : FragmentActivity() {
|
|||
val now = System.currentTimeMillis()
|
||||
|
||||
if (now - lastExitSwipeAt <= 2_000L) {
|
||||
stopService(
|
||||
Intent(
|
||||
this@WalletActivity,
|
||||
WalletMonitoringService::class.java
|
||||
)
|
||||
)
|
||||
finishAndRemoveTask()
|
||||
} else {
|
||||
lastExitSwipeAt = now
|
||||
|
|
@ -372,12 +366,6 @@ class WalletActivity : FragmentActivity() {
|
|||
val now = System.currentTimeMillis()
|
||||
|
||||
if (now - lastExitSwipeAt <= 2_000L) {
|
||||
stopService(
|
||||
Intent(
|
||||
this,
|
||||
WalletMonitoringService::class.java
|
||||
)
|
||||
)
|
||||
finishAndRemoveTask()
|
||||
} else {
|
||||
lastExitSwipeAt = now
|
||||
|
|
@ -1404,10 +1392,10 @@ class WalletActivity : FragmentActivity() {
|
|||
addView(sectionTitle("Get started"))
|
||||
addView(space(10))
|
||||
addView(primaryButton("Create new wallet") {
|
||||
createNewWallet()
|
||||
showSeedLengthDialog()
|
||||
})
|
||||
addView(space(12))
|
||||
addView(secondaryButton("Restore from 12 words") {
|
||||
addView(secondaryButton("Restore wallet") {
|
||||
showRestoreDialog()
|
||||
})
|
||||
}
|
||||
|
|
@ -1416,11 +1404,59 @@ class WalletActivity : FragmentActivity() {
|
|||
setContentView(scroll(content))
|
||||
}
|
||||
|
||||
private fun createNewWallet() {
|
||||
val entropy = ByteArray(16)
|
||||
private fun showSeedLengthDialog() {
|
||||
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)
|
||||
|
||||
val words = MnemonicCode.INSTANCE.toMnemonic(entropy)
|
||||
|
||||
require(words.size == wordCount) {
|
||||
"Unexpected recovery phrase length"
|
||||
}
|
||||
|
||||
saveMnemonic(words.joinToString(" "))
|
||||
|
||||
getSharedPreferences(PREFS, MODE_PRIVATE)
|
||||
|
|
@ -1441,7 +1477,7 @@ class WalletActivity : FragmentActivity() {
|
|||
content.addView(title("Recovery phrase"))
|
||||
content.addView(
|
||||
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."
|
||||
)
|
||||
)
|
||||
|
|
@ -1449,7 +1485,7 @@ class WalletActivity : FragmentActivity() {
|
|||
|
||||
content.addView(
|
||||
card {
|
||||
addView(sectionTitle("Your 12 words"))
|
||||
addView(sectionTitle("Your ${words.size} words"))
|
||||
addView(space(14))
|
||||
|
||||
addView(
|
||||
|
|
@ -1674,12 +1710,6 @@ class WalletActivity : FragmentActivity() {
|
|||
val now = System.currentTimeMillis()
|
||||
|
||||
if (now - lastExitSwipeAt <= 2_000L) {
|
||||
stopService(
|
||||
Intent(
|
||||
this,
|
||||
WalletMonitoringService::class.java
|
||||
)
|
||||
)
|
||||
finishAndRemoveTask()
|
||||
} else {
|
||||
lastExitSwipeAt = now
|
||||
|
|
@ -2005,6 +2035,80 @@ class WalletActivity : FragmentActivity() {
|
|||
|
||||
addView(space(12))
|
||||
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(primaryButton("Review payment") {
|
||||
if (sendInProgress) {
|
||||
|
|
@ -2355,7 +2459,7 @@ class WalletActivity : FragmentActivity() {
|
|||
|
||||
private fun showRestoreDialog() {
|
||||
val input = EditText(this).apply {
|
||||
hint = "Enter the 12 recovery words"
|
||||
hint = "Enter your 12 or 24 recovery words"
|
||||
minLines = 5
|
||||
gravity = Gravity.TOP
|
||||
setTextColor(textPrimary)
|
||||
|
|
@ -2370,7 +2474,9 @@ class WalletActivity : FragmentActivity() {
|
|||
|
||||
AlertDialog.Builder(this)
|
||||
.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)
|
||||
.setNegativeButton("Cancel", null)
|
||||
.setPositiveButton("Restore") { _, _ ->
|
||||
|
|
@ -2387,8 +2493,10 @@ class WalletActivity : FragmentActivity() {
|
|||
.split(Regex("\\s+"))
|
||||
.filter { it.isNotBlank() }
|
||||
|
||||
if (words.size != 12) {
|
||||
throw IllegalArgumentException("Exactly 12 words are required")
|
||||
if (words.size != 12 && words.size != 24) {
|
||||
throw IllegalArgumentException(
|
||||
"Recovery phrase must contain 12 or 24 words"
|
||||
)
|
||||
}
|
||||
|
||||
MnemonicCode.INSTANCE.check(words)
|
||||
|
|
@ -2527,7 +2635,7 @@ class WalletActivity : FragmentActivity() {
|
|||
.setTitle("Delete wallet?")
|
||||
.setMessage(
|
||||
"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)
|
||||
.setPositiveButton("Delete") { _, _ ->
|
||||
|
|
|
|||
Loading…
Reference in a new issue