@solana-mobile/mobile-wallet-adapter-protocol 2.1.3 → 2.1.5

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.
Files changed (47) hide show
  1. package/android/build.gradle +158 -146
  2. package/android/gradle/wrapper/gradle-wrapper.properties +5 -5
  3. package/android/gradle.properties +5 -5
  4. package/android/src/main/java/com/solanamobile/mobilewalletadapter/reactnative/JSONSerializationUtils.kt +11 -9
  5. package/android/src/main/java/com/solanamobile/mobilewalletadapter/reactnative/SolanaMobileWalletAdapterModule.kt +201 -178
  6. package/android/src/main/java/com/solanamobile/mobilewalletadapter/reactnative/SolanaMobileWalletAdapterPackage.kt +26 -7
  7. package/android/src/newarch/SolanaMobileWalletAdapter.kt +7 -0
  8. package/android/src/oldarch/SolanaMobileWalletAdapter.kt +16 -0
  9. package/lib/cjs/index.browser.js +266 -5
  10. package/lib/cjs/index.js +266 -5
  11. package/lib/cjs/index.native.js +5 -2
  12. package/lib/esm/index.browser.js +266 -6
  13. package/lib/esm/index.js +266 -6
  14. package/lib/types/index.browser.d.ts +17 -1
  15. package/lib/types/index.browser.d.ts.map +1 -1
  16. package/lib/types/index.d.ts +17 -1
  17. package/lib/types/index.d.ts.map +1 -1
  18. package/lib/types/index.native.d.ts +17 -1
  19. package/lib/types/index.native.d.ts.map +1 -1
  20. package/package.json +70 -58
  21. package/.gitignore +0 -2
  22. package/android/.gitignore +0 -14
  23. package/src/__forks__/react-native/base64Utils.ts +0 -1
  24. package/src/__forks__/react-native/transact.ts +0 -92
  25. package/src/arrayBufferToBase64String.ts +0 -10
  26. package/src/associationPort.ts +0 -19
  27. package/src/base64Utils.ts +0 -3
  28. package/src/createHelloReq.ts +0 -12
  29. package/src/createMobileWalletProxy.ts +0 -175
  30. package/src/createSIWSMessage.ts +0 -14
  31. package/src/createSequenceNumberVector.ts +0 -11
  32. package/src/encryptedMessage.ts +0 -60
  33. package/src/errors.ts +0 -95
  34. package/src/generateAssociationKeypair.ts +0 -10
  35. package/src/generateECDHKeypair.ts +0 -10
  36. package/src/getAssociateAndroidIntentURL.ts +0 -57
  37. package/src/getJWS.ts +0 -19
  38. package/src/getStringWithURLUnsafeBase64CharactersReplaced.ts +0 -11
  39. package/src/index.ts +0 -3
  40. package/src/jsonRpcMessage.ts +0 -38
  41. package/src/parseHelloRsp.ts +0 -46
  42. package/src/parseSessionProps.ts +0 -33
  43. package/src/startSession.ts +0 -94
  44. package/src/transact.ts +0 -266
  45. package/src/types.ts +0 -181
  46. package/tsconfig.cjs.json +0 -7
  47. package/tsconfig.json +0 -8
package/lib/cjs/index.js CHANGED
@@ -7,6 +7,7 @@ var walletStandardUtil = require('@solana/wallet-standard-util');
7
7
  // Typescript `enums` thwart tree-shaking. See https://bargsten.org/jsts/enums/
8
8
  const SolanaMobileWalletAdapterErrorCode = {
9
9
  ERROR_ASSOCIATION_PORT_OUT_OF_RANGE: 'ERROR_ASSOCIATION_PORT_OUT_OF_RANGE',
10
+ ERROR_REFLECTOR_ID_OUT_OF_RANGE: 'ERROR_REFLECTOR_ID_OUT_OF_RANGE',
10
11
  ERROR_FORBIDDEN_WALLET_BASE_URL: 'ERROR_FORBIDDEN_WALLET_BASE_URL',
11
12
  ERROR_SECURE_CONTEXT_REQUIRED: 'ERROR_SECURE_CONTEXT_REQUIRED',
12
13
  ERROR_SESSION_CLOSED: 'ERROR_SESSION_CLOSED',
@@ -410,6 +411,24 @@ function getStringWithURLUnsafeCharactersReplaced(unsafeBase64EncodedString) {
410
411
  }[m]));
411
412
  }
412
413
 
414
+ function getRandomReflectorId() {
415
+ return assertReflectorId(getRandomInt(0, 9007199254740991)); // 0 < id < 2^53 - 1
416
+ }
417
+ function getRandomInt(min, max) {
418
+ const randomBuffer = new Uint32Array(1);
419
+ window.crypto.getRandomValues(randomBuffer);
420
+ let randomNumber = randomBuffer[0] / (0xffffffff + 1);
421
+ min = Math.ceil(min);
422
+ max = Math.floor(max);
423
+ return Math.floor(randomNumber * (max - min + 1)) + min;
424
+ }
425
+ function assertReflectorId(id) {
426
+ if (id < 0 || id > 9007199254740991) { // 0 < id < 2^53 - 1
427
+ throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_REFLECTOR_ID_OUT_OF_RANGE, `Association port number must be between 49152 and 65535. ${id} given.`, { id });
428
+ }
429
+ return id;
430
+ }
431
+
413
432
  const INTENT_NAME = 'solana-wallet';
414
433
  function getPathParts(pathString) {
415
434
  return (pathString
@@ -451,6 +470,21 @@ function getAssociateAndroidIntentURL(associationPublicKey, putativePort, associ
451
470
  return url;
452
471
  });
453
472
  }
473
+ function getRemoteAssociateAndroidIntentURL(associationPublicKey, hostAuthority, putativeId, associationURLBase, protocolVersions = ['v1']) {
474
+ return __awaiter(this, void 0, void 0, function* () {
475
+ const reflectorId = assertReflectorId(putativeId);
476
+ const exportedKey = yield crypto.subtle.exportKey('raw', associationPublicKey);
477
+ const encodedKey = arrayBufferToBase64String(exportedKey);
478
+ const url = getIntentURL('v1/associate/remote', associationURLBase);
479
+ url.searchParams.set('association', getStringWithURLUnsafeCharactersReplaced(encodedKey));
480
+ url.searchParams.set('reflector', `${hostAuthority}`);
481
+ url.searchParams.set('id', `${reflectorId}`);
482
+ protocolVersions.forEach((version) => {
483
+ url.searchParams.set('v', version);
484
+ });
485
+ return url;
486
+ });
487
+ }
454
488
 
455
489
  // Typescript `enums` thwart tree-shaking. See https://bargsten.org/jsts/enums/
456
490
  const Browser = {
@@ -493,10 +527,8 @@ function launchUrlThroughHiddenFrame(url) {
493
527
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
494
528
  _frame.contentWindow.location.href = url.toString();
495
529
  }
496
- function startSession(associationPublicKey, associationURLBase) {
530
+ function launchAssociation(associationUrl) {
497
531
  return __awaiter(this, void 0, void 0, function* () {
498
- const randomAssociationPort = getRandomAssociationPort();
499
- const associationUrl = yield getAssociateAndroidIntentURL(associationPublicKey, randomAssociationPort, associationURLBase);
500
532
  if (associationUrl.protocol === 'https:') {
501
533
  // The association URL is an Android 'App Link' or iOS 'Universal Link'.
502
534
  // These are regular web URLs that are designed to launch an app if it
@@ -527,9 +559,23 @@ function startSession(associationPublicKey, associationURLBase) {
527
559
  throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND, 'Found no installed wallet that supports the mobile wallet protocol.');
528
560
  }
529
561
  }
562
+ });
563
+ }
564
+ function startSession(associationPublicKey, associationURLBase) {
565
+ return __awaiter(this, void 0, void 0, function* () {
566
+ const randomAssociationPort = getRandomAssociationPort();
567
+ const associationUrl = yield getAssociateAndroidIntentURL(associationPublicKey, randomAssociationPort, associationURLBase);
568
+ yield launchAssociation(associationUrl);
530
569
  return randomAssociationPort;
531
570
  });
532
571
  }
572
+ function getRemoteSessionUrl(associationPublicKey, hostAuthority, associationURLBase) {
573
+ return __awaiter(this, void 0, void 0, function* () {
574
+ const randomReflectorId = getRandomReflectorId();
575
+ const associationUrl = yield getRemoteAssociateAndroidIntentURL(associationPublicKey, hostAuthority, randomReflectorId, associationURLBase);
576
+ return { associationUrl, reflectorId: randomReflectorId };
577
+ });
578
+ }
533
579
 
534
580
  const WEBSOCKET_CONNECTION_CONFIG = {
535
581
  /**
@@ -591,8 +637,14 @@ function transact(callback, config) {
591
637
  `Got \`${state.__type}\`.`);
592
638
  return;
593
639
  }
594
- const { associationKeypair } = state;
595
640
  socket.removeEventListener('open', handleOpen);
641
+ // previous versions of this library and walletlib incorrectly implemented the MWA session
642
+ // establishment protocol for local connections. The dapp is supposed to wait for the
643
+ // APP_PING message before sending the HELLO_REQ. Instead, the dapp was sending the HELLO_REQ
644
+ // immediately upon connection to the websocket server regardless of wether or not an
645
+ // APP_PING was sent by the wallet/websocket server. We must continue to support this behavior
646
+ // in case the user is using a wallet that has not updated their walletlib implementation.
647
+ const { associationKeypair } = state;
596
648
  const ecdhKeypair = yield generateECDHKeypair();
597
649
  socket.send(yield createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
598
650
  state = {
@@ -613,7 +665,7 @@ function transact(callback, config) {
613
665
  const handleError = (_evt) => __awaiter(this, void 0, void 0, function* () {
614
666
  disposeSocket();
615
667
  if (Date.now() - connectionStartTime >= WEBSOCKET_CONNECTION_CONFIG.timeoutMs) {
616
- reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_TIMEOUT, `Failed to connect to the wallet websocket on port ${sessionPort}.`));
668
+ reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_TIMEOUT, `Failed to connect to the wallet websocket at ${websocketURL}.`));
617
669
  }
618
670
  else {
619
671
  yield new Promise((resolve) => {
@@ -626,6 +678,18 @@ function transact(callback, config) {
626
678
  const handleMessage = (evt) => __awaiter(this, void 0, void 0, function* () {
627
679
  const responseBuffer = yield evt.data.arrayBuffer();
628
680
  switch (state.__type) {
681
+ case 'connecting':
682
+ if (responseBuffer.byteLength !== 0) {
683
+ throw new Error('Encountered unexpected message while connecting');
684
+ }
685
+ const ecdhKeypair = yield generateECDHKeypair();
686
+ socket.send(yield createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
687
+ state = {
688
+ __type: 'hello_req_sent',
689
+ associationPublicKey: associationKeypair.publicKey,
690
+ ecdhPrivateKey: ecdhKeypair.privateKey,
691
+ };
692
+ break;
629
693
  case 'connected':
630
694
  try {
631
695
  const sequenceNumberVector = responseBuffer.slice(0, SEQUENCE_NUMBER_BYTES);
@@ -651,6 +715,17 @@ function transact(callback, config) {
651
715
  }
652
716
  break;
653
717
  case 'hello_req_sent': {
718
+ // if we receive an APP_PING message (empty message), resend the HELLO_REQ (see above)
719
+ if (responseBuffer.byteLength === 0) {
720
+ const ecdhKeypair = yield generateECDHKeypair();
721
+ socket.send(yield createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
722
+ state = {
723
+ __type: 'hello_req_sent',
724
+ associationPublicKey: associationKeypair.publicKey,
725
+ ecdhPrivateKey: ecdhKeypair.privateKey,
726
+ };
727
+ break;
728
+ }
654
729
  const sharedSecret = yield parseHelloRsp(responseBuffer, state.associationPublicKey, state.ecdhPrivateKey);
655
730
  const sessionPropertiesBuffer = responseBuffer.slice(ENCODED_PUBLIC_KEY_LENGTH_BYTES);
656
731
  const sessionProperties = sessionPropertiesBuffer.byteLength !== 0
@@ -738,6 +813,191 @@ function transact(callback, config) {
738
813
  });
739
814
  });
740
815
  }
816
+ function transactRemote(callback, config) {
817
+ return __awaiter(this, void 0, void 0, function* () {
818
+ assertSecureContext();
819
+ const associationKeypair = yield generateAssociationKeypair();
820
+ const { associationUrl, reflectorId } = yield getRemoteSessionUrl(associationKeypair.publicKey, config.remoteHostAuthority, config === null || config === void 0 ? void 0 : config.baseUri);
821
+ const websocketURL = `wss://${config === null || config === void 0 ? void 0 : config.remoteHostAuthority}/reflect?id=${reflectorId}`;
822
+ let connectionStartTime;
823
+ const getNextRetryDelayMs = (() => {
824
+ const schedule = [...WEBSOCKET_CONNECTION_CONFIG.retryDelayScheduleMs];
825
+ return () => (schedule.length > 1 ? schedule.shift() : schedule[0]);
826
+ })();
827
+ let nextJsonRpcMessageId = 1;
828
+ let lastKnownInboundSequenceNumber = 0;
829
+ let state = { __type: 'disconnected' };
830
+ return { associationUrl, result: new Promise((resolve, reject) => {
831
+ let socket;
832
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
833
+ const jsonRpcResponsePromises = {};
834
+ const handleOpen = () => __awaiter(this, void 0, void 0, function* () {
835
+ if (state.__type !== 'connecting') {
836
+ console.warn('Expected adapter state to be `connecting` at the moment the websocket opens. ' +
837
+ `Got \`${state.__type}\`.`);
838
+ return;
839
+ }
840
+ socket.removeEventListener('open', handleOpen);
841
+ });
842
+ const handleClose = (evt) => {
843
+ if (evt.wasClean) {
844
+ state = { __type: 'disconnected' };
845
+ }
846
+ else {
847
+ reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_CLOSED, `The wallet session dropped unexpectedly (${evt.code}: ${evt.reason}).`, { closeEvent: evt }));
848
+ }
849
+ disposeSocket();
850
+ };
851
+ const handleError = (_evt) => __awaiter(this, void 0, void 0, function* () {
852
+ disposeSocket();
853
+ if (Date.now() - connectionStartTime >= WEBSOCKET_CONNECTION_CONFIG.timeoutMs) {
854
+ reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_TIMEOUT, `Failed to connect to the wallet websocket at ${websocketURL}.`));
855
+ }
856
+ else {
857
+ yield new Promise((resolve) => {
858
+ const retryDelayMs = getNextRetryDelayMs();
859
+ retryWaitTimeoutId = window.setTimeout(resolve, retryDelayMs);
860
+ });
861
+ attemptSocketConnection();
862
+ }
863
+ });
864
+ const handleMessage = (evt) => __awaiter(this, void 0, void 0, function* () {
865
+ const responseBuffer = yield evt.data.arrayBuffer();
866
+ switch (state.__type) {
867
+ case 'connecting':
868
+ if (responseBuffer.byteLength !== 0) {
869
+ throw new Error('Encountered unexpected message while connecting');
870
+ }
871
+ const ecdhKeypair = yield generateECDHKeypair();
872
+ socket.send(yield createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
873
+ state = {
874
+ __type: 'hello_req_sent',
875
+ associationPublicKey: associationKeypair.publicKey,
876
+ ecdhPrivateKey: ecdhKeypair.privateKey,
877
+ };
878
+ break;
879
+ case 'connected':
880
+ try {
881
+ const sequenceNumberVector = responseBuffer.slice(0, SEQUENCE_NUMBER_BYTES);
882
+ const sequenceNumber = getSequenceNumberFromByteArray(sequenceNumberVector);
883
+ if (sequenceNumber !== (lastKnownInboundSequenceNumber + 1)) {
884
+ throw new Error('Encrypted message has invalid sequence number');
885
+ }
886
+ lastKnownInboundSequenceNumber = sequenceNumber;
887
+ const jsonRpcMessage = yield decryptJsonRpcMessage(responseBuffer, state.sharedSecret);
888
+ const responsePromise = jsonRpcResponsePromises[jsonRpcMessage.id];
889
+ delete jsonRpcResponsePromises[jsonRpcMessage.id];
890
+ responsePromise.resolve(jsonRpcMessage.result);
891
+ }
892
+ catch (e) {
893
+ if (e instanceof SolanaMobileWalletAdapterProtocolError) {
894
+ const responsePromise = jsonRpcResponsePromises[e.jsonRpcMessageId];
895
+ delete jsonRpcResponsePromises[e.jsonRpcMessageId];
896
+ responsePromise.reject(e);
897
+ }
898
+ else {
899
+ throw e;
900
+ }
901
+ }
902
+ break;
903
+ case 'hello_req_sent': {
904
+ const sharedSecret = yield parseHelloRsp(responseBuffer, state.associationPublicKey, state.ecdhPrivateKey);
905
+ const sessionPropertiesBuffer = responseBuffer.slice(ENCODED_PUBLIC_KEY_LENGTH_BYTES);
906
+ const sessionProperties = sessionPropertiesBuffer.byteLength !== 0
907
+ ? yield (() => __awaiter(this, void 0, void 0, function* () {
908
+ const sequenceNumberVector = sessionPropertiesBuffer.slice(0, SEQUENCE_NUMBER_BYTES);
909
+ const sequenceNumber = getSequenceNumberFromByteArray(sequenceNumberVector);
910
+ if (sequenceNumber !== (lastKnownInboundSequenceNumber + 1)) {
911
+ throw new Error('Encrypted message has invalid sequence number');
912
+ }
913
+ lastKnownInboundSequenceNumber = sequenceNumber;
914
+ return parseSessionProps(sessionPropertiesBuffer, sharedSecret);
915
+ }))() : { protocol_version: 'legacy' };
916
+ state = { __type: 'connected', sharedSecret, sessionProperties };
917
+ const wallet = createMobileWalletProxy(sessionProperties.protocol_version, (method, params) => __awaiter(this, void 0, void 0, function* () {
918
+ const id = nextJsonRpcMessageId++;
919
+ socket.send(yield encryptJsonRpcMessage({
920
+ id,
921
+ jsonrpc: '2.0',
922
+ method,
923
+ params: params !== null && params !== void 0 ? params : {},
924
+ }, sharedSecret));
925
+ return new Promise((resolve, reject) => {
926
+ jsonRpcResponsePromises[id] = {
927
+ resolve(result) {
928
+ switch (method) {
929
+ case 'authorize':
930
+ case 'reauthorize': {
931
+ const { wallet_uri_base } = result;
932
+ if (wallet_uri_base != null) {
933
+ try {
934
+ assertSecureEndpointSpecificURI(wallet_uri_base);
935
+ }
936
+ catch (e) {
937
+ reject(e);
938
+ return;
939
+ }
940
+ }
941
+ break;
942
+ }
943
+ }
944
+ resolve(result);
945
+ },
946
+ reject,
947
+ };
948
+ });
949
+ }));
950
+ try {
951
+ resolve(yield callback(new Proxy(wallet, {
952
+ get(target, p) {
953
+ if (p == 'terminateSession') {
954
+ return function () {
955
+ return __awaiter(this, void 0, void 0, function* () {
956
+ disposeSocket();
957
+ socket.close();
958
+ return;
959
+ });
960
+ };
961
+ }
962
+ else
963
+ return target[p];
964
+ },
965
+ })));
966
+ }
967
+ catch (e) {
968
+ reject(e);
969
+ }
970
+ break;
971
+ }
972
+ }
973
+ });
974
+ let disposeSocket;
975
+ let retryWaitTimeoutId;
976
+ const attemptSocketConnection = () => {
977
+ if (disposeSocket) {
978
+ disposeSocket();
979
+ }
980
+ state = { __type: 'connecting', associationKeypair };
981
+ if (connectionStartTime === undefined) {
982
+ connectionStartTime = Date.now();
983
+ }
984
+ socket = new WebSocket(websocketURL, [WEBSOCKET_PROTOCOL]);
985
+ socket.addEventListener('open', handleOpen);
986
+ socket.addEventListener('close', handleClose);
987
+ socket.addEventListener('error', handleError);
988
+ socket.addEventListener('message', handleMessage);
989
+ disposeSocket = () => {
990
+ window.clearTimeout(retryWaitTimeoutId);
991
+ socket.removeEventListener('open', handleOpen);
992
+ socket.removeEventListener('close', handleClose);
993
+ socket.removeEventListener('error', handleError);
994
+ socket.removeEventListener('message', handleMessage);
995
+ };
996
+ };
997
+ attemptSocketConnection();
998
+ }) };
999
+ });
1000
+ }
741
1001
 
742
1002
  exports.SolanaCloneAuthorization = SolanaCloneAuthorization;
743
1003
  exports.SolanaMobileWalletAdapterError = SolanaMobileWalletAdapterError;
@@ -747,3 +1007,4 @@ exports.SolanaMobileWalletAdapterProtocolErrorCode = SolanaMobileWalletAdapterPr
747
1007
  exports.SolanaSignInWithSolana = SolanaSignInWithSolana;
748
1008
  exports.SolanaSignTransactions = SolanaSignTransactions;
749
1009
  exports.transact = transact;
1010
+ exports.transactRemote = transactRemote;
@@ -9,6 +9,7 @@ var jsBase64 = require('js-base64');
9
9
  // Typescript `enums` thwart tree-shaking. See https://bargsten.org/jsts/enums/
10
10
  const SolanaMobileWalletAdapterErrorCode = {
11
11
  ERROR_ASSOCIATION_PORT_OUT_OF_RANGE: 'ERROR_ASSOCIATION_PORT_OUT_OF_RANGE',
12
+ ERROR_REFLECTOR_ID_OUT_OF_RANGE: 'ERROR_REFLECTOR_ID_OUT_OF_RANGE',
12
13
  ERROR_FORBIDDEN_WALLET_BASE_URL: 'ERROR_FORBIDDEN_WALLET_BASE_URL',
13
14
  ERROR_SECURE_CONTEXT_REQUIRED: 'ERROR_SECURE_CONTEXT_REQUIRED',
14
15
  ERROR_SESSION_CLOSED: 'ERROR_SESSION_CLOSED',
@@ -71,6 +72,8 @@ function __awaiter(thisArg, _arguments, P, generator) {
71
72
  });
72
73
  }
73
74
 
75
+ var NativeSolanaMobileWalletAdapter = reactNative.TurboModuleRegistry.getEnforcing('SolanaMobileWalletAdapter');
76
+
74
77
  function createSIWSMessage(payload) {
75
78
  return walletStandardUtil.createSignInMessageText(payload);
76
79
  }
@@ -242,8 +245,8 @@ const LINKING_ERROR = `The package 'solana-mobile-wallet-adapter-protocol' doesn
242
245
  ' - You have added `@solana-mobile/mobile-wallet-adapter-protocol` as an explicit dependency, and\n' +
243
246
  ' - You have added `@solana-mobile/mobile-wallet-adapter-protocol` to the `nohoist` section of your package.json\n' +
244
247
  '- You are not using Expo managed workflow\n';
245
- const SolanaMobileWalletAdapter = reactNative.Platform.OS === 'android' && reactNative.NativeModules.SolanaMobileWalletAdapter
246
- ? reactNative.NativeModules.SolanaMobileWalletAdapter
248
+ const SolanaMobileWalletAdapter = reactNative.Platform.OS === 'android' && NativeSolanaMobileWalletAdapter
249
+ ? NativeSolanaMobileWalletAdapter
247
250
  : new Proxy({}, {
248
251
  get() {
249
252
  throw new Error(reactNative.Platform.OS !== 'android'