mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-22 02:07:58 +00:00
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:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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.
@@ -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
|
||||
}
|
||||
+33
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -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()
|
||||
}
|
||||
}
|
||||
+47
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -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")
|
||||
}
|
||||
}
|
||||
+21
@@ -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
|
||||
}
|
||||
+37
@@ -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())
|
||||
+20
@@ -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() }
|
||||
}
|
||||
}
|
||||
+117
@@ -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)
|
||||
}
|
||||
+34
@@ -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)
|
||||
}
|
||||
}
|
||||
+51
@@ -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
|
||||
}
|
||||
+39
@@ -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()
|
||||
+10
@@ -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)
|
||||
}
|
||||
+83
@@ -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))
|
||||
}
|
||||
}
|
||||
+12
@@ -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()
|
||||
+164
@@ -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"]),
|
||||
)
|
||||
}
|
||||
+32
@@ -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()
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package io.novafoundation.nova.common.data.network.runtime.binding
|
||||
|
||||
import java.math.BigInteger
|
||||
|
||||
typealias BalanceOf = BigInteger
|
||||
+100
@@ -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() }
|
||||
+20
@@ -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)
|
||||
+57
@@ -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()
|
||||
}
|
||||
}
|
||||
+22
@@ -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?
|
||||
}
|
||||
+42
@@ -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()
|
||||
}
|
||||
}
|
||||
+64
@@ -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()
|
||||
}
|
||||
+25
@@ -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
|
||||
}
|
||||
+42
@@ -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)
|
||||
}
|
||||
+47
@@ -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())
|
||||
}
|
||||
+11
@@ -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)
|
||||
}
|
||||
+65
@@ -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()
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package io.novafoundation.nova.common.data.network.runtime.binding
|
||||
|
||||
import java.math.BigInteger
|
||||
|
||||
typealias ParaId = BigInteger
|
||||
+23
@@ -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()
|
||||
+34
@@ -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)
|
||||
}
|
||||
}
|
||||
+86
@@ -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()
|
||||
}
|
||||
}
|
||||
+8
@@ -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)
|
||||
)
|
||||
+11
@@ -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
|
||||
)
|
||||
)
|
||||
+10
@@ -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
|
||||
)
|
||||
)
|
||||
+8
@@ -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()
|
||||
)
|
||||
+11
@@ -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)
|
||||
)
|
||||
+8
@@ -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()
|
||||
)
|
||||
+10
@@ -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
|
||||
)
|
||||
)
|
||||
+8
@@ -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)
|
||||
)
|
||||
+8
@@ -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()
|
||||
)
|
||||
+10
@@ -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
|
||||
)
|
||||
)
|
||||
+34
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -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()
|
||||
+13
@@ -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,
|
||||
)
|
||||
}
|
||||
+20
@@ -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
|
||||
}
|
||||
+59
@@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -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\"} }"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -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)
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package io.novafoundation.nova.common.data.providers.deviceid
|
||||
|
||||
interface DeviceIdProvider {
|
||||
fun getDeviceId(): String
|
||||
}
|
||||
+53
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -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
Reference in New Issue
Block a user