WEB3DEV

Cover image for Ethers: Poder do Rust ou Atropelo do ethers::abigen!
Isabela Curado Nehme
Isabela Curado Nehme

Posted on

Ethers: Poder do Rust ou Atropelo do ethers::abigen!

17 de junho de 2023

É justo dizer que todos os interessados ​​em criptomoedas já tentaram criar contratos em Solidity. Não há dúvida de que a Ethereum éa blockchain mais bombada com uma máquina virtual Turing completa. Reconhecidamente, o JavaScript é a linguagem mais usada para interação com quase qualquer blockchain. No que diz respeito à Ethereum, no início, havia o projeto web3 que, por acaso, passou a ser um pioneiro, um modelo a ser seguido. Ainda assim, nada fica parado e é o ethers.js que goza de popularidade crescente nos dias de hoje. Nesse aspecto, as bibliotecas Rust trilharam o mesmo caminho que o JS. Inicialmente, começou com o crate web3, mas depois surgiu o ethers-rs.

Qual é o recurso mais proeminente do Rust que ficou marcado como um divisor de águas no desenvolvimento de linguagens de programação? É o seu sistema macro. Para ser franco com os usuários do JS, é algo parecido com o babel, mas com esteróides e construído na própria linguagem. Isso significa que, como no caso do Rust, é possível interferir na construção da árvore ASTde seu código, analisar e gerar um código arbitrário conforme seu desejo.

Não é grande coisa que alguém possa gerar objetos arbitrários com quaisquer propriedades e funções em tempo de execução do JSem tempo real. Afinal, essa é a própria essência das linguagens de script. Já uma ideia de implementar a mesma funcionalidade em linguagens compiladas é outra coisa.

Observação: se você quiser tirar conclusões precipitadas, sinta-se à vontade para pular para a seção de recursos principais ocultos.

Abordagem JS

Para ilustrar o ponto, digamos que haja um contrato MyContract declarado da seguinte forma:

//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8;

contract MyContract {
  uint public x;
  constructor () {
  }
  function setX(uint _x) external {
    x = _x;
  }
}
Enter fullscreen mode Exit fullscreen mode

Agora, vamos compilá-lo com o npx hardhat compile e executar o seguinte script através do npx hardhat run scripts/runMyContract.ts:

import { ethers } from 'hardhat';

async function main() {
  const MyContract = await ethers.getContractFactory('MyContract');
  const mc = await MyContract.deploy();
  console.log(await mc.x()); // BigNumber { valor: "0" }
  const tx = await mc.setX(100);
  await tx.wait(); // espera até que tx esteja comprometido com um bloco
  console.log(await mc.x()); // BigNumber { valor: "100" }
}

main()
  .catch(err => void console.error(err))
Enter fullscreen mode Exit fullscreen mode

Você notou que, dentro do código do JS, operamos sobre um objeto com uma interface do contrato? Apesar do fato de que isso é JSe não Solidity? Basta dar uma olhada nas seguintes linhas:

const tx = await mc.setX(100);
Enter fullscreen mode Exit fullscreen mode

e

console.log(await mc.x());
Enter fullscreen mode Exit fullscreen mode

No ritmo das linguagens de script nos dias de hoje, quase ninguém se preocupou em prestar atenção ao fato de que é uma conquista. Refiro-me à oportunidade fornecida pelas bibliotecas para perseguir o objetivo de sua blockchain tendo todas as coisas codificadas/decodificadas sob o capô sem expor quaisquer detalhes.

Para linguagens de script, isso é comum, apenas um padrão básico. Que tal, digamos, C++? Pode-se esperar o mesmo serviço de biblioteca? A mesma transparência de interfaces? É certo que existem alguns geradores de código, como o protobuf/grpc e o KaitaiStruct, para citar alguns. No entanto, seu uso envolve algum trabalho manual para configurá-lo como especificar algumas chamadas no CMakeLists.txt para gerar algum código de ligação antes da compilação real de seu próprio código. Para dizer o mínimo, e o mesmo acontece para um contrato Solidity, é uma chatice.

Rust e seu magnífico sistema macro surgem

Para se tornar um divisor de águas, como foi mencionado acima, o Rust percorreu o mesmo caminho da web3 para o ethers e eu vou me concentrar no último. Embora seja errôneo afirmar que não há documentação sobre ele, seria enganoso chamá-lo de exaustivo, mais notavelmente, no que diz respeito à documentação sobre sistemas macro. É normal para todos os crates no Rust. A documentação de macro está em todos os lugares irregular, desigual ou até mesmo inexistente. Por exemplo, o substrate do Polkadot é uma grande e magnífica biblioteca permeada por Myriads de linhas de código macro, dirigidas por elas e carregadas com as mesmas. Ainda assim, todas essas entranhas do pallet não estão bem documentadas.

