/* SPDX-License-Identifier: Apache-2.0
 * Copyright 2026 Rhett Creighton
 *
 * Pure BIP39 mnemonic encoding + PBKDF2 seed derivation. Replays from
 * (entropy, passphrase) alone — no clock, RNG, allocation, or I/O.
 *
 * The math:
 *   - SHA-256 over entropy → 1-byte checksum (first 4-8 bits used).
 *   - 11-bit packing of (entropy || checksum) → word indices into the
 *     BIP39 English wordlist (2048 entries, lowercase-sorted).
 *   - PBKDF2-HMAC-SHA512 with 2048 rounds, salt = "mnemonic"||passphrase.
 *
 * The static BIP39 English wordlist is part of the spec and lives here
 * (it's pure const data — no allocation, no IO). The wallet's *random*
 * entropy generation stays in lib/wallet/ behind the platform.rng port.
 */

#include "domain/wallet/mnemonic.h"

#include "crypto/pbkdf2_sha512.h"
#include "crypto/sha256.h"
#include "support/cleanse.h"

#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>

/* ── BIP39 English wordlist (2048 words, lexicographically sorted) ── */

static const char *const bip39_english[2048] = {
    "abandon",
    "ability",
    "able",
    "about",
    "above",
    "absent",
    "absorb",
    "abstract",
    "absurd",
    "abuse",
    "access",
    "accident",
    "account",
    "accuse",
    "achieve",
    "acid",
    "acoustic",
    "acquire",
    "across",
    "act",
    "action",
    "actor",
    "actress",
    "actual",
    "adapt",
    "add",
    "addict",
    "address",
    "adjust",
    "admit",
    "adult",
    "advance",
    "advice",
    "aerobic",
    "affair",
    "afford",
    "afraid",
    "again",
    "age",
    "agent",
    "agree",
    "ahead",
    "aim",
    "air",
    "airport",
    "aisle",
    "alarm",
    "album",
    "alcohol",
    "alert",
    "alien",
    "all",
    "alley",
    "allow",
    "almost",
    "alone",
    "alpha",
    "already",
    "also",
    "alter",
    "always",
    "amateur",
    "amazing",
    "among",
    "amount",
    "amused",
    "analyst",
    "anchor",
    "ancient",
    "anger",
    "angle",
    "angry",
    "animal",
    "ankle",
    "announce",
    "annual",
    "another",
    "answer",
    "antenna",
    "antique",
    "anxiety",
    "any",
    "apart",
    "apology",
    "appear",
    "apple",
    "approve",
    "april",
    "arch",
    "arctic",
    "area",
    "arena",
    "argue",
    "arm",
    "armed",
    "armor",
    "army",
    "around",
    "arrange",
    "arrest",
    "arrive",
    "arrow",
    "art",
    "artefact",
    "artist",
    "artwork",
    "ask",
    "aspect",
    "assault",
    "asset",
    "assist",
    "assume",
    "asthma",
    "athlete",
    "atom",
    "attack",
    "attend",
    "attitude",
    "attract",
    "auction",
    "audit",
    "august",
    "aunt",
    "author",
    "auto",
    "autumn",
    "average",
    "avocado",
    "avoid",
    "awake",
    "aware",
    "away",
    "awesome",
    "awful",
    "awkward",
    "axis",
    "baby",
    "bachelor",
    "bacon",
    "badge",
    "bag",
    "balance",
    "balcony",
    "ball",
    "bamboo",
    "banana",
    "banner",
    "bar",
    "barely",
    "bargain",
    "barrel",
    "base",
    "basic",
    "basket",
    "battle",
    "beach",
    "bean",
    "beauty",
    "because",
    "become",
    "beef",
    "before",
    "begin",
    "behave",
    "behind",
    "believe",
    "below",
    "belt",
    "bench",
    "benefit",
    "best",
    "betray",
    "better",
    "between",
    "beyond",
    "bicycle",
    "bid",
    "bike",
    "bind",
    "biology",
    "bird",
    "birth",
    "bitter",
    "black",
    "blade",
    "blame",
    "blanket",
    "blast",
    "bleak",
    "bless",
    "blind",
    "blood",
    "blossom",
    "blouse",
    "blue",
    "blur",
    "blush",
    "board",
    "boat",
    "body",
    "boil",
    "bomb",
    "bone",
    "bonus",
    "book",
    "boost",
    "border",
    "boring",
    "borrow",
    "boss",
    "bottom",
    "bounce",
    "box",
    "boy",
    "bracket",
    "brain",
    "brand",
    "brass",
    "brave",
    "bread",
    "breeze",
    "brick",
    "bridge",
    "brief",
    "bright",
    "bring",
    "brisk",
    "broccoli",
    "broken",
    "bronze",
    "broom",
    "brother",
    "brown",
    "brush",
    "bubble",
    "buddy",
    "budget",
    "buffalo",
    "build",
    "bulb",
    "bulk",
    "bullet",
    "bundle",
    "bunker",
    "burden",
    "burger",
    "burst",
    "bus",
    "business",
    "busy",
    "butter",
    "buyer",
    "buzz",
    "cabbage",
    "cabin",
    "cable",
    "cactus",
    "cage",
    "cake",
    "call",
    "calm",
    "camera",
    "camp",
    "can",
    "canal",
    "cancel",
    "candy",
    "cannon",
    "canoe",
    "canvas",
    "canyon",
    "capable",
    "capital",
    "captain",
    "car",
    "carbon",
    "card",
    "cargo",
    "carpet",
    "carry",
    "cart",
    "case",
    "cash",
    "casino",
    "castle",
    "casual",
    "cat",
    "catalog",
    "catch",
    "category",
    "cattle",
    "caught",
    "cause",
    "caution",
    "cave",
    "ceiling",
    "celery",
    "cement",
    "census",
    "century",
    "cereal",
    "certain",
    "chair",
    "chalk",
    "champion",
    "change",
    "chaos",
    "chapter",
    "charge",
    "chase",
    "chat",
    "cheap",
    "check",
    "cheese",
    "chef",
    "cherry",
    "chest",
    "chicken",
    "chief",
    "child",
    "chimney",
    "choice",
    "choose",
    "chronic",
    "chuckle",
    "chunk",
    "churn",
    "cigar",
    "cinnamon",
    "circle",
    "citizen",
    "city",
    "civil",
    "claim",
    "clap",
    "clarify",
    "claw",
    "clay",
    "clean",
    "clerk",
    "clever",
    "click",
    "client",
    "cliff",
    "climb",
    "clinic",
    "clip",
    "clock",
    "clog",
    "close",
    "cloth",
    "cloud",
    "clown",
    "club",
    "clump",
    "cluster",
    "clutch",
    "coach",
    "coast",
    "coconut",
    "code",
    "coffee",
    "coil",
    "coin",
    "collect",
    "color",
    "column",
    "combine",
    "come",
    "comfort",
    "comic",
    "common",
    "company",
    "concert",
    "conduct",
    "confirm",
    "congress",
    "connect",
    "consider",
    "control",
    "convince",
    "cook",
    "cool",
    "copper",
    "copy",
    "coral",
    "core",
    "corn",
    "correct",
    "cost",
    "cotton",
    "couch",
    "country",
    "couple",
    "course",
    "cousin",
    "cover",
    "coyote",
    "crack",
    "cradle",
    "craft",
    "cram",
    "crane",
    "crash",
    "crater",
    "crawl",
    "crazy",
    "cream",
    "credit",
    "creek",
    "crew",
    "cricket",
    "crime",
    "crisp",
    "critic",
    "crop",
    "cross",
    "crouch",
    "crowd",
    "crucial",
    "cruel",
    "cruise",
    "crumble",
    "crunch",
    "crush",
    "cry",
    "crystal",
    "cube",
    "culture",
    "cup",
    "cupboard",
    "curious",
    "current",
    "curtain",
    "curve",
    "cushion",
    "custom",
    "cute",
    "cycle",
    "dad",
    "damage",
    "damp",
    "dance",
    "danger",
    "daring",
    "dash",
    "daughter",
    "dawn",
    "day",
    "deal",
    "debate",
    "debris",
    "decade",
    "december",
    "decide",
    "decline",
    "decorate",
    "decrease",
    "deer",
    "defense",
    "define",
    "defy",
    "degree",
    "delay",
    "deliver",
    "demand",
    "demise",
    "denial",
    "dentist",
    "deny",
    "depart",
    "depend",
    "deposit",
    "depth",
    "deputy",
    "derive",
    "describe",
    "desert",
    "design",
    "desk",
    "despair",
    "destroy",
    "detail",
    "detect",
    "develop",
    "device",
    "devote",
    "diagram",
    "dial",
    "diamond",
    "diary",
    "dice",
    "diesel",
    "diet",
    "differ",
    "digital",
    "dignity",
    "dilemma",
    "dinner",
    "dinosaur",
    "direct",
    "dirt",
    "disagree",
    "discover",
    "disease",
    "dish",
    "dismiss",
    "disorder",
    "display",
    "distance",
    "divert",
    "divide",
    "divorce",
    "dizzy",
    "doctor",
    "document",
    "dog",
    "doll",
    "dolphin",
    "domain",
    "donate",
    "donkey",
    "donor",
    "door",
    "dose",
    "double",
    "dove",
    "draft",
    "dragon",
    "drama",
    "drastic",
    "draw",
    "dream",
    "dress",
    "drift",
    "drill",
    "drink",
    "drip",
    "drive",
    "drop",
    "drum",
    "dry",
    "duck",
    "dumb",
    "dune",
    "during",
    "dust",
    "dutch",
    "duty",
    "dwarf",
    "dynamic",
    "eager",
    "eagle",
    "early",
    "earn",
    "earth",
    "easily",
    "east",
    "easy",
    "echo",
    "ecology",
    "economy",
    "edge",
    "edit",
    "educate",
    "effort",
    "egg",
    "eight",
    "either",
    "elbow",
    "elder",
    "electric",
    "elegant",
    "element",
    "elephant",
    "elevator",
    "elite",
    "else",
    "embark",
    "embody",
    "embrace",
    "emerge",
    "emotion",
    "employ",
    "empower",
    "empty",
    "enable",
    "enact",
    "end",
    "endless",
    "endorse",
    "enemy",
    "energy",
    "enforce",
    "engage",
    "engine",
    "enhance",
    "enjoy",
    "enlist",
    "enough",
    "enrich",
    "enroll",
    "ensure",
    "enter",
    "entire",
    "entry",
    "envelope",
    "episode",
    "equal",
    "equip",
    "era",
    "erase",
    "erode",
    "erosion",
    "error",
    "erupt",
    "escape",
    "essay",
    "essence",
    "estate",
    "eternal",
    "ethics",
    "evidence",
    "evil",
    "evoke",
    "evolve",
    "exact",
    "example",
    "excess",
    "exchange",
    "excite",
    "exclude",
    "excuse",
    "execute",
    "exercise",
    "exhaust",
    "exhibit",
    "exile",
    "exist",
    "exit",
    "exotic",
    "expand",
    "expect",
    "expire",
    "explain",
    "expose",
    "express",
    "extend",
    "extra",
    "eye",
    "eyebrow",
    "fabric",
    "face",
    "faculty",
    "fade",
    "faint",
    "faith",
    "fall",
    "false",
    "fame",
    "family",
    "famous",
    "fan",
    "fancy",
    "fantasy",
    "farm",
    "fashion",
    "fat",
    "fatal",
    "father",
    "fatigue",
    "fault",
    "favorite",
    "feature",
    "february",
    "federal",
    "fee",
    "feed",
    "feel",
    "female",
    "fence",
    "festival",
    "fetch",
    "fever",
    "few",
    "fiber",
    "fiction",
    "field",
    "figure",
    "file",
    "film",
    "filter",
    "final",
    "find",
    "fine",
    "finger",
    "finish",
    "fire",
    "firm",
    "first",
    "fiscal",
    "fish",
    "fit",
    "fitness",
    "fix",
    "flag",
    "flame",
    "flash",
    "flat",
    "flavor",
    "flee",
    "flight",
    "flip",
    "float",
    "flock",
    "floor",
    "flower",
    "fluid",
    "flush",
    "fly",
    "foam",
    "focus",
    "fog",
    "foil",
    "fold",
    "follow",
    "food",
    "foot",
    "force",
    "forest",
    "forget",
    "fork",
    "fortune",
    "forum",
    "forward",
    "fossil",
    "foster",
    "found",
    "fox",
    "fragile",
    "frame",
    "frequent",
    "fresh",
    "friend",
    "fringe",
    "frog",
    "front",
    "frost",
    "frown",
    "frozen",
    "fruit",
    "fuel",
    "fun",
    "funny",
    "furnace",
    "fury",
    "future",
    "gadget",
    "gain",
    "galaxy",
    "gallery",
    "game",
    "gap",
    "garage",
    "garbage",
    "garden",
    "garlic",
    "garment",
    "gas",
    "gasp",
    "gate",
    "gather",
    "gauge",
    "gaze",
    "general",
    "genius",
    "genre",
    "gentle",
    "genuine",
    "gesture",
    "ghost",
    "giant",
    "gift",
    "giggle",
    "ginger",
    "giraffe",
    "girl",
    "give",
    "glad",
    "glance",
    "glare",
    "glass",
    "glide",
    "glimpse",
    "globe",
    "gloom",
    "glory",
    "glove",
    "glow",
    "glue",
    "goat",
    "goddess",
    "gold",
    "good",
    "goose",
    "gorilla",
    "gospel",
    "gossip",
    "govern",
    "gown",
    "grab",
    "grace",
    "grain",
    "grant",
    "grape",
    "grass",
    "gravity",
    "great",
    "green",
    "grid",
    "grief",
    "grit",
    "grocery",
    "group",
    "grow",
    "grunt",
    "guard",
    "guess",
    "guide",
    "guilt",
    "guitar",
    "gun",
    "gym",
    "habit",
    "hair",
    "half",
    "hammer",
    "hamster",
    "hand",
    "happy",
    "harbor",
    "hard",
    "harsh",
    "harvest",
    "hat",
    "have",
    "hawk",
    "hazard",
    "head",
    "health",
    "heart",
    "heavy",
    "hedgehog",
    "height",
    "hello",
    "helmet",
    "help",
    "hen",
    "hero",
    "hidden",
    "high",
    "hill",
    "hint",
    "hip",
    "hire",
    "history",
    "hobby",
    "hockey",
    "hold",
    "hole",
    "holiday",
    "hollow",
    "home",
    "honey",
    "hood",
    "hope",
    "horn",
    "horror",
    "horse",
    "hospital",
    "host",
    "hotel",
    "hour",
    "hover",
    "hub",
    "huge",
    "human",
    "humble",
    "humor",
    "hundred",
    "hungry",
    "hunt",
    "hurdle",
    "hurry",
    "hurt",
    "husband",
    "hybrid",
    "ice",
    "icon",
    "idea",
    "identify",
    "idle",
    "ignore",
    "ill",
    "illegal",
    "illness",
    "image",
    "imitate",
    "immense",
    "immune",
    "impact",
    "impose",
    "improve",
    "impulse",
    "inch",
    "include",
    "income",
    "increase",
    "index",
    "indicate",
    "indoor",
    "industry",
    "infant",
    "inflict",
    "inform",
    "inhale",
    "inherit",
    "initial",
    "inject",
    "injury",
    "inmate",
    "inner",
    "innocent",
    "input",
    "inquiry",
    "insane",
    "insect",
    "inside",
    "inspire",
    "install",
    "intact",
    "interest",
    "into",
    "invest",
    "invite",
    "involve",
    "iron",
    "island",
    "isolate",
    "issue",
    "item",
    "ivory",
    "jacket",
    "jaguar",
    "jar",
    "jazz",
    "jealous",
    "jeans",
    "jelly",
    "jewel",
    "job",
    "join",
    "joke",
    "journey",
    "joy",
    "judge",
    "juice",
    "jump",
    "jungle",
    "junior",
    "junk",
    "just",
    "kangaroo",
    "keen",
    "keep",
    "ketchup",
    "key",
    "kick",
    "kid",
    "kidney",
    "kind",
    "kingdom",
    "kiss",
    "kit",
    "kitchen",
    "kite",
    "kitten",
    "kiwi",
    "knee",
    "knife",
    "knock",
    "know",
    "lab",
    "label",
    "labor",
    "ladder",
    "lady",
    "lake",
    "lamp",
    "language",
    "laptop",
    "large",
    "later",
    "latin",
    "laugh",
    "laundry",
    "lava",
    "law",
    "lawn",
    "lawsuit",
    "layer",
    "lazy",
    "leader",
    "leaf",
    "learn",
    "leave",
    "lecture",
    "left",
    "leg",
    "legal",
    "legend",
    "leisure",
    "lemon",
    "lend",
    "length",
    "lens",
    "leopard",
    "lesson",
    "letter",
    "level",
    "liar",
    "liberty",
    "library",
    "license",
    "life",
    "lift",
    "light",
    "like",
    "limb",
    "limit",
    "link",
    "lion",
    "liquid",
    "list",
    "little",
    "live",
    "lizard",
    "load",
    "loan",
    "lobster",
    "local",
    "lock",
    "logic",
    "lonely",
    "long",
    "loop",
    "lottery",
    "loud",
    "lounge",
    "love",
    "loyal",
    "lucky",
    "luggage",
    "lumber",
    "lunar",
    "lunch",
    "luxury",
    "lyrics",
    "machine",
    "mad",
    "magic",
    "magnet",
    "maid",
    "mail",
    "main",
    "major",
    "make",
    "mammal",
    "man",
    "manage",
    "mandate",
    "mango",
    "mansion",
    "manual",
    "maple",
    "marble",
    "march",
    "margin",
    "marine",
    "market",
    "marriage",
    "mask",
    "mass",
    "master",
    "match",
    "material",
    "math",
    "matrix",
    "matter",
    "maximum",
    "maze",
    "meadow",
    "mean",
    "measure",
    "meat",
    "mechanic",
    "medal",
    "media",
    "melody",
    "melt",
    "member",
    "memory",
    "mention",
    "menu",
    "mercy",
    "merge",
    "merit",
    "merry",
    "mesh",
    "message",
    "metal",
    "method",
    "middle",
    "midnight",
    "milk",
    "million",
    "mimic",
    "mind",
    "minimum",
    "minor",
    "minute",
    "miracle",
    "mirror",
    "misery",
    "miss",
    "mistake",
    "mix",
    "mixed",
    "mixture",
    "mobile",
    "model",
    "modify",
    "mom",
    "moment",
    "monitor",
    "monkey",
    "monster",
    "month",
    "moon",
    "moral",
    "more",
    "morning",
    "mosquito",
    "mother",
    "motion",
    "motor",
    "mountain",
    "mouse",
    "move",
    "movie",
    "much",
    "muffin",
    "mule",
    "multiply",
    "muscle",
    "museum",
    "mushroom",
    "music",
    "must",
    "mutual",
    "myself",
    "mystery",
    "myth",
    "naive",
    "name",
    "napkin",
    "narrow",
    "nasty",
    "nation",
    "nature",
    "near",
    "neck",
    "need",
    "negative",
    "neglect",
    "neither",
    "nephew",
    "nerve",
    "nest",
    "net",
    "network",
    "neutral",
    "never",
    "news",
    "next",
    "nice",
    "night",
    "noble",
    "noise",
    "nominee",
    "noodle",
    "normal",
    "north",
    "nose",
    "notable",
    "note",
    "nothing",
    "notice",
    "novel",
    "now",
    "nuclear",
    "number",
    "nurse",
    "nut",
    "oak",
    "obey",
    "object",
    "oblige",
    "obscure",
    "observe",
    "obtain",
    "obvious",
    "occur",
    "ocean",
    "october",
    "odor",
    "off",
    "offer",
    "office",
    "often",
    "oil",
    "okay",
    "old",
    "olive",
    "olympic",
    "omit",
    "once",
    "one",
    "onion",
    "online",
    "only",
    "open",
    "opera",
    "opinion",
    "oppose",
    "option",
    "orange",
    "orbit",
    "orchard",
    "order",
    "ordinary",
    "organ",
    "orient",
    "original",
    "orphan",
    "ostrich",
    "other",
    "outdoor",
    "outer",
    "output",
    "outside",
    "oval",
    "oven",
    "over",
    "own",
    "owner",
    "oxygen",
    "oyster",
    "ozone",
    "pact",
    "paddle",
    "page",
    "pair",
    "palace",
    "palm",
    "panda",
    "panel",
    "panic",
    "panther",
    "paper",
    "parade",
    "parent",
    "park",
    "parrot",
    "party",
    "pass",
    "patch",
    "path",
    "patient",
    "patrol",
    "pattern",
    "pause",
    "pave",
    "payment",
    "peace",
    "peanut",
    "pear",
    "peasant",
    "pelican",
    "pen",
    "penalty",
    "pencil",
    "people",
    "pepper",
    "perfect",
    "permit",
    "person",
    "pet",
    "phone",
    "photo",
    "phrase",
    "physical",
    "piano",
    "picnic",
    "picture",
    "piece",
    "pig",
    "pigeon",
    "pill",
    "pilot",
    "pink",
    "pioneer",
    "pipe",
    "pistol",
    "pitch",
    "pizza",
    "place",
    "planet",
    "plastic",
    "plate",
    "play",
    "please",
    "pledge",
    "pluck",
    "plug",
    "plunge",
    "poem",
    "poet",
    "point",
    "polar",
    "pole",
    "police",
    "pond",
    "pony",
    "pool",
    "popular",
    "portion",
    "position",
    "possible",
    "post",
    "potato",
    "pottery",
    "poverty",
    "powder",
    "power",
    "practice",
    "praise",
    "predict",
    "prefer",
    "prepare",
    "present",
    "pretty",
    "prevent",
    "price",
    "pride",
    "primary",
    "print",
    "priority",
    "prison",
    "private",
    "prize",
    "problem",
    "process",
    "produce",
    "profit",
    "program",
    "project",
    "promote",
    "proof",
    "property",
    "prosper",
    "protect",
    "proud",
    "provide",
    "public",
    "pudding",
    "pull",
    "pulp",
    "pulse",
    "pumpkin",
    "punch",
    "pupil",
    "puppy",
    "purchase",
    "purity",
    "purpose",
    "purse",
    "push",
    "put",
    "puzzle",
    "pyramid",
    "quality",
    "quantum",
    "quarter",
    "question",
    "quick",
    "quit",
    "quiz",
    "quote",
    "rabbit",
    "raccoon",
    "race",
    "rack",
    "radar",
    "radio",
    "rail",
    "rain",
    "raise",
    "rally",
    "ramp",
    "ranch",
    "random",
    "range",
    "rapid",
    "rare",
    "rate",
    "rather",
    "raven",
    "raw",
    "razor",
    "ready",
    "real",
    "reason",
    "rebel",
    "rebuild",
    "recall",
    "receive",
    "recipe",
    "record",
    "recycle",
    "reduce",
    "reflect",
    "reform",
    "refuse",
    "region",
    "regret",
    "regular",
    "reject",
    "relax",
    "release",
    "relief",
    "rely",
    "remain",
    "remember",
    "remind",
    "remove",
    "render",
    "renew",
    "rent",
    "reopen",
    "repair",
    "repeat",
    "replace",
    "report",
    "require",
    "rescue",
    "resemble",
    "resist",
    "resource",
    "response",
    "result",
    "retire",
    "retreat",
    "return",
    "reunion",
    "reveal",
    "review",
    "reward",
    "rhythm",
    "rib",
    "ribbon",
    "rice",
    "rich",
    "ride",
    "ridge",
    "rifle",
    "right",
    "rigid",
    "ring",
    "riot",
    "ripple",
    "risk",
    "ritual",
    "rival",
    "river",
    "road",
    "roast",
    "robot",
    "robust",
    "rocket",
    "romance",
    "roof",
    "rookie",
    "room",
    "rose",
    "rotate",
    "rough",
    "round",
    "route",
    "royal",
    "rubber",
    "rude",
    "rug",
    "rule",
    "run",
    "runway",
    "rural",
    "sad",
    "saddle",
    "sadness",
    "safe",
    "sail",
    "salad",
    "salmon",
    "salon",
    "salt",
    "salute",
    "same",
    "sample",
    "sand",
    "satisfy",
    "satoshi",
    "sauce",
    "sausage",
    "save",
    "say",
    "scale",
    "scan",
    "scare",
    "scatter",
    "scene",
    "scheme",
    "school",
    "science",
    "scissors",
    "scorpion",
    "scout",
    "scrap",
    "screen",
    "script",
    "scrub",
    "sea",
    "search",
    "season",
    "seat",
    "second",
    "secret",
    "section",
    "security",
    "seed",
    "seek",
    "segment",
    "select",
    "sell",
    "seminar",
    "senior",
    "sense",
    "sentence",
    "series",
    "service",
    "session",
    "settle",
    "setup",
    "seven",
    "shadow",
    "shaft",
    "shallow",
    "share",
    "shed",
    "shell",
    "sheriff",
    "shield",
    "shift",
    "shine",
    "ship",
    "shiver",
    "shock",
    "shoe",
    "shoot",
    "shop",
    "short",
    "shoulder",
    "shove",
    "shrimp",
    "shrug",
    "shuffle",
    "shy",
    "sibling",
    "sick",
    "side",
    "siege",
    "sight",
    "sign",
    "silent",
    "silk",
    "silly",
    "silver",
    "similar",
    "simple",
    "since",
    "sing",
    "siren",
    "sister",
    "situate",
    "six",
    "size",
    "skate",
    "sketch",
    "ski",
    "skill",
    "skin",
    "skirt",
    "skull",
    "slab",
    "slam",
    "sleep",
    "slender",
    "slice",
    "slide",
    "slight",
    "slim",
    "slogan",
    "slot",
    "slow",
    "slush",
    "small",
    "smart",
    "smile",
    "smoke",
    "smooth",
    "snack",
    "snake",
    "snap",
    "sniff",
    "snow",
    "soap",
    "soccer",
    "social",
    "sock",
    "soda",
    "soft",
    "solar",
    "soldier",
    "solid",
    "solution",
    "solve",
    "someone",
    "song",
    "soon",
    "sorry",
    "sort",
    "soul",
    "sound",
    "soup",
    "source",
    "south",
    "space",
    "spare",
    "spatial",
    "spawn",
    "speak",
    "special",
    "speed",
    "spell",
    "spend",
    "sphere",
    "spice",
    "spider",
    "spike",
    "spin",
    "spirit",
    "split",
    "spoil",
    "sponsor",
    "spoon",
    "sport",
    "spot",
    "spray",
    "spread",
    "spring",
    "spy",
    "square",
    "squeeze",
    "squirrel",
    "stable",
    "stadium",
    "staff",
    "stage",
    "stairs",
    "stamp",
    "stand",
    "start",
    "state",
    "stay",
    "steak",
    "steel",
    "stem",
    "step",
    "stereo",
    "stick",
    "still",
    "sting",
    "stock",
    "stomach",
    "stone",
    "stool",
    "story",
    "stove",
    "strategy",
    "street",
    "strike",
    "strong",
    "struggle",
    "student",
    "stuff",
    "stumble",
    "style",
    "subject",
    "submit",
    "subway",
    "success",
    "such",
    "sudden",
    "suffer",
    "sugar",
    "suggest",
    "suit",
    "summer",
    "sun",
    "sunny",
    "sunset",
    "super",
    "supply",
    "supreme",
    "sure",
    "surface",
    "surge",
    "surprise",
    "surround",
    "survey",
    "suspect",
    "sustain",
    "swallow",
    "swamp",
    "swap",
    "swarm",
    "swear",
    "sweet",
    "swift",
    "swim",
    "swing",
    "switch",
    "sword",
    "symbol",
    "symptom",
    "syrup",
    "system",
    "table",
    "tackle",
    "tag",
    "tail",
    "talent",
    "talk",
    "tank",
    "tape",
    "target",
    "task",
    "taste",
    "tattoo",
    "taxi",
    "teach",
    "team",
    "tell",
    "ten",
    "tenant",
    "tennis",
    "tent",
    "term",
    "test",
    "text",
    "thank",
    "that",
    "theme",
    "then",
    "theory",
    "there",
    "they",
    "thing",
    "this",
    "thought",
    "three",
    "thrive",
    "throw",
    "thumb",
    "thunder",
    "ticket",
    "tide",
    "tiger",
    "tilt",
    "timber",
    "time",
    "tiny",
    "tip",
    "tired",
    "tissue",
    "title",
    "toast",
    "tobacco",
    "today",
    "toddler",
    "toe",
    "together",
    "toilet",
    "token",
    "tomato",
    "tomorrow",
    "tone",
    "tongue",
    "tonight",
    "tool",
    "tooth",
    "top",
    "topic",
    "topple",
    "torch",
    "tornado",
    "tortoise",
    "toss",
    "total",
    "tourist",
    "toward",
    "tower",
    "town",
    "toy",
    "track",
    "trade",
    "traffic",
    "tragic",
    "train",
    "transfer",
    "trap",
    "trash",
    "travel",
    "tray",
    "treat",
    "tree",
    "trend",
    "trial",
    "tribe",
    "trick",
    "trigger",
    "trim",
    "trip",
    "trophy",
    "trouble",
    "truck",
    "true",
    "truly",
    "trumpet",
    "trust",
    "truth",
    "try",
    "tube",
    "tuition",
    "tumble",
    "tuna",
    "tunnel",
    "turkey",
    "turn",
    "turtle",
    "twelve",
    "twenty",
    "twice",
    "twin",
    "twist",
    "two",
    "type",
    "typical",
    "ugly",
    "umbrella",
    "unable",
    "unaware",
    "uncle",
    "uncover",
    "under",
    "undo",
    "unfair",
    "unfold",
    "unhappy",
    "uniform",
    "unique",
    "unit",
    "universe",
    "unknown",
    "unlock",
    "until",
    "unusual",
    "unveil",
    "update",
    "upgrade",
    "uphold",
    "upon",
    "upper",
    "upset",
    "urban",
    "urge",
    "usage",
    "use",
    "used",
    "useful",
    "useless",
    "usual",
    "utility",
    "vacant",
    "vacuum",
    "vague",
    "valid",
    "valley",
    "valve",
    "van",
    "vanish",
    "vapor",
    "various",
    "vast",
    "vault",
    "vehicle",
    "velvet",
    "vendor",
    "venture",
    "venue",
    "verb",
    "verify",
    "version",
    "very",
    "vessel",
    "veteran",
    "viable",
    "vibrant",
    "vicious",
    "victory",
    "video",
    "view",
    "village",
    "vintage",
    "violin",
    "virtual",
    "virus",
    "visa",
    "visit",
    "visual",
    "vital",
    "vivid",
    "vocal",
    "voice",
    "void",
    "volcano",
    "volume",
    "vote",
    "voyage",
    "wage",
    "wagon",
    "wait",
    "walk",
    "wall",
    "walnut",
    "want",
    "warfare",
    "warm",
    "warrior",
    "wash",
    "wasp",
    "waste",
    "water",
    "wave",
    "way",
    "wealth",
    "weapon",
    "wear",
    "weasel",
    "weather",
    "web",
    "wedding",
    "weekend",
    "weird",
    "welcome",
    "west",
    "wet",
    "whale",
    "what",
    "wheat",
    "wheel",
    "when",
    "where",
    "whip",
    "whisper",
    "wide",
    "width",
    "wife",
    "wild",
    "will",
    "win",
    "window",
    "wine",
    "wing",
    "wink",
    "winner",
    "winter",
    "wire",
    "wisdom",
    "wise",
    "wish",
    "witness",
    "wolf",
    "woman",
    "wonder",
    "wood",
    "wool",
    "word",
    "work",
    "world",
    "worry",
    "worth",
    "wrap",
    "wreck",
    "wrestle",
    "wrist",
    "write",
    "wrong",
    "yard",
    "year",
    "yellow",
    "you",
    "young",
    "youth",
    "zebra",
    "zero",
    "zone",
    "zoo"
};

