From 362257fe2184a2179ab6ab62fa0a33a3b73c8609 Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Mon, 2 Feb 2026 10:58:47 +0500 Subject: [PATCH 01/19] feat: update WalletKit types to use Wallet interface and remove unused WalletKitWallet --- .../src/types/walletkit.ts | 34 +++---------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/packages/walletkit-android-bridge/src/types/walletkit.ts b/packages/walletkit-android-bridge/src/types/walletkit.ts index eb5e1c548..4c30e90d7 100644 --- a/packages/walletkit-android-bridge/src/types/walletkit.ts +++ b/packages/walletkit-android-bridge/src/types/walletkit.ts @@ -6,7 +6,7 @@ * */ -import type { WalletAdapter, WalletSigner, Network } from '@ton/walletkit'; +import type { Wallet, WalletAdapter, WalletSigner, Network } from '@ton/walletkit'; /** * Configuration and bridge-facing types for Ton WalletKit. @@ -40,42 +40,18 @@ export interface WalletKitNativeBridgeType { signWithCustomSigner?(signerId: string, bytes: number[]): Promise; } -/** - * Loose wallet type for bridge pass-through. - * Uses unknown/any for methods since Kotlin handles the actual data. - */ -export interface WalletKitWallet { - getWalletId?(): string; - getAddress?(): string; - getBalance?(): Promise; - getClient(): { getAccountTransactions(params: unknown): Promise<{ transactions?: unknown[] }> }; - createTransferTonTransaction(params: unknown): Promise; - createTransferMultiTonTransaction(params: unknown): Promise; - getTransactionPreview?(transaction: unknown): Promise; - sendTransaction(transaction: unknown): Promise; - // NFT methods - getNfts?(params: unknown): Promise; - getNft?(address: string): Promise; - createTransferNftTransaction?(params: unknown): Promise; - createTransferNftRawTransaction?(params: unknown): Promise; - // Jetton methods - getJettons?(params: unknown): Promise; - createTransferJettonTransaction?(params: unknown): Promise; - getJettonBalance?(address: string): Promise; - getJettonWalletAddress?(address: string): Promise; -} export type WalletKitAdapter = WalletAdapter; export type WalletKitSigner = WalletSigner; export interface WalletKitInstance { ensureInitialized?: () => Promise; - getWallets: () => WalletKitWallet[]; - getWallet(walletId: string): WalletKitWallet | undefined; + getWallets: () => Wallet[]; + getWallet(walletId: string): Wallet | undefined; getNetwork?: () => string; removeWallet(walletId: string): Promise; getApiClient(network?: Network): unknown; - addWallet(adapter: unknown): Promise; - handleNewTransaction(wallet: WalletKitWallet, transaction: unknown): Promise; + addWallet(adapter: unknown): Promise; + handleNewTransaction(wallet: Wallet, transaction: unknown): Promise; handleTonConnectUrl(url: string): Promise; listSessions?(): Promise; disconnect?(sessionId?: string): Promise; From 4a80ff22bb3cbaf0c130450a6e795adba1d18bc3 Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Mon, 2 Feb 2026 12:06:16 +0500 Subject: [PATCH 02/19] refactor: enhance type definitions and event handling --- .../src/api/eventListeners.ts | 24 +++- .../src/api/initialization.ts | 17 ++- .../src/api/tonconnect.ts | 39 +++++-- .../src/api/transactions.ts | 3 +- .../src/core/initialization.ts | 69 ++++++----- .../walletkit-android-bridge/src/inject.ts | 15 +-- .../walletkit-android-bridge/src/types/api.ts | 110 +++++++++++------- .../src/types/bridge.ts | 8 +- .../src/types/walletkit.ts | 81 +++++++++---- .../src/utils/internalBrowserResolvers.ts | 6 +- 10 files changed, 239 insertions(+), 133 deletions(-) diff --git a/packages/walletkit-android-bridge/src/api/eventListeners.ts b/packages/walletkit-android-bridge/src/api/eventListeners.ts index 81efb23de..e592ffd59 100644 --- a/packages/walletkit-android-bridge/src/api/eventListeners.ts +++ b/packages/walletkit-android-bridge/src/api/eventListeners.ts @@ -6,15 +6,27 @@ * */ +import type { + ConnectionRequestEvent, + DisconnectionEvent, + RequestErrorEvent, + SendTransactionRequestEvent, + SignDataRequestEvent, +} from '@ton/walletkit'; + /** * Shared event listener references used to manage WalletKit callbacks. */ -export type BridgeEventListener = ((event: unknown) => void) | null; +export type ConnectEventListener = ((event: ConnectionRequestEvent) => void) | null; +export type TransactionEventListener = ((event: SendTransactionRequestEvent) => void) | null; +export type SignDataEventListener = ((event: SignDataRequestEvent) => void) | null; +export type DisconnectEventListener = ((event: DisconnectionEvent) => void) | null; +export type ErrorEventListener = ((event: RequestErrorEvent) => void) | null; export const eventListeners = { - onConnectListener: null as BridgeEventListener, - onTransactionListener: null as BridgeEventListener, - onSignDataListener: null as BridgeEventListener, - onDisconnectListener: null as BridgeEventListener, - onErrorListener: null as BridgeEventListener, + onConnectListener: null as ConnectEventListener, + onTransactionListener: null as TransactionEventListener, + onSignDataListener: null as SignDataEventListener, + onDisconnectListener: null as DisconnectEventListener, + onErrorListener: null as ErrorEventListener, }; diff --git a/packages/walletkit-android-bridge/src/api/initialization.ts b/packages/walletkit-android-bridge/src/api/initialization.ts index a790f75da..b9539659c 100644 --- a/packages/walletkit-android-bridge/src/api/initialization.ts +++ b/packages/walletkit-android-bridge/src/api/initialization.ts @@ -12,6 +12,13 @@ * Simplified bridge for WalletKit initialization and event listener management. */ +import type { + ConnectionRequestEvent, + DisconnectionEvent, + RequestErrorEvent, + SendTransactionRequestEvent, + SignDataRequestEvent, +} from '@ton/walletkit'; import type { WalletKitBridgeInitConfig, SetEventsListenersArgs, WalletKitBridgeEventCallback } from '../types'; import { ensureWalletKitLoaded } from '../core/moduleLoader'; import { initTonWalletKit, requireWalletKit } from '../core/initialization'; @@ -49,7 +56,7 @@ export function setEventsListeners(args?: SetEventsListenersArgs): { ok: true } kit.removeConnectRequestCallback(); } - eventListeners.onConnectListener = (event: unknown) => { + eventListeners.onConnectListener = (event: ConnectionRequestEvent) => { callback('connectRequest', event); }; @@ -59,7 +66,7 @@ export function setEventsListeners(args?: SetEventsListenersArgs): { ok: true } kit.removeTransactionRequestCallback(); } - eventListeners.onTransactionListener = (event: unknown) => { + eventListeners.onTransactionListener = (event: SendTransactionRequestEvent) => { callback('transactionRequest', event); }; @@ -69,7 +76,7 @@ export function setEventsListeners(args?: SetEventsListenersArgs): { ok: true } kit.removeSignDataRequestCallback(); } - eventListeners.onSignDataListener = (event: unknown) => { + eventListeners.onSignDataListener = (event: SignDataRequestEvent) => { callback('signDataRequest', event); }; @@ -79,7 +86,7 @@ export function setEventsListeners(args?: SetEventsListenersArgs): { ok: true } kit.removeDisconnectCallback(); } - eventListeners.onDisconnectListener = (event: unknown) => { + eventListeners.onDisconnectListener = (event: DisconnectionEvent) => { callback('disconnect', event); }; @@ -90,7 +97,7 @@ export function setEventsListeners(args?: SetEventsListenersArgs): { ok: true } kit.removeErrorCallback(); } - eventListeners.onErrorListener = (event: unknown) => { + eventListeners.onErrorListener = (event: RequestErrorEvent) => { callback('requestError', event); }; diff --git a/packages/walletkit-android-bridge/src/api/tonconnect.ts b/packages/walletkit-android-bridge/src/api/tonconnect.ts index d0898d8ef..c6a3bb7ed 100644 --- a/packages/walletkit-android-bridge/src/api/tonconnect.ts +++ b/packages/walletkit-android-bridge/src/api/tonconnect.ts @@ -13,10 +13,24 @@ * Session transformation handled by Kotlin SessionResponseParser. */ +import type { + BridgeEventMessageInfo, + ConnectEvent, + ConnectEventError, + InjectedToExtensionBridgeRequestPayload, + WalletResponse, + DisconnectEvent, +} from '@ton/walletkit'; +import type { JsBridgeTransportMessage } from '../types/bridge'; import type { HandleTonConnectUrlArgs, DisconnectSessionArgs, ProcessInternalBrowserRequestArgs } from '../types'; import { callBridge } from '../utils/bridgeWrapper'; import { ensureInternalBrowserResolverMap } from '../utils/internalBrowserResolvers'; +/** + * TonConnect event payload types that can be returned from processInternalBrowserRequest. + */ +export type TonConnectEventPayload = ConnectEvent | ConnectEventError | WalletResponse | DisconnectEvent; + /** * Handles TonConnect URLs from deep links or QR codes. */ @@ -59,16 +73,16 @@ export async function processInternalBrowserRequest(args: ProcessInternalBrowser // Extract origin (with scheme) from URL - SessionManager.getSessionByDomain expects a parseable URL const domain = args.url ? new URL(args.url).origin : 'internal-browser'; - const messageInfo = { + const messageInfo: BridgeEventMessageInfo = { messageId: args.messageId, tabId: args.messageId, domain, }; - const request: Record = { + const request: InjectedToExtensionBridgeRequestPayload = { id: args.messageId, method: args.method, - params: args.params, + params: args.params ?? [], }; if (kit.processInjectedBridgeRequest) { @@ -78,22 +92,29 @@ export async function processInternalBrowserRequest(args: ProcessInternalBrowser } // Wait for response from jsBridgeTransport (via initialization.ts) - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error(`Request timeout: ${args.messageId}`)); }, 60000); // 60 second timeout const resolverMap = ensureInternalBrowserResolverMap(); resolverMap.set(args.messageId, { - resolve: (response: unknown) => { + resolve: (response: JsBridgeTransportMessage) => { clearTimeout(timeoutId); - if (response && typeof response === 'object' && 'payload' in response) { - resolve((response as { payload?: unknown }).payload ?? response); + // Extract payload from BridgeResponse - that's the actual TonConnect event + if ('payload' in response && response.payload !== undefined) { + resolve(response.payload as TonConnectEventPayload); + } else if ('result' in response && response.result !== undefined) { + resolve(response.result as TonConnectEventPayload); + } else if ('event' in response) { + // BridgeEvent contains the event directly + resolve(response.event as TonConnectEventPayload); } else { - resolve(response); + // Fallback - shouldn't happen in normal flow + reject(new Error('Unexpected response format')); } }, - reject: (error: unknown) => { + reject: (error: Error) => { clearTimeout(timeoutId); reject(error); }, diff --git a/packages/walletkit-android-bridge/src/api/transactions.ts b/packages/walletkit-android-bridge/src/api/transactions.ts index f35dab00a..9dc16bf29 100644 --- a/packages/walletkit-android-bridge/src/api/transactions.ts +++ b/packages/walletkit-android-bridge/src/api/transactions.ts @@ -13,6 +13,7 @@ * All validation, transformation, and formatting happens in Kotlin. */ +import type { Transaction } from '@ton/walletkit'; import type { GetRecentTransactionsArgs, CreateTransferTonTransactionArgs, @@ -26,7 +27,7 @@ import { warn } from '../utils/logger'; * Retrieves recent transactions for a wallet. * Returns raw WalletKit response - transformation happens in Kotlin TransactionResponseParser. */ -export async function getRecentTransactions(args: GetRecentTransactionsArgs): Promise { +export async function getRecentTransactions(args: GetRecentTransactionsArgs): Promise { return callBridge('getRecentTransactions', async (kit) => { const wallet = kit.getWallet?.(args.walletId); diff --git a/packages/walletkit-android-bridge/src/core/initialization.ts b/packages/walletkit-android-bridge/src/core/initialization.ts index 778d768fd..a3d3c0667 100644 --- a/packages/walletkit-android-bridge/src/core/initialization.ts +++ b/packages/walletkit-android-bridge/src/core/initialization.ts @@ -9,10 +9,11 @@ /** * WalletKit initialization helpers used by the bridge entry point. */ +import type { BridgeResponse, BridgeEvent } from '@ton/walletkit'; import { TONCONNECT_BRIDGE_EVENT } from '@ton/walletkit'; import { TONCONNECT_BRIDGE_RESPONSE } from '@ton/walletkit/bridge'; -import type { WalletKitBridgeInitConfig, BridgePayload, WalletKitBridgeEvent, WalletKitInstance } from '../types'; +import type { WalletKitBridgeInitConfig, BridgePayload, WalletKitBridgeEvent, WalletKitInstance, JsBridgeTransportMessage } from '../types'; import { log, warn } from '../utils/logger'; import { walletKit, setWalletKit } from './state'; import { ensureWalletKitLoaded, TonWalletKit } from './moduleLoader'; @@ -29,18 +30,6 @@ export interface InitTonWalletKitDeps { AndroidStorageAdapter: new () => unknown; } -type JsBridgeMessage = { - type: string; - messageId?: string; - payload?: { - event?: string; - [key: string]: unknown; - }; - source?: unknown; - traceId?: string; - [key: string]: unknown; -}; - type NativeStorageBridge = { storageGet: (key: string) => string | null; storageSet: (key: string, value: string) => void; @@ -115,34 +104,42 @@ export async function initTonWalletKit( if (config?.bridgeUrl) { kitOptions.bridge = { bridgeUrl: config.bridgeUrl, - jsBridgeTransport: async (sessionId: string, message: JsBridgeMessage) => { + jsBridgeTransport: async (sessionId: string, message: unknown) => { + // Cast to our transport message type (walletkit types this as unknown) + const typedMessage = message as JsBridgeTransportMessage; + log('[walletkitBridge] 📤 jsBridgeTransport called:', { sessionId, - messageType: message.type, - messageId: message.messageId, - hasPayload: !!message.payload, - payloadEvent: message.payload?.event, + messageType: typedMessage.type, + hasPayload: 'payload' in typedMessage, }); - log('[walletkitBridge] 📤 Full message:', JSON.stringify(message, null, 2)); - - let bridgeMessage: JsBridgeMessage = message; - - if ( - bridgeMessage.type === TONCONNECT_BRIDGE_RESPONSE && - bridgeMessage.payload?.event === 'disconnect' && - !bridgeMessage.messageId - ) { - log('[walletkitBridge] 🔄 Transforming disconnect response to event'); - bridgeMessage = { - type: TONCONNECT_BRIDGE_EVENT, - source: bridgeMessage.source, - event: bridgeMessage.payload, - traceId: bridgeMessage.traceId, - }; - log('[walletkitBridge] 🔄 Transformed message:', JSON.stringify(bridgeMessage, null, 2)); + log('[walletkitBridge] 📤 Full message:', JSON.stringify(typedMessage, null, 2)); + + let bridgeMessage: JsBridgeTransportMessage = typedMessage; + + // Handle disconnect responses that need to be transformed to events + if (bridgeMessage.type === TONCONNECT_BRIDGE_RESPONSE) { + const responseMsg = bridgeMessage as BridgeResponse; + const payload = responseMsg.payload as { event?: string; id?: number } | undefined; + + if (payload?.event === 'disconnect' && !responseMsg.messageId) { + log('[walletkitBridge] 🔄 Transforming disconnect response to event'); + bridgeMessage = { + type: TONCONNECT_BRIDGE_EVENT, + source: responseMsg.source, + event: { + event: 'disconnect', + id: payload.id ?? 0, + payload: {}, + }, + traceId: responseMsg.traceId, + } as BridgeEvent; + log('[walletkitBridge] 🔄 Transformed message:', JSON.stringify(bridgeMessage, null, 2)); + } } - if (bridgeMessage.messageId) { + // Handle responses with messageId (internal browser requests) + if (bridgeMessage.type === TONCONNECT_BRIDGE_RESPONSE && bridgeMessage.messageId) { log('[walletkitBridge] 🔵 Message has messageId, checking for pending promise'); const resolvers = getInternalBrowserResolverMap(); const resolver = resolvers?.get(bridgeMessage.messageId); diff --git a/packages/walletkit-android-bridge/src/inject.ts b/packages/walletkit-android-bridge/src/inject.ts index 19f61d1d4..85c3b0b0f 100644 --- a/packages/walletkit-android-bridge/src/inject.ts +++ b/packages/walletkit-android-bridge/src/inject.ts @@ -8,7 +8,7 @@ // Bridge injection for Android internal browser import { injectBridgeCode, TONCONNECT_BRIDGE_EVENT, TONCONNECT_BRIDGE_REQUEST } from '@ton/walletkit/bridge'; -import type { InjectedToExtensionBridgeRequestPayload, JSBridgeInjectOptions } from '@ton/walletkit'; +import type { BridgeEvent, InjectedToExtensionBridgeRequestPayload, JSBridgeInjectOptions } from '@ton/walletkit'; import type { Transport } from '@ton/walletkit'; import { error } from './utils/logger'; @@ -55,9 +55,9 @@ const isAndroidWebView = typeof tonWindow.AndroidTonConnect !== 'undefined'; class AndroidWebViewTransport implements Transport { private pendingRequests = new Map< string, - { resolve: (value: unknown) => void; reject: (error: Error) => void; timeout: ReturnType } + { resolve: (value: BridgeEvent) => void; reject: (error: Error) => void; timeout: ReturnType } >(); - private eventCallbacks: Array<(event: unknown) => void> = []; + private eventCallbacks: Array<(event: BridgeEvent) => void> = []; constructor() { // Set up notification handlers and postMessage relay @@ -163,11 +163,12 @@ class AndroidWebViewTransport implements Transport { while (bridge.hasEvent(frameId)) { const eventStr = bridge.pullEvent(frameId); if (eventStr) { - const data = JSON.parse(eventStr) as { type?: string; event?: unknown }; + const data = JSON.parse(eventStr) as { type?: string; event?: BridgeEvent }; if (data.type === TONCONNECT_BRIDGE_EVENT && data.event) { + const event = data.event; this.eventCallbacks.forEach((callback) => { try { - callback(data.event); + callback(event); } catch (err) { error('[AndroidTransport] Event callback error:', err); } @@ -180,7 +181,7 @@ class AndroidWebViewTransport implements Transport { } } - async send(request: Omit): Promise { + async send(request: Omit): Promise { const messageId = `msg-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; const bridge = tonWindow.AndroidTonConnect; if (!bridge?.postMessage) { @@ -207,7 +208,7 @@ class AndroidWebViewTransport implements Transport { }); } - onEvent(callback: (event: unknown) => void): void { + onEvent(callback: (event: BridgeEvent) => void): void { this.eventCallbacks.push(callback); } diff --git a/packages/walletkit-android-bridge/src/types/api.ts b/packages/walletkit-android-bridge/src/types/api.ts index 8b00b54db..bf58cb547 100644 --- a/packages/walletkit-android-bridge/src/types/api.ts +++ b/packages/walletkit-android-bridge/src/types/api.ts @@ -6,6 +6,24 @@ * */ +import type { + BridgeEvent, + ConnectionRequestEventPreview, + DAppInfo, + ISigner, + Jetton, + JettonsResponse, + NFT, + NFTsResponse, + SendTransactionResponse, + TokenAmount, + TONConnectSession, + Transaction, + TransactionEmulatedPreview, + TransactionRequest, + Wallet, + WalletAdapter, +} from '@ton/walletkit'; import type { WalletKitBridgeEventCallback } from './events'; import type { WalletKitBridgeInitConfig } from './walletkit'; @@ -86,16 +104,14 @@ export interface CreateTransferMultiTonTransactionArgs { export interface TransactionContentArgs { walletId: string; - transactionContent: unknown; // Can be object (from Kotlin) or string (legacy) + transactionContent: TransactionRequest | string; // Can be object (from Kotlin) or string (legacy) } -export interface TonConnectRequestEvent extends Record { - id?: string; - wallet?: unknown; - walletAddress?: string; - request?: Record & { from?: string }; - preview?: Record & { manifest?: { url?: string } }; - dAppInfo?: Record & { url?: string }; +export interface TonConnectRequestEvent extends BridgeEvent { + wallet?: Wallet; + request?: BridgeEvent & { from?: string }; + preview?: ConnectionRequestEventPreview; + dAppInfo?: DAppInfo; domain?: string; isJsBridge?: boolean; tabId?: string; @@ -182,7 +198,7 @@ export interface CreateTransferNftRawTransactionArgs { walletId: string; nftAddress: string; transferAmount: string; - transferMessage: unknown; + transferMessage: TransactionRequest; } export interface GetJettonsArgs { @@ -211,7 +227,7 @@ export interface GetJettonWalletAddressArgs { export interface ProcessInternalBrowserRequestArgs { messageId: string; method: string; - params?: unknown; + params?: Record; from?: string; url?: string; manifestUrl?: string; @@ -244,7 +260,7 @@ export interface WalletDescriptor { } export interface WalletKitBridgeApi { - init(config?: WalletKitBridgeInitConfig): PromiseOrValue; + init(config?: WalletKitBridgeInitConfig): PromiseOrValue<{ ok: true }>; setEventsListeners(args?: SetEventsListenersArgs): PromiseOrValue<{ ok: true }>; removeEventListeners(): PromiseOrValue<{ ok: true }>; // Returns raw keyPair with Uint8Array - Kotlin handles conversion @@ -253,55 +269,61 @@ export interface WalletKitBridgeApi { sign(args: SignArgs): PromiseOrValue; // Returns mnemonic words array directly createTonMnemonic(args?: CreateTonMnemonicArgs): PromiseOrValue; - // Returns temp ID and raw signer - Kotlin extracts signerId and publicKey - createSigner(args: CreateSignerArgs): PromiseOrValue<{ _tempId: string; signer: unknown }>; - // Returns temp ID and raw adapter - Kotlin extracts adapterId and address - createAdapter(args: CreateAdapterArgs): PromiseOrValue<{ _tempId: string; adapter: unknown }>; + // Returns temp ID and signer - Kotlin extracts signerId and publicKey + createSigner(args: CreateSignerArgs): PromiseOrValue<{ _tempId: string; signer: ISigner }>; + // Returns temp ID and adapter - Kotlin extracts adapterId and address + createAdapter(args: CreateAdapterArgs): PromiseOrValue<{ _tempId: string; adapter: WalletAdapter }>; // Returns address string directly getAdapterAddress(args: { adapterId: string }): PromiseOrValue; // Returns walletId with wallet object, or null - addWallet(args: AddWalletArgs): PromiseOrValue<{ walletId: string | undefined; wallet: unknown } | null>; + addWallet(args: AddWalletArgs): PromiseOrValue<{ walletId: string | undefined; wallet: Wallet } | null>; // Returns array of walletId with wallet objects - getWallets(): PromiseOrValue<{ walletId: string | undefined; wallet: unknown }[]>; + getWallets(): PromiseOrValue<{ walletId: string | undefined; wallet: Wallet }[]>; // Takes walletId, returns walletId with wallet object or null - getWallet(args: { walletId: string }): PromiseOrValue<{ walletId: string | undefined; wallet: unknown } | null>; + getWallet(args: { walletId: string }): PromiseOrValue<{ walletId: string | undefined; wallet: Wallet } | null>; // Returns address string or null directly getWalletAddress(args: { walletId: string }): PromiseOrValue; // Returns void removeWallet(args: RemoveWalletArgs): PromiseOrValue; - // Returns balance - type is unknown since wallet.getBalance() returns Promise - getBalance(args: GetBalanceArgs): PromiseOrValue; + // Returns balance as TokenAmount + getBalance(args: GetBalanceArgs): PromiseOrValue; // Returns transactions array directly - getRecentTransactions(args: GetRecentTransactionsArgs): PromiseOrValue; - handleTonConnectUrl(args: HandleTonConnectUrlArgs): PromiseOrValue; + getRecentTransactions(args: GetRecentTransactionsArgs): PromiseOrValue; + handleTonConnectUrl(args: HandleTonConnectUrlArgs): PromiseOrValue; // Returns transaction and optional preview createTransferTonTransaction( args: CreateTransferTonTransactionArgs, - ): PromiseOrValue<{ transaction: unknown; preview?: unknown }>; + ): PromiseOrValue<{ transaction: TransactionRequest; preview?: TransactionEmulatedPreview }>; createTransferMultiTonTransaction( args: CreateTransferMultiTonTransactionArgs, - ): PromiseOrValue<{ transaction: unknown; preview?: unknown }>; - getTransactionPreview(args: TransactionContentArgs): PromiseOrValue; + ): PromiseOrValue<{ transaction: TransactionRequest; preview?: TransactionEmulatedPreview }>; + getTransactionPreview(args: TransactionContentArgs): PromiseOrValue; handleNewTransaction(args: TransactionContentArgs): PromiseOrValue<{ success: boolean }>; - // Returns raw result from wallet.sendTransaction - sendTransaction(args: TransactionContentArgs): PromiseOrValue; - approveConnectRequest(args: ApproveConnectRequestArgs): PromiseOrValue; - rejectConnectRequest(args: RejectConnectRequestArgs): PromiseOrValue; - approveTransactionRequest(args: ApproveTransactionRequestArgs): PromiseOrValue; - rejectTransactionRequest(args: RejectTransactionRequestArgs): PromiseOrValue; - approveSignDataRequest(args: ApproveSignDataRequestArgs): PromiseOrValue; - rejectSignDataRequest(args: RejectSignDataRequestArgs): PromiseOrValue; - listSessions(): PromiseOrValue; - disconnectSession(args?: DisconnectSessionArgs): PromiseOrValue; - getNfts(args: GetNftsArgs): PromiseOrValue; - getNft(args: GetNftArgs): PromiseOrValue; - createTransferNftTransaction(args: CreateTransferNftTransactionArgs): PromiseOrValue; - createTransferNftRawTransaction(args: CreateTransferNftRawTransactionArgs): PromiseOrValue; - getJettons(args: GetJettonsArgs): PromiseOrValue; - createTransferJettonTransaction(args: CreateTransferJettonTransactionArgs): PromiseOrValue; - getJettonBalance(args: GetJettonBalanceArgs): PromiseOrValue; - getJettonWalletAddress(args: GetJettonWalletAddressArgs): PromiseOrValue; - processInternalBrowserRequest(args: ProcessInternalBrowserRequestArgs): PromiseOrValue; + // Returns result from wallet.sendTransaction + sendTransaction(args: TransactionContentArgs): PromiseOrValue; + approveConnectRequest(args: ApproveConnectRequestArgs): PromiseOrValue; + rejectConnectRequest(args: RejectConnectRequestArgs): PromiseOrValue; + approveTransactionRequest(args: ApproveTransactionRequestArgs): PromiseOrValue<{ signedBoc: string }>; + rejectTransactionRequest(args: RejectTransactionRequestArgs): PromiseOrValue; + approveSignDataRequest(args: ApproveSignDataRequestArgs): PromiseOrValue<{ signature: string; timestamp: number }>; + rejectSignDataRequest(args: RejectSignDataRequestArgs): PromiseOrValue; + listSessions(): PromiseOrValue; + disconnectSession(args?: DisconnectSessionArgs): PromiseOrValue; + getNfts(args: GetNftsArgs): PromiseOrValue; + getNft(args: GetNftArgs): PromiseOrValue; + createTransferNftTransaction( + args: CreateTransferNftTransactionArgs, + ): PromiseOrValue<{ transaction: TransactionRequest; preview?: TransactionEmulatedPreview }>; + createTransferNftRawTransaction( + args: CreateTransferNftRawTransactionArgs, + ): PromiseOrValue<{ transaction: TransactionRequest; preview?: TransactionEmulatedPreview }>; + getJettons(args: GetJettonsArgs): PromiseOrValue; + createTransferJettonTransaction( + args: CreateTransferJettonTransactionArgs, + ): PromiseOrValue<{ transaction: TransactionRequest; preview?: TransactionEmulatedPreview }>; + getJettonBalance(args: GetJettonBalanceArgs): PromiseOrValue; + getJettonWalletAddress(args: GetJettonWalletAddressArgs): PromiseOrValue; + processInternalBrowserRequest(args: ProcessInternalBrowserRequestArgs): PromiseOrValue; emitBrowserPageStarted(args: EmitBrowserPageArgs): PromiseOrValue<{ success: boolean }>; emitBrowserPageFinished(args: EmitBrowserPageArgs): PromiseOrValue<{ success: boolean }>; emitBrowserError(args: EmitBrowserErrorArgs): PromiseOrValue<{ success: boolean }>; diff --git a/packages/walletkit-android-bridge/src/types/bridge.ts b/packages/walletkit-android-bridge/src/types/bridge.ts index 9990d9d2d..0011991f0 100644 --- a/packages/walletkit-android-bridge/src/types/bridge.ts +++ b/packages/walletkit-android-bridge/src/types/bridge.ts @@ -6,6 +6,7 @@ * */ +import type { BridgeResponse, BridgeEvent } from '@ton/walletkit'; import type { WalletKitBridgeEvent } from './events'; import type { WalletKitBridgeApi } from './api'; @@ -13,6 +14,11 @@ export type WalletKitApiMethod = keyof WalletKitBridgeApi; export type DiagnosticStage = 'start' | 'checkpoint' | 'success' | 'error'; +/** + * Union type for all messages passed through jsBridgeTransport from walletkit. + */ +export type JsBridgeTransportMessage = BridgeResponse | BridgeEvent; + export type BridgePayload = | { kind: 'response'; id: string; result?: unknown; error?: { message: string } } | { kind: 'event'; event: WalletKitBridgeEvent } @@ -32,7 +38,7 @@ export type BridgePayload = timestamp: number; message?: string; } - | { kind: 'jsBridgeEvent'; sessionId: string; event: unknown }; + | { kind: 'jsBridgeEvent'; sessionId: string; event: JsBridgeTransportMessage }; export interface CallContext { id: string; diff --git a/packages/walletkit-android-bridge/src/types/walletkit.ts b/packages/walletkit-android-bridge/src/types/walletkit.ts index 4c30e90d7..de966d3b7 100644 --- a/packages/walletkit-android-bridge/src/types/walletkit.ts +++ b/packages/walletkit-android-bridge/src/types/walletkit.ts @@ -6,7 +6,28 @@ * */ -import type { Wallet, WalletAdapter, WalletSigner, Network } from '@ton/walletkit'; +import type { + ApiClient, + BridgeEventMessageInfo, + ConnectionApprovalResponse, + ConnectionRequestEvent, + DeviceInfo, + DisconnectionEvent, + InjectedToExtensionBridgeRequestPayload, + Network, + RequestErrorEvent, + SendTransactionApprovalResponse, + SendTransactionRequestEvent, + SignDataApprovalResponse, + SignDataRequestEvent, + TONConnectSession, + TransactionRequest, + Wallet, + WalletAdapter, + WalletInfo, + WalletSigner, +} from '@ton/walletkit'; +import type { CONNECT_EVENT_ERROR_CODES, SendTransactionRpcResponseError } from '@tonconnect/protocol'; /** * Configuration and bridge-facing types for Ton WalletKit. @@ -15,8 +36,8 @@ export interface WalletKitBridgeInitConfig { bridgeUrl?: string; bridgeName?: string; allowMemoryStorage?: boolean; - walletManifest?: unknown; - deviceInfo?: unknown; + walletManifest?: WalletInfo; + deviceInfo?: DeviceInfo; disableNetworkSend?: boolean; /** * Network configurations matching native SDK format. @@ -49,31 +70,47 @@ export interface WalletKitInstance { getWallet(walletId: string): Wallet | undefined; getNetwork?: () => string; removeWallet(walletId: string): Promise; - getApiClient(network?: Network): unknown; - addWallet(adapter: unknown): Promise; - handleNewTransaction(wallet: Wallet, transaction: unknown): Promise; - handleTonConnectUrl(url: string): Promise; - listSessions?(): Promise; + getApiClient(network?: Network): ApiClient; + addWallet(adapter: WalletAdapter): Promise; + handleNewTransaction(wallet: Wallet, transaction: TransactionRequest): Promise; + handleTonConnectUrl(url: string): Promise; + listSessions?(): Promise; disconnect?(sessionId?: string): Promise; processInjectedBridgeRequest?( - messageInfo: Record, - request: Record, - ): Promise; - onConnectRequest(callback: (event: unknown) => void): void; + messageInfo: BridgeEventMessageInfo, + request: InjectedToExtensionBridgeRequestPayload, + ): Promise; + onConnectRequest(callback: (event: ConnectionRequestEvent) => void): void; removeConnectRequestCallback(): void; - onTransactionRequest(callback: (event: unknown) => void): void; + onTransactionRequest(callback: (event: SendTransactionRequestEvent) => void): void; removeTransactionRequestCallback(): void; - onSignDataRequest(callback: (event: unknown) => void): void; + onSignDataRequest(callback: (event: SignDataRequestEvent) => void): void; removeSignDataRequestCallback(): void; - onDisconnect(callback: (event: unknown) => void): void; + onDisconnect(callback: (event: DisconnectionEvent) => void): void; removeDisconnectCallback(): void; - onRequestError(callback: (event: unknown) => void): void; + onRequestError(callback: (event: RequestErrorEvent) => void): void; removeErrorCallback(): void; // Request approval methods - event and response are separate parameters - approveConnectRequest(event: unknown, response?: unknown): Promise; - rejectConnectRequest(event: unknown, reason?: string, errorCode?: number): Promise; - approveTransactionRequest(event: unknown, response?: unknown): Promise; - rejectTransactionRequest(event: unknown, reason?: string | { code: number; message: string }): Promise; - approveSignDataRequest(event: unknown, response?: unknown): Promise; - rejectSignDataRequest(event: unknown, reason?: string | { code: number; message: string }): Promise; + approveConnectRequest(event: ConnectionRequestEvent, response?: ConnectionApprovalResponse): Promise; + rejectConnectRequest( + event: ConnectionRequestEvent, + reason?: string, + errorCode?: CONNECT_EVENT_ERROR_CODES, + ): Promise; + approveTransactionRequest( + event: SendTransactionRequestEvent, + response?: SendTransactionApprovalResponse, + ): Promise; + rejectTransactionRequest( + event: SendTransactionRequestEvent, + reason?: string | SendTransactionRpcResponseError['error'], + ): Promise; + approveSignDataRequest( + event: SignDataRequestEvent, + response?: SignDataApprovalResponse, + ): Promise; + rejectSignDataRequest( + event: SignDataRequestEvent, + reason?: string | SendTransactionRpcResponseError['error'], + ): Promise; } diff --git a/packages/walletkit-android-bridge/src/utils/internalBrowserResolvers.ts b/packages/walletkit-android-bridge/src/utils/internalBrowserResolvers.ts index e80398815..cc66f63b7 100644 --- a/packages/walletkit-android-bridge/src/utils/internalBrowserResolvers.ts +++ b/packages/walletkit-android-bridge/src/utils/internalBrowserResolvers.ts @@ -6,9 +6,11 @@ * */ +import type { JsBridgeTransportMessage } from '../types/bridge'; + export type InternalBrowserResponseResolver = { - resolve: (response: unknown) => void; - reject: (error: unknown) => void; + resolve: (response: JsBridgeTransportMessage) => void; + reject: (error: Error) => void; }; export type InternalBrowserResolverRegistry = Map; From fc4b020b3a1e1808ab146cbc24f0e252b4e984f0 Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Mon, 2 Feb 2026 12:24:34 +0500 Subject: [PATCH 03/19] refactor: improve type definitions and enhance API method signatures --- .../src/api/eventListeners.ts | 10 +++ .../src/api/initialization.ts | 1 + .../src/api/jettons.ts | 22 +++--- .../walletkit-android-bridge/src/api/nft.ts | 24 ++++--- .../src/api/requests.ts | 72 +++++++++++-------- .../src/api/tonconnect.ts | 22 +++--- .../src/api/transactions.ts | 24 ++++++- .../src/core/initialization.ts | 30 +++++--- .../walletkit-android-bridge/src/inject.ts | 6 +- .../walletkit-android-bridge/src/types/api.ts | 33 +++++---- .../src/types/bridge.ts | 1 + 11 files changed, 160 insertions(+), 85 deletions(-) diff --git a/packages/walletkit-android-bridge/src/api/eventListeners.ts b/packages/walletkit-android-bridge/src/api/eventListeners.ts index e592ffd59..ebef79dee 100644 --- a/packages/walletkit-android-bridge/src/api/eventListeners.ts +++ b/packages/walletkit-android-bridge/src/api/eventListeners.ts @@ -23,6 +23,16 @@ export type SignDataEventListener = ((event: SignDataRequestEvent) => void) | nu export type DisconnectEventListener = ((event: DisconnectionEvent) => void) | null; export type ErrorEventListener = ((event: RequestErrorEvent) => void) | null; +/** + * Union type for all bridge event listeners. + */ +export type BridgeEventListener = + | ConnectEventListener + | TransactionEventListener + | SignDataEventListener + | DisconnectEventListener + | ErrorEventListener; + export const eventListeners = { onConnectListener: null as ConnectEventListener, onTransactionListener: null as TransactionEventListener, diff --git a/packages/walletkit-android-bridge/src/api/initialization.ts b/packages/walletkit-android-bridge/src/api/initialization.ts index b9539659c..1d4d45aa2 100644 --- a/packages/walletkit-android-bridge/src/api/initialization.ts +++ b/packages/walletkit-android-bridge/src/api/initialization.ts @@ -19,6 +19,7 @@ import type { SendTransactionRequestEvent, SignDataRequestEvent, } from '@ton/walletkit'; + import type { WalletKitBridgeInitConfig, SetEventsListenersArgs, WalletKitBridgeEventCallback } from '../types'; import { ensureWalletKitLoaded } from '../core/moduleLoader'; import { initTonWalletKit, requireWalletKit } from '../core/initialization'; diff --git a/packages/walletkit-android-bridge/src/api/jettons.ts b/packages/walletkit-android-bridge/src/api/jettons.ts index 7ccc2e416..f9b592ab0 100644 --- a/packages/walletkit-android-bridge/src/api/jettons.ts +++ b/packages/walletkit-android-bridge/src/api/jettons.ts @@ -12,6 +12,8 @@ * Simplified bridge for jetton balance queries and transfer transactions. */ +import type { JettonsResponse, TransactionRequest, TransactionEmulatedPreview } from '@ton/walletkit'; + import type { GetJettonsArgs, CreateTransferJettonTransactionArgs, @@ -23,20 +25,24 @@ import { callBridge, callOnWalletBridge } from '../utils/bridgeWrapper'; /** * Fetches jetton balances for a wallet with optional pagination. */ -export async function getJettons(args: GetJettonsArgs) { +export async function getJettons(args: GetJettonsArgs): Promise { return callBridge('getJettons', async () => { - return await callOnWalletBridge(args.walletId, 'getJettons', { + return await callOnWalletBridge(args.walletId, 'getJettons', { pagination: args.pagination, }); }); } +type JettonTransactionResult = { transaction: TransactionRequest; preview?: TransactionEmulatedPreview }; + /** * Builds a jetton transfer transaction. */ -export async function createTransferJettonTransaction(args: CreateTransferJettonTransactionArgs) { +export async function createTransferJettonTransaction( + args: CreateTransferJettonTransactionArgs, +): Promise { return callBridge('createTransferJettonTransaction', async () => { - return await callOnWalletBridge(args.walletId, 'createTransferJettonTransaction', { + return await callOnWalletBridge(args.walletId, 'createTransferJettonTransaction', { jettonAddress: args.jettonAddress, amount: args.amount, toAddress: args.toAddress, @@ -48,17 +54,17 @@ export async function createTransferJettonTransaction(args: CreateTransferJetton /** * Retrieves a jetton balance for the specified wallet. */ -export async function getJettonBalance(args: GetJettonBalanceArgs) { +export async function getJettonBalance(args: GetJettonBalanceArgs): Promise { return callBridge('getJettonBalance', async () => { - return await callOnWalletBridge(args.walletId, 'getJettonBalance', args.jettonAddress); + return await callOnWalletBridge(args.walletId, 'getJettonBalance', args.jettonAddress); }); } /** * Resolves the jetton wallet address for a specific jetton contract. */ -export async function getJettonWalletAddress(args: GetJettonWalletAddressArgs) { +export async function getJettonWalletAddress(args: GetJettonWalletAddressArgs): Promise { return callBridge('getJettonWalletAddress', async () => { - return await callOnWalletBridge(args.walletId, 'getJettonWalletAddress', args.jettonAddress); + return await callOnWalletBridge(args.walletId, 'getJettonWalletAddress', args.jettonAddress); }); } diff --git a/packages/walletkit-android-bridge/src/api/nft.ts b/packages/walletkit-android-bridge/src/api/nft.ts index 561cd0535..fd6342eda 100644 --- a/packages/walletkit-android-bridge/src/api/nft.ts +++ b/packages/walletkit-android-bridge/src/api/nft.ts @@ -12,6 +12,8 @@ * Simplified bridge for NFT listing and transfer transactions. */ +import type { NFT, NFTsResponse, TransactionRequest, TransactionEmulatedPreview } from '@ton/walletkit'; + import type { GetNftsArgs, GetNftArgs, @@ -23,9 +25,9 @@ import { callBridge, callOnWalletBridge } from '../utils/bridgeWrapper'; /** * Fetches NFTs owned by a wallet with optional pagination. */ -export async function getNfts(args: GetNftsArgs) { +export async function getNfts(args: GetNftsArgs): Promise { return callBridge('getNfts', async () => { - return await callOnWalletBridge(args.walletId, 'getNfts', { + return await callOnWalletBridge(args.walletId, 'getNfts', { pagination: args.pagination, collectionAddress: args.collectionAddress, indirectOwnership: args.indirectOwnership, @@ -36,18 +38,22 @@ export async function getNfts(args: GetNftsArgs) { /** * Fetches details for a single NFT by address. */ -export async function getNft(args: GetNftArgs) { +export async function getNft(args: GetNftArgs): Promise { return callBridge('getNft', async () => { - return await callOnWalletBridge(args.walletId, 'getNft', args.nftAddress); + return await callOnWalletBridge(args.walletId, 'getNft', args.nftAddress); }); } +type NftTransactionResult = { transaction: TransactionRequest; preview?: TransactionEmulatedPreview }; + /** * Builds an NFT transfer transaction (human-readable parameters). */ -export async function createTransferNftTransaction(args: CreateTransferNftTransactionArgs) { +export async function createTransferNftTransaction( + args: CreateTransferNftTransactionArgs, +): Promise { return callBridge('createTransferNftTransaction', async () => { - return await callOnWalletBridge(args.walletId, 'createTransferNftTransaction', { + return await callOnWalletBridge(args.walletId, 'createTransferNftTransaction', { nftAddress: args.nftAddress, toAddress: args.toAddress, transferAmount: args.transferAmount, @@ -59,9 +65,11 @@ export async function createTransferNftTransaction(args: CreateTransferNftTransa /** * Builds an NFT transfer transaction (raw message parameters). */ -export async function createTransferNftRawTransaction(args: CreateTransferNftRawTransactionArgs) { +export async function createTransferNftRawTransaction( + args: CreateTransferNftRawTransactionArgs, +): Promise { return callBridge('createTransferNftRawTransaction', async () => { - return await callOnWalletBridge(args.walletId, 'createTransferNftRawTransaction', { + return await callOnWalletBridge(args.walletId, 'createTransferNftRawTransaction', { nftAddress: args.nftAddress, transferAmount: args.transferAmount, transferMessage: args.transferMessage, diff --git a/packages/walletkit-android-bridge/src/api/requests.ts b/packages/walletkit-android-bridge/src/api/requests.ts index 3581b777e..9efaad297 100644 --- a/packages/walletkit-android-bridge/src/api/requests.ts +++ b/packages/walletkit-android-bridge/src/api/requests.ts @@ -15,6 +15,8 @@ * The event is stored in args.event and passed directly to TonWalletKit. */ +import type { ConnectionRequestEvent, SendTransactionRequestEvent, SignDataRequestEvent } from '@ton/walletkit'; + import type { ApproveConnectRequestArgs, RejectConnectRequestArgs, @@ -28,37 +30,43 @@ import { log } from '../utils/logger'; /** * Approves a connect request. + * The event object is passed from Kotlin and contains all required fields. */ -export async function approveConnectRequest(args: ApproveConnectRequestArgs) { +export async function approveConnectRequest(args: ApproveConnectRequestArgs): Promise { return callBridge('approveConnectRequest', async (kit) => { log('approveConnectRequest walletId:', args.walletId); - const event = args.event as { walletId?: string; id?: string }; + const event = args.event; if (!event) { throw new Error('Event is required for connect request approval'); } // Set walletId on the event (wallet lookup not needed - wallet is managed by Kotlin) - event.walletId = args.walletId; - - // Pass event and response as separate parameters (new API) - const result = await kit.approveConnectRequest(event, args.response); + (event as { walletId?: string }).walletId = args.walletId; - return result; + // Cast to the expected type - Kotlin sends the full event with all required fields + await kit.approveConnectRequest( + event as unknown as ConnectionRequestEvent, + args.response as Parameters[1], + ); }); } /** * Rejects a connect request. */ -export async function rejectConnectRequest(args: RejectConnectRequestArgs) { +export async function rejectConnectRequest(args: RejectConnectRequestArgs): Promise<{ success: boolean }> { return callBridge('rejectConnectRequest', async (kit) => { - const event = args.event as { id?: string }; + const event = args.event; if (!event) { throw new Error('Event is required for connect request rejection'); } - const result = await kit.rejectConnectRequest(event, args.reason, args.errorCode); + const result = await kit.rejectConnectRequest( + event as unknown as ConnectionRequestEvent, + args.reason, + args.errorCode, + ); return result ?? { success: true }; }); @@ -67,31 +75,34 @@ export async function rejectConnectRequest(args: RejectConnectRequestArgs) { /** * Approves a transaction request. */ -export async function approveTransactionRequest(args: ApproveTransactionRequestArgs) { +export async function approveTransactionRequest(args: ApproveTransactionRequestArgs): Promise<{ signedBoc: string }> { return callBridge('approveTransactionRequest', async (kit) => { - const event = args.event as { walletId?: string; id?: string }; + const event = args.event; if (!event) { throw new Error('Event is required for transaction request approval'); } // Set walletId on the event if (args.walletId) { - event.walletId = args.walletId; + (event as { walletId?: string }).walletId = args.walletId; } - // Pass event and response as separate parameters (new API) - const result = await kit.approveTransactionRequest(event, args.response); + // Cast to the expected type - Kotlin sends the full event with all required fields + const result = await kit.approveTransactionRequest( + event as unknown as SendTransactionRequestEvent, + args.response as Parameters[1], + ); - return result; + return result as { signedBoc: string }; }); } /** * Rejects a transaction request. */ -export async function rejectTransactionRequest(args: RejectTransactionRequestArgs) { +export async function rejectTransactionRequest(args: RejectTransactionRequestArgs): Promise<{ success: boolean }> { return callBridge('rejectTransactionRequest', async (kit) => { - const event = args.event as { id?: string }; + const event = args.event; if (!event) { throw new Error('Event is required for transaction request rejection'); } @@ -102,7 +113,7 @@ export async function rejectTransactionRequest(args: RejectTransactionRequestArg ? { code: args.errorCode, message: args.reason || 'Transaction rejected' } : args.reason; - const result = await kit.rejectTransactionRequest(event, reason); + const result = await kit.rejectTransactionRequest(event as unknown as SendTransactionRequestEvent, reason); return result ?? { success: true }; }); @@ -111,11 +122,13 @@ export async function rejectTransactionRequest(args: RejectTransactionRequestArg /** * Approves a sign-data request. */ -export async function approveSignDataRequest(args: ApproveSignDataRequestArgs) { +export async function approveSignDataRequest( + args: ApproveSignDataRequestArgs, +): Promise<{ signature: string; timestamp: number }> { return callBridge('approveSignDataRequest', async (kit) => { log('approveSignDataRequest args:', args); - const event = args.event as { walletId?: string; id?: string }; + const event = args.event; if (!event) { throw new Error('Event is required for sign-data request approval'); } @@ -124,24 +137,27 @@ export async function approveSignDataRequest(args: ApproveSignDataRequestArgs) { // Set walletId on the event if (args.walletId) { - event.walletId = args.walletId; + (event as { walletId?: string }).walletId = args.walletId; } - // Pass event and response as separate parameters (new API) + // Cast to the expected type - Kotlin sends the full event with all required fields log('approveSignDataRequest calling kit.approveSignDataRequest with event:', event, 'response:', args.response); - const result = await kit.approveSignDataRequest(event, args.response); + const result = await kit.approveSignDataRequest( + event as unknown as SignDataRequestEvent, + args.response as Parameters[1], + ); log('approveSignDataRequest result:', result); - return result; + return result as { signature: string; timestamp: number }; }); } /** * Rejects a sign-data request. */ -export async function rejectSignDataRequest(args: RejectSignDataRequestArgs) { +export async function rejectSignDataRequest(args: RejectSignDataRequestArgs): Promise<{ success: boolean }> { return callBridge('rejectSignDataRequest', async (kit) => { - const event = args.event as { id?: string }; + const event = args.event; if (!event) { throw new Error('Event is required for sign-data request rejection'); } @@ -152,7 +168,7 @@ export async function rejectSignDataRequest(args: RejectSignDataRequestArgs) { ? { code: args.errorCode, message: args.reason || 'Sign data rejected' } : args.reason; - const result = await kit.rejectSignDataRequest(event, reason); + const result = await kit.rejectSignDataRequest(event as unknown as SignDataRequestEvent, reason); return result ?? { success: true }; }); diff --git a/packages/walletkit-android-bridge/src/api/tonconnect.ts b/packages/walletkit-android-bridge/src/api/tonconnect.ts index c6a3bb7ed..639df3619 100644 --- a/packages/walletkit-android-bridge/src/api/tonconnect.ts +++ b/packages/walletkit-android-bridge/src/api/tonconnect.ts @@ -13,24 +13,18 @@ * Session transformation handled by Kotlin SessionResponseParser. */ -import type { - BridgeEventMessageInfo, - ConnectEvent, - ConnectEventError, - InjectedToExtensionBridgeRequestPayload, - WalletResponse, - DisconnectEvent, -} from '@ton/walletkit'; +import type { BridgeEventMessageInfo, InjectedToExtensionBridgeRequestPayload } from '@ton/walletkit'; + import type { JsBridgeTransportMessage } from '../types/bridge'; -import type { HandleTonConnectUrlArgs, DisconnectSessionArgs, ProcessInternalBrowserRequestArgs } from '../types'; +import type { + HandleTonConnectUrlArgs, + DisconnectSessionArgs, + ProcessInternalBrowserRequestArgs, + TonConnectEventPayload, +} from '../types'; import { callBridge } from '../utils/bridgeWrapper'; import { ensureInternalBrowserResolverMap } from '../utils/internalBrowserResolvers'; -/** - * TonConnect event payload types that can be returned from processInternalBrowserRequest. - */ -export type TonConnectEventPayload = ConnectEvent | ConnectEventError | WalletResponse | DisconnectEvent; - /** * Handles TonConnect URLs from deep links or QR codes. */ diff --git a/packages/walletkit-android-bridge/src/api/transactions.ts b/packages/walletkit-android-bridge/src/api/transactions.ts index 9dc16bf29..84f3da0d6 100644 --- a/packages/walletkit-android-bridge/src/api/transactions.ts +++ b/packages/walletkit-android-bridge/src/api/transactions.ts @@ -14,6 +14,7 @@ */ import type { Transaction } from '@ton/walletkit'; + import type { GetRecentTransactionsArgs, CreateTransferTonTransactionArgs, @@ -54,7 +55,14 @@ export async function createTransferTonTransaction(args: CreateTransferTonTransa throw new Error(`Wallet not found: ${args.walletId}`); } - const transaction = await wallet.createTransferTonTransaction(args); + // Map from bridge args to walletkit's TONTransferRequest + const transaction = await wallet.createTransferTonTransaction({ + transferAmount: args.amount, + recipientAddress: args.toAddress, + comment: args.comment, + body: args.body, + stateInit: args.stateInit, + } as Parameters[0]); if (wallet.getTransactionPreview) { try { @@ -80,7 +88,19 @@ export async function createTransferMultiTonTransaction(args: CreateTransferMult throw new Error(`Wallet not found: ${args.walletId}`); } - const transaction = await wallet.createTransferMultiTonTransaction(args); + // Map from bridge args to walletkit's TONTransferRequest[] + const requests = args.messages.map((msg) => ({ + transferAmount: msg.amount, + recipientAddress: msg.toAddress, + comment: msg.comment, + body: msg.body, + stateInit: msg.stateInit, + })); + + // Cast to expected type - implementation accepts TONTransferRequest[] + const transaction = await wallet.createTransferMultiTonTransaction( + requests as unknown as Parameters[0], + ); if (wallet.getTransactionPreview) { try { diff --git a/packages/walletkit-android-bridge/src/core/initialization.ts b/packages/walletkit-android-bridge/src/core/initialization.ts index a3d3c0667..b8e8723b5 100644 --- a/packages/walletkit-android-bridge/src/core/initialization.ts +++ b/packages/walletkit-android-bridge/src/core/initialization.ts @@ -13,7 +13,13 @@ import type { BridgeResponse, BridgeEvent } from '@ton/walletkit'; import { TONCONNECT_BRIDGE_EVENT } from '@ton/walletkit'; import { TONCONNECT_BRIDGE_RESPONSE } from '@ton/walletkit/bridge'; -import type { WalletKitBridgeInitConfig, BridgePayload, WalletKitBridgeEvent, WalletKitInstance, JsBridgeTransportMessage } from '../types'; +import type { + WalletKitBridgeInitConfig, + BridgePayload, + WalletKitBridgeEvent, + WalletKitInstance, + JsBridgeTransportMessage, +} from '../types'; import { log, warn } from '../utils/logger'; import { walletKit, setWalletKit } from './state'; import { ensureWalletKitLoaded, TonWalletKit } from './moduleLoader'; @@ -107,7 +113,7 @@ export async function initTonWalletKit( jsBridgeTransport: async (sessionId: string, message: unknown) => { // Cast to our transport message type (walletkit types this as unknown) const typedMessage = message as JsBridgeTransportMessage; - + log('[walletkitBridge] 📤 jsBridgeTransport called:', { sessionId, messageType: typedMessage.type, @@ -120,19 +126,19 @@ export async function initTonWalletKit( // Handle disconnect responses that need to be transformed to events if (bridgeMessage.type === TONCONNECT_BRIDGE_RESPONSE) { const responseMsg = bridgeMessage as BridgeResponse; - const payload = responseMsg.payload as { event?: string; id?: number } | undefined; - - if (payload?.event === 'disconnect' && !responseMsg.messageId) { + // BridgeResponse has 'result' field, not 'payload' + const result = responseMsg.result as { event?: string; id?: number } | undefined; + + if (result?.event === 'disconnect' && !responseMsg.messageId) { log('[walletkitBridge] 🔄 Transforming disconnect response to event'); bridgeMessage = { type: TONCONNECT_BRIDGE_EVENT, source: responseMsg.source, event: { event: 'disconnect', - id: payload.id ?? 0, + id: result.id ?? 0, payload: {}, }, - traceId: responseMsg.traceId, } as BridgeEvent; log('[walletkitBridge] 🔄 Transformed message:', JSON.stringify(bridgeMessage, null, 2)); } @@ -142,13 +148,15 @@ export async function initTonWalletKit( if (bridgeMessage.type === TONCONNECT_BRIDGE_RESPONSE && bridgeMessage.messageId) { log('[walletkitBridge] 🔵 Message has messageId, checking for pending promise'); const resolvers = getInternalBrowserResolverMap(); - const resolver = resolvers?.get(bridgeMessage.messageId); + // messageId is a number in BridgeResponse, convert to string for resolver map + const messageIdStr = String(bridgeMessage.messageId); + const resolver = resolvers?.get(messageIdStr); if (resolver) { - log('[walletkitBridge] ✅ Resolving response promise for messageId:', bridgeMessage.messageId); - resolvers?.delete(bridgeMessage.messageId); + log('[walletkitBridge] ✅ Resolving response promise for messageId:', messageIdStr); + resolvers?.delete(messageIdStr); resolver.resolve(bridgeMessage); } else { - warn('[walletkitBridge] ⚠️ No pending promise for messageId:', bridgeMessage.messageId); + warn('[walletkitBridge] ⚠️ No pending promise for messageId:', messageIdStr); } } diff --git a/packages/walletkit-android-bridge/src/inject.ts b/packages/walletkit-android-bridge/src/inject.ts index 85c3b0b0f..5b05b06a9 100644 --- a/packages/walletkit-android-bridge/src/inject.ts +++ b/packages/walletkit-android-bridge/src/inject.ts @@ -55,7 +55,11 @@ const isAndroidWebView = typeof tonWindow.AndroidTonConnect !== 'undefined'; class AndroidWebViewTransport implements Transport { private pendingRequests = new Map< string, - { resolve: (value: BridgeEvent) => void; reject: (error: Error) => void; timeout: ReturnType } + { + resolve: (value: BridgeEvent) => void; + reject: (error: Error) => void; + timeout: ReturnType; + } >(); private eventCallbacks: Array<(event: BridgeEvent) => void> = []; diff --git a/packages/walletkit-android-bridge/src/types/api.ts b/packages/walletkit-android-bridge/src/types/api.ts index bf58cb547..776d4b535 100644 --- a/packages/walletkit-android-bridge/src/types/api.ts +++ b/packages/walletkit-android-bridge/src/types/api.ts @@ -9,21 +9,28 @@ import type { BridgeEvent, ConnectionRequestEventPreview, + ConnectEvent, + ConnectEventError, DAppInfo, - ISigner, - Jetton, + DisconnectEvent, JettonsResponse, NFT, NFTsResponse, SendTransactionResponse, - TokenAmount, TONConnectSession, Transaction, TransactionEmulatedPreview, TransactionRequest, Wallet, WalletAdapter, + WalletResponse, + WalletSigner, } from '@ton/walletkit'; + +/** + * TonConnect event payload types that can be returned from processInternalBrowserRequest. + */ +export type TonConnectEventPayload = ConnectEvent | ConnectEventError | WalletResponse | DisconnectEvent; import type { WalletKitBridgeEventCallback } from './events'; import type { WalletKitBridgeInitConfig } from './walletkit'; @@ -270,7 +277,7 @@ export interface WalletKitBridgeApi { // Returns mnemonic words array directly createTonMnemonic(args?: CreateTonMnemonicArgs): PromiseOrValue; // Returns temp ID and signer - Kotlin extracts signerId and publicKey - createSigner(args: CreateSignerArgs): PromiseOrValue<{ _tempId: string; signer: ISigner }>; + createSigner(args: CreateSignerArgs): PromiseOrValue<{ _tempId: string; signer: WalletSigner }>; // Returns temp ID and adapter - Kotlin extracts adapterId and address createAdapter(args: CreateAdapterArgs): PromiseOrValue<{ _tempId: string; adapter: WalletAdapter }>; // Returns address string directly @@ -285,8 +292,8 @@ export interface WalletKitBridgeApi { getWalletAddress(args: { walletId: string }): PromiseOrValue; // Returns void removeWallet(args: RemoveWalletArgs): PromiseOrValue; - // Returns balance as TokenAmount - getBalance(args: GetBalanceArgs): PromiseOrValue; + // Returns balance as string or undefined + getBalance(args: GetBalanceArgs): PromiseOrValue; // Returns transactions array directly getRecentTransactions(args: GetRecentTransactionsArgs): PromiseOrValue; handleTonConnectUrl(args: HandleTonConnectUrlArgs): PromiseOrValue; @@ -302,13 +309,13 @@ export interface WalletKitBridgeApi { // Returns result from wallet.sendTransaction sendTransaction(args: TransactionContentArgs): PromiseOrValue; approveConnectRequest(args: ApproveConnectRequestArgs): PromiseOrValue; - rejectConnectRequest(args: RejectConnectRequestArgs): PromiseOrValue; + rejectConnectRequest(args: RejectConnectRequestArgs): PromiseOrValue<{ success: boolean }>; approveTransactionRequest(args: ApproveTransactionRequestArgs): PromiseOrValue<{ signedBoc: string }>; - rejectTransactionRequest(args: RejectTransactionRequestArgs): PromiseOrValue; + rejectTransactionRequest(args: RejectTransactionRequestArgs): PromiseOrValue<{ success: boolean }>; approveSignDataRequest(args: ApproveSignDataRequestArgs): PromiseOrValue<{ signature: string; timestamp: number }>; - rejectSignDataRequest(args: RejectSignDataRequestArgs): PromiseOrValue; - listSessions(): PromiseOrValue; - disconnectSession(args?: DisconnectSessionArgs): PromiseOrValue; + rejectSignDataRequest(args: RejectSignDataRequestArgs): PromiseOrValue<{ success: boolean }>; + listSessions(): PromiseOrValue<{ items: TONConnectSession[] }>; + disconnectSession(args?: DisconnectSessionArgs): PromiseOrValue<{ ok: boolean }>; getNfts(args: GetNftsArgs): PromiseOrValue; getNft(args: GetNftArgs): PromiseOrValue; createTransferNftTransaction( @@ -321,9 +328,9 @@ export interface WalletKitBridgeApi { createTransferJettonTransaction( args: CreateTransferJettonTransactionArgs, ): PromiseOrValue<{ transaction: TransactionRequest; preview?: TransactionEmulatedPreview }>; - getJettonBalance(args: GetJettonBalanceArgs): PromiseOrValue; + getJettonBalance(args: GetJettonBalanceArgs): PromiseOrValue; getJettonWalletAddress(args: GetJettonWalletAddressArgs): PromiseOrValue; - processInternalBrowserRequest(args: ProcessInternalBrowserRequestArgs): PromiseOrValue; + processInternalBrowserRequest(args: ProcessInternalBrowserRequestArgs): PromiseOrValue; emitBrowserPageStarted(args: EmitBrowserPageArgs): PromiseOrValue<{ success: boolean }>; emitBrowserPageFinished(args: EmitBrowserPageArgs): PromiseOrValue<{ success: boolean }>; emitBrowserError(args: EmitBrowserErrorArgs): PromiseOrValue<{ success: boolean }>; diff --git a/packages/walletkit-android-bridge/src/types/bridge.ts b/packages/walletkit-android-bridge/src/types/bridge.ts index 0011991f0..34d4b532e 100644 --- a/packages/walletkit-android-bridge/src/types/bridge.ts +++ b/packages/walletkit-android-bridge/src/types/bridge.ts @@ -7,6 +7,7 @@ */ import type { BridgeResponse, BridgeEvent } from '@ton/walletkit'; + import type { WalletKitBridgeEvent } from './events'; import type { WalletKitBridgeApi } from './api'; From 5a598faf97a5e47bef066a83871e59b1394984b8 Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Mon, 2 Feb 2026 12:49:24 +0500 Subject: [PATCH 04/19] refactor: improve error handling and enhance internal request processing --- .../src/adapters/AndroidStorageAdapter.ts | 2 -- .../src/api/tonconnect.ts | 19 +++++++++++++++++-- .../src/api/wallets.ts | 4 ++-- .../src/core/initialization.ts | 3 ++- .../src/core/moduleLoader.ts | 4 ++++ .../walletkit-android-bridge/src/inject.ts | 2 +- .../src/utils/bridgeWrapper.ts | 8 +++++++- 7 files changed, 33 insertions(+), 9 deletions(-) diff --git a/packages/walletkit-android-bridge/src/adapters/AndroidStorageAdapter.ts b/packages/walletkit-android-bridge/src/adapters/AndroidStorageAdapter.ts index 7b953b6d0..7f2eb846d 100644 --- a/packages/walletkit-android-bridge/src/adapters/AndroidStorageAdapter.ts +++ b/packages/walletkit-android-bridge/src/adapters/AndroidStorageAdapter.ts @@ -39,7 +39,6 @@ export class AndroidStorageAdapter implements StorageAdapter { async get(key: string): Promise { try { const value = this.androidBridge.storageGet(key); - log('[AndroidStorageAdapter] get:', key, '=', value ? `${value.substring(0, 100)}...` : 'null'); if (!value) { return null; } @@ -53,7 +52,6 @@ export class AndroidStorageAdapter implements StorageAdapter { async set(key: string, value: T): Promise { try { const serialized = JSON.stringify(value); - log('[AndroidStorageAdapter] set:', key, '=', serialized.substring(0, 100) + '...'); this.androidBridge.storageSet(key, serialized); } catch (err) { error('[AndroidStorageAdapter] Failed to set key:', key, err); diff --git a/packages/walletkit-android-bridge/src/api/tonconnect.ts b/packages/walletkit-android-bridge/src/api/tonconnect.ts index 639df3619..b23b9efe2 100644 --- a/packages/walletkit-android-bridge/src/api/tonconnect.ts +++ b/packages/walletkit-android-bridge/src/api/tonconnect.ts @@ -58,6 +58,21 @@ export async function disconnectSession(args?: DisconnectSessionArgs) { }); } +/** Default timeout for internal browser requests in milliseconds */ +const INTERNAL_BROWSER_REQUEST_TIMEOUT_MS = 60000; + +/** + * Safely extracts origin from URL, falling back to default if parsing fails. + */ +function safeGetOrigin(url: string | undefined, fallback: string): string { + if (!url) return fallback; + try { + return new URL(url).origin; + } catch { + return fallback; + } +} + /** * Processes requests from the in-app browser TonConnect bridge. * Domain resolution and request preparation handled by Kotlin InternalBrowserRequestProcessor. @@ -65,7 +80,7 @@ export async function disconnectSession(args?: DisconnectSessionArgs) { export async function processInternalBrowserRequest(args: ProcessInternalBrowserRequestArgs) { return callBridge('processInternalBrowserRequest', async (kit) => { // Extract origin (with scheme) from URL - SessionManager.getSessionByDomain expects a parseable URL - const domain = args.url ? new URL(args.url).origin : 'internal-browser'; + const domain = safeGetOrigin(args.url, 'internal-browser'); const messageInfo: BridgeEventMessageInfo = { messageId: args.messageId, @@ -89,7 +104,7 @@ export async function processInternalBrowserRequest(args: ProcessInternalBrowser return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error(`Request timeout: ${args.messageId}`)); - }, 60000); // 60 second timeout + }, INTERNAL_BROWSER_REQUEST_TIMEOUT_MS); const resolverMap = ensureInternalBrowserResolverMap(); resolverMap.set(args.messageId, { diff --git a/packages/walletkit-android-bridge/src/api/wallets.ts b/packages/walletkit-android-bridge/src/api/wallets.ts index b4b354b2e..fb93a73af 100644 --- a/packages/walletkit-android-bridge/src/api/wallets.ts +++ b/packages/walletkit-android-bridge/src/api/wallets.ts @@ -129,7 +129,7 @@ export async function createSigner(args: CreateSignerArgs) { : ((await Signer!.fromPrivateKey(args.secretKey!)) as SignerInstance); // Store signer with temp ID for Kotlin to retrieve - const tempId = `signer_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const tempId = `signer_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; signerStore.set(tempId, signer); return { _tempId: tempId, signer }; @@ -153,7 +153,7 @@ export async function createAdapter(args: CreateAdapterArgs) { })) as AdapterInstance; // Store adapter with temp ID for Kotlin to retrieve - const tempId = `adapter_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const tempId = `adapter_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; adapterStore.set(tempId, adapter); // Return only the temp ID and the raw adapter object diff --git a/packages/walletkit-android-bridge/src/core/initialization.ts b/packages/walletkit-android-bridge/src/core/initialization.ts index b8e8723b5..2f806e883 100644 --- a/packages/walletkit-android-bridge/src/core/initialization.ts +++ b/packages/walletkit-android-bridge/src/core/initialization.ts @@ -124,12 +124,13 @@ export async function initTonWalletKit( let bridgeMessage: JsBridgeTransportMessage = typedMessage; // Handle disconnect responses that need to be transformed to events + const DISCONNECT_EVENT = 'disconnect'; if (bridgeMessage.type === TONCONNECT_BRIDGE_RESPONSE) { const responseMsg = bridgeMessage as BridgeResponse; // BridgeResponse has 'result' field, not 'payload' const result = responseMsg.result as { event?: string; id?: number } | undefined; - if (result?.event === 'disconnect' && !responseMsg.messageId) { + if (result?.event === DISCONNECT_EVENT && !responseMsg.messageId) { log('[walletkitBridge] 🔄 Transforming disconnect response to event'); bridgeMessage = { type: TONCONNECT_BRIDGE_EVENT, diff --git a/packages/walletkit-android-bridge/src/core/moduleLoader.ts b/packages/walletkit-android-bridge/src/core/moduleLoader.ts index ce84504b4..89eddde8a 100644 --- a/packages/walletkit-android-bridge/src/core/moduleLoader.ts +++ b/packages/walletkit-android-bridge/src/core/moduleLoader.ts @@ -52,6 +52,7 @@ export let WalletV5R1Adapter: AdapterFactory | null = null; /** * Ensures WalletKit and TON core modules are loaded once and cached. + * @throws Error if required modules fail to load */ export async function ensureWalletKitLoaded(): Promise { if (TonWalletKit && Signer && MnemonicToKeyPair && DefaultSignature && WalletV4R2Adapter && WalletV5R1Adapter) { @@ -67,6 +68,9 @@ export async function ensureWalletKitLoaded(): Promise { !WalletV5R1Adapter ) { const module = (await walletKitModulePromise) as unknown as WalletKitModule; + if (!module.TonWalletKit) { + throw new Error('Failed to load TonWalletKit module'); + } TonWalletKit = module.TonWalletKit; CreateTonMnemonic = module.CreateTonMnemonic ?? CreateTonMnemonic; MnemonicToKeyPair = module.MnemonicToKeyPair ?? MnemonicToKeyPair; diff --git a/packages/walletkit-android-bridge/src/inject.ts b/packages/walletkit-android-bridge/src/inject.ts index 5b05b06a9..867369b92 100644 --- a/packages/walletkit-android-bridge/src/inject.ts +++ b/packages/walletkit-android-bridge/src/inject.ts @@ -44,7 +44,7 @@ const tonWindow = window as TonConnectWindow; const frameId = tonWindow.__tonconnect_frameId || (tonWindow.__tonconnect_frameId = - window === window.top ? 'main' : `frame-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`); + window === window.top ? 'main' : `frame-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`); const isAndroidWebView = typeof tonWindow.AndroidTonConnect !== 'undefined'; diff --git a/packages/walletkit-android-bridge/src/utils/bridgeWrapper.ts b/packages/walletkit-android-bridge/src/utils/bridgeWrapper.ts index 45e7fdeeb..6b1e8b21f 100644 --- a/packages/walletkit-android-bridge/src/utils/bridgeWrapper.ts +++ b/packages/walletkit-android-bridge/src/utils/bridgeWrapper.ts @@ -46,7 +46,13 @@ export async function callBridge(_method: string, operation: (kit: WalletKitI export async function callOnWalletBridge(walletId: string, method: string, args?: unknown): Promise { return callBridge(`wallet.${method}`, async (kit) => { const wallet = kit.getWallet?.(walletId); - const methodRef = (wallet as unknown as Record)?.[method]; + if (!wallet) { + throw new Error(`Wallet not found: ${walletId}`); + } + const methodRef = (wallet as unknown as Record)[method]; + if (typeof methodRef !== 'function') { + throw new Error(`Method '${method}' not found on wallet`); + } return (await (methodRef as (args?: unknown) => Promise).call(wallet, args)) as T; }); } From 1898da20dd2e79465ebef49e4cb3fc33cd71f003 Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Wed, 4 Feb 2026 13:08:33 +0500 Subject: [PATCH 05/19] refactor: consolidate bridge calls and remove redundant code --- .../src/api/browser.ts | 41 +--- .../src/api/cryptography.ts | 63 ++---- .../walletkit-android-bridge/src/api/index.ts | 8 +- .../src/api/initialization.ts | 11 +- .../src/api/jettons.ts | 62 +----- .../walletkit-android-bridge/src/api/nft.ts | 70 +----- .../src/api/requests.ts | 168 ++------------ .../src/api/tonconnect.ts | 151 ++++--------- .../src/api/transactions.ts | 168 +------------- .../src/api/wallets.ts | 207 +++++++----------- .../src/core/initialization.ts | 13 -- .../src/core/moduleLoader.ts | 4 +- .../src/transport/index.ts | 11 - .../walletkit-android-bridge/src/types/api.ts | 25 +-- .../src/utils/bridge.ts | 130 +++++++++++ .../src/utils/bridgeWrapper.ts | 58 ----- .../src/utils/index.ts | 9 - 17 files changed, 335 insertions(+), 864 deletions(-) delete mode 100644 packages/walletkit-android-bridge/src/transport/index.ts create mode 100644 packages/walletkit-android-bridge/src/utils/bridge.ts delete mode 100644 packages/walletkit-android-bridge/src/utils/bridgeWrapper.ts delete mode 100644 packages/walletkit-android-bridge/src/utils/index.ts diff --git a/packages/walletkit-android-bridge/src/api/browser.ts b/packages/walletkit-android-bridge/src/api/browser.ts index ad39bf08d..f0e17afba 100644 --- a/packages/walletkit-android-bridge/src/api/browser.ts +++ b/packages/walletkit-android-bridge/src/api/browser.ts @@ -9,49 +9,24 @@ /** * Internal browser events dispatched back to the native layer. */ -import type { EmitBrowserPageArgs, EmitBrowserErrorArgs, EmitBrowserBridgeRequestArgs } from '../types'; import { emit } from '../transport/messaging'; -/** - * Signals that the internal browser started loading a page. - * - * @param args - Page metadata. - */ -export function emitBrowserPageStarted(args: EmitBrowserPageArgs) { - emit('browserPageStarted', { url: args.url }); +export function emitBrowserPageStarted(args: { url: string }) { + emit('browserPageStarted', args); return { success: true }; } -/** - * Signals that the internal browser finished loading a page. - * - * @param args - Page metadata. - */ -export function emitBrowserPageFinished(args: EmitBrowserPageArgs) { - emit('browserPageFinished', { url: args.url }); +export function emitBrowserPageFinished(args: { url: string }) { + emit('browserPageFinished', args); return { success: true }; } -/** - * Reports a browser error to the native layer. - * - * @param args - Error details. - */ -export function emitBrowserError(args: EmitBrowserErrorArgs) { - emit('browserError', { message: args.message }); +export function emitBrowserError(args: { message: string }) { + emit('browserError', args); return { success: true }; } -/** - * Emits a TonConnect bridge request originating in the internal browser. - * - * @param args - Request metadata for analytics/UI. - */ -export function emitBrowserBridgeRequest(args: EmitBrowserBridgeRequestArgs) { - emit('browserBridgeRequest', { - messageId: args.messageId, - method: args.method, - request: args.request, - }); +export function emitBrowserBridgeRequest(args: { messageId: string; method: string; request: unknown }) { + emit('browserBridgeRequest', args); return { success: true }; } diff --git a/packages/walletkit-android-bridge/src/api/cryptography.ts b/packages/walletkit-android-bridge/src/api/cryptography.ts index c1c487b30..57a767b0c 100644 --- a/packages/walletkit-android-bridge/src/api/cryptography.ts +++ b/packages/walletkit-android-bridge/src/api/cryptography.ts @@ -8,64 +8,33 @@ /** * Cryptographic helpers backed by WalletKit and custom signer coordination. - * - * Note: Array.from() conversions for Uint8Array → number[] are necessary glue code. - * JSON.stringify cannot serialize Uint8Array, so we must convert to number arrays - * for RPC communication between JS and Kotlin layers. */ import type { Hex } from '@ton/walletkit'; -import type { MnemonicToKeyPairArgs, SignArgs, CreateTonMnemonicArgs } from '../types'; import { CreateTonMnemonic, MnemonicToKeyPair, DefaultSignature } from '../core/moduleLoader'; -import { callBridge } from '../utils/bridgeWrapper'; -/** - * Signs data using a custom signer stored in Kotlin. - * This is called by custom signer wrappers created in createAdapter. - */ export async function signWithCustomSigner(signerId: string, bytes: Uint8Array): Promise { - const result = await callBridge('signWithCustomSigner', async () => { - // Call back to Kotlin's SignerManager - return window.WalletKitNative?.signWithCustomSigner?.(signerId, Array.from(bytes)); - }); + const result = await window.WalletKitNative?.signWithCustomSigner?.(signerId, Array.from(bytes)); return result as Hex; } -/** - * Converts a mnemonic phrase to a key pair (public + secret keys). - * Returns raw keyPair object - Kotlin handles Uint8Array to ByteArray conversion. - * - * @param args - Mnemonic words and optional type ('ton' or 'bip39'). - */ -export async function mnemonicToKeyPair(args: MnemonicToKeyPairArgs) { - return callBridge('mnemonicToKeyPair', async () => { - return await MnemonicToKeyPair!(args.mnemonic, args.mnemonicType ?? 'ton'); - }); +export async function mnemonicToKeyPair(args: { mnemonic: string[]; mnemonicType?: string }) { + if (!MnemonicToKeyPair) { + throw new Error('MnemonicToKeyPair module not loaded'); + } + return MnemonicToKeyPair(args.mnemonic, args.mnemonicType ?? 'ton'); } -/** - * Signs arbitrary data using a secret key. - * Returns signature hex string directly. - * - * @param args - Data bytes and secret key bytes. - */ -export async function sign(args: SignArgs) { - return callBridge('sign', async () => { - const dataBytes = Uint8Array.from(args.data); - const secretKeyBytes = Uint8Array.from(args.secretKey); - return DefaultSignature!(dataBytes, secretKeyBytes); - }); +export async function sign(args: { data: number[]; secretKey: number[] }) { + if (!DefaultSignature) { + throw new Error('DefaultSignature module not loaded'); + } + return DefaultSignature(Uint8Array.from(args.data), Uint8Array.from(args.secretKey)); } -/** - * Generates a TON mnemonic phrase. - * Returns array of words directly. - * - * @param _args - Optional generation parameters. - */ -export async function createTonMnemonic(_args: CreateTonMnemonicArgs = { count: 24 }) { - return callBridge('createTonMnemonic', async () => { - const mnemonicResult = await CreateTonMnemonic!(); - return Array.isArray(mnemonicResult) ? mnemonicResult : `${mnemonicResult}`.split(' ').filter(Boolean); - }); +export async function createTonMnemonic() { + if (!CreateTonMnemonic) { + throw new Error('CreateTonMnemonic module not loaded'); + } + return CreateTonMnemonic(); } diff --git a/packages/walletkit-android-bridge/src/api/index.ts b/packages/walletkit-android-bridge/src/api/index.ts index 52207f0fb..28629bf0f 100644 --- a/packages/walletkit-android-bridge/src/api/index.ts +++ b/packages/walletkit-android-bridge/src/api/index.ts @@ -23,7 +23,7 @@ import { eventListeners } from './eventListeners'; export { eventListeners }; -const apiImpl: WalletKitBridgeApi = { +export const api: WalletKitBridgeApi = { // Initialization init: initialization.init, setEventsListeners: initialization.setEventsListeners, @@ -40,7 +40,7 @@ const apiImpl: WalletKitBridgeApi = { getAdapterAddress: wallets.getAdapterAddress, addWallet: wallets.addWallet, getWallets: wallets.getWallets, - getWallet: wallets.getWallet, + getWallet: wallets.getWalletById, getWalletAddress: wallets.getWalletAddress, removeWallet: wallets.removeWallet, getBalance: wallets.getBalance, @@ -84,8 +84,6 @@ const apiImpl: WalletKitBridgeApi = { emitBrowserPageFinished: browser.emitBrowserPageFinished, emitBrowserError: browser.emitBrowserError, emitBrowserBridgeRequest: browser.emitBrowserBridgeRequest, -}; - -export const api = apiImpl; +} as unknown as WalletKitBridgeApi; export type { BridgeEventListener } from './eventListeners'; diff --git a/packages/walletkit-android-bridge/src/api/initialization.ts b/packages/walletkit-android-bridge/src/api/initialization.ts index 1d4d45aa2..0266f3772 100644 --- a/packages/walletkit-android-bridge/src/api/initialization.ts +++ b/packages/walletkit-android-bridge/src/api/initialization.ts @@ -22,7 +22,8 @@ import type { import type { WalletKitBridgeInitConfig, SetEventsListenersArgs, WalletKitBridgeEventCallback } from '../types'; import { ensureWalletKitLoaded } from '../core/moduleLoader'; -import { initTonWalletKit, requireWalletKit } from '../core/initialization'; +import { initTonWalletKit } from '../core/initialization'; +import { getKit } from '../utils/bridge'; import { emit } from '../transport/messaging'; import { postToNative } from '../transport/nativeBridge'; import { eventListeners } from './eventListeners'; @@ -44,8 +45,8 @@ export async function init(config?: WalletKitBridgeInitConfig) { /** * Registers bridge event listeners, proxying WalletKit events to the native layer. */ -export function setEventsListeners(args?: SetEventsListenersArgs): { ok: true } { - const kit = requireWalletKit(); +export async function setEventsListeners(args?: SetEventsListenersArgs): Promise<{ ok: true }> { + const kit = await getKit(); const callback: WalletKitBridgeEventCallback = args?.callback ?? @@ -110,8 +111,8 @@ export function setEventsListeners(args?: SetEventsListenersArgs): { ok: true } /** * Removes all previously registered bridge event listeners. */ -export function removeEventListeners(): { ok: true } { - const kit = requireWalletKit(); +export async function removeEventListeners(): Promise<{ ok: true }> { + const kit = await getKit(); if (eventListeners.onConnectListener) { kit.removeConnectRequestCallback(); diff --git a/packages/walletkit-android-bridge/src/api/jettons.ts b/packages/walletkit-android-bridge/src/api/jettons.ts index f9b592ab0..45755b5bb 100644 --- a/packages/walletkit-android-bridge/src/api/jettons.ts +++ b/packages/walletkit-android-bridge/src/api/jettons.ts @@ -9,62 +9,12 @@ /** * jettons.ts – Jetton operations * - * Simplified bridge for jetton balance queries and transfer transactions. + * Minimal bridge for jetton operations. */ -import type { JettonsResponse, TransactionRequest, TransactionEmulatedPreview } from '@ton/walletkit'; +import { walletCall } from '../utils/bridge'; -import type { - GetJettonsArgs, - CreateTransferJettonTransactionArgs, - GetJettonBalanceArgs, - GetJettonWalletAddressArgs, -} from '../types'; -import { callBridge, callOnWalletBridge } from '../utils/bridgeWrapper'; - -/** - * Fetches jetton balances for a wallet with optional pagination. - */ -export async function getJettons(args: GetJettonsArgs): Promise { - return callBridge('getJettons', async () => { - return await callOnWalletBridge(args.walletId, 'getJettons', { - pagination: args.pagination, - }); - }); -} - -type JettonTransactionResult = { transaction: TransactionRequest; preview?: TransactionEmulatedPreview }; - -/** - * Builds a jetton transfer transaction. - */ -export async function createTransferJettonTransaction( - args: CreateTransferJettonTransactionArgs, -): Promise { - return callBridge('createTransferJettonTransaction', async () => { - return await callOnWalletBridge(args.walletId, 'createTransferJettonTransaction', { - jettonAddress: args.jettonAddress, - amount: args.amount, - toAddress: args.toAddress, - comment: args.comment, - }); - }); -} - -/** - * Retrieves a jetton balance for the specified wallet. - */ -export async function getJettonBalance(args: GetJettonBalanceArgs): Promise { - return callBridge('getJettonBalance', async () => { - return await callOnWalletBridge(args.walletId, 'getJettonBalance', args.jettonAddress); - }); -} - -/** - * Resolves the jetton wallet address for a specific jetton contract. - */ -export async function getJettonWalletAddress(args: GetJettonWalletAddressArgs): Promise { - return callBridge('getJettonWalletAddress', async () => { - return await callOnWalletBridge(args.walletId, 'getJettonWalletAddress', args.jettonAddress); - }); -} +export const getJettons = (args: { walletId: string }) => walletCall('getJettons', args); +export const createTransferJettonTransaction = (args: { walletId: string }) => walletCall('createTransferJettonTransaction', args); +export const getJettonBalance = (args: { walletId: string }) => walletCall('getJettonBalance', args); +export const getJettonWalletAddress = (args: { walletId: string }) => walletCall('getJettonWalletAddress', args); diff --git a/packages/walletkit-android-bridge/src/api/nft.ts b/packages/walletkit-android-bridge/src/api/nft.ts index fd6342eda..6c2c2b330 100644 --- a/packages/walletkit-android-bridge/src/api/nft.ts +++ b/packages/walletkit-android-bridge/src/api/nft.ts @@ -9,70 +9,12 @@ /** * nft.ts – NFT operations * - * Simplified bridge for NFT listing and transfer transactions. + * Minimal bridge for NFT operations. */ -import type { NFT, NFTsResponse, TransactionRequest, TransactionEmulatedPreview } from '@ton/walletkit'; +import { walletCall } from '../utils/bridge'; -import type { - GetNftsArgs, - GetNftArgs, - CreateTransferNftTransactionArgs, - CreateTransferNftRawTransactionArgs, -} from '../types'; -import { callBridge, callOnWalletBridge } from '../utils/bridgeWrapper'; - -/** - * Fetches NFTs owned by a wallet with optional pagination. - */ -export async function getNfts(args: GetNftsArgs): Promise { - return callBridge('getNfts', async () => { - return await callOnWalletBridge(args.walletId, 'getNfts', { - pagination: args.pagination, - collectionAddress: args.collectionAddress, - indirectOwnership: args.indirectOwnership, - }); - }); -} - -/** - * Fetches details for a single NFT by address. - */ -export async function getNft(args: GetNftArgs): Promise { - return callBridge('getNft', async () => { - return await callOnWalletBridge(args.walletId, 'getNft', args.nftAddress); - }); -} - -type NftTransactionResult = { transaction: TransactionRequest; preview?: TransactionEmulatedPreview }; - -/** - * Builds an NFT transfer transaction (human-readable parameters). - */ -export async function createTransferNftTransaction( - args: CreateTransferNftTransactionArgs, -): Promise { - return callBridge('createTransferNftTransaction', async () => { - return await callOnWalletBridge(args.walletId, 'createTransferNftTransaction', { - nftAddress: args.nftAddress, - toAddress: args.toAddress, - transferAmount: args.transferAmount, - comment: args.comment, - }); - }); -} - -/** - * Builds an NFT transfer transaction (raw message parameters). - */ -export async function createTransferNftRawTransaction( - args: CreateTransferNftRawTransactionArgs, -): Promise { - return callBridge('createTransferNftRawTransaction', async () => { - return await callOnWalletBridge(args.walletId, 'createTransferNftRawTransaction', { - nftAddress: args.nftAddress, - transferAmount: args.transferAmount, - transferMessage: args.transferMessage, - }); - }); -} +export const getNfts = (args: { walletId: string }) => walletCall('getNfts', args); +export const getNft = (args: { walletId: string }) => walletCall('getNft', args); +export const createTransferNftTransaction = (args: { walletId: string }) => walletCall('createTransferNftTransaction', args); +export const createTransferNftRawTransaction = (args: { walletId: string }) => walletCall('createTransferNftRawTransaction', args); diff --git a/packages/walletkit-android-bridge/src/api/requests.ts b/packages/walletkit-android-bridge/src/api/requests.ts index 9efaad297..c5512ae2c 100644 --- a/packages/walletkit-android-bridge/src/api/requests.ts +++ b/packages/walletkit-android-bridge/src/api/requests.ts @@ -6,170 +6,28 @@ * */ -/** - * requests.ts – Request approval handlers - * - * Bridge for connect, transaction, and sign-data request approvals/rejections. - * - * The Android SDK sends the full event object along with the response when approving requests. - * The event is stored in args.event and passed directly to TonWalletKit. - */ - -import type { ConnectionRequestEvent, SendTransactionRequestEvent, SignDataRequestEvent } from '@ton/walletkit'; - -import type { - ApproveConnectRequestArgs, - RejectConnectRequestArgs, - ApproveTransactionRequestArgs, - RejectTransactionRequestArgs, - ApproveSignDataRequestArgs, - RejectSignDataRequestArgs, -} from '../types'; -import { callBridge } from '../utils/bridgeWrapper'; -import { log } from '../utils/logger'; - -/** - * Approves a connect request. - * The event object is passed from Kotlin and contains all required fields. - */ -export async function approveConnectRequest(args: ApproveConnectRequestArgs): Promise { - return callBridge('approveConnectRequest', async (kit) => { - log('approveConnectRequest walletId:', args.walletId); - - const event = args.event; - if (!event) { - throw new Error('Event is required for connect request approval'); - } +import { kit } from '../utils/bridge'; - // Set walletId on the event (wallet lookup not needed - wallet is managed by Kotlin) - (event as { walletId?: string }).walletId = args.walletId; - - // Cast to the expected type - Kotlin sends the full event with all required fields - await kit.approveConnectRequest( - event as unknown as ConnectionRequestEvent, - args.response as Parameters[1], - ); - }); +export async function approveConnectRequest(args: unknown[]) { + return kit('approveConnectRequest', ...args); } -/** - * Rejects a connect request. - */ -export async function rejectConnectRequest(args: RejectConnectRequestArgs): Promise<{ success: boolean }> { - return callBridge('rejectConnectRequest', async (kit) => { - const event = args.event; - if (!event) { - throw new Error('Event is required for connect request rejection'); - } - - const result = await kit.rejectConnectRequest( - event as unknown as ConnectionRequestEvent, - args.reason, - args.errorCode, - ); - - return result ?? { success: true }; - }); +export async function rejectConnectRequest(args: unknown[]) { + return kit('rejectConnectRequest', ...args); } -/** - * Approves a transaction request. - */ -export async function approveTransactionRequest(args: ApproveTransactionRequestArgs): Promise<{ signedBoc: string }> { - return callBridge('approveTransactionRequest', async (kit) => { - const event = args.event; - if (!event) { - throw new Error('Event is required for transaction request approval'); - } - - // Set walletId on the event - if (args.walletId) { - (event as { walletId?: string }).walletId = args.walletId; - } - - // Cast to the expected type - Kotlin sends the full event with all required fields - const result = await kit.approveTransactionRequest( - event as unknown as SendTransactionRequestEvent, - args.response as Parameters[1], - ); - - return result as { signedBoc: string }; - }); +export async function approveTransactionRequest(args: unknown[]) { + return kit('approveTransactionRequest', ...args); } -/** - * Rejects a transaction request. - */ -export async function rejectTransactionRequest(args: RejectTransactionRequestArgs): Promise<{ success: boolean }> { - return callBridge('rejectTransactionRequest', async (kit) => { - const event = args.event; - if (!event) { - throw new Error('Event is required for transaction request rejection'); - } - - // If errorCode is provided, pass it as an error object; otherwise just pass the reason string - const reason = - args.errorCode !== undefined - ? { code: args.errorCode, message: args.reason || 'Transaction rejected' } - : args.reason; - - const result = await kit.rejectTransactionRequest(event as unknown as SendTransactionRequestEvent, reason); - - return result ?? { success: true }; - }); +export async function rejectTransactionRequest(args: unknown[]) { + return kit('rejectTransactionRequest', ...args); } -/** - * Approves a sign-data request. - */ -export async function approveSignDataRequest( - args: ApproveSignDataRequestArgs, -): Promise<{ signature: string; timestamp: number }> { - return callBridge('approveSignDataRequest', async (kit) => { - log('approveSignDataRequest args:', args); - - const event = args.event; - if (!event) { - throw new Error('Event is required for sign-data request approval'); - } - - log('approveSignDataRequest event:', event); - - // Set walletId on the event - if (args.walletId) { - (event as { walletId?: string }).walletId = args.walletId; - } - - // Cast to the expected type - Kotlin sends the full event with all required fields - log('approveSignDataRequest calling kit.approveSignDataRequest with event:', event, 'response:', args.response); - const result = await kit.approveSignDataRequest( - event as unknown as SignDataRequestEvent, - args.response as Parameters[1], - ); - log('approveSignDataRequest result:', result); - - return result as { signature: string; timestamp: number }; - }); +export async function approveSignDataRequest(args: unknown[]) { + return kit('approveSignDataRequest', ...args); } -/** - * Rejects a sign-data request. - */ -export async function rejectSignDataRequest(args: RejectSignDataRequestArgs): Promise<{ success: boolean }> { - return callBridge('rejectSignDataRequest', async (kit) => { - const event = args.event; - if (!event) { - throw new Error('Event is required for sign-data request rejection'); - } - - // If errorCode is provided, pass it as an error object; otherwise just pass the reason string - const reason = - args.errorCode !== undefined - ? { code: args.errorCode, message: args.reason || 'Sign data rejected' } - : args.reason; - - const result = await kit.rejectSignDataRequest(event as unknown as SignDataRequestEvent, reason); - - return result ?? { success: true }; - }); +export async function rejectSignDataRequest(args: unknown[]) { + return kit('rejectSignDataRequest', ...args); } diff --git a/packages/walletkit-android-bridge/src/api/tonconnect.ts b/packages/walletkit-android-bridge/src/api/tonconnect.ts index b23b9efe2..d74949a47 100644 --- a/packages/walletkit-android-bridge/src/api/tonconnect.ts +++ b/packages/walletkit-android-bridge/src/api/tonconnect.ts @@ -6,128 +6,61 @@ * */ -/** - * tonconnect.ts – TonConnect operations - * - * Simplified bridge for TonConnect URL handling, session management, and internal browser requests. - * Session transformation handled by Kotlin SessionResponseParser. - */ - -import type { BridgeEventMessageInfo, InjectedToExtensionBridgeRequestPayload } from '@ton/walletkit'; - -import type { JsBridgeTransportMessage } from '../types/bridge'; -import type { - HandleTonConnectUrlArgs, - DisconnectSessionArgs, - ProcessInternalBrowserRequestArgs, - TonConnectEventPayload, -} from '../types'; -import { callBridge } from '../utils/bridgeWrapper'; +import { kit } from '../utils/bridge'; import { ensureInternalBrowserResolverMap } from '../utils/internalBrowserResolvers'; -/** - * Handles TonConnect URLs from deep links or QR codes. - */ -export async function handleTonConnectUrl(args: HandleTonConnectUrlArgs) { - return callBridge('handleTonConnectUrl', async (kit) => { - return await kit.handleTonConnectUrl(args.url); - }); +export async function handleTonConnectUrl(args: string) { + return kit('handleTonConnectUrl', args); } -/** - * Retrieves active TonConnect sessions. - * Session transformation handled by Kotlin SessionResponseParser. - */ export async function listSessions() { - return callBridge('listSessions', async (kit) => { - const fetchedSessions = kit.listSessions ? await kit.listSessions() : []; - const sessions = Array.isArray(fetchedSessions) ? fetchedSessions : []; - return { items: sessions }; - }); + return kit('listSessions'); } -/** - * Disconnects a TonConnect session. - */ -export async function disconnectSession(args?: DisconnectSessionArgs) { - return callBridge('disconnectSession', async (kit) => { - if (kit.disconnect) { - await kit.disconnect(args?.sessionId); - } - return { ok: true }; - }); +export async function disconnectSession(args?: string) { + return kit('disconnect', args); } -/** Default timeout for internal browser requests in milliseconds */ -const INTERNAL_BROWSER_REQUEST_TIMEOUT_MS = 60000; - /** - * Safely extracts origin from URL, falling back to default if parsing fails. + * Processes internal browser TonConnect requests. + * args: [messageInfo, request] where messageInfo has { messageId, tabId, domain } + * + * This function calls processInjectedBridgeRequest and then waits for the response + * to come back via jsBridgeTransport (which resolves the promise via the resolver map). */ -function safeGetOrigin(url: string | undefined, fallback: string): string { - if (!url) return fallback; - try { - return new URL(url).origin; - } catch { - return fallback; +export async function processInternalBrowserRequest(args: unknown[]) { + // Extract messageId from messageInfo (first element of args array) + const messageInfo = args[0] as { messageId?: string } | undefined; + const messageId = messageInfo?.messageId; + + if (!messageId) { + throw new Error('processInternalBrowserRequest: messageId is required in messageInfo'); } -} - -/** - * Processes requests from the in-app browser TonConnect bridge. - * Domain resolution and request preparation handled by Kotlin InternalBrowserRequestProcessor. - */ -export async function processInternalBrowserRequest(args: ProcessInternalBrowserRequestArgs) { - return callBridge('processInternalBrowserRequest', async (kit) => { - // Extract origin (with scheme) from URL - SessionManager.getSessionByDomain expects a parseable URL - const domain = safeGetOrigin(args.url, 'internal-browser'); - - const messageInfo: BridgeEventMessageInfo = { - messageId: args.messageId, - tabId: args.messageId, - domain, - }; - - const request: InjectedToExtensionBridgeRequestPayload = { - id: args.messageId, - method: args.method, - params: args.params ?? [], - }; - - if (kit.processInjectedBridgeRequest) { - await kit.processInjectedBridgeRequest(messageInfo, request); - } else { - throw new Error('processInjectedBridgeRequest not available'); - } - - // Wait for response from jsBridgeTransport (via initialization.ts) - return new Promise((resolve, reject) => { - const timeoutId = setTimeout(() => { - reject(new Error(`Request timeout: ${args.messageId}`)); - }, INTERNAL_BROWSER_REQUEST_TIMEOUT_MS); + + // Call processInjectedBridgeRequest - this queues the event but doesn't return the response + await kit('processInjectedBridgeRequest', ...args); + + // Wait for response from jsBridgeTransport (via initialization.ts) + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + reject(new Error(`Request timeout: ${messageId}`)); + }, 60000); // 60 second timeout - const resolverMap = ensureInternalBrowserResolverMap(); - resolverMap.set(args.messageId, { - resolve: (response: JsBridgeTransportMessage) => { - clearTimeout(timeoutId); - // Extract payload from BridgeResponse - that's the actual TonConnect event - if ('payload' in response && response.payload !== undefined) { - resolve(response.payload as TonConnectEventPayload); - } else if ('result' in response && response.result !== undefined) { - resolve(response.result as TonConnectEventPayload); - } else if ('event' in response) { - // BridgeEvent contains the event directly - resolve(response.event as TonConnectEventPayload); - } else { - // Fallback - shouldn't happen in normal flow - reject(new Error('Unexpected response format')); - } - }, - reject: (error: Error) => { - clearTimeout(timeoutId); - reject(error); - }, - }); + const resolverMap = ensureInternalBrowserResolverMap(); + resolverMap.set(messageId, { + resolve: (response: unknown) => { + clearTimeout(timeoutId); + // Extract payload if present + if (response && typeof response === 'object' && 'payload' in response) { + resolve((response as { payload?: unknown }).payload ?? response); + } else { + resolve(response); + } + }, + reject: (error: unknown) => { + clearTimeout(timeoutId); + reject(error instanceof Error ? error : new Error(String(error))); + }, }); }); } diff --git a/packages/walletkit-android-bridge/src/api/transactions.ts b/packages/walletkit-android-bridge/src/api/transactions.ts index 84f3da0d6..cce167342 100644 --- a/packages/walletkit-android-bridge/src/api/transactions.ts +++ b/packages/walletkit-android-bridge/src/api/transactions.ts @@ -9,165 +9,21 @@ /** * transactions.ts – TON transaction operations * - * Simplified bridge that passes requests directly to WalletKit. - * All validation, transformation, and formatting happens in Kotlin. + * Minimal bridge - just forwards calls to WalletKit. */ -import type { Transaction } from '@ton/walletkit'; +import type { TransactionRequest } from '@ton/walletkit'; -import type { - GetRecentTransactionsArgs, - CreateTransferTonTransactionArgs, - CreateTransferMultiTonTransactionArgs, - TransactionContentArgs, -} from '../types'; -import { callBridge } from '../utils/bridgeWrapper'; -import { warn } from '../utils/logger'; +import { wallet, clientCall, walletCall, getKit, getWallet } from '../utils/bridge'; -/** - * Retrieves recent transactions for a wallet. - * Returns raw WalletKit response - transformation happens in Kotlin TransactionResponseParser. - */ -export async function getRecentTransactions(args: GetRecentTransactionsArgs): Promise { - return callBridge('getRecentTransactions', async (kit) => { - const wallet = kit.getWallet?.(args.walletId); - - // Extract address from walletId (format: "{chainId}:{address}") - const address = wallet?.getAddress?.() ?? args.walletId.split(':')[1]; - - const response = await wallet!.getClient().getAccountTransactions({ - address: [address], - limit: args.limit || 10, - }); - - return response?.transactions || []; - }); -} - -/** - * Creates a single-recipient TON transfer transaction. - * Returns raw transaction and optional preview - Kotlin handles structure. - */ -export async function createTransferTonTransaction(args: CreateTransferTonTransactionArgs) { - return callBridge('createTransferTonTransaction', async (kit) => { - const wallet = kit.getWallet?.(args.walletId); - if (!wallet) { - throw new Error(`Wallet not found: ${args.walletId}`); - } - - // Map from bridge args to walletkit's TONTransferRequest - const transaction = await wallet.createTransferTonTransaction({ - transferAmount: args.amount, - recipientAddress: args.toAddress, - comment: args.comment, - body: args.body, - stateInit: args.stateInit, - } as Parameters[0]); - - if (wallet.getTransactionPreview) { - try { - const preview = await wallet.getTransactionPreview(transaction); - return { transaction, preview }; - } catch (err) { - warn('[walletkitBridge] getTransactionPreview failed', err); - } - } - - return { transaction }; - }); -} - -/** - * Creates a multi-recipient TON transfer transaction. - * Returns raw transaction and optional preview - Kotlin handles structure. - */ -export async function createTransferMultiTonTransaction(args: CreateTransferMultiTonTransactionArgs) { - return callBridge('createTransferMultiTonTransaction', async (kit) => { - const wallet = kit.getWallet?.(args.walletId); - if (!wallet) { - throw new Error(`Wallet not found: ${args.walletId}`); - } - - // Map from bridge args to walletkit's TONTransferRequest[] - const requests = args.messages.map((msg) => ({ - transferAmount: msg.amount, - recipientAddress: msg.toAddress, - comment: msg.comment, - body: msg.body, - stateInit: msg.stateInit, - })); - - // Cast to expected type - implementation accepts TONTransferRequest[] - const transaction = await wallet.createTransferMultiTonTransaction( - requests as unknown as Parameters[0], - ); - - if (wallet.getTransactionPreview) { - try { - const preview = await wallet.getTransactionPreview(transaction); - return { transaction, preview }; - } catch (err) { - warn('[walletkitBridge] getTransactionPreview failed', err); - } - } - - return { transaction }; - }); -} - -/** - * Gets transaction preview (fee estimation). - */ -export async function getTransactionPreview(args: TransactionContentArgs) { - return callBridge('getTransactionPreview', async (kit) => { - const wallet = kit.getWallet?.(args.walletId); - if (!wallet) { - throw new Error(`Wallet not found: ${args.walletId}`); - } - - // Accept object directly (preferred) or parse string (legacy) - const transaction = - typeof args.transactionContent === 'string' ? JSON.parse(args.transactionContent) : args.transactionContent; - - if (!wallet.getTransactionPreview) { - throw new Error('getTransactionPreview not available on wallet'); - } - return await wallet.getTransactionPreview(transaction); - }); -} - -/** - * Handles new transaction (triggers confirmation flow). - */ -export async function handleNewTransaction(args: TransactionContentArgs) { - return callBridge('handleNewTransaction', async (kit) => { - const wallet = kit.getWallet?.(args.walletId); - if (!wallet) { - throw new Error(`Wallet not found: ${args.walletId}`); - } - - const transaction = - typeof args.transactionContent === 'string' ? JSON.parse(args.transactionContent) : args.transactionContent; - - await kit.handleNewTransaction(wallet, transaction); - - return { success: true }; - }); -} - -/** - * Sends a transaction to the network. - * Returns raw result object with signedBoc. - */ -export async function sendTransaction(args: TransactionContentArgs) { - return callBridge('sendTransaction', async (kit) => { - const wallet = kit.getWallet?.(args.walletId); - if (!wallet) { - throw new Error(`Wallet not found: ${args.walletId}`); - } +export const createTransferTonTransaction = (args: { walletId: string }) => walletCall('createTransferTonTransaction', args); +export const createTransferMultiTonTransaction = (args: { walletId: string }) => walletCall('createTransferMultiTonTransaction', args); +export const getTransactionPreview = (args: { walletId: string }) => walletCall('getTransactionPreview', args); +export const sendTransaction = (args: { walletId: string }) => walletCall('sendTransaction', args); +export const getRecentTransactions = (args: { walletId: string }) => clientCall('getAccountTransactions', args); - const transaction = - typeof args.transactionContent === 'string' ? JSON.parse(args.transactionContent) : args.transactionContent; - return await wallet.sendTransaction(transaction); - }); +export async function handleNewTransaction(args: [string, unknown]) { + const k = await getKit(); + const w = await getWallet(args[0]); + return k.handleNewTransaction(w, args[1] as TransactionRequest); } diff --git a/packages/walletkit-android-bridge/src/api/wallets.ts b/packages/walletkit-android-bridge/src/api/wallets.ts index fb93a73af..777b815be 100644 --- a/packages/walletkit-android-bridge/src/api/wallets.ts +++ b/packages/walletkit-android-bridge/src/api/wallets.ts @@ -13,93 +13,67 @@ * Kotlin is responsible for adapting to whatever JS returns. */ -import type { Hex } from '@ton/walletkit'; - -import type { - RemoveWalletArgs, - GetBalanceArgs, - CreateSignerArgs, - CreateAdapterArgs, - AddWalletArgs, - WalletKitSigner, - WalletKitAdapter, -} from '../types'; +import type { Hex, Network, WalletAdapter } from '@ton/walletkit'; + import { Signer, WalletV4R2Adapter, WalletV5R1Adapter } from '../core/moduleLoader'; -import { callBridge } from '../utils/bridgeWrapper'; +import { kit, wallet, getKit } from '../utils/bridge'; import { signWithCustomSigner } from './cryptography'; -type SignerInstance = WalletKitSigner; -type AdapterInstance = WalletKitAdapter; +type SignerInstance = { sign: (bytes: Iterable) => Promise; publicKey: Hex }; /** * Lists all wallets. - * Returns walletId with each wallet since network can't be inferred from wallet properties. */ export async function getWallets() { - return callBridge('getWallets', async (kit) => { - const wallets = kit.getWallets?.() ?? []; - // Include walletId since getNetwork()/getAddress() are methods that don't serialize - return wallets.map((w) => ({ - walletId: w.getWalletId?.(), - wallet: w, - })); - }); + const wallets = await kit('getWallets') as { getWalletId?: () => string }[]; + return wallets.map((w) => ({ walletId: w.getWalletId?.(), wallet: w })); } /** * Get a single wallet by walletId. - * Returns walletId with wallet since network can't be inferred from wallet properties. */ -export async function getWallet(args: { walletId: string }) { - return callBridge('getWallet', async (kit) => { - const w = kit.getWallet?.(args.walletId); - if (!w) return null; - return { walletId: w.getWalletId?.(), wallet: w }; - }); +export async function getWalletById(args: { walletId: string }) { + const w = await kit('getWallet', args.walletId); + if (!w) return null; + return { walletId: (w as { getWalletId?: () => string }).getWalletId?.(), wallet: w }; } -/** - * Gets the address of a wallet. - * Returns raw result - Kotlin adapts to the response. - */ export async function getWalletAddress(args: { walletId: string }) { - return callBridge('getWalletAddress', async (kit) => { - const wallet = kit.getWallet?.(args.walletId); - return wallet?.getAddress?.() ?? null; - }); + return wallet(args.walletId, 'getAddress'); } -/** - * Removes a wallet from storage. - * Returns raw result - Kotlin adapts to the response. - */ -export async function removeWallet(args: RemoveWalletArgs) { - return callBridge('removeWallet', async (kit) => { - return await kit.removeWallet?.(args.walletId); - }); +export async function removeWallet(args: { walletId: string }) { + return kit('removeWallet', args.walletId); } -/** - * Fetches wallet balance. - * Returns raw balance - Kotlin adapts to the response. - */ -export async function getBalance(args: GetBalanceArgs) { - return callBridge('getBalance', async (kit) => { - const wallet = kit.getWallet?.(args.walletId); - return await wallet?.getBalance?.(); - }); +export async function getBalance(args: { walletId: string }) { + return wallet(args.walletId, 'getBalance'); } -// Store for signers and adapters const signerStore = new Map(); -const adapterStore = new Map(); +const adapterStore = new Map(); + +type CreateAdapterArgs = { + signerId: string; + isCustom?: boolean; + publicKey?: string; + walletVersion?: string; + network: string; + workchain: number; + walletId?: string; +}; + +type CreateSignerArgs = { + mnemonic?: string[]; + secretKey?: string; + mnemonicType?: string; +}; + +type AddWalletArgs = { + adapterId: string; +}; -/** - * Retrieves or creates a signer instance based on the arguments. - * Handles both custom signers (hardware wallets) and regular signers. - */ async function getSigner(args: CreateAdapterArgs): Promise { - // Handle custom signers (hardware wallets) that live in Kotlin if (args.isCustom && args.publicKey) { return { sign: async (bytes: Iterable): Promise => { @@ -109,7 +83,6 @@ async function getSigner(args: CreateAdapterArgs): Promise { }; } - // Handle regular signers stored in JavaScript const storedSigner = signerStore.get(args.signerId); if (!storedSigner) { throw new Error(`Signer not found: ${args.signerId}`); @@ -117,81 +90,63 @@ async function getSigner(args: CreateAdapterArgs): Promise { return storedSigner; } -/** - * Creates a signer from mnemonic or secret key. - * Returns raw signer object - Kotlin generates signerId and extracts publicKey. - */ export async function createSigner(args: CreateSignerArgs) { - return callBridge('createSigner', async (_kit) => { - const signer = - args.mnemonic && args.mnemonic.length > 0 - ? ((await Signer!.fromMnemonic(args.mnemonic, { type: args.mnemonicType || 'ton' })) as SignerInstance) - : ((await Signer!.fromPrivateKey(args.secretKey!)) as SignerInstance); + if (!Signer) { + throw new Error('Signer module not loaded'); + } + if (!args.mnemonic?.length && !args.secretKey) { + throw new Error('Either mnemonic or secretKey is required'); + } + const signer = + args.mnemonic && args.mnemonic.length > 0 + ? ((await Signer.fromMnemonic(args.mnemonic, { type: args.mnemonicType || 'ton' })) as SignerInstance) + : ((await Signer.fromPrivateKey(args.secretKey as string)) as SignerInstance); - // Store signer with temp ID for Kotlin to retrieve - const tempId = `signer_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; - signerStore.set(tempId, signer); + const tempId = `signer_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; + signerStore.set(tempId, signer); - return { _tempId: tempId, signer }; - }); + return { _tempId: tempId, signer }; } -/** - * Creates a wallet adapter from a signer. - * Supports both regular signers (from mnemonic/secretKey) and custom signers (hardware wallets). - * Returns adapter ID - Kotlin is responsible for all mapping and transformation. - */ export async function createAdapter(args: CreateAdapterArgs) { - return callBridge('createAdapter', async (kit) => { - const signer = await getSigner(args); - const AdapterClass = args.walletVersion === 'v5r1' ? WalletV5R1Adapter : WalletV4R2Adapter; - const adapter = (await AdapterClass!.create(signer, { - client: kit.getApiClient(args.network), - network: args.network, - workchain: args.workchain, - walletId: args.walletId, - })) as AdapterInstance; - - // Store adapter with temp ID for Kotlin to retrieve - const tempId = `adapter_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; - adapterStore.set(tempId, adapter); - - // Return only the temp ID and the raw adapter object - // Kotlin is responsible for extracting any needed properties - return { _tempId: tempId, adapter }; + const instance = await getKit(); + const signer = await getSigner(args); + const AdapterClass = args.walletVersion === 'v5r1' ? WalletV5R1Adapter : WalletV4R2Adapter; + if (!AdapterClass) { + throw new Error(`WalletAdapter module not loaded`); + } + const network = args.network as unknown as Network; + const adapter = await AdapterClass.create(signer, { + client: instance.getApiClient(network), + network, + workchain: args.workchain, + walletId: args.walletId, }); + + const tempId = `adapter_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; + adapterStore.set(tempId, adapter); + + return { _tempId: tempId, adapter }; } -/** - * Gets the address from a stored adapter. - * Returns raw address - Kotlin adapts to the response. - */ + export async function getAdapterAddress(args: { adapterId: string }) { - return callBridge('getAdapterAddress', async (_kit) => { - const adapter = adapterStore.get(args.adapterId); - if (!adapter) { - throw new Error(`Adapter not found: ${args.adapterId}`); - } - return adapter.getAddress(); - }); + const adapter = adapterStore.get(args.adapterId) as WalletAdapter | undefined; + if (!adapter) { + throw new Error(`Adapter not found: ${args.adapterId}`); + } + return adapter.getAddress(); } -/** - * Adds a wallet to WalletKit using an adapter. - * Returns walletId with wallet since getWalletId() is a method that doesn't serialize. - */ export async function addWallet(args: AddWalletArgs) { - return callBridge('addWallet', async (kit) => { - const adapter = adapterStore.get(args.adapterId); - if (!adapter) { - throw new Error(`Adapter not found: ${args.adapterId}`); - } - - const wallet = await kit.addWallet(adapter); + const instance = await getKit(); + const adapter = adapterStore.get(args.adapterId); + if (!adapter) { + throw new Error(`Adapter not found: ${args.adapterId}`); + } - // Clean up the adapter from store after use - adapterStore.delete(args.adapterId); + const w = await instance.addWallet(adapter as Parameters[0]); + adapterStore.delete(args.adapterId); - if (!wallet) return null; - return { walletId: wallet.getWalletId?.(), wallet }; - }); + if (!w) return null; + return { walletId: w.getWalletId?.(), wallet: w }; } diff --git a/packages/walletkit-android-bridge/src/core/initialization.ts b/packages/walletkit-android-bridge/src/core/initialization.ts index 2f806e883..013f355ed 100644 --- a/packages/walletkit-android-bridge/src/core/initialization.ts +++ b/packages/walletkit-android-bridge/src/core/initialization.ts @@ -208,16 +208,3 @@ export async function initTonWalletKit( log('[walletkitBridge] WalletKit ready'); return { ok: true }; } - -/** - * Ensures WalletKit has been initialized before performing an operation. - * Returns the initialized WalletKit instance for type-safe usage. - * - * @throws If WalletKit is not yet ready. - */ -export function requireWalletKit(): NonNullable { - if (!walletKit) { - throw new Error('WalletKit not initialized'); - } - return walletKit; -} diff --git a/packages/walletkit-android-bridge/src/core/moduleLoader.ts b/packages/walletkit-android-bridge/src/core/moduleLoader.ts index 89eddde8a..fea8b8e42 100644 --- a/packages/walletkit-android-bridge/src/core/moduleLoader.ts +++ b/packages/walletkit-android-bridge/src/core/moduleLoader.ts @@ -26,7 +26,7 @@ type AdapterFactory = { type WalletKitModule = { TonWalletKit: TonWalletKitConstructor; - CreateTonMnemonic?: () => Promise; + CreateTonMnemonic?: () => Promise; MnemonicToKeyPair?: ( mnemonic: string[], type: string, @@ -41,7 +41,7 @@ type WalletKitModule = { }; export let TonWalletKit: TonWalletKitConstructor | null = null; -export let CreateTonMnemonic: (() => Promise) | null = null; +export let CreateTonMnemonic: (() => Promise) | null = null; export let MnemonicToKeyPair: | ((mnemonic: string[], type: string) => Promise<{ publicKey: Uint8Array; secretKey: Uint8Array }>) | null = null; diff --git a/packages/walletkit-android-bridge/src/transport/index.ts b/packages/walletkit-android-bridge/src/transport/index.ts deleted file mode 100644 index e51e3d334..000000000 --- a/packages/walletkit-android-bridge/src/transport/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -export * from './nativeBridge'; -export * from './diagnostics'; -export * from './messaging'; diff --git a/packages/walletkit-android-bridge/src/types/api.ts b/packages/walletkit-android-bridge/src/types/api.ts index 776d4b535..2f4e03508 100644 --- a/packages/walletkit-android-bridge/src/types/api.ts +++ b/packages/walletkit-android-bridge/src/types/api.ts @@ -89,16 +89,16 @@ export interface GetRecentTransactionsArgs { export interface CreateTransferTonTransactionArgs { walletId: string; - toAddress: string; - amount: string; + recipientAddress: string; + transferAmount: string; comment?: string; body?: string; stateInit?: string; } export interface MultiTransferMessage { - toAddress: string; - amount: string; + recipientAddress: string; + transferAmount: string; comment?: string; body?: string; stateInit?: string; @@ -127,7 +127,6 @@ export interface TonConnectRequestEvent extends BridgeEvent { export interface ApproveConnectRequestArgs { event: TonConnectRequestEvent; - walletId: string; response?: { proof: { signature: string; @@ -149,7 +148,6 @@ export interface RejectConnectRequestArgs { export interface ApproveTransactionRequestArgs { event: TonConnectRequestEvent; - walletId?: string; response?: { signedBoc: string; }; @@ -157,13 +155,11 @@ export interface ApproveTransactionRequestArgs { export interface RejectTransactionRequestArgs { event: TonConnectRequestEvent; - reason?: string; - errorCode?: number; + reason?: string | { code: number; message: string }; } export interface ApproveSignDataRequestArgs { event: TonConnectRequestEvent; - walletId?: string; response?: { signature: string; timestamp: number; @@ -174,7 +170,6 @@ export interface ApproveSignDataRequestArgs { export interface RejectSignDataRequestArgs { event: TonConnectRequestEvent; reason?: string; - errorCode?: number; } export interface DisconnectSessionArgs { @@ -196,8 +191,8 @@ export interface GetNftArgs { export interface CreateTransferNftTransactionArgs { walletId: string; nftAddress: string; - transferAmount: string; - toAddress: string; + transferAmount?: string; + recipientAddress: string; comment?: string; } @@ -205,7 +200,7 @@ export interface CreateTransferNftRawTransactionArgs { walletId: string; nftAddress: string; transferAmount: string; - transferMessage: TransactionRequest; + message: TransactionRequest; } export interface GetJettonsArgs { @@ -216,8 +211,8 @@ export interface GetJettonsArgs { export interface CreateTransferJettonTransactionArgs { walletId: string; jettonAddress: string; - amount: string; - toAddress: string; + transferAmount: string; + recipientAddress: string; comment?: string; } diff --git a/packages/walletkit-android-bridge/src/utils/bridge.ts b/packages/walletkit-android-bridge/src/utils/bridge.ts new file mode 100644 index 000000000..8029600b5 --- /dev/null +++ b/packages/walletkit-android-bridge/src/utils/bridge.ts @@ -0,0 +1,130 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +/** + * bridge.ts - Minimal unified bridge for all operations + * + * Single entry point for calling WalletKit, Wallet, and Client methods. + * Handles initialization, wallet lookup, and error handling in one place. + */ + +import type { Wallet } from '@ton/walletkit'; + +import type { WalletKitInstance } from '../core/state'; +import { walletKit } from '../core/state'; + +// Re-export for external use +export type { WalletKitInstance }; + +/** + * Ensures WalletKit is initialized and ready. + */ +async function ensureReady(): Promise { + if (!walletKit) { + throw new Error('WalletKit not initialized'); + } + await walletKit.ensureInitialized?.(); + return walletKit; +} + +/** + * Gets a wallet by ID, throwing if not found. + */ +function getWalletOrThrow(kit: WalletKitInstance, walletId: string): Wallet { + const wallet = kit.getWallet(walletId); + if (!wallet) { + throw new Error(`Wallet not found: ${walletId}`); + } + return wallet; +} + +/** + * Calls a method on WalletKit instance. + * + * @example + * await kit('removeWallet', walletId); + * await kit('handleTonConnectUrl', url); + * await kit('listSessions'); + */ +export async function kit( + method: M, + ...args: unknown[] +): Promise { + const instance = await ensureReady(); + const fn = instance[method]; + if (typeof fn !== 'function') { + throw new Error(`Method '${method}' not found on WalletKit`); + } + return (fn as (...a: unknown[]) => unknown).apply(instance, args); +} + +/** + * Calls a method on a Wallet instance. + * + * @example + * await wallet(walletId, 'getBalance'); + * await wallet(walletId, 'getJettons', { pagination }); + * await wallet(walletId, 'createTransferTonTransaction', request); + */ +export async function wallet( + walletId: string, + method: M, + ...args: unknown[] +): Promise { + const instance = await ensureReady(); + const w = getWalletOrThrow(instance, walletId); + const fn = w[method]; + if (typeof fn !== 'function') { + throw new Error(`Method '${method}' not found on Wallet`); + } + return (fn as (...a: unknown[]) => unknown).apply(w, args); +} + +/** + * Get the raw WalletKit instance (for complex operations). + * Use sparingly - prefer kit(), wallet() for type safety. + */ +export async function getKit(): Promise { + return ensureReady(); +} + +/** + * Get a wallet instance (for complex operations). + * Use sparingly - prefer wallet() for type safety. + */ +export async function getWallet(walletId: string): Promise { + const instance = await ensureReady(); + return getWalletOrThrow(instance, walletId); +} + +/** + * Calls a method on a Wallet instance. Extracts walletId from args.walletId. + */ +export async function walletCall(method: string, args: { walletId: string; [k: string]: unknown }): Promise { + const instance = await ensureReady(); + const w = getWalletOrThrow(instance, args.walletId); + const fn = (w as unknown as Record)[method]; + if (typeof fn !== 'function') { + throw new Error(`Method '${method}' not found on Wallet`); + } + return (fn as (args?: unknown) => Promise).call(w, args); +} + +/** + * Calls a method on a Wallet's ApiClient. Extracts walletId from args.walletId. + */ +export async function clientCall(method: string, args: { walletId: string; [k: string]: unknown }): Promise { + const instance = await ensureReady(); + const w = getWalletOrThrow(instance, args.walletId); + const apiClient = w.getClient(); + const fn = (apiClient as unknown as Record)[method]; + if (typeof fn !== 'function') { + throw new Error(`Method '${method}' not found on ApiClient`); + } + return (fn as (args?: unknown) => Promise).call(apiClient, args); +} diff --git a/packages/walletkit-android-bridge/src/utils/bridgeWrapper.ts b/packages/walletkit-android-bridge/src/utils/bridgeWrapper.ts deleted file mode 100644 index 6b1e8b21f..000000000 --- a/packages/walletkit-android-bridge/src/utils/bridgeWrapper.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -/** - * bridgeWrapper.ts - Unified bridge operation wrappers - * - * Provides consistent initialization and error handling for all bridge operations. - */ - -import type { WalletKitInstance } from '../core/state'; -import { walletKit } from '../core/state'; - -/** - * Unified wrapper for all bridge operations. - * Handles initialization and ensures WalletKit is ready before executing operation. - * Passes the initialized walletKit instance to the callback to avoid null assertions. - * - * @param _method - Operation name for error logging (unused, kept for API consistency) - * @param operation - Function receiving the initialized walletKit instance - * @returns Result of the operation - */ -export async function callBridge(_method: string, operation: (kit: WalletKitInstance) => Promise): Promise { - if (!walletKit) { - throw new Error('WalletKit not initialized'); - } - if (walletKit.ensureInitialized) { - await walletKit.ensureInitialized(); - } - return await operation(walletKit); -} - -/** - * Wrapper for wallet-specific operations. - * Gets a wallet by walletId and calls a method on it. - * - * @param walletId - Wallet ID (format: "{chainId}:{address}") - * @param method - Wallet method name to call - * @param args - Optional arguments to pass to the method - * @returns Result of the wallet method - */ -export async function callOnWalletBridge(walletId: string, method: string, args?: unknown): Promise { - return callBridge(`wallet.${method}`, async (kit) => { - const wallet = kit.getWallet?.(walletId); - if (!wallet) { - throw new Error(`Wallet not found: ${walletId}`); - } - const methodRef = (wallet as unknown as Record)[method]; - if (typeof methodRef !== 'function') { - throw new Error(`Method '${method}' not found on wallet`); - } - return (await (methodRef as (args?: unknown) => Promise).call(wallet, args)) as T; - }); -} diff --git a/packages/walletkit-android-bridge/src/utils/index.ts b/packages/walletkit-android-bridge/src/utils/index.ts deleted file mode 100644 index d81a5d09c..000000000 --- a/packages/walletkit-android-bridge/src/utils/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -export * from './serialization'; From 7ed328873cc5ba9cb1dc0601e26bba79bce687ee Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Wed, 4 Feb 2026 14:14:26 +0500 Subject: [PATCH 06/19] fix: fix lint --- .../walletkit-android-bridge/src/api/jettons.ts | 3 ++- packages/walletkit-android-bridge/src/api/nft.ts | 6 ++++-- .../src/api/tonconnect.ts | 8 ++++---- .../src/api/transactions.ts | 6 ++++-- .../walletkit-android-bridge/src/api/wallets.ts | 2 +- .../walletkit-android-bridge/src/utils/bridge.ts | 15 +++++++++------ 6 files changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/walletkit-android-bridge/src/api/jettons.ts b/packages/walletkit-android-bridge/src/api/jettons.ts index 45755b5bb..17bb430f0 100644 --- a/packages/walletkit-android-bridge/src/api/jettons.ts +++ b/packages/walletkit-android-bridge/src/api/jettons.ts @@ -15,6 +15,7 @@ import { walletCall } from '../utils/bridge'; export const getJettons = (args: { walletId: string }) => walletCall('getJettons', args); -export const createTransferJettonTransaction = (args: { walletId: string }) => walletCall('createTransferJettonTransaction', args); +export const createTransferJettonTransaction = (args: { walletId: string }) => + walletCall('createTransferJettonTransaction', args); export const getJettonBalance = (args: { walletId: string }) => walletCall('getJettonBalance', args); export const getJettonWalletAddress = (args: { walletId: string }) => walletCall('getJettonWalletAddress', args); diff --git a/packages/walletkit-android-bridge/src/api/nft.ts b/packages/walletkit-android-bridge/src/api/nft.ts index 6c2c2b330..b504973ce 100644 --- a/packages/walletkit-android-bridge/src/api/nft.ts +++ b/packages/walletkit-android-bridge/src/api/nft.ts @@ -16,5 +16,7 @@ import { walletCall } from '../utils/bridge'; export const getNfts = (args: { walletId: string }) => walletCall('getNfts', args); export const getNft = (args: { walletId: string }) => walletCall('getNft', args); -export const createTransferNftTransaction = (args: { walletId: string }) => walletCall('createTransferNftTransaction', args); -export const createTransferNftRawTransaction = (args: { walletId: string }) => walletCall('createTransferNftRawTransaction', args); +export const createTransferNftTransaction = (args: { walletId: string }) => + walletCall('createTransferNftTransaction', args); +export const createTransferNftRawTransaction = (args: { walletId: string }) => + walletCall('createTransferNftRawTransaction', args); diff --git a/packages/walletkit-android-bridge/src/api/tonconnect.ts b/packages/walletkit-android-bridge/src/api/tonconnect.ts index d74949a47..6462533c8 100644 --- a/packages/walletkit-android-bridge/src/api/tonconnect.ts +++ b/packages/walletkit-android-bridge/src/api/tonconnect.ts @@ -24,7 +24,7 @@ export async function disconnectSession(args?: string) { /** * Processes internal browser TonConnect requests. * args: [messageInfo, request] where messageInfo has { messageId, tabId, domain } - * + * * This function calls processInjectedBridgeRequest and then waits for the response * to come back via jsBridgeTransport (which resolves the promise via the resolver map). */ @@ -32,14 +32,14 @@ export async function processInternalBrowserRequest(args: unknown[]) { // Extract messageId from messageInfo (first element of args array) const messageInfo = args[0] as { messageId?: string } | undefined; const messageId = messageInfo?.messageId; - + if (!messageId) { throw new Error('processInternalBrowserRequest: messageId is required in messageInfo'); } - + // Call processInjectedBridgeRequest - this queues the event but doesn't return the response await kit('processInjectedBridgeRequest', ...args); - + // Wait for response from jsBridgeTransport (via initialization.ts) return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { diff --git a/packages/walletkit-android-bridge/src/api/transactions.ts b/packages/walletkit-android-bridge/src/api/transactions.ts index cce167342..cbaa8317e 100644 --- a/packages/walletkit-android-bridge/src/api/transactions.ts +++ b/packages/walletkit-android-bridge/src/api/transactions.ts @@ -16,8 +16,10 @@ import type { TransactionRequest } from '@ton/walletkit'; import { wallet, clientCall, walletCall, getKit, getWallet } from '../utils/bridge'; -export const createTransferTonTransaction = (args: { walletId: string }) => walletCall('createTransferTonTransaction', args); -export const createTransferMultiTonTransaction = (args: { walletId: string }) => walletCall('createTransferMultiTonTransaction', args); +export const createTransferTonTransaction = (args: { walletId: string }) => + walletCall('createTransferTonTransaction', args); +export const createTransferMultiTonTransaction = (args: { walletId: string }) => + walletCall('createTransferMultiTonTransaction', args); export const getTransactionPreview = (args: { walletId: string }) => walletCall('getTransactionPreview', args); export const sendTransaction = (args: { walletId: string }) => walletCall('sendTransaction', args); export const getRecentTransactions = (args: { walletId: string }) => clientCall('getAccountTransactions', args); diff --git a/packages/walletkit-android-bridge/src/api/wallets.ts b/packages/walletkit-android-bridge/src/api/wallets.ts index 777b815be..e9f32c221 100644 --- a/packages/walletkit-android-bridge/src/api/wallets.ts +++ b/packages/walletkit-android-bridge/src/api/wallets.ts @@ -25,7 +25,7 @@ type SignerInstance = { sign: (bytes: Iterable) => Promise; publicK * Lists all wallets. */ export async function getWallets() { - const wallets = await kit('getWallets') as { getWalletId?: () => string }[]; + const wallets = (await kit('getWallets')) as { getWalletId?: () => string }[]; return wallets.map((w) => ({ walletId: w.getWalletId?.(), wallet: w })); } diff --git a/packages/walletkit-android-bridge/src/utils/bridge.ts b/packages/walletkit-android-bridge/src/utils/bridge.ts index 8029600b5..7cb99cdee 100644 --- a/packages/walletkit-android-bridge/src/utils/bridge.ts +++ b/packages/walletkit-android-bridge/src/utils/bridge.ts @@ -51,10 +51,7 @@ function getWalletOrThrow(kit: WalletKitInstance, walletId: string): Wallet { * await kit('handleTonConnectUrl', url); * await kit('listSessions'); */ -export async function kit( - method: M, - ...args: unknown[] -): Promise { +export async function kit(method: M, ...args: unknown[]): Promise { const instance = await ensureReady(); const fn = instance[method]; if (typeof fn !== 'function') { @@ -105,7 +102,10 @@ export async function getWallet(walletId: string): Promise { /** * Calls a method on a Wallet instance. Extracts walletId from args.walletId. */ -export async function walletCall(method: string, args: { walletId: string; [k: string]: unknown }): Promise { +export async function walletCall( + method: string, + args: { walletId: string; [k: string]: unknown }, +): Promise { const instance = await ensureReady(); const w = getWalletOrThrow(instance, args.walletId); const fn = (w as unknown as Record)[method]; @@ -118,7 +118,10 @@ export async function walletCall(method: string, args: { walletId: /** * Calls a method on a Wallet's ApiClient. Extracts walletId from args.walletId. */ -export async function clientCall(method: string, args: { walletId: string; [k: string]: unknown }): Promise { +export async function clientCall( + method: string, + args: { walletId: string; [k: string]: unknown }, +): Promise { const instance = await ensureReady(); const w = getWalletOrThrow(instance, args.walletId); const apiClient = w.getClient(); From 854b814941e82ba577815384c373eb226567d7c3 Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Wed, 4 Feb 2026 16:33:46 +0500 Subject: [PATCH 07/19] fix: fix lint --- packages/walletkit-android-bridge/src/api/transactions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/walletkit-android-bridge/src/api/transactions.ts b/packages/walletkit-android-bridge/src/api/transactions.ts index cbaa8317e..13286bd08 100644 --- a/packages/walletkit-android-bridge/src/api/transactions.ts +++ b/packages/walletkit-android-bridge/src/api/transactions.ts @@ -14,7 +14,7 @@ import type { TransactionRequest } from '@ton/walletkit'; -import { wallet, clientCall, walletCall, getKit, getWallet } from '../utils/bridge'; +import { walletCall, clientCall, getKit, getWallet } from '../utils/bridge'; export const createTransferTonTransaction = (args: { walletId: string }) => walletCall('createTransferTonTransaction', args); From 7876dc6768bb48f627dadcd787247864abecfae9 Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Wed, 4 Feb 2026 17:10:54 +0500 Subject: [PATCH 08/19] refactor: remove preview from create transaction methods --- .../walletkit-android-bridge/src/types/api.ts | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/packages/walletkit-android-bridge/src/types/api.ts b/packages/walletkit-android-bridge/src/types/api.ts index 2f4e03508..0c4233808 100644 --- a/packages/walletkit-android-bridge/src/types/api.ts +++ b/packages/walletkit-android-bridge/src/types/api.ts @@ -292,13 +292,8 @@ export interface WalletKitBridgeApi { // Returns transactions array directly getRecentTransactions(args: GetRecentTransactionsArgs): PromiseOrValue; handleTonConnectUrl(args: HandleTonConnectUrlArgs): PromiseOrValue; - // Returns transaction and optional preview - createTransferTonTransaction( - args: CreateTransferTonTransactionArgs, - ): PromiseOrValue<{ transaction: TransactionRequest; preview?: TransactionEmulatedPreview }>; - createTransferMultiTonTransaction( - args: CreateTransferMultiTonTransactionArgs, - ): PromiseOrValue<{ transaction: TransactionRequest; preview?: TransactionEmulatedPreview }>; + createTransferTonTransaction(args: CreateTransferTonTransactionArgs): PromiseOrValue; + createTransferMultiTonTransaction(args: CreateTransferMultiTonTransactionArgs): PromiseOrValue; getTransactionPreview(args: TransactionContentArgs): PromiseOrValue; handleNewTransaction(args: TransactionContentArgs): PromiseOrValue<{ success: boolean }>; // Returns result from wallet.sendTransaction @@ -313,16 +308,10 @@ export interface WalletKitBridgeApi { disconnectSession(args?: DisconnectSessionArgs): PromiseOrValue<{ ok: boolean }>; getNfts(args: GetNftsArgs): PromiseOrValue; getNft(args: GetNftArgs): PromiseOrValue; - createTransferNftTransaction( - args: CreateTransferNftTransactionArgs, - ): PromiseOrValue<{ transaction: TransactionRequest; preview?: TransactionEmulatedPreview }>; - createTransferNftRawTransaction( - args: CreateTransferNftRawTransactionArgs, - ): PromiseOrValue<{ transaction: TransactionRequest; preview?: TransactionEmulatedPreview }>; + createTransferNftTransaction(args: CreateTransferNftTransactionArgs): PromiseOrValue; + createTransferNftRawTransaction(args: CreateTransferNftRawTransactionArgs): PromiseOrValue; getJettons(args: GetJettonsArgs): PromiseOrValue; - createTransferJettonTransaction( - args: CreateTransferJettonTransactionArgs, - ): PromiseOrValue<{ transaction: TransactionRequest; preview?: TransactionEmulatedPreview }>; + createTransferJettonTransaction(args: CreateTransferJettonTransactionArgs): PromiseOrValue; getJettonBalance(args: GetJettonBalanceArgs): PromiseOrValue; getJettonWalletAddress(args: GetJettonWalletAddressArgs): PromiseOrValue; processInternalBrowserRequest(args: ProcessInternalBrowserRequestArgs): PromiseOrValue; From 5c8677189bae31d5435771df02fb464e4b77dc94 Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Tue, 3 Feb 2026 10:12:58 +0500 Subject: [PATCH 09/19] feat: add IntentHandler for processing TonConnect intent deep links --- .../src/api/eventListeners.ts | 6 +- .../walletkit-android-bridge/src/api/index.ts | 11 + .../src/api/initialization.ts | 17 + .../src/api/intents.ts | 226 ++++ .../walletkit-android-bridge/src/types/api.ts | 168 +++ .../src/types/events.ts | 1 + .../src/types/walletkit.ts | 28 + packages/walletkit/src/core/IntentHandler.ts | 964 ++++++++++++++++++ packages/walletkit/src/core/TonWalletKit.ts | 150 +++ .../src/core/wallet/extensions/jetton.ts | 3 + packages/walletkit/src/index.ts | 29 + packages/walletkit/src/types/intents.ts | 377 +++++++ packages/walletkit/src/types/kit.ts | 16 + 13 files changed, 1995 insertions(+), 1 deletion(-) create mode 100644 packages/walletkit-android-bridge/src/api/intents.ts create mode 100644 packages/walletkit/src/core/IntentHandler.ts create mode 100644 packages/walletkit/src/types/intents.ts diff --git a/packages/walletkit-android-bridge/src/api/eventListeners.ts b/packages/walletkit-android-bridge/src/api/eventListeners.ts index ebef79dee..236cbe480 100644 --- a/packages/walletkit-android-bridge/src/api/eventListeners.ts +++ b/packages/walletkit-android-bridge/src/api/eventListeners.ts @@ -12,6 +12,7 @@ import type { RequestErrorEvent, SendTransactionRequestEvent, SignDataRequestEvent, + IntentEvent, } from '@ton/walletkit'; /** @@ -22,6 +23,7 @@ export type TransactionEventListener = ((event: SendTransactionRequestEvent) => export type SignDataEventListener = ((event: SignDataRequestEvent) => void) | null; export type DisconnectEventListener = ((event: DisconnectionEvent) => void) | null; export type ErrorEventListener = ((event: RequestErrorEvent) => void) | null; +export type IntentEventListener = ((event: IntentEvent) => void) | null; /** * Union type for all bridge event listeners. @@ -31,7 +33,8 @@ export type BridgeEventListener = | TransactionEventListener | SignDataEventListener | DisconnectEventListener - | ErrorEventListener; + | ErrorEventListener + | IntentEventListener; export const eventListeners = { onConnectListener: null as ConnectEventListener, @@ -39,4 +42,5 @@ export const eventListeners = { onSignDataListener: null as SignDataEventListener, onDisconnectListener: null as DisconnectEventListener, onErrorListener: null as ErrorEventListener, + onIntentListener: null as IntentEventListener, }; diff --git a/packages/walletkit-android-bridge/src/api/index.ts b/packages/walletkit-android-bridge/src/api/index.ts index 28629bf0f..5bdbdd2ac 100644 --- a/packages/walletkit-android-bridge/src/api/index.ts +++ b/packages/walletkit-android-bridge/src/api/index.ts @@ -16,6 +16,7 @@ import * as wallets from './wallets'; import * as transactions from './transactions'; import * as requests from './requests'; import * as tonconnect from './tonconnect'; +import * as intents from './intents'; import * as nft from './nft'; import * as jettons from './jettons'; import * as browser from './browser'; @@ -67,6 +68,16 @@ export const api: WalletKitBridgeApi = { disconnectSession: tonconnect.disconnectSession, processInternalBrowserRequest: tonconnect.processInternalBrowserRequest, + // Intents + handleIntentUrl: intents.handleIntentUrl, + isIntentUrl: intents.isIntentUrl, + intentItemsToTransactionRequest: intents.intentItemsToTransactionRequest, + approveTransactionIntent: intents.approveTransactionIntent, + approveSignDataIntent: intents.approveSignDataIntent, + approveActionIntent: intents.approveActionIntent, + rejectIntent: intents.rejectIntent, + processConnectAfterIntent: intents.processConnectAfterIntent, + // NFTs getNfts: nft.getNfts, getNft: nft.getNft, diff --git a/packages/walletkit-android-bridge/src/api/initialization.ts b/packages/walletkit-android-bridge/src/api/initialization.ts index 0266f3772..4f119c9ca 100644 --- a/packages/walletkit-android-bridge/src/api/initialization.ts +++ b/packages/walletkit-android-bridge/src/api/initialization.ts @@ -15,6 +15,7 @@ import type { ConnectionRequestEvent, DisconnectionEvent, + IntentEvent, RequestErrorEvent, SendTransactionRequestEvent, SignDataRequestEvent, @@ -105,6 +106,17 @@ export async function setEventsListeners(args?: SetEventsListenersArgs): Promise kit.onRequestError(eventListeners.onErrorListener); + // Register intent listener - forwards IntentEvent for wallet UI + if (eventListeners.onIntentListener) { + kit.removeIntentRequestCallback?.(); + } + + eventListeners.onIntentListener = (event: IntentEvent) => { + callback('intentRequest', event); + }; + + kit.onIntentRequest?.(eventListeners.onIntentListener); + return { ok: true }; } @@ -139,5 +151,10 @@ export async function removeEventListeners(): Promise<{ ok: true }> { eventListeners.onErrorListener = null; } + if (eventListeners.onIntentListener) { + kit.removeIntentRequestCallback?.(); + eventListeners.onIntentListener = null; + } + return { ok: true }; } diff --git a/packages/walletkit-android-bridge/src/api/intents.ts b/packages/walletkit-android-bridge/src/api/intents.ts new file mode 100644 index 000000000..f5e0a3d04 --- /dev/null +++ b/packages/walletkit-android-bridge/src/api/intents.ts @@ -0,0 +1,226 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +/** + * intents.ts – Intent URL handling operations + * + * Handles TonConnect intent deep links (tc://intent_inline?...) that allow + * dApps to request actions without a prior TonConnect session. + */ + +import type { + TransactionRequest, + IntentItem, + TransactionIntentEvent, + SignDataIntentEvent, + ActionIntentEvent, + IntentEvent, + IntentTransactionResponseSuccess, + IntentSignDataResponseSuccess, + IntentResponseError, + IntentResponse, + SignDataIntentPayload, + Wallet, +} from '@ton/walletkit'; + +import type { + HandleIntentUrlArgs, + IsIntentUrlArgs, + IntentItemsToTransactionRequestArgs, + ApproveTransactionIntentArgs, + ApproveSignDataIntentArgs, + ApproveActionIntentArgs, + ProcessConnectAfterIntentArgs, + RejectIntentArgs, +} from '../types'; +import type { WalletKitInstance } from '../types/walletkit'; +import { callBridge, callOnWalletBridge } from '../utils/bridgeWrapper'; + +/** + * Check if a URL is an intent URL (tc://intent_inline?... or tc://intent?...) + */ +export async function isIntentUrl(args: IsIntentUrlArgs): Promise { + return callBridge('isIntentUrl', async (kit) => { + return kit.isIntentUrl(args.url); + }); +} + +/** + * Handle an intent URL + * Parses the URL and emits an intent event for the wallet UI + */ +export async function handleIntentUrl(args: HandleIntentUrlArgs): Promise { + return callBridge('handleIntentUrl', async (kit) => { + return await kit.handleIntentUrl(args.url); + }); +} + +/** + * Convert intent items to a transaction request + * Used when approving an intent to build the actual transaction + */ +export async function intentItemsToTransactionRequest( + args: IntentItemsToTransactionRequestArgs, +): Promise { + return callOnWalletBridge( + args.walletId, + 'intentItemsToTransactionRequest', + async (kit: WalletKitInstance, wallet: Wallet) => { + // Convert the simplified event structure to TransactionIntentEvent + const event: TransactionIntentEvent = { + id: args.event.id, + type: args.event.type, + clientId: '', // Not needed for conversion + hasConnectRequest: false, + network: args.event.network, + validUntil: args.event.validUntil, + items: args.event.items as IntentItem[], + }; + + return await kit.intentItemsToTransactionRequest(event, wallet); + }, + ); +} + +/** + * Approve a transaction intent (txIntent or signMsg) + * + * For txIntent: Signs and sends the transaction to the blockchain + * For signMsg: Signs but does NOT send (for gasless transactions) + */ +export async function approveTransactionIntent( + args: ApproveTransactionIntentArgs, +): Promise { + return callBridge('approveTransactionIntent', async (kit) => { + // Convert the simplified event structure to TransactionIntentEvent + const event: TransactionIntentEvent = { + id: args.event.id, + clientId: args.event.clientId, + hasConnectRequest: args.event.hasConnectRequest, + type: args.event.type, + network: args.event.network, + validUntil: args.event.validUntil, + items: args.event.items as IntentItem[], + }; + + if (!kit.approveTransactionIntent) { + throw new Error('approveTransactionIntent not available'); + } + return await kit.approveTransactionIntent(event, args.walletId); + }); +} + +/** + * Approve a sign data intent (signIntent) + */ +export async function approveSignDataIntent( + args: ApproveSignDataIntentArgs, +): Promise { + return callBridge('approveSignDataIntent', async (kit) => { + // Convert payload format + const payload: SignDataIntentPayload = (() => { + switch (args.event.payload.type) { + case 'text': + return { type: 'text' as const, text: args.event.payload.text! }; + case 'binary': + return { type: 'binary' as const, bytes: args.event.payload.bytes! }; + case 'cell': + return { type: 'cell' as const, schema: args.event.payload.schema!, cell: args.event.payload.cell! }; + default: + throw new Error(`Unknown payload type: ${args.event.payload.type}`); + } + })(); + + const event: SignDataIntentEvent = { + id: args.event.id, + clientId: args.event.clientId, + hasConnectRequest: args.event.hasConnectRequest, + type: 'signIntent', + network: args.event.network, + manifestUrl: args.event.manifestUrl, + payload, + }; + + if (!kit.approveSignDataIntent) { + throw new Error('approveSignDataIntent not available'); + } + return await kit.approveSignDataIntent(event, args.walletId); + }); +} + +/** + * Reject an intent request + */ +export function rejectIntent(args: RejectIntentArgs): IntentResponseError { + // Note: rejectIntent is synchronous - it just builds the response object + // The actual response sending is done by the wallet via another mechanism + return { + error: { + code: args.errorCode ?? 300, // USER_DECLINED + message: args.reason ?? 'User declined the request', + }, + id: args.event.id, + }; +} + +/** + * Approve an action intent (actionIntent) + * + * Fetches action details from the action URL and executes the action. + */ +export async function approveActionIntent( + args: ApproveActionIntentArgs, +): Promise { + return callBridge('approveActionIntent', async (kit) => { + const event: ActionIntentEvent = { + id: args.event.id, + clientId: args.event.clientId, + hasConnectRequest: args.event.hasConnectRequest, + type: 'actionIntent', + network: args.event.network, + actionUrl: args.event.actionUrl, + manifestUrl: args.event.manifestUrl, + }; + + if (!kit.approveActionIntent) { + throw new Error('approveActionIntent not available'); + } + return await kit.approveActionIntent(event, args.walletId); + }); +} + +/** + * Process connect request after intent approval + * + * Creates a proper session for the dApp after intent approval. + */ +export async function processConnectAfterIntent( + args: ProcessConnectAfterIntentArgs, +): Promise { + return callBridge('processConnectAfterIntent', async (kit) => { + // Build the IntentEvent from args + const event: IntentEvent = { + id: args.event.id, + clientId: args.event.clientId, + hasConnectRequest: args.event.hasConnectRequest, + type: args.event.type, + connectRequest: args.event.connectRequest ? { + manifestUrl: args.event.connectRequest.manifestUrl, + items: args.event.connectRequest.items?.map(item => ({ + name: item.name as 'ton_addr' | 'ton_proof', + payload: item.payload, + })), + } : undefined, + }; + + if (!kit.processConnectAfterIntent) { + throw new Error('processConnectAfterIntent not available'); + } + return await kit.processConnectAfterIntent(event, args.walletId, args.proof); + }); +} diff --git a/packages/walletkit-android-bridge/src/types/api.ts b/packages/walletkit-android-bridge/src/types/api.ts index 0c4233808..5323de498 100644 --- a/packages/walletkit-android-bridge/src/types/api.ts +++ b/packages/walletkit-android-bridge/src/types/api.ts @@ -253,6 +253,162 @@ export interface HandleTonConnectUrlArgs { url: string; } +export interface HandleIntentUrlArgs { + url: string; +} + +export interface IsIntentUrlArgs { + url: string; +} + +export interface IntentItemsToTransactionRequestArgs { + /** The intent event with items */ + event: { + id: string; + type: 'txIntent' | 'signMsg'; + network?: string; + validUntil?: number; + items: Array<{ + t: 'ton' | 'jetton' | 'nft'; + // TON fields + a?: string; + am?: string; + p?: string; + si?: string; + ec?: Record; + // Jetton fields + ma?: string; + qi?: number; + ja?: string; + d?: string; + rd?: string; + cp?: string; + fta?: string; + fp?: string; + // NFT fields + na?: string; + no?: string; + }>; + }; + /** The wallet ID to use for jetton/NFT address resolution */ + walletId: string; +} + +/** Arguments for approving a transaction intent (txIntent or signMsg) */ +export interface ApproveTransactionIntentArgs { + /** The full transaction intent event */ + event: { + id: string; + clientId: string; + hasConnectRequest: boolean; + type: 'txIntent' | 'signMsg'; + network?: string; + validUntil?: number; + items: Array<{ + t: 'ton' | 'jetton' | 'nft'; + a?: string; + am?: string; + p?: string; + si?: string; + ec?: Record; + ma?: string; + qi?: number; + ja?: string; + d?: string; + rd?: string; + cp?: string; + fta?: string; + fp?: string; + na?: string; + no?: string; + }>; + }; + /** The wallet ID to use for signing */ + walletId: string; +} + +/** Arguments for approving a sign data intent (signIntent) */ +export interface ApproveSignDataIntentArgs { + /** The full sign data intent event */ + event: { + id: string; + clientId: string; + hasConnectRequest: boolean; + type: 'signIntent'; + network?: string; + manifestUrl: string; + payload: { + type: 'text' | 'binary' | 'cell'; + text?: string; + bytes?: string; + schema?: string; + cell?: string; + }; + }; + /** The wallet ID to use for signing */ + walletId: string; +} + +/** Arguments for rejecting any intent */ +export interface RejectIntentArgs { + /** The intent event to reject */ + event: { + id: string; + clientId: string; + type: 'txIntent' | 'signMsg' | 'signIntent' | 'actionIntent'; + }; + /** Optional rejection reason */ + reason?: string; + /** Optional error code */ + errorCode?: number; +} + +/** Arguments for approving an action intent (actionIntent) */ +export interface ApproveActionIntentArgs { + /** The action intent event */ + event: { + id: string; + clientId: string; + hasConnectRequest: boolean; + type: 'actionIntent'; + network?: string; + actionUrl: string; + manifestUrl?: string; + }; + /** The wallet ID to use for signing */ + walletId: string; +} + +/** Arguments for processing connect request after intent approval */ +export interface ProcessConnectAfterIntentArgs { + /** The intent event with connect request */ + event: { + id: string; + clientId: string; + hasConnectRequest: boolean; + type: 'txIntent' | 'signMsg' | 'signIntent' | 'actionIntent'; + connectRequest?: { + manifestUrl: string; + items?: Array<{ + name: string; + payload?: string; + }>; + }; + }; + /** The wallet ID to use for the connection */ + walletId: string; + /** Optional proof (signature, timestamp, domain, payload) */ + proof?: { + signature: string; + timestamp: number; + domain: { + lengthBytes: number; + value: string; + }; + payload: string; + }; +} + export interface WalletDescriptor { address: string; publicKey: string; @@ -292,6 +448,18 @@ export interface WalletKitBridgeApi { // Returns transactions array directly getRecentTransactions(args: GetRecentTransactionsArgs): PromiseOrValue; handleTonConnectUrl(args: HandleTonConnectUrlArgs): PromiseOrValue; + // Intent URL handling + handleIntentUrl(args: HandleIntentUrlArgs): PromiseOrValue; + isIntentUrl(args: IsIntentUrlArgs): PromiseOrValue; + intentItemsToTransactionRequest(args: IntentItemsToTransactionRequestArgs): PromiseOrValue; + approveTransactionIntent(args: ApproveTransactionIntentArgs): PromiseOrValue<{ result: string; id: string }>; + approveSignDataIntent(args: ApproveSignDataIntentArgs): PromiseOrValue<{ + result: { signature: string; address: string; timestamp: number; domain: string; payload: { type: string; text?: string; bytes?: string; schema?: string; cell?: string } }; + id: string; + }>; + rejectIntent(args: RejectIntentArgs): PromiseOrValue<{ error: { code: number; message: string }; id: string }>; + approveActionIntent(args: ApproveActionIntentArgs): PromiseOrValue<{ result: unknown; id: string }>; + processConnectAfterIntent(args: ProcessConnectAfterIntentArgs): PromiseOrValue; createTransferTonTransaction(args: CreateTransferTonTransactionArgs): PromiseOrValue; createTransferMultiTonTransaction(args: CreateTransferMultiTonTransactionArgs): PromiseOrValue; getTransactionPreview(args: TransactionContentArgs): PromiseOrValue; diff --git a/packages/walletkit-android-bridge/src/types/events.ts b/packages/walletkit-android-bridge/src/types/events.ts index 9d2424018..7b5b74281 100644 --- a/packages/walletkit-android-bridge/src/types/events.ts +++ b/packages/walletkit-android-bridge/src/types/events.ts @@ -14,6 +14,7 @@ export type WalletKitBridgeEventType = | 'connectRequest' | 'transactionRequest' | 'signDataRequest' + | 'intentRequest' | 'disconnect' | 'requestError' | 'browserPageStarted' diff --git a/packages/walletkit-android-bridge/src/types/walletkit.ts b/packages/walletkit-android-bridge/src/types/walletkit.ts index de966d3b7..0b956075e 100644 --- a/packages/walletkit-android-bridge/src/types/walletkit.ts +++ b/packages/walletkit-android-bridge/src/types/walletkit.ts @@ -14,13 +14,19 @@ import type { DeviceInfo, DisconnectionEvent, InjectedToExtensionBridgeRequestPayload, + IntentEvent, + IntentTransactionResponseSuccess, + IntentSignDataResponseSuccess, + IntentResponseError, Network, RequestErrorEvent, SendTransactionApprovalResponse, SendTransactionRequestEvent, SignDataApprovalResponse, + SignDataIntentEvent, SignDataRequestEvent, TONConnectSession, + TransactionIntentEvent, TransactionRequest, Wallet, WalletAdapter, @@ -74,6 +80,28 @@ export interface WalletKitInstance { addWallet(adapter: WalletAdapter): Promise; handleNewTransaction(wallet: Wallet, transaction: TransactionRequest): Promise; handleTonConnectUrl(url: string): Promise; + // Intent URL handling + isIntentUrl(url: string): boolean; + handleIntentUrl(url: string): Promise; + intentItemsToTransactionRequest( + event: TransactionIntentEvent, + wallet: Wallet, + ): Promise; + approveTransactionIntent?( + event: TransactionIntentEvent, + walletId: string, + ): Promise; + approveSignDataIntent?( + event: SignDataIntentEvent, + walletId: string, + ): Promise; + rejectIntent?( + event: IntentEvent, + reason?: string, + errorCode?: number, + ): IntentResponseError; + onIntentRequest?(callback: (event: IntentEvent) => void): void; + removeIntentRequestCallback?(): void; listSessions?(): Promise; disconnect?(sessionId?: string): Promise; processInjectedBridgeRequest?( diff --git a/packages/walletkit/src/core/IntentHandler.ts b/packages/walletkit/src/core/IntentHandler.ts new file mode 100644 index 000000000..025b61b70 --- /dev/null +++ b/packages/walletkit/src/core/IntentHandler.ts @@ -0,0 +1,964 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +/** + * IntentHandler - Handles TonConnect intent deep links + * + * Parses and processes intent URLs (tc://intent_inline?...) that allow + * dApps to request actions without a prior TonConnect session. + */ + +import { Address } from '@ton/core'; +import { sha256_sync } from '@ton/crypto'; +import type { ConnectRequest } from '@tonconnect/protocol'; + +import { globalLogger } from './Logger'; +import type { WalletManager } from './WalletManager'; +import type { EventEmitter } from './EventEmitter'; +import type { RequestProcessor } from './RequestProcessor'; +import { WalletKitError, ERROR_CODES } from '../errors'; +import type { Wallet } from '../api/interfaces'; +import type { + IntentRequest, + IntentMethod, + ParsedIntentUrl, + IntentEvent, + TransactionIntentEvent, + SignDataIntentEvent, + ActionIntentEvent, + SendTransactionIntentRequest, + SignMessageIntentRequest, + SignDataIntentRequest, + SendActionIntentRequest, + IntentItem, + SendTonIntentItem, + SendJettonIntentItem, + SendNftIntentItem, + IntentTransactionResponseSuccess, + IntentSignDataResponseSuccess, + IntentResponseError, + IntentResponse, + SignDataIntentPayload, + ActionUrlResponse, + SendTransactionAction, + SignDataAction, + ActionTransactionMessage, +} from '../types/intents'; +import type { TransactionRequest, TransactionRequestMessage, PreparedSignData, SignData } from '../api/models'; +import type { Base64String, Hex } from '../api/models/core/Primitives'; +import type { + ConnectionRequestEvent, + ConnectionRequestEventRequestedItem, + ConnectionRequestEventPreviewPermission, +} from '../api/models/bridge/ConnectionRequestEvent'; +import type { ConnectionApprovalProof } from '../api/models/bridge/ConnectionApprovalResponse'; + +const log = globalLogger.createChild('IntentHandler'); + +/** + * Intent URL schemes + */ +const INTENT_INLINE_SCHEME = 'tc://intent_inline'; +const INTENT_SCHEME = 'tc://intent'; + +/** + * Intent error codes (from spec) + */ +export const INTENT_ERROR_CODES = { + UNKNOWN: 0, + BAD_REQUEST: 1, + UNKNOWN_APP: 100, + ACTION_URL_UNREACHABLE: 200, + USER_DECLINED: 300, + METHOD_NOT_SUPPORTED: 400, +} as const; + +export type IntentErrorCode = (typeof INTENT_ERROR_CODES)[keyof typeof INTENT_ERROR_CODES]; + +/** + * Handles TonConnect intent deep links + */ +export class IntentHandler { + constructor( + private walletManager: WalletManager, + private eventEmitter: EventEmitter, + private requestProcessor: RequestProcessor, + ) {} + + // ======================================================================== + // URL Parsing + // ======================================================================== + + /** + * Check if a URL is an intent URL + */ + isIntentUrl(url: string): boolean { + const normalizedUrl = url.trim().toLowerCase(); + return normalizedUrl.startsWith(INTENT_INLINE_SCHEME) || normalizedUrl.startsWith(INTENT_SCHEME); + } + + /** + * Parse an intent URL and extract the request payload + */ + parseIntentUrl(url: string): ParsedIntentUrl { + log.debug('Parsing intent URL', { url }); + + try { + const parsedUrl = new URL(url); + + // Get client ID (public key) + const clientId = parsedUrl.searchParams.get('id'); + if (!clientId) { + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Missing client ID (id) in intent URL'); + } + + // Check if it's inline (Approach 2) or storage-based (Approach 1) + if (url.toLowerCase().startsWith(INTENT_INLINE_SCHEME)) { + return this.parseInlineIntent(parsedUrl, clientId); + } else { + // Approach 1 with object storage - not implemented yet + throw new WalletKitError( + ERROR_CODES.VALIDATION_ERROR, + 'Object storage intents (Approach 1) are not yet supported. Use intent_inline instead.', + ); + } + } catch (error) { + if (error instanceof WalletKitError) { + throw error; + } + log.error('Failed to parse intent URL', { error, url }); + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Invalid intent URL format', error as Error, { + url, + }); + } + } + + /** + * Parse inline intent (Approach 2: URL-Embedded Data) + * + * The `r` parameter can be encoded in two ways: + * 1. base64url(json.stringify(payload)) - standard spec format + * 2. encodeURIComponent(json.stringify(payload)) - URL-encoded JSON + * + * We detect the format by checking if the payload starts with characters + * that look like JSON (after URL decoding) or base64url. + */ + private parseInlineIntent(parsedUrl: URL, clientId: string): ParsedIntentUrl { + const encodedPayload = parsedUrl.searchParams.get('r'); + if (!encodedPayload) { + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Missing payload (r) in intent URL'); + } + + // Try to decode the payload - detect format automatically + const jsonPayload = this.decodeIntentPayload(encodedPayload); + log.debug('Decoded intent payload', { jsonPayload }); + + // Parse JSON to IntentRequest + let request: IntentRequest; + try { + request = JSON.parse(jsonPayload) as IntentRequest; + } catch (error) { + throw new WalletKitError( + ERROR_CODES.VALIDATION_ERROR, + 'Invalid JSON in intent payload', + error as Error, + ); + } + + // Validate the request + this.validateIntentRequest(request); + + return { + clientId, + request, + }; + } + + /** + * Decode intent payload - handles both URL-encoded JSON and base64url formats + * + * Detection logic: + * - If the string starts with `%7B` or `{`, it's URL-encoded JSON + * - Otherwise, try base64url decoding + */ + private decodeIntentPayload(encoded: string): string { + // Check if it's URL-encoded JSON (starts with %7B which is `{` URL-encoded) + // or already decoded JSON (starts with `{`) + if (encoded.startsWith('%7B') || encoded.startsWith('%257B') || encoded.startsWith('{')) { + // URL-encoded JSON - decode it + // Handle double-encoding (%25 = %) + let decoded = decodeURIComponent(encoded); + // Check if still URL-encoded (double-encoding case) + if (decoded.startsWith('%7B') || decoded.startsWith('%')) { + decoded = decodeURIComponent(decoded); + } + return decoded; + } + + // Try base64url decoding + return this.decodeBase64Url(encoded); + } + + /** + * Decode base64url to string + */ + private decodeBase64Url(encoded: string): string { + // Replace URL-safe characters with standard base64 characters + let base64 = encoded.replace(/-/g, '+').replace(/_/g, '/'); + + // Add padding if necessary + const padding = base64.length % 4; + if (padding) { + base64 += '='.repeat(4 - padding); + } + + // Decode + if (typeof atob === 'function') { + return atob(base64); + } else { + // Node.js environment + return Buffer.from(base64, 'base64').toString('utf-8'); + } + } + + /** + * Validate an intent request + */ + private validateIntentRequest(request: IntentRequest): void { + if (!request.id) { + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Intent request missing id'); + } + + if (!request.m) { + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Intent request missing method (m)'); + } + + const validMethods: IntentMethod[] = ['txIntent', 'signMsg', 'signIntent', 'actionIntent']; + if (!validMethods.includes(request.m)) { + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, `Unknown intent method: ${request.m}`); + } + + // Validate specific intent types + switch (request.m) { + case 'txIntent': + case 'signMsg': + this.validateTransactionIntent(request); + break; + case 'signIntent': + this.validateSignDataIntent(request); + break; + case 'actionIntent': + this.validateActionIntent(request); + break; + } + } + + private validateTransactionIntent(request: SendTransactionIntentRequest | SignMessageIntentRequest): void { + if (!request.i || !Array.isArray(request.i) || request.i.length === 0) { + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Intent missing items (i)'); + } + + // Validate each item + for (const item of request.i) { + if (!item.t) { + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Intent item missing type (t)'); + } + + const validTypes = ['ton', 'jetton', 'nft']; + if (!validTypes.includes(item.t)) { + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, `Unknown intent item type: ${item.t}`); + } + + this.validateIntentItem(item); + } + } + + private validateIntentItem(item: IntentItem): void { + switch (item.t) { + case 'ton': { + const tonItem = item as SendTonIntentItem; + if (!tonItem.a) { + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'TON intent item missing address (a)'); + } + if (!tonItem.am) { + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'TON intent item missing amount (am)'); + } + break; + } + case 'jetton': { + const jettonItem = item as SendJettonIntentItem; + if (!jettonItem.ma) { + throw new WalletKitError( + ERROR_CODES.VALIDATION_ERROR, + 'Jetton intent item missing master address (ma)', + ); + } + if (!jettonItem.ja) { + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Jetton intent item missing amount (ja)'); + } + if (!jettonItem.d) { + throw new WalletKitError( + ERROR_CODES.VALIDATION_ERROR, + 'Jetton intent item missing destination (d)', + ); + } + break; + } + case 'nft': { + const nftItem = item as SendNftIntentItem; + if (!nftItem.na) { + throw new WalletKitError( + ERROR_CODES.VALIDATION_ERROR, + 'NFT intent item missing NFT address (na)', + ); + } + if (!nftItem.no) { + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'NFT intent item missing new owner (no)'); + } + break; + } + } + } + + private validateSignDataIntent(request: SignDataIntentRequest): void { + // Manifest URL can be in `mu` or `c.manifestUrl` + const manifestUrl = request.mu || request.c?.manifestUrl; + if (!manifestUrl) { + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Sign data intent missing manifest URL (mu or c.manifestUrl)'); + } + if (!request.p) { + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Sign data intent missing payload (p)'); + } + if (!request.p.type) { + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Sign data payload missing type'); + } + } + + private validateActionIntent(request: SendActionIntentRequest): void { + if (!request.a) { + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Action intent missing action URL (a)'); + } + } + + // ======================================================================== + // Intent Processing + // ======================================================================== + + /** + * Handle an intent URL + * Parses the URL and emits an intent event for the wallet UI + */ + async handleIntentUrl(url: string): Promise { + log.info('Handling intent URL', { url }); + + const parsed = this.parseIntentUrl(url); + const event = this.createIntentEvent(parsed); + + // Emit the intent event for wallet UI to handle + this.eventEmitter.emit('intent', event); + + log.info('Intent event emitted', { type: event.type, id: event.id }); + } + + /** + * Create an intent event from parsed URL + */ + private createIntentEvent(parsed: ParsedIntentUrl): IntentEvent { + const { clientId, request } = parsed; + const hasConnectRequest = !!request.c; + + const baseEvent = { + id: request.id, + clientId, + hasConnectRequest, + connectRequest: request.c, + }; + + switch (request.m) { + case 'txIntent': + case 'signMsg': { + const txRequest = request as SendTransactionIntentRequest | SignMessageIntentRequest; + return { + ...baseEvent, + type: request.m, + network: txRequest.n, + validUntil: txRequest.vu, + items: txRequest.i, + } as TransactionIntentEvent; + } + case 'signIntent': { + const signRequest = request as SignDataIntentRequest; + // Manifest URL can be in `mu` or `c.manifestUrl` + const manifestUrl = signRequest.mu || signRequest.c?.manifestUrl || ''; + return { + ...baseEvent, + type: 'signIntent', + network: signRequest.n, + manifestUrl, + payload: signRequest.p, + } as SignDataIntentEvent; + } + case 'actionIntent': { + const actionRequest = request as SendActionIntentRequest; + return { + ...baseEvent, + type: 'actionIntent', + actionUrl: actionRequest.a, + } as ActionIntentEvent; + } + } + } + + // ======================================================================== + // Intent Approval/Rejection + // ======================================================================== + + /** + * Approve a transaction intent (txIntent) + * + * Signs and sends the transaction to the blockchain, + * then returns the signed BoC for the dApp to verify. + * + * @param event - The transaction intent event + * @param walletId - The wallet to use for signing + * @returns The approval response with signed BoC + */ + async approveTransactionIntent( + event: TransactionIntentEvent, + walletId: string, + ): Promise { + log.info('Approving transaction intent', { id: event.id, walletId, type: event.type }); + + const wallet = this.walletManager.getWallet(walletId); + if (!wallet) { + throw new WalletKitError(ERROR_CODES.WALLET_NOT_FOUND, `Wallet not found: ${walletId}`); + } + + // Convert intent items to transaction request + const transactionRequest = await this.intentItemsToTransactionRequest( + event.items, + wallet, + event.network, + event.validUntil, + ); + + // Sign the transaction + const signedBoc = await wallet.getSignedSendTransaction(transactionRequest); + + // For txIntent, send to blockchain + if (event.type === 'txIntent') { + // Use the wallet's API client + await wallet.client.sendBoc(signedBoc); + log.info('Transaction sent to blockchain', { id: event.id }); + } + + // Note: signMsg type does NOT send to blockchain - that's the key difference + // The dApp will use the signed BoC for gasless transaction relaying + + const response: IntentTransactionResponseSuccess = { + result: signedBoc, + id: event.id, + }; + + log.info('Intent approved successfully', { id: event.id, type: event.type }); + return response; + } + + /** + * Approve a sign data intent (signIntent) + * + * Signs the data and returns the signature. + * + * @param event - The sign data intent event + * @param walletId - The wallet to use for signing + * @returns The approval response with signature + */ + async approveSignDataIntent( + event: SignDataIntentEvent, + walletId: string, + ): Promise { + log.info('Approving sign data intent', { id: event.id, walletId }); + + const wallet = this.walletManager.getWallet(walletId); + if (!wallet) { + throw new WalletKitError(ERROR_CODES.WALLET_NOT_FOUND, `Wallet not found: ${walletId}`); + } + + // Get wallet address + const address = wallet.getAddress().toString(); + const timestamp = Math.floor(Date.now() / 1000); + + // Extract domain from manifest URL + const domain = new URL(event.manifestUrl).hostname; + + // Convert intent payload to SignData format + let signData: SignData; + switch (event.payload.type) { + case 'text': + signData = { type: 'text', value: { content: event.payload.text } }; + break; + case 'binary': + signData = { type: 'binary', value: { content: event.payload.bytes as Base64String } }; + break; + case 'cell': + signData = { type: 'cell', value: { schema: event.payload.schema, content: event.payload.cell as Base64String } }; + break; + } + + // Build PreparedSignData + const dataToHash = JSON.stringify({ address, timestamp, domain, payload: signData }); + const dataHash = sha256_sync(dataToHash).toString('hex'); + + const preparedSignData: PreparedSignData = { + address, + timestamp, + domain, + payload: { + network: event.network ? { chainId: event.network } : undefined, + data: signData, + }, + hash: `0x${dataHash}` as Hex, + }; + + // Sign the data + const signature = await wallet.getSignedSignData(preparedSignData); + + const response: IntentSignDataResponseSuccess = { + result: { + signature, + address, + timestamp, + domain, + payload: event.payload, + }, + id: event.id, + }; + + log.info('Sign data intent approved', { id: event.id }); + return response; + } + + /** + * Approve an action intent (actionIntent) + * + * Fetches action details from the action URL, then executes the appropriate action. + * The action URL should return a JSON object with action_type and action fields. + * + * @param event - The action intent event + * @param walletId - The wallet to use for the action + * @returns The approval response (either transaction or sign data response) + */ + async approveActionIntent( + event: ActionIntentEvent, + walletId: string, + ): Promise { + log.info('Approving action intent', { id: event.id, walletId, actionUrl: event.actionUrl }); + + const wallet = this.walletManager.getWallet(walletId); + if (!wallet) { + throw new WalletKitError(ERROR_CODES.WALLET_NOT_FOUND, `Wallet not found: ${walletId}`); + } + + // Build the action URL with address parameter + const walletAddress = wallet.getAddress().toString(); + const actionUrlWithAddress = new URL(event.actionUrl); + actionUrlWithAddress.searchParams.set('address', walletAddress); + + // Fetch action details from the URL + let actionResponse: ActionUrlResponse; + try { + const response = await fetch(actionUrlWithAddress.toString()); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + actionResponse = await response.json() as ActionUrlResponse; + } catch (error) { + throw new WalletKitError( + ERROR_CODES.NETWORK_ERROR, + `Failed to fetch action from URL: ${error instanceof Error ? error.message : 'Unknown error'}`, + ); + } + + // Validate action response + if (!actionResponse.action_type || !actionResponse.action) { + throw new WalletKitError( + ERROR_CODES.VALIDATION_ERROR, + 'Invalid action response: missing action_type or action', + ); + } + + // Execute based on action type + switch (actionResponse.action_type) { + case 'sendTransaction': { + // Build transaction request from action + const txAction = actionResponse.action as SendTransactionAction; + const transactionRequest: TransactionRequest = { + messages: txAction.messages.map((msg: ActionTransactionMessage) => ({ + address: msg.address, + amount: msg.amount, + payload: msg.payload as Base64String | undefined, + stateInit: msg.stateInit as Base64String | undefined, + extraCurrency: msg.extra_currency, + })), + network: txAction.network ? { chainId: txAction.network } : undefined, + validUntil: txAction.valid_until, + }; + + // Sign and send + const signedBoc = await wallet.getSignedSendTransaction(transactionRequest); + await wallet.client.sendBoc(signedBoc); + + log.info('Action intent (sendTransaction) approved', { id: event.id }); + return { result: signedBoc, id: event.id }; + } + + case 'signData': { + // Build sign data event from action + const signAction = actionResponse.action as SignDataAction; + + // Determine the manifest URL for domain binding: + // 1. Use from connectRequest if available + // 2. Otherwise use action URL origin as fallback + let manifestUrl = event.actionUrl; + if (event.connectRequest?.manifestUrl) { + manifestUrl = event.connectRequest.manifestUrl; + } else { + // Use action URL origin as the manifest URL (proper domain binding) + try { + const actionOrigin = new URL(event.actionUrl); + manifestUrl = `${actionOrigin.origin}/tonconnect-manifest.json`; + } catch (_) { + // Keep original action URL if parsing fails + } + } + + // Create a synthetic sign data event + const signDataEvent: SignDataIntentEvent = { + id: event.id, + clientId: event.clientId, + hasConnectRequest: event.hasConnectRequest, + connectRequest: event.connectRequest, + type: 'signIntent', + network: signAction.network, + manifestUrl, + payload: signAction as SignDataIntentPayload, + }; + + return this.approveSignDataIntent(signDataEvent, walletId); + } + + default: + throw new WalletKitError( + ERROR_CODES.VALIDATION_ERROR, + `Unknown action type: ${actionResponse.action_type}`, + ); + } + } + + /** + * Reject an intent request + * + * @param event - The intent event to reject + * @param reason - Optional rejection reason + * @param errorCode - Optional error code (defaults to USER_DECLINED) + * @returns The rejection response + */ + rejectIntent( + event: IntentEvent, + reason?: string, + errorCode?: IntentErrorCode, + ): IntentResponseError { + log.info('Rejecting intent', { id: event.id, reason }); + + const response: IntentResponseError = { + error: { + code: errorCode ?? INTENT_ERROR_CODES.USER_DECLINED, + message: reason ?? 'User declined the request', + }, + id: event.id, + }; + + return response; + } + + /** + * Process connect request after intent approval + * + * This creates a proper ConnectionRequestEvent from the intent's ConnectRequest + * and uses the existing connection infrastructure to establish a session. + * + * @param event - The intent event with connect request + * @param walletId - The wallet to use for the connection + * @param proof - Optional proof response (signature, timestamp, domain, payload) + */ + async processConnectAfterIntent( + event: IntentEvent, + walletId: string, + proof?: ConnectionApprovalProof, + ): Promise { + if (!event.hasConnectRequest || !event.connectRequest) { + log.info('No connect request to process', { id: event.id }); + return; + } + + log.info('Processing connect request after intent', { + id: event.id, + walletId, + manifestUrl: event.connectRequest.manifestUrl, + }); + + const wallet = this.walletManager.getWallet(walletId); + if (!wallet) { + throw new WalletKitError(ERROR_CODES.WALLET_NOT_FOUND, `Wallet not found: ${walletId}`); + } + + // Create ConnectionRequestEvent from the ConnectRequest + const connectRequest = event.connectRequest; + + // Build requested items + const requestedItems: ConnectionRequestEventRequestedItem[] = []; + if (connectRequest.items) { + for (const item of connectRequest.items) { + if (item.name === 'ton_addr') { + requestedItems.push({ type: 'ton_addr' }); + } else if (item.name === 'ton_proof' && 'payload' in item) { + requestedItems.push({ + type: 'ton_proof', + value: { payload: item.payload as string }, + }); + } + } + } + + // Fetch manifest for dApp info + let manifest: { name?: string; url?: string; iconUrl?: string; description?: string } | null = null; + const manifestUrl = connectRequest.manifestUrl; + + if (manifestUrl) { + try { + const response = await fetch(manifestUrl); + if (response.ok) { + manifest = await response.json(); + } + } catch (error) { + log.warn('Failed to fetch manifest for intent connect', { error, manifestUrl }); + } + } + + // Extract domain from manifest URL + let domain = ''; + if (manifestUrl) { + try { + domain = new URL(manifestUrl).hostname; + } catch (_) { + // ignore + } + } + + // Build permissions + const permissions: ConnectionRequestEventPreviewPermission[] = []; + if (requestedItems.some(item => item.type === 'ton_addr')) { + permissions.push({ + name: 'ton_addr', + title: 'TON Address', + description: 'Gives dApp information about your TON address', + }); + } + if (requestedItems.some(item => item.type === 'ton_proof')) { + permissions.push({ + name: 'ton_proof', + title: 'TON Proof', + description: 'Gives dApp signature that can be used to verify your access to private key', + }); + } + + // Create the ConnectionRequestEvent + const connectionRequestEvent: ConnectionRequestEvent = { + id: `intent-connect-${event.id}`, + from: event.clientId, + walletId: walletId, + walletAddress: wallet.getAddress(), + domain: domain, + isJsBridge: false, + requestedItems, + preview: { + permissions, + dAppInfo: { + url: manifest?.url || domain, + name: manifest?.name || domain, + description: manifest?.description, + iconUrl: manifest?.iconUrl, + manifestUrl: manifestUrl, + }, + }, + dAppInfo: { + url: manifest?.url || domain, + name: manifest?.name || domain, + description: manifest?.description, + iconUrl: manifest?.iconUrl, + manifestUrl: manifestUrl, + }, + }; + + // Use the RequestProcessor to approve the connect request + // This will create a session and send the response via the bridge + await this.requestProcessor.approveConnectRequest(connectionRequestEvent, proof ? { proof } : undefined); + + log.info('Connect request processed after intent', { id: event.id }); + } + + // ======================================================================== + // Intent Item Conversion + // ======================================================================== + + /** + * Convert intent items to transaction request messages + * @param items - Intent items to convert + * @param wallet - Wallet to use for jetton wallet address resolution + * @param network - Optional network chain ID + * @param validUntil - Optional validity timestamp + */ + async intentItemsToTransactionRequest( + items: IntentItem[], + wallet: Wallet, + network?: string, + validUntil?: number, + ): Promise { + const messages: TransactionRequestMessage[] = []; + const walletAddress = wallet.getAddress().toString(); + + for (const item of items) { + const message = await this.intentItemToMessage(item, wallet, walletAddress); + messages.push(message); + } + + return { + messages, + network: network ? { chainId: network } : undefined, + validUntil: validUntil ?? Math.floor(Date.now() / 1000) + 300, + }; + } + + /** + * Convert a single intent item to a transaction message + */ + private async intentItemToMessage( + item: IntentItem, + wallet: Wallet, + walletAddress: string, + ): Promise { + switch (item.t) { + case 'ton': + return this.tonIntentToMessage(item); + case 'jetton': + return this.jettonIntentToMessage(item, wallet, walletAddress); + case 'nft': + return this.nftIntentToMessage(item, walletAddress); + default: + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, `Unknown intent item type: ${(item as IntentItem).t}`); + } + } + + /** + * Convert TON intent item to message + */ + private tonIntentToMessage(item: SendTonIntentItem): TransactionRequestMessage { + return { + address: item.a, + amount: item.am, + payload: item.p as Base64String | undefined, + stateInit: item.si as Base64String | undefined, + extraCurrency: item.ec ? (item.ec as Record) : undefined, + }; + } + + /** + * Convert Jetton intent item to message + * Builds the jetton transfer message body + */ + private async jettonIntentToMessage( + item: SendJettonIntentItem, + wallet: Wallet, + walletAddress: string, + ): Promise { + const { beginCell, Cell } = await import('@ton/core'); + + log.info('jettonIntentToMessage v2 - using Cell.fromBase64', { + hasFp: !!item.fp, + hasCp: !!item.cp, + fpLength: item.fp?.length ?? 0 + }); + + // Build jetton transfer body according to TEP-74 + // Note: fp and cp are base64-encoded BoC (Bag of Cells), not raw bytes + const forwardPayloadCell = item.fp ? Cell.fromBase64(item.fp) : null; + const customPayloadCell = item.cp ? Cell.fromBase64(item.cp) : null; + + log.info('Payload cells created', { + fpBits: forwardPayloadCell?.bits.length ?? 0, + cpBits: customPayloadCell?.bits.length ?? 0, + }); + + const body = beginCell() + .storeUint(0xf8a7ea5, 32) // op: transfer + .storeUint(item.qi ?? 0, 64) // query_id + .storeCoins(BigInt(item.ja)) // amount + .storeAddress(Address.parse(item.d)) // destination + .storeAddress(item.rd ? Address.parse(item.rd) : Address.parse(walletAddress)) // response_destination + .storeMaybeRef(customPayloadCell) // custom_payload (already a Cell) + .storeCoins(BigInt(item.fta ?? '0')) // forward_ton_amount + .storeMaybeRef(forwardPayloadCell) // forward_payload (already a Cell) + .endCell(); + + log.info('Jetton transfer body built', { bits: body.bits.length, refs: body.refs.length }); + + // Get jetton wallet address for the user using the wallet's method + const jettonWalletAddress = await wallet.getJettonWalletAddress(item.ma); + + // Default amount for jetton transfer fee + const feeAmount = item.fta ? BigInt(item.fta) + BigInt(50000000) : BigInt(50000000); // 0.05 TON + forward + + return { + address: jettonWalletAddress, + amount: feeAmount.toString(), + payload: body.toBoc().toString('base64') as Base64String, + }; + } + + /** + * Convert NFT intent item to message + * Builds the NFT transfer message body + */ + private async nftIntentToMessage(item: SendNftIntentItem, walletAddress: string): Promise { + const { beginCell, Cell } = await import('@ton/core'); + + // Build NFT transfer body according to TEP-62 + // Note: fp and cp are base64-encoded BoC (Bag of Cells), not raw bytes + const forwardPayloadCell = item.fp ? Cell.fromBase64(item.fp) : null; + const customPayloadCell = item.cp ? Cell.fromBase64(item.cp) : null; + + const body = beginCell() + .storeUint(0x5fcc3d14, 32) // op: transfer + .storeUint(item.qi ?? 0, 64) // query_id + .storeAddress(Address.parse(item.no)) // new_owner + .storeAddress(item.rd ? Address.parse(item.rd) : Address.parse(walletAddress)) // response_destination + .storeMaybeRef(customPayloadCell) // custom_payload (already a Cell) + .storeCoins(BigInt(item.fta ?? '0')) // forward_amount + .storeMaybeRef(forwardPayloadCell) // forward_payload (already a Cell) + .endCell(); + + // Default amount for NFT transfer fee + const feeAmount = BigInt(50000000); // 0.05 TON + + return { + address: item.na, + amount: feeAmount.toString(), + payload: body.toBoc().toString('base64') as Base64String, + }; + } +} diff --git a/packages/walletkit/src/core/TonWalletKit.ts b/packages/walletkit/src/core/TonWalletKit.ts index e8947d3a6..6b6b24ec4 100644 --- a/packages/walletkit/src/core/TonWalletKit.ts +++ b/packages/walletkit/src/core/TonWalletKit.ts @@ -27,7 +27,19 @@ import type { TONConnectSessionManager } from '../api/interfaces/TONConnectSessi import type { EventRouter } from './EventRouter'; import type { RequestProcessor } from './RequestProcessor'; import { JettonsManager } from './JettonsManager'; +import { IntentHandler } from './IntentHandler'; import type { JettonsAPI } from '../types/jettons'; +import type { + IntentEvent, + TransactionIntentEvent, + SignDataIntentEvent, + ActionIntentEvent, + IntentTransactionResponseSuccess, + IntentSignDataResponseSuccess, + IntentResponseError, + IntentResponse, +} from '../types/intents'; +import { IntentErrorCode } from './IntentHandler'; import type { RawBridgeEventConnect, RawBridgeEventRestoreConnection, @@ -59,6 +71,7 @@ import type { SignDataApprovalResponse, TONConnectSession, ConnectionApprovalResponse, + ConnectionApprovalProof, } from '../api/models'; import { asAddressFriendly } from '../utils'; @@ -84,6 +97,7 @@ export class TonWalletKit implements ITonWalletKit { // private responseHandler!: ResponseHandler; private networkManager: NetworkManager; private jettonsManager!: JettonsManager; + private intentHandler!: IntentHandler; private initializer: Initializer; private eventProcessor!: StorageEventProcessor; private bridgeManager!: BridgeManager; @@ -215,6 +229,13 @@ export class TonWalletKit implements ITonWalletKit { this.requestProcessor = components.requestProcessor; this.eventProcessor = components.eventProcessor; this.bridgeManager = components.bridgeManager; + + // Initialize IntentHandler after we have all components + this.intentHandler = new IntentHandler( + this.walletManager, + this.eventEmitter, + this.requestProcessor, + ); } /** @@ -487,6 +508,14 @@ export class TonWalletKit implements ITonWalletKit { this.eventRouter.removeErrorCallback(); } + onIntentRequest(cb: (event: IntentEvent) => void): void { + this.eventEmitter.on('intent', cb); + } + + removeIntentRequestCallback(cb: (event: IntentEvent) => void): void { + this.eventEmitter.off('intent', cb); + } + // === URL Processing API === /** @@ -543,6 +572,127 @@ export class TonWalletKit implements ITonWalletKit { await this.eventRouter.routeEvent(bridgeEvent); } + // === Intent Processing API === + + /** + * Check if a URL is an intent URL (tc://intent_inline?... or tc://intent?...) + */ + isIntentUrl(url: string): boolean { + return this.intentHandler?.isIntentUrl(url) ?? false; + } + + /** + * Handle an intent URL + * Parses the URL and emits an intent event for the wallet UI + */ + async handleIntentUrl(url: string): Promise { + await this.ensureInitialized(); + return this.intentHandler.handleIntentUrl(url); + } + + /** + * Convert intent items to a transaction request + * Used when approving an intent to build the actual transaction + */ + async intentItemsToTransactionRequest( + event: TransactionIntentEvent, + wallet: Wallet, + ): Promise { + await this.ensureInitialized(); + return this.intentHandler.intentItemsToTransactionRequest( + event.items, + wallet, + event.network, + event.validUntil, + ); + } + + /** + * Approve a transaction intent (txIntent or signMsg) + * + * For txIntent: Signs and sends the transaction to the blockchain + * For signMsg: Signs but does NOT send (for gasless transactions) + * + * @param event - The transaction intent event + * @param walletId - The wallet ID to use for signing + * @returns The approval response with signed BoC + */ + async approveTransactionIntent( + event: TransactionIntentEvent, + walletId: string, + ): Promise { + await this.ensureInitialized(); + return this.intentHandler.approveTransactionIntent(event, walletId); + } + + /** + * Approve a sign data intent (signIntent) + * + * Signs the data and returns the signature. + * + * @param event - The sign data intent event + * @param walletId - The wallet ID to use for signing + * @returns The approval response with signature + */ + async approveSignDataIntent( + event: SignDataIntentEvent, + walletId: string, + ): Promise { + await this.ensureInitialized(); + return this.intentHandler.approveSignDataIntent(event, walletId); + } + + /** + * Approve an action intent (actionIntent) + * + * Fetches action details from URL and executes the action. + * + * @param event - The action intent event + * @param walletId - The wallet ID to use for signing + * @returns The approval response (transaction or sign data) + */ + async approveActionIntent( + event: ActionIntentEvent, + walletId: string, + ): Promise { + await this.ensureInitialized(); + return this.intentHandler.approveActionIntent(event, walletId); + } + + /** + * Process connect request after intent approval + * + * Creates a proper session for the dApp after intent approval. + * + * @param event - The intent event with connect request + * @param walletId - The wallet to use for the connection + * @param proof - Optional proof response + */ + async processConnectAfterIntent( + event: IntentEvent, + walletId: string, + proof?: ConnectionApprovalProof, + ): Promise { + await this.ensureInitialized(); + return this.intentHandler.processConnectAfterIntent(event, walletId, proof); + } + + /** + * Reject an intent request + * + * @param event - The intent event to reject + * @param reason - Optional rejection reason + * @param errorCode - Optional error code (defaults to USER_DECLINED) + * @returns The rejection response + */ + rejectIntent( + event: IntentEvent, + reason?: string, + errorCode?: IntentErrorCode, + ): IntentResponseError { + return this.intentHandler.rejectIntent(event, reason, errorCode); + } + /** * Parse TON Connect URL to extract connection parameters */ diff --git a/packages/walletkit/src/core/wallet/extensions/jetton.ts b/packages/walletkit/src/core/wallet/extensions/jetton.ts index b482d86f5..b9d956fd7 100644 --- a/packages/walletkit/src/core/wallet/extensions/jetton.ts +++ b/packages/walletkit/src/core/wallet/extensions/jetton.ts @@ -149,6 +149,9 @@ export class WalletJettonClass implements WalletJettonInterface { const parsedStack = ParseStack(result.stack); // Extract the jetton wallet address from the result + if (!parsedStack || parsedStack.length === 0 || !parsedStack[0]) { + throw new Error('Empty response from jetton master contract - jetton may not exist'); + } const jettonWalletAddress = parsedStack[0].type === 'slice' || parsedStack[0].type === 'cell' ? parsedStack[0].cell.asSlice().loadAddress() diff --git a/packages/walletkit/src/index.ts b/packages/walletkit/src/index.ts index ff16607f8..5a7594369 100644 --- a/packages/walletkit/src/index.ts +++ b/packages/walletkit/src/index.ts @@ -103,6 +103,35 @@ export type { AnalyticsAppInfo, AnalyticsManagerOptions } from './analytics'; export { isValidAddress } from './utils/address'; export type { ToncenterEmulationResult } from './utils/toncenterEmulation'; +// Intent types +export { IntentHandler, INTENT_ERROR_CODES } from './core/IntentHandler'; +export type { IntentErrorCode } from './core/IntentHandler'; +export type { + IntentRequest, + IntentMethod, + IntentResponse, + IntentEvent, + TransactionIntentEvent, + SignDataIntentEvent, + ActionIntentEvent, + ParsedIntentUrl, + IntentItem, + SendTonIntentItem, + SendJettonIntentItem, + SendNftIntentItem, + SendTransactionIntentRequest, + SignMessageIntentRequest, + SignDataIntentRequest, + SendActionIntentRequest, + SignDataIntentPayload, + TextSignDataPayload, + BinarySignDataPayload, + CellSignDataPayload, + IntentTransactionResponseSuccess, + IntentSignDataResponseSuccess, + IntentResponseError, +} from './types/intents'; + // API Client types (ApiClient is exported above) export type { TransactionsByAddressRequest, diff --git a/packages/walletkit/src/types/intents.ts b/packages/walletkit/src/types/intents.ts new file mode 100644 index 000000000..2e495062e --- /dev/null +++ b/packages/walletkit/src/types/intents.ts @@ -0,0 +1,377 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +/** + * Intent types for TonConnect deep-link flows. + * + * Intents allow dApps to prepare actions and hand them to wallets + * without requiring a prior TonConnect session. + * + * @see https://github.com/the-ton-tech/ton-connect/blob/feature/instant-transactions/requests-responses.md#intents + */ + +import type { ConnectRequest } from '@tonconnect/protocol'; + +// ============================================================================ +// Intent Item Types (shared across txIntent and signMsg) +// ============================================================================ + +/** + * TON transfer intent item + */ +export interface SendTonIntentItem { + /** Intent type */ + t: 'ton'; + /** Message destination in user-friendly format */ + a: string; + /** Number of nanocoins to send as a decimal string */ + am: string; + /** Raw one-cell BoC encoded in Base64 (payload) */ + p?: string; + /** Raw one-cell BoC encoded in Base64 (stateInit) */ + si?: string; + /** Extra currencies to send with the message */ + ec?: Record; +} + +/** + * Jetton transfer intent item + */ +export interface SendJettonIntentItem { + /** Intent type */ + t: 'jetton'; + /** Jetton master contract address */ + ma: string; + /** Arbitrary request number (query_id) */ + qi?: number; + /** Amount of transferring jettons in elementary units */ + ja: string; + /** Address of the new owner of the jettons (destination) */ + d: string; + /** Response destination address (defaults to user's address) */ + rd?: string; + /** Custom payload BoC (Base64) */ + cp?: string; + /** Forward TON amount in nanotons */ + fta?: string; + /** Forward payload BoC (Base64) */ + fp?: string; +} + +/** + * NFT transfer intent item + */ +export interface SendNftIntentItem { + /** Intent type */ + t: 'nft'; + /** Address of the NFT item to transfer */ + na: string; + /** Arbitrary request number (query_id) */ + qi?: number; + /** Address of the new owner of the NFT */ + no: string; + /** Response destination address (defaults to user's address) */ + rd?: string; + /** Custom payload BoC (Base64) */ + cp?: string; + /** Forward TON amount in nanotons */ + fta?: string; + /** Forward payload BoC (Base64) */ + fp?: string; +} + +/** + * Union type for all intent items + */ +export type IntentItem = SendTonIntentItem | SendJettonIntentItem | SendNftIntentItem; + +// ============================================================================ +// Sign Data Payload Types (for signIntent) +// ============================================================================ + +/** + * Text sign data payload + */ +export interface TextSignDataPayload { + type: 'text'; + text: string; +} + +/** + * Binary sign data payload + */ +export interface BinarySignDataPayload { + type: 'binary'; + bytes: string; // base64 encoded +} + +/** + * Cell sign data payload + */ +export interface CellSignDataPayload { + type: 'cell'; + schema: string; // TL-B schema + cell: string; // base64 encoded BoC +} + +/** + * Union type for sign data payloads + */ +export type SignDataIntentPayload = TextSignDataPayload | BinarySignDataPayload | CellSignDataPayload; + +// ============================================================================ +// Intent Request Types +// ============================================================================ + +/** + * Base interface for all intent requests + */ +export interface BaseIntentRequest { + /** Request ID */ + id: string; + /** Optional connect request to establish connection after intent */ + c?: ConnectRequest; +} + +/** + * Send Transaction Intent request + * Sends the transaction to the blockchain + */ +export interface SendTransactionIntentRequest extends BaseIntentRequest { + /** Intent method */ + m: 'txIntent'; + /** Valid until (unix timestamp) */ + vu?: number; + /** Target network ("-239" for mainnet, "-3" for testnet) */ + n?: string; + /** Ordered list of intent items */ + i: IntentItem[]; +} + +/** + * Sign Message Intent request + * Signs the message but does NOT send to blockchain + * Returns the signed BoC for gasless transactions + */ +export interface SignMessageIntentRequest extends BaseIntentRequest { + /** Intent method */ + m: 'signMsg'; + /** Valid until (unix timestamp) */ + vu?: number; + /** Target network ("-239" for mainnet, "-3" for testnet) */ + n?: string; + /** Ordered list of intent items */ + i: IntentItem[]; +} + +/** + * Sign Data Intent request + */ +export interface SignDataIntentRequest extends BaseIntentRequest { + /** Intent method */ + m: 'signIntent'; + /** Target network */ + n?: string; + /** Manifest URL for domain binding (optional if c.manifestUrl is present) */ + mu?: string; + /** Sign data payload (text, binary, or cell) */ + p: SignDataIntentPayload; +} + +/** + * Send Action Intent request + * Fetches action details from a URL + */ +export interface SendActionIntentRequest extends BaseIntentRequest { + /** Intent method */ + m: 'actionIntent'; + /** Action URL that wallet will call to get action details */ + a: string; +} + +/** + * Union type for all intent requests + */ +export type IntentRequest = + | SendTransactionIntentRequest + | SignMessageIntentRequest + | SignDataIntentRequest + | SendActionIntentRequest; + +/** + * Intent method types + */ +export type IntentMethod = 'txIntent' | 'signMsg' | 'signIntent' | 'actionIntent'; + +// ============================================================================ +// Intent Response Types +// ============================================================================ + +/** + * Success response for transaction/signMessage intents + */ +export interface IntentTransactionResponseSuccess { + /** Signed message BoC (base64 encoded) */ + result: string; + id: string; +} + +/** + * Error response for intents + */ +export interface IntentResponseError { + error: { + code: number; + message: string; + }; + id: string; +} + +/** + * Success response for sign data intent + * Matches spec: SignDataResponseSuccess + */ +export interface IntentSignDataResponseSuccess { + result: { + /** Base64 encoded signature */ + signature: string; + /** Wallet address in raw format (0:hex) */ + address: string; + /** UNIX timestamp in seconds (UTC) */ + timestamp: number; + /** App domain name (as url part, without encoding) */ + domain: string; + /** Payload from the request */ + payload: SignDataIntentPayload; + }; + id: string; +} + +/** + * Union type for intent responses + */ +export type IntentResponse = + | IntentTransactionResponseSuccess + | IntentSignDataResponseSuccess + | IntentResponseError; + +// ============================================================================ +// Parsed Intent URL +// ============================================================================ + +/** + * Parsed intent URL parameters + */ +export interface ParsedIntentUrl { + /** Client public key (hex) */ + clientId: string; + /** Intent request payload */ + request: IntentRequest; +} + +// ============================================================================ +// Intent Request Events (for wallet UI) +// ============================================================================ + +/** + * Base intent event with common fields + */ +export interface BaseIntentEvent { + /** Unique event ID */ + id: string; + /** Client public key */ + clientId: string; + /** Whether a connect flow should follow */ + hasConnectRequest: boolean; + /** The raw connect request if present */ + connectRequest?: ConnectRequest; +} + +/** + * Transaction/SignMessage intent event for wallet UI + */ +export interface TransactionIntentEvent extends BaseIntentEvent { + /** Intent type */ + type: 'txIntent' | 'signMsg'; + /** Network chain ID */ + network?: string; + /** Valid until timestamp */ + validUntil?: number; + /** Intent items to display */ + items: IntentItem[]; +} + +/** + * Sign data intent event for wallet UI + */ +export interface SignDataIntentEvent extends BaseIntentEvent { + /** Intent type */ + type: 'signIntent'; + /** Network chain ID */ + network?: string; + /** Manifest URL */ + manifestUrl: string; + /** Sign data payload */ + payload: SignDataIntentPayload; +} + +/** + * Action intent event for wallet UI + */ +export interface ActionIntentEvent extends BaseIntentEvent { + /** Intent type */ + type: 'actionIntent'; + /** Action URL */ + actionUrl: string; +} + +/** + * Union type for all intent events + */ +export type IntentEvent = TransactionIntentEvent | SignDataIntentEvent | ActionIntentEvent; + +// ============================================================================ +// Action URL Response Types (for actionIntent) +// ============================================================================ + +/** + * Message structure for sendTransaction action + */ +export interface ActionTransactionMessage { + address: string; + amount: string; + payload?: string; + stateInit?: string; + extra_currency?: Record; +} + +/** + * Send transaction action details + */ +export interface SendTransactionAction { + valid_until?: number; + network?: string; + from?: string; + messages: ActionTransactionMessage[]; +} + +/** + * Sign data action (uses same payload types as signIntent) + */ +export type SignDataAction = SignDataIntentPayload & { + network?: string; + from?: string; +}; + +/** + * Response from action URL + */ +export interface ActionUrlResponse { + action_type: 'sendTransaction' | 'signData'; + action: SendTransactionAction | SignDataAction; +} diff --git a/packages/walletkit/src/types/kit.ts b/packages/walletkit/src/types/kit.ts index 1e77674e7..f43a1c0c1 100644 --- a/packages/walletkit/src/types/kit.ts +++ b/packages/walletkit/src/types/kit.ts @@ -13,6 +13,7 @@ import type { CONNECT_EVENT_ERROR_CODES, SendTransactionRpcResponseError } from import type { JettonsAPI } from './jettons'; import type { ApiClient } from './toncenter/ApiClient'; import type { Wallet, WalletAdapter } from '../api/interfaces'; +import type { IntentEvent, TransactionIntentEvent } from './intents'; import type { Network } from '../api/models/core/Network'; import type { WalletId } from '../utils/walletId'; import type { @@ -79,9 +80,21 @@ export interface ITonWalletKit { /** Handle pasted TON Connect URL/link */ handleTonConnectUrl(url: string): Promise; + /** Handle intent URL (tc://intent_inline?...) */ + handleIntentUrl(url: string): Promise; + + /** Check if URL is an intent URL */ + isIntentUrl(url: string): boolean; + /** Handle new transaction */ handleNewTransaction(wallet: Wallet, data: TransactionRequest): Promise; + /** Convert intent items to transaction request */ + intentItemsToTransactionRequest( + event: TransactionIntentEvent, + wallet: Wallet, + ): Promise; + // === Request Processing === /** Approve a connect request */ @@ -131,6 +144,9 @@ export interface ITonWalletKit { /** Register error handler */ onRequestError(cb: (event: RequestErrorEvent) => void): void; + /** Register intent request handler */ + onIntentRequest(cb: (event: IntentEvent) => void): void; + /** Remove request handlers */ removeConnectRequestCallback(cb: (event: ConnectionRequestEvent) => void): void; removeTransactionRequestCallback(cb: (event: SendTransactionRequestEvent) => void): void; From 362a80ab59e18213a3d46a08c981335e48e76a8e Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Tue, 3 Feb 2026 10:54:23 +0500 Subject: [PATCH 10/19] refactor: fix lint --- .../src/api/intents.ts | 55 ++++++++++------- .../walletkit-android-bridge/src/types/api.ts | 25 +++++++- .../src/types/walletkit.ts | 21 +++---- packages/walletkit/src/core/IntentHandler.ts | 60 +++++++++---------- packages/walletkit/src/core/TonWalletKit.ts | 34 ++--------- packages/walletkit/src/types/intents.ts | 5 +- packages/walletkit/src/types/kit.ts | 5 +- 7 files changed, 102 insertions(+), 103 deletions(-) diff --git a/packages/walletkit-android-bridge/src/api/intents.ts b/packages/walletkit-android-bridge/src/api/intents.ts index f5e0a3d04..7a6cd50cd 100644 --- a/packages/walletkit-android-bridge/src/api/intents.ts +++ b/packages/walletkit-android-bridge/src/api/intents.ts @@ -26,7 +26,9 @@ import type { IntentResponse, SignDataIntentPayload, Wallet, + ConnectionApprovalProof, } from '@ton/walletkit'; +import type { ConnectItem } from '@tonconnect/protocol'; import type { HandleIntentUrlArgs, @@ -118,9 +120,7 @@ export async function approveTransactionIntent( /** * Approve a sign data intent (signIntent) */ -export async function approveSignDataIntent( - args: ApproveSignDataIntentArgs, -): Promise { +export async function approveSignDataIntent(args: ApproveSignDataIntentArgs): Promise { return callBridge('approveSignDataIntent', async (kit) => { // Convert payload format const payload: SignDataIntentPayload = (() => { @@ -130,7 +130,11 @@ export async function approveSignDataIntent( case 'binary': return { type: 'binary' as const, bytes: args.event.payload.bytes! }; case 'cell': - return { type: 'cell' as const, schema: args.event.payload.schema!, cell: args.event.payload.cell! }; + return { + type: 'cell' as const, + schema: args.event.payload.schema!, + cell: args.event.payload.cell!, + }; default: throw new Error(`Unknown payload type: ${args.event.payload.type}`); } @@ -173,18 +177,14 @@ export function rejectIntent(args: RejectIntentArgs): IntentResponseError { * * Fetches action details from the action URL and executes the action. */ -export async function approveActionIntent( - args: ApproveActionIntentArgs, -): Promise { +export async function approveActionIntent(args: ApproveActionIntentArgs): Promise { return callBridge('approveActionIntent', async (kit) => { const event: ActionIntentEvent = { id: args.event.id, clientId: args.event.clientId, hasConnectRequest: args.event.hasConnectRequest, type: 'actionIntent', - network: args.event.network, actionUrl: args.event.actionUrl, - manifestUrl: args.event.manifestUrl, }; if (!kit.approveActionIntent) { @@ -199,28 +199,41 @@ export async function approveActionIntent( * * Creates a proper session for the dApp after intent approval. */ -export async function processConnectAfterIntent( - args: ProcessConnectAfterIntentArgs, -): Promise { +export async function processConnectAfterIntent(args: ProcessConnectAfterIntentArgs): Promise { return callBridge('processConnectAfterIntent', async (kit) => { // Build the IntentEvent from args + // We need to construct a minimal valid IntentEvent - use TransactionIntentEvent as base const event: IntentEvent = { id: args.event.id, clientId: args.event.clientId, hasConnectRequest: args.event.hasConnectRequest, - type: args.event.type, - connectRequest: args.event.connectRequest ? { - manifestUrl: args.event.connectRequest.manifestUrl, - items: args.event.connectRequest.items?.map(item => ({ - name: item.name as 'ton_addr' | 'ton_proof', - payload: item.payload, - })), - } : undefined, + type: args.event.type as 'txIntent', + items: [], // Empty items for processConnectAfterIntent + connectRequest: args.event.connectRequest + ? { + manifestUrl: args.event.connectRequest.manifestUrl, + items: (args.event.connectRequest.items ?? []).map((item) => ({ + name: item.name, + payload: item.payload, + })) as ConnectItem[], + } + : undefined, }; if (!kit.processConnectAfterIntent) { throw new Error('processConnectAfterIntent not available'); } - return await kit.processConnectAfterIntent(event, args.walletId, args.proof); + + // Convert proof to ConnectionApprovalProof type (signature needs to be cast as Base64String) + const proof = args.proof + ? { + signature: args.proof.signature as unknown as ConnectionApprovalProof['signature'], + timestamp: args.proof.timestamp, + domain: args.proof.domain, + payload: args.proof.payload, + } + : undefined; + + return await kit.processConnectAfterIntent(event, args.walletId, proof); }); } diff --git a/packages/walletkit-android-bridge/src/types/api.ts b/packages/walletkit-android-bridge/src/types/api.ts index 5323de498..69d08192b 100644 --- a/packages/walletkit-android-bridge/src/types/api.ts +++ b/packages/walletkit-android-bridge/src/types/api.ts @@ -454,11 +454,32 @@ export interface WalletKitBridgeApi { intentItemsToTransactionRequest(args: IntentItemsToTransactionRequestArgs): PromiseOrValue; approveTransactionIntent(args: ApproveTransactionIntentArgs): PromiseOrValue<{ result: string; id: string }>; approveSignDataIntent(args: ApproveSignDataIntentArgs): PromiseOrValue<{ - result: { signature: string; address: string; timestamp: number; domain: string; payload: { type: string; text?: string; bytes?: string; schema?: string; cell?: string } }; + result: { + signature: string; + address: string; + timestamp: number; + domain: string; + payload: { type: string; text?: string; bytes?: string; schema?: string; cell?: string }; + }; id: string; }>; rejectIntent(args: RejectIntentArgs): PromiseOrValue<{ error: { code: number; message: string }; id: string }>; - approveActionIntent(args: ApproveActionIntentArgs): PromiseOrValue<{ result: unknown; id: string }>; + approveActionIntent( + args: ApproveActionIntentArgs, + ): PromiseOrValue< + | { result: string; id: string } + | { + result: { + signature: string; + address: string; + timestamp: number; + domain: string; + payload: { type: string; text?: string; bytes?: string; schema?: string; cell?: string }; + }; + id: string; + } + | { error: { code: number; message: string }; id: string } + >; processConnectAfterIntent(args: ProcessConnectAfterIntentArgs): PromiseOrValue; createTransferTonTransaction(args: CreateTransferTonTransactionArgs): PromiseOrValue; createTransferMultiTonTransaction(args: CreateTransferMultiTonTransactionArgs): PromiseOrValue; diff --git a/packages/walletkit-android-bridge/src/types/walletkit.ts b/packages/walletkit-android-bridge/src/types/walletkit.ts index 0b956075e..4d50def52 100644 --- a/packages/walletkit-android-bridge/src/types/walletkit.ts +++ b/packages/walletkit-android-bridge/src/types/walletkit.ts @@ -7,14 +7,17 @@ */ import type { + ActionIntentEvent, ApiClient, BridgeEventMessageInfo, + ConnectionApprovalProof, ConnectionApprovalResponse, ConnectionRequestEvent, DeviceInfo, DisconnectionEvent, InjectedToExtensionBridgeRequestPayload, IntentEvent, + IntentResponse, IntentTransactionResponseSuccess, IntentSignDataResponseSuccess, IntentResponseError, @@ -83,23 +86,15 @@ export interface WalletKitInstance { // Intent URL handling isIntentUrl(url: string): boolean; handleIntentUrl(url: string): Promise; - intentItemsToTransactionRequest( - event: TransactionIntentEvent, - wallet: Wallet, - ): Promise; + intentItemsToTransactionRequest(event: TransactionIntentEvent, wallet: Wallet): Promise; approveTransactionIntent?( event: TransactionIntentEvent, walletId: string, ): Promise; - approveSignDataIntent?( - event: SignDataIntentEvent, - walletId: string, - ): Promise; - rejectIntent?( - event: IntentEvent, - reason?: string, - errorCode?: number, - ): IntentResponseError; + approveSignDataIntent?(event: SignDataIntentEvent, walletId: string): Promise; + approveActionIntent?(event: ActionIntentEvent, walletId: string): Promise; + processConnectAfterIntent?(event: IntentEvent, walletId: string, proof?: ConnectionApprovalProof): Promise; + rejectIntent?(event: IntentEvent, reason?: string, errorCode?: number): IntentResponseError; onIntentRequest?(callback: (event: IntentEvent) => void): void; removeIntentRequestCallback?(): void; listSessions?(): Promise; diff --git a/packages/walletkit/src/core/IntentHandler.ts b/packages/walletkit/src/core/IntentHandler.ts index 025b61b70..d1bf1096c 100644 --- a/packages/walletkit/src/core/IntentHandler.ts +++ b/packages/walletkit/src/core/IntentHandler.ts @@ -163,11 +163,7 @@ export class IntentHandler { try { request = JSON.parse(jsonPayload) as IntentRequest; } catch (error) { - throw new WalletKitError( - ERROR_CODES.VALIDATION_ERROR, - 'Invalid JSON in intent payload', - error as Error, - ); + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Invalid JSON in intent payload', error as Error); } // Validate the request @@ -312,10 +308,7 @@ export class IntentHandler { case 'nft': { const nftItem = item as SendNftIntentItem; if (!nftItem.na) { - throw new WalletKitError( - ERROR_CODES.VALIDATION_ERROR, - 'NFT intent item missing NFT address (na)', - ); + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'NFT intent item missing NFT address (na)'); } if (!nftItem.no) { throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'NFT intent item missing new owner (no)'); @@ -329,7 +322,10 @@ export class IntentHandler { // Manifest URL can be in `mu` or `c.manifestUrl` const manifestUrl = request.mu || request.c?.manifestUrl; if (!manifestUrl) { - throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Sign data intent missing manifest URL (mu or c.manifestUrl)'); + throw new WalletKitError( + ERROR_CODES.VALIDATION_ERROR, + 'Sign data intent missing manifest URL (mu or c.manifestUrl)', + ); } if (!request.p) { throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Sign data intent missing payload (p)'); @@ -478,10 +474,7 @@ export class IntentHandler { * @param walletId - The wallet to use for signing * @returns The approval response with signature */ - async approveSignDataIntent( - event: SignDataIntentEvent, - walletId: string, - ): Promise { + async approveSignDataIntent(event: SignDataIntentEvent, walletId: string): Promise { log.info('Approving sign data intent', { id: event.id, walletId }); const wallet = this.walletManager.getWallet(walletId); @@ -506,7 +499,10 @@ export class IntentHandler { signData = { type: 'binary', value: { content: event.payload.bytes as Base64String } }; break; case 'cell': - signData = { type: 'cell', value: { schema: event.payload.schema, content: event.payload.cell as Base64String } }; + signData = { + type: 'cell', + value: { schema: event.payload.schema, content: event.payload.cell as Base64String }, + }; break; } @@ -576,7 +572,7 @@ export class IntentHandler { if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } - actionResponse = await response.json() as ActionUrlResponse; + actionResponse = (await response.json()) as ActionUrlResponse; } catch (error) { throw new WalletKitError( ERROR_CODES.NETWORK_ERROR, @@ -620,7 +616,7 @@ export class IntentHandler { case 'signData': { // Build sign data event from action const signAction = actionResponse.action as SignDataAction; - + // Determine the manifest URL for domain binding: // 1. Use from connectRequest if available // 2. Otherwise use action URL origin as fallback @@ -668,11 +664,7 @@ export class IntentHandler { * @param errorCode - Optional error code (defaults to USER_DECLINED) * @returns The rejection response */ - rejectIntent( - event: IntentEvent, - reason?: string, - errorCode?: IntentErrorCode, - ): IntentResponseError { + rejectIntent(event: IntentEvent, reason?: string, errorCode?: IntentErrorCode): IntentResponseError { log.info('Rejecting intent', { id: event.id, reason }); const response: IntentResponseError = { @@ -719,7 +711,7 @@ export class IntentHandler { // Create ConnectionRequestEvent from the ConnectRequest const connectRequest = event.connectRequest; - + // Build requested items const requestedItems: ConnectionRequestEventRequestedItem[] = []; if (connectRequest.items) { @@ -738,7 +730,7 @@ export class IntentHandler { // Fetch manifest for dApp info let manifest: { name?: string; url?: string; iconUrl?: string; description?: string } | null = null; const manifestUrl = connectRequest.manifestUrl; - + if (manifestUrl) { try { const response = await fetch(manifestUrl); @@ -762,14 +754,14 @@ export class IntentHandler { // Build permissions const permissions: ConnectionRequestEventPreviewPermission[] = []; - if (requestedItems.some(item => item.type === 'ton_addr')) { + if (requestedItems.some((item) => item.type === 'ton_addr')) { permissions.push({ name: 'ton_addr', title: 'TON Address', description: 'Gives dApp information about your TON address', }); } - if (requestedItems.some(item => item.type === 'ton_proof')) { + if (requestedItems.some((item) => item.type === 'ton_proof')) { permissions.push({ name: 'ton_proof', title: 'TON Proof', @@ -860,7 +852,10 @@ export class IntentHandler { case 'nft': return this.nftIntentToMessage(item, walletAddress); default: - throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, `Unknown intent item type: ${(item as IntentItem).t}`); + throw new WalletKitError( + ERROR_CODES.VALIDATION_ERROR, + `Unknown intent item type: ${(item as IntentItem).t}`, + ); } } @@ -888,10 +883,10 @@ export class IntentHandler { ): Promise { const { beginCell, Cell } = await import('@ton/core'); - log.info('jettonIntentToMessage v2 - using Cell.fromBase64', { - hasFp: !!item.fp, + log.info('jettonIntentToMessage v2 - using Cell.fromBase64', { + hasFp: !!item.fp, hasCp: !!item.cp, - fpLength: item.fp?.length ?? 0 + fpLength: item.fp?.length ?? 0, }); // Build jetton transfer body according to TEP-74 @@ -934,7 +929,10 @@ export class IntentHandler { * Convert NFT intent item to message * Builds the NFT transfer message body */ - private async nftIntentToMessage(item: SendNftIntentItem, walletAddress: string): Promise { + private async nftIntentToMessage( + item: SendNftIntentItem, + walletAddress: string, + ): Promise { const { beginCell, Cell } = await import('@ton/core'); // Build NFT transfer body according to TEP-62 diff --git a/packages/walletkit/src/core/TonWalletKit.ts b/packages/walletkit/src/core/TonWalletKit.ts index 6b6b24ec4..4953b90d3 100644 --- a/packages/walletkit/src/core/TonWalletKit.ts +++ b/packages/walletkit/src/core/TonWalletKit.ts @@ -231,11 +231,7 @@ export class TonWalletKit implements ITonWalletKit { this.bridgeManager = components.bridgeManager; // Initialize IntentHandler after we have all components - this.intentHandler = new IntentHandler( - this.walletManager, - this.eventEmitter, - this.requestProcessor, - ); + this.intentHandler = new IntentHandler(this.walletManager, this.eventEmitter, this.requestProcessor); } /** @@ -594,17 +590,9 @@ export class TonWalletKit implements ITonWalletKit { * Convert intent items to a transaction request * Used when approving an intent to build the actual transaction */ - async intentItemsToTransactionRequest( - event: TransactionIntentEvent, - wallet: Wallet, - ): Promise { + async intentItemsToTransactionRequest(event: TransactionIntentEvent, wallet: Wallet): Promise { await this.ensureInitialized(); - return this.intentHandler.intentItemsToTransactionRequest( - event.items, - wallet, - event.network, - event.validUntil, - ); + return this.intentHandler.intentItemsToTransactionRequest(event.items, wallet, event.network, event.validUntil); } /** @@ -634,10 +622,7 @@ export class TonWalletKit implements ITonWalletKit { * @param walletId - The wallet ID to use for signing * @returns The approval response with signature */ - async approveSignDataIntent( - event: SignDataIntentEvent, - walletId: string, - ): Promise { + async approveSignDataIntent(event: SignDataIntentEvent, walletId: string): Promise { await this.ensureInitialized(); return this.intentHandler.approveSignDataIntent(event, walletId); } @@ -651,10 +636,7 @@ export class TonWalletKit implements ITonWalletKit { * @param walletId - The wallet ID to use for signing * @returns The approval response (transaction or sign data) */ - async approveActionIntent( - event: ActionIntentEvent, - walletId: string, - ): Promise { + async approveActionIntent(event: ActionIntentEvent, walletId: string): Promise { await this.ensureInitialized(); return this.intentHandler.approveActionIntent(event, walletId); } @@ -685,11 +667,7 @@ export class TonWalletKit implements ITonWalletKit { * @param errorCode - Optional error code (defaults to USER_DECLINED) * @returns The rejection response */ - rejectIntent( - event: IntentEvent, - reason?: string, - errorCode?: IntentErrorCode, - ): IntentResponseError { + rejectIntent(event: IntentEvent, reason?: string, errorCode?: IntentErrorCode): IntentResponseError { return this.intentHandler.rejectIntent(event, reason, errorCode); } diff --git a/packages/walletkit/src/types/intents.ts b/packages/walletkit/src/types/intents.ts index 2e495062e..ae7c73074 100644 --- a/packages/walletkit/src/types/intents.ts +++ b/packages/walletkit/src/types/intents.ts @@ -255,10 +255,7 @@ export interface IntentSignDataResponseSuccess { /** * Union type for intent responses */ -export type IntentResponse = - | IntentTransactionResponseSuccess - | IntentSignDataResponseSuccess - | IntentResponseError; +export type IntentResponse = IntentTransactionResponseSuccess | IntentSignDataResponseSuccess | IntentResponseError; // ============================================================================ // Parsed Intent URL diff --git a/packages/walletkit/src/types/kit.ts b/packages/walletkit/src/types/kit.ts index f43a1c0c1..556ede445 100644 --- a/packages/walletkit/src/types/kit.ts +++ b/packages/walletkit/src/types/kit.ts @@ -90,10 +90,7 @@ export interface ITonWalletKit { handleNewTransaction(wallet: Wallet, data: TransactionRequest): Promise; /** Convert intent items to transaction request */ - intentItemsToTransactionRequest( - event: TransactionIntentEvent, - wallet: Wallet, - ): Promise; + intentItemsToTransactionRequest(event: TransactionIntentEvent, wallet: Wallet): Promise; // === Request Processing === From d0ae1ac94a4d4f98976c86994a470edbc6b84063 Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Tue, 3 Feb 2026 11:49:22 +0500 Subject: [PATCH 11/19] refactor: fix lint --- demo/v4ledger-adapter/src/WalletV4R2LedgerAdapter.ts | 3 ++- packages/walletkit-android-bridge/src/types/api.ts | 4 +--- packages/walletkit/src/core/IntentHandler.ts | 2 -- packages/walletkit/src/core/TonWalletKit.ts | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/demo/v4ledger-adapter/src/WalletV4R2LedgerAdapter.ts b/demo/v4ledger-adapter/src/WalletV4R2LedgerAdapter.ts index 194a23e0c..874c937dd 100644 --- a/demo/v4ledger-adapter/src/WalletV4R2LedgerAdapter.ts +++ b/demo/v4ledger-adapter/src/WalletV4R2LedgerAdapter.ts @@ -112,7 +112,8 @@ export class WalletV4R2LedgerAdapter implements WalletAdapter { * Get wallet's TON address */ getAddress(options?: { testnet?: boolean }): UserFriendlyAddress { - return formatWalletAddress(this.walletContract.address, options?.testnet); + // Convert to string to avoid Address type version mismatch between @ton/core versions + return formatWalletAddress(this.walletContract.address.toString(), options?.testnet); } getWalletId(): WalletId { diff --git a/packages/walletkit-android-bridge/src/types/api.ts b/packages/walletkit-android-bridge/src/types/api.ts index 69d08192b..f11f7bc83 100644 --- a/packages/walletkit-android-bridge/src/types/api.ts +++ b/packages/walletkit-android-bridge/src/types/api.ts @@ -464,9 +464,7 @@ export interface WalletKitBridgeApi { id: string; }>; rejectIntent(args: RejectIntentArgs): PromiseOrValue<{ error: { code: number; message: string }; id: string }>; - approveActionIntent( - args: ApproveActionIntentArgs, - ): PromiseOrValue< + approveActionIntent(args: ApproveActionIntentArgs): PromiseOrValue< | { result: string; id: string } | { result: { diff --git a/packages/walletkit/src/core/IntentHandler.ts b/packages/walletkit/src/core/IntentHandler.ts index d1bf1096c..27d504b60 100644 --- a/packages/walletkit/src/core/IntentHandler.ts +++ b/packages/walletkit/src/core/IntentHandler.ts @@ -15,7 +15,6 @@ import { Address } from '@ton/core'; import { sha256_sync } from '@ton/crypto'; -import type { ConnectRequest } from '@tonconnect/protocol'; import { globalLogger } from './Logger'; import type { WalletManager } from './WalletManager'; @@ -42,7 +41,6 @@ import type { IntentTransactionResponseSuccess, IntentSignDataResponseSuccess, IntentResponseError, - IntentResponse, SignDataIntentPayload, ActionUrlResponse, SendTransactionAction, diff --git a/packages/walletkit/src/core/TonWalletKit.ts b/packages/walletkit/src/core/TonWalletKit.ts index 4953b90d3..3727aa064 100644 --- a/packages/walletkit/src/core/TonWalletKit.ts +++ b/packages/walletkit/src/core/TonWalletKit.ts @@ -39,7 +39,7 @@ import type { IntentResponseError, IntentResponse, } from '../types/intents'; -import { IntentErrorCode } from './IntentHandler'; +import type { IntentErrorCode } from './IntentHandler'; import type { RawBridgeEventConnect, RawBridgeEventRestoreConnection, From b3fc4ecb8c29e1b8942c0c192a91879ed9519a47 Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Tue, 3 Feb 2026 12:20:47 +0500 Subject: [PATCH 12/19] feat: enhance intent handling with async responses and bridge integration --- .../src/api/intents.ts | 26 ++++++---- .../src/types/walletkit.ts | 2 +- packages/walletkit/src/core/BridgeManager.ts | 43 +++++++++++++++ packages/walletkit/src/core/IntentHandler.ts | 52 ++++++++++++++++++- packages/walletkit/src/core/TonWalletKit.ts | 4 +- 5 files changed, 114 insertions(+), 13 deletions(-) diff --git a/packages/walletkit-android-bridge/src/api/intents.ts b/packages/walletkit-android-bridge/src/api/intents.ts index 7a6cd50cd..5b8b0c5b5 100644 --- a/packages/walletkit-android-bridge/src/api/intents.ts +++ b/packages/walletkit-android-bridge/src/api/intents.ts @@ -160,16 +160,22 @@ export async function approveSignDataIntent(args: ApproveSignDataIntentArgs): Pr /** * Reject an intent request */ -export function rejectIntent(args: RejectIntentArgs): IntentResponseError { - // Note: rejectIntent is synchronous - it just builds the response object - // The actual response sending is done by the wallet via another mechanism - return { - error: { - code: args.errorCode ?? 300, // USER_DECLINED - message: args.reason ?? 'User declined the request', - }, - id: args.event.id, - }; +export async function rejectIntent(args: RejectIntentArgs): Promise { + return callBridge('rejectIntent', async (kit) => { + const event: IntentEvent = args.event; + + if (!kit.rejectIntent) { + // Fallback for older kit versions - just build the response locally + return { + error: { + code: args.errorCode ?? 300, // USER_DECLINED + message: args.reason ?? 'User declined the request', + }, + id: args.event.id, + }; + } + return await kit.rejectIntent(event, args.reason, args.errorCode); + }); } /** diff --git a/packages/walletkit-android-bridge/src/types/walletkit.ts b/packages/walletkit-android-bridge/src/types/walletkit.ts index 4d50def52..bb1086343 100644 --- a/packages/walletkit-android-bridge/src/types/walletkit.ts +++ b/packages/walletkit-android-bridge/src/types/walletkit.ts @@ -94,7 +94,7 @@ export interface WalletKitInstance { approveSignDataIntent?(event: SignDataIntentEvent, walletId: string): Promise; approveActionIntent?(event: ActionIntentEvent, walletId: string): Promise; processConnectAfterIntent?(event: IntentEvent, walletId: string, proof?: ConnectionApprovalProof): Promise; - rejectIntent?(event: IntentEvent, reason?: string, errorCode?: number): IntentResponseError; + rejectIntent?(event: IntentEvent, reason?: string, errorCode?: number): Promise; onIntentRequest?(callback: (event: IntentEvent) => void): void; removeIntentRequestCallback?(): void; listSessions?(): Promise; diff --git a/packages/walletkit/src/core/BridgeManager.ts b/packages/walletkit/src/core/BridgeManager.ts index 7b75e68c8..0633313e4 100644 --- a/packages/walletkit/src/core/BridgeManager.ts +++ b/packages/walletkit/src/core/BridgeManager.ts @@ -259,6 +259,49 @@ export class BridgeManager { } } + /** + * Send an intent response to a dApp's clientId + * Used for TonConnect intents where there's no pre-existing session + * + * @param clientId - The dApp's public key (hex string from intent URL) + * @param response - The response payload to send + * @param sessionCrypto - The wallet's session crypto for encryption + * @param traceId - Optional trace ID for tracking + */ + async sendIntentResponse( + clientId: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + response: any, + sessionCrypto: SessionCrypto, + traceId?: string, + ): Promise { + if (!this.bridgeProvider) { + throw new WalletKitError( + ERROR_CODES.BRIDGE_NOT_INITIALIZED, + 'Bridge not initialized for sending intent response', + ); + } + + try { + await this.bridgeProvider.send(response, sessionCrypto, clientId, { + traceId, + }); + + log.debug('Intent response sent successfully', { clientId: clientId.slice(0, 16) + '...' }); + } catch (error) { + log.error('Failed to send intent response through bridge', { + clientId: clientId.slice(0, 16) + '...', + error, + }); + throw WalletKitError.fromError( + ERROR_CODES.BRIDGE_RESPONSE_SEND_FAILED, + 'Failed to send intent response through bridge', + error, + { clientId }, + ); + } + } + async sendJsBridgeResponse( sessionId: string, _isJsBridge: boolean, diff --git a/packages/walletkit/src/core/IntentHandler.ts b/packages/walletkit/src/core/IntentHandler.ts index 27d504b60..c350fb03d 100644 --- a/packages/walletkit/src/core/IntentHandler.ts +++ b/packages/walletkit/src/core/IntentHandler.ts @@ -15,8 +15,10 @@ import { Address } from '@ton/core'; import { sha256_sync } from '@ton/crypto'; +import { SessionCrypto } from '@tonconnect/protocol'; import { globalLogger } from './Logger'; +import type { BridgeManager } from './BridgeManager'; import type { WalletManager } from './WalletManager'; import type { EventEmitter } from './EventEmitter'; import type { RequestProcessor } from './RequestProcessor'; @@ -82,12 +84,51 @@ export type IntentErrorCode = (typeof INTENT_ERROR_CODES)[keyof typeof INTENT_ER * Handles TonConnect intent deep links */ export class IntentHandler { + private bridgeManager: BridgeManager | null = null; + constructor( private walletManager: WalletManager, private eventEmitter: EventEmitter, private requestProcessor: RequestProcessor, ) {} + /** + * Set the bridge manager reference + * Called after initialization since BridgeManager is created later + */ + setBridgeManager(bridgeManager: BridgeManager): void { + this.bridgeManager = bridgeManager; + } + + /** + * Send an intent response to the dApp through the bridge + * @param clientId - The dApp's public key (from intent URL) + * @param response - The response to send + * @param traceId - Optional trace ID for tracking + */ + private async sendIntentResponse( + clientId: string, + response: IntentTransactionResponseSuccess | IntentSignDataResponseSuccess | IntentResponseError, + traceId?: string, + ): Promise { + if (!this.bridgeManager) { + log.warn('Bridge manager not available, cannot send intent response'); + return; + } + + try { + // Create a new session crypto for this intent response + const sessionCrypto = new SessionCrypto(); + + // Send the response through the bridge to the dApp's clientId + await this.bridgeManager.sendIntentResponse(clientId, response, sessionCrypto, traceId); + log.info('Intent response sent to dApp', { clientId: clientId.slice(0, 16) + '...', responseId: response.id }); + } catch (error) { + log.error('Failed to send intent response', { clientId: clientId.slice(0, 16) + '...', error }); + // Don't throw - the signing was successful, sending is best-effort + } + } + // ======================================================================== // URL Parsing // ======================================================================== @@ -459,6 +500,9 @@ export class IntentHandler { id: event.id, }; + // Send response to dApp through bridge + await this.sendIntentResponse(event.clientId, response); + log.info('Intent approved successfully', { id: event.id, type: event.type }); return response; } @@ -533,6 +577,9 @@ export class IntentHandler { id: event.id, }; + // Send response to dApp through bridge + await this.sendIntentResponse(event.clientId, response); + log.info('Sign data intent approved', { id: event.id }); return response; } @@ -662,7 +709,7 @@ export class IntentHandler { * @param errorCode - Optional error code (defaults to USER_DECLINED) * @returns The rejection response */ - rejectIntent(event: IntentEvent, reason?: string, errorCode?: IntentErrorCode): IntentResponseError { + async rejectIntent(event: IntentEvent, reason?: string, errorCode?: IntentErrorCode): Promise { log.info('Rejecting intent', { id: event.id, reason }); const response: IntentResponseError = { @@ -673,6 +720,9 @@ export class IntentHandler { id: event.id, }; + // Send rejection response to dApp through bridge + await this.sendIntentResponse(event.clientId, response); + return response; } diff --git a/packages/walletkit/src/core/TonWalletKit.ts b/packages/walletkit/src/core/TonWalletKit.ts index 3727aa064..53043659c 100644 --- a/packages/walletkit/src/core/TonWalletKit.ts +++ b/packages/walletkit/src/core/TonWalletKit.ts @@ -232,6 +232,8 @@ export class TonWalletKit implements ITonWalletKit { // Initialize IntentHandler after we have all components this.intentHandler = new IntentHandler(this.walletManager, this.eventEmitter, this.requestProcessor); + // Set bridge manager reference for sending intent responses + this.intentHandler.setBridgeManager(this.bridgeManager); } /** @@ -667,7 +669,7 @@ export class TonWalletKit implements ITonWalletKit { * @param errorCode - Optional error code (defaults to USER_DECLINED) * @returns The rejection response */ - rejectIntent(event: IntentEvent, reason?: string, errorCode?: IntentErrorCode): IntentResponseError { + async rejectIntent(event: IntentEvent, reason?: string, errorCode?: IntentErrorCode): Promise { return this.intentHandler.rejectIntent(event, reason, errorCode); } From fa266bcf97f0d98fffbbcb7fd63d0aa473efddf9 Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Tue, 3 Feb 2026 12:25:26 +0500 Subject: [PATCH 13/19] fix: fix lint --- packages/walletkit-android-bridge/src/api/intents.ts | 6 +++++- packages/walletkit-android-bridge/src/types/walletkit.ts | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/walletkit-android-bridge/src/api/intents.ts b/packages/walletkit-android-bridge/src/api/intents.ts index 5b8b0c5b5..b3fbb5583 100644 --- a/packages/walletkit-android-bridge/src/api/intents.ts +++ b/packages/walletkit-android-bridge/src/api/intents.ts @@ -162,7 +162,11 @@ export async function approveSignDataIntent(args: ApproveSignDataIntentArgs): Pr */ export async function rejectIntent(args: RejectIntentArgs): Promise { return callBridge('rejectIntent', async (kit) => { - const event: IntentEvent = args.event; + // Only need id and clientId for rejection + const event = { + id: args.event.id, + clientId: args.event.clientId, + }; if (!kit.rejectIntent) { // Fallback for older kit versions - just build the response locally diff --git a/packages/walletkit-android-bridge/src/types/walletkit.ts b/packages/walletkit-android-bridge/src/types/walletkit.ts index bb1086343..472f58d6d 100644 --- a/packages/walletkit-android-bridge/src/types/walletkit.ts +++ b/packages/walletkit-android-bridge/src/types/walletkit.ts @@ -94,7 +94,11 @@ export interface WalletKitInstance { approveSignDataIntent?(event: SignDataIntentEvent, walletId: string): Promise; approveActionIntent?(event: ActionIntentEvent, walletId: string): Promise; processConnectAfterIntent?(event: IntentEvent, walletId: string, proof?: ConnectionApprovalProof): Promise; - rejectIntent?(event: IntentEvent, reason?: string, errorCode?: number): Promise; + rejectIntent?( + event: IntentEvent | { id: string; clientId: string }, + reason?: string, + errorCode?: number, + ): Promise; onIntentRequest?(callback: (event: IntentEvent) => void): void; removeIntentRequestCallback?(): void; listSessions?(): Promise; From bc87cf703d71f258bc9bf2b67749d737eb8d17d1 Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Tue, 3 Feb 2026 12:51:04 +0500 Subject: [PATCH 14/19] fix: prettifier --- packages/walletkit/src/core/IntentHandler.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/walletkit/src/core/IntentHandler.ts b/packages/walletkit/src/core/IntentHandler.ts index c350fb03d..6e6fe2a72 100644 --- a/packages/walletkit/src/core/IntentHandler.ts +++ b/packages/walletkit/src/core/IntentHandler.ts @@ -122,7 +122,10 @@ export class IntentHandler { // Send the response through the bridge to the dApp's clientId await this.bridgeManager.sendIntentResponse(clientId, response, sessionCrypto, traceId); - log.info('Intent response sent to dApp', { clientId: clientId.slice(0, 16) + '...', responseId: response.id }); + log.info('Intent response sent to dApp', { + clientId: clientId.slice(0, 16) + '...', + responseId: response.id, + }); } catch (error) { log.error('Failed to send intent response', { clientId: clientId.slice(0, 16) + '...', error }); // Don't throw - the signing was successful, sending is best-effort From 5f339275fca6c17bc62a0415cb00d49d467a14d0 Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Wed, 4 Feb 2026 19:24:46 +0500 Subject: [PATCH 15/19] refactor: simplify intent handling by removing bridge calls and directly using kit methods --- .../src/api/intents.ts | 276 +++++++++--------- .../src/core/wallet/extensions/jetton.ts | 1 + packages/walletkit/src/utils/tvmStack.ts | 6 + 3 files changed, 142 insertions(+), 141 deletions(-) diff --git a/packages/walletkit-android-bridge/src/api/intents.ts b/packages/walletkit-android-bridge/src/api/intents.ts index b3fbb5583..153d35e11 100644 --- a/packages/walletkit-android-bridge/src/api/intents.ts +++ b/packages/walletkit-android-bridge/src/api/intents.ts @@ -40,16 +40,14 @@ import type { ProcessConnectAfterIntentArgs, RejectIntentArgs, } from '../types'; -import type { WalletKitInstance } from '../types/walletkit'; -import { callBridge, callOnWalletBridge } from '../utils/bridgeWrapper'; +import { getKit, getWallet } from '../utils/bridge'; /** * Check if a URL is an intent URL (tc://intent_inline?... or tc://intent?...) */ export async function isIntentUrl(args: IsIntentUrlArgs): Promise { - return callBridge('isIntentUrl', async (kit) => { - return kit.isIntentUrl(args.url); - }); + const kit = await getKit(); + return kit.isIntentUrl(args.url); } /** @@ -57,9 +55,8 @@ export async function isIntentUrl(args: IsIntentUrlArgs): Promise { * Parses the URL and emits an intent event for the wallet UI */ export async function handleIntentUrl(args: HandleIntentUrlArgs): Promise { - return callBridge('handleIntentUrl', async (kit) => { - return await kit.handleIntentUrl(args.url); - }); + const kit = await getKit(); + return await kit.handleIntentUrl(args.url); } /** @@ -69,24 +66,21 @@ export async function handleIntentUrl(args: HandleIntentUrlArgs): Promise export async function intentItemsToTransactionRequest( args: IntentItemsToTransactionRequestArgs, ): Promise { - return callOnWalletBridge( - args.walletId, - 'intentItemsToTransactionRequest', - async (kit: WalletKitInstance, wallet: Wallet) => { - // Convert the simplified event structure to TransactionIntentEvent - const event: TransactionIntentEvent = { - id: args.event.id, - type: args.event.type, - clientId: '', // Not needed for conversion - hasConnectRequest: false, - network: args.event.network, - validUntil: args.event.validUntil, - items: args.event.items as IntentItem[], - }; - - return await kit.intentItemsToTransactionRequest(event, wallet); - }, - ); + const kit = await getKit(); + const wallet = await getWallet(args.walletId); + + // Convert the simplified event structure to TransactionIntentEvent + const event: TransactionIntentEvent = { + id: args.event.id, + type: args.event.type, + clientId: '', // Not needed for conversion + hasConnectRequest: false, + network: args.event.network, + validUntil: args.event.validUntil, + items: args.event.items as IntentItem[], + }; + + return await kit.intentItemsToTransactionRequest(event, wallet); } /** @@ -98,88 +92,88 @@ export async function intentItemsToTransactionRequest( export async function approveTransactionIntent( args: ApproveTransactionIntentArgs, ): Promise { - return callBridge('approveTransactionIntent', async (kit) => { - // Convert the simplified event structure to TransactionIntentEvent - const event: TransactionIntentEvent = { - id: args.event.id, - clientId: args.event.clientId, - hasConnectRequest: args.event.hasConnectRequest, - type: args.event.type, - network: args.event.network, - validUntil: args.event.validUntil, - items: args.event.items as IntentItem[], - }; - - if (!kit.approveTransactionIntent) { - throw new Error('approveTransactionIntent not available'); - } - return await kit.approveTransactionIntent(event, args.walletId); - }); + const kit = await getKit(); + + // Convert the simplified event structure to TransactionIntentEvent + const event: TransactionIntentEvent = { + id: args.event.id, + clientId: args.event.clientId, + hasConnectRequest: args.event.hasConnectRequest, + type: args.event.type, + network: args.event.network, + validUntil: args.event.validUntil, + items: args.event.items as IntentItem[], + }; + + if (!kit.approveTransactionIntent) { + throw new Error('approveTransactionIntent not available'); + } + return await kit.approveTransactionIntent(event, args.walletId); } /** * Approve a sign data intent (signIntent) */ export async function approveSignDataIntent(args: ApproveSignDataIntentArgs): Promise { - return callBridge('approveSignDataIntent', async (kit) => { - // Convert payload format - const payload: SignDataIntentPayload = (() => { - switch (args.event.payload.type) { - case 'text': - return { type: 'text' as const, text: args.event.payload.text! }; - case 'binary': - return { type: 'binary' as const, bytes: args.event.payload.bytes! }; - case 'cell': - return { - type: 'cell' as const, - schema: args.event.payload.schema!, - cell: args.event.payload.cell!, - }; - default: - throw new Error(`Unknown payload type: ${args.event.payload.type}`); - } - })(); - - const event: SignDataIntentEvent = { - id: args.event.id, - clientId: args.event.clientId, - hasConnectRequest: args.event.hasConnectRequest, - type: 'signIntent', - network: args.event.network, - manifestUrl: args.event.manifestUrl, - payload, - }; - - if (!kit.approveSignDataIntent) { - throw new Error('approveSignDataIntent not available'); + const kit = await getKit(); + + // Convert payload format + const payload: SignDataIntentPayload = (() => { + switch (args.event.payload.type) { + case 'text': + return { type: 'text' as const, text: args.event.payload.text! }; + case 'binary': + return { type: 'binary' as const, bytes: args.event.payload.bytes! }; + case 'cell': + return { + type: 'cell' as const, + schema: args.event.payload.schema!, + cell: args.event.payload.cell!, + }; + default: + throw new Error(`Unknown payload type: ${args.event.payload.type}`); } - return await kit.approveSignDataIntent(event, args.walletId); - }); + })(); + + const event: SignDataIntentEvent = { + id: args.event.id, + clientId: args.event.clientId, + hasConnectRequest: args.event.hasConnectRequest, + type: 'signIntent', + network: args.event.network, + manifestUrl: args.event.manifestUrl, + payload, + }; + + if (!kit.approveSignDataIntent) { + throw new Error('approveSignDataIntent not available'); + } + return await kit.approveSignDataIntent(event, args.walletId); } /** * Reject an intent request */ export async function rejectIntent(args: RejectIntentArgs): Promise { - return callBridge('rejectIntent', async (kit) => { - // Only need id and clientId for rejection - const event = { + const kit = await getKit(); + + // Only need id and clientId for rejection + const event = { + id: args.event.id, + clientId: args.event.clientId, + }; + + if (!kit.rejectIntent) { + // Fallback for older kit versions - just build the response locally + return { + error: { + code: args.errorCode ?? 300, // USER_DECLINED + message: args.reason ?? 'User declined the request', + }, id: args.event.id, - clientId: args.event.clientId, }; - - if (!kit.rejectIntent) { - // Fallback for older kit versions - just build the response locally - return { - error: { - code: args.errorCode ?? 300, // USER_DECLINED - message: args.reason ?? 'User declined the request', - }, - id: args.event.id, - }; - } - return await kit.rejectIntent(event, args.reason, args.errorCode); - }); + } + return await kit.rejectIntent(event, args.reason, args.errorCode); } /** @@ -188,20 +182,20 @@ export async function rejectIntent(args: RejectIntentArgs): Promise { - return callBridge('approveActionIntent', async (kit) => { - const event: ActionIntentEvent = { - id: args.event.id, - clientId: args.event.clientId, - hasConnectRequest: args.event.hasConnectRequest, - type: 'actionIntent', - actionUrl: args.event.actionUrl, - }; - - if (!kit.approveActionIntent) { - throw new Error('approveActionIntent not available'); - } - return await kit.approveActionIntent(event, args.walletId); - }); + const kit = await getKit(); + + const event: ActionIntentEvent = { + id: args.event.id, + clientId: args.event.clientId, + hasConnectRequest: args.event.hasConnectRequest, + type: 'actionIntent', + actionUrl: args.event.actionUrl, + }; + + if (!kit.approveActionIntent) { + throw new Error('approveActionIntent not available'); + } + return await kit.approveActionIntent(event, args.walletId); } /** @@ -210,40 +204,40 @@ export async function approveActionIntent(args: ApproveActionIntentArgs): Promis * Creates a proper session for the dApp after intent approval. */ export async function processConnectAfterIntent(args: ProcessConnectAfterIntentArgs): Promise { - return callBridge('processConnectAfterIntent', async (kit) => { - // Build the IntentEvent from args - // We need to construct a minimal valid IntentEvent - use TransactionIntentEvent as base - const event: IntentEvent = { - id: args.event.id, - clientId: args.event.clientId, - hasConnectRequest: args.event.hasConnectRequest, - type: args.event.type as 'txIntent', - items: [], // Empty items for processConnectAfterIntent - connectRequest: args.event.connectRequest - ? { - manifestUrl: args.event.connectRequest.manifestUrl, - items: (args.event.connectRequest.items ?? []).map((item) => ({ - name: item.name, - payload: item.payload, - })) as ConnectItem[], - } - : undefined, - }; - - if (!kit.processConnectAfterIntent) { - throw new Error('processConnectAfterIntent not available'); - } - - // Convert proof to ConnectionApprovalProof type (signature needs to be cast as Base64String) - const proof = args.proof + const kit = await getKit(); + + // Build the IntentEvent from args + // We need to construct a minimal valid IntentEvent - use TransactionIntentEvent as base + const event: IntentEvent = { + id: args.event.id, + clientId: args.event.clientId, + hasConnectRequest: args.event.hasConnectRequest, + type: args.event.type as 'txIntent', + items: [], // Empty items for processConnectAfterIntent + connectRequest: args.event.connectRequest ? { - signature: args.proof.signature as unknown as ConnectionApprovalProof['signature'], - timestamp: args.proof.timestamp, - domain: args.proof.domain, - payload: args.proof.payload, + manifestUrl: args.event.connectRequest.manifestUrl, + items: (args.event.connectRequest.items ?? []).map((item) => ({ + name: item.name, + payload: item.payload, + })) as ConnectItem[], } - : undefined; - - return await kit.processConnectAfterIntent(event, args.walletId, proof); - }); + : undefined, + }; + + if (!kit.processConnectAfterIntent) { + throw new Error('processConnectAfterIntent not available'); + } + + // Convert proof to ConnectionApprovalProof type (signature needs to be cast as Base64String) + const proof = args.proof + ? { + signature: args.proof.signature as unknown as ConnectionApprovalProof['signature'], + timestamp: args.proof.timestamp, + domain: args.proof.domain, + payload: args.proof.payload, + } + : undefined; + + return await kit.processConnectAfterIntent(event, args.walletId, proof); } diff --git a/packages/walletkit/src/core/wallet/extensions/jetton.ts b/packages/walletkit/src/core/wallet/extensions/jetton.ts index b9d956fd7..3c1f9f4fc 100644 --- a/packages/walletkit/src/core/wallet/extensions/jetton.ts +++ b/packages/walletkit/src/core/wallet/extensions/jetton.ts @@ -148,6 +148,7 @@ export class WalletJettonClass implements WalletJettonInterface { ); const parsedStack = ParseStack(result.stack); + // Extract the jetton wallet address from the result if (!parsedStack || parsedStack.length === 0 || !parsedStack[0]) { throw new Error('Empty response from jetton master contract - jetton may not exist'); diff --git a/packages/walletkit/src/utils/tvmStack.ts b/packages/walletkit/src/utils/tvmStack.ts index d480b0c53..015d54487 100644 --- a/packages/walletkit/src/utils/tvmStack.ts +++ b/packages/walletkit/src/utils/tvmStack.ts @@ -24,6 +24,12 @@ function ParseStackItem(item: RawStackItem): TupleItem { return { type: 'null' }; case 'cell': return { type: 'cell', cell: Cell.fromBoc(Buffer.from(item.value, 'base64'))[0] }; + case 'slice': + // Slice is returned as base64 BOC - parse it as a slice + return { type: 'slice', cell: Cell.fromBoc(Buffer.from(item.value, 'base64'))[0] }; + case 'builder': + // Builder is returned as base64 BOC - parse it as a builder + return { type: 'builder', cell: Cell.fromBoc(Buffer.from(item.value, 'base64'))[0] }; case 'tuple': case 'list': if (item.value.length === 0) { From 50cfe5a09e4434f71dbdd84af3e9b3702345cb94 Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Wed, 4 Feb 2026 20:03:36 +0500 Subject: [PATCH 16/19] refactor: refactor intents code to use kitCall --- .../src/api/intents.ts | 168 ++---------------- .../walletkit-android-bridge/src/types/api.ts | 157 +++------------- .../src/utils/bridge.ts | 13 ++ 3 files changed, 51 insertions(+), 287 deletions(-) diff --git a/packages/walletkit-android-bridge/src/api/intents.ts b/packages/walletkit-android-bridge/src/api/intents.ts index 153d35e11..6506f1f46 100644 --- a/packages/walletkit-android-bridge/src/api/intents.ts +++ b/packages/walletkit-android-bridge/src/api/intents.ts @@ -15,20 +15,11 @@ import type { TransactionRequest, - IntentItem, - TransactionIntentEvent, - SignDataIntentEvent, - ActionIntentEvent, - IntentEvent, IntentTransactionResponseSuccess, IntentSignDataResponseSuccess, IntentResponseError, IntentResponse, - SignDataIntentPayload, - Wallet, - ConnectionApprovalProof, } from '@ton/walletkit'; -import type { ConnectItem } from '@tonconnect/protocol'; import type { HandleIntentUrlArgs, @@ -40,24 +31,18 @@ import type { ProcessConnectAfterIntentArgs, RejectIntentArgs, } from '../types'; -import { getKit, getWallet } from '../utils/bridge'; +import { kitCall, getKit, getWallet } from '../utils/bridge'; /** * Check if a URL is an intent URL (tc://intent_inline?... or tc://intent?...) */ -export async function isIntentUrl(args: IsIntentUrlArgs): Promise { - const kit = await getKit(); - return kit.isIntentUrl(args.url); -} +export const isIntentUrl = (args: IsIntentUrlArgs): Promise => kitCall('isIntentUrl', args); /** * Handle an intent URL * Parses the URL and emits an intent event for the wallet UI */ -export async function handleIntentUrl(args: HandleIntentUrlArgs): Promise { - const kit = await getKit(); - return await kit.handleIntentUrl(args.url); -} +export const handleIntentUrl = (args: HandleIntentUrlArgs): Promise => kitCall('handleIntentUrl', args); /** * Convert intent items to a transaction request @@ -68,88 +53,21 @@ export async function intentItemsToTransactionRequest( ): Promise { const kit = await getKit(); const wallet = await getWallet(args.walletId); - - // Convert the simplified event structure to TransactionIntentEvent - const event: TransactionIntentEvent = { - id: args.event.id, - type: args.event.type, - clientId: '', // Not needed for conversion - hasConnectRequest: false, - network: args.event.network, - validUntil: args.event.validUntil, - items: args.event.items as IntentItem[], - }; - - return await kit.intentItemsToTransactionRequest(event, wallet); + return await kit.intentItemsToTransactionRequest(args.event, wallet); } /** * Approve a transaction intent (txIntent or signMsg) - * - * For txIntent: Signs and sends the transaction to the blockchain - * For signMsg: Signs but does NOT send (for gasless transactions) */ -export async function approveTransactionIntent( +export const approveTransactionIntent = ( args: ApproveTransactionIntentArgs, -): Promise { - const kit = await getKit(); - - // Convert the simplified event structure to TransactionIntentEvent - const event: TransactionIntentEvent = { - id: args.event.id, - clientId: args.event.clientId, - hasConnectRequest: args.event.hasConnectRequest, - type: args.event.type, - network: args.event.network, - validUntil: args.event.validUntil, - items: args.event.items as IntentItem[], - }; - - if (!kit.approveTransactionIntent) { - throw new Error('approveTransactionIntent not available'); - } - return await kit.approveTransactionIntent(event, args.walletId); -} +): Promise => kitCall('approveTransactionIntent', args); /** * Approve a sign data intent (signIntent) */ -export async function approveSignDataIntent(args: ApproveSignDataIntentArgs): Promise { - const kit = await getKit(); - - // Convert payload format - const payload: SignDataIntentPayload = (() => { - switch (args.event.payload.type) { - case 'text': - return { type: 'text' as const, text: args.event.payload.text! }; - case 'binary': - return { type: 'binary' as const, bytes: args.event.payload.bytes! }; - case 'cell': - return { - type: 'cell' as const, - schema: args.event.payload.schema!, - cell: args.event.payload.cell!, - }; - default: - throw new Error(`Unknown payload type: ${args.event.payload.type}`); - } - })(); - - const event: SignDataIntentEvent = { - id: args.event.id, - clientId: args.event.clientId, - hasConnectRequest: args.event.hasConnectRequest, - type: 'signIntent', - network: args.event.network, - manifestUrl: args.event.manifestUrl, - payload, - }; - - if (!kit.approveSignDataIntent) { - throw new Error('approveSignDataIntent not available'); - } - return await kit.approveSignDataIntent(event, args.walletId); -} +export const approveSignDataIntent = (args: ApproveSignDataIntentArgs): Promise => + kitCall('approveSignDataIntent', args); /** * Reject an intent request @@ -157,12 +75,6 @@ export async function approveSignDataIntent(args: ApproveSignDataIntentArgs): Pr export async function rejectIntent(args: RejectIntentArgs): Promise { const kit = await getKit(); - // Only need id and clientId for rejection - const event = { - id: args.event.id, - clientId: args.event.clientId, - }; - if (!kit.rejectIntent) { // Fallback for older kit versions - just build the response locally return { @@ -173,71 +85,17 @@ export async function rejectIntent(args: RejectIntentArgs): Promise { - const kit = await getKit(); - - const event: ActionIntentEvent = { - id: args.event.id, - clientId: args.event.clientId, - hasConnectRequest: args.event.hasConnectRequest, - type: 'actionIntent', - actionUrl: args.event.actionUrl, - }; - - if (!kit.approveActionIntent) { - throw new Error('approveActionIntent not available'); - } - return await kit.approveActionIntent(event, args.walletId); -} +export const approveActionIntent = (args: ApproveActionIntentArgs): Promise => + kitCall('approveActionIntent', args); /** * Process connect request after intent approval - * - * Creates a proper session for the dApp after intent approval. */ -export async function processConnectAfterIntent(args: ProcessConnectAfterIntentArgs): Promise { - const kit = await getKit(); - - // Build the IntentEvent from args - // We need to construct a minimal valid IntentEvent - use TransactionIntentEvent as base - const event: IntentEvent = { - id: args.event.id, - clientId: args.event.clientId, - hasConnectRequest: args.event.hasConnectRequest, - type: args.event.type as 'txIntent', - items: [], // Empty items for processConnectAfterIntent - connectRequest: args.event.connectRequest - ? { - manifestUrl: args.event.connectRequest.manifestUrl, - items: (args.event.connectRequest.items ?? []).map((item) => ({ - name: item.name, - payload: item.payload, - })) as ConnectItem[], - } - : undefined, - }; - - if (!kit.processConnectAfterIntent) { - throw new Error('processConnectAfterIntent not available'); - } - - // Convert proof to ConnectionApprovalProof type (signature needs to be cast as Base64String) - const proof = args.proof - ? { - signature: args.proof.signature as unknown as ConnectionApprovalProof['signature'], - timestamp: args.proof.timestamp, - domain: args.proof.domain, - payload: args.proof.payload, - } - : undefined; - - return await kit.processConnectAfterIntent(event, args.walletId, proof); -} +export const processConnectAfterIntent = (args: ProcessConnectAfterIntentArgs): Promise => + kitCall('processConnectAfterIntent', args); diff --git a/packages/walletkit-android-bridge/src/types/api.ts b/packages/walletkit-android-bridge/src/types/api.ts index f11f7bc83..de4819de1 100644 --- a/packages/walletkit-android-bridge/src/types/api.ts +++ b/packages/walletkit-android-bridge/src/types/api.ts @@ -8,19 +8,28 @@ import type { BridgeEvent, + ConnectionApprovalProof, ConnectionRequestEventPreview, ConnectEvent, ConnectEventError, DAppInfo, DisconnectEvent, + IntentEvent, + IntentResponse, + IntentResponseError, + IntentSignDataResponseSuccess, + IntentTransactionResponseSuccess, JettonsResponse, NFT, NFTsResponse, SendTransactionResponse, + SignDataIntentEvent, TONConnectSession, Transaction, TransactionEmulatedPreview, + TransactionIntentEvent, TransactionRequest, + ActionIntentEvent, Wallet, WalletAdapter, WalletResponse, @@ -262,89 +271,24 @@ export interface IsIntentUrlArgs { } export interface IntentItemsToTransactionRequestArgs { - /** The intent event with items */ - event: { - id: string; - type: 'txIntent' | 'signMsg'; - network?: string; - validUntil?: number; - items: Array<{ - t: 'ton' | 'jetton' | 'nft'; - // TON fields - a?: string; - am?: string; - p?: string; - si?: string; - ec?: Record; - // Jetton fields - ma?: string; - qi?: number; - ja?: string; - d?: string; - rd?: string; - cp?: string; - fta?: string; - fp?: string; - // NFT fields - na?: string; - no?: string; - }>; - }; + /** The transaction intent event - Android sends this in walletkit format */ + event: TransactionIntentEvent; /** The wallet ID to use for jetton/NFT address resolution */ walletId: string; } /** Arguments for approving a transaction intent (txIntent or signMsg) */ export interface ApproveTransactionIntentArgs { - /** The full transaction intent event */ - event: { - id: string; - clientId: string; - hasConnectRequest: boolean; - type: 'txIntent' | 'signMsg'; - network?: string; - validUntil?: number; - items: Array<{ - t: 'ton' | 'jetton' | 'nft'; - a?: string; - am?: string; - p?: string; - si?: string; - ec?: Record; - ma?: string; - qi?: number; - ja?: string; - d?: string; - rd?: string; - cp?: string; - fta?: string; - fp?: string; - na?: string; - no?: string; - }>; - }; + /** The full transaction intent event - Android sends this in walletkit format */ + event: TransactionIntentEvent; /** The wallet ID to use for signing */ walletId: string; } /** Arguments for approving a sign data intent (signIntent) */ export interface ApproveSignDataIntentArgs { - /** The full sign data intent event */ - event: { - id: string; - clientId: string; - hasConnectRequest: boolean; - type: 'signIntent'; - network?: string; - manifestUrl: string; - payload: { - type: 'text' | 'binary' | 'cell'; - text?: string; - bytes?: string; - schema?: string; - cell?: string; - }; - }; + /** The full sign data intent event - Android sends this in walletkit format */ + event: SignDataIntentEvent; /** The wallet ID to use for signing */ walletId: string; } @@ -355,7 +299,6 @@ export interface RejectIntentArgs { event: { id: string; clientId: string; - type: 'txIntent' | 'signMsg' | 'signIntent' | 'actionIntent'; }; /** Optional rejection reason */ reason?: string; @@ -365,48 +308,20 @@ export interface RejectIntentArgs { /** Arguments for approving an action intent (actionIntent) */ export interface ApproveActionIntentArgs { - /** The action intent event */ - event: { - id: string; - clientId: string; - hasConnectRequest: boolean; - type: 'actionIntent'; - network?: string; - actionUrl: string; - manifestUrl?: string; - }; + /** The action intent event - Android sends this in walletkit format */ + event: ActionIntentEvent; /** The wallet ID to use for signing */ walletId: string; } /** Arguments for processing connect request after intent approval */ export interface ProcessConnectAfterIntentArgs { - /** The intent event with connect request */ - event: { - id: string; - clientId: string; - hasConnectRequest: boolean; - type: 'txIntent' | 'signMsg' | 'signIntent' | 'actionIntent'; - connectRequest?: { - manifestUrl: string; - items?: Array<{ - name: string; - payload?: string; - }>; - }; - }; + /** The intent event with connect request - Android sends this in walletkit format */ + event: IntentEvent; /** The wallet ID to use for the connection */ walletId: string; - /** Optional proof (signature, timestamp, domain, payload) */ - proof?: { - signature: string; - timestamp: number; - domain: { - lengthBytes: number; - value: string; - }; - payload: string; - }; + /** Optional proof */ + proof?: ConnectionApprovalProof; } export interface WalletDescriptor { @@ -452,32 +367,10 @@ export interface WalletKitBridgeApi { handleIntentUrl(args: HandleIntentUrlArgs): PromiseOrValue; isIntentUrl(args: IsIntentUrlArgs): PromiseOrValue; intentItemsToTransactionRequest(args: IntentItemsToTransactionRequestArgs): PromiseOrValue; - approveTransactionIntent(args: ApproveTransactionIntentArgs): PromiseOrValue<{ result: string; id: string }>; - approveSignDataIntent(args: ApproveSignDataIntentArgs): PromiseOrValue<{ - result: { - signature: string; - address: string; - timestamp: number; - domain: string; - payload: { type: string; text?: string; bytes?: string; schema?: string; cell?: string }; - }; - id: string; - }>; - rejectIntent(args: RejectIntentArgs): PromiseOrValue<{ error: { code: number; message: string }; id: string }>; - approveActionIntent(args: ApproveActionIntentArgs): PromiseOrValue< - | { result: string; id: string } - | { - result: { - signature: string; - address: string; - timestamp: number; - domain: string; - payload: { type: string; text?: string; bytes?: string; schema?: string; cell?: string }; - }; - id: string; - } - | { error: { code: number; message: string }; id: string } - >; + approveTransactionIntent(args: ApproveTransactionIntentArgs): PromiseOrValue; + approveSignDataIntent(args: ApproveSignDataIntentArgs): PromiseOrValue; + rejectIntent(args: RejectIntentArgs): PromiseOrValue; + approveActionIntent(args: ApproveActionIntentArgs): PromiseOrValue; processConnectAfterIntent(args: ProcessConnectAfterIntentArgs): PromiseOrValue; createTransferTonTransaction(args: CreateTransferTonTransactionArgs): PromiseOrValue; createTransferMultiTonTransaction(args: CreateTransferMultiTonTransactionArgs): PromiseOrValue; diff --git a/packages/walletkit-android-bridge/src/utils/bridge.ts b/packages/walletkit-android-bridge/src/utils/bridge.ts index 7cb99cdee..003c78fc7 100644 --- a/packages/walletkit-android-bridge/src/utils/bridge.ts +++ b/packages/walletkit-android-bridge/src/utils/bridge.ts @@ -131,3 +131,16 @@ export async function clientCall( } return (fn as (args?: unknown) => Promise).call(apiClient, args); } + +/** + * Calls a method on the WalletKit instance. + * Passes args directly to the walletkit method. + */ +export async function kitCall(method: string, args: Record): Promise { + const instance = await ensureReady(); + const fn = (instance as unknown as Record)[method]; + if (typeof fn !== 'function') { + throw new Error(`Method '${method}' not found on WalletKit`); + } + return (fn as (args: unknown) => Promise).call(instance, args); +} From b3aadb3374e1430cab7c10bcce384c696d9d2e9c Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Wed, 4 Feb 2026 20:31:09 +0500 Subject: [PATCH 17/19] refactor: update intent handling methods to use args object --- .../src/api/intents.ts | 88 +++-------------- .../src/utils/bridge.ts | 13 --- packages/walletkit/src/core/TonWalletKit.ts | 97 +++++++++++-------- packages/walletkit/src/types/kit.ts | 11 ++- 4 files changed, 77 insertions(+), 132 deletions(-) diff --git a/packages/walletkit-android-bridge/src/api/intents.ts b/packages/walletkit-android-bridge/src/api/intents.ts index 6506f1f46..fda3b23b2 100644 --- a/packages/walletkit-android-bridge/src/api/intents.ts +++ b/packages/walletkit-android-bridge/src/api/intents.ts @@ -13,14 +13,6 @@ * dApps to request actions without a prior TonConnect session. */ -import type { - TransactionRequest, - IntentTransactionResponseSuccess, - IntentSignDataResponseSuccess, - IntentResponseError, - IntentResponse, -} from '@ton/walletkit'; - import type { HandleIntentUrlArgs, IsIntentUrlArgs, @@ -31,71 +23,15 @@ import type { ProcessConnectAfterIntentArgs, RejectIntentArgs, } from '../types'; -import { kitCall, getKit, getWallet } from '../utils/bridge'; - -/** - * Check if a URL is an intent URL (tc://intent_inline?... or tc://intent?...) - */ -export const isIntentUrl = (args: IsIntentUrlArgs): Promise => kitCall('isIntentUrl', args); - -/** - * Handle an intent URL - * Parses the URL and emits an intent event for the wallet UI - */ -export const handleIntentUrl = (args: HandleIntentUrlArgs): Promise => kitCall('handleIntentUrl', args); - -/** - * Convert intent items to a transaction request - * Used when approving an intent to build the actual transaction - */ -export async function intentItemsToTransactionRequest( - args: IntentItemsToTransactionRequestArgs, -): Promise { - const kit = await getKit(); - const wallet = await getWallet(args.walletId); - return await kit.intentItemsToTransactionRequest(args.event, wallet); -} - -/** - * Approve a transaction intent (txIntent or signMsg) - */ -export const approveTransactionIntent = ( - args: ApproveTransactionIntentArgs, -): Promise => kitCall('approveTransactionIntent', args); - -/** - * Approve a sign data intent (signIntent) - */ -export const approveSignDataIntent = (args: ApproveSignDataIntentArgs): Promise => - kitCall('approveSignDataIntent', args); - -/** - * Reject an intent request - */ -export async function rejectIntent(args: RejectIntentArgs): Promise { - const kit = await getKit(); - - if (!kit.rejectIntent) { - // Fallback for older kit versions - just build the response locally - return { - error: { - code: args.errorCode ?? 300, // USER_DECLINED - message: args.reason ?? 'User declined the request', - }, - id: args.event.id, - }; - } - return await kit.rejectIntent(args.event, args.reason, args.errorCode); -} - -/** - * Approve an action intent (actionIntent) - */ -export const approveActionIntent = (args: ApproveActionIntentArgs): Promise => - kitCall('approveActionIntent', args); - -/** - * Process connect request after intent approval - */ -export const processConnectAfterIntent = (args: ProcessConnectAfterIntentArgs): Promise => - kitCall('processConnectAfterIntent', args); +import { kit } from '../utils/bridge'; + +export const isIntentUrl = (args: IsIntentUrlArgs) => kit('isIntentUrl', args); +export const handleIntentUrl = (args: HandleIntentUrlArgs) => kit('handleIntentUrl', args); +export const intentItemsToTransactionRequest = (args: IntentItemsToTransactionRequestArgs) => + kit('intentItemsToTransactionRequest', args); +export const approveTransactionIntent = (args: ApproveTransactionIntentArgs) => kit('approveTransactionIntent', args); +export const approveSignDataIntent = (args: ApproveSignDataIntentArgs) => kit('approveSignDataIntent', args); +export const rejectIntent = (args: RejectIntentArgs) => kit('rejectIntent', args); +export const approveActionIntent = (args: ApproveActionIntentArgs) => kit('approveActionIntent', args); +export const processConnectAfterIntent = (args: ProcessConnectAfterIntentArgs) => + kit('processConnectAfterIntent', args); diff --git a/packages/walletkit-android-bridge/src/utils/bridge.ts b/packages/walletkit-android-bridge/src/utils/bridge.ts index 003c78fc7..7cb99cdee 100644 --- a/packages/walletkit-android-bridge/src/utils/bridge.ts +++ b/packages/walletkit-android-bridge/src/utils/bridge.ts @@ -131,16 +131,3 @@ export async function clientCall( } return (fn as (args?: unknown) => Promise).call(apiClient, args); } - -/** - * Calls a method on the WalletKit instance. - * Passes args directly to the walletkit method. - */ -export async function kitCall(method: string, args: Record): Promise { - const instance = await ensureReady(); - const fn = (instance as unknown as Record)[method]; - if (typeof fn !== 'function') { - throw new Error(`Method '${method}' not found on WalletKit`); - } - return (fn as (args: unknown) => Promise).call(instance, args); -} diff --git a/packages/walletkit/src/core/TonWalletKit.ts b/packages/walletkit/src/core/TonWalletKit.ts index 53043659c..bdb9a856f 100644 --- a/packages/walletkit/src/core/TonWalletKit.ts +++ b/packages/walletkit/src/core/TonWalletKit.ts @@ -520,15 +520,15 @@ export class TonWalletKit implements ITonWalletKit { * Handle pasted TON Connect URL/link * Parses the URL and creates a connect request event */ - async handleTonConnectUrl(url: string): Promise { + async handleTonConnectUrl(args: { url: string }): Promise { await this.ensureInitialized(); try { // Parse and validate the TON Connect URL - const parsedUrl = this.parseTonConnectUrl(url); + const parsedUrl = this.parseTonConnectUrl(args.url); if (!parsedUrl) { throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Invalid TON Connect URL format', undefined, { - url, + url: args.url, }); } @@ -545,7 +545,7 @@ export class TonWalletKit implements ITonWalletKit { await this.eventRouter.routeEvent(bridgeEvent); } catch (error) { - log.error('Failed to handle TON Connect URL', { error, url }); + log.error('Failed to handle TON Connect URL', { error, url: args.url }); throw error; } } @@ -575,26 +575,38 @@ export class TonWalletKit implements ITonWalletKit { /** * Check if a URL is an intent URL (tc://intent_inline?... or tc://intent?...) */ - isIntentUrl(url: string): boolean { - return this.intentHandler?.isIntentUrl(url) ?? false; + isIntentUrl(args: { url: string }): boolean { + return this.intentHandler?.isIntentUrl(args.url) ?? false; } /** * Handle an intent URL * Parses the URL and emits an intent event for the wallet UI */ - async handleIntentUrl(url: string): Promise { + async handleIntentUrl(args: { url: string }): Promise { await this.ensureInitialized(); - return this.intentHandler.handleIntentUrl(url); + return this.intentHandler.handleIntentUrl(args.url); } /** * Convert intent items to a transaction request * Used when approving an intent to build the actual transaction */ - async intentItemsToTransactionRequest(event: TransactionIntentEvent, wallet: Wallet): Promise { + async intentItemsToTransactionRequest(args: { + event: TransactionIntentEvent; + walletId: string; + }): Promise { await this.ensureInitialized(); - return this.intentHandler.intentItemsToTransactionRequest(event.items, wallet, event.network, event.validUntil); + const wallet = this.getWallet(args.walletId); + if (!wallet) { + throw new Error(`Wallet not found: ${args.walletId}`); + } + return this.intentHandler.intentItemsToTransactionRequest( + args.event.items, + wallet, + args.event.network, + args.event.validUntil, + ); } /** @@ -603,16 +615,16 @@ export class TonWalletKit implements ITonWalletKit { * For txIntent: Signs and sends the transaction to the blockchain * For signMsg: Signs but does NOT send (for gasless transactions) * - * @param event - The transaction intent event - * @param walletId - The wallet ID to use for signing + * @param args.event - The transaction intent event + * @param args.walletId - The wallet ID to use for signing * @returns The approval response with signed BoC */ - async approveTransactionIntent( - event: TransactionIntentEvent, - walletId: string, - ): Promise { + async approveTransactionIntent(args: { + event: TransactionIntentEvent; + walletId: string; + }): Promise { await this.ensureInitialized(); - return this.intentHandler.approveTransactionIntent(event, walletId); + return this.intentHandler.approveTransactionIntent(args.event, args.walletId); } /** @@ -620,13 +632,16 @@ export class TonWalletKit implements ITonWalletKit { * * Signs the data and returns the signature. * - * @param event - The sign data intent event - * @param walletId - The wallet ID to use for signing + * @param args.event - The sign data intent event + * @param args.walletId - The wallet ID to use for signing * @returns The approval response with signature */ - async approveSignDataIntent(event: SignDataIntentEvent, walletId: string): Promise { + async approveSignDataIntent(args: { + event: SignDataIntentEvent; + walletId: string; + }): Promise { await this.ensureInitialized(); - return this.intentHandler.approveSignDataIntent(event, walletId); + return this.intentHandler.approveSignDataIntent(args.event, args.walletId); } /** @@ -634,13 +649,13 @@ export class TonWalletKit implements ITonWalletKit { * * Fetches action details from URL and executes the action. * - * @param event - The action intent event - * @param walletId - The wallet ID to use for signing + * @param args.event - The action intent event + * @param args.walletId - The wallet ID to use for signing * @returns The approval response (transaction or sign data) */ - async approveActionIntent(event: ActionIntentEvent, walletId: string): Promise { + async approveActionIntent(args: { event: ActionIntentEvent; walletId: string }): Promise { await this.ensureInitialized(); - return this.intentHandler.approveActionIntent(event, walletId); + return this.intentHandler.approveActionIntent(args.event, args.walletId); } /** @@ -648,29 +663,33 @@ export class TonWalletKit implements ITonWalletKit { * * Creates a proper session for the dApp after intent approval. * - * @param event - The intent event with connect request - * @param walletId - The wallet to use for the connection - * @param proof - Optional proof response + * @param args.event - The intent event with connect request + * @param args.walletId - The wallet to use for the connection + * @param args.proof - Optional proof response */ - async processConnectAfterIntent( - event: IntentEvent, - walletId: string, - proof?: ConnectionApprovalProof, - ): Promise { + async processConnectAfterIntent(args: { + event: IntentEvent; + walletId: string; + proof?: ConnectionApprovalProof; + }): Promise { await this.ensureInitialized(); - return this.intentHandler.processConnectAfterIntent(event, walletId, proof); + return this.intentHandler.processConnectAfterIntent(args.event, args.walletId, args.proof); } /** * Reject an intent request * - * @param event - The intent event to reject - * @param reason - Optional rejection reason - * @param errorCode - Optional error code (defaults to USER_DECLINED) + * @param args.event - The intent event to reject + * @param args.reason - Optional rejection reason + * @param args.errorCode - Optional error code (defaults to USER_DECLINED) * @returns The rejection response */ - async rejectIntent(event: IntentEvent, reason?: string, errorCode?: IntentErrorCode): Promise { - return this.intentHandler.rejectIntent(event, reason, errorCode); + async rejectIntent(args: { + event: IntentEvent; + reason?: string; + errorCode?: IntentErrorCode; + }): Promise { + return this.intentHandler.rejectIntent(args.event, args.reason, args.errorCode); } /** diff --git a/packages/walletkit/src/types/kit.ts b/packages/walletkit/src/types/kit.ts index 556ede445..3cca1485d 100644 --- a/packages/walletkit/src/types/kit.ts +++ b/packages/walletkit/src/types/kit.ts @@ -78,19 +78,22 @@ export interface ITonWalletKit { // === URL Processing === /** Handle pasted TON Connect URL/link */ - handleTonConnectUrl(url: string): Promise; + handleTonConnectUrl(args: { url: string }): Promise; /** Handle intent URL (tc://intent_inline?...) */ - handleIntentUrl(url: string): Promise; + handleIntentUrl(args: { url: string }): Promise; /** Check if URL is an intent URL */ - isIntentUrl(url: string): boolean; + isIntentUrl(args: { url: string }): boolean; /** Handle new transaction */ handleNewTransaction(wallet: Wallet, data: TransactionRequest): Promise; /** Convert intent items to transaction request */ - intentItemsToTransactionRequest(event: TransactionIntentEvent, wallet: Wallet): Promise; + intentItemsToTransactionRequest(args: { + event: TransactionIntentEvent; + walletId: string; + }): Promise; // === Request Processing === From a3689db9a08e3366e80f262deaa7857a6b9ef954 Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Wed, 4 Feb 2026 20:41:54 +0500 Subject: [PATCH 18/19] refactor: fix lint --- packages/walletkit/src/core/TonWalletKit.ts | 8 ++++---- packages/walletkit/src/types/kit.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/walletkit/src/core/TonWalletKit.ts b/packages/walletkit/src/core/TonWalletKit.ts index bdb9a856f..a7ea10925 100644 --- a/packages/walletkit/src/core/TonWalletKit.ts +++ b/packages/walletkit/src/core/TonWalletKit.ts @@ -520,15 +520,15 @@ export class TonWalletKit implements ITonWalletKit { * Handle pasted TON Connect URL/link * Parses the URL and creates a connect request event */ - async handleTonConnectUrl(args: { url: string }): Promise { + async handleTonConnectUrl(url: string): Promise { await this.ensureInitialized(); try { // Parse and validate the TON Connect URL - const parsedUrl = this.parseTonConnectUrl(args.url); + const parsedUrl = this.parseTonConnectUrl(url); if (!parsedUrl) { throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Invalid TON Connect URL format', undefined, { - url: args.url, + url, }); } @@ -545,7 +545,7 @@ export class TonWalletKit implements ITonWalletKit { await this.eventRouter.routeEvent(bridgeEvent); } catch (error) { - log.error('Failed to handle TON Connect URL', { error, url: args.url }); + log.error('Failed to handle TON Connect URL', { error, url }); throw error; } } diff --git a/packages/walletkit/src/types/kit.ts b/packages/walletkit/src/types/kit.ts index 3cca1485d..840f6d863 100644 --- a/packages/walletkit/src/types/kit.ts +++ b/packages/walletkit/src/types/kit.ts @@ -78,7 +78,7 @@ export interface ITonWalletKit { // === URL Processing === /** Handle pasted TON Connect URL/link */ - handleTonConnectUrl(args: { url: string }): Promise; + handleTonConnectUrl(url: string): Promise; /** Handle intent URL (tc://intent_inline?...) */ handleIntentUrl(args: { url: string }): Promise; From 1e7c645ae8da8eb770b10482e641a7fbde0a078e Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Thu, 5 Feb 2026 16:11:01 +0500 Subject: [PATCH 19/19] refactor: update getSignedSendTransaction options to support internal message signing --- .../src/api/interfaces/WalletAdapter.ts | 4 +++- .../src/contracts/v4r2/WalletV4R2Adapter.ts | 5 ++++- .../src/contracts/w5/WalletV5R1Adapter.ts | 17 +++++++++++++---- packages/walletkit/src/core/IntentHandler.ts | 5 ++++- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/walletkit/src/api/interfaces/WalletAdapter.ts b/packages/walletkit/src/api/interfaces/WalletAdapter.ts index 922e84445..f81f4c379 100644 --- a/packages/walletkit/src/api/interfaces/WalletAdapter.ts +++ b/packages/walletkit/src/api/interfaces/WalletAdapter.ts @@ -40,7 +40,9 @@ export interface WalletAdapter { getSignedSendTransaction( input: TransactionRequest, options?: { - fakeSignature: boolean; + fakeSignature?: boolean; + /** Use internal message opcode (0x73696e74) instead of external (0x7369676e) for gasless relaying */ + internal?: boolean; }, ): Promise; getSignedSignData( diff --git a/packages/walletkit/src/contracts/v4r2/WalletV4R2Adapter.ts b/packages/walletkit/src/contracts/v4r2/WalletV4R2Adapter.ts index 03ce31853..7eec71a00 100644 --- a/packages/walletkit/src/contracts/v4r2/WalletV4R2Adapter.ts +++ b/packages/walletkit/src/contracts/v4r2/WalletV4R2Adapter.ts @@ -137,8 +137,11 @@ export class WalletV4R2Adapter implements WalletAdapter { async getSignedSendTransaction( input: TransactionRequest, - _options: { fakeSignature: boolean }, + options?: { fakeSignature?: boolean; internal?: boolean }, ): Promise { + if (options?.internal) { + throw new Error('WalletV4R2 does not support internal message signing (gasless). Use WalletV5R1.'); + } if (input.messages.length === 0) { throw new Error('Ledger does not support empty messages'); } diff --git a/packages/walletkit/src/contracts/w5/WalletV5R1Adapter.ts b/packages/walletkit/src/contracts/w5/WalletV5R1Adapter.ts index f37ba3610..ff48371ea 100644 --- a/packages/walletkit/src/contracts/w5/WalletV5R1Adapter.ts +++ b/packages/walletkit/src/contracts/w5/WalletV5R1Adapter.ts @@ -153,7 +153,7 @@ export class WalletV5R1Adapter implements WalletAdapter { async getSignedSendTransaction( input: TransactionRequest, - options: { fakeSignature: boolean }, + options?: { fakeSignature?: boolean; internal?: boolean }, ): Promise { const actions = packActionsList( input.messages.map((m) => { @@ -201,7 +201,7 @@ export class WalletV5R1Adapter implements WalletAdapter { }), ); - const createBodyOptions: { validUntil: number | undefined; fakeSignature: boolean } = { + const createBodyOptions: { validUntil: number | undefined; fakeSignature?: boolean; internal?: boolean } = { ...options, validUntil: undefined, }; @@ -309,15 +309,24 @@ export class WalletV5R1Adapter implements WalletAdapter { seqno: number, walletId: bigint, actionsList: Cell, - options: { validUntil: number | undefined; fakeSignature: boolean }, + options: { validUntil: number | undefined; fakeSignature?: boolean; internal?: boolean }, ) { const Opcodes = { auth_signed: 0x7369676e, + auth_signed_internal: 0x73696e74, }; + // Use internal opcode for gasless relaying (signMsg intent) + const opcode = options.internal ? Opcodes.auth_signed_internal : Opcodes.auth_signed; + log.debug('createBodyV5 signing with opcode', { + internal: options.internal, + opcode: `0x${opcode.toString(16)}`, + opcodeAscii: Buffer.from(opcode.toString(16), 'hex').toString('ascii'), + }); + const expireAt = options.validUntil ?? Math.floor(Date.now() / 1000) + 300; const payload = beginCell() - .storeUint(Opcodes.auth_signed, 32) + .storeUint(opcode, 32) .storeUint(walletId, 32) .storeUint(expireAt, 32) .storeUint(seqno, 32) // seqno diff --git a/packages/walletkit/src/core/IntentHandler.ts b/packages/walletkit/src/core/IntentHandler.ts index 6e6fe2a72..b3ad57977 100644 --- a/packages/walletkit/src/core/IntentHandler.ts +++ b/packages/walletkit/src/core/IntentHandler.ts @@ -486,7 +486,10 @@ export class IntentHandler { ); // Sign the transaction - const signedBoc = await wallet.getSignedSendTransaction(transactionRequest); + // signMsg uses internal opcode (0x73696e74) for gasless relaying + const signedBoc = await wallet.getSignedSendTransaction(transactionRequest, { + internal: event.type === 'signMsg', + }); // For txIntent, send to blockchain if (event.type === 'txIntent') {