OWS currently has three EVM signing functions, each with a specific prefix/encoding:
┌─────────────────┬─────────────────────────────────────────────────────────────────┐
│ Function │ What it does internally │
├─────────────────┼─────────────────────────────────────────────────────────────────┤
│ signMessage │ Wraps with EIP-191 prefix, then │
│ │ secp256k1.sign(keccak256(\x19Ethereum...\n + msg)) │
├─────────────────┼─────────────────────────────────────────────────────────────────┤
│ signTypedData │ Wraps with EIP-712 prefix, then │
│ │ secp256k1.sign(keccak256(\x19\x01 + domain + struct)) │
├─────────────────┼─────────────────────────────────────────────────────────────────┤
│ signTransaction │ Hashes raw tx bytes, then secp256k1.sign(keccak256(txBytes)) │
└─────────────────┴─────────────────────────────────────────────────────────────────┘
EIP-7702 (account delegation) needs to sign an authorization tuple:
secp256k1.sign(keccak256(0x05 || rlp([chainId, address, nonce]))). This is a raw hash
signing operation — no EIP-191 prefix, no EIP-712 domain, not a transaction.
There's no OWS function for this. You can't use signMessage because it always adds the
EIP-191 prefix, which changes the hash and produces an invalid signature.
Current workaround
signTransaction happens to work because it doesn't validate the input format — it just
hashes whatever bytes you give it and signs. So passing the authorization payload (0x05
|| rlp([...])) as a "transaction" produces a valid authorization signature. But this
is a hack that relies on implementation details and could break if OWS adds transaction
format validation.
What EIP-7702 needs vs what OWS offers today
┌───────────────┬────────────────────────────────┬────────────────────┬───────────┐
│ Operation │ Signing primitive needed │ OWS function │ Status │
├───────────────┼────────────────────────────────┼────────────────────┼───────────┤
│ Authorization │ secp256k1.sign(keccak256(0x05 │ None (workaround: │ Gap │
│ signing │ || rlp([...]))) │ signTransaction) │ │
├───────────────┼────────────────────────────────┼────────────────────┼───────────┤
│ Type-4 tx │ secp256k1.sign(keccak256(0x04 │ signTransaction │ Supported │
│ signing │ || rlp([...]))) │ │ │
├───────────────┼────────────────────────────────┼────────────────────┼───────────┤
│ Paymaster │ EIP-712 typed data │ signTypedData │ Supported(after #148 is merged) │
│ permits │ │ │ │
├───────────────┼────────────────────────────────┼────────────────────┼───────────┤
│ UserOp hash │ EIP-712 typed data │ signTypedData │ Supported(after #148 is merged) │
│ signing │ │ │ │
└───────────────┴────────────────────────────────┴────────────────────┴───────────┘
Proposed solution
Add a signHash function that does raw secp256k1.sign() on a 32-byte hash without any
prefix or transformation:
function signHash(
wallet: string,
chain: string,
hash: string, // 32-byte hex-encoded hash
passphrase?: string, // passphrase or API key token
index?: number,
vaultPath?: string
): SignResult;
This would support EIP-7702 today and any future EIP that introduces a new signing
prefix (the Ethereum ecosystem has done this for EIP-191, EIP-712, EIP-7702, and may do
it again). Each time a new prefix is added, signHash just works — the caller computes
the hash however the EIP specifies and passes it in.
Why not just add signAuthorization?
A dedicated signAuthorization(chainId, address, nonce) function would also work but
only solves EIP-7702. A generic signHash is more future-proof and covers any raw
signing need. **Both would be useful — signAuthorization as a convenience, signHash as
the primitive.**
TL;DR: There's no clean way to sign EIP-7702 authorizations at the moment. The only workaround is leverage the fact that
signTransactionhappens to work because it doesn't validate the input format. Add asignHashmethod and optionally a conveniencesignAuthorization.