Реализация подписания сообщений (EIP-191/EIP-712) в мобильном кошельке
Подписание сообщений — это не подписание транзакции. Пользователь подтверждает произвольный payload: логин в dApp, офчейн ордер на DEX, разрешение на трансфер токенов. При неправильной реализации подпись под «безобидным» сообщением может быть переиспользована для авторизации нежелательного действия.
EIP-191 vs EIP-712 — в чём разница на практике
EIP-191 — базовый стандарт. Personal sign добавляет префикс "\x19Ethereum Signed Message:\n" + len(message) перед хэшированием. Это защищает от replay-атаки: подпись под сырым сообщением нельзя переиспользовать как подпись транзакции, потому что форматы различаются.
keccak256("\x19Ethereum Signed Message:\n" + message.length + message)
EIP-712 — структурированные данные. Вместо строки подписывается типизированная структура:
keccak256("\x19\x01" + domainSeparator + hashStruct(message))
domainSeparator включает chainId, verifyingContract и name — это привязывает подпись к конкретному контракту и сети. Подпись, сделанная для контракта A в Ethereum mainnet, не принимается контрактом B или той же сетью тестнет.
Реализация EIP-712 на мобильном
Сложность в корректном хэшировании структур. hashStruct рекурсивен — вложенные типы хэшируются отдельно. Типичная ошибка — не включить вложенный тип в encodeType:
Для структуры:
Mail { Person from; Person to; string contents }
Person { address wallet; string name }
typeHash для Mail должен включать строку "Mail(Person from,Person to,string contents)Person(address wallet,string name)" — оба типа в алфавитном порядке вложенных.
В React Native используем @metamask/eth-sig-util или ethers.js v6 TypedDataEncoder. На Flutter — нативный плагин или web3dart с кастомным EIP-712 хэшером (готовых решений мало, придётся писать самому по спецификации).
UI для подписи сообщения
Пользователь должен видеть что именно подписывает. Для EIP-712 — декодированная структура с читаемыми полями, не hex-строка. Минимум:
- Имя dApp + домен из
domain.name - Тип операции из типов структуры
- Ключевые поля: адреса, суммы, deadline
MetaMask показывает полное дерево структуры. Для мобильного UI достаточно выделить критичные поля, остальное — под кнопкой «Показать детали».
Биометрия / PIN перед подписью — обязательны, аналогично транзакциям.
Верификация на контракте
После подписи мобильным приложением контракт должен это верифицировать:
function verify(address signer, Mail calldata mail, bytes calldata signature) public view returns (bool) {
bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
keccak256("Mail(address from,address to,string contents)"),
mail.from, mail.to,
keccak256(bytes(mail.contents))
)));
return signer == ECDSA.recover(digest, signature);
}
Тестируем совместимость: мобильная подпись → верификация в Hardhat-тесте. Это единственный надёжный способ убедиться, что хэши совпадают.
Сроки — 2–3 дня: EIP-191 для personal sign, EIP-712 для конкретных структур, UI с декодированием, биометрическое подтверждение, тест в контракте.







