
Security News
The Code You Didn't Write Is Still Yours to Defend
AI agents are pulling packages into environments no scanner is watching, creating exposure before security teams can see it.
@ton/walletkit
Advanced tools
A production-ready wallet-side integration layer for TON Connect, designed for building TON wallets at scale
WalletKit Demo: https://walletkit-demo-wallet.vercel.app/
AppKit Demo: https://appkit-minter.vercel.app/
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
import {
TonWalletKit, // Main SDK class
Signer, // Handles cryptographic signing
WalletV5R1Adapter, // Latest wallet version (recommended)
Network, // Network configuration (mainnet/testnet)
MemoryStorageAdapter,
} from '@ton/walletkit';
import { getTonConnectDeviceInfo, getTonConnectWalletManifest } from './wallet-manifest';
const kit = new TonWalletKit({
deviceInfo: getTonConnectDeviceInfo(),
walletManifest: getTonConnectWalletManifest(),
storage: new MemoryStorageAdapter({}),
// Multi-network API configuration
networks: {
[Network.mainnet().chainId]: {
apiClient: {
// Optional API key for Toncenter get on https://t.me/toncenter
key: process.env.APP_TONCENTER_KEY,
url: 'https://toncenter.com', // default
// or use self-hosted from https://github.com/toncenter/ton-http-api
},
},
// Optionally configure testnet as well
// [CHAIN.TESTNET]: {
// apiClient: {
// key: process.env.APP_TONCENTER_KEY_TESTNET,
// url: 'https://testnet.toncenter.com',
// },
// },
},
bridge: {
// TON Connect bridge for dApp communication
bridgeUrl: 'https://connect.ton.org/bridge',
// or use self-hosted bridge from https://github.com/ton-connect/bridge
},
});
// Wait for initialization to complete
await kit.waitForReady();
// Add a wallet from mnemonic (24-word seed phrase) ton or bip39
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());
}
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.
req.preview): Information about the dApp asking to connect. Includes manifest (name, description, icon), requestedItems, and permissions your UI can show before approval.tx.preview): Human-readable transaction summary. On success, preview.moneyFlow.ourTransfers contains an array of net asset changes (TON and jettons) with positive amounts for incoming and negative for outgoing. preview.moneyFlow.inputs and preview.moneyFlow.outputs show raw TON flow, and preview.emulationResult has low-level emulation details. On error, preview.result === 'error' with an emulationError.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.
Register callbacks that show UI and then approve or reject via kit methods. Note: getSelectedWalletAddress() is a placeholder for your own wallet selection logic.
// Connect requests - triggered when a dApp wants to connect
kit.onConnectRequest(async (event: ConnectionRequestEvent) => {
try {
// Use event.preview to display dApp info in your UI
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()}`);
// Set walletId on the request before approving
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');
}
});
// Transaction requests - triggered when a dApp wants to execute a transaction
kit.onTransactionRequest(async (event: SendTransactionRequestEvent) => {
try {
// Use tx.preview.moneyFlow.ourTransfers to show net asset changes
// Each transfer shows positive amounts for incoming, negative for outgoing
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');
}
});
// Sign data requests - triggered when a dApp wants to sign arbitrary data
kit.onSignDataRequest(async (event: SignDataRequestEvent) => {
try {
// Use event.preview.kind to determine how to display the data
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');
}
});
// Disconnect events - triggered when a dApp disconnects
kit.onDisconnect((event: DisconnectionEvent) => {
// Clean up any UI state related to this connection
console.log(`Disconnected from wallet: ${event.walletAddress}`);
});
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.
// Example: from a QR scanner, deep link, or URL parameter
async function onTonConnectLink(url: string) {
// url format: tc://connect?...
await kit.handleTonConnectUrl(url);
}
const selectedWalletId = getSelectedWalletId();
const wallet = kit.getWallet(selectedWalletId);
if (!wallet) {
console.error('Selected wallet not found');
return;
}
// Query balance
const balance = await wallet.getBalance();
console.log('WalletBalance', wallet.getAddress(), balance.toString());
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',
};
}
// MoneyFlow now provides ourTransfers - a simplified array of net asset changes
const transfers = preview.moneyFlow ? preview.moneyFlow.ourTransfers : []; // Array of TransactionTraceMoneyFlow
// Each transfer has:
// - assetType: 'ton' | 'jetton' | 'nft'
// - amount: string (positive for incoming, negative for outgoing)
// - tokenAddress?: string (jetton master address, if type === 'jetton' or 'nft')
return {
kind: 'success' as const,
transfers: transfers.map((transfer) => ({
assetType: transfer.assetType,
jettonAddress: transfer.assetType === AssetType.ton ? 'TON' : (transfer.tokenAddress ?? ''),
amount: transfer.amount, // string, can be positive or negative
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 ? 'TON' : (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());
// info?.name, info?.symbol, info?.image
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.
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(), // 1 TON in nanotons
// Optional comment OR body (base64 BOC), not both
comment: 'Thanks!',
};
// 1) Build transaction content
const tx = await from.createTransferTonTransaction(tonTransfer);
// 2) Route into the normal flow (triggers onTransactionRequest)
await kit.handleNewTransaction(from, tx);
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', // raw amount per token decimals
comment: 'Payment',
};
const tx = await wallet.createTransferJettonTransaction(jettonTransfer);
await kit.handleNewTransaction(wallet, tx);
Notes:
amount is the raw integer amount (apply jetton decimals yourself)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', // TON used to invoke NFT transfer (nanotons)
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 } });
// items.items is an array of NftItem
console.log(`✓ Fetched ${items?.nfts?.length ?? 0} NFTs`);
Note: The getNfts method returns NFTsResponse with a nfts field (not items).
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;
// Set wallet address on the request
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;
}
Live Demo: https://walletkit-demo-wallet.vercel.app/
The store slices walletCoreSlice.ts and tonConnectSlice.ts show how to:
onConnectRequest and onTransactionRequest to open modalsMIT License - see LICENSE file for details
FAQs
Wallet kit for TON Connect
We found that @ton/walletkit demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 9 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
AI agents are pulling packages into environments no scanner is watching, creating exposure before security teams can see it.

Security News
GitHub Actions checkout now blocks risky pull_request_target checkouts by default to help prevent pwn request supply chain attacks.

Product
Socket now supports Custom Roles and Repository Access Permissions so organizations can control who can access specific repositories and actions.