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 := ðService{
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}