DEV Community

Cover image for How Do You Connect a TON Wallet to STON.fi?

How Do You Connect a TON Wallet to STON.fi?

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

How Wallet Connection Works on STON.fi

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

Integration Architecture for STON.fi

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:

  1. Connect: establish a communication session and obtain the public address.
  2. 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

Project setup cheklist

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
Enter fullscreen mode Exit fullscreen mode

Install TON Connect and the STON.fi packages:

npm install @tonconnect/ui-react @ston-fi/api @ston-fi/sdk @ton/ton
Enter fullscreen mode Exit fullscreen mode

Start the development server:

npm run dev
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,
      },
    }),
  ],
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>,
);
Enter fullscreen mode Exit fullscreen mode

For production, define the manifest URL explicitly:

VITE_TONCONNECT_MANIFEST_URL=https://your-domain.example/tonconnect-manifest.json
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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

React Wallet Connection Flow

TON mainnet and testnet use different chain identifiers:

Mainnet: -239
Testnet: -3
Enter fullscreen mode Exit fullscreen mode

The current network is available through:

wallet.account.chain
Enter fullscreen mode Exit fullscreen mode

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.");
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

From Swap Quote to Wallet Signature

  1. Simulate the requested swap.
  2. Receive the router metadata from the simulation.
  3. Create the appropriate router through dexFactory().
  4. Build transaction parameters for the selected swap type.
  5. 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,
  });
}
Enter fullscreen mode Exit fullscreen mode

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"),
      },
    ],
  };
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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

Mainnet Checks and Common Errors

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)