@tonconnect/sdk 3.0.0 → 3.0.1-beta.1

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.
package/lib/cjs/index.cjs CHANGED
@@ -346,6 +346,217 @@ function encodeTelegramUrlParameters(parameters) {
346
346
  .replaceAll('%', '--');
347
347
  }
348
348
 
349
+ /**
350
+ * Delays the execution of code for a specified number of milliseconds.
351
+ * @param {number} timeout - The number of milliseconds to delay the execution.
352
+ * @param {DelayOptions} [options] - Optional configuration options for the delay.
353
+ * @return {Promise<void>} - A promise that resolves after the specified delay, or rejects if the delay is aborted.
354
+ */
355
+ function delay(timeout, options) {
356
+ return __awaiter(this, void 0, void 0, function* () {
357
+ return new Promise((resolve, reject) => {
358
+ var _a, _b;
359
+ if ((_a = options === null || options === void 0 ? void 0 : options.signal) === null || _a === void 0 ? void 0 : _a.aborted) {
360
+ reject(new TonConnectError('Delay aborted'));
361
+ return;
362
+ }
363
+ const timeoutId = setTimeout(() => resolve(), timeout);
364
+ (_b = options === null || options === void 0 ? void 0 : options.signal) === null || _b === void 0 ? void 0 : _b.addEventListener('abort', () => {
365
+ clearTimeout(timeoutId);
366
+ reject(new TonConnectError('Delay aborted'));
367
+ });
368
+ });
369
+ });
370
+ }
371
+
372
+ /**
373
+ * Creates an AbortController instance with an optional AbortSignal.
374
+ *
375
+ * @param {AbortSignal} [signal] - An optional AbortSignal to use for aborting the controller.
376
+ * @returns {AbortController} - An instance of AbortController.
377
+ */
378
+ function createAbortController(signal) {
379
+ const abortController = new AbortController();
380
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
381
+ abortController.abort();
382
+ }
383
+ else {
384
+ signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', () => abortController.abort(), { once: true });
385
+ }
386
+ return abortController;
387
+ }
388
+
389
+ /**
390
+ * Function to call ton api until we get response.
391
+ * Because ton network is pretty unstable we need to make sure response is final.
392
+ * @param {T} fn - function to call
393
+ * @param {CallForSuccessOptions} [options] - optional configuration options
394
+ */
395
+ function callForSuccess(fn, options) {
396
+ var _a, _b;
397
+ return __awaiter(this, void 0, void 0, function* () {
398
+ const attempts = (_a = options === null || options === void 0 ? void 0 : options.attempts) !== null && _a !== void 0 ? _a : 10;
399
+ const delayMs = (_b = options === null || options === void 0 ? void 0 : options.delayMs) !== null && _b !== void 0 ? _b : 200;
400
+ const abortController = createAbortController(options === null || options === void 0 ? void 0 : options.signal);
401
+ if (typeof fn !== 'function') {
402
+ throw new TonConnectError(`Expected a function, got ${typeof fn}`);
403
+ }
404
+ let i = 0;
405
+ let lastError;
406
+ while (i < attempts) {
407
+ if (abortController.signal.aborted) {
408
+ throw new TonConnectError(`Aborted after attempts ${i}`);
409
+ }
410
+ try {
411
+ return yield fn({ signal: abortController.signal });
412
+ }
413
+ catch (err) {
414
+ lastError = err;
415
+ i++;
416
+ if (i < attempts) {
417
+ yield delay(delayMs);
418
+ }
419
+ }
420
+ }
421
+ throw lastError;
422
+ });
423
+ }
424
+
425
+ function logDebug(...args) {
426
+ {
427
+ try {
428
+ console.debug('[TON_CONNECT_SDK]', ...args);
429
+ }
430
+ catch (_a) { }
431
+ }
432
+ }
433
+ function logError(...args) {
434
+ {
435
+ try {
436
+ console.error('[TON_CONNECT_SDK]', ...args);
437
+ }
438
+ catch (_a) { }
439
+ }
440
+ }
441
+ function logWarning(...args) {
442
+ {
443
+ try {
444
+ console.warn('[TON_CONNECT_SDK]', ...args);
445
+ }
446
+ catch (_a) { }
447
+ }
448
+ }
449
+
450
+ /**
451
+ * Create a resource.
452
+ *
453
+ * @template T - The type of the resource.
454
+ * @template Args - The type of the arguments for creating the resource.
455
+ *
456
+ * @param {(...args: Args) => Promise<T>} createFn - A function that creates the resource.
457
+ * @param {(resource: T) => Promise<void>} [disposeFn] - An optional function that disposes the resource.
458
+ */
459
+ function createResource(createFn, disposeFn) {
460
+ let currentResource = null;
461
+ let currentArgs = null;
462
+ let currentPromise = null;
463
+ let currentSignal = null;
464
+ let abortController = null;
465
+ // create a new resource
466
+ const create = (signal, ...args) => __awaiter(this, void 0, void 0, function* () {
467
+ currentSignal = signal !== null && signal !== void 0 ? signal : null;
468
+ abortController === null || abortController === void 0 ? void 0 : abortController.abort();
469
+ abortController = createAbortController(signal);
470
+ if (abortController.signal.aborted) {
471
+ throw new TonConnectError('Resource creation was aborted');
472
+ }
473
+ currentArgs = args !== null && args !== void 0 ? args : null;
474
+ const promise = createFn(signal, ...args);
475
+ currentPromise = promise;
476
+ const resource = yield promise;
477
+ if (currentPromise !== promise) {
478
+ yield disposeFn(resource);
479
+ throw new TonConnectError('Resource creation was aborted by a new resource creation');
480
+ }
481
+ currentResource = resource;
482
+ return currentResource;
483
+ });
484
+ // get the current resource
485
+ const current = () => {
486
+ return currentResource !== null && currentResource !== void 0 ? currentResource : null;
487
+ };
488
+ // dispose the current resource
489
+ const dispose = () => __awaiter(this, void 0, void 0, function* () {
490
+ try {
491
+ const resource = currentResource;
492
+ currentResource = null;
493
+ const promise = currentPromise;
494
+ currentPromise = null;
495
+ abortController === null || abortController === void 0 ? void 0 : abortController.abort();
496
+ yield Promise.allSettled([
497
+ resource ? disposeFn(resource) : Promise.resolve(),
498
+ promise ? disposeFn(yield promise) : Promise.resolve()
499
+ ]);
500
+ }
501
+ catch (e) {
502
+ logError('Failed to dispose the resource', e);
503
+ }
504
+ });
505
+ // recreate the current resource
506
+ const recreate = (delayMs) => __awaiter(this, void 0, void 0, function* () {
507
+ const resource = currentResource;
508
+ const promise = currentPromise;
509
+ const args = currentArgs;
510
+ const signal = currentSignal;
511
+ yield delay(delayMs);
512
+ if (resource === currentResource &&
513
+ promise === currentPromise &&
514
+ args === currentArgs &&
515
+ signal === currentSignal) {
516
+ return create(currentSignal, ...(args !== null && args !== void 0 ? args : []));
517
+ }
518
+ throw new TonConnectError('Resource recreation was aborted by a new resource creation');
519
+ });
520
+ return {
521
+ create,
522
+ current,
523
+ dispose,
524
+ recreate
525
+ };
526
+ }
527
+
528
+ /**
529
+ * Executes a function and provides deferred behavior, allowing for a timeout and abort functionality.
530
+ *
531
+ * @param {Deferrable<T>} fn - The function to execute. It should return a promise that resolves with the desired result.
532
+ * @param {DeferOptions} options - Optional configuration options for the defer behavior.
533
+ * @returns {Promise<T>} - A promise that resolves with the result of the executed function, or rejects with an error if it times out or is aborted.
534
+ */
535
+ function timeout(fn, options) {
536
+ const timeout = options === null || options === void 0 ? void 0 : options.timeout;
537
+ const signal = options === null || options === void 0 ? void 0 : options.signal;
538
+ const abortController = createAbortController(signal);
539
+ return new Promise((resolve, reject) => {
540
+ if (abortController.signal.aborted) {
541
+ reject(new TonConnectError('Operation aborted'));
542
+ return;
543
+ }
544
+ let timeoutId;
545
+ if (typeof timeout !== 'undefined') {
546
+ timeoutId = setTimeout(() => {
547
+ abortController.abort();
548
+ reject(new TonConnectError(`Timeout after ${timeout}ms`));
549
+ }, timeout);
550
+ }
551
+ abortController.signal.addEventListener('abort', () => {
552
+ clearTimeout(timeoutId);
553
+ reject(new TonConnectError('Operation aborted'));
554
+ }, { once: true });
555
+ const deferOptions = { timeout, abort: abortController.signal };
556
+ fn(resolve, reject, deferOptions).finally(() => clearTimeout(timeoutId));
557
+ });
558
+ }
559
+
349
560
  class BridgeGateway {
350
561
  constructor(storage, bridgeUrl, sessionId, listener, errorsListener) {
351
562
  this.bridgeUrl = bridgeUrl;
@@ -356,67 +567,85 @@ class BridgeGateway {
356
567
  this.postPath = 'message';
357
568
  this.heartbeatMessage = 'heartbeat';
358
569
  this.defaultTtl = 300;
359
- this.isClosed = false;
570
+ this.defaultReconnectDelay = 5000;
571
+ this.eventSource = createResource((signal, openingDeadlineMS) => __awaiter(this, void 0, void 0, function* () {
572
+ const eventSourceConfig = {
573
+ bridgeUrl: this.bridgeUrl,
574
+ ssePath: this.ssePath,
575
+ sessionId: this.sessionId,
576
+ bridgeGatewayStorage: this.bridgeGatewayStorage,
577
+ errorHandler: this.errorsHandler.bind(this),
578
+ messageHandler: this.messagesHandler.bind(this),
579
+ signal: signal,
580
+ openingDeadlineMS: openingDeadlineMS
581
+ };
582
+ return yield createEventSource(eventSourceConfig);
583
+ }), (resource) => __awaiter(this, void 0, void 0, function* () {
584
+ resource.close();
585
+ }));
360
586
  this.bridgeGatewayStorage = new HttpBridgeGatewayStorage(storage, bridgeUrl);
361
587
  }
588
+ get isReady() {
589
+ const eventSource = this.eventSource.current();
590
+ return (eventSource === null || eventSource === void 0 ? void 0 : eventSource.readyState) === EventSource.OPEN;
591
+ }
592
+ get isClosed() {
593
+ const eventSource = this.eventSource.current();
594
+ return (eventSource === null || eventSource === void 0 ? void 0 : eventSource.readyState) !== EventSource.OPEN;
595
+ }
596
+ get isConnecting() {
597
+ const eventSource = this.eventSource.current();
598
+ return (eventSource === null || eventSource === void 0 ? void 0 : eventSource.readyState) === EventSource.CONNECTING;
599
+ }
362
600
  registerSession(options) {
363
601
  return __awaiter(this, void 0, void 0, function* () {
364
- const url = new URL(addPathToUrl(this.bridgeUrl, this.ssePath));
365
- url.searchParams.append('client_id', this.sessionId);
366
- const lastEventId = yield this.bridgeGatewayStorage.getLastEventId();
367
- if (this.isClosed) {
368
- return;
369
- }
370
- if (lastEventId) {
371
- url.searchParams.append('last_event_id', lastEventId);
372
- }
373
- this.eventSource = new EventSource(url.toString());
374
- return new Promise((resolve, reject) => {
375
- const timeout = (options === null || options === void 0 ? void 0 : options.openingDeadlineMS) ? setTimeout(() => {
376
- var _a;
377
- if (((_a = this.eventSource) === null || _a === void 0 ? void 0 : _a.readyState) !== EventSource.OPEN) {
378
- reject(new TonConnectError('Bridge connection timeout'));
379
- this.close();
380
- }
381
- }, options.openingDeadlineMS) : undefined;
382
- this.eventSource.onerror = () => reject;
383
- this.eventSource.onopen = () => {
384
- clearTimeout(timeout);
385
- this.isClosed = false;
386
- this.eventSource.onerror = this.errorsHandler.bind(this);
387
- this.eventSource.onmessage = this.messagesHandler.bind(this);
388
- resolve();
389
- };
390
- });
602
+ yield this.eventSource.create(options === null || options === void 0 ? void 0 : options.signal, options === null || options === void 0 ? void 0 : options.openingDeadlineMS);
391
603
  });
392
604
  }
393
- send(message, receiver, topic, ttl) {
605
+ send(message, receiver, topic, ttlOrOptions) {
606
+ var _a;
394
607
  return __awaiter(this, void 0, void 0, function* () {
608
+ // TODO: remove deprecated method
609
+ const options = {};
610
+ if (typeof ttlOrOptions === 'number') {
611
+ options.ttl = ttlOrOptions;
612
+ }
613
+ else {
614
+ options.ttl = ttlOrOptions === null || ttlOrOptions === void 0 ? void 0 : ttlOrOptions.ttl;
615
+ options.signal = ttlOrOptions === null || ttlOrOptions === void 0 ? void 0 : ttlOrOptions.signal;
616
+ options.attempts = ttlOrOptions === null || ttlOrOptions === void 0 ? void 0 : ttlOrOptions.attempts;
617
+ }
395
618
  const url = new URL(addPathToUrl(this.bridgeUrl, this.postPath));
396
619
  url.searchParams.append('client_id', this.sessionId);
397
620
  url.searchParams.append('to', receiver);
398
- url.searchParams.append('ttl', (ttl || this.defaultTtl).toString());
621
+ url.searchParams.append('ttl', ((options === null || options === void 0 ? void 0 : options.ttl) || this.defaultTtl).toString());
399
622
  url.searchParams.append('topic', topic);
400
- const response = yield fetch(url, {
401
- method: 'post',
402
- body: protocol.Base64.encode(message)
623
+ const body = protocol.Base64.encode(message);
624
+ yield callForSuccess((options) => __awaiter(this, void 0, void 0, function* () {
625
+ const response = yield this.post(url, body, options.signal);
626
+ if (!response.ok) {
627
+ throw new TonConnectError(`Bridge send failed, status ${response.status}`);
628
+ }
629
+ }), {
630
+ attempts: (_a = options === null || options === void 0 ? void 0 : options.attempts) !== null && _a !== void 0 ? _a : Number.MAX_SAFE_INTEGER,
631
+ delayMs: 5000,
632
+ signal: options === null || options === void 0 ? void 0 : options.signal
403
633
  });
404
- if (!response.ok) {
405
- throw new TonConnectError(`Bridge send failed, status ${response.status}`);
406
- }
407
634
  });
408
635
  }
409
636
  pause() {
410
- var _a;
411
- (_a = this.eventSource) === null || _a === void 0 ? void 0 : _a.close();
637
+ this.eventSource.dispose().catch(e => logError(`Bridge pause failed, ${e}`));
412
638
  }
413
639
  unPause() {
414
- return this.registerSession();
640
+ return __awaiter(this, void 0, void 0, function* () {
641
+ const RECREATE_WITHOUT_DELAY = 0;
642
+ yield this.eventSource.recreate(RECREATE_WITHOUT_DELAY);
643
+ });
415
644
  }
416
645
  close() {
417
- var _a;
418
- this.isClosed = true;
419
- (_a = this.eventSource) === null || _a === void 0 ? void 0 : _a.close();
646
+ return __awaiter(this, void 0, void 0, function* () {
647
+ yield this.eventSource.dispose().catch(e => logError(`Bridge close failed, ${e}`));
648
+ });
420
649
  }
421
650
  setListener(listener) {
422
651
  this.listener = listener;
@@ -424,20 +653,38 @@ class BridgeGateway {
424
653
  setErrorsListener(errorsListener) {
425
654
  this.errorsListener = errorsListener;
426
655
  }
427
- errorsHandler(e) {
428
- var _a, _b;
429
- if (!this.isClosed) {
430
- if (((_a = this.eventSource) === null || _a === void 0 ? void 0 : _a.readyState) === EventSource.CLOSED) {
431
- this.eventSource.close();
432
- this.registerSession();
433
- return;
656
+ post(url, body, signal) {
657
+ return __awaiter(this, void 0, void 0, function* () {
658
+ const response = yield fetch(url, {
659
+ method: 'post',
660
+ body: body,
661
+ signal: signal
662
+ });
663
+ if (!response.ok) {
664
+ throw new TonConnectError(`Bridge send failed, status ${response.status}`);
434
665
  }
435
- if (((_b = this.eventSource) === null || _b === void 0 ? void 0 : _b.readyState) === EventSource.CONNECTING) {
436
- console.debug('[TON_CONNET_SDK_ERROR]: Bridge error', JSON.stringify(e));
437
- return;
666
+ return response;
667
+ });
668
+ }
669
+ errorsHandler(e) {
670
+ return __awaiter(this, void 0, void 0, function* () {
671
+ try {
672
+ if (this.isConnecting) {
673
+ logError('Bridge error', JSON.stringify(e));
674
+ return;
675
+ }
676
+ if (this.isReady) {
677
+ this.errorsListener(e);
678
+ return;
679
+ }
680
+ if (this.isClosed) {
681
+ logDebug(`Bridge reconnecting, ${this.defaultReconnectDelay}ms delay`);
682
+ yield this.eventSource.recreate(this.defaultReconnectDelay);
683
+ return;
684
+ }
438
685
  }
439
- this.errorsListener(e);
440
- }
686
+ catch (e) { }
687
+ });
441
688
  }
442
689
  messagesHandler(e) {
443
690
  return __awaiter(this, void 0, void 0, function* () {
@@ -445,19 +692,71 @@ class BridgeGateway {
445
692
  return;
446
693
  }
447
694
  yield this.bridgeGatewayStorage.storeLastEventId(e.lastEventId);
448
- if (!this.isClosed) {
449
- let bridgeIncomingMessage;
450
- try {
451
- bridgeIncomingMessage = JSON.parse(e.data);
452
- }
453
- catch (e) {
454
- throw new TonConnectError(`Bridge message parse failed, message ${e.data}`);
455
- }
456
- this.listener(bridgeIncomingMessage);
695
+ if (this.isClosed) {
696
+ return;
697
+ }
698
+ let bridgeIncomingMessage;
699
+ try {
700
+ bridgeIncomingMessage = JSON.parse(e.data);
457
701
  }
702
+ catch (e) {
703
+ throw new TonConnectError(`Bridge message parse failed, message ${e.data}`);
704
+ }
705
+ this.listener(bridgeIncomingMessage);
458
706
  });
459
707
  }
460
708
  }
709
+ /**
710
+ * Creates an event source.
711
+ * @param {CreateEventSourceConfig} config - Configuration for creating an event source.
712
+ */
713
+ function createEventSource(config) {
714
+ return __awaiter(this, void 0, void 0, function* () {
715
+ return yield timeout((resolve, reject, deferOptions) => __awaiter(this, void 0, void 0, function* () {
716
+ var _a;
717
+ const abortController = createAbortController(deferOptions.signal);
718
+ const signal = abortController.signal;
719
+ if (signal.aborted) {
720
+ reject(new TonConnectError('Bridge connection aborted'));
721
+ return;
722
+ }
723
+ const url = new URL(addPathToUrl(config.bridgeUrl, config.ssePath));
724
+ url.searchParams.append('client_id', config.sessionId);
725
+ const lastEventId = yield config.bridgeGatewayStorage.getLastEventId();
726
+ if (lastEventId) {
727
+ url.searchParams.append('last_event_id', lastEventId);
728
+ }
729
+ if (signal.aborted) {
730
+ reject(new TonConnectError('Bridge connection aborted'));
731
+ return;
732
+ }
733
+ const eventSource = new EventSource(url.toString());
734
+ eventSource.onerror = (reason) => {
735
+ if (signal.aborted) {
736
+ eventSource.close();
737
+ reject(new TonConnectError('Bridge connection aborted'));
738
+ return;
739
+ }
740
+ config.errorHandler(reason);
741
+ };
742
+ eventSource.onopen = () => {
743
+ if (signal.aborted) {
744
+ eventSource.close();
745
+ reject(new TonConnectError('Bridge connection aborted'));
746
+ return;
747
+ }
748
+ resolve(eventSource);
749
+ };
750
+ eventSource.onmessage = (event) => {
751
+ config.messageHandler(event);
752
+ };
753
+ (_a = config.signal) === null || _a === void 0 ? void 0 : _a.addEventListener('abort', () => {
754
+ eventSource.close();
755
+ reject(new TonConnectError('Bridge connection aborted'));
756
+ });
757
+ }), { timeout: config.openingDeadlineMS, signal: config.signal });
758
+ });
759
+ }
461
760
 
462
761
  function isPendingConnectionHttp(connection) {
463
762
  return !('connectEvent' in connection);
@@ -622,31 +921,6 @@ class BridgeConnectionStorage {
622
921
 
623
922
  const PROTOCOL_VERSION = 2;
624
923
 
625
- function logDebug(...args) {
626
- {
627
- try {
628
- console.debug('[TON_CONNECT_SDK]', ...args);
629
- }
630
- catch (_a) { }
631
- }
632
- }
633
- function logError(...args) {
634
- {
635
- try {
636
- console.error('[TON_CONNECT_SDK]', ...args);
637
- }
638
- catch (_a) { }
639
- }
640
- }
641
- function logWarning(...args) {
642
- {
643
- try {
644
- console.warn('[TON_CONNECT_SDK]', ...args);
645
- }
646
- catch (_a) { }
647
- }
648
- }
649
-
650
924
  class BridgeProvider {
651
925
  constructor(storage, walletConnectionSource) {
652
926
  this.storage = storage;
@@ -658,6 +932,7 @@ class BridgeProvider {
658
932
  this.gateway = null;
659
933
  this.pendingGateways = [];
660
934
  this.listeners = [];
935
+ this.defaultOpeningDeadlineMS = 5000;
661
936
  this.connectionStorage = new BridgeConnectionStorage(storage);
662
937
  }
663
938
  static fromStorage(storage) {
@@ -670,7 +945,11 @@ class BridgeProvider {
670
945
  return new BridgeProvider(storage, { bridgeUrl: connection.session.bridgeUrl });
671
946
  });
672
947
  }
673
- connect(message) {
948
+ connect(message, options) {
949
+ var _a;
950
+ const abortController = createAbortController(options === null || options === void 0 ? void 0 : options.signal);
951
+ (_a = this.abortController) === null || _a === void 0 ? void 0 : _a.abort();
952
+ this.abortController = abortController;
674
953
  this.closeGateways();
675
954
  const sessionCrypto = new protocol.SessionCrypto();
676
955
  this.session = {
@@ -685,20 +964,43 @@ class BridgeProvider {
685
964
  connectionSource: this.walletConnectionSource,
686
965
  sessionCrypto
687
966
  })
688
- .then(() => this.openGateways(sessionCrypto));
967
+ .then(() => __awaiter(this, void 0, void 0, function* () {
968
+ if (abortController.signal.aborted) {
969
+ return;
970
+ }
971
+ yield callForSuccess(_options => this.openGateways(sessionCrypto, {
972
+ openingDeadlineMS: options === null || options === void 0 ? void 0 : options.openingDeadlineMS,
973
+ signal: _options === null || _options === void 0 ? void 0 : _options.signal
974
+ }), {
975
+ attempts: Number.MAX_SAFE_INTEGER,
976
+ delayMs: 5000,
977
+ signal: abortController.signal
978
+ });
979
+ }));
689
980
  const universalLink = 'universalLink' in this.walletConnectionSource &&
690
981
  this.walletConnectionSource.universalLink
691
982
  ? this.walletConnectionSource.universalLink
692
983
  : this.standardUniversalLink;
693
984
  return this.generateUniversalLink(universalLink, message);
694
985
  }
695
- restoreConnection() {
986
+ restoreConnection(options) {
987
+ var _a, _b;
696
988
  return __awaiter(this, void 0, void 0, function* () {
989
+ const abortController = createAbortController(options === null || options === void 0 ? void 0 : options.signal);
990
+ (_a = this.abortController) === null || _a === void 0 ? void 0 : _a.abort();
991
+ this.abortController = abortController;
992
+ if (abortController.signal.aborted) {
993
+ return;
994
+ }
697
995
  this.closeGateways();
698
996
  const storedConnection = yield this.connectionStorage.getHttpConnection();
699
997
  if (!storedConnection) {
700
998
  return;
701
999
  }
1000
+ if (abortController.signal.aborted) {
1001
+ return;
1002
+ }
1003
+ const openingDeadlineMS = (_b = options === null || options === void 0 ? void 0 : options.openingDeadlineMS) !== null && _b !== void 0 ? _b : this.defaultOpeningDeadlineMS;
702
1004
  if (isPendingConnectionHttp(storedConnection)) {
703
1005
  this.session = {
704
1006
  sessionCrypto: storedConnection.sessionCrypto,
@@ -706,25 +1008,55 @@ class BridgeProvider {
706
1008
  ? this.walletConnectionSource.bridgeUrl
707
1009
  : ''
708
1010
  };
709
- return this.openGateways(storedConnection.sessionCrypto, { openingDeadlineMS: 5000 });
1011
+ return yield this.openGateways(storedConnection.sessionCrypto, {
1012
+ openingDeadlineMS: openingDeadlineMS,
1013
+ signal: abortController === null || abortController === void 0 ? void 0 : abortController.signal
1014
+ });
710
1015
  }
711
1016
  if (Array.isArray(this.walletConnectionSource)) {
712
1017
  throw new TonConnectError('Internal error. Connection source is array while WalletConnectionSourceHTTP was expected.');
713
1018
  }
714
1019
  this.session = storedConnection.session;
1020
+ if (this.gateway) {
1021
+ logDebug('Gateway is already opened, closing previous gateway');
1022
+ yield this.gateway.close();
1023
+ }
715
1024
  this.gateway = new BridgeGateway(this.storage, this.walletConnectionSource.bridgeUrl, storedConnection.session.sessionCrypto.sessionId, this.gatewayListener.bind(this), this.gatewayErrorsListener.bind(this));
1025
+ if (abortController.signal.aborted) {
1026
+ return;
1027
+ }
1028
+ // notify listeners about stored connection
1029
+ this.listeners.forEach(listener => listener(storedConnection.connectEvent));
1030
+ // wait for the connection to be opened
716
1031
  try {
717
- yield this.gateway.registerSession({ openingDeadlineMS: 5000 });
1032
+ yield callForSuccess(options => this.gateway.registerSession({
1033
+ openingDeadlineMS: openingDeadlineMS,
1034
+ signal: options.signal
1035
+ }), {
1036
+ attempts: Number.MAX_SAFE_INTEGER,
1037
+ delayMs: 5000,
1038
+ signal: abortController.signal
1039
+ });
718
1040
  }
719
1041
  catch (e) {
720
- yield this.disconnect();
1042
+ yield this.disconnect({ signal: abortController.signal });
721
1043
  return;
722
1044
  }
723
- this.listeners.forEach(listener => listener(storedConnection.connectEvent));
724
1045
  });
725
1046
  }
726
- sendRequest(request, onRequestSent) {
1047
+ sendRequest(request, optionsOrOnRequestSent) {
1048
+ // TODO: remove deprecated method
1049
+ const options = {};
1050
+ if (typeof optionsOrOnRequestSent === 'function') {
1051
+ options.onRequestSent = optionsOrOnRequestSent;
1052
+ }
1053
+ else {
1054
+ options.onRequestSent = optionsOrOnRequestSent === null || optionsOrOnRequestSent === void 0 ? void 0 : optionsOrOnRequestSent.onRequestSent;
1055
+ options.signal = optionsOrOnRequestSent === null || optionsOrOnRequestSent === void 0 ? void 0 : optionsOrOnRequestSent.signal;
1056
+ options.attempts = optionsOrOnRequestSent === null || optionsOrOnRequestSent === void 0 ? void 0 : optionsOrOnRequestSent.attempts;
1057
+ }
727
1058
  return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
1059
+ var _a;
728
1060
  if (!this.gateway || !this.session || !('walletPublicKey' in this.session)) {
729
1061
  throw new TonConnectError('Trying to send bridge request without session');
730
1062
  }
@@ -733,8 +1065,8 @@ class BridgeProvider {
733
1065
  logDebug('Send http-bridge request:', Object.assign(Object.assign({}, request), { id }));
734
1066
  const encodedRequest = this.session.sessionCrypto.encrypt(JSON.stringify(Object.assign(Object.assign({}, request), { id })), protocol.hexToByteArray(this.session.walletPublicKey));
735
1067
  try {
736
- yield this.gateway.send(encodedRequest, this.session.walletPublicKey, request.method);
737
- onRequestSent === null || onRequestSent === void 0 ? void 0 : onRequestSent();
1068
+ yield this.gateway.send(encodedRequest, this.session.walletPublicKey, request.method, { attempts: options === null || options === void 0 ? void 0 : options.attempts, signal: options === null || options === void 0 ? void 0 : options.signal });
1069
+ (_a = options === null || options === void 0 ? void 0 : options.onRequestSent) === null || _a === void 0 ? void 0 : _a.call(options);
738
1070
  this.pendingRequests.set(id.toString(), resolve);
739
1071
  }
740
1072
  catch (e) {
@@ -748,23 +1080,41 @@ class BridgeProvider {
748
1080
  this.session = null;
749
1081
  this.gateway = null;
750
1082
  }
751
- disconnect() {
1083
+ disconnect(options) {
752
1084
  return __awaiter(this, void 0, void 0, function* () {
753
1085
  return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
754
1086
  let called = false;
1087
+ let timeoutId = null;
755
1088
  const onRequestSent = () => {
756
- called = true;
757
- this.removeBridgeAndSession().then(resolve);
1089
+ if (!called) {
1090
+ called = true;
1091
+ this.removeBridgeAndSession().then(resolve);
1092
+ }
758
1093
  };
759
1094
  try {
760
- yield this.sendRequest({ method: 'disconnect', params: [] }, onRequestSent);
1095
+ this.closeGateways();
1096
+ const abortController = createAbortController(options === null || options === void 0 ? void 0 : options.signal);
1097
+ timeoutId = setTimeout(() => {
1098
+ abortController.abort();
1099
+ }, this.defaultOpeningDeadlineMS);
1100
+ yield this.sendRequest({ method: 'disconnect', params: [] }, {
1101
+ onRequestSent: onRequestSent,
1102
+ signal: abortController.signal,
1103
+ attempts: 1,
1104
+ });
761
1105
  }
762
1106
  catch (e) {
763
- console.debug(e);
1107
+ logDebug('Disconnect error:', e);
764
1108
  if (!called) {
765
1109
  this.removeBridgeAndSession().then(resolve);
766
1110
  }
767
1111
  }
1112
+ finally {
1113
+ if (timeoutId) {
1114
+ clearTimeout(timeoutId);
1115
+ }
1116
+ onRequestSent();
1117
+ }
768
1118
  }));
769
1119
  });
770
1120
  }
@@ -789,10 +1139,14 @@ class BridgeProvider {
789
1139
  pendingGatewaysListener(gateway, bridgeUrl, bridgeIncomingMessage) {
790
1140
  return __awaiter(this, void 0, void 0, function* () {
791
1141
  if (!this.pendingGateways.includes(gateway)) {
792
- gateway.close();
1142
+ yield gateway.close();
793
1143
  return;
794
1144
  }
795
1145
  this.closeGateways({ except: gateway });
1146
+ if (this.gateway) {
1147
+ logDebug('Gateway is already opened, closing previous gateway');
1148
+ yield this.gateway.close();
1149
+ }
796
1150
  this.session.bridgeUrl = bridgeUrl;
797
1151
  this.gateway = gateway;
798
1152
  this.gateway.setErrorsListener(this.gatewayErrorsListener.bind(this));
@@ -831,6 +1185,7 @@ class BridgeProvider {
831
1185
  yield this.updateSession(walletMessage, bridgeIncomingMessage.from);
832
1186
  }
833
1187
  if (walletMessage.event === 'disconnect') {
1188
+ logDebug(`Removing bridge and session: received disconnect event`);
834
1189
  yield this.removeBridgeAndSession();
835
1190
  }
836
1191
  listeners.forEach(listener => listener(walletMessage));
@@ -896,6 +1251,9 @@ class BridgeProvider {
896
1251
  openGateways(sessionCrypto, options) {
897
1252
  return __awaiter(this, void 0, void 0, function* () {
898
1253
  if (Array.isArray(this.walletConnectionSource)) {
1254
+ // close all gateways before opening new ones
1255
+ this.pendingGateways.map(bridge => bridge.close().catch(e => console.error(e)));
1256
+ // open new gateways
899
1257
  this.pendingGateways = this.walletConnectionSource.map(source => {
900
1258
  const gateway = new BridgeGateway(this.storage, source.bridgeUrl, sessionCrypto.sessionId, () => { }, e => {
901
1259
  console.error(e);
@@ -903,12 +1261,31 @@ class BridgeProvider {
903
1261
  gateway.setListener(message => this.pendingGatewaysListener(gateway, source.bridgeUrl, message));
904
1262
  return gateway;
905
1263
  });
906
- yield Promise.allSettled(this.pendingGateways.map(bridge => bridge.registerSession(options)));
1264
+ yield Promise.allSettled(this.pendingGateways.map(bridge => callForSuccess((_options) => {
1265
+ if (!this.pendingGateways.some(item => item === bridge)) {
1266
+ return bridge.close();
1267
+ }
1268
+ return bridge.registerSession({
1269
+ openingDeadlineMS: options === null || options === void 0 ? void 0 : options.openingDeadlineMS,
1270
+ signal: _options.signal
1271
+ });
1272
+ }, {
1273
+ attempts: Number.MAX_SAFE_INTEGER,
1274
+ delayMs: 5000,
1275
+ signal: options === null || options === void 0 ? void 0 : options.signal
1276
+ })));
907
1277
  return;
908
1278
  }
909
1279
  else {
1280
+ if (this.gateway) {
1281
+ logDebug(`Gateway is already opened, closing previous gateway`);
1282
+ yield this.gateway.close();
1283
+ }
910
1284
  this.gateway = new BridgeGateway(this.storage, this.walletConnectionSource.bridgeUrl, sessionCrypto.sessionId, this.gatewayListener.bind(this), this.gatewayErrorsListener.bind(this));
911
- return this.gateway.registerSession(options);
1285
+ return yield this.gateway.registerSession({
1286
+ openingDeadlineMS: options === null || options === void 0 ? void 0 : options.openingDeadlineMS,
1287
+ signal: options === null || options === void 0 ? void 0 : options.signal
1288
+ });
912
1289
  }
913
1290
  });
914
1291
  }
@@ -1171,14 +1548,24 @@ class InjectedProvider {
1171
1548
  this.listeners.push(eventsCallback);
1172
1549
  return () => (this.listeners = this.listeners.filter(listener => listener !== eventsCallback));
1173
1550
  }
1174
- sendRequest(request, onRequestSent) {
1551
+ sendRequest(request, optionsOrOnRequestSent) {
1552
+ var _a;
1175
1553
  return __awaiter(this, void 0, void 0, function* () {
1554
+ // TODO: remove deprecated method
1555
+ const options = {};
1556
+ if (typeof optionsOrOnRequestSent === 'function') {
1557
+ options.onRequestSent = optionsOrOnRequestSent;
1558
+ }
1559
+ else {
1560
+ options.onRequestSent = optionsOrOnRequestSent === null || optionsOrOnRequestSent === void 0 ? void 0 : optionsOrOnRequestSent.onRequestSent;
1561
+ options.signal = optionsOrOnRequestSent === null || optionsOrOnRequestSent === void 0 ? void 0 : optionsOrOnRequestSent.signal;
1562
+ }
1176
1563
  const id = (yield this.connectionStorage.getNextRpcRequestId()).toString();
1177
1564
  yield this.connectionStorage.increaseNextRpcRequestId();
1178
1565
  logDebug('Send injected-bridge request:', Object.assign(Object.assign({}, request), { id }));
1179
1566
  const result = this.injectedWallet.send(Object.assign(Object.assign({}, request), { id }));
1180
1567
  result.then(response => logDebug('Wallet message received:', response));
1181
- onRequestSent === null || onRequestSent === void 0 ? void 0 : onRequestSent();
1568
+ (_a = options === null || options === void 0 ? void 0 : options.onRequestSent) === null || _a === void 0 ? void 0 : _a.call(options);
1182
1569
  return result;
1183
1570
  });
1184
1571
  }
@@ -1195,7 +1582,7 @@ class InjectedProvider {
1195
1582
  this.listeners.forEach(listener => listener(connectEvent));
1196
1583
  }
1197
1584
  catch (e) {
1198
- logDebug(e);
1585
+ logDebug('Injected Provider connect error:', e);
1199
1586
  const connectEventError = {
1200
1587
  event: 'connect_error',
1201
1588
  payload: {
@@ -1324,19 +1711,6 @@ const FALLBACK_WALLETS_LIST = [
1324
1711
  ],
1325
1712
  platforms: ['ios', 'android', 'chrome', 'firefox', 'macos']
1326
1713
  },
1327
- {
1328
- app_name: 'openmask',
1329
- name: 'OpenMask',
1330
- image: 'https://raw.githubusercontent.com/OpenProduct/openmask-extension/main/public/openmask-logo-288.png',
1331
- about_url: 'https://www.openmask.app/',
1332
- bridge: [
1333
- {
1334
- type: 'js',
1335
- key: 'openmask'
1336
- }
1337
- ],
1338
- platforms: ['chrome']
1339
- },
1340
1714
  {
1341
1715
  app_name: 'mytonwallet',
1342
1716
  name: 'MyTonWallet',
@@ -1353,7 +1727,20 @@ const FALLBACK_WALLETS_LIST = [
1353
1727
  url: 'https://tonconnectbridge.mytonwallet.org/bridge/'
1354
1728
  }
1355
1729
  ],
1356
- platforms: ['chrome', 'windows', 'macos', 'linux']
1730
+ platforms: ['chrome', 'windows', 'macos', 'linux', 'ios', 'android', 'firefox']
1731
+ },
1732
+ {
1733
+ app_name: 'openmask',
1734
+ name: 'OpenMask',
1735
+ image: 'https://raw.githubusercontent.com/OpenProduct/openmask-extension/main/public/openmask-logo-288.png',
1736
+ about_url: 'https://www.openmask.app/',
1737
+ bridge: [
1738
+ {
1739
+ type: 'js',
1740
+ key: 'openmask'
1741
+ }
1742
+ ],
1743
+ platforms: ['chrome']
1357
1744
  },
1358
1745
  {
1359
1746
  app_name: 'tonhub',
@@ -1373,19 +1760,6 @@ const FALLBACK_WALLETS_LIST = [
1373
1760
  ],
1374
1761
  platforms: ['ios', 'android']
1375
1762
  },
1376
- {
1377
- app_name: 'tonflow',
1378
- name: 'TonFlow',
1379
- image: 'https://tonflow.net/assets/images/tonflow_ico_192.png',
1380
- about_url: 'https://tonflow.net',
1381
- bridge: [
1382
- {
1383
- type: 'js',
1384
- key: 'tonflow'
1385
- }
1386
- ],
1387
- platforms: ['chrome']
1388
- },
1389
1763
  {
1390
1764
  app_name: 'dewallet',
1391
1765
  name: 'DeWallet',
@@ -1692,59 +2066,128 @@ class TonConnect {
1692
2066
  }
1693
2067
  };
1694
2068
  }
1695
- connect(wallet, request) {
1696
- var _a;
2069
+ connect(wallet, requestOrOptions) {
2070
+ var _a, _b;
2071
+ // TODO: remove deprecated method
2072
+ const options = {};
2073
+ if (typeof requestOrOptions === 'object' && 'tonProof' in requestOrOptions) {
2074
+ options.request = requestOrOptions;
2075
+ }
2076
+ if (typeof requestOrOptions === 'object' &&
2077
+ ('openingDeadlineMS' in requestOrOptions ||
2078
+ 'signal' in requestOrOptions ||
2079
+ 'request' in requestOrOptions)) {
2080
+ options.request = requestOrOptions === null || requestOrOptions === void 0 ? void 0 : requestOrOptions.request;
2081
+ options.openingDeadlineMS = requestOrOptions === null || requestOrOptions === void 0 ? void 0 : requestOrOptions.openingDeadlineMS;
2082
+ options.signal = requestOrOptions === null || requestOrOptions === void 0 ? void 0 : requestOrOptions.signal;
2083
+ }
1697
2084
  if (this.connected) {
1698
2085
  throw new WalletAlreadyConnectedError();
1699
2086
  }
1700
- (_a = this.provider) === null || _a === void 0 ? void 0 : _a.closeConnection();
2087
+ const abortController = createAbortController(options === null || options === void 0 ? void 0 : options.signal);
2088
+ (_a = this.abortController) === null || _a === void 0 ? void 0 : _a.abort();
2089
+ this.abortController = abortController;
2090
+ if (abortController.signal.aborted) {
2091
+ throw new TonConnectError('Connection was aborted');
2092
+ }
2093
+ (_b = this.provider) === null || _b === void 0 ? void 0 : _b.closeConnection();
1701
2094
  this.provider = this.createProvider(wallet);
1702
- return this.provider.connect(this.createConnectRequest(request));
2095
+ abortController.signal.addEventListener('abort', () => {
2096
+ var _a;
2097
+ (_a = this.provider) === null || _a === void 0 ? void 0 : _a.closeConnection();
2098
+ this.provider = null;
2099
+ });
2100
+ return this.provider.connect(this.createConnectRequest(options === null || options === void 0 ? void 0 : options.request), {
2101
+ openingDeadlineMS: options === null || options === void 0 ? void 0 : options.openingDeadlineMS,
2102
+ signal: abortController.signal
2103
+ });
1703
2104
  }
1704
2105
  /**
1705
2106
  * Try to restore existing session and reconnect to the corresponding wallet. Call it immediately when your app is loaded.
1706
2107
  */
1707
- restoreConnection() {
2108
+ restoreConnection(options) {
2109
+ var _a, _b;
1708
2110
  return __awaiter(this, void 0, void 0, function* () {
2111
+ const abortController = createAbortController(options === null || options === void 0 ? void 0 : options.signal);
2112
+ (_a = this.abortController) === null || _a === void 0 ? void 0 : _a.abort();
2113
+ this.abortController = abortController;
2114
+ if (abortController.signal.aborted) {
2115
+ return;
2116
+ }
2117
+ // TODO: potentially race condition here
1709
2118
  const [bridgeConnectionType, embeddedWallet] = yield Promise.all([
1710
2119
  this.bridgeConnectionStorage.storedConnectionType(),
1711
2120
  this.walletsList.getEmbeddedWallet()
1712
2121
  ]);
2122
+ if (abortController.signal.aborted) {
2123
+ return;
2124
+ }
2125
+ let provider = null;
1713
2126
  try {
1714
2127
  switch (bridgeConnectionType) {
1715
2128
  case 'http':
1716
- this.provider = yield BridgeProvider.fromStorage(this.dappSettings.storage);
2129
+ provider = yield BridgeProvider.fromStorage(this.dappSettings.storage);
1717
2130
  break;
1718
2131
  case 'injected':
1719
- this.provider = yield InjectedProvider.fromStorage(this.dappSettings.storage);
2132
+ provider = yield InjectedProvider.fromStorage(this.dappSettings.storage);
1720
2133
  break;
1721
2134
  default:
1722
2135
  if (embeddedWallet) {
1723
- this.provider = yield this.createProvider(embeddedWallet);
2136
+ provider = this.createProvider(embeddedWallet);
1724
2137
  }
1725
2138
  else {
1726
2139
  return;
1727
2140
  }
1728
2141
  }
1729
2142
  }
1730
- catch (_a) {
2143
+ catch (_c) {
1731
2144
  yield this.bridgeConnectionStorage.removeConnection();
1732
- this.provider = null;
2145
+ provider === null || provider === void 0 ? void 0 : provider.closeConnection();
2146
+ provider = null;
1733
2147
  return;
1734
2148
  }
1735
- this.provider.listen(this.walletEventsListener.bind(this));
1736
- return this.provider.restoreConnection();
2149
+ if (abortController.signal.aborted) {
2150
+ provider === null || provider === void 0 ? void 0 : provider.closeConnection();
2151
+ return;
2152
+ }
2153
+ if (!provider) {
2154
+ logError('Provider is not restored');
2155
+ return;
2156
+ }
2157
+ (_b = this.provider) === null || _b === void 0 ? void 0 : _b.closeConnection();
2158
+ this.provider = provider;
2159
+ provider.listen(this.walletEventsListener.bind(this));
2160
+ abortController.signal.addEventListener('abort', () => {
2161
+ provider === null || provider === void 0 ? void 0 : provider.closeConnection();
2162
+ provider = null;
2163
+ });
2164
+ return yield callForSuccess((_options) => __awaiter(this, void 0, void 0, function* () {
2165
+ return provider === null || provider === void 0 ? void 0 : provider.restoreConnection({
2166
+ openingDeadlineMS: options === null || options === void 0 ? void 0 : options.openingDeadlineMS,
2167
+ signal: _options.signal
2168
+ });
2169
+ }), {
2170
+ attempts: Number.MAX_SAFE_INTEGER,
2171
+ delayMs: 5000,
2172
+ signal: options === null || options === void 0 ? void 0 : options.signal
2173
+ });
1737
2174
  });
1738
2175
  }
1739
- /**
1740
- * Asks connected wallet to sign and send the transaction.
1741
- * @param transaction transaction to send.
1742
- * @param onRequestSent (optional) will be called after the transaction is sent to the wallet.
1743
- * @returns signed transaction boc that allows you to find the transaction in the blockchain.
1744
- * If user rejects transaction, method will throw the corresponding error.
1745
- */
1746
- sendTransaction(transaction, onRequestSent) {
2176
+ sendTransaction(transaction, optionsOrOnRequestSent) {
1747
2177
  return __awaiter(this, void 0, void 0, function* () {
2178
+ // TODO: remove deprecated method
2179
+ const options = {};
2180
+ if (typeof optionsOrOnRequestSent === 'function') {
2181
+ options.onRequestSent = optionsOrOnRequestSent;
2182
+ }
2183
+ else {
2184
+ options.onRequestSent = optionsOrOnRequestSent === null || optionsOrOnRequestSent === void 0 ? void 0 : optionsOrOnRequestSent.onRequestSent;
2185
+ options.signal = optionsOrOnRequestSent === null || optionsOrOnRequestSent === void 0 ? void 0 : optionsOrOnRequestSent.signal;
2186
+ }
2187
+ const abortController = createAbortController(options === null || options === void 0 ? void 0 : options.signal);
2188
+ if (abortController.signal.aborted) {
2189
+ throw new TonConnectError('Transaction sending was aborted');
2190
+ }
1748
2191
  this.checkConnection();
1749
2192
  checkSendTransactionSupport(this.wallet.device.features, {
1750
2193
  requiredMessagesNumber: transaction.messages.length
@@ -1753,7 +2196,7 @@ class TonConnect {
1753
2196
  const from = transaction.from || this.account.address;
1754
2197
  const network = transaction.network || this.account.chain;
1755
2198
  const response = yield this.provider.sendRequest(sendTransactionParser.convertToRpcRequest(Object.assign(Object.assign({}, tx), { valid_until: validUntil, from,
1756
- network })), onRequestSent);
2199
+ network })), { onRequestSent: options.onRequestSent, signal: abortController.signal });
1757
2200
  if (sendTransactionParser.isError(response)) {
1758
2201
  return sendTransactionParser.parseAndThrowError(response);
1759
2202
  }
@@ -1763,13 +2206,23 @@ class TonConnect {
1763
2206
  /**
1764
2207
  * Disconnect form thw connected wallet and drop current session.
1765
2208
  */
1766
- disconnect() {
2209
+ disconnect(options) {
2210
+ var _a;
1767
2211
  return __awaiter(this, void 0, void 0, function* () {
1768
2212
  if (!this.connected) {
1769
2213
  throw new WalletNotConnectedError();
1770
2214
  }
1771
- yield this.provider.disconnect();
2215
+ const abortController = createAbortController(options === null || options === void 0 ? void 0 : options.signal);
2216
+ const prevAbortController = this.abortController;
2217
+ this.abortController = abortController;
2218
+ if (abortController.signal.aborted) {
2219
+ throw new TonConnectError('Disconnect was aborted');
2220
+ }
1772
2221
  this.onWalletDisconnected();
2222
+ yield ((_a = this.provider) === null || _a === void 0 ? void 0 : _a.disconnect({
2223
+ signal: abortController.signal
2224
+ }));
2225
+ prevAbortController === null || prevAbortController === void 0 ? void 0 : prevAbortController.abort();
1773
2226
  });
1774
2227
  }
1775
2228
  /**
@@ -1804,12 +2257,12 @@ class TonConnect {
1804
2257
  this.pauseConnection();
1805
2258
  }
1806
2259
  else {
1807
- this.unPauseConnection();
2260
+ this.unPauseConnection().catch(e => logError('Cannot unpause connection', e));
1808
2261
  }
1809
2262
  });
1810
2263
  }
1811
2264
  catch (e) {
1812
- console.error('Cannot subscribe to the document.visibilitychange: ', e);
2265
+ logError('Cannot subscribe to the document.visibilitychange: ', e);
1813
2266
  }
1814
2267
  }
1815
2268
  createProvider(wallet) {