TON Cookbook
During product development, various questions often arise regarding interactions with different contracts on TON.
This document is created to gather the best practices from all developers and share them with everyone.
How to convert (user friendly <-> raw), assemble, and extract addresses from strings?
On TON, depending on the service, addresses can be seen in two formats: user-friendly
and raw
.
User-friendly: EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
Raw: 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e
User-friendly addresses are encoded in base64, while raw addresses are encoded in hex. In the raw format, the workchain in which the address is located is written separately before the ":" character, and the case of the characters does not matter.
To obtain an address from a string, you can use the following code:
- JS (@ton)
- JS (tonweb)
- Go
- Python
import { Address } from "@ton/core";
const address1 = Address.parse('EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF');
const address2 = Address.parse('0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e');
// toStrings arguments: urlSafe, bounceable, testOnly
// defaults values: true, true, false
console.log(address1.toString()); // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
console.log(address1.toRawString()); // 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e
console.log(address2.toString()); // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
console.log(address2.toRawString()); // 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e
const TonWeb = require('tonweb');
const address1 = new TonWeb.utils.Address('EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF');
const address2 = new TonWeb.utils.Address('0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e');
// toString arguments: isUserFriendly, isUrlSafe, isBounceable, isTestOnly
console.log(address1.toString(true, true, true)); // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
console.log(address1.toString(isUserFriendly = false)); // 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e
console.log(address1.toString(true, true, true)); // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
console.log(address2.toString(isUserFriendly = false)); // 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e
package main
import (
"fmt"
"github.com/xssnick/tonutils-go/address"
)
// Here, we will need to manually implement the handling of raw addresses since they are not supported by the library.
func main() {
address1 := address.MustParseAddr("EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF")
address2 := mustParseRawAddr("0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e", true, false)
fmt.Println(address1.String()) // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
fmt.Println(printRawAddr(address1)) // 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e
fmt.Println(address2.String()) // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
fmt.Println(printRawAddr(address2)) // 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e
}
func mustParseRawAddr(s string, bounceable bool, testnet bool) *address.Address {
addr, err := parseRawAddr(s, bounceable, testnet)
if err != nil {
panic(err)
}
return addr
}
func parseRawAddr(s string, bounceable bool, testnet bool) (*address.Address, error) {
var (
workchain int32
data []byte
)
_, err := fmt.Sscanf(s, "%d:%x", &workchain, &data)
if err != nil {
return nil, err
}
if len(data) != 32 {
return nil, fmt.Errorf("address len must be 32 bytes")
}
var flags byte = 0b00010001
if !bounceable {
setBit(&flags, 6)
}
if testnet {
setBit(&flags, 7)
}
return address.NewAddress(flags, byte(workchain), data), nil
}
func printRawAddr(addr *address.Address) string {
return fmt.Sprintf("%v:%x", addr.Workchain, addr.Data())
}
func setBit(n *byte, pos uint) {
*n |= 1 << pos
}
from pytoniq_core import Address
address1 = Address('EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF')
address2 = Address('0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e')
# to_str() arguments: is_user_friendly, is_url_safe, is_bounceable, is_test_only
print(address1.to_str(is_user_friendly=True, is_bounceable=True, is_url_safe=True)) # EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
print(address1.to_str(is_user_friendly=False)) # 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e
print(address2.to_str(is_user_friendly=True, is_bounceable=True, is_url_safe=True)) # EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
print(address2.to_str(is_user_friendly=False)) # 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e
How to obtain different types of addresses and determine the address type?
Addresses come in three formats: bounceable, non-bounceable, and testnet. This can be easily understood by looking at the first letter of the address, because it is the first byte (8 bits) that contains flags according to TEP-2:
Letter | Binary form | Bounceable | Testnet |
---|---|---|---|
E | 00010001 | yes | no |
U | 01010001 | no | no |
k | 10010001 | yes | yes |
0 | 11010001 | no | yes |
It's important to note that in base64 encoding, each character represents 6 bits of information. As you can observe, in all cases, the last 2 bits remain unchanged, so in this case, we can focus on the first letter. If they changed, it would affect the next character in the address.
Also, in some libraries, you may notice a field called "url safe." The thing is, the base64 format is not url safe, which means there can be issues when transmitting this address in a link. When urlSafe = true, all +
symbols are replaced with -
, and all /
symbols are replaced with _
. You can obtain these address formats using the following code:
- JS (@ton)
- JS (tonweb)
- Go
- Python
import { Address } from "@ton/core";
const address = Address.parse('EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF');
// toStrings arguments: urlSafe, bounceable, testOnly
// defaults values: true, true, false
console.log(address.toString()); // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHFэ
console.log(address.toString({urlSafe: false})) // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff+W72r5gqPrHF
console.log(address.toString({bounceable: false})) // UQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPuwA
console.log(address.toString({testOnly: true})) // kQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPgpP
console.log(address.toString({bounceable: false, testOnly: true})) // 0QDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPleK
const TonWeb = require('tonweb');
const address = new TonWeb.utils.Address('EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF');
// toString arguments: isUserFriendly, isUrlSafe, isBounceable, isTestOnly
console.log(address.toString(true, true, true, false)); // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
console.log(address.toString(true, false, true, false)); // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff+W72r5gqPrHF
console.log(address.toString(true, true, false, false)); // UQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPuwA
console.log(address.toString(true, true, true, true)); // kQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPgpP
console.log(address.toString(true, true, false, true)); // 0QDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPleK
package main
import (
"fmt"
"github.com/xssnick/tonutils-go/address"
)
func main() {
address := address.MustParseAddr("EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF")
fmt.Println(address.String()) // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
address.SetBounce(false)
fmt.Println(address.String()) // UQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPuwA
address.SetBounce(true)
address.SetTestnetOnly(true) // kQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPgpP
fmt.Println(address.String())
address.SetBounce(false) // 0QDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPleK
fmt.Println(address.String())
}
from pytoniq_core import Address
address = Address('EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF')
# to_str() arguments: is_user_friendly, is_url_safe, is_bounceable, is_test_only
print(address.to_str(is_user_friendly=True, is_bounceable=True, is_url_safe=True, is_test_only=False)) # EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
print(address.to_str(is_user_friendly=True, is_bounceable=True, is_url_safe=False, is_test_only=False)) # EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff+W72r5gqPrHF
print(address.to_str(is_user_friendly=True, is_bounceable=False, is_url_safe=True, is_test_only=False)) # UQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPuwA
print(address.to_str(is_user_friendly=True, is_bounceable=True, is_url_safe=True, is_test_only=True)) # kQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPgpP
print(address.to_str(is_user_friendly=True, is_bounceable=False, is_url_safe=True, is_test_only=True)) # 0QDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPleK
How to send standard TON transfer message?
To send a standard TON transfer message, first you need to open your wallet contract, after that, get your wallet seqno. And only after that can you send your TON transfer. Note that if you are using a non-V4 version of the wallet, you will need to rename WalletContractV4 to WalletContract{your wallet version}, for example, WalletContractV3R2.
import { TonClient, WalletContractV4, internal } from "@ton/ton";
import { mnemonicNew, mnemonicToPrivateKey } from "@ton/crypto";
const client = new TonClient({
endpoint: 'https://testnet.toncenter.com/api/v2/jsonRPC',
});
// Convert mnemonics to private key
let mnemonics = "word1 word2 ...".split(" ");
let keyPair = await mnemonicToPrivateKey(mnemonics);
// Create wallet contract
let workchain = 0; // Usually you need a workchain 0
let wallet = WalletContractV4.create({ workchain, publicKey: keyPair.publicKey });
let contract = client.open(wallet);
// Create a transfer
let seqno: number = await contract.getSeqno();
await contract.sendTransfer({
seqno,
secretKey: keyPair.secretKey,
messages: [internal({
value: '1',
to: 'EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N',
body: 'Example transfer body',
})]
});
How to calculate user's Jetton wallet address?
To calculate the user's jetton wallet address, we need to call the "get_wallet_address" get-method of the jetton master contract with user address actually. For this task we can easily use getWalletAddress method from JettonMaster or call master contract by ourselves.
- Use getWalletAddress method
- Call get-method by ourselves
const { Address, beginCell } = require("@ton/core")
const { TonClient, JettonMaster } = require("@ton/ton")
const client = new TonClient({
endpoint: 'https://toncenter.com/api/v2/jsonRPC',
});
const jettonMasterAddress = Address.parse('...') // for example EQBlqsm144Dq6SjbPI4jjZvA1hqTIP3CvHovbIfW_t-SCALE
const userAddress = Address.parse('...')
const jettonMaster = client.open(JettonMaster.create(jettonMasterAddress))
console.log(await jettonMaster.getWalletAddress(userAddress))
const { Address, beginCell } = require("@ton/core")
const { TonClient } = require("@ton/ton")
async function getUserWalletAddress(userAddress, jettonMasterAddress) {
const client = new TonClient({
endpoint: 'https://toncenter.com/api/v2/jsonRPC',
});
const userAddressCell = beginCell().storeAddress(userAddress).endCell()
const response = await client.runMethod(jettonMasterAddress, "get_wallet_address", [{type: "slice", cell: userAddressCell}])
return response.stack.readAddress()
}
const jettonMasterAddress = Address.parse('...') // for example EQBlqsm144Dq6SjbPI4jjZvA1hqTIP3CvHovbIfW_t-SCALE
const userAddress = Address.parse('...')
getUserWalletAddress(userAddress, jettonMasterAddress)
.then((userJettonWalletAddress) => {
console.log(userJettonWalletAddress)
}
)
How to construct a message for a jetton transfer with a comment?
To understand how to construct a message for token transfer, we use TEP-74, which describes the token standard. It's important to note that each token can have its own decimals
, which defaults to 9
. So, in the example below, we multiply the quantity by 10^9. If decimals were different, you would need to multiply by a different value.
- JS (@ton)
- JS (tonweb)
import { Address, beginCell, internal, storeMessageRelaxed, toNano } from "@ton/core";
async function main() {
const jettonWalletAddress = Address.parse('put your jetton wallet address');
const destinationAddress = Address.parse('put destination wallet address');
const forwardPayload = beginCell()
.storeUint(0, 32) // 0 opcode means we have a comment
.storeStringTail('Hello, TON!')
.endCell();
const messageBody = beginCell()
.storeUint(0x0f8a7ea5, 32) // opcode for jetton transfer
.storeUint(0, 64) // query id
.storeCoins(toNano(5)) // jetton amount, amount * 10^9
.storeAddress(destinationAddress)
.storeAddress(destinationAddress) // response destination
.storeBit(0) // no custom payload
.storeCoins(toNano('0.02')) // forward amount
.storeBit(1) // we store forwardPayload as a reference
.storeRef(forwardPayload)
.endCell();
const internalMessage = internal({
to: jettonWalletAddress,
value: toNano('0.1'),
bounce: true,
body: messageBody
});
const internalMessageCell = beginCell()
.store(storeMessageRelaxed(internalMessage))
.endCell();
}
main().finally(() => console.log("Exiting..."));
const TonWeb = require("tonweb");
const {mnemonicToKeyPair} = require("tonweb-mnemonic");
async function main() {
const tonweb = new TonWeb(new TonWeb.HttpProvider(
'https://toncenter.com/api/v2/jsonRPC', {
apiKey: 'put your api key'
})
);
const destinationAddress = new TonWeb.Address('put destination wallet address');
const forwardPayload = new TonWeb.boc.Cell();
forwardPayload.bits.writeUint(0, 32); // 0 opcode means we have a comment
forwardPayload.bits.writeString('Hello, TON!');
/*
Tonweb has a built-in class for interacting with jettons, which has
a method for creating a transfer. However, it has disadvantages, so
we manually create the message body. Additionally, this way we have a
better understanding of what is stored and how it functions.
*/
const jettonTransferBody = new TonWeb.boc.Cell();
jettonTransferBody.bits.writeUint(0xf8a7ea5, 32); // opcode for jetton transfer
jettonTransferBody.bits.writeUint(0, 64); // query id
jettonTransferBody.bits.writeCoins(new TonWeb.utils.BN('5')); // jetton amount, amount * 10^9
jettonTransferBody.bits.writeAddress(destinationAddress);
jettonTransferBody.bits.writeAddress(destinationAddress); // response destination
jettonTransferBody.bits.writeBit(false); // no custom payload
jettonTransferBody.bits.writeCoins(TonWeb.utils.toNano('0.02')); // forward amount
jettonTransferBody.bits.writeBit(true); // we store forwardPayload as a reference
jettonTransferBody.refs.push(forwardPayload);
const keyPair = await mnemonicToKeyPair('put your mnemonic'.split(' '));
const jettonWallet = new TonWeb.token.ft.JettonWallet(tonweb.provider, {
address: 'put your jetton wallet address'
});
// available wallet types: simpleR1, simpleR2, simpleR3,
// v2R1, v2R2, v3R1, v3R2, v4R1, v4R2
const wallet = new tonweb.wallet.all['v4R2'](tonweb.provider, {
publicKey: keyPair.publicKey,
wc: 0 // workchain
});
await wallet.methods.transfer({
secretKey: keyPair.secretKey,
toAddress: jettonWallet.address,
amount: tonweb.utils.toNano('0.1'),
seqno: await wallet.methods.seqno().call(),
payload: jettonTransferBody,
sendMode: 3
}).send(); // create transfer and send it
}
main().finally(() => console.log("Exiting..."));
To indicate that we want to include a comment, we specify 32 zero bits and then write our comment. We also specify the response destination
, which means that a response regarding the successful transfer will be sent to this address. If we don't want a response, we can specify 2 zero bits instead of an address.
How to use NFT batch deploy?
Smart contracts for collections allow deploying up to 250 NFTs in a single transaction. However, it's essential to consider that, in practice, this maximum is around 100-130 NFTs due to the computation fee limit of 1 ton. To achieve this, we need to store information about the new NFTs in a dictionary.
- JS (@ton)
import { Address, Cell, Dictionary, beginCell, internal, storeMessageRelaxed, toNano } from "@ton/core";
import { TonClient } from "@ton/ton";
async function main() {
const collectionAddress = Address.parse('put your collection address');
const nftMinStorage = '0.05';
const client = new TonClient({
endpoint: 'https://testnet.toncenter.com/api/v2/jsonRPC' // for Testnet
});
const ownersAddress = [
Address.parse('EQBbQljOpEM4Z6Hvv8Dbothp9xp2yM-TFYVr01bSqDQskHbx'),
Address.parse('EQAUTbQiM522Y_XJ_T98QPhPhTmb4nV--VSPiha8kC6kRfPO'),
Address.parse('EQDWTH7VxFyk_34J1CM6wwEcjVeqRQceNwzPwGr30SsK43yo')
];
const nftsMeta = [
'0/meta.json',
'1/meta.json',
'2/meta.json'
];
const getMethodResult = await client.runMethod(collectionAddress, 'get_collection_data');
let nextItemIndex = getMethodResult.stack.readNumber();
To begin with, let's assume that the minimum amount of TON for the storage fee is 0.05
. This means that after deploying an NFT, the smart contract of the collection will send this much TON to its balance. Next, we obtain arrays with the owners of the new NFTs and their content. Afterward, we get the next_item_index
using the GET method get_collection_data
.
- JS (@ton)
let counter = 0;
const nftDict = Dictionary.empty<number, Cell>();
for (let index = 0; index < 3; index++) {
const metaCell = beginCell()
.storeStringTail(nftsMeta[index])
.endCell();
const nftContent = beginCell()
.storeAddress(ownersAddress[index])
.storeRef(metaCell)
.endCell();
nftDict.set(nextItemIndex, nftContent);
nextItemIndex++;
counter++;
}
/*
We need to write our custom serialization and deserialization
functions to store data correctly in the dictionary since the
built-in functions in the library are not suitable for our case.
*/
const messageBody = beginCell()
.storeUint(2, 32)
.storeUint(0, 64)
.storeDict(nftDict, Dictionary.Keys.Uint(64), {
serialize: (src, builder) => {
builder.storeCoins(toNano(nftMinStorage));
builder.storeRef(src);
},
parse: (src) => {
return beginCell()
.storeCoins(src.loadCoins())
.storeRef(src.loadRef())
.endCell();
}
})
.endCell();
const totalValue = String(
(counter * parseFloat(nftMinStorage) + 0.015 * counter).toFixed(6)
);
const internalMessage = internal({
to: collectionAddress,
value: totalValue,
bounce: true,
body: messageBody
});
const internalMessageCell = beginCell()
.store(storeMessageRelaxed(internalMessage))
.endCell();
}
main().finally(() => console.log("Exiting..."));
Next, we need to correctly calculate the total transaction cost. The value of 0.015
was obtained through testing, but it can vary for each case. This mainly depends on the content of the NFT, as an increase in content size results in a higher forward fee (the fee for delivery).
How to change the owner of a collection's smart contract?
Changing the owner of a collection is very simple. To do this, you need to specify opcode = 3, any query_id, and the address of the new owner:
- JS (@ton)
- JS (tonweb)
import { Address, beginCell, internal, storeMessageRelaxed, toNano } from "@ton/core";
async function main() {
const collectionAddress = Address.parse('put your collection address');
const newOwnerAddress = Address.parse('put new owner wallet address');
const messageBody = beginCell()
.storeUint(3, 32) // opcode for changing owner
.storeUint(0, 64) // query id
.storeAddress(newOwnerAddress)
.endCell();
const internalMessage = internal({
to: collectionAddress,
value: toNano('0.05'),
bounce: true,
body: messageBody
});
const internalMessageCell = beginCell()
.store(storeMessageRelaxed(internalMessage))
.endCell();
}
main().finally(() => console.log("Exiting..."));
const TonWeb = require("tonweb");
const {mnemonicToKeyPair} = require("tonweb-mnemonic");
async function main() {
const tonweb = new TonWeb(new TonWeb.HttpProvider(
'https://toncenter.com/api/v2/jsonRPC', {
apiKey: 'put your api key'
})
);
const collectionAddress = new TonWeb.Address('put your collection address');
const newOwnerAddress = new TonWeb.Address('put new owner wallet address');
const messageBody = new TonWeb.boc.Cell();
messageBody.bits.writeUint(3, 32); // opcode for changing owner
messageBody.bits.writeUint(0, 64); // query id
messageBody.bits.writeAddress(newOwnerAddress);
// available wallet types: simpleR1, simpleR2, simpleR3,
// v2R1, v2R2, v3R1, v3R2, v4R1, v4R2
const keyPair = await mnemonicToKeyPair('put your mnemonic'.split(' '));
const wallet = new tonweb.wallet.all['v4R2'](tonweb.provider, {
publicKey: keyPair.publicKey,
wc: 0 // workchain
});
await wallet.methods.transfer({
secretKey: keyPair.secretKey,
toAddress: collectionAddress,
amount: tonweb.utils.toNano('0.05'),
seqno: await wallet.methods.seqno().call(),
payload: messageBody,
sendMode: 3
}).send(); // create transfer and send it
}
main().finally(() => console.log("Exiting..."));
How to change the content in a collection's smart contract?
To change the content of a smart contract's collection, we need to understand how it is stored. The collection stores all the content in a single cell, inside of which there are two cells: collection content and NFT common content. The first cell contains the collection's metadata, while the second one contains the base URL for the NFT metadata.
Often, the collection's metadata is stored in a format similar to 0.json
and continues incrementing, while the address before this file remains the same. It is this address that should be stored in the NFT common content.
- JS (@ton)
- JS (tonweb)
import { Address, beginCell, internal, storeMessageRelaxed, toNano } from "@ton/core";
async function main() {
const collectionAddress = Address.parse('put your collection address');
const newCollectionMeta = 'put url fol collection meta';
const newNftCommonMeta = 'put common url for nft meta';
const royaltyAddress = Address.parse('put royalty address');
const collectionMetaCell = beginCell()
.storeUint(1, 8) // we have offchain metadata
.storeStringTail(newCollectionMeta)
.endCell();
const nftCommonMetaCell = beginCell()
.storeUint(1, 8) // we have offchain metadata
.storeStringTail(newNftCommonMeta)
.endCell();
const contentCell = beginCell()
.storeRef(collectionMetaCell)
.storeRef(nftCommonMetaCell)
.endCell();
const royaltyCell = beginCell()
.storeUint(5, 16) // factor
.storeUint(100, 16) // base
.storeAddress(royaltyAddress) // this address will receive 5% of each sale
.endCell();
const messageBody = beginCell()
.storeUint(4, 32) // opcode for changing content
.storeUint(0, 64) // query id
.storeRef(contentCell)
.storeRef(royaltyCell)
.endCell();
const internalMessage = internal({
to: collectionAddress,
value: toNano('0.05'),
bounce: true,
body: messageBody
});
const internalMessageCell = beginCell()
.store(storeMessageRelaxed(internalMessage))
.endCell();
}
main().finally(() => console.log("Exiting..."));
const TonWeb = require("tonweb");
const {mnemonicToKeyPair} = require("tonweb-mnemonic");
async function main() {
const tonweb = new TonWeb(new TonWeb.HttpProvider(
'https://testnet.toncenter.com/api/v2/jsonRPC', {
apiKey: 'put your api key'
})
);
const collectionAddress = new TonWeb.Address('put your collection address');
const newCollectionMeta = 'put url fol collection meta';
const newNftCommonMeta = 'put common url for nft meta';
const royaltyAddress = new TonWeb.Address('put royalty address');
const collectionMetaCell = new TonWeb.boc.Cell();
collectionMetaCell.bits.writeUint(1, 8); // we have offchain metadata
collectionMetaCell.bits.writeString(newCollectionMeta);
const nftCommonMetaCell = new TonWeb.boc.Cell();
nftCommonMetaCell.bits.writeUint(1, 8); // we have offchain metadata
nftCommonMetaCell.bits.writeString(newNftCommonMeta);
const contentCell = new TonWeb.boc.Cell();
contentCell.refs.push(collectionMetaCell);
contentCell.refs.push(nftCommonMetaCell);
const royaltyCell = new TonWeb.boc.Cell();
royaltyCell.bits.writeUint(5, 16); // factor
royaltyCell.bits.writeUint(100, 16); // base
royaltyCell.bits.writeAddress(royaltyAddress); // this address will receive 5% of each sale
const messageBody = new TonWeb.boc.Cell();
messageBody.bits.writeUint(4, 32);
messageBody.bits.writeUint(0, 64);
messageBody.refs.push(contentCell);
messageBody.refs.push(royaltyCell);
// available wallet types: simpleR1, simpleR2, simpleR3,
// v2R1, v2R2, v3R1, v3R2, v4R1, v4R2
const keyPair = await mnemonicToKeyPair('put your mnemonic'.split(' '));
const wallet = new tonweb.wallet.all['v4R2'](tonweb.provider, {
publicKey: keyPair.publicKey,
wc: 0 // workchain
});
await wallet.methods.transfer({
secretKey: keyPair.secretKey,
toAddress: collectionAddress,
amount: tonweb.utils.toNano('0.05'),
seqno: await wallet.methods.seqno().call(),
payload: messageBody,
sendMode: 3
}).send(); // create transfer and send it
}
main().finally(() => console.log("Exiting..."));
Additionally, we need to include royalty information in our message, as they also change using this opcode. It's important to note that it's not necessary to specify new values everywhere. If, for example, only the NFT common content needs to be changed, then all other values can be specified as they were before.
Processing Snake Cells
Some times it's necessary to store long strings (or other large information) while cells can hold maximum 1023 bits. In this case, we can use snake cells. Snake cells are cells that contain a reference to another cell, which, in turn, contains a reference to another cell, and so on.
- JS (tonweb)
const TonWeb = require("tonweb");
function writeStringTail(str, cell) {
const bytes = Math.floor(cell.bits.getFreeBits() / 8); // 1 symbol = 8 bits
if(bytes < str.length) { // if we can't write all string
cell.bits.writeString(str.substring(0, bytes)); // write part of string
const newCell = writeStringTail(str.substring(bytes), new TonWeb.boc.Cell()); // create new cell
cell.refs.push(newCell); // add new cell to current cell's refs
} else {
cell.bits.writeString(str); // write all string
}
return cell;
}
function readStringTail(cell) {
const slice = cell.beginParse(); // converting cell to slice
if(cell.refs.length > 0) {
const str = new TextDecoder('ascii').decode(slice.array); // decode uint8array to string
return str + readStringTail(cell.refs[0]); // read next cell
} else {
return new TextDecoder('ascii').decode(slice.array);
}
}
let cell = new TonWeb.boc.Cell();
const str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In euismod, ligula vel lobortis hendrerit, lectus sem efficitur enim, vel efficitur nibh dui a elit. Quisque augue nisi, vulputate vitae mauris sit amet, iaculis lobortis nisi. Aenean molestie ultrices massa eu fermentum. Cras rhoncus ipsum mauris, et egestas nibh interdum in. Maecenas ante ipsum, sodales eget suscipit at, placerat ut turpis. Nunc ac finibus dui. Donec sit amet leo id augue tempus aliquet. Vestibulum eu aliquam ex, sit amet suscipit odio. Vestibulum et arcu dui.";
cell = writeStringTail(str, cell);
const text = readStringTail(cell);
console.log(text);
This example will help you understand how you can work with such cells using recursion.