Cliente de proba TON (Telegram Open Network) e novo idioma de Fift para contratos intelixentes

Hai máis dun ano, coñeceuse os plans do messenger de Telegram para lanzar a súa propia rede descentralizada Telegrama de rede aberta. Entón quedou dispoñible un voluminoso documento técnico, que supostamente foi escrito por Nikolai Durov e describiu a estrutura da futura rede. Para aqueles que o perderon, recoméndolles que lean o meu relato deste documento (parte de 1, parte de 2; a terceira parte, por desgraza, segue recollendo po nas correntes de aire).

Desde entón, non houbo noticias significativas sobre o estado do desenvolvemento de TON ata hai un par de días (nun dos canles non oficiais) non apareceu a ligazón á páxina https://test.ton.org/download.html, onde se atopan:
ton-test-liteclient-full.tar.xz — fontes dun cliente de luz para a rede de proba TON;
ton-lite-client-test1.config.json — ficheiro de configuración para conectarse á rede de proba;
README — información sobre a creación e posta en marcha do cliente;
COMO — instrucións paso a paso para crear un contrato intelixente mediante un cliente;
tonelada.pdf — documento actualizado (con data do 2 de marzo de 2019) cunha visión xeral técnica da rede TON;
tvm.pdf — descrición técnica de TVM (TON Virtual Machine, TON virtual machine);
tblkch.pdf — descrición técnica da cadea de bloques TON;
quinta base.pdf — descrición do novo idioma Fift, deseñado para crear contratos intelixentes en TON.

Repito, non houbo confirmación oficial da páxina e de todos estes documentos de Telegram, pero o volume destes materiais fainos bastante plausibles. Inicie o cliente publicado baixo o seu propio risco.

Creación dun cliente de proba

Primeiro, intentemos crear e executar un cliente de proba, afortunadamente, README describe este proceso sinxelo en detalle. Farei isto usando macOS 10.14.5 como exemplo; non podo garantir o éxito da compilación noutros sistemas.

  1. Descargar e desempaquetar arquivo de orixe. É importante descargar a versión máis recente xa que a compatibilidade con versións anteriores non está garantida neste momento.

  2. Asegúrese de que as últimas versións de make, cmake (versión 3.0.2 ou superior), OpenSSL (incluíndo ficheiros de cabeceira C), g++ ou clang estean instaladas no sistema. Non tiven que instalar nada, todo xuntouse de inmediato.

  3. Supoñamos que as fontes están desempaquetadas nun cartafol ~/lite-client. Separadamente, cree un cartafol baleiro para o proxecto ensamblado (por exemplo, ~/liteclient-build), e a partir del (cd ~/liteclient-build) chama aos comandos:

    cmake ~/lite-client
    cmake --build . --target test-lite-client

    Cliente de proba TON (Telegram Open Network) e novo idioma de Fift para contratos intelixentes

    Tamén chamamos para crear o intérprete de idiomas de Fift para contratos intelixentes (máis información a continuación).

    cmake --build . --target fift

  4. Descarga o actual ficheiro de configuración conectarse á rede de proba e poñelo no cartafol co cliente montado.

  5. Acabado, pode iniciar o cliente:

    ./test-lite-client -C ton-lite-client-test1.config.json

Se todo está feito correctamente, deberías ver algo como isto:

Cliente de proba TON (Telegram Open Network) e novo idioma de Fift para contratos intelixentes

Como podemos ver, hai poucos comandos dispoñibles:
help — mostrar esta lista de comandos;
quit - saír;
time — mostrar a hora actual no servidor;
status — mostrar a conexión e o estado da base de datos local;
last — actualizar o estado da cadea de bloques (descargar o último bloque). É importante executar este comando antes de realizar calquera solicitude para asegurarse de que ve o estado actual da rede.
sendfile <filename> — cargar un ficheiro local á rede TON. Así é como se produce a interacción coa rede, incluíndo, por exemplo, a creación de novos contratos intelixentes e solicitudes de transferencia de fondos entre contas;
getaccount <address> - mostrar o actual (no momento en que se executou o comando) last) o estado da conta co enderezo especificado;
privkey <filename> — Cargue a clave privada dun ficheiro local.

Se ao iniciar o cliente transfire un cartafol a el mediante a opción -D, entón engadirá o último bloque da masterchain:

./test-lite-client -C ton-lite-client-test1.config.json -D ~/ton-db-dir

Agora podemos pasar a cousas máis interesantes: aprende o idioma Fift, tenta compilar un contrato intelixente (por exemplo, crea unha carteira de proba), cárguea á rede e tenta transferir fondos entre contas.

Lingua Quinta

Do documento quinta base.pdf podes descubrir que o equipo de Telegram creou unha nova linguaxe de pila para crear contratos intelixentes Quinto (aparentemente do numeral quinto, semellante ao Forth, unha lingua coa que Fifth ten moito en común).

O documento é bastante voluminoso, 87 páxinas, e non vou contar o seu contido en detalle no marco deste artigo (polo menos porque non rematei eu de lelo :). Centrareime nos puntos principais e poñerei un par de exemplos de código nesta linguaxe.

Nun nivel básico, a sintaxe de Fift é bastante sinxela: o seu código consiste en palabras, normalmente separados por espazos ou saltos de liña (caso especial: algunhas palabras non requiren un separador despois de si mesmas). Calquera a palabra é unha secuencia de caracteres que distingue entre maiúsculas e minúsculas que corresponde a un determinado determinación de (aproximadamente, o que debe facer o intérprete cando atopa esta palabra). Se non existe unha definición dunha palabra, o intérprete tenta analizala como un número e poñelas na pila. Por certo, os números aquí son, de súpeto, enteiros de 257 bits, e non hai fraccións en absoluto; máis precisamente, convértense inmediatamente nun par de números enteiros, formando o numerador e o denominador dunha fracción racional.

As palabras tenden a interactuar cos valores na parte superior da pila. Un tipo de palabras separado - prefixo — non usa a pila, senón os caracteres posteriores do ficheiro fonte. Por exemplo, así é como se implementan os literais de cadea: o carácter de comiña (") é unha palabra de prefixo que busca a seguinte comiña (de peche) e coloca a cadea entre elas na pila. Os one-liners compórtanse do mesmo xeito (//) e multiliña (/*) comentarios.

Aquí é onde remata case toda a estrutura interna da lingua. Todo o demais (incluídas as construcións de control) defínese como palabras (xa sexan internas, como operacións aritméticas e a definición de novas palabras; ou definidas na "biblioteca estándar" Fift.fif, que está no cartafol crypto/fift nas fontes).

Un exemplo sinxelo de programa en Fift:

{ dup =: x dup * =: y } : setxy
3 setxy x . y . x y + .
7 setxy x . y . x y + .

A primeira liña define unha palabra nova setxy (nótese no prefixo {, que crea un bloque antes do de peche } e prefixo :, que realmente define a palabra). setxy toma un número da parte superior da pila, defíneo (ou redefine) como global constante x, e o cadrado deste número como unha constante y (Dado que se poden redefinir os valores das constantes, prefiro chamalas variables, pero sigo a convención de nomenclatura na linguaxe).

As dúas liñas seguintes colocan un número na pila e chaman setxy, entón móstranse os valores das constantes x, y (a palabra úsase para a saída .), ambas as constantes colócanse na pila, suman e tamén se imprime o resultado. Como resultado veremos:

3 9 12 ok
7 49 56 ok

(O intérprete imprime a liña "ok" cando remata de procesar a liña actual no modo de entrada interactivo)

Ben, un exemplo de código completo:

"Asm.fif" include

-1 constant wc  // create a wallet in workchain -1 (masterchain)

// Create new simple wallet
<{  SETCP0 DUP IFNOTRET INC 32 THROWIF  // return if recv_internal, fail unless recv_external
    512 INT LDSLICEX DUP 32 PLDU   // sign cs cnt
    c4 PUSHCTR CTOS 32 LDU 256 LDU ENDS  // sign cs cnt cnt' pubk
    s1 s2 XCPU            // sign cs cnt pubk cnt' cnt
    EQUAL 33 THROWIFNOT   // ( seqno mismatch? )
    s2 PUSH HASHSU        // sign cs cnt pubk hash
    s0 s4 s4 XC2PU        // pubk cs cnt hash sign pubk
    CHKSIGNU              // pubk cs cnt ?
    34 THROWIFNOT         // signature mismatch
    ACCEPT
    SWAP 32 LDU NIP 
    DUP SREFS IF:<{
      8 LDU LDREF         // pubk cnt mode msg cs
      s0 s2 XCHG SENDRAWMSG  // pubk cnt cs ; ( message sent )
    }>
    ENDS
    INC NEWC 32 STU 256 STU ENDC c4 POPCTR
}>c
// code
<b 0 32 u, 
   newkeypair swap dup constant wallet_pk 
   "new-wallet.pk" B>file
   B, 
b> // data
// no libraries
<b b{00110} s, rot ref, swap ref, b>  // create StateInit
dup ."StateInit: " <s csr. cr
dup hash dup constant wallet_addr
."new wallet address = " wc . .": " dup x. cr
wc over 7 smca>$ type cr
256 u>B "new-wallet.addr" B>file
<b 0 32 u, b>
dup ."signing message: " <s csr. cr
dup hash wallet_pk ed25519_sign_uint rot
<b b{1000100} s, wc 8 i, wallet_addr 256 u, b{000010} s, swap <s s, b{0} s, swap B, swap <s s, b>
dup ."External message for initialization is " <s csr. cr
2 boc+>B dup Bx. cr
"new-wallet-query.boc" tuck B>file
."(Saved to file " type .")" cr

Este ficheiro de aspecto asustado é para crear un contrato intelixente: colocarase nun ficheiro new-wallet-query.boc despois da execución. Teña en conta que aquí utilízase outra linguaxe de montaxe para a máquina virtual TON (non vou determe nela en detalle), cuxas instrucións colocaranse na cadea de bloques.

Así, o ensamblador para TVM está escrito en Fift; as fontes deste ensamblador están no ficheiro crypto/fift/Asm.fif e están conectados ao principio do fragmento de código anterior.

Que podo dicir, ao parecer a Nikolai Durov lle encanta crear novas linguaxes de programación :)

