[#1327] Ktlint 1.1.0

* [#1327] Ktlint 1.1.0

Closes #1327

* [#1327] Autoformatting with Ktlint

* Ktlint formatting warnings fix
This commit is contained in:
Honza Rychnovský 2024-01-04 21:21:32 +01:00 committed by GitHub
parent e550cd9943
commit 73d7afc2a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
240 changed files with 4723 additions and 3814 deletions

View File

@ -6,7 +6,6 @@ import org.junit.Test
import kotlin.test.assertContentEquals import kotlin.test.assertContentEquals
class RustDerivationToolTest { class RustDerivationToolTest {
companion object { companion object {
private const val SEED_PHRASE = private const val SEED_PHRASE =
"kitchen renew wide common vague fold vacuum tilt amazing pear square gossip jewel month tree shock scan" + "kitchen renew wide common vague fold vacuum tilt amazing pear square gossip jewel month tree shock scan" +
@ -14,12 +13,13 @@ class RustDerivationToolTest {
} }
@Test @Test
fun create_spending_key_does_not_mutate_passed_bytes() = runTest { fun create_spending_key_does_not_mutate_passed_bytes() =
val bytesOne = Mnemonics.MnemonicCode(SEED_PHRASE).toEntropy() runTest {
val bytesTwo = Mnemonics.MnemonicCode(SEED_PHRASE).toEntropy() val bytesOne = Mnemonics.MnemonicCode(SEED_PHRASE).toEntropy()
val bytesTwo = Mnemonics.MnemonicCode(SEED_PHRASE).toEntropy()
RustDerivationTool.new().deriveUnifiedSpendingKey(bytesOne, networkId = 1, accountIndex = 0) RustDerivationTool.new().deriveUnifiedSpendingKey(bytesOne, networkId = 1, accountIndex = 0)
assertContentEquals(bytesTwo, bytesOne) assertContentEquals(bytesTwo, bytesOne)
} }
} }

View File

@ -14,7 +14,6 @@ import cash.z.ecc.android.sdk.internal.model.JniWalletSummary
*/ */
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
interface Backend { interface Backend {
val networkId: Int val networkId: Int
suspend fun initBlockMetaDb(): Int suspend fun initBlockMetaDb(): Int
@ -53,7 +52,11 @@ interface Backend {
* @throws RuntimeException as a common indicator of the operation failure * @throws RuntimeException as a common indicator of the operation failure
*/ */
@Throws(RuntimeException::class) @Throws(RuntimeException::class)
suspend fun createAccount(seed: ByteArray, treeState: ByteArray, recoverUntil: Long?): JniUnifiedSpendingKey suspend fun createAccount(
seed: ByteArray,
treeState: ByteArray,
recoverUntil: Long?
): JniUnifiedSpendingKey
fun isValidShieldedAddr(addr: String): Boolean fun isValidShieldedAddr(addr: String): Boolean
@ -75,7 +78,10 @@ interface Backend {
* @throws RuntimeException as a common indicator of the operation failure * @throws RuntimeException as a common indicator of the operation failure
*/ */
@Throws(RuntimeException::class) @Throws(RuntimeException::class)
suspend fun getMemoAsUtf8(txId: ByteArray, outputIndex: Int): String? suspend fun getMemoAsUtf8(
txId: ByteArray,
outputIndex: Int
): String?
suspend fun getNearestRewindHeight(height: Long): Long suspend fun getNearestRewindHeight(height: Long): Long
@ -139,7 +145,10 @@ interface Backend {
* @throws RuntimeException as a common indicator of the operation failure * @throws RuntimeException as a common indicator of the operation failure
*/ */
@Throws(RuntimeException::class) @Throws(RuntimeException::class)
suspend fun scanBlocks(fromHeight: Long, limit: Long) suspend fun scanBlocks(
fromHeight: Long,
limit: Long
)
/** /**
* @throws RuntimeException as a common indicator of the operation failure * @throws RuntimeException as a common indicator of the operation failure
@ -157,6 +166,7 @@ interface Backend {
suspend fun rewindBlockMetadataToHeight(height: Long) suspend fun rewindBlockMetadataToHeight(height: Long)
suspend fun getVerifiedTransparentBalance(address: String): Long suspend fun getVerifiedTransparentBalance(address: String): Long
suspend fun getTotalTransparentBalance(address: String): Long suspend fun getTotalTransparentBalance(address: String): Long
/** /**

View File

@ -6,29 +6,29 @@ import java.util.concurrent.Executors
internal object SdkExecutors { internal object SdkExecutors {
/** /**
* Executor used for database IO that's shared with the Rust native library. * Executor used for database IO that's shared with the Rust native library.
*/ *
/*
* Based on internal discussion, keep the SDK internals confined to a single IO thread. * Based on internal discussion, keep the SDK internals confined to a single IO thread.
* *
* We don't expect things to break, but we don't have the WAL enabled for SQLite so this * We don't expect things to break, but we don't have the WAL enabled for SQLite so this
* is a simple solution. * is a simple solution.
*/ */
val DATABASE_IO = Executors.newSingleThreadExecutor { val DATABASE_IO =
Thread(it, "zc-io").apply { isDaemon = true } Executors.newSingleThreadExecutor {
} Thread(it, "zc-io").apply { isDaemon = true }
}
} }
object SdkDispatchers { object SdkDispatchers {
/** /**
* Dispatcher used for database IO that's shared with the Rust native library. * Dispatcher used for database IO that's shared with the Rust native library.
*/ *
/*
* Based on internal discussion, keep the SDK internals confined to a single IO thread. * Based on internal discussion, keep the SDK internals confined to a single IO thread.
* *
* We don't expect things to break, but we don't have the WAL enabled for SQLite so this * We don't expect things to break, but we don't have the WAL enabled for SQLite so this
* is a simple solution. * is a simple solution.
*
* Don't use `Dispatchers.IO.limitedParallelism(1)`.
* While it executes serially, each dispatch can be on a different thread.
*/ */
// Don't use `Dispatchers.IO.limitedParallelism(1)`.
// While it executes serially, each dispatch can be on a different thread.
val DATABASE_IO = SdkExecutors.DATABASE_IO.asCoroutineDispatcher() val DATABASE_IO = SdkExecutors.DATABASE_IO.asCoroutineDispatcher()
} }

View File

@ -46,6 +46,7 @@ internal class NativeLibraryLoader(private val libraryName: String) {
} }
} }
private suspend fun loadLibrarySuspend(libraryName: String) = withContext(Dispatchers.IO) { private suspend fun loadLibrarySuspend(libraryName: String) =
System.loadLibrary(libraryName) withContext(Dispatchers.IO) {
} System.loadLibrary(libraryName)
}

View File

@ -25,7 +25,6 @@ class RustBackend private constructor(
private val saplingSpendFile: File, private val saplingSpendFile: File,
private val saplingOutputFile: File, private val saplingOutputFile: File,
) : Backend { ) : Backend {
/** /**
* This function deletes the data database file and the cache directory (compact blocks files) if set by input * This function deletes the data database file and the cache directory (compact blocks files) if set by input
* parameters. * parameters.
@ -35,7 +34,10 @@ class RustBackend private constructor(
* *
* @return false in case of any required and failed deletion, true otherwise. * @return false in case of any required and failed deletion, true otherwise.
*/ */
suspend fun clear(clearCache: Boolean = true, clearDataDb: Boolean = true): Boolean { suspend fun clear(
clearCache: Boolean = true,
clearDataDb: Boolean = true
): Boolean {
var cacheClearResult = true var cacheClearResult = true
var dataClearResult = true var dataClearResult = true
if (clearCache) { if (clearCache) {
@ -55,19 +57,21 @@ class RustBackend private constructor(
// Wrapper Functions // Wrapper Functions
// //
override suspend fun initBlockMetaDb() = withContext(SdkDispatchers.DATABASE_IO) { override suspend fun initBlockMetaDb() =
initBlockMetaDb( withContext(SdkDispatchers.DATABASE_IO) {
fsBlockDbRoot.absolutePath, initBlockMetaDb(
) fsBlockDbRoot.absolutePath,
} )
}
override suspend fun initDataDb(seed: ByteArray?) = withContext(SdkDispatchers.DATABASE_IO) { override suspend fun initDataDb(seed: ByteArray?) =
initDataDb( withContext(SdkDispatchers.DATABASE_IO) {
dataDbFile.absolutePath, initDataDb(
seed, dataDbFile.absolutePath,
networkId = networkId seed,
) networkId = networkId
} )
}
override suspend fun createAccount( override suspend fun createAccount(
seed: ByteArray, seed: ByteArray,
@ -108,15 +112,17 @@ class RustBackend private constructor(
} }
} }
override suspend fun getMemoAsUtf8(txId: ByteArray, outputIndex: Int) = override suspend fun getMemoAsUtf8(
withContext(SdkDispatchers.DATABASE_IO) { txId: ByteArray,
getMemoAsUtf8( outputIndex: Int
dataDbFile.absolutePath, ) = withContext(SdkDispatchers.DATABASE_IO) {
txId, getMemoAsUtf8(
outputIndex, dataDbFile.absolutePath,
networkId = networkId txId,
) outputIndex,
} networkId = networkId
)
}
override suspend fun writeBlockMetadata(blockMetadata: List<JniBlockMeta>) = override suspend fun writeBlockMetadata(blockMetadata: List<JniBlockMeta>) =
withContext(SdkDispatchers.DATABASE_IO) { withContext(SdkDispatchers.DATABASE_IO) {
@ -217,10 +223,11 @@ class RustBackend private constructor(
override suspend fun getFullyScannedHeight() = override suspend fun getFullyScannedHeight() =
withContext(SdkDispatchers.DATABASE_IO) { withContext(SdkDispatchers.DATABASE_IO) {
val height = getFullyScannedHeight( val height =
dataDbFile.absolutePath, getFullyScannedHeight(
networkId = networkId dataDbFile.absolutePath,
) networkId = networkId
)
if (-1L == height) { if (-1L == height) {
null null
@ -231,10 +238,11 @@ class RustBackend private constructor(
override suspend fun getMaxScannedHeight() = override suspend fun getMaxScannedHeight() =
withContext(SdkDispatchers.DATABASE_IO) { withContext(SdkDispatchers.DATABASE_IO) {
val height = getMaxScannedHeight( val height =
dataDbFile.absolutePath, getMaxScannedHeight(
networkId = networkId dataDbFile.absolutePath,
) networkId = networkId
)
if (-1L == height) { if (-1L == height) {
null null
@ -260,7 +268,10 @@ class RustBackend private constructor(
} }
} }
override suspend fun scanBlocks(fromHeight: Long, limit: Long) { override suspend fun scanBlocks(
fromHeight: Long,
limit: Long
) {
return withContext(SdkDispatchers.DATABASE_IO) { return withContext(SdkDispatchers.DATABASE_IO) {
scanBlocks( scanBlocks(
fsBlockDbRoot.absolutePath, fsBlockDbRoot.absolutePath,
@ -287,19 +298,20 @@ class RustBackend private constructor(
to: String, to: String,
value: Long, value: Long,
memo: ByteArray? memo: ByteArray?
): ByteArray = withContext(SdkDispatchers.DATABASE_IO) { ): ByteArray =
createToAddress( withContext(SdkDispatchers.DATABASE_IO) {
dataDbFile.absolutePath, createToAddress(
unifiedSpendingKey, dataDbFile.absolutePath,
to, unifiedSpendingKey,
value, to,
memo ?: ByteArray(0), value,
spendParamsPath = saplingSpendFile.absolutePath, memo ?: ByteArray(0),
outputParamsPath = saplingOutputFile.absolutePath, spendParamsPath = saplingSpendFile.absolutePath,
networkId = networkId, outputParamsPath = saplingOutputFile.absolutePath,
useZip317Fees = IS_USE_ZIP_317_FEES networkId = networkId,
) useZip317Fees = IS_USE_ZIP_317_FEES
} )
}
override suspend fun shieldToAddress( override suspend fun shieldToAddress(
account: Int, account: Int,
@ -339,17 +351,13 @@ class RustBackend private constructor(
) )
} }
override fun isValidShieldedAddr(addr: String) = override fun isValidShieldedAddr(addr: String) = isValidShieldedAddress(addr, networkId = networkId)
isValidShieldedAddress(addr, networkId = networkId)
override fun isValidTransparentAddr(addr: String) = override fun isValidTransparentAddr(addr: String) = isValidTransparentAddress(addr, networkId = networkId)
isValidTransparentAddress(addr, networkId = networkId)
override fun isValidUnifiedAddr(addr: String) = override fun isValidUnifiedAddr(addr: String) = isValidUnifiedAddress(addr, networkId = networkId)
isValidUnifiedAddress(addr, networkId = networkId)
override fun getBranchIdForHeight(height: Long): Long = override fun getBranchIdForHeight(height: Long): Long = branchIdForHeight(height, networkId = networkId)
branchIdForHeight(height, networkId = networkId)
/** /**
* Exposes all of the librustzcash functions along with helpers for loading the static library. * Exposes all of the librustzcash functions along with helpers for loading the static library.
@ -397,7 +405,11 @@ class RustBackend private constructor(
private external fun initBlockMetaDb(fsBlockDbRoot: String): Int private external fun initBlockMetaDb(fsBlockDbRoot: String): Int
@JvmStatic @JvmStatic
private external fun initDataDb(dbDataPath: String, seed: ByteArray?, networkId: Int): Int private external fun initDataDb(
dbDataPath: String,
seed: ByteArray?,
networkId: Int
): Int
@JvmStatic @JvmStatic
private external fun createAccount( private external fun createAccount(
@ -422,7 +434,11 @@ class RustBackend private constructor(
private external fun getSaplingReceiverForUnifiedAddress(ua: String): String? private external fun getSaplingReceiverForUnifiedAddress(ua: String): String?
@JvmStatic @JvmStatic
private external fun listTransparentReceivers(dbDataPath: String, account: Int, networkId: Int): Array<String> private external fun listTransparentReceivers(
dbDataPath: String,
account: Int,
networkId: Int
): Array<String>
fun validateUnifiedSpendingKey(bytes: ByteArray) = isValidSpendingKey(bytes) fun validateUnifiedSpendingKey(bytes: ByteArray) = isValidSpendingKey(bytes)
@ -430,13 +446,22 @@ class RustBackend private constructor(
private external fun isValidSpendingKey(bytes: ByteArray): Boolean private external fun isValidSpendingKey(bytes: ByteArray): Boolean
@JvmStatic @JvmStatic
private external fun isValidShieldedAddress(addr: String, networkId: Int): Boolean private external fun isValidShieldedAddress(
addr: String,
networkId: Int
): Boolean
@JvmStatic @JvmStatic
private external fun isValidTransparentAddress(addr: String, networkId: Int): Boolean private external fun isValidTransparentAddress(
addr: String,
networkId: Int
): Boolean
@JvmStatic @JvmStatic
private external fun isValidUnifiedAddress(addr: String, networkId: Int): Boolean private external fun isValidUnifiedAddress(
addr: String,
networkId: Int
): Boolean
@JvmStatic @JvmStatic
private external fun getMemoAsUtf8( private external fun getMemoAsUtf8(
@ -563,7 +588,10 @@ class RustBackend private constructor(
): ByteArray ): ByteArray
@JvmStatic @JvmStatic
private external fun branchIdForHeight(height: Long, networkId: Int): Long private external fun branchIdForHeight(
height: Long,
networkId: Int
): Long
@JvmStatic @JvmStatic
@Suppress("LongParameterList") @Suppress("LongParameterList")

View File

@ -4,19 +4,16 @@ import cash.z.ecc.android.sdk.internal.Derivation
import cash.z.ecc.android.sdk.internal.model.JniUnifiedSpendingKey import cash.z.ecc.android.sdk.internal.model.JniUnifiedSpendingKey
class RustDerivationTool private constructor() : Derivation { class RustDerivationTool private constructor() : Derivation {
override fun deriveUnifiedFullViewingKeys( override fun deriveUnifiedFullViewingKeys(
seed: ByteArray, seed: ByteArray,
networkId: Int, networkId: Int,
numberOfAccounts: Int numberOfAccounts: Int
): Array<String> = ): Array<String> = deriveUnifiedFullViewingKeysFromSeed(seed, numberOfAccounts, networkId = networkId)
deriveUnifiedFullViewingKeysFromSeed(seed, numberOfAccounts, networkId = networkId)
override fun deriveUnifiedFullViewingKey( override fun deriveUnifiedFullViewingKey(
usk: JniUnifiedSpendingKey, usk: JniUnifiedSpendingKey,
networkId: Int networkId: Int
): String = ): String = deriveUnifiedFullViewingKey(usk.bytes, networkId = networkId)
deriveUnifiedFullViewingKey(usk.bytes, networkId = networkId)
override fun deriveUnifiedSpendingKey( override fun deriveUnifiedSpendingKey(
seed: ByteArray, seed: ByteArray,
@ -24,8 +21,11 @@ class RustDerivationTool private constructor() : Derivation {
accountIndex: Int accountIndex: Int
): JniUnifiedSpendingKey = deriveSpendingKey(seed, accountIndex, networkId = networkId) ): JniUnifiedSpendingKey = deriveSpendingKey(seed, accountIndex, networkId = networkId)
override fun deriveUnifiedAddress(seed: ByteArray, networkId: Int, accountIndex: Int): String = override fun deriveUnifiedAddress(
deriveUnifiedAddressFromSeed(seed, accountIndex = accountIndex, networkId = networkId) seed: ByteArray,
networkId: Int,
accountIndex: Int
): String = deriveUnifiedAddressFromSeed(seed, accountIndex = accountIndex, networkId = networkId)
/** /**
* Given a Unified Full Viewing Key string, return the associated Unified Address. * Given a Unified Full Viewing Key string, return the associated Unified Address.
@ -38,8 +38,7 @@ class RustDerivationTool private constructor() : Derivation {
override fun deriveUnifiedAddress( override fun deriveUnifiedAddress(
viewingKey: String, viewingKey: String,
networkId: Int networkId: Int
): String = ): String = deriveUnifiedAddressFromViewingKey(viewingKey, networkId = networkId)
deriveUnifiedAddressFromViewingKey(viewingKey, networkId = networkId)
companion object { companion object {
suspend fun new(): Derivation { suspend fun new(): Derivation {
@ -63,7 +62,10 @@ class RustDerivationTool private constructor() : Derivation {
): Array<String> ): Array<String>
@JvmStatic @JvmStatic
private external fun deriveUnifiedFullViewingKey(usk: ByteArray, networkId: Int): String private external fun deriveUnifiedFullViewingKey(
usk: ByteArray,
networkId: Int
): String
@JvmStatic @JvmStatic
private external fun deriveUnifiedAddressFromSeed( private external fun deriveUnifiedAddressFromSeed(
@ -73,6 +75,9 @@ class RustDerivationTool private constructor() : Derivation {
): String ): String
@JvmStatic @JvmStatic
private external fun deriveUnifiedAddressFromViewingKey(key: String, networkId: Int): String private external fun deriveUnifiedAddressFromViewingKey(
key: String,
networkId: Int
): String
} }
} }

View File

@ -24,7 +24,10 @@ class JniSubtreeRoot(
} }
companion object { companion object {
fun new(rootHash: ByteArray, completingBlockHeight: Long): JniSubtreeRoot { fun new(
rootHash: ByteArray,
completingBlockHeight: Long
): JniSubtreeRoot {
return JniSubtreeRoot( return JniSubtreeRoot(
rootHash = rootHash, rootHash = rootHash,
completingBlockHeight = completingBlockHeight completingBlockHeight = completingBlockHeight

View File

@ -24,7 +24,6 @@ class JniUnifiedSpendingKey(
*/ */
val bytes: ByteArray val bytes: ByteArray
) { ) {
// Override to prevent leaking key to logs // Override to prevent leaking key to logs
override fun toString() = "JniUnifiedSpendingKey(account=$account, bytes=***)" override fun toString() = "JniUnifiedSpendingKey(account=$account, bytes=***)"

View File

@ -3,7 +3,6 @@ package cash.z.ecc.android.sdk.internal.fixture
import cash.z.ecc.android.sdk.internal.model.JniAccountBalance import cash.z.ecc.android.sdk.internal.model.JniAccountBalance
object JniAccountBalanceFixture { object JniAccountBalanceFixture {
const val ACCOUNT_ID: Int = 0 const val ACCOUNT_ID: Int = 0
const val SAPLING_TOTAL_BALANCE: Long = 0L const val SAPLING_TOTAL_BALANCE: Long = 0L
const val SAPLING_VERIFIED_BALANCE: Long = 0L const val SAPLING_VERIFIED_BALANCE: Long = 0L
@ -12,7 +11,6 @@ object JniAccountBalanceFixture {
account: Int = ACCOUNT_ID, account: Int = ACCOUNT_ID,
saplingTotalBalance: Long = SAPLING_TOTAL_BALANCE, saplingTotalBalance: Long = SAPLING_TOTAL_BALANCE,
saplingVerifiedBalance: Long = SAPLING_VERIFIED_BALANCE, saplingVerifiedBalance: Long = SAPLING_VERIFIED_BALANCE,
) = JniAccountBalance( ) = JniAccountBalance(
account = account, account = account,
saplingTotalBalance = saplingTotalBalance, saplingTotalBalance = saplingTotalBalance,

View File

@ -7,13 +7,14 @@ import kotlin.test.assertIs
class JniBlockMetaTest { class JniBlockMetaTest {
@Test @Test
fun attributes_within_constraints() { fun attributes_within_constraints() {
val instance = JniBlockMeta( val instance =
height = UInt.MAX_VALUE.toLong(), JniBlockMeta(
hash = byteArrayOf(), height = UInt.MAX_VALUE.toLong(),
time = 0L, hash = byteArrayOf(),
saplingOutputsCount = UInt.MIN_VALUE.toLong(), time = 0L,
orchardOutputsCount = UInt.MIN_VALUE.toLong() saplingOutputsCount = UInt.MIN_VALUE.toLong(),
) orchardOutputsCount = UInt.MIN_VALUE.toLong()
)
assertIs<JniBlockMeta>(instance) assertIs<JniBlockMeta>(instance)
} }

View File

@ -7,11 +7,12 @@ import kotlin.test.assertIs
class JniScanRangeTest { class JniScanRangeTest {
@Test @Test
fun attributes_within_constraints() { fun attributes_within_constraints() {
val instance = JniScanRange( val instance =
startHeight = UInt.MIN_VALUE.toLong(), JniScanRange(
endHeight = UInt.MAX_VALUE.toLong(), startHeight = UInt.MIN_VALUE.toLong(),
priority = 10 endHeight = UInt.MAX_VALUE.toLong(),
) priority = 10
)
assertIs<JniScanRange>(instance) assertIs<JniScanRange>(instance)
} }

View File

@ -7,10 +7,11 @@ import kotlin.test.assertIs
class JniSubtreeRootTest { class JniSubtreeRootTest {
@Test @Test
fun attributes_within_constraints() { fun attributes_within_constraints() {
val instance = JniSubtreeRoot( val instance =
rootHash = byteArrayOf(), JniSubtreeRoot(
completingBlockHeight = UInt.MAX_VALUE.toLong() rootHash = byteArrayOf(),
) completingBlockHeight = UInt.MAX_VALUE.toLong()
)
assertIs<JniSubtreeRoot>(instance) assertIs<JniSubtreeRoot>(instance)
} }

View File

@ -8,11 +8,12 @@ import kotlin.test.assertIs
class JniWalletSummaryTest { class JniWalletSummaryTest {
@Test @Test
fun both_attribute_within_constraints() { fun both_attribute_within_constraints() {
val instance = JniWalletSummary( val instance =
accountBalances = arrayOf(JniAccountBalanceFixture.new()), JniWalletSummary(
progressNumerator = 1L, accountBalances = arrayOf(JniAccountBalanceFixture.new()),
progressDenominator = 100L progressNumerator = 1L,
) progressDenominator = 100L
)
assertIs<JniWalletSummary>(instance) assertIs<JniWalletSummary>(instance)
} }

View File

@ -5,7 +5,7 @@ plugins {
val ktlint by configurations.creating val ktlint by configurations.creating
dependencies { dependencies {
ktlint("com.pinterest:ktlint:${project.property("KTLINT_VERSION")}") { ktlint("com.pinterest.ktlint:ktlint-cli:${project.property("KTLINT_VERSION")}") {
attributes { attributes {
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named<Bundling>(Bundling.EXTERNAL)) attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named<Bundling>(Bundling.EXTERNAL))
} }

View File

@ -7,17 +7,19 @@ import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
// TODO [#1224]: Refactor and re-enable disabled darkside tests
// TODO [#1224]: https://github.com/zcash/zcash-android-wallet-sdk/issues/1224
/** /**
* Integration test to run in order to catch any regressions in transparent behavior. * Integration test to run in order to catch any regressions in transparent behavior.
*/ */
// TODO [#1224]: Refactor and re-enable disabled darkside tests
// TODO [#1224]: https://github.com/zcash/zcash-android-wallet-sdk/issues/1224
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class TransparentIntegrationTest : DarksideTest() { class TransparentIntegrationTest : DarksideTest() {
@Before @Before
fun setup() = runOnce { fun setup() =
// sithLord.await() runOnce {
} // sithLord.await()
}
@Test @Test
@Ignore("This test is broken") @Ignore("This test is broken")

View File

@ -14,7 +14,6 @@ import org.junit.runner.RunWith
// TODO [#1224]: https://github.com/zcash/zcash-android-wallet-sdk/issues/1224 // TODO [#1224]: https://github.com/zcash/zcash-android-wallet-sdk/issues/1224
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class InboundTxTests : ScopedTest() { class InboundTxTests : ScopedTest() {
@Test @Test
@Ignore("Temporarily disabled") @Ignore("Temporarily disabled")
fun testTargetBlock_synced() { fun testTargetBlock_synced() {
@ -35,12 +34,15 @@ class InboundTxTests : ScopedTest() {
@Ignore("Temporarily disabled") @Ignore("Temporarily disabled")
fun testTxCountAfter() { fun testTxCountAfter() {
// add 2 transactions to block 663188 and 'mine' that block // add 2 transactions to block 663188 and 'mine' that block
addTransactions(targetTxBlock, tx663174, tx663188) addTransactions(targetTxBlock, TX_663174, TX_663188)
// sithLord.await(timeout = 30_000L, targetHeight = targetTxBlock) // sithLord.await(timeout = 30_000L, targetHeight = targetTxBlock)
validator.validateTxCount(2) validator.validateTxCount(2)
} }
private fun addTransactions(targetHeight: BlockHeight, vararg txs: String) { private fun addTransactions(
targetHeight: BlockHeight,
vararg txs: String
) {
// val overwriteBlockCount = 5 // val overwriteBlockCount = 5
chainMaker chainMaker
// .stageEmptyBlocks(targetHeight, overwriteBlockCount) // .stageEmptyBlocks(targetHeight, overwriteBlockCount)
@ -48,40 +50,42 @@ class InboundTxTests : ScopedTest() {
.applyTipHeight(targetHeight) .applyTipHeight(targetHeight)
} }
@Suppress("MaxLineLength") @Suppress("MaxLineLength", "UnusedPrivateProperty", "ktlint:standard:max-line-length")
companion object { companion object {
private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt" private const val BLOCKS_URL = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"
private const val tx663174 = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/0821a89be7f2fc1311792c3fa1dd2171a8cdfb2effd98590cbd5ebcdcfcf491f.txt" private const val TX_663174 = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/0821a89be7f2fc1311792c3fa1dd2171a8cdfb2effd98590cbd5ebcdcfcf491f.txt"
private const val tx663188 = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/15a677b6770c5505fb47439361d3d3a7c21238ee1a6874fdedad18ae96850590.txt" private const val TX_663188 = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/15a677b6770c5505fb47439361d3d3a7c21238ee1a6874fdedad18ae96850590.txt"
private const val txIndexReorg = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/tx-index-reorg/t1.txt" private const val TX_INDEX_REORG = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/tx-index-reorg/t1.txt"
private val txSend = arrayOf( private val txSend =
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/t-shielded-spend.txt", arrayOf(
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/c9e35e6ff444b071d63bf9bab6480409d6361760445c8a28d24179adb35c2495.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/t-shielded-spend.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/72a29d7db511025da969418880b749f7fc0fc910cdb06f52193b5fa5c0401d9d.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/c9e35e6ff444b071d63bf9bab6480409d6361760445c8a28d24179adb35c2495.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/ff6ea36765dc29793775c7aa71de19fca039c5b5b873a0497866e9c4bc48af01.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/72a29d7db511025da969418880b749f7fc0fc910cdb06f52193b5fa5c0401d9d.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/34e507cab780546f980176f3ff2695cd404917508c7e5ee18cc1d2ff3858cb08.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/ff6ea36765dc29793775c7aa71de19fca039c5b5b873a0497866e9c4bc48af01.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/6edf869063eccff3345676b0fed9f1aa6988fb2524e3d9ca7420a13cfadcd76c.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/34e507cab780546f980176f3ff2695cd404917508c7e5ee18cc1d2ff3858cb08.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/de97394ae220c28a33ba78b944e82dabec8cb404a4407650b134b3d5950358c0.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/6edf869063eccff3345676b0fed9f1aa6988fb2524e3d9ca7420a13cfadcd76c.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/4eaa902279f8380914baf5bcc470d8b7c11d84fda809f67f517a7cb48912b87b.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/de97394ae220c28a33ba78b944e82dabec8cb404a4407650b134b3d5950358c0.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/73c5edf8ffba774d99155121ccf07e67fbcf14284458f7e732751fea60d3bcbc.txt" "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/4eaa902279f8380914baf5bcc470d8b7c11d84fda809f67f517a7cb48912b87b.txt",
) "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/73c5edf8ffba774d99155121ccf07e67fbcf14284458f7e732751fea60d3bcbc.txt"
)
private val txRecv = arrayOf( private val txRecv =
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/8f064d23c66dc36e32445e5f3b50e0f32ac3ddb78cff21fb521eb6c19c07c99a.txt", arrayOf(
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/15a677b6770c5505fb47439361d3d3a7c21238ee1a6874fdedad18ae96850590.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/8f064d23c66dc36e32445e5f3b50e0f32ac3ddb78cff21fb521eb6c19c07c99a.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/d2e7be14bbb308f9d4d68de424d622cbf774226d01cd63cc6f155fafd5cd212c.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/15a677b6770c5505fb47439361d3d3a7c21238ee1a6874fdedad18ae96850590.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/e6566be3a4f9a80035dab8e1d97e40832a639e3ea938fb7972ea2f8482ff51ce.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/d2e7be14bbb308f9d4d68de424d622cbf774226d01cd63cc6f155fafd5cd212c.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/0821a89be7f2fc1311792c3fa1dd2171a8cdfb2effd98590cbd5ebcdcfcf491f.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/e6566be3a4f9a80035dab8e1d97e40832a639e3ea938fb7972ea2f8482ff51ce.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/e9527891b5d43d1ac72f2c0a3ac18a33dc5a0529aec04fa600616ed35f8123f8.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/0821a89be7f2fc1311792c3fa1dd2171a8cdfb2effd98590cbd5ebcdcfcf491f.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/4dcc95dd0a2f1f51bd64bb9f729b423c6de1690664a1b6614c75925e781662f7.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/e9527891b5d43d1ac72f2c0a3ac18a33dc5a0529aec04fa600616ed35f8123f8.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/75f2cdd2ff6a94535326abb5d9e663d53cbfa5f31ebb24b4d7e420e9440d41a2.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/4dcc95dd0a2f1f51bd64bb9f729b423c6de1690664a1b6614c75925e781662f7.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/7690c8ec740c1be3c50e2aedae8bf907ac81141ae8b6a134c1811706c73f49a6.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/75f2cdd2ff6a94535326abb5d9e663d53cbfa5f31ebb24b4d7e420e9440d41a2.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/71935e29127a7de0b96081f4c8a42a9c11584d83adedfaab414362a6f3d965cf.txt" "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/7690c8ec740c1be3c50e2aedae8bf907ac81141ae8b6a134c1811706c73f49a6.txt",
) "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/71935e29127a7de0b96081f4c8a42a9c11584d83adedfaab414362a6f3d965cf.txt"
)
private val firstBlock = BlockHeight.new(ZcashNetwork.Mainnet, 663150L) private val firstBlock = BlockHeight.new(ZcashNetwork.Mainnet, 663150L)
private val targetTxBlock = BlockHeight.new(ZcashNetwork.Mainnet, 663188L) private val targetTxBlock = BlockHeight.new(ZcashNetwork.Mainnet, 663188L)
private const val lastBlockHash = "2fc7b4682f5ba6ba6f86e170b40f0aa9302e1d3becb2a6ee0db611ff87835e4a" private const val LAST_BLOCK_HASH = "2fc7b4682f5ba6ba6f86e170b40f0aa9302e1d3becb2a6ee0db611ff87835e4a"
private val sithLord = DarksideTestCoordinator() private val sithLord = DarksideTestCoordinator()
private val validator = sithLord.validator private val validator = sithLord.validator
private val chainMaker = sithLord.chainMaker private val chainMaker = sithLord.chainMaker
@ -92,7 +96,7 @@ class InboundTxTests : ScopedTest() {
sithLord.enterTheDarkside() sithLord.enterTheDarkside()
chainMaker chainMaker
.resetBlocks(blocksUrl, startHeight = firstBlock, tipHeight = targetTxBlock) .resetBlocks(BLOCKS_URL, startHeight = firstBlock, tipHeight = targetTxBlock)
.stageEmptyBlocks(firstBlock + 1, 100) .stageEmptyBlocks(firstBlock + 1, 100)
.applyTipHeight(BlockHeight.new(ZcashNetwork.Mainnet, targetTxBlock.value - 1)) .applyTipHeight(BlockHeight.new(ZcashNetwork.Mainnet, targetTxBlock.value - 1))

View File

@ -13,7 +13,6 @@ import org.junit.runner.RunWith
// TODO [#1224]: https://github.com/zcash/zcash-android-wallet-sdk/issues/1224 // TODO [#1224]: https://github.com/zcash/zcash-android-wallet-sdk/issues/1224
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ReorgSetupTest : ScopedTest() { class ReorgSetupTest : ScopedTest() {
/* /*
private val birthdayHeight = BlockHeight.new(ZcashNetwork.Mainnet, 663150) private val birthdayHeight = BlockHeight.new(ZcashNetwork.Mainnet, 663150)
private val targetHeight = BlockHeight.new(ZcashNetwork.Mainnet, 663250) private val targetHeight = BlockHeight.new(ZcashNetwork.Mainnet, 663250)
@ -26,20 +25,21 @@ class ReorgSetupTest : ScopedTest() {
@Test @Test
@Ignore("Temporarily disabled") @Ignore("Temporarily disabled")
fun testBeforeReorg_minHeight() = timeout(30_000L) { fun testBeforeReorg_minHeight() =
// validate that we are synced, at least to the birthday height timeout(30_000L) {
// validator.validateMinHeightSynced(birthdayHeight) // validate that we are synced, at least to the birthday height
} // validator.validateMinHeightSynced(birthdayHeight)
}
@Test @Test
@Ignore("Temporarily disabled") @Ignore("Temporarily disabled")
fun testBeforeReorg_maxHeight() = timeout(30_000L) { fun testBeforeReorg_maxHeight() =
// validate that we are not synced beyond the target height timeout(30_000L) {
// validator.validateMaxHeightSynced(targetHeight) // validate that we are not synced beyond the target height
} // validator.validateMaxHeightSynced(targetHeight)
}
companion object { companion object {
private val sithLord = DarksideTestCoordinator() private val sithLord = DarksideTestCoordinator()
private val validator = sithLord.validator private val validator = sithLord.validator

View File

@ -12,7 +12,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ReorgSmallTest : ScopedTest() { class ReorgSmallTest : ScopedTest() {
/* /*
private val targetHeight = BlockHeight.new( private val targetHeight = BlockHeight.new(
ZcashNetwork.Mainnet, ZcashNetwork.Mainnet,
@ -29,27 +28,29 @@ class ReorgSmallTest : ScopedTest() {
@Test @Test
@Ignore("Temporarily disabled") @Ignore("Temporarily disabled")
fun testBeforeReorg_latestBlockHash() = timeout(30_000L) { fun testBeforeReorg_latestBlockHash() =
// validator.validateBlockHash(targetHeight, hashBeforeReorg) timeout(30_000L) {
} // validator.validateBlockHash(targetHeight, hashBeforeReorg)
}
@Test @Test
@Ignore("Temporarily disabled") @Ignore("Temporarily disabled")
fun testAfterReorg_callbackTriggered() = timeout(30_000L) { fun testAfterReorg_callbackTriggered() =
hadReorg = false timeout(30_000L) {
hadReorg = false
// sithLord.triggerSmallReorg() // sithLord.triggerSmallReorg()
// sithLord.await() // sithLord.await()
assertTrue(hadReorg) assertTrue(hadReorg)
} }
@Test @Test
@Ignore("Temporarily disabled") @Ignore("Temporarily disabled")
fun testAfterReorg_latestBlockHash() = timeout(30_000L) { fun testAfterReorg_latestBlockHash() =
// validator.validateBlockHash(targetHeight, hashAfterReorg) timeout(30_000L) {
} // validator.validateBlockHash(targetHeight, hashAfterReorg)
}
companion object { companion object {
private val sithLord = DarksideTestCoordinator() private val sithLord = DarksideTestCoordinator()
private val validator = sithLord.validator private val validator = sithLord.validator
private var hadReorg = false private var hadReorg = false

View File

@ -12,8 +12,7 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class SetupTest : ScopedTest() { class SetupTest : ScopedTest() {
// @Test
// @Test
// fun testFirstBlockExists() { // fun testFirstBlockExists() {
// validator.validateHasBlock( // validator.validateHasBlock(
// firstBlock // firstBlock
@ -38,8 +37,9 @@ class SetupTest : ScopedTest() {
@Test @Test
@Ignore("This test is broken") @Ignore("This test is broken")
fun tempTest() { fun tempTest() {
val phrase = "still champion voice habit trend flight survey between bitter process artefact blind" + val phrase =
" carbon truly provide dizzy crush flush breeze blouse charge solid fish spread" "still champion voice habit trend flight survey between bitter process artefact blind" +
" carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
val result = SimpleMnemonics().toSeed(phrase.toCharArray()).toHex() val result = SimpleMnemonics().toSeed(phrase.toCharArray()).toHex()
assertEquals("abc", result) assertEquals("abc", result)
} }
@ -54,12 +54,12 @@ class SetupTest : ScopedTest() {
assertEquals("a", "${ent.toHex()}|${String(phrase)}") assertEquals("a", "${ent.toHex()}|${String(phrase)}")
} }
@Suppress("MaxLineLength", "UnusedPrivateProperty", "ktlint:standard:max-line-length")
companion object { companion object {
@Suppress("MaxLineLength") private const val BLOCKS_URL = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"
private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt" private const val FIRST_BLOCK = 663150
private const val firstBlock = 663150 private const val LAST_BLOCK = 663200
private const val lastBlock = 663200 private const val LAST_BLOCK_HASH = "2fc7b4682f5ba6ba6f86e170b40f0aa9302e1d3becb2a6ee0db611ff87835e4a"
private const val lastBlockHash = "2fc7b4682f5ba6ba6f86e170b40f0aa9302e1d3becb2a6ee0db611ff87835e4a"
private val sithLord = DarksideTestCoordinator() private val sithLord = DarksideTestCoordinator()
private val validator = sithLord.validator private val validator = sithLord.validator

View File

@ -11,6 +11,7 @@ open class DarksideTest : ScopedTest() {
ranOnce = true ranOnce = true
} }
} }
companion object { companion object {
private var ranOnce = false private var ranOnce = false
} }

View File

@ -49,28 +49,29 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
/** /**
* Setup dependencies, including the synchronizer and the darkside API connection * Setup dependencies, including the synchronizer and the darkside API connection
*/ */
fun enterTheDarkside(): DarksideTestCoordinator = runBlocking { fun enterTheDarkside(): DarksideTestCoordinator =
// verify that we are on the darkside runBlocking {
try { // verify that we are on the darkside
initiate() try {
// In the future, we may want to have the SDK internally verify being on the darkside by matching the initiate()
// network type // In the future, we may want to have the SDK internally verify being on the darkside by matching the
// network type
// synchronizer.getServerInfo().apply { // synchronizer.getServerInfo().apply {
// assertTrue( // assertTrue(
// "Error: not on the darkside", // "Error: not on the darkside",
// vendor.contains("dark", true) // vendor.contains("dark", true)
// or chainName.contains("dark", true) // or chainName.contains("dark", true)
// ) // )
// } // }
} catch (error: StatusRuntimeException) { } catch (error: StatusRuntimeException) {
Assert.fail( Assert.fail(
"Error while fetching server status. Testing cannot begin due to:" + "Error while fetching server status. Testing cannot begin due to:" +
" ${error.message} Caused by: ${error.cause} Verify that the server is running!" " ${error.message} Caused by: ${error.cause} Verify that the server is running!"
) )
}
this@DarksideTestCoordinator
} }
this@DarksideTestCoordinator
}
/** /**
* Setup the synchronizer and darksidewalletd with their initial state * Setup the synchronizer and darksidewalletd with their initial state
@ -80,7 +81,7 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
darkside.reset(BlockHeightUnsafe(wallet.network.saplingActivationHeight.value)) darkside.reset(BlockHeightUnsafe(wallet.network.saplingActivationHeight.value))
} }
// fun triggerSmallReorg() { // fun triggerSmallReorg() {
// darkside.setBlocksUrl(smallReorg) // darkside.setBlocksUrl(smallReorg)
// } // }
// //
@ -89,6 +90,7 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
// } // }
// redo this as a call to wallet but add delay time to wallet join() function // redo this as a call to wallet but add delay time to wallet join() function
/** /**
* Waits for, at most, the given amount of time for the synchronizer to download and scan blocks * Waits for, at most, the given amount of time for the synchronizer to download and scan blocks
* and reach a 'SYNCED' status. * and reach a 'SYNCED' status.
@ -125,25 +127,27 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
// wallet.send(toAddress, memo, zatoshi, fromAccountIndex) // wallet.send(toAddress, memo, zatoshi, fromAccountIndex)
// } // }
fun stall(delay: Long = 5000L) = runBlocking { @Suppress("ktlint:standard:no-consecutive-comments")
delay(delay) fun stall(delay: Long = 5000L) =
} runBlocking {
delay(delay)
}
// //
// Validation // Validation
// //
inner class DarksideTestValidator { inner class DarksideTestValidator {
fun validateLatestHeight(height: BlockHeight) =
fun validateLatestHeight(height: BlockHeight) = runBlocking<Unit> { runBlocking<Unit> {
val info = synchronizer.processorInfo.first() val info = synchronizer.processorInfo.first()
val networkBlockHeight = info.networkBlockHeight val networkBlockHeight = info.networkBlockHeight
assertTrue( assertTrue(
"Expected latestHeight of $height but the server last reported a height of" + "Expected latestHeight of $height but the server last reported a height of" +
" $networkBlockHeight! Full details: $info", " $networkBlockHeight! Full details: $info",
networkBlockHeight == height networkBlockHeight == height
) )
} }
/* /*
fun validateMinHeightSynced(minHeight: BlockHeight) = runBlocking<Unit> { fun validateMinHeightSynced(minHeight: BlockHeight) = runBlocking<Unit> {
@ -182,7 +186,10 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
assertEquals("Expected $count transactions but found $txCount instead!", count, txCount) assertEquals("Expected $count transactions but found $txCount instead!", count, txCount)
} }
fun validateMinBalance(available: Long = -1, total: Long = -1) { fun validateMinBalance(
available: Long = -1,
total: Long = -1
) {
val balance = synchronizer.saplingBalances.value val balance = synchronizer.saplingBalances.value
if (available > 0) { if (available > 0) {
assertTrue( assertTrue(
@ -198,7 +205,11 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
} }
} }
suspend fun validateBalance(available: Long = -1, total: Long = -1, account: Account) { suspend fun validateBalance(
available: Long = -1,
total: Long = -1,
account: Account
) {
val balance = synchronizer.processor.getBalanceInfo(account) val balance = synchronizer.processor.getBalanceInfo(account)
if (available > 0) { if (available > 0) {
assertEquals("invalid available balance", available, balance.available) assertEquals("invalid available balance", available, balance.available)
@ -224,33 +235,47 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
blocksUrl: String, blocksUrl: String,
startHeight: BlockHeight = DEFAULT_START_HEIGHT, startHeight: BlockHeight = DEFAULT_START_HEIGHT,
tipHeight: BlockHeight = startHeight + 100 tipHeight: BlockHeight = startHeight + 100
): DarksideChainMaker = apply { ): DarksideChainMaker =
darkside apply {
.reset(BlockHeightUnsafe(startHeight.value)) darkside
.stageBlocks(blocksUrl) .reset(BlockHeightUnsafe(startHeight.value))
applyTipHeight(tipHeight) .stageBlocks(blocksUrl)
} applyTipHeight(tipHeight)
fun stageTransaction(url: String, targetHeight: BlockHeight): DarksideChainMaker = apply {
darkside.stageTransactions(url, BlockHeightUnsafe(targetHeight.value))
}
fun stageTransactions(targetHeight: BlockHeight, vararg urls: String): DarksideChainMaker = apply {
urls.forEach {
darkside.stageTransactions(it, BlockHeightUnsafe(targetHeight.value))
} }
}
fun stageEmptyBlocks(startHeight: BlockHeight, count: Int = 10): DarksideChainMaker = apply { fun stageTransaction(
darkside.stageEmptyBlocks(BlockHeightUnsafe(startHeight.value), count) url: String,
} targetHeight: BlockHeight
): DarksideChainMaker =
apply {
darkside.stageTransactions(url, BlockHeightUnsafe(targetHeight.value))
}
fun stageTransactions(
targetHeight: BlockHeight,
vararg urls: String
): DarksideChainMaker =
apply {
urls.forEach {
darkside.stageTransactions(it, BlockHeightUnsafe(targetHeight.value))
}
}
fun stageEmptyBlocks(
startHeight: BlockHeight,
count: Int = 10
): DarksideChainMaker =
apply {
darkside.stageEmptyBlocks(BlockHeightUnsafe(startHeight.value), count)
}
fun stageEmptyBlock() = stageEmptyBlocks(lastTipHeight!! + 1, 1) fun stageEmptyBlock() = stageEmptyBlocks(lastTipHeight!! + 1, 1)
fun applyTipHeight(tipHeight: BlockHeight): DarksideChainMaker = apply { fun applyTipHeight(tipHeight: BlockHeight): DarksideChainMaker =
darkside.applyBlocks(BlockHeightUnsafe(tipHeight.value)) apply {
lastTipHeight = tipHeight darkside.applyBlocks(BlockHeightUnsafe(tipHeight.value))
} lastTipHeight = tipHeight
}
/** /**
* Creates a chain with 100 blocks and a transaction in the middle. * Creates a chain with 100 blocks and a transaction in the middle.
@ -281,7 +306,7 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
} }
} }
@Suppress("MaxLineLength") @Suppress("MaxLineLength", "UnusedPrivateProperty")
companion object { companion object {
/** /**
* This is a special localhost value on the Android emulator, which allows it to contact * This is a special localhost value on the Android emulator, which allows it to contact
@ -290,14 +315,15 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
const val COMPUTER_LOCALHOST = "10.0.2.2" const val COMPUTER_LOCALHOST = "10.0.2.2"
// Block URLS // Block URLS
private const val beforeReorg = private const val BEFORE_REORG =
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt" "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"
private const val smallReorg = private const val SMALL_REORG =
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-small-reorg.txt" "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-small-reorg.txt"
private const val largeReorg = private const val LARGE_REORG =
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-large-reorg.txt" "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-large-reorg.txt"
private val DEFAULT_START_HEIGHT = BlockHeight.new(ZcashNetwork.Mainnet, 663150) private val DEFAULT_START_HEIGHT = BlockHeight.new(ZcashNetwork.Mainnet, 663150)
private const val DEFAULT_SEED_PHRASE = private const val DEFAULT_SEED_PHRASE =
"still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread" "still champion voice habit trend flight survey between bitter process artefact blind carbon truly" +
" provide dizzy crush flush breeze blouse charge solid fish spread"
} }
} }

View File

@ -39,15 +39,16 @@ open class DarksideTestPrerequisites {
* ApplicationInfo object (`BuildInfo` is useless for libraries.) * ApplicationInfo object (`BuildInfo` is useless for libraries.)
*/ */
private fun isDebuggable(context: Context): Boolean { private fun isDebuggable(context: Context): Boolean {
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val packageInfo =
context.packageManager.getPackageInfo( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.packageName, context.packageManager.getPackageInfo(
PackageManager.PackageInfoFlags.of(0L) context.packageName,
) PackageManager.PackageInfoFlags.of(0L)
} else { )
@Suppress("Deprecation") } else {
context.packageManager.getPackageInfo(context.packageName, 0) @Suppress("Deprecation")
} context.packageManager.getPackageInfo(context.packageName, 0)
}
// Normally shouldn't be null, but could be with a MockContext // Normally shouldn't be null, but could be with a MockContext
return packageInfo.applicationInfo?.let { return packageInfo.applicationInfo?.let {

View File

@ -26,21 +26,27 @@ open class ScopedTest(val defaultTimeout: Long = 2000L) : DarksideTestPrerequisi
@Before @Before
fun start() { fun start() {
testScope = CoroutineScope( testScope =
Job(classScope.coroutineContext[Job]!!) + newFixedThreadPoolContext( CoroutineScope(
5, Job(classScope.coroutineContext[Job]!!) +
this.javaClass.simpleName newFixedThreadPoolContext(
5,
this.javaClass.simpleName
)
) )
)
} }
@After @After
fun end() = runBlocking<Unit> { fun end() =
testScope.cancel() runBlocking<Unit> {
testScope.coroutineContext[Job]?.join() testScope.cancel()
} testScope.coroutineContext[Job]?.join()
}
fun timeout(duration: Long, block: suspend () -> Unit) = timeoutWith(testScope, duration, block) fun timeout(
duration: Long,
block: suspend () -> Unit
) = timeoutWith(testScope, duration, block)
companion object { companion object {
@JvmStatic @JvmStatic
@ -49,20 +55,26 @@ open class ScopedTest(val defaultTimeout: Long = 2000L) : DarksideTestPrerequisi
@BeforeClass @BeforeClass
@JvmStatic @JvmStatic
fun createScope() { fun createScope() {
classScope = CoroutineScope( classScope =
SupervisorJob() + newFixedThreadPoolContext(2, this::class.java.simpleName) CoroutineScope(
) SupervisorJob() + newFixedThreadPoolContext(2, this::class.java.simpleName)
)
} }
@AfterClass @AfterClass
@JvmStatic @JvmStatic
fun destroyScope() = runBlocking<Unit> { fun destroyScope() =
classScope.cancel() runBlocking<Unit> {
classScope.coroutineContext[Job]?.join() classScope.cancel()
} classScope.coroutineContext[Job]?.join()
}
@JvmStatic @JvmStatic
fun timeoutWith(scope: CoroutineScope, duration: Long, block: suspend () -> Unit) { fun timeoutWith(
scope: CoroutineScope,
duration: Long,
block: suspend () -> Unit
) {
scope.launch { scope.launch {
delay(duration) delay(duration)
val message = "ERROR: Test timed out after ${duration}ms" val message = "ERROR: Test timed out after ${duration}ms"

View File

@ -10,11 +10,18 @@ import java.util.Locale
class SimpleMnemonics : MnemonicPlugin { class SimpleMnemonics : MnemonicPlugin {
override fun fullWordList(languageCode: String) = Mnemonics.getCachedWords(Locale.ENGLISH.language) override fun fullWordList(languageCode: String) = Mnemonics.getCachedWords(Locale.ENGLISH.language)
override fun nextEntropy(): ByteArray = WordCount.COUNT_24.toEntropy() override fun nextEntropy(): ByteArray = WordCount.COUNT_24.toEntropy()
override fun nextMnemonic(): CharArray = MnemonicCode(WordCount.COUNT_24).chars override fun nextMnemonic(): CharArray = MnemonicCode(WordCount.COUNT_24).chars
override fun nextMnemonic(seed: ByteArray): CharArray = MnemonicCode(seed).chars override fun nextMnemonic(seed: ByteArray): CharArray = MnemonicCode(seed).chars
override fun nextMnemonicList(): List<CharArray> = MnemonicCode(WordCount.COUNT_24).words override fun nextMnemonicList(): List<CharArray> = MnemonicCode(WordCount.COUNT_24).words
override fun nextMnemonicList(seed: ByteArray): List<CharArray> = MnemonicCode(seed).words override fun nextMnemonicList(seed: ByteArray): List<CharArray> = MnemonicCode(seed).words
override fun toSeed(mnemonic: CharArray): ByteArray = MnemonicCode(mnemonic).toSeed() override fun toSeed(mnemonic: CharArray): ByteArray = MnemonicCode(mnemonic).toSeed()
override fun toWordList(mnemonic: CharArray): List<CharArray> = MnemonicCode(mnemonic).words override fun toWordList(mnemonic: CharArray): List<CharArray> = MnemonicCode(mnemonic).words
} }

View File

@ -48,9 +48,10 @@ class TestWallet(
alias = alias alias = alias
) )
val walletScope = CoroutineScope( val walletScope =
SupervisorJob() + newFixedThreadPoolContext(3, this.javaClass.simpleName) CoroutineScope(
) SupervisorJob() + newFixedThreadPoolContext(3, this.javaClass.simpleName)
)
// Although runBlocking isn't great, this usage is OK because this is only used within the // Although runBlocking isn't great, this usage is OK because this is only used within the
// automated tests // automated tests
@ -60,16 +61,17 @@ class TestWallet(
private val seed: ByteArray = Mnemonics.MnemonicCode(seedPhrase).toSeed() private val seed: ByteArray = Mnemonics.MnemonicCode(seedPhrase).toSeed()
private val shieldedSpendingKey = private val shieldedSpendingKey =
runBlocking { DerivationTool.getInstance().deriveUnifiedSpendingKey(seed, network = network, account) } runBlocking { DerivationTool.getInstance().deriveUnifiedSpendingKey(seed, network = network, account) }
val synchronizer: SdkSynchronizer = Synchronizer.newBlocking( val synchronizer: SdkSynchronizer =
context, Synchronizer.newBlocking(
network, context,
alias, network,
endpoint, alias,
seed, endpoint,
startHeight, seed,
// Using existing wallet init mode as simplification for the test startHeight,
walletInitMode = WalletInitMode.ExistingWallet // Using existing wallet init mode as simplification for the test
) as SdkSynchronizer walletInitMode = WalletInitMode.ExistingWallet
) as SdkSynchronizer
val available get() = synchronizer.saplingBalances.value?.available val available get() = synchronizer.saplingBalances.value?.available
val unifiedAddress = val unifiedAddress =
@ -85,12 +87,13 @@ class TestWallet(
} }
suspend fun sync(timeout: Long = -1): TestWallet { suspend fun sync(timeout: Long = -1): TestWallet {
val killSwitch = walletScope.launch { val killSwitch =
if (timeout > 0) { walletScope.launch {
delay(timeout) if (timeout > 0) {
throw TimeoutException("Failed to sync wallet within ${timeout}ms") delay(timeout)
throw TimeoutException("Failed to sync wallet within ${timeout}ms")
}
} }
}
// block until synced // block until synced
synchronizer.status.first { it == Synchronizer.Status.SYNCED } synchronizer.status.first { it == Synchronizer.Status.SYNCED }

View File

@ -20,6 +20,9 @@ import java.util.regex.Pattern
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
// TODO [#809]: Enable macrobenchmark on CI
// TODO [#809]: https://github.com/zcash/zcash-android-wallet-sdk/issues/809
/** /**
* Purpose of this class is to provide a basic startup measurements, and captured system traces for investigating the * Purpose of this class is to provide a basic startup measurements, and captured system traces for investigating the
* app's performance. It navigates to the device's home screen, and launches the default activity. * app's performance. It navigates to the device's home screen, and launches the default activity.
@ -29,11 +32,7 @@ import kotlin.time.Duration.Companion.seconds
* We ideally run this against a physical device with Android SDK level 29, at least, as profiling is provided by this * We ideally run this against a physical device with Android SDK level 29, at least, as profiling is provided by this
* version and later on. * version and later on.
*/ */
// TODO [#809]: Enable macrobenchmark on CI
// TODO [#809]: https://github.com/zcash/zcash-android-wallet-sdk/issues/809
class StartupBenchmark : UiTestPrerequisites() { class StartupBenchmark : UiTestPrerequisites() {
companion object { companion object {
private const val APP_TARGET_PACKAGE_NAME = "cash.z.ecc.android.sdk.demoapp.mainnet" // NON-NLS private const val APP_TARGET_PACKAGE_NAME = "cash.z.ecc.android.sdk.demoapp.mainnet" // NON-NLS
private const val APP_TARGET_ACTIVITY_NAME = "cash.z.ecc.android.sdk.demoapp.MainActivity" // NON-NLS private const val APP_TARGET_ACTIVITY_NAME = "cash.z.ecc.android.sdk.demoapp.MainActivity" // NON-NLS
@ -55,18 +54,19 @@ class StartupBenchmark : UiTestPrerequisites() {
* This test starts the Demo-app on Home screen and measures its metrics. * This test starts the Demo-app on Home screen and measures its metrics.
*/ */
@Test @Test
fun appStartup() = benchmarkRule.measureRepeated( fun appStartup() =
packageName = APP_TARGET_PACKAGE_NAME, benchmarkRule.measureRepeated(
metrics = listOf(StartupTimingMetric()), packageName = APP_TARGET_PACKAGE_NAME,
iterations = 5, metrics = listOf(StartupTimingMetric()),
startupMode = StartupMode.COLD, iterations = 5,
setupBlock = { startupMode = StartupMode.COLD,
// Press home button before each run to ensure the starting activity isn't visible setupBlock = {
pressHome() // Press home button before each run to ensure the starting activity isn't visible
pressHome()
}
) {
startLegacyActivityAndWait()
} }
) {
startLegacyActivityAndWait()
}
/** /**
* Advanced trace events startup test, which starts the Demo-app on the Home screen and then navigates to the * Advanced trace events startup test, which starts the Demo-app on the Home screen and then navigates to the
@ -74,24 +74,26 @@ class StartupBenchmark : UiTestPrerequisites() {
*/ */
@Test @Test
@OptIn(ExperimentalMetricApi::class) @OptIn(ExperimentalMetricApi::class)
fun tracesSdkStartup() = benchmarkRule.measureRepeated( fun tracesSdkStartup() =
packageName = APP_TARGET_PACKAGE_NAME, benchmarkRule.measureRepeated(
metrics = listOf( packageName = APP_TARGET_PACKAGE_NAME,
TraceSectionMetric(ADDRESS_SCREEN_SECTION, TraceSectionMetric.Mode.First, false), metrics =
TraceSectionMetric(UNIFIED_ADDRESS_SECTION, TraceSectionMetric.Mode.First, false), listOf(
TraceSectionMetric(SAPLING_ADDRESS_SECTION, TraceSectionMetric.Mode.First, false), TraceSectionMetric(ADDRESS_SCREEN_SECTION, TraceSectionMetric.Mode.First, false),
TraceSectionMetric(TRANSPARENT_ADDRESS_SECTION, TraceSectionMetric.Mode.First, false) TraceSectionMetric(UNIFIED_ADDRESS_SECTION, TraceSectionMetric.Mode.First, false),
), TraceSectionMetric(SAPLING_ADDRESS_SECTION, TraceSectionMetric.Mode.First, false),
compilationMode = CompilationMode.Full(), TraceSectionMetric(TRANSPARENT_ADDRESS_SECTION, TraceSectionMetric.Mode.First, false)
startupMode = StartupMode.COLD, ),
iterations = 5, compilationMode = CompilationMode.Full(),
measureBlock = { startupMode = StartupMode.COLD,
startLegacyActivityAndWait() iterations = 5,
gotoAddressScreen() measureBlock = {
waitForAddressScreen() startLegacyActivityAndWait()
closeAddressScreen() gotoAddressScreen()
} waitForAddressScreen()
) closeAddressScreen()
}
)
private fun MacrobenchmarkScope.closeAddressScreen() { private fun MacrobenchmarkScope.closeAddressScreen() {
// To close the Address screen and disconnect from SDK Synchronizer // To close the Address screen and disconnect from SDK Synchronizer
@ -109,7 +111,10 @@ class StartupBenchmark : UiTestPrerequisites() {
} }
} }
private fun MacrobenchmarkScope.waitForAddressAppear(addressPattern: Pattern, timeout: Duration): Boolean { private fun MacrobenchmarkScope.waitForAddressAppear(
addressPattern: Pattern,
timeout: Duration
): Boolean {
return device.waitFor(Until.hasObject(By.text(addressPattern)), timeout) return device.waitFor(Until.hasObject(By.text(addressPattern)), timeout)
} }
@ -125,9 +130,10 @@ class StartupBenchmark : UiTestPrerequisites() {
} }
private fun MacrobenchmarkScope.startLegacyActivityAndWait() { private fun MacrobenchmarkScope.startLegacyActivityAndWait() {
val intent = Intent(Intent.ACTION_MAIN).apply { val intent =
component = ComponentName(APP_TARGET_PACKAGE_NAME, APP_TARGET_ACTIVITY_NAME) Intent(Intent.ACTION_MAIN).apply {
} component = ComponentName(APP_TARGET_PACKAGE_NAME, APP_TARGET_ACTIVITY_NAME)
}
startActivityAndWait(intent) startActivityAndWait(intent)
} }

View File

@ -18,6 +18,9 @@ import org.junit.Test
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
// TODO [#809]: Enable macrobenchmark on CI
// TODO [#809]: https://github.com/zcash/zcash-android-wallet-sdk/issues/809
/** /**
* This benchmark class provides measurements and captured custom traces for investigating SDK syncing mechanisms * This benchmark class provides measurements and captured custom traces for investigating SDK syncing mechanisms
* with restricted blockchain range. It always resets the SDK before the next sync iteration. It uses UIAutomator to * with restricted blockchain range. It always resets the SDK before the next sync iteration. It uses UIAutomator to
@ -29,11 +32,7 @@ import kotlin.time.Duration.Companion.seconds
* We ideally run this on a physical device with Android SDK level 29, at least, as profiling is provided by this * We ideally run this on a physical device with Android SDK level 29, at least, as profiling is provided by this
* version and later on. * version and later on.
*/ */
// TODO [#809]: Enable macrobenchmark on CI
// TODO [#809]: https://github.com/zcash/zcash-android-wallet-sdk/issues/809
class SyncBlockchainBenchmark : UiTestPrerequisites() { class SyncBlockchainBenchmark : UiTestPrerequisites() {
companion object { companion object {
private const val APP_TARGET_PACKAGE_NAME = "cash.z.ecc.android.sdk.demoapp.mainnet" // NON-NLS private const val APP_TARGET_PACKAGE_NAME = "cash.z.ecc.android.sdk.demoapp.mainnet" // NON-NLS
private const val APP_TARGET_ACTIVITY_NAME = "cash.z.ecc.android.sdk.demoapp.MainActivity" // NON-NLS private const val APP_TARGET_ACTIVITY_NAME = "cash.z.ecc.android.sdk.demoapp.MainActivity" // NON-NLS
@ -54,26 +53,28 @@ class SyncBlockchainBenchmark : UiTestPrerequisites() {
*/ */
@Test @Test
@OptIn(ExperimentalMetricApi::class) @OptIn(ExperimentalMetricApi::class)
fun tracesSyncBlockchain() = benchmarkRule.measureRepeated( fun tracesSyncBlockchain() =
packageName = APP_TARGET_PACKAGE_NAME, benchmarkRule.measureRepeated(
metrics = listOf( packageName = APP_TARGET_PACKAGE_NAME,
TraceSectionMetric(BALANCE_SCREEN_SECTION, TraceSectionMetric.Mode.First, false), metrics =
TraceSectionMetric(BLOCKCHAIN_SYNC_SECTION, TraceSectionMetric.Mode.First, false), listOf(
TraceSectionMetric(DOWNLOAD_SECTION, TraceSectionMetric.Mode.First, false), TraceSectionMetric(BALANCE_SCREEN_SECTION, TraceSectionMetric.Mode.First, false),
TraceSectionMetric(VALIDATION_SECTION, TraceSectionMetric.Mode.First, false), TraceSectionMetric(BLOCKCHAIN_SYNC_SECTION, TraceSectionMetric.Mode.First, false),
TraceSectionMetric(SCAN_SECTION, TraceSectionMetric.Mode.First, false) TraceSectionMetric(DOWNLOAD_SECTION, TraceSectionMetric.Mode.First, false),
), TraceSectionMetric(VALIDATION_SECTION, TraceSectionMetric.Mode.First, false),
compilationMode = CompilationMode.Full(), TraceSectionMetric(SCAN_SECTION, TraceSectionMetric.Mode.First, false)
startupMode = StartupMode.COLD, ),
iterations = 3, compilationMode = CompilationMode.Full(),
measureBlock = { startupMode = StartupMode.COLD,
startLegacyActivityAndWait() iterations = 3,
resetSDK() measureBlock = {
gotoBalanceScreen() startLegacyActivityAndWait()
waitForBalanceScreen() resetSDK()
closeBalanceScreen() gotoBalanceScreen()
} waitForBalanceScreen()
) closeBalanceScreen()
}
)
// TODO [#808]: Add demo-ui-lib module (and reference the hardcoded texts here) // TODO [#808]: Add demo-ui-lib module (and reference the hardcoded texts here)
// TODO [#808]: https://github.com/zcash/zcash-android-wallet-sdk/issues/808 // TODO [#808]: https://github.com/zcash/zcash-android-wallet-sdk/issues/808
@ -103,9 +104,10 @@ class SyncBlockchainBenchmark : UiTestPrerequisites() {
} }
private fun MacrobenchmarkScope.startLegacyActivityAndWait() { private fun MacrobenchmarkScope.startLegacyActivityAndWait() {
val intent = Intent(Intent.ACTION_MAIN).apply { val intent =
component = ComponentName(APP_TARGET_PACKAGE_NAME, APP_TARGET_ACTIVITY_NAME) Intent(Intent.ACTION_MAIN).apply {
} component = ComponentName(APP_TARGET_PACKAGE_NAME, APP_TARGET_ACTIVITY_NAME)
}
startActivityAndWait(intent) startActivityAndWait(intent)
} }

View File

@ -6,7 +6,11 @@ import androidx.test.core.app.ApplicationProvider
fun getAppContext(): Context = ApplicationProvider.getApplicationContext() fun getAppContext(): Context = ApplicationProvider.getApplicationContext()
fun getStringResource(@StringRes resId: Int) = getAppContext().getString(resId) fun getStringResource(
@StringRes resId: Int
) = getAppContext().getString(resId)
fun getStringResourceWithArgs(@StringRes resId: Int, vararg formatArgs: String) = fun getStringResourceWithArgs(
getAppContext().getString(resId, *formatArgs) @StringRes resId: Int,
vararg formatArgs: String
) = getAppContext().getString(resId, *formatArgs)

View File

@ -6,10 +6,16 @@ import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2 import androidx.test.uiautomator.UiObject2
import kotlin.time.Duration import kotlin.time.Duration
fun UiDevice.waitFor(condition: SearchCondition<Boolean>, timeout: Duration): Boolean { fun UiDevice.waitFor(
condition: SearchCondition<Boolean>,
timeout: Duration
): Boolean {
return wait(condition, timeout.inWholeMilliseconds) return wait(condition, timeout.inWholeMilliseconds)
} }
fun UiObject2.clickAndWaitFor(condition: EventCondition<Boolean>, timeout: Duration): Boolean { fun UiObject2.clickAndWaitFor(
condition: EventCondition<Boolean>,
timeout: Duration
): Boolean {
return clickAndWait(condition, timeout.inWholeMilliseconds) return clickAndWait(condition, timeout.inWholeMilliseconds)
} }

View File

@ -25,8 +25,9 @@ open class UiTestPrerequisites {
} }
private fun isScreenOn(): Boolean { private fun isScreenOn(): Boolean {
val powerService = ApplicationProvider.getApplicationContext<Context>() val powerService =
.getSystemService(Context.POWER_SERVICE) as PowerManager ApplicationProvider.getApplicationContext<Context>()
.getSystemService(Context.POWER_SERVICE) as PowerManager
return powerService.isInteractive return powerService.isInteractive
} }
@ -40,7 +41,7 @@ open class UiTestPrerequisites {
val keyguardService = ( val keyguardService = (
ApplicationProvider.getApplicationContext<Context>() ApplicationProvider.getApplicationContext<Context>()
.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager .getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
) )
return keyguardService.isKeyguardLocked return keyguardService.isKeyguardLocked
} }

View File

@ -37,7 +37,6 @@ import org.junit.Test
* https://github.com/EdgeApp/eosjs-node-cli/blob/paul/cleanup/app.js * https://github.com/EdgeApp/eosjs-node-cli/blob/paul/cleanup/app.js
*/ */
class SampleCodeTest { class SampleCodeTest {
// /////////////////////////////////////////////////// // ///////////////////////////////////////////////////
// Seed derivation // Seed derivation
@Ignore("This test is not implemented") @Ignore("This test is not implemented")
@ -71,11 +70,12 @@ class SampleCodeTest {
// /////////////////////////////////////////////////// // ///////////////////////////////////////////////////
// Get Address // Get Address
@Test @Test
fun getAddress() = runBlocking { fun getAddress() =
val address = synchronizer.getUnifiedAddress(Account.DEFAULT) runBlocking {
assertFalse(address.isBlank()) val address = synchronizer.getUnifiedAddress(Account.DEFAULT)
log("Address: $address") assertFalse(address.isBlank())
} log("Address: $address")
}
// /////////////////////////////////////////////////// // ///////////////////////////////////////////////////
// Derive address from Extended Full Viewing Key // Derive address from Extended Full Viewing Key
@ -87,61 +87,64 @@ class SampleCodeTest {
// Query latest block height // Query latest block height
@Test @Test
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
fun getLatestBlockHeightTest() = runTest { fun getLatestBlockHeightTest() =
// Test the result, only if there is no server communication problem. runTest {
runCatching { // Test the result, only if there is no server communication problem.
LightWalletClient.new(context, lightwalletdHost).getLatestBlockHeight() runCatching {
}.onFailure { LightWalletClient.new(context, lightwalletdHost).getLatestBlockHeight()
Twig.debug(it) { "Failed to retrieve data" } }.onFailure {
}.onSuccess { Twig.debug(it) { "Failed to retrieve data" }
assertTrue(it is Response.Success<BlockHeightUnsafe>) }.onSuccess {
Twig.debug { "Latest Block: ${(it as Response.Success<BlockHeightUnsafe>).result}" } assertTrue(it is Response.Success<BlockHeightUnsafe>)
Twig.debug { "Latest Block: ${(it as Response.Success<BlockHeightUnsafe>).result}" }
}
} }
}
// /////////////////////////////////////////////////// // ///////////////////////////////////////////////////
// Download compact block range // Download compact block range
@Test @Test
@OptIn(ExperimentalCoroutinesApi::class) fun getBlockRange() =
fun getBlockRange() = runTest { runTest {
val blockRange = BlockHeightUnsafe( @Suppress("ktlint:standard:multiline-expression-wrapping")
BlockHeight.new( val blockRange =
ZcashNetwork.Mainnet, BlockHeightUnsafe(
500_000 BlockHeight.new(
).value ZcashNetwork.Mainnet,
)..BlockHeightUnsafe( 500_000
( ).value
BlockHeight.new( )..BlockHeightUnsafe(
ZcashNetwork.Mainnet, (
500_009 BlockHeight.new(
).value ZcashNetwork.Mainnet,
500_009
).value
)
) )
)
val lightWalletClient = LightWalletClient.new(context, lightwalletdHost) val lightWalletClient = LightWalletClient.new(context, lightwalletdHost)
// Test the result, only if there is no server communication problem. // Test the result, only if there is no server communication problem.
runCatching { runCatching {
lightWalletClient.getBlockRange(blockRange) lightWalletClient.getBlockRange(blockRange)
}.onFailure { }.onFailure {
Twig.debug(it) { "Failed to retrieve data" } Twig.debug(it) { "Failed to retrieve data" }
}.onSuccess { }.onSuccess {
it.onEach { response -> it.onEach { response ->
assert(response is Response.Success) { "Server communication failed." } assert(response is Response.Success) { "Server communication failed." }
}
.filterIsInstance<Response.Success<CompactBlockUnsafe>>()
.map { response ->
response.result
}.toList()
.also { blocks ->
assertEquals(blockRange.endInclusive.value - blockRange.start.value, blocks.count())
blocks.forEachIndexed { i, block ->
log("Block #$i: height:${block.height} hash:${block.hash.toHex()}")
}
} }
.filterIsInstance<Response.Success<CompactBlockUnsafe>>()
.map { response ->
response.result
}.toList()
.also { blocks ->
assertEquals(blockRange.endInclusive.value - blockRange.start.value, blocks.count())
blocks.forEachIndexed { i, block ->
log("Block #$i: height:${block.height} hash:${block.hash.toHex()}")
}
}
}
} }
}
// /////////////////////////////////////////////////// // ///////////////////////////////////////////////////
// Query account outgoing transactions // Query account outgoing transactions
@ -175,17 +178,19 @@ class SampleCodeTest {
// /////////////////////////////////////////////////// // ///////////////////////////////////////////////////
// Create a signed transaction (with memo) and broadcast // Create a signed transaction (with memo) and broadcast
@Test @Test
fun submitTransaction() = runBlocking { fun submitTransaction() =
val amount = 0.123.convertZecToZatoshi() runBlocking {
val address = "ztestsapling1tklsjr0wyw0d58f3p7wufvrj2cyfv6q6caumyueadq8qvqt8lda6v6tpx474rfru9y6u75u7qnw" val amount = 0.123.convertZecToZatoshi()
val memo = "Test Transaction" val address = "ztestsapling1tklsjr0wyw0d58f3p7wufvrj2cyfv6q6caumyueadq8qvqt8lda6v6tpx474rfru9y6u75u7qnw"
val spendingKey = DerivationTool.getInstance().deriveUnifiedSpendingKey( val memo = "Test Transaction"
seed, val spendingKey =
ZcashNetwork.Mainnet, DerivationTool.getInstance().deriveUnifiedSpendingKey(
Account.DEFAULT seed,
) ZcashNetwork.Mainnet,
synchronizer.sendToAddress(spendingKey, amount, address, memo) Account.DEFAULT
} )
synchronizer.sendToAddress(spendingKey, amount, address, memo)
}
// ///////////////////////////////////////////////////// // /////////////////////////////////////////////////////
// Utility Functions // Utility Functions
@ -196,18 +201,19 @@ class SampleCodeTest {
private val lightwalletdHost = LightWalletEndpoint.Mainnet private val lightwalletdHost = LightWalletEndpoint.Mainnet
private val context = InstrumentationRegistry.getInstrumentation().targetContext private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val synchronizer: Synchronizer = run { private val synchronizer: Synchronizer =
val network = ZcashNetwork.fromResources(context) run {
Synchronizer.newBlocking( val network = ZcashNetwork.fromResources(context)
context, Synchronizer.newBlocking(
network, context,
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network), network,
seed = seed, lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
birthday = null, seed = seed,
// Using existing wallet init mode as simplification for the test birthday = null,
walletInitMode = WalletInitMode.ExistingWallet // Using existing wallet init mode as simplification for the test
) walletInitMode = WalletInitMode.ExistingWallet
} )
}
fun log(message: String?) = Twig.debug { message ?: "null" } fun log(message: String?) = Twig.debug { message ?: "null" }
} }

View File

@ -10,7 +10,6 @@ import kotlinx.coroutines.flow.flowOf
*/ */
class MockPreferenceProvider(mutableMapFactory: () -> MutableMap<String, String?> = { mutableMapOf() }) : class MockPreferenceProvider(mutableMapFactory: () -> MutableMap<String, String?> = { mutableMapOf() }) :
PreferenceProvider { PreferenceProvider {
private val map = mutableMapFactory() private val map = mutableMapFactory()
override suspend fun getString(key: Key) = map[key.key] override suspend fun getString(key: Key) = map[key.key]
@ -20,7 +19,10 @@ class MockPreferenceProvider(mutableMapFactory: () -> MutableMap<String, String?
override suspend fun hasKey(key: Key) = map.containsKey(key.key) override suspend fun hasKey(key: Key) = map.containsKey(key.key)
override suspend fun putString(key: Key, value: String?) { override suspend fun putString(
key: Key,
value: String?
) {
map[key.key] = value map[key.key] = value
} }
} }

View File

@ -5,6 +5,8 @@ import cash.z.ecc.android.sdk.demoapp.preference.model.entry.Key
object BooleanPreferenceDefaultFixture { object BooleanPreferenceDefaultFixture {
val KEY = Key("some_boolean_key") // $NON-NLS val KEY = Key("some_boolean_key") // $NON-NLS
fun newTrue() = BooleanPreferenceDefault(KEY, true) fun newTrue() = BooleanPreferenceDefault(KEY, true)
fun newFalse() = BooleanPreferenceDefault(KEY, false) fun newFalse() = BooleanPreferenceDefault(KEY, false)
} }

View File

@ -6,5 +6,9 @@ import cash.z.ecc.android.sdk.demoapp.preference.model.entry.Key
object IntegerPreferenceDefaultFixture { object IntegerPreferenceDefaultFixture {
val KEY = Key("some_string_key") // $NON-NLS val KEY = Key("some_string_key") // $NON-NLS
const val DEFAULT_VALUE = 123 const val DEFAULT_VALUE = 123
fun new(key: Key = KEY, value: Int = DEFAULT_VALUE) = IntegerPreferenceDefault(key, value)
fun new(
key: Key = KEY,
value: Int = DEFAULT_VALUE
) = IntegerPreferenceDefault(key, value)
} }

View File

@ -6,5 +6,9 @@ import cash.z.ecc.android.sdk.demoapp.preference.model.entry.StringPreferenceDef
object StringDefaultPreferenceFixture { object StringDefaultPreferenceFixture {
val KEY = Key("some_string_key") // $NON-NLS val KEY = Key("some_string_key") // $NON-NLS
const val DEFAULT_VALUE = "some_default_value" // $NON-NLS const val DEFAULT_VALUE = "some_default_value" // $NON-NLS
fun new(key: Key = KEY, value: String = DEFAULT_VALUE) = StringPreferenceDefault(key, value)
fun new(
key: Key = KEY,
value: String = DEFAULT_VALUE
) = StringPreferenceDefault(key, value)
} }

View File

@ -16,32 +16,38 @@ class BooleanPreferenceDefaultTest {
} }
@Test @Test
fun value_default_true() = runTest { fun value_default_true() =
val entry = BooleanPreferenceDefaultFixture.newTrue() runTest {
assertTrue(entry.getValue(MockPreferenceProvider())) val entry = BooleanPreferenceDefaultFixture.newTrue()
} assertTrue(entry.getValue(MockPreferenceProvider()))
@Test
fun value_default_false() = runTest {
val entry = BooleanPreferenceDefaultFixture.newFalse()
assertFalse(entry.getValue(MockPreferenceProvider()))
}
@Test
fun value_from_config_false() = runTest {
val entry = BooleanPreferenceDefaultFixture.newTrue()
val mockPreferenceProvider = MockPreferenceProvider {
mutableMapOf(BooleanPreferenceDefaultFixture.KEY.key to false.toString())
} }
assertFalse(entry.getValue(mockPreferenceProvider))
}
@Test @Test
fun value_from_config_true() = runTest { fun value_default_false() =
val entry = BooleanPreferenceDefaultFixture.newTrue() runTest {
val mockPreferenceProvider = MockPreferenceProvider { val entry = BooleanPreferenceDefaultFixture.newFalse()
mutableMapOf(BooleanPreferenceDefaultFixture.KEY.key to true.toString()) assertFalse(entry.getValue(MockPreferenceProvider()))
}
@Test
fun value_from_config_false() =
runTest {
val entry = BooleanPreferenceDefaultFixture.newTrue()
val mockPreferenceProvider =
MockPreferenceProvider {
mutableMapOf(BooleanPreferenceDefaultFixture.KEY.key to false.toString())
}
assertFalse(entry.getValue(mockPreferenceProvider))
}
@Test
fun value_from_config_true() =
runTest {
val entry = BooleanPreferenceDefaultFixture.newTrue()
val mockPreferenceProvider =
MockPreferenceProvider {
mutableMapOf(BooleanPreferenceDefaultFixture.KEY.key to true.toString())
}
assertTrue(entry.getValue(mockPreferenceProvider))
} }
assertTrue(entry.getValue(mockPreferenceProvider))
}
} }

View File

@ -15,20 +15,23 @@ class IntegerPreferenceDefaultTest {
} }
@Test @Test
fun value_default() = runTest { fun value_default() =
val entry = IntegerPreferenceDefaultFixture.new() runTest {
assertEquals(IntegerPreferenceDefaultFixture.DEFAULT_VALUE, entry.getValue(MockPreferenceProvider())) val entry = IntegerPreferenceDefaultFixture.new()
} assertEquals(IntegerPreferenceDefaultFixture.DEFAULT_VALUE, entry.getValue(MockPreferenceProvider()))
@Test
fun value_override() = runTest {
val expected = IntegerPreferenceDefaultFixture.DEFAULT_VALUE + 5
val entry = IntegerPreferenceDefaultFixture.new()
val mockPreferenceProvider = MockPreferenceProvider {
mutableMapOf(StringDefaultPreferenceFixture.KEY.key to expected.toString())
} }
assertEquals(expected, entry.getValue(mockPreferenceProvider)) @Test
} fun value_override() =
runTest {
val expected = IntegerPreferenceDefaultFixture.DEFAULT_VALUE + 5
val entry = IntegerPreferenceDefaultFixture.new()
val mockPreferenceProvider =
MockPreferenceProvider {
mutableMapOf(StringDefaultPreferenceFixture.KEY.key to expected.toString())
}
assertEquals(expected, entry.getValue(mockPreferenceProvider))
}
} }

View File

@ -14,19 +14,22 @@ class StringPreferenceDefaultTest {
} }
@Test @Test
fun value_default() = runTest { fun value_default() =
val entry = StringDefaultPreferenceFixture.new() runTest {
assertEquals(StringDefaultPreferenceFixture.DEFAULT_VALUE, entry.getValue(MockPreferenceProvider())) val entry = StringDefaultPreferenceFixture.new()
} assertEquals(StringDefaultPreferenceFixture.DEFAULT_VALUE, entry.getValue(MockPreferenceProvider()))
@Test
fun value_override() = runTest {
val entry = StringDefaultPreferenceFixture.new()
val mockPreferenceProvider = MockPreferenceProvider {
mutableMapOf(StringDefaultPreferenceFixture.KEY.key to "override")
} }
assertEquals("override", entry.getValue(mockPreferenceProvider)) @Test
} fun value_override() =
runTest {
val entry = StringDefaultPreferenceFixture.new()
val mockPreferenceProvider =
MockPreferenceProvider {
mutableMapOf(StringDefaultPreferenceFixture.KEY.key to "override")
}
assertEquals("override", entry.getValue(mockPreferenceProvider))
}
} }

View File

@ -23,8 +23,9 @@ open class UiTestPrerequisites {
} }
private fun isScreenOn(): Boolean { private fun isScreenOn(): Boolean {
val powerService = ApplicationProvider.getApplicationContext<Context>() val powerService =
.getSystemService(Context.POWER_SERVICE) as PowerManager ApplicationProvider.getApplicationContext<Context>()
.getSystemService(Context.POWER_SERVICE) as PowerManager
return powerService.isInteractive return powerService.isInteractive
} }
@ -38,7 +39,7 @@ open class UiTestPrerequisites {
val keyguardService = ( val keyguardService = (
ApplicationProvider.getApplicationContext<Context>() ApplicationProvider.getApplicationContext<Context>()
.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager .getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
) )
return keyguardService.isKeyguardLocked return keyguardService.isKeyguardLocked
} }

View File

@ -17,52 +17,55 @@ import kotlin.time.TimeMark
import kotlin.time.TimeSource import kotlin.time.TimeSource
class FlowExtTest { class FlowExtTest {
@OptIn(ExperimentalTime::class, ExperimentalCoroutinesApi::class) @OptIn(ExperimentalTime::class, ExperimentalCoroutinesApi::class)
@Test @Test
@SmallTest @SmallTest
fun throttle_one_sec() = runTest { fun throttle_one_sec() =
val timer = TimeSource.Monotonic.markNow() runTest {
val flow = flow { val timer = TimeSource.Monotonic.markNow()
while (timer.elapsedNow() <= 5.seconds) { val flow =
emit(1) flow {
} while (timer.elapsedNow() <= 5.seconds) {
}.throttle(1.seconds) emit(1)
}
}.throttle(1.seconds)
var timeMark: TimeMark? = null var timeMark: TimeMark? = null
flow.collect { flow.collect {
if (timeMark == null) { if (timeMark == null) {
timeMark = TimeSource.Monotonic.markNow() timeMark = TimeSource.Monotonic.markNow()
} else { } else {
assert(timeMark!!.elapsedNow() >= 1.seconds) assert(timeMark!!.elapsedNow() >= 1.seconds)
timeMark = TimeSource.Monotonic.markNow() timeMark = TimeSource.Monotonic.markNow()
}
} }
} }
}
@OptIn(ExperimentalTime::class) @OptIn(ExperimentalTime::class)
private fun raceConditionTest(duration: Duration): Boolean = runBlocking { private fun raceConditionTest(duration: Duration): Boolean =
val flow = (0..1000).asFlow().throttle(duration) runBlocking {
val flow = (0..1000).asFlow().throttle(duration)
val values = mutableListOf<Int>() val values = mutableListOf<Int>()
flow.collect { flow.collect {
values.add(it) values.add(it)
}
return@runBlocking values.zipWithNext().all { it.first <= it.second }
} }
return@runBlocking values.zipWithNext().all { it.first <= it.second }
}
@FlakyTest @FlakyTest
@Test @Test
fun stressTest() = runBlocking { fun stressTest() =
for (i in 0..10) { runBlocking {
assertTrue { raceConditionTest(0.001.seconds) } for (i in 0..10) {
assertTrue { raceConditionTest(0.001.seconds) }
}
for (i in 0..10) {
assertTrue { raceConditionTest(0.0001.seconds) }
}
for (i in 0..10) {
assertTrue { raceConditionTest(0.00001.seconds) }
}
} }
for (i in 0..10) {
assertTrue { raceConditionTest(0.0001.seconds) }
}
for (i in 0..10) {
assertTrue { raceConditionTest(0.00001.seconds) }
}
}
} }

View File

@ -7,9 +7,13 @@ import androidx.annotation.StringRes
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import java.util.Locale import java.util.Locale
fun getStringResource(@StringRes resId: Int) = ApplicationProvider.getApplicationContext<Context>().getString(resId) fun getStringResource(
@StringRes resId: Int
) = ApplicationProvider.getApplicationContext<Context>().getString(resId)
fun getStringResourceWithArgs(@StringRes resId: Int, formatArgs: Array<Any>) = fun getStringResourceWithArgs(
ApplicationProvider.getApplicationContext<Context>().getString(resId, *formatArgs) @StringRes resId: Int,
formatArgs: Array<Any>
) = ApplicationProvider.getApplicationContext<Context>().getString(resId, *formatArgs)
fun isLocaleRTL(locale: Locale) = TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL fun isLocaleRTL(locale: Locale) = TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL

View File

@ -27,18 +27,18 @@ class ScreenTimeoutTest : UiTestPrerequisites() {
@Test @Test
@MediumTest @MediumTest
fun acquireAndReleaseScreenTimeout() = runTest { fun acquireAndReleaseScreenTimeout() =
val testSetup = TestSetup(composeTestRule) runTest {
val testSetup = TestSetup(composeTestRule)
assertEquals(1, testSetup.getScreenTimeoutCount()) assertEquals(1, testSetup.getScreenTimeoutCount())
testSetup.mutableScreenTimeoutFlag.update { false } testSetup.mutableScreenTimeoutFlag.update { false }
composeTestRule.awaitIdle() composeTestRule.awaitIdle()
assertEquals(0, testSetup.getScreenTimeoutCount()) assertEquals(0, testSetup.getScreenTimeoutCount())
} }
private class TestSetup(composeTestRule: ComposeContentTestRule) { private class TestSetup(composeTestRule: ComposeContentTestRule) {
val mutableScreenTimeoutFlag = MutableStateFlow(true) val mutableScreenTimeoutFlag = MutableStateFlow(true)
private val screenTimeout = ScreenTimeout() private val screenTimeout = ScreenTimeout()
@ -58,6 +58,7 @@ class ScreenTimeoutTest : UiTestPrerequisites() {
} }
@Composable @Composable
@Suppress("ktlint:standard:function-naming")
private fun TestView(disableScreenTimeout: Boolean) { private fun TestView(disableScreenTimeout: Boolean) {
if (disableScreenTimeout) { if (disableScreenTimeout) {
DisableScreenTimeout() DisableScreenTimeout()

View File

@ -4,7 +4,6 @@ import androidx.multidex.MultiDexApplication
import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.internal.Twig
class App : MultiDexApplication() { class App : MultiDexApplication() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()

View File

@ -21,7 +21,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
abstract class BaseDemoFragment<T : ViewBinding> : Fragment() { abstract class BaseDemoFragment<T : ViewBinding> : Fragment() {
/** /**
* Since the lightwalletClient is not a component that apps typically use, directly, we provide * Since the lightwalletClient is not a component that apps typically use, directly, we provide
* this from one place. Everything that can be done with the service can/should be done with the * this from one place. Everything that can be done with the service can/should be done with the
@ -83,7 +82,10 @@ abstract class BaseDemoFragment<T : ViewBinding> : Fragment() {
/** /**
* Convenience function to the given text to the clipboard. * Convenience function to the given text to the clipboard.
*/ */
open fun copyToClipboard(text: String, description: String = "Copied to clipboard!") { open fun copyToClipboard(
text: String,
description: String = "Copied to clipboard!"
) {
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager) (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)
.setPrimaryClip(ClipData.newPlainText("DemoAppClip", text)) .setPrimaryClip(ClipData.newPlainText("DemoAppClip", text))
toast(description) toast(description)

View File

@ -33,6 +33,7 @@ class ComposeActivity : ComponentActivity() {
} }
@Composable @Composable
@Suppress("ktlint:standard:function-naming")
private fun MainContent() { private fun MainContent() {
when (walletViewModel.secretState.collectAsStateWithLifecycle().value) { when (walletViewModel.secretState.collectAsStateWithLifecycle().value) {
SecretState.Loading -> { SecretState.Loading -> {

View File

@ -63,14 +63,15 @@ class MainActivity :
val navController = findNavController(R.id.nav_host_fragment) val navController = findNavController(R.id.nav_host_fragment)
// Passing each menu ID as a set of Ids because each // Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations. // menu should be considered as top level destinations.
appBarConfiguration = AppBarConfiguration( appBarConfiguration =
setOf( AppBarConfiguration(
R.id.nav_home, R.id.nav_address, R.id.nav_balance, R.id.nav_block, R.id.nav_private_key, setOf(
R.id.nav_latest_height, R.id.nav_block_range, R.id.nav_home, R.id.nav_address, R.id.nav_balance, R.id.nav_block, R.id.nav_private_key,
R.id.nav_transactions, R.id.nav_utxos, R.id.nav_send R.id.nav_latest_height, R.id.nav_block_range,
), R.id.nav_transactions, R.id.nav_utxos, R.id.nav_send
drawerLayout ),
) drawerLayout
)
setupActionBarWithNavController(navController, appBarConfiguration) setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController) navView.setupWithNavController(navController)
drawerLayout.addDrawerListener(this) drawerLayout.addDrawerListener(this)
@ -127,10 +128,11 @@ class MainActivity :
lightwalletClient?.shutdown() lightwalletClient?.shutdown()
} }
val network = ZcashNetwork.fromResources(applicationContext) val network = ZcashNetwork.fromResources(applicationContext)
lightwalletClient = LightWalletClient.new( lightwalletClient =
applicationContext, LightWalletClient.new(
LightWalletEndpoint.defaultForNetwork(network) applicationContext,
) LightWalletEndpoint.defaultForNetwork(network)
)
} }
private fun onFabClicked() { private fun onFabClicked() {
@ -162,7 +164,10 @@ class MainActivity :
} }
@Suppress("EmptyFunctionBlock") @Suppress("EmptyFunctionBlock")
override fun onDrawerSlide(drawerView: View, slideOffset: Float) { override fun onDrawerSlide(
drawerView: View,
slideOffset: Float
) {
} }
override fun onDrawerOpened(drawerView: View) { override fun onDrawerOpened(drawerView: View) {
@ -176,9 +181,10 @@ class MainActivity :
private fun newBrowserIntent(url: String): Intent { private fun newBrowserIntent(url: String): Intent {
val uri = Uri.parse(url) val uri = Uri.parse(url)
val intent = Intent(Intent.ACTION_VIEW, uri).apply { val intent =
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) Intent(Intent.ACTION_VIEW, uri).apply {
} addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
return intent return intent
} }

View File

@ -36,7 +36,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
@Suppress("LongMethod") @Suppress("LongMethod", "ktlint:standard:function-naming")
internal fun ComposeActivity.Navigation() { internal fun ComposeActivity.Navigation() {
val navController = rememberNavController() val navController = rememberNavController()
@ -187,10 +187,11 @@ private fun copyToClipboard(
) { ) {
val clipboardManager = context.getSystemService(ClipboardManager::class.java) val clipboardManager = context.getSystemService(ClipboardManager::class.java)
val data = ClipData.newPlainText( val data =
tag, ClipData.newPlainText(
textToCopy tag,
) textToCopy
)
clipboardManager.setPrimaryClip(data) clipboardManager.setPrimaryClip(data)
// Notify users with Snackbar only on Android level 32 and lower, as 33 and higher notifies users by its own system // Notify users with Snackbar only on Android level 32 and lower, as 33 and higher notifies users by its own system
@ -207,9 +208,10 @@ private fun copyToClipboard(
private fun newBrowserIntent(url: String): Intent { private fun newBrowserIntent(url: String): Intent {
val uri = Uri.parse(url) val uri = Uri.parse(url)
val intent = Intent(Intent.ACTION_VIEW, uri).apply { val intent =
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) Intent(Intent.ACTION_VIEW, uri).apply {
} addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
return intent return intent
} }

View File

@ -40,74 +40,78 @@ import kotlin.time.Duration.Companion.seconds
* Shared mutable state for the demo * Shared mutable state for the demo
*/ */
class SharedViewModel(application: Application) : AndroidViewModel(application) { class SharedViewModel(application: Application) : AndroidViewModel(application) {
private val _seedPhrase = MutableStateFlow(DemoConstants.INITIAL_SEED_WORDS) private val _seedPhrase = MutableStateFlow(DemoConstants.INITIAL_SEED_WORDS)
private val _blockHeight = MutableStateFlow<BlockHeight?>( private val _birthdayHeight =
runBlocking { MutableStateFlow<BlockHeight?>(
BlockHeight.ofLatestCheckpoint( runBlocking {
getApplication(), BlockHeight.ofLatestCheckpoint(
ZcashNetwork.fromResources(application) getApplication(),
) ZcashNetwork.fromResources(application)
} )
) }
)
// publicly, this is read-only // publicly, this is read-only
val seedPhrase: StateFlow<String> get() = _seedPhrase val seedPhrase: StateFlow<String> get() = _seedPhrase
// publicly, this is read-only // publicly, this is read-only
val birthdayHeight: StateFlow<BlockHeight?> get() = _blockHeight val birthdayHeight: StateFlow<BlockHeight?> get() = _birthdayHeight
private val lockoutMutex = Mutex() private val lockoutMutex = Mutex()
private val lockoutIdFlow = MutableStateFlow<UUID?>(null) private val lockoutIdFlow = MutableStateFlow<UUID?>(null)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private val synchronizerOrLockout: Flow<InternalSynchronizerStatus> = lockoutIdFlow.flatMapLatest { lockoutId -> private val synchronizerOrLockout: Flow<InternalSynchronizerStatus> =
if (null != lockoutId) { lockoutIdFlow.flatMapLatest { lockoutId ->
flowOf(InternalSynchronizerStatus.Lockout(lockoutId)) if (null != lockoutId) {
} else { flowOf(InternalSynchronizerStatus.Lockout(lockoutId))
callbackFlow<InternalSynchronizerStatus> { } else {
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already callbackFlow<InternalSynchronizerStatus> {
// have the seed stored // Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
val seedBytes = Mnemonics.MnemonicCode(seedPhrase.value).toSeed() // have the seed stored
val seedBytes = Mnemonics.MnemonicCode(seedPhrase.value).toSeed()
val network = ZcashNetwork.fromResources(application) val network = ZcashNetwork.fromResources(application)
val synchronizer = Synchronizer.new( val synchronizer =
application, Synchronizer.new(
network, application,
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network), network,
seed = seedBytes, lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
birthday = if (BenchmarkingExt.isBenchmarking()) { seed = seedBytes,
BlockHeight.new(ZcashNetwork.Mainnet, BenchmarkingBlockRangeFixture.new().start) birthday =
} else { if (BenchmarkingExt.isBenchmarking()) {
birthdayHeight.value BlockHeight.new(ZcashNetwork.Mainnet, BenchmarkingBlockRangeFixture.new().start)
}, } else {
// We use restore mode as this is always initialization with an older seed birthdayHeight.value
walletInitMode = WalletInitMode.RestoreWallet, },
alias = OLD_UI_SYNCHRONIZER_ALIAS // We use restore mode as this is always initialization with an older seed
) walletInitMode = WalletInitMode.RestoreWallet,
alias = OLD_UI_SYNCHRONIZER_ALIAS
)
send(InternalSynchronizerStatus.Available(synchronizer)) send(InternalSynchronizerStatus.Available(synchronizer))
awaitClose { awaitClose {
synchronizer.close() synchronizer.close()
}
} }
} }
} }
}
// Note that seed and birthday shouldn't be changed once a synchronizer is first collected // Note that seed and birthday shouldn't be changed once a synchronizer is first collected
val synchronizerFlow: StateFlow<Synchronizer?> = synchronizerOrLockout.map { val synchronizerFlow: StateFlow<Synchronizer?> =
when (it) { synchronizerOrLockout.map {
is InternalSynchronizerStatus.Available -> it.synchronizer when (it) {
is InternalSynchronizerStatus.Lockout -> null is InternalSynchronizerStatus.Available -> it.synchronizer
} is InternalSynchronizerStatus.Lockout -> null
}.stateIn( }
viewModelScope, }.stateIn(
SharingStarted.WhileSubscribed(DEFAULT_ANDROID_STATE_TIMEOUT.inWholeMilliseconds, 0), viewModelScope,
initialValue = SharingStarted.WhileSubscribed(DEFAULT_ANDROID_STATE_TIMEOUT.inWholeMilliseconds, 0),
null initialValue =
) null
)
fun updateSeedPhrase(newPhrase: String?): Boolean { fun updateSeedPhrase(newPhrase: String?): Boolean {
return if (isValidSeedPhrase(newPhrase)) { return if (isValidSeedPhrase(newPhrase)) {
@ -128,11 +132,12 @@ class SharedViewModel(application: Application) : AndroidViewModel(application)
.filterIsInstance<InternalSynchronizerStatus.Lockout>() .filterIsInstance<InternalSynchronizerStatus.Lockout>()
.filter { it.id == lockoutId } .filter { it.id == lockoutId }
.onFirst { .onFirst {
val didDelete = Synchronizer.erase( val didDelete =
appContext = getApplication(), Synchronizer.erase(
network = ZcashNetwork.fromResources(getApplication()), appContext = getApplication(),
alias = OLD_UI_SYNCHRONIZER_ALIAS network = ZcashNetwork.fromResources(getApplication()),
) alias = OLD_UI_SYNCHRONIZER_ALIAS
)
Twig.debug { "SDK erase result: $didDelete" } Twig.debug { "SDK erase result: $didDelete" }
} }
@ -162,6 +167,7 @@ class SharedViewModel(application: Application) : AndroidViewModel(application)
private sealed class InternalSynchronizerStatus { private sealed class InternalSynchronizerStatus {
class Available(val synchronizer: Synchronizer) : InternalSynchronizerStatus() class Available(val synchronizer: Synchronizer) : InternalSynchronizerStatus()
class Lockout(val id: UUID) : InternalSynchronizerStatus() class Lockout(val id: UUID) : InternalSynchronizerStatus()
} }

View File

@ -5,7 +5,6 @@ import android.os.StrictMode
import cash.z.ecc.android.sdk.demoapp.util.AndroidApiVersion import cash.z.ecc.android.sdk.demoapp.util.AndroidApiVersion
object StrictModeHelper { object StrictModeHelper {
fun enableStrictMode() { fun enableStrictMode() {
configureStrictMode() configureStrictMode()
} }

View File

@ -8,22 +8,24 @@ import cash.z.ecc.android.sdk.demoapp.util.LazyWithArgument
import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
private val lazy = LazyWithArgument<Context, WalletCoordinator> { private val lazy =
LazyWithArgument<Context, WalletCoordinator> {
/* /*
* A flow of the user's stored wallet. Null indicates that no wallet has been stored. * A flow of the user's stored wallet. Null indicates that no wallet has been stored.
*/ */
val persistableWalletFlow = flow { val persistableWalletFlow =
// EncryptedPreferenceSingleton.getInstance() is a suspending function, which is why we need flow {
// the flow builder to provide a coroutine context. // EncryptedPreferenceSingleton.getInstance() is a suspending function, which is why we need
val encryptedPreferenceProvider = EncryptedPreferenceSingleton.getInstance(it) // the flow builder to provide a coroutine context.
val encryptedPreferenceProvider = EncryptedPreferenceSingleton.getInstance(it)
emitAll(EncryptedPreferenceKeys.PERSISTABLE_WALLET.observe(encryptedPreferenceProvider)) emitAll(EncryptedPreferenceKeys.PERSISTABLE_WALLET.observe(encryptedPreferenceProvider))
}
WalletCoordinator(
context = it,
persistableWallet = persistableWalletFlow
)
} }
WalletCoordinator(
context = it,
persistableWallet = persistableWalletFlow
)
}
fun WalletCoordinator.Companion.getInstance(context: Context) = lazy.getInstance(context) fun WalletCoordinator.Companion.getInstance(context: Context) = lazy.getInstance(context)

View File

@ -23,7 +23,6 @@ import kotlinx.coroutines.launch
* that is used, update the `DemoConfig.seedWords` value. * that is used, update the `DemoConfig.seedWords` value.
*/ */
class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() { class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
private lateinit var viewingKey: UnifiedFullViewingKey private lateinit var viewingKey: UnifiedFullViewingKey
private fun displayAddress() { private fun displayAddress() {
@ -67,7 +66,10 @@ class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
reportTraceEvent(ProvideAddressBenchmarkTrace.Event.ADDRESS_SCREEN_START) reportTraceEvent(ProvideAddressBenchmarkTrace.Event.ADDRESS_SCREEN_START)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
displayAddress() displayAddress()

View File

@ -34,7 +34,6 @@ import kotlinx.coroutines.launch
*/ */
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() { class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetBalanceBinding = override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetBalanceBinding =
FragmentGetBalanceBinding.inflate(layoutInflater) FragmentGetBalanceBinding.inflate(layoutInflater)
@ -48,7 +47,10 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
reportTraceEvent(SyncBlockchainBenchmarkTrace.Event.BALANCE_SCREEN_END) reportTraceEvent(SyncBlockchainBenchmarkTrace.Event.BALANCE_SCREEN_END)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val seedPhrase = sharedViewModel.seedPhrase.value val seedPhrase = sharedViewModel.seedPhrase.value
@ -110,25 +112,19 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
} }
} }
private fun onOrchardBalance( private fun onOrchardBalance(orchardBalance: WalletBalance?) {
orchardBalance: WalletBalance?
) {
binding.orchardBalance.apply { binding.orchardBalance.apply {
text = orchardBalance.humanString() text = orchardBalance.humanString()
} }
} }
private fun onSaplingBalance( private fun onSaplingBalance(saplingBalance: WalletBalance?) {
saplingBalance: WalletBalance?
) {
binding.saplingBalance.apply { binding.saplingBalance.apply {
text = saplingBalance.humanString() text = saplingBalance.humanString()
} }
} }
private fun onTransparentBalance( private fun onTransparentBalance(transparentBalance: WalletBalance?) {
transparentBalance: WalletBalance?
) {
binding.transparentBalance.apply { binding.transparentBalance.apply {
text = transparentBalance.humanString() text = transparentBalance.humanString()
} }
@ -136,26 +132,28 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
binding.shield.apply { binding.shield.apply {
// TODO [#776]: Support variable fees // TODO [#776]: Support variable fees
// TODO [#776]: https://github.com/zcash/zcash-android-wallet-sdk/issues/776 // TODO [#776]: https://github.com/zcash/zcash-android-wallet-sdk/issues/776
visibility = if ((transparentBalance?.available ?: Zatoshi(0)) > ZcashSdk.MINERS_FEE) { visibility =
View.VISIBLE if ((transparentBalance?.available ?: Zatoshi(0)) > ZcashSdk.MINERS_FEE) {
} else { View.VISIBLE
View.GONE } else {
} View.GONE
}
} }
} }
private fun onStatus(status: Synchronizer.Status) { private fun onStatus(status: Synchronizer.Status) {
Twig.debug { "Synchronizer status: $status" } Twig.debug { "Synchronizer status: $status" }
// report benchmark event // report benchmark event
val traceEvents = when (status) { val traceEvents =
Synchronizer.Status.SYNCING -> { when (status) {
SyncBlockchainBenchmarkTrace.Event.BLOCKCHAIN_SYNC_START Synchronizer.Status.SYNCING -> {
SyncBlockchainBenchmarkTrace.Event.BLOCKCHAIN_SYNC_START
}
Synchronizer.Status.SYNCED -> {
SyncBlockchainBenchmarkTrace.Event.BLOCKCHAIN_SYNC_END
}
else -> null
} }
Synchronizer.Status.SYNCED -> {
SyncBlockchainBenchmarkTrace.Event.BLOCKCHAIN_SYNC_END
}
else -> null
}
traceEvents?.let { reportTraceEvent(it) } traceEvents?.let { reportTraceEvent(it) }
binding.textStatus.text = "Status: $status" binding.textStatus.text = "Status: $status"
@ -175,12 +173,13 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
} }
@Suppress("MagicNumber") @Suppress("MagicNumber")
private fun WalletBalance?.humanString() = if (null == this) { private fun WalletBalance?.humanString() =
"Calculating balance" if (null == this) {
} else { "Calculating balance"
""" } else {
Pending balance: ${pending.convertZatoshiToZecString(12)} """
Available balance: ${available.convertZatoshiToZecString(12)} Pending balance: ${pending.convertZatoshiToZecString(12)}
Total balance: ${total.convertZatoshiToZecString(12)} Available balance: ${available.convertZatoshiToZecString(12)}
""".trimIndent() Total balance: ${total.convertZatoshiToZecString(12)}
} """.trimIndent()
}

View File

@ -7,15 +7,15 @@ import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBlockBinding import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBlockBinding
import cash.z.ecc.android.sdk.demoapp.util.mainActivity import cash.z.ecc.android.sdk.demoapp.util.mainActivity
// TODO [#973]: Eliminate old UI demo-app
// TODO [#973]: https://github.com/zcash/zcash-android-wallet-sdk/issues/973
/** /**
* Retrieves a compact block from the lightwalletd server and displays basic information about it. * Retrieves a compact block from the lightwalletd server and displays basic information about it.
* This demonstrates the basic ability to connect to the server, request a compact block and parse * This demonstrates the basic ability to connect to the server, request a compact block and parse
* the response. * the response.
*/ */
class GetBlockFragment : BaseDemoFragment<FragmentGetBlockBinding>() { class GetBlockFragment : BaseDemoFragment<FragmentGetBlockBinding>() {
// TODO [#973]: Eliminate old UI demo-app
// TODO [#973]: https://github.com/zcash/zcash-android-wallet-sdk/issues/973
/* /*
var coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main) var coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main)
@ -78,7 +78,10 @@ class GetBlockFragment : BaseDemoFragment<FragmentGetBlockBinding>() {
// Android Lifecycle overrides // Android Lifecycle overrides
// //
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding.buttonApply.setOnClickListener(::onApply) binding.buttonApply.setOnClickListener(::onApply)
binding.buttonPrevious.setOnClickListener { binding.buttonPrevious.setOnClickListener {
@ -92,7 +95,6 @@ class GetBlockFragment : BaseDemoFragment<FragmentGetBlockBinding>() {
// //
// Base Fragment overrides // Base Fragment overrides
// //
@Suppress("MaxLineLength")
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetBlockBinding = override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetBlockBinding = FragmentGetBlockBinding.inflate(layoutInflater)
FragmentGetBlockBinding.inflate(layoutInflater)
} }

View File

@ -19,7 +19,6 @@ import kotlin.math.max
* block ranges for instance, to find the block with the most shielded transactions in a range. * block ranges for instance, to find the block with the most shielded transactions in a range.
*/ */
class GetBlockRangeFragment : BaseDemoFragment<FragmentGetBlockRangeBinding>() { class GetBlockRangeFragment : BaseDemoFragment<FragmentGetBlockRangeBinding>() {
// TODO [#973]: Eliminate old UI demo-app // TODO [#973]: Eliminate old UI demo-app
// TODO [#973]: https://github.com/zcash/zcash-android-wallet-sdk/issues/973 // TODO [#973]: https://github.com/zcash/zcash-android-wallet-sdk/issues/973
@Suppress("MaxLineLength", "MagicNumber", "UNUSED_PARAMETER") @Suppress("MaxLineLength", "MagicNumber", "UNUSED_PARAMETER")
@ -75,16 +74,18 @@ class GetBlockRangeFragment : BaseDemoFragment<FragmentGetBlockRangeBinding>() {
private fun onApply(unused: View) { private fun onApply(unused: View) {
val network = ZcashNetwork.fromResources(requireApplicationContext()) val network = ZcashNetwork.fromResources(requireApplicationContext())
val start = max( val start =
binding.textStartHeight.text.toString().toLongOrNull() max(
?: network.saplingActivationHeight.value, binding.textStartHeight.text.toString().toLongOrNull()
network.saplingActivationHeight.value ?: network.saplingActivationHeight.value,
) network.saplingActivationHeight.value
val end = max( )
binding.textEndHeight.text.toString().toLongOrNull() val end =
?: network.saplingActivationHeight.value, max(
network.saplingActivationHeight.value binding.textEndHeight.text.toString().toLongOrNull()
) ?: network.saplingActivationHeight.value,
network.saplingActivationHeight.value
)
if (start <= end) { if (start <= end) {
@Suppress("TooGenericExceptionCaught") @Suppress("TooGenericExceptionCaught")
try { try {
@ -122,7 +123,10 @@ class GetBlockRangeFragment : BaseDemoFragment<FragmentGetBlockRangeBinding>() {
// Android Lifecycle overrides // Android Lifecycle overrides
// //
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding.buttonApply.setOnClickListener(::onApply) binding.buttonApply.setOnClickListener(::onApply)
} }

View File

@ -11,7 +11,6 @@ import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetLatestHeightBinding
* instance. * instance.
*/ */
class GetLatestHeightFragment : BaseDemoFragment<FragmentGetLatestHeightBinding>() { class GetLatestHeightFragment : BaseDemoFragment<FragmentGetLatestHeightBinding>() {
// TODO [#973]: Eliminate old UI demo-app // TODO [#973]: Eliminate old UI demo-app
// TODO [#973]: https://github.com/zcash/zcash-android-wallet-sdk/issues/973 // TODO [#973]: https://github.com/zcash/zcash-android-wallet-sdk/issues/973
private fun displayLatestHeight() { private fun displayLatestHeight() {

View File

@ -24,7 +24,6 @@ import kotlinx.coroutines.launch
* HomeFragment. * HomeFragment.
*/ */
class GetPrivateKeyFragment : BaseDemoFragment<FragmentGetPrivateKeyBinding>() { class GetPrivateKeyFragment : BaseDemoFragment<FragmentGetPrivateKeyBinding>() {
private lateinit var seedPhrase: String private lateinit var seedPhrase: String
private lateinit var seed: ByteArray private lateinit var seed: ByteArray
@ -47,17 +46,19 @@ class GetPrivateKeyFragment : BaseDemoFragment<FragmentGetPrivateKeyBinding>() {
lifecycleScope.launch { lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
@Suppress("MagicNumber") @Suppress("MagicNumber")
val spendingKey = DerivationTool.getInstance().deriveUnifiedSpendingKey( val spendingKey =
seed, DerivationTool.getInstance().deriveUnifiedSpendingKey(
ZcashNetwork.fromResources(requireApplicationContext()), seed,
Account(5) ZcashNetwork.fromResources(requireApplicationContext()),
) Account(5)
)
// derive the key that allows you to view but not spend transactions // derive the key that allows you to view but not spend transactions
val viewingKey = DerivationTool.getInstance().deriveUnifiedFullViewingKey( val viewingKey =
spendingKey, DerivationTool.getInstance().deriveUnifiedFullViewingKey(
ZcashNetwork.fromResources(requireApplicationContext()) spendingKey,
) ZcashNetwork.fromResources(requireApplicationContext())
)
// display the keys in the UI // display the keys in the UI
binding.textInfo.setText("Spending Key:\n$spendingKey\n\nViewing Key:\n$viewingKey") binding.textInfo.setText("Spending Key:\n$spendingKey\n\nViewing Key:\n$viewingKey")
@ -79,7 +80,10 @@ class GetPrivateKeyFragment : BaseDemoFragment<FragmentGetPrivateKeyBinding>() {
return view return view
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
displayKeys() displayKeys()
} }

View File

@ -18,11 +18,12 @@ import kotlinx.coroutines.launch
*/ */
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
class HomeFragment : BaseDemoFragment<FragmentHomeBinding>() { class HomeFragment : BaseDemoFragment<FragmentHomeBinding>() {
override fun inflateBinding(layoutInflater: LayoutInflater) = FragmentHomeBinding.inflate(layoutInflater)
override fun inflateBinding(layoutInflater: LayoutInflater) = override fun onViewCreated(
FragmentHomeBinding.inflate(layoutInflater) view: View,
savedInstanceState: Bundle?
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { ) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding.textSeedPhrase.setOnClickListener(::onEditSeedPhrase) binding.textSeedPhrase.setOnClickListener(::onEditSeedPhrase)
binding.buttonPaste.setOnClickListener(::onPasteSeedPhrase) binding.buttonPaste.setOnClickListener(::onPasteSeedPhrase)

View File

@ -111,7 +111,10 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
initTransactionUI() initTransactionUI()
monitorChanges() monitorChanges()

View File

@ -23,7 +23,6 @@ class TransactionAdapter : ListAdapter<TransactionOverview, TransactionViewHolde
) = oldItem == newItem ) = oldItem == newItem
} }
) { ) {
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
viewType: Int viewType: Int

View File

@ -33,7 +33,12 @@ class TransactionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
} }
@Suppress("MagicNumber") @Suppress("MagicNumber")
fun bindTo(isInbound: Boolean, minedHeight: BlockHeight?, time: Long?, value: Zatoshi) { fun bindTo(
isInbound: Boolean,
minedHeight: BlockHeight?,
time: Long?,
value: Zatoshi
) {
amountText.text = value.convertZatoshiToZecString() amountText.text = value.convertZatoshiToZecString()
timeText.text = minedHeight?.let { timeText.text = minedHeight?.let {
time?.let { time?.let {

View File

@ -49,8 +49,8 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
private val isSynced get() = status == Synchronizer.Status.SYNCED private val isSynced get() = status == Synchronizer.Status.SYNCED
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentListUtxosBinding = @Suppress("MaxLineLength")
FragmentListUtxosBinding.inflate(layoutInflater) override fun inflateBinding(layoutInflater: LayoutInflater): FragmentListUtxosBinding = FragmentListUtxosBinding.inflate(layoutInflater)
private fun initUi() { private fun initUi() {
binding.inputAddress.setText(address) binding.inputAddress.setText(address)
@ -131,7 +131,10 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
// private val now get() = System.currentTimeMillis() // private val now get() = System.currentTimeMillis()
private fun updateStatus(message: String, append: Boolean = true) { private fun updateStatus(
message: String,
append: Boolean = true
) {
if (append) { if (append) {
binding.textStatus.text = "${binding.textStatus.text} $message" binding.textStatus.text = "${binding.textStatus.text} $message"
} else { } else {
@ -140,7 +143,10 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
Twig.debug { message } Twig.debug { message }
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
initUi() initUi()
monitorStatus() monitorStatus()

View File

@ -23,7 +23,6 @@ class UtxoAdapter : ListAdapter<TransactionOverview, UtxoViewHolder>(
) = oldItem == newItem ) = oldItem == newItem
} }
) { ) {
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
viewType: Int viewType: Int

View File

@ -25,7 +25,11 @@ class UtxoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
} }
@Suppress("MagicNumber") @Suppress("MagicNumber")
private fun bindToHelper(amount: Zatoshi, minedHeight: BlockHeight?, time: Long?) { private fun bindToHelper(
amount: Zatoshi,
minedHeight: BlockHeight?,
time: Long?
) {
amountText.text = amount.convertZatoshiToZecString() amountText.text = amount.convertZatoshiToZecString()
timeText.text = minedHeight?.let { timeText.text = minedHeight?.let {
time?.let { time?.let {

View File

@ -34,7 +34,6 @@ import kotlinx.coroutines.launch
*/ */
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
class SendFragment : BaseDemoFragment<FragmentSendBinding>() { class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
private lateinit var amountInput: TextView private lateinit var amountInput: TextView
private lateinit var addressInput: TextView private lateinit var addressInput: TextView
@ -67,12 +66,14 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
// //
private fun initSendUi() { private fun initSendUi() {
amountInput = binding.inputAmount.apply { amountInput =
setText(DemoConstants.SEND_AMOUNT.toZecString()) binding.inputAmount.apply {
} setText(DemoConstants.SEND_AMOUNT.toZecString())
addressInput = binding.inputAddress.apply { }
setText(DemoConstants.TO_ADDRESS) addressInput =
} binding.inputAddress.apply {
setText(DemoConstants.TO_ADDRESS)
}
binding.buttonSend.setOnClickListener(::onSend) binding.buttonSend.setOnClickListener(::onSend)
} }
@ -130,10 +131,11 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
private fun onBalance(balance: WalletBalance?) { private fun onBalance(balance: WalletBalance?) {
this.balance = balance this.balance = balance
if (!isSyncing) { if (!isSyncing) {
binding.textBalance.text = """ binding.textBalance.text =
"""
Available balance: ${balance?.available.convertZatoshiToZecString(12)} Available balance: ${balance?.available.convertZatoshiToZecString(12)}
Total balance: ${balance?.total.convertZatoshiToZecString(12)} Total balance: ${balance?.total.convertZatoshiToZecString(12)}
""".trimIndent() """.trimIndent()
} }
} }
@ -191,7 +193,10 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
return view return view
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
initSendUi() initSendUi()
monitorChanges() monitorChanges()
@ -200,7 +205,6 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
// //
// BaseDemoFragment overrides // BaseDemoFragment overrides
// //
@Suppress("MaxLineLength")
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentSendBinding = override fun inflateBinding(layoutInflater: LayoutInflater): FragmentSendBinding = FragmentSendBinding.inflate(layoutInflater)
FragmentSendBinding.inflate(layoutInflater)
} }

View File

@ -10,7 +10,6 @@ import cash.z.ecc.android.sdk.model.Zatoshi
@Suppress("MagicNumber") @Suppress("MagicNumber")
object WalletSnapshotFixture { object WalletSnapshotFixture {
val STATUS = Synchronizer.Status.SYNCED val STATUS = Synchronizer.Status.SYNCED
val PROGRESS = PercentDecimal.ZERO_PERCENT val PROGRESS = PercentDecimal.ZERO_PERCENT
val TRANSPARENT_BALANCE: WalletBalance = WalletBalance(Zatoshi(8), Zatoshi(1)) val TRANSPARENT_BALANCE: WalletBalance = WalletBalance(Zatoshi(8), Zatoshi(1))
@ -21,11 +20,12 @@ 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 =
null, 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

@ -25,8 +25,7 @@ import java.util.concurrent.Executors
* For a given preference file, it is expected that only a single instance is constructed and that * For a given preference file, it is expected that only a single instance is constructed and that
* this instance lives for the lifetime of the application. Constructing multiple instances will * this instance lives for the lifetime of the application. Constructing multiple instances will
* potentially corrupt preference data and will leak resources. * potentially corrupt preference data and will leak resources.
*/ *
/*
* Implementation note: EncryptedSharedPreferences are not thread-safe, so this implementation * Implementation note: EncryptedSharedPreferences are not thread-safe, so this implementation
* confines them to a single background thread. * confines them to a single background thread.
*/ */
@ -34,13 +33,16 @@ class AndroidPreferenceProvider(
private val sharedPreferences: SharedPreferences, private val sharedPreferences: SharedPreferences,
private val dispatcher: CoroutineDispatcher private val dispatcher: CoroutineDispatcher
) : PreferenceProvider { ) : PreferenceProvider {
override suspend fun hasKey(key: Key) =
override suspend fun hasKey(key: Key) = withContext(dispatcher) { withContext(dispatcher) {
sharedPreferences.contains(key.key) sharedPreferences.contains(key.key)
} }
@SuppressLint("ApplySharedPref") @SuppressLint("ApplySharedPref")
override suspend fun putString(key: Key, value: String?) = withContext(dispatcher) { override suspend fun putString(
key: Key,
value: String?
) = withContext(dispatcher) {
val editor = sharedPreferences.edit() val editor = sharedPreferences.edit()
editor.putString(key.key, value) editor.putString(key.key, value)
@ -50,65 +52,77 @@ class AndroidPreferenceProvider(
Unit Unit
} }
override suspend fun getString(key: Key) = withContext(dispatcher) { override suspend fun getString(key: Key) =
sharedPreferences.getString(key.key, null) withContext(dispatcher) {
} sharedPreferences.getString(key.key, null)
}
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override fun observe(key: Key): Flow<Unit> = callbackFlow<Unit> { override fun observe(key: Key): Flow<Unit> =
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> callbackFlow<Unit> {
// Callback on main thread val listener =
SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
// Callback on main thread
trySend(Unit)
}
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
// Kickstart the emissions
trySend(Unit) trySend(Unit)
}
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
// Kickstart the emissions awaitClose {
trySend(Unit) sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
}
awaitClose { }.flowOn(dispatcher)
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
}
}.flowOn(dispatcher)
companion object { companion object {
suspend fun newStandard(context: Context, filename: String): PreferenceProvider { suspend fun newStandard(
context: Context,
filename: String
): PreferenceProvider {
/* /*
* Because of this line, we don't want multiple instances of this object created * Because of this line, we don't want multiple instances of this object created
* because we don't clean up the thread afterwards. * because we don't clean up the thread afterwards.
*/ */
val singleThreadedDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() val singleThreadedDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
val sharedPreferences = withContext(singleThreadedDispatcher) { val sharedPreferences =
context.getSharedPreferences(filename, Context.MODE_PRIVATE) withContext(singleThreadedDispatcher) {
} context.getSharedPreferences(filename, Context.MODE_PRIVATE)
}
return AndroidPreferenceProvider(sharedPreferences, singleThreadedDispatcher) return AndroidPreferenceProvider(sharedPreferences, singleThreadedDispatcher)
} }
suspend fun newEncrypted(context: Context, filename: String): PreferenceProvider { suspend fun newEncrypted(
context: Context,
filename: String
): PreferenceProvider {
/* /*
* Because of this line, we don't want multiple instances of this object created * Because of this line, we don't want multiple instances of this object created
* because we don't clean up the thread afterwards. * because we don't clean up the thread afterwards.
*/ */
val singleThreadedDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() val singleThreadedDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
val mainKey = withContext(singleThreadedDispatcher) { val mainKey =
@Suppress("BlockingMethodInNonBlockingContext") withContext(singleThreadedDispatcher) {
MasterKey.Builder(context).apply { @Suppress("BlockingMethodInNonBlockingContext")
setKeyScheme(MasterKey.KeyScheme.AES256_GCM) MasterKey.Builder(context).apply {
}.build() setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
} }.build()
}
val sharedPreferences = withContext(singleThreadedDispatcher) { val sharedPreferences =
@Suppress("BlockingMethodInNonBlockingContext") withContext(singleThreadedDispatcher) {
EncryptedSharedPreferences.create( @Suppress("BlockingMethodInNonBlockingContext")
context, EncryptedSharedPreferences.create(
filename, context,
mainKey, filename,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, mainKey,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
) EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
} )
}
return AndroidPreferenceProvider(sharedPreferences, singleThreadedDispatcher) return AndroidPreferenceProvider(sharedPreferences, singleThreadedDispatcher)
} }

View File

@ -3,6 +3,5 @@ package cash.z.ecc.android.sdk.demoapp.preference
import cash.z.ecc.android.sdk.demoapp.preference.model.entry.Key import cash.z.ecc.android.sdk.demoapp.preference.model.entry.Key
object EncryptedPreferenceKeys { object EncryptedPreferenceKeys {
val PERSISTABLE_WALLET = PersistableWalletPreferenceDefault(Key("persistable_wallet")) val PERSISTABLE_WALLET = PersistableWalletPreferenceDefault(Key("persistable_wallet"))
} }

View File

@ -5,12 +5,12 @@ import cash.z.ecc.android.sdk.demoapp.preference.api.PreferenceProvider
import cash.z.ecc.android.sdk.demoapp.util.SuspendingLazy import cash.z.ecc.android.sdk.demoapp.util.SuspendingLazy
object EncryptedPreferenceSingleton { object EncryptedPreferenceSingleton {
private const val PREF_FILENAME = "co.electriccoin.zcash.encrypted" private const val PREF_FILENAME = "co.electriccoin.zcash.encrypted"
private val lazy = SuspendingLazy<Context, PreferenceProvider> { private val lazy =
AndroidPreferenceProvider.newEncrypted(it, PREF_FILENAME) SuspendingLazy<Context, PreferenceProvider> {
} AndroidPreferenceProvider.newEncrypted(it, PREF_FILENAME)
}
suspend fun getInstance(context: Context) = lazy.getInstance(context) suspend fun getInstance(context: Context) = lazy.getInstance(context)
} }

View File

@ -9,7 +9,6 @@ import org.json.JSONObject
data class PersistableWalletPreferenceDefault( data class PersistableWalletPreferenceDefault(
override val key: Key override val key: Key
) : PreferenceDefault<PersistableWallet?> { ) : PreferenceDefault<PersistableWallet?> {
override suspend fun getValue(preferenceProvider: PreferenceProvider) = override suspend fun getValue(preferenceProvider: PreferenceProvider) =
preferenceProvider.getString(key)?.let { PersistableWallet.from(JSONObject(it)) } preferenceProvider.getString(key)?.let { PersistableWallet.from(JSONObject(it)) }

View File

@ -4,10 +4,12 @@ import cash.z.ecc.android.sdk.demoapp.preference.model.entry.Key
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface PreferenceProvider { interface PreferenceProvider {
suspend fun hasKey(key: Key): Boolean suspend fun hasKey(key: Key): Boolean
suspend fun putString(key: Key, value: String?) suspend fun putString(
key: Key,
value: String?
)
suspend fun getString(key: Key): String? suspend fun getString(key: Key): String?

View File

@ -6,19 +6,22 @@ data class BooleanPreferenceDefault(
override val key: Key, override val key: Key,
private val defaultValue: Boolean private val defaultValue: Boolean
) : PreferenceDefault<Boolean> { ) : PreferenceDefault<Boolean> {
@Suppress("SwallowedException") @Suppress("SwallowedException")
override suspend fun getValue(preferenceProvider: PreferenceProvider) = preferenceProvider.getString(key)?.let { override suspend fun getValue(preferenceProvider: PreferenceProvider) =
try { preferenceProvider.getString(key)?.let {
it.toBooleanStrict() try {
} catch (e: IllegalArgumentException) { it.toBooleanStrict()
// TODO [#32]: Log coercion failure instead of just silently returning default } catch (e: IllegalArgumentException) {
// TODO [#32]: https://github.com/zcash/zcash-android-wallet-sdk/issues/32 // TODO [#32]: Log coercion failure instead of just silently returning default
defaultValue // TODO [#32]: https://github.com/zcash/zcash-android-wallet-sdk/issues/32
} defaultValue
} ?: defaultValue }
} ?: defaultValue
override suspend fun putValue(preferenceProvider: PreferenceProvider, newValue: Boolean) { override suspend fun putValue(
preferenceProvider: PreferenceProvider,
newValue: Boolean
) {
preferenceProvider.putString(key, newValue.toString()) preferenceProvider.putString(key, newValue.toString())
} }
} }

View File

@ -6,18 +6,21 @@ data class IntegerPreferenceDefault(
override val key: Key, override val key: Key,
private val defaultValue: Int private val defaultValue: Int
) : PreferenceDefault<Int> { ) : PreferenceDefault<Int> {
override suspend fun getValue(preferenceProvider: PreferenceProvider) =
preferenceProvider.getString(key)?.let {
try {
it.toInt()
} catch (e: NumberFormatException) {
// TODO [#32]: Log coercion failure instead of just silently returning default
// TODO [#32]: https://github.com/zcash/zcash-android-wallet-sdk/issues/32
defaultValue
}
} ?: defaultValue
override suspend fun getValue(preferenceProvider: PreferenceProvider) = preferenceProvider.getString(key)?.let { override suspend fun putValue(
try { preferenceProvider: PreferenceProvider,
it.toInt() newValue: Int
} catch (e: NumberFormatException) { ) {
// TODO [#32]: Log coercion failure instead of just silently returning default
// TODO [#32]: https://github.com/zcash/zcash-android-wallet-sdk/issues/32
defaultValue
}
} ?: defaultValue
override suspend fun putValue(preferenceProvider: PreferenceProvider, newValue: Int) {
preferenceProvider.putString(key, newValue.toString()) preferenceProvider.putString(key, newValue.toString())
} }
} }

View File

@ -10,8 +10,7 @@ import kotlinx.coroutines.flow.map
* multiple parts of the code can fetch the same preference without duplication or accidental * multiple parts of the code can fetch the same preference without duplication or accidental
* variation in default value. Clients define the key and default value together, rather than just * variation in default value. Clients define the key and default value together, rather than just
* the key. * the key.
*/ *
/*
* API note: the default value is not available through the public interface in order to prevent * API note: the default value is not available through the public interface in order to prevent
* clients from accidentally using the default value instead of the preference value. * clients from accidentally using the default value instead of the preference value.
* *
@ -21,7 +20,6 @@ import kotlinx.coroutines.flow.map
* and perhaps many Integer values will also fit within the autoboxing cache. * and perhaps many Integer values will also fit within the autoboxing cache.
*/ */
interface PreferenceDefault<T> { interface PreferenceDefault<T> {
val key: Key val key: Key
/** /**
@ -34,14 +32,18 @@ interface PreferenceDefault<T> {
* @param preferenceProvider Provides actual preference values. * @param preferenceProvider Provides actual preference values.
* @param newValue New value to write. * @param newValue New value to write.
*/ */
suspend fun putValue(preferenceProvider: PreferenceProvider, newValue: T) suspend fun putValue(
preferenceProvider: PreferenceProvider,
newValue: T
)
/** /**
* @param preferenceProvider Provides actual preference values. * @param preferenceProvider Provides actual preference values.
* @return Flow that emits preference changes. Note that implementations should emit an initial value * @return Flow that emits preference changes. Note that implementations should emit an initial value
* indicating what was stored in the preferences, in addition to subsequent updates. * indicating what was stored in the preferences, in addition to subsequent updates.
*/ */
fun observe(preferenceProvider: PreferenceProvider): Flow<T> = preferenceProvider.observe(key) fun observe(preferenceProvider: PreferenceProvider): Flow<T> =
.map { getValue(preferenceProvider) } preferenceProvider.observe(key)
.distinctUntilChanged() .map { getValue(preferenceProvider) }
.distinctUntilChanged()
} }

View File

@ -6,11 +6,14 @@ data class StringPreferenceDefault(
override val key: Key, override val key: Key,
private val defaultValue: String private val defaultValue: String
) : PreferenceDefault<String> { ) : PreferenceDefault<String> {
override suspend fun getValue(preferenceProvider: PreferenceProvider) =
preferenceProvider.getString(key)
?: defaultValue
override suspend fun getValue(preferenceProvider: PreferenceProvider) = preferenceProvider.getString(key) override suspend fun putValue(
?: defaultValue preferenceProvider: PreferenceProvider,
newValue: String
override suspend fun putValue(preferenceProvider: PreferenceProvider, newValue: String) { ) {
preferenceProvider.putString(key, newValue) preferenceProvider.putString(key, newValue)
} }
} }

View File

@ -9,6 +9,7 @@ import cash.z.ecc.android.sdk.ext.collectWith
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@Composable @Composable
@Suppress("ktlint:standard:function-naming")
fun ComponentActivity.BindCompLocalProvider(content: @Composable () -> Unit) { fun ComponentActivity.BindCompLocalProvider(content: @Composable () -> Unit) {
val screenTimeout = ScreenTimeout() val screenTimeout = ScreenTimeout()
observeScreenTimeoutFlag(screenTimeout) observeScreenTimeoutFlag(screenTimeout)

View File

@ -18,41 +18,43 @@ import kotlin.time.TimeSource
fun <T> Flow<T>.throttle( fun <T> Flow<T>.throttle(
duration: Duration, duration: Duration,
timeSource: TimeSource = TimeSource.Monotonic timeSource: TimeSource = TimeSource.Monotonic
): Flow<T> = flow { ): Flow<T> =
coroutineScope { flow {
val context = coroutineContext coroutineScope {
val mutex = Mutex() val context = coroutineContext
val mutex = Mutex()
var timeMark = timeSource.markNow() var timeMark = timeSource.markNow()
var delayEmit: Deferred<Unit>? = null var delayEmit: Deferred<Unit>? = null
var firstValue = true var firstValue = true
var valueToEmit: T var valueToEmit: T
collect { value -> collect { value ->
if (firstValue) { if (firstValue) {
firstValue = false firstValue = false
emit(value) emit(value)
timeMark = timeSource.markNow()
return@collect
}
delayEmit?.cancel()
valueToEmit = value
if (timeMark.elapsedNow() >= duration) {
mutex.withLock {
emit(valueToEmit)
timeMark = timeSource.markNow() timeMark = timeSource.markNow()
return@collect
} }
} else { delayEmit?.cancel()
delayEmit = async(Dispatchers.Default) { valueToEmit = value
if (timeMark.elapsedNow() >= duration) {
mutex.withLock { mutex.withLock {
delay(duration) emit(valueToEmit)
withContext(context) {
emit(valueToEmit)
}
timeMark = timeSource.markNow() timeMark = timeSource.markNow()
} }
} else {
delayEmit =
async(Dispatchers.Default) {
mutex.withLock {
delay(duration)
withContext(context) {
emit(valueToEmit)
}
timeMark = timeSource.markNow()
}
}
} }
} }
} }
} }
}

View File

@ -29,6 +29,7 @@ class ScreenTimeout {
val LocalScreenTimeout = compositionLocalOf { ScreenTimeout() } val LocalScreenTimeout = compositionLocalOf { ScreenTimeout() }
@Composable @Composable
@Suppress("ktlint:standard:function-naming")
fun DisableScreenTimeout() { fun DisableScreenTimeout() {
val screenTimeout = LocalScreenTimeout.current val screenTimeout = LocalScreenTimeout.current
DisposableEffect(screenTimeout) { DisposableEffect(screenTimeout) {

View File

@ -31,6 +31,7 @@ import kotlinx.coroutines.flow.flow
@Preview(name = "Addresses") @Preview(name = "Addresses")
@Composable @Composable
@Suppress("ktlint:standard:function-naming")
private fun ComposablePreview() { private fun ComposablePreview() {
MaterialTheme { MaterialTheme {
// TODO [#1090]: Demo: Add Addresses and Transactions Compose Previews // TODO [#1090]: Demo: Add Addresses and Transactions Compose Previews
@ -43,6 +44,7 @@ private fun ComposablePreview() {
* @param copyToClipboard First string is a tag, the second string is the text to copy. * @param copyToClipboard First string is a tag, the second string is the text to copy.
*/ */
@Composable @Composable
@Suppress("ktlint:standard:function-naming")
fun Addresses( fun Addresses(
synchronizer: Synchronizer, synchronizer: Synchronizer,
copyToClipboard: (String, String) -> Unit, copyToClipboard: (String, String) -> Unit,
@ -55,11 +57,12 @@ fun Addresses(
) { paddingValues -> ) { paddingValues ->
// TODO [#846]: Slow addresses providing // TODO [#846]: Slow addresses providing
// TODO [#846]: https://github.com/zcash/zcash-android-wallet-sdk/issues/846 // TODO [#846]: https://github.com/zcash/zcash-android-wallet-sdk/issues/846
val walletAddresses = flow { val walletAddresses =
emit(WalletAddresses.new(synchronizer)) flow {
}.collectAsState( emit(WalletAddresses.new(synchronizer))
initial = null }.collectAsState(
).value initial = null
).value
if (null != walletAddresses) { if (null != walletAddresses) {
AddressesMainContent( AddressesMainContent(
paddingValues = paddingValues, paddingValues = paddingValues,
@ -72,6 +75,7 @@ fun Addresses(
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Suppress("ktlint:standard:function-naming")
private fun AddressesTopAppBar(onBack: () -> Unit) { private fun AddressesTopAppBar(onBack: () -> Unit) {
TopAppBar( TopAppBar(
title = { Text(text = stringResource(id = R.string.menu_address)) }, title = { Text(text = stringResource(id = R.string.menu_address)) },
@ -89,6 +93,7 @@ private fun AddressesTopAppBar(onBack: () -> Unit) {
} }
@Composable @Composable
@Suppress("ktlint:standard:function-naming")
private fun AddressesMainContent( private fun AddressesMainContent(
paddingValues: PaddingValues, paddingValues: PaddingValues,
addresses: WalletAddresses, addresses: WalletAddresses,

View File

@ -31,6 +31,7 @@ import cash.z.ecc.android.sdk.model.toZecString
@Preview(name = "Balance") @Preview(name = "Balance")
@Composable @Composable
@Suppress("ktlint:standard:function-naming")
private fun ComposablePreview() { private fun ComposablePreview() {
MaterialTheme { MaterialTheme {
Balance( Balance(
@ -44,6 +45,7 @@ private fun ComposablePreview() {
} }
@Composable @Composable
@Suppress("ktlint:standard:function-naming")
fun Balance( fun Balance(
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
sendState: SendState, sendState: SendState,
@ -69,6 +71,7 @@ fun Balance(
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Suppress("ktlint:standard:function-naming")
private fun BalanceTopAppBar( private fun BalanceTopAppBar(
onBack: () -> Unit, onBack: () -> Unit,
onRefresh: () -> Unit onRefresh: () -> Unit
@ -97,6 +100,7 @@ private fun BalanceTopAppBar(
} }
@Composable @Composable
@Suppress("ktlint:standard:function-naming")
private fun BalanceMainContent( private fun BalanceMainContent(
paddingValues: PaddingValues, paddingValues: PaddingValues,
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,

View File

@ -33,6 +33,7 @@ import cash.z.ecc.android.sdk.demoapp.ui.screen.home.viewmodel.WalletSnapshot
@Preview(name = "Home") @Preview(name = "Home")
@Composable @Composable
@Suppress("ktlint:standard:function-naming")
private fun ComposablePreviewHome() { private fun ComposablePreviewHome() {
MaterialTheme { MaterialTheme {
Home( Home(
@ -49,8 +50,8 @@ private fun ComposablePreviewHome() {
} }
} }
@Suppress("LongParameterList")
@Composable @Composable
@Suppress("LongParameterList", "ktlint:standard:function-naming")
fun Home( fun Home(
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
isTestnet: Boolean, isTestnet: Boolean,
@ -83,6 +84,7 @@ fun Home(
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Suppress("ktlint:standard:function-naming")
private fun HomeTopAppBar( private fun HomeTopAppBar(
isTestnet: Boolean, isTestnet: Boolean,
goTestnetFaucet: () -> Unit, goTestnetFaucet: () -> Unit,
@ -103,6 +105,7 @@ private fun HomeTopAppBar(
} }
@Composable @Composable
@Suppress("ktlint:standard:function-naming")
private fun DebugMenu( private fun DebugMenu(
isTestnet: Boolean, isTestnet: Boolean,
goTestnetFaucet: () -> Unit, goTestnetFaucet: () -> Unit,
@ -146,7 +149,7 @@ private fun DebugMenu(
} }
@Composable @Composable
@Suppress("LongParameterList") @Suppress("LongParameterList", "ktlint:standard:function-naming")
private fun HomeMainContent( private fun HomeMainContent(
paddingValues: PaddingValues, paddingValues: PaddingValues,
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,

View File

@ -19,8 +19,9 @@ data class WalletSnapshot(
// TODO [#776]: Support variable fees // TODO [#776]: Support variable fees
// TODO [#776]: https://github.com/zcash/zcash-android-wallet-sdk/issues/776 // TODO [#776]: https://github.com/zcash/zcash-android-wallet-sdk/issues/776
// Note: the wallet is effectively empty if it cannot cover the miner's fee // Note: the wallet is effectively empty if it cannot cover the miner's fee
val hasFunds = saplingBalance.available.value > val hasFunds =
(ZcashSdk.MINERS_FEE.value.toDouble() / Zatoshi.ZATOSHI_PER_ZEC) // 0.0001 saplingBalance.available.value >
(ZcashSdk.MINERS_FEE.value.toDouble() / Zatoshi.ZATOSHI_PER_ZEC) // 0.0001
val hasSaplingBalance = saplingBalance.total.value > 0 val hasSaplingBalance = saplingBalance.total.value > 0
val isSendEnabled: Boolean get() = status == Synchronizer.Status.SYNCED && hasFunds val isSendEnabled: Boolean get() = status == Synchronizer.Status.SYNCED && hasFunds

View File

@ -66,68 +66,74 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
/** /**
* Synchronizer that is retained long enough to survive configuration changes. * Synchronizer that is retained long enough to survive configuration changes.
*/ */
val synchronizer = walletCoordinator.synchronizer.stateIn( val synchronizer =
viewModelScope, walletCoordinator.synchronizer.stateIn(
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
null
)
val secretState: StateFlow<SecretState> = walletCoordinator.persistableWallet
.map { persistableWallet ->
if (null == persistableWallet) {
SecretState.None
} else {
SecretState.Ready(persistableWallet)
}
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
SecretState.Loading
)
val spendingKey = secretState
.filterIsInstance<SecretState.Ready>()
.map { it.persistableWallet }
.map {
val bip39Seed = withContext(Dispatchers.IO) {
Mnemonics.MnemonicCode(it.seedPhrase.joinToString()).toSeed()
}
DerivationTool.getInstance().deriveUnifiedSpendingKey(
seed = bip39Seed,
network = it.network,
account = Account.DEFAULT
)
}.stateIn(
viewModelScope, viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
null null
) )
val secretState: StateFlow<SecretState> =
walletCoordinator.persistableWallet
.map { persistableWallet ->
if (null == persistableWallet) {
SecretState.None
} else {
SecretState.Ready(persistableWallet)
}
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
SecretState.Loading
)
val spendingKey =
secretState
.filterIsInstance<SecretState.Ready>()
.map { it.persistableWallet }
.map {
val bip39Seed =
withContext(Dispatchers.IO) {
Mnemonics.MnemonicCode(it.seedPhrase.joinToString()).toSeed()
}
DerivationTool.getInstance().deriveUnifiedSpendingKey(
seed = bip39Seed,
network = it.network,
account = Account.DEFAULT
)
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
null
)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
val walletSnapshot: StateFlow<WalletSnapshot?> = synchronizer val walletSnapshot: StateFlow<WalletSnapshot?> =
.flatMapLatest { synchronizer
if (null == it) { .flatMapLatest {
flowOf(null) if (null == it) {
} else { flowOf(null)
it.toWalletSnapshot() } else {
it.toWalletSnapshot()
}
} }
} .throttle(1.seconds)
.throttle(1.seconds) .stateIn(
.stateIn( viewModelScope,
viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), null
null )
)
val addresses: StateFlow<WalletAddresses?> = synchronizer val addresses: StateFlow<WalletAddresses?> =
.filterNotNull() synchronizer
.map { .filterNotNull()
WalletAddresses.new(it) .map {
}.stateIn( WalletAddresses.new(it)
viewModelScope, }.stateIn(
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), viewModelScope,
null SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
) null
)
private val mutableSendState = MutableStateFlow<SendState>(SendState.None) private val mutableSendState = MutableStateFlow<SendState>(SendState.None)
@ -136,8 +142,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
/** /**
* Creates a wallet asynchronously and then persists it. Clients observe * Creates a wallet asynchronously and then persists it. Clients observe
* [secretState] to see the side effects. This would be used for a user creating a new wallet. * [secretState] to see the side effects. This would be used for a user creating a new wallet.
*/ *
/*
* Although waiting for the wallet to be written and then read back is slower, it is probably * Although waiting for the wallet to be written and then read back is slower, it is probably
* safer because it 1. guarantees the wallet is written to disk and 2. has a single source of truth. * safer because it 1. guarantees the wallet is written to disk and 2. has a single source of truth.
*/ */
@ -146,12 +151,13 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
viewModelScope.launch { viewModelScope.launch {
val network = ZcashNetwork.fromResources(application) val network = ZcashNetwork.fromResources(application)
val newWallet = PersistableWallet.new( val newWallet =
application = application, PersistableWallet.new(
zcashNetwork = network, application = application,
endpoint = LightWalletEndpoint.defaultForNetwork(network), zcashNetwork = network,
walletInitMode = WalletInitMode.NewWallet endpoint = LightWalletEndpoint.defaultForNetwork(network),
) walletInitMode = WalletInitMode.NewWallet
)
persistWallet(newWallet) persistWallet(newWallet)
} }
} }
@ -268,7 +274,9 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
*/ */
sealed class SecretState { sealed class SecretState {
object Loading : SecretState() object Loading : SecretState()
object None : SecretState() object None : SecretState()
class Ready(val persistableWallet: PersistableWallet) : SecretState() class Ready(val persistableWallet: PersistableWallet) : SecretState()
} }
@ -276,22 +284,27 @@ sealed class SendState {
object None : SendState() { object None : SendState() {
override fun toString(): String = "None" override fun toString(): String = "None"
} }
object Sending : SendState() { object Sending : SendState() {
override fun toString(): String = "Sending" override fun toString(): String = "Sending"
} }
class Sent(val localTxId: Long) : SendState() { class Sent(val localTxId: Long) : SendState() {
override fun toString(): String = "Sent" override fun toString(): String = "Sent"
} }
class Error(val error: Throwable) : SendState() { class Error(val error: Throwable) : SendState() {
override fun toString(): String = "Error ${error.message}" override fun toString(): String = "Error ${error.message}"
} }
} }
// TODO [#529]: Localize Synchronizer Errors
// TODO [#529]: https://github.com/zcash/secant-android-wallet/issues/529
/** /**
* Represents all kind of Synchronizer errors * Represents all kind of Synchronizer errors
*/ */
// TODO [#529]: Localize Synchronizer Errors
// TODO [#529]: https://github.com/zcash/secant-android-wallet/issues/529
sealed class SynchronizerError { sealed class SynchronizerError {
abstract fun getCauseMessage(): String? abstract fun getCauseMessage(): String?
@ -316,51 +329,59 @@ sealed class SynchronizerError {
} }
} }
private fun Synchronizer.toCommonError(): Flow<SynchronizerError?> = callbackFlow { private fun Synchronizer.toCommonError(): Flow<SynchronizerError?> =
// just for initial default value emit callbackFlow {
trySend(null) // just for initial default value emit
trySend(null)
onCriticalErrorHandler = { onCriticalErrorHandler = {
Twig.error { "WALLET - Error Critical: $it" } Twig.error { "WALLET - Error Critical: $it" }
trySend(SynchronizerError.Critical(it)) trySend(SynchronizerError.Critical(it))
false false
} }
onProcessorErrorHandler = { onProcessorErrorHandler = {
Twig.error { "WALLET - Error Processor: $it" } Twig.error { "WALLET - Error Processor: $it" }
trySend(SynchronizerError.Processor(it)) trySend(SynchronizerError.Processor(it))
false false
} }
onSubmissionErrorHandler = { onSubmissionErrorHandler = {
Twig.error { "WALLET - Error Submission: $it" } Twig.error { "WALLET - Error Submission: $it" }
trySend(SynchronizerError.Submission(it)) trySend(SynchronizerError.Submission(it))
false false
} }
onSetupErrorHandler = { onSetupErrorHandler = {
Twig.error { "WALLET - Error Setup: $it" } Twig.error { "WALLET - Error Setup: $it" }
trySend(SynchronizerError.Setup(it)) trySend(SynchronizerError.Setup(it))
false false
} }
onChainErrorHandler = { x, y -> onChainErrorHandler = { x, y ->
Twig.error { "WALLET - Error Chain: $x, $y" } Twig.error { "WALLET - Error Chain: $x, $y" }
trySend(SynchronizerError.Chain(x, y)) trySend(SynchronizerError.Chain(x, y))
} }
awaitClose { awaitClose {
// nothing to close here // nothing to close here
}
} }
}
// No good way around needing magic numbers for the indices // No good way around needing magic numbers for the indices
@Suppress("MagicNumber") @Suppress("MagicNumber")
private fun Synchronizer.toWalletSnapshot() = private fun Synchronizer.toWalletSnapshot() =
combine( combine(
status, // 0 // 0
processorInfo, // 1 status,
orchardBalances, // 2 // 1
saplingBalances, // 3 processorInfo,
transparentBalances, // 4 // 2
progress, // 5 orchardBalances,
toCommonError() // 6 // 3
saplingBalances,
// 4
transparentBalances,
// 5
progress,
// 6
toCommonError()
) { flows -> ) { flows ->
val orchardBalance = flows[2] as WalletBalance? val orchardBalance = flows[2] as WalletBalance?
val saplingBalance = flows[3] as WalletBalance? val saplingBalance = flows[3] as WalletBalance?

View File

@ -26,6 +26,7 @@ import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
@Preview(name = "Seed") @Preview(name = "Seed")
@Composable @Composable
@Suppress("ktlint:standard:function-naming")
private fun ComposablePreview() { private fun ComposablePreview() {
MaterialTheme { MaterialTheme {
Seed( Seed(
@ -37,6 +38,7 @@ private fun ComposablePreview() {
} }
@Composable @Composable
@Suppress("ktlint:standard:function-naming")
fun Seed( fun Seed(
zcashNetwork: ZcashNetwork, zcashNetwork: ZcashNetwork,
onExistingWallet: (PersistableWallet) -> Unit, onExistingWallet: (PersistableWallet) -> Unit,
@ -56,6 +58,7 @@ fun Seed(
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Suppress("ktlint:standard:function-naming")
private fun ConfigureSeedTopAppBar() { private fun ConfigureSeedTopAppBar() {
TopAppBar( TopAppBar(
title = { Text(text = stringResource(id = R.string.configure_seed)) } title = { Text(text = stringResource(id = R.string.configure_seed)) }
@ -63,6 +66,7 @@ private fun ConfigureSeedTopAppBar() {
} }
@Composable @Composable
@Suppress("ktlint:standard:function-naming")
private fun ConfigureSeedMainContent( private fun ConfigureSeedMainContent(
paddingValues: PaddingValues, paddingValues: PaddingValues,
zcashNetwork: ZcashNetwork, zcashNetwork: ZcashNetwork,
@ -76,13 +80,14 @@ private fun ConfigureSeedMainContent(
) { ) {
Button( Button(
onClick = { onClick = {
val newWallet = PersistableWallet( val newWallet =
network = zcashNetwork, PersistableWallet(
endpoint = LightWalletEndpoint.defaultForNetwork(zcashNetwork), network = zcashNetwork,
birthday = WalletFixture.Alice.getBirthday(zcashNetwork), endpoint = LightWalletEndpoint.defaultForNetwork(zcashNetwork),
seedPhrase = SeedPhrase.new(WalletFixture.Alice.seedPhrase), birthday = WalletFixture.Alice.getBirthday(zcashNetwork),
walletInitMode = WalletInitMode.RestoreWallet seedPhrase = SeedPhrase.new(WalletFixture.Alice.seedPhrase),
) walletInitMode = WalletInitMode.RestoreWallet
)
onExistingWallet(newWallet) onExistingWallet(newWallet)
} }
) { ) {
@ -90,13 +95,14 @@ private fun ConfigureSeedMainContent(
} }
Button( Button(
onClick = { onClick = {
val newWallet = PersistableWallet( val newWallet =
network = zcashNetwork, PersistableWallet(
endpoint = LightWalletEndpoint.defaultForNetwork(zcashNetwork), network = zcashNetwork,
birthday = WalletFixture.Ben.getBirthday(zcashNetwork), endpoint = LightWalletEndpoint.defaultForNetwork(zcashNetwork),
seedPhrase = SeedPhrase.new(WalletFixture.Ben.seedPhrase), birthday = WalletFixture.Ben.getBirthday(zcashNetwork),
walletInitMode = WalletInitMode.RestoreWallet seedPhrase = SeedPhrase.new(WalletFixture.Ben.seedPhrase),
) walletInitMode = WalletInitMode.RestoreWallet
)
onExistingWallet(newWallet) onExistingWallet(newWallet)
} }
) { ) {

View File

@ -52,6 +52,7 @@ import cash.z.ecc.android.sdk.model.toZecString
@Preview(name = "Send") @Preview(name = "Send")
@Composable @Composable
@Suppress("ktlint:standard:function-naming")
private fun ComposablePreview() { private fun ComposablePreview() {
MaterialTheme { MaterialTheme {
Send( Send(
@ -64,6 +65,7 @@ private fun ComposablePreview() {
} }
@Composable @Composable
@Suppress("ktlint:standard:function-naming")
fun Send( fun Send(
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
sendState: SendState, sendState: SendState,
@ -84,6 +86,7 @@ fun Send(
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Suppress("ktlint:standard:function-naming")
private fun SendTopAppBar(onBack: () -> Unit) { private fun SendTopAppBar(onBack: () -> Unit) {
TopAppBar( TopAppBar(
title = { Text(text = stringResource(id = R.string.menu_send)) }, title = { Text(text = stringResource(id = R.string.menu_send)) },
@ -101,7 +104,7 @@ private fun SendTopAppBar(onBack: () -> Unit) {
} }
@Composable @Composable
@Suppress("LongMethod") @Suppress("LongMethod", "ktlint:standard:function-naming")
private fun SendMainContent( private fun SendMainContent(
paddingValues: PaddingValues, paddingValues: PaddingValues,
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
@ -227,20 +230,20 @@ private fun SendMainContent(
Button( Button(
onClick = { onClick = {
val zecSendValidation = ZecSendExt.new( val zecSendValidation =
context, ZecSendExt.new(
recipientAddressString, context,
amountZecString, recipientAddressString,
memoString, amountZecString,
monetarySeparators memoString,
) monetarySeparators
)
when (zecSendValidation) { when (zecSendValidation) {
is ZecSendExt.ZecSendValidation.Valid -> onSend(zecSendValidation.zecSend) is ZecSendExt.ZecSendValidation.Valid -> onSend(zecSendValidation.zecSend)
is ZecSendExt.ZecSendValidation.Invalid -> validation = zecSendValidation.validationErrors is ZecSendExt.ZecSendValidation.Invalid -> validation = zecSendValidation.validationErrors
} }
}, },
// Needs actual validation // Needs actual validation
enabled = amountZecString.isNotBlank() && recipientAddressString.isNotBlank() enabled = amountZecString.isNotBlank() && recipientAddressString.isNotBlank()
) { ) {

View File

@ -39,6 +39,7 @@ import kotlinx.coroutines.launch
@Preview(name = "Transactions") @Preview(name = "Transactions")
@Composable @Composable
@Suppress("ktlint:standard:function-naming")
private fun ComposablePreview() { private fun ComposablePreview() {
MaterialTheme { MaterialTheme {
// TODO [#1090]: Demo: Add Addresses and Transactions Compose Previews // TODO [#1090]: Demo: Add Addresses and Transactions Compose Previews
@ -48,6 +49,7 @@ private fun ComposablePreview() {
} }
@Composable @Composable
@Suppress("ktlint:standard:function-naming")
fun Transactions( fun Transactions(
synchronizer: Synchronizer, synchronizer: Synchronizer,
onBack: () -> Unit onBack: () -> Unit
@ -64,11 +66,12 @@ fun Transactions(
) { paddingValues -> ) { paddingValues ->
// TODO [#846]: Slow addresses providing // TODO [#846]: Slow addresses providing
// TODO [#846]: https://github.com/zcash/zcash-android-wallet-sdk/issues/846 // TODO [#846]: https://github.com/zcash/zcash-android-wallet-sdk/issues/846
val walletAddresses = flow { val walletAddresses =
emit(WalletAddresses.new(synchronizer)) flow {
}.collectAsState( emit(WalletAddresses.new(synchronizer))
initial = null }.collectAsState(
).value initial = null
).value
if (null != walletAddresses) { if (null != walletAddresses) {
TransactionsMainContent( TransactionsMainContent(
paddingValues = paddingValues, paddingValues = paddingValues,
@ -82,6 +85,7 @@ fun Transactions(
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Suppress("ktlint:standard:function-naming")
private fun TransactionsTopAppBar( private fun TransactionsTopAppBar(
onBack: () -> Unit, onBack: () -> Unit,
onRefresh: () -> Unit onRefresh: () -> Unit
@ -110,6 +114,7 @@ private fun TransactionsTopAppBar(
} }
@Composable @Composable
@Suppress("ktlint:standard:function-naming")
private fun TransactionsMainContent( private fun TransactionsMainContent(
paddingValues: PaddingValues, paddingValues: PaddingValues,
synchronizer: Synchronizer, synchronizer: Synchronizer,
@ -138,14 +143,16 @@ private fun TransactionsMainContent(
} }
} }
}) { }) {
val time = tx.minedHeight?.let { val time =
tx.blockTimeEpochSeconds?.let { kotlinx.datetime.Instant.fromEpochSeconds(it) } ?: "Unknown" tx.minedHeight?.let {
} ?: "Pending" tx.blockTimeEpochSeconds?.let { kotlinx.datetime.Instant.fromEpochSeconds(it) } ?: "Unknown"
val value = if (tx.isSentTransaction) { } ?: "Pending"
-tx.netValue.value val value =
} else { if (tx.isSentTransaction) {
tx.netValue.value -tx.netValue.value
} } else {
tx.netValue.value
}
Text("$time, $value") Text("$time, $value")
} }
} }

View File

@ -11,7 +11,9 @@ internal object AndroidApiVersion {
* [sdk]. * [sdk].
*/ */
@ChecksSdkIntAtLeast(parameter = 0) @ChecksSdkIntAtLeast(parameter = 0)
fun isAtLeast(@IntRange(from = Build.VERSION_CODES.BASE.toLong()) sdk: Int): Boolean { fun isAtLeast(
@IntRange(from = Build.VERSION_CODES.BASE.toLong()) sdk: Int
): Boolean {
return Build.VERSION.SDK_INT >= sdk return Build.VERSION.SDK_INT >= sdk
} }

View File

@ -94,7 +94,7 @@ FOOJAY_TOOLCHAIN_RESOLVER_VERSION=0.5.0
FULLADLE_VERSION=0.17.4 FULLADLE_VERSION=0.17.4
GRADLE_VERSIONS_PLUGIN_VERSION=0.50.0 GRADLE_VERSIONS_PLUGIN_VERSION=0.50.0
KSP_VERSION=1.8.20-1.0.10 KSP_VERSION=1.8.20-1.0.10
KTLINT_VERSION=0.50.0 KTLINT_VERSION=1.1.0
PROTOBUF_GRADLE_PLUGIN_VERSION=0.9.4 PROTOBUF_GRADLE_PLUGIN_VERSION=0.9.4
RUST_GRADLE_PLUGIN_VERSION=0.9.3 RUST_GRADLE_PLUGIN_VERSION=0.9.3

View File

@ -7,7 +7,6 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
class BenchmarkingExtTest { class BenchmarkingExtTest {
@Test @Test
@SmallTest @SmallTest
fun check_build_config() { fun check_build_config() {

View File

@ -5,7 +5,6 @@ import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
class BlockRangeFixtureTest { class BlockRangeFixtureTest {
@Test @Test
@SmallTest @SmallTest
fun compare_default_values() { fun compare_default_values() {

View File

@ -4,10 +4,7 @@ import io.grpc.Status
import io.grpc.StatusRuntimeException import io.grpc.StatusRuntimeException
object StatusExceptionFixture { object StatusExceptionFixture {
fun new(status: Status): StatusRuntimeException {
fun new(
status: Status
): StatusRuntimeException {
return StatusRuntimeException(status) return StatusRuntimeException(status)
} }
} }

View File

@ -6,7 +6,6 @@ import kotlin.test.Ignore
import kotlin.test.Test import kotlin.test.Test
class ChannelFactoryTest { class ChannelFactoryTest {
private val channelFactory = AndroidChannelFactory(getAppContext()) private val channelFactory = AndroidChannelFactory(getAppContext())
@Test @Test

View File

@ -10,7 +10,6 @@ import kotlin.test.assertNull
import kotlin.test.assertTrue import kotlin.test.assertTrue
class GrpcStatusResolverTest { class GrpcStatusResolverTest {
@Test @Test
@SmallTest @SmallTest
fun resolve_explicitly_caught_server_error_test() { fun resolve_explicitly_caught_server_error_test() {

View File

@ -6,7 +6,6 @@ import kotlin.test.Ignore
import kotlin.test.Test import kotlin.test.Test
class LightWalletClientImplTest { class LightWalletClientImplTest {
private val channelFactory = AndroidChannelFactory(getAppContext()) private val channelFactory = AndroidChannelFactory(getAppContext())
@Test @Test

View File

@ -6,7 +6,11 @@ import androidx.test.core.app.ApplicationProvider
fun getAppContext(): Context = ApplicationProvider.getApplicationContext() fun getAppContext(): Context = ApplicationProvider.getApplicationContext()
fun getStringResource(@StringRes resId: Int) = getAppContext().getString(resId) fun getStringResource(
@StringRes resId: Int
) = getAppContext().getString(resId)
fun getStringResourceWithArgs(@StringRes resId: Int, vararg formatArgs: String) = fun getStringResourceWithArgs(
getAppContext().getString(resId, *formatArgs) @StringRes resId: Int,
vararg formatArgs: String
) = getAppContext().getString(resId, *formatArgs)

View File

@ -19,7 +19,6 @@ import kotlinx.coroutines.flow.Flow
* Client for interacting with lightwalletd. * Client for interacting with lightwalletd.
*/ */
interface LightWalletClient { interface LightWalletClient {
/** /**
* @return the full transaction info. * @return the full transaction info.
*/ */
@ -116,5 +115,4 @@ interface LightWalletClient {
fun LightWalletClient.Companion.new( fun LightWalletClient.Companion.new(
context: Context, context: Context,
lightWalletEndpoint: LightWalletEndpoint lightWalletEndpoint: LightWalletEndpoint
): LightWalletClient = ): LightWalletClient = LightWalletClientImpl.new(AndroidChannelFactory(context), lightWalletEndpoint)
LightWalletClientImpl.new(AndroidChannelFactory(context), lightWalletEndpoint)

View File

@ -6,7 +6,6 @@ import androidx.annotation.VisibleForTesting
* Used for getting mocked blocks range for benchmarking purposes. * Used for getting mocked blocks range for benchmarking purposes.
*/ */
object BenchmarkingBlockRangeFixture { object BenchmarkingBlockRangeFixture {
// Be aware that changing these bounds values in a broader range may result in a timeout reached in // Be aware that changing these bounds values in a broader range may result in a timeout reached in
// SyncBlockchainBenchmark. So if changing these, don't forget to align also the test timeout in // SyncBlockchainBenchmark. So if changing these, don't forget to align also the test timeout in
// waitForBalanceScreen() appropriately. // waitForBalanceScreen() appropriately.

View File

@ -11,12 +11,10 @@ import kotlinx.coroutines.flow.asFlow
*/ */
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
object ListOfCompactBlocksFixture { object ListOfCompactBlocksFixture {
val DEFAULT_FILE_BLOCK_RANGE = FileBlockRangeFixture.new() val DEFAULT_FILE_BLOCK_RANGE = FileBlockRangeFixture.new()
fun newSequence( @Suppress("MaxLineLength")
blocksHeightRange: ClosedRange<BlockHeightUnsafe> = DEFAULT_FILE_BLOCK_RANGE fun newSequence(blocksHeightRange: ClosedRange<BlockHeightUnsafe> = DEFAULT_FILE_BLOCK_RANGE): Sequence<CompactBlockUnsafe> {
): Sequence<CompactBlockUnsafe> {
val blocks = mutableListOf<CompactBlockUnsafe>() val blocks = mutableListOf<CompactBlockUnsafe>()
for (blockHeight in blocksHeightRange.start.value..blocksHeightRange.endInclusive.value) { for (blockHeight in blocksHeightRange.start.value..blocksHeightRange.endInclusive.value) {
@ -28,9 +26,8 @@ object ListOfCompactBlocksFixture {
return blocks.asSequence() return blocks.asSequence()
} }
fun newFlow( @Suppress("MaxLineLength")
blocksHeightRange: ClosedRange<BlockHeightUnsafe> = DEFAULT_FILE_BLOCK_RANGE fun newFlow(blocksHeightRange: ClosedRange<BlockHeightUnsafe> = DEFAULT_FILE_BLOCK_RANGE): Flow<CompactBlockUnsafe> {
): Flow<CompactBlockUnsafe> {
return newSequence(blocksHeightRange).asFlow() return newSequence(blocksHeightRange).asFlow()
} }
} }

View File

@ -7,14 +7,15 @@ import java.nio.ByteBuffer
* Used for getting single mocked compact block for processing and persisting purposes. * Used for getting single mocked compact block for processing and persisting purposes.
*/ */
internal object SingleCompactBlockFixture { internal object SingleCompactBlockFixture {
internal const val DEFAULT_HEIGHT = 500_000L internal const val DEFAULT_HEIGHT = 500_000L
internal const val DEFAULT_TIME = 0 internal const val DEFAULT_TIME = 0
internal const val DEFAULT_SAPLING_OUTPUT_COUNT = 1u internal const val DEFAULT_SAPLING_OUTPUT_COUNT = 1u
internal const val DEFAULT_ORCHARD_OUTPUT_COUNT = 2u internal const val DEFAULT_ORCHARD_OUTPUT_COUNT = 2u
internal const val DEFAULT_HASH = DEFAULT_HEIGHT internal const val DEFAULT_HASH = DEFAULT_HEIGHT
internal const val DEFAULT_BLOCK_BYTES = DEFAULT_HEIGHT internal const val DEFAULT_BLOCK_BYTES = DEFAULT_HEIGHT
internal fun heightToFixtureData(height: Long) = BytesConversionHelper.longToBytes(height) internal fun heightToFixtureData(height: Long) = BytesConversionHelper.longToBytes(height)
internal fun fixtureDataToHeight(byteArray: ByteArray) = BytesConversionHelper.bytesToLong(byteArray) internal fun fixtureDataToHeight(byteArray: ByteArray) = BytesConversionHelper.bytesToLong(byteArray)
@Suppress("LongParameterList") @Suppress("LongParameterList")

View File

@ -3,6 +3,7 @@ package co.electriccoin.lightwallet.client.internal
internal object Constants { internal object Constants {
const val LOG_TAG = "LightWalletClient" // NON-NLS const val LOG_TAG = "LightWalletClient" // NON-NLS
const val ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE_COMMON = "Illegal argument provided:" // NON-NLS const val ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE_COMMON = "Illegal argument provided:" // NON-NLS
const val ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE_EMPTY = "$ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE_COMMON " + const val ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE_EMPTY =
"can't be empty:" // NON-NLS "$ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE_COMMON " +
"can't be empty:" // NON-NLS
} }

View File

@ -24,7 +24,6 @@ class DarksideApi private constructor(
private val channel: ManagedChannel, private val channel: ManagedChannel,
private val singleRequestTimeout: Duration = 10.seconds private val singleRequestTimeout: Duration = 10.seconds
) { ) {
companion object { companion object {
internal fun new( internal fun new(
channelFactory: ChannelFactory, channelFactory: ChannelFactory,
@ -38,7 +37,8 @@ class DarksideApi private constructor(
fun reset( fun reset(
saplingActivationHeight: BlockHeightUnsafe, saplingActivationHeight: BlockHeightUnsafe,
branchId: String = "e9ff75a6", // Canopy, // Canopy
branchId: String = "e9ff75a6",
chainName: String = "darksidemainnet" chainName: String = "darksidemainnet"
) = apply { ) = apply {
Darkside.DarksideMetaState.newBuilder() Darkside.DarksideMetaState.newBuilder()
@ -50,11 +50,15 @@ class DarksideApi private constructor(
} }
} }
fun stageBlocks(url: String) = apply { fun stageBlocks(url: String) =
createStub().stageBlocks(url.toUrl()) apply {
} createStub().stageBlocks(url.toUrl())
}
fun stageTransactions(url: String, targetHeight: BlockHeightUnsafe) = apply { fun stageTransactions(
url: String,
targetHeight: BlockHeightUnsafe
) = apply {
createStub().stageTransactions( createStub().stageTransactions(
DarksideTransactionsURL.newBuilder().setHeight(targetHeight.value.toInt()).setUrl(url).build() DarksideTransactionsURL.newBuilder().setHeight(targetHeight.value.toInt()).setUrl(url).build()
) )
@ -71,7 +75,10 @@ class DarksideApi private constructor(
) )
} }
fun stageTransactions(txs: Iterator<Service.RawTransaction>?, tipHeight: BlockHeightUnsafe) { fun stageTransactions(
txs: Iterator<Service.RawTransaction>?,
tipHeight: BlockHeightUnsafe
) {
if (txs == null) { if (txs == null) {
return return
} }
@ -147,6 +154,7 @@ class DarksideApi private constructor(
var completed = false var completed = false
var error: Throwable? = null var error: Throwable? = null
override fun onNext(value: Service.Empty?) { override fun onNext(value: Service.Empty?) {
// No implementation // No implementation
} }
@ -171,8 +179,7 @@ class DarksideApi private constructor(
} }
} }
private fun BlockHeightUnsafe.toHeight() = private fun BlockHeightUnsafe.toHeight() = Darkside.DarksideHeight.newBuilder().setHeight(this.value.toInt()).build()
Darkside.DarksideHeight.newBuilder().setHeight(this.value.toInt()).build()
fun DarksideApi.Companion.new( fun DarksideApi.Companion.new(
context: Context, context: Context,

Some files were not shown because too many files have changed in this diff Show More