synapsecns/sanguine

View on GitHub
packages/synapse-interface/scripts/generateMaps.js

Summary

Maintainability
C
1 day
Test Coverage
const { ethers } = require('ethers')

const { prettyPrintTS } = require('./utils/prettyPrintTs')
const { fetchRfqData } = require('./utils/fetchRfqData')
// Provider URLs
const providers = require('./data/providers.json')
// List of ignored bridge symbols
const ignoredBridgeSymbols = require('./data/ignoredBridgeSymbols.json')
// Symbol overrides (for tokens with incorrect on-chain symbols)
const symbolOverrides = require('./data/symbolOverrides.json')
// Contract ABIs
const SynapseRouterABI = require('./abi/SynapseRouter.json')
const SynapseCCTPABI = require('./abi/SynapseCCTP.json')
const SynapseCCTPRouterABI = require('./abi/SynapseCCTPRouter.json')
const SwapQuoterABI = require('./abi/SwapQuoter.json')
const FastBridgeRouterABI = require('./abi/FastBridgeRouter.json')
const ERC20ABI = require('./abi/IERC20Metadata.json')
const DefaultPoolABI = require('./abi/IDefaultPool.json')
// const rfqResponse = require('./data/rfqResponse.json')
// ETH address
const ETH = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'

// Format is { chainId: providerUrl }
// Replace providerUrl with new ethers.providers.JsonRpcProvider(providerUrl)
Object.keys(providers).forEach((chainId) => {
  providers[chainId] = new ethers.providers.JsonRpcProvider(providers[chainId])
})

// Contract addresses
const SynapseRouterAddress = '0x7e7a0e201fd38d3adaa9523da6c109a07118c96a'
const SynapseCCTPRouterAddress = '0xd5a597d6e7ddf373a92C8f477DAAA673b0902F48'
const SynapseCCTPAddress = '0x12715a66773BD9C54534a01aBF01d05F6B4Bd35E'
const FastBridgeRouterAddress = '0x00cD000000003f7F682BE4813200893d4e690000'

// Chain IDs where SynapseBridge is allowed
const allowedChainIdsForSynapseBridge = [
  1, 10, 25, 56, 137, 250, 288, 1088, 1284, 1285, 2000, 7700, 8217, 8453, 81457,
  42161, 43114, 53935, 1313161554, 1666600000,
]

// Chain IDs where SynapseCCTPRouter is allowed
const allowedChainIdsForSynapseCCTPRouter = [1, 10, 137, 8453, 42161, 43114]

// Chain IDs where RFQ is allowed
const allowedChainIdsForRfq = [
  1, 10, 56, 480, 8453, 42161, 59144, 81457, 534352,
]

// Get SynapseRouter contract instances for each chain
const SynapseRouters = {}
const SwapQuoters = {}
allowedChainIdsForSynapseBridge.forEach((chainId) => {
  SynapseRouters[chainId] = new ethers.Contract(
    SynapseRouterAddress,
    SynapseRouterABI,
    providers[chainId]
  )
})

// Get SynapseCCTPRouter contract instances for each chain
// Only include chains where SynapseCCTPRouter is allowed
const SynapseCCTPRouters = {}
const SynapseCCTPs = {}
allowedChainIdsForSynapseCCTPRouter.forEach((chainId) => {
  SynapseCCTPRouters[chainId] = new ethers.Contract(
    SynapseCCTPRouterAddress,
    SynapseCCTPRouterABI,
    providers[chainId]
  )
  SynapseCCTPs[chainId] = new ethers.Contract(
    SynapseCCTPAddress,
    SynapseCCTPABI,
    providers[chainId]
  )
})

const FastBridgeRouters = {}
allowedChainIdsForRfq.forEach((chainId) => {
  FastBridgeRouters[chainId] = new ethers.Contract(
    FastBridgeRouterAddress,
    FastBridgeRouterABI,
    providers[chainId]
  )
})

const getSwapQuoter = async (chainId) => {
  if (SwapQuoters[chainId]) {
    return SwapQuoters[chainId]
  }
  const router = SynapseRouters[chainId] || FastBridgeRouters[chainId]
  const swapQuoterAddr = router ? await router.swapQuoter() : null
  if (!swapQuoterAddr) {
    return null
  }
  SwapQuoters[chainId] = new ethers.Contract(
    swapQuoterAddr,
    SwapQuoterABI,
    providers[chainId]
  )
  return SwapQuoters[chainId]
}

// Function to get list of tokens that could be swapped
// into SynapseBridge tokens for a given chain.
const getBridgeOriginMap = async (chainId) => {
  const swapQuoter = await getSwapQuoter(chainId)
  if (!swapQuoter) {
    return {
      originMap: {},
      poolSets: [],
    }
  }

  // Get WETH address
  const weth = await swapQuoter.weth()
  // Get list of supported tokens
  let bridgeTokens = (await SynapseRouters[chainId]?.bridgeTokens()) || []
  const pools = await swapQuoter.allPools()

  // Collect map from bridge token to symbols by doing tokenToSymbol for each bridge token
  const allTokenSymbols = {}
  await Promise.all(
    bridgeTokens.map(async (bridgeToken) => {
      const symbol = await SynapseRouters[chainId].tokenToSymbol(bridgeToken)
      // Skip if symbol is in ignoredBridgeSymbols
      if (!ignoredBridgeSymbols.includes(symbol)) {
        allTokenSymbols[bridgeToken] = new Set([symbol])
      }
    })
  )
  // List of bridge tokens without ignored symbols
  bridgeTokens = Object.keys(allTokenSymbols)
  // Collect map from supported tokens into set of bridge token symbols
  const tokensToSymbols = {}
  // Add all bridge tokens to tokensToSymbols
  Object.keys(allTokenSymbols).forEach((bridgeToken) => {
    tokensToSymbols[bridgeToken] = new Set(allTokenSymbols[bridgeToken])
  })
  // List of sets of tokens in each pool
  const poolSets = []
  pools.forEach((pool) => {
    // Collect set of bridge token symbols that are present in the pool
    const bridgeSymbols = getBridgeSymbolsSet(
      allTokenSymbols,
      pool.tokens.map((token) => token.token).concat(pool.lpToken)
    )
    const poolSet = new Set()
    // Add collected set to tokensToSymbols for each token in the pool
    pool.tokens.forEach((token) => {
      addSetToMap(tokensToSymbols, token.token, bridgeSymbols)
      poolSet.add(token.token)
    })
    // Add collected set to tokensToSymbols for the pool lpToken (if it is a bridge token)
    if (bridgeTokens.includes(pool.lpToken)) {
      addSetToMap(tokensToSymbols, pool.lpToken, bridgeSymbols)
      poolSet.add(pool.lpToken)
    }
    // Save set of tokens in the pool
    poolSets.push(poolSet)
  })
  // If WETH is present in the map, add ETH with the same set of bridge token symbols
  if (weth in tokensToSymbols) {
    addSetToMap(tokensToSymbols, ETH, tokensToSymbols[weth])
    poolSets.forEach((poolSet) => {
      if (poolSet.has(weth)) {
        poolSet.add(ETH)
      }
    })
  }
  return {
    originMap: tokensToSymbols,
    poolSets,
  }
}

// Function to get list of tokens that could be swapped
// into SynapseCCTP tokens for a given chain.
const getCCTPOriginMap = async (chainId) => {
  // Return empty map if CCTP is not supported on the chain
  if (!SynapseCCTPs[chainId]) {
    return {}
  }
  // Get map from token to set of bridge token symbols
  const cctpTokenSymbols = await getCCTPBridgeSymbols(chainId)
  const tokensToSymbols = {}
  // Add all bridge tokens to tokensToSymbols
  Object.keys(cctpTokenSymbols).forEach((token) => {
    tokensToSymbols[token] = new Set([cctpTokenSymbols[token]])
  })
  // Add tokens from whitelisted pools
  await Promise.all(
    Object.keys(cctpTokenSymbols).map(async (cctpToken) => {
      const pool = await SynapseCCTPs[chainId].circleTokenPool(cctpToken)
      const tokens = await getPoolTokens(chainId, pool)
      tokens.forEach((token) => {
        addSetToMap(
          tokensToSymbols,
          token,
          new Set([cctpTokenSymbols[cctpToken]])
        )
      })
    })
  )
  return tokensToSymbols
}

const getFastBridgeOriginMap = async (chainId, rfqResponse) => {
  // Return empty map if FastBridge is not supported on the chain
  if (!FastBridgeRouters[chainId]) {
    return {}
  }
  const rfqTokens = getRFQBridgeTokens(chainId, rfqResponse)
  // Create map from token to symbol
  const rfqTokenSymbols = {}
  await Promise.all(
    rfqTokens.map(async (token) => {
      rfqTokenSymbols[token] = getRFQSymbol(
        await getTokenSymbol(chainId, token)
      )
    })
  )
  const tokensToSymbols = {}
  // Add all bridge tokens to tokensToSymbols
  Object.keys(rfqTokenSymbols).forEach((token) => {
    tokensToSymbols[token] = new Set([rfqTokenSymbols[token]])
  })
  const swapQuoter = await getSwapQuoter(chainId)
  if (!swapQuoter) {
    return tokensToSymbols
  }
  const weth = await swapQuoter.weth()
  if (rfqTokenSymbols[ETH]) {
    rfqTokenSymbols[weth] = rfqTokenSymbols[ETH]
    tokensToSymbols[weth] = new Set([rfqTokenSymbols[ETH]])
  }
  const pools = await swapQuoter.allPools()
  pools.forEach((pool) => {
    // Get the symbols of supported RFQ tokens in the pool
    const poolRFQSymbols = pool.tokens
      .filter((token) => rfqTokenSymbols[token.token])
      .map((token) => rfqTokenSymbols[token.token])
    if (poolRFQSymbols.length === 0) {
      return
    }
    // Every token in the pools is swappable into these symbols
    pool.tokens.forEach((token) => {
      addSetToMap(tokensToSymbols, token.token, new Set(poolRFQSymbols))
    })
  })
  return tokensToSymbols
}

// Function to get a list of bridge token symbols that could be swapped
// into a token on a destination chain.
const getDestinationBridgeSymbols = async (chainId, token) => {
  const symbolSet = new Set()
  if (SynapseRouters[chainId]) {
    // Get list of connected bridge tokens: (symbol, token) pairs
    const connectedBridgeTokens = await SynapseRouters[
      chainId
    ].getConnectedBridgeTokens(token)
    connectedBridgeTokens.forEach((bridgeToken) => {
      symbolSet.add(bridgeToken.symbol)
    })
  }
  // Get a list of bridge token symbols from CCTP if CCTP is supported on the chain
  if (SynapseCCTPRouters[chainId]) {
    const connectedCctpTokens = await SynapseCCTPRouters[
      chainId
    ].getConnectedBridgeTokens(token)
    connectedCctpTokens.forEach((bridgeToken) => {
      symbolSet.add(bridgeToken.symbol)
    })
  }
  return Array.from(symbolSet).sort()
}

// Function to get a map from CCTP tokens to bridge token symbols
const getCCTPBridgeSymbols = async (chainId) => {
  // Return empty map if CCTP is not supported on the chain
  if (!SynapseCCTPs[chainId]) {
    return {}
  }
  // Get a list of bridge tokens
  // List entries are (symbol, token) pairs
  const cctpTokens = await SynapseCCTPs[chainId].getBridgeTokens()
  // Return map from token to bridge symbol
  const cctpTokenToSymbol = {}
  cctpTokens.forEach((bridgeToken) => {
    cctpTokenToSymbol[bridgeToken.token] = bridgeToken.symbol
  })
  return cctpTokenToSymbol
}

const getRFQBridgeTokens = (chainId, rfqResponse) => {
  return [
    ...new Set(
      rfqResponse
        .filter((quote) => quote.origin_chain_id === Number(chainId))
        .map((quote) => ethers.utils.getAddress(quote.origin_token_addr))
    ),
  ]
}

// Function to get a list of tokens in a pool
const getPoolTokens = async (chainId, poolAddress) => {
  const pool = new ethers.Contract(
    poolAddress,
    DefaultPoolABI,
    providers[chainId]
  )
  // To get a list of tokens we do getToken(i) calls until we get a revert
  const tokens = []
  for (let i = 0; ; i++) {
    try {
      tokens.push(await pool.getToken(i))
    } catch (e) {
      break
    }
  }
  return tokens
}

// Gets a set of bridge token symbols from a list of tokens
const getBridgeSymbolsSet = (allTokenSymbols, tokenAddresses) => {
  const bridgeSymbols = new Set()
  tokenAddresses.forEach((token) => {
    if (token in allTokenSymbols) {
      // Add all values from allTokenSymbols[token] to bridgeSymbols
      allTokenSymbols[token].forEach((value) => {
        bridgeSymbols.add(value)
      })
    }
  })
  return bridgeSymbols
}

