WEB3DEV

Cover image for Especificação ABI de Contrato Ethereum. Interação com o contrato
Fatima Lima
Fatima Lima

Posted on

Especificação ABI de Contrato Ethereum. Interação com o contrato

Image description

Neste artigo, quero apresentar a você como os dados são codificados em uma transação de acordo com a Especificação ABI de contrato. Vamos decompor manualmente todo o processo de codificação, criar um contrato e chamar seus métodos. No final, mostrarei como usar a ABI (interface binária de aplicação) de Contrato para criar um objeto wrapper com o web3.js e chamar os métodos do contrato por meio desse objeto.

Plano

  1. Configuração do Ambiente
  2. Criação do Contrato
  3. Interação com o contrato
  4. Objeto wrapper no contrato

Configuração do ambiente

O que precisaremos: o compilador Solidity, o contrato em si, a conexão com a rede de teste Sepolia e uma conta com Ether de teste em seu saldo. Também precisaremos adicionar a chave privada dessa conta à Wallet (carteira) da biblioteca web3.js.

Vamos começar com o compilador Solidity. Há diferentes maneiras de instalar o compilador Solidity, tudo depende do seu sistema operacional e de como você deseja instalá-lo: npm, Docker, Linux Packages etc. Como instalar o compilador pode ser encontrado aqui.

Vamos criar um diretório de trabalho, instalar o web3.js e adicionar nosso contrato a ele. Versão do Web3.js no momento da redação deste artigo: 4.0.1

$ mkdir raw-contract
$ cd raw-contract
$ npm install web3
$ nano Faucet.sol
Enter fullscreen mode Exit fullscreen mode

Código do contrato:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

contract Faucet {
   // Aceita qualquer valor de entrada
   receive() external payable {}

   // Distribui ether para qualquer pessoa que pedir
   function withdraw(uint withdraw_amount) public {
       //Limite do valor de saque
       require(withdraw_amount <= 0.01 ether);

       // Envia o valor para o endereço que o solicitou       payable(msg.sender).transfer(withdraw_amount);
   }
}
Enter fullscreen mode Exit fullscreen mode

Eu peguei o contrato do meu exemplo anterior quando o implantamos na blockchain Ganache local. A lógica do contrato permite que você adicione Ether ao seu saldo e permite que todos retirem 0,01 Ether por transação.

Por via das dúvidas, verifique a versão do compilador, que deve ser 0.8.x:

$ solc --version

// saída:
solc, the solidity compiler commandline interface
Version: 0.8.20+commit.a1b79de6.Darwin.appleclang
Enter fullscreen mode Exit fullscreen mode

Agora vamos compilar o contrato e obter sua representação binária:

$ solc --bin Faucet.sol

======= Faucet.sol:Faucet =======
Binary:
608060405234801561000f575f80fd5b506101468061001d5f395ff3fe608060405260043610610021575f3560e01c80632e1a7d4d1461002c57610028565b3661002857005b5f80fd5b348015610037575f80fd5b50610052600480360381019061004d91906100e5565b610054565b005b662386f26fc10000811115610067575f80fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc8290811502906040515f60405180830381858888f193505050501580156100aa573d5f803e3d5ffd5b5050565b5f80fd5b5f819050919050565b6100c4816100b2565b81146100ce575f80fd5b50565b5f813590506100df816100bb565b92915050565b5f602082840312156100fa576100f96100ae565b5b5f610107848285016100d1565b9150509291505056fea2646970667358221220c9fe2f78a923e108f0619a2d655b35d18297668335e649a10272c09a790a188964736f6c63430008140033
Enter fullscreen mode Exit fullscreen mode

O que temos é uma representação binária do contrato, que adicionaremos ao campo data da transação e enviaremos à rede Ethereum. Depois disso, nosso contrato será implantado.

Vamos começar a configurar uma conexão com a rede Ethereum e, para isso, abriremos o console do node.js:

$ node
Enter fullscreen mode Exit fullscreen mode

Conecte-se à rede de teste Sepolia:

> const { Web3 } = require('web3');
> const web3 = new Web3('https://rpc2.sepolia.org');
Enter fullscreen mode Exit fullscreen mode

Para que o web3.js possa assinar nossa transação, devemos adicionar a chave privada da conta da qual enviaremos essa transação. Vamos passar a chave privada para o método add():

> web3.eth.accounts.wallet.add('0x0e...e3');
Enter fullscreen mode Exit fullscreen mode

Saída:

