Skip to main content

TON Connect for React

The UI React SDK is the recommended option for React apps. It provides a high-level React component to interact with TON Connect.

Implementation

Installation

To start integrating TON Connect into your DApp, you need to install the @tonconnect/ui-react package:

npm i @tonconnect/ui-react

Set up TON Connect

After installing the package, you should create a manifest.json file for your application. More information on creating a manifest.json file can be found here.

After creating the manifest file, import TonConnectUIProvider into the root of your Mini App and pass the manifest URL:

import { TonConnectUIProvider } from '@tonconnect/ui-react';

export function App() {
return (
<TonConnectUIProvider manifestUrl="https://<YOUR_APP_URL>/tonconnect-manifest.json">
{ /* Your app */ }
</TonConnectUIProvider>
);
}

Connect to the wallet

Add the TonConnectButton.

import { TonConnectButton } from '@tonconnect/ui-react';

export const Header = () => {
return (
<header>
<span>My App with React UI</span>
<TonConnectButton />
</header>
);
};

The TonConnectButton is a universal UI component for initializing a connection. After the wallet is connected, it transforms into a wallet menu. Place the Connect button in the top right corner of your app.

You can add the className and style props to the button:

<TonConnectButton className="my-button-class" style={{ float: "right" }}/>

Note that you cannot pass children to the TonConnectButton.

You can always initiate the connection manually using the useTonConnectUI hook and openModal method.

import { useTonConnectUI } from '@tonconnect/ui-react';

export const Header = () => {
const [tonConnectUI, setOptions] = useTonConnectUI();
return (
<header>
<span>My App with React UI</span>
<button onClick={() => tonConnectUI.openModal()}>
Connect Wallet
</button>
</header>
);
};

Connect with a specific wallet

To open a modal window for a specific wallet, use the openSingleWalletModal() method. This method takes the wallet's app_name as a parameter (refer to the wallets-list.json file) and opens the corresponding wallet modal. It returns a promise that resolves once the modal window opens successfully.

<button onClick={() => tonConnectUI.openSingleWalletModal('tonwallet')}>
Connect Wallet
</button>

Redirects

If you want to redirect the user to a specific page after wallet connection, you can use the useTonConnectUI hook and customize your return strategy.

Telegram Mini Apps

If you want to redirect the user to a Telegram Mini App after wallet connection, you can customize the TonConnectUIProvider element:

      <TonConnectUIProvider
// ... other parameters
actionsConfiguration={{
twaReturnUrl: 'https://t.me/<YOUR_APP_NAME>'
}}
>
</TonConnectUIProvider>

Open example on GitHub

UI customization

To customize the UI of the modal, you can use the useTonConnectUI hook and the setOptions function. See more about the useTonConnectUI hook in the Hooks section.

Hooks

If you want to use some low-level TON Connect UI SDK features in your React app, you can use hooks from the @tonconnect/ui-react package.

useTonAddress

Use it to get the user's current TON wallet address. Pass the boolean parameter isUserFriendly to choose the address format, where the isUserFriendly is true by default. The hook will return an empty string if the wallet is not connected.

import { useTonAddress } from '@tonconnect/ui-react';

export const Address = () => {
const userFriendlyAddress = useTonAddress();
const rawAddress = useTonAddress(false);

return (
userFriendlyAddress && (
<div>
<span>User-friendly address: {userFriendlyAddress}</span>
<span>Raw address: {rawAddress}</span>
</div>
)
);
};

useTonConnectModal

Use this hook to access the functions for opening and closing the modal window. The hook returns an object with the current modal state and methods to open and close the modal.

import { useTonConnectModal } from '@tonconnect/ui-react';

export const ModalControl = () => {
const { state, open, close } = useTonConnectModal();

return (
<div>
<div>Modal state: {state?.status}</div>
<button onClick={open}>Open modal</button>
<button onClick={close}>Close modal</button>
</div>
);
};

useTonWallet

Use this hook to retrieve the user's current TON wallet. The hook will return null if the wallet is not connected. The wallet object provides common data such as the user's address, provider, TON proof, and other attributes (see the Wallet interface).

Additionally, you can access more specific details about the connected wallet, such as its name, image, and other attributes (refer to the WalletInfo interface).

import { useTonWallet } from '@tonconnect/ui-react';

export const Wallet = () => {
const wallet = useTonWallet();

return (
wallet && (
<div>
<span>Connected wallet address: {wallet.account.address}</span>
<span>Device: {wallet.device.appName}</span>
<span>Connected via: {wallet.provider}</span>
{wallet.connectItems?.tonProof?.proof && <span>TON proof: {wallet.connectItems.tonProof.proof}</span>}

<div>Connected wallet info:</div>
<div>
{wallet.name} <img src={wallet.imageUrl} />
</div>
</div>
)
);
};

useTonConnectUI

Access the TonConnectUI instance and update the UI options function.

See more about TonConnectUI instance methods

See more about the setOptions function

import { Locales, useTonConnectUI } from '@tonconnect/ui-react';

export const Settings = () => {
const [tonConnectUI, setOptions] = useTonConnectUI();

const onLanguageChange = (language: Locales) => {
setOptions({ language });
};

return (
<div>
<label>language</label>
<select onChange={(e) => onLanguageChange(e.target.value as Locales)}>
<option value="en">en</option>
<option value="ru">ru</option>
</select>
</div>
);
};

useIsConnectionRestored

useIsConnectionRestored indicates the current status of the connection restoring process. You can use it to detect when the connection restoring process is finished.

import { useIsConnectionRestored } from '@tonconnect/ui-react';

export const EntrypointPage = () => {
const connectionRestored = useIsConnectionRestored();

if (!connectionRestored) {
return <Loader>Please wait...</Loader>;
}

return <MainPage />;
};

Usage

Let's look at how to use the React UI SDK in practice.

Sending transactions

Send Toncoin to a specific address:

import { useTonConnectUI } from '@tonconnect/ui-react';

const transaction: SendTransactionRequest = {
validUntil: Date.now() + 5 * 60 * 1000, // 5 minutes
messages: [
{
address:
"0QD-SuoCHsCL2pIZfE8IAKsjc0aDpDUQAoo-ALHl2mje04A-", // message destination in user-friendly format
amount: "20000000", // Toncoin in nanotons
},
],
};

export const Settings = () => {
const [tonConnectUI, setOptions] = useTonConnectUI();

return (
<div>
<button onClick={() => tonConnectUI.sendTransaction(transaction)}>
Send transaction
</button>
</div>
);
};

Understanding transaction status by hash

Check out the Transaction lookup page for more details.

Optional check on the backend: ton_proof

tip

Understand how to sign and verify messages: Signing and verification

To ensure that the user truly owns the declared address, you can use ton_proof. This feature is particularly useful for verifying ownership of off-chain assets such as Telegram usernames or virtual phone numbers on platforms like Fragment.

Use the tonConnectUI.setConnectRequestParameters function to set up your connection request parameters. You can use it for:

  • Loading state: Show a loading state while waiting for a response from your backend.
  • Ready state with ton_proof: Set the state to 'ready' and include the ton_proof value.
  • If an error occurs, remove the loader and create the connect request without additional parameters.
const [tonConnectUI] = useTonConnectUI();

// Set loading state
tonConnectUI.setConnectRequestParameters({ state: "loading" });

// Fetch tonProofPayload from the backend
const tonProofPayload: string | null =
await fetchTonProofPayloadFromBackend();

if (tonProofPayload) {
// Set ready state with tonProof
tonConnectUI.setConnectRequestParameters({
state: "ready",
value: { tonProof: tonProofPayload },
});
} else {
// Remove loader
tonConnectUI.setConnectRequestParameters(null);
}

Handling ton_proof result

You can find the ton_proof result in the wallet object when the wallet is connected:

useEffect(() => {
tonConnectUI.onStatusChange((wallet) => {
if (
wallet.connectItems?.tonProof &&
"proof" in wallet.connectItems.tonProof
) {
checkProofInYourBackend(
wallet.connectItems.tonProof.proof,
wallet.account.address
);
}
});
}, [tonConnectUI]);

Structure of ton_proof

type TonProofItemReplySuccess = {
name: "ton_proof";
proof: {
timestamp: string; // Unix epoch time (seconds)
domain: {
lengthBytes: number; // Domain length
value: string; // Domain name
};
signature: string; // Base64-encoded signature
payload: string; // Payload from the request
}
}
How to verify the ton_proof signature by public key

