parent
7523619afa
commit
f5e0af6545
|
@ -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>
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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]];
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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() })
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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) }
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun check_confirm_regex_functionality_valid_inputs() {
|
||||
getConfirmRegex().also {
|
||||
assertTrue(it.matches("123"))
|
||||
assertTrue(it.matches(".123"))
|
||||
assertTrue(it.matches("1,234"))
|
||||
assertTrue(it.matches("1,234,567,890"))
|
||||
assertTrue(it.matches("1.2"))
|
||||
assertTrue(it.matches("123.4"))
|
||||
assertTrue(it.matches("1.234"))
|
||||
assertTrue(it.matches("1,123."))
|
||||
assertTrue(it.matches("1,234.567"))
|
||||
assertTrue(it.matches("1,234,567.890"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun check_confirm_regex_functionality_invalid_inputs() {
|
||||
getContinuousRegex().also {
|
||||
assertFalse(it.matches("+@#$~^&*="))
|
||||
assertFalse(it.matches("asdf"))
|
||||
assertFalse(it.matches(".."))
|
||||
assertFalse(it.matches(","))
|
||||
assertFalse(it.matches(",,"))
|
||||
assertFalse(it.matches(",."))
|
||||
assertFalse(it.matches(".,"))
|
||||
assertFalse(it.matches(",123"))
|
||||
assertFalse(it.matches("1,2,3"))
|
||||
assertFalse(it.matches("1.2,3,4"))
|
||||
assertFalse(it.matches("123,,456"))
|
||||
assertFalse(it.matches("123..456"))
|
||||
assertFalse(it.matches("1.234,567"))
|
||||
assertFalse(it.matches("1.234,567,890"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun check_digits_between_grouping_separators_valid_test() {
|
||||
assertTrue(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "123"))
|
||||
assertTrue(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}234"))
|
||||
assertTrue(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}234${EN_US_SEPARATORS.grouping}"))
|
||||
assertTrue(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}234${EN_US_SEPARATORS.grouping}5"))
|
||||
assertTrue(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}234${EN_US_SEPARATORS.grouping}567${EN_US_SEPARATORS.grouping}8"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun check_digits_between_grouping_separators_invalid_test() {
|
||||
assertFalse(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}1${EN_US_SEPARATORS.grouping}2"))
|
||||
assertFalse(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}12${EN_US_SEPARATORS.grouping}3"))
|
||||
assertFalse(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}1234${EN_US_SEPARATORS.grouping}"))
|
||||
assertFalse(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}123${EN_US_SEPARATORS.grouping}4${EN_US_SEPARATORS.grouping}"))
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package cash.z.ecc.sdk.ext.ui.test
|
||||
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import java.util.Locale
|
||||
|
||||
fun getStringResource(@StringRes resId: Int) = ApplicationProvider.getApplicationContext<Context>().getString(resId)
|
||||
|
||||
fun getStringResourceWithArgs(@StringRes resId: Int, formatArgs: Array<Any>) = ApplicationProvider.getApplicationContext<Context>().getString(resId, *formatArgs)
|
||||
|
||||
fun isLocaleRTL(locale: Locale) = TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest>
|
||||
|
||||
<application />
|
||||
|
||||
</manifest>
|
|
@ -1,90 +0,0 @@
|
|||
@file:Suppress("ktlint:filename")
|
||||
|
||||
package cash.z.ecc.sdk.ext.ui
|
||||
|
||||
import cash.z.ecc.android.sdk.ext.Conversions
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.sdk.ext.ui.model.FiatCurrencyConversionRateState
|
||||
import cash.z.ecc.sdk.ext.ui.model.Locale
|
||||
import cash.z.ecc.sdk.ext.ui.model.MonetarySeparators
|
||||
import cash.z.ecc.sdk.ext.ui.model.toJavaLocale
|
||||
import cash.z.ecc.sdk.model.CurrencyConversion
|
||||
import kotlinx.datetime.Clock
|
||||
import java.math.BigDecimal
|
||||
import java.math.MathContext
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import java.text.NumberFormat
|
||||
import java.util.Currency
|
||||
import kotlin.time.Duration
|
||||
|
||||
fun Zatoshi.toFiatCurrencyState(
|
||||
currencyConversion: CurrencyConversion?,
|
||||
locale: Locale,
|
||||
monetarySeparators: MonetarySeparators,
|
||||
clock: Clock = Clock.System
|
||||
): FiatCurrencyConversionRateState {
|
||||
if (currencyConversion == null) {
|
||||
return FiatCurrencyConversionRateState.Unavailable
|
||||
}
|
||||
|
||||
val fiatCurrencyConversionRate = toFiatString(currencyConversion, locale, monetarySeparators)
|
||||
|
||||
val currentSystemTime = clock.now()
|
||||
|
||||
val age = currentSystemTime - currencyConversion.timestamp
|
||||
|
||||
return if (age < Duration.ZERO && age.absoluteValue > FiatCurrencyConversionRateState.FUTURE_CUTOFF_AGE_INCLUSIVE) {
|
||||
// Special case if the device's clock is set to the future.
|
||||
// TODO [#535]: Consider using NTP requests to get the correct time instead of relying on the device's clock.
|
||||
FiatCurrencyConversionRateState.Unavailable
|
||||
} else if (age <= FiatCurrencyConversionRateState.CURRENT_CUTOFF_AGE_INCLUSIVE) {
|
||||
FiatCurrencyConversionRateState.Current(fiatCurrencyConversionRate)
|
||||
} else if (age <= FiatCurrencyConversionRateState.STALE_CUTOFF_AGE_INCLUSIVE) {
|
||||
FiatCurrencyConversionRateState.Stale(fiatCurrencyConversionRate)
|
||||
} else {
|
||||
FiatCurrencyConversionRateState.Unavailable
|
||||
}
|
||||
}
|
||||
|
||||
fun Zatoshi.toFiatString(
|
||||
currencyConversion: CurrencyConversion,
|
||||
locale: Locale,
|
||||
monetarySeparators: MonetarySeparators
|
||||
) =
|
||||
convertZatoshiToZecDecimal()
|
||||
.convertZecDecimalToFiatDecimal(BigDecimal(currencyConversion.priceOfZec))
|
||||
.convertFiatDecimalToFiatString(
|
||||
Currency.getInstance(currencyConversion.fiatCurrency.code),
|
||||
locale.toJavaLocale(),
|
||||
monetarySeparators
|
||||
)
|
||||
|
||||
private fun Zatoshi.convertZatoshiToZecDecimal(): BigDecimal {
|
||||
return BigDecimal(value, MathContext.DECIMAL128).divide(
|
||||
Conversions.ONE_ZEC_IN_ZATOSHI,
|
||||
MathContext.DECIMAL128
|
||||
).setScale(Conversions.ZEC_FORMATTER.maximumFractionDigits, RoundingMode.HALF_EVEN)
|
||||
}
|
||||
|
||||
private fun BigDecimal.convertZecDecimalToFiatDecimal(zecPrice: BigDecimal): BigDecimal {
|
||||
return multiply(zecPrice, MathContext.DECIMAL128)
|
||||
}
|
||||
|
||||
private fun BigDecimal.convertFiatDecimalToFiatString(
|
||||
fiatCurrency: Currency,
|
||||
locale: java.util.Locale,
|
||||
monetarySeparators: MonetarySeparators
|
||||
): String {
|
||||
return NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = fiatCurrency
|
||||
roundingMode = RoundingMode.HALF_EVEN
|
||||
if (this is DecimalFormat) {
|
||||
decimalFormatSymbols.apply {
|
||||
decimalSeparator = monetarySeparators.decimal
|
||||
monetaryDecimalSeparator = monetarySeparators.decimal
|
||||
groupingSeparator = monetarySeparators.grouping
|
||||
}
|
||||
}
|
||||
}.format(this)
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
package cash.z.ecc.sdk.ext.ui
|
||||
|
||||
import android.content.Context
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.sdk.ext.ui.model.MonetarySeparators
|
||||
import cash.z.ecc.sdk.ext.ui.model.fromZecString
|
||||
import cash.z.ecc.sdk.model.Memo
|
||||
import cash.z.ecc.sdk.model.WalletAddress
|
||||
import cash.z.ecc.sdk.model.ZecSend
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
object ZecSendExt {
|
||||
|
||||
fun new(
|
||||
context: Context,
|
||||
destinationString: String,
|
||||
zecString: String,
|
||||
memoString: String,
|
||||
monetarySeparators: MonetarySeparators
|
||||
): ZecSendValidation {
|
||||
// This runBlocking shouldn't have a performance impact, since everything needs to be loaded at this point.
|
||||
// However it would be better to eliminate it entirely.
|
||||
val destination = runBlocking { WalletAddress.Unified.new(destinationString) }
|
||||
val amount = Zatoshi.fromZecString(context, zecString, monetarySeparators)
|
||||
val memo = Memo(memoString)
|
||||
|
||||
val validationErrors = buildSet {
|
||||
if (null == amount) {
|
||||
add(ZecSendValidation.Invalid.ValidationError.INVALID_AMOUNT)
|
||||
}
|
||||
|
||||
// TODO [#250]: Implement all validation
|
||||
// TODO [#342]: https://github.com/zcash/zcash-android-wallet-sdk/issues/342
|
||||
}
|
||||
|
||||
return if (validationErrors.isEmpty()) {
|
||||
ZecSendValidation.Valid(ZecSend(destination, amount!!, memo))
|
||||
} else {
|
||||
ZecSendValidation.Invalid(validationErrors)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ZecSendValidation {
|
||||
data class Valid(val zecSend: ZecSend) : ZecSendValidation()
|
||||
data class Invalid(val validationErrors: Set<ValidationError>) : ZecSendValidation() {
|
||||
enum class ValidationError {
|
||||
INVALID_ADDRESS,
|
||||
INVALID_AMOUNT,
|
||||
INVALID_MEMO
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
package cash.z.ecc.sdk.ext.ui
|
||||
|
||||
import android.content.Context
|
||||
import cash.z.ecc.sdk.ext.ui.model.MonetarySeparators
|
||||
|
||||
object ZecStringExt {
|
||||
|
||||
private const val DIGITS_BETWEEN_GROUP_SEPARATORS = 3
|
||||
|
||||
/**
|
||||
* Builds filter with current local monetary separators for continuous input checking. The
|
||||
* solution is built upon regex validation and other common string validation checks.
|
||||
*
|
||||
* Regex example: ^([0-9]*([0-9]+([,]$|[,][0-9]+))*([.]$|[.][0-9]+)?)?$
|
||||
* Inputs may differ according to user locale.
|
||||
*
|
||||
* Valid amounts: "" . | .123 | 123, | 123. | 123,456 | 123.456 | 123,456.789 | 123,456,789 | 123,456,789.123 | etc.
|
||||
* Invalid amounts: 123,, | 123,. | 123.. | ,123 | 123.456.789 | etc.
|
||||
*
|
||||
* @param context used for loading localized pattern from strings.xml
|
||||
* @param separators which consist of localized monetary separators
|
||||
* @param zecString to be validated
|
||||
*
|
||||
* @return true in case of validation success, false otherwise
|
||||
*/
|
||||
fun filterContinuous(context: Context, separators: MonetarySeparators, zecString: String): Boolean {
|
||||
if (!context.getString(
|
||||
R.string.co_electriccoin_zcash_zec_amount_regex_continuous_filter,
|
||||
separators.grouping,
|
||||
separators.decimal
|
||||
).toRegex().matches(zecString) || !checkFor3Digits(separators, zecString)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for at least 3 digits between grouping separators.
|
||||
*
|
||||
* @param separators which consist of localized monetary separators
|
||||
* @param zecString to be validated
|
||||
*
|
||||
* @return true in case of validation success, false otherwise
|
||||
*/
|
||||
fun checkFor3Digits(separators: MonetarySeparators, zecString: String): Boolean {
|
||||
if (zecString.count { it == separators.grouping } >= 2) {
|
||||
val groups = zecString.split(separators.grouping)
|
||||
for (i in 1 until (groups.size - 1)) {
|
||||
if (groups[i].length != DIGITS_BETWEEN_GROUP_SEPARATORS) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds filter with current local monetary separators for validation of entered ZEC amount
|
||||
* after confirm button is pressed. The solution is built upon regex validation and other common
|
||||
* string validation checks.
|
||||
*
|
||||
* Regex example: ^([0-9]{1,3}(?:[,]?[0-9]{3})*)*(?:[0-9]*[.][0-9]*)?$
|
||||
* Inputs may differ according to user locale.
|
||||
*
|
||||
* Valid amounts: 123 | .123 | 123. | 123, | 123.456 | 123,456 | 123,456.789 | 123,456,789 | 123,456,789.123 | etc.
|
||||
* Invalid amounts: "" | , | . | 123,, | 123,. | 123.. | ,123 | 123.456.789 | etc.
|
||||
*
|
||||
* @param context used for loading localized pattern from strings.xml
|
||||
* @param separators which consist of localized monetary separators
|
||||
* @param zecString to be validated
|
||||
*
|
||||
* @return true in case of validation success, false otherwise
|
||||
*/
|
||||
fun filterConfirm(context: Context, separators: MonetarySeparators, zecString: String): Boolean {
|
||||
if (zecString.isBlank() ||
|
||||
zecString == separators.grouping.toString() ||
|
||||
zecString == separators.decimal.toString()
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
context.getString(
|
||||
R.string.co_electriccoin_zcash_zec_amount_regex_confirm_filter,
|
||||
separators.grouping,
|
||||
separators.decimal
|
||||
).toRegex().matches(zecString) && checkFor3Digits(separators, zecString)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package cash.z.ecc.sdk.ext.ui.fixture
|
||||
|
||||
import cash.z.ecc.sdk.ext.ui.model.Locale
|
||||
|
||||
object LocaleFixture {
|
||||
const val LANGUAGE = "en"
|
||||
const val COUNTRY = "US"
|
||||
val VARIANT: String? = null
|
||||
|
||||
fun new(
|
||||
language: String = LANGUAGE,
|
||||
country: String? = COUNTRY,
|
||||
variant: String? = VARIANT
|
||||
) = Locale(language, country, variant)
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package cash.z.ecc.sdk.ext.ui.fixture
|
||||
|
||||
import cash.z.ecc.sdk.ext.ui.model.MonetarySeparators
|
||||
|
||||
object MonetarySeparatorsFixture {
|
||||
const val US_GROUPING_SEPARATOR = ','
|
||||
const val US_DECIMAL_SEPARATOR = '.'
|
||||
|
||||
fun new(
|
||||
grouping: Char = US_GROUPING_SEPARATOR,
|
||||
decimal: Char = US_DECIMAL_SEPARATOR
|
||||
) = MonetarySeparators(grouping, decimal)
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
package cash.z.ecc.sdk.ext.ui.model
|
||||
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
/**
|
||||
* Represents a state of current fiat currency conversion to ZECs.
|
||||
*/
|
||||
sealed class FiatCurrencyConversionRateState {
|
||||
/**
|
||||
* @param formattedFiatValue A fiat value formatted as a localized string. E.g. $1.00.
|
||||
*/
|
||||
data class Current(val formattedFiatValue: String) : FiatCurrencyConversionRateState()
|
||||
|
||||
/**
|
||||
* @param formattedFiatValue A fiat value formatted as a localized string. E.g. $1.00.
|
||||
*/
|
||||
data class Stale(val formattedFiatValue: String) : FiatCurrencyConversionRateState()
|
||||
object Unavailable : FiatCurrencyConversionRateState()
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Cutoff negative age. Some users may intentionally set their clock forward 10 minutes
|
||||
* because they're always late to things. This allows the app to mostly work for those users,
|
||||
* while still failing if the clock is way off.
|
||||
*/
|
||||
val FUTURE_CUTOFF_AGE_INCLUSIVE = 10.minutes
|
||||
|
||||
/**
|
||||
* Cutoff age for next attempt to refresh the conversion rate from the API.
|
||||
*/
|
||||
val CURRENT_CUTOFF_AGE_INCLUSIVE = 1.minutes
|
||||
|
||||
/**
|
||||
* Cutoff age for displaying conversion rate from prior app launches or background refresh.
|
||||
*/
|
||||
val STALE_CUTOFF_AGE_INCLUSIVE = 1.days
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package cash.z.ecc.sdk.ext.ui.model
|
||||
|
||||
data class Locale(val language: String, val region: String?, val variant: String?) {
|
||||
companion object
|
||||
}
|
||||
|
||||
fun Locale.toJavaLocale(): java.util.Locale {
|
||||
return if (!region.isNullOrEmpty() && !variant.isNullOrEmpty()) {
|
||||
java.util.Locale(language, region, variant)
|
||||
} else if (!region.isNullOrEmpty() && variant.isNullOrEmpty()) {
|
||||
java.util.Locale(language, region)
|
||||
} else {
|
||||
java.util.Locale(language)
|
||||
}
|
||||
}
|
||||
|
||||
fun java.util.Locale.toKotlinLocale(): Locale {
|
||||
val resultCountry = if (country.isNullOrEmpty()) {
|
||||
null
|
||||
} else {
|
||||
country
|
||||
}
|
||||
|
||||
val resultVariant = if (variant.isNullOrEmpty()) {
|
||||
null
|
||||
} else {
|
||||
variant
|
||||
}
|
||||
|
||||
return Locale(language, resultCountry, resultVariant)
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
package cash.z.ecc.sdk.ext.ui.model
|
||||
|
||||
import android.content.Context
|
||||
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
|
||||
import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.sdk.ext.ui.ZecStringExt
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.text.ParseException
|
||||
import java.util.Locale
|
||||
|
||||
object ZecString {
|
||||
|
||||
fun allowedCharacters(monetarySeparators: MonetarySeparators) = buildSet<Char> {
|
||||
add('0')
|
||||
add('1')
|
||||
add('2')
|
||||
add('3')
|
||||
add('4')
|
||||
add('5')
|
||||
add('6')
|
||||
add('7')
|
||||
add('8')
|
||||
add('9')
|
||||
add(monetarySeparators.decimal)
|
||||
add(monetarySeparators.grouping)
|
||||
}
|
||||
}
|
||||
|
||||
data class MonetarySeparators(val grouping: Char, val decimal: Char) {
|
||||
init {
|
||||
require(grouping != decimal) { "Grouping and decimal separator cannot be the same character" }
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* @return The current localized monetary separators. Do not cache this value, as it
|
||||
* can change if the system Locale changes.
|
||||
*/
|
||||
fun current(): MonetarySeparators {
|
||||
val decimalFormatSymbols = DecimalFormatSymbols.getInstance()
|
||||
|
||||
return MonetarySeparators(
|
||||
decimalFormatSymbols.groupingSeparator,
|
||||
decimalFormatSymbols.monetaryDecimalSeparator
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val DECIMALS = 8
|
||||
|
||||
// TODO [#412]: https://github.com/zcash/zcash-android-wallet-sdk/issues/412
|
||||
// The SDK needs to fix the API for currency conversion
|
||||
fun Zatoshi.toZecString() = convertZatoshiToZecString(DECIMALS, DECIMALS)
|
||||
|
||||
/*
|
||||
* ZEC is our own currency, so there's not going to be an existing localization that matches it perfectly.
|
||||
*
|
||||
* To ensure consistent behavior regardless of user Locale, use US localization except that we swap out the
|
||||
* separator characters based on the user's current Locale. This should avoid unexpected surprises
|
||||
* while also localizing the separator format.
|
||||
*/
|
||||
/**
|
||||
* @return [zecString] parsed into Zatoshi or null if parsing failed.
|
||||
*/
|
||||
@SuppressWarnings("ReturnCount")
|
||||
fun Zatoshi.Companion.fromZecString(
|
||||
context: Context,
|
||||
zecString: String,
|
||||
monetarySeparators: MonetarySeparators
|
||||
): Zatoshi? {
|
||||
if (!ZecStringExt.filterConfirm(context, monetarySeparators, zecString)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val symbols = DecimalFormatSymbols.getInstance(Locale.US).apply {
|
||||
this.groupingSeparator = monetarySeparators.grouping
|
||||
this.decimalSeparator = monetarySeparators.decimal
|
||||
}
|
||||
val localizedPattern = "#${monetarySeparators.grouping}##0${monetarySeparators.decimal}0#"
|
||||
|
||||
// TODO [#321]: https://github.com/zcash/secant-android-wallet/issues/321
|
||||
val decimalFormat = DecimalFormat(localizedPattern, symbols).apply {
|
||||
isParseBigDecimal = true
|
||||
roundingMode = RoundingMode.HALF_EVEN // aka Bankers rounding
|
||||
}
|
||||
|
||||
// TODO [#343]: https://github.com/zcash/secant-android-wallet/issues/343
|
||||
val bigDecimal = try {
|
||||
decimalFormat.parse(zecString) as BigDecimal
|
||||
} catch (e: NumberFormatException) {
|
||||
null
|
||||
} catch (e: ParseException) {
|
||||
null
|
||||
}
|
||||
|
||||
@Suppress("SwallowedException")
|
||||
return try {
|
||||
bigDecimal.convertZecToZatoshi()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="co_electriccoin_zcash_zec_amount_regex_continuous_filter" formatted="true" tools:ignore="TypographyDashes">^([0-9]*([0-9]+([<xliff:g id="group" example=",">%1$s</xliff:g>]$|[<xliff:g id="group" example=",">%1$s</xliff:g>][0-9]+))*([<xliff:g id="dec" example=".">%2$s</xliff:g>]$|[<xliff:g id="dec" example=".">%2$s</xliff:g>][0-9]+)?)?$</string>
|
||||
<string name="co_electriccoin_zcash_zec_amount_regex_confirm_filter" formatted="true" tools:ignore="TypographyDashes">^([0-9]{1,3}(?:[<xliff:g id="group" example=",">%1$s</xliff:g>]?[0-9]{3})*)*(?:[0-9]*[<xliff:g id="group" example=".">%2$s</xliff:g>][0-9]*)?$</string>
|
||||
</resources>
|
|
@ -228,6 +228,7 @@ dependencyResolutionManagement {
|
|||
library("play-update", "com.google.android.play:app-update:$playAppUpdateVersion")
|
||||
library("play-update-ktx", "com.google.android.play:app-update-ktx:$playAppUpdateKtxVersion")
|
||||
library("zcash-sdk", "cash.z.ecc.android:zcash-android-sdk:$zcashSdkVersion")
|
||||
library("zcash-sdk-incubator", "cash.z.ecc.android:zcash-android-sdk-incubator:$zcashSdkVersion")
|
||||
library("zcash-bip39", "cash.z.ecc.android:kotlin-bip39:$zcashBip39Version")
|
||||
library("zcash-walletplgns", "cash.z.ecc.android:zcash-android-wallet-plugins:$zcashBip39Version")
|
||||
library("zxing", "com.google.zxing:core:$zxingVersion")
|
||||
|
@ -311,7 +312,6 @@ include("crash-android-lib")
|
|||
include("preference-api-lib")
|
||||
include("preference-impl-android-lib")
|
||||
include("sdk-ext-lib")
|
||||
include("sdk-ext-ui-lib")
|
||||
include("spackle-lib")
|
||||
include("spackle-android-lib")
|
||||
include("test-lib")
|
||||
|
|
|
@ -68,6 +68,7 @@ dependencies {
|
|||
implementation(libs.kotlinx.coroutines.guava)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
implementation(libs.zcash.sdk)
|
||||
implementation(libs.zcash.sdk.incubator)
|
||||
implementation(libs.zcash.bip39)
|
||||
implementation(libs.zxing)
|
||||
|
||||
|
@ -76,7 +77,6 @@ dependencies {
|
|||
implementation(projects.preferenceApiLib)
|
||||
implementation(projects.preferenceImplAndroidLib)
|
||||
implementation(projects.sdkExtLib)
|
||||
implementation(projects.sdkExtUiLib)
|
||||
implementation(projects.spackleAndroidLib)
|
||||
api(projects.uiDesignLib)
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@ import androidx.compose.ui.test.onNodeWithContentDescription
|
|||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.sdk.fixture.WalletAddressesFixture
|
||||
import cash.z.ecc.sdk.model.WalletAddresses
|
||||
import cash.z.ecc.android.sdk.fixture.WalletAddressesFixture
|
||||
import cash.z.ecc.android.sdk.model.WalletAddresses
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
|
|
|
@ -59,7 +59,6 @@ class HomeTestSetup(
|
|||
onRequestCount.incrementAndGet()
|
||||
},
|
||||
resetSdk = {},
|
||||
wipeEntireWallet = {},
|
||||
isDebugMenuEnabled = false,
|
||||
updateAvailable = false
|
||||
)
|
||||
|
|
|
@ -9,7 +9,7 @@ import androidx.compose.ui.test.onNodeWithTag
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.sdk.model.PercentDecimal
|
||||
import cash.z.ecc.android.sdk.model.PercentDecimal
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.screen.home.HomeTag
|
||||
|
|
|
@ -2,9 +2,9 @@ package co.electriccoin.zcash.ui.screen.home.model
|
|||
|
||||
import androidx.test.filters.SmallTest
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.sdk.ext.ui.model.FiatCurrencyConversionRateState
|
||||
import cash.z.ecc.sdk.ext.ui.model.toZecString
|
||||
import cash.z.ecc.sdk.model.PercentDecimal
|
||||
import cash.z.ecc.android.sdk.model.FiatCurrencyConversionRateState
|
||||
import cash.z.ecc.android.sdk.model.PercentDecimal
|
||||
import cash.z.ecc.android.sdk.model.toZecString
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.test.getAppContext
|
||||
|
|
|
@ -4,8 +4,8 @@ import androidx.compose.runtime.CompositionLocalProvider
|
|||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.sdk.model.WalletAddress
|
||||
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.android.sdk.model.WalletAddress
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.common.LocalScreenBrightness
|
||||
import co.electriccoin.zcash.ui.common.ScreenBrightness
|
||||
|
|
|
@ -4,8 +4,8 @@ import androidx.compose.runtime.CompositionLocalProvider
|
|||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.sdk.model.WalletAddress
|
||||
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.android.sdk.model.WalletAddress
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.common.LocalScreenTimeout
|
||||
import co.electriccoin.zcash.ui.common.ScreenTimeout
|
||||
|
|
|
@ -7,8 +7,8 @@ import androidx.compose.ui.test.onNodeWithText
|
|||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.sdk.model.WalletAddress
|
||||
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.android.sdk.model.WalletAddress
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.screen.profile.util.ProfileConfiguration
|
||||
|
|
|
@ -9,9 +9,9 @@ import androidx.compose.ui.test.performClick
|
|||
import androidx.compose.ui.test.performTextClearance
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.sdk.ext.ui.model.MonetarySeparators
|
||||
import cash.z.ecc.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.sdk.fixture.ZecRequestFixture
|
||||
import cash.z.ecc.sdk.model.ZecRequest
|
||||
import cash.z.ecc.sdk.model.ZecRequestMessage
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package co.electriccoin.zcash.ui.screen.restore.model
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import cash.z.ecc.sdk.model.SeedPhrase
|
||||
import cash.z.ecc.android.sdk.model.SeedPhrase
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
|
|
|
@ -16,8 +16,8 @@ import androidx.compose.ui.test.performClick
|
|||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.sdk.model.SeedPhrase
|
||||
import cash.z.ecc.sdk.fixture.SeedPhraseFixture
|
||||
import cash.z.ecc.sdk.model.SeedPhrase
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.design.component.CommonTag
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package co.electriccoin.zcash.ui.screen.send.ext
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import cash.z.ecc.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
|
||||
import co.electriccoin.zcash.ui.test.getAppContext
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
|
@ -2,8 +2,8 @@ package co.electriccoin.zcash.ui.screen.send.ext
|
|||
|
||||
import androidx.compose.runtime.saveable.SaverScope
|
||||
import androidx.test.filters.SmallTest
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import cash.z.ecc.sdk.fixture.ZecSendFixture
|
||||
import cash.z.ecc.sdk.model.ZecSend
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
|
|
|
@ -14,14 +14,14 @@ import androidx.compose.ui.test.performTextClearance
|
|||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.android.sdk.ext.collectWith
|
||||
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.android.sdk.model.Memo
|
||||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.sdk.ext.ui.model.MonetarySeparators
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import cash.z.ecc.sdk.fixture.MemoFixture
|
||||
import cash.z.ecc.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.sdk.fixture.ZatoshiFixture
|
||||
import cash.z.ecc.sdk.fixture.ZecRequestFixture
|
||||
import cash.z.ecc.sdk.model.Memo
|
||||
import cash.z.ecc.sdk.model.ZecSend
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
|
|
|
@ -13,7 +13,6 @@ import co.electriccoin.zcash.ui.test.getStringResource
|
|||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
@ -79,27 +78,11 @@ class SettingsViewTest : UiTestPrerequisites() {
|
|||
assertEquals(1, testSetup.getAnalyticsToggleCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
@Ignore("Wipe has been disabled in Settings and is now a debug-only option")
|
||||
fun wipe() = runTest {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
|
||||
assertEquals(0, testSetup.getBackupCount())
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.settings_wipe)).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
assertEquals(1, testSetup.getWipeCount())
|
||||
}
|
||||
|
||||
private class TestSetup(private val composeTestRule: ComposeContentTestRule) {
|
||||
|
||||
private val onBackCount = AtomicInteger(0)
|
||||
private val onBackupCount = AtomicInteger(0)
|
||||
private val onRescanCount = AtomicInteger(0)
|
||||
private val onWipeCount = AtomicInteger(0)
|
||||
private val onAnalyticsChangedCount = AtomicInteger(0)
|
||||
|
||||
fun getOnBackCount(): Int {
|
||||
|
@ -117,11 +100,6 @@ class SettingsViewTest : UiTestPrerequisites() {
|
|||
return onRescanCount.get()
|
||||
}
|
||||
|
||||
fun getWipeCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onWipeCount.get()
|
||||
}
|
||||
|
||||
fun getAnalyticsToggleCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onAnalyticsChangedCount.get()
|
||||
|
@ -141,9 +119,6 @@ class SettingsViewTest : UiTestPrerequisites() {
|
|||
onRescanWallet = {
|
||||
onRescanCount.incrementAndGet()
|
||||
},
|
||||
onWipeWallet = {
|
||||
onWipeCount.incrementAndGet()
|
||||
},
|
||||
onAnalyticsSettingsChanged = {
|
||||
onAnalyticsChangedCount.incrementAndGet()
|
||||
}
|
||||
|
|
|
@ -1,220 +1,26 @@
|
|||
package co.electriccoin.zcash.global
|
||||
|
||||
import android.content.Context
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.ext.onFirst
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||
import cash.z.ecc.sdk.model.PersistableWallet
|
||||
import cash.z.ecc.sdk.type.fromResources
|
||||
import cash.z.ecc.android.sdk.WalletCoordinator
|
||||
import co.electriccoin.zcash.spackle.LazyWithArgument
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.preference.EncryptedPreferenceKeys
|
||||
import co.electriccoin.zcash.ui.preference.EncryptedPreferenceSingleton
|
||||
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
|
||||
import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.flatMapConcat
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.UUID
|
||||
|
||||
class WalletCoordinator(context: Context) {
|
||||
companion object {
|
||||
private val lazy = LazyWithArgument<Context, WalletCoordinator> { WalletCoordinator(it) }
|
||||
|
||||
fun getInstance(context: Context) = lazy.getInstance(context)
|
||||
}
|
||||
|
||||
private val applicationContext = context.applicationContext
|
||||
|
||||
/*
|
||||
* We want a global scope that is independent of the lifecycles of either
|
||||
* WorkManager or the UI.
|
||||
*/
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private val walletScope = CoroutineScope(GlobalScope.coroutineContext + Dispatchers.Main)
|
||||
|
||||
private val synchronizerMutex = Mutex()
|
||||
|
||||
private val lazy = LazyWithArgument<Context, WalletCoordinator> {
|
||||
/**
|
||||
* A flow of the user's stored wallet. Null indicates that no wallet has been stored.
|
||||
*/
|
||||
val persistableWallet = flow {
|
||||
// EncryptedPreferenceSingleton.getInstance() is a suspending function, which is why we need
|
||||
// the flow builder to provide a coroutine context.
|
||||
val encryptedPreferenceProvider = EncryptedPreferenceSingleton.getInstance(applicationContext)
|
||||
val encryptedPreferenceProvider = EncryptedPreferenceSingleton.getInstance(it)
|
||||
|
||||
emitAll(EncryptedPreferenceKeys.PERSISTABLE_WALLET.observe(encryptedPreferenceProvider))
|
||||
}
|
||||
|
||||
private val lockoutMutex = Mutex()
|
||||
private val synchronizerLockoutId = MutableStateFlow<UUID?>(null)
|
||||
|
||||
private sealed class InternalSynchronizerStatus {
|
||||
object NoWallet : InternalSynchronizerStatus()
|
||||
class Available(val synchronizer: Synchronizer) : InternalSynchronizerStatus()
|
||||
class Lockout(val id: UUID) : InternalSynchronizerStatus()
|
||||
}
|
||||
|
||||
private val synchronizerOrLockoutId: Flow<Flow<InternalSynchronizerStatus>> = persistableWallet
|
||||
.combine(synchronizerLockoutId) { persistableWallet: PersistableWallet?, lockoutId: UUID? ->
|
||||
if (null != lockoutId) { // this one needs to come first
|
||||
flowOf(InternalSynchronizerStatus.Lockout(lockoutId))
|
||||
} else if (null == persistableWallet) {
|
||||
flowOf(InternalSynchronizerStatus.NoWallet)
|
||||
} else {
|
||||
callbackFlow<InternalSynchronizerStatus.Available> {
|
||||
val closeableSynchronizer = synchronizerMutex.withLock {
|
||||
Synchronizer.new(
|
||||
context = context,
|
||||
zcashNetwork = persistableWallet.network,
|
||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(persistableWallet.network),
|
||||
birthday = persistableWallet.birthday,
|
||||
seed = persistableWallet.seedPhrase.toByteArray()
|
||||
)
|
||||
}
|
||||
|
||||
trySend(InternalSynchronizerStatus.Available(closeableSynchronizer))
|
||||
awaitClose {
|
||||
Twig.info { "Closing flow and synchronizer" }
|
||||
closeableSynchronizer.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronizer for the Zcash SDK. Emits null until a wallet secret is persisted.
|
||||
*
|
||||
* Note that this synchronizer is closed as soon as it stops being collected. For UI use
|
||||
* cases, see [WalletViewModel].
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val synchronizer: StateFlow<Synchronizer?> = synchronizerOrLockoutId
|
||||
.flatMapLatest { it }
|
||||
.map {
|
||||
when (it) {
|
||||
is InternalSynchronizerStatus.Available -> it.synchronizer
|
||||
is InternalSynchronizerStatus.Lockout -> null
|
||||
InternalSynchronizerStatus.NoWallet -> null
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
walletScope,
|
||||
SharingStarted.WhileSubscribed(),
|
||||
null
|
||||
)
|
||||
|
||||
/**
|
||||
* Rescans the blockchain.
|
||||
*
|
||||
* In order for a rescan to occur, the synchronizer must be loaded already
|
||||
* which would happen if the UI is collecting it.
|
||||
*
|
||||
* @return True if the rescan was performed and false if the rescan was not performed.
|
||||
*/
|
||||
suspend fun rescanBlockchain(): Boolean {
|
||||
synchronizerMutex.withLock {
|
||||
synchronizer.value?.let {
|
||||
it.latestBirthdayHeight?.let { height ->
|
||||
it.rewindToNearestHeight(height, true)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets persisted data in the SDK, but preserves the wallet secret. This will cause the
|
||||
* synchronizer to emit a new instance.
|
||||
*/
|
||||
@OptIn(FlowPreview::class)
|
||||
fun resetSdk() {
|
||||
walletScope.launch {
|
||||
lockoutMutex.withLock {
|
||||
val lockoutId = UUID.randomUUID()
|
||||
synchronizerLockoutId.value = lockoutId
|
||||
|
||||
synchronizerOrLockoutId
|
||||
.flatMapConcat { it }
|
||||
.filterIsInstance<InternalSynchronizerStatus.Lockout>()
|
||||
.filter { it.id == lockoutId }
|
||||
.onFirst {
|
||||
synchronizerMutex.withLock {
|
||||
val didDelete = Synchronizer.erase(
|
||||
appContext = applicationContext,
|
||||
network = ZcashNetwork.fromResources(applicationContext)
|
||||
)
|
||||
Twig.info { "SDK erase result: $didDelete" }
|
||||
}
|
||||
}
|
||||
|
||||
synchronizerLockoutId.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipes the wallet. Will cause the app-wide synchronizer to be reset with a new instance.
|
||||
*/
|
||||
@OptIn(FlowPreview::class)
|
||||
fun wipeEntireWallet() {
|
||||
walletScope.launch {
|
||||
lockoutMutex.withLock {
|
||||
val lockoutId = UUID.randomUUID()
|
||||
synchronizerLockoutId.value = lockoutId
|
||||
|
||||
synchronizerOrLockoutId
|
||||
.flatMapConcat { it }
|
||||
.filterIsInstance<InternalSynchronizerStatus.Lockout>()
|
||||
.filter { it.id == lockoutId }
|
||||
.onFirst {
|
||||
// Note that clearing the data here is non-atomic since multiple files must be modified
|
||||
|
||||
EncryptedPreferenceSingleton.getInstance(applicationContext).also { provider ->
|
||||
EncryptedPreferenceKeys.PERSISTABLE_WALLET.putValue(provider, null)
|
||||
}
|
||||
|
||||
StandardPreferenceSingleton.getInstance(applicationContext).also { provider ->
|
||||
StandardPreferenceKeys.IS_USER_BACKUP_COMPLETE.putValue(provider, false)
|
||||
}
|
||||
|
||||
synchronizerMutex.withLock {
|
||||
val didDelete = Synchronizer.erase(
|
||||
appContext = applicationContext,
|
||||
network = ZcashNetwork.fromResources(applicationContext)
|
||||
)
|
||||
Twig.info { "SDK erase result: $didDelete" }
|
||||
}
|
||||
|
||||
synchronizerLockoutId.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
WalletCoordinator(it, persistableWallet)
|
||||
}
|
||||
|
||||
fun WalletCoordinator.Companion.getInstance(context: Context) = lazy.getInstance(context)
|
||||
|
|
|
@ -4,4 +4,4 @@ package co.electriccoin.zcash.ui.common
|
|||
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
|
||||
fun Locale.toKotlinLocale() = cash.z.ecc.sdk.ext.ui.model.Locale(language, region, script)
|
||||
fun Locale.toKotlinLocale() = cash.z.ecc.android.sdk.model.Locale(language, region, script)
|
||||
|
|
|
@ -2,9 +2,9 @@ package co.electriccoin.zcash.ui.fixture
|
|||
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.model.PercentDecimal
|
||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.sdk.model.PercentDecimal
|
||||
import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.screen.home.viewmodel.SynchronizerError
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package co.electriccoin.zcash.ui.preference
|
||||
|
||||
import cash.z.ecc.sdk.model.FiatCurrency
|
||||
import cash.z.ecc.android.sdk.model.FiatCurrency
|
||||
import co.electriccoin.zcash.preference.api.PreferenceProvider
|
||||
import co.electriccoin.zcash.preference.model.entry.Key
|
||||
import co.electriccoin.zcash.preference.model.entry.PreferenceDefault
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package co.electriccoin.zcash.ui.preference
|
||||
|
||||
import cash.z.ecc.sdk.model.PersistableWallet
|
||||
import cash.z.ecc.android.sdk.model.PersistableWallet
|
||||
import co.electriccoin.zcash.preference.api.PreferenceProvider
|
||||
import co.electriccoin.zcash.preference.model.entry.Key
|
||||
import co.electriccoin.zcash.preference.model.entry.PreferenceDefault
|
||||
|
|
|
@ -38,8 +38,8 @@ import androidx.compose.ui.graphics.painter.ColorPainter
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cash.z.ecc.sdk.fixture.WalletAddressesFixture
|
||||
import cash.z.ecc.sdk.model.WalletAddresses
|
||||
import cash.z.ecc.android.sdk.fixture.WalletAddressesFixture
|
||||
import cash.z.ecc.android.sdk.model.WalletAddresses
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
||||
import co.electriccoin.zcash.ui.design.component.Body
|
||||
|
|
|
@ -8,7 +8,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import cash.z.ecc.sdk.model.PersistableWallet
|
||||
import cash.z.ecc.android.sdk.model.PersistableWallet
|
||||
import co.electriccoin.zcash.ui.MainActivity
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.screen.backup.ext.Saver
|
||||
|
|
|
@ -38,8 +38,8 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.tooling.preview.Devices
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import cash.z.ecc.android.sdk.model.PersistableWallet
|
||||
import cash.z.ecc.sdk.fixture.PersistableWalletFixture
|
||||
import cash.z.ecc.sdk.model.PersistableWallet
|
||||
import co.electriccoin.zcash.spackle.model.Index
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.SecureScreen
|
||||
|
|
|
@ -14,7 +14,6 @@ import co.electriccoin.zcash.ui.MainActivity
|
|||
import co.electriccoin.zcash.ui.screen.home.view.Home
|
||||
import co.electriccoin.zcash.ui.screen.home.viewmodel.CheckUpdateViewModel
|
||||
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
|
||||
import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImp
|
||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
|
||||
|
||||
|
@ -81,15 +80,6 @@ internal fun WrapHome(
|
|||
resetSdk = {
|
||||
walletViewModel.resetSdk()
|
||||
},
|
||||
wipeEntireWallet = {
|
||||
// Although this is debug only, it still might be nice to show a warning dialog
|
||||
// before performing this action
|
||||
walletViewModel.wipeEntireWallet()
|
||||
|
||||
val onboardingViewModel by activity.viewModels<OnboardingViewModel>()
|
||||
onboardingViewModel.onboardingState.goToBeginning()
|
||||
onboardingViewModel.isImporting.value = false
|
||||
},
|
||||
updateAvailable = updateAvailable
|
||||
)
|
||||
|
||||
|
|
|
@ -3,11 +3,11 @@ package co.electriccoin.zcash.ui.screen.home.model
|
|||
import android.content.Context
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.sdk.ext.ui.model.FiatCurrencyConversionRateState
|
||||
import cash.z.ecc.sdk.ext.ui.model.MonetarySeparators
|
||||
import cash.z.ecc.sdk.ext.ui.model.toZecString
|
||||
import cash.z.ecc.sdk.ext.ui.toFiatCurrencyState
|
||||
import cash.z.ecc.sdk.model.PercentDecimal
|
||||
import cash.z.ecc.android.sdk.model.FiatCurrencyConversionRateState
|
||||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||
import cash.z.ecc.android.sdk.model.PercentDecimal
|
||||
import cash.z.ecc.android.sdk.model.toFiatCurrencyState
|
||||
import cash.z.ecc.android.sdk.model.toZecString
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.toKotlinLocale
|
||||
import kotlin.math.roundToInt
|
||||
|
|
|
@ -3,9 +3,9 @@ package co.electriccoin.zcash.ui.screen.home.model
|
|||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||
import cash.z.ecc.android.sdk.model.PercentDecimal
|
||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.sdk.model.PercentDecimal
|
||||
import co.electriccoin.zcash.ui.screen.home.viewmodel.SynchronizerError
|
||||
|
||||
// TODO [#292]: Should be moved to SDK-EXT-UI module.
|
||||
|
|
|
@ -42,8 +42,8 @@ import androidx.compose.ui.platform.testTag
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cash.z.ecc.sdk.ext.ui.model.FiatCurrencyConversionRateState
|
||||
import cash.z.ecc.sdk.model.PercentDecimal
|
||||
import cash.z.ecc.android.sdk.model.FiatCurrencyConversionRateState
|
||||
import cash.z.ecc.android.sdk.model.PercentDecimal
|
||||
import co.electriccoin.zcash.crash.android.GlobalCrashReporter
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
||||
|
@ -73,7 +73,6 @@ fun ComposablePreview() {
|
|||
goSend = {},
|
||||
goRequest = {},
|
||||
resetSdk = {},
|
||||
wipeEntireWallet = {},
|
||||
isDebugMenuEnabled = false,
|
||||
updateAvailable = false
|
||||
)
|
||||
|
@ -92,12 +91,11 @@ fun Home(
|
|||
goSend: () -> Unit,
|
||||
goRequest: () -> Unit,
|
||||
resetSdk: () -> Unit,
|
||||
wipeEntireWallet: () -> Unit,
|
||||
isDebugMenuEnabled: Boolean,
|
||||
updateAvailable: Boolean
|
||||
) {
|
||||
Scaffold(topBar = {
|
||||
HomeTopAppBar(isDebugMenuEnabled, resetSdk, wipeEntireWallet)
|
||||
HomeTopAppBar(isDebugMenuEnabled, resetSdk)
|
||||
}) { paddingValues ->
|
||||
HomeMainContent(
|
||||
paddingValues,
|
||||
|
@ -117,20 +115,21 @@ fun Home(
|
|||
private fun HomeTopAppBar(
|
||||
isDebugMenuEnabled: Boolean,
|
||||
resetSdk: () -> Unit,
|
||||
wipeEntireWallet: () -> Unit
|
||||
) {
|
||||
TopAppBar(
|
||||
title = { Text(text = stringResource(id = R.string.app_name)) },
|
||||
actions = {
|
||||
if (isDebugMenuEnabled) {
|
||||
DebugMenu(resetSdk, wipeEntireWallet)
|
||||
DebugMenu(resetSdk)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DebugMenu(resetSdk: () -> Unit, wipeEntireWallet: () -> Unit) {
|
||||
private fun DebugMenu(
|
||||
resetSdk: () -> Unit
|
||||
) {
|
||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||
IconButton(onClick = { expanded = true }) {
|
||||
Icon(Icons.Default.MoreVert, contentDescription = null)
|
||||
|
@ -164,13 +163,6 @@ private fun DebugMenu(resetSdk: () -> Unit, wipeEntireWallet: () -> Unit) {
|
|||
expanded = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Wipe entire wallet") },
|
||||
onClick = {
|
||||
wipeEntireWallet()
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,19 +6,23 @@ import androidx.lifecycle.viewModelScope
|
|||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.bip39.toSeed
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.WalletCoordinator
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.FiatCurrency
|
||||
import cash.z.ecc.android.sdk.model.PendingTransaction
|
||||
import cash.z.ecc.android.sdk.model.PercentDecimal
|
||||
import cash.z.ecc.android.sdk.model.PersistableWallet
|
||||
import cash.z.ecc.android.sdk.model.WalletAddresses
|
||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.isMined
|
||||
import cash.z.ecc.android.sdk.model.isSubmitSuccess
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import cash.z.ecc.sdk.model.FiatCurrency
|
||||
import cash.z.ecc.sdk.model.PercentDecimal
|
||||
import cash.z.ecc.sdk.model.PersistableWallet
|
||||
import cash.z.ecc.sdk.model.WalletAddresses
|
||||
import cash.z.ecc.sdk.type.fromResources
|
||||
import co.electriccoin.zcash.global.getInstance
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT
|
||||
import co.electriccoin.zcash.ui.common.throttle
|
||||
|
@ -59,7 +63,7 @@ import kotlin.time.ExperimentalTime
|
|||
// TODO [#292]: Should be moved to SDK-EXT-UI module.
|
||||
// TODO [#292]: https://github.com/zcash/secant-android-wallet/issues/292
|
||||
class WalletViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val walletCoordinator = co.electriccoin.zcash.global.WalletCoordinator.getInstance(application)
|
||||
private val walletCoordinator = WalletCoordinator.getInstance(application)
|
||||
|
||||
/*
|
||||
* Using the Mutex may be overkill, but it ensures that if multiple calls are accidentally made
|
||||
|
@ -184,7 +188,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
|||
val application = getApplication<Application>()
|
||||
|
||||
viewModelScope.launch {
|
||||
val newWallet = PersistableWallet.new(application)
|
||||
val newWallet = PersistableWallet.new(application, ZcashNetwork.fromResources(application))
|
||||
persistExistingWallet(newWallet)
|
||||
}
|
||||
}
|
||||
|
@ -244,20 +248,6 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
|||
fun resetSdk() {
|
||||
walletCoordinator.resetSdk()
|
||||
}
|
||||
|
||||
/**
|
||||
* This asynchronously wipes the entire wallet state.
|
||||
*
|
||||
* This is destructive, as the seed phrase is deleted along with the SDK state.
|
||||
*
|
||||
* This could be used as part of testing, to quickly reset the app state.
|
||||
*
|
||||
* A more complete reset of app state can be performed in Android Settings, as this will not
|
||||
* clear application state beyond the SDK and wallet secret.
|
||||
*/
|
||||
fun wipeEntireWallet() {
|
||||
walletCoordinator.wipeEntireWallet()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -9,10 +9,10 @@ import androidx.activity.viewModels
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
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.fixture.SeedPhraseFixture
|
||||
import cash.z.ecc.sdk.model.PersistableWallet
|
||||
import cash.z.ecc.sdk.model.SeedPhrase
|
||||
import cash.z.ecc.sdk.type.fromResources
|
||||
import co.electriccoin.zcash.spackle.EmulatorWtfUtil
|
||||
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
|
||||
|
|
|
@ -6,7 +6,7 @@ import androidx.activity.ComponentActivity
|
|||
import androidx.activity.viewModels
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import cash.z.ecc.sdk.model.WalletAddresses
|
||||
import cash.z.ecc.android.sdk.model.WalletAddresses
|
||||
import co.electriccoin.zcash.ui.MainActivity
|
||||
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.screen.profile.view.Profile
|
||||
|
|
|
@ -22,8 +22,8 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cash.z.ecc.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.sdk.model.WalletAddress
|
||||
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.android.sdk.model.WalletAddress
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.BrightenScreen
|
||||
import co.electriccoin.zcash.ui.common.DisableScreenTimeout
|
||||
|
|
|
@ -26,13 +26,13 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||
import cash.z.ecc.android.sdk.model.WalletAddress
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.sdk.ext.ui.ZecStringExt
|
||||
import cash.z.ecc.sdk.ext.ui.model.MonetarySeparators
|
||||
import cash.z.ecc.sdk.ext.ui.model.ZecString
|
||||
import cash.z.ecc.sdk.ext.ui.model.fromZecString
|
||||
import cash.z.ecc.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.sdk.model.WalletAddress
|
||||
import cash.z.ecc.android.sdk.model.ZecString
|
||||
import cash.z.ecc.android.sdk.model.ZecStringExt
|
||||
import cash.z.ecc.android.sdk.model.fromZecString
|
||||
import cash.z.ecc.sdk.model.ZecRequest
|
||||
import cash.z.ecc.sdk.model.ZecRequestMessage
|
||||
import co.electriccoin.zcash.ui.R
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package co.electriccoin.zcash.ui.screen.restore.model
|
||||
|
||||
import cash.z.ecc.sdk.model.SeedPhrase
|
||||
import cash.z.ecc.android.sdk.model.SeedPhrase
|
||||
import co.electriccoin.zcash.ui.common.first
|
||||
import java.util.Locale
|
||||
|
||||
|
|
|
@ -17,8 +17,8 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import cash.z.ecc.android.sdk.model.PersistableWallet
|
||||
import cash.z.ecc.sdk.fixture.PersistableWalletFixture
|
||||
import cash.z.ecc.sdk.model.PersistableWallet
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.SecureScreen
|
||||
import co.electriccoin.zcash.ui.design.component.Body
|
||||
|
|
|
@ -5,7 +5,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import cash.z.ecc.sdk.model.WalletAddress
|
||||
import cash.z.ecc.android.sdk.model.WalletAddress
|
||||
import co.electriccoin.zcash.ui.R
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package co.electriccoin.zcash.ui.screen.send.ext
|
||||
|
||||
import androidx.compose.runtime.saveable.mapSaver
|
||||
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
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
private const val KEY_ADDRESS = "address" // $NON-NLS
|
||||
|
|
|
@ -31,15 +31,15 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cash.z.ecc.android.sdk.model.Memo
|
||||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.sdk.ext.ui.ZecSendExt
|
||||
import cash.z.ecc.sdk.ext.ui.ZecStringExt
|
||||
import cash.z.ecc.sdk.ext.ui.model.MonetarySeparators
|
||||
import cash.z.ecc.sdk.ext.ui.model.ZecString
|
||||
import cash.z.ecc.sdk.ext.ui.model.toZecString
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import cash.z.ecc.android.sdk.model.ZecSendExt
|
||||
import cash.z.ecc.android.sdk.model.ZecString
|
||||
import cash.z.ecc.android.sdk.model.ZecStringExt
|
||||
import cash.z.ecc.android.sdk.model.toZecString
|
||||
import cash.z.ecc.sdk.fixture.ZatoshiFixture
|
||||
import cash.z.ecc.sdk.model.Memo
|
||||
import cash.z.ecc.sdk.model.ZecSend
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
||||
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
||||
|
|
|
@ -8,7 +8,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.electriccoin.zcash.ui.MainActivity
|
||||
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
|
||||
import co.electriccoin.zcash.ui.screen.settings.view.Settings
|
||||
import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel
|
||||
|
||||
|
@ -46,13 +45,6 @@ private fun WrapSettings(
|
|||
onRescanWallet = {
|
||||
walletViewModel.rescanBlockchain()
|
||||
},
|
||||
onWipeWallet = {
|
||||
walletViewModel.wipeEntireWallet()
|
||||
|
||||
val onboardingViewModel by activity.viewModels<OnboardingViewModel>()
|
||||
onboardingViewModel.onboardingState.goToBeginning()
|
||||
onboardingViewModel.isImporting.value = false
|
||||
},
|
||||
onAnalyticsSettingsChanged = {
|
||||
settingsViewModel.setAnalyticsEnabled(it)
|
||||
}
|
||||
|
|
|
@ -42,7 +42,6 @@ fun PreviewSettings() {
|
|||
isAnalyticsEnabled = true,
|
||||
onBack = {},
|
||||
onBackupWallet = {},
|
||||
onWipeWallet = {},
|
||||
onRescanWallet = {},
|
||||
onAnalyticsSettingsChanged = {}
|
||||
)
|
||||
|
@ -57,7 +56,6 @@ fun Settings(
|
|||
isAnalyticsEnabled: Boolean,
|
||||
onBack: () -> Unit,
|
||||
onBackupWallet: () -> Unit,
|
||||
onWipeWallet: () -> Unit,
|
||||
onRescanWallet: () -> Unit,
|
||||
onAnalyticsSettingsChanged: (Boolean) -> Unit
|
||||
) {
|
||||
|
@ -68,7 +66,6 @@ fun Settings(
|
|||
paddingValues,
|
||||
isAnalyticsEnabled,
|
||||
onBackupWallet = onBackupWallet,
|
||||
onWipeWallet = onWipeWallet,
|
||||
onRescanWallet = onRescanWallet,
|
||||
onAnalyticsSettingsChanged = onAnalyticsSettingsChanged
|
||||
)
|
||||
|
@ -99,7 +96,6 @@ private fun SettingsMainContent(
|
|||
paddingValues: PaddingValues,
|
||||
isAnalyticsEnabled: Boolean,
|
||||
onBackupWallet: () -> Unit,
|
||||
@Suppress("UNUSED_PARAMETER") onWipeWallet: () -> Unit,
|
||||
onRescanWallet: () -> Unit,
|
||||
onAnalyticsSettingsChanged: (Boolean) -> Unit
|
||||
) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package co.electriccoin.zcash.ui.screen.support.model
|
||||
|
||||
import android.content.Context
|
||||
import cash.z.ecc.sdk.ext.ui.model.MonetarySeparators
|
||||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||
import co.electriccoin.zcash.global.StorageChecker
|
||||
import java.util.Locale
|
||||
|
||||
|
|
|
@ -9,7 +9,8 @@ import androidx.work.PeriodicWorkRequest
|
|||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkerParameters
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import co.electriccoin.zcash.global.WalletCoordinator
|
||||
import cash.z.ecc.android.sdk.WalletCoordinator
|
||||
import co.electriccoin.zcash.global.getInstance
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
|
|
@ -59,7 +59,6 @@ dependencies {
|
|||
implementation(projects.testLib)
|
||||
implementation(projects.spackleAndroidLib)
|
||||
implementation(projects.sdkExtLib)
|
||||
implementation(projects.sdkExtUiLib)
|
||||
|
||||
implementation(libs.bundles.androidx.test)
|
||||
implementation(libs.bundles.androidx.compose.core)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue