Sobre como escribir e publicar un contrato intelixente na rede aberta de Telegram (TON)

Sobre como escribir e publicar un contrato intelixente en TON

De que trata este artigo?

No artigo falarei de como participei no primeiro (dos dous) concurso blockchain de Telegram, non levei ningún premio e decidín deixar constancia da miña experiencia nun artigo para que non se afunda no esquecemento e, quizais, axude. alguén.

Como non quería escribir código abstracto, senón facer algo que funcione, para o artigo escribín un contrato intelixente para unha lotería instantánea e un sitio web que mostra datos de contrato intelixente directamente desde TON sen usar almacenamento intermedio.

O artigo será útil para aqueles que queiran facer o seu primeiro contrato intelixente en TON, pero non saben por onde comezar.

Usando a lotería como exemplo, pasarei de instalar o entorno a publicar un contrato intelixente, interactuar con el e escribir un sitio web para recibir e publicar datos.

Sobre a participación no concurso

O pasado outubro, Telegram anunciou un concurso de blockchain con novos idiomas Fift и FunC. Era necesario escoller entre escribir calquera dos cinco contratos intelixentes propostos. Pensei que sería bo facer algo diferente, aprender un idioma e facer algo, aínda que non teña que escribir nada máis no futuro. Ademais, o tema está constantemente nos beizos.

Paga a pena dicir que non tiña experiencia no desenvolvemento de contratos intelixentes.

Pensei participar ata o final ata que puidese e despois escribir un artigo de revisión, pero fallei de inmediato no primeiro. eu escribiu unha carteira con multisinatura activada FunC e en xeral funcionou. Tomeino como base contrato intelixente en Solidity.

Nese momento, pensei que iso era definitivamente suficiente para ocupar polo menos algún lugar no premio. Como resultado, uns 40 de cada 60 participantes convertéronse en premiados e eu non estaba entre eles. En xeral, non hai nada de malo con isto, pero unha cousa me molestou. No momento do anuncio dos resultados non se fixera a revisión da proba do meu contrato, preguntei aos participantes no chat se había alguén máis que non o tiña, non había ningún.

Ao parecer, prestando atención ás miñas mensaxes, dous días despois os xuíces publicaron un comentario e aínda non entendo se perderon accidentalmente o meu contrato intelixente durante o xuízo ou simplemente pensaron que era tan malo que non precisaba un comentario. Fixen unha pregunta na páxina, pero non recibín resposta. Aínda que non é ningún segredo quen xulgou, considerei innecesario escribir mensaxes persoais.

Dedicouse moito tempo á comprensión, polo que se decidiu escribir un artigo. Dado que aínda non hai moita información, este artigo axudará a aforrar tempo para todos os interesados.

O concepto de contratos intelixentes en TON

Antes de escribir nada, cómpre descubrir de que lado abordar esta cousa. Polo tanto, agora vouche dicir de que partes está composto o sistema. Máis precisamente, que partes debes saber para redactar polo menos algún tipo de contrato laboral.

Centrarémonos en escribir un contrato intelixente e traballar con el TON Virtual Machine (TVM), Fift и FunC, polo que o artigo é máis ben unha descrición do desenvolvemento dun programa regular. Non nos detendremos en como funciona a propia plataforma aquí.

En xeral sobre como funciona TVM e lingua Fift hai boa documentación oficial. Mentres participaba no concurso e agora mentres escribía o contrato actual, recorrei moitas veces a ela.

O idioma principal no que se escriben os contratos intelixentes é FunC. Non hai documentación neste momento, polo que para escribir algo cómpre estudar exemplos de contratos intelixentes do repositorio oficial e a implementación da propia linguaxe alí, ademais podes ver exemplos de contratos intelixentes dos dous últimos. competicións. Ligazóns ao final do artigo.

Digamos que xa escribimos un contrato intelixente para FunC, despois compilamos o código no ensamblador Fift.

O contrato intelixente compilado aínda está por publicar. Para iso, debes escribir unha función Fift, que tomará o código do contrato intelixente e algúns outros parámetros como entrada, e a saída será un ficheiro coa extensión .boc (que significa "bolsa de celas"), e, dependendo de como o escribamos, unha clave privada e un enderezo, que se xera a partir do código do contrato intelixente. Xa podes enviar gramos ao enderezo dun contrato intelixente que aínda non se publicou.

