github.com/jeffallen/go-ethereum@v1.1.4-0.20150910155051-571d3236c49c/cmd/geth/js.go (about)

     1  // Copyright 2014 The go-ethereum Authors
     2  // This file is part of go-ethereum.
     3  //
     4  // go-ethereum is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // go-ethereum is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU General Public License
    15  // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package main
    18  
    19  import (
    20  	"bufio"
    21  	"fmt"
    22  	"math/big"
    23  	"os"
    24  	"os/signal"
    25  	"path/filepath"
    26  	"regexp"
    27  	"strings"
    28  
    29  	"sort"
    30  
    31  	"github.com/ethereum/go-ethereum/cmd/utils"
    32  	"github.com/ethereum/go-ethereum/common"
    33  	"github.com/ethereum/go-ethereum/common/docserver"
    34  	"github.com/ethereum/go-ethereum/common/natspec"
    35  	"github.com/ethereum/go-ethereum/common/registrar"
    36  	"github.com/ethereum/go-ethereum/eth"
    37  	re "github.com/ethereum/go-ethereum/jsre"
    38  	"github.com/ethereum/go-ethereum/rpc"
    39  	"github.com/ethereum/go-ethereum/rpc/api"
    40  	"github.com/ethereum/go-ethereum/rpc/codec"
    41  	"github.com/ethereum/go-ethereum/rpc/comms"
    42  	"github.com/ethereum/go-ethereum/rpc/shared"
    43  	"github.com/ethereum/go-ethereum/xeth"
    44  	"github.com/peterh/liner"
    45  	"github.com/robertkrimen/otto"
    46  )
    47  
    48  var passwordRegexp = regexp.MustCompile("personal.[nu]")
    49  
    50  const passwordRepl = ""
    51  
    52  type prompter interface {
    53  	AppendHistory(string)
    54  	Prompt(p string) (string, error)
    55  	PasswordPrompt(p string) (string, error)
    56  }
    57  
    58  type dumbterm struct{ r *bufio.Reader }
    59  
    60  func (r dumbterm) Prompt(p string) (string, error) {
    61  	fmt.Print(p)
    62  	line, err := r.r.ReadString('\n')
    63  	return strings.TrimSuffix(line, "\n"), err
    64  }
    65  
    66  func (r dumbterm) PasswordPrompt(p string) (string, error) {
    67  	fmt.Println("!! Unsupported terminal, password will echo.")
    68  	fmt.Print(p)
    69  	input, err := bufio.NewReader(os.Stdin).ReadString('\n')
    70  	fmt.Println()
    71  	return input, err
    72  }
    73  
    74  func (r dumbterm) AppendHistory(string) {}
    75  
    76  type jsre struct {
    77  	ds         *docserver.DocServer
    78  	re         *re.JSRE
    79  	ethereum   *eth.Ethereum
    80  	xeth       *xeth.XEth
    81  	wait       chan *big.Int
    82  	ps1        string
    83  	atexit     func()
    84  	corsDomain string
    85  	client     comms.EthereumClient
    86  	prompter
    87  }
    88  
    89  var (
    90  	loadedModulesMethods map[string][]string
    91  )
    92  
    93  func keywordCompleter(line string) []string {
    94  	results := make([]string, 0)
    95  
    96  	if strings.Contains(line, ".") {
    97  		elements := strings.Split(line, ".")
    98  		if len(elements) == 2 {
    99  			module := elements[0]
   100  			partialMethod := elements[1]
   101  			if methods, found := loadedModulesMethods[module]; found {
   102  				for _, method := range methods {
   103  					if strings.HasPrefix(method, partialMethod) { // e.g. debug.se
   104  						results = append(results, module+"."+method)
   105  					}
   106  				}
   107  			}
   108  		}
   109  	} else {
   110  		for module, methods := range loadedModulesMethods {
   111  			if line == module { // user typed in full module name, show all methods
   112  				for _, method := range methods {
   113  					results = append(results, module+"."+method)
   114  				}
   115  			} else if strings.HasPrefix(module, line) { // partial method name, e.g. admi
   116  				results = append(results, module)
   117  			}
   118  		}
   119  	}
   120  	return results
   121  }
   122  
   123  func apiWordCompleter(line string, pos int) (head string, completions []string, tail string) {
   124  	if len(line) == 0 {
   125  		return "", nil, ""
   126  	}
   127  
   128  	i := 0
   129  	for i = pos - 1; i > 0; i-- {
   130  		if line[i] == '.' || (line[i] >= 'a' && line[i] <= 'z') || (line[i] >= 'A' && line[i] <= 'Z') {
   131  			continue
   132  		}
   133  		if i >= 3 && line[i] == '3' && line[i-3] == 'w' && line[i-2] == 'e' && line[i-1] == 'b' {
   134  			continue
   135  		}
   136  		i += 1
   137  		break
   138  	}
   139  
   140  	begin := line[:i]
   141  	keyword := line[i:pos]
   142  	end := line[pos:]
   143  
   144  	completionWords := keywordCompleter(keyword)
   145  	return begin, completionWords, end
   146  }
   147  
   148  func newLightweightJSRE(libPath string, client comms.EthereumClient, interactive bool) *jsre {
   149  	js := &jsre{ps1: "> "}
   150  	js.wait = make(chan *big.Int)
   151  	js.client = client
   152  	js.ds = docserver.New("/")
   153  
   154  	// update state in separare forever blocks
   155  	js.re = re.New(libPath)
   156  	if err := js.apiBindings(js); err != nil {
   157  		utils.Fatalf("Unable to initialize console - %v", err)
   158  	}
   159  
   160  	if !liner.TerminalSupported() || !interactive {
   161  		js.prompter = dumbterm{bufio.NewReader(os.Stdin)}
   162  	} else {
   163  		lr := liner.NewLiner()
   164  		js.withHistory(func(hist *os.File) { lr.ReadHistory(hist) })
   165  		lr.SetCtrlCAborts(true)
   166  		js.loadAutoCompletion()
   167  		lr.SetWordCompleter(apiWordCompleter)
   168  		lr.SetTabCompletionStyle(liner.TabPrints)
   169  		js.prompter = lr
   170  		js.atexit = func() {
   171  			js.withHistory(func(hist *os.File) { hist.Truncate(0); lr.WriteHistory(hist) })
   172  			lr.Close()
   173  			close(js.wait)
   174  		}
   175  	}
   176  	return js
   177  }
   178  
   179  func newJSRE(ethereum *eth.Ethereum, libPath, corsDomain string, client comms.EthereumClient, interactive bool, f xeth.Frontend) *jsre {
   180  	js := &jsre{ethereum: ethereum, ps1: "> "}
   181  	// set default cors domain used by startRpc from CLI flag
   182  	js.corsDomain = corsDomain
   183  	if f == nil {
   184  		f = js
   185  	}
   186  	js.ds = docserver.New("/")
   187  	js.xeth = xeth.New(ethereum, f)
   188  	js.wait = js.xeth.UpdateState()
   189  	js.client = client
   190  	if clt, ok := js.client.(*comms.InProcClient); ok {
   191  		if offeredApis, err := api.ParseApiString(shared.AllApis, codec.JSON, js.xeth, ethereum); err == nil {
   192  			clt.Initialize(api.Merge(offeredApis...))
   193  		}
   194  	}
   195  
   196  	// update state in separare forever blocks
   197  	js.re = re.New(libPath)
   198  	if err := js.apiBindings(f); err != nil {
   199  		utils.Fatalf("Unable to connect - %v", err)
   200  	}
   201  
   202  	if !liner.TerminalSupported() || !interactive {
   203  		js.prompter = dumbterm{bufio.NewReader(os.Stdin)}
   204  	} else {
   205  		lr := liner.NewLiner()
   206  		js.withHistory(func(hist *os.File) { lr.ReadHistory(hist) })
   207  		lr.SetCtrlCAborts(true)
   208  		js.loadAutoCompletion()
   209  		lr.SetWordCompleter(apiWordCompleter)
   210  		lr.SetTabCompletionStyle(liner.TabPrints)
   211  		js.prompter = lr
   212  		js.atexit = func() {
   213  			js.withHistory(func(hist *os.File) { hist.Truncate(0); lr.WriteHistory(hist) })
   214  			lr.Close()
   215  			close(js.wait)
   216  		}
   217  	}
   218  	return js
   219  }
   220  
   221  func (self *jsre) loadAutoCompletion() {
   222  	if modules, err := self.supportedApis(); err == nil {
   223  		loadedModulesMethods = make(map[string][]string)
   224  		for module, _ := range modules {
   225  			loadedModulesMethods[module] = api.AutoCompletion[module]
   226  		}
   227  	}
   228  }
   229  
   230  func (self *jsre) batch(statement string) {
   231  	err := self.re.EvalAndPrettyPrint(statement)
   232  
   233  	if err != nil {
   234  		fmt.Printf("error: %v", err)
   235  	}
   236  
   237  	if self.atexit != nil {
   238  		self.atexit()
   239  	}
   240  
   241  	self.re.Stop(false)
   242  }
   243  
   244  // show summary of current geth instance
   245  func (self *jsre) welcome() {
   246  	self.re.Run(`
   247  		(function () {
   248  			console.log('instance: ' + web3.version.client);
   249  			console.log(' datadir: ' + admin.datadir);
   250  			console.log("coinbase: " + eth.coinbase);
   251  			var ts = 1000 * eth.getBlock(eth.blockNumber).timestamp;
   252  			console.log("at block: " + eth.blockNumber + " (" + new Date(ts) + ")");
   253  		})();
   254  	`)
   255  	if modules, err := self.supportedApis(); err == nil {
   256  		loadedModules := make([]string, 0)
   257  		for api, version := range modules {
   258  			loadedModules = append(loadedModules, fmt.Sprintf("%s:%s", api, version))
   259  		}
   260  		sort.Strings(loadedModules)
   261  		fmt.Println("modules:", strings.Join(loadedModules, " "))
   262  	}
   263  }
   264  
   265  func (self *jsre) supportedApis() (map[string]string, error) {
   266  	return self.client.SupportedModules()
   267  }
   268  
   269  func (js *jsre) apiBindings(f xeth.Frontend) error {
   270  	apis, err := js.supportedApis()
   271  	if err != nil {
   272  		return err
   273  	}
   274  
   275  	apiNames := make([]string, 0, len(apis))
   276  	for a, _ := range apis {
   277  		apiNames = append(apiNames, a)
   278  	}
   279  
   280  	apiImpl, err := api.ParseApiString(strings.Join(apiNames, ","), codec.JSON, js.xeth, js.ethereum)
   281  	if err != nil {
   282  		utils.Fatalf("Unable to determine supported api's: %v", err)
   283  	}
   284  
   285  	jeth := rpc.NewJeth(api.Merge(apiImpl...), js.re, js.client, f)
   286  	js.re.Set("jeth", struct{}{})
   287  	t, _ := js.re.Get("jeth")
   288  	jethObj := t.Object()
   289  
   290  	jethObj.Set("send", jeth.Send)
   291  	jethObj.Set("sendAsync", jeth.Send)
   292  
   293  	err = js.re.Compile("bignumber.js", re.BigNumber_JS)
   294  	if err != nil {
   295  		utils.Fatalf("Error loading bignumber.js: %v", err)
   296  	}
   297  
   298  	err = js.re.Compile("ethereum.js", re.Web3_JS)
   299  	if err != nil {
   300  		utils.Fatalf("Error loading web3.js: %v", err)
   301  	}
   302  
   303  	_, err = js.re.Run("var web3 = require('web3');")
   304  	if err != nil {
   305  		utils.Fatalf("Error requiring web3: %v", err)
   306  	}
   307  
   308  	_, err = js.re.Run("web3.setProvider(jeth)")
   309  	if err != nil {
   310  		utils.Fatalf("Error setting web3 provider: %v", err)
   311  	}
   312  
   313  	// load only supported API's in javascript runtime
   314  	shortcuts := "var eth = web3.eth; "
   315  	for _, apiName := range apiNames {
   316  		if apiName == shared.Web3ApiName {
   317  			continue // manually mapped
   318  		}
   319  
   320  		if err = js.re.Compile(fmt.Sprintf("%s.js", apiName), api.Javascript(apiName)); err == nil {
   321  			shortcuts += fmt.Sprintf("var %s = web3.%s; ", apiName, apiName)
   322  		} else {
   323  			utils.Fatalf("Error loading %s.js: %v", apiName, err)
   324  		}
   325  	}
   326  
   327  	_, err = js.re.Run(shortcuts)
   328  
   329  	if err != nil {
   330  		utils.Fatalf("Error setting namespaces: %v", err)
   331  	}
   332  
   333  	js.re.Run(`var GlobalRegistrar = eth.contract(` + registrar.GlobalRegistrarAbi + `);	 registrar = GlobalRegistrar.at("` + registrar.GlobalRegistrarAddr + `");`)
   334  	return nil
   335  }
   336  
   337  func (self *jsre) ConfirmTransaction(tx string) bool {
   338  	if self.ethereum.NatSpec {
   339  		notice := natspec.GetNotice(self.xeth, tx, self.ds)
   340  		fmt.Println(notice)
   341  		answer, _ := self.Prompt("Confirm Transaction [y/n]")
   342  		return strings.HasPrefix(strings.Trim(answer, " "), "y")
   343  	} else {
   344  		return true
   345  	}
   346  }
   347  
   348  func (self *jsre) UnlockAccount(addr []byte) bool {
   349  	fmt.Printf("Please unlock account %x.\n", addr)
   350  	pass, err := self.PasswordPrompt("Passphrase: ")
   351  	if err != nil {
   352  		return false
   353  	}
   354  	// TODO: allow retry
   355  	if err := self.ethereum.AccountManager().Unlock(common.BytesToAddress(addr), pass); err != nil {
   356  		return false
   357  	} else {
   358  		fmt.Println("Account is now unlocked for this session.")
   359  		return true
   360  	}
   361  }
   362  
   363  func (self *jsre) exec(filename string) error {
   364  	if err := self.re.Exec(filename); err != nil {
   365  		self.re.Stop(false)
   366  		return fmt.Errorf("Javascript Error: %v", err)
   367  	}
   368  	self.re.Stop(true)
   369  	return nil
   370  }
   371  
   372  func (self *jsre) interactive() {
   373  	// Read input lines.
   374  	prompt := make(chan string)
   375  	inputln := make(chan string)
   376  	go func() {
   377  		defer close(inputln)
   378  		for {
   379  			line, err := self.Prompt(<-prompt)
   380  			if err != nil {
   381  				if err == liner.ErrPromptAborted { // ctrl-C
   382  					self.resetPrompt()
   383  					inputln <- ""
   384  					continue
   385  				}
   386  				return
   387  			}
   388  			inputln <- line
   389  		}
   390  	}()
   391  	// Wait for Ctrl-C, too.
   392  	sig := make(chan os.Signal, 1)
   393  	signal.Notify(sig, os.Interrupt)
   394  
   395  	defer func() {
   396  		if self.atexit != nil {
   397  			self.atexit()
   398  		}
   399  		self.re.Stop(false)
   400  	}()
   401  	for {
   402  		prompt <- self.ps1
   403  		select {
   404  		case <-sig:
   405  			fmt.Println("caught interrupt, exiting")
   406  			return
   407  		case input, ok := <-inputln:
   408  			if !ok || indentCount <= 0 && input == "exit" {
   409  				return
   410  			}
   411  			if input == "" {
   412  				continue
   413  			}
   414  			str += input + "\n"
   415  			self.setIndent()
   416  			if indentCount <= 0 {
   417  				hist := hidepassword(str[:len(str)-1])
   418  				if len(hist) > 0 {
   419  					self.AppendHistory(hist)
   420  				}
   421  				self.parseInput(str)
   422  				str = ""
   423  			}
   424  		}
   425  	}
   426  }
   427  
   428  func hidepassword(input string) string {
   429  	if passwordRegexp.MatchString(input) {
   430  		return passwordRepl
   431  	} else {
   432  		return input
   433  	}
   434  }
   435  
   436  func (self *jsre) withHistory(op func(*os.File)) {
   437  	datadir := common.DefaultDataDir()
   438  	if self.ethereum != nil {
   439  		datadir = self.ethereum.DataDir
   440  	}
   441  
   442  	hist, err := os.OpenFile(filepath.Join(datadir, "history"), os.O_RDWR|os.O_CREATE, os.ModePerm)
   443  	if err != nil {
   444  		fmt.Printf("unable to open history file: %v\n", err)
   445  		return
   446  	}
   447  	op(hist)
   448  	hist.Close()
   449  }
   450  
   451  func (self *jsre) parseInput(code string) {
   452  	defer func() {
   453  		if r := recover(); r != nil {
   454  			fmt.Println("[native] error", r)
   455  		}
   456  	}()
   457  	if err := self.re.EvalAndPrettyPrint(code); err != nil {
   458  		if ottoErr, ok := err.(*otto.Error); ok {
   459  			fmt.Println(ottoErr.String())
   460  		} else {
   461  			fmt.Println(err)
   462  		}
   463  		return
   464  	}
   465  }
   466  
   467  var indentCount = 0
   468  var str = ""
   469  
   470  func (self *jsre) resetPrompt() {
   471  	indentCount = 0
   472  	str = ""
   473  	self.ps1 = "> "
   474  }
   475  
   476  func (self *jsre) setIndent() {
   477  	open := strings.Count(str, "{")
   478  	open += strings.Count(str, "(")
   479  	closed := strings.Count(str, "}")
   480  	closed += strings.Count(str, ")")
   481  	indentCount = open - closed
   482  	if indentCount <= 0 {
   483  		self.ps1 = "> "
   484  	} else {
   485  		self.ps1 = strings.Join(make([]string, indentCount*2), "..")
   486  		self.ps1 += " "
   487  	}
   488  }