The signature must be verified by public key:

  1. First, try to extract the public key from walletStateInit:

    • Use TonAddressItemReply.walletStateInit and attempt to parse as StateInit cell.
    • Compare walletStateInit.code with the code of known standard wallet contracts. If a match is found, parse the walletStateInit.data according to the wallet version.
    • Take publicKey from parsed walletStateInit.data
  2. If publicKey cannot be taken:

    • Call the on-chain get_public_key get-method on the smart contract deployed at TonAddressItemReply.address.
    • Extract the publicKey from the result of this method call.
  3. Verify publicKey and all params:

    • Check that TonAddressItemReply.publicKey matches the extracted public key from walletStateInit or from on-chain method get_public_key call.
    • Verify that TonAddressItemReply.walletStateInit.hash() equals TonAddressItemReply.address.hash() (.hash() refers to the BoC hash).
  4. After that, make sure that the ton_proof signature is signed by that publicKey:

    • Create a message payload according specification in the Address proof signature (ton_proof) section and make a valid hash.
    • Verify that the ton_proof signature matches with hash and publicKey.

This approach prioritizes local parsing of stateInit, and uses the on-chain get_public_key method only as a fallback, if local parsing is not possible. This reduces unnecessary blockchain calls and improves efficiency.

You can find an example of authentication on this page.

Wallet disconnection

Call to disconnect the wallet:

const [tonConnectUI] = useTonConnectUI();

await tonConnectUI.disconnect();

Deploying contract

Deploying a contract using TON Connect is pretty straightforward. You must:

  • Obtain the contract code and data
  • Store them as a stateInit cell
  • Send a transaction using the statement field provided

Note that CONTRACT_CODE and CONTRACT_INIT_DATA may be found in wrappers.

import { beginCell, Cell, contractAddress, StateInit, storeStateInit } from '@ton/core';

const [tonConnectUI] = useTonConnectUI();

const init = {
code: Cell.fromBase64('<CONTRACT_CODE>'),
data: Cell.fromBase64('<CONTRACT_INIT_DATA>')
} satisfies StateInit;

const stateInit = beginCell()
.store(storeStateInit(init))
.endCell();

const address = contractAddress(0, init);

await tonConnectUI.sendTransaction({
validUntil: Date.now() + 5 * 60 * 1000, // 5 minutes
messages: [
{
address: address.toRawString(),
amount: '5000000',
stateInit: stateInit.toBoc().toString('base64')
}
]
});

Wrappers

Wrappers are classes that simplify interaction with the contract, allowing you to work without concerning yourself with the underlying details.

  • When developing a contract in FunC, write the wrapper yourself.
  • When using the Tact language, wrappers are automatically generated for you.
tip

Check out the Blueprint documentation on how to develop and deploy contracts.

Let's take a look at the default Blueprint counter wrapper example and how we can use it:

Details

Wrapper usage Counter wrapper class:

import { Address, beginCell, Cell, Contract, contractAddress, ContractProvider, Sender, SendMode } from '@ton/core';

export type CounterConfig = {
id: number;
counter: number;
};

export function counterConfigToCell(config: CounterConfig): Cell {
return beginCell().storeUint(config.id, 32).storeUint(config.counter, 32).endCell();
}

export const Opcodes = {
increase: 0x7e8764ef,
};

export class Counter implements contract {
constructor(
readonly address: Address,
readonly init?: { code: Cell; data: Cell },
) {}

static createFromAddress(address: Address) {
return new Counter(address);
}

static createFromConfig(config: CounterConfig, code: Cell, workchain = 0) {
const data = counterConfigToCell(config);
const init = { code, data };
return new Counter(contractAddress(workchain, init), init);
}

async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) {
await provider.internal(via, {
value,
sendMode: SendMode.PAY_GAS_SEPARATELY,
body: beginCell().endCell(),
});
}

async sendIncrease(
provider: ContractProvider,
via: Sender,
opts: {
increaseBy: number;
value: bigint;
queryID?: number;
},
) {
await provider.internal(via, {
value: opts.value,
sendMode: SendMode.PAY_GAS_SEPARATELY,
body: beginCell()
.storeUint(Opcodes.increase, 32)
.storeUint(opts.queryID ?? 0, 64)
.storeUint(opts.increaseBy, 32)
.endCell(),
});
}

