@unruggable/gateways
Version:
Trustless Ethereum Multichain CCIP-Read Gateway
120 lines (119 loc) • 4.19 kB
JavaScript
import { AbiCoder } from 'ethers/abi';
import { id as keccakStr } from 'ethers/hash';
export const ABI_CODER = AbiCoder.defaultAbiCoder();
// https://adraffy.github.io/keccak.js/test/demo.html#algo=keccak-256&s=&escape=1&encoding=utf8
// "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
export const NULL_CODE_HASH = keccakStr('');
export const EVM_BLOCKHASH_DEPTH = 256;
// TODO: make this a function of Chain
export const MAINNET_BLOCK_SEC = 12;
export const LATEST_BLOCK_TAG = 'latest';
// hex-prefixed w/o zero-padding
export function toUnpaddedHex(x) {
return '0x' + BigInt(x).toString(16);
}
// hex-prefixed left-pad w/truncation
export function toPaddedHex(x, width = 32) {
const i = x === '0x' ? 0n : BigInt.asUintN(width << 3, BigInt(x));
return '0x' + i.toString(16).padStart(width << 1, '0');
}
// manual polyfill: ES2024
export function withResolvers() {
let resolve;
let reject;
const promise = new Promise((ful, rej) => {
resolve = ful;
reject = rej;
});
return { promise, resolve, reject };
}
export function isBlockTag(x) {
return typeof x === 'string' && !x.startsWith('0x');
}
export async function fetchBlock(provider, relBlockTag = LATEST_BLOCK_TAG) {
if (!isBlockTag(relBlockTag)) {
let i = BigInt(relBlockTag);
if (i < 0)
i += await fetchBlockNumber(provider);
relBlockTag = toUnpaddedHex(i);
}
const json = await provider.send('eth_getBlockByNumber', [
relBlockTag,
false,
]);
if (!json)
throw new Error(`no block: ${relBlockTag}`);
return json;
}
export async function fetchBlockFromHash(provider, blockHash) {
const block = await provider.send('eth_getBlockByHash', [blockHash, false]);
if (!block)
throw new Error(`no blockhash: ${blockHash}`);
return block;
}
// avoid an rpc if possible
// use negative (-100) for offset from "latest" (#-100)
export async function fetchBlockNumber(provider, relBlockTag = LATEST_BLOCK_TAG) {
if (relBlockTag === LATEST_BLOCK_TAG) {
return BigInt(await provider.send('eth_blockNumber', []));
}
else if (isBlockTag(relBlockTag)) {
const info = await fetchBlock(provider, relBlockTag);
return BigInt(info.number);
}
else {
let i = BigInt(relBlockTag);
if (i < 0)
i += await fetchBlockNumber(provider);
return i;
}
}
// avoid an rpc if possible
// convert negative (-100) => absolute (#-100)
export async function fetchBlockTag(provider, relBlockTag = LATEST_BLOCK_TAG) {
return isBlockTag(relBlockTag)
? relBlockTag
: fetchBlockNumber(provider, relBlockTag);
}
export async function fetchStorage(provider, target, slot, relBlockTag = LATEST_BLOCK_TAG) {
const data = await provider.send('eth_getStorageAt', [
target,
toPaddedHex(slot),
relBlockTag,
]);
if (!data) {
throw new Error(`expected storage: ${target}<${toUnpaddedHex(slot)}>@${relBlockTag}`);
}
// i think i've seen "0x" before...
return data.length === 66 ? data : toPaddedHex(data);
}
export function isEthersError(err) {
return err instanceof Error && 'code' in err && 'shortMessage' in err;
}
export function isRevert(err) {
return isEthersError(err) && err.code === 'CALL_EXCEPTION';
}
export function isRPCError(err, ...code) {
return (isEthersError(err) &&
err.error instanceof Object &&
'code' in err.error &&
typeof err.error.code === 'number' &&
code.includes(err.error.code));
}
export function flattenErrors(err, stringify = stringifyError) {
const errors = [stringify(err)];
for (let e = err; e instanceof Error && e.cause; e = e.cause) {
errors.push(stringify(e.cause));
}
return errors.join(' <== ');
}
function stringifyError(err) {
if (isEthersError(err) && err.code === 'SERVER_ERROR') {
// this leaks api key via "requestUrl"
// https://github.com/ethers-io/ethers.js/blob/d2c9ca0e0fd15e7884bcaab7d5152d68662e3e43/src.ts/utils/fetch.ts#L953
return err.shortMessage;
}
else {
return String(err);
}
}