Skip to content

Commit

Permalink
Read Etherscan URLs from config file (#36)
Browse files Browse the repository at this point in the history
* Bump TypeChain version

* Downgrade ESLint to v7 because presets we use do not support v8 yet (parserOptions problem)

* Draft configuration reference in README

* Document etherscanKey in README

* Accept user provided etherscanURLs in config file

* Get rid of browserURL

* Improve README

* Make a comment more concise

* Make Features heading smaller

* Add changeset
  • Loading branch information
hasparus authored Nov 8, 2021
1 parent af9317b commit d00cfeb
Show file tree
Hide file tree
Showing 15 changed files with 191 additions and 128 deletions.
5 changes: 5 additions & 0 deletions .changeset/ten-humans-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@dethcrypto/eth-sdk': minor
---

Read custom Etherscan URLs from `"etherscanURLs"` property in config file
50 changes: 31 additions & 19 deletions packages/eth-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,36 +22,34 @@
- [`contracts`](#contracts)
- [`outputPath`](#outputpath)
- [`etherscanKey`](#etherscankey)
- [`etherscanURLs`](#etherscanurls)
- [Examples](#examples)
- [Motivation and use cases](#motivation-and-use-cases)
- [Configuration](#configuration-1)
- [State of the project](#state-of-the-project)

---

## Installation
# Installation

```
yarn add --dev @dethcrypto/eth-sdk @dethcrypto/eth-sdk-client
```

`eth-sdk` uses ethers.js and TypeScript, so these dependencies have to be installed as well.

## Usage
# Usage

```bash
eth-sdk [options]
```

### CLI Options
## CLI Options

Options:

- `-p, --path <path>` working directory (default: `./eth-sdk`)

`eth-sdk` looks for the config file in this directory, and saves downloaded ABIs there.

### Getting started
## Getting started

`eth-sdk` takes a JSON config file with ethereum addresses and generates a fully type-safe SDK that you can use right
away. The SDK is an object consisting of ethers.js contracts initialized with ABIs provided by etherscan and with types
Expand Down Expand Up @@ -108,7 +106,7 @@ main()
})
```

### Configuration
## Configuration

`eth-sdk` looks for a file named `config` or `eth-sdk.config` with `.ts`, `.json`, `.js` or `.cjs` extension inside of
the directory specified by `--path` CLI argument.
Expand All @@ -133,7 +131,7 @@ export default defineConfig({
})
```

#### `contracts`
### `contracts`

A map from network identifier into deeply nested key-value pairs of contract names and addresses.

Expand All @@ -150,7 +148,7 @@ A map from network identifier into deeply nested key-value pairs of contract nam
}
```

Supported network identifiers are:
Predefined network identifiers are:

```
"mainnet" "ropsten" "rinkeby"
Expand All @@ -161,7 +159,9 @@ Supported network identifiers are:
"arbitrumOne" "arbitrumTestnet"
```

#### `outputPath`
You can configure your own Etherscan URLs in [`etherscanURLs`](#etherscanurls).

### `outputPath`

Output directory for generated SDK.

Expand All @@ -173,7 +173,7 @@ Output directory for generated SDK.
}
```

#### `etherscanKey`
### `etherscanKey`

Etherscan API key.

Expand All @@ -185,7 +185,22 @@ Etherscan API key.
}
```

## Examples
### `etherscanURLs`

Key-value pairs of network identifier and Etherscan API URL to fetch ABIs from.

```json
{
"etherscanURLs": {
"helloworld": "https://api.etherscan.io/api"
},
"contracts": {
"helloworld": {}
}
}
```

# Examples

Check out examples of using `eth-sdk` in [`/examples`][examples] directory.

Expand All @@ -194,7 +209,7 @@ Check out examples of using `eth-sdk` in [`/examples`][examples] directory.

[examples]: https://github.com/dethcrypto/eth-sdk/tree/master/examples

## Motivation and use cases
# Motivation and use cases

The primary motivation for the project is reducing the ceremony needed to interact with smart contracts on Ethereum
while using JavaScript or TypeScript. It takes care of boring parts like ABI management and auto-generates all the
Expand All @@ -204,10 +219,7 @@ have type information so your IDE can assist you.
It works well with all sorts of scripts, backend services, and even frontend apps. Note: If you develop smart contracts
it's better to use TypeChain directly (especially via HardHat integration).

## Configuration

## State of the project
# State of the project

The project is in a very experimental stage. Don't hesitate to create an issue / pull request helping to steer the
vision. Particularly things like input configuration are not set in stone (how should JSON config look like? should we
support `.yml` etc)
vision.
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { expect, mockFn } from 'earljs'
import { constants } from 'ethers'

import { parseAddress, UserEtherscanURLs } from '../../config'
import { UserProvidedNetworkSymbol } from '../networks'
import { FetchAbi, getABIFromEtherscan } from './getAbiFromEtherscan'

describe(getABIFromEtherscan.name, () => {
it('fetches from predefined etherscan URL', async () => {
const apiKey = '{{ API_KEY }}'
const fetch = mockEndpoint()
const actual = await getABIFromEtherscan('mainnet', DAI_ADDRESS, apiKey, {}, fetch)

expect(actual).toEqual(RETURNED_ABI)
expect(fetch).toHaveBeenCalledWith([
`https://api.etherscan.io/api?module=contract&action=getabi&address=${DAI_ADDRESS}&apikey=${apiKey}`,
])
})

it('fetches from user-specified URL', async () => {
const symbol = UserProvidedNetworkSymbol('dethcryptoscan')
const apiKey = 'woop'

const userNetworks: UserEtherscanURLs = {
[symbol]: 'https://dethcryptoscan.test/api/v1',
}

const fetch = mockEndpoint()

const actual = await getABIFromEtherscan(symbol, ADDRESS_ZERO, apiKey, userNetworks, fetch)

expect(actual).toEqual(RETURNED_ABI)
expect(fetch).toHaveBeenCalledWith([
`https://dethcryptoscan.test/api/v1?module=contract&action=getabi&address=${ADDRESS_ZERO}&apikey=${apiKey}`,
])
})
})

