@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
@@ -3,6 +3,7 @@ import { createSignInMessageText } from '@solana/wallet-standard-util';
3
3
  // Typescript `enums` thwart tree-shaking. See https://bargsten.org/jsts/enums/
4
4
  const SolanaMobileWalletAdapterErrorCode = {
5
5
  ERROR_ASSOCIATION_PORT_OUT_OF_RANGE: 'ERROR_ASSOCIATION_PORT_OUT_OF_RANGE',
6
+ ERROR_REFLECTOR_ID_OUT_OF_RANGE: 'ERROR_REFLECTOR_ID_OUT_OF_RANGE',
6
7
  ERROR_FORBIDDEN_WALLET_BASE_URL: 'ERROR_FORBIDDEN_WALLET_BASE_URL',
7
8
  ERROR_SECURE_CONTEXT_REQUIRED: 'ERROR_SECURE_CONTEXT_REQUIRED',
8
9
  ERROR_SESSION_CLOSED: 'ERROR_SESSION_CLOSED',
@@ -406,6 +407,24 @@ function getStringWithURLUnsafeCharactersReplaced(unsafeBase64EncodedString) {
406
407
  }[m]));
407
408
  }
408
409
 
410
+ function getRandomReflectorId() {
411
+ return assertReflectorId(getRandomInt(0, 9007199254740991)); // 0 < id < 2^53 - 1
412
+ }
413
+ function getRandomInt(min, max) {
414
+ const randomBuffer = new Uint32Array(1);
415
+ window.crypto.getRandomValues(randomBuffer);
416
+ let randomNumber = randomBuffer[0] / (0xffffffff + 1);
417
+ min = Math.ceil(min);
418
+ max = Math.floor(max);
419
+ return Math.floor(randomNumber * (max - min + 1)) + min;
420
+ }
421
+ function assertReflectorId(id) {
422
+ if (id < 0 || id > 9007199254740991) { // 0 < id < 2^53 - 1
423
+ throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_REFLECTOR_ID_OUT_OF_RANGE, `Association port number must be between 49152 and 65535. ${id} given.`, { id });
424
+ }
425
+ return id;
426
+ }
427
+
409
428
  const INTENT_NAME = 'solana-wallet';
410
429
  function getPathParts(pathString) {
411
430
  return (pathString
@@ -447,6 +466,21 @@ function getAssociateAndroidIntentURL(associationPublicKey, putativePort, associ
447
466
  return url;
448
467
  });
449
468
  }
469
+ function getRemoteAssociateAndroidIntentURL(associationPublicKey, hostAuthority, putativeId, associationURLBase, protocolVersions = ['v1']) {
470
+ return __awaiter(this, void 0, void 0, function* () {
471
+ const reflectorId = assertReflectorId(putativeId);
472
+ const exportedKey = yield crypto.subtle.exportKey('raw', associationPublicKey);
473
+ const encodedKey = arrayBufferToBase64String(exportedKey);
474
+ const url = getIntentURL('v1/associate/remote', associationURLBase);
475
+ url.searchParams.set('association', getStringWithURLUnsafeCharactersReplaced(encodedKey));
476
+ url.searchParams.set('reflector', `${hostAuthority}`);
477
+ url.searchParams.set('id', `${reflectorId}`);
478
+ protocolVersions.forEach((version) => {
479
+ url.searchParams.set('v', version);
480
+ });
481
+ return url;
482
+ });
483
+ }
450
484
 
451
485
  // Typescript `enums` thwart tree-shaking. See https://bargsten.org/jsts/enums/
