mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-19 10:22:23 +00:00
wip
This commit is contained in:
parent
c79192e67e
commit
a5470a6ab5
14 changed files with 57 additions and 1107 deletions
|
|
@ -1,379 +0,0 @@
|
||||||
// Copyright 2016 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 (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
|
||||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
|
||||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
|
||||||
"github.com/ethereum/go-ethereum/console"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"gopkg.in/urfave/cli.v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
walletCommand = cli.Command{
|
|
||||||
Name: "wallet",
|
|
||||||
Usage: "Manage Ethereum presale wallets",
|
|
||||||
ArgsUsage: "",
|
|
||||||
Category: "ACCOUNT COMMANDS",
|
|
||||||
Description: `
|
|
||||||
geth wallet import /path/to/my/presale.wallet
|
|
||||||
|
|
||||||
will prompt for your password and imports your ether presale account.
|
|
||||||
It can be used non-interactively with the --password option taking a
|
|
||||||
passwordfile as argument containing the wallet password in plaintext.`,
|
|
||||||
Subcommands: []cli.Command{
|
|
||||||
{
|
|
||||||
|
|
||||||
Name: "import",
|
|
||||||
Usage: "Import Ethereum presale wallet",
|
|
||||||
ArgsUsage: "<keyFile>",
|
|
||||||
Action: utils.MigrateFlags(importWallet),
|
|
||||||
Category: "ACCOUNT COMMANDS",
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
utils.DataDirFlag,
|
|
||||||
utils.KeyStoreDirFlag,
|
|
||||||
utils.PasswordFileFlag,
|
|
||||||
utils.LightKDFFlag,
|
|
||||||
},
|
|
||||||
Description: `
|
|
||||||
geth wallet [options] /path/to/my/presale.wallet
|
|
||||||
|
|
||||||
will prompt for your password and imports your ether presale account.
|
|
||||||
It can be used non-interactively with the --password option taking a
|
|
||||||
passwordfile as argument containing the wallet password in plaintext.`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
accountCommand = cli.Command{
|
|
||||||
Name: "account",
|
|
||||||
Usage: "Manage accounts",
|
|
||||||
Category: "ACCOUNT COMMANDS",
|
|
||||||
Description: `
|
|
||||||
|
|
||||||
Manage accounts, list all existing accounts, import a private key into a new
|
|
||||||
account, create a new account or update an existing account.
|
|
||||||
|
|
||||||
It supports interactive mode, when you are prompted for password as well as
|
|
||||||
non-interactive mode where passwords are supplied via a given password file.
|
|
||||||
Non-interactive mode is only meant for scripted use on test networks or known
|
|
||||||
safe environments.
|
|
||||||
|
|
||||||
Make sure you remember the password you gave when creating a new account (with
|
|
||||||
either new or import). Without it you are not able to unlock your account.
|
|
||||||
|
|
||||||
Note that exporting your key in unencrypted format is NOT supported.
|
|
||||||
|
|
||||||
Keys are stored under <DATADIR>/keystore.
|
|
||||||
It is safe to transfer the entire directory or the individual keys therein
|
|
||||||
between ethereum nodes by simply copying.
|
|
||||||
|
|
||||||
Make sure you backup your keys regularly.`,
|
|
||||||
Subcommands: []cli.Command{
|
|
||||||
{
|
|
||||||
Name: "list",
|
|
||||||
Usage: "Print summary of existing accounts",
|
|
||||||
Action: utils.MigrateFlags(accountList),
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
utils.DataDirFlag,
|
|
||||||
utils.KeyStoreDirFlag,
|
|
||||||
},
|
|
||||||
Description: `
|
|
||||||
Print a short summary of all accounts`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "new",
|
|
||||||
Usage: "Create a new account",
|
|
||||||
Action: utils.MigrateFlags(accountCreate),
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
utils.DataDirFlag,
|
|
||||||
utils.KeyStoreDirFlag,
|
|
||||||
utils.PasswordFileFlag,
|
|
||||||
utils.LightKDFFlag,
|
|
||||||
},
|
|
||||||
Description: `
|
|
||||||
geth account new
|
|
||||||
|
|
||||||
Creates a new account and prints the address.
|
|
||||||
|
|
||||||
The account is saved in encrypted format, you are prompted for a passphrase.
|
|
||||||
|
|
||||||
You must remember this passphrase to unlock your account in the future.
|
|
||||||
|
|
||||||
For non-interactive use the passphrase can be specified with the --password flag:
|
|
||||||
|
|
||||||
Note, this is meant to be used for testing only, it is a bad idea to save your
|
|
||||||
password to file or expose in any other way.
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "update",
|
|
||||||
Usage: "Update an existing account",
|
|
||||||
Action: utils.MigrateFlags(accountUpdate),
|
|
||||||
ArgsUsage: "<address>",
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
utils.DataDirFlag,
|
|
||||||
utils.KeyStoreDirFlag,
|
|
||||||
utils.LightKDFFlag,
|
|
||||||
},
|
|
||||||
Description: `
|
|
||||||
geth account update <address>
|
|
||||||
|
|
||||||
Update an existing account.
|
|
||||||
|
|
||||||
The account is saved in the newest version in encrypted format, you are prompted
|
|
||||||
for a passphrase to unlock the account and another to save the updated file.
|
|
||||||
|
|
||||||
This same command can therefore be used to migrate an account of a deprecated
|
|
||||||
format to the newest format or change the password for an account.
|
|
||||||
|
|
||||||
For non-interactive use the passphrase can be specified with the --password flag:
|
|
||||||
|
|
||||||
geth account update [options] <address>
|
|
||||||
|
|
||||||
Since only one password can be given, only format update can be performed,
|
|
||||||
changing your password is only possible interactively.
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "import",
|
|
||||||
Usage: "Import a private key into a new account",
|
|
||||||
Action: utils.MigrateFlags(accountImport),
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
utils.DataDirFlag,
|
|
||||||
utils.KeyStoreDirFlag,
|
|
||||||
utils.PasswordFileFlag,
|
|
||||||
utils.LightKDFFlag,
|
|
||||||
},
|
|
||||||
ArgsUsage: "<keyFile>",
|
|
||||||
Description: `
|
|
||||||
geth account import <keyfile>
|
|
||||||
|
|
||||||
Imports an unencrypted private key from <keyfile> and creates a new account.
|
|
||||||
Prints the address.
|
|
||||||
|
|
||||||
The keyfile is assumed to contain an unencrypted private key in hexadecimal format.
|
|
||||||
|
|
||||||
The account is saved in encrypted format, you are prompted for a passphrase.
|
|
||||||
|
|
||||||
You must remember this passphrase to unlock your account in the future.
|
|
||||||
|
|
||||||
For non-interactive use the passphrase can be specified with the -password flag:
|
|
||||||
|
|
||||||
geth account import [options] <keyfile>
|
|
||||||
|
|
||||||
Note:
|
|
||||||
As you can directly copy your encrypted accounts to another ethereum instance,
|
|
||||||
this import mechanism is not needed when you transfer an account between
|
|
||||||
nodes.
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func accountList(ctx *cli.Context) error {
|
|
||||||
stack, _ := makeConfigNode(ctx)
|
|
||||||
var index int
|
|
||||||
for _, wallet := range stack.AccountManager().Wallets() {
|
|
||||||
for _, account := range wallet.Accounts() {
|
|
||||||
fmt.Printf("Account #%d: {%x} %s\n", index, account.Address, &account.URL)
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// tries unlocking the specified account a few times.
|
|
||||||
func unlockAccount(ctx *cli.Context, ks *keystore.KeyStore, address string, i int, passwords []string) (accounts.Account, string) {
|
|
||||||
account, err := utils.MakeAddress(ks, address)
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatalf("Could not list accounts: %v", err)
|
|
||||||
}
|
|
||||||
for trials := 0; trials < 3; trials++ {
|
|
||||||
prompt := fmt.Sprintf("Unlocking account %s | Attempt %d/%d", address, trials+1, 3)
|
|
||||||
password := getPassPhrase(prompt, false, i, passwords)
|
|
||||||
err = ks.Unlock(account, password)
|
|
||||||
if err == nil {
|
|
||||||
log.Info("Unlocked account", "address", account.Address.Hex())
|
|
||||||
return account, password
|
|
||||||
}
|
|
||||||
if err, ok := err.(*keystore.AmbiguousAddrError); ok {
|
|
||||||
log.Info("Unlocked account", "address", account.Address.Hex())
|
|
||||||
return ambiguousAddrRecovery(ks, err, password), password
|
|
||||||
}
|
|
||||||
if err != keystore.ErrDecrypt {
|
|
||||||
// No need to prompt again if the error is not decryption-related.
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// All trials expended to unlock account, bail out
|
|
||||||
utils.Fatalf("Failed to unlock account %s (%v)", address, err)
|
|
||||||
|
|
||||||
return accounts.Account{}, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// getPassPhrase retrieves the password associated with an account, either fetched
|
|
||||||
// from a list of preloaded passphrases, or requested interactively from the user.
|
|
||||||
func getPassPhrase(prompt string, confirmation bool, i int, passwords []string) string {
|
|
||||||
// If a list of passwords was supplied, retrieve from them
|
|
||||||
if len(passwords) > 0 {
|
|
||||||
if i < len(passwords) {
|
|
||||||
return passwords[i]
|
|
||||||
}
|
|
||||||
return passwords[len(passwords)-1]
|
|
||||||
}
|
|
||||||
// Otherwise prompt the user for the password
|
|
||||||
if prompt != "" {
|
|
||||||
fmt.Println(prompt)
|
|
||||||
}
|
|
||||||
password, err := console.Stdin.PromptPassword("Passphrase: ")
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatalf("Failed to read passphrase: %v", err)
|
|
||||||
}
|
|
||||||
if confirmation {
|
|
||||||
confirm, err := console.Stdin.PromptPassword("Repeat passphrase: ")
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatalf("Failed to read passphrase confirmation: %v", err)
|
|
||||||
}
|
|
||||||
if password != confirm {
|
|
||||||
utils.Fatalf("Passphrases do not match")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return password
|
|
||||||
}
|
|
||||||
|
|
||||||
func ambiguousAddrRecovery(ks *keystore.KeyStore, err *keystore.AmbiguousAddrError, auth string) accounts.Account {
|
|
||||||
fmt.Printf("Multiple key files exist for address %x:\n", err.Addr)
|
|
||||||
for _, a := range err.Matches {
|
|
||||||
fmt.Println(" ", a.URL)
|
|
||||||
}
|
|
||||||
fmt.Println("Testing your passphrase against all of them...")
|
|
||||||
var match *accounts.Account
|
|
||||||
for _, a := range err.Matches {
|
|
||||||
if err := ks.Unlock(a, auth); err == nil {
|
|
||||||
match = &a
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if match == nil {
|
|
||||||
utils.Fatalf("None of the listed files could be unlocked.")
|
|
||||||
}
|
|
||||||
fmt.Printf("Your passphrase unlocked %s\n", match.URL)
|
|
||||||
fmt.Println("In order to avoid this warning, you need to remove the following duplicate key files:")
|
|
||||||
for _, a := range err.Matches {
|
|
||||||
if a != *match {
|
|
||||||
fmt.Println(" ", a.URL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return *match
|
|
||||||
}
|
|
||||||
|
|
||||||
// accountCreate creates a new account into the keystore defined by the CLI flags.
|
|
||||||
func accountCreate(ctx *cli.Context) error {
|
|
||||||
cfg := gethConfig{Node: defaultNodeConfig()}
|
|
||||||
// Load config file.
|
|
||||||
if file := ctx.GlobalString(configFileFlag.Name); file != "" {
|
|
||||||
if err := loadConfig(file, &cfg); err != nil {
|
|
||||||
utils.Fatalf("%v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
utils.SetNodeConfig(ctx, &cfg.Node)
|
|
||||||
scryptN, scryptP, keydir, err := cfg.Node.AccountConfig()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatalf("Failed to read configuration: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
password := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx))
|
|
||||||
|
|
||||||
address, err := keystore.StoreKey(keydir, password, scryptN, scryptP)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatalf("Failed to create account: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("Address: {%x}\n", address)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// accountUpdate transitions an account from a previous format to the current
|
|
||||||
// one, also providing the possibility to change the pass-phrase.
|
|
||||||
func accountUpdate(ctx *cli.Context) error {
|
|
||||||
if len(ctx.Args()) == 0 {
|
|
||||||
utils.Fatalf("No accounts specified to update")
|
|
||||||
}
|
|
||||||
stack, _ := makeConfigNode(ctx)
|
|
||||||
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
|
||||||
|
|
||||||
for _, addr := range ctx.Args() {
|
|
||||||
account, oldPassword := unlockAccount(ctx, ks, addr, 0, nil)
|
|
||||||
newPassword := getPassPhrase("Please give a new password. Do not forget this password.", true, 0, nil)
|
|
||||||
if err := ks.Update(account, oldPassword, newPassword); err != nil {
|
|
||||||
utils.Fatalf("Could not update the account: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func importWallet(ctx *cli.Context) error {
|
|
||||||
keyfile := ctx.Args().First()
|
|
||||||
if len(keyfile) == 0 {
|
|
||||||
utils.Fatalf("keyfile must be given as argument")
|
|
||||||
}
|
|
||||||
keyJSON, err := ioutil.ReadFile(keyfile)
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatalf("Could not read wallet file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
stack, _ := makeConfigNode(ctx)
|
|
||||||
passphrase := getPassPhrase("", false, 0, utils.MakePasswordList(ctx))
|
|
||||||
|
|
||||||
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
|
||||||
acct, err := ks.ImportPreSaleKey(keyJSON, passphrase)
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatalf("%v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("Address: {%x}\n", acct.Address)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func accountImport(ctx *cli.Context) error {
|
|
||||||
keyfile := ctx.Args().First()
|
|
||||||
if len(keyfile) == 0 {
|
|
||||||
utils.Fatalf("keyfile must be given as argument")
|
|
||||||
}
|
|
||||||
key, err := crypto.LoadECDSA(keyfile)
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatalf("Failed to load the private key: %v", err)
|
|
||||||
}
|
|
||||||
stack, _ := makeConfigNode(ctx)
|
|
||||||
passphrase := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx))
|
|
||||||
|
|
||||||
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
|
||||||
acct, err := ks.ImportECDSA(key, passphrase)
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatalf("Could not create the account: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("Address: {%x}\n", acct.Address)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,296 +0,0 @@
|
||||||
// Copyright 2016 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 (
|
|
||||||
"io/ioutil"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/cespare/cp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// These tests are 'smoke tests' for the account related
|
|
||||||
// subcommands and flags.
|
|
||||||
//
|
|
||||||
// For most tests, the test files from package accounts
|
|
||||||
// are copied into a temporary keystore directory.
|
|
||||||
|
|
||||||
func tmpDatadirWithKeystore(t *testing.T) string {
|
|
||||||
datadir := tmpdir(t)
|
|
||||||
keystore := filepath.Join(datadir, "keystore")
|
|
||||||
source := filepath.Join("..", "..", "accounts", "keystore", "testdata", "keystore")
|
|
||||||
if err := cp.CopyAll(keystore, source); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
return datadir
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAccountListEmpty(t *testing.T) {
|
|
||||||
geth := runGeth(t, "account", "list")
|
|
||||||
geth.ExpectExit()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAccountList(t *testing.T) {
|
|
||||||
datadir := tmpDatadirWithKeystore(t)
|
|
||||||
geth := runGeth(t, "account", "list", "--datadir", datadir)
|
|
||||||
defer geth.ExpectExit()
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
geth.Expect(`
|
|
||||||
Account #0: {7ef5a6135f1fd6a02593eedc869c6d41d934aef8} keystore://{{.Datadir}}\keystore\UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8
|
|
||||||
Account #1: {f466859ead1932d743d622cb74fc058882e8648a} keystore://{{.Datadir}}\keystore\aaa
|
|
||||||
Account #2: {289d485d9771714cce91d3393d764e1311907acc} keystore://{{.Datadir}}\keystore\zzz
|
|
||||||
`)
|
|
||||||
} else {
|
|
||||||
geth.Expect(`
|
|
||||||
Account #0: {7ef5a6135f1fd6a02593eedc869c6d41d934aef8} keystore://{{.Datadir}}/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8
|
|
||||||
Account #1: {f466859ead1932d743d622cb74fc058882e8648a} keystore://{{.Datadir}}/keystore/aaa
|
|
||||||
Account #2: {289d485d9771714cce91d3393d764e1311907acc} keystore://{{.Datadir}}/keystore/zzz
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAccountNew(t *testing.T) {
|
|
||||||
geth := runGeth(t, "account", "new", "--lightkdf")
|
|
||||||
defer geth.ExpectExit()
|
|
||||||
geth.Expect(`
|
|
||||||
Your new account is locked with a password. Please give a password. Do not forget this password.
|
|
||||||
!! Unsupported terminal, password will be echoed.
|
|
||||||
Passphrase: {{.InputLine "foobar"}}
|
|
||||||
Repeat passphrase: {{.InputLine "foobar"}}
|
|
||||||
`)
|
|
||||||
geth.ExpectRegexp(`Address: \{[0-9a-f]{40}\}\n`)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAccountNewBadRepeat(t *testing.T) {
|
|
||||||
geth := runGeth(t, "account", "new", "--lightkdf")
|
|
||||||
defer geth.ExpectExit()
|
|
||||||
geth.Expect(`
|
|
||||||
Your new account is locked with a password. Please give a password. Do not forget this password.
|
|
||||||
!! Unsupported terminal, password will be echoed.
|
|
||||||
Passphrase: {{.InputLine "something"}}
|
|
||||||
Repeat passphrase: {{.InputLine "something else"}}
|
|
||||||
Fatal: Passphrases do not match
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAccountUpdate(t *testing.T) {
|
|
||||||
datadir := tmpDatadirWithKeystore(t)
|
|
||||||
geth := runGeth(t, "account", "update",
|
|
||||||
"--datadir", datadir, "--lightkdf",
|
|
||||||
"f466859ead1932d743d622cb74fc058882e8648a")
|
|
||||||
defer geth.ExpectExit()
|
|
||||||
geth.Expect(`
|
|
||||||
Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3
|
|
||||||
!! Unsupported terminal, password will be echoed.
|
|
||||||
Passphrase: {{.InputLine "foobar"}}
|
|
||||||
Please give a new password. Do not forget this password.
|
|
||||||
Passphrase: {{.InputLine "foobar2"}}
|
|
||||||
Repeat passphrase: {{.InputLine "foobar2"}}
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWalletImport(t *testing.T) {
|
|
||||||
geth := runGeth(t, "wallet", "import", "--lightkdf", "testdata/guswallet.json")
|
|
||||||
defer geth.ExpectExit()
|
|
||||||
geth.Expect(`
|
|
||||||
!! Unsupported terminal, password will be echoed.
|
|
||||||
Passphrase: {{.InputLine "foo"}}
|
|
||||||
Address: {d4584b5f6229b7be90727b0fc8c6b91bb427821f}
|
|
||||||
`)
|
|
||||||
|
|
||||||
files, err := ioutil.ReadDir(filepath.Join(geth.Datadir, "keystore"))
|
|
||||||
if len(files) != 1 {
|
|
||||||
t.Errorf("expected one key file in keystore directory, found %d files (error: %v)", len(files), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWalletImportBadPassword(t *testing.T) {
|
|
||||||
geth := runGeth(t, "wallet", "import", "--lightkdf", "testdata/guswallet.json")
|
|
||||||
defer geth.ExpectExit()
|
|
||||||
geth.Expect(`
|
|
||||||
!! Unsupported terminal, password will be echoed.
|
|
||||||
Passphrase: {{.InputLine "wrong"}}
|
|
||||||
Fatal: could not decrypt key with given passphrase
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnlockFlag(t *testing.T) {
|
|
||||||
datadir := tmpDatadirWithKeystore(t)
|
|
||||||
geth := runGeth(t,
|
|
||||||
"--datadir", datadir, "--nat", "none", "--nodiscover", "--maxpeers", "0", "--port", "0",
|
|
||||||
"--unlock", "f466859ead1932d743d622cb74fc058882e8648a",
|
|
||||||
"js", "testdata/empty.js")
|
|
||||||
geth.Expect(`
|
|
||||||
Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3
|
|
||||||
!! Unsupported terminal, password will be echoed.
|
|
||||||
Passphrase: {{.InputLine "foobar"}}
|
|
||||||
`)
|
|
||||||
geth.ExpectExit()
|
|
||||||
|
|
||||||
wantMessages := []string{
|
|
||||||
"Unlocked account",
|
|
||||||
"=0xf466859eAD1932D743d622CB74FC058882E8648A",
|
|
||||||
}
|
|
||||||
for _, m := range wantMessages {
|
|
||||||
if !strings.Contains(geth.StderrText(), m) {
|
|
||||||
t.Errorf("stderr text does not contain %q", m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnlockFlagWrongPassword(t *testing.T) {
|
|
||||||
datadir := tmpDatadirWithKeystore(t)
|
|
||||||
geth := runGeth(t,
|
|
||||||
"--datadir", datadir, "--nat", "none", "--nodiscover", "--maxpeers", "0", "--port", "0",
|
|
||||||
"--unlock", "f466859ead1932d743d622cb74fc058882e8648a")
|
|
||||||
defer geth.ExpectExit()
|
|
||||||
geth.Expect(`
|
|
||||||
Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3
|
|
||||||
!! Unsupported terminal, password will be echoed.
|
|
||||||
Passphrase: {{.InputLine "wrong1"}}
|
|
||||||
Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 2/3
|
|
||||||
Passphrase: {{.InputLine "wrong2"}}
|
|
||||||
Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 3/3
|
|
||||||
Passphrase: {{.InputLine "wrong3"}}
|
|
||||||
Fatal: Failed to unlock account f466859ead1932d743d622cb74fc058882e8648a (could not decrypt key with given passphrase)
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/ethereum/go-ethereum/issues/1785
|
|
||||||
func TestUnlockFlagMultiIndex(t *testing.T) {
|
|
||||||
datadir := tmpDatadirWithKeystore(t)
|
|
||||||
geth := runGeth(t,
|
|
||||||
"--datadir", datadir, "--nat", "none", "--nodiscover", "--maxpeers", "0", "--port", "0",
|
|
||||||
"--unlock", "0,2",
|
|
||||||
"js", "testdata/empty.js")
|
|
||||||
geth.Expect(`
|
|
||||||
Unlocking account 0 | Attempt 1/3
|
|
||||||
!! Unsupported terminal, password will be echoed.
|
|
||||||
Passphrase: {{.InputLine "foobar"}}
|
|
||||||
Unlocking account 2 | Attempt 1/3
|
|
||||||
Passphrase: {{.InputLine "foobar"}}
|
|
||||||
`)
|
|
||||||
geth.ExpectExit()
|
|
||||||
|
|
||||||
wantMessages := []string{
|
|
||||||
"Unlocked account",
|
|
||||||
"=0x7EF5A6135f1FD6a02593eEdC869c6D41D934aef8",
|
|
||||||
"=0x289d485D9771714CCe91D3393D764E1311907ACc",
|
|
||||||
}
|
|
||||||
for _, m := range wantMessages {
|
|
||||||
if !strings.Contains(geth.StderrText(), m) {
|
|
||||||
t.Errorf("stderr text does not contain %q", m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnlockFlagPasswordFile(t *testing.T) {
|
|
||||||
datadir := tmpDatadirWithKeystore(t)
|
|
||||||
geth := runGeth(t,
|
|
||||||
"--datadir", datadir, "--nat", "none", "--nodiscover", "--maxpeers", "0", "--port", "0",
|
|
||||||
"--password", "testdata/passwords.txt", "--unlock", "0,2",
|
|
||||||
"js", "testdata/empty.js")
|
|
||||||
geth.ExpectExit()
|
|
||||||
|
|
||||||
wantMessages := []string{
|
|
||||||
"Unlocked account",
|
|
||||||
"=0x7EF5A6135f1FD6a02593eEdC869c6D41D934aef8",
|
|
||||||
"=0x289d485D9771714CCe91D3393D764E1311907ACc",
|
|
||||||
}
|
|
||||||
for _, m := range wantMessages {
|
|
||||||
if !strings.Contains(geth.StderrText(), m) {
|
|
||||||
t.Errorf("stderr text does not contain %q", m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnlockFlagPasswordFileWrongPassword(t *testing.T) {
|
|
||||||
datadir := tmpDatadirWithKeystore(t)
|
|
||||||
geth := runGeth(t,
|
|
||||||
"--datadir", datadir, "--nat", "none", "--nodiscover", "--maxpeers", "0", "--port", "0",
|
|
||||||
"--password", "testdata/wrong-passwords.txt", "--unlock", "0,2")
|
|
||||||
defer geth.ExpectExit()
|
|
||||||
geth.Expect(`
|
|
||||||
Fatal: Failed to unlock account 0 (could not decrypt key with given passphrase)
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnlockFlagAmbiguous(t *testing.T) {
|
|
||||||
store := filepath.Join("..", "..", "accounts", "keystore", "testdata", "dupes")
|
|
||||||
geth := runGeth(t,
|
|
||||||
"--keystore", store, "--nat", "none", "--nodiscover", "--maxpeers", "0", "--port", "0",
|
|
||||||
"--unlock", "f466859ead1932d743d622cb74fc058882e8648a",
|
|
||||||
"js", "testdata/empty.js")
|
|
||||||
defer geth.ExpectExit()
|
|
||||||
|
|
||||||
// Helper for the expect template, returns absolute keystore path.
|
|
||||||
geth.SetTemplateFunc("keypath", func(file string) string {
|
|
||||||
abs, _ := filepath.Abs(filepath.Join(store, file))
|
|
||||||
return abs
|
|
||||||
})
|
|
||||||
geth.Expect(`
|
|
||||||
Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3
|
|
||||||
!! Unsupported terminal, password will be echoed.
|
|
||||||
Passphrase: {{.InputLine "foobar"}}
|
|
||||||
Multiple key files exist for address f466859ead1932d743d622cb74fc058882e8648a:
|
|
||||||
keystore://{{keypath "1"}}
|
|
||||||
keystore://{{keypath "2"}}
|
|
||||||
Testing your passphrase against all of them...
|
|
||||||
Your passphrase unlocked keystore://{{keypath "1"}}
|
|
||||||
In order to avoid this warning, you need to remove the following duplicate key files:
|
|
||||||
keystore://{{keypath "2"}}
|
|
||||||
`)
|
|
||||||
geth.ExpectExit()
|
|
||||||
|
|
||||||
wantMessages := []string{
|
|
||||||
"Unlocked account",
|
|
||||||
"=0xf466859eAD1932D743d622CB74FC058882E8648A",
|
|
||||||
}
|
|
||||||
for _, m := range wantMessages {
|
|
||||||
if !strings.Contains(geth.StderrText(), m) {
|
|
||||||
t.Errorf("stderr text does not contain %q", m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnlockFlagAmbiguousWrongPassword(t *testing.T) {
|
|
||||||
store := filepath.Join("..", "..", "accounts", "keystore", "testdata", "dupes")
|
|
||||||
geth := runGeth(t,
|
|
||||||
"--keystore", store, "--nat", "none", "--nodiscover", "--maxpeers", "0", "--port", "0",
|
|
||||||
"--unlock", "f466859ead1932d743d622cb74fc058882e8648a")
|
|
||||||
defer geth.ExpectExit()
|
|
||||||
|
|
||||||
// Helper for the expect template, returns absolute keystore path.
|
|
||||||
geth.SetTemplateFunc("keypath", func(file string) string {
|
|
||||||
abs, _ := filepath.Abs(filepath.Join(store, file))
|
|
||||||
return abs
|
|
||||||
})
|
|
||||||
geth.Expect(`
|
|
||||||
Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3
|
|
||||||
!! Unsupported terminal, password will be echoed.
|
|
||||||
Passphrase: {{.InputLine "wrong"}}
|
|
||||||
Multiple key files exist for address f466859ead1932d743d622cb74fc058882e8648a:
|
|
||||||
keystore://{{keypath "1"}}
|
|
||||||
keystore://{{keypath "2"}}
|
|
||||||
Testing your passphrase against all of them...
|
|
||||||
Fatal: None of the listed files could be unlocked.
|
|
||||||
`)
|
|
||||||
geth.ExpectExit()
|
|
||||||
}
|
|
||||||
|
|
@ -25,21 +25,18 @@ import (
|
||||||
godebug "runtime/debug"
|
godebug "runtime/debug"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/elastic/gosigar"
|
"github.com/elastic/gosigar"
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
|
||||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
|
||||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||||
"github.com/ethereum/go-ethereum/console"
|
"github.com/ethereum/go-ethereum/console"
|
||||||
"github.com/ethereum/go-ethereum/eth"
|
"github.com/ethereum/go-ethereum/eth"
|
||||||
"github.com/ethereum/go-ethereum/ethclient"
|
|
||||||
"github.com/ethereum/go-ethereum/internal/debug"
|
"github.com/ethereum/go-ethereum/internal/debug"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/ethereum/go-ethereum/metrics"
|
"github.com/ethereum/go-ethereum/metrics"
|
||||||
"github.com/ethereum/go-ethereum/node"
|
"github.com/ethereum/go-ethereum/node"
|
||||||
"gopkg.in/urfave/cli.v1"
|
"gopkg.in/urfave/cli.v1"
|
||||||
|
"github.com/ethereum/go-ethereum/internal/ethapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -173,9 +170,6 @@ func init() {
|
||||||
dumpCommand,
|
dumpCommand,
|
||||||
// See monitorcmd.go:
|
// See monitorcmd.go:
|
||||||
monitorCommand,
|
monitorCommand,
|
||||||
// See accountcmd.go:
|
|
||||||
accountCommand,
|
|
||||||
walletCommand,
|
|
||||||
// See consolecmd.go:
|
// See consolecmd.go:
|
||||||
consoleCommand,
|
consoleCommand,
|
||||||
attachCommand,
|
attachCommand,
|
||||||
|
|
@ -261,59 +255,6 @@ func geth(ctx *cli.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func startInternalAccountManagement(ctx *cli.Context, stack *node.Node) {
|
|
||||||
// Unlock any account specifically requested
|
|
||||||
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
|
||||||
|
|
||||||
passwords := utils.MakePasswordList(ctx)
|
|
||||||
unlocks := strings.Split(ctx.GlobalString(utils.UnlockedAccountFlag.Name), ",")
|
|
||||||
for i, account := range unlocks {
|
|
||||||
if trimmed := strings.TrimSpace(account); trimmed != "" {
|
|
||||||
unlockAccount(ctx, ks, trimmed, i, passwords)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Register wallet event handlers to open and auto-derive wallets
|
|
||||||
events := make(chan accounts.WalletEvent, 16)
|
|
||||||
stack.AccountManager().Subscribe(events)
|
|
||||||
go func() {
|
|
||||||
// Create a chain state reader for self-derivation
|
|
||||||
rpcClient, err := stack.Attach()
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatalf("Failed to attach to self: %v", err)
|
|
||||||
}
|
|
||||||
stateReader := ethclient.NewClient(rpcClient)
|
|
||||||
|
|
||||||
// Open any wallets already attached
|
|
||||||
for _, wallet := range stack.AccountManager().Wallets() {
|
|
||||||
if err := wallet.Open(""); err != nil {
|
|
||||||
log.Warn("Failed to open wallet", "url", wallet.URL(), "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Listen for wallet event till termination
|
|
||||||
for event := range events {
|
|
||||||
switch event.Kind {
|
|
||||||
case accounts.WalletArrived:
|
|
||||||
if err := event.Wallet.Open(""); err != nil {
|
|
||||||
log.Warn("New wallet appeared, failed to open", "url", event.Wallet.URL(), "err", err)
|
|
||||||
}
|
|
||||||
case accounts.WalletOpened:
|
|
||||||
status, _ := event.Wallet.Status()
|
|
||||||
log.Info("New wallet appeared", "url", event.Wallet.URL(), "status", status)
|
|
||||||
|
|
||||||
derivationPath := accounts.DefaultBaseDerivationPath
|
|
||||||
if event.Wallet.URL().Scheme == "ledger" {
|
|
||||||
derivationPath = accounts.DefaultLedgerBaseDerivationPath
|
|
||||||
}
|
|
||||||
event.Wallet.SelfDerive(derivationPath, stateReader)
|
|
||||||
|
|
||||||
case accounts.WalletDropped:
|
|
||||||
log.Info("Old wallet dropped", "url", event.Wallet.URL())
|
|
||||||
event.Wallet.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// startNode boots up the system node and all registered protocols, after which
|
// startNode boots up the system node and all registered protocols, after which
|
||||||
// it unlocks any requested accounts, and starts the RPC/IPC interfaces and the
|
// it unlocks any requested accounts, and starts the RPC/IPC interfaces and the
|
||||||
// miner.
|
// miner.
|
||||||
|
|
@ -325,12 +266,14 @@ func startNode(ctx *cli.Context, stack *node.Node) {
|
||||||
|
|
||||||
// Start account management
|
// Start account management
|
||||||
if ctx.IsSet(utils.ExternalSignerFlag.Name){
|
if ctx.IsSet(utils.ExternalSignerFlag.Name){
|
||||||
extApi := ctx.GlobalString(utils.ExternalSignerFlag.Name)
|
extEndpoint := ctx.GlobalString(utils.ExternalSignerFlag.Name)
|
||||||
|
extApi, err := ethapi.NewExternalSigner(extEndpoint)
|
||||||
}else{
|
if err != nil{
|
||||||
startInternalAccountManagement(ctx, stack)
|
utils.Fatalf("Could not connect to external signer at %v: %v" , extEndpoint, err)
|
||||||
|
}
|
||||||
|
log.Info("External signer connected", "url", extEndpoint)
|
||||||
|
stack.SetExternalAPI(extApi)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start auxiliary services if enabled
|
// Start auxiliary services if enabled
|
||||||
if ctx.GlobalBool(utils.MiningEnabledFlag.Name) || ctx.GlobalBool(utils.DeveloperFlag.Name) {
|
if ctx.GlobalBool(utils.MiningEnabledFlag.Name) || ctx.GlobalBool(utils.DeveloperFlag.Name) {
|
||||||
// Mining only makes sense if a full Ethereum node is running
|
// Mining only makes sense if a full Ethereum node is running
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,10 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
"github.com/ethereum/go-ethereum/accounts"
|
||||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/fdlimit"
|
"github.com/ethereum/go-ethereum/common/fdlimit"
|
||||||
"github.com/ethereum/go-ethereum/consensus"
|
"github.com/ethereum/go-ethereum/consensus"
|
||||||
|
|
@ -792,34 +790,23 @@ func makeDatabaseHandles() int {
|
||||||
|
|
||||||
// MakeAddress converts an account specified directly as a hex encoded string or
|
// MakeAddress converts an account specified directly as a hex encoded string or
|
||||||
// a key index in the key store to an internal account representation.
|
// a key index in the key store to an internal account representation.
|
||||||
func MakeAddress(ks *keystore.KeyStore, account string) (accounts.Account, error) {
|
func MakeAddress(account string) (accounts.Account, error) {
|
||||||
// If the specified account is a valid address, return it
|
// If the specified account is a valid address, return it
|
||||||
if common.IsHexAddress(account) {
|
ma, err := common.NewMixedcaseAddressFromString(account);
|
||||||
|
if err != nil {
|
||||||
|
return accounts.Account{}, fmt.Errorf("Not valid hex address (%v): %v", account, err)
|
||||||
|
}
|
||||||
|
if !ma.ValidChecksum(){
|
||||||
|
return accounts.Account{}, fmt.Errorf("Invalid checksum on address %v", account)
|
||||||
|
}
|
||||||
return accounts.Account{Address: common.HexToAddress(account)}, nil
|
return accounts.Account{Address: common.HexToAddress(account)}, nil
|
||||||
}
|
}
|
||||||
// Otherwise try to interpret the account as a keystore index
|
|
||||||
index, err := strconv.Atoi(account)
|
|
||||||
if err != nil || index < 0 {
|
|
||||||
return accounts.Account{}, fmt.Errorf("invalid account address or index %q", account)
|
|
||||||
}
|
|
||||||
log.Warn("-------------------------------------------------------------------")
|
|
||||||
log.Warn("Referring to accounts by order in the keystore folder is dangerous!")
|
|
||||||
log.Warn("This functionality is deprecated and will be removed in the future!")
|
|
||||||
log.Warn("Please use explicit addresses! (can search via `geth account list`)")
|
|
||||||
log.Warn("-------------------------------------------------------------------")
|
|
||||||
|
|
||||||
accs := ks.Accounts()
|
|
||||||
if len(accs) <= index {
|
|
||||||
return accounts.Account{}, fmt.Errorf("index %d higher than number of accounts %d", index, len(accs))
|
|
||||||
}
|
|
||||||
return accs[index], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// setEtherbase retrieves the etherbase either from the directly specified
|
// setEtherbase retrieves the etherbase either from the directly specified
|
||||||
// command line flags or from the keystore if CLI indexed.
|
// command line flags or from the keystore if CLI indexed.
|
||||||
func setEtherbase(ctx *cli.Context, ks *keystore.KeyStore, cfg *eth.Config) {
|
func setEtherbase(ctx *cli.Context, cfg *eth.Config) {
|
||||||
if ctx.GlobalIsSet(EtherbaseFlag.Name) {
|
if ctx.GlobalIsSet(EtherbaseFlag.Name) {
|
||||||
account, err := MakeAddress(ks, ctx.GlobalString(EtherbaseFlag.Name))
|
account, err := MakeAddress(ctx.GlobalString(EtherbaseFlag.Name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Fatalf("Option %q: %v", EtherbaseFlag.Name, err)
|
Fatalf("Option %q: %v", EtherbaseFlag.Name, err)
|
||||||
}
|
}
|
||||||
|
|
@ -1061,8 +1048,7 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *eth.Config) {
|
||||||
checkExclusive(ctx, LightServFlag, LightModeFlag)
|
checkExclusive(ctx, LightServFlag, LightModeFlag)
|
||||||
checkExclusive(ctx, LightServFlag, SyncModeFlag, "light")
|
checkExclusive(ctx, LightServFlag, SyncModeFlag, "light")
|
||||||
|
|
||||||
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
setEtherbase(ctx, cfg)
|
||||||
setEtherbase(ctx, ks, cfg)
|
|
||||||
setGPO(ctx, &cfg.GPO)
|
setGPO(ctx, &cfg.GPO)
|
||||||
setTxPool(ctx, &cfg.TxPool)
|
setTxPool(ctx, &cfg.TxPool)
|
||||||
setEthash(ctx, cfg)
|
setEthash(ctx, cfg)
|
||||||
|
|
@ -1135,24 +1121,8 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *eth.Config) {
|
||||||
cfg.NetworkId = 1337
|
cfg.NetworkId = 1337
|
||||||
}
|
}
|
||||||
// Create new developer account or reuse existing one
|
// Create new developer account or reuse existing one
|
||||||
var (
|
// TODO @holiman
|
||||||
developer accounts.Account
|
Fatalf("Failed to create developer account: not implemented", )
|
||||||
err error
|
|
||||||
)
|
|
||||||
if accs := ks.Accounts(); len(accs) > 0 {
|
|
||||||
developer = ks.Accounts()[0]
|
|
||||||
} else {
|
|
||||||
developer, err = ks.NewAccount("")
|
|
||||||
if err != nil {
|
|
||||||
Fatalf("Failed to create developer account: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := ks.Unlock(developer, ""); err != nil {
|
|
||||||
Fatalf("Failed to unlock developer account: %v", err)
|
|
||||||
}
|
|
||||||
log.Info("Using developer account", "address", developer.Address)
|
|
||||||
|
|
||||||
cfg.Genesis = core.DeveloperGenesisBlock(uint64(ctx.GlobalInt(DeveloperPeriodFlag.Name)), developer.Address)
|
|
||||||
if !ctx.GlobalIsSet(GasPriceFlag.Name) {
|
if !ctx.GlobalIsSet(GasPriceFlag.Name) {
|
||||||
cfg.GasPrice = big.NewInt(1)
|
cfg.GasPrice = big.NewInt(1)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
"github.com/ethereum/go-ethereum/common/math"
|
||||||
"github.com/ethereum/go-ethereum/core"
|
"github.com/ethereum/go-ethereum/core"
|
||||||
|
|
@ -217,10 +216,6 @@ func (b *EthAPIBackend) EventMux() *event.TypeMux {
|
||||||
return b.eth.EventMux()
|
return b.eth.EventMux()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *EthAPIBackend) AccountManager() *accounts.Manager {
|
|
||||||
return b.eth.AccountManager()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *EthAPIBackend) BloomStatus() (uint64, uint64) {
|
func (b *EthAPIBackend) BloomStatus() (uint64, uint64) {
|
||||||
sections, _, _ := b.eth.bloomIndexer.Sections()
|
sections, _, _ := b.eth.bloomIndexer.Sections()
|
||||||
return params.BloomBitsBlocks, sections
|
return params.BloomBitsBlocks, sections
|
||||||
|
|
@ -231,3 +226,8 @@ func (b *EthAPIBackend) ServiceFilter(ctx context.Context, session *bloombits.Ma
|
||||||
go session.Multiplex(bloomRetrievalBatch, bloomRetrievalWait, b.eth.bloomRequests)
|
go session.Multiplex(bloomRetrievalBatch, bloomRetrievalWait, b.eth.bloomRequests)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func (b *EthAPIBackend) ExternalSigner() string {
|
||||||
|
return b.eth.externalSigner
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
"github.com/ethereum/go-ethereum/consensus"
|
"github.com/ethereum/go-ethereum/consensus"
|
||||||
|
|
@ -77,7 +76,6 @@ type Ethereum struct {
|
||||||
|
|
||||||
eventMux *event.TypeMux
|
eventMux *event.TypeMux
|
||||||
engine consensus.Engine
|
engine consensus.Engine
|
||||||
accountManager *accounts.Manager
|
|
||||||
externalSigner string
|
externalSigner string
|
||||||
|
|
||||||
bloomRequests chan chan *bloombits.Retrieval // Channel receiving bloom data retrieval requests
|
bloomRequests chan chan *bloombits.Retrieval // Channel receiving bloom data retrieval requests
|
||||||
|
|
@ -124,7 +122,6 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) {
|
||||||
chainDb: chainDb,
|
chainDb: chainDb,
|
||||||
chainConfig: chainConfig,
|
chainConfig: chainConfig,
|
||||||
eventMux: ctx.EventMux,
|
eventMux: ctx.EventMux,
|
||||||
accountManager: ctx.AccountManager,
|
|
||||||
engine: CreateConsensusEngine(ctx, chainConfig, &config.Ethash, config.MinerNotify, chainDb),
|
engine: CreateConsensusEngine(ctx, chainConfig, &config.Ethash, config.MinerNotify, chainDb),
|
||||||
shutdownChan: make(chan bool),
|
shutdownChan: make(chan bool),
|
||||||
networkID: config.NetworkId,
|
networkID: config.NetworkId,
|
||||||
|
|
@ -310,18 +307,6 @@ func (s *Ethereum) Etherbase() (eb common.Address, err error) {
|
||||||
if etherbase != (common.Address{}) {
|
if etherbase != (common.Address{}) {
|
||||||
return etherbase, nil
|
return etherbase, nil
|
||||||
}
|
}
|
||||||
if wallets := s.AccountManager().Wallets(); len(wallets) > 0 {
|
|
||||||
if accounts := wallets[0].Accounts(); len(accounts) > 0 {
|
|
||||||
etherbase := accounts[0].Address
|
|
||||||
|
|
||||||
s.lock.Lock()
|
|
||||||
s.etherbase = etherbase
|
|
||||||
s.lock.Unlock()
|
|
||||||
|
|
||||||
log.Info("Etherbase automatically configured", "address", etherbase)
|
|
||||||
return etherbase, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return common.Address{}, fmt.Errorf("etherbase must be explicitly specified")
|
return common.Address{}, fmt.Errorf("etherbase must be explicitly specified")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,13 +325,13 @@ func (s *Ethereum) StartMining(local bool) error {
|
||||||
log.Error("Cannot start mining without etherbase", "err", err)
|
log.Error("Cannot start mining without etherbase", "err", err)
|
||||||
return fmt.Errorf("etherbase missing: %v", err)
|
return fmt.Errorf("etherbase missing: %v", err)
|
||||||
}
|
}
|
||||||
if clique, ok := s.engine.(*clique.Clique); ok {
|
if _, ok := s.engine.(*clique.Clique); ok {
|
||||||
wallet, err := s.accountManager.Find(accounts.Account{Address: eb})
|
|
||||||
if wallet == nil || err != nil {
|
//TODO @holiman
|
||||||
log.Error("Etherbase account unavailable locally", "err", err)
|
log.Error("Etherbase account unavailable locally", "err", err)
|
||||||
return fmt.Errorf("signer missing: %v", err)
|
return fmt.Errorf("signer not configured: %v", err)
|
||||||
}
|
|
||||||
clique.Authorize(eb, wallet.SignHash)
|
|
||||||
}
|
}
|
||||||
if local {
|
if local {
|
||||||
// If local (CPU) mining is started, we can disable the transaction rejection
|
// If local (CPU) mining is started, we can disable the transaction rejection
|
||||||
|
|
@ -363,7 +348,6 @@ func (s *Ethereum) StopMining() { s.miner.Stop() }
|
||||||
func (s *Ethereum) IsMining() bool { return s.miner.Mining() }
|
func (s *Ethereum) IsMining() bool { return s.miner.Mining() }
|
||||||
func (s *Ethereum) Miner() *miner.Miner { return s.miner }
|
func (s *Ethereum) Miner() *miner.Miner { return s.miner }
|
||||||
|
|
||||||
func (s *Ethereum) AccountManager() *accounts.Manager { return s.accountManager }
|
|
||||||
func (s *Ethereum) BlockChain() *core.BlockChain { return s.blockchain }
|
func (s *Ethereum) BlockChain() *core.BlockChain { return s.blockchain }
|
||||||
func (s *Ethereum) TxPool() *core.TxPool { return s.txPool }
|
func (s *Ethereum) TxPool() *core.TxPool { return s.txPool }
|
||||||
func (s *Ethereum) EventMux() *event.TypeMux { return s.eventMux }
|
func (s *Ethereum) EventMux() *event.TypeMux { return s.eventMux }
|
||||||
|
|
|
||||||
|
|
@ -177,33 +177,10 @@ func (s *PublicTxPoolAPI) Inspect() map[string]map[string]map[string]string {
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublicAccountAPI provides an API to access accounts managed by this node.
|
|
||||||
// It offers only methods that can retrieve accounts.
|
|
||||||
type PublicAccountAPI struct {
|
|
||||||
am *accounts.Manager
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPublicAccountAPI creates a new PublicAccountAPI.
|
|
||||||
func NewPublicAccountAPI(am *accounts.Manager) *PublicAccountAPI {
|
|
||||||
return &PublicAccountAPI{am: am}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accounts returns the collection of accounts this node manages
|
|
||||||
func (s *PublicAccountAPI) Accounts() []common.Address {
|
|
||||||
addresses := make([]common.Address, 0) // return [] instead of nil if empty
|
|
||||||
for _, wallet := range s.am.Wallets() {
|
|
||||||
for _, account := range wallet.Accounts() {
|
|
||||||
addresses = append(addresses, account.Address)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return addresses
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrivateAccountAPI provides an API to access accounts managed by this node.
|
// PrivateAccountAPI provides an API to access accounts managed by this node.
|
||||||
// It offers methods to create, (un)lock en list accounts. Some methods accept
|
// It offers methods to create, (un)lock en list accounts. Some methods accept
|
||||||
// passwords and are therefore considered private by default.
|
// passwords and are therefore considered private by default.
|
||||||
type PrivateAccountAPI struct {
|
type PrivateAccountAPI struct {
|
||||||
am *accounts.Manager
|
|
||||||
nonceLock *AddrLocker
|
nonceLock *AddrLocker
|
||||||
b Backend
|
b Backend
|
||||||
extapi *ExternalSignerAPI // external signer, nil if not used
|
extapi *ExternalSignerAPI // external signer, nil if not used
|
||||||
|
|
@ -212,7 +189,6 @@ type PrivateAccountAPI struct {
|
||||||
// NewPrivateAccountAPI create a new PrivateAccountAPI.
|
// NewPrivateAccountAPI create a new PrivateAccountAPI.
|
||||||
func NewPrivateAccountAPI(b Backend, nonceLock *AddrLocker) *PrivateAccountAPI {
|
func NewPrivateAccountAPI(b Backend, nonceLock *AddrLocker) *PrivateAccountAPI {
|
||||||
p := &PrivateAccountAPI{
|
p := &PrivateAccountAPI{
|
||||||
am: b.AccountManager(),
|
|
||||||
nonceLock: nonceLock,
|
nonceLock: nonceLock,
|
||||||
b: b,
|
b: b,
|
||||||
}
|
}
|
||||||
|
|
@ -236,11 +212,6 @@ func (s *PrivateAccountAPI) ListAccounts() []common.Address {
|
||||||
}
|
}
|
||||||
return addresses
|
return addresses
|
||||||
}
|
}
|
||||||
for _, wallet := range s.am.Wallets() {
|
|
||||||
for _, account := range wallet.Accounts() {
|
|
||||||
addresses = append(addresses, account.Address)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return addresses
|
return addresses
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,79 +224,10 @@ type rawWallet struct {
|
||||||
Accounts []accounts.Account `json:"accounts,omitempty"`
|
Accounts []accounts.Account `json:"accounts,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListWallets will return a list of wallets this node manages.
|
|
||||||
func (s *PrivateAccountAPI) ListWallets() []rawWallet {
|
|
||||||
wallets := make([]rawWallet, 0) // return [] instead of nil if empty
|
|
||||||
if s.extapi != nil {
|
|
||||||
return wallets
|
|
||||||
}
|
|
||||||
for _, wallet := range s.am.Wallets() {
|
|
||||||
status, failure := wallet.Status()
|
|
||||||
|
|
||||||
raw := rawWallet{
|
|
||||||
URL: wallet.URL().String(),
|
|
||||||
Status: status,
|
|
||||||
Accounts: wallet.Accounts(),
|
|
||||||
}
|
|
||||||
if failure != nil {
|
|
||||||
raw.Failure = failure.Error()
|
|
||||||
}
|
|
||||||
wallets = append(wallets, raw)
|
|
||||||
}
|
|
||||||
return wallets
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenWallet initiates a hardware wallet opening procedure, establishing a USB
|
|
||||||
// connection and attempting to authenticate via the provided passphrase. Note,
|
|
||||||
// the method may return an extra challenge requiring a second open (e.g. the
|
|
||||||
// Trezor PIN matrix challenge).
|
|
||||||
func (s *PrivateAccountAPI) OpenWallet(url string, passphrase *string) error {
|
|
||||||
if s.extapi != nil {
|
|
||||||
return errors.New("openWallet disabled when external signer is used")
|
|
||||||
}
|
|
||||||
|
|
||||||
wallet, err := s.am.Wallet(url)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
pass := ""
|
|
||||||
if passphrase != nil {
|
|
||||||
pass = *passphrase
|
|
||||||
}
|
|
||||||
return wallet.Open(pass)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeriveAccount requests a HD wallet to derive a new account, optionally pinning
|
|
||||||
// it for later reuse.
|
|
||||||
func (s *PrivateAccountAPI) DeriveAccount(url string, path string, pin *bool) (accounts.Account, error) {
|
|
||||||
if s.extapi != nil {
|
|
||||||
return accounts.Account{}, errors.New("deriveAccount disabled when external signer is used")
|
|
||||||
}
|
|
||||||
wallet, err := s.am.Wallet(url)
|
|
||||||
if err != nil {
|
|
||||||
return accounts.Account{}, err
|
|
||||||
}
|
|
||||||
derivPath, err := accounts.ParseDerivationPath(path)
|
|
||||||
if err != nil {
|
|
||||||
return accounts.Account{}, err
|
|
||||||
}
|
|
||||||
if pin == nil {
|
|
||||||
pin = new(bool)
|
|
||||||
}
|
|
||||||
return wallet.Derive(derivPath, *pin)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAccount will create a new account and returns the address for the new account.
|
// NewAccount will create a new account and returns the address for the new account.
|
||||||
func (s *PrivateAccountAPI) NewAccount(password string) (common.Address, error) {
|
func (s *PrivateAccountAPI) NewAccount(password string) (common.Address, error) {
|
||||||
if s.extapi != nil {
|
|
||||||
return s.extapi.newAccount()
|
return s.extapi.newAccount()
|
||||||
}
|
}
|
||||||
acc, err := fetchKeystore(s.am).NewAccount(password)
|
|
||||||
if err == nil {
|
|
||||||
return acc.Address, nil
|
|
||||||
}
|
|
||||||
return common.Address{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetchKeystore retrives the encrypted keystore from the account manager.
|
// fetchKeystore retrives the encrypted keystore from the account manager.
|
||||||
func fetchKeystore(am *accounts.Manager) *keystore.KeyStore {
|
func fetchKeystore(am *accounts.Manager) *keystore.KeyStore {
|
||||||
|
|
@ -335,45 +237,20 @@ func fetchKeystore(am *accounts.Manager) *keystore.KeyStore {
|
||||||
// ImportRawKey stores the given hex encoded ECDSA key into the key directory,
|
// ImportRawKey stores the given hex encoded ECDSA key into the key directory,
|
||||||
// encrypting it with the passphrase.
|
// encrypting it with the passphrase.
|
||||||
func (s *PrivateAccountAPI) ImportRawKey(privkey string, password string) (common.Address, error) {
|
func (s *PrivateAccountAPI) ImportRawKey(privkey string, password string) (common.Address, error) {
|
||||||
if s.extapi != nil {
|
|
||||||
return common.Address{}, errors.New("Importing of raw keys is disabled when external signer is used.")
|
return common.Address{}, errors.New("Importing of raw keys is disabled when external signer is used.")
|
||||||
}
|
}
|
||||||
|
|
||||||
key, err := crypto.HexToECDSA(privkey)
|
|
||||||
if err != nil {
|
|
||||||
return common.Address{}, err
|
|
||||||
}
|
|
||||||
acc, err := fetchKeystore(s.am).ImportECDSA(key, password)
|
|
||||||
return acc.Address, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnlockAccount will unlock the account associated with the given address with
|
// UnlockAccount will unlock the account associated with the given address with
|
||||||
// the given password for duration seconds. If duration is nil it will use a
|
// the given password for duration seconds. If duration is nil it will use a
|
||||||
// default of 300 seconds. It returns an indication if the account was unlocked.
|
// default of 300 seconds. It returns an indication if the account was unlocked.
|
||||||
func (s *PrivateAccountAPI) UnlockAccount(addr common.Address, password string, duration *uint64) (bool, error) {
|
func (s *PrivateAccountAPI) UnlockAccount(addr common.Address, password string, duration *uint64) (bool, error) {
|
||||||
if s.extapi != nil {
|
|
||||||
return false, errors.New("Unlock is disabled when external signer is used.")
|
return false, errors.New("Unlock is disabled when external signer is used.")
|
||||||
}
|
}
|
||||||
const max = uint64(time.Duration(math.MaxInt64) / time.Second)
|
|
||||||
var d time.Duration
|
|
||||||
if duration == nil {
|
|
||||||
d = 300 * time.Second
|
|
||||||
} else if *duration > max {
|
|
||||||
return false, errors.New("unlock duration too large")
|
|
||||||
} else {
|
|
||||||
d = time.Duration(*duration) * time.Second
|
|
||||||
}
|
|
||||||
err := fetchKeystore(s.am).TimedUnlock(accounts.Account{Address: addr}, password, d)
|
|
||||||
return err == nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// LockAccount will lock the account associated with the given address when it's unlocked.
|
// LockAccount will lock the account associated with the given address when it's unlocked.
|
||||||
func (s *PrivateAccountAPI) LockAccount(addr common.Address) bool {
|
func (s *PrivateAccountAPI) LockAccount(addr common.Address) bool {
|
||||||
if s.extapi != nil {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return fetchKeystore(s.am).Lock(addr) == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// signTransactions sets defaults and signs the given transaction
|
// signTransactions sets defaults and signs the given transaction
|
||||||
// NOTE: the caller needs to ensure that the nonceLock is held, if applicable,
|
// NOTE: the caller needs to ensure that the nonceLock is held, if applicable,
|
||||||
|
|
@ -383,26 +260,9 @@ func (s *PrivateAccountAPI) signTransaction(ctx context.Context, args SendTxArgs
|
||||||
if err := args.setDefaults(ctx, s.b); err != nil {
|
if err := args.setDefaults(ctx, s.b); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if s.extapi != nil {
|
|
||||||
return s.extapi.signTransaction(ctx, args)
|
return s.extapi.signTransaction(ctx, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look up the wallet containing the requested signer
|
|
||||||
account := accounts.Account{Address: args.From}
|
|
||||||
wallet, err := s.am.Find(account)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Assemble the transaction and sign with the wallet
|
|
||||||
tx := args.toTransaction()
|
|
||||||
|
|
||||||
var chainID *big.Int
|
|
||||||
if config := s.b.ChainConfig(); config.IsEIP155(s.b.CurrentBlock().Number()) {
|
|
||||||
chainID = config.ChainID
|
|
||||||
}
|
|
||||||
return wallet.SignTxWithPassphrase(account, passwd, tx, chainID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendTransaction will create a transaction from the given arguments and
|
// SendTransaction will create a transaction from the given arguments and
|
||||||
// tries to sign it with the key associated with args.To. If the given passwd isn't
|
// tries to sign it with the key associated with args.To. If the given passwd isn't
|
||||||
// able to decrypt the key it fails.
|
// able to decrypt the key it fails.
|
||||||
|
|
@ -459,32 +319,6 @@ func signHash(data []byte) []byte {
|
||||||
return crypto.Keccak256([]byte(msg))
|
return crypto.Keccak256([]byte(msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sign calculates an Ethereum ECDSA signature for:
|
|
||||||
// keccack256("\x19Ethereum Signed Message:\n" + len(message) + message))
|
|
||||||
//
|
|
||||||
// Note, the produced signature conforms to the secp256k1 curve R, S and V values,
|
|
||||||
// where the V value will be 27 or 28 for legacy reasons.
|
|
||||||
//
|
|
||||||
// The key used to calculate the signature is decrypted with the given password.
|
|
||||||
//
|
|
||||||
// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign
|
|
||||||
func (s *PrivateAccountAPI) Sign(ctx context.Context, data hexutil.Bytes, addr common.Address, passwd string) (hexutil.Bytes, error) {
|
|
||||||
// Look up the wallet containing the requested signer
|
|
||||||
account := accounts.Account{Address: addr}
|
|
||||||
|
|
||||||
wallet, err := s.b.AccountManager().Find(account)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Assemble sign the data with the wallet
|
|
||||||
signature, err := wallet.SignHashWithPassphrase(account, passwd, signHash(data))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
|
|
||||||
return signature, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// EcRecover returns the address for the account that was used to create the signature.
|
// EcRecover returns the address for the account that was used to create the signature.
|
||||||
// Note, this function is compatible with eth_sign and personal_sign. As such it recovers
|
// Note, this function is compatible with eth_sign and personal_sign. As such it recovers
|
||||||
// the address of:
|
// the address of:
|
||||||
|
|
@ -514,7 +348,7 @@ func (s *PrivateAccountAPI) EcRecover(ctx context.Context, data, sig hexutil.Byt
|
||||||
// SignAndSendTransaction was renamed to SendTransaction. This method is deprecated
|
// SignAndSendTransaction was renamed to SendTransaction. This method is deprecated
|
||||||
// and will be removed in the future. It primary goal is to give clients time to update.
|
// and will be removed in the future. It primary goal is to give clients time to update.
|
||||||
func (s *PrivateAccountAPI) SignAndSendTransaction(ctx context.Context, args SendTxArgs, passwd string) (common.Hash, error) {
|
func (s *PrivateAccountAPI) SignAndSendTransaction(ctx context.Context, args SendTxArgs, passwd string) (common.Hash, error) {
|
||||||
return s.SendTransaction(ctx, args, passwd)
|
return common.Hash{}, errors.New("SignAndSend is deprecated, please use sendTransaction instead")
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublicBlockChainAPI provides an API to access the Ethereum blockchain.
|
// PublicBlockChainAPI provides an API to access the Ethereum blockchain.
|
||||||
|
|
@ -661,15 +495,8 @@ func (s *PublicBlockChainAPI) doCall(ctx context.Context, args CallArgs, blockNr
|
||||||
if state == nil || err != nil {
|
if state == nil || err != nil {
|
||||||
return nil, 0, false, err
|
return nil, 0, false, err
|
||||||
}
|
}
|
||||||
// Set sender address or use a default if none specified
|
// Set sender address
|
||||||
addr := args.From
|
addr := args.From
|
||||||
if addr == (common.Address{}) {
|
|
||||||
if wallets := s.b.AccountManager().Wallets(); len(wallets) > 0 {
|
|
||||||
if accts := wallets[0].Accounts(); len(accts) > 0 {
|
|
||||||
addr = accts[0].Address
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Set default gas & gas price if none were set
|
// Set default gas & gas price if none were set
|
||||||
gas, gasPrice := uint64(args.Gas), args.GasPrice.ToInt()
|
gas, gasPrice := uint64(args.Gas), args.GasPrice.ToInt()
|
||||||
if gas == 0 {
|
if gas == 0 {
|
||||||
|
|
@ -1145,23 +972,6 @@ func (s *PublicTransactionPoolAPI) GetTransactionReceipt(ctx context.Context, ha
|
||||||
return fields, nil
|
return fields, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// sign is a helper function that signs a transaction with the private key of the given address.
|
|
||||||
func (s *PublicTransactionPoolAPI) sign(addr common.Address, tx *types.Transaction) (*types.Transaction, error) {
|
|
||||||
// Look up the wallet containing the requested signer
|
|
||||||
account := accounts.Account{Address: addr}
|
|
||||||
|
|
||||||
wallet, err := s.b.AccountManager().Find(account)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Request the wallet to sign the transaction
|
|
||||||
var chainID *big.Int
|
|
||||||
if config := s.b.ChainConfig(); config.IsEIP155(s.b.CurrentBlock().Number()) {
|
|
||||||
chainID = config.ChainID
|
|
||||||
}
|
|
||||||
return wallet.SignTx(account, tx, chainID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendTxArgs represents the arguments to sumbit a new transaction into the transaction pool.
|
// SendTxArgs represents the arguments to sumbit a new transaction into the transaction pool.
|
||||||
type SendTxArgs struct {
|
type SendTxArgs struct {
|
||||||
From common.Address `json:"from"`
|
From common.Address `json:"from"`
|
||||||
|
|
@ -1292,19 +1102,7 @@ func (s *PublicTransactionPoolAPI) SendRawTransaction(ctx context.Context, encod
|
||||||
//
|
//
|
||||||
// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign
|
// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign
|
||||||
func (s *PublicTransactionPoolAPI) Sign(addr common.Address, data hexutil.Bytes) (hexutil.Bytes, error) {
|
func (s *PublicTransactionPoolAPI) Sign(addr common.Address, data hexutil.Bytes) (hexutil.Bytes, error) {
|
||||||
// Look up the wallet containing the requested signer
|
return nil, errors.New("Sign data not implemented")
|
||||||
account := accounts.Account{Address: addr}
|
|
||||||
|
|
||||||
wallet, err := s.b.AccountManager().Find(account)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Sign the requested hash with the wallet
|
|
||||||
signature, err := wallet.SignHash(account, signHash(data))
|
|
||||||
if err == nil {
|
|
||||||
signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
|
|
||||||
}
|
|
||||||
return signature, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignTransactionResult represents a RLP encoded signed transaction.
|
// SignTransactionResult represents a RLP encoded signed transaction.
|
||||||
|
|
@ -1333,11 +1131,9 @@ func (s *PublicTransactionPoolAPI) SignTransaction(ctx context.Context, args Sen
|
||||||
tx *types.Transaction
|
tx *types.Transaction
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
if s.extapi != nil {
|
|
||||||
tx, err = s.extapi.signTransaction(ctx, args)
|
tx, err = s.extapi.signTransaction(ctx, args)
|
||||||
} else {
|
|
||||||
tx, err = s.sign(args.From, args.toTransaction())
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -1351,28 +1147,9 @@ func (s *PublicTransactionPoolAPI) SignTransaction(ctx context.Context, args Sen
|
||||||
// PendingTransactions returns the transactions that are in the transaction pool
|
// PendingTransactions returns the transactions that are in the transaction pool
|
||||||
// and have a from address that is one of the accounts this node manages.
|
// and have a from address that is one of the accounts this node manages.
|
||||||
func (s *PublicTransactionPoolAPI) PendingTransactions() ([]*RPCTransaction, error) {
|
func (s *PublicTransactionPoolAPI) PendingTransactions() ([]*RPCTransaction, error) {
|
||||||
pending, err := s.b.GetPoolTransactions()
|
return nil, errors.New("Not implemented")
|
||||||
if err != nil {
|
// TODO @holiman, make this call take a list of addresses, and show the penading transactions that are from that
|
||||||
return nil, err
|
// set of addresses
|
||||||
}
|
|
||||||
accounts := make(map[common.Address]struct{})
|
|
||||||
for _, wallet := range s.b.AccountManager().Wallets() {
|
|
||||||
for _, account := range wallet.Accounts() {
|
|
||||||
accounts[account.Address] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
transactions := make([]*RPCTransaction, 0, len(pending))
|
|
||||||
for _, tx := range pending {
|
|
||||||
var signer types.Signer = types.HomesteadSigner{}
|
|
||||||
if tx.Protected() {
|
|
||||||
signer = types.NewEIP155Signer(tx.ChainId())
|
|
||||||
}
|
|
||||||
from, _ := types.Sender(signer, tx)
|
|
||||||
if _, exists := accounts[from]; exists {
|
|
||||||
transactions = append(transactions, newRPCPendingTransaction(tx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return transactions, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resend accepts an existing transaction and a new gas price and limit. It will remove
|
// Resend accepts an existing transaction and a new gas price and limit. It will remove
|
||||||
|
|
@ -1407,11 +1184,7 @@ func (s *PublicTransactionPoolAPI) Resend(ctx context.Context, sendArgs SendTxAr
|
||||||
if gasLimit != nil && *gasLimit != 0 {
|
if gasLimit != nil && *gasLimit != 0 {
|
||||||
sendArgs.Gas = gasLimit
|
sendArgs.Gas = gasLimit
|
||||||
}
|
}
|
||||||
if s.extapi != nil {
|
|
||||||
signedTx, err = s.extapi.signTransaction(ctx, sendArgs)
|
signedTx, err = s.extapi.signTransaction(ctx, sendArgs)
|
||||||
} else {
|
|
||||||
signedTx, err = s.sign(sendArgs.From, sendArgs.toTransaction())
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return common.Hash{}, err
|
return common.Hash{}, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/core"
|
"github.com/ethereum/go-ethereum/core"
|
||||||
"github.com/ethereum/go-ethereum/core/state"
|
"github.com/ethereum/go-ethereum/core/state"
|
||||||
|
|
@ -43,7 +42,6 @@ type Backend interface {
|
||||||
SuggestPrice(ctx context.Context) (*big.Int, error)
|
SuggestPrice(ctx context.Context) (*big.Int, error)
|
||||||
ChainDb() ethdb.Database
|
ChainDb() ethdb.Database
|
||||||
EventMux() *event.TypeMux
|
EventMux() *event.TypeMux
|
||||||
AccountManager() *accounts.Manager
|
|
||||||
ExternalSigner() string
|
ExternalSigner() string
|
||||||
|
|
||||||
// BlockChain API
|
// BlockChain API
|
||||||
|
|
@ -104,11 +102,6 @@ func GetAPIs(apiBackend Backend) []rpc.API {
|
||||||
Namespace: "debug",
|
Namespace: "debug",
|
||||||
Version: "1.0",
|
Version: "1.0",
|
||||||
Service: NewPrivateDebugAPI(apiBackend),
|
Service: NewPrivateDebugAPI(apiBackend),
|
||||||
}, {
|
|
||||||
Namespace: "eth",
|
|
||||||
Version: "1.0",
|
|
||||||
Service: NewPublicAccountAPI(apiBackend.AccountManager()),
|
|
||||||
Public: true,
|
|
||||||
},{
|
},{
|
||||||
Namespace: "personal",
|
Namespace: "personal",
|
||||||
Version: "1.0",
|
Version: "1.0",
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
"github.com/ethereum/go-ethereum/common/math"
|
||||||
"github.com/ethereum/go-ethereum/core"
|
"github.com/ethereum/go-ethereum/core"
|
||||||
|
|
@ -183,10 +182,6 @@ func (b *LesApiBackend) EventMux() *event.TypeMux {
|
||||||
return b.eth.eventMux
|
return b.eth.eventMux
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *LesApiBackend) AccountManager() *accounts.Manager {
|
|
||||||
return b.eth.accountManager
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *LesApiBackend) ExternalSigner() string {
|
func (b *LesApiBackend) ExternalSigner() string {
|
||||||
return b.eth.externalSigner
|
return b.eth.externalSigner
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
"github.com/ethereum/go-ethereum/consensus"
|
"github.com/ethereum/go-ethereum/consensus"
|
||||||
|
|
@ -72,7 +71,6 @@ type LightEthereum struct {
|
||||||
|
|
||||||
eventMux *event.TypeMux
|
eventMux *event.TypeMux
|
||||||
engine consensus.Engine
|
engine consensus.Engine
|
||||||
accountManager *accounts.Manager
|
|
||||||
externalSigner string
|
externalSigner string
|
||||||
|
|
||||||
networkId uint64
|
networkId uint64
|
||||||
|
|
@ -102,7 +100,6 @@ func New(ctx *node.ServiceContext, config *eth.Config) (*LightEthereum, error) {
|
||||||
eventMux: ctx.EventMux,
|
eventMux: ctx.EventMux,
|
||||||
peers: peers,
|
peers: peers,
|
||||||
reqDist: newRequestDistributor(peers, quitSync),
|
reqDist: newRequestDistributor(peers, quitSync),
|
||||||
accountManager: ctx.AccountManager,
|
|
||||||
engine: eth.CreateConsensusEngine(ctx, chainConfig, &config.Ethash, nil, chainDb),
|
engine: eth.CreateConsensusEngine(ctx, chainConfig, &config.Ethash, nil, chainDb),
|
||||||
shutdownChan: make(chan bool),
|
shutdownChan: make(chan bool),
|
||||||
networkId: config.NetworkId,
|
networkId: config.NetworkId,
|
||||||
|
|
|
||||||
40
node/node.go
40
node/node.go
|
|
@ -26,7 +26,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
"github.com/ethereum/go-ethereum/event"
|
"github.com/ethereum/go-ethereum/event"
|
||||||
"github.com/ethereum/go-ethereum/internal/debug"
|
"github.com/ethereum/go-ethereum/internal/debug"
|
||||||
|
|
@ -34,16 +33,15 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/p2p"
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
"github.com/prometheus/prometheus/util/flock"
|
"github.com/prometheus/prometheus/util/flock"
|
||||||
|
"github.com/ethereum/go-ethereum/internal/ethapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Node is a container on which services can be registered.
|
// Node is a container on which services can be registered.
|
||||||
type Node struct {
|
type Node struct {
|
||||||
eventmux *event.TypeMux // Event multiplexer used between the services of a stack
|
eventmux *event.TypeMux // Event multiplexer used between the services of a stack
|
||||||
config *Config
|
config *Config
|
||||||
accman *accounts.Manager
|
extapi *ethapi.ExternalSignerAPI// If non-empty, the ipc-path or http(s)-url to an external signer
|
||||||
extapi string // If non-empty, the ipc-path or http(s)-url to an external signer
|
|
||||||
|
|
||||||
ephemeralKeystore string // if non-empty, the key directory that will be removed by Stop
|
|
||||||
instanceDirLock flock.Releaser // prevents concurrent use of instance directory
|
instanceDirLock flock.Releaser // prevents concurrent use of instance directory
|
||||||
|
|
||||||
serverConfig p2p.Config
|
serverConfig p2p.Config
|
||||||
|
|
@ -98,27 +96,12 @@ func New(conf *Config) (*Node, error) {
|
||||||
if strings.HasSuffix(conf.Name, ".ipc") {
|
if strings.HasSuffix(conf.Name, ".ipc") {
|
||||||
return nil, errors.New(`Config.Name cannot end in ".ipc"`)
|
return nil, errors.New(`Config.Name cannot end in ".ipc"`)
|
||||||
}
|
}
|
||||||
// Ensure that the AccountManager method works before the node has started.
|
|
||||||
// We rely on this in cmd/geth.
|
|
||||||
var (
|
|
||||||
am *accounts.Manager
|
|
||||||
ephemeralKeystore string
|
|
||||||
)
|
|
||||||
if conf.ExternalSigner != "" {
|
|
||||||
var err error
|
|
||||||
if am, ephemeralKeystore, err = makeAccountManager(conf); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if conf.Logger == nil {
|
if conf.Logger == nil {
|
||||||
conf.Logger = log.New()
|
conf.Logger = log.New()
|
||||||
}
|
}
|
||||||
// Note: any interaction with Config that would create/touch files
|
// Note: any interaction with Config that would create/touch files
|
||||||
// in the data directory or instance directory is delayed until Start.
|
// in the data directory or instance directory is delayed until Start.
|
||||||
return &Node{
|
return &Node{
|
||||||
accman: am,
|
|
||||||
extapi: conf.ExternalSigner,
|
|
||||||
ephemeralKeystore: ephemeralKeystore,
|
|
||||||
config: conf,
|
config: conf,
|
||||||
serviceFuncs: []ServiceConstructor{},
|
serviceFuncs: []ServiceConstructor{},
|
||||||
ipcEndpoint: conf.IPCEndpoint(),
|
ipcEndpoint: conf.IPCEndpoint(),
|
||||||
|
|
@ -181,7 +164,6 @@ func (n *Node) Start() error {
|
||||||
config: n.config,
|
config: n.config,
|
||||||
services: make(map[reflect.Type]Service),
|
services: make(map[reflect.Type]Service),
|
||||||
EventMux: n.eventmux,
|
EventMux: n.eventmux,
|
||||||
AccountManager: n.accman,
|
|
||||||
}
|
}
|
||||||
for kind, s := range services { // copy needed for threaded access
|
for kind, s := range services { // copy needed for threaded access
|
||||||
ctx.services[kind] = s
|
ctx.services[kind] = s
|
||||||
|
|
@ -443,18 +425,9 @@ func (n *Node) Stop() error {
|
||||||
// unblock n.Wait
|
// unblock n.Wait
|
||||||
close(n.stop)
|
close(n.stop)
|
||||||
|
|
||||||
// Remove the keystore if it was created ephemerally.
|
|
||||||
var keystoreErr error
|
|
||||||
if n.ephemeralKeystore != "" {
|
|
||||||
keystoreErr = os.RemoveAll(n.ephemeralKeystore)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(failure.Services) > 0 {
|
if len(failure.Services) > 0 {
|
||||||
return failure
|
return failure
|
||||||
}
|
}
|
||||||
if keystoreErr != nil {
|
|
||||||
return keystoreErr
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -545,11 +518,6 @@ func (n *Node) InstanceDir() string {
|
||||||
return n.config.instanceDir()
|
return n.config.instanceDir()
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountManager retrieves the account manager used by the protocol stack.
|
|
||||||
func (n *Node) AccountManager() *accounts.Manager {
|
|
||||||
return n.accman
|
|
||||||
}
|
|
||||||
|
|
||||||
// IPCEndpoint retrieves the current IPC endpoint used by the protocol stack.
|
// IPCEndpoint retrieves the current IPC endpoint used by the protocol stack.
|
||||||
func (n *Node) IPCEndpoint() string {
|
func (n *Node) IPCEndpoint() string {
|
||||||
return n.ipcEndpoint
|
return n.ipcEndpoint
|
||||||
|
|
@ -586,6 +554,10 @@ func (n *Node) ResolvePath(x string) string {
|
||||||
return n.config.ResolvePath(x)
|
return n.config.ResolvePath(x)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *Node) SetExternalAPI(api *ethapi.ExternalSignerAPI){
|
||||||
|
n.extapi = api
|
||||||
|
}
|
||||||
|
|
||||||
// apis returns the collection of RPC descriptors this node offers.
|
// apis returns the collection of RPC descriptors this node offers.
|
||||||
func (n *Node) apis() []rpc.API {
|
func (n *Node) apis() []rpc.API {
|
||||||
return []rpc.API{
|
return []rpc.API{
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ package node
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
"github.com/ethereum/go-ethereum/event"
|
"github.com/ethereum/go-ethereum/event"
|
||||||
"github.com/ethereum/go-ethereum/p2p"
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
|
|
@ -33,7 +32,6 @@ type ServiceContext struct {
|
||||||
config *Config
|
config *Config
|
||||||
services map[reflect.Type]Service // Index of the already constructed services
|
services map[reflect.Type]Service // Index of the already constructed services
|
||||||
EventMux *event.TypeMux // Event multiplexer used for decoupled notifications
|
EventMux *event.TypeMux // Event multiplexer used for decoupled notifications
|
||||||
AccountManager *accounts.Manager // Account manager created by the node.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenDatabase opens an existing database with the given name (or creates one
|
// OpenDatabase opens an existing database with the given name (or creates one
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,7 @@ func (ui *CommandlineUI) ApproveImport(request *ImportRequest) (ImportResponse,
|
||||||
|
|
||||||
// ApproveListing prompt the user for confirmation to list accounts
|
// ApproveListing prompt the user for confirmation to list accounts
|
||||||
// the list of accounts to list can be modified by the UI
|
// the list of accounts to list can be modified by the UI
|
||||||
func (ui *CommandlineUI) ApproveListing(request *ListRequest) (ListAccountsResponse, error) {
|
func (ui *CommandlineUI) ApproveListing(request *ListAccountsRequest) (ListAccountsResponse, error) {
|
||||||
|
|
||||||
ui.mu.Lock()
|
ui.mu.Lock()
|
||||||
defer ui.mu.Unlock()
|
defer ui.mu.Unlock()
|
||||||
|
|
@ -199,7 +199,7 @@ func (ui *CommandlineUI) ApproveListing(request *ListRequest) (ListAccountsRespo
|
||||||
fmt.Printf("A request has been made to list all accounts. \n")
|
fmt.Printf("A request has been made to list all accounts. \n")
|
||||||
fmt.Printf("You can select which accounts the caller can see\n")
|
fmt.Printf("You can select which accounts the caller can see\n")
|
||||||
for _, account := range request.Accounts {
|
for _, account := range request.Accounts {
|
||||||
fmt.Printf("\t[x] %v\n", account.Address.Hex())
|
fmt.Printf("\t[x] %v\n", account.Hex())
|
||||||
}
|
}
|
||||||
fmt.Printf("-------------------------------------------\n")
|
fmt.Printf("-------------------------------------------\n")
|
||||||
showMetadata(request.Meta)
|
showMetadata(request.Meta)
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ func (ui *StdIOUI) ApproveImport(request *ImportRequest) (ImportResponse, error)
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *StdIOUI) ApproveListing(request *ListRequest) (ListAccountsResponse, error) {
|
func (ui *StdIOUI) ApproveListing(request *ListAccountsRequest) (ListAccountsResponse, error) {
|
||||||
var result ListAccountsResponse
|
var result ListAccountsResponse
|
||||||
err := ui.dispatch("ApproveListing", request, &result)
|
err := ui.dispatch("ApproveListing", request, &result)
|
||||||
return result, err
|
return result, err
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue