EIPs
EIP-4361 — Sign-In with Ethereum
SIWE. An off-chain authentication message that binds a wallet signature to a specific domain, URI, chain, nonce, and timestamp. Used by apps that want “log in with your wallet” semantics without running an Ethereum tx.
import { build_siwe_message, generate_siwe_nonce } from "@ethernauta/eip/4361";
const message = build_siwe_message({
domain: "example.com",
address: signer_address,
uri: "https://example.com",
version: "1",
chain_id: 1,
nonce: generate_siwe_nonce(),
issued_at: new Date().toISOString(),
}); Surface
| Export | Type | Purpose |
|---|---|---|
build_siwe_message | (fields) => string | Compose the canonical SIWE string. |
parse_siwe_message | (message: string) => SiweMessage | Parse a SIWE string back into fields. |
is_siwe_message | (message: string) => boolean | Predicate. |
generate_siwe_nonce | () => string | Cryptographically random nonce. |
SiweMessage | type | Parsed SIWE fields. |
siweMessageSchema | Valibot schema | Validate parsed input. |
Signing and verifying
Signing is just personal_sign over the SIWE string:
import { personal_sign } from "@ethernauta/eip/191";
const signature = await personal_sign({
account,
message,
})(signer({ chain_id: eip155_1.chain_id })); Verification uses the SIWE-specific verifier (it cross-checks the embedded fields):
import { verify_siwe_message } from "@ethernauta/crypto";
const result = await verify_siwe_message({
message,
signature,
domain: "example.com",
nonce: expected_nonce,
})(reader({ chain_id: eip155_1.chain_id }));
if (!result.valid) {
switch (result.reason) {
case "expired":
// ...
case "wrong_domain":
// ...
}
} VerifySiweMessageFailureReason enumerates every typed rejection mode.
See also
- EIP-191 — the underlying signature format.
- @ethernauta/crypto —
verify_siwe_message.