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
|
|
|
|
import cash.z.ecc.android.sdk.ext.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-03-10 10:10:03 -08:00
|
|
|
import java.util.Arrays
|
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 {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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(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) =
|
|
|
|
"saplingtree/${network.networkName.lowercase()}"
|
|
|
|
|
|
|
|
internal fun birthdayHeight(fileName: String) = fileName.split('.').first().toInt()
|
|
|
|
|
|
|
|
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 07:38:52 -07:00
|
|
|
val treeFiles = listBirthdayDirectoryContents(context, directory)?.sortDescending()
|
2020-09-11 00:33:25 -07:00
|
|
|
if (treeFiles.isNullOrEmpty()) throw BirthdayException.MissingBirthdayFilesException(
|
2021-04-09 18:43:07 -07:00
|
|
|
directory
|
2020-09-11 00:33:25 -07:00
|
|
|
)
|
|
|
|
twig("found ${treeFiles.size} sapling tree checkpoints: ${Arrays.toString(treeFiles)}")
|
|
|
|
val file: String
|
|
|
|
try {
|
|
|
|
file = if (birthdayHeight == null) treeFiles.first() else {
|
|
|
|
treeFiles.first {
|
2021-09-11 07:38:52 -07:00
|
|
|
birthdayHeight(it) <= birthdayHeight
|
2020-09-11 00:33:25 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (t: Throwable) {
|
|
|
|
throw BirthdayException.BirthdayFileNotFoundException(
|
2021-04-09 18:43:07 -07:00
|
|
|
directory,
|
2020-09-11 00:33:25 -07:00
|
|
|
birthdayHeight
|
|
|
|
)
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
val reader = JsonReader(
|
2021-04-09 18:43:07 -07:00
|
|
|
InputStreamReader(context.assets.open("$directory/$file"))
|
2020-09-11 00:33:25 -07:00
|
|
|
)
|
|
|
|
return Gson().fromJson(reader, WalletBirthday::class.java)
|
|
|
|
} catch (t: Throwable) {
|
|
|
|
throw BirthdayException.MalformattedBirthdayFilesException(
|
2021-04-09 18:43:07 -07:00
|
|
|
directory,
|
2020-09-11 00:33:25 -07:00
|
|
|
treeFiles[0]
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-03-10 10:10:03 -08:00
|
|
|
}
|