Initial commit: Pezkuwi Wallet Android

Security hardened release:
- Code obfuscation enabled (minifyEnabled=true, shrinkResources=true)
- Sensitive files excluded (google-services.json, keystores)
- Branch.io key moved to BuildConfig placeholder
- Updated dependencies: OkHttp 4.12.0, Gson 2.10.1, BouncyCastle 1.77
- Comprehensive ProGuard rules for crypto wallet
- Navigation 2.7.7, Lifecycle 2.7.0, ConstraintLayout 2.1.4
This commit is contained in:
2026-02-12 05:19:41 +03:00
commit a294aa1a6b
7687 changed files with 441811 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+86
View File
@@ -0,0 +1,86 @@
apply plugin: 'kotlin-parcelize'
apply from: '../tests.gradle'
apply from: '../scripts/secrets.gradle'
android {
defaultConfig {
buildConfigField "String", "KARURA_NOVA_REFERRAL", "\"0x9642d0db9f3b301b44df74b63b0b930011e3f52154c5ca24b4dc67b3c7322f15\""
buildConfigField "String", "ACALA_NOVA_REFERRAL", "\"0x08eb319467ea54784cd9edfbd03bbcc53f7a021ed8d9ed2ca97b6ae46b3f6014\""
buildConfigField "String", "BIFROST_NOVA_REFERRAL", "\"FRLS69\""
buildConfigField "String", "BIFROST_TERMS_LINKS", "\"https://docs.google.com/document/d/1PDpgHnIcAmaa7dEFusmLYgjlvAbk2VKtMd755bdEsf4\""
buildConfigField "String", "ACALA_TERMS_LINK", "\"https://acala.network/acala/terms\""
buildConfigField "String", "ACALA_TEST_AUTH_TOKEN", readStringSecret("ACALA_TEST_AUTH_TOKEN")
buildConfigField "String", "ACALA_PROD_AUTH_TOKEN", readStringSecret("ACALA_PROD_AUTH_TOKEN")
buildConfigField "String", "MOONBEAM_TEST_AUTH_TOKEN", readStringSecret("MOONBEAM_TEST_AUTH_TOKEN")
buildConfigField "String", "MOONBEAM_PROD_AUTH_TOKEN", readStringSecret("MOONBEAM_PROD_AUTH_TOKEN")
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
namespace 'io.novafoundation.nova.feature_crowdloan_impl'
buildFeatures {
viewBinding true
}
}
dependencies {
implementation project(':core-db')
implementation project(':common')
implementation project(':feature-crowdloan-api')
implementation project(':feature-account-api')
implementation project(':feature-wallet-api')
implementation project(':feature-currency-api')
implementation project(':runtime')
implementation kotlinDep
implementation androidDep
implementation materialDep
implementation cardViewDep
implementation constraintDep
implementation permissionsDep
implementation coroutinesDep
implementation coroutinesAndroidDep
implementation viewModelKtxDep
implementation liveDataKtxDep
implementation lifeCycleKtxDep
implementation insetterDep
implementation daggerDep
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
ksp daggerCompiler
implementation roomDep
ksp roomCompiler
implementation lifecycleDep
ksp lifecycleCompiler
testImplementation jUnitDep
testImplementation mockitoDep
implementation substrateSdkDep
compileOnly wsDep
implementation gsonDep
implementation retrofitDep
implementation shimmerDep
implementation coilDep
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
</manifest>
@@ -0,0 +1,20 @@
package io.novafoundation.nova.feature_crowdloan_impl.data
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.runtime.ext.isUtilityAsset
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.state.SelectableSingleAssetSharedState
import io.novafoundation.nova.runtime.state.NothingAdditional
import io.novafoundation.nova.runtime.state.uniqueOption
private const val CROWDLOAN_SHARED_STATE = "CROWDLOAN_SHARED_STATE"
class CrowdloanSharedState(
chainRegistry: ChainRegistry,
preferences: Preferences,
) : SelectableSingleAssetSharedState<NothingAdditional>(
preferences = preferences,
chainRegistry = chainRegistry,
supportedOptions = uniqueOption { chain, chainAsset -> chain.hasCrowdloans and chainAsset.isUtilityAsset },
preferencesKey = CROWDLOAN_SHARED_STATE
)
@@ -0,0 +1,76 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.network.api.acala
import io.novafoundation.nova.feature_crowdloan_impl.BuildConfig
import io.novafoundation.nova.runtime.ext.Geneses
import io.novafoundation.nova.runtime.ext.addressOf
import io.novafoundation.nova.runtime.ext.requireGenesisHash
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.runtime.AccountId
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Path
private fun authHeader(token: String) = "Bearer $token"
interface AcalaApi {
companion object {
private val URL_BY_GENESIS = mapOf(
Chain.Geneses.ROCOCO_ACALA to "crowdloan.aca-dev.network",
Chain.Geneses.POLKADOT to "crowdloan.aca-api.network",
Chain.Geneses.KUSAMA to "api.aca-staging.network"
)
private val AUTH_BY_GENESIS = mapOf(
Chain.Geneses.POLKADOT to authHeader(BuildConfig.ACALA_PROD_AUTH_TOKEN),
Chain.Geneses.ROCOCO_ACALA to authHeader(BuildConfig.ACALA_TEST_AUTH_TOKEN)
)
fun getAuthHeader(chain: Chain) = AUTH_BY_GENESIS[chain.requireGenesisHash()]
?: notSupportedChain(chain)
fun getBaseUrl(chain: Chain) = URL_BY_GENESIS[chain.requireGenesisHash()]
?: notSupportedChain(chain)
private fun notSupportedChain(chain: Chain): Nothing {
throw UnsupportedOperationException("Chain ${chain.name} is not supported for Acala/Karura crowdloans")
}
}
@GET("//{baseUrl}/referral/{referral}")
suspend fun isReferralValid(
@Header("Authorization") authHeader: String,
@Path("baseUrl") baseUrl: String,
@Path("referral") referral: String,
): ReferralCheck
@GET("//{baseUrl}/statement")
suspend fun getStatement(
@Header("Authorization") authHeader: String,
@Path("baseUrl") baseUrl: String,
): AcalaStatement
@POST("//{baseUrl}/contribute")
suspend fun directContribute(
@Header("Authorization") authHeader: String,
@Path("baseUrl") baseUrl: String,
@Body body: AcalaDirectContributeRequest,
): Any?
@POST("//{baseUrl}/transfer")
suspend fun liquidContribute(
@Header("Authorization") authHeader: String,
@Path("baseUrl") baseUrl: String,
@Body body: AcalaLiquidContributeRequest,
): Any?
@GET("//{baseUrl}/contribution/{address}")
suspend fun getContributions(
@Path("baseUrl") baseUrl: String,
@Path("address") address: String,
): AcalaContribution
}
suspend fun AcalaApi.getContributions(chain: Chain, accountId: AccountId) = getContributions(AcalaApi.getBaseUrl(chain), chain.addressOf(accountId))
@@ -0,0 +1,7 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.network.api.acala
import java.math.BigInteger
class AcalaContribution(
val proxyAmount: BigInteger?,
)
@@ -0,0 +1,16 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.network.api.acala
import java.math.BigInteger
class AcalaDirectContributeRequest(
val address: String,
val amount: BigInteger,
val referral: String?,
val signature: String,
)
class AcalaLiquidContributeRequest(
val address: String,
val amount: BigInteger,
val referral: String?,
)
@@ -0,0 +1,6 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.network.api.acala
class AcalaStatement(
val statement: String,
val proxyAddress: String,
)
@@ -0,0 +1,5 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.network.api.acala
class ReferralCheck(
val result: Boolean
)
@@ -0,0 +1,17 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.network.api.bifrost
import io.novafoundation.nova.common.data.network.subquery.SubQueryResponse
import retrofit2.http.Body
import retrofit2.http.POST
interface BifrostApi {
companion object {
const val BASE_URL = "https://salp-api.bifrost.finance"
}
@POST("/")
suspend fun getAccountByReferralCode(@Body body: BifrostReferralCheckRequest): SubQueryResponse<GetAccountByReferralCodeResponse>
}
suspend fun BifrostApi.getAccountByReferralCode(code: String) = getAccountByReferralCode(BifrostReferralCheckRequest(code))
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.network.api.bifrost
class BifrostReferralCheckRequest(code: String) {
val query = """
{
getAccountByInvitationCode(code: "$code") {
account
}
}
""".trimIndent()
}
@@ -0,0 +1,8 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.network.api.bifrost
class GetAccountByReferralCodeResponse(
val getAccountByInvitationCode: GetAccountByReferralCode
) {
class GetAccountByReferralCode(val account: String?)
}
@@ -0,0 +1,13 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.network.api.moonbeam
import com.google.gson.annotations.SerializedName
class AgreeRemarkRequest(
val address: String,
@SerializedName("signed-message")
val signedMessage: String,
)
class AgreeRemarkResponse(
val remark: String,
)
@@ -0,0 +1,5 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.network.api.moonbeam
class CheckRemarkResponse(
val verified: Boolean,
)
@@ -0,0 +1,16 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.network.api.moonbeam
import com.google.gson.annotations.SerializedName
import java.util.UUID
class MakeSignatureRequest(
val address: String,
@SerializedName("previous-total-contribution")
val previousTotalContribution: String,
val contribution: String,
val guid: String = UUID.randomUUID().toString(),
)
class MakeSignatureResponse(
val signature: String,
)
@@ -0,0 +1,73 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.network.api.moonbeam
import io.novafoundation.nova.common.data.network.TimeHeaderInterceptor
import io.novafoundation.nova.feature_crowdloan_api.data.repository.ParachainMetadata
import io.novafoundation.nova.feature_crowdloan_api.data.repository.getExtra
import io.novafoundation.nova.feature_crowdloan_impl.BuildConfig
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Headers
import retrofit2.http.POST
import retrofit2.http.Path
private val AUTH_TOKENS = mapOf(
"MOONBEAM_TEST_AUTH_TOKEN" to BuildConfig.MOONBEAM_TEST_AUTH_TOKEN,
"MOONBEAM_PROD_AUTH_TOKEN" to BuildConfig.MOONBEAM_PROD_AUTH_TOKEN
)
interface MoonbeamApi {
@GET("https://raw.githubusercontent.com/moonbeam-foundation/crowdloan-self-attestation/main/moonbeam/README.md")
suspend fun getLegalText(): String
@GET("//{baseUrl}/check-remark/{address}")
suspend fun checkRemark(
@Path("baseUrl") baseUrl: String,
@Header("x-api-key") apiToken: String?,
@Path("address") address: String,
): CheckRemarkResponse
@POST("//{baseUrl}/agree-remark")
suspend fun agreeRemark(
@Path("baseUrl") baseUrl: String,
@Header("x-api-key") apiToken: String?,
@Body body: AgreeRemarkRequest,
): AgreeRemarkResponse
@POST("//{baseUrl}/verify-remark")
@Headers(TimeHeaderInterceptor.LONG_CONNECT, TimeHeaderInterceptor.LONG_READ, TimeHeaderInterceptor.LONG_WRITE)
suspend fun verifyRemark(
@Path("baseUrl") baseUrl: String,
@Header("x-api-key") apiToken: String?,
@Body body: VerifyRemarkRequest,
): VerifyRemarkResponse
@POST("//{baseUrl}/make-signature")
suspend fun makeSignature(
@Path("baseUrl") baseUrl: String,
@Header("x-api-key") apiToken: String?,
@Body body: MakeSignatureRequest,
): MakeSignatureResponse
}
fun ParachainMetadata.moonbeamChainId() = getExtra("paraId")
private fun ParachainMetadata.apiBaseUrl() = getExtra("apiLink")
private fun ParachainMetadata.apiToken() = AUTH_TOKENS[getExtra("apiTokenName")]
suspend fun MoonbeamApi.checkRemark(chainMetadata: ParachainMetadata, address: String): CheckRemarkResponse {
return checkRemark(chainMetadata.apiBaseUrl(), chainMetadata.apiToken(), address)
}
suspend fun MoonbeamApi.agreeRemark(chainMetadata: ParachainMetadata, body: AgreeRemarkRequest): AgreeRemarkResponse {
return agreeRemark(chainMetadata.apiBaseUrl(), chainMetadata.apiToken(), body)
}
suspend fun MoonbeamApi.verifyRemark(chainMetadata: ParachainMetadata, body: VerifyRemarkRequest): VerifyRemarkResponse {
return verifyRemark(chainMetadata.apiBaseUrl(), chainMetadata.apiToken(), body)
}
suspend fun MoonbeamApi.makeSignature(chainMetadata: ParachainMetadata, body: MakeSignatureRequest): MakeSignatureResponse {
return makeSignature(chainMetadata.apiBaseUrl(), chainMetadata.apiToken(), body)
}
@@ -0,0 +1,15 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.network.api.moonbeam
import com.google.gson.annotations.SerializedName
class VerifyRemarkRequest(
val address: String,
@SerializedName("extrinsic-hash")
val extrinsicHash: String,
@SerializedName("block-hash")
val blockHash: String,
)
class VerifyRemarkResponse(
val verified: Boolean,
)
@@ -0,0 +1,19 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.network.api.parachain
import io.novafoundation.nova.feature_crowdloan_api.data.repository.ParachainMetadata
fun mapParachainMetadataRemoteToParachainMetadata(parachainMetadata: ParachainMetadataRemote) =
with(parachainMetadata) {
ParachainMetadata(
paraId = paraid,
movedToParaId = movedToParaId,
iconLink = icon,
name = name,
description = description,
rewardRate = rewardRate?.toBigDecimal(),
website = website,
customFlow = customFlow,
token = token,
extras = extras.orEmpty()
)
}
@@ -0,0 +1,12 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.network.api.parachain
import retrofit2.http.GET
import retrofit2.http.Url
interface ParachainMetadataApi {
@GET()
suspend fun getParachainMetadata(
@Url url: String
): List<ParachainMetadataRemote>
}
@@ -0,0 +1,16 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.network.api.parachain
import java.math.BigInteger
class ParachainMetadataRemote(
val description: String,
val icon: String,
val name: String,
val paraid: BigInteger,
val token: String,
val rewardRate: Double?,
val customFlow: String?,
val website: String,
val extras: Map<String, String>?,
val movedToParaId: BigInteger?,
)
@@ -0,0 +1,17 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.network.api.parallel
import retrofit2.http.GET
import retrofit2.http.Path
interface ParallelApi {
companion object {
const val BASE_URL = "https://auction-service-prod.parallel.fi/crowdloan/rewards/"
}
@GET("{network}/{address}")
suspend fun getContributions(
@Path("network") network: String,
@Path("address") address: String,
): List<ParallelContribution>
}
@@ -0,0 +1,8 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.network.api.parallel
import java.math.BigInteger
class ParallelContribution(
val paraId: BigInteger,
val amount: BigInteger,
)
@@ -0,0 +1,51 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.network.blockhain.extrinsic
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
import io.novafoundation.nova.common.data.network.runtime.binding.ParaId
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
import io.novasama.substrate_sdk_android.runtime.extrinsic.call
import java.math.BigInteger
fun ExtrinsicBuilder.contribute(
parachainId: ParaId,
contribution: BigInteger,
signature: Any?,
): ExtrinsicBuilder {
return call(
moduleName = "Crowdloan",
callName = "contribute",
arguments = mapOf(
"index" to parachainId,
"value" to contribution,
"signature" to signature
)
)
}
fun ExtrinsicBuilder.addMemo(parachainId: ParaId, memo: String): ExtrinsicBuilder {
return addMemo(parachainId, memo.toByteArray())
}
fun ExtrinsicBuilder.addMemo(parachainId: ParaId, memo: ByteArray): ExtrinsicBuilder {
return call(
moduleName = "Crowdloan",
callName = "add_memo",
arguments = mapOf(
"index" to parachainId,
"memo" to memo
)
)
}
fun ExtrinsicBuilder.claimContribution(parachainId: ParaId, block: BlockNumber, depositor: AccountId) {
call(
moduleName = "AhOps",
callName = "withdraw_crowdloan_contribution",
arguments = mapOf(
"block" to block,
"para_id" to parachainId,
"depositor" to depositor
)
)
}
@@ -0,0 +1,79 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.network.updater
import android.util.Log
import io.novafoundation.nova.common.utils.LOG_TAG
import io.novafoundation.nova.common.utils.flowOfAll
import io.novafoundation.nova.common.utils.transformLatestDiffed
import io.novafoundation.nova.core.updater.UpdateSystem
import io.novafoundation.nova.core.updater.Updater
import io.novafoundation.nova.feature_crowdloan_api.data.network.updater.ContributionsUpdateSystemFactory
import io.novafoundation.nova.feature_crowdloan_api.data.network.updater.ContributionsUpdaterFactory
import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory
import io.novafoundation.nova.runtime.ext.isFullSync
import io.novafoundation.nova.runtime.ext.utilityAsset
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.transformLatest
class RealContributionsUpdateSystemFactory(
private val chainRegistry: ChainRegistry,
private val contributionsUpdaterFactory: ContributionsUpdaterFactory,
private val assetBalanceScopeFactory: AssetBalanceScopeFactory,
private val storageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory,
) : ContributionsUpdateSystemFactory {
override fun create(): UpdateSystem {
return ContributionsUpdateSystem(
chainRegistry,
contributionsUpdaterFactory,
assetBalanceScopeFactory,
storageSharedRequestsBuilderFactory
)
}
}
class ContributionsUpdateSystem(
private val chainRegistry: ChainRegistry,
private val contributionsUpdaterFactory: ContributionsUpdaterFactory,
private val assetBalanceScopeFactory: AssetBalanceScopeFactory,
private val storageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory,
) : UpdateSystem {
override fun start(): Flow<Updater.SideEffect> {
return flowOfAll {
chainRegistry.currentChains.mapLatest { chains ->
chains.filter { it.connectionState.isFullSync && it.hasCrowdloans }
}.transformLatestDiffed {
emitAll(run(it))
}
}.flowOn(Dispatchers.Default)
}
private fun run(chain: Chain): Flow<Updater.SideEffect> {
return flowOfAll {
// we do not start subscription builder since it is not needed for contributions
val subscriptionBuilder = storageSharedRequestsBuilderFactory.create(chain.id)
val invalidationScope = assetBalanceScopeFactory.create(chain, chain.utilityAsset)
val updater = contributionsUpdaterFactory.create(chain, invalidationScope)
invalidationScope.invalidationFlow().transformLatest {
kotlin.runCatching {
updater.listenForUpdates(subscriptionBuilder, it)
.catch { logError(chain, it) }
}.onSuccess { updaterFlow ->
emitAll(updaterFlow)
}
}
}.catch { logError(chain, it) }
}
private fun logError(chain: Chain, exception: Throwable) {
Log.e(LOG_TAG, "Failed to run contributions updater for ${chain.name}", exception)
}
}
@@ -0,0 +1,104 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.network.updater
import io.novafoundation.nova.common.utils.CollectionDiffer
import io.novafoundation.nova.common.utils.flowOfAll
import io.novafoundation.nova.common.utils.sumByBigInteger
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
import io.novafoundation.nova.core.updater.Updater
import io.novafoundation.nova.core_db.dao.ContributionDao
import io.novafoundation.nova.core_db.dao.ExternalBalanceDao
import io.novafoundation.nova.core_db.dao.updateExternalBalance
import io.novafoundation.nova.core_db.model.ContributionLocal
import io.novafoundation.nova.core_db.model.ExternalBalanceLocal
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_crowdloan_api.data.network.updater.AssetBalanceScope
import io.novafoundation.nova.feature_crowdloan_api.data.network.updater.AssetBalanceScope.ScopeValue
import io.novafoundation.nova.feature_crowdloan_api.data.network.updater.ContributionsUpdaterFactory
import io.novafoundation.nova.feature_crowdloan_api.data.repository.ContributionsRepository
import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.mapContributionToLocal
import io.novafoundation.nova.runtime.ext.utilityAsset
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.onEach
class RealContributionsUpdaterFactory(
private val contributionsRepository: ContributionsRepository,
private val contributionDao: ContributionDao,
private val externalBalanceDao: ExternalBalanceDao,
) : ContributionsUpdaterFactory {
override fun create(chain: Chain, assetBalanceScope: AssetBalanceScope): Updater<ScopeValue> {
return ContributionsUpdater(
assetBalanceScope,
chain,
contributionsRepository,
contributionDao,
externalBalanceDao,
)
}
}
class ContributionsUpdater(
override val scope: AssetBalanceScope,
private val chain: Chain,
private val contributionsRepository: ContributionsRepository,
private val contributionDao: ContributionDao,
private val externalBalanceDao: ExternalBalanceDao,
) : Updater<ScopeValue> {
override val requiredModules: List<String> = emptyList()
override suspend fun listenForUpdates(
storageSubscriptionBuilder: SharedRequestsBuilder,
scopeValue: ScopeValue,
): Flow<Updater.SideEffect> {
return flowOfAll {
if (scopeValue.asset.token.configuration.enabled) {
sync(scopeValue)
} else {
emptyFlow()
}
}.noSideAffects()
}
private suspend fun sync(scopeValue: ScopeValue): Flow<Any> {
val metaAccount = scopeValue.metaAccount
val chainAsset = chain.utilityAsset
val accountId = metaAccount.accountIdIn(chain) ?: return emptyFlow()
return contributionsRepository.loadContributionsGraduallyFlow(
chain = chain,
accountId = accountId,
).onEach { (sourceId, contributionsResult) ->
contributionsResult.onSuccess { contributions ->
val newContributions = contributions.map { mapContributionToLocal(metaAccount.id, it) }
val oldContributions = contributionDao.getContributions(metaAccount.id, chain.id, chainAsset.id, sourceId)
val collectionDiffer = CollectionDiffer.findDiff(newContributions, oldContributions, false)
contributionDao.updateContributions(collectionDiffer)
insertExternalBalance(newContributions, sourceId, chainAsset, metaAccount)
}
}
}
private suspend fun insertExternalBalance(
contributions: List<ContributionLocal>,
sourceId: String,
chainAsset: Chain.Asset,
metaAccount: MetaAccount
) {
val totalSourceContributions = contributions.sumByBigInteger { it.amountInPlanks }
val externalBalance = ExternalBalanceLocal(
metaId = metaAccount.id,
chainId = chain.id,
assetId = chainAsset.id,
type = ExternalBalanceLocal.Type.CROWDLOAN,
subtype = sourceId,
amount = totalSourceContributions
)
externalBalanceDao.updateExternalBalance(externalBalance)
}
}
@@ -0,0 +1,42 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.network.updater
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_crowdloan_api.data.network.updater.AssetBalanceScope
import io.novafoundation.nova.feature_crowdloan_api.data.network.updater.AssetBalanceScope.ScopeValue
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
class AssetBalanceScopeFactory(
private val walletRepository: WalletRepository,
private val accountRepository: AccountRepository,
) {
fun create(chain: Chain, asset: Chain.Asset): AssetBalanceScope {
return RealAssetBalanceScope(chain, asset, walletRepository, accountRepository)
}
}
class RealAssetBalanceScope(
private val chain: Chain,
private val asset: Chain.Asset,
private val walletRepository: WalletRepository,
private val accountRepository: AccountRepository,
) : AssetBalanceScope {
override fun invalidationFlow(): Flow<ScopeValue> {
return accountRepository.selectedMetaAccountFlow().flatMapLatest { metaAccount ->
walletRepository.assetFlow(metaAccount.id, asset).map { asset ->
ScopeValue(metaAccount, asset)
}
}
.distinctUntilChanged { old, new ->
old.asset.totalInPlanks == new.asset.totalInPlanks &&
old.metaAccount.id == new.metaAccount.id &&
old.metaAccount.accountIdIn(chain).contentEquals(new.metaAccount.accountIdIn(chain))
}
}
}
@@ -0,0 +1,100 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.repository
import io.novafoundation.nova.common.data.network.runtime.binding.ParaId
import io.novafoundation.nova.common.utils.crowdloan
import io.novafoundation.nova.common.utils.numberConstant
import io.novafoundation.nova.common.utils.slots
import io.novafoundation.nova.feature_crowdloan_api.data.network.blockhain.binding.FundInfo
import io.novafoundation.nova.feature_crowdloan_api.data.network.blockhain.binding.LeaseEntry
import io.novafoundation.nova.feature_crowdloan_api.data.network.blockhain.binding.bindFundInfo
import io.novafoundation.nova.feature_crowdloan_api.data.network.blockhain.binding.bindLeases
import io.novafoundation.nova.feature_crowdloan_api.data.repository.CrowdloanRepository
import io.novafoundation.nova.feature_crowdloan_api.data.repository.LeasePeriodToBlocksConverter
import io.novafoundation.nova.feature_crowdloan_api.data.repository.ParachainMetadata
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.parachain.ParachainMetadataApi
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.parachain.mapParachainMetadataRemoteToParachainMetadata
import io.novafoundation.nova.runtime.ext.externalApi
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.metadata.storage
import io.novasama.substrate_sdk_android.runtime.metadata.storageKey
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
import java.math.BigInteger
class CrowdloanRepositoryImpl(
private val remoteStorage: StorageDataSource,
private val chainRegistry: ChainRegistry,
private val parachainMetadataApi: ParachainMetadataApi
) : CrowdloanRepository {
override suspend fun allFundInfos(chainId: ChainId): Map<ParaId, FundInfo> {
return remoteStorage.query(chainId) {
runtime.metadata.crowdloan().storage("Funds").entries(
keyExtractor = { (paraId: BigInteger) -> paraId },
binding = { instance, paraId -> bindFundInfo(instance, runtime, paraId) }
)
}
}
override suspend fun getWinnerInfo(chainId: ChainId, funds: Map<ParaId, FundInfo>): Map<ParaId, Boolean> {
return remoteStorage.query(chainId) {
runtime.metadata.slots().storage("Leases").singleArgumentEntries(
keysArguments = funds.keys,
binding = { decoded, paraId ->
val leases = decoded?.let { bindLeases(it) }
val fund = funds.getValue(paraId)
leases?.let { isWinner(leases, fund) } ?: false
}
)
}
}
private fun isWinner(leases: List<LeaseEntry?>, fundInfo: FundInfo): Boolean {
return leases.any { it.isOwnedBy(fundInfo.bidderAccountId) || it.isOwnedBy(fundInfo.pre9180BidderAccountId) }
}
private fun LeaseEntry?.isOwnedBy(accountId: AccountId): Boolean = this?.accountId.contentEquals(accountId)
override suspend fun getParachainMetadata(chain: Chain): Map<ParaId, ParachainMetadata> {
return withContext(Dispatchers.Default) {
chain.externalApi<Chain.ExternalApi.Crowdloans>()?.let { section ->
parachainMetadataApi.getParachainMetadata(section.url)
.associateBy { it.paraid }
.mapValues { (_, remoteMetadata) -> mapParachainMetadataRemoteToParachainMetadata(remoteMetadata) }
} ?: emptyMap()
}
}
override suspend fun leasePeriodToBlocksConverter(chainId: ChainId): LeasePeriodToBlocksConverter {
val runtime = runtimeFor(chainId)
val slots = runtime.metadata.slots()
return LeasePeriodToBlocksConverter(
blocksPerLease = slots.numberConstant("LeasePeriod", runtime),
blocksOffset = slots.numberConstant("LeaseOffset", runtime)
)
}
override fun fundInfoFlow(chainId: ChainId, parachainId: ParaId): Flow<FundInfo> {
return remoteStorage.observe(
keyBuilder = { it.metadata.crowdloan().storage("Funds").storageKey(it, parachainId) },
binder = { scale, runtime -> bindFundInfo(scale!!, runtime, parachainId) },
chainId = chainId
)
}
override suspend fun minContribution(chainId: ChainId): BigInteger {
val runtime = runtimeFor(chainId)
return runtime.metadata.crowdloan().numberConstant("MinContribution", runtime)
}
private suspend fun runtimeFor(chainId: String) = chainRegistry.getRuntime(chainId)
}
@@ -0,0 +1,47 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.repository.contributions.network
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
import io.novafoundation.nova.common.data.network.runtime.binding.ParaId
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdKey
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
import io.novafoundation.nova.common.data.network.runtime.binding.castToList
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import io.novafoundation.nova.runtime.storage.source.query.StorageQueryContext
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableModule
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry3
import io.novafoundation.nova.runtime.storage.source.query.api.storage3
import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata
import io.novasama.substrate_sdk_android.runtime.metadata.module
import io.novasama.substrate_sdk_android.runtime.metadata.module.Module
@JvmInline
value class AhOpsApi(override val module: Module) : QueryableModule
context(StorageQueryContext)
val RuntimeMetadata.ahOps: AhOpsApi
get() = AhOpsApi(module("AhOps"))
context(StorageQueryContext)
val AhOpsApi.rcCrowdloanReserve: QueryableStorageEntry3<BlockNumber, ParaId, AccountIdKey, Any>
get() = storage3(
name = "RcCrowdloanReserve",
binding = { _, _, _, decoded -> decoded },
key3ToInternalConverter = { it.value },
key3FromInternalConverter = ::bindAccountIdKey
)
context(StorageQueryContext)
val AhOpsApi.rcCrowdloanContribution: QueryableStorageEntry3<BlockNumber, ParaId, AccountIdKey, Balance>
get() = storage3(
name = "RcCrowdloanContribution",
binding = { decoded, _, _, _ -> bindContribution(decoded) },
key3ToInternalConverter = { it.value },
key3FromInternalConverter = ::bindAccountIdKey
)
private fun bindContribution(decoded: Any?): Balance {
val (_, balance) = decoded.castToList() // Tuple
return bindNumber(balance)
}
@@ -0,0 +1,27 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.repository.contributions.source
import io.novafoundation.nova.feature_crowdloan_api.data.source.contribution.ExternalContributionSource
import io.novafoundation.nova.feature_crowdloan_api.data.source.contribution.ExternalContributionSource.ExternalContribution
import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.Contribution
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.acala.AcalaApi
import io.novafoundation.nova.runtime.ext.Geneses
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.repository.ParachainInfoRepository
import io.novasama.substrate_sdk_android.runtime.AccountId
class LiquidAcalaContributionSource(
private val acalaApi: AcalaApi,
private val parachainInfoRepository: ParachainInfoRepository,
) : ExternalContributionSource {
override val supportedChains = setOf(Chain.Geneses.POLKADOT)
override val sourceId: String = Contribution.LIQUID_SOURCE_ID
override suspend fun getContributions(
chain: Chain,
accountId: AccountId,
): Result<List<ExternalContribution>> {
return Result.success(emptyList())
}
}
@@ -0,0 +1,24 @@
package io.novafoundation.nova.feature_crowdloan_impl.data.repository.contributions.source
import io.novafoundation.nova.feature_crowdloan_api.data.source.contribution.ExternalContributionSource
import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.Contribution
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.parallel.ParallelApi
import io.novafoundation.nova.runtime.ext.Geneses
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.runtime.AccountId
class ParallelContributionSource(
private val parallelApi: ParallelApi,
) : ExternalContributionSource {
override val supportedChains = setOf(Chain.Geneses.POLKADOT)
override val sourceId: String = Contribution.PARALLEL_SOURCE_ID
override suspend fun getContributions(
chain: Chain,
accountId: AccountId,
): Result<List<ExternalContributionSource.ExternalContribution>> {
return Result.success(emptyList())
}
}
@@ -0,0 +1,73 @@
package io.novafoundation.nova.feature_crowdloan_impl.di
import dagger.BindsInstance
import dagger.Component
import io.novafoundation.nova.common.di.CommonApi
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.core_db.di.DbApi
import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi
import io.novafoundation.nova.feature_crowdloan_api.di.CrowdloanFeatureApi
import io.novafoundation.nova.feature_crowdloan_impl.di.contributions.ContributionsModule
import io.novafoundation.nova.feature_crowdloan_impl.di.validations.CrowdloansValidationsModule
import io.novafoundation.nova.feature_crowdloan_impl.presentation.CrowdloanRouter
import io.novafoundation.nova.feature_crowdloan_impl.presentation.claimControbution.di.ClaimContributionComponent
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.confirm.di.ConfirmContributeComponent
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.di.CustomContributeComponent
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.moonbeam.terms.di.MoonbeamCrowdloanTermsComponent
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.referral.ReferralContributeView
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.select.di.CrowdloanContributeComponent
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contributions.di.UserContributionsComponent
import io.novafoundation.nova.feature_crowdloan_impl.presentation.main.di.CrowdloanComponent
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
import io.novafoundation.nova.runtime.di.RuntimeApi
@Component(
dependencies = [
CrowdloanFeatureDependencies::class
],
modules = [
CrowdloanFeatureModule::class,
CrowdloanUpdatersModule::class,
CrowdloansValidationsModule::class,
ContributionsModule::class
]
)
@FeatureScope
interface CrowdloanFeatureComponent : CrowdloanFeatureApi {
fun crowdloansFactory(): CrowdloanComponent.Factory
fun userContributionsFactory(): UserContributionsComponent.Factory
fun selectContributeFactory(): CrowdloanContributeComponent.Factory
fun confirmContributeFactory(): ConfirmContributeComponent.Factory
fun customContributeFactory(): CustomContributeComponent.Factory
fun moonbeamTermsFactory(): MoonbeamCrowdloanTermsComponent.Factory
fun claimContributions(): ClaimContributionComponent.Factory
fun inject(view: ReferralContributeView)
@Component.Factory
interface Factory {
fun create(
@BindsInstance router: CrowdloanRouter,
deps: CrowdloanFeatureDependencies,
): CrowdloanFeatureComponent
}
@Component(
dependencies = [
CommonApi::class,
DbApi::class,
RuntimeApi::class,
AccountFeatureApi::class,
WalletFeatureApi::class
]
)
interface CrowdloanFeatureDependenciesComponent : CrowdloanFeatureDependencies
}
@@ -0,0 +1,126 @@
package io.novafoundation.nova.feature_crowdloan_impl.di
import coil.ImageLoader
import com.google.gson.Gson
import io.novafoundation.nova.common.address.AddressIconGenerator
import io.novafoundation.nova.common.data.network.AppLinksProvider
import io.novafoundation.nova.common.data.network.HttpExceptionHandler
import io.novafoundation.nova.common.data.network.NetworkApiCreator
import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer
import io.novafoundation.nova.common.presentation.AssetIconProvider
import io.novafoundation.nova.common.presentation.masking.formatter.MaskableValueFormatterFactory
import io.novafoundation.nova.common.presentation.masking.formatter.MaskableValueFormatterProvider
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.validation.ValidationExecutor
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core_db.dao.ContributionDao
import io.novafoundation.nova.core_db.dao.ExternalBalanceDao
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
import io.novafoundation.nova.feature_account_api.domain.updaters.AccountUpdateScope
import io.novafoundation.nova.feature_account_api.presenatation.account.AddressDisplayUseCase
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions
import io.novafoundation.nova.feature_account_api.presenatation.navigation.ExtrinsicNavigationWrapper
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletConstants
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.AssetModelFormatter
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2
import io.novafoundation.nova.runtime.di.LOCAL_STORAGE_SOURCE
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.repository.ChainStateRepository
import io.novafoundation.nova.runtime.repository.ParachainInfoRepository
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import javax.inject.Named
interface CrowdloanFeatureDependencies {
val maskableValueFormatterFactory: MaskableValueFormatterFactory
val maskableValueFormatterProvider: MaskableValueFormatterProvider
val amountFormatter: AmountFormatter
val parachainInfoRepository: ParachainInfoRepository
val signerProvider: SignerProvider
val storageStorageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory
val externalBalanceDao: ExternalBalanceDao
val assetModelFormatter: AssetModelFormatter
val assetIconProvider: AssetIconProvider
val extrinsicNavigationWrapper: ExtrinsicNavigationWrapper
val feeLoaderMixinV2Factory: FeeLoaderMixinV2.Factory
val walletUIUseCase: WalletUiUseCase
fun contributionDao(): ContributionDao
fun accountUpdaterScope(): AccountUpdateScope
fun selectedAccountUseCase(): SelectedAccountUseCase
fun walletConstants(): WalletConstants
fun storageCache(): StorageCache
fun imageLoader(): ImageLoader
fun accountRepository(): AccountRepository
fun addressIconGenerator(): AddressIconGenerator
fun appLinksProvider(): AppLinksProvider
fun walletRepository(): WalletRepository
fun tokenRepository(): TokenRepository
fun resourceManager(): ResourceManager
fun externalAccountActions(): ExternalActions.Presentation
fun networkApiCreator(): NetworkApiCreator
fun httpExceptionHandler(): HttpExceptionHandler
fun gson(): Gson
fun addressxDisplayUseCase(): AddressDisplayUseCase
fun extrinsicService(): ExtrinsicService
fun validationExecutor(): ValidationExecutor
@Named(REMOTE_STORAGE_SOURCE)
fun remoteStorageSource(): StorageDataSource
@Named(LOCAL_STORAGE_SOURCE)
fun localStorageSource(): StorageDataSource
fun chainStateRepository(): ChainStateRepository
fun chainRegistry(): ChainRegistry
fun preferences(): Preferences
fun secretStoreV2(): SecretStoreV2
fun customDialogDisplayer(): CustomDialogDisplayer.Presentation
fun feeLoaderMixinFactory(): FeeLoaderMixin.Factory
}
@@ -0,0 +1,31 @@
package io.novafoundation.nova.feature_crowdloan_impl.di
import io.novafoundation.nova.common.di.FeatureApiHolder
import io.novafoundation.nova.common.di.FeatureContainer
import io.novafoundation.nova.common.di.scope.ApplicationScope
import io.novafoundation.nova.core_db.di.DbApi
import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi
import io.novafoundation.nova.feature_crowdloan_impl.presentation.CrowdloanRouter
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
import io.novafoundation.nova.runtime.di.RuntimeApi
import javax.inject.Inject
@ApplicationScope
class CrowdloanFeatureHolder @Inject constructor(
featureContainer: FeatureContainer,
private val router: CrowdloanRouter
) : FeatureApiHolder(featureContainer) {
override fun initializeDependencies(): Any {
val dependencies = DaggerCrowdloanFeatureComponent_CrowdloanFeatureDependenciesComponent.builder()
.commonApi(commonApi())
.runtimeApi(getFeature(RuntimeApi::class.java))
.dbApi(getFeature(DbApi::class.java))
.walletFeatureApi(getFeature(WalletFeatureApi::class.java))
.accountFeatureApi(getFeature(AccountFeatureApi::class.java))
.build()
return DaggerCrowdloanFeatureComponent.factory()
.create(router, dependencies)
}
}
@@ -0,0 +1,105 @@
package io.novafoundation.nova.feature_crowdloan_impl.di
import dagger.Module
import dagger.Provides
import io.novafoundation.nova.common.data.network.NetworkApiCreator
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_crowdloan_api.data.repository.ContributionsRepository
import io.novafoundation.nova.feature_crowdloan_api.data.repository.CrowdloanRepository
import io.novafoundation.nova.feature_crowdloan_impl.data.CrowdloanSharedState
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.parachain.ParachainMetadataApi
import io.novafoundation.nova.feature_crowdloan_impl.data.repository.CrowdloanRepositoryImpl
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.CustomContributeManager
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.CustomContributeModule
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.CrowdloanContributeInteractor
import io.novafoundation.nova.feature_crowdloan_impl.domain.main.CrowdloanInteractor
import io.novafoundation.nova.feature_wallet_api.di.common.SelectableAssetUseCaseModule
import io.novafoundation.nova.feature_wallet_api.domain.TokenUseCase
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.repository.ChainStateRepository
import io.novafoundation.nova.runtime.state.SelectableSingleAssetSharedState
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import javax.inject.Named
@Module(
includes = [
CustomContributeModule::class,
SelectableAssetUseCaseModule::class,
]
)
class CrowdloanFeatureModule {
@Provides
@FeatureScope
fun provideCrowdloanSharedState(
chainRegistry: ChainRegistry,
preferences: Preferences,
) = CrowdloanSharedState(chainRegistry, preferences)
@Provides
@FeatureScope
fun provideSelectableSharedState(crowdloanSharedState: CrowdloanSharedState): SelectableSingleAssetSharedState<*> = crowdloanSharedState
@Provides
@FeatureScope
fun provideFeeLoaderMixin(
feeLoaderMixinFactory: FeeLoaderMixin.Factory,
tokenUseCase: TokenUseCase,
): FeeLoaderMixin.Presentation = feeLoaderMixinFactory.create(tokenUseCase)
@Provides
@FeatureScope
fun crowdloanRepository(
@Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource,
crowdloanMetadataApi: ParachainMetadataApi,
chainRegistry: ChainRegistry,
): CrowdloanRepository = CrowdloanRepositoryImpl(
remoteStorageSource,
chainRegistry,
crowdloanMetadataApi
)
@Provides
@FeatureScope
fun provideCrowdloanInteractor(
crowdloanRepository: CrowdloanRepository,
chainStateRepository: ChainStateRepository,
contributionsRepository: ContributionsRepository
) = CrowdloanInteractor(
crowdloanRepository,
chainStateRepository,
contributionsRepository
)
@Provides
@FeatureScope
fun provideCrowdloanMetadataApi(networkApiCreator: NetworkApiCreator): ParachainMetadataApi {
return networkApiCreator.create(ParachainMetadataApi::class.java)
}
@Provides
@FeatureScope
fun provideCrowdloanContributeInteractor(
extrinsicService: ExtrinsicService,
accountRepository: AccountRepository,
chainStateRepository: ChainStateRepository,
sharedState: CrowdloanSharedState,
crowdloanRepository: CrowdloanRepository,
customContributeManager: CustomContributeManager,
contributionsRepository: ContributionsRepository
) = CrowdloanContributeInteractor(
extrinsicService,
accountRepository,
chainStateRepository,
customContributeManager,
sharedState,
crowdloanRepository,
contributionsRepository
)
}
@@ -0,0 +1,46 @@
package io.novafoundation.nova.feature_crowdloan_impl.di
import dagger.Module
import dagger.Provides
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.core.storage.StorageCache
import io.novafoundation.nova.core.updater.UpdateSystem
import io.novafoundation.nova.feature_crowdloan_impl.data.CrowdloanSharedState
import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.network.updaters.SharedAssetBlockNumberUpdater
import io.novafoundation.nova.runtime.network.updaters.multiChain.DelegateToTimeLineChainUpdater
import io.novafoundation.nova.runtime.network.updaters.multiChain.DelegateToTimelineChainIdHolder
import io.novafoundation.nova.runtime.network.updaters.multiChain.GroupBySyncChainMultiChainUpdateSystem
@Module
class CrowdloanUpdatersModule {
@Provides
@FeatureScope
fun provideTimelineDelegatingHolder(sharedState: CrowdloanSharedState) = DelegateToTimelineChainIdHolder(sharedState)
@Provides
@FeatureScope
fun provideBlockNumberUpdater(
chainRegistry: ChainRegistry,
chainIdHolder: DelegateToTimelineChainIdHolder,
storageCache: StorageCache,
) = SharedAssetBlockNumberUpdater(chainRegistry, chainIdHolder, storageCache)
@Provides
@FeatureScope
fun provideCrowdloanUpdateSystem(
chainRegistry: ChainRegistry,
crowdloanSharedState: CrowdloanSharedState,
blockNumberUpdater: SharedAssetBlockNumberUpdater,
storageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory,
): UpdateSystem = GroupBySyncChainMultiChainUpdateSystem(
updaters = listOf(
DelegateToTimeLineChainUpdater(blockNumberUpdater)
),
chainRegistry = chainRegistry,
singleAssetSharedState = crowdloanSharedState,
storageSharedRequestsBuilderFactory = storageSharedRequestsBuilderFactory
)
}
@@ -0,0 +1,121 @@
package io.novafoundation.nova.feature_crowdloan_impl.di.contributions
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoSet
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.core_db.dao.ContributionDao
import io.novafoundation.nova.core_db.dao.ExternalBalanceDao
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_crowdloan_api.data.network.updater.ContributionsUpdateSystemFactory
import io.novafoundation.nova.feature_crowdloan_api.data.network.updater.ContributionsUpdaterFactory
import io.novafoundation.nova.feature_crowdloan_api.data.repository.ContributionsRepository
import io.novafoundation.nova.feature_crowdloan_api.data.repository.CrowdloanRepository
import io.novafoundation.nova.feature_crowdloan_api.data.source.contribution.ExternalContributionSource
import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.ContributionsInteractor
import io.novafoundation.nova.feature_crowdloan_impl.data.CrowdloanSharedState
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.acala.AcalaApi
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.parallel.ParallelApi
import io.novafoundation.nova.feature_crowdloan_impl.data.network.updater.AssetBalanceScopeFactory
import io.novafoundation.nova.feature_crowdloan_impl.data.network.updater.RealContributionsUpdateSystemFactory
import io.novafoundation.nova.feature_crowdloan_impl.data.network.updater.RealContributionsUpdaterFactory
import io.novafoundation.nova.feature_crowdloan_impl.data.repository.contributions.source.LiquidAcalaContributionSource
import io.novafoundation.nova.feature_crowdloan_impl.data.repository.contributions.source.ParallelContributionSource
import io.novafoundation.nova.feature_crowdloan_impl.domain.contributions.RealContributionsInteractor
import io.novafoundation.nova.feature_crowdloan_impl.domain.contributions.RealContributionsRepository
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.repository.ChainStateRepository
import io.novafoundation.nova.runtime.repository.ParachainInfoRepository
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import javax.inject.Named
@Module
class ContributionsModule {
@Provides
@FeatureScope
@IntoSet
fun acalaLiquidSource(
acalaApi: AcalaApi,
parachainInfoRepository: ParachainInfoRepository
): ExternalContributionSource = LiquidAcalaContributionSource(acalaApi, parachainInfoRepository)
@Provides
@FeatureScope
@IntoSet
fun parallelSource(
parallelApi: ParallelApi,
): ExternalContributionSource = ParallelContributionSource(parallelApi)
@Provides
@FeatureScope
fun provideContributionsInteractor(
crowdloanRepository: CrowdloanRepository,
accountRepository: AccountRepository,
crowdloanSharedState: CrowdloanSharedState,
chainStateRepository: ChainStateRepository,
contributionsRepository: ContributionsRepository,
chainRegistry: ChainRegistry,
contributionsUpdateSystemFactory: ContributionsUpdateSystemFactory
): ContributionsInteractor = RealContributionsInteractor(
crowdloanRepository = crowdloanRepository,
accountRepository = accountRepository,
selectedAssetCrowdloanState = crowdloanSharedState,
chainStateRepository = chainStateRepository,
contributionsRepository = contributionsRepository,
chainRegistry = chainRegistry,
contributionsUpdateSystemFactory = contributionsUpdateSystemFactory
)
@Provides
@FeatureScope
fun provideContributionsRepository(
externalContributionsSources: Set<@JvmSuppressWildcards ExternalContributionSource>,
chainRegistry: ChainRegistry,
@Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource,
contributionDao: ContributionDao
): ContributionsRepository {
return RealContributionsRepository(
externalContributionsSources.toList(),
chainRegistry,
remoteStorageSource,
contributionDao
)
}
@Provides
@FeatureScope
fun provideContributionsUpdaterFactory(
contributionsRepository: ContributionsRepository,
contributionDao: ContributionDao,
externalBalanceDao: ExternalBalanceDao
): ContributionsUpdaterFactory = RealContributionsUpdaterFactory(
contributionsRepository,
contributionDao,
externalBalanceDao
)
@Provides
@FeatureScope
fun provideContributionUpdateSystemFactory(
contributionsUpdaterFactory: ContributionsUpdaterFactory,
chainRegistry: ChainRegistry,
assetBalanceScopeFactory: AssetBalanceScopeFactory,
storageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory,
): ContributionsUpdateSystemFactory = RealContributionsUpdateSystemFactory(
chainRegistry = chainRegistry,
contributionsUpdaterFactory = contributionsUpdaterFactory,
assetBalanceScopeFactory = assetBalanceScopeFactory,
storageSharedRequestsBuilderFactory = storageSharedRequestsBuilderFactory
)
@Provides
@FeatureScope
fun provideAssetBalanceScopeFactory(
walletRepository: WalletRepository,
accountRepository: AccountRepository
) = AssetBalanceScopeFactory(walletRepository, accountRepository)
}
@@ -0,0 +1,45 @@
package io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan
import android.content.Context
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.PrivateCrowdloanSignatureProvider
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.ConfirmContributeCustomization
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.CustomContributeSubmitter
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.CustomContributeView
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.CustomContributeViewState
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.SelectContributeCustomization
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.StartFlowInterceptor
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.model.CustomContributePayload
import kotlinx.coroutines.CoroutineScope
interface CustomContributeFactory {
val flowType: String
val privateCrowdloanSignatureProvider: PrivateCrowdloanSignatureProvider?
get() = null
val submitter: CustomContributeSubmitter
val startFlowInterceptor: StartFlowInterceptor?
get() = null
val extraBonusFlow: ExtraBonusFlow?
get() = null
val selectContributeCustomization: SelectContributeCustomization?
get() = null
val confirmContributeCustomization: ConfirmContributeCustomization?
get() = null
}
interface ExtraBonusFlow {
fun createViewState(scope: CoroutineScope, payload: CustomContributePayload): CustomContributeViewState
fun createView(context: Context): CustomContributeView
}
fun CustomContributeFactory.supports(otherFlowType: String): Boolean {
return otherFlowType == flowType
}
@@ -0,0 +1,30 @@
package io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan
class CustomContributeManager(
private val factories: Set<CustomContributeFactory>
) {
fun getFactoryOrNull(flowType: String): CustomContributeFactory? = relevantFactoryOrNull(flowType)
private fun relevantFactory(flowType: String) = relevantFactoryOrNull(flowType) ?: noFactoryFound(flowType)
private fun relevantFactoryOrNull(
flowType: String,
): CustomContributeFactory? {
return factories.firstOrNull { it.supports(flowType) }
}
fun relevantExtraBonusFlow(flowType: String): ExtraBonusFlow {
val factory = relevantFactory(flowType)
return factory.extraBonusFlow ?: unexpectedBonusFlow(flowType)
}
private fun noFactoryFound(flowType: String): Nothing = throw NoSuchElementException("Factory for $flowType was not found")
private fun unexpectedBonusFlow(flowType: String): Nothing = throw IllegalStateException("No extra bonus flow found for flow $flowType")
}
fun CustomContributeManager.hasExtraBonusFlow(flowType: String) = getFactoryOrNull(flowType)?.extraBonusFlow != null
fun CustomContributeManager.supportsPrivateCrowdloans(flowType: String) = getFactoryOrNull(flowType)?.privateCrowdloanSignatureProvider != null
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan
import dagger.Module
import dagger.Provides
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.acala.AcalaContributionModule
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.astar.AstarContributionModule
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.bifrost.BifrostContributionModule
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.moonbeam.MoonbeamContributionModule
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.parallel.ParallelContributionModule
@Module(
includes = [
AcalaContributionModule::class,
BifrostContributionModule::class,
MoonbeamContributionModule::class,
AstarContributionModule::class,
ParallelContributionModule::class
]
)
class CustomContributeModule {
@Provides
@FeatureScope
fun provideCustomContributionManager(
factories: @JvmSuppressWildcards Set<CustomContributeFactory>,
) = CustomContributeManager(factories)
}
@@ -0,0 +1,79 @@
package io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.acala
import android.content.Context
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_crowdloan_impl.BuildConfig
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.CustomContributeFactory
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.ExtraBonusFlow
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.acala.AcalaContributeInteractor
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.CustomContributeViewState
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.acala.bonus.AcalaContributeSubmitter
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.acala.bonus.AcalaContributeViewState
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.acala.main.confirm.AcalaConfirmContributeCustomization
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.acala.main.select.AcalaSelectContributeCustomization
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.model.CustomContributePayload
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.referral.ReferralContributeView
import kotlinx.coroutines.CoroutineScope
import java.math.BigDecimal
abstract class AcalaBasedContributeFactory(
override val submitter: AcalaContributeSubmitter,
override val extraBonusFlow: AcalaBasedExtraBonusFlow,
) : CustomContributeFactory
abstract class AcalaBasedExtraBonusFlow(
private val interactor: AcalaContributeInteractor,
private val resourceManager: ResourceManager,
private val defaultReferralCode: String,
) : ExtraBonusFlow {
protected open val bonusMultiplier: BigDecimal = 0.05.toBigDecimal()
override fun createViewState(scope: CoroutineScope, payload: CustomContributePayload): CustomContributeViewState {
return AcalaContributeViewState(interactor, payload, resourceManager, defaultReferralCode, bonusMultiplier)
}
override fun createView(context: Context) = ReferralContributeView(context)
}
class AcalaContributeFactory(
submitter: AcalaContributeSubmitter,
extraBonusFlow: AcalaExtraBonusFlow,
override val selectContributeCustomization: AcalaSelectContributeCustomization,
override val confirmContributeCustomization: AcalaConfirmContributeCustomization,
) : AcalaBasedContributeFactory(
submitter = submitter,
extraBonusFlow = extraBonusFlow
) {
override val flowType: String = "Acala"
}
class AcalaExtraBonusFlow(
interactor: AcalaContributeInteractor,
resourceManager: ResourceManager,
) : AcalaBasedExtraBonusFlow(
interactor = interactor,
resourceManager = resourceManager,
defaultReferralCode = BuildConfig.ACALA_NOVA_REFERRAL
)
class KaruraContributeFactory(
submitter: AcalaContributeSubmitter,
extraBonusFlow: KaruraExtraBonusFlow,
) : AcalaBasedContributeFactory(
submitter = submitter,
extraBonusFlow = extraBonusFlow
) {
override val flowType: String = "Karura"
}
class KaruraExtraBonusFlow(
interactor: AcalaContributeInteractor,
resourceManager: ResourceManager,
) : AcalaBasedExtraBonusFlow(
interactor = interactor,
resourceManager = resourceManager,
defaultReferralCode = BuildConfig.KARURA_NOVA_REFERRAL
)
@@ -0,0 +1,116 @@
package io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.acala
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoSet
import io.novafoundation.nova.common.data.network.HttpExceptionHandler
import io.novafoundation.nova.common.data.network.NetworkApiCreator
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_crowdloan_impl.data.CrowdloanSharedState
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.acala.AcalaApi
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.CustomContributeFactory
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.acala.AcalaContributeInteractor
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.acala.bonus.AcalaContributeSubmitter
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.acala.main.confirm.AcalaConfirmContributeCustomization
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.acala.main.confirm.AcalaConfirmContributeViewStateFactory
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.acala.main.select.AcalaSelectContributeCustomization
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
@Module
class AcalaContributionModule {
@Provides
@FeatureScope
fun provideAcalaApi(
networkApiCreator: NetworkApiCreator,
) = networkApiCreator.create(AcalaApi::class.java)
@Provides
@FeatureScope
fun provideAcalaInteractor(
acalaApi: AcalaApi,
httpExceptionHandler: HttpExceptionHandler,
selectAssetSharedState: CrowdloanSharedState,
chainRegistry: ChainRegistry,
accountRepository: AccountRepository,
signerProvider: SignerProvider
) = AcalaContributeInteractor(
acalaApi = acalaApi,
httpExceptionHandler = httpExceptionHandler,
accountRepository = accountRepository,
chainRegistry = chainRegistry,
selectedAssetState = selectAssetSharedState,
signerProvider = signerProvider
)
@Provides
@FeatureScope
fun provideAcalaSubmitter(
interactor: AcalaContributeInteractor,
) = AcalaContributeSubmitter(interactor)
@Provides
@FeatureScope
fun provideAcalaExtraBonusFlow(
acalaInteractor: AcalaContributeInteractor,
resourceManager: ResourceManager,
): AcalaExtraBonusFlow = AcalaExtraBonusFlow(
interactor = acalaInteractor,
resourceManager = resourceManager,
)
@Provides
@FeatureScope
fun provideKaruraExtraBonusFlow(
acalaInteractor: AcalaContributeInteractor,
resourceManager: ResourceManager,
): KaruraExtraBonusFlow = KaruraExtraBonusFlow(
interactor = acalaInteractor,
resourceManager = resourceManager,
)
@Provides
@FeatureScope
fun provideAcalaSelectContributeCustomization(): AcalaSelectContributeCustomization = AcalaSelectContributeCustomization()
@Provides
@FeatureScope
fun provideAcalaConfirmContributeViewStateFactory(
resourceManager: ResourceManager,
) = AcalaConfirmContributeViewStateFactory(resourceManager)
@Provides
@FeatureScope
fun provideAcalaConfirmContributeCustomization(
viewStateFactory: AcalaConfirmContributeViewStateFactory,
): AcalaConfirmContributeCustomization = AcalaConfirmContributeCustomization(viewStateFactory)
@Provides
@FeatureScope
@IntoSet
fun provideAcalaFactory(
submitter: AcalaContributeSubmitter,
acalaExtraBonusFlow: AcalaExtraBonusFlow,
acalaSelectContributeCustomization: AcalaSelectContributeCustomization,
acalaConfirmContributeCustomization: AcalaConfirmContributeCustomization,
): CustomContributeFactory = AcalaContributeFactory(
submitter = submitter,
extraBonusFlow = acalaExtraBonusFlow,
selectContributeCustomization = acalaSelectContributeCustomization,
confirmContributeCustomization = acalaConfirmContributeCustomization
)
@Provides
@FeatureScope
@IntoSet
fun provideKaruraFactory(
submitter: AcalaContributeSubmitter,
karuraExtraBonusFlow: KaruraExtraBonusFlow,
): CustomContributeFactory = KaruraContributeFactory(
submitter = submitter,
extraBonusFlow = karuraExtraBonusFlow
)
}
@@ -0,0 +1,47 @@
package io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.astar
import android.content.Context
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.CustomContributeFactory
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.ExtraBonusFlow
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.astar.AstarContributeInteractor
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.astar.AstarContributeSubmitter
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.astar.AstarContributeViewState
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.model.CustomContributePayload
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.referral.ReferralContributeView
import kotlinx.coroutines.CoroutineScope
import java.math.BigDecimal
private const val ASTAR_TERMS_LINK = "https://docs.google.com/document/d/1vKZrDqSdh706hg0cqJ_NnxfRSlXR2EThVHwoRl0nAkk"
private const val NOVA_REFERRAL_CODE = "1ChFWeNRLarAPRCTM3bfJmncJbSAbSS9yqjueWz7jX7iTVZ"
private val ASTAR_BONUS = 0.01.toBigDecimal() // 1%
class AstarExtraBonusFlow(
private val interactor: AstarContributeInteractor,
private val resourceManager: ResourceManager,
private val termsLink: String = ASTAR_TERMS_LINK,
private val novaReferralCode: String = NOVA_REFERRAL_CODE,
private val bonusPercentage: BigDecimal = ASTAR_BONUS,
) : ExtraBonusFlow {
override fun createViewState(scope: CoroutineScope, payload: CustomContributePayload): AstarContributeViewState {
return AstarContributeViewState(
interactor = interactor,
customContributePayload = payload,
resourceManager = resourceManager,
defaultReferralCode = novaReferralCode,
bonusPercentage = bonusPercentage,
termsLink = termsLink
)
}
override fun createView(context: Context) = ReferralContributeView(context)
}
class AstarContributeFactory(
override val submitter: AstarContributeSubmitter,
override val extraBonusFlow: AstarExtraBonusFlow,
) : CustomContributeFactory {
override val flowType = "Astar"
}
@@ -0,0 +1,48 @@
package io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.astar
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoSet
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_crowdloan_impl.data.CrowdloanSharedState
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.CustomContributeFactory
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.astar.AstarContributeInteractor
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.astar.AstarContributeSubmitter
@Module
class AstarContributionModule {
@Provides
@FeatureScope
fun provideAstarInteractor(
selectedAssetSharedState: CrowdloanSharedState,
) = AstarContributeInteractor(selectedAssetSharedState)
@Provides
@FeatureScope
fun provideAstarSubmitter(
interactor: AstarContributeInteractor,
) = AstarContributeSubmitter(interactor)
@Provides
@FeatureScope
fun provideAstarExtraFlow(
interactor: AstarContributeInteractor,
resourceManager: ResourceManager,
) = AstarExtraBonusFlow(
interactor = interactor,
resourceManager = resourceManager
)
@Provides
@FeatureScope
@IntoSet
fun provideAstarFactory(
submitter: AstarContributeSubmitter,
astarExtraBonusFlow: AstarExtraBonusFlow,
): CustomContributeFactory = AstarContributeFactory(
submitter = submitter,
extraBonusFlow = astarExtraBonusFlow
)
}
@@ -0,0 +1,43 @@
package io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.bifrost
import android.content.Context
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_crowdloan_impl.BuildConfig
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.CustomContributeFactory
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.ExtraBonusFlow
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.bifrost.BifrostContributeInteractor
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.bifrost.BifrostContributeSubmitter
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.bifrost.BifrostContributeViewState
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.model.CustomContributePayload
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.referral.ReferralContributeView
import kotlinx.coroutines.CoroutineScope
private val BIFROST_BONUS_MULTIPLIER = 0.05.toBigDecimal() // 5%
class BifrostExtraBonusFlow(
private val interactor: BifrostContributeInteractor,
private val resourceManager: ResourceManager,
private val termsLink: String = BuildConfig.BIFROST_TERMS_LINKS,
) : ExtraBonusFlow {
override fun createViewState(scope: CoroutineScope, payload: CustomContributePayload): BifrostContributeViewState {
return BifrostContributeViewState(
interactor = interactor,
customContributePayload = payload,
resourceManager = resourceManager,
termsLink = termsLink,
bonusPercentage = BIFROST_BONUS_MULTIPLIER,
bifrostInteractor = interactor
)
}
override fun createView(context: Context) = ReferralContributeView(context)
}
class BifrostContributeFactory(
override val submitter: BifrostContributeSubmitter,
override val extraBonusFlow: BifrostExtraBonusFlow,
) : CustomContributeFactory {
override val flowType = "Bifrost"
}
@@ -0,0 +1,55 @@
package io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.bifrost
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoSet
import io.novafoundation.nova.common.data.network.HttpExceptionHandler
import io.novafoundation.nova.common.data.network.NetworkApiCreator
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_crowdloan_impl.BuildConfig
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.bifrost.BifrostApi
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.CustomContributeFactory
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.bifrost.BifrostContributeInteractor
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.bifrost.BifrostContributeSubmitter
@Module
class BifrostContributionModule {
@Provides
@FeatureScope
fun provideBifrostApi(networkApiCreator: NetworkApiCreator): BifrostApi {
return networkApiCreator.create(BifrostApi::class.java, customBaseUrl = BifrostApi.BASE_URL)
}
@Provides
@FeatureScope
fun provideBifrostInteractor(
bifrostApi: BifrostApi,
httpExceptionHandler: HttpExceptionHandler,
) = BifrostContributeInteractor(BuildConfig.BIFROST_NOVA_REFERRAL, bifrostApi, httpExceptionHandler)
@Provides
@FeatureScope
fun provideBifrostSubmitter(
interactor: BifrostContributeInteractor,
) = BifrostContributeSubmitter(interactor)
@Provides
@FeatureScope
fun provideBifrostExtraFlow(
interactor: BifrostContributeInteractor,
resourceManager: ResourceManager,
) = BifrostExtraBonusFlow(interactor, resourceManager)
@Provides
@FeatureScope
@IntoSet
fun provideBifrostFactory(
submitter: BifrostContributeSubmitter,
bifrostExtraBonusFlow: BifrostExtraBonusFlow,
): CustomContributeFactory = BifrostContributeFactory(
submitter,
bifrostExtraBonusFlow
)
}
@@ -0,0 +1,19 @@
package io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.moonbeam
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.CustomContributeFactory
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.moonbeam.MoonbeamPrivateSignatureProvider
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.CustomContributeSubmitter
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.moonbeam.MoonbeamStartFlowInterceptor
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.moonbeam.main.ConfirmContributeMoonbeamCustomization
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.moonbeam.main.SelectContributeMoonbeamCustomization
class MoonbeamContributeFactory(
override val submitter: CustomContributeSubmitter,
override val startFlowInterceptor: MoonbeamStartFlowInterceptor,
override val privateCrowdloanSignatureProvider: MoonbeamPrivateSignatureProvider,
override val selectContributeCustomization: SelectContributeMoonbeamCustomization,
override val confirmContributeCustomization: ConfirmContributeMoonbeamCustomization,
) : CustomContributeFactory {
override val flowType: String = "Moonbeam"
}
@@ -0,0 +1,121 @@
package io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.moonbeam
import coil.ImageLoader
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoSet
import io.novafoundation.nova.common.address.AddressIconGenerator
import io.novafoundation.nova.common.data.network.HttpExceptionHandler
import io.novafoundation.nova.common.data.network.NetworkApiCreator
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_crowdloan_impl.data.CrowdloanSharedState
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.moonbeam.MoonbeamApi
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.CustomContributeFactory
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.moonbeam.MoonbeamCrowdloanInteractor
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.moonbeam.MoonbeamPrivateSignatureProvider
import io.novafoundation.nova.feature_crowdloan_impl.presentation.CrowdloanRouter
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.moonbeam.MoonbeamCrowdloanSubmitter
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.moonbeam.MoonbeamStartFlowInterceptor
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.moonbeam.main.ConfirmContributeMoonbeamCustomization
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.moonbeam.main.MoonbeamMainFlowCustomViewStateFactory
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.moonbeam.main.SelectContributeMoonbeamCustomization
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
@Module
class MoonbeamContributionModule {
@Provides
@FeatureScope
fun provideMoonbeamApi(
networkApiCreator: NetworkApiCreator,
) = networkApiCreator.create(MoonbeamApi::class.java)
@Provides
@FeatureScope
fun provideMoonbeamInteractor(
accountRepository: AccountRepository,
extrinsicService: ExtrinsicService,
moonbeamApi: MoonbeamApi,
selectedAssetSharedState: CrowdloanSharedState,
httpExceptionHandler: HttpExceptionHandler,
chainRegistry: ChainRegistry,
signerProvider: SignerProvider
) = MoonbeamCrowdloanInteractor(
accountRepository = accountRepository,
extrinsicService = extrinsicService,
moonbeamApi = moonbeamApi,
selectedChainAssetState = selectedAssetSharedState,
chainRegistry = chainRegistry,
httpExceptionHandler = httpExceptionHandler,
signerProvider = signerProvider
)
@Provides
@FeatureScope
fun provideMoonbeamSubmitter(interactor: MoonbeamCrowdloanInteractor) = MoonbeamCrowdloanSubmitter(interactor)
@Provides
@FeatureScope
fun provideMoonbeamStartFlowInterceptor(
router: CrowdloanRouter,
resourceManager: ResourceManager,
interactor: MoonbeamCrowdloanInteractor,
customDialogDisplayer: CustomDialogDisplayer.Presentation,
) = MoonbeamStartFlowInterceptor(
crowdloanRouter = router,
resourceManager = resourceManager,
moonbeamInteractor = interactor,
customDialogDisplayer = customDialogDisplayer,
)
@Provides
@FeatureScope
fun provideMoonbeamPrivateSignatureProvider(
moonbeamApi: MoonbeamApi,
httpExceptionHandler: HttpExceptionHandler,
) = MoonbeamPrivateSignatureProvider(moonbeamApi, httpExceptionHandler)
@Provides
@FeatureScope
fun provideSelectContributeMoonbeamViewStateFactory(
interactor: MoonbeamCrowdloanInteractor,
resourceManager: ResourceManager,
iconGenerator: AddressIconGenerator,
) = MoonbeamMainFlowCustomViewStateFactory(interactor, resourceManager, iconGenerator)
@Provides
@FeatureScope
fun provideSelectContributeMoonbeamCustomization(
viewStateFactory: MoonbeamMainFlowCustomViewStateFactory,
imageLoader: ImageLoader,
) = SelectContributeMoonbeamCustomization(viewStateFactory, imageLoader)
@Provides
@FeatureScope
fun provideConfirmContributeMoonbeamCustomization(
viewStateFactory: MoonbeamMainFlowCustomViewStateFactory,
imageLoader: ImageLoader,
) = ConfirmContributeMoonbeamCustomization(viewStateFactory, imageLoader)
@Provides
@FeatureScope
@IntoSet
fun provideMoonbeamFactory(
submitter: MoonbeamCrowdloanSubmitter,
moonbeamStartFlowInterceptor: MoonbeamStartFlowInterceptor,
privateSignatureProvider: MoonbeamPrivateSignatureProvider,
selectContributeMoonbeamCustomization: SelectContributeMoonbeamCustomization,
confirmContributeMoonbeamCustomization: ConfirmContributeMoonbeamCustomization,
): CustomContributeFactory = MoonbeamContributeFactory(
submitter = submitter,
startFlowInterceptor = moonbeamStartFlowInterceptor,
privateCrowdloanSignatureProvider = privateSignatureProvider,
selectContributeCustomization = selectContributeMoonbeamCustomization,
confirmContributeCustomization = confirmContributeMoonbeamCustomization
)
}
@@ -0,0 +1,17 @@
package io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.parallel
import dagger.Module
import dagger.Provides
import io.novafoundation.nova.common.data.network.NetworkApiCreator
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.parallel.ParallelApi
@Module
class ParallelContributionModule {
@Provides
@FeatureScope
fun provideParallelApi(
networkApiCreator: NetworkApiCreator,
) = networkApiCreator.create(ParallelApi::class.java, customBaseUrl = ParallelApi.BASE_URL)
}
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_crowdloan_impl.di.validations
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Select
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Confirm
@@ -0,0 +1,119 @@
package io.novafoundation.nova.feature_crowdloan_impl.di.validations
import dagger.Module
import dagger.Provides
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.feature_crowdloan_api.data.repository.CrowdloanRepository
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.CustomContributeManager
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.BonusAppliedValidation
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.CapExceededValidation
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.ContributeEnoughToPayFeesValidation
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.ContributeExistentialDepositValidation
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.ContributeValidation
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.ContributeValidationFailure
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.CrowdloanNotEndedValidation
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.DefaultMinContributionValidation
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.PublicCrowdloanValidation
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletConstants
import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks
import io.novafoundation.nova.feature_wallet_api.domain.model.balanceCountedTowardsED
import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughAmountToTransferValidationGeneric
import io.novafoundation.nova.runtime.repository.ChainStateRepository
@Module
class ContributeValidationsModule {
@Provides
@FeatureScope
fun provideFeesValidation(): ContributeEnoughToPayFeesValidation = EnoughAmountToTransferValidationGeneric(
feeExtractor = { it.fee },
availableBalanceProducer = { it.asset.transferable },
extraAmountExtractor = { it.contributionAmount },
errorProducer = { ContributeValidationFailure.CannotPayFees }
)
@Provides
@FeatureScope
fun provideMinContributionValidation(
crowdloanRepository: CrowdloanRepository,
) = DefaultMinContributionValidation(crowdloanRepository)
@Provides
@FeatureScope
fun provideCapExceededValidation() = CapExceededValidation()
@Provides
@FeatureScope
fun provideCrowdloanNotEndedValidation(
chainStateRepository: ChainStateRepository,
crowdloanRepository: CrowdloanRepository,
) = CrowdloanNotEndedValidation(chainStateRepository, crowdloanRepository)
@Provides
@FeatureScope
fun provideExistentialWarningValidation(
walletConstants: WalletConstants,
) = ContributeExistentialDepositValidation(
countableTowardsEdBalance = { it.asset.balanceCountedTowardsED() },
feeProducer = { listOf(it.fee) },
extraAmountProducer = { it.contributionAmount },
existentialDeposit = {
val inPlanks = walletConstants.existentialDeposit(it.asset.token.configuration.chainId)
it.asset.token.amountFromPlanks(inPlanks)
},
errorProducer = { _, _ -> ContributeValidationFailure.ExistentialDepositCrossed },
)
@Provides
@FeatureScope
fun providePublicCrowdloanValidation(
customContributeManager: CustomContributeManager,
) = PublicCrowdloanValidation(customContributeManager)
@Provides
@FeatureScope
fun provideBonusAppliedValidation(
customContributeManager: CustomContributeManager,
) = BonusAppliedValidation(customContributeManager)
@Provides
@Select
@FeatureScope
fun provideSelectContributeValidationSet(
feesValidation: ContributeEnoughToPayFeesValidation,
minContributionValidation: DefaultMinContributionValidation,
capExceededValidation: CapExceededValidation,
crowdloanNotEndedValidation: CrowdloanNotEndedValidation,
contributeExistentialDepositValidation: ContributeExistentialDepositValidation,
publicCrowdloanValidation: PublicCrowdloanValidation,
bonusAppliedValidation: BonusAppliedValidation,
): Set<ContributeValidation> = setOf(
feesValidation,
minContributionValidation,
capExceededValidation,
crowdloanNotEndedValidation,
contributeExistentialDepositValidation,
publicCrowdloanValidation,
bonusAppliedValidation
)
@Provides
@Confirm
@FeatureScope
fun provideConfirmContributeValidationSet(
feesValidation: ContributeEnoughToPayFeesValidation,
minContributionValidation: DefaultMinContributionValidation,
capExceededValidation: CapExceededValidation,
crowdloanNotEndedValidation: CrowdloanNotEndedValidation,
contributeExistentialDepositValidation: ContributeExistentialDepositValidation,
publicCrowdloanValidation: PublicCrowdloanValidation,
): Set<ContributeValidation> = setOf(
feesValidation,
minContributionValidation,
capExceededValidation,
crowdloanNotEndedValidation,
contributeExistentialDepositValidation,
publicCrowdloanValidation,
)
}
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_crowdloan_impl.di.validations
import dagger.Module
@Module(
includes = [
ContributeValidationsModule::class,
MoonbeamTermsValidationsModule::class
]
)
class CrowdloansValidationsModule
@@ -0,0 +1,32 @@
package io.novafoundation.nova.feature_crowdloan_impl.di.validations
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoSet
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.validation.CompositeValidation
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.custom.moonbeam.MoonbeamTermsValidation
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.custom.moonbeam.MoonbeamTermsValidationFailure
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.custom.moonbeam.MoonbeamTermsValidationSystem
import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughAmountToTransferValidationGeneric
@Module
class MoonbeamTermsValidationsModule {
@Provides
@IntoSet
@FeatureScope
fun provideFeesValidation(): MoonbeamTermsValidation = EnoughAmountToTransferValidationGeneric(
feeExtractor = { it.fee },
availableBalanceProducer = { it.asset.transferable },
errorProducer = { MoonbeamTermsValidationFailure.CANNOT_PAY_FEES }
)
@Provides
@FeatureScope
fun provideValidationSystem(
contributeValidations: @JvmSuppressWildcards Set<MoonbeamTermsValidation>
) = MoonbeamTermsValidationSystem(
validation = CompositeValidation(contributeValidations.toList())
)
}
@@ -0,0 +1,81 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.claimContributions
import io.novafoundation.nova.common.di.scope.ScreenScope
import io.novafoundation.nova.common.utils.flowOfAll
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.ExtrinsicExecutionResult
import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.requireOk
import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_crowdloan_api.data.repository.ContributionsRepository
import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.Contribution
import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.ContributionClaimStatus
import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.ContributionsWithTotalAmount
import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.claimStatusOf
import io.novafoundation.nova.feature_crowdloan_impl.data.CrowdloanSharedState
import io.novafoundation.nova.feature_crowdloan_impl.data.network.blockhain.extrinsic.claimContribution
import io.novafoundation.nova.runtime.ext.timelineChainIdOrSelf
import io.novafoundation.nova.runtime.repository.ChainStateRepository
import io.novafoundation.nova.runtime.repository.blockDurationEstimatorFlow
import io.novafoundation.nova.runtime.state.chain
import io.novafoundation.nova.runtime.state.chainAndAsset
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import javax.inject.Inject
@ScreenScope
class ClaimContributionsInteractor @Inject constructor(
private val accountRepository: AccountRepository,
private val crowdloanState: CrowdloanSharedState,
private val chainStateRepository: ChainStateRepository,
private val contributionsRepository: ContributionsRepository,
private val extrinsicService: ExtrinsicService,
) {
fun claimableContributions(): Flow<ContributionsWithTotalAmount<Contribution>> {
return flowOfAll {
val account = accountRepository.getSelectedMetaAccount()
val (chain, asset) = crowdloanState.chainAndAsset()
combine(
chainStateRepository.blockDurationEstimatorFlow(chain.timelineChainIdOrSelf()),
contributionsRepository.observeContributions(account, chain, asset)
) { blockDurationEstimator, contributions ->
contributions.filter {
val claimStatus = blockDurationEstimator.claimStatusOf(it)
claimStatus is ContributionClaimStatus.Claimable
}
}
.map { claimableContributions -> ContributionsWithTotalAmount.fromContributions(claimableContributions) }
}
}
suspend fun estimateFee(contributions: List<Contribution>): SubmissionFee {
val chain = crowdloanState.chain()
return extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) { context ->
val depositor = context.submissionOrigin.executingAccount
claim(contributions, depositor)
}
}
suspend fun claim(contributions: List<Contribution>): Result<ExtrinsicExecutionResult> {
val chain = crowdloanState.chain()
return extrinsicService.submitExtrinsicAndAwaitExecution(chain, TransactionOrigin.SelectedWallet) { context ->
val depositor = context.submissionOrigin.executingAccount
claim(contributions, depositor)
}
.requireOk()
}
private fun ExtrinsicBuilder.claim(contributions: List<Contribution>, depositor: AccountId) {
contributions.forEach { contribution ->
claimContribution(contribution.paraId, contribution.unlockBlock, depositor)
}
}
}
@@ -0,0 +1,14 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.claimContributions.validation
import io.novafoundation.nova.feature_wallet_api.domain.validation.NotEnoughToPayFeesError
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import java.math.BigDecimal
sealed class ClaimContributionValidationFailure {
class NotEnoughBalanceToPayFees(
override val chainAsset: Chain.Asset,
override val maxUsable: BigDecimal,
override val fee: BigDecimal
) : ClaimContributionValidationFailure(), NotEnoughToPayFeesError
}
@@ -0,0 +1,13 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.claimContributions.validation
import io.novafoundation.nova.feature_account_api.data.model.Fee
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
class ClaimContributionValidationPayload(
val fee: Fee,
val asset: Asset,
)
val ClaimContributionValidationPayload.chainId: ChainId
get() = asset.token.configuration.chainId
@@ -0,0 +1,26 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.claimContributions.validation
import io.novafoundation.nova.common.validation.ValidationSystem
import io.novafoundation.nova.common.validation.ValidationSystemBuilder
import io.novafoundation.nova.feature_wallet_api.domain.validation.sufficientBalance
typealias ClaimContributionValidationSystem = ValidationSystem<ClaimContributionValidationPayload, ClaimContributionValidationFailure>
typealias ClaimContributionValidationSystemBuilder = ValidationSystemBuilder<ClaimContributionValidationPayload, ClaimContributionValidationFailure>
fun ValidationSystem.Companion.claimContribution(): ClaimContributionValidationSystem = ValidationSystem {
enoughToPayFees()
}
private fun ClaimContributionValidationSystemBuilder.enoughToPayFees() {
sufficientBalance(
fee = { it.fee },
available = { it.asset.transferable },
error = {
ClaimContributionValidationFailure.NotEnoughBalanceToPayFees(
chainAsset = it.payload.asset.token.configuration,
maxUsable = it.maxUsable,
fee = it.fee
)
}
)
}
@@ -0,0 +1,129 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute
import android.os.Parcelable
import io.novafoundation.nova.common.data.network.runtime.binding.ParaId
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission
import io.novafoundation.nova.feature_account_api.data.model.Fee
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.addressIn
import io.novafoundation.nova.feature_crowdloan_api.data.repository.ContributionsRepository
import io.novafoundation.nova.feature_crowdloan_api.data.repository.CrowdloanRepository
import io.novafoundation.nova.feature_crowdloan_api.data.repository.ParachainMetadata
import io.novafoundation.nova.feature_crowdloan_impl.data.CrowdloanSharedState
import io.novafoundation.nova.feature_crowdloan_impl.data.network.blockhain.extrinsic.contribute
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.CustomContributeManager
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.PrivateCrowdloanSignatureProvider.Mode
import io.novafoundation.nova.feature_crowdloan_impl.domain.main.Crowdloan
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.BonusPayload
import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.repository.ChainStateRepository
import io.novafoundation.nova.runtime.state.chainAndAsset
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.withContext
import java.math.BigDecimal
import java.math.BigInteger
typealias OnChainSubmission = suspend ExtrinsicBuilder.() -> Unit
class CrowdloanContributeInteractor(
private val extrinsicService: ExtrinsicService,
private val accountRepository: AccountRepository,
private val chainStateRepository: ChainStateRepository,
private val customContributeManager: CustomContributeManager,
private val crowdloanSharedState: CrowdloanSharedState,
private val crowdloanRepository: CrowdloanRepository,
private val contributionsRepository: ContributionsRepository
) {
fun crowdloanStateFlow(
parachainId: ParaId,
parachainMetadata: ParachainMetadata?,
): Flow<Crowdloan> = emptyFlow() // this flow is no longer accessible and deprecated. We will remove entire crowdloan feature soon
suspend fun estimateFee(
crowdloan: Crowdloan,
contribution: BigDecimal,
bonusPayload: BonusPayload?,
customizationPayload: Parcelable?,
): Fee = formingSubmission(
crowdloan = crowdloan,
contribution = contribution,
bonusPayload = bonusPayload,
customizationPayload = customizationPayload,
toCalculateFee = true
) { submission, chain, _ ->
extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet, formExtrinsic = { submission() })
}
suspend fun contribute(
crowdloan: Crowdloan,
contribution: BigDecimal,
bonusPayload: BonusPayload?,
customizationPayload: Parcelable?,
): Result<ExtrinsicSubmission> = runCatching {
crowdloan.parachainMetadata?.customFlow?.let {
customContributeManager.getFactoryOrNull(it)?.submitter?.submitOffChain(customizationPayload, bonusPayload, contribution)
}
formingSubmission(
crowdloan = crowdloan,
contribution = contribution,
bonusPayload = bonusPayload,
toCalculateFee = false,
customizationPayload = customizationPayload
) { submission, chain, account ->
extrinsicService.submitExtrinsic(chain, TransactionOrigin.Wallet(account)) { submission() }
}.getOrThrow()
}
private suspend fun <T> formingSubmission(
crowdloan: Crowdloan,
contribution: BigDecimal,
bonusPayload: BonusPayload?,
customizationPayload: Parcelable?,
toCalculateFee: Boolean,
finalAction: suspend (OnChainSubmission, Chain, MetaAccount) -> T,
): T = withContext(Dispatchers.Default) {
val (chain, chainAsset) = crowdloanSharedState.chainAndAsset()
val contributionInPlanks = chainAsset.planksFromAmount(contribution)
val account = accountRepository.getSelectedMetaAccount()
val privateSignature = crowdloan.parachainMetadata?.customFlow?.let {
val previousContribution = crowdloan.myContribution?.amountInPlanks ?: BigInteger.ZERO
val signatureProvider = customContributeManager.getFactoryOrNull(it)?.privateCrowdloanSignatureProvider
val address = account.addressIn(chain)!!
signatureProvider?.provideSignature(
chainMetadata = crowdloan.parachainMetadata,
previousContribution = previousContribution,
newContribution = contributionInPlanks,
address = address,
mode = if (toCalculateFee) Mode.FEE else Mode.SUBMIT
)
}
val submitter = crowdloan.parachainMetadata?.customFlow?.let {
customContributeManager.getFactoryOrNull(it)?.submitter
}
val submission: OnChainSubmission = {
contribute(crowdloan.parachainId, contributionInPlanks, privateSignature)
submitter?.let {
val injection = if (toCalculateFee) submitter::injectFeeCalculation else submitter::injectOnChainSubmission
injection(crowdloan, customizationPayload, bonusPayload, contribution, this)
}
}
finalAction(submission, chain, account)
}
}
@@ -0,0 +1,84 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
import io.novafoundation.nova.feature_crowdloan_api.data.network.blockhain.binding.FundInfo
import io.novafoundation.nova.feature_crowdloan_api.data.repository.LeasePeriodToBlocksConverter
import io.novafoundation.nova.feature_crowdloan_api.data.repository.ParachainMetadata
import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.Contribution
import io.novafoundation.nova.feature_crowdloan_impl.domain.main.Crowdloan
import java.math.BigInteger
import java.math.MathContext
fun mapFundInfoToCrowdloan(
fundInfo: FundInfo,
parachainMetadata: ParachainMetadata?,
parachainId: BigInteger,
currentBlockNumber: BlockNumber,
expectedBlockTimeInMillis: BigInteger,
leasePeriodToBlocksConverter: LeasePeriodToBlocksConverter,
contribution: Contribution?,
hasWonAuction: Boolean,
): Crowdloan {
val leasePeriodInMillis = leasePeriodInMillis(leasePeriodToBlocksConverter, currentBlockNumber, fundInfo.lastSlot, expectedBlockTimeInMillis)
val state = if (isCrowdloanActive(fundInfo, currentBlockNumber, leasePeriodToBlocksConverter, hasWonAuction)) {
val remainingTime = expectedRemainingTime(currentBlockNumber, fundInfo.end, expectedBlockTimeInMillis)
Crowdloan.State.Active(remainingTime)
} else {
Crowdloan.State.Finished
}
return Crowdloan(
parachainMetadata = parachainMetadata,
raisedFraction = fundInfo.raised.toBigDecimal().divide(fundInfo.cap.toBigDecimal(), MathContext.DECIMAL32),
parachainId = parachainId,
leasePeriodInMillis = leasePeriodInMillis,
leasedUntilInMillis = System.currentTimeMillis() + leasePeriodInMillis,
state = state,
fundInfo = fundInfo,
myContribution = contribution
)
}
private fun isCrowdloanActive(
fundInfo: FundInfo,
currentBlockNumber: BigInteger,
leasePeriodToBlocksConverter: LeasePeriodToBlocksConverter,
hasWonAuction: Boolean,
): Boolean {
return currentBlockNumber < fundInfo.end && // crowdloan is not ended
// first slot is not yet passed
leasePeriodToBlocksConverter.leaseIndexFromBlock(currentBlockNumber) <= fundInfo.firstSlot &&
// cap is not reached
fundInfo.raised < fundInfo.cap &&
// crowdloan considered closed if parachain already won auction
!hasWonAuction
}
fun leasePeriodInMillis(
leasePeriodToBlocksConverter: LeasePeriodToBlocksConverter,
currentBlockNumber: BigInteger,
endingLeasePeriod: BigInteger,
expectedBlockTimeInMillis: BigInteger
): Long {
val unlockedAtPeriod = endingLeasePeriod + BigInteger.ONE // next period after end one
val unlockedAtBlock = leasePeriodToBlocksConverter.startBlockFor(unlockedAtPeriod)
return expectedRemainingTime(
currentBlockNumber,
unlockedAtBlock,
expectedBlockTimeInMillis
)
}
private fun expectedRemainingTime(
currentBlock: BlockNumber,
targetBlock: BlockNumber,
expectedBlockTimeInMillis: BigInteger,
): Long {
val blockDifference = targetBlock - currentBlock
val expectedTimeDifference = blockDifference * expectedBlockTimeInMillis
return expectedTimeDifference.toLong()
}
@@ -0,0 +1,19 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom
import io.novafoundation.nova.feature_crowdloan_api.data.repository.ParachainMetadata
import java.math.BigInteger
interface PrivateCrowdloanSignatureProvider {
enum class Mode {
FEE, SUBMIT
}
suspend fun provideSignature(
chainMetadata: ParachainMetadata,
previousContribution: BigInteger,
newContribution: BigInteger,
address: String,
mode: Mode,
): Any?
}
@@ -0,0 +1,159 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.acala
import io.novafoundation.nova.common.base.BaseException
import io.novafoundation.nova.common.data.network.HttpExceptionHandler
import io.novafoundation.nova.common.utils.asHexString
import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.acala.AcalaApi
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.acala.AcalaDirectContributeRequest
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.acala.AcalaLiquidContributeRequest
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.nativeTransfer
import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount
import io.novafoundation.nova.runtime.ext.ChainGeneses
import io.novafoundation.nova.runtime.ext.accountIdOf
import io.novafoundation.nova.runtime.ext.addressOf
import io.novafoundation.nova.runtime.extrinsic.systemRemarkWithEvent
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.state.SingleAssetSharedState
import io.novafoundation.nova.runtime.state.chain
import io.novafoundation.nova.runtime.state.chainAndAsset
import io.novafoundation.nova.runtime.state.chainAsset
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignerPayloadRaw
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.fromUtf8
import java.math.BigDecimal
class AcalaContributeInteractor(
private val acalaApi: AcalaApi,
private val httpExceptionHandler: HttpExceptionHandler,
private val accountRepository: AccountRepository,
private val chainRegistry: ChainRegistry,
private val selectedAssetState: SingleAssetSharedState,
private val signerProvider: SignerProvider,
) {
suspend fun registerContributionOffChain(
amount: BigDecimal,
contributionType: ContributionType,
referralCode: String?,
): Result<Unit> = runCatching {
httpExceptionHandler.wrap {
val selectedMetaAccount = accountRepository.getSelectedMetaAccount()
val signer = signerProvider.rootSignerFor(selectedMetaAccount)
val (chain, chainAsset) = selectedAssetState.chainAndAsset()
val accountIdInCurrentChain = selectedMetaAccount.accountIdIn(chain)!!
// api requires polkadot address even in rococo testnet
val addressInPolkadot = chainRegistry.getChain(ChainGeneses.POLKADOT).addressOf(accountIdInCurrentChain)
val amountInPlanks = chainAsset.planksFromAmount(amount)
val statement = getStatement(chain).statement
val signerPayload = SignerPayloadRaw.fromUtf8(statement, accountIdInCurrentChain)
when (contributionType) {
ContributionType.DIRECT -> {
val request = AcalaDirectContributeRequest(
address = addressInPolkadot,
amount = amountInPlanks,
referral = referralCode,
signature = signer.signRaw(signerPayload).asHexString()
)
acalaApi.directContribute(
baseUrl = AcalaApi.getBaseUrl(chain),
authHeader = AcalaApi.getAuthHeader(chain),
body = request
)
}
ContributionType.LIQUID -> {
val request = AcalaLiquidContributeRequest(
address = addressInPolkadot,
amount = amountInPlanks,
referral = referralCode
)
acalaApi.liquidContribute(
baseUrl = AcalaApi.getBaseUrl(chain),
authHeader = AcalaApi.getAuthHeader(chain),
body = request
)
}
}
}
}
suspend fun isReferralValid(referralCode: String) = try {
val chain = selectedAssetState.chain()
httpExceptionHandler.wrap {
acalaApi.isReferralValid(
baseUrl = AcalaApi.getBaseUrl(chain),
authHeader = AcalaApi.getAuthHeader(chain),
referral = referralCode
).result
}
} catch (e: BaseException) {
if (e.kind == BaseException.Kind.HTTP) {
false // acala api return an error http code for some invalid codes, so catch it here
} else {
throw e
}
}
suspend fun injectOnChainSubmission(
contributionType: ContributionType,
referralCode: String?,
amount: BigDecimal,
extrinsicBuilder: ExtrinsicBuilder,
) = with(extrinsicBuilder) {
if (contributionType == ContributionType.LIQUID) {
resetCalls()
val (chain, chainAsset) = selectedAssetState.chainAndAsset()
val amountInPlanks = chainAsset.planksFromAmount(amount)
val statement = httpExceptionHandler.wrap { getStatement(chain) }
val proxyAccountId = chain.accountIdOf(statement.proxyAddress)
nativeTransfer(proxyAccountId, amountInPlanks)
systemRemarkWithEvent(statement.statement)
referralCode?.let { systemRemarkWithEvent(referralRemark(it)) }
}
}
suspend fun injectFeeCalculation(
contributionType: ContributionType,
referralCode: String?,
amount: BigDecimal,
extrinsicBuilder: ExtrinsicBuilder,
) = with(extrinsicBuilder) {
if (contributionType == ContributionType.LIQUID) {
resetCalls()
val chainAsset = selectedAssetState.chainAsset()
val amountInPlanks = chainAsset.planksFromAmount(amount)
val fakeDestination = ByteArray(32)
nativeTransfer(accountId = fakeDestination, amount = amountInPlanks)
val fakeAgreementRemark = ByteArray(185) // acala agreement is 185 bytes
systemRemarkWithEvent(fakeAgreementRemark)
referralCode?.let { systemRemarkWithEvent(referralRemark(referralCode)) }
}
}
private suspend fun getStatement(
chain: Chain,
) = acalaApi.getStatement(
baseUrl = AcalaApi.getBaseUrl(chain),
authHeader = AcalaApi.getAuthHeader(chain)
)
private fun referralRemark(referralCode: String) = "referrer:$referralCode"
}
@@ -0,0 +1,5 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.acala
enum class ContributionType {
DIRECT, LIQUID
}
@@ -0,0 +1,31 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.astar
import io.novafoundation.nova.common.data.network.runtime.binding.ParaId
import io.novafoundation.nova.feature_crowdloan_impl.data.network.blockhain.extrinsic.addMemo
import io.novafoundation.nova.runtime.ext.accountIdOf
import io.novafoundation.nova.runtime.ext.isValidAddress
import io.novafoundation.nova.runtime.state.SingleAssetSharedState
import io.novafoundation.nova.runtime.state.chain
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
class AstarContributeInteractor(
private val selectedAssetSharedState: SingleAssetSharedState,
) {
suspend fun isReferralCodeValid(code: String): Boolean {
val currentChain = selectedAssetSharedState.chain()
return currentChain.isValidAddress(code)
}
suspend fun submitOnChain(
paraId: ParaId,
referralCode: String,
extrinsicBuilder: ExtrinsicBuilder,
) {
val currentChain = selectedAssetSharedState.chain()
val referralAccountId = currentChain.accountIdOf(referralCode)
extrinsicBuilder.addMemo(paraId, referralAccountId)
}
}
@@ -0,0 +1,27 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.bifrost
import io.novafoundation.nova.common.data.network.HttpExceptionHandler
import io.novafoundation.nova.common.data.network.runtime.binding.ParaId
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.bifrost.BifrostApi
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.bifrost.getAccountByReferralCode
import io.novafoundation.nova.feature_crowdloan_impl.data.network.blockhain.extrinsic.addMemo
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
class BifrostContributeInteractor(
val novaReferralCode: String,
private val bifrostApi: BifrostApi,
private val httpExceptionHandler: HttpExceptionHandler,
) {
suspend fun isCodeValid(code: String): Boolean {
val response = httpExceptionHandler.wrap { bifrostApi.getAccountByReferralCode(code) }
return response.data.getAccountByInvitationCode.account.isNullOrEmpty().not()
}
fun submitOnChain(
paraId: ParaId,
referralCode: String,
extrinsicBuilder: ExtrinsicBuilder,
) = extrinsicBuilder.addMemo(paraId, referralCode)
}
@@ -0,0 +1,8 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.moonbeam
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
class CrossChainRewardDestination(
val addressInDestination: String,
val destination: Chain,
)
@@ -0,0 +1,171 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.moonbeam
import android.util.Log
import io.novafoundation.nova.common.data.network.HttpExceptionHandler
import io.novafoundation.nova.common.utils.LOG_TAG
import io.novafoundation.nova.common.utils.asHexString
import io.novafoundation.nova.common.utils.sha256
import io.novafoundation.nova.core.model.CryptoType
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
import io.novafoundation.nova.feature_account_api.data.extrinsic.awaitStatus
import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.watch.ExtrinsicWatchResult
import io.novafoundation.nova.feature_account_api.data.model.Fee
import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.model.addressIn
import io.novafoundation.nova.feature_account_api.domain.model.cryptoTypeIn
import io.novafoundation.nova.feature_crowdloan_api.data.repository.ParachainMetadata
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.moonbeam.AgreeRemarkRequest
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.moonbeam.MoonbeamApi
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.moonbeam.VerifyRemarkRequest
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.moonbeam.agreeRemark
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.moonbeam.checkRemark
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.moonbeam.moonbeamChainId
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.moonbeam.verifyRemark
import io.novafoundation.nova.feature_crowdloan_impl.data.network.blockhain.extrinsic.addMemo
import io.novafoundation.nova.feature_crowdloan_impl.domain.main.Crowdloan
import io.novafoundation.nova.runtime.extrinsic.ExtrinsicStatus
import io.novafoundation.nova.runtime.extrinsic.systemRemark
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.state.SingleAssetSharedState
import io.novafoundation.nova.runtime.state.chain
import io.novasama.substrate_sdk_android.extensions.fromHex
import io.novasama.substrate_sdk_android.extensions.toHexString
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignerPayloadRaw
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.fromUtf8
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import retrofit2.HttpException
class VerificationError : Exception()
private val SUPPORTED_CRYPTO_TYPES = setOf(CryptoType.SR25519, CryptoType.ED25519)
class MoonbeamCrowdloanInteractor(
private val accountRepository: AccountRepository,
private val extrinsicService: ExtrinsicService,
private val moonbeamApi: MoonbeamApi,
private val selectedChainAssetState: SingleAssetSharedState,
private val chainRegistry: ChainRegistry,
private val httpExceptionHandler: HttpExceptionHandler,
private val signerProvider: SignerProvider,
) {
fun getTermsLink() = "https://github.com/moonbeam-foundation/crowdloan-self-attestation/blob/main/moonbeam/README.md"
suspend fun getMoonbeamRewardDestination(parachainMetadata: ParachainMetadata): CrossChainRewardDestination {
val currentAccount = accountRepository.getSelectedMetaAccount()
val moonbeamChain = chainRegistry.getChain(parachainMetadata.moonbeamChainId())
return CrossChainRewardDestination(
addressInDestination = currentAccount.addressIn(moonbeamChain)!!,
destination = moonbeamChain
)
}
suspend fun additionalSubmission(
crowdloan: Crowdloan,
extrinsicBuilder: ExtrinsicBuilder,
) {
val rewardDestination = getMoonbeamRewardDestination(crowdloan.parachainMetadata!!)
extrinsicBuilder.addMemo(
parachainId = crowdloan.parachainId,
memo = rewardDestination.addressInDestination.fromHex()
)
}
suspend fun flowStatus(parachainMetadata: ParachainMetadata): Result<MoonbeamFlowStatus> = withContext(Dispatchers.Default) {
runCatching {
val metaAccount = accountRepository.getSelectedMetaAccount()
val moonbeamChainId = parachainMetadata.moonbeamChainId()
val moonbeamChain = chainRegistry.getChain(moonbeamChainId)
val currentChain = selectedChainAssetState.chain()
val currentAddress = metaAccount.addressIn(currentChain)!!
when {
!metaAccount.hasAccountIn(moonbeamChain) -> MoonbeamFlowStatus.NeedsChainAccount(
chainId = moonbeamChainId,
metaId = metaAccount.id
)
metaAccount.cryptoTypeIn(currentChain) !in SUPPORTED_CRYPTO_TYPES -> MoonbeamFlowStatus.UnsupportedAccountEncryption
else -> when (checkRemark(parachainMetadata, currentAddress)) {
null -> MoonbeamFlowStatus.RegionNotSupported
true -> MoonbeamFlowStatus.Completed
false -> MoonbeamFlowStatus.ReadyToComplete
}
}
}
}
suspend fun calculateTermsFee(): Fee = withContext(Dispatchers.Default) {
val chain = selectedChainAssetState.chain()
extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) {
systemRemark(fakeRemark())
}
}
suspend fun submitAgreement(parachainMetadata: ParachainMetadata): Result<ExtrinsicWatchResult<*>> = withContext(Dispatchers.Default) {
runCatching {
val chain = selectedChainAssetState.chain()
val metaAccount = accountRepository.getSelectedMetaAccount()
val currentAddress = metaAccount.addressIn(chain)!!
val accountId = metaAccount.accountIdIn(chain)!!
val legalText = httpExceptionHandler.wrap { moonbeamApi.getLegalText() }
val legalHash = legalText.encodeToByteArray().sha256().toHexString(withPrefix = false)
val signer = signerProvider.rootSignerFor(metaAccount)
val signerPayload = SignerPayloadRaw.fromUtf8(legalHash, accountId)
val signedHash = signer.signRaw(signerPayload).asHexString()
val agreeRemarkRequest = AgreeRemarkRequest(currentAddress, signedHash)
val remark = httpExceptionHandler.wrap { moonbeamApi.agreeRemark(parachainMetadata, agreeRemarkRequest) }.remark
val result = extrinsicService.submitAndWatchExtrinsic(chain, TransactionOrigin.SelectedWallet) {
systemRemark(remark.encodeToByteArray())
}
.getOrThrow()
.awaitStatus<ExtrinsicStatus.Finalized>()
Log.d(this@MoonbeamCrowdloanInteractor.LOG_TAG, "Finalized ${result.status.extrinsicHash} in block ${result.status.blockHash}")
val verificationRequest = VerifyRemarkRequest(
address = currentAddress,
extrinsicHash = result.status.extrinsicHash,
blockHash = result.status.blockHash
)
val verificationResponse = httpExceptionHandler.wrap { moonbeamApi.verifyRemark(parachainMetadata, verificationRequest) }
if (!verificationResponse.verified) throw VerificationError()
result
}
}
private fun fakeRemark() = ByteArray(32)
/**
* @return null if Geo-fenced or application unavailable. True if user already agreed with terms. False otherwise
*/
private suspend fun checkRemark(parachainMetadata: ParachainMetadata, address: String): Boolean? = try {
moonbeamApi.checkRemark(parachainMetadata, address).verified
} catch (e: HttpException) {
if (e.code() == 403) { // Moonbeam answers with 403 in case geo-fenced or application unavailable
null
} else {
throw httpExceptionHandler.transformException(e)
}
} catch (e: Exception) {
throw httpExceptionHandler.transformException(e)
}
}
@@ -0,0 +1,16 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.moonbeam
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
sealed class MoonbeamFlowStatus {
object RegionNotSupported : MoonbeamFlowStatus()
object Completed : MoonbeamFlowStatus()
class NeedsChainAccount(val chainId: ChainId, val metaId: Long) : MoonbeamFlowStatus()
object UnsupportedAccountEncryption : MoonbeamFlowStatus()
object ReadyToComplete : MoonbeamFlowStatus()
}
@@ -0,0 +1,44 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.moonbeam
import io.novafoundation.nova.common.data.network.HttpExceptionHandler
import io.novafoundation.nova.feature_crowdloan_api.data.repository.ParachainMetadata
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.moonbeam.MakeSignatureRequest
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.moonbeam.MoonbeamApi
import io.novafoundation.nova.feature_crowdloan_impl.data.network.api.moonbeam.makeSignature
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.PrivateCrowdloanSignatureProvider
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.PrivateCrowdloanSignatureProvider.Mode
import io.novasama.substrate_sdk_android.encrypt.EncryptionType
import io.novasama.substrate_sdk_android.extensions.fromHex
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.MultiSignature
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.prepareForEncoding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.math.BigInteger
class MoonbeamPrivateSignatureProvider(
private val moonbeamApi: MoonbeamApi,
private val httpExceptionHandler: HttpExceptionHandler,
) : PrivateCrowdloanSignatureProvider {
override suspend fun provideSignature(
chainMetadata: ParachainMetadata,
previousContribution: BigInteger,
newContribution: BigInteger,
address: String,
mode: Mode,
): Any = withContext(Dispatchers.Default) {
when (mode) {
Mode.FEE -> sr25519SignatureOf(ByteArray(64)) // sr25519 is 65 bytes
Mode.SUBMIT -> {
val request = MakeSignatureRequest(address, previousContribution.toString(), newContribution.toString())
val response = httpExceptionHandler.wrap { moonbeamApi.makeSignature(chainMetadata, request) }
sr25519SignatureOf(response.signature.fromHex())
}
}
}
private fun sr25519SignatureOf(bytes: ByteArray): Any {
return MultiSignature(EncryptionType.SR25519, bytes).prepareForEncoding()
}
}
@@ -0,0 +1,24 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations
import io.novafoundation.nova.common.validation.DefaultFailureLevel
import io.novafoundation.nova.common.validation.ValidationStatus
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.CustomContributeManager
class BonusAppliedValidation(
private val customContributeManager: CustomContributeManager,
) : ContributeValidation {
override suspend fun validate(value: ContributeValidationPayload): ValidationStatus<ContributeValidationFailure> {
val factory = value.crowdloan.parachainMetadata?.customFlow?.let {
customContributeManager.getFactoryOrNull(it)
}
val shouldHaveBonusPayload = factory?.extraBonusFlow != null
return if (shouldHaveBonusPayload && value.bonusPayload == null) {
ValidationStatus.NotValid(DefaultFailureLevel.WARNING, ContributeValidationFailure.BonusNotApplied)
} else {
ValidationStatus.Valid()
}
}
}
@@ -0,0 +1,29 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations
import io.novafoundation.nova.common.validation.DefaultFailureLevel
import io.novafoundation.nova.common.validation.ValidationStatus
import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks
class CapExceededValidation : ContributeValidation {
override suspend fun validate(value: ContributeValidationPayload): ValidationStatus<ContributeValidationFailure> {
val token = value.asset.token
return with(value.crowdloan.fundInfo) {
val raisedAmount = token.amountFromPlanks(raised)
val capAmount = token.amountFromPlanks(cap)
when {
raisedAmount >= capAmount -> ValidationStatus.NotValid(DefaultFailureLevel.ERROR, ContributeValidationFailure.CapExceeded.FromRaised)
raisedAmount + value.contributionAmount > capAmount -> {
val maxAllowedContribution = capAmount - raisedAmount
val reason = ContributeValidationFailure.CapExceeded.FromAmount(maxAllowedContribution, token.configuration)
ValidationStatus.NotValid(DefaultFailureLevel.ERROR, reason)
}
else -> ValidationStatus.Valid()
}
}
}
}
@@ -0,0 +1,32 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import java.math.BigDecimal
sealed class ContributeValidationFailure {
class LessThanMinContribution(
val minContribution: BigDecimal,
val chainAsset: Chain.Asset
) : ContributeValidationFailure()
sealed class CapExceeded : ContributeValidationFailure() {
class FromAmount(
val maxAllowedContribution: BigDecimal,
val chainAsset: Chain.Asset
) : CapExceeded()
object FromRaised : CapExceeded()
}
object CrowdloanEnded : ContributeValidationFailure()
object CannotPayFees : ContributeValidationFailure()
object ExistentialDepositCrossed : ContributeValidationFailure()
object BonusNotApplied : ContributeValidationFailure()
object PrivateCrowdloanNotSupported : ContributeValidationFailure()
}
@@ -0,0 +1,17 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations
import android.os.Parcelable
import io.novafoundation.nova.feature_crowdloan_impl.domain.main.Crowdloan
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.BonusPayload
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
import io.novafoundation.nova.feature_account_api.data.model.Fee
import java.math.BigDecimal
class ContributeValidationPayload(
val crowdloan: Crowdloan,
val customizationPayload: Parcelable?,
val asset: Asset,
val fee: Fee,
val bonusPayload: BonusPayload?,
val contributionAmount: BigDecimal,
)
@@ -0,0 +1,30 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations
import io.novafoundation.nova.common.validation.DefaultFailureLevel
import io.novafoundation.nova.common.validation.ValidationStatus
import io.novafoundation.nova.feature_crowdloan_api.data.repository.CrowdloanRepository
import io.novafoundation.nova.runtime.repository.ChainStateRepository
class CrowdloanNotEndedValidation(
private val chainStateRepository: ChainStateRepository,
private val crowdloanRepository: CrowdloanRepository
) : ContributeValidation {
override suspend fun validate(value: ContributeValidationPayload): ValidationStatus<ContributeValidationFailure> {
val chainId = value.asset.token.configuration.chainId
val currentBlock = chainStateRepository.currentBlock(chainId)
val leasePeriodToBlocksConverter = crowdloanRepository.leasePeriodToBlocksConverter(chainId)
val currentLeaseIndex = leasePeriodToBlocksConverter.leaseIndexFromBlock(currentBlock)
return when {
currentBlock >= value.crowdloan.fundInfo.end -> crowdloanEndedFailure()
currentLeaseIndex > value.crowdloan.fundInfo.firstSlot -> crowdloanEndedFailure()
else -> ValidationStatus.Valid()
}
}
private fun crowdloanEndedFailure(): ValidationStatus.NotValid<ContributeValidationFailure> =
ValidationStatus.NotValid(DefaultFailureLevel.ERROR, ContributeValidationFailure.CrowdloanEnded)
}
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations
import io.novafoundation.nova.common.validation.Validation
import io.novafoundation.nova.feature_account_api.data.model.Fee
import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughAmountToTransferValidation
import io.novafoundation.nova.feature_wallet_api.domain.validation.ExistentialDepositValidation
typealias ContributeValidation = Validation<ContributeValidationPayload, ContributeValidationFailure>
typealias ContributeEnoughToPayFeesValidation = EnoughAmountToTransferValidation<ContributeValidationPayload, ContributeValidationFailure>
typealias ContributeExistentialDepositValidation = ExistentialDepositValidation<ContributeValidationPayload, ContributeValidationFailure, Fee>
@@ -0,0 +1,34 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations
import io.novafoundation.nova.common.validation.ValidationStatus
import io.novafoundation.nova.common.validation.validOrError
import io.novafoundation.nova.feature_crowdloan_api.data.repository.CrowdloanRepository
import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks
import java.math.BigInteger
class DefaultMinContributionValidation(
private val crowdloanRepository: CrowdloanRepository,
) : MinContributionValidation() {
override suspend fun minContribution(payload: ContributeValidationPayload): BigInteger {
val chainAsset = payload.asset.token.configuration
return crowdloanRepository.minContribution(chainAsset.chainId)
}
}
abstract class MinContributionValidation : ContributeValidation {
abstract suspend fun minContribution(payload: ContributeValidationPayload): BigInteger
override suspend fun validate(value: ContributeValidationPayload): ValidationStatus<ContributeValidationFailure> {
val chainAsset = value.asset.token.configuration
val minContributionInPlanks = minContribution(value)
val minContribution = chainAsset.amountFromPlanks(minContributionInPlanks)
return validOrError(value.contributionAmount >= minContribution) {
ContributeValidationFailure.LessThanMinContribution(minContribution, chainAsset)
}
}
}
@@ -0,0 +1,22 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations
import io.novafoundation.nova.common.validation.ValidationStatus
import io.novafoundation.nova.common.validation.validOrError
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.CustomContributeManager
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.supportsPrivateCrowdloans
class PublicCrowdloanValidation(
private val customContributeManager: CustomContributeManager,
) : ContributeValidation {
override suspend fun validate(value: ContributeValidationPayload): ValidationStatus<ContributeValidationFailure> {
val isPublic = value.crowdloan.fundInfo.verifier == null
val flowType = value.crowdloan.parachainMetadata?.customFlow
val supportsPrivate = flowType?.let(customContributeManager::supportsPrivateCrowdloans) ?: false
return validOrError(isPublic || supportsPrivate) {
ContributeValidationFailure.PrivateCrowdloanNotSupported
}
}
}
@@ -0,0 +1,29 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.custom.acala
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.custom.acala.ContributionType
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.ContributeValidationPayload
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.MinContributionValidation
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.acala.main.AcalaCustomizationPayload
import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount
import java.math.BigInteger
private val ACALA_MIN_CONTRIBUTION = 1.toBigDecimal()
class AcalaMinContributionValidation(
private val fallback: MinContributionValidation,
) : MinContributionValidation() {
override suspend fun minContribution(payload: ContributeValidationPayload): BigInteger {
val customization = payload.customizationPayload
require(customization is AcalaCustomizationPayload)
return when (customization.contributionType) {
ContributionType.DIRECT -> fallback.minContribution(payload)
ContributionType.LIQUID -> {
val asset = payload.asset.token.configuration
return asset.planksFromAmount(ACALA_MIN_CONTRIBUTION)
}
}
}
}
@@ -0,0 +1,9 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.custom.moonbeam
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
import io.novafoundation.nova.feature_account_api.data.model.Fee
class MoonbeamTermsPayload(
val fee: Fee,
val asset: Asset
)
@@ -0,0 +1,5 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.custom.moonbeam
enum class MoonbeamTermsValidationFailure {
CANNOT_PAY_FEES
}
@@ -0,0 +1,10 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.custom.moonbeam
import io.novafoundation.nova.common.validation.Validation
import io.novafoundation.nova.common.validation.ValidationSystem
import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughAmountToTransferValidation
typealias MoonbeamTermsValidationSystem = ValidationSystem<MoonbeamTermsPayload, MoonbeamTermsValidationFailure>
typealias MoonbeamTermsValidation = Validation<MoonbeamTermsPayload, MoonbeamTermsValidationFailure>
typealias MoonbeamTermsFeeValidation = EnoughAmountToTransferValidation<MoonbeamTermsPayload, MoonbeamTermsValidationFailure>
@@ -0,0 +1,120 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contributions
import io.novafoundation.nova.common.data.network.runtime.binding.ParaId
import io.novafoundation.nova.common.utils.combineToPair
import io.novafoundation.nova.common.utils.sumByBigInteger
import io.novafoundation.nova.core.updater.Updater
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_crowdloan_api.data.network.updater.ContributionsUpdateSystemFactory
import io.novafoundation.nova.feature_crowdloan_api.data.repository.ContributionsRepository
import io.novafoundation.nova.feature_crowdloan_api.data.repository.CrowdloanRepository
import io.novafoundation.nova.feature_crowdloan_api.data.repository.ParachainMetadata
import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.Contribution
import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.ContributionMetadata
import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.ContributionWithMetadata
import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.ContributionsInteractor
import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.ContributionsWithTotalAmount
import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.claimStatusOf
import io.novafoundation.nova.runtime.ext.timelineChainIdOrSelf
import io.novafoundation.nova.runtime.ext.utilityAsset
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.multiNetwork.chainWithAsset
import io.novafoundation.nova.runtime.repository.ChainStateRepository
import io.novafoundation.nova.runtime.repository.blockDurationEstimatorFlow
import io.novafoundation.nova.runtime.state.SingleAssetSharedState
import io.novafoundation.nova.runtime.state.selectedChainFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import java.math.BigInteger
class RealContributionsInteractor(
private val crowdloanRepository: CrowdloanRepository,
private val accountRepository: AccountRepository,
private val selectedAssetCrowdloanState: SingleAssetSharedState,
private val chainStateRepository: ChainStateRepository,
private val contributionsRepository: ContributionsRepository,
private val contributionsUpdateSystemFactory: ContributionsUpdateSystemFactory,
private val chainRegistry: ChainRegistry,
) : ContributionsInteractor {
override fun runUpdate(): Flow<Updater.SideEffect> {
return contributionsUpdateSystemFactory.create()
.start()
}
override fun observeSelectedChainContributionsWithMetadata(): Flow<ContributionsWithTotalAmount<ContributionWithMetadata>> {
val metaAccountFlow = accountRepository.selectedMetaAccountFlow()
val chainFlow = selectedAssetCrowdloanState.selectedChainFlow()
return combineToPair(metaAccountFlow, chainFlow)
.flatMapLatest { (metaAccount, chain) ->
observeChainContributionsWithMetadata(metaAccount, chain, chain.utilityAsset)
}
}
override fun observeChainContributions(
metaAccount: MetaAccount,
chainId: ChainId,
assetId: ChainAssetId
): Flow<ContributionsWithTotalAmount<Contribution>> {
return flow {
val (chain, asset) = chainRegistry.chainWithAsset(chainId, assetId)
emitAll(contributionsRepository.observeContributions(metaAccount, chain, asset))
}.map { contributions ->
contributions.totalContributions { it.amountInPlanks }
}
}
private suspend fun getParachainMetadata(chain: Chain): Map<ParaId, ParachainMetadata> {
return runCatching {
crowdloanRepository.getParachainMetadata(chain)
}.getOrDefault(emptyMap())
}
private suspend fun observeChainContributionsWithMetadata(
metaAccount: MetaAccount,
chain: Chain,
asset: Chain.Asset
): Flow<ContributionsWithTotalAmount<ContributionWithMetadata>> {
val parachainMetadatas = getParachainMetadata(chain)
return combine(
chainStateRepository.blockDurationEstimatorFlow(chain.timelineChainIdOrSelf()),
contributionsRepository.observeContributions(metaAccount, chain, asset)
) { blockDurationEstimator, contributions ->
contributions.map { contribution ->
val parachainMetadata = parachainMetadatas[contribution.paraId]
val claimStatus = blockDurationEstimator.claimStatusOf(contribution)
ContributionWithMetadata(
contribution = contribution,
metadata = ContributionMetadata(
claimStatus = claimStatus,
parachainMetadata = parachainMetadata,
)
)
}
.sortedWith(
compareBy<ContributionWithMetadata> { it.contribution.unlockBlock }
.thenBy { it.contribution.paraId }
)
.totalContributions { it.contribution.amountInPlanks }
}
}
private fun <T> List<T>.totalContributions(amount: (T) -> BigInteger): ContributionsWithTotalAmount<T> {
return ContributionsWithTotalAmount(
totalContributed = sumByBigInteger(amount),
contributions = this
)
}
}
@@ -0,0 +1,106 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.contributions
import android.util.Log
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.address.intoKey
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
import io.novafoundation.nova.common.data.network.runtime.binding.ParaId
import io.novafoundation.nova.common.utils.mapList
import io.novafoundation.nova.common.utils.metadata
import io.novafoundation.nova.core_db.dao.ContributionDao
import io.novafoundation.nova.core_db.dao.DeleteAssetContributionsParams
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_crowdloan_api.data.repository.ContributionsRepository
import io.novafoundation.nova.feature_crowdloan_api.data.source.contribution.ExternalContributionSource
import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.Contribution
import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.Contribution.Companion.DIRECT_SOURCE_ID
import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.mapContributionFromLocal
import io.novafoundation.nova.feature_crowdloan_impl.data.repository.contributions.network.ahOps
import io.novafoundation.nova.feature_crowdloan_impl.data.repository.contributions.network.rcCrowdloanContribution
import io.novafoundation.nova.feature_crowdloan_impl.data.repository.contributions.network.rcCrowdloanReserve
import io.novafoundation.nova.runtime.ext.utilityAsset
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import io.novafoundation.nova.runtime.storage.source.queryCatching
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
class RealContributionsRepository(
private val externalContributionsSources: List<ExternalContributionSource>,
private val chainRegistry: ChainRegistry,
private val remoteStorage: StorageDataSource,
private val contributionDao: ContributionDao
) : ContributionsRepository {
override fun observeContributions(metaAccount: MetaAccount): Flow<List<Contribution>> {
val contributionsFlow = contributionDao.observeContributions(metaAccount.id)
val chainsFlow = chainRegistry.chainsById
return combine(contributionsFlow, chainsFlow) { contributions, chains ->
contributions.map {
mapContributionFromLocal(it, chains.getValue(it.chainId))
}
}
}
override fun observeContributions(metaAccount: MetaAccount, chain: Chain, asset: Chain.Asset): Flow<List<Contribution>> {
return contributionDao.observeContributions(metaAccount.id, chain.id, asset.id)
.mapList { mapContributionFromLocal(it, chain) }
}
override fun loadContributionsGraduallyFlow(
chain: Chain,
accountId: ByteArray,
): Flow<Pair<String, Result<List<Contribution>>>> = flow {
if (!chain.hasCrowdloans) {
return@flow
}
val directContributions = getDirectContributions(chain, chain.utilityAsset, accountId)
.onFailure { Log.e("RealContributionsRepository", "Failed to fetch direct contributions on ${chain.name}", it) }
emit(DIRECT_SOURCE_ID to directContributions)
}
override suspend fun getDirectContributions(
chain: Chain,
asset: Chain.Asset,
accountId: ByteArray,
): Result<List<Contribution>> {
return withContext(Dispatchers.Default) {
remoteStorage.queryCatching(chain.id) {
val reserves = metadata.ahOps.rcCrowdloanReserve.keys()
val contributionKeys = reserves.map { (unlockBlock, paraId, _) -> Triple(unlockBlock, paraId, accountId.intoKey()) }
val contributionEntries = metadata.ahOps.rcCrowdloanContribution.entries(contributionKeys)
contributionEntries.map { (key, balance) ->
val (unlockBlock, paraId) = key
Contribution(
chain = chain,
asset = asset,
amountInPlanks = balance,
paraId = paraId,
sourceId = DIRECT_SOURCE_ID,
unlockBlock = unlockBlock,
leaseDepositor = reserves.getLeaseDepositor(paraId)
)
}
}
}
}
private fun LeaseReserves.getLeaseDepositor(paraId: ParaId): AccountIdKey {
return first { it.second == paraId }.third
}
override suspend fun deleteContributions(assetIds: List<FullChainAssetId>) {
val params = assetIds.map { DeleteAssetContributionsParams(it.chainId, it.assetId) }
contributionDao.deleteAssetContributions(params)
}
}
private typealias LeaseReserves = Set<Triple<BlockNumber, ParaId, AccountIdKey>>
@@ -0,0 +1,38 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.main
import io.novafoundation.nova.common.data.network.runtime.binding.ParaId
import io.novafoundation.nova.feature_crowdloan_api.data.network.blockhain.binding.FundInfo
import io.novafoundation.nova.feature_crowdloan_api.data.repository.ParachainMetadata
import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.Contribution
import java.math.BigDecimal
import kotlin.reflect.KClass
class Crowdloan(
val parachainMetadata: ParachainMetadata?,
val parachainId: ParaId,
val raisedFraction: BigDecimal,
val state: State,
val leasePeriodInMillis: Long,
val leasedUntilInMillis: Long,
val fundInfo: FundInfo,
val myContribution: Contribution?,
) {
sealed class State {
companion object {
val STATE_CLASS_COMPARATOR = Comparator<KClass<out State>> { first, _ ->
when (first) {
Active::class -> -1
Finished::class -> 1
else -> 0
}
}
}
object Finished : State()
class Active(val remainingTimeInMillis: Long) : State()
}
}
@@ -0,0 +1,50 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.main
import io.novafoundation.nova.common.list.GroupedList
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_crowdloan_api.data.repository.ContributionsRepository
import io.novafoundation.nova.feature_crowdloan_api.data.repository.CrowdloanRepository
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.repository.ChainStateRepository
import io.novasama.substrate_sdk_android.runtime.AccountId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlin.reflect.KClass
typealias GroupedCrowdloans = GroupedList<KClass<out Crowdloan.State>, Crowdloan>
class CrowdloanInteractor(
private val crowdloanRepository: CrowdloanRepository,
private val chainStateRepository: ChainStateRepository,
private val contributionsRepository: ContributionsRepository
) {
fun groupedCrowdloansFlow(chain: Chain, account: MetaAccount): Flow<GroupedCrowdloans> {
return crowdloansFlow(chain, account)
.map { groupCrowdloans(it) }
}
private fun crowdloansFlow(chain: Chain, account: MetaAccount): Flow<List<Crowdloan>> {
return flow {
val accountId = account.accountIdIn(chain)
emitAll(crowdloanListFlow(chain, accountId))
}
}
private fun groupCrowdloans(crowdloans: List<Crowdloan>): GroupedCrowdloans {
return crowdloans.groupBy { it.state::class }
.toSortedMap(Crowdloan.State.STATE_CLASS_COMPARATOR)
}
private suspend fun crowdloanListFlow(
chain: Chain,
contributor: AccountId?,
): Flow<List<Crowdloan>> {
// Crowdloans are no longer accessible and are deprecated. We will remove entire crowdloan feature soon
return flowOf(emptyList())
}
}
@@ -0,0 +1,30 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.main.statefull
import io.novafoundation.nova.common.presentation.LoadingState
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_crowdloan_impl.domain.main.GroupedCrowdloans
import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountModel
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
interface StatefulCrowdloanMixin {
interface Factory {
fun create(scope: CoroutineScope): StatefulCrowdloanMixin
}
class ContributionsInfo(
val contributionsCount: Int,
val isUserHasContributions: Boolean,
val totalContributed: AmountModel
)
val selectedAccount: Flow<MetaAccount>
val selectedChain: Flow<Chain>
val contributionsInfoFlow: Flow<LoadingState<ContributionsInfo>>
val groupedCrowdloansFlow: Flow<LoadingState<GroupedCrowdloans>>
}
@@ -0,0 +1,82 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.main.statefull
import io.novafoundation.nova.common.presentation.mapLoading
import io.novafoundation.nova.common.utils.WithCoroutineScopeExtensions
import io.novafoundation.nova.common.utils.combineToPair
import io.novafoundation.nova.common.utils.withLoading
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.ContributionsInteractor
import io.novafoundation.nova.feature_crowdloan_impl.domain.main.CrowdloanInteractor
import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase
import io.novafoundation.nova.feature_wallet_api.domain.getCurrentAsset
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.formatAmountToAmountModel
import io.novafoundation.nova.runtime.ext.utilityAsset
import io.novafoundation.nova.runtime.state.SingleAssetSharedState
import io.novafoundation.nova.runtime.state.selectedChainFlow
import kotlinx.coroutines.CoroutineScope
class StatefulCrowdloanProviderFactory(
private val singleAssetSharedState: SingleAssetSharedState,
private val crowdloanInteractor: CrowdloanInteractor,
private val contributionsInteractor: ContributionsInteractor,
private val selectedAccountUseCase: SelectedAccountUseCase,
private val amountFormatter: AmountFormatter,
private val assetUseCase: AssetUseCase,
) : StatefulCrowdloanMixin.Factory {
override fun create(scope: CoroutineScope): StatefulCrowdloanMixin {
return StatefulCrowdloanProvider(
singleAssetSharedState = singleAssetSharedState,
crowdloanInteractor = crowdloanInteractor,
contributionsInteractor = contributionsInteractor,
selectedAccountUseCase = selectedAccountUseCase,
assetUseCase = assetUseCase,
amountFormatter = amountFormatter,
coroutineScope = scope
)
}
}
class StatefulCrowdloanProvider(
singleAssetSharedState: SingleAssetSharedState,
selectedAccountUseCase: SelectedAccountUseCase,
private val crowdloanInteractor: CrowdloanInteractor,
private val contributionsInteractor: ContributionsInteractor,
private val assetUseCase: AssetUseCase,
coroutineScope: CoroutineScope,
private val amountFormatter: AmountFormatter,
) : StatefulCrowdloanMixin,
CoroutineScope by coroutineScope,
WithCoroutineScopeExtensions by WithCoroutineScopeExtensions(coroutineScope) {
override val selectedChain = singleAssetSharedState.selectedChainFlow()
.shareInBackground()
override val selectedAccount = selectedAccountUseCase.selectedMetaAccountFlow()
.shareInBackground()
private val chainAndAccount = combineToPair(selectedChain, selectedAccount)
.shareInBackground()
override val groupedCrowdloansFlow = chainAndAccount.withLoading { (chain, account) ->
crowdloanInteractor.groupedCrowdloansFlow(chain, account)
}
.shareInBackground()
override val contributionsInfoFlow = chainAndAccount.withLoading { (chain, account) ->
contributionsInteractor.observeChainContributions(account, chain.id, chain.utilityAsset.id)
}
.mapLoading {
val amountModel = amountFormatter.formatAmountToAmountModel(
it.totalContributed,
assetUseCase.getCurrentAsset()
)
StatefulCrowdloanMixin.ContributionsInfo(
contributionsCount = it.contributions.size,
isUserHasContributions = it.contributions.isNotEmpty(),
totalContributed = amountModel
)
}
}
@@ -0,0 +1,14 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.main.validations
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.validation.NoChainAccountFoundError
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
sealed class MainCrowdloanValidationFailure {
class NoRelaychainAccount(
override val chain: Chain,
override val account: MetaAccount,
override val addAccountState: NoChainAccountFoundError.AddAccountState
) : MainCrowdloanValidationFailure(), NoChainAccountFoundError
}
@@ -0,0 +1,9 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.main.validations
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
class MainCrowdloanValidationPayload(
val metaAccount: MetaAccount,
val chain: Chain
)
@@ -0,0 +1,15 @@
package io.novafoundation.nova.feature_crowdloan_impl.domain.main.validations
import io.novafoundation.nova.common.validation.ValidationSystem
import io.novafoundation.nova.feature_account_api.domain.validation.hasChainAccount
import io.novafoundation.nova.feature_crowdloan_impl.domain.main.validations.MainCrowdloanValidationFailure.NoRelaychainAccount
typealias MainCrowdloanValidationSystem = ValidationSystem<MainCrowdloanValidationPayload, MainCrowdloanValidationFailure>
fun ValidationSystem.Companion.mainCrowdloan(): MainCrowdloanValidationSystem = ValidationSystem {
hasChainAccount(
chain = MainCrowdloanValidationPayload::chain,
metaAccount = MainCrowdloanValidationPayload::metaAccount,
error = ::NoRelaychainAccount
)
}
@@ -0,0 +1,38 @@
package io.novafoundation.nova.feature_crowdloan_impl.presentation
import io.novafoundation.nova.feature_account_api.presenatation.account.add.AddAccountPayload
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.confirm.parcel.ConfirmContributePayload
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.BonusPayload
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.model.CustomContributePayload
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.select.parcel.ContributePayload
import kotlinx.coroutines.flow.Flow
interface CrowdloanRouter {
fun openContribute(payload: ContributePayload)
val customBonusFlow: Flow<BonusPayload?>
val latestCustomBonus: BonusPayload?
fun openCustomContribute(payload: CustomContributePayload)
fun setCustomBonus(payload: BonusPayload)
fun openConfirmContribute(payload: ConfirmContributePayload)
fun back()
fun returnToMain()
fun openMoonbeamFlow(payload: ContributePayload)
fun openAddAccount(payload: AddAccountPayload)
fun openUserContributions()
fun openSwitchWallet()
fun openWalletDetails(metaId: Long)
fun openClaimContribution()
}
@@ -0,0 +1,49 @@
package io.novafoundation.nova.feature_crowdloan_impl.presentation.claimControbution
import io.novafoundation.nova.common.base.BaseFragment
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.mixin.impl.observeValidations
import io.novafoundation.nova.common.presentation.showLoadingState
import io.novafoundation.nova.common.view.setProgressState
import io.novafoundation.nova.feature_account_api.presenatation.actions.setupExternalActions
import io.novafoundation.nova.feature_crowdloan_api.di.CrowdloanFeatureApi
import io.novafoundation.nova.feature_crowdloan_impl.databinding.FragmentClaimContributionsBinding
import io.novafoundation.nova.feature_crowdloan_impl.di.CrowdloanFeatureComponent
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.setupFeeLoading
class ClaimContributionFragment : BaseFragment<ClaimContributionViewModel, FragmentClaimContributionsBinding>() {
override fun createBinding() = FragmentClaimContributionsBinding.inflate(layoutInflater)
override fun initViews() {
binder.crowdloanClaimContributionsToolbar.setHomeButtonListener { viewModel.backClicked() }
binder.crowdloanClaimContributionsExtrinsicInfo.setOnAccountClickedListener { viewModel.originAccountClicked() }
binder.crowdloanClaimContributionsConfirm.prepareForProgress(viewLifecycleOwner)
binder.crowdloanClaimContributionsConfirm.setOnClickListener { viewModel.confirmClicked() }
}
override fun inject() {
FeatureUtils.getFeature<CrowdloanFeatureComponent>(
requireContext(),
CrowdloanFeatureApi::class.java
)
.claimContributions()
.create(this)
.inject(this)
}
override fun subscribe(viewModel: ClaimContributionViewModel) {
observeValidations(viewModel)
setupExternalActions(viewModel)
setupFeeLoading(viewModel.originFeeMixin, binder.crowdloanClaimContributionsExtrinsicInfo.fee)
viewModel.showNextProgress.observe(binder.crowdloanClaimContributionsConfirm::setProgressState)
viewModel.currentAccountModelFlow.observe(binder.crowdloanClaimContributionsExtrinsicInfo::setAccount)
viewModel.walletFlow.observe(binder.crowdloanClaimContributionsExtrinsicInfo::setWallet)
viewModel.redeemableAmountModelFlow.observe(binder.crowdloanClaimContributionsAmount::showLoadingState)
}
}
@@ -0,0 +1,139 @@
package io.novafoundation.nova.feature_crowdloan_impl.presentation.claimControbution
import androidx.lifecycle.viewModelScope
import io.novafoundation.nova.common.base.BaseViewModel
import io.novafoundation.nova.common.base.TitleAndMessage
import io.novafoundation.nova.common.mixin.api.Validatable
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.launchUnit
import io.novafoundation.nova.common.utils.withSafeLoading
import io.novafoundation.nova.common.validation.ValidationExecutor
import io.novafoundation.nova.common.validation.progressConsumer
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions
import io.novafoundation.nova.feature_account_api.presenatation.actions.showAddressActions
import io.novafoundation.nova.feature_account_api.presenatation.navigation.ExtrinsicNavigationWrapper
import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.Contribution
import io.novafoundation.nova.feature_crowdloan_impl.R
import io.novafoundation.nova.feature_crowdloan_impl.domain.claimContributions.ClaimContributionsInteractor
import io.novafoundation.nova.feature_crowdloan_impl.domain.claimContributions.validation.ClaimContributionValidationFailure
import io.novafoundation.nova.feature_crowdloan_impl.domain.claimContributions.validation.ClaimContributionValidationPayload
import io.novafoundation.nova.feature_crowdloan_impl.domain.claimContributions.validation.ClaimContributionValidationSystem
import io.novafoundation.nova.feature_crowdloan_impl.presentation.CrowdloanRouter
import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase
import io.novafoundation.nova.feature_wallet_api.domain.validation.handleNotEnoughFeeError
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.formatAmountToAmountModel
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.awaitFee
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.connectWith
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.createDefault
import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState
import io.novafoundation.nova.runtime.state.chain
import io.novafoundation.nova.runtime.state.selectedAssetFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
class ClaimContributionViewModel(
private val router: CrowdloanRouter,
private val resourceManager: ResourceManager,
private val validationSystem: ClaimContributionValidationSystem,
private val interactor: ClaimContributionsInteractor,
private val feeLoaderMixinV2Factory: FeeLoaderMixinV2.Factory,
private val externalActions: ExternalActions.Presentation,
private val selectedAssetState: AnySelectedAssetOptionSharedState,
private val validationExecutor: ValidationExecutor,
private val extrinsicNavigationWrapper: ExtrinsicNavigationWrapper,
selectedAccountUseCase: SelectedAccountUseCase,
assetUseCase: AssetUseCase,
walletUiUseCase: WalletUiUseCase,
private val amountFormatter: AmountFormatter
) : BaseViewModel(),
Validatable by validationExecutor,
ExternalActions by externalActions,
ExtrinsicNavigationWrapper by extrinsicNavigationWrapper {
private val assetFlow = assetUseCase.currentAssetFlow()
.shareInBackground()
private val claimableContributionsFlow = interactor.claimableContributions()
.shareInBackground()
val redeemableAmountModelFlow = combine(claimableContributionsFlow, assetFlow) { claimableContributions, asset ->
amountFormatter.formatAmountToAmountModel(claimableContributions.totalContributed, asset)
}
.withSafeLoading()
.shareInBackground()
val currentAccountModelFlow = selectedAccountUseCase.selectedAddressModelFlow { selectedAssetState.chain() }
.shareInBackground()
val walletFlow = walletUiUseCase.selectedWalletUiFlow()
.shareInBackground()
val originFeeMixin = feeLoaderMixinV2Factory.createDefault(viewModelScope, selectedAssetState.selectedAssetFlow())
private val _showNextProgress = MutableStateFlow(false)
val showNextProgress: StateFlow<Boolean> = _showNextProgress
init {
originFeeMixin.connectWith(claimableContributionsFlow) { _, claimableContributions ->
interactor.estimateFee(claimableContributions.contributions)
}
}
fun confirmClicked() {
sendTransactionIfValid()
}
fun backClicked() {
router.back()
}
fun originAccountClicked() = launch {
val address = currentAccountModelFlow.first().address
externalActions.showAddressActions(address, selectedAssetState.chain())
}
private fun sendTransactionIfValid() = launchUnit {
_showNextProgress.value = true
val payload = ClaimContributionValidationPayload(
fee = originFeeMixin.awaitFee(),
asset = assetFlow.first()
)
val claimableContributions = claimableContributionsFlow.first()
validationExecutor.requireValid(
validationSystem = validationSystem,
payload = payload,
validationFailureTransformer = ::formatRedeemFailure,
progressConsumer = _showNextProgress.progressConsumer()
) {
sendTransaction(claimableContributions.contributions)
}
}
private fun sendTransaction(redeemableContributions: List<Contribution>) = launch {
interactor.claim(redeemableContributions)
.onFailure(::showError)
.onSuccess { submissionResult ->
showToast(resourceManager.getString(R.string.common_transaction_submitted))
startNavigation(submissionResult.submissionHierarchy) { router.back() }
}
_showNextProgress.value = false
}
private fun formatRedeemFailure(failure: ClaimContributionValidationFailure): TitleAndMessage {
return when (failure) {
is ClaimContributionValidationFailure.NotEnoughBalanceToPayFees -> handleNotEnoughFeeError(failure, resourceManager)
}
}
}
@@ -0,0 +1,26 @@
package io.novafoundation.nova.feature_crowdloan_impl.presentation.claimControbution.di
import androidx.fragment.app.Fragment
import dagger.BindsInstance
import dagger.Subcomponent
import io.novafoundation.nova.common.di.scope.ScreenScope
import io.novafoundation.nova.feature_crowdloan_impl.presentation.claimControbution.ClaimContributionFragment
@Subcomponent(
modules = [
ClaimContributionModule::class
]
)
@ScreenScope
interface ClaimContributionComponent {
@Subcomponent.Factory
interface Factory {
fun create(
@BindsInstance fragment: Fragment,
): ClaimContributionComponent
}
fun inject(fragment: ClaimContributionFragment)
}
@@ -0,0 +1,78 @@
package io.novafoundation.nova.feature_crowdloan_impl.presentation.claimControbution.di
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoMap
import io.novafoundation.nova.common.di.scope.ScreenScope
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.validation.ValidationExecutor
import io.novafoundation.nova.common.validation.ValidationSystem
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions
import io.novafoundation.nova.feature_account_api.presenatation.navigation.ExtrinsicNavigationWrapper
import io.novafoundation.nova.feature_crowdloan_impl.domain.claimContributions.ClaimContributionsInteractor
import io.novafoundation.nova.feature_crowdloan_impl.domain.claimContributions.validation.ClaimContributionValidationSystem
import io.novafoundation.nova.feature_crowdloan_impl.domain.claimContributions.validation.claimContribution
import io.novafoundation.nova.feature_crowdloan_impl.presentation.CrowdloanRouter
import io.novafoundation.nova.feature_crowdloan_impl.presentation.claimControbution.ClaimContributionViewModel
import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2
import io.novafoundation.nova.runtime.state.AnySelectedAssetOptionSharedState
@Module(includes = [ViewModelModule::class])
class ClaimContributionModule {
@ScreenScope
@Provides
fun provideValidationSystem() = ValidationSystem.claimContribution()
@Provides
@IntoMap
@ViewModelKey(ClaimContributionViewModel::class)
fun provideViewModel(
router: CrowdloanRouter,
resourceManager: ResourceManager,
validationSystem: ClaimContributionValidationSystem,
interactor: ClaimContributionsInteractor,
feeLoaderMixinV2Factory: FeeLoaderMixinV2.Factory,
externalActions: ExternalActions.Presentation,
selectedAssetState: AnySelectedAssetOptionSharedState,
validationExecutor: ValidationExecutor,
extrinsicNavigationWrapper: ExtrinsicNavigationWrapper,
selectedAccountUseCase: SelectedAccountUseCase,
assetUseCase: AssetUseCase,
walletUiUseCase: WalletUiUseCase,
amountFormatter: AmountFormatter
): ViewModel {
return ClaimContributionViewModel(
router = router,
resourceManager = resourceManager,
validationSystem = validationSystem,
interactor = interactor,
feeLoaderMixinV2Factory = feeLoaderMixinV2Factory,
externalActions = externalActions,
selectedAssetState = selectedAssetState,
validationExecutor = validationExecutor,
selectedAccountUseCase = selectedAccountUseCase,
assetUseCase = assetUseCase,
walletUiUseCase = walletUiUseCase,
extrinsicNavigationWrapper = extrinsicNavigationWrapper,
amountFormatter = amountFormatter
)
}
@Provides
fun provideViewModelCreator(
fragment: Fragment,
viewModelFactory: ViewModelProvider.Factory,
): ClaimContributionViewModel {
return ViewModelProvider(fragment, viewModelFactory).get(ClaimContributionViewModel::class.java)
}
}
@@ -0,0 +1,93 @@
package io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute
import io.novafoundation.nova.common.mixin.api.Action
import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.validation.TransformedFailure
import io.novafoundation.nova.common.validation.ValidationFlowActions
import io.novafoundation.nova.feature_crowdloan_impl.R
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.ContributeValidationFailure
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatTokenAmount
fun contributeValidationFailure(
reason: ContributeValidationFailure,
validationFlowActions: ValidationFlowActions<*>,
resourceManager: ResourceManager,
onOpenCustomContribute: Action?,
): TransformedFailure {
return when (reason) {
ContributeValidationFailure.CannotPayFees -> {
TransformedFailure.Default(
resourceManager.getString(R.string.common_not_enough_funds_title) to
resourceManager.getString(R.string.common_not_enough_funds_message)
)
}
ContributeValidationFailure.ExistentialDepositCrossed -> {
TransformedFailure.Default(
resourceManager.getString(R.string.common_existential_warning_title) to
resourceManager.getString(R.string.common_existential_warning_message_v2_2_0)
)
}
ContributeValidationFailure.CrowdloanEnded -> {
TransformedFailure.Default(
resourceManager.getString(R.string.crowdloan_ended_title) to
resourceManager.getString(R.string.crowdloan_ended_message)
)
}
ContributeValidationFailure.CapExceeded.FromRaised -> {
TransformedFailure.Default(
resourceManager.getString(R.string.crowdloan_cap_reached_title) to
resourceManager.getString(R.string.crowdloan_cap_reached_raised_message)
)
}
is ContributeValidationFailure.CapExceeded.FromAmount -> {
val formattedAmount = with(reason) {
maxAllowedContribution.formatTokenAmount(chainAsset)
}
TransformedFailure.Default(
resourceManager.getString(R.string.crowdloan_cap_reached_title) to
resourceManager.getString(R.string.crowdloan_cap_reached_amount_message, formattedAmount)
)
}
is ContributeValidationFailure.LessThanMinContribution -> {
val formattedAmount = with(reason) {
minContribution.formatTokenAmount(chainAsset)
}
TransformedFailure.Default(
resourceManager.getString(R.string.crowdloan_too_small_contribution_title) to
resourceManager.getString(R.string.crowdloan_too_small_contribution_message, formattedAmount)
)
}
ContributeValidationFailure.PrivateCrowdloanNotSupported -> {
TransformedFailure.Default(
resourceManager.getString(R.string.crodloan_private_crowdloan_title) to
resourceManager.getString(R.string.crodloan_private_crowdloan_message)
)
}
ContributeValidationFailure.BonusNotApplied -> {
TransformedFailure.Custom(
CustomDialogDisplayer.Payload(
title = resourceManager.getString(R.string.crowdloan_missing_bonus_title),
message = resourceManager.getString(R.string.crowdloan_missing_bonus_message),
okAction = CustomDialogDisplayer.Payload.DialogAction(
title = resourceManager.getString(R.string.crowdloan_missing_bonus_action),
action = { onOpenCustomContribute?.invoke() }
),
cancelAction = CustomDialogDisplayer.Payload.DialogAction(
title = resourceManager.getString(R.string.common_skip),
action = { validationFlowActions.resumeFlow() }
),
customStyle = R.style.AccentNegativeAlertDialogTheme
)
)
}
}
}
@@ -0,0 +1,107 @@
package io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.confirm
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import coil.ImageLoader
import io.novafoundation.nova.common.base.BaseFragment
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.mixin.impl.observeBrowserEvents
import io.novafoundation.nova.common.mixin.impl.observeValidations
import io.novafoundation.nova.common.presentation.masking.dataOrNull
import io.novafoundation.nova.common.utils.setVisible
import io.novafoundation.nova.common.view.setProgressState
import io.novafoundation.nova.feature_account_api.presenatation.actions.setupExternalActions
import io.novafoundation.nova.feature_crowdloan_api.di.CrowdloanFeatureApi
import io.novafoundation.nova.feature_crowdloan_impl.databinding.FragmentContributeConfirmBinding
import io.novafoundation.nova.feature_crowdloan_impl.di.CrowdloanFeatureComponent
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.confirm.parcel.ConfirmContributePayload
import kotlinx.coroutines.flow.filterNotNull
import javax.inject.Inject
private const val KEY_PAYLOAD = "KEY_PAYLOAD"
class ConfirmContributeFragment : BaseFragment<ConfirmContributeViewModel, FragmentContributeConfirmBinding>() {
@Inject
lateinit var imageLoader: ImageLoader
companion object {
fun getBundle(payload: ConfirmContributePayload) = Bundle().apply {
putParcelable(KEY_PAYLOAD, payload)
}
}
override fun createBinding() = FragmentContributeConfirmBinding.inflate(layoutInflater)
override fun initViews() {
binder.confirmContributeToolbar.setHomeButtonListener { viewModel.backClicked() }
binder.confirmContributeConfirm.prepareForProgress(viewLifecycleOwner)
binder.confirmContributeConfirm.setOnClickListener { viewModel.nextClicked() }
binder.confirmContributeOriginAcount.setWholeClickListener { viewModel.originAccountClicked() }
}
override fun inject() {
val payload = argument<ConfirmContributePayload>("KEY_PAYLOAD")
FeatureUtils.getFeature<CrowdloanFeatureComponent>(
requireContext(),
CrowdloanFeatureApi::class.java
)
.confirmContributeFactory()
.create(this, payload)
.inject(this)
}
override fun subscribe(viewModel: ConfirmContributeViewModel) {
observeBrowserEvents(viewModel)
observeValidations(viewModel)
setupExternalActions(viewModel)
viewModel.showNextProgress.observe(binder.confirmContributeConfirm::setProgressState)
viewModel.assetModelFlow.observe {
binder.confirmContributeAmount.setAssetBalance(it.assetBalance.dataOrNull() ?: "")
binder.confirmContributeAmount.setAssetName(it.tokenSymbol)
binder.confirmContributeAmount.loadAssetImage(it.icon)
}
binder.confirmContributeAmount.amountInput.setText(viewModel.selectedAmount)
viewModel.enteredFiatAmountFlow.observe {
it.let(binder.confirmContributeAmount::setFiatAmount)
}
viewModel.feeFlow.observe(binder.confirmContributeFee::setFeeStatus)
with(binder.confirmContributeReward) {
val reward = viewModel.estimatedReward
setVisible(reward != null)
reward?.let { showValue(it) }
}
viewModel.crowdloanInfoFlow.observe {
binder.confirmContributeLeasingPeriod.showValue(it.leasePeriod, it.leasedUntil)
}
viewModel.selectedAddressModelFlow.observe {
binder.confirmContributeOriginAcount.setMessage(it.nameOrAddress)
binder.confirmContributeOriginAcount.setTextIcon(it.image)
}
viewModel.bonusFlow.observe {
binder.confirmContributeBonus.setVisible(it != null)
it?.let(binder.confirmContributeBonus::showValue)
}
viewModel.customizationConfiguration.filterNotNull().observe { (customization, customViewState) ->
customization.injectViews(binder.confirmContributeContainer, customViewState, viewLifecycleOwner.lifecycleScope)
}
}
}
@@ -0,0 +1,228 @@
package io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.confirm
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import io.novafoundation.nova.common.address.AddressIconGenerator
import io.novafoundation.nova.common.base.BaseViewModel
import io.novafoundation.nova.common.mixin.api.Validatable
import io.novafoundation.nova.common.presentation.AssetIconProvider
import io.novafoundation.nova.common.presentation.masking.MaskableModel
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.Event
import io.novafoundation.nova.common.utils.flowOf
import io.novafoundation.nova.common.utils.inBackground
import io.novafoundation.nova.common.utils.lazyAsync
import io.novafoundation.nova.common.validation.CompositeValidation
import io.novafoundation.nova.common.validation.ValidationExecutor
import io.novafoundation.nova.common.validation.ValidationSystem
import io.novafoundation.nova.common.validation.progressConsumer
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
import io.novafoundation.nova.feature_account_api.presenatation.account.icon.createAccountAddressModel
import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions
import io.novafoundation.nova.feature_account_api.presenatation.actions.showAddressActions
import io.novafoundation.nova.feature_account_api.presenatation.navigation.ExtrinsicNavigationWrapper
import io.novafoundation.nova.feature_crowdloan_impl.R
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.CustomContributeManager
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.CrowdloanContributeInteractor
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.ContributeValidation
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.ContributeValidationPayload
import io.novafoundation.nova.feature_crowdloan_impl.presentation.CrowdloanRouter
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.confirm.model.LeasePeriodModel
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.confirm.parcel.ConfirmContributePayload
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.contributeValidationFailure
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.custom.ConfirmContributeCustomization
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.select.parcel.mapParachainMetadataFromParcel
import io.novafoundation.nova.feature_currency_api.presentation.formatters.formatAsCurrency
import io.novafoundation.nova.feature_wallet_api.data.mappers.mapAssetToAssetModel
import io.novafoundation.nova.feature_wallet_api.data.mappers.mapFeeToFeeModel
import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel
import io.novafoundation.nova.runtime.state.SingleAssetSharedState
import io.novafoundation.nova.runtime.state.chain
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
class ConfirmContributeViewModel(
private val assetIconProvider: AssetIconProvider,
private val router: CrowdloanRouter,
private val contributionInteractor: CrowdloanContributeInteractor,
private val resourceManager: ResourceManager,
assetUseCase: AssetUseCase,
accountUseCase: SelectedAccountUseCase,
addressModelGenerator: AddressIconGenerator,
private val validationExecutor: ValidationExecutor,
private val payload: ConfirmContributePayload,
private val validations: Collection<ContributeValidation>,
private val customContributeManager: CustomContributeManager,
private val externalActions: ExternalActions.Presentation,
private val assetSharedState: SingleAssetSharedState,
private val extrinsicNavigationWrapper: ExtrinsicNavigationWrapper,
private val amountFormatter: AmountFormatter
) : BaseViewModel(),
Validatable by validationExecutor,
ExternalActions by externalActions,
ExtrinsicNavigationWrapper by extrinsicNavigationWrapper {
private val decimalFee = mapFeeFromParcel(payload.fee)
private val chain by lazyAsync { assetSharedState.chain() }
override val openBrowserEvent = MutableLiveData<Event<String>>()
private val _showNextProgress = MutableLiveData(false)
val showNextProgress: LiveData<Boolean> = _showNextProgress
private val assetFlow = assetUseCase.currentAssetFlow()
.share()
val assetModelFlow = assetFlow
.map {
mapAssetToAssetModel(
assetIconProvider,
it,
resourceManager,
// Very rude way to show transferable balance but we don't support contributions so I don't se a reason for deeper refactoring
MaskableModel.Unmasked(it.transferableInPlanks)
)
}
.inBackground()
.share()
val selectedAddressModelFlow = accountUseCase.selectedMetaAccountFlow()
.map { metaAccount ->
addressModelGenerator.createAccountAddressModel(chain.await(), metaAccount)
}
.shareInBackground()
val selectedAmount = payload.amount.toString()
val feeFlow = assetFlow.map { asset ->
val feeModel = mapFeeToFeeModel(decimalFee, asset.token, amountFormatter = amountFormatter)
FeeStatus.Loaded(feeModel)
}
.inBackground()
.share()
val enteredFiatAmountFlow = assetFlow.map { asset ->
asset.token.amountToFiat(payload.amount).formatAsCurrency(asset.token.currency)
}
.inBackground()
.share()
private val parachainMetadata = payload.metadata?.let(::mapParachainMetadataFromParcel)
private val relevantCustomFlowFactory = parachainMetadata?.customFlow?.let {
customContributeManager.getFactoryOrNull(it)
}
val customizationConfiguration: Flow<Pair<ConfirmContributeCustomization, ConfirmContributeCustomization.ViewState>?> = flowOf {
relevantCustomFlowFactory?.confirmContributeCustomization?.let {
it to it.createViewState(coroutineScope = this, parachainMetadata!!, payload.customizationPayload)
}
}
.inBackground()
.share()
val estimatedReward = payload.estimatedRewardDisplay
private val crowdloanFlow = contributionInteractor.crowdloanStateFlow(payload.paraId, parachainMetadata)
.inBackground()
.share()
val crowdloanInfoFlow = crowdloanFlow.map { crowdloan ->
LeasePeriodModel(
leasePeriod = resourceManager.formatDuration(crowdloan.leasePeriodInMillis),
leasedUntil = resourceManager.formatDateTime(crowdloan.leasedUntilInMillis)
)
}
.inBackground()
.share()
val bonusFlow = flow {
val bonusDisplay = payload.bonusPayload?.bonusText(payload.amount)
emit(bonusDisplay)
}
.inBackground()
.share()
private val customizedValidationSystem = flowOf {
val validations = relevantCustomFlowFactory?.confirmContributeCustomization?.modifyValidations(validations)
?: validations
ValidationSystem(CompositeValidation(validations))
}
.inBackground()
.share()
fun nextClicked() {
maybeGoToNext()
}
fun backClicked() {
router.back()
}
fun originAccountClicked() {
launch {
val accountAddress = selectedAddressModelFlow.first().address
val chain = assetSharedState.chain()
externalActions.showAddressActions(accountAddress, chain)
}
}
private fun maybeGoToNext() = launch {
val validationPayload = ContributeValidationPayload(
crowdloan = crowdloanFlow.first(),
fee = decimalFee,
asset = assetFlow.first(),
customizationPayload = payload.customizationPayload,
bonusPayload = payload.bonusPayload,
contributionAmount = payload.amount
)
validationExecutor.requireValid(
validationSystem = customizedValidationSystem.first(),
payload = validationPayload,
progressConsumer = _showNextProgress.progressConsumer(),
validationFailureTransformerCustom = { status, actions ->
contributeValidationFailure(
reason = status.reason,
validationFlowActions = actions,
resourceManager = resourceManager,
onOpenCustomContribute = null
)
}
) {
sendTransaction()
}
}
private fun sendTransaction() {
launch {
val crowdloan = crowdloanFlow.first()
contributionInteractor.contribute(
crowdloan = crowdloan,
contribution = payload.amount,
bonusPayload = payload.bonusPayload,
customizationPayload = payload.customizationPayload
)
.onFailure(::showError)
.onSuccess {
showToast(resourceManager.getString(R.string.common_transaction_submitted))
startNavigation(it.submissionHierarchy) { router.returnToMain() }
}
_showNextProgress.value = false
}
}
}
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.confirm.di
import androidx.fragment.app.Fragment
import dagger.BindsInstance
import dagger.Subcomponent
import io.novafoundation.nova.common.di.scope.ScreenScope
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.confirm.ConfirmContributeFragment
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.confirm.parcel.ConfirmContributePayload
@Subcomponent(
modules = [
ConfirmContributeModule::class
]
)
@ScreenScope
interface ConfirmContributeComponent {
@Subcomponent.Factory
interface Factory {
fun create(
@BindsInstance fragment: Fragment,
@BindsInstance payload: ConfirmContributePayload
): ConfirmContributeComponent
}
fun inject(fragment: ConfirmContributeFragment)
}
@@ -0,0 +1,78 @@
package io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.confirm.di
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoMap
import io.novafoundation.nova.common.address.AddressIconGenerator
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
import io.novafoundation.nova.common.presentation.AssetIconProvider
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.validation.ValidationExecutor
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions
import io.novafoundation.nova.feature_account_api.presenatation.navigation.ExtrinsicNavigationWrapper
import io.novafoundation.nova.feature_crowdloan_impl.data.CrowdloanSharedState
import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.CustomContributeManager
import io.novafoundation.nova.feature_crowdloan_impl.di.validations.Confirm
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.CrowdloanContributeInteractor
import io.novafoundation.nova.feature_crowdloan_impl.domain.contribute.validations.ContributeValidation
import io.novafoundation.nova.feature_crowdloan_impl.presentation.CrowdloanRouter
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.confirm.ConfirmContributeViewModel
import io.novafoundation.nova.feature_crowdloan_impl.presentation.contribute.confirm.parcel.ConfirmContributePayload
import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
@Module(includes = [ViewModelModule::class])
class ConfirmContributeModule {
@Provides
@IntoMap
@ViewModelKey(ConfirmContributeViewModel::class)
fun provideViewModel(
assetIconProvider: AssetIconProvider,
interactor: CrowdloanContributeInteractor,
router: CrowdloanRouter,
resourceManager: ResourceManager,
assetUseCase: AssetUseCase,
validationExecutor: ValidationExecutor,
payload: ConfirmContributePayload,
accountUseCase: SelectedAccountUseCase,
addressIconGenerator: AddressIconGenerator,
@Confirm contributeValidations: @JvmSuppressWildcards Set<ContributeValidation>,
externalActions: ExternalActions.Presentation,
customContributeManager: CustomContributeManager,
singleAssetSharedState: CrowdloanSharedState,
extrinsicNavigationWrapper: ExtrinsicNavigationWrapper,
amountFormatter: AmountFormatter
): ViewModel {
return ConfirmContributeViewModel(
assetIconProvider,
router,
interactor,
resourceManager,
assetUseCase,
accountUseCase,
addressIconGenerator,
validationExecutor,
payload,
contributeValidations,
customContributeManager,
externalActions,
singleAssetSharedState,
extrinsicNavigationWrapper,
amountFormatter = amountFormatter
)
}
@Provides
fun provideViewModelCreator(
fragment: Fragment,
viewModelFactory: ViewModelProvider.Factory
): ConfirmContributeViewModel {
return ViewModelProvider(fragment, viewModelFactory).get(ConfirmContributeViewModel::class.java)
}
}

Some files were not shown because too many files have changed in this diff Show More