Sobre como escrever e publicar um contrato inteligente na Telegram Open Network (TON)

Sobre como redigir e publicar um contrato inteligente no TON

Sobre o que é este artigo?

No artigo contarei como participei da primeira (de duas) competições de blockchain do Telegram, não ganhei prêmio e resolvi registrar minha experiência em um artigo para que não caísse no esquecimento e, quem sabe, ajudasse alguém.

Como eu não queria escrever código abstrato, mas sim fazer algo que funcionasse, para o artigo escrevi um contrato inteligente para uma loteria instantânea e um site que mostra dados de contratos inteligentes diretamente do TON sem usar armazenamento intermediário.

O artigo será útil para quem deseja fazer seu primeiro contrato inteligente em TON, mas não sabe por onde começar.

Usando a loteria como exemplo, irei desde a instalação do ambiente até a publicação de um contrato inteligente, interagindo com ele e escrevendo um site para recebimento e publicação de dados.

Sobre a participação na competição

Em outubro passado, o Telegram anunciou uma competição de blockchain com novos idiomas Fift и FunC. Foi necessário escolher qualquer um dos cinco contratos inteligentes propostos. Achei que seria legal fazer algo diferente, aprender um idioma e fazer alguma coisa, mesmo que não precise escrever mais nada no futuro. Além disso, o assunto está constantemente na boca.

Vale dizer que não tive experiência no desenvolvimento de contratos inteligentes.

Planejei participar até o fim até que pudesse e depois escrever um artigo de revisão, mas falhei logo no primeiro. EU escreveu uma carteira com assinatura múltipla ativada FunC e geralmente funcionou. Eu tomei isso como base contrato inteligente no Solidity.

Naquela época, pensei que isso era definitivamente o suficiente para levar pelo menos algum lugar premiado. Como resultado, cerca de 40 dos 60 participantes tornaram-se vencedores e eu não estava entre eles. Em geral, não há nada de errado com isso, mas uma coisa me incomodou. No momento do anúncio dos resultados não havia sido feita a revisão da prova do meu contrato, perguntei aos participantes do chat se havia mais alguém que não tinha, não havia.

Aparentemente prestando atenção às minhas mensagens, dois dias depois os jurados publicaram um comentário e ainda não entendo se eles acidentalmente perderam meu contrato inteligente durante o julgamento ou simplesmente pensaram que era tão ruim que não precisava de comentário. Fiz uma pergunta na página, mas não recebi resposta. Embora não seja segredo quem julgou, considerei desnecessário escrever mensagens pessoais.

Muito tempo foi gasto na compreensão, então decidiu-se escrever um artigo. Como ainda não há muitas informações, este artigo ajudará a economizar tempo de todos os interessados.

O conceito de contratos inteligentes em TON

Antes de escrever qualquer coisa, você precisa descobrir de que lado abordar isso. Portanto, agora direi em que partes consiste o sistema. Mais precisamente, quais partes você precisa saber para redigir pelo menos algum tipo de contrato de trabalho.

Vamos nos concentrar em escrever um contrato inteligente e trabalhar com TON Virtual Machine (TVM), Fift и FunC, então o artigo é mais como uma descrição do desenvolvimento de um programa regular. Não vamos nos alongar sobre como a plataforma funciona aqui.

Em geral sobre como funciona TVM e linguagem Fift existe uma boa documentação oficial. Enquanto participava da competição e agora enquanto redigia o contrato atual, muitas vezes recorria a ela.

A principal linguagem em que os contratos inteligentes são escritos é FunC. Não há documentação sobre isso no momento, então para escrever algo você precisa estudar exemplos de contratos inteligentes do repositório oficial e a implementação da própria linguagem lá, além de poder ver exemplos de contratos inteligentes dos dois últimos competições. Links no final do artigo.

Digamos que já escrevemos um contrato inteligente para FunC, depois compilamos o código no montador Fift.

O contrato inteligente compilado ainda não foi publicado. Para fazer isso você precisa escrever uma função em Fift, que receberá o código do contrato inteligente e alguns outros parâmetros como entrada, e a saída será um arquivo com a extensão .boc (que significa “saco de células”) e, dependendo de como o escrevemos, uma chave privada e um endereço, que é gerado com base no código do contrato inteligente. Você já pode enviar gramas para o endereço de um contrato inteligente que ainda não foi publicado.

Para publicar um contrato inteligente em TON recebido .boc o arquivo precisará ser enviado para o blockchain usando um cliente leve (mais sobre isso abaixo). Mas antes de publicar, é necessário transferir os gramas para o endereço gerado, caso contrário o contrato inteligente não será publicado. Após a publicação, você pode interagir com o contrato inteligente enviando mensagens de fora (por exemplo, usando um cliente leve) ou de dentro (por exemplo, um contrato inteligente envia a outro uma mensagem dentro do TON).

Depois de entendermos como o código é publicado, fica mais fácil. Sabemos aproximadamente o que queremos escrever e como nosso programa funcionará. E enquanto escrevemos, procuramos como isso já está implementado em contratos inteligentes existentes ou analisamos o código de implementação Fift и FunC no repositório oficial ou consulte a documentação oficial.

Muitas vezes procurei palavras-chave no chat do Telegram onde se reuniam todos os participantes da competição e funcionários do Telegram, e aconteceu que durante a competição todos se reuniram lá e começaram a discutir Fift e FunC. Link no final do artigo.

É hora de passar da teoria à prática.

Preparando o ambiente para trabalhar com a TON

Eu fiz tudo o que será descrito no artigo sobre macOS e verifiquei tudo novamente em um ambiente limpo. Ubuntu Ubuntu 18.04 LTS no Docker.

A primeira coisa que você precisa fazer é baixar e instalar lite-client com o qual você pode enviar solicitações para TON.

As instruções no site oficial descrevem o processo de instalação de forma bastante completa e clara, omitindo alguns detalhes. Aqui, seguimos as instruções, instalando as dependências que faltavam ao longo do processo. Não compilei cada projeto individualmente, utilizando apenas os arquivos do repositório oficial. Ubuntu (no MacOS eu usei brew).

apt -y install git 
apt -y install wget 
apt -y install cmake 
apt -y install g++ 
apt -y install zlib1g-dev 
apt -y install libssl-dev 

