WEB3DEV

Cover image for Como Escrever e Implantar um Contrato Inteligente em Rust
Paulo Gio
Paulo Gio

Posted on • Atualizado em

Como Escrever e Implantar um Contrato Inteligente em Rust

Sumário
  1. Introdução
  2. Pré-requisitos
  3. Configuração
  4. Escrevendo o contrato
  5. Estrutura principal
  6. Implementação-padrão
  7. Lógica central
  8. Testando o contrato
  9. Compilando o contrato
  10. Implantando o contrato
  11. Interagindo com o contrato
  12. Conclusão

Neste tutorial, vamos escrever e testar um contrato inteligente usando o Rust

Introdução

Neste tutorial, vamos escrever e testar um contrato inteligente usando a linguagem Rust. Em seguida, vamos implantá-lo na rede de testes (Testnet) da NEAR.

Por que Rust? Rust é a linguagem de programação preferida para escrever contratos inteligentes no protocolo NEAR. O Rust oferece muitos recursos como segurança de memória, tempo de execução pequeno, etc. Isso nos permite escrever um contrato inteligente que não tenha bugs de memória e consuma menos armazenamento na blockchain.

Pré-requisitos

Certifique-se de ter concluído o NEAR Pathway. O Pathway abrange os fundamentos do desenvolvimento na NEAR.

Requisitos

Você deve ter os seguintes requisitos instalados:

  • Rust (Guia de instalação. Se quiser aprender mais sobre Rust, veja este guia AQUI)
  • CLI da NEAR (Guia de instalação)
  • Conta da rede de testes da NEAR (Se você não possui conta da rede de testes, consulte este guia AQUI)

Configuração

Para configurar nosso projeto, precisamos adicionar o destino WASM (WebAssembly) ao nosso conjunto de ferramentas (toolchain). Para adicionar isso, precisamos executar o seguinte comando no terminal:

rustup target add wasm32-unknown-unknown
Enter fullscreen mode Exit fullscreen mode

A saída no terminal será:

info: downloading component 'rust-std' for 'wasm32-unknown-unknown'
info: installing component 'rust-std' for 'wasm32-unknown-unknown'
info: using up to 500.0 MiB of RAM to unpack components 13.9 MiB /  13.9 MiB (100 %)  10.0 MiB/s in  1s ETA:  0s
Enter fullscreen mode Exit fullscreen mode

Se o destino já foi adicionado, a saída no terminal será:

info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date
Enter fullscreen mode Exit fullscreen mode

O que é conjunto de ferramentas Rust? Um conjunto de ferramentas é uma versão específica da coleção de programas necessários para compilar um aplicativo Rust.

Por que precisamos adicionar o destino WASM? Para implantar nosso contrato inteligente na NEAR, precisamos compilá-lo para WebAssembly (arquivo .wasm). O comando rustup acima instala as bibliotecas-padrão para o trio de destino do WebAssembly (wasm32-unknown-unknown). Leia mais sobre compilação cruzada na documentação do rustup.

Agora, vamos criar um diretório chamado key_value_storage, mudar para esse diretório e executar o seguinte comando no terminal:

cargo init --lib
Enter fullscreen mode Exit fullscreen mode

A saída no terminal será:

Created library package
Enter fullscreen mode Exit fullscreen mode

Abra o arquivo-padrão Cargo.toml que foi gerado, remova o conteúdo existente e cole o seguinte:

[package]
name = "key_value_storage"
version = "0.1.0"
authors = ["Seu nome <Seu Email>"]
edition = "2018"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
near-sdk = "3.1.0"

[profile.release]
codegen-units = 1
opt-level = "z"
lto = true
debug = false
panic = "abort"
overflow-checks = true
Enter fullscreen mode Exit fullscreen mode

