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
@@ -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,42 @@
package io.novafoundation.nova.feature_buy_impl.di
import dagger.BindsInstance
import dagger.Component
import io.novafoundation.nova.common.di.CommonApi
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi
import io.novafoundation.nova.feature_buy_api.di.BuyFeatureApi
import io.novafoundation.nova.feature_buy_impl.presentation.BuyRouter
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
import io.novafoundation.nova.runtime.di.RuntimeApi
@Component(
dependencies = [
BuyFeatureDependencies::class
],
modules = [
BuyFeatureModule::class
]
)
@FeatureScope
interface BuyFeatureComponent : BuyFeatureApi {
@Component.Factory
interface Factory {
fun create(
@BindsInstance router: BuyRouter,
deps: BuyFeatureDependencies
): BuyFeatureComponent
}
@Component(
dependencies = [
CommonApi::class,
RuntimeApi::class,
AccountFeatureApi::class,
WalletFeatureApi::class,
]
)
interface BuyFeatureDependenciesComponent : BuyFeatureDependencies
}
@@ -0,0 +1,35 @@
package io.novafoundation.nova.feature_buy_impl.di
import android.content.Context
import com.google.gson.Gson
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.ip.IpAddressReceiver
import io.novafoundation.nova.common.utils.webView.InterceptingWebViewClientFactory
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import okhttp3.OkHttpClient
interface BuyFeatureDependencies {
val context: Context
val amountFormatter: AmountFormatter
val chainRegistry: ChainRegistry
val accountUseCase: SelectedAccountUseCase
val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory
val interceptingWebViewClientFactory: InterceptingWebViewClientFactory
val gson: Gson
val okHttpClient: OkHttpClient
val resourceManager: ResourceManager
val ipAddressReceiver: IpAddressReceiver
}
@@ -0,0 +1,29 @@
package io.novafoundation.nova.feature_buy_impl.di
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.feature_account_api.di.AccountFeatureApi
import io.novafoundation.nova.feature_buy_impl.presentation.BuyRouter
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
import io.novafoundation.nova.runtime.di.RuntimeApi
import javax.inject.Inject
@ApplicationScope
class BuyFeatureHolder @Inject constructor(
featureContainer: FeatureContainer,
private val router: BuyRouter
) : FeatureApiHolder(featureContainer) {
override fun initializeDependencies(): Any {
val dependencies = DaggerBuyFeatureComponent_BuyFeatureDependenciesComponent.builder()
.commonApi(commonApi())
.accountFeatureApi(getFeature(AccountFeatureApi::class.java))
.walletFeatureApi(getFeature(WalletFeatureApi::class.java))
.runtimeApi(getFeature(RuntimeApi::class.java))
.build()
return DaggerBuyFeatureComponent.factory()
.create(router, dependencies)
}
}
@@ -0,0 +1,125 @@
package io.novafoundation.nova.feature_buy_impl.di
import android.content.Context
import com.google.gson.Gson
import dagger.Module
import dagger.Provides
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.utils.ip.IpAddressReceiver
import io.novafoundation.nova.common.utils.webView.InterceptingWebViewClientFactory
import io.novafoundation.nova.feature_buy_api.presentation.trade.TradeTokenRegistry
import io.novafoundation.nova.feature_buy_api.presentation.mixin.TradeMixin
import io.novafoundation.nova.feature_buy_impl.presentation.common.MercuryoSignatureFactory
import io.novafoundation.nova.feature_buy_api.presentation.trade.interceptors.mercuryo.MercuryoBuyRequestInterceptorFactory
import io.novafoundation.nova.feature_buy_api.presentation.trade.interceptors.mercuryo.MercuryoSellRequestInterceptorFactory
import io.novafoundation.nova.feature_buy_impl.BuildConfig
import io.novafoundation.nova.feature_buy_impl.di.deeplinks.DeepLinkModule
import io.novafoundation.nova.feature_buy_impl.presentation.common.RealMercuryoSignatureFactory
import io.novafoundation.nova.feature_buy_impl.presentation.trade.RealTradeTokenRegistry
import io.novafoundation.nova.feature_buy_impl.presentation.trade.providers.banxa.BanxaProvider
import io.novafoundation.nova.feature_buy_impl.presentation.trade.providers.mercurio.MercuryoIntegratorFactory
import io.novafoundation.nova.feature_buy_impl.presentation.trade.providers.mercurio.MercuryoProvider
import io.novafoundation.nova.feature_buy_impl.presentation.trade.providers.transak.TransakProvider
import io.novafoundation.nova.feature_buy_impl.presentation.mixin.TradeMixinFactory
import io.novafoundation.nova.feature_buy_impl.presentation.trade.interceptors.mercuryo.RealMercuryoBuyRequestInterceptorFactory
import io.novafoundation.nova.feature_buy_impl.presentation.trade.interceptors.mercuryo.RealMercuryoSellRequestInterceptorFactory
import okhttp3.OkHttpClient
@Module(includes = [DeepLinkModule::class])
class BuyFeatureModule {
@Provides
@FeatureScope
fun provideMercuryoSellRequestInterceptorFactory(
gson: Gson,
okHttpClient: OkHttpClient
): MercuryoSellRequestInterceptorFactory = RealMercuryoSellRequestInterceptorFactory(
gson = gson,
okHttpClient = okHttpClient
)
@Provides
@FeatureScope
fun provideMercuryoBuyRequestInterceptorFactory(
gson: Gson,
okHttpClient: OkHttpClient
): MercuryoBuyRequestInterceptorFactory = RealMercuryoBuyRequestInterceptorFactory(
gson = gson,
okHttpClient = okHttpClient
)
@Provides
@FeatureScope
fun provideMercuryoSignatureGenerator(
ipAddressReceiver: IpAddressReceiver
): MercuryoSignatureFactory = RealMercuryoSignatureFactory(ipAddressReceiver)
@Provides
@FeatureScope
fun provideMercuryoIntegratorFactory(
mercuryoBuyInterceptorFactory: MercuryoBuyRequestInterceptorFactory,
mercuryoSellInterceptorFactory: MercuryoSellRequestInterceptorFactory,
interceptingWebViewClientFactory: InterceptingWebViewClientFactory,
mercuryoSignatureFactory: MercuryoSignatureFactory
): MercuryoIntegratorFactory {
return MercuryoIntegratorFactory(
mercuryoBuyInterceptorFactory,
mercuryoSellInterceptorFactory,
interceptingWebViewClientFactory,
mercuryoSignatureFactory
)
}
@Provides
@FeatureScope
fun provideBanxaProvider(): BanxaProvider {
return BanxaProvider(BuildConfig.BANXA_HOST)
}
@Provides
@FeatureScope
fun provideMercuryoProvider(integratorFactory: MercuryoIntegratorFactory): MercuryoProvider {
return MercuryoProvider(
host = BuildConfig.MERCURYO_HOST,
widgetId = BuildConfig.MERCURYO_WIDGET_ID,
secret = BuildConfig.MERCURYO_SECRET,
integratorFactory = integratorFactory
)
}
@Provides
@FeatureScope
fun provideTransakProvider(context: Context): TransakProvider {
val environment = if (BuildConfig.DEBUG) "STAGING" else "PRODUCTION"
return TransakProvider(
host = BuildConfig.TRANSAK_HOST,
referrerDomain = context.packageName,
environment = environment
)
}
@Provides
@FeatureScope
fun provideBuyTokenIntegration(
transakProvider: TransakProvider,
mercuryoProvider: MercuryoProvider,
banxaProvider: BanxaProvider
): TradeTokenRegistry {
return RealTradeTokenRegistry(
providers = listOf(
mercuryoProvider,
transakProvider,
banxaProvider,
)
)
}
@Provides
@FeatureScope
fun provideBuyMixinFactory(
buyTokenRegistry: TradeTokenRegistry
): TradeMixin.Factory = TradeMixinFactory(
buyTokenRegistry = buyTokenRegistry
)
}
@@ -0,0 +1,24 @@
package io.novafoundation.nova.feature_buy_impl.di.deeplinks
import dagger.Module
import dagger.Provides
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_buy_api.di.deeplinks.BuyDeepLinks
import io.novafoundation.nova.feature_buy_impl.presentation.deeplink.BuyCallbackDeepLinkHandler
@Module
class DeepLinkModule {
@Provides
@FeatureScope
fun provideBuyCallbackDeepLinkHandler(
resourceManager: ResourceManager
) = BuyCallbackDeepLinkHandler(resourceManager)
@Provides
@FeatureScope
fun provideDeepLinks(buyCallback: BuyCallbackDeepLinkHandler): BuyDeepLinks {
return BuyDeepLinks(listOf(buyCallback))
}
}
@@ -0,0 +1,5 @@
package io.novafoundation.nova.feature_buy_impl.presentation
import io.novafoundation.nova.common.navigation.ReturnableRouter
interface BuyRouter : ReturnableRouter
@@ -0,0 +1,6 @@
package io.novafoundation.nova.feature_buy_impl.presentation.common
interface MercuryoSignatureFactory {
suspend fun createSignature(address: String, secret: String, merchantTransactionId: String): String
}
@@ -0,0 +1,19 @@
package io.novafoundation.nova.feature_buy_impl.presentation.common
import io.novafoundation.nova.common.utils.ip.IpAddressReceiver
import io.novafoundation.nova.common.utils.sha512
import io.novasama.substrate_sdk_android.extensions.toHexString
class RealMercuryoSignatureFactory(
private val ipAddressReceiver: IpAddressReceiver
) : MercuryoSignatureFactory {
override suspend fun createSignature(address: String, secret: String, merchantTransactionId: String): String {
val ip = ipAddressReceiver.get()
val signature = "$address$secret$ip$merchantTransactionId".encodeToByteArray()
.sha512()
.toHexString()
return "v2:$signature"
}
}
@@ -0,0 +1,7 @@
package io.novafoundation.nova.feature_buy_impl.presentation.common
import java.util.UUID
fun generateMerchantTransactionId(): String {
return UUID.randomUUID().toString()
}
@@ -0,0 +1,27 @@
package io.novafoundation.nova.feature_buy_impl.presentation.deeplink
import android.net.Uri
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.singleReplaySharedFlow
import io.novafoundation.nova.feature_buy_api.presentation.trade.providers.ProviderUtils
import io.novafoundation.nova.feature_deep_linking.R
import io.novafoundation.nova.feature_deep_linking.presentation.handling.CallbackEvent
import io.novafoundation.nova.feature_deep_linking.presentation.handling.DeepLinkHandler
import kotlinx.coroutines.flow.MutableSharedFlow
class BuyCallbackDeepLinkHandler(
private val resourceManager: ResourceManager
) : DeepLinkHandler {
override val callbackFlow: MutableSharedFlow<CallbackEvent> = singleReplaySharedFlow()
override suspend fun matches(data: Uri): Boolean {
val link = data.toString()
return ProviderUtils.REDIRECT_URL_BASE in link
}
override suspend fun handleDeepLink(data: Uri) = runCatching {
val message = resourceManager.getString(R.string.buy_completed)
callbackFlow.emit(CallbackEvent.Message(message))
}
}
@@ -0,0 +1,38 @@
package io.novafoundation.nova.feature_buy_impl.presentation.mixin
import io.novafoundation.nova.common.utils.WithCoroutineScopeExtensions
import io.novafoundation.nova.feature_buy_api.presentation.trade.TradeProvider
import io.novafoundation.nova.feature_buy_api.presentation.trade.TradeTokenRegistry
import io.novafoundation.nova.feature_buy_api.presentation.mixin.TradeMixin
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import kotlinx.coroutines.CoroutineScope
internal class TradeMixinFactory(
private val buyTokenRegistry: TradeTokenRegistry,
) : TradeMixin.Factory {
override fun create(scope: CoroutineScope): TradeMixin.Presentation {
return TradeProviderMixin(
buyTokenRegistry = buyTokenRegistry,
coroutineScope = scope
)
}
}
private class TradeProviderMixin(
private val buyTokenRegistry: TradeTokenRegistry,
coroutineScope: CoroutineScope,
) : TradeMixin.Presentation,
CoroutineScope by coroutineScope,
WithCoroutineScopeExtensions by WithCoroutineScopeExtensions(coroutineScope) {
override fun providersFor(chainAsset: Chain.Asset, tradeType: TradeTokenRegistry.TradeType): List<TradeProvider> {
return buyTokenRegistry.availableProvidersFor(chainAsset, tradeType)
}
@Suppress("UNCHECKED_CAST")
override fun <T> providerFor(chainAsset: Chain.Asset, tradeFlow: TradeTokenRegistry.TradeType, providerId: String): T {
return providersFor(chainAsset, tradeFlow)
.first { it.id == providerId } as T
}
}
@@ -0,0 +1,29 @@
package io.novafoundation.nova.feature_buy_impl.presentation.trade
import io.novafoundation.nova.common.utils.hasIntersectionWith
import io.novafoundation.nova.common.utils.mapToSet
import io.novafoundation.nova.feature_buy_api.presentation.trade.TradeTokenRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
class RealTradeTokenRegistry(private val providers: List<TradeTokenRegistry.Provider<*>>) : TradeTokenRegistry {
override fun hasProvider(chainAsset: Chain.Asset): Boolean {
val supportedProviderIds = providers.mapToSet { it.id }
return supportedProviderIds.hasIntersectionWith(chainAsset.buyProviders.keys) ||
supportedProviderIds.hasIntersectionWith(chainAsset.sellProviders.keys)
}
override fun hasProvider(chainAsset: Chain.Asset, tradeType: TradeTokenRegistry.TradeType): Boolean {
return availableProvidersFor(chainAsset, tradeType).isNotEmpty()
}
override fun availableProvidersFor(chainAsset: Chain.Asset, tradeType: TradeTokenRegistry.TradeType) = providers
.filter { provider ->
val providersByType = when (tradeType) {
TradeTokenRegistry.TradeType.BUY -> chainAsset.buyProviders
TradeTokenRegistry.TradeType.SELL -> chainAsset.sellProviders
}
provider.id in providersByType
}
}
@@ -0,0 +1,92 @@
package io.novafoundation.nova.feature_buy_impl.presentation.trade.interceptors.mercuryo
import android.webkit.WebResourceRequest
import com.google.gson.Gson
import io.novafoundation.nova.common.utils.webView.makeRequestBlocking
import io.novafoundation.nova.common.utils.webView.toOkHttpRequestBuilder
import io.novafoundation.nova.feature_buy_api.presentation.trade.common.OnTradeOperationFinishedListener
import io.novafoundation.nova.feature_buy_api.presentation.trade.interceptors.mercuryo.MercuryoBuyRequestInterceptor
import io.novafoundation.nova.feature_buy_api.presentation.trade.interceptors.mercuryo.MercuryoBuyRequestInterceptorFactory
import okhttp3.OkHttpClient
class RealMercuryoBuyRequestInterceptorFactory(
private val okHttpClient: OkHttpClient,
private val gson: Gson
) : MercuryoBuyRequestInterceptorFactory {
override fun create(onTradeOperationFinishedListener: OnTradeOperationFinishedListener): MercuryoBuyRequestInterceptor {
return RealMercuryoBuyRequestInterceptor(okHttpClient, gson, onTradeOperationFinishedListener)
}
}
class RealMercuryoBuyRequestInterceptor(
private val okHttpClient: OkHttpClient,
private val gson: Gson,
private val onTradeOperationFinishedListener: OnTradeOperationFinishedListener
) : MercuryoBuyRequestInterceptor {
private val interceptionPattern = Regex("https://api\\.mercuryo\\.io/[a-zA-Z0-9.]+/widget/buy/([a-zA-Z0-9]+)/status.*")
override fun intercept(request: WebResourceRequest): Boolean {
val url = request.url.toString()
val matches = interceptionPattern.find(url)
if (matches != null) {
return performOkHttpRequest(request)
}
return false
}
private fun performOkHttpRequest(request: WebResourceRequest): Boolean {
val requestBuilder = request.toOkHttpRequestBuilder()
return try {
val response = okHttpClient.makeRequestBlocking(requestBuilder)
val buyStatusResponse = gson.fromJson(response.body!!.string(), BuyStatusResponse::class.java)
if (buyStatusResponse.isPaid()) {
onTradeOperationFinishedListener.onTradeOperationFinished(success = true)
}
true
} catch (e: Exception) {
false
}
}
}
/**
* {
* "status": 200,
* "data": {
* "id": "0da637056a0c85319",
* "status": "paid", // new, pending, paid
* "payment_status": "charged",
* "withdraw_transaction": {
* "id": "0da63727a3ce33010",
* "address": "12gkMmfdKq7aEnAXwb2NSxh9vLqKifoCaoafLrR6E6swZRmc",
* "fee": "0",
* "url": ""
* },
* "currency": "DOT",
* "amount": "2.4742695641",
* "fiat_currency": "USD",
* "fiat_amount": "11.00",
* "address": "12gkMmfdKq7aEnAXwb2NSxh9vLqKifoCaoafLrR6E6swZRmc",
* "transaction": {
* "id": "0da637056f0821757"
* },
* "local_fiat_currency_total": {
* "local_fiat_currency": "EUR",
* "local_fiat_amount": "10.25"
* }
* }
* }
*/
private class BuyStatusResponse(val data: Data) {
class Data(val status: String)
}
private fun BuyStatusResponse.isPaid() = data.status == "paid"
@@ -0,0 +1,140 @@
package io.novafoundation.nova.feature_buy_impl.presentation.trade.interceptors.mercuryo
import android.webkit.WebResourceRequest
import com.google.gson.Gson
import io.novafoundation.nova.common.utils.webView.makeRequestBlocking
import io.novafoundation.nova.common.utils.webView.toOkHttpRequestBuilder
import io.novafoundation.nova.feature_buy_api.presentation.trade.common.OnSellOrderCreatedListener
import io.novafoundation.nova.feature_buy_api.presentation.trade.common.OnTradeOperationFinishedListener
import io.novafoundation.nova.feature_buy_api.presentation.trade.interceptors.mercuryo.MercuryoSellRequestInterceptor
import io.novafoundation.nova.feature_buy_api.presentation.trade.interceptors.mercuryo.MercuryoSellRequestInterceptorFactory
import java.math.BigDecimal
import okhttp3.OkHttpClient
class RealMercuryoSellRequestInterceptorFactory(
private val okHttpClient: OkHttpClient,
private val gson: Gson
) : MercuryoSellRequestInterceptorFactory {
override fun create(
tradeSellCallback: OnSellOrderCreatedListener,
onTradeOperationFinishedListener: OnTradeOperationFinishedListener
): MercuryoSellRequestInterceptor {
return RealMercuryoSellRequestInterceptor(okHttpClient, gson, tradeSellCallback, onTradeOperationFinishedListener)
}
}
class RealMercuryoSellRequestInterceptor(
private val okHttpClient: OkHttpClient,
private val gson: Gson,
private val tradeSellCallback: OnSellOrderCreatedListener,
private val onTradeOperationFinishedListener: OnTradeOperationFinishedListener
) : MercuryoSellRequestInterceptor {
private val openedOrderIds = mutableSetOf<String>()
private val interceptionPattern = Regex("https://api\\.mercuryo\\.io/[a-zA-Z0-9.]+/widget/sell-request/([a-zA-Z0-9]+)/status.*")
override fun intercept(request: WebResourceRequest): Boolean {
val url = request.url.toString()
val matches = interceptionPattern.find(url)
if (matches != null) {
val orderId = matches.groupValues[1]
return performOkHttpRequest(orderId, request)
}
return false
}
private fun performOkHttpRequest(orderId: String, request: WebResourceRequest): Boolean {
val requestBuilder = request.toOkHttpRequestBuilder()
return try {
val response = okHttpClient.makeRequestBlocking(requestBuilder)
val sellStatusResponse = gson.fromJson(response.body!!.string(), SellStatusResponse::class.java)
// We should check that this data is exist in response before handling. Otherwise we will get an exception
val address = sellStatusResponse.getAddress() ?: error("Address must be not null")
val amount = sellStatusResponse.getAmount() ?: error("Amount must be not null")
when {
sellStatusResponse.isNew() && orderId !in openedOrderIds -> {
tradeSellCallback.onSellOrderCreated(orderId, address, amount)
openedOrderIds.add(orderId)
}
sellStatusResponse.isCompleted() -> onTradeOperationFinishedListener.onTradeOperationFinished(success = true)
}
true
} catch (e: Exception) {
false
}
}
}
/**
* {
* "status": 200,
* "data": {
* "status": "completed", // Status may be new, pending, completed
* "is_partially_paid": 0,
* "amounts": {
* "request": {
* "amount": "3.55",
* "currency": "DOT",
* "fiat_amount": "25.00",
* "fiat_currency": "EUR"
* },
* "deposit": {
* "amount": "3.5544722879",
* "currency": "DOT",
* "fiat_amount": "28.00",
* "fiat_currency": "EUR"
* },
* "payout": {
* "amount": "3.5544722879",
* "currency": "DOT",
* "fiat_amount": "24.98",
* "fiat_currency": "EUR"
* }
* },
* "next": null,
* "deposit_transaction": {
* "id": "1gb8dnc28jds8ch",
* "address": "15AsDPtQ6rZdJgsLsEmQCahym5STRVBVaUYjWFiRRinMjYYaw",
* "url": "https://polkadot.subscan.io/extrinsic/0x178f96e1f8837a3dd75ff8b5a5d4422c5c0f7848fbf5c00e343f03b9466e408b"
* },
* "address": "15AsDPtQ6rZdJgsLsEmQCahym5STRVBVaUYjWFiRRinMjYYaw",
* "fiat_card_id": "1gb8dnc28jds8ch"
* }
* }
*/
private class SellStatusResponse(val data: Data?) {
class Data(
val status: String?,
val amounts: Amounts?,
val address: String?
)
class Amounts(val request: Request?) {
class Request(
val amount: String?,
)
}
}
private fun SellStatusResponse.getAmount(): BigDecimal? {
return data?.amounts?.request?.amount?.toBigDecimal()
}
private fun SellStatusResponse.getAddress(): String? {
return data?.address
}
private fun SellStatusResponse.isNew() = data?.status == "new"
private fun SellStatusResponse.isCompleted() = data?.status == "completed"
@@ -0,0 +1,80 @@
package io.novafoundation.nova.feature_buy_impl.presentation.trade.providers.banxa
import android.net.Uri
import android.webkit.WebView
import io.novafoundation.nova.common.utils.appendNullableQueryParameter
import io.novafoundation.nova.feature_buy_api.presentation.trade.TradeTokenRegistry
import io.novafoundation.nova.feature_buy_api.presentation.trade.common.OnTradeOperationFinishedListener
import io.novafoundation.nova.feature_buy_api.presentation.trade.common.OnSellOrderCreatedListener
import io.novafoundation.nova.feature_buy_api.presentation.trade.providers.WebViewIntegrationProvider
import io.novafoundation.nova.feature_buy_impl.R
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
private const val COIN_KEY = "coinType"
private const val BLOCKCHAIN_KEY = "blockchain"
class BanxaProvider(
private val host: String
) : WebViewIntegrationProvider {
override val id: String = "banxa"
override val name: String = "Banxa"
override val officialUrl: String = "banxa.com"
override val logoRes: Int = R.drawable.ic_banxa_provider_logo
override fun getDescriptionRes(tradeType: TradeTokenRegistry.TradeType): Int {
return R.string.banxa_provider_description
}
override fun getPaymentMethods(tradeType: TradeTokenRegistry.TradeType): List<TradeTokenRegistry.PaymentMethod> {
return when (tradeType) {
TradeTokenRegistry.TradeType.BUY -> listOf(
TradeTokenRegistry.PaymentMethod.Visa,
TradeTokenRegistry.PaymentMethod.MasterCard,
TradeTokenRegistry.PaymentMethod.ApplePay,
TradeTokenRegistry.PaymentMethod.GooglePay,
TradeTokenRegistry.PaymentMethod.Sepa,
TradeTokenRegistry.PaymentMethod.Other(5)
)
TradeTokenRegistry.TradeType.SELL -> emptyList()
}
}
override fun createIntegrator(
chainAsset: Chain.Asset,
address: String,
tradeFlow: TradeTokenRegistry.TradeType,
onCloseListener: OnTradeOperationFinishedListener,
onSellOrderCreatedListener: OnSellOrderCreatedListener
): WebViewIntegrationProvider.Integrator {
val providerDetails = chainAsset.buyProviders.getValue(id)
val blockchain = providerDetails[BLOCKCHAIN_KEY] as? String
val coinType = providerDetails[COIN_KEY] as? String
return BanxaIntegrator(host, blockchain, coinType, address)
}
private class BanxaIntegrator(
private val host: String,
private val blockchain: String?,
private val coinType: String?,
private val address: String
) : WebViewIntegrationProvider.Integrator {
override suspend fun run(using: WebView) {
using.loadUrl(createLink())
}
private fun createLink(): String {
return Uri.Builder()
.scheme("https")
.authority(host)
.appendNullableQueryParameter(BLOCKCHAIN_KEY, blockchain)
.appendNullableQueryParameter(COIN_KEY, coinType)
.appendQueryParameter("walletAddress", address)
.build()
.toString()
}
}
}
@@ -0,0 +1,109 @@
package io.novafoundation.nova.feature_buy_impl.presentation.trade.providers.mercurio
import android.net.Uri
import android.webkit.WebView
import io.novafoundation.nova.common.utils.TokenSymbol
import io.novafoundation.nova.common.utils.appendNullableQueryParameter
import io.novafoundation.nova.common.utils.urlEncoded
import io.novafoundation.nova.common.utils.webView.InterceptingWebViewClient
import io.novafoundation.nova.common.utils.webView.InterceptingWebViewClientFactory
import io.novafoundation.nova.feature_buy_api.presentation.trade.TradeTokenRegistry
import io.novafoundation.nova.feature_buy_api.presentation.trade.common.OnSellOrderCreatedListener
import io.novafoundation.nova.feature_buy_api.presentation.trade.common.OnTradeOperationFinishedListener
import io.novafoundation.nova.feature_buy_impl.presentation.common.MercuryoSignatureFactory
import io.novafoundation.nova.feature_buy_impl.presentation.common.generateMerchantTransactionId
import io.novafoundation.nova.feature_buy_api.presentation.trade.interceptors.mercuryo.MercuryoBuyRequestInterceptorFactory
import io.novafoundation.nova.feature_buy_api.presentation.trade.interceptors.mercuryo.MercuryoSellRequestInterceptorFactory
import io.novafoundation.nova.feature_buy_api.presentation.trade.providers.WebViewIntegrationProvider
import io.novafoundation.nova.feature_buy_api.presentation.trade.providers.ProviderUtils
import io.novafoundation.nova.feature_buy_impl.presentation.trade.providers.mercurio.MercuryoIntegrator.Payload
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class MercuryoIntegratorFactory(
private val mercuryoBuyInterceptorFactory: MercuryoBuyRequestInterceptorFactory,
private val mercuryoSellInterceptorFactory: MercuryoSellRequestInterceptorFactory,
private val interceptingWebViewClientFactory: InterceptingWebViewClientFactory,
private val signatureGenerator: MercuryoSignatureFactory
) {
fun create(
payload: Payload,
onSellOrderCreatedListener: OnSellOrderCreatedListener,
onCloseListener: OnTradeOperationFinishedListener
): MercuryoIntegrator {
val webViewClient = interceptingWebViewClientFactory.create(
listOf(
mercuryoBuyInterceptorFactory.create(onCloseListener),
mercuryoSellInterceptorFactory.create(onSellOrderCreatedListener, onCloseListener)
)
)
return MercuryoIntegrator(
payload,
webViewClient,
signatureGenerator
)
}
}
class MercuryoIntegrator(
private val payload: Payload,
private val webViewClient: InterceptingWebViewClient,
private val signatureGenerator: MercuryoSignatureFactory
) : WebViewIntegrationProvider.Integrator {
class Payload(
val host: String,
val widgetId: String,
val tokenSymbol: TokenSymbol,
val network: String?,
val address: String,
val secret: String,
val tradeFlow: TradeTokenRegistry.TradeType
)
override suspend fun run(using: WebView) {
withContext(Dispatchers.Main) {
using.webViewClient = webViewClient
runCatching {
val link = withContext(Dispatchers.IO) { createLink() }
using.loadUrl(link)
}
}
}
private suspend fun createLink(): String {
// Merchant transaction id is a custom id we can provide to mercuryo to track a transaction.
// Seems useless for us now but required for signature
val merchantTransactionId = generateMerchantTransactionId()
val signature = signatureGenerator.createSignature(payload.address, payload.secret, merchantTransactionId)
val urlBuilder = Uri.Builder()
.scheme("https")
.authority(payload.host)
.appendQueryParameter("widget_id", payload.widgetId)
.appendQueryParameter("merchant_transaction_id", merchantTransactionId)
.appendQueryParameter("type", payload.tradeFlow.getType())
.appendNullableQueryParameter(MERCURYO_NETWORK_KEY, payload.network)
.appendQueryParameter("currency", payload.tokenSymbol.value)
.appendQueryParameter("return_url", ProviderUtils.REDIRECT_URL_BASE.urlEncoded())
.appendQueryParameter("signature", signature)
.appendQueryParameter("fix_currency", true.toString())
when (payload.tradeFlow) {
TradeTokenRegistry.TradeType.BUY -> urlBuilder.appendQueryParameter("address", payload.address)
TradeTokenRegistry.TradeType.SELL -> urlBuilder.appendQueryParameter("refund_address", payload.address)
.appendQueryParameter("hide_refund_address", true.toString())
}
return urlBuilder.build().toString()
}
private fun TradeTokenRegistry.TradeType.getType(): String {
return when (this) {
TradeTokenRegistry.TradeType.BUY -> "buy"
TradeTokenRegistry.TradeType.SELL -> "sell"
}
}
}
@@ -0,0 +1,60 @@
package io.novafoundation.nova.feature_buy_impl.presentation.trade.providers.mercurio
import io.novafoundation.nova.feature_buy_api.presentation.trade.TradeTokenRegistry
import io.novafoundation.nova.feature_buy_api.presentation.trade.common.OnTradeOperationFinishedListener
import io.novafoundation.nova.feature_buy_api.presentation.trade.common.OnSellOrderCreatedListener
import io.novafoundation.nova.feature_buy_api.presentation.trade.providers.WebViewIntegrationProvider
import io.novafoundation.nova.feature_buy_impl.R
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
const val MERCURYO_NETWORK_KEY = "network"
class MercuryoProvider(
private val host: String,
private val widgetId: String,
private val secret: String,
private val integratorFactory: MercuryoIntegratorFactory
) : WebViewIntegrationProvider {
override val id: String = "mercuryo"
override val name: String = "Mercuryo"
override val officialUrl: String = "mercuryo.io"
override val logoRes: Int = R.drawable.ic_mercurio_provider_logo
override fun getDescriptionRes(tradeType: TradeTokenRegistry.TradeType): Int {
return R.string.mercurio_provider_description
}
override fun getPaymentMethods(tradeFlow: TradeTokenRegistry.TradeType): List<TradeTokenRegistry.PaymentMethod> {
return when (tradeFlow) {
TradeTokenRegistry.TradeType.BUY -> listOf(
TradeTokenRegistry.PaymentMethod.Visa,
TradeTokenRegistry.PaymentMethod.MasterCard,
TradeTokenRegistry.PaymentMethod.ApplePay,
TradeTokenRegistry.PaymentMethod.GooglePay,
TradeTokenRegistry.PaymentMethod.Sepa,
TradeTokenRegistry.PaymentMethod.Other(5)
)
TradeTokenRegistry.TradeType.SELL -> listOf(
TradeTokenRegistry.PaymentMethod.Visa,
TradeTokenRegistry.PaymentMethod.MasterCard,
TradeTokenRegistry.PaymentMethod.Sepa,
TradeTokenRegistry.PaymentMethod.BankTransfer
)
}
}
override fun createIntegrator(
chainAsset: Chain.Asset,
address: String,
tradeFlow: TradeTokenRegistry.TradeType,
onCloseListener: OnTradeOperationFinishedListener,
onSellOrderCreatedListener: OnSellOrderCreatedListener
): WebViewIntegrationProvider.Integrator {
val network = chainAsset.buyProviders.getValue(id)[MERCURYO_NETWORK_KEY] as? String
val payload = MercuryoIntegrator.Payload(host, widgetId, chainAsset.symbol, network, address, secret, tradeFlow)
return integratorFactory.create(payload, onSellOrderCreatedListener, onCloseListener)
}
}
@@ -0,0 +1,72 @@
package io.novafoundation.nova.feature_buy_impl.presentation.trade.providers.transak
import android.net.Uri
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import io.novafoundation.nova.common.utils.TokenSymbol
import io.novafoundation.nova.common.utils.appendNullableQueryParameter
import io.novafoundation.nova.feature_buy_api.presentation.trade.TradeTokenRegistry
import io.novafoundation.nova.feature_buy_api.presentation.trade.providers.WebViewIntegrationProvider
import io.novafoundation.nova.feature_buy_api.presentation.trade.common.OnTradeOperationFinishedListener
import io.novafoundation.nova.feature_buy_api.presentation.trade.common.OnSellOrderCreatedListener
// You can find a valid implementation in https://github.com/agtransak/TransakAndroidSample/blob/events/app/src/main/java/com/transak/sample/MainActivity.kt
private const val JS_BRIDGE_NAME = "Android"
class TransakIntegrator(
private val payload: Payload,
private val closeListener: OnTradeOperationFinishedListener,
private val sellOrderCreatedListener: OnSellOrderCreatedListener
) : WebViewIntegrationProvider.Integrator {
class Payload(
val host: String,
val network: String?,
val referrerDomain: String,
val environment: String,
val tokenSymbol: TokenSymbol,
val address: String,
val tradeFlow: TradeTokenRegistry.TradeType
)
override suspend fun run(using: WebView) {
using.webViewClient = TransakWebViewClient()
using.addJavascriptInterface(TransakJsEventBridge(closeListener, sellOrderCreatedListener), JS_BRIDGE_NAME)
using.loadUrl(createLink())
}
private fun createLink(): String {
val urlBuilder = Uri.Builder()
.scheme("https")
.authority(payload.host)
.appendQueryParameter("productsAvailed", payload.tradeFlow.getType())
.appendQueryParameter("environment", payload.environment)
.appendQueryParameter("cryptoCurrencyCode", payload.tokenSymbol.value)
.appendQueryParameter("referrerDomain", payload.referrerDomain)
.appendNullableQueryParameter(TRANSAK_NETWORK_KEY, payload.network)
if (payload.tradeFlow == TradeTokenRegistry.TradeType.BUY) {
urlBuilder.appendQueryParameter("walletAddress", payload.address)
.appendQueryParameter("disableWalletAddressForm", "true")
}
return urlBuilder.build().toString()
}
private fun TradeTokenRegistry.TradeType.getType(): String {
return when (this) {
TradeTokenRegistry.TradeType.BUY -> "BUY"
TradeTokenRegistry.TradeType.SELL -> "SELL"
}
}
}
private class TransakWebViewClient : WebViewClient() {
// We use it to override base transak loading otherwise transak navigates to android native browser
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
view.loadUrl(request.url.toString())
return true
}
}
@@ -0,0 +1,39 @@
package io.novafoundation.nova.feature_buy_impl.presentation.trade.providers.transak
import android.util.Log
import android.webkit.JavascriptInterface
import io.novafoundation.nova.feature_buy_api.presentation.trade.common.OnTradeOperationFinishedListener
import io.novafoundation.nova.feature_buy_api.presentation.trade.common.OnSellOrderCreatedListener
import org.json.JSONObject
class TransakJsEventBridge(
private val closeListener: OnTradeOperationFinishedListener,
private val tradeSellCallback: OnSellOrderCreatedListener
) {
@JavascriptInterface
fun postMessage(eventData: String) {
val json = JSONObject(eventData)
val eventId = json.getString("event_id")
Log.d("TransakEvent", "Event: $eventId, Data: $eventData")
val data = json.get("data")
when (eventId) {
"TRANSAK_WIDGET_CLOSE" -> {
val isOrderSuccessful = data == true // For unsuccessful order data is JSONObject
closeListener.onTradeOperationFinished(isOrderSuccessful)
}
"TRANSAK_ORDER_CREATED" -> {
require(data is JSONObject)
if (data.getString("isBuyOrSell") == "SELL") {
tradeSellCallback.onSellOrderCreated(
data.getString("id"),
data.getJSONObject("cryptoPaymentData").getString("paymentAddress"),
data.getString("cryptoAmount").toBigDecimal()
)
}
}
}
}
}
@@ -0,0 +1,73 @@
package io.novafoundation.nova.feature_buy_impl.presentation.trade.providers.transak
import io.novafoundation.nova.feature_buy_api.presentation.trade.TradeTokenRegistry
import io.novafoundation.nova.feature_buy_api.presentation.trade.common.OnTradeOperationFinishedListener
import io.novafoundation.nova.feature_buy_api.presentation.trade.common.OnSellOrderCreatedListener
import io.novafoundation.nova.feature_buy_api.presentation.trade.providers.WebViewIntegrationProvider
import io.novafoundation.nova.feature_buy_impl.R
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
const val TRANSAK_NETWORK_KEY = "network"
class TransakProvider(
private val host: String,
private val referrerDomain: String,
private val environment: String
) : WebViewIntegrationProvider {
override val id = "transak"
override val name = "Transak"
override val officialUrl: String = "transak.com"
override val logoRes: Int = R.drawable.ic_transak_provider_logo
override fun getDescriptionRes(tradeType: TradeTokenRegistry.TradeType): Int {
return when (tradeType) {
TradeTokenRegistry.TradeType.BUY -> R.string.transak_provider_buy_description
TradeTokenRegistry.TradeType.SELL -> R.string.transak_provider_sell_description
}
}
override fun getPaymentMethods(tradeType: TradeTokenRegistry.TradeType): List<TradeTokenRegistry.PaymentMethod> {
return when (tradeType) {
TradeTokenRegistry.TradeType.BUY -> listOf(
TradeTokenRegistry.PaymentMethod.Visa,
TradeTokenRegistry.PaymentMethod.MasterCard,
TradeTokenRegistry.PaymentMethod.ApplePay,
TradeTokenRegistry.PaymentMethod.GooglePay,
TradeTokenRegistry.PaymentMethod.Sepa,
TradeTokenRegistry.PaymentMethod.Other(12)
)
TradeTokenRegistry.TradeType.SELL -> listOf(
TradeTokenRegistry.PaymentMethod.Visa,
TradeTokenRegistry.PaymentMethod.MasterCard,
TradeTokenRegistry.PaymentMethod.Sepa,
TradeTokenRegistry.PaymentMethod.BankTransfer
)
}
}
override fun createIntegrator(
chainAsset: Chain.Asset,
address: String,
tradeFlow: TradeTokenRegistry.TradeType,
onCloseListener: OnTradeOperationFinishedListener,
onSellOrderCreatedListener: OnSellOrderCreatedListener
): WebViewIntegrationProvider.Integrator {
val network = chainAsset.buyProviders.getValue(id)[TRANSAK_NETWORK_KEY] as? String
return TransakIntegrator(
payload = TransakIntegrator.Payload(
host = host,
referrerDomain = referrerDomain,
environment = environment,
network = network,
tokenSymbol = chainAsset.symbol,
address = address,
tradeFlow = tradeFlow
),
onCloseListener,
onSellOrderCreatedListener
)
}
}
@@ -0,0 +1,49 @@
<?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/secondary_screen_background">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_primary_list_item"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingTop="12dp"
android:paddingEnd="19dp"
android:paddingBottom="12dp">
<ImageView
android:id="@+id/itemSheetBuyProviderImage"
android:layout_width="24dp"
android:layout_height="24dp"
tools:src="@drawable/ic_people_outline"
app:tint="@color/icon_primary"
tools:tint="@color/text_primary" />
<TextView
android:id="@+id/itemSheetBuyProviderText"
style="@style/TextAppearance.NovaFoundation.Body1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_weight="1"
android:includeFontPadding="false"
android:textColor="@color/text_primary"
tools:text="Ramp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
app:tint="@color/icon_primary"
android:src="@drawable/ic_chevron_right" />
</LinearLayout>
</LinearLayout>