TON: Ultimate developer guide from smart contracts to Jettons
If you’re a Web3 developer, the evolving ecosystem of blockchain technology continually presents innovation avenues. As a leading blockchain service provider, we at Chainstack believe in empowering developers like you with the insights and tools necessary to stay ahead in this dynamic space. That’s why we have put together this extensive guide for developing on one of the most promising blockchains today—the TON Network.
TON offers a unique space for creating fungible tokens, Jettons, that extend far beyond basic smart contract deployment. This guide aims to dismantle complexities and guide you through a systematic development process on TON using key tools like the Blueprint SDK, Sandbox, and how to integrate high-performance Chainstack infrastructure into the mix.
Let’s begin!
How to set up your environment for TON?
As a Web3 developer, the foundation of your journey into TON development is setting up the right environment. It’s crucial to have the right tools and access to help you build, test, and deploy effectively. Here’s how you can set it up:
1.1 Deploy a TON node on Chainstack
To begin with, hop onto the Chainstack console and get started with a free Developer account. Once your account is active, you’ll gain instant access to deploy nodes on various blockchains, including TON. By deploying a TON testnet node, you set the stage for interacting with the blockchain. Obtain the necessary credentials and API keys for your node for future transactions. Here’s a quick video guide on how you can navigate the interface to do that:
1.2 Install prerequisites
A few additional installations are necessary to ensure smooth operations. Ensure you have the current version of Node.js and a package manager (i.e., npm or yarn) installed on your system. Also, you’ll need a TON wallet with some testnet tokens.
Don’t worry if don’t have any testnet tokens; they are readily obtainable from the TON faucet. And if you are not familiar with TON wallets, you can learn more about the most popular choices and how they fare against each other in our dedicated guide here.
1.3 Initialize the Blueprint SDK
The Blueprint SDK is a valuable tool that simplifies TON development. Use it to initialize a new project directory and you’ll get a structured directory complete with folders dedicated to managing your smart contracts, scripts, tests, and wrappers. You can choose from pre-existing templates or start an empty project based on your preferences. Run the following in your CLI then select an empty project in TACT to start fresh:
npm create ton@latest
Once your project is created, the Blueprint SDK automatically organizes your files into a structured directory system, making the development process smoother. Here’s a quick overview of the key directories:
- build/: Stores the compiled output of your smart contracts, including bytecode and other important artifacts generated after running the build command.
- contracts/: This is where your smart contracts reside. Any
.tact
file, such as yourSimpleStorage
contract, will be saved in this directory. - scripts/: Used for deployment and interaction scripts. These scripts are essential for deploying your smart contracts to the TON blockchain and interacting with them, such as retrieving contract data or sending transactions.
- tests/: Contains all the test files for your smart contracts. You can write and execute tests here to verify that your contracts behave correctly. The default test ensures the contract deploys successfully, but you can extend it to test other functionalities.
- wrappers/: Holds the TypeScript wrappers for your smart contracts. These wrappers allow you to interact with the contracts in a type-safe manner when writing scripts or running tests.
This structured setup helps streamline the development lifecycle from contract creation to deployment and testing.
How to deploy a TON smart contract?
Web3 developers know that the core of any blockchain development lies in the successful deployment of a smart contract. Once your environment is set, it’s time to get hands-on with TON smart contracts using the powerful Blueprint SDK. Meanwhile, Chainstack’s seamless node management will ensure you keep your focus where it truly matters—creating exceptional Web3 experiences.
2.1 Write the TON smart contract in TACT
You’ll locate the contracts/
folder inside your Blueprint project. Here, create a new file where you will script your smart contract. Start with a simple storage contract in the TACT language, storing a number and incrementing a counter for each interaction.
// ./contracts/simple_contract.tact
import "@stdlib/deploy";
// Allows the contract to receive a custom object of type "Save" specifying the number to save in the contract.
message Save {
amount: Int as uint32;
}
// This is an example of a simple storage contract. It has a function that increments the saved number by one when the function is called.
contract SimpleContract with Deployable {
// Declare variables
// Variables structure: name: type
id: Int as uint32; // This is an ID for contract deployment
savedNumber: Int as uint32;
counter: Int as uint32;
// init is similar to a contructor in Solidity
init(id: Int) {
// Init the id assed from the contructor
self.id = id;
// Initialize the variables to zero when the contract is deployed
self.savedNumber = 0;
self.counter = 0;
}
// TON contracts can recevie messages
// This function makes an action when a specific message is recevied
// In this case, when the contract recevies the message "add 1" will increment the counter variable by 1
receive("add 1"){
self.counter = self.counter + 1;
}
// This allows the contract to recevie objects, in this case of type "Save"
// Save a value in the contract
receive(msg: Save){
self.savedNumber = msg.amount;
}
// Getter function to read the variable
get fun Number(): Int { // Int is the type of value returned
return self.savedNumber;
}
// Getter function to read the counter variable
get fun Counter(): Int { // Int is the type of value returned
return self.counter;
}
// Getter function for the ID
get fun Id(): Int {
return self.id;
}
}
2.2 Understand the TON smart contract components
While the contract might look simple, it has components crucial for more complex contracts you might write in the future. These include State Variables, the Initialization Function, Message Handlers, and Getter Functions—each performing a unique role inside your contract.
Imports and message declaration
- The contract starts by importing essential modules using
import "@stdlib/deploy";
. - A custom message type called
Save
is declared. This message contains anamount
field of typeInt
, aliased asuint32
.
Contract declaration
- The contract is defined as
SimpleContract
and includes theDeployable
trait, allowing it to be deployed on the blockchain. - Three variables are declared within the contract:
id
: Identifies the contract.savedNumber
: Stores a number in the contract.counter
: Tracks how many times a specific message is received.
Initialization function
- The
init()
function acts like a constructor and is called when the contract is deployed.- It initializes the
id
field with a custom value provided during deployment. - The
savedNumber
andcounter
fields are both initialized to zero.
- It initializes the
Message handlers
- The contract is designed to handle messages:
- The
receive("add 1")
handler increments thecounter
by 1 whenever it receives the message"add 1"
. - The
receive(msg: Save)
handler accepts aSave
message object and stores the providedamount
insavedNumber
.
- The
Getter functions
- The contract includes getter functions to retrieve the values stored in the variables:
get fun Number()
: Returns the current value ofsavedNumber
.get fun Counter()
: Returns the current value of thecounter
.get fun Id()
: Returns theid
of the contract.
Overview
The SimpleContract provides a basic example of a storage contract on the TON Network. It initializes with custom values, processes incoming messages to perform specific actions (like saving numbers or incrementing a counter), and allows users to query the stored values using getter functions.
2.3 Compile and build the contract
The next step after successful scripting is compiling the contract with Blueprint SDK. Successful compilation takes your TACT code and translates it into a format that TON’s nodes will understand.
Run the following in CLI to compile build your contract:
npx blueprint build
The response will be similar to this:
Using file: SimpleContract
Build script running, compiling SimpleContract
⏳ Compiling... > 👀 Enabling debug
> SimpleContract: tact compiler
> SimpleContract: func compiler
> SimpleContract: fift decompiler
> Packaging
> SimpleContract
> Bindings
> SimpleContract
> Reports
> SimpleContract
⚠️ Make sure to disable debug mode in contract wrappers before doing production deployments!
✅ Compiled successfully! Cell BOC result:
{
"hash": "8bb0916eb10debd617ebaba79be7097cc21e41597dc940d16af521dbed9dad25",
"hashBase64": "i7CRbrEN69YX66unm+cJfMIeQVl9yUDRavUh2+2drSU=",
"hex": "b5ee9c724102110100029f000114ff00f4a413f4bcf2c80b0102016202070298d001d0d3030171b0a301fa400120d74981010bbaf2e08820d70b0a208104ffbaf2d0898309baf2e088545053036f04f86102f862db3c59db3cf2e082c8f84301cc7f01ca000101cb1fc9ed54090301f6eda2edfb0192307fe07021d749c21f953020d70b1fde208210946a98b6ba8ea830d31f018210946a98b6baf2e081d33f0131c8018210aff90f5758cb1fcb3fc9f84201706ddb3c7fe0c0008e2af90182eb0d7299a100d9de4cf674453aeb6aa320067fc00702b62aa29243621d23217dba94a47fdb31e09130e27004013a6d6d226eb3995b206ef2d0806f22019132e2102470030480425023db3c0501cac87101ca01500701ca007001ca02500520d74981010bbaf2e08820d70b0a208104ffbaf2d0898309baf2e088cf165003fa027001ca68236eb3917f93246eb3e2973333017001ca00e30d216eb39c7f01ca0001206ef2d08001cc95317001ca00e2c901fb000600987f01ca00c87001ca007001ca00246eb39d7f01ca0004206ef2d0805004cc9634037001ca00e2246eb39d7f01ca0004206ef2d0805004cc9634037001ca00e27001ca00027f01ca0002c958cc020120080c020fbd10c6d9e6d9e18c090b013ced44d0d401f863d2000194d31f0131e030f828d70b0a8309baf2e089db3c0a0002700002200201200d0e00b9bbbd182705cec3d5d2cae7b1e84ec39d64a851b6682709dd6352d2b647cb322d3af2dfdf1623982702055c01b80676394ce583aae4725b2c382701bd49def954596f1c753d3de0559c32682709d974e5ab34ecb733a0e966d9466e8a480201480f100011b0afbb5134348000600075b26ee3435697066733a2f2f516d576165594177773744717159335651704e5136456232414146466d67416346323365383955655a7947327764820ab944fa3"
}
✅ Wrote compilation artifact to build/SimpleContract.compiled.json
2.4 Write tests for the contract
Your contract is only as good as it performs, and the TON Sandbox is the perfect tool for you to write tests and validate your contract’s deployment and interactions before the actual deployment. Here’s a sample test file you can use to do a TON smart contract test:
// ./tests/SimpleContract.spec.ts
import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox';
import { toNano } from '@ton/core';
import { SimpleContract } from '../wrappers/SimpleContract';
import '@ton/test-utils';
// On TON we can test by creating a virtual chain
describe('SimpleContract', () => {
let blockchain: Blockchain; // Init a virtual chain
let deployer: SandboxContract<TreasuryContract>;
let simpleContract: SandboxContract<SimpleContract>; // Init the smart contract instance
const contractId = 1648n; // Id for deployment that will be passed in the contructor. Random value in this example
beforeEach(async () => {
blockchain = await Blockchain.create();
simpleContract = blockchain.openContract(await SimpleContract.fromInit(contractId));
// Init the deployer. It comes with 1M TON tokens
deployer = await blockchain.treasury('deployer');
const deployResult = await simpleContract.send(
deployer.getSender(),
{
value: toNano('0.05'), // Value to send to the contract
},
{
$$type: 'Deploy', // This because the contract inherits the Deployable trait.
queryId: 0n,
},
);
// Here is the test. In this case it tests that the contract is deployed correctly.
expect(deployResult.transactions).toHaveTransaction({
from: deployer.address,
to: simpleContract.address,
deploy: true,
success: true,
});
});
it('should deploy', async () => {
// the check is done inside beforeEach
// blockchain and simpleContract are ready to use
console.log('Deploying contract...');
const conttactId = await simpleContract.getId();
console.log(`Fetched ID during deployment: ${conttactId}`);
});
it('should increase', async () => {
console.log('Testing increase by 1 function...');
const counterBefore = await simpleContract.getCounter();
console.log('counterBefore - ', counterBefore);
await simpleContract.send(
deployer.getSender(),
{
value: toNano('0.02'),
},
'add 1', // The message the contract expects
);
const counterAfter = await simpleContract.getCounter();
console.log('counterAfter - ', counterAfter);
// Check it incremented the value
expect(counterBefore).toBeLessThan(counterAfter);
});
it('should save the amount', async () => {
console.log('Testing increase by given value function...');
const numeberBefore = await simpleContract.getNumber();
const amount = 10n;
console.log(`Value to save: ${amount}`);
console.log(`Number saved before: ${numeberBefore}`);
await simpleContract.send(
deployer.getSender(),
{
value: toNano('0.02'),
},
{
$$type: 'Save', // This time it's an object and not just text
amount: amount,
},
);
const numberAfter = await simpleContract.getNumber();
console.log(`Number saved after: ${numberAfter}`);
});
});
Let’s get a better understanding of how the test and its components work:
Imports
- The necessary modules and utilities are imported from TON Sandbox, core libraries, and the SimpleContract wrapper.
- These imports allow interaction with the virtual chain and smart contracts in a test environment.
Test suite initialization (describe block)
- The test suite is defined inside the
describe
block, which initializes key variables:- blockchain: Represents a virtual chain used to test contract behavior.
- deployer: A sandbox instance representing the contract deployer with 1M TON tokens for testing purposes.
- simpleContract: The instance of the SimpleContract being tested.
beforeEach hook
- The
beforeEach
hook is executed before each individual test to set up the testing environment:- A virtual blockchain is initialized.
- The contract is deployed using the deployer account.
- A transaction is sent to deploy the contract, and the result is checked to ensure the deployment was successful.
Test 1: Deployment verification
- This test checks whether the contract is deployed correctly.
- It retrieves the contract ID after deployment and logs the ID for verification.
- The actual deployment is validated in the
beforeEach
hook.
Test 2: Counter increment functionality
- This test verifies that the
add 1
message increments thecounter
in the contract. - It checks the
counter
value before and after the message is sent, ensuring that the value increases as expected.
Test 3: Saving a number in the contract
- This test validates the functionality of the
Save
message. - It sends a message to store a specified amount in the
savedNumber
variable, and then verifies that the value was correctly updated in the contract.
Running the tests
- All tests can be run using the Blueprint SDK by executing the test command:
bash Copy code npx blueprint test
2.5 Deploy the TON smart contract to testnet
The Blueprint SDK provides built-in endpoints for deploying contracts on both mainnet and testnet. However, to improve performance and reliability, it’s recommended to use your Chainstack endpoint instead. To configure this:
- Create a new configuration file named
blueprint.config.ts
in the project’s root directory. - Set up the network settings in this file by specifying the Chainstack endpoint, along with the network type (testnet or mainnet) and the version.
import { Config } from '@ton/blueprint';
export const config: Config = {
network: {
endpoint: 'YOUR_CHAINSTACK_ENDPOINT',
type: 'testnet',
version: 'v4',
//key: 'YOUR_API_KEY',
},
};
After configuring the custom endpoint, update the deployment script to include the contract ID. This ID can be generated randomly or customized based on the specific contract address generated during deployment.
// ./scripts/deploySimpleContract.ts
import { toNano } from '@ton/core';
import { SimpleContract } from '../wrappers/SimpleContract';
import { NetworkProvider } from '@ton/blueprint';
export async function run(provider: NetworkProvider) {
// Edit this ID
const contractId = 1648n;
const simpleContract = provider.open(await SimpleContract.fromInit(contractId));
await simpleContract.send(
provider.sender(),
{
value: toNano('0.5'),
},
{
$$type: 'Deploy',
queryId: 0n,
},
);
// Deploy contract
await provider.waitForDeploy(simpleContract.address);
console.log(`Deployed at address ${simpleContract.address}`);
// run methods on `simpleContract`
}
Before you run the script, however, do add the your wallet’s mnemonic phrase and version to your environment variables:
export WALLET_MNEMONIC=""
export WALLET_VERSION="v4"
Great! Now it’s time to run your script and deploy your TON smart contract:
npx blueprint run
The response should look like this:
Using file: deploySimpleContract
? Which network do you want to use? testnet
? Which wallet are you using? Mnemonic
Connected to wallet at address: EQDrNXDLYKstXHj5xV6_md1nYvvrb6y6v4bFyTZReZ-vFYdx
Sent transaction
Contract deployed at address EQDVoYZ96Gtc-nQM0U4-rj0mporVOTlSpmB64Tn6HJax98VN
You can view it at <https://testnet.tonscan.org/address/EQDVoYZ96Gtc-nQM0U4-rj0mporVOTlSpmB64Tn6HJax98VN>
Deployed at address EQDVoYZ96Gtc-nQM0U4-rj0mporVOTlSpmB64Tn6HJax98VN
2.6 How to interact with the deployed TON smart contract?
Calling a method in your contract executes an interaction, whether it’s incrementing the counter or saving a number. Reading the state can confirm the changes. Writing a script at this stage to interact with the contract on the testnet allows you to observe the contract’s behavior and ensure it’s performing as expected:
// ./scripts/getCounter.ts
import { SimpleContract } from '../wrappers/SimpleContract';
import { NetworkProvider } from '@ton/blueprint';
export async function run(provider: NetworkProvider) {
const contractId = 1648n; // Random in this case
const simpleContract = provider.open(await SimpleContract.fromInit(contractId));
const id = await simpleContract.getId();
const savedNumber = await simpleContract.getNumber();
const counter = await simpleContract.getCounter();
console.log(`Fethching smart contract data...`);
console.log(`Contract ID: ${id}`);
console.log(`Current saved number: ${savedNumber}`);
console.log(`Current counter: ${counter}`);
}
After running it via npx blueprint run
your result will follow this pattern:
? Choose file to use
? Choose file to use getCounter
? Which network do you want to use?
? Which network do you want to use? testnet
? Which wallet are you using?
? Which wallet are you using? Mnemonic
Connected to wallet at address: EQDrNXDLYKstXHj5xV6_md1nYvvrb6y6v4bFyTZReZ-vFYdx
Fethching smart contract data...
Contract ID: 1648
Current counter: 0
You can then use your wallet to commit a transaction with the message “Save” and some TON token to save the value with this script:
// ./scripts/getCounter.ts
import { toNano } from '@ton/core';
import { SimpleContract } from '../wrappers/SimpleContract';
import { NetworkProvider } from '@ton/blueprint';
export async function run(provider: NetworkProvider) {
const contractId = 1648n;
const simpleContract = provider.open(await SimpleContract.fromInit(contractId));
const id = await simpleContract.getId();
const counter = await simpleContract.getNumber();
console.log(`Sending increasing value...`);
console.log(`Contract ID: ${id}`);
console.log(`Current counter: ${counter}`);
// Call the Add function and add 7
await simpleContract.send(provider.sender(), { value: toNano('0.02') }, { $$type: 'Save', amount: 7n });
}
That’s it! You’ve now successfully deployed a TON smart contract and interacted with it.
How to develop TON fungible tokens (Jettons)?
With a solid foundation in TON smart contract development, let’s now explore Jettons—the unique fungible tokens on the TON Network. As a Web3 developer, you not only need to understand what these tokens are, but also how to effectively create and manage them.
3.1 What are TON Jettons?
On TON, fungible tokens are known as Jettons, and they follow a specific standard outlined in TEP74. This standard defines the key mechanisms for Jetton transfers and how to access essential information such as the token’s name, circulating supply, and other common metadata. The metadata specification for Jettons is detailed in TEP64.
Jettons are structured around two key contract types:
- Master smart contract (Jetton Minter): Responsible for minting new Jettons, managing the total supply, and providing shared information about the token.
- Jetton-wallet contracts: These individual smart contracts hold the token balance for each user in a decentralized manner, ensuring that each owner has a unique Jetton wallet.
The TON core team has provided example contracts for the Jetton minter and wallet, which serve as the foundation for further development.
3.2 Get started with TON Jetton smart contracts
To start creating Jettons, you can set up a new Blueprint project or extend your existing one. This involves adding both the Jetton minter and wallet contracts to your contracts/
folder.
Jetton Minter and Wallet contracts serve different purposes in the Jetton ecosystem. The Jetton Minter contract takes charge of creating tokens and managing the total supply. On the other hand, the Jetton Wallet contract is assigned to each user and takes care of user balances and token transactions.
First run npm create ton@latest .
in your CLI, then create a blueprint.config.ts
file in your project root, if you haven’t done that in the previous steps already:
import { Config } from '@ton/blueprint';
export const config: Config = {
network: {
endpoint: '<https://ton-testnet.core.chainstack.com/.../api/v2/jsonRPC>',
type: 'testnet',
version: 'v2',
// key: 'YOUR_API_KEY',
},
};
Next, copy the necessary contracts from the standard implementation into your project’s contracts
folder. You will need the following files: jetton-minter-discoverable.fc
, jetton-wallet.fc
, jetton-utils.fc
, discovery-params.fc
, op-codes.fc
, and params.fc
. Here’s a brief overview of their roles:
- The minter contract allows the admin (contract owner) to control the minting process and manage the total token supply. It also implements the TEP89 feature, which simplifies the discovery of Jetton wallet addresses by other smart contracts. This is an important feature because, previously, contracts couldn’t directly access methods from other contracts.
- The wallet contract manages the transfer, storage, and burning of Jettons for individual users. It keeps track of the wallet balance, the owner’s address, and information about the Jetton minter contract, updating these details after any transfers or burns.
- The jetton-utils.fc file provides essential functions for creating and managing Jetton wallets. It ensures that all necessary data is packed and calculates the correct wallet address for each user.
- The discovery-params.fc and op-code.fc files contain standard operational codes for the minter and wallet contracts. Additionally, they include the
is_resolvable
function, which checks whether an address is within the same workchain as the contract by comparing the workchain ID from the address with that of the contract. These checks are also included inparams.fc
.
Ensure your contracts have the necessary imports before proceeding.
// ./contracts/jetton-minter-discoverable.fc
#include "imports/stdlib.fc";
#include "jetton-utils.fc";
#include "discovery-params.fc";
#include "op-codes.fc";
For the wallet contract:
// ./contracts/jetton-wallet.fc
#include "jetton-utils.fc";
#include "op-codes.fc";
For utility functions:
// ./contracts/jetton-utils.fc
#include "params.fc";
And for contract parameters:
// ./contracts/params.fc
#include "imports/stdlib.fc";
3.3 Compile and build the TON Jetton smart contracts
Just like with TON smart contracts, your Jetton contracts must also be compiled using the Blueprint SDK. In the wrappers
folder, create or update the compilation files:
// ./wrappers/JettonMinter.compile.ts
import { CompilerConfig } from '@ton/blueprint';
export const compile: CompilerConfig = {
lang: 'func',
targets: [
'contracts/jetton-minter-discoverable.fc'
],
};
For the Jetton wallet:
// ./wrappers/JettonWallet.compile.ts
import { CompilerConfig } from '@ton/blueprint';
export const compile: CompilerConfig = {
lang: 'func',
targets: [
'contracts/jetton-wallet.fc'
],
};
Finally, run the build command:
npx blueprint build
3.4 Create wrappers for Jettons
To run scripts and tests for the smart contracts, you’ll need to create TypeScript interfaces. These interfaces are contained in the wrappers
folder and implement the Contract class from @ton/core
. They also include serialization functions, getter wrappers, and compilation functions that streamline the interaction with your smart contracts.
To set this up, copy the following files into your project’s wrappers
folder: JettonMinter.ts
, JettonWallet.ts
, JettonConstants.ts
, and ui-utils.ts
. If you prefer, you can use shortened versions of these files from an existing repository. Note that ui-utils.ts
contains some additional adjustments for usability.
The Blueprint framework integrates Sandbox, a tool that allows developers to simulate the behavior of smart contracts as if they were deployed on a real blockchain. You can use this tool to test contracts within a controlled environment. Copy the test scripts from our repository, similar to how you copied the wrappers. These tests have been shortened and adjusted based on examples provided by the TON core team.
To run the tests using Sandbox, execute the following command:
npx blueprint test
3.5 Deploy the TON Jetton smart contracts
In the scripts
folder, create a file called deployJettonMinter.ts
. This script will handle the deployment of your Jetton minter contract. The following code sets up the deployment process:
// ./scripts/deployJettonMinter.ts
import {toNano} from '@ton/core';
import {JettonMinter} from '../wrappers/JettonMinter';
import {compile, NetworkProvider} from '@ton/blueprint';
import {jettonWalletCodeFromLibrary, promptUrl, promptUserFriendlyAddress} from "../wrappers/ui-utils";
export async function run(provider: NetworkProvider) {
const isTestnet = provider.network() !== 'mainnet';
const ui = provider.ui();
const jettonWalletCodeRaw = await compile('JettonWallet');
const adminAddress = await promptUserFriendlyAddress("Enter the address of the jetton owner (admin):", ui, isTestnet);
const jettonMetadataUri = await promptUrl("Enter jetton metadata uri (<https://jettonowner.com/jetton.json>)", ui)
const jettonWalletCode = jettonWalletCodeFromLibrary(jettonWalletCodeRaw);
const minter = provider.open(JettonMinter.createFromConfig({
admin: adminAddress.address,
wallet_code: jettonWalletCode,
jetton_content: {type: 1, uri: jettonMetadataUri}
},
await compile('JettonMinter'))
);
await minter.sendDeploy(provider.sender(), toNano("1.5")); // send 1.5 TON
}
The metadata for the Jetton token must be provided in JSON format. The JSON should contain details such as the token name, description, symbol, decimals, and base64-encoded image data. Here’s an example:
{
"name": "Example Token",
"description": "Official token",
"symbol": "EXTO",
"decimals": 9,
"image_data": "4bWxuczPHN2ZyB0..."
}
With that out of the way, you are ready to deploy your Jetton contracts to the TON testnet. To do that run the following command:
npx blueprint run
This will start the deployment process, and once complete, your Jetton minter contract will be live on the TON testnet.
How to customize TON fungible tokens (Jettons)?
Developing fungible tokens in the form of Jettons is only part of the exciting journey on the TON Blockchain. As a Web3 developer working with Chainstack, you’ll be creating far more than standard tokens. You’ll be customizing those tokens to have unique features such as a capped supply and minting price. Here’s how to do it:
4.1 Modify the TON Jetton minter smart contract
The customization of your Jettons in this tutorial involves adding a capped supply and minting price. Capping supply limits the total number of tokens that can go into circulation, and the mint price ensures users pay a specific amount of TON for each Jetton minted.
To add these features, you’ll need to update the storage of the Jetton minter contract to include capped_supply
and price
fields:
(int, int, int, slice, cell, cell) load_data() inline {
slice ds = get_data().begin_parse();
return (
ds~load_coins(), ;; total_supply
ds~load_coins(), ;; capped_supply
ds~load_uint(32), ;; price
ds~load_msg_addr(), ;; admin_address
ds~load_ref(), ;; content
ds~load_ref() ;; jetton_wallet_code
);
}
() save_data(int total_supply, int capped_supply, int price, slice admin_address, cell content, cell jetton_wallet_code) impure inline {
set_data(begin_cell()
.store_coins(total_supply)
.store_coins(capped_supply)
.store_uint(price, 32)
.store_slice(admin_address)
.store_ref(content)
.store_ref(jetton_wallet_code)
.end_cell()
);
}
You’ll also need to add getter methods to fetch this data from the contract at any time:
(int, int, int, slice, cell, cell) get_jetton_data() method_id {
(int total_supply, int capped_supply, int price, slice admin_address, cell content, cell jetton_wallet_code) = load_data();
return (total_supply, capped_supply, price, admin_address, content, jetton_wallet_code);
}
slice get_wallet_address(slice owner_address) method_id {
(_, _, _, _, _, cell jetton_wallet_code) = load_data();
return calculate_user_jetton_wallet_address(owner_address, my_address(), jetton_wallet_code);
}
int get_token_price() method_id {
(_, _, int price, _, _, _) = load_data();
return price;
Your minting logic should be tweaked to ensure no minting occurs once the capped supply is reached. Further, you’ll need to compute exactly how many Jettons a user gets for the TON sent, factoring in the price per Jetton:
if (op == op::mint()) {
slice to_address = in_msg_body~load_msg_addr();
int buy_amount = msg_value - min_tons_for_storage;
int jetton_amount = muldiv(buy_amount, 1, price);
;; Check if minting exceeds the capped supply
throw_unless(256, total_supply + jetton_amount <= capped_supply);
var mint_request = begin_cell()
.store_uint(op::internal_transfer(), 32)
.store_uint(0, 64)
.store_coins(jetton_amount) ;; max 124 bit
.store_uint(0, 2) ;; from_address, addr_none$00
.store_slice(my_address()) ;; response_address, 3 + 8 + 256 = 267 bit
.store_coins(0) ;; forward_amount, 4 bit if zero
.store_uint(0, 1) ;; no forward_payload, 1 bit
.end_cell();
mint_tokens(to_address, jetton_wallet_code, min_tons_for_storage, mint_request);
save_data(total_supply + jetton_amount, capped_supply, price, admin_address, content, jetton_wallet_code);
return ();
}
4.2 Update the TON Jetton wrappers
In line with these changes, update your TypeScript wrappers for the Jetton contracts. They should now allow interaction with the capped supply and mint price, and gracefully handle any errors that arise if the capped supply is exceeded.
First, you’ll need to update the configuration in JettonMinter.ts
to include two new fields: capped_supply
and price
. This will allow the minter contract to track the maximum token supply and set a price for minting new tokens.
export type JettonMinterConfig = {
admin: Address;
jetton_content: Cell | JettonMinterContent;
wallet_code: Cell;
capped_supply: bigint;
price: bigint;
};
export function jettonMinterConfigToCell(config: JettonMinterConfig): Cell {
const content = config.jetton_content instanceof Cell ? config.jetton_content : jettonContentToCell(config.jetton_content);
return beginCell()
.storeCoins(0)
.storeCoins(config.capped_supply)
.storeUint(config.price, 32)
.storeAddress(config.admin)
.storeRef(content)
.storeRef(config.wallet_code)
.endCell();
}
This updated configuration ensures that both the capped supply and price are stored and initialized during contract deployment.
Then as the next, embed the minting logic within the minter contract itself. The interface will now creates a simple minting message containing the necessary TON amount and the recipient’s address.
static mintMessage(from: Address, to: Address, query_id: number | bigint = 0) {
return beginCell().storeUint(Op.mint, 32).storeUint(query_id, 64) // op, queryId
.storeAddress(to)
.endCell();
}
async sendMint(provider: ContractProvider, via: Sender, to: Address, forward_ton_amount: bigint, total_ton_amount: bigint) {
if(total_ton_amount < forward_ton_amount) {
throw new Error("Total ton amount should be > forward amount");
}
await provider.internal(via, {
sendMode: SendMode.PAY_GAS_SEPARATELY,
body: JettonMinter.mintMessage(this.address, to),
value: total_ton_amount
});
}
This structure ensures that minting new Jettons is handled by sending a pre-packaged message that specifies the necessary details.
Since you’ve added new properties, the getter functions also need to be updated. The following methods retrieve information about the total supply, capped supply, and token price, and they include the newly added fields.
async getJettonData(provider: ContractProvider) {
const res = await provider.get('get_jetton_data', []);
const totalSupply = res.stack.readBigNumber();
const cappedSupply = res.stack.readBigNumber();
const price = res.stack.readBigNumber();
const adminAddress = res.stack.readAddress();
const content = res.stack.readCell();
const walletCode = res.stack.readCell();
return {
totalSupply,
cappedSupply,
price,
adminAddress,
content,
walletCode
};
}
async getWalletAddress(provider: ContractProvider, owner: Address): Promise<Address> {
const res = await provider.get('get_wallet_address', [{
type: 'slice',
cell: beginCell().storeAddress(owner).endCell()
}])
return res.stack.readAddress();
}
async getTokenPrice(provider: ContractProvider): Promise<BigInt> {
const res = await provider.get('get_token_price', []);
return res.stack.readBigNumber();
}
These updated getters allow you to retrieve and interact with the new properties, such as the token price and capped supply, making it easier to manage Jetton data.
4.3 Test the Jettons customization
Now it’s time to go back to the Sandbox and test your newly customized Jettons. Ensure your tests validate that the capped supply and mint price work correctly during minting. As always, make sure to cover scenarios where the total supply exceeds the cap or improper amounts of TON are sent for minting.
This first example test checks whether Jettons are minted correctly based on the amount of TON sent. It calculates the number of Jettons to be purchased, factoring in the costs, including storage fees. The initial Jetton balance and total supply are retrieved, and the minting message is sent:
it('should mint correct amount of jettons based on the sent TON amount', async () => {
// Calculate costs of minting
const jettonsToPurchase = (await jettonMinter.getJettonData()).cappedSupply;
const jettonsCost = jettonsToPurchase * price;
const amountToSend = jettonsCost + toNano('1'); // Assuming 1 TON for storage fees
const forwardFee = toNano('0.01');
const expectedMintedJettons = jettonsCost / price;
// Retrieve initial balance and supply
const userJettonWallet = await userWallet(user.address);
const initUserJettonBalance = await userJettonWallet.getJettonBalance();
const initJettonSupply = (await jettonMinter.getJettonData()).totalSupply;
// Send the minting message
const res = await jettonMinter.sendMint(
user.getSender(),
user.address,
forwardFee,
amountToSend
);
// Verify the transaction
expect(res.transactions).toHaveTransaction({
on: userJettonWallet.address,
op: Op.internal_transfer,
success: true,
deploy: true
});
// Verify that the user's minted jettons match the expected amount
const currentUserJettonBalance = await userJettonWallet.getJettonBalance();
const mintedUserJettons = currentUserJettonBalance - initUserJettonBalance;
expect(mintedUserJettons).toEqual(expectedMintedJettons);
// Verify that the total supply matches the expected amount of minted jettons
const updatedTotalSupply = (await jettonMinter.getJettonData()).totalSupply;
const mintedTotalSupply = updatedTotalSupply - initJettonSupply;
expect(mintedTotalSupply).toEqual(expectedMintedJettons);
printTransactionFees(res.transactions);
});
After the transaction is complete, the test verifies that the correct amount of Jettons has been minted, checking both the user’s balance and the total supply. Additionally, the transaction details are validated, ensuring that the internal transfer was successful and the deployment of the user wallet occurred.
As the second example test, the goal is to check that the minting process does not exceed the capped supply. It calculates the amount of Jettons to purchase beyond the cap, along with the necessary costs:
it('should not mint more than capped supply', async () => {
// Calculate costs of minting
const jettonsToPurchase = (await jettonMinter.getJettonData()).cappedSupply + 1n;
const jettonsCost = jettonsToPurchase * price;
const amountToSend = jettonsCost + toNano('1'); // Assuming 1 TON for storage fees
const forwardFee = toNano('0.01');
// Send the minting message
const res = await jettonMinter.sendMint(
user.getSender(),
user.address,
forwardFee,
amountToSend
);
// Verify the transaction
expect(res.transactions).toHaveTransaction({
from: user.address,
to: jettonMinter.address,
aborted: true, // High exit codes are considered to be fatal
exitCode: 256,
});
});
The test then attempts to send the minting message and verifies that the transaction is aborted due to exceeding the capped supply. The transaction is checked for the correct exit code to ensure that it failed as expected.
4.4 Deploy the customized TON Jettons
With your tests successful, you are ready to deploy this customization into the TON testnet. Deploy the Jetton minter contract that now includes capped supply and mint price. Once deployed, you can mint Jettons and check whether your rules are enforced correctly by interacting with the Jettons contracts:
import {toNano} from '@ton/core';
import {JettonMinter} from '../wrappers/JettonMinter';
import {compile, NetworkProvider} from '@ton/blueprint';
import {jettonWalletCodeFromLibrary, promptUrl, promptUserFriendlyAddress} from "../wrappers/ui-utils";
export async function run(provider: NetworkProvider) {
const isTestnet = provider.network() !== 'mainnet';
const ui = provider.ui();
const jettonWalletCodeRaw = await compile('JettonWallet');
const adminAddress = await promptUserFriendlyAddress("Enter the address of the jetton owner (admin):", ui, isTestnet);
const jettonMetadataUri = await promptUrl("Enter jetton metadata uri (<https://jettonowner.com/jetton.json>)", ui)
const jettonWalletCode = jettonWalletCodeFromLibrary(jettonWalletCodeRaw);
const minter = provider.open(JettonMinter.createFromConfig({
admin: adminAddress.address,
wallet_code: jettonWalletCode,
jetton_content: {type: 1, uri: jettonMetadataUri},
capped_supply: 1000n,
price: toNano('0.01')
},
await compile('JettonMinter'))
);
await minter.sendDeploy(provider.sender(), toNano("1.5")); // send 1.5 TON
}
Further reading
Expand your TON knowledge and development skills with these comprehensive Chainstack resources:
- TON tool suite: Learn how to interact with your Chainstack TON node and develop DApps.
- TON glossary: Get a better understanding of key TON terminology and its definitions.
- TON master guide to custom wallet RPC endpoints: Learn how to customize TON wallet RPC endpoints and compare the leading provider options.
- TON ultimate guide to APIs and interaction libraries: Explore the full range of TON APIs and interaction libraries available to developers with this guide.
- How to deploy a TON smart contract: Step-by-step guide to deploying a smart contract on TON, including setting up the environment and interacting with your deployed contract.
- How to develop TON fungible tokens (Jettons): Learn how to create and deploy fungible tokens on the TON network, covering the key components like the minter and wallet contracts.
- How to customize TON fungible tokens (Jettons): Discover how to add custom functionality to Jettons, including setting capped supply and token pricing.
- The ultimate guide to building DApps on Chainstack: Explore our ultimate guide covering protocol selection, API authentication, error handling, and more.
- Make your DApp more reliable with Chainstack: Learn how to use multiple Chainstack nodes with load balancer logic to make your DApp more performant and reliable.
Bringing it all together
Congratulations! You’ve successfully traversed an educational journey from deploying a smart contract on the TON Network, developing Jetton tokens, to customizing them with advanced functionality like capped supply and mint price. With the knowledge you’ve gained, you’re ready to build, test, and deploy complex DApps on the TON Network using Chainstack, Blueprint SDK, and the exclusive capabilities of the ecosystem.
This detailed guide aimed to give you a structured, step-by-step approach to mastering TON development from basic smart contracts to fully customized fungible tokens. As you continue to explore and develop on TON, remember that Chainstack is with you every step of the way, ready to empower your blockchain adventures with our powerful and easy-to-use node infrastructure.
Power-boost your project on Chainstack
- Discover how you can save thousands in infra costs every month with our unbeatable pricing on the most complete Web3 development platform.
- Input your workload and see how affordable Chainstack is compared to other RPC providers.
- Connect to Ethereum, Solana, BNB Smart Chain, Polygon, Arbitrum, Base, Optimism, Avalanche, TON, Ronin, zkSync Era, Starknet, Scroll, Aptos, Fantom, Cronos, Gnosis Chain, Klaytn, Moonbeam, Celo, Aurora, Oasis Sapphire, Polygon zkEVM, Bitcoin, Tezos and Harmony mainnet or testnets through an interface designed to help you get the job done.
- To learn more about Chainstack, visit our Developer Portal or join our Discord server and Telegram group.
- Are you in need of testnet tokens? Request some from our faucets. Multi-chain faucet, Sepolia faucet, Holesky faucet, BNB faucet, zkSync faucet, Scroll faucet.
Have you already explored what you can achieve with Chainstack? Get started for free today.