📲 Building a decentralized Mobile Application on Solana

📲 Building a decentralized Mobile Application on Solana

What to expect?

Let's embark on an exciting journey into the realm of the Solana Mobile Stack (SMS). Together, we'll craft an Android Application that incorporates the Mobile Wallet Adapter from the SMS. Even if you've just made a simple to-do list Android app, you're good to go!

Solana

Solana is a blockchain built for mass adoption. It's a high-performance network that is utilized for a range of use cases, including finance, NFTs, payments, and gaming. Solana operates as a single global state machine and is open, interoperable, and decentralized.

Solana Mobile Stack

The Solana Mobile Stack (SMS) is a collection of key technologies for building mobile applications that can interact with the Solana blockchain.

Developing for the Solana Mobile Stack essentially means developing for Android. The software toolkit equips developers with essential libraries for creating wallets and applications, enabling them to design immersive mobile experiences for the Solana network. Whether you're working on a web app that's mobile-friendly, a React Native app for Android, or a dedicated mobile wallet app, these resources including libraries, examples, and model implementations will guide you in utilizing the Solana network on Android devices.

Mobile Wallet Adapter

Mobile Wallet Adapter (MWA) is a protocol specification for connecting mobile dApps to mobile Wallet Apps, enabling communication for Solana transactions and message signing. dApps that implement MWA are able to connect to any compatible MWA Wallet App and request authorization, signing, and sending for transactions/messages.

Mobile PlatformIs MWA Supported?Notes
AndroidFull support for dApps and Wallet apps.
Mobile Web - Chrome (Android)Automatic integration if using @solana/wallet-adapter-react.
iOSMWA is not currently available for any iOS platform (app or browser).
Mobile Web - Safari, Firefox, Opera, BraveThese browsers currently do not support MWA on Android (or iOS).

If you're developing an MWA-compatible wallet app, see the walletlib Android Library that implements the wallet side of the MWA protocol.

Seed Vault

The Seed Vault is a system service providing secure key custody to Wallet apps. By integrating with secure execution environments available on mobile devices (such as secure operating modes of the processor and/or secure auxiliary coprocessors), Seed Vault helps to keep your secrets safe, by moving them to the highest privileged environment available on the device. Your keys, seeds, and secrets never leave the secure execution environment, while UI components built into Android handle interaction with the user to provide a secure transaction signing experience to users.

Solana dApp Store

The Solana dApp Store is an alternate app distribution system, well suited to distributing apps developed by the Solana ecosystem.

It will provide a distribution channel for apps that want to establish direct relationships with their customers, without other app stores’ rules restricting the relationship or seeking a large revenue share. The goal of the Solana dApp Store is to empower the Solana community to eventually play a key role in managing the contents of this app store.

Let's build an Android dApp

We will be using MWA to implement two functionalities in our Android app:
1. Connect to the wallet
2. Sign a message

Pre-requisites

To get started, make sure you have the necessary tools and devices set up.

  • Android Studio: Giraffe | 2022.3.1

  • Emulator or Mobile Device (used to test the app) should have a wallet application (Solfare recommended) installed with a wallet setup

Setup

Open Android Studio and Create a new Project. Select “Empty Activity”.

Enter the application name as “Snap” and click on continue.

UI

We will create a simple UI containing some text description and two buttons. One is for connecting to a wallet and the other is to sign a message. In MainActivity inside the Greeting composable, add the following code:

@Composable
fun Greeting(modifier: Modifier = Modifier) {
    Column(
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to Snap!", style = MaterialTheme.typography.titleLarge)
        Spacer(modifier = Modifier.height(8.dp))
        Text(
            text = "You can connect to your wallet and sign a message on chain.",
            style = MaterialTheme.typography.labelMedium,
            textAlign = TextAlign.Center
        )
        Spacer(modifier = Modifier.height(8.dp))
        Button(onClick = { /*TODO*/ }) {
            Text(text = "Connect Wallet")
        }
        Button(onClick = { /*TODO*/ }) {
            Text(text = "Sign Message")
        }
    }
}

Connect wallet

Add the following dependencies in build.gradle.kts. We are importing MWA and Web3 Library by Portto, it is a similar implementation of web3js library but for Android. Click on "Sync Now" after this.

dependencies {

    ...

    implementation("com.solanamobile:mobile-wallet-adapter-clientlib-ktx:1.1.0")
    implementation ("com.portto.solana:web3:0.1.3")

}

Create a ViewModel Class in com/example/snap/SnapViewModel.kt. This class will contain all the logic for our Greetings composable. We create a SnapViewState which will be used to create data flow between the ViewModel and the UI. It contains wallet details and some booleans. These will be updated when the wallet is connected/disconnected. We have also created a MutableStateFlow for this inside the ViewModel Class.

package com.example.snap

import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update

data class SnapViewState(
    val canTransact: Boolean = false,
    val userAddress: String = "",
    val userLabel: String = "",
    val authToken: String = "",
    val noWallet: Boolean = false
)

