Ethereum Contract ABI Specification. Взаимодействие с контрактом | End Way - форум программирования и сливов различных скриптов
  • Присоединяйтесь к нам в телеграм канал! EndWay канал | EndSoft канал | EWStudio канал
  • Хочешь поставить скрипт, но не умеешь?
    А может ты хочешь свой уникальный скрипт?

    Тогда добро пожаловать в нашу студию разработки!

    Телеграм бот: EWStudioBot
    Телеграм канал: EWStudio

Ethereum Contract ABI Specification. Взаимодействие с контрактом

ZiZCoder

Website: https://zizcode.pro/
Автор темы
30 Июн 2023
25
28
13
Создание контракта и взаимодействие с ним

Создание контракта и взаимодействие с ним

В данной статье я хочу познакомить вас с тем, как осуществляется кодирование данных в транзакции в соответствии с Contract ABI Specification. Мы вручную разберём весь процесс кодирования, создадим контракт и произведём вызов его методов. В конце я покажу как при помощи Contract ABI создать объект-оболочку через web3.js, и через него вызывать методы контракта.



План

  1. Настройка окружения
  2. Создание контракта
  3. Взаимодействие с контрактом
  4. Объект-оболочка над контрактом
Настройка окружения

Нам потребуются: компилятор Solidity, сам контракт, подключение к тестовой сети Sepolia и аккаунт с тестовыми Ether на балансе. Так же нам необходимо будет добавить приватный ключ этого аккаунта в Wallet библиотеки web3.js



Начнём с компилятора Solidity. Существуют разные способы установки компилятора Solidity, всё зависит от вашей ОС и каким способом вы хотите его установить: npm, Docker, Linux Packages и т.д. Как установить компилятор можно посмотреть здесь.



Создадим рабочий каталог, установим web3.js и добавим в него наш контракт. Версия web3.js на момент написания статьи 4.0.1

JavaScript:
 $ mkdir raw-contract

$ cd raw-contract

$ npm install web3

$ nano Faucet.sol

Код контракта:

JavaScript:
 // SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.8.0 <0.9.0;

