В предыдущем посте мы обсуждали метатранзакции и стандарт 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 поддерживает его и может использоваться прямо сейчас. Есть три шага — создать сигнатуру функции, создать транзакцию и подписать ее. После этого его можно отправить на смарт-контракт, а плату за газ может оплатить кто-то другой.

Ссылки