452
486
  const Browser = {
@@ -489,10 +523,8 @@ function launchUrlThroughHiddenFrame(url) {
489
523
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
490
524
  _frame.contentWindow.location.href = url.toString();
491
525
  }
492
- function startSession(associationPublicKey, associationURLBase) {
526
+ function launchAssociation(associationUrl) {
493
527
  return __awaiter(this, void 0, void 0, function* () {
494
- const randomAssociationPort = getRandomAssociationPort();
495
- const associationUrl = yield getAssociateAndroidIntentURL(associationPublicKey, randomAssociationPort, associationURLBase);
496
528
  if (associationUrl.protocol === 'https:') {
497
529
  // The association URL is an Android 'App Link' or iOS 'Universal Link'.
498
530
  // These are regular web URLs that are designed to launch an app if it
@@ -523,9 +555,23 @@ function startSession(associationPublicKey, associationURLBase) {
523
555
  throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND, 'Found no installed wallet that supports the mobile wallet protocol.');
524
556
  }
525
557
  }
558
+ });
559
+ }
560
+ function startSession(associationPublicKey, associationURLBase) {
561
+ return __awaiter(this, void 0, void 0, function* () {
562
+ const randomAssociationPort = getRandomAssociationPort();
563
+ const associationUrl = yield getAssociateAndroidIntentURL(associationPublicKey, randomAssociationPort, associationURLBase);
564
+ yield launchAssociation(associationUrl);
526
565
  return randomAssociationPort;
527
566
  });
528
567
  }
568
+ function getRemoteSessionUrl(associationPublicKey, hostAuthority, associationURLBase) {
569
+ return __awaiter(this, void 0, void 0, function* () {
570
+ const randomReflectorId = getRandomReflectorId();
571
+ const associationUrl = yield getRemoteAssociateAndroidIntentURL(associationPublicKey, hostAuthority, randomReflectorId, associationURLBase);
572
+ return { associationUrl, reflectorId: randomReflectorId };
573
+ });
574
+ }
529
575
 
530
576
  const WEBSOCKET_CONNECTION_CONFIG = {
531
577
  /**
@@ -587,8 +633,14 @@ function transact(callback, config) {
587
633
  `Got \`${state.__type}\`.`);
588
634
  return;
589
635
  }
590
- const { associationKeypair } = state;
591
636
  socket.removeEventListener('open', handleOpen);
637
+ // previous versions of this library and walletlib incorrectly implemented the MWA session
638
+ // establishment protocol for local connections. The dapp is supposed to wait for the
639
+ // APP_PING message before sending the HELLO_REQ. Instead, the dapp was sending the HELLO_REQ
640
+ // immediately upon connection to the websocket server regardless of wether or not an
641
+ // APP_PING was sent by the wallet/websocket server. We must continue to support this behavior
642
+ // in case the user is using a wallet that has not updated their walletlib implementation.
643
+ const { associationKeypair } = state;
592
644
  const ecdhKeypair = yield generateECDHKeypair();
593
645
  socket.send(yield createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
594
646
  state = {
@@ -609,7 +661,7 @@ function transact(callback, config) {
609
661
  const handleError = (_evt) => __awaiter(this, void 0, void 0, function* () {
610
662
  disposeSocket();
611
663
  if (Date.now() - connectionStartTime >= WEBSOCKET_CONNECTION_CONFIG.timeoutMs) {
612
- reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_TIMEOUT, `Failed to connect to the wallet websocket on port ${sessionPort}.`));
664
+ reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_TIMEOUT, `Failed to connect to the wallet websocket at ${websocketURL}.`));
613
665
  }
614
666
  else {
615
667
  yield new Promise((resolve) => {
@@ -622,6 +674,18 @@ function transact(callback, config) {
622
674
  const handleMessage = (evt) => __awaiter(this, void 0, void 0, function* () {
623
675
  const responseBuffer = yield evt.data.arrayBuffer();
624
676
  switch (state.__type) {
677
+ case 'connecting':
678
+ if (responseBuffer.byteLength !== 0) {
679
+ throw new Error('Encountered unexpected message while connecting');
680
+ }
681
+ const ecdhKeypair = yield generateECDHKeypair();
682
+ socket.send(yield createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
683
+ state = {
684
+ __type: 'hello_req_sent',
685
+ associationPublicKey: associationKeypair.publicKey,
686
+ ecdhPrivateKey: ecdhKeypair.privateKey,
687
+ };
688
+ break;
625
689
  case 'connected':
626
690
  try {
627
691
  const sequenceNumberVector = responseBuffer.slice(0, SEQUENCE_NUMBER_BYTES);
@@ -647,6 +711,17 @@ function transact(callback, config) {
647
711
  }
648
712
  break;
649
713
  case 'hello_req_sent': {
714
+ // if we receive an APP_PING message (empty message), resend the HELLO_REQ (see above)
715
+ if (responseBuffer.byteLength === 0) {
716
+ const ecdhKeypair = yield generateECDHKeypair();
717
+ socket.send(yield createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
718
+ state = {
719
+ __type: 'hello_req_sent',
720
+ associationPublicKey: associationKeypair.publicKey,
721
+ ecdhPrivateKey: ecdhKeypair.privateKey,
722
+ };
723
+ break;
724
+ }
650
725
  const sharedSecret = yield parseHelloRsp(responseBuffer, state.associationPublicKey, state.ecdhPrivateKey);
651
726
  const sessionPropertiesBuffer = responseBuffer.slice(ENCODED_PUBLIC_KEY_LENGTH_BYTES);
652
727
  const sessionProperties = sessionPropertiesBuffer.byteLength !== 0
@@ -734,5 +809,190 @@ function transact(callback, config) {
734
809
  });
735
810
  });
736
811
  }
812
+ function transactRemote(callback, config) {
813
+ return __awaiter(this, void 0, void 0, function* () {
814
+ assertSecureContext();
815
+ const associationKeypair = yield generateAssociationKeypair();
816
+ const { associationUrl, reflectorId } = yield getRemoteSessionUrl(associationKeypair.publicKey, config.remoteHostAuthority, config === null || config === void 0 ? void 0 : config.baseUri);
817
+ const websocketURL = `wss://${config === null || config === void 0 ? void 0 : config.remoteHostAuthority}/reflect?id=${reflectorId}`;
818
+ let connectionStartTime;
819
+ const getNextRetryDelayMs = (() => {
820
+ const schedule = [...WEBSOCKET_CONNECTION_CONFIG.retryDelayScheduleMs];
821
+ return () => (schedule.length > 1 ? schedule.shift() : schedule[0]);
822
+ })();
823
+ let nextJsonRpcMessageId = 1;
824
+ let lastKnownInboundSequenceNumber = 0;
825
+ let state = { __type: 'disconnected' };
826
+ return { associationUrl, result: new Promise((resolve, reject) => {
827
+ let socket;
828
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
829
+ const jsonRpcResponsePromises = {};
830
+ const handleOpen = () => __awaiter(this, void 0, void 0, function* () {
831
+ if (state.__type !== 'connecting') {
832
+ console.warn('Expected adapter state to be `connecting` at the moment the websocket opens. ' +
833
+ `Got \`${state.__type}\`.`);
834
+ return;
835
+ }
836
+ socket.removeEventListener('open', handleOpen);
837
+ });
838
+ const handleClose = (evt) => {
839
+ if (evt.wasClean) {
840
+ state = { __type: 'disconnected' };
841
+ }
842
+ else {
843
+ reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_CLOSED, `The wallet session dropped unexpectedly (${evt.code}: ${evt.reason}).`, { closeEvent: evt }));
844
+ }
845
+ disposeSocket();
846
+ };
847
+ const handleError = (_evt) => __awaiter(this, void 0, void 0, function* () {
848
+ disposeSocket();
849
+ if (Date.now() - connectionStartTime >= WEBSOCKET_CONNECTION_CONFIG.timeoutMs) {
850
+ reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_TIMEOUT, `Failed to connect to the wallet websocket at ${websocketURL}.`));
851
+ }
852
+ else {
853
+ yield new Promise((resolve) => {
854
+ const retryDelayMs = getNextRetryDelayMs();
855
+ retryWaitTimeoutId = window.setTimeout(resolve, retryDelayMs);
856
+ });
857
+ attemptSocketConnection();
858
+ }
859
+ });
860
+ const handleMessage = (evt) => __awaiter(this, void 0, void 0, function* () {
861
+ const responseBuffer = yield evt.data.arrayBuffer();
862
+ switch (state.__type) {
863
+ case 'connecting':
864
+ if (responseBuffer.byteLength !== 0) {
865
+ throw new Error('Encountered unexpected message while connecting');
866
+ }
867
+ const ecdhKeypair = yield generateECDHKeypair();
868
+ socket.send(yield createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
869
+ state = {
870
+ __type: 'hello_req_sent',
871
+ associationPublicKey: associationKeypair.publicKey,
872
+ ecdhPrivateKey: ecdhKeypair.privateKey,
873
+ };
874
+ break;
875
+ case 'connected':
876
+ try {
877
+ const sequenceNumberVector = responseBuffer.slice(0, SEQUENCE_NUMBER_BYTES);
878
+ const sequenceNumber = getSequenceNumberFromByteArray(sequenceNumberVector);
879
+ if (sequenceNumber !== (lastKnownInboundSequenceNumber + 1)) {
880
+ throw new Error('Encrypted message has invalid sequence number');
881
+ }
882
+ lastKnownInboundSequenceNumber = sequenceNumber;
883
+ const jsonRpcMessage = yield decryptJsonRpcMessage(responseBuffer, state.sharedSecret);
884
+ const responsePromise = jsonRpcResponsePromises[jsonRpcMessage.id];
885
+ delete jsonRpcResponsePromises[jsonRpcMessage.id];
886
+ responsePromise.resolve(jsonRpcMessage.result);
887
+ }
888
+ catch (e) {
889
+ if (e instanceof SolanaMobileWalletAdapterProtocolError) {
890
+ const responsePromise = jsonRpcResponsePromises[e.jsonRpcMessageId];
891
+ delete jsonRpcResponsePromises[e.jsonRpcMessageId];
892
+ responsePromise.reject(e);
893
+ }
894
+ else {
895
+ throw e;
896
+ }
897
+ }
898
+ break;
899
+ case 'hello_req_sent': {
900
+ const sharedSecret = yield parseHelloRsp(responseBuffer, state.associationPublicKey, state.ecdhPrivateKey);
901
+ const sessionPropertiesBuffer = responseBuffer.slice(ENCODED_PUBLIC_KEY_LENGTH_BYTES);
902
+ const sessionProperties = sessionPropertiesBuffer.byteLength !== 0
903
+ ? yield (() => __awaiter(this, void 0, void 0, function* () {
904
+ const sequenceNumberVector = sessionPropertiesBuffer.slice(0, SEQUENCE_NUMBER_BYTES);
905
+ const sequenceNumber = getSequenceNumberFromByteArray(sequenceNumberVector);
906
+ if (sequenceNumber !== (lastKnownInboundSequenceNumber + 1)) {
907
+ throw new Error('Encrypted message has invalid sequence number');
908
+ }
909
+ lastKnownInboundSequenceNumber = sequenceNumber;
910
+ return parseSessionProps(sessionPropertiesBuffer, sharedSecret);
911
+ }))() : { protocol_version: 'legacy' };
912
+ state = { __type: 'connected', sharedSecret, sessionProperties };
913
+ const wallet = createMobileWalletProxy(sessionProperties.protocol_version, (method, params) => __awaiter(this, void 0, void 0, function* () {
914
+ const id = nextJsonRpcMessageId++;
915
+ socket.send(yield encryptJsonRpcMessage({
916
+ id,
917
+ jsonrpc: '2.0',
918
+ method,
919
+ params: params !== null && params !== void 0 ? params : {},
920
+ }, sharedSecret));
921
+ return new Promise((resolve, reject) => {
922
+ jsonRpcResponsePromises[id] = {
923
+ resolve(result) {
924
+ switch (method) {
925
+ case 'authorize':
926
+ case 'reauthorize': {
927
+ const { wallet_uri_base } = result;
928
+ if (wallet_uri_base != null) {
929
+ try {
930
+ assertSecureEndpointSpecificURI(wallet_uri_base);
931
+ }
932
+ catch (e) {
933
+ reject(e);
934
+ return;
935
+ }
936
+ }
937
+ break;
938
+ }
939
+ }
940
+ resolve(result);
941
+ },
942
+ reject,
943
+ };
944
+ });
945
+ }));
946
+ try {
947
+ resolve(yield callback(new Proxy(wallet, {
948
+ get(target, p) {
949
+ if (p == 'terminateSession') {
950
+ return function () {
951
+ return __awaiter(this, void 0, void 0, function* () {
952
+ disposeSocket();
953
+ socket.close();
954
+ return;
955
+ });
956
+ };
957
+ }
958
+ else
959
+ return target[p];
960
+ },
961
+ })));
962
+ }
963
+ catch (e) {
964
+ reject(e);
965
+ }
966
+ break;
967
+ }
968
+ }
969
+ });
970
+ let disposeSocket;
971
+ let retryWaitTimeoutId;
972
+ const attemptSocketConnection = () => {
973
+ if (disposeSocket) {
974
+ disposeSocket();
975
+ }
976
+ state = { __type: 'connecting', associationKeypair };
977
+ if (connectionStartTime === undefined) {
978
+ connectionStartTime = Date.now();
979
+ }
980
+ socket = new WebSocket(websocketURL, [WEBSOCKET_PROTOCOL]);
981
+ socket.addEventListener('open', handleOpen);
982
+ socket.addEventListener('close', handleClose);
983
+ socket.addEventListener('error', handleError);
984
+ socket.addEventListener('message', handleMessage);
985
+ disposeSocket = () => {
986
+ window.clearTimeout(retryWaitTimeoutId);
987
+ socket.removeEventListener('open', handleOpen);
988
+ socket.removeEventListener('close', handleClose);
989
+ socket.removeEventListener('error', handleError);
990
+ socket.removeEventListener('message', handleMessage);
991
+ };
992
+ };
993
+ attemptSocketConnection();
994
+ }) };
995
+ });
996
+ }
737
997
 
738
- export { SolanaCloneAuthorization, SolanaMobileWalletAdapterError, SolanaMobileWalletAdapterErrorCode, SolanaMobileWalletAdapterProtocolError, SolanaMobileWalletAdapterProtocolErrorCode, SolanaSignInWithSolana, SolanaSignTransactions, transact };
998
+ export { SolanaCloneAuthorization, SolanaMobileWalletAdapterError, SolanaMobileWalletAdapterErrorCode, SolanaMobileWalletAdapterProtocolError, SolanaMobileWalletAdapterProtocolErrorCode, SolanaSignInWithSolana, SolanaSignTransactions, transact, transactRemote };