ethers-opt
Version:
Collection of heavily optimized functions for ethers.js V6
294 lines (260 loc) • 9.67 kB
text/typescript
import { webcrypto } from 'crypto';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (!(BigInt.prototype as any).toJSON) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(BigInt.prototype as any).toJSON = function () {
return this.toString();
};
}
/**
* Detects (heuristically) whether runtime is Node.js.
* @returns {boolean} True if running in Node.js, false otherwise (browser).
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isNode = !(process as any)?.browser && typeof (globalThis as any).window === 'undefined';
/**
* Computes optimal concurrency and batchSize for a given rate-per-second
* limit and batch interval (delays), maximizing both under the constraint:
* concurrency * batchSize <= ratePerBatch
*
* We allow 0 delays to execute batches without delays but the batch size would remain the same
*
* @param {number} ratePerSecond - Maximum calls per second allowed.
* @param {number} [maxBatch=5] - Maximum batch size.
* @param {number} [delays=1000] - Fixed delay time for each batch (ms).
* @returns {{ concurrency: number, batchSize: number, delays: number }} An object containing concurrency, batchSize, and delays.
*/
export function createBatchRateConfig(
ratePerSecond: number,
maxBatch = 5,
delays = 1000,
): { concurrency: number; batchSize: number; delays: number } {
if (ratePerSecond < 1) throw new Error('ratePerSecond must be >= 1');
if (maxBatch < 1) throw new Error('maxBatch must be >= 1');
//if (delays < 1) throw new Error('delays must be >= 1');
const _delays = delays > 1000 ? delays : 1000;
const ratePerBatch = ratePerSecond * (_delays / 1000);
// Highest batch not to exceed ratePerBatch and maxBatch
const batch = Math.min(maxBatch, Math.floor(ratePerBatch));
// At least 1
const safeBatch = Math.max(batch, 1);
// Maximum possible batchSize for this concurrency (>=1)
const concurrency = Math.max(1, Math.floor(ratePerBatch / safeBatch));
return {
concurrency,
batchSize: safeBatch,
delays,
};
}
/**
* Creates an array of block tag ranges for batching.
* @param {number} fromBlock - First block.
* @param {number} toBlock - Last block.
* @param {number} [batchSize=1000] - Number of blocks per batch.
* @param {boolean} [reverse=false] - If true, returns ranges in reverse order.
* @returns {Array<{fromBlock: number, toBlock: number}>} Array of objects specifying the range for each batch.
* @throws {Error} If the block range is invalid.
*/
export function createBlockTags(
fromBlock: number,
toBlock: number,
batchSize = 1000,
reverse = false,
): { fromBlock: number; toBlock: number }[] {
const batches: { fromBlock: number; toBlock: number }[] = [];
if (toBlock - fromBlock > batchSize) {
for (let i = fromBlock; i < toBlock + 1; i += batchSize) {
const j = i + batchSize - 1 > toBlock ? toBlock : i + batchSize - 1;
batches.push({ fromBlock: i, toBlock: j });
}
} else if (toBlock - fromBlock >= 0) {
batches.push({ fromBlock, toBlock });
} else {
throw new Error(`Invalid block range ${fromBlock}~${toBlock}`);
}
if (reverse) {
batches.reverse();
}
return batches;
}
/**
* Generates a range of numbers (inclusive).
* @param {number} start - First value.
* @param {number} stop - Last value.
* @param {number} [step=1] - Increment.
* @returns {number[]} Array containing the generated range.
*/
export function range(start: number, stop: number, step = 1): number[] {
return Array(Math.ceil((stop - start) / step) + 1)
.fill(start)
.map((x, y) => x + y * step);
}
/**
* Splits an array into chunks of a given size.
* @param {T[]} arr - Array to split.
* @param {number} size - Maximum size of each chunk.
* @returns {T[][]} An array of arrays, each with up to 'size' elements.
* @template T
*/
export function chunk<T>(arr: T[], size: number): T[][] {
return [...Array(Math.ceil(arr.length / size))].map((_, i) => arr.slice(size * i, size + size * i));
}
/**
* Returns a promise resolved after the specified duration.
* @param {number} ms - Milliseconds to sleep.
* @returns {Promise<void>} Promise that resolves after the delay.
*/
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Node/browser-compatible cryptography interface.
*/
export const crypto = isNode ? webcrypto : (globalThis.crypto as typeof webcrypto);
/**
* Performs a digest (SHA-256 or other) of a byte array.
* @param {Uint8Array} bytes - Input bytes.
* @param {string} [algorithm='SHA-256'] - Algorithm name.
* @returns {Promise<Uint8Array>} Digest as a Uint8Array.
*/
export async function digest(bytes: Uint8Array, algorithm = 'SHA-256'): Promise<Uint8Array> {
return new Uint8Array(await crypto.subtle.digest(algorithm, bytes));
}
/**
* Hashes a hex string to another hex string digest.
* @param {string} hexStr - Input hex string.
* @param {string} [algorithm='SHA-256'] - Algorithm to use.
* @returns {Promise<string>} Hex string (with 0x) of the digest.
*/
export async function digestHex(hexStr: string, algorithm = 'SHA-256'): Promise<string> {
return bytesToHex(await digest(hexToBytes(hexStr), algorithm));
}
/**
* Generates a cryptographically random byte buffer.
* @param {number} [length=32] - Number of bytes.
* @returns {Uint8Array} Randomly generated bytes.
*/
export function rBytes(length = 32): Uint8Array {
return crypto.getRandomValues(new Uint8Array(length));
}
/**
* Converts Node.js Buffer to Uint8Array.
* @param {Buffer} b - Node.js Buffer.
* @returns {Uint8Array} Converted Uint8Array.
*/
export function bufferToBytes(b: Buffer): Uint8Array {
return Uint8Array.from(b);
}
/**
* Concatenates multiple Uint8Arrays into one.
* @param {...Uint8Array[]} arrays - Arrays to concatenate.
* @returns {Uint8Array} New concatenated Uint8Array.
*/
export function concatBytes(...arrays: Uint8Array[]): Uint8Array {
const totalSize = arrays.reduce((acc, e) => acc + e.length, 0);
const merged = new Uint8Array(totalSize);
arrays.forEach((array, i, arrays) => {
const offset = arrays.slice(0, i).reduce((acc, e) => acc + e.length, 0);
merged.set(array, offset);
});
return merged;
}
/**
* Converts a 0x-prefixed hex string or bigint to a Uint8Array.
* @param {bigint | string} input - The input hex string or bigint.
* @returns {Uint8Array} The bytes.
*/
export function hexToBytes(input: bigint | string): Uint8Array {
let hex: string = typeof input === 'bigint' ? input.toString(16) : input;
if (hex.startsWith('0x')) {
hex = hex.slice(2);
}
if (hex.length % 2 !== 0) {
hex = '0' + hex;
}
return Uint8Array.from((hex.match(/.{1,2}/g) as string[]).map((byte) => parseInt(byte, 16)));
}
/**
* Converts a Uint8Array to a 0x-prefixed hex string.
* @param {Uint8Array} bytes - Input bytes.
* @returns {string} Hex string of the bytes, 0x-prefixed.
*/
export function bytesToHex(bytes: Uint8Array): string {
return (
'0x' +
Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
);
}
/**
* Pads to even-length hex string and ensures 0x prefix.
* @param {string} hexStr - Hex string with or without prefix.
* @returns {string} 0x-prefixed, even-length hex string.
*/
export function toEvenHex(hexStr: string) {
if (hexStr.startsWith('0x')) {
hexStr = hexStr.slice(2);
}
if (hexStr.length % 2 !== 0) {
hexStr = '0' + hexStr;
}
return '0x' + hexStr;
}
/**
* Converts a bigint/number/string into a 0x-prefixed, fixed-length-zero-padded hex string.
* @param {bigint | number | string} numberish - The number, bigint, or numeric string.
* @param {number} [length=32] - Number of bytes in output.
* @returns {string} Fixed-length, 0x-prefixed hex string.
*/
export function toFixedHex(numberish: bigint | number | string, length = 32): string {
return (
'0x' +
BigInt(numberish)
.toString(16)
.padStart(length * 2, '0')
);
}
/**
* Base64
*/
/**
* Converts a base64 string to a Uint8Array.
* @param {string} base64 - Input base64 string.
* @returns {Uint8Array} Decoded bytes as a Uint8Array.
*/
export function base64ToBytes(base64: string): Uint8Array {
return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
}
/**
* Converts bytes to a base64 string.
* @param {Uint8Array} bytes - Bytes to encode.
* @returns {string} Base64-encoded string.
*/
export function bytesToBase64(bytes: Uint8Array): string {
return btoa(bytes.reduce((data, byte) => data + String.fromCharCode(byte), ''));
}
/**
* Converts a base64-encoded string to a 0x-prefixed hex string.
* @param {string} base64 - Base64 string.
* @returns {string} Hex string representation.
*/
export function base64ToHex(base64: string): string {
return bytesToHex(base64ToBytes(base64));
}
/**
* Converts a 0x-prefixed hex string to a base64 string.
* @param {string} hex - Input hex string, prefixed or not.
* @returns {string} Base64-encoded version.
*/
export function hexToBase64(hex: string): string {
return bytesToBase64(hexToBytes(hex));
}
/**
* Returns true if the string is a valid 0x-prefixed hex representation.
* @param {string} value - String to check.
* @returns {boolean} True if valid hex, false otherwise.
*/
export function isHex(value: string) {
return /^0x[0-9a-fA-F]*$/.test(value);
}