contract Faucet {

// Accept any incoming amount

receive() external payable {}

<span class="hljs-comment" style="transition: opacity 0.2s ease-in-out 0s, color 0.2s ease-in-out 0s, text-decoration 0.2s ease-in-out 0s, background-color 0.2s ease-in-out 0s, -webkit-text-decoration 0.2s ease-in-out 0s; color: rgb(142, 144, 140); quotes: &quot;«&quot; &quot;»&quot;;">// Give out ether to anyone who asks</span>

<span class="hljs-function" style="transition: opacity 0.2s ease-in-out 0s, color 0.2s ease-in-out 0s, text-decoration 0.2s ease-in-out 0s, background-color 0.2s ease-in-out 0s, -webkit-text-decoration 0.2s ease-in-out 0s; quotes: &quot;«&quot; &quot;»&quot;;"><span class="hljs-keyword" style="transition: opacity 0.2s ease-in-out 0s, color 0.2s ease-in-out 0s, text-decoration 0.2s ease-in-out 0s, background-color 0.2s ease-in-out 0s, -webkit-text-decoration 0.2s ease-in-out 0s; font-weight: 700; color: rgb(137, 89, 168); quotes: &quot;«&quot; &quot;»&quot;;">function</span> <span class="hljs-title" style="transition: opacity 0.2s ease-in-out 0s, color 0.2s ease-in-out 0s, text-decoration 0.2s ease-in-out 0s, background-color 0.2s ease-in-out 0s, -webkit-text-decoration 0.2s ease-in-out 0s; color: rgb(66, 113, 174); font-weight: 700; quotes: &quot;«&quot; &quot;»&quot;;">withdraw</span>(<span class="hljs-params" style="transition: opacity 0.2s ease-in-out 0s, color 0.2s ease-in-out 0s, text-decoration 0.2s ease-in-out 0s, background-color 0.2s ease-in-out 0s, -webkit-text-decoration 0.2s ease-in-out 0s; color: rgb(245, 135, 31); quotes: &quot;«&quot; &quot;»&quot;;">uint withdraw_amount</span>) <span class="hljs-title" style="transition: opacity 0.2s ease-in-out 0s, color 0.2s ease-in-out 0s, text-decoration 0.2s ease-in-out 0s, background-color 0.2s ease-in-out 0s, -webkit-text-decoration 0.2s ease-in-out 0s; color: rgb(66, 113, 174); font-weight: 700; quotes: &quot;«&quot; &quot;»&quot;;">public</span> </span>{

<span class="hljs-comment" style="transition: opacity 0.2s ease-in-out 0s, color 0.2s ease-in-out 0s, text-decoration 0.2s ease-in-out 0s, background-color 0.2s ease-in-out 0s, -webkit-text-decoration 0.2s ease-in-out 0s; color: rgb(142, 144, 140); quotes: &quot;«&quot; &quot;»&quot;;">// Limit withdrawal amount</span>

<span class="hljs-built_in" style="transition: opacity 0.2s ease-in-out 0s, color 0.2s ease-in-out 0s, text-decoration 0.2s ease-in-out 0s, background-color 0.2s ease-in-out 0s, -webkit-text-decoration 0.2s ease-in-out 0s; color: rgb(245, 135, 31); quotes: &quot;«&quot; &quot;»&quot;;">require</span>(withdraw_amount &lt;= <span class="hljs-number" style="transition: opacity 0.2s ease-in-out 0s, color 0.2s ease-in-out 0s, text-decoration 0.2s ease-in-out 0s, background-color 0.2s ease-in-out 0s, -webkit-text-decoration 0.2s ease-in-out 0s; color: rgb(245, 135, 31); quotes: &quot;«&quot; &quot;»&quot;;">0.01</span> ether);



<span class="hljs-comment" style="transition: opacity 0.2s ease-in-out 0s, color 0.2s ease-in-out 0s, text-decoration 0.2s ease-in-out 0s, background-color 0.2s ease-in-out 0s, -webkit-text-decoration 0.2s ease-in-out 0s; color: rgb(142, 144, 140); quotes: &quot;«&quot; &quot;»&quot;;">// Send the amount to the address that requested it</span>

payable(msg.sender).transfer(withdraw_amount);

}



}

Контракт я взял из своего примера, когда мы деплоили его в локальный блокчейн Ganache. Логика контракта позволяет зачислять Ether на его баланс, и даёт возможность каждому снять в свою пользу по 0.01 Ether за одну транзакцию.



На всякий случай проверим версию компилятора, она должна быть 0.8.x:

JavaScript:
 $ solc --version

// out:

solc, the solidity compiler commandline interface

Version: 0.8.20+commit.a1b79de6.Darwin.appleclang

Теперь скомпилируем контракт и получим его бинарное представление:

JavaScript:
 $ solc --bin Faucet.sol

======= Faucet.sol:Faucet =======

Binary:

608060405234801561000f575f80fd5b506101468061001d5f395ff3fe608060405260043610610021575f3560e01c80632e1a7d4d1461002c57610028565b3661002857005b5f80fd5b348015610037575f80fd5b50610052600480360381019061004d91906100e5565b610054565b005b662386f26fc10000811115610067575f80fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc8290811502906040515f60405180830381858888f193505050501580156100aa573d5f803e3d5ffd5b5050565b5f80fd5b5f819050919050565b6100c4816100b2565b81146100ce575f80fd5b50565b5f813590506100df816100bb565b92915050565b5f602082840312156100fa576100f96100ae565b5b5f610107848285016100d1565b9150509291505056fea2646970667358221220c9fe2f78a923e108f0619a2d655b35d18297668335e649a10272c09a790a188964736f6c63430008140033