Creando un contrato intelixente e interactuando con TON

Entón, supoñamos que reunimos o cliente TON e o intérprete de Fift tal e como se describe anteriormente e que nos familiarizamos co idioma. Como crear un contrato intelixente agora? Isto descríbese no ficheiro COMO, adxunto ás fontes.

Contas en TON

Como describín en Revisión TON, esta rede contén máis dunha cadea de bloques: hai unha común, a chamada. "cadea mestra", así como un número arbitrario de "cadeas de traballo" adicionais, identificadas por un número de 32 bits. A cadea mestra ten un identificador de -1; ademais, tamén se pode usar unha cadea de traballo "base" cun identificador de 0. Cada cadea de traballo pode ter a súa propia configuración. Internamente, cada cadea de traballo está dividida en shardchains, pero este é un detalle de implementación que non é necesario ter en conta.

Dentro dunha cadea de traballo, gárdanse moitas contas que teñen os seus propios identificadores account_id. Para a cadea mestra e a cadea de traballo cero, teñen unha lonxitude de 256 bits. Así, o identificador da conta escríbese, por exemplo, así:

-1:8156775b79325e5d62e742d9b96c30b6515a5cd2f1f64c5da4b193c03f070e0d

Este é o formato "en bruto": primeiro o ID da cadea de traballo, despois os dous puntos e o ID da conta en notación hexadecimal.

Ademais, hai un formato abreviado: o número da cadea de traballo e o enderezo da conta están codificados en forma binaria, engádeselles unha suma de verificación e todo isto está codificado en Base64:

Ef+BVndbeTJeXWLnQtm5bDC2UVpc0vH2TF2ksZPAPwcODSkb

Coñecendo este formato de rexistro, podemos solicitar o estado actual dunha conta a través dun cliente de proba mediante o comando

getaccount -1:8156775b79325e5d62e742d9b96c30b6515a5cd2f1f64c5da4b193c03f070e0d

Teremos algo así:

[ 3][t 2][1558746708.815218925][test-lite-client.cpp:631][!testnode]    requesting account state for -1:8156775B79325E5D62E742D9B96C30B6515A5CD2F1F64C5DA4B193C03F070E0D
[ 3][t 2][1558746708.858564138][test-lite-client.cpp:652][!testnode]    got account state for -1:8156775B79325E5D62E742D9B96C30B6515A5CD2F1F64C5DA4B193C03F070E0D with respect to blocks (-1,8000000000000000,72355):F566005749C1B97F18EDE013EBA7A054B9014961BC1AD91F475B9082919A2296:1BD5DE54333164025EE39D389ECE2E93DA2871DA616D488253953E52B50DC03F and (-1,8000000000000000,72355):F566005749C1B97F18EDE013EBA7A054B9014961BC1AD91F475B9082919A2296:1BD5DE54333164025EE39D389ECE2E93DA2871DA616D488253953E52B50DC03F
account state is (account
  addr:(addr_std
    anycast:nothing workchain_id:-1 address:x8156775B79325E5D62E742D9B96C30B6515A5CD2F1F64C5DA4B193C03F070E0D)
  storage_stat:(storage_info
    used:(storage_used
      cells:(var_uint len:1 value:3)
      bits:(var_uint len:2 value:539)
      public_cells:(var_uint len:0 value:0)) last_paid:0
    due_payment:nothing)
  storage:(account_storage last_trans_lt:74208000003
    balance:(currencies
      grams:(nanograms
        amount:(var_uint len:7 value:999928362430000))
      other:(extra_currencies
        dict:hme_empty))
    state:(account_active
      (
        split_depth:nothing
        special:nothing
        code:(just
          value:(raw@^Cell 
            x{}
             x{FF0020DDA4F260D31F01ED44D0D31FD166BAF2A1F80001D307D4D1821804A817C80073FB0201FB00A4C8CB1FC9ED54}
            ))
        data:(just
          value:(raw@^Cell 
            x{}
             x{0000000D}
            ))
        library:hme_empty))))
x{CFF8156775B79325E5D62E742D9B96C30B6515A5CD2F1F64C5DA4B193C03F070E0D2068086C000000000000000451C90E00DC0E35B7DB5FB8C134_}
 x{FF0020DDA4F260D31F01ED44D0D31FD166BAF2A1F80001D307D4D1821804A817C80073FB0201FB00A4C8CB1FC9ED54}
 x{0000000D}

Vemos a estrutura que se almacena no DHT da cadea de traballo especificada. Por exemplo, no campo storage.balance é o saldo da conta corrente, en storage.state.code - código de contrato intelixente e en storage.state.data - os seus datos actuais. Teña en conta que o almacenamento de datos TON (Célula, celas) é de tipo árbore, cada cela pode ter os seus propios datos e celas fillas. Isto móstrase como sangría nas últimas liñas.

Construír un contrato intelixente

Agora imos crear esa estrutura nós mesmos (chámase BOC - bolsa de células) empregando a linguaxe Fift. Afortunadamente, non tes que escribir un contrato intelixente no cartafol crypto/block hai un ficheiro do arquivo de orixe new-wallet.fif, que nos axudará a crear unha nova carteira. Imos copialo no cartafol co cliente montado (~/liteclient-build, se seguiu as instrucións anteriores). Citei o seu contido arriba como exemplo de código en Fift.

Execute este ficheiro do seguinte xeito:

./crypto/fift -I"<source-directory>/crypto/fift" new-wallet.fif

Aquí <source-directory> debe substituírse polo camiño ás fontes descomprimidas (o símbolo "~", desafortunadamente, non se pode usar aquí, é necesario o camiño completo). En lugar de usar unha chave -I pode definir unha variable de ambiente FIFTPATH e pon este camiño.

Desde que lanzamos Fift co nome do ficheiro new-wallet.fif, executarao e sairá. Se omites o nome do ficheiro, podes xogar co intérprete de forma interactiva.

Despois da execución, debería mostrarse algo así na consola:

StateInit: x{34_}
 x{FF0020DDA4F260810200D71820D70B1FED44D0D31FD3FFD15112BAF2A122F901541044F910F2A2F80001D31F3120D74A96D307D402FB00DED1A4C8CB1FCBFFC9ED54}
 x{0000000055375F730EDC2292E8CB15C42E8036EE9C25AA958EE002D2DE48A205E3A3426B}

new wallet address = -1 : 4fcd520b8fcca096b567d734be3528edc6bed005f6930a9ec9ac1aa714f211f2 
0f9PzVILj8yglrVn1zS-NSjtxr7QBfaTCp7JrBqnFPIR8nhZ
signing message: x{00000000}

