Skip to content

Commit

Permalink
Upgrade MWA ktx API (#511)
Browse files Browse the repository at this point in the history
* Move things to a new models file

* Add functionality that utilizes provided credentials

* Try passing back nullable auth result object, but this seems to be falling short

* Getting closer

* Update authresult to be a getter with intential exception catching. Add creds providing sample code in sample app.

* Start to implement special case "connect" method in mwa

* Fix using wrong property for public key

* Standup an attempt at testing the MWA ktx class

* Get initial test completing by using appropriate mocks

* Add more nice test stuff

* Get more things ready for proper API and testing

* Get a lot more unit tests in place

* Finish up initial set of MWA tests

* Some small UX rearrangements to make things a little more clear.

* Some more cleanup

* Some final small improvementws
  • Loading branch information
creativedrewy authored Jul 28, 2023
1 parent 2b355a2 commit 1f30932
Show file tree
Hide file tree
Showing 9 changed files with 574 additions and 198 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# macOS finder
.DS_Store
.idea
8 changes: 8 additions & 0 deletions android/clientlib-ktx/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,16 @@ dependencies {
api(project(":clientlib"))
implementation 'androidx.core:core-ktx:1.9.0'
implementation "androidx.activity:activity-ktx:1.6.1"
implementation 'androidx.test.ext:junit-ktx:1.1.5'

testImplementation 'junit:junit:4.13.2'
testImplementation 'androidx.test:core:1.5.0'
testImplementation 'androidx.test:rules:1.5.0'
testImplementation 'org.jetbrains.kotlin:kotlin-test:1.8.21'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.2'
testImplementation 'org.robolectric:robolectric:4.10.3'
implementation 'org.mockito.kotlin:mockito-kotlin:4.1.0'
implementation 'org.mockito:mockito-inline:5.1.1'

androidTestImplementation 'androidx.test.ext:junit:1.1.4'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.solana.mobilewalletadapter.clientlib

import com.solana.mobilewalletadapter.clientlib.scenario.LocalAssociationScenario

class AssociationScenarioProvider {

fun provideAssociationScenario(timeoutMs: Int): LocalAssociationScenario {
return LocalAssociationScenario(timeoutMs)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.solana.mobilewalletadapter.clientlib

import android.net.Uri
import com.solana.mobilewalletadapter.clientlib.protocol.MobileWalletAdapterClient.AuthorizationResult

sealed class CredentialState {
data class Provided(
val credentials: ConnectionCredentials
): CredentialState()

object NotProvided: CredentialState()
}

data class ConnectionCredentials(
val identityUri: Uri,
val iconUri: Uri,
val identityName: String,
val rpcCluster: RpcCluster = RpcCluster.Devnet,
val authToken: String? = null
)

/**
* Convenience property to access success payload. Will be null if not successful.
*/
val <T> TransactionResult<T>.successPayload: T?
get() = (this as? TransactionResult.Success)?.payload

sealed class TransactionResult<T> {
class Success<T>(
val payload: T,
private val result: AuthorizationResult? = null
): TransactionResult<T>() {

val authResult: AuthorizationResult
get() = try {
result!!
} catch (e: NullPointerException) {
throw IllegalStateException("Auth result accessor is only available when connections credentials have been provided prior to authorize/reauthorize.")
}
}

class Failure<T>(
val message: String,
val e: Exception
): TransactionResult<T>()

class NoWalletFound<T>(
val message: String
): TransactionResult<T>()
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import android.content.ActivityNotFoundException
import com.solana.mobilewalletadapter.clientlib.protocol.JsonRpc20Client
import com.solana.mobilewalletadapter.clientlib.protocol.MobileWalletAdapterClient
import com.solana.mobilewalletadapter.clientlib.scenario.LocalAssociationIntentCreator
import com.solana.mobilewalletadapter.clientlib.scenario.LocalAssociationScenario
import com.solana.mobilewalletadapter.clientlib.scenario.Scenario
import com.solana.mobilewalletadapter.common.ProtocolContract
import kotlinx.coroutines.*
Expand All @@ -15,47 +14,42 @@ import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException

sealed class TransactionResult<T> {
data class Success<T>(
val payload: T
): TransactionResult<T>()

class Failure<T>(
val message: String,
val e: Exception
): TransactionResult<T>()

class NoWalletFound<T>(
val message: String
): TransactionResult<T>()
}

/**
* Convenience property to access success payload. Will be null if not successful.
*/
val <T> TransactionResult<T>.successPayload: T?
get() = (this as? TransactionResult.Success)?.payload

class MobileWalletAdapter(
private val timeout: Int = Scenario.DEFAULT_CLIENT_TIMEOUT_MS,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
private val scenarioProvider: AssociationScenarioProvider = AssociationScenarioProvider()
) {

private var credsState: CredentialState = CredentialState.NotProvided

private val adapterOperations = LocalAdapterOperations(ioDispatcher)

fun provideCredentials(credentials: ConnectionCredentials) {
credsState = CredentialState.Provided(credentials)
}

suspend fun connect(sender: ActivityResultSender): TransactionResult<Unit> {
return transact(sender) {
if (credsState is CredentialState.NotProvided) {
throw IllegalStateException("App credentials must be provided prior to utilizing the connect method.")
}
}
}

suspend fun <T> transact(
sender: ActivityResultSender,
block: suspend AdapterOperations.() -> T,
): TransactionResult<T> = coroutineScope {
return@coroutineScope try {
val scenario = LocalAssociationScenario(timeout)
val scenario = scenarioProvider.provideAssociationScenario(timeout)
val details = scenario.associationDetails()

val intent = LocalAssociationIntentCreator.createAssociationIntent(
details.uriPrefix,
details.port,
details.session
)

try {
withTimeout(ASSOCIATION_SEND_INTENT_TIMEOUT_MS) {
sender.startActivityForResult(intent) {
Expand All @@ -80,11 +74,25 @@ class MobileWalletAdapter(
try {
@Suppress("BlockingMethodInNonBlockingContext")
val client = scenario.start().get(ASSOCIATION_CONNECT_DISCONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)

adapterOperations.client = client

val authResult = credsState.let { creds ->
if (creds is CredentialState.Provided) {
with (creds.credentials) {
if (authToken == null) {
adapterOperations.authorize(identityUri, iconUri, identityName, rpcCluster)
} else {
adapterOperations.reauthorize(identityUri, iconUri, identityName, authToken)
}
}
} else {
null
}
}

val result = block(adapterOperations)

TransactionResult.Success(result)
TransactionResult.Success(result, authResult)
} catch (e: InterruptedException) {
TransactionResult.Failure("Interrupted while waiting for local association to be ready", e)
} catch (e: TimeoutException) {
Expand Down Expand Up @@ -135,6 +143,8 @@ class MobileWalletAdapter(
return@coroutineScope TransactionResult.Failure("Request was interrupted", e)
} catch (e: ActivityNotFoundException) {
return@coroutineScope TransactionResult.NoWalletFound("No compatible wallet found.")
} catch (e: java.lang.IllegalStateException) {
return@coroutineScope TransactionResult.Failure(e.message.toString(), e)
}
}

Expand Down
Loading

0 comments on commit 1f30932

Please sign in to comment.