Para publicar un contrato intelixente en TON recibido .boc o ficheiro terá que enviarse á cadea de bloques usando un cliente lixeiro (máis sobre iso a continuación). Pero antes de publicar, cómpre transferir gramos ao enderezo xerado, se non, o contrato intelixente non se publicará. Despois da publicación, pode interactuar co contrato intelixente enviándolle mensaxes desde fóra (por exemplo, mediante un cliente lixeiro) ou desde dentro (por exemplo, un contrato intelixente envía unha mensaxe a outro dentro de TON).

Unha vez que entendemos como se publica o código, faise máis fácil. Aproximadamente sabemos o que queremos escribir e como funcionará o noso programa. E mentres escribimos, buscamos como isto xa está implementado nos contratos intelixentes existentes ou analizamos o código de implementación Fift и FunC no repositorio oficial, ou busque na documentación oficial.

Moitas veces buscaba palabras clave no chat de Telegram onde se reunían todos os participantes da competición e os empregados de Telegram, e ocorreu que durante a competición todos se reunían alí e comezaron a falar de Fift e FunC. Ligazón ao final do artigo.

É o momento de pasar da teoría á práctica.

Preparando o ambiente para traballar con TON

Fixen todo o que se describirá no artigo sobre MacOS e verifiqueino dúas veces en Ubuntu 18.04 LTS limpo en Docker.

O primeiro que cómpre facer é descargar e instalar lite-client co que pode enviar solicitudes a TON.

