Created
July 7, 2018 22:25
-
-
Save miguelmota/ee0fd9756e1651f38f4cd38c6e99b8bf to your computer and use it in GitHub Desktop.
Golang Ethereum HD Wallet implementation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package hdwallet | |
import ( | |
"crypto/ecdsa" | |
"errors" | |
"fmt" | |
"github.com/btcsuite/btcd/chaincfg" | |
"github.com/btcsuite/btcutil/hdkeychain" | |
"github.com/ethereum/go-ethereum/accounts" | |
"github.com/ethereum/go-ethereum/common" | |
"github.com/ethereum/go-ethereum/common/hexutil" | |
"github.com/ethereum/go-ethereum/crypto" | |
"github.com/tyler-smith/go-bip32" | |
"github.com/tyler-smith/go-bip39" | |
) | |
// Wallet ... | |
type Wallet struct { | |
mnemonic string | |
path string | |
root *hdkeychain.ExtendedKey | |
extendedKey *hdkeychain.ExtendedKey | |
privateKey *ecdsa.PrivateKey | |
publicKey *ecdsa.PublicKey | |
} | |
// Config ... | |
type Config struct { | |
Mnemonic string | |
Path string | |
} | |
// New ... | |
func New(config *Config) (*Wallet, error) { | |
if config.Path == "" { | |
config.Path = `m/44'/60'/0'/0` | |
} | |
if config.Mnemonic == "" { | |
return nil, errors.New("mnemonic is required") | |
} | |
seed := bip39.NewSeed(config.Mnemonic, "") | |
dpath, err := accounts.ParseDerivationPath(config.Path) | |
if err != nil { | |
return nil, err | |
} | |
masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams) | |
if err != nil { | |
return nil, err | |
} | |
key := masterKey | |
for _, n := range dpath { | |
key, err = key.Child(n) | |
if err != nil { | |
return nil, err | |
} | |
} | |
privateKey, err := key.ECPrivKey() | |
privateKeyECDSA := privateKey.ToECDSA() | |
if err != nil { | |
return nil, err | |
} | |
publicKey := privateKeyECDSA.Public() | |
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) | |
if !ok { | |
return nil, errors.New("failed ot get public key") | |
} | |
wallet := &Wallet{ | |
mnemonic: config.Mnemonic, | |
path: config.Path, | |
root: masterKey, | |
extendedKey: key, | |
privateKey: privateKeyECDSA, | |
publicKey: publicKeyECDSA, | |
} | |
return wallet, nil | |
} | |
// Derive ... | |
func (s Wallet) Derive(index interface{}) (*Wallet, error) { | |
var idx uint32 | |
switch v := index.(type) { | |
case int: | |
idx = uint32(v) | |
case int8: | |
idx = uint32(v) | |
case int16: | |
idx = uint32(v) | |
case int32: | |
idx = uint32(v) | |
case int64: | |
idx = uint32(v) | |
case uint: | |
idx = uint32(v) | |
case uint8: | |
idx = uint32(v) | |
case uint16: | |
idx = uint32(v) | |
case uint32: | |
idx = v | |
case uint64: | |
idx = uint32(v) | |
default: | |
return nil, errors.New("unsupported index type") | |
} | |
address, err := s.extendedKey.Child(idx) | |
if err != nil { | |
return nil, err | |
} | |
privateKey, err := address.ECPrivKey() | |
privateKeyECDSA := privateKey.ToECDSA() | |
if err != nil { | |
return nil, err | |
} | |
publicKey := privateKeyECDSA.Public() | |
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) | |
if !ok { | |
return nil, errors.New("failed ot get public key") | |
} | |
path := fmt.Sprintf("%s/%v", s.path, idx) | |
wallet := &Wallet{ | |
path: path, | |
root: s.extendedKey, | |
extendedKey: address, | |
privateKey: privateKeyECDSA, | |
publicKey: publicKeyECDSA, | |
} | |
return wallet, nil | |
} | |
// PrivateKey ... | |
func (s Wallet) PrivateKey() *ecdsa.PrivateKey { | |
return s.privateKey | |
} | |
// PrivateKeyBytes ... | |
func (s Wallet) PrivateKeyBytes() []byte { | |
return crypto.FromECDSA(s.PrivateKey()) | |
} | |
// PrivateKeyHex ... | |
func (s Wallet) PrivateKeyHex() string { | |
return hexutil.Encode(s.PrivateKeyBytes())[2:] | |
} | |
// PublicKey ... | |
func (s Wallet) PublicKey() *ecdsa.PublicKey { | |
return s.publicKey | |
} | |
// PublicKeyBytes ... | |
func (s Wallet) PublicKeyBytes() []byte { | |
return crypto.FromECDSAPub(s.PublicKey()) | |
} | |
// PublicKeyHex ... | |
func (s Wallet) PublicKeyHex() string { | |
return hexutil.Encode(s.PublicKeyBytes())[4:] | |
} | |
// Address ... | |
func (s Wallet) Address() common.Address { | |
return crypto.PubkeyToAddress(*s.publicKey) | |
} | |
// AddressHex ... | |
func (s Wallet) AddressHex() string { | |
return s.Address().Hex() | |
} | |
// Path ... | |
func (s Wallet) Path() string { | |
return s.path | |
} | |
// Mnemonic ... | |
func (s Wallet) Mnemonic() string { | |
return s.mnemonic | |
} | |
// NewMnemonic ... | |
func NewMnemonic() (string, error) { | |
entropy, err := bip39.NewEntropy(128) | |
if err != nil { | |
return "", err | |
} | |
return bip39.NewMnemonic(entropy) | |
} | |
// NewSeed ... | |
func NewSeed() ([]byte, error) { | |
return bip32.NewSeed() | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package hdwallet | |
import ( | |
"testing" | |
) | |
// TODO: table test | |
func TestNew(t *testing.T) { | |
mnemonic := "tag volcano eight thank tide danger coast health above argue embrace heavy" | |
root, err := New(&Config{ | |
Mnemonic: mnemonic, | |
Path: "m/44'/60'/0'/0", | |
}) | |
if err != nil { | |
t.Error(err) | |
} | |
if root.PrivateKeyHex() != "7657783b9ba4d4b16062337235432bbc5c80e3dce39fdc91e62d744fdb665cad" { | |
t.Error("wrong private key") | |
} | |
if root.PublicKeyHex() != "177c0776ca4c9e160822a1006eb6d236039eb882da8d7687ba20049d73e6230cae699eb8037aeeee2098d433d4210401a0cc1bf635c3fee2a40933d22c1206e7" { | |
t.Error("wrong public key") | |
} | |
if root.AddressHex() != "0xAF1c991f6068Ac832eC60A8557eF1C7D8B9BcCD6" { | |
t.Error("wrong address") | |
} | |
if root.Path() != `m/44'/60'/0'/0` { | |
t.Error("wrong hdpath") | |
} | |
wallet, err := root.Derive(0) | |
if err != nil { | |
t.Error(err) | |
} | |
if wallet.PrivateKeyHex() != "63e21d10fd50155dbba0e7d3f7431a400b84b4c2ac1ee38872f82448fe3ecfb9" { | |
t.Error("wrong private key") | |
} | |
if wallet.PublicKeyHex() != "6005c86a6718f66221713a77073c41291cc3abbfcd03aa4955e9b2b50dbf7f9b6672dad0d46ade61e382f79888a73ea7899d9419becf1d6c9ec2087c1188fa18" { | |
t.Error("wrong public key") | |
} | |
if wallet.AddressHex() != "0xC49926C4124cEe1cbA0Ea94Ea31a6c12318df947" { | |
t.Error("wrong address") | |
} | |
if wallet.Path() != `m/44'/60'/0'/0/0` { | |
t.Error("wrong hdpath") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment