Initial commit: Pezkuwi Wallet Android

Security hardened release:
- Code obfuscation enabled (minifyEnabled=true, shrinkResources=true)
- Sensitive files excluded (google-services.json, keystores)
- Branch.io key moved to BuildConfig placeholder
- Updated dependencies: OkHttp 4.12.0, Gson 2.10.1, BouncyCastle 1.77
- Comprehensive ProGuard rules for crypto wallet
- Navigation 2.7.7, Lifecycle 2.7.0, ConstraintLayout 2.1.4
This commit is contained in:
2026-02-12 05:19:41 +03:00
commit a294aa1a6b
7687 changed files with 441811 additions and 0 deletions
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -0,0 +1,30 @@
package io.novafoundation.nova.feature_swap_core_api.data.network
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import java.math.BigInteger
typealias HydraDxAssetId = BigInteger
typealias HydraRemoteToLocalMapping = Map<HydraDxAssetId, Chain.Asset>
interface HydraDxAssetIdConverter {
val systemAssetId: HydraDxAssetId
suspend fun toOnChainIdOrNull(chainAsset: Chain.Asset): HydraDxAssetId?
suspend fun toChainAssetOrNull(chain: Chain, onChainId: HydraDxAssetId): Chain.Asset?
suspend fun allOnChainIds(chain: Chain): HydraRemoteToLocalMapping
}
fun HydraDxAssetIdConverter.isSystemAsset(assetId: HydraDxAssetId): Boolean {
return assetId == systemAssetId
}
suspend fun HydraDxAssetIdConverter.toOnChainIdOrThrow(chainAsset: Chain.Asset): HydraDxAssetId {
return requireNotNull(toOnChainIdOrNull(chainAsset))
}
suspend fun HydraDxAssetIdConverter.toChainAssetOrThrow(chain: Chain, onChainId: HydraDxAssetId): Chain.Asset {
return requireNotNull(toChainAssetOrNull(chain, onChainId))
}
@@ -0,0 +1,10 @@
package io.novafoundation.nova.feature_swap_core_api.data.paths
import io.novafoundation.nova.common.utils.graph.Path
import io.novafoundation.nova.feature_swap_core_api.data.paths.model.PathRoughFeeEstimation
import io.novafoundation.nova.feature_swap_core_api.data.paths.model.QuotedEdge
interface PathFeeEstimator<E> {
suspend fun roughlyEstimateFee(path: Path<QuotedEdge<E>>): PathRoughFeeEstimation
}
@@ -0,0 +1,32 @@
package io.novafoundation.nova.feature_swap_core_api.data.paths
import io.novafoundation.nova.common.utils.graph.EdgeVisitFilter
import io.novafoundation.nova.common.utils.graph.Graph
import io.novafoundation.nova.feature_swap_core_api.data.paths.model.BestPathQuote
import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge
import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import java.math.BigInteger
interface PathQuoter<E : QuotableEdge> {
interface Factory {
fun <E : QuotableEdge> create(
graphFlow: Flow<Graph<FullChainAssetId, E>>,
computationalScope: CoroutineScope,
pathFeeEstimation: PathFeeEstimator<E>? = null,
filter: EdgeVisitFilter<E>? = null
): PathQuoter<E>
}
suspend fun findBestPath(
chainAssetIn: Chain.Asset,
chainAssetOut: Chain.Asset,
amount: BigInteger,
swapDirection: SwapDirection,
): BestPathQuote<E>
}
@@ -0,0 +1,8 @@
package io.novafoundation.nova.feature_swap_core_api.data.paths.model
class BestPathQuote<E>(
val candidates: List<QuotedPath<E>>
) {
val bestPath: QuotedPath<E> = candidates.max()
}
@@ -0,0 +1,14 @@
package io.novafoundation.nova.feature_swap_core_api.data.paths.model
import io.novafoundation.nova.common.data.network.runtime.binding.BalanceOf
import java.math.BigInteger
class PathRoughFeeEstimation(val inAssetOut: BalanceOf, val inAssetIn: BalanceOf) {
companion object {
fun zero(): PathRoughFeeEstimation {
return PathRoughFeeEstimation(BigInteger.ZERO, BigInteger.ZERO)
}
}
}
@@ -0,0 +1,9 @@
package io.novafoundation.nova.feature_swap_core_api.data.paths.model
import java.math.BigInteger
class QuotedEdge<E>(
val quotedAmount: BigInteger,
val quote: BigInteger,
val edge: E
)
@@ -0,0 +1,74 @@
package io.novafoundation.nova.feature_swap_core_api.data.paths.model
import io.novafoundation.nova.common.utils.graph.Path
import io.novafoundation.nova.common.utils.graph.WeightedEdge
import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.SwapDirection
import java.math.BigInteger
class QuotedPath<E>(
val direction: SwapDirection,
val path: Path<QuotedEdge<E>>,
val roughFeeEstimation: PathRoughFeeEstimation,
) : Comparable<QuotedPath<E>> {
private val amountOutAfterFees: BigInteger = lastSegmentQuote - roughFeeEstimation.inAssetOut
private val amountInAfterFees: BigInteger = firstSegmentQuote + roughFeeEstimation.inAssetIn
override fun compareTo(other: QuotedPath<E>): Int {
return when (direction) {
// When we want to sell a token, the bigger the quote - the better
SwapDirection.SPECIFIED_IN -> (amountOutAfterFees - other.amountOutAfterFees).signum()
// When we want to buy a token, the smaller the quote - the better
SwapDirection.SPECIFIED_OUT -> (other.amountInAfterFees - amountInAfterFees).signum()
}
}
}
class WeightBreakdown private constructor(
val individualWeights: List<Int>,
val total: Int
) {
companion object {
fun <N, E : WeightedEdge<N>> fromQuotedPath(path: QuotedPath<E>): WeightBreakdown {
val weightedPath = mutableListOf<E>()
val individualWeights = mutableListOf<Int>()
var weight = 0
path.path.forEach { quotedEdge ->
val edgeWeight = quotedEdge.edge.weightForAppendingTo(weightedPath)
weight += edgeWeight
weightedPath += quotedEdge.edge
individualWeights += edgeWeight
}
return WeightBreakdown(individualWeights, weight)
}
}
}
val QuotedPath<*>.quote: BigInteger
get() = when (direction) {
SwapDirection.SPECIFIED_IN -> lastSegmentQuote
SwapDirection.SPECIFIED_OUT -> firstSegmentQuote
}
val QuotedPath<*>.quotedAmount: BigInteger
get() = when (direction) {
SwapDirection.SPECIFIED_IN -> firstSegmentQuotedAmount
SwapDirection.SPECIFIED_OUT -> lastSegmentQuotedAmount
}
val QuotedPath<*>.lastSegmentQuotedAmount: BigInteger
get() = path.last().quotedAmount
val QuotedPath<*>.lastSegmentQuote: BigInteger
get() = path.last().quote
val QuotedPath<*>.firstSegmentQuote: BigInteger
get() = path.first().quote
val QuotedPath<*>.firstSegmentQuotedAmount: BigInteger
get() = path.first().quotedAmount
@@ -0,0 +1,29 @@
package io.novafoundation.nova.feature_swap_core_api.data.primitive
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge
import io.novasama.substrate_sdk_android.runtime.AccountId
import kotlinx.coroutines.flow.Flow
interface SwapQuoting {
interface QuotingHost {
val sharedSubscriptions: SwapQuotingSubscriptions
}
/**
* Perform initial data sync needed to later perform [runSubscriptions]
* This is separated from [runSubscriptions] since [runSubscriptions] might be io-intense
* [sync] should be sufficient for [availableSwapDirections] to work
* whereas [runSubscriptions] should enable [QuotableEdge.quote] method to work
*/
suspend fun sync()
suspend fun availableSwapDirections(): List<QuotableEdge>
suspend fun runSubscriptions(
userAccountId: AccountId,
subscriptionBuilder: SharedRequestsBuilder
): Flow<Unit>
}
@@ -0,0 +1,10 @@
package io.novafoundation.nova.feature_swap_core_api.data.primitive
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import kotlinx.coroutines.flow.Flow
interface SwapQuotingSubscriptions {
suspend fun blockNumber(chainId: ChainId): Flow<BlockNumber>
}
@@ -0,0 +1,6 @@
package io.novafoundation.nova.feature_swap_core_api.data.primitive.errors
sealed class SwapQuoteException : Exception() {
object NotEnoughLiquidity : SwapQuoteException()
}
@@ -0,0 +1,18 @@
package io.novafoundation.nova.feature_swap_core_api.data.primitive.model
import io.novafoundation.nova.common.utils.graph.WeightedEdge
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
import java.math.BigInteger
interface QuotableEdge : WeightedEdge<FullChainAssetId> {
companion object {
// Allow [0..100] precision for smaller weights
const val DEFAULT_SEGMENT_WEIGHT = 100
}
suspend fun quote(
amount: BigInteger,
direction: SwapDirection
): BigInteger
}
@@ -0,0 +1,12 @@
package io.novafoundation.nova.feature_swap_core_api.data.primitive.model
enum class SwapDirection {
SPECIFIED_IN, SPECIFIED_OUT
}
fun SwapDirection.flip(): SwapDirection {
return when (this) {
SwapDirection.SPECIFIED_IN -> SwapDirection.SPECIFIED_OUT
SwapDirection.SPECIFIED_OUT -> SwapDirection.SPECIFIED_IN
}
}
@@ -0,0 +1,14 @@
package io.novafoundation.nova.feature_swap_core_api.data.types.hydra
import io.novafoundation.nova.feature_swap_core_api.data.primitive.SwapQuoting
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
interface HydraDxQuoting : SwapQuoting {
interface Factory {
fun create(chain: Chain, host: SwapQuoting.QuotingHost): HydraDxQuoting
}
fun getSource(id: String): HydraDxQuotingSource<*>
}
@@ -0,0 +1,26 @@
package io.novafoundation.nova.feature_swap_core_api.data.types.hydra
import io.novafoundation.nova.common.utils.Identifiable
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
import io.novafoundation.nova.feature_swap_core_api.data.primitive.SwapQuoting
import io.novafoundation.nova.feature_swap_core_api.data.primitive.model.QuotableEdge
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.runtime.AccountId
import kotlinx.coroutines.flow.Flow
interface HydraDxQuotingSource<E : QuotableEdge> : Identifiable {
suspend fun sync()
suspend fun availableSwapDirections(): Collection<E>
suspend fun runSubscriptions(
userAccountId: AccountId,
subscriptionBuilder: SharedRequestsBuilder
): Flow<Unit>
interface Factory<S : HydraDxQuotingSource<*>> {
fun create(chain: Chain, host: SwapQuoting.QuotingHost): S
}
}
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_swap_core_api.data.types.hydra
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId
interface HydrationAcceptedFeeCurrenciesFetcher {
suspend fun fetchAcceptedFeeCurrencies(chain: Chain): Result<Set<ChainAssetId>>
suspend fun isAcceptedCurrency(chainAsset: Chain.Asset): Result<Boolean>
}
@@ -0,0 +1,12 @@
package io.novafoundation.nova.feature_swap_core_api.data.types.hydra
import io.novafoundation.nova.common.data.network.runtime.binding.BalanceOf
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
interface HydrationPriceConversionFallback {
suspend fun convertNativeAmount(
amount: BalanceOf,
conversionTarget: Chain.Asset
): BalanceOf
}
@@ -0,0 +1,20 @@
package io.novafoundation.nova.feature_swap_core_api.di
import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter
import io.novafoundation.nova.feature_swap_core_api.data.paths.PathQuoter
import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydraDxQuoting
import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydrationAcceptedFeeCurrenciesFetcher
import io.novafoundation.nova.feature_swap_core_api.data.types.hydra.HydrationPriceConversionFallback
interface SwapCoreApi {
val hydrationAcceptedFeeCurrenciesFetcher: HydrationAcceptedFeeCurrenciesFetcher
val hydraDxQuotingFactory: HydraDxQuoting.Factory
val hydraDxAssetIdConverter: HydraDxAssetIdConverter
val hydrationPriceConversionFallback: HydrationPriceConversionFallback
val pathQuoterFactory: PathQuoter.Factory
}