/* ── Wordlist access ──────────────────────────────────────────────── */

const char *domain_wallet_mnemonic_wordlist_get(int index)
{
    if (index < 0 || index >= DOMAIN_WALLET_BIP39_WORDLIST_SIZE)
        return NULL;
    return bip39_english[index];
}

int domain_wallet_mnemonic_wordlist_find(const char *word)
{
    if (!word)
        return -1;

    /* Binary search — the BIP39 English wordlist is lexicographically
     * sorted by construction (BIP39 spec invariant). */
    int lo = 0, hi = DOMAIN_WALLET_BIP39_WORDLIST_SIZE - 1;
    while (lo <= hi) {
        int mid = lo + (hi - lo) / 2;
        int cmp = strcmp(word, bip39_english[mid]);
        if (cmp == 0)
            return mid;
        if (cmp < 0)
            hi = mid - 1;
        else
            lo = mid + 1;
    }
    return -1;
}

/* ── Internal helpers ─────────────────────────────────────────────── */

/* Get bit at position `pos` from a byte array (MSB-first). */
static inline int get_bit(const uint8_t *data, int pos)
{
    return (data[pos / 8] >> (7 - (pos % 8))) & 1;
}

static bool valid_entropy_len(size_t entropy_len)
{
    return entropy_len == 16 || entropy_len == 20 || entropy_len == 24
        || entropy_len == 28 || entropy_len == 32;
}

size_t domain_wallet_mnemonic_entropy_bytes_for_words(int word_count)
{
    switch (word_count) {
    case 12: return 16;
    case 15: return 20;
    case 18: return 24;
    case 21: return 28;
    case 24: return 32;
    default: return 0;
    }
}

/* ── Entropy → mnemonic ───────────────────────────────────────────── */

struct zcl_result domain_wallet_mnemonic_from_entropy(
        const uint8_t *entropy,
        size_t entropy_len,
        char *phrase_out,
        size_t phrase_size,
        size_t *written_out)
{
    if (!entropy)
        return ZCL_ERR(DOMAIN_WALLET_MNEMONIC_ERR_NULL_ENTROPY,
                       "mnemonic_from_entropy: null entropy");
    if (!phrase_out)
        return ZCL_ERR(DOMAIN_WALLET_MNEMONIC_ERR_NULL_BUF,
                       "mnemonic_from_entropy: null phrase_out");
    if (!written_out)
        return ZCL_ERR(DOMAIN_WALLET_MNEMONIC_ERR_NULL_OUT,
                       "mnemonic_from_entropy: null written_out");
    if (!valid_entropy_len(entropy_len))
        return ZCL_ERR(DOMAIN_WALLET_MNEMONIC_ERR_ENTROPY_LEN,
                       "mnemonic_from_entropy: bad entropy_len=%zu (need 16/20/24/28/32)",
                       entropy_len);
    if (phrase_size == 0)
        return ZCL_ERR(DOMAIN_WALLET_MNEMONIC_ERR_BUF_TOO_SMALL,
                       "mnemonic_from_entropy: zero phrase_size");

    size_t ent_bits = entropy_len * 8;
    size_t cs_bits = ent_bits / 32;
    size_t total_bits = ent_bits + cs_bits;
    size_t word_count = total_bits / 11;

    /* SHA-256 over entropy → take first cs_bits bits as the checksum. */
    uint8_t hash[32];
    struct sha256_ctx ctx;
    sha256_init(&ctx);
    sha256_write(&ctx, entropy, entropy_len);
    sha256_finalize(&ctx, hash);

    /* Build combined buffer: entropy || one byte of checksum. cs_bits
     * is at most 8 (entropy_len ≤ 32 → cs_bits ≤ 8), so 1 byte suffices. */
    uint8_t combined[33];
    memcpy(combined, entropy, entropy_len);
    combined[entropy_len] = hash[0];

    /* Pack 11-bit groups into words. */
    struct zcl_result result = ZCL_OK;
    size_t offset = 0;
    for (size_t w = 0; w < word_count; w++) {
        int index = 0;
        for (int b = 0; b < 11; b++)
            index = (index << 1) | get_bit(combined, (int)(w * 11 + b));

        const char *word = bip39_english[index];
        size_t wlen = strlen(word);

        if (w > 0) {
            if (offset + 1 >= phrase_size) {
                result = ZCL_ERR(DOMAIN_WALLET_MNEMONIC_ERR_BUF_TOO_SMALL,
                                 "mnemonic_from_entropy: phrase_size=%zu too small",
                                 phrase_size);
                goto cleanup;
            }
            phrase_out[offset++] = ' ';
        }
        if (offset + wlen >= phrase_size) {
            result = ZCL_ERR(DOMAIN_WALLET_MNEMONIC_ERR_BUF_TOO_SMALL,
                             "mnemonic_from_entropy: phrase_size=%zu too small",
                             phrase_size);
            goto cleanup;
        }
        memcpy(phrase_out + offset, word, wlen);
        offset += wlen;
    }
    phrase_out[offset] = '\0';
    *written_out = offset;

cleanup:
    /* Wipe the SHA-256 checksum hash and the entropy||checksum buffer —
     * both hold secret material derived from the input entropy. The
     * phrase has already been emitted by this point (success path) or
     * is incomplete (error path); neither buffer is read after here. */
    memory_cleanse(hash, sizeof(hash));
    memory_cleanse(combined, sizeof(combined));
    return result;
}