То что мы получили, это бинарное представление контракта, которое мы добавим в поле data транзакции и отправим в сеть Ethereum, после чего наш контракт будет задеплоен.



Подготовим подключение к сети Ethereum, для этого зайдем в консоль node.js:



$ node

Подключимся к тестовому блокчейну Sepolia:

JavaScript:
 > const { Web3 } = require('web3');

> const web3 = new Web3('https://rpc2.sepolia.org');

Чтобы web3.js смог подписать нашу транзакцию, мы должны добавить приватный ключ аккаунта, с которого будем отправлять эту транзакцию. Передадим в метод add() приватный ключ:

JavaScript:
 > web3.eth.accounts.wallet.add('0x0e...e3');

Вывод:



JavaScript:
 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'

]

Для отправки транзакции я воспользуюсь своим существующим аккаунтом и тестовыми Ether на балансе. Если у вас нет своего аккаунта, то вы можете с лёгкостью его создать одной командой, и пополнить тестовыми Ether.



У нас всё готово для отправки транзакции на создание контракта.



Создание контракта

Добавим префикс 0x к началу кода контракта и поместим его в переменную:



JavaScript:
 > var contractCode = '0x608060405234801561000f575f80fd5b506101468061001d5f395ff3fe608060405260043610610021575f3560e01c80632e1a7d4d1461002c57610028565b3661002857005b5f80fd5b348015610037575f80fd5b50610052600480360381019061004d91906100e5565b610054565b005b662386f26fc10000811115610067575f80fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc8290811502906040515f60405180830381858888f193505050501580156100aa573d5f803e3d5ffd5b5050565b5f80fd5b5f819050919050565b6100c4816100b2565b81146100ce575f80fd5b50565b5f813590506100df816100bb565b92915050565b5f602082840312156100fa576100f96100ae565b5b5f610107848285016100d1565b9150509291505056fea2646970667358221220c9fe2f78a923e108f0619a2d655b35d18297668335e649a10272c09a790a188964736f6c63430008140033';

Пробуем отправить транзакцию:



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

В данном случае мы получили ошибку:



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

}

Всё дело в том что нам не хватило Gas для отправки транзакции. В коде сообщения мы видим, что было проставлено 21000, но требовалось 58368. Что самое интересное, если мы установим требуемое значение в 58368, то это не значит, что мы успешно создадим контракт. Этого хватит лишь для отправки транзакции, но не хватит для создания самого контракта в EVM, и мы увидим ситуацию как здесь:

Неудачная транзакция по созданию контракта

Неудачная транзакция по созданию контракта


Упрощённо, что здесь произошло: транзакция попала в сеть Ethereum, распространилась по нодам, и была помещена в Mempool на них. Proof of Stake алгоритм выбрал ноду, которая в данный момент будет валидировать, исполнять и помещать транзакции из Mempool в новый блок. Нашей транзакции посчастливилось, и она была выбрана нодой для добавления в блок.



Нода взяла транзакцию в обработку и запустила на своей локальной EVM код который находился в поле data. В процессе исполнения был создан аккаунт для контракта, и начался процесс деплоя контракта в storage этого аккаунта. В ходе деплоя было обнаружено, что в транзакции недостаточно Gas для завершения процесса, и возникла ошибка: Out of Gas error.



В итоге у нас появилась ситуация при которой аккаунт под контракт был создан, а его код не был сохранён в storage аккаунта. К тому же мы потеряли 21000 Gas, которые были использованы при исполнении кода, так как нода потратила свои вычислительные ресурсы на исполнение этого кода. Как видим, операции создания аккаунта контракта и его деплоя не атомарны в сети Ethereum. Изменения из storage аккаунта откатились, но сам созданный аккаунт так и остался в блокчейне с нулём вместо кода контракта:



Аккаунт контракта после неудачного деплоя

Аккаунт контракта после неудачного деплоя


