В предыдущем посте мы обсуждали метатранзакции и стандарт EIP-712. На этот раз мы рассмотрим, как подписать транзакцию с библиотекой Ethers.js и выполнить ее в смарт-контракте. Это означает, что плату за газ будет платить кто-то другой.
Упаковать транзакцию
Сначала нам нужно создать транзакцию и упаковать ее со всеми необходимыми компонентами по стандарту EIP-712. Нам понадобятся три основные части:
- определить используемые типы в сообщении;
- информация о домене для проверки транзакции в смарт-контракте;
- сообщение транзакции, которое содержит данные.
Создать неподписанную транзакцию
Для создания неподписанной транзакции при вызове функции смарт-контракта нам нужно использовать функцию populateTransaction
из библиотеки EthersJS. Он возвращает неподписанную транзакцию, которую необходимо подписать и отправить в сеть блокчейн.
Давайте создадим функцию смарт-контракта, которая установит текущего сотрудника.
struct EmployeeData { uint256 employeeId; string employeeName; } EmployeeData private currentEmployee; function set(uint256 id, string memory name) external { currentEmployee = EmployeeData(id, name); }
При создании транзакции нам необходимо указать все параметры функции и выполнить ее.
const transaction = await employee.populateTransaction.set(1, 'John');
Он вернет объект транзакции, который содержит данные, которые являются сигнатурой функции, адресом from
, адресом смарт-контракта to
и лимитом газа. Нас в первую очередь интересует сигнатура функции, которая представляет собой шестнадцатеричное значение. В начале это имя хэш-функции, за которым следуют все значения параметров.
{ data: '0x64371977...00', to: '0x5FbDB2315678afecb367f032d93F642f64180aa3', from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', gasLimit: BigNumber { value: "29022232" } }
Собираем сообщение
Начнем с объекта типов, который состоит всего из трех частей:
- одноразовый номер, чтобы избежать дублирования транзакций;
- с адреса подписавшего транзакцию;
- подпись функции, которую мы уже создали.
const types = { "MetaTransaction": [ { "name": "nonce", "type": "uint256" }, { "name": "from", "type": "address" }, { "name": "functionSignature", "type": "bytes" } ] };
Далее идет объект предметной области, в котором определены несколько частей:
- имя домена для отличия транзакции в другом смарт-контракте;
- стандартная версия EIP-712, которая в нашем случае будет 1;
- проверка адреса смарт-контракта, который мы можем получить из неподписанной транзакции;
salt
— это шестнадцатеричное значение идентификатора цепочки, дополненное0
, пока его длина не станет 64 символа и не начнется с0x
.
const salt = ethers.utils.hexZeroPad(ethers.utils.hexValue(network.config.chainId ?? 0), 32); const domain = { name: "Employee", version: "1", verifyingContract: transaction.to, salt: salt };
Наконец, мы можем создать объект сообщения, но сначала нам нужно получить одноразовый номер из смарт-контракта блокчейна. Для этого нам нужно передать адрес подписавшего. Используя одноразовый номер, мы избегаем ситуации дублирования транзакций.
const nonce = await employee.getNonce(signer.address);
Объект сообщения состоит из трех частей:
- одноразовый номер, который предоставляется смарт-контрактом;
from
адрес, который является адресом подписывающей стороны;- подпись функции, которую мы получили ранее из неподписанной транзакции.
const message = { "nonce": nonce, "from": signer.address, "functionSignature": transaction.data ?? '' };
Подпишите транзакцию
Мы можем подписать транзакцию теперь, когда у нас есть все необходимые части, определенные в стандарте EIP-712, который описывает, как типы структурированы и хешированы.
В библиотеке Ethers.js есть функция _signTypedData
, которая подписывает данные согласно спецификации EIP-712. Это все еще экспериментальная функция, и она будет переименована без подчеркивания, но многие проекты уже используют ее в производстве.
const signature = await signer._signTypedData(domain, types, message);
Подтвердить подпись
В результате мы получаем хешированную подпись, из которой мы можем получить подписавшего и убедиться, что она верна. В библиотеке Ethers.js есть вспомогательная функция verifyTypedData
, которая проверяет данные транзакции и подпись. Взамен мы получаем адрес кошелька подписывающей стороны.
const signerAddress = ethers.utils.verifyTypedData( domain, types, message, signature );
Мы подписали транзакцию, и она готова к отправке в блокчейн и выполнению другим держателем кошелька. Это означает, что нашим пользователям не нужно платить за газ, но мы можем оплачивать и выполнять транзакции от их имени. Мы рассмотрим, как это сделать, в одном из следующих постов.
TL;DR
Подписать транзакцию в соответствии со стандартом EIP-712 с библиотекой Ethers JS очень просто. К счастью, эта библиотека JavaScript поддерживает его и может использоваться прямо сейчас. Есть три шага — создать сигнатуру функции, создать транзакцию и подписать ее. После этого его можно отправить на смарт-контракт, а плату за газ может оплатить кто-то другой.