Files
Daniel DimitrovandGitHub 0ac5b74ffb feat(web): add trusted Safe list to mitigate address poisoning (#7099)
* feat(web): create curated safe list experience

feat(web): rename pinning

feat(web): warn user that he is interacting with the tx flow of a safe that is not in his trusted safe list

fix(web): prettier

fix(web): cleanup

fix(web): simplify complexity

fix(web): multichain safes not shown correctly

refactor(web): move untrusted safe warning to safe-shield

fix(web): cleanup

fix(web): clean up

fix(web): cleanup

fix(web): clean up

* fix(web): improve UI copy

* fix(web): failing unit tests

* fix(web): add analytics evets

* fix(web): cypress tests with new trusted list

* fix(web): adjust cypress tests

* fix(web): prettier

* fix(web): knip errors

* fix(web): cypress tests

* fix(web): reduce code complexity

* fix(web): use monospace font
2026-02-09 09:29:53 +01:00

284 lines
8.5 KiB
JavaScript

let LOCAL_STORAGE_MEMORY = {}
Cypress.Commands.add('saveLocalStorageCache', () => {
Object.keys(localStorage).forEach((key) => {
LOCAL_STORAGE_MEMORY[key] = localStorage[key]
})
})
Cypress.Commands.add('restoreLocalStorageCache', () => {
Object.keys(LOCAL_STORAGE_MEMORY).forEach((key) => {
localStorage.setItem(key, LOCAL_STORAGE_MEMORY[key])
})
})
/**
* Wait for a thing by polling for it
*
* @param {(string|function)} item - A jQuery selector string or a function that returns a boolean
* @param {object} [options] - An options object
* @param {number} [options.timeout=200] - The time between tries in milliseconds
* @param {number} [options.tries=300] - The amount of times to try before failing
*
* @return {Promise} - A Cypress promise, more at https://docs.cypress.io/api/utilities/promise.html
*/
const waitForSelector = (item, options = {}) => {
if (typeof item !== 'string' && !(item instanceof Function)) {
throw new Error('Cypress plugin waitForSelector: The first parameter should be a string or a function')
}
const defaultSettings = {
timeout: 200,
tries: 300,
}
const SETTINGS = { ...defaultSettings, ...options }
const check = (item) => {
if (typeof item === 'string') {
return Cypress.$(item).length > 0
} else {
return item()
}
}
return new Cypress.Promise((resolve, reject) => {
let index = 0
const interval = setInterval(() => {
if (check(item)) {
clearInterval(interval)
resolve()
}
if (index > SETTINGS.tries) {
reject()
}
index++
}, SETTINGS.timeout)
})
}
Cypress.Commands.add('waitForSelector', waitForSelector)
const DEFAULT_OPTS = {
log: true,
timeout: 30000,
}
const DEFAULT_IFRAME_SELECTOR = 'iframe'
function sleep(timeout) {
return new Promise((resolve) => setTimeout(resolve, timeout))
}
// This command checks that an iframe has loaded onto the page
// - This will verify that the iframe is loaded to any page other than 'about:blank'
// cy.frameLoaded()
// - This will verify that the iframe is loaded to any url containing the given path part
// cy.frameLoaded({ url: 'https://google.com' })
// cy.frameLoaded({ url: '/join' })
// cy.frameLoaded({ url: '?some=query' })
// cy.frameLoaded({ url: '#/hash/path' })
// - You can also give it a selector to check that a specific iframe has loaded
// cy.frameLoaded('#my-frame')
// cy.frameLoaded('#my-frame', { url: '/join' })
Cypress.Commands.add('frameLoaded', (selector, opts) => {
if (selector === undefined) {
selector = DEFAULT_IFRAME_SELECTOR
} else if (typeof selector === 'object') {
opts = selector
selector = DEFAULT_IFRAME_SELECTOR
}
const fullOpts = {
...DEFAULT_OPTS,
...opts,
}
const log = fullOpts.log
? Cypress.log({
name: 'frame loaded',
displayName: 'frame loaded',
message: [selector],
}).snapshot()
: null
return cy.get(selector, { log: false }).then({ timeout: fullOpts.timeout }, async ($frame) => {
log?.set('$el', $frame)
if ($frame.length !== 1) {
throw new Error(
`cypress-iframe commands can only be applied to exactly one iframe at a time. Instead found ${$frame.length}`,
)
}
const contentWindow = $frame.prop('contentWindow')
const hasNavigated = fullOpts.url
? () =>
typeof fullOpts.url === 'string'
? contentWindow.location.toString().includes(fullOpts.url)
: fullOpts.url?.test(contentWindow.location.toString())
: () => contentWindow.location.toString() !== 'about:blank'
while (!hasNavigated()) {
await sleep(100)
}
if (contentWindow.document.readyState === 'complete') {
return $frame
}
const loadLog = Cypress.log({
name: 'Frame Load',
message: [contentWindow.location.toString()],
event: true,
}).snapshot()
await new Promise((resolve) => {
Cypress.$(contentWindow).on('load', resolve)
})
loadLog.end()
log?.finish()
return $frame
})
})
// This will cause subsequent commands to be executed inside of the given iframe
// - This will verify that the iframe is loaded to any page other than 'about:blank'
// cy.iframe().find('.some-button').should('be.visible').click()
// cy.iframe().contains('Some hidden element').should('not.be.visible')
// cy.find('#outside-iframe').click() // this will be executed outside the iframe
// - You can also give it a selector to find elements inside of a specific iframe
// cy.iframe('#my-frame').find('.some-button').should('be.visible').click()
// cy.iframe('#my-second-frame').contains('Some hidden element').should('not.be.visible')
Cypress.Commands.add('iframe', (selector, opts) => {
if (selector === undefined) {
selector = DEFAULT_IFRAME_SELECTOR
} else if (typeof selector === 'object') {
opts = selector
selector = DEFAULT_IFRAME_SELECTOR
}
const fullOpts = {
...DEFAULT_OPTS,
...opts,
}
const log = fullOpts.log
? Cypress.log({
name: 'iframe',
displayName: 'iframe',
message: [selector],
}).snapshot()
: null
return cy.frameLoaded(selector, { ...fullOpts, log: false }).then(($frame) => {
log?.set('$el', $frame).end()
const contentWindow = $frame.prop('contentWindow')
return Cypress.$(contentWindow.document.body)
})
})
// This can be used to execute a group of commands within an iframe
// - This will verify that the iframe is loaded to any page other than 'about:blank'
// cy.enter().then(getBody => {
// getBody().find('.some-button').should('be.visible').click()
// getBody().contains('Some hidden element').should('not.be.visible')
// })
// - You can also give it a selector to find elements inside of a specific iframe
// cy.enter('#my-iframe').then(getBody => {
// getBody().find('.some-button').should('be.visible').click()
// getBody().contains('Some hidden element').should('not.be.visible')
// })
Cypress.Commands.add('enter', (selector, opts) => {
if (selector === undefined) {
selector = DEFAULT_IFRAME_SELECTOR
} else if (typeof selector === 'object') {
opts = selector
selector = DEFAULT_IFRAME_SELECTOR
}
const fullOpts = {
...DEFAULT_OPTS,
...opts,
}
const log = fullOpts.log
? Cypress.log({
name: 'enter',
displayName: 'enter',
message: [selector],
}).snapshot()
: null
return cy.iframe(selector, { ...fullOpts, log: false }).then(($body) => {
log?.set('$el', $body).end()
return () => cy.wrap($body, { log: false })
})
})
Cypress.Commands.add('setupInterceptors', () => {
cy.intercept('*', (req) => {
req.headers['Origin'] = 'http://localhost:8080'
console.log('Intercepted request with headers:', req.headers)
req.continue()
}).as('headers')
})
const CHAIN_PREFIX_TO_ID = {
eth: '1',
gor: '5',
gno: '100',
matic: '137',
sep: '11155111',
}
function autoTrustSafeFromUrl(url) {
const match = url.match(/safe=([a-z]+):(0x[a-fA-F0-9]{40})/)
if (!match) return
const [, prefix, address] = match
const chainId = CHAIN_PREFIX_TO_ID[prefix]
if (!chainId) return
const key = 'SAFE_v2__addedSafes'
const existing = localStorage.getItem(key)
const addedSafes = existing ? JSON.parse(existing) : {}
if (!addedSafes[chainId]) addedSafes[chainId] = {}
if (!addedSafes[chainId][address]) {
addedSafes[chainId][address] = { owners: [], threshold: 1, ethBalance: '0' }
}
localStorage.setItem(key, JSON.stringify(addedSafes))
}
Cypress.Commands.overwrite('visit', (originalFn, url, options = {}) => {
const maxRetries = 3
let attempt = 0
if (options.skipAutoTrust !== true) {
autoTrustSafeFromUrl(url)
}
function attemptVisit(resolve, reject) {
originalFn(url, options)
.then((response) => {
if (response && response.status === 429) {
if (attempt < maxRetries) {
attempt++
const waitTime = 6000
console.warn(
`Rate limit (429) detected! Retrying in ${waitTime / 1000} seconds... Attempt ${attempt}/${maxRetries}`,
)
cy.wait(waitTime).then(() => attemptVisit(resolve, reject))
} else {
reject(new Error(`cy.visit failed after ${maxRetries + 1} attempts due to 429 rate limit.`))
}
} else {
resolve(response)
}
})
.catch(reject)
}
return new Cypress.Promise((resolve, reject) => {
attemptVisit(resolve, reject)
})
})