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
}
}