Skip to content

Commit

Permalink
Update Hardhat config to remove the need for boilerplate code (#44)
Browse files Browse the repository at this point in the history
* Clean up hardhat config generation

* Cleaning up

* Use process.env.MNEMONIC if it exists for networks config

* Ensure hardhat function doesn't throw in non-Node environment

* PR feedback

* Rename exposed API

* Update README

* Final review
  • Loading branch information
andreogle authored Jul 5, 2023
1 parent 1b881fd commit 3574b15
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 78 deletions.
35 changes: 19 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -53,7 +54,7 @@ A static array of `Chain` objects.
```ts
import { CHAINS } from '@api3/chains';
console.log(CHAINS);
/*
/*
[
{
name: 'Arbitrum testnet',
Expand All @@ -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": {
Expand All @@ -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: {
Expand All @@ -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',
...
]
*/
Expand Down
70 changes: 70 additions & 0 deletions src/hardhat-config.ts
Original file line number Diff line number Diff line change
@@ -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);
}

55 changes: 2 additions & 53 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
22 changes: 13 additions & 9 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,23 @@ export type ChainExplorer = z.infer<typeof chainExplorerSchema>;
export type ChainExplorerAPI = z.infer<typeof chainExplorerAPISchema>;
export type ChainExplorerAPIKey = z.infer<typeof chainExplorerAPIKeySchema>;

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[];
}

40 changes: 40 additions & 0 deletions src/utils/strings.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
9 changes: 9 additions & 0 deletions src/utils/strings.ts
Original file line number Diff line number Diff line change
@@ -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();
}

0 comments on commit 3574b15

Please sign in to comment.