theseus/bundled-addons/aegis/lib/import-derive.js

297 lines
15 KiB
JavaScript
Raw Normal View History

// 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({
feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets Ported from Digibyte.X/dgb-wallet @ 989a2696. BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's official 2018-19 Android/iOS wallets were BreadWallet forks and used "DigiByte seed" as the HMAC key instead, so the same twelve words produce a completely different key tree — every address differs, and a standard scan finds nothing at all. Anyone importing one of those seeds into Aegis got a valid, empty address and a zero balance with no way to tell why. Same shape as the Bitcoin.com coin-type-0 problem. - rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC key, matching upstream, and exports HMAC_BITCOIN_SEED / HMAC_DIGIBYTE_SEED. - lib/import-derive.js grows the same option, since that is what actually derives the address on an import, and importWallet records the variant on the spec so the entry says which tree its stored address came from. - The path scanner added in 0.24.0 now covers DigiByte: all four purposes under the standard key, plus m/0' and BIP44 under the legacy one. So the answer to "which tree holds my coins" is a scan rather than a guess, and the Use button carries the key along with the path. Verified against an independent HMAC-SHA512 computation, and end to end: the same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn. Also fixes a coupling this exposed: lib/import-derive.js derives taproot addresses but never called initEccLib, relying on chain-btc.js (and formerly lib/dgb/core/address.js, before it went lazy here) doing it at load time. It now installs the schnorr backend itself, once, so its taproot output no longer depends on an unrelated module's import order. The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is upstream-only for now — Aegis's imported DGB adapter watches a single stored address rather than scanning a gap.
2026-10-03 14:13:25 +02:00
HDKey, secp256k1, ed25519, sha256, sha512, hmac, 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;
feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets Ported from Digibyte.X/dgb-wallet @ 989a2696. BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's official 2018-19 Android/iOS wallets were BreadWallet forks and used "DigiByte seed" as the HMAC key instead, so the same twelve words produce a completely different key tree — every address differs, and a standard scan finds nothing at all. Anyone importing one of those seeds into Aegis got a valid, empty address and a zero balance with no way to tell why. Same shape as the Bitcoin.com coin-type-0 problem. - rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC key, matching upstream, and exports HMAC_BITCOIN_SEED / HMAC_DIGIBYTE_SEED. - lib/import-derive.js grows the same option, since that is what actually derives the address on an import, and importWallet records the variant on the spec so the entry says which tree its stored address came from. - The path scanner added in 0.24.0 now covers DigiByte: all four purposes under the standard key, plus m/0' and BIP44 under the legacy one. So the answer to "which tree holds my coins" is a scan rather than a guess, and the Use button carries the key along with the path. Verified against an independent HMAC-SHA512 computation, and end to end: the same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn. Also fixes a coupling this exposed: lib/import-derive.js derives taproot addresses but never called initEccLib, relying on chain-btc.js (and formerly lib/dgb/core/address.js, before it went lazy here) doing it at load time. It now installs the schnorr backend itself, once, so its taproot output no longer depends on an unrelated module's import order. The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is upstream-only for now — Aegis's imported DGB adapter watches a single stored address rather than scanning a gap.
2026-10-03 14:13:25 +02:00
// Taproot needs bitcoinjs-lib's schnorr backend installed via initEccLib,
// which is process-global and must happen once. This module used to rely on
// chain-btc.js (and, before it went lazy, lib/dgb/core/address.js) doing it
// at load time — a hidden dependency on an unrelated module's import order,
// which broke the moment either stopped being eagerly loaded. Own it here.
let eccInstalled = false;
function ensureEcc() {
if (eccInstalled) return;
try { if (bitcoinjs && bitcoinjs.initEccLib && ecc) bitcoinjs.initEccLib(ecc); } catch { /* p2tr will report it */ }
eccInstalled = true;
}
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);
feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets Ported from Digibyte.X/dgb-wallet @ 989a2696. BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's official 2018-19 Android/iOS wallets were BreadWallet forks and used "DigiByte seed" as the HMAC key instead, so the same twelve words produce a completely different key tree — every address differs, and a standard scan finds nothing at all. Anyone importing one of those seeds into Aegis got a valid, empty address and a zero balance with no way to tell why. Same shape as the Bitcoin.com coin-type-0 problem. - rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC key, matching upstream, and exports HMAC_BITCOIN_SEED / HMAC_DIGIBYTE_SEED. - lib/import-derive.js grows the same option, since that is what actually derives the address on an import, and importWallet records the variant on the spec so the entry says which tree its stored address came from. - The path scanner added in 0.24.0 now covers DigiByte: all four purposes under the standard key, plus m/0' and BIP44 under the legacy one. So the answer to "which tree holds my coins" is a scan rather than a guess, and the Use button carries the key along with the path. Verified against an independent HMAC-SHA512 computation, and end to end: the same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn. Also fixes a coupling this exposed: lib/import-derive.js derives taproot addresses but never called initEccLib, relying on chain-btc.js (and formerly lib/dgb/core/address.js, before it went lazy here) doing it at load time. It now installs the schnorr backend itself, once, so its taproot output no longer depends on an unrelated module's import order. The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is upstream-only for now — Aegis's imported DGB adapter watches a single stored address rather than scanning a gap.
2026-10-03 14:13:25 +02:00
ensureEcc();
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);
feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets Ported from Digibyte.X/dgb-wallet @ 989a2696. BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's official 2018-19 Android/iOS wallets were BreadWallet forks and used "DigiByte seed" as the HMAC key instead, so the same twelve words produce a completely different key tree — every address differs, and a standard scan finds nothing at all. Anyone importing one of those seeds into Aegis got a valid, empty address and a zero balance with no way to tell why. Same shape as the Bitcoin.com coin-type-0 problem. - rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC key, matching upstream, and exports HMAC_BITCOIN_SEED / HMAC_DIGIBYTE_SEED. - lib/import-derive.js grows the same option, since that is what actually derives the address on an import, and importWallet records the variant on the spec so the entry says which tree its stored address came from. - The path scanner added in 0.24.0 now covers DigiByte: all four purposes under the standard key, plus m/0' and BIP44 under the legacy one. So the answer to "which tree holds my coins" is a scan rather than a guess, and the Use button carries the key along with the path. Verified against an independent HMAC-SHA512 computation, and end to end: the same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn. Also fixes a coupling this exposed: lib/import-derive.js derives taproot addresses but never called initEccLib, relying on chain-btc.js (and formerly lib/dgb/core/address.js, before it went lazy here) doing it at load time. It now installs the schnorr backend itself, once, so its taproot output no longer depends on an unrelated module's import order. The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is upstream-only for now — Aegis's imported DGB adapter watches a single stored address rather than scanning a gap.
2026-10-03 14:13:25 +02:00
ensureEcc();
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;
}
feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets Ported from Digibyte.X/dgb-wallet @ 989a2696. BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's official 2018-19 Android/iOS wallets were BreadWallet forks and used "DigiByte seed" as the HMAC key instead, so the same twelve words produce a completely different key tree — every address differs, and a standard scan finds nothing at all. Anyone importing one of those seeds into Aegis got a valid, empty address and a zero balance with no way to tell why. Same shape as the Bitcoin.com coin-type-0 problem. - rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC key, matching upstream, and exports HMAC_BITCOIN_SEED / HMAC_DIGIBYTE_SEED. - lib/import-derive.js grows the same option, since that is what actually derives the address on an import, and importWallet records the variant on the spec so the entry says which tree its stored address came from. - The path scanner added in 0.24.0 now covers DigiByte: all four purposes under the standard key, plus m/0' and BIP44 under the legacy one. So the answer to "which tree holds my coins" is a scan rather than a guess, and the Use button carries the key along with the path. Verified against an independent HMAC-SHA512 computation, and end to end: the same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn. Also fixes a coupling this exposed: lib/import-derive.js derives taproot addresses but never called initEccLib, relying on chain-btc.js (and formerly lib/dgb/core/address.js, before it went lazy here) doing it at load time. It now installs the schnorr backend itself, once, so its taproot output no longer depends on an unrelated module's import order. The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is upstream-only for now — Aegis's imported DGB adapter watches a single stored address rather than scanning a gap.
2026-10-03 14:13:25 +02:00
// DigiByte's 2018-19 official mobile wallets (BreadWallet forks) built the
// master node with HMAC-SHA512 key "DigiByte seed" rather than BIP32's
// "Bitcoin seed", so the same words yield a different key tree and a
// standard scan finds nothing. Pass hmacKey to recover those.
// Ported from Digibyte.X/dgb-wallet @ 989a2696.
const HMAC_BITCOIN_SEED = "Bitcoin seed";
const HMAC_DIGIBYTE_SEED = "DigiByte seed";
function dgbRootFromSeed(seedBytes, net, hmacKey) {
const bip32 = bip32Factory(ecc);
feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets Ported from Digibyte.X/dgb-wallet @ 989a2696. BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's official 2018-19 Android/iOS wallets were BreadWallet forks and used "DigiByte seed" as the HMAC key instead, so the same twelve words produce a completely different key tree — every address differs, and a standard scan finds nothing at all. Anyone importing one of those seeds into Aegis got a valid, empty address and a zero balance with no way to tell why. Same shape as the Bitcoin.com coin-type-0 problem. - rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC key, matching upstream, and exports HMAC_BITCOIN_SEED / HMAC_DIGIBYTE_SEED. - lib/import-derive.js grows the same option, since that is what actually derives the address on an import, and importWallet records the variant on the spec so the entry says which tree its stored address came from. - The path scanner added in 0.24.0 now covers DigiByte: all four purposes under the standard key, plus m/0' and BIP44 under the legacy one. So the answer to "which tree holds my coins" is a scan rather than a guess, and the Use button carries the key along with the path. Verified against an independent HMAC-SHA512 computation, and end to end: the same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn. Also fixes a coupling this exposed: lib/import-derive.js derives taproot addresses but never called initEccLib, relying on chain-btc.js (and formerly lib/dgb/core/address.js, before it went lazy here) doing it at load time. It now installs the schnorr backend itself, once, so its taproot output no longer depends on an unrelated module's import order. The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is upstream-only for now — Aegis's imported DGB adapter watches a single stored address rather than scanning a gap.
2026-10-03 14:13:25 +02:00
if (!hmacKey || hmacKey === HMAC_BITCOIN_SEED) return bip32.fromSeed(Buffer.from(seedBytes), net);
if (!hmac || !sha512) throw new Error("this build cannot derive the legacy DigiByte master key");
const I = hmac(sha512, new TextEncoder().encode(hmacKey), Buffer.from(seedBytes));
return bip32.fromPrivateKey(Buffer.from(I.slice(0, 32)), Buffer.from(I.slice(32)), net);
}
function deriveDgbFromSeed(seedHex, path, hmacKey) {
const net = digibyteNetwork();
feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets Ported from Digibyte.X/dgb-wallet @ 989a2696. BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's official 2018-19 Android/iOS wallets were BreadWallet forks and used "DigiByte seed" as the HMAC key instead, so the same twelve words produce a completely different key tree — every address differs, and a standard scan finds nothing at all. Anyone importing one of those seeds into Aegis got a valid, empty address and a zero balance with no way to tell why. Same shape as the Bitcoin.com coin-type-0 problem. - rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC key, matching upstream, and exports HMAC_BITCOIN_SEED / HMAC_DIGIBYTE_SEED. - lib/import-derive.js grows the same option, since that is what actually derives the address on an import, and importWallet records the variant on the spec so the entry says which tree its stored address came from. - The path scanner added in 0.24.0 now covers DigiByte: all four purposes under the standard key, plus m/0' and BIP44 under the legacy one. So the answer to "which tree holds my coins" is a scan rather than a guess, and the Use button carries the key along with the path. Verified against an independent HMAC-SHA512 computation, and end to end: the same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn. Also fixes a coupling this exposed: lib/import-derive.js derives taproot addresses but never called initEccLib, relying on chain-btc.js (and formerly lib/dgb/core/address.js, before it went lazy here) doing it at load time. It now installs the schnorr backend itself, once, so its taproot output no longer depends on an unrelated module's import order. The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is upstream-only for now — Aegis's imported DGB adapter watches a single stored address rather than scanning a gap.
2026-10-03 14:13:25 +02:00
const node = dgbRootFromSeed(fromHex(seedHex), net, hmacKey).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);
feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets Ported from Digibyte.X/dgb-wallet @ 989a2696. BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's official 2018-19 Android/iOS wallets were BreadWallet forks and used "DigiByte seed" as the HMAC key instead, so the same twelve words produce a completely different key tree — every address differs, and a standard scan finds nothing at all. Anyone importing one of those seeds into Aegis got a valid, empty address and a zero balance with no way to tell why. Same shape as the Bitcoin.com coin-type-0 problem. - rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC key, matching upstream, and exports HMAC_BITCOIN_SEED / HMAC_DIGIBYTE_SEED. - lib/import-derive.js grows the same option, since that is what actually derives the address on an import, and importWallet records the variant on the spec so the entry says which tree its stored address came from. - The path scanner added in 0.24.0 now covers DigiByte: all four purposes under the standard key, plus m/0' and BIP44 under the legacy one. So the answer to "which tree holds my coins" is a scan rather than a guess, and the Use button carries the key along with the path. Verified against an independent HMAC-SHA512 computation, and end to end: the same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn. Also fixes a coupling this exposed: lib/import-derive.js derives taproot addresses but never called initEccLib, relying on chain-btc.js (and formerly lib/dgb/core/address.js, before it went lazy here) doing it at load time. It now installs the schnorr backend itself, once, so its taproot output no longer depends on an unrelated module's import order. The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is upstream-only for now — Aegis's imported DGB adapter watches a single stored address rather than scanning a gap.
2026-10-03 14:13:25 +02:00
ensureEcc();
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;
feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets Ported from Digibyte.X/dgb-wallet @ 989a2696. BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's official 2018-19 Android/iOS wallets were BreadWallet forks and used "DigiByte seed" as the HMAC key instead, so the same twelve words produce a completely different key tree — every address differs, and a standard scan finds nothing at all. Anyone importing one of those seeds into Aegis got a valid, empty address and a zero balance with no way to tell why. Same shape as the Bitcoin.com coin-type-0 problem. - rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC key, matching upstream, and exports HMAC_BITCOIN_SEED / HMAC_DIGIBYTE_SEED. - lib/import-derive.js grows the same option, since that is what actually derives the address on an import, and importWallet records the variant on the spec so the entry says which tree its stored address came from. - The path scanner added in 0.24.0 now covers DigiByte: all four purposes under the standard key, plus m/0' and BIP44 under the legacy one. So the answer to "which tree holds my coins" is a scan rather than a guess, and the Use button carries the key along with the path. Verified against an independent HMAC-SHA512 computation, and end to end: the same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn. Also fixes a coupling this exposed: lib/import-derive.js derives taproot addresses but never called initEccLib, relying on chain-btc.js (and formerly lib/dgb/core/address.js, before it went lazy here) doing it at load time. It now installs the schnorr backend itself, once, so its taproot output no longer depends on an unrelated module's import order. The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is upstream-only for now — Aegis's imported DGB adapter watches a single stored address rather than scanning a gap.
2026-10-03 14:13:25 +02:00
ensureEcc();
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);
// HMAC-SHA512(HMAC_KEY, seed) → I = I_L || I_R, sk = I_L, cc = I_R; each
// step is HMAC-SHA512(cc, 0x00 || sk || idx). Node's own HMAC, same as
// chain-sol.js: the add-on runs in Electron main, and the previous
// `new require("crypto").createHmac` plus a bare require of an ESM-only
// @noble/hashes subpath threw on every Solana seed import.
const nodeCrypto = require("node:crypto");
const hmac = (_h, key, data) => new Uint8Array(nodeCrypto.createHmac("sha512", Buffer.from(key)).update(Buffer.from(data)).digest());
const sha512 = null;
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);
}
// A 64-byte Solana keypair is seed || public key. The second half was
// ignored, so a corrupted or mismatched export imported silently as some
// other address; it must match the key the seed produces.
function checkKeypairHalf(full, pub) {
if (full.length !== 64) return;
for (let i = 0; i < 32; i++) if (full[32 + i] !== pub[i]) throw new Error("this 64-byte Solana key is inconsistent: its public half does not match its secret half");
}
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);
checkKeypairHalf(priv, pub);
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);
checkKeypairHalf(bytes, pub);
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 },
feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets Ported from Digibyte.X/dgb-wallet @ 989a2696. BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's official 2018-19 Android/iOS wallets were BreadWallet forks and used "DigiByte seed" as the HMAC key instead, so the same twelve words produce a completely different key tree — every address differs, and a standard scan finds nothing at all. Anyone importing one of those seeds into Aegis got a valid, empty address and a zero balance with no way to tell why. Same shape as the Bitcoin.com coin-type-0 problem. - rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC key, matching upstream, and exports HMAC_BITCOIN_SEED / HMAC_DIGIBYTE_SEED. - lib/import-derive.js grows the same option, since that is what actually derives the address on an import, and importWallet records the variant on the spec so the entry says which tree its stored address came from. - The path scanner added in 0.24.0 now covers DigiByte: all four purposes under the standard key, plus m/0' and BIP44 under the legacy one. So the answer to "which tree holds my coins" is a scan rather than a guess, and the Use button carries the key along with the path. Verified against an independent HMAC-SHA512 computation, and end to end: the same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn. Also fixes a coupling this exposed: lib/import-derive.js derives taproot addresses but never called initEccLib, relying on chain-btc.js (and formerly lib/dgb/core/address.js, before it went lazy here) doing it at load time. It now installs the schnorr backend itself, once, so its taproot output no longer depends on an unrelated module's import order. The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is upstream-only for now — Aegis's imported DGB adapter watches a single stored address rather than scanning a gap.
2026-10-03 14:13:25 +02:00
dgb: {
fromSeed: deriveDgbFromSeed, fromWif: deriveDgbFromWif,
HMAC_BITCOIN_SEED, HMAC_DIGIBYTE_SEED,
},
eth: { fromSeed: deriveEthFromSeed, fromPrivHex: deriveEthFromPrivHex },
trx: { fromSeed: deriveTrxFromSeed, fromPrivHex: deriveTrxFromPrivHex },
sol: { fromSeed: deriveSolFromSeed, fromPrivHex: deriveSolFromPrivHex, fromBase58: deriveSolFromBase58 },
sc: { fromMnemonic: deriveScFromMnemonic, fromSeedHex: deriveScFromSeedHex, addressAt: deriveScAddressFromSeed },
};
};