Sign Messages
Sign arbitrary messages with the user's passkey. Same API across Bitcoin + EVM, different wire format per chain.
signMessage produces a cryptographic signature over an arbitrary string, authenticated by the user's passkey. Works on every chain family — the signature format is dispatched by the session's network type.
import { signMessage } from '@oviato/connect';
const r = await signMessage('Hello, Oviato!');
if (r.status === 'success' && r.type === 'signMessageSuccess') {
console.log(r.data.signature);
}What you get back
type Result =
| { status: 'success'; type: 'signMessageSuccess'; data: { signature: string; message: string; publicKey: string } }
| { status: 'error'; message: string };Narrow on status:
const r = await signMessage('Verify my ownership');
if (r.status === 'success' && r.type === 'signMessageSuccess') {
await sendToBackend({
message: r.data.message,
signature: r.data.signature,
publicKey: r.data.publicKey,
address: session.address,
});
}How the signature is formatted
Different chains, different wire formats — same API.
On EVM networks, messages are signed per EIP-191 — the message is prefixed with \x19Ethereum Signed Message:\n<len> before signing, so you can't trick a user into signing a raw transaction hash.
// Signed message: '\x19Ethereum Signed Message:\n13Hello, world!'
const r = await signMessage('Hello, world!');
// r.data.signature → '0x…' (65 bytes: r + s + v)Verify with evm.verifyMessage:
import { evm } from '@oviato/connect';
const valid = await evm.verifyMessage({
address: session.address,
message: r.data.message,
signature: r.data.signature,
});On UTXO networks, messages are signed using the Bitcoin Signed Message standard — a prefix of \x18Bitcoin Signed Message:\n<len> wrapped in a double-sha256, signed with the address's private key.
const r = await signMessage('Hello, world!');
// r.data.signature → base64 string (65 bytes raw → base64)Verification uses standard Bitcoin libraries (bitcoinjs, bitcore, etc.). The SDK doesn't ship a built-in verifier today — pass the signature to your backend for verification.
Common use cases
Prove wallet ownership
Before a backend grants a user access to a premium feature, have them sign a challenge that includes a nonce + timestamp.
// 1. Fetch a challenge from your backend
const { challenge } = await fetch('/api/auth/challenge').then((r) => r.json());
// challenge example: 'Sign to prove ownership: nonce=abc123 ts=1700000000'
// 2. Have the user sign it
const r = await signMessage(challenge);
if (r.status !== 'success' || r.type !== 'signMessageSuccess') return;
// 3. Send back for verification
const { granted } = await fetch('/api/auth/verify', {
method: 'POST',
body: JSON.stringify({
message: r.data.message,
signature: r.data.signature,
address: session.address,
}),
}).then((r) => r.json());Off-chain consent
Get a signed consent to avoid an on-chain gas cost:
const intent = `I authorize a transfer of 100 USDC to 0x... at ${Date.now()}`;
const r = await signMessage(intent);
// Store r.data.signature for later — execute on-chain if/when neededSign-in with SIWE (EVM)
SIWE is the canonical "sign in with Ethereum" flow. Build the message per spec, sign it:
const domain = 'example.com';
const statement = 'Sign in to Example';
const nonce = crypto.randomUUID();
const issuedAt = new Date().toISOString();
const message = `${domain} wants you to sign in with your Ethereum account:
${session.address}
${statement}
URI: https://${domain}
Version: 1
Chain ID: 1
Nonce: ${nonce}
Issued At: ${issuedAt}`;
const r = await signMessage(message);
// Send r.data.signature + message to your backend for verificationSecurity notes
Never have users sign an untrusted message. Messages can be structured to look like plain text but actually encode a transaction. Always show users the full message content before signing, and treat any signing request as deliberate on your part.
- Signatures produced by
signMessageauthenticate the user's passkey, not their on-chain wallet directly. Verification recovers the public key, which you then match against the expected session address. - Nonces + timestamps in signed messages prevent replay attacks.
- Oviato's bridge UI displays the full message to the user before they approve. For ASCII messages this is straightforward; for binary payloads users see a hex dump — avoid binary content in user-facing flows.
Options
await signMessage('...', {
method: 'popup', // or 'iframe' — how the approval UI opens
});Defaults to iframe — the SDK only falls back to popup on localhost / non-HTTPS or on Safari/iOS for passkey operations. In production you get iframe unless you override.
Related
- EVM Writes → signTypedData — structured EIP-712 data
- EVM Reads → Verify — recover + check a signature