package wallet import ( "fmt" "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/chaincfg" bip39 "github.com/tyler-smith/go-bip39" ) // ⚠️ OFFLINE ONLY. Everything in this file touches the BIP39 seed / private keys. // It exists for (a) generating the account xpub to hand to the watcher, and // (b) deriving per-address private keys for offline sweep signing (Phase D). // It must NEVER be linked into or run on the internet-facing pangolin-pay // watcher — the hot service only ever handles the account xpub (see derive.go). const ( purposeBIP44 = 44 coinTypeTRON = 195 // hardenedOffset marks a derivation index as hardened (requires the private // key). BIP44's first three levels (purpose'/coin'/account') are hardened. hardenedOffset = hdkeychain.HardenedKeyStart // 0x80000000 ) // AccountKeyFromMnemonic derives the account-level extended *private* key at // m/44'/195'/' from a BIP39 mnemonic (+ optional passphrase). // OFFLINE ONLY. func AccountKeyFromMnemonic(mnemonic, passphrase string, account uint32) (*hdkeychain.ExtendedKey, error) { if !bip39.IsMnemonicValid(mnemonic) { return nil, fmt.Errorf("wallet: invalid BIP39 mnemonic (checksum/wordlist)") } seed := bip39.NewSeed(mnemonic, passphrase) master, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams) if err != nil { return nil, fmt.Errorf("wallet: master key: %w", err) } for _, step := range []uint32{ hardenedOffset + purposeBIP44, hardenedOffset + coinTypeTRON, hardenedOffset + account, } { master, err = master.Derive(step) if err != nil { return nil, fmt.Errorf("wallet: derive account path: %w", err) } } return master, nil } // AccountXpubFromMnemonic returns the account-level xpub string to hand to the // watcher. OFFLINE ONLY — run this once on the air-gapped machine, copy only the // returned xpub to the hot service. func AccountXpubFromMnemonic(mnemonic, passphrase string, account uint32) (string, error) { k, err := AccountKeyFromMnemonic(mnemonic, passphrase, account) if err != nil { return "", err } pub, err := k.Neuter() // strip the private key -> xpub if err != nil { return "", fmt.Errorf("wallet: neuter: %w", err) } return pub.String(), nil } // PrivKeyHexFromMnemonic derives the raw secp256k1 private key (hex) for the // address at m/44'/195'/'//, for offline sweep signing. // OFFLINE ONLY. func PrivKeyHexFromMnemonic(mnemonic, passphrase string, account, change, index uint32) (string, error) { acct, err := AccountKeyFromMnemonic(mnemonic, passphrase, account) if err != nil { return "", err } chainKey, err := acct.Derive(change) if err != nil { return "", fmt.Errorf("wallet: derive change: %w", err) } addrKey, err := chainKey.Derive(index) if err != nil { return "", fmt.Errorf("wallet: derive index: %w", err) } priv, err := addrKey.ECPrivKey() if err != nil { return "", fmt.Errorf("wallet: ec privkey: %w", err) } return fmt.Sprintf("%x", priv.Serialize()), nil } // AddressFromMnemonic derives the TRON address at m/44'/195'/'// // straight from the mnemonic. OFFLINE ONLY — used by the sweep signer to verify // that a derived key matches the address it is about to sign for. func AddressFromMnemonic(mnemonic, passphrase string, account, change, index uint32) (string, error) { acct, err := AccountKeyFromMnemonic(mnemonic, passphrase, account) if err != nil { return "", err } chainKey, err := acct.Derive(change) if err != nil { return "", fmt.Errorf("wallet: derive change: %w", err) } addrKey, err := chainKey.Derive(index) if err != nil { return "", fmt.Errorf("wallet: derive index: %w", err) } pub, err := addrKey.ECPubKey() if err != nil { return "", fmt.Errorf("wallet: ec pubkey: %w", err) } return PubKeyToTronAddress(pub.SerializeUncompressed()), nil }