diff --git a/examples/example-clientlib-ktx-app/app/build.gradle b/examples/example-clientlib-ktx-app/app/build.gradle index e7da017af..972cbf266 100644 --- a/examples/example-clientlib-ktx-app/app/build.gradle +++ b/examples/example-clientlib-ktx-app/app/build.gradle @@ -19,6 +19,16 @@ android { vectorDrawables { useSupportLibrary true } + + Properties properties = new Properties() + def propertiesFile = project.rootProject.file('local.properties') + if (propertiesFile.exists()) { + properties.load(propertiesFile.newDataInputStream()) + + buildConfigField "String", "HELIUS_KEY", "\"${properties.getProperty("HELIUS_KEY")}\"" + } else { + buildConfigField "String", "HELIUS_KEY", "\"\"" + } } buildTypes { diff --git a/examples/example-clientlib-ktx-app/app/src/main/java/com/solanamobile/ktxclientsample/ui/SampleScreen.kt b/examples/example-clientlib-ktx-app/app/src/main/java/com/solanamobile/ktxclientsample/ui/SampleScreen.kt index b5fecd9e7..666ecc7fd 100644 --- a/examples/example-clientlib-ktx-app/app/src/main/java/com/solanamobile/ktxclientsample/ui/SampleScreen.kt +++ b/examples/example-clientlib-ktx-app/app/src/main/java/com/solanamobile/ktxclientsample/ui/SampleScreen.kt @@ -129,7 +129,7 @@ fun SampleScreen( Text( modifier = Modifier.weight(1f), - text = if (viewState.canTransact && viewState.solBalance >= 0) viewState.solBalance.toString() else "-", + text = if (viewState.solBalance >= 0) viewState.solBalance.toString() else "-", style = MaterialTheme.typography.h5, maxLines = 1, overflow = TextOverflow.Ellipsis @@ -151,30 +151,6 @@ fun SampleScreen( ) } } - - val buttonText = when { - viewState.canTransact && viewState.walletFound -> "Disconnect" - !viewState.walletFound -> "Please install a compatible wallet" - else -> "Add funds to get started" - } - - Button( - modifier = Modifier - .fillMaxWidth() - .padding(top = 4.dp), - enabled = viewState.canTransact, - colors = ButtonDefaults.buttonColors( - backgroundColor = Color.Red.copy(red = 0.7f) - ), - onClick = { - viewModel.disconnect() - } - ) { - Text( - color = MaterialTheme.colors.onPrimary, - text = buttonText - ) - } } } @@ -231,9 +207,10 @@ fun SampleScreen( modifier = Modifier .weight(1f) .padding(end = 8.dp), - enabled = viewState.canTransact && memoText.isNotEmpty(), onClick = { - viewModel.publishMemo(intentSender, memoText) + if (memoText.isNotEmpty()) { + viewModel.publishMemo(intentSender, memoText) + } } ) { Text("Publish Memo") diff --git a/examples/example-clientlib-ktx-app/app/src/main/java/com/solanamobile/ktxclientsample/usecase/PersistanceUseCase.kt b/examples/example-clientlib-ktx-app/app/src/main/java/com/solanamobile/ktxclientsample/usecase/PersistanceUseCase.kt index 17b92a919..c1b218287 100644 --- a/examples/example-clientlib-ktx-app/app/src/main/java/com/solanamobile/ktxclientsample/usecase/PersistanceUseCase.kt +++ b/examples/example-clientlib-ktx-app/app/src/main/java/com/solanamobile/ktxclientsample/usecase/PersistanceUseCase.kt @@ -2,6 +2,7 @@ package com.solanamobile.ktxclientsample.usecase import android.content.SharedPreferences import com.solana.core.PublicKey +import java.lang.IllegalArgumentException import javax.inject.Inject sealed class WalletConnection @@ -18,6 +19,10 @@ class PersistanceUseCase @Inject constructor( private val sharedPreferences: SharedPreferences ) { + val connected: Connected + get() = getWalletConnection() as? Connected + ?: throw IllegalArgumentException("Only use this property when you are sure you have a valid connection.") + private var connection: WalletConnection = NotConnected fun getWalletConnection(): WalletConnection { @@ -49,16 +54,6 @@ class PersistanceUseCase @Inject constructor( connection = Connected(pubKey, accountLabel, token) } - fun clearConnection() { - sharedPreferences.edit().apply { - putString(PUBKEY_KEY, "") - putString(ACCOUNT_LABEL, "") - putString(AUTH_TOKEN_KEY, "") - }.apply() - - connection = NotConnected - } - companion object { const val PUBKEY_KEY = "stored_pubkey" const val ACCOUNT_LABEL = "stored_account_label" diff --git a/examples/example-clientlib-ktx-app/app/src/main/java/com/solanamobile/ktxclientsample/usecase/SolanaRpcUseCase.kt b/examples/example-clientlib-ktx-app/app/src/main/java/com/solanamobile/ktxclientsample/usecase/SolanaRpcUseCase.kt index 99a912974..3a1af6c63 100644 --- a/examples/example-clientlib-ktx-app/app/src/main/java/com/solanamobile/ktxclientsample/usecase/SolanaRpcUseCase.kt +++ b/examples/example-clientlib-ktx-app/app/src/main/java/com/solanamobile/ktxclientsample/usecase/SolanaRpcUseCase.kt @@ -10,12 +10,15 @@ import com.solana.core.PublicKey import com.solana.models.SignatureStatusRequestConfiguration import com.solana.networking.Commitment import com.solana.networking.HttpNetworkingRouter +import com.solana.networking.Network import com.solana.networking.RPCEndpoint +import com.solanamobile.ktxclientsample.BuildConfig import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext +import java.net.URL import javax.inject.Inject class SolanaRpcUseCase @Inject constructor() { @@ -23,7 +26,9 @@ class SolanaRpcUseCase @Inject constructor() { private val api: Api init { - val endPoint = RPCEndpoint.devnetSolana + val url = URL("https://devnet.helius-rpc.com/?api-key=${BuildConfig.HELIUS_KEY}") + + val endPoint = RPCEndpoint.custom(url, url, Network.devnet) val network = HttpNetworkingRouter(endPoint) api = Solana(network).api diff --git a/examples/example-clientlib-ktx-app/app/src/main/java/com/solanamobile/ktxclientsample/viewmodel/SampleViewModel.kt b/examples/example-clientlib-ktx-app/app/src/main/java/com/solanamobile/ktxclientsample/viewmodel/SampleViewModel.kt index 11351ff83..7b4395dd7 100644 --- a/examples/example-clientlib-ktx-app/app/src/main/java/com/solanamobile/ktxclientsample/viewmodel/SampleViewModel.kt +++ b/examples/example-clientlib-ktx-app/app/src/main/java/com/solanamobile/ktxclientsample/viewmodel/SampleViewModel.kt @@ -23,7 +23,6 @@ import javax.inject.Inject data class SampleViewState( val isLoading: Boolean = false, - val canTransact: Boolean = false, val solBalance: Double = 0.0, val userAddress: String = "", val userLabel: String = "", @@ -51,19 +50,11 @@ class SampleViewModel @Inject constructor( val persistedConn = persistanceUseCase.getWalletConnection() if (persistedConn is Connected) { - _state.value.copy( - isLoading = true, - canTransact = true, - userAddress = persistedConn.publicKey.toBase58(), - userLabel = persistedConn.accountLabel, - ).updateViewState() - viewModelScope.launch { - val balance = solanaRpcUseCase.getBalance(persistedConn.publicKey) - _state.value.copy( - isLoading = false, - solBalance = balance + userAddress = persistedConn.publicKey.toBase58(), + userLabel = persistedConn.accountLabel, + solBalance = solanaRpcUseCase.getBalance(persistedConn.publicKey) ).updateViewState() } @@ -73,128 +64,132 @@ class SampleViewModel @Inject constructor( fun addFunds(sender: ActivityResultSender) { viewModelScope.launch { - val conn = persistanceUseCase.getWalletConnection() + if (connectIfNeeded(sender)) { + val conn = persistanceUseCase.connected - if (conn is Connected) { requestAirdrop(conn.publicKey) - } else { - when (val result = walletAdapter.connect(sender)) { - is TransactionResult.Success -> { - val currentConn = Connected( - PublicKey(result.authResult.publicKey), - result.authResult.accountLabel ?: "", - result.authResult.authToken - ) - - val balance = solanaRpcUseCase.getBalance(currentConn.publicKey) - - persistanceUseCase.persistConnection(currentConn.publicKey, currentConn.accountLabel, currentConn.authToken) - - _state.value.copy( - isLoading = true, - solBalance = balance, - userAddress = currentConn.publicKey.toBase58(), - userLabel = currentConn.accountLabel - ).updateViewState() - - requestAirdrop(currentConn.publicKey) - } - - is TransactionResult.NoWalletFound -> { - _state.value.copy( - walletFound = false - ).updateViewState() - - } - - is TransactionResult.Failure -> { - _state.value.copy( - isLoading = false, - canTransact = false, - userAddress = "", - userLabel = "", - ).updateViewState() - } - } } } } - private suspend fun requestAirdrop(publicKey: PublicKey) { - try { - val tx = solanaRpcUseCase.requestAirdrop(publicKey) - val confirmed = solanaRpcUseCase.awaitConfirmationAsync(tx).await() + fun publishMemo(sender: ActivityResultSender, memoText: String) { + viewModelScope.launch { + if (connectIfNeeded(sender)) { + val conn = persistanceUseCase.connected - if (confirmed) { - _state.value.copy( - isLoading = false, - solBalance = solanaRpcUseCase.getBalance(publicKey) - ).updateViewState() - } else { _state.value.copy( - isLoading = false, + isLoading = true ).updateViewState() + + viewModelScope.launch { + val blockHash = solanaRpcUseCase.getLatestBlockHash() + + val tx = Transaction() + tx.add(MemoProgram.writeUtf8(conn.publicKey, memoText)) + tx.setRecentBlockHash(blockHash!!) + tx.feePayer = conn.publicKey + + val bytes = tx.serialize(SerializeConfig(requireAllSignatures = false)) + val result = walletAdapter.transact(sender) { + signAndSendTransactions(arrayOf(bytes)) + } + + when (result) { + is TransactionResult.Success -> { + val updatedAuth = result.authResult + //TODO: At some point in the future add a method to just persist + //just the auth token value as that is all we need in this case + persistanceUseCase.persistConnection( + PublicKey(updatedAuth.publicKey), + updatedAuth.accountLabel ?: "", + updatedAuth.authToken + ) + + val sig = result.payload.signatures.firstOrNull() + val readableSig = Base58.encode(sig) + + _state.value.copy( + isLoading = false, + memoTx = readableSig + ).updateViewState() + + //Clear out the recent transaction + delay(5000) + _state.value.copy(memoTx = "").updateViewState() + } + else -> { + _state.value.copy( + isLoading = false, + ).updateViewState() + } + } + } } - } catch (e: Throwable) { - _state.value.copy( - isLoading = false, - userAddress = "Error airdropping", - userLabel = "", - ).updateViewState() } } - fun publishMemo(sender: ActivityResultSender, memoText: String) { + private suspend fun connectIfNeeded(sender: ActivityResultSender): Boolean { val conn = persistanceUseCase.getWalletConnection() - if (conn is Connected) { - _state.value.copy( - isLoading = true - ).updateViewState() + return if (conn is Connected) { + true + } else { + when (val result = walletAdapter.connect(sender)) { + is TransactionResult.Success -> { + val currentConn = Connected( + PublicKey(result.authResult.publicKey), + result.authResult.accountLabel ?: "", + result.authResult.authToken + ) - viewModelScope.launch { - val blockHash = solanaRpcUseCase.getLatestBlockHash() + persistanceUseCase.persistConnection(currentConn.publicKey, currentConn.accountLabel, currentConn.authToken) - val tx = Transaction() - tx.add(MemoProgram.writeUtf8(conn.publicKey, memoText)) - tx.setRecentBlockHash(blockHash!!) - tx.feePayer = conn.publicKey + _state.value.copy( + userAddress = currentConn.publicKey.toBase58(), + userLabel = currentConn.accountLabel, + solBalance = solanaRpcUseCase.getBalance(currentConn.publicKey) + ).updateViewState() - val bytes = tx.serialize(SerializeConfig(requireAllSignatures = false)) - val result = walletAdapter.transact(sender) { - signAndSendTransactions(arrayOf(bytes)) + true } + is TransactionResult.NoWalletFound -> { + _state.value.copy( + walletFound = false + ).updateViewState() - (result as? TransactionResult.Success)?.let { txResult -> - val updatedAuth = txResult.authResult - //TODO: At some point in the future add a method to just persist - //just the auth token value as that is all we need in this case - persistanceUseCase.persistConnection(PublicKey(updatedAuth.publicKey), updatedAuth.accountLabel ?: "", updatedAuth.authToken) - - val sig = txResult.payload.signatures.firstOrNull() - val readableSig = Base58.encode(sig) - + false + } + is TransactionResult.Failure -> { _state.value.copy( - isLoading = false, - memoTx = readableSig + userAddress = "", + userLabel = "", ).updateViewState() - //Clear out the recent transaction - delay(5000) - _state.value.copy(memoTx = "").updateViewState() + false } } } + } - fun disconnect() { - viewModelScope.launch { - val conn = persistanceUseCase.getWalletConnection() - if (conn is Connected) { - persistanceUseCase.clearConnection() + private suspend fun requestAirdrop(publicKey: PublicKey) { + try { + val tx = solanaRpcUseCase.requestAirdrop(publicKey) + val confirmed = solanaRpcUseCase.awaitConfirmationAsync(tx).await() - SampleViewState().updateViewState() + if (confirmed) { + _state.value.copy( + isLoading = false, + solBalance = solanaRpcUseCase.getBalance(publicKey) + ).updateViewState() } + } catch (e: Throwable) { + _state.value.copy( + userAddress = "Error airdropping", + userLabel = "", + ).updateViewState() } + + _state.value.copy(isLoading = false).updateViewState() } } \ No newline at end of file