From fe5adc6fda8530858525110a213228be996fb975 Mon Sep 17 00:00:00 2001 From: Andrew Watson Date: Mon, 16 Oct 2023 12:42:34 -0700 Subject: [PATCH 1/6] Remove button, enable memo publish button pre-connection --- .../ktxclientsample/ui/SampleScreen.kt | 31 +++---------------- .../viewmodel/SampleViewModel.kt | 3 -- 2 files changed, 4 insertions(+), 30 deletions(-) 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/viewmodel/SampleViewModel.kt b/examples/example-clientlib-ktx-app/app/src/main/java/com/solanamobile/ktxclientsample/viewmodel/SampleViewModel.kt index 11351ff83..2c8fc50f1 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 = "", @@ -53,7 +52,6 @@ class SampleViewModel @Inject constructor( if (persistedConn is Connected) { _state.value.copy( isLoading = true, - canTransact = true, userAddress = persistedConn.publicKey.toBase58(), userLabel = persistedConn.accountLabel, ).updateViewState() @@ -110,7 +108,6 @@ class SampleViewModel @Inject constructor( is TransactionResult.Failure -> { _state.value.copy( isLoading = false, - canTransact = false, userAddress = "", userLabel = "", ).updateViewState() From 3c752e922fabf3535ad7f4a375715d8ccbd769b4 Mon Sep 17 00:00:00 2001 From: Andrew Watson Date: Mon, 16 Oct 2023 12:46:28 -0700 Subject: [PATCH 2/6] Remove disconnect method as we are not using anymore --- .../ktxclientsample/viewmodel/SampleViewModel.kt | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) 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 2c8fc50f1..d688de4d3 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 @@ -84,10 +84,10 @@ class SampleViewModel @Inject constructor( result.authResult.authToken ) - val balance = solanaRpcUseCase.getBalance(currentConn.publicKey) - persistanceUseCase.persistConnection(currentConn.publicKey, currentConn.accountLabel, currentConn.authToken) + val balance = solanaRpcUseCase.getBalance(currentConn.publicKey) + _state.value.copy( isLoading = true, solBalance = balance, @@ -183,15 +183,4 @@ class SampleViewModel @Inject constructor( } } } - - fun disconnect() { - viewModelScope.launch { - val conn = persistanceUseCase.getWalletConnection() - if (conn is Connected) { - persistanceUseCase.clearConnection() - - SampleViewState().updateViewState() - } - } - } } \ No newline at end of file From d819d09c361c97936898db31438add211aa47ae3 Mon Sep 17 00:00:00 2001 From: Andrew Watson Date: Mon, 16 Oct 2023 13:36:36 -0700 Subject: [PATCH 3/6] Move to unified connect-if-needed operation --- .../usecase/PersistanceUseCase.kt | 15 +- .../viewmodel/SampleViewModel.kt | 152 ++++++++++-------- 2 files changed, 87 insertions(+), 80 deletions(-) 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/viewmodel/SampleViewModel.kt b/examples/example-clientlib-ktx-app/app/src/main/java/com/solanamobile/ktxclientsample/viewmodel/SampleViewModel.kt index d688de4d3..3314a1d00 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 @@ -71,52 +71,107 @@ 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 - ) + } + } + } + + fun publishMemo(sender: ActivityResultSender, memoText: String) { + viewModelScope.launch { + if (connectIfNeeded(sender)) { + val conn = persistanceUseCase.connected - persistanceUseCase.persistConnection(currentConn.publicKey, currentConn.accountLabel, currentConn.authToken) + _state.value.copy( + isLoading = true + ).updateViewState() - val balance = solanaRpcUseCase.getBalance(currentConn.publicKey) + viewModelScope.launch { + val blockHash = solanaRpcUseCase.getLatestBlockHash() - _state.value.copy( - isLoading = true, - solBalance = balance, - userAddress = currentConn.publicKey.toBase58(), - userLabel = currentConn.accountLabel - ).updateViewState() + val tx = Transaction() + tx.add(MemoProgram.writeUtf8(conn.publicKey, memoText)) + tx.setRecentBlockHash(blockHash!!) + tx.feePayer = conn.publicKey - requestAirdrop(currentConn.publicKey) + val bytes = tx.serialize(SerializeConfig(requireAllSignatures = false)) + val result = walletAdapter.transact(sender) { + signAndSendTransactions(arrayOf(bytes)) } - 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) - is TransactionResult.Failure -> { _state.value.copy( isLoading = false, - userAddress = "", - userLabel = "", + memoTx = readableSig ).updateViewState() + + //Clear out the recent transaction + delay(5000) + _state.value.copy(memoTx = "").updateViewState() } } } } } + private suspend fun connectIfNeeded(sender: ActivityResultSender): Boolean { + val conn = persistanceUseCase.getWalletConnection() + + 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 + ) + + persistanceUseCase.persistConnection(currentConn.publicKey, currentConn.accountLabel, currentConn.authToken) + + _state.value.copy( + isLoading = false, + solBalance = solanaRpcUseCase.getBalance(currentConn.publicKey) + ).updateViewState() + + true + } + is TransactionResult.NoWalletFound -> { + _state.value.copy( + walletFound = false + ).updateViewState() + + false + } + is TransactionResult.Failure -> { + _state.value.copy( + isLoading = false, + userAddress = "", + userLabel = "", + ).updateViewState() + + false + } + } + } + + } + private suspend fun requestAirdrop(publicKey: PublicKey) { try { val tx = solanaRpcUseCase.requestAirdrop(publicKey) @@ -140,47 +195,4 @@ class SampleViewModel @Inject constructor( ).updateViewState() } } - - fun publishMemo(sender: ActivityResultSender, memoText: String) { - val conn = persistanceUseCase.getWalletConnection() - - if (conn is Connected) { - _state.value.copy( - 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)) - } - - (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) - - _state.value.copy( - isLoading = false, - memoTx = readableSig - ).updateViewState() - - //Clear out the recent transaction - delay(5000) - _state.value.copy(memoTx = "").updateViewState() - } - } - } - } } \ No newline at end of file From fe491afcf8cc7a95ab116040b0b7ff8d1a31f332 Mon Sep 17 00:00:00 2001 From: Andrew Watson Date: Mon, 16 Oct 2023 13:47:57 -0700 Subject: [PATCH 4/6] Consume new api endpoint to make the rpc calls wrok --- examples/example-clientlib-ktx-app/app/build.gradle | 5 +++++ .../ktxclientsample/usecase/SolanaRpcUseCase.kt | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/examples/example-clientlib-ktx-app/app/build.gradle b/examples/example-clientlib-ktx-app/app/build.gradle index e7da017af..b71b28f90 100644 --- a/examples/example-clientlib-ktx-app/app/build.gradle +++ b/examples/example-clientlib-ktx-app/app/build.gradle @@ -19,6 +19,11 @@ android { vectorDrawables { useSupportLibrary true } + + Properties properties = new Properties() + properties.load(project.rootProject.file("local.properties").newDataInputStream()) + + buildConfigField "String", "HELIUS_KEY", "\"${properties.getProperty("HELIUS_KEY")}\"" } buildTypes { 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 From f1d924555de04aa376efd4e988ec7645ff5e6096 Mon Sep 17 00:00:00 2001 From: Andrew Watson Date: Mon, 16 Oct 2023 14:16:46 -0700 Subject: [PATCH 5/6] Finish up new UX flow --- .../viewmodel/SampleViewModel.kt | 73 +++++++++---------- 1 file changed, 35 insertions(+), 38 deletions(-) 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 3314a1d00..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 @@ -50,18 +50,11 @@ class SampleViewModel @Inject constructor( val persistedConn = persistanceUseCase.getWalletConnection() if (persistedConn is Connected) { - _state.value.copy( - isLoading = 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() } @@ -101,27 +94,34 @@ class SampleViewModel @Inject constructor( signAndSendTransactions(arrayOf(bytes)) } - (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) - - _state.value.copy( - isLoading = false, - memoTx = readableSig - ).updateViewState() - - //Clear out the recent transaction - delay(5000) - _state.value.copy(memoTx = "").updateViewState() + 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() + } } } } @@ -145,7 +145,8 @@ class SampleViewModel @Inject constructor( persistanceUseCase.persistConnection(currentConn.publicKey, currentConn.accountLabel, currentConn.authToken) _state.value.copy( - isLoading = false, + userAddress = currentConn.publicKey.toBase58(), + userLabel = currentConn.accountLabel, solBalance = solanaRpcUseCase.getBalance(currentConn.publicKey) ).updateViewState() @@ -160,7 +161,6 @@ class SampleViewModel @Inject constructor( } is TransactionResult.Failure -> { _state.value.copy( - isLoading = false, userAddress = "", userLabel = "", ).updateViewState() @@ -182,17 +182,14 @@ class SampleViewModel @Inject constructor( isLoading = false, solBalance = solanaRpcUseCase.getBalance(publicKey) ).updateViewState() - } else { - _state.value.copy( - isLoading = false, - ).updateViewState() } } catch (e: Throwable) { _state.value.copy( - isLoading = false, userAddress = "Error airdropping", userLabel = "", ).updateViewState() } + + _state.value.copy(isLoading = false).updateViewState() } } \ No newline at end of file From 34ed1279d830bf6f4e69eebf72f4ae9c32299711 Mon Sep 17 00:00:00 2001 From: Andrew Watson Date: Mon, 16 Oct 2023 14:24:24 -0700 Subject: [PATCH 6/6] Fix local properties file issue --- examples/example-clientlib-ktx-app/app/build.gradle | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/example-clientlib-ktx-app/app/build.gradle b/examples/example-clientlib-ktx-app/app/build.gradle index b71b28f90..972cbf266 100644 --- a/examples/example-clientlib-ktx-app/app/build.gradle +++ b/examples/example-clientlib-ktx-app/app/build.gradle @@ -21,9 +21,14 @@ android { } Properties properties = new Properties() - properties.load(project.rootProject.file("local.properties").newDataInputStream()) + def propertiesFile = project.rootProject.file('local.properties') + if (propertiesFile.exists()) { + properties.load(propertiesFile.newDataInputStream()) - buildConfigField "String", "HELIUS_KEY", "\"${properties.getProperty("HELIUS_KEY")}\"" + buildConfigField "String", "HELIUS_KEY", "\"${properties.getProperty("HELIUS_KEY")}\"" + } else { + buildConfigField "String", "HELIUS_KEY", "\"\"" + } } buildTypes {