synapsecns/sanguine

View on GitHub
packages/sdk-router/src/sdk.test.ts

Summary

Maintainability
F
3 wks
Test Coverage
import { Provider } from '@ethersproject/abstract-provider'
import { BigNumber, PopulatedTransaction } from 'ethers'
import { AddressZero, Zero } from '@ethersproject/constants'
import { parseFixed } from '@ethersproject/bignumber'

import { SynapseSDK } from './sdk'
import {
  ARB_GMX,
  ARB_NETH,
  ARB_NUSD,
  ARB_POOL_ETH_WRAPPER,
  ARB_POOL_NETH,
  ARB_POOL_NUSD,
  ARB_USDC,
  ARB_USDC_E,
  ARB_USDT,
  ARB_WETH,
  AVAX_GMX,
  AVAX_GOHM,
  AVAX_USDC,
  AVAX_USDC_E,
  BSC_GOHM,
  BSC_USDC,
  ETH_DAI,
  ETH_POOL_NUSD,
  ETH_USDC,
  ETH_USDT,
  NATIVE_ADDRESS,
} from './constants/testValues'
import { getTestProvider } from './constants/testProviders'
import {
  CCTP_ROUTER_ADDRESS_MAP,
  MEDIAN_TIME_BRIDGE,
  MEDIAN_TIME_CCTP,
  ROUTER_ADDRESS_MAP,
  SupportedChainId,
} from './constants'
import {
  BridgeQuote,
  FeeConfig,
  Query,
  RouterQuery,
  SwapQuote,
  SynapseModuleSet,
} from './module'
import * as operations from './operations'

// Override fetch to exclude RFQ from tests
global.fetch = jest.fn(() =>
  Promise.resolve({
    ok: true,
    json: () => Promise.resolve([]),
  })
) as any

// Retry the flaky tests up to 3 times
jest.retryTimes(3)

const EXPECTED_GAS_DROP: { [chainId: number]: BigNumber } = {
  [SupportedChainId.ETH]: BigNumber.from(0),
  // TODO: reenable once both ARB airdrops are adjusted
  // [SupportedChainId.ARBITRUM]: parseFixed('0.0003', 18),
  [SupportedChainId.BSC]: parseFixed('0.002', 18),
  [SupportedChainId.AVALANCHE]: parseFixed('0.025', 18),
}

const expectCorrectFeeConfig = (feeConfig: FeeConfig) => {
  expect(feeConfig).toBeDefined()
  expect(feeConfig.bridgeFee).toBeGreaterThan(0)
  expect(feeConfig.minFee.gt(0)).toBe(true)
  expect(feeConfig.maxFee.gt(0)).toBe(true)
}

const expectCorrectBridgeQuote = (bridgeQuote: BridgeQuote) => {
  expect(bridgeQuote).toBeDefined()
  expect(bridgeQuote.feeAmount.gt(0)).toBe(true)
  expectCorrectFeeConfig(bridgeQuote.feeConfig)
  expect(bridgeQuote.routerAddress?.length).toBeGreaterThan(0)
  expect(bridgeQuote.maxAmountOut.gt(0)).toBe(true)
  expect(bridgeQuote.originQuery).toBeDefined()
  expect(bridgeQuote.destQuery).toBeDefined()
  expect(bridgeQuote.originChainId).toBeDefined()
  expect(bridgeQuote.destChainId).toBeDefined()
}

const expectCorrectPopulatedTransaction = (
  populatedTransaction: PopulatedTransaction,
  expectedValue: BigNumber = Zero
) => {
  expect(populatedTransaction).toBeDefined()
  expect(populatedTransaction.data?.length).toBeGreaterThan(0)
  expect(populatedTransaction.to?.length).toBeGreaterThan(0)
  expect(populatedTransaction.value).toEqual(expectedValue)
}

const createBridgeQuoteTests = (
  synapse: SynapseSDK,
  originChainId: number,
  destChainId: number,
  token: string,
  amount: BigNumber,
  resultPromise: Promise<BridgeQuote>
) => {
  let result: BridgeQuote
  beforeAll(async () => {
    result = await resultPromise
  })

  it('Generates a bridge quote with valid uuid', async () => {
    expect(typeof result.id).toBe('string')
  })

  it('Fetches a bridge quote', async () => {
    expectCorrectBridgeQuote(result)
  })

  it('Could be used for bridging', async () => {
    const expectedValue = token === NATIVE_ADDRESS ? amount : Zero
    const data = await synapse.bridge(
      '0x0000000000000000000000000000000000001337',
      result.routerAddress,
      originChainId,
      destChainId,
      token,
      amount,
      result.originQuery,
      result.destQuery
    )
    expectCorrectPopulatedTransaction(data, expectedValue)
  })
}

const createSwapQuoteTests = (
  synapse: SynapseSDK,
  chainId: number,
  token: string,
  amount: BigNumber,
  resultPromise: Promise<SwapQuote>
) => {
  let result: SwapQuote
  beforeAll(async () => {
    result = await resultPromise
  })

  it('Fetches a swap quote', async () => {
    expect(result).toBeDefined()
    expect(result.routerAddress?.length).toBeGreaterThan(0)
    expect(result.maxAmountOut.gt(0)).toBe(true)
    expect(result.query).toBeDefined()
  })

  it('Could be used for swapping', async () => {
    const expectedValue = token === NATIVE_ADDRESS ? amount : Zero
    const data = await synapse.swap(
      chainId,
      '0x0000000000000000000000000000000000001337',
      token,
      amount,
      result.query
    )
    expectCorrectPopulatedTransaction(data, expectedValue)
  })
}

