Typed Data подпись — EIP-712
Как подписывать структурированные данные через EIP-712 в MetaMask и ethers.js v6, а затем проверять подпись через verifyTypedData.
Что даёт EIP-712
EIP-712 — это стандарт подписи структурированных данных.
Вместо длинной строки кошелёк показывает пользователю понятные поля: домен приложения, сеть, контракт и само действие. Так подписывают ордера, разрешения, off-chain заявки и login payload, когда обычного EIP-191 сообщения уже недостаточно.
Подписываем typed data и проверяем адрес
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 подписывает типизированные поля:
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.
Что дальше
- Подпись сообщений — EIP-191 — понять базовую подпись обычного текста
- Подключение MetaMask через BrowserProvider — получить signer из браузерного кошелька
- EOA vs Contract Account — разобраться, чей ключ создаёт подпись