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,31 @@
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
|
||||
android {
|
||||
|
||||
defaultConfig {
|
||||
|
||||
|
||||
|
||||
namespace 'io.novafoundation.nova.feature_swap_api'
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation coroutinesDep
|
||||
|
||||
implementation substrateSdkDep
|
||||
implementation daggerDep
|
||||
|
||||
implementation project(':runtime')
|
||||
implementation project(':common')
|
||||
implementation project(":feature-swap-core")
|
||||
|
||||
api project(":feature-wallet-api")
|
||||
api project(":feature-account-api")
|
||||
|
||||
api project(':core-api')
|
||||
}
|
||||
Vendored
+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,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package io.novafoundation.nova.feature_swap_api.di
|
||||
|
||||
import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor
|
||||
import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService
|
||||
import io.novafoundation.nova.feature_swap_api.presentation.formatters.SwapRateFormatter
|
||||
import io.novafoundation.nova.feature_swap_api.presentation.navigation.SwapFlowScopeAggregator
|
||||
import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettingsStateProvider
|
||||
|
||||
interface SwapFeatureApi {
|
||||
|
||||
val swapService: SwapService
|
||||
|
||||
val swapSettingsStateProvider: SwapSettingsStateProvider
|
||||
|
||||
val swapAvailabilityInteractor: SwapAvailabilityInteractor
|
||||
|
||||
val swapRateFormatter: SwapRateFormatter
|
||||
|
||||
val swapFlowScopeAggregator: SwapFlowScopeAggregator
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package io.novafoundation.nova.feature_swap_api.domain.interactor
|
||||
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface SwapAvailabilityInteractor {
|
||||
|
||||
suspend fun sync(coroutineScope: CoroutineScope)
|
||||
|
||||
suspend fun warmUpCommonlyUsedChains(computationScope: CoroutineScope)
|
||||
|
||||
fun anySwapAvailableFlow(): Flow<Boolean>
|
||||
|
||||
suspend fun swapAvailableFlow(asset: Chain.Asset, coroutineScope: CoroutineScope): Flow<Boolean>
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package io.novafoundation.nova.feature_swap_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.ChainAssetIdWithAmount
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
|
||||
sealed class AtomicOperationDisplayData {
|
||||
|
||||
data class Transfer(
|
||||
val from: FullChainAssetId,
|
||||
val to: FullChainAssetId,
|
||||
val amount: Balance
|
||||
) : AtomicOperationDisplayData()
|
||||
|
||||
data class Swap(
|
||||
val from: ChainAssetIdWithAmount,
|
||||
val to: ChainAssetIdWithAmount,
|
||||
) : AtomicOperationDisplayData()
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package io.novafoundation.nova.feature_swap_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.data.model.FeeBase
|
||||
import io.novafoundation.nova.feature_swap_api.domain.model.AtomicOperationFeeDisplayData.SwapFeeComponentDisplay
|
||||
import io.novafoundation.nova.feature_swap_api.domain.model.AtomicOperationFeeDisplayData.SwapFeeType
|
||||
|
||||
class AtomicOperationFeeDisplayData(
|
||||
val components: List<SwapFeeComponentDisplay>
|
||||
) {
|
||||
|
||||
class SwapFeeComponentDisplay(
|
||||
val fees: List<FeeBase>,
|
||||
val type: SwapFeeType
|
||||
) {
|
||||
|
||||
companion object;
|
||||
}
|
||||
|
||||
enum class SwapFeeType {
|
||||
NETWORK, CROSS_CHAIN
|
||||
}
|
||||
}
|
||||
|
||||
fun SwapFeeComponentDisplay.Companion.network(vararg fee: FeeBase): SwapFeeComponentDisplay {
|
||||
return SwapFeeComponentDisplay(fee.toList(), SwapFeeType.NETWORK)
|
||||
}
|
||||
|
||||
fun SwapFeeComponentDisplay.Companion.crossChain(vararg fee: FeeBase): SwapFeeComponentDisplay {
|
||||
return SwapFeeComponentDisplay(fee.toList(), SwapFeeType.CROSS_CHAIN)
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package io.novafoundation.nova.feature_swap_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency
|
||||
import io.novafoundation.nova.feature_account_api.data.model.FeeBase
|
||||
import io.novafoundation.nova.feature_account_api.data.model.totalAmount
|
||||
import io.novafoundation.nova.feature_account_api.data.model.totalPlanksEnsuringAsset
|
||||
import io.novafoundation.nova.feature_account_api.data.signer.SubmissionHierarchy
|
||||
import io.novafoundation.nova.feature_swap_api.domain.model.fee.AtomicSwapOperationFee
|
||||
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.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
|
||||
interface AtomicSwapOperation {
|
||||
|
||||
val estimatedSwapLimit: SwapLimit
|
||||
|
||||
val assetIn: FullChainAssetId
|
||||
|
||||
val assetOut: FullChainAssetId
|
||||
|
||||
suspend fun constructDisplayData(): AtomicOperationDisplayData
|
||||
|
||||
suspend fun estimateFee(): AtomicSwapOperationFee
|
||||
|
||||
/**
|
||||
* Calculates how much of assetIn (of the current segment) is needed to buy given [extraOutAmount] of asset out (of the current segment)
|
||||
* Used to estimate how much extra amount of assetIn to add to the user input to accommodate future segment fees
|
||||
*/
|
||||
suspend fun requiredAmountInToGetAmountOut(extraOutAmount: Balance): Balance
|
||||
|
||||
/**
|
||||
* Additional amount that max amount calculation should leave aside for the **first** operation in the swap
|
||||
* One example is Existential Deposit in case operation executes in "keep alive" manner
|
||||
*/
|
||||
suspend fun additionalMaxAmountDeduction(): SwapMaxAdditionalAmountDeduction
|
||||
|
||||
suspend fun execute(args: AtomicSwapOperationSubmissionArgs): Result<SwapExecutionCorrection>
|
||||
|
||||
suspend fun submit(args: AtomicSwapOperationSubmissionArgs): Result<SwapSubmissionResult>
|
||||
}
|
||||
|
||||
class AtomicSwapOperationSubmissionArgs(
|
||||
val actualSwapLimit: SwapLimit,
|
||||
)
|
||||
|
||||
class AtomicSwapOperationArgs(
|
||||
val estimatedSwapLimit: SwapLimit,
|
||||
val feePaymentCurrency: FeePaymentCurrency,
|
||||
)
|
||||
|
||||
fun AtomicSwapOperationFee.amountToLeaveOnOriginToPayTxFees(): Balance {
|
||||
val submissionAsset = submissionFee.asset
|
||||
return submissionFee.amount + postSubmissionFees.paidByAccount.totalAmount(submissionAsset, submissionFee.submissionOrigin.executingAccount)
|
||||
}
|
||||
|
||||
fun AtomicSwapOperationFee.totalFeeEnsuringSubmissionAsset(): Balance {
|
||||
val postSubmissionFeesByAccount = postSubmissionFees.paidByAccount.totalPlanksEnsuringAsset(submissionFee.asset)
|
||||
val postSubmissionFeesFromHolding = postSubmissionFees.paidByAccount.totalPlanksEnsuringAsset(submissionFee.asset)
|
||||
|
||||
return submissionFee.amount + postSubmissionFeesByAccount + postSubmissionFeesFromHolding
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects all [FeeBase] instances from fee components
|
||||
*/
|
||||
fun AtomicSwapOperationFee.allBasicFees(): List<FeeBase> {
|
||||
return buildList {
|
||||
add(submissionFee)
|
||||
postSubmissionFees.paidByAccount.onEach(::add)
|
||||
postSubmissionFees.paidFromAmount.onEach(::add)
|
||||
}
|
||||
}
|
||||
|
||||
fun AtomicSwapOperationFee.allFeeAssets(): List<Chain.Asset> {
|
||||
return allBasicFees()
|
||||
.map { it.asset }
|
||||
.distinctBy { it.fullId }
|
||||
}
|
||||
|
||||
class SwapExecutionCorrection(
|
||||
val actualReceivedAmount: Balance
|
||||
)
|
||||
|
||||
class SwapSubmissionResult(
|
||||
val submissionHierarchy: SubmissionHierarchy
|
||||
)
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package io.novafoundation.nova.feature_swap_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import java.math.BigDecimal
|
||||
import kotlin.time.Duration
|
||||
|
||||
interface AtomicSwapOperationPrototype {
|
||||
|
||||
val fromChain: ChainId
|
||||
|
||||
/**
|
||||
* Roughly estimate fees for the current operation in native asset
|
||||
* Implementations should favour speed instead of precision as this is called for each quoting action
|
||||
*/
|
||||
suspend fun roughlyEstimateNativeFee(usdConverter: UsdConverter): BigDecimal
|
||||
|
||||
suspend fun maximumExecutionTime(): Duration
|
||||
}
|
||||
|
||||
interface UsdConverter {
|
||||
|
||||
suspend fun nativeAssetEquivalentOf(usdAmount: Double): BigDecimal
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package io.novafoundation.nova.feature_swap_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.data.model.FeeBase
|
||||
import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee
|
||||
|
||||
interface WithDebugLabel {
|
||||
val debugLabel: String
|
||||
}
|
||||
|
||||
class SubmissionFeeWithLabel(
|
||||
val fee: SubmissionFee,
|
||||
override val debugLabel: String = "Submission"
|
||||
) : WithDebugLabel, SubmissionFee by fee
|
||||
|
||||
fun SubmissionFeeWithLabel(fee: SubmissionFee?, debugLabel: String): SubmissionFeeWithLabel? {
|
||||
return fee?.let { SubmissionFeeWithLabel(it, debugLabel) }
|
||||
}
|
||||
|
||||
class FeeWithLabel(
|
||||
val fee: FeeBase,
|
||||
override val debugLabel: String
|
||||
) : WithDebugLabel, FeeBase by fee
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package io.novafoundation.nova.feature_swap_api.domain.model
|
||||
|
||||
typealias ReQuoteTrigger = Unit
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_swap_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.common.utils.Fraction
|
||||
import io.novafoundation.nova.common.utils.Fraction.Companion.percents
|
||||
|
||||
class SlippageConfig(
|
||||
val defaultSlippage: Fraction,
|
||||
val slippageTips: List<Fraction>,
|
||||
val minAvailableSlippage: Fraction,
|
||||
val maxAvailableSlippage: Fraction,
|
||||
val smallSlippage: Fraction,
|
||||
val bigSlippage: Fraction
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun default(): SlippageConfig {
|
||||
return SlippageConfig(
|
||||
defaultSlippage = 0.5.percents,
|
||||
slippageTips = listOf(0.1.percents, 0.5.percents, 1.0.percents),
|
||||
minAvailableSlippage = 0.01.percents,
|
||||
maxAvailableSlippage = 50.0.percents,
|
||||
smallSlippage = 0.05.percents,
|
||||
bigSlippage = 1.0.percents
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.novafoundation.nova.feature_swap_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.common.utils.sum
|
||||
import kotlin.time.Duration
|
||||
|
||||
class SwapExecutionEstimate(
|
||||
val atomicOperationsEstimates: List<Duration>,
|
||||
val additionalBuffer: Duration
|
||||
)
|
||||
|
||||
fun SwapExecutionEstimate.totalTime(): Duration {
|
||||
return remainingTimeWhenExecuting(stepIndex = 0)
|
||||
}
|
||||
|
||||
fun SwapExecutionEstimate.remainingTimeWhenExecuting(stepIndex: Int): Duration {
|
||||
return atomicOperationsEstimates.drop(stepIndex).sum() + additionalBuffer
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package io.novafoundation.nova.feature_swap_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.data.model.FeeBase
|
||||
import io.novafoundation.nova.feature_account_api.data.model.SubstrateFeeBase
|
||||
import io.novafoundation.nova.feature_account_api.data.model.getAmount
|
||||
import io.novafoundation.nova.feature_account_api.data.model.totalAmount
|
||||
import io.novafoundation.nova.feature_swap_api.domain.model.fee.AtomicSwapOperationFee
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.mixin.addressInput.maxAction.MaxAvailableDeduction
|
||||
import io.novafoundation.nova.runtime.ext.fullId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
class SwapFee(
|
||||
val segments: List<SwapSegment>,
|
||||
|
||||
/**
|
||||
* Fees for second and subsequent segments converted to assetIn
|
||||
*/
|
||||
val intermediateSegmentFeesInAssetIn: FeeBase,
|
||||
|
||||
/**
|
||||
* Additional deductions from max amount of asset in that are not directly caused by fees
|
||||
*/
|
||||
val additionalMaxAmountDeduction: SwapMaxAdditionalAmountDeduction,
|
||||
) : MaxAvailableDeduction {
|
||||
|
||||
data class SwapSegment(val fee: AtomicSwapOperationFee, val operation: AtomicSwapOperation)
|
||||
|
||||
val firstSegmentFee = segments.first().fee
|
||||
|
||||
val initialSubmissionFee = firstSegmentFee.submissionFee
|
||||
|
||||
private val initialPostSubmissionFees = firstSegmentFee.postSubmissionFees
|
||||
|
||||
private val assetIn = intermediateSegmentFeesInAssetIn.asset
|
||||
|
||||
// Always in asset in
|
||||
val additionalAmountForSwap = additionalAmountForSwap()
|
||||
|
||||
override fun maxAmountDeductionFor(amountAsset: Chain.Asset): Balance {
|
||||
return totalFeeAmount(amountAsset) + additionalMaxAmountDeduction(amountAsset)
|
||||
}
|
||||
|
||||
fun allBasicFees(): List<FeeBase> {
|
||||
return segments.flatMap { it.fee.allBasicFees() }
|
||||
}
|
||||
|
||||
fun totalFeeAmount(amountAsset: Chain.Asset): Balance {
|
||||
val executingAccount = initialSubmissionFee.submissionOrigin.executingAccount
|
||||
|
||||
val submissionFeeAmount = initialSubmissionFee.getAmount(amountAsset, executingAccount)
|
||||
val additionalFeesAmount = initialPostSubmissionFees.paidByAccount.totalAmount(amountAsset, executingAccount)
|
||||
|
||||
return submissionFeeAmount + additionalFeesAmount + additionalAmountForSwap.getAmount(amountAsset)
|
||||
}
|
||||
|
||||
private fun additionalMaxAmountDeduction(amountAsset: Chain.Asset): Balance {
|
||||
// TODO deducting `fromCountedTowardsEd` from max amount is over-conservative
|
||||
// Ideally we should deduct max((fromCountedTowardsEd - (countedTowardsEd - transferable)) , 0)
|
||||
return if (amountAsset.fullId == assetIn.fullId) additionalMaxAmountDeduction.fromCountedTowardsEd else Balance.ZERO
|
||||
}
|
||||
|
||||
private fun additionalAmountForSwap(): FeeBase {
|
||||
val amountTakenFromAssetIn = initialPostSubmissionFees.paidFromAmount.totalAmount(assetIn)
|
||||
val totalFutureFeeInAssetIn = amountTakenFromAssetIn + intermediateSegmentFeesInAssetIn.amount
|
||||
|
||||
return SubstrateFeeBase(totalFutureFeeInAssetIn, assetIn)
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package io.novafoundation.nova.feature_swap_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.common.utils.graph.Graph
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
|
||||
interface SwapGraphEdge : QuotableEdge {
|
||||
|
||||
/**
|
||||
* Begin a fully-constructed, ready to submit operation
|
||||
*/
|
||||
suspend fun beginOperation(args: AtomicSwapOperationArgs): AtomicSwapOperation
|
||||
|
||||
/**
|
||||
* Append current swap edge execution to the existing transaction
|
||||
* Return null if it is not possible, indicating that the new transaction should be initiated to handle this edge via
|
||||
* [beginOperation]
|
||||
*/
|
||||
suspend fun appendToOperation(currentTransaction: AtomicSwapOperation, args: AtomicSwapOperationArgs): AtomicSwapOperation?
|
||||
|
||||
/**
|
||||
* Begin a operation prototype that should reflect similar structure to [beginOperation] and [appendToOperation] but is limited to available functionality
|
||||
* Used during quoting to construct the operations array when not all parameters are still known
|
||||
*/
|
||||
suspend fun beginOperationPrototype(): AtomicSwapOperationPrototype
|
||||
|
||||
/**
|
||||
* Append current swap edge execution to the existing transaction prototype
|
||||
* Return null if it is not possible, indicating that the new transaction should be initiated to handle this edge via
|
||||
* [beginOperationPrototype]
|
||||
*/
|
||||
suspend fun appendToOperationPrototype(currentTransaction: AtomicSwapOperationPrototype): AtomicSwapOperationPrototype?
|
||||
|
||||
/**
|
||||
* Debug label to describe this edge for logging
|
||||
*/
|
||||
suspend fun debugLabel(): String
|
||||
|
||||
/**
|
||||
* Whether this Edge fee check should be skipped when adding to after a specified [predecessor]
|
||||
* The main purpose is to mirror the behavior of [appendToOperation] - multiple segments appended together
|
||||
* most likely will only use fee configuration from the first segment in the batch
|
||||
* Note that returning true here means that [canPayNonNativeFeesInIntermediatePosition] wont be called and checked
|
||||
*/
|
||||
fun predecessorHandlesFees(predecessor: SwapGraphEdge): Boolean
|
||||
|
||||
/**
|
||||
* This indicates whether this segment can be appended to the previous one to form a single transaction
|
||||
* It defaults to [predecessorHandlesFees] since ultimately they do the same thing, just under different name
|
||||
*/
|
||||
fun canAppendToPredecessor(predecessor: SwapGraphEdge): Boolean {
|
||||
return predecessorHandlesFees(predecessor)
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to define additional restrictions on top of default one, "is able to pay submission fee on origin"
|
||||
* This will only be called for intermediate hops for non-utility assets since other cases are always payable
|
||||
*/
|
||||
suspend fun canPayNonNativeFeesInIntermediatePosition(): Boolean
|
||||
|
||||
/**
|
||||
* Determines whether it is possible to spend whole asset-in to receive asset-out
|
||||
* This has to be true for edge to be considered a valid intermediate segment
|
||||
* since we don't want dust leftovers across intermediate segments
|
||||
*/
|
||||
suspend fun canTransferOutWholeAccountBalance(): Boolean
|
||||
}
|
||||
|
||||
typealias SwapGraph = Graph<FullChainAssetId, SwapGraphEdge>
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
package io.novafoundation.nova.feature_swap_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.common.utils.Fraction
|
||||
import io.novafoundation.nova.common.utils.Fraction.Companion.fractions
|
||||
import io.novafoundation.nova.common.utils.atLeastZero
|
||||
import io.novafoundation.nova.common.utils.divideToDecimal
|
||||
import io.novafoundation.nova.common.utils.isZero
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import java.math.BigDecimal
|
||||
|
||||
sealed class SwapLimit {
|
||||
|
||||
companion object;
|
||||
|
||||
data class SpecifiedIn(
|
||||
val amountIn: Balance,
|
||||
val amountOutQuote: Balance,
|
||||
val amountOutMin: Balance
|
||||
) : SwapLimit()
|
||||
|
||||
data class SpecifiedOut(
|
||||
val amountOut: Balance,
|
||||
val amountInQuote: Balance,
|
||||
val amountInMax: Balance
|
||||
) : SwapLimit()
|
||||
}
|
||||
|
||||
val SwapLimit.swapDirection: SwapDirection
|
||||
get() = when (this) {
|
||||
is SwapLimit.SpecifiedIn -> SwapDirection.SPECIFIED_IN
|
||||
is SwapLimit.SpecifiedOut -> SwapDirection.SPECIFIED_OUT
|
||||
}
|
||||
|
||||
val SwapLimit.quotedAmount: Balance
|
||||
get() = when (this) {
|
||||
is SwapLimit.SpecifiedIn -> amountIn
|
||||
is SwapLimit.SpecifiedOut -> amountOut
|
||||
}
|
||||
|
||||
val SwapLimit.estimatedAmountIn: Balance
|
||||
get() = when (this) {
|
||||
is SwapLimit.SpecifiedIn -> amountIn
|
||||
is SwapLimit.SpecifiedOut -> amountInQuote
|
||||
}
|
||||
|
||||
val SwapLimit.amountOutMin: Balance
|
||||
get() = when (this) {
|
||||
is SwapLimit.SpecifiedIn -> amountOutMin
|
||||
is SwapLimit.SpecifiedOut -> amountOut
|
||||
}
|
||||
|
||||
val SwapLimit.estimatedAmountOut: Balance
|
||||
get() = when (this) {
|
||||
is SwapLimit.SpecifiedIn -> amountOutQuote
|
||||
is SwapLimit.SpecifiedOut -> amountOut
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts SwapLimit to the [newAmountIn] based on the quoted swap rate
|
||||
* This is only suitable for small changes amount in, as it implicitly assumes the swap rate stays the same
|
||||
*/
|
||||
fun SwapLimit.replaceAmountIn(newAmountIn: Balance, shouldReplaceBuyWithSell: Boolean): SwapLimit {
|
||||
return when (this) {
|
||||
is SwapLimit.SpecifiedIn -> updateInAmount(newAmountIn)
|
||||
is SwapLimit.SpecifiedOut -> {
|
||||
if (shouldReplaceBuyWithSell) {
|
||||
updateInAmountChangingToSell(newAmountIn)
|
||||
} else {
|
||||
updateInAmount(newAmountIn)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun SwapLimit.Companion.createAggregated(firstLimit: SwapLimit, lastLimit: SwapLimit): SwapLimit {
|
||||
return when (firstLimit) {
|
||||
is SwapLimit.SpecifiedIn -> {
|
||||
require(lastLimit is SwapLimit.SpecifiedIn)
|
||||
|
||||
SwapLimit.SpecifiedIn(
|
||||
amountIn = firstLimit.amountIn,
|
||||
amountOutQuote = lastLimit.amountOutQuote,
|
||||
amountOutMin = lastLimit.amountOutMin
|
||||
)
|
||||
}
|
||||
|
||||
is SwapLimit.SpecifiedOut -> {
|
||||
require(lastLimit is SwapLimit.SpecifiedOut)
|
||||
|
||||
SwapLimit.SpecifiedOut(
|
||||
amountOut = lastLimit.amountOut,
|
||||
amountInQuote = firstLimit.amountInQuote,
|
||||
amountInMax = firstLimit.amountInMax
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun SwapLimit.SpecifiedOut.updateInAmountChangingToSell(newAmountIn: Balance): SwapLimit {
|
||||
val slippage = slippage()
|
||||
|
||||
val inferredQuotedBalance = replacedInQuoteAmount(newAmountIn, amountOut)
|
||||
|
||||
return SpecifiedIn(amount = newAmountIn, slippage, quotedBalance = inferredQuotedBalance)
|
||||
}
|
||||
|
||||
private fun SwapLimit.SpecifiedOut.slippage(): Fraction {
|
||||
if (amountInQuote.isZero) return Fraction.ZERO
|
||||
|
||||
val slippageAsFraction = (amountInMax.divideToDecimal(amountInQuote) - BigDecimal.ONE).atLeastZero()
|
||||
return slippageAsFraction.fractions
|
||||
}
|
||||
|
||||
private fun SwapLimit.SpecifiedIn.replaceInMultiplier(amount: Balance): BigDecimal {
|
||||
return amount.divideToDecimal(amountIn)
|
||||
}
|
||||
|
||||
private fun SwapLimit.SpecifiedIn.replacingInAmount(newInAmount: Balance, replacingAmount: Balance): Balance {
|
||||
return (replaceInMultiplier(replacingAmount) * newInAmount.toBigDecimal()).toBigInteger()
|
||||
}
|
||||
|
||||
private fun SwapLimit.SpecifiedIn.updateInAmount(newAmountIn: Balance): SwapLimit.SpecifiedIn {
|
||||
return SwapLimit.SpecifiedIn(
|
||||
amountIn = newAmountIn,
|
||||
amountOutQuote = replacingInAmount(newAmountIn, replacingAmount = amountOutQuote),
|
||||
amountOutMin = replacingInAmount(newAmountIn, replacingAmount = amountOutMin)
|
||||
)
|
||||
}
|
||||
|
||||
private fun SwapLimit.SpecifiedOut.replaceInQuoteMultiplier(amount: Balance): BigDecimal {
|
||||
return amount.divideToDecimal(amountInQuote)
|
||||
}
|
||||
|
||||
private fun SwapLimit.SpecifiedOut.replacedInQuoteAmount(newInQuoteAmount: Balance, replacingAmount: Balance): Balance {
|
||||
return (replaceInQuoteMultiplier(replacingAmount) * newInQuoteAmount.toBigDecimal()).toBigInteger()
|
||||
}
|
||||
|
||||
private fun SwapLimit.SpecifiedOut.updateInAmount(newAmountInQuote: Balance): SwapLimit.SpecifiedOut {
|
||||
return SwapLimit.SpecifiedOut(
|
||||
amountOut = replacedInQuoteAmount(newAmountInQuote, amountOut),
|
||||
amountInQuote = newAmountInQuote,
|
||||
amountInMax = replacedInQuoteAmount(newAmountInQuote, amountInMax)
|
||||
)
|
||||
}
|
||||
|
||||
fun SwapQuote.toExecuteArgs(slippage: Fraction, firstSegmentFees: FeePaymentCurrency): SwapFeeArgs {
|
||||
return SwapFeeArgs(
|
||||
assetIn = amountIn.chainAsset,
|
||||
slippage = slippage,
|
||||
direction = quotedPath.direction,
|
||||
executionPath = quotedPath.path.map { quotedSwapEdge -> SegmentExecuteArgs(quotedSwapEdge) },
|
||||
firstSegmentFees = firstSegmentFees
|
||||
)
|
||||
}
|
||||
|
||||
fun SwapLimit(direction: SwapDirection, amount: Balance, slippage: Fraction, quotedBalance: Balance): SwapLimit {
|
||||
return when (direction) {
|
||||
SwapDirection.SPECIFIED_IN -> SpecifiedIn(amount, slippage, quotedBalance)
|
||||
SwapDirection.SPECIFIED_OUT -> SpecifiedOut(amount, slippage, quotedBalance)
|
||||
}
|
||||
}
|
||||
|
||||
private fun SpecifiedIn(amount: Balance, slippage: Fraction, quotedBalance: Balance): SwapLimit.SpecifiedIn {
|
||||
val lessAmountCoefficient = BigDecimal.ONE - slippage.inFraction.toBigDecimal()
|
||||
val amountOutMin = quotedBalance.toBigDecimal() * lessAmountCoefficient
|
||||
|
||||
return SwapLimit.SpecifiedIn(
|
||||
amountIn = amount,
|
||||
amountOutQuote = quotedBalance,
|
||||
amountOutMin = amountOutMin.toBigInteger()
|
||||
)
|
||||
}
|
||||
|
||||
private fun SpecifiedOut(amount: Balance, slippage: Fraction, quotedBalance: Balance): SwapLimit.SpecifiedOut {
|
||||
val moreAmountCoefficient = BigDecimal.ONE + slippage.inFraction.toBigDecimal()
|
||||
val amountInMax = quotedBalance.toBigDecimal() * moreAmountCoefficient
|
||||
|
||||
return SwapLimit.SpecifiedOut(
|
||||
amountOut = amount,
|
||||
amountInQuote = quotedBalance,
|
||||
amountInMax = amountInMax.toBigInteger()
|
||||
)
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package io.novafoundation.nova.feature_swap_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
|
||||
/**
|
||||
* Deductions from account balance other than those caused by fees
|
||||
*/
|
||||
class SwapMaxAdditionalAmountDeduction(
|
||||
val fromCountedTowardsEd: Balance
|
||||
)
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package io.novafoundation.nova.feature_swap_api.domain.model
|
||||
|
||||
sealed class SwapOperationSubmissionException : Throwable() {
|
||||
|
||||
class SimulationFailed : SwapOperationSubmissionException()
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package io.novafoundation.nova.feature_swap_api.domain.model
|
||||
|
||||
sealed class SwapProgress {
|
||||
|
||||
class StepStarted(val step: SwapProgressStep) : SwapProgress()
|
||||
|
||||
class Failure(val error: Throwable, val attemptedStep: SwapProgressStep) : SwapProgress()
|
||||
|
||||
data object Done : SwapProgress()
|
||||
}
|
||||
|
||||
class SwapProgressStep(
|
||||
val index: Int,
|
||||
val displayData: AtomicOperationDisplayData,
|
||||
val operation: AtomicSwapOperation
|
||||
)
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package io.novafoundation.nova.feature_swap_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.common.utils.Fraction
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedPath
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.ChainAssetWithAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class SwapQuote(
|
||||
val amountIn: ChainAssetWithAmount,
|
||||
val amountOut: ChainAssetWithAmount,
|
||||
val priceImpact: Fraction,
|
||||
val quotedPath: QuotedPath<SwapGraphEdge>,
|
||||
val executionEstimate: SwapExecutionEstimate,
|
||||
val direction: SwapDirection,
|
||||
) {
|
||||
|
||||
val assetIn: Chain.Asset
|
||||
get() = amountIn.chainAsset
|
||||
|
||||
val assetOut: Chain.Asset
|
||||
get() = amountOut.chainAsset
|
||||
|
||||
val planksIn: Balance
|
||||
get() = amountIn.amount
|
||||
|
||||
val planksOut: Balance
|
||||
get() = amountOut.amount
|
||||
}
|
||||
|
||||
fun SwapQuote.swapRate(): BigDecimal {
|
||||
return amountIn rateAgainst amountOut
|
||||
}
|
||||
|
||||
infix fun ChainAssetWithAmount.rateAgainst(assetOut: ChainAssetWithAmount): BigDecimal {
|
||||
if (amount == Balance.ZERO) return BigDecimal.ZERO
|
||||
|
||||
val amountIn = chainAsset.amountFromPlanks(amount)
|
||||
val amountOut = assetOut.chainAsset.amountFromPlanks(assetOut.amount)
|
||||
|
||||
return amountOut / amountIn
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package io.novafoundation.nova.feature_swap_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.common.utils.Fraction
|
||||
import io.novafoundation.nova.common.utils.graph.Path
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedEdge
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Token
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
data class SwapQuoteArgs(
|
||||
val tokenIn: Token,
|
||||
val tokenOut: Token,
|
||||
val amount: Balance,
|
||||
val swapDirection: SwapDirection,
|
||||
)
|
||||
|
||||
open class SwapFeeArgs(
|
||||
val assetIn: Chain.Asset,
|
||||
val slippage: Fraction,
|
||||
val executionPath: Path<SegmentExecuteArgs>,
|
||||
val direction: SwapDirection,
|
||||
val firstSegmentFees: FeePaymentCurrency
|
||||
)
|
||||
|
||||
class SegmentExecuteArgs(
|
||||
val quotedSwapEdge: QuotedEdge<SwapGraphEdge>,
|
||||
)
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package io.novafoundation.nova.feature_swap_api.domain.model.fee
|
||||
|
||||
import io.novafoundation.nova.feature_swap_api.domain.model.AtomicOperationFeeDisplayData
|
||||
import io.novafoundation.nova.feature_swap_api.domain.model.FeeWithLabel
|
||||
import io.novafoundation.nova.feature_swap_api.domain.model.SubmissionFeeWithLabel
|
||||
|
||||
interface AtomicSwapOperationFee {
|
||||
|
||||
/**
|
||||
* Fee that is paid when submitting transaction
|
||||
*/
|
||||
val submissionFee: SubmissionFeeWithLabel
|
||||
|
||||
val postSubmissionFees: PostSubmissionFees
|
||||
|
||||
fun constructDisplayData(): AtomicOperationFeeDisplayData
|
||||
|
||||
class PostSubmissionFees(
|
||||
/**
|
||||
* Post-submission fees paid by (some) origin account.
|
||||
* This is typed as `SubmissionFee` as those fee might still use different accounts (e.g. delivery fees are always paid from requested account)
|
||||
*/
|
||||
val paidByAccount: List<SubmissionFeeWithLabel> = emptyList(),
|
||||
|
||||
/**
|
||||
* Post-submission fees paid from swapping amount directly. Its payment is isolated and does not involve any withdrawals from accounts
|
||||
*/
|
||||
val paidFromAmount: List<FeeWithLabel> = emptyList()
|
||||
)
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package io.novafoundation.nova.feature_swap_api.domain.model.fee
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee
|
||||
import io.novafoundation.nova.feature_swap_api.domain.model.AtomicOperationFeeDisplayData
|
||||
import io.novafoundation.nova.feature_swap_api.domain.model.AtomicOperationFeeDisplayData.SwapFeeComponentDisplay
|
||||
import io.novafoundation.nova.feature_swap_api.domain.model.SubmissionFeeWithLabel
|
||||
import io.novafoundation.nova.feature_swap_api.domain.model.fee.AtomicSwapOperationFee.PostSubmissionFees
|
||||
|
||||
class SubmissionOnlyAtomicSwapOperationFee(submissionFee: SubmissionFee) : AtomicSwapOperationFee {
|
||||
|
||||
override val submissionFee: SubmissionFeeWithLabel = SubmissionFeeWithLabel(submissionFee)
|
||||
|
||||
override val postSubmissionFees: PostSubmissionFees = PostSubmissionFees()
|
||||
|
||||
override fun constructDisplayData(): AtomicOperationFeeDisplayData {
|
||||
return AtomicOperationFeeDisplayData(
|
||||
components = listOf(
|
||||
SwapFeeComponentDisplay(
|
||||
type = AtomicOperationFeeDisplayData.SwapFeeType.NETWORK,
|
||||
fees = listOf(submissionFee)
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package io.novafoundation.nova.feature_swap_api.domain.swap
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger
|
||||
import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig
|
||||
import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee
|
||||
import io.novafoundation.nova.feature_swap_api.domain.model.SwapFeeArgs
|
||||
import io.novafoundation.nova.feature_swap_api.domain.model.SwapProgress
|
||||
import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote
|
||||
import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs
|
||||
import io.novafoundation.nova.feature_swap_api.domain.model.SwapSubmissionResult
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface SwapService {
|
||||
|
||||
suspend fun warmUpCommonChains(computationScope: CoroutineScope): Result<Unit>
|
||||
|
||||
suspend fun sync(coroutineScope: CoroutineScope)
|
||||
|
||||
suspend fun assetsAvailableForSwap(computationScope: CoroutineScope): Flow<Set<FullChainAssetId>>
|
||||
|
||||
suspend fun availableSwapDirectionsFor(asset: Chain.Asset, computationScope: CoroutineScope): Flow<Set<FullChainAssetId>>
|
||||
|
||||
suspend fun hasAvailableSwapDirections(asset: Chain.Asset, computationScope: CoroutineScope): Flow<Boolean>
|
||||
|
||||
suspend fun quote(
|
||||
args: SwapQuoteArgs,
|
||||
computationSharingScope: CoroutineScope
|
||||
): Result<SwapQuote>
|
||||
|
||||
suspend fun estimateFee(executeArgs: SwapFeeArgs): SwapFee
|
||||
|
||||
suspend fun swap(calculatedFee: SwapFee): Flow<SwapProgress>
|
||||
|
||||
suspend fun submitFirstSwapStep(calculatedFee: SwapFee): Result<SwapSubmissionResult>
|
||||
|
||||
suspend fun defaultSlippageConfig(chainId: ChainId): SlippageConfig
|
||||
|
||||
fun runSubscriptions(metaAccount: MetaAccount): Flow<ReQuoteTrigger>
|
||||
|
||||
suspend fun isDeepSwapAllowed(): Boolean
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_swap_api.presentation.formatters
|
||||
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import java.math.BigDecimal
|
||||
|
||||
interface SwapRateFormatter {
|
||||
|
||||
fun format(rate: BigDecimal, assetIn: Chain.Asset, assetOut: Chain.Asset): String
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package io.novafoundation.nova.feature_swap_api.presentation.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
enum class SwapDirectionParcel : Parcelable {
|
||||
SPECIFIED_IN,
|
||||
SPECIFIED_OUT
|
||||
}
|
||||
|
||||
fun SwapDirectionParcel.mapFromModel(): SwapDirection {
|
||||
return when (this) {
|
||||
SwapDirectionParcel.SPECIFIED_IN -> SwapDirection.SPECIFIED_IN
|
||||
SwapDirectionParcel.SPECIFIED_OUT -> SwapDirection.SPECIFIED_OUT
|
||||
}
|
||||
}
|
||||
|
||||
fun SwapDirection.toParcel(): SwapDirectionParcel {
|
||||
return when (this) {
|
||||
SwapDirection.SPECIFIED_IN -> SwapDirectionParcel.SPECIFIED_IN
|
||||
SwapDirection.SPECIFIED_OUT -> SwapDirectionParcel.SPECIFIED_OUT
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package io.novafoundation.nova.feature_swap_api.presentation.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload
|
||||
|
||||
sealed interface SwapSettingsPayload : Parcelable {
|
||||
|
||||
val assetIn: AssetPayload
|
||||
|
||||
@Parcelize
|
||||
class DefaultFlow(override val assetIn: AssetPayload) : SwapSettingsPayload
|
||||
|
||||
@Parcelize
|
||||
class RepeatOperation(
|
||||
override val assetIn: AssetPayload,
|
||||
val assetOut: AssetPayload,
|
||||
val amount: Balance,
|
||||
val direction: SwapDirectionParcel
|
||||
) : SwapSettingsPayload
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package io.novafoundation.nova.feature_swap_api.presentation.navigation
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
interface SwapFlowScopeAggregator {
|
||||
|
||||
fun getFlowScope(screenScope: CoroutineScope): CoroutineScope
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.novafoundation.nova.feature_swap_api.presentation.state
|
||||
|
||||
import io.novafoundation.nova.common.utils.Fraction
|
||||
import io.novafoundation.nova.common.utils.Fraction.Companion.percents
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
val DEFAULT_SLIPPAGE = 0.5.percents
|
||||
|
||||
data class SwapSettings(
|
||||
val assetIn: Chain.Asset? = null,
|
||||
val assetOut: Chain.Asset? = null,
|
||||
val amount: Balance? = null,
|
||||
val swapDirection: SwapDirection? = null,
|
||||
val slippage: Fraction
|
||||
)
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package io.novafoundation.nova.feature_swap_api.presentation.state
|
||||
|
||||
import io.novafoundation.nova.common.utils.Fraction
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.state.SelectedOptionSharedState
|
||||
|
||||
interface SwapSettingsState : SelectedOptionSharedState<SwapSettings> {
|
||||
|
||||
suspend fun setAssetIn(asset: Chain.Asset)
|
||||
|
||||
fun setAssetOut(asset: Chain.Asset)
|
||||
|
||||
fun setAmount(amount: Balance?, swapDirection: SwapDirection)
|
||||
|
||||
fun setSlippage(slippage: Fraction)
|
||||
|
||||
suspend fun flipAssets(): SwapSettings
|
||||
|
||||
fun setSwapSettings(swapSettings: SwapSettings)
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package io.novafoundation.nova.feature_swap_api.presentation.state
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
interface SwapSettingsStateProvider {
|
||||
|
||||
suspend fun getSwapSettingsState(coroutineScope: CoroutineScope): SwapSettingsState
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package io.novafoundation.nova.feature_swap_api.presentation.view
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.utils.WithContextExtensions
|
||||
import io.novafoundation.nova.common.utils.images.Icon
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.common.utils.setTextColorRes
|
||||
import io.novafoundation.nova.common.utils.setTextOrHide
|
||||
import io.novafoundation.nova.common.view.shape.getInputBackground
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.loadChainIcon
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.setTokenIcon
|
||||
import io.novafoundation.nova.feature_swap_api.R
|
||||
import io.novafoundation.nova.feature_swap_api.databinding.ViewSwapAssetBinding
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountModel
|
||||
|
||||
class SwapAssetView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : ConstraintLayout(context, attrs, defStyleAttr),
|
||||
WithContextExtensions by WithContextExtensions(context) {
|
||||
|
||||
private val binder = ViewSwapAssetBinding.inflate(inflater(), this)
|
||||
|
||||
private val imageLoader: ImageLoader by lazy(LazyThreadSafetyMode.NONE) {
|
||||
FeatureUtils.getCommonApi(context).imageLoader()
|
||||
}
|
||||
|
||||
init {
|
||||
background = context.getInputBackground()
|
||||
}
|
||||
|
||||
fun setModel(model: Model) {
|
||||
setAssetImageUrl(model.assetIcon)
|
||||
setAmount(model.amount)
|
||||
setNetwork(model.chainUi)
|
||||
binder.swapAssetAmount.setTextColorRes(model.amountTextColorRes)
|
||||
}
|
||||
|
||||
private fun setAssetImageUrl(icon: Icon) {
|
||||
binder.swapAssetImage.setTokenIcon(icon, imageLoader)
|
||||
binder.swapAssetImage.setBackgroundResource(R.drawable.bg_token_container)
|
||||
}
|
||||
|
||||
private fun setAmount(amount: AmountModel) {
|
||||
binder.swapAssetAmount.text = amount.token
|
||||
binder.swapAssetFiat.setTextOrHide(amount.fiat)
|
||||
}
|
||||
|
||||
private fun setNetwork(chainUi: ChainUi) {
|
||||
binder.swapAssetNetworkImage.loadChainIcon(chainUi.icon, imageLoader)
|
||||
binder.swapAssetNetwork.text = chainUi.name
|
||||
}
|
||||
|
||||
class Model(
|
||||
val assetIcon: Icon,
|
||||
val amount: AmountModel,
|
||||
val chainUi: ChainUi,
|
||||
@ColorRes val amountTextColorRes: Int = R.color.text_primary
|
||||
)
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package io.novafoundation.nova.feature_swap_api.presentation.view
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.feature_swap_api.databinding.ViewSwapAssetsBinding
|
||||
|
||||
class SwapAssetsView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : ConstraintLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private val binder = ViewSwapAssetsBinding.inflate(inflater(), this)
|
||||
|
||||
fun setModel(model: Model) {
|
||||
setAssetIn(model.assetIn)
|
||||
setAssetOut(model.assetOut)
|
||||
}
|
||||
|
||||
fun setAssetIn(model: SwapAssetView.Model) {
|
||||
binder.viewSwapAssetsIn.setModel(model)
|
||||
}
|
||||
|
||||
fun setAssetOut(model: SwapAssetView.Model) {
|
||||
binder.viewSwapAssetsOut.setModel(model)
|
||||
}
|
||||
|
||||
class Model(
|
||||
val assetIn: SwapAssetView.Model,
|
||||
val assetOut: SwapAssetView.Model
|
||||
)
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package io.novafoundation.nova.feature_swap_api.presentation.view.bottomSheet.description
|
||||
|
||||
import io.novafoundation.nova.common.view.bottomSheet.description.DescriptionBottomSheetLauncher
|
||||
import io.novafoundation.nova.feature_swap_api.R
|
||||
|
||||
fun DescriptionBottomSheetLauncher.launchSwapRateDescription() {
|
||||
launchDescriptionBottomSheet(
|
||||
titleRes = R.string.swap_rate_title,
|
||||
descriptionRes = R.string.swap_rate_description
|
||||
)
|
||||
}
|
||||
|
||||
fun DescriptionBottomSheetLauncher.launchPriceDifferenceDescription() {
|
||||
launchDescriptionBottomSheet(
|
||||
titleRes = R.string.swap_price_difference_title,
|
||||
descriptionRes = R.string.swap_price_difference_description
|
||||
)
|
||||
}
|
||||
|
||||
fun DescriptionBottomSheetLauncher.launchSlippageDescription() {
|
||||
launchDescriptionBottomSheet(
|
||||
titleRes = R.string.swap_slippage_title,
|
||||
descriptionRes = R.string.swap_slippage_description
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:background="@color/divider"
|
||||
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/swapAssetImage"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/bg_token_container"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/ic_token_dot_colored" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/swapAssetAmount"
|
||||
style="@style/TextAppearance.NovaFoundation.SemiBold.Body"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:ellipsize="middle"
|
||||
android:includeFontPadding="false"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/text_primary"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/swapAssetImage"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="50.79 USDT" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/swapAssetFiat"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:ellipsize="middle"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/text_secondary"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/swapAssetAmount"
|
||||
tools:text="135.87$" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="12dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/swapAssetFiat"
|
||||
app:layout_goneMarginTop="36dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/swapAssetNetworkImage"
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="12dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/swapAssetNetwork"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:src="@drawable/ic_pezkuwi_logo" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/swapAssetNetwork"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:lines="1"
|
||||
android:textColor="@color/text_secondary"
|
||||
tools:text="Polkadot Asset Hub" />
|
||||
|
||||
</LinearLayout>
|
||||
</merge>
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
||||
|
||||
<io.novafoundation.nova.feature_swap_api.presentation.view.SwapAssetView
|
||||
android:id="@+id/viewSwapAssetsIn"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/viewSwapAssetsOut"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<io.novafoundation.nova.feature_swap_api.presentation.view.SwapAssetView
|
||||
android:id="@+id/viewSwapAssetsOut"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/viewSwapAssetsIn"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:background="@drawable/bg_secondary_background_circle"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_arrow_right"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/viewSwapAssetsOut"
|
||||
app:layout_constraintStart_toEndOf="@+id/viewSwapAssetsIn"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="@color/icon_secondary" />
|
||||
|
||||
</merge>
|
||||
Reference in New Issue
Block a user