Skip to content
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
42 changes: 18 additions & 24 deletions packages/client/src/MWPClient.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { WebBasedWalletCommunicator } from 'src/components/communicator/webBased/Communicator';

import { postRequestToWallet } from './components/communication/postRequestToWallet';
import { KeyManager } from './components/key/KeyManager';
import { MWPClient } from './MWPClient';
import {
Expand All @@ -25,11 +24,10 @@ jest.mock(':core/util/utils', () => {
};
});

jest.mock('./components/communication/postRequestToWallet');

jest.mock('expo-web-browser', () => ({
openBrowserAsync: jest.fn(),
WebBrowserPresentationStyle: {
FORM_SHEET: 'FORM_SHEET',
},
openAuthSessionAsync: jest.fn(),
dismissBrowser: jest.fn(),
}));

Expand Down Expand Up @@ -70,15 +68,12 @@ describe('MWPClient', () => {

beforeEach(async () => {
mockMetadata = {
appName: 'test',
appChainIds: [1],
appDeeplinkUrl: 'https://example.com',
appCustomScheme: 'myapp://',
name: 'test',
chainIds: [1],
customScheme: 'myapp://',
};

jest
.spyOn(WebBasedWalletCommunicator, 'postRequestAndWaitForResponse')
.mockResolvedValue(mockSuccessResponse);
(postRequestToWallet as jest.Mock).mockResolvedValue(mockSuccessResponse);

mockKeyManager = new KeyManager({
wallet: mockWallet,
Expand All @@ -104,10 +99,9 @@ describe('MWPClient', () => {
expect(client['chain']).toEqual({ id: 1 });
expect(client['accounts']).toEqual([]);
expect(client['metadata']).toEqual({
appName: 'test',
appChainIds: [1],
appDeeplinkUrl: `https://example.com/${MWP_RESPONSE_PATH}`,
appCustomScheme: `myapp:///${MWP_RESPONSE_PATH}`,
name: 'test',
chainIds: [1],
customScheme: `myapp:///${MWP_RESPONSE_PATH}`,
});
});

Expand Down Expand Up @@ -147,9 +141,7 @@ describe('MWPClient', () => {
content: { failure: mockError },
timestamp: new Date(),
};
(WebBasedWalletCommunicator.postRequestAndWaitForResponse as jest.Mock).mockResolvedValue(
mockResponse
);
(postRequestToWallet as jest.Mock).mockResolvedValue(mockResponse);

await expect(client.handshake()).rejects.toThrowError(mockError);
});
Expand Down Expand Up @@ -188,12 +180,13 @@ describe('MWPClient', () => {
const result = await client.request(mockRequest);

expect(encryptContent).toHaveBeenCalled();
expect(WebBasedWalletCommunicator.postRequestAndWaitForResponse).toHaveBeenCalledWith(
expect(postRequestToWallet).toHaveBeenCalledWith(
expect.objectContaining({
sender: '0xPublicKey',
content: { encrypted: encryptedData },
}),
mockWallet.scheme
`${mockMetadata.customScheme}/${MWP_RESPONSE_PATH}`,
mockWallet
);
expect(result).toEqual('0xSignature');
});
Expand Down Expand Up @@ -227,12 +220,13 @@ describe('MWPClient', () => {

await client.request(mockRequest);

expect(WebBasedWalletCommunicator.postRequestAndWaitForResponse).toHaveBeenCalledWith(
expect(postRequestToWallet).toHaveBeenCalledWith(
expect.objectContaining({
sender: '0xPublicKey',
content: { encrypted: encryptedData },
}),
mockWallet.scheme
`${mockMetadata.customScheme}/${MWP_RESPONSE_PATH}`,
mockWallet
);
});

Expand Down
25 changes: 11 additions & 14 deletions packages/client/src/MWPClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const ACCOUNTS_KEY = 'accounts';
const ACTIVE_CHAIN_STORAGE_KEY = 'activeChain';
const AVAILABLE_CHAINS_STORAGE_KEY = 'availableChains';
const WALLET_CAPABILITIES_STORAGE_KEY = 'walletCapabilities';
import * as Communicator from './components/communicator';
import { postRequestToWallet } from './components/communication/postRequestToWallet';
import { LIB_VERSION } from './version';
import {
appendMWPResponsePath,
Expand Down Expand Up @@ -47,11 +47,8 @@ export class MWPClient {
private constructor({ metadata, wallet }: MWPClientOptions) {
this.metadata = {
...metadata,
appName: metadata.appName || 'Dapp',
appDeeplinkUrl: appendMWPResponsePath(metadata.appDeeplinkUrl),
appCustomScheme: metadata.appCustomScheme
? appendMWPResponsePath(metadata.appCustomScheme)
: undefined,
name: metadata.name || 'Dapp',
customScheme: appendMWPResponsePath(metadata.customScheme),
};

this.wallet = wallet;
Expand All @@ -61,7 +58,7 @@ export class MWPClient {
// default values
this.accounts = [];
this.chain = {
id: metadata.appChainIds?.[0] ?? 1,
id: metadata.chainIds?.[0] ?? 1,
};

this.handshake = this.handshake.bind(this);
Expand Down Expand Up @@ -94,13 +91,14 @@ export class MWPClient {
handshake: {
method: 'eth_requestAccounts',
params: {
appName: this.metadata.appName,
appLogoUrl: this.metadata.appLogoUrl,
appName: this.metadata.name,
appLogoUrl: this.metadata.logoUrl,
},
},
});
const response: RPCResponseMessage = await Communicator.postRequestToWallet(
const response: RPCResponseMessage = await postRequestToWallet(
handshakeMessage,
this.metadata.customScheme,
this.wallet
);

Expand Down Expand Up @@ -179,7 +177,7 @@ export class MWPClient {
await this.keyManager.clear();
this.accounts = [];
this.chain = {
id: this.metadata.appChainIds?.[0] ?? 1,
id: this.metadata.chainIds?.[0] ?? 1,
};
}

Expand Down Expand Up @@ -225,7 +223,7 @@ export class MWPClient {
);
const message = await this.createRequestMessage({ encrypted });

return Communicator.postRequestToWallet(message, this.wallet);
return postRequestToWallet(message, this.metadata.customScheme, this.wallet);
}

private async createRequestMessage(
Expand All @@ -238,8 +236,7 @@ export class MWPClient {
content,
sdkVersion: LIB_VERSION,
timestamp: new Date(),
callbackUrl: this.metadata.appDeeplinkUrl,
customScheme: this.metadata.appCustomScheme,
callbackUrl: this.metadata.customScheme,
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import * as WebBrowser from 'expo-web-browser';

import { postRequestToWallet } from './postRequestToWallet';
import { decodeResponseURLParams, encodeRequestURLParams } from './utils/encoding';
import { RPCRequestMessage, RPCResponseMessage } from ':core/message';
import { Wallet } from ':core/wallet';

jest.mock('expo-web-browser', () => ({
openAuthSessionAsync: jest.fn(),
dismissBrowser: jest.fn(),
}));

jest.mock('./utils/encoding', () => ({
...jest.requireActual('./utils/encoding'),
decodeResponseURLParams: jest.fn(),
}));

const mockAppCustomScheme = 'myapp://';
const mockWalletScheme = 'https://example.com';

describe('postRequestToWallet', () => {
const mockRequest: RPCRequestMessage = {
id: '1-2-3-4-5',
sdkVersion: '1.0.0',
content: {
handshake: {
method: 'eth_requestAccounts',
params: { appName: 'test' },
},
},
callbackUrl: 'https://example.com',
sender: 'Sender',
timestamp: new Date(),
};
const mockResponse: RPCResponseMessage = {
id: '2-2-3-4-5',
requestId: '1-2-3-4-5',
content: {
encrypted: {
iv: new Uint8Array([1]),
cipherText: new Uint8Array([2]),
},
},
sender: 'some-sender',
timestamp: new Date(),
};
let requestUrl: URL;

beforeEach(() => {
requestUrl = new URL(mockWalletScheme);
requestUrl.search = encodeRequestURLParams(mockRequest);
jest.clearAllMocks();
});

it('should successfully post request to a web-based wallet', async () => {
const webWallet: Wallet = { type: 'web', scheme: mockWalletScheme } as Wallet;
(WebBrowser.openAuthSessionAsync as jest.Mock).mockResolvedValue({
type: 'success',
url: 'https://example.com/response',
});
(decodeResponseURLParams as jest.Mock).mockResolvedValue(mockResponse);

const result = await postRequestToWallet(mockRequest, mockAppCustomScheme, webWallet);

expect(WebBrowser.openAuthSessionAsync).toHaveBeenCalledWith(
requestUrl.toString(),
mockAppCustomScheme,
{
preferEphemeralSession: false,
}
);
expect(result).toEqual(mockResponse);
});

it('should throw an error if the user cancels the request', async () => {
const webWallet: Wallet = { type: 'web', scheme: mockWalletScheme } as Wallet;
(WebBrowser.openAuthSessionAsync as jest.Mock).mockResolvedValue({
type: 'cancel',
});

await expect(postRequestToWallet(mockRequest, mockAppCustomScheme, webWallet)).rejects.toThrow(
'User rejected the request'
);
});

it('should throw an error for native wallet type', async () => {
const nativeWallet: Wallet = { type: 'native', scheme: mockWalletScheme } as Wallet;

await expect(
postRequestToWallet(mockRequest, mockAppCustomScheme, nativeWallet)
).rejects.toThrow('Native wallet not supported yet');
});

it('should throw an error for unsupported wallet type', async () => {
const unsupportedWallet: Wallet = {
type: 'unsupported' as any,
scheme: mockWalletScheme,
} as Wallet;

await expect(
postRequestToWallet(mockRequest, mockAppCustomScheme, unsupportedWallet)
).rejects.toThrow('Unsupported wallet type');
});

it('should pass through any errors from WebBrowser', async () => {
const webWallet: Wallet = { type: 'web', scheme: mockWalletScheme } as Wallet;
const mockError = new Error('Communication error');
(WebBrowser.openAuthSessionAsync as jest.Mock).mockRejectedValue(mockError);

await expect(postRequestToWallet(mockRequest, mockAppCustomScheme, webWallet)).rejects.toThrow(
'User rejected the request'
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as WebBrowser from 'expo-web-browser';

import { decodeResponseURLParams } from './utils/encoding';
import { encodeRequestURLParams } from './utils/encoding';
import { standardErrors } from ':core/error';
import { RPCRequestMessage, RPCResponseMessage } from ':core/message';
import { Wallet } from ':core/wallet';

/**
* Posts a request to a wallet and waits for the response.
*
* @param request - The request to send.
* @param wallet - The wallet to send the request to.
* @returns A promise that resolves to the response.
*/
export async function postRequestToWallet(
request: RPCRequestMessage,
appCustomScheme: string,
wallet: Wallet
): Promise<RPCResponseMessage> {
const { type, scheme } = wallet;

if (type === 'web') {
return new Promise((resolve, reject) => {
// 1. generate request URL
const requestUrl = new URL(scheme);
requestUrl.search = encodeRequestURLParams(request);

// 2. send request via Expo WebBrowser
WebBrowser.openAuthSessionAsync(requestUrl.toString(), appCustomScheme, {
preferEphemeralSession: false,
})
.then((result) => {
if (result.type === 'cancel') {
// iOS only: user cancelled the request
reject(standardErrors.provider.userRejectedRequest());
WebBrowser.dismissBrowser();
}

if (result.type === 'success') {
const { searchParams } = new URL(result.url);
const response = decodeResponseURLParams(searchParams);

resolve(response);
}
})
.catch(() => {
reject(standardErrors.provider.userRejectedRequest());
WebBrowser.dismissBrowser();
});
});
}

if (type === 'native') {
throw new Error('Native wallet not supported yet');
}

throw new Error('Unsupported wallet type');
}
Loading
Loading