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
- Configuração do Ambiente
- Criação do Contrato
- Interação com o contrato
- 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
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);
}
}
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
Agora vamos compilar o contrato e obter sua representação binária:
$ solc --bin Faucet.sol
======= Faucet.sol:Faucet =======
Binary:
608060405234801561000f575f80fd5b506101468061001d5f395ff3fe608060405260043610610021575f3560e01c80632e1a7d4d1461002c57610028565b3661002857005b5f80fd5b348015610037575f80fd5b50610052600480360381019061004d91906100e5565b610054565b005b662386f26fc10000811115610067575f80fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc8290811502906040515f60405180830381858888f193505050501580156100aa573d5f803e3d5ffd5b5050565b5f80fd5b5f819050919050565b6100c4816100b2565b81146100ce575f80fd5b50565b5f813590506100df816100bb565b92915050565b5f602082840312156100fa576100f96100ae565b5b5f610107848285016100d1565b9150509291505056fea2646970667358221220c9fe2f78a923e108f0619a2d655b35d18297668335e649a10272c09a790a188964736f6c63430008140033
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
Conecte-se à rede de teste Sepolia:
> const { Web3 } = require('web3');
> const web3 = new Web3('https://rpc2.sepolia.org');
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');
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'
]
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';
Tentando enviar a transação:
> await web3.eth.sendTransaction({from: '0xf79ac5fa7F6e76590Ae6cE641aB42b33CAB86C47', gasLimit: 21000, data: contractCode});
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
}
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:
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:
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});
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
}
O contrato foi criado com sucesso, aqui está seu endereço:
0x13c96729039f1da4ea42ffe1a7e9cac1cf42801d
Conta do contrato:
Conta do contrato criada com sucesso
Código do contrato:
Conta do contrato e seu código
A transação que criou o contrato:
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});
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
}
O saldo aumentou 0.1 Ether:
O saldo do contrato aumentou 0,1 Ether
Transação que transferiu 0,1 Ether para o saldo do contrato:
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)
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'
e pegue os primeiros 4 bytes do hash calculado (um byte tem dois caracteres hexadecimais, sem contar o prefixo 0x):
0x2e1a7d4d
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'
Em seguida, converta esse valor para o formato hexadecimal:
> web3.utils.toHex(10000000000000000);
// Saída:
'0x2386f26fc10000'
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
E agora colocamos o argumento após o seletor de função:
0x2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000
Ó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'});
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
}
A transação foi bem-sucedida. Chamamos o método withdraw() de uma conta externa e recebemos 0,01 Ether.
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`'
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
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"}]
ABI formatada:
[
{
"inputs": [
{
"internalType": "uint256",
"name": "withdraw_amount",
"type": "uint256"
}
],
"name": "withdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]
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"}]');
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'});
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});
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
}
Vemos que o saldo do contrato diminuiu novamente em 0,01 Ether:
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:
- Local environment for learning Web3.js and Ethereum. 2023
- Create, sign and send an Ethereum transaction manually using only Web3.js and Ganache. 2023 \ Denis
- Connecting to the Ethereum Testnet using only web3.js and the console
- Addresses in Ethereum
- Smart contract. Solidity + Ganache
Esse artigo foi escrito por Denis Avtsin e traduzido por Fátima Lima. O original pode ser lido aqui.
Oldest comments (0)