Depois que todas as dependências estiverem instaladas, você pode instalar lite-client, Fift, FunC.

Primeiro, clonamos o repositório TON junto com suas dependências. Para maior comodidade faremos tudo em uma pasta ~/TON.

cd ~/TON
git clone https://github.com/ton-blockchain/ton.git
cd ./ton
git submodule update --init --recursive

O repositório também armazena implementações Fift и FunC.

Agora estamos prontos para montar o projeto. O código do repositório é clonado em uma pasta ~/TON/ton. Em ~/TON crie uma pasta build e colete o projeto nele.

mkdir ~/TON/build 
cd ~/TON/build
cmake ../ton

Como vamos escrever um contrato inteligente, precisamos não apenas lite-clientMas Fift с FunC, então vamos compilar tudo. Não é um processo rápido, por isso estamos aguardando.

cmake --build . --target lite-client
cmake --build . --target fift
cmake --build . --target func

Em seguida, baixe o arquivo de configuração que contém dados sobre o nó ao qual lite-client irá se conectar.

wget https://test.ton.org/ton-lite-client-test1.config.json

Fazendo os primeiros pedidos à TON

Agora vamos lançar lite-client.

cd ~/TON/build
./lite-client/lite-client -C ton-lite-client-test1.config.json

Se a construção foi bem-sucedida, após o lançamento você verá um log da conexão do cliente light ao nó.

[ 1][t 2][1582054822.963129282][lite-client.h:201][!testnode]   conn ready
[ 2][t 2][1582054823.085654020][lite-client.cpp:277][!testnode] server version is 1.1, capabilities 7
[ 3][t 2][1582054823.085725069][lite-client.cpp:286][!testnode] server time is 1582054823 (delta 0)
...

Você pode executar o comando help e veja quais comandos estão disponíveis.

help

Vamos listar os comandos que usaremos neste artigo.

list of available commands:
last    Get last block and state info from server
sendfile <filename> Load a serialized message from <filename> and send it to server
getaccount <addr> [<block-id-ext>]  Loads the most recent state of specified account; <addr> is in [<workchain>:]<hex-or-base64-addr> format
runmethod <addr> [<block-id-ext>] <method-id> <params>...   Runs GET method <method-id> of account <addr> with specified parameters

last получает последний созданный блок с сервера. 

sendfile <filename> отправляет в TON файл с сообщением, именно с помощью этой команды публикуется смарт-контракт и запрсосы к нему. 

getaccount <addr> загружает текущее состояние смарт-контракта с указанным адресом. 

runmethod <addr> [<block-id-ext>] <method-id> <params>  запускает get-методы смартконтракта. 

Agora estamos prontos para redigir o contrato em si.

Implementação

Idéia

Como escrevi acima, o contrato inteligente que estamos escrevendo é uma loteria.

Além disso, não se trata de uma loteria em que você precisa comprar um bilhete e esperar uma hora, um dia ou um mês, mas sim uma loteria instantânea em que o usuário transfere para o endereço do contrato N gramas e recupera instantaneamente 2 * N gramas ou perde. Faremos com que a probabilidade de ganhar seja de cerca de 40%. Se não houver gramas suficientes para pagamento, consideraremos a transação como uma recarga.

Além disso, é importante que as apostas possam ser visualizadas em tempo real e de forma cómoda, para que o utilizador possa perceber imediatamente se ganhou ou perdeu. Portanto, você precisa fazer um site que mostre apostas e resultados diretamente da TON.

Escrevendo um contrato inteligente

Por conveniência, destaquei o código do FunC; o plugin pode ser encontrado e instalado na pesquisa do Visual Studio Code; se de repente você quiser adicionar algo, disponibilizei o plugin publicamente. Além disso, alguém já criou um plugin para trabalhar com Fift, você também pode instalá-lo e encontrá-lo no VSC.

Vamos criar imediatamente um repositório onde iremos submeter os resultados intermediários.

Para facilitar nossa vida, escreveremos um contrato inteligente e o testaremos localmente até que esteja pronto. Só depois publicaremos na TON.

O contrato inteligente possui dois métodos externos que podem ser acessados. Primeiro, recv_external() esta função é executada quando uma solicitação ao contrato vem do mundo exterior, ou seja, não da TON, por exemplo, quando nós mesmos geramos uma mensagem e a enviamos através do lite-client. Segundo, recv_internal() é quando, dentro da própria TON, algum contrato se refere ao nosso. Em ambos os casos, você pode passar parâmetros para a função.

Vamos começar com um exemplo simples que funcionará se for publicado, mas não há carga funcional nele.

() recv_internal(slice in_msg) impure {
    ;; TODO: implementation 
}

() recv_external(slice in_msg) impure {
    ;; TODO: implementation  
}

Aqui precisamos explicar o que é slice. Todos os dados armazenados no TON Blockchain são uma coleção TVM cell ou simplesmente cell, em tal célula você pode armazenar até 1023 bits de dados e até 4 links para outras células.

TVM cell slice ou slice isso faz parte do existente cell é usado para analisá-lo, isso ficará claro mais tarde. O principal para nós é que podemos transferir slice e dependendo do tipo de mensagem, processar os dados em recv_external() ou recv_internal().

impure — uma palavra-chave que indica que a função modifica os dados do contrato inteligente.

Vamos salvar o código do contrato em lottery-code.fc e compilar.

~/TON/build/crypto/func -APSR -o lottery-compiled.fif ~/TON/ton/crypto/smartcont/stdlib.fc ./lottery-code.fc 

O significado das bandeiras pode ser visualizado usando o comando

~/TON/build/crypto/func -help

Compilamos o código assembler Fift em lottery-compiled.fif:

// lottery-compiled.fif

"Asm.fif" include
// automatically generated from `/Users/rajymbekkapisev/TON/ton/crypto/smartcont/stdlib.fc` `./lottery-code.fc` 
PROGRAM{
  DECLPROC recv_internal
  DECLPROC recv_external
  recv_internal PROC:<{
    //  in_msg
    DROP    // 
  }>
  recv_external PROC:<{
    //  in_msg
    DROP    // 
  }>
}END>c

Pode ser lançado localmente, para isso prepararemos o ambiente.

Observe que a primeira linha conecta Asm.fif, este é o código escrito em Fift para o montador Fift.

Como queremos executar e testar o contrato inteligente localmente, criaremos um arquivo lottery-test-suite.fif e copie o código compilado lá, substituindo a última linha nele, que grava o código do contrato inteligente em uma constante codepara transferi-lo para a máquina virtual:

"TonUtil.fif" include
"Asm.fif" include

PROGRAM{
  DECLPROC recv_internal
  DECLPROC recv_external
  recv_internal PROC:<{
    //  in_msg
    DROP    // 
  }>
  recv_external PROC:<{
    //  in_msg
    DROP    // 
  }>
}END>s constant code

Até agora parece claro, agora vamos adicionar ao mesmo arquivo o código que usaremos para lançar o TVM.

0 tuple 0x076ef1ea , // magic
0 , 0 , // actions msg_sents
1570998536 , // unix_time
1 , 1 , 3 , // block_lt, trans_lt, rand_seed
0 tuple 100000000000000 , dictnew , , // remaining balance
0 , dictnew , // contract_address, global_config
1 tuple // wrap to another tuple
constant c7

0 constant recv_internal // to run recv_internal() 
-1 constant recv_external // to invoke recv_external()

В c7 registramos o contexto, ou seja, os dados com os quais o TVM (ou estado da rede) será lançado. Ainda durante a competição, um dos desenvolvedores mostrou como criar c7 e eu copiei. Neste artigo, talvez precisemos alterar rand_seed já que a geração de um número aleatório depende disso e se não for alterado, o mesmo número será retornado todas as vezes.

recv_internal и recv_external constantes com valores 0 e -1 serão responsáveis ​​por chamar as funções correspondentes no contrato inteligente.

Agora estamos prontos para criar o primeiro teste para nosso contrato inteligente vazio. Para maior clareza, por enquanto adicionaremos todos os testes ao mesmo arquivo lottery-test-suite.fif.

Vamos criar uma variável storage e escreva um vazio nele cell, este será o armazenamento de contrato inteligente.

message Esta é a mensagem que transmitiremos ao contato inteligente de fora. Também o deixaremos vazio por enquanto.

variable storage 
<b b> storage ! 

variable message 
<b b> message ! 

Depois de prepararmos as constantes e variáveis, lançamos o TVM usando o comando runvmctx e passe os parâmetros criados para a entrada.

message @ 
recv_external 
code 
storage @ 
c7 
runvmctx 

No final teremos sucesso tal código intermediário para Fift.

Agora podemos executar o código resultante.

export FIFTPATH=~/TON/ton/crypto/fift/lib // выполняем один раз для удобства 
~/TON/build/crypto/fift -s lottery-test-suite.fif 

O programa deve rodar sem erros e na saída veremos o log de execução:

execute SETCP 0
execute DICTPUSHCONST 19 (xC_,1)
execute DICTIGETJMPZ
execute DROP
execute implicit RET
[ 3][t 0][1582281699.325381279][vm.cpp:479]     steps: 5 gas: used=304, max=9223372036854775807, limit=9223372036854775807, credit=0

Ótimo, escrevemos a primeira versão funcional do contrato inteligente.

Agora precisamos adicionar funcionalidade. Primeiro vamos lidar com mensagens que vêm do mundo exterior para recv_external()

O próprio desenvolvedor escolhe o formato da mensagem que o contrato pode aceitar.

Mas usualmente

  • Em primeiro lugar, queremos proteger o nosso contrato do mundo exterior e fazer com que apenas o proprietário do contrato possa enviar-lhe mensagens externas.
  • em segundo lugar, quando enviamos uma mensagem válida para a TON, queremos que isso aconteça exatamente uma vez e quando enviamos a mesma mensagem novamente, o contrato inteligente a rejeita.

Então quase todo contrato resolve esses dois problemas, já que nosso contrato aceita mensagens externas, precisamos cuidar disso também.

Faremos isso na ordem inversa. Primeiro, vamos resolver o problema da repetição: se o contrato já recebeu tal mensagem e a processou, não a executará uma segunda vez. E então resolveremos o problema para que apenas um determinado círculo de pessoas possa enviar mensagens para o contrato inteligente.

Existem diferentes maneiras de resolver o problema de mensagens duplicadas. Veja como faremos isso. No contrato inteligente, inicializamos o contador de mensagens recebidas com o valor inicial 0. Em cada mensagem do contrato inteligente, adicionaremos o valor atual do contador. Se o valor do contador na mensagem não corresponder ao valor do contrato inteligente, então não o processamos; se corresponder, então o processamos e aumentamos o contador no contrato inteligente em 1.

Voltamos para lottery-test-suite.fif e adicione um segundo teste a ele. Se enviarmos um número incorreto, o código deverá lançar uma exceção. Por exemplo, deixe os dados do contrato armazenarem 166 e enviaremos 165.

<b 166 32 u, b> storage !
<b 165 32 u, b> message !

message @ 
recv_external 
code 
storage @ 
c7 
runvmctx

drop 
exit_code ! 
."Exit code " exit_code @ . cr 
exit_code @ 33 - abort"Test #2 Not passed"

Vamos lançar.

 ~/TON/build/crypto/fift -s lottery-test-suite.fif 

E veremos que o teste é executado com erro.

[ 1][t 0][1582283084.210902214][words.cpp:3046] lottery-test-suite.fif:67: abort": Test #2 Not passed
[ 1][t 0][1582283084.210941076][fift-main.cpp:196]      Error interpreting file `lottery-test-suite.fif`: error interpreting included file `lottery-test-suite.fif` : lottery-test-suite.fif:67: abort": Test #2 Not passed

Neste estágio lottery-test-suite.fif deveria parecer по ссылке.

Agora vamos adicionar a lógica do contador ao contrato inteligente em lottery-code.fc.

() recv_internal(slice in_msg) impure {
    ;; TODO: implementation 
}

() recv_external(slice in_msg) impure {
    if (slice_empty?(in_msg)) {
        return (); 
    }
    int msg_seqno = in_msg~load_uint(32);
    var ds = begin_parse(get_data());
    int stored_seqno = ds~load_uint(32);
    throw_unless(33, msg_seqno == stored_seqno);
}

В slice in_msg reside a mensagem que enviamos.

A primeira coisa que fazemos é verificar se a mensagem contém dados, caso contrário simplesmente saímos.

