The following reflects only the author’s current understanding.
=====================
I recently built a mostly front-end interface for EOA wallet login. You could call it the beginning of my journey into blockchain development, and it also gave me some hands-on experience with how the EVM actually works.

A Wallet Login
Although this is described as a single login, it actually involves two interactions that require confirmation from the wallet. The first is initiated by the page to request access to the wallet address. The second comes from the server, which sends the wallet a message containing an authentication statement, requesting domain, nonce, and other fields in a protocol-compliant structure, then asks the wallet to verify it. The structure currently imitates EIP-4361, though personally, I feel that much of it is more about UI and traceability. In practice, the nonce alone should probably be enough to distinguish requests.
Why must the wallet address be obtained before verification can begin? Presumably because neither the browser nor the wallet inherently trusts the other. If all data exchanged between them is considered untrustworthy, then only an address confirmed by the wallet can be trusted, after which wallet verification can proceed.
I Control This Wallet!
What Is Your Wallet Address?
This is essentially an interaction between the browser and the wallet that confirms which wallet address should be selected and authorized for disclosure to the website.
This project uses wagmi’s connect functionality to request the wallet interaction.
// wagmi configexport const wagmiConfig = createConfig({ ..., connectors: [injected({ shimDisconnect: true })], ...});
// get wagmi injected connectorconst injectedConnector = useMemo( () => connectors.find((connector) => connector.id === "injected") ?? connectors[0], [connectors] );// connect walletconnect({ connector: injectedConnector });
// auto get wallet connect infoconst { address, chainId, isConnected } = useAccount();It also uses wagmi’s useAccount to automatically retrieve the updated information after the wallet confirms the request.
So, Is This Wallet Really Yours?
I Need You to Authenticate This Claim
Now that the address to authenticate is known, we can prepare a standard SIWE (Sign-In with Ethereum) login signature message. It looks roughly like this:
localhost:3000 wants you to sign in with your Ethereum account:0xYourWalletAddress
Sign in to web3walletLogin with this wallet.
URI: http://localhost:3000Version: 1Chain ID: 1Nonce: <server-issued nonce>Issued At: <generated timestamp>The message is then standardized according to EIP-191 so that it can be distinguished from other signatures and ordinary transactions:
"\x19Ethereum Signed Message:\n" + len(message) + message
The implementation uses wagmi’s signMessageAsync to send the actual authentication request to the wallet.
const { signMessageAsync, isPending: isSigning } = useSignMessage();
// make a siwe messageconst siweMessage = new SiweMessage({ domain: window.location.host, address, statement: "Sign in to web3walletLogin with this wallet.", uri: siteOrigin, version: "1", chainId, nonce });const preparedMessage = siweMessage.prepareMessage();// send sign request to walletconst signature = await signMessageAsync({ message: preparedMessage });Only My Private Key Can Produce This Proof
When the wallet receives this message and authentication request, it must ensure that its signature can prove control over the address provided earlier. To do this, the wallet signs the hash of the message using its private key, while ensuring that the signature can still be verified by a website that knows only the wallet address and the requested message. Naturally, a series of mathematical transformations is needed to make this verification method work. For now, the implementation simply uses wagmi’s algorithm to verify the returned signature (
// use the message (the client send) and sign(the wallet back)const verifyResponse = await fetch("/api/auth/verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: preparedMessage, signature }) });
// sign message verifyconst siwe = new SiweMessage(body.message);const result = await siwe.verify({ signature: body.signature, domain: expectedDomain, nonce: siwe.nonce });
// the fail resultif (!result.success) { return NextResponse.json({ error: "invalid signature" }, { status: 401 }); }As for the actual verification math?
That brings us to the secp256k1 curve used for EOA wallet signatures. For an EOA wallet, it works roughly like this:
- EOA private key: a random number d of approximately 256 bits
- Public key: an elliptic-curve point Q = d * G
- Address: the last 20 bytes of keccak256(publicKey)
- Signature: ECDSA over secp256k1
I do not really understand the actual calculations here either, but my rough understanding is that the private key d cannot be reverse-engineered from the public key Q recovered using the signature and message hash because doing so would require solving the elliptic-curve discrete logarithm problem . For secp256k1, a 256-bit curve, this would require roughly operations.
Ultimately, the returned signature () and the hash of the message sent by the website can be used to recover a public key and derive its address. If that address matches the one provided earlier, it proves that the wallet controls the address.
Conclusion
That is about it for my first hands-on blockchain development project. Honestly, looking at the actual implementations of private-key signing and signature recovery reminded me a little of when I was learning ACM-style competitive programming (
Still, this feels like a great gateway project, and it has definitely sparked my interest in learning more about blockchain.
If this article helped you, please share it with others!
Some information may be outdated





