Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2fc4c82
docs: add Solana roadmap
WRRicht3r Jan 26, 2026
9a57b33
feat: bootstrap solana chain metadata
WRRicht3r Jan 26, 2026
b032dc2
feat: add Solana account foundation
WRRicht3r Jan 26, 2026
3ecd5e8
chore: drop solanaj dependency
WRRicht3r Jan 26, 2026
51079ef
fix: stabilize diff and formatter tests
WRRicht3r Jan 26, 2026
00b8332
fix: align crypto detail formatter with tests
WRRicht3r Jan 26, 2026
69e857b
fix(core-db): add solana column handling
WRRicht3r Jan 26, 2026
9ebec8a
fix(core-db): correct solana account lookup
WRRicht3r Jan 26, 2026
b03cb13
fix: allow Solana json import
WRRicht3r Jan 26, 2026
20bc425
fix: pass solana derivation path
WRRicht3r Jan 26, 2026
8b059cb
fix: ensure solana derivation default
WRRicht3r Jan 27, 2026
f496da6
chore: harden solana chain fallback
WRRicht3r Jan 27, 2026
7efab84
chain fixes, milestone 1
WRRicht3r Jan 27, 2026
9234a09
feat(ui): refresh bottom nav and move NFTs to dedicated tab
WRRicht3r Jan 27, 2026
45b94b5
fix(onboarding): handle Solana banner gradient without painterResource
WRRicht3r Jan 27, 2026
f269984
docs(roadmap): add Solana native staking milestone
WRRicht3r Jan 27, 2026
9bb69bb
docs(roadmap): renumber settings milestone
WRRicht3r Jan 27, 2026
8898d20
fix(staking): ignore null active era entries
WRRicht3r Jan 27, 2026
ebfdbc9
fix(staking): align with develop after #1245
WRRicht3r Jan 29, 2026
0b15fd2
Merge branch 'develop' into fearless-five
WRRicht3r Jan 29, 2026
99849a1
fix: align PR1246 deltas and roadmap
WRRicht3r Jan 30, 2026
9a4917f
feat: advance solana staking milestone
WRRicht3r Jan 30, 2026
253a6a1
Merge branch 'develop' into fearless-five
WRRicht3r Jan 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,12 @@ class MainFragment : BaseFragment<MainViewModel>(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!!)
Expand Down
7 changes: 6 additions & 1 deletion app/src/main/res/navigation/bottom_nav_graph.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
android:name="jp.co.soramitsu.staking.impl.presentation.staking.main.StakingFragment"
android:label="StakingFragment" />

<fragment
android:id="@+id/nftCollectionsFragment"
android:name="jp.co.soramitsu.wallet.impl.presentation.balance.nft.list.NftCollectionsFragment"
android:label="NftCollectionsFragment" />

<fragment
android:id="@+id/walletFragment"
android:name="jp.co.soramitsu.wallet.impl.presentation.balance.list.BalanceListFragment"
Expand All @@ -36,4 +41,4 @@
tools:layout="@layout/fragment_crowdloans"
android:name="jp.co.soramitsu.crowdloan.impl.presentation.main.CrowdloanFragment"
android:label="CrowdloanFragment" />
</navigation>
</navigation>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
Original file line number Diff line number Diff line change
@@ -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<SolanaSecrets> {
override fun put(metaId: Long, secrets: EncodableStruct<SolanaSecrets>) {
encryptedPreferences.putEncryptedString("$metaId:$SOLANA_SECRETS", secrets.toHexString())
}

override fun get(metaId: Long): EncodableStruct<SolanaSecrets>? {
return encryptedPreferences.getDecryptedString("$metaId:$SOLANA_SECRETS")
?.let(SolanaSecrets::read)
}
}

object SolanaSecrets : Schema<SolanaSecrets>() {
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> = SolanaSecrets { secrets ->
secrets[Seed] = seed
secrets[PrivateKey] = solanaKeypair.privateKey
secrets[PublicKey] = solanaKeypair.publicKey
secrets[DerivationPath] = derivationPath
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -236,6 +237,12 @@ class CommonModule {
encryptedPreferences: EncryptedPreferences
) = TonSecretStore(encryptedPreferences)

@Provides
@Singleton
fun provideSolanaSecretStore(
encryptedPreferences: EncryptedPreferences
) = SolanaSecretStore(encryptedPreferences)

@Provides
@Singleton
fun provideNetworkStateMixin() = NetworkStateService()
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ 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) {
Ecosystem.Substrate -> WalletEcosystem.Substrate
Ecosystem.Ton -> WalletEcosystem.Ton
Ecosystem.EthereumBased,
Ecosystem.Ethereum -> WalletEcosystem.Ethereum
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ object Base58Ext {
private val base58 = Base58()

fun String.fromBase58Check() = base58.decodeChecked(this)

fun ByteArray.toBase58(): String = base58.encode(this)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>,
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<Int> {
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
)
}
11 changes: 11 additions & 0 deletions common/src/main/res/drawable/background_banner_solana.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">

<gradient
android:startColor="#9945FF"
android:endColor="#14F195"
android:angle="0" />

<corners android:radius="24dp" />
</shape>
15 changes: 15 additions & 0 deletions common/src/main/res/drawable/ic_nav_nft.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,2L4,7v10l8,5l8,-5V7L12,2zM6.5,8.44l5.5,-3.44l5.5,3.44v7.12l-5.5,3.44l-5.5,-3.44V8.44z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M9.25,9.25h1.5v5.5h-1.5z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M13.25,9.25h1.5v5.5h-1.5z" />
</vector>
30 changes: 30 additions & 0 deletions common/src/main/res/drawable/ic_nav_polkaswap.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M18.167,15.154C19.057,16.071 19.057,17.514 18.167,18.432C17.276,19.35 15.87,19.35 14.98,18.432C14.089,17.514 12.167,12.321 12.167,12.321C12.167,12.321 17.276,14.237 18.167,15.154Z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M11.833,12.321C11.833,12.321 9.91,17.512 9.02,18.432C8.13,19.35 6.724,19.35 5.833,18.432C4.943,17.514 4.943,16.071 5.833,15.154C6.724,14.237 11.833,12.321 11.833,12.321Z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M18.167,5.568C19.057,6.486 19.057,7.929 18.167,8.846C17.276,9.764 12.167,11.679 12.167,11.679C12.167,11.679 14.089,6.488 14.98,5.568C15.874,4.649 17.276,4.649 18.167,5.568Z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M9.02,5.568C9.91,6.486 11.833,11.679 11.833,11.679C11.833,11.679 6.726,9.764 5.833,8.846C4.943,7.929 4.943,6.486 5.833,5.568C6.724,4.649 8.13,4.649 9.02,5.568Z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M23.25,12.333C23.25,13.597 22.271,14.625 21.059,14.625C19.848,14.625 12.834,12.333 12.834,12.333C12.834,12.333 19.848,10.042 21.059,10.042C22.271,10.042 23.25,11.07 23.25,12.333Z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M11.166,12.333C11.166,12.333 4.152,14.625 2.941,14.625C1.729,14.625 0.75,13.597 0.75,12.333C0.75,11.07 1.729,10.042 2.941,10.042C4.152,10.042 11.166,12.333 11.166,12.333Z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M12,23.75C10.788,23.75 9.809,22.722 9.809,21.458C9.809,20.195 12,12.75 12,12.75C12,12.75 14.191,20.195 14.191,21.458C14.191,22.722 13.212,23.75 12,23.75Z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M12,11.917C12,11.917 9.809,4.472 9.809,3.208C9.809,1.945 10.788,0.917 12,0.917C13.212,0.917 14.191,1.945 14.191,3.208C14.191,4.472 12,11.917 12,11.917Z" />
</vector>
22 changes: 18 additions & 4 deletions common/src/main/res/menu/bottom_navigations.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,24 @@


<item
android:id="@+id/discoverDappFragment"
android:id="@+id/stakingFragment"
android:enabled="true"
android:icon="@drawable/ic_nav_dapp"
android:title="@string/tc_discover_dapp"
android:icon="@drawable/ic_nav_staking"
android:title="@string/tabbar_staking_title"
app:showAsAction="ifRoom" />

<item
android:id="@+id/swapTokensFragment"
android:enabled="true"
android:icon="@drawable/ic_nav_polkaswap"
android:title="@string/tabbar_polkaswap_title"
app:showAsAction="ifRoom" />

<item
android:id="@+id/nftCollectionsFragment"
android:enabled="true"
android:icon="@drawable/ic_nav_nft"
android:title="@string/tabbar_nft_title"
app:showAsAction="ifRoom" />

<item
Expand All @@ -23,4 +37,4 @@
android:icon="@drawable/ic_settings"
android:title="@string/profile_settings_title"
app:showAsAction="ifRoom" />
</menu>
</menu>
Loading