TonWalletKit
A production-ready wallet-side integration layer for TON Connect, designed for building TON wallets at scale

Overview
- 🔗 TON Connect Protocol - Handle connect/disconnect/transaction/sign-data requests
- 💼 Wallet Management - Multi-wallet support with persistent storage
- 🌉 Bridge & JS Bridge - HTTP bridge and browser extension support
- 🎨 Previews for actions - Transaction emulation with money flow analysis
- 🪙 Asset Support - GRAM, Jettons, NFTs with metadata
WalletKit Demo: https://walletkit-demo-wallet.vercel.app/
AppKit Demo: https://appkit-minter.vercel.app/
Documentation

Tutorials
Quick start
This guide shows how to integrate @ton/walletkit into your app with minimal boilerplate. It abstracts TON Connect and wallet implementation details behind a clean API and UI-friendly events.
After you complete this guide, you'll have your wallet fully integrated with the TON ecosystem. You'll be able to interact with dApps, NFTs, and jettons.
npm install @ton/walletkit
Initialize the kit
import {
TonWalletKit,
Signer,
WalletV5R1Adapter,
Network,
MemoryStorageAdapter,
} from '@ton/walletkit';
import { getTonConnectDeviceInfo, getTonConnectWalletManifest } from './wallet-manifest';
const kit = new TonWalletKit({
deviceInfo: getTonConnectDeviceInfo(),
walletManifest: getTonConnectWalletManifest(),
storage: new MemoryStorageAdapter({}),
networks: {
[Network.mainnet().chainId]: {
apiClient: {
key: process.env.APP_TONCENTER_KEY,
url: 'https://toncenter.com',
},
},
},
bridge: {
bridgeUrl: 'https://connect.ton.org/bridge',
},
});
await kit.waitForReady();
const mnemonic = process.env.WALLET_MNEMONIC!.split(' ');
const signer = await Signer.fromMnemonic(mnemonic, { type: 'ton' });
const walletV5R1Adapter = await WalletV5R1Adapter.create(signer, {
client: kit.getApiClient(Network.mainnet()),
network: Network.mainnet(),
});
const walletV5R1 = await kit.addWallet(walletV5R1Adapter);
if (walletV5R1) {
console.log('V5R1 Address:', walletV5R1.getAddress());
console.log('V5R1 Balance:', await walletV5R1.getBalance());
}
Understanding previews (for your UI)
Before handling requests, it's helpful to understand the preview data that the kit provides for each request type. These previews help you display user-friendly confirmation dialogs.
- ConnectPreview (
req.preview): Information about the dApp asking to connect. Includes manifest (name, description, icon), requestedItems, and permissions your UI can show before approval.
- TransactionPreview (
tx.preview): Human-readable transaction summary. On success, preview.moneyFlow.ourTransfers contains an array of net asset changes (GRAM and jettons) with positive amounts for incoming and negative for outgoing. preview.moneyFlow.inputs and preview.moneyFlow.outputs show raw GRAM flow, and preview.emulationResult has low-level emulation details. On error, preview.result === 'error' with an emulationError.
- SignDataPreview (
sd.preview): Shape of the data to sign. kind is 'text' | 'binary' | 'cell'. Use this to render a safe preview.
You can display these previews directly in your confirmation modals.
Listen for requests from dApps
Register callbacks that show UI and then approve or reject via kit methods. Note: getSelectedWalletAddress() is a placeholder for your own wallet selection logic.
kit.onConnectRequest(async (event: ConnectionRequestEvent) => {
try {
const name = event.dAppInfo?.name;
if (yourConfirmLogic(`Connect to ${name}?`)) {
const selectedWalletId = getSelectedWalletId();
const wallet = kit.getWallet(selectedWalletId);
if (!wallet) {
console.error('Selected wallet not found');
await kit.rejectConnectRequest(event, 'No wallet available');
return;
}
console.log(`Using wallet ID: ${wallet.getWalletId()}, address: ${wallet.getAddress()}`);
event.walletId = wallet.getWalletId();
await kit.approveConnectRequest(event);
} else {
await kit.rejectConnectRequest(event, 'User rejected');
}
} catch (error) {
console.error('Connect request failed:', error);
await kit.rejectConnectRequest(event, 'Error processing request');
}
});
kit.onTransactionRequest(async (event: SendTransactionRequestEvent) => {
try {
if (yourConfirmLogic('Do you confirm this transaction?')) {
await kit.approveTransactionRequest(event);
} else {
await kit.rejectTransactionRequest(event, 'User rejected');
}
} catch (error) {
console.error('Transaction request failed:', error);
await kit.rejectTransactionRequest(event, 'Error processing request');
}
});
kit.onSignDataRequest(async (event: SignDataRequestEvent) => {
try {
if (yourConfirmLogic('Sign this data?')) {
await kit.approveSignDataRequest(event);
} else {
await kit.rejectSignDataRequest(event, 'User rejected');
}
} catch (error) {
console.error('Sign data request failed:', error);
await kit.rejectSignDataRequest(event, 'Error processing request');
}
});
kit.onDisconnect((event: DisconnectionEvent) => {
console.log(`Disconnected from wallet: ${event.walletAddress}`);
});
Handle TON Connect links
When users scan a QR code or click a deep link from a dApp, pass the TON Connect URL to the kit. This will trigger your onConnectRequest callback.
async function onTonConnectLink(url: string) {
await kit.handleTonConnectUrl(url);
}
Basic wallet operations
const selectedWalletId = getSelectedWalletId();
const wallet = kit.getWallet(selectedWalletId);
if (!wallet) {
console.error('Selected wallet not found');
return;
}
const balance = await wallet.getBalance();
console.log('WalletBalance', wallet.getAddress(), balance.toString());
Rendering previews (reference)
The snippets below mirror how the demo wallet renders previews in its modals. Adapt them to your UI framework.
Render Connect preview:
function renderConnectPreview(req: ConnectionRequestEvent) {
const name = req.preview.dAppInfo?.name ?? req.dAppInfo?.name;
const description = req.preview.dAppInfo?.description;
const iconUrl = req.preview.dAppInfo?.iconUrl;
const permissions = req.preview.permissions ?? [];
return {
title: `Connect to ${name}?`,
iconUrl,
description,
permissions: permissions.map((p) => ({
title: p.title,
description: p.description,
})),
};
}
Render Transaction preview (money flow overview):
import type { TransactionEmulatedPreview } from '@ton/walletkit';
import { AssetType, Result } from '@ton/walletkit';
function summarizeTransaction(preview: TransactionEmulatedPreview) {
if (preview.result === Result.failure) {
return {
kind: 'error',
message: preview?.error?.message ?? 'Unknown error',
};
}
const transfers = preview.moneyFlow ? preview.moneyFlow.ourTransfers : [];
return {
kind: 'success' as const,
transfers: transfers.map((transfer) => ({
assetType: transfer.assetType,
jettonAddress: transfer.assetType === AssetType.ton ? 'GRAM' : (transfer.tokenAddress ?? ''),
amount: transfer.amount,
isIncoming: BigInt(transfer.amount) >= 0n,
})),
};
}
Example UI rendering:
import type { TransactionTraceMoneyFlowItem } from '@ton/walletkit';
import { AssetType } from '@ton/walletkit';
function renderMoneyFlow(transfers: TransactionTraceMoneyFlowItem[]) {
if (transfers.length === 0) {
return <div>This transaction doesn't involve any token transfers</div>;
}
return transfers.map((transfer: TransactionTraceMoneyFlowItem) => {
const amount = BigInt(transfer.amount);
const isIncoming = amount >= 0n;
const jettonAddress = transfer.assetType === AssetType.ton ? 'GRAM' : (transfer.tokenAddress ?? '');
return (
<div key={jettonAddress}>
<span>
{isIncoming ? '+' : ''}
{transfer.amount}
</span>
<span>{jettonAddress}</span>
</div>
);
});
}
Render Sign-Data preview:
function renderSignDataPreview(preview: SignDataPreview) {
switch (preview.type) {
case 'text':
return { type: 'text', content: preview.value.content };
case 'binary':
return { type: 'binary', content: preview.value.content };
case 'cell':
return {
type: 'cell',
content: preview.value.content,
schema: preview.value.schema,
parsed: preview.value.parsed,
};
}
}
Tip: For jetton names/symbols and images in transaction previews, you can enrich the UI using:
const info = kit.jettons.getJettonInfo(jettonAddress, Network.mainnet());
Sending assets programmatically
You can create transactions from your wallet app (not from dApps) and feed them into the regular approval flow via handleNewTransaction. This triggers your onTransactionRequest callback, allowing the same UI confirmation flow for both dApp and wallet-initiated transactions.
Send GRAM
import type { TONTransferRequest } from '@ton/walletkit';
const from = kit.getWallet(getSelectedWalletAddress());
if (!from) throw new Error('No wallet');
const tonTransfer: TONTransferRequest = {
recipientAddress: 'EQC...recipient...',
transferAmount: (1n * 10n ** 9n).toString(),
comment: 'Thanks!',
};
const tx = await from.createTransferTonTransaction(tonTransfer);
await kit.handleNewTransaction(from, tx);
Send Jettons (fungible tokens)
import type { JettonsTransferRequest } from '@ton/walletkit';
const wallet = kit.getWallet(getSelectedWalletAddress());
if (!wallet) throw new Error('No wallet');
const jettonTransfer: JettonsTransferRequest = {
recipientAddress: 'EQC...recipient...',
jettonAddress: 'EQD...jetton-master...',
transferAmount: '1000000000',
comment: 'Payment',
};
const tx = await wallet.createTransferJettonTransaction(jettonTransfer);
await kit.handleNewTransaction(wallet, tx);
Notes:
amount is the raw integer amount (apply jetton decimals yourself)
- The transaction includes GRAM for gas automatically
Send NFTs
import type { NFTTransferRequest } from '@ton/walletkit';
const wallet = kit.getWallet(getSelectedWalletAddress());
if (!wallet) throw new Error('No wallet');
const nftTransfer: NFTTransferRequest = {
nftAddress: 'EQD...nft-item...',
recipientAddress: 'EQC...recipient...',
transferAmount: '1',
comment: 'Gift',
};
const tx = await wallet.createTransferNftTransaction(nftTransfer);
await kit.handleNewTransaction(wallet, tx);
Fetching NFTs:
const items = await wallet.getNfts({ pagination: { offset: 0, limit: 50 } });
console.log(`✓ Fetched ${items?.nfts?.length ?? 0} NFTs`);
Note: The getNfts method returns NFTsResponse with a nfts field (not items).
Example: minimal UI state wiring
type AppState = {
connectModal?: { request: ConnectionRequestEvent };
txModal?: { request: SendTransactionRequestEvent };
};
const state: AppState = {};
kit.onConnectRequest((req) => {
state.connectModal = { request: req };
});
kit.onTransactionRequest((tx) => {
state.txModal = { request: tx };
});
async function approveConnect() {
if (!state.connectModal) return;
const address = getSelectedWalletAddress();
const wallet = kit.getWallet(address);
if (!wallet) return;
state.connectModal.request.walletAddress = wallet.getAddress();
await kit.approveConnectRequest(state.connectModal.request);
state.connectModal = undefined;
}
async function rejectConnect() {
if (!state.connectModal) return;
await kit.rejectConnectRequest(state.connectModal.request, 'User rejected');
state.connectModal = undefined;
}
async function approveTx() {
if (!state.txModal) return;
await kit.approveTransactionRequest(state.txModal.request);
state.txModal = undefined;
}
async function rejectTx() {
if (!state.txModal) return;
await kit.rejectTransactionRequest(state.txModal.request, 'User rejected');
state.txModal = undefined;
}
Demo wallet reference
Live Demo: https://walletkit-demo-wallet.vercel.app/
The store slices walletCoreSlice.ts and tonConnectSlice.ts show how to:
- Initialize the kit and add a wallet from mnemonic
- Wire
onConnectRequest and onTransactionRequest to open modals
- Approve or reject requests using the kit methods
Resources
License
MIT License - see LICENSE file for details