Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quick-start-guide/Developing smart-contracts #1019

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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
```

Original file line number Diff line number Diff line change
@@ -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.

Original file line number Diff line number Diff line change
@@ -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.

Loading