As instrucións do sitio web oficial describen o proceso de instalación con bastante detalle e claridade e omiten algúns detalles. Aquí seguimos as instrucións, instalando as dependencias que faltan ao longo do camiño. Eu non compilei cada proxecto e instalei dende o repositorio oficial de Ubuntu (en MacOS que 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 

Unha vez instaladas todas as dependencias, podes instalar lite-client, Fift, FunC.

En primeiro lugar, clonamos o repositorio TON xunto coas súas dependencias. Por comodidade, faremos todo nun cartafol ~/TON.

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

O repositorio tamén almacena implementacións Fift и FunC.

Agora estamos preparados para montar o proxecto. O código do repositorio está clonado nun cartafol ~/TON/ton. En ~/TON crear un cartafol build e recolle o proxecto nel.

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

Xa que imos escribir un contrato intelixente, non só necesitamos lite-clientPero Fift с FunC, entón imos compilar todo. Non é un proceso rápido, así que estamos á espera.

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

A continuación, descarga o ficheiro de configuración que contén datos sobre o nodo ao que lite-client conectarase.

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

Facendo as primeiras solicitudes a TON

Agora imos lanzar lite-client.

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

Se a compilación foi exitosa, despois do lanzamento verá un rexistro da conexión do cliente lixeiro ao nodo.

[ 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)
...

Pode executar o comando help e ver que comandos están dispoñibles.

help

Imos enumerar 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 preparados para escribir o propio contrato.

Implantación

Idea

Como escribín arriba, o contrato intelixente que estamos a escribir é unha lotería.

Ademais, non se trata dun sorteo no que hai que comprar un boleto e esperar unha hora, día ou mes, senón un instantáneo no que o usuario se traslada ao enderezo do contrato. N gramos e recuperalo ao instante 2 * N gramos ou perde. Faremos que a probabilidade de gañar un 40%. Se non hai gramos suficientes para o pago, consideraremos a transacción como unha recarga.

Ademais, é importante que as apostas se poidan ver en tempo real e dunha forma cómoda, para que o usuario poida comprender inmediatamente se gañou ou perdeu. Polo tanto, cómpre crear un sitio web que mostrará apostas e resultados directamente de TON.

Escribir un contrato intelixente

Para comodidade, destaquei o código para FunC; o complemento pódese atopar e instalar na busca de Visual Studio Code; se de súpeto queres engadir algo, puxen o complemento dispoñible públicamente. Ademais, alguén fixo previamente un complemento para traballar con Fift, tamén podes instalalo e atopalo en VSC.

Imos crear inmediatamente un repositorio onde enviaremos os resultados intermedios.

Para facilitarnos a vida, escribiremos un contrato intelixente e probalo localmente ata que estea listo. Só despois diso o publicaremos en TON.

O contrato intelixente ten dous métodos externos aos que se pode acceder. Primeira, recv_external() esta función execútase cando unha solicitude ao contrato vén do mundo exterior, é dicir, non de TON, por exemplo, cando nós mesmos xeramos unha mensaxe e a enviamos a través do lite-client. Segundo, recv_internal() é cando, dentro da propia TON, calquera contrato fai referencia ao noso. En ambos os casos, pode pasar parámetros á función.

Comecemos cun exemplo sinxelo que funcionará se se publica, pero non hai carga funcional nel.

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

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

Aquí temos que explicar o que é slice. Todos os datos almacenados en TON Blockchain son unha colección TVM cell ou simplemente cell, nesta cela pode almacenar ata 1023 bits de datos e ata 4 ligazóns a outras celas.

TVM cell slice ou slice esta forma parte da existente cell úsase para analizalo, quedará claro máis tarde. O principal para nós é que podemos transferir slice e, dependendo do tipo de mensaxe, procesar os datos en recv_external() ou recv_internal().

impure — unha palabra clave que indica que a función modifica os datos do contrato intelixente.

Gardamos o código do contrato 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 pódese ver mediante o comando

~/TON/build/crypto/func -help

Compilamos o código ensamblador de Fift en 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

Pódese poñer en marcha localmente, para iso prepararemos o medio ambiente.

Teña en conta que a primeira liña conecta Asm.fif, este é un código escrito en Fift para o ensamblador Fift.

Xa que queremos executar e probar o contrato intelixente localmente, crearemos un ficheiro lottery-test-suite.fif e copie alí o código compilado, substituíndo a última liña nel, que escribe o código do contrato intelixente nunha constante codepara transferilo á 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

Polo de agora parece claro, agora imos engadir ao mesmo ficheiro o código que usaremos para lanzar 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 rexistramos o contexto, é dicir, os datos cos que se lanzará o TVM (ou estado da rede). Mesmo durante a competición, un dos desenvolvedores mostrou como crear c7 e copieino. Neste artigo é posible que teñamos que cambiar rand_seed xa que diso depende a xeración dun número aleatorio e se non se modifica, devolverase o mesmo número cada vez.

recv_internal и recv_external as constantes con valores 0 e -1 serán as encargadas de chamar ás funcións correspondentes no contrato intelixente.

Agora estamos preparados para crear a primeira proba para o noso contrato intelixente baleiro. Para máis claridade, polo momento engadiremos todas as probas ao mesmo ficheiro lottery-test-suite.fif.

Imos crear unha variable storage e escribe nel un baleiro cell, este será o almacenamento do contrato intelixente.

message Esta é a mensaxe que transmitiremos ao contacto intelixente desde o exterior. Tamén o deixaremos baleiro polo momento.

variable storage 
<b b> storage ! 

variable message 
<b b> message ! 

Despois de preparar as constantes e variables, iniciamos TVM mediante o comando runvmctx e pase os parámetros creados á entrada.

message @ 
recv_external 
code 
storage @ 
c7 
runvmctx 

Ao final teremos éxito así código intermedio 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 debería executarse sen erros e na saída veremos o rexistro de execución:

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

Xenial, escribimos a primeira versión funcional do contrato intelixente.

Agora necesitamos engadir funcionalidades. Primeiro imos tratar as mensaxes que veñen do mundo exterior recv_external()

O propio desenvolvedor escolle o formato de mensaxe que pode aceptar o contrato.

Pero normalmente

  • en primeiro lugar, queremos protexer o noso contrato do mundo exterior e facelo de xeito que só o propietario do contrato poida enviarlle mensaxes externas.
  • en segundo lugar, cando enviamos unha mensaxe válida a TON, queremos que isto suceda exactamente unha vez e cando enviamos a mesma mensaxe de novo, o contrato intelixente rexéitaa.

Polo tanto, case todos os contratos resolven estes dous problemas, xa que o noso contrato acepta mensaxes externas, tamén temos que coidar diso.

Farémolo en orde inversa. En primeiro lugar, imos resolver o problema coa repetición; se o contrato xa recibiu tal mensaxe e a procesou, non a executará unha segunda vez. E entón resolveremos o problema para que só un determinado círculo de persoas poida enviar mensaxes ao contrato intelixente.

Hai diferentes formas de resolver o problema coas mensaxes duplicadas. Velaquí como o faremos. No contrato intelixente, inicializamos o contador de mensaxes recibidas co valor inicial 0. En cada mensaxe ao contrato intelixente, engadiremos o valor do contador actual. Se o valor do contador da mensaxe non coincide co valor do contrato intelixente, non o procesamos; se o fai, procesámolo e aumentamos o contador do contrato intelixente en 1.

Volvamos a lottery-test-suite.fif e engadirlle unha segunda proba. Se enviamos un número incorrecto, o código debería xerar unha excepción. Por exemplo, deixe que os datos do contrato almacene 166 e enviarémoslle 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"

Imos lanzar.

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

E veremos que a proba se executa cun 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

Nesta fase lottery-test-suite.fif debería parecer по ссылке.

Agora imos engadir a lóxica do contador ao contrato intelixente 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 mensaxe que enviamos.

O primeiro que facemos é comprobar se a mensaxe contén datos, se non, simplemente saímos.

A continuación analizamos a mensaxe. in_msg~load_uint(32) carga o número 165, de 32 bits unsigned int a partir da mensaxe transmitida.

A continuación cargamos 32 bits do almacenamento do contrato intelixente. Comprobamos que o número cargado coincide co aprobado; se non, lanzamos unha excepción. No noso caso, xa que estamos pasando un non partido, debería lanzarse unha excepción.

Agora imos compilar.

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

Copia o código resultante en lottery-test-suite.fif, sen esquecer substituír a última liña.

Comprobamos que a proba pasa:

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

Aquí Podes ver o commit correspondente cos resultados actuais.

Teña en conta que é inconveniente copiar constantemente o código compilado dun contrato intelixente nun ficheiro con probas, polo que escribiremos un script que escribirá o código nunha constante para nós e simplemente conectaremos o código compilado ás nosas probas usando "include".

Crea un ficheiro no cartafol do proxecto build.sh co seguinte contido.

#!/bin/bash

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

Imos facelo executable.

chmod +x ./build.sh

Agora só tes que executar o noso script para compilar o contrato. Pero ademais disto, necesitamos escribilo nunha constante code. Entón, crearemos un novo ficheiro lotter-compiled-for-test.fif, que incluiremos no ficheiro lottery-test-suite.fif.

Engademos código skirpt a sh, que simplemente duplicará o ficheiro compilado lotter-compiled-for-test.fif e cambia a última liña nel.

# 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 comprobar, imos executar o script resultante e xerarase un ficheiro lottery-compiled-for-test.fif, que incluiremos no noso lottery-test-suite.fif

В lottery-test-suite.fif eliminar o código do contrato e engadir a liña "lottery-compiled-for-test.fif" include.

Facemos probas para comprobar que pasan.

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

Xenial, agora para automatizar o lanzamento de probas, imos crear un ficheiro test.sh, que se executará primeiro build.sh, e despois executa as probas.

touch test.sh
chmod +x test.sh

Escribimos dentro

./build.sh 

echo "nCompilation completedn"

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

Fagámolo test.sh e execútao para asegurarse de que as probas funcionan.

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

Comprobamos que se compila o contrato e que se executan as probas.

Xenial, agora no inicio test.sh As probas compilaranse e executaranse inmediatamente. Aquí tedes a ligazón a comprometerse.

Vale, antes de continuar, imos facer unha cousa máis por comodidade.

Imos crear un cartafol build onde almacenaremos o contrato copiado e o seu clon escrito nunha constante lottery-compiled.fif, lottery-compiled-for-test.fif. Imos tamén crear un cartafol test onde se almacenará o ficheiro de proba? lottery-test-suite.fif e, potencialmente, outros ficheiros de apoio. Ligazón aos cambios relevantes.

Sigamos desenvolvendo o contrato intelixente.

A continuación debería haber unha proba que comprobe que se recibe a mensaxe e o contador está actualizado na tenda cando enviamos o número correcto. Pero iso farémolo máis tarde.

Agora pensemos en que estrutura de datos e que datos hai que almacenar no contrato intelixente.

Vou describir todo o que almacenamos.

`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` переменная типа словарь, хранит последние двадцать ставок. 

A continuación cómpre escribir dúas funcións. Chamemos ao primeiro pack_state(), que empaquetará os datos para o seu posterior aforro no almacenamento do contrato intelixente. Chamemos ao segundo unpack_state() lerá e devolverá os datos do almacenamento.

_ 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;
}

