diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index a1e9cf6fc0..f6308b0f14 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -1,4 +1,4 @@ -name: 'Cypress Test' +name: 'E2E Smoke' # concurrency: # group: pr-workflow-${{ github.ref }} @@ -116,7 +116,7 @@ jobs: run: |+ #!/bin/bash yarn preview & - yarn test-e2e -c baseUrl='http://127.0.0.1:4173/' -e grepTags=smoke,NETWORK=Ethereum + yarn test:e2e -c baseUrl='http://127.0.0.1:4173/' -e grepTags=smoke,NETWORK=Ethereum env: DISPLAY: :0.0 diff --git a/.github/workflows/schedule.yml b/.github/workflows/schedule.yml index 8649a988e8..0cb4af026e 100644 --- a/.github/workflows/schedule.yml +++ b/.github/workflows/schedule.yml @@ -1,4 +1,4 @@ -name: 'E2E Testing Schedule' +name: 'E2E Regression' on: schedule: @@ -54,7 +54,7 @@ jobs: - name: Run Cypress Test run: |+ #!/bin/bash - yarn test-schedule -c baseUrl='https://kyberswap.com/' -e grepTags=regression,NETWORK=${{ matrix.network }} + yarn test:e2e -c baseUrl='https://kyberswap.com/' -e grepTags=regression,NETWORK=${{ matrix.network }} env: DISPLAY: :0.0 diff --git a/cypress.config.ts b/cypress.config.ts index 1085451248..ff52e33d50 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -13,6 +13,8 @@ export default defineConfig({ }, userAgent: 'synpress', chromeWebSecurity: true, + // video: false, + // videoCompression: false, viewportWidth: 1920, viewportHeight: 1080, env: { @@ -20,10 +22,20 @@ export default defineConfig({ grepOmitFiltered: true, }, e2e: { + testIsolation: false, setupNodeEvents(on, config) { // eslint-disable-next-line @typescript-eslint/no-var-requires require('@cypress/grep/src/plugin')(config) synpressPlugins(on, config) + const options = { + printLogsToFile: 'always', + outputRoot: config.projectRoot + '/target/', + specRoot: 'cypress/e2e/specs', + outputTarget: { + 'cypress-logs|json': 'json', + }, + } + require('cypress-terminal-report/src/installLogsPrinter')(on, options) on('after:run', async results => { if (results) { const register = new client.Registry() diff --git a/cypress/e2e/pages/header.po.cy.ts b/cypress/e2e/pages/header.po.cy.ts new file mode 100644 index 0000000000..e7641eb8dc --- /dev/null +++ b/cypress/e2e/pages/header.po.cy.ts @@ -0,0 +1,23 @@ +import { + CrossChainLocators, + HeaderLocators, + LimitOrderLocators, + NetworkLocators, + SwapPageLocators, + TokenCatalogLocators, + WalletLocators, +} from '../selectors/selectors.cy' + +export interface myCallbackType { + (myArgument: T): void +} +export const Header = { + connectWallet() { + cy.get(WalletLocators.btnConnectWallet).should('be.visible').click() + cy.connectWallet() + }, + + getStatusConnectedWallet() { + cy.get(WalletLocators.statusConnected, { timeout: 10000 }).should('be.visible') + }, +} diff --git a/cypress/e2e/pages/pools-page.po.cy.ts b/cypress/e2e/pages/pools-page.po.cy.ts new file mode 100644 index 0000000000..2975f3b1a5 --- /dev/null +++ b/cypress/e2e/pages/pools-page.po.cy.ts @@ -0,0 +1,51 @@ +import { PoolLocators } from '../selectors/selectors.cy' + +export enum CustomRange { + FullRange = 'Full Range', + Safe = 'Safe', + Common = 'Common', + Expert = 'Expert', +} +export type FarmingRange = { + minPrice: string + maxPrice: string +} +export const PoolsPage = { + open(chain: string) { + cy.visit('/pools/' + chain) + }, + searchByPoolAddress(poolAddress: string) { + cy.get(PoolLocators.txtSearchPool).clear().type(poolAddress) + cy.wait(2000) + }, + selectCustomRange(range: CustomRange) { + cy.get('button').contains(range).click() + }, + selectFarmingRange(farmingRange: FarmingRange) { + cy.get('[role=button]').contains('Farming Ranges').click() + cy.get(PoolLocators.txtPriceValue).eq(0).clear().type(farmingRange.minPrice) + cy.get(PoolLocators.txtPriceValue).eq(1).clear().type(farmingRange.maxPrice) + }, + getCurrentPrice() { + return cy + .get(PoolLocators.lblCurrentPrice) + .invoke('text') + .then(currenPriceValue => { + return currenPriceValue + }) + }, + addLiquidity(poolAddress: string, amountIn: string, customRange?: CustomRange, farmingRange?: FarmingRange) { + PoolsPage.searchByPoolAddress(poolAddress) + cy.get('button').contains('Add Liquidity').click() + cy.get(PoolLocators.btnZapIn).click() + if (typeof customRange != 'undefined') { + PoolsPage.selectCustomRange(customRange) + } + if (typeof farmingRange != 'undefined') { + PoolsPage.selectFarmingRange(farmingRange) + } + cy.get(PoolLocators.txtAmountIn).type(amountIn) + cy.wait(20000) + cy.go('back') + }, +} diff --git a/cypress/e2e/pages/swap-page.po.cy.ts b/cypress/e2e/pages/swap-page.po.cy.ts index 36e67bad01..af6d09aed8 100644 --- a/cypress/e2e/pages/swap-page.po.cy.ts +++ b/cypress/e2e/pages/swap-page.po.cy.ts @@ -1,127 +1,137 @@ -import { CrossChainLocators, HeaderLocators, LimitOrderLocators, NetworkLocators, SwapPageLocators, TokenCatalogLocators, WalletLocators } from "../selectors/selectors.cy" +import { + CrossChainLocators, + HeaderLocators, + LimitOrderLocators, + NetworkLocators, + SwapPageLocators, + TokenCatalogLocators, + WalletLocators, +} from '../selectors/selectors.cy' + +export const NETWORK = Cypress.env('NETWORK') export interface myCallbackType { - (myArgument: T): void + (myArgument: T): void } export const SwapPage = { - open(url: string) { - cy.visit('/' + url) - cy.url().should('include', url) - cy.closeTutorialPopup() - }, - - selectTokenIn(): TokenCatalog { - cy.selectToken(SwapPageLocators.dropdownTokenIn) - return new TokenCatalog() - }, - selectTokenOut(): TokenCatalog { - cy.selectToken(SwapPageLocators.dropdownTokenOut) - return new TokenCatalog() - }, - - getCurrentTokenIn(text: myCallbackType) { - cy.getContent(SwapPageLocators.dropdownTokenIn, text) - }, - - getCurrentTokenOut(text: myCallbackType) { - cy.getContent(SwapPageLocators.dropdownTokenOut, text) - }, - - connectWallet() { - cy.get(WalletLocators.btnConnectWallet).should('be.visible').click() - cy.connectWallet() - }, - - getStatusConnectedWallet() { - cy.get(WalletLocators.statusConnected, { timeout: 10000 }).should('be.visible') - }, - - goToLimitOrder() { - cy.get(LimitOrderLocators.btnLimit).click() - }, - - goToCrossChain() { - cy.get(CrossChainLocators.btnCrossChain).click() - }, - - goToFarmPage() { - cy.get(HeaderLocators.dropdownEarn).click({ force: true }) - cy.get(HeaderLocators.lblFarms).click({ force: true }) - }, - - goToPoolPage() { - cy.get(HeaderLocators.dropdownEarn).click({ force: true }) - cy.get(HeaderLocators.lblPools).click({ force: true }) - }, - - goToMyPoolsPage() { - cy.get(HeaderLocators.dropdownEarn).click({ force: true }) - cy.get(HeaderLocators.lblMyPools).click({ force: true }) - }, + open(url: string) { + cy.visit('/' + url) + cy.url().should('include', url) + cy.closeTutorialPopup() + }, + + selectTokenIn(): TokenCatalog { + cy.selectToken(SwapPageLocators.dropdownTokenIn) + return new TokenCatalog() + }, + selectTokenOut(): TokenCatalog { + cy.selectToken(SwapPageLocators.dropdownTokenOut) + return new TokenCatalog() + }, + + getCurrentTokenIn(text: myCallbackType) { + cy.getContent(SwapPageLocators.dropdownTokenIn, text) + }, + + getCurrentTokenOut(text: myCallbackType) { + cy.getContent(SwapPageLocators.dropdownTokenOut, text) + }, + + connectWallet() { + cy.get(WalletLocators.btnConnectWallet).should('be.visible').click() + cy.connectWallet() + }, + + getStatusConnectedWallet() { + cy.get(WalletLocators.statusConnected, { timeout: 10000 }).should('be.visible') + }, + + goToLimitOrder() { + cy.get(LimitOrderLocators.btnLimit).click() + }, + + goToCrossChain() { + cy.get(CrossChainLocators.btnCrossChain).click() + }, + + goToFarmPage() { + cy.get(HeaderLocators.dropdownEarn).click({ force: true }) + cy.get(HeaderLocators.lblFarms).click({ force: true }) + }, + + goToPoolPage() { + cy.get(HeaderLocators.dropdownEarn).click({ force: true }) + cy.get(HeaderLocators.lblPools).click({ force: true }) + }, + + goToMyPoolsPage() { + cy.get(HeaderLocators.dropdownEarn).click({ force: true }) + cy.get(HeaderLocators.lblMyPools).click({ force: true }) + }, } export class Network { - selectNetwork(network: string) { - cy.get(NetworkLocators.btnSelectNetwork, { timeout: 30000 }).should('be.visible').click() - cy.get(NetworkLocators.btnNetwork).contains(network).click({ force: true }) - } + selectNetwork(network: string) { + cy.get(NetworkLocators.btnSelectNetwork, { timeout: 30000 }).should('be.visible').click() + cy.get(NetworkLocators.btnNetwork).contains(network).click({ force: true }) + } } export class TokenCatalog { - searchToken(value: string) { - cy.searchToken(value) - } - - selectImportTab() { - cy.selectImportTab() - } - - selectFavoriteToken(tokenSymbol: string) { - cy.selectFavoriteToken(tokenSymbol) - } - - selectTokenBySymbol(tokenSymbol: string) { - this.searchToken(tokenSymbol) - cy.selectTokenBySymbol(tokenSymbol) - } - - addFavoriteToken(tokenSymbol: Array) { - tokenSymbol.forEach(element => { - this.searchToken(element) - cy.wait(2000) - cy.addFavoriteToken() - }); - } - - removeFavoriteToken(tokenSymbol: string) { - cy.removeFavoriteToken(tokenSymbol) - } - - importNewTokens(address: Array) { - address.forEach(element => { - SwapPage.selectTokenIn() - cy.importNewToken(element) - }) - } - - deleteImportedToken(value: string) { - cy.deleteImportedToken(value) - } - - clearAllImportedTokens() { - cy.clearAllImportedTokens() - } - - getFavoriteTokens(list: myCallbackType) { - cy.getList(TokenCatalogLocators.lblFavoriteToken, list) - } - - getWhitelistTokens(list: myCallbackType) { - cy.getList(TokenCatalogLocators.lblTokenSymbol, list) - } - - getNoResultsFound(text: myCallbackType) { - cy.getContent(TokenCatalogLocators.lblNotFound, text) - } -} \ No newline at end of file + searchToken(value: string) { + cy.searchToken(value) + } + + selectImportTab() { + cy.selectImportTab() + } + + selectFavoriteToken(tokenSymbol: string) { + cy.selectFavoriteToken(tokenSymbol) + } + + selectTokenBySymbol(tokenSymbol: string) { + this.searchToken(tokenSymbol) + cy.selectTokenBySymbol(tokenSymbol) + } + + addFavoriteToken(tokenSymbol: Array) { + tokenSymbol.forEach(element => { + this.searchToken(element) + cy.wait(2000) + cy.addFavoriteToken() + }) + } + + removeFavoriteToken(tokenSymbol: string) { + cy.removeFavoriteToken(tokenSymbol) + } + + importNewTokens(address: Array) { + address.forEach(element => { + SwapPage.selectTokenIn() + cy.importNewToken(element) + }) + } + + deleteImportedToken(value: string) { + cy.deleteImportedToken(value) + } + + clearAllImportedTokens() { + cy.clearAllImportedTokens() + } + + getFavoriteTokens(list: myCallbackType) { + cy.getList(TokenCatalogLocators.lblFavoriteToken, list) + } + + getWhitelistTokens(list: myCallbackType) { + cy.getList(TokenCatalogLocators.lblTokenSymbol, list) + } + + getNoResultsFound(text: myCallbackType) { + cy.getContent(TokenCatalogLocators.lblNotFound, text) + } +} diff --git a/cypress/e2e/selectors/constants.cy.ts b/cypress/e2e/selectors/constants.cy.ts index 00e804a953..5914bc8043 100644 --- a/cypress/e2e/selectors/constants.cy.ts +++ b/cypress/e2e/selectors/constants.cy.ts @@ -1,99 +1,105 @@ export const UNWHITELIST_SYMBOL_TOKENS = ['KNNC', 'KCCN'] -export const NORESULTS_TEXT = "No results found." -export const NOTOKENS_TEXT = "Select a token" -export const DEFAULT_NETWORK = "Ethereum" -export const NETWORK = Cypress.env('NETWORK') +export const NORESULTS_TEXT = 'No results found.' +export const NOTOKENS_TEXT = 'Select a token' +export const DEFAULT_NETWORK = 'Ethereum' +export const NETWORK: string = Cypress.env('NETWORK') export const DEFAULT_URL = `swap/${NETWORK}`.toLowerCase() - export enum TAG { - smoke = 'smoke', - regression = 'regression', + smoke = 'smoke', + regression = 'regression', + zap = 'zap', } export const TOKEN_SYMBOLS = { - 'Ethereum': ['BAND', 'ETH', 'USDT', 'USDC', '1INCH'], - 'Arbitrum': ['ANGLE', 'ARB', 'USDT', 'USDC.e', 'BOB'], - 'Optimism': ['BOND', 'ETH', 'USDT', 'USDC', 'BOB'], - 'Avalanche': ['AAVE.e', 'sAVAX', 'USDT.e', 'USDC.e', 'BUSD.e'], - 'BNB': ['RICE', 'BUSD', 'USDT', 'USDC', 'BOB'] + Ethereum: ['BAND', 'ETH', 'USDT', 'USDC', '1INCH'], + Arbitrum: ['ANGLE', 'ARB', 'USDT', 'USDC.e', 'BOB'], + Optimism: ['BOND', 'ETH', 'USDT', 'USDC', 'BOB'], + Avalanche: ['AAVE.e', 'sAVAX', 'USDT.e', 'USDC.e', 'BUSD.e'], + BNB: ['RICE', 'BUSD', 'USDT', 'USDC', 'BOB'], } -export const NETWORK_LIST = ['Ethereum', 'Arbitrum', 'Optimism', 'Avalanche', 'BNB Chain', 'Polygon PoS', 'Fantom', 'Linea', 'Base'] +export const NETWORK_LIST = [ + 'Ethereum', + 'Arbitrum', + 'Optimism', + 'Avalanche', + 'BNB Chain', + 'Polygon PoS', + 'Fantom', + 'Linea', + 'Base', +] export const UNWHITELIST_TOKENS = { - "Ethereum": - [ - { - symbol: 'SCOOBY', - address: '0xAd497eE6a70aCcC3Cbb5eB874e60d87593B86F2F', - }, - { - symbol: 'UNIBOT', - address: '0x25127685dc35d4dc96c7feac7370749d004c5040', - }, - { - symbol: 'BGB', - address: '0x19de6b897ed14a376dda0fe53a5420d2ac828a28', - }, - ], - "Arbitrum": [ - { - symbol: 'OHM', - address: '0xf0cb2dc0db5e6c66b9a70ac27b06b878da017028', - }, - { - symbol: 'GBL', - address: '0xe9a264e9d45ff72e1b4a85d77643cdbd4c950207', - }, - { - symbol: 'Y2K', - address: '0x65c936f008bc34fe819bce9fa5afd9dc2d49977f', - }, - ], - "Optimism": - [ - { - symbol: 'CHI', - address: '0xca0e54b636db823847b29f506bffee743f57729d', - }, - { - symbol: 'ACX', - address: '0xFf733b2A3557a7ed6697007ab5D11B79FdD1b76B', - }, - { - symbol: 'PSP', - address: '0xd3594e879b358f430e20f82bea61e83562d49d48', - }, - ], - "Avalanche": - [ - { - symbol: 'RADIO', - address: '0x02bfd11499847003de5f0f5aa081c43854d48815', - }, - { - symbol: 'EUROC', - address: '0xc891eb4cbdeff6e073e859e987815ed1505c2acd', - }, - { - symbol: 'MELD', - address: '0x333000333b26ee30214b4af6419d9ab07a450400', - }, - ], - "BNB": - [ - { - symbol: 'TUSD', - address: '0x40af3827f39d0eacbf4a168f8d4ee67c121d11c9', - }, - { - symbol: 'ARA', - address: '0x5542958fa9bd89c96cb86d1a6cb7a3e644a3d46e', - }, - { - symbol: 'FLASH', - address: '0xc3111096b3b46873393055dea14036ea603cfa95', - } - ], -} \ No newline at end of file + Ethereum: [ + { + symbol: 'SCOOBY', + address: '0xAd497eE6a70aCcC3Cbb5eB874e60d87593B86F2F', + }, + { + symbol: 'UNIBOT', + address: '0x25127685dc35d4dc96c7feac7370749d004c5040', + }, + { + symbol: 'BGB', + address: '0x19de6b897ed14a376dda0fe53a5420d2ac828a28', + }, + ], + Arbitrum: [ + { + symbol: 'OHM', + address: '0xf0cb2dc0db5e6c66b9a70ac27b06b878da017028', + }, + { + symbol: 'GBL', + address: '0xe9a264e9d45ff72e1b4a85d77643cdbd4c950207', + }, + { + symbol: 'Y2K', + address: '0x65c936f008bc34fe819bce9fa5afd9dc2d49977f', + }, + ], + Optimism: [ + { + symbol: 'CHI', + address: '0xca0e54b636db823847b29f506bffee743f57729d', + }, + { + symbol: 'ACX', + address: '0xFf733b2A3557a7ed6697007ab5D11B79FdD1b76B', + }, + { + symbol: 'PSP', + address: '0xd3594e879b358f430e20f82bea61e83562d49d48', + }, + ], + Avalanche: [ + { + symbol: 'RADIO', + address: '0x02bfd11499847003de5f0f5aa081c43854d48815', + }, + { + symbol: 'EUROC', + address: '0xc891eb4cbdeff6e073e859e987815ed1505c2acd', + }, + { + symbol: 'MELD', + address: '0x333000333b26ee30214b4af6419d9ab07a450400', + }, + ], + BNB: [ + { + symbol: 'TUSD', + address: '0x40af3827f39d0eacbf4a168f8d4ee67c121d11c9', + }, + { + symbol: 'ARA', + address: '0x5542958fa9bd89c96cb86d1a6cb7a3e644a3d46e', + }, + { + symbol: 'FLASH', + address: '0xc3111096b3b46873393055dea14036ea603cfa95', + }, + ], +} diff --git a/cypress/e2e/selectors/selectors.cy.ts b/cypress/e2e/selectors/selectors.cy.ts index 369b57a13b..3ac8d89c5f 100644 --- a/cypress/e2e/selectors/selectors.cy.ts +++ b/cypress/e2e/selectors/selectors.cy.ts @@ -13,7 +13,7 @@ export const TokenCatalogLocators = { btnUnderstand: '[data-testid=button-confirm-import-token]', btnClearAll: '[data-testid=button-clear-all-import-token]', btnAllTab: '[data-testid=tab-all]', - btnImportTab: '[data-testid=tab-import]' + btnImportTab: '[data-testid=tab-import]', } export const SwapPageLocators = { @@ -30,7 +30,7 @@ export const LimitOrderLocators = { txtSellingRate: '[data-testid=input-selling-rate]', lblBalanceIn: '[data-testid=limit-order-input-tokena] [data-testid=balance]', lblErrorMessage: '[data-testid=error-message]', - btnGetStarted: '[data-testid=get-started-button]' + btnGetStarted: '[data-testid=get-started-button]', } export const CrossChainLocators = { @@ -38,7 +38,7 @@ export const CrossChainLocators = { btnNetworkIn: '[data-testid=swap-currency-input] [data-testid=network-button]', btnNetworkOut: '[data-testid=swap-currency-output] [data-testid=network-button]', btnUnderstand: '[data-testid=understand-button]', - rechartsSurface: '.recharts-surface' //it's in the library so don't use data-testid + rechartsSurface: '.recharts-surface', //it's in the library so don't use data-testid } export const WalletLocators = { @@ -63,4 +63,12 @@ export const HeaderLocators = { export const FarmLocators = { lblApr: '[data-testid=apr-value]', lblTvl: '[data-testid=tvl-value]', -} \ No newline at end of file +} + +export const PoolLocators = { + txtSearchPool: 'input[data-testid="search-pool"]', + txtPriceValue: 'input[data-testid="price-value"]', + txtAmountIn: '[data-testid="token-amount-input"]', + lblCurrentPrice: '[data-testid="current-price"]', + btnZapIn: '[data-testid="zap-in-btn"]', +} diff --git a/cypress/e2e/specs/connect-wallet.e2e.cy.ts b/cypress/e2e/specs/connect-wallet.e2e.cy.ts index bd512ef615..aa43af977c 100644 --- a/cypress/e2e/specs/connect-wallet.e2e.cy.ts +++ b/cypress/e2e/specs/connect-wallet.e2e.cy.ts @@ -1,29 +1,27 @@ - import { Network, SwapPage } from '../pages/swap-page.po.cy' import { DEFAULT_NETWORK, DEFAULT_URL, NETWORK, TAG } from '../selectors/constants.cy' - const wallet = new Network() describe('Metamask Extension tests', { tags: TAG.regression }, () => { - beforeEach(() => { - SwapPage.open(DEFAULT_URL) - SwapPage.connectWallet() - }) + before(() => { + SwapPage.open(DEFAULT_URL) + SwapPage.connectWallet() + cy.acceptMetamaskAccess() + SwapPage.getStatusConnectedWallet() + }) - it('Redirects to swap page when a user has already connected a wallet', { tags: TAG.smoke }, () => { - cy.acceptMetamaskAccess() - SwapPage.getStatusConnectedWallet() - cy.url().should('include', '/swap') - }) + it('Redirects to swap page when a user has already connected a wallet', { tags: TAG.smoke }, () => { + cy.url().should('include', '/swap') + }) - it('Should approve permission to switch network', () => { - if (NETWORK !== DEFAULT_NETWORK) { - SwapPage.getStatusConnectedWallet() - wallet.selectNetwork(NETWORK) - cy.allowMetamaskToAddAndSwitchNetwork().then(approved => { - expect(approved).to.be.true - }) - } - }) + it('Should approve permission to switch network', () => { + if (NETWORK !== DEFAULT_NETWORK) { + SwapPage.getStatusConnectedWallet() + wallet.selectNetwork(NETWORK) + cy.allowMetamaskToAddAndSwitchNetwork().then(approved => { + expect(approved).to.be.true + }) + } + }) }) diff --git a/cypress/e2e/specs/constants.ts b/cypress/e2e/specs/constants.ts new file mode 100644 index 0000000000..0ab7fc596f --- /dev/null +++ b/cypress/e2e/specs/constants.ts @@ -0,0 +1,3 @@ +const noResultsText = 'No results found.' +const noTokensText = 'Select a token' +const unListedToken = ['KNNC', 'KCCN'] diff --git a/cypress/e2e/specs/swap-page.e2e.cy.ts b/cypress/e2e/specs/swap-page.e2e.cy.ts index 2939d15ebb..bc93ebf5c0 100644 --- a/cypress/e2e/specs/swap-page.e2e.cy.ts +++ b/cypress/e2e/specs/swap-page.e2e.cy.ts @@ -1,5 +1,14 @@ -import { SwapPage, TokenCatalog } from "../pages/swap-page.po.cy" -import { DEFAULT_URL, NETWORK, NORESULTS_TEXT, NOTOKENS_TEXT, TAG, TOKEN_SYMBOLS, UNWHITELIST_SYMBOL_TOKENS, UNWHITELIST_TOKENS } from "../selectors/constants.cy" +import { SwapPage, TokenCatalog } from '../pages/swap-page.po.cy' +import { + DEFAULT_URL, + NETWORK, + NORESULTS_TEXT, + NOTOKENS_TEXT, + TAG, + TOKEN_SYMBOLS, + UNWHITELIST_SYMBOL_TOKENS, + UNWHITELIST_TOKENS, +} from '../selectors/constants.cy' const unWhitelistTokens = UNWHITELIST_TOKENS[NETWORK] const tokenSymbols = TOKEN_SYMBOLS[NETWORK] @@ -7,163 +16,164 @@ const tokenSymbols = TOKEN_SYMBOLS[NETWORK] const arrAddress = [unWhitelistTokens[0].address, unWhitelistTokens[1].address, unWhitelistTokens[2].address] const arrSymbol = [unWhitelistTokens[0].symbol, unWhitelistTokens[1].symbol, unWhitelistTokens[2].symbol] +const tokenCatalog = new TokenCatalog() -const tokenCatalog = new TokenCatalog(); +describe(`Token Catalog on ${NETWORK}`, { tags: TAG.regression }, () => { + before(() => { + SwapPage.open(DEFAULT_URL) + SwapPage.connectWallet() + SwapPage.getStatusConnectedWallet() + }) + + describe('Select token in favorite tokens list', () => { + it('Should be selected tokenIn in favorite tokens list successfully', () => { + SwapPage.selectTokenIn().getFavoriteTokens(arr => { + tokenCatalog.selectFavoriteToken(arr[1]) + SwapPage.getCurrentTokenIn(text => { + expect(text).to.equal(arr[1]) + }) + }) + }) + + it('Should be selected tokenOut in favorite tokens list successfully', () => { + SwapPage.selectTokenOut().getFavoriteTokens(arr => { + tokenCatalog.selectFavoriteToken(arr[2]) + SwapPage.getCurrentTokenOut(text => { + expect(text).to.equal(arr[2]) + }) + }) + }) + }) + + describe('Remove/add token with favorite tokens list', () => { + it('Should be removed tokenIn from favorite tokens list', () => { + SwapPage.selectTokenIn().getFavoriteTokens(arr => { + tokenCatalog.removeFavoriteToken(arr[1]) + cy.wait(2000) + tokenCatalog.getFavoriteTokens(list => { + expect(list).not.to.include.members([arr[1]]) + }) + }) + }) + it('Should be added tokenIn to favorite tokens list', () => { + SwapPage.selectTokenIn().addFavoriteToken([tokenSymbols[0]]) + tokenCatalog.getFavoriteTokens(list => { + expect(list).to.include.members([tokenSymbols[0]]) + }) + }) + + it('Should be removed tokenOut from favorite tokens list', () => { + SwapPage.selectTokenOut().getFavoriteTokens(arr => { + tokenCatalog.removeFavoriteToken(arr[2]) + cy.wait(2000) + tokenCatalog.getFavoriteTokens(list => { + expect(list).not.to.include.members([arr[2]]) + }) + }) + }) -describe(`Token Catalog on ${NETWORK}`, { tags: TAG.regression }, () => { - beforeEach(() => { - SwapPage.open(DEFAULT_URL) - SwapPage.connectWallet() - SwapPage.getStatusConnectedWallet() - }) - - describe('Select token in favorite tokens list', () => { - it('Should be selected tokenIn in favorite tokens list successfully', () => { - SwapPage.selectTokenIn().getFavoriteTokens((arr) => { - tokenCatalog.selectFavoriteToken(arr[1]) - SwapPage.getCurrentTokenIn((text) => { - expect(text).to.equal(arr[1]) - }) - }) - }) - - it('Should be selected tokenOut in favorite tokens list successfully', () => { - SwapPage.selectTokenOut().getFavoriteTokens((arr) => { - tokenCatalog.selectFavoriteToken(arr[2]) - SwapPage.getCurrentTokenOut((text) => { - expect(text).to.equal(arr[2]) - }) - }) - }) - }) - - describe('Remove/add token with favorite tokens list', () => { - it('Should be removed tokenIn from favorite tokens list', () => { - SwapPage.selectTokenIn().getFavoriteTokens((arr) => { - tokenCatalog.removeFavoriteToken(arr[1]) - cy.wait(2000) - tokenCatalog.getFavoriteTokens((list) => { - expect(list).not.to.include.members([arr[1]]) - }) - }) - }) - - it('Should be added tokenIn to favorite tokens list', () => { - SwapPage.selectTokenIn().addFavoriteToken([tokenSymbols[0]]) - tokenCatalog.getFavoriteTokens((list) => { - expect(list).to.include.members([tokenSymbols[0]]) - }) - }) - - it('Should be removed tokenOut from favorite tokens list', () => { - SwapPage.selectTokenOut().getFavoriteTokens((arr) => { - tokenCatalog.removeFavoriteToken(arr[2]) - cy.wait(2000) - tokenCatalog.getFavoriteTokens((list) => { - expect(list).not.to.include.members([arr[2]]) - }) - }) - }) - - it('Should be added tokenOut to favorite tokens list', () => { - SwapPage.selectTokenOut().addFavoriteToken([tokenSymbols[0]]) - tokenCatalog.getFavoriteTokens((list) => { - expect(list).to.include.members([tokenSymbols[0]]) - }) - }) - }) - - describe('Select token by symbol', () => { - it('Should be selected tokenIn by symbol successfully', () => { - SwapPage.selectTokenIn().selectTokenBySymbol(tokenSymbols[0]) - SwapPage.getCurrentTokenIn((text) => { - expect(text).to.equal(tokenSymbols[0]) - }) - }) - - it('Should be selected tokenOut by symbol successfully', () => { - SwapPage.selectTokenOut().selectTokenBySymbol(tokenSymbols[1]) - SwapPage.getCurrentTokenOut((text) => { - expect(text).to.equal(tokenSymbols[1]) - }) - }) - - it('Should be unselected tokenIn not exist in whitelist', () => { - SwapPage.selectTokenIn().searchToken(UNWHITELIST_SYMBOL_TOKENS[0]) - tokenCatalog.getNoResultsFound((text) => { - expect(text).to.equal(NORESULTS_TEXT) - }) - }) - - it('Should be unselected tokenOut not exist in whitelist', () => { - SwapPage.selectTokenOut().searchToken(UNWHITELIST_SYMBOL_TOKENS[0]) - tokenCatalog.getNoResultsFound((text) => { - expect(text).to.equal(NORESULTS_TEXT) - }) - }) - }) - - describe('Import and delete token', () => { - it('Should be imported then deleted tokenIn successfully', () => { - tokenCatalog.importNewTokens(arrAddress) - SwapPage.selectTokenIn().selectImportTab() - tokenCatalog.getWhitelistTokens((list) => { - expect(list).to.include.members(arrSymbol) - }) - - tokenCatalog.deleteImportedToken(arrSymbol[2]) - tokenCatalog.getWhitelistTokens((list) => { - expect(list).not.to.include.members([arrSymbol[2]]) - }) - - tokenCatalog.clearAllImportedTokens() - tokenCatalog.getNoResultsFound((text) => { - expect(text).to.equal(NORESULTS_TEXT) - }) - }) - - it('Should be imported then deleted tokenOut successfully', () => { - tokenCatalog.importNewTokens(arrAddress) - SwapPage.selectTokenOut().selectImportTab() - tokenCatalog.getWhitelistTokens((list) => { - expect(list).to.include.members(arrSymbol) - }) - - tokenCatalog.deleteImportedToken(arrSymbol[1]) - tokenCatalog.getWhitelistTokens((list) => { - expect(list).not.to.include.members([arrSymbol[1]]) - }) - - tokenCatalog.clearAllImportedTokens() - tokenCatalog.getNoResultsFound((text) => { - expect(text).to.equal(NORESULTS_TEXT) - }) - }) - }) - - describe(`E2E Token Catalog`, () => { - it('Should be selected tokenIn and tokenOut to swap', { tags: TAG.smoke }, () => { - tokenCatalog.importNewTokens([arrAddress[2]]) - SwapPage.getCurrentTokenIn((text) => { - expect(text).to.equal(arrSymbol[2]) - }) - - SwapPage.selectTokenOut().getFavoriteTokens((arr) => { - tokenCatalog.selectFavoriteToken(arr[1]) - SwapPage.getCurrentTokenOut((text) => { - expect(text).to.equal(arr[1]) - }) - }) - - SwapPage.selectTokenOut() - tokenCatalog.deleteImportedToken(arrSymbol[2]) - tokenCatalog.getNoResultsFound((text) => { - expect(text).to.equal(NORESULTS_TEXT) - }) - SwapPage.getCurrentTokenIn((text) => { - expect(text).to.equal(NOTOKENS_TEXT) - }) - }) - }) -}) \ No newline at end of file + it('Should be added tokenOut to favorite tokens list', () => { + SwapPage.selectTokenOut().addFavoriteToken([tokenSymbols[0]]) + tokenCatalog.getFavoriteTokens(list => { + expect(list).to.include.members([tokenSymbols[0]]) + }) + }) + }) + + describe('Select token by symbol', () => { + it('Should be selected tokenIn by symbol successfully', () => { + SwapPage.selectTokenIn().selectTokenBySymbol(tokenSymbols[0]) + SwapPage.getCurrentTokenIn(text => { + expect(text).to.equal(tokenSymbols[0]) + }) + }) + + it('Should be selected tokenOut by symbol successfully', () => { + SwapPage.selectTokenOut().selectTokenBySymbol(tokenSymbols[1]) + SwapPage.getCurrentTokenOut(text => { + expect(text).to.equal(tokenSymbols[1]) + }) + }) + + it('Should be unselected tokenIn not exist in whitelist', () => { + SwapPage.selectTokenIn().searchToken(UNWHITELIST_SYMBOL_TOKENS[0]) + tokenCatalog.getNoResultsFound(text => { + expect(text).to.equal(NORESULTS_TEXT) + }) + }) + + it('Should be unselected tokenOut not exist in whitelist', () => { + SwapPage.selectTokenOut().searchToken(UNWHITELIST_SYMBOL_TOKENS[0]) + tokenCatalog.getNoResultsFound(text => { + expect(text).to.equal(NORESULTS_TEXT) + }) + }) + }) + + describe('Import and delete token', () => { + it('Should be imported then deleted tokenIn successfully', () => { + tokenCatalog.importNewTokens(arrAddress) + SwapPage.selectTokenIn().selectImportTab() + tokenCatalog.getWhitelistTokens(list => { + expect(list).to.include.members(arrSymbol) + }) + + tokenCatalog.deleteImportedToken(arrSymbol[2]) + tokenCatalog.getWhitelistTokens(list => { + expect(list).not.to.include.members([arrSymbol[2]]) + }) + + tokenCatalog.clearAllImportedTokens() + tokenCatalog.getNoResultsFound(text => { + expect(text).to.equal(NORESULTS_TEXT) + }) + }) + + it('Should be imported then deleted tokenOut successfully', () => { + tokenCatalog.importNewTokens(arrAddress) + SwapPage.selectTokenOut().selectImportTab() + tokenCatalog.getWhitelistTokens(list => { + expect(list).to.include.members(arrSymbol) + }) + + tokenCatalog.deleteImportedToken(arrSymbol[1]) + tokenCatalog.getWhitelistTokens(list => { + expect(list).not.to.include.members([arrSymbol[1]]) + }) + + tokenCatalog.clearAllImportedTokens() + tokenCatalog.getNoResultsFound(text => { + expect(text).to.equal(NORESULTS_TEXT) + }) + }) + }) + + describe(`E2E Token Catalog`, () => { + it('Should be selected tokenIn and tokenOut to swap', { tags: TAG.smoke }, () => { + tokenCatalog.importNewTokens([arrAddress[2]]) + SwapPage.getCurrentTokenIn(text => { + expect(text).to.equal(arrSymbol[2]) + }) + + SwapPage.selectTokenOut().getFavoriteTokens(arr => { + tokenCatalog.selectFavoriteToken(arr[1]) + SwapPage.getCurrentTokenOut(text => { + expect(text).to.equal(arr[1]) + }) + }) + + SwapPage.selectTokenOut() + tokenCatalog.deleteImportedToken(arrSymbol[2]) + tokenCatalog.getNoResultsFound(text => { + expect(text).to.equal(NORESULTS_TEXT) + }) + SwapPage.getCurrentTokenIn(text => { + expect(text).to.equal(NOTOKENS_TEXT) + }) + }) + }) + afterEach(() => { + cy.reload() + }) +}) diff --git a/cypress/e2e/specs/zap-in-simulator-test.mjs b/cypress/e2e/specs/zap-in-simulator-test.mjs new file mode 100644 index 0000000000..a951e60ccb --- /dev/null +++ b/cypress/e2e/specs/zap-in-simulator-test.mjs @@ -0,0 +1,27 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import output from '../../../target/cypress-logs/zap.e2e.cy.json' assert {type: 'json'} +import { simulateTenderly,getTokenInFromZapEncodeData } from '../../support/tenderly.js'; +import 'dotenv/config' +const suite = output['cypress/e2e/specs/zap.e2e.cy.ts']; +const testcases = Object.keys(suite) + +testcases.forEach((testcase)=>{ + test(testcase, async () => { + const zapInData = suite[testcase] + .filter(log => { + return log.type == 'cons:debug' && log.message.includes('zap data') && log.severity.includes('success') + }) + .slice(-1) + .pop()['message'].replace('zap data,\n', '') + + const data = JSON.parse(zapInData) + data.chainId = process.env.CHAIN_ID + data.tokenIn = getTokenInFromZapEncodeData(data) + + const result = await simulateTenderly(data) + console.log(result) + assert.equal(result.success, true) + }) +}) \ No newline at end of file diff --git a/cypress/e2e/specs/zap.e2e.cy.ts b/cypress/e2e/specs/zap.e2e.cy.ts new file mode 100644 index 0000000000..d713ce4f81 --- /dev/null +++ b/cypress/e2e/specs/zap.e2e.cy.ts @@ -0,0 +1,40 @@ +import arbitrumTestCases from '../../fixtures/zap/arbitrum.json' +import avalancheTestCases from '../../fixtures/zap/avalanche.json' +import bscTestCases from '../../fixtures/zap/bsc.json' +import ethereumTestCases from '../../fixtures/zap/ethereum.json' +import optimismTestCases from '../../fixtures/zap/optimism.json' +import { CustomRange, FarmingRange, PoolsPage } from '../pages/pools-page.po.cy' +import { Network, SwapPage } from '../pages/swap-page.po.cy' +import { DEFAULT_NETWORK, DEFAULT_URL, NETWORK, TAG } from '../selectors/constants.cy' + +const wallet = new Network() + +const DataSet = { + Arbitrum: arbitrumTestCases, + Ethereum: ethereumTestCases, + Avalanche: avalancheTestCases, + 'BNB Chain': bscTestCases, + Optimism: optimismTestCases, +} +describe('Zap In', { tags: TAG.zap }, () => { + const zapTestData = DataSet[NETWORK] + before(() => { + SwapPage.open(DEFAULT_URL) + SwapPage.connectWallet() + if (NETWORK !== DEFAULT_NETWORK) { + cy.acceptMetamaskAccess() + SwapPage.getStatusConnectedWallet() + wallet.selectNetwork(NETWORK) + cy.allowMetamaskToAddAndSwitchNetwork().then(approved => { + expect(approved).to.be.true + }) + SwapPage.goToPoolPage() + } + }) + + zapTestData.forEach(testData => { + it(`${NETWORK}: ${testData.pair} ${testData.feeTier}`, function () { + PoolsPage.addLiquidity(testData.id, testData.amountIn, testData.customRange as CustomRange, testData.farmingRange) + }) + }) +}) diff --git a/cypress/fixtures/zap/arbitrum.json b/cypress/fixtures/zap/arbitrum.json new file mode 100644 index 0000000000..1ca8325fe1 --- /dev/null +++ b/cypress/fixtures/zap/arbitrum.json @@ -0,0 +1,26 @@ +[ + { + "id": "0x83fe9065ed68506a0d2ece59cd71c43bbff6e450", + "amountIn": "100", + "pair": "wstETH-axl.wstET", + "feeTier": "0.008%", + "farmingRange": { + "minPrice": "0.999", + "maxPrice": "1.2" + } + }, + { + "id": "0xdf03ca6c633f784ac5e062dd708b15728b488621", + "amountIn": "100", + "pair": "ETH-ARB", + "feeTier": "0.25%", + "customRange": "Full Range" + }, + { + "id": "0xc23f1d198477c0bcae0cac2ec734ceda438a8990", + "amountIn": "100", + "pair": "USDC-USDC.e", + "feeTier": "0.008%", + "customRange": "Safe" + } +] diff --git a/cypress/fixtures/zap/avalanche.json b/cypress/fixtures/zap/avalanche.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/cypress/fixtures/zap/avalanche.json @@ -0,0 +1 @@ +[] diff --git a/cypress/fixtures/zap/bsc.json b/cypress/fixtures/zap/bsc.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/cypress/fixtures/zap/bsc.json @@ -0,0 +1 @@ +[] diff --git a/cypress/fixtures/zap/ethereum.json b/cypress/fixtures/zap/ethereum.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/cypress/fixtures/zap/ethereum.json @@ -0,0 +1 @@ +[] diff --git a/cypress/fixtures/zap/optimism.json b/cypress/fixtures/zap/optimism.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/cypress/fixtures/zap/optimism.json @@ -0,0 +1 @@ +[] diff --git a/cypress/support/KSZAPRouterABI.json b/cypress/support/KSZAPRouterABI.json new file mode 100644 index 0000000000..be4e790ca7 --- /dev/null +++ b/cypress/support/KSZAPRouterABI.json @@ -0,0 +1,268 @@ +[ + { "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, + { + "anonymous": false, + "inputs": [{ "indexed": false, "internalType": "bytes", "name": "_clientData", "type": "bytes" }], + "name": "ClientData", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": false, "internalType": "string", "name": "reason", "type": "string" }], + "name": "Error", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "_executor", "type": "address" }, + { "indexed": true, "internalType": "bool", "name": "_grantOrRevoke", "type": "bool" } + ], + "name": "ExecutorWhitelisted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "previousOwner", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "newOwner", "type": "address" } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": false, "internalType": "address", "name": "account", "type": "address" }], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "contract IERC20", "name": "_token", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "_amount", "type": "uint256" }, + { "indexed": false, "internalType": "bool", "name": "_isNative", "type": "bool" }, + { "indexed": false, "internalType": "bool", "name": "_isPermit", "type": "bool" } + ], + "name": "TokenCollected", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": false, "internalType": "address", "name": "account", "type": "address" }], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "address", "name": "user", "type": "address" }, + { "indexed": false, "internalType": "bool", "name": "grantOrRevoke", "type": "bool" } + ], + "name": "UpdateGuardian", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "address", "name": "user", "type": "address" }, + { "indexed": false, "internalType": "bool", "name": "grantOrRevoke", "type": "bool" } + ], + "name": "UpdateOperator", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "_validator", "type": "address" }, + { "indexed": true, "internalType": "bool", "name": "_grantOrRevoke", "type": "bool" } + ], + "name": "ValidatorWhitelisted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "uint8", "name": "_dexType", "type": "uint8" }, + { "indexed": true, "internalType": "contract IERC20", "name": "_srcToken", "type": "address" }, + { "indexed": true, "internalType": "uint256", "name": "_srcAmount", "type": "uint256" }, + { "indexed": false, "internalType": "address", "name": "_validator", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "_executor", "type": "address" }, + { "indexed": false, "internalType": "bytes", "name": "_zapInfo", "type": "bytes" }, + { "indexed": false, "internalType": "bytes", "name": "_extraData", "type": "bytes" }, + { "indexed": false, "internalType": "bytes", "name": "_initialData", "type": "bytes" }, + { "indexed": false, "internalType": "bytes", "name": "_zapResults", "type": "bytes" } + ], + "name": "ZapExecuted", + "type": "event" + }, + { "inputs": [], "name": "disableLogic", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, + { "inputs": [], "name": "enableLogic", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "guardians", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "operators", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { "inputs": [], "name": "renounceOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "address", "name": "recipient", "type": "address" } + ], + "name": "rescueFunds", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "newOwner", "type": "address" }], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" }, + { "internalType": "bool", "name": "grantOrRevoke", "type": "bool" } + ], + "name": "updateGuardian", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" }, + { "internalType": "bool", "name": "grantOrRevoke", "type": "bool" } + ], + "name": "updateOperator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address[]", "name": "_executors", "type": "address[]" }, + { "internalType": "bool", "name": "_grantOrRevoke", "type": "bool" } + ], + "name": "whitelistExecutors", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address[]", "name": "_validators", "type": "address[]" }, + { "internalType": "bool", "name": "_grantOrRevoke", "type": "bool" } + ], + "name": "whitelistValidators", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "whitelistedExecutor", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "whitelistedValidator", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "uint8", "name": "dexType", "type": "uint8" }, + { "internalType": "contract IERC20", "name": "srcToken", "type": "address" }, + { "internalType": "uint256", "name": "srcAmount", "type": "uint256" }, + { "internalType": "bytes", "name": "zapInfo", "type": "bytes" }, + { "internalType": "bytes", "name": "extraData", "type": "bytes" }, + { "internalType": "bytes", "name": "permitData", "type": "bytes" } + ], + "internalType": "struct IKSZapRouter.ZapDescription", + "name": "_desc", + "type": "tuple" + }, + { + "components": [ + { "internalType": "address", "name": "validator", "type": "address" }, + { "internalType": "address", "name": "executor", "type": "address" }, + { "internalType": "uint32", "name": "deadline", "type": "uint32" }, + { "internalType": "bytes", "name": "executorData", "type": "bytes" }, + { "internalType": "bytes", "name": "clientData", "type": "bytes" } + ], + "internalType": "struct IKSZapRouter.ZapExecutionData", + "name": "_exe", + "type": "tuple" + } + ], + "name": "zapIn", + "outputs": [{ "internalType": "bytes", "name": "zapResults", "type": "bytes" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "uint8", "name": "dexType", "type": "uint8" }, + { "internalType": "contract IERC20", "name": "srcToken", "type": "address" }, + { "internalType": "uint256", "name": "srcAmount", "type": "uint256" }, + { "internalType": "bytes", "name": "zapInfo", "type": "bytes" }, + { "internalType": "bytes", "name": "extraData", "type": "bytes" }, + { "internalType": "bytes", "name": "permitData", "type": "bytes" } + ], + "internalType": "struct IKSZapRouter.ZapDescription", + "name": "_desc", + "type": "tuple" + }, + { + "components": [ + { "internalType": "address", "name": "validator", "type": "address" }, + { "internalType": "address", "name": "executor", "type": "address" }, + { "internalType": "uint32", "name": "deadline", "type": "uint32" }, + { "internalType": "bytes", "name": "executorData", "type": "bytes" }, + { "internalType": "bytes", "name": "clientData", "type": "bytes" } + ], + "internalType": "struct IKSZapRouter.ZapExecutionData", + "name": "_exe", + "type": "tuple" + } + ], + "name": "zapInWithNative", + "outputs": [{ "internalType": "bytes", "name": "zapResults", "type": "bytes" }], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/cypress/support/constants.js b/cypress/support/constants.js new file mode 100644 index 0000000000..74357698ef --- /dev/null +++ b/cypress/support/constants.js @@ -0,0 +1,36 @@ +const BIG_AMOUNT = '11579208923731619542357098500868790785326998466564056' +const NATIVE_TOKEN_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' +const FROM_WALLET_ADDRESS = '0x0193a8a52d77e27bdd4f12e0cdd52d8ff1d97d68' +const MAX_UINT = '10000000000000000000000000000000000000000000000000000000000000000000' +const ZAP_ROUTER_ADDESS = '0x30c5322e4e08ad500c348007f92f120ab4e2b79e' + +const Network = { + MAINNET: 1, + BSC: 56, + POLYGON: 137, + ZKEVM: 1101, + AVALANCHE: 43114, + FANTOM: 250, + ARBITRUM: 42161, + OPTIMISM: 10, + BASE: 8453, +} +const Holders = { + [Network.MAINNET]: {}, + [Network.POLYGON]: {}, + [Network.ARBITRUM]: { + '0xaf88d065e77c8cC2239327C5EDb3A432268e5831': '0xa843392198862f98d17e3aa1421b08f2c2020cff', //USDC + '0x5979D7b546E38E414F7E9822514be443A4800529': '0x513c7e3a9c69ca3e22550ef58ac1c0088e918fff', // wstETH + '0x9cfB13E6c11054ac9fcB92BA89644F30775436e4': '0x9cfb13e6c11054ac9fcb92ba89644f30775436e4', // axl.wstETH + }, +} + +module.exports = { + NATIVE_TOKEN_ADDRESS, + FROM_WALLET_ADDRESS, + BIG_AMOUNT, + MAX_UINT, + ZAP_ROUTER_ADDESS, + Network, + Holders, +} diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 070521e240..ae7df02f44 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -15,9 +15,19 @@ import '@cypress/grep' import registerCypressGrep from '@cypress/grep/src/support' import '@synthetixio/synpress/support/index' +import installLogsCollector from 'cypress-terminal-report/src/installLogsCollector' import './commands' import './connectWalletCommands' import './selectTokenCommands' +const configOption = { + collectTypes: ['cons:debug'], + filterLog: function (args: [installLogsCollector.LogType, string, installLogsCollector.Severity]) { + const [logType, message] = args + return logType === 'cons:debug' && message.includes('zap data') + }, +} + +installLogsCollector(configOption) registerCypressGrep() diff --git a/cypress/support/erc20.json b/cypress/support/erc20.json new file mode 100644 index 0000000000..78e48df019 --- /dev/null +++ b/cypress/support/erc20.json @@ -0,0 +1,308 @@ +[ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "guy", + "type": "address" + }, + { + "name": "wad", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "src", + "type": "address" + }, + { + "name": "dst", + "type": "address" + }, + { + "name": "wad", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "wad", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "dst", + "type": "address" + }, + { + "name": "wad", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "deposit", + "outputs": [], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address" + }, + { + "name": "", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "payable": true, + "stateMutability": "payable", + "type": "fallback" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "src", + "type": "address" + }, + { + "indexed": true, + "name": "guy", + "type": "address" + }, + { + "indexed": false, + "name": "wad", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "src", + "type": "address" + }, + { + "indexed": true, + "name": "dst", + "type": "address" + }, + { + "indexed": false, + "name": "wad", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "dst", + "type": "address" + }, + { + "indexed": false, + "name": "wad", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "src", + "type": "address" + }, + { + "indexed": false, + "name": "wad", + "type": "uint256" + } + ], + "name": "Withdrawal", + "type": "event" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "redeem", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "mintAmount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "payable": true, + "stateMutability": "payable", + "type": "function" + } +] diff --git a/cypress/support/tenderly.js b/cypress/support/tenderly.js new file mode 100644 index 0000000000..1e385f0649 --- /dev/null +++ b/cypress/support/tenderly.js @@ -0,0 +1,295 @@ +const axios = require('axios') +const { Interface } = require('@ethersproject/abi') +const { StaticJsonRpcProvider } = require('@ethersproject/providers') +require('dotenv').config() + +const { TENDERLY_USER, TENDERLY_PROJECT, TENDERLY_ACCESS_KEY } = process.env +const { + FROM_WALLET_ADDRESS, + NATIVE_TOKEN_ADDRESS, + ZAP_ROUTER_ADDESS, + BIG_AMOUNT, + MAX_UINT, + Holders, +} = require('./constants.js') + +const projectBase = `account/${TENDERLY_USER}/project/${TENDERLY_PROJECT}` + +const KSZapABI = require('./KSZAPRouterABI.json') +const Erc20ABI = require('./erc20.json') +const { Console } = require('console') +const erc20Interface = new Interface(Erc20ABI) +const ksZapRouterInterface = new Interface(KSZapABI) + +const anAxiosOnTenderly = () => + axios.create({ + baseURL: 'https://api.tenderly.co/api/v1', + headers: { + 'X-Access-Key': TENDERLY_ACCESS_KEY || '', + 'Content-Type': 'application/json', + }, + }) +class TenderlySimulation { + maxGasLimit = 80000000 + constructor() {} + async setup(chainId) { + this.chainId = chainId + const tAxios = anAxiosOnTenderly() + const resp = await tAxios.post(`${projectBase}/fork`, { + network_id: chainId, + }) + + const forkId = resp.data.simulation_fork.id + const lastTx = resp.data.root_transaction.id + this.forkId = forkId + this.lastTx = lastTx + this.tAxios = anAxiosOnTenderly() + } + + async applyOverride(contractAddress) { + const balanceFns = ['balanceOf', 'balances', '_balances', 'shares'] + const allowanceFns = ['allowance', 'allowances', '_allowances', 'allowed'] + const overrideStorageObj = await this.tAxios + .get(`https://api.tenderly.co/api/v1/public-contracts/${this.chainId}/${contractAddress}`) + .then(response => { + if ('states' in response.data.data) { + let fns = response.data.data.states.map(state => state.name) + const allowanceFnName = getMatch(allowanceFns, fns) + const balanceFnName = getMatch(balanceFns, fns) + + if (allowanceFnName.length == 0) { + throw new Error('Cannot find allowance method name for token ' + contractAddress) + } + if (balanceFnName.length == 0) { + throw new Error('Cannot find balance method name for token ' + contractAddress) + } + + const addBalance = constructAddBalanceFn(balanceFnName[0]) + const addAllowance = constructAddBAllowanceFn(allowanceFnName[0]) + + const balanceStorage = addBalance(FROM_WALLET_ADDRESS, BIG_AMOUNT) + const allowanceStorage = addAllowance(FROM_WALLET_ADDRESS, ZAP_ROUTER_ADDESS, BIG_AMOUNT) + const value = { ...balanceStorage, ...allowanceStorage } + const overrides = { + networkID: `${this.chainId}`, + stateOverrides: { + [`${contractAddress}`]: { + value: value, + }, + }, + } + return overrides + } else { + return {} + } + }) + return overrideStorageObj + } + async encodeState(tokenIn) { + try { + if (tokenIn.toLowerCase() == NATIVE_TOKEN_ADDRESS.toLowerCase()) { + return { + [`${FROM_WALLET_ADDRESS}`]: { + balance: `${BIG_AMOUNT}`, + }, + } + } else { + const stateOverridesPayload = await this.applyOverride(tokenIn) + if (isObjectEmpty(stateOverridesPayload)) { + return {} + } else { + const encodeState = await this.tAxios + .post(`${projectBase}/contracts/encode-states`, stateOverridesPayload) + .catch(function (error) { + console.log('Tenderly_generate_override_storage', error) + }) + + return Object.keys(encodeState.data.stateOverrides).reduce((acc, contract) => { + const _storage = encodeState.data.stateOverrides[contract].value + acc[contract] = { + storage: _storage, + } + return acc + }, {}) + } + } + } catch (e) { + console.log(`TenderlySimulation_encode-states:`, e.response.data.error.message) + return {} + } + } + + async simulate(params, stateOverrides = {}) { + let _params = { + network_id: this.chainId, + from: params.from, + to: params.to, + save: true, + root: this.lastTx, + value: params.value || '0', + gas: this.maxGasLimit, + input: params.data, + state_objects: {}, + } + try { + if (stateOverrides) { + _params.state_objects = stateOverrides + } + const { data } = await this.tAxios.post(`${projectBase}/fork/${this.forkId}/simulate`, _params) + const lastTx = data.simulation.id + if (data.transaction.status) { + this.lastTx = lastTx + return { + success: true, + gasUsed: data.transaction.gas_used, + tenderlyUrl: `https://dashboard.tenderly.co/${TENDERLY_USER}/${TENDERLY_PROJECT}/fork/${this.forkId}/simulation/${lastTx}`, + // transaction: data.transaction, + } + } else { + return { + success: false, + tenderlyUrl: `https://dashboard.tenderly.co/${TENDERLY_USER}/${TENDERLY_PROJECT}/fork/${this.forkId}/simulation/${lastTx}`, + error: `Simulation failed: ${data.transaction.error_info.error_message} at ${data.transaction.error_info.address}`, + } + } + } catch (e) { + console.error(`TenderlySimulation_simulate:`, e) + return { + success: false, + tenderlyUrl: '', + } + } + } +} + +const getForkRpcUrl = async chainId => { + const RPCs = { + 1101: process.env.POLYGON_ZKEVM_NODE_URL, + 59144: process.env.LINEA_NODE_URL, + 8453: process.env.BASE_NODE_URL, + 137: process.env.POLYGON_NODE_URL, + 534352: process.env.SCROLL_NODE_URL, + 42161: process.env.ARBITRUM_NODE_URL, + } + const rpcUrl = RPCs[chainId] ?? (await createFork(chainId)) + return rpcUrl +} + +const isObjectEmpty = objectName => { + return objectName && Object.keys(objectName).length === 0 && objectName.constructor === Object +} + +function allowTokenTransferProxyParams(tokenAddress, holderAddress, routerAddress) { + return { + from: holderAddress, + to: tokenAddress, + data: erc20Interface.encodeFunctionData('approve', [routerAddress, MAX_UINT]), + value: '0', + } +} + +async function simulateTenderly(input) { + const { data, chainId, tokenIn } = input + // Holders[chainId] + // const holder = '0xa843392198862f98d17e3aa1421b08f2c2020cff' + + const ts = new TenderlySimulation() + await ts.setup(chainId) + const stateOverrideObj = await ts.encodeState(tokenIn) + let holder = FROM_WALLET_ADDRESS + + /** + * Some token cannot find allowance and balance function to override state, + * so this step will finding the holder via scanner + * then fake allowance for kyber router + */ + if (isObjectEmpty(stateOverrideObj)) { + holder = Holders[chainId][tokenIn] + if (tokenIn.toLowerCase() != NATIVE_TOKEN_ADDRESS.toLowerCase()) { + const allowParams = allowTokenTransferProxyParams(tokenIn, holder, ZAP_ROUTER_ADDESS) + const allowanceTx = await ts.simulate({ + from: holder, + ...allowParams, + }) + if (!allowanceTx.success) console.log(allowanceTx.url) + } + } + + let zapInParams = { + from: holder, + data: data, + to: ZAP_ROUTER_ADDESS, + value: input.value || '0', + } + + return await ts.simulate(zapInParams, stateOverrideObj) +} + +function getMatch(a, b) { + var matches = [] + for (var i = 0; i < a.length; i++) { + for (var e = 0; e < b.length; e++) { + if (a[i] === b[e]) matches.push(a[i]) + } + } + return matches +} + +function constructAddBalanceFn(varName) { + return (address, amount) => { + return { + [`${varName}[${address}]`]: amount, + } + } +} + +function constructAddBAllowanceFn(varName) { + return (address, spender, amount) => { + return { + [`${varName}[${address}][${spender}]`]: amount, + } + } +} +async function estimateGas(input) { + const rpc = await getForkRpcUrl(input.chain) + const provider = new StaticJsonRpcProvider(rpc, Number(input.chain)) + let txObject = { + from: input.to, + data: input.encodedSwapData, + value: 0, + to: input.routerAddress, + } + + try { + const result = await provider.estimateGas(txObject) + return { + success: true, + gasUsed: result.toNumber().toString(), + } + } catch (error) { + return { + success: false, + error: error, + } + } +} + +function getTokenInFromZapEncodeData(zapInData) { + let funcSig = zapInData['data'].slice(0, 10) + switch (funcSig) { + case '0x0779b145': + return NATIVE_TOKEN_ADDRESS + case '0xbea67258': + let decode = ksZapRouterInterface.decodeFunctionData('zapIn', zapInData['data']) + const tokenInAddress = decode[0][1] + return tokenInAddress + default: + throw 'function selector' + } +} +module.exports = { + simulateTenderly, + getForkRpcUrl, + estimateGas, + getTokenInFromZapEncodeData, +} diff --git a/package.json b/package.json index 4601d8a960..03ce74aef3 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,8 @@ "start-dev": "vite --mode dev --host", "start-stg": "vite --mode stg --host", "start-prod": "vite --mode production --host", - "test-e2e": "synpress run -cf cypress.config.ts", - "test-schedule": "synpress run -cf cypress.config.ts" + "test:e2e": "synpress run -cf cypress.config.ts", + "test:zap-sim": "node --test --test-reporter spec cypress/e2e/specs/zap-in-simulator-test.mjs" }, "browserslist": [ "chrome >= 52", @@ -177,6 +177,7 @@ "@vitejs/plugin-react": "^3.1.0", "babel-plugin-lodash": "^3.3.4", "babel-plugin-macros": "^3.1.0", + "cypress-terminal-report": "^5.3.9", "env-cmd": "^10.1.0", "eslint": "^8.38.0", "eslint-config-prettier": "^8.5.0", diff --git a/src/components/InputStepCounter/InputStepCounter.tsx b/src/components/InputStepCounter/InputStepCounter.tsx index 826d647349..2992b5fdb8 100644 --- a/src/components/InputStepCounter/InputStepCounter.tsx +++ b/src/components/InputStepCounter/InputStepCounter.tsx @@ -159,6 +159,7 @@ const StepCounter = ({ { diff --git a/src/pages/AddLiquidityV2/index.tsx b/src/pages/AddLiquidityV2/index.tsx index 02520b9c48..b76daa5a2d 100644 --- a/src/pages/AddLiquidityV2/index.tsx +++ b/src/pages/AddLiquidityV2/index.tsx @@ -1458,6 +1458,7 @@ export default function AddLiquidity() { diff --git a/src/pages/AddLiquidityV2/styled.tsx b/src/pages/AddLiquidityV2/styled.tsx index 2a4672f85f..f739a445eb 100644 --- a/src/pages/AddLiquidityV2/styled.tsx +++ b/src/pages/AddLiquidityV2/styled.tsx @@ -298,7 +298,7 @@ export const MethodSelector = ({ > - + Zap In