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