Engadimos estas dúas funcións ao comezo do contrato intelixente. Funcionará así resultado intermedio.

Para gardar datos, terá que chamar á función integrada set_data() e escribirá datos desde pack_state() no almacenamento de contrato intelixente.

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

Agora que temos funcións convenientes para escribir e ler datos, podemos seguir adiante.

Debemos comprobar que a mensaxe que chega dende fóra está asinada polo propietario do contrato (ou outro usuario que teña acceso á clave privada).

Cando publicamos un contrato intelixente, podemos inicializalo cos datos que necesitamos no almacenamento, que se gardarán para o seu uso futuro. Alí gravaremos a chave pública para poder comprobar que a mensaxe entrante foi asinada coa clave privada correspondente.

Antes de continuar, creemos unha chave privada e escribimos test/keys/owner.pk. Para iso, lancemos Fift en modo interactivo e executemos catro comandos.

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

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

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

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

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

Imos crear un cartafol keys dentro do cartafol test e escriba alí a chave privada.

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 un ficheiro no cartafol actual owner.pk.

Eliminamos a chave pública da pila e cando sexa necesario podemos obtela da privada.

Agora necesitamos escribir unha verificación de sinatura. Imos comezar coa proba. Primeiro lemos a clave privada do ficheiro usando a función file>B e escríbeo nunha variable owner_private_key, e despois usando a función priv>pub converta a clave privada nunha clave pública e escriba o resultado 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 !