class SnapViewModel: ViewModel() {

    private fun SnapViewState.updateViewState() {
        _state.update { this }
    }

    private val _state = MutableStateFlow(SnapViewState())

    val viewState: StateFlow<SnapViewState>
        get() = _state

    init {
        viewModelScope.launch {
            _state.value = SnapViewState()
        }
    }



}

To kick off the protocol, a dApp makes the initial connection with a mobile wallet and sets up an MWA session. Using the latest SDKs, this session is started through Android intents. In this process, the dApp sends out a message with the 'solana-wallet://' code.

Session Establishment Diagram

Let's create connect function inside SnapViewModel. This function will be responsible for handling wallet connections. We first create an instance of MobileWalletAdapter provided by MWA Library. We then call the function transact which requires ActivityResultSender , as it opens the MWA-compatible wallet. Within the transact function, we'll initiate the authorize process by passing the required arguments.

fun connect(
    identityUri: Uri,
    iconUri: Uri,
    identityName: String,
    activityResultSender: ActivityResultSender
) {
    viewModelScope.launch {
        val walletAdapterClient = MobileWalletAdapter()
        val result = walletAdapterClient.transact(activityResultSender) {
            authorize(
                identityUri = identityUri,
                iconUri = iconUri,
                identityName = identityName,
                rpcCluster = RpcCluster.Devnet
            )
        }

}

After this, we will check if the result is a Success, Failure or No Wallet Application was found in the device. If the connection is successful, we update the _state value. Here, PublicKey is provided by the web3 library. We use it to first convert the ByteArray to PublicKey then use toBase58() to get the public key as String . In the other cases, we update the _state booleans accordingly.

when (result) {
    is TransactionResult.Success -> {
        _state.value.copy(
            userAddress = PublicKey(result.payload.publicKey).toBase58(),
            userLabel = result.payload.accountLabel ?: "",
            authToken = result.payload.authToken,
            canTransact = true
        ).updateViewState()
        Log.d(TAG, "connect: $result")
    }

    is TransactionResult.NoWalletFound -> {
        _state.value.copy(
            noWallet = true,
            canTransact = false
        ).updateViewState()
    }

    is TransactionResult.Failure -> {
        _state.value.copy(
            canTransact = false
        ).updateViewState()
    }
}

The MWA session is closed as soon as the wallet is closed, we just fetch the important details such as the user public key, auth token, etc and then save it in our view state. So to disconnect, we simply have to update _state to its default values. Add this function to the SnapViewModel:

fun disconnect() {
    viewModelScope.launch {
        _state.update {
            _state.value.copy(
                userAddress = "",
                userLabel = "",
                authToken = "",
                canTransact = false
            )
        }
    }
}

We are all set with the ViewModel functions. Back to our Greeting composable in MainActivity, we the required arguments and create an instance of the view state.

@Composable
fun Greeting(
    identityUri: Uri,
    iconUri: Uri,
    identityName: String,
    activityResultSender: ActivityResultSender,
    snapViewModel: SnapViewModel = SnapViewModel()
) {

    val viewState by snapViewModel.viewState.collectAsState()

    ...

}

Then we edit the "wallet connect" button, starting with the onClick handler, here we call the connect() and disconnect() function that we previously wrote based on the viewState.userAddress. We also change the button text accordingly.

Button(onClick = {
    if (viewState.userAddress.isEmpty()) {
        snapViewModel.connect(identityUri, iconUri, identityName, activityResultSender)
    } else {
        snapViewModel.disconnect()
    }
}) {
    val pubKey = viewState.userAddress
    val buttonText = when {
        viewState.noWallet -> "Please install a wallet"
        pubKey.isEmpty() -> "Connect Wallet"
        viewState.userAddress.isNotEmpty() -> pubKey.take(4).plus("...").plus(pubKey.takeLast(4))
        else -> ""
    }

    Text(
        modifier = Modifier.padding(start = 8.dp),
        text = buttonText,
        maxLines = 1,
        )
}

Lastly, in the MainActivity we create an instance of activityResultSender and pass the arguments in the Greeting. (At this point, feel free to remove the @Preview section of Greeting)

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val activityResultSender = ActivityResultSender(this)

        setContent {
            SnapTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting(
                        identityUri = Uri.parse(application.getString((R.string.id_url))),
                        iconUri = Uri.parse(application.getString(R.string.id_favicon)),
                        identityName = application.getString(R.string.app_name),
                        activityResultSender = activityResultSender,
                    )
                }
            }
        }
    }
}

Also, add the string resource values in values/strings.xml

<resources>
    <string name="app_name">Snap</string>
    <string name="id_url" translatable="false">https://snap.com</string>
    <string name="id_favicon" translatable="false">favicon.ico</string>
</resources>

Wallet Connection completed, Awesome!

Sign a message

A Solana dApp will have some kind of transaction signing required. So to give an overview let's use the Memo Program from the Solana Program Library, to call a transaction.

