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 }