Wallet(1) [
 {
   address: '0xf79ac5fa7F6e76590Ae6cE641aB42b33CAB86C47',
   privateKey: '0x0e...e3',
   signTransaction: [Function: signTransaction],
   sign: [Function: sign],
   encrypt: [Function: encrypt]
 },
 _accountProvider: {
   create: [Function: createWithContext],
   privateKeyToAccount: [Function: privateKeyToAccountWithContext],
   decrypt: [Function: decryptWithContext]
 },
 _addressMap: Map(1) { '0xf79ac5fa7f6e76590ae6ce641ab42b33cab86c47' => 0 },
 _defaultKeyName: 'web3js_wallet'
]
Enter fullscreen mode Exit fullscreen mode

Para enviar a transação, usarei minha conta existente e o Ether de teste do saldo. Se você não tiver sua própria conta, poderá criá-la facilmente com um comando e obter algum Ether de teste. Descrevi como fazer isso em um de meus artigos anteriores.

Estamos prontos para enviar uma transação de criação de contrato.

Criação de Contrato

Vamos adicionar o prefixo 0x ao início do código do contrato e colocá-lo em uma variável:

> var contractCode = '0x608060405234801561000f575f80fd5b506101468061001d5f395ff3fe608060405260043610610021575f3560e01c80632e1a7d4d1461002c57610028565b3661002857005b5f80fd5b348015610037575f80fd5b50610052600480360381019061004d91906100e5565b610054565b005b662386f26fc10000811115610067575f80fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc8290811502906040515f60405180830381858888f193505050501580156100aa573d5f803e3d5ffd5b5050565b5f80fd5b5f819050919050565b6100c4816100b2565b81146100ce575f80fd5b50565b5f813590506100df816100bb565b92915050565b5f602082840312156100fa576100f96100ae565b5b5f610107848285016100d1565b9150509291505056fea2646970667358221220c9fe2f78a923e108f0619a2d655b35d18297668335e649a10272c09a790a188964736f6c63430008140033';
Enter fullscreen mode Exit fullscreen mode

Tentando enviar a transação:

> await web3.eth.sendTransaction({from: '0xf79ac5fa7F6e76590Ae6cE641aB42b33CAB86C47', gasLimit: 21000, data: contractCode});
Enter fullscreen mode Exit fullscreen mode

Nesse caso, obtivemos um erro:

Uncaught TransactionRevertInstructionError: Transaction has been reverted by the EVM
  ...
 innerError: undefined,
 reason: 'err: intrinsic gas too low: have 21000, want 58368 (supplied gas 21000)',
 signature: undefined,
 receipt: undefined,
 data: undefined,
 code: 402
}
Enter fullscreen mode Exit fullscreen mode

O problema é que não tínhamos gas suficiente para enviar a transação. No código da mensagem, vemos que 21000 foi inserido, mas era necessário o valor de 58368. O mais interessante é que, se definirmos o valor necessário como 58368, isso não significa que criaremos o contrato com êxito. Isso é suficiente apenas para enviar uma transação, mas não é suficiente para criar o contrato em si na EVM, e veremos uma situação como esta:

Image description

Falha na transação de criação do contrato

Simplificando o que aconteceu aqui: a transação entrou na rede Ethereum, se espalhou pelos nós e foi colocada no Mempool deles. Usando o algoritmo Proof of Stake (Prova de participação), um nó foi selecionado. Esse nó atualmente validará, executará e adicionará transações do Mempool em um novo bloco. Nossa transação teve sorte e foi escolhida pelo nó para ser adicionada ao bloco.

O nó pegou a transação em processamento e executou o código que estava no campo data (dados) da transação em sua EVM local. Durante o processo de execução, foi criada uma conta para o contrato e o processo de implantação do contrato no storage (armazenamento) dessa conta foi iniciado. Durante a implantação, descobriu-se que não havia gas suficiente na transação para concluir o processo e ocorreu um erro Out of Gas error (falta de gás).

Como resultado, tivemos uma situação em que foi criada uma conta para um contrato, mas o código do contrato não foi salvo na conta storage. Além disso, perdemos o gas que foi usado na execução do código, já que o nó gastou seus recursos de computação na execução desse código. Como você pode ver, as operações de criação de uma conta e colocação do código do contrato nela não são atômicas na rede Ethereum. As alterações da conta storage foram revertidas, mas a própria conta criada permaneceu na blockchain com zero em vez do código do contrato:

Image description

Conta de contrato após falha na implantação

