Author(s): STON.fi Team
This guide was contributed by the STON.fi Team, and has not been validated by Privy. Please review
and test thoroughly before using in production. If you find a bug or problem with this resource,
please let Privy support know and we’ll alert STON.fi Team.
This guide demonstrates how to integrate Privy with the TON blockchain in a Vite + React app to enable wallet login as well as message and transaction signing.
Resources
Creating your Privy app
Installing with Vite
Create a new Vite + React + TypeScript application and install the required dependencies:
npm create vite@latest privy-ton-app --template react-ts
cd privy-ton-app
Install the required packages for TON integration:
npm install @privy-io/react-auth @ton/ton viem
Additionally, install the Node.js polyfills plugin for Vite, which is necessary to provide Buffer and other Node.js APIs in the browser environment (required by TON libraries):
npm install -D vite-plugin-node-polyfills
Next, install Tailwind CSS and its Vite plugin:
npm install -D tailwindcss @tailwindcss/vite
Configure the Vite plugin by updating vite.config.js file:
import {defineConfig} from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
import {nodePolyfills} from 'vite-plugin-node-polyfills';
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
tailwindcss(),
nodePolyfills({
include: ['buffer'],
globals: {
Buffer: true
}
})
]
});
Then, import Tailwind CSS in your main CSS file. Open src/index.css and replace any existing code with:
You can also remove src/App.css (we don’t need it), and remove the import statement import './App.css' from src/App.tsx if it exists.
For simplicity, this guide uses Tailwind CSS for styling the components. The setup for Tailwind
CSS is already included in the instructions above, so you don’t need to set it up separately.
Environment variables
Vite requires environment variables to be prefixed with VITE_:
# .env (or .env.development)
# Get your Privy App ID from https://dashboard.privy.io after creating an app
VITE_PRIVY_APP_ID=your_privy_app_id
# You can get a free API key from https://toncenter.com/
VITE_TON_API_KEY=your_toncenter_api_key
Setting up the app entry point
Set up the main entry point with the Privy provider directly in your main.tsx file:
// src/main.tsx
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import './index.css';
import App from './App.tsx';
import {PrivyProvider} from '@privy-io/react-auth';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<PrivyProvider
appId={import.meta.env.VITE_PRIVY_APP_ID}
config={{
loginMethods: ['email']
}}
>
<App />
</PrivyProvider>
</StrictMode>
);
Using Privy in your app
With Privy integrated, you can authenticate users, generate embedded wallets, and facilitate message and transaction signing.
Log in with Privy
To log in users with Privy, use the useLogin hook:
// src/components/LoginButton.tsx
import {useLogin} from '@privy-io/react-auth';
export function LoginButton() {
const {login} = useLogin({
onComplete: (user) => {
console.log('User logged in:', user);
}
});
return (
<button className="bg-blue-500 text-white px-4 py-2 rounded-md" onClick={login}>
Log in with Privy
</button>
);
}
Creating a TON embedded wallet
First, create a custom hook to access the TON wallet from Privy’s linked accounts:
// src/hooks/useTonWallet.ts
import {usePrivy} from '@privy-io/react-auth';
export function useTonWallet() {
const {user} = usePrivy();
const tonWallet = user?.linkedAccounts?.find(
(account) => account.type === 'wallet' && 'chainType' in account && account.chainType === 'ton'
) as any;
const address = tonWallet?.address;
const walletId = tonWallet?.id; // Wallet ID needed for Privy API calls
return {
tonWallet,
address,
walletId,
exists: !!tonWallet
};
}
Now use the useCreateWallet hook from extended chains to create a wallet:
// src/components/CreateWalletButton.tsx
import {useCreateWallet} from '@privy-io/react-auth/extended-chains';
import {useTonWallet} from '../hooks/useTonWallet';
export function CreateWalletButton() {
const {exists: hasTonWallet} = useTonWallet();
const {createWallet} = useCreateWallet();
const handleCreateWallet = async () => {
try {
const {wallet} = await createWallet({chainType: 'ton'});
console.log('TON wallet created:', wallet.address);
} catch (error) {
console.error('Error creating wallet:', error);
}
};
if (hasTonWallet) {
return <div>TON wallet already created</div>;
}
return (
<button className="bg-blue-500 text-white px-4 py-2 rounded-md" onClick={handleCreateWallet}>
Create TON Wallet
</button>
);
}
Getting wallet balance
To check the balance of a TON wallet using our custom hooks and utilities:
// src/hooks/useTonBalance.ts
import {useTonWallet} from './useTonWallet';
import {Address, fromNano, TonClient} from '@ton/ton';
import {useEffect, useState} from 'react';
export function useTonBalance() {
const {address} = useTonWallet();
const [balance, setBalance] = useState<string>('0');
useEffect(() => {
if (!address) return;
const fetchBalance = async () => {
const client = new TonClient({
endpoint: 'https://toncenter.com/api/v2/jsonRPC',
apiKey: import.meta.env.VITE_TON_API_KEY
});
const nano = await client.getBalance(Address.parse(address));
setBalance(fromNano(nano));
};
fetchBalance();
}, [address]);
return {balance, address};
}
Checking wallet deployment status
TON wallets are smart contracts that need to be deployed before they can be used. Create a hook to check the deployment status:
// src/hooks/useWalletDeployment.ts
import {useEffect, useState} from 'react';
import {TonClient, Address} from '@ton/ton';
export function useWalletDeployment(address: string | undefined) {
const [isDeployed, setIsDeployed] = useState<boolean | null>(null);
useEffect(() => {
if (!address) return;
const checkDeployment = async () => {
try {
const client = new TonClient({
endpoint: 'https://toncenter.com/api/v2/jsonRPC',
apiKey: import.meta.env.VITE_TON_API_KEY
});
const state = await client.getContractState(Address.parse(address));
setIsDeployed(state.state === 'active');
} catch (error) {
console.error('Failed to check wallet deployment:', error);
setIsDeployed(false);
}
};
checkDeployment();
// Check periodically
const interval = setInterval(checkDeployment, 10000); // Every 10 seconds
return () => clearInterval(interval);
}, [address]);
return {isDeployed};
}
Deploying the wallet
Ensure your wallet has sufficient balance (minimum 0.05 TON) before attempting deployment. The
deployment transaction requires gas fees.
// src/components/DeployWalletButton.tsx
import {usePrivy} from '@privy-io/react-auth';
import {useSignRawHash} from '@privy-io/react-auth/extended-chains';
import {toNano, internal, SendMode, TonClient, WalletContractV4} from '@ton/ton';
import {useTonBalance} from '../hooks/useTonBalance';
import {useTonWallet} from '../hooks/useTonWallet';
import {toHex} from 'viem';
export function DeployWalletButton() {
const {getAccessToken} = usePrivy();
const {signRawHash} = useSignRawHash();
const {balance, address} = useTonBalance();
const {walletId} = useTonWallet();
const handleDeploy = async () => {
if (!address || !walletId || parseFloat(balance) < 0.05) return;
// Fetch wallet public key from Privy
const accessToken = await getAccessToken();
const res = await fetch(`https://auth.privy.io/api/v1/wallets/${walletId}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
'privy-app-id': import.meta.env.VITE_PRIVY_APP_ID
}
});
const data = await res.json();
let publicKey = (data.public_key || data.publicKey).replace('0x', '');
// Strip Ed25519 prefix if present
if (publicKey.length === 66 && publicKey.startsWith('00')) {
publicKey = publicKey.slice(2);
}
// Create wallet and client
const wallet = WalletContractV4.create({
workchain: 0,
publicKey: Buffer.from(publicKey, 'hex')
});
const client = new TonClient({
endpoint: 'https://toncenter.com/api/v2/jsonRPC',
apiKey: import.meta.env.VITE_TON_API_KEY
});
const contract = client.open(wallet);
// Create deployment message
const deployMessage = await wallet.createTransfer({
seqno: 0,
messages: [
internal({
value: toNano('0.01'),
to: wallet.address,
body: 'Deploy'
})
],
sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS,
signer: async (msgCell) => {
const {signature} = await signRawHash({
address,
chainType: 'ton' as const,
hash: toHex(msgCell.hash()) as `0x${string}`
});
return Buffer.from(signature.slice(2), 'hex');
}
});
// Send the deployment message
await contract.send(deployMessage);
};
return (
<button
onClick={handleDeploy}
disabled={parseFloat(balance) < 0.05}
className="bg-blue-500 text-white px-4 py-2 rounded-md"
>
Deploy Wallet
</button>
);
}
Signing a message
To sign a message with a TON embedded wallet, we use the browser’s crypto.subtle API to hash the message to 32 bytes (SHA-256), which is then signed via signRawHash:
// src/components/SignMessageButton.tsx
import {useSignRawHash} from '@privy-io/react-auth/extended-chains';
import {useTonWallet} from '../hooks/useTonWallet';
import {toHex} from 'viem';
async function sha256Hex(message: string): Promise<`0x${string}`> {
const data = new TextEncoder().encode(message);
const digest = await crypto.subtle.digest('SHA-256', data);
return toHex(new Uint8Array(digest)) as `0x${string}`;
}
export function SignMessageButton() {
const {signRawHash} = useSignRawHash();
const {address} = useTonWallet();
const handleSignMessage = async () => {
if (!address) return;
try {
const message = 'Hello from Privy!';
const hash = await sha256Hex(message);
const {signature} = await signRawHash({
address,
chainType: 'ton' as const,
hash
});
console.log('Message signature:', signature);
alert('Message signed! Check console for signature.');
} catch (error) {
console.error('Error signing message:', error);
}
};
return <button onClick={handleSignMessage}>Sign Message</button>;
}
Sending a transaction
To send TON from the embedded wallet using browser-compatible methods:
// src/components/SendTransactionButton.tsx
import {usePrivy} from '@privy-io/react-auth';
import {useSignRawHash} from '@privy-io/react-auth/extended-chains';
import {toNano, internal, SendMode, TonClient, WalletContractV4} from '@ton/ton';
import {useTonWallet} from '../hooks/useTonWallet';
import {useState} from 'react';
import {toHex} from 'viem';
export function SendTransactionButton() {
const {getAccessToken} = usePrivy();
const {signRawHash} = useSignRawHash();
const {address, tonWallet, walletId} = useTonWallet();
const [recipientAddress, setRecipientAddress] = useState('');
const [sendAmount, setSendAmount] = useState('0.1');
const [isLoading, setIsLoading] = useState(false);
const handleSendTransaction = async () => {
if (!address || !tonWallet || !walletId || !recipientAddress) {
alert('Please enter a recipient address');
return;
}
try {
setIsLoading(true);
// Get access token and fetch public key from Privy API using wallet ID
const accessToken = await getAccessToken();
const res = await fetch(`https://auth.privy.io/api/v1/wallets/${walletId}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
'privy-app-id': import.meta.env.VITE_PRIVY_APP_ID
}
});
if (!res.ok) throw new Error('Failed to fetch wallet public key');
const data = await res.json();
let publicKey = (data.public_key || data.publicKey).replace('0x', '');
// Strip Ed25519 prefix if present
if (publicKey.length === 66 && publicKey.startsWith('00')) {
publicKey = publicKey.slice(2);
}
const amount = toNano(sendAmount);
// Create wallet contract
const wallet = WalletContractV4.create({
workchain: 0,
publicKey: Buffer.from(publicKey, 'hex')
});
// Create TON client
const tonApiKey = import.meta.env.VITE_TON_API_KEY as string | undefined;
const client = new TonClient({
endpoint: 'https://toncenter.com/api/v2/jsonRPC',
apiKey: tonApiKey
});
const seqno = await client.open(wallet).getSeqno();
const transfer = await wallet.createTransfer({
seqno,
messages: [
internal({
value: amount,
to: recipientAddress,
body: 'Transfer from Privy'
})
],
sendMode: SendMode.PAY_GAS_SEPARATELY,
signer: async (msgCell) => {
const hash = msgCell.hash();
const hashHex = toHex(hash) as `0x${string}`;
const {signature} = await signRawHash({
address,
chainType: 'ton' as const,
hash: hashHex
});
return Buffer.from(signature.slice(2), 'hex');
}
});
await client.open(wallet).send(transfer);
console.log('Transaction sent');
alert('Transaction sent successfully!');
} catch (error) {
console.error('Error sending transaction:', error);
alert('Failed to send transaction');
} finally {
setIsLoading(false);
}
};
return (
<div className="space-y-2">
<input
type="text"
placeholder="Recipient address (EQ...)"
value={recipientAddress}
onChange={(e) => setRecipientAddress(e.target.value)}
className="w-full p-2 border rounded"
/>
<input
type="text"
placeholder="Amount in TON"
value={sendAmount}
onChange={(e) => setSendAmount(e.target.value)}
className="w-full p-2 border rounded"
/>
<button
onClick={handleSendTransaction}
disabled={isLoading || !recipientAddress}
className="w-full bg-orange-500 text-white px-4 py-2 rounded hover:bg-orange-600 disabled:opacity-50"
>
{isLoading ? 'Sending...' : `Send ${sendAmount} TON`}
</button>
</div>
);
}
Wallet deployment status component
Create a component to inform users about wallet deployment:
// src/components/WalletDeployStatus.tsx
import {useWalletDeployment} from '../hooks/useWalletDeployment';
import {useTonBalance} from '../hooks/useTonBalance';
import {DeployWalletButton} from './DeployWalletButton';
export function WalletDeployStatus({address}: {address: string}) {
const {isDeployed} = useWalletDeployment(address);
const {balance} = useTonBalance();
// Don't show anything if wallet is deployed or still checking
if (!address || isDeployed === null || isDeployed) {
return null;
}
const hasSufficientBalance = balance && parseFloat(balance) >= 0.05;
return (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4">
<div className="flex items-start gap-3">
<svg className="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
<div className="flex-1">
<h3 className="text-sm font-medium text-yellow-800">Wallet Not Deployed</h3>
<p className="text-sm text-yellow-700 mt-1">
Your TON wallet needs to be deployed before you can send transactions or make swaps.
</p>
{hasSufficientBalance ? (
<div className="mt-3">
<p className="text-sm font-semibold text-green-700 mb-2">
✓ Your wallet has {balance} TON (minimum 0.05 TON required)
</p>
<DeployWalletButton />
</div>
) : (
<div className="mt-3">
<p className="text-sm font-medium text-yellow-700">To deploy your wallet:</p>
<ol className="list-decimal list-inside text-sm text-yellow-700 mt-1">
<li>Send at least 0.05 TON to your wallet address</li>
<li>Click "Deploy Wallet" once funded</li>
</ol>
<div className="mt-2 p-2 bg-yellow-100 rounded">
<p className="text-xs font-medium text-yellow-800">Your wallet address:</p>
<p className="text-xs font-mono mt-1 break-all">{address}</p>
</div>
</div>
)}
</div>
</div>
</div>
);
}
Complete example container
Here’s the complete application that brings together all the components we’ve created:
// src/components/TonWalletManager.tsx
import {usePrivy} from '@privy-io/react-auth';
import {useTonWallet} from '../hooks/useTonWallet';
import {useTonBalance} from '../hooks/useTonBalance';
import {useWalletDeployment} from '../hooks/useWalletDeployment';
import {LoginButton} from './LoginButton';
import {CreateWalletButton} from './CreateWalletButton';
import {WalletDeployStatus} from './WalletDeployStatus';
import {SignMessageButton} from './SignMessageButton';
import {SendTransactionButton} from './SendTransactionButton';
export function TonWalletManager() {
const {user, logout, authenticated} = usePrivy();
const {address, exists} = useTonWallet();
const {balance} = useTonBalance();
const {isDeployed} = useWalletDeployment(address);
if (!authenticated) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="bg-white p-8 rounded-lg shadow-md text-center">
<h1 className="text-2xl font-bold mb-4">TON Wallet Manager</h1>
<p className="mb-4">Please login to manage your TON wallet</p>
<LoginButton />
</div>
</div>
);
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100 p-8">
<div className="max-w-2xl w-full bg-white rounded-lg shadow-md p-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">TON Wallet Manager</h1>
<button onClick={logout} className="text-red-500 hover:text-red-700">
Logout
</button>
</div>
<div className="mb-4">
<p className="text-gray-600">User: {user?.email?.address}</p>
</div>
{!exists ? (
<div className="mb-6">
<CreateWalletButton />
</div>
) : (
<div className="space-y-4">
{/* Show deployment warning if wallet is not deployed */}
{address && <WalletDeployStatus address={address} />}
<div className="bg-gray-50 p-4 rounded">
<p className="font-semibold">Wallet Address:</p>
<p className="text-sm break-all font-mono">{address}</p>
<p className="mt-2">
<span className="font-semibold">Balance:</span> {balance} TON
</p>
{isDeployed && <p className="text-sm text-green-600 mt-1">✓ Wallet is deployed</p>}
</div>
{/* Only show transaction functions if wallet is deployed */}
{isDeployed && (
<>
<div className="flex gap-2">
<SignMessageButton />
</div>
<div className="border-t pt-4">
<h3 className="font-semibold mb-2">Send TON</h3>
<SendTransactionButton />
</div>
</>
)}
</div>
)}
</div>
</div>
);
}
Creating the main App component
The App.tsx component will contain your main application logic:
// src/App.tsx
import {TonWalletManager} from './components/TonWalletManager';
export default function App() {
return <TonWalletManager />;
}
Run
Open the printed local URL (typically http://localhost:5173).
Summary
-
Configure env
- In
.env (or .env.development), set:
VITE_PRIVY_APP_ID=<your app id>
VITE_TON_API_KEY=<optional toncenter api key>
-
Start the app
- Run
npm run dev and open the local URL.
-
Log in
- Click Log in with Privy (email auth).
-
Create a TON wallet (once)
- If you don’t have one yet, click Create TON Wallet.
-
Fund the wallet
- Send ≥ 0.05 TON to the wallet address shown in the UI.
-
Deploy the wallet
- Click Deploy Wallet once funded.
- Wait until you see ✓ Wallet is deployed.
-
Sign a message
- Click Sign Message to sign an example message (check the console for the signature).
-
Send TON
- Enter Recipient address (EQ…) and Amount in TON.
- Click Send … TON and wait for the success notice.