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