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
+11
View File
@@ -0,0 +1,11 @@
android {
namespace 'io.novafoundation.nova.core'
}
dependencies {
implementation coroutinesDep
implementation substrateSdkDep
api web3jDep
}
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
</manifest>
@@ -0,0 +1,14 @@
package io.novafoundation.nova.core.ethereum
import io.novafoundation.nova.core.ethereum.log.Topic
import kotlinx.coroutines.flow.Flow
import org.web3j.protocol.Web3j
import org.web3j.protocol.websocket.events.LogNotification
import org.web3j.protocol.websocket.events.NewHeadsNotification
interface Web3Api : Web3j {
fun newHeadsFlow(): Flow<NewHeadsNotification>
fun logsNotifications(addresses: List<String>, topics: List<Topic>): Flow<LogNotification>
}
@@ -0,0 +1,13 @@
package io.novafoundation.nova.core.ethereum.log
sealed class Topic {
object Any : Topic()
data class Single(val value: String) : Topic()
data class AnyOf(val values: List<String>) : Topic() {
constructor(vararg values: String) : this(values.toList())
}
}
@@ -0,0 +1,7 @@
package io.novafoundation.nova.core.model
enum class CryptoType {
SR25519,
ED25519,
ECDSA
}
@@ -0,0 +1,6 @@
package io.novafoundation.nova.core.model
data class Language(
val iso639Code: String,
val name: String? = null
)
@@ -0,0 +1,7 @@
package io.novafoundation.nova.core.model
data class Network(
val type: Node.NetworkType
) {
val name = type.readableName
}
@@ -0,0 +1,64 @@
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS")
package io.novafoundation.nova.core.model
data class Node(
val id: Int,
val name: String,
val networkType: NetworkType,
val link: String,
val isActive: Boolean,
val isDefault: Boolean,
) {
enum class NetworkType(
val readableName: String,
val runtimeConfiguration: RuntimeConfiguration,
) {
KUSAMA(
"Kusama",
RuntimeConfiguration(
addressByte = 2,
genesisHash = "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe",
erasPerDay = 4
)
),
POLKADOT(
"Polkadot",
RuntimeConfiguration(
addressByte = 0,
genesisHash = "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3",
erasPerDay = 1,
)
),
WESTEND(
"Westend",
RuntimeConfiguration(
addressByte = 42,
genesisHash = "e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e",
erasPerDay = 4
)
),
ROCOCO(
"Rococo",
RuntimeConfiguration(
addressByte = 43, // TODO wrong address type, actual is 42, but it will conflict with Westend
genesisHash = "0x1ab7fbd1d7c3532386268ec23fe4ff69f5bb6b3e3697947df3a2ec2786424de3",
erasPerDay = 4
)
);
companion object {
fun <T> find(value: T, extractor: (NetworkType) -> T): NetworkType? {
return values().find { extractor(it) == value }
}
fun findByAddressByte(addressByte: Short) = find(addressByte) { it.runtimeConfiguration.addressByte }
fun findByGenesis(genesis: String) = find(genesis.removePrefix("0x")) { it.runtimeConfiguration.genesisHash }
}
}
}
val Node.NetworkType.chainId
get() = runtimeConfiguration.genesisHash
@@ -0,0 +1,9 @@
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
package io.novafoundation.nova.core.model
class RuntimeConfiguration(
val genesisHash: String,
val erasPerDay: Int,
val addressByte: Short,
)
@@ -0,0 +1,77 @@
package io.novafoundation.nova.core.model
import io.novasama.substrate_sdk_android.encrypt.keypair.Keypair
sealed class SecuritySource(
val keypair: Keypair
) {
open class Specified(
final override val seed: ByteArray?,
keypair: Keypair
) : SecuritySource(keypair), WithJson, WithSeed {
override fun jsonFormer() = jsonFormer(seed)
class Create(
seed: ByteArray?,
keypair: Keypair,
override val mnemonic: String,
override val derivationPath: String?
) : Specified(seed, keypair), WithMnemonic, WithDerivationPath
class Seed(
seed: ByteArray?,
keypair: Keypair,
override val derivationPath: String?
) : Specified(seed, keypair), WithDerivationPath
class Mnemonic(
seed: ByteArray?,
keypair: Keypair,
override val mnemonic: String,
override val derivationPath: String?
) : Specified(seed, keypair), WithMnemonic, WithDerivationPath
class Json(
seed: ByteArray?,
keypair: Keypair
) : Specified(seed, keypair)
}
open class Unspecified(
keypair: Keypair
) : SecuritySource(keypair)
}
interface WithMnemonic {
val mnemonic: String
fun mnemonicWords() = mnemonic.split(" ")
}
interface WithSeed {
val seed: ByteArray?
}
interface WithJson {
fun jsonFormer(): JsonFormer
}
interface WithDerivationPath {
val derivationPath: String?
}
sealed class JsonFormer {
object KeyPair : JsonFormer()
class Seed(val seed: ByteArray) : JsonFormer()
}
fun jsonFormer(seed: ByteArray?): JsonFormer {
return if (seed != null) {
JsonFormer.Seed(seed)
} else {
JsonFormer.KeyPair
}
}
@@ -0,0 +1,3 @@
package io.novafoundation.nova.core.model
data class StorageChange(val block: String, val key: String, val value: String?)
@@ -0,0 +1,6 @@
package io.novafoundation.nova.core.model
class StorageEntry(
val storageKey: String,
val content: String?,
)
@@ -0,0 +1,61 @@
package io.novafoundation.nova.core.storage
import io.novafoundation.nova.core.model.StorageEntry
import kotlinx.coroutines.flow.Flow
interface StorageCache {
suspend fun isPrefixInCache(prefixKey: String, chainId: String): Boolean
suspend fun isFullKeyInCache(fullKey: String, chainId: String): Boolean
suspend fun insert(entry: StorageEntry, chainId: String)
suspend fun insert(entries: List<StorageEntry>, chainId: String)
suspend fun insertPrefixEntries(entries: List<StorageEntry>, prefixKey: String, chainId: String)
suspend fun removeByPrefix(prefixKey: String, chainId: String)
suspend fun removeByPrefixExcept(
prefixKey: String,
fullKeyExceptions: List<String>,
chainId: String
)
fun observeEntry(key: String, chainId: String): Flow<StorageEntry>
/**
* First result will be emitted when all keys are found in the cache
* Thus, result.size == fullKeys.size
*/
fun observeEntries(keys: List<String>, chainId: String): Flow<List<StorageEntry>>
suspend fun observeEntries(keyPrefix: String, chainId: String): Flow<List<StorageEntry>>
/**
* Should suspend until any matched result found
*/
suspend fun getEntry(key: String, chainId: String): StorageEntry
suspend fun filterKeysInCache(keys: List<String>, chainId: String): List<String>
suspend fun getKeys(keyPrefix: String, chainId: String): List<String>
/**
* Should suspend until all keys will be found
* Thus, result.size == fullKeys.size
*/
suspend fun getEntries(fullKeys: List<String>, chainId: String): List<StorageEntry>
}
suspend fun StorageCache.insert(entries: Map<String, String?>, chainId: String) {
val changes = entries.map { (key, value) -> StorageEntry(key, value) }
insert(changes, chainId)
}
suspend fun StorageCache.insertPrefixEntries(entries: Map<String, String?>, prefix: String, chainId: String) {
val changes = entries.map { (key, value) -> StorageEntry(key, value) }
insertPrefixEntries(changes, prefixKey = prefix, chainId = chainId)
}
@@ -0,0 +1,36 @@
package io.novafoundation.nova.core.updater
import io.novafoundation.nova.core.ethereum.Web3Api
import io.novafoundation.nova.core.ethereum.log.Topic
import io.novafoundation.nova.core.model.StorageChange
import io.novasama.substrate_sdk_android.wsrpc.SocketService
import kotlinx.coroutines.flow.Flow
import org.web3j.protocol.core.Request
import org.web3j.protocol.core.Response
import org.web3j.protocol.websocket.events.LogNotification
import java.util.concurrent.CompletableFuture
interface SubstrateSubscriptionBuilder {
val socketService: SocketService?
fun subscribe(key: String): Flow<StorageChange>
}
interface EthereumSharedRequestsBuilder {
val callApi: Web3Api?
val subscriptionApi: Web3Api?
fun <S, T : Response<*>> ethBatchRequestAsync(batchId: String, request: Request<S, T>): CompletableFuture<T>
fun subscribeEthLogs(address: String, topics: List<Topic>): Flow<LogNotification>
}
val EthereumSharedRequestsBuilder.callApiOrThrow: Web3Api
get() = requireNotNull(callApi) {
"Chain doesn't have any ethereum apis available"
}
interface SharedRequestsBuilder : SubstrateSubscriptionBuilder, EthereumSharedRequestsBuilder
@@ -0,0 +1,63 @@
package io.novafoundation.nova.core.updater
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.transform
/**
* We do not want this extension to be visible outside of update system
* So, we put it into marker interface, which will allow to reach it in consumers code
*/
interface SideEffectScope {
fun <T> Flow<T>.noSideAffects(): Flow<Updater.SideEffect> = transform { }
}
interface UpdateScope<S> {
fun invalidationFlow(): Flow<S?>
}
object GlobalScope : UpdateScope<Unit> {
override fun invalidationFlow() = flowOf(Unit)
}
class EmptyScope<T> : UpdateScope<T> {
override fun invalidationFlow() = emptyFlow<T>()
}
interface GlobalScopeUpdater : Updater<Unit> {
override val scope
get() = GlobalScope
}
interface Updater<V> : SideEffectScope {
@Deprecated(
"This feature is not flexible enough" +
"Updaters should check presense of relevant modules themselves and fallback to no-op in case module is not found"
)
val requiredModules: List<String>
get() = emptyList()
val scope: UpdateScope<V>
/**
* Implementations should be aware of cancellation
*/
suspend fun listenForUpdates(
storageSubscriptionBuilder: SharedRequestsBuilder,
scopeValue: V,
): Flow<SideEffect>
interface SideEffect
}
interface UpdateSystem {
fun start(): Flow<Updater.SideEffect>
}