mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-22 02:07:58 +00:00
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:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,32 @@
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
|
||||
android {
|
||||
|
||||
defaultConfig {
|
||||
|
||||
|
||||
}
|
||||
|
||||
namespace 'io.novafoundation.nova.feature_governance_api'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation coroutinesDep
|
||||
implementation project(':runtime')
|
||||
implementation project(":common")
|
||||
implementation project(':feature-deep-linking')
|
||||
|
||||
api project(":feature-wallet-api")
|
||||
api project(":feature-account-api")
|
||||
api project(":feature-dapp-api")
|
||||
|
||||
implementation markwonDep
|
||||
|
||||
implementation daggerDep
|
||||
|
||||
implementation substrateSdkDep
|
||||
|
||||
implementation androidDep
|
||||
|
||||
api project(':core-api')
|
||||
}
|
||||
+21
@@ -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,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data
|
||||
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
interface MutableGovernanceState {
|
||||
fun update(chainId: String, assetId: Int, governanceType: Chain.Governance)
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.model
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.AccountVote
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumId
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.VoteType
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.constructAccountVote
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.types.custom.vote.Conviction
|
||||
import java.math.BigInteger
|
||||
|
||||
data class TinderGovBasketItem(
|
||||
val metaId: Long,
|
||||
val chainId: String,
|
||||
val referendumId: ReferendumId,
|
||||
val voteType: VoteType,
|
||||
val conviction: Conviction,
|
||||
val amount: BigInteger
|
||||
)
|
||||
|
||||
fun TinderGovBasketItem.accountVote(): AccountVote {
|
||||
return AccountVote.constructAccountVote(amount, conviction, voteType)
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.model
|
||||
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.types.custom.vote.Conviction
|
||||
import java.math.BigInteger
|
||||
|
||||
class VotingPower(
|
||||
val metaId: Long,
|
||||
val chainId: ChainId,
|
||||
val amount: BigInteger,
|
||||
val conviction: Conviction
|
||||
)
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.network.blockhain.model
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.types.custom.vote.Conviction
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
|
||||
data class Delegation(
|
||||
val vote: Vote,
|
||||
val delegator: AccountId,
|
||||
val delegate: AccountId,
|
||||
) {
|
||||
|
||||
data class Vote(
|
||||
val amount: Balance,
|
||||
val conviction: Conviction
|
||||
)
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.network.blockhain.model
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import java.math.BigInteger
|
||||
|
||||
@JvmInline
|
||||
value class TrackId(val value: BigInteger) {
|
||||
|
||||
override fun toString(): String {
|
||||
return value.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmInline
|
||||
value class ReferendumId(val value: BigInteger) {
|
||||
|
||||
override fun toString(): String {
|
||||
return value.toString()
|
||||
}
|
||||
}
|
||||
|
||||
class OnChainReferendum(
|
||||
val status: OnChainReferendumStatus,
|
||||
val id: ReferendumId,
|
||||
)
|
||||
|
||||
sealed class OnChainReferendumStatus {
|
||||
|
||||
class Ongoing(
|
||||
val track: TrackId,
|
||||
val proposal: Proposal,
|
||||
val submitted: BlockNumber,
|
||||
val submissionDeposit: ReferendumDeposit?,
|
||||
val decisionDeposit: ReferendumDeposit?,
|
||||
val deciding: DecidingStatus?,
|
||||
val tally: Tally,
|
||||
val inQueue: Boolean,
|
||||
val threshold: VotingThreshold
|
||||
) : OnChainReferendumStatus()
|
||||
|
||||
class Approved(override val since: BlockNumber) : OnChainReferendumStatus(), TimeSinceStatus
|
||||
|
||||
class Rejected(override val since: BlockNumber) : OnChainReferendumStatus(), TimeSinceStatus
|
||||
|
||||
class Cancelled(override val since: BlockNumber) : OnChainReferendumStatus(), TimeSinceStatus
|
||||
|
||||
class TimedOut(override val since: BlockNumber) : OnChainReferendumStatus(), TimeSinceStatus
|
||||
|
||||
class Killed(override val since: BlockNumber) : OnChainReferendumStatus(), TimeSinceStatus
|
||||
|
||||
interface TimeSinceStatus {
|
||||
val since: BlockNumber
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Proposal {
|
||||
|
||||
class Legacy(val hash: ByteArray) : Proposal()
|
||||
|
||||
class Inline(val encodedCall: ByteArray, val call: GenericCall.Instance) : Proposal()
|
||||
|
||||
class Lookup(val hash: ByteArray, val callLength: BigInteger) : Proposal()
|
||||
}
|
||||
|
||||
class DecidingStatus(
|
||||
val since: BlockNumber,
|
||||
val confirming: ConfirmingSource
|
||||
)
|
||||
|
||||
sealed class ConfirmingSource {
|
||||
|
||||
class FromThreshold(val end: BlockNumber) : ConfirmingSource()
|
||||
|
||||
class OnChain(val status: ConfirmingStatus?) : ConfirmingSource()
|
||||
}
|
||||
|
||||
class ConfirmingStatus(val till: BlockNumber)
|
||||
|
||||
class Tally(
|
||||
// post-conviction
|
||||
val ayes: Balance,
|
||||
// post-conviction
|
||||
val nays: Balance,
|
||||
// pre-conviction
|
||||
val support: Balance
|
||||
)
|
||||
|
||||
class ReferendumDeposit(
|
||||
val who: AccountId,
|
||||
val amount: Balance
|
||||
)
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.network.blockhain.model
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.Perbill
|
||||
import io.novafoundation.nova.common.utils.castOrNull
|
||||
import io.novafoundation.nova.common.utils.divideToDecimal
|
||||
import io.novafoundation.nova.common.utils.filterValuesIsInstance
|
||||
import io.novafoundation.nova.common.utils.mapToSet
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.common.ReferendumVoting.Approval
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import java.math.BigInteger
|
||||
import io.novasama.substrate_sdk_android.hash.Hasher.blake2b256
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
|
||||
fun OnChainReferendum.isFinished(): Boolean {
|
||||
return status !is OnChainReferendumStatus.Ongoing
|
||||
}
|
||||
|
||||
fun OnChainReferendum.proposal(): Proposal? {
|
||||
return status.asOngoingOrNull()?.proposal
|
||||
}
|
||||
|
||||
fun Proposal.hash(): ByteArray {
|
||||
return when (this) {
|
||||
is Proposal.Inline -> encodedCall.blake2b256()
|
||||
is Proposal.Legacy -> hash
|
||||
is Proposal.Lookup -> hash
|
||||
}
|
||||
}
|
||||
|
||||
fun OnChainReferendum.track(): TrackId? {
|
||||
return status.asOngoingOrNull()?.track
|
||||
}
|
||||
|
||||
fun OnChainReferendum.submissionDeposit(): ReferendumDeposit? {
|
||||
return when (status) {
|
||||
is OnChainReferendumStatus.Ongoing -> status.submissionDeposit
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun OnChainReferendumStatus.inQueue(): Boolean {
|
||||
return this is OnChainReferendumStatus.Ongoing && inQueue
|
||||
}
|
||||
|
||||
fun OnChainReferendumStatus.asOngoing(): OnChainReferendumStatus.Ongoing {
|
||||
return asOngoingOrNull() ?: error("Referendum is not ongoing")
|
||||
}
|
||||
|
||||
fun OnChainReferendumStatus.sinceOrThrow(): BlockNumber {
|
||||
return asTimeSinceStatusOrNull()?.since ?: error("Status doesn't have since field")
|
||||
}
|
||||
|
||||
fun OnChainReferendumStatus.asOngoingOrNull(): OnChainReferendumStatus.Ongoing? {
|
||||
return castOrNull<OnChainReferendumStatus.Ongoing>()
|
||||
}
|
||||
|
||||
fun OnChainReferendumStatus.asTimeSinceStatusOrNull(): OnChainReferendumStatus.TimeSinceStatus? {
|
||||
return castOrNull<OnChainReferendumStatus.TimeSinceStatus>()
|
||||
}
|
||||
|
||||
fun Tally.ayeVotes(): Approval.Votes {
|
||||
return votesOf(Tally::ayes)
|
||||
}
|
||||
|
||||
fun Tally.nayVotes(): Approval.Votes {
|
||||
return votesOf(Tally::nays)
|
||||
}
|
||||
|
||||
fun Map<TrackId, Voting>.flattenCastingVotes(): Map<ReferendumId, AccountVote> {
|
||||
return flatMap { (_, voting) ->
|
||||
when (voting) {
|
||||
is Voting.Casting -> voting.votes.toList()
|
||||
is Voting.Delegating -> emptyList()
|
||||
}
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
fun Map<TrackId, Voting>.delegations(to: AccountId? = null): Map<TrackId, Voting.Delegating> {
|
||||
val onlyDelegations = filterValuesIsInstance<TrackId, Voting.Delegating>()
|
||||
|
||||
return if (to != null) {
|
||||
onlyDelegations.filterValues { it.target.contentEquals(to) }
|
||||
} else {
|
||||
onlyDelegations
|
||||
}
|
||||
}
|
||||
|
||||
val OnChainReferendumStatus.Ongoing.proposer: AccountId?
|
||||
get() = submissionDeposit?.who
|
||||
|
||||
fun OnChainReferendumStatus.Ongoing.proposerDeposit(): Balance? {
|
||||
return proposer?.let(::depositBy)
|
||||
}
|
||||
|
||||
fun OnChainReferendumStatus.Ongoing.depositBy(accountId: AccountId): Balance {
|
||||
return submissionDeposit.amountBy(accountId) + decisionDeposit.amountBy(accountId)
|
||||
}
|
||||
|
||||
fun ReferendumDeposit?.amountBy(accountId: AccountId): Balance {
|
||||
if (this == null) return Balance.ZERO
|
||||
|
||||
return amount.takeIf { who.contentEquals(accountId) }.orZero()
|
||||
}
|
||||
|
||||
@Suppress("FunctionName")
|
||||
private fun EmptyVotes() = Approval.Votes(
|
||||
amount = Balance.ZERO,
|
||||
fraction = Perbill.ZERO
|
||||
)
|
||||
|
||||
private inline fun Tally.votesOf(field: (Tally) -> Balance): Approval.Votes {
|
||||
val totalVotes = ayes + nays
|
||||
|
||||
if (totalVotes == Balance.ZERO) return EmptyVotes()
|
||||
|
||||
val amount = field(this)
|
||||
val fraction = amount.divideToDecimal(totalVotes)
|
||||
|
||||
return Approval.Votes(
|
||||
amount = amount,
|
||||
fraction = fraction
|
||||
)
|
||||
}
|
||||
|
||||
fun Set<BigInteger>.toTrackIds(): Set<TrackId> {
|
||||
return mapToSet { TrackId(it) }
|
||||
}
|
||||
|
||||
fun Set<TrackId>.fromTrackIds(): Set<BigInteger> {
|
||||
return mapToSet { it.value }
|
||||
}
|
||||
|
||||
fun ConfirmingSource.asOnChain(): ConfirmingSource.OnChain {
|
||||
return this as ConfirmingSource.OnChain
|
||||
}
|
||||
|
||||
fun ConfirmingSource.asOnChainOrNull(): ConfirmingSource.OnChain? {
|
||||
return this as? ConfirmingSource.OnChain
|
||||
}
|
||||
|
||||
fun ConfirmingSource.till(): BlockNumber {
|
||||
return asOnChain().status!!.till
|
||||
}
|
||||
|
||||
fun ConfirmingSource.tillOrNull(): BlockNumber? {
|
||||
return asOnChainOrNull()?.status?.till
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.network.blockhain.model
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
|
||||
class PreImage(val encodedCall: ByteArray, val call: GenericCall.Instance)
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.network.blockhain.model
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
|
||||
class ReferendumVoter(
|
||||
val accountId: AccountId,
|
||||
val vote: AccountVote,
|
||||
val delegators: List<Delegation>
|
||||
)
|
||||
|
||||
fun ReferendumVoter.getAllAccountIds(): List<AccountId> {
|
||||
return buildList {
|
||||
add(accountId)
|
||||
addAll(delegators.map { it.delegator })
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.network.blockhain.model
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.Perbill
|
||||
|
||||
data class TrackInfo(
|
||||
val id: TrackId,
|
||||
val name: String,
|
||||
val preparePeriod: BlockNumber,
|
||||
val decisionPeriod: BlockNumber,
|
||||
val confirmPeriod: BlockNumber,
|
||||
val minApproval: VotingCurve?,
|
||||
val minSupport: VotingCurve?
|
||||
)
|
||||
|
||||
interface VotingCurve {
|
||||
|
||||
val name: String
|
||||
|
||||
fun threshold(x: Perbill): Perbill
|
||||
|
||||
fun delay(y: Perbill): Perbill
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.network.blockhain.model
|
||||
|
||||
class TrackQueue(val referenda: List<ReferendumId>) {
|
||||
|
||||
class Position(val index: Int, val maxSize: Int)
|
||||
|
||||
companion object;
|
||||
}
|
||||
|
||||
fun TrackQueue.Companion.empty(): TrackQueue = TrackQueue(emptyList())
|
||||
|
||||
fun TrackQueue.positionOf(referendumId: ReferendumId): TrackQueue.Position {
|
||||
return TrackQueue.Position(
|
||||
index = referenda.indexOf(referendumId) + 1,
|
||||
maxSize = referenda.size
|
||||
)
|
||||
}
|
||||
|
||||
fun TrackQueue?.orEmpty(): TrackQueue {
|
||||
return this ?: TrackQueue.empty()
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.network.blockhain.model
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import java.math.BigInteger
|
||||
|
||||
class TreasuryProposal(
|
||||
val id: Id,
|
||||
val proposer: AccountId,
|
||||
val amount: Balance,
|
||||
val beneficiary: AccountId,
|
||||
val bond: Balance
|
||||
) {
|
||||
|
||||
@JvmInline
|
||||
value class Id(val value: BigInteger)
|
||||
}
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.network.blockhain.model
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.voters.GenericVoter
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.types.custom.vote.Conviction
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.types.custom.vote.Vote
|
||||
import io.novasama.substrate_sdk_android.hash.isPositive
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
sealed class Voting {
|
||||
|
||||
data class Casting(
|
||||
val votes: Map<ReferendumId, AccountVote>,
|
||||
val prior: PriorLock
|
||||
) : Voting()
|
||||
|
||||
class Delegating(
|
||||
val amount: Balance,
|
||||
val target: AccountId,
|
||||
val conviction: Conviction,
|
||||
val prior: PriorLock
|
||||
) : Voting()
|
||||
}
|
||||
|
||||
sealed class AccountVote {
|
||||
|
||||
data class Standard(
|
||||
val vote: Vote,
|
||||
val balance: Balance
|
||||
) : AccountVote()
|
||||
|
||||
class Split(
|
||||
val aye: Balance,
|
||||
val nay: Balance,
|
||||
) : AccountVote()
|
||||
|
||||
class SplitAbstain(
|
||||
val aye: Balance,
|
||||
val nay: Balance,
|
||||
val abstain: Balance,
|
||||
) : AccountVote()
|
||||
|
||||
object Unsupported : AccountVote()
|
||||
|
||||
companion object
|
||||
}
|
||||
|
||||
data class PriorLock(
|
||||
val unlockAt: BlockNumber,
|
||||
val amount: Balance,
|
||||
)
|
||||
|
||||
enum class VoteType {
|
||||
AYE, NAY, ABSTAIN
|
||||
}
|
||||
|
||||
fun VoteType.isAye(): Boolean {
|
||||
return this == VoteType.AYE
|
||||
}
|
||||
|
||||
fun Voting.trackVotesNumber(): Int {
|
||||
return when (this) {
|
||||
is Voting.Casting -> votes.size
|
||||
is Voting.Delegating -> 0
|
||||
}
|
||||
}
|
||||
|
||||
fun Voting.votedReferenda(): Collection<ReferendumId> {
|
||||
return when (this) {
|
||||
is Voting.Casting -> votes.keys
|
||||
is Voting.Delegating -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun AyeVote(amount: Balance, conviction: Conviction) = AccountVote.Standard(
|
||||
vote = Vote(
|
||||
aye = true,
|
||||
conviction = conviction
|
||||
),
|
||||
balance = amount
|
||||
)
|
||||
|
||||
fun AccountVote.amount(): Balance {
|
||||
return when (this) {
|
||||
is AccountVote.Standard -> balance
|
||||
is AccountVote.Split -> aye + nay
|
||||
is AccountVote.SplitAbstain -> aye + nay + abstain
|
||||
AccountVote.Unsupported -> Balance.ZERO
|
||||
}
|
||||
}
|
||||
|
||||
fun AccountVote.conviction(): Conviction? {
|
||||
return when (this) {
|
||||
is AccountVote.Standard -> vote.conviction
|
||||
is AccountVote.Split -> Conviction.None
|
||||
is AccountVote.SplitAbstain -> Conviction.None
|
||||
AccountVote.Unsupported -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun AccountVote.votedFor(type: VoteType): Boolean {
|
||||
return when (this) {
|
||||
// we still want to show zero votes since it might have delegators
|
||||
is AccountVote.Standard -> voteType == type
|
||||
|
||||
is AccountVote.Split -> hasPositiveAmountFor(type)
|
||||
|
||||
is AccountVote.SplitAbstain -> hasPositiveAmountFor(type)
|
||||
|
||||
AccountVote.Unsupported -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun AccountVote.hasPositiveAmountFor(type: VoteType): Boolean {
|
||||
val amount = amountFor(type)
|
||||
|
||||
return amount != null && amount.isPositive()
|
||||
}
|
||||
|
||||
fun AccountVote.amountFor(type: VoteType): Balance? {
|
||||
return when (this) {
|
||||
is AccountVote.Standard -> {
|
||||
if (voteType == type) balance else Balance.ZERO
|
||||
}
|
||||
|
||||
is AccountVote.Split -> when (type) {
|
||||
VoteType.AYE -> aye
|
||||
VoteType.NAY -> nay
|
||||
VoteType.ABSTAIN -> Balance.ZERO
|
||||
}
|
||||
|
||||
is AccountVote.SplitAbstain -> when (type) {
|
||||
VoteType.AYE -> aye
|
||||
VoteType.NAY -> nay
|
||||
VoteType.ABSTAIN -> abstain
|
||||
}
|
||||
|
||||
AccountVote.Unsupported -> null
|
||||
}
|
||||
}
|
||||
|
||||
private val AccountVote.Standard.voteType: VoteType
|
||||
get() = if (vote.aye) VoteType.AYE else VoteType.NAY
|
||||
|
||||
fun Voting.votes(): Map<ReferendumId, AccountVote> {
|
||||
return when (this) {
|
||||
is Voting.Casting -> votes
|
||||
is Voting.Delegating -> emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
fun Voting.totalLock(): Balance {
|
||||
return when (this) {
|
||||
is Voting.Casting -> {
|
||||
val fromVotes = votes.maxOfOrNull { it.value.amount() }.orZero()
|
||||
|
||||
fromVotes.max(prior.amount)
|
||||
}
|
||||
|
||||
is Voting.Delegating -> amount.max(prior.amount)
|
||||
}
|
||||
}
|
||||
|
||||
fun AccountVote.completedReferendumLockDuration(referendumOutcome: VoteType, lockPeriod: BlockNumber): BlockNumber {
|
||||
return when (this) {
|
||||
AccountVote.Unsupported -> BlockNumber.ZERO
|
||||
|
||||
is AccountVote.Standard -> {
|
||||
val approved = referendumOutcome == VoteType.AYE
|
||||
|
||||
// vote has the same direction as outcome
|
||||
if (approved == vote.aye) {
|
||||
vote.conviction.lockDuration(lockPeriod)
|
||||
} else {
|
||||
BlockNumber.ZERO
|
||||
}
|
||||
}
|
||||
|
||||
is AccountVote.Split -> BlockNumber.ZERO
|
||||
is AccountVote.SplitAbstain -> BlockNumber.ZERO
|
||||
}
|
||||
}
|
||||
|
||||
fun AccountVote.maxLockDuration(lockPeriod: BlockNumber): BlockNumber {
|
||||
return when (this) {
|
||||
is AccountVote.Standard -> vote.conviction.lockDuration(lockPeriod)
|
||||
AccountVote.Unsupported -> BlockNumber.ZERO
|
||||
is AccountVote.Split -> BlockNumber.ZERO
|
||||
is AccountVote.SplitAbstain -> BlockNumber.ZERO
|
||||
}
|
||||
}
|
||||
|
||||
fun Conviction.lockDuration(lockPeriod: BlockNumber): BlockNumber {
|
||||
return lockPeriods() * lockPeriod
|
||||
}
|
||||
|
||||
fun Conviction.lockPeriods(): BigInteger {
|
||||
val multiplier = when (this) {
|
||||
Conviction.None -> 0
|
||||
Conviction.Locked1x -> 1
|
||||
Conviction.Locked2x -> 2
|
||||
Conviction.Locked3x -> 4
|
||||
Conviction.Locked4x -> 8
|
||||
Conviction.Locked5x -> 16
|
||||
Conviction.Locked6x -> 32
|
||||
}
|
||||
|
||||
return multiplier.toBigInteger()
|
||||
}
|
||||
|
||||
fun Conviction.votesFor(amount: BigDecimal): BigDecimal {
|
||||
return amountMultiplier() * amount
|
||||
}
|
||||
|
||||
fun Conviction.amountMultiplier(): BigDecimal {
|
||||
val multiplier: Double = when (this) {
|
||||
Conviction.None -> 0.1
|
||||
Conviction.Locked1x -> 1.0
|
||||
Conviction.Locked2x -> 2.0
|
||||
Conviction.Locked3x -> 3.0
|
||||
Conviction.Locked4x -> 4.0
|
||||
Conviction.Locked5x -> 5.0
|
||||
Conviction.Locked6x -> 6.0
|
||||
}
|
||||
|
||||
return multiplier.toBigDecimal()
|
||||
}
|
||||
|
||||
fun Voting.Delegating.getConvictionVote(chainAsset: Chain.Asset): GenericVoter.ConvictionVote {
|
||||
return GenericVoter.ConvictionVote(chainAsset.amountFromPlanks(amount), conviction)
|
||||
}
|
||||
|
||||
fun AccountVote.Companion.constructAccountVote(amount: BigInteger, conviction: Conviction, voteType: VoteType): AccountVote {
|
||||
return if (voteType == VoteType.ABSTAIN) {
|
||||
AccountVote.SplitAbstain(
|
||||
aye = BigInteger.ZERO,
|
||||
nay = BigInteger.ZERO,
|
||||
abstain = amount
|
||||
)
|
||||
} else {
|
||||
AccountVote.Standard(
|
||||
vote = Vote(
|
||||
aye = voteType == VoteType.AYE,
|
||||
conviction = conviction
|
||||
),
|
||||
balance = amount
|
||||
)
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.network.blockhain.model
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.Perbill
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import java.math.BigDecimal
|
||||
|
||||
interface VotingThreshold {
|
||||
|
||||
class Threshold<T>(val value: T, val currentlyPassing: Boolean, val projectedPassing: ProjectedPassing) {
|
||||
companion object;
|
||||
}
|
||||
|
||||
class ProjectedPassing(val delayFraction: Perbill, val passingInFuture: Boolean)
|
||||
|
||||
fun supportThreshold(tally: Tally, totalIssuance: Balance, passedSinceDecidingFraction: Perbill): Threshold<Balance>
|
||||
|
||||
fun ayesFractionThreshold(tally: Tally, totalIssuance: Balance, passedSinceDecidingFraction: Perbill): Threshold<Perbill>
|
||||
}
|
||||
|
||||
fun VotingThreshold.Threshold.Companion.simple(value: BigDecimal, currentlyPassing: Boolean) =
|
||||
VotingThreshold.Threshold<BigDecimal>(value, currentlyPassing = true, VotingThreshold.ProjectedPassing(value, currentlyPassing))
|
||||
|
||||
fun <T> VotingThreshold.Threshold.Companion.passing(value: T) =
|
||||
VotingThreshold.Threshold(value, currentlyPassing = true, VotingThreshold.ProjectedPassing(BigDecimal.ZERO, passingInFuture = true))
|
||||
|
||||
fun <T> VotingThreshold.Threshold.Companion.notPassing(value: T) =
|
||||
VotingThreshold.Threshold(value, currentlyPassing = false, VotingThreshold.ProjectedPassing(BigDecimal.ONE, passingInFuture = false))
|
||||
|
||||
fun VotingThreshold.ProjectedPassing.merge(another: VotingThreshold.ProjectedPassing): VotingThreshold.ProjectedPassing {
|
||||
return VotingThreshold.ProjectedPassing(
|
||||
delayFraction = delayFraction.coerceAtLeast(another.delayFraction),
|
||||
passingInFuture = passingInFuture && another.passingInFuture
|
||||
)
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.network.offchain.model.delegation
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
|
||||
class DelegateDetailedStats(
|
||||
val accountId: AccountId,
|
||||
val delegationsCount: Int,
|
||||
val delegatedVotes: Balance,
|
||||
val recentVotes: Int,
|
||||
val allVotes: Int
|
||||
)
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.network.offchain.model.delegation
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
|
||||
class DelegateMetadata(
|
||||
val accountId: AccountId,
|
||||
val shortDescription: String,
|
||||
val longDescription: String?,
|
||||
val profileImageUrl: String?,
|
||||
val isOrganization: Boolean,
|
||||
val name: String,
|
||||
)
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.network.offchain.model.delegation
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
|
||||
class DelegateStats(
|
||||
val accountId: AccountId,
|
||||
val delegationsCount: Int,
|
||||
val delegatedVotes: Balance,
|
||||
val recentVotes: Int
|
||||
)
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.network.offchain.model.referendum
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.details.ReferendumTimeline
|
||||
|
||||
class OffChainReferendumDetails(
|
||||
val title: String?,
|
||||
val description: String?,
|
||||
val proposerName: String?,
|
||||
val proposerAddress: String?,
|
||||
val timeLine: List<ReferendumTimeline.Entry>?
|
||||
)
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.network.offchain.model.referendum
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumId
|
||||
|
||||
class OffChainReferendumPreview(
|
||||
val title: String?,
|
||||
val referendumId: ReferendumId
|
||||
)
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.network.offchain.model.referendum
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.Tally
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackId
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.offchain.model.referendum.OffChainReferendumVotingDetails.VotingInfo
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import java.math.BigDecimal
|
||||
|
||||
class OffChainReferendumVotingDetails(
|
||||
val trackId: TrackId,
|
||||
val votingInfo: VotingInfo
|
||||
) {
|
||||
|
||||
sealed interface VotingInfo {
|
||||
|
||||
class Abstain(val abstain: BigDecimal) : VotingInfo
|
||||
|
||||
class Full(
|
||||
val aye: BigDecimal,
|
||||
val nay: BigDecimal,
|
||||
val abstain: BigDecimal,
|
||||
val support: Balance
|
||||
) : VotingInfo {
|
||||
companion object
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun VotingInfo.Full.Companion.empty() = VotingInfo.Full(
|
||||
aye = BigDecimal.ZERO,
|
||||
nay = BigDecimal.ZERO,
|
||||
abstain = BigDecimal.ZERO,
|
||||
support = Balance.ZERO
|
||||
)
|
||||
|
||||
operator fun VotingInfo.Full.plus(other: VotingInfo.Full): VotingInfo.Full {
|
||||
return VotingInfo.Full(
|
||||
aye = this.aye + other.aye,
|
||||
nay = this.nay + other.nay,
|
||||
abstain = this.abstain + other.abstain,
|
||||
support = this.support + other.support
|
||||
)
|
||||
}
|
||||
|
||||
fun VotingInfo.Full.toTally(): Tally = Tally(
|
||||
ayes = this.aye.toBigInteger(),
|
||||
nays = this.nay.toBigInteger(),
|
||||
support = this.support
|
||||
)
|
||||
|
||||
fun VotingInfo.getAbstain(): BigDecimal {
|
||||
return when (this) {
|
||||
is VotingInfo.Abstain -> this.abstain
|
||||
is VotingInfo.Full -> this.abstain
|
||||
}
|
||||
}
|
||||
|
||||
fun VotingInfo.toTallyOrNull(): Tally? {
|
||||
return when (this) {
|
||||
is VotingInfo.Full -> toTally()
|
||||
is VotingInfo.Abstain -> null
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.network.offchain.model.vote
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.AccountVote
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
|
||||
sealed class UserVote {
|
||||
|
||||
class Direct(val vote: AccountVote) : UserVote()
|
||||
|
||||
class Delegated(val delegate: AccountId, val vote: AccountVote) : UserVote()
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.repository
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.AccountVote
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumId
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumVoter
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackId
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.VoteType
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.Voting
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.delegations
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.offchain.model.referendum.OffChainReferendumVotingDetails
|
||||
import io.novafoundation.nova.feature_governance_api.domain.locks.ClaimSchedule
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceLockId
|
||||
import io.novafoundation.nova.runtime.extrinsic.multi.CallBuilder
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.math.BigInteger
|
||||
|
||||
interface ConvictionVotingRepository {
|
||||
|
||||
val voteLockId: BalanceLockId
|
||||
|
||||
suspend fun maxAvailableForVote(asset: Asset): Balance
|
||||
|
||||
suspend fun voteLockingPeriod(chainId: ChainId): BlockNumber
|
||||
|
||||
suspend fun maxTrackVotes(chainId: ChainId): BigInteger
|
||||
|
||||
fun trackLocksFlow(accountId: AccountId, chainAssetId: FullChainAssetId): Flow<Map<TrackId, Balance>>
|
||||
|
||||
suspend fun votingFor(accountId: AccountId, chainId: ChainId): Map<TrackId, Voting>
|
||||
|
||||
suspend fun votingFor(accountId: AccountId, chainId: ChainId, trackId: TrackId): Voting?
|
||||
|
||||
suspend fun votingFor(accountId: AccountId, chainId: ChainId, trackIds: Collection<TrackId>): Map<TrackId, Voting>
|
||||
|
||||
suspend fun votersOf(referendumId: ReferendumId, chain: Chain, type: VoteType): List<ReferendumVoter>
|
||||
|
||||
suspend fun delegatingFor(accountId: AccountId, chainId: ChainId): Map<TrackId, Voting.Delegating> {
|
||||
return votingFor(accountId, chainId).delegations()
|
||||
}
|
||||
|
||||
fun ExtrinsicBuilder.unlock(accountId: AccountId, claimable: ClaimSchedule.UnlockChunk.Claimable)
|
||||
|
||||
fun ExtrinsicBuilder.vote(referendumId: ReferendumId, vote: AccountVote)
|
||||
|
||||
fun CallBuilder.vote(referendumId: ReferendumId, vote: AccountVote)
|
||||
fun ExtrinsicBuilder.removeVote(trackId: TrackId, referendumId: ReferendumId)
|
||||
|
||||
fun isAbstainVotingAvailable(): Boolean
|
||||
|
||||
suspend fun abstainVotingDetails(referendumId: ReferendumId, chain: Chain): OffChainReferendumVotingDetails?
|
||||
|
||||
suspend fun fullVotingDetails(referendumId: ReferendumId, chain: Chain): OffChainReferendumVotingDetails?
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.repository
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.Delegation
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumId
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackId
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.offchain.model.delegation.DelegateDetailedStats
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.offchain.model.delegation.DelegateMetadata
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.offchain.model.delegation.DelegateStats
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.offchain.model.vote.UserVote
|
||||
import io.novafoundation.nova.feature_governance_api.data.repository.common.RecentVotesDateThreshold
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.runtime.extrinsic.multi.CallBuilder
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.types.custom.vote.Conviction
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
|
||||
interface DelegationsRepository {
|
||||
|
||||
suspend fun isDelegationSupported(chain: Chain): Boolean
|
||||
|
||||
suspend fun getDelegatesStats(
|
||||
recentVotesDateThreshold: RecentVotesDateThreshold,
|
||||
chain: Chain
|
||||
): List<DelegateStats>
|
||||
|
||||
suspend fun getDelegatesStatsByAccountIds(
|
||||
recentVotesDateThreshold: RecentVotesDateThreshold,
|
||||
accountIds: List<AccountId>,
|
||||
chain: Chain
|
||||
): List<DelegateStats>
|
||||
|
||||
suspend fun getDetailedDelegateStats(
|
||||
delegateAddress: String,
|
||||
recentVotesDateThreshold: RecentVotesDateThreshold,
|
||||
chain: Chain,
|
||||
): DelegateDetailedStats?
|
||||
|
||||
suspend fun getDelegatesMetadata(chain: Chain): List<DelegateMetadata>
|
||||
|
||||
suspend fun getDelegateMetadata(chain: Chain, delegate: AccountId): DelegateMetadata?
|
||||
|
||||
suspend fun getDelegationsTo(delegate: AccountId, chain: Chain): List<Delegation>
|
||||
|
||||
suspend fun allHistoricalVotesOf(user: AccountId, chain: Chain): Map<ReferendumId, UserVote>?
|
||||
|
||||
suspend fun historicalVoteOf(user: AccountId, referendumId: ReferendumId, chain: Chain): UserVote?
|
||||
|
||||
suspend fun directHistoricalVotesOf(
|
||||
user: AccountId,
|
||||
chain: Chain,
|
||||
recentVotesDateThreshold: RecentVotesDateThreshold?
|
||||
): Map<ReferendumId, UserVote.Direct>?
|
||||
|
||||
suspend fun CallBuilder.delegate(
|
||||
delegate: AccountId,
|
||||
trackId: TrackId,
|
||||
amount: Balance,
|
||||
conviction: Conviction
|
||||
)
|
||||
|
||||
suspend fun CallBuilder.undelegate(trackId: TrackId)
|
||||
}
|
||||
|
||||
suspend fun DelegationsRepository.getDelegatesMetadataOrEmpty(chain: Chain): List<DelegateMetadata> {
|
||||
return runCatching { getDelegatesMetadata(chain) }
|
||||
.onFailure { Log.e(LOG_TAG, "Failed to fetch delegate metadatas", it) }
|
||||
.getOrDefault(emptyList())
|
||||
}
|
||||
|
||||
suspend fun DelegationsRepository.getDelegateMetadataOrNull(chain: Chain, delegate: AccountId): DelegateMetadata? {
|
||||
return runCatching { getDelegateMetadata(chain, delegate) }
|
||||
.onFailure { Log.e(LOG_TAG, "Failed to fetch delegate metadata", it) }
|
||||
.getOrNull()
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.repository
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumId
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.details.ReferendumDApp
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface GovernanceDAppsRepository {
|
||||
|
||||
fun observeReferendumDApps(chainId: ChainId, referendumId: ReferendumId): Flow<List<ReferendumDApp>>
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.repository
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumId
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.offchain.model.referendum.OffChainReferendumDetails
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.offchain.model.referendum.OffChainReferendumPreview
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
interface OffChainReferendaInfoRepository {
|
||||
|
||||
suspend fun referendumPreviews(chain: Chain): List<OffChainReferendumPreview>
|
||||
|
||||
suspend fun referendumDetails(referendumId: ReferendumId, chain: Chain): OffChainReferendumDetails?
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.repository
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.OnChainReferendum
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumId
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackId
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackInfo
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackQueue
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface OnChainReferendaRepository {
|
||||
|
||||
suspend fun electorate(chainId: ChainId): Balance
|
||||
|
||||
suspend fun undecidingTimeout(chainId: ChainId): BlockNumber
|
||||
|
||||
suspend fun getTracks(chainId: ChainId): Collection<TrackInfo>
|
||||
|
||||
suspend fun getTrackQueues(trackIds: Set<TrackId>, chainId: ChainId): Map<TrackId, TrackQueue>
|
||||
|
||||
suspend fun getAllOnChainReferenda(chainId: ChainId): Collection<OnChainReferendum>
|
||||
|
||||
suspend fun getOnChainReferenda(chainId: ChainId, referendaIds: Collection<ReferendumId>): Map<ReferendumId, OnChainReferendum>
|
||||
|
||||
suspend fun onChainReferendumFlow(chainId: ChainId, referendumId: ReferendumId): Flow<OnChainReferendum?>
|
||||
|
||||
suspend fun getReferendaExecutionBlocks(chainId: ChainId, approvedReferendaIds: Collection<ReferendumId>): Map<ReferendumId, BlockNumber>
|
||||
}
|
||||
|
||||
suspend fun OnChainReferendaRepository.getTracksById(chainId: ChainId): Map<TrackId, TrackInfo> {
|
||||
return getTracks(chainId).associateBy(TrackInfo::id)
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.repository
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.PreImage
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.Proposal
|
||||
import io.novafoundation.nova.feature_governance_api.data.repository.PreImageRequest.FetchCondition.ALWAYS
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novasama.substrate_sdk_android.extensions.toHexString
|
||||
import java.math.BigInteger
|
||||
|
||||
typealias HexHash = String
|
||||
|
||||
class PreImageRequest(
|
||||
val hash: ByteArray,
|
||||
val knownSize: BigInteger?,
|
||||
val fetchIf: FetchCondition,
|
||||
) {
|
||||
|
||||
val hashHex: HexHash = hash.toHexString()
|
||||
|
||||
enum class FetchCondition {
|
||||
ALWAYS, SMALL_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
interface PreImageRepository {
|
||||
|
||||
suspend fun getPreimageFor(request: PreImageRequest, chainId: ChainId): PreImage?
|
||||
|
||||
suspend fun getPreimagesFor(requests: Collection<PreImageRequest>, chainId: ChainId): Map<HexHash, PreImage?>
|
||||
}
|
||||
|
||||
suspend fun PreImageRepository.preImageOf(
|
||||
proposal: Proposal?,
|
||||
chainId: ChainId,
|
||||
): PreImage? {
|
||||
return when (proposal) {
|
||||
is Proposal.Inline -> {
|
||||
PreImage(encodedCall = proposal.encodedCall, call = proposal.call)
|
||||
}
|
||||
|
||||
is Proposal.Legacy -> {
|
||||
val request = PreImageRequest(proposal.hash, knownSize = null, fetchIf = ALWAYS)
|
||||
getPreimageFor(request, chainId)
|
||||
}
|
||||
|
||||
is Proposal.Lookup -> {
|
||||
val request = PreImageRequest(proposal.hash, knownSize = proposal.callLength, fetchIf = ALWAYS)
|
||||
getPreimageFor(request, chainId)
|
||||
}
|
||||
|
||||
null -> null
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.repository
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TreasuryProposal
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
|
||||
interface TreasuryRepository {
|
||||
|
||||
suspend fun getTreasuryProposal(chainId: ChainId, id: TreasuryProposal.Id): TreasuryProposal?
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.repository.common
|
||||
|
||||
import java.math.BigInteger
|
||||
|
||||
sealed interface RecentVotesDateThreshold {
|
||||
|
||||
companion object;
|
||||
|
||||
class BlockNumber(val number: BigInteger) : RecentVotesDateThreshold
|
||||
class Timestamp(val timestampMs: Long) : RecentVotesDateThreshold
|
||||
}
|
||||
|
||||
fun RecentVotesDateThreshold.Companion.zeroPoint() = RecentVotesDateThreshold.BlockNumber(BigInteger.ZERO)
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.source
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackId
|
||||
import io.novafoundation.nova.feature_governance_api.data.repository.ConvictionVotingRepository
|
||||
import io.novafoundation.nova.feature_governance_api.data.repository.DelegationsRepository
|
||||
import io.novafoundation.nova.feature_governance_api.data.repository.GovernanceDAppsRepository
|
||||
import io.novafoundation.nova.feature_governance_api.data.repository.OffChainReferendaInfoRepository
|
||||
import io.novafoundation.nova.feature_governance_api.data.repository.OnChainReferendaRepository
|
||||
import io.novafoundation.nova.feature_governance_api.data.repository.PreImageRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
interface GovernanceSource {
|
||||
|
||||
val referenda: OnChainReferendaRepository
|
||||
|
||||
val convictionVoting: ConvictionVotingRepository
|
||||
|
||||
val offChainInfo: OffChainReferendaInfoRepository
|
||||
|
||||
val dappsRepository: GovernanceDAppsRepository
|
||||
|
||||
val preImageRepository: PreImageRepository
|
||||
|
||||
val delegationsRepository: DelegationsRepository
|
||||
}
|
||||
|
||||
fun ConvictionVotingRepository.trackLocksFlowOrEmpty(voterAccountId: AccountId?, chainAssetId: FullChainAssetId): Flow<Map<TrackId, Balance>> {
|
||||
return if (voterAccountId != null) {
|
||||
trackLocksFlow(voterAccountId, chainAssetId)
|
||||
} else {
|
||||
flowOf(emptyMap())
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.source
|
||||
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.state.SelectableAssetAdditionalData
|
||||
import io.novafoundation.nova.runtime.state.SelectedAssetOptionSharedState
|
||||
|
||||
interface GovernanceSourceRegistry {
|
||||
|
||||
suspend fun sourceFor(option: SupportedGovernanceOption): GovernanceSource
|
||||
|
||||
suspend fun sourceFor(option: Chain.Governance): GovernanceSource
|
||||
}
|
||||
|
||||
typealias SupportedGovernanceOption = SelectedAssetOptionSharedState.SupportedAssetOption<GovernanceAdditionalState>
|
||||
|
||||
interface GovernanceAdditionalState : SelectableAssetAdditionalData {
|
||||
|
||||
val governanceType: Chain.Governance
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.thresold.gov1
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.Perbill
|
||||
import io.novafoundation.nova.common.utils.divideToDecimal
|
||||
import io.novafoundation.nova.common.utils.intSqrt
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.Tally
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.VotingThreshold
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.VotingThreshold.Threshold
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ayeVotes
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.notPassing
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.simple
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.passing
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import java.math.BigInteger
|
||||
|
||||
enum class Gov1VotingThreshold(val readableName: String) : VotingThreshold {
|
||||
|
||||
SUPER_MAJORITY_APPROVE("SimpleMajorityApprove") {
|
||||
|
||||
// https://github.com/paritytech/substrate/blob/5ae005c244295d23586c93da43148b2bc826b137/frame/democracy/src/vote_threshold.rs#L101
|
||||
// nays / sqrt(turnout) < ayes / sqrt(total_issuance) =>
|
||||
// ayes > nays * sqrt(total_issuance) / sqrt(turnout) =>
|
||||
// ayes / (ayes + nays) > [nays / (ayes + nays)] * [sqrt(total_issuance) / sqrt(turnout)]
|
||||
// let a = ayes / (ayes + nays), to = sqrt(total_issuance), tu = sqrt(turnout)
|
||||
// a > (1 - a) * to / tu
|
||||
// a > to / tu - a * to / tu => a > (to / tu) / (1 + to / tu)
|
||||
// a > to / (tu + to)
|
||||
override fun ayesFractionThreshold(tally: Tally, totalIssuance: Balance, passedSinceDecidingFraction: Perbill): Threshold<Perbill> {
|
||||
if (totalIssuance == Balance.ZERO || tally.support == Balance.ZERO) return Threshold.notPassing(Perbill.ONE)
|
||||
|
||||
val sqrtTurnout = tally.support.intSqrt()
|
||||
val sqrtTotalIssuance = totalIssuance.intSqrt()
|
||||
|
||||
val aysFraction = tally.ayeVotes().fraction
|
||||
|
||||
val threshold = sqrtTotalIssuance.divideToDecimal(sqrtTurnout + sqrtTotalIssuance)
|
||||
|
||||
return Threshold.simple(
|
||||
value = threshold,
|
||||
currentlyPassing = aysFraction > threshold
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
SUPER_MAJORITY_AGAINST("SimpleMajority") {
|
||||
|
||||
// https://github.com/paritytech/substrate/blob/5ae005c244295d23586c93da43148b2bc826b137/frame/democracy/src/vote_threshold.rs#L103
|
||||
// nays / sqrt(total_issuance) < ayes / sqrt(turnout) =>
|
||||
// ayes > nays * sqrt(turnout) / sqrt(total_issuance) =>
|
||||
// ayes / (ayes + nays) > [nays / (ayes + nays)] * [sqrt(turnout) / sqrt(total_issuance)]
|
||||
// let a = ayes / (ayes + nays), to = sqrt(total_issuance), tu = sqrt(turnout)
|
||||
// a > tu / (tu + to)
|
||||
override fun ayesFractionThreshold(tally: Tally, totalIssuance: Balance, passedSinceDecidingFraction: Perbill): Threshold<Perbill> {
|
||||
if (totalIssuance == Balance.ZERO || tally.support == Balance.ZERO) return Threshold.notPassing(Perbill.ONE)
|
||||
|
||||
val sqrtTurnout = tally.support.intSqrt()
|
||||
val sqrtTotalIssuance = totalIssuance.intSqrt()
|
||||
|
||||
val aysFraction = tally.ayeVotes().fraction
|
||||
|
||||
val threshold = sqrtTurnout.divideToDecimal(sqrtTurnout + sqrtTotalIssuance)
|
||||
|
||||
return Threshold.simple(
|
||||
value = threshold,
|
||||
currentlyPassing = aysFraction > threshold
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
SIMPLE_MAJORITY("SimpleMajority") {
|
||||
|
||||
// https://github.com/paritytech/substrate/blob/5ae005c244295d23586c93da43148b2bc826b137/frame/democracy/src/vote_threshold.rs#L105
|
||||
// ayes > nays =>
|
||||
// ayes / (ayes + nays) > 0.5
|
||||
override fun ayesFractionThreshold(tally: Tally, totalIssuance: Balance, passedSinceDecidingFraction: Perbill): Threshold<Perbill> {
|
||||
val threshold = 0.5.toBigDecimal()
|
||||
|
||||
val aysFraction = tally.ayeVotes().fraction
|
||||
|
||||
return Threshold.simple(
|
||||
value = threshold,
|
||||
currentlyPassing = aysFraction > threshold
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
override fun supportThreshold(tally: Tally, totalIssuance: Balance, passedSinceDecidingFraction: Perbill): Threshold<Balance> {
|
||||
return Threshold.passing(BigInteger.ZERO)
|
||||
}
|
||||
}
|
||||
|
||||
fun VotingThreshold.asGovV1VotingThresholdOrNull(): Gov1VotingThreshold? {
|
||||
return this as? Gov1VotingThreshold
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.thresold.gov2
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.Perbill
|
||||
import io.novafoundation.nova.common.utils.divideOrNull
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.Tally
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackInfo
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.VotingCurve
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.VotingThreshold
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.VotingThreshold.Threshold
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ayeVotes
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import java.math.MathContext
|
||||
|
||||
class Gov2VotingThreshold(
|
||||
val supportCurve: VotingCurve,
|
||||
val approvalCurve: VotingCurve
|
||||
) : VotingThreshold {
|
||||
|
||||
override fun supportThreshold(tally: Tally, totalIssuance: Balance, passedSinceDecidingFraction: Perbill): Threshold<Balance> {
|
||||
val supportNeeded = supportCurve.threshold(passedSinceDecidingFraction) * totalIssuance.toBigDecimal()
|
||||
val supportNeededIntegral = supportNeeded.toBigInteger()
|
||||
|
||||
val currentSupport = tally.support.toBigDecimal()
|
||||
val totalSupport = totalIssuance.toBigDecimal()
|
||||
val supportFraction = currentSupport.divideOrNull(totalSupport, MathContext.DECIMAL64) ?: Perbill.ZERO
|
||||
|
||||
return Threshold(
|
||||
value = supportNeededIntegral,
|
||||
currentlyPassing = tally.support >= supportNeededIntegral,
|
||||
getProjectedPassing(supportCurve, supportFraction)
|
||||
)
|
||||
}
|
||||
|
||||
override fun ayesFractionThreshold(tally: Tally, totalIssuance: Balance, passedSinceDecidingFraction: Perbill): Threshold<Perbill> {
|
||||
val approvalThreshold = approvalCurve.threshold(passedSinceDecidingFraction)
|
||||
val ayeFraction = tally.ayeVotes().fraction
|
||||
|
||||
return Threshold(
|
||||
value = approvalThreshold,
|
||||
currentlyPassing = ayeFraction >= approvalThreshold,
|
||||
getProjectedPassing(approvalCurve, ayeFraction)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getProjectedPassing(curve: VotingCurve, fraction: Perbill): VotingThreshold.ProjectedPassing {
|
||||
val delay = curve.delay(fraction)
|
||||
val threshold = curve.threshold(delay)
|
||||
|
||||
return VotingThreshold.ProjectedPassing(
|
||||
delayFraction = delay,
|
||||
passingInFuture = fraction >= threshold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun Gov2VotingThreshold(trackInfo: TrackInfo): Gov2VotingThreshold {
|
||||
return Gov2VotingThreshold(
|
||||
supportCurve = trackInfo.minSupport!!,
|
||||
approvalCurve = trackInfo.minApproval!!
|
||||
)
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.thresold.gov2.curve
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.Perbill
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.VotingCurve
|
||||
import java.math.RoundingMode
|
||||
|
||||
/**
|
||||
* Linear curve starting at `(0, ceil)`, proceeding linearly to `(length, floor)`, then
|
||||
* remaining at `floor` until the end of the period.
|
||||
*
|
||||
* @see <a href="https://github.com/paritytech/substrate/blob/37664fe5b3513eb996225f016eceaf74963b8133/frame/referenda/src/types.rs#L264">Source</a>
|
||||
*/
|
||||
class LinearDecreasingCurve(
|
||||
private val length: Perbill,
|
||||
private val floor: Perbill,
|
||||
private val ceil: Perbill
|
||||
) : VotingCurve {
|
||||
|
||||
override val name: String = "LinearDecreasing"
|
||||
|
||||
override fun threshold(x: Perbill): Perbill {
|
||||
return ceil - x.coerceAtMost(length).divide(length, RoundingMode.DOWN) * (ceil - floor)
|
||||
}
|
||||
|
||||
override fun delay(y: Perbill): Perbill {
|
||||
return when {
|
||||
y < floor -> Perbill.ONE
|
||||
y > ceil -> Perbill.ZERO
|
||||
else -> (ceil - y).divide(ceil - floor, RoundingMode.UP).multiply(length)
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.thresold.gov2.curve
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.FixedI64
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.Perbill
|
||||
import io.novafoundation.nova.common.utils.coerceInOrNull
|
||||
import io.novafoundation.nova.common.utils.divideOrNull
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.VotingCurve
|
||||
import java.math.BigDecimal
|
||||
import java.math.MathContext
|
||||
import java.math.RoundingMode
|
||||
|
||||
/**
|
||||
* A reciprocal (`K/(x+S)-T`) curve: `factor` is `K` and `xOffset` is `S`, `yOffset` is `T`.
|
||||
*
|
||||
* @see <a href="https://github.com/paritytech/substrate/blob/37664fe5b3513eb996225f016eceaf74963b8133/frame/referenda/src/types.rs#L271">Source</a>
|
||||
*/
|
||||
class ReciprocalCurve(
|
||||
private val factor: FixedI64,
|
||||
private val xOffset: FixedI64,
|
||||
private val yOffset: FixedI64
|
||||
) : VotingCurve {
|
||||
|
||||
override val name: String = "Reciprocal"
|
||||
|
||||
override fun threshold(x: Perbill): Perbill {
|
||||
return factor.divide(x + xOffset, MathContext(MathContext.DECIMAL64.precision, RoundingMode.DOWN)) + yOffset
|
||||
}
|
||||
|
||||
override fun delay(y: Perbill): Perbill {
|
||||
val maybeTerm = factor.divideOrNull(y - yOffset, MathContext(MathContext.DECIMAL64.precision, RoundingMode.UP))
|
||||
|
||||
return maybeTerm?.let { it - xOffset }
|
||||
?.coerceInOrNull(BigDecimal.ZERO, BigDecimal.ONE)
|
||||
?: Perbill.ONE
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package io.novafoundation.nova.feature_governance_api.data.thresold.gov2.curve
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.Perbill
|
||||
import io.novafoundation.nova.common.utils.lessEpsilon
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.VotingCurve
|
||||
|
||||
/**
|
||||
* Stepped curve, beginning at `(0, begin)`, then remaining constant for `period`, at which
|
||||
* point it steps down to `(period, begin - step)`. It then remains constant for another
|
||||
* `period` before stepping down to `(period * 2, begin - step * 2)`. This pattern continues
|
||||
* but the `y` component has a lower limit of `end`.
|
||||
*
|
||||
* @see <a href="https://github.com/paritytech/substrate/blob/37664fe5b3513eb996225f016eceaf74963b8133/frame/referenda/src/types.rs#L269">Source</a>
|
||||
*/
|
||||
class SteppedDecreasingCurve(
|
||||
private val begin: Perbill,
|
||||
private val end: Perbill,
|
||||
private val step: Perbill,
|
||||
private val period: Perbill
|
||||
) : VotingCurve {
|
||||
|
||||
override val name: String = "SteppedDecreasing"
|
||||
|
||||
override fun threshold(x: Perbill): Perbill {
|
||||
val passedPeriods = x.divideToIntegralValue(period)
|
||||
val decrease = passedPeriods * step
|
||||
|
||||
return (begin - decrease).coerceIn(end, begin)
|
||||
}
|
||||
|
||||
override fun delay(y: Perbill): Perbill {
|
||||
return when {
|
||||
y < end -> Perbill.ONE
|
||||
else -> period.multiply((begin - y.coerceAtMost(begin) + step.lessEpsilon()).divideToIntegralValue(step))
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package io.novafoundation.nova.feature_governance_api.di
|
||||
|
||||
import io.novafoundation.nova.core.updater.UpdateSystem
|
||||
import io.novafoundation.nova.feature_governance_api.data.MutableGovernanceState
|
||||
import io.novafoundation.nova.feature_governance_api.data.source.GovernanceSourceRegistry
|
||||
import io.novafoundation.nova.feature_governance_api.di.deeplinks.GovernanceDeepLinks
|
||||
import io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.delegators.DelegateDelegatorsInteractor
|
||||
import io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.details.model.DelegateDetailsInteractor
|
||||
import io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.list.DelegateListInteractor
|
||||
import io.novafoundation.nova.feature_governance_api.domain.delegation.delegation.common.chooseTrack.ChooseTrackInteractor
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.details.ReferendumDetailsInteractor
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.list.ReferendaListInteractor
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.voters.ReferendumVotersInteractor
|
||||
import io.novafoundation.nova.feature_governance_api.presentation.referenda.common.ReferendaStatusFormatter
|
||||
import io.novafoundation.nova.feature_governance_api.presentation.referenda.details.deeplink.configurators.ReferendumDetailsDeepLinkConfigurator
|
||||
|
||||
interface GovernanceFeatureApi {
|
||||
|
||||
val governanceSourceRegistry: GovernanceSourceRegistry
|
||||
|
||||
val referendaListInteractor: ReferendaListInteractor
|
||||
|
||||
val referendumDetailsInteractor: ReferendumDetailsInteractor
|
||||
|
||||
val referendumVotersInteractor: ReferendumVotersInteractor
|
||||
|
||||
val governanceUpdateSystem: UpdateSystem
|
||||
|
||||
val delegateListInteractor: DelegateListInteractor
|
||||
|
||||
val delegateDetailsInteractor: DelegateDetailsInteractor
|
||||
|
||||
val newDelegationChooseTrackInteractor: ChooseTrackInteractor
|
||||
|
||||
val delegateDelegatorsInteractor: DelegateDelegatorsInteractor
|
||||
|
||||
val mutableGovernanceState: MutableGovernanceState
|
||||
|
||||
val referendaStatusFormatter: ReferendaStatusFormatter
|
||||
|
||||
val governanceDeepLinks: GovernanceDeepLinks
|
||||
|
||||
val referendumDetailsDeepLinkConfigurator: ReferendumDetailsDeepLinkConfigurator
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package io.novafoundation.nova.feature_governance_api.di.deeplinks
|
||||
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.handling.DeepLinkHandler
|
||||
|
||||
class GovernanceDeepLinks(val deepLinkHandlers: List<DeepLinkHandler>)
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.delegation.delegate
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.data.model.OnChainIdentity
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
|
||||
interface Delegate {
|
||||
|
||||
val accountId: AccountId
|
||||
|
||||
val metadata: Metadata?
|
||||
|
||||
val onChainIdentity: OnChainIdentity?
|
||||
|
||||
interface Metadata {
|
||||
|
||||
val name: String?
|
||||
|
||||
val iconUrl: String?
|
||||
|
||||
val accountType: DelegateAccountType
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.delegation.delegate
|
||||
|
||||
enum class DelegateAccountType {
|
||||
INDIVIDUAL, ORGANIZATION
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.delegators
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.delegators.model.Delegator
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface DelegateDelegatorsInteractor {
|
||||
|
||||
fun delegatorsFlow(delegateId: AccountId): Flow<List<Delegator>>
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.delegators.model
|
||||
|
||||
import io.novafoundation.nova.common.utils.sumByBigDecimal
|
||||
import io.novafoundation.nova.feature_account_api.domain.account.identity.Identity
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.Delegation
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.Voting
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.getConvictionVote
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.voters.GenericVoter
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.voters.GenericVoter.ConvictionVote
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import java.math.BigDecimal
|
||||
|
||||
class Delegator(
|
||||
override val accountId: AccountId,
|
||||
override val identity: Identity?,
|
||||
override val vote: Vote
|
||||
) : GenericVoter<Delegator.Vote> {
|
||||
|
||||
sealed class Vote(override val totalVotes: BigDecimal) : GenericVoter.Vote {
|
||||
|
||||
class SingleTrack(val delegation: ConvictionVote) : Vote(delegation.totalVotes)
|
||||
|
||||
class MultiTrack(val trackCount: Int, totalVotes: BigDecimal) : Vote(totalVotes)
|
||||
}
|
||||
}
|
||||
|
||||
fun Delegator(
|
||||
accountId: AccountId,
|
||||
identity: Identity?,
|
||||
delegatorTrackDelegations: List<Delegation.Vote>,
|
||||
chainAsset: Chain.Asset,
|
||||
): Delegator {
|
||||
val vote = requireNotNull(DelegatorVote(delegatorTrackDelegations, chainAsset))
|
||||
|
||||
return Delegator(accountId, identity, vote)
|
||||
}
|
||||
|
||||
fun DelegatorVote(delegatorTrackDelegations: List<Delegation.Vote>, chainAsset: Chain.Asset): Delegator.Vote? {
|
||||
val simpleVotes = delegatorTrackDelegations.map {
|
||||
ConvictionVote(chainAsset.amountFromPlanks(it.amount), it.conviction)
|
||||
}
|
||||
|
||||
return DelegatorVote(simpleVotes)
|
||||
}
|
||||
|
||||
@JvmName("DelegatorVoteFromDelegating")
|
||||
fun DelegatorVote(delegations: Collection<Voting.Delegating>, chainAsset: Chain.Asset): Delegator.Vote? {
|
||||
val simpleVotes = delegations.map { it.getConvictionVote(chainAsset) }
|
||||
|
||||
return DelegatorVote(simpleVotes)
|
||||
}
|
||||
|
||||
@JvmName("DelegatorVoteFromConvictionVote")
|
||||
fun DelegatorVote(votes: Collection<ConvictionVote>): Delegator.Vote? {
|
||||
return when (votes.size) {
|
||||
0 -> null
|
||||
1 -> Delegator.Vote.SingleTrack(votes.single())
|
||||
else -> {
|
||||
val totalVotes = votes.sumByBigDecimal(ConvictionVote::totalVotes)
|
||||
Delegator.Vote.MultiTrack(trackCount = votes.size, totalVotes = totalVotes)
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.details.model
|
||||
|
||||
import io.novafoundation.nova.common.validation.ValidationSystem
|
||||
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
|
||||
|
||||
typealias AddDelegationValidationSystem = ValidationSystem<AddDelegationValidationPayload, AddDelegationValidationFailure>
|
||||
|
||||
sealed interface AddDelegationValidationFailure {
|
||||
class NoChainAccountFailure(
|
||||
override val chain: Chain,
|
||||
override val account: MetaAccount,
|
||||
override val addAccountState: NoChainAccountFoundError.AddAccountState
|
||||
) : AddDelegationValidationFailure, NoChainAccountFoundError
|
||||
}
|
||||
|
||||
data class AddDelegationValidationPayload(
|
||||
val chain: Chain,
|
||||
val metaAccount: MetaAccount
|
||||
)
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.details.model
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.data.model.OnChainIdentity
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.Voting
|
||||
import io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.Delegate
|
||||
import io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.DelegateAccountType
|
||||
import io.novafoundation.nova.feature_governance_api.domain.track.Track
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
|
||||
data class DelegateDetails(
|
||||
override val accountId: AccountId,
|
||||
val stats: Stats?,
|
||||
override val metadata: Metadata?,
|
||||
override val onChainIdentity: OnChainIdentity?,
|
||||
val userDelegations: Map<Track, Voting.Delegating>
|
||||
) : Delegate {
|
||||
|
||||
data class Metadata(
|
||||
val shortDescription: String,
|
||||
val longDescription: String?,
|
||||
override val iconUrl: String?,
|
||||
override val accountType: DelegateAccountType,
|
||||
override val name: String?
|
||||
) : Delegate.Metadata
|
||||
|
||||
data class Stats(val delegationsCount: Int, val delegatedVotes: Balance, val recentVotes: Int, val allVotes: Int)
|
||||
}
|
||||
|
||||
val DelegateDetails.Metadata.description: String
|
||||
get() = longDescription ?: shortDescription
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.details.model
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface DelegateDetailsInteractor {
|
||||
|
||||
fun delegateDetailsFlow(
|
||||
delegateAccountId: AccountId,
|
||||
): Flow<DelegateDetails>
|
||||
|
||||
fun validationSystemFor(): AddDelegationValidationSystem
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.label
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.data.model.OnChainIdentity
|
||||
import io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.Delegate
|
||||
import io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.DelegateAccountType
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
|
||||
class DelegateLabel(
|
||||
override val accountId: AccountId,
|
||||
override val metadata: Delegate.Metadata?,
|
||||
override val onChainIdentity: OnChainIdentity?
|
||||
) : Delegate {
|
||||
|
||||
class Metadata(
|
||||
override val name: String?,
|
||||
override val iconUrl: String?,
|
||||
override val accountType: DelegateAccountType
|
||||
) : Delegate.Metadata
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.label
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
|
||||
interface DelegateLabelUseCase {
|
||||
|
||||
suspend fun getDelegateLabel(delegate: AccountId): DelegateLabel
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.list
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.data.source.SupportedGovernanceOption
|
||||
import io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.list.model.DelegateFiltering
|
||||
import io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.list.model.DelegatePreview
|
||||
import io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.list.model.DelegateSorting
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface DelegateListInteractor {
|
||||
|
||||
fun shouldShowDelegationBanner(): Flow<Boolean>
|
||||
|
||||
suspend fun hideDelegationBanner()
|
||||
|
||||
suspend fun getDelegates(
|
||||
governanceOption: SupportedGovernanceOption,
|
||||
scope: CoroutineScope
|
||||
): Flow<List<DelegatePreview>>
|
||||
|
||||
suspend fun getUserDelegates(
|
||||
governanceOption: SupportedGovernanceOption,
|
||||
scope: CoroutineScope
|
||||
): Flow<List<DelegatePreview>>
|
||||
|
||||
suspend fun applySortingAndFiltering(
|
||||
sorting: DelegateSorting,
|
||||
filtering: DelegateFiltering,
|
||||
delegates: List<DelegatePreview>
|
||||
): List<DelegatePreview>
|
||||
|
||||
suspend fun applySorting(
|
||||
sorting: DelegateSorting,
|
||||
delegates: List<DelegatePreview>
|
||||
): List<DelegatePreview>
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.list.model
|
||||
|
||||
import io.novafoundation.nova.common.utils.Filter
|
||||
import io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.Delegate
|
||||
import io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.DelegateAccountType
|
||||
|
||||
enum class DelegateFiltering : Filter<Delegate> {
|
||||
|
||||
ALL_ACCOUNTS {
|
||||
override fun shouldInclude(model: Delegate): Boolean {
|
||||
return true
|
||||
}
|
||||
},
|
||||
|
||||
ORGANIZATIONS {
|
||||
override fun shouldInclude(model: Delegate): Boolean {
|
||||
val accountType = model.metadata?.accountType ?: return false
|
||||
|
||||
return accountType == DelegateAccountType.ORGANIZATION
|
||||
}
|
||||
},
|
||||
|
||||
INDIVIDUALS {
|
||||
override fun shouldInclude(model: Delegate): Boolean {
|
||||
val accountType = model.metadata?.accountType ?: return false
|
||||
|
||||
return accountType == DelegateAccountType.INDIVIDUAL
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.list.model
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.data.model.OnChainIdentity
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.Voting
|
||||
import io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.Delegate
|
||||
import io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.DelegateAccountType
|
||||
import io.novafoundation.nova.feature_governance_api.domain.track.Track
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
|
||||
data class DelegatePreview(
|
||||
override val accountId: AccountId,
|
||||
val stats: Stats?,
|
||||
override val metadata: Metadata?,
|
||||
override val onChainIdentity: OnChainIdentity?,
|
||||
val userDelegations: Map<Track, Voting.Delegating>
|
||||
) : Delegate {
|
||||
|
||||
data class Metadata(
|
||||
val shortDescription: String,
|
||||
override val iconUrl: String?,
|
||||
override val accountType: DelegateAccountType,
|
||||
override val name: String?
|
||||
) : Delegate.Metadata
|
||||
|
||||
data class Stats(val delegationsCount: Int, val delegatedVotes: Balance, val recentVotes: Int)
|
||||
}
|
||||
|
||||
fun DelegatePreview.hasMetadata(): Boolean {
|
||||
return metadata != null
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.list.model
|
||||
|
||||
enum class DelegateSorting {
|
||||
DELEGATIONS, DELEGATED_VOTES, VOTING_ACTIVITY
|
||||
}
|
||||
|
||||
fun DelegateSorting.delegateComparator(): Comparator<DelegatePreview> {
|
||||
return when (this) {
|
||||
DelegateSorting.DELEGATIONS -> compareByDescending { it.stats?.delegationsCount }
|
||||
DelegateSorting.DELEGATED_VOTES -> compareByDescending { it.stats?.delegatedVotes }
|
||||
DelegateSorting.VOTING_ACTIVITY -> compareByDescending { it.stats?.recentVotes }
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.search
|
||||
|
||||
import io.novafoundation.nova.common.domain.ExtendedLoadingState
|
||||
import io.novafoundation.nova.feature_governance_api.data.source.GovernanceAdditionalState
|
||||
import io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.list.model.DelegatePreview
|
||||
import io.novafoundation.nova.runtime.state.SelectedAssetOptionSharedState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class DelegateSearchResult(
|
||||
val delegates: List<DelegatePreview>,
|
||||
val query: String
|
||||
)
|
||||
|
||||
interface DelegateSearchInteractor {
|
||||
|
||||
suspend fun searchDelegates(
|
||||
queryFlow: Flow<String>,
|
||||
selectedOption: SelectedAssetOptionSharedState.SupportedAssetOption<GovernanceAdditionalState>,
|
||||
scope: CoroutineScope
|
||||
): Flow<ExtendedLoadingState<List<DelegatePreview>>>
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.delegation.delegation.common.chooseTrack
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.domain.delegation.delegation.common.chooseTrack.model.ChooseTrackData
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ChooseTrackInteractor {
|
||||
|
||||
suspend fun isAllowedToShowRemoveVotesSuggestion(): Boolean
|
||||
|
||||
suspend fun disallowShowRemoveVotesSuggestion()
|
||||
|
||||
fun observeTracksByChain(chainId: ChainId, govType: Chain.Governance): Flow<ChooseTrackData>
|
||||
|
||||
fun observeNewDelegationTrackData(): Flow<ChooseTrackData>
|
||||
|
||||
fun observeEditDelegationTrackData(delegateId: AccountId): Flow<ChooseTrackData>
|
||||
|
||||
fun observeRevokeDelegationTrackData(delegateId: AccountId): Flow<ChooseTrackData>
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.delegation.delegation.common.chooseTrack.model
|
||||
|
||||
data class ChooseTrackData(
|
||||
val trackPartition: TrackPartition,
|
||||
val presets: List<TrackPreset>
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun empty(): ChooseTrackData {
|
||||
return ChooseTrackData(
|
||||
TrackPartition(emptySet(), emptyList(), emptyList(), emptyList()),
|
||||
emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.delegation.delegation.common.chooseTrack.model
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackId
|
||||
import io.novafoundation.nova.feature_governance_api.domain.track.Track
|
||||
|
||||
data class TrackPartition(
|
||||
val preCheckedTrackIds: Set<TrackId>,
|
||||
val available: List<Track>,
|
||||
val alreadyVoted: List<Track>,
|
||||
val alreadyDelegated: List<Track>
|
||||
)
|
||||
|
||||
fun TrackPartition.hasUnavailableTracks(): Boolean {
|
||||
return alreadyVoted.isNotEmpty() || alreadyDelegated.isNotEmpty()
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.delegation.delegation.common.chooseTrack.model
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackId
|
||||
import io.novafoundation.nova.feature_governance_api.domain.track.Track
|
||||
|
||||
data class TrackPreset(
|
||||
val type: Type,
|
||||
val trackIds: List<TrackId>
|
||||
) {
|
||||
|
||||
companion object;
|
||||
|
||||
enum class Type {
|
||||
|
||||
ALL, TREASURY, FELLOWSHIP, GOVERNANCE
|
||||
}
|
||||
}
|
||||
|
||||
fun TrackPreset.Companion.all(trackIds: List<TrackId>): TrackPreset {
|
||||
return TrackPreset(
|
||||
type = TrackPreset.Type.ALL,
|
||||
trackIds = trackIds
|
||||
)
|
||||
}
|
||||
|
||||
@JvmName("allFromTrackInfo")
|
||||
fun TrackPreset.Companion.all(trackInfos: List<Track>): TrackPreset {
|
||||
return all(trackInfos.map(Track::id))
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.delegation.delegation.create.chooseAmount
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.domain.locks.reusable.LocksChange
|
||||
import io.novafoundation.nova.feature_governance_api.domain.locks.reusable.ReusableLock
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.types.custom.vote.Conviction
|
||||
|
||||
interface DelegateAssistant {
|
||||
|
||||
suspend fun estimateLocksAfterDelegating(amount: Balance, conviction: Conviction, asset: Asset): LocksChange
|
||||
|
||||
suspend fun reusableLocks(): List<ReusableLock>
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.delegation.delegation.create.chooseAmount
|
||||
|
||||
import io.novafoundation.nova.common.utils.multiResult.RetriableMultiResult
|
||||
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_governance_api.data.network.blockhain.model.TrackId
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.runtime.extrinsic.ExtrinsicStatus
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.types.custom.vote.Conviction
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface NewDelegationChooseAmountInteractor {
|
||||
|
||||
suspend fun maxAvailableBalanceToDelegate(asset: Asset): Balance
|
||||
|
||||
fun delegateAssistantFlow(
|
||||
coroutineScope: CoroutineScope
|
||||
): Flow<DelegateAssistant>
|
||||
|
||||
suspend fun estimateFee(
|
||||
amount: Balance,
|
||||
conviction: Conviction,
|
||||
delegate: AccountId,
|
||||
tracks: Collection<TrackId>,
|
||||
shouldRemoveOtherTracks: Boolean,
|
||||
): Fee
|
||||
|
||||
suspend fun delegate(
|
||||
amount: Balance,
|
||||
conviction: Conviction,
|
||||
delegate: AccountId,
|
||||
tracks: Collection<TrackId>,
|
||||
shouldRemoveOtherTracks: Boolean,
|
||||
): RetriableMultiResult<ExtrinsicWatchResult<ExtrinsicStatus.InBlock>>
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.delegation.delegation.removeVotes
|
||||
|
||||
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_governance_api.data.network.blockhain.model.TrackId
|
||||
import io.novafoundation.nova.runtime.extrinsic.ExtrinsicStatus
|
||||
|
||||
interface RemoveTrackVotesInteractor {
|
||||
|
||||
suspend fun calculateFee(trackIds: Collection<TrackId>): Fee
|
||||
|
||||
suspend fun removeTrackVotes(trackIds: Collection<TrackId>): Result<ExtrinsicWatchResult<ExtrinsicStatus.InBlock>>
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.locks
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.castOrNull
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumId
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackId
|
||||
import io.novafoundation.nova.feature_governance_api.domain.locks.ClaimSchedule.UnlockChunk
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
|
||||
@JvmInline
|
||||
value class ClaimSchedule(val chunks: List<UnlockChunk>) {
|
||||
|
||||
sealed class UnlockChunk {
|
||||
|
||||
data class Claimable(val amount: Balance, val actions: List<ClaimAction>) : UnlockChunk()
|
||||
|
||||
data class Pending(val amount: Balance, val claimableAt: ClaimTime) : UnlockChunk()
|
||||
}
|
||||
|
||||
sealed class ClaimAction {
|
||||
|
||||
data class Unlock(val trackId: TrackId) : ClaimAction()
|
||||
|
||||
data class RemoveVote(val trackId: TrackId, val referendumId: ReferendumId) : ClaimAction()
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ClaimTime : Comparable<ClaimTime> {
|
||||
|
||||
object UntilAction : ClaimTime()
|
||||
|
||||
data class At(val block: BlockNumber) : ClaimTime()
|
||||
|
||||
override operator fun compareTo(other: ClaimTime): Int {
|
||||
return when {
|
||||
this is At && other is At -> block.compareTo(other.block)
|
||||
this is UntilAction && other is UntilAction -> 0
|
||||
this is UntilAction -> 1
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ClaimSchedule.claimableChunk(): UnlockChunk.Claimable? {
|
||||
return chunks.firstOrNull().castOrNull<UnlockChunk.Claimable>()
|
||||
}
|
||||
|
||||
fun ClaimSchedule.hasClaimableLocks(): Boolean {
|
||||
return claimableChunk() != null
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.locks
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.AccountVote
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumId
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
|
||||
interface ClaimScheduleCalculator {
|
||||
|
||||
fun totalGovernanceLock(): Balance
|
||||
|
||||
fun maxConvictionEndOf(vote: AccountVote, referendumId: ReferendumId): BlockNumber
|
||||
|
||||
fun estimateClaimSchedule(): ClaimSchedule
|
||||
}
|
||||
+437
@@ -0,0 +1,437 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.locks
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.AccountVote
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ConfirmingSource
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.OnChainReferendum
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.OnChainReferendumStatus
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumId
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackId
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackInfo
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.VoteType
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.Voting
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.amount
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.completedReferendumLockDuration
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.maxLockDuration
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.totalLock
|
||||
import io.novafoundation.nova.feature_governance_api.domain.locks.ClaimSchedule.ClaimAction
|
||||
import io.novafoundation.nova.feature_governance_api.domain.locks.ClaimSchedule.ClaimAction.RemoveVote
|
||||
import io.novafoundation.nova.feature_governance_api.domain.locks.ClaimSchedule.ClaimAction.Unlock
|
||||
import io.novafoundation.nova.feature_governance_api.domain.locks.ClaimSchedule.UnlockChunk
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novasama.substrate_sdk_android.hash.isPositive
|
||||
|
||||
private data class ClaimableLock(
|
||||
val claimAt: ClaimTime,
|
||||
val amount: Balance,
|
||||
val affected: Set<ClaimAffect>,
|
||||
)
|
||||
|
||||
private sealed class ClaimAffect(open val trackId: TrackId) {
|
||||
|
||||
data class Track(override val trackId: TrackId) : ClaimAffect(trackId)
|
||||
|
||||
data class Vote(override val trackId: TrackId, val referendumId: ReferendumId) : ClaimAffect(trackId)
|
||||
}
|
||||
|
||||
private class GroupedClaimAffects(
|
||||
val trackId: TrackId,
|
||||
val hasPriorAffect: Boolean,
|
||||
val votes: List<ClaimAffect.Vote>
|
||||
)
|
||||
|
||||
typealias UnlockGap = Map<TrackId, Balance>
|
||||
|
||||
class RealClaimScheduleCalculator(
|
||||
private val votingByTrack: Map<TrackId, Voting>,
|
||||
private val currentBlockNumber: BlockNumber,
|
||||
private val referenda: Map<ReferendumId, OnChainReferendum>,
|
||||
private val tracks: Map<TrackId, TrackInfo>,
|
||||
private val undecidingTimeout: BlockNumber,
|
||||
private val voteLockingPeriod: BlockNumber,
|
||||
private val trackLocks: Map<TrackId, Balance>,
|
||||
) : ClaimScheduleCalculator {
|
||||
|
||||
override fun totalGovernanceLock(): Balance {
|
||||
return trackLocks.values.maxOrNull().orZero()
|
||||
}
|
||||
|
||||
@Suppress("IfThenToElvis")
|
||||
override fun maxConvictionEndOf(vote: AccountVote, referendumId: ReferendumId): BlockNumber {
|
||||
val referendum = referenda[referendumId]
|
||||
|
||||
return if (referendum != null) {
|
||||
referendum.maxConvictionEnd(vote)
|
||||
} else {
|
||||
// referendum is not in the map, which means it is cancelled and votes can be unlocked immediately
|
||||
currentBlockNumber
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the information about Voting (priors + active votes), statuses of referenda and TrackLocks
|
||||
* Constructs the estimated claiming schedule.
|
||||
* The schedule is exact when all involved referenda are completed. Only ongoing referenda' end time is estimateted
|
||||
*
|
||||
* The claiming schedule shows how much tokens will be unlocked and when.
|
||||
* Schedule may consist of zero or one [ClaimSchedule.UnlockChunk.Claimable] chunk
|
||||
* and zero or more [ClaimSchedule.UnlockChunk.Pending] chunks.
|
||||
*
|
||||
* [ClaimSchedule.UnlockChunk.Pending] chunks also provides a set of [ClaimSchedule.ClaimAction] actions
|
||||
* needed to claim whole chunk.
|
||||
*
|
||||
* The algorithm itself consists of several parts
|
||||
*
|
||||
* 1. Determine individual unlocks
|
||||
* This step is based on prior [Voting.Casting.prior] and [AccountVote.Standard] standard votes
|
||||
* a. Each non-zero prior has a single individual unlock
|
||||
* b. Each non-zero vote has a single individual unlock.
|
||||
* However, unlock time for votes is at least unlock time of corresponding prior.
|
||||
* c. Find a gap between [votingByTrack] and [trackLocks], which indicates an extra claimable amount
|
||||
* To provide additive effect of gap, we add total voting lock on top of it:
|
||||
if [votingByTrack] has some pending locks - they gonna delay their amount but always leaving trackGap untouched & claimable
|
||||
On the other hand, if other tracks have locks bigger than [votingByTrack]'s total lock,
|
||||
trackGap will be partially or full delayed by them
|
||||
*
|
||||
* During this step we also determine the list of [ClaimAffect],
|
||||
* which later gets translated to [ClaimSchedule.ClaimAction].
|
||||
*
|
||||
* 2. Combine all locks with the same unlock time into single lock
|
||||
* a. Result's amount is the maximum between combined locks
|
||||
* b. Result's affects is a concatenation of all affects from combined locks
|
||||
*
|
||||
* 3. Construct preliminary unlock schedule based on the following algorithm
|
||||
* a. Sort pairs from step (2) by descending [ClaimableLock.claimAt] order
|
||||
* b. For each item in the sorted list, find the difference between the biggest currently processed lock and item's amount
|
||||
* c. Since we start from the most far locks in the future, finding a positive difference means that
|
||||
* this difference is actually an entry in desired unlock schedule. Negative difference means that this unlock is
|
||||
* completely covered by future's unlock with bigger amount. Thus, we should discard it from the schedule and move its affects
|
||||
* to the currently known maximum lock in order to not to loose its actions when unlocking maximum lock.
|
||||
*
|
||||
* 4. Check which if unlocks are claimable and which are not by constructing [ClaimSchedule.UnlockChunk] based on [currentBlockNumber]
|
||||
* 5. Fold all [ClaimSchedule.UnlockChunk] into single chunk.
|
||||
* 6. If gap exists, then we should add it to claimable chunk. We should also check if we should perform extra [ClaimSchedule.ClaimAction.Unlock]
|
||||
* for each track that is included in the gap. We do that by finding by checking which [ClaimSchedule.ClaimAction.Unlock] unlocks are already present
|
||||
* in claimable chunk's actions in order to not to do them twice.
|
||||
*/
|
||||
override fun estimateClaimSchedule(): ClaimSchedule {
|
||||
// step 1 - determine/estimate individual unlocks for all priors and votes
|
||||
// result example: [(1500, 1 KSM), (1200, 2 KSM), (1000, 1 KSM)]
|
||||
val claimableLocks = individualClaimableLocks()
|
||||
|
||||
// step 2 - fold all locks with same lockAt
|
||||
// { 1500: 1 KSM, 1200: 2 KSM, 1000: 1 KSM }
|
||||
val maxUnlockedByTime = combineSameUnlockAt(claimableLocks)
|
||||
|
||||
// step 3 - convert individual schedule to global
|
||||
// [(1500, 1 KSM), (1200, 1 KSM)]
|
||||
val unlockSchedule = constructUnlockSchedule(maxUnlockedByTime)
|
||||
|
||||
// step 4 - convert locks affects to claim actions
|
||||
val chunks = unlockSchedule.toUnlockChunks()
|
||||
|
||||
return ClaimSchedule(chunks)
|
||||
}
|
||||
|
||||
private fun individualClaimableLocks(): List<ClaimableLock> {
|
||||
val gapBetweenVotingAndLocked = votingByTrack.gapWith(trackLocks)
|
||||
|
||||
return votingByTrack.flatMap { (trackId, voting) ->
|
||||
buildList {
|
||||
gapClaimableLock(trackId, voting, gapBetweenVotingAndLocked)
|
||||
|
||||
when (voting) {
|
||||
is Voting.Casting -> castingClaimableLocks(trackId, voting)
|
||||
is Voting.Delegating -> delegatingClaimableLocks(trackId, voting)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<ClaimableLock>.gapClaimableLock(trackId: TrackId, voting: Voting, gap: UnlockGap) {
|
||||
val trackGap = gap[trackId].orZero()
|
||||
|
||||
if (trackGap.isPositive()) {
|
||||
val lock = ClaimableLock(
|
||||
claimAt = ClaimTime.At(currentBlockNumber),
|
||||
amount = trackGap + voting.totalLock(),
|
||||
affected = setOf(ClaimAffect.Track(trackId))
|
||||
)
|
||||
|
||||
add(lock)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<ClaimableLock>.delegatingClaimableLocks(trackId: TrackId, voting: Voting.Delegating) {
|
||||
val delegationLock = ClaimableLock(
|
||||
claimAt = ClaimTime.UntilAction,
|
||||
amount = voting.amount,
|
||||
affected = emptySet()
|
||||
)
|
||||
val priorLock = ClaimableLock(
|
||||
claimAt = ClaimTime.At(voting.prior.unlockAt),
|
||||
amount = voting.prior.amount,
|
||||
affected = setOf(ClaimAffect.Track(trackId))
|
||||
)
|
||||
|
||||
add(delegationLock)
|
||||
if (priorLock.reasonableToClaim()) add(priorLock)
|
||||
}
|
||||
|
||||
private fun MutableList<ClaimableLock>.castingClaimableLocks(trackId: TrackId, voting: Voting.Casting) {
|
||||
val priorLock = ClaimableLock(
|
||||
claimAt = ClaimTime.At(voting.prior.unlockAt),
|
||||
amount = voting.prior.amount,
|
||||
affected = setOf(ClaimAffect.Track(trackId))
|
||||
)
|
||||
|
||||
val standardVoteLocks = voting.votes.map { (referendumId, standardVote) ->
|
||||
val estimatedEnd = maxConvictionEndOf(standardVote, referendumId)
|
||||
val lock = ClaimableLock(
|
||||
claimAt = ClaimTime.At(estimatedEnd),
|
||||
amount = standardVote.amount(),
|
||||
affected = setOf(ClaimAffect.Vote(trackId, referendumId))
|
||||
)
|
||||
|
||||
// we estimate whether prior will affect the vote when performing `removeVote`
|
||||
lock.timeAtLeast(priorLock.claimAt)
|
||||
}
|
||||
|
||||
if (priorLock.reasonableToClaim()) add(priorLock)
|
||||
addAll(standardVoteLocks)
|
||||
}
|
||||
|
||||
private fun combineSameUnlockAt(claimableLocks: List<ClaimableLock>) =
|
||||
claimableLocks.groupBy(ClaimableLock::claimAt)
|
||||
.mapValues { (_, locks) ->
|
||||
locks.reduce { current, next -> current.foldSameTime(next) }
|
||||
}
|
||||
|
||||
private fun constructUnlockSchedule(maxUnlockedByTime: Map<ClaimTime, ClaimableLock>): List<ClaimableLock> {
|
||||
var currentMaxLock = Balance.ZERO
|
||||
var currentMaxLockAt: ClaimTime? = null
|
||||
|
||||
val result = maxUnlockedByTime.toMutableMap()
|
||||
|
||||
maxUnlockedByTime.entries.sortedByDescending { it.key }
|
||||
.forEach { (at, lock) ->
|
||||
val newMaxLock = currentMaxLock.max(lock.amount)
|
||||
val unlockedAmount = lock.amount - currentMaxLock
|
||||
|
||||
val shouldSetNewMax = currentMaxLockAt == null || currentMaxLock < newMaxLock
|
||||
if (shouldSetNewMax) {
|
||||
currentMaxLock = newMaxLock
|
||||
currentMaxLockAt = at
|
||||
}
|
||||
|
||||
if (unlockedAmount.isPositive()) {
|
||||
// there is something to unlock at this point
|
||||
result[at] = lock.copy(amount = unlockedAmount)
|
||||
} else {
|
||||
// this lock is completely shadowed by later (in time) lock with greater value
|
||||
result.remove(at)
|
||||
|
||||
// but we want to keep its actions so we move it to the current known maximum that goes later in time
|
||||
result.computeIfPresent(currentMaxLockAt!!) { _, maxLock ->
|
||||
maxLock.copy(affected = maxLock.affected + lock.affected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.toSortedMap().values.toList()
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun List<ClaimableLock>.toUnlockChunks(): List<UnlockChunk> {
|
||||
val chunks = map { it.toUnlockChunk(currentBlockNumber) }
|
||||
val (claimable, nonClaimable) = chunks.partition { it is UnlockChunk.Claimable }
|
||||
|
||||
// fold all claimable chunks to single one
|
||||
val initialClaimable = Balance.ZERO to emptyList<ClaimAction>()
|
||||
|
||||
val (claimableAmount, claimableActions) = (claimable as List<UnlockChunk.Claimable>).fold(initialClaimable) { (amount, actions), unlockChunk ->
|
||||
val nextAmount = amount + unlockChunk.amount
|
||||
val nextActions = actions + unlockChunk.actions
|
||||
nextAmount to nextActions
|
||||
}
|
||||
val claimableChunk = constructClaimableChunk(claimableAmount, claimableActions)
|
||||
|
||||
return buildList {
|
||||
if (claimableChunk.amount.isPositive()) {
|
||||
add(claimableChunk)
|
||||
}
|
||||
|
||||
addAll(nonClaimable)
|
||||
}
|
||||
}
|
||||
|
||||
private fun constructClaimableChunk(
|
||||
claimableAmount: Balance,
|
||||
claimableActions: List<ClaimAction>
|
||||
): UnlockChunk.Claimable {
|
||||
return UnlockChunk.Claimable(claimableAmount, claimableActions.dedublicateUnlocks())
|
||||
}
|
||||
|
||||
// We want to avoid doing multiple unlocks for the same track
|
||||
// For that we also need to move unlock() calls to the end
|
||||
private fun List<ClaimAction>.dedublicateUnlocks(): List<ClaimAction> {
|
||||
return distinct().sortedBy { it is Unlock }
|
||||
}
|
||||
|
||||
private fun OnChainReferendum.maxConvictionEnd(vote: AccountVote): BlockNumber {
|
||||
return when (val status = status) {
|
||||
is OnChainReferendumStatus.Ongoing -> status.maxConvictionEnd(vote)
|
||||
|
||||
is OnChainReferendumStatus.Approved -> maxCompletedConvictionEnd(
|
||||
vote = vote,
|
||||
referendumOutcome = VoteType.AYE,
|
||||
completedSince = status.since
|
||||
)
|
||||
|
||||
is OnChainReferendumStatus.Rejected -> maxCompletedConvictionEnd(
|
||||
vote = vote,
|
||||
referendumOutcome = VoteType.NAY,
|
||||
completedSince = status.since
|
||||
)
|
||||
|
||||
is OnChainReferendumStatus.Cancelled -> status.since
|
||||
is OnChainReferendumStatus.Killed -> status.since
|
||||
is OnChainReferendumStatus.TimedOut -> status.since
|
||||
}
|
||||
}
|
||||
|
||||
private fun maxCompletedConvictionEnd(
|
||||
vote: AccountVote,
|
||||
referendumOutcome: VoteType,
|
||||
completedSince: BlockNumber
|
||||
): BlockNumber {
|
||||
val convictionPart = vote.completedReferendumLockDuration(referendumOutcome, voteLockingPeriod)
|
||||
|
||||
return completedSince + convictionPart
|
||||
}
|
||||
|
||||
private fun OnChainReferendumStatus.Ongoing.maxConvictionEnd(vote: AccountVote): BlockNumber {
|
||||
val trackInfo = tracks.getValue(track)
|
||||
val decisionPeriod = trackInfo.decisionPeriod
|
||||
|
||||
val blocksAfterCompleted = vote.maxLockDuration(voteLockingPeriod)
|
||||
|
||||
val maxCompletedAt = when {
|
||||
inQueue -> {
|
||||
val maxDecideSince = submitted + undecidingTimeout
|
||||
|
||||
maxDecideSince + decisionPeriod
|
||||
}
|
||||
|
||||
deciding != null -> {
|
||||
when (val source = deciding.confirming) {
|
||||
is ConfirmingSource.FromThreshold -> source.end
|
||||
|
||||
is ConfirmingSource.OnChain -> if (source.status != null) {
|
||||
// confirming
|
||||
val approveBlock = source.status.till
|
||||
val rejectBlock = deciding.since + decisionPeriod
|
||||
|
||||
approveBlock.max(rejectBlock)
|
||||
} else {
|
||||
// rejecting
|
||||
val rejectBlock = deciding.since + decisionPeriod
|
||||
|
||||
rejectBlock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// preparing
|
||||
else -> {
|
||||
val maxDecideSince = submitted + undecidingTimeout.max(trackInfo.preparePeriod)
|
||||
|
||||
maxDecideSince + decisionPeriod
|
||||
}
|
||||
}
|
||||
|
||||
return maxCompletedAt + blocksAfterCompleted
|
||||
}
|
||||
}
|
||||
|
||||
private fun ClaimableLock.foldSameTime(another: ClaimableLock): ClaimableLock {
|
||||
require(claimAt == another.claimAt)
|
||||
|
||||
return ClaimableLock(
|
||||
claimAt = claimAt,
|
||||
amount = amount.max(another.amount),
|
||||
affected = affected + another.affected
|
||||
)
|
||||
}
|
||||
|
||||
private fun ClaimableLock.reasonableToClaim(): Boolean {
|
||||
return amount.isPositive()
|
||||
}
|
||||
|
||||
private infix fun ClaimableLock.timeAtLeast(time: ClaimTime): ClaimableLock {
|
||||
val newClaimAt = maxOf(claimAt, time)
|
||||
|
||||
return copy(claimAt = newClaimAt)
|
||||
}
|
||||
|
||||
private fun ClaimableLock.claimableAt(at: BlockNumber): Boolean {
|
||||
return when (claimAt) {
|
||||
is ClaimTime.At -> claimAt.block <= at
|
||||
ClaimTime.UntilAction -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun ClaimableLock.toUnlockChunk(currentBlockNumber: BlockNumber): UnlockChunk {
|
||||
return if (claimableAt(currentBlockNumber)) {
|
||||
UnlockChunk.Claimable(
|
||||
amount = amount,
|
||||
actions = affected.toClaimActions()
|
||||
)
|
||||
} else {
|
||||
UnlockChunk.Pending(amount, claimAt)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Map<TrackId, Voting>.gapWith(locksByTrackId: Map<TrackId, Balance>): UnlockGap {
|
||||
val gapByTrack = mapValues { (trackId, voting) ->
|
||||
val trackLock = locksByTrackId[trackId].orZero()
|
||||
val gap = (trackLock - voting.totalLock()).coerceAtLeast(Balance.ZERO)
|
||||
|
||||
gap
|
||||
}
|
||||
|
||||
return gapByTrack
|
||||
}
|
||||
|
||||
private fun Collection<ClaimAffect>.toClaimActions(): List<ClaimAction> {
|
||||
return groupByTrack().flatMap { trackAffects ->
|
||||
buildList {
|
||||
if (trackAffects.hasPriorAffect) {
|
||||
val requiresStandaloneUnlock = trackAffects.votes.isEmpty()
|
||||
|
||||
if (requiresStandaloneUnlock) {
|
||||
add(Unlock(trackAffects.trackId))
|
||||
}
|
||||
}
|
||||
|
||||
if (trackAffects.votes.isNotEmpty()) {
|
||||
trackAffects.votes.forEach { voteAffect ->
|
||||
add(RemoveVote(voteAffect.trackId, voteAffect.referendumId))
|
||||
}
|
||||
|
||||
add(Unlock(trackAffects.votes.first().trackId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Collection<ClaimAffect>.groupByTrack(): List<GroupedClaimAffects> {
|
||||
return groupBy(ClaimAffect::trackId).entries.map { (trackId, trackAffects) ->
|
||||
GroupedClaimAffects(
|
||||
trackId = trackId,
|
||||
hasPriorAffect = trackAffects.any { it is ClaimAffect.Track },
|
||||
votes = trackAffects.filterIsInstance<ClaimAffect.Vote>()
|
||||
)
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.locks.reusable
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.common.Change
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novasama.substrate_sdk_android.hash.isPositive
|
||||
import kotlin.time.Duration
|
||||
|
||||
class LocksChange(
|
||||
val lockedAmountChange: Change<Balance>,
|
||||
val lockedPeriodChange: Change<Duration>,
|
||||
val transferableChange: Change<Balance>
|
||||
)
|
||||
|
||||
class ReusableLock(val type: Type, val amount: Balance) {
|
||||
enum class Type {
|
||||
GOVERNANCE, ALL
|
||||
}
|
||||
}
|
||||
|
||||
fun MutableList<ReusableLock>.addIfPositive(type: ReusableLock.Type, amount: Balance) {
|
||||
if (amount.isPositive()) {
|
||||
add(ReusableLock(type, amount))
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.common
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import java.math.BigInteger
|
||||
import kotlin.time.Duration
|
||||
|
||||
sealed class Change<T>(open val previousValue: T, open val newValue: T) {
|
||||
|
||||
data class Changed<T>(
|
||||
override val previousValue: T,
|
||||
override val newValue: T,
|
||||
val absoluteDifference: T,
|
||||
val positive: Boolean
|
||||
) : Change<T>(previousValue, newValue)
|
||||
|
||||
data class Same<T>(val value: T) : Change<T>(previousValue = value, newValue = value)
|
||||
}
|
||||
|
||||
fun Change<Balance>.absoluteDifference(): Balance {
|
||||
return when (this) {
|
||||
is Change.Changed -> absoluteDifference
|
||||
is Change.Same -> Balance.ZERO
|
||||
}
|
||||
}
|
||||
|
||||
fun Change<Duration>.absoluteDifference(): Duration {
|
||||
return when (this) {
|
||||
is Change.Changed -> absoluteDifference
|
||||
is Change.Same -> Duration.ZERO
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Comparable<T>> Change(
|
||||
previousValue: T,
|
||||
newValue: T,
|
||||
absoluteDifference: T
|
||||
): Change<T> {
|
||||
return if (previousValue == newValue) {
|
||||
Change.Same(newValue)
|
||||
} else {
|
||||
Change.Changed(
|
||||
previousValue = previousValue,
|
||||
newValue = newValue,
|
||||
absoluteDifference = absoluteDifference,
|
||||
positive = newValue > previousValue
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun Change(
|
||||
previousValue: BigInteger,
|
||||
newValue: BigInteger,
|
||||
): Change<BigInteger> {
|
||||
val absoluteDifference = (newValue - previousValue).abs()
|
||||
|
||||
return Change(previousValue, newValue, absoluteDifference)
|
||||
}
|
||||
|
||||
fun Change(
|
||||
previousValue: Duration,
|
||||
newValue: Duration,
|
||||
): Change<Duration> {
|
||||
val absoluteDifference = (newValue - previousValue).absoluteValue
|
||||
|
||||
return Change(previousValue, newValue, absoluteDifference)
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.common
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
|
||||
data class ReferendumProposer(val accountId: AccountId, val offChainNickname: String?)
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.common
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.Perbill
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.VotingThreshold
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.VotingThreshold.Threshold
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.merge
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
|
||||
data class ReferendumThreshold(
|
||||
val support: Threshold<Balance>,
|
||||
val approval: Threshold<Perbill>
|
||||
)
|
||||
|
||||
fun Threshold<*>.currentlyPassing(): Boolean {
|
||||
return currentlyPassing
|
||||
}
|
||||
|
||||
fun ReferendumThreshold.currentlyPassing(): Boolean {
|
||||
return support.currentlyPassing() && approval.currentlyPassing()
|
||||
}
|
||||
|
||||
fun ReferendumThreshold.projectedPassing(): VotingThreshold.ProjectedPassing {
|
||||
return support.projectedPassing.merge(approval.projectedPassing)
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.common
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.domain.track.Track
|
||||
|
||||
data class ReferendumTrack(val track: Track, val sameWithOther: Boolean)
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.common
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.Perbill
|
||||
import io.novafoundation.nova.common.domain.ExtendedLoadingState
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import java.math.BigInteger
|
||||
|
||||
data class ReferendumVoting(
|
||||
val support: ExtendedLoadingState<Support?>,
|
||||
val approval: ExtendedLoadingState<Approval?>,
|
||||
val abstainVotes: ExtendedLoadingState<BigInteger?>
|
||||
) {
|
||||
|
||||
data class Support(
|
||||
val turnout: Balance,
|
||||
val electorate: Balance,
|
||||
)
|
||||
|
||||
data class Approval(
|
||||
val ayeVotes: Votes,
|
||||
val nayVotes: Votes,
|
||||
) {
|
||||
|
||||
// post-conviction
|
||||
data class Votes(
|
||||
val amount: Balance,
|
||||
val fraction: Perbill
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun ReferendumVoting.Approval.totalVotes(): Balance {
|
||||
return ayeVotes.amount + nayVotes.amount
|
||||
}
|
||||
|
||||
fun ReferendumVoting.Approval.ayeVotesIfNotEmpty(): ReferendumVoting.Approval.Votes? {
|
||||
return ayeVotes.takeIf { totalVotes() != Balance.ZERO }
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.details
|
||||
|
||||
sealed class PreimagePreview {
|
||||
|
||||
object TooLong : PreimagePreview()
|
||||
|
||||
class Display(val value: String) : PreimagePreview()
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.details
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.PreImage
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumId
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.VotingCurve
|
||||
import io.novafoundation.nova.feature_governance_api.data.thresold.gov1.Gov1VotingThreshold
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.common.ReferendumProposer
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.common.ReferendumThreshold
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.common.ReferendumTrack
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.common.ReferendumVoting
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.list.ReferendumStatus
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.list.ReferendumVote
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
|
||||
data class ReferendumDetails(
|
||||
val id: ReferendumId,
|
||||
val offChainMetadata: OffChainMetadata?,
|
||||
val onChainMetadata: OnChainMetadata?,
|
||||
val proposer: ReferendumProposer?,
|
||||
val track: ReferendumTrack?,
|
||||
val voting: ReferendumVoting?,
|
||||
val threshold: ReferendumThreshold?,
|
||||
val userVote: ReferendumVote?,
|
||||
val timeline: ReferendumTimeline,
|
||||
val fullDetails: FullDetails
|
||||
) {
|
||||
|
||||
data class FullDetails(
|
||||
val deposit: Balance?,
|
||||
val voteThreshold: Gov1VotingThreshold?,
|
||||
val approvalCurve: VotingCurve?,
|
||||
val supportCurve: VotingCurve?,
|
||||
)
|
||||
|
||||
data class OffChainMetadata(val title: String?, val description: String?)
|
||||
|
||||
data class OnChainMetadata(val preImage: PreImage?, val preImageHash: ByteArray)
|
||||
}
|
||||
|
||||
data class ReferendumTimeline(val currentStatus: ReferendumStatus, val pastEntries: List<Entry>) {
|
||||
|
||||
data class Entry(val state: State, val at: Long?) {
|
||||
companion object // extensions
|
||||
}
|
||||
|
||||
enum class State {
|
||||
CREATED, APPROVED, REJECTED, EXECUTED, CANCELLED, KILLED, TIMED_OUT
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.details
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.runtime.ext.fullId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
|
||||
sealed class ReferendumCall {
|
||||
|
||||
open fun combineWith(other: ReferendumCall): ReferendumCall = this
|
||||
|
||||
data class TreasuryRequest(
|
||||
val amount: Balance,
|
||||
val beneficiary: AccountId,
|
||||
val chainAsset: Chain.Asset,
|
||||
) : ReferendumCall() {
|
||||
|
||||
override fun combineWith(other: ReferendumCall): ReferendumCall {
|
||||
if (other is TreasuryRequest && other.chainAsset.fullId == chainAsset.fullId) {
|
||||
return copy(amount = amount + other.amount)
|
||||
}
|
||||
|
||||
return super.combineWith(other)
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.details
|
||||
|
||||
class ReferendumDApp(
|
||||
val chainId: String,
|
||||
val name: String,
|
||||
val referendumUrl: String,
|
||||
val iconUrl: String,
|
||||
val details: String
|
||||
)
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.details
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.PreImage
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumId
|
||||
import io.novafoundation.nova.feature_governance_api.data.source.SupportedGovernanceOption
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ReferendumDetailsInteractor {
|
||||
|
||||
fun referendumDetailsFlow(
|
||||
referendumId: ReferendumId,
|
||||
selectedGovernanceOption: SupportedGovernanceOption,
|
||||
voterAccountId: AccountId?,
|
||||
coroutineScope: CoroutineScope
|
||||
): Flow<ReferendumDetails?>
|
||||
|
||||
suspend fun detailsFor(
|
||||
preImage: PreImage,
|
||||
chain: Chain,
|
||||
): ReferendumCall?
|
||||
|
||||
suspend fun previewFor(preImage: PreImage): PreimagePreview
|
||||
|
||||
suspend fun isSupportAbstainVoting(selectedGovernanceOption: SupportedGovernanceOption): Boolean
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.details
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.list.ReferendumStatus
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.list.ReferendumVote
|
||||
|
||||
fun ReferendumDetails.isUserDelegatedVote() = userVote is ReferendumVote.UserDelegated
|
||||
|
||||
fun ReferendumDetails.isUserDirectVote() = userVote is ReferendumVote.UserDirect
|
||||
|
||||
fun ReferendumDetails.noVote() = userVote == null
|
||||
|
||||
fun ReferendumStatus.isOngoing(): Boolean {
|
||||
return this is ReferendumStatus.Ongoing
|
||||
}
|
||||
|
||||
fun ReferendumDetails.isFinished() = !timeline.currentStatus.isOngoing()
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.details.valiadtions
|
||||
|
||||
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 ReferendumPreVoteValidationFailure {
|
||||
|
||||
class NoRelaychainAccount(
|
||||
override val chain: Chain,
|
||||
override val account: MetaAccount,
|
||||
override val addAccountState: NoChainAccountFoundError.AddAccountState
|
||||
) : ReferendumPreVoteValidationFailure(), NoChainAccountFoundError
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.details.valiadtions
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
class ReferendumPreVoteValidationPayload(
|
||||
val metaAccount: MetaAccount,
|
||||
val chain: Chain
|
||||
)
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.details.valiadtions
|
||||
|
||||
import io.novafoundation.nova.common.validation.ValidationSystem
|
||||
import io.novafoundation.nova.feature_account_api.domain.validation.hasChainAccount
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.details.valiadtions.ReferendumPreVoteValidationFailure.NoRelaychainAccount
|
||||
|
||||
typealias ReferendumPreVoteValidationSystem = ValidationSystem<ReferendumPreVoteValidationPayload, ReferendumPreVoteValidationFailure>
|
||||
|
||||
fun ValidationSystem.Companion.referendumPreVote(): ReferendumPreVoteValidationSystem = ValidationSystem {
|
||||
hasChainAccount(
|
||||
chain = ReferendumPreVoteValidationPayload::chain,
|
||||
metaAccount = ReferendumPreVoteValidationPayload::metaAccount,
|
||||
error = ::NoRelaychainAccount
|
||||
)
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.filters
|
||||
|
||||
enum class ReferendumType {
|
||||
ALL, NOT_VOTED, VOTED
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.filters
|
||||
|
||||
import io.novafoundation.nova.common.utils.OptionsFilter
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.list.ReferendumPreview
|
||||
|
||||
class ReferendumTypeFilter(val selectedType: ReferendumType) : OptionsFilter<ReferendumPreview, ReferendumType> {
|
||||
|
||||
override val options: List<ReferendumType>
|
||||
get() = ReferendumType.values().toList()
|
||||
|
||||
override fun shouldInclude(model: ReferendumPreview): Boolean {
|
||||
val vote = model.referendumVote?.vote
|
||||
return when (selectedType) {
|
||||
ReferendumType.ALL -> true
|
||||
ReferendumType.VOTED -> vote != null
|
||||
ReferendumType.NOT_VOTED -> vote == null
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.list
|
||||
|
||||
import io.novafoundation.nova.common.domain.ExtendedLoadingState
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_governance_api.data.source.SupportedGovernanceOption
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.filters.ReferendumTypeFilter
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.SelectableAssetAndOption
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ReferendaListInteractor {
|
||||
|
||||
suspend fun availableVoteAmount(option: SelectableAssetAndOption): Balance
|
||||
|
||||
fun searchReferendaListStateFlow(
|
||||
metaAccount: MetaAccount,
|
||||
queryFlow: Flow<String>,
|
||||
voterAccountId: AccountId?,
|
||||
selectedGovernanceOption: SupportedGovernanceOption,
|
||||
coroutineScope: CoroutineScope
|
||||
): Flow<ExtendedLoadingState<List<ReferendumPreview>>>
|
||||
|
||||
fun referendaListStateFlow(
|
||||
metaAccount: MetaAccount,
|
||||
voterAccountId: AccountId?,
|
||||
selectedGovernanceOption: SupportedGovernanceOption,
|
||||
coroutineScope: CoroutineScope,
|
||||
referendumTypeFilterFlow: Flow<ReferendumTypeFilter>
|
||||
): Flow<ExtendedLoadingState<ReferendaListState>>
|
||||
|
||||
fun votedReferendaListFlow(
|
||||
voter: Voter,
|
||||
onlyRecentVotes: Boolean
|
||||
): Flow<List<ReferendumPreview>>
|
||||
}
|
||||
|
||||
class Voter(val accountId: AccountId, val type: Type) {
|
||||
|
||||
companion object;
|
||||
|
||||
enum class Type {
|
||||
USER, ACCOUNT
|
||||
}
|
||||
}
|
||||
|
||||
fun Voter.Companion.user(accountId: AccountId) = Voter(accountId, Voter.Type.USER)
|
||||
|
||||
fun Voter.Companion.account(accountId: AccountId) = Voter(accountId, Voter.Type.ACCOUNT)
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.list
|
||||
|
||||
import io.novafoundation.nova.common.list.GroupedList
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
|
||||
class ReferendaListState(
|
||||
val groupedReferenda: GroupedList<ReferendumGroup, ReferendumPreview>,
|
||||
val locksOverview: GovernanceLocksOverview?,
|
||||
val delegated: DelegatedState,
|
||||
val availableToVoteReferenda: List<ReferendumPreview>
|
||||
)
|
||||
|
||||
class GovernanceLocksOverview(
|
||||
val locked: Balance,
|
||||
val hasClaimableLocks: Boolean
|
||||
)
|
||||
|
||||
sealed class DelegatedState {
|
||||
|
||||
object DelegationNotSupported : DelegatedState()
|
||||
|
||||
object NotDelegated : DelegatedState()
|
||||
|
||||
class Delegated(val amount: Balance) : DelegatedState()
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.list
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.OnChainReferendum
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumId
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackId
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackInfo
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.Voting
|
||||
|
||||
class ReferendaState(
|
||||
val voting: Map<TrackId, Voting>,
|
||||
val currentBlockNumber: BlockNumber,
|
||||
val onChainReferenda: Map<ReferendumId, OnChainReferendum>,
|
||||
val referenda: List<ReferendumPreview>,
|
||||
val tracksById: Map<TrackId, TrackInfo>,
|
||||
)
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.list
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumId
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.common.ReferendumThreshold
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.common.ReferendumTrack
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.common.ReferendumVoting
|
||||
|
||||
enum class ReferendumGroup {
|
||||
ONGOING, COMPLETED
|
||||
}
|
||||
|
||||
data class ReferendumPreview(
|
||||
val id: ReferendumId,
|
||||
val status: ReferendumStatus,
|
||||
val offChainMetadata: OffChainMetadata?,
|
||||
val onChainMetadata: OnChainMetadata?,
|
||||
val track: ReferendumTrack?,
|
||||
val voting: ReferendumVoting?,
|
||||
val threshold: ReferendumThreshold?,
|
||||
val referendumVote: ReferendumVote?,
|
||||
) {
|
||||
|
||||
data class OffChainMetadata(val title: String)
|
||||
|
||||
data class OnChainMetadata(val proposal: ReferendumProposal)
|
||||
}
|
||||
|
||||
fun ReferendumPreview.getName(): String? {
|
||||
return offChainMetadata?.title
|
||||
?: getOnChainName()
|
||||
}
|
||||
|
||||
private fun ReferendumPreview.getOnChainName(): String? {
|
||||
return when (val proposal = onChainMetadata?.proposal) {
|
||||
is ReferendumProposal.Call -> "${proposal.call.module.name}.${proposal.call.function.name}"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.list
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
|
||||
sealed class ReferendumProposal {
|
||||
|
||||
class Hash(val callHash: String) : ReferendumProposal()
|
||||
|
||||
class Call(val call: GenericCall.Instance) : ReferendumProposal()
|
||||
}
|
||||
|
||||
fun ReferendumProposal.toCallOrNull() = this as? ReferendumProposal.Call
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.list
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
|
||||
import io.novafoundation.nova.common.utils.formatting.TimerValue
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackQueue
|
||||
|
||||
enum class ReferendumStatusType {
|
||||
WAITING_DEPOSIT,
|
||||
PREPARING,
|
||||
IN_QUEUE,
|
||||
DECIDING,
|
||||
CONFIRMING,
|
||||
APPROVED,
|
||||
EXECUTED,
|
||||
TIMED_OUT,
|
||||
KILLED,
|
||||
CANCELLED,
|
||||
REJECTED;
|
||||
|
||||
companion object
|
||||
}
|
||||
|
||||
sealed class ReferendumStatus {
|
||||
|
||||
abstract val type: ReferendumStatusType
|
||||
|
||||
sealed class Ongoing : ReferendumStatus() {
|
||||
data class Preparing(val reason: PreparingReason, val timeOutIn: TimerValue) : Ongoing() {
|
||||
override val type = when (reason) {
|
||||
is PreparingReason.WaitingForDeposit -> ReferendumStatusType.WAITING_DEPOSIT
|
||||
is PreparingReason.DecidingIn -> ReferendumStatusType.PREPARING
|
||||
}
|
||||
}
|
||||
|
||||
data class InQueue(val timeOutIn: TimerValue, val position: TrackQueue.Position) : Ongoing() {
|
||||
override val type = ReferendumStatusType.IN_QUEUE
|
||||
}
|
||||
|
||||
data class DecidingReject(val rejectIn: TimerValue) : Ongoing() {
|
||||
override val type = ReferendumStatusType.DECIDING
|
||||
}
|
||||
|
||||
data class DecidingApprove(val approveIn: TimerValue) : Ongoing() {
|
||||
override val type = ReferendumStatusType.DECIDING
|
||||
}
|
||||
|
||||
data class Confirming(val approveIn: TimerValue) : Ongoing() {
|
||||
override val type = ReferendumStatusType.CONFIRMING
|
||||
}
|
||||
}
|
||||
|
||||
data class Approved(val since: BlockNumber, val executeIn: TimerValue) : ReferendumStatus() {
|
||||
override val type = ReferendumStatusType.APPROVED
|
||||
}
|
||||
|
||||
object Executed : ReferendumStatus() {
|
||||
override val type = ReferendumStatusType.EXECUTED
|
||||
}
|
||||
|
||||
sealed class NotExecuted : ReferendumStatus() {
|
||||
|
||||
object TimedOut : NotExecuted() {
|
||||
override val type = ReferendumStatusType.TIMED_OUT
|
||||
}
|
||||
|
||||
object Killed : NotExecuted() {
|
||||
override val type = ReferendumStatusType.KILLED
|
||||
}
|
||||
|
||||
object Cancelled : NotExecuted() {
|
||||
override val type = ReferendumStatusType.CANCELLED
|
||||
}
|
||||
|
||||
object Rejected : NotExecuted() {
|
||||
override val type = ReferendumStatusType.REJECTED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class PreparingReason {
|
||||
|
||||
object WaitingForDeposit : PreparingReason()
|
||||
|
||||
data class DecidingIn(val timeLeft: TimerValue) : PreparingReason()
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.list
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.domain.account.identity.Identity
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.AccountVote
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
|
||||
sealed class ReferendumVote(val vote: AccountVote) {
|
||||
|
||||
class UserDirect(vote: AccountVote) : ReferendumVote(vote)
|
||||
|
||||
class UserDelegated(
|
||||
override val who: AccountId,
|
||||
override val whoIdentity: Identity?,
|
||||
vote: AccountVote
|
||||
) : ReferendumVote(vote), WithDifferentVoter
|
||||
|
||||
class OtherAccount(
|
||||
override val who: AccountId,
|
||||
override val whoIdentity: Identity?,
|
||||
vote: AccountVote
|
||||
) : ReferendumVote(vote), WithDifferentVoter
|
||||
}
|
||||
|
||||
interface WithDifferentVoter {
|
||||
|
||||
val who: AccountId
|
||||
|
||||
val whoIdentity: Identity?
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.track.category
|
||||
|
||||
enum class TrackCategory {
|
||||
|
||||
TREASURY, GOVERNANCE, FELLOWSHIP, OTHER
|
||||
}
|
||||
|
||||
enum class TrackType {
|
||||
ROOT,
|
||||
WHITELISTED_CALLER, FELLOWSHIP_ADMIN,
|
||||
STAKING_ADMIN, LEASE_ADMIN, AUCTION_ADMIN,
|
||||
GENERAL_ADMIN, REFERENDUM_CANCELLER, REFERENDUM_KILLER,
|
||||
TREASURER, SMALL_TIPPER, BIG_TIPPER, SMALL_SPEND, MEDIUM_SPEND, BIG_SPEND,
|
||||
OTHER
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.vote
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.AccountVote
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.OnChainReferendum
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumId
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.Voting
|
||||
import io.novafoundation.nova.feature_governance_api.domain.locks.reusable.LocksChange
|
||||
import io.novafoundation.nova.feature_governance_api.domain.locks.reusable.ReusableLock
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
|
||||
interface GovernanceVoteAssistant {
|
||||
|
||||
val onChainReferenda: List<OnChainReferendum>
|
||||
|
||||
val trackVoting: List<Voting>
|
||||
|
||||
suspend fun estimateLocksAfterVoting(votes: Map<ReferendumId, AccountVote>, asset: Asset): LocksChange
|
||||
|
||||
suspend fun reusableLocks(): List<ReusableLock>
|
||||
}
|
||||
|
||||
suspend fun GovernanceVoteAssistant.estimateLocksAfterVoting(referendumId: ReferendumId, accountVote: AccountVote, asset: Asset): LocksChange {
|
||||
return estimateLocksAfterVoting(mapOf(referendumId to accountVote), asset)
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.vote
|
||||
|
||||
import io.novafoundation.nova.common.utils.multiResult.RetriableMultiResult
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission
|
||||
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_governance_api.data.network.blockhain.model.AccountVote
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumId
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.runtime.extrinsic.ExtrinsicStatus
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface VoteReferendumInteractor {
|
||||
|
||||
suspend fun maxAvailableForVote(asset: Asset): Balance
|
||||
|
||||
fun voteAssistantFlow(referendumId: ReferendumId, scope: CoroutineScope): Flow<GovernanceVoteAssistant>
|
||||
|
||||
fun voteAssistantFlow(referendaIds: List<ReferendumId>, scope: CoroutineScope): Flow<GovernanceVoteAssistant>
|
||||
|
||||
suspend fun estimateFee(referendumId: ReferendumId, vote: AccountVote): Fee
|
||||
|
||||
suspend fun estimateFee(votes: Map<ReferendumId, AccountVote>): Fee
|
||||
|
||||
suspend fun voteReferendum(referendumId: ReferendumId, vote: AccountVote): Result<ExtrinsicSubmission>
|
||||
|
||||
suspend fun voteReferenda(votes: Map<ReferendumId, AccountVote>): RetriableMultiResult<ExtrinsicWatchResult<ExtrinsicStatus.InBlock>>
|
||||
|
||||
suspend fun isAbstainSupported(): Boolean
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.voters
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.domain.account.identity.Identity
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.amountMultiplier
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.types.custom.vote.Conviction
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import java.math.BigDecimal
|
||||
|
||||
interface GenericVoter<V : GenericVoter.Vote?> {
|
||||
|
||||
val vote: V
|
||||
|
||||
val identity: Identity?
|
||||
|
||||
val accountId: AccountId
|
||||
|
||||
interface Vote {
|
||||
|
||||
val totalVotes: BigDecimal
|
||||
}
|
||||
|
||||
class ConvictionVote(val amount: BigDecimal, val conviction: Conviction) : Vote {
|
||||
override val totalVotes = amount * conviction.amountMultiplier()
|
||||
}
|
||||
}
|
||||
|
||||
fun SplitVote(amount: BigDecimal): GenericVoter.ConvictionVote = GenericVoter.ConvictionVote(amount, Conviction.None)
|
||||
fun SplitVote(planks: Balance, chainAsset: Chain.Asset): GenericVoter.ConvictionVote {
|
||||
return GenericVoter.ConvictionVote(chainAsset.amountFromPlanks(planks), Conviction.None)
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.voters
|
||||
|
||||
import io.novafoundation.nova.common.utils.sumByBigDecimal
|
||||
import io.novafoundation.nova.feature_account_api.domain.account.identity.Identity
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.AccountVote
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.VoteType
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.amountFor
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.conviction
|
||||
import io.novafoundation.nova.feature_governance_api.domain.delegation.delegate.label.DelegateLabel
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import java.math.BigDecimal
|
||||
|
||||
class ReferendumVoter(
|
||||
override val vote: Vote,
|
||||
override val identity: Identity?,
|
||||
override val accountId: AccountId,
|
||||
val metadata: DelegateLabel.Metadata?,
|
||||
) : GenericVoter<ReferendumVoter.Vote> {
|
||||
|
||||
sealed class Vote : GenericVoter.Vote {
|
||||
|
||||
class OnlySelf(val selfVote: GenericVoter.ConvictionVote) : Vote(), GenericVoter.Vote by selfVote
|
||||
|
||||
class WithDelegators(override val totalVotes: BigDecimal, val delegators: List<ReferendumVoterDelegator>) : Vote()
|
||||
}
|
||||
}
|
||||
|
||||
class ReferendumVoterDelegator(
|
||||
override val accountId: AccountId,
|
||||
override val vote: GenericVoter.ConvictionVote,
|
||||
val metadata: DelegateLabel.Metadata?,
|
||||
override val identity: Identity?,
|
||||
) : GenericVoter<GenericVoter.ConvictionVote>
|
||||
|
||||
fun ReferendumVoter(
|
||||
accountVote: AccountVote,
|
||||
voteType: VoteType,
|
||||
identity: Identity?,
|
||||
accountId: AccountId,
|
||||
chainAsset: Chain.Asset,
|
||||
metadata: DelegateLabel.Metadata?,
|
||||
delegators: List<ReferendumVoterDelegator>
|
||||
): ReferendumVoter {
|
||||
val selfVote = ConvictionVote(accountVote, chainAsset, voteType)
|
||||
|
||||
val referendumVote = if (delegators.isNotEmpty()) {
|
||||
val totalVotes = delegators.sumByBigDecimal { it.vote.totalVotes } + selfVote.totalVotes
|
||||
|
||||
val selfAsDelegator = ReferendumVoterDelegator(accountId, selfVote, metadata, identity)
|
||||
val sortedDelegators = delegators.sortedByDescending { it.vote.totalVotes }
|
||||
val delegatorsPlusSelf = sortedDelegators + selfAsDelegator
|
||||
|
||||
ReferendumVoter.Vote.WithDelegators(totalVotes, delegatorsPlusSelf)
|
||||
} else {
|
||||
ReferendumVoter.Vote.OnlySelf(selfVote)
|
||||
}
|
||||
|
||||
return ReferendumVoter(
|
||||
vote = referendumVote,
|
||||
identity = identity,
|
||||
accountId = accountId,
|
||||
metadata = metadata,
|
||||
)
|
||||
}
|
||||
|
||||
private fun ConvictionVote(accountVote: AccountVote, chainAsset: Chain.Asset, voteType: VoteType): GenericVoter.ConvictionVote {
|
||||
val amount = accountVote.amountFor(voteType)
|
||||
val conviction = accountVote.conviction()
|
||||
|
||||
return if (amount != null && conviction != null) {
|
||||
GenericVoter.ConvictionVote(chainAsset.amountFromPlanks(amount), conviction)
|
||||
} else {
|
||||
error("Expected $accountVote to contain vote of type $voteType")
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.referendum.voters
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumId
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.VoteType
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ReferendumVotersInteractor {
|
||||
|
||||
fun votersFlow(
|
||||
referendumId: ReferendumId,
|
||||
type: VoteType
|
||||
): Flow<List<ReferendumVoter>>
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_governance_api.domain.track
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackId
|
||||
|
||||
class Track(val id: TrackId, val name: String)
|
||||
|
||||
fun <V> Map<TrackId, V>.matchWith(tracks: Map<TrackId, Track>): Map<Track, V> {
|
||||
return mapKeys { (trackId, _) -> tracks.getValue(trackId) }
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package io.novafoundation.nova.feature_governance_api.presentation.referenda.common
|
||||
|
||||
import io.novafoundation.nova.feature_governance_api.domain.referendum.list.ReferendumStatusType
|
||||
|
||||
interface ReferendaStatusFormatter {
|
||||
fun formatStatus(status: ReferendumStatusType): String
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package io.novafoundation.nova.feature_governance_api.presentation.referenda.details
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.ReferendumId
|
||||
import java.math.BigInteger
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class ReferendumDetailsPayload(
|
||||
val referendumId: BigInteger,
|
||||
val allowVoting: Boolean,
|
||||
val prefilledData: PrefilledData?
|
||||
) : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
class PrefilledData(
|
||||
val referendumNumber: String,
|
||||
val title: String,
|
||||
val status: StatusData,
|
||||
val voting: VotingData?
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
class StatusData(
|
||||
val statusName: String,
|
||||
val statusColor: Int
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
class VotingData(
|
||||
val positiveFraction: Float?,
|
||||
val thresholdFraction: Float?,
|
||||
@DrawableRes val votingResultIcon: Int,
|
||||
@ColorRes val votingResultIconColor: Int,
|
||||
val thresholdInfo: String?,
|
||||
val thresholdInfoVisible: Boolean,
|
||||
val positivePercentage: String,
|
||||
val negativePercentage: String,
|
||||
val thresholdPercentage: String?,
|
||||
) : Parcelable
|
||||
}
|
||||
|
||||
fun ReferendumDetailsPayload.toReferendumId(): ReferendumId {
|
||||
return ReferendumId(referendumId)
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package io.novafoundation.nova.feature_governance_api.presentation.referenda.details.deeplink.configurators
|
||||
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.DeepLinkConfigurator
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import java.math.BigInteger
|
||||
|
||||
class ReferendumDeepLinkData(
|
||||
val chainId: String,
|
||||
val referendumId: BigInteger,
|
||||
val governanceType: Chain.Governance
|
||||
)
|
||||
|
||||
interface ReferendumDetailsDeepLinkConfigurator : DeepLinkConfigurator<ReferendumDeepLinkData> {
|
||||
|
||||
companion object {
|
||||
const val ACTION = "open"
|
||||
const val SCREEN = "gov"
|
||||
const val PREFIX = "/$ACTION/$SCREEN"
|
||||
const val CHAIN_ID_PARAM = "chainId"
|
||||
const val REFERENDUM_ID_PARAM = "id"
|
||||
const val GOVERNANCE_TYPE_PARAM = "type"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user