[#536] Adopt BlockHeight API (#537)

Note that this change modifies the on-disk representation of the wallet.  Anyone who has an earlier build install will see a runtime crash.  This is expected, as we are not supporting data migrations at this stage of development.  The solution is to uninstall/reinstall the app.
This commit is contained in:
Carter Jernigan 2022-07-26 10:46:23 -04:00 committed by GitHub
parent 072d73e99c
commit f9afd2e5f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 63 additions and 155 deletions

View File

@ -132,7 +132,7 @@ PLAY_CORE_KTX_VERSION=1.8.1
ZCASH_ANDROID_WALLET_PLUGINS_VERSION=1.0.0 ZCASH_ANDROID_WALLET_PLUGINS_VERSION=1.0.0
ZCASH_BIP39_VERSION=1.0.2 ZCASH_BIP39_VERSION=1.0.2
# TODO [#279]: Revert to stable SDK before app release # TODO [#279]: Revert to stable SDK before app release
ZCASH_SDK_VERSION=1.7.0-beta01 ZCASH_SDK_VERSION=1.8.0-beta01-SNAPSHOT
ZXING_VERSION=3.5.0 ZXING_VERSION=3.5.0

View File

@ -1,57 +0,0 @@
package cash.z.ecc.sdk.model
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.type.WalletBirthday
import cash.z.ecc.sdk.fixture.WalletBirthdayFixture
import cash.z.ecc.sdk.test.count
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class WalletBirthdayTest {
@Test
@SmallTest
fun serialize() {
val walletBirthday = WalletBirthdayFixture.new()
val jsonObject = walletBirthday.toJson()
assertEquals(5, jsonObject.keys().count())
assertTrue(jsonObject.has(WalletBirthday.KEY_VERSION))
assertTrue(jsonObject.has(WalletBirthday.KEY_HEIGHT))
assertTrue(jsonObject.has(WalletBirthday.KEY_HASH))
assertTrue(jsonObject.has(WalletBirthday.KEY_EPOCH_SECONDS))
assertTrue(jsonObject.has(WalletBirthday.KEY_TREE))
assertEquals(1, jsonObject.getInt(WalletBirthday.KEY_VERSION))
assertEquals(WalletBirthdayFixture.HEIGHT, jsonObject.getInt(WalletBirthday.KEY_HEIGHT))
assertEquals(WalletBirthdayFixture.HASH, jsonObject.getString(WalletBirthday.KEY_HASH))
assertEquals(WalletBirthdayFixture.EPOCH_SECONDS, jsonObject.getLong(WalletBirthday.KEY_EPOCH_SECONDS))
assertEquals(WalletBirthdayFixture.TREE, jsonObject.getString(WalletBirthday.KEY_TREE))
}
@Test
@SmallTest
fun epoch_seconds_as_long_that_would_overflow_int() {
val walletBirthday = WalletBirthdayFixture.new(time = Long.MAX_VALUE)
val jsonObject = walletBirthday.toJson()
assertEquals(Long.MAX_VALUE, jsonObject.getLong(WalletBirthday.KEY_EPOCH_SECONDS))
WalletBirthday.from(jsonObject).also {
assertEquals(Long.MAX_VALUE, it.time)
}
}
@Test
@SmallTest
fun round_trip() {
val walletBirthday = WalletBirthdayFixture.new()
val deserialized = WalletBirthday.from(walletBirthday.toJson())
assertEquals(walletBirthday, deserialized)
assertFalse(walletBirthday === deserialized)
}
}

View File

@ -1,6 +1,6 @@
package cash.z.ecc.sdk.fixture package cash.z.ecc.sdk.fixture
import cash.z.ecc.android.sdk.type.WalletBirthday import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.ecc.sdk.model.PersistableWallet import cash.z.ecc.sdk.model.PersistableWallet
import cash.z.ecc.sdk.model.SeedPhrase import cash.z.ecc.sdk.model.SeedPhrase
@ -9,13 +9,15 @@ object PersistableWalletFixture {
val NETWORK = ZcashNetwork.Testnet val NETWORK = ZcashNetwork.Testnet
val BIRTHDAY = WalletBirthdayFixture.new() // These came from the mainnet 1500000.json file
@Suppress("MagicNumber")
val BIRTHDAY = BlockHeight.new(ZcashNetwork.Mainnet, 1500000L)
val SEED_PHRASE = SeedPhraseFixture.new() val SEED_PHRASE = SeedPhraseFixture.new()
fun new( fun new(
network: ZcashNetwork = NETWORK, network: ZcashNetwork = NETWORK,
birthday: WalletBirthday = BIRTHDAY, birthday: BlockHeight = BIRTHDAY,
seedPhrase: SeedPhrase = SEED_PHRASE seedPhrase: SeedPhrase = SEED_PHRASE
) = PersistableWallet(network, birthday, seedPhrase) ) = PersistableWallet(network, birthday, seedPhrase)
} }

