Theseus core:
- addons-host: manifest.category ("plugin") propagates through snapshot(); new
addon API surface checkAndStageSelfUpdate() + restartApp() so a plug-in
can offer in-panel "update now → restart to apply" without pushing the
user to Settings.
- main.js: wires the two new hooks into the AddonHost constructor.
- settings.html: Extensions listing filters out category==="plugin"; those
add-ons live in Plug-ins instead, single source of truth.
Aegis 0.6.31:
- BTC picker trimmed to Signet only; testnet3 hidden (adapter kept so any
existing wallet still loads).
- Wallet strip groups by chain, not chain:network; ticker gets a ▾ chevron
and a dropdown listing every subnetwork with its own totals. Mainnet
reads as the plain ticker; testnets carry a small Chipnet/Signet/Sepolia
pill inline.
- Per-unit price sits directly under the ticker; amount + fiat mirror on
the right — one glance covers name/price/holding/value.
- + Add and ⋯ More promoted from the strip into the header's action row,
next to the new ✎ chip (was the redundant top ⋯). Duplicate "Manage
current wallet" entry removed from the More menu.
- Footer update chip is a two-step flow via the new API: stage → restart.
Falls back to opening Settings on any Theseus that lacks the hooks.
- Manifest declares "category": "plugin".
218 lines
10 KiB
JavaScript
218 lines
10 KiB
JavaScript
// 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,
|
|
cashaddr, base58check, bitcoinjs, bip32Factory, ecpairFactory, ecc, bip39,
|
|
dgbCore,
|
|
}) {
|
|
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);
|
|
}
|
|
|
|
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 },
|
|
};
|
|
};
|