zcash-android-wallet-sdk/src/main/java/cash/z/ecc/android/sdk/tool/WalletBirthdayTool.kt

126 lines
4.8 KiB
Kotlin
Raw Normal View History

package cash.z.ecc.android.sdk.tool
import android.content.Context
import cash.z.ecc.android.sdk.exception.BirthdayException
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.twig
import com.google.gson.Gson
import com.google.gson.stream.JsonReader
import java.io.InputStreamReader
import java.util.*
/**
* 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) {
val context = appContext.applicationContext
/**
* Load the nearest checkpoint to the given birthday height. If null is given, then this
* will load the most recent checkpoint available.
*/
fun loadNearest(birthdayHeight: Int? = null): WalletBirthday {
return loadBirthdayFromAssets(context, birthdayHeight)
}
/**
* Model object for holding a wallet birthday.
*
* @param height the height at the time the wallet was born.
* @param hash the hash of the block at the height.
* @param time the block time at the height. Represented as seconds since the Unix epoch.
* @param tree the sapling tree corresponding to the height.
*/
data class WalletBirthday(
val height: Int = -1,
val hash: String = "",
val time: Long = -1,
val tree: String = ""
)
companion object {
/**
* Directory within the assets folder where birthday data
* (i.e. sapling trees for a given height) can be found.
*/
private const val BIRTHDAY_DIRECTORY = "zcash/saplingtree"
/**
* Load the nearest checkpoint to the given birthday height. If null is given, then this
* will load the most recent checkpoint available.
*/
fun loadNearest(context: Context, birthdayHeight: Int? = null): WalletBirthday {
// TODO: potentially pull from shared preferences first
return loadBirthdayFromAssets(context, birthdayHeight)
}
/**
* 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.
*/
fun loadExact(context: Context, birthdayHeight: Int) =
loadNearest(context, birthdayHeight).also {
if (it.height != birthdayHeight)
throw BirthdayException.ExactBirthdayNotFoundException(
birthdayHeight,
it.height
)
}
/**
* 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,
birthdayHeight: Int? = null
): WalletBirthday {
twig("loading birthday from assets: $birthdayHeight")
val treeFiles =
context.assets.list(BIRTHDAY_DIRECTORY)?.apply { sortByDescending { fileName ->
try {
fileName.split('.').first().toInt()
} catch (t: Throwable) {
ZcashSdk.SAPLING_ACTIVATION_HEIGHT
}
} }
if (treeFiles.isNullOrEmpty()) throw BirthdayException.MissingBirthdayFilesException(
BIRTHDAY_DIRECTORY
)
twig("found ${treeFiles.size} sapling tree checkpoints: ${Arrays.toString(treeFiles)}")
val file: String
try {
file = if (birthdayHeight == null) treeFiles.first() else {
treeFiles.first {
it.split(".").first().toInt() <= birthdayHeight
}
}
} catch (t: Throwable) {
throw BirthdayException.BirthdayFileNotFoundException(
BIRTHDAY_DIRECTORY,
birthdayHeight
)
}
try {
val reader = JsonReader(
InputStreamReader(context.assets.open("${BIRTHDAY_DIRECTORY}/$file"))
)
return Gson().fromJson(reader, WalletBirthday::class.java)
} catch (t: Throwable) {
throw BirthdayException.MalformattedBirthdayFilesException(
BIRTHDAY_DIRECTORY,
treeFiles[0]
)
}
}
}
}