diff --git a/packages/walletkit/src/core/EventRouter.ts b/packages/walletkit/src/core/EventRouter.ts index d3313467..6004a986 100644 --- a/packages/walletkit/src/core/EventRouter.ts +++ b/packages/walletkit/src/core/EventRouter.ts @@ -8,6 +8,8 @@ // Event routing and handler coordination +import type { WalletResponseTemplateError } from '@tonconnect/protocol'; + import type { RawBridgeEvent, EventHandler, EventCallback, EventType } from '../types/internal'; import { ConnectHandler } from '../handlers/ConnectHandler'; import { TransactionHandler } from '../handlers/TransactionHandler'; @@ -32,6 +34,23 @@ import type { TonWalletKitOptions } from '../types/config'; const log = globalLogger.createChild('EventRouter'); +export type DispatchBridgeEventSuccess = { + ok: true; + bridgeEvent: BridgeEvent; + notify: () => Promise; +}; + +export type DispatchBridgeEventFailure = + | { ok: false; reason: 'invalid_event' } + | { ok: false; reason: 'no_handler' } + | { + ok: false; + rawEvent: RawBridgeEvent; + errorResult: WalletResponseTemplateError & { id: string }; + }; + +export type DispatchBridgeEventResult = DispatchBridgeEventSuccess | DispatchBridgeEventFailure; + export class EventRouter { private handlers: EventHandler[] = []; private bridgeManager!: BridgeManager; @@ -58,34 +77,65 @@ export class EventRouter { } /** - * Route incoming bridge event to appropriate handler + * Run validation and handler.handle without notifying listeners or sending bridge responses. + * Use {@link routeEvent} for the full path including notify + error responses. */ - async routeEvent(event: RawBridgeEvent): Promise { - // Validate event structure + async handleBridgeEvent(event: RawBridgeEvent): Promise { const validation = validateBridgeEvent(event); if (!validation.isValid) { log.error('Invalid bridge event', { errors: validation.errors }); - return; + return { ok: false, reason: 'invalid_event' }; } + for (const handler of this.handlers) { + if (handler.canHandle(event)) { + const result = await handler.handle(event); + if ('error' in result) { + return { + ok: false, + rawEvent: event, + errorResult: result as WalletResponseTemplateError & { id: string }, + }; + } + const bridgeEvent = result as BridgeEvent; + return { + ok: true, + bridgeEvent, + notify: () => handler.notify(bridgeEvent), + }; + } + } + + return { ok: false, reason: 'no_handler' }; + } + + /** + * Route incoming bridge event to appropriate handler + */ + async routeEvent(event: RawBridgeEvent): Promise { try { - // Find appropriate handler - for (const handler of this.handlers) { - if (handler.canHandle(event)) { - const result = await handler.handle(event); - if ('error' in result) { - this.notifyErrorCallback({ id: result.id, data: { ...event }, error: result.error }); - try { - await this.bridgeManager.sendResponse(event, result); - } catch (error) { - log.error('Error sending response for error event', { error, event, result }); - } - return; - } - await handler.notify(result as BridgeEvent); - break; + const dispatched = await this.handleBridgeEvent(event); + if (!dispatched.ok) { + if ('reason' in dispatched) { + return; + } + this.notifyErrorCallback({ + id: dispatched.errorResult.id, + data: { ...dispatched.rawEvent }, + error: dispatched.errorResult.error, + }); + try { + await this.bridgeManager.sendResponse(dispatched.rawEvent, dispatched.errorResult); + } catch (error) { + log.error('Error sending response for error event', { + error, + event: dispatched.rawEvent, + result: dispatched.errorResult, + }); } + return; } + await dispatched.notify(); } catch (error) { log.error('Error routing event', { error }); throw error; diff --git a/packages/walletkit/src/core/TonWalletKit.ts b/packages/walletkit/src/core/TonWalletKit.ts index 2f7eb742..8a6b85de 100644 --- a/packages/walletkit/src/core/TonWalletKit.ts +++ b/packages/walletkit/src/core/TonWalletKit.ts @@ -522,35 +522,52 @@ export class TonWalletKit implements ITonWalletKit { this.eventRouter.removeErrorCallback(); } - // === URL Processing API === + // === URL Parsing API === /** - * Handle pasted TON Connect URL/link - * Parses the URL and creates a connect request event + * Allow to convert url to ConnectionRequestEvent to use inline way */ - async handleTonConnectUrl(url: string): Promise { + async connectionEventFromUrl(url: string): Promise { await this.ensureInitialized(); try { - // Parse and validate the TON Connect URL - const parsedUrl = this.parseTonConnectUrl(url); - if (!parsedUrl) { - throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Invalid TON Connect URL format', undefined, { + const bridgeEvent = this.connectBridgeEventFromTonConnectUrl(url); + + const dispatched = await this.eventRouter.handleBridgeEvent(bridgeEvent); + if (!dispatched.ok) { + if ('reason' in dispatched) { + throw new WalletKitError( + ERROR_CODES.VALIDATION_ERROR, + dispatched.reason === 'invalid_event' + ? 'Invalid TON Connect bridge event' + : 'No handler for TON Connect bridge event', + undefined, + { url, reason: dispatched.reason }, + ); + } + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'TON Connect request failed', undefined, { url, + error: dispatched.errorResult, }); } + return dispatched.bridgeEvent as ConnectionRequestEvent; + } catch (error) { + log.error('Failed to create connection event from URL', { error, url }); + throw error; + } + } - // Create a bridge event from the parsed URL - const bridgeEvent = this.createConnectEventFromUrl(parsedUrl); - if (!bridgeEvent) { - throw new WalletKitError( - ERROR_CODES.VALIDATION_ERROR, - 'Invalid TON Connect URL - unable to create bridge event', - undefined, - { parsedUrl }, - ); - } + // === URL Processing API === + + /** + * Handle pasted TON Connect URL/link + * Parses the URL and creates a connect request event + */ + async handleTonConnectUrl(url: string): Promise { + await this.ensureInitialized(); + try { + const bridgeEvent = this.connectBridgeEventFromTonConnectUrl(url); await this.eventRouter.routeEvent(bridgeEvent); } catch (error) { log.error('Failed to handle TON Connect URL', { error, url }); @@ -652,6 +669,31 @@ export class TonWalletKit implements ITonWalletKit { }; } + /** + * Parse a TON Connect link into a raw connect bridge event. + * @throws WalletKitError if the URL is invalid or cannot be turned into a connect event + */ + private connectBridgeEventFromTonConnectUrl(url: string): RawBridgeEventConnect { + const parsedUrl = this.parseTonConnectUrl(url); + if (!parsedUrl) { + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Invalid TON Connect URL format', undefined, { + url, + }); + } + + const bridgeEvent = this.createConnectEventFromUrl(parsedUrl); + if (!bridgeEvent) { + throw new WalletKitError( + ERROR_CODES.VALIDATION_ERROR, + 'Invalid TON Connect URL - unable to create bridge event', + undefined, + { parsedUrl }, + ); + } + + return bridgeEvent; + } + // === Request Processing API (Delegated) === async approveConnectRequest(event: ConnectionRequestEvent, response?: ConnectionApprovalResponse): Promise { diff --git a/packages/walletkit/src/types/kit.ts b/packages/walletkit/src/types/kit.ts index 6159043e..308791a4 100644 --- a/packages/walletkit/src/types/kit.ts +++ b/packages/walletkit/src/types/kit.ts @@ -75,6 +75,13 @@ export interface ITonWalletKit { /** List all active sessions */ listSessions(): Promise; + // === URL Parsing API === + + /** + * Allow to convert url to ConnectionRequestEvent to use inline way + */ + connectionEventFromUrl(url: string): Promise; + // === URL Processing === /** Handle pasted TON Connect URL/link */