├── .gitignore ├── Gopkg.lock ├── Gopkg.toml ├── README.md ├── address.go ├── command.go ├── db.go ├── ethereum-cold-wallet.yml.example ├── etherscan.go ├── image.go ├── main.go ├── sub.go ├── sync.go ├── tx.go └── util.go /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/* 2 | .vscode/* 3 | debug -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | branch = "master" 6 | digest = "1:54dfd275e69699de2aff5a1ab3b7a0abfbaae0d97bc52f57812a926c2bd3ed29" 7 | name = "github.com/aristanetworks/goarista" 8 | packages = ["monotime"] 9 | pruneopts = "" 10 | revision = "5faa74ffbed7096292069fdcd0eae96146a3158a" 11 | 12 | [[projects]] 13 | branch = "master" 14 | digest = "1:670fed8cdc3568289e2ba821d5b9473f3d92800b5cd584d2e1bce2bff3f78cc6" 15 | name = "github.com/btcsuite/btcd" 16 | packages = [ 17 | "btcec", 18 | "chaincfg", 19 | "chaincfg/chainhash", 20 | "wire", 21 | ] 22 | pruneopts = "" 23 | revision = "3dcf298fed2d5fd65918dc560b3942b2aa0629e8" 24 | 25 | [[projects]] 26 | branch = "master" 27 | digest = "1:56b87c786a316d6e9b9c7ba8f3dd64e3199ca3b33a55cc596c633023bed20264" 28 | name = "github.com/btcsuite/btcutil" 29 | packages = [ 30 | ".", 31 | "base58", 32 | "bech32", 33 | "hdkeychain", 34 | ] 35 | pruneopts = "" 36 | revision = "ab6388e0c60ae4834a1f57511e20c17b5f78be4b" 37 | 38 | [[projects]] 39 | branch = "master" 40 | digest = "1:534bfc1a194affafadc94be31ac3e221de127e47c0c28b3fe8eddc7ca1656f2f" 41 | name = "github.com/chzyer/readline" 42 | packages = ["."] 43 | pruneopts = "" 44 | revision = "2972be24d48e78746da79ba8e24e8b488c9880de" 45 | 46 | [[projects]] 47 | digest = "1:aaeffbff5bd24654cb4c190ed75d6c7b57b4f5d6741914c1a7a6bb7447e756c5" 48 | name = "github.com/deckarep/golang-set" 49 | packages = ["."] 50 | pruneopts = "" 51 | revision = "cbaa98ba5575e67703b32b4b19f73c91f3c4159e" 52 | version = "v1.7.1" 53 | 54 | [[projects]] 55 | digest = "1:c205f1963071408c1fac73c1b37c86ef9b98d80f17e690a2239853cde255ad3d" 56 | name = "github.com/ethereum/go-ethereum" 57 | packages = [ 58 | ".", 59 | "accounts", 60 | "accounts/keystore", 61 | "common", 62 | "common/hexutil", 63 | "common/math", 64 | "common/mclock", 65 | "common/prque", 66 | "core/types", 67 | "crypto", 68 | "crypto/secp256k1", 69 | "crypto/sha3", 70 | "ethclient", 71 | "ethdb", 72 | "event", 73 | "log", 74 | "metrics", 75 | "p2p/netutil", 76 | "params", 77 | "rlp", 78 | "rpc", 79 | "trie", 80 | ] 81 | pruneopts = "" 82 | revision = "58632d44021bf095b43a1bb2443e6e3690a94739" 83 | version = "v1.8.18" 84 | 85 | [[projects]] 86 | digest = "1:eb53021a8aa3f599d29c7102e65026242bdedce998a54837dc67f14b6a97c5fd" 87 | name = "github.com/fsnotify/fsnotify" 88 | packages = ["."] 89 | pruneopts = "" 90 | revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" 91 | version = "v1.4.7" 92 | 93 | [[projects]] 94 | digest = "1:e692d16fdfbddb94e9e4886aaf6c08bdbae5cb4ac80651445de9181b371c6e46" 95 | name = "github.com/go-sql-driver/mysql" 96 | packages = ["."] 97 | pruneopts = "" 98 | revision = "72cd26f257d44c1114970e19afddcd812016007e" 99 | version = "v1.4.1" 100 | 101 | [[projects]] 102 | digest = "1:a01080d20c45c031c13f3828c56e58f4f51d926a482ad10cc0316225097eb7ea" 103 | name = "github.com/go-stack/stack" 104 | packages = ["."] 105 | pruneopts = "" 106 | revision = "2fee6af1a9795aafbe0253a0cfbdf668e1fb8a9a" 107 | version = "v1.8.0" 108 | 109 | [[projects]] 110 | branch = "master" 111 | digest = "1:30d80a3d5f7ac0ca84ae7a46aa2648f93efb85a2a498431f2db1c2928fb048b4" 112 | name = "github.com/gocarina/gocsv" 113 | packages = ["."] 114 | pruneopts = "" 115 | revision = "cde31a6ec2a87636e8a5f13d18468e9dd683149e" 116 | 117 | [[projects]] 118 | branch = "master" 119 | digest = "1:05f7dd1dc7530cd6b959f8b49660cd898f8dc9601b4e764ef9bdf211e6318774" 120 | name = "github.com/golang/freetype" 121 | packages = [ 122 | ".", 123 | "raster", 124 | "truetype", 125 | ] 126 | pruneopts = "" 127 | revision = "e2365dfdc4a05e4b8299a783240d4a7d5a65d4e4" 128 | 129 | [[projects]] 130 | branch = "master" 131 | digest = "1:2a5888946cdbc8aa360fd43301f9fc7869d663f60d5eedae7d4e6e5e4f06f2bf" 132 | name = "github.com/golang/snappy" 133 | packages = ["."] 134 | pruneopts = "" 135 | revision = "2e65f85255dbc3072edf28d6b5b8efc472979f5a" 136 | 137 | [[projects]] 138 | digest = "1:a25a2c5ae694b01713fb6cd03c3b1ac1ccc1902b9f0a922680a88ec254f968e1" 139 | name = "github.com/google/uuid" 140 | packages = ["."] 141 | pruneopts = "" 142 | revision = "9b3b1e0f5f99ae461456d768e7d301a7acdaa2d8" 143 | version = "v1.1.0" 144 | 145 | [[projects]] 146 | digest = "1:d14365c51dd1d34d5c79833ec91413bfbb166be978724f15701e17080dc06dec" 147 | name = "github.com/hashicorp/hcl" 148 | packages = [ 149 | ".", 150 | "hcl/ast", 151 | "hcl/parser", 152 | "hcl/printer", 153 | "hcl/scanner", 154 | "hcl/strconv", 155 | "hcl/token", 156 | "json/parser", 157 | "json/scanner", 158 | "json/token", 159 | ] 160 | pruneopts = "" 161 | revision = "8cb6e5b959231cc1119e43259c4a608f9c51a241" 162 | version = "v1.0.0" 163 | 164 | [[projects]] 165 | digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be" 166 | name = "github.com/inconshreveable/mousetrap" 167 | packages = ["."] 168 | pruneopts = "" 169 | revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" 170 | version = "v1.0" 171 | 172 | [[projects]] 173 | digest = "1:d269638dbd514822446c3c818b6389c880058af79ec54d97731818b34fe66921" 174 | name = "github.com/jinzhu/gorm" 175 | packages = [ 176 | ".", 177 | "dialects/mysql", 178 | ] 179 | pruneopts = "" 180 | revision = "6ed508ec6a4ecb3531899a69cbc746ccf65a4166" 181 | version = "v1.9.1" 182 | 183 | [[projects]] 184 | branch = "master" 185 | digest = "1:d9a7385b84d8187fd94e0357045c6fa1147ca94caa56fdd539336c7c102fc728" 186 | name = "github.com/jinzhu/inflection" 187 | packages = ["."] 188 | pruneopts = "" 189 | revision = "04140366298a54a039076d798123ffa108fff46c" 190 | 191 | [[projects]] 192 | branch = "master" 193 | digest = "1:14a9763035ddd561bd2434463ff342a5cec5fd94730d2bd33d21a14714ee57e1" 194 | name = "github.com/juju/ansiterm" 195 | packages = [ 196 | ".", 197 | "tabwriter", 198 | ] 199 | pruneopts = "" 200 | revision = "720a0952cc2ac777afc295d9861263e2a4cf96a1" 201 | 202 | [[projects]] 203 | digest = "1:6a874e3ddfb9db2b42bd8c85b6875407c702fa868eed20634ff489bc896ccfd3" 204 | name = "github.com/konsorten/go-windows-terminal-sequences" 205 | packages = ["."] 206 | pruneopts = "" 207 | revision = "5c8c8bd35d3832f5d134ae1e1e375b69a4d25242" 208 | version = "v1.0.1" 209 | 210 | [[projects]] 211 | branch = "master" 212 | digest = "1:c1c15feaec29751c1cd5491983089fc59d0543545e7063d303f6e2e3b8963458" 213 | name = "github.com/llgcode/draw2d" 214 | packages = [ 215 | ".", 216 | "draw2dbase", 217 | "draw2dimg", 218 | ] 219 | pruneopts = "" 220 | revision = "f52c8a71aff06ab8df41843d33ab167b36c971cd" 221 | 222 | [[projects]] 223 | branch = "master" 224 | digest = "1:a1df2b8580d14ed39468ed683fa344ef2ebabd77eccb45cbad057b30181bb6c0" 225 | name = "github.com/lunixbochs/vtclean" 226 | packages = ["."] 227 | pruneopts = "" 228 | revision = "2d01aacdc34a083dca635ba869909f5fc0cd4f41" 229 | 230 | [[projects]] 231 | digest = "1:961dc3b1d11f969370533390fdf203813162980c858e1dabe827b60940c909a5" 232 | name = "github.com/magiconair/properties" 233 | packages = ["."] 234 | pruneopts = "" 235 | revision = "c2353362d570a7bfa228149c62842019201cfb71" 236 | version = "v1.8.0" 237 | 238 | [[projects]] 239 | branch = "master" 240 | digest = "1:212bebc561f4f654a653225868b2a97353cd5e160dc0b0bbc7232b06608474ec" 241 | name = "github.com/mailru/easyjson" 242 | packages = [ 243 | ".", 244 | "buffer", 245 | "jlexer", 246 | "jwriter", 247 | ] 248 | pruneopts = "" 249 | revision = "60711f1a8329503b04e1c88535f419d0bb440bff" 250 | 251 | [[projects]] 252 | branch = "master" 253 | digest = "1:7ae079165755b6a8e01de8fc40129c17a0656d38f22b5b29ef7b664c578e8195" 254 | name = "github.com/manifoldco/promptui" 255 | packages = [ 256 | ".", 257 | "list", 258 | "screenbuf", 259 | ] 260 | pruneopts = "" 261 | revision = "ad16ba47f57219da6f554d1136cd07a8543c1564" 262 | 263 | [[projects]] 264 | digest = "1:9ea83adf8e96d6304f394d40436f2eb44c1dc3250d223b74088cc253a6cd0a1c" 265 | name = "github.com/mattn/go-colorable" 266 | packages = ["."] 267 | pruneopts = "" 268 | revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072" 269 | version = "v0.0.9" 270 | 271 | [[projects]] 272 | digest = "1:3140e04675a6a91d2a20ea9d10bdadf6072085502e6def6768361260aee4b967" 273 | name = "github.com/mattn/go-isatty" 274 | packages = ["."] 275 | pruneopts = "" 276 | revision = "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c" 277 | version = "v0.0.4" 278 | 279 | [[projects]] 280 | branch = "master" 281 | digest = "1:096a8a9182648da3d00ff243b88407838902b6703fc12657f76890e08d1899bf" 282 | name = "github.com/mitchellh/go-homedir" 283 | packages = ["."] 284 | pruneopts = "" 285 | revision = "ae18d6b8b3205b561c79e8e5f69bff09736185f4" 286 | 287 | [[projects]] 288 | digest = "1:bcc46a0fbd9e933087bef394871256b5c60269575bb661935874729c65bbbf60" 289 | name = "github.com/mitchellh/mapstructure" 290 | packages = ["."] 291 | pruneopts = "" 292 | revision = "3536a929edddb9a5b34bd6861dc4a9647cb459fe" 293 | version = "v1.1.2" 294 | 295 | [[projects]] 296 | digest = "1:fe67641b990bdc1802f8a1e462a4924210a8762a8a17b72e09656049c906b871" 297 | name = "github.com/moul/http2curl" 298 | packages = ["."] 299 | pruneopts = "" 300 | revision = "9ac6cf4d929b2fa8fd2d2e6dec5bb0feb4f4911d" 301 | version = "v1.0.0" 302 | 303 | [[projects]] 304 | digest = "1:c4fcd4b569d7e98ce7928c43d3faf8d5e1420d4ea8144826743072530b6d9259" 305 | name = "github.com/olivere/elastic" 306 | packages = [ 307 | ".", 308 | "config", 309 | "uritemplates", 310 | ] 311 | pruneopts = "" 312 | revision = "0f13c62d4153081f2be7d0e136353b57dd3f1619" 313 | version = "v6.2.13" 314 | 315 | [[projects]] 316 | digest = "1:1d1d7cc9c374bc5bd1884d07705dbde6d2257de17f5f6504e4335e4d2b60a350" 317 | name = "github.com/parnurzeal/gorequest" 318 | packages = ["."] 319 | pruneopts = "" 320 | revision = "a578a48e8d6ca8b01a3b18314c43c6716bb5f5a3" 321 | version = "v0.2.15" 322 | 323 | [[projects]] 324 | digest = "1:a5484d4fa43127138ae6e7b2299a6a52ae006c7f803d98d717f60abf3e97192e" 325 | name = "github.com/pborman/uuid" 326 | packages = ["."] 327 | pruneopts = "" 328 | revision = "adf5a7427709b9deb95d29d3fa8a2bf9cfd388f1" 329 | version = "v1.2" 330 | 331 | [[projects]] 332 | digest = "1:894aef961c056b6d85d12bac890bf60c44e99b46292888bfa66caf529f804457" 333 | name = "github.com/pelletier/go-toml" 334 | packages = ["."] 335 | pruneopts = "" 336 | revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194" 337 | version = "v1.2.0" 338 | 339 | [[projects]] 340 | digest = "1:7365acd48986e205ccb8652cc746f09c8b7876030d53710ea6ef7d0bd0dcd7ca" 341 | name = "github.com/pkg/errors" 342 | packages = ["."] 343 | pruneopts = "" 344 | revision = "645ef00459ed84a119197bfb8d8205042c6df63d" 345 | version = "v0.8.0" 346 | 347 | [[projects]] 348 | digest = "1:6938b6ee0351393c55baa54b64352e49a741a1b7b616a7d134d237106812070f" 349 | name = "github.com/rjeczalik/notify" 350 | packages = ["."] 351 | pruneopts = "" 352 | revision = "69d839f37b13a8cb7a78366f7633a4071cb43be7" 353 | version = "v0.9.2" 354 | 355 | [[projects]] 356 | digest = "1:5f47c69f85311c4dc292be6cc995a0a3fe8337a6ce38ef4f71e5b7efd5ad42e0" 357 | name = "github.com/rs/cors" 358 | packages = ["."] 359 | pruneopts = "" 360 | revision = "9a47f48565a795472d43519dd49aac781f3034fb" 361 | version = "v1.6.0" 362 | 363 | [[projects]] 364 | digest = "1:615c827f6a892973a587c754ae5fad7acfc4352657aff23d0238fe0ba2a154df" 365 | name = "github.com/shopspring/decimal" 366 | packages = ["."] 367 | pruneopts = "" 368 | revision = "cd690d0c9e2447b1ef2a129a6b7b49077da89b8e" 369 | version = "1.1.0" 370 | 371 | [[projects]] 372 | digest = "1:9d57e200ef5ccc4217fe0a34287308bac652435e7c6513f6263e0493d2245c56" 373 | name = "github.com/sirupsen/logrus" 374 | packages = ["."] 375 | pruneopts = "" 376 | revision = "bcd833dfe83d3cebad139e4a29ed79cb2318bf95" 377 | version = "v1.2.0" 378 | 379 | [[projects]] 380 | branch = "master" 381 | digest = "1:5a8a35c2323f64306651615b855edc31c05c99be7e5d6fb4f3716581ba4e0adf" 382 | name = "github.com/skip2/go-qrcode" 383 | packages = [ 384 | ".", 385 | "bitset", 386 | "reedsolomon", 387 | ] 388 | pruneopts = "" 389 | revision = "cf5f9fa2f0d847edb8e038db7ed975e239095e1a" 390 | 391 | [[projects]] 392 | digest = "1:d0431c2fd72e39ee43ea7742322abbc200c3e704c9102c5c3c2e2e667095b0ca" 393 | name = "github.com/spf13/afero" 394 | packages = [ 395 | ".", 396 | "mem", 397 | ] 398 | pruneopts = "" 399 | revision = "d40851caa0d747393da1ffb28f7f9d8b4eeffebd" 400 | version = "v1.1.2" 401 | 402 | [[projects]] 403 | digest = "1:ae3493c780092be9d576a1f746ab967293ec165e8473425631f06658b6212afc" 404 | name = "github.com/spf13/cast" 405 | packages = ["."] 406 | pruneopts = "" 407 | revision = "8c9545af88b134710ab1cd196795e7f2388358d7" 408 | version = "v1.3.0" 409 | 410 | [[projects]] 411 | digest = "1:a1403cc8a94b8d7956ee5e9694badef0e7b051af289caad1cf668331e3ffa4f6" 412 | name = "github.com/spf13/cobra" 413 | packages = ["."] 414 | pruneopts = "" 415 | revision = "ef82de70bb3f60c65fb8eebacbb2d122ef517385" 416 | version = "v0.0.3" 417 | 418 | [[projects]] 419 | digest = "1:9ceffa4ab5f7195ecf18b3a7fff90c837a9ed5e22e66d18069e4bccfe1f52aa0" 420 | name = "github.com/spf13/jwalterweatherman" 421 | packages = ["."] 422 | pruneopts = "" 423 | revision = "4a4406e478ca629068e7768fc33f3f044173c0a6" 424 | version = "v1.0.0" 425 | 426 | [[projects]] 427 | digest = "1:cbaf13cdbfef0e4734ed8a7504f57fe893d471d62a35b982bf6fb3f036449a66" 428 | name = "github.com/spf13/pflag" 429 | packages = ["."] 430 | pruneopts = "" 431 | revision = "298182f68c66c05229eb03ac171abe6e309ee79a" 432 | version = "v1.0.3" 433 | 434 | [[projects]] 435 | digest = "1:1ed7a19588d3b74fc6a45fe89f95aa980a34870342fb9c3183b19cad08b18a1e" 436 | name = "github.com/spf13/viper" 437 | packages = ["."] 438 | pruneopts = "" 439 | revision = "2c12c60302a5a0e62ee102ca9bc996277c2f64f5" 440 | version = "v1.2.1" 441 | 442 | [[projects]] 443 | branch = "master" 444 | digest = "1:81afc08d660bf2e3532e74733a7ea3d8c370b26892858634554731cbfb31df5f" 445 | name = "github.com/syndtr/goleveldb" 446 | packages = [ 447 | "leveldb", 448 | "leveldb/cache", 449 | "leveldb/comparer", 450 | "leveldb/errors", 451 | "leveldb/filter", 452 | "leveldb/iterator", 453 | "leveldb/journal", 454 | "leveldb/memdb", 455 | "leveldb/opt", 456 | "leveldb/storage", 457 | "leveldb/table", 458 | "leveldb/util", 459 | ] 460 | pruneopts = "" 461 | revision = "f9080354173f192dfc8821931eacf9cfd6819253" 462 | 463 | [[projects]] 464 | branch = "master" 465 | digest = "1:b63cb1d01ba9c94bcd09b569ce9142e18bf3407cf574a2ee286ed8ee6aa170d4" 466 | name = "github.com/tyler-smith/go-bip39" 467 | packages = [ 468 | ".", 469 | "wordlists", 470 | ] 471 | pruneopts = "" 472 | revision = "dbb3b84ba2ef14e894f5e33d6c6e43641e665738" 473 | 474 | [[projects]] 475 | branch = "master" 476 | digest = "1:f7be435e0ca22e2cd62b2d2542081a231685837170a87a3662abb7cdf9f3f1cd" 477 | name = "golang.org/x/crypto" 478 | packages = [ 479 | "pbkdf2", 480 | "ripemd160", 481 | "scrypt", 482 | "ssh/terminal", 483 | ] 484 | pruneopts = "" 485 | revision = "3d3f9f413869b949e48070b5bc593aa22cc2b8f2" 486 | 487 | [[projects]] 488 | branch = "master" 489 | digest = "1:5af73a1d89fa7bcb0f7f3e49c8d7d1f7b3da80430424b2a2055de414b779f31a" 490 | name = "golang.org/x/image" 491 | packages = [ 492 | "draw", 493 | "font", 494 | "math/f64", 495 | "math/fixed", 496 | "tiff", 497 | "tiff/lzw", 498 | ] 499 | pruneopts = "" 500 | revision = "cd38e8056d9b27bb2f265effa37fb0ea6b8a7f0f" 501 | 502 | [[projects]] 503 | branch = "master" 504 | digest = "1:4ac199b027ed34460ec4e0a92c882156f561e78cd046fef095e50f867462435a" 505 | name = "golang.org/x/net" 506 | packages = [ 507 | "idna", 508 | "publicsuffix", 509 | "websocket", 510 | ] 511 | pruneopts = "" 512 | revision = "adae6a3d119ae4890b46832a2e88a95adc62b8e7" 513 | 514 | [[projects]] 515 | branch = "master" 516 | digest = "1:303c0ee48d6229a2423950f41b3ccb5a2067dc4c7b65f8863cfbd962bef05a85" 517 | name = "golang.org/x/sys" 518 | packages = [ 519 | "unix", 520 | "windows", 521 | ] 522 | pruneopts = "" 523 | revision = "62eef0e2fa9b2c385f7b2778e763486da6880d37" 524 | 525 | [[projects]] 526 | digest = "1:5acd3512b047305d49e8763eef7ba423901e85d5dd2fd1e71778a0ea8de10bd4" 527 | name = "golang.org/x/text" 528 | packages = [ 529 | "collate", 530 | "collate/build", 531 | "internal/colltab", 532 | "internal/gen", 533 | "internal/tag", 534 | "internal/triegen", 535 | "internal/ucd", 536 | "language", 537 | "secure/bidirule", 538 | "transform", 539 | "unicode/bidi", 540 | "unicode/cldr", 541 | "unicode/norm", 542 | "unicode/rangetable", 543 | ] 544 | pruneopts = "" 545 | revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" 546 | version = "v0.3.0" 547 | 548 | [[projects]] 549 | branch = "master" 550 | digest = "1:dd89343644062d82cf065354c6a014e654fabe30580cc68f3b2bb23f345feb84" 551 | name = "gonum.org/v1/plot" 552 | packages = [ 553 | "vg", 554 | "vg/fonts", 555 | "vg/vgimg", 556 | ] 557 | pruneopts = "" 558 | revision = "59819fff2fb90906d88e6aef7f06349b08ef451f" 559 | 560 | [[projects]] 561 | digest = "1:77d3cff3a451d50be4b52db9c7766c0d8570ba47593f0c9dc72173adb208e788" 562 | name = "google.golang.org/appengine" 563 | packages = ["cloudsql"] 564 | pruneopts = "" 565 | revision = "4a4468ece617fc8205e99368fa2200e9d1fad421" 566 | version = "v1.3.0" 567 | 568 | [[projects]] 569 | branch = "v2" 570 | digest = "1:4f830ee018eb8c56d0def653ad7c9a1d2a053f0cef2ac6b2200f73b98fa6a681" 571 | name = "gopkg.in/natefinch/npipe.v2" 572 | packages = ["."] 573 | pruneopts = "" 574 | revision = "c1b8fa8bdccecb0b8db834ee0b92fdbcfa606dd6" 575 | 576 | [[projects]] 577 | digest = "1:f0620375dd1f6251d9973b5f2596228cc8042e887cd7f827e4220bc1ce8c30e2" 578 | name = "gopkg.in/yaml.v2" 579 | packages = ["."] 580 | pruneopts = "" 581 | revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" 582 | version = "v2.2.1" 583 | 584 | [solve-meta] 585 | analyzer-name = "dep" 586 | analyzer-version = 1 587 | input-imports = [ 588 | "github.com/btcsuite/btcd/chaincfg", 589 | "github.com/btcsuite/btcutil/hdkeychain", 590 | "github.com/ethereum/go-ethereum/accounts/keystore", 591 | "github.com/ethereum/go-ethereum/common", 592 | "github.com/ethereum/go-ethereum/common/hexutil", 593 | "github.com/ethereum/go-ethereum/core/types", 594 | "github.com/ethereum/go-ethereum/crypto", 595 | "github.com/ethereum/go-ethereum/ethclient", 596 | "github.com/ethereum/go-ethereum/rlp", 597 | "github.com/gocarina/gocsv", 598 | "github.com/jinzhu/gorm", 599 | "github.com/jinzhu/gorm/dialects/mysql", 600 | "github.com/manifoldco/promptui", 601 | "github.com/mitchellh/go-homedir", 602 | "github.com/olivere/elastic", 603 | "github.com/parnurzeal/gorequest", 604 | "github.com/pborman/uuid", 605 | "github.com/shopspring/decimal", 606 | "github.com/sirupsen/logrus", 607 | "github.com/skip2/go-qrcode", 608 | "github.com/spf13/cobra", 609 | "github.com/spf13/viper", 610 | "github.com/tyler-smith/go-bip39", 611 | "gonum.org/v1/plot/vg", 612 | "gonum.org/v1/plot/vg/vgimg", 613 | ] 614 | solver-name = "gps-cdcl" 615 | solver-version = 1 616 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | name = "github.com/ethereum/go-ethereum" 26 | version = "1.8.8" 27 | 28 | [[constraint]] 29 | name = "github.com/spf13/cobra" 30 | version = "0.0.3" 31 | 32 | [[constraint]] 33 | name = "github.com/spf13/viper" 34 | version = "1.0.2" 35 | 36 | [[constraint]] 37 | branch = "master" 38 | name = "github.com/mitchellh/go-homedir" 39 | 40 | [[constraint]] 41 | name = "github.com/sirupsen/logrus" 42 | version = "1.0.5" 43 | 44 | [[constraint]] 45 | name = "github.com/manifoldco/promptui" 46 | branch = "master" 47 | 48 | [[constraint]] 49 | branch = "master" 50 | name = "github.com/tyler-smith/go-bip39" 51 | 52 | [[constraint]] 53 | branch = "master" 54 | name = "github.com/btcsuite/btcutil" 55 | 56 | [[constraint]] 57 | name = "github.com/olivere/elastic" 58 | version = "6.1.22" 59 | 60 | [[constraint]] 61 | branch = "master" 62 | name = "github.com/gocarina/gocsv" 63 | 64 | [[constraint]] 65 | name = "github.com/jinzhu/gorm" 66 | version = "1.9.1" 67 | 68 | [[constraint]] 69 | name = "github.com/parnurzeal/gorequest" 70 | version = "0.2.15" 71 | 72 | [[constraint]] 73 | branch = "master" 74 | name = "github.com/skip2/go-qrcode" 75 | 76 | [[constraint]] 77 | branch = "master" 78 | name = "gonum.org/v1/plot" 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ethereum-cold-wallet 2 | Generate Ethereum HD wallet & offline sign && broadcast signed tx to network. Solution for eth cold wallet. 3 | ### Install 4 | Environment Require 5 | - Golang 6 | - Ethereum Private chain 7 | - dep 8 | - cgo or [xgo](https://github.com/karalabe/xgo) (for go-ethereum dependency) 9 | - MySQL (construct tx) 10 | 11 | ```bash 12 | go get -u github.com/wenweih/ethereum-cold-wallet 13 | cd $GOPATH/src/github.com/wenweih/ethereum-cold-wallet 14 | dep ensure -v -update 15 | ``` 16 | because of the codebase import [go-ethereum](https://github.com/ethereum/go-ethereum), which is dependent on c, so cross compile need cgo. I hightly recommend a tool name [xgo](https://github.com/karalabe/xgo) for Go CGO cross compiler, which is based on the concept of lightweight Linux containers. 17 | ```bash 18 | go get github.com/karalabe/xgo 19 | ``` 20 | Next step is compile binary package for specify platform server, like this: 21 | ```bash 22 | # for ubuner server 23 | xgo --targets=linux/amd64 ./ 24 | ``` 25 | if you are insterested in xgo usage, pls read the docutment: [xgo#usage](https://github.com/karalabe/xgo#usage) 26 | ### Usage 27 | Firstly, modify configure and put it in ~/ethereum-cold-wallet.yml 28 | ```bash 29 | ./ethereum-cold-wallet -h 30 | time="2018-08-12T23:47:55+08:00" level=warning Note="all operate is recorded" Time:="Sun Aug 12 23:47:55 2018" 31 | Generate Ethereum account and sign tx 32 | 33 | Usage: 34 | ethereum-service [command] 35 | 36 | Available Commands: 37 | construct construct transactio 38 | genaccount Generate ethereum account 39 | help Help about any command 40 | send broadcast signex transaction to ethereum network 41 | sign sigin transactio 42 | sub subscribe new block event 43 | 44 | Flags: 45 | -h, --help help for ethereum-service 46 | 47 | Use "ethereum-service [command] --help" for more information about a command. 48 | ``` 49 | #### Generate HD Wallet 50 | ```bash 51 | ./ethereum-cold-wallet genaccount -n 3 52 | time="2018-08-13T15:40:10+08:00" level=warning Note="all operate is recorded" Time:="Mon Aug 13 15:40:10 2018" 53 | time="2018-08-13T15:40:11+08:00" level=info Generate Ethereum account=0xe5379d64Cd7d2D963B03da01fB052218a9aCB0Ce Time:="Mon Aug 13 15:40:11 2018" 54 | time="2018-08-13T15:40:12+08:00" level=info Generate Ethereum account=0x8Dc63ce8b979627C11f5EEf673990814D4815613 Time:="Mon Aug 13 15:40:12 2018" 55 | time="2018-08-13T15:40:13+08:00" level=info Generate Ethereum account=0x48031a8E6150B6ED53F0342451D269f109934729 Time:="Mon Aug 13 15:40:13 2018" 56 | time="2018-08-13T15:40:13+08:00" level=warning Time:="Mon Aug 13 15:40:13 2018" export address to file=/Users/hww/account/eth_address.csv 57 | ``` 58 | All elements about wallet are generated in **~/account** folder: 59 | ```bash 60 | ▶ tree account 61 | account 62 | ├── eth_address.csv 63 | ├── keystore 64 | │   └── version_1_2018-08-13_15-40-10 65 | │   ├── 0x48031a8E6150B6ED53F0342451D269f109934729.json 66 | │   ├── 0x8Dc63ce8b979627C11f5EEf673990814D4815613.json 67 | │   └── 0xe5379d64Cd7d2D963B03da01fB052218a9aCB0Ce.json 68 | ├── mnemonic_qrcode 69 | │   └── version_1_2018-08-13_15-40-10 70 | │   ├── 0x48031a8E6150B6ED53F0342451D269f109934729 71 | │   │   ├── 0x48031a8E6150B6ED53F0342451D269f109934729_aesdecrypt_key_marked.png 72 | │   │   └── 0x48031a8E6150B6ED53F0342451D269f109934729_aesdecrypt_mnemonic_marked.png 73 | │   ├── 0x8Dc63ce8b979627C11f5EEf673990814D4815613 74 | │   │   ├── 0x8Dc63ce8b979627C11f5EEf673990814D4815613_aesdecrypt_key_marked.png 75 | │   │   └── 0x8Dc63ce8b979627C11f5EEf673990814D4815613_aesdecrypt_mnemonic_marked.png 76 | │   └── 0xe5379d64Cd7d2D963B03da01fB052218a9aCB0Ce 77 | │   ├── 0xe5379d64Cd7d2D963B03da01fB052218a9aCB0Ce_aesdecrypt_key_marked.png 78 | │   └── 0xe5379d64Cd7d2D963B03da01fB052218a9aCB0Ce_aesdecrypt_mnemonic_marked.png 79 | ├── random_pwd_first 80 | │   └── version_1_2018-08-13_15-40-10 81 | │   └── randompwd.json 82 | └── random_pwd_second 83 | └── version_1_2018-08-13_15-40-10 84 | └── randompwd.json 85 | ``` 86 | #### construct transacion 87 | we have generated some wallets, next step is send amount of eth to the address. By conveniently, we sent ETH using Private Ethereum in our laptop. 88 | [Ethereum 私有链和 web3.js 使用](https://huangwenwei.com/blogs/ethereum-private-chain-and-web3js) 89 | 90 | ```bash 91 | # deposit 30 ETH to test address 92 | Welcome to the Geth JavaScript console! 93 | 94 | instance: Geth/v1.8.10-unstable-7677ec1f/darwin-amd64/go1.9.2 95 | coinbase: 0x37764d6eae4fad0c69cb7194896e0af7cf260885 96 | at block: 570 (Wed, 08 Aug 2018 18:22:01 CST) 97 | datadir: /Users/hww/geth_private_data 98 | modules: admin:1.0 debug:1.0 eth:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0 99 | 100 | > 101 | > eth.sendTransaction({from: eth.coinbase, to: "0xe5379d64Cd7d2D963B03da01fB052218a9aCB0Ce",value:web3.toWei(30,"ether")}) 102 | "0xc8167e6ad819c6fa9dbbb8f45cf0da3c00213553af8bbfcf89bdb631e60d48da" 103 | > web3.fromWei(web3.eth.getBalance("0xe5379d64Cd7d2D963B03da01fB052218a9aCB0Ce"),"ether") 104 | 30 105 | ``` 106 | soft link to **eth_address.csv** and construct transacion for address **0xe5379d64Cd7d2D963B03da01fB052218a9aCB0Ce** 107 | ```bash 108 | ln /Users/hww/account/eth_address.csv ~/ 109 | 110 | ▶ ethereum-cold-wallet construct -n geth 111 | time="2018-08-13T15:45:46+08:00" level=warning Note="all operate is recorded" Time:="Mon Aug 13 15:45:46 2018" 112 | time="2018-08-13T15:45:46+08:00" level=info Time:="Mon Aug 13 15:45:46 2018" Using Configure file=/Users/hww/ethereum-cold-wallet.yml 113 | time="2018-08-13T15:45:46+08:00" level=info msg="csv2db done" 114 | time="2018-08-13T15:45:46+08:00" level=info msg="Exported HexTx to /Users/hww/tx/unsign/unsign_from.0xe5379d64Cd7d2D963B03da01fB052218a9aCB0Ce.json" 115 | time="2018-08-13T15:45:46+08:00" level=warning msg="Ignore: 0x8Dc63ce8b979627C11f5EEf673990814D4815613 balance not great than the configure amount" 116 | time="2018-08-13T15:45:46+08:00" level=warning msg="Ignore: 0x48031a8E6150B6ED53F0342451D269f109934729 balance not great than the configure amount" 117 | ``` 118 | as you can see, the contructed transaction is export to ```/Users/hww/tx/unsign/``` folder, we can copy these unsign transaction to offline computer, which is holder our wallet keys, in this example, we handle it in my laptop too. 119 | #### Sign raw transaction 120 | ```bash 121 | ▶ ethereum-cold-wallet sign 122 | time="2018-08-13T15:59:03+08:00" level=warning Note="all operate is recorded" Time:="Mon Aug 13 15:59:03 2018" 123 | time="2018-08-13T15:59:03+08:00" level=info Time:="Mon Aug 13 15:59:03 2018" Using Configure file=/Users/hww/ethereum-cold-wallet.yml 124 | time="2018-08-13T15:59:03+08:00" level=info msg="签名交易: 0x3f00ff54245328604a6f43f4de279de100d4afc8d5e7536eeaee7b531c2d64d2 To: 0x8Dc63ce8b979627C11f5EEf673990814D4815613" 125 | time="2018-08-13T15:59:04+08:00" level=info msg="Exported HexTx to /Users/hww/tx/signed/signed_from.0xe5379d64Cd7d2D963B03da01fB052218a9aCB0Ce.json" 126 | ``` 127 | The transaction we constructed is signed and export json file to ```/Users/hww/tx/signed/``` folder, copy the result to broadcast the signed sendTransaction. 128 | #### broadcast signed transacion 129 | ```bash 130 | ▶ ethereum-cold-wallet send 131 | time="2018-08-13T16:03:18+08:00" level=warning Note="all operate is recorded" Time:="Mon Aug 13 16:03:18 2018" 132 | time="2018-08-13T16:03:18+08:00" level=info Time:="Mon Aug 13 16:03:18 2018" Using Configure file=/Users/hww/ethereum-cold-wallet.yml 133 | time="2018-08-13T16:03:18+08:00" level=info msg="send tx: 0xbdfece2382b6e08c265928578b11b00292582670ad5b2c7a90243267b892d41b success" 134 | ``` 135 | log show we have send the transacion successfully, now we query the tx related addresses balance in web3 console: 136 | - from 0xe5379d64Cd7d2D963B03da01fB052218a9aCB0Ce should be 0 137 | - to 0x8Dc63ce8b979627C11f5EEf673990814D4815613 should be 30 - fee 138 | 139 | ```bash 140 | > web3.fromWei(web3.eth.getBalance("0xe5379d64Cd7d2D963B03da01fB052218a9aCB0Ce"),"ether") 141 | 0 142 | > web3.fromWei(web3.eth.getBalance("0x8Dc63ce8b979627C11f5EEf673990814D4815613"),"ether") 143 | 29.999999999999979 144 | ``` 145 | ### Links 146 | - [xgo](https://github.com/karalabe/xgo) 147 | - [Cross compiling Ethereum](https://github.com/ethereum/go-ethereum/wiki/Cross-compiling-Ethereum) 148 | -------------------------------------------------------------------------------- /address.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "crypto/ecdsa" 6 | "crypto/sha256" 7 | "encoding/base64" 8 | "encoding/hex" 9 | "encoding/json" 10 | "errors" 11 | "io" 12 | "io/ioutil" 13 | "os" 14 | "strings" 15 | "time" 16 | 17 | "github.com/gocarina/gocsv" 18 | log "github.com/sirupsen/logrus" 19 | qrcode "github.com/skip2/go-qrcode" 20 | bip39 "github.com/tyler-smith/go-bip39" 21 | 22 | "github.com/btcsuite/btcd/chaincfg" 23 | "github.com/btcsuite/btcutil/hdkeychain" 24 | 25 | "github.com/ethereum/go-ethereum/accounts/keystore" 26 | "github.com/ethereum/go-ethereum/crypto" 27 | "github.com/pborman/uuid" 28 | ) 29 | 30 | // RandomPwdJSON 随机密码 31 | type RandomPwdJSON struct { 32 | Address string `json:"address"` 33 | Randompwd string `json:"randompwd"` 34 | } 35 | 36 | // FixedPwdJSON 固定密码 37 | type FixedPwdJSON struct { 38 | Address string `json:"address"` 39 | FixedPwd string `json:"fixedpwd"` 40 | } 41 | 42 | // MnemonicJSON 助记词 43 | type MnemonicJSON struct { 44 | Address string `json:"address"` 45 | Mnemonic string `json:"mnemonic"` 46 | PATH string `json:"path"` 47 | } 48 | 49 | type csvAddress struct { 50 | Address string `csv:"address"` 51 | } 52 | 53 | func createAccount(accoutDir, timeDir string) (*string, error) { 54 | // Generate a mnemonic for memorization or user-friendly seeds 55 | mnemonic, err := mnemonicFun() 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | privateKey, path, err := hdWallet(*mnemonic) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | // pristr := hex.EncodeToString(privateKey.D.Bytes()) 66 | 67 | // get the address 68 | address := crypto.PubkeyToAddress(privateKey.PublicKey).Hex() 69 | 70 | // generate first rondom password 71 | randomPwdFirst := RandStringBytesMaskImprSrc(50) 72 | 73 | // generate second rondom password 74 | randomPwdSecond := RandStringBytesMaskImprSrc(60) 75 | 76 | // save mnemonic qrcode 77 | saveAESEncryptMnemonicQrcode(address, *mnemonic, *path, accoutDir, timeDir) 78 | 79 | // save keystore to configure path 80 | saveKeystore(privateKey, randomPwdFirst, randomPwdSecond, accoutDir, timeDir) 81 | // save random pwd with address to configure path 82 | saveRandomPwd(address, randomPwdFirst, accoutDir, "random_pwd_first", timeDir) 83 | saveRandomPwd(address, randomPwdSecond, accoutDir, "random_pwd_second", timeDir) 84 | 85 | log.WithFields(log.Fields{ 86 | "Generate Ethereum account": address, 87 | "Time:": time.Now().Format("Mon Jan _2 15:04:05 2006"), 88 | }).Info("") 89 | 90 | return &address, nil 91 | } 92 | 93 | func accountAuth(randomPwdFirst, randomPwdSecond string) string { 94 | h := sha256.New() 95 | h.Write([]byte(randomPwdFirst)) 96 | h.Write([]byte(randomPwdSecond)) 97 | auth := hex.EncodeToString(h.Sum(nil)) 98 | return auth 99 | } 100 | 101 | func mnemonicFun() (*string, error) { 102 | // Generate a mnemonic for memorization or user-friendly seeds 103 | entropy, err := bip39.NewEntropy(128) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | mnemonic, err := bip39.NewMnemonic(entropy) 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | return &mnemonic, nil 114 | } 115 | 116 | func hdWallet(mnemonic string) (*ecdsa.PrivateKey, *string, error) { 117 | // Generate a Bip32 HD wallet for the mnemonic and a user supplied password 118 | seed := bip39.NewSeed(mnemonic, "") 119 | 120 | // Generate a new master node using the seed. 121 | masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams) 122 | if err != nil { 123 | return nil, nil, err 124 | } 125 | 126 | // This gives the path: m/44H 127 | acc44H, err := masterKey.Child(hdkeychain.HardenedKeyStart + 44) 128 | if err != nil { 129 | return nil, nil, err 130 | } 131 | 132 | // This gives the path: m/44H/60H 133 | acc44H60H, err := acc44H.Child(hdkeychain.HardenedKeyStart + 60) 134 | if err != nil { 135 | return nil, nil, err 136 | } 137 | 138 | // This gives the path: m/44H/60H/0H 139 | acc44H60H0H, err := acc44H60H.Child(hdkeychain.HardenedKeyStart + 0) 140 | if err != nil { 141 | return nil, nil, err 142 | } 143 | 144 | // This gives the path: m/44H/60H/0H/0 145 | acc44H60H0H0, err := acc44H60H0H.Child(0) 146 | if err != nil { 147 | return nil, nil, err 148 | } 149 | 150 | // This gives the path: m/44H/60H/0H/0/0 151 | acc44H60H0H00, err := acc44H60H0H0.Child(0) 152 | if err != nil { 153 | return nil, nil, err 154 | } 155 | 156 | btcecPrivKey, err := acc44H60H0H00.ECPrivKey() 157 | if err != nil { 158 | return nil, nil, err 159 | } 160 | 161 | privateKey := btcecPrivKey.ToECDSA() 162 | 163 | path := "m/44H/60H/0H/0/0" 164 | 165 | return privateKey, &path, nil 166 | } 167 | 168 | func saveFixedPwd(address, fixedPwd, dir string) { 169 | fixedPwdJSON := FixedPwdJSON{ 170 | address, 171 | fixedPwd, 172 | } 173 | hexFixedPwdJSON, err := json.Marshal(fixedPwdJSON) 174 | if err != nil { 175 | log.Fatalf(err.Error()) 176 | } 177 | hexFixedPwdJSON = append(hexFixedPwdJSON, '\n') 178 | fixedPwdPath, err := mkdirBySlice([]string{dir, "fixed_pwd"}) 179 | if err != nil { 180 | log.Fatalln("Could not create directory", err.Error()) 181 | } 182 | fixedPwdFile := strings.Join([]string{*fixedPwdPath, "fixedpwd.json"}, "/") 183 | if err = appenFile(fixedPwdFile, hexFixedPwdJSON, 0600); err != nil { 184 | log.Fatalln("Failed to write keyfile to", err.Error()) 185 | } 186 | } 187 | 188 | func saveRandomPwd(address, randomPwd, dir, rdname, timeDir string) { 189 | randomPwdJSON := RandomPwdJSON{ 190 | address, 191 | randomPwd, 192 | } 193 | hexRandomPwdJSON, err := json.Marshal(randomPwdJSON) 194 | if err != nil { 195 | log.Fatalf(err.Error()) 196 | } 197 | hexRandomPwdJSON = append(hexRandomPwdJSON, '\n') 198 | randomPwdPath, err := mkdirBySlice([]string{dir, rdname, timeDir}) 199 | if err != nil { 200 | log.Fatalln("Could not create directory", err.Error()) 201 | } 202 | randomPwdFile := strings.Join([]string{*randomPwdPath, "randompwd.json"}, "/") 203 | if err = appenFile(randomPwdFile, hexRandomPwdJSON, 0600); err != nil { 204 | log.Fatalln("Failed to write keyfile to", err.Error()) 205 | } 206 | } 207 | 208 | func readPwd(address, pwdType, timeDir string) (*string, error) { 209 | var ( 210 | PwdFile string 211 | ) 212 | switch pwdType { 213 | case "random_pwd_first": 214 | dir := strings.Join([]string{HomeDir(), "account", "random_pwd_first", timeDir}, "/") 215 | PwdFile = strings.Join([]string{dir, "randompwd.json"}, "/") 216 | case "random_pwd_second": 217 | dir := strings.Join([]string{HomeDir(), "account", "random_pwd_second", timeDir}, "/") 218 | PwdFile = strings.Join([]string{dir, "randompwd.json"}, "/") 219 | default: 220 | return nil, errors.New("pwdType error") 221 | } 222 | 223 | jsonFile, err := os.Open(PwdFile) 224 | if err != nil { 225 | return nil, err 226 | } 227 | defer jsonFile.Close() 228 | 229 | reader := bufio.NewReader(jsonFile) 230 | 231 | var pwd = new(string) 232 | for { 233 | line, _, err := reader.ReadLine() 234 | if err == io.EOF { 235 | break 236 | } 237 | 238 | var randompwd RandomPwdJSON 239 | json.Unmarshal(line, &randompwd) 240 | if randompwd.Address == address { 241 | pwd = &(randompwd.Randompwd) 242 | return pwd, nil 243 | } 244 | } 245 | return pwd, nil 246 | } 247 | 248 | func saveAESEncryptMnemonicQrcode(address, mnemonic, path, dir, timeStr string) { 249 | // AES encrypt key should be 16 bytes (AES-128) or 32 (AES-256). 250 | randomPwd := RandStringBytesMaskImprSrc(32) 251 | m := &MnemonicJSON{ 252 | address, 253 | mnemonic, 254 | path, 255 | } 256 | bMnemonicJSON, _ := json.Marshal(m) 257 | 258 | mNemonicCrypted, err := AesEncrypt(bMnemonicJSON, []byte(randomPwd)) 259 | if err != nil { 260 | log.Fatalln("crypted mnemonic error", err.Error()) 261 | } 262 | 263 | // save ASE 256 encode mnemonic and randomPwd(AesDecrypt key) qrcode 264 | saveAES256EncodeMnemonicQrcode(mNemonicCrypted, randomPwd, address, dir, timeStr, 512) 265 | } 266 | 267 | func saveAES256EncodeMnemonicQrcode(mNemonicCrypted []byte, key, address, dir, timeDir string, size int) { 268 | h := sha256.New() 269 | h.Write(mNemonicCrypted) 270 | mnemonicSha := base64.URLEncoding.EncodeToString(h.Sum(nil)) 271 | mnemonicScryptedStr := base64.StdEncoding.EncodeToString(mNemonicCrypted) 272 | mnemonicSha256AndAESResult := strings.Join([]string{mnemonicScryptedStr, mnemonicSha}, "") 273 | 274 | mnemonicPNGPath, err := mkdirBySlice([]string{dir, "mnemonic_qrcode", timeDir, address}) 275 | if err != nil { 276 | log.Fatalln("Could not create directory", err.Error()) 277 | } 278 | 279 | mnemonicAesDecryptPNGName := strings.Join([]string{address, "aesdecrypt_mnemonic.png"}, "_") 280 | mnemonicAesDecryptPNGFile := strings.Join([]string{*mnemonicPNGPath, mnemonicAesDecryptPNGName}, "/") 281 | if err := qrcode.WriteFile(mnemonicSha256AndAESResult, qrcode.Highest, size, mnemonicAesDecryptPNGFile); err != nil { 282 | log.Fatalln("encode encrypt qrcode error", err.Error()) 283 | } 284 | 285 | wm(mnemonicAesDecryptPNGFile, address, "aesdecrypt_mnemonic") 286 | 287 | AesDecryptKeyPNGName := strings.Join([]string{address, "aesdecrypt_key.png"}, "_") 288 | AesDecryptKeyPNGFile := strings.Join([]string{*mnemonicPNGPath, AesDecryptKeyPNGName}, "/") 289 | if err := qrcode.WriteFile(key, qrcode.Medium, size, AesDecryptKeyPNGFile); err != nil { 290 | log.Fatalln("encode key qrcode error", err.Error()) 291 | } 292 | 293 | wm(AesDecryptKeyPNGFile, address, "aesdecrypt_key") 294 | 295 | os.Remove(mnemonicAesDecryptPNGFile) 296 | os.Remove(AesDecryptKeyPNGFile) 297 | } 298 | 299 | func saveMnemonic(address, mnemonic, path, dir string) { 300 | m := &MnemonicJSON{ 301 | address, 302 | mnemonic, 303 | path, 304 | } 305 | 306 | hexMnemonicJSON, _ := json.Marshal(m) 307 | mnemonicPath, err := mkdirBySlice([]string{dir, "mnemonic"}) 308 | if err != nil { 309 | log.Fatalln("Could not create directory", err.Error()) 310 | } 311 | 312 | mnemonicName := strings.Join([]string{address, "json"}, ".") 313 | mnemonicfile := strings.Join([]string{*mnemonicPath, mnemonicName}, "/") 314 | if err := ioutil.WriteFile(mnemonicfile, hexMnemonicJSON, 0600); err != nil { 315 | log.Fatalln("Failed to write keyfile to", err.Error()) 316 | } 317 | } 318 | 319 | func saveKeystore(key *ecdsa.PrivateKey, randomPwdFirst, randomPwdSecond, dir, timeDir string) { 320 | ks := &keystore.Key{ 321 | Id: uuid.NewRandom(), 322 | Address: crypto.PubkeyToAddress(key.PublicKey), 323 | PrivateKey: key, 324 | } 325 | auth := accountAuth(randomPwdFirst, randomPwdSecond) 326 | keyjson, err := keystore.EncryptKey(ks, auth, keystore.StandardScryptN, keystore.StandardScryptP) 327 | if err != nil { 328 | log.Fatalf(err.Error()) 329 | } 330 | 331 | keystorePath, err := mkdirBySlice([]string{dir, "keystore", timeDir}) 332 | if err != nil { 333 | log.Fatalln("Could not create directory", err.Error()) 334 | } 335 | 336 | address := crypto.PubkeyToAddress(key.PublicKey).Hex() 337 | keystoreName := strings.Join([]string{address, "json"}, ".") 338 | keystorefile := strings.Join([]string{*keystorePath, keystoreName}, "/") 339 | if err := ioutil.WriteFile(keystorefile, keyjson, 0600); err != nil { 340 | log.Fatalln("Failed to write keyfile to", err.Error()) 341 | } 342 | } 343 | 344 | func readKeyStore(address, path string) ([]byte, error) { 345 | keystoreName := strings.Join([]string{address, "json"}, ".") 346 | keystorefile := strings.Join([]string{path, keystoreName}, "/") 347 | return ioutil.ReadFile(keystorefile) 348 | } 349 | 350 | func decodeKS2Key(addressHex string) (*keystore.Key, error) { 351 | timeDir, err := accountDir(addressHex) 352 | if err != nil { 353 | return nil, err 354 | } 355 | ksPath := strings.Join([]string{HomeDir(), "account", "keystore", *timeDir}, "/") 356 | keyjson, err := readKeyStore(addressHex, ksPath) 357 | if err != nil { 358 | return nil, errors.New(strings.Join([]string{"read keystore error", err.Error()}, " ")) 359 | } 360 | 361 | randomPwdFirst, err := readPwd(addressHex, "random_pwd_first", *timeDir) 362 | if err != nil { 363 | return nil, errors.New(strings.Join([]string{"read random_pwd_first error", err.Error()}, " ")) 364 | } 365 | 366 | randomPwdSecond, err := readPwd(addressHex, "random_pwd_second", *timeDir) 367 | if err != nil { 368 | return nil, errors.New(strings.Join([]string{"read random_pwd_second error", err.Error()}, " ")) 369 | } 370 | 371 | auth := accountAuth(*randomPwdFirst, *randomPwdSecond) 372 | key, err := keystore.DecryptKey(keyjson, auth) 373 | if err != nil { 374 | return nil, err 375 | } 376 | return key, nil 377 | } 378 | 379 | func export2CSV(addresses []*csvAddress, path string) { 380 | addressPath := strings.Join([]string{path, "eth_address.csv"}, "/") 381 | addressFile, err := os.OpenFile(addressPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, os.ModePerm) 382 | if err != nil { 383 | log.Fatalln(err.Error()) 384 | } 385 | defer addressFile.Close() 386 | originAddress := []*csvAddress{} 387 | if err := gocsv.UnmarshalFile(addressFile, &originAddress); err != nil { 388 | if err := gocsv.MarshalFile(&addresses, addressFile); err != nil { 389 | log.Fatalln(err.Error()) 390 | } 391 | } else { 392 | gocsv.MarshalWithoutHeaders(&addresses, addressFile) 393 | } 394 | 395 | log.WithFields(log.Fields{ 396 | "export address to file": addressFile.Name(), 397 | "Time:": time.Now().Format("Mon Jan _2 15:04:05 2006"), 398 | }).Warn() 399 | } 400 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/ethereum/go-ethereum/ethclient" 8 | log "github.com/sirupsen/logrus" 9 | 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | var ( 15 | number int 16 | node string 17 | ) 18 | 19 | // EtherScan 配置 20 | type EtherScan struct { 21 | Key string 22 | URL string 23 | } 24 | 25 | type configure struct { 26 | ElasticURL string 27 | ElasticSniff bool 28 | EthRPC string 29 | MaxBalance float64 30 | To []string 31 | NetMode string 32 | RawTx string 33 | SignedTx string 34 | DB string 35 | GethRPC string 36 | ParityRPC string 37 | EtherscanRPC string 38 | } 39 | 40 | // rootCmd represents the base command when called without any subcommands 41 | var rootCmd = &cobra.Command{ 42 | Use: "ethereum-service", 43 | Short: "Generate Ethereum account and sign tx", 44 | } 45 | 46 | // apiCmd represents the chain command 47 | var genAccountCmd = &cobra.Command{ 48 | Use: "genaccount", 49 | Short: "Generate ethereum account", 50 | Run: func(cmd *cobra.Command, args []string) { 51 | timeFormat := time.Now().Format("2006-01-02_15-04-05") 52 | timeDir := strings.Join([]string{"version_1", timeFormat}, "_") 53 | 54 | accountDir, err := mkdirBySlice([]string{HomeDir(), "account"}) 55 | if err != nil { 56 | log.Fatalln("Fail to create account directory") 57 | } 58 | addresses := []*csvAddress{} 59 | for index := 0; index < number; index++ { 60 | address, err := createAccount(*accountDir, timeDir) 61 | if err != nil { 62 | log.Fatalln(err.Error()) 63 | } 64 | addresses = append(addresses, &csvAddress{Address: *address}) 65 | } 66 | export2CSV(addresses, *accountDir) 67 | }, 68 | } 69 | 70 | var syncCmd = &cobra.Command{ 71 | Use: "sync", 72 | Short: "sync chain data to elasticsearch", 73 | Run: func(cmd *cobra.Command, args []string) { 74 | config.InitConfig() 75 | sync() 76 | }, 77 | } 78 | 79 | var subscribeNewBlockCmd = &cobra.Command{ 80 | Use: "sub", 81 | Short: "subscribe new block event", 82 | Run: func(cmd *cobra.Command, args []string) { 83 | config.InitConfig() 84 | subNewBlockCmd() 85 | }, 86 | } 87 | 88 | var constructCmd = &cobra.Command{ 89 | Use: "construct", 90 | Short: "construct transactio", 91 | Run: func(cmd *cobra.Command, args []string) { 92 | config.InitConfig() 93 | if !Contains([]string{"geth", "parity", "etherscan"}, node) { 94 | log.Errorln("Only support geth, parity, etherscan") 95 | return 96 | } 97 | constructTxCmd() 98 | }, 99 | } 100 | 101 | var signCmd = &cobra.Command{ 102 | Use: "sign", 103 | Short: "sigin transactio", 104 | Run: func(cmd *cobra.Command, args []string) { 105 | config.InitConfig() 106 | signTxCmd() 107 | }, 108 | } 109 | 110 | var sendCmd = &cobra.Command{ 111 | Use: "send", 112 | Short: "broadcast signex transaction to ethereum network", 113 | Run: func(cmd *cobra.Command, args []string) { 114 | config.InitConfig() 115 | nodeClient, err := ethclient.Dial(config.EthRPC) 116 | if err != nil { 117 | log.Fatalln(err.Error()) 118 | } 119 | sendTxCmd(nodeClient) 120 | }, 121 | } 122 | 123 | // Execute 命令行入口 124 | func Execute() { 125 | if err := rootCmd.Execute(); err != nil { 126 | log.Fatalf(err.Error()) 127 | } 128 | } 129 | 130 | func (conf *configure) InitConfig() { 131 | viper.SetConfigType("yaml") 132 | viper.AddConfigPath(HomeDir()) 133 | viper.SetConfigName("ethereum-cold-wallet") 134 | viper.AutomaticEnv() // read in environment variables that match 135 | 136 | // If a config file is found, read it in. 137 | err := viper.ReadInConfig() 138 | if err == nil { 139 | log.WithFields(log.Fields{ 140 | "Using Configure file": viper.ConfigFileUsed(), 141 | "Time:": time.Now().Format("Mon Jan _2 15:04:05 2006"), 142 | }).Info() 143 | } else { 144 | log.Fatal("Error: ethereum-cold-wallet.yml not found in: ", HomeDir()) 145 | } 146 | 147 | for key, value := range viper.AllSettings() { 148 | switch key { 149 | case "elastic_url": 150 | conf.ElasticURL = value.(string) 151 | case "elastic_sniff": 152 | conf.ElasticSniff = value.(bool) 153 | case "eth_rpc": 154 | conf.EthRPC = value.(string) 155 | case "max_balance": 156 | conf.MaxBalance = value.(float64) 157 | case "to": 158 | conf.To = viper.GetStringSlice(key) 159 | case "net_mode": 160 | conf.NetMode = value.(string) 161 | case "raw_tx_path": 162 | conf.RawTx = value.(string) 163 | case "signed_tx_path": 164 | conf.SignedTx = value.(string) 165 | case "db_mysql": 166 | conf.DB = value.(string) 167 | case "geth_rpc": 168 | conf.GethRPC = value.(string) 169 | case "parity_rpc": 170 | conf.ParityRPC = value.(string) 171 | case "etherscan_rpc": 172 | subv := viper.Sub("etherscan_rpc") 173 | for subKey, subValue := range subv.AllSettings() { 174 | switch subKey { 175 | case "key": 176 | etherscan.Key = subValue.(string) 177 | case "url": 178 | etherscan.URL = subValue.(string) 179 | } 180 | } 181 | } 182 | } 183 | } 184 | 185 | func init() { 186 | config = new(configure) 187 | etherscan = new(EtherScan) 188 | initLogger() 189 | rootCmd.AddCommand(genAccountCmd) 190 | rootCmd.AddCommand(subscribeNewBlockCmd) 191 | rootCmd.AddCommand(constructCmd) 192 | rootCmd.AddCommand(signCmd) 193 | rootCmd.AddCommand(sendCmd) 194 | // rootCmd.AddCommand(syncCmd) 195 | genAccountCmd.Flags().IntVarP(&number, "number", "n", 10, "Generate ethereum accounts") 196 | genAccountCmd.MarkFlagRequired("number") 197 | 198 | constructCmd.Flags().StringVarP(&node, "node", "n", "parity", "Ethereum node type, support geth, parity, etherscan") 199 | constructCmd.MarkFlagRequired("node") 200 | } 201 | -------------------------------------------------------------------------------- /db.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "math/big" 7 | "os" 8 | "strings" 9 | 10 | "github.com/gocarina/gocsv" 11 | "github.com/jinzhu/gorm" 12 | _ "github.com/jinzhu/gorm/dialects/mysql" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // SubAddress 监听地址 17 | type SubAddress struct { 18 | gorm.Model 19 | Address string `gorm:"type:varchar(42);not null;unique_index"` 20 | } 21 | 22 | type ormBbAlias struct { 23 | *gorm.DB 24 | } 25 | 26 | func dbConn() *gorm.DB { 27 | w := bytes.Buffer{} 28 | w.WriteString(config.DB) 29 | w.WriteString("?charset=utf8&parseTime=True") 30 | dbInfo := w.String() 31 | db, err := gorm.Open("mysql", dbInfo) 32 | if err != nil { 33 | panic(err) 34 | } 35 | return db 36 | } 37 | 38 | // DBMigrate 数据库表迁移 39 | func (db ormBbAlias) DBMigrate() { 40 | db.AutoMigrate(&SubAddress{}) 41 | } 42 | 43 | func (db ormBbAlias) csv2db() { 44 | addressPath := strings.Join([]string{HomeDir(), "eth_address.csv"}, "/") 45 | addressFile, err := os.OpenFile(addressPath, os.O_RDWR, os.ModePerm) 46 | if err != nil { 47 | log.Fatalln(err.Error()) 48 | } 49 | defer addressFile.Close() 50 | 51 | addresses := []*csvAddress{} 52 | if err := gocsv.UnmarshalFile(addressFile, &addresses); err != nil { 53 | log.Fatalln(err.Error()) 54 | } 55 | 56 | for _, address := range addresses { 57 | subAddress := SubAddress{ 58 | Address: address.Address, 59 | } 60 | db.Where(csvAddress{Address: address.Address}).Attrs(csvAddress{Address: address.Address}).FirstOrCreate(&subAddress) 61 | } 62 | log.Info("csv2db done") 63 | } 64 | 65 | func (db ormBbAlias) constructTxField(address string) (*string, *big.Int, *uint64, *big.Int, error) { 66 | subAddress, err := db.getSubAddress(address) 67 | if err != nil { 68 | return nil, nil, nil, nil, err 69 | } 70 | 71 | switch node { 72 | case "geth": 73 | balance, nonce, gasPrice, err := nodeConstructTxField("geth", *subAddress) 74 | if err != nil { 75 | return nil, nil, nil, nil, err 76 | } 77 | return subAddress, balance, nonce, gasPrice, nil 78 | case "parity": 79 | balance, nonce, gasPrice, err := nodeConstructTxField("parity", *subAddress) 80 | if err != nil { 81 | return nil, nil, nil, nil, err 82 | } 83 | return subAddress, balance, nonce, gasPrice, nil 84 | case "etherscan": 85 | balance, nonce, gasPrice, err := etherscan.etherscanConstructTxField(*subAddress) 86 | if err != nil { 87 | return nil, nil, nil, nil, err 88 | } 89 | return subAddress, balance, nonce, gasPrice, nil 90 | default: 91 | return nil, nil, nil, nil, errors.New("Only support geth, parity, etherscan") 92 | } 93 | } 94 | 95 | func (db ormBbAlias) getSubAddress(address string) (*string, error) { 96 | var subAddress SubAddress 97 | db.Where("address = ?", address).First(&subAddress) 98 | if strings.Compare(strings.ToLower(subAddress.Address), strings.ToLower(address)) != 0 { 99 | return nil, errors.New(strings.Join([]string{address, "not found in db"}, " ")) 100 | } 101 | return &(subAddress.Address), nil 102 | } 103 | -------------------------------------------------------------------------------- /ethereum-cold-wallet.yml.example: -------------------------------------------------------------------------------- 1 | elastic_url: "http://host:port" 2 | elastic_sniff: false 3 | eth_rpc: "ws://127.0.0.1:8546" 4 | geth_rpc: "ws://127.0.0.1:8546" 5 | parity_rpc: "ws://host:port" 6 | etherscan_rpc: 7 | key: "" 8 | url: "https://api.etherscan.io/api" 9 | net_mode: "privatenet" 10 | db_mysql: "root:hww8773084123@tcp(127.0.0.1:32768)/eth_service" 11 | max_balance: 20.0 12 | to: ["0x0cEabC861BeEBE8e57a19C26586C14c6f5E7B174", "0x8DeFdA5f8143dfA41DdbcFa305230e35564B3665"] 13 | raw_tx_path: "tx/unsign" 14 | signed_tx_path: "tx/signed" 15 | -------------------------------------------------------------------------------- /etherscan.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "math/big" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/parnurzeal/gorequest" 11 | ) 12 | 13 | var ( 14 | // APIKEY ethereum oauth token 15 | APIKEY string 16 | request *gorequest.SuperAgent 17 | ) 18 | 19 | // AccountRespBody EtherScan Response body 20 | type AccountRespBody struct { 21 | Status string `json:"status"` 22 | Message string `json:"message"` 23 | Result string `json:"result"` 24 | } 25 | 26 | // ProxyRespBody EtherScan Response body 27 | type ProxyRespBody struct { 28 | JSONRPC string `json:"jsonrpc"` 29 | ID int `json:"id"` 30 | Result string `json:"result"` 31 | } 32 | 33 | func (es EtherScan) getBalance(address string) (*big.Int, error) { 34 | resp, body, err := request.Get(etherscan.URL).Query(map[string]interface{}{ 35 | "module": "account", 36 | "action": "balance", 37 | "address": address, 38 | "tag": "latest", 39 | "apikey": APIKEY, 40 | }).End() 41 | 42 | if err != nil { 43 | return nil, errors.New(strings.Join([]string{"etherscan: get balance error:", address, err[0].Error()}, " ")) 44 | } 45 | if handleStatus(resp) { 46 | var ( 47 | respBody = new(AccountRespBody) 48 | balance = new(big.Int) 49 | ) 50 | if err := json.Unmarshal([]byte(body), respBody); err != nil { 51 | return nil, errors.New("etherscan getBalance Unmarshal error") 52 | } 53 | balance.SetString(respBody.Result, 10) 54 | return balance, nil 55 | } 56 | return nil, errors.New("etherscan get balance error") 57 | } 58 | 59 | func (es EtherScan) getGasPrice() (*big.Int, error) { 60 | resp, body, err := request.Get(etherscan.URL).Query(map[string]interface{}{ 61 | "module": "proxy", 62 | "action": "eth_gasPrice", 63 | "apikey": APIKEY, 64 | }).End() 65 | 66 | if err != nil { 67 | return nil, errors.New(strings.Join([]string{"etherscan: get balance error:", err[0].Error()}, " ")) 68 | } 69 | if handleStatus(resp) { 70 | var ( 71 | respBody = new(ProxyRespBody) 72 | gasPrice = new(big.Int) 73 | ) 74 | if err := json.Unmarshal([]byte(body), respBody); err != nil { 75 | return nil, errors.New("etherscan getBalance Unmarshal error") 76 | } 77 | resultWithoutHex := strings.Replace(respBody.Result, "0x", "", -1) 78 | gasPrice.SetString(resultWithoutHex, 10) 79 | return gasPrice, nil 80 | } 81 | return nil, errors.New("etherscan get gasPrice error") 82 | } 83 | 84 | func (es EtherScan) getAccountNonce(address string) (*uint64, error) { 85 | resp, body, err := request.Get(etherscan.URL).Query(map[string]interface{}{ 86 | "module": "proxy", 87 | "action": "eth_getTransactionCount", 88 | "address": address, 89 | "tag": "latest", 90 | "apikey": APIKEY, 91 | }).End() 92 | 93 | if err != nil { 94 | return nil, errors.New(strings.Join([]string{"etherscan: get account nonce error:", address, err[0].Error()}, " ")) 95 | } 96 | if handleStatus(resp) { 97 | var respBody = new(ProxyRespBody) 98 | if err := json.Unmarshal([]byte(body), respBody); err != nil { 99 | return nil, errors.New("etherscan getBalance Unmarshal error") 100 | } 101 | 102 | nonce, _ := strconv.ParseUint(strings.Replace(respBody.Result, "0x", "", -1), 16, 64) 103 | return &nonce, nil 104 | } 105 | return nil, errors.New("etherscan get account nonce error") 106 | } 107 | 108 | func (es EtherScan) etherscanConstructTxField(address string) (*big.Int, *uint64, *big.Int, error) { 109 | balance, err := es.getBalance(address) 110 | if err != nil { 111 | return nil, nil, nil, err 112 | } 113 | 114 | err = balanceIsLessThanConfig(address, balance) 115 | if err != nil { 116 | return nil, nil, nil, err 117 | } 118 | 119 | accountNonce, err := es.getAccountNonce(address) 120 | if err != nil { 121 | return nil, nil, nil, errors.New(strings.Join([]string{"etherscan: get account nonce error:", address, err.Error()}, " ")) 122 | } 123 | 124 | gasPrice, err := es.getGasPrice() 125 | if err != nil { 126 | return nil, nil, nil, err 127 | } 128 | 129 | return balance, accountNonce, gasPrice, nil 130 | } 131 | 132 | func handleStatus(resp gorequest.Response) bool { 133 | if resp.StatusCode == 200 { 134 | return true 135 | } 136 | return false 137 | } 138 | 139 | func init() { 140 | APIKEY = etherscan.Key 141 | request = gorequest.New() 142 | } 143 | -------------------------------------------------------------------------------- /image.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "path" 6 | "strings" 7 | 8 | "os" 9 | 10 | "math" 11 | "math/rand" 12 | "time" 13 | 14 | "image" 15 | "image/color" 16 | "image/draw" 17 | "image/png" 18 | 19 | "gonum.org/v1/plot/vg" 20 | "gonum.org/v1/plot/vg/vgimg" 21 | // "gonum.org/v1/plot/plotter" 22 | log "github.com/sirupsen/logrus" 23 | ) 24 | 25 | func init() { 26 | rand.Seed(time.Now().UnixNano()) 27 | } 28 | 29 | // WaterMark for adding a watermark on the image 30 | func WaterMark(img image.Image, address, qrcodeType string) (image.Image, error) { 31 | // image's length to canvas's length 32 | bounds := img.Bounds() 33 | w := vg.Length(bounds.Max.X) * vg.Inch / vgimg.DefaultDPI 34 | h := vg.Length(bounds.Max.Y) * vg.Inch / vgimg.DefaultDPI 35 | diagonal := vg.Length(math.Sqrt(float64(w*w + h*h))) 36 | // create a canvas, which width and height are diagonal 37 | c := vgimg.New(diagonal, diagonal/2) 38 | 39 | // make a fontStyle, which width is vg.Inch * 0.7 40 | fontStyle, _ := vg.MakeFont("Courier", diagonal/42) 41 | 42 | // set the color of markText 43 | c.SetColor(color.RGBA{0, 0, 0, 200}) 44 | c.FillString(fontStyle, vg.Point{X: vg.Length(bounds.Min.X + 10), Y: diagonal/2 - 20}, "Ethereum Address: ") 45 | c.FillString(fontStyle, vg.Point{X: vg.Length(bounds.Min.X + 10), Y: diagonal/2 - 35}, address) 46 | 47 | c.FillString(fontStyle, vg.Point{X: vg.Length(bounds.Min.X + 10), Y: diagonal/2 - 60}, strings.Join([]string{"Generate Time:", time.Now().Format("2006-01-02 15:04:05")}, " ")) 48 | c.FillString(fontStyle, vg.Point{X: vg.Length(bounds.Min.X + 10), Y: diagonal/2 - 80}, strings.Join([]string{"qrcode type:", qrcodeType}, " ")) 49 | 50 | // canvas writeto jpeg 51 | // canvas.img is private 52 | // so use a buffer to transfer 53 | jc := vgimg.PngCanvas{Canvas: c} 54 | buff := new(bytes.Buffer) 55 | jc.WriteTo(buff) 56 | img, _, err := image.Decode(buff) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | // get the center point of the image 62 | ctp := int(diagonal * vgimg.DefaultDPI / vg.Inch / 2) 63 | 64 | // cutout the marked image 65 | size := bounds.Size() 66 | bounds = image.Rect(ctp-size.X/2, ctp-size.Y/2, ctp+size.X/2, ctp+size.Y/2) 67 | rv := image.NewRGBA(bounds) 68 | draw.Draw(rv, bounds, img, image.Point{0, 0}, draw.Src) 69 | return rv, nil 70 | } 71 | 72 | // MarkingPicture for marking picture with text 73 | func MarkingPicture(filepath, address, qrcodeType string) (image.Image, error) { 74 | f, err := os.Open(filepath) 75 | if err != nil { 76 | return nil, err 77 | } 78 | defer f.Close() 79 | 80 | img, _, err := image.Decode(f) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | img, err = WaterMark(img, address, qrcodeType) 86 | if err != nil { 87 | return nil, err 88 | } 89 | return img, nil 90 | } 91 | 92 | func wm(target, address, qrcodeType string) { 93 | img, err := MarkingPicture(target, address, qrcodeType) 94 | if err != nil { 95 | log.Fatalln(err.Error()) 96 | } 97 | 98 | srcFile, err := os.Open(target) 99 | if err != nil { 100 | log.Fatalln(err.Error()) 101 | } 102 | 103 | srcImage, _, err := image.Decode(srcFile) 104 | if err != nil { 105 | log.Fatalln(err.Error()) 106 | } 107 | 108 | sb := srcImage.Bounds() 109 | r2 := image.Rectangle{} 110 | r2.Min.X = 0 111 | r2.Min.Y = sb.Max.Y 112 | r2.Max.X = sb.Max.X 113 | r2.Max.Y = sb.Max.Y * 2 114 | r := image.Rectangle{image.Point{0, 0}, r2.Max} 115 | rgba := image.NewRGBA(r) 116 | 117 | draw.Draw(rgba, sb, srcImage, image.Point{0, 0}, draw.Src) 118 | draw.Draw(rgba, r2, img, image.Point{img.Bounds().Min.X, img.Bounds().Min.Y - 10}, draw.Src) 119 | 120 | ext := path.Ext(target) 121 | base := strings.Split(path.Base(target), ".")[0] + "_marked" 122 | wmFileName := strings.Join([]string{base, ext}, "") 123 | f, err := os.Create(strings.Join([]string{path.Dir(target), wmFileName}, "/")) 124 | if err != nil { 125 | log.Fatalln(err.Error()) 126 | } 127 | png.Encode(f, rgba) 128 | } 129 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var ( 4 | config *configure 5 | etherscan *EtherScan 6 | ) 7 | 8 | func main() { 9 | Execute() 10 | } 11 | -------------------------------------------------------------------------------- /sub.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "math/big" 7 | "strings" 8 | 9 | log "github.com/sirupsen/logrus" 10 | 11 | "github.com/ethereum/go-ethereum/core/types" 12 | "github.com/ethereum/go-ethereum/ethclient" 13 | ) 14 | 15 | func subNewBlockCmd() { 16 | ormDB := ormBbAlias{dbConn()} 17 | ormDB.DBMigrate() 18 | defer ormDB.Close() 19 | 20 | ctx := context.Background() 21 | nodeClient, err := ethclient.Dial(config.EthRPC) 22 | if err != nil { 23 | log.Fatalln(err.Error()) 24 | } 25 | 26 | blockCh := make(chan *types.Header) 27 | sub, err := nodeClient.SubscribeNewHead(ctx, blockCh) 28 | 29 | if err != nil { 30 | log.Error(err.Error()) 31 | } 32 | 33 | var ( 34 | // maintain orderHeight and increase 1 each subscribe callback, because head.number would jump blocks 35 | orderHeight = new(big.Int) 36 | ) 37 | for { 38 | select { 39 | case err := <-sub.Err(): 40 | log.Fatalln(err.Error()) 41 | case head := <-blockCh: 42 | ordertmp, err := subHandle(orderHeight, head, nodeClient) 43 | if err != nil { 44 | log.Errorln(err.Error()) 45 | } 46 | orderHeight = ordertmp 47 | } 48 | } 49 | } 50 | 51 | func subHandle(orderHeight *big.Int, head *types.Header, nodeClient *ethclient.Client) (*big.Int, error) { 52 | ctx := context.Background() 53 | number := head.Number 54 | originBlock, err := nodeClient.BlockByNumber(ctx, number) 55 | if err != nil { 56 | return nil, errors.New(strings.Join([]string{"get origin block error, height:", number.String(), err.Error()}, " ")) 57 | } 58 | 59 | number.Sub(number, big.NewInt(1)) 60 | parentBlock, err := nodeClient.BlockByNumber(ctx, number) 61 | if err != nil { 62 | return nil, errors.New(strings.Join([]string{"get parent block error", number.String(), err.Error()}, " ")) 63 | } 64 | 65 | if originBlock.ParentHash().Hex() != parentBlock.Hash().Hex() { 66 | return nil, errors.New(strings.Join([]string{"uncle block, stable block's height:", originBlock.Number().String()}, " ")) 67 | } 68 | 69 | if orderHeight.Cmp(big.NewInt(0)) == 0 { 70 | orderHeight = originBlock.Number() 71 | } 72 | 73 | var pushJumpBlock string 74 | log.Infoln("sub message coming,", "order height:", orderHeight.Int64(), "sub block height:", originBlock.Number().Int64()) 75 | for blockNumber := orderHeight.Int64(); blockNumber <= originBlock.Number().Int64(); blockNumber++ { 76 | block, err := nodeClient.BlockByNumber(ctx, big.NewInt(blockNumber)) 77 | if err != nil { 78 | log.Warnln("Get block error, height:", blockNumber) 79 | continue 80 | } 81 | 82 | if blockNumber < originBlock.Number().Int64() { 83 | pushJumpBlock = "jump" 84 | } else { 85 | pushJumpBlock = "" 86 | } 87 | log.Infoln("New", pushJumpBlock, "block, Height:", block.Number().String(), "blockHash:", block.Hash().Hex()) 88 | iteratorBlockTx(block, nodeClient) 89 | orderHeight.Add(orderHeight, big.NewInt(1)) 90 | } 91 | return orderHeight, nil 92 | } 93 | 94 | func iteratorBlockTx(block *types.Block, nodeClient *ethclient.Client) { 95 | txs := block.Transactions() 96 | for _, tx := range txs { 97 | var to string 98 | pto := tx.To() 99 | 100 | // contract creation transaction, to field is empty 101 | if pto != nil { 102 | to = (*pto).Hex() 103 | } else { 104 | continue 105 | } 106 | log.Infoln(to) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /sync.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/big" 7 | 8 | "github.com/ethereum/go-ethereum/common" 9 | "github.com/ethereum/go-ethereum/core/types" 10 | "github.com/ethereum/go-ethereum/ethclient" 11 | "github.com/olivere/elastic" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | func sync() { 16 | ctx := context.Background() 17 | nodeClient, err := ethclient.Dial(config.EthRPC) 18 | if err != nil { 19 | fmt.Println(config.EthRPC) 20 | log.Fatalln(err.Error()) 21 | } 22 | 23 | // esClient, err := elastic.NewClient(elastic.SetURL(config.ElasticURL), elastic.SetSniff(config.ElasticSniff)) 24 | // if err != nil { 25 | // log.Fatalln(err.Error()) 26 | // } 27 | // 28 | // indices := []string{"esblock", "estx", "esaccount", "escontract"} 29 | // for _, index := range indices { 30 | // switch index { 31 | // case "esblock": 32 | // esIndex(ctx, esClient, index, blockMapping) 33 | // case "estx": 34 | // esIndex(ctx, esClient, index, txMapping) 35 | // case "esaccount": 36 | // case "escontract": 37 | // esIndex(ctx, esClient, index, contractMapping) 38 | // } 39 | // } 40 | 41 | block, err := nodeClient.BlockByNumber(ctx, big.NewInt(4692053)) 42 | if err != nil { 43 | log.Fatalln(err.Error()) 44 | } 45 | 46 | // blockParams := esBlockFunc(block) 47 | 48 | // esClient.Index().Index("esblock").Type("block").Id(block.Number().String()).BodyJson(blockParams).Do(ctx) 49 | txs := block.Transactions() 50 | for _, tx := range txs { 51 | fmt.Println("tx", tx.Hash().Hex()) 52 | fmt.Println("to", tx.To()) 53 | } 54 | d, err := nodeClient.BalanceAt(ctx, common.HexToAddress("0xd24400ae8BfEBb18cA49Be86258a3C749cf46853"), nil) 55 | if err != nil { 56 | log.Fatalln(err.Error()) 57 | } 58 | fmt.Println(d) 59 | } 60 | 61 | func esIndex(ctx context.Context, client *elastic.Client, index, mapping string) { 62 | exists, err := client.IndexExists(index).Do(ctx) 63 | if err != nil { 64 | log.Fatalf(err.Error()) 65 | } 66 | 67 | if !exists { 68 | result, err := client.CreateIndex(index).BodyString(mapping).Do(ctx) 69 | if err != nil { 70 | log.Fatalf(err.Error()) 71 | } 72 | if !result.Acknowledged { 73 | log.Fatalf("create index faild") 74 | } 75 | 76 | } 77 | } 78 | 79 | func esBlockFunc(block *types.Block) interface{} { 80 | rawTxs := block.Transactions() 81 | var txs []string 82 | for _, tx := range rawTxs { 83 | txs = append(txs, tx.Hash().Hex()) 84 | } 85 | 86 | b := map[string]interface{}{ 87 | "height": block.Header().Number, 88 | "hash": block.Hash().Hex(), 89 | "time": block.Time().String(), 90 | "parenthash": block.ParentHash().Hex(), 91 | "sha3uncles": block.UncleHash().Hex(), 92 | "miner": block.Coinbase().Hex(), 93 | "difficulty": block.Difficulty(), 94 | "size": float64(block.Size()), 95 | "gasused": block.GasUsed(), 96 | "gaslimit": block.GasLimit(), 97 | "nonce": block.Nonce(), 98 | "txs": txs, 99 | } 100 | return b 101 | } 102 | 103 | func esTxFunc(from, to, bhash, thash string, value big.Int) interface{} { 104 | t := map[string]interface{}{ 105 | "thash": thash, 106 | "bhash": bhash, 107 | "from": from, 108 | "to": to, 109 | "value": value, 110 | } 111 | return t 112 | } 113 | 114 | type esBlock struct { 115 | Height big.Int `json:"height"` 116 | Hash string `json:"hash"` 117 | Time string `json:"time"` 118 | Sha3Uncles string `json:"sha3uncles"` 119 | Miner string `json:"miner"` 120 | Difficulty big.Int `json:"difficulty"` 121 | Size float64 `json:"size"` 122 | GasLimit uint64 `json:"gaslimit"` 123 | GasUsed uint64 `json:"gasused"` 124 | Nonce uint64 `json:"nonce"` 125 | Txs []string `json:"txs"` 126 | } 127 | 128 | type esTx struct { 129 | THash string `json:"thash"` 130 | BHash string `json:"bhash"` 131 | From string `json:"from"` 132 | To string `json:"to"` 133 | Value big.Int `json:"value"` 134 | } 135 | 136 | type esContract struct { 137 | Owner string `json:"owner"` 138 | Tx string `json:"tx"` 139 | ABI string `json:"abi"` 140 | } 141 | 142 | type esSubAddress struct { 143 | Address string `json:"address"` 144 | } 145 | 146 | func findOrCreateFromSubAddress(ctx context.Context, esClient *elastic.Client, address *csvAddress) { 147 | q := elastic.NewBoolQuery() 148 | q = q.Must(elastic.NewTermQuery("address", address.Address)) 149 | searchResult, _ := esClient.Search().Index("eth_sub_address").Type("sub_address").Query(q).Do(ctx) 150 | if len(searchResult.Hits.Hits) < 1 { 151 | var newSubAddress = new(esSubAddress) 152 | newSubAddress.Address = address.Address 153 | esClient.Index().Index("eth_sub_address").Type("sub_address").BodyJson(newSubAddress).Refresh("true").Do(ctx) 154 | } 155 | } 156 | 157 | const blockMapping = ` 158 | { 159 | "settings": { 160 | "number_of_shards": 1, 161 | "number_of_replicas": 0 162 | }, 163 | "mappings": { 164 | "block": { 165 | "properties": { 166 | "hash": { 167 | "type": "keyword" 168 | }, 169 | "size": { 170 | "type": "integer" 171 | }, 172 | "height": { 173 | "type": "long" 174 | }, 175 | "sha3uncles": { 176 | "type": "text" 177 | }, 178 | "time": { 179 | "type": "long" 180 | }, 181 | "miner": { 182 | "type": "text" 183 | }, 184 | "nonce": { 185 | "type": "long" 186 | }, 187 | "difficulty": { 188 | "type": "long" 189 | }, 190 | "size": { 191 | "type": "double" 192 | }, 193 | "size": { 194 | "type": "double" 195 | }, 196 | "gaslimit": { 197 | "type": "long" 198 | }, 199 | "gasused": { 200 | "type": "long" 201 | }, 202 | "txs": { 203 | "type":"keyword" 204 | } 205 | } 206 | } 207 | } 208 | }` 209 | 210 | const txMapping = ` 211 | { 212 | "settings": { 213 | "number_of_shards": 1, 214 | "number_of_replicas": 0 215 | }, 216 | "mappings": { 217 | "tx": { 218 | "properties": { 219 | "thash": { 220 | "type": "keyword" 221 | }, 222 | "bhash": { 223 | "type": "keyword" 224 | }, 225 | "from": { 226 | "type": "keyword" 227 | }, 228 | "to": { 229 | "type": "keyword" 230 | }, 231 | "value": { 232 | "type": "double" 233 | }, 234 | "input": { 235 | "type": "text" 236 | } 237 | } 238 | } 239 | } 240 | }` 241 | 242 | const contractMapping = ` 243 | { 244 | "settings": { 245 | "number_of_shards": 1, 246 | "number_of_replicas": 0 247 | }, 248 | "mappings": { 249 | "contract": { 250 | "properties": { 251 | "owner": { 252 | "type": "keyword" 253 | }, 254 | "tx": { 255 | "type": "text" 256 | }, 257 | "abi": { 258 | "type": "text" 259 | } 260 | } 261 | } 262 | } 263 | }` 264 | 265 | const subAddressMapping = ` 266 | { 267 | "settings": { 268 | "number_of_shards": 1, 269 | "number_of_replicas": 0 270 | }, 271 | "mappings": { 272 | "sub_address": { 273 | "properties": { 274 | "address": { 275 | "type": "keyword" 276 | } 277 | } 278 | } 279 | } 280 | }` 281 | -------------------------------------------------------------------------------- /tx.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "io/ioutil" 9 | "math/big" 10 | "strings" 11 | 12 | log "github.com/sirupsen/logrus" 13 | 14 | "github.com/ethereum/go-ethereum/common" 15 | "github.com/ethereum/go-ethereum/common/hexutil" 16 | "github.com/ethereum/go-ethereum/core/types" 17 | "github.com/ethereum/go-ethereum/ethclient" 18 | "github.com/ethereum/go-ethereum/rlp" 19 | ) 20 | 21 | // Tx 交易结构体 22 | type Tx struct { 23 | From string `json:"from"` 24 | To string `json:"to"` 25 | TxHex string `json:"txhex"` 26 | Value big.Int `json:"value"` 27 | Nonce uint64 `json:"nonce"` 28 | Hash string `json:"hash"` 29 | } 30 | 31 | func exportHexTx(from, to, txHex, hash string, value *big.Int, nonce *uint64, signed bool) error { 32 | tx := &Tx{ 33 | From: from, 34 | To: to, 35 | TxHex: txHex, 36 | Value: *value, 37 | Nonce: *nonce, 38 | Hash: hash, 39 | } 40 | 41 | bTx, err := json.Marshal(tx) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | var configurePath, txFileName string 47 | if signed { 48 | configurePath = config.SignedTx 49 | txFileName = strings.Join([]string{"signed_from", from, "json"}, ".") 50 | } else { 51 | configurePath = config.RawTx 52 | txFileName = strings.Join([]string{"unsign_from", from, "json"}, ".") 53 | } 54 | TxPath, err := mkdirBySlice([]string{HomeDir(), configurePath}) 55 | if err != nil { 56 | return errors.New(strings.Join([]string{"Could not create directory", err.Error()}, " ")) 57 | } 58 | 59 | txfile := strings.Join([]string{*TxPath, txFileName}, "/") 60 | if err := ioutil.WriteFile(txfile, bTx, 0600); err != nil { 61 | return errors.New(strings.Join([]string{"Failed to write tx to", err.Error()}, " ")) 62 | } 63 | log.Infoln("Exported HexTx to", txfile) 64 | return nil 65 | } 66 | 67 | func constructTxCmd() { 68 | ormDB := ormBbAlias{dbConn()} 69 | ormDB.DBMigrate() 70 | defer ormDB.Close() 71 | ormDB.csv2db() 72 | 73 | var subAddresses []*SubAddress 74 | ormDB.Find(&subAddresses) 75 | for _, subaddress := range subAddresses { 76 | from, balance, pendingNonceAt, gasPrice, err := ormDB.constructTxField(subaddress.Address) 77 | if err != nil { 78 | log.Warnln(err.Error()) 79 | continue 80 | } 81 | 82 | to := randomPickFromSlice(config.To) 83 | if err := applyWithdrawAndConstructRawTx(balance, gasPrice, pendingNonceAt, *from, to); err != nil { 84 | log.Warnln(err.Error()) 85 | } 86 | } 87 | } 88 | 89 | func applyWithdrawAndConstructRawTx(balance, gasPrice *big.Int, nonce *uint64, from, to string) error { 90 | if err := balanceIsLessThanConfig(from, balance); err != nil { 91 | return err 92 | } 93 | 94 | fromHex, toHex, rawTxHex, txHashHex, value, err := constructTx(*nonce, balance, gasPrice, from, to) 95 | if err != nil { 96 | return errors.New(strings.Join([]string{"constructTx error", err.Error()}, " ")) 97 | } 98 | if err := exportHexTx(*fromHex, *toHex, *rawTxHex, *txHashHex, value, nonce, false); err != nil { 99 | return errors.New(strings.Join([]string{"sub address:", from, "hased applied withdraw, but fail to export rawTxHex to ", config.RawTx, err.Error()}, " ")) 100 | } 101 | return nil 102 | } 103 | 104 | func constructTx(nonce uint64, balance, gasPrice *big.Int, hexAddressFrom, hexAddressTo string) (*string, *string, *string, *string, *big.Int, error) { 105 | gasLimit := uint64(21000) // in units 106 | 107 | if !common.IsHexAddress(hexAddressTo) { 108 | return nil, nil, nil, nil, nil, errors.New(strings.Join([]string{hexAddressTo, "invalidate"}, " ")) 109 | } 110 | 111 | var ( 112 | txFee = new(big.Int) 113 | value = new(big.Int) 114 | ) 115 | 116 | txFee = txFee.Mul(gasPrice, big.NewInt(int64(gasLimit))) 117 | value = value.Sub(balance, txFee) 118 | 119 | tx := types.NewTransaction(nonce, common.HexToAddress(hexAddressTo), value, gasLimit, gasPrice, nil) 120 | rawTxHex, err := encodeTx(tx) 121 | if err != nil { 122 | return nil, nil, nil, nil, nil, errors.New(strings.Join([]string{"encode raw tx error", err.Error()}, " ")) 123 | } 124 | txHashHex := tx.Hash().Hex() 125 | return &hexAddressFrom, &hexAddressTo, rawTxHex, &txHashHex, value, nil 126 | } 127 | 128 | func nodeConstructTxField(node, address string) (*big.Int, *uint64, *big.Int, error) { 129 | client, err := nodeClient(node) 130 | if err != nil { 131 | return nil, nil, nil, err 132 | } 133 | balance, nonce, gasPrice, err := getBalanceAndPendingNonceAtAndGasPrice(client, address) 134 | if err != nil { 135 | return nil, nil, nil, err 136 | } 137 | return balance, nonce, gasPrice, nil 138 | } 139 | 140 | func getBalanceAndPendingNonceAtAndGasPrice(node *ethclient.Client, address string) (*big.Int, *uint64, *big.Int, error) { 141 | ctx := context.Background() 142 | balance, err := node.BalanceAt(ctx, common.HexToAddress(address), nil) 143 | if err != nil { 144 | return nil, nil, nil, errors.New(strings.Join([]string{"Failed to get ethereum balance from address:", address, err.Error()}, " ")) 145 | } 146 | 147 | if err := balanceIsLessThanConfig(address, balance); err != nil { 148 | return nil, nil, nil, err 149 | } 150 | 151 | pendingNonceAt, err := node.PendingNonceAt(ctx, common.HexToAddress(address)) 152 | if err != nil { 153 | return nil, nil, nil, errors.New(strings.Join([]string{"Failed to get account nonce from address:", address, err.Error()}, " ")) 154 | } 155 | 156 | gasPrice, err := node.SuggestGasPrice(ctx) 157 | if err != nil { 158 | return nil, nil, nil, errors.New(strings.Join([]string{"get gasPrice error", err.Error()}, " ")) 159 | } 160 | 161 | return balance, &pendingNonceAt, gasPrice, nil 162 | 163 | } 164 | 165 | func decodeTx(txHex string) (*types.Transaction, error) { 166 | txc, err := hexutil.Decode(txHex) 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | var txde types.Transaction 172 | 173 | t, err := &txde, rlp.Decode(bytes.NewReader(txc), &txde) 174 | if err != nil { 175 | return nil, err 176 | } 177 | 178 | return t, nil 179 | } 180 | 181 | func encodeTx(tx *types.Transaction) (*string, error) { 182 | txb, err := rlp.EncodeToBytes(tx) 183 | if err != nil { 184 | return nil, err 185 | } 186 | txHex := hexutil.Encode(txb) 187 | return &txHex, nil 188 | } 189 | 190 | func signTxCmd() { 191 | files, err := ioutil.ReadDir(strings.Join([]string{HomeDir(), config.RawTx}, "/")) 192 | if err != nil { 193 | log.Fatalln("read raw tx error", err.Error()) 194 | } 195 | 196 | for _, file := range files { 197 | fileName := file.Name() 198 | tx, err := readTxHex(&fileName, false) 199 | if err != nil { 200 | log.Errorln(err.Error()) 201 | continue 202 | } 203 | 204 | from, to, signedTxHex, hash, value, nonce, err := signTx(tx) 205 | if err != nil { 206 | log.Errorln(strings.Join([]string{"sign tx from", tx.From, "error", err.Error()}, " ")) 207 | continue 208 | } 209 | if err := exportHexTx(*from, *to, *signedTxHex, *hash, value, nonce, true); err != nil { 210 | log.Errorln(strings.Join([]string{"export signed tx hex to", fileName, "error, issue by address:", *from, err.Error()}, " ")) 211 | continue 212 | } 213 | } 214 | } 215 | 216 | func signTx(simpletx *Tx) (*string, *string, *string, *string, *big.Int, *uint64, error) { 217 | txHex := simpletx.TxHex 218 | fromAddressHex := simpletx.From 219 | tx, err := decodeTx(txHex) 220 | if err != nil { 221 | return nil, nil, nil, nil, nil, nil, errors.New(strings.Join([]string{"decode tx error", err.Error()}, " ")) 222 | } 223 | 224 | if Contains(config.To, tx.To().Hex()) { 225 | log.Infoln("签名交易:", tx.Hash().Hex(), " To:", tx.To().Hex()) 226 | } else { 227 | promptSign(tx.To().Hex()) 228 | } 229 | 230 | key, err := decodeKS2Key(fromAddressHex) 231 | if err != nil { 232 | return nil, nil, nil, nil, nil, nil, errors.New(strings.Join([]string{"decode keystore to key error:", err.Error()}, " ")) 233 | } 234 | 235 | // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md 236 | // chain id 237 | // 1 Ethereum mainnet 238 | // 61 Ethereum Classic mainnet 239 | // 62 Ethereum Classic testnet 240 | // 1337 Geth private chains (default) 241 | var chainID *big.Int 242 | switch config.NetMode { 243 | case "privatenet": 244 | chainID = big.NewInt(1337) 245 | case "mainnet": 246 | chainID = big.NewInt(1) 247 | default: 248 | return nil, nil, nil, nil, nil, nil, errors.New("you must set net_mode in configure") 249 | } 250 | signtx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), key.PrivateKey) 251 | if err != nil { 252 | return nil, nil, nil, nil, nil, nil, errors.New(strings.Join([]string{"sign tx error", err.Error()}, " ")) 253 | } 254 | msg, err := signtx.AsMessage(types.NewEIP155Signer(chainID)) 255 | if err != nil { 256 | return nil, nil, nil, nil, nil, nil, errors.New(strings.Join([]string{"tx to msg error", err.Error()}, " ")) 257 | } 258 | 259 | from := msg.From().Hex() 260 | to := msg.To().Hex() 261 | value := msg.Value() 262 | nonce := msg.Nonce() 263 | signTxHex, err := encodeTx(signtx) 264 | hash := signtx.Hash().Hex() 265 | return &from, &to, signTxHex, &hash, value, &nonce, nil 266 | } 267 | 268 | func sendTxCmd(nodeClient *ethclient.Client) { 269 | files, err := ioutil.ReadDir(strings.Join([]string{HomeDir(), config.SignedTx}, "/")) 270 | if err != nil { 271 | log.Fatalln("read raw tx error", err.Error()) 272 | } 273 | 274 | for _, file := range files { 275 | fileName := file.Name() 276 | tx, err := readTxHex(&fileName, true) 277 | if err != nil { 278 | log.Errorln(err.Error()) 279 | } 280 | 281 | signedTxHex := tx.TxHex 282 | hash, err := sendTx(signedTxHex, nodeClient) 283 | if err != nil { 284 | log.Errorln("send tx: ", fileName, "fail", err.Error()) 285 | } else { 286 | log.Infoln("send tx: ", *hash, "success") 287 | } 288 | } 289 | } 290 | 291 | func readTxHex(fileName *string, signed bool) (*Tx, error) { 292 | var filePath string 293 | if signed { 294 | filePath = strings.Join([]string{HomeDir(), config.SignedTx, *fileName}, "/") 295 | } else { 296 | filePath = strings.Join([]string{HomeDir(), config.RawTx, *fileName}, "/") 297 | } 298 | 299 | bRawTx, err := ioutil.ReadFile(filePath) 300 | if err != nil { 301 | return nil, errors.New(strings.Join([]string{"can't read", filePath, err.Error()}, " ")) 302 | } 303 | 304 | var tx Tx 305 | if err := json.Unmarshal(bRawTx, &tx); err != nil { 306 | return nil, errors.New(strings.Join([]string{"can't Unmarshal", filePath, "to RawTx struct"}, " ")) 307 | } 308 | return &tx, nil 309 | } 310 | 311 | func sendTx(signTxHex string, nodeClient *ethclient.Client) (*string, error) { 312 | signTx, err := decodeTx(signTxHex) 313 | if err != nil { 314 | return nil, errors.New(strings.Join([]string{"Send tx error:", "decode tx error", err.Error()}, " ")) 315 | } 316 | 317 | if !Contains(config.To, signTx.To().Hex()) { 318 | return nil, errors.New(strings.Join([]string{"Send tx error: ", signTx.To().Hex(), "is not contained in configure to value"}, " ")) 319 | } 320 | 321 | if err := nodeClient.SendTransaction(context.Background(), signTx); err != nil { 322 | return nil, err 323 | } 324 | h := signTx.Hash().Hex() 325 | return &h, nil 326 | } 327 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "math/big" 12 | "math/rand" 13 | "os" 14 | "strings" 15 | "time" 16 | 17 | "github.com/ethereum/go-ethereum/ethclient" 18 | "github.com/manifoldco/promptui" 19 | homedir "github.com/mitchellh/go-homedir" 20 | "github.com/shopspring/decimal" 21 | log "github.com/sirupsen/logrus" 22 | ) 23 | 24 | // HomeDir 获取服务器当前用户目录路径 25 | func HomeDir() string { 26 | home, err := homedir.Dir() 27 | if err != nil { 28 | log.Fatal(err.Error()) 29 | } 30 | return home 31 | } 32 | 33 | func initLogger() { 34 | path := strings.Join([]string{HomeDir(), ".ethereum_service"}, "/") 35 | if err := os.MkdirAll(path, 0700); err != nil { 36 | log.Fatalln(err.Error()) 37 | } 38 | 39 | filepath := strings.Join([]string{path, "out.log"}, "/") 40 | file, err := os.OpenFile(filepath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) 41 | mw := io.MultiWriter(os.Stdout, file) 42 | if err == nil { 43 | log.SetOutput(mw) 44 | log.WithFields(log.Fields{ 45 | "Note": "all operate is recorded", 46 | "Time:": time.Now().Format("Mon Jan _2 15:04:05 2006"), 47 | }).Warn("") 48 | } else { 49 | log.Error(err.Error()) 50 | } 51 | } 52 | 53 | func promptPwd() (*string, error) { 54 | promptOne := promptui.Prompt{ 55 | Label: "Password", 56 | Mask: '*', 57 | } 58 | 59 | resultOne, err := promptOne.Run() 60 | if err != nil { 61 | fmt.Printf("Prompt failed %v\n", err) 62 | return nil, err 63 | } 64 | 65 | validate := func(input string) error { 66 | if resultOne != input { 67 | return errors.New("password not match") 68 | } 69 | return nil 70 | } 71 | 72 | promptTwo := promptui.Prompt{ 73 | Label: "Password", 74 | Validate: validate, 75 | Mask: '*', 76 | } 77 | 78 | resultTwo, err := promptTwo.Run() 79 | if err != nil { 80 | fmt.Printf("Prompt failed %v\n", err) 81 | return nil, err 82 | } 83 | return &resultTwo, nil 84 | } 85 | 86 | func promptSign(to string) { 87 | prompt := promptui.Prompt{ 88 | Label: strings.Join([]string{"To 地址不在配置文件中,请确认是否转入地址:", to}, " "), 89 | IsConfirm: true, 90 | } 91 | 92 | _, err := prompt.Run() 93 | if err != nil { 94 | log.Fatalln("退出...") 95 | } 96 | } 97 | 98 | // RandStringBytesMaskImprSrc 随机数 99 | // https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang 100 | func RandStringBytesMaskImprSrc(n int) string { 101 | var src = rand.NewSource(time.Now().UnixNano()) 102 | const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 103 | const ( 104 | letterIdxBits = 6 // 6 bits to represent a letter index 105 | letterIdxMask = 1<= 0; { 111 | if remain == 0 { 112 | cache, remain = src.Int63(), letterIdxMax 113 | } 114 | if idx := int(cache & letterIdxMask); idx < len(letterBytes) { 115 | b[i] = letterBytes[idx] 116 | i-- 117 | } 118 | cache >>= letterIdxBits 119 | remain-- 120 | } 121 | 122 | return string(b) 123 | } 124 | 125 | // PKCS7Padding PKCS7 填充 https://www.jianshu.com/p/b63095c59361 126 | func PKCS7Padding(ciphertext []byte, blockSize int) []byte { 127 | padding := blockSize - len(ciphertext)%blockSize 128 | padtext := bytes.Repeat([]byte{byte(padding)}, padding) 129 | return append(ciphertext, padtext...) 130 | } 131 | 132 | // PKCS7UnPadding 还原 PKCS7 填充 133 | func PKCS7UnPadding(origData []byte) []byte { 134 | length := len(origData) 135 | unpadding := int(origData[length-1]) 136 | return origData[:(length - unpadding)] 137 | } 138 | 139 | // AesEncrypt 加密 140 | func AesEncrypt(origData, key []byte) ([]byte, error) { 141 | block, err := aes.NewCipher(key) 142 | if err != nil { 143 | return nil, err 144 | } 145 | blockSize := block.BlockSize() 146 | origData = PKCS7Padding(origData, blockSize) 147 | blockMode := cipher.NewCBCEncrypter(block, key[:blockSize]) 148 | crypted := make([]byte, len(origData)) 149 | blockMode.CryptBlocks(crypted, origData) 150 | return crypted, nil 151 | } 152 | 153 | // AesDecrypt 解密 154 | func AesDecrypt(crypted, key []byte) ([]byte, error) { 155 | block, err := aes.NewCipher(key) 156 | if err != nil { 157 | return nil, err 158 | } 159 | blockSize := block.BlockSize() 160 | blockMode := cipher.NewCBCDecrypter(block, key[:blockSize]) 161 | origData := make([]byte, len(crypted)) 162 | blockMode.CryptBlocks(origData, crypted) 163 | origData = PKCS7UnPadding(origData) 164 | return origData, nil 165 | } 166 | 167 | func nodeClient(node string) (*ethclient.Client, error) { 168 | var nodeConfig string 169 | if node == "geth" { 170 | nodeConfig = config.GethRPC 171 | } else if node == "parity" { 172 | nodeConfig = config.ParityRPC 173 | } 174 | 175 | client, err := ethclient.Dial(nodeConfig) 176 | if err != nil { 177 | return nil, errors.New(strings.Join([]string{"node error", err.Error()}, " ")) 178 | } 179 | return client, nil 180 | } 181 | 182 | func balanceIsLessThanConfig(address string, balance *big.Int) error { 183 | balanceDecimal, _ := decimal.NewFromString(balance.String()) 184 | ethFac, _ := decimal.NewFromString("0.000000000000000001") 185 | amount := balanceDecimal.Mul(ethFac) 186 | settingBalance := decimal.NewFromFloat(config.MaxBalance) 187 | if amount.LessThan(settingBalance) { 188 | return errors.New(strings.Join([]string{"Ignore:", address, "balance not great than the configure amount"}, " ")) 189 | } 190 | return nil 191 | } 192 | 193 | func appenFile(filename string, data []byte, perm os.FileMode) error { 194 | f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, perm) 195 | if err != nil { 196 | return err 197 | } 198 | n, err := f.Write(data) 199 | if err == nil && n < len(data) { 200 | err = io.ErrShortWrite 201 | } 202 | if err1 := f.Close(); err == nil { 203 | err = err1 204 | } 205 | return err 206 | } 207 | 208 | func mkdirBySlice(slice []string) (*string, error) { 209 | path := strings.Join(slice, "/") 210 | if _, err := os.Stat(path); os.IsNotExist(err) { 211 | if err := os.MkdirAll(path, 0700); err != nil { 212 | return nil, err 213 | } 214 | } 215 | return &path, nil 216 | } 217 | 218 | // Contains tells whether a contains x. 219 | func Contains(a []string, x string) bool { 220 | for _, n := range a { 221 | if strings.Compare(strings.ToLower(x), strings.ToLower(n)) == 0 { 222 | return true 223 | } 224 | } 225 | return false 226 | } 227 | 228 | func randomPickFromSlice(slice []string) string { 229 | s := rand.NewSource(time.Now().Unix()) 230 | r := rand.New(s) 231 | return slice[r.Intn(len(slice))] 232 | } 233 | 234 | func accountDir(address string) (*string, error) { 235 | ksPath := strings.Join([]string{HomeDir(), "account", "keystore"}, "/") 236 | timeFolders, err := ioutil.ReadDir(ksPath) 237 | if err != nil { 238 | return nil, errors.New("Get keystore directory error") 239 | } 240 | 241 | keystoreName := strings.Join([]string{address, "json"}, ".") 242 | for _, timeDir := range timeFolders { 243 | timePath := strings.Join([]string{HomeDir(), "account", "keystore", timeDir.Name()}, "/") 244 | files, err := ioutil.ReadDir(timePath) 245 | if err != nil { 246 | return nil, errors.New("Get timeDir error") 247 | } 248 | for _, f := range files { 249 | if strings.Compare(strings.ToLower(f.Name()), strings.ToLower(keystoreName)) == 0 { 250 | path := timeDir.Name() 251 | return &path, nil 252 | } 253 | } 254 | } 255 | return nil, errors.New("Account directory not found") 256 | } 257 | --------------------------------------------------------------------------------