/** * XmrUtils - Standalone Monero cryptocurrency utilities * * STANDALONE USAGE (outside Bitrequest): * ---------------------------------------- * * * * * * FEATURES: * - Monero key derivation (spend key, view key, subaddresses) * - Address generation and validation * - Transaction parsing and payment detection * - RingCT amount decryption * - Payment ID handling * - Mnemonic seed conversion * * DEPENDENCIES: * - sjcl.js * - crypto_utils.js (for hex_to_bytes, bytes_to_hex, keccak_256, egcd, etc.) * * @version 1.1.0 * @license AGPL-3.0 * @see https://github.com/bitrequest/bitrequest.github.io */ // ============================================ // CONSTANTS // ============================================ const l = 7237005577332262213973186563042994240857116359379907606001950938285454250989n, xmr_words = [ "abbey", "abducts", "ability", "ablaze", "abnormal", "abort", "abrasive", "absorb", "abyss", "academy", "aces", "aching", "acidic", "acoustic", "acquire", "across", "actress", "acumen", "adapt", "addicted", "adept", "adhesive", "adjust", "adopt", "adrenalin", "adult", "adventure", "aerial", "afar", "affair", "afield", "afloat", "afoot", "afraid", "after", "against", "agenda", "aggravate", "agile", "aglow", "agnostic", "agony", "agreed", "ahead", "aided", "ailments", "aimless", "airport", "aisle", "ajar", "akin", "alarms", "album", "alchemy", "alerts", "algebra", "alkaline", "alley", "almost", "aloof", "alpine", "already", "also", "altitude", "alumni", "always", "amaze", "ambush", "amended", "amidst", "ammo", "amnesty", "among", "amply", "amused", "anchor", "android", "anecdote", "angled", "ankle", "annoyed", "answers", "antics", "anvil", "anxiety", "anybody", "apart", "apex", "aphid", "aplomb", "apology", "apply", "apricot", "aptitude", "aquarium", "arbitrary", "archer", "ardent", "arena", "argue", "arises", "army", "around", "arrow", "arsenic", "artistic", "ascend", "ashtray", "aside", "asked", "asleep", "aspire", "assorted", "asylum", "athlete", "atlas", "atom", "atrium", "attire", "auburn", "auctions", "audio", "august", "aunt", "austere", "autumn", "avatar", "avidly", "avoid", "awakened", "awesome", "awful", "awkward", "awning", "awoken", "axes", "axis", "axle", "aztec", "azure", "baby", "bacon", "badge", "baffles", "bagpipe", "bailed", "bakery", "balding", "bamboo", "banjo", "baptism", "basin", "batch", "bawled", "bays", "because", "beer", "befit", "begun", "behind", "being", "below", "bemused", "benches", "berries", "bested", "betting", "bevel", "beware", "beyond", "bias", "bicycle", "bids", "bifocals", "biggest", "bikini", "bimonthly", "binocular", "biology", "biplane", "birth", "biscuit", "bite", "biweekly", "blender", "blip", "bluntly", "boat", "bobsled", "bodies", "bogeys", "boil", "boldly", "bomb", "border", "boss", "both", "bounced", "bovine", "bowling", "boxes", "boyfriend", "broken", "brunt", "bubble", "buckets", "budget", "buffet", "bugs", "building", "bulb", "bumper", "bunch", "business", "butter", "buying", "buzzer", "bygones", "byline", "bypass", "cabin", "cactus", "cadets", "cafe", "cage", "cajun", "cake", "calamity", "camp", "candy", "casket", "catch", "cause", "cavernous", "cease", "cedar", "ceiling", "cell", "cement", "cent", "certain", "chlorine", "chrome", "cider", "cigar", "cinema", "circle", "cistern", "citadel", "civilian", "claim", "click", "clue", "coal", "cobra", "cocoa", "code", "coexist", "coffee", "cogs", "cohesive", "coils", "colony", "comb", "cool", "copy", "corrode", "costume", "cottage", "cousin", "cowl", "criminal", "cube", "cucumber", "cuddled", "cuffs", "cuisine", "cunning", "cupcake", "custom", "cycling", "cylinder", "cynical", "dabbing", "dads", "daft", "dagger", "daily", "damp", "dangerous", "dapper", "darted", "dash", "dating", "dauntless", "dawn", "daytime", "dazed", "debut", "decay", "dedicated", "deepest", "deftly", "degrees", "dehydrate", "deity", "dejected", "delayed", "demonstrate", "dented", "deodorant", "depth", "desk", "devoid", "dewdrop", "dexterity", "dialect", "dice", "diet", "different", "digit", "dilute", "dime", "dinner", "diode", "diplomat", "directed", "distance", "ditch", "divers", "dizzy", "doctor", "dodge", "does", "dogs", "doing", "dolphin", "domestic", "donuts", "doorway", "dormant", "dosage", "dotted", "double", "dove", "down", "dozen", "dreams", "drinks", "drowning", "drunk", "drying", "dual", "dubbed", "duckling", "dude", "duets", "duke", "dullness", "dummy", "dunes", "duplex", "duration", "dusted", "duties", "dwarf", "dwelt", "dwindling", "dying", "dynamite", "dyslexic", "each", "eagle", "earth", "easy", "eating", "eavesdrop", "eccentric", "echo", "eclipse", "economics", "ecstatic", "eden", "edgy", "edited", "educated", "eels", "efficient", "eggs", "egotistic", "eight", "either", "eject", "elapse", "elbow", "eldest", "eleven", "elite", "elope", "else", "eluded", "emails", "ember", "emerge", "emit", "emotion", "empty", "emulate", "energy", "enforce", "enhanced", "enigma", "enjoy", "enlist", "enmity", "enough", "enraged", "ensign", "entrance", "envy", "epoxy", "equip", "erase", "erected", "erosion", "error", "eskimos", "espionage", "essential", "estate", "etched", "eternal", "ethics", "etiquette", "evaluate", "evenings", "evicted", "evolved", "examine", "excess", "exhale", "exit", "exotic", "exquisite", "extra", "exult", "fabrics", "factual", "fading", "fainted", "faked", "fall", "family", "fancy", "farming", "fatal", "faulty", "fawns", "faxed", "fazed", "feast", "february", "federal", "feel", "feline", "females", "fences", "ferry", "festival", "fetches", "fever", "fewest", "fiat", "fibula", "fictional", "fidget", "fierce", "fifteen", "fight", "films", "firm", "fishing", "fitting", "five", "fixate", "fizzle", "fleet", "flippant", "flying", "foamy", "focus", "foes", "foggy", "foiled", "folding", "fonts", "foolish", "fossil", "fountain", "fowls", "foxes", "foyer", "framed", "friendly", "frown", "fruit", "frying", "fudge", "fuel", "fugitive", "fully", "fuming", "fungal", "furnished", "fuselage", "future", "fuzzy", "gables", "gadget", "gags", "gained", "galaxy", "gambit", "gang", "gasp", "gather", "gauze", "gave", "gawk", "gaze", "gearbox", "gecko", "geek", "gels", "gemstone", "general", "geometry", "germs", "gesture", "getting", "geyser", "ghetto", "ghost", "giant", "giddy", "gifts", "gigantic", "gills", "gimmick", "ginger", "girth", "giving", "glass", "gleeful", "glide", "gnaw", "gnome", "goat", "goblet", "godfather", "goes", "goggles", "going", "goldfish", "gone", "goodbye", "gopher", "gorilla", "gossip", "gotten", "gourmet", "governing", "gown", "greater", "grunt", "guarded", "guest", "guide", "gulp", "gumball", "guru", "gusts", "gutter", "guys", "gymnast", "gypsy", "gyrate", "habitat", "hacksaw", "haggled", "hairy", "hamburger", "happens", "hashing", "hatchet", "haunted", "having", "hawk", "haystack", "hazard", "hectare", "hedgehog", "heels", "hefty", "height", "hemlock", "hence", "heron", "hesitate", "hexagon", "hickory", "hiding", "highway", "hijack", "hiker", "hills", "himself", "hinder", "hippo", "hire", "history", "hitched", "hive", "hoax", "hobby", "hockey", "hoisting", "hold", "honked", "hookup", "hope", "hornet", "hospital", "hotel", "hounded", "hover", "howls", "hubcaps", "huddle", "huge", "hull", "humid", "hunter", "hurried", "husband", "huts", "hybrid", "hydrogen", "hyper", "iceberg", "icing", "icon", "identity", "idiom", "idled", "idols", "igloo", "ignore", "iguana", "illness", "imagine", "imbalance", "imitate", "impel", "inactive", "inbound", "incur", "industrial", "inexact", "inflamed", "ingested", "initiate", "injury", "inkling", "inline", "inmate", "innocent", "inorganic", "input", "inquest", "inroads", "insult", "intended", "inundate", "invoke", "inwardly", "ionic", "irate", "iris", "irony", "irritate", "island", "isolated", "issued", "italics", "itches", "items", "itinerary", "itself", "ivory", "jabbed", "jackets", "jaded", "jagged", "jailed", "jamming", "january", "jargon", "jaunt", "javelin", "jaws", "jazz", "jeans", "jeers", "jellyfish", "jeopardy", "jerseys", "jester", "jetting", "jewels", "jigsaw", "jingle", "jittery", "jive", "jobs", "jockey", "jogger", "joining", "joking", "jolted", "jostle", "journal", "joyous", "jubilee", "judge", "juggled", "juicy", "jukebox", "july", "jump", "junk", "jury", "justice", "juvenile", "kangaroo", "karate", "keep", "kennel", "kept", "kernels", "kettle", "keyboard", "kickoff", "kidneys", "king", "kiosk", "kisses", "kitchens", "kiwi", "knapsack", "knee", "knife", "knowledge", "knuckle", "koala", "laboratory", "ladder", "lagoon", "lair", "lakes", "lamb", "language", "laptop", "large", "last", "later", "launching", "lava", "lawsuit", "layout", "lazy", "lectures", "ledge", "leech", "left", "legion", "leisure", "lemon", "lending", "leopard", "lesson", "lettuce", "lexicon", "liar", "library", "licks", "lids", "lied", "lifestyle", "light", "likewise", "lilac", "limits", "linen", "lion", "lipstick", "liquid", "listen", "lively", "loaded", "lobster", "locker", "lodge", "lofty", "logic", "loincloth", "long", "looking", "lopped", "lordship", "losing", "lottery", "loudly", "love", "lower", "loyal", "lucky", "luggage", "lukewarm", "lullaby", "lumber", "lunar", "lurk", "lush", "luxury", "lymph", "lynx", "lyrics", "macro", "madness", "magically", "mailed", "major", "makeup", "malady", "mammal", "maps", "masterful", "match", "maul", "maverick", "maximum", "mayor", "maze", "meant", "mechanic", "medicate", "meeting", "megabyte", "melting", "memoir", "menu", "merger", "mesh", "metro", "mews", "mice", "midst", "mighty", "mime", "mirror", "misery", "mittens", "mixture", "moat", "mobile", "mocked", "mohawk", "moisture", "molten", "moment", "money", "moon", "mops", "morsel", "mostly", "motherly", "mouth", "movement", "mowing", "much", "muddy", "muffin", "mugged", "mullet", "mumble", "mundane", "muppet", "mural", "musical", "muzzle", "myriad", "mystery", "myth", "nabbing", "nagged", "nail", "names", "nanny", "napkin", "narrate", "nasty", "natural", "nautical", "navy", "nearby", "necklace", "needed", "negative", "neither", "neon", "nephew", "nerves", "nestle", "network", "neutral", "never", "newt", "nexus", "nibs", "niche", "niece", "nifty", "nightly", "nimbly", "nineteen", "nirvana", "nitrogen", "nobody", "nocturnal", "nodes", "noises", "nomad", "noodles", "northern", "nostril", "noted", "nouns", "novelty", "nowhere", "nozzle", "nuance", "nucleus", "nudged", "nugget", "nuisance", "null", "number", "nuns", "nurse", "nutshell", "nylon", "oaks", "oars", "oasis", "oatmeal", "obedient", "object", "obliged", "obnoxious", "observant", "obtains", "obvious", "occur", "ocean", "october", "odds", "odometer", "offend", "often", "oilfield", "ointment", "okay", "older", "olive", "olympics", "omega", "omission", "omnibus", "onboard", "oncoming", "oneself", "ongoing", "onion", "online", "onslaught", "onto", "onward", "oozed", "opacity", "opened", "opposite", "optical", "opus", "orange", "orbit", "orchid", "orders", "organs", "origin", "ornament", "orphans", "oscar", "ostrich", "otherwise", "otter", "ouch", "ought", "ounce", "ourselves", "oust", "outbreak", "oval", "oven", "owed", "owls", "owner", "oxidant", "oxygen", "oyster", "ozone", "pact", "paddles", "pager", "pairing", "palace", "pamphlet", "pancakes", "paper", "paradise", "pastry", "patio", "pause", "pavements", "pawnshop", "payment", "peaches", "pebbles", "peculiar", "pedantic", "peeled", "pegs", "pelican", "pencil", "people", "pepper", "perfect", "pests", "petals", "phase", "pheasants", "phone", "phrases", "physics", "piano", "picked", "pierce", "pigment", "piloted", "pimple", "pinched", "pioneer", "pipeline", "pirate", "pistons", "pitched", "pivot", "pixels", "pizza", "playful", "pledge", "pliers", "plotting", "plus", "plywood", "poaching", "pockets", "podcast", "poetry", "point", "poker", "polar", "ponies", "pool", "popular", "portents", "possible", "potato", "pouch", "poverty", "powder", "pram", "present", "pride", "problems", "pruned", "prying", "psychic", "public", "puck", "puddle", "puffin", "pulp", "pumpkins", "punch", "puppy", "purged", "push", "putty", "puzzled", "pylons", "pyramid", "python", "queen", "quick", "quote", "rabbits", "racetrack", "radar", "rafts", "rage", "railway", "raking", "rally", "ramped", "randomly", "rapid", "rarest", "rash", "rated", "ravine", "rays", "razor", "react", "rebel", "recipe", "reduce", "reef", "refer", "regular", "reheat", "reinvest", "rejoices", "rekindle", "relic", "remedy", "renting", "reorder", "repent", "request", "reruns", "rest", "return", "reunion", "revamp", "rewind", "rhino", "rhythm", "ribbon", "richly", "ridges", "rift", "rigid", "rims", "ringing", "riots", "ripped", "rising", "ritual", "river", "roared", "robot", "rockets", "rodent", "rogue", "roles", "romance", "roomy", "roped", "roster", "rotate", "rounded", "rover", "rowboat", "royal", "ruby", "rudely", "ruffled", "rugged", "ruined", "ruling", "rumble", "runway", "rural", "rustled", "ruthless", "sabotage", "sack", "sadness", "safety", "saga", "sailor", "sake", "salads", "sample", "sanity", "sapling", "sarcasm", "sash", "satin", "saucepan", "saved", "sawmill", "saxophone", "sayings", "scamper", "scenic", "school", "science", "scoop", "scrub", "scuba", "seasons", "second", "sedan", "seeded", "segments", "seismic", "selfish", "semifinal", "sensible", "september", "sequence", "serving", "session", "setup", "seventh", "sewage", "shackles", "shelter", "shipped", "shocking", "shrugged", "shuffled", "shyness", "siblings", "sickness", "sidekick", "sieve", "sifting", "sighting", "silk", "simplest", "sincerely", "sipped", "siren", "situated", "sixteen", "sizes", "skater", "skew", "skirting", "skulls", "skydive", "slackens", "sleepless", "slid", "slower", "slug", "smash", "smelting", "smidgen", "smog", "smuggled", "snake", "sneeze", "sniff", "snout", "snug", "soapy", "sober", "soccer", "soda", "software", "soggy", "soil", "solved", "somewhere", "sonic", "soothe", "soprano", "sorry", "southern", "sovereign", "sowed", "soya", "space", "speedy", "sphere", "spiders", "splendid", "spout", "sprig", "spud", "spying", "square", "stacking", "stellar", "stick", "stockpile", "strained", "stunning", "stylishly", "subtly", "succeed", "suddenly", "suede", "suffice", "sugar", "suitcase", "sulking", "summon", "sunken", "superior", "surfer", "sushi", "suture", "swagger", "swept", "swiftly", "sword", "swung", "syllabus", "symptoms", "syndrome", "syringe", "system", "taboo", "tacit", "tadpoles", "tagged", "tail", "taken", "talent", "tamper", "tanks", "tapestry", "tarnished", "tasked", "tattoo", "taunts", "tavern", "tawny", "taxi", "teardrop", "technical", "tedious", "teeming", "tell", "template", "tender", "tepid", "tequila", "terminal", "testing", "tether", "textbook", "thaw", "theatrics", "thirsty", "thorn", "threaten", "thumbs", "thwart", "ticket", "tidy", "tiers", "tiger", "tilt", "timber", "tinted", "tipsy", "tirade", "tissue", "titans", "toaster", "tobacco", "today", "toenail", "toffee", "together", "toilet", "token", "tolerant", "tomorrow", "tonic", "toolbox", "topic", "torch", "tossed", "total", "touchy", "towel", "toxic", "toyed", "trash", "trendy", "tribal", "trolling", "truth", "trying", "tsunami", "tubes", "tucks", "tudor", "tuesday", "tufts", "tugs", "tuition", "tulips", "tumbling", "tunnel", "turnip", "tusks", "tutor", "tuxedo", "twang", "tweezers", "twice", "twofold", "tycoon", "typist", "tyrant", "ugly", "ulcers", "ultimate", "umbrella", "umpire", "unafraid", "unbending", "uncle", "under", "uneven", "unfit", "ungainly", "unhappy", "union", "unjustly", "unknown", "unlikely", "unmask", "unnoticed", "unopened", "unplugs", "unquoted", "unrest", "unsafe", "until", "unusual", "unveil", "unwind", "unzip", "upbeat", "upcoming", "update", "upgrade", "uphill", "upkeep", "upload", "upon", "upper", "upright", "upstairs", "uptight", "upwards", "urban", "urchins", "urgent", "usage", "useful", "usher", "using", "usual", "utensils", "utility", "utmost", "utopia", "uttered", "vacation", "vague", "vain", "value", "vampire", "vane", "vapidly", "vary", "vastness", "vats", "vaults", "vector", "veered", "vegan", "vehicle", "vein", "velvet", "venomous", "verification", "vessel", "veteran", "vexed", "vials", "vibrate", "victim", "video", "viewpoint", "vigilant", "viking", "village", "vinegar", "violin", "vipers", "virtual", "visited", "vitals", "vivid", "vixen", "vocal", "vogue", "voice", "volcano", "vortex", "voted", "voucher", "vowels", "voyage", "vulture", "wade", "waffle", "wagtail", "waist", "waking", "wallets", "wanted", "warped", "washing", "water", "waveform", "waxing", "wayside", "weavers", "website", "wedge", "weekday", "weird", "welders", "went", "wept", "were", "western", "wetsuit", "whale", "when", "whipped", "whole", "wickets", "width", "wield", "wife", "wiggle", "wildly", "winter", "wipeout", "wiring", "wise", "withdrawn", "wives", "wizard", "wobbly", "woes", "woken", "wolf", "womanly", "wonders", "woozy", "worry", "wounded", "woven", "wrap", "wrist", "wrong", "yacht", "yahoo", "yanks", "yard", "yawning", "yearbook", "yellow", "yesterday", "yeti", "yields", "yodel", "yoga", "younger", "yoyo", "zapped", "zeal", "zebra", "zero", "zesty", "zigzags", "zinger", "zippers", "zodiac", "zombie", "zones", "zoom" ], xmr_CURVE = { "a": -1n, "d": 37095705934669439343138083508754565189542113879843219016388785533085940283555n, "P": 2n ** 255n - 19n, "n": 2n ** 252n + 27742317777372353535851937790883648493n, "h": 8n, "Gx": 15112221349535400772501151409588531511454012693041857206046113283949847762202n, "Gy": 46316835694926478169428394003475163141307993866256225615783033603165251855960n }, ENCODING_LENGTH = 32, DIV_8_MINUS_3 = (xmr_CURVE.P + 3n) / 8n, xmr_pointPrecomputes = new WeakMap(); // Precomputed constant for point decompression let xmr_I = null; // ============================================ // TEST CONSTANTS // ============================================ const xmr_utils_const = { "version": "1.1.0", // bip39 (All addresses / xpubs in this app are test addresses derived from the following testphrase, taken from https://github.com/bitcoinbook/bitcoinbook/blob/f8b883dcd4e3d1b9adf40fed59b7e898fbd9241f/ch05.asciidoc) // "army van defense carry jealous true garbage claim echo media make crunch" // via BIP44 path m/44'/128'/0'/0/0 + sc_reduce32(fasthash(privkey)) "test_spend_key": "007d984c3df532fdd86cd83bf42482a5c2e180a51ae1d0096e13048fba1fa108", "test_view_key": "e4d63789cdfa2ec48571e93e47520690b2c6e11386c90448e8b357d1cd917c00", "test_address": "477h3C6E6C4VLMR36bQL3yLcA8Aq3jts1AHLzm5QXipDdXVCYPnKEvUKykh2GTYqkkeQoTEhWpzvVQ4rMgLM1YpeD6qdHbS" }; // ============================================ // MONERO BASE58 ENCODING // ============================================ const cn_base_58 = (function() { const b58 = {}, alphabet_str = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz", alphabet = []; for (let i = 0; i < alphabet_str.length; i++) { alphabet.push(alphabet_str.charCodeAt(i)); } const encoded_block_sizes = [0, 2, 3, 5, 6, 7, 9, 10, 11], full_block_size = 8, full_encoded_block_size = 11, UINT64_MAX = BigInt("18446744073709551615"); function hextobin(hex) { if (hex.length % 2 !== 0) throw "Hex string has invalid length!"; const res = new Uint8Array(hex.length / 2); for (let i = 0; i < hex.length / 2; ++i) { res[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); } return res; } function bintohex(bin) { const out = []; for (let i = 0; i < bin.length; ++i) { out.push(("0" + bin[i].toString(16)).slice(-2)); } return out.join(""); } function uint8_be_to_64(data) { let num = 0n; for (let i = 0; i < data.length; i++) { num = (num << 8n) | BigInt(data[i]); } return num; } function uint64_to_8be(num, size) { const res = new Uint8Array(size); if (size < 1 || size > 8) { throw "Invalid input length"; } let twopow8 = 256n; for (let i = size - 1; i >= 0; i--) { res[i] = Number(num % twopow8); num = num / twopow8; } return res; } function encode_block(data, buf, index) { let num = uint8_be_to_64(data); const i = encoded_block_sizes[data.length]; for (let s = i - 1; s >= 0; s--) { buf[index + s] = alphabet[Number(num % 58n)]; num = num / 58n; } return buf; } function decode_block(data, buf, index) { if (data.length < 1 || data.length > full_encoded_block_size) { throw "Invalid block length: " + data.length; } const idx = encoded_block_sizes.indexOf(data.length); if (idx === -1) { throw "Invalid block size"; } let num = 0n; for (let i = 0; i < data.length; i++) { const alpha_idx = alphabet.indexOf(data[i]); if (alpha_idx === -1) { throw "Invalid character"; } num = num * 58n + BigInt(alpha_idx); } if (num > UINT64_MAX) { throw "Overflow"; } const res = uint64_to_8be(num, idx); for (let i = 0; i < res.length; i++) { buf[index + i] = res[i]; } return buf; } b58.encode = function(hex) { const data = hextobin(hex); if (data.length === 0) return ""; const full_block_count = Math.floor(data.length / full_block_size), last_block_size = data.length % full_block_size, res_size = full_block_count * full_encoded_block_size + encoded_block_sizes[last_block_size]; let res = new Uint8Array(res_size); for (let i = 0; i < full_block_count; i++) { res = encode_block(data.subarray(i * full_block_size, i * full_block_size + full_block_size), res, i * full_encoded_block_size); } if (last_block_size > 0) { res = encode_block(data.subarray(full_block_count * full_block_size, full_block_count * full_block_size + last_block_size), res, full_block_count * full_encoded_block_size); } return bintohex(res); }; b58.decode = function(enc) { const data = str_to_bin(enc); if (data.length === 0) return ""; const full_block_count = Math.floor(data.length / full_encoded_block_size), last_block_size = data.length % full_encoded_block_size, last_block_decoded_size = encoded_block_sizes.indexOf(last_block_size); if (last_block_decoded_size < 0) { throw "Invalid encoded length"; } const data_size = full_block_count * full_block_size + last_block_decoded_size; let data_res = new Uint8Array(data_size); for (let i = 0; i < full_block_count; i++) { data_res = decode_block(data.subarray(i * full_encoded_block_size, i * full_encoded_block_size + full_encoded_block_size), data_res, i * full_block_size); } if (last_block_size > 0) { data_res = decode_block(data.subarray(full_block_count * full_encoded_block_size, full_block_count * full_encoded_block_size + last_block_size), data_res, full_block_count * full_block_size); } return bintohex(data_res); }; return b58; })(); // ============================================ // BYTE/NUMBER UTILITIES // ============================================ // Converts string to binary Uint8Array function str_to_bin(str) { const res = new Uint8Array(str.length); for (let i = 0; i < str.length; i++) { res[i] = str.charCodeAt(i); } return res; } // Converts unsigned 64-bit integer to big-endian byte array function uint64_to_8be(num, size) { const res = new Uint8Array(size); if (size < 1 || size > 8) { throw "Invalid input length"; } let twopow8 = 256n; for (let i = size - 1; i >= 0; i--) { res[i] = Number(num % twopow8); num = num / twopow8; } return res; } // Converts Uint8Array to BigInt (little-endian) function bytes_to_number_le(uint8a) { let num = 0n; for (let i = uint8a.length - 1; i >= 0; i--) { num = (num << 8n) | BigInt(uint8a[i]); } return num; } // Converts 32-bit unsigned integer to 8-character hex string function uint32_hex(value) { const buffer = new ArrayBuffer(4), view = new DataView(buffer); view.setUint32(0, value, true); return Array.from(new Uint8Array(buffer)).map(b => b.toString(16).padStart(2, "0")).join(""); } // Converts BigInt to hexadecimal string with even length padding function xmr_number_to_hex(num) { const hex = num.toString(16); return hex.length % 2 === 1 ? "0" + hex : hex; } // ============================================ // MONERO MODULAR ARITHMETIC // ============================================ // Computes modular reduction with positive result (ed25519 curve) function xmod(a, b = xmr_CURVE.P) { const res = a % b; return res >= 0n ? res : b + res; } // Computes modular exponentiation using square-and-multiply algorithm (ed25519) function xpow_mod(a, power, m = xmr_CURVE.P) { let res = 1n; while (power > 0n) { if (power & 1n) { res = xmod(res * a, m); } power >>= 1n; a = xmod(a * a, m); } return res; } // Computes modular multiplicative inverse using Extended Euclidean Algorithm function xmr_invert(number, modulo = xmr_CURVE.P) { if (number === 0n || modulo <= 0n) { throw new Error("invert: expected positive integers"); } let [gcd, x] = egcd(xmod(number, modulo), modulo); if (gcd !== 1n) { throw new Error("invert: does not exist"); } return xmod(x, modulo); } // Efficiently computes multiple modular inverses using Montgomery's batch inversion function xmr_invert_batch(nums, n = xmr_CURVE.P) { const len = nums.length, scratch = new Array(len); let acc = 1n; for (let i = 0; i < len; i++) { if (nums[i] === 0n) continue; scratch[i] = acc; acc = xmod(acc * nums[i], n); } acc = xmr_invert(acc, n); for (let i = len - 1; i >= 0; i--) { if (nums[i] === 0n) continue; let tmp = xmod(acc * nums[i], n); nums[i] = xmod(acc * scratch[i], n); acc = tmp; } return nums; } // ============================================ // EXTENDED POINT (ED25519) // ============================================ class ExtendedPoint { constructor(x, y, z, t) { this.x = x; this.y = y; this.z = z; this.t = t; } static fromAffine(p) { if (!(p instanceof xPoint)) { throw new TypeError("ExtendedPoint#fromAffine: expected Point"); } if (p.equals(xPoint.ZERO)) return ExtendedPoint.ZERO; return new ExtendedPoint(p.x, p.y, 1n, xmod(p.x * p.y)); } static toAffineBatch(points) { const toInv = xmr_invert_batch(points.map((p) => p.z)); return points.map((p, i) => p.toAffine(toInv[i])); } static normalizeZ(points) { return this.toAffineBatch(points).map(this.fromAffine); } negate() { return new ExtendedPoint(xmod(-this.x), this.y, this.z, xmod(-this.t)); } double() { const X1 = this.x, Y1 = this.y, Z1 = this.z, { a } = xmr_CURVE, A = xmod(X1 ** 2n), B = xmod(Y1 ** 2n), C = xmod(2n * Z1 ** 2n), D = xmod(a * A), E = xmod((X1 + Y1) ** 2n - A - B), G = xmod(D + B), F = xmod(G - C), H = xmod(D - B), X3 = xmod(E * F), Y3 = xmod(G * H), T3 = xmod(E * H), Z3 = xmod(F * G); return new ExtendedPoint(X3, Y3, Z3, T3); } add(other) { const X1 = this.x, Y1 = this.y, Z1 = this.z, T1 = this.t, X2 = other.x, Y2 = other.y, Z2 = other.z, T2 = other.t, A = xmod((Y1 - X1) * (Y2 + X2)), B = xmod((Y1 + X1) * (Y2 - X2)), F = xmod(B - A); if (F === 0n) { return this.double(); } const C = xmod(Z1 * 2n * T2), D = xmod(T1 * 2n * Z2), E = xmod(D + C), G = xmod(B + A), H = xmod(D - C), X3 = xmod(E * F), Y3 = xmod(G * H), T3 = xmod(E * H), Z3 = xmod(F * G); return new ExtendedPoint(X3, Y3, Z3, T3); } precomputeWindow(W) { const windows = 256 / W + 1; let points = [], p = this, base = p; for (let window = 0; window < windows; window++) { base = p; points.push(base); for (let i = 1; i < 2 ** (W - 1); i++) { base = base.add(p); points.push(base); } p = base.double(); } return points; } wNAF(n, affinePoint) { if (!affinePoint && this.equals(ExtendedPoint.BASE)) affinePoint = xPoint.BASE; const W = (affinePoint && affinePoint._WINDOW_SIZE) || 1; if (256 % W) { throw new Error("Point#wNAF: Invalid precomputation window, must be power of 2"); } let precomputes = affinePoint && xmr_pointPrecomputes.get(affinePoint); if (!precomputes) { precomputes = this.precomputeWindow(W); if (affinePoint && W !== 1) { precomputes = ExtendedPoint.normalizeZ(precomputes); xmr_pointPrecomputes.set(affinePoint, precomputes); } } let p = ExtendedPoint.ZERO, f = ExtendedPoint.ZERO; const windows = 256 / W + 1, windowSize = 2 ** (W - 1), mask = BigInt(2 ** W - 1), maxNumber = 2 ** W, shiftBy = BigInt(W); for (let window = 0; window < windows; window++) { const offset = window * windowSize; let wbits = Number(n & mask); n >>= shiftBy; if (wbits > windowSize) { wbits -= maxNumber; n += 1n; } if (wbits === 0) { f = f.add(window % 2 ? precomputes[offset].negate() : precomputes[offset]); } else { const cached = precomputes[offset + Math.abs(wbits) - 1]; p = p.add(wbits < 0 ? cached.negate() : cached); } } return [p, f]; } multiply(scalar, affinePoint) { if (typeof scalar !== "number" && typeof scalar !== "bigint") { throw new TypeError("Point#multiply: expected number or bigint"); } const n = xmod(BigInt(scalar), xmr_CURVE.n); if (n <= 0) { throw new Error("Point#multiply: invalid scalar, expected positive integer"); } return ExtendedPoint.normalizeZ(this.wNAF(n, affinePoint))[0]; } toAffine(invZ = xmr_invert(this.z)) { const x = xmod(this.x * invZ), y = xmod(this.y * invZ); return new xPoint(x, y); } equals(other) { return this.x === other.x && this.y === other.y && this.z === other.z && this.t === other.t; } } ExtendedPoint.ZERO = new ExtendedPoint(0n, 1n, 1n, 0n); // ============================================ // AFFINE POINT (ED25519) // ============================================ class xPoint { constructor(x, y) { this.x = x; this.y = y; } _setWindowSize(windowSize) { this._WINDOW_SIZE = windowSize; xmr_pointPrecomputes.delete(this); } static fromHex(hash) { const { d, P } = xmr_CURVE, bytes = hash instanceof Uint8Array ? hash : hex_to_bytes(hash), len = bytes.length - 1, normedLast = bytes[len] & ~0x80, isLastByteOdd = (bytes[len] & 0x80) !== 0, normed = Uint8Array.from(Array.from(bytes.slice(0, len)).concat(normedLast)), y = bytes_to_number_le(normed); if (y >= P) { throw new Error("Point#fromHex expects hex <= Fp"); } const sqrY = y * y, sqrX = xmod((sqrY - 1n) * xmr_invert(d * sqrY + 1n)); // Lazy init xmr_I if (xmr_I === null) { xmr_I = xpow_mod(2n, (xmr_CURVE.P + 1n) / 4n, xmr_CURVE.P); } let x = xpow_mod(sqrX, DIV_8_MINUS_3); if (xmod(x * x - sqrX) !== 0n) { x = xmod(x * xmr_I); } const isXOdd = (x & 1n) === 1n; if (isLastByteOdd !== isXOdd) { x = xmod(-x); } return new xPoint(x, y); } toRawBytes() { const hex = xmr_number_to_hex(this.y), u8 = uint_8array(ENCODING_LENGTH); for (let i = hex.length - 2, j = 0; j < ENCODING_LENGTH && i >= 0; i -= 2, j++) { u8[j] = parseInt(hex[i] + hex[i + 1], 16); } const mask = this.x & 1n ? 0x80 : 0; u8[ENCODING_LENGTH - 1] |= mask; return u8; } toHex() { return bytes_to_hex(this.toRawBytes()); } equals(other) { return this.x === other.x && this.y === other.y; } add(other) { return ExtendedPoint.fromAffine(this).add(ExtendedPoint.fromAffine(other)).toAffine(); } multiply(scalar) { return ExtendedPoint.fromAffine(this).multiply(scalar, this).toAffine(); } } xPoint.BASE = new xPoint(xmr_CURVE.Gx, xmr_CURVE.Gy); xPoint.ZERO = new xPoint(0n, 1n); xPoint.BASE._setWindowSize(8); // ============================================ // KEY OPERATIONS // ============================================ // Constructs an elliptic curve point from a 32-byte hex string representation function xmr_getpoint(hex) { return xPoint.fromHex(hex); } // Performs scalar multiplication of curve base point with given hex-encoded scalar function point_multiply(hex) { return xPoint.BASE.multiply(xmr_getpoint(hex).y); } // Derives a Monero public key by performing scalar multiplication with curve base point function xmr_get_publickey(privateKey) { return point_multiply(privateKey).toHex(); } // Converts an elliptic curve point to Monero's compressed hex format function point_to_monero_hex(point) { const y_hex = point.y.toString(16).padStart(64, "0"), y_bytes = []; for (let i = y_hex.length - 2; i >= 0; i -= 2) { y_bytes.push(parseInt(y_hex.slice(i, i + 2), 16)); } if ((point.x & 1n) === 1n) { y_bytes[31] |= 0x80; } return y_bytes.map(b => b.toString(16).padStart(2, "0")).join(""); } // ============================================ // MNEMONIC & SECRET KEY GENERATION // ============================================ // Generates cryptographically secure random bits using browser's crypto API function mn_random(bits) { "use strict"; if (bits % 32 !== 0) throw "Something weird went wrong: Invalid number of bits - " + bits; let array = new Uint32Array(bits / 32); if (!crypto) throw "JavaScript Crypto API not supported"; let i = 0; function arr_is_zero() { for (let j = 0; j < bits / 32; ++j) { if (array[j] !== 0) return false } return true } do { crypto.getRandomValues(array); ++i; } while (i < 5 && arr_is_zero()); if (arr_is_zero()) { throw "Something went wrong and we could not securely generate random data for your account"; } let out = ""; for (let j = 0; j < bits / 32; ++j) { out += ("0000000" + array[j].toString(16)).slice(-8); } return out; } // Converts a 32-byte secret spend key into a 25-word Monero mnemonic with checksum function secret_spend_key_to_words(ssk) { const seed = []; let for_checksum = ""; for (let i = 0; i < 32; i += 4) { let w0 = 0; for (let j = 3; j >= 0; j--) w0 = w0 * 256 + ssk[i + j]; let xmrwl = xmr_words.length, w1 = w0 % xmrwl, w2 = ((w0 / xmrwl | 0) + w1) % xmrwl, w3 = (((w0 / xmrwl | 0) / xmrwl | 0) + w2) % xmrwl; seed.push(xmr_words[w1]); seed.push(xmr_words[w2]); seed.push(xmr_words[w3]); for_checksum += xmr_words[w1].substring(0, 3); for_checksum += xmr_words[w2].substring(0, 3); for_checksum += xmr_words[w3].substring(0, 3); } seed.push(seed[crc_32(for_checksum) % 24]); return seed.join(" "); } // Derives a Monero secret spend key from either a BIP39 mnemonic phrase or its seed function get_ssk(bip39, seed) { const p_rootkey = (seed === true) ? get_rootkey(bip39) : get_rootkey(mnemonic_to_seed(bip39)), dx_dat = { "dpath": "m/44'/128'/0'/0/0", "key": p_rootkey.slice(0, 64), "cc": p_rootkey.slice(64) }, x_keys_dat = derive_x(dx_dat), rootkey = x_keys_dat.key; return sc_reduce32(fasthash(rootkey)); } // Performs modular reduction of a 32-byte hex string against Monero's curve order l function sc_reduce32(hex) { return hex_to_bytes(str_pad((BigInt("0x" + bytes_to_hex(hex_to_bytes(hex).reverse())) % l).toString(16), 64)).reverse(); } // ============================================ // CRC32 CHECKSUM // ============================================ // Calculates a 32-bit CRC checksum using the IEEE 802.3 polynomial function crc_32(str) { let crcTable = window.crcTable || (window.crcTable = make_crc_table()), crc = 0 ^ (-1); for (let i = 0; i < str.length; i++) { crc = (crc >>> 8) ^ crcTable[(crc ^ str.charCodeAt(i)) & 0xFF]; } return (crc ^ (-1)) >>> 0; } // Constructs a lookup table for CRC-32 polynomial 0xEDB88320 calculations function make_crc_table() { let c, crcTable = []; for (let n = 0; n < 256; n++) { c = n; for (let k = 0; k < 8; k++) { c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1)); } crcTable[n] = c; } return crcTable; } // ============================================ // ADDRESS GENERATION // ============================================ // Generates a standardized Monero address from public keys with network-specific prefix function pub_keys_to_address(psk, pvk, index) { const pref = (index < 1) ? "12" : "2a", res_hex = pref + psk + pvk, cpa = res_hex + fasthash(res_hex).slice(0, 8); return base58_encode(hex_to_bytes(cpa)); } // Generates Monero keys and addresses from secret spend key function xmr_getpubs(ssk, index) { const sskh = bytes_to_hex(ssk), svk = bytes_to_hex(sc_reduce32(fasthash(sskh))), psk = xmr_get_publickey(sskh), pvk = xmr_get_publickey(svk), account = pub_keys_to_address(psk, pvk, 0); if (index < 1) { return { "index": 0, "account": account, "address": account, "ssk": sskh, "svk": svk, "psk": psk, "pvk": pvk } } const pubp = point_multiply(sc_reduce32(fasthash(5375624164647200 + svk + uint32_hex(0) + uint32_hex(index)))), pskp = xmr_getpoint(psk), np = pskp.add(pubp), sub_psk = np.toHex(), sub_pvk = np.multiply(xmr_getpoint(svk).y).toHex(); return { "index": index, "account": account, "address": pub_keys_to_address(sub_psk, sub_pvk, index), "ssk": sskh, "svk": svk, "psk": sub_psk, "pvk": sub_pvk } } // ============================================ // HASHING // ============================================ // Computes and returns a Keccak-256 hash of input hexadecimal data function fasthash(hex) { return keccak_256(hex_to_bytes(hex)); } // ============================================ // BASE58 ENCODING (MONERO-SPECIFIC) // ============================================ // Implements Monero's modified Base58 encoding algorithm for address generation function base58_encode(data) { const ab = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz", ab_map = {}, btl = [0, 2, 3, 5, 6, 7, 9, 10, 11], base = ab.length; for (let z = 0; z < ab.length; z++) { const x = ab.charAt(z); if (ab_map[x] !== undefined) throw new TypeError(x + " is ambiguous"); ab_map[x] = z; } function encode_partial(data, pos) { let len = 8; if (pos + len > data.length) len = data.length - pos; const digits = [0]; for (let i = 0; i < len; ++i) { for (var j = 0, carry = data[pos + i]; j < digits.length; ++j) { carry += digits[j] << 8; digits[j] = carry % base; carry = (carry / base) | 0; } while (carry > 0) { digits.push(carry % base); carry = (carry / base) | 0; } } let res = ""; for (let k = digits.length; k < btl[len]; ++k) res += ab[0]; for (let q = digits.length - 1; q >= 0; --q) res += ab[digits[q]]; return res; } let resu = ""; for (let i = 0; i < data.length; i += 8) { resu += encode_partial(data, i); } return resu; } // ============================================ // VIEW KEY MANAGEMENT // ============================================ // Retrieves cached view key data for a Monero address from storage function get_vk(address) { const ad_li = filter_addressli("monero", "address", address), ad_dat = (ad_li.length) ? ad_li.data() : {}, ad_vk = ad_dat.vk; if (ad_vk && ad_vk != "") { return vk_obj(ad_vk); } return false } // Parses view key string into structured account and key data function vk_obj(vk) { if (vk.length === 64) { return { "account": false, "vk": vk } } if (vk.length === 159) { return { "account": vk.slice(0, 95), "vk": vk.slice(95) } } return false } // Checks user preference for view key sharing setting function share_vk() { const vkshare = cs_node("monero", "Share viewkey", true).selected; if (vkshare === true) { return true } return false } // Extracts spend pubkey from Monero address function get_spend_pubkey_from_address(address) { try { const decoded = cn_base_58.decode(address); return decoded.slice(2, 66); } catch (e) { console.error("Could not extract spend pubkey:", e); return null; } } // ============================================ // PAYMENT ID FUNCTIONS // ============================================ // Generates a 16-byte cryptographically secure random payment ID function xmr_pid() { return mn_random(256).slice(0, 16); } // Validates payment ID format: must be 16 hexadecimal characters function check_pid(payment_id) { const payment_id_length = payment_id.length; if (payment_id_length !== 16) { return false } const pattern = RegExp("^[0-9a-fA-F]{16}$"); if (pattern.test(payment_id) != true) { return false } return true } // ============================================ // TRANSACTION PARSING // ============================================ // Decodes a Monero transaction from hex string into a structured object function parse_xmr_tx_hex(tx_hex) { const bytes = hex_to_bytes(tx_hex); let offset = 0; function read_varint() { let result = 0n, shift = 0n; while (offset < bytes.length) { const byte = bytes[offset++]; result |= BigInt(byte & 0x7f) << shift; shift += 7n; if ((byte & 0x80) === 0) break; } return result; } function read_bytes(count) { const result = bytes.slice(offset, offset + count); offset += count; return result; } const tx = {}; tx.version = Number(read_varint()); tx.unlock_time = Number(read_varint()); const vin_count = read_varint(); tx.vin = []; for (let i = 0; i < vin_count; i++) { const input_type = bytes[offset++]; if (input_type === 0x02) { const input = {}; input.amount = Number(read_varint()); const key_offsets_count = read_varint(); input.key_offsets = []; for (let j = 0; j < key_offsets_count; j++) { input.key_offsets.push(Number(read_varint())); } input.k_image = bytes_to_hex(read_bytes(32)); tx.vin.push({ "key": input }); } } const vout_count = Number(read_varint()); tx.vout = []; for (let i = 0; i < vout_count; i++) { const output = {}; output.amount = Number(read_varint()); const target_type = bytes[offset++]; if (target_type === 0x03) { const key = bytes_to_hex(read_bytes(32)), view_tag = bytes_to_hex(read_bytes(1)); output.target = { tagged_key: { key, view_tag } }; } tx.vout.push(output); } const extra_size = Number(read_varint()); tx.extra = Array.from(read_bytes(extra_size)); if (offset < bytes.length) { const rct_type = bytes[offset++]; tx.rct_signatures = { "type": rct_type }; if (rct_type > 0) { tx.rct_signatures.txnFee = Number(read_varint()); if (rct_type === 5 || rct_type === 6) { tx.rct_signatures.ecdhInfo = []; for (let i = 0; i < vout_count; i++) { const amount = bytes_to_hex(read_bytes(8)); tx.rct_signatures.ecdhInfo.push({ amount }); } } } } return tx; } // Find payment ID tag (0x02) in extra field function extract_xmr_payment_id(extra_bytes, tx_pub_key, view_key) { let i = 0; while (i < extra_bytes.length) { const tag = extra_bytes[i]; if (tag === 0x00) { i++; continue; } if (tag === 0x01) { i += 33; continue; } if (tag === 0x02) { if (i + 2 >= extra_bytes.length) break; const length = extra_bytes[i + 1], nonce_type = extra_bytes[i + 2]; if (nonce_type === 0x01 && length === 0x09) { if (i + 11 > extra_bytes.length) return false; const encrypted_pid = extra_bytes.slice(i + 3, i + 11), r_point = xmr_getpoint(tx_pub_key), a_scalar = bytes_to_number_le(hex_to_bytes(view_key)), derivation_point = r_point.multiply(a_scalar).multiply(8n), derivation_bytes = derivation_point.toRawBytes(), tail_byte = new Uint8Array([0x8d]), hash_input = concat_bytes(derivation_bytes, tail_byte), hash = fasthash(bytes_to_hex(hash_input)), key = hex_to_bytes(hash.slice(0, 16)), decrypted_pid = new Uint8Array(8); for (let j = 0; j < 8; j++) { decrypted_pid[j] = encrypted_pid[j] ^ key[j]; } const payment_id_hex = bytes_to_hex(decrypted_pid); if (payment_id_hex === "0000000000000000") { return false; } return { "type": "encrypted", "payment_id": payment_id_hex }; } else if (nonce_type === 0x00 && length === 0x21) { if (i + 35 > extra_bytes.length) return false; const unencrypted_pid = extra_bytes.slice(i + 3, i + 35), payment_id_hex = bytes_to_hex(unencrypted_pid); if (payment_id_hex === "00000000000000000000000000000000000000000000000000000000000000") { return false; } return { "type": "unencrypted", "payment_id": payment_id_hex }; } i += 2 + length; continue; } if (tag === 0x04) { const count = extra_bytes[i + 1]; i += 2 + (count * 32); continue; } i++; } return false; } // Decrypts the amount for a specific output in a RingCT transaction function decode_rct_amount(rct, output_idx, shared_secret_hex) { const encrypted_amount_hex = rct?.ecdhInfo?.[output_idx]?.amount; if (!encrypted_amount_hex) return null; const rct_type = rct.type; let scalar; if (rct_type === 5 || rct_type === 6) { const derivation_data = concat_bytes( hex_to_bytes(shared_secret_hex), encode_varint(output_idx) ); scalar = bytes_to_hex(sc_reduce32(fasthash(bytes_to_hex(derivation_data)))); } else { scalar = bytes_to_hex(sc_reduce32(fasthash(shared_secret_hex))); } const prefix_bytes = str_to_bin("amount"), hash_input = concat_bytes(prefix_bytes, hex_to_bytes(scalar)), amount_key = fasthash(bytes_to_hex(hash_input)), key_bytes = hex_to_bytes(amount_key.slice(0, 16)), encrypted_bytes = hex_to_bytes(encrypted_amount_hex), decrypted_bytes = new Uint8Array(8); for (let i = 0; i < 8; i++) { decrypted_bytes[i] = encrypted_bytes[i] ^ key_bytes[i]; } return bytes_to_number_le(decrypted_bytes); } // ============================================ // COMPATIBILITY TESTING // ============================================ // Tests Secret Spend Key → Full Wallet Keys derivation // Can be called with custom spend_key/expected_address or uses defaults from xmr_utils_const function test_xmr_derivation(spend_key, expected_address) { try { const ssk = spend_key || hex_to_bytes(xmr_utils_const.test_spend_key), expected = expected_address || xmr_utils_const.test_address, derived = xmr_getpubs(ssk, 0); return derived.address === expected; } catch (e) { console.error("XmrUtils test_xmr_derivation:", e.message); return false; } } // Tests XMR public key derivation from spend key function test_xmr_keys() { try { const spend_key = hex_to_bytes(xmr_utils_const.test_spend_key), derived = xmr_getpubs(spend_key, 0); return derived.svk === xmr_utils_const.test_view_key; } catch (e) { console.error("XmrUtils test_xmr_keys:", e.message); return false; } } // Tests XMR address generation function test_xmr_address() { try { const spend_key = hex_to_bytes(xmr_utils_const.test_spend_key), derived = xmr_getpubs(spend_key, 0); return derived.address === xmr_utils_const.test_address; } catch (e) { console.error("XmrUtils test_xmr_address:", e.message); return false; } } // Full compatibility test - calls CryptoUtils tests + XMR tests function test_xmr_compatibility() { const start_time = typeof performance !== "undefined" ? performance.now() : Date.now(), results = { "compatible": false, "crypto_api": false, "bigint": false, "keys": false, "address": false, "errors": [], "timing_ms": 0 }; // Fail fast: Check crypto basics from CryptoUtils results.crypto_api = CryptoUtils.test_crypto_api(); if (!results.crypto_api) { results.errors.push("crypto API not available"); results.timing_ms = (typeof performance !== "undefined" ? performance.now() : Date.now()) - start_time; console.error("XmrUtils: Compatibility test failed", results.errors); return results; } results.bigint = CryptoUtils.test_bigint(); if (!results.bigint) { results.errors.push("BigInt not functional"); results.timing_ms = (typeof performance !== "undefined" ? performance.now() : Date.now()) - start_time; console.error("XmrUtils: Compatibility test failed", results.errors); return results; } // Test XMR key derivation results.keys = test_xmr_keys(); if (!results.keys) { results.errors.push("XMR key derivation failed"); } // Test XMR address generation results.address = test_xmr_address(); if (!results.address) { results.errors.push("XMR address generation failed"); } results.compatible = results.keys && results.address; results.timing_ms = (typeof performance !== "undefined" ? performance.now() : Date.now()) - start_time; if (results.errors.length > 0) { console.error("XmrUtils: Compatibility test failed", results.errors); } return results; } // ============================================ // MODULE EXPORT // ============================================ const XmrUtils = { // Library info VERSION: "1.1.0", // Curve parameters xmr_CURVE: xmr_CURVE, // === Byte/Number Utilities === str_to_bin, uint64_to_8be, bytes_to_number_le, uint32_hex, xmr_number_to_hex, // === Elliptic Curve Operations === xmod, xpow_mod, xmr_invert, xmr_invert_batch, xmr_getpoint, point_multiply, point_to_monero_hex, // === Key Operations === xmr_get_publickey, get_ssk, sc_reduce32, xmr_getpubs, // === Mnemonic === mn_random, secret_spend_key_to_words, // === Address Operations === pub_keys_to_address, get_vk, vk_obj, share_vk, get_spend_pubkey_from_address, base58_encode, base58_decode: cn_base_58.decode, // === Hashing === fasthash, crc_32, make_crc_table, // === Payment ID === xmr_pid, check_pid, // === Transaction Parsing === parse_xmr_tx_hex, extract_xmr_payment_id, decode_rct_amount, // === Constants === xmr_utils_const, // === Testing === test_xmr_derivation, test_xmr_keys, test_xmr_address, test_xmr_compatibility };