Skip to content

Commit

Permalink
Merge pull request #78 from stellar/stellar-hd-wallet
Browse files Browse the repository at this point in the history
stellar-hd-wallet
  • Loading branch information
bartekn authored Dec 28, 2017
2 parents 15b50c7 + ee8e4b4 commit 3534889
Show file tree
Hide file tree
Showing 9 changed files with 341 additions and 0 deletions.
3 changes: 3 additions & 0 deletions glide.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions glide.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -295,3 +295,5 @@ import:
subpackages:
- socks
- package: github.com/btcsuite/websocket
- package: github.com/bartekn/go-bip39
version: a05967ea095d81c8fe4833776774cfaff8e5036c
11 changes: 11 additions & 0 deletions tools/stellar-hd-wallet/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Changelog

All notable changes to this project will be documented in this
file. This project adheres to [Semantic Versioning](http://semver.org/).

As this project is pre 1.0, breaking changes may happen for minor version
bumps. A breaking change will get clearly notified in this log.

## [v0.0.1] - 2017-12-28

Initial release.
23 changes: 23 additions & 0 deletions tools/stellar-hd-wallet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# stellar-hd-wallet

Console tool to generate Stellar HD wallet for a given seed. Implements [SEP-0005](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0005.md).

This is experimental software. Use at your own risk.

## Usage

```
Simple HD wallet for Stellar Lumens. THIS PROGRAM IS STILL EXPERIMENTAL. USE AT YOUR OWN RISK.
Usage:
stellar-hd-wallet [command]
Available Commands:
accounts Display accounts for a given mnemonic code
new Generates a new mnemonic code
Flags:
-h, --help help for stellar-hd-wallet
Use "stellar-hd-wallet [command] --help" for more information about a command.
```
85 changes: 85 additions & 0 deletions tools/stellar-hd-wallet/commands/accounts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package commands

import (
"encoding/hex"
"fmt"
"regexp"
"strings"

"github.com/bartekn/go-bip39"
"github.com/spf13/cobra"
"github.com/stellar/go/exp/crypto/derivation"
"github.com/stellar/go/keypair"
"github.com/stellar/go/support/errors"
)

var wordsRegexp = regexp.MustCompile(`^[a-z]+$`)
var count, startID uint32

var allowedNumbers = map[uint32]bool{12: true, 15: true, 18: true, 21: true, 24: true}

var AccountsCmd = &cobra.Command{
Use: "accounts",
Short: "Display accounts for a given mnemonic code",
Long: "",
RunE: func(cmd *cobra.Command, args []string) error {
printf("How many words? ")
wordsCount := readUint()
if _, exist := allowedNumbers[wordsCount]; !exist {
return errors.New("Invalid value, allowed values: 12, 15, 18, 21, 24")
}

words := make([]string, wordsCount)
for i := uint32(0); i < wordsCount; i++ {
printf("Enter word #%-4d", i+1)
words[i] = readString()
if !wordsRegexp.MatchString(words[i]) {
println("Invalid word, try again.")
i--
}
}

printf("Enter password (leave empty if none): ")
password := readString()

mnemonic := strings.Join(words, " ")
println("Mnemonic:", mnemonic)

seed, err := bip39.NewSeedWithErrorChecking(mnemonic, password)
if err != nil {
return errors.New("Invalid words or checksum")
}

println("BIP39 Seed:", hex.EncodeToString(seed))

masterKey, err := derivation.DeriveForPath(derivation.StellarAccountPrefix, seed)
if err != nil {
return errors.Wrap(err, "Error deriving master key")
}

println("m/44'/148' key:", hex.EncodeToString(masterKey.Key))

println("")

for i := uint32(startID); i < startID+count; i++ {
key, err := masterKey.Derive(derivation.FirstHardenedIndex + i)
if err != nil {
return errors.Wrap(err, "Error deriving child key")
}

kp, err := keypair.FromRawSeed(key.RawSeed())
if err != nil {
return errors.Wrap(err, "Error creating key pair")
}

println(fmt.Sprintf(derivation.StellarAccountPathFormat, i), kp.Address(), kp.Seed())
}

return nil
},
}

func init() {
AccountsCmd.Flags().Uint32VarP(&count, "count", "c", 10, "number of accounts to display")
AccountsCmd.Flags().Uint32VarP(&startID, "start", "s", 0, "ID of the first wallet to display")
}
114 changes: 114 additions & 0 deletions tools/stellar-hd-wallet/commands/accounts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package commands

import (
"bufio"
"bytes"
"fmt"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

func TestAccounts(t *testing.T) {
tests := []struct {
Words string
Passphrase string
Error string
Want string
}{
{
Words: "illness spike retreat truth genius clock brain pass fit cave bargain toe",
Want: `m/44'/148'/0' GDRXE2BQUC3AZNPVFSCEZ76NJ3WWL25FYFK6RGZGIEKWE4SOOHSUJUJ6 SBGWSG6BTNCKCOB3DIFBGCVMUPQFYPA2G4O34RMTB343OYPXU5DJDVMN
m/44'/148'/1' GBAW5XGWORWVFE2XTJYDTLDHXTY2Q2MO73HYCGB3XMFMQ562Q2W2GJQX SCEPFFWGAG5P2VX5DHIYK3XEMZYLTYWIPWYEKXFHSK25RVMIUNJ7CTIS
m/44'/148'/2' GAY5PRAHJ2HIYBYCLZXTHID6SPVELOOYH2LBPH3LD4RUMXUW3DOYTLXW SDAILLEZCSA67DUEP3XUPZJ7NYG7KGVRM46XA7K5QWWUIGADUZCZWTJP
m/44'/148'/3' GAOD5NRAEORFE34G5D4EOSKIJB6V4Z2FGPBCJNQI6MNICVITE6CSYIAE SBMWLNV75BPI2VB4G27RWOMABVRTSSF7352CCYGVELZDSHCXWCYFKXIX
m/44'/148'/4' GBCUXLFLSL2JE3NWLHAWXQZN6SQC6577YMAU3M3BEMWKYPFWXBSRCWV4 SCPCY3CEHMOP2TADSV2ERNNZBNHBGP4V32VGOORIEV6QJLXD5NMCJUXI
m/44'/148'/5' GBRQY5JFN5UBG5PGOSUOL4M6D7VRMAYU6WW2ZWXBMCKB7GPT3YCBU2XZ SCK27SFHI3WUDOEMJREV7ZJQG34SCBR6YWCE6OLEXUS2VVYTSNGCRS6X
m/44'/148'/6' GBY27SJVFEWR3DUACNBSMJB6T4ZPR4C7ZXSTHT6GMZUDL23LAM5S2PQX SDJ4WDPOQAJYR3YIAJOJP3E6E4BMRB7VZ4QAEGCP7EYVDW6NQD3LRJMZ
m/44'/148'/7' GAY7T23Z34DWLSTEAUKVBPHHBUE4E3EMZBAQSLV6ZHS764U3TKUSNJOF SA3HXJUCE2N27TBIZ5JRBLEBF3TLPQEBINP47E6BTMIWW2RJ5UKR2B3L
m/44'/148'/8' GDJTCF62UUYSAFAVIXHPRBR4AUZV6NYJR75INVDXLLRZLZQ62S44443R SCD5OSHUUC75MSJG44BAT3HFZL2HZMMQ5M4GPDL7KA6HJHV3FLMUJAME
m/44'/148'/9' GBTVYYDIYWGUQUTKX6ZMLGSZGMTESJYJKJWAATGZGITA25ZB6T5REF44 SCJGVMJ66WAUHQHNLMWDFGY2E72QKSI3XGSBYV6BANDFUFE7VY4XNXXR`,
},
{
Words: "resource asthma orphan phone ice canvas fire useful arch jewel impose vague theory cushion top",
Want: `m/44'/148'/0' GAVXVW5MCK7Q66RIBWZZKZEDQTRXWCZUP4DIIFXCCENGW2P6W4OA34RH SAKS7I2PNDBE5SJSUSU2XLJ7K5XJ3V3K4UDFAHMSBQYPOKE247VHAGDB
m/44'/148'/1' GDFCYVCICATX5YPJUDS22KM2GW5QU2KKSPPPT2IC5AQIU6TP3BZSLR5K SAZ2H5GLAVWCUWNPQMB6I3OHRI63T2ACUUAWSH7NAGYYPXGIOPLPW3Q4
m/44'/148'/2' GAUA3XK3SGEQFNCBM423WIM5WCZ4CR4ZDPDFCYSFLCTODGGGJMPOHAAE SDVSSLPL76I33DKAI4LFTOAKCHJNCXUERGPCMVFT655Z4GRLWM6ZZTSC
m/44'/148'/3' GAH3S77QXTAPZ77REY6LGFIJ2XWVXFOKXHCFLA6HQTL3POLVZJDHHUDM SCH56YSGOBYVBC6DO3ZI2PY62GBVXT4SEJSXJOBQYGC2GCEZSB5PEVBZ
m/44'/148'/4' GCSCZVGV2Y3EQ2RATJ7TE6PVWTW5OH5SMG754AF6W6YM3KJF7RMNPB4Y SBWBM73VUNBGBMFD4E2BA7Q756AKVEAAVTQH34RYEUFD6X64VYL5KXQ2
m/44'/148'/5' GDKWYAJE3W6PWCXDZNMFNFQSPTF6BUDANE6OVRYMJKBYNGL62VKKCNCC SAVS4CDQZI6PSA5DPCC42S5WLKYIPKXPCJSFYY4N3VDK25T2XX2BTGVX
m/44'/148'/6' GCDTVB4XDLNX22HI5GUWHBXJFBCPB6JNU6ZON7E57FA3LFURS74CWDJH SDFC7WZT3GDQVQUQMXN7TC7UWDW5E3GSMFPHUT2TSTQ7RKWTRA4PLBAL
m/44'/148'/7' GBTDPL5S4IOUQHDLCZ7I2UXJ2TEHO6DYIQ3F2P5OOP3IS7JSJI4UMHQJ SA6UO2FIYC6AS2MSDECLR6F7NKCJTG67F7R4LV2GYB4HCZYXJZRLPOBB
m/44'/148'/8' GD3KWA24OIM7V3MZKDAVSLN3NBHGKVURNJ72ZCTAJSDTF7RIGFXPW5FQ SBDNHDDICLLMBIDZ2IF2D3LH44OVUGGAVHQVQ6BZQI5IQO6AB6KNJCOV
m/44'/148'/9' GB3C6RRQB3V7EPDXEDJCMTS45LVDLSZQ46PTIGKZUY37DXXEOAKJIWSV SDHRG2J34MGDAYHMOVKVJC6LX2QZMCTIKRO5I4JQ6BJQ36KVL6QUTT72`,
},
{
Words: "bench hurt jump file august wise shallow faculty impulse spring exact slush thunder author capable act festival slice deposit sauce coconut afford frown better",
Want: `m/44'/148'/0' GC3MMSXBWHL6CPOAVERSJITX7BH76YU252WGLUOM5CJX3E7UCYZBTPJQ SAEWIVK3VLNEJ3WEJRZXQGDAS5NVG2BYSYDFRSH4GKVTS5RXNVED5AX7
m/44'/148'/1' GB3MTYFXPBZBUINVG72XR7AQ6P2I32CYSXWNRKJ2PV5H5C7EAM5YYISO SBKSABCPDWXDFSZISAVJ5XKVIEWV4M5O3KBRRLSPY3COQI7ZP423FYB4
m/44'/148'/2' GDYF7GIHS2TRGJ5WW4MZ4ELIUIBINRNYPPAWVQBPLAZXC2JRDI4DGAKU SD5CCQAFRIPB3BWBHQYQ5SC66IB2AVMFNWWPBYGSUXVRZNCIRJ7IHESQ
m/44'/148'/3' GAFLH7DGM3VXFVUID7JUKSGOYG52ZRAQPZHQASVCEQERYC5I4PPJUWBD SBSGSAIKEF7JYQWQSGXKB4SRHNSKDXTEI33WZDRR6UHYQCQ5I6ZGZQPK
m/44'/148'/4' GAXG3LWEXWCAWUABRO6SMAEUKJXLB5BBX6J2KMHFRIWKAMDJKCFGS3NN SBIZH53PIRFTPI73JG7QYA3YAINOAT2XMNAUARB3QOWWVZVBAROHGXWM
m/44'/148'/5' GA6RUD4DZ2NEMAQY4VZJ4C6K6VSEYEJITNSLUQKLCFHJ2JOGC5UCGCFQ SCVM6ZNVRUOP4NMCMMKLTVBEMAF2THIOMHPYSSMPCD2ZU7VDPARQQ6OY
m/44'/148'/6' GCUDW6ZF5SCGCMS3QUTELZ6LSAH6IVVXNRPRLAUNJ2XYLCA7KH7ZCVQS SBSHUZQNC45IAIRSAHMWJEJ35RY7YNW6SMOEBZHTMMG64NKV7Y52ZEO2
m/44'/148'/7' GBJ646Q524WGBN5X5NOAPIF5VQCR2WZCN6QZIDOSY6VA2PMHJ2X636G4 SC2QO2K2B4EBNBJMBZIKOYSHEX4EZAZNIF4UNLH63AQYV6BE7SMYWC6E
m/44'/148'/8' GDHX4LU6YBSXGYTR7SX2P4ZYZSN24VXNJBVAFOB2GEBKNN3I54IYSRM4 SCGMC5AHAAVB3D4JXQPCORWW37T44XJZUNPEMLRW6DCOEARY3H5MAQST
m/44'/148'/9' GDXOY6HXPIDT2QD352CH7VWX257PHVFR72COWQ74QE3TEV4PK2KCKZX7 SCPA5OX4EYINOPAUEQCPY6TJMYICUS5M7TVXYKWXR3G5ZRAJXY3C37GF`,
},
{
Words: "cable spray genius state float twenty onion head street palace net private method loan turn phrase state blanket interest dry amazing dress blast tube",
Passphrase: "p4ssphr4se",
Want: `m/44'/148'/0' GDAHPZ2NSYIIHZXM56Y36SBVTV5QKFIZGYMMBHOU53ETUSWTP62B63EQ SAFWTGXVS7ELMNCXELFWCFZOPMHUZ5LXNBGUVRCY3FHLFPXK4QPXYP2X
m/44'/148'/1' GDY47CJARRHHL66JH3RJURDYXAMIQ5DMXZLP3TDAUJ6IN2GUOFX4OJOC SBQPDFUGLMWJYEYXFRM5TQX3AX2BR47WKI4FDS7EJQUSEUUVY72MZPJF
m/44'/148'/2' GCLAQF5H5LGJ2A6ACOMNEHSWYDJ3VKVBUBHDWFGRBEPAVZ56L4D7JJID SAF2LXRW6FOSVQNC4HHIIDURZL4SCGCG7UEGG23ZQG6Q2DKIGMPZV6BZ
m/44'/148'/3' GBC36J4KG7ZSIQ5UOSJFQNUP4IBRN6LVUFAHQWT2ODEQ7Y3ASWC5ZN3B SDCCVBIYZDMXOR4VPC3IYMIPODNEDZCS44LDN7B5ZWECIE57N3BTV4GQ
m/44'/148'/4' GA6NHA4KPH5LFYD6LZH35SIX3DU5CWU3GX6GCKPJPPTQCCQPP627E3CB SA5TRXTO7BG2Z6QTQT3O2LC7A7DLZZ2RBTGUNCTG346PLVSSHXPNDVNT
m/44'/148'/5' GBOWMXTLABFNEWO34UJNSJJNVEF6ESLCNNS36S5SX46UZT2MNYJOLA5L SDEOED2KPHV355YNOLLDLVQB7HDPQVIGKXCAJMA3HTM4325ZHFZSKKUC
m/44'/148'/6' GBL3F5JUZN3SQKZ7SL4XSXEJI2SNSVGO6WZWNJLG666WOJHNDDLEXTSZ SDYNO6TLFNV3IM6THLNGUG5FII4ET2H7NH3KCT6OAHIUSHKR4XBEEI6A
m/44'/148'/7' GA5XPPWXL22HFFL5K5CE37CEPUHXYGSP3NNWGM6IK6K4C3EFHZFKSAND SDXMJXAY45W3WEFWMYEPLPIF4CXAD5ECQ37XKMGY5EKLM472SSRJXCYD
m/44'/148'/8' GDS5I7L7LWFUVSYVAOHXJET2565MGGHJ4VHGVJXIKVKNO5D4JWXIZ3XU SAIZA26BUP55TDCJ4U7I2MSQEAJDPDSZSBKBPWQTD5OQZQSJAGNN2IQB
m/44'/148'/9' GBOSMFQYKWFDHJWCMCZSMGUMWCZOM4KFMXXS64INDHVCJ2A2JAABCYRR SDXDYPDNRMGOF25AWYYKPHFAD3M54IT7LCLG7RWTGR3TS32A4HTUXNOS`,
},
{
Words: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
Want: `m/44'/148'/0' GB3JDWCQJCWMJ3IILWIGDTQJJC5567PGVEVXSCVPEQOTDN64VJBDQBYX SBUV3MRWKNS6AYKZ6E6MOUVF2OYMON3MIUASWL3JLY5E3ISDJFELYBRZ
m/44'/148'/1' GDVSYYTUAJ3ACHTPQNSTQBDQ4LDHQCMNY4FCEQH5TJUMSSLWQSTG42MV SCHDCVCWGAKGIMTORV6K5DYYV3BY4WG3RA4M6MCBGJLHUCWU2MC6DL66
m/44'/148'/2' GBFPWBTN4AXHPWPTQVQBP4KRZ2YVYYOGRMV2PEYL2OBPPJDP7LECEVHR SAPLVTLUXSDLFRDGCCFLPDZMTCEVMP3ZXTM74EBJCVKZKM34LGQPF7K3
m/44'/148'/3' GCCCOWAKYVFY5M6SYHOW33TSNC7Z5IBRUEU2XQVVT34CIZU7CXZ4OQ4O SDQYXOP2EAUZP4YOEQ5BUJIQ3RDSP5XV4ZFI6C5Y3QCD5Y63LWPXT7PW
m/44'/148'/4' GCQ3J35MKPKJX7JDXRHC5YTXTULFMCBMZ5IC63EDR66QA3LO7264ZL7Q SCT7DUHYZD6DRCETT6M73GWKFJI4D56P3SNWNWNJ7ANLJZS6XIFYYXSB
m/44'/148'/5' GDTA7622ZA5PW7F7JL7NOEFGW62M7GW2GY764EQC2TUJ42YJQE2A3QUL SDTWG5AFDI6GRQNLPWOC7IYS7AKOGMI2GX4OXTBTZHHYPMNZ2PX4ONWU
m/44'/148'/6' GD7A7EACTPTBCYCURD43IEZXGIBCEXNBHN3OFWV2FOX67XKUIGRCTBNU SDJMWY4KFRS4PTA5WBFVCPS2GKYLXOMCLQSBNEIBG7KRGHNQOM25KMCP
m/44'/148'/7' GAF4AGPVLQXFKEWQV3DZU5YEFU6YP7XJHAEEQH4G3R664MSF77FLLRK3 SDOJH5JRCNGT57QTPTJEQGBEBZJPXE7XUDYDB24VTOPP7PH3ALKHAHFG
m/44'/148'/8' GABTYCZJMCP55SS6I46SR76IHETZDLG4L37MLZRZKQDGBLS5RMP65TSX SC6N6GYQ2VA4T7CUP2BWGBRT2P6L2HQSZIUNQRHNDLISF6ND7TW4P4ER
m/44'/148'/9' GAKFARYSPI33KUJE7HYLT47DCX2PFWJ77W3LZMRBPSGPGYPMSDBE7W7X SALJ5LPBTXCFML2CQ7ORP7WJNJOZSVBVRQAAODMVHMUF4P4XXFZB7MKY`,
},
// Invalid:
{
Words: "illness spike retreat truth genius clock brain pass fit cave bargain illness",
Error: "Invalid words or checksum",
},
}

for _, test := range tests {

t.Run(fmt.Sprintf("words %s passphrase %s", test.Words, test.Passphrase), func(t *testing.T) {
words := strings.Split(test.Words, " ")
input := fmt.Sprintf("%d\n%s\n%s\n", len(words), strings.Join(words, "\n"), test.Passphrase)

// Global variables, AFAIK there is no elegant way to pass it to cobra.Command
reader = bufio.NewReader(bytes.NewBufferString(input))
out = &bytes.Buffer{}

err := AccountsCmd.RunE(nil, []string{})
if test.Error != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), test.Error)
} else {
assert.NoError(t, err)
output := out.(*bytes.Buffer).String()
assert.Contains(t, output, test.Want)
}
})
}
}
37 changes: 37 additions & 0 deletions tools/stellar-hd-wallet/commands/io.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package commands