Em seguida, analisamos a mensagem. in_msg~load_uint(32) carrega o número 165, 32 bits unsigned int da mensagem transmitida.

Em seguida, carregamos 32 bits do armazenamento do contrato inteligente. Verificamos se o número carregado corresponde ao passado; caso contrário, lançamos uma exceção. No nosso caso, como estamos passando uma não correspondência, uma exceção deve ser lançada.

Agora vamos compilar.

~/TON/build/crypto/func -APSR -o lottery-compiled.fif ~/TON/ton/crypto/smartcont/stdlib.fc ./lottery-code.fc 

Copie o código resultante para lottery-test-suite.fif, não esquecendo de substituir a última linha.

Verificamos se o teste foi aprovado:

~/TON/build/crypto/fift -s lottery-test-suite.fif

Aqui Você pode ver o commit correspondente com os resultados atuais.

Observe que é inconveniente copiar constantemente o código compilado de um contrato inteligente em um arquivo com testes, então vamos escrever um script que escreverá o código em uma constante para nós e simplesmente conectaremos o código compilado aos nossos testes usando "include".

Crie um arquivo na pasta do projeto build.sh com o seguinte conteúdo.

#!/bin/bash

~/TON/build/crypto/func -SPA -R -o lottery-compiled.fif ~/TON/ton/crypto/smartcont/stdlib.fc ./lottery-code.fc

Vamos torná-lo executável.

chmod +x ./build.sh

Agora é só executar nosso script para compilar o contrato. Mas além disso, precisamos escrevê-lo em uma constante code. Então vamos criar um novo arquivo lotter-compiled-for-test.fif, que incluiremos no arquivo lottery-test-suite.fif.

Vamos adicionar o código skirpt ao sh, que simplesmente duplicará o arquivo compilado em lotter-compiled-for-test.fif e altere a última linha nele.

# copy and change for test 
cp lottery-compiled.fif lottery-compiled-for-test.fif
sed '$d' lottery-compiled-for-test.fif > test.fif
rm lottery-compiled-for-test.fif
mv test.fif lottery-compiled-for-test.fif
echo -n "}END>s constant code" >> lottery-compiled-for-test.fif

Agora, para verificar, vamos executar o script resultante e um arquivo será gerado lottery-compiled-for-test.fif, que incluiremos em nosso lottery-test-suite.fif

В lottery-test-suite.fif exclua o código do contrato e adicione a linha "lottery-compiled-for-test.fif" include.

Fazemos testes para verificar se eles passam.

~/TON/build/crypto/fift -s lottery-test-suite.fif

Ótimo, agora para automatizar o lançamento dos testes, vamos criar um arquivo test.sh, que será executado primeiro build.she, em seguida, execute os testes.

touch test.sh
chmod +x test.sh

Nós escrevemos dentro

./build.sh 

echo "nCompilation completedn"

export FIFTPATH=~/TON/ton/crypto/fift/lib
~/TON/build/crypto/fift -s lottery-test-suite.fif

Nós fazemos a test.sh e execute-o para garantir que os testes funcionem.

chmod +x ./test.sh
./test.sh

Verificamos se o contrato é compilado e os testes são executados.

Ótimo, agora na inicialização test.sh Os testes serão compilados e executados imediatamente. Aqui está o link para comprometer-se.

Ok, antes de continuarmos, vamos fazer mais uma coisa por conveniência.

Vamos criar uma pasta build onde armazenaremos o contrato copiado e seu clone escrito em uma constante lottery-compiled.fif, lottery-compiled-for-test.fif. Vamos também criar uma pasta test onde o arquivo de teste será armazenado? lottery-test-suite.fif e potencialmente outros arquivos de suporte. Link para alterações relevantes.

Vamos continuar desenvolvendo o contrato inteligente.

A seguir deverá haver um teste que verifica se a mensagem foi recebida e se o contador é atualizado na loja quando enviamos o número correto. Mas faremos isso mais tarde.

Agora vamos pensar sobre qual estrutura de dados e quais dados precisam ser armazenados no contrato inteligente.

Descreverei tudo o que armazenamos.

`seqno` 32-х битное целое положительное число счетчик. 

`pubkey` 256-ти битное целое положительное число публичный ключ, с помощью которого, мы будем проверять подпись отправленного извне сообщения, о чем ниже. 

`order_seqno` 32-х битное целое положительное число хранит счетчик количества ставок. 

`number_of_wins` 32-х битное целое положительное число хранит  количество побед. 

`incoming_amount` тип данных Gram (первые 4 бита отвечает за длину), хранит общее количество грамов, которые были отправлены на контртакт. 

`outgoing_amount` общее количество грамов, которое было отправлено победителям. 

`owner_wc` номер воркчейна, 32-х битное (в некоторых местах написано, что 8-ми битное) целое число. В данный момент всего два -1 и 0. 

`owner_account_id` 256-ти битное целое положительное число, адрес контракта в текущем воркчейне. 

`orders` переменная типа словарь, хранит последние двадцать ставок. 

Em seguida você precisa escrever duas funções. Vamos ligar para o primeiro pack_state(), que empacotará os dados para salvamento posterior no armazenamento de contrato inteligente. Vamos ligar para o segundo unpack_state() irá ler e retornar dados do armazenamento.

_ pack_state(int seqno, int pubkey, int order_seqno, int number_of_wins, int incoming_amount, int outgoing_amount, int owner_wc, int owner_account_id, cell orders) inline_ref {
    return begin_cell()
            .store_uint(seqno, 32)
            .store_uint(pubkey, 256)
            .store_uint(order_seqno, 32)
            .store_uint(number_of_wins, 32)
            .store_grams(incoming_amount)
            .store_grams(outgoing_amount)
            .store_int(owner_wc, 32)
            .store_uint(owner_account_id, 256)
            .store_dict(orders)
            .end_cell();
}

_ unpack_state() inline_ref {
    var ds = begin_parse(get_data());
    var unpacked = (ds~load_uint(32), ds~load_uint(256), ds~load_uint(32), ds~load_uint(32), ds~load_grams(), ds~load_grams(), ds~load_int(32), ds~load_uint(256), ds~load_dict());
    ds.end_parse();
    return unpacked;
}

Adicionamos essas duas funções ao início do contrato inteligente. Vai dar certo tal resultado intermediário.

