Sumário
- Introdução
- Pré-requisitos
- Configuração
- Escrevendo o contrato
- Estrutura principal
- Implementação-padrão
- Lógica central
- Testando o contrato
- Compilando o contrato
- Implantando o contrato
- Interagindo com o contrato
- 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
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
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
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
A saída no terminal será:
Created library package
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
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
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;
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>,
}
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())
}
}
}
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);
}
}
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
}
É 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()
);
}
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()));
}
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
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
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
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
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
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:
Depois de clicar em “Permitir” (Allow
), você será solicitado a confirmar esta autorização inserindo o nome da conta.
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
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.
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 aCONTRACT_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
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
🎉🎉 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
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
''
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
A saída será:
View call: CONTRACT_ID.read({"k": "first_key"})
Log [CONTRACT_ID]: read
'1'
Como o método
read
não muda o estado do nosso contrato, devemos usarview
no lugar decall
. 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
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
''
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!
Latest comments (0)