Necesitaremos as dúas chaves.

Inicializamos o almacenamento do contrato intelixente con datos arbitrarios na mesma secuencia que na función pack_state()e escríbeo nunha variable 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 continuación, redactaremos unha mensaxe asinada, só conterá a sinatura e o valor do contador.

Primeiro, creamos os datos que queremos transmitir, despois asinámolo cunha clave privada e finalmente xeramos unha mensaxe asinada.

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 mensaxe que enviaremos ao contrato intelixente rexístrase nunha variable message_to_send, sobre funcións hashu, ed25519_sign_uint podes ler na documentación do Quinto.

E para realizar a proba volvemos chamar.

message_to_send @ 
recv_external 
code 
storage @
c7
runvmctx

Entón O ficheiro con probas debería verse así nesta fase.

Imos realizar a proba e fallará, polo que cambiaremos o contrato intelixente para que reciba mensaxes deste formato e verifique a sinatura.

En primeiro lugar, contamos 512 bits da sinatura da mensaxe e escribimos nunha variable, despois contamos 32 bits da variable contador.

Dado que temos unha función para ler datos do almacenamento do contrato intelixente, utilizaremos.

A continuación é comprobar o contador transferido co almacenamento e comprobar a sinatura. Se algo non coincide, lanzamos unha excepción co código axeitado.

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));

Compromiso relevante aquí.

Imos realizar as probas e ver que a segunda proba falla. Por dous motivos, non hai suficientes bits na mensaxe e non hai suficientes bits no almacenamento, polo que o código falla ao analizar. Necesitamos engadir unha sinatura á mensaxe que estamos enviando e copiar o almacenamento da última proba.

Na segunda proba, engadiremos unha sinatura de mensaxe e cambiaremos o almacenamento do contrato intelixente. Entón o ficheiro con probas parece neste momento.

Escribamos unha cuarta proba, na que enviaremos unha mensaxe asinada coa chave privada doutra persoa. Imos crear outra clave privada e gardala nun ficheiro not-owner.pk. Asinaremos a mensaxe con esta chave privada. Imos realizar as probas e asegúrese de que todas as aproben. Comprometerse neste momento.

Agora por fin podemos pasar á implementación da lóxica do contrato intelixente.
В recv_external() aceptaremos dous tipos de mensaxes.

Dado que o noso contrato acumulará as perdas dos xogadores, este diñeiro debe ser transferido ao creador da lotería. O enderezo da carteira do creador da lotería rexístrase no almacenamento cando se crea o contrato.

