@@ -382,6 +421,30 @@ 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 13d92b411..d8fa17f91 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,
{
@@ -232,29 +236,17 @@ export function createWalletStore(options: CreateWalletStoreOptions = {}) {
state.clearExpiredRequests();
}
- // Resume processing if there are queued requests
- // if (
- // state.tonConnect.requestQueue.items.length > 0 &&
- // !state.tonConnect.requestQueue.isProcessing &&
- // state.processNextRequest
- // ) {
- // log.info('Resuming queue processing after rehydration');
- // state.processNextRequest();
- // }
-
- const processTimeoutCallback = () => {
+ // Resume processing if there are queued requests after a short delay
+ setTimeout(() => {
if (
state.tonConnect.requestQueue.items.length > 0 &&
!state.tonConnect.requestQueue.isProcessing &&
state.processNextRequest
) {
- log.info('Calling processNextRequest after timeout');
+ log.info('Resuming queue processing after rehydration');
state.processNextRequest();
}
- setTimeout(() => processTimeoutCallback(), 1000);
- };
- processTimeoutCallback();
- // setTimeout(() => {}, 1000);
+ }, 100);
},
},
),
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..e6907ccc8
--- /dev/null
+++ b/demo/wallet-core/src/store/slices/intentSlice.ts
@@ -0,0 +1,266 @@
+/**
+ * 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,
+ IntentTransactionResponse,
+ IntentSignDataResponse,
+} 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');
+ }
+
+ 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) {
+ 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) {
+ 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.approveTransactionDraft(
+ cloneEvent(event) as TransactionIntentRequestEvent,
+ activeWallet.kitWalletId,
+ );
+ break;
+ case 'signData':
+ result = await walletKit.approveSignDataIntent(cloneEvent(event), activeWallet.kitWalletId);
+ break;
+ case 'action':
+ result = await walletKit.approveActionDraft(cloneEvent(event), 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) {
+ 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) {
+ 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) {
+ 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) => {
+ walletKit.onIntentRequest((event) => {
+ log.info('Intent request received:', { type: event.type });
+
+ if (event.type === 'batched') {
+ 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 f2b4e1e42..7d89154d3 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..30f879aa1 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/demo/wallet-core/src/utils/walletManifest.ts b/demo/wallet-core/src/utils/walletManifest.ts
index 92580b82f..2e9c4dc02 100644
--- a/demo/wallet-core/src/utils/walletManifest.ts
+++ b/demo/wallet-core/src/utils/walletManifest.ts
@@ -41,7 +41,6 @@ export function getTonConnectDeviceInfo(): DeviceInfo {
export function getTonConnectFeatures(): Feature[] {
return [
- 'SendTransaction',
{
name: 'SendTransaction',
maxMessages: 4,
@@ -50,5 +49,8 @@ export function getTonConnectFeatures(): Feature[] {
name: 'SignData',
types: ['text', 'binary', 'cell'],
},
- ];
+ { name: 'SendTransactionDraft' },
+ { name: 'SignMessageDraft' },
+ { name: 'SendActionDraft' },
+ ] as Feature[];
}