github.com/ethereum-optimism/optimism@v1.7.2/packages/contracts-ts/wagmi.config.ts (about)

     1  import { ContractConfig, defineConfig, Plugin } from '@wagmi/cli'
     2  import { actions, react } from '@wagmi/cli/plugins'
     3  import * as glob from 'glob'
     4  import { readFileSync, writeFileSync } from 'fs'
     5  import type { Abi, Address } from 'abitype'
     6  import { isDeepStrictEqual } from 'util'
     7  import { camelCase, constantCase } from 'change-case'
     8  
     9  /**
    10   * Predeployed contract addresses
    11   * In future it would be nice to have a json file in contracts bedrock be generated as source of truth
    12   * Keep this in sync with op-bindings/predeploys/addresses.go in meantime
    13   */
    14  const predeployContracts = {
    15    LegacyMessagePasser: {
    16      address: '0x4200000000000000000000000000000000000000',
    17      introduced: 'Legacy',
    18      deprecated: true,
    19      proxied: true,
    20    },
    21    DeployerWhitelist: {
    22      address: '0x4200000000000000000000000000000000000002',
    23      introduced: 'Legacy',
    24      deprecated: true,
    25      proxied: true,
    26    },
    27    LegacyERC20ETH: {
    28      address: '0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000',
    29      introduced: 'Legacy',
    30      deprecated: true,
    31      proxied: false,
    32    },
    33    WETH9: {
    34      address: '0x4200000000000000000000000000000000000006',
    35      introduced: 'Legacy',
    36      deprecated: false,
    37      proxied: false,
    38    },
    39    L2CrossDomainMessenger: {
    40      address: '0x4200000000000000000000000000000000000007',
    41      introduced: 'Legacy',
    42      deprecated: false,
    43      proxied: true,
    44    },
    45    L2StandardBridge: {
    46      address: '0x4200000000000000000000000000000000000010',
    47      introduced: 'Legacy',
    48      deprecated: false,
    49      proxied: true,
    50    },
    51    SequencerFeeVault: {
    52      address: '0x4200000000000000000000000000000000000011',
    53      introduced: 'Legacy',
    54      deprecated: false,
    55      proxied: true,
    56    },
    57    OptimismMintableERC20Factory: {
    58      address: '0x4200000000000000000000000000000000000012',
    59      introduced: 'Legacy',
    60      deprecated: false,
    61      proxied: true,
    62    },
    63    L1BlockNumber: {
    64      address: '0x4200000000000000000000000000000000000013',
    65      introduced: 'Legacy',
    66      deprecated: true,
    67      proxied: true,
    68    },
    69    GasPriceOracle: {
    70      address: '0x420000000000000000000000000000000000000F',
    71      introduced: 'Legacy',
    72      deprecated: false,
    73      proxied: true,
    74    },
    75    GovernanceToken: {
    76      address: '0x4200000000000000000000000000000000000042',
    77      introduced: 'Legacy',
    78      deprecated: false,
    79      proxied: false,
    80    },
    81    L1Block: {
    82      address: '0x4200000000000000000000000000000000000015',
    83      introduced: 'Bedrock',
    84      deprecated: false,
    85      proxied: true,
    86    },
    87    L2ToL1MessagePasser: {
    88      address: '0x4200000000000000000000000000000000000016',
    89      introduced: 'Bedrock',
    90      deprecated: false,
    91      proxied: true,
    92    },
    93    L2ERC721Bridge: {
    94      address: '0x4200000000000000000000000000000000000014',
    95      introduced: 'Legacy',
    96      deprecated: false,
    97      proxied: true,
    98    },
    99    OptimismMintableERC721Factory: {
   100      address: '0x4200000000000000000000000000000000000017',
   101      introduced: 'Bedrock',
   102      deprecated: false,
   103      proxied: true,
   104    },
   105    ProxyAdmin: {
   106      address: '0x4200000000000000000000000000000000000018',
   107      introduced: 'Bedrock',
   108      deprecated: false,
   109      proxied: true,
   110    },
   111    BaseFeeVault: {
   112      address: '0x4200000000000000000000000000000000000019',
   113      introduced: 'Bedrock',
   114      deprecated: false,
   115      proxied: true,
   116    },
   117    L1FeeVault: {
   118      address: '0x420000000000000000000000000000000000001a',
   119      introduced: 'Bedrock',
   120      deprecated: false,
   121      proxied: true,
   122    },
   123  } as const
   124  
   125  type DeploymentJson = {
   126    abi: Abi
   127    address: `0x${string}`
   128  }
   129  const chains = {
   130    1: 'mainnet',
   131    10: 'optimism-mainnet',
   132    5: 'goerli',
   133    420: 'optimism-goerli',
   134  } as const
   135  
   136  if (!glob.sync('node_modules/*').length) {
   137    throw new Error(
   138      'No node_modules found. Please run `pnpm install` before running this script'
   139    )
   140  }
   141  
   142  const deployments = {
   143    [1]: glob.sync(
   144      `node_modules/@eth-optimism/contracts-bedrock/deployments/${chains[1]}/*.json`
   145    ),
   146    [10]: glob.sync(
   147      `node_modules/@eth-optimism/contracts-bedrock/deployments/${chains[10]}/*.json`
   148    ),
   149    [5]: glob.sync(
   150      `node_modules/@eth-optimism/contracts-bedrock/deployments/${chains[5]}/*.json`
   151    ),
   152    [420]: glob.sync(
   153      `node_modules/@eth-optimism/contracts-bedrock/deployments/${chains[420]}/*.json`
   154    ),
   155  }
   156  Object.entries(deployments).forEach(([chain, deploymentFiles]) => {
   157    if (deploymentFiles.length === 0) {
   158      throw new Error(`No bedrock deployments found for ${chains[chain]}`)
   159    }
   160  })
   161  
   162  const getWagmiContracts = (
   163    deploymentFiles: string[],
   164    filterDuplicates = false
   165  ) =>
   166    deploymentFiles.map((artifactPath) => {
   167      const deployment = JSON.parse(
   168        readFileSync(artifactPath, 'utf8')
   169      ) as DeploymentJson
   170  
   171      // There is a known bug in the wagmi/cli repo where some contracts have FOO_CASE and fooCase in same contract causing issues
   172      // This is a common pattern at OP
   173      // @see https://github.com/wagmi-dev/wagmi/issues/2724
   174      const abi = filterDuplicates
   175        ? deployment.abi.filter((item) => {
   176            if (item.type !== 'function') {
   177              return true
   178            }
   179            if (item.name !== constantCase(item.name)) {
   180              return true
   181            }
   182            // if constante case make sure it is not a duplicate
   183            // e.g. make sure fooBar doesn't exist with FOO_BAR
   184            return !deployment.abi.some(
   185              (otherItem) =>
   186                otherItem.type === 'function' &&
   187                otherItem.name !== item.name &&
   188                otherItem.name === camelCase(item.name)
   189            )
   190          })
   191        : deployment.abi
   192      const contractConfig = {
   193        abi,
   194        name: artifactPath.split('/').reverse()[0]?.replace('.json', ''),
   195        address: deployment.address,
   196      } satisfies ContractConfig
   197      if (!contractConfig.name) {
   198        throw new Error(
   199          'Unable to identify the name of the contract at ' + artifactPath
   200        )
   201      }
   202      return contractConfig
   203    })
   204  
   205  /**
   206   * Returns the contracts for the wagmi cli config
   207   */
   208  const getContractConfigs = (filterDuplicates = false) => {
   209    const contracts = {
   210      1: getWagmiContracts(deployments[1], filterDuplicates),
   211      10: getWagmiContracts(deployments[10], filterDuplicates),
   212      5: getWagmiContracts(deployments[5], filterDuplicates),
   213      420: getWagmiContracts(deployments[420], filterDuplicates),
   214    }
   215  
   216    const allContracts = Object.values(contracts).flat()
   217  
   218    const config: ContractConfig[] = []
   219  
   220    // this for loop is not terribly efficient but seems fast enough for the scale here
   221    for (const contract of allContracts) {
   222      // we will only process the implementation ABI but will use teh proxy addresses for deployments
   223      const isProxy = contract.name.endsWith('Proxy')
   224      // once we see the first deployment of a contract we will process all networks all at once
   225      const alreadyProcessedContract = config.find(
   226        (c) => c.name === contract.name
   227      )
   228      if (isProxy || alreadyProcessedContract) {
   229        continue
   230      }
   231  
   232      const implementations = {
   233        // @warning Later code assumes mainnet is first!!!
   234        [1]: contracts[1].find((c) => c.name === contract.name),
   235        // @warning Later code assumes mainnet is first!!!
   236        [10]: contracts[10].find((c) => c.name === contract.name),
   237        [5]: contracts[5].find((c) => c.name === contract.name),
   238        [420]: contracts[420].find((c) => c.name === contract.name),
   239      }
   240      const maybeProxyName = contract.name + 'Proxy'
   241      const proxies = {
   242        // @warning Later code assumes mainnet is first!!!
   243        [1]: contracts[1].find((c) => c.name === maybeProxyName),
   244        // @warning Later code assumes mainnet is first!!!
   245        [10]: contracts[10].find((c) => c.name === maybeProxyName),
   246        [5]: contracts[5].find((c) => c.name === maybeProxyName),
   247        [420]: contracts[420].find((c) => c.name === maybeProxyName),
   248      }
   249  
   250      const predeploy = predeployContracts[
   251        contract.name as keyof typeof predeployContracts
   252      ] as { address: Address } | undefined
   253  
   254      // If the contract has different abis on different networks we don't want to group them as a single abi
   255      const isContractUnique = !Object.values(implementations).some(
   256        (implementation) =>
   257          implementation && !isDeepStrictEqual(implementation.abi, contract.abi)
   258      )
   259      if (!isContractUnique) {
   260        Object.entries(implementations)
   261          .filter(([_, implementation]) => implementation)
   262          .forEach(([chain, implementation], i) => {
   263            if (implementation) {
   264              // make the first one canonical.  This will be mainnet or op mainnet if they exist
   265              const name =
   266                i === 0 ? contract.name : `${contract.name}_${chains[chain]}`
   267              const nextConfig = {
   268                abi: implementation.abi,
   269                name,
   270                address: {
   271                  [Number.parseInt(chain)]:
   272                    predeploy?.address ??
   273                    proxies[chain]?.address ??
   274                    implementation?.address,
   275                }, // predeploy?.address ?? proxies[chain]?.address ?? implementation?.address
   276              } satisfies ContractConfig
   277              config.push(nextConfig)
   278            }
   279          })
   280        continue
   281      }
   282  
   283      const wagmiConfig = {
   284        abi: contract.abi,
   285        name: contract.name,
   286        address: {},
   287      } satisfies ContractConfig
   288  
   289      Object.entries(implementations).forEach(([chain, proxy]) => {
   290        if (proxy) {
   291          wagmiConfig.address[chain] =
   292            predeploy?.address ?? proxy.address ?? contract.address
   293        }
   294      })
   295      // if proxies exist overwrite the address with the proxy address
   296      Object.entries(proxies).forEach(([chain, proxy]) => {
   297        if (proxy) {
   298          wagmiConfig.address[chain] = predeploy?.address ?? proxy.address
   299        }
   300      })
   301  
   302      config.push(wagmiConfig)
   303    }
   304  
   305    return config
   306  }
   307  
   308  /**
   309   * This plugin will create a addresses mapping from contract name to address
   310   */
   311  const addressesByContractByNetworkPlugin: Plugin = {
   312    name: 'addressesByContractByNetwork',
   313    run: async ({ contracts }) => {
   314      const addresses = Object.fromEntries(
   315        contracts.map((contract) => [contract.name, contract.address ?? {}])
   316      )
   317      // write to json file so it's easy to audit in prs relative to the generated file diff
   318      writeFileSync('./addresses.json', JSON.stringify(addresses, null, 2))
   319      return {
   320        content: [
   321          `export const addresses = ${JSON.stringify(addresses)} as const`,
   322          `export const predeploys = ${JSON.stringify(predeployContracts)}`,
   323        ].join('\n'),
   324      }
   325    },
   326  }
   327  
   328  /**
   329   * This plugin will create an abi mapping from contract name to abi
   330   */
   331  const abiPlugin: Plugin = {
   332    name: 'abisByContractByNetwork',
   333    run: async ({ contracts }) => {
   334      const abis = Object.fromEntries(
   335        contracts.map((contract) => [contract.name, contract.abi])
   336      )
   337      // write to json file so it's easy to audit in prs relative to the generated file diff
   338      writeFileSync('./abis.json', JSON.stringify(abis, null, 2))
   339      return {
   340        content: `export const abis = ${JSON.stringify(abis)} as const`,
   341      }
   342    },
   343  }
   344  
   345  /**
   346   * This plugin adds an eslint ignore to the generated code
   347   */
   348  const eslintIgnorePlugin: Plugin = {
   349    name: 'eslintIgnore',
   350    run: async () => {
   351      return {
   352        prepend: `/* eslint-disable */`,
   353        content: ``,
   354      }
   355    },
   356  }
   357  
   358  const contracts = getContractConfigs()
   359  // there is a known wagmi bug with contracts who have both FOO_BAR and fooBar method
   360  const contractsWithFilteredDuplicates = getContractConfigs(true)
   361  // @see https://wagmi.sh/cli
   362  export default defineConfig([
   363    {
   364      out: 'src/constants.ts',
   365      contracts,
   366      plugins: [
   367        eslintIgnorePlugin,
   368        addressesByContractByNetworkPlugin,
   369        abiPlugin,
   370      ],
   371    },
   372    {
   373      out: 'src/actions.ts',
   374      contracts: contractsWithFilteredDuplicates,
   375      plugins: [
   376        eslintIgnorePlugin,
   377        actions({
   378          getContract: true,
   379          // don't include actions because they can be more simply done via getContract
   380          prepareWriteContract: false,
   381          readContract: false,
   382          watchContractEvent: false,
   383          writeContract: false,
   384        }),
   385      ],
   386    },
   387    {
   388      out: 'src/react.ts',
   389      contracts: contractsWithFilteredDuplicates,
   390      plugins: [
   391        eslintIgnorePlugin,
   392        react({
   393          useContractRead: true,
   394          useContractWrite: true,
   395          useContractEvent: true,
   396          // don't include more niche actions to keep api more simple
   397          useContractFunctionRead: false,
   398          useContractFunctionWrite: false,
   399          useContractItemEvent: false,
   400          usePrepareContractFunctionWrite: false,
   401          usePrepareContractWrite: false,
   402        }),
   403      ],
   404    },
   405  ])