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