@solana-mobile/mobile-wallet-adapter-protocol 2.0.1 → 2.1.0-alpha1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,5 @@
1
+ import { createSignInMessageText } from '@solana/wallet-standard-util';
2
+
1
3
  // Typescript `enums` thwart tree-shaking. See https://bargsten.org/jsts/enums/
2
4
  const SolanaMobileWalletAdapterErrorCode = {
3
5
  ERROR_ASSOCIATION_PORT_OUT_OF_RANGE: 'ERROR_ASSOCIATION_PORT_OUT_OF_RANGE',
@@ -6,6 +8,7 @@ const SolanaMobileWalletAdapterErrorCode = {
6
8
  ERROR_SESSION_CLOSED: 'ERROR_SESSION_CLOSED',
7
9
  ERROR_SESSION_TIMEOUT: 'ERROR_SESSION_TIMEOUT',
8
10
  ERROR_WALLET_NOT_FOUND: 'ERROR_WALLET_NOT_FOUND',
11
+ ERROR_INVALID_PROTOCOL_VERSION: 'ERROR_INVALID_PROTOCOL_VERSION',
9
12
  };
10
13
  class SolanaMobileWalletAdapterError extends Error {
11
14
  constructor(...args) {
@@ -73,6 +76,175 @@ function createHelloReq(ecdhPublicKey, associationKeypairPrivateKey) {
73
76
  });
74
77
  }
75
78
 
79
+ function encode(input) {
80
+ return window.btoa(input);
81
+ }
82
+
83
+ function createSIWSMessage(payload) {
84
+ return createSignInMessageText(payload);
85
+ }
86
+ function createSIWSMessageBase64(payload) {
87
+ return encode(createSIWSMessage(payload));
88
+ }
89
+
90
+ // optional features
91
+ const SolanaSignTransactions = 'solana:signTransactions';
92
+ const SolanaCloneAuthorization = 'solana:cloneAuthorization';
93
+ const SolanaSignInWithSolana = 'solana:signInWithSolana';
94
+
95
+ /**
96
+ * Creates a {@link MobileWallet} proxy that handles backwards compatibility and API to RPC conversion.
97
+ *
98
+ * @param protocolVersion the protocol version in use for this session/request
99
+ * @param protocolRequestHandler callback function that handles sending the RPC request to the wallet endpoint.
100
+ * @returns a {@link MobileWallet} proxy
101
+ */
102
+ function createMobileWalletProxy(protocolVersion, protocolRequestHandler) {
103
+ return new Proxy({}, {
104
+ get(target, p) {
105
+ if (target[p] == null) {
106
+ target[p] = function (inputParams) {
107
+ return __awaiter(this, void 0, void 0, function* () {
108
+ const { method, params } = handleMobileWalletRequest(p, inputParams, protocolVersion);
109
+ const result = yield protocolRequestHandler(method, params);
110
+ // if the request tried to sign in but the wallet did not return a sign in result, fallback on message signing
111
+ if (method === 'authorize' && params.sign_in_payload && !result.sign_in_result) {
112
+ result['sign_in_result'] = yield signInFallback(params.sign_in_payload, result, protocolRequestHandler);
113
+ }
114
+ return handleMobileWalletResponse(p, result, protocolVersion);
115
+ });
116
+ };
117
+ }
118
+ return target[p];
119
+ },
120
+ defineProperty() {
121
+ return false;
122
+ },
123
+ deleteProperty() {
124
+ return false;
125
+ },
126
+ });
127
+ }
128
+ /**
129
+ * Handles all {@link MobileWallet} API requests and determines the correct MWA RPC method and params to call.
130
+ * This handles backwards compatibility, based on the provided @protocolVersion.
131
+ *
132
+ * @param methodName the name of {@link MobileWallet} method that was called
133
+ * @param methodParams the parameters that were passed to the method
134
+ * @param protocolVersion the protocol version in use for this session/request
135
+ * @returns the RPC request method and params that should be sent to the wallet endpoint
136
+ */
137
+ function handleMobileWalletRequest(methodName, methodParams, protocolVersion) {
138
+ let params = methodParams;
139
+ let method = methodName
140
+ .toString()
141
+ .replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
142
+ .toLowerCase();
143
+ switch (methodName) {
144
+ case 'authorize': {
145
+ let { chain } = params;
146
+ if (protocolVersion === 'legacy') {
147
+ switch (chain) {
148
+ case 'solana:testnet': {
149
+ chain = 'testnet';
150
+ break;
151
+ }
152
+ case 'solana:devnet': {
153
+ chain = 'devnet';
154
+ break;
155
+ }
156
+ case 'solana:mainnet': {
157
+ chain = 'mainnet-beta';
158
+ break;
159
+ }
160
+ default: {
161
+ chain = params.cluster;
162
+ }
163
+ }
164
+ params.cluster = chain;
165
+ }
166
+ else {
167
+ switch (chain) {
168
+ case 'testnet':
169
+ case 'devnet': {
170
+ chain = `solana:${chain}`;
171
+ break;
172
+ }
173
+ case 'mainnet-beta': {
174
+ chain = 'solana:mainnet';
175
+ break;
176
+ }
177
+ }
178
+ params.chain = chain;
179
+ }
180
+ }
181
+ case 'reauthorize': {
182
+ const { auth_token, identity } = params;
183
+ if (auth_token) {
184
+ switch (protocolVersion) {
185
+ case 'legacy': {
186
+ method = 'reauthorize';
187
+ params = { auth_token: auth_token, identity: identity };
188
+ break;
189
+ }
190
+ default: {
191
+ method = 'authorize';
192
+ break;
193
+ }
194
+ }
195
+ }
196
+ break;
197
+ }
198
+ }
199
+ return { method, params };
200
+ }
201
+ /**
202
+ * Handles all {@link MobileWallet} API responses and modifies the response for backwards compatibility, if needed
203
+ *
204
+ * @param method the {@link MobileWallet} method that was called
205
+ * @param response the original response that was returned by the method call
206
+ * @param protocolVersion the protocol version in use for this session/request
207
+ * @returns the possibly modified response
208
+ */
209
+ function handleMobileWalletResponse(method, response, protocolVersion) {
210
+ switch (method) {
211
+ case 'getCapabilities': {
212
+ const capabilities = response;
213
+ switch (protocolVersion) {
214
+ case 'legacy': {
215
+ const features = [SolanaSignTransactions];
216
+ if (capabilities.supports_clone_authorization === true) {
217
+ features.push(SolanaCloneAuthorization);
218
+ }
219
+ return Object.assign(Object.assign({}, capabilities), { features: features });
220
+ }
221
+ case 'v1': {
222
+ return Object.assign(Object.assign({}, capabilities), { supports_sign_and_send_transactions: true, supports_clone_authorization: capabilities.features.includes(SolanaCloneAuthorization) });
223
+ }
224
+ }
225
+ }
226
+ }
227
+ return response;
228
+ }
229
+ function signInFallback(signInPayload, authorizationResult, protocolRequestHandler) {
230
+ var _a;
231
+ return __awaiter(this, void 0, void 0, function* () {
232
+ const domain = (_a = signInPayload.domain) !== null && _a !== void 0 ? _a : window.location.host;
233
+ const address = authorizationResult.accounts[0].address;
234
+ const siwsMessage = createSIWSMessageBase64(Object.assign(Object.assign({}, signInPayload), { domain, address }));
235
+ const signMessageResult = yield protocolRequestHandler('sign_messages', {
236
+ addresses: [address],
237
+ payloads: [siwsMessage]
238
+ });
239
+ const signInResult = {
240
+ address: address,
241
+ signed_message: siwsMessage,
242
+ signature: signMessageResult.signed_payloads[0].slice(siwsMessage.length)
243
+ };
244
+ return signInResult;
245
+ });
246
+ }
247
+
76
248
  const SEQUENCE_NUMBER_BYTES = 4;
77
249
  function createSequenceNumberVector(sequenceNumber) {
78
250
  if (sequenceNumber >= 4294967296) {
@@ -84,29 +256,11 @@ function createSequenceNumberVector(sequenceNumber) {
84
256
  return new Uint8Array(byteArray);
85
257
  }
86
258
 
87
- function generateAssociationKeypair() {
88
- return __awaiter(this, void 0, void 0, function* () {
89
- return yield crypto.subtle.generateKey({
90
- name: 'ECDSA',
91
- namedCurve: 'P-256',
92
- }, false /* extractable */, ['sign'] /* keyUsages */);
93
- });
94
- }
95
-
96
- function generateECDHKeypair() {
97
- return __awaiter(this, void 0, void 0, function* () {
98
- return yield crypto.subtle.generateKey({
99
- name: 'ECDH',
100
- namedCurve: 'P-256',
101
- }, false /* extractable */, ['deriveKey', 'deriveBits'] /* keyUsages */);
102
- });
103
- }
104
-
105
259
  const INITIALIZATION_VECTOR_BYTES = 12;
106
- function encryptJsonRpcMessage(jsonRpcMessage, sharedSecret) {
260
+ const ENCODED_PUBLIC_KEY_LENGTH_BYTES = 65;
261
+ function encryptMessage(plaintext, sequenceNumber, sharedSecret) {
107
262
  return __awaiter(this, void 0, void 0, function* () {
108
- const plaintext = JSON.stringify(jsonRpcMessage);
109
- const sequenceNumberVector = createSequenceNumberVector(jsonRpcMessage.id);
263
+ const sequenceNumberVector = createSequenceNumberVector(sequenceNumber);
110
264
  const initializationVector = new Uint8Array(INITIALIZATION_VECTOR_BYTES);
111
265
  crypto.getRandomValues(initializationVector);
112
266
  const ciphertext = yield crypto.subtle.encrypt(getAlgorithmParams(sequenceNumberVector, initializationVector), sharedSecret, new TextEncoder().encode(plaintext));
@@ -117,18 +271,14 @@ function encryptJsonRpcMessage(jsonRpcMessage, sharedSecret) {
117
271
  return response;
118
272
  });
119
273
  }
120
- function decryptJsonRpcMessage(message, sharedSecret) {
274
+ function decryptMessage(message, sharedSecret) {
121
275
  return __awaiter(this, void 0, void 0, function* () {
122
276
  const sequenceNumberVector = message.slice(0, SEQUENCE_NUMBER_BYTES);
123
277
  const initializationVector = message.slice(SEQUENCE_NUMBER_BYTES, SEQUENCE_NUMBER_BYTES + INITIALIZATION_VECTOR_BYTES);
124
278
  const ciphertext = message.slice(SEQUENCE_NUMBER_BYTES + INITIALIZATION_VECTOR_BYTES);
125
279
  const plaintextBuffer = yield crypto.subtle.decrypt(getAlgorithmParams(sequenceNumberVector, initializationVector), sharedSecret, ciphertext);
126
280
  const plaintext = getUtf8Decoder().decode(plaintextBuffer);
127
- const jsonRpcMessage = JSON.parse(plaintext);
128
- if (Object.hasOwnProperty.call(jsonRpcMessage, 'error')) {
129
- throw new SolanaMobileWalletAdapterProtocolError(jsonRpcMessage.id, jsonRpcMessage.error.code, jsonRpcMessage.error.message);
130
- }
131
- return jsonRpcMessage;
281
+ return plaintext;
132
282
  });
133
283
  }
134
284
  function getAlgorithmParams(sequenceNumber, initializationVector) {
@@ -147,12 +297,48 @@ function getUtf8Decoder() {
147
297
  return _utf8Decoder;
148
298
  }
149
299
 
300
+ function generateAssociationKeypair() {
301
+ return __awaiter(this, void 0, void 0, function* () {
302
+ return yield crypto.subtle.generateKey({
303
+ name: 'ECDSA',
304
+ namedCurve: 'P-256',
305
+ }, false /* extractable */, ['sign'] /* keyUsages */);
306
+ });
307
+ }
308
+
309
+ function generateECDHKeypair() {
310
+ return __awaiter(this, void 0, void 0, function* () {
311
+ return yield crypto.subtle.generateKey({
312
+ name: 'ECDH',
313
+ namedCurve: 'P-256',
314
+ }, false /* extractable */, ['deriveKey', 'deriveBits'] /* keyUsages */);
315
+ });
316
+ }
317
+
318
+ function encryptJsonRpcMessage(jsonRpcMessage, sharedSecret) {
319
+ return __awaiter(this, void 0, void 0, function* () {
320
+ const plaintext = JSON.stringify(jsonRpcMessage);
321
+ const sequenceNumber = jsonRpcMessage.id;
322
+ return encryptMessage(plaintext, sequenceNumber, sharedSecret);
323
+ });
324
+ }
325
+ function decryptJsonRpcMessage(message, sharedSecret) {
326
+ return __awaiter(this, void 0, void 0, function* () {
327
+ const plaintext = yield decryptMessage(message, sharedSecret);
328
+ const jsonRpcMessage = JSON.parse(plaintext);
329
+ if (Object.hasOwnProperty.call(jsonRpcMessage, 'error')) {
330
+ throw new SolanaMobileWalletAdapterProtocolError(jsonRpcMessage.id, jsonRpcMessage.error.code, jsonRpcMessage.error.message);
331
+ }
332
+ return jsonRpcMessage;
333
+ });
334
+ }
335
+
150
336
  function parseHelloRsp(payloadBuffer, // The X9.62-encoded wallet endpoint ephemeral ECDH public keypoint.
151
337
  associationPublicKey, ecdhPrivateKey) {
152
338
  return __awaiter(this, void 0, void 0, function* () {
153
339
  const [associationPublicKeyBuffer, walletPublicKey] = yield Promise.all([
154
340
  crypto.subtle.exportKey('raw', associationPublicKey),
155
- crypto.subtle.importKey('raw', payloadBuffer, { name: 'ECDH', namedCurve: 'P-256' }, false /* extractable */, [] /* keyUsages */),
341
+ crypto.subtle.importKey('raw', payloadBuffer.slice(0, ENCODED_PUBLIC_KEY_LENGTH_BYTES), { name: 'ECDH', namedCurve: 'P-256' }, false /* extractable */, [] /* keyUsages */),
156
342
  ]);
157
343
  const sharedSecret = yield crypto.subtle.deriveBits({ name: 'ECDH', public: walletPublicKey }, ecdhPrivateKey, 256);
158
344
  const ecdhSecretKey = yield crypto.subtle.importKey('raw', sharedSecret, 'HKDF', false /* extractable */, ['deriveKey'] /* keyUsages */);
@@ -166,6 +352,31 @@ associationPublicKey, ecdhPrivateKey) {
166
352
  });
167
353
  }
168
354
 
355
+ function parseSessionProps(message, sharedSecret) {
356
+ return __awaiter(this, void 0, void 0, function* () {
357
+ const plaintext = yield decryptMessage(message, sharedSecret);
358
+ const jsonProperties = JSON.parse(plaintext);
359
+ let protocolVersion = 'legacy';
360
+ if (Object.hasOwnProperty.call(jsonProperties, 'v')) {
361
+ switch (jsonProperties.v) {
362
+ case 1:
363
+ case '1':
364
+ case 'v1':
365
+ protocolVersion = 'v1';
366
+ break;
367
+ case 'legacy':
368
+ protocolVersion = 'legacy';
369
+ break;
370
+ default:
371
+ throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_INVALID_PROTOCOL_VERSION, `Unknown/unsupported protocol version: ${jsonProperties.v}`);
372
+ }
373
+ }
374
+ return ({
375
+ protocol_version: protocolVersion
376
+ });
377
+ });
378
+ }
379
+
169
380
  function getRandomAssociationPort() {
170
381
  return assertAssociationPort(49152 + Math.floor(Math.random() * (65535 - 49152 + 1)));
171
382
  }
@@ -222,7 +433,7 @@ function getIntentURL(methodPathname, intentUrlBase) {
222
433
  [...getPathParts(baseUrl.pathname), ...getPathParts(methodPathname)].join('/');
223
434
  return new URL(pathname, baseUrl);
224
435
  }
225
- function getAssociateAndroidIntentURL(associationPublicKey, putativePort, associationURLBase) {
436
+ function getAssociateAndroidIntentURL(associationPublicKey, putativePort, associationURLBase, protocolVersions = ['v1']) {
226
437
  return __awaiter(this, void 0, void 0, function* () {
227
438
  const associationPort = assertAssociationPort(putativePort);
228
439
  const exportedKey = yield crypto.subtle.exportKey('raw', associationPublicKey);
@@ -230,6 +441,9 @@ function getAssociateAndroidIntentURL(associationPublicKey, putativePort, associ
230
441
  const url = getIntentURL('v1/associate/local', associationURLBase);
231
442
  url.searchParams.set('association', getStringWithURLUnsafeCharactersReplaced(encodedKey));
232
443
  url.searchParams.set('port', `${associationPort}`);
444
+ protocolVersions.forEach((version) => {
445
+ url.searchParams.set('v', version);
446
+ });
233
447
  return url;
234
448
  });
235
449
  }
@@ -434,59 +648,51 @@ function transact(callback, config) {
434
648
  break;
435
649
  case 'hello_req_sent': {
436
650
  const sharedSecret = yield parseHelloRsp(responseBuffer, state.associationPublicKey, state.ecdhPrivateKey);
437
- state = { __type: 'connected', sharedSecret };
438
- const wallet = new Proxy({}, {
439
- get(target, p) {
440
- if (target[p] == null) {
441
- const method = p
442
- .toString()
443
- .replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
444
- .toLowerCase();
445
- target[p] = function (params) {
446
- return __awaiter(this, void 0, void 0, function* () {
447
- const id = nextJsonRpcMessageId++;
448
- socket.send(yield encryptJsonRpcMessage({
449
- id,
450
- jsonrpc: '2.0',
451
- method,
452
- params: params !== null && params !== void 0 ? params : {},
453
- }, sharedSecret));
454
- return new Promise((resolve, reject) => {
455
- jsonRpcResponsePromises[id] = {
456
- resolve(result) {
457
- switch (p) {
458
- case 'authorize':
459
- case 'reauthorize': {
460
- const { wallet_uri_base } = result;
461
- if (wallet_uri_base != null) {
462
- try {
463
- assertSecureEndpointSpecificURI(wallet_uri_base);
464
- }
465
- catch (e) {
466
- reject(e);
467
- return;
468
- }
469
- }
470
- break;
471
- }
472
- }
473
- resolve(result);
474
- },
475
- reject,
476
- };
477
- });
478
- });
479
- };
651
+ const sessionPropertiesBuffer = responseBuffer.slice(ENCODED_PUBLIC_KEY_LENGTH_BYTES);
652
+ const sessionProperties = sessionPropertiesBuffer.byteLength !== 0
653
+ ? yield (() => __awaiter(this, void 0, void 0, function* () {
654
+ const sequenceNumberVector = sessionPropertiesBuffer.slice(0, SEQUENCE_NUMBER_BYTES);
655
+ const sequenceNumber = getSequenceNumberFromByteArray(sequenceNumberVector);
656
+ if (sequenceNumber !== (lastKnownInboundSequenceNumber + 1)) {
657
+ throw new Error('Encrypted message has invalid sequence number');
480
658
  }
481
- return target[p];
482
- },
483
- defineProperty() {
484
- return false;
485
- },
486
- deleteProperty() {
487
- return false;
488
- },
489
- });
659
+ lastKnownInboundSequenceNumber = sequenceNumber;
660
+ return parseSessionProps(sessionPropertiesBuffer, sharedSecret);
661
+ }))() : { protocol_version: 'legacy' };
662
+ state = { __type: 'connected', sharedSecret, sessionProperties };
663
+ const wallet = createMobileWalletProxy(sessionProperties.protocol_version, (method, params) => __awaiter(this, void 0, void 0, function* () {
664
+ const id = nextJsonRpcMessageId++;
665
+ socket.send(yield encryptJsonRpcMessage({
666
+ id,
667
+ jsonrpc: '2.0',
668
+ method,
669
+ params: params !== null && params !== void 0 ? params : {},
670
+ }, sharedSecret));
671
+ return new Promise((resolve, reject) => {
672
+ jsonRpcResponsePromises[id] = {
673
+ resolve(result) {
674
+ switch (method) {
675
+ case 'authorize':
676
+ case 'reauthorize': {
677
+ const { wallet_uri_base } = result;
678
+ if (wallet_uri_base != null) {
679
+ try {
680
+ assertSecureEndpointSpecificURI(wallet_uri_base);
681
+ }
682
+ catch (e) {
683
+ reject(e);
684
+ return;
685
+ }
686
+ }
687
+ break;
688
+ }
689
+ }
690
+ resolve(result);
691
+ },
692
+ reject,
693
+ };
694
+ });
695
+ }));
490
696
  try {
491
697
  resolve(yield callback(wallet));
492
698
  }
@@ -529,4 +735,4 @@ function transact(callback, config) {
529
735
  });
530
736
  }
531
737
 
532
- export { SolanaMobileWalletAdapterError, SolanaMobileWalletAdapterErrorCode, SolanaMobileWalletAdapterProtocolError, SolanaMobileWalletAdapterProtocolErrorCode, transact };
738
+ export { SolanaCloneAuthorization, SolanaMobileWalletAdapterError, SolanaMobileWalletAdapterErrorCode, SolanaMobileWalletAdapterProtocolError, SolanaMobileWalletAdapterProtocolErrorCode, SolanaSignInWithSolana, SolanaSignTransactions, transact };