Sending messages
After authenticating users with the optional ton_proof
verification, TON Connect also allows sending outgoing messages via connected wallets.
You will understand:
- how to send messages from the DApp to the blockchain
- how to send multiple messages in one transaction
- how to deploy a contract using TON Connect
Before sending any transactions you must establish the connection. The examples below assume that such a connection has already been created and successfully verified.
In all the examples below we use @tonconnect/react-ui
and the vanilla @tonconnect/ui
libraries.
Additionally, every snippet shows how to bootstrap assets-sdk
so you can immediately work with jettons, NFTs and other on-chain assets.
Quick start
Add Ton Connect UI to your project (React or plain JS) as described under the Install TON Connect section.
Here is a minimal page you can paste into .html
file and open in your browser to play with.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>TON Connect UI — minimal demo</title>
<!-- TON Connect UI -->
<script src="https://unpkg.com/@tonconnect/ui@latest/dist/tonconnect-ui.min.js"></script>
<!-- TON Core library -->
<script type="module">
import * as TonCore from 'https://esm.sh/@ton/core';
window.ton = TonCore;
// Buffer polyfill for browsers (needed for comment payload)
import { Buffer } from 'https://esm.sh/buffer';
window.Buffer = Buffer;
</script>
<style>
body {font-family: Arial, sans-serif; background:#10161F; color:#FFF; margin:0; padding:20px;}
button {margin:10px 0; padding:8px 14px; border:0; border-radius:8px; cursor:pointer;
background:#66AAEE; color:#FFF; font-size:15px;}
#log {white-space:pre-wrap; margin-top:20px; font-family:monospace;}
</style>
</head>
<body>
<!-- The connect-wallet button will be injected here -->
<div id="ton-connect"></div>
<!-- Action buttons -->
<button id="btn-multi">Send 0.2 TON to self + 0.1 TON x2 to another address</button><br>
<button id="btn-comment">Send 0.2 TON with comment</button>
<div id="log"></div>
<script>
/***** 1. Initialise TON Connect UI *****/
const tonConnectUI = new TON_CONNECT_UI.TonConnectUI({
manifestUrl: 'https://ton-connect.github.io/demo-dapp-with-react-ui/tonconnect-manifest.json',
buttonRootId: 'ton-connect' // the element where the wallet button is injected
});
const log = msg => document.getElementById('log').textContent = msg;
// additional code will be added here
// ...
// ...
</script>
</body>
</html>
Sending multiple messages
Understanding a task
We will send two messages in one transaction: one to your address, carrying 0.2 TON, and one to the other wallet address, carrying 0.1 TON.
By the way, there is a limit to the number of messages sent in one transaction:
- standard (v3/v4) wallets: 4 outgoing messages;
- highload wallets: 255 outgoing messages (close to blockchain limitations).
Sending the messages
Paste the following code and run it by clicking the button:
document.getElementById('btn-multi').onclick = async () => {
const tx = {
validUntil: Math.floor(Date.now() / 1000) + 360,
messages: [
{
address: tonConnectUI.account.address,
amount: '200000000' // 0.2 TON (in nanotons)
},
{
address: 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c',
amount: '100000000'
},
{
address: 'UQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJKZ',
amount: '100000000'
}
]
};
try {
const result = await tonConnectUI.sendTransaction(tx);
log('Multi-message tx sent!\n' + JSON.stringify(result, null, 2));
} catch (e) {
log('Error: ' + e);
}
};
Open your wallet application. There is a request showing what you are sending and where the coins would go. Please, accept it.
Getting the result
The function will exit, and the output from the blockchain will be printed:
{
boc: "te6ccgEBBAEA5QABRYgA0XgCE4CyoItLqaTNoE767Ld3ED2SWNt2oQ//+B+F5LgMAQKeRt7mE4Tv4WfY/kR062A1F7kTD2LdPWR7gp+Se/XEXjqSIY4V9iiFmakjtKGG7Tbk1iiQmtCnB+jvIMsFtL2nCCmpoxdoeRALAAAAcgADAwIDAGhiADReAITgLKgi0uppM2gTvrst3cQPZJY23ahD//4H4XkuIF9eEAAAAAAAAAAAAAAAAAAAAGhiAFlQ9nqqLwO2abVyi3U/XvmVdQBGVRXCV8wzX2WQRErWoC+vCAAAAAAAAAAAAAAAAAAA"
}
BoC is bag of cells, the way data is stored in TON. Now, we can decode it.
Decode this BoC in the tool of your choice, for example boc-printer, and you'll get the following tree of cells:
x{8800D178021380B2A08B4BA9A4CDA04EFAECB777103D9258DB76A10FFFF81F85E4B80C_}
x{46DEE61384EFE167D8FE4474EB603517B9130F62DD3D647B829F927BF5C45E3A92218E15F6288599A923B4A186ED36E4D628909AD0A707E8EF20CB05B4BDA70829A9A3176879100B00000072000303}
x{6200345E0084E02CA822D2EA69336813BEBB2DDDC40F649636DDA843FFFE07E1792E205F5E100000000000000000000000000000}
x{62005950F67AAA2F03B669B5728B753F5EF9957500465515C257CC335F6590444AD6A02FAF080000000000000000000000000000}
This is a serialized external message, internal message and two references are outgoing messages representations.
x{88016543D9EAA8BC0ED9A6D5CA2DD4FD7BE655D401195457095F30CD7D964111...
$10 ext_in_msg_info
$00 src:MsgAddressExt (null address)
"EQ..."a dest:MsgAddressInt (your wallet)
0 import_fee:Grams
$0 (no state_init)
$1 (body starts in ref cell)
...
Using TL-B you can convert the BoC to a human-readable format:
kind: MessageAny
anon0:
kind: Message
info:
kind: CommonMsgInfo_ext_in_msg_info
src: null
dest: EQBovAEJwFlQRaXU0mbQJ312W7uIHsksbbtQh__8D8LyXB77
import_fee: "0"
init:
kind: Maybe_nothing
body:
kind: Either_right
value: b5ee9c724101030100bf00029e46dee61384efe167d8fe4474eb603517b9130f62dd3d647b829f927bf5c45e3a92218e15f6288599a923b4a186ed36e4d628909ad0a707e8ef20cb05b4bda70829a9a3176879100b00000072000303010200686200345e0084e02ca822d2ea69336813bebb2dddc40f649636dda843fffe07e1792e205f5e100000000000000000000000000000006862005950f67aaa2f03b669b5728b753f5ef9957500465515c257cc335f6590444ad6a02faf0800000000000000000000000000002998243e
Returning the transaction's BoC allows developers to track the sent transaction.
Processing transactions initiated with TON Connect
To find a transaction by extInMsg
, you need to do the following:
- Parse the received
extInMsg
as a cell. - Calculate the
hash()
of the obtained cell.
The received hash is what the sendBocReturnHash
methods of TON Center API are already returning to you.
-
Search for the required transaction using this hash through an indexer:
- TON Center api_v3_transactionsByMessage_get.
- TON API
/v2/blockchain/messages/{msg_id}/transaction
endpoint. - Collect transactions independently and search for the required
extInMsg
by its hash: see example.
It's important to note that extInMsg
may not be unique, which means collisions can occur. However, all transactions are unique.
If you are using this for an informative display, this method should be sufficient. With standard wallet contracts, collisions can occur only in exceptional situations.
Sending complex transactions
Serialization of cells
Before we proceed, let's talk about the format of the messages we will send.
- payload (string base64, optional): raw one-cell BoC encoded in Base64 – we will use it to store text comments on a transfer.
- stateInit (string base64, optional): raw one-cell BoC encoded in Base64 – we will use it to deploy a smart contract.
After building a message, you can serialise it into a BoC:
const payload = payloadCell.toBoc().toString('base64');
console.log(payload);
Transfer with comment
You can use the modern @ton/ton
SDK (used across this documentation) or any other tool that can serialise cells to BoC.
Text comments on transfer are encoded as opcode 0 (32 zero bits) + UTF-8 bytes of the comment.
Here's an example for our minimal page:
document.getElementById('btn-comment').onclick = async () => {
const { beginCell } = ton;
const commentBody = beginCell()
.storeUint(0, 32)
.storeStringTail('Hello, TON!')
.endCell();
const tx = {
validUntil: Math.floor(Date.now() / 1000) + 360,
messages: [
{
address: tonConnectUI.account.address,
amount: '200000000',
payload: commentBody.toBoc().toString('base64')
}
]
};
try {
const result = await tonConnectUI.sendTransaction(tx);
log('Comment tx sent!\n' + JSON.stringify(result, null, 2));
} catch (e) {
log('Error: ' + e);
}
};
Smart contract deployment
We'll deploy an instance of the simple chatbot Doge smart contract, which is featured as one of the smart-contract examples. To create our own unique instance, we'll load the contract code and store a unique value in the data section. Finally, we'll combine the code and data into a stateInit
structure.
document.getElementById('btn-generate-contract').onclick = async () => {
const { Cell, beginCell, contractAddress } = ton;
// Doge contract code
const code = Cell.fromBase64('te6cckEBAgEARAABFP8A9KQT9LzyyAsBAGrTMAGCCGlJILmRMODQ0wMx+kAwi0ZG9nZYcCCAGMjLBVAEzxaARfoCE8tqEssfAc8WyXP7AN4uuM8=');
// Create unique data with current timestamp
const data = beginCell()
.storeUint(Math.floor(Date.now() / 1000), 64)
.endCell();
// Create state init
const stateInit = { code, data };
// Calculate contract address
const doge_address = contractAddress(0, stateInit).toString();
// Create state init BOC
const state_init_boc = beginCell()
.storeUint(6, 5)
.storeRef(code)
.storeRef(data)
.endCell()
.toBoc()
.toString('base64');
// Store for deployment
contractData = {
address: doge_address,
stateInit: state_init_boc
};
And now it's time to send our transaction:
document.getElementById('btn-deploy-contract').onclick = async () => {
// Deploy transaction
const transaction = {
validUntil: Math.floor(Date.now() / 1000) + 360,
messages: [
{
address: contractData.address,
amount: '69000000', // 0.069 TON
stateInit: contractData.stateInit
}
]
};
try {
const result = await tonConnectUI.sendTransaction(transaction);
log('Doge contract deployed!\n' + JSON.stringify(result, null, 2));
} catch (e) {
log('Error: ' + e);
}
};
See the full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Smart Contract Deployment Example</title>
<!-- TON Connect UI -->
<script src="https://unpkg.com/@tonconnect/ui@latest/dist/tonconnect-ui.min.js"></script>
<!-- TON Core library -->
<script type="module">
import * as TonCore from 'https://esm.sh/@ton/core'; // или нужную вам версию
// сделаем то, что пытались получить "автоматом": положим API в window
window.ton = TonCore;
// если нужен Buffer – подключаем полифилл
import { Buffer } from 'https://esm.sh/buffer';
window.Buffer = Buffer;
</script>
<style>
body {
font-family: Arial, sans-serif;
background: #10161F;
color: #FFF;
margin: 0;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
button {
margin: 10px 0;
padding: 12px 20px;
border: 0;
border-radius: 8px;
cursor: pointer;
background: #66AAEE;
color: #FFF;
font-size: 15px;
}
button:hover {
background: #5599DD;
}
button:disabled {
background: #444;
cursor: not-allowed;
}
.section {
margin: 20px 0;
padding: 20px;
background: #1A2332;
border-radius: 10px;
}
.code-output {
background: #0F1419;
padding: 15px;
border-radius: 5px;
font-family: monospace;
white-space: pre-wrap;
margin: 10px 0;
overflow-x: auto;
}
.status {
padding: 10px;
border-radius: 5px;
margin: 10px 0;
}
.status.success {
background: #2D5A2D;
color: #90EE90;
}
.status.error {
background: #5A2D2D;
color: #FF6B6B;
}
.status.info {
background: #2D4A5A;
color: #87CEEB;
}
#log {
white-space: pre-wrap;
margin-top: 20px;
font-family: monospace;
background: #0F1419;
padding: 15px;
border-radius: 5px;
max-height: 300px;
overflow-y: auto;
}
</style>
</head>
<body>
<h1>TON Connect Examples</h1>
<p>Complete examples of TON Connect functionality</p>
<!-- TON Connect -->
<div class="section">
<h2>1. Connect Wallet</h2>
<div id="ton-connect"></div>
<div id="wallet-info"></div>
</div>
<!-- Simple Transactions -->
<div class="section">
<h2>2. Simple Transactions</h2>
<button id="btn-multi">Send 0.2 TON to self + 0.1 TON x2 to another address</button>
<button id="btn-comment">Send 0.2 TON with comment</button>
<h3>Address Converter</h3>
<input type="text" id="address-input" placeholder="Enter TON address in any format" style="width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #444; border-radius: 5px; background: #2A3441; color: #FFF; font-size: 14px;">
<button id="btn-convert-address">Convert Address</button>
<div id="conversion-result"></div>
<div class="status info" style="margin-top: 15px;">
<strong>Address formats by network:</strong><br>
<strong>Mainnet (workchain 0):</strong><br>
- <strong>Bounceable (EQ...):</strong> Transaction bounces back if contract doesn't exist<br>
- <strong>Non-bounceable (UQ...):</strong> Transaction is final even if address doesn't exist<br>
<strong>Testnet (workchain 0):</strong><br>
- <strong>Bounceable (kQ...):</strong> At the moment, this does not work in testnet, use mainnet address<br>
- <strong>Non-bounceable (0Q...):</strong> Transaction is final even if address doesn't exist<br>
</div>
</div>
<!-- Contract Info -->
<div class="section">
<h2>3. Smart Contract Deployment</h2>
<button id="btn-generate-contract">Generate Contract Info</button>
<div id="contract-info"></div>
<button id="btn-deploy-contract" disabled>Deploy Doge Contract</button>
<div id="deploy-status"></div>
</div>
<!-- Transaction Log -->
<div class="section">
<h2>4. Transaction Log</h2>
<button id="btn-clear-log">Clear Log</button>
<div id="log"></div>
</div>
<script>
// Initialize TON Connect UI
const tonConnectUI = new TON_CONNECT_UI.TonConnectUI({
manifestUrl: 'https://ton-connect.github.io/demo-dapp-with-react-ui/tonconnect-manifest.json',
buttonRootId: 'ton-connect'
});
// Global variables for contract
let contractData = null;
// Logging function
const log = msg => {
const logElement = document.getElementById('log');
const timestamp = new Date().toLocaleTimeString();
logElement.textContent += `[${timestamp}] ${msg}\n`;
logElement.scrollTop = logElement.scrollHeight;
};
// Update UI when wallet connection changes
tonConnectUI.onStatusChange((wallet) => {
const walletInfo = document.getElementById('wallet-info');
const deployButton = document.getElementById('btn-deploy-contract');
if (wallet) {
walletInfo.innerHTML = `
`;
deployButton.disabled = !contractData;
log(`Wallet connected: ${wallet.account.address}`);
} else {
walletInfo.innerHTML = `
`;
deployButton.disabled = true;
log('Wallet disconnected');
}
});
// Multiple messages transaction
document.getElementById('btn-multi').onclick = async () => {
if (!tonConnectUI.account) {
alert('Please connect your wallet first!');
return;
}
const tx = {
validUntil: Math.floor(Date.now() / 1000) + 360,
messages: [
{
address: tonConnectUI.account.address,
amount: '200000000' // 0.2 TON (in nanotons)
},
{
address: 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c',
amount: '100000000'
},
{
address: 'UQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJKZ',
amount: '100000000'
}
]
};
try {
const result = await tonConnectUI.sendTransaction(tx);
log('Multi-message tx sent!\n' + JSON.stringify(result, null, 2));
} catch (e) {
log('Error: ' + e);
}
};
// Transfer with comment
document.getElementById('btn-comment').onclick = async () => {
if (!tonConnectUI.account) {
alert('Please connect your wallet first!');
return;
}
const { beginCell } = ton;
const commentBody = beginCell()
.storeUint(0, 32)
.storeStringTail('Hello, TON!')
.endCell();
const tx = {
validUntil: Math.floor(Date.now() / 1000) + 360,
messages: [
{
address: tonConnectUI.account.address,
amount: '200000000',
payload: commentBody.toBoc().toString('base64')
}
]
};
try {
const result = await tonConnectUI.sendTransaction(tx);
log('Comment tx sent!\n' + JSON.stringify(result, null, 2));
} catch (e) {
log('Error: ' + e);
}
};
// Convert address formats
document.getElementById('btn-convert-address').onclick = async () => {
const { Address } = ton;
const addressInput = document.getElementById('address-input').value.trim();
const resultDiv = document.getElementById('conversion-result');
if (!addressInput) {
resultDiv.innerHTML = `
`;
return;
}
try {
const address = Address.parse(addressInput);
const testnetBounceable = address.toString({ bounceable: true, testOnly: true });
const testnetNonBounceable = address.toString({ bounceable: false, testOnly: true });
const mainnetBounceable = address.toString({ bounceable: true, testOnly: false });
const mainnetNonBounceable = address.toString({ bounceable: false, testOnly: false });
resultDiv.innerHTML = `
`;
log(`Address converted: ${addressInput}`);
log(`Testnet bounceable: ${testnetBounceable}`);
log(`Testnet non-bounceable: ${testnetNonBounceable}`);
log(`Mainnet bounceable: ${mainnetBounceable}`);
log(`Mainnet non-bounceable: ${mainnetNonBounceable}`);
} catch (e) {
resultDiv.innerHTML = `
`;
log('Error parsing address: ' + e.message);
}
};
// Generate contract information
document.getElementById('btn-generate-contract').onclick = async () => {
const { Cell, beginCell, contractAddress } = ton;
// Doge contract code
const code = Cell.fromBase64('te6cckEBAgEARAABFP8A9KQT9LzyyAsBAGrTMAGCCGlJILmRMODQ0wMx+kAwi0ZG9nZYcCCAGMjLBVAEzxaARfoCE8tqEssfAc8WyXP7AN4uuM8=');
// Create unique data with current timestamp
const data = beginCell()
.storeUint(Math.floor(Date.now() / 1000), 64)
.endCell();
// Create state init
const stateInit = { code, data };
// Calculate contract address
const doge_address = contractAddress(0, stateInit).toString();
// Create state init BOC
const state_init_boc = beginCell()
.storeUint(6, 5)
.storeRef(code)
.storeRef(data)
.endCell()
.toBoc()
.toString('base64');
// Store for deployment
contractData = {
address: doge_address,
stateInit: state_init_boc
};
// Display contract info
document.getElementById('contract-info').innerHTML = `
`;
// Enable deploy button if wallet is connected
const deployButton = document.getElementById('btn-deploy-contract');
deployButton.disabled = !tonConnectUI.wallet;
log(`Contract generated: ${doge_address}`);
};
// Deploy contract
document.getElementById('btn-deploy-contract').onclick = async () => {
if (!tonConnectUI.account) {
alert('Please connect your wallet first!');
return;
}
try {
// Deploy transaction
const transaction = {
validUntil: Math.floor(Date.now() / 1000) + 360,
messages: [
{
address: contractData.address,
amount: '69000000', // 0.069 TON
stateInit: contractData.stateInit
}
]
};
document.getElementById('deploy-status').innerHTML = `
`;
log('Deploying contract...');
// Use built-in TON Connect UI sendTransaction method
const result = await tonConnectUI.sendTransaction(transaction);
document.getElementById('deploy-status').innerHTML = `
`;
log('Contract deployed successfully!\n' + JSON.stringify(result, null, 2));
} catch (error) {
document.getElementById('deploy-status').innerHTML = `
`;
log('Deployment failed: ' + error.message);
}
};
// Clear log
document.getElementById('btn-clear-log').onclick = () => {
document.getElementById('log').textContent = '';
};
// Initial log message
log('TON Connect examples initialized. Please connect your wallet to start.');
</script>
</body>
</html>
After confirmation you can see the transaction on tonscan.org.
What happens if the user rejects a transaction request?
It's pretty easy to handle request rejection, but it's better to know what would happen in advance when you're developing some project.
When a user clicks Cancel in the popup in the wallet application, an exception is thrown:
Error: [TON_CONNECT_SDK_ERROR] The Wallet declined the request
This error can be considered final (unlike connection cancellation) — if it has been raised, the requested transaction will definitely not happen until the next request is sent.