UNPKG

22.6 kBPlain TextView Raw
1"use strict";
2
3// See: https://github.com/ethereum/wiki/wiki/JSON-RPC
4
5import { Provider, TransactionRequest, TransactionResponse } from "@ethersproject/abstract-provider";
6import { Signer, TypedDataDomain, TypedDataField, TypedDataSigner } from "@ethersproject/abstract-signer";
7import { BigNumber } from "@ethersproject/bignumber";
8import { Bytes, hexlify, hexValue, isHexString } from "@ethersproject/bytes";
9import { _TypedDataEncoder } from "@ethersproject/hash";
10import { Network, Networkish } from "@ethersproject/networks";
11import { checkProperties, deepCopy, Deferrable, defineReadOnly, getStatic, resolveProperties, shallowCopy } from "@ethersproject/properties";
12import { toUtf8Bytes } from "@ethersproject/strings";
13import { AccessList, accessListify } from "@ethersproject/transactions";
14import { ConnectionInfo, fetchJson, poll } from "@ethersproject/web";
15
16import { Logger } from "@ethersproject/logger";
17import { version } from "./_version";
18const logger = new Logger(version);
19
20import { BaseProvider, Event } from "./base-provider";
21
22
23const errorGas = [ "call", "estimateGas" ];
24
25function checkError(method: string, error: any, params: any): any {
26 // Undo the "convenience" some nodes are attempting to prevent backwards
27 // incompatibility; maybe for v6 consider forwarding reverts as errors
28 if (method === "call" && error.code === Logger.errors.SERVER_ERROR) {
29 const e = error.error;
30 if (e && e.message.match("reverted") && isHexString(e.data)) {
31 return e.data;
32 }
33 }
34
35 let message = error.message;
36 if (error.code === Logger.errors.SERVER_ERROR && error.error && typeof(error.error.message) === "string") {
37 message = error.error.message;
38 } else if (typeof(error.body) === "string") {
39 message = error.body;
40 } else if (typeof(error.responseText) === "string") {
41 message = error.responseText;
42 }
43 message = (message || "").toLowerCase();
44
45 const transaction = params.transaction || params.signedTransaction;
46
47 // "insufficient funds for gas * price + value + cost(data)"
48 if (message.match(/insufficient funds/)) {
49 logger.throwError("insufficient funds for intrinsic transaction cost", Logger.errors.INSUFFICIENT_FUNDS, {
50 error, method, transaction
51 });
52 }
53
54 // "nonce too low"
55 if (message.match(/nonce too low/)) {
56 logger.throwError("nonce has already been used", Logger.errors.NONCE_EXPIRED, {
57 error, method, transaction
58 });
59 }
60
61 // "replacement transaction underpriced"
62 if (message.match(/replacement transaction underpriced/)) {
63 logger.throwError("replacement fee too low", Logger.errors.REPLACEMENT_UNDERPRICED, {
64 error, method, transaction
65 });
66 }
67
68 // "replacement transaction underpriced"
69 if (message.match(/only replay-protected/)) {
70 logger.throwError("legacy pre-eip-155 transactions not supported", Logger.errors.UNSUPPORTED_OPERATION, {
71 error, method, transaction
72 });
73 }
74
75 if (errorGas.indexOf(method) >= 0 && message.match(/gas required exceeds allowance|always failing transaction|execution reverted/)) {
76 logger.throwError("cannot estimate gas; transaction may fail or may require manual gas limit", Logger.errors.UNPREDICTABLE_GAS_LIMIT, {
77 error, method, transaction
78 });
79 }
80
81 throw error;
82}
83
84function timer(timeout: number): Promise<any> {
85 return new Promise(function(resolve) {
86 setTimeout(resolve, timeout);
87 });
88}
89
90function getResult(payload: { error?: { code?: number, data?: any, message?: string }, result?: any }): any {
91 if (payload.error) {
92 // @TODO: not any
93 const error: any = new Error(payload.error.message);
94 error.code = payload.error.code;
95 error.data = payload.error.data;
96 throw error;
97 }
98
99 return payload.result;
100}
101
102function getLowerCase(value: string): string {
103 if (value) { return value.toLowerCase(); }
104 return value;
105}
106
107const _constructorGuard = {};
108
109export class JsonRpcSigner extends Signer implements TypedDataSigner {
110 readonly provider: JsonRpcProvider;
111 _index: number;
112 _address: string;
113
114 constructor(constructorGuard: any, provider: JsonRpcProvider, addressOrIndex?: string | number) {
115 logger.checkNew(new.target, JsonRpcSigner);
116
117 super();
118
119 if (constructorGuard !== _constructorGuard) {
120 throw new Error("do not call the JsonRpcSigner constructor directly; use provider.getSigner");
121 }
122
123 defineReadOnly(this, "provider", provider);
124
125 if (addressOrIndex == null) { addressOrIndex = 0; }
126
127 if (typeof(addressOrIndex) === "string") {
128 defineReadOnly(this, "_address", this.provider.formatter.address(addressOrIndex));
129 defineReadOnly(this, "_index", null);
130
131 } else if (typeof(addressOrIndex) === "number") {
132 defineReadOnly(this, "_index", addressOrIndex);
133 defineReadOnly(this, "_address", null);
134
135 } else {
136 logger.throwArgumentError("invalid address or index", "addressOrIndex", addressOrIndex);
137 }
138 }
139
140 connect(provider: Provider): JsonRpcSigner {
141 return logger.throwError("cannot alter JSON-RPC Signer connection", Logger.errors.UNSUPPORTED_OPERATION, {
142 operation: "connect"
143 });
144 }
145
146 connectUnchecked(): JsonRpcSigner {
147 return new UncheckedJsonRpcSigner(_constructorGuard, this.provider, this._address || this._index);
148 }
149
150 getAddress(): Promise<string> {
151 if (this._address) {
152 return Promise.resolve(this._address);
153 }
154
155 return this.provider.send("eth_accounts", []).then((accounts) => {
156 if (accounts.length <= this._index) {
157 logger.throwError("unknown account #" + this._index, Logger.errors.UNSUPPORTED_OPERATION, {
158 operation: "getAddress"
159 });
160 }
161 return this.provider.formatter.address(accounts[this._index])
162 });
163 }
164
165 sendUncheckedTransaction(transaction: Deferrable<TransactionRequest>): Promise<string> {
166 transaction = shallowCopy(transaction);
167
168 const fromAddress = this.getAddress().then((address) => {
169 if (address) { address = address.toLowerCase(); }
170 return address;
171 });
172
173 // The JSON-RPC for eth_sendTransaction uses 90000 gas; if the user
174 // wishes to use this, it is easy to specify explicitly, otherwise
175 // we look it up for them.
176 if (transaction.gasLimit == null) {
177 const estimate = shallowCopy(transaction);
178 estimate.from = fromAddress;
179 transaction.gasLimit = this.provider.estimateGas(estimate);
180 }
181
182 return resolveProperties({
183 tx: resolveProperties(transaction),
184 sender: fromAddress
185 }).then(({ tx, sender }) => {
186 if (tx.from != null) {
187 if (tx.from.toLowerCase() !== sender) {
188 logger.throwArgumentError("from address mismatch", "transaction", transaction);
189 }
190 } else {
191 tx.from = sender;
192 }
193
194 const hexTx = (<any>this.provider.constructor).hexlifyTransaction(tx, { from: true });
195
196 return this.provider.send("eth_sendTransaction", [ hexTx ]).then((hash) => {
197 return hash;
198 }, (error) => {
199 return checkError("sendTransaction", error, hexTx);
200 });
201 });
202 }
203
204 signTransaction(transaction: Deferrable<TransactionRequest>): Promise<string> {
205 return logger.throwError("signing transactions is unsupported", Logger.errors.UNSUPPORTED_OPERATION, {
206 operation: "signTransaction"
207 });
208 }
209
210 sendTransaction(transaction: Deferrable<TransactionRequest>): Promise<TransactionResponse> {
211 return this.sendUncheckedTransaction(transaction).then((hash) => {
212 return poll(() => {
213 return this.provider.getTransaction(hash).then((tx: TransactionResponse) => {
214 if (tx === null) { return undefined; }
215 return this.provider._wrapTransaction(tx, hash);
216 });
217 }, { oncePoll: this.provider }).catch((error: Error) => {
218 (<any>error).transactionHash = hash;
219 throw error;
220 });
221 });
222 }
223
224 async signMessage(message: Bytes | string): Promise<string> {
225 const data = ((typeof(message) === "string") ? toUtf8Bytes(message): message);
226 const address = await this.getAddress();
227
228 // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign
229 return await this.provider.send("eth_sign", [ address.toLowerCase(), hexlify(data) ]);
230 }
231
232 async _signTypedData(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, value: Record<string, any>): Promise<string> {
233 // Populate any ENS names (in-place)
234 const populated = await _TypedDataEncoder.resolveNames(domain, types, value, (name: string) => {
235 return this.provider.resolveName(name);
236 });
237
238 const address = await this.getAddress();
239
240 return await this.provider.send("eth_signTypedData_v4", [
241 address.toLowerCase(),
242 JSON.stringify(_TypedDataEncoder.getPayload(populated.domain, types, populated.value))
243 ]);
244 }
245
246 async unlock(password: string): Promise<boolean> {
247 const provider = this.provider;
248
249 const address = await this.getAddress();
250
251 return provider.send("personal_unlockAccount", [ address.toLowerCase(), password, null ]);
252 }
253}
254
255class UncheckedJsonRpcSigner extends JsonRpcSigner {
256 sendTransaction(transaction: Deferrable<TransactionRequest>): Promise<TransactionResponse> {
257 return this.sendUncheckedTransaction(transaction).then((hash) => {
258 return <TransactionResponse>{
259 hash: hash,
260 nonce: null,
261 gasLimit: null,
262 gasPrice: null,
263 data: null,
264 value: null,
265 chainId: null,
266 confirmations: 0,
267 from: null,
268 wait: (confirmations?: number) => { return this.provider.waitForTransaction(hash, confirmations); }
269 };
270 });
271 }
272}
273
274const allowedTransactionKeys: { [ key: string ]: boolean } = {
275 chainId: true, data: true, gasLimit: true, gasPrice:true, nonce: true, to: true, value: true,
276 type: true, accessList: true
277}
278
279export class JsonRpcProvider extends BaseProvider {
280 readonly connection: ConnectionInfo;
281
282 _pendingFilter: Promise<number>;
283 _nextId: number;
284
285 // During any given event loop, the results for a given call will
286 // all be the same, so we can dedup the calls to save requests and
287 // bandwidth. @TODO: Try out generalizing this against send?
288 _eventLoopCache: Record<string, Promise<any>>;
289 get _cache(): Record<string, Promise<any>> {
290 if (this._eventLoopCache == null) {
291 this._eventLoopCache = { };
292 }
293 return this._eventLoopCache;
294 }
295
296 constructor(url?: ConnectionInfo | string, network?: Networkish) {
297 logger.checkNew(new.target, JsonRpcProvider);
298
299 let networkOrReady: Networkish | Promise<Network> = network;
300
301 // The network is unknown, query the JSON-RPC for it
302 if (networkOrReady == null) {
303 networkOrReady = new Promise((resolve, reject) => {
304 setTimeout(() => {
305 this.detectNetwork().then((network) => {
306 resolve(network);
307 }, (error) => {
308 reject(error);
309 });
310 }, 0);
311 });
312 }
313
314 super(networkOrReady);
315
316 // Default URL
317 if (!url) { url = getStatic<() => string>(this.constructor, "defaultUrl")(); }
318
319 if (typeof(url) === "string") {
320 defineReadOnly(this, "connection",Object.freeze({
321 url: url
322 }));
323 } else {
324 defineReadOnly(this, "connection", Object.freeze(shallowCopy(url)));
325 }
326
327 this._nextId = 42;
328 }
329
330 static defaultUrl(): string {
331 return "http:/\/localhost:8545";
332 }
333
334 detectNetwork(): Promise<Network> {
335 if (!this._cache["detectNetwork"]) {
336 this._cache["detectNetwork"] = this._uncachedDetectNetwork();
337
338 // Clear this cache at the beginning of the next event loop
339 setTimeout(() => {
340 this._cache["detectNetwork"] = null;
341 }, 0);
342 }
343 return this._cache["detectNetwork"];
344 }
345
346 async _uncachedDetectNetwork(): Promise<Network> {
347 await timer(0);
348
349 let chainId = null;
350 try {
351 chainId = await this.send("eth_chainId", [ ]);
352 } catch (error) {
353 try {
354 chainId = await this.send("net_version", [ ]);
355 } catch (error) { }
356 }
357
358 if (chainId != null) {
359 const getNetwork = getStatic<(network: Networkish) => Network>(this.constructor, "getNetwork");
360 try {
361 return getNetwork(BigNumber.from(chainId).toNumber());
362 } catch (error) {
363 return logger.throwError("could not detect network", Logger.errors.NETWORK_ERROR, {
364 chainId: chainId,
365 event: "invalidNetwork",
366 serverError: error
367 });
368 }
369 }
370
371 return logger.throwError("could not detect network", Logger.errors.NETWORK_ERROR, {
372 event: "noNetwork"
373 });
374 }
375
376 getSigner(addressOrIndex?: string | number): JsonRpcSigner {
377 return new JsonRpcSigner(_constructorGuard, this, addressOrIndex);
378 }
379
380 getUncheckedSigner(addressOrIndex?: string | number): UncheckedJsonRpcSigner {
381 return this.getSigner(addressOrIndex).connectUnchecked();
382 }
383
384 listAccounts(): Promise<Array<string>> {
385 return this.send("eth_accounts", []).then((accounts: Array<string>) => {
386 return accounts.map((a) => this.formatter.address(a));
387 });
388 }
389
390 send(method: string, params: Array<any>): Promise<any> {
391 const request = {
392 method: method,
393 params: params,
394 id: (this._nextId++),
395 jsonrpc: "2.0"
396 };
397
398 this.emit("debug", {
399 action: "request",
400 request: deepCopy(request),
401 provider: this
402 });
403
404 // We can expand this in the future to any call, but for now these
405 // are the biggest wins and do not require any serializing parameters.
406 const cache = ([ "eth_chainId", "eth_blockNumber" ].indexOf(method) >= 0);
407 if (cache && this._cache[method]) {
408 return this._cache[method];
409 }
410
411 const result = fetchJson(this.connection, JSON.stringify(request), getResult).then((result) => {
412 this.emit("debug", {
413 action: "response",
414 request: request,
415 response: result,
416 provider: this
417 });
418
419 return result;
420
421 }, (error) => {
422 this.emit("debug", {
423 action: "response",
424 error: error,
425 request: request,
426 provider: this
427 });
428
429 throw error;
430 });
431
432 // Cache the fetch, but clear it on the next event loop
433 if (cache) {
434 this._cache[method] = result;
435 setTimeout(() => {
436 this._cache[method] = null;
437 }, 0);
438 }
439
440 return result;
441 }
442
443 prepareRequest(method: string, params: any): [ string, Array<any> ] {
444 switch (method) {
445 case "getBlockNumber":
446 return [ "eth_blockNumber", [] ];
447
448 case "getGasPrice":
449 return [ "eth_gasPrice", [] ];
450
451 case "getBalance":
452 return [ "eth_getBalance", [ getLowerCase(params.address), params.blockTag ] ];
453
454 case "getTransactionCount":
455 return [ "eth_getTransactionCount", [ getLowerCase(params.address), params.blockTag ] ];
456
457 case "getCode":
458 return [ "eth_getCode", [ getLowerCase(params.address), params.blockTag ] ];
459
460 case "getStorageAt":
461 return [ "eth_getStorageAt", [ getLowerCase(params.address), params.position, params.blockTag ] ];
462
463 case "sendTransaction":
464 return [ "eth_sendRawTransaction", [ params.signedTransaction ] ]
465
466 case "getBlock":
467 if (params.blockTag) {
468 return [ "eth_getBlockByNumber", [ params.blockTag, !!params.includeTransactions ] ];
469 } else if (params.blockHash) {
470 return [ "eth_getBlockByHash", [ params.blockHash, !!params.includeTransactions ] ];
471 }
472 return null;
473
474 case "getTransaction":
475 return [ "eth_getTransactionByHash", [ params.transactionHash ] ];
476
477 case "getTransactionReceipt":
478 return [ "eth_getTransactionReceipt", [ params.transactionHash ] ];
479
480 case "call": {
481 const hexlifyTransaction = getStatic<(t: TransactionRequest, a?: { [key: string]: boolean }) => { [key: string]: string }>(this.constructor, "hexlifyTransaction");
482 return [ "eth_call", [ hexlifyTransaction(params.transaction, { from: true }), params.blockTag ] ];
483 }
484
485 case "estimateGas": {
486 const hexlifyTransaction = getStatic<(t: TransactionRequest, a?: { [key: string]: boolean }) => { [key: string]: string }>(this.constructor, "hexlifyTransaction");
487 return [ "eth_estimateGas", [ hexlifyTransaction(params.transaction, { from: true }) ] ];
488 }
489
490 case "getLogs":
491 if (params.filter && params.filter.address != null) {
492 params.filter.address = getLowerCase(params.filter.address);
493 }
494 return [ "eth_getLogs", [ params.filter ] ];
495
496 default:
497 break;
498 }
499
500 return null;
501 }
502
503 async perform(method: string, params: any): Promise<any> {
504 const args = this.prepareRequest(method, params);
505
506 if (args == null) {
507 logger.throwError(method + " not implemented", Logger.errors.NOT_IMPLEMENTED, { operation: method });
508 }
509 try {
510 return await this.send(args[0], args[1])
511 } catch (error) {
512 return checkError(method, error, params);
513 }
514 }
515
516 _startEvent(event: Event): void {
517 if (event.tag === "pending") { this._startPending(); }
518 super._startEvent(event);
519 }
520
521 _startPending(): void {
522 if (this._pendingFilter != null) { return; }
523 const self = this;
524
525 const pendingFilter: Promise<number> = this.send("eth_newPendingTransactionFilter", []);
526 this._pendingFilter = pendingFilter;
527
528 pendingFilter.then(function(filterId) {
529 function poll() {
530 self.send("eth_getFilterChanges", [ filterId ]).then(function(hashes: Array<string>) {
531 if (self._pendingFilter != pendingFilter) { return null; }
532
533 let seq = Promise.resolve();
534 hashes.forEach(function(hash) {
535 // @TODO: This should be garbage collected at some point... How? When?
536 self._emitted["t:" + hash.toLowerCase()] = "pending";
537 seq = seq.then(function() {
538 return self.getTransaction(hash).then(function(tx) {
539 self.emit("pending", tx);
540 return null;
541 });
542 });
543 });
544
545 return seq.then(function() {
546 return timer(1000);
547 });
548 }).then(function() {
549 if (self._pendingFilter != pendingFilter) {
550 self.send("eth_uninstallFilter", [ filterId ]);
551 return;
552 }
553 setTimeout(function() { poll(); }, 0);
554
555 return null;
556 }).catch((error: Error) => { });
557 }
558 poll();
559
560 return filterId;
561 }).catch((error: Error) => { });
562 }
563
564 _stopEvent(event: Event): void {
565 if (event.tag === "pending" && this.listenerCount("pending") === 0) {
566 this._pendingFilter = null;
567 }
568 super._stopEvent(event);
569 }
570
571
572 // Convert an ethers.js transaction into a JSON-RPC transaction
573 // - gasLimit => gas
574 // - All values hexlified
575 // - All numeric values zero-striped
576 // - All addresses are lowercased
577 // NOTE: This allows a TransactionRequest, but all values should be resolved
578 // before this is called
579 // @TODO: This will likely be removed in future versions and prepareRequest
580 // will be the preferred method for this.
581 static hexlifyTransaction(transaction: TransactionRequest, allowExtra?: { [key: string]: boolean }): { [key: string]: string | AccessList } {
582 // Check only allowed properties are given
583 const allowed = shallowCopy(allowedTransactionKeys);
584 if (allowExtra) {
585 for (const key in allowExtra) {
586 if (allowExtra[key]) { allowed[key] = true; }
587 }
588 }
589
590 checkProperties(transaction, allowed);
591
592 const result: { [key: string]: string | AccessList } = {};
593
594 // Some nodes (INFURA ropsten; INFURA mainnet is fine) do not like leading zeros.
595 ["gasLimit", "gasPrice", "type", "nonce", "value"].forEach(function(key) {
596 if ((<any>transaction)[key] == null) { return; }
597 const value = hexValue((<any>transaction)[key]);
598 if (key === "gasLimit") { key = "gas"; }
599 result[key] = value;
600 });
601
602 ["from", "to", "data"].forEach(function(key) {
603 if ((<any>transaction)[key] == null) { return; }
604 result[key] = hexlify((<any>transaction)[key]);
605 });
606
607 if ((<any>transaction).accessList) {
608 result["accessList"] = accessListify((<any>transaction).accessList);
609 }
610
611 return result;
612 }
613}