External message for initialization is x{89FEE120E20C7E953E31546F64C23CD654002C1AA919ADD24DB12DDF85C6F3B58AE41198A28AD8DAF3B9588E7A629252BA3DB88F030D00BC1016110B2073359EAC3C13823C53245B65D056F2C070B940CDA09789585935C7ABA4D2AD4BED139281CFA1200000001_}
 x{FF0020DDA4F260810200D71820D70B1FED44D0D31FD3FFD15112BAF2A122F901541044F910F2A2F80001D31F3120D74A96D307D402FB00DED1A4C8CB1FCBFFC9ED54}
 x{0000000055375F730EDC2292E8CB15C42E8036EE9C25AA958EE002D2DE48A205E3A3426B}

B5EE9C724104030100000000D60002CF89FEE120E20C7E953E31546F64C23CD654002C1AA919ADD24DB12DDF85C6F3B58AE41198A28AD8DAF3B9588E7A629252BA3DB88F030D00BC1016110B2073359EAC3C13823C53245B65D056F2C070B940CDA09789585935C7ABA4D2AD4BED139281CFA1200000001001020084FF0020DDA4F260810200D71820D70B1FED44D0D31FD3FFD15112BAF2A122F901541044F910F2A2F80001D31F3120D74A96D307D402FB00DED1A4C8CB1FCBFFC9ED5400480000000055375F730EDC2292E8CB15C42E8036EE9C25AA958EE002D2DE48A205E3A3426B6290698B
(Saved to file new-wallet-query.boc)

Isto significa que a carteira co DNI -1:4fcd520b8fcca096b567d734be3528edc6bed005f6930a9ec9ac1aa714f211f2 (ou, o que é o mesmo, 0f9PzVILj8yglrVn1zS-NSjtxr7QBfaTCp7JrBqnFPIR8nhZ) creado correctamente. O código correspondente estará no ficheiro new-wallet-query.boc, o seu enderezo está en new-wallet.addr, e a clave privada está en new-wallet.pk (Ten coidado: executar o script de novo sobrescribirá estes ficheiros).

Por suposto, a rede TON aínda non coñece esta carteira; só se almacena en forma destes ficheiros. Agora hai que cargalo á rede. Non obstante, o problema é que para crear un contrato intelixente cómpre pagar unha comisión e o saldo da túa conta aínda é cero.

No modo de traballo, este problema resolverase comprando gramos no intercambio (ou transferindo desde outra carteira). Ben, no modo de proba actual, creouse un contrato intelixente especial, do que podes pedir ata 20 gramos así.

Xerar unha solicitude para o contrato intelixente doutra persoa

Facemos unha solicitude a un contrato intelixente que distribúe gramos á esquerda e á dereita deste xeito. No mesmo cartafol crypto/block atopar o ficheiro testgiver.fif:

// "testgiver.addr" file>B 256 B>u@ 
0x8156775b79325e5d62e742d9b96c30b6515a5cd2f1f64c5da4b193c03f070e0d
dup constant wallet_addr ."Test giver address = " x. cr

0x4fcd520b8fcca096b567d734be3528edc6bed005f6930a9ec9ac1aa714f211f2
constant dest_addr

-1 constant wc
0x00000011 constant seqno

1000000000 constant Gram
{ Gram swap */ } : Gram*/

6.666 Gram*/ constant amount

// b x --> b'  ( serializes a Gram amount )
{ -1 { 1+ 2dup 8 * ufits } until
  rot over 4 u, -rot 8 * u, } : Gram, 

// create a message (NB: 01b00.., b = bounce)
<b b{010000100} s, wc 8 i, dest_addr 256 u, amount Gram, 0 9 64 32 + + 1+ 1+ u, "GIFT" $, b>
<b seqno 32 u, 1 8 u, swap ref, b>
dup ."enveloping message: " <s csr. cr
<b b{1000100} s, wc 8 i, wallet_addr 256 u, 0 Gram, b{00} s,
   swap <s s, b>
dup ."resulting external message: " <s csr. cr
2 boc+>B dup Bx. cr
"wallet-query.boc" B>file

Tamén o gardaremos no cartafol co cliente montado, pero corrixiremos a quinta liña, antes da liña "constant dest_addr". Substitúímolo polo enderezo da carteira que creaches antes (completo, non abreviado). Non é necesario escribir "-1:" ao principio, en lugar de poñer "0x" ao principio.

Tamén pode cambiar a liña 6.666 Gram*/ constant amount — Esta é a cantidade en gramos que solicita (non máis de 20). Aínda que especifique un número enteiro, deixe o punto decimal.