Para salvar dados, você precisará chamar a função integrada set_data() e ele gravará dados de pack_state() no armazenamento de contrato inteligente.

cell packed_state = pack_state(arg_1, .., arg_n); 
set_data(packed_state);

Agora que temos funções convenientes para escrever e ler dados, podemos seguir em frente.

Precisamos verificar se a mensagem vinda de fora é assinada pelo proprietário do contrato (ou outro usuário que tenha acesso à chave privada).

Quando publicamos um contrato inteligente, podemos inicializá-lo com os dados que precisamos no armazenamento, que serão salvos para uso futuro. Registraremos a chave pública lá para que possamos verificar se a mensagem recebida foi assinada com a chave privada correspondente.

Antes de continuar, vamos criar uma chave privada e gravá-la test/keys/owner.pk. Para fazer isso, vamos iniciar o Fift no modo interativo e executar quatro comandos.

`newkeypair` генерация публичного и приватного ключа и запись их в стек. 

`drop` удаления из стека верхнего элемента (в данном случае публичный ключ)  

`.s` просто посмотреть что лежит в стеке в данный момент 

`"owner.pk" B>file` запись приватного ключа в файл с именем `owner.pk`. 

`bye` завершает работу с Fift. 

Vamos criar uma pasta keys dentro da pasta test e escreva a chave privada lá.

mkdir test/keys
cd test/keys
~/TON/build/crypto/fift -i 
newkeypair
 ok
.s 
BYTES:128DB222CEB6CF5722021C3F21D4DF391CE6D5F70C874097E28D06FCE9FD6917 BYTES:DD0A81AAF5C07AAAA0C7772BB274E494E93BB0123AA1B29ECE7D42AE45184128 
drop 
 ok
"owner.pk" B>file
 ok
bye

Vemos um arquivo na pasta atual owner.pk.

Removemos a chave pública da pilha e quando necessário podemos obtê-la da privada.

Agora precisamos escrever uma verificação de assinatura. Vamos começar com o teste. Primeiro lemos a chave privada do arquivo usando a função file>B e escreva-o em uma variável owner_private_key, então usando a função priv>pub converta a chave privada em uma chave pública e escreva o resultado em owner_public_key.

variable owner_private_key
variable owner_public_key 

"./keys/owner.pk" file>B owner_private_key !
owner_private_key @ priv>pub owner_public_key !

Precisaremos de ambas as chaves.

Inicializamos o armazenamento do contrato inteligente com dados arbitrários na mesma sequência da função pack_state()e escreva-o em uma variável storage.

variable owner_private_key
variable owner_public_key 
variable orders
variable owner_wc
variable owner_account_id

"./keys/owner.pk" file>B owner_private_key !
owner_private_key @ priv>pub owner_public_key !
dictnew orders !
0 owner_wc !
0 owner_account_id !

<b 0 32 u, owner_public_key @ B, 0 32 u, 0 32 u, 0 Gram, 0 Gram, owner_wc @ 32 i, owner_account_id @ 256 u,  orders @ dict, b> storage !

A seguir, redigiremos uma mensagem assinada, ela conterá apenas a assinatura e o valor do contador.

Primeiro criamos os dados que queremos transmitir, depois assinamos com uma chave privada e por último geramos uma mensagem assinada.

variable message_to_sign
variable message_to_send
variable signature
<b 0 32 u, b> message_to_sign !
message_to_sign @ hashu owner_private_key @ ed25519_sign_uint signature !
<b signature @ B, 0 32 u, b> <s  message_to_send !  

Como resultado, a mensagem que enviaremos ao contrato inteligente é registrada em uma variável message_to_send, sobre funções hashu, ed25519_sign_uint você pode ler na documentação do Fift.

E para executar o teste chamamos novamente.

message_to_send @ 
recv_external 
code 
storage @
c7
runvmctx

Aqui tão O arquivo com testes deve ficar assim nesta fase.

Vamos fazer o teste e ele falhará, então vamos alterar o contrato inteligente para que ele possa receber mensagens neste formato e verificar a assinatura.

Primeiro contamos 512 bits da assinatura da mensagem e a escrevemos em uma variável, depois contamos 32 bits da variável do contador.

Como temos uma função de leitura de dados do armazenamento de contrato inteligente, iremos utilizá-la.

Em seguida é verificar o contador transferido com o armazenamento e verificar a assinatura. Se algo não corresponder, lançamos uma exceção com o código apropriado.

var signature = in_msg~load_bits(512);
var message = in_msg;
int msg_seqno = message~load_uint(32);
(int stored_seqno, int pubkey, int order_seqno, int number_of_wins, int incoming_amount, int outgoing_amount, int owner_wc, int owner_account_id, cell orders) = unpack_state();
throw_unless(33, msg_seqno == stored_seqno);
throw_unless(34, check_signature(slice_hash(in_msg), signature, pubkey));

Confirmação relevante aqui.

Vamos executar os testes e ver se o segundo teste falha. Por dois motivos, não há bits suficientes na mensagem e não há bits suficientes no armazenamento, então o código falha durante a análise. Precisamos adicionar uma assinatura à mensagem que estamos enviando e copiar o armazenamento do último teste.

No segundo teste, adicionaremos uma assinatura de mensagem e alteraremos o armazenamento do contrato inteligente. Aqui tão o arquivo com testes está como está no momento.

Vamos escrever um quarto teste, no qual enviaremos uma mensagem assinada com a chave privada de outra pessoa. Vamos criar outra chave privada e salvá-la em um arquivo not-owner.pk. Assinaremos a mensagem com esta chave privada. Vamos executar os testes e garantir que todos os testes sejam aprovados. Comprometer-se no momento

Agora podemos finalmente avançar para a implementação da lógica do contrato inteligente.
В recv_external() aceitaremos dois tipos de mensagens.

Como nosso contrato acumulará as perdas dos apostadores, esse dinheiro deverá ser repassado ao criador da loteria. O endereço da carteira do criador da loteria é registrado no armazenamento quando o contrato é criado.

Por precaução, precisamos alterar o endereço para o qual enviamos os gramas dos perdedores. Também deveríamos poder enviar gramas da loteria para o endereço do proprietário.

Vamos começar com o primeiro. Vamos primeiro escrever um teste que irá verificar se após o envio da mensagem o contrato inteligente salvou o novo endereço no armazenamento. Observe que na mensagem, além do contador e do novo endereço, também transmitimos action Um número inteiro não negativo de 7 bits, dependendo dele escolheremos como processar a mensagem no contrato inteligente.

<b 0 32 u, 1 @ 7 u, new_owner_wc @  32 i, new_owner_account_id @ 256 u, b> message_to_sign !

No teste você pode ver como o armazenamento do smartcontract é desserializado storage em Quinze. A desserialização de variáveis ​​é descrita na documentação do Fift.

Link de confirmação com adição de massa.

Vamos executar o teste e ter certeza de que ele falha. Agora vamos adicionar lógica para alterar o endereço do dono da loteria.

No contrato inteligente, continuamos a analisar message, leia em action. Lembramos que teremos dois action: alterar endereço e enviar gramas.

Em seguida, lemos o novo endereço do titular do contrato e o salvamos.
Executamos os testes e vemos que o terceiro teste falha. Ele trava devido ao fato de que o contrato agora analisa adicionalmente 7 bits da mensagem, que estão faltando no teste. Adicione um inexistente à mensagem action. Vamos fazer os testes e ver se tudo passa. Aqui comprometer-se com mudanças. Ótimo.

Agora vamos escrever a lógica para enviar o número especificado de gramas para o endereço salvo anteriormente.

Primeiro, vamos escrever um teste. Escreveremos dois testes, um quando não houver saldo suficiente e o segundo quando tudo passar com sucesso. Os testes podem ser visualizados neste commit.

Agora vamos adicionar o código. Primeiro, vamos escrever dois métodos auxiliares. O primeiro método get é descobrir o saldo atual de um contrato inteligente.

int balance() inline_ref method_id {
    return get_balance().pair_first();
}

E a segunda é para enviar gramas para outro contrato inteligente. Copiei completamente esse método de outro contrato inteligente.

() send_grams(int wc, int addr, int grams) impure {
    ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 011000
    cell msg = begin_cell()
    ;;  .store_uint(0, 1) ;; 0 <= format indicator int_msg_info$0 
    ;;  .store_uint(1, 1) ;; 1 <= ihr disabled
    ;;  .store_uint(1, 1) ;; 1 <= bounce = true
    ;;  .store_uint(0, 1) ;; 0 <= bounced = false
    ;;  .store_uint(4, 5)  ;; 00100 <= address flags, anycast = false, 8-bit workchain
        .store_uint (196, 9)
        .store_int(wc, 8)
        .store_uint(addr, 256)
        .store_grams(grams)
        .store_uint(0, 107) ;; 106 zeroes +  0 as an indicator that there is no cell with the data.
        .end_cell(); 
    send_raw_message(msg, 3); ;; mode, 2 for ignoring errors, 1 for sender pays fees, 64 for returning inbound message value
}

Vamos adicionar esses dois métodos ao contrato inteligente e escrever a lógica. Primeiro, analisamos o número de gramas da mensagem. A seguir verificamos o saldo, se não for suficiente lançamos uma exceção. Se estiver tudo bem, enviamos os gramas para o endereço salvo e atualizamos o contador.

int amount_to_send = message~load_grams();
throw_if(36, amount_to_send + 500000000 > balance());
accept_message();
send_grams(owner_wc, owner_account_id, amount_to_send);
set_data(pack_state(stored_seqno + 1, pubkey, order_seqno, number_of_wins, incoming_amount, outgoing_amount, owner_wc, owner_account_id, orders));

Aqui tão parece o contrato inteligente no momento. Vamos fazer os testes e garantir que eles passem.

A propósito, uma comissão é deduzida do contrato inteligente sempre que uma mensagem é processada. Para que as mensagens do contrato inteligente executem a solicitação, após as verificações básicas você precisa ligar accept_message().

Agora vamos passar para as mensagens internas. Na verdade, só aceitaremos gramas e devolveremos o dobro do valor ao jogador se ele ganhar e um terço ao proprietário se ele perder.

Primeiro, vamos escrever um teste simples. Para fazer isso, precisamos de um endereço de teste do contrato inteligente, do qual supostamente enviamos gramas para o contrato inteligente.

O endereço do contrato inteligente consiste em dois números, um número inteiro de 32 bits responsável pela cadeia de trabalho e um número de conta exclusivo inteiro não negativo de 256 bits nesta cadeia de trabalho. Por exemplo, -1 e 12345, este é o endereço que salvaremos em um arquivo.

Copiei a função para salvar o endereço de TonUtil.fif.

// ( wc addr fname -- )  Save address to file in 36-byte format
{ -rot 256 u>B swap 32 i>B B+ swap B>file } : save-address

Vejamos como a função funciona, isso dará uma compreensão de como o Fift funciona. Inicie o Fift no modo interativo.

~/TON/build/crypto/fift -i 

Primeiro colocamos -1, 12345 e o nome do futuro arquivo "sender.addr" na pilha:

-1 12345 "sender.addr" 

O próximo passo é executar a função -rot, que desloca a pilha de forma que no topo da pilha haja um número exclusivo de contrato inteligente:

"sender.addr" -1 12345

256 u>B converte um número inteiro não negativo de 256 bits em bytes.

"sender.addr" -1 BYTES:0000000000000000000000000000000000000000000000000000000000003039

swap troca os dois primeiros elementos da pilha.

"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039 -1

32 i>B converte um número inteiro de 32 bits em bytes.

"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039 BYTES:FFFFFFFF

B+ conecta duas sequências de bytes.

 "sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039FFFFFFFF

Novamente swap.

BYTES:0000000000000000000000000000000000000000000000000000000000003039FFFFFFFF "sender.addr" 

E finalmente os bytes são gravados no arquivo B>file. Depois disso, nossa pilha está vazia. Nós paramos Fift. Um arquivo foi criado na pasta atual sender.addr. Vamos mover o arquivo para a pasta criada test/addresses/.

Vamos escrever um teste simples que enviará gramas para um contrato inteligente. Aqui está o commit.

Agora vamos dar uma olhada na lógica da loteria.

A primeira coisa que fazemos é verificar a mensagem bounced ou não se bounced, então nós o ignoramos. bounced significa que o contrato retornará gramas se ocorrer algum erro. Não devolveremos gramas se ocorrer um erro repentino.

