Skip to content

Shuklax/Midnight-Wallet

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Midnight Wallet

A browser-based HD wallet demo for Ethereum and Solana. One seed phrase, multiple accounts on both chains — all keys derived and stored locally, nothing ever leaves your browser.

npm install
npm run dev
# → http://localhost:3000

How it works (the short version)

Generate a 12-word seed phrase → stretch it into a 512-bit master seed → derive ETH and SOL accounts from that seed using standardized paths. That's the whole flow. The sections below go deeper on each step.


Seed Phrases — BIP-39

The wallet generates 128 bits of random entropy using window.crypto.getRandomValues(), then converts it to a human-readable 12-word mnemonic. The process:

  1. Take 128 random bits, hash them with SHA-256, append the first 4 bits as a checksum → 132 bits total
  2. Split into 12 groups of 11 bits each
  3. Map each group to a word from the BIP-39 wordlist (2048 words)

That 12-word phrase encodes 2¹²⁸ possible wallets — effectively impossible to brute force.

The mnemonic isn't used for derivation directly. It first gets stretched into a 512-bit seed via PBKDF2-HMAC-SHA512 (2048 rounds, salt = "mnemonic"). This is the actual root from which all keys are derived.


Ethereum Accounts — BIP-32 / BIP-44

Ethereum follows BIP-44, which defines a 5-level derivation path:

m / purpose' / coin_type' / account' / change / index
m / 44'      / 60'        / 0'       / 0      / n
  • 44' — BIP-44 standard
  • 60' — Ethereum's registered coin type (SLIP-44)
  • 0' — first account group
  • 0 — external/receiving addresses
  • n — individual account index (0, 1, 2…)

Under the hood, each derivation step runs HMAC-SHA512 on the parent's chain code + key material, splits the 64-byte output in half, and tweaks the left half into a new private key via modular addition on the secp256k1 curve. The right half becomes the next chain code.

Address derivation from a private key:

private key → secp256k1 multiply → public key (64 bytes) → keccak256 → last 20 bytes → EIP-55 checksum

EIP-55 checksums encode error detection into the address itself by using mixed casing based on the hash of the lowercase address.

ethers v6 handles all of this:

ethers.HDNodeWallet.fromMnemonic(
  ethers.Mnemonic.fromPhrase(mnemonic),
  `m/44'/60'/0'/0/${i}`
)

Solana Accounts — SLIP-0010

Here's where it gets interesting. BIP-32 was designed for the secp256k1 curve and can't be used directly with Solana's Ed25519 curve — the math doesn't transfer cleanly. SLIP-0010 fixes this.

The key differences from BIP-32:

  • The master seed label changes from "Bitcoin seed" to "ed25519 seed"
  • Child key derivation skips the modular addition step — I_L becomes the private key directly
  • All path segments must be hardened (no non-hardened derivation possible with Ed25519)

Solana's path:

m / 44' / 501' / n' / 0'

Notice every segment has ' — that's not optional, it's a requirement of the curve. The account index n moves to the third level (unlike ETH where it's the last).

The full SLIP-0010 derivation runs entirely in-browser using @noble/hashes:

// Master key
let I  = hmac(sha512, "ed25519 seed", seed)
let kL = I[0:32]  // private key
let kR = I[32:64] // chain code

// Each path segment (all hardened)
data  = 0x00 + kL + ser32(index | 0x80000000)
I     = hmac(sha512, kR, data)
kL    = I[0:32]  // new private key — used as-is, no tweaking
kR    = I[32:64] // new chain code

The final private key is passed to ed25519.getPublicKey() which does scalar multiplication on the curve, producing a 32-byte compressed point. That point is Base58-encoded to get the familiar Solana address format.


secp256k1 vs Ed25519

Both are elliptic curves used for signing, but they're meaningfully different:

secp256k1 (ETH) Ed25519 (SOL)
Type Weierstrass Twisted Edwards
Signatures ECDSA EdDSA
Public key size 33 or 65 bytes 32 bytes (always)
Signature size ~71 bytes 64 bytes (always)
Deterministic Optional (RFC 6979) Always
Malleable Yes (historically) No

Ed25519 is faster, produces smaller fixed-size signatures, and is deterministic by design — the same message + key always produces the same signature, removing a whole class of bugs.


Hardened Derivation

A quick note since it comes up a lot: the ' in a path means hardened. The difference matters:

  • Non-hardened (/0/1/2) — uses the parent public key in the HMAC. Enables child public key derivation without the private key (useful for watch-only wallets), but if a child private key leaks alongside the chain code, the parent private key can be reconstructed.
  • Hardened (/0'/1'/2') — uses the parent private key. No derivation is possible without it, completely isolating the branch.

This is why BIP-44 hardens purpose, coin_type, and account but keeps address_index non-hardened for Ethereum — it allows xpub-based account scanning without exposing the root. Solana can't offer that option at all given Ed25519's constraints.


Library choices

Package Why
bip39 BIP-39 mnemonic generation + PBKDF2 seed stretching
ethers v6 BIP-32/44 HD tree for Ethereum, battle-tested
@noble/hashes Browser-native HMAC-SHA512 for SLIP-0010
@noble/curves Ed25519 scalar multiplication for Solana public keys
bs58 Base58 encoding for Solana addresses

@solana/web3.js was intentionally skipped — it pulls in Node.js crypto internals that require webpack polyfills in Next.js. The @noble libraries are purpose-built for browsers, audited, and give us exactly the two primitives we need with no overhead.

All imports are dynamic (await import(...)) so they only run client-side and don't break Next.js SSR.


Below are some references which you can read and understand how things like derivations paths, seed generation, etc. works.

Standards: BIP-39 · BIP-32 · BIP-44 · SLIP-0010 · EIP-55

About

A simple web based wallet which supports ETH and SOL

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors