gitlab.com/aquachain/aquachain@v1.17.16-rc3.0.20221018032414-e3ddf1e1c055/opt/console/console.go (about) 1 // Copyright 2018 The aquachain Authors 2 // This file is part of the aquachain library. 3 // 4 // The aquachain library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser 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 // The aquachain library 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 Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the aquachain library. If not, see <http://www.gnu.org/licenses/>. 16 17 package console 18 19 import ( 20 "fmt" 21 "io" 22 "io/ioutil" 23 "os" 24 "os/signal" 25 "path/filepath" 26 "regexp" 27 "sort" 28 "strings" 29 "syscall" 30 31 colorable "github.com/mattn/go-colorable" 32 "github.com/peterh/liner" 33 "github.com/robertkrimen/otto" 34 "gitlab.com/aquachain/aquachain/internal/jsre" 35 "gitlab.com/aquachain/aquachain/internal/web3ext" 36 rpc "gitlab.com/aquachain/aquachain/rpc/rpcclient" 37 ) 38 39 var ( 40 passwordRegexp = regexp.MustCompile(`personal.[nus]`) 41 onlyWhitespace = regexp.MustCompile(`^\s*$`) 42 exit = regexp.MustCompile(`^\s*exit\s*;*\s*$`) 43 help = regexp.MustCompile(`^\s*help\s*;*\s*$`) 44 sendline = regexp.MustCompile(`^\s*send\s*;*\s*$`) 45 ) 46 47 // HistoryFile is the file within the data directory to store input scrollback. 48 const HistoryFile = "history" 49 50 // DefaultPrompt is the default prompt line prefix to use for user input querying. 51 const DefaultPrompt = "AQUA> " 52 53 const helpText = ` 54 Web links: 55 56 Website: https://aquachain.github.io 57 Explorer: https://aquachain.github.io/explorer/ 58 Wiki: http://github.com/aquanetwork/aquachain/wiki/Basics 59 60 Common AQUA console commands:: 61 62 New address: personal.newAccount() 63 Import private key: personal.importRawKey('the private key') 64 Get balance: aqua.balance(aqua.coinbase) 65 Get all balances: balance() 66 Get pending balances: pending() 67 Send transaction: send 68 List accounts: aqua.accounts 69 Show Transaction: aqua.getTransaction('the tx hash') 70 Show Block #1000: aqua.getBlock('1000') 71 Show Latest: aqua.getBlock('latest') 72 Convert 1 AQUA to wei: web3.toWei(1) 73 Convert Hex to Decimal: web3.toDecimal('0x123') 74 75 Show peers: admin.peers 76 Show Aquachain Node info: admin.nodeInfo 77 Show Scheduled hard forks:admin.nodeInfo.protocols 78 79 80 In this javascript console, you can define variables and load script. 81 82 loadScript('filename.js') 83 block = aqua.getBlock 84 myBlock = block('0x92cd50f36edddd9347ec37ef93206135518acd4115941f6287ea00407f186e15') 85 tx = aqua.getTransaction('0x23eabf63f8da796e2e68cd2ae602c1b5a9cb8f9946ad9d87a9561924e3d20db8') 86 web3.fromWei(tx.value) 87 88 You are in the AQUA console. 89 Type any command, press TAB to autocomplete commands. 90 ` 91 92 const logo = ` _ _ 93 __ _ __ _ _ _ __ _ ___| |__ __ _(_)_ __ 94 / _ '|/ _' | | | |/ _' |/ __| '_ \ / _' | | '_ \ 95 | (_| | (_| | |_| | (_| | (__| | | | (_| | | | | | 96 \__,_|\__, |\__,_|\__,_|\___|_| |_|\__,_|_|_| |_| 97 |_|` + "\nUpdate Often! https://gitlab.com/aquachain/aquachain\n\n" 98 99 // Config is the collection of configurations to fine tune the behavior of the 100 // JavaScript console. 101 type Config struct { 102 DataDir string // Data directory to store the console history at 103 DocRoot string // Filesystem path from where to load JavaScript files from 104 Client *rpc.Client // RPC client to execute Aquachain requests through 105 Prompt string // Input prompt prefix string (defaults to DefaultPrompt) 106 Prompter UserPrompter // Input prompter to allow interactive user feedback (defaults to TerminalPrompter) 107 Printer io.Writer // Output writer to serialize any display strings to (defaults to os.Stdout) 108 Preload []string // Absolute paths to JavaScript files to preload 109 } 110 111 // Console is a JavaScript interpreted runtime environment. It is a fully fleged 112 // JavaScript console attached to a running node via an external or in-process RPC 113 // client. 114 type Console struct { 115 client *rpc.Client // RPC client to execute Aquachain requests through 116 jsre *jsre.JSRE // JavaScript runtime environment running the interpreter 117 prompt string // Input prompt prefix string 118 prompter UserPrompter // Input prompter to allow interactive user feedback 119 histPath string // Absolute path to the console scrollback history 120 history []string // Scroll history maintained by the console 121 printer io.Writer // Output writer to serialize any display strings to 122 } 123 124 func New(config Config) (*Console, error) { 125 // Handle unset config values gracefully 126 if config.Prompter == nil { 127 config.Prompter = Stdin 128 } 129 if config.Prompt == "" { 130 config.Prompt = DefaultPrompt 131 } 132 if config.Printer == nil { 133 config.Printer = colorable.NewColorableStdout() 134 } 135 // Initialize the console and return 136 console := &Console{ 137 client: config.Client, 138 jsre: jsre.New(config.DocRoot, config.Printer), 139 prompt: config.Prompt, 140 prompter: config.Prompter, 141 printer: config.Printer, 142 histPath: filepath.Join(config.DataDir, HistoryFile), 143 } 144 if err := os.MkdirAll(config.DataDir, 0700); err != nil { 145 return nil, err 146 } 147 if err := console.init(config.Preload); err != nil { 148 return nil, err 149 } 150 return console, nil 151 } 152 153 // init retrieves the available APIs from the remote RPC provider and initializes 154 // the console's JavaScript namespaces based on the exposed modules. 155 func (c *Console) init(preload []string) error { 156 // Initialize the JavaScript <-> Go RPC bridge 157 bridge := newBridge(c.client, c.prompter, c.printer) 158 c.jsre.Set("jeth", struct{}{}) 159 160 jethObj, _ := c.jsre.Get("jeth") 161 jethObj.Object().Set("send", bridge.Send) 162 jethObj.Object().Set("sendAsync", bridge.Send) 163 164 consoleObj, _ := c.jsre.Get("console") 165 consoleObj.Object().Set("log", c.consoleOutput) 166 consoleObj.Object().Set("error", c.consoleOutput) 167 168 // Load all the internal utility JavaScript libraries 169 if err := c.jsre.Compile("bignumber.js", jsre.BigNumber_JS); err != nil { 170 return fmt.Errorf("bignumber.js: %v", err) 171 } 172 if err := c.jsre.Compile("web3.js", jsre.Web3_JS); err != nil { 173 return fmt.Errorf("web3.js: %v", err) 174 } 175 if _, err := c.jsre.Run("var Web3 = require('web3');"); err != nil { 176 return fmt.Errorf("web3 require: %v", err) 177 } 178 if _, err := c.jsre.Run("var web3 = new Web3(jeth);"); err != nil { 179 return fmt.Errorf("web3 provider: %v", err) 180 } 181 // Load the supported APIs into the JavaScript runtime environment 182 apis, err := c.client.SupportedModules() 183 if err != nil { 184 return fmt.Errorf("api modules: %v", err) 185 } 186 flatten := "var aqua = web3.aqua; var personal = web3.personal; " 187 for api := range apis { 188 if api == "web3" { 189 continue // manually mapped or ignore 190 } 191 if file, ok := web3ext.Modules[api]; ok { 192 // Load our extension for the module. 193 if err = c.jsre.Compile(fmt.Sprintf("%s.js", api), file); err != nil { 194 return fmt.Errorf("%s.js: %v", api, err) 195 } 196 flatten += fmt.Sprintf("var %s = web3.%s; ", api, api) 197 } else if obj, err := c.jsre.Run("web3." + api); err == nil && obj.IsObject() { 198 // Enable web3.js built-in extension if available. 199 flatten += fmt.Sprintf("var %s = web3.%s; ", api, api) 200 } 201 } 202 if _, err = c.jsre.Run(flatten); err != nil { 203 return fmt.Errorf("namespace flattening: %v", err) 204 } 205 // Initialize the global name register (disabled for now) 206 //c.jsre.Run(`var GlobalRegistrar = aqua.contract(` + registrar.GlobalRegistrarAbi + `); registrar = GlobalRegistrar.at("` + registrar.GlobalRegistrarAddr + `");`) 207 208 // If the console is in interactive mode, instrument password related methods to query the user 209 if c.prompter != nil { 210 // Retrieve the account management object to instrument 211 personal, err := c.jsre.Get("personal") 212 if err != nil { 213 return err 214 } 215 // Override the openWallet, unlockAccount, newAccount and sign methods since 216 // these require user interaction. Assign these method in the Console the 217 // original web3 callbacks. These will be called by the jeth.* methods after 218 // they got the password from the user and send the original web3 request to 219 // the backend. 220 if obj := personal.Object(); obj != nil { // make sure the personal api is enabled over the interface 221 if _, err = c.jsre.Run(`jeth.openWallet = personal.openWallet;`); err != nil { 222 return fmt.Errorf("personal.openWallet: %v", err) 223 } 224 if _, err = c.jsre.Run(`jeth.unlockAccount = personal.unlockAccount;`); err != nil { 225 return fmt.Errorf("personal.unlockAccount: %v", err) 226 } 227 if _, err = c.jsre.Run(`jeth.newAccount = personal.newAccount;`); err != nil { 228 return fmt.Errorf("personal.newAccount: %v", err) 229 } 230 if _, err = c.jsre.Run(`jeth.sign = personal.sign;`); err != nil { 231 return fmt.Errorf("personal.sign: %v", err) 232 } 233 obj.Set("openWallet", bridge.OpenWallet) 234 obj.Set("unlockAccount", bridge.UnlockAccount) 235 obj.Set("newAccount", bridge.NewAccount) 236 obj.Set("sign", bridge.Sign) 237 } 238 } 239 // The admin.sleep and admin.sleepBlocks are offered by the console and not by the RPC layer. 240 admin, err := c.jsre.Get("admin") 241 if err != nil { 242 return err 243 } 244 if obj := admin.Object(); obj != nil { // make sure the admin api is enabled over the interface 245 obj.Set("sleepBlocks", bridge.SleepBlocks) 246 obj.Set("sleep", bridge.Sleep) 247 obj.Set("clearHistory", c.clearHistory) 248 } 249 // Preload any JavaScript files before starting the console 250 for _, path := range preload { 251 if err := c.jsre.Exec(path); err != nil { 252 failure := err.Error() 253 if ottoErr, ok := err.(*otto.Error); ok { 254 failure = ottoErr.String() 255 } 256 return fmt.Errorf("%s: %v", path, failure) 257 } 258 } 259 // Configure the console's input prompter for scrollback and tab completion 260 if c.prompter != nil { 261 if content, err := ioutil.ReadFile(c.histPath); err != nil { 262 c.prompter.SetHistory(nil) 263 } else { 264 c.history = strings.Split(string(content), "\n") 265 c.prompter.SetHistory(c.history) 266 } 267 c.prompter.SetWordCompleter(c.AutoCompleteInput) 268 } 269 return nil 270 } 271 272 func (c *Console) clearHistory() { 273 c.history = nil 274 c.prompter.ClearHistory() 275 if err := os.Remove(c.histPath); err != nil { 276 fmt.Fprintln(c.printer, "can't delete history file:", err) 277 } else { 278 fmt.Fprintln(c.printer, "history file deleted") 279 } 280 } 281 282 // consoleOutput is an override for the console.log and console.error methods to 283 // stream the output into the configured output stream instead of stdout. 284 func (c *Console) consoleOutput(call otto.FunctionCall) otto.Value { 285 output := []string{} 286 for _, argument := range call.ArgumentList { 287 output = append(output, fmt.Sprintf("%v", argument)) 288 } 289 fmt.Fprintln(c.printer, strings.Join(output, " ")) 290 return otto.Value{} 291 } 292 293 // AutoCompleteInput is a pre-assembled word completer to be used by the user 294 // input prompter to provide hints to the user about the methods available. 295 func (c *Console) AutoCompleteInput(line string, pos int) (string, []string, string) { 296 // No completions can be provided for empty inputs 297 if len(line) == 0 || pos == 0 { 298 return "", c.jsre.CompleteKeywords(""), "" 299 } 300 // Chunck data to relevant part for autocompletion 301 // E.g. in case of nested lines aqua.getBalance(aqua.coinb<tab><tab> 302 start := pos - 1 303 for ; start > 0; start-- { 304 // Skip all methods and namespaces (i.e. including the dot) 305 if line[start] == '.' || (line[start] >= 'a' && line[start] <= 'z') || (line[start] >= 'A' && line[start] <= 'Z') { 306 continue 307 } 308 // Handle web3 in a special way (i.e. other numbers aren't auto completed) 309 if start >= 3 && line[start-3:start] == "web3" { 310 start -= 3 311 continue 312 } 313 // We've hit an unexpected character, autocomplete form here 314 start++ 315 break 316 } 317 return line[:start], c.jsre.CompleteKeywords(line[start:pos]), line[pos:] 318 } 319 320 // Welcome show summary of current Aquachain instance and some metadata about the 321 // console's available modules. 322 func (c *Console) Welcome() { 323 // friendly balance 324 c.jsre.Run(` 325 function pending() { 326 var totalBal = 0; 327 for (var acctNum in aqua.accounts) { 328 var acct = aqua.accounts[acctNum]; 329 var acctBal = aqua.balance(acct, 'pending'); 330 totalBal += parseFloat(acctBal); 331 console.log(" aqua.accounts[" + acctNum + "]: \t" + acct + " \tbalance: " + acctBal + " AQUA"); 332 } 333 console.log("Pending balance: " + totalBal + " AQUA"); 334 return totalBal; 335 }; 336 function balance() { 337 var totalBal = 0; 338 for (var acctNum in aqua.accounts) { 339 var acct = aqua.accounts[acctNum]; 340 var acctBal = aqua.balance(acct, 'latest'); 341 totalBal += parseFloat(acctBal); 342 console.log(" aqua.accounts[" + acctNum + "]: \t" + acct + " \tbalance: " + acctBal + " AQUA"); 343 } 344 console.log(" Total balance: " + totalBal + " AQUA"); 345 return totalBal; 346 }; 347 `) 348 349 // Print some generic Aquachain metadata 350 fmt.Fprintf(c.printer, "\nWelcome to the Aquachain JavaScript console!\n") 351 fmt.Fprintf(c.printer, logo) 352 353 c.jsre.Run(` 354 console.log("instance: " + web3.version.node); 355 console.log("coinbase: " + aqua.coinbase); 356 console.log("at block: " + aqua.blockNumber + " (" + new Date(1000 * aqua.getBlock(aqua.blockNumber).timestamp) + ")"); 357 console.log(" algo: " + aqua.getBlock(aqua.blockNumber).version); 358 console.log(" datadir: " + admin.datadir); 359 `) 360 361 // List all the supported modules for the user to call 362 if apis, err := c.client.SupportedModules(); err == nil { 363 modules := make([]string, 0, len(apis)) 364 for api, version := range apis { 365 if api == "eth" { 366 continue 367 } 368 modules = append(modules, fmt.Sprintf("%s:%s", api, version)) 369 } 370 sort.Strings(modules) 371 fmt.Fprintln(c.printer, " modules:", strings.Join(modules, " ")) 372 } 373 fmt.Fprintln(c.printer) 374 } 375 376 // Evaluate executes code and pretty prints the result to the specified output 377 // stream. 378 func (c *Console) Evaluate(statement string) error { 379 defer func() { 380 if r := recover(); r != nil { 381 fmt.Fprintf(c.printer, "[native] error: %v\n", r) 382 } 383 }() 384 return c.jsre.Evaluate(statement, c.printer) 385 } 386 387 // Interactive starts an interactive user session, where input is propted from 388 // the configured user prompter. 389 func (c *Console) Interactive() { 390 var ( 391 prompt = c.prompt // Current prompt line (used for multi-line inputs) 392 indents = 0 // Current number of input indents (used for multi-line inputs) 393 input = "" // Current user input 394 scheduler = make(chan string) // Channel to send the next prompt on and receive the input 395 ) 396 // Start a goroutine to listen for promt requests and send back inputs 397 go func() { 398 for { 399 // Read the next user input 400 line, err := c.prompter.PromptInput(<-scheduler) 401 if err != nil { 402 // In case of an error, either clear the prompt or fail 403 if err == liner.ErrPromptAborted { // ctrl-C 404 prompt, indents, input = c.prompt, 0, "" 405 scheduler <- "" 406 continue 407 } 408 close(scheduler) 409 return 410 } 411 // User input retrieved, send for interpretation and loop 412 scheduler <- line 413 } 414 }() 415 // Monitor Ctrl-C too in case the input is empty and we need to bail 416 abort := make(chan os.Signal, 1) 417 signal.Notify(abort, syscall.SIGINT, syscall.SIGTERM) 418 419 // Start sending prompts to the user and reading back inputs 420 for { 421 // Send the next prompt, triggering an input read and process the result 422 scheduler <- prompt 423 select { 424 case <-abort: 425 // User forcefully quite the console 426 fmt.Fprintln(c.printer, "caught interrupt, exiting") 427 return 428 429 case line, ok := <-scheduler: 430 // User input was returned by the prompter, handle special cases 431 if !ok || (indents <= 0 && exit.MatchString(line)) { 432 return 433 } 434 if onlyWhitespace.MatchString(line) { 435 continue 436 } 437 if !ok || (indents <= 0 && help.MatchString(line)) { 438 fmt.Fprintf(c.printer, "%s", helpText) 439 continue 440 } 441 442 // command: 'send' 443 if sendline.MatchString(line) { 444 err := handleSend(c) 445 if err != nil { 446 fmt.Fprintln(c.printer, "Error:", err) 447 continue 448 } 449 fmt.Fprintln(c.printer, "TX Sent!") 450 continue 451 } 452 // Append the line to the input and check for multi-line interpretation 453 input += line + "\n" 454 455 indents = countIndents(input) 456 if indents <= 0 { 457 prompt = c.prompt 458 } else { 459 prompt = strings.Repeat(".", indents*3) + " " 460 } 461 // If all the needed lines are present, save the command and run 462 if indents <= 0 { 463 if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) { 464 if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] { 465 c.history = append(c.history, command) 466 if c.prompter != nil { 467 c.prompter.AppendHistory(command) 468 } 469 } 470 } 471 c.Evaluate(input) 472 input = "" 473 } 474 } 475 } 476 } 477 478 func handleSend(c *Console) error { 479 480 cont, err := c.prompter.PromptConfirm("You are about to create a transaction from your Aquabase. Right?") 481 if err != nil { 482 return fmt.Errorf("input error: %v", err) 483 } 484 if !cont { 485 return fmt.Errorf("transaction canceled") 486 } 487 _, err = c.jsre.Run(`personal.unlockAccount(aqua.coinbase);`) 488 if err != nil { 489 return fmt.Errorf("error: %v", err) 490 } 491 492 amount, err := c.prompter.PromptInput("How much AQUA to send? For example: 0.1: ") 493 if err != nil { 494 return fmt.Errorf("input error: %v", err) 495 } 496 497 fmt.Fprintf(c.printer, "Send %q to whom?", amount) 498 499 destination, err := c.prompter.PromptInput("Where to send? With 0x prefix: ") 500 if err != nil { 501 return fmt.Errorf("input error: %v", err) 502 } 503 504 cont, err = c.prompter.PromptConfirm(fmt.Sprintf("Send %s to %s?", amount, destination)) 505 if err != nil { 506 return fmt.Errorf("input error: %v", err) 507 } 508 if !cont { 509 return fmt.Errorf("transaction canceled") 510 } 511 512 fmt.Fprintln(c.printer, "Running:\n"+`aqua.sendTransaction({from: aqua.coinbase, to: '`+destination+`', value: web3.toWei(`+amount+`,'aqua')});`) 513 cont, err = c.prompter.PromptConfirm(fmt.Sprintf("REALLY Send %s to %s?", amount, destination)) 514 if err != nil { 515 return fmt.Errorf("input error: %v", err) 516 } 517 if !cont { 518 return fmt.Errorf("transaction canceled") 519 } 520 if !strings.HasPrefix(destination, "0x") && !strings.HasPrefix(destination, "aqua.accounts[") { 521 return fmt.Errorf("does not have 0x prefix") 522 } 523 _, err = c.jsre.Run(`aqua.sendTransaction({from: aqua.coinbase, to: '` + destination + `', value: web3.toWei(` + amount + `,'aqua')});`) 524 if err != nil { 525 return fmt.Errorf("error: %v", err) 526 } 527 528 return nil 529 } 530 531 // countIndents returns the number of identations for the given input. 532 // In case of invalid input such as var a = } the result can be negative. 533 func countIndents(input string) int { 534 var ( 535 indents = 0 536 inString = false 537 strOpenChar = ' ' // keep track of the string open char to allow var str = "I'm ...."; 538 charEscaped = false // keep track if the previous char was the '\' char, allow var str = "abc\"def"; 539 ) 540 541 for _, c := range input { 542 switch c { 543 case '\\': 544 // indicate next char as escaped when in string and previous char isn't escaping this backslash 545 if !charEscaped && inString { 546 charEscaped = true 547 } 548 case '\'', '"': 549 if inString && !charEscaped && strOpenChar == c { // end string 550 inString = false 551 } else if !inString && !charEscaped { // begin string 552 inString = true 553 strOpenChar = c 554 } 555 charEscaped = false 556 case '{', '(': 557 if !inString { // ignore brackets when in string, allow var str = "a{"; without indenting 558 indents++ 559 } 560 charEscaped = false 561 case '}', ')': 562 if !inString { 563 indents-- 564 } 565 charEscaped = false 566 default: 567 charEscaped = false 568 } 569 } 570 571 return indents 572 } 573 574 // Execute runs the JavaScript file specified as the argument. 575 func (c *Console) Execute(path string) error { 576 return c.jsre.Exec(path) 577 } 578 579 // Stop cleans up the console and terminates the runtime environment. 580 func (c *Console) Stop(graceful bool) error { 581 if err := ioutil.WriteFile(c.histPath, []byte(strings.Join(c.history, "\n")), 0600); err != nil { 582 return err 583 } 584 if err := os.Chmod(c.histPath, 0600); err != nil { // Force 0600, even if it was different previously 585 return err 586 } 587 c.jsre.Stop(graceful) 588 return nil 589 }