Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: you can now configure the SolanaMobileWalletAdapter with an address selector #171

Merged
merged 1 commit into from
Jul 27, 2022
Merged
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
7 changes: 6 additions & 1 deletion examples/example-web-app/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { createTheme } from '@mui/material';
import { WalletAdapterNetwork } from '@solana/wallet-adapter-base';
import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
import { clusterApiUrl } from '@solana/web3.js';
import { createDefaultAuthorizationResultCache, SolanaMobileWalletAdapter } from '@solana-mobile/wallet-adapter-mobile';
import {
createDefaultAddressSelector,
createDefaultAuthorizationResultCache,
SolanaMobileWalletAdapter,
} from '@solana-mobile/wallet-adapter-mobile';
import type { AppProps } from 'next/app';
import { SnackbarProvider } from 'notistack';
import { useMemo } from 'react';
Expand All @@ -22,6 +26,7 @@ function ExampleMobileDApp({ Component, pageProps }: AppProps) {
? [] // No wallet adapters when server-side rendering.
: [
new SolanaMobileWalletAdapter({
addressSelector: createDefaultAddressSelector(),
appIdentity: {
icon: 'images/app_icon.png',
name: 'Mobile Web dApp',
Expand Down
2 changes: 2 additions & 0 deletions js/packages/wallet-adapter-mobile/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Create an instance of the mobile wallet adapter like this.

```typescript
new SolanaMobileWalletAdapter({
addressSelector: createDefaultAddressSelector(),
appIdentity: {
name: 'My app',
uri: 'https://myapp.io',
Expand All @@ -24,6 +25,7 @@ Use that adapter instance alongside the other adapters used by your app.
```typescript
const wallets = useMemo(() => [
new SolanaMobileWalletAdapter({
addressSelector: createDefaultAddressSelector(),
appIdentity: {
name: 'My app',
uri: 'https://myapp.io',
Expand Down
53 changes: 32 additions & 21 deletions js/packages/wallet-adapter-mobile/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import { Web3MobileWallet, transact } from '@solana-mobile/mobile-wallet-adapter-protocol-web3js';
import {
AppIdentity,
AuthorizationResult,
AuthToken,
Base64EncodedAddress,
} from '@solana-mobile/mobile-wallet-adapter-protocol';
import { AppIdentity, AuthorizationResult, Base64EncodedAddress } from '@solana-mobile/mobile-wallet-adapter-protocol';
import {
BaseMessageSignerWalletAdapter,
WalletConnectionError,
Expand All @@ -29,6 +24,10 @@ export interface AuthorizationResultCache {
set(authorizationResult: AuthorizationResult): Promise<void>;
}

export interface AddressSelector {
select(addresses: Base64EncodedAddress[]): Promise<Base64EncodedAddress>;
}

export const SolanaMobileWalletAdapterWalletName = 'Default wallet app' as WalletName;

const SIGNATURE_LENGTH_IN_BYTES = 64;
Expand All @@ -44,21 +43,25 @@ export class SolanaMobileWalletAdapter extends BaseMessageSignerWalletAdapter {
icon =
'data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjI4IiB3aWR0aD0iMjgiIHZpZXdCb3g9Ii0zIDAgMjggMjgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0iI0RDQjhGRiI+PHBhdGggZD0iTTE3LjQgMTcuNEgxNXYyLjRoMi40di0yLjRabTEuMi05LjZoLTIuNHYyLjRoMi40VjcuOFoiLz48cGF0aCBkPSJNMjEuNiAzVjBoLTIuNHYzaC0zLjZWMGgtMi40djNoLTIuNHY2LjZINC41YTIuMSAyLjEgMCAxIDEgMC00LjJoMi43VjNINC41QTQuNSA0LjUgMCAwIDAgMCA3LjVWMjRoMjEuNnYtNi42aC0yLjR2NC4ySDIuNFYxMS41Yy41LjMgMS4yLjQgMS44LjVoNy41QTYuNiA2LjYgMCAwIDAgMjQgOVYzaC0yLjRabTAgNS43YTQuMiA0LjIgMCAxIDEtOC40IDBWNS40aDguNHYzLjNaIi8+PC9nPjwvc3ZnPg==';

private _addressSelector: AddressSelector;
private _appIdentity: AppIdentity;
private _authorizationResult: AuthorizationResult | undefined;
private _authorizationResultCache: AuthorizationResultCache;
private _connecting = false;
private _cluster: Cluster;
private _publicKey: PublicKey | undefined;
private _readyState: WalletReadyState = getIsSupported() ? WalletReadyState.Loadable : WalletReadyState.Unsupported;
private _selectedAddress: Base64EncodedAddress | undefined;

constructor(config: {
addressSelector: AddressSelector;
appIdentity: AppIdentity;
authorizationResultCache: AuthorizationResultCache;
cluster: Cluster;
}) {
super();
this._authorizationResultCache = config.authorizationResultCache;
this._addressSelector = config.addressSelector;
this._appIdentity = config.appIdentity;
this._cluster = config.cluster;
if (this._readyState !== WalletReadyState.Unsupported) {
Expand All @@ -74,12 +77,9 @@ export class SolanaMobileWalletAdapter extends BaseMessageSignerWalletAdapter {
}

get publicKey(): PublicKey | null {
if (this._publicKey == null && this._authorizationResult != null) {
if (this._publicKey == null && this._selectedAddress != null) {
try {
this._publicKey = getPublicKeyFromAddress(
// TODO(#44): support multiple addresses
this._authorizationResult.addresses[0],
);
this._publicKey = getPublicKeyFromAddress(this._selectedAddress);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be missing something here, but how can you reliably get a public key from an address? Addresses (if not on Solana) may not be reversible to pubkeys.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I presumed that the Solana wallet-adapter would only ever work with Solana wallets. We’d have to change the return type of useWallet, which right now only has the concept of publicKey.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that's a fair assumption then! I think I was not seeing that the context of this code is wallet-adapter-mobile and not mobile-wallet-adapter

} catch (e) {
throw new WalletPublicKeyError((e instanceof Error && e?.message) || 'Unknown error', e);
}
Expand Down Expand Up @@ -121,9 +121,10 @@ export class SolanaMobileWalletAdapter extends BaseMessageSignerWalletAdapter {
if (this._readyState !== WalletReadyState.Installed) {
this.emit('readyStateChange', (this._readyState = WalletReadyState.Installed));
}
this._selectedAddress = await this._addressSelector.select(cachedAuthorizationResult.addresses);
this.emit(
'connect',
// Having just set an `authorizationResult`, `this.publicKey` is definitely non-null
// Having just set `this._selectedAddress`, `this.publicKey` is definitely non-null
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.publicKey!,
);
Expand All @@ -146,16 +147,26 @@ export class SolanaMobileWalletAdapter extends BaseMessageSignerWalletAdapter {
}

private async handleAuthorizationResult(authorizationResult: AuthorizationResult): Promise<void> {
const didPublicKeyChange = this._authorizationResult?.addresses[0] !== authorizationResult.addresses[0]; // TODO(#44): support multiple addresses
const didPublicKeysChange =
// Case 1: We started from having no authorization.
this._authorizationResult == null ||
// Case 2: The number of authorized public keys changed.
this._authorizationResult?.addresses.length !== authorizationResult.addresses.length ||
// Case 3: The new list of addresses isn't exactly the same as the old list, in the same order.
this._authorizationResult.addresses.some((address, ii) => address !== authorizationResult.addresses[ii]);
this._authorizationResult = authorizationResult;
if (didPublicKeyChange) {
delete this._publicKey;
this.emit(
'connect',
// Having just set an `authorizationResult`, `this.publicKey` is definitely non-null
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.publicKey!,
);
if (didPublicKeysChange) {
const nextSelectedAddress = await this._addressSelector.select(authorizationResult.addresses);
if (nextSelectedAddress !== this._selectedAddress) {
this._selectedAddress = nextSelectedAddress;
delete this._publicKey;
this.emit(
'connect',
// Having just set `this._selectedAddress`, `this.publicKey` is definitely non-null
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.publicKey!,
);
}
}
await this._authorizationResultCache.set(authorizationResult);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { AddressSelector } from './adapter';

export default function createDefaultAddressSelector(): AddressSelector {
return {
async select(addresses) {
return addresses[0];
},
};
}
1 change: 1 addition & 0 deletions js/packages/wallet-adapter-mobile/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './adapter';
export { default as createDefaultAddressSelector } from './createDefaultAddressSelector';
export { default as createDefaultAuthorizationResultCache } from './createDefaultAuthorizationResultCache';