/* ── Mnemonic → entropy (with checksum validation) ────────────────── */

/* Shared decode core — parses words, packs bits, verifies checksum,
 * optionally writes entropy. Used by mnemonic_to_entropy and validate. */
static struct zcl_result mnemonic_decode_core(
        const char *phrase,
        uint8_t *entropy_out,
        size_t *entropy_len_out)
{
    if (!phrase)
        return ZCL_ERR(DOMAIN_WALLET_MNEMONIC_ERR_NULL_PHRASE,
                       "mnemonic decode: null phrase");

    /* Secret-bearing buffers, declared up front and zero-initialised so
     * every error path can route through `cleanup` and wipe them, even
     * when an early return leaves some of them unpopulated. */
    char buf[DOMAIN_WALLET_BIP39_PHRASE_MAX];   /* mutable phrase copy   */
    uint8_t data[33];                            /* packed 11-bit groups  */
    uint8_t entropy[32];                         /* recovered entropy     */
    uint8_t hash[32];                            /* checksum SHA-256       */
    memset(buf, 0, sizeof(buf));
    memset(data, 0, sizeof(data));
    memset(entropy, 0, sizeof(entropy));
    memset(hash, 0, sizeof(hash));

    struct zcl_result result = ZCL_OK;

    /* Internal mutable copy for strtok_r. Cap at phrase max to avoid
     * unbounded stacks; matches the legacy wrapper's behaviour. */
    size_t len = strlen(phrase);
    if (len >= sizeof(buf)) {
        result = ZCL_ERR(DOMAIN_WALLET_MNEMONIC_ERR_PHRASE_LEN,
                         "mnemonic decode: phrase too long: %zu (cap %zu)",
                         len, sizeof(buf));
        goto cleanup;
    }
    memcpy(buf, phrase, len + 1);

    int indices[DOMAIN_WALLET_BIP39_MAX_WORDS];
    int word_count = 0;

    char *saveptr = NULL;
    char *token = strtok_r(buf, " ", &saveptr);
    while (token) {
        if (word_count >= DOMAIN_WALLET_BIP39_MAX_WORDS) {
            result = ZCL_ERR(DOMAIN_WALLET_MNEMONIC_ERR_BAD_WORD_COUNT,
                             "mnemonic decode: more than %d words",
                             DOMAIN_WALLET_BIP39_MAX_WORDS);
            goto cleanup;
        }
        int idx = domain_wallet_mnemonic_wordlist_find(token);
        if (idx < 0) {
            result = ZCL_ERR(DOMAIN_WALLET_MNEMONIC_ERR_UNKNOWN_WORD,
                             "mnemonic decode: unknown word: %s", token);
            goto cleanup;
        }
        indices[word_count++] = idx;
        token = strtok_r(NULL, " ", &saveptr);
    }

    if (word_count != 12 && word_count != 15 && word_count != 18
        && word_count != 21 && word_count != 24) {
        result = ZCL_ERR(DOMAIN_WALLET_MNEMONIC_ERR_BAD_WORD_COUNT,
                         "mnemonic decode: bad word count: %d", word_count);
        goto cleanup;
    }

    size_t total_bits = (size_t)word_count * 11;
    size_t cs_bits = (size_t)word_count / 3;
    size_t ent_bits = total_bits - cs_bits;
    size_t entropy_len = ent_bits / 8;

    /* Pack 11-bit indices into a byte array. */
    for (int w = 0; w < word_count; w++) {
        for (int b = 0; b < 11; b++) {
            int bit_pos = w * 11 + b;
            if (indices[w] & (1 << (10 - b)))
                data[bit_pos / 8] |= (uint8_t)(1 << (7 - (bit_pos % 8)));
        }
    }

    /* Verify checksum: SHA-256 prefix over the entropy bytes equals
     * the trailing cs_bits in `data`. */
    memcpy(entropy, data, entropy_len);

    struct sha256_ctx ctx;
    sha256_init(&ctx);
    sha256_write(&ctx, entropy, entropy_len);
    sha256_finalize(&ctx, hash);

    uint8_t actual_cs = data[entropy_len];
    uint8_t expected_cs = hash[0];
    uint8_t mask = (uint8_t)(0xFF << (8 - cs_bits));
    if ((actual_cs & mask) != (expected_cs & mask)) {
        result = ZCL_ERR(DOMAIN_WALLET_MNEMONIC_ERR_CHECKSUM,
                         "mnemonic decode: checksum mismatch");
        goto cleanup;
    }

    if (entropy_out)
        memcpy(entropy_out, entropy, entropy_len);
    if (entropy_len_out)
        *entropy_len_out = entropy_len;

cleanup:
    /* Wipe every secret-bearing buffer on all exit paths. The entropy
     * has already been copied to entropy_out (success path) before this
     * point; on error paths nothing was emitted. */
    memory_cleanse(buf, sizeof(buf));
    memory_cleanse(data, sizeof(data));
    memory_cleanse(entropy, sizeof(entropy));
    memory_cleanse(hash, sizeof(hash));
    return result;
}

