Initial commit: Pezkuwi Wallet Android

Security hardened release:
- Code obfuscation enabled (minifyEnabled=true, shrinkResources=true)
- Sensitive files excluded (google-services.json, keystores)
- Branch.io key moved to BuildConfig placeholder
- Updated dependencies: OkHttp 4.12.0, Gson 2.10.1, BouncyCastle 1.77
- Comprehensive ProGuard rules for crypto wallet
- Navigation 2.7.7, Lifecycle 2.7.0, ConstraintLayout 2.1.4
This commit is contained in:
2026-02-12 05:19:41 +03:00
commit a294aa1a6b
7687 changed files with 441811 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+157
View File
@@ -0,0 +1,157 @@
apply plugin: 'kotlin-parcelize'
android {
defaultConfig {
buildConfigField "String", "WEBSITE_URL", "\"https://pezkuwichain.io\""
buildConfigField "String", "PRIVACY_URL", "\"https://pezkuwichain.io/privacy\""
buildConfigField "String", "TERMS_URL", "\"https://pezkuwichain.io/terms\""
buildConfigField "String", "GITHUB_URL", "\"https://github.com/pezkuwichain\""
buildConfigField "String", "TELEGRAM_URL", "\"https://t.me/pezkuwichain\""
buildConfigField "String", "TWITTER_URL", "\"https://twitter.com/pezkuwichain\""
buildConfigField "String", "RATE_URL", "\"market://details?id=${rootProject.applicationId}.${releaseApplicationSuffix}\""
buildConfigField "String", "EMAIL", "\"support@pezkuwichain.io\""
buildConfigField "String", "YOUTUBE_URL", "\"https://www.youtube.com/@SatoshiQazi\""
buildConfigField "String", "TWITTER_ACCOUNT_TEMPLATE", "\"https://twitter.com/%s\""
buildConfigField "String", "RECOMMENDED_VALIDATORS_LEARN_MORE", "\"https://docs.pezkuwichain.io/wallet-wiki/staking/staking-faq#q-how-does-pezkuwi-wallet-select-validators-collators\""
buildConfigField "String", "PAYOUTS_LEARN_MORE", "\"https://docs.pezkuwichain.io/wallet-wiki/staking/staking-faq#q-what-is-the-difference-between-restake-rewards-and-transferable-rewards\""
buildConfigField "String", "SET_CONTROLLER_LEARN_MORE", "\"https://docs.pezkuwichain.io/wallet-wiki/staking/staking-faq#q-what-are-stash-and-controller-accounts\""
buildConfigField "String", "SET_CONTROLLER_DEPRECATED_LEARN_MORE", "\"https://docs.pezkuwichain.io/wallet-wiki/staking/controller-account-deprecation\""
buildConfigField "String", "PARITY_SIGNER_TROUBLESHOOTING", "\"https://docs.pezkuwichain.io/wallet-wiki/hardware-wallets/parity-signer/troubleshooting\""
buildConfigField "String", "POLKADOT_VAULT_TROUBLESHOOTING", "\"https://docs.pezkuwichain.io/wallet-wiki/hardware-wallets/polkadot-vault/troubleshooting\""
buildConfigField "String", "PEZKUWI_WALLET_WIKI_BASE", "\"https://docs.pezkuwichain.io/wallet-wiki/about-pezkuwi-wallet\""
buildConfigField "String", "PEZKUWI_WALLET_WIKI_PROXY", "\"https://docs.pezkuwichain.io/wallet-wiki/wallet-management/delegated-authorities-proxies\""
buildConfigField "String", "PEZKUWI_WALLET_WIKI_INTEGRATE_NETWORK", "\"https://docs.pezkuwichain.io/wallet-wiki/misc/developer-documentation/integrate-network\""
buildConfigField "String", "LEDGER_MIGRATION_ARTICLE", "\"https://docs.pezkuwichain.io/wallet-wiki/wallet-management/hardware-wallets/ledger-nano-x/ledger-app-migration\""
buildConfigField "String", "LEDGER_CONNECTION_GUIDE", "\"https://docs.pezkuwichain.io/wallet-wiki/wallet-management/hardware-wallets/ledger-devices\""
buildConfigField "String", "APP_UPDATE_SOURCE_LINK", "\"https://wallet.pezkuwichain.io\""
buildConfigField "String", "PEZKUWI_CARD_WIDGET_URL", "\"https://exchange.mercuryo.io\""
buildConfigField "String", "ASSET_COLORED_ICON_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/main/icons/tokens/colored\""
buildConfigField "String", "ASSET_WHITE_ICON_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/main/icons/tokens/white\""
buildConfigField "String", "UNIFIED_ADDRESS_ARTICLE", "\"https://docs.pezkuwichain.io/wallet-wiki/asset-management/how-to-receive-tokens#unified-and-legacy-addresses\""
buildConfigField "String", "MULTISIGS_WIKI_URL", "\"https://docs.pezkuwichain.io/wallet-wiki/wallet-management/multisig-wallets\""
buildConfigField "String", "GIFTS_WIKI_URL", "\"https://docs.pezkuwichain.io/wallet-wiki/asset-management/gifting-tokens\""
buildConfigField "long", "CLOUD_PROJECT_NUMBER", "171267697857L"
buildConfigField "String", "GLOBAL_CONFIG_URL", "\"https://wallet.pezkuwichain.io/config/global_config_dev.json\""
}
buildTypes {
debug {
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
buildConfigField "long", "CLOUD_PROJECT_NUMBER", "802342409053L"
buildConfigField "String", "GLOBAL_CONFIG_URL", "\"https://wallet.pezkuwichain.io/config/global_config.json\""
}
releaseGithub {
initWith buildTypes.release
matchingFallbacks = ['release']
buildConfigField "String", "APP_UPDATE_SOURCE_LINK", "\"https://github.com/pezkuwichain/pezWallet/releases\""
}
}
namespace 'io.novafoundation.nova.common'
buildFeatures {
viewBinding true
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation(name: 'renderscript-toolkit', ext: 'aar')
api project(":core-api")
implementation kotlinDep
implementation androidDep
implementation cardViewDep
implementation recyclerViewDep
implementation materialDep
implementation constraintDep
implementation biometricDep
implementation bouncyCastleDep
api substrateSdkDep
implementation coroutinesDep
api liveDataKtxDep
implementation lifeCycleKtxDep
implementation viewModelKtxDep
implementation daggerDep
ksp daggerCompiler
implementation lifecycleDep
ksp lifecycleCompiler
implementation retrofitDep
api gsonConvertedDep
implementation scalarsConverterDep
implementation interceptorVersion
implementation zXingCoreDep
implementation zXingEmbeddedDep
implementation progressButtonDep
implementation wsDep
api insetterDep
api coilDep
api coilSvg
api web3jDep
api coroutinesFutureDep
api coroutinesRxDep
implementation shimmerDep
implementation playIntegrity
testImplementation jUnitDep
testImplementation mockitoDep
testImplementation project(':test-shared')
implementation permissionsDep
implementation flexBoxDep
implementation markwonDep
implementation markwonImage
implementation markwonTables
implementation markwonLinkify
implementation markwonStrikethrough
implementation markwonHtml
implementation kotlinReflect
implementation playServicesAuthDep
}
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>
@@ -0,0 +1,49 @@
package io.novafoundation.nova.common.address
import io.novasama.substrate_sdk_android.extensions.fromHex
import io.novafoundation.nova.common.utils.HexString
import io.novasama.substrate_sdk_android.extensions.toHexString
import io.novasama.substrate_sdk_android.runtime.AccountId
class AccountIdKey(val value: AccountId) {
companion object;
override fun equals(other: Any?): Boolean {
return this === other || other is AccountIdKey && this.value contentEquals other.value
}
override fun hashCode(): Int = value.contentHashCode()
override fun toString(): String = value.contentToString()
}
fun AccountId.intoKey() = AccountIdKey(this)
fun AccountIdKey.toHex(): HexString {
return value.toHexString()
}
fun AccountIdKey.toHexWithPrefix(): HexString {
return value.toHexString(withPrefix = true)
}
fun AccountIdKey.Companion.fromHex(src: HexString): Result<AccountIdKey> {
return runCatching { src.fromHex().intoKey() }
}
fun AccountIdKey.Companion.fromHexOrNull(src: HexString): AccountIdKey? {
return fromHex(src).getOrNull()
}
fun AccountIdKey.Companion.fromHexOrThrow(src: HexString): AccountIdKey {
return fromHex(src).getOrThrow()
}
operator fun <T> Map<AccountIdKey, T>.get(key: AccountId) = get(AccountIdKey(key))
fun <T> Map<AccountIdKey, T>.getValue(key: AccountId) = getValue(AccountIdKey(key))
interface WithAccountId {
val accountId: AccountIdKey
}
@@ -0,0 +1,20 @@
package io.novafoundation.nova.common.address
import android.os.Parcelable
import io.novasama.substrate_sdk_android.extensions.fromHex
import io.novasama.substrate_sdk_android.runtime.AccountId
import kotlinx.android.parcel.Parcelize
@Parcelize
class AccountIdParcel(private val value: AccountId) : Parcelable {
companion object {
fun fromHex(hexAccountId: String): AccountIdParcel {
return AccountIdParcel(hexAccountId.fromHex())
}
}
val accountId: AccountIdKey
get() = value.intoKey()
}
@@ -0,0 +1,44 @@
package io.novafoundation.nova.common.address
import com.google.gson.JsonArray
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import java.lang.reflect.Type
class AccountIdSerializer : JsonSerializer<AccountIdKey>, JsonDeserializer<AccountIdKey> {
override fun serialize(src: AccountIdKey, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
return JsonPrimitive(src.toHex())
}
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): AccountIdKey {
return AccountIdKey.fromHex(json.asString).getOrThrow()
}
}
class AccountIdKeyListAdapter : JsonSerializer<List<AccountIdKey>>, JsonDeserializer<List<AccountIdKey>> {
private val delegate = AccountIdSerializer()
override fun serialize(
src: List<AccountIdKey>,
typeOfSrc: Type,
context: JsonSerializationContext
): JsonElement {
val jsonArray = JsonArray()
src.forEach { jsonArray.add(delegate.serialize(it, AccountIdKey::class.java, context)) }
return jsonArray
}
override fun deserialize(
json: JsonElement,
typeOfT: Type,
context: JsonDeserializationContext
): List<AccountIdKey> {
return json.asJsonArray.map {
delegate.deserialize(it, AccountIdKey::class.java, context)
}
}
}
@@ -0,0 +1,97 @@
package io.novafoundation.nova.common.address
import android.graphics.drawable.Drawable
import androidx.annotation.ColorRes
import io.novafoundation.nova.common.R
import io.novafoundation.nova.common.resources.ResourceManager
import io.novasama.substrate_sdk_android.exceptions.AddressFormatException
import io.novasama.substrate_sdk_android.extensions.toHexString
import io.novasama.substrate_sdk_android.icon.IconGenerator
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentHashMap
// TODO ethereum address icon generation
interface AddressIconGenerator {
companion object {
const val SIZE_SMALL = 18
const val SIZE_MEDIUM = 24
const val SIZE_BIG = 32
val BACKGROUND_LIGHT = R.color.address_icon_background
val BACKGROUND_TRANSPARENT = android.R.color.transparent
val BACKGROUND_DEFAULT = BACKGROUND_LIGHT
}
suspend fun createAddressIcon(
accountId: AccountId,
sizeInDp: Int,
@ColorRes backgroundColorRes: Int = BACKGROUND_DEFAULT
): Drawable
}
@Throws(AddressFormatException::class)
suspend fun AddressIconGenerator.createSubstrateAddressModel(
accountAddress: String,
sizeInDp: Int,
accountName: String? = null,
@ColorRes background: Int = AddressIconGenerator.BACKGROUND_DEFAULT
): AddressModel {
val icon = createSubstrateAddressIcon(accountAddress, sizeInDp, background)
return AddressModel(accountAddress, icon, accountName)
}
@Throws(AddressFormatException::class)
suspend fun AddressIconGenerator.createSubstrateAddressIcon(
accountAddress: String,
sizeInDp: Int,
@ColorRes background: Int = AddressIconGenerator.BACKGROUND_DEFAULT
) = withContext(Dispatchers.Default) {
val addressId = accountAddress.toAccountId()
createAddressIcon(addressId, sizeInDp, background)
}
class CachingAddressIconGenerator(
private val delegate: AddressIconGenerator
) : AddressIconGenerator {
val cache = ConcurrentHashMap<String, Drawable>()
override suspend fun createAddressIcon(
accountId: AccountId,
sizeInDp: Int,
@ColorRes backgroundColorRes: Int
): Drawable = withContext(Dispatchers.Default) {
val key = "${accountId.toHexString()}:$sizeInDp:$backgroundColorRes"
cache.getOrPut(key) {
delegate.createAddressIcon(accountId, sizeInDp, backgroundColorRes)
}
}
}
class StatelessAddressIconGenerator(
private val iconGenerator: IconGenerator,
private val resourceManager: ResourceManager
) : AddressIconGenerator {
override suspend fun createAddressIcon(
accountId: AccountId,
sizeInDp: Int,
@ColorRes backgroundColorRes: Int
) = withContext(Dispatchers.Default) {
val sizeInPx = resourceManager.measureInPx(sizeInDp)
val backgroundColor = resourceManager.getColor(backgroundColorRes)
val drawable = iconGenerator.getSvgImage(accountId, sizeInPx, backgroundColor = backgroundColor)
drawable.setBounds(0, 0, sizeInPx, sizeInPx)
drawable
}
}
@@ -0,0 +1,19 @@
package io.novafoundation.nova.common.address
import android.graphics.drawable.Drawable
open class AddressModel(
val address: String,
val image: Drawable,
val name: String? = null
) {
val nameOrAddress = name ?: address
}
class OptionalAddressModel(
val address: String,
val image: Drawable?,
val name: String? = null
) {
val nameOrAddress = name ?: address
}
@@ -0,0 +1,49 @@
package io.novafoundation.nova.common.address.format
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.utils.UNIFIED_ADDRESS_PREFIX
import io.novasama.substrate_sdk_android.ss58.SS58Encoder
interface AddressFormat {
val scheme: AddressScheme
companion object {
fun evm(): AddressFormat {
return EthereumAddressFormat()
}
fun defaultForScheme(scheme: AddressScheme, substrateAddressPrefix: Short = SS58Encoder.UNIFIED_ADDRESS_PREFIX): AddressFormat {
return when (scheme) {
AddressScheme.EVM -> EthereumAddressFormat()
AddressScheme.SUBSTRATE -> SubstrateAddressFormat.forSS58rPrefix(substrateAddressPrefix)
}
}
}
@JvmInline
value class PublicKey(val value: ByteArray)
@JvmInline
value class AccountId(val value: ByteArray)
@JvmInline
value class Address(val value: String)
fun addressOf(accountId: AccountId): Address
fun accountIdOf(address: Address): AccountId
fun accountIdOf(publicKey: PublicKey): AccountId
fun isValidAddress(address: Address): Boolean
}
fun ByteArray.asPublicKey() = AddressFormat.PublicKey(this)
fun ByteArray.asAccountId() = AddressFormat.AccountId(this)
fun String.asAddress() = AddressFormat.Address(this)
fun AddressFormat.addressOf(accountIdKey: AccountIdKey): AddressFormat.Address {
return addressOf(accountIdKey.value.asAccountId())
}
@@ -0,0 +1,51 @@
package io.novafoundation.nova.common.address.format
import io.novafoundation.nova.common.address.AccountIdKey
import io.novasama.substrate_sdk_android.runtime.AccountId
enum class AddressScheme {
/**
* 20-byte address, Ethereum-like address encoding
*/
EVM,
/**
* 32-byte address, ss58 address encoding
*/
SUBSTRATE;
companion object {
fun findFromAccountId(accountId: AccountId): AddressScheme? {
return when (accountId.size) {
32 -> SUBSTRATE
20 -> EVM
else -> null
}
}
}
}
fun AccountIdKey.getAddressScheme(): AddressScheme? {
return AddressScheme.findFromAccountId(value)
}
fun AccountIdKey.getAddressSchemeOrThrow(): AddressScheme {
return requireNotNull(getAddressScheme()) {
"Could not detect address scheme from account id of length ${value.size}"
}
}
val AddressScheme.defaultOrdering
get() = when (this) {
AddressScheme.SUBSTRATE -> 0
AddressScheme.EVM -> 1
}
fun AddressScheme.isSubstrate(): Boolean {
return this == AddressScheme.SUBSTRATE
}
fun AddressScheme.isEvm(): Boolean {
return this == AddressScheme.EVM
}
@@ -0,0 +1,33 @@
package io.novafoundation.nova.common.address.format
import io.novafoundation.nova.common.R
import io.novafoundation.nova.common.di.scope.ApplicationScope
import io.novafoundation.nova.common.resources.ResourceManager
import javax.inject.Inject
interface AddressSchemeFormatter {
fun addressLabel(addressScheme: AddressScheme): String
fun accountsLabel(addressScheme: AddressScheme): String
}
@ApplicationScope
internal class RealAddressSchemeFormatter @Inject constructor(
private val resourceManager: ResourceManager
) : AddressSchemeFormatter {
override fun addressLabel(addressScheme: AddressScheme): String {
return when (addressScheme) {
AddressScheme.SUBSTRATE -> resourceManager.getString(R.string.common_substrate_address)
AddressScheme.EVM -> resourceManager.getString(R.string.common_evm_address)
}
}
override fun accountsLabel(addressScheme: AddressScheme): String {
return when (addressScheme) {
AddressScheme.SUBSTRATE -> resourceManager.getString(R.string.account_substrate_accounts)
AddressScheme.EVM -> resourceManager.getString(R.string.account_evm_accounts)
}
}
}
@@ -0,0 +1,32 @@
package io.novafoundation.nova.common.address.format
import io.novasama.substrate_sdk_android.extensions.asEthereumAccountId
import io.novasama.substrate_sdk_android.extensions.asEthereumAddress
import io.novasama.substrate_sdk_android.extensions.asEthereumPublicKey
import io.novasama.substrate_sdk_android.extensions.isValid
import io.novasama.substrate_sdk_android.extensions.toAccountId
import io.novasama.substrate_sdk_android.extensions.toAddress
class EthereumAddressFormat : AddressFormat {
override val scheme: AddressScheme = AddressScheme.EVM
override fun addressOf(accountId: AddressFormat.AccountId): AddressFormat.Address {
return accountId.value.asEthereumAccountId()
.toAddress().value.asAddress()
}
override fun accountIdOf(address: AddressFormat.Address): AddressFormat.AccountId {
return address.value.asEthereumAddress()
.toAccountId().value.asAccountId()
}
override fun accountIdOf(publicKey: AddressFormat.PublicKey): AddressFormat.AccountId {
return publicKey.value.asEthereumPublicKey()
.toAccountId().value.asAccountId()
}
override fun isValidAddress(address: AddressFormat.Address): Boolean {
return address.value.asEthereumAddress().isValid()
}
}
@@ -0,0 +1,47 @@
package io.novafoundation.nova.common.address.format
import io.novafoundation.nova.common.utils.GENERIC_ADDRESS_PREFIX
import io.novafoundation.nova.common.utils.substrateAccountId
import io.novasama.substrate_sdk_android.ss58.SS58Encoder
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.addressPrefix
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAddress
class SubstrateAddressFormat private constructor(
private val addressPrefix: Short?
) : AddressFormat {
override val scheme: AddressScheme = AddressScheme.SUBSTRATE
companion object {
fun forSS58rPrefix(prefix: Short): SubstrateAddressFormat {
return SubstrateAddressFormat(prefix)
}
}
override fun addressOf(accountId: AddressFormat.AccountId): AddressFormat.Address {
val addressPrefixOrDefault = addressPrefix ?: SS58Encoder.GENERIC_ADDRESS_PREFIX
return accountId.value.toAddress(addressPrefixOrDefault).asAddress()
}
override fun accountIdOf(address: AddressFormat.Address): AddressFormat.AccountId {
val accountId = address.value.toAccountId()
addressPrefix?.let {
require(addressPrefix == address.value.addressPrefix()) {
"Address prefix mismatch. Expected: $addressPrefix, Got: ${address.value}"
}
}
return accountId.asAccountId()
}
override fun accountIdOf(publicKey: AddressFormat.PublicKey): AddressFormat.AccountId {
return publicKey.value.substrateAccountId().asAccountId()
}
override fun isValidAddress(address: AddressFormat.Address): Boolean {
return kotlin.runCatching { accountIdOf(address) }.isSuccess
}
}
@@ -0,0 +1,56 @@
package io.novafoundation.nova.common.base
import android.content.Context
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.lifecycle.LifecycleOwner
import androidx.viewbinding.ViewBinding
import io.novafoundation.nova.common.di.FeatureContainer
import io.novafoundation.nova.common.utils.showToast
import javax.inject.Inject
abstract class BaseActivity<T : BaseViewModel, B : ViewBinding> :
AppCompatActivity(), BaseScreenMixin<T> {
override val providedContext: Context
get() = this
override val lifecycleOwner: LifecycleOwner
get() = this
protected lateinit var binder: B
private set
@Inject
override lateinit var viewModel: T
protected abstract fun createBinding(): B
override fun attachBaseContext(base: Context) {
val commonApi = (base.applicationContext as FeatureContainer).commonApi()
val contextManager = commonApi.contextManager()
applyOverrideConfiguration(contextManager.setLocale(base).resources.configuration)
super.attachBaseContext(contextManager.setLocale(base))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
binder = createBinding()
setContentView(binder.root)
inject()
initViews()
subscribe(viewModel)
viewModel.errorLiveData.observeEvent(::showError)
viewModel.toastLiveData.observeEvent { showToast(it) }
}
abstract fun changeLanguage()
}
@@ -0,0 +1,43 @@
package io.novafoundation.nova.common.base
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import javax.inject.Inject
abstract class BaseBottomSheetFragment<T : BaseViewModel, B : ViewBinding> : BottomSheetDialogFragment(), BaseFragmentMixin<T> {
@Inject
override lateinit var viewModel: T
protected lateinit var binder: B
private set
override val fragment: Fragment
get() = this
private val delegate by lazy(LazyThreadSafetyMode.NONE) { BaseFragmentDelegate(this) }
protected abstract fun createBinding(): B
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binder = createBinding()
return binder.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
delegate.onViewCreated(view, savedInstanceState)
}
protected fun getBehaviour(): BottomSheetBehavior<*> {
return (dialog as BottomSheetDialog).behavior
}
}
@@ -0,0 +1,36 @@
package io.novafoundation.nova.common.base
import java.io.IOException
class BaseException(
val kind: Kind,
message: String,
exception: Throwable? = null
) : RuntimeException(message, exception) {
enum class Kind {
BUSINESS,
NETWORK,
HTTP,
UNEXPECTED
}
companion object {
fun businessError(message: String): BaseException {
return BaseException(Kind.BUSINESS, message)
}
fun httpError(errorCode: Int, message: String): BaseException {
return BaseException(Kind.HTTP, message)
}
fun networkError(message: String, exception: IOException): BaseException {
return BaseException(Kind.NETWORK, message, exception)
}
fun unexpectedError(exception: Throwable): BaseException {
return BaseException(Kind.UNEXPECTED, exception.message ?: "", exception)
}
}
}
@@ -0,0 +1,64 @@
package io.novafoundation.nova.common.base
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.children
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
import io.novafoundation.nova.common.utils.insets.applySystemBarInsets
import javax.inject.Inject
abstract class BaseFragment<T : BaseViewModel, B : ViewBinding> : Fragment(), BaseFragmentMixin<T> {
@Inject
override lateinit var viewModel: T
protected lateinit var binder: B
private set
override val fragment: Fragment
get() = this
private val delegate by lazy(LazyThreadSafetyMode.NONE) { BaseFragmentDelegate(this) }
protected abstract fun createBinding(): B
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binder = createBinding()
return binder.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
applyInsetsToChildrenLegacy()
applyInsets(view)
delegate.onViewCreated(view, savedInstanceState)
}
open fun applyInsets(rootView: View) {
rootView.applySystemBarInsets()
}
/**
* Fix insets for android 7-10.
* For some reason Fragments doesn't send insets to their children after root view so we push them forcibly
* TODO: I haven't found the reason of this issue so I think this fix is temporary until we found the reason
*/
private fun applyInsetsToChildrenLegacy() {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
ViewCompat.setOnApplyWindowInsetsListener(binder.root) { view, insets ->
val viewGroup = (view as? ViewGroup) ?: return@setOnApplyWindowInsetsListener insets
viewGroup.children.forEach {
ViewCompat.dispatchApplyWindowInsets(it, insets)
}
insets
}
}
}
}
@@ -0,0 +1,92 @@
package io.novafoundation.nova.common.base
import android.content.Context
import android.os.Bundle
import android.view.View
import android.widget.EditText
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import io.novafoundation.nova.common.utils.bindTo
import io.novafoundation.nova.common.utils.showToast
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
interface BaseFragmentMixin<T : BaseViewModel> : BaseScreenMixin<T> {
val fragment: Fragment
override val providedContext: Context
get() = fragment.requireContext()
override val lifecycleOwner: LifecycleOwner
get() = fragment.viewLifecycleOwner
fun onBackPressed(action: () -> Unit) {
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
action()
}
}
fragment.requireActivity().onBackPressedDispatcher.addCallback(fragment.viewLifecycleOwner, callback)
}
fun <V> Flow<V>.observe(collector: suspend (V) -> Unit) {
fragment.lifecycleScope.launchWhenResumed {
collect(collector)
}
}
fun <V> Flow<V>.observeWhenCreated(collector: suspend (V) -> Unit) {
fragment.lifecycleScope.launchWhenCreated {
collect(collector)
}
}
fun <V> Flow<V>.observeFirst(collector: suspend (V) -> Unit) {
fragment.lifecycleScope.launchWhenCreated {
collector(first())
}
}
fun <V> Flow<V>.observeWhenVisible(collector: suspend (V) -> Unit) {
fragment.viewLifecycleOwner.lifecycleScope.launchWhenResumed {
collect(collector)
}
}
fun <V> LiveData<V>.observe(observer: (V) -> Unit) {
observe(fragment.viewLifecycleOwner, observer)
}
fun EditText.bindTo(liveData: MutableLiveData<String>) = bindTo(liveData, fragment.viewLifecycleOwner)
@Suppress("UNCHECKED_CAST")
fun <A> argument(key: String): A = fragment.arguments!![key] as A
@Suppress("UNCHECKED_CAST")
fun <A> argumentOrNull(key: String): A? = fragment.arguments?.get(key) as? A
}
class BaseFragmentDelegate<T : BaseViewModel>(
private val mixin: BaseFragmentMixin<T>
) {
fun onViewCreated(view: View, savedInstanceState: Bundle?) = with(mixin) {
inject()
initViews()
subscribe(viewModel)
viewModel.errorLiveData.observeEvent(::showError)
viewModel.errorWithTitleLiveData.observeEvent {
showErrorWithTitle(it.first, it.second)
}
viewModel.toastLiveData.observeEvent { view.context.showToast(it) }
}
}
@@ -0,0 +1,5 @@
package io.novafoundation.nova.common.base
fun BaseFragmentMixin<*>.blockBackPressing() = onBackPressed {
// do nothing
}
@@ -0,0 +1,39 @@
package io.novafoundation.nova.common.base
import android.widget.Toast
import io.novafoundation.nova.common.R
import io.novafoundation.nova.common.utils.WithContextExtensions
import io.novafoundation.nova.common.utils.WithLifecycleExtensions
import io.novafoundation.nova.common.view.dialog.dialog
interface BaseScreenMixin<T : BaseViewModel> : WithContextExtensions, WithLifecycleExtensions {
val viewModel: T
fun initViews()
fun inject()
fun subscribe(viewModel: T)
fun showError(errorMessage: String) {
dialog(providedContext) {
setTitle(providedContext.getString(R.string.common_error_general_title))
setMessage(errorMessage)
setPositiveButton(R.string.common_ok) { _, _ -> }
}
}
fun showErrorWithTitle(title: String, errorMessage: CharSequence?) {
dialog(providedContext) {
setTitle(title)
setMessage(errorMessage)
setPositiveButton(R.string.common_ok) { _, _ -> }
}
}
fun showMessage(text: String) {
Toast.makeText(providedContext, text, Toast.LENGTH_SHORT)
.show()
}
}
@@ -0,0 +1,98 @@
package io.novafoundation.nova.common.base
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import io.novafoundation.nova.common.base.errors.shouldIgnore
import io.novafoundation.nova.common.data.memory.ComputationalScope
import io.novafoundation.nova.common.utils.Event
import io.novafoundation.nova.common.utils.WithCoroutineScopeExtensions
import io.novafoundation.nova.common.validation.ProgressConsumer
import io.novafoundation.nova.common.validation.TransformedFailure
import io.novafoundation.nova.common.validation.ValidationExecutor
import io.novafoundation.nova.common.validation.ValidationFlowActions
import io.novafoundation.nova.common.validation.ValidationStatus
import io.novafoundation.nova.common.validation.ValidationSystem
import kotlinx.coroutines.CoroutineScope
import kotlin.coroutines.CoroutineContext
typealias TitleAndMessage = Pair<String, CharSequence?>
open class BaseViewModel :
ViewModel(),
CoroutineScope,
ComputationalScope,
WithCoroutineScopeExtensions {
private val _errorLiveData = MutableLiveData<Event<String>>()
val errorLiveData: LiveData<Event<String>> = _errorLiveData
private val _errorWithTitleLiveData = MutableLiveData<Event<TitleAndMessage>>()
val errorWithTitleLiveData: LiveData<Event<TitleAndMessage>> = _errorWithTitleLiveData
private val _toastLiveData = MutableLiveData<Event<String>>()
val toastLiveData: LiveData<Event<String>> = _toastLiveData
fun showToast(text: String) {
_toastLiveData.postValue(Event(text))
}
fun showError(title: String, text: CharSequence) {
_errorWithTitleLiveData.postValue(Event(title to text))
}
fun showError(text: String) {
_errorLiveData.postValue(Event(text))
}
fun showError(throwable: Throwable) {
if (!shouldIgnore(throwable)) {
throwable.printStackTrace()
throwable.message?.let(this::showError)
}
}
override val coroutineContext: CoroutineContext
get() = viewModelScope.coroutineContext
override val coroutineScope: CoroutineScope
get() = this
suspend fun <P, S> ValidationExecutor.requireValid(
validationSystem: ValidationSystem<P, S>,
payload: P,
validationFailureTransformer: (S) -> TitleAndMessage,
progressConsumer: ProgressConsumer? = null,
autoFixPayload: (original: P, failureStatus: S) -> P = { original, _ -> original },
block: (P) -> Unit,
) = requireValid(
validationSystem = validationSystem,
payload = payload,
errorDisplayer = { showError(it) },
validationFailureTransformerDefault = validationFailureTransformer,
progressConsumer = progressConsumer,
autoFixPayload = autoFixPayload,
block = block,
scope = viewModelScope
)
suspend fun <P, S> ValidationExecutor.requireValid(
validationSystem: ValidationSystem<P, S>,
payload: P,
validationFailureTransformerCustom: (ValidationStatus.NotValid<S>, ValidationFlowActions<P>) -> TransformedFailure?,
autoFixPayload: (original: P, failureStatus: S) -> P = { original, _ -> original },
progressConsumer: ProgressConsumer? = null,
block: (P) -> Unit,
) = requireValid(
validationSystem = validationSystem,
payload = payload,
errorDisplayer = ::showError,
validationFailureTransformerCustom = validationFailureTransformerCustom,
progressConsumer = progressConsumer,
autoFixPayload = autoFixPayload,
block = block,
scope = viewModelScope
)
}
@@ -0,0 +1,9 @@
package io.novafoundation.nova.common.base
fun BaseViewModel.showError(model: TitleAndMessage) {
if (model.second != null) {
showError(model.first, model.second!!)
} else {
showError(model.first)
}
}
@@ -0,0 +1,3 @@
package io.novafoundation.nova.common.base.errors
class CompoundException(val nested: List<Throwable>) : Exception()
@@ -0,0 +1,26 @@
package io.novafoundation.nova.common.base.errors
import io.novafoundation.nova.common.resources.ResourceManager
class NovaException(
val kind: Kind,
message: String?,
exception: Throwable? = null,
) : RuntimeException(message, exception) {
enum class Kind {
NETWORK,
UNEXPECTED
}
companion object {
fun networkError(resourceManager: ResourceManager, throwable: Throwable): NovaException {
return NovaException(Kind.NETWORK, "", throwable) // TODO: add common error text to resources
}
fun unexpectedError(exception: Throwable): NovaException {
return NovaException(Kind.UNEXPECTED, exception.message ?: "", exception) // TODO: add common error text to resources
}
}
}
@@ -0,0 +1,3 @@
package io.novafoundation.nova.common.base.errors
class SigningCancelledException : Exception()
@@ -0,0 +1,3 @@
package io.novafoundation.nova.common.base.errors
fun shouldIgnore(exception: Throwable) = exception is SigningCancelledException
@@ -0,0 +1,39 @@
package io.novafoundation.nova.common.data
import android.content.Context
import android.net.Uri
import io.novafoundation.nova.common.interfaces.FileProvider
import java.io.File
import java.util.UUID
import androidx.core.content.FileProvider as AndroidFileProvider
class FileProviderImpl(
private val context: Context
) : FileProvider {
override fun getFileInExternalCacheStorage(fileName: String): File {
val cacheDir = context.externalCacheDir?.absolutePath ?: directoryNotAvailable()
return File(cacheDir, fileName)
}
override fun getFileInInternalCacheStorage(fileName: String): File {
val cacheDir = context.cacheDir?.absolutePath ?: directoryNotAvailable()
return File(cacheDir, fileName)
}
override fun generateTempFile(fixedName: String?): File {
val name = fixedName ?: UUID.randomUUID().toString()
return getFileInExternalCacheStorage(name)
}
override fun uriOf(file: File): Uri {
return AndroidFileProvider.getUriForFile(context, "${context.packageName}.provider", file)
}
private fun directoryNotAvailable(): Nothing {
throw IllegalStateException("Cache directory is unavailable")
}
}
@@ -0,0 +1,21 @@
package io.novafoundation.nova.common.data
import android.content.Context
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
interface GoogleApiAvailabilityProvider {
fun isAvailable(): Boolean
}
internal class RealGoogleApiAvailabilityProvider(
val context: Context
) : GoogleApiAvailabilityProvider {
override fun isAvailable(): Boolean {
val googleApiAvailability = GoogleApiAvailability.getInstance()
val resultCode = googleApiAvailability.isGooglePlayServicesAvailable(context)
return resultCode == ConnectionResult.SUCCESS
}
}
@@ -0,0 +1,10 @@
package io.novafoundation.nova.common.data.config
import io.novafoundation.nova.common.BuildConfig
import retrofit2.http.GET
interface GlobalConfigApi {
@GET(BuildConfig.GLOBAL_CONFIG_URL)
suspend fun getGlobalConfig(): GlobalConfigRemote
}
@@ -0,0 +1,37 @@
package io.novafoundation.nova.common.data.config
import io.novafoundation.nova.common.domain.config.GlobalConfig
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
interface GlobalConfigDataSource {
suspend fun getGlobalConfig(): GlobalConfig
}
class RealGlobalConfigDataSource(
private val api: GlobalConfigApi
) : GlobalConfigDataSource {
private var globalConfig: GlobalConfig? = null
private val mutex = Mutex()
override suspend fun getGlobalConfig(): GlobalConfig {
if (globalConfig != null) return globalConfig!!
mutex.withLock {
if (globalConfig != null) return globalConfig!!
val remoteConfig = api.getGlobalConfig()
globalConfig = remoteConfig.toDomain()
}
return globalConfig!!
}
private fun GlobalConfigRemote.toDomain() = GlobalConfig(
multisigsApiUrl = multisigsApiUrl,
proxyApiUrl = proxyApiUrl,
multiStakingApiUrl = multiStakingApiUrl
)
}
@@ -0,0 +1,7 @@
package io.novafoundation.nova.common.data.config
class GlobalConfigRemote(
val multisigsApiUrl: String,
val proxyApiUrl: String,
val multiStakingApiUrl: String
)
@@ -0,0 +1,6 @@
package io.novafoundation.nova.common.data.holders
interface ChainIdHolder {
suspend fun chainId(): String
}
@@ -0,0 +1,10 @@
package io.novafoundation.nova.common.data.holders
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
interface RuntimeHolder {
suspend fun runtime(): RuntimeSnapshot
}
suspend inline fun <T> RuntimeHolder.useRuntime(block: (RuntimeSnapshot) -> T) = block(runtime())
@@ -0,0 +1,20 @@
package io.novafoundation.nova.common.data.mappers
import io.novafoundation.nova.core.model.CryptoType
import io.novasama.substrate_sdk_android.encrypt.EncryptionType
fun mapCryptoTypeToEncryption(cryptoType: CryptoType): EncryptionType {
return when (cryptoType) {
CryptoType.SR25519 -> EncryptionType.SR25519
CryptoType.ED25519 -> EncryptionType.ED25519
CryptoType.ECDSA -> EncryptionType.ECDSA
}
}
fun mapEncryptionToCryptoType(cryptoType: EncryptionType): CryptoType {
return when (cryptoType) {
EncryptionType.SR25519 -> CryptoType.SR25519
EncryptionType.ED25519 -> CryptoType.ED25519
EncryptionType.ECDSA -> CryptoType.ECDSA
}
}
@@ -0,0 +1,34 @@
package io.novafoundation.nova.common.data.memory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
interface ComputationalCache {
/**
* Caches [computation] between calls until all supplied [scope]s have been cancelled
*/
suspend fun <T> useCache(
key: String,
scope: CoroutineScope,
computation: suspend CoroutineScope.() -> T
): T
fun <T> useSharedFlow(
key: String,
scope: CoroutineScope,
flowLazy: suspend CoroutineScope.() -> Flow<T>
): Flow<T>
}
context(ComputationalScope)
suspend fun <T> ComputationalCache.useCache(
key: String,
computation: suspend CoroutineScope.() -> T
): T = useCache(key, this@ComputationalScope, computation)
context(ComputationalScope)
fun <T> ComputationalCache.useSharedFlow(
key: String,
flowLazy: suspend CoroutineScope.() -> Flow<T>
): Flow<T> = useSharedFlow(key, this@ComputationalScope, flowLazy)
@@ -0,0 +1,13 @@
package io.novafoundation.nova.common.data.memory
import kotlinx.coroutines.CoroutineScope
/**
* A specialization of `CoroutineScope` to avoid context receiver pollution when used as `context(ComputationalScope)`
*/
interface ComputationalScope : CoroutineScope
fun ComputationalScope(scope: CoroutineScope): ComputationalScope = InlineComputationalScope(scope)
@JvmInline
private value class InlineComputationalScope(val scope: CoroutineScope) : ComputationalScope, CoroutineScope by scope
@@ -0,0 +1,71 @@
package io.novafoundation.nova.common.data.memory
import io.novafoundation.nova.common.utils.invokeOnCompletion
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
interface LazyAsyncCache<K, V> {
suspend fun getOrCompute(key: K): V
}
/**
* In-memory cache primitive that caches asynchronously computed value
* Lifetime of the cache itself is determine by supplied [CoroutineScope]
*/
fun <K, V> LazyAsyncCache(coroutineScope: CoroutineScope, compute: AsyncCacheCompute<K, V>): LazyAsyncCache<K, V> {
return RealLazyAsyncCache(coroutineScope, compute)
}
/**
* Specialization of [LazyAsyncCache] that's cached value is a [SharedFlow] shared in the supplied [coroutineScope]
*/
inline fun <K, V> SharedFlowCache(
coroutineScope: CoroutineScope,
crossinline compute: suspend (key: K) -> Flow<V>
): LazyAsyncCache<K, SharedFlow<V>> {
return LazyAsyncCache(coroutineScope) { key ->
compute(key).shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1)
}
}
typealias AsyncCacheCompute<K, V> = suspend (key: K) -> V
private class RealLazyAsyncCache<K, V>(
private val lifetime: CoroutineScope,
private val compute: AsyncCacheCompute<K, V>,
) : LazyAsyncCache<K, V> {
private val mutex = Mutex()
private val cache = mutableMapOf<K, V>()
override suspend fun getOrCompute(key: K): V {
mutex.withLock {
if (key in cache) return cache.getValue(key)
return compute(key).also {
cache[key] = it
}
}
}
init {
lifetime.invokeOnCompletion {
clearCache()
}
}
// GlobalScope job is fine here since it just for clearing the map
@OptIn(DelicateCoroutinesApi::class)
private fun clearCache() = GlobalScope.launch {
mutex.withLock { cache.clear() }
}
}
@@ -0,0 +1,92 @@
package io.novafoundation.nova.common.data.memory
import android.util.Log
import io.novafoundation.nova.common.utils.invokeOnCompletion
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.Collections
interface LazyAsyncMultiCache<K, V> {
suspend fun getOrCompute(keys: Collection<K>): Map<K, V>
suspend fun put(key: K, value: V)
suspend fun putAll(map: Map<K, V>)
}
/**
* In-memory cache primitive that caches asynchronously computed values
* This is a generalization of [LazyAsyncCache] that can request batch of elements at the same time
* Lifetime of the cache itself is determine by supplied [CoroutineScope]
*/
fun <K, V> LazyAsyncMultiCache(
coroutineScope: CoroutineScope,
debugLabel: String = "LazyAsyncMultiCache",
compute: AsyncMultiCacheCompute<K, V>
): LazyAsyncMultiCache<K, V> {
return RealLazyAsyncMultiCache(coroutineScope, debugLabel, compute)
}
typealias AsyncMultiCacheCompute<K, V> = suspend (keys: List<K>) -> Map<K, V>
private class RealLazyAsyncMultiCache<K, V>(
lifetime: CoroutineScope,
private val debugLabel: String,
private val compute: AsyncMultiCacheCompute<K, V>,
) : LazyAsyncMultiCache<K, V> {
private val mutex = Mutex()
private val cache = mutableMapOf<K, V>()
override suspend fun getOrCompute(keys: Collection<K>): Map<K, V> {
mutex.withLock {
Log.d(debugLabel, "Requested to fetch ${keys.size} keys")
val missingKeys = keys - cache.keys
if (missingKeys.isNotEmpty()) {
Log.d(debugLabel, "Missing ${keys.size} keys")
val newKeys = compute(missingKeys)
require(newKeys.size == missingKeys.size) {
"compute() returned less keys than was requested. Make sure you return values for all requested keys"
}
cache.putAll(newKeys)
} else {
Log.d(debugLabel, "All keys are already in cache")
}
// Return the view of the whole cache to avoid extra allocations of the map
return Collections.unmodifiableMap(cache)
}
}
override suspend fun put(key: K, value: V) {
mutex.withLock {
cache[key] = value
}
}
override suspend fun putAll(map: Map<K, V>) {
mutex.withLock {
cache.putAll(map)
}
}
init {
lifetime.invokeOnCompletion {
clearCache()
}
}
// GlobalScope job is fine here since it just for clearing the map
@OptIn(DelicateCoroutinesApi::class)
private fun clearCache() = GlobalScope.launch {
mutex.withLock { cache.clear() }
}
}
@@ -0,0 +1,117 @@
package io.novafoundation.nova.common.data.memory
import android.util.Log
import io.novafoundation.nova.common.utils.LOG_TAG
import io.novafoundation.nova.common.utils.flowOfAll
import io.novafoundation.nova.common.utils.inBackground
import io.novafoundation.nova.common.utils.invokeOnCompletion
import io.novafoundation.nova.common.utils.singleReplaySharedFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
private typealias Awaitable<T> = suspend () -> T
private typealias AwaitableConstructor<T> = suspend CoroutineScope.() -> Awaitable<T>
internal class RealComputationalCache : ComputationalCache, CoroutineScope by CoroutineScope(Dispatchers.Default) {
private class Entry(
val dependents: MutableSet<CoroutineScope>,
val aggregateScope: CoroutineScope,
val awaitable: Awaitable<Any?>
)
private val memory = mutableMapOf<String, Entry>()
private val mutex = Mutex()
override suspend fun <T> useCache(
key: String,
scope: CoroutineScope,
computation: suspend CoroutineScope.() -> T
): T = withContext(Dispatchers.Default) {
useCacheInternal(key, scope) {
val deferred = async { this@useCacheInternal.computation() }
return@useCacheInternal { deferred.await() }
}
}
override fun <T> useSharedFlow(
key: String,
scope: CoroutineScope,
flowLazy: suspend CoroutineScope.() -> Flow<T>
): Flow<T> {
return flowOfAll {
useCacheInternal(key, scope) {
val inner = singleReplaySharedFlow<T>()
launch {
flowLazy(this@useCacheInternal)
.onEach { inner.emit(it) }
.inBackground()
.launchIn(this@useCacheInternal)
}
return@useCacheInternal { inner }
}
}
}
@Suppress("UNCHECKED_CAST")
private suspend fun <T> useCacheInternal(
key: String,
scope: CoroutineScope,
cachedAction: AwaitableConstructor<T>
): T {
val awaitable = mutex.withLock {
if (key in memory) {
Log.d(LOG_TAG, "Key $key requested - already present")
val entry = memory.getValue(key)
entry.dependents += scope
entry.awaitable
} else {
Log.d(LOG_TAG, "Key $key requested - creating new operation")
val aggregateScope = CoroutineScope(Dispatchers.Default)
val awaitable = cachedAction(aggregateScope)
memory[key] = Entry(dependents = mutableSetOf(scope), aggregateScope, awaitable)
awaitable
}
}
scope.invokeOnCompletion {
this@RealComputationalCache.launch {
mutex.withLock {
memory[key]?.let { entry ->
entry.dependents -= scope
if (entry.dependents.isEmpty()) {
Log.d(this@RealComputationalCache.LOG_TAG, "Key $key - last scope cancelled")
memory.remove(key)
entry.aggregateScope.cancel()
} else {
Log.d(this@RealComputationalCache.LOG_TAG, "Key $key - scope cancelled, ${entry.dependents.size} remaining")
}
}
}
}
}
return awaitable() as T
}
}
@@ -0,0 +1,29 @@
package io.novafoundation.nova.common.data.memory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
abstract class SharedComputation(
private val computationalCache: ComputationalCache
) {
context(ComputationalScope)
protected fun <T> cachedFlow(
vararg keyArgs: String,
flowLazy: suspend CoroutineScope.() -> Flow<T>
): Flow<T> {
val key = keyArgs.joinToString(separator = ".")
return computationalCache.useSharedFlow(key, flowLazy)
}
context(ComputationalScope)
protected suspend fun <T> cachedValue(
vararg keyArgs: String,
valueLazy: suspend CoroutineScope.() -> T
): T {
val key = keyArgs.joinToString(separator = ".")
return computationalCache.useCache(key, valueLazy)
}
}
@@ -0,0 +1,36 @@
package io.novafoundation.nova.common.data.memory
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
interface SingleValueCache<T> {
suspend operator fun invoke(): T
}
typealias SingleValueCacheCompute<T> = suspend () -> T
fun <T> SingleValueCache(compute: SingleValueCacheCompute<T>): SingleValueCache<T> {
return RealSingleValueCache(compute)
}
private class RealSingleValueCache<T>(
private val compute: SingleValueCacheCompute<T>,
) : SingleValueCache<T> {
private val mutex = Mutex()
private var cache: Any? = NULL
@Suppress("UNCHECKED_CAST")
override suspend operator fun invoke(): T {
mutex.withLock {
if (cache === NULL) {
cache = compute()
}
return cache as T
}
}
private object NULL
}
@@ -0,0 +1,5 @@
package io.novafoundation.nova.common.data.model
enum class AssetIconMode {
COLORED, WHITE
}
@@ -0,0 +1,12 @@
package io.novafoundation.nova.common.data.model
enum class AssetViewMode {
TOKENS, NETWORKS
}
fun AssetViewMode.switch(): AssetViewMode {
return when (this) {
AssetViewMode.TOKENS -> AssetViewMode.NETWORKS
AssetViewMode.NETWORKS -> AssetViewMode.TOKENS
}
}
@@ -0,0 +1,45 @@
package io.novafoundation.nova.common.data.model
import io.novafoundation.nova.common.utils.castOrNull
data class DataPage<T>(
val nextOffset: PageOffset,
val items: List<T>
) : List<T> by items {
companion object {
fun <T> empty(): DataPage<T> = DataPage(nextOffset = PageOffset.FullData, items = emptyList())
}
}
sealed class PageOffset {
companion object;
sealed class Loadable : PageOffset() {
data class Cursor(val value: String) : Loadable()
data class PageNumber(val page: Int) : Loadable()
object FirstPage : Loadable()
}
object FullData : PageOffset()
}
fun PageOffset.Companion.CursorOrFull(value: String?): PageOffset = if (value != null) {
PageOffset.Loadable.Cursor(value)
} else {
PageOffset.FullData
}
fun PageOffset.asCursorOrNull(): PageOffset.Loadable.Cursor? {
return castOrNull()
}
fun PageOffset.requirePageNumber(): PageOffset.Loadable.PageNumber {
require(this is PageOffset.Loadable.PageNumber)
return this
}
@@ -0,0 +1,5 @@
package io.novafoundation.nova.common.data.model
enum class MaskingMode {
ENABLED, DISABLED
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.common.data.network
import android.util.Log
import io.novasama.substrate_sdk_android.wsrpc.logging.Logger
const val TAG = "AndroidLogger"
class AndroidLogger(
private val debug: Boolean
) : Logger {
override fun log(message: String?) {
if (debug) {
Log.d(TAG, message.toString())
}
}
override fun log(throwable: Throwable?) {
if (debug) {
throwable?.printStackTrace()
}
}
}
@@ -0,0 +1,40 @@
package io.novafoundation.nova.common.data.network
class AppLinksProvider(
val termsUrl: String,
val privacyUrl: String,
val telegram: String,
val twitter: String,
val rateApp: String,
val website: String,
val github: String,
val email: String,
val youtube: String,
val payoutsLearnMore: String,
val recommendedValidatorsLearnMore: String,
val twitterAccountTemplate: String,
val setControllerLearnMore: String,
val setControllerDeprecatedLeanMore: String,
val paritySignerTroubleShooting: String,
val polkadotVaultTroubleShooting: String,
val ledgerConnectionGuide: String,
val wikiBase: String,
val wikiProxy: String,
val integrateNetwork: String,
val storeUrl: String,
val ledgerMigrationArticle: String,
val pezkuwiCardWidgetUrl: String,
val unifiedAddressArticle: String,
val multisigsWikiUrl: String,
val giftsWikiUrl: String,
) {
fun getTwitterAccountUrl(
accountName: String
): String = twitterAccountTemplate.format(accountName)
}
@@ -0,0 +1,34 @@
package io.novafoundation.nova.common.data.network
import io.novafoundation.nova.common.R
import io.novafoundation.nova.common.base.BaseException
import io.novafoundation.nova.common.resources.ResourceManager
import retrofit2.HttpException
import java.io.IOException
class HttpExceptionHandler(
private val resourceManager: ResourceManager
) {
suspend fun <T> wrap(block: suspend () -> T): T {
return try {
block()
} catch (e: Throwable) {
throw transformException(e)
}
}
fun transformException(exception: Throwable): BaseException {
return when (exception) {
is HttpException -> {
val response = exception.response()!!
val errorCode = response.code()
response.errorBody()?.close()
BaseException.httpError(errorCode, resourceManager.getString(R.string.common_undefined_error_message))
}
is IOException -> BaseException.networkError(resourceManager.getString(R.string.connection_error_message_v2_2_0), exception)
else -> BaseException.unexpectedError(exception)
}
}
}
@@ -0,0 +1,26 @@
package io.novafoundation.nova.common.data.network
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory
class NetworkApiCreator(
private val okHttpClient: OkHttpClient,
private val baseUrl: String
) {
fun <T> create(
service: Class<T>,
customBaseUrl: String = baseUrl
): T {
val retrofit = Retrofit.Builder()
.client(okHttpClient)
.baseUrl(customBaseUrl)
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build()
return retrofit.create(service)
}
}
@@ -0,0 +1,51 @@
package io.novafoundation.nova.common.data.network
import okhttp3.Interceptor
import okhttp3.Response
import java.util.concurrent.TimeUnit
class TimeHeaderInterceptor : Interceptor {
companion object {
private const val CONNECT_TIMEOUT = "CONNECT_TIMEOUT"
private const val READ_TIMEOUT = "READ_TIMEOUT"
private const val WRITE_TIMEOUT = "WRITE_TIMEOUT"
private const val LONG_REQUEST_DURATION = 60_000 // 60 sec
const val LONG_CONNECT = "$CONNECT_TIMEOUT: $LONG_REQUEST_DURATION"
const val LONG_READ = "$READ_TIMEOUT: $LONG_REQUEST_DURATION"
const val LONG_WRITE = "$WRITE_TIMEOUT: $LONG_REQUEST_DURATION"
}
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
var connectTimeout = chain.connectTimeoutMillis()
var readTimeout = chain.readTimeoutMillis()
var writeTimeout = chain.writeTimeoutMillis()
val builder = request.newBuilder()
request.header(CONNECT_TIMEOUT)?.also {
connectTimeout = it.toInt()
builder.removeHeader(CONNECT_TIMEOUT)
}
request.header(READ_TIMEOUT)?.also {
readTimeout = it.toInt()
builder.removeHeader(READ_TIMEOUT)
}
request.header(WRITE_TIMEOUT)?.also {
writeTimeout = it.toInt()
builder.removeHeader(WRITE_TIMEOUT)
}
return chain
.withConnectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
.withReadTimeout(readTimeout, TimeUnit.MILLISECONDS)
.withWriteTimeout(writeTimeout, TimeUnit.MILLISECONDS)
.proceed(builder.build())
}
}
@@ -0,0 +1,9 @@
package io.novafoundation.nova.common.data.network
object UserAgent {
const val PEZKUWI = "User-Agent: Pezkuwi Wallet (Android)"
@Deprecated("Use PEZKUWI instead", replaceWith = ReplaceWith("PEZKUWI"))
const val NOVA = PEZKUWI
}
@@ -0,0 +1,39 @@
package io.novafoundation.nova.common.data.network.coingecko
import android.net.Uri
import io.novafoundation.nova.common.utils.Urls
private const val COINGECKO_HOST = "www.coingecko.com"
private const val COINGECKO_PATH_LANGUAGE = "en"
private const val COINGECKO_PATH_SEGMENT = "coins"
class CoinGeckoLinkParser {
class Content(val priceId: String)
fun parse(input: String): Result<Content> = runCatching {
val parsedUri = parseToUri(input)
require(parsedUri.host == COINGECKO_HOST)
val (language, coinSegment, priceId) = parsedUri.pathSegments
require(coinSegment == COINGECKO_PATH_SEGMENT)
Content(priceId)
}
fun format(priceId: String): String {
return Uri.Builder()
.scheme("https")
.authority(COINGECKO_HOST)
.appendPath(COINGECKO_PATH_LANGUAGE)
.appendPath(COINGECKO_PATH_SEGMENT)
.appendPath(priceId)
.build()
.toString()
}
private fun parseToUri(input: String): Uri {
val withProtocol = Urls.ensureHttpsProtocol(input)
return Uri.parse(withProtocol)
}
}
@@ -0,0 +1,8 @@
package io.novafoundation.nova.common.data.network.coingecko
import java.math.BigDecimal
class PriceInfo(
val price: BigDecimal?,
val rateChange: BigDecimal?
)
@@ -0,0 +1,17 @@
package io.novafoundation.nova.common.data.network.ext
import io.novafoundation.nova.common.data.network.runtime.binding.AccountInfo
import io.novafoundation.nova.common.domain.balance.TransferableMode
import io.novafoundation.nova.common.domain.balance.calculateTransferable
import java.math.BigInteger
fun AccountInfo.transferableBalance(): BigInteger {
return transferableMode.calculateTransferable(data)
}
val AccountInfo.transferableMode: TransferableMode
get() = if (data.flags.holdsAndFreezesEnabled()) {
TransferableMode.HOLDS_AND_FREEZES
} else {
TransferableMode.REGULAR
}
@@ -0,0 +1,6 @@
package io.novafoundation.nova.common.data.network.http
object CacheControl {
const val NO_CACHE = "Cache-control: no-cache"
}
@@ -0,0 +1,146 @@
package io.novafoundation.nova.common.data.network.rpc
import io.novafoundation.nova.common.data.network.runtime.binding.BlockHash
import io.novasama.substrate_sdk_android.wsrpc.SocketService
import io.novasama.substrate_sdk_android.wsrpc.executeAsync
import io.novasama.substrate_sdk_android.wsrpc.mappers.nonNull
import io.novasama.substrate_sdk_android.wsrpc.mappers.pojoList
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.RuntimeRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext
import kotlin.coroutines.coroutineContext
class GetKeysPagedRequest(
keyPrefix: String,
pageSize: Int,
fullKeyOffset: String?,
) : RuntimeRequest(
method = "state_getKeysPaged",
params = listOfNotNull(
keyPrefix,
pageSize,
fullKeyOffset,
)
)
class GetKeys(
keyPrefix: String,
at: BlockHash?
) : RuntimeRequest(
method = "state_getKeys",
params = listOfNotNull(
keyPrefix,
at
)
)
class QueryStorageAtRequest(
keys: List<String>,
at: String?
) : RuntimeRequest(
method = "state_queryStorageAt",
params = listOfNotNull(
keys,
at
)
)
class QueryStorageAtResponse(
val block: String,
val changes: List<List<String?>>
) {
fun changesAsMap(): Map<String, String?> {
return changes.map { it[0]!! to it[1] }.toMap()
}
}
class BulkRetriever(private val pageSize: Int) {
/**
* Retrieves all keys starting with [keyPrefix] from [at] block
* Returns only first [defaultPageSize] elements in case historical querying is used ([at] is not null)
*/
suspend fun retrieveAllKeys(
socketService: SocketService,
keyPrefix: String,
at: BlockHash? = null
): List<String> = withContext(Dispatchers.IO) {
if (at != null) {
queryKeysByPrefixHistorical(socketService, keyPrefix, at)
} else {
queryKeysByPrefixCurrent(socketService, keyPrefix)
}
}
suspend fun queryKeys(
socketService: SocketService,
keys: List<String>,
at: BlockHash? = null
): Map<String, String?> = withContext(Dispatchers.IO) {
val chunks = keys.chunked(pageSize)
chunks.fold(mutableMapOf()) { acc, chunk ->
ensureActive()
val request = QueryStorageAtRequest(chunk, at)
val chunkValues = socketService.executeAsync(request, mapper = pojoList<QueryStorageAtResponse>().nonNull())
.first().changesAsMap()
acc.putAll(chunkValues)
acc
}
}
/**
* Note: the amount of keys returned by this method is limited by [defaultPageSize]
* So it is should not be used for storages with big amount of entries
*/
private suspend fun queryKeysByPrefixHistorical(
socketService: SocketService,
prefix: String,
at: BlockHash
): List<String> {
// We use `state_getKeys` for historical prefix queries instead of `state_getKeysPaged`
// since most of the chains always return empty list when the same is requested via `state_getKeysPaged`
// Thus, we can only request up to 1000 first historical keys
val request = GetKeys(prefix, at)
return socketService.executeAsync(request, mapper = pojoList<String>().nonNull())
}
private suspend fun queryKeysByPrefixCurrent(
socketService: SocketService,
prefix: String
): List<String> {
val result = mutableListOf<String>()
var currentOffset: String? = null
while (true) {
coroutineContext.ensureActive()
val request = GetKeysPagedRequest(prefix, pageSize, currentOffset)
val page = socketService.executeAsync(request, mapper = pojoList<String>().nonNull())
result += page
if (isLastPage(page)) break
currentOffset = page.last()
}
return result
}
private fun isLastPage(page: List<String>) = page.size < pageSize
}
suspend fun BulkRetriever.queryKey(
socketService: SocketService,
key: String,
at: BlockHash? = null
): String? = queryKeys(socketService, listOf(key), at).values.first()
@@ -0,0 +1,10 @@
package io.novafoundation.nova.common.data.network.rpc
import io.novafoundation.nova.common.data.network.runtime.binding.BlockHash
import io.novasama.substrate_sdk_android.wsrpc.SocketService
suspend fun BulkRetriever.retrieveAllValues(socketService: SocketService, keyPrefix: String, at: BlockHash? = null): Map<String, String?> {
val allKeys = retrieveAllKeys(socketService, keyPrefix, at)
return queryKeys(socketService, allKeys, at)
}
@@ -0,0 +1,18 @@
package io.novafoundation.nova.common.data.network.rpc
import io.novasama.substrate_sdk_android.extensions.toHexString
import java.io.ByteArrayOutputStream
private const val CHILD_KEY_DEFAULT = ":child_storage:default:"
suspend fun childStateKey(
builder: suspend ByteArrayOutputStream.() -> Unit
): String {
val buffer = ByteArrayOutputStream().apply {
write(CHILD_KEY_DEFAULT.encodeToByteArray())
builder()
}
return buffer.toByteArray().toHexString(withPrefix = true)
}
@@ -0,0 +1,83 @@
package io.novafoundation.nova.common.data.network.rpc
import com.google.gson.Gson
import com.neovisionaries.ws.client.WebSocket
import com.neovisionaries.ws.client.WebSocketAdapter
import com.neovisionaries.ws.client.WebSocketException
import com.neovisionaries.ws.client.WebSocketFactory
import io.novafoundation.nova.common.base.errors.NovaException
import io.novafoundation.nova.common.resources.ResourceManager
import io.novasama.substrate_sdk_android.wsrpc.logging.Logger
import io.novasama.substrate_sdk_android.wsrpc.mappers.ResponseMapper
import io.novasama.substrate_sdk_android.wsrpc.request.base.RpcRequest
import io.novasama.substrate_sdk_android.wsrpc.response.RpcResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@Suppress("EXPERIMENTAL_API_USAGE")
class SocketSingleRequestExecutor(
private val jsonMapper: Gson,
private val logger: Logger,
private val wsFactory: WebSocketFactory,
private val resourceManager: ResourceManager
) {
suspend fun <R> executeRequest(
request: RpcRequest,
url: String,
mapper: ResponseMapper<R>
): R {
val response = executeRequest(request, url)
return withContext(Dispatchers.Default) {
mapper.map(response, jsonMapper)
}
}
suspend fun executeRequest(
request: RpcRequest,
url: String
): RpcResponse = withContext(Dispatchers.IO) {
try {
executeRequestInternal(request, url)
} catch (e: Exception) {
throw NovaException.networkError(resourceManager, e)
}
}
private suspend fun executeRequestInternal(
request: RpcRequest,
url: String
): RpcResponse = suspendCancellableCoroutine { cont ->
val webSocket: WebSocket = wsFactory.createSocket(url)
cont.invokeOnCancellation {
webSocket.clearListeners()
webSocket.disconnect()
}
webSocket.addListener(object : WebSocketAdapter() {
override fun onTextMessage(websocket: WebSocket, text: String) {
logger.log("[RECEIVED] $text")
val response = jsonMapper.fromJson(text, RpcResponse::class.java)
cont.resume(response)
webSocket.disconnect()
}
override fun onError(websocket: WebSocket, cause: WebSocketException) {
cont.resumeWithException(cause)
}
})
webSocket.connect()
webSocket.sendText(jsonMapper.toJson(request))
}
}
@@ -0,0 +1,12 @@
package io.novafoundation.nova.common.data.network.runtime.binding
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.address.intoKey
import io.novasama.substrate_sdk_android.runtime.AccountId
@HelperBinding
fun bindAccountId(dynamicInstance: Any?): AccountId = dynamicInstance.cast()
fun bindAccountIdKey(dynamicInstance: Any?): AccountIdKey = bindAccountId(dynamicInstance).intoKey()
fun bindNullableAccountId(dynamicInstance: Any?): AccountId? = dynamicInstance.nullableCast()
@@ -0,0 +1,164 @@
package io.novafoundation.nova.common.data.network.runtime.binding
import io.novafoundation.nova.common.domain.balance.EDCountingMode
import io.novafoundation.nova.common.domain.balance.TransferableMode
import io.novafoundation.nova.common.utils.orZero
import io.novafoundation.nova.common.utils.system
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.Struct
import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHexOrNull
import io.novasama.substrate_sdk_android.runtime.metadata.storage
import java.math.BigInteger
open class AccountBalance(
val free: BigInteger,
val reserved: BigInteger,
val frozen: BigInteger
) {
companion object {
fun empty(): AccountBalance {
return AccountBalance(
free = BigInteger.ZERO,
reserved = BigInteger.ZERO,
frozen = BigInteger.ZERO,
)
}
}
}
fun AccountBalance?.orEmpty(): AccountBalance = this ?: AccountBalance.empty()
class AccountData(
free: BigInteger,
reserved: BigInteger,
frozen: BigInteger,
val flags: AccountDataFlags,
) : AccountBalance(free, reserved, frozen)
@JvmInline
value class AccountDataFlags(val value: BigInteger) {
companion object {
fun default() = AccountDataFlags(BigInteger.ZERO)
private val HOLD_AND_FREEZES_ENABLED_MASK: BigInteger = BigInteger("80000000000000000000000000000000", 16)
}
fun holdsAndFreezesEnabled(): Boolean {
return flagEnabled(HOLD_AND_FREEZES_ENABLED_MASK)
}
@Suppress("SameParameterValue")
private fun flagEnabled(flag: BigInteger) = value and flag == flag
}
fun AccountDataFlags.transferableMode(): TransferableMode {
return if (holdsAndFreezesEnabled()) {
TransferableMode.HOLDS_AND_FREEZES
} else {
TransferableMode.REGULAR
}
}
fun AccountDataFlags.edCountingMode(): EDCountingMode {
return if (holdsAndFreezesEnabled()) {
EDCountingMode.FREE
} else {
EDCountingMode.TOTAL
}
}
class AccountInfo(
val consumers: BigInteger,
val providers: BigInteger,
val sufficients: BigInteger,
val data: AccountData
) {
companion object {
fun empty() = AccountInfo(
consumers = BigInteger.ZERO,
providers = BigInteger.ZERO,
sufficients = BigInteger.ZERO,
data = AccountData(
free = BigInteger.ZERO,
reserved = BigInteger.ZERO,
frozen = BigInteger.ZERO,
flags = AccountDataFlags.default(),
)
)
}
}
@HelperBinding
fun bindAccountData(dynamicInstance: Struct.Instance): AccountData {
val frozen = if (hasSplitFrozen(dynamicInstance)) {
val miscFrozen = bindNumber(dynamicInstance["miscFrozen"])
val feeFrozen = bindNumber(dynamicInstance["feeFrozen"])
miscFrozen.max(feeFrozen)
} else {
bindNumber(dynamicInstance["frozen"])
}
return AccountData(
free = bindNumber(dynamicInstance["free"]),
reserved = bindNumber(dynamicInstance["reserved"]),
frozen = frozen,
flags = bindAccountDataFlags(dynamicInstance["flags"])
)
}
private fun hasSplitFrozen(accountInfo: Struct.Instance): Boolean {
return "miscFrozen" in accountInfo.mapping
}
private fun bindAccountDataFlags(instance: Any?): AccountDataFlags {
return if (instance != null) {
AccountDataFlags(bindNumber(instance))
} else {
AccountDataFlags.default()
}
}
@HelperBinding
fun bindNonce(dynamicInstance: Any?): BigInteger {
return bindNumber(dynamicInstance)
}
@UseCaseBinding
fun bindAccountInfo(scale: String, runtime: RuntimeSnapshot): AccountInfo {
val type = runtime.metadata.system().storage("Account").returnType()
val dynamicInstance = type.fromHexOrNull(runtime, scale)
return bindAccountInfo(dynamicInstance)
}
fun bindAccountInfo(decoded: Any?): AccountInfo {
val dynamicInstance = decoded.cast<Struct.Instance>()
return AccountInfo(
consumers = dynamicInstance.getTyped<BigInteger?>("consumers").orZero(),
providers = dynamicInstance.getTyped<BigInteger?>("providers").orZero(),
sufficients = dynamicInstance.getTyped<BigInteger?>("sufficients").orZero(),
data = bindAccountData(dynamicInstance.getTyped("data"))
)
}
fun bindOrmlAccountBalanceOrEmpty(decoded: Any?): AccountBalance {
return decoded?.let { bindOrmlAccountData(decoded) } ?: AccountBalance.empty()
}
fun bindOrmlAccountData(decoded: Any?): AccountBalance {
val dynamicInstance = decoded.cast<Struct.Instance>()
return AccountBalance(
free = bindNumber(dynamicInstance["free"]),
reserved = bindNumber(dynamicInstance["reserved"]),
frozen = bindNumber(dynamicInstance["frozen"]),
)
}
@@ -0,0 +1,32 @@
package io.novafoundation.nova.common.data.network.runtime.binding
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.Struct
import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHex
import java.math.BigInteger
/*
"ActiveEraInfo": {
"type": "struct",
"type_mapping": [
[
"index",
"EraIndex"
],
[
"start",
"Option<Moment>"
]
]
}
*/
@UseCaseBinding
fun bindActiveEraIndex(
scale: String,
runtime: RuntimeSnapshot
): BigInteger {
val returnType = runtime.metadata.storageReturnType("Staking", "ActiveEra")
val decoded = returnType.fromHex(runtime, scale) as? Struct.Instance ?: incompatible()
return decoded.get<BigInteger>("index") ?: incompatible()
}
@@ -0,0 +1,5 @@
package io.novafoundation.nova.common.data.network.runtime.binding
import java.math.BigInteger
typealias BalanceOf = BigInteger
@@ -0,0 +1,100 @@
package io.novafoundation.nova.common.data.network.runtime.binding
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.types.RuntimeType
import io.novasama.substrate_sdk_android.runtime.definitions.types.Type
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.Struct
import io.novasama.substrate_sdk_android.runtime.definitions.types.fromByteArray
import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHex
import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata
import io.novasama.substrate_sdk_android.runtime.metadata.module
import io.novasama.substrate_sdk_android.runtime.metadata.module.StorageEntry
import io.novasama.substrate_sdk_android.runtime.metadata.storage
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
annotation class UseCaseBinding
annotation class HelperBinding
fun incompatible(): Nothing = throw IllegalStateException("Binding is incompatible")
typealias Binder<T> = (scale: String?, RuntimeSnapshot) -> T
typealias NonNullBinder<T> = (scale: String, RuntimeSnapshot) -> T
typealias NonNullBinderWithType<T> = (scale: String, RuntimeSnapshot, Type<*>) -> T
typealias BinderWithType<T> = (scale: String?, RuntimeSnapshot, Type<*>) -> T
@OptIn(ExperimentalContracts::class)
inline fun <reified T> requireType(dynamicInstance: Any?): T {
contract {
returns() implies (dynamicInstance is T)
}
return dynamicInstance as? T ?: incompatible()
}
inline fun <reified T> Any?.cast(): T {
return this as? T ?: incompatible()
}
inline fun <reified T> Any?.nullableCast(): T? {
if (this == null) return null
return this as? T ?: incompatible()
}
inline fun <reified T> Any?.castOrNull(): T? {
return this as? T
}
@OptIn(ExperimentalContracts::class)
fun Any?.castToStruct(): Struct.Instance {
contract {
returns() implies (this@castToStruct is Struct.Instance)
}
return cast()
}
@OptIn(ExperimentalContracts::class)
fun Any?.castToDictEnum(): DictEnum.Entry<*> {
contract {
returns() implies (this@castToDictEnum is DictEnum.Entry<*>)
}
return cast()
}
fun Any?.castToStructOrNull(): Struct.Instance? {
return castOrNull()
}
fun Any?.castToList(): List<*> {
return cast()
}
inline fun <reified R> Struct.Instance.getTyped(key: String) = get<R>(key) ?: incompatible()
fun Struct.Instance.getList(key: String) = get<List<*>>(key) ?: incompatible()
fun Struct.Instance.getStruct(key: String) = get<Struct.Instance>(key) ?: incompatible()
inline fun <T> bindOrNull(binder: () -> T): T? = runCatching(binder).getOrNull()
fun StorageEntry.returnType() = type.value ?: incompatible()
fun RuntimeMetadata.storageReturnType(moduleName: String, storageName: String): Type<*> {
return module(moduleName).storage(storageName).returnType()
}
fun <D> RuntimeType<*, D>.fromHexOrIncompatible(scale: String, runtime: RuntimeSnapshot): D = successOrIncompatible {
fromHex(runtime, scale)
}
fun <D> RuntimeType<*, D>.fromByteArrayOrIncompatible(scale: ByteArray, runtime: RuntimeSnapshot): D = successOrIncompatible {
fromByteArray(runtime, scale)
}
private fun <T> successOrIncompatible(block: () -> T): T = runCatching {
block()
}.getOrElse { incompatible() }
@@ -0,0 +1,20 @@
package io.novafoundation.nova.common.data.network.runtime.binding
import io.novafoundation.nova.common.utils.system
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.metadata.storage
import java.math.BigInteger
typealias BlockNumber = BigInteger
typealias BlockHash = String
fun bindBlockNumber(scale: String, runtime: RuntimeSnapshot): BlockNumber {
val type = runtime.metadata.system().storage("Number").returnType()
val dynamicInstance = type.fromHexOrIncompatible(scale, runtime)
return bindNumber(dynamicInstance)
}
fun bindBlockNumber(dynamic: Any?) = bindNumber(dynamic)
@@ -0,0 +1,57 @@
package io.novafoundation.nova.common.data.network.runtime.binding
import io.novafoundation.nova.common.utils.mapToSet
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
fun <T> bindList(dynamicInstance: Any?, itemBinder: (Any?) -> T): List<T> {
if (dynamicInstance == null) return emptyList()
return dynamicInstance.cast<List<*>>().map {
itemBinder(it)
}
}
fun <T> bindSet(dynamicInstance: Any?, itemBinder: (Any?) -> T): Set<T> {
if (dynamicInstance == null) return emptySet()
return dynamicInstance.cast<List<*>>().mapToSet { itemBinder(it) }
}
inline fun <T1, T2> bindPair(
dynamicInstance: Any,
firstComponent: (Any?) -> T1,
secondComponent: (Any?) -> T2
): Pair<T1, T2> {
val (first, second) = dynamicInstance.cast<List<*>>()
return firstComponent(first) to secondComponent(second)
}
// Maps are encoded as List<Pair<K, V>>
fun <K, V> bindMap(dynamicInstance: Any?, keyBinder: (Any?) -> K, valueBinder: (Any?) -> V): Map<K, V> {
if (dynamicInstance == null) return emptyMap()
return dynamicInstance.cast<List<*>>().associateBy(
keySelector = {
val (keyRaw, _) = it.cast<List<*>>()
keyBinder(keyRaw)
},
valueTransform = {
val (_, valueRaw) = it.cast<List<*>>()
valueBinder(valueRaw)
}
)
}
inline fun <reified T : Enum<T>> bindCollectionEnum(
dynamicInstance: Any?,
enumValueFromName: (String) -> T = ::enumValueOf
): T {
return when (dynamicInstance) {
is String -> enumValueFromName(dynamicInstance) // collection enum
is DictEnum.Entry<*> -> enumValueFromName(dynamicInstance.name) // dict enum with empty values
else -> incompatible()
}
}
@@ -0,0 +1,22 @@
package io.novafoundation.nova.common.data.network.runtime.binding
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.types.fromByteArrayOrNull
import io.novasama.substrate_sdk_android.runtime.metadata.module.Constant
import java.math.BigInteger
@HelperBinding
fun bindNumberConstant(
constant: Constant,
runtime: RuntimeSnapshot
): BigInteger = bindNullableNumberConstant(constant, runtime) ?: incompatible()
@HelperBinding
fun bindNullableNumberConstant(
constant: Constant,
runtime: RuntimeSnapshot
): BigInteger? {
val decoded = constant.type?.fromByteArrayOrNull(runtime, constant.value) ?: incompatible()
return decoded as BigInteger?
}
@@ -0,0 +1,42 @@
package io.novafoundation.nova.common.data.network.runtime.binding
import io.novasama.substrate_sdk_android.extensions.toHexString
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Data as DataType
sealed class Data {
abstract fun asString(): String?
object None : Data() {
override fun asString(): String? = null
}
class Raw(val value: ByteArray) : Data() {
override fun asString() = String(value)
}
class Hash(val value: ByteArray, val type: Type) : Data() {
enum class Type {
BLAKE_2B_256, SHA_256, KECCAK_256, SHA_3_256
}
override fun asString() = value.toHexString(withPrefix = true)
}
}
@HelperBinding
fun bindData(dynamicInstance: Any?): Data {
requireType<DictEnum.Entry<Any?>>(dynamicInstance)
return when (dynamicInstance.name) {
DataType.NONE -> Data.None
DataType.RAW -> Data.Raw(dynamicInstance.value.cast())
DataType.BLAKE_2B_256 -> Data.Hash(dynamicInstance.value.cast(), Data.Hash.Type.BLAKE_2B_256)
DataType.SHA_256 -> Data.Hash(dynamicInstance.value.cast(), Data.Hash.Type.SHA_256)
DataType.KECCAK_256 -> Data.Hash(dynamicInstance.value.cast(), Data.Hash.Type.KECCAK_256)
DataType.SHA_3_256 -> Data.Hash(dynamicInstance.value.cast(), Data.Hash.Type.SHA_3_256)
else -> incompatible()
}
}
@@ -0,0 +1,64 @@
package io.novafoundation.nova.common.data.network.runtime.binding
import io.novafoundation.nova.common.utils.RuntimeContext
import io.novafoundation.nova.common.utils.metadata
import io.novasama.substrate_sdk_android.runtime.metadata.error
import io.novasama.substrate_sdk_android.runtime.metadata.module
import io.novasama.substrate_sdk_android.runtime.metadata.module.ErrorMetadata
import io.novasama.substrate_sdk_android.runtime.metadata.module.Module as RuntimeModule
sealed class DispatchError : Throwable() {
data class Module(val module: RuntimeModule, val error: ErrorMetadata) : DispatchError() {
override val message: String
get() = toString()
override fun toString(): String {
return "${module.name}.${error.name}"
}
}
object Token : DispatchError() {
override val message: String
get() = toString()
override fun toString(): String {
return "Not enough tokens"
}
}
object Unknown : DispatchError()
}
context(RuntimeContext)
fun bindDispatchError(decoded: Any?): DispatchError {
val asDictEnum = decoded.castToDictEnum()
return when (asDictEnum.name) {
"Module" -> {
val moduleErrorStruct = asDictEnum.value.castToStruct()
val moduleIndex = bindInt(moduleErrorStruct["index"])
val errorIndex = bindModuleError(moduleErrorStruct["error"])
val module = metadata.module(moduleIndex)
val error = module.error(errorIndex)
DispatchError.Module(module, error)
}
"Token" -> DispatchError.Token
else -> DispatchError.Unknown
}
}
private fun bindModuleError(errorEncoded: ByteArray?): Int {
requireNotNull(errorEncoded) {
"Error should exist"
}
return errorEncoded[0].toInt()
}
@@ -0,0 +1,25 @@
package io.novafoundation.nova.common.data.network.runtime.binding
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
import java.math.BigInteger
sealed class DispatchTime {
class At(val block: BlockNumber) : DispatchTime()
class After(val block: BlockNumber) : DispatchTime()
}
fun bindDispatchTime(decoded: DictEnum.Entry<*>): DispatchTime {
return when (decoded.name) {
"At" -> DispatchTime.At(block = bindBlockNumber(decoded.value))
"After" -> DispatchTime.After(block = bindBlockNumber(decoded.value))
else -> incompatible()
}
}
val DispatchTime.minimumRequiredBlock
get() = when (this) {
is DispatchTime.After -> block + BigInteger.ONE
is DispatchTime.At -> block
}
@@ -0,0 +1,42 @@
package io.novafoundation.nova.common.data.network.runtime.binding
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.Struct
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent
import java.math.BigInteger
class EventRecord(val phase: Phase, val event: GenericEvent.Instance)
sealed class Phase {
class ApplyExtrinsic(val extrinsicId: BigInteger) : Phase()
object Finalization : Phase()
object Initialization : Phase()
}
fun bindEventRecords(decoded: Any?): List<EventRecord> {
return bindList(decoded, ::bindEventRecord)
}
fun bindEvent(decoded: Any?): GenericEvent.Instance {
return decoded.cast()
}
private fun bindEventRecord(dynamicInstance: Any?): EventRecord {
requireType<Struct.Instance>(dynamicInstance)
val phaseDynamic = dynamicInstance.getTyped<DictEnum.Entry<*>>("phase")
val phase = when (phaseDynamic.name) {
"ApplyExtrinsic" -> Phase.ApplyExtrinsic(bindNumber(phaseDynamic.value))
"Finalization" -> Phase.Finalization
"Initialization" -> Phase.Initialization
else -> incompatible()
}
val dynamicEvent = dynamicInstance.getTyped<GenericEvent.Instance>("event")
return EventRecord(phase, dynamicEvent)
}
@@ -0,0 +1,47 @@
package io.novafoundation.nova.common.data.network.runtime.binding
import io.novafoundation.nova.common.utils.Fraction
import io.novafoundation.nova.common.utils.Fraction.Companion.percents
import java.math.BigDecimal
import java.math.BigInteger
import io.novafoundation.nova.common.utils.Perbill as PerbillTyped
typealias Perbill = BigDecimal
typealias FixedI64 = BigDecimal
const val PERBILL_MANTISSA_SIZE = 9
const val PERMILL_MANTISSA_SIZE = 6
const val PERQUINTILL_MANTISSA_SIZE = 18
@HelperBinding
fun bindPerbillNumber(value: BigInteger, mantissa: Int = PERBILL_MANTISSA_SIZE): Perbill {
return value.toBigDecimal(scale = mantissa)
}
fun bindPerbill(dynamic: Any?, mantissa: Int = PERBILL_MANTISSA_SIZE): Perbill {
return bindPerbillNumber(dynamic.cast(), mantissa)
}
fun bindPercentFraction(dynamic: Any?): Fraction {
return bindNumber(dynamic).percents
}
fun bindFixedI64Number(value: BigInteger): FixedI64 {
return bindPerbillNumber(value)
}
fun bindFixedI64(dynamic: Any?): FixedI64 {
return bindPerbill(dynamic)
}
fun bindPerbillTyped(dynamic: Any?, mantissa: Int = PERBILL_MANTISSA_SIZE): PerbillTyped {
return PerbillTyped(bindPerbill(dynamic, mantissa).toDouble())
}
fun bindPermill(dynamic: Any?): PerbillTyped {
return bindPerbillTyped(dynamic, mantissa = PERMILL_MANTISSA_SIZE)
}
fun BigInteger.asPerQuintill(): PerbillTyped {
return PerbillTyped(toBigDecimal(scale = PERQUINTILL_MANTISSA_SIZE).toDouble())
}
@@ -0,0 +1,11 @@
package io.novafoundation.nova.common.data.network.runtime.binding
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
fun bindGenericCall(decoded: Any?): GenericCall.Instance {
return decoded.cast()
}
fun bindGenericCallList(decoded: Any?): List<GenericCall.Instance> {
return bindList(decoded, ::bindGenericCall)
}
@@ -0,0 +1,65 @@
package io.novafoundation.nova.common.data.network.runtime.binding
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
import java.math.BigInteger
sealed class MultiAddress {
companion object {
const val TYPE_ID = "Id"
const val TYPE_INDEX = "Index"
const val TYPE_RAW = "Raw"
const val TYPE_ADDRESS32 = "Address32"
const val TYPE_ADDRESS20 = "Address20"
}
class Id(val value: ByteArray) : MultiAddress()
class Index(val value: BigInteger) : MultiAddress()
class Raw(val value: ByteArray) : MultiAddress()
class Address32(val value: ByteArray) : MultiAddress() {
init {
require(value.size == 32) {
"Address32 should be 32 bytes long"
}
}
}
class Address20(val value: ByteArray) : MultiAddress() {
init {
require(value.size == 20) {
"Address20 should be 20 bytes long"
}
}
}
}
fun bindMultiAddress(multiAddress: MultiAddress): DictEnum.Entry<*> {
return when (multiAddress) {
is MultiAddress.Id -> DictEnum.Entry(MultiAddress.TYPE_ID, multiAddress.value)
is MultiAddress.Index -> DictEnum.Entry(MultiAddress.TYPE_INDEX, multiAddress.value)
is MultiAddress.Raw -> DictEnum.Entry(MultiAddress.TYPE_RAW, multiAddress.value)
is MultiAddress.Address32 -> DictEnum.Entry(MultiAddress.TYPE_ADDRESS32, multiAddress.value)
is MultiAddress.Address20 -> DictEnum.Entry(MultiAddress.TYPE_ADDRESS20, multiAddress.value)
}
}
fun bindMultiAddress(dynamicInstance: DictEnum.Entry<*>): MultiAddress {
return when (dynamicInstance.name) {
MultiAddress.TYPE_ID -> MultiAddress.Id(dynamicInstance.value.cast())
MultiAddress.TYPE_INDEX -> MultiAddress.Index(dynamicInstance.value.cast())
MultiAddress.TYPE_RAW -> MultiAddress.Raw(dynamicInstance.value.cast())
MultiAddress.TYPE_ADDRESS32 -> MultiAddress.Address32(dynamicInstance.value.cast())
MultiAddress.TYPE_ADDRESS20 -> MultiAddress.Address20(dynamicInstance.value.cast())
else -> incompatible()
}
}
fun bindAccountIdentifier(dynamicInstance: Any?) = when (dynamicInstance) {
// MultiAddress
is DictEnum.Entry<*> -> (bindMultiAddress(dynamicInstance) as MultiAddress.Id).value
// GenericAccountId or EthereumAddress
is ByteArray -> dynamicInstance
else -> incompatible()
}
@@ -0,0 +1,5 @@
package io.novafoundation.nova.common.data.network.runtime.binding
import java.math.BigInteger
typealias ParaId = BigInteger
@@ -0,0 +1,23 @@
package io.novafoundation.nova.common.data.network.runtime.binding
import io.novafoundation.nova.common.utils.orZero
import java.math.BigInteger
@HelperBinding
fun bindNumber(dynamicInstance: Any?): BigInteger = dynamicInstance.cast()
fun bindNumberOrNull(dynamicInstance: Any?): BigInteger? = dynamicInstance?.cast()
fun bindInt(dynamicInstance: Any?): Int = bindNumber(dynamicInstance).toInt()
@HelperBinding
fun bindNumberOrZero(dynamicInstance: Any?): BigInteger = dynamicInstance?.let(::bindNumber).orZero()
@HelperBinding
fun bindString(dynamicInstance: Any?): String = dynamicInstance.cast<ByteArray>().decodeToString()
@HelperBinding
fun bindBoolean(dynamicInstance: Any?): Boolean = dynamicInstance.cast()
@HelperBinding
fun bindByteArray(dynamicInstance: Any?): ByteArray = dynamicInstance.cast()
@@ -0,0 +1,34 @@
package io.novafoundation.nova.common.data.network.runtime.binding
sealed class ScaleResult<out T, out E> {
class Ok<T>(val value: T) : ScaleResult<T, Nothing>()
class Error<E>(val error: E) : ScaleResult<Nothing, E>()
companion object {
fun <T, E> bind(
dynamicInstance: Any?,
bindOk: (Any?) -> T,
bindError: (Any?) -> E
): ScaleResult<T, E> {
val asEnum = dynamicInstance.castToDictEnum()
return when (asEnum.name) {
"Ok" -> Ok(bindOk(asEnum.value))
"Err" -> Error(bindError(asEnum.value))
else -> error("Unknown Result variant: ${asEnum.name}")
}
}
}
}
class ScaleResultError(val content: Any?) : Throwable()
fun <T, R> ScaleResult<T, R>.toResult(): Result<T> {
return when (this) {
is ScaleResult.Error -> Result.failure(ScaleResultError(error))
is ScaleResult.Ok -> Result.success(value)
}
}
@@ -0,0 +1,86 @@
package io.novafoundation.nova.common.data.network.runtime.binding
import io.novafoundation.nova.common.utils.Min
import io.novafoundation.nova.common.utils.atLeastZero
import io.novafoundation.nova.common.utils.scale.ToDynamicScaleInstance
import io.novafoundation.nova.common.utils.structOf
import io.novafoundation.nova.common.utils.times
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.Struct
import java.math.BigInteger
typealias Weight = BigInteger
data class WeightV2(val refTime: BigInteger, val proofSize: BigInteger) : ToDynamicScaleInstance, Min<WeightV2> {
companion object {
val MAX_DIMENSION = "184467440737090".toBigInteger()
fun max(): WeightV2 {
return WeightV2(MAX_DIMENSION, MAX_DIMENSION)
}
fun fromV1(refTime: BigInteger): WeightV2 {
return WeightV2(refTime, proofSize = BigInteger.ZERO)
}
fun zero(): WeightV2 {
return WeightV2(BigInteger.ZERO, BigInteger.ZERO)
}
}
operator fun times(multiplier: Double): WeightV2 {
return WeightV2(refTime = refTime.times(multiplier), proofSize = proofSize.times(multiplier))
}
operator fun plus(other: WeightV2): WeightV2 {
return WeightV2(refTime + other.refTime, proofSize + other.proofSize)
}
operator fun minus(other: WeightV2): WeightV2 {
return WeightV2(
refTime = (refTime - other.refTime).atLeastZero(),
proofSize = (proofSize - other.proofSize).atLeastZero()
)
}
override fun toEncodableInstance(): Struct.Instance {
return structOf("refTime" to refTime, "proofSize" to proofSize)
}
override fun min(other: WeightV2): WeightV2 {
return WeightV2(
refTime = refTime.min(other.refTime),
proofSize = proofSize.min(other.proofSize)
)
}
}
fun WeightV2.fitsIn(limit: WeightV2): Boolean {
return refTime <= limit.refTime && proofSize <= limit.proofSize
}
fun bindWeight(decoded: Any?): Weight {
return when (decoded) {
// weight v1
is BalanceOf -> decoded
// weight v2
is Struct.Instance -> bindWeightV2(decoded).refTime
else -> incompatible()
}
}
fun bindWeightV2(decoded: Any?): WeightV2 {
return when (decoded) {
is BalanceOf -> WeightV2.fromV1(decoded)
is Struct.Instance -> WeightV2(
refTime = bindNumber(decoded["refTime"]),
proofSize = bindNumber(decoded["proofSize"])
)
else -> incompatible()
}
}
@@ -0,0 +1,8 @@
package io.novafoundation.nova.common.data.network.runtime.calls
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.RuntimeRequest
class FeeCalculationRequest(extrinsicInHex: String) : RuntimeRequest(
method = "payment_queryInfo",
params = listOf(extrinsicInHex)
)
@@ -0,0 +1,11 @@
package io.novafoundation.nova.common.data.network.runtime.calls
import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.RuntimeRequest
class GetBlockHashRequest(blockNumber: BlockNumber?) : RuntimeRequest(
method = "chain_getBlockHash",
params = listOfNotNull(
blockNumber
)
)
@@ -0,0 +1,10 @@
package io.novafoundation.nova.common.data.network.runtime.calls
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.RuntimeRequest
class GetBlockRequest(blockHash: String? = null) : RuntimeRequest(
method = "chain_getBlock",
params = listOfNotNull(
blockHash
)
)
@@ -0,0 +1,8 @@
package io.novafoundation.nova.common.data.network.runtime.calls
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.RuntimeRequest
class GetChainRequest : RuntimeRequest(
method = "system_chain",
params = emptyList()
)
@@ -0,0 +1,11 @@
package io.novafoundation.nova.common.data.network.runtime.calls
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.RuntimeRequest
class GetChildStateRequest(
storageKey: String,
childKey: String
) : RuntimeRequest(
method = "childstate_getStorage",
params = listOf(childKey, storageKey)
)
@@ -0,0 +1,8 @@
package io.novafoundation.nova.common.data.network.runtime.calls
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.RuntimeRequest
object GetFinalizedHeadRequest : RuntimeRequest(
method = "chain_getFinalizedHead",
params = emptyList()
)
@@ -0,0 +1,10 @@
package io.novafoundation.nova.common.data.network.runtime.calls
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.RuntimeRequest
class GetHeaderRequest(blockHash: String? = null) : RuntimeRequest(
method = "chain_getHeader",
params = listOfNotNull(
blockHash
)
)
@@ -0,0 +1,8 @@
package io.novafoundation.nova.common.data.network.runtime.calls
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.RuntimeRequest
class GetStorageSize(key: String) : RuntimeRequest(
method = "state_getStorageSize",
params = listOfNotNull(key)
)
@@ -0,0 +1,8 @@
package io.novafoundation.nova.common.data.network.runtime.calls
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.RuntimeRequest
class GetSystemPropertiesRequest : RuntimeRequest(
method = "system_properties",
params = emptyList()
)
@@ -0,0 +1,10 @@
package io.novafoundation.nova.common.data.network.runtime.calls
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.RuntimeRequest
class NextAccountIndexRequest(accountAddress: String) : RuntimeRequest(
method = "system_accountNextIndex",
params = listOf(
accountAddress
)
)
@@ -0,0 +1,34 @@
package io.novafoundation.nova.common.data.network.runtime.model
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.google.gson.annotations.JsonAdapter
import io.novafoundation.nova.common.data.network.runtime.binding.WeightV2
import java.lang.reflect.Type
import java.math.BigInteger
class FeeResponse(
val partialFee: BigInteger,
@JsonAdapter(WeightDeserizalier::class)
val weight: WeightV2
)
class WeightDeserizalier : JsonDeserializer<WeightV2> {
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): WeightV2 {
return when {
// weight v1
json is JsonPrimitive -> WeightV2.fromV1(json.asLong.toBigInteger())
// weight v2
json is JsonObject -> WeightV2(
refTime = json["ref_time"].asLong.toBigInteger(),
proofSize = json["proof_size"].asLong.toBigInteger()
)
else -> error("Unsupported weight type")
}
}
}
@@ -0,0 +1,15 @@
package io.novafoundation.nova.common.data.network.runtime.model
import com.google.gson.annotations.SerializedName
import io.novafoundation.nova.common.utils.removeHexPrefix
class SignedBlock(val block: Block, val justification: Any?) {
class Block(val extrinsics: List<String>, val header: Header) {
class Header(@SerializedName("number") private val numberRaw: String, val parentHash: String?) {
val number: Int
get() {
return numberRaw.removeHexPrefix().toInt(radix = 16)
}
}
}
}
@@ -0,0 +1,36 @@
package io.novafoundation.nova.common.data.network.runtime.model
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.annotations.JsonAdapter
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
class SystemProperties(
val ss58Format: Int?,
val SS58Prefix: Int?,
@JsonAdapter(WrapToListSerializer::class)
val tokenDecimals: List<Int>,
@JsonAdapter(WrapToListSerializer::class)
val tokenSymbol: List<String>
)
private class WrapToListSerializer : JsonDeserializer<List<*>> {
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): List<*> {
val valueType = (typeOfT as ParameterizedType).actualTypeArguments[0]
if (json.isJsonPrimitive) {
return listOf(context.deserialize<Any?>(json, valueType))
}
return json.asJsonArray.map {
context.deserialize<Any?>(it, valueType)
}
}
}
fun SystemProperties.firstTokenDecimals() = tokenDecimals.first()
fun SystemProperties.firstTokenSymbol() = tokenSymbol.first()
@@ -0,0 +1,13 @@
package io.novafoundation.nova.common.data.network.subquery
import java.math.BigInteger
class EraValidatorInfoQueryResponse(val eraValidatorInfos: SubQueryNodes<EraValidatorInfo>?) {
class EraValidatorInfo(
val id: String,
val address: String,
val era: BigInteger,
val total: String,
val own: String,
)
}
@@ -0,0 +1,20 @@
package io.novafoundation.nova.common.data.network.subquery
class SubQueryResponse<T>(
val data: T
)
class SubQueryNodes<T>(val nodes: List<T>)
class SubQueryTotalCount(val totalCount: Int)
class SubQueryGroupedAggregates<T : GroupedAggregate>(val groupedAggregates: List<T>)
sealed class GroupedAggregate(val keys: List<String>) {
class Sum<T>(val sum: T, keys: List<String>) : GroupedAggregate(keys)
}
fun <T> SubQueryGroupedAggregates<GroupedAggregate.Sum<T>>.firstSum(): T? {
return groupedAggregates.firstOrNull()?.sum
}
@@ -0,0 +1,59 @@
package io.novafoundation.nova.common.data.network.subquery
object SubqueryExpressions {
fun or(vararg innerExpressions: String): String {
return compoundExpression("or", *innerExpressions)
}
fun or(innerExpressions: Collection<String>) = or(*innerExpressions.toTypedArray())
infix fun String.or(another: String): String {
return or(this, another)
}
fun anyOf(innerExpressions: Collection<String>) = or(innerExpressions)
fun anyOf(vararg innerExpressions: String) = or(*innerExpressions)
fun allOf(vararg innerExpressions: String) = and(*innerExpressions)
fun and(vararg innerExpressions: String): String {
return compoundExpression("and", *innerExpressions)
}
fun presentIn(vararg values: String): String {
return compoundExpression("in", *values)
}
fun presentIn(values: List<String>): String {
return presentIn(*values.toTypedArray())
}
infix fun String.and(another: String): String {
return and(this, another)
}
fun and(innerExpressions: Collection<String>) = and(*innerExpressions.toTypedArray())
fun not(expression: String): String {
return "not: {$expression}"
}
private fun compoundExpression(name: String, vararg innerExpressions: String): String {
if (innerExpressions.isEmpty()) {
return ""
}
if (innerExpressions.size == 1) {
return innerExpressions.first()
}
return innerExpressions.joinToString(
prefix = "$name: [",
postfix = "]",
separator = ","
) {
"{$it}"
}
}
}
@@ -0,0 +1,42 @@
package io.novafoundation.nova.common.data.network.subquery
import io.novafoundation.nova.common.data.network.subquery.SubqueryExpressions.or
interface SubQueryFilters {
companion object : SubQueryFilters
infix fun String.equalTo(value: String) = "$this: { equalTo: \"$value\" }"
infix fun String.equalTo(value: Boolean) = "$this: { equalTo: $value }"
infix fun String.equalTo(value: Int) = "$this: { equalTo: $value }"
infix fun String.equalToEnum(value: String) = "$this: { equalTo: $value }"
fun queryParams(
filter: String
): String {
if (filter.isEmpty()) {
return ""
}
return "(filter: { $filter })"
}
infix fun String.presentIn(values: List<String>): String {
val queryValues = values.joinToString(separator = ",") { "\"${it}\"" }
return "$this: { in: [$queryValues] }"
}
fun String.containsFilter(field: String, value: String?): String {
return if (value != null) {
"$this: { contains: { $field: \"$value\" } }"
} else {
or(
"$this: { contains: { $field: null } }",
"not: { $this: { containsKey: \"$field\"} }"
)
}
}
}
@@ -0,0 +1,15 @@
package io.novafoundation.nova.common.data.providers.deviceid
import android.annotation.SuppressLint
import android.content.Context
import android.provider.Settings.Secure
class AndroidDeviceIdProvider(
private val context: Context
) : DeviceIdProvider {
@SuppressLint("HardwareIds")
override fun getDeviceId(): String {
return Secure.getString(context.contentResolver, Secure.ANDROID_ID)
}
}
@@ -0,0 +1,5 @@
package io.novafoundation.nova.common.data.providers.deviceid
interface DeviceIdProvider {
fun getDeviceId(): String
}
@@ -0,0 +1,53 @@
package io.novafoundation.nova.common.data.repository
import io.novafoundation.nova.common.data.model.AssetIconMode
import io.novafoundation.nova.common.data.storage.Preferences
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
interface AssetsIconModeRepository {
fun assetsIconModeFlow(): Flow<AssetIconMode>
fun setAssetsIconMode(assetsViewMode: AssetIconMode)
fun getIconMode(): AssetIconMode
}
private const val PREFS_ASSETS_ICON_MODE = "PREFS_ASSETS_ICON_MODE"
private val ASSET_ICON_MODE_DEFAULT = AssetIconMode.COLORED
class RealAssetsIconModeRepository(
private val preferences: Preferences
) : AssetsIconModeRepository {
override fun assetsIconModeFlow(): Flow<AssetIconMode> {
return preferences.stringFlow(PREFS_ASSETS_ICON_MODE)
.map {
it?.fromPrefsValue() ?: ASSET_ICON_MODE_DEFAULT
}
}
override fun setAssetsIconMode(assetsViewMode: AssetIconMode) {
preferences.putString(PREFS_ASSETS_ICON_MODE, assetsViewMode.toPrefsValue())
}
override fun getIconMode(): AssetIconMode {
return preferences.getString(PREFS_ASSETS_ICON_MODE)?.fromPrefsValue() ?: ASSET_ICON_MODE_DEFAULT
}
private fun AssetIconMode.toPrefsValue(): String {
return when (this) {
AssetIconMode.COLORED -> "colored"
AssetIconMode.WHITE -> "white"
}
}
private fun String.fromPrefsValue(): AssetIconMode? {
return when (this) {
"colored" -> AssetIconMode.COLORED
"white" -> AssetIconMode.WHITE
else -> null
}
}
}
@@ -0,0 +1,55 @@
package io.novafoundation.nova.common.data.repository
import io.novafoundation.nova.common.data.model.AssetViewMode
import io.novafoundation.nova.common.data.storage.Preferences
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
interface AssetsViewModeRepository {
fun getAssetViewMode(): AssetViewMode
fun assetsViewModeFlow(): Flow<AssetViewMode>
suspend fun setAssetsViewMode(assetsViewMode: AssetViewMode)
}
private const val PREFS_ASSETS_VIEW_MODE = "PREFS_ASSETS_VIEW_MODE"
private val ASSET_VIEW_MODE_DEFAULT = AssetViewMode.TOKENS
class RealAssetsViewModeRepository(
private val preferences: Preferences
) : AssetsViewModeRepository {
override fun getAssetViewMode(): AssetViewMode {
return preferences.getString(PREFS_ASSETS_VIEW_MODE)?.fromPrefsValue() ?: ASSET_VIEW_MODE_DEFAULT
}
override fun assetsViewModeFlow(): Flow<AssetViewMode> {
return preferences.stringFlow(PREFS_ASSETS_VIEW_MODE)
.map {
it?.fromPrefsValue() ?: ASSET_VIEW_MODE_DEFAULT
}
}
override suspend fun setAssetsViewMode(assetsViewMode: AssetViewMode) = withContext(Dispatchers.IO) {
preferences.putString(PREFS_ASSETS_VIEW_MODE, assetsViewMode.toPrefsValue())
}
private fun AssetViewMode.toPrefsValue(): String {
return when (this) {
AssetViewMode.NETWORKS -> "networks"
AssetViewMode.TOKENS -> "tokens"
}
}
private fun String.fromPrefsValue(): AssetViewMode? {
return when (this) {
"networks" -> AssetViewMode.NETWORKS
"tokens" -> AssetViewMode.TOKENS
else -> null
}
}
}

Some files were not shown because too many files have changed in this diff Show More