китаев.tech

Typed Data подпись — EIP-712

Как подписывать структурированные данные через EIP-712 в MetaMask и ethers.js v6, а затем проверять подпись через verifyTypedData.

Что даёт EIP-712

EIP-712 — это стандарт подписи структурированных данных.

Вместо длинной строки кошелёк показывает пользователю понятные поля: домен приложения, сеть, контракт и само действие. Так подписывают ордера, разрешения, off-chain заявки и login payload, когда обычного EIP-191 сообщения уже недостаточно.


Подписываем typed data и проверяем адрес

typescript
import { BrowserProvider, verifyTypedData } from "ethers";
import type { Eip1193Provider, TypedDataDomain, TypedDataField } from "ethers";

declare global {
  interface Window {
    ethereum?: Eip1193Provider;
  }
}

const app = document.createElement("main");
app.innerHTML = `
  <button id="sign-typed-data">Подписать typed data</button>
  <pre id="typed-data-info">Typed data не подписаны</pre>
`;
document.body.appendChild(app);

const button = document.querySelector<HTMLButtonElement>("#sign-typed-data");
const output = document.querySelector<HTMLPreElement>("#typed-data-info");

if (!button || !output) {
  throw new Error("Не удалось найти элементы интерфейса");
}

const domain: TypedDataDomain = {
  name: "Kitaev Docs",
  version: "1",
  chainId: 1,
  verifyingContract: "0x0000000000000000000000000000000000000000",
};

const types: Record<string, TypedDataField[]> = {
  Login: [
    { name: "wallet", type: "address" },
    { name: "statement", type: "string" },
    { name: "nonce", type: "uint256" },
    { name: "deadline", type: "uint256" },
  ],
};

async function signTypedData() {
  if (!window.ethereum) {
    output.textContent = "MetaMask не найден. Установите расширение браузера.";
    return;
  }

  const provider = new BrowserProvider(window.ethereum);

  await provider.send("eth_requestAccounts", []);

  const signer = await provider.getSigner();
  const address = await signer.getAddress();

  const value = {
    wallet: address,
    statement: "Я вхожу на kitaev.tech через EIP-712",
    nonce: 1n,
    deadline: 1_798_761_600n,
  };

  const signature = await signer.signTypedData(domain, types, value);
  const recoveredAddress = verifyTypedData(domain, types, value, signature);
  const isValid = recoveredAddress.toLowerCase() === address.toLowerCase();

  output.textContent = [
    `Адрес: ${address}`,
    `Подпись: ${signature}`,
    `Восстановленный адрес: ${recoveredAddress}`,
    `Подпись валидна: ${isValid}`,
  ].join("\n");
}

button.addEventListener("click", () => {
  signTypedData().catch((error: unknown) => {
    const message = error instanceof Error ? error.message : "Неизвестная ошибка";
    output.textContent = `Ошибка typed data подписи: ${message}`;
  });
});

MetaMask покажет пользователю структурированное сообщение: название приложения, сеть, контракт и поля Login.


Как это работает

domain — контекст подписи. Он привязывает подпись к приложению, версии, сети и контракту.

types — схема данных. Здесь описано, какие поля есть у Login и какие у них Solidity-типы.

value — конкретные данные, которые подписывает пользователь.

signer.signTypedData(domain, types, value) — просит MetaMask подписать EIP-712 payload.

verifyTypedData(domain, types, value, signature) — восстанавливает адрес подписанта из тех же данных и подписи.


Чем EIP-712 отличается от EIP-191

EIP-191 подписывает обычную строку. Пользователь видит текст, но кошелёк не знает структуру этого текста.

EIP-712 подписывает типизированные поля:

text
Login.wallet    address
Login.statement string
Login.nonce     uint256
Login.deadline  uint256

Это удобнее для сложных действий: ордеров на DEX, permit-подписей, delegations и off-chain заявок. Пользователь видит не просто фразу, а данные с понятными типами.


Частые ошибки

Менять domain после подписиname, version, chainId и verifyingContract входят в подпись. Если изменить хотя бы одно поле, проверка вернёт другой адрес.

Добавлять EIP712Domain в types → в ethers v6 передавайте только пользовательские типы вроде Login, Order или Permit.

Использовать один nonce повторно → typed data подпись можно переиспользовать, если backend или контракт не проверяет nonce и deadline.


Что дальше

Материалы китаev.tech публикуются в образовательных целях и не являются инвестиционной рекомендацией. Примеры кода и описания протоколов — для обучения; использование в продакшне на ваш собственный риск. Дисклеймер и политика конфиденциальности.