Para não reproduzir o cenário descrito, bem como para não calcular o valor exato do Gas para a implantação do contrato, definiremos o gasLimit (limite de gas) com uma margem. O Gas restante após a implantação do contrato retornará ao saldo da nossa conta. Uma quantidade mais precisa de Gas pode ser encontrada implantando o contrato em uma blockchain local, como a Ganache, ou implantando-o no RemixIDE e, em seguida, vendo a quantidade de Gas usada. Ao usar bibliotecas, você pode recorrer a funções auxiliares que permitem calcular a quantidade necessária de Gas antes de enviar uma transação.

Então, vamos ver o gasLimit com uma margem e enviar uma transação para criar um contrato:

> await web3.eth.sendTransaction({from: '0xf79ac5fa7F6e76590Ae6cE641aB42b33CAB86C47', gasLimit: 300000, data: contractCode});
Enter fullscreen mode Exit fullscreen mode

Recibo da transação:

{
 blockHash: '0x57e6957ca0be6079ddd8a4af7e28a677f5fce8c19ff4a84fdc8bebf3c4957ad7',
 blockNumber: 3749703n,
 contractAddress: '0x13c96729039f1da4ea42ffe1a7e9cac1cf42801d',
 cumulativeGasUsed: 29713841n,
 effectiveGasPrice: 294172321n,
 from: '0xf79ac5fa7f6e76590ae6ce641ab42b33cab86c47',
 gasUsed: 123683n,
 logs: [],
 logsBloom: '0x00000000000000000000000...0000000000000000000000000000',
 status: 1n,
 transactionHash: '0x3650d8427dd426fa76967a2d69dd84e67def5cc81cf9875e54221fb97ea14aaa',
 transactionIndex: 35n,
 type: 0n
}
Enter fullscreen mode Exit fullscreen mode

O contrato foi criado com sucesso, aqui está seu endereço:

0x13c96729039f1da4ea42ffe1a7e9cac1cf42801d
Enter fullscreen mode Exit fullscreen mode

Conta do contrato:

Image description

Conta do contrato criada com sucesso

Código do contrato:

Image description

Conta do contrato e seu código

A transação que criou o contrato:

Image description

A transação que criou o contrato

Ótimo. Uma vez que o contrato tenha sido criado, vamos chamar seus métodos. Por exemplo, vamos adicionar um pouco de Ether ao saldo do contrato.

Interação com o contrato

Neste artigo, usei a estrutura Truffle para simplificar a interação com o contrato. Recebemos um objeto wrapper e chamamos os métodos do contrato por meio dele. Desta vez, chamaremos os métodos do contrato diretamente, enviando transações com um nome de método codificado e argumentos no campo data.

Felizmente, não precisamos codificar nada para enviar Ether para o saldo do contrato, pois temos uma função fallback (plano B), a receive(), que funcionará quando uma transação convencional chegar sem um campo data. Tudo o que precisamos é adicionar um pouco de Ether no campo value dessa transação. Escrevi mais sobre funções fallback aqui.

Portanto, vamos enviar 0,1 Ether para o contrato. O GasLimit também será definido com uma pequena margem:

> await web3.eth.sendTransaction({from: '0xf79ac5fa7F6e76590Ae6cE641aB42b33CAB86C47', to:'0x13c96729039f1da4ea42ffe1a7e9cac1cf42801d', value: web3.utils.toWei('0.1', 'ether'), gasLimit: 30000});
Enter fullscreen mode Exit fullscreen mode

Recibo:

{
 blockHash: '0xeb8f28d40966400fcfba7690c938534c93a295ba860b961f72520ad5cf5b3395',
 blockNumber: 3749766n,
 cumulativeGasUsed: 14620676n,
 effectiveGasPrice: 94971021n,
 from: '0xf79ac5fa7f6e76590ae6ce641ab42b33cab86c47',
 gasUsed: 21055n,
 logs: [],
 logsBloom: '0x000000000000000000000000000...00000000000000000000000000',
 status: 1n,
 to: '0x13c96729039f1da4ea42ffe1a7e9cac1cf42801d',
 transactionHash: '0xa50902396f4ac2c15fd7c551cef084acf13be2c92c5dd2c91308793b37fe1a95',
 transactionIndex: 101n,
 type: 0n
}
Enter fullscreen mode Exit fullscreen mode

O saldo aumentou 0.1 Ether:

Image description

O saldo do contrato aumentou 0,1 Ether

Transação que transferiu 0,1 Ether para o saldo do contrato:

Image description

Transação que transferiu 0,1 Ether para o saldo do contrato

Ótimo. Agora a parte interessante. Vamos ver como chamar uma função de contrato convencional em um formato compreensível para o protocolo Ethereum. Em nosso contrato, essa função é withdraw().