Por se acaso, necesitamos a posibilidade de cambiar o enderezo ao que enviamos os gramos dos perdedores. Tamén deberíamos poder enviar gramos da lotería ao enderezo do propietario.

Comecemos polo primeiro. Escribamos primeiro unha proba que comprobará que despois de enviar a mensaxe, o contrato intelixente gardou o novo enderezo no almacenamento. Teña en conta que na mensaxe, ademais do mostrador e do novo enderezo, tamén transmitimos action Un número enteiro de 7 bits non negativo, dependendo del, escolleremos como procesar a mensaxe no contrato intelixente.

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

Na proba podes ver como se deserializa o almacenamento smartcontract storage en Quinto. A deserialización de variables descríbese na documentación de Fift.

Ligazón de compromiso con masa engadida.

Imos realizar a proba e asegúrese de que falla. Agora imos engadir lóxica para cambiar o enderezo do propietario da lotería.

No contrato intelixente seguimos analizando message, ler action. Lembrámosvos que teremos dous action: cambia de enderezo e envía gramos.

Despois lemos o novo enderezo do propietario do contrato e gardámolo no almacenamento.
Realizamos as probas e vemos que a terceira proba falla. Falla debido ao feito de que o contrato agora analiza adicionalmente 7 bits da mensaxe, que faltan na proba. Engade un inexistente á mensaxe action. Imos facer as probas e ver que todo pasa. Aquí comprometerse con cambios. Genial.

Agora imos escribir a lóxica para enviar o número especificado de gramos ao enderezo gardado anteriormente.

Primeiro, imos escribir unha proba. Escribiremos dúas probas, unha cando non haxa o equilibrio suficiente, a segunda cando todo debe pasar con éxito. As probas pódense ver neste compromiso.

Agora imos engadir o código. En primeiro lugar, imos escribir dous métodos auxiliares. O primeiro método de obtención é descubrir o saldo actual dun contrato intelixente.

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

E o segundo é para enviar gramos a outro contrato intelixente. Copiei completamente este método doutro contrato intelixente.

() 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
}

Engademos estes dous métodos ao contrato intelixente e escribamos a lóxica. En primeiro lugar, analizamos o número de gramos da mensaxe. A continuación comprobamos o saldo, se non é suficiente botamos unha excepción. Se todo está ben, enviamos os gramos ao enderezo gardado e actualizamos 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));

Entón parece o contrato intelixente neste momento. Imos facer as probas e asegurarnos de que aproban.

Por certo, cada vez que se procesa unha mensaxe dedúcese unha comisión do contrato intelixente. Para que as mensaxes de contrato intelixente executen a solicitude, despois das comprobacións básicas debes chamar accept_message().

Agora imos pasar ás mensaxes internas. De feito, só aceptaremos gramos e devolveremos o dobre da cantidade ao xogador se gaña e un terzo ao propietario se perde.

En primeiro lugar, imos escribir unha proba sinxela. Para iso, necesitamos un enderezo de proba do contrato intelixente desde o que supostamente enviamos gramos ao contrato intelixente.

O enderezo do contrato intelixente consta de dous números, un número enteiro de 32 bits responsable da cadea de traballo e un número de conta único enteiro non negativo de 256 bits nesta cadea de traballo. Por exemplo, -1 e 12345, este é o enderezo que gardaremos nun ficheiro.

Copiei a función para gardar o enderezo 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

Vexamos como funciona a función, isto dará unha comprensión de como funciona Fift. Inicia Fift en modo interactivo.

~/TON/build/crypto/fift -i 

Primeiro introducimos -1, 12345 e o nome do ficheiro futuro "sender.addr" na pila:

-1 12345 "sender.addr" 

O seguinte paso é executar a función -rot, que cambia a pila de tal xeito que na parte superior da pila hai un número de contrato intelixente único:

"sender.addr" -1 12345

256 u>B converte un enteiro non negativo de 256 bits en bytes.

"sender.addr" -1 BYTES:0000000000000000000000000000000000000000000000000000000000003039

swap intercambia os dous elementos superiores da pila.

"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039 -1

32 i>B converte un enteiro de 32 bits en bytes.

