2020-09-11 00:33:25 -07:00
|
|
|
package cash.z.ecc.android.sdk.tool
|
|
|
|
|
|
|
|
import android.content.Context
|
2021-09-11 07:38:52 -07:00
|
|
|
import androidx.annotation.VisibleForTesting
|
2020-09-11 00:33:25 -07:00
|
|
|
import cash.z.ecc.android.sdk.exception.BirthdayException
|
2021-10-13 07:20:13 -07:00
|
|
|
import cash.z.ecc.android.sdk.internal.twig
|
2021-03-31 23:14:57 -07:00
|
|
|
import cash.z.ecc.android.sdk.type.WalletBirthday
|
2021-04-09 18:43:07 -07:00
|
|
|
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
2020-09-11 00:33:25 -07:00
|
|
|
import com.google.gson.Gson
|
|
|
|
import com.google.gson.stream.JsonReader
|
2021-09-11 07:38:52 -07:00
|
|
|
import java.io.IOException
|
2020-09-11 00:33:25 -07:00
|
|
|
import java.io.InputStreamReader
|
2021-09-25 05:13:14 -07:00
|
|
|
import java.util.Locale
|
2020-09-11 00:33:25 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Tool for loading checkpoints for the wallet, based on the height at which the wallet was born.
|
|
|
|
*
|
|
|
|
* @param appContext needed for loading checkpoints from the app's assets directory.
|
|
|
|
*/
|
|
|
|
class WalletBirthdayTool(appContext: Context) {
|
2021-09-11 07:38:52 -07:00
|
|
|
private val context = appContext.applicationContext
|
2020-09-11 00:33:25 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Load the nearest checkpoint to the given birthday height. If null is given, then this
|
|
|
|
* will load the most recent checkpoint available.
|
|
|
|
*/
|
2021-04-09 18:43:07 -07:00
|
|
|
fun loadNearest(network: ZcashNetwork, birthdayHeight: Int? = null): WalletBirthday {
|
|
|
|
return loadBirthdayFromAssets(context, network, birthdayHeight)
|
2020-09-11 00:33:25 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
|
2021-09-11 08:21:38 -07:00
|
|
|
// Behavior change implemented as a fix for issue #270. Temporarily adding a boolean
|
|
|
|
// that allows the change to be rolled back quickly if needed, although long-term
|
|
|
|
// this flag should be removed.
|
|
|
|
@VisibleForTesting
|
|
|
|
internal val IS_FALLBACK_ON_FAILURE = true
|
|
|
|
|
2020-09-11 00:33:25 -07:00
|
|
|
/**
|
|
|
|
* Load the nearest checkpoint to the given birthday height. If null is given, then this
|
|
|
|
* will load the most recent checkpoint available.
|
|
|
|
*/
|
2021-09-11 08:21:38 -07:00
|
|
|
fun loadNearest(
|
|
|
|
context: Context,
|
|
|
|
network: ZcashNetwork,
|
|
|
|
birthdayHeight: Int? = null
|
|
|
|
): WalletBirthday {
|
2020-09-11 00:33:25 -07:00
|
|
|
// TODO: potentially pull from shared preferences first
|
2021-04-09 18:43:07 -07:00
|
|
|
return loadBirthdayFromAssets(context, network, birthdayHeight)
|
2020-09-11 00:33:25 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Useful for when an exact checkpoint is needed, like for SAPLING_ACTIVATION_HEIGHT. In
|
|
|
|
* most cases, loading the nearest checkpoint is preferred for privacy reasons.
|
|
|
|
*/
|
2021-04-09 18:43:07 -07:00
|
|
|
fun loadExact(context: Context, network: ZcashNetwork, birthdayHeight: Int) =
|
|
|
|
loadNearest(context, network, birthdayHeight).also {
|
2020-09-11 00:33:25 -07:00
|
|
|
if (it.height != birthdayHeight)
|
|
|
|
throw BirthdayException.ExactBirthdayNotFoundException(
|
|
|
|
birthdayHeight,
|
|
|
|
it.height
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-09-11 07:38:52 -07:00
|
|
|
// TODO: This method performs disk IO; convert to suspending function
|
|
|
|
// Converting this to suspending will then propagate
|
|
|
|
@Throws(IOException::class)
|
|
|
|
internal fun listBirthdayDirectoryContents(context: Context, directory: String) =
|
|
|
|
context.assets.list(directory)
|
|
|
|
|
2021-04-09 18:43:07 -07:00
|
|
|
/**
|
|
|
|
* Returns the directory within the assets folder where birthday data
|
|
|
|
* (i.e. sapling trees for a given height) can be found.
|
|
|
|
*/
|
2021-09-11 07:38:52 -07:00
|
|
|
@VisibleForTesting
|
|
|
|
internal fun birthdayDirectory(network: ZcashNetwork) =
|
2021-09-25 05:13:14 -07:00
|
|
|
"saplingtree/${(network.networkName as java.lang.String).toLowerCase(Locale.US)}"
|
2021-09-11 07:38:52 -07:00
|
|
|
|
|
|
|
internal fun birthdayHeight(fileName: String) = fileName.split('.').first().toInt()
|
|
|
|
|
2021-09-11 08:21:38 -07:00
|
|
|
private fun Array<String>.sortDescending() =
|
|
|
|
apply { sortByDescending { birthdayHeight(it) } }
|
2021-04-09 18:43:07 -07:00
|
|
|
|
2020-09-11 00:33:25 -07:00
|
|
|
/**
|
|
|
|
* Load the given birthday file from the assets of the given context. When no height is
|
|
|
|
* specified, we default to the file with the greatest name.
|
|
|
|
*
|
|
|
|
* @param context the context from which to load assets.
|
|
|
|
* @param birthdayHeight the height file to look for among the file names.
|
|
|
|
*
|
|
|
|
* @return a WalletBirthday that reflects the contents of the file or an exception when
|
|
|
|
* parsing fails.
|
|
|
|
*/
|
|
|
|
private fun loadBirthdayFromAssets(
|
|
|
|
context: Context,
|
2021-04-09 18:43:07 -07:00
|
|
|
network: ZcashNetwork,
|
2020-09-11 00:33:25 -07:00
|
|
|
birthdayHeight: Int? = null
|
|
|
|
): WalletBirthday {
|
|
|
|
twig("loading birthday from assets: $birthdayHeight")
|
2021-04-09 18:43:07 -07:00
|
|
|
val directory = birthdayDirectory(network)
|
2021-09-11 08:21:38 -07:00
|
|
|
val treeFiles = getFilteredFileNames(context, directory, birthdayHeight)
|
|
|
|
|
|
|
|
twig("found ${treeFiles.size} sapling tree checkpoints: $treeFiles")
|
|
|
|
|
|
|
|
return getFirstValidWalletBirthday(context, directory, treeFiles)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun getFilteredFileNames(
|
|
|
|
context: Context,
|
|
|
|
directory: String,
|
|
|
|
birthdayHeight: Int? = null,
|
|
|
|
): List<String> {
|
|
|
|
val unfilteredTreeFiles = listBirthdayDirectoryContents(context, directory)
|
|
|
|
if (unfilteredTreeFiles.isNullOrEmpty()) {
|
|
|
|
throw BirthdayException.MissingBirthdayFilesException(directory)
|
|
|
|
}
|
|
|
|
|
|
|
|
val filteredTreeFiles = unfilteredTreeFiles
|
|
|
|
.sortDescending()
|
|
|
|
.filter { filename ->
|
|
|
|
birthdayHeight?.let { birthdayHeight(filename) <= it } ?: true
|
2020-09-11 00:33:25 -07:00
|
|
|
}
|
2021-09-11 08:21:38 -07:00
|
|
|
|
|
|
|
if (filteredTreeFiles.isEmpty()) {
|
2020-09-11 00:33:25 -07:00
|
|
|
throw BirthdayException.BirthdayFileNotFoundException(
|
2021-04-09 18:43:07 -07:00
|
|
|
directory,
|
2020-09-11 00:33:25 -07:00
|
|
|
birthdayHeight
|
|
|
|
)
|
|
|
|
}
|
2021-09-11 08:21:38 -07:00
|
|
|
|
|
|
|
return filteredTreeFiles
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param treeFiles A list of files, sorted in descending order based on `int` value of the first part of the filename.
|
|
|
|
*/
|
|
|
|
@VisibleForTesting
|
|
|
|
internal fun getFirstValidWalletBirthday(
|
|
|
|
context: Context,
|
|
|
|
directory: String,
|
|
|
|
treeFiles: List<String>
|
|
|
|
): WalletBirthday {
|
|
|
|
var lastException: Exception? = null
|
|
|
|
treeFiles.forEach { treefile ->
|
|
|
|
try {
|
|
|
|
context.assets.open("$directory/$treefile").use { inputStream ->
|
|
|
|
InputStreamReader(inputStream).use { inputStreamReader ->
|
|
|
|
JsonReader(inputStreamReader).use { jsonReader ->
|
|
|
|
return Gson().fromJson(jsonReader, WalletBirthday::class.java)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (t: Throwable) {
|
|
|
|
val exception = BirthdayException.MalformattedBirthdayFilesException(
|
|
|
|
directory,
|
|
|
|
treefile
|
|
|
|
)
|
|
|
|
lastException = exception
|
|
|
|
|
|
|
|
if (IS_FALLBACK_ON_FAILURE) {
|
|
|
|
// TODO: If we ever add crash analytics hooks, this would be something to report
|
|
|
|
twig("Malformed birthday file $t")
|
|
|
|
} else {
|
|
|
|
throw exception
|
|
|
|
}
|
|
|
|
}
|
2020-09-11 00:33:25 -07:00
|
|
|
}
|
2021-09-11 08:21:38 -07:00
|
|
|
|
|
|
|
throw lastException!!
|
2020-09-11 00:33:25 -07:00
|
|
|
}
|
|
|
|
}
|
2021-03-10 10:10:03 -08:00
|
|
|
}
|