Signing Messages
Message signing lets a dApp prove a user controls an address without submitting an on-chain transaction — commonly used for off-chain authentication (e.g. Sign-In With Ethereum).
personal_sign
Signs a UTF-8 message.
const message = 'Welcome to StarKey Wallet'
const hexMessage = '0x' + Buffer.from(message, 'utf8').toString('hex')
const signature: string = await provider.request({
method: 'personal_sign',
params: [hexMessage, accounts[0]], // [message, address] — in this order
})Params: [message: string, address: string] — hex-encoded UTF-8 message, then the signing address. Note the message comes first, address second.
Returns: Promise<string> — a hex-encoded signature.
eth_signTypedData_v4
Signs structured, typed data (EIP-712) instead of a plain string — used for readable, schema’d signing prompts (e.g. permits, order signing).
const typedData = {
domain: { name: 'MyDApp', version: '1', chainId: 1, verifyingContract: contractAddress },
primaryType: 'Message',
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
Message: [{ name: 'contents', type: 'string' }],
},
message: { contents: 'Hello from your dApp' },
}
const signature: string = await provider.request({
method: 'eth_signTypedData_v4',
params: [accounts[0], JSON.stringify(typedData)], // [address, typedData] — in this order
})Params: [address: string, typedData: string] — signing address, then the JSON-stringified EIP-712 typed data object. Note the address comes first here, opposite of personal_sign.
Returns: Promise<string> — a hex-encoded signature.
Verifying a signature
Recover the signing address from the signature and message, then compare it against the expected address — never trust the address without recovering it from the signature itself. This recovery step (ecrecover over the personal_sign-prefixed message hash) is standard ECDSA signature recovery; most Ethereum-facing tooling exposes a “recover address from signature” utility that implements it.
Error handling
- Both methods reject with
code: 4001if the user declines the sign prompt — wrap calls intry/catch. eth_signTypedData_v4throws iftypedDatadoesn’t match the EIP-712 schema shape (e.g. a missingEIP712Domaintype entry) — validate the object before calling, rather than relying on StarKey’s error message alone.
Security considerations
- Always verify a signature server-side before treating it as proof of address ownership — never trust an unverified
signature/addresspair sent from the client. - Include a nonce or timestamp in the signed payload for auth flows, to prevent a captured signature from being replayed later.
- Prefer
eth_signTypedData_v4overpersonal_signfor anything beyond a simple auth message — a structured, domain-scoped prompt is harder to phish a user into approving blindly than an opaque string.
Best practices
- Use
personal_signfor simple human-readable auth messages; useeth_signTypedData_v4when the payload has real structure (permits, orders, or anything with adomain/chainIdbinding). - Recover and compare the signing address (see Verifying a signature) rather than assuming the signature is valid just because the call resolved.