Para fazer uma chamada de método, precisamos codificar a assinatura do método e seus argumentos. A assinatura do método codificado é chamada de function selector (seletor de função) na documentação Solidity.

A assinatura de um método no Solidity é: nome da função + tipos de argumentos entre parênteses, separados por vírgulas e sem espaços. No nosso caso, a assinatura tem a seguinte aparência:

withdraw(uint256)
Enter fullscreen mode Exit fullscreen mode

Para obter o seletor de função, calcule o hash Keccak-256 a partir da assinatura do método:

> web3.utils.sha3('withdraw(uint256)');

// Saída:
'0x2e1a7d4d13322e7b96f9a57413e1525c250fb7a9021cf91d1540d5b69f16a49f'
Enter fullscreen mode Exit fullscreen mode

e pegue os primeiros 4 bytes do hash calculado (um byte tem dois caracteres hexadecimais, sem contar o prefixo 0x):

0x2e1a7d4d
Enter fullscreen mode Exit fullscreen mode

Esse é o seletor de função. Agora resta codificar o argumento em si, que no nosso caso é 0,01 Ether. Para fazer isso, primeiro convertemos 0,01 Ether em Wei, já que o protocolo Ethereum opera com valores em Wei:

> web3.utils.toWei('0.01', 'ether');

// Saída:
'10000000000000000'
Enter fullscreen mode Exit fullscreen mode

Em seguida, converta esse valor para o formato hexadecimal:

> web3.utils.toHex(10000000000000000);

// Saída:
'0x2386f26fc10000'
Enter fullscreen mode Exit fullscreen mode

Em seguida, vamos utilizar o preenchimento com zero à esquerda. Como usamos o tipo uint256 e seu tamanho é de 256 bits ou 32 bytes, precisamos enviar um número com comprimento de 256 bits. Para isso, precisamos adicionar zeros à esquerda para que o número tenha 256 bits de comprimento, ou 64 caracteres. Portanto, precisamos adicionar 50 zeros à esquerda aos nossos 14 caracteres:

000000000000000000000000000000000000000000000000002386f26fc10000
Enter fullscreen mode Exit fullscreen mode

E agora colocamos o argumento após o seletor de função:

0x2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000
Enter fullscreen mode Exit fullscreen mode

Ótimo. Nossa chamada para o método withdraw() está pronta. Agora, coloque-a no campo data da transação e a envie:

> await web3.eth.sendTransaction({from: '0xf79ac5fa7F6e76590Ae6cE641aB42b33CAB86C47', to: '0x13C96729039F1da4Ea42Ffe1a7E9Cac1cF42801D', gasLimit: 50000, data: '0x2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000'});
Enter fullscreen mode Exit fullscreen mode

Recibo:

{
 blockHash: '0xb4994dfc02f5ecbff87a28ee8fc157f2af34816b23401bd78e24ea24d169c6d0',
 blockNumber: 3750579n,
 cumulativeGasUsed: 3294721n,
 effectiveGasPrice: 27416831971n,
 from: '0xf79ac5fa7f6e76590ae6ce641ab42b33cab86c47',
 gasUsed: 28559n,
 logs: [],
 logsBloom: '0x0000000000000000000000...0000000000000000000000000',
 status: 1n,
 to: '0x13c96729039f1da4ea42ffe1a7e9cac1cf42801d',
 transactionHash: '0xf8c01ab85fb32c87d2d4b98981171ee2365aa5d77f0580844909bd4104daf129',
 transactionIndex: 16n,
 type: 0n
}
Enter fullscreen mode Exit fullscreen mode

A transação foi bem-sucedida. Chamamos o método withdraw() de uma conta externa e recebemos 0,01 Ether.

Image description

Retirada de 0,01 ETH do saldo do contrato para a conta EOA

A propósito, a codificação também pode ser feita usando os seguintes métodos no web3.js:

> web3.eth.abi.encodeFunctionSignature('withdraw(uint256)'); 
// Saída: 
'0x2e1a7d4d' 

> web3.eth.abi.encodeParameter('uint256', '10000000000000000'); \
// Saída: 
'0x000000000000000000000000000000000000000000000000002386f26fc10000`'
Enter fullscreen mode Exit fullscreen mode

Mas o objetivo era mostrar o processo de codificação passo a passo.

Objeto wrapper no contrato

