1 | "use strict";
|
2 |
|
3 |
|
4 |
|
5 | import { Provider, TransactionRequest, TransactionResponse } from "@ethersproject/abstract-provider";
|
6 | import { Signer, TypedDataDomain, TypedDataField, TypedDataSigner } from "@ethersproject/abstract-signer";
|
7 | import { BigNumber } from "@ethersproject/bignumber";
|
8 | import { Bytes, hexlify, hexValue, isHexString } from "@ethersproject/bytes";
|
9 | import { _TypedDataEncoder } from "@ethersproject/hash";
|
10 | import { Network, Networkish } from "@ethersproject/networks";
|
11 | import { checkProperties, deepCopy, Deferrable, defineReadOnly, getStatic, resolveProperties, shallowCopy } from "@ethersproject/properties";
|
12 | import { toUtf8Bytes } from "@ethersproject/strings";
|
13 | import { AccessList, accessListify } from "@ethersproject/transactions";
|
14 | import { ConnectionInfo, fetchJson, poll } from "@ethersproject/web";
|
15 |
|
16 | import { Logger } from "@ethersproject/logger";
|
17 | import { version } from "./_version";
|
18 | const logger = new Logger(version);
|
19 |
|
20 | import { BaseProvider, Event } from "./base-provider";
|
21 |
|
22 |
|
23 | const errorGas = [ "call", "estimateGas" ];
|
24 |
|
25 | function checkError(method: string, error: any, params: any): any {
|
26 |
|
27 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
84 | function timer(timeout: number): Promise<any> {
|
85 | return new Promise(function(resolve) {
|
86 | setTimeout(resolve, timeout);
|
87 | });
|
88 | }
|
89 |
|
90 | function getResult(payload: { error?: { code?: number, data?: any, message?: string }, result?: any }): any {
|
91 | if (payload.error) {
|
92 |
|
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 |
|
102 | function getLowerCase(value: string): string {
|
103 | if (value) { return value.toLowerCase(); }
|
104 | return value;
|
105 | }
|
106 |
|
107 | const _constructorGuard = {};
|
108 |
|
109 | export 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 |
|
174 |
|
175 |
|
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 |
|
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 |
|
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 |
|
255 | class 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 |
|
274 | const 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 |
|
279 | export class JsonRpcProvider extends BaseProvider {
|
280 | readonly connection: ConnectionInfo;
|
281 |
|
282 | _pendingFilter: Promise<number>;
|
283 | _nextId: number;
|
284 |
|
285 |
|
286 |
|
287 |
|
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 |
|
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 |
|
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 |
|
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 |
|
405 |
|
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 |
|
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 |
|
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 |
|
573 |
|
574 |
|
575 |
|
576 |
|
577 |
|
578 |
|
579 |
|
580 |
|
581 | static hexlifyTransaction(transaction: TransactionRequest, allowExtra?: { [key: string]: boolean }): { [key: string]: string | AccessList } {
|
582 |
|
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 |
|
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 | }
|