Initial commit: Pezkuwi Wallet Android

Security hardened release:
- Code obfuscation enabled (minifyEnabled=true, shrinkResources=true)
- Sensitive files excluded (google-services.json, keystores)
- Branch.io key moved to BuildConfig placeholder
- Updated dependencies: OkHttp 4.12.0, Gson 2.10.1, BouncyCastle 1.77
- Comprehensive ProGuard rules for crypto wallet
- Navigation 2.7.7, Lifecycle 2.7.0, ConstraintLayout 2.1.4
This commit is contained in:
2026-02-12 05:19:41 +03:00
commit a294aa1a6b
7687 changed files with 441811 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+24
View File
@@ -0,0 +1,24 @@
apply plugin: 'kotlin-parcelize'
android {
namespace 'io.novafoundation.nova.feature_external_sign_api'
buildFeatures {
viewBinding true
}
}
dependencies {
implementation project(':feature-account-api')
implementation project(':common')
implementation coroutinesDep
implementation androidDep
implementation materialDep
implementation constraintDep
testImplementation jUnitDep
testImplementation mockitoDep
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -0,0 +1,8 @@
package io.novafoundation.nova.feature_external_sign_api.di
import io.novafoundation.nova.feature_external_sign_api.domain.sign.evm.EvmTypedMessageParser
interface ExternalSignFeatureApi {
val evmTypedMessageParser: EvmTypedMessageParser
}
@@ -0,0 +1,8 @@
package io.novafoundation.nova.feature_external_sign_api.domain.sign.evm
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmTypedMessage
interface EvmTypedMessageParser {
fun parseEvmTypedMessage(message: String): EvmTypedMessage
}
@@ -0,0 +1,46 @@
package io.novafoundation.nova.feature_external_sign_api.model
import android.os.Parcelable
import io.novafoundation.nova.common.base.errors.SigningCancelledException
import io.novafoundation.nova.common.navigation.InterScreenRequester
import io.novafoundation.nova.common.navigation.InterScreenResponder
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator.Response
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignPayload
import kotlinx.parcelize.Parcelize
import kotlinx.coroutines.flow.first
interface ExternalSignRequester : InterScreenRequester<ExternalSignPayload, Response>
interface ExternalSignResponder : InterScreenResponder<ExternalSignPayload, Response>
interface ExternalSignCommunicator : ExternalSignRequester, ExternalSignResponder {
sealed class Response : Parcelable {
abstract val requestId: String
@Parcelize
class Rejected(override val requestId: String) : Response()
@Parcelize
class Signed(override val requestId: String, val signature: String, val modifiedTransaction: String? = null) : Response()
@Parcelize
class Sent(override val requestId: String, val txHash: String) : Response()
@Parcelize
class SigningFailed(override val requestId: String, val shouldPresent: Boolean = true) : Response()
}
}
suspend fun ExternalSignRequester.awaitConfirmation(request: ExternalSignPayload): Response {
openRequest(request)
return responseFlow.first { it.requestId == request.signRequest.id }
}
fun Throwable.failedSigningIfNotCancelled(requestId: String) = if (this is SigningCancelledException) {
null
} else {
Response.SigningFailed(requestId)
}
@@ -0,0 +1,40 @@
package io.novafoundation.nova.feature_external_sign_api.model.signPayload
import android.os.Parcelable
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmSignPayload
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.polkadot.PolkadotSignPayload
import kotlinx.parcelize.Parcelize
@Parcelize
class ExternalSignPayload(
val signRequest: ExternalSignRequest,
val dappMetadata: SigningDappMetadata?,
val wallet: ExternalSignWallet
) : Parcelable
@Parcelize
class SigningDappMetadata(
val icon: String?,
val name: String?,
val url: String
) : Parcelable
sealed class ExternalSignWallet : Parcelable {
@Parcelize
object Current : ExternalSignWallet()
@Parcelize
class WithId(val metaId: Long) : ExternalSignWallet()
}
sealed interface ExternalSignRequest : Parcelable {
val id: String
@Parcelize
class Polkadot(override val id: String, val payload: PolkadotSignPayload) : ExternalSignRequest
@Parcelize
class Evm(override val id: String, val payload: EvmSignPayload) : ExternalSignRequest
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm
import android.os.Parcelable
import io.novafoundation.nova.common.utils.Precision
import io.novafoundation.nova.common.utils.TokenSymbol
import kotlinx.parcelize.Parcelize
@Parcelize
data class EvmChain(
val chainId: String,
val chainName: String,
val nativeCurrency: NativeCurrency,
val rpcUrl: String,
val iconUrl: String?
) : Parcelable {
@Parcelize
class NativeCurrency(
val name: String,
val symbol: TokenSymbol,
val decimals: Precision
) : Parcelable
}
@@ -0,0 +1,9 @@
package io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
class EvmPersonalSignMessage(
val data: String
) : Parcelable
@@ -0,0 +1,47 @@
package io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
sealed class EvmSignPayload : Parcelable {
abstract val originAddress: String
@Parcelize
class ConfirmTx(
val transaction: EvmTransaction,
override val originAddress: String,
val chainSource: EvmChainSource,
val action: Action,
) : EvmSignPayload() {
enum class Action {
SIGN, SEND
}
}
@Parcelize
class SignTypedMessage(
val message: EvmTypedMessage,
override val originAddress: String,
) : EvmSignPayload()
@Parcelize
class PersonalSign(
val message: EvmPersonalSignMessage,
override val originAddress: String,
) : EvmSignPayload()
}
@Parcelize
class EvmChainSource(val evmChainId: Int, val unknownChainOptions: UnknownChainOptions) : Parcelable {
sealed class UnknownChainOptions : Parcelable {
@Parcelize
object MustBeKnown : UnknownChainOptions()
@Parcelize
class WithFallBack(val evmChain: EvmChain) : UnknownChainOptions()
}
}
@@ -0,0 +1,20 @@
package io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
sealed class EvmTransaction : Parcelable {
@Parcelize
class Struct(
val gas: String?,
val gasPrice: String?,
val from: String,
val to: String,
val data: String?,
val value: String?,
val nonce: String?,
) : EvmTransaction()
@Parcelize
class Raw(val rawContent: String) : EvmTransaction()
}
@@ -0,0 +1,10 @@
package io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
class EvmTypedMessage(
val data: String,
val raw: String?
) : Parcelable
@@ -0,0 +1,39 @@
package io.novafoundation.nova.feature_external_sign_api.model.signPayload.polkadot
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
sealed class PolkadotSignPayload : Parcelable {
abstract val address: String
@Parcelize
class Json(
override val address: String,
val blockHash: String,
val blockNumber: String,
val era: String,
val genesisHash: String,
val method: String,
val nonce: String,
val specVersion: String,
val tip: String,
val transactionVersion: String,
val metadataHash: String?,
val withSignedTransaction: Boolean?,
val signedExtensions: List<String>,
val assetId: String?,
val version: Int
) : PolkadotSignPayload()
@Parcelize
class Raw(
val data: String,
override val address: String,
val type: String?
) : PolkadotSignPayload()
}
fun PolkadotSignPayload.maybeSignExtrinsic(): PolkadotSignPayload.Json? = this as? PolkadotSignPayload.Json
fun PolkadotSignPayload.genesisHash(): String? = (this as? PolkadotSignPayload.Json)?.genesisHash
@@ -0,0 +1,3 @@
package io.novafoundation.nova.feature_external_sign_api.model.signPayload.polkadot
class PolkadotSignerResult(val id: String, val signature: String, val signedTransaction: String?)
@@ -0,0 +1,16 @@
package io.novafoundation.nova.feature_external_sign_api.presentation.dapp
import android.widget.ImageView
import coil.ImageLoader
import coil.load
import io.novafoundation.nova.feature_external_sign_api.R
fun ImageView.showDAppIcon(
url: String?,
imageLoader: ImageLoader
) {
load(url, imageLoader) {
fallback(R.drawable.ic_earth)
error(R.drawable.ic_earth)
}
}
@@ -0,0 +1,66 @@
package io.novafoundation.nova.feature_external_sign_api.presentation.externalSign
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import coil.ImageLoader
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.utils.postToSelf
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
import io.novafoundation.nova.common.utils.sequrity.awaitInteractionAllowed
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletModel
import io.novafoundation.nova.feature_account_api.view.showWallet
import io.novafoundation.nova.feature_external_sign_api.databinding.BottomSheetConfirmAuthorizeBinding
import io.novafoundation.nova.feature_external_sign_api.presentation.dapp.showDAppIcon
import kotlinx.coroutines.launch
class AuthorizeDappBottomSheet(
context: Context,
private val payload: Payload,
onConfirm: () -> Unit,
onDeny: () -> Unit,
) : ConfirmDAppActionBottomSheet(
context = context,
onConfirm = onConfirm,
onDeny = onDeny
) {
class Payload(
val dAppUrl: String,
val title: String,
val dAppIconUrl: String?,
val walletModel: WalletModel,
)
private val interactionGate: AutomaticInteractionGate
private val imageLoader: ImageLoader
override val contentBinder = BottomSheetConfirmAuthorizeBinding.inflate(LayoutInflater.from(context))
init {
FeatureUtils.getCommonApi(context).let { api ->
interactionGate = api.automaticInteractionGate
imageLoader = api.imageLoader()
}
}
override fun show() {
launch {
interactionGate.awaitInteractionAllowed()
super.show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
contentBinder.confirmAuthorizeDappIcon.showDAppIcon(payload.dAppIconUrl, imageLoader)
contentBinder.confirmAuthorizeDappWallet.postToSelf { showWallet(payload.walletModel) }
contentBinder.confirmAuthorizeDappTitle.text = payload.title
contentBinder.confirmAuthorizeDappDApp.postToSelf { showValue(payload.dAppUrl) }
}
}
@@ -0,0 +1,33 @@
package io.novafoundation.nova.feature_external_sign_api.presentation.externalSign
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import androidx.annotation.CallSuper
import androidx.viewbinding.ViewBinding
import io.novafoundation.nova.common.utils.DialogExtensions
import io.novafoundation.nova.common.view.bottomSheet.BaseBottomSheet
import io.novafoundation.nova.feature_external_sign_api.databinding.BottomSheetConfirmDappActionBinding
abstract class ConfirmDAppActionBottomSheet(
context: Context,
private val onConfirm: () -> Unit,
private val onDeny: () -> Unit
) : BaseBottomSheet<BottomSheetConfirmDappActionBinding>(context), DialogExtensions {
override val binder: BottomSheetConfirmDappActionBinding = BottomSheetConfirmDappActionBinding.inflate(LayoutInflater.from(context))
abstract val contentBinder: ViewBinding
@CallSuper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setCancelable(false)
binder.confirmInnerContent.addView(contentBinder.root)
binder.confirmDAppActionAllow.setDismissingClickListener { onConfirm() }
binder.confirmDAppActionReject.setDismissingClickListener { onDeny() }
}
}
@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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:overScrollMode="never"
android:scrollbars="none"
android:layout_height="match_parent"
tools:background="@color/bottom_sheet_background">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:background="@color/bottom_sheet_background">
<ImageView
android:id="@+id/confirmAuthorizeNovaIcon"
style="@style/Widget.Nova.Icon.Big"
android:layout_marginTop="20dp"
android:padding="16dp"
android:src="@drawable/ic_pezkuwi_logo"
app:layout_constraintEnd_toStartOf="@+id/confirmAuthorizeNovaArrow"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/confirmAuthorizeNovaArrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_bidirectonal"
app:layout_constraintBottom_toBottomOf="@+id/confirmAuthorizeNovaIcon"
app:layout_constraintEnd_toStartOf="@+id/confirmAuthorizeDappIcon"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/confirmAuthorizeNovaIcon"
app:layout_constraintTop_toTopOf="@+id/confirmAuthorizeNovaIcon"
app:tint="@color/icon_secondary" />
<ImageView
android:id="@+id/confirmAuthorizeDappIcon"
style="@style/Widget.Nova.Icon.Big"
android:layout_marginTop="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/confirmAuthorizeNovaArrow"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_earth" />
<TextView
android:id="@+id/confirmAuthorizeDappTitle"
style="@style/TextAppearance.NovaFoundation.SemiBold.Title3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="16dp"
android:gravity="center_horizontal"
android:textColor="@color/text_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/confirmAuthorizeNovaIcon"
tools:text="Allow “Polkadot.js” to access your account addresses?" />
<TextView
android:id="@+id/confirmAuthorizeDappSubTitle"
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="16dp"
android:gravity="center_horizontal"
android:text="@string/dapp_confirm_authorize_subtitle"
android:textColor="@color/text_secondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/confirmAuthorizeDappTitle" />
<io.novafoundation.nova.common.view.TableCellView
android:id="@+id/confirmAuthorizeDappWallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/confirmAuthorizeDappSubTitle"
app:title="@string/tabbar_wallet_title" />
<io.novafoundation.nova.common.view.TableCellView
android:id="@+id/confirmAuthorizeDappDApp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:dividerVisible="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/confirmAuthorizeDappWallet"
app:title="@string/dapp_dapp" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
tools:background="@color/bottom_sheet_background">
<ImageView
style="@style/Widget.Nova.Puller"
android:layout_gravity="center_horizontal"
android:layout_marginTop="6dp" />
<FrameLayout
android:id="@+id/confirmInnerContent"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
tools:layout_height="300dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginTop="24dp"
android:orientation="horizontal">
<io.novafoundation.nova.common.view.PrimaryButton
android:id="@+id/confirmDAppActionReject"
style="@style/Widget.Nova.Button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:text="@string/common_reject"
app:appearance="secondary"
android:theme="@style/NegativeAccent" />
<io.novafoundation.nova.common.view.PrimaryButton
android:id="@+id/confirmDAppActionAllow"
style="@style/Widget.Nova.Button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:text="@string/common_allow"
android:theme="@style/AccentBlue" />
</LinearLayout>
</LinearLayout>