[#757] Adopt SDK 1.14.0 (#759)

This commit is contained in:
Carter Jernigan 2023-02-17 06:05:23 -05:00 committed by GitHub
parent 7523619afa
commit f5e0af6545
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
101 changed files with 113 additions and 1981 deletions

View File

@ -1,57 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="sdk-ext-ui-lib:connectedCheck" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests">
<module name="zcash-android-app.sdk-ext-ui-lib" />
<option name="TESTING_TYPE" value="0" />
<option name="METHOD_NAME" value="" />
<option name="CLASS_NAME" value="" />
<option name="PACKAGE_NAME" value="" />
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
<option name="EXTRA_OPTIONS" value="" />
<option name="INCLUDE_GRADLE_EXTRA_OPTIONS" value="true" />
<option name="RETENTION_ENABLED" value="No" />
<option name="RETENTION_MAX_SNAPSHOTS" value="2" />
<option name="RETENTION_COMPRESS_SNAPSHOTS" value="false" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
<option name="FORCE_STOP_RUNNING_APP" value="true" />
<option name="INSPECTION_WITHOUT_ACTIVITY_RESTART" value="false" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Hybrid>
<Java />
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Java/Kotlin Method Sample (legacy)" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>

View File

@ -26,10 +26,7 @@ class ZcashApplication : CoroutineApplication() {
Twig.initialize(applicationContext)
Twig.info { "Starting application…" }
if (BuildConfig.DEBUG) {
// This is an internal API to the Zcash SDK to enable logging; it could change in the future
cash.z.ecc.android.sdk.internal.Twig.enabled(true)
} else {
if (!BuildConfig.DEBUG) {
// In release builds, logs should be stripped by R8 rules
Twig.assertLoggingStripped()
}

View File

@ -41,7 +41,6 @@ The logical components of the app are implemented as a number of Gradle modules.
* `preference-impl-android-lib` — Android-specific implementation for preference storage.
* sdk
* `sdk-ext-lib` — Contains extensions on top of the to the Zcash SDK. Some of these extensions might be migrated into the SDK eventually, while others might represent Android-centric idioms. Depending on how this module evolves, it could adopt another name such as `wallet-lib` or be split into two.
* `sdk-ext-ui` — Place for Zcash SDK components (same as `sdk-ext-lib`), which are related to the UI (e.g. depend on user locale and thus need to be translated via `strings.xml`).
* spackle — Random utilities, to fill in the cracks in the frameworks.
* `spackle-lib` — Multiplatform implementation for Kotlin and JVM
* `spackle-android-lib` — Android-specific additions.
@ -55,9 +54,8 @@ The following diagram shows a rough depiction of dependencies between the module
subgraph sdk
sdkLib[[sdk-lib]];
sdkExtLib[[sdk-ext-lib]];
sdkExtUi[[sdk-ext-ui]];
end
sdkLib[[sdk-lib]] --> sdkExtLib[[sdk-ext-lib]] --> sdkExtUi[[sdk-ext-ui]];
sdkLib[[sdk-lib]] --> sdkExtLib[[sdk-ext-lib]];
subgraph preference
preferenceApiLib[[preference-api-lib]];
preferenceImplAndroidLib[[preference-impl-android-lib]];

View File

@ -149,7 +149,7 @@ ZCASH_ANDROID_WALLET_PLUGINS_VERSION=1.0.0
ZCASH_BIP39_VERSION=1.0.4
# TODO [#279]: Revert to stable SDK before app release
# TODO [#279]: https://github.com/zcash/secant-android-wallet/issues/279
ZCASH_SDK_VERSION=1.12.0-beta01-SNAPSHOT
ZCASH_SDK_VERSION=1.14.0-beta01-SNAPSHOT
ZXING_VERSION=3.5.0

View File

@ -17,6 +17,7 @@ dependencies {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
api(libs.zcash.sdk)
api(libs.zcash.sdk.incubator)
api(libs.zcash.bip39)
androidTestImplementation(libs.bundles.androidx.test)
@ -37,4 +38,3 @@ dependencies {
}
}
}

View File

@ -1,21 +0,0 @@
package cash.z.ecc.sdk.ext
import kotlin.test.Test
import kotlin.test.assertEquals
class StringExtTest {
@Test
fun sizeInBytes_empty() {
assertEquals(0, "".sizeInUtf8Bytes())
}
@Test
fun sizeInBytes_one() {
assertEquals(1, "a".sizeInUtf8Bytes())
}
@Test
fun sizeInBytes_unicode() {
assertEquals(2, "á".sizeInUtf8Bytes())
}
}

View File

@ -1,30 +0,0 @@
package cash.z.ecc.sdk.model
import cash.z.ecc.sdk.fixture.ZecSendFixture
import java.lang.IllegalArgumentException
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class MemoTest {
companion object {
private const val BYTE_STRING_513 = """
asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfa
"""
}
@Test
fun isWithinMaxSize_too_big() {
assertFalse(Memo.isWithinMaxLength(BYTE_STRING_513))
}
@Test
fun isWithinMaxSize_ok() {
assertTrue(Memo.isWithinMaxLength(ZecSendFixture.MEMO.value))
}
@Test(IllegalArgumentException::class)
fun init_max_size() {
Memo(BYTE_STRING_513)
}
}

View File

@ -1,19 +0,0 @@
package cash.z.ecc.sdk.model
import androidx.test.filters.SmallTest
import org.junit.Test
class PercentDecimalTest {
@Test(expected = IllegalArgumentException::class)
@SmallTest
fun require_greater_than_zero() {
PercentDecimal(-1.0f)
}
@Test(expected = IllegalArgumentException::class)
@SmallTest
fun require_less_than_one() {
PercentDecimal(1.5f)
}
}

View File

@ -1,51 +0,0 @@
package cash.z.ecc.sdk.model
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.sdk.fixture.PersistableWalletFixture
import cash.z.ecc.sdk.fixture.SeedPhraseFixture
import cash.z.ecc.sdk.test.count
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class PersistableWalletTest {
@Test
@SmallTest
fun serialize() {
val persistableWallet = PersistableWalletFixture.new()
val jsonObject = persistableWallet.toJson()
assertEquals(4, jsonObject.keys().count())
assertTrue(jsonObject.has(PersistableWallet.KEY_VERSION))
assertTrue(jsonObject.has(PersistableWallet.KEY_NETWORK_ID))
assertTrue(jsonObject.has(PersistableWallet.KEY_SEED_PHRASE))
assertTrue(jsonObject.has(PersistableWallet.KEY_BIRTHDAY))
assertEquals(1, jsonObject.getInt(PersistableWallet.KEY_VERSION))
assertEquals(ZcashNetwork.Testnet.id, jsonObject.getInt(PersistableWallet.KEY_NETWORK_ID))
assertEquals(PersistableWalletFixture.SEED_PHRASE.joinToString(), jsonObject.getString(PersistableWallet.KEY_SEED_PHRASE))
// Birthday serialization is tested in a separate file
}
@Test
@SmallTest
fun round_trip() {
val persistableWallet = PersistableWalletFixture.new()
val deserialized = PersistableWallet.from(persistableWallet.toJson())
assertEquals(persistableWallet, deserialized)
assertFalse(persistableWallet === deserialized)
}
@Test
@SmallTest
fun toString_security() {
val actual = PersistableWalletFixture.new().toString()
assertFalse(actual.contains(SeedPhraseFixture.SEED_PHRASE))
}
}

View File

@ -1,26 +0,0 @@
package cash.z.ecc.sdk.model
import androidx.test.filters.SmallTest
import cash.z.ecc.sdk.fixture.SeedPhraseFixture
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Test
class SeedPhraseTest {
@Test
@SmallTest
fun split_and_join() {
val seedPhrase = SeedPhrase.new(SeedPhraseFixture.SEED_PHRASE)
assertEquals(SeedPhraseFixture.SEED_PHRASE, seedPhrase.joinToString())
}
@Test
@SmallTest
fun security() {
val seedPhrase = SeedPhraseFixture.new()
seedPhrase.split.forEach {
assertFalse(seedPhrase.toString().contains(it))
}
}
}

View File

@ -1,18 +0,0 @@
package cash.z.ecc.sdk.model
import cash.z.ecc.sdk.fixture.WalletAddressFixture
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
class WalletAddressTest {
@Test
@ExperimentalCoroutinesApi
fun unified_equals_different_instance() = runTest {
val one = WalletAddressFixture.unified()
val two = WalletAddressFixture.unified()
assertEquals(one, two)
}
}

View File

@ -1,21 +0,0 @@
package cash.z.ecc.sdk.model
import androidx.test.filters.SmallTest
import cash.z.ecc.sdk.fixture.WalletAddressesFixture
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertFalse
import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class WalletAddressesTest {
@Test
@SmallTest
fun security() = runTest {
val walletAddresses = WalletAddressesFixture.new()
val actual = WalletAddressesFixture.new().toString()
assertFalse(actual.contains(walletAddresses.sapling.address))
assertFalse(actual.contains(walletAddresses.transparent.address))
assertFalse(actual.contains(walletAddresses.unified.address))
}
}

View File

@ -1,6 +1,7 @@
package cash.z.ecc.sdk.model
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.model.WalletAddress
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.sdk.fixture.Zip321UriBuildFixture
import cash.z.ecc.sdk.fixture.Zip321UriParseFixture

View File

@ -1,10 +0,0 @@
@file:Suppress("ktlint:filename")
package cash.z.ecc.sdk.test
fun <T> Iterator<T>.count(): Int {
var count = 0
forEach { count++ }
return count
}

View File

@ -4,7 +4,7 @@ package cash.z.ecc.sdk
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.sdk.model.ZecSend
import cash.z.ecc.android.sdk.model.ZecSend
suspend fun Synchronizer.send(spendingKey: UnifiedSpendingKey, send: ZecSend) = sendToAddress(
spendingKey,

View File

@ -1,9 +0,0 @@
@file:Suppress("ktlint:filename")
package cash.z.ecc.sdk.ext
import java.nio.charset.Charset
private val UTF_8 = Charset.forName("UTF-8")
fun String.sizeInUtf8Bytes() = toByteArray(UTF_8).size

View File

@ -1,18 +0,0 @@
package cash.z.ecc.sdk.fixture
import cash.z.ecc.sdk.model.CurrencyConversion
import cash.z.ecc.sdk.model.FiatCurrency
import kotlinx.datetime.Instant
import kotlinx.datetime.toInstant
object CurrencyConversionFixture {
val FIAT_CURRENCY = FiatCurrencyFixture.new()
val TIMESTAMP = "2022-07-08T11:51:44Z".toInstant()
const val PRICE_OF_ZEC = 54.98
fun new(
fiatCurrency: FiatCurrency = FIAT_CURRENCY,
timestamp: Instant = TIMESTAMP,
priceOfZec: Double = PRICE_OF_ZEC
) = CurrencyConversion(fiatCurrency, timestamp, priceOfZec)
}

View File

@ -1,9 +0,0 @@
package cash.z.ecc.sdk.fixture
import cash.z.ecc.sdk.model.FiatCurrency
object FiatCurrencyFixture {
const val USD = "USD"
fun new(code: String = USD) = FiatCurrency(code)
}

View File

@ -1,6 +1,6 @@
package cash.z.ecc.sdk.fixture
import cash.z.ecc.sdk.model.Memo
import cash.z.ecc.android.sdk.model.Memo
object MemoFixture {
const val MEMO_STRING = "Thanks for lunch"

View File

@ -1,9 +1,9 @@
package cash.z.ecc.sdk.fixture
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.PersistableWallet
import cash.z.ecc.android.sdk.model.SeedPhrase
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.sdk.model.PersistableWallet
import cash.z.ecc.sdk.model.SeedPhrase
object PersistableWalletFixture {

View File

@ -1,6 +1,6 @@
package cash.z.ecc.sdk.fixture
import cash.z.ecc.sdk.model.SeedPhrase
import cash.z.ecc.android.sdk.model.SeedPhrase
object SeedPhraseFixture {
@Suppress("MaxLineLength")

View File

@ -1,18 +0,0 @@
package cash.z.ecc.sdk.fixture
import cash.z.ecc.sdk.model.WalletAddress
object WalletAddressFixture {
// These fixture values are derived from the secret defined in PersistableWalletFixture
@Suppress("MaxLineLength")
const val UNIFIED_ADDRESS_STRING = "utest1qwerty6tj6989fcf0r57aavfnypj0krtxnmz93ty7ujr7d0spdxnz97kfdcugq9ndglvyg7v89m78dparu33q0putwaf2kvnsypsh8juzmcdvnedjlrrwhel9ldr203p7wc922luxup"
@Suppress("MaxLineLength")
const val SAPLING_ADDRESS_STRING = "zs1hf72k87gev2qnvg9228vn2xt97adfelju2hm2ap4xwrxkau5dz56mvkeseer3u8283wmy7skt4u"
const val TRANSPARENT_ADDRESS_STRING = "t1QZMTZaU1EwXppCLL5dR6U9y2M4ph3CSPK"
suspend fun unified() = WalletAddress.Unified.new(UNIFIED_ADDRESS_STRING)
suspend fun sapling() = WalletAddress.Sapling.new(SAPLING_ADDRESS_STRING)
suspend fun transparent() = WalletAddress.Transparent.new(TRANSPARENT_ADDRESS_STRING)
}

View File

@ -1,17 +0,0 @@
package cash.z.ecc.sdk.fixture
import cash.z.ecc.sdk.model.WalletAddress
import cash.z.ecc.sdk.model.WalletAddresses
object WalletAddressesFixture {
suspend fun new(
unified: String = WalletAddressFixture.UNIFIED_ADDRESS_STRING,
sapling: String = WalletAddressFixture.SAPLING_ADDRESS_STRING,
transparent: String = WalletAddressFixture.TRANSPARENT_ADDRESS_STRING
) = WalletAddresses(
WalletAddress.Unified.new(unified),
WalletAddress.Sapling.new(sapling),
WalletAddress.Transparent.new(transparent)
)
}

View File

@ -1,7 +1,8 @@
package cash.z.ecc.sdk.fixture
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
import cash.z.ecc.android.sdk.model.WalletAddress
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.sdk.model.WalletAddress
import cash.z.ecc.sdk.model.ZecRequest
import cash.z.ecc.sdk.model.ZecRequestMessage

View File

@ -1,9 +1,10 @@
package cash.z.ecc.sdk.fixture
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
import cash.z.ecc.android.sdk.model.Memo
import cash.z.ecc.android.sdk.model.WalletAddress
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.sdk.model.Memo
import cash.z.ecc.sdk.model.WalletAddress
import cash.z.ecc.sdk.model.ZecSend
import cash.z.ecc.android.sdk.model.ZecSend
object ZecSendFixture {
const val ADDRESS: String = WalletAddressFixture.UNIFIED_ADDRESS_STRING

View File

@ -1,7 +1,8 @@
package cash.z.ecc.sdk.fixture
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
import cash.z.ecc.android.sdk.model.WalletAddress
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.sdk.model.WalletAddress
import cash.z.ecc.sdk.model.ZecRequest
import cash.z.ecc.sdk.model.ZecRequestMessage
import kotlinx.coroutines.runBlocking

View File

@ -1,7 +1,8 @@
package cash.z.ecc.sdk.fixture
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
import cash.z.ecc.android.sdk.model.WalletAddress
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.sdk.model.WalletAddress
import cash.z.ecc.sdk.model.ZecRequest
import cash.z.ecc.sdk.model.ZecRequestMessage

View File

@ -1,6 +1,7 @@
package cash.z.ecc.sdk.model
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.model.PercentDecimal
fun CompactBlockProcessor.ProcessorInfo.downloadProgress(): PercentDecimal {
val lastDownloadRangeSnapshot = lastDownloadRange

View File

@ -1,35 +0,0 @@
package cash.z.ecc.sdk.model
import kotlinx.datetime.Instant
/**
* Represents a snapshot in time of a currency conversion rate.
*
* @param fiatCurrency The fiat currency for this conversion.
* @param timestamp The timestamp when this conversion was obtained. This value is returned by
* the server so it shouldn't have issues with client-side clock inaccuracy.
* @param priceOfZec The conversion rate of ZEC to the fiat currency.
*/
data class CurrencyConversion(
val fiatCurrency: FiatCurrency,
val timestamp: Instant,
val priceOfZec: Double
) {
init {
require(priceOfZec > 0) { "priceOfZec must be greater than 0" }
require(priceOfZec.isFinite()) { "priceOfZec must be finite" }
}
}
/**
* Represents an ISO 4217 currency code.
*/
@Suppress("MagicNumber")
data class FiatCurrency(val code: String) {
init {
require(code.length == 3) { "Fiat currency code must be 3 characters long." }
// TODO [#532] https://github.com/zcash/secant-android-wallet/issues/532
// Add another check to make sure the code is in the known ISO currency code list.
}
}

View File

@ -1,24 +0,0 @@
package cash.z.ecc.sdk.model
import cash.z.ecc.sdk.ext.sizeInUtf8Bytes
@JvmInline
value class Memo(val value: String) {
init {
require(isWithinMaxLength(value)) {
"Memo length in bytes must be less than $MAX_MEMO_LENGTH_BYTES but " +
"actually has length ${value.sizeInUtf8Bytes()}"
}
}
companion object {
/**
* The decoded memo contents MUST NOT exceed 512 bytes.
*
* https://zips.z.cash/zip-0321
*/
private const val MAX_MEMO_LENGTH_BYTES = 512
fun isWithinMaxLength(memoString: String) = memoString.sizeInUtf8Bytes() <= MAX_MEMO_LENGTH_BYTES
}
}

View File

@ -1,25 +0,0 @@
package cash.z.ecc.sdk.model
/**
* @param decimal A percent represented as a `Double` decimal value in the range of [0, 1].
*/
@JvmInline
value class PercentDecimal(val decimal: Float) {
init {
require(decimal >= MIN)
require(decimal <= MAX)
}
companion object {
const val MIN = 0.0f
const val MAX = 1.0f
val ZERO_PERCENT = PercentDecimal(MIN)
val ONE_HUNDRED_PERCENT = PercentDecimal(MAX)
fun newLenient(decimal: Float) = PercentDecimal(
decimal
.coerceAtLeast(MIN)
.coerceAtMost(MAX)
)
}
}

View File

@ -1,89 +0,0 @@
package cash.z.ecc.sdk.model
import android.app.Application
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toEntropy
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.sdk.type.fromResources
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
/**
* Represents everything needed to save and restore a wallet.
*/
data class PersistableWallet(
val network: ZcashNetwork,
val birthday: BlockHeight?,
val seedPhrase: SeedPhrase
) {
/**
* @return Wallet serialized to JSON format, suitable for long-term encrypted storage.
*/
// Note: We're using a hand-crafted serializer so that we're less likely to have accidental
// breakage from reflection or annotation based methods, and so that we can carefully manage versioning.
fun toJson() = JSONObject().apply {
put(KEY_VERSION, VERSION_1)
put(KEY_NETWORK_ID, network.id)
birthday?.let {
put(KEY_BIRTHDAY, it.value)
}
put(KEY_SEED_PHRASE, seedPhrase.joinToString())
}
// For security, intentionally override the toString method to reduce risk of accidentally logging secrets
override fun toString() = "PersistableWallet"
companion object {
private const val VERSION_1 = 1
internal const val KEY_VERSION = "v"
internal const val KEY_NETWORK_ID = "network_ID"
internal const val KEY_BIRTHDAY = "birthday"
internal const val KEY_SEED_PHRASE = "seed_phrase"
fun from(jsonObject: JSONObject): PersistableWallet {
when (val version = jsonObject.getInt(KEY_VERSION)) {
VERSION_1 -> {
val network = run {
val networkId = jsonObject.getInt(KEY_NETWORK_ID)
ZcashNetwork.from(networkId)
}
val birthday = if (jsonObject.has(KEY_BIRTHDAY)) {
val birthdayBlockHeightLong = jsonObject.getLong(KEY_BIRTHDAY)
BlockHeight.new(network, birthdayBlockHeightLong)
} else {
null
}
val seedPhrase = jsonObject.getString(KEY_SEED_PHRASE)
return PersistableWallet(network, birthday, SeedPhrase.new(seedPhrase))
}
else -> {
throw IllegalArgumentException("Unsupported version $version")
}
}
}
/**
* @return A new PersistableWallet with a random seed phrase.
*/
suspend fun new(application: Application): PersistableWallet {
val zcashNetwork = ZcashNetwork.fromResources(application)
val birthday = BlockHeight.ofLatestCheckpoint(application, zcashNetwork)
val seedPhrase = newSeedPhrase()
return PersistableWallet(zcashNetwork, birthday, seedPhrase)
}
}
}
// Using IO context because of https://github.com/zcash/kotlin-bip39/issues/13
private suspend fun newMnemonic() = withContext(Dispatchers.IO) {
Mnemonics.MnemonicCode(cash.z.ecc.android.bip39.Mnemonics.WordCount.COUNT_24.toEntropy()).words
}
private suspend fun newSeedPhrase() = SeedPhrase(newMnemonic().map { it.concatToString() })

View File

@ -1,30 +0,0 @@
package cash.z.ecc.sdk.model
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
// Consider using ImmutableList here
data class SeedPhrase(val split: List<String>) {
init {
require(SEED_PHRASE_SIZE == split.size) {
"Seed phrase must split into $SEED_PHRASE_SIZE words but was ${split.size}"
}
}
// For security, intentionally override the toString method to reduce risk of accidentally logging secrets
override fun toString() = "SeedPhrase"
fun joinToString() = split.joinToString(DEFAULT_DELIMITER)
suspend fun toByteArray() = withContext(Dispatchers.IO) { Mnemonics.MnemonicCode(joinToString()).toSeed() }
companion object {
const val SEED_PHRASE_SIZE = 24
const val DEFAULT_DELIMITER = " "
fun new(phrase: String) = SeedPhrase(phrase.split(DEFAULT_DELIMITER))
}
}

View File

@ -1,6 +1,7 @@
package cash.z.ecc.sdk.model
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.sdk.model.SeedPhrase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.Locale

View File

@ -1,46 +0,0 @@
package cash.z.ecc.sdk.model
sealed class WalletAddress(val address: String) {
class Unified private constructor(address: String) : WalletAddress(address) {
companion object {
suspend fun new(address: String): WalletAddress.Unified {
// https://github.com/zcash/zcash-android-wallet-sdk/issues/342
// TODO [#342]: refactor SDK to enable direct calls for address verification
return WalletAddress.Unified(address)
}
}
}
class Sapling private constructor(address: String) : WalletAddress(address) {
companion object {
suspend fun new(address: String): Sapling {
// TODO [#342]: https://github.com/zcash/zcash-android-wallet-sdk/issues/342
// TODO [#342]: refactor SDK to enable direct calls for address verification
return Sapling(address)
}
}
}
class Transparent private constructor(address: String) : WalletAddress(address) {
companion object {
suspend fun new(address: String): Transparent {
// TODO [#342]: https://github.com/zcash/zcash-android-wallet-sdk/issues/342
// TODO [#342]: refactor SDK to enable direct calls for address verification
return Transparent(address)
}
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as WalletAddress
if (address != other.address) return false
return true
}
override fun hashCode() = address.hashCode()
}

View File

@ -1,33 +0,0 @@
package cash.z.ecc.sdk.model
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.Account
data class WalletAddresses(
val unified: WalletAddress.Unified,
val sapling: WalletAddress.Sapling,
val transparent: WalletAddress.Transparent
) {
// Override to prevent leaking details in logs
override fun toString() = "WalletAddresses"
companion object {
suspend fun new(synchronizer: Synchronizer): WalletAddresses {
val unifiedAddress = WalletAddress.Unified.new(
synchronizer.getUnifiedAddress(Account.DEFAULT)
)
val saplingAddress = WalletAddress.Sapling.new(
synchronizer.getSaplingAddress(Account.DEFAULT)
)
val transparentAddress = WalletAddress.Transparent.new(
synchronizer.getTransparentAddress(Account.DEFAULT)
)
return WalletAddresses(
unified = unifiedAddress,
sapling = saplingAddress,
transparent = transparentAddress
)
}
}
}

View File

@ -1,5 +1,6 @@
package cash.z.ecc.sdk.model
import cash.z.ecc.android.sdk.model.WalletAddress
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.sdk.fixture.Zip321UriBuildFixture
import cash.z.ecc.sdk.fixture.Zip321UriParseFixture

View File

@ -1,7 +0,0 @@
package cash.z.ecc.sdk.model
import cash.z.ecc.android.sdk.model.Zatoshi
data class ZecSend(val destination: WalletAddress, val amount: Zatoshi, val memo: Memo) {
companion object
}

View File

@ -1,38 +0,0 @@
plugins {
id("com.android.library")
kotlin("android")
id("secant.android-build-conventions")
id("wtf.emulator.gradle")
id("secant.emulator-wtf-conventions")
id("secant.jacoco-conventions")
}
android {
namespace = "cash.z.ecc.sdk.ext.ui"
testNamespace = "cash.z.ecc.sdk.ext.ui.test"
resourcePrefix = "co_electriccoin_zcash_"
}
dependencies {
implementation(projects.sdkExtLib)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
androidTestImplementation(libs.bundles.androidx.test)
androidTestImplementation(libs.kotlin.test)
androidTestUtil(libs.androidx.test.services) {
artifact {
type = "apk"
}
}
if (project.property("IS_USE_TEST_ORCHESTRATOR").toString().toBoolean()) {
androidTestUtil(libs.androidx.test.orchestrator) {
artifact {
type = "apk"
}
}
}
}

View File

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
>
<!-- For test coverage -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<!-- For test coverage on API 29 only -->
<application
android:label="sdk-ext-ui-test"
android:requestLegacyExternalStorage="true" />
</manifest>

View File

@ -1,109 +0,0 @@
package cash.z.ecc.sdk.ext.ui.model
import androidx.test.filters.SmallTest
import cash.z.ecc.sdk.ext.ui.fixture.LocaleFixture
import cash.z.ecc.sdk.ext.ui.fixture.MonetarySeparatorsFixture
import cash.z.ecc.sdk.ext.ui.toFiatCurrencyState
import cash.z.ecc.sdk.fixture.CurrencyConversionFixture
import cash.z.ecc.sdk.fixture.ZatoshiFixture
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import org.junit.Test
import kotlin.test.assertIs
import kotlin.time.Duration.Companion.seconds
class FiatCurrencyConversionRateStateTest {
@Test
@SmallTest
fun future_near() {
val zatoshi = ZatoshiFixture.new()
val frozenClock = FrozenClock(
CurrencyConversionFixture.TIMESTAMP - FiatCurrencyConversionRateState.FUTURE_CUTOFF_AGE_INCLUSIVE
)
val currencyConversion = CurrencyConversionFixture.new()
val result = zatoshi.toFiatCurrencyState(currencyConversion, LocaleFixture.new(), MonetarySeparatorsFixture.new(), frozenClock)
assertIs<FiatCurrencyConversionRateState.Current>(result)
}
@Test
@SmallTest
fun future_far() {
val zatoshi = ZatoshiFixture.new()
val frozenClock = FrozenClock(
CurrencyConversionFixture.TIMESTAMP - FiatCurrencyConversionRateState.FUTURE_CUTOFF_AGE_INCLUSIVE - 1.seconds
)
val currencyConversion = CurrencyConversionFixture.new()
val result = zatoshi.toFiatCurrencyState(currencyConversion, LocaleFixture.new(), MonetarySeparatorsFixture.new(), frozenClock)
assertIs<FiatCurrencyConversionRateState.Unavailable>(result)
}
@Test
@SmallTest
fun current() {
val zatoshi = ZatoshiFixture.new()
val frozenClock = FrozenClock(CurrencyConversionFixture.TIMESTAMP)
val currencyConversion = CurrencyConversionFixture.new(
timestamp = CurrencyConversionFixture.TIMESTAMP - 1.seconds
)
val result = zatoshi.toFiatCurrencyState(currencyConversion, LocaleFixture.new(), MonetarySeparatorsFixture.new(), frozenClock)
assertIs<FiatCurrencyConversionRateState.Current>(result)
}
@Test
@SmallTest
fun stale() {
val zatoshi = ZatoshiFixture.new()
val frozenClock = FrozenClock(CurrencyConversionFixture.TIMESTAMP)
val currencyConversion = CurrencyConversionFixture.new(
timestamp = CurrencyConversionFixture.TIMESTAMP - FiatCurrencyConversionRateState.CURRENT_CUTOFF_AGE_INCLUSIVE - 1.seconds
)
val result = zatoshi.toFiatCurrencyState(currencyConversion, LocaleFixture.new(), MonetarySeparatorsFixture.new(), frozenClock)
assertIs<FiatCurrencyConversionRateState.Stale>(result)
}
@Test
@SmallTest
fun too_stale() {
val zatoshi = ZatoshiFixture.new()
val frozenClock = FrozenClock(CurrencyConversionFixture.TIMESTAMP)
val currencyConversion = CurrencyConversionFixture.new(
timestamp = CurrencyConversionFixture.TIMESTAMP - FiatCurrencyConversionRateState.STALE_CUTOFF_AGE_INCLUSIVE - 1.seconds
)
val result = zatoshi.toFiatCurrencyState(currencyConversion, LocaleFixture.new(), MonetarySeparatorsFixture.new(), frozenClock)
assertIs<FiatCurrencyConversionRateState.Unavailable>(result)
}
@Test
@SmallTest
fun null_conversion_rate() {
val zatoshi = ZatoshiFixture.new()
val result = zatoshi.toFiatCurrencyState(null, LocaleFixture.new(), MonetarySeparatorsFixture.new())
assertIs<FiatCurrencyConversionRateState.Unavailable>(result)
}
}
private class FrozenClock(private val timestamp: Instant) : Clock {
override fun now() = timestamp
}

View File

@ -1,26 +0,0 @@
package cash.z.ecc.sdk.ext.ui.model
import androidx.test.filters.SmallTest
import org.junit.Test
import kotlin.test.assertEquals
class LocaleTest {
@Test
@SmallTest
fun toKotlinLocale() {
val javaLocale = java.util.Locale.forLanguageTag("en-US")
val kotlinLocale = javaLocale.toKotlinLocale()
assertEquals("en", kotlinLocale.language)
assertEquals("US", kotlinLocale.region)
assertEquals(null, kotlinLocale.variant)
}
@Test
@SmallTest
fun toJavaLocale() {
val kotlinLocale = cash.z.ecc.sdk.ext.ui.model.Locale("en", "US", null)
val javaLocale = kotlinLocale.toJavaLocale()
assertEquals("en-US", javaLocale.toLanguageTag())
}
}

View File

@ -1,77 +0,0 @@
package cash.z.ecc.sdk.ext.ui.model
import androidx.test.filters.SmallTest
import cash.z.ecc.sdk.ext.ui.fixture.LocaleFixture
import cash.z.ecc.sdk.ext.ui.fixture.MonetarySeparatorsFixture
import cash.z.ecc.sdk.ext.ui.toFiatString
import cash.z.ecc.sdk.fixture.CurrencyConversionFixture
import cash.z.ecc.sdk.fixture.ZatoshiFixture
import org.junit.Test
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class ZatoshiExtTest {
companion object {
private val EN_US_SEPARATORS = MonetarySeparatorsFixture.new()
private val CURRENCY_CONVERSION = CurrencyConversionFixture.new()
}
@Test
@SmallTest
fun zero_zatoshi_to_fiat_conversion_test() {
val zatoshi = ZatoshiFixture.new(0L)
val fiatString = zatoshi.toFiatString(CURRENCY_CONVERSION, LocaleFixture.new(), EN_US_SEPARATORS)
fiatString.also {
assertNotNull(it)
assertTrue(it.isNotEmpty())
assertTrue(it.contains("0"))
assertTrue(it.isValidNumber(EN_US_SEPARATORS))
}
}
@Test
@SmallTest
fun regular_zatoshi_to_fiat_conversion_test() {
val zatoshi = ZatoshiFixture.new(123_456_789L)
val fiatString = zatoshi.toFiatString(CURRENCY_CONVERSION, LocaleFixture.new(), EN_US_SEPARATORS)
fiatString.also {
assertNotNull(it)
assertTrue(it.isNotEmpty())
assertTrue(it.isValidNumber(EN_US_SEPARATORS))
}
}
@Test
@SmallTest
fun rounded_zatoshi_to_fiat_conversion_test() {
val roundedZatoshi = ZatoshiFixture.new(100_000_000L)
val roundedCurrencyConversion = CurrencyConversionFixture.new(
priceOfZec = 100.0
)
val fiatString = roundedZatoshi.toFiatString(
roundedCurrencyConversion,
LocaleFixture.new(),
EN_US_SEPARATORS
)
fiatString.also {
assertNotNull(it)
assertTrue(it.isNotEmpty())
assertTrue(it.isValidNumber(EN_US_SEPARATORS))
assertTrue("$100${EN_US_SEPARATORS.decimal}00" == it)
}
}
}
private fun Char.isDigitOrSeparator(separators: MonetarySeparators): Boolean {
return this.isDigit() || this == separators.decimal || this == separators.grouping
}
private fun String.isValidNumber(separators: MonetarySeparators): Boolean {
return this
.drop(1) // remove currency symbol
.all { return it.isDigitOrSeparator(separators) }
}

View File

@ -1,100 +0,0 @@
package cash.z.ecc.sdk.ext.ui.model
import android.content.Context
import android.content.res.Configuration
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.sdk.ext.ui.fixture.MonetarySeparatorsFixture
import org.junit.Assert.assertEquals
import org.junit.Ignore
import org.junit.Test
import java.util.Locale
import kotlin.test.assertNotNull
import kotlin.test.assertNull
class ZecStringTest {
companion object {
private val EN_US_MONETARY_SEPARATORS = MonetarySeparatorsFixture.new()
private val context = run {
val applicationContext = ApplicationProvider.getApplicationContext<Context>()
val enUsConfiguration = Configuration(applicationContext.resources.configuration).apply {
setLocale(Locale.US)
}
applicationContext.createConfigurationContext(enUsConfiguration)
}
}
@Test
fun empty_string() {
val actual = Zatoshi.fromZecString(context, "", EN_US_MONETARY_SEPARATORS)
val expected = null
assertEquals(expected, actual)
}
@Test
fun decimal_monetary_separator() {
val actual = Zatoshi.fromZecString(context, "1.13", EN_US_MONETARY_SEPARATORS)
val expected = Zatoshi(113000000L)
assertEquals(expected, actual)
}
@Test
fun comma_grouping_separator() {
val actual = Zatoshi.fromZecString(context, "1,130", EN_US_MONETARY_SEPARATORS)
val expected = Zatoshi(113000000000L)
assertEquals(expected, actual)
}
@Test
fun decimal_monetary_and() {
val actual = Zatoshi.fromZecString(context, "1,130", EN_US_MONETARY_SEPARATORS)
val expected = Zatoshi(113000000000L)
assertEquals(expected, actual)
}
@Test
@Ignore("https://github.com/zcash/zcash-android-wallet-sdk/issues/412")
fun toZecString() {
val expected = "1.13000000"
val actual = Zatoshi(113000000).toZecString()
assertEquals(expected, actual)
}
@Test
@Ignore("https://github.com/zcash/zcash-android-wallet-sdk/issues/412")
fun round_trip() {
val expected = Zatoshi(113000000L)
val actual = Zatoshi.fromZecString(context, expected.toZecString(), EN_US_MONETARY_SEPARATORS)
assertEquals(expected, actual)
}
@Test
fun parse_bad_string() {
assertNull(Zatoshi.fromZecString(context, "", EN_US_MONETARY_SEPARATORS))
assertNull(Zatoshi.fromZecString(context, "+@#$~^&*=", EN_US_MONETARY_SEPARATORS))
assertNull(Zatoshi.fromZecString(context, "asdf", EN_US_MONETARY_SEPARATORS))
}
@Test
fun parse_invalid_numbers() {
assertNull(Zatoshi.fromZecString(context, "", EN_US_MONETARY_SEPARATORS))
assertNull(Zatoshi.fromZecString(context, "1,2", EN_US_MONETARY_SEPARATORS))
assertNull(Zatoshi.fromZecString(context, "1,23,", EN_US_MONETARY_SEPARATORS))
assertNull(Zatoshi.fromZecString(context, "1,234,", EN_US_MONETARY_SEPARATORS))
}
@Test
@SmallTest
fun overflow_number_test() {
assertNotNull(Zatoshi.fromZecString(context, "21,000,000", EN_US_MONETARY_SEPARATORS))
assertNull(Zatoshi.fromZecString(context, "21,000,001", EN_US_MONETARY_SEPARATORS))
}
}

View File

@ -1,169 +0,0 @@
package cash.z.ecc.sdk.ext.ui.regex
import androidx.test.filters.SmallTest
import cash.z.ecc.sdk.ext.ui.R
import cash.z.ecc.sdk.ext.ui.ZecStringExt
import cash.z.ecc.sdk.ext.ui.fixture.MonetarySeparatorsFixture
import cash.z.ecc.sdk.ext.ui.test.getStringResourceWithArgs
import org.junit.Test
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class ZecStringExtTest {
companion object {
private val EN_US_SEPARATORS = MonetarySeparatorsFixture.new()
}
private fun getContinuousRegex(): Regex {
return getStringResourceWithArgs(
R.string.co_electriccoin_zcash_zec_amount_regex_continuous_filter,
arrayOf(
EN_US_SEPARATORS.grouping,
EN_US_SEPARATORS.decimal
)
).toRegex()
}
private fun getConfirmRegex(): Regex {
return getStringResourceWithArgs(
R.string.co_electriccoin_zcash_zec_amount_regex_confirm_filter,
arrayOf(
EN_US_SEPARATORS.grouping,
EN_US_SEPARATORS.decimal
)
).toRegex()
}
@Test
@SmallTest
fun check_continuous_regex_validity() {
val regexString = getStringResourceWithArgs(
R.string.co_electriccoin_zcash_zec_amount_regex_continuous_filter,
arrayOf(
EN_US_SEPARATORS.grouping,
EN_US_SEPARATORS.decimal
)
)
assertNotNull(regexString)
val regexAmountChecker = regexString.toRegex()
regexAmountChecker.also {
assertNotNull(regexAmountChecker)
assertTrue(regexAmountChecker.pattern.isNotEmpty())
}
}
@Test
@SmallTest
fun check_confirm_regex_validity() {
val regexString = getStringResourceWithArgs(
R.string.co_electriccoin_zcash_zec_amount_regex_confirm_filter,
arrayOf(
EN_US_SEPARATORS.grouping,
EN_US_SEPARATORS.decimal
)
)
assertNotNull(regexString)
val regexAmountChecker = regexString.toRegex()
regexAmountChecker.also {
assertNotNull(regexAmountChecker)
assertTrue(regexAmountChecker.pattern.isNotEmpty())
}
}
@Test
@SmallTest
fun check_continuous_regex_functionality_valid_inputs() {
getContinuousRegex().also {
assertTrue(it.matches(""))
assertTrue(it.matches("123"))
assertTrue(it.matches("${EN_US_SEPARATORS.decimal}"))
assertTrue(it.matches("${EN_US_SEPARATORS.decimal}123"))
assertTrue(it.matches("123${EN_US_SEPARATORS.grouping}"))
assertTrue(it.matches("123${EN_US_SEPARATORS.grouping}456"))
assertTrue(it.matches("123${EN_US_SEPARATORS.decimal}"))
assertTrue(it.matches("123${EN_US_SEPARATORS.decimal}456"))
assertTrue(it.matches("123${EN_US_SEPARATORS.grouping}456${EN_US_SEPARATORS.decimal}"))
assertTrue(it.matches("123${EN_US_SEPARATORS.grouping}456${EN_US_SEPARATORS.decimal}789"))
assertTrue(it.matches("1${EN_US_SEPARATORS.grouping}234${EN_US_SEPARATORS.grouping}567${EN_US_SEPARATORS.decimal}00"))
}
}
@Test
@SmallTest
fun check_continuous_regex_functionality_invalid_inputs() {
getContinuousRegex().also {
assertFalse(it.matches("aaa"))
assertFalse(it.matches("123aaa"))
assertFalse(it.matches("${EN_US_SEPARATORS.grouping}"))
assertFalse(it.matches("${EN_US_SEPARATORS.grouping}123"))
assertFalse(it.matches("123${EN_US_SEPARATORS.grouping}${EN_US_SEPARATORS.grouping}"))
assertFalse(it.matches("123${EN_US_SEPARATORS.decimal}${EN_US_SEPARATORS.decimal}"))
assertFalse(it.matches("1${EN_US_SEPARATORS.grouping}2${EN_US_SEPARATORS.grouping}3"))
assertFalse(it.matches("1${EN_US_SEPARATORS.decimal}2${EN_US_SEPARATORS.decimal}3"))
assertFalse(it.matches("1${EN_US_SEPARATORS.decimal}2${EN_US_SEPARATORS.grouping}3"))
}
</