Чтобы не воспроизводить описанный сценарий, а так же не вычислять точное значение Gas для деплоя контракта, установим gasLimit с запасом. Оставшийся после деплоя контракта Gas вернётся на баланс нашего аккаунта. Более точное количество Gas можно узнать путём деплоя контракта в локальном блокчейне, например Ganache, или же задеплоить его в RemixIDE, а потом посмотреть количество использованного Gas. При использовании библиотек можно воспользоваться вспомогательными функциями, которые позволяют вычислить требуемое количество Gas до проведения транзакции.



Итак, установим gasLimit с запасом, и отправим транзакцию на создание контракта:



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

Квитанция транзакции:



JavaScript:
 {

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

}

Контракт успешно создан, вот его адрес:



0x13c96729039f1da4ea42ffe1a7e9cac1cf42801d

Аккаунт контракта:


Аккаунт созданного контракта

Аккаунт созданного контракта

Код контракта:



Аккаунт контракта и его код

Аккаунт контракта и его код

Транзакция, которая создала контракт:


Транзакция создавшая контракт

Транзакция создавшая контракт

Отлично, раз контракт создан, то давайте тогда обратимся к его методу. Например пополним баланс контракта.



Взаимодействие с контрактом

Я использовал фреймворк Truffle для упрощения взаимодействия с контрактом. Мы получали объект-оболочку и через него вызывали методы контракта. На этот раз мы будем вызывать методы путём отправки транзакций с закодированным вызовом метода в поле data.



К счастью, для пополнения баланса контракта, нам не нужно ничего дополнительно кодировать, так как у нас есть fallback функция receive(), которая отработает при поступлении обычной транзакции без поля data, и зачислит Ether из поля value на баланс контракта.

Итак, отправим 0.1 Ether на контракт. GasLimit тоже установим с небольшим запасом:



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

Квитанция:



JavaScript:
 {

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

}

Баланс пополнился на 0.1 Ether:


Пополнение баланса контракта на 0.1 ETH

Пополнение баланса контракта на 0.1 ETH

Транзакция на пополнение баланса контракта:

Транзакция, пополнившая баланс контракта на 0.1 ETH

Транзакция, пополнившая баланс контракта на 0.1 ETH

Отлично, а теперь самое интересное. Посмотрим как вызывать обычную функцию, а в нашем контракте это функция withdraw(), в понятной для протокола Ethereum форме.



Чтобы осуществить вызов метода, нам необходимо закодировать сигнатуру метода и её аргументы. Закодированная сигнатура метода в документации Solidity называется function selector.



Сигнатурой метода в Solidity являются: имя функции + типы аргументов в скобках через запятую и без пробелов. В нашем случае сигнатура выглядит следующим образом:



withdraw(uint256)

Чтобы получить function selector, вычислим Keccak-256 хэш от сигнатуры метода:



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

// Out:

'0x2e1a7d4d13322e7b96f9a57413e1525c250fb7a9021cf91d1540d5b69f16a49f'

и возьмём первые 4 байта от вычисленного хэша (один байт это два hex-символа не считая 0x префикса):



0x2e1a7d4d

Это и есть function selector. Теперь осталось закодировать сам аргумент, в нашем случае это 0.01 Ether. Для этого сначала сконвертируем 0.01 Ether в Wei, так как протокол Ethereum оперирует значениями в Wei:



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

// Out:

'10000000000000000'

Затем сконвертируем полученное значение в шестнаддатеричную форму:

JavaScript:
 > web3.utils.toHex(10000000000000000);

// Out:

'0x2386f26fc10000'

Добавим паддинг слева. Поскольку мы использовали тип uint256, а его размер равен 256 бит или 32 байта, то и отправить мы должны число длиной 256 бит. Для этого нам необходимо добавить нули слева, чтобы число в итоге имело размер 256 бит, или длину в 64 символа. Соответственно к нашим 14 символам добавим ещё 50 нулей слева:



000000000000000000000000000000000000000000000000002386f26fc10000

И теперь поместим сам аргумент после function selector:



0x2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000

Все. Наш вызов метода withdraw() на снятие 0.01 Ether с баланса контракта готов. Теперь поместим его в поле data транзакции и отправим её:



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

Квитанция:



JavaScript:
{



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



}

Отлично. Транзакции прошла успешно, с контракта списалось 0.01 Ether и зачислилось на баланс аккаунта, с которого был вызван метод withdraw().



Списание 0.01 ETH с баланс контракта на EOA аккаунт

Списание 0.01 ETH с баланс контракта на EOA аккаунт

Кстати, кодирование можно было осуществить и при помощи готовых методов в web3.js:



JavaScript:
> web3.eth.abi.encodeFunctionSignature('withdraw(uint256)');



// out:



'0x2e1a7d4d'



> web3.eth.abi.encodeParameter('uint256', '10000000000000000');



// out:



'0x000000000000000000000000000000000000000000000000002386f26fc10000'

Но суть была в том, чтобы показать как именно осуществляется кодирование.



Объект-оболочка над контрактом

Выше мы разобрали процесс ручного кодирования данных для взаимодействия с контрактом. Обычно при разработке Dapp приложений взаимодействие с контрактом осуществляется при помощи таких библиотек как: Truffle, web3.js, ethers.js, Web3.py, web3j. Все эти библиотеки позволяют обращаться к контракту из кода приложения как к обычному объекту путём вызова его методов. Всё необходимое кодирование данных и отправку транзакции эти объекты берут на себя. Ниже мы рассмотрим как в web3.js можно получить такой объект, и при помощи него произведём вызов метода контракта.



Для создания объекта контракта в web3.js нам понадобится ABI (Application Binary Interface) контракта, который представляет собой описание методов контракта, типов данных и прочей информации, необходимой библиотекам для взаимодействия с контрактом.



Сам ABI в json формате мы можем получить следующим образом:



JavaScript:
$ solc Faucet.sol --abi

Вывод:



JavaScript:
======= 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"}]

JavaScript:
Отформатированный ABI:







[



{



"inputs": [



{



"internalType": "uint256",



"name": "withdraw_amount",



"type": "uint256"



}



],



"name": "withdraw",



"outputs": [],



"stateMutability": "nonpayable",



"type": "function"



},



{



"stateMutability": "payable",



"type": "receive"



}



]

Здесь мы видим метод withdraw и данные о нём, а так же fallback функцию receive, которая сообщает, что контракт может принимать Ether на свой адрес.



Этот json передаётся в конструктор объекта, через который мы будем взаимодействовать с контрактом, далее сам объект уже будет выполнять операции по кодированию вызовов методов контракта.



Сконвертируем json в JavaScript объект:



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

Передадим описание и адрес контракта в конструктор, и получим сам объект контракта:



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

Обратим внимание на объект с полем from - так мы указали контракту с какого аккаунта по-умолчанию будут происходить вызовы в его адрес.



Вызов метода будет выглядеть следующим образом:



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

Квитанция:



JavaScript:
{



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



}

Видим, что снова списалось 0.01 Ether:


Списание 0.01 ETH с баланс контракта на EOA аккаунт

Списание 0.01 ETH с баланс контракта на EOA аккаунт

На этом всё. Мы познакомились с Contract ABI Specification и узнали как на самом деле происходит кодирование данных при вызове методов контракта. Научились при помощи ABI интерфейса получать объект-обёртку над контрактом и взаимодействовать с ним из кода приложения.
Благодарность выражаю - wakarimasen, именно с ним создавали первые дрейнеры.


Надеюсь вам это поможет и вы найдете применение слитым дрейнерам с помощью моей статьи, разжевали как могли):Ast
 
Like
  • 5
Реакции: 4 users
Активность:
Пока что здесь никого нет