struct zcl_result domain_wallet_mnemonic_to_entropy(
        const char *phrase,
        uint8_t *entropy_out,
        size_t entropy_capacity,
        size_t *entropy_len_out)
{
    if (!entropy_out)
        return ZCL_ERR(DOMAIN_WALLET_MNEMONIC_ERR_NULL_OUT,
                       "mnemonic_to_entropy: null entropy_out");
    if (!entropy_len_out)
        return ZCL_ERR(DOMAIN_WALLET_MNEMONIC_ERR_NULL_OUT,
                       "mnemonic_to_entropy: null entropy_len_out");
    if (entropy_capacity < DOMAIN_WALLET_BIP39_ENTROPY_MAX)
        return ZCL_ERR(DOMAIN_WALLET_MNEMONIC_ERR_BUF_TOO_SMALL,
                       "mnemonic_to_entropy: entropy_capacity=%zu < %d",
                       entropy_capacity, DOMAIN_WALLET_BIP39_ENTROPY_MAX);

    *entropy_len_out = 0;
    return mnemonic_decode_core(phrase, entropy_out, entropy_len_out);
}

struct zcl_result domain_wallet_mnemonic_validate(const char *phrase)
{
    return mnemonic_decode_core(phrase, NULL, NULL);
}

/* ── PBKDF2 seed derivation ───────────────────────────────────────── */

