diff --git a/android/fakedapp/src/main/java/com/solana/mobilewalletadapter/fakedapp/MainActivity.kt b/android/fakedapp/src/main/java/com/solana/mobilewalletadapter/fakedapp/MainActivity.kt index c79c2d061..905f622a9 100644 --- a/android/fakedapp/src/main/java/com/solana/mobilewalletadapter/fakedapp/MainActivity.kt +++ b/android/fakedapp/src/main/java/com/solana/mobilewalletadapter/fakedapp/MainActivity.kt @@ -15,7 +15,9 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar +import com.solana.mobilewalletadapter.common.protocol.SessionProperties import com.solana.mobilewalletadapter.fakedapp.databinding.ActivityMainBinding +import com.solana.mobilewalletadapter.fakedapp.usecase.Base58EncodeUseCase import com.solana.mobilewalletadapter.fakedapp.usecase.MemoTransactionVersion import com.solana.mobilewalletadapter.fakedapp.usecase.MobileWalletAdapterUseCase.StartMobileWalletAdapterActivity import kotlinx.coroutines.launch @@ -54,11 +56,22 @@ class MainActivity : AppCompatActivity() { if (spinnerPos > 0) viewBinding.spinnerTxnVer.setSelection(spinnerPos) } - viewBinding.tvAccountName.text = - uiState.accountLabel ?: getString(R.string.string_no_account_name) + viewBinding.spinnerAccounts.adapter = + ArrayAdapter(this@MainActivity, android.R.layout.simple_spinner_item, + uiState.accounts?.map { account -> + account.accountLabel ?: Base58EncodeUseCase.invoke(account.publicKey) + } ?: listOf() + ) + viewBinding.tvWalletUriPrefix.text = - uiState.walletUriBase?.toString() - ?: getString(R.string.string_no_wallet_uri_prefix) + uiState.walletUriBase?.toString() ?: getString(R.string.string_no_wallet_uri_prefix) + viewBinding.tvSessionVersion.text = + getString(uiState.sessionProtocolVersion?.let { + when (it) { + SessionProperties.ProtocolVersion.LEGACY -> R.string.string_session_version_legacy + SessionProperties.ProtocolVersion.V1 -> R.string.string_session_version_v1 + } + } ?: R.string.string_no_session_version) if (uiState.messages.isNotEmpty()) { val message = uiState.messages.first() diff --git a/android/fakedapp/src/main/java/com/solana/mobilewalletadapter/fakedapp/MainViewModel.kt b/android/fakedapp/src/main/java/com/solana/mobilewalletadapter/fakedapp/MainViewModel.kt index 1fc4247ea..e16e6b12f 100644 --- a/android/fakedapp/src/main/java/com/solana/mobilewalletadapter/fakedapp/MainViewModel.kt +++ b/android/fakedapp/src/main/java/com/solana/mobilewalletadapter/fakedapp/MainViewModel.kt @@ -12,9 +12,11 @@ import androidx.annotation.StringRes import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.solana.mobilewalletadapter.clientlib.protocol.MobileWalletAdapterClient +import com.solana.mobilewalletadapter.clientlib.protocol.MobileWalletAdapterClient.AuthorizationResult.AuthorizedAccount import com.solana.mobilewalletadapter.clientlib.scenario.LocalAssociationIntentCreator import com.solana.mobilewalletadapter.clientlib.transaction.TransactionVersion import com.solana.mobilewalletadapter.common.ProtocolContract +import com.solana.mobilewalletadapter.common.protocol.SessionProperties.ProtocolVersion import com.solana.mobilewalletadapter.fakedapp.usecase.* import com.solana.mobilewalletadapter.fakedapp.usecase.MobileWalletAdapterUseCase.StartMobileWalletAdapterActivity import kotlinx.coroutines.* @@ -111,6 +113,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { Log.d(TAG, "Capabilities: $it") Log.d(TAG, "Supports legacy transactions: ${TransactionVersion.supportsLegacy(it.supportedTransactionVersions)}") Log.d(TAG, "Supports v0 transactions: ${TransactionVersion.supportsVersion(it.supportedTransactionVersions, 0)}") + Log.d(TAG, "Supported features: ${it.supportedOptionalFeatures.contentToString()}") showMessage(R.string.msg_request_succeeded) } } catch (e: MobileWalletAdapterUseCase.LocalAssociationFailedException) { @@ -124,7 +127,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { fun requestAirdrop() = viewModelScope.launch { try { - RequestAirdropUseCase(CLUSTER_RPC_URI, _uiState.value.publicKey!!) + RequestAirdropUseCase(CLUSTER_RPC_URI, _uiState.value.primaryPublicKey!!) Log.d(TAG, "Airdrop request sent") showMessage(R.string.msg_airdrop_request_sent) } catch (e: RequestAirdropUseCase.AirdropFailedException) { @@ -152,7 +155,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } val (blockhash, _) = latestBlockhash.await() val transactions = Array(numTransactions) { - transactionUseCase.create(uiState.value.publicKey!!, blockhash) + transactionUseCase.create(uiState.value.primaryPublicKey!!, blockhash) } client.signTransactions(transactions).also { Log.d(TAG, "Signed transaction(s): $it") @@ -174,7 +177,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { val verified = signedTransactions.map { txn -> try { - transactionUseCase.verify(uiState.value.publicKey!!, txn) + transactionUseCase.verify(uiState.value.primaryPublicKey!!, txn) true } catch (e: IllegalArgumentException) { Log.e(TAG, "Memo transaction signature verification failed", e) @@ -198,7 +201,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } val (blockhash, _) = latestBlockhash.await() val transactions = arrayOf( - transactionUseCase.create(uiState.value.publicKey!!, blockhash) + transactionUseCase.create(uiState.value.primaryPublicKey!!, blockhash) ) client.signTransactions(transactions).also { Log.d(TAG, "Signed transaction(s): $it") @@ -220,7 +223,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { val verified = signedTransactions.map { txn -> try { - transactionUseCase.verify(uiState.value.publicKey!!, txn) + transactionUseCase.verify(uiState.value.primaryPublicKey!!, txn) true } catch (e: IllegalArgumentException) { Log.e(TAG, "Memo transaction signature verification failed", e) @@ -247,10 +250,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { doAuthorize(client, IDENTITY, CLUSTER_NAME) message = - "Sign this message to prove you own account ${Base58EncodeUseCase(uiState.value.publicKey!!)}".encodeToByteArray() + "Sign this message to prove you own account ${Base58EncodeUseCase(uiState.value.primaryPublicKey!!)}".encodeToByteArray() val signMessagesResult = client.signMessagesDetached( arrayOf(message), - arrayOf(uiState.value.publicKey!!) + arrayOf(uiState.value.primaryPublicKey!!) ) Log.d(TAG, "Simulating a short delay while we do something with the message the user just signed...") @@ -259,7 +262,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { val (blockhash, slot) = latestBlockhash.await() val transaction = - arrayOf(transactionUseCase.create(uiState.value.publicKey!!, blockhash)) + arrayOf(transactionUseCase.create(uiState.value.primaryPublicKey!!, blockhash)) val signAndSendTransactionsResult = client.signAndSendTransactions(transaction, slot) signMessagesResult[0] to signAndSendTransactionsResult[0] @@ -282,7 +285,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { OffChainMessageSigningUseCase.verify( signedMessage.message, signedMessage.signatures[0], - uiState.value.publicKey!!, + uiState.value.primaryPublicKey!!, message ) true @@ -315,7 +318,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { doReauthorize(client, IDENTITY, _uiState.value.authToken!!).also { Log.d(TAG, "Reauthorized: $it") } - client.signMessagesDetached(messages, arrayOf(_uiState.value.publicKey!!)).also { + client.signMessagesDetached(messages, arrayOf(_uiState.value.primaryPublicKey!!)).also { Log.d(TAG, "Signed message(s): $it") } } @@ -335,7 +338,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { OffChainMessageSigningUseCase.verify( sm.first.message, sm.first.signatures[0], - _uiState.value.publicKey!!, + _uiState.value.primaryPublicKey!!, sm.second ) } @@ -361,7 +364,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } val (blockhash, slot) = latestBlockhash.await() val transactions = Array(numTransactions) { - transactionUseCase.create(uiState.value.publicKey!!, blockhash) + transactionUseCase.create(uiState.value.primaryPublicKey!!, blockhash) } client.signAndSendTransactions(transactions, slot).also { Log.d(TAG, "Transaction signature(s): $it") @@ -406,8 +409,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { _uiState.update { it.copy( authToken = null, - publicKey = null, - accountLabel = null, + accounts = null, walletUriBase = null ) } @@ -417,8 +419,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { _uiState.update { it.copy( authToken = result.authToken, - publicKey = result.publicKey, - accountLabel = result.accountLabel, + accounts = result.accounts.asList(), walletUriBase = result.walletUriBase ) } @@ -437,8 +438,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { _uiState.update { it.copy( authToken = null, - publicKey = null, - accountLabel = null, + accounts = null, walletUriBase = null ) } @@ -448,8 +448,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { _uiState.update { it.copy( authToken = result.authToken, - publicKey = result.publicKey, - accountLabel = result.accountLabel, + accounts = result.accounts.asList(), walletUriBase = result.walletUriBase ) } @@ -467,8 +466,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { _uiState.update { it.copy( authToken = null, - publicKey = null, - accountLabel = null, + accounts = null, walletUriBase = null ) } @@ -481,7 +479,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { action: suspend (MobileWalletAdapterUseCase.Client) -> T ): T { return try { - MobileWalletAdapterUseCase.localAssociateAndExecute(intentLauncher, uriPrefix, action) + MobileWalletAdapterUseCase.localAssociateAndExecute(intentLauncher, uriPrefix) { client, sessionProperties -> + _uiState.update { + it.copy(sessionProtocolVersion = sessionProperties.protocolVersion) + } + action(client) + } } catch (e: MobileWalletAdapterUseCase.NoWalletAvailableException) { showMessage(R.string.msg_no_wallet_found) throw e @@ -490,13 +493,14 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { data class UiState( val authToken: String? = null, - val publicKey: ByteArray? = null, // TODO(#44): support multiple addresses - val accountLabel: String? = null, + val accounts: List? = null, val walletUriBase: Uri? = null, val messages: List = emptyList(), - val txnVersion: MemoTransactionVersion = MemoTransactionVersion.Legacy + val txnVersion: MemoTransactionVersion = MemoTransactionVersion.Legacy, + val sessionProtocolVersion: ProtocolVersion? = null ) { val hasAuthToken: Boolean get() = (authToken != null) + val primaryPublicKey: ByteArray? get() = (accounts?.first()?.publicKey) override fun equals(other: Any?): Boolean { if (this === other) return true @@ -505,23 +509,24 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { other as UiState if (authToken != other.authToken) return false - if (publicKey != null) { - if (other.publicKey == null) return false - if (!publicKey.contentEquals(other.publicKey)) return false - } else if (other.publicKey != null) return false + if (accounts != null && accounts.size == other.accounts?.size) { + accounts.zip(other.accounts).all { (a1, a2) -> a1.publicKey.contentEquals(a2.publicKey) } + } else if (other.accounts != null) return false if (walletUriBase != other.walletUriBase) return false if (messages != other.messages) return false if (txnVersion != other.txnVersion) return false + if (sessionProtocolVersion != other.sessionProtocolVersion) return false return true } override fun hashCode(): Int { var result = authToken?.hashCode() ?: 0 - result = 31 * result + (publicKey?.contentHashCode() ?: 0) + result = 31 * result + (accounts?.hashCode() ?: 0) result = 31 * result + (walletUriBase?.hashCode() ?: 0) result = 31 * result + messages.hashCode() result = 31 * result + txnVersion.hashCode() + result = 31 * result + (sessionProtocolVersion?.hashCode() ?: 0) return result } } diff --git a/android/fakedapp/src/main/java/com/solana/mobilewalletadapter/fakedapp/usecase/MobileWalletAdapterUseCase.kt b/android/fakedapp/src/main/java/com/solana/mobilewalletadapter/fakedapp/usecase/MobileWalletAdapterUseCase.kt index 80dc642c6..d9ce27660 100644 --- a/android/fakedapp/src/main/java/com/solana/mobilewalletadapter/fakedapp/usecase/MobileWalletAdapterUseCase.kt +++ b/android/fakedapp/src/main/java/com/solana/mobilewalletadapter/fakedapp/usecase/MobileWalletAdapterUseCase.kt @@ -21,6 +21,7 @@ import com.solana.mobilewalletadapter.clientlib.scenario.LocalAssociationIntentC import com.solana.mobilewalletadapter.clientlib.scenario.LocalAssociationScenario import com.solana.mobilewalletadapter.clientlib.scenario.Scenario import com.solana.mobilewalletadapter.common.ProtocolContract +import com.solana.mobilewalletadapter.common.protocol.SessionProperties import kotlinx.coroutines.* import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit @@ -317,13 +318,13 @@ object MobileWalletAdapterUseCase { suspend fun localAssociateAndExecute( intentLauncher: ActivityResultLauncher, uriPrefix: Uri? = null, - action: suspend (Client) -> T + action: suspend (Client, SessionProperties) -> T ): T = localAssociateAndExecuteAsync(intentLauncher, uriPrefix, action).await() suspend fun localAssociateAndExecuteAsync( intentLauncher: ActivityResultLauncher, uriPrefix: Uri? = null, - action: suspend (Client) -> T + action: suspend (Client, SessionProperties) -> T ): Deferred = coroutineScope { // Use async to launch in a new Job, for proper cancellation semantics async { @@ -373,7 +374,8 @@ object MobileWalletAdapterUseCase { contract.onMobileWalletAdapterClientConnected(this) - action(Client(mobileWalletAdapterClient)) + action(Client(mobileWalletAdapterClient), + localAssociation.session.sessionProperties) } finally { @Suppress("BlockingMethodInNonBlockingContext") // running in Dispatchers.IO; blocking is appropriate localAssociation.close() diff --git a/android/fakedapp/src/main/res/layout/activity_main.xml b/android/fakedapp/src/main/res/layout/activity_main.xml index 7de139f05..f36567506 100644 --- a/android/fakedapp/src/main/res/layout/activity_main.xml +++ b/android/fakedapp/src/main/res/layout/activity_main.xml @@ -187,26 +187,25 @@ android:enabled="false" /> - + app:layout_constraintStart_toEndOf="@id/label_accounts" + app:layout_constraintEnd_toEndOf="parent" /> + + + + diff --git a/android/fakedapp/src/main/res/values/strings.xml b/android/fakedapp/src/main/res/values/strings.xml index 33d13cc28..c8473e04e 100644 --- a/android/fakedapp/src/main/res/values/strings.xml +++ b/android/fakedapp/src/main/res/values/strings.xml @@ -33,8 +33,11 @@ x3 x20 Has auth token? - Account name: - <none> + Accounts: Wallet URI prefix: <none> + Protocol Version: + <none> + Legacy + MWA 2.0 \ No newline at end of file diff --git a/android/fakewallet/build.gradle b/android/fakewallet/build.gradle index 64fff4a3b..cbf13ebba 100644 --- a/android/fakewallet/build.gradle +++ b/android/fakewallet/build.gradle @@ -60,13 +60,11 @@ android { productFlavors { v1 { dimension "protocol_version" - applicationId "com.solana.mobilewalletadapter.fakewallet" buildConfigField "com.solana.mobilewalletadapter.common.protocol.SessionProperties.ProtocolVersion", "PROTOCOL_VERSION", "com.solana.mobilewalletadapter.common.protocol.SessionProperties.ProtocolVersion.V1" } legacy { dimension "protocol_version" - applicationId "com.solana.mobilewalletadapter.fakewallet.legacy" resValue "string", "app_name", "Fake Wallet App (Legacy)" buildConfigField "com.solana.mobilewalletadapter.common.protocol.SessionProperties.ProtocolVersion", "PROTOCOL_VERSION", "com.solana.mobilewalletadapter.common.protocol.SessionProperties.ProtocolVersion.LEGACY" diff --git a/android/fakewallet/src/main/java/com/solana/mobilewalletadapter/fakewallet/MobileWalletAdapterViewModel.kt b/android/fakewallet/src/main/java/com/solana/mobilewalletadapter/fakewallet/MobileWalletAdapterViewModel.kt index f791fb8e5..fa7fda228 100644 --- a/android/fakewallet/src/main/java/com/solana/mobilewalletadapter/fakewallet/MobileWalletAdapterViewModel.kt +++ b/android/fakewallet/src/main/java/com/solana/mobilewalletadapter/fakewallet/MobileWalletAdapterViewModel.kt @@ -58,30 +58,34 @@ class MobileWalletAdapterViewModel(application: Application) : AndroidViewModel( associationUri ) - val config = MobileWalletAdapterConfig( - 10, - 10, - arrayOf(MobileWalletAdapterConfig.LEGACY_TRANSACTION_VERSION, 0), - LOW_POWER_NO_CONNECTION_TIMEOUT_MS, - arrayOf(ProtocolContract.FEATURE_ID_SIGN_TRANSACTIONS) - ) - scenario = if (BuildConfig.PROTOCOL_VERSION == SessionProperties.ProtocolVersion.LEGACY) { // manually create the scenario here so we can override the association protocol version // this forces ProtocolVersion.LEGACY to simulate a wallet using walletlib 1.x (for testing) LocalWebSocketServerScenario( getApplication().applicationContext, - config, + MobileWalletAdapterConfig( + true, + 10, + 10, + arrayOf(MobileWalletAdapterConfig.LEGACY_TRANSACTION_VERSION, 0), + LOW_POWER_NO_CONNECTION_TIMEOUT_MS + ), AuthIssuerConfig("fakewallet"), MobileWalletAdapterScenarioCallbacks(), associationUri.associationPublicKey, - listOf(SessionProperties.ProtocolVersion.LEGACY), + listOf(), associationUri.port, ) } else { associationUri.createScenario( getApplication().applicationContext, - config, + MobileWalletAdapterConfig( + 10, + 10, + arrayOf(MobileWalletAdapterConfig.LEGACY_TRANSACTION_VERSION, 0), + LOW_POWER_NO_CONNECTION_TIMEOUT_MS, + arrayOf(ProtocolContract.FEATURE_ID_SIGN_TRANSACTIONS) + ), AuthIssuerConfig("fakewallet"), MobileWalletAdapterScenarioCallbacks() ) diff --git a/android/walletlib/src/main/java/com/solana/mobilewalletadapter/walletlib/protocol/MobileWalletAdapterConfig.java b/android/walletlib/src/main/java/com/solana/mobilewalletadapter/walletlib/protocol/MobileWalletAdapterConfig.java index c1f68d4af..2f903980c 100644 --- a/android/walletlib/src/main/java/com/solana/mobilewalletadapter/walletlib/protocol/MobileWalletAdapterConfig.java +++ b/android/walletlib/src/main/java/com/solana/mobilewalletadapter/walletlib/protocol/MobileWalletAdapterConfig.java @@ -45,8 +45,7 @@ public MobileWalletAdapterConfig(boolean supportsSignAndSendTransactions, @NonNull @Size(min = 1) Object[] supportedTransactionVersions, @IntRange(from = 0) long noConnectionWarningTimeoutMs) { this(maxTransactionsPerSigningRequest, maxMessagesPerSigningRequest, - supportedTransactionVersions, noConnectionWarningTimeoutMs, - new String[] { ProtocolContract.FEATURE_ID_SIGN_TRANSACTIONS }); + supportedTransactionVersions, noConnectionWarningTimeoutMs, new String[] {}); if (!supportsSignAndSendTransactions) throw new IllegalArgumentException("signAndSendTransactions is required in MWA 2.0"); }