Signing Messages
Sign messages with passkeys using @oviato/connect
Learn how to sign arbitrary messages using @oviato/connect in React applications.
Overview
Message signing allows users to cryptographically prove ownership of their wallet without sending a transaction. This is useful for:
- Authentication challenges
- Proof of ownership
- Off-chain signatures
- Message verification
Basic Usage
With Core Function
Use the signMessage function directly:
"use client";
import { useState } from "react";
import { useOviConnect } from "@oviato/connect/client";
import { signMessage } from "@oviato/connect";
export default function SignMessage() {
const { session } = useOviConnect();
const [signature, setSignature] = useState("");
const handleSign = async () => {
const result = await signMessage("Hello, Oviato!");
if (result.status === "success") {
setSignature(result.data.signature);
console.log("Message:", result.data.message);
console.log("Public Key:", result.data.publicKey);
}
};
if (!session) {
return <div>Please connect your wallet first</div>;
}
return (
<div>
<button onClick={handleSign}>Sign Message</button>
{signature && (
<div>
<p>Signature: {signature}</p>
</div>
)}
</div>
);
}Response Type
The signMessage function returns a BridgeResponse:
type SignMessageResponse = {
status: "success" | "error";
data?: {
message: string; // Original message
signature: string; // Cryptographic signature
publicKey: string; // Public key used for signing
};
message?: string; // Error message if failed
};Complete Example
Here's a complete example with input and error handling:
"use client";
import { useState } from "react";
import { useOviConnect } from "@oviato/connect/client";
import { signMessage } from "@oviato/connect";
export default function MessageSigner() {
const { session } = useOviConnect();
const [message, setMessage] = useState("");
const [signature, setSignature] = useState("");
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleSign = async () => {
if (!message.trim()) {
setError("Please enter a message");
return;
}
setIsLoading(true);
setError("");
setSignature("");
const result = await signMessage(message);
if (result.status === "success") {
setSignature(result.data.signature);
} else {
setError(result.message || "Failed to sign message");
}
setIsLoading(false);
};
if (!session) {
return (
<div>
<p>Please connect your wallet to sign messages</p>
</div>
);
}
return (
<div className="space-y-4">
<div>
<h2>Sign Message</h2>
<p>Address: {session.address}</p>
</div>
<div>
<label htmlFor="message">Message to sign:</label>
<textarea
id="message"
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Enter your message here..."
rows={4}
className="w-full p-2 border rounded"
/>
</div>
<button
onClick={handleSign}
disabled={isLoading || !message.trim()}
className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
>
{isLoading ? "Signing..." : "Sign Message"}
</button>
{error && (
<div className="p-3 bg-red-100 text-red-700 rounded">
Error: {error}
</div>
)}
{signature && (
<div className="p-3 bg-green-100 rounded">
<h3 className="font-bold">Signature:</h3>
<p className="break-all font-mono text-sm">{signature}</p>
</div>
)}
</div>
);
}Use Cases
1. Authentication Challenge
Prove ownership of an address for authentication:
const handleAuth = async () => {
const challenge = await fetch("/api/auth/challenge").then((r) => r.json());
const result = await signMessage(challenge.message);
if (result.status === "success") {
// Send signature to backend for verification
const response = await fetch("/api/auth/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: result.data.message,
signature: result.data.signature,
publicKey: result.data.publicKey,
}),
});
if (response.ok) {
console.log("Authentication successful!");
}
}
};2. Sign Terms of Service
Have users sign acceptance of terms:
const handleAcceptTerms = async () => {
const terms = `
I accept the Terms of Service
Date: ${new Date().toISOString()}
Address: ${session.address}
`;
const result = await signMessage(terms);
if (result.status === "success") {
// Store signature as proof of acceptance
await fetch("/api/terms/accept", {
method: "POST",
body: JSON.stringify({
signature: result.data.signature,
terms: terms,
}),
});
}
};3. Proof of Ownership
Prove you own an address without revealing private keys:
const handleProveOwnership = async () => {
const proof = `I own ${session.address} at ${new Date().toISOString()}`;
const result = await signMessage(proof);
if (result.status === "success") {
return {
address: session.address,
message: proof,
signature: result.data.signature,
publicKey: result.data.publicKey,
};
}
};Error Handling
Handle different error scenarios:
const result = await signMessage("Hello, World!");
if (result.status === "error") {
if (result.message?.includes("cancelled")) {
console.log("User cancelled signing");
} else if (result.message?.includes("timeout")) {
console.log("Signing timed out");
} else if (result.message?.includes("not authenticated")) {
console.log("User not logged in");
} else {
console.error("Signing failed:", result.message);
}
}Verifying Signatures
To verify signatures on your backend, you'll need:
- The original message
- The signature
- The public key
Example verification (pseudo-code):
// Backend verification
function verifySignature(
message: string,
signature: string,
publicKey: string
): boolean {
// Use your preferred crypto library
// e.g., bitcoinjs-lib for Bitcoin, ethers for Ethereum
return cryptoLib.verify(message, signature, publicKey);
}Security Best Practices
1. Always Show Message to User
Never sign messages without showing them to the user:
// ❌ Bad: Hidden message
await signMessage(hiddenChallenge);
// ✅ Good: Show message in UI
<div>
<p>You are signing:</p>
<pre>{messageToSign}</pre>
<button onClick={() => signMessage(messageToSign)}>Sign</button>
</div>;2. Include Timestamp
Include timestamps in messages to prevent replay attacks:
const message = `
Action: ${action}
Timestamp: ${Date.now()}
Address: ${session.address}
`;
await signMessage(message);3. Validate on Backend
Always verify signatures on your backend:
// Frontend
const result = await signMessage(challenge);
// Backend
POST /api/verify
{
"message": "...",
"signature": "...",
"publicKey": "..."
}TypeScript
Full type safety with TypeScript:
import type { BridgeResponse } from "@oviato/connect";
type SignatureData = {
message: string;
signature: string;
publicKey: string;
};
const result: BridgeResponse<SignatureData> = await signMessage("Hello!");
if (result.status === "success") {
const { message, signature, publicKey } = result.data;
// TypeScript knows the types here
}Troubleshooting
"User not authenticated"
User must be logged in. Check session first:
if (!session) {
alert("Please connect your wallet first");
return;
}
await signMessage("Hello!");"User cancelled signing"
User closed the signing modal. This is normal behavior.
Signature verification fails
Ensure you're using:
- The exact same message (including whitespace)
- The correct public key
- The right verification algorithm for the network
Next Steps
- Signing Transactions - Learn about transaction signing