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 new file mode 100644 index 0000000000..7c23cbee02 --- /dev/null +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/processing-messages.mdx @@ -0,0 +1,504 @@ +# Processing Messages + +> **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. + +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). +::: + +--- + +## 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); +}``` + +And add wrapper method to call it through our wrapper class: + +```typescript +async 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. + +### 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 +() 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 sent by the 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); +} +``` + +### 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: + +```func +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); +} +``` + +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 + +First, let's create a second counter that we will be communicating with by running following command: + +```bash +npx blueprint create +``` + +Choose counter pattern and appreciated name in interactive menu, we will use `CounterInternal`. + +:::tip +By the way, this is a good opportunity to try out one of the smart-contract development languages that you didn't use previously during this guide. +::: + +Now let's update our smart-contract to contain owner address: + +```func +global int ctx_id; +global int ctx_counter; +global slice owner_address; + +;; load_data populates storage variables using stored data +() load_data() impure { + var ds = get_data().begin_parse(); + + ctx_id = ds~load_uint(32); + ctx_counter = ds~load_uint(32); + owner_address = ds~load_msg_addr(); + + ds.end_parse(); +} + +;; save_data stores storage variables as a cell into persistent storage +() save_data() impure { + set_data( + begin_cell() + .store_uint(ctx_id, 32) + .store_uint(ctx_counter, 32) + .store_slice(owner_address) + .end_cell() + ); +} +``` + +Update `recv_internal` to ignore any non-empty messages not sent by owner, and corresponding get method: + +```func + () 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 (); + } + + load_data(); ;; here we populate the storage variables + slice sender_addr = cs~load_msg_addr(); + if (equal_slices_bits(sender_addr, owner_address) != -1) { + throw(42); + } + + + 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 + + if (op == op::increase) { + int increase_by = in_msg_body~load_uint(32); + ctx_counter += increase_by; + save_data(); + return (); + } + + throw(0xffff); ;; if the message contains an op that is not known to this contract, we throw +} + +slice get_owner() method_id { + load_data(); + return owner_address; +} +``` + +Don't forget to update wrapper class, add new get method and update deployment part to initialize storage with owner address: + +```func +export type CounterInternalConfig = { + id: number; + counter: number; + owner: Address; +}; + +export function counterInternalConfigToCell(config: CounterInternalConfig): Cell { + return beginCell() + .storeUint(config.id, 32) + .storeUint(config.counter, 32) + .storeAddress(config.owner) + .endCell(); +} + +//inside CounterInternal class +async getOwnerAddress(provider: ContractProvider) { + const result = await provider.get('get_owner', []); + return result.stack.readAddress(); +} +``` + +Finally we are ready to create our **multi-contract system**: let's add a test that deploys `HelloWorld` smart contract, then deploys `CounterInternal` contract initialized with `HelloWorld` contract address, then sends *external message* containing internal one to our counter with increment operation. To see that only `HelloWorld` is able to modify counter, let's also try to send a message from another contract and ensure that it is not able to perform it: + +```typescript +describe('Integration with HelloWorld', () => { + let codeHelloWorld: Cell; + let codeCounterInternal: Cell; + let blockchain: Blockchain; + let helloWorld: SandboxContract; + let counterInternal: SandboxContract; + let keyPair: KeyPair; + + beforeAll(async () => { + codeHelloWorld = await compile('HelloWorld'); + codeCounterInternal = await compile('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, + public_key: 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, + }); + + // Deploy CounterInternal with HelloWorld as the owner + counterInternal = blockchain.openContract( + CounterInternal.createFromConfig( + { + id: 0, + counter: 0, + owner: helloWorld.address, // Set HelloWorld as the owner + }, + codeCounterInternal + ) + ); + + const deployerCounter = await blockchain.treasury('deployerCounter'); + const deployResultCounter = await counterInternal.sendDeploy(deployerCounter.getSender(), toNano('1.00')); + + expect(deployResultCounter.transactions).toHaveTransaction({ + from: deployerCounter.address, + to: counterInternal.address, + deploy: true, + success: true, + }); + }); + + it('should only allow owner to increment counter', async () => { + // Verify owner is correctly set to HelloWorld + const ownerAddress = await counterInternal.getOwnerAddress(); + expect(ownerAddress.equals(helloWorld.address)).toBe(true); + + // Get initial counter value + const counterBefore = await counterInternal.getCounter(); + + // Try to increase counter from a non-owner account (should fail) + const nonOwner = await blockchain.treasury('nonOwner'); + const increaseBy = 5; + + const nonOwnerResult = await counterInternal.sendIncrease(nonOwner.getSender(), { + increaseBy, + value: toNano('0.05'), + }); + + // This should fail since only the owner should be able to increment + expect(nonOwnerResult.transactions).toHaveTransaction({ + from: nonOwner.address, + to: counterInternal.address, + success: false, + exitCode: 42, // The error code thrown in the contract + }); + + // Counter should remain unchanged + const counterAfterNonOwner = await counterInternal.getCounter(); + expect(counterAfterNonOwner).toBe(counterBefore); + + // Create internal message to increase counter that will be sent from HelloWorld + const internalMessageBody = beginCell() + .storeUint(0x7e8764ef, 32) // op::increase opcode + .storeUint(0, 64) // queryID = 0 + .storeUint(increaseBy, 32) // increaseBy + .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 + }); + + // Verify the external message was processed successfully + expect(result.transactions).toHaveTransaction({ + from: undefined, // External messages have no 'from' address + to: helloWorld.address, + success: true, + }); + + // Verify the internal message was sent from HelloWorld to CounterInternal + expect(result.transactions).toHaveTransaction({ + from: helloWorld.address, + to: counterInternal.address, + success: true, + }); + + // Verify the counter was increased + const counterAfter = await counterInternal.getCounter(); + expect(counterAfter).toBe(counterBefore + increaseBy); + }); +}); +``` + +Congratulations! We created our first **multi-contract** system and learned how to deal with basic `internal messages`! This example practically describes the general flow of any message chain: send `external message` -> toggle `internal messages` flow according to your system model and so on. Now, when our contracts are fully tested, we are ready to deploy our contracts and interact with them on-chain. + +:::danger +Before considering your smart-contracts production ready and deploying them to `mainnet` take a look at [Security Measures](/v3/guidelines/smart-contracts/security/overview) describing best practice of securing your smart-contract logic. +::: + diff --git a/docs/v3/guidelines/quick-start/developing-smart-contracts/program-structure.mdx b/docs/v3/guidelines/quick-start/developing-smart-contracts/program-structure.mdx new file mode 100644 index 0000000000..1bbbf65bfb --- /dev/null +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/program-structure.mdx @@ -0,0 +1,85 @@ +# Project structure + +> **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. + +## Overview + +If you chose proposed names in previous steps your project structure should look like this: + +``` +Example/ +├── contracts/ # Folder containing smart contracts code +│ ├── imports/ # Library imports for contracts +│ │ └── stdlib.fc # Standard library for FunC +│ └── hello_world.fc # Main contract file +├── scripts/ # Deployment and on-chain interaction scripts +│ ├── deployHelloWorld.ts # Script to deploy the contract +│ └── incrementHelloWorld.ts # Script to interact with the contract +├── tests/ # Test folder for local contract testing +│ └── HelloWorld.spec.ts # Test specifications for the contract +└── wrappers/ # TypeScript wrappers for contract interaction + ├── HelloWorld.ts # Wrapper class for smart contract + └── HelloWorld.compile.ts # Script for contract compilation +``` + +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 contains imports folder which is used for libraries usually containing `stdlib.fc` - standard library of `FunC` language. + +If you open this file you can see that it has quite humble for a standard library, a size of 884 lines, primarily consisting of functions representing assembler insertions processing previously added to stack function parameters. + +:::info Advanced, Internals +When we discussed available for smart-contract development programming languages we allowed ourselves a little lie. There are actually two more languages - `Fift` and `TVM-assembly`(for jedi smart-contract programmers). First one is general-purpose language like the ones that we discussed before, that is just not widely used right now. Second one - is classical assembler representing TVM instructions that you can directly access through assembler insertions in high-level languages. + +Common concept of TON smart-contract ecosystem is somewhat similar to **Java**. Smart-contracts written in one of the general-purpose languages are compiled in [TVM](/v3/documentation/tvm/tvm-overview)(TON virtual machine) `byte-code`, the one that we have seen in explorer section of getting started article and then are executed on virtual machine during transaction. +::: + +### `/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. + +To run compile script excute this command in your CLI: + +```bash +npx blueprint build +``` + +It's preferred development flow to edit smart contract code and then edit its wrapper correspondingly to updated protocol. + +:::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. +::: + +### `/tests` + +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`. + +To run your test execute following command: + +```bash +npx blueprint build +``` + +Or use interface provided by `Jest` plugins in your **IDE** or **code-editor**. + +### `/scripts` + +The scripts directory contains `TypeScript` files that help you deploy and interact with your smart contracts on-chain using previously implemented wrappers. + +You can execute those scripts using following command, but we recommend to read corresponding [deployment section](/ref/to/deployment/section) first. + +```bash +npx blueprint run +``` + +Also, you can always generate same structure for another smart-contract if you need so, 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/setup-environment.mdx b/docs/v3/guidelines/quick-start/developing-smart-contracts/setup-environment.mdx new file mode 100644 index 0000000000..72a4af994f --- /dev/null +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/setup-environment.mdx @@ -0,0 +1,70 @@ +# Setup development environment + +> **Summary:** In previous steps we learned concept of smart-contract and basic ways of interacting with TON blockchain through **wallet apps** and **explorers**. + +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. + - Familiarity with Command-line interface. + - Your preferred code-editor/IDE. + - Around __15 minutes__ of your time. + +## Setup development environment + +For this guide we will rely on [Blueprint SDK](https://github.com/ton-org/blueprint) and [Node.js](https://nodejs.org/en)/Typescript stack for writing wrappers, tests and deployments scripts for your smart-contract, because its provides easiest, ready to use environment for smart-contracts developing. + +:::info +Using native instruments and language-dependent SDK's for smart-contract development covered in more advanced sections here: + - [Compile and build smart-contracts on TON](/v3/documentation/archive/compile#ton-compiler). + - [Creating State Init for Deployment](/v3/guidelines/smart-contracts/howto/wallet#creating-the-state-init-for-deployment). +::: + +### 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. + +Check node version by executing following command: + +```bash +node -v +npm -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) + +### Step 3: Setup Blueprint SDK + +Change directory to parent folder of your future project and run following command: + +```bash +npm create ton@latest +``` + +TODO: add short language description for choosing prefered one. + +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. + +And finally, change your current directory to generated project template folder, and install all required dependencies: + +```bash +cd ./Example +npm install +``` + +### Step 4(optional): IDE and editors support + +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. + 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 new file mode 100644 index 0000000000..2997eed0df --- /dev/null +++ b/docs/v3/guidelines/quick-start/developing-smart-contracts/storage-and-get-methods.mdx @@ -0,0 +1,215 @@ +# Storage and Get Methods + +> **Summary:** In previous steps we learned how to use `Blueprint SDK` and it's project structure. + +:::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). +::: + +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. + +## Cell Structure: The Backbone of TON Storage + +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 + +You can think of Cell as the following structure: + +```typescript +// Conceptual representation of a Cell +interface Cell { + bits: BitString; // Up to 1023 bits + refs: Cell[]; // Up to 4 child cells +} +``` + +## 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 current storage cell. + +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 +global int ctx_id; +global int ctx_counter; + +;; load_data populates storage variables using stored data +() load_data() impure { + var ds = get_data().begin_parse(); + + ctx_id = ds~load_uint(32); + ctx_counter = ds~load_uint(32); + + ds.end_parse(); +} + +;; save_data stores storage variables as a cell into persistent storage +() save_data() impure { + set_data( + begin_cell() + .store_uint(ctx_id, 32) + .store_uint(ctx_counter, 32) + .end_cell() + ); +} +``` + +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: + +Result of our modifications should look like this: + +```func +;; load_data retrieves variables from TVM storage cell +(int, int, int) load_data() { + var ds = get_data().begin_parse(); + + ;; 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); + + ds.end_parse(); + + return (ctx_id, seqno, public_key); +} + +;; 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 { + set_data( + begin_cell() + .store_uint(ctx_id, 32) + .store_uint(seqno, 32) + .store_uint(public_key, 256) + .end_cell() + ); +}``` + +Don't forget to delete global variables `ctx_id`, `ctx_counter` and modify usage of the function like this, copying storage members locally: + +```func +var (ctx_id, seqno, public_key) = load_data(); +save_data(ctx_id, seqno, public_key); +``` + +## 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); +} +``` + +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. + +## 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 +export type HelloWorldConfig = { + id: number; + counter: number; + seqno: number; + public_key: bigint; +}; + +export function helloWorldConfigToCell(config: HelloWorldConfig): Cell { + return beginCell() + .storeUint(config.id, 32) + .storeUint(config.counter, 32) + .storeUint(config.seqno, 32) + .storeUint(config.public_key, 256) + .endCell(); +} +``` +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', []); + return [result.stack.readNumber(), result.stack.readBigNumber()]; + } +``` + +## 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 +helloWorld = blockchain.openContract( + HelloWorld.createFromConfig( + { + id: 0, + counter: 0, + seqno: 0, + //it will be changed later, just initialization check + public_key: 0n + }, + code + ) +); +```typescript + +And add new test case for get methods: + +```typescript +it('should correctly initialize and return the initial data', async () => { + // Define the expected initial values (same as in beforeEach) + const expectedConfig = { + id: 0, + counter: 0, + seqno: 0, + public_key: 0n + }; + + // Verify counter value + const counter = await helloWorld.getCounter(); + expect(counter).toBe(expectedConfig.counter); + + // 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); +}); +``` + +And now run your new test script by executing the following command: + +```bash +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 new file mode 100644 index 0000000000..ba0bda94b0 --- /dev/null +++ b/docs/v3/guidelines/quick-start/getting-started.mdx @@ -0,0 +1,117 @@ +import ThemedImage from '@theme/ThemedImage'; + +# Getting started + +Welcome to TON quick start guide! This guide will give you a starting point for further research of TON concepts and basic practical experience of developing applications with the TON ecosystem. + +## Prerequisites + +- Basic programming knowledge. +- Around __30 minutes__ of your time. + +> **Note**: We will provide a short explanation of core concepts during the guide, but if you prefer a more theoretical approach, you can check out core concepts of [TON blockchain](/v3/concepts/dive-into-ton/ton-blockchain/blockchain-of-blockchains) first. + +## What You'll Learn + +- Interact with TON ecosystem: Wallets and Explorers. +- Setup development environment: use Blueprint SDK for developing `smart-contracts` using `FunC`, `Tact`, and `Tolk` programming languages. +- Send transactions and read from the **blockchain** using your preferred programming language and available **SDKs**. +- Core concepts of **TON blockchain** and further learning curve. +- Basic templates ready for implementation of your project logic. + +## Concept of smart-contract + +You can think of smart-contracts in TON as a program running on blockchain following well known behavior concept of [actor](/v3/concepts/dive-into-ton/ton-blockchain/blockchain-of-blockchains#single-actor). In contrast to some other blockchains where you can call in synchronous way other contracts code, smart-contract in TON is a thing in itself, and communicates with other smart-contracts on equal basis sending asynchronous messages between each other. Each processing of those queries by receiver smart-contract is considered a transaction resulting in following actions: + - Sending further messages. + - Changing internal data or even code of smart-contract itself. + - Changing it's balance. + +Available interfaces of smart-contract are: + - Receiving **`internal messages`** from other smart-contract. + - 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. + +:::tip +Hereinafter we will use terms `actor`, `smart-contract` and `account` interchangeably. +::: + +## Interacting with TON ecosystem + +Before we step on our way of becoming a TON developer, we should become an advanced user of TON! Let's create your own `wallet`, send a few transactions, and see how our actions are reflected on the blockchain using `explorers`. + +### 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 [MyTonWallet](https://mytonwallet.io/). Go ahead, install and run it. + +### Step 2: Mainnet and Testnet + +At this moment, you should have already created your first wallet and backed up your **24-word phrase** (we will discuss its meaning later). Before we start to use it, we should discuss two variations of TON Blockchain that we can work with: **`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. + +Since TON basic transactions are very cheap, about 1 cent per transaction, investing just $5 will be enough for hundreds of them. If you decide to work on `Mainnet`, you will have a significantly smoother experience. 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. + +If you decide to use the `Testnet` version instead, you can switch to it in most wallets by entering the settings menu and tapping a few times on the wallet icon or name at the bottom. This will open the developer menu where you can switch to the `Testnet` version of the wallet. Here is an example for **MyTonWallet** (version **3.3.4**): + +
+
+ Screenshot of wallet settings menu +
+
+ Screenshot of network selection menu +
+
+ +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: 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. + +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. + +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: + +
+ Screenshot of explorer search interface +
+ +At this moment, your address should be in the `uninit` or `nonexisting` state depending on whether you have already received funds or not. Like any other `smart-contract`, wallets in TON need to be deployed to the network, and since this process requires some amount of transaction fees - `gas` - most wallet apps don't actually deploy the wallet `smart-contract` until you receive funds on your address and try to make your first transaction. + +
+ Screenshot of nonexistent wallet state +
+
+ Screenshot of uninitialized wallet state +
+ +As you can see, the `nonexisting` state is the default state for any address that has not been used before. `uninit` stands for an address that has some metadata such as funds, but hasn't been initialized by deployed `smart-contract` code or data. + +Let's send our first transaction to someone special - **ourselves**. And see how it looks on the `blockchain`. Enter the send menu in your wallet app and transfer some funds to your own address that you have copied before. In the explorer, our contract should start looking something like this: + +
+ Screenshot of active wallet state +
+ +And here we are, our wallet contract is deployed and ready to use. The first thing that we could mention is, despite the fact that we sent funds to ourselves, the balance has still been decreased by a little bit. As was mentioned before, any transaction requires some fee called `gas` depending on the amount of computation required by `smart-contract` logic, sent messages, and stored data. + +We can explore our `smart-contract` code in the corresponding tab which is represented in `TVM` byte-code - a virtual stack machine that executes byte code generated by compiling `smart-contract` programming languages: `FunC`, `Tact`, and `Tolk`. + +There is also a tab where we can execute `get-methods` provided by the `smart-contract`. Generally, these methods can be implemented in any way that doesn't change the `smart-contract` persistent data. Their primary purpose is to provide an easy way to access the contract state from outside the `blockchain`. + +The second thing to note is the contract type, `wallet_v5r1` in our case. Because some widely-used `smart-contracts` have standard implementations, most explorers are able to determine their type by analyzing the `smart-contract` code. + +It's important to understand that wallets are not special entities in the TON `blockchain`, which implements an actors model. A wallet is simply a `smart-contract` that doesn't have any specific rights or abilities compared to any other contract, such as one developed and deployed by you. + +Maybe you will be the one who creates and proposes the next version of wallet that will be accepted by the TON community, just as the TonKeeper Team did with the latest wallet version - `v5r1`! + +## Next Steps: + + - Try to experiment with `wallet app` and `wallet smart-contract`. + - Checkout [TON concepts](/v3/concepts/dive-into-ton/ton-blockchain/blockchain-of-blockchains) if you need more context. + - Continue to [Interact with blockchain](/v3/guidelines/quick-start/interact-with-blockchain) or [Developing smart-contracts](/v3/guidelines/quick-start/developing-smart-contracts/setup-environment) according to your needs. + +Happy creating on TON! + diff --git a/sidebars/guidelines.js b/sidebars/guidelines.js index 2dcccd68d1..7c349f7633 100644 --- a/sidebars/guidelines.js +++ b/sidebars/guidelines.js @@ -33,6 +33,25 @@ module.exports = [ 'type': 'html', 'value': '
', }, + { + 'type': 'html', + 'value': ' Quick Start ', + }, + 'v3/guidelines/quick-start/getting-started', + { + type: 'category', + 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/storage-and-get-methods', + 'v3/guidelines/quick-start/developing-smart-contracts/processing-messages', + ], + }, + { + 'type': 'html', + 'value': '
', + }, { 'type': 'html', 'value': ' Smart Contracts Guidelines ', diff --git a/static/img/tutorials/quick-start/active.png b/static/img/tutorials/quick-start/active.png new file mode 100644 index 0000000000..aff1449a87 Binary files /dev/null and b/static/img/tutorials/quick-start/active.png differ diff --git a/static/img/tutorials/quick-start/explorer1.png b/static/img/tutorials/quick-start/explorer1.png new file mode 100644 index 0000000000..efcbb6f4bd Binary files /dev/null and b/static/img/tutorials/quick-start/explorer1.png differ diff --git a/static/img/tutorials/quick-start/multi-contract-example-bright.png b/static/img/tutorials/quick-start/multi-contract-example-bright.png new file mode 100644 index 0000000000..ae2248cdd4 Binary files /dev/null and b/static/img/tutorials/quick-start/multi-contract-example-bright.png differ diff --git a/static/img/tutorials/quick-start/multi-contract-example-dark.png b/static/img/tutorials/quick-start/multi-contract-example-dark.png new file mode 100644 index 0000000000..fe55e5229d Binary files /dev/null and b/static/img/tutorials/quick-start/multi-contract-example-dark.png differ diff --git a/static/img/tutorials/quick-start/nonexist.png b/static/img/tutorials/quick-start/nonexist.png new file mode 100644 index 0000000000..e111b4a042 Binary files /dev/null and b/static/img/tutorials/quick-start/nonexist.png differ diff --git a/static/img/tutorials/quick-start/switch-to-testnet1.jpg b/static/img/tutorials/quick-start/switch-to-testnet1.jpg new file mode 100644 index 0000000000..8c30e08976 Binary files /dev/null and b/static/img/tutorials/quick-start/switch-to-testnet1.jpg differ diff --git a/static/img/tutorials/quick-start/switch-to-testnet2.jpg b/static/img/tutorials/quick-start/switch-to-testnet2.jpg new file mode 100644 index 0000000000..d86c9cb155 Binary files /dev/null and b/static/img/tutorials/quick-start/switch-to-testnet2.jpg differ diff --git a/static/img/tutorials/quick-start/uninit.png b/static/img/tutorials/quick-start/uninit.png new file mode 100644 index 0000000000..cdcd29f64d Binary files /dev/null and b/static/img/tutorials/quick-start/uninit.png differ diff --git a/static/img/tutorials/quick-start/wallet-address.jpg b/static/img/tutorials/quick-start/wallet-address.jpg new file mode 100644 index 0000000000..de350c4c16 Binary files /dev/null and b/static/img/tutorials/quick-start/wallet-address.jpg differ