Created birthday tool and derivation tool.
By extracting behavior from the Initializer into a more genralized class that can be used statically.
This commit is contained in:
parent
11431f5e5a
commit
a08beef93b
|
@ -1,7 +1,5 @@
|
|||
package cash.z.ecc.android.sdk.exception
|
||||
|
||||
import java.lang.RuntimeException
|
||||
|
||||
|
||||
/**
|
||||
* Marker for all custom exceptions from the SDK. Making it an interface would result in more typing
|
||||
|
@ -82,6 +80,14 @@ sealed class BirthdayException(message: String, cause: Throwable? = null) : SdkE
|
|||
"Failed to initialize wallet with alias=$alias because its birthday could not be found." +
|
||||
" Verify the alias or perhaps a new wallet should be created, instead."
|
||||
)
|
||||
|
||||
class ExactBirthdayNotFoundException(height: Int, nearestMatch: Int? = null): BirthdayException(
|
||||
"Unable to find birthday that exactly matches $height.${
|
||||
if (nearestMatch != null)
|
||||
" An exact match was request but the nearest match found was $nearestMatch."
|
||||
else ""
|
||||
}"
|
||||
)
|
||||
class BirthdayFileNotFoundException(directory: String, height: Int?) : BirthdayException(
|
||||
"Unable to find birthday file for $height verify that $directory/$height.json exists."
|
||||
)
|
||||
|
|
|
@ -7,3 +7,13 @@ internal inline fun <R> tryNull(block: () -> R): R? {
|
|||
null
|
||||
}
|
||||
}
|
||||
|
||||
internal inline fun <R> tryWarn(message: String, block: () -> R): R? {
|
||||
return try {
|
||||
block()
|
||||
} catch (t: Throwable) {
|
||||
twig("$message due to: $t")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ class RustBackend : RustBackendWelding {
|
|||
|
||||
internal var birthdayHeight: Int = -1
|
||||
get() = if (field != -1) field else throw BirthdayException.UninitializedBirthdayException
|
||||
private set
|
||||
|
||||
/**
|
||||
* Loads the library and initializes path variables. Although it is best to only call this
|
||||
|
@ -33,23 +34,27 @@ class RustBackend : RustBackendWelding {
|
|||
fun init(
|
||||
cacheDbPath: String,
|
||||
dataDbPath: String,
|
||||
paramsPath: String
|
||||
paramsPath: String,
|
||||
birthdayHeight: Int? = null
|
||||
): RustBackend {
|
||||
twig("Creating RustBackend") {
|
||||
pathCacheDb = cacheDbPath
|
||||
pathDataDb = dataDbPath
|
||||
pathParamsDir = paramsPath
|
||||
if (birthdayHeight != null) {
|
||||
this.birthdayHeight = birthdayHeight
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun clear(clearCacheDb: Boolean = true, clearDataDb: Boolean = true) {
|
||||
if (clearCacheDb) {
|
||||
twig("Deleting cache database!")
|
||||
twig("Deleting the cache database!")
|
||||
File(pathCacheDb).delete()
|
||||
}
|
||||
if (clearDataDb) {
|
||||
twig("Deleting data database!")
|
||||
twig("Deleting the data database!")
|
||||
File(pathDataDb).delete()
|
||||
}
|
||||
}
|
||||
|
@ -75,7 +80,6 @@ class RustBackend : RustBackendWelding {
|
|||
time: Long,
|
||||
saplingTree: String
|
||||
): Boolean {
|
||||
birthdayHeight = height
|
||||
return initBlocksTable(pathDataDb, height, hash, time, saplingTree)
|
||||
}
|
||||
|
||||
|
@ -123,22 +127,6 @@ class RustBackend : RustBackendWelding {
|
|||
"${pathParamsDir}/$OUTPUT_PARAM_FILE_NAME"
|
||||
)
|
||||
|
||||
override fun deriveSpendingKeys(seed: ByteArray, numberOfAccounts: Int) =
|
||||
deriveExtendedSpendingKeys(seed, numberOfAccounts)
|
||||
|
||||
|
||||
override fun deriveTAddress(seed: ByteArray): String = deriveTransparentAddress(seed)
|
||||
|
||||
override fun deriveViewingKeys(seed: ByteArray, numberOfAccounts: Int) =
|
||||
deriveExtendedFullViewingKeys(seed, numberOfAccounts)
|
||||
|
||||
override fun deriveViewingKey(spendingKey: String) = deriveExtendedFullViewingKey(spendingKey)
|
||||
|
||||
override fun deriveAddress(seed: ByteArray, accountIndex: Int) =
|
||||
deriveAddressFromSeed(seed, accountIndex)
|
||||
|
||||
override fun deriveAddress(viewingKey: String) = deriveAddressFromViewingKey(viewingKey)
|
||||
|
||||
override fun isValidShieldedAddr(addr: String) = isValidShieldedAddress(addr)
|
||||
|
||||
override fun isValidTransparentAddr(addr: String) = isValidTransparentAddress(addr)
|
||||
|
@ -258,20 +246,8 @@ class RustBackend : RustBackendWelding {
|
|||
|
||||
@JvmStatic private external fun initLogs()
|
||||
|
||||
@JvmStatic private external fun deriveExtendedSpendingKeys(seed: ByteArray, numberOfAccounts: Int): Array<String>
|
||||
|
||||
@JvmStatic private external fun deriveExtendedFullViewingKeys(seed: ByteArray, numberOfAccounts: Int): Array<String>
|
||||
|
||||
@JvmStatic private external fun deriveExtendedFullViewingKey(spendingKey: String): String
|
||||
|
||||
@JvmStatic private external fun deriveAddressFromSeed(seed: ByteArray, accountIndex: Int): String
|
||||
|
||||
@JvmStatic private external fun deriveAddressFromViewingKey(key: String): String
|
||||
|
||||
@JvmStatic private external fun branchIdForHeight(height: Int): Long
|
||||
|
||||
@JvmStatic private external fun parseTransactionDataList(serializedList: ByteArray): ByteArray
|
||||
|
||||
@JvmStatic private external fun deriveTransparentAddress(seed: ByteArray): String
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,18 +19,6 @@ interface RustBackendWelding {
|
|||
memo: ByteArray? = byteArrayOf()
|
||||
): Long
|
||||
|
||||
fun deriveAddress(viewingKey: String): String
|
||||
|
||||
fun deriveAddress(seed: ByteArray, accountIndex: Int = 0): String
|
||||
|
||||
fun deriveSpendingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array<String>
|
||||
|
||||
fun deriveTAddress(seed: ByteArray): String
|
||||
|
||||
fun deriveViewingKey(spendingKey: String): String
|
||||
|
||||
fun deriveViewingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array<String>
|
||||
|
||||
fun decryptAndStoreTransaction(tx: ByteArray)
|
||||
|
||||
fun initAccountsTable(seed: ByteArray, numberOfAccounts: Int): Array<String>
|
||||
|
@ -65,4 +53,18 @@ interface RustBackendWelding {
|
|||
|
||||
fun validateCombinedChain(): Int
|
||||
|
||||
// Implemented by `DerivationTool`
|
||||
interface Derivation {
|
||||
fun deriveShieldedAddress(viewingKey: String): String
|
||||
|
||||
fun deriveShieldedAddress(seed: ByteArray, accountIndex: Int = 0): String
|
||||
|
||||
fun deriveSpendingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array<String>
|
||||
|
||||
fun deriveTransparentAddress(seed: ByteArray): String
|
||||
|
||||
fun deriveViewingKey(spendingKey: String): String
|
||||
|
||||
fun deriveViewingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array<String>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
package cash.z.ecc.android.sdk.tool
|
||||
|
||||
import cash.z.ecc.android.sdk.jni.RustBackend
|
||||
import cash.z.ecc.android.sdk.jni.RustBackendWelding
|
||||
|
||||
class DerivationTool {
|
||||
companion object : RustBackendWelding.Derivation {
|
||||
|
||||
/**
|
||||
* Given a seed and a number of accounts, return the associated viewing keys.
|
||||
*
|
||||
* @param seed the seed from which to derive viewing keys.
|
||||
* @param numberOfAccounts the number of accounts to use. Multiple accounts are not fully
|
||||
* supported so the default value of 1 is recommended.
|
||||
*
|
||||
* @return the viewing keys that correspond to the seed, formatted as Strings.
|
||||
*/
|
||||
override fun deriveViewingKeys(seed: ByteArray, numberOfAccounts: Int): Array<String> =
|
||||
withRustBackendLoaded {
|
||||
deriveExtendedFullViewingKeys(seed, numberOfAccounts)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a spending key, return the associated viewing key.
|
||||
*
|
||||
* @param spendingKey the key from which to derive the viewing key.
|
||||
*
|
||||
* @return the viewing key that corresponds to the spending key.
|
||||
*/
|
||||
override fun deriveViewingKey(spendingKey: String): String = withRustBackendLoaded {
|
||||
deriveExtendedFullViewingKey(spendingKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a seed and a number of accounts, return the associated spending keys.
|
||||
*
|
||||
* @param seed the seed from which to derive spending keys.
|
||||
* @param numberOfAccounts the number of accounts to use. Multiple accounts are not fully
|
||||
* supported so the default value of 1 is recommended.
|
||||
*
|
||||
* @return the spending keys that correspond to the seed, formatted as Strings.
|
||||
*/
|
||||
override fun deriveSpendingKeys(seed: ByteArray, numberOfAccounts: Int): Array<String> =
|
||||
withRustBackendLoaded {
|
||||
deriveExtendedSpendingKeys(seed, numberOfAccounts)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a seed and account index, return the associated address.
|
||||
*
|
||||
* @param seed the seed from which to derive the address.
|
||||
* @param accountIndex the index of the account to use for deriving the address. Multiple
|
||||
* accounts are not fully supported so the default value of 1 is recommended.
|
||||
*
|
||||
* @return the address that corresponds to the seed and account index.
|
||||
*/
|
||||
override fun deriveShieldedAddress(seed: ByteArray, accountIndex: Int): String =
|
||||
withRustBackendLoaded {
|
||||
deriveShieldedAddressFromSeed(seed, accountIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a viewing key string, return the associated address.
|
||||
*
|
||||
* @param viewingKey the viewing key to use for deriving the address. The viewing key is tied to
|
||||
* a specific account so no account index is required.
|
||||
*
|
||||
* @return the address that corresponds to the viewing key.
|
||||
*/
|
||||
override fun deriveShieldedAddress(viewingKey: String): String = withRustBackendLoaded {
|
||||
deriveShieldedAddressFromViewingKey(viewingKey)
|
||||
}
|
||||
|
||||
// WIP probably shouldn't be used just yet. Why?
|
||||
// - because we need the private key associated with this seed and this function doesn't return it.
|
||||
// - the underlying implementation needs to be split out into a few lower-level calls
|
||||
override fun deriveTransparentAddress(seed: ByteArray): String = withRustBackendLoaded {
|
||||
deriveTransparentAddressFromSeed(seed)
|
||||
}
|
||||
|
||||
|
||||
fun validateViewingKey(viewingKey: String) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A helper function to ensure that the Rust libraries are loaded before any code in this
|
||||
* class attempts to interact with it, indirectly, by invoking JNI functions. It would be
|
||||
* nice to have an annotation like @UsesSystemLibrary for this
|
||||
*/
|
||||
private fun <T> withRustBackendLoaded(block: () -> T): T {
|
||||
RustBackend.load()
|
||||
return block()
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// JNI functions
|
||||
//
|
||||
|
||||
@JvmStatic
|
||||
private external fun deriveExtendedSpendingKeys(
|
||||
seed: ByteArray,
|
||||
numberOfAccounts: Int
|
||||
): Array<String>
|
||||
|
||||
@JvmStatic
|
||||
private external fun deriveExtendedFullViewingKeys(
|
||||
seed: ByteArray,
|
||||
numberOfAccounts: Int
|
||||
): Array<String>
|
||||
|
||||
@JvmStatic
|
||||
private external fun deriveExtendedFullViewingKey(spendingKey: String): String
|
||||
|
||||
@JvmStatic
|
||||
private external fun deriveShieldedAddressFromSeed(
|
||||
seed: ByteArray,
|
||||
accountIndex: Int
|
||||
): String
|
||||
|
||||
@JvmStatic
|
||||
private external fun deriveShieldedAddressFromViewingKey(key: String): String
|
||||
|
||||
@JvmStatic
|
||||
private external fun deriveTransparentAddressFromSeed(seed: ByteArray): String
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
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]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -171,7 +171,6 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initAccount
|
|||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveExtendedSpendingKeys(
|
||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initAccountsTableWithKeys(
|
||||
env: JNIEnv<'_>,
|
||||
_: JClass<'_>,
|
||||
|
@ -195,6 +194,9 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initAccount
|
|||
});
|
||||
unwrap_exc_or(&env, res, JNI_FALSE)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveExtendedSpendingKeys(
|
||||
env: JNIEnv<'_>,
|
||||
_: JClass<'_>,
|
||||
seed: jbyteArray,
|
||||
|
@ -229,7 +231,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initAccount
|
|||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveExtendedFullViewingKeys(
|
||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveExtendedFullViewingKeys(
|
||||
env: JNIEnv<'_>,
|
||||
_: JClass<'_>,
|
||||
seed: jbyteArray,
|
||||
|
@ -264,7 +266,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveExten
|
|||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveAddressFromSeed(
|
||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveShieldedAddressFromSeed(
|
||||
env: JNIEnv<'_>,
|
||||
_: JClass<'_>,
|
||||
seed: jbyteArray,
|
||||
|
@ -292,7 +294,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveAddre
|
|||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveAddressFromViewingKey(
|
||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveShieldedAddressFromViewingKey(
|
||||
env: JNIEnv<'_>,
|
||||
_: JClass<'_>,
|
||||
extfvk_string: JString<'_>,
|
||||
|
@ -326,7 +328,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveAddre
|
|||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveExtendedFullViewingKey(
|
||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveExtendedFullViewingKey(
|
||||
env: JNIEnv<'_>,
|
||||
_: JClass<'_>,
|
||||
extsk_string: JString<'_>,
|
||||
|
@ -683,7 +685,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_parseTransa
|
|||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveTransparentAddress(
|
||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveTransparentAddressFromSeed(
|
||||
env: JNIEnv<'_>,
|
||||
_: JClass<'_>,
|
||||
seed: jbyteArray,
|
||||
|
|
Loading…
Reference in New Issue