Finalmente, cómpre corrixir a liña 0x00000011 constant seqno. O primeiro número aquí é o número de secuencia actual, que se almacena na conta que emite gramos. De onde podo conseguilo? Como se indicou anteriormente, inicie o cliente e execute:

last
getaccount -1:8156775b79325e5d62e742d9b96c30b6515a5cd2f1f64c5da4b193c03f070e0d

Ao final, os datos do contrato intelixente conterán

...
x{FF0020DDA4F260D31F01ED44D0D31FD166BAF2A1F80001D307D4D1821804A817C80073FB0201FB00A4C8CB1FC9ED54}
 x{0000000D}

O número 0000000D (o teu será maior) é o número de secuencia que hai que substituír en testgiver.fif.

Isto é todo, garda o ficheiro e executa (./crypto/fift testgiver.fif). A saída será un ficheiro wallet-query.boc. Isto é o que se forma mensaxe ao contrato intelixente doutra persoa: unha solicitude de "transferir tantos gramos a tal conta".

Usando o cliente, subímolo á rede:

> sendfile wallet-query.boc
[ 1][t 1][1558747399.456575155][test-lite-client.cpp:577][!testnode]    sending query from file wallet-query.boc
[ 3][t 2][1558747399.500236034][test-lite-client.cpp:587][!query]   external message status is 1

Se agora chamas last, e despois volve solicitar o estado da conta desde a que pedimos gramos, entón deberíamos ver que o seu número de secuencia aumentou nun un, isto significa que enviou diñeiro á nosa conta.

Queda o último paso: descarga o código da nosa carteira (o seu saldo xa foi reabastecido, pero sen o código de contrato intelixente non poderemos xestionalo). Levamos a cabo sendfile new-wallet-query.boc - e xa está, tes a túa propia carteira na rede TON (aínda que só sexa unha de proba polo momento).

Creación de transaccións de saída

Para transferir diñeiro desde o saldo da conta creada, hai un ficheiro crypto/block/wallet.fif, que tamén hai que colocar no cartafol co cliente montado.

Do mesmo xeito que nos pasos anteriores, cómpre axustar a cantidade que está a transferir, o enderezo do destinatario (dest_addr) e o seqno da súa carteira (é igual a 1 despois de inicializar a carteira e aumenta en 1 despois de cada transacción de saída; pode véxao solicitando o estado da túa conta). Para probas, podes usar, por exemplo, a miña carteira - 0x4fcd520b8fcca096b567d734be3528edc6bed005f6930a9ec9ac1aa714f211f2.

No inicio (./crypto/fift wallet.fif) o script tomará o enderezo da súa carteira (desde onde transfire) e a súa clave privada dos ficheiros new-wallet.addr и new-wallet.pk, e escribirase a mensaxe recibida new-wallet-query.boc.

Como antes, para realizar directamente a transacción, chame sendfile new-wallet-query.boc no cliente. Despois diso, non esquezas actualizar o estado da cadea de bloques (last) e comprobe que o saldo e o seqno da nosa carteira cambiaron (getaccount <account_id>).

Cliente de proba TON (Telegram Open Network) e novo idioma de Fift para contratos intelixentes

Iso é todo, agora podemos crear contratos intelixentes en TON e enviarlles solicitudes. Como podes ver, a funcionalidade actual xa é suficiente para, por exemplo, facer unha carteira máis amigable cunha interface gráfica (non obstante, espérase que xa estea dispoñible como parte do messenger).

Só os usuarios rexistrados poden participar na enquisa. Rexístrate, por favor.

Estás interesado en continuar os artigos con análise de TON, TVM, Fift?

  • Si, estou á espera da finalización da serie de artigos cunha visión xeral de TON

  • Si, é interesante ler máis sobre a lingua de Fift

  • Si, quero saber máis sobre TON Virtual Machine e o ensamblador para ela

  • Non, nada disto é interesante

Votaron 39 usuarios. 12 usuarios abstivéronse.

Que opinas dos plans de Telegram para lanzar TON?

  • Teño moitas esperanzas neste proxecto

  • Só estou seguindo o seu desenvolvemento con interese.

  • Son escéptico e dubido do seu éxito.

  • Inclino a considerar esta iniciativa un fracaso e innecesario para as grandes masas

Votaron 47 usuarios. 12 usuarios abstivéronse.

Fonte: www.habr.com

Engadir un comentario