UNPKG

@tonconnect/sdk

Version:

Use it to connect your app to TON wallets via TonConnect protocol. You can find more details and the protocol specification in the [docs](https://docs.ton.org/develop/dapps/ton-connect/overview). See the example of sdk usage [here](https://github.com/ton

1,255 lines (1,210 loc) 172 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var protocol = require('@tonconnect/protocol'); require('@tonconnect/isomorphic-eventsource'); require('@tonconnect/isomorphic-fetch'); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; /** * Base class for TonConnect errors. You can check if the error was triggered by the @tonconnect/sdk using `err instanceof TonConnectError`. */ class TonConnectError extends Error { get info() { return ''; } constructor(message, options) { super(message, options); this.message = `${TonConnectError.prefix} ${this.constructor.name}${this.info ? ': ' + this.info : ''}${message ? '\n' + message : ''}`; Object.setPrototypeOf(this, TonConnectError.prototype); } } TonConnectError.prefix = '[TON_CONNECT_SDK_ERROR]'; /** * Thrown when passed DappMetadata is in incorrect format. */ class DappMetadataError extends TonConnectError { get info() { return 'Passed DappMetadata is in incorrect format.'; } constructor(...args) { super(...args); Object.setPrototypeOf(this, DappMetadataError.prototype); } } /** * Thrown when passed manifest contains errors. */ class ManifestContentErrorError extends TonConnectError { get info() { return 'Passed `tonconnect-manifest.json` contains errors. Check format of your manifest. See more https://github.com/ton-connect/docs/blob/main/requests-responses.md#app-manifest'; } constructor(...args) { super(...args); Object.setPrototypeOf(this, ManifestContentErrorError.prototype); } } /** * Thrown when wallet can't get manifest by passed manifestUrl. */ class ManifestNotFoundError extends TonConnectError { get info() { return 'Manifest not found. Make sure you added `tonconnect-manifest.json` to the root of your app or passed correct manifestUrl. See more https://github.com/ton-connect/docs/blob/main/requests-responses.md#app-manifest'; } constructor(...args) { super(...args); Object.setPrototypeOf(this, ManifestNotFoundError.prototype); } } /** * Thrown when wallet connection called but wallet already connected. To avoid the error, disconnect the wallet before doing a new connection. */ class WalletAlreadyConnectedError extends TonConnectError { get info() { return 'Wallet connection called but wallet already connected. To avoid the error, disconnect the wallet before doing a new connection.'; } constructor(...args) { super(...args); Object.setPrototypeOf(this, WalletAlreadyConnectedError.prototype); } } /** * Thrown when send transaction or other protocol methods called while wallet is not connected. */ class WalletNotConnectedError extends TonConnectError { get info() { return 'Send transaction or other protocol methods called while wallet is not connected.'; } constructor(...args) { super(...args); Object.setPrototypeOf(this, WalletNotConnectedError.prototype); } } /** * Thrown when there is an attempt to connect to the injected wallet while it is not exists in the webpage. */ class WalletNotInjectedError extends TonConnectError { get info() { return 'There is an attempt to connect to the injected wallet while it is not exists in the webpage.'; } constructor(...args) { super(...args); Object.setPrototypeOf(this, WalletNotInjectedError.prototype); } } /** * Thrown when wallet doesn't support requested feature method. */ class WalletNotSupportFeatureError extends TonConnectError { get info() { return "Wallet doesn't support requested feature method."; } constructor(message, options) { super(message, options); Object.setPrototypeOf(this, WalletNotSupportFeatureError.prototype); } } /** * Thrown when wallet can't get manifest by passed manifestUrl. */ class WalletMissingRequiredFeaturesError extends TonConnectError { get info() { return 'Missing required features. You need to update your wallet.'; } constructor(message, options) { super(message, options); Object.setPrototypeOf(this, WalletMissingRequiredFeaturesError.prototype); } } function isWalletConnectionSourceJS(value) { return 'jsBridgeKey' in value; } /** * Thrown when user rejects the action in the wallet. */ class UserRejectsError extends TonConnectError { get info() { return 'User rejects the action in the wallet.'; } constructor(...args) { super(...args); Object.setPrototypeOf(this, UserRejectsError.prototype); } } /** * Thrown when request to the wallet contains errors. */ class BadRequestError extends TonConnectError { get info() { return 'Request to the wallet contains errors.'; } constructor(...args) { super(...args); Object.setPrototypeOf(this, BadRequestError.prototype); } } /** * Thrown when app tries to send rpc request to the injected wallet while not connected. */ class UnknownAppError extends TonConnectError { get info() { return 'App tries to send rpc request to the injected wallet while not connected.'; } constructor(...args) { super(...args); Object.setPrototypeOf(this, UnknownAppError.prototype); } } /** * Thrown when `Storage` was not specified in the `DappMetadata` and default `localStorage` was not detected in the Node.js environment. */ class LocalstorageNotFoundError extends TonConnectError { get info() { return 'Storage was not specified in the `DappMetadata` and default `localStorage` was not detected in the environment.'; } constructor(...args) { super(...args); Object.setPrototypeOf(this, LocalstorageNotFoundError.prototype); } } /** * Thrown when an error occurred while fetching the wallets list. */ class FetchWalletsError extends TonConnectError { get info() { return 'An error occurred while fetching the wallets list.'; } constructor(...args) { super(...args); Object.setPrototypeOf(this, FetchWalletsError.prototype); } } /** * Thrown when passed address is in incorrect format. */ class WrongAddressError extends TonConnectError { get info() { return 'Passed address is in incorrect format.'; } constructor(...args) { super(...args); Object.setPrototypeOf(this, WrongAddressError.prototype); } } /** * Thrown when passed hex is in incorrect format. */ class ParseHexError extends TonConnectError { get info() { return 'Passed hex is in incorrect format.'; } constructor(...args) { super(...args); Object.setPrototypeOf(this, ParseHexError.prototype); } } /** * Unhanded unknown error. */ class UnknownError extends TonConnectError { constructor(...args) { super(...args); Object.setPrototypeOf(this, UnknownError.prototype); } } const connectEventErrorsCodes = { [protocol.CONNECT_EVENT_ERROR_CODES.UNKNOWN_ERROR]: UnknownError, [protocol.CONNECT_EVENT_ERROR_CODES.USER_REJECTS_ERROR]: UserRejectsError, [protocol.CONNECT_EVENT_ERROR_CODES.BAD_REQUEST_ERROR]: BadRequestError, [protocol.CONNECT_EVENT_ERROR_CODES.UNKNOWN_APP_ERROR]: UnknownAppError, [protocol.CONNECT_EVENT_ERROR_CODES.MANIFEST_NOT_FOUND_ERROR]: ManifestNotFoundError, [protocol.CONNECT_EVENT_ERROR_CODES.MANIFEST_CONTENT_ERROR]: ManifestContentErrorError }; class ConnectErrorsParser { parseError(error) { let ErrorConstructor = UnknownError; if (error.code in connectEventErrorsCodes) { ErrorConstructor = connectEventErrorsCodes[error.code] || UnknownError; } return new ErrorConstructor(error.message); } } const connectErrorsParser = new ConnectErrorsParser(); class RpcParser { isError(response) { return 'error' in response; } } const sendTransactionErrors = { [protocol.SEND_TRANSACTION_ERROR_CODES.UNKNOWN_ERROR]: UnknownError, [protocol.SEND_TRANSACTION_ERROR_CODES.USER_REJECTS_ERROR]: UserRejectsError, [protocol.SEND_TRANSACTION_ERROR_CODES.BAD_REQUEST_ERROR]: BadRequestError, [protocol.SEND_TRANSACTION_ERROR_CODES.UNKNOWN_APP_ERROR]: UnknownAppError }; class SendTransactionParser extends RpcParser { convertToRpcRequest(request) { return { method: 'sendTransaction', params: [JSON.stringify(request)] }; } parseAndThrowError(response) { let ErrorConstructor = UnknownError; if (response.error.code in sendTransactionErrors) { ErrorConstructor = sendTransactionErrors[response.error.code] || UnknownError; } throw new ErrorConstructor(response.error.message); } convertFromRpcResponse(rpcResponse) { return { boc: rpcResponse.result }; } } const sendTransactionParser = new SendTransactionParser(); const signDataErrors = { [protocol.SIGN_DATA_ERROR_CODES.UNKNOWN_ERROR]: UnknownError, [protocol.SIGN_DATA_ERROR_CODES.USER_REJECTS_ERROR]: UserRejectsError, [protocol.SIGN_DATA_ERROR_CODES.BAD_REQUEST_ERROR]: BadRequestError, [protocol.SIGN_DATA_ERROR_CODES.UNKNOWN_APP_ERROR]: UnknownAppError }; class SignDataParser extends RpcParser { convertToRpcRequest(payload) { return { method: 'signData', params: [JSON.stringify(payload)] }; } parseAndThrowError(response) { let ErrorConstructor = UnknownError; if (response.error.code in signDataErrors) { ErrorConstructor = signDataErrors[response.error.code] || UnknownError; } throw new ErrorConstructor(response.error.message); } convertFromRpcResponse(rpcResponse) { return rpcResponse.result; } } const signDataParser = new SignDataParser(); class HttpBridgeGatewayStorage { constructor(storage, bridgeUrl) { this.storage = storage; this.storeKey = 'ton-connect-storage_http-bridge-gateway::' + bridgeUrl; } storeLastEventId(lastEventId) { return __awaiter(this, void 0, void 0, function* () { return this.storage.setItem(this.storeKey, lastEventId); }); } removeLastEventId() { return __awaiter(this, void 0, void 0, function* () { return this.storage.removeItem(this.storeKey); }); } getLastEventId() { return __awaiter(this, void 0, void 0, function* () { const stored = yield this.storage.getItem(this.storeKey); if (!stored) { return null; } return stored; }); } } function removeUrlLastSlash(url) { if (url.slice(-1) === '/') { return url.slice(0, -1); } return url; } function addPathToUrl(url, path) { return removeUrlLastSlash(url) + '/' + path; } function isTelegramUrl(link) { if (!link) { return false; } const url = new URL(link); return url.protocol === 'tg:' || url.hostname === 't.me'; } function encodeTelegramUrlParameters(parameters) { return parameters .replaceAll('.', '%2E') .replaceAll('-', '%2D') .replaceAll('_', '%5F') .replaceAll('&', '-') .replaceAll('=', '__') .replaceAll('%', '--'); } /** * Delays the execution of code for a specified number of milliseconds. * @param {number} timeout - The number of milliseconds to delay the execution. * @param {DelayOptions} [options] - Optional configuration options for the delay. * @return {Promise<void>} - A promise that resolves after the specified delay, or rejects if the delay is aborted. */ function delay(timeout, options) { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve, reject) => { var _a, _b; if ((_a = void 0 ) === null || _a === void 0 ? void 0 : _a.aborted) { reject(new TonConnectError('Delay aborted')); return; } const timeoutId = setTimeout(() => resolve(), timeout); (_b = void 0 ) === null || _b === void 0 ? void 0 : _b.addEventListener('abort', () => { clearTimeout(timeoutId); reject(new TonConnectError('Delay aborted')); }); }); }); } /** * Creates an AbortController instance with an optional AbortSignal. * * @param {AbortSignal} [signal] - An optional AbortSignal to use for aborting the controller. * @returns {AbortController} - An instance of AbortController. */ function createAbortController(signal) { const abortController = new AbortController(); if (signal === null || signal === void 0 ? void 0 : signal.aborted) { abortController.abort(); } else { signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', () => abortController.abort(), { once: true }); } return abortController; } /** * Function to call ton api until we get response. * Because ton network is pretty unstable we need to make sure response is final. * @param {T} fn - function to call * @param {CallForSuccessOptions} [options] - optional configuration options */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function callForSuccess(fn, options) { return __awaiter(this, void 0, void 0, function* () { var _a, _b; const attempts = (_a = options === null || options === void 0 ? void 0 : options.attempts) !== null && _a !== void 0 ? _a : 10; const delayMs = (_b = options === null || options === void 0 ? void 0 : options.delayMs) !== null && _b !== void 0 ? _b : 200; const abortController = createAbortController(options === null || options === void 0 ? void 0 : options.signal); if (typeof fn !== 'function') { throw new TonConnectError(`Expected a function, got ${typeof fn}`); } let i = 0; let lastError; while (i < attempts) { if (abortController.signal.aborted) { throw new TonConnectError(`Aborted after attempts ${i}`); } try { return yield fn({ signal: abortController.signal }); } catch (err) { lastError = err; i++; if (i < attempts) { yield delay(delayMs); } } } throw lastError; }); } function logDebug(...args) { { try { console.debug('[TON_CONNECT_SDK]', ...args); } catch (_a) { } } } function logError(...args) { { try { console.error('[TON_CONNECT_SDK]', ...args); } catch (_a) { } } } function logWarning(...args) { { try { console.warn('[TON_CONNECT_SDK]', ...args); } catch (_a) { } } } /** * Create a resource. * * @template T - The type of the resource. * @template Args - The type of the arguments for creating the resource. * * @param {(...args: Args) => Promise<T>} createFn - A function that creates the resource. * @param {(resource: T) => Promise<void>} [disposeFn] - An optional function that disposes the resource. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function createResource(createFn, disposeFn) { let currentResource = null; let currentArgs = null; let currentPromise = null; let currentSignal = null; let abortController = null; // create a new resource const create = (signal, ...args) => __awaiter(this, void 0, void 0, function* () { currentSignal = signal !== null && signal !== void 0 ? signal : null; abortController === null || abortController === void 0 ? void 0 : abortController.abort(); abortController = createAbortController(signal); if (abortController.signal.aborted) { throw new TonConnectError('Resource creation was aborted'); } currentArgs = args !== null && args !== void 0 ? args : null; const promise = createFn(abortController.signal, ...args); currentPromise = promise; const resource = yield promise; if (currentPromise !== promise && resource !== currentResource) { yield disposeFn(resource); throw new TonConnectError('Resource creation was aborted by a new resource creation'); } currentResource = resource; return currentResource; }); // get the current resource const current = () => { return currentResource !== null && currentResource !== void 0 ? currentResource : null; }; // dispose the current resource const dispose = () => __awaiter(this, void 0, void 0, function* () { try { const resource = currentResource; currentResource = null; const promise = currentPromise; currentPromise = null; try { abortController === null || abortController === void 0 ? void 0 : abortController.abort(); } catch (e) { } yield Promise.allSettled([ resource ? disposeFn(resource) : Promise.resolve(), promise ? disposeFn(yield promise) : Promise.resolve() ]); } catch (e) { } }); // recreate the current resource const recreate = (delayMs) => __awaiter(this, void 0, void 0, function* () { const resource = currentResource; const promise = currentPromise; const args = currentArgs; const signal = currentSignal; yield delay(delayMs); if (resource === currentResource && promise === currentPromise && args === currentArgs && signal === currentSignal) { return yield create(currentSignal, ...(args !== null && args !== void 0 ? args : [])); } throw new TonConnectError('Resource recreation was aborted by a new resource creation'); }); return { create, current, dispose, recreate }; } /** * Executes a function and provides deferred behavior, allowing for a timeout and abort functionality. * * @param {Deferrable<T>} fn - The function to execute. It should return a promise that resolves with the desired result. * @param {DeferOptions} options - Optional configuration options for the defer behavior. * @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. */ function timeout(fn, options) { const timeout = options === null || options === void 0 ? void 0 : options.timeout; const signal = options === null || options === void 0 ? void 0 : options.signal; const abortController = createAbortController(signal); return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { if (abortController.signal.aborted) { reject(new TonConnectError('Operation aborted')); return; } let timeoutId; if (typeof timeout !== 'undefined') { timeoutId = setTimeout(() => { abortController.abort(); reject(new TonConnectError(`Timeout after ${timeout}ms`)); }, timeout); } abortController.signal.addEventListener('abort', () => { clearTimeout(timeoutId); reject(new TonConnectError('Operation aborted')); }, { once: true }); const deferOptions = { timeout, abort: abortController.signal }; yield fn((...args) => { clearTimeout(timeoutId); resolve(...args); }, () => { clearTimeout(timeoutId); reject(); }, deferOptions); })); } class BridgeGateway { get isReady() { const eventSource = this.eventSource.current(); return (eventSource === null || eventSource === void 0 ? void 0 : eventSource.readyState) === EventSource.OPEN; } get isClosed() { const eventSource = this.eventSource.current(); return (eventSource === null || eventSource === void 0 ? void 0 : eventSource.readyState) !== EventSource.OPEN; } get isConnecting() { const eventSource = this.eventSource.current(); return (eventSource === null || eventSource === void 0 ? void 0 : eventSource.readyState) === EventSource.CONNECTING; } constructor(storage, bridgeUrl, sessionId, listener, errorsListener) { this.bridgeUrl = bridgeUrl; this.sessionId = sessionId; this.listener = listener; this.errorsListener = errorsListener; this.ssePath = 'events'; this.postPath = 'message'; this.heartbeatMessage = 'heartbeat'; this.defaultTtl = 300; this.defaultReconnectDelay = 2000; this.defaultResendDelay = 5000; this.eventSource = createResource((signal, openingDeadlineMS) => __awaiter(this, void 0, void 0, function* () { const eventSourceConfig = { bridgeUrl: this.bridgeUrl, ssePath: this.ssePath, sessionId: this.sessionId, bridgeGatewayStorage: this.bridgeGatewayStorage, errorHandler: this.errorsHandler.bind(this), messageHandler: this.messagesHandler.bind(this), signal: signal, openingDeadlineMS: openingDeadlineMS }; return yield createEventSource(eventSourceConfig); }), (resource) => __awaiter(this, void 0, void 0, function* () { resource.close(); })); this.bridgeGatewayStorage = new HttpBridgeGatewayStorage(storage, bridgeUrl); } registerSession(options) { return __awaiter(this, void 0, void 0, function* () { yield this.eventSource.create(options === null || options === void 0 ? void 0 : options.signal, options === null || options === void 0 ? void 0 : options.openingDeadlineMS); }); } send(message, receiver, topic, ttlOrOptions) { return __awaiter(this, void 0, void 0, function* () { var _a; // TODO: remove deprecated method const options = {}; if (typeof ttlOrOptions === 'number') { options.ttl = ttlOrOptions; } else { options.ttl = ttlOrOptions === null || ttlOrOptions === void 0 ? void 0 : ttlOrOptions.ttl; options.signal = ttlOrOptions === null || ttlOrOptions === void 0 ? void 0 : ttlOrOptions.signal; options.attempts = ttlOrOptions === null || ttlOrOptions === void 0 ? void 0 : ttlOrOptions.attempts; } const url = new URL(addPathToUrl(this.bridgeUrl, this.postPath)); url.searchParams.append('client_id', this.sessionId); url.searchParams.append('to', receiver); url.searchParams.append('ttl', ((options === null || options === void 0 ? void 0 : options.ttl) || this.defaultTtl).toString()); url.searchParams.append('topic', topic); const body = protocol.Base64.encode(message); yield callForSuccess((options) => __awaiter(this, void 0, void 0, function* () { const response = yield this.post(url, body, options.signal); if (!response.ok) { throw new TonConnectError(`Bridge send failed, status ${response.status}`); } }), { attempts: (_a = options === null || options === void 0 ? void 0 : options.attempts) !== null && _a !== void 0 ? _a : Number.MAX_SAFE_INTEGER, delayMs: this.defaultResendDelay, signal: options === null || options === void 0 ? void 0 : options.signal }); }); } pause() { this.eventSource.dispose().catch(e => logError(`Bridge pause failed, ${e}`)); } unPause() { return __awaiter(this, void 0, void 0, function* () { const RECREATE_WITHOUT_DELAY = 0; yield this.eventSource.recreate(RECREATE_WITHOUT_DELAY); }); } close() { return __awaiter(this, void 0, void 0, function* () { yield this.eventSource.dispose().catch(e => logError(`Bridge close failed, ${e}`)); }); } setListener(listener) { this.listener = listener; } setErrorsListener(errorsListener) { this.errorsListener = errorsListener; } post(url, body, signal) { return __awaiter(this, void 0, void 0, function* () { const response = yield fetch(url, { method: 'post', body: body, signal: signal }); if (!response.ok) { throw new TonConnectError(`Bridge send failed, status ${response.status}`); } return response; }); } errorsHandler(eventSource, e) { return __awaiter(this, void 0, void 0, function* () { if (this.isConnecting) { eventSource.close(); throw new TonConnectError('Bridge error, failed to connect'); } if (this.isReady) { try { this.errorsListener(e); } catch (e) { } return; } if (this.isClosed) { eventSource.close(); logDebug(`Bridge reconnecting, ${this.defaultReconnectDelay}ms delay`); return yield this.eventSource.recreate(this.defaultReconnectDelay); } throw new TonConnectError('Bridge error, unknown state'); }); } messagesHandler(e) { return __awaiter(this, void 0, void 0, function* () { if (e.data === this.heartbeatMessage) { return; } yield this.bridgeGatewayStorage.storeLastEventId(e.lastEventId); if (this.isClosed) { return; } let bridgeIncomingMessage; try { bridgeIncomingMessage = JSON.parse(e.data); } catch (_) { throw new TonConnectError(`Bridge message parse failed, message ${e.data}`); } this.listener(bridgeIncomingMessage); }); } } /** * Creates an event source. * @param {CreateEventSourceConfig} config - Configuration for creating an event source. */ function createEventSource(config) { return __awaiter(this, void 0, void 0, function* () { return yield timeout((resolve, reject, deferOptions) => __awaiter(this, void 0, void 0, function* () { var _a; const abortController = createAbortController(deferOptions.signal); const signal = abortController.signal; if (signal.aborted) { reject(new TonConnectError('Bridge connection aborted')); return; } const url = new URL(addPathToUrl(config.bridgeUrl, config.ssePath)); url.searchParams.append('client_id', config.sessionId); const lastEventId = yield config.bridgeGatewayStorage.getLastEventId(); if (lastEventId) { url.searchParams.append('last_event_id', lastEventId); } if (signal.aborted) { reject(new TonConnectError('Bridge connection aborted')); return; } const eventSource = new EventSource(url.toString()); eventSource.onerror = (reason) => __awaiter(this, void 0, void 0, function* () { if (signal.aborted) { eventSource.close(); reject(new TonConnectError('Bridge connection aborted')); return; } try { const newInstance = yield config.errorHandler(eventSource, reason); if (newInstance !== eventSource) { eventSource.close(); } if (newInstance && newInstance !== eventSource) { resolve(newInstance); } } catch (e) { eventSource.close(); reject(e); } }); eventSource.onopen = () => { if (signal.aborted) { eventSource.close(); reject(new TonConnectError('Bridge connection aborted')); return; } resolve(eventSource); }; eventSource.onmessage = (event) => { if (signal.aborted) { eventSource.close(); reject(new TonConnectError('Bridge connection aborted')); return; } config.messageHandler(event); }; (_a = config.signal) === null || _a === void 0 ? void 0 : _a.addEventListener('abort', () => { eventSource.close(); reject(new TonConnectError('Bridge connection aborted')); }); }), { timeout: config.openingDeadlineMS, signal: config.signal }); }); } const CONNECTION_HTTP_EXPIRATION_TIME = 5 * 60 * 1000; function isPendingConnectionHttp(connection) { return !('connectEvent' in connection); } function isPendingConnectionHttpRaw(connection) { return !('connectEvent' in connection); } function isExpiredPendingConnectionHttpRaw(connection) { var _a; return Date.now() - ((_a = connection.createdAt) !== null && _a !== void 0 ? _a : 0) > CONNECTION_HTTP_EXPIRATION_TIME; } class BridgeConnectionStorage { constructor(storage) { this.storage = storage; this.storeKey = 'ton-connect-storage_bridge-connection'; } storeConnection(connection) { return __awaiter(this, void 0, void 0, function* () { if (connection.type === 'injected') { return this.storage.setItem(this.storeKey, JSON.stringify(connection)); } if (!isPendingConnectionHttp(connection)) { const rawSession = { sessionKeyPair: connection.session.sessionCrypto.stringifyKeypair(), walletPublicKey: connection.session.walletPublicKey, bridgeUrl: connection.session.bridgeUrl }; const rawConnection = { type: 'http', connectEvent: connection.connectEvent, session: rawSession, lastWalletEventId: connection.lastWalletEventId, nextRpcRequestId: connection.nextRpcRequestId }; return this.storage.setItem(this.storeKey, JSON.stringify(rawConnection)); } const rawConnection = { type: 'http', connectionSource: connection.connectionSource, sessionCrypto: connection.sessionCrypto.stringifyKeypair(), createdAt: Date.now() }; return this.storage.setItem(this.storeKey, JSON.stringify(rawConnection)); }); } removeConnection() { return __awaiter(this, void 0, void 0, function* () { return this.storage.removeItem(this.storeKey); }); } getConnection() { return __awaiter(this, void 0, void 0, function* () { const stored = yield this.storage.getItem(this.storeKey); if (!stored) { return null; } const connection = JSON.parse(stored); if (connection.type === 'injected') { return connection; } if (!isPendingConnectionHttpRaw(connection)) { const sessionCrypto = new protocol.SessionCrypto(connection.session.sessionKeyPair); return { type: 'http', connectEvent: connection.connectEvent, lastWalletEventId: connection.lastWalletEventId, nextRpcRequestId: connection.nextRpcRequestId, session: { sessionCrypto, bridgeUrl: connection.session.bridgeUrl, walletPublicKey: connection.session.walletPublicKey } }; } if (isExpiredPendingConnectionHttpRaw(connection)) { yield this.removeConnection(); return null; } return { type: 'http', sessionCrypto: new protocol.SessionCrypto(connection.sessionCrypto), connectionSource: connection.connectionSource }; }); } getHttpConnection() { return __awaiter(this, void 0, void 0, function* () { const connection = yield this.getConnection(); if (!connection) { throw new TonConnectError('Trying to read HTTP connection source while nothing is stored'); } if (connection.type === 'injected') { throw new TonConnectError('Trying to read HTTP connection source while injected connection is stored'); } return connection; }); } getHttpPendingConnection() { return __awaiter(this, void 0, void 0, function* () { const connection = yield this.getConnection(); if (!connection) { throw new TonConnectError('Trying to read HTTP connection source while nothing is stored'); } if (connection.type === 'injected') { throw new TonConnectError('Trying to read HTTP connection source while injected connection is stored'); } if (!isPendingConnectionHttp(connection)) { throw new TonConnectError('Trying to read HTTP-pending connection while http connection is stored'); } return connection; }); } getInjectedConnection() { return __awaiter(this, void 0, void 0, function* () { const connection = yield this.getConnection(); if (!connection) { throw new TonConnectError('Trying to read Injected bridge connection source while nothing is stored'); } if ((connection === null || connection === void 0 ? void 0 : connection.type) === 'http') { throw new TonConnectError('Trying to read Injected bridge connection source while HTTP connection is stored'); } return connection; }); } storedConnectionType() { return __awaiter(this, void 0, void 0, function* () { const stored = yield this.storage.getItem(this.storeKey); if (!stored) { return null; } const connection = JSON.parse(stored); return connection.type; }); } storeLastWalletEventId(id) { return __awaiter(this, void 0, void 0, function* () { const connection = yield this.getConnection(); if (connection && connection.type === 'http' && !isPendingConnectionHttp(connection)) { connection.lastWalletEventId = id; return this.storeConnection(connection); } }); } getLastWalletEventId() { return __awaiter(this, void 0, void 0, function* () { const connection = yield this.getConnection(); if (connection && 'lastWalletEventId' in connection) { return connection.lastWalletEventId; } return undefined; }); } increaseNextRpcRequestId() { return __awaiter(this, void 0, void 0, function* () { const connection = yield this.getConnection(); if (connection && 'nextRpcRequestId' in connection) { const lastId = connection.nextRpcRequestId || 0; connection.nextRpcRequestId = lastId + 1; return this.storeConnection(connection); } }); } getNextRpcRequestId() { return __awaiter(this, void 0, void 0, function* () { const connection = yield this.getConnection(); if (connection && 'nextRpcRequestId' in connection) { return connection.nextRpcRequestId || 0; } return 0; }); } } const PROTOCOL_VERSION = 2; class BridgeProvider { static fromStorage(storage) { return __awaiter(this, void 0, void 0, function* () { const bridgeConnectionStorage = new BridgeConnectionStorage(storage); const connection = yield bridgeConnectionStorage.getHttpConnection(); if (isPendingConnectionHttp(connection)) { return new BridgeProvider(storage, connection.connectionSource); } return new BridgeProvider(storage, { bridgeUrl: connection.session.bridgeUrl }); }); } constructor(storage, walletConnectionSource) { this.storage = storage; this.walletConnectionSource = walletConnectionSource; this.type = 'http'; this.standardUniversalLink = 'tc://'; this.pendingRequests = new Map(); this.session = null; this.gateway = null; this.pendingGateways = []; this.listeners = []; this.defaultOpeningDeadlineMS = 12000; this.defaultRetryTimeoutMS = 2000; this.connectionStorage = new BridgeConnectionStorage(storage); } connect(message, options) { var _a; const abortController = createAbortController(options === null || options === void 0 ? void 0 : options.signal); (_a = this.abortController) === null || _a === void 0 ? void 0 : _a.abort(); this.abortController = abortController; this.closeGateways(); const sessionCrypto = new protocol.SessionCrypto(); this.session = { sessionCrypto, bridgeUrl: 'bridgeUrl' in this.walletConnectionSource ? this.walletConnectionSource.bridgeUrl : '' }; this.connectionStorage .storeConnection({ type: 'http', connectionSource: this.walletConnectionSource, sessionCrypto }) .then(() => __awaiter(this, void 0, void 0, function* () { if (abortController.signal.aborted) { return; } yield callForSuccess(_options => { var _a; return this.openGateways(sessionCrypto, { openingDeadlineMS: (_a = options === null || options === void 0 ? void 0 : options.openingDeadlineMS) !== null && _a !== void 0 ? _a : this.defaultOpeningDeadlineMS, signal: _options === null || _options === void 0 ? void 0 : _options.signal }); }, { attempts: Number.MAX_SAFE_INTEGER, delayMs: this.defaultRetryTimeoutMS, signal: abortController.signal }); })); const universalLink = 'universalLink' in this.walletConnectionSource && this.walletConnectionSource.universalLink ? this.walletConnectionSource.universalLink : this.standardUniversalLink; return this.generateUniversalLink(universalLink, message); } restoreConnection(options) { return __awaiter(this, void 0, void 0, function* () { var _a, _b; const abortController = createAbortController(options === null || options === void 0 ? void 0 : options.signal); (_a = this.abortController) === null || _a === void 0 ? void 0 : _a.abort(); this.abortController = abortController; if (abortController.signal.aborted) { return; } this.closeGateways(); const storedConnection = yield this.connectionStorage.getHttpConnection(); if (!storedConnection) { return; } if (abortController.signal.aborted) { return; } const openingDeadlineMS = (_b = options === null || options === void 0 ? void 0 : options.openingDeadlineMS) !== null && _b !== void 0 ? _b : this.defaultOpeningDeadlineMS; if (isPendingConnectionHttp(storedConnection)) { this.session = { sessionCrypto: storedConnection.sessionCrypto, bridgeUrl: 'bridgeUrl' in this.walletConnectionSource ? this.walletConnectionSource.bridgeUrl : '' }; return yield this.openGateways(storedConnection.sessionCrypto, { openingDeadlineMS: openingDeadlineMS, signal: abortController === null || abortController === void 0 ? void 0 : abortController.signal }); } if (Array.isArray(this.walletConnectionSource)) { throw new TonConnectError('Internal error. Connection source is array while WalletConnectionSourceHTTP was expected.'); } this.session = storedConnection.session; if (this.gateway) { logDebug('Gateway is already opened, closing previous gateway'); yield this.gateway.close(); } this.gateway = new BridgeGateway(this.storage, this.walletConnectionSource.bridgeUrl, storedConnection.session.sessionCrypto.sessionId, this.gatewayListener.bind(this), this.gatewayErrorsListener.bind(this)); if (abortController.signal.aborted) { return; } // notify listeners about stored connection this.listeners.forEach(listener => listener(storedConnection.connectEvent)); // wait for the connection to be opened try { yield callForSuccess(options => this.gateway.registerSession({ openingDeadlineMS: openingDeadlineMS, signal: options.signal }), { attempts: Number.MAX_SAFE_INTEGER, delayMs: this.defaultRetryTimeoutMS, signal: abortController.signal }); } catch (e) { yield this.disconnect({ signal: abortController.signal }); return; } }); } sendRequest(request, optionsOrOnRequestSent) { // TODO: remove deprecated method const options = {}; if (typeof optionsOrOnRequestSent === 'function') { options.onRequestSent = optionsOrOnRequestSent; } else { options.onRequestSent = optionsOrOnRequestSent === null || optionsOrOnRequestSent === void 0 ? void 0 : optionsOrOnRequestSent.onRequestSent; options.signal = optionsOrOnRequestSent === null || optionsOrOnRequestSent === void 0 ? void 0 : optionsOrOnRequestSent.signal; options.attempts = optionsOrOnRequestSent === null || optionsOrOnRequestSent === void 0 ? void 0 : optionsOrOnRequestSent.attempts; } return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { var _a; if (!this.gateway || !this.session || !('walletPublicKey' in this.session)) { throw new TonConnectError('Trying to send bridge request without session'); } const id = (yield this.connectionStorage.getNextRpcRequestId()).toString(); yield this.connectionStorage.increaseNextRpcRequestId(); logDebug('Send http-bridge request:', Object.assign(Object.assign({}, request), { id })); const encodedRequest = this.session.sessionCrypto.encrypt(JSON.stringify(Object.assign(Object.assign({}, request), { id })), protocol.hexToByteArray(this.session.walletPublicKey)); try { 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 }); (_a = options === null || options === void 0 ? void 0 : options.onRequestSent) === null || _a === void 0 ? void 0 : _a.call(options); this.pendingRequests.set(id.toString(), resolve); } catch (e) { reject(e); } })); } closeConnection() { this.closeGateways(); this.listeners = []; this.session = null; this.gateway = null; } disconnect(options) { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { let called = false; let timeoutId = null; const onRequestSent = () => { if (!called) { called = true; this.removeBridgeAndSession().then(resolve); } }; try { this.closeGateways(); const abortController = createAbortController(options === null || options === void 0 ? void 0 : options.signal); timeoutId = setTimeout(() => { abortController.abort(); }, this.defaultOpeningDeadlineMS); yield this.sendRequest({ method: 'disconnect', params: [] }, { onRequestSent: onRequestSent, signal: abortController.signal, attempts: 1 }); } catch (e) { logDebug('Disconnect error:', e); if (!called) { this.removeBridgeAndSession().then(resolve); } } finally { if (timeoutId) { clearTimeout(timeoutId); } onRequestSent(); } })); }); } listen(callback) { this.listeners.push(callback); return () => (this.listeners = this.listeners.filter(listener => listener !== callback)); } pause() { var _a; (_a = this.gateway) === null || _a === void 0 ? void 0 : _a.pause(); this.pendingGateways.forEach(bridge => bridge.pause()); } unPause() { return __awaiter(this, void 0, void 0, function* () { const promises = this.pendingGateways.map(bridge => bridge.unPause()); if (this.gateway) { promises.push(this.gateway.unPause()); } yield Promise.all(promises); }); } pendingGatewaysListener(gateway, bridgeUrl, bridgeIncomingMessage) { return __awaiter(this, void 0, void 0, function* () { if (!this.pendingGateways.includes(gateway)) { yield gateway.close(); return; } this.closeGateways({ except: gateway }); if (this.gateway) { logDebug('Gateway is already opened, closing previous gateway'); yield this.gateway.close(); } this.session.bridgeUrl = bridgeUrl;