diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 5ec25db22a..abee1f191e 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,37 +1,92 @@ --optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/* --optimizationpasses 5 -allowaccessmodification -# Preserve third-party names for reflection while allowing shrinking and optimization. --keep,allowshrinking,allowoptimization class !one.mixin.android.**,!androidx.**,!com.yalantis.ucrop.**,** { *; } +# Play requires >= 25% obfuscation, optimization, and shrinking (DEX > 10 MB). +# Do not use `-keep class x.** { *; }` without allow* unless the name is a +# runtime identifier (SPI, JNI, or persisted Java serialization). + +# Gson/Room models that still use source field names as JSON/column keys. +-keepclassmembers,allowoptimization class one.mixin.android.api.**, + one.mixin.android.vo.**, + one.mixin.android.websocket.**, + one.mixin.android.db.**, + one.mixin.android.crypto.**, + one.mixin.android.web3.**, + one.mixin.android.tip.**, + one.mixin.android.media.**, + one.mixin.android.webrtc.** { + !transient !static ; +} +-keepclassmembers,allowoptimization,allowobfuscation class one.mixin.android.api.**, + one.mixin.android.vo.**, + one.mixin.android.websocket.**, + one.mixin.android.db.**, + one.mixin.android.crypto.**, + one.mixin.android.web3.**, + one.mixin.android.tip.**, + one.mixin.android.media.**, + one.mixin.android.webrtc.**, + one.mixin.android.ui.web.**, + one.mixin.android.ui.wallet.**, + one.mixin.android.ui.landing.**, + one.mixin.android.ui.transfer.** { + (...); +} -# prevent multi dex caused NoSuchProviderException --keep class org.whispersystems.** { *; } +# JSON keys come from the annotation, so field names may be renamed. +-keepclassmembers,allowobfuscation class * { + @com.google.gson.annotations.SerializedName ; +} -# Keep app types and members reachable while allowing optimization and obfuscation. --keep,allowoptimization,allowobfuscation class one.mixin.android.** { *; } +# R8 full mode can replace reflectively constructed model types with null. +-keep,allowoptimization,allowobfuscation class one.mixin.android.api.** +-keep,allowoptimization,allowobfuscation class one.mixin.android.vo.** +-keep,allowoptimization,allowobfuscation class one.mixin.android.websocket.** +-keep,allowoptimization,allowobfuscation class one.mixin.android.crypto.** +-keep,allowoptimization,allowobfuscation class one.mixin.android.web3.** +-keep,allowoptimization,allowobfuscation class one.mixin.android.tip.** +-keep,allowoptimization,allowobfuscation class one.mixin.android.media.** +-keep,allowoptimization,allowobfuscation class one.mixin.android.webrtc.** -keep class com.google.android.gms.internal.mlkit_entity_extraction.** extends java.util.Random { *; } -# Gson still relies on unannotated app fields in API, websocket, database, and cache models. --keepclassmembers class one.mixin.android.** { - !transient !static ; +# Java serialization stores class names and field names across app updates. +-keep,allowoptimization class one.mixin.android.** implements java.io.Serializable +-keepclassmembers,allowoptimization class one.mixin.android.** implements java.io.Serializable { + static final long serialVersionUID; + !transient ; } -# Persisted jobs and payloads must remain Java-serialization compatible across updates. --keep class one.mixin.android.** implements java.io.Serializable { *; } +# JobQueue persists jobs by class name and serializes non-transient fields. +-keep class one.mixin.android.job.** +-keepclassmembers,allowoptimization class one.mixin.android.job.** { + static final long serialVersionUID; + !transient ; +} +-keepclassmembers,allowoptimization,allowobfuscation class one.mixin.android.job.** { + (...); +} +-keep class com.birbit.android.jobqueue.Job +-keepclassmembers class com.birbit.android.jobqueue.Job { + static final long serialVersionUID; + !transient ; +} +-keep,allowoptimization,allowobfuscation class com.birbit.android.jobqueue.messaging.message.** { + public (); +} # These fragment arguments persist nested type names in saved state. -keepnames class one.mixin.android.ui.wallet.ImportKeyBottomSheetDialogFragment$PopupType$* -keepnames class one.mixin.android.ui.home.reminder.ReminderBottomSheetDialogFragment$PopupType$* --keep class io.jsonwebtoken.** { *; } +# Protocol strings use Enum.name; constant names must stay. +-keepclassmembers enum one.mixin.android.** { + ; +} -# webrtc +# webrtc JNI looks up classes by their original names. -dontwarn org.webrtc.NetworkMonitorAutoDetect -dontwarn android.net.Network -keep class org.webrtc.** { *; } - -keep class org.jni_zero.** { *; } -dontwarn org.jni_zero.** @@ -50,8 +105,6 @@ -dontwarn sun.net.spi.nameservice.** --keep class com.birbit.android.jobqueue.** { *; } - -keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); @@ -63,16 +116,18 @@ -keep,allowobfuscation,allowshrinking class io.reactivex.Observable -keep,allowobfuscation,allowshrinking class io.reactivex.Completable - -# https://r8.googlesource.com/r8/+/refs/heads/master/compatibility-faq.md#r8-full-mode - -keepattributes Signature --keep class * extends com.google.gson.reflect.TypeToken -# web3j --keep class org.web3j.protocol.** { *; } --keep class org.web3j.abi.** { *; } --keep class org.web3j.crypto.** { *; } +# R8 full mode drops TypeToken generic signatures unless the subclass is kept. +-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken { *; } +-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken + +# web3j Type.getTypeAsString() uses the datatype class simple name. +-keep class org.web3j.abi.datatypes.** { *; } +-keep class org.web3j.abi.TypeReference { *; } +-keep class * extends org.web3j.abi.TypeReference +-keep,allowoptimization,allowobfuscation,allowshrinking class org.web3j.crypto.** { *; } +-keep,allowoptimization,allowobfuscation,allowshrinking class org.web3j.protocol.** { *; } -dontwarn org.web3j.crypto.** -dontwarn jnr.unixsocket.** -dontwarn org.web3j.protocol.ipc.** @@ -80,13 +135,15 @@ -dontwarn org.web3j.protocol.websocket.** -dontwarn com.fasterxml.jackson.databind.** --keep class com.fasterxml.jackson.core.** { *; } --keep class com.fasterxml.jackson.databind.** { *; } --keep class com.fasterxml.jackson.annotation.** { *; } +-keep,allowoptimization,allowobfuscation,allowshrinking class com.fasterxml.jackson.** { *; } #-dontwarn java.lang.SafeVarargs -dontwarn org.slf4j.** --keep public class com.reown.walletkit.** { *; } +# JJWT discovers implementations through META-INF/services. +-keep class io.jsonwebtoken.impl.** { *; } +-keep class io.jsonwebtoken.orgjson.** { *; } + +-keep,allowoptimization,allowobfuscation,allowshrinking class com.reown.** { *; } -dontwarn com.sun.jna.** -keep class com.sun.jna.** { *; } @@ -94,9 +151,18 @@ -dontwarn uniffi.** -keep class uniffi.** { *; } +# Native Curve25519 providers are loaded by class name. +-keep class org.whispersystems.curve25519.NativeCurve25519Provider { *; } +-keep class org.whispersystems.curve25519.JavaCurve25519Provider { *; } +-keep class org.whispersystems.curve25519.OpportunisticCurve25519Provider { *; } +-keep,allowoptimization,allowobfuscation,allowshrinking class org.whispersystems.libsignal.** { *; } + -dontwarn groovy.lang.GroovyShell -dontwarn com.yalantis.ucrop** -dontwarn com.appsflyer.** --keep class kotlin.jvm.internal.** { *; } +-keep class com.appsflyer.** { *; } +-keep class com.android.installreferrer.** { *; } + +-keep,allowoptimization,allowobfuscation,allowshrinking class com.checkout.** { *; } diff --git a/app/src/main/java/one/mixin/android/MixinApplication.kt b/app/src/main/java/one/mixin/android/MixinApplication.kt index 33dc41dc0f..e908a19a47 100644 --- a/app/src/main/java/one/mixin/android/MixinApplication.kt +++ b/app/src/main/java/one/mixin/android/MixinApplication.kt @@ -87,6 +87,7 @@ import one.mixin.android.vo.CallStateLiveData import one.mixin.android.webrtc.GroupCallService import one.mixin.android.webrtc.VoiceCallService import one.mixin.android.webrtc.disconnect +import org.bitcoinj.crypto.MnemonicCode import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider import timber.log.Timber import java.util.concurrent.atomic.AtomicBoolean @@ -160,6 +161,7 @@ open class MixinApplication : override fun onCreate() { super.onCreate() + initMnemonicCode() applicationScope = getAppScope() init() registerActivityLifecycleCallbacks(this) @@ -189,6 +191,17 @@ open class MixinApplication : Session.getAccount()?.let(ThirdPartyUserIdentity::setUser) } + private fun initMnemonicCode() { + if (MnemonicCode.INSTANCE != null) return + val wordList = Thread.currentThread().contextClassLoader + ?.getResourceAsStream("en-mnemonic-word-list.txt") + ?: error("Missing BIP39 English word list") + MnemonicCode.INSTANCE = MnemonicCode( + wordList, + "ad90bf3beb7b0eb7e5acd74727dc0da96e0a280a258354e7293fb7e211ac03db", + ) + } + private fun initBugsnag() { val config = BugsnagConfiguration.load(this) config.enabledErrorTypes.anrs = true diff --git a/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeCache.kt b/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeCache.kt index f027f31331..0d2efdfb5d 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeCache.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeCache.kt @@ -1,6 +1,7 @@ package one.mixin.android.ui.wallet.home import android.content.SharedPreferences +import com.google.gson.JsonParser import com.google.gson.annotations.SerializedName import one.mixin.android.db.web3.vo.Web3TokenItem import one.mixin.android.db.web3.vo.Web3TransactionItem @@ -10,6 +11,7 @@ import one.mixin.android.vo.SnapshotItem import one.mixin.android.vo.safe.TokenItem private const val PREF_WALLET_HOME_CACHE_PREFIX = "pref_wallet_home_cache" +private val EARN_ACCOUNT_CACHE_FIELDS = arrayOf("assetId", "assetSymbol", "iconUrl", "balanceUsd", "earningsUsd") data class WalletHomeCache( @SerializedName("walletType") @@ -102,9 +104,24 @@ fun SharedPreferences.getWalletHomeCacheState( fun SharedPreferences.getWalletHomeCache( key: String, ): WalletHomeCache? = + getString(key, null).parseWalletHomeCache() + +internal fun String?.parseWalletHomeCache(): WalletHomeCache? = runCatching { - getString(key, null) - ?.let { GsonHelper.customGson.fromJson(it, WalletHomeCache::class.java) } + val root = JsonParser.parseString(this).asJsonObject + root.getAsJsonArray("earnAccounts")?.let { accounts -> + for (index in accounts.size() - 1 downTo 0) { + val account = accounts[index] + if (!account.isJsonObject || EARN_ACCOUNT_CACHE_FIELDS.any { field -> + val value = account.asJsonObject.get(field) + value == null || value.isJsonNull + } + ) { + accounts.remove(index) + } + } + } + GsonHelper.customGson.fromJson(root, WalletHomeCache::class.java) }.getOrNull() fun SharedPreferences.putWalletHomeCache( diff --git a/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeEarnAccount.kt b/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeEarnAccount.kt index 8b1d8985d7..28ae13822e 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeEarnAccount.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeEarnAccount.kt @@ -1,5 +1,6 @@ package one.mixin.android.ui.wallet.home +import com.google.gson.annotations.SerializedName import one.mixin.android.api.response.EarnAccountSummary import one.mixin.android.api.response.EarnProduct import one.mixin.android.extension.numberFormat2 @@ -7,11 +8,17 @@ import one.mixin.android.vo.safe.TokenItem import java.math.BigDecimal data class WalletHomeEarnAccount( + @SerializedName("assetId") val assetId: String, + @SerializedName("assetSymbol") val assetSymbol: String, + @SerializedName("iconUrl") val iconUrl: String, + @SerializedName("balanceUsd") val balanceUsd: BigDecimal, + @SerializedName("earningsUsd") val earningsUsd: BigDecimal, + @SerializedName("apyText") val apyText: String?, ) { val balanceAmountText: String diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index f46af19384..1240c4fda8 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -2181,16 +2181,6 @@ 지갑을 제거하기 전에 복구 문구나 개인 키를 적어 두었는지 확인하세요. 그렇지 않으면 지갑을 복구할 수 없습니다. 이 지갑은 보기 전용 지갑이므로 삭제해도 자산에 영향을 미치지 않습니다. %1$d 단어 - 12단어 또는 24단어 - 12단어 또는 24단어 니모닉 지갑을 사용하려면 다음 단계를 따르세요. - Mixin 계정을 등록하세요. - 지갑 목록으로 이동 → 지갑 추가 → 니모닉 가져오기를 선택하세요. - 가져오기를 완료하려면 12~24단어 니모닉 문구 또는 개인 키를 입력하세요. - 사용을 시작하세요. - Mixin 계정을 생성하면 기본적으로 개인정보 지갑이 생성됩니다. 귀하의 잔액, 이체, 거래 및 기타 기능은 기본적으로 개인 정보 보호로 보호됩니다. - Mixin 계정을 생성하면 기본적으로 개인정보 지갑이 생성됩니다. 귀하의 잔액, 이체, 거래 및 기타 기능은 기본적으로 %1$s의 개인 정보 보호로 보호됩니다. - Mixin 계정 만들기 - 15초 안에 등록하세요 개인 키 가져오기 감시 주소 추가 개인 키를 입력하세요 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 88926174a7..dcfad32764 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -2279,16 +2279,6 @@ 請確保您在刪除錢包之前記下恢復短語或私鑰。否則你將無法找回錢包。 這是一個觀察錢包,刪除此錢包不會影響你的資產。 %1$d 字 - 12 或 24 個字 - 使用12字或24字助記詞錢包,請依照下列步驟操作: - 註冊一個Mixin帳號。 - 進入錢包清單→新增錢包→選擇導入助記詞。 - 輸入您的12或24字助記詞或私密金鑰即可完成匯入。 - 開始使用。 - 建立Mixin帳號後,會預設產生隱私錢包。您的餘額、轉帳、交易等功能將預設受到隱私保護, - 建立Mixin帳號後,會預設產生隱私錢包。您的餘額、轉帳、交易等功能將預設為隱私保護,%1$s。 - 建立Mixin帳戶 - 15 秒內註冊 匯入私鑰 新增觀看地址 輸入您的私鑰 diff --git a/app/src/test/java/one/mixin/android/ui/wallet/home/WalletHomeCashAccountTest.kt b/app/src/test/java/one/mixin/android/ui/wallet/home/WalletHomeCashAccountTest.kt index 0cbf1b6082..55847caa4c 100644 --- a/app/src/test/java/one/mixin/android/ui/wallet/home/WalletHomeCashAccountTest.kt +++ b/app/src/test/java/one/mixin/android/ui/wallet/home/WalletHomeCashAccountTest.kt @@ -8,6 +8,7 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Test import java.math.BigDecimal @@ -156,6 +157,29 @@ class WalletHomeCashAccountTest { assertFalse(state.cards.contains(WalletHomeCardType.CASH)) } + @Test + fun walletHomeCacheDropsIncompleteEarnAccounts() { + val json = """ + { + "walletType":"PRIVACY", + "fiatTotal":"0.00", + "btcTotal":"0.00", + "fiatSymbol":"USD", + "totalTokenCount":0, + "totalTransactionCount":0, + "earnAccounts":[ + {"assetId":"asset-1","assetSymbol":"USD","iconUrl":"","earningsUsd":"0"}, + {"assetId":"asset-2","assetSymbol":"USD","iconUrl":"","balanceUsd":"1.25","earningsUsd":"0"} + ] + } + """.trimIndent() + + val cache = json.parseWalletHomeCache() + + assertTrue(cache != null) + assertEquals(listOf("asset-2"), cache?.toState()?.earnAccounts?.map { it.assetId }) + } + @Test fun walletHomeStateShowsDynamicBannerWhenStillLoading() { val banner = WalletHomeBanner(