// Adds values from set to map[key]
const addSetToMap = (map, key, set) => {
  if (!map[key]) {
    map[key] = new Set()
  }
  set.forEach((value) => {
    map[key].add(value)
  })
}

const sortMapByKeys = (map) => {
  const sortedMap = {}
  Object.keys(map)
    .sort()
    .forEach((key) => {
      sortedMap[key] = map[key]
    })
  return sortedMap
}
const printMaps = async () => {
  const bridgeMap = {}
  console.log('Starting on chains: ', Object.keys(providers))

  const rfqResponse = await fetchRfqData()
  await Promise.all(
    Object.keys(providers).map(async (chainId) => {
      // Get map from token to set of bridge token symbols
      const { originMap, poolSets } = await getBridgeOriginMap(chainId)
      // Add tokens from CCTP originMap to global originMap
      const cctpOriginMap = await getCCTPOriginMap(chainId)
      Object.keys(cctpOriginMap).forEach((token) => {
        addSetToMap(originMap, token, cctpOriginMap[token])
      })
      // Add tokens from RFQ originMap to global originMap
      const rfqOriginMap = await getFastBridgeOriginMap(chainId, rfqResponse)
      Object.keys(rfqOriginMap).forEach((token) => {
        addSetToMap(originMap, token, rfqOriginMap[token])
      })
      const tokens = {}
      await Promise.all(
        Object.keys(originMap).map(async (token) => {
          const decimals = await getTokenDecimals(chainId, token)
          const symbol = await getTokenSymbol(chainId, token)
          const origin = Array.from(originMap[token])
            .map((t) => (t === 'RFQ.USDC.e' ? 'RFQ.USDC' : t))
            .sort()
          const destination = await getDestinationBridgeSymbols(chainId, token)
          const swappable = extractSwappable(poolSets, token)

          tokens[token] = {
            decimals,
            symbol,
            origin,
            destination,
            swappable,
          }
          // Check if token is supported as destination asset in RFQ
          if (
            rfqResponse.some(
              (quote) =>
                ethers.utils.getAddress(quote.dest_token_addr) === token &&
                quote.dest_chain_id === Number(chainId)
            )
          ) {
            tokens[token].destination.push(getRFQSymbol(tokens[token].symbol))
          }
        })
      )
      bridgeMap[chainId] = sortMapByKeys(tokens)
      console.log('Finished chain: ', chainId)
    })
  )

  prettyPrintTS(bridgeMap, 'BRIDGE_MAP', './constants/bridgeMap.ts')
}

// Extracts the list of tokens that can be swapped into a token via single on-chain swap
const extractSwappable = (poolSets, token) => {
  const tokenSet = new Set()
  poolSets.forEach((poolSet) => {
    // If token is in the pool, add all tokens except token to tokenSet
    if (poolSet.has(token)) {
      poolSet.forEach((poolToken) => {
        if (poolToken !== token) {
          tokenSet.add(poolToken)
        }
      })
    }
  })
  return Array.from(tokenSet).sort()
}

const getTokenSymbol = async (chainId, token) => {
  // Check if token is ETH
  if (token === ETH) {
    const swapQuoter = await getSwapQuoter(chainId)
    if (!swapQuoter) {
      return 'ETH'
    }
    // Get WETH address from SwapQuoter
    const weth = await swapQuoter.weth()
    // Return "WETH" symbol without first character
    return getTokenSymbol(chainId, weth).then((symbol) => symbol.slice(1))
  }
  // Check if {chainId: {token: {symbol}}} is in symbolOverrides
  if (chainId in symbolOverrides && token in symbolOverrides[chainId]) {
    return symbolOverrides[chainId][token]
  }
  // Otherwise return symbol from ERC20 contract
  const symbol = await new ethers.Contract(
    token,
    ERC20ABI,
    providers[chainId]
  ).symbol()
  return symbol
}

const getTokenDecimals = async (chainId, token) => {
  // Check if token is ETH
  if (token === ETH) {
    return 18
  }
  // Otherwise return decimals from ERC20 contract
  const decimals = await new ethers.Contract(
    token,
    ERC20ABI,
    providers[chainId]
  ).decimals()
  return decimals
}

const getRFQSymbol = (symbol) => {
  if (symbol === 'USDC.e') {
    return 'RFQ.USDC'
  } else {
    return `RFQ.${symbol}`
  }
}

printMaps()