From 2bb057c9446a3651ef084b8d0180cd85ef1880a9 Mon Sep 17 00:00:00 2001 From: Alexey Ostrovsky Date: Mon, 31 Mar 2025 21:42:28 +0300 Subject: [PATCH 1/8] Feedback fixes(draft) --- ...ructure.mdx => blueprint-sdk-overview.mdx} | 54 ++- .../processing-messages.mdx | 403 +++++++++--------- .../setup-environment.mdx | 2 - .../storage-and-get-methods.mdx | 129 +++--- .../quick-start/getting-started.mdx | 15 +- sidebars/guidelines.js | 2 +- 6 files changed, 300 insertions(+), 305 deletions(-) rename docs/v3/guidelines/quick-start/developing-smart-contracts/{program-structure.mdx => blueprint-sdk-overview.mdx} (57%) diff --git a/docs/v3/guidelines/quick-start/developing-smart-contracts/program-structure.mdx b/docs/v3/guidelines/quick-start/developing-smart-contracts/blueprint-sdk-overview.mdx similarity index 57% rename from docs/v3/guidelines/quick-start/developing-smart-contracts/program-structure.mdx rename to docs/v3/guidelines/quick-start/developing-smart-contracts/blueprint-sdk-overview.mdx index 760c0191e0..38d29ed643 100644 --- a/docs/v3/guidelines/quick-start/developing-smart-contracts/program-structure.mdx +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/blueprint-sdk-overview.mdx @@ -1,19 +1,19 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -# Project structure +# Blueprint SDK overview > **Summary:** In previous steps we installed and configured all tools required for TON smart-contract development and created our first project template. -Before we proceed to research and modification of smart-contract code, let's take a brief look at project structure, purpose of them and default scenarios of its use. +Before we proceed to actual smart-contract development let's briefly describe project structure and explain how to use **`Blueprint SDK`**. -## Overview +## Project structure If you chose proposed names in previous steps your project structure should look like this: -``` +```ls title="Project structure" Example/ ├── contracts/ # Folder containing smart contracts code │ ├── imports/ # Library imports for contracts @@ -46,54 +46,52 @@ Example/ -Before we proceed to actual smart-contract development let's briefly describe project structure and explain how to use **`Blueprint SDK`**. - ### `/contracts` -This folder contains your smart contract source code written in one of the available programming languages used for TON blockchain smart contract development. And in case of `FunC` contains imports folder which is used for libraries usually containing `stdlib.fc` - standard library of `FunC` language. +This folder contains your smart contract source code written in one of the available programming languages used for TON blockchain smart contract development. ### `/wrappers` -- `HelloWorld.ts` - wrapper for smart contract. -- `HelloWorld.compile.ts` - compile config for smart-contract. - -While `@ton/ton SDK` provides us interfaces of serializing and sending messages for standard smart-contracts such as `wallets`, if we develop our own smart-contract that will deserialize received messages by its own custom protocol we need to provide some wrapper object that will serialize messages sent to smart-contract, deserialize responses from `get method`s and serialize `initial data` for contract deployment. +While `@ton/ton SDK` provides us interfaces of serializing and sending messages for standard smart-contracts such as `wallets`, if we develop our own smart-contract that will deserialize received messages by its own custom protocol we need to provide some wrapper object that will serialize messages sent to smart-contract, deserialize responses from `get method`s and serialize contract itself for deployment. -To run compile script excute this command in your CLI: +### `/tests` -```bash -npx blueprint build -``` +This directory contains test files for your smart contracts. Testing contracts directly in TON network is not the best option because it requires some amount of time and may lead to losing funds. This testing playground tool allow you to execute multiple smart-contracts and even send messages between them in your "local network". Tests are crucial for ensuring your smart contracts behave as expected before deployment to the network. -It's preferred development flow to edit smart contract code and then edit its wrapper correspondingly to updated protocol. +### `/scripts` -:::info Advanced, TL-B -Often, as a developer, you want to provide description of protocol by some formal language and TON ecosystem has standard instrument for that: [TL-B](/v3/documentation/data-formats/tlb/tl-b-language) language. `TL-B`(Type Language-Binary) schemes serve to describe binary protocol of smart-contracts somewhat similar to **Protobuf** technology. At the current moment, unfortunately, there are no instruments that provide generation of serialization/deserialization interfaces, but it's anyway a good practice to have one for smart-contracts with complex interfaces. -::: +The scripts directory contains `TypeScript` files that help you deploy and interact with your smart contracts on-chain using previously implemented wrappers. -### `/tests` +## Development flow -This directory contains test files for your smart contracts, written using the **`Jest` testing framework**. It's testing playground that uses `@ton/sandbox` tool allowing you to execute multiple smart-contracts and even send messages between them, creating your local 'network' of contracts if your project requires so, and test more complex scenarios than simple **unit-tests**. Tests are crucial for ensuring your smart contracts behave as expected before deployment to the `Mainnet`. +Almost any smart-contract project development consist of five simple steps: -To run your test execute following command: + 1. Edit smart-contract code in `/contracts` folder and build it by running build script: ```bash npx blueprint build ``` -Or use interface provided by `Jest` plugins in your **IDE** or **code-editor**. + 2. Update smart-contract wrapper in `/wrapper` folder corresponding to changes in contract. + 3. Update test's in `/tests` folder to ensure correctness of new functionality and run test script: -### `/scripts` +```bash +npx blueprint test +``` -The scripts directory contains `TypeScript` files that help you deploy and interact with your smart contracts on-chain using previously implemented wrappers. + 4. Repeat steps 1-3 until you get desired result. -You can execute those scripts using following command, but we recommend to read corresponding [deploying to netowrk](/v3/guidelines/quick-start/developing-smart-contracts/deploying-to-network) section first. + 5. Update deployment script in `/scripts` folder and run it using this command: ```bash -npx blueprint run +npx blueprint run ``` -Also, you can always generate same structure for another smart-contract if you need so, by using following command: +:::tip +All examples in this guide follow sequnce of this **1-3 steps** with corresponding code samples. **Step 5**, deployment process, is covered in last section of the guide: [Deploying to network](/v3/guidelines/quick-start/developing-smart-contracts/deploying-to-network). +::: + +Also, you can always generate same structure for another smart-contract if, for example, you want to create multiple contracts interacting with each other, by using following command: ```bash npx blueprint create PascalCase #dont forget to name contract in PascalCase diff --git a/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx b/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx index 6be7eea1f9..168b8e2aa4 100644 --- a/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx @@ -15,207 +15,6 @@ If you are stuck on some of the examples you can find original template project --- -## External Messages - -`External messages` are your main way of toggling smart contract logic from outside the blockchain. Usually, there is no need for implementation of them in smart contracts because in most cases you don't want external entry points to be accessible to anyone except you. This includes several standard approaches of verifying external message sender providing safe entry point to the TON network which we will discuss here. If this is all functionality that you want from external section - standard way is to delegate this responsibility to separate actor - `wallet` which is practically the main reason they were designed for. - -### Wallets - -When we sent coins using wallet app in [getting started](/v3/guidelines/quick-start/getting-started#step-3-exploring-the-blockchain) section what `wallet app` actually performs is sending `external message` to your `wallet` smart contract which performs sending message to destination smart-contract address that you wrote in send menu. While most wallet apps during creation of wallet deploy most modern versions of wallet smart contracts - `v5`, providing more complex functionality, let's examine `recv_external` section of more basic one - `v3`: - - - -```func -() recv_external(slice in_msg) impure { - var signature = in_msg~load_bits(512); - var cs = in_msg; - var (subwallet_id, msg_seqno) = (cs~load_uint(32), cs~load_uint(32)); - var ds = get_data().begin_parse(); - var (stored_seqno, stored_subwallet, public_key) = (ds~load_uint(32), ds~load_uint(32), ds~load_uint(256)); - ds.end_parse(); - throw_unless(33, msg_seqno == stored_seqno); - throw_unless(34, subwallet_id == stored_subwallet); - throw_unless(35, check_signature(slice_hash(in_msg), signature, public_key)); - accept_message(); - cs~touch(); - while (cs.slice_refs()) { - var mode = cs~load_uint(8); - send_raw_message(cs~load_ref(), mode); - } - set_data(begin_cell() - .store_uint(stored_seqno + 1, 32) - .store_uint(stored_subwallet, 32) - .store_uint(public_key, 256) - .end_cell()); - } -``` - - -First thing to mention - is `signature` in message body and stored `public_key`. This refers to standard mechanism of asymmetric cryptography: during deployment process you create **private and public key pair**, store the second one in initial contract storage and then during sending external message through client **sign** it with **private key** attaching calculated `signature` to message body. Smart contract on its side checks if signature matches `public_key` and accepts external message if it is so. - -Standard signature system for TON smart-contracts is `Ed25519` which is directly provided by `TVM` instruction `check_signature()`, but you can always implement another preferred algorithm by yourself. - -:::tip -When you entered **magic 24 secret words** (i.e. **mnemonic phrase**) during [wallet creation](/v3/documentation/data-formats/tlb/tl-b-language) in your app what is practically performed is concatenation of those words into one string and hashing it to create your **private key**. So remember not to show them to anyone. -::: - -Second thing is `seqno` (sequential number) as you can see this is practically just a counter that increments each time wallet smart-contract receives external message, but why do we need one? -The reason behind that lies in blockchain nature: since all transactions are visible to anyone, potential malefactor could repeatedly send already signed transaction to your smart contract. - -> **Simpliest scenario:** you transfer some amount of funds to receiver, receiver examines transaction and sends it to your contract repeatedly until you run out of funds and receiver gains almost all of them. - -Third thing is `subwallet_id` that just checks equality to the stored one, we will discuss its meaning a little bit later in internal messages section. - -### Implementation - -At this point reasons behind changes that we made to our counter in previous storage and get methods [section](/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods#smart-contract-storage-operations) should start to be more clear! We already prepared our storage to contain `seqno`, `public_key` and `ctx_id` which will serve same task as `subwallet_id` so let's adapt wallet's `recv_external` function to our project: - - - -```func -() recv_external(slice in_msg) impure { - ;; retrives validating data from message body - var signature = in_msg~load_bits(512); - var cs = in_msg; - var (ctx_id, msg_seqno) = (cs~load_uint(32), cs~load_uint(32)); - - ;; retrieves stored data for validation checks - var (stored_id, stored_seqno, public_key) = load_data(); - ;; replay protection mechanism through seqno chack and incrementing - throw_unless(33, msg_seqno == stored_seqno); - ;; id field for multiply addresess with same private_key - throw_unless(34, ctx_id == stored_id); - ;; ed25519 signature check - throw_unless(35, check_signature(slice_hash(in_msg), signature, public_key)); - ;; accepting message after all checks - accept_message(); - ;; optimization technique - ;; putting message body to stack head - cs~touch(); - ;; sending serialized on client side messages - while (cs.slice_refs()) { - var mode = cs~load_uint(8); - send_raw_message(cs~load_ref(), mode); - } - save_data(stored_id, stored_seqno + 1, public_key); -} -``` - - -```tolk -fun acceptExternalMessage(): void - asm "ACCEPT"; - -fun onExternalMessage(inMsg: slice) { - var signature = inMsg.loadBits(512); - var cs = inMsg; - var (ctx_id, msg_seqno) = (cs.loadUint(32), cs.loadUint(32)); - - // retrieves stored data for validation checks - var (stored_id, stored_seqno, public_key) = loadData(); - // replay protection mechanism through seqno chack and incrementing - assert(msg_seqno == stored_seqno, 33); - // id field for multiply addresess with same private_key - assert(ctx_id == stored_id, 34); - // ed25519 signature check - assert(isSignatureValid(sliceHash(inMsg), signature, public_key), 35); - // accepting message after all checks - acceptExternalMessage(); - // sending serialized on client side messages - while (!cs.isEndOfSliceRefs()) { - var mode = cs.loadUint(8); - sendRawMessage(cs.loadRef(), mode); - } - saveData(stored_id, stored_seqno + 1, public_key); -} -``` - - - - -And add wrapper method to call it through our wrapper class: - -```typescript -async function sendExternal( - provider: ContractProvider, - opts: { - mode: number - message: Cell, - secret_key: Buffer - } -) { - const seqno = await this.getCounter(provider) - const id = await this.getID(provider) - - const toSign = beginCell() - .storeUint(id, 32) - .storeUint(seqno, 32) - .storeUint(opts.mode, 8) - .storeRef(opts.message) - - const signature = sign(toSign.endCell().hash(), opts.secret_key) - - return await provider.external(beginCell() - .storeBuffer(signature) - .storeBuilder(toSign) - .endCell() - ); -} -``` - -Here we are preparing all required values for contract checks and add payload message that will be sent as internal message further to final receiver smart-contract. - -As you can see here we are using `provider.external()` method instead of `provider.internal()` one which requires a via argument, that now we can start to understand. The thing is that `provider.internal()` method is doing practically the same thing that we are trying to implement: since we can't directly call internal methods to test them we need to wrap them into external message and send it through some wallet. - -Now let's test our implementation: - -```typescript -it('should send an external message containing an internal message', async () => { - const receiver = await blockchain.treasury('receiver'); - - const internalMessage = beginCell() - .storeUint(0, 32) // Simple message with no specific opcode - .storeUint(42, 64) // queryID = 42 - .storeStringTail('Hello from external message!') - .endCell(); - - const messageToSend = beginCell().store(storeMessageRelaxed(internal({ - to: receiver.address, - value: toNano(0.01), - body: internalMessage, - bounce: true, - }))).endCell(); - - const receiverBalanceBefore = await receiver.getBalance(); - - const result = await helloWorld.sendExternal({ - mode: SendMode.PAY_GAS_SEPARATELY, - message: messageToSend, - secret_key: keyPair.secretKey - }); - - expect(result.transactions).toHaveTransaction({ - from: undefined, // External messages have no 'from' address - to: helloWorld.address, - success: true, - }); - - expect(result.transactions).toHaveTransaction({ - from: helloWorld.address, - to: receiver.address, - success: true, - }); - - const receiverBalanceAfter = await receiver.getBalance(); - - expect(receiverBalanceAfter).toBeGreaterThan(receiverBalanceBefore); - const [seqnoAfter] = await helloWorld.getSeqnoPKey(); - expect(seqnoAfter).toBe(1); // Since it should start from 0 and increment to 1 -}); -``` - ---- - ## Internal messages We are almost at the finish line! Let's cook a simple `internal message` to other contract and implement its processing on receiver side! We have already seen a counter that increases its values through `external messages`, now let's make it through internal one. And to make this task a little more interesting let's ensure that only one `actor` has access to this functionality. @@ -563,6 +362,206 @@ contract CounterInternal with Deployable, Ownable { export * from '../build/CounterInternal/tact_CounterInternal'; ``` + +--- + +## External Messages + +`External messages` are your main way of toggling smart contract logic from outside the blockchain. Usually, there is no need for implementation of them in smart contracts because in most cases you don't want external entry points to be accessible to anyone except you. If this is all functionality that you want from external section - standard way is to delegate this responsibility to separate actor - `wallet` which is practically the main reason they were designed for. + +### Wallets + +When we sent coins using wallet app in [getting started](/v3/guidelines/quick-start/getting-started#step-3-exploring-the-blockchain) section what `wallet app` actually performs is sending `external message` to your `wallet` smart contract which performs sending message to destination smart-contract address that you wrote in send menu. While most wallet apps during creation of wallet deploy most modern versions of wallet smart contracts - `v5`, providing more complex functionality, let's examine `recv_external` section of more basic one - `v3`: + +```func +() recv_external(slice in_msg) impure { + var signature = in_msg~load_bits(512); + var cs = in_msg; + var (subwallet_id, msg_seqno) = (cs~load_uint(32), cs~load_uint(32)); + var ds = get_data().begin_parse(); + var (stored_seqno, stored_subwallet, public_key) = (ds~load_uint(32), ds~load_uint(32), ds~load_uint(256)); + ds.end_parse(); + throw_unless(33, msg_seqno == stored_seqno); + throw_unless(34, subwallet_id == stored_subwallet); + throw_unless(35, check_signature(slice_hash(in_msg), signature, public_key)); + accept_message(); + cs~touch(); + while (cs.slice_refs()) { + var mode = cs~load_uint(8); + send_raw_message(cs~load_ref(), mode); + } + set_data(begin_cell() + .store_uint(stored_seqno + 1, 32) + .store_uint(stored_subwallet, 32) + .store_uint(public_key, 256) + .end_cell()); + } +``` + + +First thing to mention - is `signature` in message body and stored `public_key`. This refers to standard mechanism of asymmetric cryptography: during deployment process you create **private and public key pair**, store the second one in initial contract storage and then during sending external message through client **sign** it with **private key** attaching calculated `signature` to message body. Smart contract on its side checks if signature matches `public_key` and accepts external message if it is so. + +Standard signature system for TON smart-contracts is `Ed25519` which is directly provided by `TVM` instruction `check_signature()`, but you can always implement another preferred algorithm by yourself. + +:::tip +When you entered **magic 24 secret words** (i.e. **mnemonic phrase**) during [wallet creation](/v3/documentation/data-formats/tlb/tl-b-language) in your app what is practically performed is concatenation of those words into one string and hashing it to create your **private key**. So remember not to show them to anyone. +::: + +Second thing is `seqno` (sequential number) as you can see this is practically just a counter that increments each time wallet smart-contract receives external message, but why do we need one? +The reason behind that lies in blockchain nature: since all transactions are visible to anyone, potential malefactor could repeatedly send already signed transaction to your smart contract. + +> **Simpliest scenario:** you transfer some amount of funds to receiver, receiver examines transaction and sends it to your contract repeatedly until you run out of funds and receiver gains almost all of them. + +Third thing is `subwallet_id` that just checks equality to the stored one, we will discuss its meaning a little bit later in internal messages section. + +### Implementation + +At this point reasons behind changes that we made to our counter in previous storage and get methods [section](/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods#smart-contract-storage-operations) should start to be more clear! We already prepared our storage to contain `seqno`, `public_key` and `ctx_id` which will serve same task as `subwallet_id` so let's adapt wallet's `recv_external` function to our project: + + + +```func +() recv_external(slice in_msg) impure { + ;; retrives validating data from message body + var signature = in_msg~load_bits(512); + var cs = in_msg; + var (ctx_id, msg_seqno) = (cs~load_uint(32), cs~load_uint(32)); + + ;; retrieves stored data for validation checks + var (stored_id, stored_seqno, public_key) = load_data(); + ;; replay protection mechanism through seqno chack and incrementing + throw_unless(33, msg_seqno == stored_seqno); + ;; id field for multiply addresess with same private_key + throw_unless(34, ctx_id == stored_id); + ;; ed25519 signature check + throw_unless(35, check_signature(slice_hash(in_msg), signature, public_key)); + ;; accepting message after all checks + accept_message(); + ;; optimization technique + ;; putting message body to stack head + cs~touch(); + ;; sending serialized on client side messages + while (cs.slice_refs()) { + var mode = cs~load_uint(8); + send_raw_message(cs~load_ref(), mode); + } + save_data(stored_id, stored_seqno + 1, public_key); +} +``` + + +```tolk +fun acceptExternalMessage(): void + asm "ACCEPT"; + +fun onExternalMessage(inMsg: slice) { + var signature = inMsg.loadBits(512); + var cs = inMsg; + var (ctx_id, msg_seqno) = (cs.loadUint(32), cs.loadUint(32)); + + // retrieves stored data for validation checks + var (stored_id, stored_seqno, public_key) = loadData(); + // replay protection mechanism through seqno chack and incrementing + assert(msg_seqno == stored_seqno, 33); + // id field for multiply addresess with same private_key + assert(ctx_id == stored_id, 34); + // ed25519 signature check + assert(isSignatureValid(sliceHash(inMsg), signature, public_key), 35); + // accepting message after all checks + acceptExternalMessage(); + // sending serialized on client side messages + while (!cs.isEndOfSliceRefs()) { + var mode = cs.loadUint(8); + sendRawMessage(cs.loadRef(), mode); + } + saveData(stored_id, stored_seqno + 1, public_key); +} +``` + + + + +And add wrapper method to call it through our wrapper class: + +```typescript +async function sendExternal( + provider: ContractProvider, + opts: { + mode: number + message: Cell, + secret_key: Buffer + } +) { + const seqno = await this.getCounter(provider) + const id = await this.getID(provider) + + const toSign = beginCell() + .storeUint(id, 32) + .storeUint(seqno, 32) + .storeUint(opts.mode, 8) + .storeRef(opts.message) + + const signature = sign(toSign.endCell().hash(), opts.secret_key) + + return await provider.external(beginCell() + .storeBuffer(signature) + .storeBuilder(toSign) + .endCell() + ); +} +``` + +Here we are preparing all required values for contract checks and add payload message that will be sent as internal message further to final receiver smart-contract. + +As you can see here we are using `provider.external()` method instead of `provider.internal()` one which requires a via argument, that now we can start to understand. The thing is that `provider.internal()` method is doing practically the same thing that we are trying to implement: since we can't directly call internal methods to test them we need to wrap them into external message and send it through some wallet. + +Now let's test our implementation: + +```typescript +it('should send an external message containing an internal message', async () => { + const receiver = await blockchain.treasury('receiver'); + + const internalMessage = beginCell() + .storeUint(0, 32) // Simple message with no specific opcode + .storeUint(42, 64) // queryID = 42 + .storeStringTail('Hello from external message!') + .endCell(); + + const messageToSend = beginCell().store(storeMessageRelaxed(internal({ + to: receiver.address, + value: toNano(0.01), + body: internalMessage, + bounce: true, + }))).endCell(); + + const receiverBalanceBefore = await receiver.getBalance(); + + const result = await helloWorld.sendExternal({ + mode: SendMode.PAY_GAS_SEPARATELY, + message: messageToSend, + secret_key: keyPair.secretKey + }); + + expect(result.transactions).toHaveTransaction({ + from: undefined, // External messages have no 'from' address + to: helloWorld.address, + success: true, + }); + + expect(result.transactions).toHaveTransaction({ + from: helloWorld.address, + to: receiver.address, + success: true, + }); + + const receiverBalanceAfter = await receiver.getBalance(); + + expect(receiverBalanceAfter).toBeGreaterThan(receiverBalanceBefore); + const [seqnoAfter] = await helloWorld.getSeqnoPKey(); + expect(seqnoAfter).toBe(1); // Since it should start from 0 and increment to 1 +}); +``` + #### Testing the multi-contract system The following test case demonstrates how to deploy and interact with a `HelloWorld` smart contract and a `CounterInternal` contract: @@ -706,6 +705,8 @@ describe('CounterInternal', () => { }); ``` + + Congratulations! You have successfully created a **multi-contract** system and learned how to handle **internal messages**. This example illustrates the typical flow of any message chain: sending an `external message`, triggering the `internal messages` flow based on your system model, and so on. Now that our contracts have been fully tested, we are ready to deploy them and interact with them on-chain. :::danger diff --git a/docs/v3/guidelines/quick-start/developing-smart-contracts/setup-environment.mdx b/docs/v3/guidelines/quick-start/developing-smart-contracts/setup-environment.mdx index d8c7996481..1389282c51 100644 --- a/docs/v3/guidelines/quick-start/developing-smart-contracts/setup-environment.mdx +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/setup-environment.mdx @@ -4,8 +4,6 @@ This guide covers basic steps of setting up your smart-contract development environment using **`Blueprint SDK`** and creating basic project template. -But before we proceed to actual coding let's install and setup all required tools! - ## Prerequisites - Basic programming skills. diff --git a/docs/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods.mdx b/docs/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods.mdx index 5c65b05dd3..2bbc227ea5 100644 --- a/docs/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods.mdx +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods.mdx @@ -9,15 +9,26 @@ import TabItem from '@theme/TabItem'; If you are stuck on some of the examples you can find original template project with all modifications performed during this guide [here](https://github.com/ton-community/ton-onboarding-sandbox/tree/main/quick-start/smart-contracts/Example). ::: -While it's technically possible to create smart contract on TON not having any persistent storage, almost all smart-contracts need to store their `state` between transactions. This guide explains standard ways of managing `state` of smart-contract and using `get methods` to obtain it from outside the blockchain. +While it's technically possible to create smart contract on TON not having any persistent storage, almost all smart-contracts need to store their `data` between transactions. This guide explains standard ways of managing `storage` of smart-contract and using `get methods` to obtain it from outside the blockchain. -## Cell Structure: The Backbone of TON Storage + +## Smart Contract Storage Operations + +First thing that we should say is that there is a small, but important difference between smart-contract `persistent data` and [TVM storage](/v3/documentation/tvm/tvm-initialization#initial-state) existing only during execution of smart-contract. Remember that smart-contracts follow **transaction** concept - if any system or user exception is raised during execution, i.e. transaction fails - `TVM storage` will not be committed to smart-contract `persistent data`. From the realization point this means that smart-contract `persistent data` is copied to `TVM storage` before starting the execution and committed back, optionally modified, in case of successful **transaction**. For simplification of this guide we will not use those strict terms, instead we will describe both as `storage` and rely on context, so, keep these facts in mind. + +There are two main instructions that provide access to smart-contract storage: + - `get_data()` returning current storage cell. + - `set_data()` setting storage cell. + +Lets examine `Cell` entity structure to understand how we could manage contract storage. + +## Cell structure TON blockchain uses a data structure called **`Cell`** as the fundamental unit for storing data. Cells are the building blocks of the `TVM` (TON Virtual Machine) and have those characteristics: - A `Cell` can store up to 1023 bits (approximately 128 bytes) of data -- A `Cell` can reference up to 4 other `Cells` (children) -- `Cells` are immutable once created +- A `Cell` can reference up to 4 other `Cells` (childrens) +- `Cell` is immutable once created You can think of Cell as the following structure: @@ -29,19 +40,17 @@ interface Cell { } ``` -## Smart Contract Storage Operations +## Implementation -First thing that we should say is that there is a small, but important difference between smart-contract `persistent data` and [TVM storage](/v3/documentation/tvm/tvm-initialization#initial-state) existing only during execution of smart-contract. Remember that smart-contracts follow **transaction** concept - if any system or user exception is raised during execution, i.e. transaction fails - `TVM storage` will not be committed to smart-contract `persistent data`. From the realization point this means that smart-contract `persistent data` is copied to `TVM storage` before starting the execution and committed back, optionally modified, in case of successful **transaction**. For simplification of this guide we will not use those strict terms, instead we will describe both as `storage` and rely on context, so, keep these facts in mind. +Let's try to modify our smart-contract folowing standard steps decsribed in previous [Blueprint SDK overview](/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods) section. -There are two main instructions that provide access to smart-contract storage: - - `get_data()` returning current storage cell. - - `set_data()` setting current storage cell. +### Step1: Edit smart-contract code In case it's inconvenient to always serialize and deserialize storage cell, there is a pretty standard practice to define two wrapper methods that provide corresponding logic. If you didn't change smart-contract code it should contain following lines: -```func +```func hello_world.fc global int ctx_id; global int ctx_counter; @@ -87,15 +96,15 @@ fun saveData() { beginCell() .storeUint(ctxID, 32) .storeUint(ctxCounter, 32) - .endCell() - ); + .endCell()); } ``` +#### Managing storage -Let's try to modify our example a little bit. First, let's use a more common approach of passing storage members as parameters to `save_data(members...)` and retrieve them as `(members...) get_data()` moving global variables ctx_id and ctx_counter to method bodies. Also, let's rebane our counter to seqno and add additional integers 256-bit size into our storage: +Let's try to modify our example a little bit. First, let's use a more common approach of passing storage members as parameters to `save_data(members...)` and retrieve them as `(members...) get_data()` moving global variables ctx_id and ctx_counter to method bodies. Also, let's add additional integer of 256-bit size into our storage: Result of our modifications should look like this: @@ -109,8 +118,8 @@ Result of our modifications should look like this: ;; id is required to be able to create different instances of counters ;; since addresses in TON depend on the initial state of the contract int ctx_id = ds~load_uint(32); - int seqno = ds~load_uint(32); - int public_key = ds~load_uint(256); + int ctx_counter = ds~load_uint(32); + int ctx_counterExt = ds~load_uint(256); ds.end_parse(); @@ -119,12 +128,12 @@ Result of our modifications should look like this: ;; save_data stores variables as a cell into persistent storage ;; impure because of writing into TVM storage -() save_data(int ctx_id, int seqno, int public_key) impure { +() save_data(int ctx_id, int seqno, int ctxCounterExt) impure { set_data( begin_cell() .store_uint(ctx_id, 32) - .store_uint(seqno, 32) - .store_uint(public_key, 256) + .store_uint(ctxCounter, 32) + .store_uint(ctxCounterExt, 256) .end_cell() ); } @@ -140,12 +149,12 @@ fun loadData(): (int, int, int) { // id is required to be able to create different instances of counters // since addresses in TON depend on the initial state of the contract var ctxID = ds.loadUint(32); - var seqno = ds.loadUint(32); - var public_key = ds.loadUint(256); + var ctxCounter = ds.loadUint(32); + var ctxCounterExt = ds.loadUint(256); ds.assertEndOfSlice(); - return (ctxID, seqno, public_key); + return (ctxID, ctxCounter, ctxCounterExt); } // saveData stores storage variables as a cell into persistent storage @@ -153,8 +162,8 @@ fun saveData(ctxID: int, seqno: int, public_key: int) { setContractData( beginCell() .storeUint(ctxID, 32) - .storeUint(seqno, 32) - .storeUint(public_key, 256) + .storeUint(ctxCounter, 32) + .storeUint(ctxcounterExt, 256) .endCell() ); } @@ -167,38 +176,36 @@ Don't forget to delete global variables `ctx_id`, `ctx_counter` and modify usage ```func -var (ctx_id, seqno, public_key) = load_data(); -save_data(ctx_id, seqno, public_key); +var (ctx_id, ctxCounter, ctxCounterExt) = load_data(); +save_data(ctx_id, ctxCounter, ctxCounterExt); ``` ```tolk -var (ctx_id, seqno, public_key) = load_data(); -save_data(ctx_id, seqno, public_key); +var (ctx_id, ctxCounter, ctxCounterExt) = load_data(); +save_data(ctx_id, ctxCounter, ctxCounterExt); ``` -## Get methods +#### Get methods The primary use of get methods is reading our storage data from outside the blockchain using a convenient interface, primarily to extract data that is required to prepare a transaction. -Let's omit at the current moment the motivation of magical storage members `seqno` and `public_key` - we will discuss their meaning in later topics. Instead, let's provide a get method to retrieve both of them from outside the blockchain: - ```func -(int, int) get_seqno_public_key() method_id { - var (_, seqno, public_key) = load_data(); - return (seqno, public_key); +(int, int) get_counters() method_id { + var (_, ctxCounter, ctxCounterExt) = load_data(); + return (ctxCounter, ctxCounterExt); } ``` ```tolk -get get_seqno_public_key(): (int, int) { - var (_, seqno, public_key) = load_data(); - return (seqno, public_key); +get get_counters(): (int, int) { + var (_, ctxCounter, ctxCounterExt) = load_data(); + return (ctxCounter, ctxCounterExt); } ``` @@ -212,7 +219,7 @@ npm run build And that's it! In practice all get methods follow this simple flow and don't require anything more. Note that you can omit values returned from functions using '_' syntax. -## Updating wrapper +## Step2: Updating wrapper Now lets update our wrapper class corresponding to new storage layout and new `get method`. @@ -221,15 +228,15 @@ First, let's modify `helloWorldConfigToCell` function and `HelloWorldConfig` typ ```typescript export type HelloWorldConfig = { id: number; - seqno: number; - public_key: bigint; + ctxCounter: number; + ctxCounterExt: bigint; }; export function helloWorldConfigToCell(config: HelloWorldConfig): Cell { return beginCell() .storeUint(config.id, 32) - .storeUint(config.seqno, 32) - .storeUint(config.public_key, 256) + .storeUint(config.ctxCounter, 32) + .storeUint(config.ctxcounterExt, 256) .endCell(); } ``` @@ -237,20 +244,15 @@ Second, add a method to perform a request for the newly created get method: ```typescript async getSeqnoPKey(provider: ContractProvider) { - const result = await provider.get('get_seqno_public_key', []); - const seqno = result.stack.readNumber(); - const publicKeyBigInt = result.stack.readBigNumber(); + const result = await provider.get('get_counters', []); + const counter = result.stack.readNumber(); + const counterExt = result.stack.readBigNumber(); - // Convert BigInt to Buffer (32 bytes) - const publicKeyHex = publicKeyBigInt.toString(16).padStart(64, '0'); - - const publicKeyBuffer = Buffer.from(publicKeyHex, 'hex'); - - return [seqno, publicKeyBuffer] + return [counter, counterExt] } ``` -## Updating Tests +## Step3: Updating tests And finally, let's write a simple test, checking that the deployment process initializes smart contract storage and correctly retrieves its data by get methods. @@ -261,9 +263,8 @@ helloWorld = blockchain.openContract( HelloWorld.createFromConfig( { id: 0, - seqno: 0, - //it will be changed later, just initialization check - public_key: 0n + ctxCounter: 0, + ctxCounterExt: 0n }, code ) @@ -277,22 +278,22 @@ it('should correctly initialize and return the initial data', async () => { // Define the expected initial values (same as in beforeEach) const expectedConfig = { id: 0, - seqno: 0, - public_key: 0n + ctxCounter: 0, + ctxCounterExt: 0n }; // Verify counter value const counter = await helloWorld.getCounter(); - expect(counter).toBe(expectedConfig.seqno); + expect(counter).toBe(expectedConfig.ctxCounter); // Verify ID value const id = await helloWorld.getID(); expect(id).toBe(expectedConfig.id); - // Verify seqno and public_key values - const [seqno, publicKey] = await helloWorld.getSeqnoPKey(); - expect(seqno).toBe(expectedConfig.seqno); - expect(publicKey).toBe(expectedConfig.public_key); + // Verify both counters + const [ctx, ctxExt] = await helloWorld.getSeqnoPKey(); + expect(ctx).toBe(expectedConfig.ctxCounter); + expect(ctxExt).toBe(expectedConfig.ctxCounterExt); }); ``` @@ -302,9 +303,3 @@ And now run your new test script by executing the following command: npm run test ``` -## Next Steps - -Congratulations! We modified our first contract to execute a new `get method`, learned about smart-contract storage and went through the standard smart contract development flow. Now we can proceed to further sections explaining more complex actions upon smart contracts. At this point we will provide a more short description of standard actions: edit smart-contract -> edit wrapper -> edit tests, relying on your new skills. - -Consider reading your chosen [language specification](/v3/documentation/smart-contracts/overview#programming-languages) and try to add your own `get method` with corresponding wrapper and test modification by yourself and proceed to next steps when you feel ready for it. - diff --git a/docs/v3/guidelines/quick-start/getting-started.mdx b/docs/v3/guidelines/quick-start/getting-started.mdx index 32372d9a01..37ac569acd 100644 --- a/docs/v3/guidelines/quick-start/getting-started.mdx +++ b/docs/v3/guidelines/quick-start/getting-started.mdx @@ -32,7 +32,7 @@ Available interfaces of smart-contract are: - Receiving **`external messages`** from outside the blockchain. - Receiving **`get methods`** request from outside the blockchain. -In contrast to `internal` and `external` messages, `get methods` are not what you can consider as a **transaction**. Those are special functions of smart contract that cannot change contract internal state or proceed any other action except querying specific data from contract state. Contrary to what might seem intuitive, invoking `get methods` from other contracts **is not possible**, primarily due to the nature of blockchain technology and the need for consensus. +In contrast to `internal` and `external` messages, `get methods` are not what you can consider as a **transaction**. Those are special functions of smart contract that cannot change contract internal state or proceed any other action except querying specific data from contract state. Contrary to what might seem intuitive, invoking `get methods` from other contracts **is not possible**. :::tip Hereinafter we will use terms `actor`, `smart-contract` and `account` interchangeably. @@ -44,14 +44,16 @@ Before we step on our way of becoming a TON developer, we should become an advan ### Step 1: Create a new wallet using an app -The simplest way to create a `wallet` is to visit https://ton.org/wallets and choose one of the wallet apps from the list. This page explains the difference between **custodial** and **non-custodial** wallets. With a **non-custodial** wallet, you own the wallet and hold its **private key** by yourself. With a **custodial** wallet, you trust somebody else to do this for you. They are all pretty similar, let's choose [Tonkeeper](https://tonkeeper.com/). Go ahead, install and run it. +The simplest way to create a `wallet` is to visit https://ton.org/wallets and choose one of the wallet apps from the list. They are all pretty similar, let's choose [Tonkeeper](https://tonkeeper.com/). Go ahead, install and run it. ### Step 2: Mainnet and Testnet -In the TON, the `Mainnet` and `Testnet` have distinct roles. The `Mainnet` is the primary network where actual transactions take place, carrying real economic value as they involve real cryptocurrency and staked validators executing our transactions and guarantee a very high level of security. On the other hand, the `Testnet` is a testing version of the TON **blockchain** designed for development and testing purposes. Transactions on the `testnet` do not have real economic value, making it a risk-free zone for developers to test without financial implications. It's mainly used for development, testing `smart contracts`, and trying new features. +In the TON, the `Mainnet` and `Testnet` have distinct roles. The `Mainnet` is the primary network where actual transactions take place, carrying real economic value as they involve real cryptocurrency. On the other hand, the `Testnet` is a testing version of the TON **blockchain** designed for development and testing purposes. `Testnet` is a risk-free zone for developers to test without financial implications. It's mainly used for development, testing `smart contracts`, and trying new features. Since TON basic transactions are very cheap, about 1 cent per transaction, investing just $5 will be enough for hundreds of them. You can get TON coins by simply pressing the buy button in the user interface, or ask someone to send them to your `address`, which you can copy from the wallet app somewhere near your balance. Don't worry, sharing your `address` is **totally safe**, unless you don't want it to be associated with you. +For the `Testnet` version, you can request funds from the [Testgiver Ton Bot](https://t.me/testgiver_ton_bot). After a short wait, you will receive 2 TONs that will appear in your wallet app. + ### Step 3: Creating testnet wallet If you decide to use the `Testnet` version instead, you can do to it by following guide below. @@ -79,13 +81,14 @@ To create testnet wallet click *`Wallet`* -> *`Add Wallet`* - *`TestnetAccount`*
-For the `Testnet` version, you can request funds from the [Testgiver Ton Bot](https://t.me/testgiver_ton_bot). After a short wait, you will receive 2 TONs that will appear in your wallet app. ### Step 4: Exploring the blockchain -Congratulations! We created our first wallet and received some funds on it. Now let's take a look at how our actions are reflected in the `blockchain`. We can do this by using various [explorers](https://ton.app/explorers). An explorer is a tool that allows you to query data from the chain, investigate TON `smart-contracts`, and transactions. +Congratulations! We created our first wallet and received some funds on it. Now let's take a look at how our actions are reflected in the `blockchain`. We can do this by using various [explorers](https://ton.app/explorers). An explorer is a tool that allows you to query data from the chain, investigate TON `smart-contracts`, and transactions. For our examples, we are going to use [TonViewer](https://tonviewer.com/). -For our examples, we are going to use [TonViewer](https://tonviewer.com/). Note that in case of using `testnet`, you should manually change the explorer mode to the `testnet` version. Don't forget that these are different networks not sharing any transactions or `smart-contracts`, so your `testnet` wallet would not be visible in `mainnet` mode and vice versa. +:::tip +Note that in case of using `testnet`, you should manually change the explorer mode to the `testnet` version. Don't forget that these are different networks not sharing any transactions or `smart-contracts`, so your `testnet` wallet would not be visible in `mainnet` mode and vice versa. +::: Let's take a look at our newly created wallet using the explorer: copy your wallet address from the app and insert it into the search line of the explorer like this: diff --git a/sidebars/guidelines.js b/sidebars/guidelines.js index 0b94297b0e..ec4b1a22f3 100644 --- a/sidebars/guidelines.js +++ b/sidebars/guidelines.js @@ -51,7 +51,7 @@ module.exports = [ label: 'Developing smart-contracts', items: [ 'v3/guidelines/quick-start/developing-smart-contracts/setup-environment', - 'v3/guidelines/quick-start/developing-smart-contracts/program-structure', + 'v3/guidelines/quick-start/developing-smart-contracts/blueprint-sdk-overview', 'v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods', 'v3/guidelines/quick-start/developing-smart-contracts/processing-messages', 'v3/guidelines/quick-start/developing-smart-contracts/deploying-to-network' From 86fe71ad8d668bae920f528c094e0eb1f6a2d03b Mon Sep 17 00:00:00 2001 From: Alexey Ostrovsky Date: Mon, 31 Mar 2025 21:42:56 +0300 Subject: [PATCH 2/8] Feedback fixes(draft), cspell --- .../developing-smart-contracts/storage-and-get-methods.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods.mdx b/docs/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods.mdx index 2bbc227ea5..92cea4d6f5 100644 --- a/docs/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods.mdx +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods.mdx @@ -27,7 +27,7 @@ Lets examine `Cell` entity structure to understand how we could manage contract TON blockchain uses a data structure called **`Cell`** as the fundamental unit for storing data. Cells are the building blocks of the `TVM` (TON Virtual Machine) and have those characteristics: - A `Cell` can store up to 1023 bits (approximately 128 bytes) of data -- A `Cell` can reference up to 4 other `Cells` (childrens) +- A `Cell` can reference up to 4 other `Cells` (children's) - `Cell` is immutable once created You can think of Cell as the following structure: From 7547ea3e811f1a5f5137b66ac651957cdbf4aa2a Mon Sep 17 00:00:00 2001 From: Alexey Ostrovsky Date: Tue, 1 Apr 2025 15:19:38 +0300 Subject: [PATCH 3/8] Refactoring steps(draft) --- .../processing-messages.mdx | 177 +++++++----------- .../storage-and-get-methods.mdx | 30 +-- 2 files changed, 81 insertions(+), 126 deletions(-) diff --git a/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx b/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx index 168b8e2aa4..bd4ad8042b 100644 --- a/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx @@ -33,6 +33,7 @@ What we specifically are interested in is the source address of message, by obta ```func +;; This is NOT a part of the project, just an example. () recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { ;; Parse the sender address from in_msg_full slice cs = in_msg_full.begin_parse(); @@ -59,6 +60,7 @@ What we specifically are interested in is the source address of message, by obta ```tolk +// This is NOT a part of the project, just an example. fun onInternalMessage(myBalance: int, msgValue: int, msgFull: cell, msgBody: slice) { // Parse the sender address from in_msg_full var cs: slice = msgFull.beginParse(); @@ -89,6 +91,7 @@ Another common pattern in TON contracts is to include a **32-bit operation code* ```func +;; This is NOT a part of the project, just an example! const int op::increment = 1; const int op::decrement = 2; @@ -118,6 +121,7 @@ const int op::decrement = 2; ```tolk +//This is NOT a part of the project, just an example! const op::increment : int = 1; const op::decrement : int = 2; @@ -149,7 +153,7 @@ fun onInternalMessage(myBalance: int, msgValue: int, msgFull: cell, msgBody: sli By combining both of these patterns you can achieve a comprehensive description of your smart-contract's systems ensuring secure interaction between them and unleash full potential of TON actors model. -### Implementation in Tact +## Implementation in Tact Tact is a high-level programming language for the TON Blockchain, focused on efficiency and simplicity. It is designed to be easy to learn and use while being well-suited for smart contract development. Tact is a statically typed language with a simple syntax and a powerful type system. @@ -157,7 +161,9 @@ Tact is a high-level programming language for the TON Blockchain, focused on eff For more details, refer to the [Tact documentation](https://docs.tact-lang.org/#start) and [Tact By Example](https://tact-by-example.org/00-hello-world). ::: -#### Creating a Tact contract +Let's create and modify our smart-contract folowing standard steps decsribed in previous [Blueprint SDK overview](/v3/guidelines/quick-start/developing-smart-contracts/blueprint-sdk-overview) section. + +### Step1: Creating and modifying Tact contract First, let's create Tact contract. @@ -165,7 +171,54 @@ First, let's create Tact contract. npx blueprint create CounterInternal --type tact-counter ``` -At the top of the generated contract file, you may see a [message](https://docs.tact-lang.org/book/structs-and-messages/) definition: +Resulted project structure should look like this: + + + +```ls title="Project structure" +Example/ +├─ contracts/ # Smart contract source code +│ ├─ counter_internal.tact # CounterInternal contract in Tact language +│ ├─ hello_world.fc # HelloWorld contract in FunC language +├─ scripts/ # Deployment and interaction scripts +│ ├─ deployCounterInternal.ts # Script to deploy contract +│ ├─ deployHelloWorld.ts # Script to deploy contract +│ ├─ incrementCounterInternal.ts # Script to increment counter +│ └─ incrementHelloWorld.ts # Script to increment counter +├─ tests/ # Test suite for contracts +│ ├─ CounterInternal.spec.ts # Tests for CounterInternal contract +│ └─ HelloWorld.spec.ts # Tests for HelloWorld contract +└─ wrappers/ # Wrappers for contract interaction + ├─ CounterInternal.compile.ts # Compilation configuration + ├─ CounterInternal.ts # Wrapper for CounterInternal contract + ├─ HelloWorld.compile.ts # Compilation configuration + └─ HelloWorld.ts # Wrapper for HelloWorld contract +``` + + +```ls title="Project structure" +Example/ +├─ contracts/ # Smart contract source code +│ ├─ counter_internal.tact # CounterInternal contract in Tact language +│ ├─ hello_world.tolk # HelloWorld contract in FunC language +├─ scripts/ # Deployment and interaction scripts +│ ├─ deployCounterInternal.ts # Script to deploy contract +│ ├─ deployHelloWorld.ts # Script to deploy contract +│ ├─ incrementCounterInternal.ts # Script to increment counter +│ └─ incrementHelloWorld.ts # Script to increment counter +├─ tests/ # Test suite for contracts +│ ├─ CounterInternal.spec.ts # Tests for CounterInternal contract +│ └─ HelloWorld.spec.ts # Tests for HelloWorld contract +└─ wrappers/ # Wrappers for contract interaction + ├─ CounterInternal.compile.ts # Compilation configuration + ├─ CounterInternal.ts # Wrapper for CounterInternal contract + ├─ HelloWorld.compile.ts # Compilation configuration + └─ HelloWorld.ts # Wrapper for HelloWorld contract +``` + + + +At the top of the generated contract file: `counter_internal.tolk`, you may see a [message](https://docs.tact-lang.org/book/structs-and-messages/) definition: ```tact message Add { @@ -308,7 +361,7 @@ Note, that the `owner` getter is automatically defined via the `Ownable` trait. #### Complete contract -```tact +```tact title="CounterInternal.tact" import "@stdlib/deploy"; import "@stdlib/ownable"; @@ -354,7 +407,7 @@ contract CounterInternal with Deployable, Ownable { } ``` -#### Using contract wrappers +### Step2: Using contract wrappers [Wrappers](https://docs.tact-lang.org/book/compile/#wrap-ts) facilitate contract interaction from TypeScript. Unlike in FunC or Tolk, they are generated automatically during the build process: @@ -362,124 +415,26 @@ contract CounterInternal with Deployable, Ownable { export * from '../build/CounterInternal/tact_CounterInternal'; ``` +### Step3: Updating tests + +TODO: add separate test --- ## External Messages -`External messages` are your main way of toggling smart contract logic from outside the blockchain. Usually, there is no need for implementation of them in smart contracts because in most cases you don't want external entry points to be accessible to anyone except you. If this is all functionality that you want from external section - standard way is to delegate this responsibility to separate actor - `wallet` which is practically the main reason they were designed for. - -### Wallets - -When we sent coins using wallet app in [getting started](/v3/guidelines/quick-start/getting-started#step-3-exploring-the-blockchain) section what `wallet app` actually performs is sending `external message` to your `wallet` smart contract which performs sending message to destination smart-contract address that you wrote in send menu. While most wallet apps during creation of wallet deploy most modern versions of wallet smart contracts - `v5`, providing more complex functionality, let's examine `recv_external` section of more basic one - `v3`: - -```func -() recv_external(slice in_msg) impure { - var signature = in_msg~load_bits(512); - var cs = in_msg; - var (subwallet_id, msg_seqno) = (cs~load_uint(32), cs~load_uint(32)); - var ds = get_data().begin_parse(); - var (stored_seqno, stored_subwallet, public_key) = (ds~load_uint(32), ds~load_uint(32), ds~load_uint(256)); - ds.end_parse(); - throw_unless(33, msg_seqno == stored_seqno); - throw_unless(34, subwallet_id == stored_subwallet); - throw_unless(35, check_signature(slice_hash(in_msg), signature, public_key)); - accept_message(); - cs~touch(); - while (cs.slice_refs()) { - var mode = cs~load_uint(8); - send_raw_message(cs~load_ref(), mode); - } - set_data(begin_cell() - .store_uint(stored_seqno + 1, 32) - .store_uint(stored_subwallet, 32) - .store_uint(public_key, 256) - .end_cell()); - } -``` - +`External messages` are your only way of toggling smart contract logic from outside the blockchain. Usually, there is no need for implementation of them in smart contracts because in most cases you don't want external entry points to be accessible to anyone except you. If this is all functionality that you want from external section - standard way is to delegate this responsibility to separate actor - [wallet](v3/documentation/smart-contracts/contracts-specs/wallet-contracts#basic-wallets) which is practically the main reason they were designed for. -First thing to mention - is `signature` in message body and stored `public_key`. This refers to standard mechanism of asymmetric cryptography: during deployment process you create **private and public key pair**, store the second one in initial contract storage and then during sending external message through client **sign** it with **private key** attaching calculated `signature` to message body. Smart contract on its side checks if signature matches `public_key` and accepts external message if it is so. +Developing external endpoint includes several standard [approuches](/v3/documentation/smart-contracts/message-management/external-messages) and [security measures](/v3/guidelines/smart-contracts/security/overview) that might be overwhelming at this point. Therefore in this guide we will realize incrementing previously added to `hello_world` contract `ctxCounterExt` and add send message to out `Tact` contract. -Standard signature system for TON smart-contracts is `Ed25519` which is directly provided by `TVM` instruction `check_signature()`, but you can always implement another preferred algorithm by yourself. - -:::tip -When you entered **magic 24 secret words** (i.e. **mnemonic phrase**) during [wallet creation](/v3/documentation/data-formats/tlb/tl-b-language) in your app what is practically performed is concatenation of those words into one string and hashing it to create your **private key**. So remember not to show them to anyone. +:::danger +This implementation is **unsafe** and may lead to loosing your contract funds. Don't deploy it to `mainnet`, especially with high smart-contract balance. ::: -Second thing is `seqno` (sequential number) as you can see this is practically just a counter that increments each time wallet smart-contract receives external message, but why do we need one? -The reason behind that lies in blockchain nature: since all transactions are visible to anyone, potential malefactor could repeatedly send already signed transaction to your smart contract. - -> **Simpliest scenario:** you transfer some amount of funds to receiver, receiver examines transaction and sends it to your contract repeatedly until you run out of funds and receiver gains almost all of them. - -Third thing is `subwallet_id` that just checks equality to the stored one, we will discuss its meaning a little bit later in internal messages section. +## Implementation -### Implementation -At this point reasons behind changes that we made to our counter in previous storage and get methods [section](/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods#smart-contract-storage-operations) should start to be more clear! We already prepared our storage to contain `seqno`, `public_key` and `ctx_id` which will serve same task as `subwallet_id` so let's adapt wallet's `recv_external` function to our project: - - - -```func -() recv_external(slice in_msg) impure { - ;; retrives validating data from message body - var signature = in_msg~load_bits(512); - var cs = in_msg; - var (ctx_id, msg_seqno) = (cs~load_uint(32), cs~load_uint(32)); - - ;; retrieves stored data for validation checks - var (stored_id, stored_seqno, public_key) = load_data(); - ;; replay protection mechanism through seqno chack and incrementing - throw_unless(33, msg_seqno == stored_seqno); - ;; id field for multiply addresess with same private_key - throw_unless(34, ctx_id == stored_id); - ;; ed25519 signature check - throw_unless(35, check_signature(slice_hash(in_msg), signature, public_key)); - ;; accepting message after all checks - accept_message(); - ;; optimization technique - ;; putting message body to stack head - cs~touch(); - ;; sending serialized on client side messages - while (cs.slice_refs()) { - var mode = cs~load_uint(8); - send_raw_message(cs~load_ref(), mode); - } - save_data(stored_id, stored_seqno + 1, public_key); -} -``` - - -```tolk -fun acceptExternalMessage(): void - asm "ACCEPT"; - -fun onExternalMessage(inMsg: slice) { - var signature = inMsg.loadBits(512); - var cs = inMsg; - var (ctx_id, msg_seqno) = (cs.loadUint(32), cs.loadUint(32)); - - // retrieves stored data for validation checks - var (stored_id, stored_seqno, public_key) = loadData(); - // replay protection mechanism through seqno chack and incrementing - assert(msg_seqno == stored_seqno, 33); - // id field for multiply addresess with same private_key - assert(ctx_id == stored_id, 34); - // ed25519 signature check - assert(isSignatureValid(sliceHash(inMsg), signature, public_key), 35); - // accepting message after all checks - acceptExternalMessage(); - // sending serialized on client side messages - while (!cs.isEndOfSliceRefs()) { - var mode = cs.loadUint(8); - sendRawMessage(cs.loadRef(), mode); - } - saveData(stored_id, stored_seqno + 1, public_key); -} -``` - - And add wrapper method to call it through our wrapper class: diff --git a/docs/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods.mdx b/docs/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods.mdx index 92cea4d6f5..8092054488 100644 --- a/docs/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods.mdx +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods.mdx @@ -42,7 +42,7 @@ interface Cell { ## Implementation -Let's try to modify our smart-contract folowing standard steps decsribed in previous [Blueprint SDK overview](/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods) section. +Let's try to modify our smart-contract folowing standard steps decsribed in previous [Blueprint SDK overview](/v3/guidelines/quick-start/developing-smart-contracts/blueprint-sdk-overview) section. ### Step1: Edit smart-contract code @@ -50,7 +50,7 @@ In case it's inconvenient to always serialize and deserialize storage cell, ther -```func hello_world.fc +```func title="hello_world.fc" global int ctx_id; global int ctx_counter; @@ -76,7 +76,7 @@ global int ctx_counter; ``` -```tolk +```tolk title="hello_world.tolk" global ctxID: int; global ctxCounter: int; @@ -110,7 +110,7 @@ Result of our modifications should look like this: -```func +```func title="hello_world.fc" ;; load_data retrieves variables from TVM storage cell (int, int, int) load_data() { var ds = get_data().begin_parse(); @@ -140,7 +140,7 @@ Result of our modifications should look like this: ``` -```tolk +```tolk title="hello_world.tolk" // load_data retrieves variables from TVM storage cell // impure because of writting into global variables fun loadData(): (int, int, int) { @@ -175,13 +175,13 @@ Don't forget to delete global variables `ctx_id`, `ctx_counter` and modify usage -```func +```func title="hello_world.fc" var (ctx_id, ctxCounter, ctxCounterExt) = load_data(); save_data(ctx_id, ctxCounter, ctxCounterExt); ``` -```tolk +```tolk title="hello_world.tolk" var (ctx_id, ctxCounter, ctxCounterExt) = load_data(); save_data(ctx_id, ctxCounter, ctxCounterExt); ``` @@ -194,7 +194,7 @@ The primary use of get methods is reading our storage data from outside the bloc -```func +```func title="hello_world.fc" (int, int) get_counters() method_id { var (_, ctxCounter, ctxCounterExt) = load_data(); return (ctxCounter, ctxCounterExt); @@ -202,7 +202,7 @@ The primary use of get methods is reading our storage data from outside the bloc ``` -```tolk +```tolk title="hello_world.tolk" get get_counters(): (int, int) { var (_, ctxCounter, ctxCounterExt) = load_data(); return (ctxCounter, ctxCounterExt); @@ -219,13 +219,13 @@ npm run build And that's it! In practice all get methods follow this simple flow and don't require anything more. Note that you can omit values returned from functions using '_' syntax. -## Step2: Updating wrapper +### Step2: Updating wrapper Now lets update our wrapper class corresponding to new storage layout and new `get method`. First, let's modify `helloWorldConfigToCell` function and `HelloWorldConfig` type to properly initialize our storage during deployment: -```typescript +```typescript title="HelloWorld.ts" export type HelloWorldConfig = { id: number; ctxCounter: number; @@ -242,7 +242,7 @@ export function helloWorldConfigToCell(config: HelloWorldConfig): Cell { ``` Second, add a method to perform a request for the newly created get method: -```typescript +```typescript title="HelloWorld.ts" async getSeqnoPKey(provider: ContractProvider) { const result = await provider.get('get_counters', []); const counter = result.stack.readNumber(); @@ -252,13 +252,13 @@ async getSeqnoPKey(provider: ContractProvider) { } ``` -## Step3: Updating tests +### Step3: Updating tests And finally, let's write a simple test, checking that the deployment process initializes smart contract storage and correctly retrieves its data by get methods. First, let's update the `before each` section and particularly the `openContract` logic with the following one: -```typescript +```typescript title="HelloWorld.spec.ts" helloWorld = blockchain.openContract( HelloWorld.createFromConfig( { @@ -273,7 +273,7 @@ helloWorld = blockchain.openContract( And add new test case for get methods: -```typescript +```typescript title="HelloWorld.spec.ts" it('should correctly initialize and return the initial data', async () => { // Define the expected initial values (same as in beforeEach) const expectedConfig = { From 48e24d315f2b0d5ac649349df90901bccdc0e093 Mon Sep 17 00:00:00 2001 From: Alexey Ostrovsky Date: Tue, 1 Apr 2025 19:26:17 +0300 Subject: [PATCH 4/8] Finishing simplified contracts(draft) --- .../processing-messages.mdx | 348 +++++++++--------- 1 file changed, 178 insertions(+), 170 deletions(-) diff --git a/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx b/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx index bd4ad8042b..efd4b65267 100644 --- a/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx @@ -163,7 +163,7 @@ For more details, refer to the [Tact documentation](https://docs.tact-lang.org/# Let's create and modify our smart-contract folowing standard steps decsribed in previous [Blueprint SDK overview](/v3/guidelines/quick-start/developing-smart-contracts/blueprint-sdk-overview) section. -### Step1: Creating and modifying Tact contract +### Step 1: Creating and modifying Tact contract First, let's create Tact contract. @@ -407,127 +407,36 @@ contract CounterInternal with Deployable, Ownable { } ``` -### Step2: Using contract wrappers +Verify that smart-contract code is correct by running build script: -[Wrappers](https://docs.tact-lang.org/book/compile/#wrap-ts) facilitate contract interaction from TypeScript. Unlike in FunC or Tolk, they are generated automatically during the build process: - -```typescript ./wrappers/CounterInternal.ts -export * from '../build/CounterInternal/tact_CounterInternal'; +```bash +npm blueprint build ``` -### Step3: Updating tests - -TODO: add separate test - ---- - -## External Messages - -`External messages` are your only way of toggling smart contract logic from outside the blockchain. Usually, there is no need for implementation of them in smart contracts because in most cases you don't want external entry points to be accessible to anyone except you. If this is all functionality that you want from external section - standard way is to delegate this responsibility to separate actor - [wallet](v3/documentation/smart-contracts/contracts-specs/wallet-contracts#basic-wallets) which is practically the main reason they were designed for. - -Developing external endpoint includes several standard [approuches](/v3/documentation/smart-contracts/message-management/external-messages) and [security measures](/v3/guidelines/smart-contracts/security/overview) that might be overwhelming at this point. Therefore in this guide we will realize incrementing previously added to `hello_world` contract `ctxCounterExt` and add send message to out `Tact` contract. - -:::danger -This implementation is **unsafe** and may lead to loosing your contract funds. Don't deploy it to `mainnet`, especially with high smart-contract balance. -::: - -## Implementation - - - - -And add wrapper method to call it through our wrapper class: - -```typescript -async function sendExternal( - provider: ContractProvider, - opts: { - mode: number - message: Cell, - secret_key: Buffer - } -) { - const seqno = await this.getCounter(provider) - const id = await this.getID(provider) +### Step 2: Using contract wrappers - const toSign = beginCell() - .storeUint(id, 32) - .storeUint(seqno, 32) - .storeUint(opts.mode, 8) - .storeRef(opts.message) - - const signature = sign(toSign.endCell().hash(), opts.secret_key) +[Wrappers](https://docs.tact-lang.org/book/compile/#wrap-ts) facilitate contract interaction from TypeScript. Unlike in FunC or Tolk, they are generated automatically during the build process: - return await provider.external(beginCell() - .storeBuffer(signature) - .storeBuilder(toSign) - .endCell() - ); -} +```typescript ./wrappers/CounterInternal.ts +export * from '../build/CounterInternal/tact_CounterInternal'; ``` -Here we are preparing all required values for contract checks and add payload message that will be sent as internal message further to final receiver smart-contract. - -As you can see here we are using `provider.external()` method instead of `provider.internal()` one which requires a via argument, that now we can start to understand. The thing is that `provider.internal()` method is doing practically the same thing that we are trying to implement: since we can't directly call internal methods to test them we need to wrap them into external message and send it through some wallet. - -Now let's test our implementation: - -```typescript -it('should send an external message containing an internal message', async () => { - const receiver = await blockchain.treasury('receiver'); - - const internalMessage = beginCell() - .storeUint(0, 32) // Simple message with no specific opcode - .storeUint(42, 64) // queryID = 42 - .storeStringTail('Hello from external message!') - .endCell(); - - const messageToSend = beginCell().store(storeMessageRelaxed(internal({ - to: receiver.address, - value: toNano(0.01), - body: internalMessage, - bounce: true, - }))).endCell(); - - const receiverBalanceBefore = await receiver.getBalance(); - - const result = await helloWorld.sendExternal({ - mode: SendMode.PAY_GAS_SEPARATELY, - message: messageToSend, - secret_key: keyPair.secretKey - }); - - expect(result.transactions).toHaveTransaction({ - from: undefined, // External messages have no 'from' address - to: helloWorld.address, - success: true, - }); - - expect(result.transactions).toHaveTransaction({ - from: helloWorld.address, - to: receiver.address, - success: true, - }); - - const receiverBalanceAfter = await receiver.getBalance(); - - expect(receiverBalanceAfter).toBeGreaterThan(receiverBalanceBefore); - const [seqnoAfter] = await helloWorld.getSeqnoPKey(); - expect(seqnoAfter).toBe(1); // Since it should start from 0 and increment to 1 -}); -``` +### Step 3: Updating tests -#### Testing the multi-contract system +Now let's ensure that our smart-contract failes if non-owner tries to increment smart-contract by adding this test: -The following test case demonstrates how to deploy and interact with a `HelloWorld` smart contract and a `CounterInternal` contract: +```typescript title="tests/CounterInternal.spec.ts" +import { Blockchain, SandboxContract, TreasuryContract} from '@ton/sandbox'; +import { Cell, toNano } from '@ton/core'; +import { CounterInternal } from '../wrappers/CounterInternal'; +import '@ton/test-utils'; +import { compile } from '@ton/blueprint'; -```typescript describe('CounterInternal', () => { let codeHelloWorld: Cell; let blockchain: Blockchain; - let helloWorld: SandboxContract; let counterInternal: SandboxContract; - let keyPair: KeyPair; + let deployerCounter: SandboxContract; beforeAll(async () => { codeHelloWorld = await compile('HelloWorld'); @@ -536,38 +445,13 @@ describe('CounterInternal', () => { beforeEach(async () => { blockchain = await Blockchain.create(); - // Generate a key pair for HelloWorld - const seed = await getSecureRandomBytes(32); - keyPair = keyPairFromSeed(seed); - - // Deploy HelloWorld contract - helloWorld = blockchain.openContract( - HelloWorld.createFromConfig( - { - id: 0, - seqno: 0, - publicKey: keyPair.publicKey - }, - codeHelloWorld - ) - ); - - const deployerHello = await blockchain.treasury('deployerHello'); - const deployResultHello = await helloWorld.sendDeploy(deployerHello.getSender(), toNano('1.00')); - - expect(deployResultHello.transactions).toHaveTransaction({ - from: deployerHello.address, - to: helloWorld.address, - deploy: true, - success: true - }); + deployerCounter = await blockchain.treasury('deployerCounter'); // Deploy CounterInternal with HelloWorld as the owner counterInternal = blockchain.openContract( - await CounterInternal.fromInit(0n, helloWorld.address) + await CounterInternal.fromInit(0n, deployerCounter.address) ); - const deployerCounter = await blockchain.treasury('deployerCounter'); const deployResultCounter = await counterInternal.send(deployerCounter.getSender(), { value: toNano('1.00') }, { $$type: 'Deploy', queryId: 0n @@ -584,7 +468,7 @@ describe('CounterInternal', () => { it('should fail if not owner call increment', async () => { // Verify owner is correctly set to HelloWorld const ownerAddress = await counterInternal.getOwner(); - expect(ownerAddress.equals(helloWorld.address)).toBe(true); + expect(ownerAddress.equals(deployerCounter.address)).toBe(true); // Get initial counter value const counterBefore = await counterInternal.getCounter(); @@ -613,57 +497,181 @@ describe('CounterInternal', () => { const counterAfterNonOwner = await counterInternal.getCounter(); expect(counterAfterNonOwner).toBe(counterBefore); }); +}); - it('should increment from HelloWorld contract', async () => { - const increaseBy = 5n; - const counterBefore = await counterInternal.getCounter(); +``` - // Create internal message to increase counter that will be sent from HelloWorld - const internalMessageBody = beginCell().store(storeAdd({ - $$type: 'Add', - amount: increaseBy, - queryId: 0n - })).endCell(); +--- - const messageToSend = beginCell().store(storeMessageRelaxed(internal({ - to: counterInternal.address, - value: toNano(0.01), - body: internalMessageBody, - bounce: true - }))).endCell(); - - // Send external message to HelloWorld that contains internal message to CounterInternal - const result = await helloWorld.sendExternal({ - mode: SendMode.PAY_GAS_SEPARATELY, - message: messageToSend, - secret_key: keyPair.secretKey +## External Messages + +`External messages` are your only way of toggling smart contract logic from outside the blockchain. Usually, there is no need for implementation of them in smart contracts because in most cases you don't want external entry points to be accessible to anyone except you. If this is all functionality that you want from external section - standard way is to delegate this responsibility to separate actor - [wallet](v3/documentation/smart-contracts/contracts-specs/wallet-contracts#basic-wallets) which is practically the main reason they were designed for. + +Developing external endpoint includes several standard [approuches](/v3/documentation/smart-contracts/message-management/external-messages) and [security measures](/v3/guidelines/smart-contracts/security/overview) that might be overwhelming at this point. Therefore in this guide we will realize incrementing previously added to `hello_world` contract `ctxCounterExt` number and add send message to out `Tact` contract. + +:::danger +This implementation is **unsafe** and may lead to loosing your contract funds. Don't deploy it to `mainnet`, especially with high smart-contract balance. +::: + +## Implementation + +Let's modify our smart-contract to recieve external messages and send increase counter to another contract folowing standard steps decsribed in previous [Blueprint SDK overview](/v3/guidelines/quick-start/developing-smart-contracts/blueprint-sdk-overview). + +### Step1 : Edit smart-contract code + +Add `recv_external` function to `HelloWorld` smart-contract: + +```func title="HelloWorld.fc" +() recv_external(slice in_msg) impure { + accept_message(); + + var (ctx_id, ctx_counter, ctx_counter_ext) = load_data(); + + var query_id = in_msg~load_uint(64); + var addr = in_msg~load_msg_addr(); + var coins = in_msg~load_coins(); + var increase_by = in_msg~load_uint(32); + + var msg = begin_cell() + .store_uint(0x18, 6) + .store_slice(addr) + .store_coins(coins) + .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) + .store_uint(op::increase, 32) + .store_uint(query_id, 64) + .store_uint(increase_by, 32); + send_raw_message(msg.end_cell(), 0); + + ctx_counter_ext += increase_by; + save_data(ctx_id, ctx_counter, ctx_counter_ext); + + return (); +} +``` + +Verify that smart-contract code is correct by running: + +```bash +npm blueprint build +``` + +### Step 2: Update Wrapper + +And add wrapper method to call it through our wrapper class for sending external message: + +```typescript +async sendExternalIncrease( + provider: ContractProvider, + opts: { + increaseBy: number; + value: bigint; + addr: Address; + queryID?: number; + } +) { + const message = beginCell() + .storeUint(opts.queryID ?? 0, 64) + .storeAddress(opts.addr) + .storeCoins(opts.value) + .storeUint(opts.increaseBy, 32) + .endCell() + + return await provider.external(message); +} +``` + +### Step 3: Update test + +Update test to esnure that `HelloWorld` contract recieved internal message, sended internal message to `CounterInternal` contract and both updated their counters: + +```typescript title="HelloWorld.spec.ts" +import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox'; +import { Cell, toNano} from '@ton/core'; +import { HelloWorld } from '../wrappers/HelloWorld'; +import '@ton/test-utils'; +import { compile } from '@ton/blueprint'; + +describe('HelloWorld', () => { + let code: Cell; + + beforeAll(async () => { + code = await compile('HelloWorld'); + }); + + let blockchain: Blockchain; + let deployer: SandboxContract; + let helloWorld: SandboxContract; + + beforeEach(async () => { + blockchain = await Blockchain.create(); + + helloWorld = blockchain.openContract( + HelloWorld.createFromConfig( + { + id: 0, + ctxCounter: 0, + ctxCounterExt: 0n, + }, + code + ) + ); + + deployer = await blockchain.treasury('deployer'); + + const deployResult = await helloWorld.sendDeploy(deployer.getSender(), toNano('1.00')); + + expect(deployResult.transactions).toHaveTransaction({ + from: deployer.address, + to: helloWorld.address, + deploy: true, + success: true, + }); + }); + + it('should deploy', async () => { + // the check is done inside beforeEach + // blockchain and helloWorld are ready to use + }); + + it('should send an external message and update counter', async () => { + const receiver = await blockchain.treasury('receiver'); + + const receiverBalanceBefore = await receiver.getBalance(); + const [__, counterExtBefore] = await helloWorld.getCounters() + const increase = 5; + + const result = await helloWorld.sendExternalIncrease({ + increaseBy: increase, + value: toNano(0.05), + addr: receiver.address, + queryID: 0 }); - // Verify the external message was processed successfully expect(result.transactions).toHaveTransaction({ from: undefined, // External messages have no 'from' address to: helloWorld.address, - success: true + success: true, }); - // Verify the internal message was sent from HelloWorld to CounterInternal expect(result.transactions).toHaveTransaction({ from: helloWorld.address, - to: counterInternal.address, - success: true + to: receiver.address, + success: true, }); - // Verify the counter was increased - const counterAfter = await counterInternal.getCounter(); - expect(counterAfter).toBe(counterBefore + increaseBy); + const receiverBalanceAfter = await receiver.getBalance(); + + expect(receiverBalanceAfter).toBeGreaterThan(receiverBalanceBefore); + const [_, counterExt] = await helloWorld.getCounters() + expect(counterExtBefore + BigInt(increase)).toBe(counterExt); }); }); ``` +Verify that all examples is correct by running test script: +```bash +npm blueprint test +``` -Congratulations! You have successfully created a **multi-contract** system and learned how to handle **internal messages**. This example illustrates the typical flow of any message chain: sending an `external message`, triggering the `internal messages` flow based on your system model, and so on. Now that our contracts have been fully tested, we are ready to deploy them and interact with them on-chain. - -:::danger -Before deploying your smart contracts to the **mainnet**, review the [Security Measures](/v3/guidelines/smart-contracts/security/overview) to ensure your contract is secure. -::: +Congratulations! You have successfully created a **multi-contract** system and learned how to handle **internal** and **external** messages. This example illustrates the typical flow of any message chain: sending an `external message`, triggering the `internal messages` flow based on your system model, and so on. Now that our contracts have been fully tested, we are ready to deploy them and interact with them on-chain. From 14d9158f3ec931d5cafec9883d2364ad2a441f17 Mon Sep 17 00:00:00 2001 From: Alexey Ostrovsky Date: Tue, 1 Apr 2025 21:19:39 +0300 Subject: [PATCH 5/8] Feedback fixes processing messages(draft) --- .../processing-messages.mdx | 191 ++++++++------- .../setup-environment.mdx | 4 +- .../storage-and-get-methods.mdx | 219 ++++++++++++++---- 3 files changed, 276 insertions(+), 138 deletions(-) diff --git a/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx b/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx index efd4b65267..9ec667620d 100644 --- a/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx @@ -5,9 +5,8 @@ import TabItem from '@theme/TabItem'; > **Summary:** In previous steps we modified our smart-contract interaction with `storage, `get methods` and learned basic smart-contract development flow. -Now that we have learned basic examples of modifying smart-contract code and using development tools, we are ready to move on to the main functionality of smart contracts - sending and receiving messages. +Now that we have learned basic examples of modifying smart-contract code and using development tools, we are ready to move on to the main functionality of smart contracts - sending and receiving messages. In TON messages not only used for sending currency, but also as data-exchange mechanism between smart-contracts. -First thing to mention is that in TON, messages are not just currency exchange actions carrying TON coins. They are also a **data exchange mechanism** that provides an opportunity to create your own "network" of smart contracts interacting with each other and smart contracts of other network participants that are not under your direct control. :::tip If you are stuck on some of the examples you can find original template project with all modifications performed during this guide [here](https://github.com/ton-community/ton-onboarding-sandbox/tree/main/quick-start/smart-contracts/Example). @@ -17,76 +16,11 @@ If you are stuck on some of the examples you can find original template project ## Internal messages -We are almost at the finish line! Let's cook a simple `internal message` to other contract and implement its processing on receiver side! We have already seen a counter that increases its values through `external messages`, now let's make it through internal one. And to make this task a little more interesting let's ensure that only one `actor` has access to this functionality. - -### Actors and roles - -Since TON implements actor [model](/v3/concepts/dive-into-ton/ton-blockchain/blockchain-of-blockchains/#single-actor) it's natural to think about smart-contracts relations in terms of `roles`, determining who can access smart-contract functionality or not. The most common examples of roles are: - - - `anyone`: any contract that don't have distinct role. - - `owner`: contract that has exclusive access to some crucial parts of functionality. - -If you look at `recv_internal()` function signature in your smart contract you can see `in_msg_full` and `in_msg_body` arguments, while the second one carries actual payload of sender which is free to fill it anyway they want, first one consists of several values describing transaction context. You can consider `in_msg_full` as some type of message **header**. We will not dwell in detail for each of values during this guide, what is important for us now, is that this part of message is defined by TON implementation and **always validated on sender side** and as a result cannot be fabricated. - -What we specifically are interested in is the source address of message, by obtaining that address and comparing to stored one, that we previously saved, for example, during deployment, we can open crucial part of our smart contract functionality. Common approach looks like this: - - - -```func -;; This is NOT a part of the project, just an example. -() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { - ;; Parse the sender address from in_msg_full - slice cs = in_msg_full.begin_parse(); - int flags = cs~load_uint(4); - slice sender_address = cs~load_msg_addr(); - - ;; check if message was send by owner - if (equal_slices(sender_address, owner_address)) { - ;;owner operations - return - } else if (equal_slices(sender_address, other_role_address)){ - ;;other role operations - return - } else { - ;;anyone else operations - return - } - - ;;no known operation were obtained for presented role - ;;0xffff is not standard exit code, but is standard practice among TON developers - throw(0xffff); -} -``` - - -```tolk -// This is NOT a part of the project, just an example. -fun onInternalMessage(myBalance: int, msgValue: int, msgFull: cell, msgBody: slice) { - // Parse the sender address from in_msg_full - var cs: slice = msgFull.beginParse(); - val flags = cs.loadMessageFlags(); - var sender_address = cs~load_msg_address(); - - if (isSliceBitsEqual(sender_address, owner_address)) { - // owner operations - return - } else if (isSliceBitsEqual(sender_address, other_role_address)){ - // other role operations - return - } else { - // anyone else operations - return - } - - throw 0xffff; // if the message contains an op that is not known to this contract, we throw -} -``` - - +Before we proceed to implementation let's briefly describe main ways and patterns that we can use to process internal messages. ### Operations -Another common pattern in TON contracts is to include a **32-bit operation code** in message bodies which tells your contract what action to perform: +Common pattern in TON contracts is to include a **32-bit operation code** in message bodies which tells your contract what action to perform: @@ -151,6 +85,96 @@ fun onInternalMessage(myBalance: int, msgValue: int, msgFull: cell, msgBody: sli +### Actors and roles + +Since TON implements [actor](/v3/concepts/dive-into-ton/ton-blockchain/blockchain-of-blockchains/#single-actor) model it's natural to think about smart-contracts relations in terms of `roles`, determining who can access smart-contract functionality or not. The most common examples of roles are: + + - `anyone`: any contract that don't have distinct role. + - `owner`: contract that has exclusive access to some crucial parts of functionality. + +Let's examine `recv_internal` function signature to understdand how we could use that: + + + +```func +() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure +``` + - `my_balance` - balance of smart-contract at the beggining of the transaction. + - `msg_value` - funds recieved with message. + - `in_msg_full` - `cell` containing "header" fields of message. + - `in_msg_body` - [slice](/v3/documentation/smart-contracts/func/docs/types#atomic-types) containg payload pf the message. + + +```tolk +fun onInternalMessage(myBalance: int, msgValue: int, msgFull: cell, msgBody: slice) +``` + - `myBalance` - balance of smart-contract at the beggining of the transaction. + - `msgValue` - funds recieved with message. + - `msgFull` - `cell` containing "header" fields of message. + - `msgBody` - [slice](/v3/documentation/smart-contracts/func/docs/types#atomic-types) containg payload pf the message. + + + +:::info +You can find comprehensive description of sending messages in this [section](/v3/documentation/smart-contracts/message-management/sending-messages#message-layout). +::: + +What we specifically are interested in is the source address of message that we could extract from `msg_full` cell, by obtaining that address and comparing to stored one, that we previously saved, for example, during deployment, we can open crucial part of our smart contract functionality. Common approach looks like this: + + + +```func +;; This is NOT a part of the project, just an example. +() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { + ;; Parse the sender address from in_msg_full + slice cs = in_msg_full.begin_parse(); + int flags = cs~load_uint(4); + slice sender_address = cs~load_msg_addr(); + + ;; check if message was send by owner + if (equal_slices(sender_address, owner_address)) { + ;;owner operations + return + } else if (equal_slices(sender_address, other_role_address)){ + ;;other role operations + return + } else { + ;;anyone else operations + return + } + + ;;no known operation were obtained for presented role + ;;0xffff is not standard exit code, but is standard practice among TON developers + throw(0xffff); +} +``` + + +```tolk +// This is NOT a part of the project, just an example. +fun onInternalMessage(myBalance: int, msgValue: int, msgFull: cell, msgBody: slice) { + // Parse the sender address from in_msg_full + var cs: slice = msgFull.beginParse(); + val flags = cs.loadMessageFlags(); + var sender_address = cs~load_msg_address(); + + if (isSliceBitsEqual(sender_address, owner_address)) { + // owner operations + return + } else if (isSliceBitsEqual(sender_address, other_role_address)){ + // other role operations + return + } else { + // anyone else operations + return + } + + throw 0xffff; // if the message contains an op that is not known to this contract, we throw +} +``` + + + By combining both of these patterns you can achieve a comprehensive description of your smart-contract's systems ensuring secure interaction between them and unleash full potential of TON actors model. ## Implementation in Tact @@ -218,9 +242,9 @@ Example/ -At the top of the generated contract file: `counter_internal.tolk`, you may see a [message](https://docs.tact-lang.org/book/structs-and-messages/) definition: +At the top of the generated contract file: `counter_internal.tact`, you may see a [message](https://docs.tact-lang.org/book/structs-and-messages/) definition: -```tact +```tact title="counter_internal.tact" message Add { queryId: Int as uint64; amount: Int as uint32; @@ -229,7 +253,7 @@ message Add { A message is a basic structure for communication between contracts. Tact automatically serializes and deserializes messages into cells. To ensure that opcodes will be the same during message structure changes, it may be added like below: -```tact +```tact title="counter_internal.tact" message(0x7e8764ef) Add { queryId: Int as uint64; amount: Int as uint32; @@ -269,7 +293,7 @@ counter: Int as uint32; To ensure that only the contract owner can interact with specific functions, add an `owner` field: -```tact +```tact title="counter_internal.tact" id: Int as uint32; counter: Int as uint32; owner: Address; @@ -281,7 +305,7 @@ These fields are serialized similarly to structures and stored in the contract's If you compile the contract at this stage, you will encounter the error: `Field "owner" is not set`. This is because the contract needs to initialize its fields upon deployment. Define an [`init()`](https://docs.tact-lang.org/book/contracts/#init-function) function to do this: -```tact +```tact title="counter_internal.tact" init(id: Int, owner: Address) { self.id = id; self.counter = 0; @@ -293,7 +317,7 @@ init(id: Int, owner: Address) { To accept messages from other contracts, use a [receiver](https://docs.tact-lang.org/book/functions/#receiver-functions) function. Receiver functions automatically match the message's opcode and invoke the corresponding function: -```tact +```tact title="counter_internal.tact" receive(msg: Add) { self.counter += msg.amount; self.notify("Cashback".asComment()); @@ -304,7 +328,7 @@ receive(msg: Add) { To ensure that only the contract owner can send messages, use the `require` function: -```tact +```tact title="counter_internal.tact" receive(msg: Add) { // `sender()` function is used to retrieve message sender address require(sender() == self.owner, "Not owner!"); @@ -319,7 +343,7 @@ receive(msg: Add) { Tact also provides handy ways to share same logic through [traits](https://docs.tact-lang.org/book/types/#traits). Use the `Ownable` trait, which provides built-in ownership checks: -```tact +```tact title="counter_internal.tact" // Import library to use trait import "@stdlib/ownable"; @@ -346,22 +370,17 @@ Tact supports [getter functions](https://docs.tact-lang.org/book/functions/#gett Get function cannot be called from another contract. ::: -```tact +```tact title="counter_internal.tact" get fun counter(): Int { return self.counter; } - -slice get_owner() method_id { - load_data(); - return owner_address; -} ``` Note, that the `owner` getter is automatically defined via the `Ownable` trait. #### Complete contract -```tact title="CounterInternal.tact" +```tact title="counter_internal.tact" import "@stdlib/deploy"; import "@stdlib/ownable"; @@ -413,7 +432,7 @@ Verify that smart-contract code is correct by running build script: npm blueprint build ``` -### Step 2: Using contract wrappers +### Step 2: Update wrapper [Wrappers](https://docs.tact-lang.org/book/compile/#wrap-ts) facilitate contract interaction from TypeScript. Unlike in FunC or Tolk, they are generated automatically during the build process: @@ -505,7 +524,7 @@ describe('CounterInternal', () => { ## External Messages -`External messages` are your only way of toggling smart contract logic from outside the blockchain. Usually, there is no need for implementation of them in smart contracts because in most cases you don't want external entry points to be accessible to anyone except you. If this is all functionality that you want from external section - standard way is to delegate this responsibility to separate actor - [wallet](v3/documentation/smart-contracts/contracts-specs/wallet-contracts#basic-wallets) which is practically the main reason they were designed for. +`External messages` are your only way of toggling smart contract logic from outside the blockchain. Usually, there is no need for implementation of them in smart contracts because in most cases you don't want external entry points to be accessible to anyone except you. If this is all functionality that you want from external section - standard way is to delegate this responsibility to separate actor - [wallet](v3/documentation/smart-contracts/contracts-specs/wallet-contracts#basic-wallets), which is practically the main reason they were designed for. Developing external endpoint includes several standard [approuches](/v3/documentation/smart-contracts/message-management/external-messages) and [security measures](/v3/guidelines/smart-contracts/security/overview) that might be overwhelming at this point. Therefore in this guide we will realize incrementing previously added to `hello_world` contract `ctxCounterExt` number and add send message to out `Tact` contract. diff --git a/docs/v3/guidelines/quick-start/developing-smart-contracts/setup-environment.mdx b/docs/v3/guidelines/quick-start/developing-smart-contracts/setup-environment.mdx index 1389282c51..1bee7c156d 100644 --- a/docs/v3/guidelines/quick-start/developing-smart-contracts/setup-environment.mdx +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/setup-environment.mdx @@ -23,7 +23,7 @@ Using native instruments and language-dependent SDK's for smart-contract develop ### Step 1: Install Node.js -First, visit [installation page](https://nodejs.org/en/download) and execute download commands in PowerShell or Bash corresponding to your operating system Windows/Linux. +First, visit [installation page](https://nodejs.org/en/download) and execute download commands in `PowerShell` or `Bash` corresponding to your operating system Windows/Linux. Check node version by executing following command: @@ -62,5 +62,5 @@ npm install Ton community developed plugins providing syntax support for several IDE's and code editors. You can find them here: [Plugin List](https://docs.ton.org/v3/documentation/smart-contracts/getting-started/ide-plugins). -Also consider installing plugins providing support for JavaScript/TypeScript tools for your preferred IDE or code editor and, specifically, `Jest` for debugging smart-contract tests. +Also consider installing plugins providing support for **JavaScript/TypeScript** tools for your preferred IDE or code editor and, specifically, `Jest` for debugging smart-contract tests. diff --git a/docs/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods.mdx b/docs/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods.mdx index 8092054488..df2cab7a7a 100644 --- a/docs/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods.mdx +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods.mdx @@ -9,13 +9,11 @@ import TabItem from '@theme/TabItem'; If you are stuck on some of the examples you can find original template project with all modifications performed during this guide [here](https://github.com/ton-community/ton-onboarding-sandbox/tree/main/quick-start/smart-contracts/Example). ::: -While it's technically possible to create smart contract on TON not having any persistent storage, almost all smart-contracts need to store their `data` between transactions. This guide explains standard ways of managing `storage` of smart-contract and using `get methods` to obtain it from outside the blockchain. +Almost all smart-contracts need to store their `data` between transactions. This guide explains standard ways of managing `storage` of smart-contract and using `get methods` to obtain it from outside the blockchain. ## Smart Contract Storage Operations -First thing that we should say is that there is a small, but important difference between smart-contract `persistent data` and [TVM storage](/v3/documentation/tvm/tvm-initialization#initial-state) existing only during execution of smart-contract. Remember that smart-contracts follow **transaction** concept - if any system or user exception is raised during execution, i.e. transaction fails - `TVM storage` will not be committed to smart-contract `persistent data`. From the realization point this means that smart-contract `persistent data` is copied to `TVM storage` before starting the execution and committed back, optionally modified, in case of successful **transaction**. For simplification of this guide we will not use those strict terms, instead we will describe both as `storage` and rely on context, so, keep these facts in mind. - There are two main instructions that provide access to smart-contract storage: - `get_data()` returning current storage cell. - `set_data()` setting storage cell. @@ -26,8 +24,8 @@ Lets examine `Cell` entity structure to understand how we could manage contract TON blockchain uses a data structure called **`Cell`** as the fundamental unit for storing data. Cells are the building blocks of the `TVM` (TON Virtual Machine) and have those characteristics: -- A `Cell` can store up to 1023 bits (approximately 128 bytes) of data -- A `Cell` can reference up to 4 other `Cells` (children's) +- `Cell` can store up to 1023 bits (approximately 128 bytes) of data +- `Cell` can reference up to 4 other `Cells` (children's) - `Cell` is immutable once created You can think of Cell as the following structure: @@ -104,7 +102,7 @@ fun saveData() { #### Managing storage -Let's try to modify our example a little bit. First, let's use a more common approach of passing storage members as parameters to `save_data(members...)` and retrieve them as `(members...) get_data()` moving global variables ctx_id and ctx_counter to method bodies. Also, let's add additional integer of 256-bit size into our storage: +Let's try to modify our example a little bit. First, let's use a more common approach of passing storage members as parameters to `save_data(members...)` and retrieve them as `(members...) get_data()` moving global variables `ctx_id` and `ctx_counter` to method bodies. Also, let's add additional integer of 256-bit size into our storage: Result of our modifications should look like this: @@ -190,7 +188,7 @@ save_data(ctx_id, ctxCounter, ctxCounterExt); #### Get methods -The primary use of get methods is reading our storage data from outside the blockchain using a convenient interface, primarily to extract data that is required to prepare a transaction. +The primary use of **get methods** is reading our storage data from outside the blockchain using a convenient interface, primarily to extract data that is required to prepare a **transaction**. @@ -211,19 +209,103 @@ get get_counters(): (int, int) { +And that's it! In practice all **get methods** follow this simple flow and don't require anything more. Note that you can omit values returned from functions using `_` syntax. + +Final smart-contract code should look like this: + + + + +```func title="hello_world.fc" +#include "imports/stdlib.fc"; + +const op::increase = "op::increase"c; ;; create an opcode from string using the "c" prefix, this results in 0x7e8764ef opcode in this case +const op::send_increase = "op::send_increase"c; ;; create an opcode from string using the "c" prefix, this results in 0x7e8764ef opcode in this case + +(int, int, int) load_data() { + var ds = get_data().begin_parse(); + + int ctx_id = ds~load_uint(32); + int ctx_counter = ds~load_uint(32); + int ctx_counter_ext = ds~load_uint(256); + + ds.end_parse(); + + return (ctx_id, ctx_counter, ctx_counter_ext); +} + +() save_data(int ctx_id, int ctx_counter, int ctx_counter_ext) impure { + set_data( + begin_cell() + .store_uint(ctx_id, 32) + .store_uint(ctx_counter, 32) + .store_uint(ctx_counter_ext, 256) + .end_cell() + ); +} + +() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { + if (in_msg_body.slice_empty?()) { ;; ignore all empty messages + return (); + } + + slice cs = in_msg_full.begin_parse(); + int flags = cs~load_uint(4); + if (flags & 1) { ;; ignore all bounced messages + return (); + } + + var (ctx_id, ctx_counter, ctx_counter_ext) = load_data(); ;; here we populate the storage variables + + int op = in_msg_body~load_uint(32); ;; by convention, the first 32 bits of incoming message is the op + int query_id = in_msg_body~load_uint(64); ;; also by convention, the next 64 bits contain the "query id", although this is not always the case + + if (op == op::increase) { + int increase_by = in_msg_body~load_uint(32); + ctx_counter += increase_by; + save_data(ctx_id, ctx_counter, ctx_counter_ext); + return (); + } + + throw(0xffff); ;; if the message contains an op that is not known to this contract, we throw +} + +int get_counter() method_id { + var (_, ctx_counter, _) = load_data(); + return ctx_counter; +} + +int get_id() method_id { + var (ctx_id, _, _) = load_data(); + return ctx_id; +} + +(int, int) get_counters() method_id { + var (_, ctx_counter, ctx_counter_ext) = load_data(); + return (ctx_counter, ctx_counter_ext); +} +} +``` + + +```tolk title="hello_world.tolk" +get get_counters(): (int, int) { + var (_, ctxCounter, ctxCounterExt) = load_data(); + return (ctxCounter, ctxCounterExt); +} +``` + + + Don't forget to check the correctness of your changes by compiling the smart contract: ```bash npm run build ``` -And that's it! In practice all get methods follow this simple flow and don't require anything more. Note that you can omit values returned from functions using '_' syntax. - ### Step2: Updating wrapper -Now lets update our wrapper class corresponding to new storage layout and new `get method`. - -First, let's modify `helloWorldConfigToCell` function and `HelloWorldConfig` type to properly initialize our storage during deployment: +Now lets update our wrapper class corresponding to new storage layout and new `get method`. First, let's modify `helloWorldConfigToCell` function and `HelloWorldConfig` type to properly initialize our storage during deployment: ```typescript title="HelloWorld.ts" export type HelloWorldConfig = { @@ -240,7 +322,7 @@ export function helloWorldConfigToCell(config: HelloWorldConfig): Cell { .endCell(); } ``` -Second, add a method to perform a request for the newly created get method: +Second, add a method to perform a request for the newly created **get method**: ```typescript title="HelloWorld.ts" async getSeqnoPKey(provider: ContractProvider) { @@ -254,47 +336,84 @@ async getSeqnoPKey(provider: ContractProvider) { ### Step3: Updating tests -And finally, let's write a simple test, checking that the deployment process initializes smart contract storage and correctly retrieves its data by get methods. - -First, let's update the `before each` section and particularly the `openContract` logic with the following one: +And finally, let's update our test to check that the deployment process initializes smart contract storage and correctly retrieves its data by get methods. ```typescript title="HelloWorld.spec.ts" -helloWorld = blockchain.openContract( - HelloWorld.createFromConfig( - { +import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox'; +import { Cell, toNano} from '@ton/core'; +import { HelloWorld } from '../wrappers/HelloWorld'; +import '@ton/test-utils'; +import { compile } from '@ton/blueprint'; + +describe('HelloWorld', () => { + let code: Cell; + + beforeAll(async () => { + code = await compile('HelloWorld'); + }); + + let blockchain: Blockchain; + let deployer: SandboxContract; + let helloWorld: SandboxContract; + + beforeEach(async () => { + blockchain = await Blockchain.create(); + + helloWorld = blockchain.openContract( + HelloWorld.createFromConfig( + { + id: 0, + ctxCounter: 0, + ctxCounterExt: 0n, + }, + code + ) + ); + + deployer = await blockchain.treasury('deployer'); + + const deployResult = await helloWorld.sendDeploy(deployer.getSender(), toNano('1.00')); + + expect(deployResult.transactions).toHaveTransaction({ + from: deployer.address, + to: helloWorld.address, + deploy: true, + success: true, + }); + }); + + it('should correctly initialize and return the initial data', async () => { + // Define the expected initial values (same as in beforeEach) + const expectedConfig = { id: 0, - ctxCounter: 0, - ctxCounterExt: 0n - }, - code - ) -); -``` + counter: 0, + counterExt: 0n + }; + + // Log the initial configuration values before verification + console.log('Initial configuration values (before deployment):'); + console.log('- ID:', expectedConfig.id); + console.log('- Counter:', expectedConfig.counter); + console.log('- CounterExt:', expectedConfig.counterExt); + + console.log('Retrieved values after deployment:'); + // Verify counter value + const counter = await helloWorld.getCounter(); + console.log('- Counter:', counter); + expect(counter).toBe(expectedConfig.counter); + + // Verify ID value + const id = await helloWorld.getID(); + console.log('- ID:', id); + expect(id).toBe(expectedConfig.id); + + // Verify counterExt + const [_, counterExt] = await helloWorld.getCounters(); + console.log('- CounterExt', counterExt); + expect(counterExt).toBe(expectedConfig.counterExt); + }); -And add new test case for get methods: - -```typescript title="HelloWorld.spec.ts" -it('should correctly initialize and return the initial data', async () => { - // Define the expected initial values (same as in beforeEach) - const expectedConfig = { - id: 0, - ctxCounter: 0, - ctxCounterExt: 0n - }; - - // Verify counter value - const counter = await helloWorld.getCounter(); - expect(counter).toBe(expectedConfig.ctxCounter); - - // Verify ID value - const id = await helloWorld.getID(); - expect(id).toBe(expectedConfig.id); - - // Verify both counters - const [ctx, ctxExt] = await helloWorld.getSeqnoPKey(); - expect(ctx).toBe(expectedConfig.ctxCounter); - expect(ctxExt).toBe(expectedConfig.ctxCounterExt); -}); +} ``` And now run your new test script by executing the following command: From d7150c960fd643320cb493a4b0be996b7329558e Mon Sep 17 00:00:00 2001 From: Alexey Ostrovsky Date: Tue, 1 Apr 2025 22:24:34 +0300 Subject: [PATCH 6/8] Feedback common(draft) --- .../blueprint-sdk-overview.mdx | 6 +- .../deploying-to-network.mdx | 19 +- .../processing-messages.mdx | 169 +++++++++--------- .../setup-environment.mdx | 16 +- .../storage-and-get-methods.mdx | 59 +++--- 5 files changed, 140 insertions(+), 129 deletions(-) diff --git a/docs/v3/guidelines/quick-start/developing-smart-contracts/blueprint-sdk-overview.mdx b/docs/v3/guidelines/quick-start/developing-smart-contracts/blueprint-sdk-overview.mdx index 38d29ed643..9cb9d4b4e4 100644 --- a/docs/v3/guidelines/quick-start/developing-smart-contracts/blueprint-sdk-overview.mdx +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/blueprint-sdk-overview.mdx @@ -30,7 +30,7 @@ Example/ ``` -``` +```ls title="Project structure" Example/ ├── contracts/ # Folder containing smart contracts code │ └── hello_world.tolk # Main contract file @@ -52,11 +52,11 @@ This folder contains your smart contract source code written in one of the avail ### `/wrappers` -While `@ton/ton SDK` provides us interfaces of serializing and sending messages for standard smart-contracts such as `wallets`, if we develop our own smart-contract that will deserialize received messages by its own custom protocol we need to provide some wrapper object that will serialize messages sent to smart-contract, deserialize responses from `get method`s and serialize contract itself for deployment. +To interact with your smart-contract off-chain you need to serialize and desirialize messages sended to it. `Wrapper` classes developed to mirror your smart-contract implementation making it simple to use it's functionality. ### `/tests` -This directory contains test files for your smart contracts. Testing contracts directly in TON network is not the best option because it requires some amount of time and may lead to losing funds. This testing playground tool allow you to execute multiple smart-contracts and even send messages between them in your "local network". Tests are crucial for ensuring your smart contracts behave as expected before deployment to the network. +This directory contains test files for your smart contracts. Testing contracts directly in TON network is not the best option because it requires some amount of time and may lead to losing funds. This testing playground tool allow you to execute multiple smart-contracts and even send messages between them in your **"local network"**. Tests are crucial for ensuring your smart contracts behave as expected before deployment to the network. ### `/scripts` diff --git a/docs/v3/guidelines/quick-start/developing-smart-contracts/deploying-to-network.mdx b/docs/v3/guidelines/quick-start/developing-smart-contracts/deploying-to-network.mdx index 49f58da62a..440026096d 100644 --- a/docs/v3/guidelines/quick-start/developing-smart-contracts/deploying-to-network.mdx +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/deploying-to-network.mdx @@ -6,7 +6,7 @@ In this part of the guide we will proceed to deployment of previously developed ## Address and initial state -We already know that [address](/v3/documentation/smart-contracts/addresses/) is unique identifier of `smart-contract`, i.e `actor`, `account` in network which is used to send transactions and verify their sender upon receive but we still didn't discussed how it's created. The common formula of smart-contract address looks like that: +We already know that [address](/v3/documentation/smart-contracts/addresses/) is unique identifier of `smart-contract` in network which is used to send transactions and verify their sender upon receive, but we still didn't discussed how it's created. The common formula of smart-contract address looks like that: ***address=hash(state_init(code, data))*** @@ -14,9 +14,9 @@ Address of smart-contract is a hash of aggregated initial code and data of smart ### You already know the address -In TON any address that didn't accept any transaction and, as a consequnce, dont have any data is considered in `nonexzist` state, nethertheless when we created a wallet using wallet app in [Getting started](/v3/guidelines/quick-start/getting-started) section we still was able to get address of our **future** wallet smart-contract from wallet app before it's deployment and examine it in explorer. +In TON any address that don't have any data is considered in `nonexistent` state, nevertheless when we created a wallet using wallet app in [Getting started](/v3/guidelines/quick-start/getting-started) section we still was able to get address of our **future** wallet smart-contract from wallet app before it's deployment and examine it in explorer. -The reason behind that is because creating your private and public key pair through **mnemonic phrase**, where second key is part of initial data of smart-contract makes `state_init` of our contract fully determent: +The reason behind that is because creating your **private** and **public** key pair through **mnemonic phrase**, where second key is part of initial data of smart-contract makes `state_init` of our contract fully determent: - **code** is one of the standard wallet implementation, like `v5r1`. - **data** is `public_key` with other default initialized fields. @@ -40,7 +40,7 @@ init(id: Int, owner: Address) { } ``` -If we remove `id` field from its initial storage we can ensure that **only one** `CounterInternal` smart-cotnract could exzist for a particular owner, moreover, if we consider owner as wallet smart-contract, by knowing its public_key and version we could calculate wallet address and, as a consequnce, address of its `CounterInternal` contract. +If we remove `id` field from its initial storage we can ensure that **only one** `CounterInternal` smart-cotnract could exzist for a particular owner. :::info Tokens This mechanism plays cruicial role in [Jetton Processing](v3/guidelines/dapps/asset-processing/jettons), each non-native(jetton) token requires it's own `Jetton Wallet` for a particular owner and therefore provides a calculatable address from it, creating a **star scheme** with basic wallet in center. @@ -50,23 +50,19 @@ This mechanism plays cruicial role in [Jetton Processing](v3/guidelines/dapps/as Now, when our smart-contracts is fully tested, we are ready to deploy them into the TON. In `Blueprint SDK` this process is the same for both `mainnet` and `testnet` and any of the presented languages in guide: `FunC`, `Tact`, `Tolk`. Deploy scripts relays on the same wrappers that you have used in testing scripts: -```typescript +```typescript title="/scripts/deployHelloWorld" import { toNano } from '@ton/core'; import { HelloWorld } from '../wrappers/HelloWorld'; import { CounterInternal } from '../wrappers/CounterInternal'; import { compile, NetworkProvider } from '@ton/blueprint'; -import { mnemonicToWalletKey } from '@ton/crypto'; export async function run(provider: NetworkProvider) { - const mnemonic = "".split(' '); // Insert your mnemonic - const { publicKey, secretKey } = await mnemonicToWalletKey(mnemonic); - const helloWorld = provider.open( HelloWorld.createFromConfig( { id: Math.floor(Math.random() * 10000), - seqno: 0, - publicKey: publicKey + ctxCounter: 0, + ctxCounterExt: 0n, }, await compile('HelloWorld') ) @@ -99,6 +95,7 @@ export async function run(provider: NetworkProvider) { console.log('ID', await helloWorld.getID()); console.log('ID', (await counterInternal.getId()).toString()); } + ``` You can run those scripts by entering following command: diff --git a/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx b/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx index 9ec667620d..60b04afcec 100644 --- a/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx @@ -5,7 +5,7 @@ import TabItem from '@theme/TabItem'; > **Summary:** In previous steps we modified our smart-contract interaction with `storage, `get methods` and learned basic smart-contract development flow. -Now that we have learned basic examples of modifying smart-contract code and using development tools, we are ready to move on to the main functionality of smart contracts - sending and receiving messages. In TON messages not only used for sending currency, but also as data-exchange mechanism between smart-contracts. +Now we are ready to move on to the main functionality of smart contracts - **sending and receiving messages**. In TON messages not only used for sending currency, but also as data-exchange mechanism between smart-contracts. :::tip @@ -18,73 +18,6 @@ If you are stuck on some of the examples you can find original template project Before we proceed to implementation let's briefly describe main ways and patterns that we can use to process internal messages. -### Operations - -Common pattern in TON contracts is to include a **32-bit operation code** in message bodies which tells your contract what action to perform: - - - -```func -;; This is NOT a part of the project, just an example! -const int op::increment = 1; -const int op::decrement = 2; - -() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { - ;; Step 1: Check if the message is empty - if (in_msg_body.slice_empty?()) { - return; ;; Nothing to do with empty messages - } - - ;; Step 2: Extract the operation code - int op = in_msg_body~load_uint(32); - - ;; Step 3-7: Handle the requested operation - if (op == op::increment) { - increment(); ;;call to specific operation handler - return; - } else if (op == op::decrement) { - decrement(); - ;; Just accept the money - return; - } - - ;; Unknown operation - throw(0xffff); -} -``` - - -```tolk -//This is NOT a part of the project, just an example! -const op::increment : int = 1; -const op::decrement : int = 2; - -fun onInternalMessage(myBalance: int, msgValue: int, msgFull: cell, msgBody: slice) { - // Step 1: Check if the message is empty - if (slice.isEndOfSlice()) { - return; // Nothing to do with empty messages - } - - // Step 2: Extract the operation code - var op = in_msg_body~load_uint(32); - - // Step 3-7: Handle the requested operation - if (op == op::increment) { - increment(); //call to specific operation handler - return; - } else if (op == op::decrement) { - decrement(); - // Just accept the money - return; - } - - // Unknown operation - throw(0xffff); -} -``` - - - ### Actors and roles Since TON implements [actor](/v3/concepts/dive-into-ton/ton-blockchain/blockchain-of-blockchains/#single-actor) model it's natural to think about smart-contracts relations in terms of `roles`, determining who can access smart-contract functionality or not. The most common examples of roles are: @@ -92,7 +25,7 @@ Since TON implements [actor](/v3/concepts/dive-into-ton/ton-blockchain/blockchai - `anyone`: any contract that don't have distinct role. - `owner`: contract that has exclusive access to some crucial parts of functionality. -Let's examine `recv_internal` function signature to understdand how we could use that: +Let's examine `recv_internal` function signature to understand how we could use that: @@ -175,6 +108,75 @@ fun onInternalMessage(myBalance: int, msgValue: int, msgFull: cell, msgBody: sli + +### Operations + +Common pattern in TON contracts is to include a **32-bit operation code** in message bodies which tells your contract what action to perform: + + + +```func +;; This is NOT a part of the project, just an example! +const int op::increment = 1; +const int op::decrement = 2; + +() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { + ;; Step 1: Check if the message is empty + if (in_msg_body.slice_empty?()) { + return; ;; Nothing to do with empty messages + } + + ;; Step 2: Extract the operation code + int op = in_msg_body~load_uint(32); + + ;; Step 3-7: Handle the requested operation + if (op == op::increment) { + increment(); ;;call to specific operation handler + return; + } else if (op == op::decrement) { + decrement(); + ;; Just accept the money + return; + } + + ;; Unknown operation + throw(0xffff); +} +``` + + +```tolk +//This is NOT a part of the project, just an example! +const op::increment : int = 1; +const op::decrement : int = 2; + +fun onInternalMessage(myBalance: int, msgValue: int, msgFull: cell, msgBody: slice) { + // Step 1: Check if the message is empty + if (slice.isEndOfSlice()) { + return; // Nothing to do with empty messages + } + + // Step 2: Extract the operation code + var op = in_msg_body~load_uint(32); + + // Step 3-7: Handle the requested operation + if (op == op::increment) { + increment(); //call to specific operation handler + return; + } else if (op == op::decrement) { + decrement(); + // Just accept the money + return; + } + + // Unknown operation + throw(0xffff); +} +``` + + + + By combining both of these patterns you can achieve a comprehensive description of your smart-contract's systems ensuring secure interaction between them and unleash full potential of TON actors model. ## Implementation in Tact @@ -244,7 +246,7 @@ Example/ At the top of the generated contract file: `counter_internal.tact`, you may see a [message](https://docs.tact-lang.org/book/structs-and-messages/) definition: -```tact title="counter_internal.tact" +```tact title="/contracts/counter_internal.tact" message Add { queryId: Int as uint64; amount: Int as uint32; @@ -253,7 +255,7 @@ message Add { A message is a basic structure for communication between contracts. Tact automatically serializes and deserializes messages into cells. To ensure that opcodes will be the same during message structure changes, it may be added like below: -```tact title="counter_internal.tact" +```tact title="/contracts/counter_internal.tact" message(0x7e8764ef) Add { queryId: Int as uint64; amount: Int as uint32; @@ -293,7 +295,7 @@ counter: Int as uint32; To ensure that only the contract owner can interact with specific functions, add an `owner` field: -```tact title="counter_internal.tact" +```tact title="/contracts/counter_internal.tact" id: Int as uint32; counter: Int as uint32; owner: Address; @@ -305,7 +307,7 @@ These fields are serialized similarly to structures and stored in the contract's If you compile the contract at this stage, you will encounter the error: `Field "owner" is not set`. This is because the contract needs to initialize its fields upon deployment. Define an [`init()`](https://docs.tact-lang.org/book/contracts/#init-function) function to do this: -```tact title="counter_internal.tact" +```tact title="/contracts/counter_internal.tact" init(id: Int, owner: Address) { self.id = id; self.counter = 0; @@ -317,7 +319,7 @@ init(id: Int, owner: Address) { To accept messages from other contracts, use a [receiver](https://docs.tact-lang.org/book/functions/#receiver-functions) function. Receiver functions automatically match the message's opcode and invoke the corresponding function: -```tact title="counter_internal.tact" +```tact title="/contracts/counter_internal.tact" receive(msg: Add) { self.counter += msg.amount; self.notify("Cashback".asComment()); @@ -328,7 +330,7 @@ receive(msg: Add) { To ensure that only the contract owner can send messages, use the `require` function: -```tact title="counter_internal.tact" +```tact title="/contracts/counter_internal.tact" receive(msg: Add) { // `sender()` function is used to retrieve message sender address require(sender() == self.owner, "Not owner!"); @@ -343,7 +345,7 @@ receive(msg: Add) { Tact also provides handy ways to share same logic through [traits](https://docs.tact-lang.org/book/types/#traits). Use the `Ownable` trait, which provides built-in ownership checks: -```tact title="counter_internal.tact" +```tact title="/contracts/counter_internal.tact" // Import library to use trait import "@stdlib/ownable"; @@ -370,7 +372,7 @@ Tact supports [getter functions](https://docs.tact-lang.org/book/functions/#gett Get function cannot be called from another contract. ::: -```tact title="counter_internal.tact" +```tact title="/contracts/counter_internal.tact" get fun counter(): Int { return self.counter; } @@ -380,7 +382,7 @@ Note, that the `owner` getter is automatically defined via the `Ownable` trait. #### Complete contract -```tact title="counter_internal.tact" +```tact title="/contracts/counter_internal.tact" import "@stdlib/deploy"; import "@stdlib/ownable"; @@ -436,7 +438,7 @@ npm blueprint build [Wrappers](https://docs.tact-lang.org/book/compile/#wrap-ts) facilitate contract interaction from TypeScript. Unlike in FunC or Tolk, they are generated automatically during the build process: -```typescript ./wrappers/CounterInternal.ts +```typescript title="/wrappers/CounterInternal.ts" export * from '../build/CounterInternal/tact_CounterInternal'; ``` @@ -444,7 +446,7 @@ export * from '../build/CounterInternal/tact_CounterInternal'; Now let's ensure that our smart-contract failes if non-owner tries to increment smart-contract by adding this test: -```typescript title="tests/CounterInternal.spec.ts" +```typescript title="/tests/CounterInternal.spec.ts" import { Blockchain, SandboxContract, TreasuryContract} from '@ton/sandbox'; import { Cell, toNano } from '@ton/core'; import { CounterInternal } from '../wrappers/CounterInternal'; @@ -540,7 +542,7 @@ Let's modify our smart-contract to recieve external messages and send increase c Add `recv_external` function to `HelloWorld` smart-contract: -```func title="HelloWorld.fc" +```func title="/contracts/HelloWorld.fc" () recv_external(slice in_msg) impure { accept_message(); @@ -578,7 +580,7 @@ npm blueprint build And add wrapper method to call it through our wrapper class for sending external message: -```typescript +```typescript title="/wrappers/HelloWorld.ts" async sendExternalIncrease( provider: ContractProvider, opts: { @@ -603,7 +605,7 @@ async sendExternalIncrease( Update test to esnure that `HelloWorld` contract recieved internal message, sended internal message to `CounterInternal` contract and both updated their counters: -```typescript title="HelloWorld.spec.ts" +```typescript title="/tests/HelloWorld.spec.ts" import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox'; import { Cell, toNano} from '@ton/core'; import { HelloWorld } from '../wrappers/HelloWorld'; @@ -693,4 +695,3 @@ Verify that all examples is correct by running test script: npm blueprint test ``` -Congratulations! You have successfully created a **multi-contract** system and learned how to handle **internal** and **external** messages. This example illustrates the typical flow of any message chain: sending an `external message`, triggering the `internal messages` flow based on your system model, and so on. Now that our contracts have been fully tested, we are ready to deploy them and interact with them on-chain. diff --git a/docs/v3/guidelines/quick-start/developing-smart-contracts/setup-environment.mdx b/docs/v3/guidelines/quick-start/developing-smart-contracts/setup-environment.mdx index 1bee7c156d..00e9ba686e 100644 --- a/docs/v3/guidelines/quick-start/developing-smart-contracts/setup-environment.mdx +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/setup-environment.mdx @@ -8,8 +8,6 @@ This guide covers basic steps of setting up your smart-contract development envi - Basic programming skills. - Familiarity with Command-line interface. - - Your preferred code-editor/IDE. - - Around __15 minutes__ of your time. ## Setup development environment @@ -25,18 +23,20 @@ Using native instruments and language-dependent SDK's for smart-contract develop First, visit [installation page](https://nodejs.org/en/download) and execute download commands in `PowerShell` or `Bash` corresponding to your operating system Windows/Linux. -Check node version by executing following command: +Check that `npm` and `node` installed by executing following command: ```bash -node -v npm -v +node -v ``` -Node version should be at least `v18`. - ### Step 2: Choose smart-contract development language -During guide we provide example on 3 languages: `Func`, `Tact` and `Tolk`. You can choose from any of them and even combine smart-contracts on different languages on latest sections. To proceed through guide there is now need of deep understanding of choosed one, basic programming skills will be enought. You can find their breaf overview here: [Programming languages](/v3/documentation/smart-contracts/overview#programming-languages) +During guide we provide example on: `Func`, `Tolk` and `Tact`. You can choose from any of them and even combine smart-contracts on different languages. To proceed through guide there is now need of deep understanding of choosed one, basic programming skills will be enought. + +:::info +You can find breaf overview of languages here: [Programming languages](/v3/documentation/smart-contracts/overview#programming-languages) +::: ### Step 3: Setup Blueprint SDK @@ -49,7 +49,7 @@ npm create ton@latest This will run interactive script for creating project template, you can enter anything you want, but if you want to have same paths as this guide choose following: 1. Project name: `Example`. 2. First created contract name: `HelloWorld`. -3. Choose the project template: A simple counter contract corresponding to your choosen language. +3. Choose the project template: A simple counter contract on `FunC` or `Tolk`. And finally, change your current directory to generated project template folder, and install all required dependencies: diff --git a/docs/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods.mdx b/docs/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods.mdx index df2cab7a7a..7cbfc9001b 100644 --- a/docs/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods.mdx +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods.mdx @@ -12,23 +12,36 @@ If you are stuck on some of the examples you can find original template project Almost all smart-contracts need to store their `data` between transactions. This guide explains standard ways of managing `storage` of smart-contract and using `get methods` to obtain it from outside the blockchain. -## Smart Contract Storage Operations +## Smart contract storage operations + + There are two main instructions that provide access to smart-contract storage: + - `get_data()` returning current storage cell. - `set_data()` setting storage cell. -Lets examine `Cell` entity structure to understand how we could manage contract storage. + + +There are two main instructions that provide access to smart-contract storage: + + - `getContractData()` returning current storage `Cell`. + - `setContractData()` setting storage `Cell`. + + + + +Lets examine [Cell](/v3/concepts/dive-into-ton/ton-blockchain/cells-as-data-storage) entity structure to understand how we could manage contract storage: ## Cell structure -TON blockchain uses a data structure called **`Cell`** as the fundamental unit for storing data. Cells are the building blocks of the `TVM` (TON Virtual Machine) and have those characteristics: +TON blockchain uses a data structure called **`Cell`** as the fundamental unit for storing data. `Cells` are the building blocks of the smart-contract data and have those characteristics: - `Cell` can store up to 1023 bits (approximately 128 bytes) of data - `Cell` can reference up to 4 other `Cells` (children's) - `Cell` is immutable once created -You can think of Cell as the following structure: +You can think of `Cell` as the following structure: ```typescript // Conceptual representation of a Cell @@ -42,13 +55,13 @@ interface Cell { Let's try to modify our smart-contract folowing standard steps decsribed in previous [Blueprint SDK overview](/v3/guidelines/quick-start/developing-smart-contracts/blueprint-sdk-overview) section. -### Step1: Edit smart-contract code +### Step 1: Edit smart-contract code In case it's inconvenient to always serialize and deserialize storage cell, there is a pretty standard practice to define two wrapper methods that provide corresponding logic. If you didn't change smart-contract code it should contain following lines: -```func title="hello_world.fc" +```func title="/contracts/hello_world.fc" global int ctx_id; global int ctx_counter; @@ -74,7 +87,7 @@ global int ctx_counter; ``` -```tolk title="hello_world.tolk" +```tolk title="/contracts/hello_world.tolk" global ctxID: int; global ctxCounter: int; @@ -108,7 +121,7 @@ Result of our modifications should look like this: -```func title="hello_world.fc" +```func title="/contracts/hello_world.fc" ;; load_data retrieves variables from TVM storage cell (int, int, int) load_data() { var ds = get_data().begin_parse(); @@ -138,7 +151,7 @@ Result of our modifications should look like this: ``` -```tolk title="hello_world.tolk" +```tolk title="/contracts/hello_world.tolk" // load_data retrieves variables from TVM storage cell // impure because of writting into global variables fun loadData(): (int, int, int) { @@ -173,13 +186,13 @@ Don't forget to delete global variables `ctx_id`, `ctx_counter` and modify usage -```func title="hello_world.fc" +```func title="/contracts/hello_world.fc" var (ctx_id, ctxCounter, ctxCounterExt) = load_data(); save_data(ctx_id, ctxCounter, ctxCounterExt); ``` -```tolk title="hello_world.tolk" +```tolk title="/contracts/hello_world.tolk" var (ctx_id, ctxCounter, ctxCounterExt) = load_data(); save_data(ctx_id, ctxCounter, ctxCounterExt); ``` @@ -188,11 +201,11 @@ save_data(ctx_id, ctxCounter, ctxCounterExt); #### Get methods -The primary use of **get methods** is reading our storage data from outside the blockchain using a convenient interface, primarily to extract data that is required to prepare a **transaction**. +The primary use of `get methods` is reading our storage data from outside the blockchain using a convenient interface, primarily to extract data that is required to prepare a **transaction**. -```func title="hello_world.fc" +```func title="/contracts/hello_world.fc" (int, int) get_counters() method_id { var (_, ctxCounter, ctxCounterExt) = load_data(); return (ctxCounter, ctxCounterExt); @@ -200,7 +213,7 @@ The primary use of **get methods** is reading our storage data from outside the ``` -```tolk title="hello_world.tolk" +```tolk title="/contracts/hello_world.tolk" get get_counters(): (int, int) { var (_, ctxCounter, ctxCounterExt) = load_data(); return (ctxCounter, ctxCounterExt); @@ -216,7 +229,7 @@ Final smart-contract code should look like this: -```func title="hello_world.fc" +```func title="/contracts/hello_world.fc" #include "imports/stdlib.fc"; const op::increase = "op::increase"c; ;; create an opcode from string using the "c" prefix, this results in 0x7e8764ef opcode in this case @@ -288,7 +301,7 @@ int get_id() method_id { ``` -```tolk title="hello_world.tolk" +```tolk title="/contracts/hello_world.tolk" get get_counters(): (int, int) { var (_, ctxCounter, ctxCounterExt) = load_data(); return (ctxCounter, ctxCounterExt); @@ -303,11 +316,11 @@ Don't forget to check the correctness of your changes by compiling the smart con npm run build ``` -### Step2: Updating wrapper +### Step 2: Update wrapper Now lets update our wrapper class corresponding to new storage layout and new `get method`. First, let's modify `helloWorldConfigToCell` function and `HelloWorldConfig` type to properly initialize our storage during deployment: -```typescript title="HelloWorld.ts" +```typescript title="/wrappers/HelloWorld.ts" export type HelloWorldConfig = { id: number; ctxCounter: number; @@ -322,10 +335,10 @@ export function helloWorldConfigToCell(config: HelloWorldConfig): Cell { .endCell(); } ``` -Second, add a method to perform a request for the newly created **get method**: +Second, add a method to perform a request for the newly created get method: -```typescript title="HelloWorld.ts" -async getSeqnoPKey(provider: ContractProvider) { +```typescript title="/HelloWorld.ts" +async getCounters(provider: ContractProvider) { const result = await provider.get('get_counters', []); const counter = result.stack.readNumber(); const counterExt = result.stack.readBigNumber(); @@ -334,11 +347,11 @@ async getSeqnoPKey(provider: ContractProvider) { } ``` -### Step3: Updating tests +### Step 3: Update tests And finally, let's update our test to check that the deployment process initializes smart contract storage and correctly retrieves its data by get methods. -```typescript title="HelloWorld.spec.ts" +```typescript title="/tests/HelloWorld.spec.ts" import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox'; import { Cell, toNano} from '@ton/core'; import { HelloWorld } from '../wrappers/HelloWorld'; From ced00205a56e4694b39ed89985853ec7e1275195 Mon Sep 17 00:00:00 2001 From: Alexey Ostrovsky Date: Tue, 1 Apr 2025 22:33:31 +0300 Subject: [PATCH 7/8] Finalize smart-contract feedback --- .../developing-smart-contracts/blueprint-sdk-overview.mdx | 2 +- .../developing-smart-contracts/processing-messages.mdx | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/v3/guidelines/quick-start/developing-smart-contracts/blueprint-sdk-overview.mdx b/docs/v3/guidelines/quick-start/developing-smart-contracts/blueprint-sdk-overview.mdx index 9cb9d4b4e4..d7258538e0 100644 --- a/docs/v3/guidelines/quick-start/developing-smart-contracts/blueprint-sdk-overview.mdx +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/blueprint-sdk-overview.mdx @@ -56,7 +56,7 @@ To interact with your smart-contract off-chain you need to serialize and desiria ### `/tests` -This directory contains test files for your smart contracts. Testing contracts directly in TON network is not the best option because it requires some amount of time and may lead to losing funds. This testing playground tool allow you to execute multiple smart-contracts and even send messages between them in your **"local network"**. Tests are crucial for ensuring your smart contracts behave as expected before deployment to the network. +This directory contains test files for your smart contracts. Testing contracts directly in TON network is not the best option because deployment requires some amount of time and may lead to losing funds. This testing playground allow you to execute multiple smart-contracts and even send messages between them in your **"local network"**. Tests are crucial for ensuring your smart contracts behave as expected before deployment to the network. ### `/scripts` diff --git a/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx b/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx index 60b04afcec..24193f27bb 100644 --- a/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx @@ -522,6 +522,12 @@ describe('CounterInternal', () => { ``` +Don't forget to verify that all examples is correct by running test script: + +```bash +npm blueprint test +``` + --- ## External Messages From f57677dad17558b67354762a5b2a4e71523cbe7f Mon Sep 17 00:00:00 2001 From: Alexey Ostrovsky Date: Tue, 1 Apr 2025 22:42:09 +0300 Subject: [PATCH 8/8] Formating fix --- .../developing-smart-contracts/processing-messages.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx b/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx index 24193f27bb..d7b39e0ddf 100644 --- a/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx @@ -3,9 +3,9 @@ import TabItem from '@theme/TabItem'; # Processing Messages -> **Summary:** In previous steps we modified our smart-contract interaction with `storage, `get methods` and learned basic smart-contract development flow. +> **Summary:** In previous steps we modified our smart-contract interaction with `storage`, `get methods` and learned basic smart-contract development flow. -Now we are ready to move on to the main functionality of smart contracts - **sending and receiving messages**. In TON messages not only used for sending currency, but also as data-exchange mechanism between smart-contracts. +Now we are ready to move on to the main functionality of smart contracts - **sending and receiving messages**. In TON messages not only used for sending currency, but also as data-exchange mechanism between smart-contracts making them crucial for smart-contract development. :::tip