async getCounter(provider: ContractProvider) {
const result = await provider.get('get_counter', []);
return result.stack.readNumber();
}

async getID(provider: ContractProvider) {
const result = await provider.get('get_id', []);
return result.stack.readNumber();
}
}

Then you can use this class in your react component:


import "buffer";
import {
TonConnectUI,
useTonConnectUI,
useTonWallet,
} from "@tonconnect/ui-react";
import {
Address,
beginCell,
Sender,
SenderArguments,
storeStateInit,
toNano,
TonClient,
} from "@ton/ton";

class TonConnectProvider implements Sender {
/**
* The TonConnect UI instance.
* @private
*/
private readonly provider: TonConnectUI;

/**
* The address of the current account.
*/
public get address(): Address | undefined {
const address = this.provider.account?.address;
return address ? Address.parse(address) : undefined;
}

/**
* Creates a new TonConnectProvider.
* @param provider
*/
public constructor(provider: TonConnectUI) {
this.provider = provider;
}

/**
* Sends a message using the TonConnect UI.
* @param args
*/
public async send(args: SenderArguments): Promise<void> {
// The transaction is valid for 10 minutes.
const validUntil = Math.floor(Date.now() / 1000) + 600;

// The recipient's address should be in bounceable format for all smart contracts.
const address = args.to.toString({ urlSafe: true, bounceable: true });

// The address of the sender, if available.
const from = this.address?.toRawString();

// The amount to send in nano tokens.
const amount = args.value.toString();

// The state init cell for the contract.
let stateInit: string | undefined;
if (args.init) {
// State init cell for the contract.
const stateInitCell = beginCell()
.store(storeStateInit(args.init))
.endCell();
// Convert the state init cell to boc base64.
stateInit = stateInitCell.toBoc().toString("base64");
}

// The payload for the message.
let payload: string | undefined;
if (args.body) {
// Convert the message body to boc base64.
payload = args.body.toBoc().toString("base64");
}

// Use the TonConnect UI to send the message and wait for the sending
await this.provider.sendTransaction({
validUntil: validUntil,
from: from,
messages: [
{
address: address,
amount: amount,
stateInit: stateInit,
payload: payload,
},
],
});
}
}

const CONTRACT_ADDRESS = "EQAYLhGmznkBlPxpnOaGXda41eEkliJCTPF6BHtz8KXieLSc";

const getCounterInstance = async () => {
const client = new TonClient({
endpoint: "https://testnet.toncenter.com/api/v2/jsonRPC",
});

// OR you can use createApi from @ton-community/assets-sdk
// import {
// createApi,
// } from "@ton-community/assets-sdk";

// const NETWORK = "testnet";
// const client = await createApi(NETWORK);


const address = Address.parse(CONTRACT_ADDRESS);
const counterInstance = client.open(Counter.createFromAddress(address));

return counterInstance;
};

export const Header = () => {
const [tonConnectUI, setOptions] = useTonConnectUI();
const wallet = useTonWallet();

const increaseCount = async () => {
const counterInstance = await getCounterInstance();
const sender = new TonConnectProvider(tonConnectUI);

await counterInstance.sendIncrease(sender, {
increaseBy: 1,
value: toNano("0.05"),
});
};

const getCount = async () => {
const counterInstance = await getCounterInstance();

const count = await counterInstance.getCounter();
console.log("count", count);
};

return (
<main>
{!wallet && (
<button onClick={() => tonConnectUI.openModal()}>Connect Wallet</button>
)}
{wallet && (
<>
<button onClick={increaseCount}>Increase count</button>
<button onClick={getCount}>Get count</button>
</>
)}
</main>
);
};

Wrappers for jettons and NFTs

To interact with jettons or NFTs, you can use assets-sdk. This SDK provides wrappers that simplify interaction with these assets. Please check our examples section for practical examples.

API documentation

Latest API documentation

Examples

  • Step-by-step TON Hello World guide to create a simple DApp with React UI.
  • Demo DApp - Example of a DApp with @tonconnect/ui-react.
  • ton.vote - Example of React website with TON Connect implementation.

See also

Was this article useful?