github.com/klaytn/klaytn@v1.10.2/console/bridge.go (about) 1 // Modifications Copyright 2018 The klaytn Authors 2 // Copyright 2016 The go-ethereum Authors 3 // This file is part of the go-ethereum library. 4 // 5 // The go-ethereum library is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Lesser General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // The go-ethereum library is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Lesser General Public License for more details. 14 // 15 // You should have received a copy of the GNU Lesser General Public License 16 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 17 // 18 // This file is derived from console/bridge.go (2018/06/04). 19 // Modified and improved for the klaytn development. 20 21 package console 22 23 import ( 24 "encoding/json" 25 "fmt" 26 "io" 27 "strings" 28 "time" 29 30 "github.com/klaytn/klaytn/log" 31 "github.com/klaytn/klaytn/networks/rpc" 32 33 "github.com/robertkrimen/otto" 34 ) 35 36 // bridge is a collection of JavaScript utility methods to bride the .js runtime 37 // environment and the Go RPC connection backing the remote method calls. 38 type bridge struct { 39 client *rpc.Client // RPC client to execute Klaytn requests through 40 prompter UserPrompter // Input prompter to allow interactive user feedback 41 printer io.Writer // Output writer to serialize any display strings to 42 } 43 44 var logger = log.NewModuleLogger(log.Console) 45 46 // newBridge creates a new JavaScript wrapper around an RPC client. 47 func newBridge(client *rpc.Client, prompter UserPrompter, printer io.Writer) *bridge { 48 return &bridge{ 49 client: client, 50 prompter: prompter, 51 printer: printer, 52 } 53 } 54 55 // NewAccount is a wrapper around the personal.newAccount RPC method that uses a 56 // non-echoing password prompt to acquire the passphrase and executes the original 57 // RPC method (saved in jeth.newAccount) with it to actually execute the RPC call. 58 func (b *bridge) NewAccount(call otto.FunctionCall) (response otto.Value) { 59 var ( 60 password string 61 confirm string 62 err error 63 ) 64 switch { 65 // No password was specified, prompt the user for it 66 case len(call.ArgumentList) == 0: 67 if password, err = b.prompter.PromptPassword("Passphrase: "); err != nil { 68 throwJSException(err.Error()) 69 } 70 if confirm, err = b.prompter.PromptPassword("Repeat passphrase: "); err != nil { 71 throwJSException(err.Error()) 72 } 73 if password != confirm { 74 throwJSException("passphrases don't match!") 75 } 76 77 // A single string password was specified, use that 78 case len(call.ArgumentList) == 1 && call.Argument(0).IsString(): 79 password, _ = call.Argument(0).ToString() 80 81 // Otherwise fail with some error 82 default: 83 throwJSException("expected 0 or 1 string argument") 84 } 85 // Password acquired, execute the call and return 86 ret, err := call.Otto.Call("jeth.newAccount", nil, password) 87 if err != nil { 88 throwJSException(err.Error()) 89 } 90 return ret 91 } 92 93 // OpenWallet is a wrapper around personal.openWallet which can interpret and 94 // react to certain error messages, such as the Trezor PIN matrix request. 95 func (b *bridge) OpenWallet(call otto.FunctionCall) (response otto.Value) { 96 // Make sure we have a wallet specified to open 97 if !call.Argument(0).IsString() { 98 throwJSException("first argument must be the wallet URL to open") 99 } 100 wallet := call.Argument(0) 101 102 var passwd otto.Value 103 if call.Argument(1).IsUndefined() || call.Argument(1).IsNull() { 104 passwd, _ = otto.ToValue("") 105 } else { 106 passwd = call.Argument(1) 107 } 108 // Open the wallet and return if successful in itself 109 val, err := call.Otto.Call("jeth.openWallet", nil, wallet, passwd) 110 if err == nil { 111 return val 112 } 113 114 // Trezor PIN matrix input requested, display the matrix to the user and fetch the data 115 fmt.Fprintf(b.printer, "Look at the device for number positions\n\n") 116 fmt.Fprintf(b.printer, "7 | 8 | 9\n") 117 fmt.Fprintf(b.printer, "--+---+--\n") 118 fmt.Fprintf(b.printer, "4 | 5 | 6\n") 119 fmt.Fprintf(b.printer, "--+---+--\n") 120 fmt.Fprintf(b.printer, "1 | 2 | 3\n\n") 121 122 if input, err := b.prompter.PromptPassword("Please enter current PIN: "); err != nil { 123 throwJSException(err.Error()) 124 } else { 125 passwd, _ = otto.ToValue(input) 126 } 127 if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil { 128 throwJSException(err.Error()) 129 } 130 return val 131 } 132 133 // UnlockAccount is a wrapper around the personal.unlockAccount RPC method that 134 // uses a non-echoing password prompt to acquire the passphrase and executes the 135 // original RPC method (saved in jeth.unlockAccount) with it to actually execute 136 // the RPC call. 137 func (b *bridge) UnlockAccount(call otto.FunctionCall) (response otto.Value) { 138 // Make sure we have an account specified to unlock 139 if !call.Argument(0).IsString() { 140 throwJSException("first argument must be the account to unlock") 141 } 142 account := call.Argument(0) 143 144 // If password is not given or is the null value, prompt the user for it 145 var passwd otto.Value 146 147 if call.Argument(1).IsUndefined() || call.Argument(1).IsNull() { 148 fmt.Fprintf(b.printer, "Unlock account %s\n", account) 149 if input, err := b.prompter.PromptPassword("Passphrase: "); err != nil { 150 throwJSException(err.Error()) 151 } else { 152 passwd, _ = otto.ToValue(input) 153 } 154 } else { 155 if !call.Argument(1).IsString() { 156 throwJSException("password must be a string") 157 } 158 passwd = call.Argument(1) 159 } 160 // Third argument is the duration how long the account must be unlocked. 161 duration := otto.NullValue() 162 if call.Argument(2).IsDefined() && !call.Argument(2).IsNull() { 163 if !call.Argument(2).IsNumber() { 164 throwJSException("unlock duration must be a number") 165 } 166 duration = call.Argument(2) 167 } 168 // Send the request to the backend and return 169 val, err := call.Otto.Call("jeth.unlockAccount", nil, account, passwd, duration) 170 if err != nil { 171 throwJSException(err.Error()) 172 } 173 return val 174 } 175 176 // passwdByPrompt returns the password from a non-echoing password prompt. 177 func (b *bridge) passwdByPrompt(msg string) (passwd otto.Value) { 178 fmt.Fprintln(b.printer, msg) 179 if input, err := b.prompter.PromptPassword("Passphrase: "); err != nil { 180 throwJSException(err.Error()) 181 } else { 182 passwd, _ = otto.ToValue(input) 183 } 184 return 185 } 186 187 // passwdByPromptWithDuration returns the password and the duration from a non-echoing password prompt. 188 func (b *bridge) passwdByPromptWithDuration(call otto.FunctionCall, msg string) (passwd, duration otto.Value) { 189 duration = otto.NullValue() 190 passwd = otto.NullValue() 191 192 switch len(call.ArgumentList) { 193 case 0: 194 passwd = b.passwdByPrompt(msg) 195 case 1: 196 if call.Argument(0).IsString() { 197 passwd = call.Argument(0) 198 } else { 199 throwJSException("password must be a string") 200 } 201 case 2: 202 if call.Argument(0).IsString() && call.Argument(1).IsNumber() { 203 passwd = call.Argument(0) 204 duration = call.Argument(1) 205 } else { 206 throwJSException("invalid arguments") 207 } 208 default: 209 throwJSException("invalid arguments") 210 } 211 return 212 } 213 214 // UnlockParentOperator is a wrapper around the subbridge.unlockParentOperator RPC method that 215 // uses a non-echoing password prompt to acquire the passphrase and executes the 216 // original RPC method (saved in jeth.unlockParentOperator) with it to actually execute 217 // the RPC call. 218 func (b *bridge) UnlockParentOperator(call otto.FunctionCall) (response otto.Value) { 219 passwd, duration := b.passwdByPromptWithDuration(call, "Unlock parent operator account") 220 221 // Send the request to the backend and return 222 response, err := call.Otto.Call("jeth.unlockParentOperator", nil, passwd, duration) 223 if err != nil { 224 throwJSException(err.Error()) 225 } 226 return 227 } 228 229 // UnlockChildOperator is a wrapper around the subbridge.unlockChildOperator RPC method that 230 // uses a non-echoing password prompt to acquire the passphrase and executes the 231 // original RPC method (saved in jeth.unlockChildOperator) with it to actually execute 232 // the RPC call. 233 func (b *bridge) UnlockChildOperator(call otto.FunctionCall) (response otto.Value) { 234 passwd, duration := b.passwdByPromptWithDuration(call, "Unlock child operator account") 235 236 // Send the request to the backend and return 237 response, err := call.Otto.Call("jeth.unlockChildOperator", nil, passwd, duration) 238 if err != nil { 239 throwJSException(err.Error()) 240 } 241 return 242 } 243 244 // Sign is a wrapper around the personal.sign RPC method that uses a non-echoing password 245 // prompt to acquire the passphrase and executes the original RPC method (saved in 246 // jeth.sign) with it to actually execute the RPC call. 247 func (b *bridge) Sign(call otto.FunctionCall) (response otto.Value) { 248 var ( 249 message = call.Argument(0) 250 account = call.Argument(1) 251 passwd = call.Argument(2) 252 ) 253 254 if !message.IsString() { 255 throwJSException("first argument must be the message to sign") 256 } 257 if !account.IsString() { 258 throwJSException("second argument must be the account to sign with") 259 } 260 261 // if the password is not given or null ask the user and ensure password is a string 262 if passwd.IsUndefined() || passwd.IsNull() { 263 fmt.Fprintf(b.printer, "Give password for account %s\n", account) 264 if input, err := b.prompter.PromptPassword("Passphrase: "); err != nil { 265 throwJSException(err.Error()) 266 } else { 267 passwd, _ = otto.ToValue(input) 268 } 269 } 270 if !passwd.IsString() { 271 throwJSException("third argument must be the password to unlock the account") 272 } 273 274 // Send the request to the backend and return 275 val, err := call.Otto.Call("jeth.sign", nil, message, account, passwd) 276 if err != nil { 277 throwJSException(err.Error()) 278 } 279 return val 280 } 281 282 // Sleep will block the console for the specified number of seconds. 283 func (b *bridge) Sleep(call otto.FunctionCall) (response otto.Value) { 284 if call.Argument(0).IsNumber() { 285 sleep, _ := call.Argument(0).ToInteger() 286 time.Sleep(time.Duration(sleep) * time.Second) 287 return otto.TrueValue() 288 } 289 return throwJSException("usage: sleep(<number of seconds>)") 290 } 291 292 // SleepBlocks will block the console for a specified number of new blocks optionally 293 // until the given timeout is reached. 294 func (b *bridge) SleepBlocks(call otto.FunctionCall) (response otto.Value) { 295 var ( 296 blocks = int64(0) 297 sleep = int64(9999999999999999) // indefinitely 298 ) 299 // Parse the input parameters for the sleep 300 nArgs := len(call.ArgumentList) 301 if nArgs == 0 { 302 throwJSException("usage: sleepBlocks(<n blocks>[, max sleep in seconds])") 303 } 304 if nArgs >= 1 { 305 if call.Argument(0).IsNumber() { 306 blocks, _ = call.Argument(0).ToInteger() 307 } else { 308 throwJSException("expected number as first argument") 309 } 310 } 311 if nArgs >= 2 { 312 if call.Argument(1).IsNumber() { 313 sleep, _ = call.Argument(1).ToInteger() 314 } else { 315 throwJSException("expected number as second argument") 316 } 317 } 318 // go through the console, this will allow web3 to call the appropriate 319 // callbacks if a delayed response or notification is received. 320 blockNumber := func() int64 { 321 result, err := call.Otto.Run("klay.blockNumber") 322 if err != nil { 323 throwJSException(err.Error()) 324 } 325 block, err := result.ToInteger() 326 if err != nil { 327 throwJSException(err.Error()) 328 } 329 return block 330 } 331 // Poll the current block number until either it ot a timeout is reached 332 targetBlockNr := blockNumber() + blocks 333 deadline := time.Now().Add(time.Duration(sleep) * time.Second) 334 335 for time.Now().Before(deadline) { 336 if blockNumber() >= targetBlockNr { 337 return otto.TrueValue() 338 } 339 time.Sleep(time.Second) 340 } 341 return otto.FalseValue() 342 } 343 344 type jsonrpcCall struct { 345 Id int64 346 Method string 347 Params []interface{} 348 } 349 350 // Send implements the web3 provider "send" method. 351 func (b *bridge) Send(call otto.FunctionCall) (response otto.Value) { 352 // Remarshal the request into a Go value. 353 JSON, _ := call.Otto.Object("JSON") 354 reqVal, err := JSON.Call("stringify", call.Argument(0)) 355 if err != nil { 356 throwJSException(err.Error()) 357 } 358 var ( 359 rawReq = reqVal.String() 360 dec = json.NewDecoder(strings.NewReader(rawReq)) 361 reqs []jsonrpcCall 362 batch bool 363 ) 364 dec.UseNumber() // avoid float64s 365 if rawReq[0] == '[' { 366 batch = true 367 dec.Decode(&reqs) 368 } else { 369 batch = false 370 reqs = make([]jsonrpcCall, 1) 371 dec.Decode(&reqs[0]) 372 } 373 374 // Execute the requests. 375 resps, _ := call.Otto.Object("new Array()") 376 for _, req := range reqs { 377 resp, _ := call.Otto.Object(`({"jsonrpc":"2.0"})`) 378 resp.Set("id", req.Id) 379 var result json.RawMessage 380 err = b.client.Call(&result, req.Method, req.Params...) 381 switch err := err.(type) { 382 case nil: 383 if result == nil { 384 // Special case null because it is decoded as an empty 385 // raw message for some reason. 386 resp.Set("result", otto.NullValue()) 387 } else { 388 resultVal, err := JSON.Call("parse", string(result)) 389 if err != nil { 390 setError(resp, -32603, err.Error()) 391 } else { 392 resp.Set("result", resultVal) 393 } 394 } 395 case rpc.Error: 396 setError(resp, err.ErrorCode(), err.Error()) 397 default: 398 setError(resp, -32603, err.Error()) 399 } 400 resps.Call("push", resp) 401 } 402 403 // Return the responses either to the callback (if supplied) 404 // or directly as the return value. 405 if batch { 406 response = resps.Value() 407 } else { 408 response, _ = resps.Get("0") 409 } 410 if fn := call.Argument(1); fn.Class() == "Function" { 411 fn.Call(otto.NullValue(), otto.NullValue(), response) 412 return otto.UndefinedValue() 413 } 414 return response 415 } 416 417 func setError(resp *otto.Object, code int, msg string) { 418 resp.Set("error", map[string]interface{}{"code": code, "message": msg}) 419 } 420 421 // throwJSException panics on an otto.Value. The Otto VM will recover from the 422 // Go panic and throw msg as a JavaScript error. 423 func throwJSException(msg interface{}) otto.Value { 424 val, err := otto.ToValue(msg) 425 if err != nil { 426 logger.Error("Failed to serialize JavaScript exception", "exception", msg, "err", err) 427 } 428 panic(val) 429 }