diff --git a/app/src/main/java/jp/co/soramitsu/app/root/presentation/main/MainFragment.kt b/app/src/main/java/jp/co/soramitsu/app/root/presentation/main/MainFragment.kt index 80b905c7ab..5637d7d846 100644 --- a/app/src/main/java/jp/co/soramitsu/app/root/presentation/main/MainFragment.kt +++ b/app/src/main/java/jp/co/soramitsu/app/root/presentation/main/MainFragment.kt @@ -78,7 +78,12 @@ class MainFragment : BaseFragment(R.layout.fragment_main) { binding.bottomNavigationViewWithFab.setupWithNavController(navController!!) binding.bottomNavigationView.setOnItemSelectedListener { item -> - onNavDestinationSelected(item, navController!!) + if (item.itemId == R.id.swapTokensFragment) { + viewModel.navigateToSwapScreen() + false + } else { + onNavDestinationSelected(item, navController!!) + } } binding.bottomNavigationViewWithFab.setOnItemSelectedListener { item -> onNavDestinationSelected(item, navController!!) diff --git a/app/src/main/res/navigation/bottom_nav_graph.xml b/app/src/main/res/navigation/bottom_nav_graph.xml index 74b3e57916..f6b15d452a 100644 --- a/app/src/main/res/navigation/bottom_nav_graph.xml +++ b/app/src/main/res/navigation/bottom_nav_graph.xml @@ -26,6 +26,11 @@ android:name="jp.co.soramitsu.staking.impl.presentation.staking.main.StakingFragment" android:label="StakingFragment" /> + + - \ No newline at end of file + diff --git a/common/src/main/java/jp/co/soramitsu/common/address/AddressIconGenerator.kt b/common/src/main/java/jp/co/soramitsu/common/address/AddressIconGenerator.kt index 9709a44e3a..ae3a7e1609 100644 --- a/common/src/main/java/jp/co/soramitsu/common/address/AddressIconGenerator.kt +++ b/common/src/main/java/jp/co/soramitsu/common/address/AddressIconGenerator.kt @@ -103,7 +103,8 @@ suspend fun AddressIconGenerator.createAddressIcon(supportedEcosystemWithAddress val address = supportedEcosystemWithAddress.toList().sortedBy { when (it.first) { WalletEcosystem.Substrate -> 1 - WalletEcosystem.Ethereum -> 2 + WalletEcosystem.Ethereum, + WalletEcosystem.Solana -> 2 WalletEcosystem.Ton -> 3 } }[0].second @@ -122,7 +123,8 @@ suspend fun AddressIconGenerator.createAddressModel(supportedEcosystemWithAddres val address = supportedEcosystemWithAddress.toList().sortedBy { when (it.first) { WalletEcosystem.Substrate -> 1 - WalletEcosystem.Ethereum -> 2 + WalletEcosystem.Ethereum, + WalletEcosystem.Solana -> 2 WalletEcosystem.Ton -> 3 } }[0].second @@ -214,7 +216,8 @@ class StatelessAddressIconGenerator( val sizeInPx = resourceManager.measureInPx(sizeInDp) val icon = when (ecosystem) { WalletEcosystem.Substrate -> iconGenerator.getSubstrateWalletIcon(sizeInPx) - WalletEcosystem.Ethereum -> iconGenerator.getEvmWalletIcon(sizeInPx) + WalletEcosystem.Ethereum, + WalletEcosystem.Solana -> iconGenerator.getEvmWalletIcon(sizeInPx) WalletEcosystem.Ton -> iconGenerator.getTonWalletIcon(sizeInPx) } icon diff --git a/common/src/main/java/jp/co/soramitsu/common/compose/component/AssetListItem.kt b/common/src/main/java/jp/co/soramitsu/common/compose/component/AssetListItem.kt index a639b1e327..ac6b76ab38 100644 --- a/common/src/main/java/jp/co/soramitsu/common/compose/component/AssetListItem.kt +++ b/common/src/main/java/jp/co/soramitsu/common/compose/component/AssetListItem.kt @@ -126,7 +126,7 @@ fun AssetListItem( } Row { Text( - text = state.assetSymbol.uppercase(), + text = state.decoratedSymbol, style = MaterialTheme.customTypography.header3, modifier = Modifier .padding(vertical = 4.dp) diff --git a/common/src/main/java/jp/co/soramitsu/common/compose/viewstate/AssetListItemViewState.kt b/common/src/main/java/jp/co/soramitsu/common/compose/viewstate/AssetListItemViewState.kt index f1ee3f5c66..b97bfec9b1 100644 --- a/common/src/main/java/jp/co/soramitsu/common/compose/viewstate/AssetListItemViewState.kt +++ b/common/src/main/java/jp/co/soramitsu/common/compose/viewstate/AssetListItemViewState.kt @@ -15,7 +15,14 @@ data class AssetListItemViewState( val chainAssetId: String, val isSupported: Boolean, val isHidden: Boolean, - val isTestnet: Boolean + val isTestnet: Boolean, + val isMemecoin: Boolean = false ) { val key = listOf(index ?: 0, chainAssetId, chainId, isHidden).joinToString() + + val decoratedSymbol: String + get() = buildString { + if (isMemecoin) append("\uD83D\uDC38 ") + append(assetSymbol.uppercase()) + } } diff --git a/common/src/main/java/jp/co/soramitsu/common/data/secrets/v3/SolanaSecretStore.kt b/common/src/main/java/jp/co/soramitsu/common/data/secrets/v3/SolanaSecretStore.kt new file mode 100644 index 0000000000..b2817f7dea --- /dev/null +++ b/common/src/main/java/jp/co/soramitsu/common/data/secrets/v3/SolanaSecretStore.kt @@ -0,0 +1,41 @@ +package jp.co.soramitsu.common.data.secrets.v3 + +import jp.co.soramitsu.common.data.storage.encrypt.EncryptedPreferences +import jp.co.soramitsu.common.utils.invoke +import jp.co.soramitsu.shared_utils.encrypt.keypair.Keypair +import jp.co.soramitsu.shared_utils.scale.EncodableStruct +import jp.co.soramitsu.shared_utils.scale.Schema +import jp.co.soramitsu.shared_utils.scale.byteArray +import jp.co.soramitsu.shared_utils.scale.string +import jp.co.soramitsu.shared_utils.scale.toHexString + +private const val SOLANA_SECRETS = "SOLANA_SECRETS" + +class SolanaSecretStore(private val encryptedPreferences: EncryptedPreferences) : SecretStore { + override fun put(metaId: Long, secrets: EncodableStruct) { + encryptedPreferences.putEncryptedString("$metaId:$SOLANA_SECRETS", secrets.toHexString()) + } + + override fun get(metaId: Long): EncodableStruct? { + return encryptedPreferences.getDecryptedString("$metaId:$SOLANA_SECRETS") + ?.let(SolanaSecrets::read) + } +} + +object SolanaSecrets : Schema() { + val Seed by byteArray() + val PrivateKey by byteArray() + val PublicKey by byteArray() + val DerivationPath by string() +} + +fun SolanaSecrets( + seed: ByteArray, + solanaKeypair: Keypair, + derivationPath: String +): EncodableStruct = SolanaSecrets { secrets -> + secrets[Seed] = seed + secrets[PrivateKey] = solanaKeypair.privateKey + secrets[PublicKey] = solanaKeypair.publicKey + secrets[DerivationPath] = derivationPath +} diff --git a/common/src/main/java/jp/co/soramitsu/common/di/modules/CommonModule.kt b/common/src/main/java/jp/co/soramitsu/common/di/modules/CommonModule.kt index c61b0b92e3..6a2dd61add 100644 --- a/common/src/main/java/jp/co/soramitsu/common/di/modules/CommonModule.kt +++ b/common/src/main/java/jp/co/soramitsu/common/di/modules/CommonModule.kt @@ -22,6 +22,7 @@ import jp.co.soramitsu.common.data.secrets.v1.SecretStoreV1 import jp.co.soramitsu.common.data.secrets.v1.SecretStoreV1Impl import jp.co.soramitsu.common.data.secrets.v2.SecretStoreV2 import jp.co.soramitsu.common.data.secrets.v3.EthereumSecretStore +import jp.co.soramitsu.common.data.secrets.v3.SolanaSecretStore import jp.co.soramitsu.common.data.secrets.v3.SubstrateSecretStore import jp.co.soramitsu.common.data.secrets.v3.TonSecretStore import jp.co.soramitsu.common.data.storage.Preferences @@ -236,6 +237,12 @@ class CommonModule { encryptedPreferences: EncryptedPreferences ) = TonSecretStore(encryptedPreferences) + @Provides + @Singleton + fun provideSolanaSecretStore( + encryptedPreferences: EncryptedPreferences + ) = SolanaSecretStore(encryptedPreferences) + @Provides @Singleton fun provideNetworkStateMixin() = NetworkStateService() diff --git a/common/src/main/java/jp/co/soramitsu/common/domain/SolanaConstants.kt b/common/src/main/java/jp/co/soramitsu/common/domain/SolanaConstants.kt new file mode 100644 index 0000000000..765fa4f1e0 --- /dev/null +++ b/common/src/main/java/jp/co/soramitsu/common/domain/SolanaConstants.kt @@ -0,0 +1,9 @@ +package jp.co.soramitsu.common.domain + +const val SOLANA_CHAIN_ID = "solana-mainnet" +const val SOLANA_DEVNET_CHAIN_ID = "solana-devnet" +const val SOLANA_DEFAULT_PATH = "m/44'/501'/0'/0'" + +fun isSolanaChainId(chainId: String): Boolean { + return chainId == SOLANA_CHAIN_ID || chainId == SOLANA_DEVNET_CHAIN_ID +} diff --git a/common/src/main/java/jp/co/soramitsu/common/model/WalletEcosystem.kt b/common/src/main/java/jp/co/soramitsu/common/model/WalletEcosystem.kt index dd7534eb8f..dfcb9f0865 100644 --- a/common/src/main/java/jp/co/soramitsu/common/model/WalletEcosystem.kt +++ b/common/src/main/java/jp/co/soramitsu/common/model/WalletEcosystem.kt @@ -3,7 +3,7 @@ package jp.co.soramitsu.common.model import jp.co.soramitsu.core.models.Ecosystem enum class WalletEcosystem { - Substrate, Ethereum, Ton + Substrate, Ethereum, Ton, Solana } fun Ecosystem.toAccountType() = when (this) { @@ -11,4 +11,4 @@ fun Ecosystem.toAccountType() = when (this) { Ecosystem.Ton -> WalletEcosystem.Ton Ecosystem.EthereumBased, Ecosystem.Ethereum -> WalletEcosystem.Ethereum -} \ No newline at end of file +} diff --git a/common/src/main/java/jp/co/soramitsu/common/utils/Base58Ext.kt b/common/src/main/java/jp/co/soramitsu/common/utils/Base58Ext.kt index 34a048cf15..56843f9f2a 100644 --- a/common/src/main/java/jp/co/soramitsu/common/utils/Base58Ext.kt +++ b/common/src/main/java/jp/co/soramitsu/common/utils/Base58Ext.kt @@ -8,4 +8,6 @@ object Base58Ext { private val base58 = Base58() fun String.fromBase58Check() = base58.decodeChecked(this) + + fun ByteArray.toBase58(): String = base58.encode(this) } diff --git a/common/src/main/java/jp/co/soramitsu/common/utils/NumberFormatters.kt b/common/src/main/java/jp/co/soramitsu/common/utils/NumberFormatters.kt index 5a145676ae..56cdf04ca0 100644 --- a/common/src/main/java/jp/co/soramitsu/common/utils/NumberFormatters.kt +++ b/common/src/main/java/jp/co/soramitsu/common/utils/NumberFormatters.kt @@ -12,6 +12,7 @@ import jp.co.soramitsu.common.utils.formatting.NumberAbbreviation import java.math.BigDecimal import java.math.RoundingMode import java.text.DecimalFormat +import java.text.DecimalFormatSymbols import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -29,6 +30,7 @@ private val fiatAmountFormatter = FiatFormatter() private val fiatSmallAmountFormatter = FiatSmallFormatter() private val percentAmountFormatter = FixedPrecisionFormatter(MAX_DECIMALS_2) private val cryptoAmountShortFormatter = FixedPrecisionFormatter(MAX_DECIMALS_3) +private val cryptoAmountMediumFormatter = FixedPrecisionFormatter(6) private val cryptoAmountDetailFormatter = FixedPrecisionFormatter(MAX_DECIMALS_8) private val fiatAbbreviatedFormatter = fiatAbbreviatedFormatter() @@ -105,7 +107,7 @@ fun Long.formatDateTime(): String = SimpleDateFormat.getDateInstance().format(Da fun Long.formatTime(): String = SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date(this)) -fun decimalFormatterFor(pattern: String) = DecimalFormat(pattern).apply { +fun decimalFormatterFor(pattern: String) = DecimalFormat(pattern, DecimalFormatSymbols(Locale.US)).apply { roundingMode = RoundingMode.FLOOR } diff --git a/common/src/main/java/jp/co/soramitsu/common/utils/solana/SolanaKeyFactory.kt b/common/src/main/java/jp/co/soramitsu/common/utils/solana/SolanaKeyFactory.kt new file mode 100644 index 0000000000..94668b2869 --- /dev/null +++ b/common/src/main/java/jp/co/soramitsu/common/utils/solana/SolanaKeyFactory.kt @@ -0,0 +1,92 @@ +package jp.co.soramitsu.common.utils.solana + +import jp.co.soramitsu.common.domain.SOLANA_DEFAULT_PATH +import java.nio.ByteBuffer +import java.nio.ByteOrder +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import org.ton.api.pk.PrivateKeyEd25519 +import org.ton.mnemonic.Mnemonic + +data class SolanaKeypair( + val privateKey: ByteArray, + val publicKey: ByteArray +) + +object SolanaKeyFactory { + + private const val HARDENED_BIT = 0x80000000.toInt() + private val ED25519_SEED = "ed25519 seed".toByteArray() + + fun deriveKeypair( + mnemonicWords: List, + derivationPath: String = SOLANA_DEFAULT_PATH + ): SolanaKeypair { + val seed = Mnemonic.toSeed(mnemonicWords) + val master = slip10Master(seed) + val indices = parsePath(derivationPath) + + val finalNode = indices.fold(master) { current, index -> + deriveChild(current, index) + } + + val privateKey = finalNode.key + val keypair = PrivateKeyEd25519(privateKey) + val publicKey = keypair.publicKey().key.toByteArray() + + return SolanaKeypair( + privateKey = privateKey, + publicKey = publicKey + ) + } + + private fun slip10Master(seed: ByteArray): Slip10Node { + val digest = hmacSha512(ED25519_SEED, seed) + val key = digest.copyOfRange(0, 32) + val chainCode = digest.copyOfRange(32, 64) + return Slip10Node(key, chainCode) + } + + private fun deriveChild(parent: Slip10Node, index: Int): Slip10Node { + val indexBytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(index).array() + val data = ByteArray(1 + parent.key.size + 4) + data[0] = 0 + parent.key.copyInto(data, destinationOffset = 1) + indexBytes.copyInto(data, destinationOffset = 1 + parent.key.size) + + val digest = hmacSha512(parent.chainCode, data) + val key = digest.copyOfRange(0, 32) + val chainCode = digest.copyOfRange(32, 64) + + return Slip10Node(key, chainCode) + } + + private fun parsePath(path: String): List { + val raw = path.removePrefix("m/") + if (raw.isEmpty()) return emptyList() + + return raw.split("/") + .filter { it.isNotEmpty() } + .map { component -> + val hardened = component.endsWith("'") + val number = component.removeSuffix("'").toInt() + if (hardened) { + number or HARDENED_BIT + } else { + number + } + } + } + + private fun hmacSha512(key: ByteArray, data: ByteArray): ByteArray { + val mac = Mac.getInstance("HmacSHA512") + val secretKeySpec = SecretKeySpec(key, "HmacSHA512") + mac.init(secretKeySpec) + return mac.doFinal(data) + } + + private data class Slip10Node( + val key: ByteArray, + val chainCode: ByteArray + ) +} diff --git a/common/src/main/res/drawable/background_banner_solana.xml b/common/src/main/res/drawable/background_banner_solana.xml new file mode 100644 index 0000000000..db350723c4 --- /dev/null +++ b/common/src/main/res/drawable/background_banner_solana.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/common/src/main/res/drawable/ic_nav_nft.xml b/common/src/main/res/drawable/ic_nav_nft.xml new file mode 100644 index 0000000000..67cf1b4221 --- /dev/null +++ b/common/src/main/res/drawable/ic_nav_nft.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/common/src/main/res/drawable/ic_nav_polkaswap.xml b/common/src/main/res/drawable/ic_nav_polkaswap.xml new file mode 100644 index 0000000000..593b1d9047 --- /dev/null +++ b/common/src/main/res/drawable/ic_nav_polkaswap.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/common/src/main/res/menu/bottom_navigations.xml b/common/src/main/res/menu/bottom_navigations.xml index e3864e4e5e..e2cf7e53cc 100644 --- a/common/src/main/res/menu/bottom_navigations.xml +++ b/common/src/main/res/menu/bottom_navigations.xml @@ -11,10 +11,24 @@ + + + + - \ No newline at end of file + diff --git a/common/src/main/res/menu/bottom_navigations_fab.xml b/common/src/main/res/menu/bottom_navigations_fab.xml index 3111cb0bc8..b064dba272 100644 --- a/common/src/main/res/menu/bottom_navigations_fab.xml +++ b/common/src/main/res/menu/bottom_navigations_fab.xml @@ -10,10 +10,10 @@ app:showAsAction="ifRoom" />