diff --git a/README.md b/README.md index 5db1fbf947..2210e49ef4 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,7 @@ A special thanks to these services for providing community resources: - [Etherscan](https://etherscan.io/) - [INFURA](https://infura.io/) - [Alchemy](https://dashboard.alchemyapi.io/signup?referral=55a35117-028e-4b7c-9e47-e275ad0acc6d) +- [Histori](https://docs.histori.xyz/docs/archive-node/eth-block-number) Extension Packages diff --git a/docs.wrm/links/projects.txt b/docs.wrm/links/projects.txt index 33faa53b16..dcf6caeb86 100644 --- a/docs.wrm/links/projects.txt +++ b/docs.wrm/links/projects.txt @@ -22,6 +22,7 @@ link-react-native [React Native](https://reactnative.dev) link-semver [semver](https://semver.org) link-solidity [Solidity](https://solidity.readthedocs.io/) link-tally [Tally](https://tallyho.org) +link-histori [https://docs.histori.xyz/docs/archive-node-info/intro] # Project-specific link-alchemy-signup [Alchemy Signup](https://dashboard.alchemyapi.io/signup?referral=55a35117-028e-4b7c-9e47-e275ad0acc6d) @@ -30,6 +31,7 @@ link-ankr-signup [link-ankr-premium](https://www.ankr.com/protocol/plan/) link-etherscan-signup [Etherscan Signup](https://etherscan.io/apis) link-etherscan-ratelimit [link-etherscan-ratelimit](https://info.etherscan.com/api-return-errors/) link-infura-signup [INFURA Signup](https://infura.io/register) +link-histori-signup [Histori Signup](https://histori.xyz/signin) link-geth-debug [link-geth-debug](https://github.com/ethereum/go-ethereum/wiki/Management-APIs#debug) link-geth-rpc [link-geth-rpc](https://github.com/ethereum/go-ethereum/wiki/Management-APIs) link-infura-secret [link-infura-secret](https://infura.io/docs/gettingStarted/authentication) diff --git a/src.ts/providers/default-provider.ts b/src.ts/providers/default-provider.ts index 6d724b24d0..4af9d457cc 100644 --- a/src.ts/providers/default-provider.ts +++ b/src.ts/providers/default-provider.ts @@ -9,6 +9,7 @@ import { EtherscanProvider } from "./provider-etherscan.js"; import { InfuraProvider } from "./provider-infura.js"; //import { PocketProvider } from "./provider-pocket.js"; import { QuickNodeProvider } from "./provider-quicknode.js"; +import { HistoriProvider } from "./provider-histori.js"; import { FallbackProvider } from "./provider-fallback.js"; import { JsonRpcProvider } from "./provider-jsonrpc.js"; @@ -54,6 +55,7 @@ const Testnets = "goerli kovan sepolia classicKotti optimism-goerli arbitrum-goe * - ``"infura"`` * - ``"publicPolygon"`` * - ``"quicknode"`` + * - ``"histori"`` * * @example: * // Connect to a local Geth node @@ -63,11 +65,11 @@ const Testnets = "goerli kovan sepolia classicKotti optimism-goerli arbitrum-goe * // third-party services available * provider = getDefaultProvider("mainnet"); * - * // Connect to Polygon, but only allow Etherscan and - * // INFURA and use "MY_API_KEY" in calls to Etherscan. + * // Connect to Polygon, but only allow Etherscan, + * // INFURA and Histori and use "MY_API_KEY" in calls to Etherscan. * provider = getDefaultProvider("matic", { * etherscan: "MY_API_KEY", - * exclusive: [ "etherscan", "infura" ] + * exclusive: [ "etherscan", "infura", "histori" ] * }); */ export function getDefaultProvider(network?: string | Networkish | WebSocketLike, options?: any): AbstractProvider { @@ -150,6 +152,13 @@ export function getDefaultProvider(network?: string | Networkish | WebSocketLike providers.push(new InfuraProvider(network, projectId, projectSecret)); } catch (error) { } } + + if (allowService("histori")) { + try { + let projectId = options.histori; + providers.push(new HistoriProvider(network, projectId)); + } catch (error) { } + } /* if (options.pocket !== "-") { try { diff --git a/src.ts/providers/index.ts b/src.ts/providers/index.ts index 47316dd2a1..d08db5217d 100644 --- a/src.ts/providers/index.ts +++ b/src.ts/providers/index.ts @@ -67,6 +67,7 @@ export { EtherscanProvider, EtherscanPlugin } from "./provider-etherscan.js"; export { InfuraProvider, InfuraWebSocketProvider } from "./provider-infura.js"; export { PocketProvider } from "./provider-pocket.js"; export { QuickNodeProvider } from "./provider-quicknode.js"; +export { HistoriProvider } from "./provider-histori.js"; import { IpcSocketProvider } from "./provider-ipcsocket.js"; /*-browser*/ export { IpcSocketProvider }; diff --git a/src.ts/providers/provider-histori.ts b/src.ts/providers/provider-histori.ts new file mode 100644 index 0000000000..05f3a25ae1 --- /dev/null +++ b/src.ts/providers/provider-histori.ts @@ -0,0 +1,270 @@ +/** + * [[link-histori]] provides a third-party service for connecting to + * various blockchains over JSON-RPC. + * + * **Supported Networks** + * + * - Ethereum Mainnet (``mainnet``) + * - Polygon PoS Mainnet (``matic``) + * - zkSync Era Mainnet (``zksync``) + * - Ethereum Sepolia Testnet (``sepolia``) + * - Optimism Mainnet (``optimism``) + * - Arbitrum Mainnet (``arbitrum``) + * - Base (``base``) + * - Polygon zkEVM Mainnet (``polygon-zkevm``) + * - Binance Smart Chain Mainnet (BSC) (``bnb``) + * - Avalanche Mainnet (``avalanche``) + * - ApeChain Mainnet (``apechain``) + * - Linea Mainnet (``linea``) + * - Scroll Mainnet (``scroll``) + * - Blast Mainnet (``blast``) + * - Mantle Mainnet (``mantle``) + * - Celo Mainnet (``celo``) + * - WorldChain Mainnet (``worldchain``) + * - Palm Mainnet (``palm``) + * - Shape Mainnet (``shape``) + * - Geist Mainnet (``geist``) + * - Arbitrum Nova (``arbitrum-nova``) + * - Astar Mainnet (``astar``) + * - ZetaChain Mainnet (``zetachain``) + * - Fantom Mainnet (``fantom``) + * - Fraxtal Mainnet (``fraxtal``) + * - Berachain bArtio (``berachain``) + * - Zora Mainnet (``zora``) + * - Polynomial Mainnet (``polynomial``) + * - Flow EVM Mainnet (``flow-evm``) + * - Lens Testnet (``lens``) + * - Soneium Minato Testnet (``soneium-minato``) + * - Rootstock Mainnet (``rsk``) + * - Unichain Testnet (``unichain``) + * - Gnosis Mainnet (``xdai``) + * - Aurora Mainnet (``aurora``) + * - Kaia Mainnet (``kaia``) + * - Aleph Zero EVM Mainnet (``aleph-zero``) + * - Harmony (``harmony``) + * - Immutable zkEVM (``immutable``) + * - Kava Mainnet (``kava``) + * - Kroma Mainnet (``kroma``) + * - Lisk Mainnet (``lisk``) + * - Manta Pacific Mainnet (``manta``) + * - Metal L2 Mainnet (``metal-l2``) + * - Moonbeam Mainnet (``moonbeam``) + * - Neon Mainnet (``neon``) + * - Ronin Mainnet (Axie Infinity) (``ronin``) + * - Sei Mainnet (``sei``) + * - Taiko Mainnet (``taiko``) + * - Telos Mainnet (``telos``) + * - X Layer Mainnet (``xlayer``) + * - Zircuit Mainnet (``zircuit``) + * + * @_subsection: api/providers/thirdparty:Histori [providers-histori] + */ + +import { + defineProperties, + resolveProperties, + assert, + FetchRequest, + } from "../utils/index.js"; + + import { showThrottleMessage } from "./community.js"; + import { Network } from "./network.js"; + import { JsonRpcProvider } from "./provider-jsonrpc.js"; + + import type { + AbstractProvider, + PerformActionRequest, + } from "./abstract-provider.js"; + import type { CommunityResourcable } from "./community.js"; + import type { Networkish } from "./network.js"; + + const defaultProjectId = "8ry9f6t9dct1se2hlagxnd9n2a"; + + // see https://docs.histori.xyz/docs/networks + function resolveNetworkId(name: string): string { + // Combined mappings for shorthand and full names + const networkMapping: Record = { + mainnet: "eth-mainnet", // for backwards compatibility with current network namings + sepolia: "eth-sepolia", // for backwards compatibility with current network namings + arbitrum: "arbitrum-mainnet", // for backwards compatibility with current network namings + base: "base-mainnet", // for backwards compatibility with current network namings + matic: "matic-mainnet", + optimism: "optimism-mainnet", + bnb: "bsc-mainnet", + linea: "linea-mainnet", + xdai: "gnosis-mainnet", + "eth-mainnet": "eth-mainnet", + "matic-mainnet": "matic-mainnet", + "zksync-mainnet": "zksync-mainnet", + "eth-sepolia": "eth-sepolia", + "optimism-mainnet": "optimism-mainnet", + "arbitrum-mainnet": "arbitrum-mainnet", + "base-mainnet": "base-mainnet", + "polygon-zkevm-mainnet": "polygon-zkevm-mainnet", + "bsc-mainnet": "bsc-mainnet", + "avalanche-mainnet": "avalanche-mainnet", + "apechain-mainnet": "apechain-mainnet", + "linea-mainnet": "linea-mainnet", + "scroll-mainnet": "scroll-mainnet", + "blast-mainnet": "blast-mainnet", + "mantle-mainnet": "mantle-mainnet", + "celo-mainnet": "celo-mainnet", + "worldchain-mainnet": "worldchain-mainnet", + "palm-mainnet": "palm-mainnet", + "shape-mainnet": "shape-mainnet", + "geist-mainnet": "geist-mainnet", + "arbitrum-nova-mainnet": "arbitrum-nova-mainnet", + "astar-mainnet": "astar-mainnet", + "zetachain-mainnet": "zetachain-mainnet", + "fantom-mainnet": "fantom-mainnet", + "fraxtal-mainnet": "fraxtal-mainnet", + "berachain-testnet": "berachain-testnet", + "zora-mainnet": "zora-mainnet", + "polynomial-mainnet": "polynomial-mainnet", + "flow-evm-mainnet": "flow-evm-mainnet", + "lens-testnet": "lens-testnet", + "soneium-minato-testnet": "soneium-minato-testnet", + "rsk-mainnet": "rsk-mainnet", + "unichain-sepolia-testnet": "unichain-sepolia-testnet", + "gnosis-mainnet": "gnosis-mainnet", + "aurora-mainnet": "aurora-mainnet", + "kaia-mainnet": "kaia-mainnet", + "aleph-zero-mainnet": "aleph-zero-mainnet", + "harmony-mainnet": "harmony-mainnet", + "immutable-mainnet": "immutable-mainnet", + "kava-mainnet": "kava-mainnet", + "kroma-mainnet": "kroma-mainnet", + "lisk-mainnet": "lisk-mainnet", + "manta-mainnet": "manta-mainnet", + "metal-l2-mainnet": "metal-l2-mainnet", + "moonbeam-mainnet": "moonbeam-mainnet", + "neon-mainnet": "neon-mainnet", + "ronin-mainnet": "ronin-mainnet", + "sei-mainnet": "sei-mainnet", + "taiko-mainnet": "taiko-mainnet", + "telos-mainnet": "telos-mainnet", + "xlayer-mainnet": "xlayer-mainnet", + "zircuit-mainnet": "zircuit-mainnet", + }; + + // Perform a single lookup in the networkMapping + const resolvedNetworkId = networkMapping[name] ?? networkMapping[`${name}-mainnet`]; + + if (resolvedNetworkId) { + return resolvedNetworkId; + } + + // In order to not update this provider every time a new network is added, + // we optimistically assume the provided network is correct. + // In case an invalid on unsupported one is supplied, the Histori Gateway will return 400 + // with a helpful message + return name; + } + + /** + * The **HistoriProvider** connects to the [[link-histori]] + * JSON-RPC end-points. + * + * By default, a highly-throttled API key is used, which is + * appropriate for quick prototypes and simple scripts. To + * gain access to an increased rate-limit, it is highly + * recommended to [sign up here](link-histori-signup). + * + * @_docloc: api/providers/thirdparty + */ + export class HistoriProvider + extends JsonRpcProvider + implements CommunityResourcable + { + readonly projectId!: string; + + constructor(_network?: Networkish, projectId?: null | string) { + if (_network == null) { + _network = "mainnet"; + } + const network = Network.from(_network); + if (projectId == null) { + projectId = defaultProjectId; + } + + const request = HistoriProvider.getRequest(network, projectId); + super(request, network, { staticNetwork: network }); + + defineProperties(this, { projectId }); + } + + _getProvider(chainId: number): AbstractProvider { + try { + return new HistoriProvider(chainId, this.projectId); + } catch (error) {} + return super._getProvider(chainId); + } + + async _perform(req: PerformActionRequest): Promise { + // https://docs.alchemy.com/reference/trace-transaction + if (req.method === "getTransactionResult") { + const { trace, tx } = await resolveProperties({ + trace: this.send("trace_transaction", [req.hash]), + tx: this.getTransaction(req.hash), + }); + if (trace == null || tx == null) { + return null; + } + + let data: undefined | string; + let error = false; + try { + data = trace[0].result.output; + error = trace[0].error === "Reverted"; + } catch (error) {} + + if (data) { + assert( + !error, + "an error occurred during transaction executions", + "CALL_EXCEPTION", + { + action: "getTransactionResult", + data, + reason: null, + transaction: tx, + invocation: null, + revert: null, // @TODO + } + ); + return data; + } + + assert(false, "could not parse trace result", "BAD_DATA", { + value: trace, + }); + } + + return await super._perform(req); + } + + isCommunityResource(): boolean { + return this.projectId === defaultProjectId; + } + + static getRequest(network: Network, projectId?: string): FetchRequest { + if (projectId == null) { + projectId = defaultProjectId; + } + + const request = new FetchRequest( + `https:/\/node.histori.xyz/${resolveNetworkId(network.name)}/${projectId}` + ); + request.allowGzip = true; + + if (projectId === defaultProjectId) { + request.retryFunc = async (request, response, attempt) => { + showThrottleMessage("Histori"); + return true; + }; + } + + return request; + } + } + \ No newline at end of file