Skip to content

Sign a Message

Message signing is how a wallet proves ownership of an address. In the context of ERC-4337, every UserOperation must be signed by the wallet’s owner before the bundler will accept it.

signMessage() opens the widget, shows the user a “Sign Request” screen, and — upon approval — calls AWS KMS to produce a 65-byte ECDSA signature.

const { signature } = await auth.signMessage('0xdeadbeef...');

The message parameter accepts:

  • A hex string ("0x...")
  • A Uint8Array
  • A viem-compatible { raw: Uint8Array | string } object

The most common use case is signing an ERC-4337 UserOperation hash before submitting it to a bundler:

// Compute the hash from a UserOperation (implementation depends on your SDK)
const userOpHash = computeUserOpHash(userOp);
// Sign it
const { signature } = await auth.signMessage(userOpHash);
// Attach the signature
userOp.signature = signature;

When the widget opens, it shows the network name in the signing approval screen. By default, it uses the network or chainId you set at construction. You can override it per-call:

const { signature } = await auth.signMessage(hash, 'Base');

The SDK normalizes incoming message formats to a 0x-prefixed hex string before passing it to the widget. The widget sends the hash to KMS, which signs it with the user’s private key and returns a 65-byte signature (r + s + v).

One important detail: when OxGasClient uses signing internally (for smart account UserOperations), it applies the EIP-191 prefix before sending the hash to KMS. This is because the smart contract’s validateUserOp function calls ECDSA.recover(userOpHash.toEthSignedMessageHash(), signature) — meaning the contract adds the prefix on-chain, so the SDK must match it off-chain.

If you’re using signMessage() directly for your own purposes, pass the exact hash you want signed without any prefix. KMS will sign it as-is.

Signing waits up to 2 minutes for the user to approve or reject (configurable via signTimeout). If the user doesn’t respond, signMessage() rejects with a SigningTimeoutError.

interface SignMessageResult {
signature: `0x${string}`; // 65-byte ECDSA signature
}
import { SigningRejectedError, SigningTimeoutError } from '0xgas-auth';
try {
const { signature } = await auth.signMessage(hash);
} catch (err) {
if (err instanceof SigningRejectedError) {
console.log('User rejected');
} else if (err instanceof SigningTimeoutError) {
console.log('Timed out after', err.timeoutMs, 'ms');
}
}