@@ -382,6 +421,28 @@ export const WalletDashboard: React.FC = () => {
onReject={rejectSignDataRequest}
/>
)}
+
+ {/* Intent Request Modal */}
+ {pendingIntentEvent && (
+
+ )}
+
+ {/* Batched Intent Request Modal */}
+ {pendingBatchedIntentEvent && (
+
+ )}
);
};
diff --git a/demo/wallet-core/src/hooks/useWalletStore.ts b/demo/wallet-core/src/hooks/useWalletStore.ts
index d3d6bd174..724884de8 100644
--- a/demo/wallet-core/src/hooks/useWalletStore.ts
+++ b/demo/wallet-core/src/hooks/useWalletStore.ts
@@ -229,3 +229,27 @@ export const useSwap = () => {
})),
);
};
+
+/**
+ * Hook for Intent state and actions
+ */
+export const useIntents = () => {
+ return useWalletStore(
+ useShallow((state) => ({
+ pendingIntentEvent: state.intent.pendingIntentEvent,
+ pendingBatchedIntentEvent: state.intent.pendingBatchedIntentEvent,
+ isIntentModalOpen: state.intent.isIntentModalOpen,
+ isBatchedIntentModalOpen: state.intent.isBatchedIntentModalOpen,
+ intentResult: state.intent.intentResult,
+ intentError: state.intent.intentError,
+ handleIntentUrl: state.handleIntentUrl,
+ isIntentUrl: state.isIntentUrl,
+ approveIntent: state.approveIntent,
+ rejectIntent: state.rejectIntent,
+ approveBatchedIntent: state.approveBatchedIntent,
+ rejectBatchedIntent: state.rejectBatchedIntent,
+ closeIntentModal: state.closeIntentModal,
+ closeBatchedIntentModal: state.closeBatchedIntentModal,
+ })),
+ );
+};
diff --git a/demo/wallet-core/src/index.ts b/demo/wallet-core/src/index.ts
index 23cc7cd79..541851f40 100644
--- a/demo/wallet-core/src/index.ts
+++ b/demo/wallet-core/src/index.ts
@@ -30,6 +30,7 @@ export {
useNfts,
useJettons,
useSwap,
+ useIntents,
} from './hooks/useWalletStore';
export { useFormattedTonBalance, useFormattedAmount } from './hooks/useFormattedBalance';
export { useWalletInitialization } from './hooks/useWalletInitialization';
@@ -42,6 +43,7 @@ export type {
WalletCoreSlice,
WalletManagementSlice,
TonConnectSlice,
+ IntentSlice,
JettonsSlice,
NftsSlice,
SwapSlice,
diff --git a/demo/wallet-core/src/store/createWalletStore.ts b/demo/wallet-core/src/store/createWalletStore.ts
index 140df9cc1..324bbb5c6 100644
--- a/demo/wallet-core/src/store/createWalletStore.ts
+++ b/demo/wallet-core/src/store/createWalletStore.ts
@@ -17,6 +17,7 @@ import { createTonConnectSlice } from './slices/tonConnectSlice';
import { createJettonsSlice } from './slices/jettonsSlice';
import { createNftsSlice } from './slices/nftsSlice';
import { createSwapSlice } from './slices/swapSlice';
+import { createIntentSlice } from './slices/intentSlice';
import type { AppState } from '../types/store';
import type { StorageAdapter } from '../adapters/storage/types';
import type { WalletKitConfig } from '../types/wallet';
@@ -141,6 +142,9 @@ export function createWalletStore(options: CreateWalletStoreOptions = {}) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
...createSwapSlice(...a),
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ ...createIntentSlice(...a),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
})) as unknown as any,
{
diff --git a/demo/wallet-core/src/store/slices/intentSlice.ts b/demo/wallet-core/src/store/slices/intentSlice.ts
new file mode 100644
index 000000000..d54b883ad
--- /dev/null
+++ b/demo/wallet-core/src/store/slices/intentSlice.ts
@@ -0,0 +1,285 @@
+/**
+ * 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.
+ *
+ */
+
+import type {
+ IntentRequestEvent,
+ BatchedIntentEvent,
+ TransactionIntentRequestEvent,
+ SignDataIntentRequestEvent,
+ ActionIntentRequestEvent,
+ IntentTransactionResponse,
+ IntentSignDataResponse,
+} from '@ton/walletkit';
+import { TonWalletKit } from '@ton/walletkit';
+
+import { createComponentLogger } from '../../utils/logger';
+import type { SetState, IntentSliceCreator } from '../../types/store';
+
+const log = createComponentLogger('IntentSlice');
+
+/**
+ * Deep-clone an object to break Immer frozen state before passing to SDK.
+ * SDK methods may mutate event objects internally.
+ */
+function cloneEvent
(obj: T): T {
+ return structuredClone(obj);
+}
+
+export const createIntentSlice: IntentSliceCreator = (set: SetState, get) => ({
+ intent: {
+ pendingIntentEvent: undefined,
+ pendingBatchedIntentEvent: undefined,
+ isIntentModalOpen: false,
+ isBatchedIntentModalOpen: false,
+ intentResult: undefined,
+ intentError: undefined,
+ },
+
+ // === Intent URL handling ===
+
+ handleIntentUrl: async (url: string) => {
+ const state = get();
+ const walletKit = state.walletCore.walletKit;
+
+ if (!walletKit) {
+ throw new Error('WalletKit not initialized');
+ }
+
+ if (!(walletKit instanceof TonWalletKit)) {
+ throw new Error('Intent API requires TonWalletKit instance');
+ }
+
+ const activeWallet = state.getActiveWallet();
+ if (!activeWallet?.kitWalletId) {
+ throw new Error('No active wallet');
+ }
+
+ try {
+ log.info('Handling intent URL');
+ await walletKit.handleIntentUrl(url, activeWallet.kitWalletId);
+ } catch (error) {
+ log.error('Failed to handle intent URL:', error);
+ throw error;
+ }
+ },
+
+ isIntentUrl: (url: string): boolean => {
+ const state = get();
+ const walletKit = state.walletCore.walletKit;
+ if (!walletKit || !(walletKit instanceof TonWalletKit)) {
+ return false;
+ }
+ return walletKit.isIntentUrl(url);
+ },
+
+ // === Show intent request (called from listener) ===
+
+ showIntentRequest: (event: IntentRequestEvent) => {
+ set((state) => {
+ state.intent.pendingIntentEvent = event;
+ state.intent.isIntentModalOpen = true;
+ state.intent.intentResult = undefined;
+ state.intent.intentError = undefined;
+ });
+ },
+
+ showBatchedIntentRequest: (event: BatchedIntentEvent) => {
+ set((state) => {
+ state.intent.pendingBatchedIntentEvent = event;
+ state.intent.isBatchedIntentModalOpen = true;
+ state.intent.intentResult = undefined;
+ state.intent.intentError = undefined;
+ });
+ },
+
+ // === Approve / Reject ===
+
+ approveIntent: async (): Promise => {
+ const state = get();
+ const walletKit = state.walletCore.walletKit;
+ const event = state.intent.pendingIntentEvent;
+
+ if (!walletKit || !(walletKit instanceof TonWalletKit)) {
+ throw new Error('WalletKit not initialized');
+ }
+ if (!event) {
+ log.error('No pending intent request to approve');
+ return;
+ }
+
+ const activeWallet = state.getActiveWallet();
+ if (!activeWallet?.kitWalletId) {
+ throw new Error('No active wallet');
+ }
+
+ try {
+ let result: IntentTransactionResponse | IntentSignDataResponse;
+
+ switch (event.type) {
+ case 'transaction':
+ result = await walletKit.approveTransactionIntent(
+ cloneEvent(event.value) as TransactionIntentRequestEvent,
+ activeWallet.kitWalletId,
+ );
+ break;
+ case 'signData':
+ result = await walletKit.approveSignDataIntent(
+ cloneEvent(event.value) as SignDataIntentRequestEvent,
+ activeWallet.kitWalletId,
+ );
+ break;
+ case 'action':
+ result = await walletKit.approveActionIntent(
+ cloneEvent(event.value) as ActionIntentRequestEvent,
+ activeWallet.kitWalletId,
+ );
+ break;
+ default:
+ throw new Error(`Unknown intent type: ${(event as IntentRequestEvent).type}`);
+ }
+
+ log.info('Intent approved successfully', { type: event.type });
+
+ set((state) => {
+ state.intent.intentResult = result;
+ state.intent.pendingIntentEvent = undefined;
+ state.intent.isIntentModalOpen = false;
+ });
+ } catch (error) {
+ log.error('Failed to approve intent:', error);
+ set((state) => {
+ state.intent.intentError = error instanceof Error ? error.message : 'Failed to approve intent';
+ });
+ throw error;
+ }
+ },
+
+ rejectIntent: async (reason?: string): Promise => {
+ const state = get();
+ const walletKit = state.walletCore.walletKit;
+ const event = state.intent.pendingIntentEvent;
+
+ if (!walletKit || !(walletKit instanceof TonWalletKit)) {
+ throw new Error('WalletKit not initialized');
+ }
+ if (!event) {
+ log.error('No pending intent request to reject');
+ return;
+ }
+
+ try {
+ await walletKit.rejectIntent(cloneEvent(event), reason || 'User declined');
+ log.info('Intent rejected');
+
+ set((state) => {
+ state.intent.pendingIntentEvent = undefined;
+ state.intent.isIntentModalOpen = false;
+ });
+ } catch (error) {
+ log.error('Failed to reject intent:', error);
+ }
+ },
+
+ approveBatchedIntent: async (): Promise => {
+ const state = get();
+ const walletKit = state.walletCore.walletKit;
+ const batch = state.intent.pendingBatchedIntentEvent;
+
+ if (!walletKit || !(walletKit instanceof TonWalletKit)) {
+ throw new Error('WalletKit not initialized');
+ }
+ if (!batch) {
+ log.error('No pending batched intent to approve');
+ return;
+ }
+
+ const activeWallet = state.getActiveWallet();
+ if (!activeWallet?.kitWalletId) {
+ throw new Error('No active wallet');
+ }
+
+ try {
+ const result = await walletKit.approveBatchedIntent(cloneEvent(batch), activeWallet.kitWalletId);
+ log.info('Batched intent approved successfully');
+
+ set((state) => {
+ state.intent.intentResult = result;
+ state.intent.pendingBatchedIntentEvent = undefined;
+ state.intent.isBatchedIntentModalOpen = false;
+ });
+ } catch (error) {
+ log.error('Failed to approve batched intent:', error);
+ set((state) => {
+ state.intent.intentError = error instanceof Error ? error.message : 'Failed to approve batched intent';
+ });
+ throw error;
+ }
+ },
+
+ rejectBatchedIntent: async (reason?: string): Promise => {
+ const state = get();
+ const walletKit = state.walletCore.walletKit;
+ const batch = state.intent.pendingBatchedIntentEvent;
+
+ if (!walletKit || !(walletKit instanceof TonWalletKit)) {
+ throw new Error('WalletKit not initialized');
+ }
+ if (!batch) {
+ log.error('No pending batched intent to reject');
+ return;
+ }
+
+ try {
+ await walletKit.rejectIntent(cloneEvent(batch), reason || 'User declined');
+ log.info('Batched intent rejected');
+
+ set((state) => {
+ state.intent.pendingBatchedIntentEvent = undefined;
+ state.intent.isBatchedIntentModalOpen = false;
+ });
+ } catch (error) {
+ log.error('Failed to reject batched intent:', error);
+ }
+ },
+
+ closeIntentModal: () => {
+ set((state) => {
+ state.intent.pendingIntentEvent = undefined;
+ state.intent.isIntentModalOpen = false;
+ });
+ },
+
+ closeBatchedIntentModal: () => {
+ set((state) => {
+ state.intent.pendingBatchedIntentEvent = undefined;
+ state.intent.isBatchedIntentModalOpen = false;
+ });
+ },
+
+ // === Setup intent listeners (called from walletCoreSlice) ===
+
+ setupIntentListeners: (walletKit) => {
+ if (!(walletKit instanceof TonWalletKit)) {
+ log.warn('Intent listeners require TonWalletKit instance, skipping');
+ return;
+ }
+
+ walletKit.onIntentRequest((event) => {
+ log.info('Intent request received:', { type: 'type' in event ? event.type : 'batched' });
+
+ // Check if it's a batched event (has `intents` array)
+ if ('intents' in event) {
+ get().showBatchedIntentRequest(event as BatchedIntentEvent);
+ } else {
+ get().showIntentRequest(event as IntentRequestEvent);
+ }
+ });
+
+ log.info('Intent listeners initialized');
+ },
+});
diff --git a/demo/wallet-core/src/store/slices/walletCoreSlice.ts b/demo/wallet-core/src/store/slices/walletCoreSlice.ts
index 2b370f032..6ae747189 100644
--- a/demo/wallet-core/src/store/slices/walletCoreSlice.ts
+++ b/demo/wallet-core/src/store/slices/walletCoreSlice.ts
@@ -109,6 +109,7 @@ export const createWalletCoreSlice =
try {
await walletKit.ensureInitialized();
get().setupTonConnectListeners(walletKit);
+ get().setupIntentListeners(walletKit);
set((state) => {
state.walletCore.walletKit = walletKit;
diff --git a/demo/wallet-core/src/types/store.ts b/demo/wallet-core/src/types/store.ts
index 6450382fd..aaeda0555 100644
--- a/demo/wallet-core/src/types/store.ts
+++ b/demo/wallet-core/src/types/store.ts
@@ -21,6 +21,10 @@ import type {
WalletAdapter,
SwapQuote,
SwapToken,
+ IntentRequestEvent,
+ BatchedIntentEvent,
+ IntentTransactionResponse,
+ IntentSignDataResponse,
} from '@ton/walletkit';
import type {
@@ -233,12 +237,46 @@ export interface SwapSlice {
validateSwapInputs: () => string | null;
}
+// Intent slice - Intent URL handling and approval
+export interface IntentSlice {
+ intent: {
+ pendingIntentEvent?: IntentRequestEvent;
+ pendingBatchedIntentEvent?: BatchedIntentEvent;
+ isIntentModalOpen: boolean;
+ isBatchedIntentModalOpen: boolean;
+ intentResult?: IntentTransactionResponse | IntentSignDataResponse;
+ intentError?: string;
+ };
+
+ // Intent URL handling
+ handleIntentUrl: (url: string) => Promise;
+ isIntentUrl: (url: string) => boolean;
+
+ // Show intent (called from listener)
+ showIntentRequest: (event: IntentRequestEvent) => void;
+ showBatchedIntentRequest: (event: BatchedIntentEvent) => void;
+
+ // Approve / Reject
+ approveIntent: () => Promise;
+ rejectIntent: (reason?: string) => Promise;
+ approveBatchedIntent: () => Promise;
+ rejectBatchedIntent: (reason?: string) => Promise;
+
+ // Modal controls
+ closeIntentModal: () => void;
+ closeBatchedIntentModal: () => void;
+
+ // Setup listeners
+ setupIntentListeners: (walletKit: ITonWalletKit) => void;
+}
+
// Combined app state
export interface AppState
extends AuthSlice,
WalletCoreSlice,
WalletManagementSlice,
TonConnectSlice,
+ IntentSlice,
JettonsSlice,
NftsSlice,
SwapSlice {
@@ -265,6 +303,8 @@ export type NftsSliceCreator = StateCreator;
export type SwapSliceCreator = StateCreator;
+export type IntentSliceCreator = StateCreator;
+
// Migration types
export interface MigrationState {
version: number;
diff --git a/packages/walletkit/src/core/TonWalletKit.ts b/packages/walletkit/src/core/TonWalletKit.ts
index 5184d0d24..13200418f 100644
--- a/packages/walletkit/src/core/TonWalletKit.ts
+++ b/packages/walletkit/src/core/TonWalletKit.ts
@@ -598,8 +598,9 @@ export class TonWalletKit implements ITonWalletKit {
const connectItems = batch.intents.filter((i) => i.type === 'connect');
for (const item of connectItems) {
if (item.type === 'connect') {
- item.value.walletId = walletId;
- await this.requestProcessor.approveConnectRequest(item.value, proof ? { proof } : undefined);
+ // Clone to avoid mutating frozen/immutable event objects (e.g. from Immer state)
+ const connectValue = { ...item.value, walletId };
+ await this.requestProcessor.approveConnectRequest(connectValue, proof ? { proof } : undefined);
}
}