Verificamos se o saldo é inferior a meio grama, simplesmente aceitamos a mensagem e ignoramos.

A seguir, analisamos o endereço do contrato inteligente de onde veio a mensagem.

Lemos os dados do armazenamento e depois excluímos as apostas antigas do histórico se houver mais de vinte delas. Por conveniência, escrevi três funções adicionais pack_order(), unpack_order(), remove_old_orders().

A seguir verificamos se o saldo não é suficiente para o pagamento, então consideramos que não se trata de uma aposta, mas sim de uma reposição e guardamos a reposição em orders.

Finalmente, a essência do contrato inteligente.

Primeiramente, se o jogador perder, salvamos no histórico de apostas e se o valor for superior a 3 gramas, enviamos 1/3 para o dono do contrato inteligente.

Se o jogador ganhar, enviamos o dobro do valor para o endereço do jogador e depois salvamos as informações da aposta no histórico.

() recv_internal(int order_amount, cell in_msg_cell, slice in_msg) impure {
    var cs = in_msg_cell.begin_parse();
    int flags = cs~load_uint(4);  ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
    if (flags & 1) { ;; ignore bounced
        return ();
    }
    if (order_amount < 500000000) { ;; just receive grams without changing state 
        return ();
    }
    slice src_addr_slice = cs~load_msg_addr();
    (int src_wc, int src_addr) = parse_std_addr(src_addr_slice);
    (int stored_seqno, int pubkey, int order_seqno, int number_of_wins, int incoming_amount, int outgoing_amount, int owner_wc, int owner_account_id, cell orders) = unpack_state();
    orders = remove_old_orders(orders, order_seqno);
    if (balance() < 2 * order_amount + 500000000) { ;; not enough grams to pay the bet back, so this is re-fill
        builder order = pack_order(order_seqno, 1, now(), order_amount, src_wc, src_addr);
        orders~udict_set_builder(32, order_seqno, order);
        set_data(pack_state(stored_seqno, pubkey, order_seqno + 1, number_of_wins, incoming_amount + order_amount, outgoing_amount, owner_wc, owner_account_id, orders));
        return ();
    }
    if (rand(10) >= 4) {
        builder order = pack_order(order_seqno, 3, now(), order_amount, src_wc, src_addr);
        orders~udict_set_builder(32, order_seqno, order);
        set_data(pack_state(stored_seqno, pubkey, order_seqno + 1, number_of_wins, incoming_amount + order_amount, outgoing_amount, owner_wc, owner_account_id, orders));
        if (order_amount > 3000000000) {
            send_grams(owner_wc, owner_account_id, order_amount / 3);
        }
        return ();
    }
    send_grams(src_wc, src_addr, 2 * order_amount);
    builder order = pack_order(order_seqno, 2, now(), order_amount, src_wc, src_addr);
    orders~udict_set_builder(32, order_seqno, order);
    set_data(pack_state(stored_seqno, pubkey, order_seqno + 1, number_of_wins + 1, incoming_amount, outgoing_amount + 2 * order_amount, owner_wc, owner_account_id, orders));
}

É isso aí. Confirmação correspondente.

Agora tudo o que resta é simples, vamos criar métodos get para que possamos obter informações sobre o estado do contrato do mundo exterior (na verdade, ler os dados do armazenamento do contrato inteligente).

Vamos adicionar métodos get. Escreveremos abaixo sobre como receber informações sobre um contrato inteligente.

Também esqueci de adicionar o código que processará a primeira solicitação que ocorrer durante a publicação de um contrato inteligente. Confirmação correspondente... E mais fixo bug no envio de 1/3 do valor para a conta do proprietário.

A próxima etapa é publicar o contrato inteligente. Vamos criar uma pasta requests.

Tomei como base o código da publicação simple-wallet-code.fc который pode encontrar no repositório oficial.

Algo que vale a pena prestar atenção. Geramos um armazenamento de contrato inteligente e uma mensagem de entrada. Depois disso, é gerado o endereço do contrato inteligente, ou seja, o endereço é conhecido antes mesmo da publicação no TON. Em seguida, é necessário enviar vários gramas para este endereço, e só depois é necessário enviar um arquivo com o próprio contrato inteligente, já que a rede cobra uma comissão pelo armazenamento do contrato inteligente e das operações nele (validadores que armazenam e executam o smart contratos). O código pode ser visto aqui.

Em seguida, executamos o código de publicação e obtemos lottery-query.boc arquivo e endereço do contrato inteligente.

~/TON/build/crypto/fift -s requests/new-lottery.fif 0

Não esqueça de salvar os arquivos gerados: lottery-query.boc, lottery.addr, lottery.pk.

Entre outras coisas, veremos o endereço do contrato inteligente nos logs de execução.

new wallet address = 0:044910149dbeaf8eadbb2b28722e7d6a2dc6e264ec2f1d9bebd6fb209079bc2a 
(Saving address to file lottery.addr)
Non-bounceable address (for init): 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd
Bounceable address (for later access): kQAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8KpFY

Só por diversão, vamos fazer um pedido ao TON

$ ./lite-client/lite-client -C ton-lite-client-test1.config.json 
getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd

E veremos que a conta com este endereço está vazia.

account state is empty

Enviamos para o endereço 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd 2 Gram e após alguns segundos executamos o mesmo comando. Para enviar gramas eu uso carteira oficial, e você pode pedir a alguém do chat os gramas de teste, dos quais falarei no final do artigo.

> getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd

Parece um não inicializado (state:account_uninit) um contrato inteligente com o mesmo endereço e saldo de 1 nanogramas.

account state is (account
  addr:(addr_std
    anycast:nothing workchain_id:0 address:x044910149DBEAF8EADBB2B28722E7D6A2DC6E264EC2F1D9BEBD6FB209079BC2A)
  storage_stat:(storage_info
    used:(storage_used
      cells:(var_uint len:1 value:1)
      bits:(var_uint len:1 value:103)
      public_cells:(var_uint len:0 value:0)) last_paid:1583257959
    due_payment:nothing)
  storage:(account_storage last_trans_lt:3825478000002
    balance:(currencies
      grams:(nanograms
        amount:(var_uint len:4 value:2000000000))
      other:(extra_currencies
        dict:hme_empty))
    state:account_uninit))
