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
+1
View File
@@ -0,0 +1 @@
/build
+101
View File
@@ -0,0 +1,101 @@
apply plugin: 'kotlin-parcelize'
apply from: '../tests.gradle'
apply from: '../scripts/secrets.gradle'
android {
namespace 'io.novafoundation.nova.feature_buy_impl'
defaultConfig {
buildConfigField "String", "RAMP_TOKEN", "\"n8ev677z3z7enckabyc249j84ajpc28o9tmsgob7\""
buildConfigField "String", "RAMP_HOST", "\"ri-widget-staging.firebaseapp.com\""
buildConfigField "String", "TRANSAK_HOST", "\"pezkuwi-transak-dev.pezkuwichain.io\""
buildConfigField "String", "MOONPAY_PRIVATE_KEY", readStringSecret("MOONPAY_TEST_SECRET")
buildConfigField "String", "MOONPAY_HOST", "\"buy-staging.moonpay.com\""
buildConfigField "String", "MOONPAY_PUBLIC_KEY", "\"pk_test_DMRuyL6Nf1qc9OzjPBmCFBeCGkFwiZs0\""
buildConfigField "String", "MERCURYO_WIDGET_ID", "\"fde83da2-2a4c-4af9-a2ca-30aead5d65a0\""
buildConfigField "String", "MERCURYO_SECRET", readStringSecret("MERCURYO_TEST_SECRET")
buildConfigField "String", "MERCURYO_HOST", "\"sandbox-exchange.mrcr.io\""
buildConfigField "String", "BANXA_HOST", "\"pezkuwi.banxa-sandbox.com\""
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
buildConfigField "String", "RAMP_TOKEN", "\"6hrtmyabadyjf6q4jc6h45yv3k8h7s88ebgubscd\""
buildConfigField "String", "RAMP_HOST", "\"buy.ramp.network\""
buildConfigField "String", "TRANSAK_HOST", "\"pezkuwi-transak.pezkuwichain.io\""
buildConfigField "String", "MOONPAY_PRIVATE_KEY", readStringSecret("MOONPAY_PRODUCTION_SECRET")
buildConfigField "String", "MOONPAY_PUBLIC_KEY", "\"pk_live_Boi6Rl107p7XuJWBL8GJRzGWlmUSoxbz\""
buildConfigField "String", "MOONPAY_HOST", "\"buy.moonpay.com\""
buildConfigField "String", "MERCURYO_WIDGET_ID", "\"07c3ca04-f4a8-4d68-a192-83a1794ba705\""
buildConfigField "String", "MERCURYO_SECRET", readStringSecret("MERCURYO_PRODUCTION_SECRET")
buildConfigField "String", "MERCURYO_HOST", "\"exchange.mercuryo.io\""
buildConfigField "String", "BANXA_HOST", "\"pezkuwi.banxa.com\""
}
}
packagingOptions {
resources.excludes.add("META-INF/NOTICE.md")
}
buildFeatures {
viewBinding true
}
}
dependencies {
implementation project(':core-db')
implementation project(':common')
implementation project(':feature-wallet-api')
implementation project(':feature-account-api')
implementation project(':feature-buy-api')
implementation project(':runtime')
implementation project(':feature-deep-linking')
implementation kotlinDep
implementation androidDep
implementation materialDep
implementation constraintDep
implementation coroutinesDep
implementation coroutinesAndroidDep
implementation viewModelKtxDep
implementation liveDataKtxDep
implementation lifeCycleKtxDep
implementation daggerDep
ksp daggerCompiler
implementation roomDep
ksp roomCompiler
implementation lifecycleDep
ksp lifecycleCompiler
testImplementation jUnitDep
testImplementation mockitoDep
implementation insetterDep
implementation shimmerDep
androidTestImplementation androidTestRunnerDep
androidTestImplementation androidTestRulesDep
androidTestImplementation androidJunitDep
}
View File
+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,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>