"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039 BYTES:FFFFFFFF

B+ conecta dúas secuencias de bytes.

 "sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039FFFFFFFF

De novo swap.

BYTES:0000000000000000000000000000000000000000000000000000000000003039FFFFFFFF "sender.addr" 

E finalmente os bytes escríbense no ficheiro B>file. Despois diso, a nosa pila está baleira. Paramos Fift. Creouse un ficheiro no cartafol actual sender.addr. Movemos o ficheiro ao cartafol creado test/addresses/.

Escribamos unha proba sinxela que enviará gramos a un contrato intelixente. Aquí está o compromiso.

Vexamos agora a lóxica da lotería.

O primeiro que facemos é comprobar a mensaxe bounced ou non se bounced, entón ignorámolo. bounced significa que o contrato devolverá gramos se se produce algún erro. Non devolveremos gramos se se produce un erro de súpeto.

Comprobamos, se o saldo é inferior a medio gramo, entón simplemente aceptamos a mensaxe e ignorámola.

A continuación, analizamos o enderezo do contrato intelixente do que procede a mensaxe.

Lemos os datos do almacenamento e despois eliminamos do historial as apostas antigas se hai máis de vinte. Para comodidade, escribín tres funcións adicionais pack_order(), unpack_order(), remove_old_orders().

A continuación, miramos se o saldo non é suficiente para o pago, entón consideramos que esta non é unha aposta, senón unha reposición e gardamos a reposición en orders.

Despois, finalmente, a esencia do contrato intelixente.

En primeiro lugar, se o xogador perde, gardámolo no historial de apostas e se a cantidade supera os 3 gramos, enviámoslle 1/3 ao propietario do contrato intelixente.

Se o xogador gaña, enviamos o dobre da cantidade ao enderezo do xogador e despois gardamos a información sobre a aposta no historial.

() 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));
}

É iso aí. Commit correspondente.

Agora todo o que queda é simple, imos crear métodos de obtención para que poidamos obter información sobre o estado do contrato do mundo exterior (de feito, ler os datos do almacenamento do seu contrato intelixente).

Engadimos métodos get. A continuación escribiremos sobre como recibir información sobre un contrato intelixente.

Tamén esquecín engadir o código que procesará a primeira solicitude que se produza ao publicar un contrato intelixente. Commit correspondente. E máis aló corrixido erro ao enviar 1/3 do importe á conta do propietario.

O seguinte paso é publicar o contrato intelixente. Imos crear un cartafol requests.

Tomei o código de publicación como base simple-wallet-code.fc que pode atopar no repositorio oficial.

Algo ao que vale a pena prestarlle atención. Xeramos un almacenamento de contrato intelixente e unha mensaxe de entrada. Despois diso, xérase o enderezo do contrato intelixente, é dicir, o enderezo coñécese mesmo antes da publicación en TON. A continuación, cómpre enviar varios gramos a este enderezo, e só despois diso cómpre enviar un ficheiro co propio contrato intelixente, xa que a rede cobra unha comisión por almacenar o contrato intelixente e as operacións nel (validadores que almacenan e executan smart contract). contratos). O código pódese ver aquí.

A continuación executamos o código de publicación e obtemos lottery-query.boc ficheiro e enderezo do contrato intelixente.

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

Non esquezas gardar os ficheiros xerados: lottery-query.boc, lottery.addr, lottery.pk.

Entre outras cousas, veremos o enderezo do contrato intelixente nos rexistros de execución.

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 diversión, imos facer unha solicitude a TON

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

E veremos que a conta con este enderezo está baleira.

account state is empty

Enviamos ao enderezo 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd 2 gramos e despois duns segundos executamos o mesmo comando. Para enviar gramos uso carteira oficial, e podes pedirlle a alguén do chat gramos de proba, dos que falarei ao final do artigo.

> getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd

Parece un non inicializado (state:account_uninit) un contrato intelixente co mesmo enderezo e un saldo de 1 de nanogramos.

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 imos publicar o contrato intelixente. Lancemos lite-client e executemos.

> 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 

Comprobamos que o contrato foi publicado.

> last
> getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd

Entre outras cousas 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

Vemos iso account_active.

Compromiso correspondente con cambios aquí.

