Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,19 @@ public class AssociationContract {

public static final String PARAMETER_PROTOCOL_VERSION = "v";

public static final String PARAMETER_REFLECTOR_HOST_AUTHORITY = "reflector"; // type: String
public static final String PARAMETER_REFLECTOR_ID = "id"; // type: Long

public static final String LOCAL_PATH_SUFFIX = "v1/associate/local";
public static final String LOCAL_REFLECTOR_PATH_SUFFIX = "v1/associate/local/reflector";
public static final String LOCAL_PARAMETER_PORT = "port"; // type: Int

public static final String REMOTE_PATH_SUFFIX = "v1/associate/remote";
public static final String REMOTE_PARAMETER_REFLECTOR_HOST_AUTHORITY = "reflector"; // type: String
public static final String REMOTE_PARAMETER_REFLECTOR_ID = "id"; // type: Long

@Deprecated
public static final String REMOTE_PARAMETER_REFLECTOR_HOST_AUTHORITY = PARAMETER_REFLECTOR_HOST_AUTHORITY;
@Deprecated
public static final String REMOTE_PARAMETER_REFLECTOR_ID = PARAMETER_REFLECTOR_ID;

private AssociationContract() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
package com.solana.mobilewalletadapter.clientlib.scenario;

import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.util.Base64;

Expand All @@ -19,40 +17,33 @@

import java.util.Set;

public class RemoteAssociationIntentCreator {
public class ReflectorAssociationIntentCreator {

private RemoteAssociationIntentCreator() { }
private ReflectorAssociationIntentCreator() { }

@NonNull
public static Intent createAssociationIntent(@Nullable Uri endpointPrefix,
@NonNull String hostAuthority,
@NonNull byte[] reflectorId,
@NonNull MobileWalletAdapterSession session) {
@NonNull MobileWalletAdapterSession session,
@NonNull boolean isRemote) {
final byte[] associationPublicKey = session.getEncodedAssociationPublicKey();
final String associationToken = Base64.encodeToString(associationPublicKey,
Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP);
return new Intent()
.setAction(Intent.ACTION_VIEW)
.addCategory(Intent.CATEGORY_BROWSABLE)
.setData(createAssociationUri(endpointPrefix, hostAuthority, reflectorId,
associationToken, session.getSupportedProtocolVersions()));
}

public static boolean isWalletEndpointAvailable(@NonNull PackageManager pm) {
final Intent intent = new Intent()
.setAction(Intent.ACTION_VIEW)
.addCategory(Intent.CATEGORY_BROWSABLE)
.setData(createAssociationUri(null, "", new byte[1], "", Set.of()));
final ResolveInfo resolveInfo = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
return (resolveInfo != null);
associationToken, session.getSupportedProtocolVersions(), isRemote));
}

@NonNull
private static Uri createAssociationUri(@Nullable Uri endpointPrefix,
@NonNull String hostAuthority,
@NonNull byte[] reflectorId,
@NonNull String associationToken,
@NonNull Set<SessionProperties.ProtocolVersion> supportedProtocolVersions) {
@NonNull Set<SessionProperties.ProtocolVersion> supportedProtocolVersions,
boolean isRemote) {
if (endpointPrefix != null && (!"https".equals(endpointPrefix.getScheme()) || !endpointPrefix.isHierarchical())) {
throw new IllegalArgumentException("Endpoint-specific URI prefix must be absolute with scheme 'https' and hierarchical");
}
Expand All @@ -68,12 +59,12 @@ private static Uri createAssociationUri(@Nullable Uri endpointPrefix,
.scheme(AssociationContract.SCHEME_MOBILE_WALLET_ADAPTER);
}

dataUriBuilder.appendEncodedPath(AssociationContract.REMOTE_PATH_SUFFIX)
dataUriBuilder.appendEncodedPath(isRemote ? AssociationContract.REMOTE_PATH_SUFFIX : AssociationContract.LOCAL_REFLECTOR_PATH_SUFFIX)
.appendQueryParameter(AssociationContract.PARAMETER_ASSOCIATION_TOKEN,
associationToken)
.appendQueryParameter(AssociationContract.REMOTE_PARAMETER_REFLECTOR_HOST_AUTHORITY,
.appendQueryParameter(AssociationContract.PARAMETER_REFLECTOR_HOST_AUTHORITY,
hostAuthority)
.appendQueryParameter(AssociationContract.REMOTE_PARAMETER_REFLECTOR_ID,
.appendQueryParameter(AssociationContract.PARAMETER_REFLECTOR_ID,
Base64.encodeToString(reflectorId, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP));

for (SessionProperties.ProtocolVersion version : supportedProtocolVersions) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class RemoteAssociationScenario extends Scenario {
private static final String TAG = RemoteAssociationScenario.class.getSimpleName();
public class ReflectorAssociationScenario extends Scenario {
private static final String TAG = ReflectorAssociationScenario.class.getSimpleName();
private static final int CONNECT_MAX_ATTEMPTS = 34;
private static final int[] CONNECT_BACKOFF_SCHEDULE_MS = { 150, 150, 200, 500, 500, 750, 750, 1000 }; // == 30s, which allows time for a user to choose a wallet from the disambiguation dialog, and for that wallet to start
private static final int CONNECT_TIMEOUT_MS = 30000;
Expand All @@ -47,7 +47,7 @@ public class RemoteAssociationScenario extends Scenario {
private ArrayList<NotifyingCompletableFuture<Void>> mClosedFuture; // _may_ be valid in State.CLOSING

public interface ReflectorIdCallback {
void reflectorIdReceived(RemoteAssociationScenario scenario, byte[] reflectorId);
void reflectorIdReceived(ReflectorAssociationScenario scenario, byte[] reflectorId);
}
private ReflectorIdCallback mReflectorIdCallback;

Expand All @@ -59,16 +59,16 @@ public MobileWalletAdapterSession getSession() {
return mMobileWalletAdapterSession;
}

public RemoteAssociationScenario(@NonNull String hostAuthority,
@IntRange(from = 0) int clientTimeoutMs,
@NonNull ReflectorIdCallback reflectorIdCallback) {
public ReflectorAssociationScenario(@NonNull String hostAuthority,
@IntRange(from = 0) int clientTimeoutMs,
@NonNull ReflectorIdCallback reflectorIdCallback) {
this(WebSocketsTransportContract.WEBSOCKETS_REFLECTOR_SCHEME, hostAuthority, clientTimeoutMs, reflectorIdCallback);
}

// Only for testing
public RemoteAssociationScenario(@NonNull String scheme, @NonNull String hostAuthority,
@IntRange(from = 0) int clientTimeoutMs,
@NonNull ReflectorIdCallback reflectorIdCallback) {
public ReflectorAssociationScenario(@NonNull String scheme, @NonNull String hostAuthority,
@IntRange(from = 0) int clientTimeoutMs,
@NonNull ReflectorIdCallback reflectorIdCallback) {
super(clientTimeoutMs);

mHostAuthority = hostAuthority;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import com.solana.mobilewalletadapter.clientlib.scenario.LocalAssociationIntentCreator
import com.solana.mobilewalletadapter.clientlib.scenario.LocalAssociationScenario
import com.solana.mobilewalletadapter.clientlib.scenario.RemoteAssociationIntentCreator
import com.solana.mobilewalletadapter.clientlib.scenario.RemoteAssociationScenario
import com.solana.mobilewalletadapter.clientlib.scenario.ReflectorAssociationIntentCreator
import com.solana.mobilewalletadapter.clientlib.scenario.ReflectorAssociationScenario
import com.solana.mobilewalletadapter.clientlib.scenario.Scenario
import com.solana.mobilewalletadapter.common.ProtocolContract
import com.solana.mobilewalletadapter.common.signin.SignInWithSolana
Expand Down Expand Up @@ -205,13 +205,16 @@ class MainActivityTest {

// simulate client side scenario
val hostAuthority = "localhost:$port"
val remoteAssociation = RemoteAssociationScenario("ws", hostAuthority,
Scenario.DEFAULT_CLIENT_TIMEOUT_MS) { scenario, reflectorId ->
val associationIntent = RemoteAssociationIntentCreator.createAssociationIntent(
val remoteAssociation = ReflectorAssociationScenario(
"ws", hostAuthority,
Scenario.DEFAULT_CLIENT_TIMEOUT_MS
) { scenario, reflectorId ->
val associationIntent = ReflectorAssociationIntentCreator.createAssociationIntent(
null,
scenario.hostAuthority,
reflectorId,
scenario.session
scenario.session,
true
)
ActivityScenario.launch<MainActivity>(associationIntent)
}
Expand All @@ -235,6 +238,56 @@ class MainActivityTest {
assertTrue(authResult?.authToken?.isNotEmpty() == true)
}

@Test
fun authorizationFlow_SuccessfulLocalReflectorAuthorization() {
// given
val uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())

val identityUri = Uri.parse("https://test.com")
val iconUri = Uri.parse("favicon.ico")
val identityName = "Test"
val chain = ProtocolContract.CHAIN_SOLANA_TESTNET

// simulate remote reflector server
val port = 8800 + Random.nextInt(0, 100)
val server = WebSocketReflectorServer(port)
server.init()

// simulate client side scenario
val hostAuthority = "localhost:$port"
val localReflectorAssociation = ReflectorAssociationScenario(
"ws", hostAuthority,
Scenario.DEFAULT_CLIENT_TIMEOUT_MS
) { scenario, reflectorId ->
val associationIntent = ReflectorAssociationIntentCreator.createAssociationIntent(
null,
scenario.hostAuthority,
reflectorId,
scenario.session,
false
)
ActivityScenario.launch<MainActivity>(associationIntent)
}

// when
// trigger authorization from client
val authorization = localReflectorAssociation.start().get().run {
authorize(identityUri, iconUri, identityName, chain, null, null, null, null)
}

uiDevice.wait(Until.hasObject(By.res(FAKEWALLET_PACKAGE, "authorize")), WINDOW_CHANGE_TIMEOUT)

// then
onView(withId(R.id.btn_authorize))
.check(matches(isDisplayed())).perform(click())

val authResult = authorization.get()
server.close()

// verify that we got an auth token (successful auth)
assertTrue(authResult?.authToken?.isNotEmpty() == true)
}

@Test
fun authorizationFlow_SuccessfulReauthorization() {
// given
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.solana.mobilewalletadapter.walletlib.association;

import android.content.Context;
import android.net.Uri;

import androidx.annotation.NonNull;

import com.solana.mobilewalletadapter.common.AssociationContract;
import com.solana.mobilewalletadapter.walletlib.authorization.AuthIssuerConfig;
import com.solana.mobilewalletadapter.walletlib.protocol.MobileWalletAdapterConfig;
import com.solana.mobilewalletadapter.walletlib.scenario.LocalScenario;
import com.solana.mobilewalletadapter.walletlib.scenario.LocalWebSocketReflectorServerScenario;
import com.solana.mobilewalletadapter.walletlib.scenario.Scenario;

public class LocalReflectorAssociationUri extends ReflectorAssociationUri {

public LocalReflectorAssociationUri(@NonNull Uri uri) {
super(uri, AssociationContract.LOCAL_REFLECTOR_PATH_SUFFIX);
}

@NonNull
@Override
public Scenario createScenario(@NonNull Context context,
@NonNull MobileWalletAdapterConfig mobileWalletAdapterConfig,
@NonNull AuthIssuerConfig authIssuerConfig,
@NonNull Scenario.Callbacks callbacks) {
if (callbacks instanceof LocalScenario.Callbacks) {
return new LocalWebSocketReflectorServerScenario(context, mobileWalletAdapterConfig,
authIssuerConfig, (LocalScenario.Callbacks) callbacks, associationPublicKey,
associationProtocolVersions, "ws", reflectorHostAuthority, reflectorIdBytes);
} else {
throw new IllegalArgumentException("callbacks must implement " + LocalScenario.Callbacks.class.getName());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,10 @@
import com.solana.mobilewalletadapter.walletlib.scenario.RemoteWebSocketServerScenario;
import com.solana.mobilewalletadapter.walletlib.scenario.Scenario;

public class RemoteAssociationUri extends AssociationUri {
@NonNull
public final String reflectorHostAuthority;

public final byte[] reflectorIdBytes;
public class RemoteAssociationUri extends ReflectorAssociationUri {

public RemoteAssociationUri(@NonNull Uri uri) {
super(uri);
validate(uri);
reflectorHostAuthority = parseReflectorHostAuthority(uri);
reflectorIdBytes = parseReflectorId(uri);
}

private static void validate(@NonNull Uri uri) {
if (!uri.getPath().endsWith(AssociationContract.REMOTE_PATH_SUFFIX)) {
throw new IllegalArgumentException("uri must end with " +
AssociationContract.REMOTE_PATH_SUFFIX);
}
super(uri, AssociationContract.REMOTE_PATH_SUFFIX);
}

@NonNull
Expand All @@ -46,32 +32,4 @@ public Scenario createScenario(@NonNull Context context,
authIssuerConfig, callbacks, associationPublicKey, associationProtocolVersions,
"ws", reflectorHostAuthority, reflectorIdBytes);
}

@NonNull
private static String parseReflectorHostAuthority(@NonNull Uri uri) {
final String reflectorHostAuthority = uri.getQueryParameter(
AssociationContract.REMOTE_PARAMETER_REFLECTOR_HOST_AUTHORITY);
if (reflectorHostAuthority == null || reflectorHostAuthority.isEmpty()) {
throw new IllegalArgumentException("Reflector host authority must be specified");
}

return reflectorHostAuthority;
}

private static byte[] parseReflectorId(@NonNull Uri uri) {
final String reflectorIdStr = uri.getQueryParameter(
AssociationContract.REMOTE_PARAMETER_REFLECTOR_ID);
if (reflectorIdStr == null) {
throw new IllegalArgumentException("Reflector ID parameter must be specified");
}

final byte[] reflectorId;
try {
reflectorId = Base64.decode(reflectorIdStr, Base64.URL_SAFE);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Reflector ID parameter must be a base64 url encoded byte sequence", e);
}

return reflectorId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import android.util.Log
import com.solana.digitalassetlinks.AndroidAppPackageVerifier
import com.solana.mobilewalletadapter.walletlib.association.AssociationUri
import com.solana.mobilewalletadapter.walletlib.association.LocalAssociationUri
import com.solana.mobilewalletadapter.walletlib.association.LocalReflectorAssociationUri
import com.solana.mobilewalletadapter.walletlib.association.RemoteAssociationUri
import kotlinx.coroutines.*
import java.net.URI
Expand All @@ -25,7 +26,7 @@ class ClientTrustUseCase(private val repositoryScope: CoroutineScope,

init {
associationType = when (associationUri) {
is LocalAssociationUri -> {
is LocalAssociationUri, is LocalReflectorAssociationUri -> {
if (callingPackage != null) {
Log.d(TAG, "Creating client trust use case for a local app scenario")
AssociationType.LocalFromApp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ public static AssociationUri parse(@NonNull Uri uri) {
return new LocalAssociationUri(uri);
} catch (IllegalArgumentException ignored) {}

try {
return new LocalReflectorAssociationUri(uri);
} catch (IllegalArgumentException ignored) {}

try {
return new RemoteAssociationUri(uri);
} catch (IllegalArgumentException ignored) {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.solana.mobilewalletadapter.walletlib.association;

import android.content.Context;
import android.net.Uri;

import androidx.annotation.NonNull;

import com.solana.mobilewalletadapter.common.AssociationContract;
import com.solana.mobilewalletadapter.walletlib.authorization.AuthIssuerConfig;
import com.solana.mobilewalletadapter.walletlib.protocol.MobileWalletAdapterConfig;
import com.solana.mobilewalletadapter.walletlib.scenario.LocalScenario;
import com.solana.mobilewalletadapter.walletlib.scenario.LocalWebSocketReflectorServerScenario;
import com.solana.mobilewalletadapter.walletlib.scenario.Scenario;

public class LocalReflectorAssociationUri extends ReflectorAssociationUri {

public LocalReflectorAssociationUri(@NonNull Uri uri) {
super(uri, AssociationContract.LOCAL_REFLECTOR_PATH_SUFFIX);
}

@NonNull
@Override
public Scenario createScenario(@NonNull Context context,
@NonNull MobileWalletAdapterConfig mobileWalletAdapterConfig,
@NonNull AuthIssuerConfig authIssuerConfig,
@NonNull Scenario.Callbacks callbacks) {
if (callbacks instanceof LocalScenario.Callbacks) {
return new LocalWebSocketReflectorServerScenario(context, mobileWalletAdapterConfig,
authIssuerConfig, (LocalScenario.Callbacks) callbacks,
associationPublicKey, reflectorHostAuthority, reflectorIdBytes);
} else {
throw new IllegalArgumentException("callbacks must implement " + LocalScenario.Callbacks.class.getName());
}
}
}
Loading