Initial commit: Pezkuwi Wallet Android

Complete rebrand of Nova Wallet for Pezkuwichain ecosystem.

## Features
- Full Pezkuwichain support (HEZ & PEZ tokens)
- Polkadot ecosystem compatibility
- Staking, Governance, DeFi, NFTs
- XCM cross-chain transfers
- Hardware wallet support (Ledger, Polkadot Vault)
- WalletConnect v2
- Push notifications

## Languages
- English, Turkish, Kurmanci (Kurdish), Spanish, French, German, Russian, Japanese, Chinese, Korean, Portuguese, Vietnamese

Based on Nova Wallet by Novasama Technologies GmbH
© Dijital Kurdistan Tech Institute 2026
This commit is contained in:
2026-01-23 01:31:12 +03:00
commit 31c8c5995f
7621 changed files with 425838 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"/>
@@ -0,0 +1,12 @@
package io.novafoundation.nova.web3names.data.endpoints
import retrofit2.http.GET
import retrofit2.http.Url
interface TransferRecipientsApi {
@GET
suspend fun getTransferRecipientsRaw(
@Url url: String
): String
}
@@ -0,0 +1,6 @@
package io.novafoundation.nova.web3names.data.endpoints.model
class TransferRecipientDetailsRemoteV1(
val account: String,
val description: String?
)
@@ -0,0 +1,5 @@
package io.novafoundation.nova.web3names.data.endpoints.model
class TransferRecipientDetailsRemoteV2(
val description: String?
)
@@ -0,0 +1,14 @@
package io.novafoundation.nova.web3names.data.provider
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
interface Web3NamesServiceChainIdProvider {
fun getChainId(): ChainId
}
class RealWeb3NamesServiceChainIdProvider(private val chainId: ChainId) : Web3NamesServiceChainIdProvider {
override fun getChainId(): ChainId {
return chainId
}
}
@@ -0,0 +1,117 @@
package io.novafoundation.nova.web3names.data.repository
import io.novafoundation.nova.caip.caip19.Caip19MatcherFactory
import io.novafoundation.nova.caip.caip19.Caip19Parser
import io.novafoundation.nova.caip.caip19.matchers.Caip19Matcher
import io.novafoundation.nova.common.data.network.runtime.binding.bindList
import io.novafoundation.nova.common.data.network.runtime.binding.bindString
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
import io.novafoundation.nova.runtime.ext.isValidAddress
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import io.novafoundation.nova.web3names.data.provider.Web3NamesServiceChainIdProvider
import io.novafoundation.nova.web3names.data.serviceEndpoint.ServiceEndpoint
import io.novafoundation.nova.web3names.data.serviceEndpoint.W3NRecepient
import io.novafoundation.nova.web3names.data.serviceEndpoint.W3NServiceEndpointHandlerFactory
import io.novafoundation.nova.web3names.domain.exceptions.Web3NamesException.ChainProviderNotFoundException
import io.novafoundation.nova.web3names.domain.exceptions.Web3NamesException.UnsupportedAsset
import io.novafoundation.nova.web3names.domain.exceptions.Web3NamesException.ValidAccountNotFoundException
import io.novafoundation.nova.web3names.domain.models.Web3NameAccount
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.metadata.module
import io.novasama.substrate_sdk_android.runtime.metadata.storage
class RealWeb3NamesRepository(
private val remoteStorageSource: StorageDataSource,
private val web3NamesServiceChainIdProvider: Web3NamesServiceChainIdProvider,
private val caip19MatcherFactory: Caip19MatcherFactory,
private val caip19Parser: Caip19Parser,
private val serviceEndpointHandlerFactory: W3NServiceEndpointHandlerFactory,
) : Web3NamesRepository {
override suspend fun queryWeb3NameAccount(web3Name: String, chain: Chain, chainAsset: Chain.Asset): List<Web3NameAccount> {
val caip19Matcher = caip19MatcherFactory.getCaip19Matcher(chain, chainAsset)
if (caip19Matcher.isUnsupported()) throw UnsupportedAsset(web3Name, chainAsset)
val owner = getWeb3NameAccountOwner(web3Name) ?: throw ChainProviderNotFoundException(web3Name)
val serviceEndpoints = getDidServiceEndpoints(owner)
val serviceEndpointHandler = serviceEndpointHandlerFactory.getHandler(serviceEndpoints) ?: throw ValidAccountNotFoundException(web3Name, chain.name)
val recipients = serviceEndpointHandler.getRecipients(web3Name, chain)
return findChainRecipients(recipients, web3Name, chain, caip19Matcher)
}
private suspend fun getWeb3NameAccountOwner(web3Name: String): AccountId? {
return remoteStorageSource.query(web3NamesServiceChainIdProvider.getChainId()) {
runtime.metadata
.module("Web3Names")
.storage("Owner")
.query(web3Name.toByteArray(), binding = ::bindOwners)
}
}
private suspend fun getDidServiceEndpoints(accountId: AccountId): List<ServiceEndpoint> {
val serviceEndpoints = remoteStorageSource.query(web3NamesServiceChainIdProvider.getChainId()) {
runtime.metadata.module("Did")
.storage("ServiceEndpoints")
.entries(
accountId,
keyExtractor = { it },
binding = { data, _ -> bindEndpoint(data) }
)
}
return serviceEndpoints.values.toList()
}
private fun findChainRecipients(
recipients: List<W3NRecepient>,
w3nIdentifier: String,
chain: Chain,
caip19Matcher: Caip19Matcher
): List<Web3NameAccount> {
val matchingRecipients = recipients.groupBy { it.chainIdCaip19 }
.filterKeys {
val caip19Identifier = caip19Parser.parseCaip19(it).getOrNull() ?: return@filterKeys false
caip19Matcher.match(caip19Identifier)
}
if (matchingRecipients.isEmpty()) {
throw ValidAccountNotFoundException(w3nIdentifier, chain.name)
}
val web3NameAccounts = matchingRecipients.flatMap { (_, chainRecipients) -> chainRecipients }
.map {
Web3NameAccount(
address = it.account,
description = it.description,
isValid = chain.isValidAddress(it.account)
)
}
if (web3NameAccounts.none(Web3NameAccount::isValid)) {
throw ValidAccountNotFoundException(w3nIdentifier, chain.name)
}
return web3NameAccounts
}
private fun bindOwners(data: Any?): AccountId? {
if (data == null) return null
val ownerStruct = data.castToStruct()
return ownerStruct.get<AccountId>("owner")
}
private fun bindEndpoint(data: Any?): ServiceEndpoint {
val endpointStruct = data.castToStruct()
val serviceTypes = bindList(endpointStruct["serviceTypes"]) { bindString(it) }
val urls = bindList(endpointStruct["urls"]) { bindString(it) }
val id = bindString(endpointStruct["id"])
return ServiceEndpoint(id, serviceTypes, urls)
}
}
@@ -0,0 +1,9 @@
package io.novafoundation.nova.web3names.data.repository
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.web3names.domain.models.Web3NameAccount
interface Web3NamesRepository {
suspend fun queryWeb3NameAccount(web3Name: String, chain: Chain, chainAsset: Chain.Asset): List<Web3NameAccount>
}
@@ -0,0 +1,7 @@
package io.novafoundation.nova.web3names.data.serviceEndpoint
class ServiceEndpoint(
val id: String,
val serviceTypes: List<String>,
val urls: List<String>
)
@@ -0,0 +1,32 @@
package io.novafoundation.nova.web3names.data.serviceEndpoint
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.web3names.data.endpoints.TransferRecipientsApi
import io.novafoundation.nova.web3names.domain.exceptions.Web3NamesException
class W3NRecepient(
val chainIdCaip19: String,
val account: String,
val description: String?
)
abstract class W3NServiceEndpointHandler(
private val endpoint: ServiceEndpoint,
private val transferRecipientApi: TransferRecipientsApi
) {
suspend fun getRecipients(web3Name: String, chain: Chain): List<W3NRecepient> {
val url = endpoint.urls.firstOrNull() ?: throw Web3NamesException.ValidAccountNotFoundException(web3Name, chain.id)
val recipientsContent = transferRecipientApi.getTransferRecipientsRaw(url)
if (!verifyIntegrity(serviceEndpointId = endpoint.id, serviceEndpointContent = recipientsContent)) {
throw Web3NamesException.IntegrityCheckFailed(web3Name)
}
return parseRecipients(recipientsContent)
}
abstract fun verifyIntegrity(serviceEndpointId: String, serviceEndpointContent: String): Boolean
protected abstract fun parseRecipients(content: String): List<W3NRecepient>
}
@@ -0,0 +1,27 @@
package io.novafoundation.nova.web3names.data.serviceEndpoint
import com.google.gson.Gson
import io.novafoundation.nova.web3names.data.endpoints.TransferRecipientsApi
private const val TRANSFER_ASSETS_PROVIDER_DID_SERVICE_TYPE_V1 = "KiltTransferAssetRecipientV1"
private const val TRANSFER_ASSETS_PROVIDER_DID_SERVICE_TYPE_V2 = "KiltTransferAssetRecipientV2"
class W3NServiceEndpointHandlerFactory(
private val transferRecipientsApi: TransferRecipientsApi,
private val gson: Gson
) {
fun getHandler(serviceEndpoints: List<ServiceEndpoint>): W3NServiceEndpointHandler? {
val v2ServiceEndpoint = serviceEndpoints.firstOrNull { TRANSFER_ASSETS_PROVIDER_DID_SERVICE_TYPE_V2 in it.serviceTypes }
if (v2ServiceEndpoint != null) {
return W3NServiceEndpointHandlerV2(v2ServiceEndpoint, transferRecipientsApi, gson)
}
val v1ServiceEndpoint = serviceEndpoints.firstOrNull { TRANSFER_ASSETS_PROVIDER_DID_SERVICE_TYPE_V1 in it.serviceTypes }
if (v1ServiceEndpoint != null) {
return W3NServiceEndpointHandlerV1(v1ServiceEndpoint, transferRecipientsApi, gson)
}
return null
}
}
@@ -0,0 +1,38 @@
package io.novafoundation.nova.web3names.data.serviceEndpoint
import com.google.gson.Gson
import io.ipfs.multibase.Multibase
import io.novafoundation.nova.common.utils.fromJson
import io.novafoundation.nova.web3names.data.endpoints.TransferRecipientsApi
import io.novafoundation.nova.web3names.data.endpoints.model.TransferRecipientDetailsRemoteV1
import io.novasama.substrate_sdk_android.hash.Hasher.blake2b256
private typealias RecipientsByChainV1 = Map<String, List<TransferRecipientDetailsRemoteV1>>
class W3NServiceEndpointHandlerV1(
endpoint: ServiceEndpoint,
transferRecipientApi: TransferRecipientsApi,
private val gson: Gson
) : W3NServiceEndpointHandler(endpoint, transferRecipientApi) {
override fun verifyIntegrity(serviceEndpointId: String, serviceEndpointContent: String): Boolean = runCatching {
val expectedHash = Multibase.decode(serviceEndpointId)
val actualHash = serviceEndpointContent.encodeToByteArray().blake2b256()
expectedHash.contentEquals(actualHash)
}.getOrDefault(false)
override fun parseRecipients(content: String): List<W3NRecepient> {
val recipients = gson.fromJson<RecipientsByChainV1>(content)
return recipients.flatMap { (chainId, recipients) ->
recipients.map { recipient ->
W3NRecepient(
chainIdCaip19 = chainId,
account = recipient.account,
description = recipient.description
)
}
}
}
}
@@ -0,0 +1,40 @@
package io.novafoundation.nova.web3names.data.serviceEndpoint
import com.google.gson.Gson
import io.ipfs.multibase.Multibase
import io.novafoundation.nova.common.utils.fromJson
import io.novafoundation.nova.web3names.data.endpoints.TransferRecipientsApi
import io.novafoundation.nova.web3names.data.endpoints.model.TransferRecipientDetailsRemoteV2
import io.novasama.substrate_sdk_android.hash.Hasher.blake2b256
import org.erdtman.jcs.JsonCanonicalizer
private typealias RecipientsByChainV2 = Map<String, Map<String, TransferRecipientDetailsRemoteV2>>
class W3NServiceEndpointHandlerV2(
endpoint: ServiceEndpoint,
transferRecipientApi: TransferRecipientsApi,
private val gson: Gson
) : W3NServiceEndpointHandler(endpoint, transferRecipientApi) {
override fun verifyIntegrity(serviceEndpointId: String, serviceEndpointContent: String): Boolean = runCatching {
val expectedHash = Multibase.decode(serviceEndpointId)
val canonizedJson = JsonCanonicalizer(serviceEndpointContent).encodedString
val actualHash = canonizedJson.encodeToByteArray().blake2b256()
expectedHash.contentEquals(actualHash)
}.getOrDefault(false)
override fun parseRecipients(content: String): List<W3NRecepient> {
val fromJson = gson.fromJson<RecipientsByChainV2>(content)
return fromJson.flatMap { (chainId, recipients) ->
recipients.map { recipient ->
W3NRecepient(
chainIdCaip19 = chainId,
account = recipient.key,
description = recipient.value.description
)
}
}
}
}
@@ -0,0 +1,8 @@
package io.novafoundation.nova.web3names.di
import io.novafoundation.nova.web3names.domain.networking.Web3NamesInteractor
interface Web3NamesApi {
val web3NamesInteractor: Web3NamesInteractor
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.web3names.di
import com.google.gson.Gson
import io.novafoundation.nova.caip.caip19.Caip19MatcherFactory
import io.novafoundation.nova.caip.caip19.Caip19Parser
import io.novafoundation.nova.common.data.network.NetworkApiCreator
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import javax.inject.Named
interface Web3NamesDependencies {
val networkApiCreator: NetworkApiCreator
val gson: Gson
val caip19Parser: Caip19Parser
val caip19MatcherFactory: Caip19MatcherFactory
@Named(REMOTE_STORAGE_SOURCE)
fun remoteStorageSource(): StorageDataSource
}
@@ -0,0 +1,30 @@
package io.novafoundation.nova.web3names.di
import dagger.Component
import io.novafoundation.nova.caip.di.CaipApi
import io.novafoundation.nova.common.di.CommonApi
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.core_db.di.DbApi
import io.novafoundation.nova.runtime.di.RuntimeApi
@Component(
modules = [
Web3NamesModule::class
],
dependencies = [
Web3NamesDependencies::class
]
)
@FeatureScope
abstract class Web3NamesFeatureComponent : Web3NamesApi {
@Component(
dependencies = [
CommonApi::class,
DbApi::class,
RuntimeApi::class,
CaipApi::class
]
)
interface Web3NamesDependenciesComponent : Web3NamesDependencies
}
@@ -0,0 +1,28 @@
package io.novafoundation.nova.web3names.di
import io.novafoundation.nova.caip.di.CaipApi
import io.novafoundation.nova.common.di.FeatureApiHolder
import io.novafoundation.nova.common.di.FeatureContainer
import io.novafoundation.nova.common.di.scope.ApplicationScope
import io.novafoundation.nova.core_db.di.DbApi
import io.novafoundation.nova.runtime.di.RuntimeApi
import javax.inject.Inject
@ApplicationScope
class Web3NamesHolder @Inject constructor(
featureContainer: FeatureContainer
) : FeatureApiHolder(featureContainer) {
override fun initializeDependencies(): Any {
val dbDependencies = DaggerWeb3NamesFeatureComponent_Web3NamesDependenciesComponent.builder()
.commonApi(commonApi())
.dbApi(getFeature(DbApi::class.java))
.runtimeApi(getFeature(RuntimeApi::class.java))
.caipApi(getFeature(CaipApi::class.java))
.build()
return DaggerWeb3NamesFeatureComponent.builder()
.web3NamesDependencies(dbDependencies)
.build()
}
}
@@ -0,0 +1,78 @@
package io.novafoundation.nova.web3names.di
import com.google.gson.Gson
import dagger.Module
import dagger.Provides
import io.novafoundation.nova.caip.caip19.Caip19MatcherFactory
import io.novafoundation.nova.caip.caip19.Caip19Parser
import io.novafoundation.nova.common.data.network.NetworkApiCreator
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
import io.novafoundation.nova.runtime.ext.Geneses
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import io.novafoundation.nova.web3names.data.endpoints.TransferRecipientsApi
import io.novafoundation.nova.web3names.data.provider.RealWeb3NamesServiceChainIdProvider
import io.novafoundation.nova.web3names.data.provider.Web3NamesServiceChainIdProvider
import io.novafoundation.nova.web3names.data.repository.RealWeb3NamesRepository
import io.novafoundation.nova.web3names.data.repository.Web3NamesRepository
import io.novafoundation.nova.web3names.data.serviceEndpoint.W3NServiceEndpointHandlerFactory
import io.novafoundation.nova.web3names.domain.networking.RealWeb3NamesInteractor
import io.novafoundation.nova.web3names.domain.networking.Web3NamesInteractor
import javax.inject.Named
@Module
class Web3NamesModule {
@Provides
@FeatureScope
fun provideWeb3NamesServiceChainIdProvider(): Web3NamesServiceChainIdProvider {
return RealWeb3NamesServiceChainIdProvider(Chain.Geneses.KILT)
}
@Provides
@FeatureScope
fun provideTransferRecipientApi(
networkApiCreator: NetworkApiCreator
): TransferRecipientsApi {
return networkApiCreator.create(TransferRecipientsApi::class.java)
}
@Provides
@FeatureScope
fun provideW3NServiceEndpointHandlerFactory(
transferRecipientApi: TransferRecipientsApi,
gson: Gson
) = W3NServiceEndpointHandlerFactory(
transferRecipientApi,
gson
)
@Provides
@FeatureScope
fun provideWeb3NamesRepository(
@Named(REMOTE_STORAGE_SOURCE) storageDataSource: StorageDataSource,
web3NamesServiceChainIdProvider: Web3NamesServiceChainIdProvider,
caip19MatcherFactory: Caip19MatcherFactory,
caip19Parser: Caip19Parser,
w3NServiceEndpointHandlerFactory: W3NServiceEndpointHandlerFactory
): Web3NamesRepository {
return RealWeb3NamesRepository(
storageDataSource,
web3NamesServiceChainIdProvider,
caip19MatcherFactory,
caip19Parser,
w3NServiceEndpointHandlerFactory
)
}
@Provides
@FeatureScope
fun provideWeb3NamesInteractor(
web3NamesRepository: Web3NamesRepository
): Web3NamesInteractor {
return RealWeb3NamesInteractor(
web3NamesRepository
)
}
}
@@ -0,0 +1,3 @@
package io.novafoundation.nova.web3names.domain.exceptions
class ParseWeb3NameException : Exception()
@@ -0,0 +1,19 @@
package io.novafoundation.nova.web3names.domain.exceptions
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.extensions.requirePrefix
sealed class Web3NamesException(identifier: String) : Exception() {
val web3Name: String = identifier.requirePrefix("w3n:")
class ChainProviderNotFoundException(identifier: String) : Web3NamesException(identifier)
class IntegrityCheckFailed(identifier: String) : Web3NamesException(identifier)
class ValidAccountNotFoundException(identifier: String, val chainName: String) : Web3NamesException(identifier)
class UnknownException(web3NameInput: String, val chainName: String) : Web3NamesException(web3NameInput)
class UnsupportedAsset(identifier: String, val chainAsset: Chain.Asset) : Web3NamesException(identifier)
}
@@ -0,0 +1,7 @@
package io.novafoundation.nova.web3names.domain.models
class Web3NameAccount(
val address: String,
val description: String?,
val isValid: Boolean,
)
@@ -0,0 +1,50 @@
package io.novafoundation.nova.web3names.domain.networking
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.web3names.data.repository.Web3NamesRepository
import io.novafoundation.nova.web3names.domain.exceptions.ParseWeb3NameException
import io.novafoundation.nova.web3names.domain.models.Web3NameAccount
interface Web3NamesInteractor {
fun isValidWeb3Name(raw: String): Boolean
suspend fun queryAccountsByWeb3Name(w3nIdentifier: String, chain: Chain, chainAsset: Chain.Asset): List<Web3NameAccount>
fun removePrefix(w3nIdentifier: String): String
}
class RealWeb3NamesInteractor(
private val web3NamesRepository: Web3NamesRepository
) : Web3NamesInteractor {
override fun isValidWeb3Name(raw: String): Boolean {
return parseToWeb3Name(raw).isSuccess
}
override suspend fun queryAccountsByWeb3Name(w3nIdentifier: String, chain: Chain, chainAsset: Chain.Asset): List<Web3NameAccount> {
require(isValidWeb3Name(w3nIdentifier))
val web3NameNoPrefix = parseToWeb3Name(w3nIdentifier).getOrThrow()
return web3NamesRepository.queryWeb3NameAccount(web3NameNoPrefix, chain, chainAsset)
}
override fun removePrefix(w3nIdentifier: String): String {
require(isValidWeb3Name(w3nIdentifier))
return parseToWeb3Name(w3nIdentifier).getOrThrow()
}
private fun parseToWeb3Name(raw: String): Result<String> {
return runCatching {
val (web3NameKey, web3NameValue) = raw.split(":", limit = 2)
if (web3NameKey.trim() == "w3n") {
web3NameValue.trim()
} else {
throw ParseWeb3NameException()
}
}
}
}