aboutsummaryrefslogtreecommitdiffstats
path: root/cmd
diff options
context:
space:
mode:
authorPéter Szilágyi <peterke@gmail.com>2017-11-24 16:56:33 +0800
committerGitHub <noreply@github.com>2017-11-24 16:56:33 +0800
commitf9569f3cd8a05dff5be5da83a5d8c70c1b23dd3e (patch)
treefa8b344aedb3d56c2cd904173053a5273ec87a67 /cmd
parent35801f938e21430f8fb0f2e1c3de5db7dbd21666 (diff)
parenta3a2c6b0d9f963c9377612cae1ed6ded6f216c2d (diff)
downloaddexon-f9569f3cd8a05dff5be5da83a5d8c70c1b23dd3e.tar.gz
dexon-f9569f3cd8a05dff5be5da83a5d8c70c1b23dd3e.tar.zst
dexon-f9569f3cd8a05dff5be5da83a5d8c70c1b23dd3e.zip
Merge pull request #15390 from karalabe/puppeth-devcon3
cmd/puppeth: new version as presented at devcon3
Diffstat (limited to 'cmd')
-rw-r--r--cmd/faucet/faucet.go25
-rw-r--r--cmd/faucet/faucet.html17
-rw-r--r--cmd/faucet/website.go2
-rw-r--r--cmd/puppeth/genesis.go379
-rw-r--r--cmd/puppeth/module_dashboard.go307
-rw-r--r--cmd/puppeth/module_ethstats.go34
-rw-r--r--cmd/puppeth/module_explorer.go211
-rw-r--r--cmd/puppeth/module_faucet.go81
-rw-r--r--cmd/puppeth/module_nginx.go19
-rw-r--r--cmd/puppeth/module_node.go62
-rw-r--r--cmd/puppeth/module_wallet.go200
-rw-r--r--cmd/puppeth/puppeth.go2
-rw-r--r--cmd/puppeth/ssh.go1
-rw-r--r--cmd/puppeth/wizard.go14
-rw-r--r--cmd/puppeth/wizard_dashboard.go40
-rw-r--r--cmd/puppeth/wizard_ethstats.go70
-rw-r--r--cmd/puppeth/wizard_explorer.go117
-rw-r--r--cmd/puppeth/wizard_faucet.go78
-rw-r--r--cmd/puppeth/wizard_genesis.go54
-rw-r--r--cmd/puppeth/wizard_intro.go40
-rw-r--r--cmd/puppeth/wizard_netstats.go393
-rw-r--r--cmd/puppeth/wizard_network.go16
-rw-r--r--cmd/puppeth/wizard_nginx.go11
-rw-r--r--cmd/puppeth/wizard_node.go34
-rw-r--r--cmd/puppeth/wizard_wallet.go113
25 files changed, 1867 insertions, 453 deletions
diff --git a/cmd/faucet/faucet.go b/cmd/faucet/faucet.go
index 72098e68d..328029fdf 100644
--- a/cmd/faucet/faucet.go
+++ b/cmd/faucet/faucet.go
@@ -83,7 +83,8 @@ var (
captchaToken = flag.String("captcha.token", "", "Recaptcha site key to authenticate client side")
captchaSecret = flag.String("captcha.secret", "", "Recaptcha secret key to authenticate server side")
- logFlag = flag.Int("loglevel", 3, "Log level to use for Ethereum and the faucet")
+ noauthFlag = flag.Bool("noauth", false, "Enables funding requests without authentication")
+ logFlag = flag.Int("loglevel", 3, "Log level to use for Ethereum and the faucet")
)
var (
@@ -132,6 +133,7 @@ func main() {
"Amounts": amounts,
"Periods": periods,
"Recaptcha": *captchaToken,
+ "NoAuth": *noauthFlag,
})
if err != nil {
log.Crit("Failed to render the faucet template", "err", err)
@@ -374,7 +376,7 @@ func (f *faucet) apiHandler(conn *websocket.Conn) {
if err = websocket.JSON.Receive(conn, &msg); err != nil {
return
}
- if !strings.HasPrefix(msg.URL, "https://gist.github.com/") && !strings.HasPrefix(msg.URL, "https://twitter.com/") &&
+ if !*noauthFlag && !strings.HasPrefix(msg.URL, "https://gist.github.com/") && !strings.HasPrefix(msg.URL, "https://twitter.com/") &&
!strings.HasPrefix(msg.URL, "https://plus.google.com/") && !strings.HasPrefix(msg.URL, "https://www.facebook.com/") {
if err = sendError(conn, errors.New("URL doesn't link to supported services")); err != nil {
log.Warn("Failed to send URL error to client", "err", err)
@@ -435,13 +437,19 @@ func (f *faucet) apiHandler(conn *websocket.Conn) {
)
switch {
case strings.HasPrefix(msg.URL, "https://gist.github.com/"):
- username, avatar, address, err = authGitHub(msg.URL)
+ if err = sendError(conn, errors.New("GitHub authentication discontinued at the official request of GitHub")); err != nil {
+ log.Warn("Failed to send GitHub deprecation to client", "err", err)
+ return
+ }
+ continue
case strings.HasPrefix(msg.URL, "https://twitter.com/"):
username, avatar, address, err = authTwitter(msg.URL)
case strings.HasPrefix(msg.URL, "https://plus.google.com/"):
username, avatar, address, err = authGooglePlus(msg.URL)
case strings.HasPrefix(msg.URL, "https://www.facebook.com/"):
username, avatar, address, err = authFacebook(msg.URL)
+ case *noauthFlag:
+ username, avatar, address, err = authNoAuth(msg.URL)
default:
err = errors.New("Something funky happened, please open an issue at https://github.com/ethereum/go-ethereum/issues")
}
@@ -776,3 +784,14 @@ func authFacebook(url string) (string, string, common.Address, error) {
}
return username + "@facebook", avatar, address, nil
}
+
+// authNoAuth tries to interpret a faucet request as a plain Ethereum address,
+// without actually performing any remote authentication. This mode is prone to
+// Byzantine attack, so only ever use for truly private networks.
+func authNoAuth(url string) (string, string, common.Address, error) {
+ address := common.HexToAddress(regexp.MustCompile("0x[0-9a-fA-F]{40}").FindString(url))
+ if address == (common.Address{}) {
+ return "", "", common.Address{}, errors.New("No Ethereum address found to fund")
+ }
+ return address.Hex() + "@noauth", "", address, nil
+}
diff --git a/cmd/faucet/faucet.html b/cmd/faucet/faucet.html
index 5d3b8741b..ab41b2c87 100644
--- a/cmd/faucet/faucet.html
+++ b/cmd/faucet/faucet.html
@@ -80,11 +80,8 @@
<div class="row" style="margin-top: 32px;">
<div class="col-lg-12">
<h3>How does this work?</h3>
- <p>This Ether faucet is running on the {{.Network}} network. To prevent malicious actors from exhausting all available funds or accumulating enough Ether to mount long running spam attacks, requests are tied to certain common 3rd party accounts. Anyone having a GitHub, Twitter, Google+ or Facebook account may request funds within the permitted limits.</p>
+ <p>This Ether faucet is running on the {{.Network}} network. To prevent malicious actors from exhausting all available funds or accumulating enough Ether to mount long running spam attacks, requests are tied to common 3rd party social network accounts. Anyone having a Twitter, Google+ or Facebook account may request funds within the permitted limits.</p>
<dl class="dl-horizontal">
- <dt style="width: auto; margin-left: 40px;"><i class="fa fa-github-alt" aria-hidden="true" style="font-size: 36px;"></i></dt>
- <dd style="margin-left: 88px; margin-bottom: 10px;"></i> To request funds via GitHub, create a <a href="https://gist.github.com/" target="_about:blank">gist</a> with your Ethereum address embedded into the content (the file name doesn't matter).<br/>Copy-paste the gists URL into the above input box and fire away!</dd>
-
<dt style="width: auto; margin-left: 40px;"><i class="fa fa-twitter" aria-hidden="true" style="font-size: 36px;"></i></dt>
<dd style="margin-left: 88px; margin-bottom: 10px;"></i> To request funds via Twitter, make a <a href="https://twitter.com/intent/tweet?text=Requesting%20faucet%20funds%20into%200x0000000000000000000000000000000000000000%20on%20the%20%23{{.Network}}%20%23Ethereum%20test%20network." target="_about:blank">tweet</a> with your Ethereum address pasted into the contents (surrounding text doesn't matter).<br/>Copy-paste the <a href="https://support.twitter.com/articles/80586" target="_about:blank">tweets URL</a> into the above input box and fire away!</dd>
@@ -93,6 +90,11 @@
<dt style="width: auto; margin-left: 40px;"><i class="fa fa-facebook" aria-hidden="true" style="font-size: 36px;"></i></dt>
<dd style="margin-left: 88px; margin-bottom: 10px;"></i> To request funds via Facebook, publish a new <strong>public</strong> post with your Ethereum address embedded into the content (surrounding text doesn't matter).<br/>Copy-paste the <a href="https://www.facebook.com/help/community/question/?id=282662498552845" target="_about:blank">posts URL</a> into the above input box and fire away!</dd>
+
+ {{if .NoAuth}}
+ <dt class="text-danger" style="width: auto; margin-left: 40px;"><i class="fa fa-unlock-alt" aria-hidden="true" style="font-size: 36px;"></i></dt>
+ <dd class="text-danger" style="margin-left: 88px; margin-bottom: 10px;"></i> To request funds <strong>without authentication</strong>, simply copy-paste your Ethereum address into the above input box (surrounding text doesn't matter) and fire away.<br/>This mode is susceptible to Byzantine attacks. Only use for debugging or private networks!</dd>
+ {{end}}
</dl>
<p>You can track the current pending requests below the input field to see how much you have to wait until your turn comes.</p>
{{if .Recaptcha}}<em>The faucet is running invisible reCaptcha protection against bots.</em>{{end}}
@@ -126,12 +128,7 @@
};
// Define a method to reconnect upon server loss
var reconnect = function() {
- if (attempt % 2 == 0) {
- server = new WebSocket("wss://" + location.host + "/api");
- } else {
- server = new WebSocket("ws://" + location.host + "/api");
- }
- attempt++;
+ server = new WebSocket(((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + "/api");
server.onmessage = function(event) {
var msg = JSON.parse(event.data);
diff --git a/cmd/faucet/website.go b/cmd/faucet/website.go
index 6a99f8c6f..7936b158e 100644
--- a/cmd/faucet/website.go
+++ b/cmd/faucet/website.go
@@ -68,7 +68,7 @@ func (fi bindataFileInfo) Sys() interface{} {
return nil
}
-var _faucetHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x3a\x7f\x73\xdb\xb6\x92\x7f\x3b\x9f\x62\xcb\x8b\x9f\xa4\xb3\x48\xca\x76\x92\xe7\x93\x48\x75\x72\x79\x7d\x7d\xb9\xb9\xeb\xeb\xb4\xe9\xdc\xbd\x69\x3b\x37\x20\xb1\x12\x11\x83\x00\x0b\x80\x92\x55\x8f\xbe\xfb\x0d\x00\x92\xa2\x7e\xd8\x71\x9a\xdc\x5d\xfc\x87\x4c\x00\x8b\xdd\xc5\xfe\xc6\x92\xc9\x57\x7f\xf9\xfb\x9b\x77\xff\xf8\xfe\x1b\x28\x4c\xc9\xe7\xcf\x12\xfb\x0f\x38\x11\xcb\x34\x40\x11\xcc\x9f\x9d\x25\x05\x12\x3a\x7f\x76\x76\x96\x94\x68\x08\xe4\x05\x51\x1a\x4d\x1a\xd4\x66\x11\xde\x04\xbb\x85\xc2\x98\x2a\xc4\xdf\x6a\xb6\x4a\x83\xff\x0a\x7f\x7a\x1d\xbe\x91\x65\x45\x0c\xcb\x38\x06\x90\x4b\x61\x50\x98\x34\x78\xfb\x4d\x8a\x74\x89\xbd\x7d\x82\x94\x98\x06\x2b\x86\xeb\x4a\x2a\xd3\x03\x5d\x33\x6a\x8a\x94\xe2\x8a\xe5\x18\xba\xc1\x18\x98\x60\x86\x11\x1e\xea\x9c\x70\x4c\x2f\x83\xf9\x33\x8b\xc7\x30\xc3\x71\x7e\x7f\x1f\x7d\x87\x66\x2d\xd5\xed\x76\x3b\x85\xd7\xb5\x29\x50\x18\x96\x13\x83\x14\xfe\x4a\xea\x1c\x4d\x12\x7b\x48\xb7\x89\x33\x71\x0b\x85\xc2\x45\x1a\x58\xd6\xf5\x34\x8e\x73\x2a\xde\xeb\x28\xe7\xb2\xa6\x0b\x4e\x14\x46\xb9\x2c\x63\xf2\x9e\xdc\xc5\x9c\x65\x3a\x36\x6b\x66\x0c\xaa\x30\x93\xd2\x68\xa3\x48\x15\x5f\x47\xd7\xd1\x9f\xe3\x5c\xeb\xb8\x9b\x8b\x4a\x26\xa2\x5c\xeb\x00\x14\xf2\x34\xd0\x66\xc3\x51\x17\x88\x26\x80\x78\xfe\xc7\xe8\x2e\xa4\x30\x21\x59\xa3\x96\x25\xc6\x2f\xa2\x3f\x47\x13\x47\xb2\x3f\xfd\x38\x55\x4b\x56\xe7\x8a\x55\x06\xb4\xca\x9f\x4c\xf7\xfd\x6f\x35\xaa\x4d\x7c\x1d\x5d\x46\x97\xcd\xc0\xd1\x79\xaf\x83\x79\x12\x7b\x84\xf3\x4f\xc2\x1d\x0a\x69\x36\xf1\x55\xf4\x22\xba\x8c\x2b\x92\xdf\x92\x25\xd2\x96\x92\x5d\x8a\xda\xc9\xcf\x46\xf7\x21\x1d\xbe\x3f\x54\xe1\xe7\x20\x56\xca\x12\x85\x89\xde\xeb\xf8\x2a\xba\xbc\x89\x26\xed\xc4\x31\x7e\x47\xc0\x2a\xcd\x92\x3a\x8b\x56\xa8\xac\xe5\xf2\x30\x47\x61\x50\xc1\xbd\x9d\x3d\x2b\x99\x08\x0b\x64\xcb\xc2\x4c\xe1\x72\x32\x39\x9f\x9d\x9a\x5d\x15\x7e\x9a\x32\x5d\x71\xb2\x99\xc2\x82\xe3\x9d\x9f\x22\x9c\x2d\x45\xc8\x0c\x96\x7a\x0a\x1e\xb3\x5b\xd8\x3a\x9a\x95\x92\x4b\x85\x5a\x37\xc4\x2a\xa9\x99\x61\x52\x4c\xad\x45\x11\xc3\x56\x78\x0a\x56\x57\x44\x1c\x6d\x20\x99\x96\xbc\x36\x78\xc0\x48\xc6\x65\x7e\xeb\xe7\x9c\x37\xf7\x0f\x91\x4b\x2e\xd5\x14\xd6\x05\x6b\xb6\x81\x23\x04\x95\xc2\x06\x3d\x54\x84\x52\x26\x96\x53\x78\x55\x35\xe7\x81\x92\xa8\x25\x13\x53\x98\xec\xb6\x24\x71\x2b\xc6\x24\xf6\x81\xeb\xd9\x59\x92\x49\xba\x71\x3a\xa4\x6c\x05\x39\x27\x5a\xa7\xc1\x81\x88\x5d\x40\xda\x03\xb0\x71\x88\x30\xd1\x2e\xed\xad\x29\xb9\x0e\xc0\x11\x4a\x03\xcf\x44\x98\x49\x63\x64\x39\x85\x4b\xcb\x5e\xb3\xe5\x00\x1f\x0f\xf9\x32\xbc\xbc\x6a\x17\xcf\x92\xe2\xb2\x45\x62\xf0\xce\x84\x4e\x3f\x9d\x66\x82\x79\xc2\xda\xbd\x0b\x02\x0b\x12\x66\xc4\x14\x01\x10\xc5\x48\x58\x30\x4a\x51\xa4\x81\x51\x35\x5a\x3b\x62\x73\xe8\x87\xbf\x07\xa2\x5f\x71\xd9\xf2\x15\x53\xb6\x6a\x8e\xd5\x7b\x3c\x38\xe1\xc3\x87\xb8\x81\xe6\x41\x2e\x16\x1a\x4d\xd8\x3b\x53\x0f\x98\x89\xaa\x36\xe1\x52\xc9\xba\xea\xd6\xcf\x12\x37\x0b\x8c\xa6\x41\xad\x78\xd0\x84\x7f\xf7\x68\x36\x55\x23\x8a\xa0\x3b\xb8\x54\x65\x68\x35\xa1\x24\x0f\xa0\xe2\x24\xc7\x42\x72\x8a\x2a\x0d\x7e\x94\x39\x23\x1c\x84\x3f\x33\xfc\xf4\xc3\xbf\x43\xa3\x32\x26\x96\xb0\x91\xb5\x82\x6f\x4c\x81\x0a\xeb\x12\x08\xa5\xd6\x5c\xa3\x28\xea\x31\xe2\x6c\xf7\x98\xd5\x30\x33\x62\x07\x75\x96\x64\xb5\x31\xb2\x03\xcc\x8c\x80\xcc\x88\x90\xe2\x82\xd4\xdc\x00\x55\xb2\xa2\x72\x2d\x42\x23\x97\x4b\x9b\xe9\xfc\x21\xfc\xa6\x00\x28\x31\xa4\x59\x4a\x83\x16\xb6\xd5\x21\xd1\x95\xac\xea\xaa\xd1\xa2\x9f\xc4\xbb\x8a\x08\x8a\xd4\xea\x9c\x6b\x0c\xe6\xdf\xb2\x15\x42\x89\xfe\x2c\x67\x87\x26\x91\x13\x85\x26\xec\x23\x3d\x32\x8c\x24\xf6\xcc\xf8\x23\x41\xf3\x97\xd4\xbc\xc5\xd4\x1d\xa1\x44\x51\xc3\xde\x28\x54\x36\xae\x04\xf3\xfb\x7b\x45\xc4\x12\xe1\x39\xa3\x77\x63\x78\x4e\x4a\x59\x0b\x03\xd3\x14\xa2\xd7\xee\x51\x6f\xb7\x7b\xd8\x01\x12\xce\xe6\x09\x79\xcc\xbc\x41\x8a\x9c\xb3\xfc\x36\x0d\x0c\x43\x95\xde\xdf\x5b\xe4\xdb\xed\x0c\xee\xef\xd9\x02\x9e\x47\x3f\x60\x4e\x2a\x93\x17\x64\xbb\x5d\xaa\xf6\x39\xc2\x3b\xcc\x6b\x83\xc3\xd1\xfd\x3d\x72\x8d\xdb\xad\xae\xb3\x92\x99\x61\xbb\xdd\xce\x0b\xba\xdd\x5a\x9e\x1b\x3e\xb7\x5b\x88\x2d\x52\x41\xf1\x0e\x9e\x47\xdf\xa3\x62\x92\x6a\xf0\xf0\x49\x4c\xe6\x49\xcc\xd9\xbc\xd9\xb7\x2f\xa4\xb8\xe6\x3b\x7b\x89\xad\xc1\x74\x76\xee\xdc\xc6\xb1\xda\xe7\xf4\x84\x17\x2c\xc3\x8e\xfb\xc6\x1e\x34\x33\x78\x8b\x9b\x34\xb8\xbf\xef\xef\x6d\x56\x73\xc2\x79\x46\xac\x5c\xfc\xd1\xba\x4d\xbf\xa3\xb5\xd3\x15\xd3\xae\xa4\x9a\xb7\x1c\xec\xd8\x7e\xa2\x5b\x1f\x04\x2e\x23\xab\x29\x5c\x5f\xf5\xa2\xd6\x29\x8f\x7f\x75\xe0\xf1\xd7\x27\x81\x2b\x22\x90\x83\xfb\x0d\x75\x49\x78\xfb\xdc\x78\x4b\xcf\xf9\x0e\x37\x85\x36\x46\x77\xac\x75\xb1\x7e\x32\x03\xb9\x42\xb5\xe0\x72\x3d\x05\x52\x1b\x39\x83\x92\xdc\x75\xf9\xee\x7a\x32\xe9\xf3\x6d\x4b\x41\x92\x71\x74\xd1\x45\xe1\x6f\x35\x6a\xa3\xbb\x58\xe2\x97\xdc\xaf\x0d\x29\x14\x85\x46\x7a\x20\x0d\x4b\xd1\x8a\xd6\x41\xf5\x54\xdf\x09\xf3\x24\xef\x0b\x29\xbb\x14\xd2\x67\xa3\x41\xdd\xcb\x76\xc1\x3c\x31\x6a\x07\x77\x96\x18\xfa\x51\x29\x40\xd9\x12\xef\xa1\x0c\xe0\x23\x9a\x3d\x7b\x85\xa8\x7c\x7d\x61\x4d\x16\xdc\x30\x89\x0d\xfd\x04\xca\xd6\x08\x33\xa2\xf1\x29\xe4\x5d\xa6\xdf\x91\x77\xc3\x4f\xa5\x5f\x20\x51\x26\x43\x62\x9e\xc2\xc0\xa2\x16\xb4\x77\x7e\x17\x3b\x3f\x95\x81\x5a\xb0\x15\x2a\xcd\xcc\xe6\xa9\x1c\x20\xdd\xb1\xe0\xc7\xfb\x2c\x24\xb1\x51\x8f\xdb\x5a\x7f\xf0\x99\x9c\xfb\x43\x25\xc9\xf5\xfc\x6f\x72\x0d\x54\xa2\x06\x53\x30\x0d\x36\xb9\x7e\x9d\xc4\xc5\x75\x07\x52\xcd\xdf\xd9\x05\x27\x54\x58\xb8\xd2\x02\x98\x06\x55\x0b\x97\x79\xa5\x00\x53\xe0\x7e\x39\xd2\x24\xe9\x08\xde\x49\x5b\xd2\xad\x50\x18\x28\x09\x67\x39\x93\xb5\x06\x92\x1b\xa9\x34\x2c\x94\x2c\x01\xef\x0a\x52\x6b\x63\x11\xd9\xf0\x41\x56\x84\x71\xe7\x4b\x4e\xa5\x20\x15\x90\x3c\xaf\xcb\xda\x96\xa4\x62\x09\x28\x64\xbd\x2c\x1a\x5e\x8c\x04\x9f\x98\xb8\x14\xcb\x8e\x1f\x5d\x91\x12\x88\x31\x24\xbf\xd5\x63\x68\xa3\x02\x10\x85\x60\x18\x52\xbb\x2b\x47\x65\xeb\x06\xc8\x65\x59\x4a\x01\xd7\x8a\x42\x45\x94\xd9\x58\x5a\x2e\xbd\x45\xf0\x5a\x6c\xa4\x40\x28\xc8\xca\xb1\x06\xdf\x32\xf3\xb7\x3a\x1b\xc3\x3b\x7f\x9f\x18\xc3\xb7\x52\x2e\x39\x5e\x58\x0e\xff\x4a\x72\xcc\xa4\xbc\x6d\xb7\x43\x49\x36\x2d\xe1\xe6\x1c\x6b\x66\x0a\xe6\x05\x55\xa1\x2a\x2d\x0e\x0a\x9c\x95\xcc\xe8\x28\x89\xab\x5d\x6c\xdd\x65\x69\x1e\x16\x52\xb1\xdf\x6d\x89\xc3\x3b\x7d\x01\x24\xd4\x1c\xc4\x99\x36\x4c\x3a\x03\xe0\xb8\x30\x53\x78\xe1\xc3\xe4\xa1\x49\x2f\x99\x29\xea\x2c\x24\xfc\xa4\x53\xb5\x68\xdd\x3d\xd3\xa6\x9f\x29\x5c\xfb\xe2\xd6\x97\x15\xd4\xf4\x42\x22\x3d\x30\x3c\x4f\xf7\xe6\xa6\xba\xeb\x58\xe9\x2a\xe4\x49\x87\xc4\xda\xc3\xbe\x60\x56\x6c\x27\xdb\x5c\x21\x31\x08\x04\x12\x72\x70\x61\x5e\x32\x6d\x22\xcf\xbd\xbb\x72\x05\x60\x88\x5a\xa2\x49\x83\xff\x26\x99\xac\xcd\x34\xe3\x44\xdc\x06\x73\x0b\x67\x33\xbc\x93\xf7\xe9\x9a\x10\xb0\xcc\x90\x52\xa4\xc0\x84\x91\x4e\x23\x4d\x07\x02\x86\x76\xb0\x60\x1c\x5d\x91\xea\x7c\x42\x0c\xac\x36\xad\xc6\x47\x51\x92\xa9\x78\xfe\x46\x56\x9b\xb0\x22\xda\xa0\xdb\x6a\x09\x6a\x57\x8b\x76\xd8\x48\x26\x57\x08\xbe\xea\xcd\xe4\x1d\x10\x41\x61\xc1\x14\x02\x59\x93\xcd\x57\x49\x4c\xdd\x1d\xa5\x95\xe3\x1f\x57\x66\x73\xb3\xfd\xa2\x34\xd9\x79\x47\x49\x6e\x4f\x2a\xb2\x61\xda\x29\x91\x39\xa9\xc7\x66\x8d\x68\xbe\xb6\x21\x39\xfd\xc1\x23\x64\x62\x79\x7e\x35\xf1\x91\xc6\x3e\x58\xf4\xe7\x57\x13\x2b\xe1\xf3\xab\xc9\xe4\x6e\xf2\xc4\xbf\xf3\xab\x89\x14\xe7\x57\x13\x53\xe0\xf9\xd5\xe4\xfc\xea\xba\x1f\xa3\xfc\x4c\x6b\x1d\x16\x0a\xb5\xa5\xd6\x86\xae\x87\x4c\xcc\xb1\xfb\x21\x1b\x73\x06\x72\x6c\x61\x1a\x86\xba\x56\x4a\xd6\xc2\x56\x3b\x60\xcf\xfc\x24\x2b\x3b\x12\xa3\xae\xab\x4a\x2a\x13\xf5\xc5\x49\xec\xfd\x96\xa3\x8e\x6f\x26\x2f\x6f\x5e\x3d\xca\xbe\xb3\x58\x77\x86\xff\x73\xab\x5d\xba\xb0\x19\x56\xbc\xd6\xb6\xb4\x64\xf6\x4e\xf7\x45\x99\xb0\x8f\xeb\xf0\x3d\xaf\xf5\x18\xaa\x3a\xe3\x4c\x17\x40\x40\xe0\x1a\x12\x6d\x94\x14\xcb\xb9\x9b\xcd\x93\xb8\x19\x42\x25\xb5\xf9\x83\x11\xe7\x0f\x99\x83\xa5\xf7\xff\x14\x74\x16\x4d\xaa\xfb\xa2\x54\xd6\xe6\xdf\x2f\x55\x5f\x47\xee\xbb\x5e\xaf\xa3\x56\x92\xce\x77\x0b\xe4\x55\x6c\xab\x91\x5a\x30\xb3\x89\x7d\x14\x94\x22\xfe\x9a\xd1\xf4\xea\xe6\xea\xd5\xab\xab\x17\xff\x72\xf3\xf2\xe5\xd5\xcd\x8b\x97\x0f\x39\x76\x67\x14\x1f\xef\xd7\x5d\xed\xc9\x7b\x35\xdf\x3f\x64\x0d\x39\x11\x60\x14\xc9\x6f\xbd\x10\x6a\xa5\xac\x10\x2a\xf4\xe7\xef\x4a\xab\x0c\xb9\x5c\x3b\x10\x4f\x67\xc1\x90\xbb\x3a\x4b\x23\x42\x21\xd7\x50\xd6\xb9\x93\xb5\x2d\xa7\xd0\x2e\xac\x09\x33\x50\x0b\xc3\xb8\x57\x81\xa9\x95\xab\xc6\x70\xaf\x1a\x3a\xba\x6d\x27\x58\xce\xdf\xd9\x1c\x7d\x54\x84\x76\xf7\x64\x50\xf8\xc6\x83\x43\xa5\xa4\xc1\xdc\xca\x11\xc8\x92\x30\xa1\xad\x04\x5c\xbd\x85\xe5\x13\xee\xd1\xdd\x53\xf3\xb0\xeb\x09\xbb\xe5\x38\x86\x6f\xb9\xcc\x08\x87\x95\x75\x85\x8c\xdb\x02\x5a\x42\x21\xed\xd1\x7b\xd2\xd2\x86\x98\x5a\x83\x5c\xb8\x59\xcf\xb9\xdd\xbf\x22\xca\x56\xa9\x58\x56\x06\xd2\xa6\xa3\x69\xe7\x34\xaa\x55\xd3\xa7\xb5\x43\xc3\x50\xed\xad\x77\x52\x4f\xe1\xe7\x5f\x67\xcf\x1a\x56\xfe\x82\x0b\x26\x6c\xc6\x5d\xd4\xc2\x1f\xd9\x14\xc4\x34\x15\x95\x86\x9c\x4b\x5d\x2b\xcf\x21\x55\xb2\x02\xcb\x65\x8b\xa9\xc5\x6c\x17\x2a\x47\xad\x45\x32\x2c\x88\x2e\x46\x4d\x43\x56\xa1\xd3\x52\xb7\xd6\xce\x9f\x2d\xa4\x82\xa1\x45\xc0\xd2\xc9\x0c\x58\xd2\xe2\x8d\x38\x8a\xa5\x29\x66\xc0\x2e\x2e\x3a\xe0\x33\xb6\x80\x61\x0b\xf1\x33\xfb\x35\x32\x77\x91\xa5\x02\x69\x0a\x7d\x6a\x8e\x60\x83\x47\x57\x9c\xe5\x38\x64\x63\xb8\x1c\xcd\xda\xd5\x4c\x21\xb9\x6d\x47\x8d\x1e\xfd\x3f\xf7\xbb\x9d\xed\x4b\xc6\x09\x7f\x4f\x36\xbe\xdb\xa2\x81\xb8\x22\x0e\x6a\xc5\xa1\xf1\x19\xaf\x82\x4e\x21\x0e\xae\x2f\x95\x23\xbb\x6c\x1e\x1a\x9b\x6a\x8f\xe0\xd1\x44\x1a\x05\x1d\xfe\xdb\x8f\x7f\xff\x2e\xd2\x46\x31\xb1\x64\x8b\xcd\xf0\xbe\x56\x7c\x0a\xcf\x87\xc1\x3f\xd5\x8a\x07\xa3\x9f\x27\xbf\x46\x2b\xc2\x6b\x1c\x3b\x7d\x4f\xdd\xef\x11\x95\x31\x34\x8f\x53\xd8\x27\xb8\x1d\x8d\x66\xa7\x3b\x53\xbd\x46\x9a\x42\x8d\x66\x68\x01\x3b\xc3\x3f\x94\x11\x81\x12\x4d\x21\x9d\xeb\x2a\xcc\xa5\x10\x98\x1b\xa8\x2b\x29\x1a\x91\x00\x97\x5a\xef\x0c\xb1\x85\x48\x8f\x8d\xc2\x6a\xb9\xb5\xee\x73\xb8\xb2\xda\x9d\x74\xaa\x6d\x90\xa5\x2e\x48\xff\x27\x66\x3f\xca\xfc\x16\xcd\x30\x58\x6b\x1b\x1c\x03\xb8\x00\x2e\x73\x62\xf1\x45\x85\x0d\xd5\x17\x10\xc4\xa4\x62\x41\xa3\xfc\x2d\x20\xd7\xf8\x61\x64\x4f\xc2\xe5\x5f\x94\x78\x4e\x2f\x2e\xbc\x3f\xb5\x9a\x93\xa2\x44\xad\xc9\x12\xfb\x27\x74\x97\xd9\xee\x28\x56\x10\xa5\x5e\x42\x0a\x4e\xc3\x15\x51\x1a\x3d\x48\x44\x89\x21\xad\xb9\x5a\x71\x38\xb0\x34\x05\x51\x73\xbe\xb3\x72\xef\x55\xb3\xd6\x7e\xf7\xc0\x23\x9f\xe2\xbe\x4a\x53\xa8\x05\x75\x3a\xa2\xbb\x9d\xd6\x7a\x7c\xdf\x63\x14\xd9\x54\xb4\xdb\x31\x9a\xf5\xdd\x61\x0f\x1b\xd2\x0f\xa1\x43\x7a\x88\x0f\xe9\x03\x08\x5d\x9b\xe9\x31\x7c\xbe\x2d\xd5\x43\xe7\x26\x1e\xc0\x26\xea\x32\x43\xf5\x18\x3a\xdf\x66\x6a\xd0\x39\x51\xbf\x15\xa6\xb7\x77\x0c\x97\xaf\x46\x0f\x60\x47\xa5\xe4\x83\xc8\x85\x34\x9b\xe1\x3d\x27\x1b\x9b\x4f\x61\x60\x64\xf5\xc6\x75\x85\x06\x63\x97\xe4\xa7\xd0\x61\x18\xbb\x7e\xff\x14\x06\x6e\x64\xd7\x59\x89\x6e\xd7\xcb\xc9\x64\x32\x86\xf6\x45\xd9\xbf\x12\xeb\xc5\xaa\xc6\xed\x03\xfc\xe8\x3a\xcf\x6d\xad\xf1\x29\x1c\x35\x38\x3a\x9e\x9a\xf1\x27\x70\xd5\x25\x97\x3d\xb6\xe0\x4f\x7f\x82\xa3\xd5\x7d\x33\x8e\x63\xf8\x0f\xa2\x6e\x5d\x0f\xa7\x52\xb8\x72\x7d\x9e\x0e\xbe\x64\x5a\xbb\x36\x8a\x06\x2a\x05\x36\x7b\x3e\x2e\x6f\x1c\xf1\xd8\x80\xc1\x1c\x26\x87\x0c\xda\x78\xda\xcb\x2b\x27\xd2\x4d\x0f\xef\x7e\x26\x69\x25\x72\x22\x51\xb1\x12\xe1\xab\x14\x82\xa0\xbf\xf9\x08\xc2\x02\x74\xc8\xce\x34\x9a\x77\x5e\x17\xc3\x26\xbd\x9e\x4a\x7e\xa3\x31\x5c\x4f\x26\x93\xd1\x11\x13\xdb\x9d\x78\x5f\x57\xb6\xee\x02\x22\x36\x2e\xd2\x75\xb2\x75\x95\x9e\xad\xa1\x6c\x9c\xe3\x90\x4b\xce\x7d\xd1\xd3\x6c\xb5\x02\x6e\xfa\x5c\x29\x84\x97\xb3\x13\x69\xb8\x27\xc9\xde\xd1\x0e\xd5\x73\x42\xf6\x87\x2a\xda\x97\xd9\x01\x70\x78\xb9\xa7\x94\x3d\x7d\x9d\x56\xcc\x59\xc7\x37\xdb\x49\xf4\x40\x5d\x3b\x7d\x1d\xca\xac\xc7\xbf\xc7\x73\x71\xf9\xc4\x63\x74\xcb\x55\xad\x8b\xe1\x01\xa3\xa3\xd9\xb1\x6e\xde\x1a\x54\xc4\xa0\x7b\x75\xe1\x74\x81\xc2\xd8\x1a\xfb\x50\x25\xae\xfa\x56\x18\x2a\x14\x14\x55\x5b\x93\xf8\xcb\x84\xad\x20\xf7\x54\xe6\x6f\x1c\x7d\x73\xfa\x48\x87\x71\x35\x9d\x14\x08\x00\x70\xe0\x04\xce\x50\xf7\x2c\xd5\x02\x23\x27\x95\x46\x0a\x29\xf8\xef\x16\x86\xa3\xa8\x16\xec\x6e\x38\x0a\x9b\xf1\x21\x8e\x76\x7d\xd6\xdd\x2d\x5b\xb6\x2f\x52\x08\x12\xa3\x80\xd1\x74\x60\x93\xf0\xa9\x8a\xef\x02\x82\xc1\x7c\xc7\x41\x7f\x2b\x40\x62\xe8\xdc\xb5\xae\xfd\x35\xf1\x97\x20\x23\xf9\xed\xd2\xdd\xbd\xa6\xb6\x56\x1b\x1e\xa1\x25\x2b\x62\x88\x72\x58\x47\x33\xd8\x81\x37\x57\xd1\xdc\x2a\x67\x06\xfe\xc6\xeb\x3a\xe4\xd0\xbd\x55\x72\xa3\x4c\x2a\x8a\x2a\x54\x84\xb2\x5a\x4f\xe1\x45\x75\x37\xfb\xa5\x7d\xeb\xe6\xfa\xf8\x8f\xb2\x5a\x29\x9c\x1f\x71\xd4\xb4\x83\x2f\x20\x48\x62\x0b\xf0\x21\x34\xdd\x61\xfb\xdf\x4b\xc0\x89\xb7\x15\xd0\x7d\xcd\xd0\xcc\x97\x8c\x52\x8e\x96\xe1\x1d\x7a\xeb\x8c\x56\xff\x7d\x97\xda\x27\x09\xcd\x6b\x8a\xdd\x9e\xfd\xda\xea\xc4\x86\xee\x8d\xc7\xc0\x1a\x40\x68\x8f\xcc\x9c\xcc\x9b\x3e\x81\x9b\x56\x03\x27\x8b\xe6\xeb\x17\x5a\x2b\x57\x80\x0d\xc3\xc6\xc0\xc6\x30\xd0\xb6\x78\xa4\x7a\x30\x8a\x8a\xba\x24\x82\xfd\x8e\x43\x9b\x97\x46\x5e\x56\xee\x15\x4a\x70\x1c\x92\x8f\x98\xd9\xbd\xdb\x18\xb4\x39\x6e\xd0\x08\x71\xd0\x6a\xf7\xc5\xae\xa5\x30\x85\xc9\x6c\xf0\x91\x12\x3a\x4d\x25\xcc\x88\x82\xfe\x20\x6c\x93\x2f\x28\x69\xa9\xb7\x6b\x19\x51\x03\xdf\x2b\x71\x05\xbe\x90\xeb\x74\x70\x3d\xe9\x98\xf4\x8a\x76\x7a\x1e\x34\xb6\x76\xa4\x0c\xcb\x65\xeb\x9a\x73\xb8\x9e\x7c\x0e\x6e\x29\x11\x4b\x3c\x3c\x81\x51\xac\x42\x0a\x24\x37\x6c\x85\xff\x0b\x07\xf9\x0c\x42\xfe\x68\x16\xad\x1d\xb6\xc2\x73\x66\xba\xc7\xaf\x5d\xed\x64\xfb\xcf\xd6\xdf\x20\x76\x12\xbe\x80\xe0\xe4\x41\x1e\xb4\xc4\x03\xc0\x03\xd7\x7e\xd8\xef\xdd\x3b\xc1\xe0\x30\xa7\xd8\x6a\xb7\x7b\x9f\x3d\x8a\x0a\x53\xf2\x61\x90\x18\xf7\x5d\x93\xe5\xb9\xc3\xe0\x10\xf8\xe9\xfd\x92\x6e\xbb\x7f\x91\xc9\xb9\xd4\x78\x70\x51\x83\x5e\x71\xd2\x5d\xe6\xda\x4a\x04\xb6\xbb\xcf\xbf\xe2\x18\x7e\x34\x44\x19\x20\xf0\xd3\x5b\xa8\x2b\x4a\x8c\x7f\xfb\x66\xf3\xa3\xef\x48\xb6\xdf\x87\x65\x44\x69\x58\x48\xb5\x26\x8a\x36\x0d\x1e\x53\xe0\xc6\xbd\x7d\x6b\x4b\x3f\x8d\xe6\xad\x8d\x62\x2b\xc2\x87\x47\x17\xc7\xe7\xc3\x41\xd4\x57\xf9\x60\x14\x21\xc9\x8b\x63\x40\x97\xb1\x3a\xba\x29\x7c\xe7\xae\x00\xc3\xe7\x43\x53\x30\x3d\x8a\x88\x31\x6a\x38\xd8\x33\x86\xc1\xc8\xea\xf5\xb2\x77\x25\xeb\xb6\x27\x7b\x6e\xf5\x18\x8e\x5d\x31\xdd\x15\x02\x2d\x78\xae\xf5\xd0\xdb\xd5\x60\xdc\xc3\xbd\x6f\x56\x83\xf3\x41\xa7\xa8\x9d\x7b\xef\xce\x91\x9e\xe4\x64\x0f\xf5\xc0\x7a\xd9\xe0\x88\x3c\xa1\xf4\x8d\xf5\x9f\x61\x70\xc2\xd3\x0f\xad\x63\xd4\x09\xdb\xc7\xeb\x47\xa5\xec\xbf\xa4\x79\x40\xc4\x8c\x0e\x46\x91\xae\x33\xdf\xdc\x18\xbe\xec\x2e\x60\x2d\x98\x33\xde\xc3\x54\x70\x54\x50\x58\x12\xfb\x45\x45\x78\x50\x84\x3c\x92\x35\xda\xcb\xbc\x3b\xd5\x76\x6c\x05\x3e\x19\x75\xbd\xb1\x6f\xb4\x2d\xae\x7c\x5b\x78\x8d\x99\x76\x0d\x02\x68\xec\xdd\xb5\x83\x7c\xdb\xe7\xf5\xf7\x6f\x7b\xad\x9f\xce\x23\x86\x0e\x7b\xf7\xe9\xe6\xa9\x46\xcb\xc9\x6f\x45\xd7\xeb\x75\xe4\xdf\x76\xb8\x16\x6f\xd7\x89\x89\x49\xc5\xa2\xf7\x3a\x00\xa2\x37\x22\x07\x8a\x0b\x54\xf3\x1e\xfa\xa6\x3d\x93\xc4\xfe\x2b\xc6\x24\xf6\x1f\x6a\xff\x4f\x00\x00\x00\xff\xff\xf1\xa6\xb6\xb8\xb9\x2d\x00\x00")
+var _faucetHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x5a\x71\x73\xdb\xb6\x92\xff\xdb\xf9\x14\x5b\x5e\xfc\x24\x9d\x4d\x52\xb6\x93\x3c\x9f\x44\xaa\x93\x97\xd7\xd7\xcb\xcd\x5d\xdb\x69\xd3\xb9\x7b\xd3\xd7\xb9\x01\x89\x95\x88\x18\x04\x58\x00\x94\xac\x7a\xf4\xdd\x6f\x00\x90\x14\x29\xc9\x6e\xd2\xe4\xde\x34\x7f\x38\x24\xb0\xd8\x5d\xec\xfe\x16\xbb\x58\x2a\xf9\xe2\xaf\xdf\xbe\x79\xf7\xf7\xef\xbe\x82\xc2\x94\x7c\xf1\x2c\xb1\xff\x01\x27\x62\x95\x06\x28\x82\xc5\xb3\xb3\xa4\x40\x42\x17\xcf\xce\xce\x92\x12\x0d\x81\xbc\x20\x4a\xa3\x49\x83\xda\x2c\xc3\xdb\x60\x3f\x51\x18\x53\x85\xf8\x4b\xcd\xd6\x69\xf0\x3f\xe1\x8f\xaf\xc3\x37\xb2\xac\x88\x61\x19\xc7\x00\x72\x29\x0c\x0a\x93\x06\x6f\xbf\x4a\x91\xae\xb0\xb7\x4e\x90\x12\xd3\x60\xcd\x70\x53\x49\x65\x7a\xa4\x1b\x46\x4d\x91\x52\x5c\xb3\x1c\x43\xf7\x72\x09\x4c\x30\xc3\x08\x0f\x75\x4e\x38\xa6\x57\xc1\xe2\x99\xe5\x63\x98\xe1\xb8\x78\x78\x88\xbe\x41\xb3\x91\xea\x6e\xb7\x9b\xc1\xeb\xda\x14\x28\x0c\xcb\x89\x41\x0a\x7f\x23\x75\x8e\x26\x89\x3d\xa5\x5b\xc4\x99\xb8\x83\x42\xe1\x32\x0d\xac\xea\x7a\x16\xc7\x39\x15\xef\x75\x94\x73\x59\xd3\x25\x27\x0a\xa3\x5c\x96\x31\x79\x4f\xee\x63\xce\x32\x1d\x9b\x0d\x33\x06\x55\x98\x49\x69\xb4\x51\xa4\x8a\x6f\xa2\x9b\xe8\xcf\x71\xae\x75\xdc\x8d\x45\x25\x13\x51\xae\x75\x00\x0a\x79\x1a\x68\xb3\xe5\xa8\x0b\x44\x13\x40\xbc\xf8\x7d\x72\x97\x52\x98\x90\x6c\x50\xcb\x12\xe3\x17\xd1\x9f\xa3\xa9\x13\xd9\x1f\x7e\x5a\xaa\x15\xab\x73\xc5\x2a\x03\x5a\xe5\x1f\x2c\xf7\xfd\x2f\x35\xaa\x6d\x7c\x13\x5d\x45\x57\xcd\x8b\x93\xf3\x5e\x07\x8b\x24\xf6\x0c\x17\x9f\xc4\x3b\x14\xd2\x6c\xe3\xeb\xe8\x45\x74\x15\x57\x24\xbf\x23\x2b\xa4\xad\x24\x3b\x15\xb5\x83\x9f\x4d\xee\x63\x3e\x7c\x7f\xe8\xc2\xcf\x21\xac\x94\x25\x0a\x13\xbd\xd7\xf1\x75\x74\x75\x1b\x4d\xdb\x81\x63\xfe\x4e\x80\x75\x9a\x15\x75\x16\xad\x51\x59\xe4\xf2\x30\x47\x61\x50\xc1\x83\x1d\x3d\x2b\x99\x08\x0b\x64\xab\xc2\xcc\xe0\x6a\x3a\x3d\x9f\x9f\x1a\x5d\x17\x7e\x98\x32\x5d\x71\xb2\x9d\xc1\x92\xe3\xbd\x1f\x22\x9c\xad\x44\xc8\x0c\x96\x7a\x06\x9e\xb3\x9b\xd8\x39\x99\x95\x92\x2b\x85\x5a\x37\xc2\x2a\xa9\x99\x61\x52\xcc\x2c\xa2\x88\x61\x6b\x3c\x45\xab\x2b\x22\x8e\x16\x90\x4c\x4b\x5e\x1b\x3c\x50\x24\xe3\x32\xbf\xf3\x63\x2e\x9a\xfb\x9b\xc8\x25\x97\x6a\x06\x9b\x82\x35\xcb\xc0\x09\x82\x4a\x61\xc3\x1e\x2a\x42\x29\x13\xab\x19\xbc\xaa\x9a\xfd\x40\x49\xd4\x8a\x89\x19\x4c\xf7\x4b\x92\xb8\x35\x63\x12\xfb\x83\xeb\xd9\x59\x92\x49\xba\x75\x3e\xa4\x6c\x0d\x39\x27\x5a\xa7\xc1\x81\x89\xdd\x81\x34\x20\xb0\xe7\x10\x61\xa2\x9d\x1a\xcc\x29\xb9\x09\xc0\x09\x4a\x03\xaf\x44\x98\x49\x63\x64\x39\x83\x2b\xab\x5e\xb3\xe4\x80\x1f\x0f\xf9\x2a\xbc\xba\x6e\x27\xcf\x92\xe2\xaa\x65\x62\xf0\xde\x84\xce\x3f\x9d\x67\x82\x45\xc2\xda\xb5\x4b\x02\x4b\x12\x66\xc4\x14\x01\x10\xc5\x48\x58\x30\x4a\x51\xa4\x81\x51\x35\x5a\x1c\xb1\x05\xf4\x8f\xbf\x47\x4e\xbf\xe2\xaa\xd5\x2b\xa6\x6c\xdd\x6c\xab\xf7\x78\xb0\xc3\xc7\x37\x71\x0b\xcd\x83\x5c\x2e\x35\x9a\xb0\xb7\xa7\x1e\x31\x13\x55\x6d\xc2\x95\x92\x75\xd5\xcd\x9f\x25\x6e\x14\x18\x4d\x83\x5a\xf1\xa0\x39\xfe\xdd\xa3\xd9\x56\x8d\x29\x82\x6e\xe3\x52\x95\xa1\xf5\x84\x92\x3c\x80\x8a\x93\x1c\x0b\xc9\x29\xaa\x34\xf8\x41\xe6\x8c\x70\x10\x7e\xcf\xf0\xe3\xf7\xff\x09\x8d\xcb\x98\x58\xc1\x56\xd6\x0a\xbe\x32\x05\x2a\xac\x4b\x20\x94\x5a\xb8\x46\x51\xd4\x53\xc4\x61\xf7\x58\xd5\x30\x33\x62\x4f\x75\x96\x64\xb5\x31\xb2\x23\xcc\x8c\x80\xcc\x88\x90\xe2\x92\xd4\xdc\x00\x55\xb2\xa2\x72\x23\x42\x23\x57\x2b\x9b\xe9\xfc\x26\xfc\xa2\x00\x28\x31\xa4\x99\x4a\x83\x96\xb6\xf5\x21\xd1\x95\xac\xea\xaa\xf1\xa2\x1f\xc4\xfb\x8a\x08\x8a\xd4\xfa\x9c\x6b\x0c\x16\x5f\xb3\x35\x42\x89\x7e\x2f\x67\x87\x90\xc8\x89\x42\x13\xf6\x99\x1e\x01\x23\x89\xbd\x32\x7e\x4b\xd0\xfc\x4b\x6a\xde\x72\xea\xb6\x50\xa2\xa8\x61\xf0\x16\x2a\x7b\xae\x04\x8b\x87\x07\x45\xc4\x0a\xe1\x39\xa3\xf7\x97\xf0\x9c\x94\xb2\x16\x06\x66\x29\x44\xaf\xdd\xa3\xde\xed\x06\xdc\x01\x12\xce\x16\x09\x79\x0a\xde\x20\x45\xce\x59\x7e\x97\x06\x86\xa1\x4a\x1f\x1e\x2c\xf3\xdd\x6e\x0e\x0f\x0f\x6c\x09\xcf\xa3\xef\x31\x27\x95\xc9\x0b\xb2\xdb\xad\x54\xfb\x1c\xe1\x3d\xe6\xb5\xc1\xf1\xe4\xe1\x01\xb9\xc6\xdd\x4e\xd7\x59\xc9\xcc\xb8\x5d\x6e\xc7\x05\xdd\xed\xac\xce\x8d\x9e\xbb\x1d\xc4\x96\xa9\xa0\x78\x0f\xcf\xa3\xef\x50\x31\x49\x35\x78\xfa\x24\x26\x8b\x24\xe6\x6c\xd1\xac\x1b\x1a\x29\xae\xf9\x1e\x2f\xb1\x05\x4c\x87\x73\x17\x36\x4e\xd5\xbe\xa6\x27\xa2\x60\x15\x76\xda\x37\x78\xd0\xcc\xe0\x1d\x6e\xd3\xe0\xe1\xa1\xbf\xb6\x99\xcd\x09\xe7\x19\xb1\x76\xf1\x5b\xeb\x16\xfd\x8a\x16\xa7\x6b\xa6\x5d\x49\xb5\x68\x35\xd8\xab\xfd\x81\x61\x7d\x70\x70\x19\x59\xcd\xe0\xe6\xba\x77\x6a\x9d\x8a\xf8\x57\x07\x11\x7f\x73\x92\xb8\x22\x02\x39\xb8\xbf\xa1\x2e\x09\x6f\x9f\x9b\x68\xe9\x05\xdf\xe1\xa2\xd0\x9e\xd1\x9d\x6a\xdd\x59\x3f\x9d\x83\x5c\xa3\x5a\x72\xb9\x99\x01\xa9\x8d\x9c\x43\x49\xee\xbb\x7c\x77\x33\x9d\xf6\xf5\xb6\xa5\x20\xc9\x38\xba\xd3\x45\xe1\x2f\x35\x6a\xa3\xbb\xb3\xc4\x4f\xb9\xbf\xf6\x48\xa1\x28\x34\xd2\x03\x6b\x58\x89\xd6\xb4\x8e\xaa\xe7\xfa\xce\x98\x27\x75\x5f\x4a\xd9\xa5\x90\xbe\x1a\x0d\xeb\x5e\xb6\x0b\x16\x89\x51\x7b\xba\xb3\xc4\xd0\x8f\x4a\x01\xca\x96\x78\x8f\x65\x00\x7f\xa2\xd9\xbd\x57\x88\xca\xd7\x17\x16\xb2\xe0\x5e\x93\xd8\xd0\x4f\x90\x6c\x41\x98\x11\x8d\x1f\x22\xde\x65\xfa\xbd\x78\xf7\xfa\xa9\xf2\x0b\x24\xca\x64\x48\xcc\x87\x28\xb0\xac\x05\xed\xed\xdf\x9d\x9d\x9f\xaa\x40\x2d\xd8\x1a\x95\x66\x66\xfb\xa1\x1a\x20\xdd\xab\xe0\xdf\x87\x2a\x24\xb1\x51\x4f\x63\xad\xff\xf2\x99\x82\xfb\xb7\x4a\x92\x9b\xc5\xbf\xcb\x0d\x50\x89\x1a\x4c\xc1\x34\xd8\xe4\xfa\x65\x12\x17\x37\x1d\x49\xb5\x78\x67\x27\x9c\x51\x61\xe9\x4a\x0b\x60\x1a\x54\x2d\x5c\xe6\x95\x02\x4c\x81\xc3\x72\xa4\x49\xd2\x11\xbc\x93\xb6\xa4\x5b\xa3\x30\x50\x12\xce\x72\x26\x6b\x0d\x24\x37\x52\x69\x58\x2a\x59\x02\xde\x17\xa4\xd6\xc6\x32\xb2\xc7\x07\x59\x13\xc6\x5d\x2c\x39\x97\x82\x54\x40\xf2\xbc\x2e\x6b\x5b\x92\x8a\x15\xa0\x90\xf5\xaa\x68\x74\x31\x12\x7c\x62\xe2\x52\xac\x3a\x7d\x74\x45\x4a\x20\xc6\x90\xfc\x4e\x5f\x42\x7b\x2a\x00\x51\x08\x86\x21\xb5\xab\x72\x59\x96\x52\xc0\x8d\xa2\x50\x11\x65\xb6\xa0\x87\xb5\x05\xc9\x73\x97\xe5\x22\x78\x2d\xb6\x52\x20\x14\x64\xed\x34\x84\x77\xfe\x3a\x71\x09\x5f\x4b\xb9\xe2\x78\x61\x15\xfc\x1b\xc9\x31\x93\xb2\x5b\x06\x25\xd9\xb6\x72\x9b\x6d\x6c\x98\x29\x98\xb7\x53\x85\xaa\xb4\x3c\x28\x70\x56\x32\xa3\xa3\x24\xae\xf6\x47\xeb\x3e\x49\xf3\xb0\x90\x8a\xfd\x6a\x2b\x1c\xde\x3f\x47\xcd\xc1\x29\xd3\x1e\x92\xce\xfd\x1c\x97\x66\x06\x2f\xfc\x21\x79\x08\xe8\xe6\x2a\x74\x0a\xcd\x2d\x4f\x77\xc5\xb4\x99\x67\x06\x37\xbe\xae\xf5\x15\x05\x35\x3d\x0d\xe8\x01\xe6\xbc\xd0\xdb\xdb\xea\xbe\xd3\xa3\x2b\x8e\xa7\x1d\x13\x0b\x85\xa1\x51\xd6\xac\x67\xcf\x92\xdc\x21\x10\x48\xc8\xc1\x55\xb9\x51\xda\x5d\xb4\x98\x6b\x14\xc4\x66\x83\x68\xbe\xb4\x31\x9c\x7e\xef\x19\x32\xb1\x3a\xbf\x9e\x7a\x68\xda\x07\xcb\xfe\xfc\x7a\xca\x84\x91\xe7\xd7\xd3\xe9\xfd\xf4\x03\xff\x9d\x5f\x4f\xa5\x38\xbf\x9e\x9a\x02\xcf\xaf\xa7\xe7\xd7\x37\x7d\x50\xfb\x91\xb6\xc4\xb4\x54\xa8\xad\xb4\x16\xeb\x01\x18\xa2\x56\x68\xd2\xe0\x7f\x49\x26\x6b\x33\xcb\x38\x11\x77\xc1\xc2\xa9\x6b\xcb\x0e\x87\x82\xd3\x85\x2a\x54\x44\x5b\x48\x58\x8d\x1d\x4a\x9a\xa6\x88\x86\xb1\xae\x95\x92\xb5\xb0\xe9\x11\xec\x9e\x5d\xa8\x8a\x91\x45\x99\x35\xcc\x24\x4a\x32\x15\x2f\xde\xc8\x6a\x1b\x3a\x26\x6e\xf9\x91\x19\x75\x5d\x55\x52\x99\xa8\x6f\x4e\x62\x2f\x44\x1c\x75\x7c\x3b\x7d\x79\xfb\xea\x49\xf5\xb5\x2d\xb7\xdd\x1e\x3a\x0d\x49\x26\xd7\x08\xbe\xb8\xcf\xe4\x3d\x10\x41\x61\xc9\x14\x02\xd9\x90\xed\x17\x49\x4c\xdd\x55\xec\xd3\x51\xbb\x72\x81\x16\x56\xbc\xd6\xb6\x16\x61\x36\x50\xff\x50\x10\xf6\x27\x01\x7c\xc7\x6b\x7d\x09\x55\x9d\x71\xa6\x0b\x20\x20\x70\x03\x89\x36\x4a\x8a\xd5\xc2\x8d\xe6\xf6\xaa\xea\x5e\xa1\x92\xda\x3c\x85\x06\x2c\x33\xa4\xf4\x04\x1e\x7e\x27\x1c\xac\x3c\xe7\xc2\x7f\xbe\xfb\x96\xcd\xe1\xf8\x87\x72\x59\x7b\x62\xff\x51\xfd\x75\x14\xbe\x9b\xcd\x26\x6a\x2d\xe9\x62\xb7\x40\x5e\xc5\x36\x8d\xd5\x82\x99\x6d\xec\x4f\x41\x29\xe2\x2f\x19\x4d\xaf\x6f\xaf\x5f\xbd\xba\x7e\xf1\x6f\xb7\x2f\x5f\x5e\xdf\xbe\x78\xf9\x58\x60\x77\xa0\xf8\xfd\x71\xed\xaf\x43\xdf\xc8\xd7\xb5\x29\xba\xbb\x90\xc7\x4b\x5b\x83\xdb\x4a\x8b\xda\xbb\xa4\x0a\x7e\x37\x86\x6a\x61\x0b\xca\x90\xf0\x93\xb5\xe0\x47\xa0\xc8\xc1\xe8\x09\xcd\x3e\x11\x5a\x2d\x7c\x2c\x52\x64\x6d\xec\x0e\xdb\xa6\x0c\x93\xa2\x83\xd3\x25\x68\x56\x56\x7c\x0b\xf9\xde\xeb\xa7\x71\xf5\xa8\x53\x7e\x13\x56\x43\xb7\x79\x90\xb9\x2a\xae\x94\x14\x6d\xf5\xa6\x6b\x9d\x63\xe5\xba\xf5\xb6\x22\xfa\xcb\xf6\x57\x22\x0c\x13\xd8\x56\x4e\x11\x7c\x2b\xf8\x16\x6a\x8d\xb0\x94\x0a\x28\x66\xf5\x6a\xe5\xca\x3d\x05\x95\x62\x6b\x62\xb0\x2d\x97\x74\x83\x8a\x0e\x14\xbd\x1b\xaa\x2d\x5d\x79\xaf\x92\xfc\xbb\xac\x21\x27\x02\x8c\x22\xf9\x9d\x8f\x94\x5a\x29\x1b\x29\x15\xfa\xdd\x74\x05\x5b\x86\x5c\x6e\x1c\x89\xdf\xf7\x92\x21\x77\xd5\x9b\x46\x84\x42\x6e\xa0\xac\x73\x17\x90\xb6\x3a\x73\x9b\xd8\x10\x66\xa0\x16\x86\x71\x6f\x4f\x53\x2b\x61\x6b\x3d\x1c\x14\x59\x47\x77\xf8\x04\xcb\xc5\xbb\x02\x4f\x94\xb6\xdd\xed\x1b\x14\xbe\xf1\xe4\x50\x29\x69\x30\xb7\x0e\x05\xb2\x22\x4c\x68\xeb\x11\x57\xc6\x61\xf9\x01\xb7\xf3\xee\xa9\x79\xd8\x77\x9a\xdd\x74\x1c\xc3\xd7\x5c\x66\x84\xc3\xda\x22\x3d\xe3\xb6\x2c\x97\x50\x48\xbb\xf5\x9e\xb5\xb4\x21\xa6\xd6\x20\x97\x6e\xd4\x6b\x6e\xd7\xaf\x89\xb2\x1e\xc4\xb2\x32\x90\x36\x7d\x52\x3b\xa6\x51\xad\x9b\xee\xaf\x7d\x35\x0c\xd5\x60\xbe\xb3\x7a\x0a\x3f\xfd\x3c\x7f\xd6\xa8\xf2\x57\x5c\x3a\x48\x58\x7c\xfb\x2d\x9b\x82\x18\xc8\x15\x12\x83\x1a\x72\x2e\x75\xad\xbc\x86\x54\xc9\x0a\xac\x96\x2d\xa7\x96\xb3\x9d\xa8\x9c\xb4\x96\xc9\xb8\x20\xba\x98\x34\x6d\x5e\x85\xce\x4b\xdd\x5c\x3b\x7e\x66\x51\x37\xb6\x0c\x58\x3a\x9d\x03\x4b\x5a\xbe\x11\x47\xb1\x32\xc5\x1c\xd8\xc5\x45\x47\x7c\xc6\x96\x30\x6e\x29\x7e\x62\x3f\x47\xe6\x3e\xb2\x52\x20\x4d\xa1\x2f\xcd\x09\x6c\xf8\xe8\x8a\xb3\x1c\xc7\xec\x12\xae\x26\xf3\x76\x36\x53\x48\xee\xda\xb7\xc6\x8f\xfe\x3f\xf7\x77\x37\x1f\x5a\xc6\x19\x7f\x60\x1b\xdf\xc3\xd1\x40\x60\xc5\xb4\x81\x5a\x71\x68\x62\xd8\xbb\xa0\x73\x88\xa3\xeb\x5b\xe5\x08\x97\xcd\x43\x83\xa9\x76\x0b\x9e\x4d\xa4\x51\xd0\xf1\x7f\xfc\xf0\xed\x37\x91\x36\x8a\x89\x15\x5b\x6e\xc7\x0f\xb5\xe2\x33\x78\x3e\x0e\xfe\xa5\x56\x3c\x98\xfc\x34\xfd\x39\x5a\x13\x5e\xe3\xa5\xf3\xf7\xcc\xfd\x3d\x92\x72\x09\xcd\xe3\x0c\x86\x02\x77\x93\xc9\xfc\x74\xbf\xab\xd7\x9e\x53\xa8\xd1\x8c\x2d\x61\x07\xfc\x43\x1b\x11\x28\xd1\x14\xd2\x85\xae\xc2\x5c\x0a\x81\xb9\x81\xba\x92\xa2\x31\x09\x70\xa9\xf5\x1e\x88\x2d\x45\x7a\x0c\x8a\x86\x3e\x75\xc9\xfa\xbf\x31\xfb\x41\xe6\x77\x68\xc6\xe3\xf1\x86\x09\x2a\x37\x11\x97\xfe\xa8\x8d\x6c\x90\xca\x5c\x72\x48\xd3\x14\x9a\x2c\x1a\x4c\xe0\x4b\x08\x36\xda\xe6\xd3\x00\x66\xf6\xd1\x3e\x4d\xe0\x02\x0e\x97\x17\x36\xdf\x5f\x40\x10\x93\x8a\x05\x13\x1f\x0e\xad\xe1\xa5\x28\x51\x6b\xb2\xc2\xbe\x82\xee\x86\xdb\x81\xcc\xee\xa3\xd4\x2b\x48\xc1\x39\xa8\x22\x4a\xa3\x27\x89\x28\x31\xa4\x45\x9b\xc5\xac\x23\x4b\x53\x10\x35\xe7\x7b\x90\xfa\xa0\x98\xb7\xf0\x1b\x90\x47\x3e\xd7\x7c\x91\xa6\x50\x0b\xea\x4c\x4c\xf7\x2b\xad\xf3\x7d\x33\x64\x12\xd9\xbc\xb0\x5f\x31\x99\xf7\xd1\x3c\xe0\x86\xf4\xb7\xd8\x21\x3d\xe4\x87\xf4\x11\x86\xae\xf7\xf4\x14\x3f\xdf\xab\xea\xb1\x73\x03\x8f\x70\x13\x75\x99\xa1\x7a\x8a\x9d\xef\x3d\x35\xec\x9c\xa9\xdf\x0a\xd3\x5b\x7b\x09\x57\xaf\x26\x8f\x70\x47\xa5\xe4\xa3\xcc\x85\x34\xdb\xf1\x03\x27\x5b\x5b\x33\xc1\xc8\xc8\xea\x8d\x6b\x15\x8d\x2e\x5d\xc6\x9d\x41\xc7\xe1\xd2\x7d\x04\x98\xc1\xc8\xbd\xd9\x79\x56\xa2\x5b\xf5\x72\x3a\x9d\x5e\x42\xfb\xf5\xec\x2f\xc4\x06\xa1\xaa\x71\xf7\x88\x3e\xba\xce\x73\x9b\xf7\x3f\x45\xa3\x86\x47\xa7\x53\xf3\xfe\x09\x5a\x75\xb9\x61\xa0\x16\xfc\xe9\x4f\x70\x34\x3b\x84\x71\x1c\xc3\x7f\x11\x75\xe7\x1a\x3b\x95\xc2\xb5\x6b\xfe\x74\xf4\x25\xd3\xda\x35\x55\x34\x50\x29\xb0\x59\xf3\x71\xc7\xfe\x91\x8e\x0d\x19\x2c\x60\x7a\xa8\xa0\x3d\x0e\x7b\x69\xe1\x44\xb6\xe8\xf1\x1d\x26\x82\xb3\x5d\x5f\xde\x60\x25\x2b\x11\xbe\x48\x21\x08\xfa\x8b\x8f\x28\x2c\x41\xc7\xec\x4c\xa3\x79\xe7\x7d\x31\x6e\xb2\xe3\xa9\xdc\x35\xb9\x84\x9b\xe9\x74\x3a\x39\x52\x62\xb7\x37\xef\xeb\xca\x96\x4d\x40\xc4\xd6\x1d\x89\x9d\x6d\x5d\xe1\x68\x4b\x20\x7b\xa4\x71\xc8\x25\xe7\xbe\x66\x69\x96\x5a\x03\x37\x4d\xb0\x14\xc2\xab\xf9\x89\x2c\xda\xb3\x64\x6f\x6b\x87\xee\x39\x61\xfb\x43\x17\x0d\x6d\x76\x40\x1c\x5e\x0d\x9c\x32\xf0\xd7\x69\xc7\x9c\x75\x7a\xb3\xbd\x45\x0f\xdc\xb5\xf7\xd7\xa1\xcd\x7a\xfa\x7b\x3e\x17\x57\x1f\xb8\x8d\x6e\xba\xaa\x75\x31\x3e\x50\x74\x32\x3f\xf6\xcd\x5b\x83\xca\x56\xc9\xd2\xa6\x2c\xeb\x0b\x7b\x15\x50\x78\xe4\x12\x57\xaa\x2b\x0c\x15\x0a\x8a\xaa\x2d\x29\x7c\x65\x6f\x0b\xc0\x81\xcb\xfc\xad\xb2\x0f\xa7\x8f\x0c\x18\x57\x92\x49\x81\x00\x00\x07\x41\xe0\x80\x3a\x40\xaa\x25\x46\x4e\x2a\x8d\x14\x52\xf0\x3f\x66\x18\x4f\xa2\x5a\xb0\xfb\xf1\x24\x6c\xde\x0f\x79\xb4\xf3\xf3\xee\x9a\xd8\xaa\x7d\x91\x42\x90\x18\x05\x8c\xa6\xa3\x00\x2e\x4e\x85\xa0\xcd\xba\xa3\xc5\x5e\x83\xfe\x52\x80\xc4\xd0\x85\xeb\x67\xfb\xfb\xda\x3f\x82\x8c\xe4\x77\x2b\x77\x11\x9a\xd9\x52\x6b\x7c\xc4\x96\xac\x89\x21\xca\x71\x9d\xcc\x61\x4f\xde\x5c\x14\x73\xeb\x9c\x39\xf8\x1b\xa9\x6b\x9b\x43\xf7\xa9\xc9\xbd\x65\x52\x51\x54\xa1\x22\x94\xd5\x7a\x06\x2f\xaa\xfb\xf9\x3f\xda\x4f\x71\xae\xb9\xff\xa4\xaa\x95\xc2\xc5\x91\x46\x4d\x93\xf8\x02\x82\x24\xb6\x04\xbf\xc5\xa6\xdb\x6c\xff\x47\x14\x70\xe2\x13\x06\x74\x3f\x71\x68\xc6\x4b\x46\x29\x47\xab\xf0\x9e\xbd\x0d\x46\xeb\xff\x7e\x48\x0d\x45\x42\xf3\xed\x62\xbf\x66\x07\xc8\x35\x3e\xb1\xa0\xfb\x0c\x32\xb2\x00\x08\xed\x96\x99\xb3\x79\x73\xd9\x76\xc3\x6a\xe4\x6c\xd1\xfc\x24\x86\xd6\xca\xd5\x5a\xe3\xb0\x01\xd8\x25\x8c\xb4\xad\xfd\xa8\x1e\x4d\xa2\xa2\x2e\x89\x60\xbf\xe2\xd8\xe6\xa5\x89\xb7\x95\xfb\xae\x12\x1c\x1f\xc9\x47\xca\xec\x3f\x78\x8c\xda\x1c\x37\x6a\x8c\x38\x6a\xbd\xfb\x62\x7f\xb7\x9f\xc1\x74\x3e\xfa\x48\x0b\x9d\x96\x12\x66\x44\x41\xff\x25\x6c\x93\x2f\x28\x69\xa5\xb7\x73\x19\x51\x23\xdf\xc9\x70\xf5\xb9\x90\x9b\x74\x74\x33\xed\x94\xf4\x8e\x76\x7e\x1e\x35\x58\x3b\x72\x86\xd5\xb2\x0d\xcd\x05\xdc\x4c\x3f\x87\xb6\xbe\x1b\x72\xb0\x03\xa3\x58\x85\x14\x48\x6e\xd8\x1a\xff\x1f\x36\xf2\x19\x8c\xfc\xd1\x2a\x5a\x1c\xb6\xc6\x73\x30\x1d\xe8\x6b\x67\x3b\xdb\xfe\xab\x8d\x37\x88\x9d\x85\x2f\x20\x38\xb9\x91\x47\x91\x78\x40\x78\x10\xda\x8f\xc7\xbd\xfb\x50\x18\x1c\xe6\x14\x5b\xed\x76\x1f\xb9\x27\x51\x61\x4a\x3e\x0e\x12\xe3\x7e\xec\x64\x75\xee\x38\x38\x06\x7e\x78\x58\xd2\xed\x86\x17\x19\x7b\x7f\xc7\x83\x7b\x16\xf4\x8a\x93\xee\x2e\xd6\x56\x22\xb0\xdb\xff\x26\x2c\x8e\xe1\x07\x43\x94\x01\x02\x3f\xbe\x85\xba\xa2\xc4\xf8\x4f\x72\x36\x3f\xfa\xae\x73\xfb\xa3\xb1\x8c\x28\x0d\x4b\xa9\x36\x44\xd1\xa6\x3f\x63\x0a\xdc\xba\x4f\x72\x6d\xe9\xa7\xd1\xbc\xb5\xa7\xd8\x9a\xf0\xf1\xd1\xbd\xef\xf9\x78\x14\xf5\x5d\x3e\x9a\x44\x48\xf2\xe2\x98\xd0\x65\xac\x4e\x6e\x0a\xdf\xb8\x2b\xc0\xf8\xf9\xd8\x14\x4c\x4f\x22\x62\x8c\x1a\x8f\x06\x60\x18\x4d\xac\x5f\xaf\x7a\x57\xb2\x6e\x79\x32\x08\xab\xa7\x78\xec\x8b\xe9\xae\x10\x68\xc9\x73\xad\xc7\x1e\x57\xa3\xcb\x1e\xef\x21\xac\x46\xe7\xa3\xce\x51\xfb\xf0\xde\xef\x23\x3d\xa9\xc9\x80\xf5\xc8\x46\xd9\xe8\x48\x3c\xa1\xf4\x8d\x8d\x9f\x71\x70\x22\xd2\x0f\xd1\x31\xe9\x8c\xed\xcf\xeb\x27\xad\xec\x7f\x5e\xf3\x88\x89\x19\x1d\x4d\x22\x5d\x67\xbe\x37\x31\x7e\xd9\x5d\xc0\x5a\x32\x07\xde\xc3\x54\x70\x54\x50\x58\x11\xc3\xa2\x22\x3c\x28\x42\x9e\xc8\x1a\x8d\x48\xbf\xab\xdd\xa5\x35\xf8\x74\xd2\xb5\xb6\xbe\xd2\xb6\xb8\xf2\xad\xff\x0d\x66\xda\x75\x12\xa0\xc1\xbb\xeb\xe6\xf8\xae\xcd\xeb\xef\xde\xf6\x3a\x37\x5d\x44\x8c\x1d\xf7\xee\xf7\x9c\xa7\xfa\x24\x27\x7f\x40\xba\xd9\x6c\x22\xff\x45\xcb\xb5\xf1\xbb\x46\x4a\x4c\x2a\x16\xbd\xd7\x01\x10\xbd\x15\x39\x50\x5c\xa2\x5a\xf4\xd8\x37\xdd\x95\x24\xf6\x3f\x6d\x4c\x62\xff\xeb\xed\xff\x0b\x00\x00\xff\xff\x56\xf8\xb5\xef\xce\x2d\x00\x00")
func faucetHtmlBytes() ([]byte, error) {
return bindataRead(
diff --git a/cmd/puppeth/genesis.go b/cmd/puppeth/genesis.go
new file mode 100644
index 000000000..5e36f7fce
--- /dev/null
+++ b/cmd/puppeth/genesis.go
@@ -0,0 +1,379 @@
+// Copyright 2017 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package main
+
+import (
+ "encoding/binary"
+ "errors"
+ "math"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/consensus/ethash"
+ "github.com/ethereum/go-ethereum/core"
+ "github.com/ethereum/go-ethereum/params"
+)
+
+// cppEthereumGenesisSpec represents the genesis specification format used by the
+// C++ Ethereum implementation.
+type cppEthereumGenesisSpec struct {
+ SealEngine string `json:"sealEngine"`
+ Params struct {
+ AccountStartNonce hexutil.Uint64 `json:"accountStartNonce"`
+ HomesteadForkBlock hexutil.Uint64 `json:"homesteadForkBlock"`
+ EIP150ForkBlock hexutil.Uint64 `json:"EIP150ForkBlock"`
+ EIP158ForkBlock hexutil.Uint64 `json:"EIP158ForkBlock"`
+ ByzantiumForkBlock hexutil.Uint64 `json:"byzantiumForkBlock"`
+ ConstantinopleForkBlock hexutil.Uint64 `json:"constantinopleForkBlock"`
+ NetworkID hexutil.Uint64 `json:"networkID"`
+ ChainID hexutil.Uint64 `json:"chainID"`
+ MaximumExtraDataSize hexutil.Uint64 `json:"maximumExtraDataSize"`
+ MinGasLimit hexutil.Uint64 `json:"minGasLimit"`
+ MaxGasLimit hexutil.Uint64 `json:"maxGasLimit"`
+ GasLimitBoundDivisor *hexutil.Big `json:"gasLimitBoundDivisor"`
+ MinimumDifficulty *hexutil.Big `json:"minimumDifficulty"`
+ DifficultyBoundDivisor *hexutil.Big `json:"difficultyBoundDivisor"`
+ DurationLimit *hexutil.Big `json:"durationLimit"`
+ BlockReward *hexutil.Big `json:"blockReward"`
+ } `json:"params"`
+
+ Genesis struct {
+ Nonce hexutil.Bytes `json:"nonce"`
+ Difficulty *hexutil.Big `json:"difficulty"`
+ MixHash common.Hash `json:"mixHash"`
+ Author common.Address `json:"author"`
+ Timestamp hexutil.Uint64 `json:"timestamp"`
+ ParentHash common.Hash `json:"parentHash"`
+ ExtraData hexutil.Bytes `json:"extraData"`
+ GasLimit hexutil.Uint64 `json:"gasLimit"`
+ } `json:"genesis"`
+
+ Accounts map[common.Address]*cppEthereumGenesisSpecAccount `json:"accounts"`
+}
+
+// cppEthereumGenesisSpecAccount is the prefunded genesis account and/or precompiled
+// contract definition.
+type cppEthereumGenesisSpecAccount struct {
+ Balance *hexutil.Big `json:"balance"`
+ Nonce uint64 `json:"nonce,omitempty"`
+ Precompiled *cppEthereumGenesisSpecBuiltin `json:"precompiled,omitempty"`
+}
+
+// cppEthereumGenesisSpecBuiltin is the precompiled contract definition.
+type cppEthereumGenesisSpecBuiltin struct {
+ Name string `json:"name,omitempty"`
+ StartingBlock hexutil.Uint64 `json:"startingBlock,omitempty"`
+ Linear *cppEthereumGenesisSpecLinearPricing `json:"linear,omitempty"`
+}
+
+type cppEthereumGenesisSpecLinearPricing struct {
+ Base uint64 `json:"base"`
+ Word uint64 `json:"word"`
+}
+
+// newCppEthereumGenesisSpec converts a go-ethereum genesis block into a Parity specific
+// chain specification format.
+func newCppEthereumGenesisSpec(network string, genesis *core.Genesis) (*cppEthereumGenesisSpec, error) {
+ // Only ethash is currently supported between go-ethereum and cpp-ethereum
+ if genesis.Config.Ethash == nil {
+ return nil, errors.New("unsupported consensus engine")
+ }
+ // Reconstruct the chain spec in Parity's format
+ spec := &cppEthereumGenesisSpec{
+ SealEngine: "Ethash",
+ }
+ spec.Params.AccountStartNonce = 0
+ spec.Params.HomesteadForkBlock = (hexutil.Uint64)(genesis.Config.HomesteadBlock.Uint64())
+ spec.Params.EIP150ForkBlock = (hexutil.Uint64)(genesis.Config.EIP150Block.Uint64())
+ spec.Params.EIP158ForkBlock = (hexutil.Uint64)(genesis.Config.EIP158Block.Uint64())
+ spec.Params.ByzantiumForkBlock = (hexutil.Uint64)(genesis.Config.ByzantiumBlock.Uint64())
+ spec.Params.ConstantinopleForkBlock = (hexutil.Uint64)(math.MaxUint64)
+
+ spec.Params.NetworkID = (hexutil.Uint64)(genesis.Config.ChainId.Uint64())
+ spec.Params.ChainID = (hexutil.Uint64)(genesis.Config.ChainId.Uint64())
+
+ spec.Params.MaximumExtraDataSize = (hexutil.Uint64)(params.MaximumExtraDataSize)
+ spec.Params.MinGasLimit = (hexutil.Uint64)(params.MinGasLimit.Uint64())
+ spec.Params.MaxGasLimit = (hexutil.Uint64)(math.MaxUint64)
+ spec.Params.MinimumDifficulty = (*hexutil.Big)(params.MinimumDifficulty)
+ spec.Params.DifficultyBoundDivisor = (*hexutil.Big)(params.DifficultyBoundDivisor)
+ spec.Params.GasLimitBoundDivisor = (*hexutil.Big)(params.GasLimitBoundDivisor)
+ spec.Params.DurationLimit = (*hexutil.Big)(params.DurationLimit)
+ spec.Params.BlockReward = (*hexutil.Big)(ethash.FrontierBlockReward)
+
+ spec.Genesis.Nonce = (hexutil.Bytes)(make([]byte, 8))
+ binary.LittleEndian.PutUint64(spec.Genesis.Nonce[:], genesis.Nonce)
+
+ spec.Genesis.MixHash = genesis.Mixhash
+ spec.Genesis.Difficulty = (*hexutil.Big)(genesis.Difficulty)
+ spec.Genesis.Author = genesis.Coinbase
+ spec.Genesis.Timestamp = (hexutil.Uint64)(genesis.Timestamp)
+ spec.Genesis.ParentHash = genesis.ParentHash
+ spec.Genesis.ExtraData = (hexutil.Bytes)(genesis.ExtraData)
+ spec.Genesis.GasLimit = (hexutil.Uint64)(genesis.GasLimit)
+
+ spec.Accounts = make(map[common.Address]*cppEthereumGenesisSpecAccount)
+ for address, account := range genesis.Alloc {
+ spec.Accounts[address] = &cppEthereumGenesisSpecAccount{
+ Balance: (*hexutil.Big)(account.Balance),
+ Nonce: account.Nonce,
+ }
+ }
+ spec.Accounts[common.BytesToAddress([]byte{1})].Precompiled = &cppEthereumGenesisSpecBuiltin{
+ Name: "ecrecover", Linear: &cppEthereumGenesisSpecLinearPricing{Base: 3000},
+ }
+ spec.Accounts[common.BytesToAddress([]byte{2})].Precompiled = &cppEthereumGenesisSpecBuiltin{
+ Name: "sha256", Linear: &cppEthereumGenesisSpecLinearPricing{Base: 60, Word: 12},
+ }
+ spec.Accounts[common.BytesToAddress([]byte{3})].Precompiled = &cppEthereumGenesisSpecBuiltin{
+ Name: "ripemd160", Linear: &cppEthereumGenesisSpecLinearPricing{Base: 600, Word: 120},
+ }
+ spec.Accounts[common.BytesToAddress([]byte{4})].Precompiled = &cppEthereumGenesisSpecBuiltin{
+ Name: "identity", Linear: &cppEthereumGenesisSpecLinearPricing{Base: 15, Word: 3},
+ }
+ if genesis.Config.ByzantiumBlock != nil {
+ spec.Accounts[common.BytesToAddress([]byte{5})].Precompiled = &cppEthereumGenesisSpecBuiltin{
+ Name: "modexp", StartingBlock: (hexutil.Uint64)(genesis.Config.ByzantiumBlock.Uint64()),
+ }
+ spec.Accounts[common.BytesToAddress([]byte{6})].Precompiled = &cppEthereumGenesisSpecBuiltin{
+ Name: "alt_bn128_G1_add", StartingBlock: (hexutil.Uint64)(genesis.Config.ByzantiumBlock.Uint64()), Linear: &cppEthereumGenesisSpecLinearPricing{Base: 500},
+ }
+ spec.Accounts[common.BytesToAddress([]byte{7})].Precompiled = &cppEthereumGenesisSpecBuiltin{
+ Name: "alt_bn128_G1_mul", StartingBlock: (hexutil.Uint64)(genesis.Config.ByzantiumBlock.Uint64()), Linear: &cppEthereumGenesisSpecLinearPricing{Base: 40000},
+ }
+ spec.Accounts[common.BytesToAddress([]byte{8})].Precompiled = &cppEthereumGenesisSpecBuiltin{
+ Name: "alt_bn128_pairing_product", StartingBlock: (hexutil.Uint64)(genesis.Config.ByzantiumBlock.Uint64()),
+ }
+ }
+ return spec, nil
+}
+
+// parityChainSpec is the chain specification format used by Parity.
+type parityChainSpec struct {
+ Name string `json:"name"`
+ Engine struct {
+ Ethash struct {
+ Params struct {
+ MinimumDifficulty *hexutil.Big `json:"minimumDifficulty"`
+ DifficultyBoundDivisor *hexutil.Big `json:"difficultyBoundDivisor"`
+ GasLimitBoundDivisor *hexutil.Big `json:"gasLimitBoundDivisor"`
+ DurationLimit *hexutil.Big `json:"durationLimit"`
+ BlockReward *hexutil.Big `json:"blockReward"`
+ HomesteadTransition uint64 `json:"homesteadTransition"`
+ EIP150Transition uint64 `json:"eip150Transition"`
+ EIP160Transition uint64 `json:"eip160Transition"`
+ EIP161abcTransition uint64 `json:"eip161abcTransition"`
+ EIP161dTransition uint64 `json:"eip161dTransition"`
+ EIP649Reward *hexutil.Big `json:"eip649Reward"`
+ EIP100bTransition uint64 `json:"eip100bTransition"`
+ EIP649Transition uint64 `json:"eip649Transition"`
+ } `json:"params"`
+ } `json:"Ethash"`
+ } `json:"engine"`
+
+ Params struct {
+ MaximumExtraDataSize hexutil.Uint64 `json:"maximumExtraDataSize"`
+ MinGasLimit *hexutil.Big `json:"minGasLimit"`
+ NetworkID hexutil.Uint64 `json:"networkID"`
+ MaxCodeSize uint64 `json:"maxCodeSize"`
+ EIP155Transition uint64 `json:"eip155Transition"`
+ EIP98Transition uint64 `json:"eip98Transition"`
+ EIP86Transition uint64 `json:"eip86Transition"`
+ EIP140Transition uint64 `json:"eip140Transition"`
+ EIP211Transition uint64 `json:"eip211Transition"`
+ EIP214Transition uint64 `json:"eip214Transition"`
+ EIP658Transition uint64 `json:"eip658Transition"`
+ } `json:"params"`
+
+ Genesis struct {
+ Seal struct {
+ Ethereum struct {
+ Nonce hexutil.Bytes `json:"nonce"`
+ MixHash hexutil.Bytes `json:"mixHash"`
+ } `json:"ethereum"`
+ } `json:"seal"`
+
+ Difficulty *hexutil.Big `json:"difficulty"`
+ Author common.Address `json:"author"`
+ Timestamp hexutil.Uint64 `json:"timestamp"`
+ ParentHash common.Hash `json:"parentHash"`
+ ExtraData hexutil.Bytes `json:"extraData"`
+ GasLimit hexutil.Uint64 `json:"gasLimit"`
+ } `json:"genesis"`
+
+ Nodes []string `json:"nodes"`
+ Accounts map[common.Address]*parityChainSpecAccount `json:"accounts"`
+}
+
+// parityChainSpecAccount is the prefunded genesis account and/or precompiled
+// contract definition.
+type parityChainSpecAccount struct {
+ Balance *hexutil.Big `json:"balance"`
+ Nonce uint64 `json:"nonce,omitempty"`
+ Builtin *parityChainSpecBuiltin `json:"builtin,omitempty"`
+}
+
+// parityChainSpecBuiltin is the precompiled contract definition.
+type parityChainSpecBuiltin struct {
+ Name string `json:"name,omitempty"`
+ ActivateAt uint64 `json:"activate_at,omitempty"`
+ Pricing *parityChainSpecPricing `json:"pricing,omitempty"`
+}
+
+// parityChainSpecPricing represents the different pricing models that builtin
+// contracts might advertise using.
+type parityChainSpecPricing struct {
+ Linear *parityChainSpecLinearPricing `json:"linear,omitempty"`
+ ModExp *parityChainSpecModExpPricing `json:"modexp,omitempty"`
+ AltBnPairing *parityChainSpecAltBnPairingPricing `json:"alt_bn128_pairing,omitempty"`
+}
+
+type parityChainSpecLinearPricing struct {
+ Base uint64 `json:"base"`
+ Word uint64 `json:"word"`
+}
+
+type parityChainSpecModExpPricing struct {
+ Divisor uint64 `json:"divisor"`
+}
+
+type parityChainSpecAltBnPairingPricing struct {
+ Base uint64 `json:"base"`
+ Pair uint64 `json:"pair"`
+}
+
+// newParityChainSpec converts a go-ethereum genesis block into a Parity specific
+// chain specification format.
+func newParityChainSpec(network string, genesis *core.Genesis, bootnodes []string) (*parityChainSpec, error) {
+ // Only ethash is currently supported between go-ethereum and Parity
+ if genesis.Config.Ethash == nil {
+ return nil, errors.New("unsupported consensus engine")
+ }
+ // Reconstruct the chain spec in Parity's format
+ spec := &parityChainSpec{
+ Name: network,
+ Nodes: bootnodes,
+ }
+ spec.Engine.Ethash.Params.MinimumDifficulty = (*hexutil.Big)(params.MinimumDifficulty)
+ spec.Engine.Ethash.Params.DifficultyBoundDivisor = (*hexutil.Big)(params.DifficultyBoundDivisor)
+ spec.Engine.Ethash.Params.GasLimitBoundDivisor = (*hexutil.Big)(params.GasLimitBoundDivisor)
+ spec.Engine.Ethash.Params.DurationLimit = (*hexutil.Big)(params.DurationLimit)
+ spec.Engine.Ethash.Params.BlockReward = (*hexutil.Big)(ethash.FrontierBlockReward)
+ spec.Engine.Ethash.Params.HomesteadTransition = genesis.Config.HomesteadBlock.Uint64()
+ spec.Engine.Ethash.Params.EIP150Transition = genesis.Config.EIP150Block.Uint64()
+ spec.Engine.Ethash.Params.EIP160Transition = genesis.Config.EIP155Block.Uint64()
+ spec.Engine.Ethash.Params.EIP161abcTransition = genesis.Config.EIP158Block.Uint64()
+ spec.Engine.Ethash.Params.EIP161dTransition = genesis.Config.EIP158Block.Uint64()
+ spec.Engine.Ethash.Params.EIP649Reward = (*hexutil.Big)(ethash.ByzantiumBlockReward)
+ spec.Engine.Ethash.Params.EIP100bTransition = genesis.Config.ByzantiumBlock.Uint64()
+ spec.Engine.Ethash.Params.EIP649Transition = genesis.Config.ByzantiumBlock.Uint64()
+
+ spec.Params.MaximumExtraDataSize = (hexutil.Uint64)(params.MaximumExtraDataSize)
+ spec.Params.MinGasLimit = (*hexutil.Big)(params.MinGasLimit)
+ spec.Params.NetworkID = (hexutil.Uint64)(genesis.Config.ChainId.Uint64())
+ spec.Params.MaxCodeSize = params.MaxCodeSize
+ spec.Params.EIP155Transition = genesis.Config.EIP155Block.Uint64()
+ spec.Params.EIP98Transition = math.MaxUint64
+ spec.Params.EIP86Transition = math.MaxUint64
+ spec.Params.EIP140Transition = genesis.Config.ByzantiumBlock.Uint64()
+ spec.Params.EIP211Transition = genesis.Config.ByzantiumBlock.Uint64()
+ spec.Params.EIP214Transition = genesis.Config.ByzantiumBlock.Uint64()
+ spec.Params.EIP658Transition = genesis.Config.ByzantiumBlock.Uint64()
+
+ spec.Genesis.Seal.Ethereum.Nonce = (hexutil.Bytes)(make([]byte, 8))
+ binary.LittleEndian.PutUint64(spec.Genesis.Seal.Ethereum.Nonce[:], genesis.Nonce)
+
+ spec.Genesis.Seal.Ethereum.MixHash = (hexutil.Bytes)(genesis.Mixhash[:])
+ spec.Genesis.Difficulty = (*hexutil.Big)(genesis.Difficulty)
+ spec.Genesis.Author = genesis.Coinbase
+ spec.Genesis.Timestamp = (hexutil.Uint64)(genesis.Timestamp)
+ spec.Genesis.ParentHash = genesis.ParentHash
+ spec.Genesis.ExtraData = (hexutil.Bytes)(genesis.ExtraData)
+ spec.Genesis.GasLimit = (hexutil.Uint64)(genesis.GasLimit)
+
+ spec.Accounts = make(map[common.Address]*parityChainSpecAccount)
+ for address, account := range genesis.Alloc {
+ spec.Accounts[address] = &parityChainSpecAccount{
+ Balance: (*hexutil.Big)(account.Balance),
+ Nonce: account.Nonce,
+ }
+ }
+ spec.Accounts[common.BytesToAddress([]byte{1})].Builtin = &parityChainSpecBuiltin{
+ Name: "ecrecover", Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 3000}},
+ }
+ spec.Accounts[common.BytesToAddress([]byte{2})].Builtin = &parityChainSpecBuiltin{
+ Name: "sha256", Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 60, Word: 12}},
+ }
+ spec.Accounts[common.BytesToAddress([]byte{3})].Builtin = &parityChainSpecBuiltin{
+ Name: "ripemd160", Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 600, Word: 120}},
+ }
+ spec.Accounts[common.BytesToAddress([]byte{4})].Builtin = &parityChainSpecBuiltin{
+ Name: "identity", Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 15, Word: 3}},
+ }
+ if genesis.Config.ByzantiumBlock != nil {
+ spec.Accounts[common.BytesToAddress([]byte{5})].Builtin = &parityChainSpecBuiltin{
+ Name: "modexp", ActivateAt: genesis.Config.ByzantiumBlock.Uint64(), Pricing: &parityChainSpecPricing{ModExp: &parityChainSpecModExpPricing{Divisor: 20}},
+ }
+ spec.Accounts[common.BytesToAddress([]byte{6})].Builtin = &parityChainSpecBuiltin{
+ Name: "alt_bn128_add", ActivateAt: genesis.Config.ByzantiumBlock.Uint64(), Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 500}},
+ }
+ spec.Accounts[common.BytesToAddress([]byte{7})].Builtin = &parityChainSpecBuiltin{
+ Name: "alt_bn128_mul", ActivateAt: genesis.Config.ByzantiumBlock.Uint64(), Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 40000}},
+ }
+ spec.Accounts[common.BytesToAddress([]byte{8})].Builtin = &parityChainSpecBuiltin{
+ Name: "alt_bn128_pairing", ActivateAt: genesis.Config.ByzantiumBlock.Uint64(), Pricing: &parityChainSpecPricing{AltBnPairing: &parityChainSpecAltBnPairingPricing{Base: 100000, Pair: 80000}},
+ }
+ }
+ return spec, nil
+}
+
+// pyEthereumGenesisSpec represents the genesis specification format used by the
+// Python Ethereum implementation.
+type pyEthereumGenesisSpec struct {
+ Nonce hexutil.Bytes `json:"nonce"`
+ Timestamp hexutil.Uint64 `json:"timestamp"`
+ ExtraData hexutil.Bytes `json:"extraData"`
+ GasLimit hexutil.Uint64 `json:"gasLimit"`
+ Difficulty *hexutil.Big `json:"difficulty"`
+ Mixhash common.Hash `json:"mixhash"`
+ Coinbase common.Address `json:"coinbase"`
+ Alloc core.GenesisAlloc `json:"alloc"`
+ ParentHash common.Hash `json:"parentHash"`
+}
+
+// newPyEthereumGenesisSpec converts a go-ethereum genesis block into a Parity specific
+// chain specification format.
+func newPyEthereumGenesisSpec(network string, genesis *core.Genesis) (*pyEthereumGenesisSpec, error) {
+ // Only ethash is currently supported between go-ethereum and pyethereum
+ if genesis.Config.Ethash == nil {
+ return nil, errors.New("unsupported consensus engine")
+ }
+ spec := &pyEthereumGenesisSpec{
+ Timestamp: (hexutil.Uint64)(genesis.Timestamp),
+ ExtraData: genesis.ExtraData,
+ GasLimit: (hexutil.Uint64)(genesis.GasLimit),
+ Difficulty: (*hexutil.Big)(genesis.Difficulty),
+ Mixhash: genesis.Mixhash,
+ Coinbase: genesis.Coinbase,
+ Alloc: genesis.Alloc,
+ ParentHash: genesis.ParentHash,
+ }
+ spec.Nonce = (hexutil.Bytes)(make([]byte, 8))
+ binary.LittleEndian.PutUint64(spec.Nonce[:], genesis.Nonce)
+
+ return spec, nil
+}
diff --git a/cmd/puppeth/module_dashboard.go b/cmd/puppeth/module_dashboard.go
index 1cf6cab79..b6d964696 100644
--- a/cmd/puppeth/module_dashboard.go
+++ b/cmd/puppeth/module_dashboard.go
@@ -18,10 +18,12 @@ package main
import (
"bytes"
+ "encoding/json"
"fmt"
"html/template"
"math/rand"
"path/filepath"
+ "strconv"
"strings"
"github.com/ethereum/go-ethereum/log"
@@ -76,25 +78,26 @@ var dashboardContent = `
<div id="sidebar-menu" class="main_menu_side hidden-print main_menu">
<div class="menu_section">
<ul class="nav side-menu">
- {{if .EthstatsPage}}<li><a onclick="load('//{{.EthstatsPage}}')"><i class="fa fa-tachometer"></i> Network Stats</a></li>{{end}}
- {{if .ExplorerPage}}<li><a onclick="load('//{{.ExplorerPage}}')"><i class="fa fa-database"></i> Block Explorer</a></li>{{end}}
- {{if .WalletPage}}<li><a onclick="load('//{{.WalletPage}}')"><i class="fa fa-address-book-o"></i> Browser Wallet</a></li>{{end}}
- {{if .FaucetPage}}<li><a onclick="load('//{{.FaucetPage}}')"><i class="fa fa-bath"></i> Crypto Faucet</a></li>{{end}}
- <li id="connect"><a><i class="fa fa-plug"></i> Connect Yourself</a>
+ {{if .EthstatsPage}}<li id="stats_menu"><a onclick="load('#stats')"><i class="fa fa-tachometer"></i> Network Stats</a></li>{{end}}
+ {{if .ExplorerPage}}<li id="explorer_menu"><a onclick="load('#explorer')"><i class="fa fa-database"></i> Block Explorer</a></li>{{end}}
+ {{if .WalletPage}}<li id="wallet_menu"><a onclick="load('#wallet')"><i class="fa fa-address-book-o"></i> Browser Wallet</a></li>{{end}}
+ {{if .FaucetPage}}<li id="faucet_menu"><a onclick="load('#faucet')"><i class="fa fa-bath"></i> Crypto Faucet</a></li>{{end}}
+ <li id="connect_menu"><a><i class="fa fa-plug"></i> Connect Yourself</a>
<ul id="connect_list" class="nav child_menu">
- <li><a onclick="$('#connect').removeClass('active'); $('#connect_list').toggle(); load('#connect-go-ethereum-geth')">Go Ethereum: Geth</a></li>
- <li><a onclick="$('#connect').removeClass('active'); $('#connect_list').toggle(); load('#connect-go-ethereum-mist')">Go Ethereum: Wallet & Mist</a></li>
- <li><a onclick="$('#connect').removeClass('active'); $('#connect_list').toggle(); load('#connect-go-ethereum-mobile')">Go Ethereum: Android & iOS</a></li>
+ <li><a onclick="$('#connect_menu').removeClass('active'); $('#connect_list').toggle(); load('#geth')">Go Ethereum: Geth</a></li>
+ <li><a onclick="$('#connect_menu').removeClass('active'); $('#connect_list').toggle(); load('#mist')">Go Ethereum: Wallet & Mist</a></li>
+ <li><a onclick="$('#connect_menu').removeClass('active'); $('#connect_list').toggle(); load('#mobile')">Go Ethereum: Android & iOS</a></li>{{if .Ethash}}
+ <li><a onclick="$('#connect_menu').removeClass('active'); $('#connect_list').toggle(); load('#other')">Other Ethereum Clients</a></li>{{end}}
</ul>
</li>
- <li><a onclick="load('#about')"><i class="fa fa-heartbeat"></i> About Puppeth</a></li>
+ <li id="about_menu"><a onclick="load('#about')"><i class="fa fa-heartbeat"></i> About Puppeth</a></li>
</ul>
</div>
</div>
</div>
</div>
- <div class="right_col" role="main" style="padding: 0">
- <div id="connect-go-ethereum-geth" hidden style="padding: 16px;">
+ <div class="right_col" role="main" style="padding: 0 !important">
+ <div id="geth" hidden style="padding: 16px;">
<div class="page-title">
<div class="title_left">
<h3>Connect Yourself &ndash; Go Ethereum: Geth</h3>
@@ -154,7 +157,7 @@ var dashboardContent = `
<p>Initial processing required to synchronize is light, as it only verifies the validity of the headers; similarly required disk capacity is small, tallying around 500 bytes per header. Low end machines with arbitrary storage, weak CPUs and 512MB+ RAM should cope well.</p>
<br/>
<p>To run a light node, download <a href="/{{.GethGenesis}}"><code>{{.GethGenesis}}</code></a> and start Geth with:
- <pre>geth --datadir=$HOME/.{{.Network}} --light init {{.GethGenesis}}</pre>
+ <pre>geth --datadir=$HOME/.{{.Network}} init {{.GethGenesis}}</pre>
<pre>geth --networkid={{.NetworkID}} --datadir=$HOME/.{{.Network}} --syncmode=light{{if .Ethstats}} --ethstats='{{.Ethstats}}'{{end}} --bootnodes={{.BootnodesLightFlat}}</pre>
</p>
<br/>
@@ -173,8 +176,8 @@ var dashboardContent = `
<p>Initial processing required to synchronize is light, as it only verifies the validity of the headers; similarly required disk capacity is small, tallying around 500 bytes per header. Embedded machines with arbitrary storage, low power CPUs and 128MB+ RAM may work.</p>
<br/>
<p>To run an embedded node, download <a href="/{{.GethGenesis}}"><code>{{.GethGenesis}}</code></a> and start Geth with:
- <pre>geth --datadir=$HOME/.{{.Network}} --light init {{.GethGenesis}}</pre>
- <pre>geth --networkid={{.NetworkID}} --datadir=$HOME/.{{.Network}} --cache=32 --syncmode=light{{if .Ethstats}} --ethstats='{{.Ethstats}}'{{end}} --bootnodes={{.BootnodesLightFlat}}</pre>
+ <pre>geth --datadir=$HOME/.{{.Network}} init {{.GethGenesis}}</pre>
+ <pre>geth --networkid={{.NetworkID}} --datadir=$HOME/.{{.Network}} --cache=16 --ethash.cachesinmem=1 --syncmode=light{{if .Ethstats}} --ethstats='{{.Ethstats}}'{{end}} --bootnodes={{.BootnodesLightFlat}}</pre>
</p>
<br/>
<p>You can download Geth from <a href="https://geth.ethereum.org/downloads/" target="about:blank">https://geth.ethereum.org/downloads/</a>.</p>
@@ -183,7 +186,7 @@ var dashboardContent = `
</div>
</div>
</div>
- <div id="connect-go-ethereum-mist" hidden style="padding: 16px;">
+ <div id="mist" hidden style="padding: 16px;">
<div class="page-title">
<div class="title_left">
<h3>Connect Yourself &ndash; Go Ethereum: Wallet &amp; Mist</h3>
@@ -235,7 +238,7 @@ var dashboardContent = `
</div>
</div>
</div>
- <div id="connect-go-ethereum-mobile" hidden style="padding: 16px;">
+ <div id="mobile" hidden style="padding: 16px;">
<div class="page-title">
<div class="title_left">
<h3>Connect Yourself &ndash; Go Ethereum: Android &amp; iOS</h3>
@@ -309,7 +312,101 @@ try! node?.start();
</div>
</div>
</div>
- </div>
+ </div>{{if .Ethash}}
+ <div id="other" hidden style="padding: 16px;">
+ <div class="page-title">
+ <div class="title_left">
+ <h3>Connect Yourself &ndash; Other Ethereum Clients</h3>
+ </div>
+ </div>
+ <div class="clearfix"></div>
+ <div class="row">
+ <div class="col-md-6">
+ <div class="x_panel">
+ <div class="x_title">
+ <h2>
+ <svg height="14px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 115 115"><path fill="#5C8DBC" d="M9.7 83.3V35.5s0-3.4 3.3-5.2c3.3-1.8 39.6-23.5 39.6-23.5s4.6-3.1 9.4 0c0 0 43.1 23.9 42.4 25.3L85.3 43.3s-3.6-8.4-13.1-13c-11.3-5.5-29.7-6.2-42.9 13.3 0 0-8.6 13.5.3 31.6l-19 10.7s-.9-.6-.9-2.6z"/><path fill="#5C8DBC" d="M71 51.3c-2.8-4.7-7.9-7.9-13.8-7.9-8.8 0-16 7.2-16 16 0 2.8.7 5.4 2 7.7L71 51.3z"/><path fill="#194674" d="M43.1 67c2.8 4.7 7.9 7.9 13.8 7.9 8.8 0 16-7.2 16-16 0-2.8-.7-5.4-2-7.7L43.1 67z"/><path fill="#1B598E" d="M104.4 32.1s1.3 52.6-.3 53.6L58 58.6l46.4-26.5z"/><path fill="#FFF" d="M90 57h-3.9v-4.1h-4.2V57h-4v4.1h4V65h4.2v-3.9H90zm13.6 0h-3.9v-4.1h-4.2V57h-4v4.1h4V65h4.2v-3.9h3.9z"/><path fill="#194674" d="M29.5 75.1s9.2 17 28.5 16.1 27.3-16.6 27.3-16.6L104 85.4s4.1.8-41.6 25.7c0 0-4.9 3.3-10.2 0 0 0-41.3-23.1-41.6-25.3l18.9-10.7z"/></svg>
+ C++ Ethereum <small>Official C++ client from the Ethereum Foundation</small>
+ </h2>
+ <div class="clearfix"></div>
+ </div>
+ <div class="x_content">
+ <p>C++ Ethereum is the third most popular of the Ethereum clients, focusing on code portability to a broad range of operating systems and hardware. The client is currently a full node with transaction processing based synchronization.</p>
+ <br/>
+ <p>To run a cpp-ethereum node, download <a href="/{{.CppGenesis}}"><code>{{.CppGenesis}}</code></a> and start the node with:
+ <pre>eth --config {{.CppGenesis}} --datadir $HOME/.{{.Network}} --peerset "{{.CppBootnodes}}"</pre>
+ </p>
+ <br/>
+ <p>You can find cpp-ethereum at <a href="https://github.com/ethereum/cpp-ethereum/" target="about:blank">https://github.com/ethereum/cpp-ethereum/</a>.</p>
+ </div>
+ </div>
+ </div>
+ <div class="col-md-6">
+ <div class="x_panel">
+ <div class="x_title">
+ <h2>
+ <svg height="14px" version="1.1" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M46.42,13.07S24.51,18.54,35,30.6c3.09,3.55-.81,6.75-0.81,6.75s7.84-4,4.24-9.11C35,23.51,32.46,21.17,46.42,13.07ZM32.1,16.88C45.05,6.65,38.4,0,38.4,0c2.68,10.57-9.46,13.76-13.84,20.34-3,4.48,1.46,9.3,7.53,14.77C29.73,29.77,21.71,25.09,32.1,16.88Z" transform="translate(-8.4)" fill="#e57125"/><path d="M23.6,49.49c-9.84,2.75,6,8.43,18.51,3.06a23.06,23.06,0,0,1-3.52-1.72,36.62,36.62,0,0,1-13.25.56C21.16,50.92,23.6,49.49,23.6,49.49Zm17-5.36a51.7,51.7,0,0,1-17.1.82c-4.19-.43-1.45-2.46-1.45-2.46-10.84,3.6,6,7.68,21.18,3.25A7.59,7.59,0,0,1,40.62,44.13ZM51.55,54.68s1.81,1.49-2,2.64c-7.23,2.19-30.1,2.85-36.45.09-2.28-1,2-2.37,3.35-2.66a8.69,8.69,0,0,1,2.21-.25c-2.54-1.79-16.41,3.51-7,5C37.15,63.67,58.17,57.67,51.55,54.68ZM42.77,39.12a20.42,20.42,0,0,1,2.93-1.57s-4.83.86-9.65,1.27A87.37,87.37,0,0,1,20.66,39c-7.51-1,4.12-3.77,4.12-3.77A22,22,0,0,0,14.7,37.61C8.14,40.79,31,42.23,42.77,39.12Zm2.88,7.77a1,1,0,0,1-.24.31C61.44,43,55.54,32.35,47.88,35a2.19,2.19,0,0,0-1,.79,9,9,0,0,1,1.37-.37C52.1,34.66,57.65,40.65,45.64,46.89Zm0.43,14.75a94.76,94.76,0,0,1-29.17.45s1.47,1.22,9,1.7c11.53,0.74,29.22-.41,29.64-5.86C55.6,57.94,54.79,60,46.08,61.65Z" transform="translate(-8.4)" fill="#5482a2"/></svg>
+ Ethereum Harmony<small>Third party Java client from EtherCamp</small>
+ </h2>
+ <div class="clearfix"></div>
+ </div>
+ <div class="x_content">
+ <p>Ethereum Harmony is a web user-interface based graphical Ethereum client built on top of the EthereumJ Java implementation of the Ethereum protocol. The client currently is a full node with state download based synchronization.</p>
+ <br/>
+ <p>To run an Ethereum Harmony node, download <a href="/{{.HarmonyGenesis}}"><code>{{.HarmonyGenesis}}</code></a> and start the node with:
+ <pre>./gradlew runCustom -DgenesisFile={{.HarmonyGenesis}} -Dpeer.networkId={{.NetworkID}} -Ddatabase.dir=$HOME/.harmony/{{.Network}} {{.HarmonyBootnodes}} </pre>
+ </p>
+ <br/>
+ <p>You can find Ethereum Harmony at <a href="https://github.com/ether-camp/ethereum-harmony/" target="about:blank">https://github.com/ether-camp/ethereum-harmony/</a>.</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="clearfix"></div>
+ <div class="row">
+ <div class="col-md-6">
+ <div class="x_panel">
+ <div class="x_title">
+ <h2>
+ <svg height="14px" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 104.56749 104.56675" version="1.1" viewbox="0 0 144 144" y="0px" x="0px"><metadata id="metadata10"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs id="defs8" /><path style="fill:#676767;" id="path2" d="m 49.0125,12.3195 a 3.108,3.108 0 0 1 6.216,0 3.108,3.108 0 0 1 -6.216,0 m -37.077,28.14 a 3.108,3.108 0 0 1 6.216,0 3.108,3.108 0 0 1 -6.216,0 m 74.153,0.145 a 3.108,3.108 0 0 1 6.216,0 3.108,3.108 0 0 1 -6.216,0 m -65.156,4.258 c 1.43,-0.635 2.076,-2.311 1.441,-3.744 l -1.379,-3.118 h 5.423 v 24.444 h -10.941 a 38.265,38.265 0 0 1 -1.239,-14.607 z m 22.685,0.601 v -7.205 h 12.914 c 0.667,0 4.71,0.771 4.71,3.794 0,2.51 -3.101,3.41 -5.651,3.41 z m -17.631,38.793 a 3.108,3.108 0 0 1 6.216,0 3.108,3.108 0 0 1 -6.216,0 m 46.051,0.145 a 3.108,3.108 0 0 1 6.216,0 3.108,3.108 0 0 1 -6.216,0 m 0.961,-7.048 c -1.531,-0.328 -3.037,0.646 -3.365,2.18 l -1.56,7.28 a 38.265,38.265 0 0 1 -31.911,-0.153 l -1.559,-7.28 c -0.328,-1.532 -1.834,-2.508 -3.364,-2.179 l -6.427,1.38 a 38.265,38.265 0 0 1 -3.323,-3.917 h 31.272 c 0.354,0 0.59,-0.064 0.59,-0.386 v -11.062 c 0,-0.322 -0.236,-0.386 -0.59,-0.386 h -9.146 v -7.012 h 9.892 c 0.903,0 4.828,0.258 6.083,5.275 0.393,1.543 1.256,6.562 1.846,8.169 0.588,1.802 2.982,5.402 5.533,5.402 h 16.146 a 38.265,38.265 0 0 1 -3.544,4.102 z m 17.365,-29.207 a 38.265,38.265 0 0 1 0.081,6.643 h -3.926 c -0.393,0 -0.551,0.258 -0.551,0.643 v 1.803 c 0,4.244 -2.393,5.167 -4.49,5.402 -1.997,0.225 -4.211,-0.836 -4.484,-2.058 -1.178,-6.626 -3.141,-8.041 -6.241,-10.486 3.847,-2.443 7.85,-6.047 7.85,-10.871 0,-5.209 -3.571,-8.49 -6.005,-10.099 -3.415,-2.251 -7.196,-2.702 -8.216,-2.702 h -40.603 a 38.265,38.265 0 0 1 21.408,-12.082 l 4.786,5.021 c 1.082,1.133 2.874,1.175 4.006,0.092 l 5.355,-5.122 a 38.265,38.265 0 0 1 26.196,18.657 l -3.666,8.28 c -0.633,1.433 0.013,3.109 1.442,3.744 z m 9.143,0.134 -0.125,-1.28 3.776,-3.522 c 0.768,-0.716 0.481,-2.157 -0.501,-2.523 l -4.827,-1.805 -0.378,-1.246 3.011,-4.182 c 0.614,-0.85 0.05,-2.207 -0.984,-2.377 l -5.09,-0.828 -0.612,-1.143 2.139,-4.695 c 0.438,-0.956 -0.376,-2.179 -1.428,-2.139 l -5.166,0.18 -0.816,-0.99 1.187,-5.032 c 0.24,-1.022 -0.797,-2.06 -1.819,-1.82 l -5.031,1.186 -0.992,-0.816 0.181,-5.166 c 0.04,-1.046 -1.184,-1.863 -2.138,-1.429 l -4.694,2.14 -1.143,-0.613 -0.83,-5.091 c -0.168,-1.032 -1.526,-1.596 -2.376,-0.984 l -4.185,3.011 -1.244,-0.377 -1.805,-4.828 c -0.366,-0.984 -1.808,-1.267 -2.522,-0.503 l -3.522,3.779 -1.28,-0.125 -2.72,-4.395 c -0.55,-0.89 -2.023,-0.89 -2.571,0 l -2.72,4.395 -1.281,0.125 -3.523,-3.779 c -0.714,-0.764 -2.156,-0.481 -2.522,0.503 l -1.805,4.828 -1.245,0.377 -4.184,-3.011 c -0.85,-0.614 -2.209,-0.048 -2.377,0.984 l -0.83,5.091 -1.143,0.613 -4.694,-2.14 c -0.954,-0.436 -2.178,0.383 -2.138,1.429 l 0.18,5.166 -0.992,0.816 -5.031,-1.186 c -1.022,-0.238 -2.06,0.798 -1.82,1.82 l 1.185,5.032 -0.814,0.99 -5.166,-0.18 c -1.042,-0.03 -1.863,1.183 -1.429,2.139 l 2.14,4.695 -0.613,1.143 -5.09,0.828 c -1.034,0.168 -1.594,1.527 -0.984,2.377 l 3.011,4.182 -0.378,1.246 -4.828,1.805 c -0.98,0.366 -1.267,1.807 -0.501,2.523 l 3.777,3.522 -0.125,1.28 -4.394,2.72 c -0.89,0.55 -0.89,2.023 0,2.571 l 4.394,2.72 0.125,1.28 -3.777,3.523 c -0.766,0.714 -0.479,2.154 0.501,2.522 l 4.828,1.805 0.378,1.246 -3.011,4.183 c -0.612,0.852 -0.049,2.21 0.985,2.376 l 5.089,0.828 0.613,1.145 -2.14,4.693 c -0.436,0.954 0.387,2.181 1.429,2.139 l 5.164,-0.181 0.816,0.992 -1.185,5.033 c -0.24,1.02 0.798,2.056 1.82,1.816 l 5.031,-1.185 0.992,0.814 -0.18,5.167 c -0.04,1.046 1.184,1.864 2.138,1.428 l 4.694,-2.139 1.143,0.613 0.83,5.088 c 0.168,1.036 1.527,1.596 2.377,0.986 l 4.182,-3.013 1.246,0.379 1.805,4.826 c 0.366,0.98 1.808,1.269 2.522,0.501 l 3.523,-3.777 1.281,0.128 2.72,4.394 c 0.548,0.886 2.021,0.888 2.571,0 l 2.72,-4.394 1.28,-0.128 3.522,3.777 c 0.714,0.768 2.156,0.479 2.522,-0.501 l 1.805,-4.826 1.246,-0.379 4.183,3.013 c 0.85,0.61 2.208,0.048 2.376,-0.986 l 0.83,-5.088 1.143,-0.613 4.694,2.139 c 0.954,0.436 2.176,-0.38 2.138,-1.428 l -0.18,-5.167 0.991,-0.814 5.031,1.185 c 1.022,0.24 2.059,-0.796 1.819,-1.816 l -1.185,-5.033 0.814,-0.992 5.166,0.181 c 1.042,0.042 1.866,-1.185 1.428,-2.139 l -2.139,-4.693 0.612,-1.145 5.09,-0.828 c 1.036,-0.166 1.598,-1.524 0.984,-2.376 l -3.011,-4.183 0.378,-1.246 4.827,-1.805 c 0.982,-0.368 1.269,-1.808 0.501,-2.522 l -3.776,-3.523 0.125,-1.28 4.394,-2.72 c 0.89,-0.548 0.891,-2.021 10e-4,-2.571 z" /></svg>
+ Parity<small>Third party Rust client from Parity Technologies</small>
+ </h2>
+ <div class="clearfix"></div>
+ </div>
+ <div class="x_content">
+ <p>Parity is a fast, light and secure Ethereum client, supporting both headless mode of operation as well as a web user interface for direct manual interaction. The client is currently a full node with transaction processing based synchronization and state pruning enabled.</p>
+ <br/>
+ <p>To run a Parity node, download <a href="/{{.ParityGenesis}}"><code>{{.ParityGenesis}}</code></a> and start the node with:
+ <pre>parity --chain={{.ParityGenesis}}</pre>
+ </p>
+ <br/>
+ <p>You can find Parity at <a href="https://parity.io/" target="about:blank">https://parity.io/</a>.</p>
+ </div>
+ </div>
+ </div>
+ <div class="col-md-6">
+ <div class="x_panel">
+ <div class="x_title">
+ <h2>
+ <svg height="14px" version="1.1" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><linearGradient id="a" x1="13.79" y1="38.21" x2="75.87" y2="-15.2" gradientTransform="matrix(0.56, 0, 0, -0.57, -8.96, 23.53)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#5c9fd3"/><stop offset="1" stop-color="#316a99"/></linearGradient><linearGradient id="b" x1="99.87" y1="-47.53" x2="77.7" y2="-16.16" gradientTransform="matrix(0.56, 0, 0, -0.57, -8.96, 23.53)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd43d"/><stop offset="1" stop-color="#fee875"/></linearGradient></defs><g><path d="M31.62,0a43.6,43.6,0,0,0-7.3.62c-6.46,1.14-7.63,3.53-7.63,7.94v5.82H32v1.94H11a9.53,9.53,0,0,0-9.54,7.74,28.54,28.54,0,0,0,0,15.52c1.09,4.52,3.68,7.74,8.11,7.74h5.25v-7a9.7,9.7,0,0,1,9.54-9.48H39.58a7.69,7.69,0,0,0,7.63-7.76V8.56c0-4.14-3.49-7.25-7.63-7.94A47.62,47.62,0,0,0,31.62,0ZM23.37,4.68A2.91,2.91,0,1,1,20.5,7.6,2.9,2.9,0,0,1,23.37,4.68Z" transform="translate(-0.35)" fill="url(#a)"/><path d="M49.12,16.32V23.1a9.79,9.79,0,0,1-9.54,9.68H24.33a7.79,7.79,0,0,0-7.63,7.76V55.08c0,4.14,3.6,6.57,7.63,7.76a25.55,25.55,0,0,0,15.25,0c3.84-1.11,7.63-3.35,7.63-7.76V49.26H32V47.32H54.85c4.44,0,6.09-3.1,7.63-7.74s1.53-9.38,0-15.52c-1.1-4.42-3.19-7.74-7.63-7.74H49.12ZM40.54,53.14A2.91,2.91,0,1,1,37.67,56,2.88,2.88,0,0,1,40.54,53.14Z" transform="translate(-0.35)" fill="url(#b)"/></g></svg>
+ PyEthApp<small>Official Python client from the Ethereum Foundation</small>
+ </h2>
+ <div class="clearfix"></div>
+ </div>
+ <div class="x_content">
+ <p>Pyethapp is the Ethereum Foundation's research client, aiming to provide an easily hackable and extendable codebase. The client is currently a full node with transaction processing based synchronization and state pruning enabled.</p>
+ <br/>
+ <p>To run a pyethapp node, download <a href="/{{.PythonGenesis}}"><code>{{.PythonGenesis}}</code></a> and start the node with:
+ <pre>mkdir -p $HOME/.config/pyethapp/{{.Network}}</pre>
+ <pre>pyethapp -c eth.genesis="$(cat {{.PythonGenesis}})" -c eth.network_id={{.NetworkID}} -c data_dir=$HOME/.config/pyethapp/{{.Network}} -c discovery.bootstrap_nodes="[{{.PythonBootnodes}}]" -c eth.block.HOMESTEAD_FORK_BLKNUM={{.Homestead}} -c eth.block.ANTI_DOS_FORK_BLKNUM={{.Tangerine}} -c eth.block.SPURIOUS_DRAGON_FORK_BLKNUM={{.Spurious}} -c eth.block.METROPOLIS_FORK_BLKNUM={{.Byzantium}} -c eth.block.DAO_FORK_BLKNUM=18446744073709551615 run --console</pre>
+ </p>
+ <br/>
+ <p>You can find pyethapp at <a href="https://github.com/ethereum/pyethapp/" target="about:blank">https://github.com/ethereum/pyethapp/</a>.</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>{{end}}
<div id="about" hidden>
<div class="row vertical-center">
<div style="margin: 0 auto;">
@@ -344,13 +441,33 @@ try! node?.start();
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gentelella/1.3.0/js/custom.min.js"></script>
<script>
- var load = function(url) {
- $("#connect-go-ethereum-geth").fadeOut(300)
- $("#connect-go-ethereum-mist").fadeOut(300)
- $("#connect-go-ethereum-mobile").fadeOut(300)
+ var load = function(hash) {
+ window.location.hash = hash;
+
+ // Fade out all possible pages (yes, ugly, no, don't care)
+ $("#geth").fadeOut(300)
+ $("#mist").fadeOut(300)
+ $("#mobile").fadeOut(300)
+ $("#other").fadeOut(300)
$("#about").fadeOut(300)
$("#frame-wrapper").fadeOut(300);
+ // Depending on the hash, resolve it into a local or remote URL
+ var url = hash;
+ switch (hash) {
+ case "#stats":
+ url = "//{{.EthstatsPage}}";
+ break;
+ case "#explorer":
+ url = "//{{.ExplorerPage}}";
+ break;
+ case "#wallet":
+ url = "//{{.WalletPage}}";
+ break;
+ case "#faucet":
+ url = "//{{.FaucetPage}}";
+ break;
+ }
setTimeout(function() {
if (url.substring(0, 1) == "#") {
$('.body').css({overflowY: 'auto'});
@@ -364,13 +481,10 @@ try! node?.start();
}
var resize = function() {
var sidebar = $($(".navbar")[0]).width();
- var content = 1920;
var limit = document.body.clientWidth - sidebar;
- var scale = limit / content;
+ var scale = limit / 1920;
- console.log(document.body.clientHeight);
-
- $("#frame-wrapper").width(content / scale);
+ $("#frame-wrapper").width(limit);
$("#frame-wrapper").height(document.body.clientHeight / scale);
$("#frame-wrapper").css({
transform: 'scale(' + (scale) + ')',
@@ -379,9 +493,17 @@ try! node?.start();
};
$(window).resize(resize);
- var item = $(".side-menu").children()[0];
- $(item).children()[0].click();
- $(item).addClass("active");
+ if (window.location.hash == "") {
+ var item = $(".side-menu").children()[0];
+ $(item).children()[0].click();
+ $(item).addClass("active");
+ } else {
+ load(window.location.hash);
+ var menu = $(window.location.hash + "_menu");
+ if (menu !== undefined) {
+ $(menu).addClass("active");
+ }
+ }
</script>
</body>
</html>
@@ -405,6 +527,10 @@ RUN \
echo '});' >> server.js
ADD {{.Network}}.json /dashboard/{{.Network}}.json
+ADD {{.Network}}-cpp.json /dashboard/{{.Network}}-cpp.json
+ADD {{.Network}}-harmony.json /dashboard/{{.Network}}-harmony.json
+ADD {{.Network}}-parity.json /dashboard/{{.Network}}-parity.json
+ADD {{.Network}}-python.json /dashboard/{{.Network}}-python.json
ADD index.html /dashboard/index.html
ADD puppeth.png /dashboard/puppeth.png
@@ -422,8 +548,12 @@ services:
build: .
image: {{.Network}}/dashboard{{if not .VHost}}
ports:
- - "{{.Port}}:80"{{else}}
+ - "{{.Port}}:80"{{end}}
environment:
+ - ETHSTATS_PAGE={{.EthstatsPage}}
+ - EXPLORER_PAGE={{.ExplorerPage}}
+ - WALLET_PAGE={{.WalletPage}}
+ - FAUCET_PAGE={{.FaucetPage}}{{if .VHost}}
- VIRTUAL_HOST={{.VHost}}{{end}}
logging:
driver: "json-file"
@@ -436,7 +566,7 @@ services:
// deployDashboard deploys a new dashboard container to a remote machine via SSH,
// docker and docker-compose. If an instance with the specified network name
// already exists there, it will be overwritten!
-func deployDashboard(client *sshClient, network string, port int, vhost string, services map[string]string, conf *config, ethstats bool) ([]byte, error) {
+func deployDashboard(client *sshClient, network string, conf *config, config *dashboardInfos, nocache bool) ([]byte, error) {
// Generate the content to upload to the server
workdir := fmt.Sprintf("%d", rand.Int63())
files := make(map[string][]byte)
@@ -449,37 +579,95 @@ func deployDashboard(client *sshClient, network string, port int, vhost string,
composefile := new(bytes.Buffer)
template.Must(template.New("").Parse(dashboardComposefile)).Execute(composefile, map[string]interface{}{
- "Network": network,
- "Port": port,
- "VHost": vhost,
+ "Network": network,
+ "Port": config.port,
+ "VHost": config.host,
+ "EthstatsPage": config.ethstats,
+ "ExplorerPage": config.explorer,
+ "WalletPage": config.wallet,
+ "FaucetPage": config.faucet,
})
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
statsLogin := fmt.Sprintf("yournode:%s", conf.ethstats)
- if !ethstats {
+ if !config.trusted {
statsLogin = ""
}
indexfile := new(bytes.Buffer)
+ bootCpp := make([]string, len(conf.bootFull))
+ for i, boot := range conf.bootFull {
+ bootCpp[i] = "required:" + strings.TrimPrefix(boot, "enode://")
+ }
+ bootHarmony := make([]string, len(conf.bootFull))
+ for i, boot := range conf.bootFull {
+ bootHarmony[i] = fmt.Sprintf("-Dpeer.active.%d.url=%s", i, boot)
+ }
+ bootPython := make([]string, len(conf.bootFull))
+ for i, boot := range conf.bootFull {
+ bootPython[i] = "'" + boot + "'"
+ }
template.Must(template.New("").Parse(dashboardContent)).Execute(indexfile, map[string]interface{}{
"Network": network,
- "NetworkID": conf.genesis.Config.ChainId,
+ "NetworkID": conf.Genesis.Config.ChainId,
"NetworkTitle": strings.Title(network),
- "EthstatsPage": services["ethstats"],
- "ExplorerPage": services["explorer"],
- "WalletPage": services["wallet"],
- "FaucetPage": services["faucet"],
+ "EthstatsPage": config.ethstats,
+ "ExplorerPage": config.explorer,
+ "WalletPage": config.wallet,
+ "FaucetPage": config.faucet,
"GethGenesis": network + ".json",
"BootnodesFull": conf.bootFull,
"BootnodesLight": conf.bootLight,
"BootnodesFullFlat": strings.Join(conf.bootFull, ","),
"BootnodesLightFlat": strings.Join(conf.bootLight, ","),
"Ethstats": statsLogin,
+ "Ethash": conf.Genesis.Config.Ethash != nil,
+ "CppGenesis": network + "-cpp.json",
+ "CppBootnodes": strings.Join(bootCpp, " "),
+ "HarmonyGenesis": network + "-harmony.json",
+ "HarmonyBootnodes": strings.Join(bootHarmony, " "),
+ "ParityGenesis": network + "-parity.json",
+ "PythonGenesis": network + "-python.json",
+ "PythonBootnodes": strings.Join(bootPython, ","),
+ "Homestead": conf.Genesis.Config.HomesteadBlock,
+ "Tangerine": conf.Genesis.Config.EIP150Block,
+ "Spurious": conf.Genesis.Config.EIP155Block,
+ "Byzantium": conf.Genesis.Config.ByzantiumBlock,
})
files[filepath.Join(workdir, "index.html")] = indexfile.Bytes()
- genesis, _ := conf.genesis.MarshalJSON()
+ // Marshal the genesis spec files for go-ethereum and all the other clients
+ genesis, _ := conf.Genesis.MarshalJSON()
files[filepath.Join(workdir, network+".json")] = genesis
+ if conf.Genesis.Config.Ethash != nil {
+ cppSpec, err := newCppEthereumGenesisSpec(network, conf.Genesis)
+ if err != nil {
+ return nil, err
+ }
+ cppSpecJSON, _ := json.Marshal(cppSpec)
+ files[filepath.Join(workdir, network+"-cpp.json")] = cppSpecJSON
+
+ harmonySpecJSON, _ := conf.Genesis.MarshalJSON()
+ files[filepath.Join(workdir, network+"-harmony.json")] = harmonySpecJSON
+
+ paritySpec, err := newParityChainSpec(network, conf.Genesis, conf.bootFull)
+ if err != nil {
+ return nil, err
+ }
+ paritySpecJSON, _ := json.Marshal(paritySpec)
+ files[filepath.Join(workdir, network+"-parity.json")] = paritySpecJSON
+
+ pyethSpec, err := newPyEthereumGenesisSpec(network, conf.Genesis)
+ if err != nil {
+ return nil, err
+ }
+ pyethSpecJSON, _ := json.Marshal(pyethSpec)
+ files[filepath.Join(workdir, network+"-python.json")] = pyethSpecJSON
+ } else {
+ for _, client := range []string{"cpp", "harmony", "parity", "python"} {
+ files[filepath.Join(workdir, network+"-"+client+".json")] = []byte{}
+ }
+ }
files[filepath.Join(workdir, "puppeth.png")] = dashboardMascot
// Upload the deployment files to the remote server (and clean up afterwards)
@@ -489,19 +677,36 @@ func deployDashboard(client *sshClient, network string, port int, vhost string,
defer client.Run("rm -rf " + workdir)
// Build and deploy the dashboard service
- return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build", workdir, network))
+ if nocache {
+ return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate", workdir, network, network))
+ }
+ return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network))
}
// dashboardInfos is returned from an dashboard status check to allow reporting
// various configuration parameters.
type dashboardInfos struct {
- host string
- port int
+ host string
+ port int
+ trusted bool
+
+ ethstats string
+ explorer string
+ wallet string
+ faucet string
}
-// String implements the stringer interface.
-func (info *dashboardInfos) String() string {
- return fmt.Sprintf("host=%s, port=%d", info.host, info.port)
+// Report converts the typed struct into a plain string->string map, containing
+// most - but not all - fields for reporting to the user.
+func (info *dashboardInfos) Report() map[string]string {
+ return map[string]string{
+ "Website address": info.host,
+ "Website listener port": strconv.Itoa(info.port),
+ "Ethstats service": info.ethstats,
+ "Explorer service": info.explorer,
+ "Wallet service": info.wallet,
+ "Faucet service": info.faucet,
+ }
}
// checkDashboard does a health-check against a dashboard container to verify if
@@ -536,7 +741,11 @@ func checkDashboard(client *sshClient, network string) (*dashboardInfos, error)
}
// Container available, assemble and return the useful infos
return &dashboardInfos{
- host: host,
- port: port,
+ host: host,
+ port: port,
+ ethstats: infos.envvars["ETHSTATS_PAGE"],
+ explorer: infos.envvars["EXPLORER_PAGE"],
+ wallet: infos.envvars["WALLET_PAGE"],
+ faucet: infos.envvars["FAUCET_PAGE"],
}, nil
}
diff --git a/cmd/puppeth/module_ethstats.go b/cmd/puppeth/module_ethstats.go
index 6ce662f65..20b7afe23 100644
--- a/cmd/puppeth/module_ethstats.go
+++ b/cmd/puppeth/module_ethstats.go
@@ -21,6 +21,7 @@ import (
"fmt"
"math/rand"
"path/filepath"
+ "strconv"
"strings"
"text/template"
@@ -30,21 +31,9 @@ import (
// ethstatsDockerfile is the Dockerfile required to build an ethstats backend
// and associated monitoring site.
var ethstatsDockerfile = `
-FROM mhart/alpine-node:latest
-
-RUN \
- apk add --update git && \
- git clone --depth=1 https://github.com/karalabe/eth-netstats && \
- apk del git && rm -rf /var/cache/apk/* && \
- \
- cd /eth-netstats && npm install && npm install -g grunt-cli && grunt
-
-WORKDIR /eth-netstats
-EXPOSE 3000
+FROM puppeth/ethstats:latest
RUN echo 'module.exports = {trusted: [{{.Trusted}}], banned: [{{.Banned}}], reserved: ["yournode"]};' > lib/utils/config.js
-
-CMD ["npm", "start"]
`
// ethstatsComposefile is the docker-compose.yml file required to deploy and
@@ -72,7 +61,7 @@ services:
// deployEthstats deploys a new ethstats container to a remote machine via SSH,
// docker and docker-compose. If an instance with the specified network name
// already exists there, it will be overwritten!
-func deployEthstats(client *sshClient, network string, port int, secret string, vhost string, trusted []string, banned []string) ([]byte, error) {
+func deployEthstats(client *sshClient, network string, port int, secret string, vhost string, trusted []string, banned []string, nocache bool) ([]byte, error) {
// Generate the content to upload to the server
workdir := fmt.Sprintf("%d", rand.Int63())
files := make(map[string][]byte)
@@ -110,7 +99,10 @@ func deployEthstats(client *sshClient, network string, port int, secret string,
defer client.Run("rm -rf " + workdir)
// Build and deploy the ethstats service
- return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build", workdir, network))
+ if nocache {
+ return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate", workdir, network, network))
+ }
+ return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network))
}
// ethstatsInfos is returned from an ethstats status check to allow reporting
@@ -123,9 +115,15 @@ type ethstatsInfos struct {
banned []string
}
-// String implements the stringer interface.
-func (info *ethstatsInfos) String() string {
- return fmt.Sprintf("host=%s, port=%d, secret=%s, banned=%v", info.host, info.port, info.secret, info.banned)
+// Report converts the typed struct into a plain string->string map, containing
+// most - but not all - fields for reporting to the user.
+func (info *ethstatsInfos) Report() map[string]string {
+ return map[string]string{
+ "Website address": info.host,
+ "Website listener port": strconv.Itoa(info.port),
+ "Login secret": info.secret,
+ "Banned addresses": fmt.Sprintf("%v", info.banned),
+ }
}
// checkEthstats does a health-check against an ethstats server to verify whether
diff --git a/cmd/puppeth/module_explorer.go b/cmd/puppeth/module_explorer.go
new file mode 100644
index 000000000..427134153
--- /dev/null
+++ b/cmd/puppeth/module_explorer.go
@@ -0,0 +1,211 @@
+// Copyright 2017 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "html/template"
+ "math/rand"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "github.com/ethereum/go-ethereum/log"
+)
+
+// explorerDockerfile is the Dockerfile required to run a block explorer.
+var explorerDockerfile = `
+FROM puppeth/explorer:latest
+
+ADD ethstats.json /ethstats.json
+ADD chain.json /chain.json
+
+RUN \
+ echo '(cd ../eth-net-intelligence-api && pm2 start /ethstats.json)' > explorer.sh && \
+ echo '(cd ../etherchain-light && npm start &)' >> explorer.sh && \
+ echo '/parity/parity --chain=/chain.json --port={{.NodePort}} --tracing=on --fat-db=on --pruning=archive' >> explorer.sh
+
+ENTRYPOINT ["/bin/sh", "explorer.sh"]
+`
+
+// explorerEthstats is the configuration file for the ethstats javascript client.
+var explorerEthstats = `[
+ {
+ "name" : "node-app",
+ "script" : "app.js",
+ "log_date_format" : "YYYY-MM-DD HH:mm Z",
+ "merge_logs" : false,
+ "watch" : false,
+ "max_restarts" : 10,
+ "exec_interpreter" : "node",
+ "exec_mode" : "fork_mode",
+ "env":
+ {
+ "NODE_ENV" : "production",
+ "RPC_HOST" : "localhost",
+ "RPC_PORT" : "8545",
+ "LISTENING_PORT" : "{{.Port}}",
+ "INSTANCE_NAME" : "{{.Name}}",
+ "CONTACT_DETAILS" : "",
+ "WS_SERVER" : "{{.Host}}",
+ "WS_SECRET" : "{{.Secret}}",
+ "VERBOSITY" : 2
+ }
+ }
+]`
+
+// explorerComposefile is the docker-compose.yml file required to deploy and
+// maintain a block explorer.
+var explorerComposefile = `
+version: '2'
+services:
+ explorer:
+ build: .
+ image: {{.Network}}/explorer
+ ports:
+ - "{{.NodePort}}:{{.NodePort}}"
+ - "{{.NodePort}}:{{.NodePort}}/udp"{{if not .VHost}}
+ - "{{.WebPort}}:3000"{{end}}
+ volumes:
+ - {{.Datadir}}:/root/.local/share/io.parity.ethereum
+ environment:
+ - NODE_PORT={{.NodePort}}/tcp
+ - STATS={{.Ethstats}}{{if .VHost}}
+ - VIRTUAL_HOST={{.VHost}}
+ - VIRTUAL_PORT=3000{{end}}
+ logging:
+ driver: "json-file"
+ options:
+ max-size: "1m"
+ max-file: "10"
+ restart: always
+`
+
+// deployExplorer deploys a new block explorer container to a remote machine via
+// SSH, docker and docker-compose. If an instance with the specified network name
+// already exists there, it will be overwritten!
+func deployExplorer(client *sshClient, network string, chainspec []byte, config *explorerInfos, nocache bool) ([]byte, error) {
+ // Generate the content to upload to the server
+ workdir := fmt.Sprintf("%d", rand.Int63())
+ files := make(map[string][]byte)
+
+ dockerfile := new(bytes.Buffer)
+ template.Must(template.New("").Parse(explorerDockerfile)).Execute(dockerfile, map[string]interface{}{
+ "NodePort": config.nodePort,
+ })
+ files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
+
+ ethstats := new(bytes.Buffer)
+ template.Must(template.New("").Parse(explorerEthstats)).Execute(ethstats, map[string]interface{}{
+ "Port": config.nodePort,
+ "Name": config.ethstats[:strings.Index(config.ethstats, ":")],
+ "Secret": config.ethstats[strings.Index(config.ethstats, ":")+1 : strings.Index(config.ethstats, "@")],
+ "Host": config.ethstats[strings.Index(config.ethstats, "@")+1:],
+ })
+ files[filepath.Join(workdir, "ethstats.json")] = ethstats.Bytes()
+
+ composefile := new(bytes.Buffer)
+ template.Must(template.New("").Parse(explorerComposefile)).Execute(composefile, map[string]interface{}{
+ "Datadir": config.datadir,
+ "Network": network,
+ "NodePort": config.nodePort,
+ "VHost": config.webHost,
+ "WebPort": config.webPort,
+ "Ethstats": config.ethstats[:strings.Index(config.ethstats, ":")],
+ })
+ files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
+
+ files[filepath.Join(workdir, "chain.json")] = chainspec
+
+ // Upload the deployment files to the remote server (and clean up afterwards)
+ if out, err := client.Upload(files); err != nil {
+ return out, err
+ }
+ defer client.Run("rm -rf " + workdir)
+
+ // Build and deploy the boot or seal node service
+ if nocache {
+ return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate", workdir, network, network))
+ }
+ return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network))
+}
+
+// explorerInfos is returned from a block explorer status check to allow reporting
+// various configuration parameters.
+type explorerInfos struct {
+ datadir string
+ ethstats string
+ nodePort int
+ webHost string
+ webPort int
+}
+
+// Report converts the typed struct into a plain string->string map, containing
+// most - but not all - fields for reporting to the user.
+func (info *explorerInfos) Report() map[string]string {
+ report := map[string]string{
+ "Data directory": info.datadir,
+ "Node listener port ": strconv.Itoa(info.nodePort),
+ "Ethstats username": info.ethstats,
+ "Website address ": info.webHost,
+ "Website listener port ": strconv.Itoa(info.webPort),
+ }
+ return report
+}
+
+// checkExplorer does a health-check against an block explorer server to verify
+// whether it's running, and if yes, whether it's responsive.
+func checkExplorer(client *sshClient, network string) (*explorerInfos, error) {
+ // Inspect a possible block explorer container on the host
+ infos, err := inspectContainer(client, fmt.Sprintf("%s_explorer_1", network))
+ if err != nil {
+ return nil, err
+ }
+ if !infos.running {
+ return nil, ErrServiceOffline
+ }
+ // Resolve the port from the host, or the reverse proxy
+ webPort := infos.portmap["3000/tcp"]
+ if webPort == 0 {
+ if proxy, _ := checkNginx(client, network); proxy != nil {
+ webPort = proxy.port
+ }
+ }
+ if webPort == 0 {
+ return nil, ErrNotExposed
+ }
+ // Resolve the host from the reverse-proxy and the config values
+ host := infos.envvars["VIRTUAL_HOST"]
+ if host == "" {
+ host = client.server
+ }
+ // Run a sanity check to see if the devp2p is reachable
+ nodePort := infos.portmap[infos.envvars["NODE_PORT"]]
+ if err = checkPort(client.server, nodePort); err != nil {
+ log.Warn(fmt.Sprintf("Explorer devp2p port seems unreachable"), "server", client.server, "port", nodePort, "err", err)
+ }
+ // Assemble and return the useful infos
+ stats := &explorerInfos{
+ datadir: infos.volumes["/root/.local/share/io.parity.ethereum"],
+ nodePort: nodePort,
+ webHost: host,
+ webPort: webPort,
+ ethstats: infos.envvars["STATS"],
+ }
+ return stats, nil
+}
diff --git a/cmd/puppeth/module_faucet.go b/cmd/puppeth/module_faucet.go
index 3c1296bdd..57b7a2dc0 100644
--- a/cmd/puppeth/module_faucet.go
+++ b/cmd/puppeth/module_faucet.go
@@ -18,6 +18,7 @@ package main
import (
"bytes"
+ "encoding/json"
"fmt"
"html/template"
"math/rand"
@@ -25,36 +26,24 @@ import (
"strconv"
"strings"
+ "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
// faucetDockerfile is the Dockerfile required to build an faucet container to
// grant crypto tokens based on GitHub authentications.
var faucetDockerfile = `
-FROM alpine:latest
-
-RUN mkdir /go
-ENV GOPATH /go
-
-RUN \
- apk add --update git go make gcc musl-dev ca-certificates linux-headers && \
- mkdir -p $GOPATH/src/github.com/ethereum && \
- (cd $GOPATH/src/github.com/ethereum && git clone --depth=1 https://github.com/ethereum/go-ethereum) && \
- go build -v github.com/ethereum/go-ethereum/cmd/faucet && \
- apk del git go make gcc musl-dev linux-headers && \
- rm -rf $GOPATH && rm -rf /var/cache/apk/*
+FROM ethereum/client-go:alltools-latest
ADD genesis.json /genesis.json
ADD account.json /account.json
ADD account.pass /account.pass
-EXPOSE 8080
-
-CMD [ \
- "/faucet", "--genesis", "/genesis.json", "--network", "{{.NetworkID}}", "--bootnodes", "{{.Bootnodes}}", "--ethstats", "{{.Ethstats}}", "--ethport", "{{.EthPort}}", \
- "--faucet.name", "{{.FaucetName}}", "--faucet.amount", "{{.FaucetAmount}}", "--faucet.minutes", "{{.FaucetMinutes}}", "--faucet.tiers", "{{.FaucetTiers}}", \
- "--github.user", "{{.GitHubUser}}", "--github.token", "{{.GitHubToken}}", "--account.json", "/account.json", "--account.pass", "/account.pass" \
- {{if .CaptchaToken}}, "--captcha.token", "{{.CaptchaToken}}", "--captcha.secret", "{{.CaptchaSecret}}"{{end}} \
+ENTRYPOINT [ \
+ "faucet", "--genesis", "/genesis.json", "--network", "{{.NetworkID}}", "--bootnodes", "{{.Bootnodes}}", "--ethstats", "{{.Ethstats}}", "--ethport", "{{.EthPort}}", \
+ "--faucet.name", "{{.FaucetName}}", "--faucet.amount", "{{.FaucetAmount}}", "--faucet.minutes", "{{.FaucetMinutes}}", "--faucet.tiers", "{{.FaucetTiers}}", \
+ "--account.json", "/account.json", "--account.pass", "/account.pass" \
+ {{if .CaptchaToken}}, "--captcha.token", "{{.CaptchaToken}}", "--captcha.secret", "{{.CaptchaSecret}}"{{end}}{{if .NoAuth}}, "--noauth"{{end}} \
]`
// faucetComposefile is the docker-compose.yml file required to deploy and maintain
@@ -76,10 +65,9 @@ services:
- FAUCET_AMOUNT={{.FaucetAmount}}
- FAUCET_MINUTES={{.FaucetMinutes}}
- FAUCET_TIERS={{.FaucetTiers}}
- - GITHUB_USER={{.GitHubUser}}
- - GITHUB_TOKEN={{.GitHubToken}}
- CAPTCHA_TOKEN={{.CaptchaToken}}
- - CAPTCHA_SECRET={{.CaptchaSecret}}{{if .VHost}}
+ - CAPTCHA_SECRET={{.CaptchaSecret}}
+ - NO_AUTH={{.NoAuth}}{{if .VHost}}
- VIRTUAL_HOST={{.VHost}}
- VIRTUAL_PORT=8080{{end}}
logging:
@@ -93,7 +81,7 @@ services:
// deployFaucet deploys a new faucet container to a remote machine via SSH,
// docker and docker-compose. If an instance with the specified network name
// already exists there, it will be overwritten!
-func deployFaucet(client *sshClient, network string, bootnodes []string, config *faucetInfos) ([]byte, error) {
+func deployFaucet(client *sshClient, network string, bootnodes []string, config *faucetInfos, nocache bool) ([]byte, error) {
// Generate the content to upload to the server
workdir := fmt.Sprintf("%d", rand.Int63())
files := make(map[string][]byte)
@@ -104,14 +92,13 @@ func deployFaucet(client *sshClient, network string, bootnodes []string, config
"Bootnodes": strings.Join(bootnodes, ","),
"Ethstats": config.node.ethstats,
"EthPort": config.node.portFull,
- "GitHubUser": config.githubUser,
- "GitHubToken": config.githubToken,
"CaptchaToken": config.captchaToken,
"CaptchaSecret": config.captchaSecret,
"FaucetName": strings.Title(network),
"FaucetAmount": config.amount,
"FaucetMinutes": config.minutes,
"FaucetTiers": config.tiers,
+ "NoAuth": config.noauth,
})
files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
@@ -123,13 +110,12 @@ func deployFaucet(client *sshClient, network string, bootnodes []string, config
"ApiPort": config.port,
"EthPort": config.node.portFull,
"EthName": config.node.ethstats[:strings.Index(config.node.ethstats, ":")],
- "GitHubUser": config.githubUser,
- "GitHubToken": config.githubToken,
"CaptchaToken": config.captchaToken,
"CaptchaSecret": config.captchaSecret,
"FaucetAmount": config.amount,
"FaucetMinutes": config.minutes,
"FaucetTiers": config.tiers,
+ "NoAuth": config.noauth,
})
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
@@ -144,7 +130,10 @@ func deployFaucet(client *sshClient, network string, bootnodes []string, config
defer client.Run("rm -rf " + workdir)
// Build and deploy the faucet service
- return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build", workdir, network))
+ if nocache {
+ return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate", workdir, network, network))
+ }
+ return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network))
}
// faucetInfos is returned from an faucet status check to allow reporting various
@@ -156,15 +145,38 @@ type faucetInfos struct {
amount int
minutes int
tiers int
- githubUser string
- githubToken string
+ noauth bool
captchaToken string
captchaSecret string
}
-// String implements the stringer interface.
-func (info *faucetInfos) String() string {
- return fmt.Sprintf("host=%s, api=%d, eth=%d, amount=%d, minutes=%d, tiers=%d, github=%s, captcha=%v, ethstats=%s", info.host, info.port, info.node.portFull, info.amount, info.minutes, info.tiers, info.githubUser, info.captchaToken != "", info.node.ethstats)
+// Report converts the typed struct into a plain string->string map, containing
+// most - but not all - fields for reporting to the user.
+func (info *faucetInfos) Report() map[string]string {
+ report := map[string]string{
+ "Website address": info.host,
+ "Website listener port": strconv.Itoa(info.port),
+ "Ethereum listener port": strconv.Itoa(info.node.portFull),
+ "Funding amount (base tier)": fmt.Sprintf("%d Ethers", info.amount),
+ "Funding cooldown (base tier)": fmt.Sprintf("%d mins", info.minutes),
+ "Funding tiers": strconv.Itoa(info.tiers),
+ "Captha protection": fmt.Sprintf("%v", info.captchaToken != ""),
+ "Ethstats username": info.node.ethstats,
+ }
+ if info.noauth {
+ report["Debug mode (no auth)"] = "enabled"
+ }
+ if info.node.keyJSON != "" {
+ var key struct {
+ Address string `json:"address"`
+ }
+ if err := json.Unmarshal([]byte(info.node.keyJSON), &key); err == nil {
+ report["Funding account"] = common.HexToAddress(key.Address).Hex()
+ } else {
+ log.Error("Failed to retrieve signer address", "err", err)
+ }
+ }
+ return report
}
// checkFaucet does a health-check against an faucet server to verify whether
@@ -224,9 +236,8 @@ func checkFaucet(client *sshClient, network string) (*faucetInfos, error) {
amount: amount,
minutes: minutes,
tiers: tiers,
- githubUser: infos.envvars["GITHUB_USER"],
- githubToken: infos.envvars["GITHUB_TOKEN"],
captchaToken: infos.envvars["CAPTCHA_TOKEN"],
captchaSecret: infos.envvars["CAPTCHA_SECRET"],
+ noauth: infos.envvars["NO_AUTH"] == "true",
}, nil
}
diff --git a/cmd/puppeth/module_nginx.go b/cmd/puppeth/module_nginx.go
index fd6d1d74e..35c0efc8a 100644
--- a/cmd/puppeth/module_nginx.go
+++ b/cmd/puppeth/module_nginx.go
@@ -22,6 +22,7 @@ import (
"html/template"
"math/rand"
"path/filepath"
+ "strconv"
"github.com/ethereum/go-ethereum/log"
)
@@ -54,7 +55,7 @@ services:
// deployNginx deploys a new nginx reverse-proxy container to expose one or more
// HTTP services running on a single host. If an instance with the specified
// network name already exists there, it will be overwritten!
-func deployNginx(client *sshClient, network string, port int) ([]byte, error) {
+func deployNginx(client *sshClient, network string, port int, nocache bool) ([]byte, error) {
log.Info("Deploying nginx reverse-proxy", "server", client.server, "port", port)
// Generate the content to upload to the server
@@ -78,8 +79,11 @@ func deployNginx(client *sshClient, network string, port int) ([]byte, error) {
}
defer client.Run("rm -rf " + workdir)
- // Build and deploy the ethstats service
- return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build", workdir, network))
+ // Build and deploy the reverse-proxy service
+ if nocache {
+ return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate", workdir, network, network))
+ }
+ return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network))
}
// nginxInfos is returned from an nginx reverse-proxy status check to allow
@@ -88,9 +92,12 @@ type nginxInfos struct {
port int
}
-// String implements the stringer interface.
-func (info *nginxInfos) String() string {
- return fmt.Sprintf("port=%d", info.port)
+// Report converts the typed struct into a plain string->string map, containing
+// most - but not all - fields for reporting to the user.
+func (info *nginxInfos) Report() map[string]string {
+ return map[string]string{
+ "Shared listener port": strconv.Itoa(info.port),
+ }
}
// checkNginx does a health-check against an nginx reverse-proxy to verify whether
diff --git a/cmd/puppeth/module_node.go b/cmd/puppeth/module_node.go
index 375e3e646..69cb19c34 100644
--- a/cmd/puppeth/module_node.go
+++ b/cmd/puppeth/module_node.go
@@ -18,6 +18,7 @@ package main
import (
"bytes"
+ "encoding/json"
"fmt"
"math/rand"
"path/filepath"
@@ -25,6 +26,7 @@ import (
"strings"
"text/template"
+ "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
@@ -38,9 +40,9 @@ ADD genesis.json /genesis.json
ADD signer.pass /signer.pass
{{end}}
RUN \
- echo 'geth init /genesis.json' > geth.sh && \{{if .Unlock}}
+ echo 'geth --cache 512 init /genesis.json' > geth.sh && \{{if .Unlock}}
echo 'mkdir -p /root/.ethereum/keystore/ && cp /signer.json /root/.ethereum/keystore/' >> geth.sh && \{{end}}
- echo $'geth --networkid {{.NetworkID}} --cache 512 --port {{.Port}} --maxpeers {{.Peers}} {{.LightFlag}} --ethstats \'{{.Ethstats}}\' {{if .BootV4}}--bootnodesv4 {{.BootV4}}{{end}} {{if .BootV5}}--bootnodesv5 {{.BootV5}}{{end}} {{if .Etherbase}}--etherbase {{.Etherbase}} --mine{{end}}{{if .Unlock}}--unlock 0 --password /signer.pass --mine{{end}} --targetgaslimit {{.GasTarget}} --gasprice {{.GasPrice}}' >> geth.sh
+ echo $'geth --networkid {{.NetworkID}} --cache 512 --port {{.Port}} --maxpeers {{.Peers}} {{.LightFlag}} --ethstats \'{{.Ethstats}}\' {{if .BootV4}}--bootnodesv4 {{.BootV4}}{{end}} {{if .BootV5}}--bootnodesv5 {{.BootV5}}{{end}} {{if .Etherbase}}--etherbase {{.Etherbase}} --mine --minerthreads 1{{end}} {{if .Unlock}}--unlock 0 --password /signer.pass --mine{{end}} --targetgaslimit {{.GasTarget}} --gasprice {{.GasPrice}}' >> geth.sh
ENTRYPOINT ["/bin/sh", "geth.sh"]
`
@@ -58,7 +60,8 @@ services:
- "{{.FullPort}}:{{.FullPort}}/udp"{{if .Light}}
- "{{.LightPort}}:{{.LightPort}}/udp"{{end}}
volumes:
- - {{.Datadir}}:/root/.ethereum
+ - {{.Datadir}}:/root/.ethereum{{if .Ethashdir}}
+ - {{.Ethashdir}}:/root/.ethash{{end}}
environment:
- FULL_PORT={{.FullPort}}/tcp
- LIGHT_PORT={{.LightPort}}/udp
@@ -79,7 +82,7 @@ services:
// deployNode deploys a new Ethereum node container to a remote machine via SSH,
// docker and docker-compose. If an instance with the specified network name
// already exists there, it will be overwritten!
-func deployNode(client *sshClient, network string, bootv4, bootv5 []string, config *nodeInfos) ([]byte, error) {
+func deployNode(client *sshClient, network string, bootv4, bootv5 []string, config *nodeInfos, nocache bool) ([]byte, error) {
kind := "sealnode"
if config.keyJSON == "" && config.etherbase == "" {
kind = "bootnode"
@@ -114,6 +117,7 @@ func deployNode(client *sshClient, network string, bootv4, bootv5 []string, conf
template.Must(template.New("").Parse(nodeComposefile)).Execute(composefile, map[string]interface{}{
"Type": kind,
"Datadir": config.datadir,
+ "Ethashdir": config.ethashdir,
"Network": network,
"FullPort": config.portFull,
"TotalPeers": config.peersTotal,
@@ -127,9 +131,7 @@ func deployNode(client *sshClient, network string, bootv4, bootv5 []string, conf
})
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
- //genesisfile, _ := json.MarshalIndent(config.genesis, "", " ")
files[filepath.Join(workdir, "genesis.json")] = config.genesis
-
if config.keyJSON != "" {
files[filepath.Join(workdir, "signer.json")] = []byte(config.keyJSON)
files[filepath.Join(workdir, "signer.pass")] = []byte(config.keyPass)
@@ -141,7 +143,10 @@ func deployNode(client *sshClient, network string, bootv4, bootv5 []string, conf
defer client.Run("rm -rf " + workdir)
// Build and deploy the boot or seal node service
- return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build", workdir, network))
+ if nocache {
+ return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate", workdir, network, network))
+ }
+ return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network))
}
// nodeInfos is returned from a boot or seal node status check to allow reporting
@@ -150,6 +155,7 @@ type nodeInfos struct {
genesis []byte
network int64
datadir string
+ ethashdir string
ethstats string
portFull int
portLight int
@@ -164,14 +170,43 @@ type nodeInfos struct {
gasPrice float64
}
-// String implements the stringer interface.
-func (info *nodeInfos) String() string {
- discv5 := ""
+// Report converts the typed struct into a plain string->string map, containing
+// most - but not all - fields for reporting to the user.
+func (info *nodeInfos) Report() map[string]string {
+ report := map[string]string{
+ "Data directory": info.datadir,
+ "Listener port (full nodes)": strconv.Itoa(info.portFull),
+ "Peer count (all total)": strconv.Itoa(info.peersTotal),
+ "Peer count (light nodes)": strconv.Itoa(info.peersLight),
+ "Ethstats username": info.ethstats,
+ }
if info.peersLight > 0 {
- discv5 = fmt.Sprintf(", portv5=%d", info.portLight)
+ // Light server enabled
+ report["Listener port (light nodes)"] = strconv.Itoa(info.portLight)
+ }
+ if info.gasTarget > 0 {
+ // Miner or signer node
+ report["Gas limit (baseline target)"] = fmt.Sprintf("%0.3f MGas", info.gasTarget)
+ report["Gas price (minimum accepted)"] = fmt.Sprintf("%0.3f GWei", info.gasPrice)
+
+ if info.etherbase != "" {
+ // Ethash proof-of-work miner
+ report["Ethash directory"] = info.ethashdir
+ report["Miner account"] = info.etherbase
+ }
+ if info.keyJSON != "" {
+ // Clique proof-of-authority signer
+ var key struct {
+ Address string `json:"address"`
+ }
+ if err := json.Unmarshal([]byte(info.keyJSON), &key); err == nil {
+ report["Signer account"] = common.HexToAddress(key.Address).Hex()
+ } else {
+ log.Error("Failed to retrieve signer address", "err", err)
+ }
+ }
}
- return fmt.Sprintf("port=%d%s, datadir=%s, peers=%d, lights=%d, ethstats=%s, gastarget=%0.3f MGas, gasprice=%0.3f GWei",
- info.portFull, discv5, info.datadir, info.peersTotal, info.peersLight, info.ethstats, info.gasTarget, info.gasPrice)
+ return report
}
// checkNode does a health-check against an boot or seal node server to verify
@@ -223,6 +258,7 @@ func checkNode(client *sshClient, network string, boot bool) (*nodeInfos, error)
stats := &nodeInfos{
genesis: genesis,
datadir: infos.volumes["/root/.ethereum"],
+ ethashdir: infos.volumes["/root/.ethash"],
portFull: infos.portmap[infos.envvars["FULL_PORT"]],
portLight: infos.portmap[infos.envvars["LIGHT_PORT"]],
peersTotal: totalPeers,
diff --git a/cmd/puppeth/module_wallet.go b/cmd/puppeth/module_wallet.go
new file mode 100644
index 000000000..67f47c70e
--- /dev/null
+++ b/cmd/puppeth/module_wallet.go
@@ -0,0 +1,200 @@
+// Copyright 2017 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "html/template"
+ "math/rand"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "github.com/ethereum/go-ethereum/log"
+)
+
+// walletDockerfile is the Dockerfile required to run a web wallet.
+var walletDockerfile = `
+FROM puppeth/wallet:latest
+
+ADD genesis.json /genesis.json
+
+RUN \
+ echo 'node server.js &' > wallet.sh && \
+ echo 'geth --cache 512 init /genesis.json' >> wallet.sh && \
+ echo $'geth --networkid {{.NetworkID}} --port {{.NodePort}} --bootnodes {{.Bootnodes}} --ethstats \'{{.Ethstats}}\' --cache=512 --rpc --rpcaddr=0.0.0.0 --rpccorsdomain "*"' >> wallet.sh
+
+RUN \
+ sed -i 's/PuppethNetworkID/{{.NetworkID}}/g' dist/js/etherwallet-master.js && \
+ sed -i 's/PuppethNetwork/{{.Network}}/g' dist/js/etherwallet-master.js && \
+ sed -i 's/PuppethDenom/{{.Denom}}/g' dist/js/etherwallet-master.js && \
+ sed -i 's/PuppethHost/{{.Host}}/g' dist/js/etherwallet-master.js && \
+ sed -i 's/PuppethRPCPort/{{.RPCPort}}/g' dist/js/etherwallet-master.js
+
+ENTRYPOINT ["/bin/sh", "wallet.sh"]
+`
+
+// walletComposefile is the docker-compose.yml file required to deploy and
+// maintain a web wallet.
+var walletComposefile = `
+version: '2'
+services:
+ wallet:
+ build: .
+ image: {{.Network}}/wallet
+ ports:
+ - "{{.NodePort}}:{{.NodePort}}"
+ - "{{.NodePort}}:{{.NodePort}}/udp"
+ - "{{.RPCPort}}:8545"{{if not .VHost}}
+ - "{{.WebPort}}:80"{{end}}
+ volumes:
+ - {{.Datadir}}:/root/.ethereum
+ environment:
+ - NODE_PORT={{.NodePort}}/tcp
+ - STATS={{.Ethstats}}{{if .VHost}}
+ - VIRTUAL_HOST={{.VHost}}
+ - VIRTUAL_PORT=80{{end}}
+ logging:
+ driver: "json-file"
+ options:
+ max-size: "1m"
+ max-file: "10"
+ restart: always
+`
+
+// deployWallet deploys a new web wallet container to a remote machine via SSH,
+// docker and docker-compose. If an instance with the specified network name
+// already exists there, it will be overwritten!
+func deployWallet(client *sshClient, network string, bootnodes []string, config *walletInfos, nocache bool) ([]byte, error) {
+ // Generate the content to upload to the server
+ workdir := fmt.Sprintf("%d", rand.Int63())
+ files := make(map[string][]byte)
+
+ dockerfile := new(bytes.Buffer)
+ template.Must(template.New("").Parse(walletDockerfile)).Execute(dockerfile, map[string]interface{}{
+ "Network": strings.ToTitle(network),
+ "Denom": strings.ToUpper(network),
+ "NetworkID": config.network,
+ "NodePort": config.nodePort,
+ "RPCPort": config.rpcPort,
+ "Bootnodes": strings.Join(bootnodes, ","),
+ "Ethstats": config.ethstats,
+ "Host": client.address,
+ })
+ files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
+
+ composefile := new(bytes.Buffer)
+ template.Must(template.New("").Parse(walletComposefile)).Execute(composefile, map[string]interface{}{
+ "Datadir": config.datadir,
+ "Network": network,
+ "NodePort": config.nodePort,
+ "RPCPort": config.rpcPort,
+ "VHost": config.webHost,
+ "WebPort": config.webPort,
+ "Ethstats": config.ethstats[:strings.Index(config.ethstats, ":")],
+ })
+ files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
+
+ files[filepath.Join(workdir, "genesis.json")] = config.genesis
+
+ // Upload the deployment files to the remote server (and clean up afterwards)
+ if out, err := client.Upload(files); err != nil {
+ return out, err
+ }
+ defer client.Run("rm -rf " + workdir)
+
+ // Build and deploy the boot or seal node service
+ if nocache {
+ return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate", workdir, network, network))
+ }
+ return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network))
+}
+
+// walletInfos is returned from a web wallet status check to allow reporting
+// various configuration parameters.
+type walletInfos struct {
+ genesis []byte
+ network int64
+ datadir string
+ ethstats string
+ nodePort int
+ rpcPort int
+ webHost string
+ webPort int
+}
+
+// Report converts the typed struct into a plain string->string map, containing
+// most - but not all - fields for reporting to the user.
+func (info *walletInfos) Report() map[string]string {
+ report := map[string]string{
+ "Data directory": info.datadir,
+ "Ethstats username": info.ethstats,
+ "Node listener port ": strconv.Itoa(info.nodePort),
+ "RPC listener port ": strconv.Itoa(info.rpcPort),
+ "Website address ": info.webHost,
+ "Website listener port ": strconv.Itoa(info.webPort),
+ }
+ return report
+}
+
+// checkWallet does a health-check against web wallet server to verify whether
+// it's running, and if yes, whether it's responsive.
+func checkWallet(client *sshClient, network string) (*walletInfos, error) {
+ // Inspect a possible web wallet container on the host
+ infos, err := inspectContainer(client, fmt.Sprintf("%s_wallet_1", network))
+ if err != nil {
+ return nil, err
+ }
+ if !infos.running {
+ return nil, ErrServiceOffline
+ }
+ // Resolve the port from the host, or the reverse proxy
+ webPort := infos.portmap["80/tcp"]
+ if webPort == 0 {
+ if proxy, _ := checkNginx(client, network); proxy != nil {
+ webPort = proxy.port
+ }
+ }
+ if webPort == 0 {
+ return nil, ErrNotExposed
+ }
+ // Resolve the host from the reverse-proxy and the config values
+ host := infos.envvars["VIRTUAL_HOST"]
+ if host == "" {
+ host = client.server
+ }
+ // Run a sanity check to see if the devp2p and RPC ports are reachable
+ nodePort := infos.portmap[infos.envvars["NODE_PORT"]]
+ if err = checkPort(client.server, nodePort); err != nil {
+ log.Warn(fmt.Sprintf("Wallet devp2p port seems unreachable"), "server", client.server, "port", nodePort, "err", err)
+ }
+ rpcPort := infos.portmap["8545/tcp"]
+ if err = checkPort(client.server, rpcPort); err != nil {
+ log.Warn(fmt.Sprintf("Wallet RPC port seems unreachable"), "server", client.server, "port", rpcPort, "err", err)
+ }
+ // Assemble and return the useful infos
+ stats := &walletInfos{
+ datadir: infos.volumes["/root/.ethereum"],
+ nodePort: nodePort,
+ rpcPort: rpcPort,
+ webHost: host,
+ webPort: webPort,
+ ethstats: infos.envvars["STATS"],
+ }
+ return stats, nil
+}
diff --git a/cmd/puppeth/puppeth.go b/cmd/puppeth/puppeth.go
index f783a7981..26382dac1 100644
--- a/cmd/puppeth/puppeth.go
+++ b/cmd/puppeth/puppeth.go
@@ -38,7 +38,7 @@ func main() {
},
cli.IntFlag{
Name: "loglevel",
- Value: 4,
+ Value: 3,
Usage: "log level to emit to the screen",
},
}
diff --git a/cmd/puppeth/ssh.go b/cmd/puppeth/ssh.go
index ec6a1b669..a254a7f7a 100644
--- a/cmd/puppeth/ssh.go
+++ b/cmd/puppeth/ssh.go
@@ -116,6 +116,7 @@ func dial(server string, pubkey []byte) (*sshClient, error) {
keycheck := func(hostname string, remote net.Addr, key ssh.PublicKey) error {
// If no public key is known for SSH, ask the user to confirm
if pubkey == nil {
+ fmt.Println()
fmt.Printf("The authenticity of host '%s (%s)' can't be established.\n", hostname, remote)
fmt.Printf("SSH key fingerprint is %s [MD5]\n", ssh.FingerprintLegacyMD5(key))
fmt.Printf("Are you sure you want to continue connecting (yes/no)? ")
diff --git a/cmd/puppeth/wizard.go b/cmd/puppeth/wizard.go
index eb6d9e5aa..2e2b4644c 100644
--- a/cmd/puppeth/wizard.go
+++ b/cmd/puppeth/wizard.go
@@ -28,6 +28,7 @@ import (
"sort"
"strconv"
"strings"
+ "sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
@@ -38,12 +39,12 @@ import (
// config contains all the configurations needed by puppeth that should be saved
// between sessions.
type config struct {
- path string // File containing the configuration values
- genesis *core.Genesis // Genesis block to cache for node deploys
- bootFull []string // Bootnodes to always connect to by full nodes
- bootLight []string // Bootnodes to always connect to by light nodes
- ethstats string // Ethstats settings to cache for node deploys
+ path string // File containing the configuration values
+ bootFull []string // Bootnodes to always connect to by full nodes
+ bootLight []string // Bootnodes to always connect to by light nodes
+ ethstats string // Ethstats settings to cache for node deploys
+ Genesis *core.Genesis `json:"genesis,omitempty"` // Genesis block to cache for node deploys
Servers map[string][]byte `json:"servers,omitempty"`
}
@@ -75,7 +76,8 @@ type wizard struct {
servers map[string]*sshClient // SSH connections to servers to administer
services map[string][]string // Ethereum services known to be running on servers
- in *bufio.Reader // Wrapper around stdin to allow reading user input
+ in *bufio.Reader // Wrapper around stdin to allow reading user input
+ lock sync.Mutex // Lock to protect configs during concurrent service discovery
}
// read reads a single line from stdin, trimming if from spaces.
diff --git a/cmd/puppeth/wizard_dashboard.go b/cmd/puppeth/wizard_dashboard.go
index 53a28a535..5f781c415 100644
--- a/cmd/puppeth/wizard_dashboard.go
+++ b/cmd/puppeth/wizard_dashboard.go
@@ -40,6 +40,8 @@ func (w *wizard) deployDashboard() {
host: client.server,
}
}
+ existed := err == nil
+
// Figure out which port to listen on
fmt.Println()
fmt.Printf("Which port should the dashboard listen on? (default = %d)\n", infos.port)
@@ -58,7 +60,6 @@ func (w *wizard) deployDashboard() {
available[service] = append(available[service], server)
}
}
- listing := make(map[string]string)
for _, service := range []string{"ethstats", "explorer", "wallet", "faucet"} {
// Gather all the locally hosted pages of this type
var pages []string
@@ -74,6 +75,14 @@ func (w *wizard) deployDashboard() {
if infos, err := checkEthstats(client, w.network); err == nil {
port = infos.port
}
+ case "explorer":
+ if infos, err := checkExplorer(client, w.network); err == nil {
+ port = infos.webPort
+ }
+ case "wallet":
+ if infos, err := checkWallet(client, w.network); err == nil {
+ port = infos.webPort
+ }
case "faucet":
if infos, err := checkFaucet(client, w.network); err == nil {
port = infos.port
@@ -101,26 +110,43 @@ func (w *wizard) deployDashboard() {
log.Error("Invalid listing choice, aborting")
return
}
+ var page string
switch {
case choice <= len(pages):
- listing[service] = pages[choice-1]
+ page = pages[choice-1]
case choice == len(pages)+1:
fmt.Println()
fmt.Printf("Which address is the external %s service at?\n", service)
- listing[service] = w.readString()
+ page = w.readString()
default:
// No service hosting for this
}
+ // Save the users choice
+ switch service {
+ case "ethstats":
+ infos.ethstats = page
+ case "explorer":
+ infos.explorer = page
+ case "wallet":
+ infos.wallet = page
+ case "faucet":
+ infos.faucet = page
+ }
}
// If we have ethstats running, ask whether to make the secret public or not
- var ethstats bool
if w.conf.ethstats != "" {
fmt.Println()
fmt.Println("Include ethstats secret on dashboard (y/n)? (default = yes)")
- ethstats = w.readDefaultString("y") == "y"
+ infos.trusted = w.readDefaultString("y") == "y"
}
// Try to deploy the dashboard container on the host
- if out, err := deployDashboard(client, w.network, infos.port, infos.host, listing, &w.conf, ethstats); err != nil {
+ nocache := false
+ if existed {
+ fmt.Println()
+ fmt.Printf("Should the dashboard be built from scratch (y/n)? (default = no)\n")
+ nocache = w.readDefaultString("n") != "n"
+ }
+ if out, err := deployDashboard(client, w.network, &w.conf, infos, nocache); err != nil {
log.Error("Failed to deploy dashboard container", "err", err)
if len(out) > 0 {
fmt.Printf("%s\n", out)
@@ -128,5 +154,5 @@ func (w *wizard) deployDashboard() {
return
}
// All ok, run a network scan to pick any changes up
- w.networkStats(false)
+ w.networkStats()
}
diff --git a/cmd/puppeth/wizard_ethstats.go b/cmd/puppeth/wizard_ethstats.go
index 8bfa1d6e5..fb2529c26 100644
--- a/cmd/puppeth/wizard_ethstats.go
+++ b/cmd/puppeth/wizard_ethstats.go
@@ -42,6 +42,8 @@ func (w *wizard) deployEthstats() {
secret: "",
}
}
+ existed := err == nil
+
// Figure out which port to listen on
fmt.Println()
fmt.Printf("Which port should ethstats listen on? (default = %d)\n", infos.port)
@@ -62,49 +64,57 @@ func (w *wizard) deployEthstats() {
infos.secret = w.readDefaultString(infos.secret)
}
// Gather any blacklists to ban from reporting
- fmt.Println()
- fmt.Printf("Keep existing IP %v blacklist (y/n)? (default = yes)\n", infos.banned)
- if w.readDefaultString("y") != "y" {
- // The user might want to clear the entire list, although generally probably not
+ if existed {
fmt.Println()
- fmt.Printf("Clear out blacklist and start over (y/n)? (default = no)\n")
- if w.readDefaultString("n") != "n" {
- infos.banned = nil
- }
- // Offer the user to explicitly add/remove certain IP addresses
- fmt.Println()
- fmt.Println("Which additional IP addresses should be blacklisted?")
- for {
- if ip := w.readIPAddress(); ip != "" {
- infos.banned = append(infos.banned, ip)
- continue
+ fmt.Printf("Keep existing IP %v blacklist (y/n)? (default = yes)\n", infos.banned)
+ if w.readDefaultString("y") != "y" {
+ // The user might want to clear the entire list, although generally probably not
+ fmt.Println()
+ fmt.Printf("Clear out blacklist and start over (y/n)? (default = no)\n")
+ if w.readDefaultString("n") != "n" {
+ infos.banned = nil
}
- break
- }
- fmt.Println()
- fmt.Println("Which IP addresses should not be blacklisted?")
- for {
- if ip := w.readIPAddress(); ip != "" {
- for i, addr := range infos.banned {
- if ip == addr {
- infos.banned = append(infos.banned[:i], infos.banned[i+1:]...)
- break
+ // Offer the user to explicitly add/remove certain IP addresses
+ fmt.Println()
+ fmt.Println("Which additional IP addresses should be blacklisted?")
+ for {
+ if ip := w.readIPAddress(); ip != "" {
+ infos.banned = append(infos.banned, ip)
+ continue
+ }
+ break
+ }
+ fmt.Println()
+ fmt.Println("Which IP addresses should not be blacklisted?")
+ for {
+ if ip := w.readIPAddress(); ip != "" {
+ for i, addr := range infos.banned {
+ if ip == addr {
+ infos.banned = append(infos.banned[:i], infos.banned[i+1:]...)
+ break
+ }
}
+ continue
}
- continue
+ break
}
- break
+ sort.Strings(infos.banned)
}
- sort.Strings(infos.banned)
}
// Try to deploy the ethstats server on the host
+ nocache := false
+ if existed {
+ fmt.Println()
+ fmt.Printf("Should the ethstats be built from scratch (y/n)? (default = no)\n")
+ nocache = w.readDefaultString("n") != "n"
+ }
trusted := make([]string, 0, len(w.servers))
for _, client := range w.servers {
if client != nil {
trusted = append(trusted, client.address)
}
}
- if out, err := deployEthstats(client, w.network, infos.port, infos.secret, infos.host, trusted, infos.banned); err != nil {
+ if out, err := deployEthstats(client, w.network, infos.port, infos.secret, infos.host, trusted, infos.banned, nocache); err != nil {
log.Error("Failed to deploy ethstats container", "err", err)
if len(out) > 0 {
fmt.Printf("%s\n", out)
@@ -112,5 +122,5 @@ func (w *wizard) deployEthstats() {
return
}
// All ok, run a network scan to pick any changes up
- w.networkStats(false)
+ w.networkStats()
}
diff --git a/cmd/puppeth/wizard_explorer.go b/cmd/puppeth/wizard_explorer.go
new file mode 100644
index 000000000..10ef72f78
--- /dev/null
+++ b/cmd/puppeth/wizard_explorer.go
@@ -0,0 +1,117 @@
+// Copyright 2017 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "github.com/ethereum/go-ethereum/log"
+)
+
+// deployExplorer creates a new block explorer based on some user input.
+func (w *wizard) deployExplorer() {
+ // Do some sanity check before the user wastes time on input
+ if w.conf.Genesis == nil {
+ log.Error("No genesis block configured")
+ return
+ }
+ if w.conf.ethstats == "" {
+ log.Error("No ethstats server configured")
+ return
+ }
+ if w.conf.Genesis.Config.Ethash == nil {
+ log.Error("Only ethash network supported")
+ return
+ }
+ // Select the server to interact with
+ server := w.selectServer()
+ if server == "" {
+ return
+ }
+ client := w.servers[server]
+
+ // Retrieve any active node configurations from the server
+ infos, err := checkExplorer(client, w.network)
+ if err != nil {
+ infos = &explorerInfos{
+ nodePort: 30303, webPort: 80, webHost: client.server,
+ }
+ }
+ existed := err == nil
+
+ chainspec, err := newParityChainSpec(w.network, w.conf.Genesis, w.conf.bootFull)
+ if err != nil {
+ log.Error("Failed to create chain spec for explorer", "err", err)
+ return
+ }
+ chain, _ := json.MarshalIndent(chainspec, "", " ")
+
+ // Figure out which port to listen on
+ fmt.Println()
+ fmt.Printf("Which port should the explorer listen on? (default = %d)\n", infos.webPort)
+ infos.webPort = w.readDefaultInt(infos.webPort)
+
+ // Figure which virtual-host to deploy ethstats on
+ if infos.webHost, err = w.ensureVirtualHost(client, infos.webPort, infos.webHost); err != nil {
+ log.Error("Failed to decide on explorer host", "err", err)
+ return
+ }
+ // Figure out where the user wants to store the persistent data
+ fmt.Println()
+ if infos.datadir == "" {
+ fmt.Printf("Where should data be stored on the remote machine?\n")
+ infos.datadir = w.readString()
+ } else {
+ fmt.Printf("Where should data be stored on the remote machine? (default = %s)\n", infos.datadir)
+ infos.datadir = w.readDefaultString(infos.datadir)
+ }
+ // Figure out which port to listen on
+ fmt.Println()
+ fmt.Printf("Which TCP/UDP port should the archive node listen on? (default = %d)\n", infos.nodePort)
+ infos.nodePort = w.readDefaultInt(infos.nodePort)
+
+ // Set a proper name to report on the stats page
+ fmt.Println()
+ if infos.ethstats == "" {
+ fmt.Printf("What should the explorer be called on the stats page?\n")
+ infos.ethstats = w.readString() + ":" + w.conf.ethstats
+ } else {
+ fmt.Printf("What should the explorer be called on the stats page? (default = %s)\n", infos.ethstats)
+ infos.ethstats = w.readDefaultString(infos.ethstats) + ":" + w.conf.ethstats
+ }
+ // Try to deploy the explorer on the host
+ nocache := false
+ if existed {
+ fmt.Println()
+ fmt.Printf("Should the explorer be built from scratch (y/n)? (default = no)\n")
+ nocache = w.readDefaultString("n") != "n"
+ }
+ if out, err := deployExplorer(client, w.network, chain, infos, nocache); err != nil {
+ log.Error("Failed to deploy explorer container", "err", err)
+ if len(out) > 0 {
+ fmt.Printf("%s\n", out)
+ }
+ return
+ }
+ // All ok, run a network scan to pick any changes up
+ log.Info("Waiting for node to finish booting")
+ time.Sleep(3 * time.Second)
+
+ w.networkStats()
+}
diff --git a/cmd/puppeth/wizard_faucet.go b/cmd/puppeth/wizard_faucet.go
index 51c4e2f7f..191575b16 100644
--- a/cmd/puppeth/wizard_faucet.go
+++ b/cmd/puppeth/wizard_faucet.go
@@ -19,7 +19,6 @@ package main
import (
"encoding/json"
"fmt"
- "net/http"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/log"
@@ -47,8 +46,10 @@ func (w *wizard) deployFaucet() {
tiers: 3,
}
}
- infos.node.genesis, _ = json.MarshalIndent(w.conf.genesis, "", " ")
- infos.node.network = w.conf.genesis.Config.ChainId.Int64()
+ existed := err == nil
+
+ infos.node.genesis, _ = json.MarshalIndent(w.conf.Genesis, "", " ")
+ infos.node.network = w.conf.Genesis.Config.ChainId.Int64()
// Figure out which port to listen on
fmt.Println()
@@ -60,7 +61,7 @@ func (w *wizard) deployFaucet() {
log.Error("Failed to decide on faucet host", "err", err)
return
}
- // Port and proxy settings retrieved, figure out the funcing amount per perdion configurations
+ // Port and proxy settings retrieved, figure out the funding amount per period configurations
fmt.Println()
fmt.Printf("How many Ethers to release per request? (default = %d)\n", infos.amount)
infos.amount = w.readDefaultInt(infos.amount)
@@ -76,47 +77,6 @@ func (w *wizard) deployFaucet() {
log.Error("At least one funding tier must be set")
return
}
- // Accessing GitHub gists requires API authorization, retrieve it
- if infos.githubUser != "" {
- fmt.Println()
- fmt.Printf("Reuse previous (%s) GitHub API authorization (y/n)? (default = yes)\n", infos.githubUser)
- if w.readDefaultString("y") != "y" {
- infos.githubUser, infos.githubToken = "", ""
- }
- }
- if infos.githubUser == "" {
- // No previous authorization (or new one requested)
- fmt.Println()
- fmt.Println("Which GitHub user to verify Gists through?")
- infos.githubUser = w.readString()
-
- fmt.Println()
- fmt.Println("What is the GitHub personal access token of the user? (won't be echoed)")
- infos.githubToken = w.readPassword()
-
- // Do a sanity check query against github to ensure it's valid
- req, _ := http.NewRequest("GET", "https://api.github.com/user", nil)
- req.SetBasicAuth(infos.githubUser, infos.githubToken)
- res, err := http.DefaultClient.Do(req)
- if err != nil {
- log.Error("Failed to verify GitHub authentication", "err", err)
- return
- }
- defer res.Body.Close()
-
- var msg struct {
- Login string `json:"login"`
- Message string `json:"message"`
- }
- if err = json.NewDecoder(res.Body).Decode(&msg); err != nil {
- log.Error("Failed to decode authorization response", "err", err)
- return
- }
- if msg.Login != infos.githubUser {
- log.Error("GitHub authorization failed", "user", infos.githubUser, "message", msg.Message)
- return
- }
- }
// Accessing the reCaptcha service requires API authorizations, request it
if infos.captchaToken != "" {
fmt.Println()
@@ -129,7 +89,9 @@ func (w *wizard) deployFaucet() {
// No previous authorization (or old one discarded)
fmt.Println()
fmt.Println("Enable reCaptcha protection against robots (y/n)? (default = no)")
- if w.readDefaultString("n") == "y" {
+ if w.readDefaultString("n") == "n" {
+ log.Warn("Users will be able to requests funds via automated scripts")
+ } else {
// Captcha protection explicitly requested, read the site and secret keys
fmt.Println()
fmt.Printf("What is the reCaptcha site key to authenticate human users?\n")
@@ -175,7 +137,7 @@ func (w *wizard) deployFaucet() {
}
}
}
- if infos.node.keyJSON == "" {
+ for i := 0; i < 3 && infos.node.keyJSON == ""; i++ {
fmt.Println()
fmt.Println("Please paste the faucet's funding account key JSON:")
infos.node.keyJSON = w.readJSON()
@@ -186,11 +148,27 @@ func (w *wizard) deployFaucet() {
if _, err := keystore.DecryptKey([]byte(infos.node.keyJSON), infos.node.keyPass); err != nil {
log.Error("Failed to decrypt key with given passphrase")
- return
+ infos.node.keyJSON = ""
+ infos.node.keyPass = ""
}
}
+ // Check if the user wants to run the faucet in debug mode (noauth)
+ noauth := "n"
+ if infos.noauth {
+ noauth = "y"
+ }
+ fmt.Println()
+ fmt.Printf("Permit non-authenticated funding requests (y/n)? (default = %v)\n", infos.noauth)
+ infos.noauth = w.readDefaultString(noauth) != "n"
+
// Try to deploy the faucet server on the host
- if out, err := deployFaucet(client, w.network, w.conf.bootLight, infos); err != nil {
+ nocache := false
+ if existed {
+ fmt.Println()
+ fmt.Printf("Should the faucet be built from scratch (y/n)? (default = no)\n")
+ nocache = w.readDefaultString("n") != "n"
+ }
+ if out, err := deployFaucet(client, w.network, w.conf.bootLight, infos, nocache); err != nil {
log.Error("Failed to deploy faucet container", "err", err)
if len(out) > 0 {
fmt.Printf("%s\n", out)
@@ -198,5 +176,5 @@ func (w *wizard) deployFaucet() {
return
}
// All ok, run a network scan to pick any changes up
- w.networkStats(false)
+ w.networkStats()
}
diff --git a/cmd/puppeth/wizard_genesis.go b/cmd/puppeth/wizard_genesis.go
index 222fc2a7c..f255ef6e6 100644
--- a/cmd/puppeth/wizard_genesis.go
+++ b/cmd/puppeth/wizard_genesis.go
@@ -37,7 +37,7 @@ func (w *wizard) makeGenesis() {
genesis := &core.Genesis{
Timestamp: uint64(time.Now().Unix()),
GasLimit: 4700000,
- Difficulty: big.NewInt(1048576),
+ Difficulty: big.NewInt(524288),
Alloc: make(core.GenesisAlloc),
Config: &params.ChainConfig{
HomesteadBlock: big.NewInt(1),
@@ -118,24 +118,16 @@ func (w *wizard) makeGenesis() {
for i := int64(0); i < 256; i++ {
genesis.Alloc[common.BigToAddress(big.NewInt(i))] = core.GenesisAccount{Balance: big.NewInt(1)}
}
- fmt.Println()
-
// Query the user for some custom extras
fmt.Println()
fmt.Println("Specify your chain/network ID if you want an explicit one (default = random)")
genesis.Config.ChainId = new(big.Int).SetUint64(uint64(w.readDefaultInt(rand.Intn(65536))))
- fmt.Println()
- fmt.Println("Anything fun to embed into the genesis block? (max 32 bytes)")
-
- extra := w.read()
- if len(extra) > 32 {
- extra = extra[:32]
- }
- genesis.ExtraData = append([]byte(extra), genesis.ExtraData[len(extra):]...)
-
// All done, store the genesis and flush to disk
- w.conf.genesis = genesis
+ log.Info("Configured new genesis block")
+
+ w.conf.Genesis = genesis
+ w.conf.flush()
}
// manageGenesis permits the modification of chain configuration parameters in
@@ -145,44 +137,56 @@ func (w *wizard) manageGenesis() {
fmt.Println()
fmt.Println(" 1. Modify existing fork rules")
fmt.Println(" 2. Export genesis configuration")
+ fmt.Println(" 3. Remove genesis configuration")
choice := w.read()
switch {
case choice == "1":
// Fork rule updating requested, iterate over each fork
fmt.Println()
- fmt.Printf("Which block should Homestead come into effect? (default = %v)\n", w.conf.genesis.Config.HomesteadBlock)
- w.conf.genesis.Config.HomesteadBlock = w.readDefaultBigInt(w.conf.genesis.Config.HomesteadBlock)
+ fmt.Printf("Which block should Homestead come into effect? (default = %v)\n", w.conf.Genesis.Config.HomesteadBlock)
+ w.conf.Genesis.Config.HomesteadBlock = w.readDefaultBigInt(w.conf.Genesis.Config.HomesteadBlock)
fmt.Println()
- fmt.Printf("Which block should EIP150 come into effect? (default = %v)\n", w.conf.genesis.Config.EIP150Block)
- w.conf.genesis.Config.EIP150Block = w.readDefaultBigInt(w.conf.genesis.Config.EIP150Block)
+ fmt.Printf("Which block should EIP150 come into effect? (default = %v)\n", w.conf.Genesis.Config.EIP150Block)
+ w.conf.Genesis.Config.EIP150Block = w.readDefaultBigInt(w.conf.Genesis.Config.EIP150Block)
fmt.Println()
- fmt.Printf("Which block should EIP155 come into effect? (default = %v)\n", w.conf.genesis.Config.EIP155Block)
- w.conf.genesis.Config.EIP155Block = w.readDefaultBigInt(w.conf.genesis.Config.EIP155Block)
+ fmt.Printf("Which block should EIP155 come into effect? (default = %v)\n", w.conf.Genesis.Config.EIP155Block)
+ w.conf.Genesis.Config.EIP155Block = w.readDefaultBigInt(w.conf.Genesis.Config.EIP155Block)
fmt.Println()
- fmt.Printf("Which block should EIP158 come into effect? (default = %v)\n", w.conf.genesis.Config.EIP158Block)
- w.conf.genesis.Config.EIP158Block = w.readDefaultBigInt(w.conf.genesis.Config.EIP158Block)
+ fmt.Printf("Which block should EIP158 come into effect? (default = %v)\n", w.conf.Genesis.Config.EIP158Block)
+ w.conf.Genesis.Config.EIP158Block = w.readDefaultBigInt(w.conf.Genesis.Config.EIP158Block)
fmt.Println()
- fmt.Printf("Which block should Byzantium come into effect? (default = %v)\n", w.conf.genesis.Config.ByzantiumBlock)
- w.conf.genesis.Config.ByzantiumBlock = w.readDefaultBigInt(w.conf.genesis.Config.ByzantiumBlock)
+ fmt.Printf("Which block should Byzantium come into effect? (default = %v)\n", w.conf.Genesis.Config.ByzantiumBlock)
+ w.conf.Genesis.Config.ByzantiumBlock = w.readDefaultBigInt(w.conf.Genesis.Config.ByzantiumBlock)
- out, _ := json.MarshalIndent(w.conf.genesis.Config, "", " ")
+ out, _ := json.MarshalIndent(w.conf.Genesis.Config, "", " ")
fmt.Printf("Chain configuration updated:\n\n%s\n", out)
case choice == "2":
// Save whatever genesis configuration we currently have
fmt.Println()
fmt.Printf("Which file to save the genesis into? (default = %s.json)\n", w.network)
- out, _ := json.MarshalIndent(w.conf.genesis, "", " ")
+ out, _ := json.MarshalIndent(w.conf.Genesis, "", " ")
if err := ioutil.WriteFile(w.readDefaultString(fmt.Sprintf("%s.json", w.network)), out, 0644); err != nil {
log.Error("Failed to save genesis file", "err", err)
}
log.Info("Exported existing genesis block")
+ case choice == "3":
+ // Make sure we don't have any services running
+ if len(w.conf.servers()) > 0 {
+ log.Error("Genesis reset requires all services and servers torn down")
+ return
+ }
+ log.Info("Genesis block destroyed")
+
+ w.conf.Genesis = nil
+ w.conf.flush()
+
default:
log.Error("That's not something I can do")
}
diff --git a/cmd/puppeth/wizard_intro.go b/cmd/puppeth/wizard_intro.go
index 2d9a097ee..84998afc9 100644
--- a/cmd/puppeth/wizard_intro.go
+++ b/cmd/puppeth/wizard_intro.go
@@ -24,6 +24,7 @@ import (
"os"
"path/filepath"
"strings"
+ "sync"
"github.com/ethereum/go-ethereum/log"
)
@@ -63,7 +64,7 @@ func (w *wizard) run() {
for {
w.network = w.readString()
if !strings.Contains(w.network, " ") {
- fmt.Printf("Sweet, you can set this via --network=%s next time!\n\n", w.network)
+ fmt.Printf("\nSweet, you can set this via --network=%s next time!\n\n", w.network)
break
}
log.Error("I also like to live dangerously, still no spaces")
@@ -80,22 +81,33 @@ func (w *wizard) run() {
} else if err := json.Unmarshal(blob, &w.conf); err != nil {
log.Crit("Previous configuration corrupted", "path", w.conf.path, "err", err)
} else {
+ // Dial all previously known servers concurrently
+ var pend sync.WaitGroup
for server, pubkey := range w.conf.Servers {
- log.Info("Dialing previously configured server", "server", server)
- client, err := dial(server, pubkey)
- if err != nil {
- log.Error("Previous server unreachable", "server", server, "err", err)
- }
- w.servers[server] = client
+ pend.Add(1)
+
+ go func(server string, pubkey []byte) {
+ defer pend.Done()
+
+ log.Info("Dialing previously configured server", "server", server)
+ client, err := dial(server, pubkey)
+ if err != nil {
+ log.Error("Previous server unreachable", "server", server, "err", err)
+ }
+ w.lock.Lock()
+ w.servers[server] = client
+ w.lock.Unlock()
+ }(server, pubkey)
}
- w.networkStats(false)
+ pend.Wait()
+ w.networkStats()
}
// Basics done, loop ad infinitum about what to do
for {
fmt.Println()
fmt.Println("What would you like to do? (default = stats)")
fmt.Println(" 1. Show network stats")
- if w.conf.genesis == nil {
+ if w.conf.Genesis == nil {
fmt.Println(" 2. Configure new genesis")
} else {
fmt.Println(" 2. Manage existing genesis")
@@ -110,15 +122,14 @@ func (w *wizard) run() {
} else {
fmt.Println(" 4. Manage network components")
}
- //fmt.Println(" 5. ProTips for common usecases")
choice := w.read()
switch {
case choice == "" || choice == "1":
- w.networkStats(false)
+ w.networkStats()
case choice == "2":
- if w.conf.genesis == nil {
+ if w.conf.Genesis == nil {
w.makeGenesis()
} else {
w.manageGenesis()
@@ -126,7 +137,7 @@ func (w *wizard) run() {
case choice == "3":
if len(w.servers) == 0 {
if w.makeServer() != "" {
- w.networkStats(false)
+ w.networkStats()
}
} else {
w.manageServers()
@@ -138,9 +149,6 @@ func (w *wizard) run() {
w.manageComponents()
}
- case choice == "5":
- w.networkStats(true)
-
default:
log.Error("That's not something I can do")
}
diff --git a/cmd/puppeth/wizard_netstats.go b/cmd/puppeth/wizard_netstats.go
index c06972198..e19180bb1 100644
--- a/cmd/puppeth/wizard_netstats.go
+++ b/cmd/puppeth/wizard_netstats.go
@@ -18,9 +18,10 @@ package main
import (
"encoding/json"
- "fmt"
"os"
+ "sort"
"strings"
+ "sync"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/log"
@@ -29,207 +30,265 @@ import (
// networkStats verifies the status of network components and generates a protip
// configuration set to give users hints on how to do various tasks.
-func (w *wizard) networkStats(tips bool) {
+func (w *wizard) networkStats() {
if len(w.servers) == 0 {
- log.Error("No remote machines to gather stats from")
+ log.Info("No remote machines to gather stats from")
return
}
- protips := new(protips)
+ // Clear out some previous configs to refill from current scan
+ w.conf.ethstats = ""
+ w.conf.bootFull = w.conf.bootFull[:0]
+ w.conf.bootLight = w.conf.bootLight[:0]
// Iterate over all the specified hosts and check their status
- stats := tablewriter.NewWriter(os.Stdout)
- stats.SetHeader([]string{"Server", "IP", "Status", "Service", "Details"})
- stats.SetColWidth(100)
+ var pend sync.WaitGroup
+ stats := make(serverStats)
for server, pubkey := range w.conf.Servers {
- client := w.servers[server]
- logger := log.New("server", server)
- logger.Info("Starting remote server health-check")
-
- // If the server is not connected, try to connect again
- if client == nil {
- conn, err := dial(server, pubkey)
- if err != nil {
- logger.Error("Failed to establish remote connection", "err", err)
- stats.Append([]string{server, "", err.Error(), "", ""})
- continue
- }
- client = conn
- }
- // Client connected one way or another, run health-checks
- services := make(map[string]string)
- logger.Debug("Checking for nginx availability")
- if infos, err := checkNginx(client, w.network); err != nil {
- if err != ErrServiceUnknown {
- services["nginx"] = err.Error()
- }
- } else {
- services["nginx"] = infos.String()
- }
- logger.Debug("Checking for ethstats availability")
- if infos, err := checkEthstats(client, w.network); err != nil {
- if err != ErrServiceUnknown {
- services["ethstats"] = err.Error()
- }
- } else {
- services["ethstats"] = infos.String()
- protips.ethstats = infos.config
- }
- logger.Debug("Checking for bootnode availability")
- if infos, err := checkNode(client, w.network, true); err != nil {
- if err != ErrServiceUnknown {
- services["bootnode"] = err.Error()
- }
- } else {
- services["bootnode"] = infos.String()
+ pend.Add(1)
+
+ // Gather the service stats for each server concurrently
+ go func(server string, pubkey []byte) {
+ defer pend.Done()
+
+ stat := w.gatherStats(server, pubkey, w.servers[server])
- protips.genesis = string(infos.genesis)
- protips.bootFull = append(protips.bootFull, infos.enodeFull)
- if infos.enodeLight != "" {
- protips.bootLight = append(protips.bootLight, infos.enodeLight)
+ // All status checks complete, report and check next server
+ w.lock.Lock()
+ defer w.lock.Unlock()
+
+ delete(w.services, server)
+ for service := range stat.services {
+ w.services[server] = append(w.services[server], service)
}
+ stats[server] = stat
+ }(server, pubkey)
+ }
+ pend.Wait()
+
+ // Print any collected stats and return
+ stats.render()
+}
+
+// gatherStats gathers service statistics for a particular remote server.
+func (w *wizard) gatherStats(server string, pubkey []byte, client *sshClient) *serverStat {
+ // Gather some global stats to feed into the wizard
+ var (
+ genesis string
+ ethstats string
+ bootFull []string
+ bootLight []string
+ )
+ // Ensure a valid SSH connection to the remote server
+ logger := log.New("server", server)
+ logger.Info("Starting remote server health-check")
+
+ stat := &serverStat{
+ address: client.address,
+ services: make(map[string]map[string]string),
+ }
+ if client == nil {
+ conn, err := dial(server, pubkey)
+ if err != nil {
+ logger.Error("Failed to establish remote connection", "err", err)
+ stat.failure = err.Error()
+ return stat
}
- logger.Debug("Checking for sealnode availability")
- if infos, err := checkNode(client, w.network, false); err != nil {
- if err != ErrServiceUnknown {
- services["sealnode"] = err.Error()
- }
- } else {
- services["sealnode"] = infos.String()
- protips.genesis = string(infos.genesis)
+ client = conn
+ }
+ // Client connected one way or another, run health-checks
+ logger.Debug("Checking for nginx availability")
+ if infos, err := checkNginx(client, w.network); err != nil {
+ if err != ErrServiceUnknown {
+ stat.services["nginx"] = map[string]string{"offline": err.Error()}
}
- logger.Debug("Checking for faucet availability")
- if infos, err := checkFaucet(client, w.network); err != nil {
- if err != ErrServiceUnknown {
- services["faucet"] = err.Error()
- }
- } else {
- services["faucet"] = infos.String()
+ } else {
+ stat.services["nginx"] = infos.Report()
+ }
+ logger.Debug("Checking for ethstats availability")
+ if infos, err := checkEthstats(client, w.network); err != nil {
+ if err != ErrServiceUnknown {
+ stat.services["ethstats"] = map[string]string{"offline": err.Error()}
}
- logger.Debug("Checking for dashboard availability")
- if infos, err := checkDashboard(client, w.network); err != nil {
- if err != ErrServiceUnknown {
- services["dashboard"] = err.Error()
- }
- } else {
- services["dashboard"] = infos.String()
+ } else {
+ stat.services["ethstats"] = infos.Report()
+ ethstats = infos.config
+ }
+ logger.Debug("Checking for bootnode availability")
+ if infos, err := checkNode(client, w.network, true); err != nil {
+ if err != ErrServiceUnknown {
+ stat.services["bootnode"] = map[string]string{"offline": err.Error()}
}
- // All status checks complete, report and check next server
- delete(w.services, server)
- for service := range services {
- w.services[server] = append(w.services[server], service)
+ } else {
+ stat.services["bootnode"] = infos.Report()
+
+ genesis = string(infos.genesis)
+ bootFull = append(bootFull, infos.enodeFull)
+ if infos.enodeLight != "" {
+ bootLight = append(bootLight, infos.enodeLight)
}
- server, address := client.server, client.address
- for service, status := range services {
- stats.Append([]string{server, address, "online", service, status})
- server, address = "", ""
+ }
+ logger.Debug("Checking for sealnode availability")
+ if infos, err := checkNode(client, w.network, false); err != nil {
+ if err != ErrServiceUnknown {
+ stat.services["sealnode"] = map[string]string{"offline": err.Error()}
}
- if len(services) == 0 {
- stats.Append([]string{server, address, "online", "", ""})
+ } else {
+ stat.services["sealnode"] = infos.Report()
+ genesis = string(infos.genesis)
+ }
+ logger.Debug("Checking for explorer availability")
+ if infos, err := checkExplorer(client, w.network); err != nil {
+ if err != ErrServiceUnknown {
+ stat.services["explorer"] = map[string]string{"offline": err.Error()}
}
+ } else {
+ stat.services["explorer"] = infos.Report()
}
- // If a genesis block was found, load it into our configs
- if protips.genesis != "" && w.conf.genesis == nil {
- genesis := new(core.Genesis)
- if err := json.Unmarshal([]byte(protips.genesis), genesis); err != nil {
- log.Error("Failed to parse remote genesis", "err", err)
- } else {
- w.conf.genesis = genesis
- protips.network = genesis.Config.ChainId.Int64()
+ logger.Debug("Checking for wallet availability")
+ if infos, err := checkWallet(client, w.network); err != nil {
+ if err != ErrServiceUnknown {
+ stat.services["wallet"] = map[string]string{"offline": err.Error()}
}
+ } else {
+ stat.services["wallet"] = infos.Report()
}
- if protips.ethstats != "" {
- w.conf.ethstats = protips.ethstats
+ logger.Debug("Checking for faucet availability")
+ if infos, err := checkFaucet(client, w.network); err != nil {
+ if err != ErrServiceUnknown {
+ stat.services["faucet"] = map[string]string{"offline": err.Error()}
+ }
+ } else {
+ stat.services["faucet"] = infos.Report()
}
- w.conf.bootFull = protips.bootFull
- w.conf.bootLight = protips.bootLight
-
- // Print any collected stats and return
- if !tips {
- stats.Render()
+ logger.Debug("Checking for dashboard availability")
+ if infos, err := checkDashboard(client, w.network); err != nil {
+ if err != ErrServiceUnknown {
+ stat.services["dashboard"] = map[string]string{"offline": err.Error()}
+ }
} else {
- protips.print(w.network)
+ stat.services["dashboard"] = infos.Report()
}
-}
-
-// protips contains a collection of network infos to report pro-tips
-// based on.
-type protips struct {
- genesis string
- network int64
- bootFull []string
- bootLight []string
- ethstats string
-}
+ // Feed and newly discovered information into the wizard
+ w.lock.Lock()
+ defer w.lock.Unlock()
-// print analyzes the network information available and prints a collection of
-// pro tips for the user's consideration.
-func (p *protips) print(network string) {
- // If a known genesis block is available, display it and prepend an init command
- fullinit, lightinit := "", ""
- if p.genesis != "" {
- fullinit = fmt.Sprintf("geth --datadir=$HOME/.%s init %s.json && ", network, network)
- lightinit = fmt.Sprintf("geth --datadir=$HOME/.%s --light init %s.json && ", network, network)
- }
- // If an ethstats server is available, add the ethstats flag
- statsflag := ""
- if p.ethstats != "" {
- if strings.Contains(p.ethstats, " ") {
- statsflag = fmt.Sprintf(` --ethstats="yournode:%s"`, p.ethstats)
+ if genesis != "" && w.conf.Genesis == nil {
+ g := new(core.Genesis)
+ if err := json.Unmarshal([]byte(genesis), g); err != nil {
+ log.Error("Failed to parse remote genesis", "err", err)
} else {
- statsflag = fmt.Sprintf(` --ethstats=yournode:%s`, p.ethstats)
+ w.conf.Genesis = g
}
}
- // If bootnodes have been specified, add the bootnode flag
- bootflagFull := ""
- if len(p.bootFull) > 0 {
- bootflagFull = fmt.Sprintf(` --bootnodes %s`, strings.Join(p.bootFull, ","))
- }
- bootflagLight := ""
- if len(p.bootLight) > 0 {
- bootflagLight = fmt.Sprintf(` --bootnodes %s`, strings.Join(p.bootLight, ","))
+ if ethstats != "" {
+ w.conf.ethstats = ethstats
}
- // Assemble all the known pro-tips
- var tasks, tips []string
+ w.conf.bootFull = append(w.conf.bootFull, bootFull...)
+ w.conf.bootLight = append(w.conf.bootLight, bootLight...)
- tasks = append(tasks, "Run an archive node with historical data")
- tips = append(tips, fmt.Sprintf("%sgeth --networkid=%d --datadir=$HOME/.%s --cache=1024%s%s", fullinit, p.network, network, statsflag, bootflagFull))
+ return stat
+}
+
+// serverStat is a collection of service configuration parameters and health
+// check reports to print to the user.
+type serverStat struct {
+ address string
+ failure string
+ services map[string]map[string]string
+}
- tasks = append(tasks, "Run a full node with recent data only")
- tips = append(tips, fmt.Sprintf("%sgeth --networkid=%d --datadir=$HOME/.%s --cache=512 --fast%s%s", fullinit, p.network, network, statsflag, bootflagFull))
+// serverStats is a collection of server stats for multiple hosts.
+type serverStats map[string]*serverStat
- tasks = append(tasks, "Run a light node with on demand retrievals")
- tips = append(tips, fmt.Sprintf("%sgeth --networkid=%d --datadir=$HOME/.%s --light%s%s", lightinit, p.network, network, statsflag, bootflagLight))
+// render converts the gathered statistics into a user friendly tabular report
+// and prints it to the standard output.
+func (stats serverStats) render() {
+ // Start gathering service statistics and config parameters
+ table := tablewriter.NewWriter(os.Stdout)
- tasks = append(tasks, "Run an embedded node with constrained memory")
- tips = append(tips, fmt.Sprintf("%sgeth --networkid=%d --datadir=$HOME/.%s --cache=32 --light%s%s", lightinit, p.network, network, statsflag, bootflagLight))
+ table.SetHeader([]string{"Server", "Address", "Service", "Config", "Value"})
+ table.SetAlignment(tablewriter.ALIGN_LEFT)
+ table.SetColWidth(100)
- // If the tips are short, display in a table
- short := true
- for _, tip := range tips {
- if len(tip) > 100 {
- short = false
- break
+ // Find the longest lines for all columns for the hacked separator
+ separator := make([]string, 5)
+ for server, stat := range stats {
+ if len(server) > len(separator[0]) {
+ separator[0] = strings.Repeat("-", len(server))
}
+ if len(stat.address) > len(separator[1]) {
+ separator[1] = strings.Repeat("-", len(stat.address))
+ }
+ for service, configs := range stat.services {
+ if len(service) > len(separator[2]) {
+ separator[2] = strings.Repeat("-", len(service))
+ }
+ for config, value := range configs {
+ if len(config) > len(separator[3]) {
+ separator[3] = strings.Repeat("-", len(config))
+ }
+ if len(value) > len(separator[4]) {
+ separator[4] = strings.Repeat("-", len(value))
+ }
+ }
+ }
+ }
+ // Fill up the server report in alphabetical order
+ servers := make([]string, 0, len(stats))
+ for server := range stats {
+ servers = append(servers, server)
}
- fmt.Println()
- if short {
- howto := tablewriter.NewWriter(os.Stdout)
- howto.SetHeader([]string{"Fun tasks for you", "Tips on how to"})
- howto.SetColWidth(100)
+ sort.Strings(servers)
- for i := 0; i < len(tasks); i++ {
- howto.Append([]string{tasks[i], tips[i]})
+ for i, server := range servers {
+ // Add a separator between all servers
+ if i > 0 {
+ table.Append(separator)
+ }
+ // Fill up the service report in alphabetical order
+ services := make([]string, 0, len(stats[server].services))
+ for service := range stats[server].services {
+ services = append(services, service)
+ }
+ sort.Strings(services)
+
+ if len(services) == 0 {
+ table.Append([]string{server, stats[server].address, "", "", ""})
+ }
+ for j, service := range services {
+ // Add an empty line between all services
+ if j > 0 {
+ table.Append([]string{"", "", "", separator[3], separator[4]})
+ }
+ // Fill up the config report in alphabetical order
+ configs := make([]string, 0, len(stats[server].services[service]))
+ for service := range stats[server].services[service] {
+ configs = append(configs, service)
+ }
+ sort.Strings(configs)
+
+ for k, config := range configs {
+ switch {
+ case j == 0 && k == 0:
+ table.Append([]string{server, stats[server].address, service, config, stats[server].services[service][config]})
+ case k == 0:
+ table.Append([]string{"", "", service, config, stats[server].services[service][config]})
+ default:
+ table.Append([]string{"", "", "", config, stats[server].services[service][config]})
+ }
+ }
}
- howto.Render()
- return
- }
- // Meh, tips got ugly, split into many lines
- for i := 0; i < len(tasks); i++ {
- fmt.Println(tasks[i])
- fmt.Println(strings.Repeat("-", len(tasks[i])))
- fmt.Println(tips[i])
- fmt.Println()
- fmt.Println()
}
+ table.Render()
+}
+
+// protips contains a collection of network infos to report pro-tips
+// based on.
+type protips struct {
+ genesis string
+ network int64
+ bootFull []string
+ bootLight []string
+ ethstats string
}
diff --git a/cmd/puppeth/wizard_network.go b/cmd/puppeth/wizard_network.go
index c20e31fab..d780c550b 100644
--- a/cmd/puppeth/wizard_network.go
+++ b/cmd/puppeth/wizard_network.go
@@ -53,12 +53,12 @@ func (w *wizard) manageServers() {
w.conf.flush()
log.Info("Disconnected existing server", "server", server)
- w.networkStats(false)
+ w.networkStats()
return
}
// If the user requested connecting a new server, do it
if w.makeServer() != "" {
- w.networkStats(false)
+ w.networkStats()
}
}
@@ -174,9 +174,10 @@ func (w *wizard) deployComponent() {
fmt.Println(" 1. Ethstats - Network monitoring tool")
fmt.Println(" 2. Bootnode - Entry point of the network")
fmt.Println(" 3. Sealer - Full node minting new blocks")
- fmt.Println(" 4. Wallet - Browser wallet for quick sends (todo)")
- fmt.Println(" 5. Faucet - Crypto faucet to give away funds")
- fmt.Println(" 6. Dashboard - Website listing above web-services")
+ fmt.Println(" 4. Explorer - Chain analysis webservice (ethash only)")
+ fmt.Println(" 5. Wallet - Browser wallet for quick sends")
+ fmt.Println(" 6. Faucet - Crypto faucet to give away funds")
+ fmt.Println(" 7. Dashboard - Website listing above web-services")
switch w.read() {
case "1":
@@ -186,9 +187,12 @@ func (w *wizard) deployComponent() {
case "3":
w.deployNode(false)
case "4":
+ w.deployExplorer()
case "5":
- w.deployFaucet()
+ w.deployWallet()
case "6":
+ w.deployFaucet()
+ case "7":
w.deployDashboard()
default:
log.Error("That's not something I can do")
diff --git a/cmd/puppeth/wizard_nginx.go b/cmd/puppeth/wizard_nginx.go
index 86fba29f5..4eeae93a0 100644
--- a/cmd/puppeth/wizard_nginx.go
+++ b/cmd/puppeth/wizard_nginx.go
@@ -29,7 +29,8 @@ import (
//
// If the user elects not to use a reverse proxy, an empty hostname is returned!
func (w *wizard) ensureVirtualHost(client *sshClient, port int, def string) (string, error) {
- if proxy, _ := checkNginx(client, w.network); proxy != nil {
+ proxy, _ := checkNginx(client, w.network)
+ if proxy != nil {
// Reverse proxy is running, if ports match, we need a virtual host
if proxy.port == port {
fmt.Println()
@@ -41,7 +42,13 @@ func (w *wizard) ensureVirtualHost(client *sshClient, port int, def string) (str
fmt.Println()
fmt.Println("Allow sharing the port with other services (y/n)? (default = yes)")
if w.readDefaultString("y") == "y" {
- if out, err := deployNginx(client, w.network, port); err != nil {
+ nocache := false
+ if proxy != nil {
+ fmt.Println()
+ fmt.Printf("Should the reverse-proxy be rebuilt from scratch (y/n)? (default = no)\n")
+ nocache = w.readDefaultString("n") != "n"
+ }
+ if out, err := deployNginx(client, w.network, port, nocache); err != nil {
log.Error("Failed to deploy reverse-proxy", "err", err)
if len(out) > 0 {
fmt.Printf("%s\n", out)
diff --git a/cmd/puppeth/wizard_node.go b/cmd/puppeth/wizard_node.go
index 05232486b..097e2e41a 100644
--- a/cmd/puppeth/wizard_node.go
+++ b/cmd/puppeth/wizard_node.go
@@ -29,7 +29,7 @@ import (
// deployNode creates a new node configuration based on some user input.
func (w *wizard) deployNode(boot bool) {
// Do some sanity check before the user wastes time on input
- if w.conf.genesis == nil {
+ if w.conf.Genesis == nil {
log.Error("No genesis block configured")
return
}
@@ -44,7 +44,7 @@ func (w *wizard) deployNode(boot bool) {
}
client := w.servers[server]
- // Retrieve any active ethstats configurations from the server
+ // Retrieve any active node configurations from the server
infos, err := checkNode(client, w.network, boot)
if err != nil {
if boot {
@@ -53,8 +53,10 @@ func (w *wizard) deployNode(boot bool) {
infos = &nodeInfos{portFull: 30303, peersTotal: 50, peersLight: 0, gasTarget: 4.7, gasPrice: 18}
}
}
- infos.genesis, _ = json.MarshalIndent(w.conf.genesis, "", " ")
- infos.network = w.conf.genesis.Config.ChainId.Int64()
+ existed := err == nil
+
+ infos.genesis, _ = json.MarshalIndent(w.conf.Genesis, "", " ")
+ infos.network = w.conf.Genesis.Config.ChainId.Int64()
// Figure out where the user wants to store the persistent data
fmt.Println()
@@ -65,6 +67,16 @@ func (w *wizard) deployNode(boot bool) {
fmt.Printf("Where should data be stored on the remote machine? (default = %s)\n", infos.datadir)
infos.datadir = w.readDefaultString(infos.datadir)
}
+ if w.conf.Genesis.Config.Ethash != nil && !boot {
+ fmt.Println()
+ if infos.ethashdir == "" {
+ fmt.Printf("Where should the ethash mining DAGs be stored on the remote machine?\n")
+ infos.ethashdir = w.readString()
+ } else {
+ fmt.Printf("Where should the ethash mining DAGs be stored on the remote machine? (default = %s)\n", infos.ethashdir)
+ infos.ethashdir = w.readDefaultString(infos.ethashdir)
+ }
+ }
// Figure out which port to listen on
fmt.Println()
fmt.Printf("Which TCP/UDP port to listen on? (default = %d)\n", infos.portFull)
@@ -91,7 +103,7 @@ func (w *wizard) deployNode(boot bool) {
}
// If the node is a miner/signer, load up needed credentials
if !boot {
- if w.conf.genesis.Config.Ethash != nil {
+ if w.conf.Genesis.Config.Ethash != nil {
// Ethash based miners only need an etherbase to mine against
fmt.Println()
if infos.etherbase == "" {
@@ -106,7 +118,7 @@ func (w *wizard) deployNode(boot bool) {
fmt.Printf("What address should the miner user? (default = %s)\n", infos.etherbase)
infos.etherbase = w.readDefaultAddress(common.HexToAddress(infos.etherbase)).Hex()
}
- } else if w.conf.genesis.Config.Clique != nil {
+ } else if w.conf.Genesis.Config.Clique != nil {
// If a previous signer was already set, offer to reuse it
if infos.keyJSON != "" {
if key, err := keystore.DecryptKey([]byte(infos.keyJSON), infos.keyPass); err != nil {
@@ -145,7 +157,13 @@ func (w *wizard) deployNode(boot bool) {
infos.gasPrice = w.readDefaultFloat(infos.gasPrice)
}
// Try to deploy the full node on the host
- if out, err := deployNode(client, w.network, w.conf.bootFull, w.conf.bootLight, infos); err != nil {
+ nocache := false
+ if existed {
+ fmt.Println()
+ fmt.Printf("Should the node be built from scratch (y/n)? (default = no)\n")
+ nocache = w.readDefaultString("n") != "n"
+ }
+ if out, err := deployNode(client, w.network, w.conf.bootFull, w.conf.bootLight, infos, nocache); err != nil {
log.Error("Failed to deploy Ethereum node container", "err", err)
if len(out) > 0 {
fmt.Printf("%s\n", out)
@@ -156,5 +174,5 @@ func (w *wizard) deployNode(boot bool) {
log.Info("Waiting for node to finish booting")
time.Sleep(3 * time.Second)
- w.networkStats(false)
+ w.networkStats()
}
diff --git a/cmd/puppeth/wizard_wallet.go b/cmd/puppeth/wizard_wallet.go
new file mode 100644
index 000000000..7c3896a17
--- /dev/null
+++ b/cmd/puppeth/wizard_wallet.go
@@ -0,0 +1,113 @@
+// Copyright 2017 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "github.com/ethereum/go-ethereum/log"
+)
+
+// deployWallet creates a new web wallet based on some user input.
+func (w *wizard) deployWallet() {
+ // Do some sanity check before the user wastes time on input
+ if w.conf.Genesis == nil {
+ log.Error("No genesis block configured")
+ return
+ }
+ if w.conf.ethstats == "" {
+ log.Error("No ethstats server configured")
+ return
+ }
+ // Select the server to interact with
+ server := w.selectServer()
+ if server == "" {
+ return
+ }
+ client := w.servers[server]
+
+ // Retrieve any active node configurations from the server
+ infos, err := checkWallet(client, w.network)
+ if err != nil {
+ infos = &walletInfos{
+ nodePort: 30303, rpcPort: 8545, webPort: 80, webHost: client.server,
+ }
+ }
+ existed := err == nil
+
+ infos.genesis, _ = json.MarshalIndent(w.conf.Genesis, "", " ")
+ infos.network = w.conf.Genesis.Config.ChainId.Int64()
+
+ // Figure out which port to listen on
+ fmt.Println()
+ fmt.Printf("Which port should the wallet listen on? (default = %d)\n", infos.webPort)
+ infos.webPort = w.readDefaultInt(infos.webPort)
+
+ // Figure which virtual-host to deploy ethstats on
+ if infos.webHost, err = w.ensureVirtualHost(client, infos.webPort, infos.webHost); err != nil {
+ log.Error("Failed to decide on wallet host", "err", err)
+ return
+ }
+ // Figure out where the user wants to store the persistent data
+ fmt.Println()
+ if infos.datadir == "" {
+ fmt.Printf("Where should data be stored on the remote machine?\n")
+ infos.datadir = w.readString()
+ } else {
+ fmt.Printf("Where should data be stored on the remote machine? (default = %s)\n", infos.datadir)
+ infos.datadir = w.readDefaultString(infos.datadir)
+ }
+ // Figure out which port to listen on
+ fmt.Println()
+ fmt.Printf("Which TCP/UDP port should the backing node listen on? (default = %d)\n", infos.nodePort)
+ infos.nodePort = w.readDefaultInt(infos.nodePort)
+
+ fmt.Println()
+ fmt.Printf("Which port should the backing RPC API listen on? (default = %d)\n", infos.rpcPort)
+ infos.rpcPort = w.readDefaultInt(infos.rpcPort)
+
+ // Set a proper name to report on the stats page
+ fmt.Println()
+ if infos.ethstats == "" {
+ fmt.Printf("What should the wallet be called on the stats page?\n")
+ infos.ethstats = w.readString() + ":" + w.conf.ethstats
+ } else {
+ fmt.Printf("What should the wallet be called on the stats page? (default = %s)\n", infos.ethstats)
+ infos.ethstats = w.readDefaultString(infos.ethstats) + ":" + w.conf.ethstats
+ }
+ // Try to deploy the wallet on the host
+ nocache := false
+ if existed {
+ fmt.Println()
+ fmt.Printf("Should the wallet be built from scratch (y/n)? (default = no)\n")
+ nocache = w.readDefaultString("n") != "n"
+ }
+ if out, err := deployWallet(client, w.network, w.conf.bootFull, infos, nocache); err != nil {
+ log.Error("Failed to deploy wallet container", "err", err)
+ if len(out) > 0 {
+ fmt.Printf("%s\n", out)
+ }
+ return
+ }
+ // All ok, run a network scan to pick any changes up
+ log.Info("Waiting for node to finish booting")
+ time.Sleep(3 * time.Second)
+
+ w.networkStats()
+}