Build a React wallet connection with TON Connect, read the connected account, and pass STON.fi transactions to the wallet for approval.
Connecting a TON wallet to STON.fi does not require importing a private key or giving an application control over the wallet. The connection is created through TON Connect, the standard wallet communication protocol for TON.
For someone using the official STON.fi interface, the process is simple: open the application, select Connect Wallet, choose a compatible wallet, and approve the connection. For developers, the same process can be integrated into a React application with @tonconnect/ui-react.
Once connected, the application can read the public wallet address and request transactions. The wallet still holds the private keys and asks the owner to approve every transaction.
This tutorial builds that connection and then demonstrates how it fits into a STON.fi swap integration.
What you will build
By the end of the tutorial, the application will be able to:
- Display a TON Connect wallet selector.
- Restore an existing wallet session after a page reload.
- Read the connected wallet address and network.
- Reject a testnet wallet when the application targets mainnet.
- Request STON.fi swap data through the official API.
- Build a STON.fi transaction with the SDK.
- Send the transaction to the connected wallet for approval.
The integration uses three separate layers:
| Layer | Package | Responsibility |
|---|---|---|
| Wallet connection | @tonconnect/ui-react |
Opens the wallet selector and manages the session |
| STON.fi data | @ston-fi/api |
Retrieves assets, pools, simulations, and routing data |
| STON.fi transactions | @ston-fi/sdk |
Builds the transaction parameters sent to the wallet |
The wallet connection and the DEX integration are related, but they serve different purposes. TON Connect communicates with the wallet. The STON.fi API and SDK prepare the operation that the wallet will sign.
What connecting a wallet actually authorizes
A wallet connection is not a token approval and does not move funds.
After the user approves the connection, the application normally receives information such as:
- The selected TON account address.
- The network used by the account.
- The wallet application name.
- Supported wallet features.
- A session that can be restored later.
The application may then prepare a transaction and send a signing request. The wallet displays that request separately, allowing the user to review and reject it.
A secure integration should therefore treat these as two distinct events:
- Connect: establish a communication session and obtain the public address.
- Transact: ask the wallet to sign and send a specific blockchain message.
Never ask users to enter a seed phrase or private key. A normal TON Connect integration does not need either one.
Create the React project
This tutorial uses React, TypeScript, and Vite.
Create the project:
npm create vite@latest ston-wallet-demo -- --template react-ts
cd ston-wallet-demo
npm install
Install TON Connect and the STON.fi packages:
npm install @tonconnect/ui-react @ston-fi/api @ston-fi/sdk @ton/ton
Start the development server:
npm run dev
The basic wallet connection only requires @tonconnect/ui-react. The STON.fi packages become necessary when the application starts loading DEX information and building transactions.
Some browser builds may also require a Buffer polyfill when serializing a transaction body into a base64 Bag of Cells, or BOC. Add one only when your build reports that Buffer is unavailable:
npm install --save-dev vite-plugin-node-polyfills
Then update vite.config.ts:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { nodePolyfills } from "vite-plugin-node-polyfills";
export default defineConfig({
plugins: [
react(),
nodePolyfills({
include: ["buffer"],
globals: {
Buffer: true,
global: true,
process: false,
},
}),
],
});
Publish the TON Connect manifest
Before a wallet connects, it needs to know which application is requesting the connection. TON Connect provides this information through tonconnect-manifest.json.
Create the following file:
public/tonconnect-manifest.json
Add your application metadata:
{
"url": "https://your-domain.example",
"name": "STON.fi Wallet Demo",
"iconUrl": "https://your-domain.example/icon-180.png",
"termsOfUseUrl": "https://your-domain.example/terms",
"privacyPolicyUrl": "https://your-domain.example/privacy"
}
Only url, name, and iconUrl are required. The legal document fields are optional.
For production, the manifest must be available through a public HTTPS URL. It cannot require authentication or sit behind a browser challenge. The icon should be a publicly accessible PNG or ICO file, with a 180 by 180 pixel PNG being the recommended option. SVG icons are not supported by the documented manifest flow.
You should be able to open the final URL directly:
https://your-domain.example/tonconnect-manifest.json
A desktop extension may sometimes work with a local manifest, but a mobile wallet cannot reliably fetch a file from your computer's localhost. Use a public preview deployment or an HTTPS development tunnel when testing the QR flow on a phone.
Mount the TON Connect provider
The TonConnectUIProvider makes the TON Connect client available to every component below it.
Update src/main.tsx:
import React from "react";
import ReactDOM from "react-dom/client";
import { TonConnectUIProvider } from "@tonconnect/ui-react";
import App from "./App";
import "./index.css";
const manifestUrl =
import.meta.env.VITE_TONCONNECT_MANIFEST_URL ??
`${window.location.origin}/tonconnect-manifest.json`;
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<TonConnectUIProvider manifestUrl={manifestUrl}>
<App />
</TonConnectUIProvider>
</React.StrictMode>,
);
For production, define the manifest URL explicitly:
VITE_TONCONNECT_MANIFEST_URL=https://your-domain.example/tonconnect-manifest.json
Using an environment variable makes accidental domain mismatches less likely when the application has development, staging, and production deployments.
Add the wallet connection interface
TON Connect provides a ready-made button that opens the wallet selection modal and reflects the current connection state.
Replace src/App.tsx with:
import {
TonConnectButton,
useIsConnectionRestored,
useTonAddress,
useTonConnectUI,
useTonWallet,
} from "@tonconnect/ui-react";
export default function App() {
const wallet = useTonWallet();
const address = useTonAddress();
const restored = useIsConnectionRestored();
const [tonConnectUI] = useTonConnectUI();
if (!restored) {
return <main>Restoring wallet session...</main>;
}
return (
<main>
<header>
<h1>STON.fi Wallet Integration</h1>
<TonConnectButton />
</header>
{!wallet ? (
<section>
<p>Connect a TON wallet to continue.</p>
<button type="button" onClick={() => tonConnectUI.openModal()}>
Open wallet selector
</button>
</section>
) : (
<section>
<h2>Connected wallet</h2>
<dl>
<dt>Address</dt>
<dd>{address}</dd>
<dt>Wallet</dt>
<dd>{wallet.device.appName}</dd>
<dt>Network</dt>
<dd>
{wallet.account.chain === "-239" ? "Mainnet" : "Testnet"}
</dd>
</dl>
</section>
)}
</main>
);
}
useTonAddress() returns an empty string while disconnected and a user-friendly TON address after connection. useTonWallet() returns the full wallet object, while useIsConnectionRestored() tells the application whether TON Connect has finished checking for a previously saved session.
The restoration check is important. Before restoration finishes, a missing wallet does not necessarily mean that the user is disconnected. Redirecting or disabling features too early can create a visible flash of incorrect state.
Validate the network before using STON.fi
TON mainnet and testnet use different chain identifiers:
Mainnet: -239
Testnet: -3
The current network is available through:
wallet.account.chain
A mainnet STON.fi integration should block the action when the connected account is not on mainnet:
function requireMainnet(chain: string): void {
if (chain !== "-239") {
throw new Error("Connect a TON mainnet wallet before continuing.");
}
}
You should also include the expected network in every transaction request. The wallet can then reject a transaction when the connected account is on a different network. TON Connect does not provide a conventional runtime network-switch event, so separate deployments are preferable when an application needs both mainnet and testnet experiences.
Connect the wallet state to the STON.fi SDK
At this point, the application is connected to a wallet, but it has not interacted with STON.fi.
A production STON.fi v2 swap should be API-driven:
- Simulate the requested swap.
- Receive the router metadata from the simulation.
- Create the appropriate router through
dexFactory(). - Build transaction parameters for the selected swap type.
- Pass those parameters to TON Connect.
This avoids hardcoding a router that may no longer be appropriate for the requested operation. STON.fi's current v2 documentation recommends using the router information returned by the API simulation.
The following helper prepares a TON-to-jetton transaction:
import { Client, dexFactory } from "@ston-fi/sdk";
import { StonApiClient } from "@ston-fi/api";
const tonClient = new Client({
endpoint: import.meta.env.VITE_TON_RPC_URL,
});
const stonApi = new StonApiClient();
type BuildSwapInput = {
walletAddress: string;
askJettonAddress: string;
offerUnits: string;
};
export async function buildTonToJettonSwap({
walletAddress,
askJettonAddress,
offerUnits,
}: BuildSwapInput) {
const simulation = await stonApi.simulateSwap({
offerAddress: "ton",
askAddress: askJettonAddress,
offerUnits,
slippageTolerance: "0.01",
});
const routerInfo = simulation.router;
const contracts = dexFactory(routerInfo);
const router = tonClient.open(
contracts.Router.create(routerInfo.address),
);
const proxyTon = contracts.pTON.create(
routerInfo.ptonMasterAddress,
);
return router.getSwapTonToJettonTxParams({
userWalletAddress: walletAddress,
offerAmount: simulation.offerUnits,
minAskAmount: simulation.minAskUnits,
askJettonAddress: simulation.askAddress,
proxyTon,
});
}
offerUnits must use blockchain units, not a human-readable decimal string. Token precision is not universal. Some TON assets use nine decimal places, while others use a different value, so retrieve the asset metadata and convert the entered amount according to that asset's decimals. Do not apply nine decimals to every token.
The simulation also provides minAskUnits. Reusing it in the transaction ensures that the signed operation reflects the minimum output calculated for the selected slippage tolerance.
Send the STON.fi transaction through TON Connect
The STON.fi SDK returns transaction parameters containing the destination contract, attached value, and serialized message body.
Convert them into the shape expected by TON Connect:
type StonTransactionParams = {
to: {
toString(): string;
};
value: {
toString(): string;
};
body?: {
toBoc(): {
toString(encoding: "base64"): string;
};
};
};
function toTonConnectRequest(txParams: StonTransactionParams) {
return {
validUntil: Math.floor(Date.now() / 1000) + 300,
network: "-239",
messages: [
{
address: txParams.to.toString(),
amount: txParams.value.toString(),
payload: txParams.body?.toBoc().toString("base64"),
},
],
};
}
You can now use it inside a React component:
import { useState } from "react";
import {
useTonAddress,
useTonConnectUI,
useTonWallet,
} from "@tonconnect/ui-react";
import { buildTonToJettonSwap } from "./stonSwap";
type SwapButtonProps = {
askJettonAddress: string;
offerUnits: string;
};
export function SwapButton({
askJettonAddress,
offerUnits,
}: SwapButtonProps) {
const wallet = useTonWallet();
const walletAddress = useTonAddress();
const [tonConnectUI] = useTonConnectUI();
const [pending, setPending] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSwap = async () => {
setError(null);
if (!wallet || !walletAddress) {
await tonConnectUI.openModal();
return;
}
if (wallet.account.chain !== "-239") {
setError("A TON mainnet wallet is required.");
return;
}
try {
setPending(true);
const txParams = await buildTonToJettonSwap({
walletAddress,
askJettonAddress,
offerUnits,
});
await tonConnectUI.sendTransaction(
toTonConnectRequest(txParams),
);
} catch (unknownError) {
const message =
unknownError instanceof Error
? unknownError.message
: "The transaction could not be prepared or sent.";
setError(message);
} finally {
setPending(false);
}
};
return (
<div>
<button type="button" onClick={handleSwap} disabled={pending}>
{pending ? "Preparing transaction..." : "Review swap in wallet"}
</button>
{error && <p role="alert">{error}</p>}
</div>
);
}
Calling sendTransaction() does not silently execute the swap. It opens the wallet approval flow. The wallet owner can inspect and reject the request before it is broadcast. STON.fi documents the same boundary: the SDK generates txParams, and TON Connect sends the resulting message to the wallet.
Production checks and common failures
Before enabling real swaps, add a review screen that displays:
- The input asset and amount.
- The output asset.
- The expected output.
- The minimum output.
- The selected slippage tolerance.
- The connected address and network.
- Any estimated network or protocol costs available to the interface.
Do not prepare a quote once and reuse it indefinitely. Market conditions and pool reserves can change. Request a fresh simulation shortly before building the transaction.
Common integration problems include:
| Problem | Likely cause | What to check |
|---|---|---|
| Wallet modal opens but connection fails | Manifest unavailable | Public HTTPS URL, valid JSON, icon URL, and unrestricted access |
| Mobile wallet cannot load the app identity | Manifest points to localhost | Deploy the manifest or use a public HTTPS development URL |
| UI briefly says disconnected | Session restoration is unfinished | Wait for useIsConnectionRestored()
|
| Wallet rejects the transaction network | Mainnet and testnet mismatch | Check wallet.account.chain and set network: "-239"
|
Browser reports Buffer is not defined
|
BOC serialization lacks a polyfill | Add a targeted Buffer polyfill |
| Quote works but transaction fails | Quote is stale, balance changed, or TON for fees is insufficient | Simulate again and recheck balances |
| Swap uses an unexpected router | Contract address was hardcoded | Use the router returned by the latest STON.fi simulation |
STON.fi's REST API currently serves the documented production workflow from mainnet. The v2 testnet path requires manually configured contracts and liquidity, so it should not be treated as a full mirror of the mainnet API-driven experience.
A practical final test is to connect a separate wallet with a small balance, simulate a modest transaction, review every wallet field, and confirm the resulting operation on-chain before expanding the integration.
Frequently Asked Questions
Can I connect a wallet directly to the official STON.fi application?
Yes. Open the official STON.fi application, select Connect Wallet, choose a compatible TON wallet, and approve the connection. The developer steps in this tutorial are for applications that want to integrate TON Connect and STON.fi functionality into their own interface.
Does connecting a wallet give STON.fi access to the private key?
No. TON Connect establishes a communication session with the wallet, but the private key remains inside the wallet application. A dApp can request a transaction, but the wallet owner must review and approve that request separately.
Which TON Connect package should a React application use?
Use @tonconnect/ui-react for React and Next.js applications. It provides the provider, connection button, modal, and React hooks. Non-React browser applications can use @tonconnect/ui, while headless or highly customized integrations can use @tonconnect/sdk.
Why does the TON Connect manifest need a public HTTPS URL?
The wallet fetches the manifest independently to identify the requesting application. A local file, protected endpoint, authentication wall, or browser challenge can prevent the wallet from reading it. HTTPS also gives the wallet a stable origin for displaying the application identity.
How can the application tell whether a wallet uses mainnet?
Read wallet.account.chain. TON mainnet uses "-239" and testnet uses "-3". For a mainnet STON.fi application, validate the value after connection and include network: "-239" in transaction requests.
Is connecting a wallet the same as authenticating a user?
Not necessarily. A public wallet address can identify a connected account, but an application that needs secure backend authentication should request and verify ton_proof. A connection alone should not be treated as proof that an HTTP request came from the wallet owner.
Can STON.fi v2 swaps be fully tested through the public API on testnet?
Not through the same production API-driven path. STON.fi documents its REST API as mainnet-focused. Testnet swaps require manually selected contracts, test assets, and funded liquidity pools. Use that environment for controlled development rather than assuming it matches mainnet liquidity or routing.
What should be checked before requesting a STON.fi swap signature?
Check the connected network, wallet balance, asset addresses, token decimals, expected output, minimum received, slippage tolerance, transaction destination, and attached TON value. Build the transaction from a fresh STON.fi simulation and let the wallet display the final approval request.






Top comments (0)