diff --git a/.github/workflows/build/action.yml b/.github/workflows/build/action.yml index 843ad309f7..fd3d7998f2 100644 --- a/.github/workflows/build/action.yml +++ b/.github/workflows/build/action.yml @@ -16,7 +16,19 @@ inputs: runs: using: 'composite' + steps: + - name: Set environment variables + shell: bash + run: | + if [ "${{ inputs.prod }}" = "true" ]; then + echo "NEXT_PUBLIC_INFURA_TOKEN=${{ fromJSON(inputs.secrets).NEXT_PUBLIC_INFURA_TOKEN }}" >> $GITHUB_ENV + echo "NEXT_PUBLIC_SAFE_APPS_INFURA_TOKEN=${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SAFE_APPS_INFURA_TOKEN }}" >> $GITHUB_ENV + else + echo "NEXT_PUBLIC_INFURA_TOKEN=${{ fromJSON(inputs.secrets).NEXT_PUBLIC_INFURA_TOKEN_DEVSTAGING }}" >> $GITHUB_ENV + echo "NEXT_PUBLIC_SAFE_APPS_INFURA_TOKEN=${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SAFE_APPS_INFURA_TOKEN_DEVSTAGING }}" >> $GITHUB_ENV + fi + - name: Build shell: bash run: yarn build @@ -31,8 +43,6 @@ runs: NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID }} NEXT_PUBLIC_GOOGLE_TAG_MANAGER_LATEST_AUTH: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_GOOGLE_TAG_MANAGER_LATEST_AUTH }} NEXT_PUBLIC_GOOGLE_TAG_MANAGER_LIVE_AUTH: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_GOOGLE_TAG_MANAGER_LIVE_AUTH }} - NEXT_PUBLIC_INFURA_TOKEN: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_INFURA_TOKEN }} - NEXT_PUBLIC_SAFE_APPS_INFURA_TOKEN: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SAFE_APPS_INFURA_TOKEN }} NEXT_PUBLIC_SENTRY_DSN: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SENTRY_DSN }} NEXT_PUBLIC_TENDERLY_ORG_NAME: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_TENDERLY_ORG_NAME }} NEXT_PUBLIC_TENDERLY_PROJECT_NAME: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_TENDERLY_PROJECT_NAME }} diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index aa986eed3d..367059656e 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -41,7 +41,7 @@ jobs: - uses: ./.github/workflows/build with: secrets: ${{ toJSON(secrets) }} - prod: ${{ github.ref == 'refs/heads/main' }} + prod: ${{ github.base_ref == 'main' }} # PRs to main are Release Candidates and must be tested with prod settings - uses: ./.github/workflows/build-storybook @@ -53,14 +53,14 @@ jobs: # Staging - name: Deploy to the staging S3 - if: github.ref == 'refs/heads/main' + if: startsWith(github.ref, 'refs/heads/main') env: BUCKET: s3://${{ secrets.AWS_STAGING_BUCKET_NAME }}/current run: bash ./scripts/github/s3_upload.sh # Dev - name: Deploy to the dev S3 - if: github.ref == 'refs/heads/dev' + if: startsWith(github.ref, 'refs/heads/dev') env: BUCKET: s3://${{ secrets.AWS_DEVELOPMENT_BUCKET_NAME }} run: bash ./scripts/github/s3_upload.sh diff --git a/cypress/e2e/happypath/tx_history_filter_hp_1.cy.js b/cypress/e2e/happypath/tx_history_filter_hp_1.cy.js new file mode 100644 index 0000000000..454ac5e403 --- /dev/null +++ b/cypress/e2e/happypath/tx_history_filter_hp_1.cy.js @@ -0,0 +1,136 @@ +/* eslint-disable */ +import * as constants from '../../support/constants.js' +import * as main from '../pages/main.page.js' +import * as createTx from '../pages/create_tx.pages.js' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' + +let staticSafes = [] +const startDate = '01/12/2023' +const endDate = '01/12/2023' +const startDate2 = '20/12/2023' +const endDate2 = '20/12/2023' + +describe('Tx history happy path tests 1', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.transactionsHistoryUrl + staticSafes.SEP_STATIC_SAFE_7) + main.acceptCookies() + }) + + it('Verify a user can filter incoming transactions by dates, amount and token address', () => { + const uiDate = 'Dec 1, 2023' + const uiDate2 = 'Dec 1, 2023 - 8:05:00 AM' + const uiDate3 = 'Dec 1, 2023 - 7:52:36 AM' + const uiDate4 = 'Dec 15, 2023 - 10:33:00 AM' + const amount = '0.001' + const token = '0x7CB180dE9BE0d8935EbAAc9b4fc533952Df128Ae' + + // date and amount + createTx.clickOnFilterBtn() + createTx.setTxType(createTx.filterTypes.incoming) + createTx.fillFilterForm({ endDate: endDate, amount: amount }) + createTx.clickOnApplyBtn() + createTx.verifyNumberOfTransactions(2) + createTx.checkTxItemDate(0, uiDate) + createTx.checkTxItemDate(1, uiDate) + + // combined filters + createTx.clickOnFilterBtn() + createTx.fillFilterForm({ startDate: startDate }) + createTx.clickOnApplyBtn() + createTx.verifyNumberOfTransactions(2) + createTx.checkTxItemDate(0, uiDate) + createTx.checkTxItemDate(1, uiDate) + + // reset txs + createTx.clickOnFilterBtn() + createTx.clickOnClearBtn() + createTx.verifyNumberOfTransactions(25) + + // chronological order + createTx.fillFilterForm({ startDate: startDate, endDate: endDate }) + createTx.clickOnApplyBtn() + createTx.verifyNumberOfTransactions(7) + createTx.checkTxItemDate(5, uiDate2) + createTx.checkTxItemDate(6, uiDate3) + + // token + createTx.clickOnFilterBtn() + createTx.clickOnClearBtn() + createTx.fillFilterForm({ token: token }) + createTx.clickOnApplyBtn() + createTx.verifyNumberOfTransactions(1) + createTx.checkTxItemDate(0, uiDate4) + + // no txs + createTx.clickOnFilterBtn() + createTx.fillFilterForm({ startDate: startDate2, endDate: endDate2 }) + createTx.clickOnApplyBtn() + createTx.verifyNoTxDisplayed('incoming') + }) + + it('Verify a user can filter outgoing transactions by dates, nonce, amount and recipient', () => { + const uiDate = 'Nov 30, 2023 - 11:06:00 AM' + const uiDate2 = 'Dec 1, 2023 - 7:54:36 AM' + const uiDate3 = 'Dec 1, 2023 - 7:37:24 AM' + const uiDate4 = 'Nov 30, 2023 - 11:02:12 AM' + const amount = '0.000000000001' + const recipient = 'sep:0x06373d5e45AD31BD354CeBfA8dB4eD2c75B8708e' + + // date and recipient + createTx.clickOnFilterBtn() + createTx.setTxType(createTx.filterTypes.outgoing) + + createTx.fillFilterForm({ endDate: endDate, recipient: recipient }) + createTx.clickOnApplyBtn() + createTx.verifyNumberOfTransactions(1) + createTx.checkTxItemDate(0, uiDate4) + + // combined filters + createTx.clickOnFilterBtn() + createTx.fillFilterForm({ startDate: startDate }) + createTx.clickOnApplyBtn() + createTx.verifyNumberOfTransactions(0) + + // reset txs + createTx.clickOnFilterBtn() + createTx.clickOnClearBtn() + createTx.clickOnApplyBtn() + createTx.verifyNumberOfTransactions(14) + + // chronological order + createTx.clickOnFilterBtn() + createTx.fillFilterForm({ startDate: startDate, endDate: endDate }) + createTx.clickOnApplyBtn() + createTx.verifyNumberOfTransactions(2) + createTx.checkTxItemDate(0, uiDate2) + createTx.checkTxItemDate(1, uiDate3) + + // nonce + createTx.clickOnFilterBtn() + createTx.clickOnClearBtn() + createTx.fillFilterForm({ nonce: '1' }) + createTx.clickOnApplyBtn() + createTx.verifyNumberOfTransactions(1) + createTx.checkTxItemDate(0, uiDate) + + // amount + createTx.clickOnFilterBtn() + createTx.clickOnClearBtn() + createTx.fillFilterForm({ amount: amount }) + createTx.clickOnApplyBtn() + createTx.verifyNumberOfTransactions(1) + createTx.checkTxItemDate(0, uiDate4) + + // no txs + createTx.clickOnFilterBtn() + createTx.clickOnClearBtn() + createTx.fillFilterForm({ startDate: startDate2, endDate: endDate2 }) + createTx.clickOnApplyBtn() + createTx.verifyNoTxDisplayed('outgoing') + }) +}) diff --git a/cypress/e2e/happypath/tx_history_filter_hp_2.cy.js b/cypress/e2e/happypath/tx_history_filter_hp_2.cy.js new file mode 100644 index 0000000000..98e0f5d983 --- /dev/null +++ b/cypress/e2e/happypath/tx_history_filter_hp_2.cy.js @@ -0,0 +1,30 @@ +import * as constants from '../../support/constants.js' +import * as main from '../pages/main.page.js' +import * as createTx from '../pages/create_tx.pages.js' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' + +let staticSafes = [] + +describe('Tx history happy path tests 2', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.transactionsHistoryUrl + staticSafes.SEP_STATIC_SAFE_8) + main.acceptCookies() + }) + + it('Verify a user can filter outgoing transactions by module', () => { + const moduleAddress = 'sep:0xCFbFaC74C26F8647cBDb8c5caf80BB5b32E43134' + const uiDate = 'Jan 30, 2024 - 10:53:48 AM' + + createTx.clickOnFilterBtn() + createTx.setTxType(createTx.filterTypes.module) + createTx.fillFilterForm({ address: moduleAddress }) + createTx.clickOnApplyBtn() + createTx.verifyNumberOfTransactions(1) + createTx.checkTxItemDate(0, uiDate) + }) +}) diff --git a/cypress/e2e/pages/create_tx.pages.js b/cypress/e2e/pages/create_tx.pages.js index 49dd8f99eb..53ec026d18 100644 --- a/cypress/e2e/pages/create_tx.pages.js +++ b/cypress/e2e/pages/create_tx.pages.js @@ -32,6 +32,15 @@ const untrustedTokenWarningModal = '[data-testid="untrusted-token-warning"]' const sendTokensBtn = '[data-testid="send-tokens-btn"]' export const replacementNewSigner = '[data-testid="new-owner"]' export const messageItem = '[data-testid="message-item"]' +const filterStartDateInput = '[data-testid="start-date"]' +const filterEndDateInput = '[data-testid="end-date"]' +const filterAmountInput = '[data-testid="amount-input"]' +const filterTokenInput = '[data-testid="token-input"]' +const filterNonceInput = '[data-testid="nonce-input"]' +const filterApplyBtn = '[data-testid="apply-btn"]' +const filterClearBtn = '[data-testid="clear-btn"]' +const addressItem = '[data-testid="address-item"]' +const radioSelector = 'div[role="radiogroup"]' const viewTransactionBtn = 'View transaction' const transactionDetailsTitle = 'Transaction details' @@ -52,6 +61,73 @@ const signBtnStr = 'Sign' const expandAllBtnStr = 'Expand all' const collapseAllBtnStr = 'Collapse all' export const messageNestedStr = `"nestedString": "Test message 3 off-chain"` +const noTxFoundStr = (type) => `0 ${type} transactions found` + +export const filterTypes = { + incoming: 'Incoming', + outgoing: 'Outgoing', + module: 'Module-based', +} + +export function setTxType(type) { + cy.get(radioSelector).find('label').contains(type).click() +} + +export function verifyNoTxDisplayed(type) { + cy.get(transactionItem) + .should('have.length', 0) + .then(($items) => { + main.verifyElementsCount($items, 0) + }) + + cy.contains(noTxFoundStr(type)).should('be.visible') +} + +export function clickOnApplyBtn() { + cy.get(filterApplyBtn).click() +} + +export function clickOnClearBtn() { + cy.get(filterClearBtn).click() +} + +export function fillFilterForm({ address, startDate, endDate, amount, token, nonce, recipient } = {}) { + const inputMap = { + address: { selector: addressItem, findInput: true }, + startDate: { selector: filterStartDateInput, findInput: true }, + endDate: { selector: filterEndDateInput, findInput: true }, + amount: { selector: filterAmountInput, findInput: true }, + token: { selector: filterTokenInput, findInput: true }, + nonce: { selector: filterNonceInput, findInput: true }, + recipient: { selector: addressItem, findInput: true }, + } + + Object.entries({ address, startDate, endDate, amount, token, nonce, recipient }).forEach(([key, value]) => { + if (value !== undefined) { + const { selector, findInput } = inputMap[key] + const element = findInput ? cy.get(selector).find('input') : cy.get(selector) + element.clear().type(value) + } + }) +} + +export function clickOnFilterBtn() { + cy.get('button').then((buttons) => { + const filterButton = [...buttons].find((button) => { + return ['Filter', 'Incoming', 'Outgoing', 'Module-based'].includes(button.innerText) + }) + + if (filterButton) { + cy.wrap(filterButton).click() + } else { + throw new Error('No filter button found') + } + }) +} + +export function checkTxItemDate(index, date) { + cy.get(txDate).eq(index).should('contain', date) +} export function clickOnSendTokensBtn() { cy.get(sendTokensBtn).click() diff --git a/cypress/e2e/pages/owners.pages.js b/cypress/e2e/pages/owners.pages.js index 1f6a142fc1..9dd97666b9 100644 --- a/cypress/e2e/pages/owners.pages.js +++ b/cypress/e2e/pages/owners.pages.js @@ -242,5 +242,5 @@ export function verifyThreshold(startValue, endValue) { cy.get(thresholdInput).parent().click() cy.get(thresholdList).contains(endValue).should('be.visible') cy.get(thresholdList).find('li').should('have.length', endValue) - cy.get('body').click({ force: true }) + cy.get('body').click(0, 0) } diff --git a/cypress/e2e/smoke/tx_history_filter.cy.js b/cypress/e2e/smoke/tx_history_filter.cy.js new file mode 100644 index 0000000000..97cf73cc47 --- /dev/null +++ b/cypress/e2e/smoke/tx_history_filter.cy.js @@ -0,0 +1,277 @@ +/* eslint-disable */ +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import { buildQueryUrl } from '../../support/utils/txquery.js' +import * as constants from '../../support/constants.js' + +let staticSafes = [] +let safeAddress +const success = constants.transactionStatus.success.toUpperCase() +const txType_outgoing = 'multisig' +const txType_incoming = 'incoming' + +describe('[SMOKE] API Tx history filter tests', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + safeAddress = staticSafes.SEP_STATIC_SAFE_7.substring(4) + }) + + const chainId = constants.networkKeys.sepolia + + // incoming tx + it('Verify that when date range is set with 1 date, correct data is returned', () => { + const params = { + transactionType: txType_incoming, + startDate: '2023-12-14T23:00:00.000Z', + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + expect(results.length).to.eq(1) + const txType = results.filter((tx) => tx.transaction.txStatus === success) + const txdirection = results.filter( + (tx) => tx.transaction.txInfo.direction === params.transactionType.toUpperCase(), + ) + expect(txType.length, 'Number of successful transactions').to.eq(1) + expect(txdirection.length, 'Number of incoming transactions').to.eq(1) + }) + }) + + it('Verify that when a large amount is set in the amount field, error is returned', () => { + const params = { + transactionType: txType_incoming, + startDate: '2023-12-14T23:00:00.000Z', + value: '893748237489328479823749823748723984728734000000000000000000', + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request({ + url: url, + failOnStatusCode: false, + }).then((response) => { + expect(response.status).to.eq(400) + }) + }) + + it('Verify that applying a token for which no transaction exist returns no results', () => { + const params = { + transactionType: txType_incoming, + startDate: '2023-12-14T23:00:00.000Z', + token_address: constants.RECIPIENT_ADDRESS, + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + expect(results.length, 'Number of transactions').to.eq(0) + }) + }) + + it('Verify that when the date range filter is set to only one day with no transactions, it returns no results', () => { + const params = { + transactionType: txType_incoming, + startDate: '2023-12-31T23:00:00.000Z', + token_address: constants.RECIPIENT_ADDRESS, + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + expect(results.length, 'Number of transactions').to.eq(0) + }) + }) + + it('Verify setting non-existent amount with valid data range returns no results', () => { + const params = { + transactionType: txType_incoming, + startDate: '2023-11-30T23:00:00.000Z', + endDate: '2023-12-01T22:59:59.999Z', + value: '20000000000000000000', + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + expect(results.length, 'Number of transactions').to.eq(0) + }) + }) + + it('Verify timestamps are within the expected range for incoming transactions', () => { + const params = { + transactionType: txType_incoming, + startDate: '2023-11-29T23:00:00.000Z', + endDate: '2023-12-15T22:59:59.999Z', + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + results.forEach((tx) => { + const timestamp = tx.transaction.timestamp + expect(timestamp, 'Transaction timestamp').to.be.within( + new Date(params.startDate).getTime(), + new Date(params.endDate).getTime(), + ) + }) + }) + }) + + it('Verify sender and recipient addresses for incoming transactions', () => { + const params = { + transactionType: txType_incoming, + startDate: '2023-12-14T23:00:00.000Z', + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + results.forEach((tx) => { + expect(tx.transaction.txInfo.sender.value, 'Sender address').to.match(/^0x[0-9a-fA-F]{40}$/) + expect(tx.transaction.txInfo.recipient.value, 'Recipient address').to.eq(safeAddress) + }) + }) + }) + + // outgoing tx + it('Verify that when date range is set with 1 date, correct data is returned', () => { + const params = { + transactionType: txType_outgoing, + endDate: '2023-11-30T22:59:59.999Z', + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + const txType = results.filter((tx) => tx.transaction.txStatus === success) + expect(txType.length, 'Number of successful transactions').to.eq(11) + }) + }) + + it('Verify that when a large amount is set in the amount field, error is returned', () => { + const params = { + transactionType: txType_outgoing, + startDate: '2023-12-14T23:00:00.000Z', + value: '893748237489328479823749823748723984728734000000000000000000', + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request({ + url: url, + failOnStatusCode: false, + }).then((response) => { + expect(response.status).to.eq(400) + }) + }) + + it('Verify that applying a recipient for which no transaction exist returns no results', () => { + const params = { + transactionType: txType_outgoing, + startDate: '2023-12-14T23:00:00.000Z', + to: constants.RECIPIENT_ADDRESS, + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + expect(results.length, 'Number of transactions').to.eq(0) + }) + }) + + it('Verify that when the date range filter is set to only one day with no transactions, it returns no results', () => { + const params = { + transactionType: txType_outgoing, + startDate: '2023-12-31T23:00:00.000Z', + token_address: constants.RECIPIENT_ADDRESS, + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + expect(results.length, 'Number of transactions').to.eq(0) + }) + }) + + it('Verify setting existent amount with invalid data range returns no results', () => { + const params = { + transactionType: txType_outgoing, + startDate: '2023-12-15T23:00:00.000Z', + endDate: '2023-12-20T22:59:59.999Z', + value: '10000000000000000000', + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + expect(results.length, 'Number of transactions').to.eq(0) + }) + }) + + it('Verify setting existent nonce with invalid end date returns no results', () => { + const params = { + transactionType: txType_outgoing, + endDate: '2023-11-28T22:59:59.999Z', + nonce: 10, + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + expect(results.length, 'Number of transactions').to.eq(0) + }) + }) + + it('Verify timestamps are within the expected range for transactions', () => { + const params = { + transactionType: txType_outgoing, + startDate: '2023-11-29T00:00:00.000Z', + endDate: '2023-11-30T22:59:59.999Z', + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + results.forEach((tx) => { + const timestamp = tx.transaction.timestamp + expect(timestamp, 'Transaction timestamp').to.be.within( + new Date(params.startDate).getTime(), + new Date(params.endDate).getTime(), + ) + }) + }) + }) + + it('Verify sender and recipient addresses for transactions', () => { + const params = { + transactionType: txType_outgoing, + startDate: '2023-11-30T22:59:59.999Z', + endDate: '2023-11-30T22:59:59.999Z', + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + results.forEach((tx) => { + expect(tx.transaction.txInfo.sender.value, 'Sender address').to.eq(safeAddress) + expect(tx.transaction.txInfo.recipient.value, 'Recipient address').to.match(/^0x[0-9a-fA-F]{40}$/) + }) + }) + }) + + it('Verify that setting a non-existent token for transactions returns no results', () => { + const params = { + transactionType: txType_outgoing, + startDate: '2023-12-01T00:00:00.000Z', + endDate: '2023-12-01T23:59:59.999Z', + to: constants.RECIPIENT_ADDRESS, + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + expect(results.length, 'Number of transactions').to.eq(0) + }) + }) +}) diff --git a/cypress/support/utils/txquery.js b/cypress/support/utils/txquery.js new file mode 100644 index 0000000000..c4845de5ff --- /dev/null +++ b/cypress/support/utils/txquery.js @@ -0,0 +1,41 @@ +/* eslint-disable */ +import { stagingCGWUrlv1 } from '../constants' +function buildQueryUrl({ chainId, safeAddress, transactionType, ...params }) { + const baseUrlMap = { + incoming: `${stagingCGWUrlv1}/chains/${chainId}/safes/${safeAddress}/incoming-transfers/`, + multisig: `${stagingCGWUrlv1}/chains/${chainId}/safes/${safeAddress}/multisig-transactions/`, + module: `${stagingCGWUrlv1}/chains/${chainId}/safes/${safeAddress}/module-transactions/`, + } + + const defaultParams = { + safe: `sep:${safeAddress}`, + timezone_offset: '7200000', + trusted: 'false', + } + + const paramMap = { + startDate: 'execution_date__gte', + endDate: 'execution_date__lte', + value: 'value', + tokenAddress: 'token_address', + to: 'to', + nonce: 'nonce', + module: 'module', + } + + const baseUrl = baseUrlMap[transactionType] + if (!baseUrl) { + throw new Error(`Unsupported transaction type: ${transactionType}`) + } + + const mergedParams = { ...defaultParams, ...params } + const queryString = Object.entries(mergedParams) + .map(([key, value]) => `${paramMap[key] || key}=${value}`) + .join('&') + + return baseUrl + '?' + queryString +} + +export default { + buildQueryUrl, +} diff --git a/package.json b/package.json index 5e9277abb2..3b870a826a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "safe-wallet-web", "homepage": "https://github.com/safe-global/safe-wallet-web", "license": "GPL-3.0", - "version": "1.36.5", + "version": "1.37.0", "type": "module", "scripts": { "dev": "next dev", diff --git a/src/components/sidebar/SidebarNavigation/config.tsx b/src/components/sidebar/SidebarNavigation/config.tsx index 6797127c73..64ed08785d 100644 --- a/src/components/sidebar/SidebarNavigation/config.tsx +++ b/src/components/sidebar/SidebarNavigation/config.tsx @@ -9,11 +9,13 @@ import AppsIcon from '@/public/images/apps/apps-icon.svg' import SettingsIcon from '@/public/images/sidebar/settings.svg' import SwapIcon from '@/public/images/common/swap.svg' import { SvgIcon } from '@mui/material' +import Chip from '@mui/material/Chip' export type NavItem = { label: string icon?: ReactElement href: string + tag?: ReactElement } export const navItems: NavItem[] = [ @@ -31,6 +33,7 @@ export const navItems: NavItem[] = [ label: 'Swap', icon: , href: AppRoutes.swap, + tag: , }, { label: 'Transactions', diff --git a/src/components/sidebar/SidebarNavigation/index.tsx b/src/components/sidebar/SidebarNavigation/index.tsx index 91b08bf290..7c6fd9eba1 100644 --- a/src/components/sidebar/SidebarNavigation/index.tsx +++ b/src/components/sidebar/SidebarNavigation/index.tsx @@ -49,13 +49,6 @@ const Navigation = (): ReactElement => { } } - const getCounter = (item: NavItem) => { - // Indicate qeueued txs - if (item.href === AppRoutes.transactions.history) { - return queueSize - } - } - // Route Transactions to Queue if there are queued txs, otherwise to History const getRoute = (href: string) => { if (href === AppRoutes.transactions.history && queueSize) { @@ -75,6 +68,12 @@ const Navigation = (): ReactElement => { {enabledNavItems.map((item) => { const isSelected = currentSubdirectory === getSubdirectory(item.href) + let ItemTag = item.tag ? item.tag : null + + if (item.href === AppRoutes.transactions.history) { + ItemTag = queueSize ? : null + } + return ( { {item.label} - + {ItemTag} diff --git a/src/components/theme/darkPalette.ts b/src/components/theme/darkPalette.ts index 8ecf64ed9b..46818fd506 100644 --- a/src/components/theme/darkPalette.ts +++ b/src/components/theme/darkPalette.ts @@ -12,7 +12,7 @@ const darkPalette = { secondary: { dark: '#636669', main: '#FFFFFF', - light: '#12FF80', + light: '#B0FFC9', background: '#1B2A22', }, border: { diff --git a/src/components/theme/safeTheme.ts b/src/components/theme/safeTheme.ts index 766b4c96c7..e9108c3258 100644 --- a/src/components/theme/safeTheme.ts +++ b/src/components/theme/safeTheme.ts @@ -17,6 +17,7 @@ declare module '@mui/material/styles' { backdrop: Palette['primary'] static: Palette['primary'] } + export interface PaletteOptions { border: PaletteOptions['primary'] logo: PaletteOptions['primary'] @@ -33,6 +34,7 @@ declare module '@mui/material/styles' { export interface PaletteColor { background?: string } + export interface SimplePaletteColorOptions { background?: string } @@ -52,6 +54,7 @@ declare module '@mui/material/Button' { export interface ButtonPropsColorOverrides { background: true } + export interface ButtonPropsVariantOverrides { danger: true } @@ -279,6 +282,14 @@ const createSafeTheme = (mode: PaletteMode): Theme => { }, }, }, + MuiChip: { + styleOverrides: { + colorSuccess: ({ theme }) => ({ + backgroundColor: theme.palette.secondary.light, + height: '24px', + }), + }, + }, MuiAlert: { styleOverrides: { standardError: ({ theme }) => ({ diff --git a/src/components/transactions/TxFilterForm/index.tsx b/src/components/transactions/TxFilterForm/index.tsx index 2d31eb0232..9fb166b661 100644 --- a/src/components/transactions/TxFilterForm/index.tsx +++ b/src/components/transactions/TxFilterForm/index.tsx @@ -146,7 +146,7 @@ const TxFilterForm = ({ toggleFilter }: { toggleFilter: () => void }): ReactElem {!isModuleFilter && ( <> - + void }): ReactElem }} /> - + { return } - trackEvent({ ...TX_EVENTS.EXECUTE, label: TX_TYPES.batch }) + trackEvent({ ...TX_EVENTS.EXECUTE, label: TX_TYPES.bulk_execute }) } const submitDisabled = loading || !isSubmittable || !gasPrice diff --git a/src/services/analytics/events/transactions.ts b/src/services/analytics/events/transactions.ts index 48a456f7ee..131e527e6a 100644 --- a/src/services/analytics/events/transactions.ts +++ b/src/services/analytics/events/transactions.ts @@ -22,6 +22,7 @@ export enum TX_TYPES { walletconnect = 'walletconnect', custom = 'custom', native_swap = 'native_swap', + bulk_execute = 'bulk_execute', // Counterfactual activate_without_tx = 'activate_without_tx', diff --git a/src/services/tx/tx-sender/sdk.ts b/src/services/tx/tx-sender/sdk.ts index 73d84b39c1..df3f5c339b 100644 --- a/src/services/tx/tx-sender/sdk.ts +++ b/src/services/tx/tx-sender/sdk.ts @@ -15,6 +15,7 @@ import { type OnboardAPI } from '@web3-onboard/core' import type { ConnectedWallet } from '@/hooks/wallets/useOnboard' import { asError } from '@/services/exceptions/utils' import { UncheckedJsonRpcSigner } from '@/utils/providers/UncheckedJsonRpcSigner' +import get from 'lodash/get' export const getAndValidateSafeSDK = (): Safe => { const safeSDK = getSafeSDK() @@ -36,24 +37,29 @@ async function switchOrAddChain(walletProvider: ConnectedWallet['provider'], cha params: [{ chainId: hexChainId }], }) } catch (error) { - if ((error as Error & { code: number }).code !== UNKNOWN_CHAIN_ERROR_CODE) { - throw error + const errorCode = get(error, 'code') as number | undefined + + // Rabby emits the same error code as MM, but it is nested + const nestedErrorCode = get(error, 'data.originalError.code') as number | undefined + + if (errorCode === UNKNOWN_CHAIN_ERROR_CODE || nestedErrorCode === UNKNOWN_CHAIN_ERROR_CODE) { + const chain = await getChainConfig(chainId) + + return walletProvider.request({ + method: 'wallet_addEthereumChain', + params: [ + { + chainId: hexChainId, + chainName: chain.chainName, + nativeCurrency: chain.nativeCurrency, + rpcUrls: [chain.publicRpcUri.value], + blockExplorerUrls: [new URL(chain.blockExplorerUriTemplate.address).origin], + }, + ], + }) } - const chain = await getChainConfig(chainId) - - return walletProvider.request({ - method: 'wallet_addEthereumChain', - params: [ - { - chainId: hexChainId, - chainName: chain.chainName, - nativeCurrency: chain.nativeCurrency, - rpcUrls: [chain.publicRpcUri.value], - blockExplorerUrls: [new URL(chain.blockExplorerUriTemplate.address).origin], - }, - ], - }) + throw error } }