const ADDRESS_ZERO = parseAddress(constants.AddressZero)
const DAI_ADDRESS = parseAddress('0x6B175474E89094C44Da98b954EedeAC495271d0F')

const RETURNED_ABI = ['{{ RETURNED_ABI }}']
function mockEndpoint() {
const fetch: FetchAbi = async (_url) => ({
body: JSON.stringify({
status: '1',
result: JSON.stringify(RETURNED_ABI),
}),
})
return mockFn(fetch)
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import got from 'got'

import { Address } from '../../config'
import { symbolToNetworkId } from '../networks'
import { EtherscanURLs, networkIDtoEndpoints } from './urls'

export async function getABIFromEtherscan(networkSymbol: string, address: Address, apiKey: string): Promise<any> {
const etherscanUrls = getEtherscanLinkFromNetworkSymbol(networkSymbol)
if (!etherscanUrls) {
import got, { Response } from 'got'

import type { Address } from '../../config'
import type { URLString } from '../../utils/utility-types'
import { NetworkSymbol, symbolToNetworkId, UserProvidedNetworkSymbol } from '../networks'
import { networkIDtoEndpoints, UserEtherscanURLs } from './urls'

export async function getABIFromEtherscan(
networkSymbol: NetworkSymbol,
address: Address,
apiKey: string,
userNetworks: UserEtherscanURLs,
fetch: FetchAbi = got,
): Promise<object> {
const apiUrl = getEtherscanLinkFromNetworkSymbol(networkSymbol, userNetworks)
if (!apiUrl) {
throw new Error(`Can't find network info for ${networkSymbol}`)
}

const url = `${etherscanUrls.apiURL}?module=contract&action=getabi&address=${address}&apikey=${apiKey}`
const rawResponse = await got(url)
const url = `${apiUrl}?module=contract&action=getabi&address=${address}&apikey=${apiKey}`
const rawResponse = await fetch(url)
// @todo error handling for incorrect api keys
const jsonResponse = JSON.parse(rawResponse.body)

Expand All @@ -24,13 +31,25 @@ export async function getABIFromEtherscan(networkSymbol: string, address: Addres
return abi
}

function getEtherscanLinkFromNetworkSymbol(networkSymbol: string): EtherscanURLs | undefined {
const networkId = symbolToNetworkId[networkSymbol]
if (networkId === undefined) {
return undefined
/** @internal exported for tests only */
export type FetchAbi = (url: string) => Promise<Pick<Response<string>, 'body'>>

function getEtherscanLinkFromNetworkSymbol(
networkSymbol: NetworkSymbol,
userNetworks: UserEtherscanURLs,
): URLString | undefined {
if (isUserProvidedNetwork(networkSymbol, userNetworks)) {
return userNetworks[networkSymbol]
}

const etherscanUrls = networkIDtoEndpoints[networkId]
const networkId = symbolToNetworkId[networkSymbol]

return networkId && networkIDtoEndpoints[networkId]
}

return etherscanUrls
function isUserProvidedNetwork(
symbol: NetworkSymbol,
userNetworks: UserEtherscanURLs,
): symbol is UserProvidedNetworkSymbol {
return symbol in userNetworks
}
107 changes: 29 additions & 78 deletions packages/eth-sdk/src/abi-management/etherscan/urls.ts
Original file line number Diff line number Diff line change
@@ -1,82 +1,33 @@
import { NetworkID } from '../networks'
import type { URLString } from '../../utils/utility-types'
import { NetworkID, UserProvidedNetworkSymbol } from '../networks'

// note: copied from https://github.com/nomiclabs/hardhat/blob/master/packages/hardhat-etherscan/src/network/prober.ts
export interface EtherscanURLs {
apiURL: string
browserURL: string
}

type NetworkId2Etherscan = {
[networkID in NetworkID]: EtherscanURLs
}
type NetworkId2Etherscan = { [networkID in NetworkID]: URLString }

/**
* This object is adapted from hardhat-etherscan source.
* Refer to the following file to add new predefined networks:
*
* @see https://github.com/nomiclabs/hardhat/blob/master/packages/hardhat-etherscan/src/network/prober.ts
*/
export const networkIDtoEndpoints: NetworkId2Etherscan = {
[NetworkID.MAINNET]: {
apiURL: 'https://api.etherscan.io/api',
browserURL: 'https://etherscan.io',
},
[NetworkID.ROPSTEN]: {
apiURL: 'https://api-ropsten.etherscan.io/api',
browserURL: 'https://ropsten.etherscan.io',
},
[NetworkID.RINKEBY]: {
apiURL: 'https://api-rinkeby.etherscan.io/api',
browserURL: 'https://rinkeby.etherscan.io',
},
[NetworkID.GOERLI]: {
apiURL: 'https://api-goerli.etherscan.io/api',
browserURL: 'https://goerli.etherscan.io',
},
[NetworkID.KOVAN]: {
apiURL: 'https://api-kovan.etherscan.io/api',
browserURL: 'https://kovan.etherscan.io',
},
[NetworkID.BSC]: {
apiURL: 'https://api.bscscan.com/api',
browserURL: 'https://bscscan.com',
},
[NetworkID.BSC_TESTNET]: {
apiURL: 'https://api-testnet.bscscan.com/api',
browserURL: 'https://testnet.bscscan.com',
},
[NetworkID.HECO]: {
apiURL: 'https://api.hecoinfo.com/api',
browserURL: 'https://hecoinfo.com',
},
[NetworkID.HECO_TESTNET]: {
apiURL: 'https://api-testnet.hecoinfo.com/api',
browserURL: 'https://testnet.hecoinfo.com',
},
[NetworkID.OPERA]: {
apiURL: 'https://api.ftmscan.com/api',
browserURL: 'https://ftmscan.com',
},
[NetworkID.FTM_TESTNET]: {
apiURL: 'https://api-testnet.ftmscan.com/api',
browserURL: 'https://testnet.ftmscan.com',
},
[NetworkID.OPTIMISTIC_ETHEREUM]: {
apiURL: 'https://api-optimistic.etherscan.io/api',
browserURL: 'https://optimistic.etherscan.io/',
},
[NetworkID.OPTIMISTIC_KOVAN]: {
apiURL: 'https://api-kovan-optimistic.etherscan.io/api',
browserURL: 'https://kovan-optimistic.etherscan.io/',
},
[NetworkID.POLYGON]: {
apiURL: 'https://api.polygonscan.com/api',
browserURL: 'https://polygonscan.com',
},
[NetworkID.POLYGON_MUMBAI]: {
apiURL: 'https://api-testnet.polygonscan.com/api',
browserURL: 'https://mumbai.polygonscan.com/',
},
[NetworkID.ARBITRUM_ONE]: {
apiURL: 'https://api.arbiscan.io/api',
browserURL: 'https://arbiscan.io/',
},
[NetworkID.ARBITRUM_TESTNET]: {
apiURL: 'https://api-testnet.arbiscan.io/api',
browserURL: 'https://testnet.arbiscan.io/',
},
[NetworkID.MAINNET]: 'https://api.etherscan.io/api',
[NetworkID.ROPSTEN]: 'https://api-ropsten.etherscan.io/api',
[NetworkID.RINKEBY]: 'https://api-rinkeby.etherscan.io/api',
[NetworkID.GOERLI]: 'https://api-goerli.etherscan.io/api',
[NetworkID.KOVAN]: 'https://api-kovan.etherscan.io/api',
[NetworkID.BSC]: 'https://api.bscscan.com/api',
[NetworkID.BSC_TESTNET]: 'https://api-testnet.bscscan.com/api',
[NetworkID.HECO]: 'https://api.hecoinfo.com/api',
[NetworkID.HECO_TESTNET]: 'https://api-testnet.hecoinfo.com/api',
[NetworkID.OPERA]: 'https://api.ftmscan.com/api',
[NetworkID.FTM_TESTNET]: 'https://api-testnet.ftmscan.com/api',
[NetworkID.OPTIMISTIC_ETHEREUM]: 'https://api-optimistic.etherscan.io/api',
[NetworkID.OPTIMISTIC_KOVAN]: 'https://api-kovan-optimistic.etherscan.io/api',
[NetworkID.POLYGON]: 'https://api.polygonscan.com/api',
[NetworkID.POLYGON_MUMBAI]: 'https://api-testnet.polygonscan.com/api',
[NetworkID.ARBITRUM_ONE]: 'https://api.arbiscan.io/api',
[NetworkID.ARBITRUM_TESTNET]: 'https://api-testnet.arbiscan.io/api',
}

export interface UserEtherscanURLs extends Record<UserProvidedNetworkSymbol, URLString> {}
export interface UserEtherscanURLsInput extends Record<string, URLString> {}
6 changes: 3 additions & 3 deletions packages/eth-sdk/src/abi-management/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { expect, mockFn } from 'earljs'
import { mockFilesystem } from '../../test/filesystemMock'
import { parseAddress } from '../config'
import { EthSdkCtx } from '../types'
import { gatherABIs } from './index'
import { GetAbi } from './types'
import { gatherABIs, GetAbi } from './index'

const fs = mockFilesystem({})

Expand All @@ -15,7 +14,7 @@ describe(gatherABIs.name, () => {

expect(fs.test.isDirectory('outputPath/abis/kovan')).toEqual(true)
expect(fs.test.readJson('outputPath/abis/kovan/dai.json')).toEqual(abiFixtures)
expect(getAbiMock).toHaveBeenCalledWith(['kovan', contractsFixture.kovan.dai, etherscanKeyFixture])
expect(getAbiMock).toHaveBeenCalledWith(['kovan', contractsFixture.kovan.dai, etherscanKeyFixture, {}])
})
})

Expand Down Expand Up @@ -46,6 +45,7 @@ const ctxFixture: EthSdkCtx = {
outputPath: 'outputPath',
contracts: contractsFixture,
etherscanKey: etherscanKeyFixture,
etherscanURLs: {},
},
fs,
}
Expand Down
Loading

0 comments on commit d00cfeb

Please sign in to comment.