diff --git a/.github/workflows/android_verification.yml b/.github/workflows/android_verification.yml new file mode 100644 index 000000000..f88eed6ba --- /dev/null +++ b/.github/workflows/android_verification.yml @@ -0,0 +1,69 @@ +name: Android Verification + +on: + pull_request: + branches: [ "*" ] + push: + branches: [ "*" ] + workflow_dispatch: + +concurrency: + group: android-verification-${{ github.ref }} + cancel-in-progress: true + +jobs: + explorer-contracts: + name: Explorer Contracts + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + RUN_LIVE_EXPLORER_TESTS: "true" + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Set up Android SDK + uses: android-actions/setup-android@v3 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Run live explorer contract tests + run: ./gradlew :feature_blockexplorer_api:testDevelopDebugUnitTest --tests '*SoraMetricsLiveContractTest' --stacktrace --no-daemon + + blockexplorer-connected: + name: Block Explorer Connected Test + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Set up Android SDK + uses: android-actions/setup-android@v3 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Run connected block explorer test + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 36 + target: google_apis + arch: x86_64 + profile: pixel_6 + disable-animations: true + emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none + script: ./gradlew :feature_blockexplorer_api:connectedDevelopDebugAndroidTest --stacktrace --no-daemon diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 34fa614dc..c24911abc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,4 +1,6 @@ import com.github.triplet.gradle.androidpublisher.ReleaseStatus +import com.google.firebase.appdistribution.gradle.firebaseAppDistribution +import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension plugins { id("maven-publish") @@ -6,8 +8,7 @@ plugins { alias(libs.plugins.kotlinAndroid) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) - alias(libs.plugins.googleServicesPlugin) + alias(libs.plugins.ksp) alias(libs.plugins.firebaseCrashlyticsPlugin) alias(libs.plugins.firebaseAppDistributionPlugin) alias(libs.plugins.triplet) @@ -15,23 +16,37 @@ plugins { alias(libs.plugins.kover) } +val googleServicesJsonFiles = fileTree(projectDir) { + include("google-services.json") + include("src/**/google-services.json") +} +val hasGoogleServicesJson = !googleServicesJsonFiles.isEmpty + +if (hasGoogleServicesJson) { + apply(plugin = "com.google.gms.google-services") +} else { + logger.lifecycle("Skipping Google Services plugin because google-services.json is not present.") +} + kotlin { jvmToolchain(17) } // soralution 143 3.8.6.3 2024.10.31 -// sora dae 118 3.8.5.3 2024.10.23 +// sora dae 122 3.8.6.3 2024.11.21 +val appVersionCode = System.getenv("CI_BUILD_ID")?.toInt() ?: 122 +val appVersionName = "3.8.6.3" android { namespace = "jp.co.soramitsu.sora" - compileSdk = 34 + compileSdk = 36 defaultConfig { applicationId = "jp.co.soramitsu.sora" minSdk = 26 - targetSdk = 34 - versionCode = System.getenv("CI_BUILD_ID")?.toInt() ?: 143 - versionName = "3.8.6.3" + targetSdk = 36 + versionCode = appVersionCode + versionName = appVersionName testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled = true // resConfigs "en", "ru", "es", "fr", "de", "nb", "in", "tr", "ar" @@ -55,6 +70,9 @@ android { isMinifyEnabled = true signingConfig = signingConfigs.getByName("cidebug") isShrinkResources = true + configure { + mappingFileUploadEnabled = hasGoogleServicesJson + } proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" @@ -92,6 +110,7 @@ android { buildFeatures { viewBinding = true buildConfig = true + resValues = true } packaging { resources { @@ -143,15 +162,6 @@ android { } } - applicationVariants.all { - val variant = this - this.outputs.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl } - .forEach { - it.outputFileName = - "SORA_Wallet_${variant.versionName}_${variant.versionCode}_${variant.flavorName}_${variant.buildType.name}.apk" - } - } - configurations { all { exclude(module = "bcprov-jdk15on") @@ -164,8 +174,18 @@ android { } } +androidComponents { + onVariants { variant -> + variant.outputs.forEach { output -> + output.outputFileName.set( + "SORA_Wallet_${appVersionName}_${appVersionCode}_${variant.flavorName}_${variant.buildType}.apk" + ) + } + } +} + hilt { - enableAggregatingTask = true + enableAggregatingTask = false } play { @@ -179,6 +199,7 @@ play { dependencies { // implementation(libs.fileTree(dir: 'libs', include: ['*.jar']) implementation(project(":common")) + implementation(project(":common_wallet")) implementation(project(":core_db")) implementation(project(":demeter")) implementation(project(":feature_assets_api")) @@ -224,12 +245,11 @@ dependencies { implementation(libs.soramitsu.android.foundation) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) implementation(libs.hiltWorkManagerDep) - kapt(libs.hiltWorkManagerKaptDep) + ksp(libs.hiltWorkManagerCompilerDep) implementation(libs.lifecycleProcessDep) - kapt(libs.lifecycleKaptDep) implementation(libs.coroutineAndroidDep) implementation(libs.coroutineDep) @@ -277,11 +297,6 @@ dependencies { kover(project(":sorasubstrate")) kover(project(":network")) } - -kapt { - correctErrorTypes = true -} - kover { reports { variant("developDebug") { diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index fe8938d88..bb091f21e 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -214,3 +214,42 @@ -keep class com.google.api.services.drive.** { *; } + +# Optional Sora Card SDK integrations are not packaged by this app. +-dontwarn com.paywings.kyc.android.sdk.data.enums.KycErrorCode +-dontwarn com.paywings.kyc.android.sdk.data.model.PayWingsUserCredentials +-dontwarn com.paywings.kyc.android.sdk.data.model.PayWingsWhiteLabelCredentials +-dontwarn com.paywings.kyc.android.sdk.initializer.PayWingsKycClient +-dontwarn com.paywings.oauth.android.sdk.data.enums.EnvironmentType +-dontwarn com.paywings.oauth.android.sdk.data.enums.HttpRequestMethod$Companion +-dontwarn com.paywings.oauth.android.sdk.data.enums.HttpRequestMethod +-dontwarn com.paywings.oauth.android.sdk.data.enums.OAuthErrorCode +-dontwarn com.paywings.oauth.android.sdk.data.model.AccessTokenData +-dontwarn com.paywings.oauth.android.sdk.data.model.ErrorData +-dontwarn com.paywings.oauth.android.sdk.data.model.GetNewAuthorizationDataResult +-dontwarn com.paywings.oauth.android.sdk.initializer.OAuthInitializationCallback +-dontwarn com.paywings.oauth.android.sdk.initializer.PayWingsOAuthClient$Companion +-dontwarn com.paywings.oauth.android.sdk.initializer.PayWingsOAuthClient +-dontwarn com.paywings.oauth.android.sdk.service.OAuthService +-dontwarn com.paywings.oauth.android.sdk.service.callback.ChangeUnverifiedEmailCallback +-dontwarn com.paywings.oauth.android.sdk.service.callback.CheckEmailVerifiedCallback +-dontwarn com.paywings.oauth.android.sdk.service.callback.GetUserDataCallback +-dontwarn com.paywings.oauth.android.sdk.service.callback.RegisterUserCallback +-dontwarn com.paywings.oauth.android.sdk.service.callback.SendNewVerificationEmailCallback +-dontwarn com.paywings.oauth.android.sdk.service.callback.SignInWithPhoneNumberRequestOtpCallback +-dontwarn com.paywings.oauth.android.sdk.service.callback.SignInWithPhoneNumberVerifyOtpCallback + +# Optional JNDI/LDAP APIs referenced by crypto and HTTP libraries are not +# available on Android. +-dontwarn javax.naming.Binding +-dontwarn javax.naming.InvalidNameException +-dontwarn javax.naming.NamingEnumeration +-dontwarn javax.naming.NamingException +-dontwarn javax.naming.directory.Attribute +-dontwarn javax.naming.directory.Attributes +-dontwarn javax.naming.directory.DirContext +-dontwarn javax.naming.directory.InitialDirContext +-dontwarn javax.naming.directory.SearchControls +-dontwarn javax.naming.directory.SearchResult +-dontwarn javax.naming.ldap.LdapName +-dontwarn javax.naming.ldap.Rdn diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7877ff701..94ebf7710 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,8 +14,8 @@ - - + + ("ktlintCheck") { classpath = ktlint mainClass.set("com.pinterest.ktlint.Main") // see https://pinterest.github.io/ktlint/install/cli/#command-line-usage for more information - args( - "**/src/**/*.kt", - "**.kts", - "!**/build/**", - "--reporter=checkstyle,output=${project.buildDir}/reports/checkstyle/ktlint.xml" - ) + val reportFile = layout.buildDirectory.file("reports/checkstyle/ktlint.xml") + val sourceFiles = fileTree(rootDir) { + include("**/src/**/*.kt") + include("**/*.gradle.kts") + include("settings.gradle.kts") + exclude("**/build/**") + exclude(".gradle/**") + } + args(sourceFiles.files.map { it.relativeTo(rootDir).path }) + args("--reporter=checkstyle,output=${reportFile.get().asFile}") } tasks.register("ktlintFormat") { diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 3c71fa339..7f03e6643 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,41 +1,39 @@ import java.io.FileInputStream import java.util.Properties -import org.gradle.kotlin.dsl.kapt plugins { id("maven-publish") alias(libs.plugins.androidLibrary) alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.composeCompiler) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } fun secret(name: String): String { val fileProperties = File(rootProject.projectDir.absolutePath, "local.properties") - val pr = runCatching { FileInputStream(fileProperties) }.getOrNull()?.let { file -> + val pr = runCatching { FileInputStream(fileProperties) }.getOrNull()?.use { file -> Properties().apply { load(file) } } - return pr?.getProperty(name) ?: System.getenv(name)!! + return pr?.getProperty(name) ?: System.getenv(name).orEmpty() } fun maybeWrapQuotes(s: String): String { return if (s.startsWith("\"")) s else "\"" + s + "\"" } -val composeCompilerVersion: String by project - kotlin { jvmToolchain(17) } android { namespace = "jp.co.soramitsu.common" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -45,7 +43,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -57,11 +55,6 @@ android { ) } } - - composeOptions { - kotlinCompilerExtensionVersion = composeCompilerVersion - } - buildFeatures { viewBinding = true compose = true @@ -151,7 +144,7 @@ android { } dependencies { - implementation(project(":network")) + api(project(":network")) implementation(libs.activityKtxDep) implementation(libs.coreKtxDep) @@ -164,38 +157,37 @@ dependencies { implementation(libs.coroutineDep) implementation(libs.uiCoreDep) - implementation(libs.soramitsu.android.foundation) + api(libs.soramitsu.android.foundation) - implementation(libs.kotlinxSerializationJsonDep) + api(libs.kotlinxSerializationJsonDep) - implementation(libs.coilSvgDep) + api(libs.coilSvgDep) implementation(libs.webSocketLibDep) implementation(libs.uiCoreDep) implementation(libs.lifecycleProcessDep) - kapt(libs.lifecycleKaptDep) implementation(libs.timberDep) implementation(libs.svgDep) implementation(libs.jdenticonDep) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) implementation(libs.datastoreDep) implementation(libs.navigationFragmentDep) implementation(libs.navigationUiDep) - implementation(libs.xbackupDep) - implementation(libs.xsubstrateDep) + api(libs.xbackupDep) + api(libs.xsubstrateDep) implementation(libs.xcryptoDep) implementation(libs.ed25519Dep) { // exclude(module = "bcpkix-jdk15on") } implementation(libs.xercesDep) - implementation(libs.gsonDep) + api(libs.gsonDep) implementation(libs.zXingCoreDep) implementation(platform(libs.googleFirebaseBomDep)) @@ -220,6 +212,7 @@ dependencies { implementation(platform(libs.compose.bom)) implementation(libs.composeUiDep) implementation(libs.composeLiveDataDep) + implementation(libs.composeLifecycleDep) implementation(libs.composeFoundationDep) implementation(libs.composeMaterialDep) implementation(libs.composeConstraintLayoutDep) @@ -232,7 +225,3 @@ dependencies { testImplementation(libs.archCoreTestDep) testImplementation(libs.coroutineTestDep) } - -kapt { - correctErrorTypes = true -} diff --git a/common/src/main/AndroidManifest.xml b/common/src/main/AndroidManifest.xml index 668f10c71..c806dbccb 100644 --- a/common/src/main/AndroidManifest.xml +++ b/common/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ + - \ No newline at end of file + diff --git a/common/src/main/java/jp/co/soramitsu/common/di/modules/CommonModule.kt b/common/src/main/java/jp/co/soramitsu/common/di/modules/CommonModule.kt index 0eeecbf11..8a85b7e4a 100644 --- a/common/src/main/java/jp/co/soramitsu/common/di/modules/CommonModule.kt +++ b/common/src/main/java/jp/co/soramitsu/common/di/modules/CommonModule.kt @@ -65,7 +65,6 @@ import jp.co.soramitsu.common.date.DateTimeFormatter import jp.co.soramitsu.common.delegate.WithProgressImpl import jp.co.soramitsu.common.domain.AppStateProvider import jp.co.soramitsu.common.domain.InvitationHandler -import jp.co.soramitsu.common.domain.OptionsProvider import jp.co.soramitsu.common.domain.PushHandler import jp.co.soramitsu.common.inappupdate.InAppUpdateManager import jp.co.soramitsu.common.interfaces.WithProgress @@ -83,12 +82,6 @@ import jp.co.soramitsu.common.util.json_decoder.JsonAccountsEncoder import jp.co.soramitsu.common.vibration.DeviceVibrator import jp.co.soramitsu.crypto.ed25519.Ed25519Sha3 import jp.co.soramitsu.xbackup.BackupService -import jp.co.soramitsu.xnetworking.lib.datasources.blockexplorer.api.BlockExplorerRepository -import jp.co.soramitsu.xnetworking.lib.datasources.blockexplorer.impl.BlockExplorerRepositoryImpl -import jp.co.soramitsu.xnetworking.lib.datasources.chainsconfig.api.ConfigDAO -import jp.co.soramitsu.xnetworking.lib.datasources.chainsconfig.api.data.ConfigParser -import jp.co.soramitsu.xnetworking.lib.datasources.chainsconfig.impl.SuperWalletConfigDAOImpl -import jp.co.soramitsu.xnetworking.lib.datasources.chainsconfig.impl.data.RemoteConfigParserImpl import jp.co.soramitsu.xnetworking.lib.engines.rest.api.RestClient import jp.co.soramitsu.xnetworking.lib.engines.rest.api.models.AbstractRestClientConfig import jp.co.soramitsu.xnetworking.lib.engines.rest.impl.RestClientImpl @@ -142,30 +135,6 @@ class CommonModule { ignoreUnknownKeys = true } - @Singleton - @Provides - fun provideConfigParser( - restClient: RestClient - ): ConfigParser = RemoteConfigParserImpl( - restClient = restClient, - chainsRequestUrl = OptionsProvider.configXn, - ) - - @Singleton - @Provides - fun provideConfigDAO(configParser: ConfigParser): ConfigDAO = - SuperWalletConfigDAOImpl(configParser = configParser) - - @Singleton - @Provides - fun provideBlockExplorerRepository( - configDAO: ConfigDAO, - restClient: RestClient - ): BlockExplorerRepository = BlockExplorerRepositoryImpl( - configDAO = configDAO, - restClient = restClient - ) - @Singleton @Provides fun provideRestClient(json: Json): RestClient = RestClientImpl( @@ -305,7 +274,7 @@ class CommonModule { @Provides @Singleton fun provideDeviceVibrator(@ApplicationContext context: Context): DeviceVibrator { - val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + val vibrator = context.getSystemService(Vibrator::class.java) return DeviceVibrator(vibrator) } diff --git a/common/src/main/java/jp/co/soramitsu/common/domain/OptionsProvider.kt b/common/src/main/java/jp/co/soramitsu/common/domain/OptionsProvider.kt index 11ec9588a..8b181c942 100644 --- a/common/src/main/java/jp/co/soramitsu/common/domain/OptionsProvider.kt +++ b/common/src/main/java/jp/co/soramitsu/common/domain/OptionsProvider.kt @@ -40,7 +40,8 @@ object OptionsProvider { var APPLICATION_ID: String = "" const val configCommon = "https://config.polkaswap2.io/${FlavorOptionsProvider.typesFilePath}/common.json" const val configMobile = "https://config.polkaswap2.io/${FlavorOptionsProvider.typesFilePath}/mobile.json" - const val configXn = "https://config.polkaswap2.io/${FlavorOptionsProvider.typesFilePath}/xn.json" + const val polkaswapIndexerEndpoint = "https://pi.soramitsu.io/graphql" + const val blockExplorerUrl = "https://sorametrics.org" val fileProviderAuthority: String get() = "$APPLICATION_ID.soraFileProvider" val header: String by lazy { "$APPLICATION_ID/$CURRENT_VERSION_NAME/$CURRENT_VERSION_CODE/${BuildConfig.BUILD_TYPE}/${BuildConfig.FLAVOR}" @@ -53,8 +54,8 @@ object OptionsProvider { const val nbspace = Typography.nbsp const val euroSign = '€' const val soracard = "2.2.4" - const val soracardFiatPackageTest = "com.soracard.iban.wallet.test" - const val soracardFiatPackageProd = "com.soracard.iban.wallet" + const val soracardFiatPackageTest = "com.soracard.wallet.iban.test" + const val soracardFiatPackageProd = "com.soracard.wallet.iban" const val website = "https://sora.org" const val sourceLink = "https://github.com/sora-xor/Sora-Android" diff --git a/common/src/main/java/jp/co/soramitsu/common/logger/FirebaseWrapper.kt b/common/src/main/java/jp/co/soramitsu/common/logger/FirebaseWrapper.kt index 3c3031d4e..0ccf7b738 100644 --- a/common/src/main/java/jp/co/soramitsu/common/logger/FirebaseWrapper.kt +++ b/common/src/main/java/jp/co/soramitsu/common/logger/FirebaseWrapper.kt @@ -39,17 +39,34 @@ import timber.log.Timber object FirebaseWrapper { private val blackList = listOf(CancellationException::class) + @Volatile + private var crashlyticsEnabled = false + + fun setCrashlyticsEnabled(enabled: Boolean) { + crashlyticsEnabled = enabled + } + fun recordException(t: Throwable) { - blackList.forEach { kClass -> - if (!kClass.isInstance(t)) { - Timber.e(t, "ERROR") - FirebaseCrashlytics.getInstance().recordException(t) - } + if (blackList.any { it.isInstance(t) }) { + return } + + Timber.e(t, "ERROR") + crashlyticsInstance()?.recordException(t) } fun log(message: String) { Timber.d(message) - FirebaseCrashlytics.getInstance().log(message) + crashlyticsInstance()?.log(message) + } + + private fun crashlyticsInstance(): FirebaseCrashlytics? { + if (!crashlyticsEnabled) { + return null + } + + return runCatching { FirebaseCrashlytics.getInstance() } + .onFailure { crashlyticsEnabled = false } + .getOrNull() } } diff --git a/common/src/main/java/jp/co/soramitsu/common/presentation/args/BundleExt.kt b/common/src/main/java/jp/co/soramitsu/common/presentation/args/BundleExt.kt index b5ccfd47f..f0d2e77e4 100644 --- a/common/src/main/java/jp/co/soramitsu/common/presentation/args/BundleExt.kt +++ b/common/src/main/java/jp/co/soramitsu/common/presentation/args/BundleExt.kt @@ -34,6 +34,7 @@ package jp.co.soramitsu.common.presentation.args import android.os.Bundle import android.os.Parcelable +import androidx.core.os.BundleCompat import java.io.Serializable import jp.co.soramitsu.common.util.BuildUtils @@ -48,13 +49,8 @@ inline fun Bundle.getSerializableKey(key: String): T? } } -@Suppress("DEPRECATION") inline fun Bundle.getParcelableKey(key: String): T? { - return if (BuildUtils.sdkAtLeast(33)) { - getParcelable(key, T::class.java) - } else { - getParcelable(key) as? T - } + return BundleCompat.getParcelable(this, key, T::class.java) } fun Bundle.requireString(key: String): String = diff --git a/common/src/main/java/jp/co/soramitsu/common/presentation/compose/Blur.kt b/common/src/main/java/jp/co/soramitsu/common/presentation/compose/Blur.kt index f09176485..9fdfc7861 100644 --- a/common/src/main/java/jp/co/soramitsu/common/presentation/compose/Blur.kt +++ b/common/src/main/java/jp/co/soramitsu/common/presentation/compose/Blur.kt @@ -68,7 +68,7 @@ fun fastblur(sentBitmap: Bitmap, scale: Float, radius: Int): Bitmap? { val width = (sentBitmap.width * scale).roundToInt() val height = (sentBitmap.height * scale).roundToInt() sentBitmap = Bitmap.createScaledBitmap(sentBitmap, width, height, false) - val bitmap = sentBitmap.copy(sentBitmap.config, true) + val bitmap = sentBitmap.copy(sentBitmap.config ?: Bitmap.Config.ARGB_8888, true) if (radius < 1) { return null } diff --git a/common/src/main/java/jp/co/soramitsu/common/presentation/compose/components/KeyCompose.kt b/common/src/main/java/jp/co/soramitsu/common/presentation/compose/components/KeyCompose.kt index d8a89de15..cf0ee6ebf 100644 --- a/common/src/main/java/jp/co/soramitsu/common/presentation/compose/components/KeyCompose.kt +++ b/common/src/main/java/jp/co/soramitsu/common/presentation/compose/components/KeyCompose.kt @@ -38,13 +38,15 @@ import androidx.compose.foundation.layout.isImeVisible import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.currentStateAsState @OptIn(ExperimentalLayoutApi::class) @Composable fun keyboardState(): State { val lifecycle = LocalLifecycleOwner.current.lifecycle - val isResumed = lifecycle.currentState == Lifecycle.State.RESUMED + val lifecycleState = lifecycle.currentStateAsState() + val isResumed = lifecycleState.value == Lifecycle.State.RESUMED return rememberUpdatedState(WindowInsets.isImeVisible && isResumed) } diff --git a/common/src/main/java/jp/co/soramitsu/common/presentation/view/SoraBaseActivity.kt b/common/src/main/java/jp/co/soramitsu/common/presentation/view/SoraBaseActivity.kt index b93c4a07f..c7739a920 100644 --- a/common/src/main/java/jp/co/soramitsu/common/presentation/view/SoraBaseActivity.kt +++ b/common/src/main/java/jp/co/soramitsu/common/presentation/view/SoraBaseActivity.kt @@ -68,6 +68,8 @@ import jp.co.soramitsu.common.presentation.compose.components.Toolbar import jp.co.soramitsu.common.presentation.compose.theme.SoraAppTheme import jp.co.soramitsu.common.presentation.viewmodel.BaseViewModel import jp.co.soramitsu.common.util.ext.attrColor +import jp.co.soramitsu.common.util.ext.setNavigationBarColorCompat +import jp.co.soramitsu.common.util.ext.setStatusBarColorCompat import jp.co.soramitsu.ui_core.theme.customColors abstract class SoraBaseActivity : AppCompatActivity() { @@ -165,8 +167,9 @@ abstract class SoraBaseActivity : AppCompatActivity() { override fun onResume() { super.onResume() - window.statusBarColor = attrColor(backgroundColor()) - window.navigationBarColor = attrColor(backgroundColor()) + val background = attrColor(backgroundColor()) + window.setStatusBarColorCompat(background) + window.setNavigationBarColorCompat(background) } abstract fun onToolbarNavigation() diff --git a/common/src/main/java/jp/co/soramitsu/common/resourses/LanguagesHolder.kt b/common/src/main/java/jp/co/soramitsu/common/resourses/LanguagesHolder.kt index ab63792f4..ec0e5fd97 100644 --- a/common/src/main/java/jp/co/soramitsu/common/resourses/LanguagesHolder.kt +++ b/common/src/main/java/jp/co/soramitsu/common/resourses/LanguagesHolder.kt @@ -46,11 +46,6 @@ https://android-developers.googleblog.com/2022/11/per-app-language-preferences-p */ class LanguagesHolder { - companion object { - private const val LANGUAGE_PART_INDEX = 0 - private const val COUNTRY_PART_INDEX = 1 - } - private val russian = Language("ru", R.string.common_russian, R.string.common_russian_native) private val english = Language("en", R.string.common_english, R.string.common_english_native) private val spanish = Language("es", R.string.common_spanish, R.string.common_spanish_native) @@ -133,15 +128,6 @@ class LanguagesHolder { private fun languageTag(l: String) = l.substringBefore("-").substringBefore("_") - private fun mapLanguageToLocale(language: String): Locale { - val codes = language.split("_") - - return if (hasCountryCode(codes)) { - Locale(codes[LANGUAGE_PART_INDEX], codes[COUNTRY_PART_INDEX]) - } else { - Locale(language) - } - } - - private fun hasCountryCode(codes: List) = codes.size != 1 + private fun mapLanguageToLocale(language: String): Locale = + Locale.forLanguageTag(language.replace('_', '-')) } diff --git a/common/src/main/java/jp/co/soramitsu/common/util/EncryptionUtil.kt b/common/src/main/java/jp/co/soramitsu/common/util/EncryptionUtil.kt index 7652308c9..5ec351a2d 100644 --- a/common/src/main/java/jp/co/soramitsu/common/util/EncryptionUtil.kt +++ b/common/src/main/java/jp/co/soramitsu/common/util/EncryptionUtil.kt @@ -33,8 +33,6 @@ USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package jp.co.soramitsu.common.util import android.content.Context -import android.os.Build -import android.security.KeyPairGeneratorSpec import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import java.math.BigInteger @@ -47,7 +45,6 @@ import java.security.NoSuchAlgorithmException import java.security.PrivateKey import java.security.PublicKey import java.security.SecureRandom -import java.security.spec.AlgorithmParameterSpec import java.util.Calendar import javax.crypto.BadPaddingException import javax.crypto.Cipher @@ -129,29 +126,17 @@ class EncryptionUtil( val endDate = Calendar.getInstance() endDate.add(Calendar.YEAR, 25) - val spec: AlgorithmParameterSpec - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - spec = KeyPairGeneratorSpec.Builder(context) - .setAlias(KEY_ALIAS) - .setSubject(X500Principal("CN=Sora")) - .setSerialNumber(BigInteger.ONE) - .setStartDate(startDate.time) - .setEndDate(endDate.time) - .build() - } else { - spec = KeyGenParameterSpec.Builder( - KEY_ALIAS, - KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT - ) - .setBlockModes(KeyProperties.BLOCK_MODE_GCM) - .setCertificateSubject(X500Principal("CN=Sora")) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) - .setCertificateSerialNumber(BigInteger.ONE) - .setCertificateNotBefore(startDate.time) - .setCertificateNotAfter(endDate.time) - .build() - } + val spec = KeyGenParameterSpec.Builder( + KEY_ALIAS, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setCertificateSubject(X500Principal("CN=Sora")) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) + .setCertificateSerialNumber(BigInteger.ONE) + .setCertificateNotBefore(startDate.time) + .setCertificateNotAfter(endDate.time) + .build() val keyPairGenerator = KeyPairGenerator.getInstance(RSA, KEY_STORE_PROVIDER) keyPairGenerator.initialize(spec) keyPairGenerator.generateKeyPair() diff --git a/common/src/main/java/jp/co/soramitsu/common/util/QrCodeDecoder.kt b/common/src/main/java/jp/co/soramitsu/common/util/QrCodeDecoder.kt index 9160adc2a..69456126b 100644 --- a/common/src/main/java/jp/co/soramitsu/common/util/QrCodeDecoder.kt +++ b/common/src/main/java/jp/co/soramitsu/common/util/QrCodeDecoder.kt @@ -34,8 +34,10 @@ package jp.co.soramitsu.common.util import android.content.ContentResolver import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.ImageDecoder import android.net.Uri -import android.provider.MediaStore +import android.os.Build import com.google.zxing.BinaryBitmap import com.google.zxing.DecodeHintType import com.google.zxing.RGBLuminanceSource @@ -59,7 +61,7 @@ class QrCodeDecoder( private fun decode(data: Uri): String { var qrBitmap: Bitmap? = null try { - qrBitmap = MediaStore.Images.Media.getBitmap(contentResolver, data) + qrBitmap = decodeBitmap(data) val pixels = IntArray(qrBitmap.height * qrBitmap.width) qrBitmap.getPixels(pixels, 0, qrBitmap.width, 0, 0, qrBitmap.width, qrBitmap.height) val source = RGBLuminanceSource(qrBitmap.width, qrBitmap.height, pixels) @@ -78,4 +80,14 @@ class QrCodeDecoder( qrBitmap?.recycle() } } + + private fun decodeBitmap(data: Uri): Bitmap = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + ImageDecoder.decodeBitmap(ImageDecoder.createSource(contentResolver, data)) { decoder, _, _ -> + decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE + } + } else { + contentResolver.openInputStream(data)?.use(BitmapFactory::decodeStream) + ?: throw QrException.decodeError() + } } diff --git a/common/src/main/java/jp/co/soramitsu/common/util/ScreenshotBlockHelper.kt b/common/src/main/java/jp/co/soramitsu/common/util/ScreenshotBlockHelper.kt index 24c530a2f..92377b840 100644 --- a/common/src/main/java/jp/co/soramitsu/common/util/ScreenshotBlockHelper.kt +++ b/common/src/main/java/jp/co/soramitsu/common/util/ScreenshotBlockHelper.kt @@ -34,7 +34,6 @@ package jp.co.soramitsu.common.util import android.app.Activity import android.view.WindowManager -import androidx.core.view.ViewCompat class ScreenshotBlockHelper( private val activity: Activity @@ -57,7 +56,7 @@ class ScreenshotBlockHelper( } with(activity) { - if (changed && ViewCompat.isAttachedToWindow(window.decorView)) { + if (changed && window.decorView.isAttachedToWindow) { windowManager.removeViewImmediate(window.decorView) windowManager.addView(window.decorView, window.attributes) } diff --git a/common/src/main/java/jp/co/soramitsu/common/util/ext/ContextExt.kt b/common/src/main/java/jp/co/soramitsu/common/util/ext/ContextExt.kt index ab8c24322..548af3fb0 100644 --- a/common/src/main/java/jp/co/soramitsu/common/util/ext/ContextExt.kt +++ b/common/src/main/java/jp/co/soramitsu/common/util/ext/ContextExt.kt @@ -32,11 +32,13 @@ USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package jp.co.soramitsu.common.util.ext +import android.app.Activity import android.content.Context import android.content.ContextWrapper import android.graphics.Color import android.util.TypedValue import android.view.View +import android.view.Window import android.view.inputmethod.InputMethodManager import androidx.annotation.AttrRes import androidx.annotation.ColorInt @@ -82,18 +84,29 @@ fun Context.getColorFromAttrs(attr: Int): TypedValue { fun Context.dpRes2px(@DimenRes res: Int): Int = this.resources.getDimensionPixelSize(res) -// todo remove fun Context.hideSoftKeyboard() { val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputMethodManager.toggleSoftInput( - InputMethodManager.SHOW_IMPLICIT, 0 - ) + val windowToken = when (this) { + is Activity -> currentFocus?.windowToken + else -> getActivity()?.currentFocus?.windowToken + } + windowToken?.let { + inputMethodManager.hideSoftInputFromWindow(it, 0) + } } fun Context.openSoftKeyboard(view: View) { val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager view.requestFocus() - inputMethodManager.toggleSoftInput( - InputMethodManager.SHOW_IMPLICIT, 0 - ) + inputMethodManager.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT) +} + +@Suppress("DEPRECATION") +fun Window.setStatusBarColorCompat(@ColorInt color: Int) { + statusBarColor = color +} + +@Suppress("DEPRECATION") +fun Window.setNavigationBarColorCompat(@ColorInt color: Int) { + navigationBarColor = color } diff --git a/common/src/main/java/jp/co/soramitsu/common/util/ext/FragmentExt.kt b/common/src/main/java/jp/co/soramitsu/common/util/ext/FragmentExt.kt index 4f3943720..133098eea 100644 --- a/common/src/main/java/jp/co/soramitsu/common/util/ext/FragmentExt.kt +++ b/common/src/main/java/jp/co/soramitsu/common/util/ext/FragmentExt.kt @@ -42,6 +42,7 @@ import android.widget.Toast import androidx.annotation.DimenRes import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb +import androidx.core.os.BundleCompat import androidx.fragment.app.Fragment import androidx.lifecycle.coroutineScope import jp.co.soramitsu.common.domain.ResponseCode @@ -92,8 +93,10 @@ fun Fragment.dp2px(dp: Int): Int = fun Fragment.dpRes2px(@DimenRes res: Int): Int = requireContext().resources.getDimensionPixelSize(res) -fun Fragment.requireParcelable(key: String): T { - return requireNotNull(requireArguments().getParcelable(key), { "Argument [$key] not found" }) +inline fun Fragment.requireParcelable(key: String): T { + return requireNotNull( + BundleCompat.getParcelable(requireArguments(), key, T::class.java), + ) { "Argument [$key] not found" } } fun Fragment.runDelayed( @@ -116,9 +119,9 @@ fun Fragment.openSoftKeyboard(view: View) { } fun Fragment.setStatusBarColor(color: Color) { - this.requireActivity().window.statusBarColor = color.toArgb() + this.requireActivity().window.setStatusBarColorCompat(color.toArgb()) } fun Fragment.setNavbarColor(color: Color) { - this.requireActivity().window.navigationBarColor = color.toArgb() + this.requireActivity().window.setNavigationBarColorCompat(color.toArgb()) } diff --git a/common/src/main/java/jp/co/soramitsu/common/vibration/DeviceVibrator.kt b/common/src/main/java/jp/co/soramitsu/common/vibration/DeviceVibrator.kt index 15ad4e607..9ea04ed81 100644 --- a/common/src/main/java/jp/co/soramitsu/common/vibration/DeviceVibrator.kt +++ b/common/src/main/java/jp/co/soramitsu/common/vibration/DeviceVibrator.kt @@ -32,6 +32,7 @@ USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package jp.co.soramitsu.common.vibration +import android.os.VibrationEffect import android.os.Vibrator class DeviceVibrator( @@ -43,6 +44,11 @@ class DeviceVibrator( } fun makeShortVibration() { - vibrator.vibrate(SHORT_VIBRATION_DURATION) + vibrator.vibrate( + VibrationEffect.createOneShot( + SHORT_VIBRATION_DURATION, + VibrationEffect.DEFAULT_AMPLITUDE + ) + ) } } diff --git a/common/src/main/res/values-ar/strings.xml b/common/src/main/res/values-ar/strings.xml index e0429980f..58505c93b 100644 --- a/common/src/main/res/values-ar/strings.xml +++ b/common/src/main/res/values-ar/strings.xml @@ -87,6 +87,7 @@ حدد العملة Claim اغلاق + عرض في المستكشف تأكيد متصل توصيل... diff --git a/common/src/main/res/values-az/strings.xml b/common/src/main/res/values-az/strings.xml index 60996708f..a9b2ad7c6 100644 --- a/common/src/main/res/values-az/strings.xml +++ b/common/src/main/res/values-az/strings.xml @@ -87,6 +87,7 @@ Aktiv seçin iddia Bağlayın + Eksplorerdə bax Təsdiqləyin Əlaqədar Qoşulur ... diff --git a/common/src/main/res/values-de/strings.xml b/common/src/main/res/values-de/strings.xml index 76b97256d..d09d38d67 100644 --- a/common/src/main/res/values-de/strings.xml +++ b/common/src/main/res/values-de/strings.xml @@ -87,6 +87,7 @@ Asset auswählen Claim Schließen + Im Explorer anzeigen Bestätigen Verbunden Verbinden... @@ -448,10 +449,10 @@ No available invitations Link oder Adresse des Referrers Adresse des Referrers - Jedes Mal, wenn du eine Transaktion durchführst, gehen 10 % der gezahlten Netzwerkgebühr als Belohnung an deinen Referrer. + Jedes Mal, wenn du eine Transaktion durchführst, gehen 10 %% der gezahlten Netzwerkgebühr als Belohnung an deinen Referrer. Beginne mit dem Einladen - Jedes Mal, wenn deine Empfehlung eine Transaktion durchführt, erhälst du 10 % der gezahlten Netzwerkgebühr als Belohnung. - Erhalte 10 % der Netzwerkgebühren für deine Empfehlungen + Jedes Mal, wenn deine Empfehlung eine Transaktion durchführt, erhälst du 10 %% der gezahlten Netzwerkgebühr als Belohnung. + Erhalte 10 %% der Netzwerkgebühren für deine Empfehlungen Empfehlungsprogramm Gesamte Belohnung Unbond XOR diff --git a/common/src/main/res/values-es/strings.xml b/common/src/main/res/values-es/strings.xml index 5a3392125..b8e180cb9 100644 --- a/common/src/main/res/values-es/strings.xml +++ b/common/src/main/res/values-es/strings.xml @@ -87,6 +87,7 @@ Seleccionar activo Claim Cerrar + Ver en el explorador Confirmar Conectado Conectando ... diff --git a/common/src/main/res/values-fa/strings.xml b/common/src/main/res/values-fa/strings.xml index f6db0d44f..e1519da7b 100644 --- a/common/src/main/res/values-fa/strings.xml +++ b/common/src/main/res/values-fa/strings.xml @@ -1,5 +1,5 @@ - + دریافت اطلاعیه‌ها درخواست پشتیبانی ایمیل تماس @@ -87,6 +87,7 @@ دارایی را انتخاب کنید Claim بستن + مشاهده در کاوشگر تایید متصل برقراری ارتباط... @@ -154,7 +155,7 @@ Possibly the password you entered is invalid. Hebrew Hindi - + ساعت ساعت @@ -441,7 +442,7 @@ Enter desired amount of invitations Available invitations Invitation link - + invitation invitations diff --git a/common/src/main/res/values-fi/strings.xml b/common/src/main/res/values-fi/strings.xml index 3a202d27d..104118815 100644 --- a/common/src/main/res/values-fi/strings.xml +++ b/common/src/main/res/values-fi/strings.xml @@ -87,6 +87,7 @@ Valitse resurssi Claim Sulje + Näytä explorerissa Vahvista Yhdistetty Yhdistetään... @@ -450,8 +451,8 @@ Referrer\'s address Whenever you make a transaction, 10% of the network fee paid goes to your referrer as a reward. Aloita kutsuminen - Aina kun suosittelija suorittaa tapahtuman, 10 % maksetusta verkkomaksusta tulee sinulle palkkiona. - Saat 10 % suosittelemiesi jäsenten verkkomaksuista. + Aina kun suosittelija suorittaa tapahtuman, 10 %% maksetusta verkkomaksusta tulee sinulle palkkiona. + Saat 10 %% suosittelemiesi jäsenten verkkomaksuista. Suositteluohjelma Palkkiot yhteensä Unbond XOR diff --git a/common/src/main/res/values-fr/strings.xml b/common/src/main/res/values-fr/strings.xml index 8b87f4767..29c084729 100644 --- a/common/src/main/res/values-fr/strings.xml +++ b/common/src/main/res/values-fr/strings.xml @@ -1,5 +1,5 @@ - + Recevoir les annonces Demander l\'aide du Support Nous contacter @@ -87,6 +87,7 @@ Sélectionner un actif Claim Fermer + Voir dans l\'explorateur Confirmer Connecté Connexion en cours... @@ -154,7 +155,7 @@ Possibly the password you entered is invalid. Hébreu Hindi - + heure heures @@ -441,7 +442,7 @@ Entrez le nombre d\'invitations souhaité Invitations disponibles Lien d\'invitation - + invitation invitations @@ -450,8 +451,8 @@ Adresse du référent Chaque fois que vous effectuez une transaction, 10% des frais de réseau payés sont reversés à votre parrain en guise de récompense. Commencez à inviter - Chaque fois qu\'une adresse que vous parrainez effectue une transaction, 10 % des frais de réseau payés vous reviennent à titre de récompense. - Recevez 10 % des frais de réseau des adresses que vous parrainez. + Chaque fois qu\'une adresse que vous parrainez effectue une transaction, 10 %% des frais de réseau payés vous reviennent à titre de récompense. + Recevez 10 %% des frais de réseau des adresses que vous parrainez. Programme de parrainage Total des récompenses Délier XOR diff --git a/common/src/main/res/values-hi-rIN/strings.xml b/common/src/main/res/values-hi-rIN/strings.xml index e1d019902..4b19b078c 100644 --- a/common/src/main/res/values-hi-rIN/strings.xml +++ b/common/src/main/res/values-hi-rIN/strings.xml @@ -1,5 +1,5 @@ - + घोषणाएँ प्राप्त करें समर्थन के लिए पूछें संपर्क करें @@ -87,6 +87,7 @@ संपत्ति का चयन करें Claim बंद करे + एक्सप्लोरर में देखें पुष्टि करें जुड़ा जुड़ रहा है... @@ -154,7 +155,7 @@ Possibly the password you entered is invalid. Hebrew Hindi - + खाली घंटा @@ -403,20 +404,20 @@ बायोमेट्री ऑथ लॉगआउट - दिन शेष - दिन शेष + %s दिन शेष + %s दिन शेष - घंटा शेष - घंटे शेष + %s घंटा शेष + %s घंटे शेष - मिनट शेष - मिनट्स शेष + %s मिनट शेष + %s मिनट्स शेष - सेकंड शेष - सेकंड्‌स शेष + %s सेकंड शेष + %s सेकंड्‌स शेष समाप्त%s Protect yourself from losing access to your funds @@ -441,7 +442,7 @@ आमंत्रणों की वांछित मात्रा दर्ज करें उपलब्ध आमंत्रण आमंत्रण लिंक - + आमंत्रण आमंत्रण diff --git a/common/src/main/res/values-in/strings.xml b/common/src/main/res/values-in/strings.xml index aafcb5b41..d37de05b2 100644 --- a/common/src/main/res/values-in/strings.xml +++ b/common/src/main/res/values-in/strings.xml @@ -87,6 +87,7 @@ Pilih aset Claim Tutup + Lihat di explorer Konfirmasi Terhubung Menghubungkan ... diff --git a/common/src/main/res/values-iw/strings.xml b/common/src/main/res/values-iw/strings.xml index 420da72bf..6efe85601 100644 --- a/common/src/main/res/values-iw/strings.xml +++ b/common/src/main/res/values-iw/strings.xml @@ -87,6 +87,7 @@ בחר נכס Claim סגור + הצג בסייר אשר מחובר מתחבר... diff --git a/common/src/main/res/values-ja/strings.xml b/common/src/main/res/values-ja/strings.xml index 76db90d5d..05f791d8e 100644 --- a/common/src/main/res/values-ja/strings.xml +++ b/common/src/main/res/values-ja/strings.xml @@ -87,6 +87,7 @@ アセットを選択 Claim 閉じる + エクスプローラーで表示 確認 接続されました 接続しています.. diff --git a/common/src/main/res/values-ms-rMY/strings.xml b/common/src/main/res/values-ms-rMY/strings.xml index f0268a23b..858c72c56 100644 --- a/common/src/main/res/values-ms-rMY/strings.xml +++ b/common/src/main/res/values-ms-rMY/strings.xml @@ -87,6 +87,7 @@ Pilih aset Claim Tutup + Lihat di explorer Sahkan Disambungkan Menyambung ... diff --git a/common/src/main/res/values-nb/strings.xml b/common/src/main/res/values-nb/strings.xml index 9696f15f0..7a51957d8 100644 --- a/common/src/main/res/values-nb/strings.xml +++ b/common/src/main/res/values-nb/strings.xml @@ -87,6 +87,7 @@ Velg aktivum Claim Lukk + Vis i utforsker Bekrefte Tilkoblet Kobler til... diff --git a/common/src/main/res/values-nl/strings.xml b/common/src/main/res/values-nl/strings.xml index 5b13ee39d..574df04ae 100644 --- a/common/src/main/res/values-nl/strings.xml +++ b/common/src/main/res/values-nl/strings.xml @@ -87,6 +87,7 @@ Selecteer asset Claim Sluit + Bekijk in explorer Bevestigen Verbonden Verbinding maken... diff --git a/common/src/main/res/values-pt/strings.xml b/common/src/main/res/values-pt/strings.xml index 72f2471bb..67965b2f4 100644 --- a/common/src/main/res/values-pt/strings.xml +++ b/common/src/main/res/values-pt/strings.xml @@ -1,5 +1,5 @@ - + Receber Anúncios Solicitar Suporte Email de contacto @@ -87,6 +87,7 @@ Escolha o ativo Claim Fechar + Ver no explorador Confirmar Conectado Conectando... @@ -154,7 +155,7 @@ Possibly the password you entered is invalid. Hebrew Hindi - + hora horas @@ -441,7 +442,7 @@ Enter desired amount of invitations Available invitations Invitation link - + invitation invitations diff --git a/common/src/main/res/values-ru/strings.xml b/common/src/main/res/values-ru/strings.xml index eaaa665cd..67c7d9b91 100644 --- a/common/src/main/res/values-ru/strings.xml +++ b/common/src/main/res/values-ru/strings.xml @@ -1,5 +1,5 @@ - + Получайте анонсы Обратиться за поддержкой Свяжитесь с нами по почте @@ -87,6 +87,7 @@ Выбор ассета Claim Закрыть + Посмотреть в обозревателе Подтвердить Подключено Подключение… @@ -154,7 +155,7 @@ Возможно, введенный вами пароль неверен. Иврит Хинди - + часов час часа @@ -456,7 +457,7 @@ Введите желаемое количество приглашений Доступные приглашения Invitation link - + приглашение приглашения diff --git a/common/src/main/res/values-sr/strings.xml b/common/src/main/res/values-sr/strings.xml index be5295fae..c60b2d7ce 100644 --- a/common/src/main/res/values-sr/strings.xml +++ b/common/src/main/res/values-sr/strings.xml @@ -1,5 +1,5 @@ - + Примите најаве Питајте за подршку Kontakt Email @@ -87,6 +87,7 @@ Изаберите средство Claim Затвори + Прикажи у експлореру Потврди Повезан Повезивање... @@ -154,7 +155,7 @@ Possibly the password you entered is invalid. Hebrew Hindi - + сат сати сати @@ -404,24 +405,24 @@ Биометријска аутентиф. Одјављивање - дан преостао - дана преостало - дана преостало + %s дан преостао + %s дана преостало + %s дана преостало - сат преостао - сати преостало - сати преостало + %s сат преостао + %s сати преостало + %s сати преостало - минут преостао - минута преостало - минута преостало + %s минут преостао + %s минута преостало + %s минута преостало - секунд преостао - секунди преостало - секунди преостало + %s секунд преостао + %s секунди преостало + %s секунди преостало Завршено %s Protect yourself from losing access to your funds @@ -446,8 +447,9 @@ Unesite željeni broj pozivnica Dostupne pozivnice Invitation link - + invitation + invitations invitations No available invitations diff --git a/common/src/main/res/values-tr/strings.xml b/common/src/main/res/values-tr/strings.xml index 1ad8eafaa..41b69d210 100644 --- a/common/src/main/res/values-tr/strings.xml +++ b/common/src/main/res/values-tr/strings.xml @@ -87,6 +87,7 @@ Varlık seçin Claim Kapat + Gezginde görüntüle Onayla Bağlandı Bağlanıyor... diff --git a/common/src/main/res/values-vi/strings.xml b/common/src/main/res/values-vi/strings.xml index bd26b6bd7..e24157da1 100644 --- a/common/src/main/res/values-vi/strings.xml +++ b/common/src/main/res/values-vi/strings.xml @@ -87,6 +87,7 @@ Chọn tài sản Lấy Đóng + Xem trong trình khám phá Xác nhận Đã kết nối Đang kết nối... diff --git a/common/src/main/res/values-zh-rCN/strings.xml b/common/src/main/res/values-zh-rCN/strings.xml index 4f82578b7..5ab2f9123 100644 --- a/common/src/main/res/values-zh-rCN/strings.xml +++ b/common/src/main/res/values-zh-rCN/strings.xml @@ -87,6 +87,7 @@ 选择资产 领取 关闭 + 在区块浏览器中查看 确认 已连接 正在连接... diff --git a/common/src/main/res/values-zh-rTW/strings.xml b/common/src/main/res/values-zh-rTW/strings.xml index 5cbf04b9e..16a1312a7 100644 --- a/common/src/main/res/values-zh-rTW/strings.xml +++ b/common/src/main/res/values-zh-rTW/strings.xml @@ -87,6 +87,7 @@ 選擇資產 Claim 關閉 + 在區塊瀏覽器中查看 確認交換 連接的 正在連線... diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index f5b3d7751..9dcc69d42 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -87,6 +87,7 @@ Select asset Claim Close + View on explorer Confirm Connected Connecting... diff --git a/common/src/test/java/jp/co/soramitsu/common/logger/FirebaseWrapperTest.kt b/common/src/test/java/jp/co/soramitsu/common/logger/FirebaseWrapperTest.kt new file mode 100644 index 000000000..1fd1e9e35 --- /dev/null +++ b/common/src/test/java/jp/co/soramitsu/common/logger/FirebaseWrapperTest.kt @@ -0,0 +1,56 @@ +/* +This file is part of the SORA network and Polkaswap app. + +Copyright (c) 2020, 2021, Polka Biome Ltd. All rights reserved. +SPDX-License-Identifier: BSD-4-Clause + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or other +materials provided with the distribution. + +All advertising materials mentioning features or use of this software must display +the following acknowledgement: This product includes software developed by Polka Biome +Ltd., SORA, and Polkaswap. + +Neither the name of the Polka Biome Ltd. nor the names of its contributors may be used +to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY Polka Biome Ltd. AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Polka Biome Ltd. BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package jp.co.soramitsu.common.logger + +import org.junit.After +import org.junit.Before +import org.junit.Test + +class FirebaseWrapperTest { + + @Before + fun setUp() { + FirebaseWrapper.setCrashlyticsEnabled(false) + } + + @After + fun tearDown() { + FirebaseWrapper.setCrashlyticsEnabled(false) + } + + @Test + fun `log and recordException do not reach Firebase when Crashlytics is disabled`() { + FirebaseWrapper.log("Firebase disabled") + FirebaseWrapper.recordException(IllegalStateException("Firebase disabled")) + } +} diff --git a/common_wallet/build.gradle.kts b/common_wallet/build.gradle.kts index b8cd6d887..b3d4b5929 100644 --- a/common_wallet/build.gradle.kts +++ b/common_wallet/build.gradle.kts @@ -2,22 +2,21 @@ plugins { id("maven-publish") alias(libs.plugins.androidLibrary) alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.composeCompiler) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } -val composeCompilerVersion: String by project - kotlin { jvmToolchain(17) } android { namespace = "jp.co.soramitsu.common_wallet" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -27,7 +26,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -39,11 +38,6 @@ android { ) } } - - composeOptions { - kotlinCompilerExtensionVersion = composeCompilerVersion - } - buildFeatures { viewBinding = true compose = true @@ -100,18 +94,19 @@ dependencies { implementation(libs.uiCoreDep) implementation(libs.lifecycleProcessDep) - kapt(libs.lifecycleKaptDep) implementation(libs.timberDep) implementation(libs.svgDep) implementation(libs.jdenticonDep) api(libs.soramitsu.sora.card) { + exclude(group = "com.paywings.oauth", module = "android-sdk") + exclude(group = "com.paywings.kyc", module = "android-sdk") exclude(group = "com.paywings.onboarding.kyc.android-libs", module = "java-websocket-lib") } implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) implementation(libs.datastoreDep) diff --git a/common_wallet/src/main/java/jp/co/soramitsu/common_wallet/data/SoraCardNetworkClientImpl.kt b/common_wallet/src/main/java/jp/co/soramitsu/common_wallet/data/SoraCardNetworkClientImpl.kt index 53c2428a0..2f7452705 100644 --- a/common_wallet/src/main/java/jp/co/soramitsu/common_wallet/data/SoraCardNetworkClientImpl.kt +++ b/common_wallet/src/main/java/jp/co/soramitsu/common_wallet/data/SoraCardNetworkClientImpl.kt @@ -98,7 +98,7 @@ class SoraCardNetworkClientImpl( header: String?, bearerToken: String?, url: String, - body: Any, + body: String, deserializer: DeserializationStrategy ): SoraCardNetworkResponse { return try { diff --git a/core_db/build.gradle.kts b/core_db/build.gradle.kts index 501aa50d2..47968465f 100644 --- a/core_db/build.gradle.kts +++ b/core_db/build.gradle.kts @@ -4,9 +4,8 @@ plugins { alias(libs.plugins.kotlinAndroid) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") - id("com.google.devtools.ksp") alias(libs.plugins.kover) } @@ -16,7 +15,7 @@ kotlin { android { namespace = "com.example.core_db" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -24,7 +23,7 @@ android { } testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -83,14 +82,14 @@ dependencies { implementation(project(":common")) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) implementation(libs.coroutineDep) implementation(libs.coroutineAndroidDep) implementation(libs.roomDep) implementation(libs.roomKtxDep) - ksp(libs.roomKaptDep) + ksp(libs.roomCompilerDep) androidTestImplementation(project(":test_data")) androidTestImplementation(libs.soramitsu.android.foundation) diff --git a/core_db/src/main/java/jp/co/soramitsu/core_db/AppDatabase.kt b/core_db/src/main/java/jp/co/soramitsu/core_db/AppDatabase.kt index 15ecc7d4f..874102c54 100644 --- a/core_db/src/main/java/jp/co/soramitsu/core_db/AppDatabase.kt +++ b/core_db/src/main/java/jp/co/soramitsu/core_db/AppDatabase.kt @@ -113,7 +113,7 @@ abstract class AppDatabase : RoomDatabase() { AppDatabase::class.java, "app.db" ) - .fallbackToDestructiveMigrationFrom(*destructiveMigrationFromList) + .fallbackToDestructiveMigrationFrom(true, *destructiveMigrationFromList) .addMigrations(migration_poolsBaseToken_61_62) .addMigrations(migration_reorderBaseToken_62_63) .addMigrations(migration_CardHub_63_64) diff --git a/core_db/src/main/java/jp/co/soramitsu/core_db/dao/PoolDao.kt b/core_db/src/main/java/jp/co/soramitsu/core_db/dao/PoolDao.kt index 03a76b350..9212b11ec 100644 --- a/core_db/src/main/java/jp/co/soramitsu/core_db/dao/PoolDao.kt +++ b/core_db/src/main/java/jp/co/soramitsu/core_db/dao/PoolDao.kt @@ -37,6 +37,7 @@ import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.Transaction import androidx.room.Upsert import jp.co.soramitsu.core_db.model.BasePoolWithTokenLocal import jp.co.soramitsu.core_db.model.BasicPoolLocal @@ -56,6 +57,7 @@ interface PoolDao { """ } + @Transaction @Query("select * from allpools") fun subscribeBasicPoolsWithToken(): Flow> diff --git a/core_db/src/main/java/jp/co/soramitsu/core_db/migrations/CardHubMigration64.kt b/core_db/src/main/java/jp/co/soramitsu/core_db/migrations/CardHubMigration64.kt index bc78169a7..7c76cafcc 100644 --- a/core_db/src/main/java/jp/co/soramitsu/core_db/migrations/CardHubMigration64.kt +++ b/core_db/src/main/java/jp/co/soramitsu/core_db/migrations/CardHubMigration64.kt @@ -52,8 +52,8 @@ val migration_CardHub_63_64 = object : Migration(63, 64) { val assetsCursor = database.query("select * from assets where displayAsset = 1") val displayedAssets = assetsCursor.map { - val tokenId = getString(getColumnIndex("tokenId")) - val account = getString(getColumnIndex("accountAddress")) + val tokenId = getString(getColumnIndexOrThrow("tokenId")) + val account = getString(getColumnIndexOrThrow("accountAddress")) tokenId to account } displayedAssets.forEach { pair -> @@ -127,7 +127,7 @@ val migration_CardHub_63_64 = object : Migration(63, 64) { val cursor = database.query("select substrateAddress from accounts") val addresses = cursor.map { - getString(getColumnIndex("substrateAddress")) + getString(getColumnIndexOrThrow("substrateAddress")) } addresses.forEach { address -> CardHubType.values() diff --git a/demeter/build.gradle.kts b/demeter/build.gradle.kts index 935c5a0d0..089eec6a8 100644 --- a/demeter/build.gradle.kts +++ b/demeter/build.gradle.kts @@ -1,22 +1,21 @@ plugins { alias(libs.plugins.androidLibrary) alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.composeCompiler) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } -val composeCompilerVersion: String by project - kotlin { jvmToolchain(17) } android { namespace = "jp.co.soramitsu.demeter" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -26,7 +25,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -38,11 +37,6 @@ android { ) } } - - composeOptions { - kotlinCompilerExtensionVersion = composeCompilerVersion - } - buildFeatures { viewBinding = true compose = true @@ -89,7 +83,7 @@ dependencies { implementation(libs.timberDep) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) implementation(libs.coroutineDep) implementation(libs.coroutineAndroidDep) @@ -97,6 +91,7 @@ dependencies { implementation(libs.xsubstrateDep) implementation(libs.soramitsu.android.foundation) + implementation(platform(libs.compose.bom)) implementation(libs.composeRuntimeDep) implementation(libs.roomDep) diff --git a/feature_account_api/build.gradle.kts b/feature_account_api/build.gradle.kts index 55f5e9e69..cc2934f75 100644 --- a/feature_account_api/build.gradle.kts +++ b/feature_account_api/build.gradle.kts @@ -3,7 +3,7 @@ plugins { alias(libs.plugins.kotlinAndroid) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } @@ -14,7 +14,7 @@ kotlin { android { namespace = "jp.co.soramitsu.feature_account_api" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -24,7 +24,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -75,5 +75,5 @@ dependencies { implementation(libs.coroutineTestDep) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) } diff --git a/feature_account_impl/build.gradle.kts b/feature_account_impl/build.gradle.kts index 78ba05cd1..443a8c562 100644 --- a/feature_account_impl/build.gradle.kts +++ b/feature_account_impl/build.gradle.kts @@ -3,7 +3,7 @@ plugins { alias(libs.plugins.kotlinAndroid) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } @@ -14,7 +14,7 @@ kotlin { android { namespace = "jp.co.soramitsu.feature_account_impl" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -24,7 +24,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -76,7 +76,7 @@ dependencies { implementation(libs.soramitsu.android.foundation) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) implementation(libs.roomDep) implementation(libs.roomKtxDep) diff --git a/feature_account_impl/src/main/java/jp/co/soramitsu/feature_account_impl/data/repository/UserRepositoryImpl.kt b/feature_account_impl/src/main/java/jp/co/soramitsu/feature_account_impl/data/repository/UserRepositoryImpl.kt index 1c1757174..1349ebc96 100644 --- a/feature_account_impl/src/main/java/jp/co/soramitsu/feature_account_impl/data/repository/UserRepositoryImpl.kt +++ b/feature_account_impl/src/main/java/jp/co/soramitsu/feature_account_impl/data/repository/UserRepositoryImpl.kt @@ -174,13 +174,16 @@ class UserRepositoryImpl( } override suspend fun fullLogout() { - userDatasource.clearAllData() - db.withTransaction { - db.accountDao().clearAll() - db.referralsDao().clearTable() - db.nodeDao().clearTable() - db.globalCardsHubDao().clearTable() - defaultGlobalCards() + mutex.withLock { + userDatasource.clearAllData() + db.withTransaction { + db.accountDao().clearAll() + db.referralsDao().clearTable() + db.nodeDao().clearTable() + db.globalCardsHubDao().clearTable() + defaultGlobalCards() + } + currentSoraAccount.value = null } } @@ -203,10 +206,16 @@ class UserRepositoryImpl( } override suspend fun clearAccountData(address: String) { - credentialsDatasource.clearAllDataForAddress(address) - db.withTransaction { - db.accountDao().clearAccount(address) - db.referralsDao().clearTable() + mutex.withLock { + credentialsDatasource.clearAllDataForAddress(address) + db.withTransaction { + db.accountDao().clearAccount(address) + db.referralsDao().clearTable() + } + if (currentSoraAccount.value?.substrateAddress == address) { + userDatasource.setCurAccountAddress("") + currentSoraAccount.value = null + } } } diff --git a/feature_account_impl/src/test/java/jp/co/soramitsu/feature_account_impl/data/repository/UserRepositoryTest.kt b/feature_account_impl/src/test/java/jp/co/soramitsu/feature_account_impl/data/repository/UserRepositoryTest.kt index 630aad432..f27f9991d 100644 --- a/feature_account_impl/src/test/java/jp/co/soramitsu/feature_account_impl/data/repository/UserRepositoryTest.kt +++ b/feature_account_impl/src/test/java/jp/co/soramitsu/feature_account_impl/data/repository/UserRepositoryTest.kt @@ -63,6 +63,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test @@ -384,6 +385,30 @@ class UserRepositoryTest { coVerify { userDatasource.clearAllData() } } + @Test + fun `full logout clears in-memory current account`() = runTest { + coEvery { userDatasource.clearAllData() } returns Unit + coEvery { accountDao.clearAll() } returns Unit + coEvery { referralsDao.clearTable() } returns Unit + coEvery { nodeDao.clearTable() } returns Unit + coEvery { globalCardsHubDao.clearTable() } returns Unit + coEvery { globalCardsHubDao.insert(TestData.DEFAULT_GLOBAL_CARDS) } returns Unit + coEvery { globalCardsHubDao.count() } returns 0 + mockkStatic("androidx.room.RoomDatabaseKt") + val lambda = slot R>() + coEvery { db.withTransaction(capture(lambda)) } coAnswers { + lambda.captured.invoke() + } + + userRepository.fullLogout() + + coEvery { userDatasource.getCurAccountAddress() } returns "" + coEvery { accountDao.getAccounts() } returns emptyList() + val result = runCatching { userRepository.getCurSoraAccount() } + + assertTrue(result.isFailure) + } + @Test fun `save parent invite code called`() = runTest { val parentInviteCode = "parentInviteCode" @@ -432,6 +457,29 @@ class UserRepositoryTest { coVerify { credentialsDatasource.clearAllDataForAddress(address) } } + @Test + fun `clear current account data clears in-memory current account`() = runTest { + val address = "accountAddress" + mockkStatic("androidx.room.RoomDatabaseKt") + val lambda = slot R>() + coEvery { db.withTransaction(capture(lambda)) } coAnswers { + lambda.captured.invoke() + } + coEvery { accountDao.clearAccount(address) } returns Unit + coEvery { referralsDao.clearTable() } returns Unit + coEvery { credentialsDatasource.clearAllDataForAddress(address) } returns Unit + coEvery { userDatasource.setCurAccountAddress("") } returns Unit + + userRepository.clearAccountData(address) + + coEvery { userDatasource.getCurAccountAddress() } returns "" + coEvery { accountDao.getAccounts() } returns emptyList() + val result = runCatching { userRepository.getCurSoraAccount() } + + assertTrue(result.isFailure) + coVerify { userDatasource.setCurAccountAddress("") } + } + @Test fun `reset tries used called`() = runTest { coEvery { userDatasource.resetPinTriesUsed() } returns Unit diff --git a/feature_assets_api/build.gradle.kts b/feature_assets_api/build.gradle.kts index f1f9b7641..6a3e16a4f 100644 --- a/feature_assets_api/build.gradle.kts +++ b/feature_assets_api/build.gradle.kts @@ -1,22 +1,21 @@ plugins { alias(libs.plugins.androidLibrary) alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.composeCompiler) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } -val composeCompilerVersion: String by project - kotlin { jvmToolchain(17) } android { namespace = "jp.co.soramitsu.feature_assets_api" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -26,7 +25,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -43,11 +42,6 @@ android { viewBinding = true compose = true } - - composeOptions { - kotlinCompilerExtensionVersion = composeCompilerVersion - } - flavorDimensions += listOf("default") productFlavors { @@ -104,5 +98,5 @@ dependencies { implementation(libs.navigationComposeDep) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) } diff --git a/feature_assets_api/src/main/java/jp/co/soramitsu/feature_assets_api/presentation/selectsearchtoken/SelectSearchToken.kt b/feature_assets_api/src/main/java/jp/co/soramitsu/feature_assets_api/presentation/selectsearchtoken/SelectSearchToken.kt index 6142fdfce..71be492cf 100644 --- a/feature_assets_api/src/main/java/jp/co/soramitsu/feature_assets_api/presentation/selectsearchtoken/SelectSearchToken.kt +++ b/feature_assets_api/src/main/java/jp/co/soramitsu/feature_assets_api/presentation/selectsearchtoken/SelectSearchToken.kt @@ -54,7 +54,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import jp.co.soramitsu.common.R import jp.co.soramitsu.common_wallet.presentation.compose.components.AssetItem diff --git a/feature_assets_impl/build.gradle.kts b/feature_assets_impl/build.gradle.kts index 8c73ba098..d976219f8 100644 --- a/feature_assets_impl/build.gradle.kts +++ b/feature_assets_impl/build.gradle.kts @@ -1,22 +1,21 @@ plugins { alias(libs.plugins.androidLibrary) alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.composeCompiler) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } -val composeCompilerVersion: String by project - kotlin { jvmToolchain(17) } android { namespace = "jp.co.soramitsu.feature_assets_impl" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -26,7 +25,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -43,11 +42,6 @@ android { viewBinding = true compose = true } - - composeOptions { - kotlinCompilerExtensionVersion = composeCompilerVersion - } - flavorDimensions += listOf("default") productFlavors { @@ -119,12 +113,11 @@ dependencies { implementation(libs.permissionsRuntimeDep) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) implementation(libs.hiltWorkManagerDep) - kapt(libs.hiltWorkManagerKaptDep) + ksp(libs.hiltWorkManagerCompilerDep) implementation(libs.lifecycleProcessDep) - kapt(libs.lifecycleKaptDep) implementation(libs.roomDep) implementation(libs.roomKtxDep) diff --git a/feature_assets_impl/src/main/java/jp/co/soramitsu/feature_assets_impl/presentation/screens/receiverequest/QRCodeFlowViewModel.kt b/feature_assets_impl/src/main/java/jp/co/soramitsu/feature_assets_impl/presentation/screens/receiverequest/QRCodeFlowViewModel.kt index 458ff795a..ce451f4ba 100644 --- a/feature_assets_impl/src/main/java/jp/co/soramitsu/feature_assets_impl/presentation/screens/receiverequest/QRCodeFlowViewModel.kt +++ b/feature_assets_impl/src/main/java/jp/co/soramitsu/feature_assets_impl/presentation/screens/receiverequest/QRCodeFlowViewModel.kt @@ -73,7 +73,6 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -141,6 +140,14 @@ class QRCodeFlowViewModel @Inject constructor( private val loadRequestByQrScreenDataFlowJob = interactor.subscribeAssetsActiveOfCurAccount() .combine(requestByQrScreenReloadMutableSharedFlow) { assets, _ -> + if (assets.isEmpty()) { + _requestTokenScreenState.value = _requestTokenScreenState.value.copy( + screenStatus = ScreenStatus.ERROR, + assetAmountInputState = null, + ) + return@combine + } + val assetInUse = currentTokenId?.let { tokenID -> assets.find { it.token.id == tokenID } } ?: assets.first() @@ -174,6 +181,7 @@ class QRCodeFlowViewModel @Inject constructor( precision = DEFAULT_TOKEN_PRINT_PRECISION, ), ), + screenStatus = ScreenStatus.READY_TO_RENDER, ) }.catch { onError(it) @@ -181,11 +189,6 @@ class QRCodeFlowViewModel @Inject constructor( _requestTokenScreenState.value.copy( screenStatus = ScreenStatus.ERROR ) - }.onEach { - _requestTokenScreenState.value = - _requestTokenScreenState.value.copy( - screenStatus = ScreenStatus.READY_TO_RENDER, - ) }.flowOn(coroutineManager.io) .launchIn(viewModelScope) @@ -406,7 +409,7 @@ class QRCodeFlowViewModel @Inject constructor( tokenId = _requestTokenScreenState.value.assetAmountInputState ?.token?.id, amount = _requestTokenScreenState.value.assetAmountInputState - ?.amount.toString() + ?.amount?.toPlainString() ) ) } diff --git a/feature_assets_impl/src/test/java/jp/co/soramitsu/feature_assets_impl/presentation/qr/QrCodeFlowViewModelTest.kt b/feature_assets_impl/src/test/java/jp/co/soramitsu/feature_assets_impl/presentation/qr/QrCodeFlowViewModelTest.kt index 9fc02269b..cc4a4c5cf 100644 --- a/feature_assets_impl/src/test/java/jp/co/soramitsu/feature_assets_impl/presentation/qr/QrCodeFlowViewModelTest.kt +++ b/feature_assets_impl/src/test/java/jp/co/soramitsu/feature_assets_impl/presentation/qr/QrCodeFlowViewModelTest.kt @@ -37,6 +37,7 @@ import android.graphics.drawable.PictureDrawable import android.net.Uri import androidx.arch.core.executor.testing.InstantTaskExecutorRule import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.junit4.MockKRule @@ -53,6 +54,7 @@ import jp.co.soramitsu.common.account.AccountAvatarGenerator import jp.co.soramitsu.common.account.SoraAccount import jp.co.soramitsu.common.domain.Asset import jp.co.soramitsu.common.io.FileManager +import jp.co.soramitsu.common.presentation.compose.uikit.tokens.ScreenStatus import jp.co.soramitsu.common.util.NumbersFormatter import jp.co.soramitsu.common.util.QrCodeGenerator import jp.co.soramitsu.feature_assets_api.domain.AssetsInteractor @@ -139,18 +141,7 @@ class QrCodeFlowViewModelTest { every { walletRouter.showValTransferAmount(any(), any(), any()) } returns Unit every { clipboardManager.addToClipboard(any(), any()) } returns Unit - viewModel = QRCodeFlowViewModel( - interactor = assetsInteractor, - qrCodeInteractor = qrCodeInteractor, - coroutineManager = coroutineManager, - qrCodeGenerator = qrCodeGenerator, - avatarGenerator = avatarGenerator, - clipboardManager = clipboardManager, - numbersFormatter = numbersFormatter, - resourceManager = resourceManager, - fileManager = fileManager, - walletRouter = walletRouter, - ) + viewModel = createViewModel() } @Test @@ -190,6 +181,44 @@ class QrCodeFlowViewModelTest { ) } + @Test + fun `WHEN user opens confirmation screen without amount EXPECT QR input has null amount`() = + runTest { + advanceUntilIdle() + viewModel.onLoadRequestConfirmScreenDataAgainClick() + advanceUntilIdle() + + coVerify(exactly = 1) { + qrCodeInteractor.createQrInput( + userAddress = "this is sora substrate address", + userPublicKey = "this is sor public hex key", + userName = "my account name", + tokenId = TestAssets.xorAsset(balance = Big100).token.id, + amount = null, + ) + } + } + + @Test + fun `WHEN active assets list is empty EXPECT request QR screen is error`() = + runTest { + every { + assetsInteractor.subscribeAssetsActiveOfCurAccount() + } returns testAssetsListFlow(emptyList()) + viewModel = createViewModel() + + advanceUntilIdle() + + Assert.assertEquals( + ScreenStatus.ERROR, + viewModel.requestTokenScreenState.value.screenStatus + ) + Assert.assertEquals( + null, + viewModel.requestTokenScreenState.value.assetAmountInputState + ) + } + @Test fun `WHEN user tries to copy user address EXPECT clipboard manager is invoked`() = runTest { @@ -291,4 +320,17 @@ class QrCodeFlowViewModelTest { return@with this.toString() } + + private fun createViewModel() = QRCodeFlowViewModel( + interactor = assetsInteractor, + qrCodeInteractor = qrCodeInteractor, + coroutineManager = coroutineManager, + qrCodeGenerator = qrCodeGenerator, + avatarGenerator = avatarGenerator, + clipboardManager = clipboardManager, + numbersFormatter = numbersFormatter, + resourceManager = resourceManager, + fileManager = fileManager, + walletRouter = walletRouter, + ) } diff --git a/feature_blockexplorer_api/build.gradle.kts b/feature_blockexplorer_api/build.gradle.kts index f89b0eff7..59d73b9a9 100644 --- a/feature_blockexplorer_api/build.gradle.kts +++ b/feature_blockexplorer_api/build.gradle.kts @@ -1,22 +1,21 @@ plugins { alias(libs.plugins.androidLibrary) alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.composeCompiler) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } -val composeCompilerVersion: String by project - kotlin { jvmToolchain(17) } android { namespace = "jp.co.soramitsu.feature_blockexplorer_api" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -26,7 +25,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -43,11 +42,6 @@ android { viewBinding = true compose = true } - - composeOptions { - kotlinCompilerExtensionVersion = composeCompilerVersion - } - flavorDimensions += listOf("default") productFlavors { @@ -106,6 +100,7 @@ dependencies { implementation(libs.composeToolingPreviewDep) implementation(libs.composeLiveDataDep) debugImplementation(libs.composeToolingDep) + debugImplementation(libs.composeUiTestManifestDep) implementation(libs.kotlinxSerializationJsonDep) @@ -115,15 +110,20 @@ dependencies { implementation(libs.timberDep) implementation(libs.lifecycleProcessDep) - kapt(libs.lifecycleKaptDep) implementation(libs.navigationFragmentDep) implementation(libs.navigationUiDep) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) -} + ksp(libs.hiltCompilerDep) + + testImplementation(libs.coroutineTestDep) + testImplementation(libs.junitDep) + testImplementation(libs.truthDep) -kapt { - correctErrorTypes = true + androidTestImplementation(platform(libs.compose.bom)) + androidTestImplementation(libs.composeUiTestJunit4Dep) + androidTestImplementation(libs.androidxTestExtJunitDep) + androidTestImplementation(libs.androidxTestEspressoCoreDep) + androidTestImplementation(libs.androidxTestEspressoIntentsDep) } diff --git a/feature_blockexplorer_api/src/androidTest/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/BasicTxDetailsExplorerIntentTest.kt b/feature_blockexplorer_api/src/androidTest/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/BasicTxDetailsExplorerIntentTest.kt new file mode 100644 index 000000000..1c3d1af7c --- /dev/null +++ b/feature_blockexplorer_api/src/androidTest/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/BasicTxDetailsExplorerIntentTest.kt @@ -0,0 +1,93 @@ +package jp.co.soramitsu.feature_blockexplorer_api.presentation.txdetails + +import android.app.Activity +import android.app.Instrumentation.ActivityResult +import android.content.Intent +import android.net.Uri +import androidx.activity.ComponentActivity +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.Intents.intended +import androidx.test.espresso.intent.Intents.intending +import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction +import androidx.test.espresso.intent.matcher.IntentMatchers.hasData +import androidx.test.ext.junit.runners.AndroidJUnit4 +import jp.co.soramitsu.common.R +import jp.co.soramitsu.common.presentation.compose.theme.SoraAppTheme +import jp.co.soramitsu.common.util.PACKAGE_ID +import jp.co.soramitsu.common.util.ShareUtil +import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.TransactionStatus +import org.hamcrest.Matchers.allOf +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class BasicTxDetailsExplorerIntentTest { + + @get:Rule + val composeRule = createAndroidComposeRule() + + @Before + fun setUp() { + Intents.init() + intending(hasAction(Intent.ACTION_VIEW)) + .respondWith(ActivityResult(Activity.RESULT_OK, null)) + } + + @After + fun tearDown() { + Intents.release() + } + + @Test + fun viewOnExplorerOpensSoraMetricsUrl() { + composeRule.setContent { + SoraAppTheme { + BasicTxDetails( + modifier = Modifier, + state = basicState(), + imageContent = {}, + amountContent = {}, + onCloseClick = {}, + onCopy = { _ -> }, + onOpenExplorer = { ShareUtil.shareInBrowser(composeRule.activity, it) }, + ) + } + } + + composeRule + .onNodeWithTag("$PACKAGE_ID:id/ViewOnExplorerButton") + .performClick() + + intended( + allOf( + hasAction(Intent.ACTION_VIEW), + hasData(Uri.parse(EXPLORER_URL)), + ) + ) + } + + private fun basicState() = BasicTxDetailsState( + txHash = "0xabc", + blockHash = "0xblock", + sender = "sender", + infos = emptyList(), + txStatus = TransactionStatus.COMMITTED, + time = "26 May 2026 12:00", + networkFee = null, + networkFeeFiat = null, + txTypeIcon = R.drawable.ic_arrow_down_24, + txTypeTitle = "Transfer", + explorerUrl = EXPLORER_URL, + ) + + private companion object { + const val EXPLORER_URL = "https://sorametrics.org/sorav2?tab=extrinsics&q=0xabc" + } +} diff --git a/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/data/BlockExplorerManager.kt b/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/data/BlockExplorerManager.kt index 965044076..8e4cba6e3 100644 --- a/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/data/BlockExplorerManager.kt +++ b/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/data/BlockExplorerManager.kt @@ -40,16 +40,12 @@ import javax.inject.Singleton import jp.co.soramitsu.androidfoundation.format.toDoubleInfinite import jp.co.soramitsu.common.config.BuildConfigWrapper import jp.co.soramitsu.common.domain.AppStateProvider -import jp.co.soramitsu.common.domain.RetryStrategyBuilder import jp.co.soramitsu.common.logger.FirebaseWrapper import jp.co.soramitsu.common.util.mapBalance import jp.co.soramitsu.core_db.AppDatabase import jp.co.soramitsu.core_db.model.FiatTokenPriceLocal import jp.co.soramitsu.core_db.model.ReferralLocal -import jp.co.soramitsu.xnetworking.lib.datasources.blockexplorer.api.BlockExplorerRepository -import jp.co.soramitsu.xnetworking.lib.datasources.blockexplorer.api.models.Apy import jp.co.soramitsu.xnetworking.lib.engines.rest.api.RestClient -import jp.co.soramitsu.xnetworking.lib.engines.rest.api.models.RestClientException import jp.co.soramitsu.xnetworking.lib.engines.utils.JsonGetRequest import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -61,13 +57,13 @@ fun String.toDoubleNan(): Double? = this.toDoubleOrNull()?.let { @Singleton class BlockExplorerManager @Inject constructor( private val restClient: RestClient, - private val info: BlockExplorerRepository, + private val polkaswapIndexerClient: PolkaswapIndexerClient, private val db: AppDatabase, private val appStateProvider: AppStateProvider, private val soraConfigManager: SoraConfigManager, ) { - private val tempApy = mutableListOf() + private val tempApy = mutableListOf() private var assetsInfo: List>? = null @@ -96,25 +92,13 @@ class BlockExplorerManager @Inject constructor( val resultList = mutableListOf>() val fiats = mutableListOf() - RetryStrategyBuilder.build().retryIf( - retries = 3, - predicate = { t -> - t is RestClientException - }, - block = { - info.getAssetsInfo( - soraConfigManager.getGenesis(), - tokenIds, - yesterdayHour.toInt(), - ) - }, - ).forEach { assetInfo -> + polkaswapIndexerClient.getAssetsInfo(tokenIds).forEach { assetInfo -> val dbValue = tokens.find { it.tokenIdFiat == assetInfo.id } - val prevPrice = assetInfo.previousPrice + val priceChangeDay = assetInfo.priceChangeDay if (dbValue != null) { fiats += dbValue.copy( - fiatChange = prevPrice?.div(100.0), + fiatChange = priceChangeDay?.div(100.0), fiatPricePrevHTime = yesterdayHour, ) } @@ -123,7 +107,7 @@ class BlockExplorerManager @Inject constructor( tokenId = dbValue?.tokenIdFiat ?: return@forEach ) ?: return@forEach - val supply = assetInfo.liquidity.toBigIntegerOrNull()?.let { + val supply = assetInfo.liquidity?.toBigIntegerOrNull()?.let { mapBalance(it, precision) } ?: return@forEach @@ -150,8 +134,8 @@ class BlockExplorerManager @Inject constructor( if (appStateProvider.isForeground) { runCatching { updateFiatPrices( - fiatData = info.getFiat(soraConfigManager.getGenesis()).map { - FiatInfo(it.id, it.priceUSD.toDoubleNan()) + fiatData = polkaswapIndexerClient.getFiat().map { + FiatInfo(it.id, it.priceUSD?.toDoubleNan()) } ) } @@ -160,7 +144,7 @@ class BlockExplorerManager @Inject constructor( suspend fun updateReferrerRewards(address: String) { runCatching { - val rewards = info.getReferralReward(soraConfigManager.getGenesis(), address).map { + val rewards = polkaswapIndexerClient.getReferralRewards(address).map { ReferralLocal(it.referral, it.amount) } @@ -184,7 +168,7 @@ class BlockExplorerManager @Inject constructor( private suspend fun updateSbApyInternal() { runCatching { - val response = info.getApy(soraConfigManager.getGenesis()) + val response = polkaswapIndexerClient.getPoolApys() tempApy.clear() tempApy.addAll(response) } diff --git a/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/data/PolkaswapIndexerClient.kt b/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/data/PolkaswapIndexerClient.kt new file mode 100644 index 000000000..85a50ba8c --- /dev/null +++ b/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/data/PolkaswapIndexerClient.kt @@ -0,0 +1,510 @@ +package jp.co.soramitsu.feature_blockexplorer_api.data + +import javax.inject.Inject +import javax.inject.Singleton +import jp.co.soramitsu.common.domain.OptionsProvider +import jp.co.soramitsu.xnetworking.lib.engines.rest.api.RestClient +import jp.co.soramitsu.xnetworking.lib.engines.rest.api.models.AbstractRestServerRequest +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put + +data class IndexerHistoryPage( + val items: List, + val endReached: Boolean, +) + +data class IndexerHistoryElement( + val id: String, + val blockHash: String?, + val module: String, + val method: String, + val address: String? = null, + val dataFrom: String? = null, + val dataTo: String? = null, + val timestamp: String, + val networkFee: String, + val success: Boolean, + val data: List?, + val nestedData: List?, +) + +data class IndexerNestedHistoryItem( + val module: String, + val method: String, + val data: List, +) + +data class IndexerHistoryItemParam( + val paramName: String, + val paramValue: String, +) + +data class IndexerAssetInfo( + val id: String, + val priceUSD: String?, + val liquidity: String?, + val priceChangeDay: Double?, +) + +data class IndexerPoolApy( + val id: String, + val value: String?, +) + +data class IndexerReferralReward( + val referral: String, + val amount: String, +) + +@Singleton +class PolkaswapIndexerClient @Inject constructor( + private val restClient: RestClient, + private val json: Json, +) { + + suspend fun getTransactionPeers(query: String): Set { + val trimmed = query.trim() + if (trimmed.isEmpty()) return emptySet() + + return getHistoryElements( + first = 50, + filter = orFilter( + fieldFilter("dataFrom", "includesInsensitive", trimmed), + fieldFilter("dataTo", "includesInsensitive", trimmed), + fieldFilter("address", "includesInsensitive", trimmed), + ) + ).items + .flatMap { item -> + item.data.orEmpty() + .filter { it.paramName == "from" || it.paramName == "to" } + .map { it.paramValue } + listOfNotNull(item.dataFrom, item.dataTo, item.addressParam()) + } + .filter { it.contains(trimmed, ignoreCase = true) } + .toSet() + } + + suspend fun getTransaction(txHash: String): List = + getHistoryElements( + first = 1, + filter = fieldFilter("id", "equalTo", txHash) + ).items + + suspend fun getLastTransactions( + address: String, + count: Int, + ): List = + getHistoryElements( + first = count, + filter = accountHistoryFilter(address) + ).items + + suspend fun getTransactionHistory( + address: String, + page: Long, + pageCount: Int, + ): IndexerHistoryPage { + val offset = ((page - 1).coerceAtLeast(0) * pageCount).toInt() + return getHistoryElements( + first = pageCount, + offset = offset, + filter = accountHistoryFilter(address) + ) + } + + suspend fun getAssetsInfo(tokenIds: List): List { + if (tokenIds.isEmpty()) return emptyList() + return execute( + query = ASSETS_QUERY, + variables = buildJsonObject { + put("first", tokenIds.size) + put("filter", fieldFilter("id", "in", tokenIds)) + }, + deserializer = AssetsData.serializer() + ).assets.edges.map { it.node.toModel() } + } + + suspend fun getFiat(): List = + execute( + query = ASSETS_QUERY, + variables = buildJsonObject { + put("first", DEFAULT_CONNECTION_LIMIT) + put("filter", JsonNull) + }, + deserializer = AssetsData.serializer() + ).assets.edges.map { it.node.toModel() } + + suspend fun getReferralRewards(address: String): List = + execute( + query = REFERRER_REWARDS_QUERY, + variables = buildJsonObject { + put("first", DEFAULT_CONNECTION_LIMIT) + put("filter", fieldFilter("referrer", "equalTo", address)) + }, + deserializer = ReferrerRewardsData.serializer() + ).referrerRewards.edges.map { edge -> + IndexerReferralReward( + referral = edge.node.referral.orEmpty(), + amount = edge.node.amount.orEmpty(), + ) + } + + suspend fun getPoolApys(): List = + execute( + query = POOL_APY_QUERY, + variables = buildJsonObject { + put("first", DEFAULT_CONNECTION_LIMIT) + }, + deserializer = PoolApyData.serializer() + ).poolXYKs.edges.map { edge -> + IndexerPoolApy( + id = edge.node.id, + value = edge.node.strategicBonusApy, + ) + } + + private suspend fun getHistoryElements( + first: Int, + offset: Int = 0, + filter: JsonElement?, + ): IndexerHistoryPage { + val response = execute( + query = HISTORY_QUERY, + variables = buildJsonObject { + put("first", first) + put("offset", offset) + put("filter", filter ?: JsonNull) + }, + deserializer = HistoryData.serializer() + ).historyElements + + return IndexerHistoryPage( + items = response.edges.map { it.node.toModel() }, + endReached = !response.pageInfo.hasNextPage, + ) + } + + private suspend fun execute( + query: String, + variables: JsonObject, + deserializer: KSerializer, + ): T { + val response: GraphQlResponse = restClient.post( + GraphQlPostRequest>( + responseDeserializer = GraphQlResponse.serializer(deserializer), + body = GraphQlBody(query = query, variables = variables), + ) + ) + + if (response.errors.isNotEmpty()) { + throw IllegalStateException(response.errors.joinToString { it.message.orEmpty() }) + } + + return checkNotNull(response.data) + } + + private fun HistoryElementDto.toModel() = + IndexerHistoryElement( + id = id, + blockHash = blockHash, + module = module.orEmpty(), + method = method.orEmpty(), + address = address, + dataFrom = dataFrom, + dataTo = dataTo, + timestamp = timestamp?.toString().orEmpty(), + networkFee = networkFee.orEmpty(), + success = (execution as? JsonObject)?.get("success")?.jsonPrimitive?.booleanOrNull ?: true, + data = data.toParams(), + nestedData = calls?.nodes.orEmpty().map { call -> + IndexerNestedHistoryItem( + module = call.module.orEmpty(), + method = call.method.orEmpty(), + data = call.data.toParams().orEmpty(), + ) + }, + ) + + private fun AssetDto.toModel() = + IndexerAssetInfo( + id = id, + priceUSD = priceUSD, + liquidity = liquidity, + priceChangeDay = priceChangeDay, + ) + + private fun IndexerHistoryElement.addressParam(): String? = + address ?: data?.firstOrNull { it.paramName == "address" }?.paramValue + + private fun JsonElement?.toParams(): List? { + val fields = (this as? JsonObject)?.entries ?: return null + return fields.map { (name, value) -> + IndexerHistoryItemParam( + paramName = name, + paramValue = value.toParamValue(), + ) + } + } + + private fun JsonElement.toParamValue(): String = + when (this) { + JsonNull -> "" + is JsonPrimitive -> contentOrNull ?: toString() + else -> json.encodeToString(JsonElement.serializer(), this) + } + + private class GraphQlPostRequest( + override val url: String = OptionsProvider.polkaswapIndexerEndpoint, + override val userAgent: String? = OptionsProvider.header, + override val bearerToken: String? = null, + override val responseDeserializer: DeserializationStrategy, + override val body: Any, + override val requestContentType: RestClient.ContentType = RestClient.ContentType.JSON, + ) : AbstractRestServerRequest.WithBody() + + @Serializable + private data class GraphQlBody( + val query: String, + val variables: JsonObject, + ) + + @Serializable + private data class GraphQlResponse( + val data: T? = null, + val errors: List = emptyList(), + ) + + @Serializable + private data class GraphQlError( + val message: String? = null, + ) + + @Serializable + private data class HistoryData( + val historyElements: HistoryConnection, + ) + + @Serializable + private data class HistoryConnection( + val edges: List = emptyList(), + val pageInfo: PageInfo = PageInfo(), + ) + + @Serializable + private data class HistoryEdge( + val node: HistoryElementDto, + ) + + @Serializable + private data class PageInfo( + val hasNextPage: Boolean = false, + ) + + @Serializable + private data class HistoryElementDto( + val id: String, + val timestamp: Int? = null, + val blockHash: String? = null, + val module: String? = null, + val method: String? = null, + val address: String? = null, + val dataFrom: String? = null, + val dataTo: String? = null, + val networkFee: String? = null, + val execution: JsonElement? = null, + val data: JsonElement? = null, + val calls: HistoryCallConnection? = null, + ) + + @Serializable + private data class HistoryCallConnection( + val nodes: List = emptyList(), + ) + + @Serializable + private data class HistoryCallDto( + val module: String? = null, + val method: String? = null, + val data: JsonElement? = null, + ) + + @Serializable + private data class AssetsData( + val assets: AssetConnection, + ) + + @Serializable + private data class AssetConnection( + val edges: List = emptyList(), + ) + + @Serializable + private data class AssetEdge( + val node: AssetDto, + ) + + @Serializable + private data class AssetDto( + val id: String, + @SerialName("priceUSD") + val priceUSD: String? = null, + val liquidity: String? = null, + val priceChangeDay: Double? = null, + ) + + @Serializable + private data class ReferrerRewardsData( + val referrerRewards: ReferrerRewardConnection, + ) + + @Serializable + private data class ReferrerRewardConnection( + val edges: List = emptyList(), + ) + + @Serializable + private data class ReferrerRewardEdge( + val node: ReferrerRewardDto, + ) + + @Serializable + private data class ReferrerRewardDto( + val referral: String? = null, + val amount: String? = null, + ) + + @Serializable + private data class PoolApyData( + @SerialName("poolXYKs") + val poolXYKs: PoolApyConnection, + ) + + @Serializable + private data class PoolApyConnection( + val edges: List = emptyList(), + ) + + @Serializable + private data class PoolApyEdge( + val node: PoolApyDto, + ) + + @Serializable + private data class PoolApyDto( + val id: String, + val strategicBonusApy: String? = null, + ) + + private companion object { + const val DEFAULT_CONNECTION_LIMIT = 500 + + val HISTORY_QUERY = """ + query History(${'$'}first: Int!, ${'$'}offset: Int, ${'$'}filter: HistoryElementFilter) { + historyElements(first: ${'$'}first, offset: ${'$'}offset, orderBy: [TIMESTAMP_DESC, ID_DESC], filter: ${'$'}filter) { + pageInfo { hasNextPage } + edges { + node { + id + timestamp + blockHash + module + method + address + dataFrom + dataTo + networkFee + execution + data + calls { nodes { module method data } } + } + } + } + } + """ + + val ASSETS_QUERY = """ + query Assets(${'$'}first: Int!, ${'$'}filter: AssetFilter) { + assets(first: ${'$'}first, filter: ${'$'}filter) { + edges { + node { + id + priceUSD + liquidity + priceChangeDay + } + } + } + } + """ + + val REFERRER_REWARDS_QUERY = """ + query ReferrerRewards(${'$'}first: Int!, ${'$'}filter: ReferrerRewardFilter) { + referrerRewards(first: ${'$'}first, filter: ${'$'}filter) { + edges { node { referral amount } } + } + } + """ + + val POOL_APY_QUERY = """ + query PoolApys(${'$'}first: Int!) { + poolXYKs(first: ${'$'}first) { + edges { node { id strategicBonusApy } } + } + } + """ + + fun accountHistoryFilter(address: String): JsonObject = + orFilter( + fieldFilter("dataFrom", "equalTo", address), + fieldFilter("dataTo", "equalTo", address), + fieldFilter("address", "equalTo", address), + ) + + fun orFilter(vararg filters: JsonObject): JsonObject = + buildJsonObject { + put( + "or", + buildJsonArray { + filters.forEach(::add) + } + ) + } + + fun fieldFilter(field: String, operator: String, value: String): JsonObject = + buildJsonObject { + put( + field, + buildJsonObject { + put(operator, value) + } + ) + } + + fun fieldFilter(field: String, operator: String, values: List): JsonObject = + buildJsonObject { + put( + field, + buildJsonObject { + put( + operator, + buildJsonArray { + values.forEach { value -> add(JsonPrimitive(value)) } + } + ) + } + ) + } + } +} diff --git a/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/data/SoraConfigManager.kt b/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/data/SoraConfigManager.kt index 769dff4c9..3c2fb78f7 100644 --- a/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/data/SoraConfigManager.kt +++ b/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/data/SoraConfigManager.kt @@ -32,6 +32,7 @@ USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package jp.co.soramitsu.feature_blockexplorer_api.data +import java.net.URLEncoder import javax.inject.Inject import javax.inject.Singleton import jp.co.soramitsu.androidfoundation.format.addHexPrefix @@ -51,6 +52,21 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.serializer import kotlinx.serialization.json.Json +internal fun buildTransactionExplorerUrl( + blockExplorerUrl: String, + txHash: String, +): String? { + val trimmedHash = txHash.trim() + if (trimmedHash.isEmpty()) return null + + val baseUrl = blockExplorerUrl.trim() + .ifEmpty { OptionsProvider.blockExplorerUrl } + .trimEnd('/') + val encodedHash = URLEncoder.encode(trimmedHash, Charsets.UTF_8.name()) + + return "$baseUrl/sorav2?tab=extrinsics&q=$encodedHash" +} + @Singleton class SoraConfigManager @Inject constructor( private val json: Json, @@ -107,7 +123,8 @@ class SoraConfigManager @Inject constructor( } return@CachingFactory SoraConfig( - blockExplorerUrl = commonConfig.subquery, + blockExplorerUrl = OptionsProvider.blockExplorerUrl, + indexerUrl = OptionsProvider.polkaswapIndexerEndpoint, blockExplorerType = blockExplorerType, nodes = nodes, genesis = commonConfig.genesis, @@ -164,6 +181,20 @@ class SoraConfigManager @Inject constructor( soraConfigFactory.nullableValue(EmptyArgs) ?.substrateTypesUrl.orEmpty() + suspend fun getBlockExplorerUrl(): String = + soraConfigFactory.nullableValue(EmptyArgs) + ?.blockExplorerUrl ?: OptionsProvider.blockExplorerUrl + + suspend fun getTransactionExplorerUrl(txHash: String): String? = + buildTransactionExplorerUrl( + blockExplorerUrl = getBlockExplorerUrl(), + txHash = txHash + ) + + suspend fun getIndexerUrl(): String = + soraConfigFactory.nullableValue(EmptyArgs) + ?.indexerUrl ?: OptionsProvider.polkaswapIndexerEndpoint + private suspend fun getCurrencies(): List = soraConfigFactory.nullableValue(EmptyArgs) ?.currencies ?: listOf(DEFAULT_SORA_CURRENCY) @@ -181,8 +212,6 @@ class SoraConfigManager @Inject constructor( @Serializable private data class ConfigDto( - @SerialName("SUBQUERY_ENDPOINT") - val subquery: String, @SerialName("DEFAULT_NETWORKS") val nodes: List, @SerialName("CHAIN_GENESIS_HASH") diff --git a/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/data/models/SoraConfig.kt b/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/data/models/SoraConfig.kt index bb16de167..4c5047cbb 100644 --- a/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/data/models/SoraConfig.kt +++ b/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/data/models/SoraConfig.kt @@ -2,6 +2,7 @@ package jp.co.soramitsu.feature_blockexplorer_api.data.models data class SoraConfig( val blockExplorerUrl: String, + val indexerUrl: String, val blockExplorerType: ConfigExplorerType, val nodes: List, val genesis: String, diff --git a/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/BasicTxDetails.kt b/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/BasicTxDetails.kt index 8aa4eb7b8..214667d8c 100644 --- a/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/BasicTxDetails.kt +++ b/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/BasicTxDetails.kt @@ -78,6 +78,7 @@ fun BasicTxDetails( amountContent: @Composable BoxScope.() -> Unit, onCloseClick: () -> Unit, onCopy: (String) -> Unit, + onOpenExplorer: (String) -> Unit, ) { Column( modifier = modifier.padding(vertical = Dimens.x1_5, horizontal = Dimens.x2), @@ -225,6 +226,18 @@ fun BasicTxDetails( ) } } + state.explorerUrl?.let { explorerUrl -> + BleachedButton( + size = Size.Large, + modifier = Modifier + .testTagAsId("ViewOnExplorerButton") + .fillMaxWidth() + .padding(bottom = Dimens.x1), + order = Order.SECONDARY, + text = stringResource(id = R.string.tx_details_view_on_explorer), + onClick = { onOpenExplorer(explorerUrl) }, + ) + } BleachedButton( size = Size.Large, modifier = Modifier @@ -274,7 +287,8 @@ private fun PreviewBasicTxDetails() { state = previewBasicTxDetailsItem, imageContent = { Text(text = "imageContent") }, amountContent = { Text(text = "amountContent") }, - onCloseClick = { /*TODO*/ }, + onCloseClick = {}, onCopy = {}, + onOpenExplorer = {}, ) } diff --git a/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/BasicTxDetailsState.kt b/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/BasicTxDetailsState.kt index e24055a4f..142738d31 100644 --- a/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/BasicTxDetailsState.kt +++ b/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/BasicTxDetailsState.kt @@ -48,6 +48,7 @@ data class BasicTxDetailsState( @DrawableRes val txTypeIcon: Int, val txTypeTitle: String, val txTypeSubTitle: String? = null, + val explorerUrl: String? = null, ) data class BasicTxDetailsItem( diff --git a/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/TxDetailsDemeterStake.kt b/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/TxDetailsDemeterStake.kt index 7f3283784..cd57782d1 100644 --- a/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/TxDetailsDemeterStake.kt +++ b/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/TxDetailsDemeterStake.kt @@ -64,6 +64,7 @@ fun TxDetailsDemeterStake( icon3: String, onCloseClick: () -> Unit, onCopyClick: (String) -> Unit, + onOpenExplorerClick: (String) -> Unit, ) { BasicTxDetails( modifier = modifier, @@ -116,6 +117,7 @@ fun TxDetailsDemeterStake( }, onCloseClick = onCloseClick, onCopy = onCopyClick, + onOpenExplorer = onOpenExplorerClick, ) } @@ -135,5 +137,6 @@ private fun PreviewTxDetailsAddLiquidity() { icon3 = DEFAULT_ICON_URI, onCopyClick = {}, onCloseClick = {}, + onOpenExplorerClick = {}, ) } diff --git a/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/TxDetailsLiquidity.kt b/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/TxDetailsLiquidity.kt index f3fe9e10a..db4ed14ff 100644 --- a/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/TxDetailsLiquidity.kt +++ b/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/TxDetailsLiquidity.kt @@ -66,6 +66,7 @@ fun TxDetailsLiquidity( icon2: String, onCloseClick: () -> Unit, onCopyClick: (String) -> Unit, + onOpenExplorerClick: (String) -> Unit, ) { BasicTxDetails( modifier = modifier, @@ -123,6 +124,7 @@ fun TxDetailsLiquidity( }, onCloseClick = onCloseClick, onCopy = onCopyClick, + onOpenExplorer = onOpenExplorerClick, ) } @@ -141,5 +143,6 @@ private fun PreviewTxDetailsAddLiquidity() { icon2 = DEFAULT_ICON_URI, onCopyClick = {}, onCloseClick = {}, + onOpenExplorerClick = {}, ) } diff --git a/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/TxDetailsReferralAndTransfer.kt b/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/TxDetailsReferralAndTransfer.kt index c611d7f3e..0dfba4cb3 100644 --- a/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/TxDetailsReferralAndTransfer.kt +++ b/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/TxDetailsReferralAndTransfer.kt @@ -57,6 +57,7 @@ fun TxDetailsReferralOrTransferScreen( amount: String, onCloseClick: () -> Unit, onCopyClick: (String) -> Unit, + onOpenExplorerClick: (String) -> Unit, ) { BasicTxDetails( modifier = modifier, @@ -80,6 +81,7 @@ fun TxDetailsReferralOrTransferScreen( }, onCloseClick = onCloseClick, onCopy = onCopyClick, + onOpenExplorer = onOpenExplorerClick, ) } @@ -96,5 +98,6 @@ fun PreviewReferrerTxCard() { icon = DEFAULT_ICON_URI, onCloseClick = {}, onCopyClick = {}, + onOpenExplorerClick = {}, ) } diff --git a/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/TxDetailsSwap.kt b/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/TxDetailsSwap.kt index 559688a05..cedb5914d 100644 --- a/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/TxDetailsSwap.kt +++ b/feature_blockexplorer_api/src/main/java/jp/co/soramitsu/feature_blockexplorer_api/presentation/txdetails/TxDetailsSwap.kt @@ -65,6 +65,7 @@ fun TxDetailsSwap( icon2: String, onCloseClick: () -> Unit, onCopyClick: (String) -> Unit, + onOpenExplorerClick: (String) -> Unit, ) { BasicTxDetails( modifier = modifier, @@ -120,6 +121,7 @@ fun TxDetailsSwap( }, onCloseClick = onCloseClick, onCopy = onCopyClick, + onOpenExplorer = onOpenExplorerClick, ) } @@ -137,5 +139,6 @@ private fun PreviewTxSwapDetails() { icon2 = DEFAULT_ICON_URI, onCopyClick = {}, onCloseClick = {}, + onOpenExplorerClick = {}, ) } diff --git a/feature_blockexplorer_api/src/test/java/jp/co/soramitsu/feature_blockexplorer_api/data/PolkaswapIndexerClientTest.kt b/feature_blockexplorer_api/src/test/java/jp/co/soramitsu/feature_blockexplorer_api/data/PolkaswapIndexerClientTest.kt new file mode 100644 index 000000000..f9cd55087 --- /dev/null +++ b/feature_blockexplorer_api/src/test/java/jp/co/soramitsu/feature_blockexplorer_api/data/PolkaswapIndexerClientTest.kt @@ -0,0 +1,401 @@ +package jp.co.soramitsu.feature_blockexplorer_api.data + +import com.google.common.truth.Truth.assertThat +import jp.co.soramitsu.common.domain.OptionsProvider +import jp.co.soramitsu.xnetworking.lib.engines.rest.api.RestClient +import jp.co.soramitsu.xnetworking.lib.engines.rest.api.models.AbstractRestServerRequest +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import org.junit.Test + +class PolkaswapIndexerClientTest { + + private val restClient = CapturingRestClient() + private val client = PolkaswapIndexerClient(restClient, testJson) + + @Test + fun `transaction history maps loose json values and sends account filter`() = runTest { + restClient.enqueue( + """ + { + "data": { + "historyElements": { + "pageInfo": { "hasNextPage": false }, + "edges": [ + { + "node": { + "id": "tx-1", + "timestamp": 123, + "blockHash": "block-1", + "module": "Assets", + "method": "transfer", + "address": "issuer", + "dataFrom": "from-peer", + "dataTo": "to-peer", + "networkFee": "9", + "execution": { "success": false, "extra": "ignored" }, + "data": { + "from": "from-peer", + "to": "to-peer", + "amount": 42, + "assetId": "asset-1", + "nullValue": null, + "objectValue": { "nested": true } + }, + "calls": { + "nodes": [ + { + "module": "PoolXYK", + "method": "deposit_liquidity", + "data": { + "input_a_desired": "7", + "objectParam": { "a": 1 } + } + } + ] + } + } + } + ] + } + } + } + """ + ) + + val result = client.getTransactionHistory( + address = "cnAccount", + page = 3, + pageCount = 25, + ) + + assertThat(result.endReached).isTrue() + assertThat(result.items).hasSize(1) + val item = result.items.single() + assertThat(item.id).isEqualTo("tx-1") + assertThat(item.blockHash).isEqualTo("block-1") + assertThat(item.address).isEqualTo("issuer") + assertThat(item.dataFrom).isEqualTo("from-peer") + assertThat(item.dataTo).isEqualTo("to-peer") + assertThat(item.timestamp).isEqualTo("123") + assertThat(item.networkFee).isEqualTo("9") + assertThat(item.success).isFalse() + assertThat(item.data).contains(IndexerHistoryItemParam("amount", "42")) + assertThat(item.data).contains(IndexerHistoryItemParam("nullValue", "")) + assertThat(item.data?.first { it.paramName == "objectValue" }?.paramValue) + .contains("\"nested\":true") + assertThat(item.nestedData).hasSize(1) + assertThat(item.nestedData?.single()?.data?.first { it.paramName == "objectParam" }?.paramValue) + .contains("\"a\":1") + + val request = restClient.singleRequest() + assertThat(request.url).isEqualTo(OptionsProvider.polkaswapIndexerEndpoint) + assertThat(request.contentType).isEqualTo(RestClient.ContentType.JSON) + assertThat(request.query).contains("historyElements") + assertThat(request.variables.toString()).contains("\"first\":25") + assertThat(request.variables.toString()).contains("\"offset\":50") + assertThat(request.variables.toString()).contains("\"dataFrom\":{\"equalTo\":\"cnAccount\"}") + assertThat(request.variables.toString()).contains("\"dataTo\":{\"equalTo\":\"cnAccount\"}") + assertThat(request.variables.toString()).contains("\"address\":{\"equalTo\":\"cnAccount\"}") + } + + @Test + fun `transaction history clamps zero and negative pages to zero offset`() = runTest { + restClient.enqueue(emptyHistoryResponse(hasNextPage = true)) + + val result = client.getTransactionHistory( + address = "cnAccount", + page = -5, + pageCount = 10, + ) + + assertThat(result.endReached).isFalse() + assertThat(result.items).isEmpty() + assertThat(restClient.singleRequest().variables.toString()).contains("\"offset\":0") + } + + @Test + fun `blank transaction peer query skips network`() = runTest { + val result = client.getTransactionPeers(" \n\t ") + + assertThat(result).isEmpty() + assertThat(restClient.requests).isEmpty() + } + + @Test + fun `transaction peers include dataFrom dataTo address and from-to params without duplicates`() = runTest { + restClient.enqueue( + """ + { + "data": { + "historyElements": { + "pageInfo": { "hasNextPage": false }, + "edges": [ + { "node": { "id": "1", "timestamp": 1, "dataFrom": "AliceABC", "dataTo": "", "address": "", "data": {} } }, + { "node": { "id": "2", "timestamp": 1, "dataFrom": "miss", "dataTo": "xxABCxx", "address": "xxABCxx", "data": {} } }, + { "node": { "id": "3", "timestamp": 1, "dataFrom": "AliceABC", "dataTo": "", "address": null, "data": { "from": "otherABC", "to": "no-match" } } }, + { "node": { "id": "4", "timestamp": 1, "address": null, "data": { "from": "abcDEF", "memo": "abc should not be used" } } } + ] + } + } + } + """ + ) + + val result = client.getTransactionPeers("abc") + + assertThat(result).containsExactly( + "AliceABC", + "xxABCxx", + "otherABC", + "abcDEF", + ) + val request = restClient.singleRequest() + assertThat(request.variables.toString()).contains("\"includesInsensitive\":\"abc\"") + } + + @Test + fun `transaction peers ignore matches from unrelated data fields`() = runTest { + restClient.enqueue( + """ + { + "data": { + "historyElements": { + "pageInfo": { "hasNextPage": false }, + "edges": [ + { "node": { "id": "1", "timestamp": 1, "address": null, "data": { "memo": "abc", "amount": "abc" } } } + ] + } + } + } + """ + ) + + assertThat(client.getTransactionPeers("abc")).isEmpty() + } + + @Test + fun `transaction lookup uses exact hash filter`() = runTest { + restClient.enqueue(emptyHistoryResponse(hasNextPage = false)) + + assertThat(client.getTransaction("0xhash")).isEmpty() + + val request = restClient.singleRequest() + assertThat(request.variables.toString()).contains("\"id\":{\"equalTo\":\"0xhash\"}") + assertThat(request.variables.toString()).contains("\"first\":1") + } + + @Test + fun `assets info skips network for empty ids`() = runTest { + assertThat(client.getAssetsInfo(emptyList())).isEmpty() + assertThat(restClient.requests).isEmpty() + } + + @Test + fun `assets info sends id in filter and decodes null market fields`() = runTest { + restClient.enqueue( + """ + { + "data": { + "assets": { + "edges": [ + { "node": { "id": "asset-1", "priceUSD": null, "liquidity": "100", "priceChangeDay": null } }, + { "node": { "id": "asset-2", "priceUSD": "2.5", "liquidity": null, "priceChangeDay": -4.5 } } + ] + } + } + } + """ + ) + + val result = client.getAssetsInfo(listOf("asset-1", "asset-2")) + + assertThat(result).containsExactly( + IndexerAssetInfo("asset-1", null, "100", null), + IndexerAssetInfo("asset-2", "2.5", null, -4.5), + ).inOrder() + val variables = restClient.singleRequest().variables.toString() + assertThat(variables).contains("\"first\":2") + assertThat(variables).contains("\"id\":{\"in\":[\"asset-1\",\"asset-2\"]}") + } + + @Test + fun `fiat query sends null filter with default connection limit`() = runTest { + restClient.enqueue( + """ + { "data": { "assets": { "edges": [ + { "node": { "id": "xor", "priceUSD": "1", "liquidity": "2", "priceChangeDay": 3.0 } } + ] } } } + """ + ) + + assertThat(client.getFiat()).containsExactly(IndexerAssetInfo("xor", "1", "2", 3.0)) + + val variables = restClient.singleRequest().variables.toString() + assertThat(variables).contains("\"first\":500") + assertThat(variables).contains("\"filter\":null") + } + + @Test + fun `referral rewards coerce missing referral and amount to empty strings`() = runTest { + restClient.enqueue( + """ + { "data": { "referrerRewards": { "edges": [ + { "node": { "referral": null, "amount": null } }, + { "node": { "referral": "cnReferral", "amount": "123" } } + ] } } } + """ + ) + + assertThat(client.getReferralRewards("cnReferrer")).containsExactly( + IndexerReferralReward("", ""), + IndexerReferralReward("cnReferral", "123"), + ).inOrder() + assertThat(restClient.singleRequest().variables.toString()) + .contains("\"referrer\":{\"equalTo\":\"cnReferrer\"}") + } + + @Test + fun `pool apys allow null strategic bonus apy`() = runTest { + restClient.enqueue( + """ + { "data": { "poolXYKs": { "edges": [ + { "node": { "id": "pool-1", "strategicBonusApy": null } }, + { "node": { "id": "pool-2", "strategicBonusApy": "0.123" } } + ] } } } + """ + ) + + assertThat(client.getPoolApys()).containsExactly( + IndexerPoolApy("pool-1", null), + IndexerPoolApy("pool-2", "0.123"), + ).inOrder() + } + + @Test + fun `graphql errors fail with all returned messages`() = runTest { + restClient.enqueue( + """ + { + "errors": [ + { "message": "bad filter" }, + { "message": "schema rejected request" } + ] + } + """ + ) + + val error = runCatching { + client.getFiat() + }.exceptionOrNull() + + assertThat(error).isInstanceOf(IllegalStateException::class.java) + assertThat(error).hasMessageThat().contains("bad filter") + assertThat(error).hasMessageThat().contains("schema rejected request") + } + + @Test + fun `missing graphql data fails instead of returning defaults`() = runTest { + restClient.enqueue("""{ "data": null }""") + + val error = runCatching { + client.getPoolApys() + }.exceptionOrNull() + + assertThat(error).isInstanceOf(IllegalStateException::class.java) + } + + @Test + fun `non object history data is treated as absent params`() = runTest { + restClient.enqueue( + """ + { + "data": { + "historyElements": { + "pageInfo": { "hasNextPage": false }, + "edges": [ + { "node": { "id": "array-data", "timestamp": 1, "data": ["unexpected"], "calls": { "nodes": [ { "data": ["unexpected"] } ] } } } + ] + } + } + } + """ + ) + + val item = client.getLastTransactions("cnAccount", 1).single() + + assertThat(item.data).isNull() + assertThat(item.nestedData).hasSize(1) + assertThat(item.nestedData?.single()?.data).isEmpty() + } + + private fun emptyHistoryResponse(hasNextPage: Boolean) = + """ + { + "data": { + "historyElements": { + "pageInfo": { "hasNextPage": $hasNextPage }, + "edges": [] + } + } + } + """ + + private data class CapturedGraphQlRequest( + val url: String, + val contentType: RestClient.ContentType, + val query: String, + val variables: JsonObject, + ) + + private class CapturingRestClient : RestClient() { + val requests = mutableListOf() + private val responses = ArrayDeque() + + fun enqueue(response: String) { + responses.addLast(response.trimIndent()) + } + + fun singleRequest(): CapturedGraphQlRequest = requests.single() + + override suspend fun post(request: AbstractRestServerRequest.WithBody): T { + requests += CapturedGraphQlRequest( + url = request.url, + contentType = request.requestContentType, + query = request.body.readField("query") as String, + variables = request.body.readField("variables") as JsonObject, + ) + + val response = if (responses.isEmpty()) { + error("No queued response for ${request.body.readField("query")}") + } else { + responses.removeFirst() + } + + return testJson.decodeFromString(request.responseDeserializer, response) + } + + override suspend fun postAsString(request: AbstractRestServerRequest.WithBody): String = + error("postAsString is not used by PolkaswapIndexerClient") + + override suspend fun get(request: AbstractRestServerRequest): T = + error("get is not used by PolkaswapIndexerClient") + + override suspend fun getAsString(request: AbstractRestServerRequest): String = + error("getAsString is not used by PolkaswapIndexerClient") + } + + private companion object { + val testJson = Json { + isLenient = true + ignoreUnknownKeys = true + } + + fun Any.readField(name: String): Any? { + val field = javaClass.getDeclaredField(name) + field.isAccessible = true + return field.get(this) + } + } +} diff --git a/feature_blockexplorer_api/src/test/java/jp/co/soramitsu/feature_blockexplorer_api/data/SoraConfigManagerTest.kt b/feature_blockexplorer_api/src/test/java/jp/co/soramitsu/feature_blockexplorer_api/data/SoraConfigManagerTest.kt new file mode 100644 index 000000000..8480aa0ef --- /dev/null +++ b/feature_blockexplorer_api/src/test/java/jp/co/soramitsu/feature_blockexplorer_api/data/SoraConfigManagerTest.kt @@ -0,0 +1,48 @@ +package jp.co.soramitsu.feature_blockexplorer_api.data + +import com.google.common.truth.Truth.assertThat +import jp.co.soramitsu.common.domain.OptionsProvider +import org.junit.Test + +class SoraConfigManagerTest { + + @Test + fun `transaction explorer url uses default block explorer base`() { + val result = buildTransactionExplorerUrl( + blockExplorerUrl = OptionsProvider.blockExplorerUrl, + txHash = "0xabc" + ) + + assertThat(result).isEqualTo("https://sorametrics.org/sorav2?tab=extrinsics&q=0xabc") + } + + @Test + fun `transaction explorer url removes trailing slash from base`() { + val result = buildTransactionExplorerUrl( + blockExplorerUrl = "https://sorametrics.org/", + txHash = "0xabc" + ) + + assertThat(result).isEqualTo("https://sorametrics.org/sorav2?tab=extrinsics&q=0xabc") + } + + @Test + fun `transaction explorer url returns null for blank hash`() { + val result = buildTransactionExplorerUrl( + blockExplorerUrl = OptionsProvider.blockExplorerUrl, + txHash = " " + ) + + assertThat(result).isNull() + } + + @Test + fun `transaction explorer url encodes hash query value`() { + val result = buildTransactionExplorerUrl( + blockExplorerUrl = " https://sorametrics.org/ ", + txHash = " 0xabc+ /?& " + ) + + assertThat(result).isEqualTo("https://sorametrics.org/sorav2?tab=extrinsics&q=0xabc%2B+%2F%3F%26") + } +} diff --git a/feature_blockexplorer_api/src/test/java/jp/co/soramitsu/feature_blockexplorer_api/data/SoraMetricsLiveContractTest.kt b/feature_blockexplorer_api/src/test/java/jp/co/soramitsu/feature_blockexplorer_api/data/SoraMetricsLiveContractTest.kt new file mode 100644 index 000000000..be8573f22 --- /dev/null +++ b/feature_blockexplorer_api/src/test/java/jp/co/soramitsu/feature_blockexplorer_api/data/SoraMetricsLiveContractTest.kt @@ -0,0 +1,200 @@ +package jp.co.soramitsu.feature_blockexplorer_api.data + +import com.google.common.truth.Truth.assertThat +import java.net.HttpURLConnection +import java.net.URL +import org.junit.Assume.assumeTrue +import org.junit.Test + +class SoraMetricsLiveContractTest { + + @Test + fun `sorametrics tx deep link responds`() { + assumeLiveExplorerTestsEnabled() + + val response = get("https://sorametrics.org/sorav2?tab=extrinsics&q=0x123") + + assertThat(response.code).isIn(200..299) + assertThat(response.body).contains("SoraMetrics") + } + + @Test + fun `polkaswap indexer graphql responds`() { + assumeLiveExplorerTestsEnabled() + + val response = postGraphQl("""query { __typename }""") + + assertThat(response.code).isIn(200..299) + assertThat(response.body).contains("__typename") + } + + @Test + fun `polkaswap indexer supports history query shape`() { + assumeLiveExplorerTestsEnabled() + + val response = postGraphQl( + """ + query { + historyElements(first: 1, offset: 0, orderBy: [TIMESTAMP_DESC, ID_DESC]) { + pageInfo { hasNextPage } + edges { + node { + id + timestamp + blockHash + module + method + address + dataFrom + dataTo + networkFee + execution + data + calls { nodes { module method data } } + } + } + } + } + """.trimIndent() + ) + + assertGraphQlDataResponse(response, "historyElements") + } + + @Test + fun `polkaswap indexer supports assets query shape`() { + assumeLiveExplorerTestsEnabled() + + val response = postGraphQl( + """ + query { + assets(first: 1) { + edges { + node { + id + priceUSD + liquidity + priceChangeDay + } + } + } + } + """.trimIndent() + ) + + assertGraphQlDataResponse(response, "assets") + } + + @Test + fun `polkaswap indexer supports referrer rewards query shape`() { + assumeLiveExplorerTestsEnabled() + + val response = postGraphQl( + """ + query { + referrerRewards(first: 1) { + edges { node { referral amount } } + } + } + """.trimIndent() + ) + + assertGraphQlDataResponse(response, "referrerRewards") + } + + @Test + fun `polkaswap indexer supports pool apy query shape`() { + assumeLiveExplorerTestsEnabled() + + val response = postGraphQl( + """ + query { + poolXYKs(first: 1) { + edges { node { id strategicBonusApy } } + } + } + """.trimIndent() + ) + + assertGraphQlDataResponse(response, "poolXYKs") + } + + private fun assumeLiveExplorerTestsEnabled() { + assumeTrue( + System.getenv("RUN_LIVE_EXPLORER_TESTS") == "true" || + System.getenv("CI") == "true" || + System.getenv("JENKINS_URL").isNullOrBlank().not() + ) + } + + private fun assertGraphQlDataResponse( + response: HttpResponse, + rootField: String, + ) { + assertThat(response.code).isIn(200..299) + assertThat(response.body).contains("data") + assertThat(response.body).contains(rootField) + assertThat(response.body).doesNotContain("errors") + } + + private fun get(url: String): HttpResponse { + val connection = URL(url).openConnection() as HttpURLConnection + connection.connectTimeout = 10_000 + connection.readTimeout = 10_000 + + return connection.readResponse() + } + + private fun post(url: String, body: String): HttpResponse { + val connection = URL(url).openConnection() as HttpURLConnection + connection.requestMethod = "POST" + connection.connectTimeout = 10_000 + connection.readTimeout = 10_000 + connection.doOutput = true + connection.setRequestProperty("Content-Type", "application/json") + connection.outputStream.use { output -> + output.write(body.toByteArray(Charsets.UTF_8)) + } + + return connection.readResponse() + } + + private fun postGraphQl(query: String): HttpResponse = + post( + url = "https://pi.soramitsu.io/graphql", + body = """{"query":${query.toJsonString()}}""" + ) + + private fun String.toJsonString(): String = + buildString { + append('"') + this@toJsonString.forEach { char -> + when (char) { + '\\' -> append("""\\""") + '"' -> append("""\"""") + '\n' -> append("""\n""") + '\r' -> append("""\r""") + '\t' -> append("""\t""") + else -> append(char) + } + } + append('"') + } + + private fun HttpURLConnection.readResponse(): HttpResponse = + try { + val responseBody = runCatching { + inputStream.bufferedReader().use { it.readText() } + }.getOrElse { + errorStream?.bufferedReader()?.use { reader -> reader.readText() }.orEmpty() + } + HttpResponse(responseCode, responseBody) + } finally { + disconnect() + } + + private data class HttpResponse( + val code: Int, + val body: String, + ) +} diff --git a/feature_blockexplorer_impl/build.gradle.kts b/feature_blockexplorer_impl/build.gradle.kts index fa79f3a3b..c6903bc93 100644 --- a/feature_blockexplorer_impl/build.gradle.kts +++ b/feature_blockexplorer_impl/build.gradle.kts @@ -1,22 +1,21 @@ plugins { alias(libs.plugins.androidLibrary) alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.composeCompiler) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } -val composeCompilerVersion: String by project - kotlin { jvmToolchain(17) } android { namespace = "jp.co.soramitsu.feature_blockexplorer_impl" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -26,7 +25,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -43,11 +42,6 @@ android { viewBinding = true compose = true } - - composeOptions { - kotlinCompilerExtensionVersion = composeCompilerVersion - } - flavorDimensions += listOf("default") productFlavors { @@ -117,13 +111,12 @@ dependencies { implementation(libs.timberDep) implementation(libs.lifecycleProcessDep) - kapt(libs.lifecycleKaptDep) implementation(libs.navigationFragmentDep) implementation(libs.navigationUiDep) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) testImplementation(project(":test_data")) testImplementation(libs.coroutineTestDep) diff --git a/feature_blockexplorer_impl/src/main/java/jp/co/soramitsu/feature_blockexplorer_impl/data/TransactionHistoryRepositoryImpl.kt b/feature_blockexplorer_impl/src/main/java/jp/co/soramitsu/feature_blockexplorer_impl/data/TransactionHistoryRepositoryImpl.kt index 0c98aa661..234c67f4b 100644 --- a/feature_blockexplorer_impl/src/main/java/jp/co/soramitsu/feature_blockexplorer_impl/data/TransactionHistoryRepositoryImpl.kt +++ b/feature_blockexplorer_impl/src/main/java/jp/co/soramitsu/feature_blockexplorer_impl/data/TransactionHistoryRepositoryImpl.kt @@ -37,7 +37,9 @@ import javax.inject.Singleton import jp.co.soramitsu.androidfoundation.coroutine.SuspendableProperty import jp.co.soramitsu.common.account.SoraAccount import jp.co.soramitsu.common.domain.Token -import jp.co.soramitsu.feature_blockexplorer_api.data.SoraConfigManager +import jp.co.soramitsu.common.logger.FirebaseWrapper +import jp.co.soramitsu.feature_blockexplorer_api.data.IndexerHistoryElement +import jp.co.soramitsu.feature_blockexplorer_api.data.PolkaswapIndexerClient import jp.co.soramitsu.feature_blockexplorer_api.data.TransactionHistoryRepository import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.Transaction import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.TransactionStatus @@ -45,19 +47,15 @@ import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.Transact import jp.co.soramitsu.sora.substrate.runtime.Pallete import jp.co.soramitsu.sora.substrate.runtime.SubstrateOptionsProvider import jp.co.soramitsu.sora.substrate.substrate.ExtrinsicManager -import jp.co.soramitsu.xnetworking.lib.datasources.txhistory.api.TxHistoryRepository -import jp.co.soramitsu.xnetworking.lib.datasources.txhistory.api.models.ChainInfo -import jp.co.soramitsu.xnetworking.lib.datasources.txhistory.api.models.TxFilter -import jp.co.soramitsu.xnetworking.lib.datasources.txhistory.api.models.TxHistoryItem import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.debounce -@OptIn(ExperimentalCoroutinesApi::class) +@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) @Singleton class TransactionHistoryRepositoryImpl @Inject constructor( - private val txHistoryRepository: TxHistoryRepository, + private val polkaswapIndexerClient: PolkaswapIndexerClient, extrinsicManager: ExtrinsicManager, - private val soraConfigManager: SoraConfigManager, ) : TransactionHistoryRepository { init { @@ -73,7 +71,12 @@ class TransactionHistoryRepositoryImpl @Inject constructor( } override suspend fun getContacts(query: String): Set = - txHistoryRepository.getTransactionPeers(query, soraConfigManager.getGenesis()).toSet() + runCatching { + polkaswapIndexerClient.getTransactionPeers(query) + }.getOrElse { + FirebaseWrapper.recordException(it) + emptySet() + } private fun updateTransactionStatus(txHash: String, status: Boolean, block: String?) { localPendingTransactions[txHash]?.base?.status = @@ -88,11 +91,15 @@ class TransactionHistoryRepositoryImpl @Inject constructor( count: Int, filterTokenId: String?, ): List { - val tx = txHistoryRepository.getTransactionHistoryCached( - count = count, - address = soraAccount.substrateAddress, - chainId = soraConfigManager.getGenesis(), - ).filter { + val tx = runCatching { + polkaswapIndexerClient.getLastTransactions( + address = soraAccount.substrateAddress, + count = count, + ) + }.getOrElse { + FirebaseWrapper.recordException(it) + emptyList() + }.filter { if (filterTokenId != null) return@filter isReferral(it, filterTokenId) @@ -114,17 +121,17 @@ class TransactionHistoryRepositoryImpl @Inject constructor( tokens: List, soraAccount: SoraAccount ): Transaction? { - val tx = txHistoryRepository.getTransactionCached( - address = soraAccount.substrateAddress, - txHash = txHash, - chainId = soraConfigManager.getGenesis(), - ) - val transaction = - mapHistoryItemsToTransactions( - tx.items, - soraAccount.substrateAddress, - tokens - ).firstOrNull() + val tx = runCatching { + polkaswapIndexerClient.getTransaction(txHash) + }.getOrElse { + FirebaseWrapper.recordException(it) + emptyList() + } + val transaction = mapHistoryItemsToTransactions( + tx, + soraAccount.substrateAddress, + tokens + ).firstOrNull() val local = localPendingTransactions[txHash] return transaction ?: local } @@ -135,14 +142,22 @@ class TransactionHistoryRepositoryImpl @Inject constructor( soraAccount: SoraAccount, filterTokenId: String?, ): TransactionsInfo { - val historyInfo = - txHistoryRepository.getTransactionHistoryPaged( + val historyInfo = runCatching { + polkaswapIndexerClient.getTransactionHistory( address = soraAccount.substrateAddress, page = page, - chainInfo = ChainInfo.Simple(soraConfigManager.getGenesis()), pageCount = 100, - filters = TxFilter.entries.toSet() ) + }.getOrElse { + FirebaseWrapper.recordException(it) + return TransactionsInfo( + buildList { + if (page == 1L) addAll(filterLocalPendingTx(filterTokenId).sortedByDescending { tx -> tx.base.timestamp }) + }, + endReached = true, + errorMessage = it.message, + ) + } val referralTransactions = historyInfo.items .filter { item -> @@ -169,7 +184,6 @@ class TransactionHistoryRepositoryImpl @Inject constructor( addAll(referralTransactions) }, historyInfo.endReached, - historyInfo.errorMessage, ) } @@ -180,7 +194,7 @@ class TransactionHistoryRepositoryImpl @Inject constructor( _state.set(true) } - private fun isReferral(item: TxHistoryItem, tokenId: String): Boolean { + private fun isReferral(item: IndexerHistoryElement, tokenId: String): Boolean { val hasTokenIdInMainParams = item.data?.find { it.paramValue == tokenId diff --git a/feature_blockexplorer_impl/src/main/java/jp/co/soramitsu/feature_blockexplorer_impl/data/TransactionMapper.kt b/feature_blockexplorer_impl/src/main/java/jp/co/soramitsu/feature_blockexplorer_impl/data/TransactionMapper.kt index fae140509..e71009f06 100644 --- a/feature_blockexplorer_impl/src/main/java/jp/co/soramitsu/feature_blockexplorer_impl/data/TransactionMapper.kt +++ b/feature_blockexplorer_impl/src/main/java/jp/co/soramitsu/feature_blockexplorer_impl/data/TransactionMapper.kt @@ -41,6 +41,8 @@ import jp.co.soramitsu.common.domain.getByIdOrEmpty import jp.co.soramitsu.common.util.BuildUtils import jp.co.soramitsu.common.util.ext.snakeCaseToCamelCase import jp.co.soramitsu.common.util.mapBalance +import jp.co.soramitsu.feature_blockexplorer_api.data.IndexerHistoryElement +import jp.co.soramitsu.feature_blockexplorer_api.data.IndexerHistoryItemParam import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.DemeterType import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.Transaction import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.TransactionBase @@ -50,11 +52,9 @@ import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.Transact import jp.co.soramitsu.sora.substrate.runtime.Method import jp.co.soramitsu.sora.substrate.runtime.Pallete import jp.co.soramitsu.sora.substrate.runtime.SubstrateOptionsProvider -import jp.co.soramitsu.xnetworking.lib.datasources.txhistory.api.models.TxHistoryItem -import jp.co.soramitsu.xnetworking.lib.datasources.txhistory.api.models.TxHistoryItemParam fun mapHistoryItemsToTransactions( - txs: List, + txs: List, myAddress: String, tokens: List, ): List { @@ -67,7 +67,7 @@ fun mapHistoryItemsToTransactions( } private fun mapHistoryItemToTransaction( - tx: TxHistoryItem, + tx: IndexerHistoryElement, myAddress: String, tokens: List, feePrecision: Int, @@ -234,10 +234,10 @@ private fun mapHistoryItemToTransaction( return transaction } -private fun TxHistoryItem.getSuccess(): TransactionStatus = +private fun IndexerHistoryElement.getSuccess(): TransactionStatus = if (this.success) TransactionStatus.COMMITTED else TransactionStatus.REJECTED -private fun TxHistoryItem.getTimestamp(): Long = +private fun IndexerHistoryElement.getTimestamp(): Long = (this.timestamp.substringBefore(".").toLongOrNull() ?: 0) * 1000 private fun String.toBigDecimalOrDefault(v: BigDecimal = BigDecimal.ZERO): BigDecimal = @@ -246,21 +246,21 @@ private fun String.toBigDecimalOrDefault(v: BigDecimal = BigDecimal.ZERO): BigDe private fun String.toBigIntegerOrDefault(v: BigInteger = BigInteger.ZERO): BigInteger = runCatching { BigInteger(this) }.getOrDefault(v) -private fun List.toReferralBond(block: (amount: String) -> Transaction.ReferralBond): Transaction.ReferralBond? { +private fun List.toReferralBond(block: (amount: String) -> Transaction.ReferralBond): Transaction.ReferralBond? { val amount = this.firstOrNull { it.paramName == "amount" } return if (amount != null) { block.invoke(amount.paramValue) } else null } -private fun List.toReferralUnbond(block: (amount: String) -> Transaction.ReferralUnbond): Transaction.ReferralUnbond? { +private fun List.toReferralUnbond(block: (amount: String) -> Transaction.ReferralUnbond): Transaction.ReferralUnbond? { val amount = this.firstOrNull { it.paramName == "amount" } return if (amount != null) { block.invoke(amount.paramValue) } else null } -private fun List.toReferralSetReferrer( +private fun List.toReferralSetReferrer( myAddress: String, block: (String, Boolean) -> Transaction.ReferralSetReferrer ): Transaction.ReferralSetReferrer? { @@ -272,7 +272,7 @@ private fun List.toReferralSetReferrer( } else null } -private fun List.toAdarIncome(my: String, block: (amount: String, token: String, peer: String) -> Transaction.AdarIncome): Transaction.AdarIncome? { +private fun List.toAdarIncome(my: String, block: (amount: String, token: String, peer: String) -> Transaction.AdarIncome): Transaction.AdarIncome? { val to = this.firstOrNull { it.paramName == "to" } val from = this.firstOrNull { it.paramName == "from" } val token = this.firstOrNull { it.paramName == "assetId" } @@ -282,7 +282,7 @@ private fun List.toAdarIncome(my: String, block: (amount: St else null } -private fun List.toDemeterStake(block: (amount: String, base: String, target: String, reward: String) -> Transaction.DemeterFarming): Transaction.DemeterFarming? { +private fun List.toDemeterStake(block: (amount: String, base: String, target: String, reward: String) -> Transaction.DemeterFarming): Transaction.DemeterFarming? { val base = this.firstOrNull { it.paramName == "baseAssetId" } val target = this.firstOrNull { it.paramName == "assetId" } val reward = this.firstOrNull { it.paramName == "rewardAssetId" } @@ -292,7 +292,7 @@ private fun List.toDemeterStake(block: (amount: String, base else null } -private fun List.toDemeterReward(block: (amount: String, base: String) -> Transaction.DemeterFarming): Transaction.DemeterFarming? { +private fun List.toDemeterReward(block: (amount: String, base: String) -> Transaction.DemeterFarming): Transaction.DemeterFarming? { val base = this.firstOrNull { it.paramName == "assetId" } val amount = this.firstOrNull { it.paramName == "amount" } return if (base != null && amount != null) @@ -300,7 +300,7 @@ private fun List.toDemeterReward(block: (amount: String, bas else null } -private fun List.toTransfer(block: (to: String, from: String, amount: String, tokenId: String) -> Transaction.Transfer): Transaction.Transfer? { +private fun List.toTransfer(block: (to: String, from: String, amount: String, tokenId: String) -> Transaction.Transfer): Transaction.Transfer? { val to = this.firstOrNull { it.paramName == "to" } val from = this.firstOrNull { it.paramName == "from" } val amount = this.firstOrNull { it.paramName == "amount" } @@ -310,7 +310,7 @@ private fun List.toTransfer(block: (to: String, from: String else null } -private fun List.toEthTransfer(block: (amount: String, tokenId: String, hash: String, address: String) -> Transaction.EthTransfer): Transaction.EthTransfer? { +private fun List.toEthTransfer(block: (amount: String, tokenId: String, hash: String, address: String) -> Transaction.EthTransfer): Transaction.EthTransfer? { val amount = this.firstOrNull { it.paramName == "amount" } val tokenId = this.firstOrNull { it.paramName == "assetId" } val requestHash = this.firstOrNull { it.paramName == "requestHash" } @@ -320,7 +320,7 @@ private fun List.toEthTransfer(block: (amount: String, token else null } -private fun List.toSwap( +private fun List.toSwap( block: ( selectedMarket: String, baseTokenId: String, @@ -346,7 +346,7 @@ private fun List.toSwap( ) else null } -private fun List.toLiquidity( +private fun List.toLiquidity( block: ( baseTokenId: String, targetTokenId: String, @@ -369,7 +369,7 @@ private fun List.toLiquidity( ) else null } -private fun List.toLiquidityBatch( +private fun List.toLiquidityBatch( block: ( baseTokenId: String, targetTokenId: String, @@ -392,7 +392,7 @@ private fun List.toLiquidityBatch( ) else null } -private fun TxHistoryItem.isMatch(pallet: Pallete, method: Method): Boolean = +private fun IndexerHistoryElement.isMatch(pallet: Pallete, method: Method): Boolean = this.module.lowercase() == pallet.palletName.lowercase() && this.method.isMatch(method) private fun String.isMatch(method: Method): Boolean { diff --git a/feature_blockexplorer_impl/src/main/java/jp/co/soramitsu/feature_blockexplorer_impl/di/FeatureBlockExplorerModule.kt b/feature_blockexplorer_impl/src/main/java/jp/co/soramitsu/feature_blockexplorer_impl/di/FeatureBlockExplorerModule.kt index afd2d7198..ccfec622d 100644 --- a/feature_blockexplorer_impl/src/main/java/jp/co/soramitsu/feature_blockexplorer_impl/di/FeatureBlockExplorerModule.kt +++ b/feature_blockexplorer_impl/src/main/java/jp/co/soramitsu/feature_blockexplorer_impl/di/FeatureBlockExplorerModule.kt @@ -32,11 +32,9 @@ USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package jp.co.soramitsu.feature_blockexplorer_impl.di -import android.content.Context import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton import jp.co.soramitsu.feature_blockexplorer_api.data.TransactionHistoryRepository @@ -47,38 +45,11 @@ import jp.co.soramitsu.feature_blockexplorer_impl.data.TransactionHistoryReposit import jp.co.soramitsu.feature_blockexplorer_impl.domain.TransactionBuilderImpl import jp.co.soramitsu.feature_blockexplorer_impl.domain.TransactionHistoryHandlerImpl import jp.co.soramitsu.feature_blockexplorer_impl.presentation.txhistory.TransactionMappersImpl -import jp.co.soramitsu.xnetworking.lib.datasources.chainsconfig.api.ConfigDAO -import jp.co.soramitsu.xnetworking.lib.datasources.txhistory.api.HistoryItemsFilter -import jp.co.soramitsu.xnetworking.lib.datasources.txhistory.api.TxHistoryRepository -import jp.co.soramitsu.xnetworking.lib.datasources.txhistory.api.models.TxHistoryItem -import jp.co.soramitsu.xnetworking.lib.datasources.txhistory.impl.TxHistoryRepositoryImpl -import jp.co.soramitsu.xnetworking.lib.datasources.txhistory.impl.builder.ExpectActualDBDriverFactory -import jp.co.soramitsu.xnetworking.lib.engines.rest.api.RestClient @Module @InstallIn(SingletonComponent::class) class FeatureBlockExplorerModule { - private companion object { - const val txHistoryDBName = "historyDatabase.db" - } - - @Singleton - @Provides - fun provideTxHistoryRepository( - @ApplicationContext context: Context, - configDAO: ConfigDAO, - restClient: RestClient, - ): TxHistoryRepository = TxHistoryRepositoryImpl( - databaseDriverFactory = ExpectActualDBDriverFactory(context, txHistoryDBName), - configDAO = configDAO, - restClient = restClient, - historyItemsFilter = object : HistoryItemsFilter { - override fun List.filterCachedHistoryItems(): List = this - override fun List.filterPagedHistoryItems(): List = this - } - ) - @Singleton @Provides fun provideTransactionHistoryRepository( diff --git a/feature_blockexplorer_impl/src/main/java/jp/co/soramitsu/feature_blockexplorer_impl/presentation/txdetails/TxDetailsFragment.kt b/feature_blockexplorer_impl/src/main/java/jp/co/soramitsu/feature_blockexplorer_impl/presentation/txdetails/TxDetailsFragment.kt index 56064b85b..583c58128 100644 --- a/feature_blockexplorer_impl/src/main/java/jp/co/soramitsu/feature_blockexplorer_impl/presentation/txdetails/TxDetailsFragment.kt +++ b/feature_blockexplorer_impl/src/main/java/jp/co/soramitsu/feature_blockexplorer_impl/presentation/txdetails/TxDetailsFragment.kt @@ -53,6 +53,7 @@ import jp.co.soramitsu.common.base.theOnlyRoute import jp.co.soramitsu.common.domain.BottomBarController import jp.co.soramitsu.common.domain.DEFAULT_ICON_URI import jp.co.soramitsu.common.presentation.args.txHash +import jp.co.soramitsu.common.util.ShareUtil import jp.co.soramitsu.feature_blockexplorer_api.presentation.txdetails.TxDetailsDemeterStake import jp.co.soramitsu.feature_blockexplorer_api.presentation.txdetails.TxDetailsLiquidity import jp.co.soramitsu.feature_blockexplorer_api.presentation.txdetails.TxDetailsReferralOrTransferScreen @@ -97,6 +98,7 @@ class TxDetailsFragment : SoraBaseFragment() { icon2 = state.icon2 ?: DEFAULT_ICON_URI, onCloseClick = ::onBack, onCopyClick = viewModel::onCopyClicked, + onOpenExplorerClick = ::onOpenExplorerClicked, ) } TxType.DEMETER -> { @@ -113,6 +115,7 @@ class TxDetailsFragment : SoraBaseFragment() { icon3 = state.icon3 ?: DEFAULT_ICON_URI, onCloseClick = ::onBack, onCopyClick = viewModel::onCopyClicked, + onOpenExplorerClick = ::onOpenExplorerClicked, ) } TxType.REFERRAL_TRANSFER -> { @@ -126,6 +129,7 @@ class TxDetailsFragment : SoraBaseFragment() { isAmountGreen = state.isAmountGreen, onCloseClick = ::onBack, onCopyClick = viewModel::onCopyClicked, + onOpenExplorerClick = ::onOpenExplorerClicked, ) } TxType.SWAP -> { @@ -140,10 +144,15 @@ class TxDetailsFragment : SoraBaseFragment() { icon2 = state.icon2 ?: DEFAULT_ICON_URI, onCloseClick = ::onBack, onCopyClick = viewModel::onCopyClicked, + onOpenExplorerClick = ::onOpenExplorerClicked, ) } } } } } + + private fun onOpenExplorerClicked(url: String) { + ShareUtil.shareInBrowser(requireContext(), url) + } } diff --git a/feature_blockexplorer_impl/src/main/java/jp/co/soramitsu/feature_blockexplorer_impl/presentation/txdetails/TxDetailsViewModel.kt b/feature_blockexplorer_impl/src/main/java/jp/co/soramitsu/feature_blockexplorer_impl/presentation/txdetails/TxDetailsViewModel.kt index f90223bd1..8b4956f69 100644 --- a/feature_blockexplorer_impl/src/main/java/jp/co/soramitsu/feature_blockexplorer_impl/presentation/txdetails/TxDetailsViewModel.kt +++ b/feature_blockexplorer_impl/src/main/java/jp/co/soramitsu/feature_blockexplorer_impl/presentation/txdetails/TxDetailsViewModel.kt @@ -51,6 +51,7 @@ import jp.co.soramitsu.common.domain.printFiat import jp.co.soramitsu.common.presentation.viewmodel.BaseViewModel import jp.co.soramitsu.common.util.NumbersFormatter import jp.co.soramitsu.feature_assets_api.domain.AssetsInteractor +import jp.co.soramitsu.feature_blockexplorer_api.data.SoraConfigManager import jp.co.soramitsu.feature_blockexplorer_api.domain.TransactionHistoryHandler import jp.co.soramitsu.feature_blockexplorer_api.presentation.txdetails.BasicTxDetailsItem import jp.co.soramitsu.feature_blockexplorer_api.presentation.txdetails.BasicTxDetailsState @@ -81,6 +82,7 @@ class TxDetailsViewModel @AssistedInject constructor( private val resourceManager: ResourceManager, private val dateTimeFormatter: DateTimeFormatter, private val numbersFormatter: NumbersFormatter, + private val soraConfigManager: SoraConfigManager, @Assisted private val txHash: String, ) : BaseViewModel() { @@ -117,7 +119,10 @@ class TxDetailsViewModel @AssistedInject constructor( val currentAddress = assetsInteractor.getCurSoraAccount().substrateAddress val feeToken = walletInteractor.getFeeToken() val transaction = transactionHistoryHandler.getTransaction(txHash) - _txDetailsScreenState.value = when (transaction) { + val explorerUrl = transaction?.base?.txHash?.let { + soraConfigManager.getTransactionExplorerUrl(it) + } + val screenState = when (transaction) { is Transaction.EthTransfer -> { TxDetailsScreenState( basicTxDetailsState = BasicTxDetailsState( @@ -549,6 +554,11 @@ class TxDetailsViewModel @AssistedInject constructor( null -> emptyTxDetailsState } + _txDetailsScreenState.value = explorerUrl?.let { + screenState.copy( + basicTxDetailsState = screenState.basicTxDetailsState.copy(explorerUrl = it) + ) + } ?: screenState } fun onCopyClicked(text: String) { diff --git a/feature_blockexplorer_impl/src/test/java/jp/co/soramitsu/feature_blockexplorer_impl/data/TransactionHistoryRepositoryTest.kt b/feature_blockexplorer_impl/src/test/java/jp/co/soramitsu/feature_blockexplorer_impl/data/TransactionHistoryRepositoryTest.kt index 02e57a327..42ecc3b15 100644 --- a/feature_blockexplorer_impl/src/test/java/jp/co/soramitsu/feature_blockexplorer_impl/data/TransactionHistoryRepositoryTest.kt +++ b/feature_blockexplorer_impl/src/test/java/jp/co/soramitsu/feature_blockexplorer_impl/data/TransactionHistoryRepositoryTest.kt @@ -38,17 +38,18 @@ import io.mockk.coEvery import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.junit4.MockKRule +import io.mockk.mockkObject import io.mockk.mockkStatic import io.mockk.verify import jp.co.soramitsu.androidfoundation.testing.MainCoroutineRule -import jp.co.soramitsu.feature_blockexplorer_api.data.SoraConfigManager +import jp.co.soramitsu.common.logger.FirebaseWrapper +import jp.co.soramitsu.feature_blockexplorer_api.data.PolkaswapIndexerClient import jp.co.soramitsu.feature_blockexplorer_api.data.TransactionHistoryRepository import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.Transaction import jp.co.soramitsu.feature_blockexplorer_impl.testdata.TestTransactions import jp.co.soramitsu.sora.substrate.substrate.ExtrinsicManager import jp.co.soramitsu.test_data.TestAccounts import jp.co.soramitsu.test_data.TestTokens -import jp.co.soramitsu.xnetworking.lib.datasources.txhistory.api.TxHistoryRepository import junit.framework.TestCase.assertEquals import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -75,10 +76,7 @@ class TransactionHistoryRepositoryTest { val mockkRule = MockKRule(this) @MockK - private lateinit var txHistoryRepository: TxHistoryRepository - - @MockK - private lateinit var soraConfigManager: SoraConfigManager + private lateinit var polkaswapIndexerClient: PolkaswapIndexerClient @MockK private lateinit var extrinsicManager: ExtrinsicManager @@ -92,32 +90,26 @@ class TransactionHistoryRepositoryTest { @Before fun setUp() = runTest { mockkStatic(Uri::parse) + mockkObject(FirebaseWrapper) every { Uri.parse(any()) } returns mockedUri + every { FirebaseWrapper.recordException(any()) } returns Unit every { extrinsicManager.setWatchingExtrinsicListener( listener = any() ) } returns Unit - coEvery { soraConfigManager.getGenesis() } returns "7e4e" - every { - txHistoryRepository.getTransactionPeers( - query = "query", - chainId = "7e4e", - ) - } returns peersList + coEvery { polkaswapIndexerClient.getTransactionPeers("query") } returns peersList.toSet() coEvery { - txHistoryRepository.getTransactionHistoryCached( + polkaswapIndexerClient.getLastTransactions( address = TestAccounts.soraAccount.substrateAddress, count = 1, - chainId = "7e4e", ) } returns listOf(TestTransactions.txHistoryItem) transactionHistoryRepository = TransactionHistoryRepositoryImpl( - txHistoryRepository, + polkaswapIndexerClient, extrinsicManager, - soraConfigManager, ) } @@ -152,4 +144,79 @@ class TransactionHistoryRepositoryTest { } } } + + @Test + fun `getContacts returns empty set when indexer fails`() = runTest { + coEvery { + polkaswapIndexerClient.getTransactionPeers("bad query") + } throws IllegalStateException("indexer down") + + val result = transactionHistoryRepository.getContacts("bad query") + + assertEquals(emptySet(), result) + verify { FirebaseWrapper.recordException(any()) } + } + + @Test + fun `getLastTransactions keeps local pending transactions when indexer fails`() = runTest { + coEvery { + polkaswapIndexerClient.getLastTransactions( + address = TestAccounts.soraAccount.substrateAddress, + count = 10, + ) + } throws IllegalStateException("history unavailable") + transactionHistoryRepository.saveTransaction(TestTransactions.sendSuccessfulTx) + + val result = transactionHistoryRepository.getLastTransactions( + TestAccounts.soraAccount, + listOf(TestTokens.xorToken, TestTokens.valToken), + 10, + null, + ) + + assertEquals(1, result.size) + assertEquals(TestTransactions.sendSuccessfulTx.base.txHash, result.single().base.txHash) + verify { FirebaseWrapper.recordException(any()) } + } + + @Test + fun `getTransaction falls back to local pending transaction when indexer fails`() = runTest { + coEvery { + polkaswapIndexerClient.getTransaction(TestTransactions.sendSuccessfulTx.base.txHash) + } throws IllegalStateException("tx unavailable") + transactionHistoryRepository.saveTransaction(TestTransactions.sendSuccessfulTx) + + val result = transactionHistoryRepository.getTransaction( + TestTransactions.sendSuccessfulTx.base.txHash, + listOf(TestTokens.xorToken, TestTokens.valToken), + TestAccounts.soraAccount, + ) + + assertEquals(TestTransactions.sendSuccessfulTx.base.txHash, result?.base?.txHash) + verify { FirebaseWrapper.recordException(any()) } + } + + @Test + fun `getTransactionHistory returns first page pending transactions and error message when indexer fails`() = runTest { + coEvery { + polkaswapIndexerClient.getTransactionHistory( + address = TestAccounts.soraAccount.substrateAddress, + page = 1, + pageCount = 100, + ) + } throws IllegalStateException("history unavailable") + transactionHistoryRepository.saveTransaction(TestTransactions.sendSuccessfulTx) + + val result = transactionHistoryRepository.getTransactionHistory( + page = 1, + tokens = listOf(TestTokens.xorToken, TestTokens.valToken), + soraAccount = TestAccounts.soraAccount, + filterTokenId = null, + ) + + assertEquals(true, result.endReached) + assertEquals("history unavailable", result.errorMessage) + assertEquals(TestTransactions.sendSuccessfulTx.base.txHash, result.transactions.single().base.txHash) + verify { FirebaseWrapper.recordException(any()) } + } } diff --git a/feature_blockexplorer_impl/src/test/java/jp/co/soramitsu/feature_blockexplorer_impl/data/TransactionMapperAdversarialTest.kt b/feature_blockexplorer_impl/src/test/java/jp/co/soramitsu/feature_blockexplorer_impl/data/TransactionMapperAdversarialTest.kt new file mode 100644 index 000000000..ed1436494 --- /dev/null +++ b/feature_blockexplorer_impl/src/test/java/jp/co/soramitsu/feature_blockexplorer_impl/data/TransactionMapperAdversarialTest.kt @@ -0,0 +1,366 @@ +package jp.co.soramitsu.feature_blockexplorer_impl.data + +import com.google.common.truth.Truth.assertThat +import java.math.BigDecimal +import jp.co.soramitsu.common.domain.Market +import jp.co.soramitsu.common.domain.Token +import jp.co.soramitsu.feature_blockexplorer_api.data.IndexerHistoryElement +import jp.co.soramitsu.feature_blockexplorer_api.data.IndexerHistoryItemParam +import jp.co.soramitsu.feature_blockexplorer_api.data.IndexerNestedHistoryItem +import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.DemeterType +import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.Transaction +import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.TransactionLiquidityType +import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.TransactionStatus +import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.TransactionTransferType +import jp.co.soramitsu.test_data.TestTokens +import org.junit.Test + +class TransactionMapperAdversarialTest { + + @Test + fun `transfer with invalid numeric values maps to zeroes and rejected status`() { + val result = mapSingle( + history( + module = "Assets", + method = "transfer", + timestamp = "not-a-time", + networkFee = "not-a-number", + success = false, + data = params( + "from" to MY_ADDRESS, + "to" to PEER_ADDRESS, + "amount" to "not-a-decimal", + "assetId" to TestTokens.valToken.id, + ), + ) + ) as Transaction.Transfer + + assertThat(result.base.fee).isEqualTo(BigDecimal.ZERO) + assertThat(result.base.timestamp).isEqualTo(0L) + assertThat(result.base.status).isEqualTo(TransactionStatus.REJECTED) + assertThat(result.amount).isEqualTo(BigDecimal.ZERO) + assertThat(result.peer).isEqualTo(PEER_ADDRESS) + assertThat(result.transferType).isEqualTo(TransactionTransferType.OUTGOING) + assertThat(result.token.id).isEqualTo(TestTokens.valToken.id) + } + + @Test + fun `incoming transfer is detected by recipient address`() { + val result = mapSingle( + history( + module = "assets", + method = "TRANSFER", + data = params( + "from" to PEER_ADDRESS, + "to" to MY_ADDRESS, + "amount" to "12.5", + "assetId" to TestTokens.xorToken.id, + ), + ) + ) as Transaction.Transfer + + assertThat(result.peer).isEqualTo(PEER_ADDRESS) + assertThat(result.transferType).isEqualTo(TransactionTransferType.INCOMING) + assertThat(result.amount).isEqualTo(BigDecimal("12.5")) + } + + @Test + fun `transfer with missing required params is ignored`() { + assertThat( + mapMany( + history( + module = "Assets", + method = "transfer", + data = params( + "from" to MY_ADDRESS, + "to" to PEER_ADDRESS, + "amount" to "1", + ), + ) + ) + ).isEmpty() + } + + @Test + fun `unknown module and method are ignored without throwing`() { + assertThat( + mapMany( + history( + module = "NotARealPallet", + method = "not_a_real_call", + networkFee = "999", + data = params("amount" to "1"), + ) + ) + ).isEmpty() + } + + @Test + fun `swap with unknown market falls back to smart market`() { + val result = mapSingle( + history( + module = "LiquidityProxy", + method = "swap", + data = params( + "selectedMarket" to "not-a-market", + "baseAssetId" to TestTokens.xorToken.id, + "targetAssetId" to TestTokens.valToken.id, + "baseAssetAmount" to "3", + "targetAssetAmount" to "9", + ), + ) + ) as Transaction.Swap + + assertThat(result.market).isEqualTo(Market.SMART) + assertThat(result.tokenFrom.id).isEqualTo(TestTokens.xorToken.id) + assertThat(result.tokenTo.id).isEqualTo(TestTokens.valToken.id) + assertThat(result.amountFrom).isEqualTo(BigDecimal("3")) + assertThat(result.amountTo).isEqualTo(BigDecimal("9")) + } + + @Test + fun `utility batch maps nested deposit liquidity and ignores unrelated top level data`() { + val result = mapSingle( + history( + module = "Utility", + method = "batch_all", + data = params("baseAssetAmount" to "should-not-be-used"), + nestedData = listOf( + nested( + method = "deposit_liquidity", + data = params( + "input_asset_a" to TestTokens.xorToken.id, + "input_asset_b" to TestTokens.valToken.id, + "input_a_desired" to "11", + "input_b_desired" to "22", + ) + ) + ), + ) + ) as Transaction.Liquidity + + assertThat(result.type).isEqualTo(TransactionLiquidityType.ADD) + assertThat(result.amount1).isEqualTo(BigDecimal("11")) + assertThat(result.amount2).isEqualTo(BigDecimal("22")) + } + + @Test + fun `utility batch maps nested withdraw liquidity when no deposit call exists`() { + val result = mapSingle( + history( + module = "Utility", + method = "batch", + nestedData = listOf( + nested( + method = "withdraw_liquidity", + data = params( + "input_asset_a" to TestTokens.xorToken.id, + "input_asset_b" to TestTokens.valToken.id, + "input_a_desired" to "4", + "input_b_desired" to "5", + ) + ) + ), + ) + ) as Transaction.Liquidity + + assertThat(result.type).isEqualTo(TransactionLiquidityType.WITHDRAW) + assertThat(result.amount1).isEqualTo(BigDecimal("4")) + assertThat(result.amount2).isEqualTo(BigDecimal("5")) + } + + @Test + fun `referral set referrer marks my referrer when current account sets another account`() { + val result = mapSingle( + history( + module = "Referrals", + method = "set_referrer", + data = params( + "from" to MY_ADDRESS, + "to" to PEER_ADDRESS, + ), + ) + ) as Transaction.ReferralSetReferrer + + assertThat(result.who).isEqualTo(PEER_ADDRESS) + assertThat(result.myReferrer).isTrue() + assertThat(result.token.id).isEqualTo(TestTokens.xorToken.id) + } + + @Test + fun `referral set referrer marks referred account when another account sets current account`() { + val result = mapSingle( + history( + module = "Referrals", + method = "set_referrer", + data = params( + "from" to PEER_ADDRESS, + "to" to MY_ADDRESS, + ), + ) + ) as Transaction.ReferralSetReferrer + + assertThat(result.who).isEqualTo(PEER_ADDRESS) + assertThat(result.myReferrer).isFalse() + } + + @Test + fun `referral reserve with invalid amount maps amount to zero`() { + val result = mapSingle( + history( + module = "Referrals", + method = "reserve", + data = params("amount" to "bad"), + ) + ) as Transaction.ReferralBond + + assertThat(result.amount).isEqualTo(BigDecimal.ZERO) + assertThat(result.token.id).isEqualTo(TestTokens.xorToken.id) + } + + @Test + fun `demeter reward uses reward asset as all displayed tokens`() { + val result = mapSingle( + history( + module = "DemeterFarmingPlatform", + method = "get_rewards", + data = params( + "assetId" to TestTokens.pswapToken.id, + "amount" to "8", + ), + ) + ) as Transaction.DemeterFarming + + assertThat(result.type).isEqualTo(DemeterType.REWARD) + assertThat(result.amount).isEqualTo(BigDecimal("8")) + assertThat(result.baseToken.id).isEqualTo(TestTokens.pswapToken.id) + assertThat(result.targetToken.id).isEqualTo(TestTokens.pswapToken.id) + assertThat(result.rewardToken.id).isEqualTo(TestTokens.pswapToken.id) + } + + @Test + fun `demeter stake with missing reward asset is ignored`() { + assertThat( + mapMany( + history( + module = "DemeterFarmingPlatform", + method = "deposit", + data = params( + "baseAssetId" to TestTokens.xorToken.id, + "assetId" to TestTokens.valToken.id, + "amount" to "1", + ), + ) + ) + ).isEmpty() + } + + @Test + fun `adar income maps only incoming swap transfer batch`() { + val incoming = mapSingle( + history( + module = "LiquidityProxy", + method = "swap_transfer_batch", + data = params( + "from" to PEER_ADDRESS, + "to" to MY_ADDRESS, + "assetId" to TestTokens.pswapToken.id, + "amount" to "6", + ), + ) + ) as Transaction.AdarIncome + + val outgoing = mapMany( + history( + module = "LiquidityProxy", + method = "swap_transfer_batch", + data = params( + "from" to MY_ADDRESS, + "to" to PEER_ADDRESS, + "assetId" to TestTokens.pswapToken.id, + "amount" to "6", + ), + ) + ) + + assertThat(incoming.peer).isEqualTo(PEER_ADDRESS) + assertThat(incoming.amount).isEqualTo(BigDecimal("6")) + assertThat(outgoing).isEmpty() + } + + @Test + fun `network fee is scaled with xor precision`() { + val result = mapSingle( + history( + module = "Assets", + method = "transfer", + networkFee = "1000000000000000000", + data = params( + "from" to MY_ADDRESS, + "to" to PEER_ADDRESS, + "amount" to "1", + "assetId" to TestTokens.valToken.id, + ), + ) + ) + + assertThat(result.base.fee).isEqualTo(BigDecimal.ONE) + } + + private fun mapSingle(item: IndexerHistoryElement): Transaction = + mapMany(item).single() + + private fun mapMany(item: IndexerHistoryElement): List = + mapHistoryItemsToTransactions( + txs = listOf(item), + myAddress = MY_ADDRESS, + tokens = tokens, + ) + + private fun history( + module: String, + method: String, + timestamp: String = "123.456", + networkFee: String = "0", + success: Boolean = true, + data: List? = null, + nestedData: List? = null, + ) = IndexerHistoryElement( + id = "tx-$module-$method", + blockHash = "block-$module-$method", + module = module, + method = method, + timestamp = timestamp, + networkFee = networkFee, + success = success, + data = data, + nestedData = nestedData, + ) + + private fun nested( + module: String = "PoolXYK", + method: String, + data: List, + ) = IndexerNestedHistoryItem( + module = module, + method = method, + data = data, + ) + + private fun params(vararg params: Pair): List = + params.map { (name, value) -> + IndexerHistoryItemParam(name, value) + } + + private companion object { + const val MY_ADDRESS = "cnMyAddress" + const val PEER_ADDRESS = "cnPeerAddress" + + val tokens: List = listOf( + TestTokens.xorToken, + TestTokens.valToken, + TestTokens.pswapToken, + TestTokens.ethToken, + ) + } +} diff --git a/feature_blockexplorer_impl/src/test/java/jp/co/soramitsu/feature_blockexplorer_impl/presentation/txdetails/TxDetailsViewModelTest.kt b/feature_blockexplorer_impl/src/test/java/jp/co/soramitsu/feature_blockexplorer_impl/presentation/txdetails/TxDetailsViewModelTest.kt index 28e171414..868f7bc30 100644 --- a/feature_blockexplorer_impl/src/test/java/jp/co/soramitsu/feature_blockexplorer_impl/presentation/txdetails/TxDetailsViewModelTest.kt +++ b/feature_blockexplorer_impl/src/test/java/jp/co/soramitsu/feature_blockexplorer_impl/presentation/txdetails/TxDetailsViewModelTest.kt @@ -38,6 +38,7 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.junit4.MockKRule import io.mockk.verify +import java.math.BigDecimal import java.util.Date import jp.co.soramitsu.androidfoundation.phone.BasicClipboardManager import jp.co.soramitsu.androidfoundation.resource.ResourceManager @@ -49,12 +50,16 @@ import jp.co.soramitsu.common.domain.DEFAULT_ICON_URI import jp.co.soramitsu.common.domain.printFiat import jp.co.soramitsu.common.util.NumbersFormatter import jp.co.soramitsu.feature_assets_api.domain.AssetsInteractor +import jp.co.soramitsu.feature_blockexplorer_api.data.SoraConfigManager import jp.co.soramitsu.feature_blockexplorer_api.domain.TransactionHistoryHandler import jp.co.soramitsu.feature_blockexplorer_api.presentation.txdetails.BasicTxDetailsItem import jp.co.soramitsu.feature_blockexplorer_api.presentation.txdetails.BasicTxDetailsState import jp.co.soramitsu.feature_blockexplorer_api.presentation.txdetails.TxDetailsScreenState import jp.co.soramitsu.feature_blockexplorer_api.presentation.txdetails.TxType +import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.DemeterType import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.Transaction +import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.TransactionBase +import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.TransactionStatus import jp.co.soramitsu.feature_blockexplorer_impl.testdata.TestTransactions import jp.co.soramitsu.feature_wallet_api.domain.interfaces.WalletInteractor import jp.co.soramitsu.feature_wallet_api.launcher.WalletRouter @@ -93,6 +98,9 @@ class TxDetailsViewModelTest { @MockK private lateinit var transactionHistoryHandler: TransactionHistoryHandler + @MockK + private lateinit var soraConfigManager: SoraConfigManager + @MockK private lateinit var clipboardManager: BasicClipboardManager @@ -114,14 +122,46 @@ class TxDetailsViewModelTest { private lateinit var viewModel: TxDetailsViewModel + private fun explorerUrl(txHash: String) = "https://sorametrics.org/sorav2?tab=extrinsics&q=$txHash" + + private fun base(hash: String) = TransactionBase( + txHash = hash, + blockHash = "blockHash", + fee = BigDecimal.ONE, + status = TransactionStatus.COMMITTED, + timestamp = 1673918013, + ) + + private suspend fun assertExplorerUrlFor(tx: Transaction) { + initTestData(tx) + delay(10) + + assertEquals(explorerUrl(tx.base.txHash), viewModel.txDetailsScreenState.value.basicTxDetailsState.explorerUrl) + } + private suspend fun initTestData(tx: Transaction = TestTransactions.sendFailedTx) { coEvery { transactionHistoryHandler.getTransaction(txHash) } returns tx + coEvery { + soraConfigManager.getTransactionExplorerUrl(tx.base.txHash) + } returns explorerUrl(tx.base.txHash) every { transactionHistoryHandler.flowLocalTransactions() } returns flowOf(true) every { resourceManager.getString(R.string.common_recipient) } returns "recipient" every { resourceManager.getString(R.string.common_sent) } returns "sent" + every { resourceManager.getString(R.string.common_bridged) } returns "bridged" + every { resourceManager.getString(R.string.asset_sora_fullname) } returns "SORA" + every { resourceManager.getString(R.string.asset_ether_fullname) } returns "Ethereum" + every { resourceManager.getString(R.string.eth_tx_address) } returns "Ethereum address" + every { resourceManager.getString(R.string.eth_tx_hash) } returns "Ethereum transaction hash" every { resourceManager.getString(R.string.polkaswap_swapped) } returns "swapped" every { resourceManager.getString(R.string.details_sent_to_pool) } returns "sent to pool" every { resourceManager.getString(R.string.details_receive_from_pool) } returns "received from pool" + every { resourceManager.getString(R.string.demeter_claimed_reward) } returns "claimed reward" + every { + resourceManager.getString(R.string.demeter_staked_liquidity) + } returns "staked liquidity" + every { + resourceManager.getString(R.string.demeter_unstaked_liquidity) + } returns "unstaked liquidity" every { resourceManager.getString(R.string.wallet_bonded) } returns "wallet bonded" every { resourceManager.getString(R.string.wallet_unbonded) } returns "wallet unbonded" every { resourceManager.getString(R.string.referrer_set) } returns "referrer set" @@ -129,6 +169,7 @@ class TxDetailsViewModelTest { every { resourceManager.getString(R.string.activity_referral_title) } returns "referrer join" every { resourceManager.getString(R.string.history_referral) } returns "referral" every { resourceManager.getString(R.string.common_received) } returns "received" + every { resourceManager.getString(R.string.received_from_adar) } returns "received from adar" every { resourceManager.getQuantityString(R.plurals.referral_invitations, 1) } returns "invitation" every { dateTimeFormatter.formatDate( @@ -146,6 +187,7 @@ class TxDetailsViewModelTest { resourceManager, dateTimeFormatter, nf, + soraConfigManager, txHash ) } @@ -178,7 +220,8 @@ class TxDetailsViewModelTest { null, null, R.drawable.ic_new_arrow_down_24, - "received" + "received", + explorerUrl = explorerUrl(base.txHash) ), "+${token.printBalance(amount, nf)}", null, @@ -214,7 +257,8 @@ class TxDetailsViewModelTest { TestTokens.xorToken.printBalance(base.fee, nf), TestTokens.xorToken.printFiat(base.fee, nf), R.drawable.ic_new_arrow_up_24, - "sent" + "sent", + explorerUrl = explorerUrl(base.txHash) ), token.printBalance(amount, nf), null, @@ -252,7 +296,8 @@ class TxDetailsViewModelTest { TestTokens.xorToken.printBalance(base.fee, nf), TestTokens.xorToken.printFiat(base.fee, nf), R.drawable.ic_new_arrow_up_24, - "sent" + "sent", + explorerUrl = explorerUrl(base.txHash) ), token.printBalance(amount, nf), null, @@ -290,7 +335,8 @@ class TxDetailsViewModelTest { TestTokens.xorToken.printBalance(base.fee, nf), TestTokens.xorToken.printFiat(base.fee, nf), R.drawable.ic_new_arrow_up_24, - "sent" + "sent", + explorerUrl = explorerUrl(base.txHash) ), token.printBalance(amount, nf), null, @@ -326,7 +372,8 @@ class TxDetailsViewModelTest { TestTokens.xorToken.printBalance(base.fee, nf), TestTokens.xorToken.printFiat(base.fee, nf), R.drawable.ic_refresh_24, - "swapped" + "swapped", + explorerUrl = explorerUrl(base.txHash) ), tokenFrom.printBalance(amountFrom, nf), tokenTo.printBalance(amountTo, nf), @@ -362,7 +409,8 @@ class TxDetailsViewModelTest { TestTokens.xorToken.printBalance(base.fee, nf), TestTokens.xorToken.printFiat(base.fee, nf), R.drawable.ic_new_arrow_down_24, - "wallet unbonded" + "wallet unbonded", + explorerUrl = explorerUrl(base.txHash) ), token.printBalance(amount, nf), null, @@ -403,7 +451,8 @@ class TxDetailsViewModelTest { null, null, R.drawable.ic_new_arrow_up_24, - "referrer set" + "referrer set", + explorerUrl = explorerUrl(base.txHash) ), "--", null, @@ -444,7 +493,8 @@ class TxDetailsViewModelTest { TestTokens.xorToken.printBalance(base.fee, nf), TestTokens.xorToken.printFiat(base.fee, nf), R.drawable.ic_new_arrow_up_24, - "referrer join" + "referrer join", + explorerUrl = explorerUrl(base.txHash) ), "-1 invitation", null, @@ -480,7 +530,8 @@ class TxDetailsViewModelTest { TestTokens.xorToken.printBalance(base.fee, nf), TestTokens.xorToken.printFiat(base.fee, nf), R.drawable.ic_new_arrow_up_24, - "wallet bonded" + "wallet bonded", + explorerUrl = explorerUrl(base.txHash) ), token.printBalance(amount, nf), null, @@ -516,7 +567,8 @@ class TxDetailsViewModelTest { TestTokens.xorToken.printBalance(base.fee, nf), TestTokens.xorToken.printFiat(base.fee, nf), R.drawable.ic_new_arrow_up_24, - "sent to pool" + "sent to pool", + explorerUrl = explorerUrl(base.txHash) ), token1.printBalance(amount1, nf), token2.printBalance(amount2, nf), @@ -552,7 +604,8 @@ class TxDetailsViewModelTest { TestTokens.xorToken.printBalance(base.fee, nf), TestTokens.xorToken.printFiat(base.fee, nf), R.drawable.ic_new_arrow_down_24, - "received from pool" + "received from pool", + explorerUrl = explorerUrl(base.txHash) ), "+${token1.printBalance(amount1, nf)}", "+${token2.printBalance(amount2, nf)}", @@ -570,6 +623,46 @@ class TxDetailsViewModelTest { assertEquals(expectedScreenState.toString(), viewModel.txDetailsScreenState.value.toString()) } + @Test + fun `bridge tx exposes explorer url`() = runTest { + assertExplorerUrlFor( + Transaction.EthTransfer( + base = base("bridgeHash"), + amount = BigDecimal.ONE, + token = TestTokens.xorToken, + ethToken = TestTokens.ethToken, + requestHash = "requestHash", + sidechainAddress = "0xabc", + ) + ) + } + + @Test + fun `demeter tx exposes explorer url`() = runTest { + assertExplorerUrlFor( + Transaction.DemeterFarming( + base = base("demeterHash"), + type = DemeterType.STAKE, + amount = BigDecimal.ONE, + baseToken = TestTokens.xorToken, + targetToken = TestTokens.valToken, + rewardToken = TestTokens.pswapToken, + ) + ) + } + + @Test + fun `adar income tx exposes explorer url`() = runTest { + assertExplorerUrlFor( + Transaction.AdarIncome( + base = base("adarHash"), + amount = BigDecimal.ONE, + peer = "cnPeer", + token = TestTokens.xorToken, + ) + ) + } + @Test fun `onCopyClicked(String) called`() = runTest { initTestData() diff --git a/feature_blockexplorer_impl/src/test/java/jp/co/soramitsu/feature_blockexplorer_impl/testdata/TestTransactions.kt b/feature_blockexplorer_impl/src/test/java/jp/co/soramitsu/feature_blockexplorer_impl/testdata/TestTransactions.kt index 4fa54d5be..5301046ef 100644 --- a/feature_blockexplorer_impl/src/test/java/jp/co/soramitsu/feature_blockexplorer_impl/testdata/TestTransactions.kt +++ b/feature_blockexplorer_impl/src/test/java/jp/co/soramitsu/feature_blockexplorer_impl/testdata/TestTransactions.kt @@ -34,6 +34,8 @@ package jp.co.soramitsu.feature_blockexplorer_impl.testdata import java.math.BigDecimal import jp.co.soramitsu.common.domain.Market +import jp.co.soramitsu.feature_blockexplorer_api.data.IndexerHistoryElement +import jp.co.soramitsu.feature_blockexplorer_api.data.IndexerHistoryItemParam import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.Transaction import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.TransactionBase import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.TransactionLiquidityType @@ -41,11 +43,9 @@ import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.Transact import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.TransactionTransferType import jp.co.soramitsu.test_data.TestAccounts import jp.co.soramitsu.test_data.TestTokens -import jp.co.soramitsu.xnetworking.lib.datasources.txhistory.api.models.TxHistoryItem -import jp.co.soramitsu.xnetworking.lib.datasources.txhistory.api.models.TxHistoryItemParam object TestTransactions { - val txHistoryItem = TxHistoryItem( + val txHistoryItem = IndexerHistoryElement( id = "0xb594da199715b4efd01aa59faa23607e68ce51ef5226dcfe2e17d436c58dd0d0", blockHash = "0x200335cb5a84a7d85b7d7a5ae8825c1a54b3aaf6c266fb977dcdd32774b5f560", module = "poolXYK", @@ -54,23 +54,23 @@ object TestTransactions { networkFee = "70000000000000000", success = true, data = listOf( - TxHistoryItemParam( + IndexerHistoryItemParam( paramName = "baseAssetAmount", paramValue = "0.009693118078249083" ), - TxHistoryItemParam( + IndexerHistoryItemParam( paramName = "baseAssetId", paramValue = "0x0200000000000000000000000000000000000000000000000000000000000000" ), - TxHistoryItemParam( + IndexerHistoryItemParam( paramName = "targetAssetAmount", paramValue = "4.365445441023082229" ), - TxHistoryItemParam( + IndexerHistoryItemParam( paramName = "targetAssetId", paramValue = "0x0200040000000000000000000000000000000000000000000000000000000000" ), - TxHistoryItemParam( + IndexerHistoryItemParam( paramName = "type", paramValue = "Deposit" ) diff --git a/feature_ecosystem_impl/build.gradle.kts b/feature_ecosystem_impl/build.gradle.kts index 1a8708856..f3f88778e 100644 --- a/feature_ecosystem_impl/build.gradle.kts +++ b/feature_ecosystem_impl/build.gradle.kts @@ -1,22 +1,21 @@ plugins { alias(libs.plugins.androidLibrary) alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.composeCompiler) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } -val composeCompilerVersion: String by project - kotlin { jvmToolchain(17) } android { namespace = "jp.co.soramitsu.feature_ecosystem_impl" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -26,7 +25,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -43,11 +42,6 @@ android { viewBinding = true compose = true } - - composeOptions { - kotlinCompilerExtensionVersion = composeCompilerVersion - } - flavorDimensions += listOf("default") productFlavors { @@ -109,13 +103,12 @@ dependencies { implementation(libs.fragmentKtxDep) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) implementation(libs.hiltWorkManagerDep) implementation(libs.hiltNavComposeDep) - kapt(libs.hiltWorkManagerKaptDep) + ksp(libs.hiltWorkManagerCompilerDep) implementation(libs.lifecycleProcessDep) - kapt(libs.lifecycleKaptDep) implementation(libs.uiCoreDep) implementation(libs.soramitsu.android.foundation) diff --git a/feature_ecosystem_impl/src/main/java/jp/co/soramitsu/feature_ecosystem_impl/presentation/allcurrencies/AllCurrenciesScreen.kt b/feature_ecosystem_impl/src/main/java/jp/co/soramitsu/feature_ecosystem_impl/presentation/allcurrencies/AllCurrenciesScreen.kt index aa2f545dd..88091bbce 100644 --- a/feature_ecosystem_impl/src/main/java/jp/co/soramitsu/feature_ecosystem_impl/presentation/allcurrencies/AllCurrenciesScreen.kt +++ b/feature_ecosystem_impl/src/main/java/jp/co/soramitsu/feature_ecosystem_impl/presentation/allcurrencies/AllCurrenciesScreen.kt @@ -48,7 +48,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import jp.co.soramitsu.common.presentation.compose.components.NothingFoundText import jp.co.soramitsu.common_wallet.presentation.compose.components.AssetItemEnumerated diff --git a/feature_ecosystem_impl/src/main/java/jp/co/soramitsu/feature_ecosystem_impl/presentation/alldemeter/AllDemeterScreen.kt b/feature_ecosystem_impl/src/main/java/jp/co/soramitsu/feature_ecosystem_impl/presentation/alldemeter/AllDemeterScreen.kt index 011853ed2..377d51276 100644 --- a/feature_ecosystem_impl/src/main/java/jp/co/soramitsu/feature_ecosystem_impl/presentation/alldemeter/AllDemeterScreen.kt +++ b/feature_ecosystem_impl/src/main/java/jp/co/soramitsu/feature_ecosystem_impl/presentation/alldemeter/AllDemeterScreen.kt @@ -49,7 +49,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import jp.co.soramitsu.common.presentation.compose.components.NothingFoundText import jp.co.soramitsu.common.util.StringTriple import jp.co.soramitsu.common_wallet.presentation.compose.BasicFarmListItem diff --git a/feature_ecosystem_impl/src/main/java/jp/co/soramitsu/feature_ecosystem_impl/presentation/allpools/AllPoolsScreen.kt b/feature_ecosystem_impl/src/main/java/jp/co/soramitsu/feature_ecosystem_impl/presentation/allpools/AllPoolsScreen.kt index 9c05cb384..7290ba6b5 100644 --- a/feature_ecosystem_impl/src/main/java/jp/co/soramitsu/feature_ecosystem_impl/presentation/allpools/AllPoolsScreen.kt +++ b/feature_ecosystem_impl/src/main/java/jp/co/soramitsu/feature_ecosystem_impl/presentation/allpools/AllPoolsScreen.kt @@ -54,7 +54,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import jp.co.soramitsu.common.presentation.compose.components.NothingFoundText import jp.co.soramitsu.common.util.StringPair diff --git a/feature_ethereum_api/build.gradle.kts b/feature_ethereum_api/build.gradle.kts index c2c14dc70..444e4332c 100644 --- a/feature_ethereum_api/build.gradle.kts +++ b/feature_ethereum_api/build.gradle.kts @@ -3,7 +3,7 @@ plugins { alias(libs.plugins.kotlinAndroid) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } @@ -14,7 +14,7 @@ kotlin { android { namespace = "jp.co.soramitsu.feature_ethereum_api" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -24,7 +24,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -57,7 +57,7 @@ android { dependencies { implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) implementation(libs.coroutineAndroidDep) implementation(libs.coroutineDep) diff --git a/feature_main_api/build.gradle.kts b/feature_main_api/build.gradle.kts index fd08046bb..9c96edd0a 100644 --- a/feature_main_api/build.gradle.kts +++ b/feature_main_api/build.gradle.kts @@ -3,7 +3,7 @@ plugins { alias(libs.plugins.kotlinAndroid) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } @@ -14,7 +14,7 @@ kotlin { android { namespace = "jp.co.soramitsu.feature_main_api" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -24,7 +24,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -60,5 +60,5 @@ dependencies { implementation(libs.navigationUiDep) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) } diff --git a/feature_main_impl/build.gradle.kts b/feature_main_impl/build.gradle.kts index b1ba382f3..2ff01c501 100644 --- a/feature_main_impl/build.gradle.kts +++ b/feature_main_impl/build.gradle.kts @@ -1,22 +1,21 @@ plugins { alias(libs.plugins.androidLibrary) alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.composeCompiler) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } -val composeCompilerVersion: String by project - kotlin { jvmToolchain(17) } android { namespace = "jp.co.soramitsu.feature_main_impl" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -26,7 +25,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -43,11 +42,6 @@ android { viewBinding = true compose = true } - - composeOptions { - kotlinCompilerExtensionVersion = composeCompilerVersion - } - flavorDimensions += listOf("default") productFlavors { @@ -101,6 +95,7 @@ dependencies { implementation(libs.soramitsu.android.foundation) implementation(libs.coreKtxDep) + implementation(libs.zXingEmbeddedDep) implementation(libs.navigationFragmentDep) implementation(libs.navigationUiDep) @@ -135,12 +130,11 @@ dependencies { implementation(libs.workManagerDep) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) implementation(libs.hiltWorkManagerDep) - kapt(libs.hiltWorkManagerKaptDep) + ksp(libs.hiltWorkManagerCompilerDep) implementation(libs.lifecycleProcessDep) - kapt(libs.lifecycleKaptDep) implementation(libs.insetterDep) diff --git a/feature_main_impl/src/main/java/jp/co/soramitsu/feature_main_impl/presentation/MainActivity.kt b/feature_main_impl/src/main/java/jp/co/soramitsu/feature_main_impl/presentation/MainActivity.kt index ee2202939..b29f88e50 100644 --- a/feature_main_impl/src/main/java/jp/co/soramitsu/feature_main_impl/presentation/MainActivity.kt +++ b/feature_main_impl/src/main/java/jp/co/soramitsu/feature_main_impl/presentation/MainActivity.kt @@ -64,6 +64,8 @@ import jp.co.soramitsu.common.util.DebounceClickHandler import jp.co.soramitsu.common.util.ext.attrColor import jp.co.soramitsu.common.util.ext.getColorAttr import jp.co.soramitsu.common.util.ext.gone +import jp.co.soramitsu.common.util.ext.setNavigationBarColorCompat +import jp.co.soramitsu.common.util.ext.setStatusBarColorCompat import jp.co.soramitsu.common.util.ext.show import jp.co.soramitsu.feature_main_api.domain.model.PinCodeAction import jp.co.soramitsu.feature_main_api.launcher.MainRouter @@ -291,8 +293,9 @@ class MainActivity : override fun setColor(@AttrRes color: Int) { curBarsColor = color - window.statusBarColor = attrColor(color) - window.navigationBarColor = attrColor(color) + val barColor = attrColor(color) + window.setStatusBarColorCompat(barColor) + window.setNavigationBarColorCompat(barColor) } private fun showBadConnectionView(@StringRes content: Int = R.string.common_connecting) { @@ -304,7 +307,7 @@ class MainActivity : animation.duration = ANIM_DURATION binding.badConnectionView.startAnimation(animation) binding.badConnectionView.show() - window.statusBarColor = errorColor + window.setStatusBarColorCompat(errorColor) } } @@ -313,7 +316,7 @@ class MainActivity : val successColor = binding.badConnectionView.getColorAttr(R.attr.statusSuccess) binding.badConnectionView.setText(content) binding.badConnectionView.setBackgroundColor(successColor) - window.statusBarColor = successColor + window.setStatusBarColorCompat(successColor) val animation = TranslateAnimation(0f, 0f, 0f, -ANIM_START_POSITION) animation.duration = ANIM_DURATION animation.startOffset = 500 @@ -323,7 +326,9 @@ class MainActivity : override fun onAnimationEnd(p0: Animation?) { binding.badConnectionView.gone() - window.statusBarColor = binding.badConnectionView.getColorAttr(curBarsColor) + window.setStatusBarColorCompat( + binding.badConnectionView.getColorAttr(curBarsColor) + ) } override fun onAnimationStart(p0: Animation?) { diff --git a/feature_main_impl/src/main/java/jp/co/soramitsu/feature_main_impl/presentation/pincode/PinCodeViewModel.kt b/feature_main_impl/src/main/java/jp/co/soramitsu/feature_main_impl/presentation/pincode/PinCodeViewModel.kt index 7c6087766..c69f9e2cd 100644 --- a/feature_main_impl/src/main/java/jp/co/soramitsu/feature_main_impl/presentation/pincode/PinCodeViewModel.kt +++ b/feature_main_impl/src/main/java/jp/co/soramitsu/feature_main_impl/presentation/pincode/PinCodeViewModel.kt @@ -616,7 +616,7 @@ class PinCodeViewModel @Inject constructor( override fun onFinish() { buttonsDisabled = false - showFingerPrintEventLiveData.value = true + showFingerPrintEventLiveData.value = isBiometryEnabled viewModelScope.launch { interactor.resetTimerStartedTimestamp() } state = state.copy(toolbarTitleString = resourceManager.getString(R.string.pincode_enter_pin_code)) diff --git a/feature_multiaccount_api/build.gradle.kts b/feature_multiaccount_api/build.gradle.kts index 180469fc3..46104cd87 100644 --- a/feature_multiaccount_api/build.gradle.kts +++ b/feature_multiaccount_api/build.gradle.kts @@ -3,7 +3,7 @@ plugins { alias(libs.plugins.kotlinAndroid) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } @@ -14,7 +14,7 @@ kotlin { android { namespace = "jp.co.soramitsu.feature_multiaccount_api" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -24,7 +24,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -71,5 +71,5 @@ dependencies { implementation(libs.navigationUiDep) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) } diff --git a/feature_multiaccount_impl/build.gradle.kts b/feature_multiaccount_impl/build.gradle.kts index 074fab84e..9069b2cbe 100644 --- a/feature_multiaccount_impl/build.gradle.kts +++ b/feature_multiaccount_impl/build.gradle.kts @@ -1,22 +1,24 @@ plugins { alias(libs.plugins.androidLibrary) alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.composeCompiler) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } -val composeCompilerVersion: String by project - kotlin { jvmToolchain(17) + compilerOptions { + freeCompilerArgs.add("-Xstring-concat=inline") + } } android { namespace = "jp.co.soramitsu.feature_multiaccount_impl" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -26,7 +28,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -39,14 +41,6 @@ android { } } - kotlinOptions { - freeCompilerArgs += listOf("-Xstring-concat=inline") - } - - composeOptions { - kotlinCompilerExtensionVersion = composeCompilerVersion - } - buildFeatures { viewBinding = true compose = true @@ -111,8 +105,6 @@ dependencies { implementation(libs.lifecycleProcessDep) - kapt(libs.lifecycleKaptDep) - implementation(libs.navigationFragmentDep) implementation(libs.navigationUiDep) @@ -135,7 +127,7 @@ dependencies { implementation(libs.daggerDep) implementation(libs.hiltNavComposeDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) testImplementation(libs.coroutineTestDep) testImplementation(libs.junitDep) diff --git a/feature_polkaswap_api/build.gradle.kts b/feature_polkaswap_api/build.gradle.kts index b63831b38..9758768e0 100644 --- a/feature_polkaswap_api/build.gradle.kts +++ b/feature_polkaswap_api/build.gradle.kts @@ -3,7 +3,7 @@ plugins { alias(libs.plugins.kotlinAndroid) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } @@ -14,7 +14,7 @@ kotlin { android { namespace = "jp.co.soramitsu.feature_polkaswap_api" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -24,7 +24,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -77,5 +77,5 @@ dependencies { implementation(libs.coroutineDep) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) } diff --git a/feature_polkaswap_impl/build.gradle.kts b/feature_polkaswap_impl/build.gradle.kts index 61f39b80f..de7913ee0 100644 --- a/feature_polkaswap_impl/build.gradle.kts +++ b/feature_polkaswap_impl/build.gradle.kts @@ -1,22 +1,21 @@ plugins { alias(libs.plugins.androidLibrary) alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.composeCompiler) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } -val composeCompilerVersion: String by project - kotlin { jvmToolchain(17) } android { namespace = "jp.co.soramitsu.feature_polkaswap_impl" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -26,7 +25,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -43,11 +42,6 @@ android { viewBinding = true compose = true } - - composeOptions { - kotlinCompilerExtensionVersion = composeCompilerVersion - } - flavorDimensions += listOf("default") productFlavors { @@ -117,12 +111,11 @@ dependencies { implementation(libs.fragmentKtxDep) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) implementation(libs.hiltWorkManagerDep) - kapt(libs.hiltWorkManagerKaptDep) + ksp(libs.hiltWorkManagerCompilerDep) implementation(libs.lifecycleProcessDep) - kapt(libs.lifecycleKaptDep) implementation(libs.roomDep) implementation(libs.roomKtxDep) diff --git a/feature_polkaswap_impl/src/main/java/jp/co/soramitsu/feature_polkaswap_impl/presentation/components/compose/SwapSlippageScreen.kt b/feature_polkaswap_impl/src/main/java/jp/co/soramitsu/feature_polkaswap_impl/presentation/components/compose/SwapSlippageScreen.kt index 7e18f25fd..dcb9840e6 100644 --- a/feature_polkaswap_impl/src/main/java/jp/co/soramitsu/feature_polkaswap_impl/presentation/components/compose/SwapSlippageScreen.kt +++ b/feature_polkaswap_impl/src/main/java/jp/co/soramitsu/feature_polkaswap_impl/presentation/components/compose/SwapSlippageScreen.kt @@ -57,13 +57,13 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.platform.LocalLocale import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import java.math.BigDecimal import java.text.DecimalFormatSymbols -import java.util.Locale import jp.co.soramitsu.common.R import jp.co.soramitsu.common.presentation.compose.theme.SoraAppTheme import jp.co.soramitsu.ui_core.component.button.FilledButton @@ -99,10 +99,11 @@ internal fun SwapSlippageScreen( FocusRequester() } + val currentLocale = LocalLocale.current.platformLocale val visualTransformation = - remember(Locale.getDefault()) { + remember(currentLocale) { CurrencyGroupingVisualTransformation( - decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault()), + decimalFormatSymbols = DecimalFormatSymbols(currentLocale), suffix = "%" ) } diff --git a/feature_referral_api/build.gradle.kts b/feature_referral_api/build.gradle.kts index 839137799..f1cac6ca9 100644 --- a/feature_referral_api/build.gradle.kts +++ b/feature_referral_api/build.gradle.kts @@ -3,7 +3,7 @@ plugins { alias(libs.plugins.kotlinAndroid) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } @@ -14,7 +14,7 @@ kotlin { android { namespace = "jp.co.soramitsu.feature_referral_api" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -24,7 +24,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -77,5 +77,5 @@ dependencies { implementation(libs.xsubstrateDep) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) } diff --git a/feature_referral_api/src/main/java/jp/co/soramitsu/feature_referral_api/data/ReferralRepository.kt b/feature_referral_api/src/main/java/jp/co/soramitsu/feature_referral_api/data/ReferralRepository.kt index 8c7252dca..69b145f61 100644 --- a/feature_referral_api/src/main/java/jp/co/soramitsu/feature_referral_api/data/ReferralRepository.kt +++ b/feature_referral_api/src/main/java/jp/co/soramitsu/feature_referral_api/data/ReferralRepository.kt @@ -35,10 +35,14 @@ package jp.co.soramitsu.feature_referral_api.data import java.math.BigDecimal import jp.co.soramitsu.common.domain.Token import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.Transaction -import jp.co.soramitsu.xnetworking.lib.datasources.blockexplorer.api.models.ReferralReward import jp.co.soramitsu.xsubstrate.encrypt.keypair.substrate.Sr25519Keypair import kotlinx.coroutines.flow.Flow +data class ReferralReward( + val referral: String, + val amount: String, +) + interface ReferralRepository { suspend fun updateReferralRewards(address: String) diff --git a/feature_referral_impl/build.gradle.kts b/feature_referral_impl/build.gradle.kts index eaf6352e4..cb55e6ac8 100644 --- a/feature_referral_impl/build.gradle.kts +++ b/feature_referral_impl/build.gradle.kts @@ -1,22 +1,21 @@ plugins { alias(libs.plugins.androidLibrary) alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.composeCompiler) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } -val composeCompilerVersion: String by project - kotlin { jvmToolchain(17) } android { namespace = "jp.co.soramitsu.feature_referral_impl" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -26,7 +25,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -43,11 +42,6 @@ android { viewBinding = true compose = true } - - composeOptions { - kotlinCompilerExtensionVersion = composeCompilerVersion - } - flavorDimensions += listOf("default") productFlavors { @@ -121,13 +115,12 @@ dependencies { implementation(libs.xsubstrateDep) implementation(libs.lifecycleProcessDep) - kapt(libs.lifecycleKaptDep) implementation(libs.navigationFragmentDep) implementation(libs.navigationUiDep) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) testImplementation(project(":test_data")) testImplementation(libs.coroutineTestDep) diff --git a/feature_referral_impl/src/main/java/jp/co/soramitsu/feature_referral_impl/data/ReferralRepositoryImpl.kt b/feature_referral_impl/src/main/java/jp/co/soramitsu/feature_referral_impl/data/ReferralRepositoryImpl.kt index f8a08c2d2..f811f3e18 100644 --- a/feature_referral_impl/src/main/java/jp/co/soramitsu/feature_referral_impl/data/ReferralRepositoryImpl.kt +++ b/feature_referral_impl/src/main/java/jp/co/soramitsu/feature_referral_impl/data/ReferralRepositoryImpl.kt @@ -45,6 +45,7 @@ import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.Transact import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.TransactionBase import jp.co.soramitsu.feature_blockexplorer_api.presentation.txhistory.TransactionStatus import jp.co.soramitsu.feature_referral_api.data.ReferralRepository +import jp.co.soramitsu.feature_referral_api.data.ReferralReward import jp.co.soramitsu.sora.substrate.runtime.Pallete import jp.co.soramitsu.sora.substrate.runtime.RuntimeManager import jp.co.soramitsu.sora.substrate.runtime.Storage @@ -53,7 +54,6 @@ import jp.co.soramitsu.sora.substrate.substrate.SubstrateCalls import jp.co.soramitsu.sora.substrate.substrate.referralBond import jp.co.soramitsu.sora.substrate.substrate.referralUnbond import jp.co.soramitsu.sora.substrate.substrate.setReferrer -import jp.co.soramitsu.xnetworking.lib.datasources.blockexplorer.api.models.ReferralReward import jp.co.soramitsu.xsubstrate.encrypt.keypair.substrate.Sr25519Keypair import jp.co.soramitsu.xsubstrate.runtime.definitions.types.fromHex import jp.co.soramitsu.xsubstrate.runtime.metadata.module diff --git a/feature_referral_impl/src/test/java/jp/co/soramitsu/feature_referral_impl/data/ReferralRepositoryTest.kt b/feature_referral_impl/src/test/java/jp/co/soramitsu/feature_referral_impl/data/ReferralRepositoryTest.kt index af24b733d..e5ff7c0d5 100644 --- a/feature_referral_impl/src/test/java/jp/co/soramitsu/feature_referral_impl/data/ReferralRepositoryTest.kt +++ b/feature_referral_impl/src/test/java/jp/co/soramitsu/feature_referral_impl/data/ReferralRepositoryTest.kt @@ -39,10 +39,10 @@ import jp.co.soramitsu.core_db.dao.ReferralsDao import jp.co.soramitsu.core_db.model.ReferralLocal import jp.co.soramitsu.feature_blockexplorer_api.data.BlockExplorerManager import jp.co.soramitsu.feature_referral_api.data.ReferralRepository +import jp.co.soramitsu.feature_referral_api.data.ReferralReward import jp.co.soramitsu.sora.substrate.runtime.RuntimeManager import jp.co.soramitsu.sora.substrate.substrate.ExtrinsicManager import jp.co.soramitsu.sora.substrate.substrate.SubstrateCalls -import jp.co.soramitsu.xnetworking.lib.datasources.blockexplorer.api.models.ReferralReward import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.toList diff --git a/feature_referral_impl/src/test/java/jp/co/soramitsu/feature_referral_impl/domain/ReferralInteractorTest.kt b/feature_referral_impl/src/test/java/jp/co/soramitsu/feature_referral_impl/domain/ReferralInteractorTest.kt index 73bf2c195..c98b74194 100644 --- a/feature_referral_impl/src/test/java/jp/co/soramitsu/feature_referral_impl/domain/ReferralInteractorTest.kt +++ b/feature_referral_impl/src/test/java/jp/co/soramitsu/feature_referral_impl/domain/ReferralInteractorTest.kt @@ -41,10 +41,10 @@ import jp.co.soramitsu.feature_account_api.domain.interfaces.UserRepository import jp.co.soramitsu.feature_assets_api.data.AssetsRepository import jp.co.soramitsu.feature_blockexplorer_api.data.TransactionHistoryRepository import jp.co.soramitsu.feature_referral_api.data.ReferralRepository +import jp.co.soramitsu.feature_referral_api.data.ReferralReward import jp.co.soramitsu.feature_referral_impl.domain.model.Referral import jp.co.soramitsu.feature_wallet_api.domain.interfaces.WalletRepository import jp.co.soramitsu.sora.substrate.runtime.RuntimeManager -import jp.co.soramitsu.xnetworking.lib.datasources.blockexplorer.api.models.ReferralReward import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.toList diff --git a/feature_select_node_api/build.gradle.kts b/feature_select_node_api/build.gradle.kts index c9ec667a0..92a71d961 100644 --- a/feature_select_node_api/build.gradle.kts +++ b/feature_select_node_api/build.gradle.kts @@ -3,7 +3,7 @@ plugins { alias(libs.plugins.kotlinAndroid) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } @@ -14,7 +14,7 @@ kotlin { android { namespace = "jp.co.soramitsu.feature_select_node_api" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -24,7 +24,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -70,8 +70,9 @@ dependencies { implementation(libs.navigationFragmentDep) implementation(libs.navigationUiDep) + implementation(platform(libs.compose.bom)) implementation(libs.composeRuntimeDep) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) } diff --git a/feature_select_node_impl/build.gradle.kts b/feature_select_node_impl/build.gradle.kts index bc06b7c18..060a3a783 100644 --- a/feature_select_node_impl/build.gradle.kts +++ b/feature_select_node_impl/build.gradle.kts @@ -1,22 +1,21 @@ plugins { alias(libs.plugins.androidLibrary) alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.composeCompiler) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } -val composeCompilerVersion: String by project - kotlin { jvmToolchain(17) } android { namespace = "jp.co.soramitsu.feature_select_node_impl" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -26,7 +25,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -38,11 +37,6 @@ android { ) } } - - composeOptions { - kotlinCompilerExtensionVersion = composeCompilerVersion - } - buildFeatures { viewBinding = true compose = true @@ -93,7 +87,6 @@ dependencies { implementation(libs.viewmodelKtxDep) implementation(libs.lifecycleProcessDep) - kapt(libs.lifecycleKaptDep) implementation(libs.navigationFragmentDep) implementation(libs.navigationUiDep) @@ -121,7 +114,7 @@ dependencies { implementation(libs.navigationComposeDep) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) testImplementation(libs.coroutineTestDep) testImplementation(libs.junitDep) diff --git a/feature_select_node_impl/src/main/java/jp/co/soramitsu/feature_select_node_impl/domain/InetAddressesWrapper.kt b/feature_select_node_impl/src/main/java/jp/co/soramitsu/feature_select_node_impl/domain/InetAddressesWrapper.kt index c4abd6581..fd1960c89 100644 --- a/feature_select_node_impl/src/main/java/jp/co/soramitsu/feature_select_node_impl/domain/InetAddressesWrapper.kt +++ b/feature_select_node_impl/src/main/java/jp/co/soramitsu/feature_select_node_impl/domain/InetAddressesWrapper.kt @@ -32,10 +32,10 @@ USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package jp.co.soramitsu.feature_select_node_impl.domain -import android.net.InetAddresses +import androidx.core.util.PatternsCompat import javax.inject.Inject class InetAddressesWrapper @Inject constructor() { - fun isIpAddressValid(address: String) = InetAddresses.isNumericAddress(address) + fun isIpAddressValid(address: String) = PatternsCompat.IP_ADDRESS.matcher(address).matches() } diff --git a/feature_select_node_impl/src/test/java/jp/co/soramitsu/feature_select_node_impl/presentation/details/NodeDetailsViewModelTest.kt b/feature_select_node_impl/src/test/java/jp/co/soramitsu/feature_select_node_impl/presentation/details/NodeDetailsViewModelTest.kt index 2d05fcb03..2370f96f9 100644 --- a/feature_select_node_impl/src/test/java/jp/co/soramitsu/feature_select_node_impl/presentation/details/NodeDetailsViewModelTest.kt +++ b/feature_select_node_impl/src/test/java/jp/co/soramitsu/feature_select_node_impl/presentation/details/NodeDetailsViewModelTest.kt @@ -68,6 +68,7 @@ import org.junit.rules.TestRule import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.any import org.mockito.kotlin.given import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -98,7 +99,7 @@ class NodeDetailsViewModelTest { private lateinit var viewModel: NodeDetailsViewModel private val nodeManagerEvents = - MutableStateFlow(NodeManagerEvent.GenesisValidated(false)) + MutableStateFlow(NodeManagerEvent.GenesisValidated(true)) @Before fun setUp() { @@ -127,6 +128,7 @@ class NodeDetailsViewModelTest { ) given(interactor.subscribeSelectedNode()).willReturn(flowOf(CUSTOM_NODES.first())) given(interactor.subscribeNodes()).willReturn(flowOf(NODE_LIST)) + given(interactor.validateNodeAddress(any())).willReturn(ValidationEvent.Succeed) } private fun addNodeViewModel() { @@ -317,15 +319,17 @@ class NodeDetailsViewModelTest { @Test fun `genesis validation failed EXPECT error text`() = runTest { - addNodeViewModel() - given(resourceManager.getString(R.string.node_details_genesis_validation_failed)).willReturn( "error" ) + addNodeViewModel() viewModel.onAddressChanged(NODE_DETAILS_ADDRESS) advanceUntilIdle() + nodeManagerEvents.emit(NodeManagerEvent.GenesisValidated(false)) + advanceUntilIdle() + assertEquals("error", viewModel.state.addressState.descriptionText) assertTrue(viewModel.state.addressState.error) assertFalse(viewModel.state.submitButtonEnabled) diff --git a/feature_sora_card_api/build.gradle.kts b/feature_sora_card_api/build.gradle.kts index d0e697a4b..9e3d2fe6c 100644 --- a/feature_sora_card_api/build.gradle.kts +++ b/feature_sora_card_api/build.gradle.kts @@ -3,7 +3,7 @@ plugins { alias(libs.plugins.kotlinAndroid) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } @@ -14,7 +14,7 @@ kotlin { android { namespace = "jp.co.soramitsu.feature_sora_card_api" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -24,7 +24,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -71,5 +71,5 @@ dependencies { implementation(libs.kotlinxSerializationJsonDep) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) } diff --git a/feature_sora_card_api/src/main/java/jp/co/soramitsu/feature_sora_card_api/util/SoraCardContractUtil.kt b/feature_sora_card_api/src/main/java/jp/co/soramitsu/feature_sora_card_api/util/SoraCardContractUtil.kt index f961ab4e1..2ee5eb027 100644 --- a/feature_sora_card_api/src/main/java/jp/co/soramitsu/feature_sora_card_api/util/SoraCardContractUtil.kt +++ b/feature_sora_card_api/src/main/java/jp/co/soramitsu/feature_sora_card_api/util/SoraCardContractUtil.kt @@ -44,6 +44,7 @@ import jp.co.soramitsu.oauth.base.sdk.contract.IbanStatus import jp.co.soramitsu.oauth.base.sdk.contract.SoraCardBasicContractData import jp.co.soramitsu.oauth.base.sdk.contract.SoraCardContractData import jp.co.soramitsu.oauth.base.sdk.contract.SoraCardFlow +import jp.co.soramitsu.oauth.uiscreens.clientsui.UiStyle fun IbanStatus?.readyToStartGatehubOnboarding(): Boolean { return (this != null) && (this == IbanStatus.ACTIVE) @@ -67,6 +68,7 @@ fun createSoraCardGateHubContract(): SoraCardContractData { soraBackEndUrl = BuildConfigWrapper.getSoraCardBackEndUrl(), client = OptionsProvider.header, clientDark = true, + clientCase = UiStyle.SW, flow = SoraCardFlow.SoraCardGateHubFlow, ) } @@ -81,6 +83,7 @@ fun createSoraCardContract( soraBackEndUrl = BuildConfigWrapper.getSoraCardBackEndUrl(), client = OptionsProvider.header, clientDark = true, + clientCase = UiStyle.SW, flow = SoraCardFlow.SoraCardKycFlow( kycCredentials = SoraCardKycCredentials( endpointUrl = BuildConfig.SORA_CARD_KYC_ENDPOINT_URL, diff --git a/feature_sora_card_impl/build.gradle.kts b/feature_sora_card_impl/build.gradle.kts index b44b22f0a..f53d4cfda 100644 --- a/feature_sora_card_impl/build.gradle.kts +++ b/feature_sora_card_impl/build.gradle.kts @@ -1,22 +1,21 @@ plugins { alias(libs.plugins.androidLibrary) alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.composeCompiler) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } -val composeCompilerVersion: String by project - kotlin { jvmToolchain(17) } android { namespace = "jp.co.soramitsu.feature_sora_card_impl" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -26,7 +25,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -38,11 +37,6 @@ android { ) } } - - composeOptions { - kotlinCompilerExtensionVersion = composeCompilerVersion - } - buildFeatures { viewBinding = true compose = true @@ -99,7 +93,6 @@ dependencies { implementation(libs.fragmentKtxDep) implementation(libs.lifecycleProcessDep) - kapt(libs.lifecycleKaptDep) implementation(libs.navigationFragmentDep) implementation(libs.navigationUiDep) @@ -124,7 +117,7 @@ dependencies { implementation(libs.navigationComposeDep) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) testImplementation(project(":test_data")) testImplementation(libs.coroutineTestDep) diff --git a/feature_wallet_api/build.gradle.kts b/feature_wallet_api/build.gradle.kts index 04a6f05d9..ce777c1cc 100644 --- a/feature_wallet_api/build.gradle.kts +++ b/feature_wallet_api/build.gradle.kts @@ -3,7 +3,7 @@ plugins { alias(libs.plugins.kotlinAndroid) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } @@ -14,7 +14,7 @@ kotlin { android { namespace = "jp.co.soramitsu.feature_wallet_api" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -24,7 +24,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -78,9 +78,5 @@ dependencies { implementation(libs.coroutineDep) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) -} - -kapt { - correctErrorTypes = true + ksp(libs.hiltCompilerDep) } diff --git a/feature_wallet_impl/build.gradle.kts b/feature_wallet_impl/build.gradle.kts index 26203450b..56aeb40a7 100644 --- a/feature_wallet_impl/build.gradle.kts +++ b/feature_wallet_impl/build.gradle.kts @@ -1,22 +1,24 @@ plugins { alias(libs.plugins.androidLibrary) alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.composeCompiler) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } -val composeCompilerVersion: String by project - kotlin { jvmToolchain(17) + compilerOptions { + freeCompilerArgs.add("-Xstring-concat=inline") + } } android { namespace = "jp.co.soramitsu.feature_wallet_impl" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -26,7 +28,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -39,19 +41,10 @@ android { } } - kotlinOptions { - freeCompilerArgs += listOf("-Xstring-concat=inline") - } - buildFeatures { viewBinding = true compose = true } - - composeOptions { - kotlinCompilerExtensionVersion = composeCompilerVersion - } - flavorDimensions += listOf("default") productFlavors { @@ -125,19 +118,18 @@ dependencies { implementation(libs.permissionsRuntimeDep) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) implementation(libs.hiltWorkManagerDep) - kapt(libs.hiltWorkManagerKaptDep) + ksp(libs.hiltWorkManagerCompilerDep) implementation(libs.lifecycleProcessDep) - kapt(libs.lifecycleKaptDep) implementation(libs.roomDep) implementation(libs.roomKtxDep) implementation(libs.uiCoreDep) -// implementation(platform(libs.compose.bom)) + implementation(platform(libs.compose.bom)) implementation(libs.composeUiDep) implementation(libs.composeFoundationDep) implementation(libs.composeMaterialDep) diff --git a/gradle.properties b/gradle.properties index 21646fd0b..7459de4c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,12 +13,8 @@ org.gradle.jvmargs=-Xmx5120m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects org.gradle.parallel=true org.gradle.daemon=true -org.gradle.configureondemand=true org.gradle.caching=true -kapt.incremental.apt=true -dagger.gradle.incremental=true #android.lifecycleProcessor.incremental=true -room.incremental=true #kotlin.setJvmTargetFromAndroidCompileOptions=true #android.debug.obsoleteApi=false authToken=jp_aifdlm625rp3sop433o4bjh0ve @@ -38,12 +34,4 @@ android.useAndroidX=true # references to the resources it declares instead of declarations plus all # transitive dependency references. #android.nonTransitiveRClass=true -android.defaults.buildfeatures.buildconfig=false -android.defaults.buildfeatures.aidl=false -android.defaults.buildfeatures.renderscript=false -#android.defaults.buildfeatures.resvalues = false -android.defaults.buildfeatures.shaders=false android.nonTransitiveRClass=false -android.nonFinalResIds=false - -composeCompilerVersion=1.5.14 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1bc8bd25c..9787bbc24 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,68 +1,68 @@ [versions] -agp = "8.7.1" -kotlin = "1.9.24" -appcompat = "1.7.0" -coroutines = "1.8.1" -coreKtx = "1.13.1" -constraint = "2.2.0-alpha05" +agp = "9.2.0" +kotlin = "2.3.21" +ksp = "2.3.6" +appcompat = "1.7.1" +coroutines = "1.10.2" +coreKtx = "1.17.0" +constraint = "2.2.1" biometric = "1.1.0" -navigation = "2.8.3" -navigationCompose = "2.8.3" +navigation = "2.9.6" +navigationCompose = "2.9.6" lottie = "4.2.0" insetter = "0.6.1" -workManager = "2.9.1" +workManager = "2.11.0" coil = "2.7.0" svg = "1.4" jdenticon = "1.1" -archFragmentTest = "1.8.4" -mockk = "1.13.11" +archFragmentTest = "1.8.9" +mockk = "1.14.6" truth = "1.4.2" -mockitoKotlin = "5.2.1" -mockito = "5.2.0" +mockitoKotlin = "6.1.0" +mockito = "5.20.0" junit = "4.13.2" -androidxTestExtJunit = "1.2.1" -androidxTestEspressoCore = "3.6.1" +androidxTestExtJunit = "1.3.0" +androidxTestEspressoCore = "3.7.0" archCoreTest = "2.2.0" -archComponents = "2.8.6" -activityCompose = "1.9.3" +archComponents = "2.10.0" +activityCompose = "1.12.1" material = "1.12.0" -fragmentKtx = "1.8.4" +fragmentKtx = "1.8.9" permission = "1.7.1" -compose-bom = "2024.10.00" -composeLifecycle = "2.8.6" -composeConstraintLayout = "1.1.0-alpha05" -firebasebom = "32.7.0" -googleServices = "4.3.15" -firebaseCrashlytics = "2.9.9" -firebaseAppDistribution = "3.2.0" -tripletVersion = "3.8.4" +compose-bom = "2026.05.00" +composeLifecycle = "2.10.0" +composeConstraintLayout = "1.1.1" +firebasebom = "34.6.0" +googleServices = "4.4.4" +firebaseCrashlytics = "3.0.6" +firebaseAppDistribution = "5.2.0" +tripletVersion = "4.0.0" timber = "5.0.1" -ktor = "2.3.2" +ktor = "2.3.1" ed = "2.0.1" xerces = "2.9.0" -dagger = "2.52" -daggerKapt = "2.52" -hiltCompose = "1.2.0" -hiltWorkManager = "1.2.0" +dagger = "2.59.2" +hiltCompose = "1.3.0" +hiltWorkManager = "1.3.0" webSocketLib = "2.14" web3j = "4.6.0-android" zXing = "3.3.3" zXingEmbedded = "4.3.0" -activityKtx = "1.9.3" -xserializationjson = "1.6.3" -gson = "2.10.1" -datastore = "1.0.0" -room = "2.6.1" +activityKtx = "1.12.1" +xserializationjson = "1.9.0" +gson = "2.13.2" +datastore = "1.1.7" +room = "2.8.4" lazySodium = "5.0.2" jna = "5.8.0" -kover = "0.8.3" -soramitsuSoracard = "1.1.21" -soramitsuXnetworking = "1.0.10" +kover = "0.9.8" +soramitsuSoracard = "1.2.1-K2" +soramitsuXnetworking = "1.0.15-K2" soramitsuXsubstrate = "1.2.7" -soramitsuUicore = "0.2.37" +soramitsuUicore = "0.2.39" soramitsuXbackup = "1.2.3" soramitsuXcrypto = "1.2.7" -soramitsuAndroidFoundation = "0.0.3" +soramitsuAndroidFoundation = "0.0.4" [libraries] soramitsu-android-foundation = { module = "jp.co.soramitsu:android-foundation", version.ref = "soramitsuAndroidFoundation" } @@ -73,7 +73,7 @@ uiCoreDep = { module = "jp.co.soramitsu:ui-core", version.ref = "soramitsuUicore xbackupDep = { module = "jp.co.soramitsu:xbackup", version.ref = "soramitsuXbackup" } xcryptoDep = { module = "jp.co.soramitsu:xcrypto", version.ref = "soramitsuXcrypto" } roomDep = { module = "androidx.room:room-runtime", version.ref = "room" } -roomKaptDep = { module = "androidx.room:room-compiler", version.ref = "room" } +roomCompilerDep = { module = "androidx.room:room-compiler", version.ref = "room" } roomKtxDep = { module = "androidx.room:room-ktx", version.ref = "room" } roomTestHelpersDep = { module = "androidx.room:room-testing", version.ref = "room" } datastoreDep = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } @@ -88,11 +88,11 @@ coroutineDep = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", versi coroutineAndroidDep = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } coroutineTestDep = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } daggerDep = { module = "com.google.dagger:hilt-android", version.ref = "dagger" } -daggerKaptDep = { module = "com.google.dagger:hilt-compiler", version.ref = "daggerKapt" } +hiltCompilerDep = { module = "com.google.dagger:hilt-compiler", version.ref = "dagger" } hiltUiTestDep = { module = "com.google.dagger:hilt-android-testing", version.ref = "dagger" } hiltNavComposeDep = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltCompose" } hiltWorkManagerDep = { module = "androidx.hilt:hilt-work", version.ref = "hiltWorkManager" } -hiltWorkManagerKaptDep = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltWorkManager" } +hiltWorkManagerCompilerDep = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltWorkManager" } lazySodiumDep = { module = "com.goterl:lazysodium-android", version.ref = "lazySodium" } jnaDep = { module = "net.java.dev.jna:jna", version.ref = "jna" } ktorLoggingDep = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } @@ -125,10 +125,11 @@ archFragmentTestDep = { module = "androidx.fragment:fragment-testing", version.r mockkDep = { module = "io.mockk:mockk", version.ref = "mockk" } truthDep = { module = "com.google.truth:truth", version.ref = "truth" } mockitoKotlinDep = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" } -mockitoDep = { module = "org.mockito:mockito-inline", version.ref = "mockito" } +mockitoDep = { module = "org.mockito:mockito-core", version.ref = "mockito" } junitDep = { module = "junit:junit", version.ref = "junit" } androidxTestExtJunitDep = { module = "androidx.test.ext:junit", version.ref = "androidxTestExtJunit" } androidxTestEspressoCoreDep = { module = "androidx.test.espresso:espresso-core", version.ref = "androidxTestEspressoCore" } +androidxTestEspressoIntentsDep = { module = "androidx.test.espresso:espresso-intents", version.ref = "androidxTestEspressoCore" } archCoreTestDep = { module = "androidx.arch.core:core-testing", version.ref = "archCoreTest" } composeActivityDep = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } materialDep = { group = "com.google.android.material", name = "material", version.ref = "material" } @@ -142,16 +143,16 @@ composeFoundationDep = { group = "androidx.compose.foundation", name = "foundati composeToolingDep = { group = "androidx.compose.ui", name = "ui-tooling" } composeToolingPreviewDep = { group = "androidx.compose.ui", name = "ui-tooling-preview" } composeMaterialDep = { group = "androidx.compose.material", name = "material" } +composeMaterial3Dep = { group = "androidx.compose.material3", name = "material3" } composeAnimationDep = { group = "androidx.compose.animation", name = "animation" } composeAnimationGraphicsDep = { group = "androidx.compose.animation", name = "animation-graphics" } composeConstraintLayoutDep = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "composeConstraintLayout" } composeUiGraphicsDep = { group = "androidx.compose.ui", name = "ui-graphics" } -#ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } -#ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } +composeUiTestManifestDep = { group = "androidx.compose.ui", name = "ui-test-manifest" } +composeUiTestJunit4Dep = { group = "androidx.compose.ui", name = "ui-test-junit4" } #androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } #espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } lifecycleProcessDep = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "archComponents" } -lifecycleKaptDep = { group = "androidx.lifecycle", name = "lifecycle-compiler", version.ref = "archComponents" } viewmodelKtxDep = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "archComponents" } livedataKtxDep = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "archComponents" } runtimeKtxDep = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "archComponents" } @@ -163,9 +164,10 @@ androidApplication = { id = "com.android.application", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" } kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "dagger" } -kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } googleServicesPlugin = { id = "com.google.gms.google-services", version.ref = "googleServices" } firebaseCrashlyticsPlugin = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlytics" } firebaseAppDistributionPlugin = { id = "com.google.firebase.appdistribution", version.ref = "firebaseAppDistribution" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 05c38d09b..1d340cb89 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Jan 13 13:13:48 MSK 2022 +#Sun May 24 00:00:00 GST 2026 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/network/build.gradle.kts b/network/build.gradle.kts index 8d767507b..5fba3653c 100644 --- a/network/build.gradle.kts +++ b/network/build.gradle.kts @@ -2,7 +2,7 @@ plugins { alias(libs.plugins.androidLibrary) alias(libs.plugins.kotlinAndroid) alias(libs.plugins.serialization) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } @@ -13,7 +13,7 @@ kotlin { android { namespace = "jp.co.soramitsu.network" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -23,7 +23,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { diff --git a/settings.gradle.kts b/settings.gradle.kts index fcc5a2b64..09cc25a55 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,18 +10,14 @@ pluginManagement { } } -fun secret(name: String): String { +fun optionalSecret(name: String): String? { val fileProperties = File(rootProject.projectDir.absolutePath, "local.properties") - val pr = runCatching { FileInputStream(fileProperties) }.getOrNull()?.let { file -> + val pr = runCatching { FileInputStream(fileProperties) }.getOrNull()?.use { file -> Properties().apply { load(file) } } - return pr?.getProperty(name) ?: System.getenv(name)!! -} - -fun maybeWrapQuotes(s: String): String { - return if (s.startsWith("\"")) s else "\"" + s + "\"" + return pr?.getProperty(name) ?: System.getenv(name) } dependencyResolutionManagement { @@ -31,14 +27,21 @@ dependencyResolutionManagement { google() mavenCentral() maven { url = uri("https://jitpack.io") } - maven { - url = uri(secret("PAY_WINGS_REPOSITORY_URL")) - credentials { - username = secret("PAY_WINGS_USERNAME") - password = secret("PAY_WINGS_PASSWORD") + val payWingsRepositoryUrl = optionalSecret("PAY_WINGS_REPOSITORY_URL") + val payWingsUsername = optionalSecret("PAY_WINGS_USERNAME") + val payWingsPassword = optionalSecret("PAY_WINGS_PASSWORD") + if (!payWingsRepositoryUrl.isNullOrBlank() && !payWingsUsername.isNullOrBlank() && !payWingsPassword.isNullOrBlank()) { + maven { + url = uri(payWingsRepositoryUrl) + credentials { + username = payWingsUsername + password = payWingsPassword + } } } - mavenLocal() + if (providers.gradleProperty("useMavenLocal").orNull == "true") { + mavenLocal() + } } } diff --git a/sorasubstrate/build.gradle.kts b/sorasubstrate/build.gradle.kts index e52f2eaaa..bc2beb173 100644 --- a/sorasubstrate/build.gradle.kts +++ b/sorasubstrate/build.gradle.kts @@ -3,7 +3,7 @@ plugins { alias(libs.plugins.kotlinAndroid) alias(libs.plugins.serialization) alias(libs.plugins.hilt) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") alias(libs.plugins.kover) } @@ -14,7 +14,7 @@ kotlin { android { namespace = "jp.co.soramitsu.sora.substrate" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -24,7 +24,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { @@ -72,7 +72,6 @@ dependencies { implementation(libs.xsubstrateDep) implementation(libs.xcryptoDep) - implementation(libs.xnetworkingLibDep) implementation(libs.soramitsu.android.foundation) implementation(libs.gsonDep) @@ -81,5 +80,5 @@ dependencies { implementation(libs.coroutineDep) implementation(libs.timberDep) implementation(libs.daggerDep) - kapt(libs.daggerKaptDep) + ksp(libs.hiltCompilerDep) } diff --git a/test_data/build.gradle.kts b/test_data/build.gradle.kts index 29d7007e5..06ae4c47e 100644 --- a/test_data/build.gradle.kts +++ b/test_data/build.gradle.kts @@ -2,7 +2,7 @@ plugins { alias(libs.plugins.androidLibrary) alias(libs.plugins.kotlinAndroid) alias(libs.plugins.serialization) - alias(libs.plugins.kapt) + alias(libs.plugins.ksp) id("kotlin-parcelize") } @@ -12,7 +12,7 @@ kotlin { android { namespace = "jp.co.soramitsu.test_data" - compileSdk = 34 + compileSdk = 36 defaultConfig { minSdk = 26 @@ -22,7 +22,7 @@ android { testOptions { unitTests.isReturnDefaultValues = true - targetSdk = 34 + targetSdk = 36 } buildTypes { diff --git a/test_data/src/main/java/jp/co/soramitsu/test_data/SoraCard.kt b/test_data/src/main/java/jp/co/soramitsu/test_data/SoraCard.kt index 86a2a5cf6..8e5ac2d92 100644 --- a/test_data/src/main/java/jp/co/soramitsu/test_data/SoraCard.kt +++ b/test_data/src/main/java/jp/co/soramitsu/test_data/SoraCard.kt @@ -41,6 +41,7 @@ import jp.co.soramitsu.oauth.base.sdk.contract.SoraCardBasicContractData import jp.co.soramitsu.oauth.base.sdk.contract.SoraCardCommonVerification import jp.co.soramitsu.oauth.base.sdk.contract.SoraCardContractData import jp.co.soramitsu.oauth.base.sdk.contract.SoraCardFlow +import jp.co.soramitsu.oauth.uiscreens.clientsui.UiStyle object SoraCardTestData { @@ -69,6 +70,7 @@ object SoraCardTestData { client = "test android client", soraBackEndUrl = "soracard backend", clientDark = false, + clientCase = UiStyle.SW, flow = SoraCardFlow.SoraCardKycFlow( kycCredentials = SoraCardKycCredentials( endpointUrl = BuildConfig.SORA_CARD_KYC_ENDPOINT_URL, @@ -89,6 +91,7 @@ object SoraCardTestData { client = "test android client", soraBackEndUrl = "soracard backend", clientDark = false, + clientCase = UiStyle.SW, flow = SoraCardFlow.SoraCardKycFlow( kycCredentials = SoraCardKycCredentials( endpointUrl = BuildConfig.SORA_CARD_KYC_ENDPOINT_URL, @@ -109,6 +112,7 @@ object SoraCardTestData { client = "test android client", soraBackEndUrl = "soracard backend", clientDark = false, + clientCase = UiStyle.SW, flow = SoraCardFlow.SoraCardKycFlow( areAttemptsPaidSuccessfully = false, isEnoughXorAvailable = false,