TON Smart Contract Pipeline Part4 - Chatbot Contract + Writing onchain tests on the testnet
Tags: tutorial, beginners, blockchain, cryptocurrency
Published: 2023-09-07 14:30:16

## Introduction

In this tutorial, we will analyze the chatbot smart contract and then write on-chain tests for it. In this tutorial, we will focus how to inspect transactions in tests and for onchain tests.

This tutorial is part of an open source course that I am currently updating, [link to the repository](https://github.com/romanovichim/TonFunClessons_Eng), I will be glad to see your star

## About TON

TON is an [actor model](https://en.wikipedia.org/wiki/Actor_model) is a mathematical parallel computing model that underlies TON smart contracts. In it, each smart contract can receive one message, change its own state, or send one or more messages per unit of time.

Most often, to create a full-fledged application on TON, you need to write several smart contracts that seem to communicate with each other using messages. In order for the contract to understand what it needs to do when a message arrives, it is recommended to use `op`. `op` is a 32-bit identifier that should be passed in the body of the message.

Thus, inside the message using conditional statements, depending on the smart contract `op` performs different actions.

Therefore, it is important to be able to test messages, which we will do today.

The chatbot smart contract receives any internal message and responds to it with an internal message with the reply text.

## Parsing the contract

##### Standard Library

The first thing to do is [import the standard library](https://ton-blockchain.github.io/docs/#/func/stdlib). The library is just a wrapper for the most common TVM (TON virtual machine) commands that are not built-in.

```func
#include "imports/stdlib.fc";
```

To process internal messages, we need the `recv_internal()` method

```func
() recv_internal()  {

}
```
	
##### External method arguments
Here a logical question arises - how to understand what arguments a function should have so that it can receive messages on the TON network?

According to the documentation of the [TON virtual machine - TVM](https://ton-blockchain.github.io/docs/tvm.pdf), when an event occurs on an account in one of the TON chains, it triggers a transaction.

Each transaction consists of up to 5 stages. More details [here](https://ton-blockchain.github.io/docs/#/smart-contracts/tvm_overview?id=transactions-and-phases).

We are interested in **Compute phase**. And to be more specific, what is "on the stack" during initialization. For normal post-triggered transactions, the initial state of the stack looks like this:

5 elements:
- Smart contract balance (in nanoTons)
- Incoming message balance (in nanotones)
- Cell with incoming message
- Incoming message body, slice type
- Function selector (for recv_internal it is 0)

```func
() recv_internal(int balance, int msg_value, cell in_msg_full, slice in_msg_body)  {

}
```
	
But it is not necessary to write all the arguments to `recv_internal()`. By setting arguments to `recv_internal()`, we tell the smart contract code about some of them. Those arguments that the code will not know about will simply lie at the bottom of the stack, never touched. For our smart contract, this is:

```func
	() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {

	}
```

##### Gas to handle messages

Our smart contract will need to use the gas to send the message further, so we will check with what msg_value the message came, if it is very small (less than 0.01 TON), we will finish the execution of the smart contract with `return()`.

```func
#include "imports/stdlib.fc";

() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {

  if (msg_value < 10000000) { ;; 10000000 nanoton == 0.01 TON
	return ();
  }
  
}
```

##### Get the address

To send a message back, you need to get the address of the person who sent it to us. To do this, you need to parse the `in_msg` cell.

In order for us to take the address, we need to convert the cell into a slice using `begin_parse`:

```func
var cs = in_msg_full.begin_parse();
```

Now we need to "subtract" the resulting slice to the address. Using the `load_uint` function from the [FunC standard library](https://ton-blockchain.github.io/docs/#/func/stdlib) it loads an unsigned n-bit integer from the slice, "subtract" the flags.

```func
var flags = cs~load_uint(4);
```

In this lesson, we will not dwell on the flags in detail, but you can read more in paragraph [3.1.7] (https://ton-blockchain.github.io/docs/tblkch.pdf).

And finally, the address. Use `load_msg_addr()` - which loads from the slice the only prefix that is a valid MsgAddress.

```func
slice sender_address = cs~load_msg_addr(); 
```
	
Code:

```func
#include "imports/stdlib.fc";

() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {

  if (msg_value < 10000000) { ;; 10000000 nanoton == 0.01 TON
	return ();
  }
  
  slice cs = in_msg.begin_parse();
  int flags = cs~load_uint(4); 
  slice sender_address = cs~load_msg_addr(); 

}
```

##### Sending a message

Now you need to send a message back

##### Message structure

The full message structure can be found [here - message layout](https://ton-blockchain.github.io/docs/#/smart-contracts/messages?id=message-layout). But usually we don't need to control each field, so we can use the short form from [example](https://ton-blockchain.github.io/docs/#/smart-contracts/messages?id=sending-messages):
```func
 var msg = begin_cell()
	.store_uint(0x18, 6)
	.store_slice(addr)
	.store_coins(amount)
	.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
	.store_slice(message_body)
  .end_cell();
```

As you can see, functions of the [FunC standard library](https://ton-blockchain.github.io/docs/#/func/stdlib) are used to build the message. Namely, the "wrapper" functions of the Builder primitives (partially built cells, as you may remember from the first lesson). Consider:

`begin_cell()` - will create a Builder for the future cell
 `end_cell()` - will create a Cell (cell)
 `store_uint` - store uint in Builder
 `store_slice` - store the slice in the Builder
 `store_coins` - here the documentation means `store_grams` - used to store TonCoins. More details [here](https://ton-blockchain.github.io/docs/#/func/stdlib?id=store_grams).

##### Message body

In the body of the message we put `op` and our message `reply`, to put a message we need to do `slice`.

```func
slice msg_text = "reply";
```

In the recommendations about the body of the message, there is a recommendation to add `op`, despite the fact that it will not carry any functionality here, we will add it.

In order for us to create a similar client-server architecture on smart contracts described in the recommendations, it is proposed to start each message (strictly speaking, the message body) with some `op` flag, which will identify what operation the smart contract should perform.

Let's put `op` equal to 0 in our message.

Now the code looks like this:

```func
#include "imports/stdlib.fc";

() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {

  if (msg_value < 10000000) { ;; 10000000 nanoton == 0.01 TON
	return ();
  }
	  
  slice cs = in_msg.begin_parse();
  int flags = cs~load_uint(4); 
  slice sender_address = cs~load_msg_addr(); 

  slice msg_text = "reply"; 

  cell msg = begin_cell()
	  .store_uint(0x18, 6)
	  .store_slice(sender_address)
	  .store_coins(100) 
	  .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
	  .store_uint(0, 32)
	  .store_slice(msg_text) 
  .end_cell();

	}
```

The message is ready, let's send it.

##### Message sending mode(mode)

To send messages, use `send_raw_message` from the [standard library](https://ton-blockchain.github.io/docs/#/func/stdlib?id=send_raw_message).

We have already collected the msg variable, it remains to figure out `mode`. Description of each mode is in [documentation](https://ton-blockchain.github.io/docs/#/func/stdlib?id=send_raw_message). Let's look at an example to make it clearer.

Let there be 100 coins on the balance of the smart contract and we receive an internal message with 60 coins and send a message with 10, the total fee is 3.

  `mode = 0` - balance (100+60-10 = 150 coins), send(10-3 = 7 coins)
  `mode = 1` - balance (100+60-10-3 = 147 coins), send(10 coins)
  `mode = 64` - balance (100-10 = 90 coins), send (60+10-3 = 67 coins)
  `mode = 65` - balance (100-10-3=87 coins), send (60+10 = 70 coins)
  `mode = 128` -balance (0 coins), send (100+60-3 = 157 coins)

As we choose `mode`, let's go to [documentation](https://docs.ton.org/develop/smart-contracts/messages#message-modes):

- We're sending a normal message, so mode 0.
- We will pay the commission for the transfer separately from the cost of the message, which means +1.
- We will also ignore any errors that occur during the processing of this message on the action phase, so +2.

We get `mode` == 3, the final smart contract:

```func
#include "imports/stdlib.fc";

() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {

  if (msg_value < 10000000) { ;; 10000000 nanoton == 0.01 TON
	return ();
  }
	  
  slice cs = in_msg.begin_parse();
  int flags = cs~load_uint(4); 
  slice sender_address = cs~load_msg_addr(); 

  slice msg_text = "reply"; 

  cell msg = begin_cell()
	  .store_uint(0x18, 6)
	  .store_slice(sender_address)
	  .store_coins(100) 
	  .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
	  .store_uint(0, 32)
	  .store_slice(msg_text) 
  .end_cell();

  send_raw_message(msg, 3);
}
```
##hexBoC

Before deploying a smart contract, you need to compile it into hexBoС, let's take the project from the previous tutorial.

Let's rename `main.fc` to `chatbot.fc` and write our smart contract into it.

Since we changed the filename, we need to upgrade `compile.ts` as well:
```ts
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/chatbot.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"),
		})

	);

	console.log("Compiled, hexBoC:"+hexBoC);

}

compileScript();
```
Compile the smart contract with the `yarn compile` command.

You now have a `hexBoC` representation of the smart contract.

## Check if there is a transaction

Since we are using the draft of the previous tutorial as a template, we already have a test framework, open the `main.spec.ts` file and remove from there everything related to the GET method:

```ts
import { Cell, Address, toNano } from "ton-core";
import { hex } from "../build/main.compiled.json";
import { Blockchain } from "@ton-community/sandbox";
import { MainContract } from "../wrappers/MainContract";
import { send } from "process";
import "@ton-community/test-utils";

describe("test tests", () => {
	it("test of test", async() => {
		const codeCell = Cell.fromBoc(Buffer.from(hex,"hex"))[0];

		const blockchain = await Blockchain.create();

		const myContract = blockchain.openContract(
			await MainContract.createFromConfig({}, codeCell)
		);

		const senderWallet = await blockchain.treasury("sender");

		const sentMessageResult = await myContract.sendInternalMessage(senderWallet.getSender(),toNano("0.05"));

		expect(sentMessageResult.transactions).toHaveTransaction({
			from: senderWallet.address,
			to: myContract.address,
			success: true,
		});

	});
});
```    
We see that at the moment, it is checked whether the transaction has been sent to our smart contract. This is due to the `sentMessageResult.transactions` object. Let's take a close look at it and see what we can test based on this object.

If we just print this object to the console, it will consist of a lot of raw information, for convenience we will use `flattenTransaction` from `@ton-community/test-utils`:
``` ts
import { Cell, Address, toNano } from "ton-core";
import { hex } from "../build/main.compiled.json";
import { Blockchain } from "@ton-community/sandbox";
import { MainContract } from "../wrappers/MainContract";
import { send } from "process";
import "@ton-community/test-utils";
import { flattenTransaction } from "@ton-community/test-utils";



describe("msg test", () => {
	it("test", async() => {
		const codeCell = Cell.fromBoc(Buffer.from(hex,"hex"))[0];

		const blockchain = await Blockchain.create();

		const myContract = blockchain.openContract(
			await MainContract.createFromConfig({}, codeCell)
		);

		const senderWallet = await blockchain.treasury("sender");

		const sentMessageResult = await myContract.sendInternalMessage(senderWallet.getSender(),toNano("0.05"));

		expect(sentMessageResult.transactions).toHaveTransaction({
			from: senderWallet.address,
			to: myContract.address,
			success: true,
		});

		const arr = sentMessageResult.transactions.map(tx => flattenTransaction(tx));
		console.log(arr)


	});
});
```     
What you see in the console can be used for tests, let's check that the message our chatbot sent is equal to reply.

Let's assemble the message, in accordance with what we collected in the smart contract.

```ts
	let reply = beginCell().storeUint(0, 32).storeStringTail("reply").endCell();
``` 
Now, using messages, check that there is such a transaction:
```ts
import { Cell, Address, toNano, beginCell } from "ton-core";
import { hex } from "../build/main.compiled.json";
import { Blockchain } from "@ton-community/sandbox";
import { MainContract } from "../wrappers/MainContract";
import { send } from "process";
import "@ton-community/test-utils";
import { flattenTransaction } from "@ton-community/test-utils";



describe("msg test", () => {
	it("test", async() => {
		const codeCell = Cell.fromBoc(Buffer.from(hex,"hex"))[0];

		const blockchain = await Blockchain.create();

		const myContract = blockchain.openContract(
			await MainContract.createFromConfig({}, codeCell)
		);

		const senderWallet = await blockchain.treasury("sender");

		const sentMessageResult = await myContract.sendInternalMessage(senderWallet.getSender(),toNano("0.05"));

		expect(sentMessageResult.transactions).toHaveTransaction({
			from: senderWallet.address,
			to: myContract.address,
			success: true,
		});

		//const arr = sentMessageResult.transactions.map(tx => flattenTransaction(tx));

		let reply = beginCell().storeUint(0, 32).storeStringTail("reply").endCell();

		expect(sentMessageResult.transactions).toHaveTransaction({
			body: reply,
			from: myContract.address,
			to: senderWallet.address
		});

	});
});
``` 
Run the tests with the `yarn test` command and see that everything works. Thus, in tests we can collect objects the same as in a smart contract and check that the transaction was.

## Onchain tests

Sometimes a situation may arise that you need to run your smart contracts on the test network (a situation where there are a lot of contracts). Let's try this with our example.

In the `scripts` folder we will make the `onchain.ts` file, for ease of launch, add to `package.json` `"onchain": "ts-node ./scripts/onchain.ts"`:


	  "scripts": {
		"compile": "ts-node ./scripts/compile.ts",
		"test": "yarn jest",
		"deploy": "yarn compile && ts-node ./scripts/deploy.ts",
		"onchain": "ts-node ./scripts/onchain.ts"
	  },

Первое, что нам понадобиться для тестов, это адрес смарт-контракта, соберем его:

```ts
import { Cell, beginCell, contractAddress, toNano} from "ton-core";
import { hex } from "../build/main.compiled.json";
import { TonClient } from "ton";

async function onchainScript() {
	const codeCell = Cell.fromBoc(Buffer.from(hex,"hex"))[0];
	const dataCell = new Cell();

	const address = contractAddress(0,{
		code: codeCell,
		data: dataCell,
	});

	console.log("Address: ",address)

}

onchainScript();
``` 
The test for the test network will offer us to deploy a transaction via a QR code into our smart contract and check every 10 seconds if the answer has appeared on the network.

> Of course, this is a simplification for an example, the essence is just to show the logic.

Let's collect a QR code, by which we will conduct a transaction through Tonkeeper. For our example, it is important that the amount of TON is sufficient so as not to throw an exception written in the contract.
```ts
import { Cell, beginCell, contractAddress, toNano} from "ton-core";
import { hex } from "../build/main.compiled.json";
import { TonClient } from "ton";
import qs from "qs";
import qrcode from "qrcode-terminal";

async function onchainScript() {
	const codeCell = Cell.fromBoc(Buffer.from(hex,"hex"))[0];
	const dataCell = new Cell();

	const address = contractAddress(0,{
		code: codeCell,
		data: dataCell,
	});

	console.log("Address: ",address)

	let transactionLink =
	'https://app.tonkeeper.com/transfer/' +
	address.toString({
		testOnly: true,
	}) +
	"?" +
	qs.stringify({
		text: "Sent simple in",
		amount: toNano("0.6").toString(10),
	});

	console.log("Transaction link:",transactionLink);


	qrcode.generate(transactionLink, {small: true }, (qr) => {
		console.log(qr);
	});

}

onchainScript();
``` 
In order to receive data from the test network, we need some kind of data source. Data can be obtained via ADNL from Liteservers, but we will talk about ADNL in the following tutorials. In this tutorial, we will use the TON Center API.

```ts
	const API_URL = "https://testnet.toncenter.com/api/v2"
``` 
We will make requests through the Http client [axios](https://axios-http.com/ru/docs/intro), install: `yarn add axios`.

Among the Toncenter methods, we need getTransactions with the limit 1 parameter, i.e. we will take the last transaction. Let's write two helper functions for requesting information:

```ts
// axios http client // yarn add axios
async function getData(url: string): Promise<any> {
	try {
	  const config: AxiosRequestConfig = {
		url: url,
		method: "get",
	  };
	  const response: AxiosResponse = await axios(config);
	  //console.log(response)
	  return response.data.result;
	} catch (error) {
	  console.error(error);
	  throw error;
	}
  }

async function getTransactions(address: String) {
  var transactions;
  try {
	transactions = await getData(
	  `${API_URL}/getTransactions?address=${address}&limit=1`
	);
  } catch (e) {
	console.error(e);
  }
  return transactions;
}
``` 

Now we need a function that will call the API at intervals, for this there is a convenient method [SetInterval](https://developer.mozilla.org/docs/Web/API/setInterval):
```ts
import { Cell, beginCell, contractAddress, toNano} from "ton-core";
import { hex } from "../build/main.compiled.json";
import { TonClient } from "ton";
import qs from "qs";
import qrcode from "qrcode-terminal";
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";


const API_URL = "https://testnet.toncenter.com/api/v2"

	// axios http client // yarn add axios
async function getData(url: string): Promise<any> {
	try {
	  const config: AxiosRequestConfig = {
		url: url,
		method: "get",
	  };
	  const response: AxiosResponse = await axios(config);
	  //console.log(response)
	  return response.data.result;
	} catch (error) {
	  console.error(error);
	  throw error;
	}
  }

async function getTransactions(address: String) {
  var transactions;
  try {
	transactions = await getData(
	  `${API_URL}/getTransactions?address=${address}&limit=1`
	);
  } catch (e) {
	console.error(e);
  }
  return transactions;
}

async function onchainScript() {
	const codeCell = Cell.fromBoc(Buffer.from(hex,"hex"))[0];
	const dataCell = new Cell();

	const address = contractAddress(0,{
		code: codeCell,
		data: dataCell,
	});


	console.log("Address: ",address)

	let transactionLink =
	'https://app.tonkeeper.com/transfer/' +
	address.toString({
		testOnly: true,
	}) +
	"?" +
	qs.stringify({
		text: "Sent simple in",
		amount: toNano("0.6").toString(10),
		//bin: beginCell().storeUint(1,32).endCell().toBoc({idx: false}).toString("base64"),
	});

	console.log("Transaction link:",transactionLink);


	qrcode.generate(transactionLink, {small: true }, (qr) => {
		console.log(qr);
	});

	setInterval(async () => {
		const txes = await getTransactions(address.toString());
		if(txes[0].in_msg.source === "EQCj2gVRdFS0qOZnUFXdMliONgSANYXfQUDMsjd8fbTW-RuC") {

		}

	},10000)


}

onchainScript();
``` 
It is important to note here that the API returns transactions, not messages, so we need to check that IN received the address of our wallet (here I just hardcoded it) and the message (which we put under the QR), and output the message of the first message in OUT. We also display the date, we get:

```ts
import { Cell, beginCell, contractAddress, toNano} from "ton-core";
import { hex } from "../build/main.compiled.json";
import { TonClient } from "ton";
import qs from "qs";
import qrcode from "qrcode-terminal";
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";


const API_URL = "https://testnet.toncenter.com/api/v2"

	// axios http client // yarn add axios
async function getData(url: string): Promise<any> {
	try {
	  const config: AxiosRequestConfig = {
		url: url,
		method: "get",
	  };
	  const response: AxiosResponse = await axios(config);
	  //console.log(response)
	  return response.data.result;
	} catch (error) {
	  console.error(error);
	  throw error;
	}
  }

async function getTransactions(address: String) {
  var transactions;
  try {
	transactions = await getData(
	  `${API_URL}/getTransactions?address=${address}&limit=1`
	);
  } catch (e) {
	console.error(e);
  }
  return transactions;
}

async function onchainScript() {
	const codeCell = Cell.fromBoc(Buffer.from(hex,"hex"))[0];
	const dataCell = new Cell();

	const address = contractAddress(0,{
		code: codeCell,
		data: dataCell,
	});


	console.log("Address: ",address)

	let transactionLink =
	'https://app.tonkeeper.com/transfer/' +
	address.toString({
		testOnly: true,
	}) +
	"?" +
	qs.stringify({
		text: "Sent simple in",
		amount: toNano("0.6").toString(10),
		//bin: beginCell().storeUint(1,32).endCell().toBoc({idx: false}).toString("base64"),
	});

	console.log("Transaction link:",transactionLink);


		qrcode.generate(transactionLink, {small: true }, (qr) => {
			console.log(qr);
		});

		setInterval(async () => {
			const txes = await getTransactions(address.toString());
			if(txes[0].in_msg.source === "EQCj2gVRdFS0qOZnUFXdMliONgSANYXfQUDMsjd8fbTW-RuC") {

            	console.log("Last tx: " + new Date(txes[0].utime * 1000))
            	console.log("IN from: "+ txes[0].in_msg.source+" with msg: "+ txes[0].in_msg.message)
            	console.log("OUT from: "+ txes[0].out_msgs[0].source +" with msg: "+ txes[0].out_msgs[0].message)
			}

		},10000)


	}

	onchainScript();
```

We launch the `yarn onchain` command, scan the QR, send the transaction and wait for our transaction to arrive.

## Conclusion

I hope you enjoyed the pipeline series. I will be grateful to the asterisk on the repository.I publish tutorials [here](https://t.me/ton_learn), if you liked the article, subscribe so as not to miss new ones.