Over the past year, many cool smart contract development tools have appeared on the TON blockchain. And I decided to update my open source development lessons on TON.
What will be discussed below
Writing a high-quality smart contract is a cyclical process - we write a piece of logic, then we test it, add logic, then we test it again.
The Blueprint development environment makes this process convenient. The first thing to start with is writing the smart contract code and compiling it.
Let's see how to compile a FunC smart contract using the Blueprint component - func-js.
Project initialization
Make a folder for your project and go into it.
// Windows example
mkdir test_folder
cd test_folder
In this tutorial, we will use the yarn
package manager
yarn init
Let's initialize the yarn
and just click on the questions in the console, as this is a test case. After that we should get the package.json file in the folder.
Now let's add the typescript and the necessary libraries. Install them as dev dependencies:
yarn add typescript ts-node @types/node @swc/core --dev
Create a tsconfig.json
file. We need the file for the project compilation configuration. Let's add to it:
{
"compilerOptions": {
"target" : "es2020",
"module" : "commonjs",
"esModuleInterop" : true,
"forceConsistentCasingInFileNames": true,
"strict" : true,
"skipLibCheck" : true,
"resolveJsonModule" : true
},
"ts-node": {
"transpileOnly" : true,
"transpile" : "ts-node/transpilers/swc"
}
}
In this tutorial, we will not dwell on what each line of configurations means, because this tutorial is about smart contracts. Now let's install the libraries necessary to work with TON:
yarn add ton-core ton-crypto @ton-community/func-js --dev
Now let's create a smart contract on FunC. Create contracts
folder and main.fc
file with minimal code:
() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
}
recv_internal
is called when a smart contract receives an inbound internal message. There are some variables at the stack when TVM initiates, by setting arguments in recv_internal we give smart-contract code awareness about some of them. T
Now let's write a script that will compile our smart contract template. Let's create a scripts
folder and a compile.ts
file in it.
So that we can use this script, we need to register it as a parameter in the package manager, i.e. in the package.json
file, it will look like this:
{
"name": "test_folder",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {
"@swc/core": "^1.3.63",
"@ton-community/func-js": "^0.6.2",
"@types/node": "^20.3.1",
"ton-core": "^0.49.1",
"ton-crypto": "^3.2.0",
"ts-node": "^10.9.1",
"typescript": "^5.1.3"
},
"scripts": {
"compile" : "ts-node ./scripts/compile.ts"
}
}
Now let's move on to writing the compilation script in the compile.ts
file. Here we make a reservation that the result of compilation will be the representation of the bag of Cell in the format of the base64 encoded string. This result needs to be saved somewhere, so let's create a build
folder.
Finally we get to the compilation file, the first thing we do is compile our code using the function compileFunc
:
import * as fs from "fs";
import { readFileSync } from "fs";
import process from "process";
import { Cell } from "ton-core";
import { compileFunc } from "@ton-community/func-js";
async function compileScript() {
const compileResult = await compileFunc({
targets: ["./contracts/main.fc"],
sources: (path) => readFileSync(path).toString("utf8"),
});
if (compileResult.status ==="error") {
console.log("Error happend");
process.exit(1);
}
}
compileScript();
The resulting hexBoС will be written to the folder:
import * as fs from "fs";
import { readFileSync } from "fs";
import process from "process";
import { Cell } from "ton-core";
import { compileFunc } from "@ton-community/func-js";
async function compileScript() {
const compileResult = await compileFunc({
targets: ["./contracts/main.fc"],
sources: (path) => readFileSync(path).toString("utf8"),
});
if (compileResult.status ==="error") {
console.log("Error happend");
process.exit(1);
}
const hexBoC = 'build/main.compiled.json';
fs.writeFileSync(
hexBoC,
JSON.stringify({
hex: Cell.fromBoc(Buffer.from(compileResult.codeBoc,"base64"))[0]
.toBoc()
.toString("hex"),
})
);
}
compileScript();
For convenience, you can dilute the code with console.log()
so that it is clear what worked and what did not when compiling, for example, you can add it to the end:
console.log("Compiled, hexBoC:"+hexBoC);
Which will output the resulting hexBoC.
Let's go to the contract
To create contracts, we need the standard FunC function library. Create a folder imports
inside folder contracts
and add this file there.
Now go to the main.fc
file and import the library, now the file looks like this:
#include "imports/stdlib.fc";
() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
}
Let's go over briefly on the contract, detailed analyzes and lessons on FunC starts here.
The smart contract that we will write will store the sender address of the internal message and also store the number one in the smart contract. It will also implement the Get method, which, when called, will return the address of the last sender of the message to the contract and one.
An internal message comes to our function, we will first get the service flags from there, and then the sender's address, which we will save:
#include "imports/stdlib.fc";
() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
slice cs = in_msg.begin_parse();
int flags = cs~load_uint(4);
slice sender_address = cs~load_msg_addr();
}
Let's save the address and one in the contract, i.e. write the data to register c4
.
#include "imports/stdlib.fc";
() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
slice cs = in_msg.begin_parse();
int flags = cs~load_uint(4);
slice sender_address = cs~load_msg_addr();
set_data(begin_cell().store_slice(sender_address).store_uint(1,32).end_cell());
}
It's time for the Get method, the method will return an address and a number, so let's start with (slice,int)
:
(slice,int) get_sender() method_id {
}
In the method itself, we get the data from the register and return it to the user
#include "imports/stdlib.fc";
() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
slice cs = in_msg.begin_parse();
int flags = cs~load_uint(4);
slice sender_address = cs~load_msg_addr();
set_data(begin_cell().store_slice(sender_address).store_uint(1,32).end_cell());
}
(slice,int) get_sender() method_id {
slice ds = get_data().begin_parse();
return (ds~load_msg_addr(),ds~load_uint(32));
}
Final contract:
#include "imports/stdlib.fc";
() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
slice cs = in_msg.begin_parse();
int flags = cs~load_uint(4);
slice sender_address = cs~load_msg_addr();
set_data(begin_cell().store_slice(sender_address).store_uint(1,32).end_cell());
}
(slice,int) get_sender() method_id {
slice ds = get_data().begin_parse();
return (ds~load_msg_addr(),ds~load_uint(32));
}
We start compilation using the yarn compile
command and get a file c main.compiled.json
in the build
folder:
{"hex":"b5ee9c72410104010035000114ff00f4a413f4bcf2c80b0102016203020015a1418bda89a1f481a63e610028d03031d0d30331fa403071c858cf16cb1fc9ed5474696b07"}
Conclusion
The next step is to write tests for this smart contract, thanks for your attention.I publish tutorials here, if you liked the article, subscribe so as not to miss new ones.
Top comments (3)
how can add func syntax highlighting here?
yeah, it works, I told him to consider it as C language
This is a very information post!
You can provide the language in the code block so the syntax highlight is also shown, eg. 3 backticks + language short form (ie. js)
Is it possible to add link to second post here?
dev.to/roma_i_m/smart-contract-pip...