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 AST
de 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 JS
em 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;
}
}
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))
Você notou que, dentro do código do JS
, operamos sobre um objeto com uma interface do contrato? Apesar do fato de que isso é JS
e não Solidity
? Basta dar uma olhada nas seguintes linhas:
const tx = await mc.setX(100);
e
console.log(await mc.x());
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 -
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
e agora vamos alterar o fn can_generate_structs
da 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);
}
Agora podemos executar o teste no diretório ethers-rs
.
cargo test can_generate_structs -- --nocapture
A saída conteria a seguinte linha:
>>> path to file: "/home/user/.../uniswap-v3-pool.rs"
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
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 Event
e Filter
personalizadas para o seu contrato.
- Cada
Event
tem sua estrutura chamadaEventNameFilter
, por exemplo,SwapFilter
,MintFilter
,BurnFilter
,CollectFilter
e assim por diante em relação ao UniswapV3Pool. Para o ERC20 seriamTransferFilter
eApprovalFilter
. - Cada
Event
tem seu manipulador para facilmente inscrever em cada um deles, por exemplofn swap_filter
,fn mint_filter
,fn burn_filter
,fn collect_filter
para o UniswapV3Pool efn trasfer_filter
efn approval_filter
para o ERC20. - Para se inscrever em todos os
Event
de uma só vez, existe oenum 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);
}
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 UniswapV3Pool
s 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
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
);
}
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()
}
}
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)