Merge pull request #15390 from karalabe/puppeth-devcon3
cmd/puppeth: new version as presented at devcon3
This commit is contained in:
commit
f9569f3cd8
@ -83,7 +83,8 @@ var (
|
|||||||
captchaToken = flag.String("captcha.token", "", "Recaptcha site key to authenticate client side")
|
captchaToken = flag.String("captcha.token", "", "Recaptcha site key to authenticate client side")
|
||||||
captchaSecret = flag.String("captcha.secret", "", "Recaptcha secret key to authenticate server 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 (
|
var (
|
||||||
@ -132,6 +133,7 @@ func main() {
|
|||||||
"Amounts": amounts,
|
"Amounts": amounts,
|
||||||
"Periods": periods,
|
"Periods": periods,
|
||||||
"Recaptcha": *captchaToken,
|
"Recaptcha": *captchaToken,
|
||||||
|
"NoAuth": *noauthFlag,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Crit("Failed to render the faucet template", "err", err)
|
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 {
|
if err = websocket.JSON.Receive(conn, &msg); err != nil {
|
||||||
return
|
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/") {
|
!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 {
|
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)
|
log.Warn("Failed to send URL error to client", "err", err)
|
||||||
@ -435,13 +437,19 @@ func (f *faucet) apiHandler(conn *websocket.Conn) {
|
|||||||
)
|
)
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(msg.URL, "https://gist.github.com/"):
|
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/"):
|
case strings.HasPrefix(msg.URL, "https://twitter.com/"):
|
||||||
username, avatar, address, err = authTwitter(msg.URL)
|
username, avatar, address, err = authTwitter(msg.URL)
|
||||||
case strings.HasPrefix(msg.URL, "https://plus.google.com/"):
|
case strings.HasPrefix(msg.URL, "https://plus.google.com/"):
|
||||||
username, avatar, address, err = authGooglePlus(msg.URL)
|
username, avatar, address, err = authGooglePlus(msg.URL)
|
||||||
case strings.HasPrefix(msg.URL, "https://www.facebook.com/"):
|
case strings.HasPrefix(msg.URL, "https://www.facebook.com/"):
|
||||||
username, avatar, address, err = authFacebook(msg.URL)
|
username, avatar, address, err = authFacebook(msg.URL)
|
||||||
|
case *noauthFlag:
|
||||||
|
username, avatar, address, err = authNoAuth(msg.URL)
|
||||||
default:
|
default:
|
||||||
err = errors.New("Something funky happened, please open an issue at https://github.com/ethereum/go-ethereum/issues")
|
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
|
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
|
||||||
|
}
|
||||||
|
@ -80,11 +80,8 @@
|
|||||||
<div class="row" style="margin-top: 32px;">
|
<div class="row" style="margin-top: 32px;">
|
||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
<h3>How does this work?</h3>
|
<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">
|
<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>
|
<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>
|
<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>
|
<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>
|
<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>
|
</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>
|
<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}}
|
{{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
|
// Define a method to reconnect upon server loss
|
||||||
var reconnect = function() {
|
var reconnect = function() {
|
||||||
if (attempt % 2 == 0) {
|
server = new WebSocket(((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + "/api");
|
||||||
server = new WebSocket("wss://" + location.host + "/api");
|
|
||||||
} else {
|
|
||||||
server = new WebSocket("ws://" + location.host + "/api");
|
|
||||||
}
|
|
||||||
attempt++;
|
|
||||||
|
|
||||||
server.onmessage = function(event) {
|
server.onmessage = function(event) {
|
||||||
var msg = JSON.parse(event.data);
|
var msg = JSON.parse(event.data);
|
||||||
|
File diff suppressed because one or more lines are too long
379
cmd/puppeth/genesis.go
Normal file
379
cmd/puppeth/genesis.go
Normal file
@ -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
|
||||||
|
}
|
@ -18,10 +18,12 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"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 id="sidebar-menu" class="main_menu_side hidden-print main_menu">
|
||||||
<div class="menu_section">
|
<div class="menu_section">
|
||||||
<ul class="nav side-menu">
|
<ul class="nav side-menu">
|
||||||
{{if .EthstatsPage}}<li><a onclick="load('//{{.EthstatsPage}}')"><i class="fa fa-tachometer"></i> Network Stats</a></li>{{end}}
|
{{if .EthstatsPage}}<li id="stats_menu"><a onclick="load('#stats')"><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 .ExplorerPage}}<li id="explorer_menu"><a onclick="load('#explorer')"><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 .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><a onclick="load('//{{.FaucetPage}}')"><i class="fa fa-bath"></i> Crypto Faucet</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"><a><i class="fa fa-plug"></i> Connect Yourself</a>
|
<li id="connect_menu"><a><i class="fa fa-plug"></i> Connect Yourself</a>
|
||||||
<ul id="connect_list" class="nav child_menu">
|
<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_menu').removeClass('active'); $('#connect_list').toggle(); load('#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_menu').removeClass('active'); $('#connect_list').toggle(); load('#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('#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>
|
</ul>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="right_col" role="main" style="padding: 0">
|
<div class="right_col" role="main" style="padding: 0 !important">
|
||||||
<div id="connect-go-ethereum-geth" hidden style="padding: 16px;">
|
<div id="geth" hidden style="padding: 16px;">
|
||||||
<div class="page-title">
|
<div class="page-title">
|
||||||
<div class="title_left">
|
<div class="title_left">
|
||||||
<h3>Connect Yourself – Go Ethereum: Geth</h3>
|
<h3>Connect Yourself – 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>
|
<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/>
|
<br/>
|
||||||
<p>To run a light node, download <a href="/{{.GethGenesis}}"><code>{{.GethGenesis}}</code></a> and start Geth with:
|
<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>
|
<pre>geth --networkid={{.NetworkID}} --datadir=$HOME/.{{.Network}} --syncmode=light{{if .Ethstats}} --ethstats='{{.Ethstats}}'{{end}} --bootnodes={{.BootnodesLightFlat}}</pre>
|
||||||
</p>
|
</p>
|
||||||
<br/>
|
<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>
|
<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/>
|
<br/>
|
||||||
<p>To run an embedded node, download <a href="/{{.GethGenesis}}"><code>{{.GethGenesis}}</code></a> and start Geth with:
|
<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 --datadir=$HOME/.{{.Network}} init {{.GethGenesis}}</pre>
|
||||||
<pre>geth --networkid={{.NetworkID}} --datadir=$HOME/.{{.Network}} --cache=32 --syncmode=light{{if .Ethstats}} --ethstats='{{.Ethstats}}'{{end}} --bootnodes={{.BootnodesLightFlat}}</pre>
|
<pre>geth --networkid={{.NetworkID}} --datadir=$HOME/.{{.Network}} --cache=16 --ethash.cachesinmem=1 --syncmode=light{{if .Ethstats}} --ethstats='{{.Ethstats}}'{{end}} --bootnodes={{.BootnodesLightFlat}}</pre>
|
||||||
</p>
|
</p>
|
||||||
<br/>
|
<br/>
|
||||||
<p>You can download Geth from <a href="https://geth.ethereum.org/downloads/" target="about:blank">https://geth.ethereum.org/downloads/</a>.</p>
|
<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>
|
||||||
</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="page-title">
|
||||||
<div class="title_left">
|
<div class="title_left">
|
||||||
<h3>Connect Yourself – Go Ethereum: Wallet & Mist</h3>
|
<h3>Connect Yourself – Go Ethereum: Wallet & Mist</h3>
|
||||||
@ -235,7 +238,7 @@ var dashboardContent = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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="page-title">
|
||||||
<div class="title_left">
|
<div class="title_left">
|
||||||
<h3>Connect Yourself – Go Ethereum: Android & iOS</h3>
|
<h3>Connect Yourself – Go Ethereum: Android & iOS</h3>
|
||||||
@ -309,7 +312,101 @@ try! node?.start();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 – 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 id="about" hidden>
|
||||||
<div class="row vertical-center">
|
<div class="row vertical-center">
|
||||||
<div style="margin: 0 auto;">
|
<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/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 src="https://cdnjs.cloudflare.com/ajax/libs/gentelella/1.3.0/js/custom.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
var load = function(url) {
|
var load = function(hash) {
|
||||||
$("#connect-go-ethereum-geth").fadeOut(300)
|
window.location.hash = hash;
|
||||||
$("#connect-go-ethereum-mist").fadeOut(300)
|
|
||||||
$("#connect-go-ethereum-mobile").fadeOut(300)
|
// 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)
|
$("#about").fadeOut(300)
|
||||||
$("#frame-wrapper").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() {
|
setTimeout(function() {
|
||||||
if (url.substring(0, 1) == "#") {
|
if (url.substring(0, 1) == "#") {
|
||||||
$('.body').css({overflowY: 'auto'});
|
$('.body').css({overflowY: 'auto'});
|
||||||
@ -364,13 +481,10 @@ try! node?.start();
|
|||||||
}
|
}
|
||||||
var resize = function() {
|
var resize = function() {
|
||||||
var sidebar = $($(".navbar")[0]).width();
|
var sidebar = $($(".navbar")[0]).width();
|
||||||
var content = 1920;
|
|
||||||
var limit = document.body.clientWidth - sidebar;
|
var limit = document.body.clientWidth - sidebar;
|
||||||
var scale = limit / content;
|
var scale = limit / 1920;
|
||||||
|
|
||||||
console.log(document.body.clientHeight);
|
$("#frame-wrapper").width(limit);
|
||||||
|
|
||||||
$("#frame-wrapper").width(content / scale);
|
|
||||||
$("#frame-wrapper").height(document.body.clientHeight / scale);
|
$("#frame-wrapper").height(document.body.clientHeight / scale);
|
||||||
$("#frame-wrapper").css({
|
$("#frame-wrapper").css({
|
||||||
transform: 'scale(' + (scale) + ')',
|
transform: 'scale(' + (scale) + ')',
|
||||||
@ -379,9 +493,17 @@ try! node?.start();
|
|||||||
};
|
};
|
||||||
$(window).resize(resize);
|
$(window).resize(resize);
|
||||||
|
|
||||||
var item = $(".side-menu").children()[0];
|
if (window.location.hash == "") {
|
||||||
$(item).children()[0].click();
|
var item = $(".side-menu").children()[0];
|
||||||
$(item).addClass("active");
|
$(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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -405,6 +527,10 @@ RUN \
|
|||||||
echo '});' >> server.js
|
echo '});' >> server.js
|
||||||
|
|
||||||
ADD {{.Network}}.json /dashboard/{{.Network}}.json
|
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 index.html /dashboard/index.html
|
||||||
ADD puppeth.png /dashboard/puppeth.png
|
ADD puppeth.png /dashboard/puppeth.png
|
||||||
|
|
||||||
@ -422,8 +548,12 @@ services:
|
|||||||
build: .
|
build: .
|
||||||
image: {{.Network}}/dashboard{{if not .VHost}}
|
image: {{.Network}}/dashboard{{if not .VHost}}
|
||||||
ports:
|
ports:
|
||||||
- "{{.Port}}:80"{{else}}
|
- "{{.Port}}:80"{{end}}
|
||||||
environment:
|
environment:
|
||||||
|
- ETHSTATS_PAGE={{.EthstatsPage}}
|
||||||
|
- EXPLORER_PAGE={{.ExplorerPage}}
|
||||||
|
- WALLET_PAGE={{.WalletPage}}
|
||||||
|
- FAUCET_PAGE={{.FaucetPage}}{{if .VHost}}
|
||||||
- VIRTUAL_HOST={{.VHost}}{{end}}
|
- VIRTUAL_HOST={{.VHost}}{{end}}
|
||||||
logging:
|
logging:
|
||||||
driver: "json-file"
|
driver: "json-file"
|
||||||
@ -436,7 +566,7 @@ services:
|
|||||||
// deployDashboard deploys a new dashboard container to a remote machine via SSH,
|
// deployDashboard deploys a new dashboard container to a remote machine via SSH,
|
||||||
// docker and docker-compose. If an instance with the specified network name
|
// docker and docker-compose. If an instance with the specified network name
|
||||||
// already exists there, it will be overwritten!
|
// 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
|
// Generate the content to upload to the server
|
||||||
workdir := fmt.Sprintf("%d", rand.Int63())
|
workdir := fmt.Sprintf("%d", rand.Int63())
|
||||||
files := make(map[string][]byte)
|
files := make(map[string][]byte)
|
||||||
@ -449,37 +579,95 @@ func deployDashboard(client *sshClient, network string, port int, vhost string,
|
|||||||
|
|
||||||
composefile := new(bytes.Buffer)
|
composefile := new(bytes.Buffer)
|
||||||
template.Must(template.New("").Parse(dashboardComposefile)).Execute(composefile, map[string]interface{}{
|
template.Must(template.New("").Parse(dashboardComposefile)).Execute(composefile, map[string]interface{}{
|
||||||
"Network": network,
|
"Network": network,
|
||||||
"Port": port,
|
"Port": config.port,
|
||||||
"VHost": vhost,
|
"VHost": config.host,
|
||||||
|
"EthstatsPage": config.ethstats,
|
||||||
|
"ExplorerPage": config.explorer,
|
||||||
|
"WalletPage": config.wallet,
|
||||||
|
"FaucetPage": config.faucet,
|
||||||
})
|
})
|
||||||
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
|
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
|
||||||
|
|
||||||
statsLogin := fmt.Sprintf("yournode:%s", conf.ethstats)
|
statsLogin := fmt.Sprintf("yournode:%s", conf.ethstats)
|
||||||
if !ethstats {
|
if !config.trusted {
|
||||||
statsLogin = ""
|
statsLogin = ""
|
||||||
}
|
}
|
||||||
indexfile := new(bytes.Buffer)
|
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{}{
|
template.Must(template.New("").Parse(dashboardContent)).Execute(indexfile, map[string]interface{}{
|
||||||
"Network": network,
|
"Network": network,
|
||||||
"NetworkID": conf.genesis.Config.ChainId,
|
"NetworkID": conf.Genesis.Config.ChainId,
|
||||||
"NetworkTitle": strings.Title(network),
|
"NetworkTitle": strings.Title(network),
|
||||||
"EthstatsPage": services["ethstats"],
|
"EthstatsPage": config.ethstats,
|
||||||
"ExplorerPage": services["explorer"],
|
"ExplorerPage": config.explorer,
|
||||||
"WalletPage": services["wallet"],
|
"WalletPage": config.wallet,
|
||||||
"FaucetPage": services["faucet"],
|
"FaucetPage": config.faucet,
|
||||||
"GethGenesis": network + ".json",
|
"GethGenesis": network + ".json",
|
||||||
"BootnodesFull": conf.bootFull,
|
"BootnodesFull": conf.bootFull,
|
||||||
"BootnodesLight": conf.bootLight,
|
"BootnodesLight": conf.bootLight,
|
||||||
"BootnodesFullFlat": strings.Join(conf.bootFull, ","),
|
"BootnodesFullFlat": strings.Join(conf.bootFull, ","),
|
||||||
"BootnodesLightFlat": strings.Join(conf.bootLight, ","),
|
"BootnodesLightFlat": strings.Join(conf.bootLight, ","),
|
||||||
"Ethstats": statsLogin,
|
"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()
|
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
|
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
|
files[filepath.Join(workdir, "puppeth.png")] = dashboardMascot
|
||||||
|
|
||||||
// Upload the deployment files to the remote server (and clean up afterwards)
|
// 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)
|
defer client.Run("rm -rf " + workdir)
|
||||||
|
|
||||||
// Build and deploy the dashboard service
|
// 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
|
// dashboardInfos is returned from an dashboard status check to allow reporting
|
||||||
// various configuration parameters.
|
// various configuration parameters.
|
||||||
type dashboardInfos struct {
|
type dashboardInfos struct {
|
||||||
host string
|
host string
|
||||||
port int
|
port int
|
||||||
|
trusted bool
|
||||||
|
|
||||||
|
ethstats string
|
||||||
|
explorer string
|
||||||
|
wallet string
|
||||||
|
faucet string
|
||||||
}
|
}
|
||||||
|
|
||||||
// String implements the stringer interface.
|
// Report converts the typed struct into a plain string->string map, containing
|
||||||
func (info *dashboardInfos) String() string {
|
// most - but not all - fields for reporting to the user.
|
||||||
return fmt.Sprintf("host=%s, port=%d", info.host, info.port)
|
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
|
// 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
|
// Container available, assemble and return the useful infos
|
||||||
return &dashboardInfos{
|
return &dashboardInfos{
|
||||||
host: host,
|
host: host,
|
||||||
port: port,
|
port: port,
|
||||||
|
ethstats: infos.envvars["ETHSTATS_PAGE"],
|
||||||
|
explorer: infos.envvars["EXPLORER_PAGE"],
|
||||||
|
wallet: infos.envvars["WALLET_PAGE"],
|
||||||
|
faucet: infos.envvars["FAUCET_PAGE"],
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
@ -30,21 +31,9 @@ import (
|
|||||||
// ethstatsDockerfile is the Dockerfile required to build an ethstats backend
|
// ethstatsDockerfile is the Dockerfile required to build an ethstats backend
|
||||||
// and associated monitoring site.
|
// and associated monitoring site.
|
||||||
var ethstatsDockerfile = `
|
var ethstatsDockerfile = `
|
||||||
FROM mhart/alpine-node:latest
|
FROM puppeth/ethstats: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
|
|
||||||
|
|
||||||
RUN echo 'module.exports = {trusted: [{{.Trusted}}], banned: [{{.Banned}}], reserved: ["yournode"]};' > lib/utils/config.js
|
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
|
// 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,
|
// deployEthstats deploys a new ethstats container to a remote machine via SSH,
|
||||||
// docker and docker-compose. If an instance with the specified network name
|
// docker and docker-compose. If an instance with the specified network name
|
||||||
// already exists there, it will be overwritten!
|
// 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
|
// Generate the content to upload to the server
|
||||||
workdir := fmt.Sprintf("%d", rand.Int63())
|
workdir := fmt.Sprintf("%d", rand.Int63())
|
||||||
files := make(map[string][]byte)
|
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)
|
defer client.Run("rm -rf " + workdir)
|
||||||
|
|
||||||
// Build and deploy the ethstats service
|
// 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
|
// ethstatsInfos is returned from an ethstats status check to allow reporting
|
||||||
@ -123,9 +115,15 @@ type ethstatsInfos struct {
|
|||||||
banned []string
|
banned []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// String implements the stringer interface.
|
// Report converts the typed struct into a plain string->string map, containing
|
||||||
func (info *ethstatsInfos) String() string {
|
// most - but not all - fields for reporting to the user.
|
||||||
return fmt.Sprintf("host=%s, port=%d, secret=%s, banned=%v", info.host, info.port, info.secret, info.banned)
|
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
|
// checkEthstats does a health-check against an ethstats server to verify whether
|
||||||
|
211
cmd/puppeth/module_explorer.go
Normal file
211
cmd/puppeth/module_explorer.go
Normal file
@ -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
|
||||||
|
}
|
@ -18,6 +18,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
@ -25,36 +26,24 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// faucetDockerfile is the Dockerfile required to build an faucet container to
|
// faucetDockerfile is the Dockerfile required to build an faucet container to
|
||||||
// grant crypto tokens based on GitHub authentications.
|
// grant crypto tokens based on GitHub authentications.
|
||||||
var faucetDockerfile = `
|
var faucetDockerfile = `
|
||||||
FROM alpine:latest
|
FROM ethereum/client-go:alltools-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/*
|
|
||||||
|
|
||||||
ADD genesis.json /genesis.json
|
ADD genesis.json /genesis.json
|
||||||
ADD account.json /account.json
|
ADD account.json /account.json
|
||||||
ADD account.pass /account.pass
|
ADD account.pass /account.pass
|
||||||
|
|
||||||
EXPOSE 8080
|
ENTRYPOINT [ \
|
||||||
|
"faucet", "--genesis", "/genesis.json", "--network", "{{.NetworkID}}", "--bootnodes", "{{.Bootnodes}}", "--ethstats", "{{.Ethstats}}", "--ethport", "{{.EthPort}}", \
|
||||||
CMD [ \
|
"--faucet.name", "{{.FaucetName}}", "--faucet.amount", "{{.FaucetAmount}}", "--faucet.minutes", "{{.FaucetMinutes}}", "--faucet.tiers", "{{.FaucetTiers}}", \
|
||||||
"/faucet", "--genesis", "/genesis.json", "--network", "{{.NetworkID}}", "--bootnodes", "{{.Bootnodes}}", "--ethstats", "{{.Ethstats}}", "--ethport", "{{.EthPort}}", \
|
"--account.json", "/account.json", "--account.pass", "/account.pass" \
|
||||||
"--faucet.name", "{{.FaucetName}}", "--faucet.amount", "{{.FaucetAmount}}", "--faucet.minutes", "{{.FaucetMinutes}}", "--faucet.tiers", "{{.FaucetTiers}}", \
|
{{if .CaptchaToken}}, "--captcha.token", "{{.CaptchaToken}}", "--captcha.secret", "{{.CaptchaSecret}}"{{end}}{{if .NoAuth}}, "--noauth"{{end}} \
|
||||||
"--github.user", "{{.GitHubUser}}", "--github.token", "{{.GitHubToken}}", "--account.json", "/account.json", "--account.pass", "/account.pass" \
|
|
||||||
{{if .CaptchaToken}}, "--captcha.token", "{{.CaptchaToken}}", "--captcha.secret", "{{.CaptchaSecret}}"{{end}} \
|
|
||||||
]`
|
]`
|
||||||
|
|
||||||
// faucetComposefile is the docker-compose.yml file required to deploy and maintain
|
// faucetComposefile is the docker-compose.yml file required to deploy and maintain
|
||||||
@ -76,10 +65,9 @@ services:
|
|||||||
- FAUCET_AMOUNT={{.FaucetAmount}}
|
- FAUCET_AMOUNT={{.FaucetAmount}}
|
||||||
- FAUCET_MINUTES={{.FaucetMinutes}}
|
- FAUCET_MINUTES={{.FaucetMinutes}}
|
||||||
- FAUCET_TIERS={{.FaucetTiers}}
|
- FAUCET_TIERS={{.FaucetTiers}}
|
||||||
- GITHUB_USER={{.GitHubUser}}
|
|
||||||
- GITHUB_TOKEN={{.GitHubToken}}
|
|
||||||
- CAPTCHA_TOKEN={{.CaptchaToken}}
|
- CAPTCHA_TOKEN={{.CaptchaToken}}
|
||||||
- CAPTCHA_SECRET={{.CaptchaSecret}}{{if .VHost}}
|
- CAPTCHA_SECRET={{.CaptchaSecret}}
|
||||||
|
- NO_AUTH={{.NoAuth}}{{if .VHost}}
|
||||||
- VIRTUAL_HOST={{.VHost}}
|
- VIRTUAL_HOST={{.VHost}}
|
||||||
- VIRTUAL_PORT=8080{{end}}
|
- VIRTUAL_PORT=8080{{end}}
|
||||||
logging:
|
logging:
|
||||||
@ -93,7 +81,7 @@ services:
|
|||||||
// deployFaucet deploys a new faucet container to a remote machine via SSH,
|
// deployFaucet deploys a new faucet container to a remote machine via SSH,
|
||||||
// docker and docker-compose. If an instance with the specified network name
|
// docker and docker-compose. If an instance with the specified network name
|
||||||
// already exists there, it will be overwritten!
|
// 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
|
// Generate the content to upload to the server
|
||||||
workdir := fmt.Sprintf("%d", rand.Int63())
|
workdir := fmt.Sprintf("%d", rand.Int63())
|
||||||
files := make(map[string][]byte)
|
files := make(map[string][]byte)
|
||||||
@ -104,14 +92,13 @@ func deployFaucet(client *sshClient, network string, bootnodes []string, config
|
|||||||
"Bootnodes": strings.Join(bootnodes, ","),
|
"Bootnodes": strings.Join(bootnodes, ","),
|
||||||
"Ethstats": config.node.ethstats,
|
"Ethstats": config.node.ethstats,
|
||||||
"EthPort": config.node.portFull,
|
"EthPort": config.node.portFull,
|
||||||
"GitHubUser": config.githubUser,
|
|
||||||
"GitHubToken": config.githubToken,
|
|
||||||
"CaptchaToken": config.captchaToken,
|
"CaptchaToken": config.captchaToken,
|
||||||
"CaptchaSecret": config.captchaSecret,
|
"CaptchaSecret": config.captchaSecret,
|
||||||
"FaucetName": strings.Title(network),
|
"FaucetName": strings.Title(network),
|
||||||
"FaucetAmount": config.amount,
|
"FaucetAmount": config.amount,
|
||||||
"FaucetMinutes": config.minutes,
|
"FaucetMinutes": config.minutes,
|
||||||
"FaucetTiers": config.tiers,
|
"FaucetTiers": config.tiers,
|
||||||
|
"NoAuth": config.noauth,
|
||||||
})
|
})
|
||||||
files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
|
files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
|
||||||
|
|
||||||
@ -123,13 +110,12 @@ func deployFaucet(client *sshClient, network string, bootnodes []string, config
|
|||||||
"ApiPort": config.port,
|
"ApiPort": config.port,
|
||||||
"EthPort": config.node.portFull,
|
"EthPort": config.node.portFull,
|
||||||
"EthName": config.node.ethstats[:strings.Index(config.node.ethstats, ":")],
|
"EthName": config.node.ethstats[:strings.Index(config.node.ethstats, ":")],
|
||||||
"GitHubUser": config.githubUser,
|
|
||||||
"GitHubToken": config.githubToken,
|
|
||||||
"CaptchaToken": config.captchaToken,
|
"CaptchaToken": config.captchaToken,
|
||||||
"CaptchaSecret": config.captchaSecret,
|
"CaptchaSecret": config.captchaSecret,
|
||||||
"FaucetAmount": config.amount,
|
"FaucetAmount": config.amount,
|
||||||
"FaucetMinutes": config.minutes,
|
"FaucetMinutes": config.minutes,
|
||||||
"FaucetTiers": config.tiers,
|
"FaucetTiers": config.tiers,
|
||||||
|
"NoAuth": config.noauth,
|
||||||
})
|
})
|
||||||
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
|
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)
|
defer client.Run("rm -rf " + workdir)
|
||||||
|
|
||||||
// Build and deploy the faucet service
|
// 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
|
// faucetInfos is returned from an faucet status check to allow reporting various
|
||||||
@ -156,15 +145,38 @@ type faucetInfos struct {
|
|||||||
amount int
|
amount int
|
||||||
minutes int
|
minutes int
|
||||||
tiers int
|
tiers int
|
||||||
githubUser string
|
noauth bool
|
||||||
githubToken string
|
|
||||||
captchaToken string
|
captchaToken string
|
||||||
captchaSecret string
|
captchaSecret string
|
||||||
}
|
}
|
||||||
|
|
||||||
// String implements the stringer interface.
|
// Report converts the typed struct into a plain string->string map, containing
|
||||||
func (info *faucetInfos) String() string {
|
// most - but not all - fields for reporting to the user.
|
||||||
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)
|
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
|
// 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,
|
amount: amount,
|
||||||
minutes: minutes,
|
minutes: minutes,
|
||||||
tiers: tiers,
|
tiers: tiers,
|
||||||
githubUser: infos.envvars["GITHUB_USER"],
|
|
||||||
githubToken: infos.envvars["GITHUB_TOKEN"],
|
|
||||||
captchaToken: infos.envvars["CAPTCHA_TOKEN"],
|
captchaToken: infos.envvars["CAPTCHA_TOKEN"],
|
||||||
captchaSecret: infos.envvars["CAPTCHA_SECRET"],
|
captchaSecret: infos.envvars["CAPTCHA_SECRET"],
|
||||||
|
noauth: infos.envvars["NO_AUTH"] == "true",
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
)
|
)
|
||||||
@ -54,7 +55,7 @@ services:
|
|||||||
// deployNginx deploys a new nginx reverse-proxy container to expose one or more
|
// 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
|
// HTTP services running on a single host. If an instance with the specified
|
||||||
// network name already exists there, it will be overwritten!
|
// 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)
|
log.Info("Deploying nginx reverse-proxy", "server", client.server, "port", port)
|
||||||
|
|
||||||
// Generate the content to upload to the server
|
// 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)
|
defer client.Run("rm -rf " + workdir)
|
||||||
|
|
||||||
// Build and deploy the ethstats service
|
// Build and deploy the reverse-proxy 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))
|
||||||
}
|
}
|
||||||
|
|
||||||
// nginxInfos is returned from an nginx reverse-proxy status check to allow
|
// nginxInfos is returned from an nginx reverse-proxy status check to allow
|
||||||
@ -88,9 +92,12 @@ type nginxInfos struct {
|
|||||||
port int
|
port int
|
||||||
}
|
}
|
||||||
|
|
||||||
// String implements the stringer interface.
|
// Report converts the typed struct into a plain string->string map, containing
|
||||||
func (info *nginxInfos) String() string {
|
// most - but not all - fields for reporting to the user.
|
||||||
return fmt.Sprintf("port=%d", info.port)
|
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
|
// checkNginx does a health-check against an nginx reverse-proxy to verify whether
|
||||||
|
@ -18,6 +18,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -25,6 +26,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -38,9 +40,9 @@ ADD genesis.json /genesis.json
|
|||||||
ADD signer.pass /signer.pass
|
ADD signer.pass /signer.pass
|
||||||
{{end}}
|
{{end}}
|
||||||
RUN \
|
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 '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"]
|
ENTRYPOINT ["/bin/sh", "geth.sh"]
|
||||||
`
|
`
|
||||||
@ -58,7 +60,8 @@ services:
|
|||||||
- "{{.FullPort}}:{{.FullPort}}/udp"{{if .Light}}
|
- "{{.FullPort}}:{{.FullPort}}/udp"{{if .Light}}
|
||||||
- "{{.LightPort}}:{{.LightPort}}/udp"{{end}}
|
- "{{.LightPort}}:{{.LightPort}}/udp"{{end}}
|
||||||
volumes:
|
volumes:
|
||||||
- {{.Datadir}}:/root/.ethereum
|
- {{.Datadir}}:/root/.ethereum{{if .Ethashdir}}
|
||||||
|
- {{.Ethashdir}}:/root/.ethash{{end}}
|
||||||
environment:
|
environment:
|
||||||
- FULL_PORT={{.FullPort}}/tcp
|
- FULL_PORT={{.FullPort}}/tcp
|
||||||
- LIGHT_PORT={{.LightPort}}/udp
|
- LIGHT_PORT={{.LightPort}}/udp
|
||||||
@ -79,7 +82,7 @@ services:
|
|||||||
// deployNode deploys a new Ethereum node container to a remote machine via SSH,
|
// 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
|
// docker and docker-compose. If an instance with the specified network name
|
||||||
// already exists there, it will be overwritten!
|
// 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"
|
kind := "sealnode"
|
||||||
if config.keyJSON == "" && config.etherbase == "" {
|
if config.keyJSON == "" && config.etherbase == "" {
|
||||||
kind = "bootnode"
|
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{}{
|
template.Must(template.New("").Parse(nodeComposefile)).Execute(composefile, map[string]interface{}{
|
||||||
"Type": kind,
|
"Type": kind,
|
||||||
"Datadir": config.datadir,
|
"Datadir": config.datadir,
|
||||||
|
"Ethashdir": config.ethashdir,
|
||||||
"Network": network,
|
"Network": network,
|
||||||
"FullPort": config.portFull,
|
"FullPort": config.portFull,
|
||||||
"TotalPeers": config.peersTotal,
|
"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()
|
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
|
||||||
|
|
||||||
//genesisfile, _ := json.MarshalIndent(config.genesis, "", " ")
|
|
||||||
files[filepath.Join(workdir, "genesis.json")] = config.genesis
|
files[filepath.Join(workdir, "genesis.json")] = config.genesis
|
||||||
|
|
||||||
if config.keyJSON != "" {
|
if config.keyJSON != "" {
|
||||||
files[filepath.Join(workdir, "signer.json")] = []byte(config.keyJSON)
|
files[filepath.Join(workdir, "signer.json")] = []byte(config.keyJSON)
|
||||||
files[filepath.Join(workdir, "signer.pass")] = []byte(config.keyPass)
|
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)
|
defer client.Run("rm -rf " + workdir)
|
||||||
|
|
||||||
// Build and deploy the boot or seal node service
|
// 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
|
// nodeInfos is returned from a boot or seal node status check to allow reporting
|
||||||
@ -150,6 +155,7 @@ type nodeInfos struct {
|
|||||||
genesis []byte
|
genesis []byte
|
||||||
network int64
|
network int64
|
||||||
datadir string
|
datadir string
|
||||||
|
ethashdir string
|
||||||
ethstats string
|
ethstats string
|
||||||
portFull int
|
portFull int
|
||||||
portLight int
|
portLight int
|
||||||
@ -164,14 +170,43 @@ type nodeInfos struct {
|
|||||||
gasPrice float64
|
gasPrice float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// String implements the stringer interface.
|
// Report converts the typed struct into a plain string->string map, containing
|
||||||
func (info *nodeInfos) String() string {
|
// most - but not all - fields for reporting to the user.
|
||||||
discv5 := ""
|
func (info *nodeInfos) Report() map[string]string {
|
||||||
if info.peersLight > 0 {
|
report := map[string]string{
|
||||||
discv5 = fmt.Sprintf(", portv5=%d", info.portLight)
|
"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,
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("port=%d%s, datadir=%s, peers=%d, lights=%d, ethstats=%s, gastarget=%0.3f MGas, gasprice=%0.3f GWei",
|
if info.peersLight > 0 {
|
||||||
info.portFull, discv5, info.datadir, info.peersTotal, info.peersLight, info.ethstats, info.gasTarget, info.gasPrice)
|
// 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 report
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkNode does a health-check against an boot or seal node server to verify
|
// 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{
|
stats := &nodeInfos{
|
||||||
genesis: genesis,
|
genesis: genesis,
|
||||||
datadir: infos.volumes["/root/.ethereum"],
|
datadir: infos.volumes["/root/.ethereum"],
|
||||||
|
ethashdir: infos.volumes["/root/.ethash"],
|
||||||
portFull: infos.portmap[infos.envvars["FULL_PORT"]],
|
portFull: infos.portmap[infos.envvars["FULL_PORT"]],
|
||||||
portLight: infos.portmap[infos.envvars["LIGHT_PORT"]],
|
portLight: infos.portmap[infos.envvars["LIGHT_PORT"]],
|
||||||
peersTotal: totalPeers,
|
peersTotal: totalPeers,
|
||||||
|
200
cmd/puppeth/module_wallet.go
Normal file
200
cmd/puppeth/module_wallet.go
Normal file
@ -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
|
||||||
|
}
|
@ -38,7 +38,7 @@ func main() {
|
|||||||
},
|
},
|
||||||
cli.IntFlag{
|
cli.IntFlag{
|
||||||
Name: "loglevel",
|
Name: "loglevel",
|
||||||
Value: 4,
|
Value: 3,
|
||||||
Usage: "log level to emit to the screen",
|
Usage: "log level to emit to the screen",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -116,6 +116,7 @@ func dial(server string, pubkey []byte) (*sshClient, error) {
|
|||||||
keycheck := func(hostname string, remote net.Addr, key ssh.PublicKey) 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 no public key is known for SSH, ask the user to confirm
|
||||||
if pubkey == nil {
|
if pubkey == nil {
|
||||||
|
fmt.Println()
|
||||||
fmt.Printf("The authenticity of host '%s (%s)' can't be established.\n", hostname, remote)
|
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("SSH key fingerprint is %s [MD5]\n", ssh.FingerprintLegacyMD5(key))
|
||||||
fmt.Printf("Are you sure you want to continue connecting (yes/no)? ")
|
fmt.Printf("Are you sure you want to continue connecting (yes/no)? ")
|
||||||
|
@ -28,6 +28,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/core"
|
"github.com/ethereum/go-ethereum/core"
|
||||||
@ -38,12 +39,12 @@ import (
|
|||||||
// config contains all the configurations needed by puppeth that should be saved
|
// config contains all the configurations needed by puppeth that should be saved
|
||||||
// between sessions.
|
// between sessions.
|
||||||
type config struct {
|
type config struct {
|
||||||
path string // File containing the configuration values
|
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
|
||||||
bootFull []string // Bootnodes to always connect to by full nodes
|
bootLight []string // Bootnodes to always connect to by light nodes
|
||||||
bootLight []string // Bootnodes to always connect to by light nodes
|
ethstats string // Ethstats settings to cache for node deploys
|
||||||
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"`
|
Servers map[string][]byte `json:"servers,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +76,8 @@ type wizard struct {
|
|||||||
servers map[string]*sshClient // SSH connections to servers to administer
|
servers map[string]*sshClient // SSH connections to servers to administer
|
||||||
services map[string][]string // Ethereum services known to be running on servers
|
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.
|
// read reads a single line from stdin, trimming if from spaces.
|
||||||
|
@ -40,6 +40,8 @@ func (w *wizard) deployDashboard() {
|
|||||||
host: client.server,
|
host: client.server,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
existed := err == nil
|
||||||
|
|
||||||
// Figure out which port to listen on
|
// Figure out which port to listen on
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("Which port should the dashboard listen on? (default = %d)\n", infos.port)
|
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)
|
available[service] = append(available[service], server)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
listing := make(map[string]string)
|
|
||||||
for _, service := range []string{"ethstats", "explorer", "wallet", "faucet"} {
|
for _, service := range []string{"ethstats", "explorer", "wallet", "faucet"} {
|
||||||
// Gather all the locally hosted pages of this type
|
// Gather all the locally hosted pages of this type
|
||||||
var pages []string
|
var pages []string
|
||||||
@ -74,6 +75,14 @@ func (w *wizard) deployDashboard() {
|
|||||||
if infos, err := checkEthstats(client, w.network); err == nil {
|
if infos, err := checkEthstats(client, w.network); err == nil {
|
||||||
port = infos.port
|
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":
|
case "faucet":
|
||||||
if infos, err := checkFaucet(client, w.network); err == nil {
|
if infos, err := checkFaucet(client, w.network); err == nil {
|
||||||
port = infos.port
|
port = infos.port
|
||||||
@ -101,26 +110,43 @@ func (w *wizard) deployDashboard() {
|
|||||||
log.Error("Invalid listing choice, aborting")
|
log.Error("Invalid listing choice, aborting")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
var page string
|
||||||
switch {
|
switch {
|
||||||
case choice <= len(pages):
|
case choice <= len(pages):
|
||||||
listing[service] = pages[choice-1]
|
page = pages[choice-1]
|
||||||
case choice == len(pages)+1:
|
case choice == len(pages)+1:
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("Which address is the external %s service at?\n", service)
|
fmt.Printf("Which address is the external %s service at?\n", service)
|
||||||
listing[service] = w.readString()
|
page = w.readString()
|
||||||
default:
|
default:
|
||||||
// No service hosting for this
|
// 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
|
// If we have ethstats running, ask whether to make the secret public or not
|
||||||
var ethstats bool
|
|
||||||
if w.conf.ethstats != "" {
|
if w.conf.ethstats != "" {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("Include ethstats secret on dashboard (y/n)? (default = yes)")
|
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
|
// 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)
|
log.Error("Failed to deploy dashboard container", "err", err)
|
||||||
if len(out) > 0 {
|
if len(out) > 0 {
|
||||||
fmt.Printf("%s\n", out)
|
fmt.Printf("%s\n", out)
|
||||||
@ -128,5 +154,5 @@ func (w *wizard) deployDashboard() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// All ok, run a network scan to pick any changes up
|
// All ok, run a network scan to pick any changes up
|
||||||
w.networkStats(false)
|
w.networkStats()
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,8 @@ func (w *wizard) deployEthstats() {
|
|||||||
secret: "",
|
secret: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
existed := err == nil
|
||||||
|
|
||||||
// Figure out which port to listen on
|
// Figure out which port to listen on
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("Which port should ethstats listen on? (default = %d)\n", infos.port)
|
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)
|
infos.secret = w.readDefaultString(infos.secret)
|
||||||
}
|
}
|
||||||
// Gather any blacklists to ban from reporting
|
// Gather any blacklists to ban from reporting
|
||||||
fmt.Println()
|
if existed {
|
||||||
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.Println()
|
||||||
fmt.Printf("Clear out blacklist and start over (y/n)? (default = no)\n")
|
fmt.Printf("Keep existing IP %v blacklist (y/n)? (default = yes)\n", infos.banned)
|
||||||
if w.readDefaultString("n") != "n" {
|
if w.readDefaultString("y") != "y" {
|
||||||
infos.banned = nil
|
// The user might want to clear the entire list, although generally probably not
|
||||||
}
|
fmt.Println()
|
||||||
// Offer the user to explicitly add/remove certain IP addresses
|
fmt.Printf("Clear out blacklist and start over (y/n)? (default = no)\n")
|
||||||
fmt.Println()
|
if w.readDefaultString("n") != "n" {
|
||||||
fmt.Println("Which additional IP addresses should be blacklisted?")
|
infos.banned = nil
|
||||||
for {
|
|
||||||
if ip := w.readIPAddress(); ip != "" {
|
|
||||||
infos.banned = append(infos.banned, ip)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
break
|
// Offer the user to explicitly add/remove certain IP addresses
|
||||||
}
|
fmt.Println()
|
||||||
fmt.Println()
|
fmt.Println("Which additional IP addresses should be blacklisted?")
|
||||||
fmt.Println("Which IP addresses should not be blacklisted?")
|
for {
|
||||||
for {
|
if ip := w.readIPAddress(); ip != "" {
|
||||||
if ip := w.readIPAddress(); ip != "" {
|
infos.banned = append(infos.banned, ip)
|
||||||
for i, addr := range infos.banned {
|
continue
|
||||||
if ip == addr {
|
|
||||||
infos.banned = append(infos.banned[:i], infos.banned[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
continue
|
break
|
||||||
}
|
}
|
||||||
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
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
sort.Strings(infos.banned)
|
||||||
}
|
}
|
||||||
sort.Strings(infos.banned)
|
|
||||||
}
|
}
|
||||||
// Try to deploy the ethstats server on the host
|
// 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))
|
trusted := make([]string, 0, len(w.servers))
|
||||||
for _, client := range w.servers {
|
for _, client := range w.servers {
|
||||||
if client != nil {
|
if client != nil {
|
||||||
trusted = append(trusted, client.address)
|
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)
|
log.Error("Failed to deploy ethstats container", "err", err)
|
||||||
if len(out) > 0 {
|
if len(out) > 0 {
|
||||||
fmt.Printf("%s\n", out)
|
fmt.Printf("%s\n", out)
|
||||||
@ -112,5 +122,5 @@ func (w *wizard) deployEthstats() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// All ok, run a network scan to pick any changes up
|
// All ok, run a network scan to pick any changes up
|
||||||
w.networkStats(false)
|
w.networkStats()
|
||||||
}
|
}
|
||||||
|
117
cmd/puppeth/wizard_explorer.go
Normal file
117
cmd/puppeth/wizard_explorer.go
Normal file
@ -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()
|
||||||
|
}
|
@ -19,7 +19,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
@ -47,8 +46,10 @@ func (w *wizard) deployFaucet() {
|
|||||||
tiers: 3,
|
tiers: 3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
infos.node.genesis, _ = json.MarshalIndent(w.conf.genesis, "", " ")
|
existed := err == nil
|
||||||
infos.node.network = w.conf.genesis.Config.ChainId.Int64()
|
|
||||||
|
infos.node.genesis, _ = json.MarshalIndent(w.conf.Genesis, "", " ")
|
||||||
|
infos.node.network = w.conf.Genesis.Config.ChainId.Int64()
|
||||||
|
|
||||||
// Figure out which port to listen on
|
// Figure out which port to listen on
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
@ -60,7 +61,7 @@ func (w *wizard) deployFaucet() {
|
|||||||
log.Error("Failed to decide on faucet host", "err", err)
|
log.Error("Failed to decide on faucet host", "err", err)
|
||||||
return
|
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.Println()
|
||||||
fmt.Printf("How many Ethers to release per request? (default = %d)\n", infos.amount)
|
fmt.Printf("How many Ethers to release per request? (default = %d)\n", infos.amount)
|
||||||
infos.amount = w.readDefaultInt(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")
|
log.Error("At least one funding tier must be set")
|
||||||
return
|
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
|
// Accessing the reCaptcha service requires API authorizations, request it
|
||||||
if infos.captchaToken != "" {
|
if infos.captchaToken != "" {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
@ -129,7 +89,9 @@ func (w *wizard) deployFaucet() {
|
|||||||
// No previous authorization (or old one discarded)
|
// No previous authorization (or old one discarded)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("Enable reCaptcha protection against robots (y/n)? (default = no)")
|
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
|
// Captcha protection explicitly requested, read the site and secret keys
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("What is the reCaptcha site key to authenticate human users?\n")
|
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()
|
||||||
fmt.Println("Please paste the faucet's funding account key JSON:")
|
fmt.Println("Please paste the faucet's funding account key JSON:")
|
||||||
infos.node.keyJSON = w.readJSON()
|
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 {
|
if _, err := keystore.DecryptKey([]byte(infos.node.keyJSON), infos.node.keyPass); err != nil {
|
||||||
log.Error("Failed to decrypt key with given passphrase")
|
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
|
// 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)
|
log.Error("Failed to deploy faucet container", "err", err)
|
||||||
if len(out) > 0 {
|
if len(out) > 0 {
|
||||||
fmt.Printf("%s\n", out)
|
fmt.Printf("%s\n", out)
|
||||||
@ -198,5 +176,5 @@ func (w *wizard) deployFaucet() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// All ok, run a network scan to pick any changes up
|
// All ok, run a network scan to pick any changes up
|
||||||
w.networkStats(false)
|
w.networkStats()
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ func (w *wizard) makeGenesis() {
|
|||||||
genesis := &core.Genesis{
|
genesis := &core.Genesis{
|
||||||
Timestamp: uint64(time.Now().Unix()),
|
Timestamp: uint64(time.Now().Unix()),
|
||||||
GasLimit: 4700000,
|
GasLimit: 4700000,
|
||||||
Difficulty: big.NewInt(1048576),
|
Difficulty: big.NewInt(524288),
|
||||||
Alloc: make(core.GenesisAlloc),
|
Alloc: make(core.GenesisAlloc),
|
||||||
Config: ¶ms.ChainConfig{
|
Config: ¶ms.ChainConfig{
|
||||||
HomesteadBlock: big.NewInt(1),
|
HomesteadBlock: big.NewInt(1),
|
||||||
@ -118,24 +118,16 @@ func (w *wizard) makeGenesis() {
|
|||||||
for i := int64(0); i < 256; i++ {
|
for i := int64(0); i < 256; i++ {
|
||||||
genesis.Alloc[common.BigToAddress(big.NewInt(i))] = core.GenesisAccount{Balance: big.NewInt(1)}
|
genesis.Alloc[common.BigToAddress(big.NewInt(i))] = core.GenesisAccount{Balance: big.NewInt(1)}
|
||||||
}
|
}
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// Query the user for some custom extras
|
// Query the user for some custom extras
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("Specify your chain/network ID if you want an explicit one (default = random)")
|
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))))
|
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
|
// 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
|
// manageGenesis permits the modification of chain configuration parameters in
|
||||||
@ -145,44 +137,56 @@ func (w *wizard) manageGenesis() {
|
|||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println(" 1. Modify existing fork rules")
|
fmt.Println(" 1. Modify existing fork rules")
|
||||||
fmt.Println(" 2. Export genesis configuration")
|
fmt.Println(" 2. Export genesis configuration")
|
||||||
|
fmt.Println(" 3. Remove genesis configuration")
|
||||||
|
|
||||||
choice := w.read()
|
choice := w.read()
|
||||||
switch {
|
switch {
|
||||||
case choice == "1":
|
case choice == "1":
|
||||||
// Fork rule updating requested, iterate over each fork
|
// Fork rule updating requested, iterate over each fork
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("Which block should Homestead come into effect? (default = %v)\n", 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)
|
w.conf.Genesis.Config.HomesteadBlock = w.readDefaultBigInt(w.conf.Genesis.Config.HomesteadBlock)
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("Which block should EIP150 come into effect? (default = %v)\n", 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)
|
w.conf.Genesis.Config.EIP150Block = w.readDefaultBigInt(w.conf.Genesis.Config.EIP150Block)
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("Which block should EIP155 come into effect? (default = %v)\n", 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)
|
w.conf.Genesis.Config.EIP155Block = w.readDefaultBigInt(w.conf.Genesis.Config.EIP155Block)
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("Which block should EIP158 come into effect? (default = %v)\n", 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)
|
w.conf.Genesis.Config.EIP158Block = w.readDefaultBigInt(w.conf.Genesis.Config.EIP158Block)
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("Which block should Byzantium come into effect? (default = %v)\n", 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)
|
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)
|
fmt.Printf("Chain configuration updated:\n\n%s\n", out)
|
||||||
|
|
||||||
case choice == "2":
|
case choice == "2":
|
||||||
// Save whatever genesis configuration we currently have
|
// Save whatever genesis configuration we currently have
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("Which file to save the genesis into? (default = %s.json)\n", w.network)
|
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 {
|
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.Error("Failed to save genesis file", "err", err)
|
||||||
}
|
}
|
||||||
log.Info("Exported existing genesis block")
|
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:
|
default:
|
||||||
log.Error("That's not something I can do")
|
log.Error("That's not something I can do")
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
)
|
)
|
||||||
@ -63,7 +64,7 @@ func (w *wizard) run() {
|
|||||||
for {
|
for {
|
||||||
w.network = w.readString()
|
w.network = w.readString()
|
||||||
if !strings.Contains(w.network, " ") {
|
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
|
break
|
||||||
}
|
}
|
||||||
log.Error("I also like to live dangerously, still no spaces")
|
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 {
|
} else if err := json.Unmarshal(blob, &w.conf); err != nil {
|
||||||
log.Crit("Previous configuration corrupted", "path", w.conf.path, "err", err)
|
log.Crit("Previous configuration corrupted", "path", w.conf.path, "err", err)
|
||||||
} else {
|
} else {
|
||||||
|
// Dial all previously known servers concurrently
|
||||||
|
var pend sync.WaitGroup
|
||||||
for server, pubkey := range w.conf.Servers {
|
for server, pubkey := range w.conf.Servers {
|
||||||
log.Info("Dialing previously configured server", "server", server)
|
pend.Add(1)
|
||||||
client, err := dial(server, pubkey)
|
|
||||||
if err != nil {
|
go func(server string, pubkey []byte) {
|
||||||
log.Error("Previous server unreachable", "server", server, "err", err)
|
defer pend.Done()
|
||||||
}
|
|
||||||
w.servers[server] = client
|
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
|
// Basics done, loop ad infinitum about what to do
|
||||||
for {
|
for {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("What would you like to do? (default = stats)")
|
fmt.Println("What would you like to do? (default = stats)")
|
||||||
fmt.Println(" 1. Show network stats")
|
fmt.Println(" 1. Show network stats")
|
||||||
if w.conf.genesis == nil {
|
if w.conf.Genesis == nil {
|
||||||
fmt.Println(" 2. Configure new genesis")
|
fmt.Println(" 2. Configure new genesis")
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(" 2. Manage existing genesis")
|
fmt.Println(" 2. Manage existing genesis")
|
||||||
@ -110,15 +122,14 @@ func (w *wizard) run() {
|
|||||||
} else {
|
} else {
|
||||||
fmt.Println(" 4. Manage network components")
|
fmt.Println(" 4. Manage network components")
|
||||||
}
|
}
|
||||||
//fmt.Println(" 5. ProTips for common usecases")
|
|
||||||
|
|
||||||
choice := w.read()
|
choice := w.read()
|
||||||
switch {
|
switch {
|
||||||
case choice == "" || choice == "1":
|
case choice == "" || choice == "1":
|
||||||
w.networkStats(false)
|
w.networkStats()
|
||||||
|
|
||||||
case choice == "2":
|
case choice == "2":
|
||||||
if w.conf.genesis == nil {
|
if w.conf.Genesis == nil {
|
||||||
w.makeGenesis()
|
w.makeGenesis()
|
||||||
} else {
|
} else {
|
||||||
w.manageGenesis()
|
w.manageGenesis()
|
||||||
@ -126,7 +137,7 @@ func (w *wizard) run() {
|
|||||||
case choice == "3":
|
case choice == "3":
|
||||||
if len(w.servers) == 0 {
|
if len(w.servers) == 0 {
|
||||||
if w.makeServer() != "" {
|
if w.makeServer() != "" {
|
||||||
w.networkStats(false)
|
w.networkStats()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
w.manageServers()
|
w.manageServers()
|
||||||
@ -138,9 +149,6 @@ func (w *wizard) run() {
|
|||||||
w.manageComponents()
|
w.manageComponents()
|
||||||
}
|
}
|
||||||
|
|
||||||
case choice == "5":
|
|
||||||
w.networkStats(true)
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
log.Error("That's not something I can do")
|
log.Error("That's not something I can do")
|
||||||
}
|
}
|
||||||
|
@ -18,9 +18,10 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/core"
|
"github.com/ethereum/go-ethereum/core"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
@ -29,127 +30,257 @@ import (
|
|||||||
|
|
||||||
// networkStats verifies the status of network components and generates a protip
|
// networkStats verifies the status of network components and generates a protip
|
||||||
// configuration set to give users hints on how to do various tasks.
|
// 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 {
|
if len(w.servers) == 0 {
|
||||||
log.Error("No remote machines to gather stats from")
|
log.Info("No remote machines to gather stats from")
|
||||||
return
|
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
|
// Iterate over all the specified hosts and check their status
|
||||||
stats := tablewriter.NewWriter(os.Stdout)
|
var pend sync.WaitGroup
|
||||||
stats.SetHeader([]string{"Server", "IP", "Status", "Service", "Details"})
|
|
||||||
stats.SetColWidth(100)
|
|
||||||
|
|
||||||
|
stats := make(serverStats)
|
||||||
for server, pubkey := range w.conf.Servers {
|
for server, pubkey := range w.conf.Servers {
|
||||||
client := w.servers[server]
|
pend.Add(1)
|
||||||
logger := log.New("server", server)
|
|
||||||
logger.Info("Starting remote server health-check")
|
|
||||||
|
|
||||||
// If the server is not connected, try to connect again
|
// Gather the service stats for each server concurrently
|
||||||
if client == nil {
|
go func(server string, pubkey []byte) {
|
||||||
conn, err := dial(server, pubkey)
|
defer pend.Done()
|
||||||
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()
|
|
||||||
|
|
||||||
protips.genesis = string(infos.genesis)
|
stat := w.gatherStats(server, pubkey, w.servers[server])
|
||||||
protips.bootFull = append(protips.bootFull, infos.enodeFull)
|
|
||||||
if infos.enodeLight != "" {
|
// All status checks complete, report and check next server
|
||||||
protips.bootLight = append(protips.bootLight, infos.enodeLight)
|
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
|
||||||
logger.Debug("Checking for sealnode availability")
|
}(server, pubkey)
|
||||||
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)
|
|
||||||
}
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
server, address := client.server, client.address
|
|
||||||
for service, status := range services {
|
|
||||||
stats.Append([]string{server, address, "online", service, status})
|
|
||||||
server, address = "", ""
|
|
||||||
}
|
|
||||||
if len(services) == 0 {
|
|
||||||
stats.Append([]string{server, address, "online", "", ""})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// If a genesis block was found, load it into our configs
|
pend.Wait()
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if protips.ethstats != "" {
|
|
||||||
w.conf.ethstats = protips.ethstats
|
|
||||||
}
|
|
||||||
w.conf.bootFull = protips.bootFull
|
|
||||||
w.conf.bootLight = protips.bootLight
|
|
||||||
|
|
||||||
// Print any collected stats and return
|
// Print any collected stats and return
|
||||||
if !tips {
|
stats.render()
|
||||||
stats.Render()
|
}
|
||||||
} else {
|
|
||||||
protips.print(w.network)
|
// 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
|
||||||
|
}
|
||||||
|
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()}
|
||||||
|
}
|
||||||
|
} 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()}
|
||||||
|
}
|
||||||
|
} 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()}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stat.services["bootnode"] = infos.Report()
|
||||||
|
|
||||||
|
genesis = string(infos.genesis)
|
||||||
|
bootFull = append(bootFull, infos.enodeFull)
|
||||||
|
if infos.enodeLight != "" {
|
||||||
|
bootLight = append(bootLight, infos.enodeLight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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()}
|
||||||
|
}
|
||||||
|
} 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()
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
stat.services["dashboard"] = infos.Report()
|
||||||
|
}
|
||||||
|
// Feed and newly discovered information into the wizard
|
||||||
|
w.lock.Lock()
|
||||||
|
defer w.lock.Unlock()
|
||||||
|
|
||||||
|
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 {
|
||||||
|
w.conf.Genesis = g
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ethstats != "" {
|
||||||
|
w.conf.ethstats = ethstats
|
||||||
|
}
|
||||||
|
w.conf.bootFull = append(w.conf.bootFull, bootFull...)
|
||||||
|
w.conf.bootLight = append(w.conf.bootLight, bootLight...)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// serverStats is a collection of server stats for multiple hosts.
|
||||||
|
type serverStats map[string]*serverStat
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
table.SetHeader([]string{"Server", "Address", "Service", "Config", "Value"})
|
||||||
|
table.SetAlignment(tablewriter.ALIGN_LEFT)
|
||||||
|
table.SetColWidth(100)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
sort.Strings(servers)
|
||||||
|
|
||||||
|
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]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
table.Render()
|
||||||
}
|
}
|
||||||
|
|
||||||
// protips contains a collection of network infos to report pro-tips
|
// protips contains a collection of network infos to report pro-tips
|
||||||
@ -161,75 +292,3 @@ type protips struct {
|
|||||||
bootLight []string
|
bootLight []string
|
||||||
ethstats string
|
ethstats string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
|
||||||
} else {
|
|
||||||
statsflag = fmt.Sprintf(` --ethstats=yournode:%s`, p.ethstats)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 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, ","))
|
|
||||||
}
|
|
||||||
// Assemble all the known pro-tips
|
|
||||||
var tasks, tips []string
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
// If the tips are short, display in a table
|
|
||||||
short := true
|
|
||||||
for _, tip := range tips {
|
|
||||||
if len(tip) > 100 {
|
|
||||||
short = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
if short {
|
|
||||||
howto := tablewriter.NewWriter(os.Stdout)
|
|
||||||
howto.SetHeader([]string{"Fun tasks for you", "Tips on how to"})
|
|
||||||
howto.SetColWidth(100)
|
|
||||||
|
|
||||||
for i := 0; i < len(tasks); i++ {
|
|
||||||
howto.Append([]string{tasks[i], tips[i]})
|
|
||||||
}
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -53,12 +53,12 @@ func (w *wizard) manageServers() {
|
|||||||
w.conf.flush()
|
w.conf.flush()
|
||||||
|
|
||||||
log.Info("Disconnected existing server", "server", server)
|
log.Info("Disconnected existing server", "server", server)
|
||||||
w.networkStats(false)
|
w.networkStats()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// If the user requested connecting a new server, do it
|
// If the user requested connecting a new server, do it
|
||||||
if w.makeServer() != "" {
|
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(" 1. Ethstats - Network monitoring tool")
|
||||||
fmt.Println(" 2. Bootnode - Entry point of the network")
|
fmt.Println(" 2. Bootnode - Entry point of the network")
|
||||||
fmt.Println(" 3. Sealer - Full node minting new blocks")
|
fmt.Println(" 3. Sealer - Full node minting new blocks")
|
||||||
fmt.Println(" 4. Wallet - Browser wallet for quick sends (todo)")
|
fmt.Println(" 4. Explorer - Chain analysis webservice (ethash only)")
|
||||||
fmt.Println(" 5. Faucet - Crypto faucet to give away funds")
|
fmt.Println(" 5. Wallet - Browser wallet for quick sends")
|
||||||
fmt.Println(" 6. Dashboard - Website listing above web-services")
|
fmt.Println(" 6. Faucet - Crypto faucet to give away funds")
|
||||||
|
fmt.Println(" 7. Dashboard - Website listing above web-services")
|
||||||
|
|
||||||
switch w.read() {
|
switch w.read() {
|
||||||
case "1":
|
case "1":
|
||||||
@ -186,9 +187,12 @@ func (w *wizard) deployComponent() {
|
|||||||
case "3":
|
case "3":
|
||||||
w.deployNode(false)
|
w.deployNode(false)
|
||||||
case "4":
|
case "4":
|
||||||
|
w.deployExplorer()
|
||||||
case "5":
|
case "5":
|
||||||
w.deployFaucet()
|
w.deployWallet()
|
||||||
case "6":
|
case "6":
|
||||||
|
w.deployFaucet()
|
||||||
|
case "7":
|
||||||
w.deployDashboard()
|
w.deployDashboard()
|
||||||
default:
|
default:
|
||||||
log.Error("That's not something I can do")
|
log.Error("That's not something I can do")
|
||||||
|
@ -29,7 +29,8 @@ import (
|
|||||||
//
|
//
|
||||||
// If the user elects not to use a reverse proxy, an empty hostname is returned!
|
// 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) {
|
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
|
// Reverse proxy is running, if ports match, we need a virtual host
|
||||||
if proxy.port == port {
|
if proxy.port == port {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
@ -41,7 +42,13 @@ func (w *wizard) ensureVirtualHost(client *sshClient, port int, def string) (str
|
|||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("Allow sharing the port with other services (y/n)? (default = yes)")
|
fmt.Println("Allow sharing the port with other services (y/n)? (default = yes)")
|
||||||
if w.readDefaultString("y") == "y" {
|
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)
|
log.Error("Failed to deploy reverse-proxy", "err", err)
|
||||||
if len(out) > 0 {
|
if len(out) > 0 {
|
||||||
fmt.Printf("%s\n", out)
|
fmt.Printf("%s\n", out)
|
||||||
|
@ -29,7 +29,7 @@ import (
|
|||||||
// deployNode creates a new node configuration based on some user input.
|
// deployNode creates a new node configuration based on some user input.
|
||||||
func (w *wizard) deployNode(boot bool) {
|
func (w *wizard) deployNode(boot bool) {
|
||||||
// Do some sanity check before the user wastes time on input
|
// 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")
|
log.Error("No genesis block configured")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -44,7 +44,7 @@ func (w *wizard) deployNode(boot bool) {
|
|||||||
}
|
}
|
||||||
client := w.servers[server]
|
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)
|
infos, err := checkNode(client, w.network, boot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if boot {
|
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 = &nodeInfos{portFull: 30303, peersTotal: 50, peersLight: 0, gasTarget: 4.7, gasPrice: 18}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
infos.genesis, _ = json.MarshalIndent(w.conf.genesis, "", " ")
|
existed := err == nil
|
||||||
infos.network = w.conf.genesis.Config.ChainId.Int64()
|
|
||||||
|
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
|
// Figure out where the user wants to store the persistent data
|
||||||
fmt.Println()
|
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)
|
fmt.Printf("Where should data be stored on the remote machine? (default = %s)\n", infos.datadir)
|
||||||
infos.datadir = w.readDefaultString(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
|
// Figure out which port to listen on
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("Which TCP/UDP port to listen on? (default = %d)\n", infos.portFull)
|
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 the node is a miner/signer, load up needed credentials
|
||||||
if !boot {
|
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
|
// Ethash based miners only need an etherbase to mine against
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
if infos.etherbase == "" {
|
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)
|
fmt.Printf("What address should the miner user? (default = %s)\n", infos.etherbase)
|
||||||
infos.etherbase = w.readDefaultAddress(common.HexToAddress(infos.etherbase)).Hex()
|
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 a previous signer was already set, offer to reuse it
|
||||||
if infos.keyJSON != "" {
|
if infos.keyJSON != "" {
|
||||||
if key, err := keystore.DecryptKey([]byte(infos.keyJSON), infos.keyPass); err != nil {
|
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)
|
infos.gasPrice = w.readDefaultFloat(infos.gasPrice)
|
||||||
}
|
}
|
||||||
// Try to deploy the full node on the host
|
// 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)
|
log.Error("Failed to deploy Ethereum node container", "err", err)
|
||||||
if len(out) > 0 {
|
if len(out) > 0 {
|
||||||
fmt.Printf("%s\n", out)
|
fmt.Printf("%s\n", out)
|
||||||
@ -156,5 +174,5 @@ func (w *wizard) deployNode(boot bool) {
|
|||||||
log.Info("Waiting for node to finish booting")
|
log.Info("Waiting for node to finish booting")
|
||||||
time.Sleep(3 * time.Second)
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
w.networkStats(false)
|
w.networkStats()
|
||||||
}
|
}
|
||||||
|
113
cmd/puppeth/wizard_wallet.go
Normal file
113
cmd/puppeth/wizard_wallet.go
Normal file
@ -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()
|
||||||
|
}
|
@ -36,8 +36,8 @@ import (
|
|||||||
|
|
||||||
// Ethash proof-of-work protocol constants.
|
// Ethash proof-of-work protocol constants.
|
||||||
var (
|
var (
|
||||||
frontierBlockReward *big.Int = big.NewInt(5e+18) // Block reward in wei for successfully mining a block
|
FrontierBlockReward *big.Int = big.NewInt(5e+18) // Block reward in wei for successfully mining a block
|
||||||
byzantiumBlockReward *big.Int = big.NewInt(3e+18) // Block reward in wei for successfully mining a block upward from Byzantium
|
ByzantiumBlockReward *big.Int = big.NewInt(3e+18) // Block reward in wei for successfully mining a block upward from Byzantium
|
||||||
maxUncles = 2 // Maximum number of uncles allowed in a single block
|
maxUncles = 2 // Maximum number of uncles allowed in a single block
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -529,9 +529,9 @@ var (
|
|||||||
// TODO (karalabe): Move the chain maker into this package and make this private!
|
// TODO (karalabe): Move the chain maker into this package and make this private!
|
||||||
func AccumulateRewards(config *params.ChainConfig, state *state.StateDB, header *types.Header, uncles []*types.Header) {
|
func AccumulateRewards(config *params.ChainConfig, state *state.StateDB, header *types.Header, uncles []*types.Header) {
|
||||||
// Select the correct block reward based on chain progression
|
// Select the correct block reward based on chain progression
|
||||||
blockReward := frontierBlockReward
|
blockReward := FrontierBlockReward
|
||||||
if config.IsByzantium(header.Number) {
|
if config.IsByzantium(header.Number) {
|
||||||
blockReward = byzantiumBlockReward
|
blockReward = ByzantiumBlockReward
|
||||||
}
|
}
|
||||||
// Accumulate the rewards for the miner and any included uncles
|
// Accumulate the rewards for the miner and any included uncles
|
||||||
reward := new(big.Int).Set(blockReward)
|
reward := new(big.Int).Set(blockReward)
|
||||||
|
Loading…
Reference in New Issue
Block a user