x{C00044910149DBEAF8EADBB2B28722E7D6A2DC6E264EC2F1D9BEBD6FB209079BC2A20259C2F2F4CB3800000DEAC10776091DCD650004_}
last transaction lt = 3825478000001 hash = B043616AE016682699477FFF01E6E903878CDFD6846042BA1BFC64775E7AC6C4
account balance is 2000000000ng

Agora vamos publicar o contrato inteligente. Vamos lançar o lite-client e executar.

> sendfile lottery-query.boc
[ 1][t 2][1583008371.631410122][lite-client.cpp:966][!testnode] sending query from file lottery-query.boc
[ 3][t 1][1583008371.828550100][lite-client.cpp:976][!query]    external message status is 1 

Vamos verificar se o contrato foi publicado.

> last
> getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd

Entre outras coisas que conseguimos.

  storage:(account_storage last_trans_lt:3825499000002
    balance:(currencies
      grams:(nanograms
        amount:(var_uint len:4 value:1987150999))
      other:(extra_currencies
        dict:hme_empty))
    state:(account_active

Nós vemos que account_active.

Confirmação correspondente com alterações aqui.

Agora vamos criar solicitações para interagir com o contrato inteligente.

Mais precisamente, deixaremos o primeiro para alteração de endereço como um trabalho independente, e faremos o segundo para envio de gramas para o endereço do proprietário. Na verdade, precisaremos fazer o mesmo que no teste de envio de gramas.

Esta é a mensagem que enviaremos ao contrato inteligente, onde msg_seqno 165, action 2 e 9.5 gramas para envio.

<b 165 32 u, 2 7 u, 9500000000 Gram, b>

Não se esqueça de assinar a mensagem com sua chave privada lottery.pk, que foi gerado anteriormente durante a criação do contrato inteligente. Aqui está o commit correspondente.

Recebendo informações de um contrato inteligente usando métodos get

Agora vamos ver como executar métodos de obtenção de contrato inteligente.

Lançamos lite-client e execute os métodos get que escrevemos.

$ ./lite-client/lite-client -C ton-lite-client-test1.config.json
> runmethod 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd balance
arguments:  [ 104128 ] 
result:  [ 64633878952 ] 
...

В result contém o valor que a função retorna balance() do nosso contrato inteligente.
Faremos o mesmo para vários outros métodos.

> runmethod 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd get_seqno
...
arguments:  [ 77871 ] 
result:  [ 1 ] 

Vamos pedir seu histórico de apostas.

> runmethod 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd get_orders
...
arguments:  [ 67442 ] 
result:  [ ([0 1 1583258284 10000000000 0 74649920601963823558742197308127565167945016780694342660493511643532213172308] [1 3 1583258347 4000000000 0 74649920601963823558742197308127565167945016780694342660493511643532213172308] [2 1 1583259901 50000000000 0 74649920601963823558742197308127565167945016780694342660493511643532213172308]) ] 

Usaremos o lite-client e obteremos métodos para exibir informações sobre o contrato inteligente no site.

Exibindo dados de contrato inteligente no site

Escrevi um site simples em Python para exibir os dados do contrato inteligente de maneira conveniente. Aqui não vou me alongar sobre isso e vou publicar o site em um commit.

As solicitações à TON são feitas de Python via lite-client. Por conveniência, o site é empacotado em Docker e publicado no Google Cloud. Link.

Tentando

Agora vamos tentar enviar gramas para lá para reposição de carteira. Enviaremos 40 gramas. E vamos fazer algumas apostas para maior clareza. Vemos que o site mostra o histórico de apostas, a porcentagem atual de vitórias e outras informações úteis.

Nós vemosque ganhamos o primeiro, perdemos o segundo.

Posfácio

O artigo acabou sendo muito mais longo do que eu esperava, talvez pudesse ter sido mais curto, ou talvez apenas para uma pessoa que não sabe nada sobre TON e deseja escrever e publicar um contrato inteligente não tão simples com a capacidade de interagir com isto. Talvez algumas coisas pudessem ter sido explicadas de forma mais simples.

Talvez alguns aspectos da implementação pudessem ter sido feitos de forma mais eficiente e elegante, mas teria levado ainda mais tempo para preparar o artigo. Também é possível que eu tenha cometido um erro em algum lugar ou não tenha entendido alguma coisa, então se você está fazendo algo sério, precisa contar com a documentação oficial ou com o repositório oficial com o código TON.

Deve-se notar que como o próprio TON ainda está em estágio ativo de desenvolvimento, podem ocorrer alterações que quebrarão qualquer uma das etapas deste artigo (o que aconteceu enquanto eu estava escrevendo, já foi corrigido), mas a abordagem geral é dificilmente mudará.

Não vou falar sobre o futuro da TON. Talvez a plataforma se torne algo grande e devamos gastar tempo estudando-a e preencher um nicho com nossos produtos agora.

Há também a Libra do Facebook, que tem um público potencial de usuários maior que o TON. Não sei quase nada sobre Libra, a julgar pelo fórum há muito mais atividade lá do que na comunidade TON. Embora os desenvolvedores e a comunidade do TON sejam mais underground, o que também é legal.

referências

  1. Documentação oficial da TON: https://test.ton.org
  2. Repositório oficial TON: https://github.com/ton-blockchain/ton
  3. Carteira oficial para diferentes plataformas: https://wallet.ton.org
  4. Repositório de contratos inteligentes deste artigo: https://github.com/raiym/astonished
  5. Link para o site do contrato inteligente: https://ton-lottery.appspot.com
  6. Repositório para a extensão do Visual Studio Code for FunC: https://github.com/raiym/func-visual-studio-plugin
  7. Converse sobre TON no Telegram, o que realmente ajudou a descobrir no estágio inicial. Acho que não seria um erro dizer que todos que escreveram algo para a TON estão lá. Você também pode pedir gramas de teste lá. https://t.me/tondev_ru
  8. Outro bate-papo sobre TON onde encontrei informações úteis: https://t.me/TONgramDev
  9. Primeira fase da competição: https://contest.com/blockchain
  10. Segunda fase da competição: https://contest.com/blockchain-2

Fonte: habr.com

Compre hospedagem confiável para sites com proteção DDoS, servidores VPS VDS 🔥 Compre hospedagem de sites confiável com proteção contra DDoS, servidores VPS/VDS | ProHoster