diff --git a/README.md b/README.md index 9b656e40..57a648b4 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,13 @@ - [hardhat-etherscan](#hardhat-etherscan) - [API](#api) - [CHAINS](#chains) - - [hardhatConfigNetworks](#hardhatconfignetworks) - - [hardhatEtherscan](#hardhatetherscan) - - [getEnvVariables](#getenvvariables) + - [hardhatConfig.networks()](#hardhatconfignetworks) + - [hardhatConfig.etherscan()](#hardhatconfigetherscan) + - [hardhatConfig.getEnvVariableNames()](#hardhatconfiggetenvvariablenames) - [Types](#types) - [Scripts](#scripts) - [generate:chains](#generatechains) + - [providers:ping](#providersping) - [Development](#development) - [Validation](#validation) - [Building](#building) @@ -53,7 +54,7 @@ A static array of `Chain` objects. ```ts import { CHAINS } from '@api3/chains'; console.log(CHAINS); -/* +/* [ { name: 'Arbitrum testnet', @@ -62,17 +63,17 @@ console.log(CHAINS); ... }, ... -] +] */ ``` -### hardhatConfigNetworks +### hardhatConfig.networks() Returns an object where the key is each chain's alias and the value is an object that can be used as the `networks` field of [`hardhat.config.js`](https://hardhat.org/hardhat-runner/docs/config). ```ts -import { hardhatConfigNetworks } from '@api3/chains'; -console.log(hardhatConfigNetworks()); +import { hardhatConfig } from '@api3/chains'; +console.log(hardhatConfig.networks()); /* { "arbitrum-goerli-testnet": { @@ -85,13 +86,13 @@ console.log(hardhatConfigNetworks()); */ ``` -### hardhatEtherscan +### hardhatConfig.etherscan() Returns an object where the key is each chain's alias and the value is an object that can be used as the `etherscan` field of [`hardhat.config.js`](https://hardhat.org/hardhat-runner/docs/config) (requires the [`hardhat-etherscan` plugin](https://hardhat.org/hardhat-runner/plugins/nomiclabs-hardhat-etherscan)). ```ts -import { hardhatEtherscan } from '@api3/chains'; -console.log(hardhatEtherscan()); +import { hardhatConfig } from '@api3/chains'; +console.log(hardhatConfig.etherscan()); /* { apiKey: { @@ -104,17 +105,19 @@ console.log(hardhatEtherscan()); */ ``` -### getEnvVariables +### hardhatConfig.getEnvVariableNames() + +Returns an array of expected environment variable names for chains that have an API key required for the explorer. The array also includes a single `MNEMONIC` variable that can be used to configure all networks. -Returns an array of expected environment variable names for chains that have an API key required for the explorer. +NOTE: Each `ETHERSCAN_API_KEY_` environment variable has the chain alias as a suffix, where the alias has been converted to upper snake case. ```ts -import { getEnvVariables } from '@api3/chains'; -console.log(getEnvVariables()); +import { hardhatConfig } from '@api3/chains'; +console.log(hardhatConfig.getEnvVariableNames()); /* [ 'MNEMONIC', - 'ETHERSCAN_API_KEY_arbitrum-goerli-testnet', + 'ETHERSCAN_API_KEY_ARBITRUM_GOERLI_TESTNET', ... ] */ diff --git a/src/hardhat-config.ts b/src/hardhat-config.ts new file mode 100644 index 00000000..d8002882 --- /dev/null +++ b/src/hardhat-config.ts @@ -0,0 +1,70 @@ +import { CHAINS } from './generated/chains'; +import { toUpperSnakeCase } from './utils/strings'; +import { Chain, HardhatEtherscanConfig, HardhatNetworksConfig } from './types'; + +export function getEnvVariableNames(): string[] { + const hardhatApiKeyEnvNames = CHAINS.filter((chain) => chain.explorer?.api?.key?.required).map((chain) => + etherscanApiKeyName(chain) + ); + + return ['MNEMONIC', ...hardhatApiKeyEnvNames]; +} + +export function etherscanApiKeyName(chain: Chain): string { + return `ETHERSCAN_API_KEY_${toUpperSnakeCase(chain.alias)}`; +} + +// https://hardhat.org/hardhat-runner/plugins/nomicfoundation-hardhat-verify#multiple-api-keys-and-alternative-block-explorers +export function etherscan(): HardhatEtherscanConfig { + if (typeof window !== 'undefined') { + throw new Error('Cannot be run outside of a Node.js environment'); + } + + return CHAINS.reduce( + (etherscan, chain) => { + if (!chain.explorer || !chain.explorer.api) { + return etherscan; + } + + const apiKey = chain.explorer.api.key; + + const apiKeyEnvName = etherscanApiKeyName(chain); + const apiKeyValue = apiKey.required ? process.env[apiKeyEnvName] || 'NOT_FOUND' : 'DUMMY_VALUE'; + + if (apiKey.hardhatEtherscanAlias) { + etherscan.apiKey[apiKey.hardhatEtherscanAlias] = apiKeyValue; + return etherscan; + } + + etherscan.customChains.push({ + network: chain.alias, + chainId: Number(chain.id), + urls: { + apiURL: chain.explorer.api.url, + browserURL: chain.explorer.browserUrl, + }, + }); + + etherscan.apiKey[chain.alias] = apiKeyValue; + + return etherscan; + }, + { apiKey: {}, customChains: [] } as HardhatEtherscanConfig + ); +} + +export function networks(): HardhatNetworksConfig { + if (typeof window !== 'undefined') { + throw new Error('Cannot be called outside of a Node.js environment'); + } + + return CHAINS.reduce((networks, chain) => { + networks[chain.alias] = { + accounts: { mnemonic: process.env.MNEMONIC || '' }, + chainId: Number(chain.id), + url: chain.providerUrl, + }; + return networks; + }, {} as HardhatNetworksConfig); +} + diff --git a/src/index.ts b/src/index.ts index 3e8da6cc..63878c24 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,56 +1,5 @@ -import { CHAINS } from './generated/chains'; -import { HardhatConfigNetworks, HardhatEtherscanNetworks } from './types'; - -export * from './types'; - // NOTE: the following file is generated with the generate-chains.ts script export { CHAINS } from './generated/chains'; -export function hardhatConfigNetworks(): HardhatConfigNetworks { - return CHAINS.reduce((networks, chain) => { - networks[chain.alias] = { - accounts: { mnemonic: '' }, - chainId: Number(chain.id), - url: chain.providerUrl, - }; - return networks; - }, {} as HardhatConfigNetworks); -} - -export function hardhatEtherscan(): HardhatEtherscanNetworks { - return CHAINS.reduce((etherscan, chain) => { - if (!chain.explorer || !chain.explorer.api) { - return etherscan; - } - - const apiKey = chain.explorer.api.key; - const explorer = chain.explorer; - const apiKeyValue = apiKey.required ? chain.alias : "DUMMY_VALUE"; - - if (apiKey.hardhatEtherscanAlias) { - etherscan.apiKey[apiKey.hardhatEtherscanAlias] = apiKeyValue; - return etherscan; - } - - etherscan.customChains.push({ - network: chain.alias, - chainId: Number(chain.id), - urls: { - apiURL: explorer.api!.url, - browserURL: explorer.browserUrl, - }, - }); - etherscan.apiKey[chain.alias] = apiKeyValue; - - return etherscan; - }, { apiKey: {}, customChains: [] } as HardhatEtherscanNetworks); -} - -export function getEnvVariables(): string[] { - const keys = CHAINS - .filter((chain) => chain.explorer?.api?.key?.required) - .map((chain) => `ETHERSCAN_API_KEY_${chain.alias}`); - - return ['MNEMONIC', ...keys]; -} - +export * as hardhatConfig from './hardhat-config'; +export * from './types'; diff --git a/src/types.ts b/src/types.ts index 9675b5d9..4ee7a58a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -33,19 +33,23 @@ export type ChainExplorer = z.infer; export type ChainExplorerAPI = z.infer; export type ChainExplorerAPIKey = z.infer; -export interface HardhatConfigNetworks { +export interface HardhatNetworksConfig { [key: string]: { - accounts: { mnemonic: '' }; + accounts: { mnemonic: string }; chainId: number; url: string; } } -export interface HardhatEtherscanNetworks { - apiKey: { [etherscanAlias: string]: string; } - customChains: { - network: string; - chainId: number; - urls: { apiURL: string; browserURL: string; } - }[] +// https://hardhat.org/hardhat-runner/plugins/nomicfoundation-hardhat-verify#adding-support-for-other-networks +export interface HardhatEtherscanCustomChain { + network: string; + chainId: number; + urls: { apiURL: string; browserURL: string; } +} + +export interface HardhatEtherscanConfig { + apiKey: { [alias: string]: string; } + customChains: HardhatEtherscanCustomChain[]; } + diff --git a/src/utils/strings.test.ts b/src/utils/strings.test.ts new file mode 100644 index 00000000..d946cccc --- /dev/null +++ b/src/utils/strings.test.ts @@ -0,0 +1,40 @@ +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; +import { toUpperSnakeCase } from './strings'; + +describe('toUpperSnakeCase', () => { + test('converts simple words', () => { + const result = toUpperSnakeCase('hello world'); + assert.equal(result, 'HELLO_WORLD'); + }); + + test('keeps numbers in the string', () => { + const result = toUpperSnakeCase('hello world 4'); + assert.equal(result, 'HELLO_WORLD_4'); + }); + + test('trims leading and trailing whtestespaces', () => { + const result = toUpperSnakeCase(' hello world '); + assert.equal(result, 'HELLO_WORLD'); + }); + + test('converts special characters to underscores', () => { + const result = toUpperSnakeCase('hello, world!'); + assert.equal(result, 'HELLO_WORLD'); + }); + + test('converts multiple spaces to single underscores', () => { + const result = toUpperSnakeCase('hello world'); + assert.equal(result, 'HELLO_WORLD'); + }); + + test('returns an empty string when given an empty string', () => { + const result = toUpperSnakeCase(''); + assert.equal(result, ''); + }); + + test('converts mixed case strings', () => { + const result = toUpperSnakeCase('Hello World'); + assert.equal(result, 'HELLO_WORLD'); + }); +}); diff --git a/src/utils/strings.ts b/src/utils/strings.ts new file mode 100644 index 00000000..8d2580d9 --- /dev/null +++ b/src/utils/strings.ts @@ -0,0 +1,9 @@ +export function toUpperSnakeCase(input: string): string { + return input + .trim() + .replace(/\s+/g, ' ') // replace multiple spaces with a single space + .replace(/[^a-zA-Z0-9\s]+/g, '') // remove all non-alphanumeric characters + .split(' ') + .join('_') + .toUpperCase(); +}