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