Skip to content

Instantly share code, notes, and snippets.

@sofianhw
Forked from miguelmota/hdwallet.go
Created October 18, 2022 08:06
Show Gist options
  • Save sofianhw/31293f5df5b19cc06d981a302789b518 to your computer and use it in GitHub Desktop.
Save sofianhw/31293f5df5b19cc06d981a302789b518 to your computer and use it in GitHub Desktop.
Golang Ethereum HD Wallet implementation
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()
}
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