The TON (The Open Network) blockchain is one of the most innovative blockchain projects, offering high speed and extensive scalability, making it an ideal platform for developing decentralized applications (dApps). In this network, smart contracts play a crucial role in automating transactions and creating complex services.
As dApps continue to grow, developers require tools that make it easy to link digital wallets to their applications. TonConnect offers a robust and secure solution, enabling developers to integrate multiple wallets with their dApps and streamline interactions with smart contracts.
In this tutorial, we will use TonConnect and the React library to explore step-by-step how to interact with smart contracts on the TON blockchain. From setting up a project with React to connecting a digital wallet and sending transactions to the smart contract, we will cover all the steps in detail.
Prerequisites
Foundational knowledge of React and TypeScript will help you with this tutorial more easily.
Given that the TON blockchain is fundamentally based on smart contracts, it’s evident that we require a method to interact with these contracts in order to create decentralized applications (dApps).
For example, let’s say you plan to create a ticketing system and provide it to users as a website. To accomplish this, you’ll need to interact with the smart contract related to the ticketing system. This involves sending transactions to the smart contract and receiving responses from it.
In this tutorial, we’ll assume you have already created your smart contract and are ready to deploy it. For demonstration purposes, we will use a smart contract for a ticketing system. If you haven’t yet prepared your smart contract, you can refer to this sample ticketing system smart contract on GitHub.
Required Tools and Development Environment:
- Node.js and NPM: For managing packages and running the project.
- React: To build the application’s user interface.
- TypeScript: For safer coding practices and improved type management.
- TonConnect SDK: To enable communication between the wallet and the smart contract.
- TON Wallet (like Tonkeeper): For interacting with the TON network and carrying out transactions.
- A TON blockchain environment: For developing and testing smart contracts (the TON testnet can be used).
To begin, you’ll need to set up a React project. You can use any bundler you choose. In this tutorial, we will use Vite as our bundler due to its fast performance and efficiency in developing React projects.
;; creates the project with react-ts template
npm create vite@latest my-react-app -- --template react-ts
;; change directory to project folder
cd my-react-app
;; install dependecies
npm i
Once the project is set up, we will need to install a few libraries to facilitate interaction with smart contracts in React. You can do this by executing the following code:
npm i @ton/ton @tonconnect/ui-react @orbs-network/ton-access zustand
Once you have installed the necessary libraries, the next step is to add the wrapper file for your smart contract. This wrapper file enables you to interact seamlessly with the methods and attributes of the smart contract.
To accomplish this, create a new folder called contract in your project directory and save the smart contract wrapper file in that folder. This file will include all the essential functions and methods needed to interact with your smart contract.
The layout of your project folder after adding this file will look like this:
src/
contract/
TicketSystem.ts
JettonWallet.ts
JettonWallet.ts
This wrapper file will act as a bridge between your React user interface and your smart contract.
Once you have added the smart contract wrapper file to your React project, the next step is to open it with a client to enable direct interaction with the smart contract. However, before we do that, we need to set up a store to manage and store contract information at the application level. For this, we will utilize the zustand library.
In the next step, we will create a new file to manage the state of the smart contract using Zustand. This store will enable us to share the contract’s status and associated information across the application.
Create a new file named contractStore.js (or .ts if you’re using TypeScript) in the src/store directory:
import { address, OpenedContract } from '@ton/core';
import { create } from 'zustand';
import { TicketSystem } from '../contracts/TicketSystem';
import { TonClient } from '@ton/ton';
import { getHttpEndpoint } from '@orbs-network/ton-access';
import { contractAddress } from '../constants';
type State = {
ticketSystem: OpenedContract<TicketSystem> | null;
client: TonClient | null;
};
type Action = {
setupTicketSystem: () => void;
};
export const useTicketSystem = create<State & Action>((set, get) => ({
ticketSystem: null,
client: null,
setupTicketSystem: async () => {
const currTicketSystem = get().ticketSystem;
if (currTicketSystem) return;
const client = new TonClient({
endpoint: await getHttpEndpoint({ network: 'testnet' }),
});
const ticketSystem = client.open(TicketSystem.createFromAddress(address(contractAddress)));
set(() => ({ ticketSystem, client }));
},
}));
In this section, we need to store both the contract opened by the client and the client itself. Since some functions are async, we will need a function to initialize the client.
In the App.tsx file, we will import the useTicketSystem hook and the necessary items for TonConnect as follows:
import { useTicketSystem } from '../hooks/useTicket';
import { CHAIN, TonConnectButton, useTonConnectModal, useTonConnectUI } from '@tonconnect/ui-react';
After importing the hooks, we need to (initialize) the following hooks:
const { setupTicketSystem } = useTicketSystem();
const [tonConnectUI, setOptions] = useTonConnectUI();
const { open } = useTonConnectModal();
useEffect(() => {
setupTicketSystem();
setOptions({ actionsConfiguration: { modals: ['before', 'error', 'success'] } });
}, []);
we can check the wallet connection status using the following code:
const [walletConnected, setWalletConnected] = useState(false);
const walletTonConnect = useTonWallet();
useEffect(() => {
if (walletTonConnect) {
setWalletConnected(true);
} else {
setWalletConnected(false);
}
}, [walletTonConnect]);
To send a message from the user’s wallet to the smart contract, we need a state and a function, which are defined in the code below:
const [buyCount, setBuyCount] = useState(1);
const handlePay = async (pay_with: 'o1' | 'o-mini') => {
const message = await getTransaction(address(tonConnectUI.account!.address), pay_with, buyCount);
tonConnectUI
.sendTransaction({ messages: [message], validUntil: Date.now() + 500 * 60 * 1000, network: CHAIN.TESTNET })
.then((res) => {
const boc = Cell.fromBase64(res.boc);
console.log(boc.hash().toString('hex'));
})
.catch((e) => {
console.log(e);
});
onOpenChange();
};
In the code above, there is a function named getTransaction that prepares our message for sending to the network. This function can look like this:
import { Address, address, toNano, } from "@ton/ton"
import { useTicketSystem } from "../hooks/useTicket"
import { JettonMinter } from "../contracts/JettonMaster"
import { JettonWallet } from "../contracts/JettonWallet"
import { contractAddress } from "../constants"
export default async (owner: Address, paywith: "o1" | "o-mini", how_many: number) => {
const contractAddr = address(contractAddress)
const client = useTicketSystem.getState().client!
if (paywith === "o1") {
const O1master = client.open(JettonMinter.createFromAddress(address("kQDAiO286SYcHCnj6-U_bWIBbURv14yqptYPvMWzVfC7WI-J")))
const o1WalletAddr = await O1master.getWalletAddress(owner)
// @ts-expect-error
const cell = JettonWallet.transferMessage(toNano(`${how_many}`), contractAddr, owner, null, toNano("0.01"), null)
const message = {
address: o1WalletAddr.toString(),
amount: toNano("0.05").toString(),
payload: cell.toBoc().toString("base64")
}
return message
} else {
const Ominimaster = client.open(JettonMinter.createFromAddress(address("kQAuG2F399vT2Bsf_lYAXzZKpSjVP7Y7No9d--6BiX_Dq7kv")))
const ominiWallet = await Ominimaster.getWalletAddress(owner)
// @ts-expect-error
const cell = JettonWallet.transferMessage(toNano(`${how_many * 2}`), contractAddr, owner, null, toNano("0.01"), null)
const message = {
address: ominiWallet.toString(),
amount: toNano("0.05").toString(),
payload: cell.toBoc().toString("base64")
}
return message
}
}
Conclusion
In this tutorial, we explored the steps for interacting with smart contracts on the TON blockchain network. By using TonConnect and the React library, we were able to design a simple decentralized application (dApp) for sending messages to a smart contract.
The key steps in this tutorial were as follows:
- Setting Up the React Project: We created a new React project using Vite and installed the necessary libraries.
- Creating the Contract Wrapper: We added the smart contract wrapper file to the project, allowing us to easily interact with the contract’s methods.
- Managing State with Zustand: We created a store to store the smart contract information, enabling us to share the contract’s status throughout the application.
- Configuring the TonConnect Client: We initialized the client and checked the wallet connection status for users.
- Sending Messages to the Contract: We sent messages to the smart contract using a function to prepare the transaction and manage user input.
This process enables developers to easily and efficiently create their decentralized applications using TonConnect, taking advantage of the smart contract capabilities on the TON blockchain network.