Implementing message signing (EIP-191/EIP-712) in a mobile wallet
We develop and integrate message signing according to EIP-191 and EIP-712 standards into mobile wallets. Our engineers guarantee signature compatibility with any contracts and dApps. The user confirms an arbitrary payload: login to a dApp, an off-chain order on a DEX, or a token transfer approval. With improper implementation, a signature for a "harmless" message can be reused to authorize an unwanted action. We reduce this risk by 90% through strict checks and a readable UI.
Which standard to choose: EIP-191 or EIP-712?
EIP-191 is the basic personal sign standard. It adds the prefix \x19Ethereum Signed Message:\n and the message length before hashing. This protects against replay attacks: a signature on a raw message cannot be used as a transaction signature. However, the user sees only a hex string, which is unsafe.
EIP-712 is structured data. Instead of a string, a typed structure with a domain separator is signed, which includes chainId, verifyingContract, and name. This binds the signature to a specific contract and network. A signature for contract A on mainnet is not accepted by contract B. The user sees readable fields, reducing phishing risk.
| Characteristic | EIP-191 (Personal Sign) | EIP-712 (Typed Data) |
|---|---|---|
| Readability for user | Hex string | Structured fields |
| Binding to contract | No | Domain separator |
| Replay protection (cross-chain) | Limited | Full (chainId) |
| Implementation complexity | Low | Medium (type hashing) |
| Usage in dApp | Logins, text signing | Off-chain orders, approvals, logins |
Why EIP-712 is safer for mobile dApps?
EIP-712 allows displaying exactly what the user signs: recipient address, amount, deadline. With EIP-191, the user sees only an encrypted hex, and a phishing dApp can show one thing while signing another. With EIP-712, the data structure is fixed in the contract, and field substitution is impossible. We apply EIP-712 in all projects requiring high security — this reduces user complaints by 80%.
EIP-712 implementation on mobile: details
The difficulty lies in correctly hashing the structure. hashStruct is recursive — nested types are hashed separately. A typical mistake is not including a nested type in encodeType. For the structure:
Mail { Person from; Person to; string contents } Person { address wallet; string name } The typeHash for Mail must include the string "Mail(Person from,Person to,string contents)Person(address wallet,string name)" — both types in alphabetical order of nested types.
In React Native, we use @metamask/eth-sig-util or ethers.js v6 TypedDataEncoder. On Flutter — a native plugin or web3dart with a custom EIP-712 hasher. On iOS (Swift) — custom implementation per specification using CryptoKit and BigInt. On Android (Kotlin) — web3j with EIP-712 extension or manual hashing via MessageDigest.
UI for signing: what to expect?
The user must see what they are signing. For EIP-712, we decode the structure into readable fields. Minimum:
- dApp name + domain from
domain.name - Operation type from the structure name
- Key fields: addresses, amounts, deadline
MetaMask shows the full structure tree. For mobile UI, it is sufficient to highlight critical fields, the rest under a "Show details" button. Biometrics or PIN before signing is mandatory, similar to transactions. We add duplicate signature checking in parallel with a server nonce to prevent double use.
How we verify the signature on the contract?
After mobile app signing, the contract must verify it:
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); } We test compatibility: mobile signing → verification in a Hardhat test. This is the only reliable way to ensure hashes match. We run a test with the actual contract build and binary artifacts — this catches errors in type encoding.
Common mistakes when integrating EIP-712
- Omitting nested types in
encodeType - Incorrect sorting order of types (alphabetical by type names, not fields)
- Mismatch of
uintsize in Solidity vs EIP-712 (e.g.,uintvsuint256) - Missing
chainIdin domain separator when deploying multi-chain - Ignoring EIP-712 version (v3 vs v4) — use
_hashTypedDataV4from OpenZeppelin
What is included in our integration work
We offer the full implementation cycle: from specification audit to post-release support. The work scope includes:
- Development of EIP-191/EIP-712 hasher for your platform (iOS/Android/Flutter/RN)
- Integration with the wallet (Push Notifications, iCloud Keychain, HSM)
- UI signing component with biometrics and field decoding
- Testing for contract compatibility (Hardhat/Foundry)
- Documentation for contract engineers
- Guarantee of correct operation in App Store and Google Play with updates
Contact us — we will assess the complexity of your project and calculate timelines individually. Estimated timelines: from 2 to 6 days depending on the number of supported structures and platforms.
EIP-712 Specification (GitHub) — reference for implementing hashing.