import (
"bufio"
"fmt"
"io"
"log"
"os"
"strconv"
"strings"
)

var reader = bufio.NewReader(os.Stdin)
var out io.Writer = os.Stdout

func readString() string {
line, _ := reader.ReadString('\n')
return strings.TrimRight(line, "\n")
}

func readUint() uint32 {
line := readString()
number, err := strconv.Atoi(line)
if err != nil {
log.Fatal("Invalid value")
}

return uint32(number)
}

func printf(format string, a ...interface{}) {
fmt.Fprintf(out, format, a...)
}

func println(a ...interface{}) {
fmt.Fprintln(out, a...)
}
42 changes: 42 additions & 0 deletions tools/stellar-hd-wallet/commands/new.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package commands

import (
"strings"

"github.com/bartekn/go-bip39"
"github.com/spf13/cobra"
"github.com/stellar/go/support/errors"
)

const DefaultEntropySize = 256

var NewCmd = &cobra.Command{
Use: "new",
Short: "Generates a new mnemonic code",
Long: "",
RunE: func(cmd *cobra.Command, args []string) error {
entropy, err := bip39.NewEntropy(DefaultEntropySize)
if err != nil {
return errors.Wrap(err, "Error generating entropy")
}

mnemonic, err := bip39.NewMnemonic(entropy)
if err != nil {
return errors.Wrap(err, "Error generating mnemonic code")
}

words := strings.Split(mnemonic, " ")
for i := 0; i < len(words); i++ {
printf("word %02d/24: %10s", i+1, words[i])
readString()
}

println("WARNING! Store the words above in a safe place!")
println("WARNING! If you lose your words, you will lose access to funds in all derived accounts!")
println("WARNING! Anyone who has access to these words can spend your funds!")
println("")
println("Use: `stellar-hd-wallet accounts` command to see generated accounts.")

return nil
},
}
24 changes: 24 additions & 0 deletions tools/stellar-hd-wallet/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package main

import (
"log"

"github.com/spf13/cobra"
"github.com/stellar/go/tools/stellar-hd-wallet/commands"
)

var mainCmd = &cobra.Command{
Use: "stellar-hd-wallet",
Short: "Simple HD wallet for Stellar Lumens. THIS PROGRAM IS STILL EXPERIMENTAL. USE AT YOUR OWN RISK.",
}

func init() {
mainCmd.AddCommand(commands.NewCmd)
mainCmd.AddCommand(commands.AccountsCmd)
}

func main() {
if err := mainCmd.Execute(); err != nil {
log.Fatal(err)
}
}

0 comments on commit 3534889

Please sign in to comment.