Acima, discutimos o processo de codificação manual de dados para interagir com um contrato. Normalmente, ao desenvolver aplicativos Dapp, a interação com o contrato é realizada usando bibliotecas como: Truffle, web3.js, ethers.js, Web3.py, web3j. Todas essas bibliotecas permitem que você acesse o contrato a partir do código do aplicativo como se fosse um objeto comum com métodos. Esses objetos cuidam de toda a codificação de dados necessária e do envio da transação. A seguir, veremos como esse objeto pode ser obtido no web3.js e, com a ajuda dele, chamaremos o método do contrato.

Para criar um objeto de contrato no web3.js, precisamos da ABI (Application Binary Interface ou interface binária de aplicação) do contrato, que é uma descrição dos métodos do contrato, dos tipos de dados e de outras informações que as bibliotecas necessitam para interagir com o contrato.

Podemos obter a ABI no formato json da seguinte forma:

$ solc Faucet.sol --abi
Enter fullscreen mode Exit fullscreen mode

Saída:

======= Faucet.sol:Faucet =======
Contract JSON ABI
[{"inputs":[{"internalType":"uint256","name":"withdraw_amount","type":"uint256"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]
Enter fullscreen mode Exit fullscreen mode

ABI formatada:

[
 {
   "inputs": [
     {
       "internalType": "uint256",
       "name": "withdraw_amount",
       "type": "uint256"
     }
   ],
   "name": "withdraw",
   "outputs": [],
   "stateMutability": "nonpayable",
   "type": "function"
 },
 {
   "stateMutability": "payable",
   "type": "receive"
 }
]
Enter fullscreen mode Exit fullscreen mode

Aqui vemos o método withdraw e os dados sobre ele, assim como a função fallback receive, que diz que o contrato pode aceitar Ether em seu endereço.

Esse json é passado para o constructor do objeto, por meio do qual interagiremos com o contrato e, em seguida, o próprio objeto executará as operações nas chamadas de codificação para os métodos do contrato.

Converter json em objeto JavaScript:

> var contractABI = JSON.parse('[{"inputs":[{"internalType":"uint256","name":"withdraw_amount","type":"uint256"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]');
Enter fullscreen mode Exit fullscreen mode

Passe a ABI e o endereço do contrato para o constructor e obtenha o objeto do contrato:

> var myContract = new web3.eth.Contract(contractABI, '0x13c96729039f1da4ea42ffe1a7e9cac1cf42801d', {from: '0xf79ac5fa7F6e76590Ae6cE641aB42b33CAB86C47'});
Enter fullscreen mode Exit fullscreen mode

Preste atenção ao objeto com o campo from — essa é a conta padrão da qual as transações para o contrato serão enviadas.

A chamada do método terá a seguinte aparência:

> await myContract.methods.withdraw(web3.utils.toWei('0.01', 'ether')).send({gasLimit: 50000});
Enter fullscreen mode Exit fullscreen mode

Recibo:

{
 blockHash: '0xf39c80e703689eab40d9547ffc252304996e3c6004c62e654c513f8a9d03d4a4',
 blockNumber: 3763915n,
 cumulativeGasUsed: 7956770n,
 effectiveGasPrice: 3540322410n,
 from: '0xf79ac5fa7f6e76590ae6ce641ab42b33cab86c47',
 gasUsed: 28559n,
 logs: [],
 logsBloom: '0x0000000000000...00000000000000000000000000',
 status: 1n,
 to: '0x13c96729039f1da4ea42ffe1a7e9cac1cf42801d',
 transactionHash: '0xca5275a466e9acd34c723203d6847e42a4cf49f2156835cd7e9418d572924e59',
 transactionIndex: 55n,
 type: 0n
}
Enter fullscreen mode Exit fullscreen mode

Vemos que o saldo do contrato diminuiu novamente em 0,01 Ether:

Image description

Retirada de 0,01 ETH do saldo do contrato para a conta EOA

Isso é tudo. Conhecemos a especificação ABI de contrato e aprendemos como os dados são realmente codificados ao chamar métodos de contrato. Aprendemos como obter um objeto wrapper em um contrato usando a interface ABI e como interagir com ele a partir do código do aplicativo.

Meus artigos anteriores:

  1. Local environment for learning Web3.js and Ethereum. 2023
  2. Create, sign and send an Ethereum transaction manually using only Web3.js and Ganache. 2023 \ Denis
  3. Connecting to the Ethereum Testnet using only web3.js and the console
  4. Addresses in Ethereum
  5. Smart contract. Solidity + Ganache

Esse artigo foi escrito por Denis Avtsin e traduzido por Fátima Lima. O original pode ser lido aqui.

Top comments (0)