// Per-chain address derivation for imported wallets. Every helper turns // either a BIP39 mnemonic (+ path) OR a raw private key (chain-native // format — WIF for UTXO chains, hex for account chains, base58 for Solana) // into the canonical address that chain uses. // // Deps arrive from index.js loadDeps() so nothing here has to know about // npm packages — same "hand it in" pattern the other adapters use. module.exports = function makeImportDerive({ HDKey, secp256k1, ed25519, sha256, ripemd160, keccak_256, blake2b, cashaddr, base58check, bitcoinjs, bip32Factory, ecpairFactory, ecc, bip39, dgbCore, }) { // Sia's key derivation lives in lib/sia/sia.js — reuse it here so // imported SC wallets end up with byte-identical addresses to what // Sia Central Lite or walletd would show for the same seed. const sia = blake2b ? require("./sia/sia.js")({ ed25519, blake2b }) : null; const toHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join(""); const fromHex = (h) => { const s = String(h || "").replace(/^0x/i, ""); const out = new Uint8Array(s.length / 2); for (let i = 0; i < out.length; i++) out[i] = parseInt(s.substr(i * 2, 2), 16); return out; }; const hash160 = (b) => ripemd160(sha256(b)); // BIP39 mnemonic → 64-byte seed hex. Same wire format the vault-derive // path stores, so keystore-mirrored seeds land in wallet-imports.enc // identically whether they came from a mnemonic or hex directly. function mnemonicToSeedHex(m) { if (!bip39.validateMnemonic(m)) throw new Error("invalid BIP39 mnemonic"); return toHex(bip39.mnemonicToSeedSync(m)); } // ---- BTC ------------------------------------------------------------------ const BTC_NET = { mainnet: bitcoinjs.networks.bitcoin, testnet3: bitcoinjs.networks.testnet, signet: bitcoinjs.networks.testnet, // signet uses testnet params here }; function btcAddressFromNode(node, path, network) { const net = BTC_NET[network]; if (!net) throw new Error(`unknown BTC network ${network}`); // Purpose byte in the path decides the address type. m/84' -> bech32, // m/49' -> P2SH-P2WPKH, m/86' -> P2TR, m/44' -> P2PKH. const m = /^m\/(\d+)'/.exec(String(path || "")); const purpose = m ? Number(m[1]) : 84; const pk = Buffer.from(node.publicKey); if (purpose === 86) { // Taproot — bitcoinjs.p2tr wants the 32-byte x-only pubkey. const xonly = pk.slice(1, 33); return bitcoinjs.payments.p2tr({ internalPubkey: xonly, network: net }).address; } if (purpose === 84) return bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }).address; if (purpose === 49) return bitcoinjs.payments.p2sh({ redeem: bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }) }).address; return bitcoinjs.payments.p2pkh({ pubkey: pk, network: net }).address; } function deriveBtcFromSeed(seedHex, path, network) { const bip32 = bip32Factory(ecc); const node = bip32.fromSeed(Buffer.from(fromHex(seedHex)), BTC_NET[network]).derivePath(path); return btcAddressFromNode(node, path, network); } function deriveBtcFromWif(wif, network, hint) { const ECPair = ecpairFactory(ecc); const kp = ECPair.fromWIF(wif, BTC_NET[network]); // WIF alone doesn't tell us the address family; caller passes hint = 44/49/84/86. const purpose = hint || 84; const pk = kp.publicKey; const net = BTC_NET[network]; if (purpose === 86) { const xonly = pk.slice(1, 33); return bitcoinjs.payments.p2tr({ internalPubkey: xonly, network: net }).address; } if (purpose === 84) return bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }).address; if (purpose === 49) return bitcoinjs.payments.p2sh({ redeem: bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }) }).address; return bitcoinjs.payments.p2pkh({ pubkey: pk, network: net }).address; } // ---- DGB (mirrors BTC pattern with digibyte params) ----------------------- function digibyteNetwork() { if (!dgbCore) throw new Error("DGB adapter not available"); return dgbCore.digibyte; } function deriveDgbFromSeed(seedHex, path) { const bip32 = bip32Factory(ecc); const net = digibyteNetwork(); const node = bip32.fromSeed(Buffer.from(fromHex(seedHex)), net).derivePath(path); const pk = Buffer.from(node.publicKey); const m = /^m\/(\d+)'/.exec(String(path || "")); const purpose = m ? Number(m[1]) : 84; if (purpose === 86) { const xonly = pk.slice(1, 33); return bitcoinjs.payments.p2tr({ internalPubkey: xonly, network: net }).address; } if (purpose === 84) return bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }).address; if (purpose === 49) return bitcoinjs.payments.p2sh({ redeem: bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }) }).address; return bitcoinjs.payments.p2pkh({ pubkey: pk, network: net }).address; } function deriveDgbFromWif(wif, hint) { const ECPair = ecpairFactory(ecc); const net = digibyteNetwork(); const kp = ECPair.fromWIF(wif, net); const purpose = hint || 84; const pk = kp.publicKey; if (purpose === 86) return bitcoinjs.payments.p2tr({ internalPubkey: pk.slice(1, 33), network: net }).address; if (purpose === 84) return bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }).address; if (purpose === 49) return bitcoinjs.payments.p2sh({ redeem: bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }) }).address; return bitcoinjs.payments.p2pkh({ pubkey: pk, network: net }).address; } // ---- ETH (EIP-55 checksummed 0x address) ---------------------------------- function ethAddressFromPubkey(pubUncompressed64) { // Strip the 0x04 prefix if present so we hash just the 64 raw bytes. const raw = pubUncompressed64.length === 65 ? pubUncompressed64.slice(1) : pubUncompressed64; const h = keccak_256(raw); const addr20 = h.slice(-20); const hex = toHex(addr20); // EIP-55 checksum const hashOfLower = toHex(keccak_256(new TextEncoder().encode(hex))); let out = "0x"; for (let i = 0; i < hex.length; i++) { out += parseInt(hashOfLower[i], 16) >= 8 ? hex[i].toUpperCase() : hex[i]; } return out; } function deriveEthFromSeed(seedHex, path) { const node = HDKey.fromMasterSeed(fromHex(seedHex)).derive(path); // secp256k1.getPublicKey with compressed=false gives 65 bytes (04||X||Y). const pub = secp256k1.getPublicKey(node.privateKey, false); return ethAddressFromPubkey(pub); } function deriveEthFromPrivHex(hex) { const priv = fromHex(hex); if (priv.length !== 32) throw new Error("ETH private key must be 32 bytes hex"); const pub = secp256k1.getPublicKey(priv, false); return ethAddressFromPubkey(pub); } // ---- TRX (T... base58check, network 0x41) -------------------------------- function tronAddressFromPubkey(pubUncompressed65) { const raw = pubUncompressed65.length === 65 ? pubUncompressed65.slice(1) : pubUncompressed65; const h = keccak_256(raw); const last20 = h.slice(-20); const versioned = new Uint8Array(21); versioned[0] = 0x41; // Tron mainnet address prefix — same for Nile testnet versioned.set(last20, 1); return base58check.encodeCheck(versioned); } function deriveTrxFromSeed(seedHex, path) { const node = HDKey.fromMasterSeed(fromHex(seedHex)).derive(path); const pub = secp256k1.getPublicKey(node.privateKey, false); return tronAddressFromPubkey(pub); } function deriveTrxFromPrivHex(hex) { const priv = fromHex(hex); if (priv.length !== 32) throw new Error("TRX private key must be 32 bytes hex"); const pub = secp256k1.getPublicKey(priv, false); return tronAddressFromPubkey(pub); } // ---- SOL (base58 pubkey, ed25519) ---------------------------------------- // SLIP-0010 ed25519 hardened derivation. Slightly different HD scheme // from BIP32 secp256k1 — every step is hardened, index >= 0x80000000. function slip0010DeriveEd25519(seed, path) { const HMAC_KEY = new TextEncoder().encode("ed25519 seed"); const parts = String(path).split("/").slice(1); // Compute master const enc = new (require("crypto")).createHmac ? require("crypto") : null; // Not using node crypto — the deps hand in @noble/hashes hmac via sha512. // We rely on secp256k1's helpers? No — use ed25519 utils. // Simplified: compute HMAC-SHA512(HMAC_KEY, seed) → I=I_L||I_R, sk=I_L, cc=I_R. // Then each step: HMAC-SHA512(cc, 0x00 || sk || idx). // Implementation via @noble/hashes/hmac imported as `hmacSha512`. We // require it lazily so unavailable deps error out here rather than at // load time. const { hmac } = require("@noble/hashes/hmac"); const { sha512 } = require("@noble/hashes/sha2"); let I = hmac(sha512, HMAC_KEY, seed); let sk = I.slice(0, 32); let cc = I.slice(32); for (const seg of parts) { const m = /^(\d+)'?$/.exec(seg); if (!m) throw new Error(`bad path segment: ${seg}`); const idx = (Number(m[1]) | 0x80000000) >>> 0; const data = new Uint8Array(1 + 32 + 4); data[0] = 0; data.set(sk, 1); data[33] = (idx >>> 24) & 0xff; data[34] = (idx >>> 16) & 0xff; data[35] = (idx >>> 8) & 0xff; data[36] = idx & 0xff; I = hmac(sha512, cc, data); sk = I.slice(0, 32); cc = I.slice(32); } return sk; } function deriveSolFromSeed(seedHex, path) { const sk = slip0010DeriveEd25519(fromHex(seedHex), path); const pub = ed25519.getPublicKey(sk); return base58check.encodeBase58(pub); } function deriveSolFromPrivHex(hex) { const priv = fromHex(hex); if (priv.length !== 32 && priv.length !== 64) throw new Error("SOL private key must be 32 or 64 bytes hex"); const seed = priv.length === 64 ? priv.slice(0, 32) : priv; const pub = ed25519.getPublicKey(seed); return base58check.encodeBase58(pub); } function deriveSolFromBase58(b58) { const bytes = base58check.decodeBase58(b58); if (bytes.length !== 32 && bytes.length !== 64) throw new Error("SOL private key base58 must decode to 32 or 64 bytes"); const seed = bytes.length === 64 ? bytes.slice(0, 32) : bytes; const pub = ed25519.getPublicKey(seed); return base58check.encodeBase58(pub); } // ---- SC (Siacoin) -------------------------------------------------------- // Sia's walletd + Sia Central Lite Wallet both use a 32-byte root seed. // Sia Central Lite exports it as a BIP39 12-word mnemonic (PBKDF2 → // 64-byte seed → first 32 bytes = root); walletd's API accepts the raw // 32-byte hex. Address at index N: standardUnlockHash(ed25519.pub( // blake2b(root32 || u64le(N)) // )) — see lib/sia/sia.js:keyFromSeed for the byte layout. function deriveScRootFromMnemonic(m) { // BIP39 → 512-bit master seed; Sia Central takes the FIRST 32 bytes as // the walletd root. Trimming the tail keeps addresses identical to // what sialite.com and Sia Central mobile derive for the same phrase. const fullSeedHex = mnemonicToSeedHex(m); return fullSeedHex.slice(0, 64); } function deriveScRootFromHex(seedHex) { const s = String(seedHex || "").trim().toLowerCase().replace(/^0x/, ""); if (!/^[0-9a-f]{64}$/.test(s)) throw new Error("SC seed hex must be exactly 32 bytes (64 hex chars)"); return s; } function deriveScAddressFromSeed(seedHex, index) { if (!sia) throw new Error("SC derive unavailable (blake2b dep not passed)"); const root = fromHex(seedHex); if (root.length !== 32) throw new Error("SC root seed must be 32 bytes"); const idx = Number(index || 0); if (!Number.isInteger(idx) || idx < 0) throw new Error("SC index must be a non-negative integer"); const k = sia.keyFromSeed(root, idx); return k.address; // 76 hex chars, canonical Sia address form } function deriveScFromMnemonic(mnemonic, index) { return { seedHex: deriveScRootFromMnemonic(mnemonic), address: deriveScAddressFromSeed(deriveScRootFromMnemonic(mnemonic), index) }; } function deriveScFromSeedHex(seedHex, index) { const s = deriveScRootFromHex(seedHex); return { seedHex: s, address: deriveScAddressFromSeed(s, index) }; } return { mnemonicToSeedHex, btc: { fromSeed: deriveBtcFromSeed, fromWif: deriveBtcFromWif }, dgb: { fromSeed: deriveDgbFromSeed, fromWif: deriveDgbFromWif }, eth: { fromSeed: deriveEthFromSeed, fromPrivHex: deriveEthFromPrivHex }, trx: { fromSeed: deriveTrxFromSeed, fromPrivHex: deriveTrxFromPrivHex }, sol: { fromSeed: deriveSolFromSeed, fromPrivHex: deriveSolFromPrivHex, fromBase58: deriveSolFromBase58 }, sc: { fromMnemonic: deriveScFromMnemonic, fromSeedHex: deriveScFromSeedHex, addressAt: deriveScAddressFromSeed }, }; };