Skip to content

Commit 92b39a8

Browse files
authored
[BW-764] Use AuthSession for Communication (#26)
1 parent 124f0e1 commit 92b39a8

19 files changed

+218
-635
lines changed

packages/client/src/MWPClient.test.ts

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { WebBasedWalletCommunicator } from 'src/components/communicator/webBased/Communicator';
2-
1+
import { postRequestToWallet } from './components/communication/postRequestToWallet';
32
import { KeyManager } from './components/key/KeyManager';
43
import { MWPClient } from './MWPClient';
54
import {
@@ -25,11 +24,10 @@ jest.mock(':core/util/utils', () => {
2524
};
2625
});
2726

27+
jest.mock('./components/communication/postRequestToWallet');
28+
2829
jest.mock('expo-web-browser', () => ({
29-
openBrowserAsync: jest.fn(),
30-
WebBrowserPresentationStyle: {
31-
FORM_SHEET: 'FORM_SHEET',
32-
},
30+
openAuthSessionAsync: jest.fn(),
3331
dismissBrowser: jest.fn(),
3432
}));
3533

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

7169
beforeEach(async () => {
7270
mockMetadata = {
73-
appName: 'test',
74-
appChainIds: [1],
75-
appDeeplinkUrl: 'https://example.com',
76-
appCustomScheme: 'myapp://',
71+
name: 'test',
72+
chainIds: [1],
73+
customScheme: 'myapp://',
7774
};
7875

79-
jest
80-
.spyOn(WebBasedWalletCommunicator, 'postRequestAndWaitForResponse')
81-
.mockResolvedValue(mockSuccessResponse);
76+
(postRequestToWallet as jest.Mock).mockResolvedValue(mockSuccessResponse);
8277

8378
mockKeyManager = new KeyManager({
8479
wallet: mockWallet,
@@ -104,10 +99,9 @@ describe('MWPClient', () => {
10499
expect(client['chain']).toEqual({ id: 1 });
105100
expect(client['accounts']).toEqual([]);
106101
expect(client['metadata']).toEqual({
107-
appName: 'test',
108-
appChainIds: [1],
109-
appDeeplinkUrl: `https://example.com/${MWP_RESPONSE_PATH}`,
110-
appCustomScheme: `myapp:///${MWP_RESPONSE_PATH}`,
102+
name: 'test',
103+
chainIds: [1],
104+
customScheme: `myapp:///${MWP_RESPONSE_PATH}`,
111105
});
112106
});
113107

@@ -147,9 +141,7 @@ describe('MWPClient', () => {
147141
content: { failure: mockError },
148142
timestamp: new Date(),
149143
};
150-
(WebBasedWalletCommunicator.postRequestAndWaitForResponse as jest.Mock).mockResolvedValue(
151-
mockResponse
152-
);
144+
(postRequestToWallet as jest.Mock).mockResolvedValue(mockResponse);
153145

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

190182
expect(encryptContent).toHaveBeenCalled();
191-
expect(WebBasedWalletCommunicator.postRequestAndWaitForResponse).toHaveBeenCalledWith(
183+
expect(postRequestToWallet).toHaveBeenCalledWith(
192184
expect.objectContaining({
193185
sender: '0xPublicKey',
194186
content: { encrypted: encryptedData },
195187
}),
196-
mockWallet.scheme
188+
`${mockMetadata.customScheme}/${MWP_RESPONSE_PATH}`,
189+
mockWallet
197190
);
198191
expect(result).toEqual('0xSignature');
199192
});
@@ -227,12 +220,13 @@ describe('MWPClient', () => {
227220

228221
await client.request(mockRequest);
229222

230-
expect(WebBasedWalletCommunicator.postRequestAndWaitForResponse).toHaveBeenCalledWith(
223+
expect(postRequestToWallet).toHaveBeenCalledWith(
231224
expect.objectContaining({
232225
sender: '0xPublicKey',
233226
content: { encrypted: encryptedData },
234227
}),
235-
mockWallet.scheme
228+
`${mockMetadata.customScheme}/${MWP_RESPONSE_PATH}`,
229+
mockWallet
236230
);
237231
});
238232

packages/client/src/MWPClient.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const ACCOUNTS_KEY = 'accounts';
1616
const ACTIVE_CHAIN_STORAGE_KEY = 'activeChain';
1717
const AVAILABLE_CHAINS_STORAGE_KEY = 'availableChains';
1818
const WALLET_CAPABILITIES_STORAGE_KEY = 'walletCapabilities';
19-
import * as Communicator from './components/communicator';
19+
import { postRequestToWallet } from './components/communication/postRequestToWallet';
2020
import { LIB_VERSION } from './version';
2121
import {
2222
appendMWPResponsePath,
@@ -47,11 +47,8 @@ export class MWPClient {
4747
private constructor({ metadata, wallet }: MWPClientOptions) {
4848
this.metadata = {
4949
...metadata,
50-
appName: metadata.appName || 'Dapp',
51-
appDeeplinkUrl: appendMWPResponsePath(metadata.appDeeplinkUrl),
52-
appCustomScheme: metadata.appCustomScheme
53-
? appendMWPResponsePath(metadata.appCustomScheme)
54-
: undefined,
50+
name: metadata.name || 'Dapp',
51+
customScheme: appendMWPResponsePath(metadata.customScheme),
5552
};
5653

5754
this.wallet = wallet;
@@ -61,7 +58,7 @@ export class MWPClient {
6158
// default values
6259
this.accounts = [];
6360
this.chain = {
64-
id: metadata.appChainIds?.[0] ?? 1,
61+
id: metadata.chainIds?.[0] ?? 1,
6562
};
6663

6764
this.handshake = this.handshake.bind(this);
@@ -94,13 +91,14 @@ export class MWPClient {
9491
handshake: {
9592
method: 'eth_requestAccounts',
9693
params: {
97-
appName: this.metadata.appName,
98-
appLogoUrl: this.metadata.appLogoUrl,
94+
appName: this.metadata.name,
95+
appLogoUrl: this.metadata.logoUrl,
9996
},
10097
},
10198
});
102-
const response: RPCResponseMessage = await Communicator.postRequestToWallet(
99+
const response: RPCResponseMessage = await postRequestToWallet(
103100
handshakeMessage,
101+
this.metadata.customScheme,
104102
this.wallet
105103
);
106104

@@ -179,7 +177,7 @@ export class MWPClient {
179177
await this.keyManager.clear();
180178
this.accounts = [];
181179
this.chain = {
182-
id: this.metadata.appChainIds?.[0] ?? 1,
180+
id: this.metadata.chainIds?.[0] ?? 1,
183181
};
184182
}
185183

@@ -225,7 +223,7 @@ export class MWPClient {
225223
);
226224
const message = await this.createRequestMessage({ encrypted });
227225

228-
return Communicator.postRequestToWallet(message, this.wallet);
226+
return postRequestToWallet(message, this.metadata.customScheme, this.wallet);
229227
}
230228

231229
private async createRequestMessage(
@@ -238,8 +236,7 @@ export class MWPClient {
238236
content,
239237
sdkVersion: LIB_VERSION,
240238
timestamp: new Date(),
241-
callbackUrl: this.metadata.appDeeplinkUrl,
242-
customScheme: this.metadata.appCustomScheme,
239+
callbackUrl: this.metadata.customScheme,
243240
};
244241
}
245242

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import * as WebBrowser from 'expo-web-browser';
2+
3+
import { postRequestToWallet } from './postRequestToWallet';
4+
import { decodeResponseURLParams, encodeRequestURLParams } from './utils/encoding';
5+
import { RPCRequestMessage, RPCResponseMessage } from ':core/message';
6+
import { Wallet } from ':core/wallet';
7+
8+
jest.mock('expo-web-browser', () => ({
9+
openAuthSessionAsync: jest.fn(),
10+
dismissBrowser: jest.fn(),
11+
}));
12+
13+
jest.mock('./utils/encoding', () => ({
14+
...jest.requireActual('./utils/encoding'),
15+
decodeResponseURLParams: jest.fn(),
16+
}));
17+
18+
const mockAppCustomScheme = 'myapp://';
19+
const mockWalletScheme = 'https://example.com';
20+
21+
describe('postRequestToWallet', () => {
22+
const mockRequest: RPCRequestMessage = {
23+
id: '1-2-3-4-5',
24+
sdkVersion: '1.0.0',
25+
content: {
26+
handshake: {
27+
method: 'eth_requestAccounts',
28+
params: { appName: 'test' },
29+
},
30+
},
31+
callbackUrl: 'https://example.com',
32+
sender: 'Sender',
33+
timestamp: new Date(),
34+
};
35+
const mockResponse: RPCResponseMessage = {
36+
id: '2-2-3-4-5',
37+
requestId: '1-2-3-4-5',
38+
content: {
39+
encrypted: {
40+
iv: new Uint8Array([1]),
41+
cipherText: new Uint8Array([2]),
42+
},
43+
},
44+
sender: 'some-sender',
45+
timestamp: new Date(),
46+
};
47+
let requestUrl: URL;
48+
49+
beforeEach(() => {
50+
requestUrl = new URL(mockWalletScheme);
51+
requestUrl.search = encodeRequestURLParams(mockRequest);
52+
jest.clearAllMocks();
53+
});
54+
55+
it('should successfully post request to a web-based wallet', async () => {
56+
const webWallet: Wallet = { type: 'web', scheme: mockWalletScheme } as Wallet;
57+
(WebBrowser.openAuthSessionAsync as jest.Mock).mockResolvedValue({
58+
type: 'success',
59+
url: 'https://example.com/response',
60+
});
61+
(decodeResponseURLParams as jest.Mock).mockResolvedValue(mockResponse);
62+
63+
const result = await postRequestToWallet(mockRequest, mockAppCustomScheme, webWallet);
64+
65+
expect(WebBrowser.openAuthSessionAsync).toHaveBeenCalledWith(
66+
requestUrl.toString(),
67+
mockAppCustomScheme,
68+
{
69+
preferEphemeralSession: false,
70+
}
71+
);
72+
expect(result).toEqual(mockResponse);
73+
});
74+
75+
it('should throw an error if the user cancels the request', async () => {
76+
const webWallet: Wallet = { type: 'web', scheme: mockWalletScheme } as Wallet;
77+
(WebBrowser.openAuthSessionAsync as jest.Mock).mockResolvedValue({
78+
type: 'cancel',
79+
});
80+
81+
await expect(postRequestToWallet(mockRequest, mockAppCustomScheme, webWallet)).rejects.toThrow(
82+
'User rejected the request'
83+
);
84+
});
85+
86+
it('should throw an error for native wallet type', async () => {
87+
const nativeWallet: Wallet = { type: 'native', scheme: mockWalletScheme } as Wallet;
88+
89+
await expect(
90+
postRequestToWallet(mockRequest, mockAppCustomScheme, nativeWallet)
91+
).rejects.toThrow('Native wallet not supported yet');
92+
});
93+
94+
it('should throw an error for unsupported wallet type', async () => {
95+
const unsupportedWallet: Wallet = {
96+
type: 'unsupported' as any,
97+
scheme: mockWalletScheme,
98+
} as Wallet;
99+
100+
await expect(
101+
postRequestToWallet(mockRequest, mockAppCustomScheme, unsupportedWallet)
102+
).rejects.toThrow('Unsupported wallet type');
103+
});
104+
105+
it('should pass through any errors from WebBrowser', async () => {
106+
const webWallet: Wallet = { type: 'web', scheme: mockWalletScheme } as Wallet;
107+
const mockError = new Error('Communication error');
108+
(WebBrowser.openAuthSessionAsync as jest.Mock).mockRejectedValue(mockError);
109+
110+
await expect(postRequestToWallet(mockRequest, mockAppCustomScheme, webWallet)).rejects.toThrow(
111+
'User rejected the request'
112+
);
113+
});
114+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import * as WebBrowser from 'expo-web-browser';
2+
3+
import { decodeResponseURLParams } from './utils/encoding';
4+
import { encodeRequestURLParams } from './utils/encoding';
5+
import { standardErrors } from ':core/error';
6+
import { RPCRequestMessage, RPCResponseMessage } from ':core/message';
7+
import { Wallet } from ':core/wallet';
8+
9+
/**
10+
* Posts a request to a wallet and waits for the response.
11+
*
12+
* @param request - The request to send.
13+
* @param wallet - The wallet to send the request to.
14+
* @returns A promise that resolves to the response.
15+
*/
16+
export async function postRequestToWallet(
17+
request: RPCRequestMessage,
18+
appCustomScheme: string,
19+
wallet: Wallet
20+
): Promise<RPCResponseMessage> {
21+
const { type, scheme } = wallet;
22+
23+
if (type === 'web') {
24+
return new Promise((resolve, reject) => {
25+
// 1. generate request URL
26+
const requestUrl = new URL(scheme);
27+
requestUrl.search = encodeRequestURLParams(request);
28+
29+
// 2. send request via Expo WebBrowser
30+
WebBrowser.openAuthSessionAsync(requestUrl.toString(), appCustomScheme, {
31+
preferEphemeralSession: false,
32+
})
33+
.then((result) => {
34+
if (result.type === 'cancel') {
35+
// iOS only: user cancelled the request
36+
reject(standardErrors.provider.userRejectedRequest());
37+
WebBrowser.dismissBrowser();
38+
}
39+
40+
if (result.type === 'success') {
41+
const { searchParams } = new URL(result.url);
42+
const response = decodeResponseURLParams(searchParams);
43+
44+
resolve(response);
45+
}
46+
})
47+
.catch(() => {
48+
reject(standardErrors.provider.userRejectedRequest());
49+
WebBrowser.dismissBrowser();
50+
});
51+
});
52+
}
53+
54+
if (type === 'native') {
55+
throw new Error('Native wallet not supported yet');
56+
}
57+
58+
throw new Error('Unsupported wallet type');
59+
}

0 commit comments

Comments
 (0)