Documentation Index
Fetch the complete documentation index at: https://voltaire.tevm.sh/llms.txt
Use this file to discover all available pages before exploring further.
Skill — Copyable reference implementation. Use as-is or customize. See Skills Philosophy.
Sign-In with Ethereum (SIWE) enables passwordless authentication using Ethereum accounts. Users prove ownership of their address by signing a structured message (EIP-4361).
Overview
SIWE authentication flow:
- Server generates a unique nonce
- Client creates a SIWE message with the nonce
- User signs the message with their wallet
- Server verifies the signature matches the address in the message
- Server creates a session for the authenticated user
Creating SIWE Messages
Use Siwe.create() to construct a message with required fields:
import { Siwe, Address } from "@tevm/voltaire";
// Create the user's address
const userAddress = Address("0x742d35Cc6634C0532925a3b844Bc454e4438f44e");
// Create a basic SIWE message
const message = Siwe.create({
domain: "myapp.com",
address: userAddress,
uri: "https://myapp.com/login",
chainId: 1,
});
// Message automatically includes:
// - nonce (randomly generated)
// - issuedAt (current timestamp)
// - version ("1")
Adding Optional Fields
Include statement, expiration, and resources for enhanced messages:
const message = Siwe.create({
domain: "myapp.com",
address: userAddress,
uri: "https://myapp.com/login",
chainId: 1,
statement: "Sign in to access your dashboard",
expirationTime: new Date(Date.now() + 3600000).toISOString(), // 1 hour
notBefore: new Date().toISOString(),
requestId: "unique-request-123",
resources: [
"https://myapp.com/api/profile",
"https://myapp.com/api/settings",
],
});
Custom Nonce
For server-generated nonces, pass your own value:
// Generate nonce on server
const serverNonce = Siwe.generateNonce(); // 16 alphanumeric chars
// Pass to client, then create message with it
const message = Siwe.create({
domain: "myapp.com",
address: userAddress,
uri: "https://myapp.com/login",
chainId: 1,
nonce: serverNonce,
});
Convert the message object to the EIP-4361 text format for wallet signing:
const messageText = Siwe.format(message);
console.log(messageText);
Output:
myapp.com wants you to sign in with your Ethereum account:
0x742d35Cc6634C0532925a3b844Bc454e4438f44e
Sign in to access your dashboard
URI: https://myapp.com/login
Version: 1
Chain ID: 1
Nonce: abc123xyz789
Issued At: 2024-01-15T10:30:00.000Z
Expiration Time: 2024-01-15T11:30:00.000Z
Signing SIWE Messages
The message hash uses EIP-191 personal sign format. Sign with any Ethereum wallet:
Using PrivateKeySigner (Server-side or Testing)
import { Siwe, Address } from "@tevm/voltaire";
import { PrivateKeySignerImpl } from "@tevm/voltaire/crypto";
// Create signer from private key
const signer = PrivateKeySignerImpl.fromPrivateKey({
privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
});
// Create and format the message
const message = Siwe.create({
domain: "myapp.com",
address: Address(signer.address),
uri: "https://myapp.com/login",
chainId: 1,
});
const messageText = Siwe.format(message);
// Sign the message (returns hex string)
const signatureHex = await signer.signMessage(messageText);
// Convert to Uint8Array for verification
const signature = new Uint8Array(
signatureHex.slice(2).match(/.{2}/g)!.map((byte) => parseInt(byte, 16))
);
Using Browser Wallet (Client-side)
import { Siwe, Address } from "@tevm/voltaire";
// Get address from connected wallet
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
const userAddress = Address(accounts[0]);
// Create message
const message = Siwe.create({
domain: window.location.host,
address: userAddress,
uri: window.location.origin,
chainId: 1,
statement: "Sign in to MyApp",
});
// Format and sign
const messageText = Siwe.format(message);
const signatureHex = await window.ethereum.request({
method: "personal_sign",
params: [messageText, accounts[0]],
});
// Convert signature for verification
const signature = new Uint8Array(
signatureHex.slice(2).match(/.{2}/g).map((byte) => parseInt(byte, 16))
);
Verifying SIWE Signatures
Basic Verification
Siwe.verify() checks that the signature was created by the address in the message:
const isValid = Siwe.verify(message, signature);
if (isValid) {
console.log("Signature valid - user authenticated");
} else {
console.log("Invalid signature");
}
Full Message Verification
Siwe.verifyMessage() validates both the signature and message constraints (expiration, notBefore):
const result = Siwe.verifyMessage(message, signature);
if (result.valid) {
// User authenticated, create session
createSession(message.address, message.nonce);
} else {
// Handle specific error
console.error(result.error.type, result.error.message);
}
Validation Without Signature
Validate message structure and timestamps before signature verification:
const validation = Siwe.validate(message);
if (!validation.valid) {
switch (validation.error.type) {
case "expired":
console.error("Message has expired");
break;
case "not_yet_valid":
console.error("Message is not yet valid");
break;
case "invalid_nonce":
console.error("Nonce must be at least 8 characters");
break;
case "invalid_domain":
console.error("Domain is required");
break;
default:
console.error(validation.error.message);
}
}
Testing with Custom Time
For testing expiration logic:
const futureTime = new Date(Date.now() + 7200000); // 2 hours from now
const result = Siwe.validate(message, { now: futureTime });
if (!result.valid && result.error.type === "expired") {
console.log("Message would be expired at that time");
}
Session Management Patterns
Server-Side Session Flow
// 1. Generate nonce endpoint
app.get("/auth/nonce", (req, res) => {
const nonce = Siwe.generateNonce();
// Store nonce in session or database with expiration
req.session.siweNonce = nonce;
res.json({ nonce });
});
// 2. Verify endpoint
app.post("/auth/verify", (req, res) => {
const { messageText, signature } = req.body;
// Parse the message from text
const message = Siwe.parse(messageText);
// Verify nonce matches what we generated
if (message.nonce !== req.session.siweNonce) {
return res.status(401).json({ error: "Invalid nonce" });
}
// Verify domain matches our domain
if (message.domain !== "myapp.com") {
return res.status(401).json({ error: "Invalid domain" });
}
// Convert signature to Uint8Array
const sigBytes = new Uint8Array(
signature.slice(2).match(/.{2}/g).map((b) => parseInt(b, 16))
);
// Verify signature and message validity
const result = Siwe.verifyMessage(message, sigBytes);
if (!result.valid) {
return res.status(401).json({ error: result.error.message });
}
// Create authenticated session
req.session.address = message.address;
req.session.chainId = message.chainId;
// Clear used nonce
delete req.session.siweNonce;
res.json({ success: true });
});
Parsing Messages
Parse a formatted message string back to an object:
const messageText = `myapp.com wants you to sign in with your Ethereum account:
0x742d35Cc6634C0532925a3b844Bc454e4438f44e
Sign in to access your account
URI: https://myapp.com/login
Version: 1
Chain ID: 1
Nonce: abc123xyz789
Issued At: 2024-01-15T10:30:00.000Z`;
const message = Siwe.parse(messageText);
console.log(message.domain); // "myapp.com"
console.log(message.statement); // "Sign in to access your account"
Domain Binding Security
Always verify the domain matches your application to prevent phishing:
function verifyDomain(message: SiweMessageType): boolean {
const allowedDomains = ["myapp.com", "www.myapp.com", "staging.myapp.com"];
return allowedDomains.includes(message.domain);
}
// In verification flow
const message = Siwe.parse(messageText);
if (!verifyDomain(message)) {
throw new Error("Message domain does not match application");
}
Complete Example
Full client-server authentication flow:
// === Client Side ===
import { Siwe, Address } from "@tevm/voltaire";
async function signIn() {
// 1. Get nonce from server
const nonceRes = await fetch("/auth/nonce");
const { nonce } = await nonceRes.json();
// 2. Connect wallet
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
const address = Address(accounts[0]);
// 3. Create message
const message = Siwe.create({
domain: window.location.host,
address,
uri: window.location.origin,
chainId: 1,
statement: "Sign in to MyApp",
nonce,
expirationTime: new Date(Date.now() + 300000).toISOString(), // 5 min
});
// 4. Sign message
const messageText = Siwe.format(message);
const signature = await window.ethereum.request({
method: "personal_sign",
params: [messageText, accounts[0]],
});
// 5. Send to server for verification
const verifyRes = await fetch("/auth/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messageText, signature }),
});
if (verifyRes.ok) {
console.log("Authenticated!");
// Redirect to dashboard
}
}
Error Handling
Handle all validation error types:
import type { ValidationError } from "@tevm/voltaire";
function handleValidationError(error: ValidationError): string {
const messages: Record<ValidationError["type"], string> = {
invalid_domain: "Invalid domain in message",
invalid_address: "Invalid Ethereum address format",
invalid_uri: "URI is required",
invalid_version: "Unsupported SIWE version",
invalid_chain_id: "Invalid chain ID",
invalid_nonce: "Nonce must be at least 8 characters",
invalid_timestamp: "Invalid timestamp format",
expired: "Authentication request has expired",
not_yet_valid: "Authentication request is not yet valid",
signature_mismatch: "Signature does not match address",
};
return messages[error.type] || error.message;
}
API Reference
| Function | Description |
|---|
Siwe.create(params) | Create a new SIWE message |
Siwe.format(message) | Convert message to EIP-4361 text |
Siwe.parse(text) | Parse EIP-4361 text to message object |
Siwe.validate(message, options?) | Validate message structure and timestamps |
Siwe.verify(message, signature) | Verify signature matches address |
Siwe.verifyMessage(message, signature, options?) | Full validation + signature verification |
Siwe.generateNonce(length?) | Generate random alphanumeric nonce |
Siwe.getMessageHash(message) | Get EIP-191 hash for signing |