Compare commits

..

6 Commits

Author SHA1 Message Date
Richard Moore
1b4debd4a9 Limit decoded result imflation ratio from ABI-encoded data (#4537). 2024-01-12 19:45:26 -05:00
Richard Moore
6017d3d39a admin: updated dist files 2024-01-02 19:12:56 -05:00
Richard Moore
ccac24a5b0 Fix Base58 padding for string representation of binary data (#4527). 2024-01-02 19:11:11 -05:00
Richard Moore
f6d155c820 admin: update dist files 2023-12-19 04:53:50 -05:00
Richard Moore
180221574c Fix uncatchable issue when sending transactions over JSON-RPC and provide some retry-recovery for missing v (#4513). 2023-12-19 04:52:38 -05:00
Richard Moore
6ee1a5f8bb docs: Fixed some grammar in getting-started (#4486, #4487, #4488) 2023-12-05 16:51:45 -05:00
47 changed files with 958 additions and 134 deletions

View File

@@ -3,6 +3,16 @@ Change Log
This change log is maintained by `src.ts/_admin/update-changelog.ts` but may also be manually updated.
ethers/v6.9.2 (2024-01-02 19:12)
--------------------------------
- Fix Base58 padding for string representation of binary data ([#4527](https://github.com/ethers-io/ethers.js/issues/4527); [ccac24a](https://github.com/ethers-io/ethers.js/commit/ccac24a5b0a4d07a4b639c1c4d0a44703e32d418)).
ethers/v6.9.1 (2023-12-19 04:53)
--------------------------------
- Fix uncatchable issue when sending transactions over JSON-RPC and provide some retry-recovery for missing v ([#4513](https://github.com/ethers-io/ethers.js/issues/4513); [1802215](https://github.com/ethers-io/ethers.js/commit/180221574c5d2af9ad85404af4fab8752d3d5029)).
ethers/v6.9.0 (2023-11-27 06:15)
--------------------------------

90
dist/ethers.js vendored
View File

@@ -3,7 +3,7 @@ const __$G = (typeof globalThis !== 'undefined' ? globalThis: typeof window !==
/**
* The current version of Ethers.
*/
const version = "6.9.0";
const version = "6.9.2";
/**
* Property helper functions.
@@ -695,12 +695,20 @@ const BN_58 = BigInt(58);
* Encode %%value%% as a Base58-encoded string.
*/
function encodeBase58(_value) {
let value = toBigInt(getBytes(_value));
const bytes = getBytes(_value);
let value = toBigInt(bytes);
let result = "";
while (value) {
result = Alphabet[Number(value % BN_58)] + result;
value /= BN_58;
}
// Account for leading padding zeros
for (let i = 0; i < bytes.length; i++) {
if (bytes[i]) {
break;
}
result = Alphabet[0] + result;
}
return result;
}
/**
@@ -18048,16 +18056,19 @@ class AbstractProvider {
// No explicit network was set and this is our first time
if (this.#networkPromise == null) {
// Detect the current network (shared with all calls)
const detectNetwork = this._detectNetwork().then((network) => {
this.emit("network", network, null);
return network;
}, (error) => {
// Reset the networkPromise on failure, so we will try again
if (this.#networkPromise === detectNetwork) {
this.#networkPromise = null;
const detectNetwork = (async () => {
try {
const network = await this._detectNetwork();
this.emit("network", network, null);
return network;
}
throw error;
});
catch (error) {
if (this.#networkPromise === detectNetwork) {
this.#networkPromise = null;
}
throw error;
}
})();
this.#networkPromise = detectNetwork;
return (await detectNetwork).clone();
}
@@ -19444,12 +19455,45 @@ class JsonRpcSigner extends AbstractSigner {
// for it; it should show up very quickly
return await (new Promise((resolve, reject) => {
const timeouts = [1000, 100];
let invalids = 0;
const checkTx = async () => {
// Try getting the transaction
const tx = await this.provider.getTransaction(hash);
if (tx != null) {
resolve(tx.replaceableTransaction(blockNumber));
return;
try {
// Try getting the transaction
const tx = await this.provider.getTransaction(hash);
if (tx != null) {
resolve(tx.replaceableTransaction(blockNumber));
return;
}
}
catch (error) {
// If we were cancelled: stop polling.
// If the data is bad: the node returns bad transactions
// If the network changed: calling again will also fail
// If unsupported: likely destroyed
if (isError(error, "CANCELLED") || isError(error, "BAD_DATA") ||
isError(error, "NETWORK_ERROR" )) {
if (error.info == null) {
error.info = {};
}
error.info.sendTransactionHash = hash;
reject(error);
return;
}
// Stop-gap for misbehaving backends; see #4513
if (isError(error, "INVALID_ARGUMENT")) {
invalids++;
if (error.info == null) {
error.info = {};
}
error.info.sendTransactionHash = hash;
if (invalids > 10) {
reject(error);
return;
}
}
// Notify anyone that cares; but we will try again, since
// it is likely an intermittent service error
this.provider.emit("error", makeError("failed to fetch transation after sending (will try again)", "UNKNOWN_ERROR", { error }));
}
// Wait another 4 seconds
this.provider._setTimeout(() => { checkTx(); }, timeouts.pop() || 4000);
@@ -19527,7 +19571,7 @@ class JsonRpcApiProvider extends AbstractProvider {
if (this.#drainTimer) {
return;
}
// If we aren't using batching, no hard in sending it immeidately
// If we aren't using batching, no harm in sending it immediately
const stallTime = (this._getOption("batchMaxCount") === 1) ? 0 : this._getOption("batchStallTime");
this.#drainTimer = setTimeout(() => {
this.#drainTimer = null;
@@ -19690,9 +19734,15 @@ class JsonRpcApiProvider extends AbstractProvider {
// If we are ready, use ``send``, which enabled requests to be batched
if (this.ready) {
this.#pendingDetectNetwork = (async () => {
const result = Network.from(getBigInt(await this.send("eth_chainId", [])));
this.#pendingDetectNetwork = null;
return result;
try {
const result = Network.from(getBigInt(await this.send("eth_chainId", [])));
this.#pendingDetectNetwork = null;
return result;
}
catch (error) {
this.#pendingDetectNetwork = null;
throw error;
}
})();
return await this.#pendingDetectNetwork;
}

2
dist/ethers.js.map vendored

File diff suppressed because one or more lines are too long

2
dist/ethers.min.js vendored

File diff suppressed because one or more lines are too long

90
dist/ethers.umd.js vendored
View File

@@ -9,7 +9,7 @@ const __$G = (typeof globalThis !== 'undefined' ? globalThis: typeof window !==
/**
* The current version of Ethers.
*/
const version = "6.9.0";
const version = "6.9.2";
/**
* Property helper functions.
@@ -701,12 +701,20 @@ const __$G = (typeof globalThis !== 'undefined' ? globalThis: typeof window !==
* Encode %%value%% as a Base58-encoded string.
*/
function encodeBase58(_value) {
let value = toBigInt(getBytes(_value));
const bytes = getBytes(_value);
let value = toBigInt(bytes);
let result = "";
while (value) {
result = Alphabet[Number(value % BN_58)] + result;
value /= BN_58;
}
// Account for leading padding zeros
for (let i = 0; i < bytes.length; i++) {
if (bytes[i]) {
break;
}
result = Alphabet[0] + result;
}
return result;
}
/**
@@ -18054,16 +18062,19 @@ const __$G = (typeof globalThis !== 'undefined' ? globalThis: typeof window !==
// No explicit network was set and this is our first time
if (this.#networkPromise == null) {
// Detect the current network (shared with all calls)
const detectNetwork = this._detectNetwork().then((network) => {
this.emit("network", network, null);
return network;
}, (error) => {
// Reset the networkPromise on failure, so we will try again
if (this.#networkPromise === detectNetwork) {
this.#networkPromise = null;
const detectNetwork = (async () => {
try {
const network = await this._detectNetwork();
this.emit("network", network, null);
return network;
}
throw error;
});
catch (error) {
if (this.#networkPromise === detectNetwork) {
this.#networkPromise = null;
}
throw error;
}
})();
this.#networkPromise = detectNetwork;
return (await detectNetwork).clone();
}
@@ -19450,12 +19461,45 @@ const __$G = (typeof globalThis !== 'undefined' ? globalThis: typeof window !==
// for it; it should show up very quickly
return await (new Promise((resolve, reject) => {
const timeouts = [1000, 100];
let invalids = 0;
const checkTx = async () => {
// Try getting the transaction
const tx = await this.provider.getTransaction(hash);
if (tx != null) {
resolve(tx.replaceableTransaction(blockNumber));
return;
try {
// Try getting the transaction
const tx = await this.provider.getTransaction(hash);
if (tx != null) {
resolve(tx.replaceableTransaction(blockNumber));
return;
}
}
catch (error) {
// If we were cancelled: stop polling.
// If the data is bad: the node returns bad transactions
// If the network changed: calling again will also fail
// If unsupported: likely destroyed
if (isError(error, "CANCELLED") || isError(error, "BAD_DATA") ||
isError(error, "NETWORK_ERROR" )) {
if (error.info == null) {
error.info = {};
}
error.info.sendTransactionHash = hash;
reject(error);
return;
}
// Stop-gap for misbehaving backends; see #4513
if (isError(error, "INVALID_ARGUMENT")) {
invalids++;
if (error.info == null) {
error.info = {};
}
error.info.sendTransactionHash = hash;
if (invalids > 10) {
reject(error);
return;
}
}
// Notify anyone that cares; but we will try again, since
// it is likely an intermittent service error
this.provider.emit("error", makeError("failed to fetch transation after sending (will try again)", "UNKNOWN_ERROR", { error }));
}
// Wait another 4 seconds
this.provider._setTimeout(() => { checkTx(); }, timeouts.pop() || 4000);
@@ -19533,7 +19577,7 @@ const __$G = (typeof globalThis !== 'undefined' ? globalThis: typeof window !==
if (this.#drainTimer) {
return;
}
// If we aren't using batching, no hard in sending it immeidately
// If we aren't using batching, no harm in sending it immediately
const stallTime = (this._getOption("batchMaxCount") === 1) ? 0 : this._getOption("batchStallTime");
this.#drainTimer = setTimeout(() => {
this.#drainTimer = null;
@@ -19696,9 +19740,15 @@ const __$G = (typeof globalThis !== 'undefined' ? globalThis: typeof window !==
// If we are ready, use ``send``, which enabled requests to be batched
if (this.ready) {
this.#pendingDetectNetwork = (async () => {
const result = Network.from(getBigInt(await this.send("eth_chainId", [])));
this.#pendingDetectNetwork = null;
return result;
try {
const result = Network.from(getBigInt(await this.send("eth_chainId", [])));
this.#pendingDetectNetwork = null;
return result;
}
catch (error) {
this.#pendingDetectNetwork = null;
throw error;
}
})();
return await this.#pendingDetectNetwork;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -100,7 +100,7 @@ const rotlBL = (h, l, s) => (h << (s - 32)) | (l >>> (64 - s));
/**
* The current version of Ethers.
*/
const version = "6.9.0";
const version = "6.9.2";
/**
* Property helper functions.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -129,8 +129,8 @@ _code: @lang<script>
// If MetaMask is not installed, we use the default provider,
// which is backed by a variety of third-party services (such
// as INFURA). They do not have private keys installed so are
// only have read-only access
// as INFURA). They do not have private keys installed,
// so they only have read-only access
console.log("MetaMask not installed; using read-only defaults")
provider = ethers.getDefaultProvider()
@@ -156,7 +156,7 @@ you can use the [[JsonRpcProvider]] directly, which communicates
using the [[link-jsonrpc]] protocol.
When using your own Ethereum node or a developer-base blockchain,
such as Hardhat or Ganache, you can get access the accounts with
such as Hardhat or Ganache, you can get access to the accounts with
[[JsonRpcProvider-getSigner]].
_code: connecting to a JSON-RPC URL @lang<script>
@@ -177,7 +177,7 @@ results when performing mathematic operations.
As a result, the internal units used (e.g. wei) which are suited for
machine-readable purposes and maths are often very large and not
terribly human-readable.
easily human-readable.
For example, imagine dealing with dollars and cents; you would show
values like ``"$2.56"``. In the blockchain world, we would keep all

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=test-provider-jsonrpc.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"test-provider-jsonrpc.d.ts","sourceRoot":"","sources":["../../src.ts/_tests/test-provider-jsonrpc.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,160 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const assert_1 = tslib_1.__importDefault(require("assert"));
const index_js_1 = require("../index.js");
const StatusMessages = {
200: "OK",
400: "BAD REQUEST",
500: "SERVER ERROR",
};
const wallet = new index_js_1.Wallet((0, index_js_1.id)("test"));
function createProvider(testFunc) {
let blockNumber = 1;
const ticker = setInterval(() => { blockNumber++; }, 100);
if (ticker.unref) {
ticker.unref();
}
const processReq = (req) => {
let result = testFunc(req.method, req.params, blockNumber);
if (result === undefined) {
switch (req.method) {
case "eth_blockNumber":
result = blockNumber;
break;
case "eth_chainId":
result = "0x1337";
break;
case "eth_accounts":
result = [wallet.address];
break;
default:
console.log("****", req);
return { id: index_js_1.id, error: "unsupported", jsonrpc: "2.0" };
}
}
return { id: req.id, result, jsonrpc: "2.0" };
};
const req = new index_js_1.FetchRequest("http:/\/localhost:8082/");
req.getUrlFunc = async (_req, signal) => {
const req = JSON.parse(_req.hasBody() ? (0, index_js_1.toUtf8String)(_req.body) : "");
let statusCode = 200;
const headers = {};
let resp;
try {
if (Array.isArray(req)) {
resp = req.map((r) => processReq(r));
}
else {
resp = processReq(req);
}
}
catch (error) {
statusCode = 500;
resp = error.message;
}
const body = (0, index_js_1.toUtf8Bytes)(JSON.stringify(resp));
return {
statusCode,
statusMessage: StatusMessages[statusCode],
headers, body
};
};
return new index_js_1.JsonRpcProvider(req, undefined, { cacheTimeout: -1 });
}
describe("Ensure Catchable Errors", function () {
it("Can catch bad broadcast replies", async function () {
this.timeout(15000);
const txInfo = {
chainId: 1337,
gasLimit: 100000,
maxFeePerGas: 2000000000,
maxPriorityFeePerGas: 1000000000,
to: wallet.address,
value: 1,
};
const txSign = await wallet.signTransaction(txInfo);
const txObj = index_js_1.Transaction.from(txSign);
let count = 0;
const provider = createProvider((method, params, blockNumber) => {
switch (method) {
case "eth_sendTransaction":
return txObj.hash;
case "eth_getTransactionByHash": {
count++;
// First time; fail!
if (count === 1) {
throw (0, index_js_1.makeError)("Faux Error", "SERVER_ERROR", {
request: ({})
});
}
// Second time; return null
if (count === 2) {
return null;
}
// Return a valid tx...
const result = Object.assign({}, txObj.toJSON(), txObj.signature.toJSON(), { hash: txObj.hash, from: wallet.address });
// ...eventually mined
if (count > 4) {
result.blockNumber = blockNumber;
result.blockHash = (0, index_js_1.id)("test");
}
return result;
}
}
return undefined;
});
const signer = await provider.getSigner();
const tx = await signer.sendTransaction(txInfo);
(0, assert_1.default)(tx);
});
it("Missing v is recovered", async function () {
this.timeout(15000);
const txInfo = {
chainId: 1337,
gasLimit: 100000,
maxFeePerGas: 2000000000,
maxPriorityFeePerGas: 1000000000,
to: wallet.address,
value: 1,
};
const txSign = await wallet.signTransaction(txInfo);
const txObj = index_js_1.Transaction.from(txSign);
let count = 0;
// A provider which is mocked to return a "missing v"
// in getTransaction
const provider = createProvider((method, params, blockNumber) => {
switch (method) {
case "eth_sendTransaction":
return txObj.hash;
case "eth_getTransactionByHash": {
count++;
// The fully valid tx response
const result = Object.assign({}, txObj.toJSON(), txObj.signature.toJSON(), { hash: txObj.hash, from: wallet.address, sig: null });
// First time; fail with a missing v!
if (count < 2) {
delete result.v;
}
// Debug
result._count = count;
return result;
}
}
return undefined;
});
// Track any "missing v" error
let missingV = null;
provider.on("error", (e) => {
if ((0, index_js_1.isError)(e, "UNKNOWN_ERROR") && (0, index_js_1.isError)(e.error, "INVALID_ARGUMENT")) {
if (e.error.argument === "signature" && e.error.shortMessage === "missing v") {
missingV = e.error;
}
}
});
const signer = await provider.getSigner();
const tx = await signer.sendTransaction(txInfo);
assert_1.default.ok(!!tx, "we got a transaction");
assert_1.default.ok(!!missingV, "missing v error present");
});
});
//# sourceMappingURL=test-provider-jsonrpc.js.map

File diff suppressed because one or more lines are too long

View File

@@ -5,5 +5,5 @@ exports.version = void 0;
/**
* The current version of Ethers.
*/
exports.version = "6.9.0";
exports.version = "6.9.2";
//# sourceMappingURL=_version.js.map

File diff suppressed because one or more lines are too long

View File

@@ -544,16 +544,19 @@ class AbstractProvider {
// No explicit network was set and this is our first time
if (this.#networkPromise == null) {
// Detect the current network (shared with all calls)
const detectNetwork = this._detectNetwork().then((network) => {
this.emit("network", network, null);
return network;
}, (error) => {
// Reset the networkPromise on failure, so we will try again
if (this.#networkPromise === detectNetwork) {
this.#networkPromise = null;
const detectNetwork = (async () => {
try {
const network = await this._detectNetwork();
this.emit("network", network, null);
return network;
}
throw error;
});
catch (error) {
if (this.#networkPromise === detectNetwork) {
this.#networkPromise = null;
}
throw error;
}
})();
this.#networkPromise = detectNetwork;
return (await detectNetwork).clone();
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -136,12 +136,45 @@ class JsonRpcSigner extends abstract_signer_js_1.AbstractSigner {
// for it; it should show up very quickly
return await (new Promise((resolve, reject) => {
const timeouts = [1000, 100];
let invalids = 0;
const checkTx = async () => {
// Try getting the transaction
const tx = await this.provider.getTransaction(hash);
if (tx != null) {
resolve(tx.replaceableTransaction(blockNumber));
return;
try {
// Try getting the transaction
const tx = await this.provider.getTransaction(hash);
if (tx != null) {
resolve(tx.replaceableTransaction(blockNumber));
return;
}
}
catch (error) {
// If we were cancelled: stop polling.
// If the data is bad: the node returns bad transactions
// If the network changed: calling again will also fail
// If unsupported: likely destroyed
if ((0, index_js_5.isError)(error, "CANCELLED") || (0, index_js_5.isError)(error, "BAD_DATA") ||
(0, index_js_5.isError)(error, "NETWORK_ERROR" || (0, index_js_5.isError)(error, "UNSUPPORTED_OPERATION"))) {
if (error.info == null) {
error.info = {};
}
error.info.sendTransactionHash = hash;
reject(error);
return;
}
// Stop-gap for misbehaving backends; see #4513
if ((0, index_js_5.isError)(error, "INVALID_ARGUMENT")) {
invalids++;
if (error.info == null) {
error.info = {};
}
error.info.sendTransactionHash = hash;
if (invalids > 10) {
reject(error);
return;
}
}
// Notify anyone that cares; but we will try again, since
// it is likely an intermittent service error
this.provider.emit("error", (0, index_js_5.makeError)("failed to fetch transation after sending (will try again)", "UNKNOWN_ERROR", { error }));
}
// Wait another 4 seconds
this.provider._setTimeout(() => { checkTx(); }, timeouts.pop() || 4000);
@@ -220,7 +253,7 @@ class JsonRpcApiProvider extends abstract_provider_js_1.AbstractProvider {
if (this.#drainTimer) {
return;
}
// If we aren't using batching, no hard in sending it immeidately
// If we aren't using batching, no harm in sending it immediately
const stallTime = (this._getOption("batchMaxCount") === 1) ? 0 : this._getOption("batchStallTime");
this.#drainTimer = setTimeout(() => {
this.#drainTimer = null;
@@ -383,9 +416,15 @@ class JsonRpcApiProvider extends abstract_provider_js_1.AbstractProvider {
// If we are ready, use ``send``, which enabled requests to be batched
if (this.ready) {
this.#pendingDetectNetwork = (async () => {
const result = network_js_1.Network.from((0, index_js_5.getBigInt)(await this.send("eth_chainId", [])));
this.#pendingDetectNetwork = null;
return result;
try {
const result = network_js_1.Network.from((0, index_js_5.getBigInt)(await this.send("eth_chainId", [])));
this.#pendingDetectNetwork = null;
return result;
}
catch (error) {
this.#pendingDetectNetwork = null;
throw error;
}
})();
return await this.#pendingDetectNetwork;
}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"version":3,"file":"base58.d.ts","sourceRoot":"","sources":["../../src.ts/utils/base58.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAsB5C;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAQtD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOlD"}
{"version":3,"file":"base58.d.ts","sourceRoot":"","sources":["../../src.ts/utils/base58.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAsB5C;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAiBtD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOlD"}

View File

@@ -36,12 +36,20 @@ const BN_58 = BigInt(58);
* Encode %%value%% as a Base58-encoded string.
*/
function encodeBase58(_value) {
let value = (0, maths_js_1.toBigInt)((0, data_js_1.getBytes)(_value));
const bytes = (0, data_js_1.getBytes)(_value);
let value = (0, maths_js_1.toBigInt)(bytes);
let result = "";
while (value) {
result = Alphabet[Number(value % BN_58)] + result;
value /= BN_58;
}
// Account for leading padding zeros
for (let i = 0; i < bytes.length; i++) {
if (bytes[i]) {
break;
}
result = Alphabet[0] + result;
}
return result;
}
exports.encodeBase58 = encodeBase58;

View File

@@ -1 +1 @@
{"version":3,"file":"base58.js","sourceRoot":"","sources":["../../src.ts/utils/base58.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;;AAEH,uCAAqC;AACrC,2CAA6C;AAC7C,yCAAsC;AAKtC,MAAM,QAAQ,GAAG,4DAA4D,CAAC;AAC9E,IAAI,MAAM,GAAkC,IAAI,CAAC;AAEjD,SAAS,QAAQ,CAAC,MAAc;IAC5B,IAAI,MAAM,IAAI,IAAI,EAAE;QAChB,MAAM,GAAG,EAAG,CAAC;QACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;YACtC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;SACnC;KACJ;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAC9B,IAAA,0BAAc,EAAC,MAAM,IAAI,IAAI,EAAE,sBAAsB,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IACzE,OAAO,MAAM,CAAC;AAClB,CAAC;AAGD,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;AACvB,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC;AAEzB;;GAEG;AACH,SAAgB,YAAY,CAAC,MAAiB;IAC1C,IAAI,KAAK,GAAG,IAAA,mBAAQ,EAAC,IAAA,kBAAQ,EAAC,MAAM,CAAC,CAAC,CAAC;IACvC,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,OAAO,KAAK,EAAE;QACV,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC;QAClD,KAAK,IAAI,KAAK,CAAC;KAClB;IACD,OAAO,MAAM,CAAC;AAClB,CAAC;AARD,oCAQC;AAED;;GAEG;AACH,SAAgB,YAAY,CAAC,KAAa;IACtC,IAAI,MAAM,GAAG,IAAI,CAAC;IAClB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QACnC,MAAM,IAAI,KAAK,CAAC;QAChB,MAAM,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;KAChC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC;AAPD,oCAOC"}
{"version":3,"file":"base58.js","sourceRoot":"","sources":["../../src.ts/utils/base58.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;;AAEH,uCAAqC;AACrC,2CAA6C;AAC7C,yCAAsC;AAKtC,MAAM,QAAQ,GAAG,4DAA4D,CAAC;AAC9E,IAAI,MAAM,GAAkC,IAAI,CAAC;AAEjD,SAAS,QAAQ,CAAC,MAAc;IAC5B,IAAI,MAAM,IAAI,IAAI,EAAE;QAChB,MAAM,GAAG,EAAG,CAAC;QACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;YACtC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;SACnC;KACJ;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAC9B,IAAA,0BAAc,EAAC,MAAM,IAAI,IAAI,EAAE,sBAAsB,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IACzE,OAAO,MAAM,CAAC;AAClB,CAAC;AAGD,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;AACvB,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC;AAEzB;;GAEG;AACH,SAAgB,YAAY,CAAC,MAAiB;IAC1C,MAAM,KAAK,GAAG,IAAA,kBAAQ,EAAC,MAAM,CAAC,CAAC;IAE/B,IAAI,KAAK,GAAG,IAAA,mBAAQ,EAAC,KAAK,CAAC,CAAC;IAC5B,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,OAAO,KAAK,EAAE;QACV,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC;QAClD,KAAK,IAAI,KAAK,CAAC;KAClB;IAED,oCAAoC;IACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QACnC,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE;YAAE,MAAM;SAAE;QACxB,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC;KACjC;IAED,OAAO,MAAM,CAAC;AAClB,CAAC;AAjBD,oCAiBC;AAED;;GAEG;AACH,SAAgB,YAAY,CAAC,KAAa;IACtC,IAAI,MAAM,GAAG,IAAI,CAAC;IAClB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QACnC,MAAM,IAAI,KAAK,CAAC;QAChB,MAAM,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;KAChC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC;AAPD,oCAOC"}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=test-provider-jsonrpc.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"test-provider-jsonrpc.d.ts","sourceRoot":"","sources":["../../src.ts/_tests/test-provider-jsonrpc.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,157 @@
import assert from "assert";
import { id, isError, makeError, toUtf8Bytes, toUtf8String, FetchRequest, JsonRpcProvider, Transaction, Wallet } from "../index.js";
const StatusMessages = {
200: "OK",
400: "BAD REQUEST",
500: "SERVER ERROR",
};
const wallet = new Wallet(id("test"));
function createProvider(testFunc) {
let blockNumber = 1;
const ticker = setInterval(() => { blockNumber++; }, 100);
if (ticker.unref) {
ticker.unref();
}
const processReq = (req) => {
let result = testFunc(req.method, req.params, blockNumber);
if (result === undefined) {
switch (req.method) {
case "eth_blockNumber":
result = blockNumber;
break;
case "eth_chainId":
result = "0x1337";
break;
case "eth_accounts":
result = [wallet.address];
break;
default:
console.log("****", req);
return { id, error: "unsupported", jsonrpc: "2.0" };
}
}
return { id: req.id, result, jsonrpc: "2.0" };
};
const req = new FetchRequest("http:/\/localhost:8082/");
req.getUrlFunc = async (_req, signal) => {
const req = JSON.parse(_req.hasBody() ? toUtf8String(_req.body) : "");
let statusCode = 200;
const headers = {};
let resp;
try {
if (Array.isArray(req)) {
resp = req.map((r) => processReq(r));
}
else {
resp = processReq(req);
}
}
catch (error) {
statusCode = 500;
resp = error.message;
}
const body = toUtf8Bytes(JSON.stringify(resp));
return {
statusCode,
statusMessage: StatusMessages[statusCode],
headers, body
};
};
return new JsonRpcProvider(req, undefined, { cacheTimeout: -1 });
}
describe("Ensure Catchable Errors", function () {
it("Can catch bad broadcast replies", async function () {
this.timeout(15000);
const txInfo = {
chainId: 1337,
gasLimit: 100000,
maxFeePerGas: 2000000000,
maxPriorityFeePerGas: 1000000000,
to: wallet.address,
value: 1,
};
const txSign = await wallet.signTransaction(txInfo);
const txObj = Transaction.from(txSign);
let count = 0;
const provider = createProvider((method, params, blockNumber) => {
switch (method) {
case "eth_sendTransaction":
return txObj.hash;
case "eth_getTransactionByHash": {
count++;
// First time; fail!
if (count === 1) {
throw makeError("Faux Error", "SERVER_ERROR", {
request: ({})
});
}
// Second time; return null
if (count === 2) {
return null;
}
// Return a valid tx...
const result = Object.assign({}, txObj.toJSON(), txObj.signature.toJSON(), { hash: txObj.hash, from: wallet.address });
// ...eventually mined
if (count > 4) {
result.blockNumber = blockNumber;
result.blockHash = id("test");
}
return result;
}
}
return undefined;
});
const signer = await provider.getSigner();
const tx = await signer.sendTransaction(txInfo);
assert(tx);
});
it("Missing v is recovered", async function () {
this.timeout(15000);
const txInfo = {
chainId: 1337,
gasLimit: 100000,
maxFeePerGas: 2000000000,
maxPriorityFeePerGas: 1000000000,
to: wallet.address,
value: 1,
};
const txSign = await wallet.signTransaction(txInfo);
const txObj = Transaction.from(txSign);
let count = 0;
// A provider which is mocked to return a "missing v"
// in getTransaction
const provider = createProvider((method, params, blockNumber) => {
switch (method) {
case "eth_sendTransaction":
return txObj.hash;
case "eth_getTransactionByHash": {
count++;
// The fully valid tx response
const result = Object.assign({}, txObj.toJSON(), txObj.signature.toJSON(), { hash: txObj.hash, from: wallet.address, sig: null });
// First time; fail with a missing v!
if (count < 2) {
delete result.v;
}
// Debug
result._count = count;
return result;
}
}
return undefined;
});
// Track any "missing v" error
let missingV = null;
provider.on("error", (e) => {
if (isError(e, "UNKNOWN_ERROR") && isError(e.error, "INVALID_ARGUMENT")) {
if (e.error.argument === "signature" && e.error.shortMessage === "missing v") {
missingV = e.error;
}
}
});
const signer = await provider.getSigner();
const tx = await signer.sendTransaction(txInfo);
assert.ok(!!tx, "we got a transaction");
assert.ok(!!missingV, "missing v error present");
});
});
//# sourceMappingURL=test-provider-jsonrpc.js.map

File diff suppressed because one or more lines are too long

View File

@@ -2,5 +2,5 @@
/**
* The current version of Ethers.
*/
export const version = "6.9.0";
export const version = "6.9.2";
//# sourceMappingURL=_version.js.map

File diff suppressed because one or more lines are too long

View File

@@ -540,16 +540,19 @@ export class AbstractProvider {
// No explicit network was set and this is our first time
if (this.#networkPromise == null) {
// Detect the current network (shared with all calls)
const detectNetwork = this._detectNetwork().then((network) => {
this.emit("network", network, null);
return network;
}, (error) => {
// Reset the networkPromise on failure, so we will try again
if (this.#networkPromise === detectNetwork) {
this.#networkPromise = null;
const detectNetwork = (async () => {
try {
const network = await this._detectNetwork();
this.emit("network", network, null);
return network;
}
throw error;
});
catch (error) {
if (this.#networkPromise === detectNetwork) {
this.#networkPromise = null;
}
throw error;
}
})();
this.#networkPromise = detectNetwork;
return (await detectNetwork).clone();
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -16,7 +16,7 @@ import { AbiCoder } from "../abi/index.js";
import { getAddress, resolveAddress } from "../address/index.js";
import { TypedDataEncoder } from "../hash/index.js";
import { accessListify } from "../transaction/index.js";
import { defineProperties, getBigInt, hexlify, isHexString, toQuantity, toUtf8Bytes, makeError, assert, assertArgument, FetchRequest, resolveProperties } from "../utils/index.js";
import { defineProperties, getBigInt, hexlify, isHexString, toQuantity, toUtf8Bytes, isError, makeError, assert, assertArgument, FetchRequest, resolveProperties } from "../utils/index.js";
import { AbstractProvider, UnmanagedSubscriber } from "./abstract-provider.js";
import { AbstractSigner } from "./abstract-signer.js";
import { Network } from "./network.js";
@@ -133,12 +133,45 @@ export class JsonRpcSigner extends AbstractSigner {
// for it; it should show up very quickly
return await (new Promise((resolve, reject) => {
const timeouts = [1000, 100];
let invalids = 0;
const checkTx = async () => {
// Try getting the transaction
const tx = await this.provider.getTransaction(hash);
if (tx != null) {
resolve(tx.replaceableTransaction(blockNumber));
return;
try {
// Try getting the transaction
const tx = await this.provider.getTransaction(hash);
if (tx != null) {
resolve(tx.replaceableTransaction(blockNumber));
return;
}
}
catch (error) {
// If we were cancelled: stop polling.
// If the data is bad: the node returns bad transactions
// If the network changed: calling again will also fail
// If unsupported: likely destroyed
if (isError(error, "CANCELLED") || isError(error, "BAD_DATA") ||
isError(error, "NETWORK_ERROR" || isError(error, "UNSUPPORTED_OPERATION"))) {
if (error.info == null) {
error.info = {};
}
error.info.sendTransactionHash = hash;
reject(error);
return;
}
// Stop-gap for misbehaving backends; see #4513
if (isError(error, "INVALID_ARGUMENT")) {
invalids++;
if (error.info == null) {
error.info = {};
}
error.info.sendTransactionHash = hash;
if (invalids > 10) {
reject(error);
return;
}
}
// Notify anyone that cares; but we will try again, since
// it is likely an intermittent service error
this.provider.emit("error", makeError("failed to fetch transation after sending (will try again)", "UNKNOWN_ERROR", { error }));
}
// Wait another 4 seconds
this.provider._setTimeout(() => { checkTx(); }, timeouts.pop() || 4000);
@@ -216,7 +249,7 @@ export class JsonRpcApiProvider extends AbstractProvider {
if (this.#drainTimer) {
return;
}
// If we aren't using batching, no hard in sending it immeidately
// If we aren't using batching, no harm in sending it immediately
const stallTime = (this._getOption("batchMaxCount") === 1) ? 0 : this._getOption("batchStallTime");
this.#drainTimer = setTimeout(() => {
this.#drainTimer = null;
@@ -379,9 +412,15 @@ export class JsonRpcApiProvider extends AbstractProvider {
// If we are ready, use ``send``, which enabled requests to be batched
if (this.ready) {
this.#pendingDetectNetwork = (async () => {
const result = Network.from(getBigInt(await this.send("eth_chainId", [])));
this.#pendingDetectNetwork = null;
return result;
try {
const result = Network.from(getBigInt(await this.send("eth_chainId", [])));
this.#pendingDetectNetwork = null;
return result;
}
catch (error) {
this.#pendingDetectNetwork = null;
throw error;
}
})();
return await this.#pendingDetectNetwork;
}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"version":3,"file":"base58.d.ts","sourceRoot":"","sources":["../../src.ts/utils/base58.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAsB5C;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAQtD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOlD"}
{"version":3,"file":"base58.d.ts","sourceRoot":"","sources":["../../src.ts/utils/base58.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAsB5C;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAiBtD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOlD"}

View File

@@ -33,12 +33,20 @@ const BN_58 = BigInt(58);
* Encode %%value%% as a Base58-encoded string.
*/
export function encodeBase58(_value) {
let value = toBigInt(getBytes(_value));
const bytes = getBytes(_value);
let value = toBigInt(bytes);
let result = "";
while (value) {
result = Alphabet[Number(value % BN_58)] + result;
value /= BN_58;
}
// Account for leading padding zeros
for (let i = 0; i < bytes.length; i++) {
if (bytes[i]) {
break;
}
result = Alphabet[0] + result;
}
return result;
}
/**

View File

@@ -1 +1 @@
{"version":3,"file":"base58.js","sourceRoot":"","sources":["../../src.ts/utils/base58.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAKtC,MAAM,QAAQ,GAAG,4DAA4D,CAAC;AAC9E,IAAI,MAAM,GAAkC,IAAI,CAAC;AAEjD,SAAS,QAAQ,CAAC,MAAc;IAC5B,IAAI,MAAM,IAAI,IAAI,EAAE;QAChB,MAAM,GAAG,EAAG,CAAC;QACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;YACtC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;SACnC;KACJ;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAC9B,cAAc,CAAC,MAAM,IAAI,IAAI,EAAE,sBAAsB,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IACzE,OAAO,MAAM,CAAC;AAClB,CAAC;AAGD,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;AACvB,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC;AAEzB;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,MAAiB;IAC1C,IAAI,KAAK,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IACvC,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,OAAO,KAAK,EAAE;QACV,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC;QAClD,KAAK,IAAI,KAAK,CAAC;KAClB;IACD,OAAO,MAAM,CAAC;AAClB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa;IACtC,IAAI,MAAM,GAAG,IAAI,CAAC;IAClB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QACnC,MAAM,IAAI,KAAK,CAAC;QAChB,MAAM,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;KAChC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC"}
{"version":3,"file":"base58.js","sourceRoot":"","sources":["../../src.ts/utils/base58.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAKtC,MAAM,QAAQ,GAAG,4DAA4D,CAAC;AAC9E,IAAI,MAAM,GAAkC,IAAI,CAAC;AAEjD,SAAS,QAAQ,CAAC,MAAc;IAC5B,IAAI,MAAM,IAAI,IAAI,EAAE;QAChB,MAAM,GAAG,EAAG,CAAC;QACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;YACtC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;SACnC;KACJ;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAC9B,cAAc,CAAC,MAAM,IAAI,IAAI,EAAE,sBAAsB,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IACzE,OAAO,MAAM,CAAC;AAClB,CAAC;AAGD,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;AACvB,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC;AAEzB;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,MAAiB;IAC1C,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;IAE/B,IAAI,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC5B,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,OAAO,KAAK,EAAE;QACV,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC;QAClD,KAAK,IAAI,KAAK,CAAC;KAClB;IAED,oCAAoC;IACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QACnC,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE;YAAE,MAAM;SAAE;QACxB,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC;KACjC;IAED,OAAO,MAAM,CAAC;AAClB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa;IACtC,IAAI,MAAM,GAAG,IAAI,CAAC;IAClB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QACnC,MAAM,IAAI,KAAK,CAAC;QAChB,MAAM,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;KAChC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC"}

View File

@@ -93,7 +93,7 @@
"url": "https://www.buymeacoffee.com/ricmoo"
}
],
"gitHead": "f8f11c754aa2c9b541db73d3bde66a8ffa5146f0",
"gitHead": "ccac24a5b0a4d07a4b639c1c4d0a44703e32d418",
"homepage": "https://ethers.org",
"keywords": [
"ethereum",
@@ -128,8 +128,8 @@
"test-browser": "node lib.esm/_admin/test-browser",
"test-commonjs": "mocha --reporter ./reporter.cjs ./lib.commonjs/_tests/test-*.js",
"test-coverage": "c8 -o output -r lcov -r text mocha --no-color --reporter ./reporter.cjs ./lib.esm/_tests/test-*.js | tee output/summary.txt",
"test-esm": "mocha --reporter ./reporter.cjs ./lib.esm/_tests/test-*.js"
"test-esm": "mocha --trace-warnings --reporter ./reporter.cjs ./lib.esm/_tests/test-*.js"
},
"sideEffects": false,
"version": "6.9.0"
"version": "6.9.2"
}

View File

@@ -0,0 +1,207 @@
import assert from "assert";
import {
id, isError, makeError, toUtf8Bytes, toUtf8String,
FetchRequest,
JsonRpcProvider, Transaction, Wallet
} from "../index.js";
const StatusMessages: Record<number, string> = {
200: "OK",
400: "BAD REQUEST",
500: "SERVER ERROR",
};
type ProcessRequest = (method: string, params: Array<string>, blockNumber: number) => any;
const wallet = new Wallet(id("test"));
function createProvider(testFunc: ProcessRequest): JsonRpcProvider {
let blockNumber = 1;
const ticker = setInterval(() => { blockNumber++ }, 100);
if (ticker.unref) { ticker.unref(); }
const processReq = (req: { method: string, params: Array<string>, id: any }) => {
let result = testFunc(req.method, req.params, blockNumber);
if (result === undefined) {
switch (req.method) {
case "eth_blockNumber":
result = blockNumber;
break;
case "eth_chainId":
result = "0x1337";
break;
case "eth_accounts":
result = [ wallet.address ];
break;
default:
console.log("****", req);
return { id, error: "unsupported", jsonrpc: "2.0" };
}
}
return { id: req.id, result, jsonrpc: "2.0" };
};
const req = new FetchRequest("http:/\/localhost:8082/");
req.getUrlFunc = async (_req, signal) => {
const req = JSON.parse(_req.hasBody() ? toUtf8String(_req.body): "");
let statusCode = 200;
const headers = { };
let resp: any;
try {
if (Array.isArray(req)) {
resp = req.map((r) => processReq(r));
} else {
resp = processReq(req);
}
} catch(error: any) {
statusCode = 500;
resp = error.message;
}
const body = toUtf8Bytes(JSON.stringify(resp));
return {
statusCode,
statusMessage: StatusMessages[statusCode],
headers, body
};
};
return new JsonRpcProvider(req, undefined, { cacheTimeout: -1 });
}
describe("Ensure Catchable Errors", function() {
it("Can catch bad broadcast replies", async function() {
this.timeout(15000);
const txInfo = {
chainId: 1337,
gasLimit: 100000,
maxFeePerGas: 2000000000,
maxPriorityFeePerGas: 1000000000,
to: wallet.address,
value: 1,
};
const txSign = await wallet.signTransaction(txInfo);
const txObj = Transaction.from(txSign);
let count = 0;
const provider = createProvider((method, params, blockNumber) => {
switch (method) {
case "eth_sendTransaction":
return txObj.hash;
case "eth_getTransactionByHash": {
count++;
// First time; fail!
if (count === 1) {
throw makeError("Faux Error", "SERVER_ERROR", {
request: <any>({ })
});
}
// Second time; return null
if (count === 2) { return null; }
// Return a valid tx...
const result = Object.assign({ },
txObj.toJSON(),
txObj.signature!.toJSON(),
{ hash: txObj.hash, from: wallet.address });
// ...eventually mined
if (count > 4) {
result.blockNumber = blockNumber;
result.blockHash = id("test");
}
return result;
}
}
return undefined;
});
const signer = await provider.getSigner();
const tx = await signer.sendTransaction(txInfo);
assert(tx);
});
it("Missing v is recovered", async function() {
this.timeout(15000);
const txInfo = {
chainId: 1337,
gasLimit: 100000,
maxFeePerGas: 2000000000,
maxPriorityFeePerGas: 1000000000,
to: wallet.address,
value: 1,
};
const txSign = await wallet.signTransaction(txInfo);
const txObj = Transaction.from(txSign);
let count = 0;
// A provider which is mocked to return a "missing v"
// in getTransaction
const provider = createProvider((method, params, blockNumber) => {
switch (method) {
case "eth_sendTransaction":
return txObj.hash;
case "eth_getTransactionByHash": {
count++;
// The fully valid tx response
const result = Object.assign({ },
txObj.toJSON(),
txObj.signature!.toJSON(),
{ hash: txObj.hash, from: wallet.address, sig: null });
// First time; fail with a missing v!
if (count < 2) { delete result.v; }
// Debug
result._count = count;
return result;
}
}
return undefined;
});
// Track any "missing v" error
let missingV: Error | null = null;
provider.on("error", (e) => {
if (isError(e, "UNKNOWN_ERROR") && isError(e.error, "INVALID_ARGUMENT")) {
if (e.error.argument === "signature" && e.error.shortMessage === "missing v") {
missingV = e.error;
}
}
});
const signer = await provider.getSigner();
const tx = await signer.sendTransaction(txInfo);
assert.ok(!!tx, "we got a transaction");
assert.ok(!!missingV, "missing v error present");
});
});

View File

@@ -3,4 +3,4 @@
/**
* The current version of Ethers.
*/
export const version: string = "6.9.0";
export const version: string = "6.9.2";

View File

@@ -53,7 +53,7 @@ const paramTypeNumber = new RegExp(/^(u?int)([0-9]*)$/);
let defaultCoder: null | AbiCoder = null;
let defaultMaxInflation = 1024;
function getBuiltinCallException(action: CallExceptionAction, tx: { to?: null | string, from?: null | string, data?: string }, data: null | BytesLike, abiCoder: AbiCoder): CallExceptionError {
let message = "missing revert data";
@@ -206,7 +206,12 @@ export class AbiCoder {
decode(types: ReadonlyArray<string | ParamType>, data: BytesLike, loose?: boolean): Result {
const coders: Array<Coder> = types.map((type) => this.#getCoder(ParamType.from(type)));
const coder = new TupleCoder(coders, "_");
return coder.decode(new Reader(data, loose));
return coder.decode(new Reader(data, loose, defaultMaxInflation));
}
static _setDefaultMaxInflation(value: number): void {
assertArgument(typeof(value) === "number" && Number.isInteger(value), "invalid defaultMaxInflation factor", "value", value);
defaultMaxInflation = value;
}
/**

View File

@@ -414,10 +414,17 @@ export class Reader {
readonly #data: Uint8Array;
#offset: number;
constructor(data: BytesLike, allowLoose?: boolean) {
#bytesRead: number;
#parent: null | Reader;
#maxInflation: number;
constructor(data: BytesLike, allowLoose?: boolean, maxInflation?: number) {
defineProperties<Reader>(this, { allowLoose: !!allowLoose });
this.#data = getBytesCopy(data);
this.#bytesRead = 0;
this.#parent = null;
this.#maxInflation = (maxInflation != null) ? maxInflation: 1024;
this.#offset = 0;
}
@@ -427,6 +434,21 @@ export class Reader {
get consumed(): number { return this.#offset; }
get bytes(): Uint8Array { return new Uint8Array(this.#data); }
#incrementBytesRead(count: number): void {
if (this.#parent) { return this.#parent.#incrementBytesRead(count); }
this.#bytesRead += count;
// Check for excessive inflation (see: #4537)
assert(this.#maxInflation < 1 || this.#bytesRead <= this.#maxInflation * this.dataLength, `compressed ABI data exceeds inflation ratio of ${ this.#maxInflation } ( see: https:/\/github.com/ethers-io/ethers.js/issues/4537 )`, "BUFFER_OVERRUN", {
buffer: getBytesCopy(this.#data), offset: this.#offset,
length: count, info: {
bytesRead: this.#bytesRead,
dataLength: this.dataLength
}
});
}
#peekBytes(offset: number, length: number, loose?: boolean): Uint8Array {
let alignedLength = Math.ceil(length / WordSize) * WordSize;
if (this.#offset + alignedLength > this.#data.length) {
@@ -445,12 +467,15 @@ export class Reader {
// Create a sub-reader with the same underlying data, but offset
subReader(offset: number): Reader {
return new Reader(this.#data.slice(this.#offset + offset), this.allowLoose);
const reader = new Reader(this.#data.slice(this.#offset + offset), this.allowLoose, this.#maxInflation);
reader.#parent = this;
return reader;
}
// Read bytes
readBytes(length: number, loose?: boolean): Uint8Array {
let bytes = this.#peekBytes(0, length, !!loose);
this.#incrementBytesRead(length);
this.#offset += bytes.length;
// @TODO: Make sure the length..end bytes are all 0?
return bytes.slice(0, length);

View File

@@ -862,16 +862,18 @@ export class AbstractProvider implements Provider {
if (this.#networkPromise == null) {
// Detect the current network (shared with all calls)
const detectNetwork = this._detectNetwork().then((network) => {
this.emit("network", network, null);
return network;
}, (error) => {
// Reset the networkPromise on failure, so we will try again
if (this.#networkPromise === detectNetwork) {
this.#networkPromise = null;
const detectNetwork = (async () => {
try {
const network = await this._detectNetwork();
this.emit("network", network, null);
return network;
} catch (error) {
if (this.#networkPromise === detectNetwork!) {
this.#networkPromise = null;
}
throw error;
}
throw error;
});
})();
this.#networkPromise = detectNetwork;
return (await detectNetwork).clone();

View File

@@ -21,7 +21,7 @@ import { TypedDataEncoder } from "../hash/index.js";
import { accessListify } from "../transaction/index.js";
import {
defineProperties, getBigInt, hexlify, isHexString, toQuantity, toUtf8Bytes,
makeError, assert, assertArgument,
isError, makeError, assert, assertArgument,
FetchRequest, resolveProperties
} from "../utils/index.js";
@@ -41,7 +41,6 @@ import type { Signer } from "./signer.js";
type Timer = ReturnType<typeof setTimeout>;
const Primitive = "bigint,boolean,function,number,string,symbol".split(/,/g);
//const Methods = "getAddress,then".split(/,/g);
function deepCopy<T = any>(value: T): T {
@@ -363,12 +362,49 @@ export class JsonRpcSigner extends AbstractSigner<JsonRpcApiProvider> {
// for it; it should show up very quickly
return await (new Promise((resolve, reject) => {
const timeouts = [ 1000, 100 ];
let invalids = 0;
const checkTx = async () => {
// Try getting the transaction
const tx = await this.provider.getTransaction(hash);
if (tx != null) {
resolve(tx.replaceableTransaction(blockNumber));
return;
try {
// Try getting the transaction
const tx = await this.provider.getTransaction(hash);
if (tx != null) {
resolve(tx.replaceableTransaction(blockNumber));
return;
}
} catch (error) {
// If we were cancelled: stop polling.
// If the data is bad: the node returns bad transactions
// If the network changed: calling again will also fail
// If unsupported: likely destroyed
if (isError(error, "CANCELLED") || isError(error, "BAD_DATA") ||
isError(error, "NETWORK_ERROR" || isError(error, "UNSUPPORTED_OPERATION"))) {
if (error.info == null) { error.info = { }; }
error.info.sendTransactionHash = hash;
reject(error);
return;
}
// Stop-gap for misbehaving backends; see #4513
if (isError(error, "INVALID_ARGUMENT")) {
invalids++;
if (error.info == null) { error.info = { }; }
error.info.sendTransactionHash = hash;
if (invalids > 10) {
reject(error);
return;
}
}
// Notify anyone that cares; but we will try again, since
// it is likely an intermittent service error
this.provider.emit("error", makeError("failed to fetch transation after sending (will try again)", "UNKNOWN_ERROR", { error }));
}
// Wait another 4 seconds
@@ -468,7 +504,7 @@ export abstract class JsonRpcApiProvider extends AbstractProvider {
#scheduleDrain(): void {
if (this.#drainTimer) { return; }
// If we aren't using batching, no hard in sending it immeidately
// If we aren't using batching, no harm in sending it immediately
const stallTime = (this._getOption("batchMaxCount") === 1) ? 0: this._getOption("batchStallTime");
this.#drainTimer = setTimeout(() => {
@@ -613,6 +649,7 @@ export abstract class JsonRpcApiProvider extends AbstractProvider {
* and should generally call ``super._perform`` as a fallback.
*/
async _perform(req: PerformActionRequest): Promise<any> {
// Legacy networks do not like the type field being passed along (which
// is fair), so we delete type if it is 0 and a non-EIP-1559 network
if (req.method === "call" || req.method === "estimateGas") {
@@ -664,9 +701,14 @@ export abstract class JsonRpcApiProvider extends AbstractProvider {
// If we are ready, use ``send``, which enabled requests to be batched
if (this.ready) {
this.#pendingDetectNetwork = (async () => {
const result = Network.from(getBigInt(await this.send("eth_chainId", [ ])));
this.#pendingDetectNetwork = null;
return result;
try {
const result = Network.from(getBigInt(await this.send("eth_chainId", [ ])));
this.#pendingDetectNetwork = null;
return result;
} catch (error) {
this.#pendingDetectNetwork = null;
throw error;
}
})();
return await this.#pendingDetectNetwork;
}
@@ -1186,7 +1228,6 @@ export class JsonRpcProvider extends JsonRpcApiPollingProvider {
const request = this._getConnection();
request.body = JSON.stringify(payload);
request.setHeader("content-type", "application/json");
const response = await request.send();
response.assertOk();

View File

@@ -42,12 +42,21 @@ const BN_58 = BigInt(58);
* Encode %%value%% as a Base58-encoded string.
*/
export function encodeBase58(_value: BytesLike): string {
let value = toBigInt(getBytes(_value));
const bytes = getBytes(_value);
let value = toBigInt(bytes);
let result = "";
while (value) {
result = Alphabet[Number(value % BN_58)] + result;
value /= BN_58;
}
// Account for leading padding zeros
for (let i = 0; i < bytes.length; i++) {
if (bytes[i]) { break; }
result = Alphabet[0] + result;
}
return result;
}