arnaucube
arnaucube/kestoPublic Self-hosted repository
kesto/recover-geth-funds/main.go
recover-geth-funds/main.goRaw
  1package main
  2
  3import (
  4	"context"
  5	"fmt"
  6	"io/ioutil"
  7	"math"
  8	"math/big"
  9	"os"
 10	"strings"
 11
 12	"github.com/ethereum/go-ethereum/accounts"
 13	"github.com/ethereum/go-ethereum/accounts/keystore"
 14	"github.com/ethereum/go-ethereum/common"
 15	"github.com/ethereum/go-ethereum/core/types"
 16	"github.com/ethereum/go-ethereum/ethclient"
 17	"github.com/spf13/viper"
 18	"github.com/urfave/cli"
 19)
 20
 21var cfg Config
 22
 23type Config struct {
 24	Web3 struct {
 25		Url string
 26	}
 27	KeyStore struct {
 28		Path        string
 29		Address     string
 30		Password    string
 31		KeyJsonPath string
 32	}
 33}
 34
 35var cliCommands = []cli.Command{
 36	{
 37		Name:    "info",
 38		Aliases: []string{},
 39		Usage:   "get info",
 40		Action:  cmdInfo,
 41	},
 42	{
 43		Name:    "sendall",
 44		Aliases: []string{},
 45		Usage:   "send all eth to address",
 46		Action:  cmdSendAll,
 47	},
 48}
 49
 50func cmdInfo(c *cli.Context) error {
 51	if err := mustRead(c); err != nil {
 52		return err
 53	}
 54
 55	ks, acc := loadKeyStore()
 56	ethSrv := loadWeb3(ks, &acc)
 57	balance, err := ethSrv.GetBalance(acc.Address)
 58	if err != nil {
 59		fmt.Println("error getting balance")
 60		return err
 61	}
 62	fmt.Println("Current balance " + balance.String() + " ETH")
 63	return nil
 64}
 65
 66func cmdSendAll(c *cli.Context) error {
 67	if err := mustRead(c); err != nil {
 68		return err
 69	}
 70
 71	toAddrStr := c.GlobalString("address")
 72	if toAddrStr == "" {
 73		return fmt.Errorf("no address to send the ETH specified")
 74	}
 75	fmt.Println("Sending to:", toAddrStr)
 76
 77	ks, acc := loadKeyStore()
 78	ethSrv := loadWeb3(ks, &acc)
 79
 80	toAddr := common.HexToAddress(toAddrStr)
 81	if err := ethSrv.SendAll(toAddr); err != nil {
 82		return err
 83	}
 84
 85	return nil
 86}
 87
 88func main() {
 89	app := cli.NewApp()
 90	app.Name = "recover-geth-funds"
 91	app.Usage = "cli to send all the funds from a geth keystore to an address"
 92	app.Version = "0.0.1"
 93	app.Flags = []cli.Flag{
 94		cli.StringFlag{Name: "config"},
 95		&cli.StringFlag{Name: "address"},
 96	}
 97
 98	app.Commands = []cli.Command{}
 99	app.Commands = append(app.Commands, cliCommands...)
100
101	err := app.Run(os.Args)
102	if err != nil {
103		fmt.Println(err)
104		os.Exit(3)
105	}
106}
107
108// load
109
110const (
111	passwdPrefix = "passwd:"
112	filePrefix   = "file:"
113)
114
115func Assert(msg string, err error) {
116	if err != nil {
117		fmt.Println(msg, " ", err.Error())
118		os.Exit(1)
119	}
120}
121
122func mustRead(c *cli.Context) error {
123	viper.SetConfigType("yaml")
124	viper.SetConfigName("config")
125	viper.AddConfigPath(".") // adding home directory as first search path
126
127	if c.GlobalString("config") != "" {
128		viper.SetConfigFile(c.GlobalString("config"))
129	}
130
131	if err := viper.ReadInConfig(); err != nil {
132		return err
133	}
134	err := viper.Unmarshal(&cfg)
135	if err != nil {
136		return err
137	}
138
139	return nil
140}
141
142func loadKeyStore() (*keystore.KeyStore, accounts.Account) {
143	var err error
144	var passwd string
145
146	ks := keystore.NewKeyStore(cfg.KeyStore.Path, keystore.StandardScryptN, keystore.StandardScryptP)
147
148	if strings.HasPrefix(cfg.KeyStore.Password, passwdPrefix) {
149		passwd = cfg.KeyStore.Password[len(passwdPrefix):]
150	} else {
151		filename := cfg.KeyStore.Password
152		if strings.HasPrefix(filename, filePrefix) {
153			filename = cfg.KeyStore.Password[len(filePrefix):]
154		}
155		passwdbytes, err := ioutil.ReadFile(filename)
156		Assert("Cannot read password ", err)
157		passwd = string(passwdbytes)
158	}
159
160	acc, err := ks.Find(accounts.Account{
161		Address: common.HexToAddress(cfg.KeyStore.Address),
162	})
163	Assert("Cannot find keystore account", err)
164
165	Assert("Cannot unlock account", ks.Unlock(acc, passwd))
166	fmt.Println("Keystore and account unlocked successfully:", acc.Address.Hex())
167
168	return ks, acc
169}
170
171func loadWeb3(ks *keystore.KeyStore, acc *accounts.Account) *ethService {
172	// Create geth client
173	url := cfg.Web3.Url
174	hidden := strings.HasPrefix(url, "hidden:")
175	if hidden {
176		url = url[len("hidden:"):]
177	}
178	passwd, err := readPassword(cfg.KeyStore.Password)
179	if err != nil {
180		fmt.Println(err)
181		os.Exit(0)
182	}
183	ethsrv := newEthService(url, ks, acc, cfg.KeyStore.KeyJsonPath, passwd)
184	if hidden {
185		fmt.Println("Connection to web3 server opened", "url", "(hidden)")
186	} else {
187		fmt.Println("Connection to web3 server opened", "url", cfg.Web3.Url)
188	}
189	return ethsrv
190}
191
192func readPassword(configPassword string) (string, error) {
193	var passwd string
194	if strings.HasPrefix(cfg.KeyStore.Password, passwdPrefix) {
195		passwd = cfg.KeyStore.Password[len(passwdPrefix):]
196	} else {
197		filename := cfg.KeyStore.Password
198		if strings.HasPrefix(filename, filePrefix) {
199			filename = cfg.KeyStore.Password[len(filePrefix):]
200		}
201		passwdbytes, err := ioutil.ReadFile(filename)
202		if err != nil {
203			return passwd, err
204		}
205		passwd = string(passwdbytes)
206	}
207	return passwd, nil
208}
209
210// eth
211
212type ethService struct {
213	ks       *keystore.KeyStore
214	acc      *accounts.Account
215	client   *ethclient.Client
216	KeyStore struct {
217		Path     string
218		Password string
219	}
220}
221
222func newEthService(url string, ks *keystore.KeyStore, acc *accounts.Account, keystorePath, password string) *ethService {
223	client, err := ethclient.Dial(url)
224	if err != nil {
225		fmt.Println("Can not open connection to web3 (config.Web3.Url: " + url + ")\n" + err.Error() + "\n")
226		os.Exit(0)
227	}
228
229	service := &ethService{
230		ks:     ks,
231		acc:    acc,
232		client: client,
233		KeyStore: struct {
234			Path     string
235			Password string
236		}{
237			Path:     keystorePath,
238			Password: password,
239		},
240	}
241
242	return service
243}
244
245func (ethSrv *ethService) GetBalance(address common.Address) (*big.Float, error) {
246	balance, err := ethSrv.client.BalanceAt(context.Background(), address, nil)
247	if err != nil {
248		return nil, err
249	}
250	ethBalance := gweiToEth(balance)
251	return ethBalance, nil
252}
253
254func gweiToEth(g *big.Int) *big.Float {
255	f := new(big.Float)
256	f.SetString(g.String())
257	e := new(big.Float).Quo(f, big.NewFloat(math.Pow10(18)))
258	return e
259}
260
261func (ethSrv *ethService) SendAll(toAddr common.Address) error {
262	balance, err := ethSrv.client.BalanceAt(context.Background(), ethSrv.acc.Address, nil)
263	if err != nil {
264		return err
265	}
266	ethBalance := gweiToEth(balance)
267	fmt.Println("Current balance:", ethBalance, "ETH")
268
269	nonce, err := ethSrv.client.PendingNonceAt(context.Background(), ethSrv.acc.Address)
270	if err != nil {
271		return err
272	}
273	fmt.Println("Nonce:", nonce)
274	gasLimit := uint64(21000)
275	gasLimitBI := big.NewInt(int64(gasLimit))
276	gasPrice, err := ethSrv.client.SuggestGasPrice(context.Background())
277	if err != nil {
278		return err
279	}
280	value := new(big.Int).Sub(balance, new(big.Int).Mul(gasLimitBI, gasPrice))
281	fmt.Println("balance", gweiToEth(balance), "ETH")
282	fmt.Println(" tosend", gweiToEth(value), "ETH (substracting the fees)")
283
284	confirmed := askConfirmation()
285	if !confirmed {
286		return fmt.Errorf("operation cancelled")
287	}
288	fmt.Println("operation confirmed")
289
290	var data []byte
291	tx := types.NewTransaction(nonce, toAddr, value, gasLimit, gasPrice, data)
292
293	chainID, err := ethSrv.client.NetworkID(context.Background())
294	if err != nil {
295		return err
296	}
297
298	signedTx, err := ethSrv.ks.SignTx(*ethSrv.acc, tx, chainID)
299	if err != nil {
300		return err
301	}
302
303	err = ethSrv.client.SendTransaction(context.Background(), signedTx)
304	if err != nil {
305		return err
306	}
307
308	fmt.Printf("tx sent: %s", signedTx.Hash().Hex())
309
310	return nil
311}
312
313func askConfirmation() bool {
314	var s string
315
316	fmt.Printf("(y/N): ")
317	_, err := fmt.Scan(&s)
318	if err != nil {
319		panic(err)
320	}
321
322	s = strings.TrimSpace(s)
323	s = strings.ToLower(s)
324
325	if s == "y" || s == "yes" {
326		return true
327	}
328	return false
329}