View File

@ -1,20 +0,0 @@
package cash.z.ecc.sdk.fixture
import cash.z.ecc.android.sdk.type.WalletBirthday
object WalletBirthdayFixture {
const val HEIGHT = 1500000
const val HASH = "00047a34c61409682f44640af9352023ad92f69b827d0f2b288f152ebea50f46"
const val EPOCH_SECONDS = 1627076501L
@Suppress("MaxLineLength")
const val TREE = "01172b95f271c6af8f68388f08c8ef970db8ec8d8d61204ecb7b2bb2c38262b92d0010016284585a6c85dadfef27ff33f1403926b4bb391de92e8be797e4280cc4ca2971000001a1ff388639379c0120782b3929bd8871af797be4b651f694aa961bad65a9c12400000001d806c98bda9653d5ae22757eed750871e16e0fb657f52c3d771a4411668e84330001260f6e9fac0922f98d58afbcc3f391ac19d5d944081466929a33b99df19c0e6a0000013d2fd009bf8a22d68f720eac19c411c99014ed9c5f85d5942e15d1fc039e28680001f08f39275112dd8905b854170b7f247cf2df18454d4fa94e6e4f9320cca05f24011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39"
fun new(
height: Int = HEIGHT,
hash: String = HASH,
time: Long = EPOCH_SECONDS,
tree: String = TREE
) = WalletBirthday(height = height, hash = hash, time = time, tree = tree)
}

View File

@ -2,28 +2,42 @@ package cash.z.ecc.sdk.model
import cash.z.ecc.android.sdk.block.CompactBlockProcessor import cash.z.ecc.android.sdk.block.CompactBlockProcessor
fun CompactBlockProcessor.ProcessorInfo.downloadProgress() = if (lastDownloadRange.isEmpty()) { fun CompactBlockProcessor.ProcessorInfo.downloadProgress(): PercentDecimal {
PercentDecimal.ONE_HUNDRED_PERCENT val lastDownloadRangeSnapshot = lastDownloadRange
} else { val lastDownloadedHeightSnapshot = lastDownloadedHeight
val numerator = (lastDownloadedHeight - lastDownloadRange.first + 1)
.toFloat()
.coerceAtLeast(PercentDecimal.MIN)
val denominator = (lastDownloadRange.last - lastDownloadRange.first + 1).toFloat()
val progress = (numerator / denominator).coerceAtMost(PercentDecimal.MAX) return if (lastDownloadRangeSnapshot?.isEmpty() != false || lastDownloadedHeightSnapshot == null) {
PercentDecimal.ONE_HUNDRED_PERCENT
} else {
val numerator = (lastDownloadedHeightSnapshot.value - lastDownloadRangeSnapshot.start.value + 1)
.toFloat()
.coerceAtLeast(PercentDecimal.MIN)
val denominator = (lastDownloadRangeSnapshot.endInclusive.value - lastDownloadRangeSnapshot.start.value + 1)
.toFloat()
PercentDecimal(progress) val progress = (numerator / denominator).coerceAtMost(PercentDecimal.MAX)
PercentDecimal(progress)
}
} }
fun CompactBlockProcessor.ProcessorInfo.scanProgress() = if (lastScanRange.isEmpty()) { fun CompactBlockProcessor.ProcessorInfo.scanProgress(): PercentDecimal {
PercentDecimal.ONE_HUNDRED_PERCENT val lastScanRangeSnapshot = lastScanRange
} else { val lastScannedHeightSnapshot = lastScannedHeight
val numerator = (lastScannedHeight - lastScanRange.first + 1).toFloat().coerceAtLeast(PercentDecimal.MIN)
val demonimator = (lastScanRange.last - lastScanRange.first + 1).toFloat()
val progress = (numerator / demonimator).coerceAtMost(PercentDecimal.MAX) return if (lastScanRangeSnapshot?.isEmpty() != false || lastScannedHeightSnapshot == null) {
PercentDecimal.ONE_HUNDRED_PERCENT
} else {
val numerator = (lastScannedHeightSnapshot.value - lastScanRangeSnapshot.start.value + 1)
.toFloat()
.coerceAtLeast(PercentDecimal.MIN)
val demonimator = (lastScanRangeSnapshot.endInclusive.value - lastScanRangeSnapshot.start.value + 1)
.toFloat()
PercentDecimal(progress) val progress = (numerator / demonimator).coerceAtMost(PercentDecimal.MAX)
PercentDecimal(progress)
}
} }
// These are estimates // These are estimates

View File

@ -3,8 +3,7 @@ package cash.z.ecc.sdk.model
import android.app.Application import android.app.Application
import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toEntropy import cash.z.ecc.android.bip39.toEntropy
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.WalletBirthday
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.ecc.sdk.type.fromResources import cash.z.ecc.sdk.type.fromResources
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -16,7 +15,7 @@ import org.json.JSONObject
*/ */
data class PersistableWallet( data class PersistableWallet(
val network: ZcashNetwork, val network: ZcashNetwork,
val birthday: WalletBirthday?, val birthday: BlockHeight?,
val seedPhrase: SeedPhrase val seedPhrase: SeedPhrase
) { ) {
@ -29,7 +28,7 @@ data class PersistableWallet(
put(KEY_VERSION, VERSION_1) put(KEY_VERSION, VERSION_1)
put(KEY_NETWORK_ID, network.id) put(KEY_NETWORK_ID, network.id)
birthday?.let { birthday?.let {
put(KEY_BIRTHDAY, it.toJson()) put(KEY_BIRTHDAY, it.value)
} }
put(KEY_SEED_PHRASE, seedPhrase.joinToString()) put(KEY_SEED_PHRASE, seedPhrase.joinToString())
} }
@ -48,15 +47,19 @@ data class PersistableWallet(
fun from(jsonObject: JSONObject): PersistableWallet { fun from(jsonObject: JSONObject): PersistableWallet {
when (val version = jsonObject.getInt(KEY_VERSION)) { when (val version = jsonObject.getInt(KEY_VERSION)) {
VERSION_1 -> { VERSION_1 -> {
val networkId = jsonObject.getInt(KEY_NETWORK_ID) val network = run {
val networkId = jsonObject.getInt(KEY_NETWORK_ID)
ZcashNetwork.from(networkId)
}
val birthday = if (jsonObject.has(KEY_BIRTHDAY)) { val birthday = if (jsonObject.has(KEY_BIRTHDAY)) {
WalletBirthday.from(jsonObject.getJSONObject(KEY_BIRTHDAY)) val birthdayBlockHeightLong = jsonObject.getLong(KEY_BIRTHDAY)
BlockHeight.new(network, birthdayBlockHeightLong)
} else { } else {
null null
} }
val seedPhrase = jsonObject.getString(KEY_SEED_PHRASE) val seedPhrase = jsonObject.getString(KEY_SEED_PHRASE)
return PersistableWallet(ZcashNetwork.from(networkId), birthday, SeedPhrase.new(seedPhrase)) return PersistableWallet(network, birthday, SeedPhrase.new(seedPhrase))
} }
else -> { else -> {
throw IllegalArgumentException("Unsupported version $version") throw IllegalArgumentException("Unsupported version $version")
@ -69,11 +72,11 @@ data class PersistableWallet(
*/ */
suspend fun new(application: Application): PersistableWallet { suspend fun new(application: Application): PersistableWallet {
val zcashNetwork = ZcashNetwork.fromResources(application) val zcashNetwork = ZcashNetwork.fromResources(application)
val walletBirthday = WalletBirthdayTool.loadNearest(application, zcashNetwork) val birthday = BlockHeight.ofLatestCheckpoint(application, zcashNetwork)
val seedPhrase = newSeedPhrase() val seedPhrase = newSeedPhrase()
return PersistableWallet(zcashNetwork, walletBirthday, seedPhrase) return PersistableWallet(zcashNetwork, birthday, seedPhrase)
} }
} }
} }

View File

@ -1,43 +0,0 @@
package cash.z.ecc.sdk.model
import cash.z.ecc.android.sdk.type.WalletBirthday
import org.json.JSONObject
internal val WalletBirthday.Companion.VERSION_1
get() = 1
internal val WalletBirthday.Companion.KEY_VERSION
get() = "version"
internal val WalletBirthday.Companion.KEY_HEIGHT
get() = "height"
internal val WalletBirthday.Companion.KEY_HASH
get() = "hash"
internal val WalletBirthday.Companion.KEY_EPOCH_SECONDS
get() = "epoch_seconds"
internal val WalletBirthday.Companion.KEY_TREE
get() = "tree"
fun WalletBirthday.Companion.from(jsonString: String) = from(JSONObject(jsonString))
fun WalletBirthday.Companion.from(jsonObject: JSONObject): WalletBirthday {
when (val version = jsonObject.getInt(WalletBirthday.KEY_VERSION)) {
WalletBirthday.VERSION_1 -> {
val height = jsonObject.getInt(WalletBirthday.KEY_HEIGHT)
val hash = jsonObject.getString(WalletBirthday.KEY_HASH)
val epochSeconds = jsonObject.getLong(WalletBirthday.KEY_EPOCH_SECONDS)
val tree = jsonObject.getString(WalletBirthday.KEY_TREE)
return WalletBirthday(height, hash, epochSeconds, tree)
}
else -> {
throw IllegalArgumentException("Unsupported version $version")
}
}
}
fun WalletBirthday.toJson() = JSONObject().apply {
put(WalletBirthday.KEY_VERSION, WalletBirthday.VERSION_1)
put(WalletBirthday.KEY_HEIGHT, height)
put(WalletBirthday.KEY_HASH, hash)
put(WalletBirthday.KEY_EPOCH_SECONDS, time)
put(WalletBirthday.KEY_TREE, tree)
}

View File

@ -136,13 +136,15 @@ class WalletCoordinator(context: Context) {
* In order for a rescan to occur, the synchronizer must be loaded already * In order for a rescan to occur, the synchronizer must be loaded already
* which would happen if the UI is collecting it. * which would happen if the UI is collecting it.
* *
* @return True if the rewind was performed and false if the wipe was not performed. * @return True if the rescan was performed and false if the rescan was not performed.
*/ */
suspend fun rescanBlockchain(): Boolean { suspend fun rescanBlockchain(): Boolean {
synchronizerMutex.withLock { synchronizerMutex.withLock {
synchronizer.value?.let { synchronizer.value?.let {
it.rewindToNearestHeight(it.latestBirthdayHeight, true) it.latestBirthdayHeight?.let { height ->
return true it.rewindToNearestHeight(height, true)
return true
}
} }
} }
@ -235,6 +237,6 @@ private suspend fun PersistableWallet.toConfig(): Initializer.Config {
val vk = deriveViewingKey() val vk = deriveViewingKey()
return Initializer.Config { return Initializer.Config {
it.importWallet(vk, birthday?.height, network, network.defaultHost, network.defaultPort) it.importWallet(vk, birthday, network, network.defaultHost, network.defaultPort)
} }
} }

View File

@ -21,7 +21,13 @@ object WalletSnapshotFixture {
@Suppress("LongParameterList") @Suppress("LongParameterList")
fun new( fun new(
status: Synchronizer.Status = STATUS, status: Synchronizer.Status = STATUS,
processorInfo: CompactBlockProcessor.ProcessorInfo = CompactBlockProcessor.ProcessorInfo(), processorInfo: CompactBlockProcessor.ProcessorInfo = CompactBlockProcessor.ProcessorInfo(
null,
null,
null,
null,
null
),
orchardBalance: WalletBalance = ORCHARD_BALANCE, orchardBalance: WalletBalance = ORCHARD_BALANCE,
saplingBalance: WalletBalance = SAPLING_BALANCE, saplingBalance: WalletBalance = SAPLING_BALANCE,
transparentBalance: WalletBalance = TRANSPARENT_BALANCE, transparentBalance: WalletBalance = TRANSPARENT_BALANCE,

View File

@ -11,6 +11,7 @@ import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.db.entity.Transaction import cash.z.ecc.android.sdk.db.entity.Transaction
import cash.z.ecc.android.sdk.db.entity.isMined import cash.z.ecc.android.sdk.db.entity.isMined
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.WalletBalance import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.tool.DerivationTool
@ -282,7 +283,7 @@ sealed class SynchronizerError {
override fun getCauseMessage(): String? = error?.localizedMessage override fun getCauseMessage(): String? = error?.localizedMessage
} }
class Chain(val x: Int, val y: Int) : SynchronizerError() { class Chain(val x: BlockHeight, val y: BlockHeight) : SynchronizerError() {
override fun getCauseMessage(): String = "$x, $y" override fun getCauseMessage(): String = "$x, $y"
} }
} }