Bem, de volta ao ethers-rs sua falta de documentação sobre o macro abigen!. É verdade que é mostrado como usá-lo, mas não há descrição de sua saída, o que se deve esperar que apareça em seu código. Essa é a lacuna que pretendo preencher por meio deste artigo.

Para aprofundar, vamos compilar algo. Vamos escolher algo bem conhecido, digamos, o uniswap/v3-core. Para fazer isso, devemos clonar os repositórios e compilar o Uniswap:

mkdir test-abigen
cd !:1
git clone https://github.com/gakonst/ethers-rs.git
git clone https://github.com/Uniswap/v3-core.git
cd v3-core
yarn i && yarn compile
cd -
Enter fullscreen mode Exit fullscreen mode

Agora, vamos obter a saída bruta do macro abigen!. Para esse objetivo, vamos escrever um teste e obter os resultados:

cd ethers-rs
vim ethers-contract/ethers-contract-abigen/src/lib.rs
Enter fullscreen mode Exit fullscreen mode

e agora vamos alterar o fn can_generate_structsda seguinte forma:

  #[test]
    fn can_generate_structs() {

        let greeter = include_str!("../../../../v3-core/artifacts/contracts/UniswapV3Pool.sol/UniswapV3Pool.json");
        let abigen = Abigen::new("UniswapV3Pool", greeter).unwrap();
        let gen = abigen.generate().unwrap();
        let out = gen.tokens.to_string();

        use std::io::Write;
        use std::os::fd::AsRawFd;
        let mut f = std::fs::File::create("../uniswap-v3-pool.rs").unwrap();
        f.write_all(out.as_bytes()).unwrap();
        let path_in_proc = std::path::PathBuf::from(format!("/proc/self/fd/{}", f.as_raw_fd())); 
        let f_name = std::fs::read_link(path_in_proc).unwrap();
        println!("\n\n>>> path to file: {:?}\n\n", f_name);
    }
Enter fullscreen mode Exit fullscreen mode

Agora podemos executar o teste no diretório ethers-rs.

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

A saída conteria a seguinte linha:

>>> path to file: "/home/user/.../uniswap-v3-pool.rs"
Enter fullscreen mode Exit fullscreen mode

Como é um código gerado, não está em um formato legível por humanos. No entanto, é possível destrinchá-lo por meio de alguns truques úteis.

cat uniswap-v3-pool.rs | sed -e 's/#/\n#/g' -e 's/{/{\n/g' -e 's/}/\n}\n/g' | less
Enter fullscreen mode Exit fullscreen mode

Neste ponto, deixo o processo de exploração para o leitor, enquanto vou destacar algumas linhas úteis no código gerado que são negligenciadas na documentação.

Principais recursos ocultos

Em primeiro lugar, é uma questão de decência emitir alguns eventos em seu código Solidity quando o estado é alterado, portanto, há uma necessidade de obtê-los. Evidentemente, seria muito conveniente que uma biblioteca fornecesse algumas estruturas de dados e manipuladores prontos para serem usados para fazer o truque. E aqui está! O ethers::abigen! gera tais confortos modernos para você.

Por exemplo, veja essa saída do UniswapV3Pool: muito menos estruturas para argumentos e retornos, instância digitada para todas as visualizações, funções externas e públicas (como foi explorado acima, não é um passeio no parque para uma linguagem compilada, porém está disponível e não há surpresas.) O que é mais interessante é que apresenta estruturas de Evente Filter personalizadas para o seu contrato.

  • Cada Event tem sua estrutura chamada EventNameFilter, por exemplo, SwapFilter, MintFilter, BurnFilter, CollectFilter e assim por diante em relação ao UniswapV3Pool. Para o ERC20 seriam TransferFilter e ApprovalFilter.
  • Cada Event tem seu manipulador para facilmente inscrever em cada um deles, por exemplo fn swap_filter, fn mint_filter, fn burn_filter , fn collect_filter para o UniswapV3Pool e fn trasfer_filter e fn approval_filter para o ERC20.
  • Para se inscrever em todos os Event de uma só vez, existe o enum UniswapV3PoolEvents(procure no código gerado).

E é exatamente isso que gostaria de destacar neste artigo, uma vez que não se espera intuitivamente que seja fornecido com tais ferramentas. Para além disso, não há uma única palavra sobre isso na documentação, por isso, é um pouco surpreendente. Não importa o quão engraçado pareça, mas esse é o caso.

Bem, vamos aos exemplos.

  • MinimalExample (Exemplo mínimo)

Digamos que você está disposto a colocar alguns ganchos no evento Swap de um UniswapV3Pool, então você seguir o seguinte trecho de código:

use std::sync::Arc;
use futures::{future, StreamExt, FutureExt};
use ethers::{
  types::{U256, H160, Filter},
  providers::{Provider, Ws, Middleware},
  contract::LogMeta,
};
use hex_literal::hex;
use tracing::info;

mod abis {
    ethers::contract::abigen!(
        UniswapV3Pool,
        "js/v3-core/artifacts/contracts/UniswapV3Pool.sol/UniswapV3Pool.json",
        event_derives (serde::Deserialize, serde::Serialize);
    );
}

const UNI_V3_POOL_ADDR: H160 = H160(hex!("9Db9e0e53058C89e5B94e29621a205198648425B"));

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();
    let provider = Arc::new(Provider::<Ws>::connect("wss://alchemy").await.unwrap());
    let univ3pool = abis::UniswapV3Pool::new(UNI_V3_POOL_ADDR, provider.clone());
    let filter = univ3pool.swap_filter();
    let sub = filter.stream().await.unwrap();
    sub.for_each(move |log| {
      match log {
        Ok(abis::uniswap_v3_pool::SwapFilter{ sqrt_price_x96, liquidity, .. }) => {
          on_swap_event(sqrt_price_x96, liquidity).left_future()
        },
        Err(_) => {
          future::ready(()).right_future()
        }
      }
    }).await;
}

async fn on_swap_event(sqrt_price_x96: U256, liquidity: u128) {
    info!("{{ sqrt_price_x96: {}, liquidity: {} }}", sqrt_price_x96, liquidity);
}
Enter fullscreen mode Exit fullscreen mode

O caminho para os artefatos essenciais do V3 foi gerado acima, se é que tenha sido gerado, então, você deve substituir seu caminho. Além disso, você deve colar seu URL wss no Alchemy ou outro nó Geth.

  • ExtendedExample (Exemplo estendido)

Digamos que você deseja se inscrever em uma série de UniswapV3Pools e em todos os eventos possíveis de uma só vez. Então você deve alterar apenas algumas linhas como as seguintes:

   let ev = univ3pool.event_with_filter(Filter::new()) // através do Deref para o ContractInstance
        .address(vec![UNI_V3_POOL_ADDR].into()); // Vetor de endereços
    let sub = ev.stream().await.unwrap().with_meta(); // adiciona with_meta() reflete a origem do evento
Enter fullscreen mode Exit fullscreen mode

Atualizando os manipuladores:

// obtém o endereço do pool e toda a estrutura
async fn on_swap_event(address: H160, abis::SwapFilter{ sqrt_price_x96, liquidity, .. }: abis::SwapFilter) {
    info!(
        "addr: {}: Swap {{ sqrt_price_x96: {}, liquidity: {} }}",
        address, sqrt_price_x96, liquidity
    );
}

async fn on_mint_event(address: H160, abis::MintFilter{ tick_upper, tick_lower, amount, ..}: abis::MintFilter) {
    info!(
        "addr: {}: Mint {{ tick_upper: {}, tick_lower: {}, amount: {} }}",
        address, tick_upper, tick_lower, amount
    );
}
Enter fullscreen mode Exit fullscreen mode

E por último atualizando a partida:

      match log {
                    Ok((abis::uniswap_v3_pool::UniswapV3PoolEvents::SwapFilter(sf), LogMeta{ address, ..})) => {
          on_swap_event(address, sf).left_future()
        },
                                        Ok((abis::uniswap_v3_pool::UniswapV3PoolEvents::MintFilter(mf), LogMeta{ address, ..})) => {
            on_mint_event(address, mf).left_future().right_future()
        },
        _ => {
          future::ready(()).right_future().right_future()
        }
      }
Enter fullscreen mode Exit fullscreen mode

Assim, é fácil de ver que o ethers-rs é um nível acima para a experiência em web3 e uma forma incrivelmente atrativa de interação para uma linguagem compilada! É verdade que, quanto ao seu macro abigen!, falta-lhe documentação adequada nas páginas docs.rs. No entanto, neste artigo, espero ter mostrado como obter as ligações geradas para examiná-las e obter as informações procuradas por você mesmo, bem como exemplos com os recursos mais valiosos não notados.

Esse artigo foi escrito por Unegare e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.

Top comments (0)