From d10bcd663a2c282c22eaacc1b03500af0004801c Mon Sep 17 00:00:00 2001 From: Spite Date: Fri, 15 Sep 2023 17:49:25 +0400 Subject: [PATCH 1/2] Add wrappers cookbok and initial examples --- docs/develop/dapps/cookbook.md | 492 +++++++++++++++++++++++++++++++++ sidebars.js | 2 +- 2 files changed, 493 insertions(+), 1 deletion(-) create mode 100644 docs/develop/dapps/cookbook.md diff --git a/docs/develop/dapps/cookbook.md b/docs/develop/dapps/cookbook.md new file mode 100644 index 0000000000..f22983b80f --- /dev/null +++ b/docs/develop/dapps/cookbook.md @@ -0,0 +1,492 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Wrappers 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`. + +```bash +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 +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 +``` + + + + +```js +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 +``` + + + + +```go +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 +} +``` + + + + +```py +from tonsdk.utils._address import Address + +address1 = Address('EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF') +address2 = Address('0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e') + +# to_string() arguments: is_user_friendly, is_url_safe, is_bounceable, is_test_only + +print(address1.to_string(is_user_friendly=True, is_bounceable=True, is_url_safe=True)) # EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF +print(address1.to_string(is_user_friendly=False)) # 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e + +print(address2.to_string(is_user_friendly=True, is_bounceable=True, is_url_safe=True)) # EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF +print(address2.to_string(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](https://github.com/ton-blockchain/TEPs/blob/master/text/0002-address.md#smart-contract-addresses): + +Letter | Binary form | Bounceable | Testnet +:---: | :---: | :---: | :---: +E | 00010001 | yes | no | +U | 01010001 | no | no | +k | 10010001 | yes | yes | +0 | 11010001 | no | yes | + +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 +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 +``` + + + + +```js +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 +``` + + + + +```go +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()) +} +``` + + + + +```py +from tonsdk.utils._address import Address + +address = Address('EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF') + +# to_string() arguments: is_user_friendly, is_url_safe, is_bounceable, is_test_only + +print(address.to_string(is_user_friendly=True, is_bounceable=True, is_url_safe=True, is_test_only=False)) # EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF +print(address.to_string(is_user_friendly=True, is_bounceable=True, is_url_safe=False, is_test_only=False)) # EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff+W72r5gqPrHF +print(address.to_string(is_user_friendly=True, is_bounceable=False, is_url_safe=True, is_test_only=False)) # UQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPuwA +print(address.to_string(is_user_friendly=True, is_bounceable=True, is_url_safe=True, is_test_only=True)) # kQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPgpP +print(address.to_string(is_user_friendly=True, is_bounceable=False, is_url_safe=True, is_test_only=True)) # 0QDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPleK +``` + + + + +### 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](https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md#1-transfer), 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 +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 your wallet address that owns jetton wallet'); + + 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) + .storeCoins(toNano(5)) // jetton amount, amount * 10^9 + .storeAddress(destinationAddress) + .storeAddress(destinationAddress) // response destination + .storeBit(0) // no custom payload + .storeCoins(toNano('0.02')) + .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...")); +``` + + + + +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 +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('EQBbQljOpEM4Z6Hvv8Dbothp9xp2yM-TFYVr01bSqDQskHbx'), + Address.parse('EQBbQljOpEM4Z6Hvv8Dbothp9xp2yM-TFYVr01bSqDQskHbx') + ]; + const nftsMeta = [ + '0/meta.json', + '0/meta.json', + '0/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 + let counter = 0; + const nftDict = Dictionary.empty(); + 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 +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) + .storeUint(0, 64) + .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...")); +``` + + + + + +### 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 +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) + .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...")); +``` + + + + +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. \ No newline at end of file diff --git a/sidebars.js b/sidebars.js index 41a3c6b9a8..c1cae83f7b 100644 --- a/sidebars.js +++ b/sidebars.js @@ -239,7 +239,7 @@ const sidebars = { label: 'DApps Development', items: [ 'develop/dapps/README', - + 'develop/dapps/cookbook', { type: 'category', label: 'Telegram Web Apps', From 6e7c69a2050c692f8d8493454232bfb69e9e4779 Mon Sep 17 00:00:00 2001 From: Spite Date: Sat, 16 Sep 2023 21:51:49 +0400 Subject: [PATCH 2/2] Add some improvements --- docs/develop/dapps/cookbook.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/develop/dapps/cookbook.md b/docs/develop/dapps/cookbook.md index f22983b80f..616bf5c6c6 100644 --- a/docs/develop/dapps/cookbook.md +++ b/docs/develop/dapps/cookbook.md @@ -155,6 +155,8 @@ 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: @@ -258,12 +260,12 @@ async function main() { const messageBody = beginCell() .storeUint(0x0f8a7ea5, 32) // opcode for jetton transfer - .storeUint(0, 64) + .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')) + .storeCoins(toNano('0.02')) // forward amount .storeBit(1) // we store forwardPayload as a reference .storeRef(forwardPayload) .endCell(); @@ -306,13 +308,13 @@ async function main() { }); const ownersAddress = [ Address.parse('EQBbQljOpEM4Z6Hvv8Dbothp9xp2yM-TFYVr01bSqDQskHbx'), - Address.parse('EQBbQljOpEM4Z6Hvv8Dbothp9xp2yM-TFYVr01bSqDQskHbx'), - Address.parse('EQBbQljOpEM4Z6Hvv8Dbothp9xp2yM-TFYVr01bSqDQskHbx') + Address.parse('EQAUTbQiM522Y_XJ_T98QPhPhTmb4nV--VSPiha8kC6kRfPO'), + Address.parse('EQDWTH7VxFyk_34J1CM6wwEcjVeqRQceNwzPwGr30SsK43yo') ]; const nftsMeta = [ '0/meta.json', - '0/meta.json', - '0/meta.json' + '1/meta.json', + '2/meta.json' ]; const getMethodResult = await client.runMethod(collectionAddress, 'get_collection_data'); @@ -403,8 +405,8 @@ async function main() { const newOwnerAddress = Address.parse('put new owner wallet address'); const messageBody = beginCell() - .storeUint(3, 32) - .storeUint(0, 64) + .storeUint(3, 32) // opcode for changing owner + .storeUint(0, 64) // query id .storeAddress(newOwnerAddress) .endCell(); @@ -466,7 +468,7 @@ async function main() { const messageBody = beginCell() .storeUint(4, 32) // opcode for changing content - .storeUint(0, 64) + .storeUint(0, 64) // query id .storeRef(contentCell) .storeRef(royaltyCell) .endCell();