React Counter dApp tutorial
This tutorial goes through the process of setting up the application that implements the on-chain counter with increment feature in React.
As a result of this tutorial, we are going to create a dApp that can connect to a TON wallet, deploy counter smart contract, read its state and run the Add or Subtract contract functions. You will get to know TON and specifically Foton development with this hands-on experience.
Make sure you have a Node.js (opens in a new tab) version 18 or higher installed on your machine.
Create Vite app
Vite (opens in a new tab) is a modern bundler for single-page web applications. The following command will help you to create a template application that we are going to fill with decentralization later. It will prompt you to select a project name, framework, whether TypeScript is needed or not.
For this tutorial, select React framework (TypeScript + SWC preset). Notice that Foton is not tied to any of the frameworks and can work both in the browser and in the server environment.
npm create vite@latest
Since the application deals with cryptography, we need to adjust the template by adding Buffer polyfill. Install the Vite plugin for it:
npm install -D vite-plugin-node-polyfills
Now, head to the vite.config.ts
file and update the plugins
section.
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import { nodePolyfills } from 'vite-plugin-node-polyfills';
export default defineConfig({
plugins: [
react(),
nodePolyfills({
include: ['buffer'],
}),
],
})
Add and compile your first contract
TON blockchain supports three smart contract languages: Fift, FunC, and Tact (opens in a new tab). This tutorial focuses on the Tact since it is the only high-level language from the list and the easiest of them to get started with.
Let's write the first contract in the contracts/counter.tact
file:
import "@stdlib/deploy";
message Add {
queryId: Int as uint64;
amount: Int as uint32;
}
message Subtract {
queryId: Int as uint64;
amount: Int as uint32;
}
contract Counter with Deployable {
counter: Int as uint32;
init() {
self.counter = 0;
}
receive(msg: Add) {
self.counter += msg.amount;
}
receive(msg: Subtract) {
self.counter -= msg.amount;
}
get fun counter(): Int {
return self.counter;
}
}
We are not going to cover the language features here, so please refer to the Tact book (opens in a new tab) for learning its syntax and semantics.
To put it simply, the Counter contract has only one variable in its state called counter
with type uint32
. It also has two methods (receivers) Add
and Subtract
, which increase or decrease the counter
value by the provided amount. Lastly, the counter
getter returns the value of the counter
state.
Now, we need to install the Tact compiler to process the smart contract:
npm install -D @tact-lang/compiler
Then, let's create a configuration file tact.config.json
for the Tact compiler that will point to the .tact
files and define the output folder.
{
"$schema": "http://raw.githubusercontent.com/tact-lang/tact/main/grammar/configSchema.json",
"projects": [
{
"name": "counter",
"path": "./contracts/counter.tact",
"output": "./contracts/counter"
}
]
}
When the configuration is ready, we only need to modify the scripts
object of the package.json
:
{
"scripts": {
"compile": "tact --config ./tact.config.json"
}
}
Compile your first smart contract:
npm run compile
If you open the contracts/counter
directory, you will see many files created by Tact: ABI file, MD file, PKG file, results of compilation to FunC and Fift languages. However, the most important file of all compilation is the counter_Counter.ts
. Take a moment to look and read the resulting Counter
class.
Let's export the Counter
class for the future usage. Create contracts/index.ts
file:
export { Counter } from './counter/counter_Counter';
Making a dApp
To create a dApp, let's install the @fotonjs/core
library:
npm install @fotonjs/core @ton/core
In order to allow the app to be connected to wallets in TON, the app must provide a manifest. The manifest is a JSON file with a name, URL and icon of the dApp. In Vite, the best place to put it is public/tonconnect-manifest.json
:
{
"name": "Counter",
"url": "https://example.com",
"icon": "https://example.com/icon.png"
}
The manifest must be accessible via its URL. That means that you should deploy the Counter dApp with the manifest file, get the link and use it with Foton. If you are not ready to deploy the app, you can use the Foton's manifest: https://counter.foton.sh/tonconnect-manifest.json (opens in a new tab).
Foton library lets you create clients – interfaces to interact with the TON blockchain. There are four main clients:
WalletClientUI
– connect to a wallet on TON and send basic transactions with a user interface. To connect, user will choose in a modal window from one of many wallets. Available only in browser environment.WalletClient
– connect to any wallet on TON without user interface. Available in both server and browser environments.PublicClient
– read data from the blockchain. Does not require a wallet connection.ContractClient
– deploy smart contracts to the blockchain, call their methods, and read their states.
For this tutorial, we will need a wallet client with UI, public client, and a contract client. Let's define them in src/ton-clients.ts
:
import { createWalletClientUI, createPublicClient, createContractClient } from '@fotonjs/core';
import { Counter } from '../contracts';
export const walletClient = createWalletClientUI({
// Use TON testnet for development purposes
chain: 'testnet',
// Provide a link to the deployed manifest file
manifestUrl: 'https://counter.foton.sh/tonconnect-manifest.json',
// If the wallet was already connected, restore the connection immediately
restoreConnection: true,
});
export const publicClient = createPublicClient({
api: 'testnet',
// Provide your API key from Ton Center to increase the rate limits
// authToken: 'token'
});
export const counterClient = createContractClient({
// Use compiled Counter contract for the contractClient
contract: Counter,
// Provide the public and wallet clients to the contract client,
// so it can access both the public method and the wallet connection
publicClient,
walletClient,
});
Having the clients defined, you can easily interact with the blockchain! We can start by connecting to the wallet in src/use-wallet.tsx
:
import { useMemo, useState } from 'react';
import { walletClient } from './ton-clients.ts';
export const useWallet = () => {
const [loading, setLoading] = useState(false);
const [userAddress, setUserAddress] = useState<string>();
// Don't show the full address, only the first and last characters. It also saves space
const shortAddress = useMemo(() => {
if (!userAddress) return '';
return userAddress.slice(0, 5) + '...' + (userAddress || '').slice(-4);
}, [userAddress]);
// Open wallet connection modal and wait while users connects. Show loading spinner meanwhile.
const onConnect = async () => {
setLoading(true);
const res = await walletClient.connect();
if (res.error) {
alert(res.error.message);
} else {
setUserAddress(res.data.account.address);
}
setLoading(false);
};
const onDisconnect = async () => {
setLoading(true);
const res = await walletClient.disconnect();
if (res.data) {
setUserAddress(undefined);
}
setLoading(false);
};
const connectButton = (
<button disabled={loading} onClick={onConnect}>
{loading ? 'Loading...' : 'Connect'}
</button>
);
const disconnectButton = (
<button disabled={loading} onClick={onDisconnect}>
{loading ? 'Loading...' : 'Disconnect'}
</button>
);
return {
userAddress: shortAddress,
connectButton,
disconnectButton,
};
};
In this hook we defined the basic logic for connecting and disconnecting the wallet. The same can be easily done with the Counter contract. Create a new file src/use-contract.tsx
:
import { useEffect, useState } from 'react';
import { parseTon } from '@fotonjs/core';
import { counterClient, publicClient } from './ton-clients.ts';
// Define localStorage key for storing the deployed smart contract address without deploying it each time.
const LS_KEY = 'counter-contract-address';
export const useContract = () => {
const [loading, setLoading] = useState(false);
const [contractAddress, setContractAddress] = useState<string | undefined>(localStorage.getItem(LS_KEY) || undefined);
const [counterAmount, setCounterAmount] = useState<number | undefined>(undefined);
// When the contract address is set, update the localStorage too
const setAddress = (address: string) => {
localStorage.setItem(LS_KEY, address);
setContractAddress(address);
};
// Set contract address for the contractClient on initial render.
// This way Foton will know to which contract to send the requests.
useEffect(() => {
counterClient.setAddress(contractAddress);
}, []);
// Subscribe to the counter state with interval
useEffect(() => {
getCounterAmount();
const interval = setInterval(getCounterAmount, 5000);
return () => clearInterval(interval);
}, [contractAddress]);
// Read the counter state from the contract with the getter 'counter'
const getCounterAmount = async () => {
if (!contractAddress) return;
const res = await counterClient.read({
getter: 'counter',
arguments: [],
});
if (typeof res.data === 'number') {
setCounterAmount(res.data);
}
};
// Deploy the contract with 0.05 TON and with random queryId (alternative to `seqno`)
const onDeploy = async () => {
const res = await counterClient.deploy({
value: parseTon('0.05'),
arguments: [],
payload: {
queryId: BigInt(Math.floor(Math.random() * 1000)),
},
});
if (res.data) {
setAddress(res.data.address);
} else {
alert(res.error.message);
}
};
// Increment the counter by amount:1 on-chain with 0.05 TON
const onIncrement = async () => {
if (!contractAddress) return;
setLoading(true);
const res = await counterClient.write({
method: 'Add',
value: parseTon('0.05'),
payload: {
queryId: 1n,
amount: 1n,
},
});
// If the transaction was successful, wait for the transaction receipt with the help of the publicClient
if (res.data) {
await publicClient.waitForTransaction({ hash: res.data });
} else {
alert(res.error?.message || '');
}
setLoading(false);
};
const deployButton = (
<button onClick={onDeploy}>
Deploy Counter contract
</button>
);
const counterButtons = (
<>
<span>Counter: {counterAmount?.toString()}</span>
<button disabled={loading} onClick={onIncrement}>
{loading ? 'Loading...' : 'Increment'}
</button>
</>
);
return {
contractAddress,
deployButton,
counterButtons,
};
};
Here is a breakdown of the Foton features used in the hook:
contractClient.setAddress()
– sets the address of a contract for the contract client. Without this address, thewrite
andread
methods will throw an error.contractClient.deploy()
– deploys the contract to the blockchain. It requires the value to be sent with the contract, the arguments for the contract constructor, and the payload with the queryId.
This hook deploys the Counter contract, then queries it each 5 seconds to get the current counter value. It also provides a button to increment the counter by 1. Notice how the counterClient
and publicClient
are both used for its purposes.
Finally, let's update the main component src/App.tsx
:
import './App.css'
import { useWallet } from './use-wallet.tsx';
import { useContract } from './use-contract.tsx';
function App() {
const { userAddress, connectButton, disconnectButton } = useWallet();
const { contractAddress, deployButton, counterButtons} = useContract();
return (
<>
<h1>Counter dApp</h1>
{!userAddress && (
<div className="card">
{connectButton}
</div>
)}
{!!userAddress && (
<div className="card">
<span>{userAddress}</span>
{disconnectButton}
</div>
)}
{!!userAddress && !contractAddress && (
<div className="card">
{deployButton}
</div>
)}
{!!userAddress && !!contractAddress && (
<div className="card">
{counterButtons}
</div>
)}
</>
)
}
export default App
The App component uses two hooks that we defined before to render the Counter dApp in the simplest way. If the user is not connected to the wallet, the connect button is shown. If the user is connected, the address is displayed along with the disconnect button. If the contract is not deployed, the deploy button is shown. Otherwise, the counter value and the increment button are displayed in the component.
And just one final touch. Let's change the styles in src/App.css
:
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.card {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 2em;
}
The application is ready! We can now run it to check the logic. Start the development server:
npm run dev
Open the browser and navigate to http://localhost:5173
or another link the Vite will give you. You should be able to connect to the wallet, deploy the Counter contract, and increment the counter value. All of this is on chain!