diff --git a/glide.lock b/glide.lock index 37c1e77297..712db9c0f7 100644 --- a/glide.lock +++ b/glide.lock @@ -349,6 +349,8 @@ imports: - suite - name: github.com/tyler-smith/go-bip32 version: 2c9cfd17756470a0b7c3e4b7954bae7d11035504 +- name: github.com/bartekn/go-bip39 + version: a05967ea095d81c8fe4833776774cfaff8e5036c - name: github.com/tylerb/graceful version: 7116c7a8115899e80197cd9e0b97998c0f97ed8e repo: https://github.com/tylerb/graceful @@ -403,6 +405,7 @@ imports: version: 1f22c0103821b9390939b6776727195525381532 repo: https://go.googlesource.com/crypto subpackages: + - pbkdf2 - ripemd160 - ssh/terminal - name: golang.org/x/net diff --git a/glide.yaml b/glide.yaml index 434e6335dc..29160a80f6 100644 --- a/glide.yaml +++ b/glide.yaml @@ -295,3 +295,5 @@ import: subpackages: - socks - package: github.com/btcsuite/websocket +- package: github.com/bartekn/go-bip39 + version: a05967ea095d81c8fe4833776774cfaff8e5036c diff --git a/tools/stellar-hd-wallet/CHANGELOG.md b/tools/stellar-hd-wallet/CHANGELOG.md new file mode 100644 index 0000000000..4099077918 --- /dev/null +++ b/tools/stellar-hd-wallet/CHANGELOG.md @@ -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. diff --git a/tools/stellar-hd-wallet/README.md b/tools/stellar-hd-wallet/README.md new file mode 100644 index 0000000000..a5353e400e --- /dev/null +++ b/tools/stellar-hd-wallet/README.md @@ -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. +``` diff --git a/tools/stellar-hd-wallet/commands/accounts.go b/tools/stellar-hd-wallet/commands/accounts.go new file mode 100644 index 0000000000..3f07328351 --- /dev/null +++ b/tools/stellar-hd-wallet/commands/accounts.go @@ -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") +} diff --git a/tools/stellar-hd-wallet/commands/accounts_test.go b/tools/stellar-hd-wallet/commands/accounts_test.go new file mode 100644 index 0000000000..e112378a98 --- /dev/null +++ b/tools/stellar-hd-wallet/commands/accounts_test.go @@ -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) + } + }) + } +} diff --git a/tools/stellar-hd-wallet/commands/io.go b/tools/stellar-hd-wallet/commands/io.go new file mode 100644 index 0000000000..49fd0ba000 --- /dev/null +++ b/tools/stellar-hd-wallet/commands/io.go @@ -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...) +} diff --git a/tools/stellar-hd-wallet/commands/new.go b/tools/stellar-hd-wallet/commands/new.go new file mode 100644 index 0000000000..23820cfa30 --- /dev/null +++ b/tools/stellar-hd-wallet/commands/new.go @@ -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 + }, +} diff --git a/tools/stellar-hd-wallet/main.go b/tools/stellar-hd-wallet/main.go new file mode 100644 index 0000000000..8b84393354 --- /dev/null +++ b/tools/stellar-hd-wallet/main.go @@ -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) + } +}