mirror of
https://github.com/luxfi/sdk.git
synced 2026-10-30 15:19:23 +00:00
146 lines
4.3 KiB
Go
146 lines
4.3 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package keys provides key derivation and management utilities for LUX.
|
|
package keys
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/luxfi/crypto/secp256k1"
|
|
"github.com/luxfi/go-bip32"
|
|
"github.com/luxfi/go-bip39"
|
|
"github.com/luxfi/utxo/secp256k1fx"
|
|
)
|
|
|
|
const (
|
|
// LUXCoinType is the BIP-44 coin type for LUX P/X-Chain (9000')
|
|
LUXCoinType = 9000
|
|
|
|
// EthCoinType is the BIP-44 coin type for C-Chain/EVM (60')
|
|
EthCoinType = 60
|
|
|
|
// DefaultDerivationPath is the standard BIP-44 path for LUX P/X-Chain: m/44'/9000'/0'/0/0
|
|
DefaultDerivationPath = "m/44'/9000'/0'/0/0"
|
|
)
|
|
|
|
// DeriveKeyFromMnemonic derives a private key from a BIP-39 mnemonic using BIP-44 path.
|
|
// Default path: m/44'/9000'/0'/0/0 (P/X-Chain)
|
|
func DeriveKeyFromMnemonic(mnemonic string, accountIndex uint32) ([]byte, error) {
|
|
if !bip39.IsMnemonicValid(mnemonic) {
|
|
return nil, fmt.Errorf("invalid mnemonic phrase")
|
|
}
|
|
seed := bip39.NewSeed(mnemonic, "")
|
|
return DeriveKeyFromSeed(seed, accountIndex)
|
|
}
|
|
|
|
// DeriveKeyFromSeed derives a private key from a BIP-39 seed using BIP-44 path.
|
|
// Path: m/44'/9000'/0'/0/{accountIndex} (P/X-Chain)
|
|
func DeriveKeyFromSeed(seed []byte, accountIndex uint32) ([]byte, error) {
|
|
// Create master key from seed
|
|
masterKey, err := bip32.NewMasterKey(seed)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create master key: %w", err)
|
|
}
|
|
|
|
// BIP-44 path: m/44'/9000'/0'/0/{accountIndex} (Lux P/X-Chain)
|
|
// m/44' (purpose)
|
|
key, err := masterKey.NewChildKey(bip32.FirstHardenedChild + 44)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to derive purpose: %w", err)
|
|
}
|
|
|
|
// m/44'/9000' (coin type - Lux P/X-Chain)
|
|
key, err = key.NewChildKey(bip32.FirstHardenedChild + LUXCoinType)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to derive coin type: %w", err)
|
|
}
|
|
|
|
// m/44'/9000'/0' (account)
|
|
key, err = key.NewChildKey(bip32.FirstHardenedChild + 0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to derive account: %w", err)
|
|
}
|
|
|
|
// m/44'/9000'/0'/0 (change)
|
|
key, err = key.NewChildKey(0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to derive change: %w", err)
|
|
}
|
|
|
|
// m/44'/9000'/0'/0/{accountIndex} (address index)
|
|
key, err = key.NewChildKey(accountIndex)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to derive address index: %w", err)
|
|
}
|
|
|
|
return key.Key, nil
|
|
}
|
|
|
|
// LoadKeyFromFile reads a hex-encoded private key from a file.
|
|
func LoadKeyFromFile(path string) ([]byte, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read key file: %w", err)
|
|
}
|
|
keyHex := strings.TrimSpace(string(data))
|
|
keyBytes, err := hex.DecodeString(keyHex)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode key hex: %w", err)
|
|
}
|
|
return keyBytes, nil
|
|
}
|
|
|
|
// LoadKeyFromEnv loads a key from environment variables.
|
|
// Priority: 1) PRIVATE_KEY (hex), 2) MNEMONIC (24 words)
|
|
func LoadKeyFromEnv() ([]byte, error) {
|
|
if envKey := os.Getenv("PRIVATE_KEY"); envKey != "" {
|
|
keyBytes, err := hex.DecodeString(strings.TrimSpace(envKey))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode PRIVATE_KEY: %w", err)
|
|
}
|
|
return keyBytes, nil
|
|
}
|
|
|
|
if mnemonic := os.Getenv("MNEMONIC"); mnemonic != "" {
|
|
return DeriveKeyFromMnemonic(mnemonic, 0)
|
|
}
|
|
|
|
return nil, fmt.Errorf("no key found: set PRIVATE_KEY or MNEMONIC environment variable")
|
|
}
|
|
|
|
// LoadKey loads a key from file, environment, or returns an error.
|
|
// Priority: 1) keyPath if non-empty, 2) PRIVATE_KEY env, 3) MNEMONIC env
|
|
func LoadKey(keyPath string) ([]byte, error) {
|
|
if keyPath != "" {
|
|
return LoadKeyFromFile(keyPath)
|
|
}
|
|
return LoadKeyFromEnv()
|
|
}
|
|
|
|
// ToPrivateKey converts raw key bytes to a secp256k1 private key.
|
|
func ToPrivateKey(keyBytes []byte) (*secp256k1.PrivateKey, error) {
|
|
return secp256k1.ToPrivateKey(keyBytes)
|
|
}
|
|
|
|
// NewKeychain creates a secp256k1fx.Keychain from raw key bytes.
|
|
func NewKeychain(keyBytes []byte) (*secp256k1fx.Keychain, error) {
|
|
sk, err := ToPrivateKey(keyBytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse private key: %w", err)
|
|
}
|
|
return secp256k1fx.NewKeychain(sk), nil
|
|
}
|
|
|
|
// MustNewKeychain creates a secp256k1fx.Keychain from raw key bytes, panics on error.
|
|
func MustNewKeychain(keyBytes []byte) *secp256k1fx.Keychain {
|
|
kc, err := NewKeychain(keyBytes)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return kc
|
|
}
|