Veja mais chaves de manifesto do cargo e suas definições em https://doc.rust-lang.org/cargo/reference/manifest.html. Use overflow-checks = true para aceitar verificações extras de segurança em operações aritméticas: https://stackoverflow.com/a/64136471/249801. Use opt-level = "z" para informar ao compilador Rust para otimizar o tamanho do código para que seja menor.

Estamos com tudo pronto agora! Isso pode ser usado como modelo e ponto de partida para qualquer projeto futuro.

Escrevendo o contrato

Criaremos um backend CRUD (Create, Read, Update, Delete, ou seja, Criar, Ler, Atualizar, Excluir) simples em Rust que utiliza o armazenamento na cadeia oferecido pela NEAR. Mais informações sobre o armazenamento da NEAR estão disponíveis nos documentos oficiais do protocolo NEAR.

Podemos começar removendo todo o código existente em lib.rs e colando o seguinte trecho de código:

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::{env, near_bindgen};
use near_sdk::collections::UnorderedMap;

near_sdk::setup_alloc!();

// 1. Estrutura principal

// 2. Implementação-padrão

// 3. Lógica Central

// 4. Testes
Enter fullscreen mode Exit fullscreen mode

No topo do contrato, precisamos importar alguns módulos de código com a declaração use. Explicaremos mais sobre essas partes do near_sdk abaixo.

Em seguida, vamos configurar o alocador global da biblioteca (crate) wee_alloc usando a macro setup_alloc!(). Os alocadores são a maneira como os programas em Rust obtêm memória do sistema em tempo de execução. wee_alloc é um alocador de memória projetado para WebAssembly. Ele gera menos de um kilobyte de código WebAssembly não compactado. Esta macro é uma abreviação para o código boilerplate:

#[cfg(target_arch = "wasm32")]
#[global_allocator]
static ALLOC: near_sdk::wee_alloc::WeeAlloc<'_> = near_sdk::wee_alloc::WeeAlloc::INIT;
Enter fullscreen mode Exit fullscreen mode

Leia mais sobre o alocador global e a biblioteca wee_alloc.

Estrutura principal

Ao escrever nosso contrato inteligente, seguiremos um padrão usando uma estrutura (struct) e uma implementação (impl) associada a ela. Esse é um padrão usado na maioria dos contratos Rust na NEAR. Adicione o seguinte trecho abaixo do comentário // 1. Estrutura Principal em lib.rs:

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct KeyValue {
    pairs: UnorderedMap<String, String>,
}
Enter fullscreen mode Exit fullscreen mode

Temos nossa estrutura principal KeyValue, que possui um campo chamado pairs. pairs é do tipo UnorderedMap, que importamos do near_sdk::collections. UnorderedMap é uma estrutura de dados que utiliza o armazenamento de triagem subjacente da blockchain de maneira mais eficiente. near_sdk::collections oferece algumas outras maneiras de armazenar dados na cadeia. Para obter uma melhor visão geral de todas as formas disponíveis de armazenamento que podem ser usadas, confira a documentação do módulo collections.

#[near_bindgen] e #[derive(BorshDeserialize, BorshSerialize)] são atributos.

O que são atributos? Uma tag declarativa que é usada para transmitir informações ao tempo de execução sobre os comportamentos de vários elementos, como classes, métodos, estruturas, enumeradores, assemblies, etc.

Ao adicionar a macro #[near_bindgen], fornecemos à nossa struct KeyValue o código boilerplate gerado para torná-lo compatível com a blockchain da NEAR. A segunda macro, #[derive(BorshDeserialize, BorshSerialize)], auxilia na serialização e desserialização dos dados para enviá-los ou recuperá-los da NEAR.

Implementação-padrão

Cada tipo em Rust tem uma implementação-padrão (Default), mas aqui queremos fornecer nossa própria implementação-padrão para a struct keyValue. Adicione o seguinte trecho abaixo do comentário // 2. Implementação-padrão em lib.rs:

impl Default for KeyValue {
    fn default() -> Self {
        Self {
            pairs: UnorderedMap::new(b"r".to_vec())
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora, vamos passo a passo: Primeiro, estamos criando uma implementação Default para KeyValue. Depois disso, adicionamos o método default dentro dessa implementação, que retorna Self. Self refere-se ao tipo atual, que é KeyValue. Por último, estamos retornando Self com um novo mapa não ordenado. Ao criar um novo mapa não ordenado, devemos passar o ID como tipo Vec<u8>, então estamos convertendo b"r", que é uma string de bytes, para Vec<u8>, usando a função to_vec(). O prefixo b é usado para especificar que queremos um array de bytes da string. Você pode ler sobre literais de string de bytes na documentação do Rust.

Lógica central

Agora vamos adicionar métodos à struct KeyValue. Esses métodos são a lógica central do nosso contrato inteligente. Adicione o seguinte trecho abaixo do comentário // 3. Lógica Central:

#[near_bindgen]
impl KeyValue {
    pub fn create_update(&mut self, k: String, v: String) {
        env::log(b"created or updated");
        self.pairs.insert(&k, &v);
    }

    pub fn read(&self, k: String) -> Option<String> {
        env::log(b"read");
        return self.pairs.get(&k);
    }

    pub fn delete(&mut self, k: String) {
        env::log(b"delete");
        self.pairs.remove(&k);
    }
}
Enter fullscreen mode Exit fullscreen mode

Ao criar métodos, devemos ter um bloco de implementação definido pela palavra-chave impl, seguido do nome da struct a ser implementada. A palavra-chave pub torna os métodos publicamente disponíveis, o que significa que eles podem ser chamados por qualquer pessoa com acesso ao protocolo e um meio de assinar a transação.

O primeiro método create_update é usado para criar ou atualizar um par específico. Ele recebe três argumentos: self, k e v.

Estamos usando &mut self para emprestar o self mutavelmente. Você pode aprender mais sobre empréstimos aqui.

k e v são a chave e o valor que vamos armazenar.

env::log(b"created or updated") é usado para registrar logs. Logs podem ser usados para exibir mensagens informativas ou avisos aos usuários. Além disso, esses logs ajudam o desenvolvedor no processo de desenvolvimento. Você verá esses logs nos terminais quando interagirmos com o contrato inteligente na parte posterior do tutorial. Você também pode ver os logs no console do desenvolvedor em seu navegador quando criar um aplicativo da web para o seu contrato inteligente.

Depois disso, chamamos o método insert em self.pairs. Isso criará um par chave-valor se este ainda não estiver presente; caso contrário, atualizará o valor associado à chave fornecida.

Os dois próximos métodos são bem semelhantes, mas em vez de chamar o método insert, chamamos os métodos get e remove em self.pairs para ler ou remover o par chave-valor.

Testando o contrato

O código para o nosso contrato inteligente CRUD agora está completo. Uma das boas características do Rust é que ele permite testes unitários inline. Isso significa que podemos escrever nossos testes de unidade no mesmo arquivo de origem que nosso contrato, lib.rs!

Por que devemos escrever testes de unidade para um contrato inteligente? Testes de unidade são uma prática comum no desenvolvimento de software. Ao escrever contratos inteligentes, os testes de unidades são importantes porque os contratos inteligentes muitas vezes são imutáveis e às vezes responsáveis ​​por gerenciar somas de dinheiro. Escrever bons testes de unidade é um componente-chave do desenvolvimento de contratos inteligentes seguros e confiáveis.

Copie e cole o seguinte código abaixo do comentário // 4. Testes em lib.rs:

#[cfg(not(target_arch = "wasm32"))]
#[cfg(test)]
mod tests {
    use super::*;
    use near_sdk::MockedBlockchain;
    use near_sdk::{testing_env, VMContext};

    fn get_context(input: Vec<u8>, is_view: bool) -> VMContext {
        VMContext {
            current_account_id: "alice_near".to_string(),
            signer_account_id: "bob_near".to_string(),
            signer_account_pk: vec![0, 1, 2],
            predecessor_account_id: "carol_near".to_string(),
            input,
            block_index: 0,
            block_timestamp: 0,
            account_balance: 0,
            account_locked_balance: 0,
            storage_usage: 0,
            attached_deposit: 0,
            prepaid_gas: 10u64.pow(18),
            random_seed: vec![0, 1, 2],
            is_view,
            output_data_receivers: vec![],
            epoch_height: 0,
        }
    }

    // Teste 1

    // Teste 2

}
Enter fullscreen mode Exit fullscreen mode

É aqui que configuramos nosso ambiente de testes com vários parâmetros e uma blockchain simulada. Leia mais sobre o contexto da máquina virtual e o ambiente de testes nos documentos do NEAR-SDK.

Vamos escrever nosso primeiro teste para os métodos create_update e read. Cole o seguinte trecho abaixo do comentário // Test 1 in lib.rs:

#[test]
    fn create_read_pair() {
        let context = get_context(vec![], false);
        testing_env!(context);
        let mut contract = KeyValue::default();
        contract.create_update("first_key".to_string(), "hello".to_string());
        assert_eq!(
            "hello".to_string(),
            contract.read("first_key".to_string()).unwrap()
        );
    }
Enter fullscreen mode Exit fullscreen mode

Primeiro, criamos uma variável de contexto (context) chamando a função get_context e passando-a para a macro testing_env!(), que cria um ambiente de testes usando os parâmetros fornecidos. Em seguida, temos que criar uma variável de contrato mutável contract, que usará o contrato que acabamos de escrever. Podemos então usar essa variável de contrato para chamar nossos métodos para os testes.

Agora, vamos chamar o método create_update para definir pares chave-valor. A chave será first_key e o valor será hello. Depois de criar o par, queremos verificar se os valores corretos estão no armazenamento. Para verificar, usaremos a macro assert_eq!(). Ela recebe 2 argumentos: o valor esperado e o valor atual.

Passaremos o valor esperado como "hello".to_string() e, para o valor atual, chamaremos o método read com first_key como argumento. O método read retorna o valor do tipo Option<String>, mas o tipo de valor esperado é String. É por isso que usamos o método unwrap para obter o valor do tipo String fora do tipo Option<String>.

Para o nosso segundo teste, vamos assumir que a chave não está presente no armazenamento. Nesse caso, None deve ser retornado quando tentamos ler a chave. Adicione o seguinte código abaixo do comentário // Teste 2:

 #[test]
    fn read_nonexistent_pair() {
        let context = get_context(vec![], true);
        testing_env!(context);
        let contract = KeyValue::default();
        assert_eq!(None, contract.read("first_key".to_string()));
    }
Enter fullscreen mode Exit fullscreen mode

Assim como no primeiro teste, criaremos nosso ambiente de testes e a variável contract. No entanto, neste teste, a variável do contrato é imutável, pois não vamos alterá-la. Para verificar se None é retornado após o acesso a uma chave inexistente usando contract.read("first_key".to_string()), usaremos assert_eq!().

Agora é hora de testar nosso código. Execute o seguinte comando no terminal:

cargo test -- --nocapture
Enter fullscreen mode Exit fullscreen mode

Os testes serão aprovados apenas se o contrato estiver funcionando corretamente. Como nosso contrato inteligente está funcionando corretamente, veremos a seguinte saída:

Finished test [unoptimized + debuginfo] target(s) in 1m 05s
     Running target/debug/deps/key_value_storage-958f616e81cf3269

running 2 tests
test tests::read_nonexistent_pair ... ok
test tests::create_read_pair ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests key_value_storage

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Enter fullscreen mode Exit fullscreen mode

Compilando o contrato

Agora que escrevemos e testamos o contrato inteligente em Rust, compilaremos em WebAssembly para a implantação na NEAR. Execute o seguinte comando no terminal (Observação: usuários do Windows terão que executar os dois comandos separadamente, set para as variáveis de ambiente e, em seguida, o comando cargo build):

// Usuários do Linux e do macOS podem usar estes comandos:
env 'RUSTFLAGS=-C link-arg=-s' 
cargo build --target wasm32-unknown-unknown --release

// Usuários do Windows podem usar estes comandos:
set RUSTFLAGS=-C link-arg=-s
cargo build --target wasm32-unknown-unknown --release
Enter fullscreen mode Exit fullscreen mode

A saída do cargo build será similar a isso:

Compiling near-sdk v3.1.0
Compiling key_value_storage v0.1.0 (/home/xqc/key_value_storage)
Finished release [optimized] target(s) in 1m 00s
Enter fullscreen mode Exit fullscreen mode

Agora geramos um arquivo WebAssembly otimizado que podemos implantar na NEAR, e para este tutorial, vamos implantá-lo na rede de testes da NEAR.

Implantando o contrato

Primeiro, você deve fazer login na sua conta usando a near-cli. Execute:

near login
Enter fullscreen mode Exit fullscreen mode

Isso irá redirecionar para a carteira NEAR solicitando acesso total à sua conta. A partir daqui, selecione para qual conta você deseja uma chave de acesso:

Carteira NEAR solicitando chave de acesso

Depois de clicar em “Permitir” (Allow), você será solicitado a confirmar esta autorização inserindo o nome da conta.

Confirmando autorização de acesso da NEAR

Depois de concluído, você terá sua chave de acesso armazenada localmente em um diretório oculto chamado .near-credentials. Este diretório está localizado na raiz do seu diretório HOME:

  • ~/.near-credentials (MAC / Linux)
  • C:\Users\SUA_CONTA\.near-credentials (Windows)

Em seguida, dar um nome ao nosso contrato parece uma ótima ideia. Para conseguir isso, usaremos a near-cli para criar uma nova conta pertencente à nossa conta principal.

near create-account CONTRACT_NAME.ACCOUNT_ID --masterAcount ACCOUNT_ID --initialBalance 10
Enter fullscreen mode Exit fullscreen mode

A saída ficará assim:

Saving key to '/home/xxx/.near-credentials/testnet/CONTRACT_NAME.ACCOUNT_ID.json'
Account CONTRACT_NAME.ACCOUNT_ID.testnet for network "testnet" was created.
Enter fullscreen mode Exit fullscreen mode

Por exemplo, supondo que seu current account_id na rede de testes da NEAR seja fido.testnet e você gostaria de nomear o contrato de dodo, então você criará um novo account_id, dodo.fido.testnet, que será o contract_id.

--initialBalance (saldo inicial), se omitido, será de 100 Near por padrão. Mais adiante neste tutorial, CONTRACT_ID irá se referir a CONTRACT_NAME.ACCOUNT_ID. Leia mais sobre as contas da NEAR aqui.

Agora podemos implantar nosso contrato inteligente Rust na NEAR. Execute o seguinte comando no terminal (Observação: substitua YOUR_ACCOUNT_HERE pelo nome da sua conta, ex. example.near):

near deploy --wasmFile target/wasm32-unknown-unknown/release/key_value_storage.wasm --accountId CONTRACT_ID
Enter fullscreen mode Exit fullscreen mode

Após a conclusão da implantação, você verá uma saída semelhante a esta no terminal:

Starting deployment. Account id: CONTRACT_ID, node: https://rpc.testnet.near.org, helper: https://helper.testnet.near.org, file: target/wasm32-unknown-unknown/release/key_value_storage.wasm
Transaction Id E4uT8wV5uXSgsJpB73Uox27iPgXztWfL3b5gzxfA3fHo
To see the transaction in the transaction explorer, please open this url in your browser
https://explorer.testnet.near.org/transactions/E4uT8wV5uXSgsJpB73Uox27iPgXztWfL3b5gzxfA3fHo
Done deploying to CONTRACT_ID
Enter fullscreen mode Exit fullscreen mode

🎉🎉 Implantamos com sucesso nosso primeiro contrato inteligente Rust.

Interagindo com o contrato

Agora que implantamos nosso contrato, podemos interagir com ele usando a CLI da NEAR.

Criaremos um par chave-valor e o leremos.

Este comando criará um par chave-valor:

near call CONTRACT_ID create_update '{"k": "first_key", "v" : "1"}' --accountId ACCOUNT_ID
Enter fullscreen mode Exit fullscreen mode

A saída será:

Scheduling a call: CONTRACT_ID.create_update({"k": "first_key", "v" : "1"})
Receipt: 6bCmuWuAbdiXWvPaTvidbJP4f3mSG4UVcE52g18ZWMq5
        Log [CONTRACT_ID]: created or updated
Transaction Id AQWwThAtXWhU7HJsuD5bvi2FXHpnw5xbj5SEe94Q3MTp
To see the transaction in the transaction explorer, please open this URL in your browser
https://explorer.testnet.near.org/transactions/AQWwThAtXWhU7HJsuD5bvi2FXHpnw5xbj5SEe94Q3MTp
''
Enter fullscreen mode Exit fullscreen mode

Os argumentos da função devem ser fornecidos como uma string JSON após o nome do método.

Agora, vamos ler o valor da primeira chave:

near view CONTRACT_ID read '{"k": "first_key"}' --accountId ACCOUNT_ID
Enter fullscreen mode Exit fullscreen mode

A saída será:

View call: CONTRACT_ID.read({"k": "first_key"})
Log [CONTRACT_ID]: read
'1'
Enter fullscreen mode Exit fullscreen mode

Como o método read não muda o estado do nosso contrato, devemos usar view no lugar de call. Fazer isso tem as seguintes vantagens:

  • Não temos que pagar nenhuma taxa
  • A resposta à nossa consulta ocorre quase imediatamente

Por fim, excluiremos a chave:

near call CONTRACT_ID delete '{"k": "first_key"}' --accountId ACCOUNT_ID
Enter fullscreen mode Exit fullscreen mode

A saída será:

Scheduling a call: CONTRACT_ID.delete({"k": "first_key"})
Receipt: wp8YoFZC7CKUNty66VoYuaHMVej7UqcbLcK2FjpMZzk
        Log [CONTRACT_ID]: delete
Transaction Id A4aDmpkfbEP8JwM5KspUiWe1zYnKgUnA5wosCiQBwour
To see the transaction in the transaction explorer, please open this url in your browser
https://explorer.testnet.near.org/transactions/A4aDmpkfbEP8JwM5KspUiWe1zYnKgUnA5wosCiQBwour
''
Enter fullscreen mode Exit fullscreen mode

Conclusão

Neste tutorial, abordamos os fundamentos da programação de contratos inteligentes na NEAR usando o Rust - incluindo a estrutura de um contrato inteligente Rust; o uso de funções e macros oferecidos pelo SDK da NEAR; como utilizar o armazenamento na cadeia; testes de unidade de um contrato Rust; compilação e implantação de WebAssembly e, por fim, como interagir com contratos Rust implantados na blockchain NEAR usando a CLI da NEAR.

Agradecemos por acompanhar este tutorial e esperamos que você possa utilizar esse conhecimento para criar coisas incríveis na plataforma de contratos inteligentes da NEAR!

Artigo original publicado por Nikhil Bhintade. Traduzido por Paulinho Giovannini.


A partir de julho de 2023, esses grupos de estudos serão conduzidos por membros e monitores da comunidade DEV, oferecendo uma oportunidade única de interação e crescimento conjunto. Os moderadores e membros da comunidade estão disponíveis para fornecer suporte e orientação. As sessões são gravadas e disponibilizadas posteriormente. Participe no Discord!
Convite para o grupo de estudos em solidity e rust


Convite para os builds de Solana

Top comments (0)