describe('SynapseSDK', () => {
  const ethProvider: Provider = getTestProvider(SupportedChainId.ETH)

  const arbProvider: Provider = getTestProvider(SupportedChainId.ARBITRUM)

  const opProvider: Provider = getTestProvider(SupportedChainId.OPTIMISM)

  const avaxProvider: Provider = getTestProvider(SupportedChainId.AVALANCHE)

  const bscProvider: Provider = getTestProvider(SupportedChainId.BSC)

  // Chain where CCTP is unlikely to be deployed
  const moonbeamProvider: Provider = getTestProvider(SupportedChainId.MOONBEAM)

  describe('#constructor', () => {
    const synapse = new SynapseSDK(
      [
        SupportedChainId.ETH,
        SupportedChainId.ARBITRUM,
        SupportedChainId.MOONBEAM,
      ],
      [ethProvider, arbProvider, moonbeamProvider]
    )

    it('fails with unequal amount of chains to providers', () => {
      const chainIds = [SupportedChainId.ETH, SupportedChainId.ARBITRUM]
      const testProviders = [ethProvider]
      expect(() => new SynapseSDK(chainIds, testProviders)).toThrow(
        'Amount of chains and providers does not equal'
      )
    })

    it('Instantiates SynapseRouters for each chain', () => {
      expect(synapse.synapseRouterSet).toBeDefined()
      expect(
        synapse.synapseRouterSet.routers[SupportedChainId.ETH]
      ).toBeDefined()
      expect(
        synapse.synapseRouterSet.routers[SupportedChainId.ARBITRUM]
      ).toBeDefined()
      expect(
        synapse.synapseRouterSet.routers[SupportedChainId.MOONBEAM]
      ).toBeDefined()
    })

    it('Does not instantiate SynapseRouters for chains without providers', () => {
      expect(
        synapse.synapseRouterSet.routers[SupportedChainId.AVALANCHE]
      ).toBeUndefined()
    })

    it('Instantiates SynapseCCTPRouters for each chain with CCTP', () => {
      expect(synapse.synapseCCTPRouterSet).toBeDefined()
      expect(
        synapse.synapseCCTPRouterSet.routers[SupportedChainId.ETH]
      ).toBeDefined()
      expect(
        synapse.synapseCCTPRouterSet.routers[SupportedChainId.ARBITRUM]
      ).toBeDefined()
    })

    it('Does not instantiate SynapseCCTPRouters for chains without CCTP', () => {
      expect(
        synapse.synapseCCTPRouterSet.routers[SupportedChainId.MOONBEAM]
      ).toBeUndefined()
    })

    it('Does not instantiate SynapseCCTPRouters for chains without providers', () => {
      expect(
        synapse.synapseCCTPRouterSet.routers[SupportedChainId.AVALANCHE]
      ).toBeUndefined()
    })

    it('Saves providers', () => {
      expect(synapse.providers[SupportedChainId.ETH]).toBe(ethProvider)
      expect(synapse.providers[SupportedChainId.ARBITRUM]).toBe(arbProvider)
      expect(synapse.providers[SupportedChainId.MOONBEAM]).toBe(
        moonbeamProvider
      )
    })
  })

  describe('Bridging: ETH -> ARB', () => {
    const synapse = new SynapseSDK(
      [SupportedChainId.ETH, SupportedChainId.ARBITRUM],
      [ethProvider, arbProvider]
    )

    describe('ETH USDC -> ARB USDC', () => {
      const amount = BigNumber.from(10).pow(9)
      const resultPromise: Promise<BridgeQuote> = synapse.bridgeQuote(
        SupportedChainId.ETH,
        SupportedChainId.ARBITRUM,
        ETH_USDC,
        ARB_USDC,
        amount
      )

      createBridgeQuoteTests(
        synapse,
        SupportedChainId.ETH,
        SupportedChainId.ARBITRUM,
        ETH_USDC,
        amount,
        resultPromise
      )
    })

    describe('ETH Native -> ARB Native', () => {
      const amount = BigNumber.from(10).pow(18)
      const resultPromise: Promise<BridgeQuote> = synapse.bridgeQuote(
        SupportedChainId.ETH,
        SupportedChainId.ARBITRUM,
        NATIVE_ADDRESS,
        NATIVE_ADDRESS,
        amount
      )

      createBridgeQuoteTests(
        synapse,
        SupportedChainId.ETH,
        SupportedChainId.ARBITRUM,
        NATIVE_ADDRESS,
        amount,
        resultPromise
      )

      it('Fetches a Synapse bridge quote', async () => {
        const result = await resultPromise
        expect(result.routerAddress).toEqual(
          ROUTER_ADDRESS_MAP[SupportedChainId.ETH]
        )
        // SynapseRouterQuery has swapAdapter property
        expect(result.originQuery.swapAdapter).toBeDefined()
        // Estimated time must match the SynapseBridge median time
        expect(result.estimatedTime).toEqual(
          MEDIAN_TIME_BRIDGE[SupportedChainId.ETH]
        )
        expect(result.bridgeModuleName).toEqual('SynapseBridge')
        // TODO: reenable
        // expect(result.gasDropAmount).toEqual(
        //   EXPECTED_GAS_DROP[SupportedChainId.ARBITRUM]
        // )
        expect(result.originChainId).toEqual(SupportedChainId.ETH)
        expect(result.destChainId).toEqual(SupportedChainId.ARBITRUM)
      })
    })
  })

  describe('Bridging: AVAX -> BSC', () => {
    const synapse = new SynapseSDK(
      [SupportedChainId.AVALANCHE, SupportedChainId.BSC],
      [avaxProvider, bscProvider]
    )

    describe('AVAX USDC.e -> BSC USDC', () => {
      const amount = BigNumber.from(10).pow(9)
      const resultPromise: Promise<BridgeQuote> = synapse.bridgeQuote(
        SupportedChainId.AVALANCHE,
        SupportedChainId.BSC,
        AVAX_USDC_E,
        BSC_USDC,
        amount
      )

      createBridgeQuoteTests(
        synapse,
        SupportedChainId.AVALANCHE,
        SupportedChainId.BSC,
        AVAX_USDC_E,
        amount,
        resultPromise
      )

      it('Fetches a Synapse bridge quote', async () => {
        const result = await resultPromise
        expect(result.routerAddress).toEqual(
          ROUTER_ADDRESS_MAP[SupportedChainId.AVALANCHE]
        )
        // SynapseRouterQuery has swapAdapter property
        expect(result.originQuery.swapAdapter).toBeDefined()
        // Estimated time must match the SynapseBridge median time
        expect(result.estimatedTime).toEqual(
          MEDIAN_TIME_BRIDGE[SupportedChainId.AVALANCHE]
        )
        expect(result.bridgeModuleName).toEqual('SynapseBridge')
        expect(result.gasDropAmount).toEqual(
          EXPECTED_GAS_DROP[SupportedChainId.BSC]
        )
        expect(result.originChainId).toEqual(SupportedChainId.AVALANCHE)
        expect(result.destChainId).toEqual(SupportedChainId.BSC)
      })
    })

    describe('AVAX gOHM -> BSC gOHM', () => {
      const amount = BigNumber.from(10).pow(21)
      const resultPromise: Promise<BridgeQuote> = synapse.bridgeQuote(
        SupportedChainId.AVALANCHE,
        SupportedChainId.BSC,
        AVAX_GOHM,
        BSC_GOHM,
        amount
      )

      createBridgeQuoteTests(
        synapse,
        SupportedChainId.AVALANCHE,
        SupportedChainId.BSC,
        AVAX_GOHM,
        amount,
        resultPromise
      )

      it('Fetches a Synapse bridge quote', async () => {
        const result = await resultPromise
        expect(result.routerAddress).toEqual(
          ROUTER_ADDRESS_MAP[SupportedChainId.AVALANCHE]
        )
        // SynapseRouterQuery has swapAdapter property
        expect(result.originQuery.swapAdapter).toBeDefined()
        // Estimated time must match the SynapseBridge median time
        expect(result.estimatedTime).toEqual(
          MEDIAN_TIME_BRIDGE[SupportedChainId.AVALANCHE]
        )
        expect(result.bridgeModuleName).toEqual('SynapseBridge')
        expect(result.gasDropAmount).toEqual(
          EXPECTED_GAS_DROP[SupportedChainId.BSC]
        )
        expect(result.originChainId).toEqual(SupportedChainId.AVALANCHE)
        expect(result.destChainId).toEqual(SupportedChainId.BSC)
      })
    })
  })

  describe('Bridging: ARB -> ETH', () => {
    const synapse = new SynapseSDK(
      [SupportedChainId.ETH, SupportedChainId.ARBITRUM],
      [ethProvider, arbProvider]
    )

    describe('ARB USDC -> ETH USDC (using Bridge)', () => {
      const amount = BigNumber.from(10).pow(12).add(1)
      const resultPromise: Promise<BridgeQuote> = synapse.bridgeQuote(
        SupportedChainId.ARBITRUM,
        SupportedChainId.ETH,
        ARB_USDC,
        ETH_USDC,
        amount
      )

      const secondResultPromise: Promise<BridgeQuote> = synapse.bridgeQuote(
        SupportedChainId.ARBITRUM,
        SupportedChainId.ETH,
        ARB_USDC,
        ETH_USDC,
        amount
      )

      createBridgeQuoteTests(
        synapse,
        SupportedChainId.ARBITRUM,
        SupportedChainId.ETH,
        ETH_USDC,
        amount,
        resultPromise
      )

      it('Fetches a Synapse bridge quote', async () => {
        const result = await resultPromise
        expect(result.routerAddress).toEqual(
          ROUTER_ADDRESS_MAP[SupportedChainId.ARBITRUM]
        )
        // SynapseRouterQuery has swapAdapter property
        expect(result.originQuery.swapAdapter).toBeDefined()
        // Estimated time must match the SynapseBridge median time
        expect(result.estimatedTime).toEqual(
          MEDIAN_TIME_BRIDGE[SupportedChainId.ARBITRUM]
        )
        expect(result.bridgeModuleName).toEqual('SynapseBridge')
        expect(result.gasDropAmount).toEqual(
          EXPECTED_GAS_DROP[SupportedChainId.ETH]
        )
        expect(result.originChainId).toEqual(SupportedChainId.ARBITRUM)
        expect(result.destChainId).toEqual(SupportedChainId.ETH)
      })

      it('Fetches a second Synapse bridge quote with a different ID', async () => {
        const firstQuote = await resultPromise
        const secondQuote = await secondResultPromise

        expect(firstQuote.id).not.toEqual(secondQuote.id)
      })
    })

    describe('ARB USDC -> ETH USDC (using CCTP)', () => {
      const amount = BigNumber.from(10).pow(12)
      const resultPromise: Promise<BridgeQuote> = synapse.bridgeQuote(
        SupportedChainId.ARBITRUM,
        SupportedChainId.ETH,
        ARB_USDC,
        ETH_USDC,
        amount
      )

      createBridgeQuoteTests(
        synapse,
        SupportedChainId.ARBITRUM,
        SupportedChainId.ETH,
        ETH_USDC,
        amount,
        resultPromise
      )

      it('Fetches a CCTP bridge quote', async () => {
        const result = await resultPromise
        expect(result.routerAddress).toEqual(
          CCTP_ROUTER_ADDRESS_MAP[SupportedChainId.ARBITRUM]
        )
        // SynapseCCTPRouterQuery has routerAdapter property
        expect(result.originQuery.routerAdapter).toBeDefined()
        // Estimated time must match the SynapseCCTP median time
        expect(result.estimatedTime).toEqual(
          MEDIAN_TIME_CCTP[SupportedChainId.ARBITRUM]
        )
        expect(result.bridgeModuleName).toEqual('SynapseCCTP')
        expect(result.gasDropAmount).toEqual(
          EXPECTED_GAS_DROP[SupportedChainId.ETH]
        )
        expect(result.originChainId).toEqual(SupportedChainId.ARBITRUM)
        expect(result.destChainId).toEqual(SupportedChainId.ETH)
      })
    })

    describe('ARB USDT -> ETH USDC (excludeCCTP flag tests)', () => {
      // Use $1000 USDT as amount. SynapseCCTP requires less gas on Ethereum to be completed,
      // when USDC is used as a tokenOut (compared to SynapseBridge route).
      // Therefore we can expect that the min fees would be lower. Meaning for amount this low,
      // we should get a CCTP quote unless we explicitly exclude CCTP.
      const amount = BigNumber.from(10).pow(9)

      describe('excludeCCTP flag omitted', () => {
        const resultPromise: Promise<BridgeQuote> = synapse.bridgeQuote(
          SupportedChainId.ARBITRUM,
          SupportedChainId.ETH,
          ARB_USDT,
          ETH_USDC,
          amount
        )

        createBridgeQuoteTests(
          synapse,
          SupportedChainId.ARBITRUM,
          SupportedChainId.ETH,
          ETH_USDC,
          amount,
          resultPromise
        )

        it('Fetches a CCTP bridge quote', async () => {
          const result = await resultPromise
          expect(result.routerAddress).toEqual(
            CCTP_ROUTER_ADDRESS_MAP[SupportedChainId.ARBITRUM]
          )
          // SynapseCCTPRouterQuery has routerAdapter property
          expect(result.originQuery.routerAdapter).toBeDefined()
          // Estimated time must match the SynapseCCTP median time
          expect(result.estimatedTime).toEqual(
            MEDIAN_TIME_CCTP[SupportedChainId.ARBITRUM]
          )
          expect(result.bridgeModuleName).toEqual('SynapseCCTP')
          expect(result.gasDropAmount).toEqual(
            EXPECTED_GAS_DROP[SupportedChainId.ETH]
          )
          expect(result.originChainId).toEqual(SupportedChainId.ARBITRUM)
          expect(result.destChainId).toEqual(SupportedChainId.ETH)
        })
      })

      describe('excludeCCTP flag off', () => {
        const resultPromise: Promise<BridgeQuote> = synapse.bridgeQuote(
          SupportedChainId.ARBITRUM,
          SupportedChainId.ETH,
          ARB_USDT,
          ETH_USDC,
          amount
        )

        createBridgeQuoteTests(
          synapse,
          SupportedChainId.ARBITRUM,
          SupportedChainId.ETH,
          ETH_USDC,
          amount,
          resultPromise
        )

        it('Fetches a CCTP bridge quote', async () => {
          const result = await resultPromise
          expect(result.routerAddress).toEqual(
            CCTP_ROUTER_ADDRESS_MAP[SupportedChainId.ARBITRUM]
          )
          // SynapseCCTPRouterQuery has routerAdapter property
          expect(result.originQuery.routerAdapter).toBeDefined()
          // Estimated time must match the SynapseCCTP median time
          expect(result.estimatedTime).toEqual(
            MEDIAN_TIME_CCTP[SupportedChainId.ARBITRUM]
          )
          expect(result.bridgeModuleName).toEqual('SynapseCCTP')
          expect(result.gasDropAmount).toEqual(
            EXPECTED_GAS_DROP[SupportedChainId.ETH]
          )
          expect(result.originChainId).toEqual(SupportedChainId.ARBITRUM)
          expect(result.destChainId).toEqual(SupportedChainId.ETH)
        })
      })

      describe('excludeCCTP flag on', () => {
        const resultPromise: Promise<BridgeQuote> = synapse.bridgeQuote(
          SupportedChainId.ARBITRUM,
          SupportedChainId.ETH,
          ARB_USDT,
          ETH_USDC,
          amount,
          {
            excludedModules: ['SynapseCCTP'],
          }
        )

        createBridgeQuoteTests(
          synapse,
          SupportedChainId.ARBITRUM,
          SupportedChainId.ETH,
          ETH_USDC,
          amount,
          resultPromise
        )

        it('Fetches a Synapse bridge quote', async () => {
          const result = await resultPromise
          expect(result.routerAddress).toEqual(
            ROUTER_ADDRESS_MAP[SupportedChainId.ARBITRUM]
          )
          // SynapseRouterQuery has swapAdapter property
          expect(result.originQuery.swapAdapter).toBeDefined()
          // Estimated time must match the SynapseBridge median time
          expect(result.estimatedTime).toEqual(
            MEDIAN_TIME_BRIDGE[SupportedChainId.ARBITRUM]
          )
          expect(result.bridgeModuleName).toEqual('SynapseBridge')
          expect(result.gasDropAmount).toEqual(
            EXPECTED_GAS_DROP[SupportedChainId.ETH]
          )
          expect(result.originChainId).toEqual(SupportedChainId.ARBITRUM)
          expect(result.destChainId).toEqual(SupportedChainId.ETH)
        })
      })
    })

    describe('ARB Native -> ETH Native', () => {
      const amount = BigNumber.from(10).pow(18)
      const resultPromise: Promise<BridgeQuote> = synapse.bridgeQuote(
        SupportedChainId.ARBITRUM,
        SupportedChainId.ETH,
        NATIVE_ADDRESS,
        NATIVE_ADDRESS,
        amount
      )

      createBridgeQuoteTests(
        synapse,
        SupportedChainId.ARBITRUM,
        SupportedChainId.ETH,
        NATIVE_ADDRESS,
        amount,
        resultPromise
      )

      it('Fetches a Synapse bridge quote', async () => {
        const result = await resultPromise
        expect(result.routerAddress).toEqual(
          ROUTER_ADDRESS_MAP[SupportedChainId.ARBITRUM]
        )
        // SynapseRouterQuery has swapAdapter property
        expect(result.originQuery.swapAdapter).toBeDefined()
        // Estimated time must match the SynapseBridge median time
        expect(result.estimatedTime).toEqual(
          MEDIAN_TIME_BRIDGE[SupportedChainId.ARBITRUM]
        )
        expect(result.bridgeModuleName).toEqual('SynapseBridge')
        expect(result.gasDropAmount).toEqual(
          EXPECTED_GAS_DROP[SupportedChainId.ETH]
        )
        expect(result.originChainId).toEqual(SupportedChainId.ARBITRUM)
        expect(result.destChainId).toEqual(SupportedChainId.ETH)
      })
    })
  })

  describe('Bridging: BSC -> AVAX', () => {
    const synapse = new SynapseSDK(
      [SupportedChainId.AVALANCHE, SupportedChainId.BSC],
      [avaxProvider, bscProvider]
    )

    describe('BSC USDC -> AVAX USDC', () => {
      // USDC has 18 decimals on BSC. Don't ask me why.
      const amount = BigNumber.from(10).pow(21)
      const resultPromise: Promise<BridgeQuote> = synapse.bridgeQuote(
        SupportedChainId.BSC,
        SupportedChainId.AVALANCHE,
        BSC_USDC,
        AVAX_USDC,
        amount
      )

      createBridgeQuoteTests(
        synapse,
        SupportedChainId.BSC,
        SupportedChainId.AVALANCHE,
        AVAX_USDC,
        amount,
        resultPromise
      )

      it('Fetches a Synapse bridge quote', async () => {
        const result = await resultPromise
        expect(result.routerAddress).toEqual(
          ROUTER_ADDRESS_MAP[SupportedChainId.BSC]
        )
        // SynapseRouterQuery has swapAdapter property
        expect(result.originQuery.swapAdapter).toBeDefined()
        // Estimated time must match the SynapseBridge median time
        expect(result.estimatedTime).toEqual(
          MEDIAN_TIME_BRIDGE[SupportedChainId.BSC]
        )
        expect(result.bridgeModuleName).toEqual('SynapseBridge')
        expect(result.gasDropAmount).toEqual(
          EXPECTED_GAS_DROP[SupportedChainId.AVALANCHE]
        )
        expect(result.originChainId).toEqual(SupportedChainId.BSC)
        expect(result.destChainId).toEqual(SupportedChainId.AVALANCHE)
      })
    })

    describe('BSC gOHM -> AVAX gOHM', () => {
      const amount = BigNumber.from(10).pow(21)
      const resultPromise: Promise<BridgeQuote> = synapse.bridgeQuote(
        SupportedChainId.BSC,
        SupportedChainId.AVALANCHE,
        BSC_GOHM,
        AVAX_GOHM,
        amount
      )

      createBridgeQuoteTests(
        synapse,
        SupportedChainId.BSC,
        SupportedChainId.AVALANCHE,
        AVAX_GOHM,
        amount,
        resultPromise
      )

      it('Fetches a Synapse bridge quote', async () => {
        const result = await resultPromise
        expect(result.routerAddress).toEqual(
          ROUTER_ADDRESS_MAP[SupportedChainId.BSC]
        )
        // SynapseRouterQuery has swapAdapter property
        expect(result.originQuery.swapAdapter).toBeDefined()
        // Estimated time must match the SynapseBridge median time
        expect(result.estimatedTime).toEqual(
          MEDIAN_TIME_BRIDGE[SupportedChainId.BSC]
        )
        expect(result.bridgeModuleName).toEqual('SynapseBridge')
        expect(result.gasDropAmount).toEqual(
          EXPECTED_GAS_DROP[SupportedChainId.AVALANCHE]
        )
        expect(result.originChainId).toEqual(SupportedChainId.BSC)
        expect(result.destChainId).toEqual(SupportedChainId.AVALANCHE)
      })
    })
  })

  describe('Gas drop edge cases', () => {
    const synapse = new SynapseSDK(
      [
        SupportedChainId.ARBITRUM,
        SupportedChainId.AVALANCHE,
        SupportedChainId.MOONBEAM,
      ],
      [arbProvider, avaxProvider, moonbeamProvider]
    )

    describe('GMX', () => {
      it('ARB -> AVAX: non-zero gas drop', async () => {
        const result = await synapse.bridgeQuote(
          SupportedChainId.ARBITRUM,
          SupportedChainId.AVALANCHE,
          ARB_GMX,
          AVAX_GMX,
          parseFixed('100', 18)
        )
        expect(result).toBeDefined()
        expect(result.bridgeModuleName).toEqual('SynapseBridge')
        expect(result.gasDropAmount).toEqual(
          EXPECTED_GAS_DROP[SupportedChainId.AVALANCHE]
        )
      })

      it('AVAX -> ARB: zero gas drop', async () => {
        const result = await synapse.bridgeQuote(
          SupportedChainId.AVALANCHE,
          SupportedChainId.ARBITRUM,
          AVAX_GMX,
          ARB_GMX,
          parseFixed('100', 18)
        )
        expect(result).toBeDefined()
        expect(result.bridgeModuleName).toEqual('SynapseBridge')
        expect(result.gasDropAmount).toEqual(Zero)
      })
    })
  })

  describe('allBridgeQuotes', () => {
    const synapse = new SynapseSDK(
      [SupportedChainId.ETH, SupportedChainId.ARBITRUM],
      [ethProvider, arbProvider]
    )

    it('Fetches SynapseBridge and SynapseCCTP quotes for USDC', async () => {
      const allQuotes = await synapse.allBridgeQuotes(
        SupportedChainId.ETH,
        SupportedChainId.ARBITRUM,
        ETH_USDC,
        ARB_USDT,
        BigNumber.from(10).pow(9)
      )
      expect(allQuotes.length).toEqual(2)
      expectCorrectBridgeQuote(allQuotes[0])
      expectCorrectBridgeQuote(allQuotes[1])
      // First quote should have better quote
      expect(allQuotes[0].maxAmountOut.gte(allQuotes[1].maxAmountOut)).toBe(
        true
      )
      // One should be SynapseBridge and the other SynapseCCTP
      expect(allQuotes[0].bridgeModuleName).not.toEqual(
        allQuotes[1].bridgeModuleName
      )
      expect(
        allQuotes[0].bridgeModuleName === 'SynapseBridge' ||
          allQuotes[1].bridgeModuleName === 'SynapseBridge'
      ).toBe(true)
      expect(
        allQuotes[0].bridgeModuleName === 'SynapseCCTP' ||
          allQuotes[1].bridgeModuleName === 'SynapseCCTP'
      ).toBe(true)
      // TODO: reenable
      // expect(allQuotes[0].gasDropAmount).toEqual(
      //   EXPECTED_GAS_DROP[SupportedChainId.ARBITRUM]
      // )
      // expect(allQuotes[1].gasDropAmount).toEqual(
      //   EXPECTED_GAS_DROP[SupportedChainId.ARBITRUM]
      // )
      expect(allQuotes[0].originChainId).toEqual(SupportedChainId.ETH)
      expect(allQuotes[0].destChainId).toEqual(SupportedChainId.ARBITRUM)
      expect(allQuotes[1].originChainId).toEqual(SupportedChainId.ETH)
      expect(allQuotes[1].destChainId).toEqual(SupportedChainId.ARBITRUM)
    })

    it('Generates unique IDs for SynapseBridge and SynapseCCTP quotes for USDC', async () => {
      const allQuotes = await synapse.allBridgeQuotes(
        SupportedChainId.ETH,
        SupportedChainId.ARBITRUM,
        ETH_USDC,
        ARB_USDT,
        BigNumber.from(10).pow(9)
      )
      expect(allQuotes[0].id).not.toEqual(allQuotes[1].id)
    })

    it('Fetches only SynapseBridge quotes for ETH', async () => {
      const allQuotes = await synapse.allBridgeQuotes(
        SupportedChainId.ETH,
        SupportedChainId.ARBITRUM,
        NATIVE_ADDRESS,
        NATIVE_ADDRESS,
        BigNumber.from(10).pow(18)
      )
      expect(allQuotes.length).toEqual(1)
      expectCorrectBridgeQuote(allQuotes[0])
      expect(allQuotes[0].bridgeModuleName).toEqual('SynapseBridge')
      // TODO: reenable
      // expect(allQuotes[0].gasDropAmount).toEqual(
      //   EXPECTED_GAS_DROP[SupportedChainId.ARBITRUM]
      // )
    })
  })

  describe('applyBridgeSlippage', () => {
    const synapse = new SynapseSDK(
      [SupportedChainId.ETH, SupportedChainId.ARBITRUM],
      [ethProvider, arbProvider]
    )

    const originQuery: Query = {
      routerAdapter: '1',
      tokenOut: '2',
      minAmountOut: BigNumber.from(3),
      deadline: BigNumber.from(4),
      rawParams: '5',
    }
    const destQuery: Query = {
      routerAdapter: '6',
      tokenOut: '7',
      minAmountOut: BigNumber.from(8),
      deadline: BigNumber.from(9),
      rawParams: '10',
    }

    const createApplySlippageTests = (moduleSet: SynapseModuleSet) => {
      describe(`${moduleSet.bridgeModuleName} module`, () => {
        beforeEach(() => {
          jest.spyOn(moduleSet, 'applySlippage').mockImplementation(jest.fn())
        })

        it('Applies slippage', () => {
          synapse.applyBridgeSlippage(
            moduleSet.bridgeModuleName,
            originQuery,
            destQuery,
            10,
            100
          )
          expect(moduleSet.applySlippage).toHaveBeenCalledWith(
            originQuery,
            destQuery,
            10,
            100
          )
        })

        it('Uses default denominator of 10000', () => {
          synapse.applyBridgeSlippage(
            moduleSet.bridgeModuleName,
            originQuery,
            destQuery,
            10
          )
          expect(moduleSet.applySlippage).toHaveBeenCalledWith(
            originQuery,
            destQuery,
            10,
            10000
          )
        })

        it('Uses default slippage of 10 bips', () => {
          synapse.applyBridgeSlippage(
            moduleSet.bridgeModuleName,
            originQuery,
            destQuery
          )
          expect(moduleSet.applySlippage).toHaveBeenCalledWith(
            originQuery,
            destQuery,
            10,
            10000
          )
        })
      })
    }

    createApplySlippageTests(synapse.synapseRouterSet)

    createApplySlippageTests(synapse.synapseCCTPRouterSet)

    createApplySlippageTests(synapse.fastBridgeRouterSet)

    it('Throws on unknown bridge module', () => {
      expect(() =>
        synapse.applyBridgeSlippage(
          'UnknownBridgeModule',
          originQuery,
          destQuery,
          10,
          10000
        )
      ).toThrow('Unknown bridge module')
    })
  })

  describe('Errors', () => {
    const synapse = new SynapseSDK(
      [SupportedChainId.ETH, SupportedChainId.ARBITRUM],
      [ethProvider, arbProvider]
    )

    const amount = BigNumber.from(10).pow(9)
    const emptyQuery: RouterQuery = {
      swapAdapter: AddressZero,
      tokenOut: ETH_USDC,
      minAmountOut: amount,
      deadline: BigNumber.from(0),
      rawParams: '0x',
    }
    const mockAddress = '0x0000000000000000000000000000000000001337'

    describe('origin == destination', () => {
      it('bridgeQuote throws', async () => {
        await expect(
          synapse.bridgeQuote(
            SupportedChainId.ETH,
            SupportedChainId.ETH,
            ETH_USDC,
            BSC_USDC,
            amount
          )
        ).rejects.toThrow(
          'Origin chainId cannot be equal to destination chainId'
        )
      })

      it('bridge throws', async () => {
        await expect(
          synapse.bridge(
            mockAddress,
            ROUTER_ADDRESS_MAP[SupportedChainId.ETH],
            SupportedChainId.ETH,
            SupportedChainId.ETH,
            ETH_USDC,
            amount,
            emptyQuery,
            emptyQuery
          )
        ).rejects.toThrow(
          'Origin chainId cannot be equal to destination chainId'
        )
      })
    })

    it('bridgeQuote throws on unknown chainId', async () => {
      await expect(
        synapse.bridgeQuote(
          SupportedChainId.ETH,
          SupportedChainId.AVALANCHE,
          ETH_USDC,
          AVAX_USDC_E,
          amount
        )
      ).rejects.toThrow('No route found')
    })

    it('bridgeQuote throws when amount too low', async () => {
      await expect(
        synapse.bridgeQuote(
          SupportedChainId.ETH,
          SupportedChainId.ARBITRUM,
          ETH_USDC,
          ARB_USDC,
          BigNumber.from(10).pow(3)
        )
      ).rejects.toThrow('No route found')
    })

    it('bridge throws when incorrect router address', async () => {
      // Use MockAddress as router address
      await expect(
        synapse.bridge(
          mockAddress,
          mockAddress,
          SupportedChainId.ETH,
          SupportedChainId.BSC,
          ETH_USDC,
          amount,
          emptyQuery,
          emptyQuery
        )
      ).rejects.toThrow('Invalid router address')
    })
  })

  describe('Swap', () => {
    const synapse = new SynapseSDK([SupportedChainId.ARBITRUM], [arbProvider])
    const amount = BigNumber.from(10).pow(9)
    const resultPromise: Promise<SwapQuote> = synapse.swapQuote(
      SupportedChainId.ARBITRUM,
      ARB_USDC,
      ARB_USDC_E,
      amount
    )

    createSwapQuoteTests(
      synapse,
      SupportedChainId.ARBITRUM,
      ARB_USDC,
      amount,
      resultPromise
    )
  })

  describe('Bridge Tx Status', () => {
    const synapse = new SynapseSDK(
      [
        SupportedChainId.ARBITRUM,
        SupportedChainId.ETH,
        SupportedChainId.OPTIMISM,
      ],
      [arbProvider, ethProvider, opProvider]
    )

    // https://etherscan.io/tx/0xe3f0f0c1d139c48730492c900f9978449d70c0939c654d5abbfd6b191f9c7b3d
    // https://arbiscan.io/tx/0xb13d5c9156e2d88662fa2f252bd2e1d77d768f0de9d27ca60a79e40b493f6ef2
    const bridgeEthToArbTx = {
      txHash:
        '0xe3f0f0c1d139c48730492c900f9978449d70c0939c654d5abbfd6b191f9c7b3d',
      synapseTxId:
        '0x2f223fb1509f04f777b5c9dd2287931b6e63d994a6a697db7a08cfbe784b5e90',
    }

    // https://arbiscan.io/tx/0xe226c7e38e4b83072aa9d947e533be32c8bb38120bbdd8f490c5c6a5894e62c9
    // https://etherscan.io/tx/0xb88feb2a92690448b840851dff41dbc7cdc975c1fb740f0523b5c2e407ac9f38
    const bridgeArbToEthTx = {
      txHash:
        '0xe226c7e38e4b83072aa9d947e533be32c8bb38120bbdd8f490c5c6a5894e62c9',
      synapseTxId:
        '0xf7b8085d96b1ea3f6bf7a07ad93d1861b8fcd551ef56665d6a22c9fb7633a097',
    }

    // https://etherscan.io/tx/0x1a25b0dfde1e2cc43f1dc659ba60f2b8e7ff8177555773fea0c4fba2d6e9c393
    // https://arbiscan.io/tx/0x0166c1e99b0ec8942ed10527cd7ac9003111ee697e0c0519312228e669a61378
    const cctpEthToArbTx = {
      txHash:
        '0x1a25b0dfde1e2cc43f1dc659ba60f2b8e7ff8177555773fea0c4fba2d6e9c393',
      synapseTxId:
        '0x492b923b5a0ace2715a8d0a80fb93c094bf6d35b142a010bdc3761b8613439fc',
    }

    // https://arbiscan.io/tx/0x2a6d04ba5a48331454f00d136b3666869d03f004395fea25d97d42715c119096
    // https://etherscan.io/tx/0xefb946d2acf8343ac5526de66de498e0d5f70ae73c81b833181616ee058a22d7
    const cctpArbToEthTx = {
      txHash:
        '0x2a6d04ba5a48331454f00d136b3666869d03f004395fea25d97d42715c119096',
      synapseTxId:
        '0xed98b02f712c940d3b37a1aa9005a5986ecefa5cdbb4505118a22ae65d4903af',
    }

    // https://optimistic.etherscan.io/tx/0xf8c736aa8f0455853e68fc4c26c251b6264d77e613efe1cde2a8400ec7a9355f
    // https://arbiscan.io/tx/0x93287ed477a034a4b843088bd60affc78be7fa0a199ae1ae399e82ccebea8a43
    const rfqOpToArbTx = {
      txHash:
        '0xf8c736aa8f0455853e68fc4c26c251b6264d77e613efe1cde2a8400ec7a9355f',
      synapseTxId:
        '0xfe7914246c17a5069024168fda5ceb8f31ed1b1c929da7f586b2a415f75fdc5e',
    }

    // https://arbiscan.io/tx/0xc93e4abbbad0e5c6f724928bf42ed9b8ea9c4ac70483c1a00e374a7f002cdb72
    // https://optimistic.etherscan.io/tx/0xbd45074e933e68795e02c5b3b7378f1911972415aac6e00d33e29933f86b5462
    const rfqArbToOpTx = {
      txHash:
        '0xc93e4abbbad0e5c6f724928bf42ed9b8ea9c4ac70483c1a00e374a7f002cdb72',
      synapseTxId:
        '0xd8e5d4b4658beccfa7ccd69d85c84181cd24b1f8f35d88993c033c0f732b1dd3',
    }

    describe('getSynapseTxId', () => {
      describe('SynapseBridge', () => {
        const ethSynBridge = '0x2796317b0fF8538F253012862c06787Adfb8cEb6'
        const events =
          'TokenDeposit, TokenDepositAndSwap, TokenRedeem, TokenRedeemAndRemove, TokenRedeemAndSwap, TokenRedeemV2'

        it('ETH -> ARB', async () => {
          const synapseTxId = await synapse.getSynapseTxId(
            SupportedChainId.ETH,
            'SynapseBridge',
            bridgeEthToArbTx.txHash
          )
          expect(synapseTxId).toEqual(bridgeEthToArbTx.synapseTxId)
        })

        it('ARB -> ETH', async () => {
          const synapseTxId = await synapse.getSynapseTxId(
            SupportedChainId.ARBITRUM,
            'SynapseBridge',
            bridgeArbToEthTx.txHash
          )
          expect(synapseTxId).toEqual(bridgeArbToEthTx.synapseTxId)
        })

        it('Throws when given a txHash that does not exist', async () => {
          // Use txHash for another chain
          await expect(
            synapse.getSynapseTxId(
              SupportedChainId.ETH,
              'SynapseBridge',
              bridgeArbToEthTx.txHash
            )
          ).rejects.toThrow('Failed to get transaction receipt')
        })

        it('Throws when origin tx does not refer to SynapseBridge', async () => {
          const errorMsg =
            `Contract ${ethSynBridge} in transaction ${cctpEthToArbTx.txHash}` +
            ` did not emit any of the expected events: ${events}`
          await expect(
            synapse.getSynapseTxId(
              SupportedChainId.ETH,
              'SynapseBridge',
              cctpEthToArbTx.txHash
            )
          ).rejects.toThrow(errorMsg)
        })

        it('Throws when given a destination tx', async () => {
          // Destination tx hash for ARB -> ETH
          const txHash =
            '0xefb946d2acf8343ac5526de66de498e0d5f70ae73c81b833181616ee058a22d7'
          const errorMsg =
            `Contract ${ethSynBridge} in transaction ${txHash}` +
            ` did not emit any of the expected events: ${events}`
          await expect(
            synapse.getSynapseTxId(
              SupportedChainId.ETH,
              'SynapseBridge',
              txHash
            )
          ).rejects.toThrow(errorMsg)
        })
      })

      describe('SynapseCCTP', () => {
        const ethSynCCTP = '0x12715a66773BD9C54534a01aBF01d05F6B4Bd35E'
        const events = 'CircleRequestSent'

        it('ETH -> ARB', async () => {
          const synapseTxId = await synapse.getSynapseTxId(
            SupportedChainId.ETH,
            'SynapseCCTP',
            cctpEthToArbTx.txHash
          )
          expect(synapseTxId).toEqual(cctpEthToArbTx.synapseTxId)
        })

        it('ARB -> ETH', async () => {
          const synapseTxId = await synapse.getSynapseTxId(
            SupportedChainId.ARBITRUM,
            'SynapseCCTP',
            cctpArbToEthTx.txHash
          )
          expect(synapseTxId).toEqual(cctpArbToEthTx.synapseTxId)
        })

        it('Throws when given a txHash that does not exist', async () => {
          // Use txHash for another chain
          await expect(
            synapse.getSynapseTxId(
              SupportedChainId.ETH,
              'SynapseCCTP',
              cctpArbToEthTx.txHash
            )
          ).rejects.toThrow('Failed to get transaction receipt')
        })

        it('Throws when origin tx does not refer to SynapseCCTP', async () => {
          const errorMsg =
            `Contract ${ethSynCCTP} in transaction ${bridgeEthToArbTx.txHash}` +
            ` did not emit any of the expected events: ${events}`
          await expect(
            synapse.getSynapseTxId(
              SupportedChainId.ETH,
              'SynapseCCTP',
              bridgeEthToArbTx.txHash
            )
          ).rejects.toThrow(errorMsg)
        })

        it('Throws when given a destination tx', async () => {
          // Destination tx hash for ARB -> ETH
          const txHash =
            '0xefb946d2acf8343ac5526de66de498e0d5f70ae73c81b833181616ee058a22d7'
          const errorMsg =
            `Contract ${ethSynCCTP} in transaction ${txHash}` +
            ` did not emit any of the expected events: ${events}`
          await expect(
            synapse.getSynapseTxId(SupportedChainId.ETH, 'SynapseCCTP', txHash)
          ).rejects.toThrow(errorMsg)
        })
      })

      describe.skip('SynapseRFQ', () => {
        const arbSynRFQ = '0x6C0771aD91442D670159a8171C35F4828E19aFd2'
        const events = 'BridgeRequested'

        it('OP -> ARB', async () => {
          const synapseTxId = await synapse.getSynapseTxId(
            SupportedChainId.OPTIMISM,
            'SynapseRFQ',
            rfqOpToArbTx.txHash
          )
          expect(synapseTxId).toEqual(rfqOpToArbTx.synapseTxId)
        })

        it('ARB -> OP', async () => {
          const synapseTxId = await synapse.getSynapseTxId(
            SupportedChainId.ARBITRUM,
            'SynapseRFQ',
            rfqArbToOpTx.txHash
          )
          expect(synapseTxId).toEqual(rfqArbToOpTx.synapseTxId)
        })

        it('Throws when given a txHash that does not exist', async () => {
          // Use txHash for another chain
          await expect(
            synapse.getSynapseTxId(
              SupportedChainId.OPTIMISM,
              'SynapseRFQ',
              rfqArbToOpTx.txHash
            )
          ).rejects.toThrow('Failed to get transaction receipt')
        })

        it('Throws when origin tx does not refer to SynapseRFQ', async () => {
          const errorMsg =
            `Contract ${arbSynRFQ} in transaction ${bridgeArbToEthTx.txHash}` +
            ` did not emit any of the expected events: ${events}`
          await expect(
            synapse.getSynapseTxId(
              SupportedChainId.ARBITRUM,
              'SynapseRFQ',
              bridgeArbToEthTx.txHash
            )
          ).rejects.toThrow(errorMsg)
        })

        it('Throws when given a destination tx', async () => {
          // Destination tx hash for OP -> ARB
          const txHash =
            '0x53a8e543bc0e3f0c1cae509e50d9435c3b62073eecf1aee7ece63c3be285db30'
          const errorMsg =
            `Contract ${arbSynRFQ} in transaction ${txHash}` +
            ` did not emit any of the expected events: ${events}`
          await expect(
            synapse.getSynapseTxId(
              SupportedChainId.ARBITRUM,
              'SynapseRFQ',
              txHash
            )
          ).rejects.toThrow(errorMsg)
        })
      })

      it('Throws when bridge module name is invalid', async () => {
        await expect(
          synapse.getSynapseTxId(
            SupportedChainId.ETH,
            'SynapseSynapse',
            bridgeEthToArbTx.txHash
          )
        ).rejects.toThrow('Unknown bridge module')
      })
    })

    describe('getBridgeTxStatus', () => {
      describe('SynapseBridge', () => {
        it('ETH -> ARB', async () => {
          const txStatus = await synapse.getBridgeTxStatus(
            SupportedChainId.ARBITRUM,
            'SynapseBridge',
            bridgeEthToArbTx.synapseTxId
          )
          expect(txStatus).toBe(true)
        })

        it('ARB -> ETH', async () => {
          const txStatus = await synapse.getBridgeTxStatus(
            SupportedChainId.ETH,
            'SynapseBridge',
            bridgeArbToEthTx.synapseTxId
          )
          expect(txStatus).toBe(true)
        })

        it('Returns false when unknown synapseTxId', async () => {
          // Using txHash instead of synapseTxId
          const txStatus = await synapse.getBridgeTxStatus(
            SupportedChainId.ETH,
            'SynapseBridge',
            bridgeArbToEthTx.txHash
          )
          expect(txStatus).toBe(false)
        })

        it('Returns false when origin chain is used instead of destination', async () => {
          // First argument should be destination chainId
          const txStatus = await synapse.getBridgeTxStatus(
            SupportedChainId.ETH,
            'SynapseBridge',
            bridgeEthToArbTx.synapseTxId
          )
          expect(txStatus).toBe(false)
        })
      })

      describe('SynapseCCTP', () => {
        it('ETH -> ARB', async () => {
          const txStatus = await synapse.getBridgeTxStatus(
            SupportedChainId.ARBITRUM,
            'SynapseCCTP',
            cctpEthToArbTx.synapseTxId
          )
          expect(txStatus).toBe(true)
        })

        it('ARB -> ETH', async () => {
          const txStatus = await synapse.getBridgeTxStatus(
            SupportedChainId.ETH,
            'SynapseCCTP',
            cctpArbToEthTx.synapseTxId
          )
          expect(txStatus).toBe(true)
        })

        it('Returns false when unknown synapseTxId', async () => {
          // Using txHash instead of synapseTxId
          const txStatus = await synapse.getBridgeTxStatus(
            SupportedChainId.ETH,
            'SynapseCCTP',
            cctpArbToEthTx.txHash
          )
          expect(txStatus).toBe(false)
        })

        it('Returns false when origin chain is used instead of destination', async () => {
          // First argument should be destination chainId
          const txStatus = await synapse.getBridgeTxStatus(
            SupportedChainId.ETH,
            'SynapseCCTP',
            cctpEthToArbTx.synapseTxId
          )
          expect(txStatus).toBe(false)
        })
      })

      describe.skip('SynapseRFQ', () => {
        it('OP -> ARB', async () => {
          const txStatus = await synapse.getBridgeTxStatus(
            SupportedChainId.ARBITRUM,
            'SynapseRFQ',
            rfqOpToArbTx.synapseTxId
          )
          expect(txStatus).toBe(true)
        })

        it('ARB -> OP', async () => {
          const txStatus = await synapse.getBridgeTxStatus(
            SupportedChainId.OPTIMISM,
            'SynapseRFQ',
            rfqArbToOpTx.synapseTxId
          )
          expect(txStatus).toBe(true)
        })

        it('Returns false when unknown synapseTxId', async () => {
          // Using txHash instead of synapseTxId
          const txStatus = await synapse.getBridgeTxStatus(
            SupportedChainId.OPTIMISM,
            'SynapseRFQ',
            rfqArbToOpTx.txHash
          )
          expect(txStatus).toBe(false)
        })

        it('Returns false when origin chain is used instead of destination', async () => {
          // First argument should be destination chainId
          const txStatus = await synapse.getBridgeTxStatus(
            SupportedChainId.OPTIMISM,
            'SynapseRFQ',
            rfqOpToArbTx.synapseTxId
          )
          expect(txStatus).toBe(false)
        })
      })

      it('Throws when bridge module name is invalid', async () => {
        await expect(
          synapse.getBridgeTxStatus(
            SupportedChainId.ETH,
            'SynapseSynapse',
            bridgeEthToArbTx.txHash
          )
        ).rejects.toThrow('Unknown bridge module')
      })
    })
  })

  describe('getBridgeModuleName', () => {
    const synapse = new SynapseSDK([], [])

    // https://github.com/synapsecns/synapse-contracts/blob/3f592a879baa4487a62ca8d2cfd44d329bc22e62/contracts/bridge/SynapseBridge.sol#L63-L121
    describe('SynapseBridge events', () => {
      const contractEvents = [
        'TokenDeposit',
        'TokenRedeem',
        'TokenWithdraw',
        'TokenMint',
        'TokenDepositAndSwap',
        'TokenMintAndSwap',
        'TokenRedeemAndSwap',
        'TokenRedeemAndRemove',
        'TokenWithdrawAndRemove',
        'TokenRedeemV2',
      ]

      contractEvents.forEach((contractEvent) => {
        it(contractEvent, () => {
          // Event naming in contract and explorer is a bit different
          // schema: TokenDeposit => DepositEvent
          const explorerEvent = `${contractEvent.slice(5)}Event`
          expect(synapse.getBridgeModuleName(explorerEvent)).toEqual(
            'SynapseBridge'
          )
        })
      })
    })

    // https://github.com/synapsecns/synapse-contracts/blob/3f592a879baa4487a62ca8d2cfd44d329bc22e62/contracts/cctp/events/SynapseCCTPEvents.sol#L5-L45
    describe('SynapseCCTP events', () => {
      const contractEvents = ['CircleRequestSent', 'CircleRequestFulfilled']

      contractEvents.forEach((contractEvent) => {
        it(contractEvent, () => {
          // Event naming in contract and explorer is a bit different
          // schema: CircleRequestSent => CircleRequestSentEvent
          const explorerEvent = `${contractEvent}Event`
          expect(synapse.getBridgeModuleName(explorerEvent)).toEqual(
            'SynapseCCTP'
          )
        })
      })
    })

    it('Throws when event name is unknown', () => {
      expect(() => synapse.getBridgeModuleName('SomeUnknownEvent')).toThrow(
        'Unknown event'
      )
    })
  })

  describe('getEstimatedTime', () => {
    const synapse = new SynapseSDK(
      [SupportedChainId.ETH, SupportedChainId.MOONBEAM],
      [ethProvider, moonbeamProvider]
    )

    describe('Chain with a provider', () => {
      it('Returns estimated time for SynapseBridge', () => {
        expect(
          synapse.getEstimatedTime(SupportedChainId.ETH, 'SynapseBridge')
        ).toEqual(MEDIAN_TIME_BRIDGE[SupportedChainId.ETH])

        expect(
          synapse.getEstimatedTime(SupportedChainId.MOONBEAM, 'SynapseBridge')
        ).toEqual(MEDIAN_TIME_BRIDGE[SupportedChainId.MOONBEAM])
      })

      it('Returns estimated time for SynapseCCTP', () => {
        expect(
          synapse.getEstimatedTime(SupportedChainId.ETH, 'SynapseCCTP')
        ).toEqual(MEDIAN_TIME_CCTP[SupportedChainId.ETH])
      })

      it('Throws when bridge module does not exist on a chain', () => {
        expect(() =>
          synapse.getEstimatedTime(SupportedChainId.MOONBEAM, 'SynapseCCTP')
        ).toThrow('No estimated time for chain 1284')
      })

      it('Throws when bridge module name is invalid', () => {
        expect(() =>
          synapse.getEstimatedTime(SupportedChainId.ETH, 'SynapseSynapse')
        ).toThrow('Unknown bridge module')
      })
    })

    describe('Chain without a provider', () => {
      it('Returns estimated time for SynapseBridge', () => {
        expect(
          synapse.getEstimatedTime(SupportedChainId.BSC, 'SynapseBridge')
        ).toEqual(MEDIAN_TIME_BRIDGE[SupportedChainId.BSC])
      })

      it('Returns estimated time for SynapseCCTP', () => {
        expect(
          synapse.getEstimatedTime(SupportedChainId.ARBITRUM, 'SynapseCCTP')
        ).toEqual(MEDIAN_TIME_CCTP[SupportedChainId.ARBITRUM])
      })

      it('Throws when bridge module does not exist on a chain', () => {
        expect(() =>
          synapse.getEstimatedTime(SupportedChainId.DOGECHAIN, 'SynapseCCTP')
        ).toThrow('No estimated time for chain 2000')
      })

      it('Throws when bridge module name is invalid', () => {
        expect(() =>
          synapse.getEstimatedTime(SupportedChainId.BSC, 'SynapseSynapse')
        ).toThrow('Unknown bridge module')
      })
    })
  })

  describe('Get bridge gas', () => {
    const synapse = new SynapseSDK(
      [SupportedChainId.ETH, SupportedChainId.ARBITRUM],
      [ethProvider, arbProvider]
    )

    it('Returns zero for ETH', async () => {
      const gas = await synapse.getBridgeGas(SupportedChainId.ETH)
      expect(gas).toEqual(Zero)
    })

    it('Returns non-zero for ARBITRUM', async () => {
      const gas = await synapse.getBridgeGas(SupportedChainId.ARBITRUM)
      expect(gas.gt(0)).toBe(true)
    })
  })

  // TODO: improve tests
  describe('Pool inspection', () => {
    const synapse = new SynapseSDK([SupportedChainId.ARBITRUM], [arbProvider])

    it('Get all pools', async () => {
      const pools = await synapse.getAllPools(SupportedChainId.ARBITRUM)
      expect(pools).toBeDefined()
      expect(pools.length).toBeGreaterThan(0)
      expect(pools[0]?.tokens?.[0]?.token?.length).toBeGreaterThan(0)
    })

    it('Get pool info', async () => {
      const poolInfo = await synapse.getPoolInfo(
        SupportedChainId.ARBITRUM,
        ARB_POOL_NUSD
      )
      expect(poolInfo).toBeDefined()
      expect(poolInfo.lpToken).toEqual(
        '0xcFd72be67Ee69A0dd7cF0f846Fc0D98C33d60F16'
      )
      expect(poolInfo.tokens).toEqual(BigNumber.from(3))
    })

    it('Get pool tokens', async () => {
      const poolTokens = await synapse.getPoolTokens(
        SupportedChainId.ARBITRUM,
        ARB_POOL_NUSD
      )
      expect(poolTokens).toBeDefined()
      expect(poolTokens.length).toEqual(3)
      expect(poolTokens[0].token).toEqual(ARB_NUSD)
      expect(poolTokens[1].token).toEqual(ARB_USDC_E)
      expect(poolTokens[2].token).toEqual(ARB_USDT)
    })
  })

  describe('calculate add liquidity', () => {
    const synapse = new SynapseSDK(
      [SupportedChainId.ARBITRUM, SupportedChainId.ETH],
      [arbProvider, ethProvider]
    )

    it('Arbitrum nETH pool', async () => {
      const amounts: Record<string, BigNumber> = {}
      amounts[ARB_NETH] = BigNumber.from(10).pow(18)
      amounts[ARB_WETH] = BigNumber.from(10).pow(18).mul(2)
      const result = await synapse.calculateAddLiquidity(
        SupportedChainId.ARBITRUM,
        ARB_POOL_NETH,
        amounts
      )
      expect(result).toBeDefined()
      expect(result.amount.gt(0)).toBe(true)
      expect(result.routerAddress).toEqual(
        ROUTER_ADDRESS_MAP[SupportedChainId.ARBITRUM]
      )
    })

    it('Handles lowercase token addresses', async () => {
      const amounts: Record<string, BigNumber> = {}
      amounts[ARB_NETH.toLowerCase()] = BigNumber.from(10).pow(18)
      amounts[ARB_WETH.toLowerCase()] = BigNumber.from(10).pow(18).mul(2)
      const result = await synapse.calculateAddLiquidity(
        SupportedChainId.ARBITRUM,
        ARB_POOL_NETH,
        amounts
      )
      expect(result).toBeDefined()
      expect(result.amount.gt(0)).toBe(true)
      expect(result.routerAddress).toEqual(
        ROUTER_ADDRESS_MAP[SupportedChainId.ARBITRUM]
      )
    })

    it('Handles uppercase token addresses', async () => {
      const amounts: Record<string, BigNumber> = {}
      amounts[ARB_NETH.toUpperCase()] = BigNumber.from(10).pow(18)
      amounts[ARB_WETH.toLowerCase()] = BigNumber.from(10).pow(18).mul(2)
      const result = await synapse.calculateAddLiquidity(
        SupportedChainId.ARBITRUM,
        ARB_POOL_NETH,
        amounts
      )
      expect(result).toBeDefined()
      expect(result.amount.gt(0)).toBe(true)
      expect(result.routerAddress).toEqual(
        ROUTER_ADDRESS_MAP[SupportedChainId.ARBITRUM]
      )
    })

    it('Handles single token: first one', async () => {
      const amounts: Record<string, BigNumber> = {}
      amounts[ARB_NETH] = BigNumber.from(10).pow(18)
      const amountsFull: Record<string, BigNumber> = {}
      amountsFull[ARB_NETH] = BigNumber.from(10).pow(18)
      amountsFull[ARB_WETH] = Zero
      const result = await synapse.calculateAddLiquidity(
        SupportedChainId.ARBITRUM,
        ARB_POOL_NETH,
        amounts
      )
      const expectedResult = await synapse.calculateAddLiquidity(
        SupportedChainId.ARBITRUM,
        ARB_POOL_NETH,
        amountsFull
      )
      expect(result).toBeDefined()
      expect(expectedResult).toBeDefined()
      expect(result).toEqual(expectedResult)
    })

    it('Handles single token: second one', async () => {
      const amounts: Record<string, BigNumber> = {}
      amounts[ARB_WETH] = BigNumber.from(10).pow(18)
      const amountsFull: Record<string, BigNumber> = {}
      amountsFull[ARB_NETH] = Zero
      amountsFull[ARB_WETH] = BigNumber.from(10).pow(18)
      const result = await synapse.calculateAddLiquidity(
        SupportedChainId.ARBITRUM,
        ARB_POOL_NETH,
        amounts
      )
      const expectedResult = await synapse.calculateAddLiquidity(
        SupportedChainId.ARBITRUM,
        ARB_POOL_NETH,
        amountsFull
      )
      expect(result).toBeDefined()
      expect(expectedResult).toBeDefined()
      expect(result).toEqual(expectedResult)
    })

    it('Ethereum nUSD pool', async () => {
      const amounts: Record<string, BigNumber> = {}
      amounts[ETH_USDC] = BigNumber.from(10).pow(6)
      amounts[ETH_DAI] = BigNumber.from(10).pow(18).mul(2)
      amounts[ETH_USDT] = BigNumber.from(10).pow(6).mul(3)
      const result = await synapse.calculateAddLiquidity(
        SupportedChainId.ETH,
        ETH_POOL_NUSD,
        amounts
      )
      expect(result).toBeDefined()
      expect(result.amount.gt(0)).toBe(true)
      expect(result.routerAddress).toEqual(
        ROUTER_ADDRESS_MAP[SupportedChainId.ETH]
      )
    })
  })

  describe('calculate remove liquidity', () => {
    const synapse = new SynapseSDK(
      [SupportedChainId.ARBITRUM, SupportedChainId.ETH],
      [arbProvider, ethProvider]
    )

    it('Arbitrum EthWrapper', async () => {
      const result = await synapse.calculateRemoveLiquidity(
        SupportedChainId.ARBITRUM,
        ARB_POOL_ETH_WRAPPER,
        BigNumber.from(10).pow(18)
      )
      expect(result).toBeDefined()
      expect(result.amounts.length).toEqual(2)
      expect(result.amounts[0].value.gt(0)).toBe(true)
      expect(result.amounts[0].index).toEqual(0)
      expect(result.amounts[1].value.gt(0)).toBe(true)
      expect(result.amounts[1].index).toEqual(1)
      expect(result.routerAddress).toEqual(
        ROUTER_ADDRESS_MAP[SupportedChainId.ARBITRUM]
      )
    })
  })

  describe('calculate remove liquidity one', () => {
    const synapse = new SynapseSDK(
      [SupportedChainId.ARBITRUM, SupportedChainId.ETH],
      [arbProvider, ethProvider]
    )

    it('Arbitrum EthWrapper', async () => {
      const poolIndex = 0
      const result = await synapse.calculateRemoveLiquidityOne(
        SupportedChainId.ARBITRUM,
        ARB_POOL_ETH_WRAPPER,
        BigNumber.from(10).pow(18),
        poolIndex
      )
      expect(result).toBeDefined()
      expect(result.amount.value.gt(0)).toBe(true)
      expect(result.amount.index).toEqual(poolIndex)
      expect(result.routerAddress).toEqual(
        ROUTER_ADDRESS_MAP[SupportedChainId.ARBITRUM]
      )
    })
  })

  describe('Internal functions', () => {
    const synapse = new SynapseSDK(
      [SupportedChainId.ARBITRUM, SupportedChainId.ETH],
      [arbProvider, ethProvider]
    )
    describe('getModuleSet', () => {
      it('Returns correct set for SynapseBridge', () => {
        const routerSet = operations.getModuleSet.call(synapse, 'SynapseBridge')
        expect(routerSet).toEqual(synapse.synapseRouterSet)
      })

      it('Returns correct set for SynapseCCTP', () => {
        const routerSet = operations.getModuleSet.call(synapse, 'SynapseCCTP')
        expect(routerSet).toEqual(synapse.synapseCCTPRouterSet)
      })

      it('Returns correct set for SynapseRFQ', () => {
        const routerSet = operations.getModuleSet.call(synapse, 'SynapseRFQ')
        expect(routerSet).toEqual(synapse.fastBridgeRouterSet)
      })

      it('Throws when bridge module name is invalid', () => {
        expect(() =>
          operations.getModuleSet.call(synapse, 'SynapseSynapse')
        ).toThrow('Unknown bridge module')
      })
    })
  })
})