github.com/klaytn/klaytn@v1.12.1/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 "errors" 26 "fmt" 27 "io" 28 "reflect" 29 "strings" 30 "time" 31 32 "github.com/dop251/goja" 33 "github.com/klaytn/klaytn/common/hexutil" 34 "github.com/klaytn/klaytn/console/jsre" 35 "github.com/klaytn/klaytn/networks/rpc" 36 ) 37 38 // bridge is a collection of JavaScript utility methods to bride the .js runtime 39 // environment and the Go RPC connection backing the remote method calls. 40 type bridge struct { 41 client *rpc.Client // RPC client to execute Klaytn requests through 42 prompter UserPrompter // Input prompter to allow interactive user feedback 43 printer io.Writer // Output writer to serialize any display strings to 44 } 45 46 func getJeth(vm *goja.Runtime) *goja.Object { 47 jeth := vm.Get("jeth") 48 if jeth == nil { 49 panic(vm.ToValue("jeth object does not exist")) 50 } 51 return jeth.ToObject(vm) 52 } 53 54 // newBridge creates a new JavaScript wrapper around an RPC client. 55 func newBridge(client *rpc.Client, prompter UserPrompter, printer io.Writer) *bridge { 56 return &bridge{ 57 client: client, 58 prompter: prompter, 59 printer: printer, 60 } 61 } 62 63 // NewAccount is a wrapper around the personal.newAccount RPC method that uses a 64 // non-echoing password prompt to acquire the passphrase and executes the original 65 // RPC method (saved in jeth.newAccount) with it to actually execute the RPC call. 66 func (b *bridge) NewAccount(call jsre.Call) (goja.Value, error) { 67 var ( 68 password string 69 confirm string 70 err error 71 ) 72 switch { 73 // No password was specified, prompt the user for it 74 case len(call.Arguments) == 0: 75 if password, err = b.prompter.PromptPassword("Passphrase: "); err != nil { 76 return nil, err 77 } 78 if confirm, err = b.prompter.PromptPassword("Repeat passphrase: "); err != nil { 79 return nil, err 80 } 81 if password != confirm { 82 return nil, errors.New("passwords don't match!") 83 } 84 // A single string password was specified, use that 85 case len(call.Arguments) == 1 && call.Argument(0).ToString() != nil: 86 password = call.Argument(0).ToString().String() 87 default: 88 return nil, errors.New("expected 0 or 1 string argument") 89 } 90 // Password acquired, execute the call and return 91 newAccount, callable := goja.AssertFunction(getJeth(call.VM).Get("newAccount")) 92 if !callable { 93 return nil, errors.New("jeth.newAccount is not callable") 94 } 95 ret, err := newAccount(goja.Null(), call.VM.ToValue(password)) 96 if err != nil { 97 return nil, err 98 } 99 return ret, nil 100 } 101 102 // OpenWallet is a wrapper around personal.openWallet which can interpret and 103 // react to certain error messages, such as the Trezor PIN matrix request. 104 func (b *bridge) OpenWallet(call jsre.Call) (goja.Value, error) { 105 // Make sure we have a wallet specified to open 106 if call.Argument(0).ToObject(call.VM).ClassName() != "String" { 107 return nil, errors.New("first argument must be the wallet URL to open") 108 } 109 wallet := call.Argument(0) 110 111 var passwd goja.Value 112 if goja.IsUndefined(call.Argument(1)) || goja.IsNull(call.Argument(1)) { 113 passwd = call.VM.ToValue("") 114 } else { 115 passwd = call.Argument(1) 116 } 117 // Open the wallet and return if successful in itself 118 openWallet, callable := goja.AssertFunction(getJeth(call.VM).Get("openWallet")) 119 if !callable { 120 return nil, errors.New("jeth.openWallet is not callable") 121 } 122 val, err := openWallet(goja.Null(), wallet, passwd) 123 if err == nil { 124 return val, nil 125 } 126 127 // Trezor PIN matrix input requested, display the matrix to the user and fetch the data 128 fmt.Fprintf(b.printer, "Look at the device for number positions\n\n") 129 fmt.Fprintf(b.printer, "7 | 8 | 9\n") 130 fmt.Fprintf(b.printer, "--+---+--\n") 131 fmt.Fprintf(b.printer, "4 | 5 | 6\n") 132 fmt.Fprintf(b.printer, "--+---+--\n") 133 fmt.Fprintf(b.printer, "1 | 2 | 3\n\n") 134 135 input, err := b.prompter.PromptPassword("Please enter current PIN: ") 136 if err != nil { 137 return nil, err 138 } 139 openWallet, callable = goja.AssertFunction(getJeth(call.VM).Get("openWallet")) 140 if !callable { 141 return nil, errors.New("jeth.openWallet is not callable") 142 } 143 return openWallet(goja.Null(), wallet, call.VM.ToValue(input)) 144 } 145 146 // UnlockAccount is a wrapper around the personal.unlockAccount RPC method that 147 // uses a non-echoing password prompt to acquire the passphrase and executes the 148 // original RPC method (saved in jeth.unlockAccount) with it to actually execute 149 // the RPC call. 150 func (b *bridge) UnlockAccount(call jsre.Call) (goja.Value, error) { 151 if len(call.Arguments) < 1 { 152 return nil, errors.New("usage: unlockAccount(account, [ password, duration ])") 153 } 154 155 account := call.Argument(0) 156 // Make sure we have an account specified to unlock. 157 if goja.IsUndefined(account) || goja.IsNull(account) || account.ExportType().Kind() != reflect.String { 158 return nil, errors.New("first argument must be the account to unlock") 159 } 160 161 // If password is not given or is the null value, prompt the user for it. 162 var passwd goja.Value 163 if goja.IsUndefined(call.Argument(1)) || goja.IsNull(call.Argument(1)) { 164 fmt.Fprintf(b.printer, "Unlock account %s\n", account) 165 input, err := b.prompter.PromptPassword("Passphrase: ") 166 if err != nil { 167 return nil, err 168 } 169 passwd = call.VM.ToValue(input) 170 } else { 171 if call.Argument(1).ExportType().Kind() != reflect.String { 172 return nil, errors.New("password must be a string") 173 } 174 passwd = call.Argument(1) 175 } 176 177 // Third argument is the duration how long the account should be unlocked. 178 duration := goja.Null() 179 if !goja.IsUndefined(call.Argument(2)) && !goja.IsNull(call.Argument(2)) { 180 if !isNumber(call.Argument(2)) { 181 return nil, errors.New("unlock duration must be a number") 182 } 183 duration = call.Argument(2) 184 } 185 186 // Send the request to the backend and return. 187 unlockAccount, callable := goja.AssertFunction(getJeth(call.VM).Get("unlockAccount")) 188 if !callable { 189 return nil, errors.New("jeth.unlockAccount is not callable") 190 } 191 return unlockAccount(goja.Null(), account, passwd, duration) 192 } 193 194 // passwdByPrompt returns the password from a non-echoing password prompt. 195 func (b *bridge) passwdByPrompt(call jsre.Call, msg string) (goja.Value, error) { 196 fmt.Fprintln(b.printer, msg) 197 input, err := b.prompter.PromptPassword("Passphrase: ") 198 if err != nil { 199 return nil, err 200 } 201 return call.VM.ToValue(input), nil 202 } 203 204 // passwdByPromptWithDuration returns the password and the duration from a non-echoing password prompt. 205 func (b *bridge) passwdByPromptWithDuration(call jsre.Call, msg string) (goja.Value, goja.Value, error) { 206 var ( 207 passwd goja.Value 208 duration = goja.Null() 209 err error 210 ) 211 212 switch len(call.Arguments) { 213 case 0: 214 passwd, err = b.passwdByPrompt(call, msg) 215 if err != nil { 216 return nil, nil, err 217 } 218 case 1: 219 if call.Argument(0).ExportType().Kind() != reflect.String { 220 return nil, nil, err 221 } 222 passwd = call.Argument(0) 223 case 2: 224 if call.Argument(0).ExportType().Kind() == reflect.String && isNumber(call.Argument(1)) { 225 passwd = call.Argument(0) 226 duration = call.Argument(1) 227 } else { 228 return nil, nil, err 229 } 230 default: 231 return nil, nil, errors.New("invalid arguments") 232 } 233 return passwd, duration, nil 234 } 235 236 // UnlockParentOperator is a wrapper around the subbridge.unlockParentOperator RPC method that 237 // uses a non-echoing password prompt to acquire the passphrase and executes the 238 // original RPC method (saved in jeth.unlockParentOperator) with it to actually execute 239 // the RPC call. 240 func (b *bridge) UnlockParentOperator(call jsre.Call) (goja.Value, error) { 241 passwd, duration, err := b.passwdByPromptWithDuration(call, "Unlock parent operator account") 242 if err != nil { 243 return nil, err 244 } 245 246 // Send the request to the backend and return 247 response, callable := goja.AssertFunction(getJeth(call.VM).Get("unlockParentOperator")) 248 if !callable { 249 return nil, errors.New("jeth.unlockParentOperator is not callable") 250 } 251 return response(goja.Null(), passwd, duration) 252 } 253 254 // UnlockChildOperator is a wrapper around the subbridge.unlockChildOperator RPC method that 255 // uses a non-echoing password prompt to acquire the passphrase and executes the 256 // original RPC method (saved in jeth.unlockChildOperator) with it to actually execute 257 // the RPC call. 258 func (b *bridge) UnlockChildOperator(call jsre.Call) (goja.Value, error) { 259 passwd, duration, err := b.passwdByPromptWithDuration(call, "Unlock child operator account") 260 if err != nil { 261 return nil, err 262 } 263 264 // Send the request to the backend and return 265 response, callable := goja.AssertFunction(getJeth(call.VM).Get("unlockChildOperator")) 266 if !callable { 267 return nil, errors.New("jeth.unlockChildOperator is not callable") 268 } 269 return response(goja.Null(), passwd, duration) 270 } 271 272 // Sign is a wrapper around the personal.sign RPC method that uses a non-echoing password 273 // prompt to acquire the passphrase and executes the original RPC method (saved in 274 // jeth.sign) with it to actually execute the RPC call. 275 func (b *bridge) Sign(call jsre.Call) (goja.Value, error) { 276 if nArgs := len(call.Arguments); nArgs < 2 { 277 return nil, errors.New("usage: sign(message, account, [ password ])") 278 } 279 var ( 280 message = call.Argument(0) 281 account = call.Argument(1) 282 passwd = call.Argument(2) 283 ) 284 285 if goja.IsUndefined(message) || message.ExportType().Kind() != reflect.String { 286 return nil, errors.New("first argument must be the message to sign") 287 } 288 if goja.IsUndefined(account) || account.ExportType().Kind() != reflect.String { 289 return nil, errors.New("second argument must be the account to sign with") 290 } 291 292 // if the password is not given or null ask the user and ensure password is a string 293 if goja.IsUndefined(passwd) || goja.IsNull(passwd) { 294 fmt.Fprintf(b.printer, "Give password for account %s\n", account) 295 input, err := b.prompter.PromptPassword("Password: ") 296 if err != nil { 297 return nil, err 298 } 299 passwd = call.VM.ToValue(input) 300 } else if passwd.ExportType().Kind() != reflect.String { 301 return nil, errors.New("third argument must be the password to unlock the account") 302 } 303 304 // Send the request to the backend and return 305 sign, callable := goja.AssertFunction(getJeth(call.VM).Get("sign")) 306 if !callable { 307 return nil, errors.New("jeth.sign is not callable") 308 } 309 return sign(goja.Null(), message, account, passwd) 310 } 311 312 // Sleep will block the console for the specified number of seconds. 313 func (b *bridge) Sleep(call jsre.Call) (goja.Value, error) { 314 if nArgs := len(call.Arguments); nArgs < 1 { 315 return nil, errors.New("usage: sleep(<number of seconds>)") 316 } 317 sleepObj := call.Argument(0) 318 if goja.IsUndefined(sleepObj) || goja.IsNull(sleepObj) || !isNumber(sleepObj) { 319 return nil, errors.New("usage: sleep(<number of seconds>)") 320 } 321 sleep := sleepObj.ToFloat() 322 time.Sleep(time.Duration(sleep * float64(time.Second))) 323 return call.VM.ToValue(true), nil 324 } 325 326 // SleepBlocks will block the console for a specified number of new blocks optionally 327 // until the given timeout is reached. 328 func (b *bridge) SleepBlocks(call jsre.Call) (goja.Value, error) { 329 // Parse the input parameters for the sleep. 330 var ( 331 blocks = int64(0) 332 sleep = int64(9999999999999999) // indefinitely 333 ) 334 nArgs := len(call.Arguments) 335 if nArgs == 0 { 336 return nil, errors.New("usage: sleepBlocks(<n blocks>[, max sleep in seconds])") 337 } 338 if nArgs >= 1 { 339 if goja.IsNull(call.Argument(0)) || goja.IsUndefined(call.Argument(0)) || !isNumber(call.Argument(0)) { 340 return nil, errors.New("expected number as first argument") 341 } 342 blocks = call.Argument(0).ToInteger() 343 } 344 if nArgs >= 2 { 345 if goja.IsNull(call.Argument(1)) || goja.IsUndefined(call.Argument(1)) || !isNumber(call.Argument(1)) { 346 return nil, errors.New("expected number as second argument") 347 } 348 sleep = call.Argument(1).ToInteger() 349 } 350 351 // Poll the current block number until either it or a timeout is reached. 352 deadline := time.Now().Add(time.Duration(sleep) * time.Second) 353 var lastNumber hexutil.Uint64 354 if err := b.client.Call(&lastNumber, "klay_blockNumber"); err != nil { 355 return nil, err 356 } 357 for time.Now().Before(deadline) { 358 var number hexutil.Uint64 359 if err := b.client.Call(&number, "klay_blockNumber"); err != nil { 360 return nil, err 361 } 362 if number != lastNumber { 363 lastNumber = number 364 blocks-- 365 } 366 if blocks <= 0 { 367 break 368 } 369 time.Sleep(time.Second) 370 } 371 return call.VM.ToValue(true), nil 372 } 373 374 type jsonrpcCall struct { 375 ID int64 376 Method string 377 Params []interface{} 378 } 379 380 // Send implements the web3 provider "send" method. 381 func (b *bridge) Send(call jsre.Call) (goja.Value, error) { 382 // Remarshal the request into a Go value. 383 reqVal, err := call.Argument(0).ToObject(call.VM).MarshalJSON() 384 if err != nil { 385 return nil, err 386 } 387 388 var ( 389 rawReq = string(reqVal) 390 dec = json.NewDecoder(strings.NewReader(rawReq)) 391 reqs []jsonrpcCall 392 batch bool 393 resps []*goja.Object 394 ) 395 dec.UseNumber() // avoid float64s 396 if rawReq[0] == '[' { 397 batch = true 398 dec.Decode(&reqs) 399 } else { 400 batch = false 401 reqs = make([]jsonrpcCall, 1) 402 dec.Decode(&reqs[0]) 403 } 404 405 // Execute the requests. 406 for _, req := range reqs { 407 resp := call.VM.NewObject() 408 resp.Set("jsonrpc", "2.0") 409 resp.Set("id", req.ID) 410 411 var result json.RawMessage 412 if err = b.client.Call(&result, req.Method, req.Params...); err == nil { 413 if result == nil { 414 // Special case null because it is decoded as an empty 415 // raw message for some reason. 416 resp.Set("result", goja.Null()) 417 } else { 418 JSON := call.VM.Get("JSON").ToObject(call.VM) 419 parse, callable := goja.AssertFunction(JSON.Get("parse")) 420 if !callable { 421 return nil, errors.New("JSON.parse is not a function") 422 } 423 resultVal, err := parse(goja.Null(), call.VM.ToValue(string(result))) 424 if err != nil { 425 setError(resp, -32603, err.Error(), nil) 426 } else { 427 resp.Set("result", resultVal) 428 } 429 } 430 } else { 431 code := -32603 432 var data interface{} 433 if err, ok := err.(rpc.Error); ok { 434 code = err.ErrorCode() 435 } 436 if err, ok := err.(rpc.DataError); ok { 437 data = err.ErrorData() 438 } 439 setError(resp, code, err.Error(), data) 440 } 441 resps = append(resps, resp) 442 } 443 // Return the responses either to the callback (if supplied) 444 // or directly as the return value. 445 var result goja.Value 446 if batch { 447 result = call.VM.ToValue(resps) 448 } else { 449 result = resps[0] 450 } 451 if fn, isFunc := goja.AssertFunction(call.Argument(1)); isFunc { 452 fn(goja.Null(), goja.Null(), result) 453 return goja.Undefined(), nil 454 } 455 return result, nil 456 } 457 458 func setError(resp *goja.Object, code int, msg string, data interface{}) { 459 err := make(map[string]interface{}) 460 err["code"] = code 461 err["message"] = msg 462 if data != nil { 463 err["data"] = data 464 } 465 resp.Set("error", err) 466 } 467 468 // isNumber returns true if input value is a JS number. 469 func isNumber(v goja.Value) bool { 470 k := v.ExportType().Kind() 471 return k >= reflect.Int && k <= reflect.Float64 472 } 473 474 func getObject(vm *goja.Runtime, name string) *goja.Object { 475 v := vm.Get(name) 476 if v == nil { 477 return nil 478 } 479 return v.ToObject(vm) 480 }