Back in SnapViewModel create a new function sign_message. This function will create the transaction and send it for signing. We first require the latest Blockhash. To get it, we declare api which is the Connection Cluster (here we are using Devnet). We then call the function getLatestBlockhash.

class SnapViewModel: ViewModel() {

    private val api by lazy { Connection(Cluster.DEVNET) }

    ...

    fun sign_message(
        identityUri: Uri,
        iconUri: Uri,
        identityName: String,
        activityResultSender: ActivityResultSender

    ){
        viewModelScope.launch {
            withContext(Dispatchers.IO) {
                val blockHash = api.getLatestBlockhash(Commitment.FINALIZED)

            }
        }
    }
}

Now, we create the Transaction object, and pass the recent Blockhash. We also add an instruction from the MemoProgram (This is from the web3 library). We then set the fee payer as the user. We accomplished this because the user had previously linked their wallet, allowing us to store the PublicKey in the view state. Lastly, we serialize the transaction object.

fun sign_message(
    identityUri: Uri,
    iconUri: Uri,
    identityName: String,
    activityResultSender: ActivityResultSender

){
    viewModelScope.launch {
        withContext(Dispatchers.IO) {
            val blockHash = api.getLatestBlockhash(Commitment.FINALIZED)

            val tx = Transaction()
            tx.add(MemoProgram.writeUtf8(PublicKey(_state.value.userAddress), "memoText"))
            tx.setRecentBlockHash(blockHash!!)
            tx.feePayer = PublicKey(_state.value.userAddress)

            val bytes = tx.serialize(SerializeConfig(requireAllSignatures = false))



        }
    }
}

The transaction object is all set, now we send it using the MobileWalletAdapter object again.

Authorize and Sign Diagram

This time we perform two operations inside the transact scope. First, we reauthorize the user, this is done using the authToken we had saved before.

Then we call signAndSendTransaction this function, as the name suggests, sign the transactions from the user and then send it to the chain.

Sign and Send Diagram

    fun sign_message(
        identityUri: Uri,
        iconUri: Uri,
        identityName: String,
        activityResultSender: ActivityResultSender

    ){
        viewModelScope.launch {
            withContext(Dispatchers.IO) {

                ...

                val walletAdapterClient = MobileWalletAdapter()
                val result = walletAdapterClient.transact(activityResultSender) {
                    reauthorize(identityUri, iconUri, identityName, _state.value.authToken)
                    signAndSendTransactions(arrayOf(bytes))
                }

            }
        }
    }

The result which will be received, if successful, it will contain the transaction signature. To decode it, we first need to import another library. Add this inside build.gradle.kts (Don't forget to click on "Sync Now")

dependencies {

    ...

    implementation ("org.bitcoinj:bitcoinj-core:0.16.2")


}

Back to the sign_message function, we extract the signature from the result. Make it in a readable format using Base58.encode() by the library we just imported. We then log the transaction link, so that after we run, we can check the transaction from the logs.

fun sign_message(
    identityUri: Uri,
    iconUri: Uri,
    identityName: String,
    activityResultSender: ActivityResultSender

){
    viewModelScope.launch {
        withContext(Dispatchers.IO) {

            ...

            result.successPayload?.signatures?.firstOrNull()?.let { sig ->
                val readableSig = Base58.encode(sig)
                Log.d(TAG, "sign_message: https://explorer.solana.com/tx/$readableSig?cluster=devnet")
            }

        }
    }
}

Finally, in the Greetings composable, we add the function sign_message when "Sign Message" button is clicked.

Button(
    onClick = {
        snapViewModel.sign_message(identityUri, iconUri, identityName, activityResultSender)
    },
) {
    Text(text = "Sign Message")
}

Transaction signing completed, Awesome!

Build and Run

Everything is set, let's run the app now🤞

Click on "Connect Wallet"

You will be redirected to an MWA-compatible wallet (If you have installed it, lol).

After the wallet connection is successful, click on "Sign Message"

Again, it will open the wallet, and confirm the transaction.

And, done!

You can check the transaction link, in the Logcat.

What's next?

This brings us to the end of this blog. We started with Solana Mobile Stack, exploring what it offers. We then created an Android Application using a Mobile Wallet Adapter provided by SMS, to connect a wallet and sign transactions. The codebase is available on GitHub. I highly recommend the docs to get a deep-level understanding of the Solana Mobile Stack.
Before you go, these are the things I will recommend to explore after this:

  1. Proper Implementation: Right now, if we close the app, or if the UI refreshes the wallet details will be back to default. Implement a proper architecture with Data persistence and Data injection.
  1. SolanaKT: This is an open-source library on Kotlin for Solana protocol. Leverage it to integrate your own Smart Contract in the code.

  2. SolanaPay: This protocol was developed independently of the Solana Mobile Stack, but combining payments with a mobile device is a natural fit for Solana Pay.

  3. Minty Fresh: A Kotlin Android app where you can take a picture and mint it into NFT.

Feel free to reach out if you have any doubts :)