struct zcl_result domain_wallet_mnemonic_to_seed(
        const char *phrase,
        const char *passphrase,
        uint8_t *seed_out,
        size_t seed_capacity)
{
    if (!phrase)
        return ZCL_ERR(DOMAIN_WALLET_MNEMONIC_ERR_NULL_PHRASE,
                       "mnemonic_to_seed: null phrase");
    if (!seed_out)
        return ZCL_ERR(DOMAIN_WALLET_MNEMONIC_ERR_NULL_SEED,
                       "mnemonic_to_seed: null seed_out");
    if (seed_capacity < DOMAIN_WALLET_BIP39_SEED_BYTES)
        return ZCL_ERR(DOMAIN_WALLET_MNEMONIC_ERR_BUF_TOO_SMALL,
                       "mnemonic_to_seed: seed_capacity=%zu < %d",
                       seed_capacity, DOMAIN_WALLET_BIP39_SEED_BYTES);

    if (!passphrase)
        passphrase = "";

    /* Salt = "mnemonic" || passphrase. Bounded so we never allocate. */
    const char *prefix = "mnemonic";
    size_t prefix_len = 8;
    size_t pass_len = strlen(passphrase);
    size_t salt_len = prefix_len + pass_len;

    uint8_t salt[256];
    if (salt_len > sizeof(salt))
        return ZCL_ERR(DOMAIN_WALLET_MNEMONIC_ERR_PHRASE_LEN,
                       "mnemonic_to_seed: passphrase too long: %zu (cap %zu)",
                       pass_len, sizeof(salt) - prefix_len);
    memcpy(salt, prefix, prefix_len);
    memcpy(salt + prefix_len, passphrase, pass_len);

    pbkdf2_hmac_sha512((const uint8_t *)phrase, strlen(phrase),
                       salt, salt_len,
                       DOMAIN_WALLET_BIP39_PBKDF2_ROUNDS,
                       seed_out, DOMAIN_WALLET_BIP39_SEED_BYTES);

    /* Wipe the salt — it embeds the secret passphrase. PBKDF2 has
     * already consumed it into seed_out, so it is dead after this. */
    memory_cleanse(salt, sizeof(salt));
    return ZCL_OK;
}
