From e49b51bafe5923438bf782854a279b1f6df89540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mithat=20Sinan=20Sar=C4=B1?= <36641492+mitsinsar@users.noreply.github.com> Date: Tue, 25 Feb 2025 08:52:47 +0300 Subject: [PATCH 1/4] PERA-1678 :: Cherry-pick SingleAssetRepository common-sdk changes to dev (#158) --- .../data/database/model/AssetDetailEntity.kt | 5 +- .../wallet/asset/data/model/AssetResponse.kt | 5 +- .../repository/SingleAssetRepositoryImpl.kt | 53 ++++++++ .../wallet/asset/di/SingleAssetModule.kt | 66 ++++++++++ .../wallet/asset/domain/model/Asset.kt | 7 +- .../repository/SingleAssetRepository.kt | 25 ++++ .../asset/domain/usecase/AssetUseCases.kt | 12 ++ ....kt => AssetDetailEntityMapperImplTest.kt} | 11 +- .../SingleAssetRepositoryImplTest.kt | 120 ++++++++++++++++++ 9 files changed, 292 insertions(+), 12 deletions(-) create mode 100644 common-sdk/src/main/kotlin/com/algorand/wallet/asset/data/repository/SingleAssetRepositoryImpl.kt create mode 100644 common-sdk/src/main/kotlin/com/algorand/wallet/asset/di/SingleAssetModule.kt create mode 100644 common-sdk/src/main/kotlin/com/algorand/wallet/asset/domain/repository/SingleAssetRepository.kt rename common-sdk/src/test/kotlin/com/algorand/wallet/asset/data/mapper/entity/{AssetAssetInfoEntityMapperImplTest.kt => AssetDetailEntityMapperImplTest.kt} (93%) create mode 100644 common-sdk/src/test/kotlin/com/algorand/wallet/asset/data/repository/SingleAssetRepositoryImplTest.kt diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/data/database/model/AssetDetailEntity.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/asset/data/database/model/AssetDetailEntity.kt index c542173b..267bd8e0 100644 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/data/database/model/AssetDetailEntity.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/asset/data/database/model/AssetDetailEntity.kt @@ -16,6 +16,7 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import com.algorand.wallet.asset.data.database.model.AssetDetailEntity.Companion.ASSET_DETAIL_TABLE_NAME +import java.math.BigDecimal @Entity(tableName = ASSET_DETAIL_TABLE_NAME) internal data class AssetDetailEntity( @@ -33,7 +34,7 @@ internal data class AssetDetailEntity( val decimals: Int, @ColumnInfo("usd_value") - val usdValue: String?, + val usdValue: BigDecimal?, @ColumnInfo("max_supply") val maxSupply: String, @@ -72,7 +73,7 @@ internal data class AssetDetailEntity( val totalSupply: String?, @ColumnInfo("last_24_hours_algo_price_change_percentage") - val last24HoursAlgoPriceChangePercentage: String?, + val last24HoursAlgoPriceChangePercentage: BigDecimal?, @ColumnInfo("available_on_discover_mobile") val availableOnDiscoverMobile: Boolean, diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/data/model/AssetResponse.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/asset/data/model/AssetResponse.kt index 33e54986..1accb1d4 100644 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/data/model/AssetResponse.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/asset/data/model/AssetResponse.kt @@ -14,6 +14,7 @@ package com.algorand.wallet.asset.data.model import com.algorand.wallet.asset.data.model.collectible.CollectibleResponse import com.google.gson.annotations.SerializedName +import java.math.BigDecimal internal data class AssetResponse( @SerializedName("asset_id") val assetId: Long? = null, @@ -21,7 +22,7 @@ internal data class AssetResponse( @SerializedName("logo") val logoUri: String? = null, @SerializedName("unit_name") val shortName: String? = null, @SerializedName("fraction_decimals") val fractionDecimals: Int? = null, - @SerializedName("usd_value") val usdValue: String? = null, + @SerializedName("usd_value") val usdValue: BigDecimal? = null, @SerializedName("creator") val assetCreator: AssetCreatorResponse? = null, @SerializedName("collectible") val collectible: CollectibleResponse? = null, @SerializedName("total") val maxSupply: String? = null, @@ -36,6 +37,6 @@ internal data class AssetResponse( @SerializedName("description") val description: String? = null, @SerializedName("url") val url: String? = null, @SerializedName("total_supply") val totalSupply: String? = null, - @SerializedName("last_24_hours_algo_price_change_percentage") val last24HoursAlgoPriceChangePercentage: String? = null, + @SerializedName("last_24_hours_algo_price_change_percentage") val last24HoursAlgoPriceChangePercentage: BigDecimal? = null, @SerializedName("available_on_discover_mobile") val isAvailableOnDiscoverMobile: Boolean? = null ) diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/data/repository/SingleAssetRepositoryImpl.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/asset/data/repository/SingleAssetRepositoryImpl.kt new file mode 100644 index 00000000..33675e62 --- /dev/null +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/asset/data/repository/SingleAssetRepositoryImpl.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.wallet.asset.data.repository + +import com.algorand.wallet.asset.data.mapper.model.AssetMapper +import com.algorand.wallet.asset.data.service.AssetDetailApiService +import com.algorand.wallet.asset.domain.model.Asset +import com.algorand.wallet.asset.domain.repository.SingleAssetRepository +import com.algorand.wallet.foundation.cache.CacheResult +import com.algorand.wallet.foundation.cache.SingleInMemoryLocalCache +import com.algorand.wallet.foundation.network.utils.request +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapNotNull + +internal class SingleAssetRepositoryImpl @Inject constructor( + private val assetDetailApi: AssetDetailApiService, + private val assetCache: SingleInMemoryLocalCache, + private val assetMapper: AssetMapper +) : SingleAssetRepository { + + override suspend fun cacheAssetDetail(assetId: Long) { + request { assetDetailApi.getAssetDetail(assetId) }.use( + onSuccess = { + val asset = assetMapper(it) + asset?.let { assetCache.put(CacheResult.Success.create(asset)) } + }, + onFailed = { exception, code -> + assetCache.put(CacheResult.Error.create(exception, code = code)) + } + ) + } + + override fun getAssetDetailFlow(): Flow { + return assetCache.cacheFlow.mapNotNull { + it?.getDataOrNull() + } + } + + override suspend fun clearCache() { + assetCache.clear() + } +} diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/di/SingleAssetModule.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/asset/di/SingleAssetModule.kt new file mode 100644 index 00000000..d14896d7 --- /dev/null +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/asset/di/SingleAssetModule.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.wallet.asset.di + +import com.algorand.wallet.asset.data.mapper.model.AssetMapper +import com.algorand.wallet.asset.data.repository.SingleAssetRepositoryImpl +import com.algorand.wallet.asset.data.service.AssetDetailApiService +import com.algorand.wallet.asset.domain.repository.SingleAssetRepository +import com.algorand.wallet.asset.domain.usecase.CacheSingleAssetDetail +import com.algorand.wallet.asset.domain.usecase.ClearSingleAssetCache +import com.algorand.wallet.asset.domain.usecase.GetSingleAssetDetailFlow +import com.algorand.wallet.foundation.cache.SingleInMemoryLocalCache +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object SingleAssetModule { + + @Provides + @Singleton + fun provideSingleAssetRepository( + assetDetailApiService: AssetDetailApiService, + assetMapper: AssetMapper + ): SingleAssetRepository { + return SingleAssetRepositoryImpl( + assetDetailApiService, + SingleInMemoryLocalCache(), + assetMapper + ) + } + + @Provides + fun provideCacheSingleAssetDetail( + repository: SingleAssetRepository + ): CacheSingleAssetDetail { + return CacheSingleAssetDetail(repository::cacheAssetDetail) + } + + @Provides + fun provideGetSingleAssetDetailFlow( + repository: SingleAssetRepository + ): GetSingleAssetDetailFlow { + return GetSingleAssetDetailFlow(repository::getAssetDetailFlow) + } + + @Provides + fun provideClearSingleAssetCache( + repository: SingleAssetRepository + ): ClearSingleAssetCache { + return ClearSingleAssetCache(repository::clearCache) + } +} diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/domain/model/Asset.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/asset/domain/model/Asset.kt index 78e613a3..b0024ada 100644 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/domain/model/Asset.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/asset/domain/model/Asset.kt @@ -13,6 +13,7 @@ package com.algorand.wallet.asset.domain.model import com.algorand.wallet.asset.domain.util.AssetConstants.ALGO_ID +import java.math.BigDecimal sealed interface Asset { @@ -29,7 +30,7 @@ sealed interface Asset { val shortName: String? get() = assetInfo?.name?.shortName - val usdValue: String? + val usdValue: BigDecimal? get() = assetInfo?.fiat?.usdValue val creatorAddress: String? @@ -88,7 +89,7 @@ sealed interface Asset { ) data class Fiat( - val usdValue: String?, - val last24HoursAlgoPriceChangePercentage: String? + val usdValue: BigDecimal?, + val last24HoursAlgoPriceChangePercentage: BigDecimal? ) } diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/domain/repository/SingleAssetRepository.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/asset/domain/repository/SingleAssetRepository.kt new file mode 100644 index 00000000..9fa5ebbe --- /dev/null +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/asset/domain/repository/SingleAssetRepository.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.wallet.asset.domain.repository + +import com.algorand.wallet.asset.domain.model.Asset +import kotlinx.coroutines.flow.Flow + +internal interface SingleAssetRepository { + + suspend fun cacheAssetDetail(assetId: Long) + + fun getAssetDetailFlow(): Flow + + suspend fun clearCache() +} diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/domain/usecase/AssetUseCases.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/asset/domain/usecase/AssetUseCases.kt index e318b941..35b5b249 100644 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/domain/usecase/AssetUseCases.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/asset/domain/usecase/AssetUseCases.kt @@ -62,3 +62,15 @@ fun interface GetCollectiblesDetail { fun interface InitializeAssets { suspend operator fun invoke(assetIds: List) } + +fun interface CacheSingleAssetDetail { + suspend operator fun invoke(assetId: Long) +} + +fun interface GetSingleAssetDetailFlow { + operator fun invoke(): Flow +} + +fun interface ClearSingleAssetCache { + suspend operator fun invoke() +} diff --git a/common-sdk/src/test/kotlin/com/algorand/wallet/asset/data/mapper/entity/AssetAssetInfoEntityMapperImplTest.kt b/common-sdk/src/test/kotlin/com/algorand/wallet/asset/data/mapper/entity/AssetDetailEntityMapperImplTest.kt similarity index 93% rename from common-sdk/src/test/kotlin/com/algorand/wallet/asset/data/mapper/entity/AssetAssetInfoEntityMapperImplTest.kt rename to common-sdk/src/test/kotlin/com/algorand/wallet/asset/data/mapper/entity/AssetDetailEntityMapperImplTest.kt index ca8b0fea..7c9395ec 100644 --- a/common-sdk/src/test/kotlin/com/algorand/wallet/asset/data/mapper/entity/AssetAssetInfoEntityMapperImplTest.kt +++ b/common-sdk/src/test/kotlin/com/algorand/wallet/asset/data/mapper/entity/AssetDetailEntityMapperImplTest.kt @@ -19,12 +19,13 @@ import com.algorand.wallet.asset.data.model.AssetCreatorResponse import com.algorand.wallet.asset.data.model.AssetResponse import io.mockk.every import io.mockk.mockk +import java.math.BigDecimal import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Test -internal class AssetAssetInfoEntityMapperImplTest { +internal class AssetDetailEntityMapperImplTest { private val verificationTierEntityMapper: VerificationTierEntityMapper = mockk() @@ -73,7 +74,7 @@ internal class AssetAssetInfoEntityMapperImplTest { fullName = "fullName", shortName = "shortName", fractionDecimals = 2, - usdValue = "10", + usdValue = BigDecimal.TEN, maxSupply = "10", explorerUrl = "explorerUrl", projectUrl = "projectUrl", @@ -87,7 +88,7 @@ internal class AssetAssetInfoEntityMapperImplTest { twitterUsername = "twitterUsername", discordUrl = "discordUrl", isAvailableOnDiscoverMobile = true, - last24HoursAlgoPriceChangePercentage = "10", + last24HoursAlgoPriceChangePercentage = BigDecimal.TEN, verificationTier = null, assetCreator = AssetCreatorResponse( publicKey = "publicKey", @@ -102,7 +103,7 @@ internal class AssetAssetInfoEntityMapperImplTest { name = "fullName", unitName = "shortName", decimals = 2, - usdValue = "10", + usdValue = BigDecimal.TEN, maxSupply = "10", explorerUrl = "explorerUrl", projectUrl = "projectUrl", @@ -116,7 +117,7 @@ internal class AssetAssetInfoEntityMapperImplTest { twitterUsername = "twitterUsername", discordUrl = "discordUrl", availableOnDiscoverMobile = true, - last24HoursAlgoPriceChangePercentage = "10", + last24HoursAlgoPriceChangePercentage = BigDecimal.TEN, verificationTier = VerificationTierEntity.UNKNOWN, assetCreatorAddress = "publicKey", assetCreatorId = 1L, diff --git a/common-sdk/src/test/kotlin/com/algorand/wallet/asset/data/repository/SingleAssetRepositoryImplTest.kt b/common-sdk/src/test/kotlin/com/algorand/wallet/asset/data/repository/SingleAssetRepositoryImplTest.kt new file mode 100644 index 00000000..5090f8a4 --- /dev/null +++ b/common-sdk/src/test/kotlin/com/algorand/wallet/asset/data/repository/SingleAssetRepositoryImplTest.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.wallet.asset.data.repository + +import com.algorand.test.peraFixture +import com.algorand.test.test +import com.algorand.wallet.asset.data.mapper.model.AssetMapper +import com.algorand.wallet.asset.data.model.AssetResponse +import com.algorand.wallet.asset.data.service.AssetDetailApiService +import com.algorand.wallet.asset.domain.model.Asset +import com.algorand.wallet.foundation.cache.CacheResult +import com.algorand.wallet.foundation.cache.SingleInMemoryLocalCache +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import okhttp3.ResponseBody.Companion.toResponseBody +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import retrofit2.Response + +class SingleAssetRepositoryImplTest { + + private val assetCache: SingleInMemoryLocalCache = mockk(relaxed = true) + private val assetDetailApi: AssetDetailApiService = mockk() + private val assetMapper: AssetMapper = mockk() + + private val sut = SingleAssetRepositoryImpl(assetDetailApi, assetCache, assetMapper) + + @Test + fun `EXPECT clear to be cached()`() = runTest { + sut.clearCache() + + verify { assetCache.clear() } + } + + @Test + fun `EXPECT asset to be cached WHEN fetching succeeds`() = runTest { + coEvery { assetDetailApi.getAssetDetail(ASSET_DETAIL.id) } returns Response.success(ASSET_RESPONSE) + every { assetMapper(ASSET_RESPONSE) } returns ASSET_DETAIL + val cacheSlot = slot>() + every { assetCache.put(capture(cacheSlot)) } returns Unit + + sut.cacheAssetDetail(ASSET_DETAIL.id) + + val captured = cacheSlot.captured + assertEquals(ASSET_DETAIL, captured.data) + } + + @Test + fun `EXPECT error to be cached WHEN fetching fails`() = runTest { + val code = 404 + val errorBody = Response.error(code, "".toResponseBody(null)) + coEvery { assetDetailApi.getAssetDetail(ASSET_DETAIL.id) } returns errorBody + val cacheSlot = slot>() + every { assetCache.put(capture(cacheSlot)) } returns Unit + + sut.cacheAssetDetail(ASSET_DETAIL.id) + + val captured = cacheSlot.captured + assertTrue(captured is CacheResult.Error) + } + + @Test + fun `EXPECT nothing to be cached WHEN fetching succeeds but mapping fails`() = runTest { + coEvery { assetDetailApi.getAssetDetail(ASSET_DETAIL.id) } returns Response.success(ASSET_RESPONSE) + every { assetMapper(ASSET_RESPONSE) } returns null + + sut.cacheAssetDetail(ASSET_DETAIL.id) + + verify(exactly = 0) { assetCache.put(any()) } + } + + @Test + fun `EXPECT mapped asset detail flow WHEN cached data is not null`() { + val cachedAsset = CacheResult.Success.create(ASSET_DETAIL) + val assetCacheFlow = MutableStateFlow(cachedAsset) + every { assetCache.cacheFlow } returns assetCacheFlow + + val testObserver = sut.getAssetDetailFlow().test() + + testObserver.assertValueHistory(ASSET_DETAIL) + } + + @Test + fun `EXPECT nothing to be emitted WHEN cache is empty`() { + val testObserver = sut.getAssetDetailFlow().test() + + testObserver.assertValueHistory() + } + + @Test + fun `EXPECT nothing to be emitted WHEN cached data is null`() { + val cacheFlow = MutableStateFlow?>(null) + every { assetCache.cacheFlow } returns cacheFlow + + val testObserver = sut.getAssetDetailFlow().test() + + testObserver.assertNoValue() + } + + private companion object { + val ASSET_DETAIL = peraFixture() + val ASSET_RESPONSE = peraFixture() + } +} From 2b2be9e745fae651f35106a83f5047e246ad3cff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mithat=20Sinan=20Sar=C4=B1?= <36641492+mitsinsar@users.noreply.github.com> Date: Thu, 27 Feb 2025 00:44:50 +0300 Subject: [PATCH 2/4] PERA-1697 :: Implement kover test coverage report (#162) --- .github/workflows/android-app-tests.yml | 26 ++++++ build.gradle.kts | 1 + common-sdk/build.gradle.kts | 2 + common-sdk/test-coverage/coverageValidator.sh | 90 +++++++++++++++++++ common-sdk/test-coverage/excludes.gradle.kts | 44 +++++++++ common-sdk/test-coverage/kover.gradle | 21 +++++ common-sdk/test-coverage/testCoverage.sh | 10 +++ gradle/libs.versions.toml | 2 + 8 files changed, 196 insertions(+) create mode 100755 common-sdk/test-coverage/coverageValidator.sh create mode 100644 common-sdk/test-coverage/excludes.gradle.kts create mode 100644 common-sdk/test-coverage/kover.gradle create mode 100755 common-sdk/test-coverage/testCoverage.sh diff --git a/.github/workflows/android-app-tests.yml b/.github/workflows/android-app-tests.yml index 34f75e49..2b5bc999 100644 --- a/.github/workflows/android-app-tests.yml +++ b/.github/workflows/android-app-tests.yml @@ -55,6 +55,32 @@ jobs: path: ./**/build/reports/** overwrite: true + android-test-coverage: + name: "Android common-sdk Test Coverage" + runs-on: ubuntu-latest + steps: + - name: "Checkout" + uses: actions/checkout@v4 + - name: "Install JDK 21" + uses: actions/setup-java@v4 + with: + distribution: "zulu" + java-version: "21" + cache: "gradle" + - name: "Update packages" + run: sudo apt-get update + - name: "Install xmlstarlet" + run: sudo apt-get install xmlstarlet + - name: "Generate Report and Validate Coverage" + run: ./common-sdk/test-coverage/testCoverage.sh + - name: "Archive Test Coverage Results" + uses: actions/upload-artifact@v4 + if: ${{ always() }} + with: + name: "test-coverage-result" + path: ./common-sdk/build/reports/kover/** + overwrite: true + android-bundle-publish-test: name: "Android App Bundle Publish Test" runs-on: ubuntu-latest diff --git a/build.gradle.kts b/build.gradle.kts index 00a2bad2..a5c2c67c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,6 +15,7 @@ buildscript { classpath(libs.ksp.gradle.plugin) classpath(libs.navigation.safe.args.gradle.plugin) classpath(libs.firebase.perf.plugin) + classpath(libs.kover.plugin) // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/common-sdk/build.gradle.kts b/common-sdk/build.gradle.kts index 1d940da0..528468c6 100644 --- a/common-sdk/build.gradle.kts +++ b/common-sdk/build.gradle.kts @@ -7,6 +7,8 @@ plugins { id("dagger.hilt.android.plugin") } +apply(from = "./test-coverage/kover.gradle") + android { namespace = "com.algorand.wallet" compileSdk = libs.versions.android.compileSdk.get().toInt() diff --git a/common-sdk/test-coverage/coverageValidator.sh b/common-sdk/test-coverage/coverageValidator.sh new file mode 100755 index 00000000..02ceb985 --- /dev/null +++ b/common-sdk/test-coverage/coverageValidator.sh @@ -0,0 +1,90 @@ +#!/bin/bash + +set -e + +MIN_ACCEPTABLE_CLASS_COVERAGE=48.0 +MIN_ACCEPTABLE_METHOD_COVERAGE=45.3 +MIN_ACCEPTABLE_BRANCH_COVERAGE=44.0 +MIN_ACCEPTABLE_LINE_COVERAGE=48.6 +MIN_ACCEPTABLE_INSTRUCTION_COVERAGE=43.8 + +COVERAGE_REPORT="common-sdk/build/reports/kover/reportCustomDebug.xml" + +get_overall_report_tag() { + echo "//report[1]" +} + +get_covered_number_of_type() { + xmlstarlet sel -t -m "$(get_overall_report_tag)" \ + -v "counter[@type='$1']/@covered" \ + -n "$COVERAGE_REPORT" +} + +get_missed_number_of_type() { + xmlstarlet sel -t -m "$(get_overall_report_tag)" \ + -v "counter[@type='$1']/@missed" \ + -n "$COVERAGE_REPORT" +} + +get_coverage_percentage() { + total=$(echo "$1 + $2" | bc) + ratio=$(echo "scale=3; $1 / $total" | bc) + printf "%.1f\n" "$(echo "$ratio * 100" | bc)" +} + +get_coverage_report_percentage() { + counterType=$1 + covered=$(get_covered_number_of_type "$counterType") + missed=$(get_missed_number_of_type "$counterType") + get_coverage_percentage "$covered" "$missed" +} + +validate_coverage_with_message() { + currentCoverage=$1 + minAcceptableCoverage=$2 + coverageType=$3 + echo "Coverage for $coverageType: $currentCoverage%" + if (($(echo "$currentCoverage < $minAcceptableCoverage" | bc -l))); then + echo "$coverageType coverage is below the required threshold of $minAcceptableCoverage%." + exit 1 + fi +} + +validate_line_coverage() { + lineCoverage=$(get_coverage_report_percentage "LINE") + validate_coverage_with_message "$lineCoverage" "$1" "Line" +} + +validate_branch_coverage() { + branchCoverage=$(get_coverage_report_percentage "BRANCH") + validate_coverage_with_message "$branchCoverage" "$1" "Branch" +} + +validate_method_coverage() { + methodCoverage=$(get_coverage_report_percentage "METHOD") + validate_coverage_with_message "$methodCoverage" "$1" "Method" +} + +validate_class_coverage() { + classCoverage=$(get_coverage_report_percentage "CLASS") + validate_coverage_with_message "$classCoverage" "$1" "Class" +} + +validate_instruction_coverage() { + instructionCoverage=$(get_coverage_report_percentage "INSTRUCTION") + validate_coverage_with_message "$instructionCoverage" "$1" "Instruction" +} + +validate_test_coverage() { + validate_class_coverage "$MIN_ACCEPTABLE_CLASS_COVERAGE" + validate_method_coverage "$MIN_ACCEPTABLE_METHOD_COVERAGE" + validate_branch_coverage "$MIN_ACCEPTABLE_BRANCH_COVERAGE" + validate_line_coverage "$MIN_ACCEPTABLE_LINE_COVERAGE" + validate_instruction_coverage "$MIN_ACCEPTABLE_INSTRUCTION_COVERAGE" + echo "All coverage thresholds are met." + exit 0 +} + +validate_test_coverage + +exit 1 diff --git a/common-sdk/test-coverage/excludes.gradle.kts b/common-sdk/test-coverage/excludes.gradle.kts new file mode 100644 index 00000000..6833a8c2 --- /dev/null +++ b/common-sdk/test-coverage/excludes.gradle.kts @@ -0,0 +1,44 @@ +val excludedClasses = listOf( + // android + "*.R", + "*.R$*", + "*.BuildConfig", + "*.Manifest*", + "android.*.*.*", + // dagger + "*.*_MembersInjector", + "*.Dagger*Component", + "*.Dagger*Component\$Builder", + "*.*Module_*Factory", + "*.di.module.*", + "*.*_Factory*", + "*.*Module*", + "*.*Dagger*", + "*.*Hilt*", + "*.*GeneratedInjector*", + "*.codegen.*", + "*.*_Impl*", + // kotlin + "*.*Component*", + "*.*BR*", + "*.*\$Lambda$*", + "*.*Companion*", + "*.*MembersInjector*", + "*.*_Provide*Factory*", + "*.*Extensions*", + // Pera + "*.di.*", + "*.domain.model.*", + "*.data.model.*", + "*.data.service.*", + "*.database.model.*", + "*.database.dao.*", + "*PeraResult*" +) + +val excludedPackages = listOf( + "com.algorand.wallet.viewmodel" +) + +extra["excludedClasses"] = excludedClasses +extra["excludedPackages"] = excludedPackages diff --git a/common-sdk/test-coverage/kover.gradle b/common-sdk/test-coverage/kover.gradle new file mode 100644 index 00000000..0d246796 --- /dev/null +++ b/common-sdk/test-coverage/kover.gradle @@ -0,0 +1,21 @@ +apply plugin: 'org.jetbrains.kotlinx.kover' +apply from: './test-coverage/excludes.gradle.kts' + +kover { + currentProject { + createVariant("customDebug") { + add(["debug"], false) + } + createVariant("customRelease") { + add(["release"], false) + } + } + reports { + filters { + excludes { + classes(excludedClasses) + packages(excludedPackages) + } + } + } +} diff --git a/common-sdk/test-coverage/testCoverage.sh b/common-sdk/test-coverage/testCoverage.sh new file mode 100755 index 00000000..bdb65391 --- /dev/null +++ b/common-sdk/test-coverage/testCoverage.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -e + +# Generate HTML report for devs +./gradlew koverHtmlReportCustomDebug + +# Generate XML report for coverage calculation +./gradlew koverXmlReportCustomDebug + +./common-sdk/test-coverage/coverageValidator.sh diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e752a6ae..e6767ff5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,6 +50,7 @@ kotlin = "2.0.21" kotlinfixture = "1.2.0" kotlinxDatetime = "0.5.0" kotlinxSerialization = "1.7.3" +kover = "0.8.3" kspGradlePlugin = "2.0.21-1.0.25" ksp = "2.0.21-1.0.25" ktlint = "0.39.0" @@ -200,6 +201,7 @@ espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = google-services = { module = "com.google.gms:google-services", version.ref = "googleServicesVersion" } junit = { module = "junit:junit", version.ref = "junit" } kotlinfixture = { module = "com.appmattus.fixture:fixture", version.ref = "kotlinfixture" } +kover-plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" } ktlint = { module = "com.pinterest:ktlint", version.ref = "ktlint" } mockito = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockito" } mockk = { module = "io.mockk:mockk", version.ref = "mockk" } From db59507be629864b9938cd65fb1a483bc2243715 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mithat=20Sinan=20Sar=C4=B1?= <36641492+mitsinsar@users.noreply.github.com> Date: Thu, 27 Feb 2025 15:45:27 +0300 Subject: [PATCH 3/4] PERA-1704 Update coverage thresholds (#163) --- common-sdk/test-coverage/coverageValidator.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/common-sdk/test-coverage/coverageValidator.sh b/common-sdk/test-coverage/coverageValidator.sh index 02ceb985..efcaee27 100755 --- a/common-sdk/test-coverage/coverageValidator.sh +++ b/common-sdk/test-coverage/coverageValidator.sh @@ -2,11 +2,11 @@ set -e -MIN_ACCEPTABLE_CLASS_COVERAGE=48.0 -MIN_ACCEPTABLE_METHOD_COVERAGE=45.3 -MIN_ACCEPTABLE_BRANCH_COVERAGE=44.0 -MIN_ACCEPTABLE_LINE_COVERAGE=48.6 -MIN_ACCEPTABLE_INSTRUCTION_COVERAGE=43.8 +MIN_ACCEPTABLE_CLASS_COVERAGE=48.2 +MIN_ACCEPTABLE_METHOD_COVERAGE=45.6 +MIN_ACCEPTABLE_BRANCH_COVERAGE=45.4 +MIN_ACCEPTABLE_LINE_COVERAGE=49.6 +MIN_ACCEPTABLE_INSTRUCTION_COVERAGE=44.7 COVERAGE_REPORT="common-sdk/build/reports/kover/reportCustomDebug.xml" From 178eb799561f2f7953631fe779a671d8fa9946a2 Mon Sep 17 00:00:00 2001 From: Yasin <6695727+yasin-ce@users.noreply.github.com> Date: Thu, 27 Feb 2025 16:13:06 +0100 Subject: [PATCH 4/4] Pera 1689 :: Create new IsAccountRekeyedToAnotherAccountUseCase and replace isAccountRekeyed function with it (#164) --- .../modules/accounts/di/AccountsModule.kt | 7 -- .../CreateArc59ClaimTransactionUseCase.kt | 22 +++--- .../CreateArc59RejectTransactionUseCase.kt | 12 ++-- .../usecase/CreateArc59SendTransaction.kt | 2 +- .../CreateArc59SendTransactionUseCase.kt | 17 +++-- .../usecase/CreateArc59TransactionsUseCase.kt | 2 +- .../usecase/CreateKeyRegTransactionUseCase.kt | 8 +-- .../CreateSwapQuoteTransactionsUseCase.kt | 6 +- .../android/usecase/AccountDetailUseCase.kt | 13 +--- .../account/detail/di/AccountDetailModule.kt | 11 ++- .../domain/usecase/AccountDetailUseCases.kt | 4 ++ ...IsAccountRekeyedToAnotherAccountUseCase.kt | 26 +++++++ .../info/di/AccountInformationModule.kt | 6 +- .../usecase/AccountInformationUseCases.kt | 2 +- ...countRekeyedToAnotherAccountUseCaseTest.kt | 69 +++++++++++++++++++ 15 files changed, 153 insertions(+), 54 deletions(-) create mode 100644 common-sdk/src/main/kotlin/com/algorand/wallet/account/detail/domain/usecase/IsAccountRekeyedToAnotherAccountUseCase.kt create mode 100644 common-sdk/src/test/kotlin/com/algorand/wallet/account/detail/domain/usecase/IsAccountRekeyedToAnotherAccountUseCaseTest.kt diff --git a/app/src/main/kotlin/com/algorand/android/modules/accounts/di/AccountsModule.kt b/app/src/main/kotlin/com/algorand/android/modules/accounts/di/AccountsModule.kt index 44c76d50..a1769070 100644 --- a/app/src/main/kotlin/com/algorand/android/modules/accounts/di/AccountsModule.kt +++ b/app/src/main/kotlin/com/algorand/android/modules/accounts/di/AccountsModule.kt @@ -13,7 +13,6 @@ package com.algorand.android.modules.accounts.di import com.algorand.android.modules.accounts.domain.usecase.GetAuthAddressOfAnAccount -import com.algorand.android.modules.accounts.domain.usecase.IsSenderRekeyedToAnotherAccount import com.algorand.android.usecase.AccountDetailUseCase import dagger.Module import dagger.Provides @@ -25,12 +24,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object AccountsModule { - @Provides - @Singleton - fun provideIsSenderRekeyedToAnotherAccount( - useCase: AccountDetailUseCase - ): IsSenderRekeyedToAnotherAccount = IsSenderRekeyedToAnotherAccount(useCase::isAccountRekeyed) - @Provides @Singleton fun provideGetAuthAddressOfAnAccount( diff --git a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/detail/receivedetail/domain/usecase/CreateArc59ClaimTransactionUseCase.kt b/app/src/main/kotlin/com/algorand/android/modules/assetinbox/detail/receivedetail/domain/usecase/CreateArc59ClaimTransactionUseCase.kt index 7fe61055..8e36c629 100644 --- a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/detail/receivedetail/domain/usecase/CreateArc59ClaimTransactionUseCase.kt +++ b/app/src/main/kotlin/com/algorand/android/modules/assetinbox/detail/receivedetail/domain/usecase/CreateArc59ClaimTransactionUseCase.kt @@ -20,20 +20,24 @@ import com.algorand.android.models.TransactionParams import com.algorand.android.modules.assetinbox.detail.receivedetail.domain.model.Arc59ClaimTransactionPayload import com.algorand.android.modules.assetinbox.detail.receivedetail.domain.model.BaseArc59ClaimRejectTransaction.Arc59ClaimTransaction import com.algorand.android.repository.TransactionsRepository -import com.algorand.android.usecase.AccountDetailUseCase import com.algorand.android.usecase.IsOnTestnetUseCase import com.algorand.android.utils.toSuggestedParams +import com.algorand.wallet.account.detail.domain.usecase.IsAccountRekeyedToAnotherAccount +import com.algorand.wallet.account.info.domain.usecase.IsAssetOwnedByAccount +import com.algorand.wallet.account.info.domain.usecase.GetAccountRekeyAdminAddress import javax.inject.Inject class CreateArc59ClaimTransactionUseCase @Inject constructor( - private val accountDetailUseCase: AccountDetailUseCase, private val transactionsRepository: TransactionsRepository, - private val isOnTestnetUseCase: IsOnTestnetUseCase + private val isOnTestnetUseCase: IsOnTestnetUseCase, + private val getAccountRekeyAdminAddress: GetAccountRekeyAdminAddress, + private val isAccountRekeyedToAnotherAccount: IsAccountRekeyedToAnotherAccount, + private val isAssetOwnedByAccount: IsAssetOwnedByAccount ) : CreateArc59ClaimTransaction { override suspend fun invoke(payload: Arc59ClaimTransactionPayload): Result> { - val isAccountRekeyed = accountDetailUseCase.isAccountRekeyed(payload.receiverAddress) - val authAddress = accountDetailUseCase.getAuthAddress(payload.receiverAddress) + val authAddress = getAccountRekeyAdminAddress(payload.receiverAddress) + val isAccountRekeyed = isAccountRekeyedToAnotherAccount(payload.receiverAddress) return transactionsRepository.getTransactionParams().map { transactionParams -> createTransactions(payload, transactionParams).map { transactionByteArray -> Arc59ClaimTransaction( @@ -46,11 +50,7 @@ class CreateArc59ClaimTransactionUseCase @Inject constructor( } } - private fun isReceiverOptedInToAsset(address: String, assetId: Long): Boolean { - return accountDetailUseCase.getCachedAccountDetail(address)?.data?.accountInformation?.hasAsset(assetId) == true - } - - private fun createTransactions( + private suspend fun createTransactions( payload: Arc59ClaimTransactionPayload, transactionParams: TransactionParams ): List { @@ -63,7 +63,7 @@ class CreateArc59ClaimTransactionUseCase @Inject constructor( appID, assetId, transactionParams.toSuggestedParams(), - isReceiverOptedInToAsset(payload.receiverAddress, assetId), + isAssetOwnedByAccount(payload.receiverAddress, assetId), payload.isClaimingAlgo ) } diff --git a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/detail/receivedetail/domain/usecase/CreateArc59RejectTransactionUseCase.kt b/app/src/main/kotlin/com/algorand/android/modules/assetinbox/detail/receivedetail/domain/usecase/CreateArc59RejectTransactionUseCase.kt index abe9e414..2a9478e5 100644 --- a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/detail/receivedetail/domain/usecase/CreateArc59RejectTransactionUseCase.kt +++ b/app/src/main/kotlin/com/algorand/android/modules/assetinbox/detail/receivedetail/domain/usecase/CreateArc59RejectTransactionUseCase.kt @@ -20,20 +20,22 @@ import com.algorand.android.models.TransactionParams import com.algorand.android.modules.assetinbox.detail.receivedetail.domain.model.Arc59RejectTransactionPayload import com.algorand.android.modules.assetinbox.detail.receivedetail.domain.model.BaseArc59ClaimRejectTransaction.Arc59RejectTransaction import com.algorand.android.repository.TransactionsRepository -import com.algorand.android.usecase.AccountDetailUseCase import com.algorand.android.usecase.IsOnTestnetUseCase import com.algorand.android.utils.toSuggestedParams +import com.algorand.wallet.account.detail.domain.usecase.IsAccountRekeyedToAnotherAccount +import com.algorand.wallet.account.info.domain.usecase.GetAccountRekeyAdminAddress import javax.inject.Inject class CreateArc59RejectTransactionUseCase @Inject constructor( - private val accountDetailUseCase: AccountDetailUseCase, private val transactionsRepository: TransactionsRepository, - private val isOnTestnetUseCase: IsOnTestnetUseCase + private val isOnTestnetUseCase: IsOnTestnetUseCase, + private val isAccountRekeyedToAnotherAccount: IsAccountRekeyedToAnotherAccount, + private val getAccountRekeyAdminAddress: GetAccountRekeyAdminAddress ) : CreateArc59RejectTransaction { override suspend fun invoke(payload: Arc59RejectTransactionPayload): Result> { - val isAccountRekeyed = accountDetailUseCase.isAccountRekeyed(payload.receiverAddress) - val authAddress = accountDetailUseCase.getAuthAddress(payload.receiverAddress) + val isAccountRekeyed = isAccountRekeyedToAnotherAccount(payload.receiverAddress) + val authAddress = getAccountRekeyAdminAddress(payload.receiverAddress) return transactionsRepository.getTransactionParams().map { transactionParams -> createTransactions(payload, transactionParams).map { transactionByteArray -> Arc59RejectTransaction( diff --git a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/send/domain/usecase/CreateArc59SendTransaction.kt b/app/src/main/kotlin/com/algorand/android/modules/assetinbox/send/domain/usecase/CreateArc59SendTransaction.kt index fd8d72ce..5ca742e8 100644 --- a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/send/domain/usecase/CreateArc59SendTransaction.kt +++ b/app/src/main/kotlin/com/algorand/android/modules/assetinbox/send/domain/usecase/CreateArc59SendTransaction.kt @@ -17,7 +17,7 @@ import com.algorand.android.modules.assetinbox.send.domain.model.Arc59SendTransa import com.algorand.android.modules.assetinbox.send.domain.model.Arc59TransactionPayload interface CreateArc59SendTransaction { - operator fun invoke( + suspend operator fun invoke( txnParams: TransactionParams, payload: Arc59TransactionPayload ): List? diff --git a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/send/domain/usecase/CreateArc59SendTransactionUseCase.kt b/app/src/main/kotlin/com/algorand/android/modules/assetinbox/send/domain/usecase/CreateArc59SendTransactionUseCase.kt index c2d9b073..6d6f236c 100644 --- a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/send/domain/usecase/CreateArc59SendTransactionUseCase.kt +++ b/app/src/main/kotlin/com/algorand/android/modules/assetinbox/send/domain/usecase/CreateArc59SendTransactionUseCase.kt @@ -17,20 +17,25 @@ import com.algorand.android.BuildConfig import com.algorand.android.models.TransactionParams import com.algorand.android.modules.assetinbox.send.domain.model.Arc59SendTransaction import com.algorand.android.modules.assetinbox.send.domain.model.Arc59TransactionPayload -import com.algorand.android.usecase.AccountDetailUseCase import com.algorand.android.usecase.IsOnTestnetUseCase import com.algorand.android.utils.toSuggestedParams import com.algorand.android.utils.toUint64 +import com.algorand.wallet.account.detail.domain.usecase.IsAccountRekeyedToAnotherAccount +import com.algorand.wallet.account.info.domain.usecase.GetAccountRekeyAdminAddress import javax.inject.Inject class CreateArc59SendTransactionUseCase @Inject constructor( - private val accountDetailUseCase: AccountDetailUseCase, - private val isOnTestnetUseCase: IsOnTestnetUseCase + private val isOnTestnetUseCase: IsOnTestnetUseCase, + private val isAccountRekeyedToAnotherAccount: IsAccountRekeyedToAnotherAccount, + private val getAccountRekeyAdminAddress: GetAccountRekeyAdminAddress ) : CreateArc59SendTransaction { - override fun invoke(txnParams: TransactionParams, payload: Arc59TransactionPayload): List { - val senderAuthAddress = accountDetailUseCase.getAuthAddress(payload.senderAddress) - val isSenderRekeyedToAnotherAccount = accountDetailUseCase.isAccountRekeyed(payload.senderAddress) + override suspend fun invoke( + txnParams: TransactionParams, + payload: Arc59TransactionPayload + ): List { + val senderAuthAddress = getAccountRekeyAdminAddress(payload.senderAddress) + val isSenderRekeyedToAnotherAccount = isAccountRekeyedToAnotherAccount(payload.senderAddress) val transactions = txnParams.createTransactions(payload) return transactions.map { transactionByteArray -> Arc59SendTransaction( diff --git a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/send/domain/usecase/CreateArc59TransactionsUseCase.kt b/app/src/main/kotlin/com/algorand/android/modules/assetinbox/send/domain/usecase/CreateArc59TransactionsUseCase.kt index 1e19a815..c4a2c444 100644 --- a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/send/domain/usecase/CreateArc59TransactionsUseCase.kt +++ b/app/src/main/kotlin/com/algorand/android/modules/assetinbox/send/domain/usecase/CreateArc59TransactionsUseCase.kt @@ -38,7 +38,7 @@ class CreateArc59TransactionsUseCase @Inject constructor( ) } - private fun TransactionParams.createArc59Transactions( + private suspend fun TransactionParams.createArc59Transactions( payload: Arc59TransactionPayload ): Result> { val sendTransactions = createArc59SendTransaction(this, payload) ?: emptyList() diff --git a/app/src/main/kotlin/com/algorand/android/modules/keyreg/domain/usecase/CreateKeyRegTransactionUseCase.kt b/app/src/main/kotlin/com/algorand/android/modules/keyreg/domain/usecase/CreateKeyRegTransactionUseCase.kt index 1c8dda8d..36bc74d9 100644 --- a/app/src/main/kotlin/com/algorand/android/modules/keyreg/domain/usecase/CreateKeyRegTransactionUseCase.kt +++ b/app/src/main/kotlin/com/algorand/android/modules/keyreg/domain/usecase/CreateKeyRegTransactionUseCase.kt @@ -17,7 +17,6 @@ import com.algorand.android.models.Result.Error import com.algorand.android.models.Result.Success import com.algorand.android.models.TransactionParams import com.algorand.android.modules.accounts.domain.usecase.GetAuthAddressOfAnAccount -import com.algorand.android.modules.accounts.domain.usecase.IsSenderRekeyedToAnotherAccount import com.algorand.android.modules.algosdk.domain.model.OfflineKeyRegTransactionPayload import com.algorand.android.modules.algosdk.domain.model.OnlineKeyRegTransactionPayload import com.algorand.android.modules.algosdk.domain.usecase.BuildKeyRegOfflineTransaction @@ -25,6 +24,7 @@ import com.algorand.android.modules.algosdk.domain.usecase.BuildKeyRegOnlineTran import com.algorand.android.modules.keyreg.domain.model.KeyRegTransaction import com.algorand.android.modules.keyreg.ui.model.KeyRegTransactionDetail import com.algorand.android.modules.transaction.domain.GetTransactionParams +import com.algorand.wallet.account.detail.domain.usecase.IsAccountRekeyedToAnotherAccount import javax.inject.Inject fun interface CreateKeyRegTransaction { @@ -32,7 +32,7 @@ fun interface CreateKeyRegTransaction { } internal class CreateKeyRegTransactionUseCase @Inject constructor( - private val isSenderRekeyedToAnotherAccount: IsSenderRekeyedToAnotherAccount, + private val isAccountRekeyedToAnotherAccount: IsAccountRekeyedToAnotherAccount, private val getAuthAddressOfAnAccount: GetAuthAddressOfAnAccount, private val getTransactionParams: GetTransactionParams, private val buildKeyRegOfflineTransaction: BuildKeyRegOfflineTransaction, @@ -76,7 +76,7 @@ internal class CreateKeyRegTransactionUseCase @Inject constructor( } } - private fun createKeyRegTransactionResult( + private suspend fun createKeyRegTransactionResult( txnDetail: KeyRegTransactionDetail, txnByteArray: ByteArray ): KeyRegTransaction { @@ -84,7 +84,7 @@ internal class CreateKeyRegTransactionUseCase @Inject constructor( transactionByteArray = txnByteArray, accountAddress = txnDetail.address, accountAuthAddress = getAuthAddressOfAnAccount(txnDetail.address), - isRekeyedToAnotherAccount = isSenderRekeyedToAnotherAccount(txnDetail.address) + isRekeyedToAnotherAccount = isAccountRekeyedToAnotherAccount(txnDetail.address) ) } diff --git a/app/src/main/kotlin/com/algorand/android/modules/swap/confirmswap/domain/usecase/CreateSwapQuoteTransactionsUseCase.kt b/app/src/main/kotlin/com/algorand/android/modules/swap/confirmswap/domain/usecase/CreateSwapQuoteTransactionsUseCase.kt index c49ec8bc..26b0043c 100644 --- a/app/src/main/kotlin/com/algorand/android/modules/swap/confirmswap/domain/usecase/CreateSwapQuoteTransactionsUseCase.kt +++ b/app/src/main/kotlin/com/algorand/android/modules/swap/confirmswap/domain/usecase/CreateSwapQuoteTransactionsUseCase.kt @@ -24,7 +24,7 @@ import com.algorand.android.modules.swap.confirmswap.domain.model.UnsignedSwapSi import com.algorand.android.usecase.NetworkSlugUseCase import com.algorand.android.utils.DataResource import com.algorand.android.utils.decodeBase64 -import com.algorand.wallet.account.info.domain.usecase.GetAccountRekeyAuthAddress +import com.algorand.wallet.account.info.domain.usecase.GetAccountRekeyAdminAddress import javax.inject.Inject import javax.inject.Named import kotlinx.coroutines.flow.collectLatest @@ -37,7 +37,7 @@ class CreateSwapQuoteTransactionsUseCase @Inject constructor( private val swapTransactionItemFactory: SwapTransactionItemFactory, private val networkSlugUseCase: NetworkSlugUseCase, private val parseTransactionMsgPackUseCase: ParseTransactionMsgPackUseCase, - private val getAccountRekeyAuthAddress: GetAccountRekeyAuthAddress + private val getAccountRekeyAdminAddress: GetAccountRekeyAdminAddress ) { suspend fun createQuoteTransactions( @@ -111,7 +111,7 @@ class CreateSwapQuoteTransactionsUseCase @Inject constructor( transactionListIndex = index, transactionMsgPack = unsignedTransaction, accountAddress = accountAddress, - accountAuthAddress = getAccountRekeyAuthAddress(accountAddress), + accountAuthAddress = getAccountRekeyAdminAddress(accountAddress), rawTransaction = unsignedTransaction?.decodeBase64() ?.run { parseTransactionMsgPackUseCase.parse(this) } ) diff --git a/app/src/main/kotlin/com/algorand/android/usecase/AccountDetailUseCase.kt b/app/src/main/kotlin/com/algorand/android/usecase/AccountDetailUseCase.kt index 9d3439ec..9b31b5fd 100644 --- a/app/src/main/kotlin/com/algorand/android/usecase/AccountDetailUseCase.kt +++ b/app/src/main/kotlin/com/algorand/android/usecase/AccountDetailUseCase.kt @@ -21,7 +21,6 @@ import com.algorand.android.models.AccountDetail import com.algorand.android.repository.AccountRepository import com.algorand.android.utils.CacheResult import com.algorand.android.utils.exceptions.AccountNotFoundException -import com.algorand.android.utils.isRekeyedToAnotherAccount import com.algorand.android.utils.recordException import com.algorand.android.utils.toShortenedAddress import java.math.BigInteger @@ -46,7 +45,7 @@ class AccountDetailUseCase @Inject constructor( .distinctUntilChanged() } - fun getCachedAccountDetails() = getAccountDetailCacheFlow().value.values + private fun getCachedAccountDetails() = getAccountDetailCacheFlow().value.values fun getCachedAccountDetail(publicKey: String): CacheResult? { return accountRepository.getCachedAccountDetail(publicKey) @@ -101,7 +100,7 @@ class AccountDetailUseCase @Inject constructor( } } - fun isAuthAccountInDevice(accountAddress: String): Boolean { + private fun isAuthAccountInDevice(accountAddress: String): Boolean { val accountAuthAddress = getAuthAddress(accountAddress) ?: return false val authAccountDetail = getCachedAccountDetail(accountAuthAddress)?.data ?: return false return canAccountSignTransaction(authAccountDetail.account.address) @@ -127,14 +126,6 @@ class AccountDetailUseCase @Inject constructor( return accountInformation?.rekeyAdminAddress } - fun isAccountRekeyed(publicKey: String): Boolean { - val authAddress = accountRepository.getCachedAccountDetail(publicKey) - ?.data - ?.accountInformation - ?.rekeyAdminAddress - return isRekeyedToAnotherAccount(authAddress, publicKey) - } - fun isThereAnyAccountWithPublicKey(publicKey: String): Boolean { return accountManager.isThereAnyAccountWithPublicKey(publicKey) } diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/account/detail/di/AccountDetailModule.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/account/detail/di/AccountDetailModule.kt index d8208f28..75dfc6bb 100644 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/account/detail/di/AccountDetailModule.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/account/detail/di/AccountDetailModule.kt @@ -24,6 +24,8 @@ import com.algorand.wallet.account.detail.domain.usecase.GetAccountsDetails import com.algorand.wallet.account.detail.domain.usecase.GetAccountsDetailsUseCase import com.algorand.wallet.account.detail.domain.usecase.GetLocalRekeyedAccountCount import com.algorand.wallet.account.detail.domain.usecase.GetLocalRekeyedAccountCountUseCase +import com.algorand.wallet.account.detail.domain.usecase.IsAccountRekeyedToAnotherAccount +import com.algorand.wallet.account.detail.domain.usecase.IsAccountRekeyedToAnotherAccountUseCase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -51,5 +53,12 @@ internal object AccountDetailModule { fun provideGetAccountDetail(useCase: GetAccountDetailUseCase): GetAccountDetail = useCase @Provides - fun provideGetRekeyedAccountCount(useCase: GetLocalRekeyedAccountCountUseCase): GetLocalRekeyedAccountCount = useCase + fun provideGetRekeyedAccountCount( + useCase: GetLocalRekeyedAccountCountUseCase + ): GetLocalRekeyedAccountCount = useCase + + @Provides + fun provideIsAccountRekeyedToAnotherAccount( + useCase: IsAccountRekeyedToAnotherAccountUseCase + ): IsAccountRekeyedToAnotherAccount = useCase } \ No newline at end of file diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/account/detail/domain/usecase/AccountDetailUseCases.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/account/detail/domain/usecase/AccountDetailUseCases.kt index a2947c30..3bf68113 100644 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/account/detail/domain/usecase/AccountDetailUseCases.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/account/detail/domain/usecase/AccountDetailUseCases.kt @@ -40,3 +40,7 @@ fun interface GetAccountsDetails { fun interface GetLocalRekeyedAccountCount { suspend operator fun invoke(authAddress: String): Int } + +fun interface IsAccountRekeyedToAnotherAccount { + suspend operator fun invoke(address: String): Boolean +} diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/account/detail/domain/usecase/IsAccountRekeyedToAnotherAccountUseCase.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/account/detail/domain/usecase/IsAccountRekeyedToAnotherAccountUseCase.kt new file mode 100644 index 00000000..c9371efb --- /dev/null +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/account/detail/domain/usecase/IsAccountRekeyedToAnotherAccountUseCase.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.wallet.account.detail.domain.usecase + +import com.algorand.wallet.account.info.domain.usecase.GetAccountRekeyAdminAddress +import javax.inject.Inject + +internal class IsAccountRekeyedToAnotherAccountUseCase @Inject constructor( + private val getAccountRekeyAdminAddress: GetAccountRekeyAdminAddress +) : IsAccountRekeyedToAnotherAccount { + + override suspend fun invoke(address: String): Boolean { + val adminAddress = getAccountRekeyAdminAddress(address) + return !adminAddress.isNullOrBlank() && adminAddress != address + } +} diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/account/info/di/AccountInformationModule.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/account/info/di/AccountInformationModule.kt index a6339a46..02944ddb 100644 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/account/info/di/AccountInformationModule.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/account/info/di/AccountInformationModule.kt @@ -54,7 +54,7 @@ import com.algorand.wallet.account.info.domain.usecase.GetAccountDetailCacheStat import com.algorand.wallet.account.info.domain.usecase.GetAccountDetailCacheStatusFlowUseCase import com.algorand.wallet.account.info.domain.usecase.GetAccountInformation import com.algorand.wallet.account.info.domain.usecase.GetAccountInformationFlow -import com.algorand.wallet.account.info.domain.usecase.GetAccountRekeyAuthAddress +import com.algorand.wallet.account.info.domain.usecase.GetAccountRekeyAdminAddress import com.algorand.wallet.account.info.domain.usecase.GetAllAccountInformationFlow import com.algorand.wallet.account.info.domain.usecase.GetAllAssetHoldingIds import com.algorand.wallet.account.info.domain.usecase.GetAllFailedCachedAccountAddresses @@ -284,9 +284,9 @@ internal object AccountInformationModule { } @Provides - fun provideGetAccountRekeyAuthAddress( + fun provideGetAccountRekeyAdminAddress( repository: AccountInformationRepository - ): GetAccountRekeyAuthAddress = GetAccountRekeyAuthAddress(repository::getRekeyAuthAddress) + ): GetAccountRekeyAdminAddress = GetAccountRekeyAdminAddress(repository::getRekeyAuthAddress) @Provides fun provideGetAccountAssetHoldingsFlow( diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/account/info/domain/usecase/AccountInformationUseCases.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/account/info/domain/usecase/AccountInformationUseCases.kt index b06be6e2..58550e69 100644 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/account/info/domain/usecase/AccountInformationUseCases.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/account/info/domain/usecase/AccountInformationUseCases.kt @@ -103,6 +103,6 @@ fun interface IsAccountCachedSuccessfully { suspend operator fun invoke(address: String): Boolean } -fun interface GetAccountRekeyAuthAddress { +fun interface GetAccountRekeyAdminAddress { suspend operator fun invoke(address: String): String? } diff --git a/common-sdk/src/test/kotlin/com/algorand/wallet/account/detail/domain/usecase/IsAccountRekeyedToAnotherAccountUseCaseTest.kt b/common-sdk/src/test/kotlin/com/algorand/wallet/account/detail/domain/usecase/IsAccountRekeyedToAnotherAccountUseCaseTest.kt new file mode 100644 index 00000000..c731a72a --- /dev/null +++ b/common-sdk/src/test/kotlin/com/algorand/wallet/account/detail/domain/usecase/IsAccountRekeyedToAnotherAccountUseCaseTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2022 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.wallet.account.detail.domain.usecase + +import com.algorand.wallet.account.info.domain.usecase.GetAccountRekeyAdminAddress +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class IsAccountRekeyedToAnotherAccountUseCaseTest { + + private val getAccountRekeyAdminAddress: GetAccountRekeyAdminAddress = mockk() + private val sut = IsAccountRekeyedToAnotherAccountUseCase(getAccountRekeyAdminAddress) + + @Test + fun `EXPECT false WHEN admin address is null`() = runTest { + val address = "some-address" + coEvery { getAccountRekeyAdminAddress(address) } returns null + + val result = sut.invoke(address) + + assertFalse(result) + } + + @Test + fun `EXPECT false WHEN admin address is blank`() = runTest { + val address = "some-address" + coEvery { getAccountRekeyAdminAddress(address) } returns "" + + val result = sut.invoke(address) + + assertFalse(result) + } + + @Test + fun `EXPECT false WHEN admin address is the same as the given address`() = runTest { + val address = "some-address" + coEvery { getAccountRekeyAdminAddress(address) } returns address + + val result = sut.invoke(address) + + assertFalse(result) + } + + @Test + fun `EXPECT true WHEN admin address is different`() = runTest { + val address = "some-address" + val adminAddress = "other-address" + coEvery { getAccountRekeyAdminAddress(address) } returns adminAddress + + val result = sut.invoke(address) + + assertTrue(result) + } +} \ No newline at end of file