6 Commits

Author SHA1 Message Date
cced1a34ce Bump version to 1.1.0 and unify signing config
- Bump versionName to 1.1.0 and versionCode to 1_01_00_800
- Use placeholder keystore for debug builds to match release signing
2026-03-21 04:20:46 -05:00
d501255c6b Add reorg detection, server failover, and live server switching
- Add reorg detection with user-facing repair dialog (clear app data)
- Add server picker dialog with connectivity indicators for all 6 lite servers
- Probe server reachability via TCP before connecting; auto-skip dead servers
- Support live server switching without app restart (resetSynchronizer)
- Update server list and default server URL to dragonx.is endpoints
- Update build config for DragonX mainnet
2026-03-21 03:50:04 -05:00
fekt
2d676f27ee Merge pull request '1.0.4 release' (#4) from dev into master
Reviewed-on: https://git.hush.is/dragonx/SilentDragonXAndroid/pulls/4
2024-11-18 18:55:49 +01:00
fekt
b913dacebf Fix build depedencies 2024-11-18 17:52:51 +00:00
fekt
fb0d13dffb Bump version 2024-11-18 15:09:39 +00:00
fekt
ec853d8bdc Merge pull request 'Merge master into dev' (#3) from master into dev
Reviewed-on: https://git.hush.is/dragonx/SilentDragonXAndroid/pulls/3
2024-11-18 16:06:47 +01:00
10 changed files with 208 additions and 24 deletions

1
.gitignore vendored
View File

@@ -71,3 +71,4 @@ fastlane/readme.md
# misc. # misc.
backup/ backup/
.editorconfig .editorconfig
external/

View File

@@ -47,13 +47,13 @@ android {
zcashtestnet { zcashtestnet {
dimension 'network' dimension 'network'
applicationId 'cash.z.ecc.android.testnet' applicationId 'cash.z.ecc.android.testnet'
buildConfigField "String", "DEFAULT_SERVER_URL", '"dragonlite.printogre.com"' buildConfigField "String", "DEFAULT_SERVER_URL", '"lite.dragonx.is"'
matchingFallbacks = ['zcashtestnet', 'debug'] matchingFallbacks = ['zcashtestnet', 'debug']
} }
zcashmainnet { zcashmainnet {
dimension 'network' dimension 'network'
buildConfigField "String", "DEFAULT_SERVER_URL", '"dragonlite.printogre.com"' buildConfigField "String", "DEFAULT_SERVER_URL", '"lite.dragonx.is"'
matchingFallbacks = ['zcashmainnet', 'release'] matchingFallbacks = ['zcashmainnet', 'release']
} }
} }
@@ -76,6 +76,7 @@ android {
minifyEnabled false minifyEnabled false
shrinkResources false shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.placeholder
} }
// builds for testing only in the wallet team, typically unfinished features // builds for testing only in the wallet team, typically unfinished features
// this flavor can be installed alongside the others // this flavor can be installed alongside the others
@@ -180,7 +181,10 @@ dependencies {
// Misc. // Misc.
implementation Deps.Misc.LOTTIE implementation Deps.Misc.LOTTIE
implementation Deps.Misc.CHIPS implementation Deps.Misc.CHIPS, {
exclude module: 'ChipsLayoutManager'
}
implementation 'com.github.BelooS:ChipsLayoutManager:v0.3.7'
implementation Deps.Misc.Plugins.QR_SCANNER implementation Deps.Misc.Plugins.QR_SCANNER
// Tests // Tests

View File

@@ -6,7 +6,10 @@ import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.ext.Const import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.feedback.* import cash.z.ecc.android.feedback.*
import cash.z.ecc.android.lockbox.LockBox import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.ui.util.DebugFileTwig import cash.z.ecc.android.ui.util.DebugFileTwig
import cash.z.ecc.android.util.SilentTwig import cash.z.ecc.android.util.SilentTwig
import cash.z.ecc.android.util.Twig import cash.z.ecc.android.util.Twig
@@ -18,7 +21,38 @@ object DependenciesHolder {
val initializerComponent by lazy { InitializerComponent() } val initializerComponent by lazy { InitializerComponent() }
val synchronizer by lazy { Synchronizer.newBlocking(initializerComponent.initializer) } private var _synchronizer: Synchronizer? = null
val synchronizer: Synchronizer
get() {
if (_synchronizer == null) {
_synchronizer = Synchronizer.newBlocking(initializerComponent.initializer)
}
return _synchronizer!!
}
/**
* Stop the current synchronizer and reinitialize with the server
* currently saved in preferences, so a new synchronizer will be
* created on next access pointing to the new server.
*/
fun resetSynchronizer() {
_synchronizer?.let {
it.stop()
_synchronizer = null
}
// Rebuild the initializer with the new server from prefs
val host = prefs[Const.Pref.SERVER_HOST] ?: Const.Default.Server.HOST
val port = prefs[Const.Pref.SERVER_PORT] ?: Const.Default.Server.PORT
val network = ZcashWalletApp.instance.defaultNetwork
val extfvk = lockBox.getCharsUtf8(Const.Backup.VIEWING_KEY)
val extpub = lockBox.getCharsUtf8(Const.Backup.PUBLIC_KEY)
if (extfvk != null && extpub != null) {
val vk = UnifiedViewingKey(extfvk = String(extfvk), extpub = String(extpub))
initializerComponent.createInitializer(Initializer.Config {
it.importWallet(vk, null, network, LightWalletEndpoint(host, port, true))
})
}
}
val clipboardManager by lazy { provideAppContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager } val clipboardManager by lazy { provideAppContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager }

View File

@@ -48,9 +48,13 @@ object Const {
object Default { object Default {
object Server { object Server {
// Select a random server from list // Select a random server from list
private val serverList = listOf( val serverList = listOf(
"dragonlite.printogre.com", "lite.dragonx.is",
"lite.dragonx.is" "lite1.dragonx.is",
"lite2.dragonx.is",
"lite3.dragonx.is",
"lite4.dragonx.is",
"lite5.dragonx.is"
) )
private val randomIndex = Random.nextInt(serverList.size); private val randomIndex = Random.nextInt(serverList.size);
private val randomServer = serverList[randomIndex] private val randomServer = serverList[randomIndex]

View File

@@ -5,12 +5,20 @@ import android.app.Dialog
import android.content.Context import android.content.Context
import android.text.Html import android.text.Html
import android.util.Log import android.util.Log
import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.TextView
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import cash.z.ecc.android.R import cash.z.ecc.android.R
import cash.z.ecc.android.feedback.Report import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.ui.MainActivity
import cash.z.ecc.android.ui.scan.ScanFragment import cash.z.ecc.android.ui.scan.ScanFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.*
import java.net.InetSocketAddress
import java.net.Socket
import kotlin.system.exitProcess import kotlin.system.exitProcess
fun Context.showClearDataConfirmation(onDismiss: () -> Unit = {}, onCancel: () -> Unit = {}): Dialog { fun Context.showClearDataConfirmation(onDismiss: () -> Unit = {}, onCancel: () -> Unit = {}): Dialog {
@@ -99,15 +107,9 @@ fun Context.showCriticalMessage(title: String, message: String, onDismiss: () ->
var pluckedError = splitError[0] var pluckedError = splitError[0]
if(pluckedError == "UNAVAILABLE"){ if(pluckedError == "UNAVAILABLE"){
return MaterialAlertDialogBuilder(this) return showServerPickerDialog(onServerSelected = { host ->
.setTitle("Server Unavailable") onDismiss()
.setMessage("Please close and restart the app to try another random server.") })
.setCancelable(false)
.setNegativeButton("Exit") { dialog, _ ->
dialog.dismiss()
exitProcess(0)
}
.show()
} }
return MaterialAlertDialogBuilder(this) return MaterialAlertDialogBuilder(this)
@@ -212,3 +214,87 @@ fun Context.showSharedLibraryCriticalError(e: Throwable): Dialog = showCriticalM
messageResId = R.string.dialog_error_critical_link_message, messageResId = R.string.dialog_error_critical_link_message,
onDismiss = { throw e } onDismiss = { throw e }
) )
fun Context.showReorgRepairDialog(onDismiss: () -> Unit = {}): Dialog {
return MaterialAlertDialogBuilder(this)
.setTitle("Incompatible Block Data")
.setMessage(
"The wallet contains block data from an old or incompatible chain. " +
"This prevents syncing.\n\n" +
"Would you like to clear the old data and sync fresh from the network?\n" +
"(Your wallet keys and addresses will be preserved)"
)
.setCancelable(false)
.setPositiveButton("Clear & Resync") { dialog, _ ->
dialog.dismiss()
onDismiss()
getSystemService<ActivityManager>()?.clearApplicationUserData()
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.dismiss()
onDismiss()
}
.show()
}
fun Context.showServerPickerDialog(onServerSelected: (String) -> Unit = {}): Dialog {
val servers = Const.Default.Server.serverList
val port = Const.Default.Server.PORT
// Display names start as "Checking..." and get updated
val displayItems = servers.map { "$it — checking..." }.toMutableList()
val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, displayItems)
val listView = ListView(this).apply {
this.adapter = adapter
setPadding(32, 16, 32, 16)
}
val dialog = MaterialAlertDialogBuilder(this)
.setTitle("Select a Server")
.setMessage("The current server is unavailable. Choose a server to connect to:")
.setView(listView)
.setCancelable(false)
.setNegativeButton("Exit") { d, _ ->
d.dismiss()
exitProcess(0)
}
.show()
// Check each server's connectivity in the background
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
servers.forEachIndexed { index, host ->
scope.launch {
val online = try {
Socket().use { socket ->
socket.connect(InetSocketAddress(host, port), 5000)
true
}
} catch (e: Exception) {
false
}
withContext(Dispatchers.Main) {
displayItems[index] = if (online) "$host" else "$host (offline)"
adapter.notifyDataSetChanged()
}
}
}
listView.setOnItemClickListener { _, _, position, _ ->
val host = servers[position]
// Save selected server to preferences
val prefs = cash.z.ecc.android.di.DependenciesHolder.prefs
prefs[Const.Pref.SERVER_HOST] = host
prefs[Const.Pref.SERVER_PORT] = port
dialog.dismiss()
scope.cancel()
onServerSelected(host)
// Reconnect with the new server without restarting
(this as? MainActivity)?.let { activity ->
cash.z.ecc.android.di.DependenciesHolder.resetSynchronizer()
activity.startSync(isRestart = true)
}
}
return dialog
}

View File

@@ -528,6 +528,7 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) {
// TODO: clean up this error handling // TODO: clean up this error handling
private var ignoredErrors = 0 private var ignoredErrors = 0
private var reorgCount = 0
private fun onProcessorError(error: Throwable?): Boolean { private fun onProcessorError(error: Throwable?): Boolean {
var notified = false var notified = false
when (error) { when (error) {
@@ -553,6 +554,17 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) {
} }
} }
} }
is CompactBlockProcessorException.FailedReorgRepair -> {
if (dialog == null) {
notified = true
runOnUiThread {
dialog = showReorgRepairDialog {
dialog = null
}
}
}
return false // stop retrying
}
} }
if (!notified) { if (!notified) {
ignoredErrors++ ignoredErrors++
@@ -573,6 +585,8 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) {
} }
private fun onChainError(errorHeight: BlockHeight, rewindHeight: BlockHeight) { private fun onChainError(errorHeight: BlockHeight, rewindHeight: BlockHeight) {
reorgCount++
twig("Chain reorg detected (#$reorgCount): error at $errorHeight, rewinding to $rewindHeight")
feedback.report(Reorg(errorHeight, rewindHeight)) feedback.report(Reorg(errorHeight, rewindHeight))
} }

View File

@@ -20,6 +20,8 @@ import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.* import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.*
import cash.z.ecc.android.util.twig import cash.z.ecc.android.util.twig
import cash.z.ecc.kotlin.mnemonic.Mnemonics import cash.z.ecc.kotlin.mnemonic.Mnemonics
import java.net.InetSocketAddress
import java.net.Socket
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
@@ -99,12 +101,13 @@ class WalletSetupViewModel : ViewModel() {
val vk = val vk =
loadUnifiedViewingKey() ?: onMissingViewingKey(network).also { overwriteVks = true } loadUnifiedViewingKey() ?: onMissingViewingKey(network).also { overwriteVks = true }
val birthdayHeight = loadBirthdayHeight() ?: onMissingBirthday(network) val birthdayHeight = loadBirthdayHeight() ?: onMissingBirthday(network)
val host = prefs[Const.Pref.SERVER_HOST] ?: Const.Default.Server.HOST val savedHost = prefs[Const.Pref.SERVER_HOST] ?: Const.Default.Server.HOST
val port = prefs[Const.Pref.SERVER_PORT] ?: Const.Default.Server.PORT val port = prefs[Const.Pref.SERVER_PORT] ?: Const.Default.Server.PORT
Log.d("SilentDragon", "host: $host") // Check if the preferred server is reachable; if not, try others
val host = findReachableServer(savedHost, port)
// TODO: Maybe check server availability here Log.d("SilentDragon", "host: $host")
twig("Done loading config variables") twig("Done loading config variables")
return Initializer.Config { return Initializer.Config {
@@ -113,6 +116,43 @@ class WalletSetupViewModel : ViewModel() {
} }
} }
/**
* Check if the preferred server is reachable via TCP. If not, try each server
* in the list until one responds. Returns the first reachable server, or falls
* back to the preferred server if none respond.
*/
private fun findReachableServer(preferredHost: String, port: Int): String {
// Try preferred server first
if (isServerReachable(preferredHost, port)) {
return preferredHost
}
twig("Preferred server $preferredHost is unreachable, trying alternatives...")
// Try each server in the list
for (server in Const.Default.Server.serverList) {
if (server != preferredHost && isServerReachable(server, port)) {
twig("Found reachable server: $server")
// Save working server to preferences for next time
prefs[Const.Pref.SERVER_HOST] = server
return server
}
}
twig("WARNING: No servers responded, using preferred: $preferredHost")
return preferredHost
}
private fun isServerReachable(host: String, port: Int): Boolean {
return try {
Socket().use { socket ->
socket.connect(InetSocketAddress(host, port), 5000)
true
}
} catch (e: Exception) {
false
}
}
private fun loadUnifiedViewingKey(): UnifiedViewingKey? { private fun loadUnifiedViewingKey(): UnifiedViewingKey? {
val extfvk = lockBox.getCharsUtf8(Const.Backup.VIEWING_KEY) val extfvk = lockBox.getCharsUtf8(Const.Backup.VIEWING_KEY)
val extpub = lockBox.getCharsUtf8(Const.Backup.PUBLIC_KEY) val extpub = lockBox.getCharsUtf8(Const.Backup.PUBLIC_KEY)

View File

@@ -9,8 +9,8 @@ object Deps {
const val compileSdkVersion = 31 const val compileSdkVersion = 31
const val minSdkVersion = 21 const val minSdkVersion = 21
const val targetSdkVersion = 30 const val targetSdkVersion = 30
const val versionName = "1.0.3" const val versionName = "1.1.0"
const val versionCode = 1_03_00 // last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release. const val versionCode = 1_01_00_800 // last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release.
const val packageName = "dragonx.android" const val packageName = "dragonx.android"
@@ -69,7 +69,7 @@ object Deps {
} }
} }
object Zcash { object Zcash {
const val ANDROID_WALLET_PLUGINS = "cash.z.ecc.android:zcash-android-wallet-plugins:1.0.0" const val ANDROID_WALLET_PLUGINS = "com.github.zcash:zcash-android-wallet-plugins:1.0.0"
const val KOTLIN_BIP39 = "cash.z.ecc.android:kotlin-bip39:1.0.4" const val KOTLIN_BIP39 = "cash.z.ecc.android:kotlin-bip39:1.0.4"
/* SDK uses mavenLocal build with DRGX customizations for now /* SDK uses mavenLocal build with DRGX customizations for now

View File

@@ -10,7 +10,9 @@ import java.util.*
import java.util.Locale.ENGLISH import java.util.Locale.ENGLISH
class Mnemonics : MnemonicPlugin { class Mnemonics : MnemonicPlugin {
/* Build fails for overrides nothing
override fun fullWordList(languageCode: String) = Mnemonics.getCachedWords(Locale.ENGLISH.language) override fun fullWordList(languageCode: String) = Mnemonics.getCachedWords(Locale.ENGLISH.language)
*/
override fun nextEntropy(): ByteArray = WordCount.COUNT_24.toEntropy() override fun nextEntropy(): ByteArray = WordCount.COUNT_24.toEntropy()
override fun nextMnemonic(): CharArray = MnemonicCode(WordCount.COUNT_24).chars override fun nextMnemonic(): CharArray = MnemonicCode(WordCount.COUNT_24).chars
override fun nextMnemonic(entropy: ByteArray): CharArray = MnemonicCode(entropy).chars override fun nextMnemonic(entropy: ByteArray): CharArray = MnemonicCode(entropy).chars
@@ -22,4 +24,4 @@ class Mnemonics : MnemonicPlugin {
fun validate(mnemonic: CharArray) { fun validate(mnemonic: CharArray) {
MnemonicCode(mnemonic).validate() MnemonicCode(mnemonic).validate()
} }
} }

View File

@@ -17,7 +17,6 @@ dependencyResolutionManagement {
google() google()
mavenCentral() mavenCentral()
maven("https://jitpack.io") maven("https://jitpack.io")
jcenter()
/* /*
// Uncomment to use a snapshot version of the SDK, e.g. when the SDK version ends in -SNAPSHOT // Uncomment to use a snapshot version of the SDK, e.g. when the SDK version ends in -SNAPSHOT