Agora imos crear solicitudes para interactuar co contrato intelixente.

Máis precisamente, deixaremos o primeiro para cambiar o enderezo como obra independente, e o segundo faremos para enviar gramos ao enderezo do propietario. De feito, teremos que facer o mesmo que na proba de envío de gramos.

Esta é a mensaxe que enviaremos ao contrato intelixente, onde msg_seqno 165, action 2 e 9.5 gramos para o envío.

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

Non esquezas asinar a mensaxe coa túa clave privada lottery.pk, que se xerou anteriormente ao crear o contrato intelixente. Aquí está o compromiso correspondente.

Recibir información dun contrato intelixente mediante métodos get

Agora vexamos como executar os métodos de obtención de contratos intelixentes.

Lanzamento lite-client e executa os métodos get que escribimos.

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

В result contén o valor que devolve a función balance() do noso contrato intelixente.
Faremos o mesmo con varios métodos máis.

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

Solicitamos o teu historial 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 lite-client e obteremos métodos para mostrar información sobre o contrato intelixente no sitio.

Mostrando datos de contrato intelixente no sitio web

Escribín un sitio web sinxelo en Python para mostrar os datos do contrato intelixente dun xeito cómodo. Aquí non vou determe en detalle e publicarei o sitio nun compromiso.

As solicitudes a TON realízanse desde Python coa axuda lite-client. Para maior comodidade, o sitio está empaquetado en Docker e publicado en Google Cloud. Ligazón.

Intentando

Agora imos tentar enviar gramos alí para a súa reposición carteira. Enviaremos 40 gramos. E imos facer un par de apostas por claridade. Vemos que o sitio mostra o historial de apostas, a porcentaxe actual de vitorias e outra información útil.

Vemosque gañamos o primeiro, perdemos o segundo.

Posterior

O artigo resultou ser moito máis longo do que esperaba, quizais podería ser máis breve, ou quizais só para unha persoa que non sabe nada de TON e quere escribir e publicar un contrato intelixente non tan sinxelo con capacidade para interactuar con eles. iso. Quizais algunhas cousas poderían ser explicadas máis sinxelamente.

Quizais algúns aspectos da implementación poderían ter sido feitos de forma máis eficiente e elegante, pero entón levaría aínda máis tempo preparar o artigo. Tamén é posible que cometín un erro nalgún lugar ou non entendín algo, polo que se estás facendo algo serio, debes confiar na documentación oficial ou no repositorio oficial co código TON.

Cómpre sinalar que, dado que a propia TON aínda está na fase activa de desenvolvemento, poden producirse cambios que romperán calquera dos pasos deste artigo (o que ocorreu mentres escribía, xa foi corrixido), pero o enfoque xeral é improbable que cambie.

Non vou falar do futuro de TON. Quizais a plataforma se converta en algo grande e deberíamos dedicar tempo a estudala e encher un nicho cos nosos produtos agora.

Tamén está Libra de Facebook, que ten unha audiencia potencial de usuarios superior a TON. Non sei case nada de Libra, a xulgar polo foro hai moita máis actividade alí que na comunidade TON. Aínda que os desenvolvedores e a comunidade de TON son máis como underground, o que tamén é xenial.

referencias

  1. Documentación oficial TON: https://test.ton.org
  2. Repositorio oficial de TON: https://github.com/ton-blockchain/ton
  3. Carteira oficial para diferentes plataformas: https://wallet.ton.org
  4. Repositorio de contratos intelixentes deste artigo: https://github.com/raiym/astonished
  5. Ligazón ao sitio web do contrato intelixente: https://ton-lottery.appspot.com
  6. Repositorio para a extensión para Visual Studio Code para FunC: https://github.com/raiym/func-visual-studio-plugin
  7. Chatea sobre TON en Telegram, o que realmente axudou a descubrilo na fase inicial. Creo que non será un erro se digo que todos os que escribiron algo para TON están alí. Tamén podes pedir gramos de proba alí. https://t.me/tondev_ru
  8. Outra charla sobre TON na que atopei información útil: https://t.me/TONgramDev
  9. Primeira fase do concurso: https://contest.com/blockchain
  10. Segunda fase do concurso: https://contest.com/blockchain-2

Fonte: www.habr.com

Engadir un comentario