diff --git a/.env.custom b/.env.custom new file mode 100644 index 0000000000..b35cf91924 --- /dev/null +++ b/.env.custom @@ -0,0 +1 @@ +APPLICATION_VERSION=1.36.4 \ No newline at end of file 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..a56a220e14 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' }} + if: startsWith(github.ref, 'refs/heads/main') - 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/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 66ca16f382..84a0e6ba61 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -10,9 +10,12 @@ jobs: id-token: write runs-on: ubuntu-latest + name: Deploy release + env: ARCHIVE_NAME: ${{ github.event.repository.name }}-${{ github.event.release.tag_name }} + steps: - uses: actions/checkout@v4 @@ -36,36 +39,16 @@ jobs: aws-region: ${{ secrets.AWS_REGION }} # Script to upload release files - - name: 'Upload release build files for production' + - name: Upload release build files for production env: BUCKET: s3://${{ secrets.AWS_STAGING_BUCKET_NAME }}/releases/${{ github.event.release.tag_name }} CHECKSUM_FILE: ${{ env.ARCHIVE_NAME }}-sha256-checksum.txt run: bash ./scripts/github/s3_upload.sh # Script to prepare production deployments - - run: bash ./scripts/github/prepare_production_deployment.sh + - name: Prepare deployment + run: bash ./scripts/github/prepare_production_deployment.sh env: PROD_DEPLOYMENT_HOOK_TOKEN: ${{ secrets.PROD_DEPLOYMENT_HOOK_TOKEN }} PROD_DEPLOYMENT_HOOK_URL: ${{ secrets.PROD_DEPLOYMENT_HOOK_URL }} VERSION_TAG: ${{ github.event.release.tag_name }} - - # Update the GitHub release with a checksummed archive - - name: Upload archive - uses: actions/upload-release-asset@v1 - with: - upload_url: ${{ github.event.release.upload_url }} - asset_path: ${{ env.ARCHIVE_NAME }}.tar.gz - asset_name: ${{ env.ARCHIVE_NAME }}.tar.gz - asset_content_type: application/gzip - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: Upload checksum - uses: actions/upload-release-asset@v1 - with: - upload_url: ${{ github.event.release.upload_url }} - asset_path: ${{ env.ARCHIVE_NAME }}-sha256-checksum.txt - asset_name: ${{ env.ARCHIVE_NAME }}-sha256-checksum.txt - asset_content_type: text/plain - env: - GITHUB_TOKEN: ${{ github.token }} diff --git a/README.md b/README.md index 6e426486de..9a2b592618 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,6 @@ Here's the list of all the environment variables: | `NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION` | FCM vapid key on production | `NEXT_PUBLIC_FIREBASE_OPTIONS_STAGING` | FCM `initializeApp` options on staging | `NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING` | FCM vapid key on staging -| `NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_PRODUCTION` | Web3Auth and Google credentials (production) -| `NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_STAGING` | Web3Auth and Google credentials (staging) | `NEXT_PUBLIC_SPINDL_SDK_KEY` | [Spindl](http://spindl.xyz) SDK key If you don't provide some of the variables, the corresponding features will be disabled in the UI. diff --git a/cypress.config.js b/cypress.config.js index a518d1421d..b03786f202 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -10,7 +10,7 @@ export default defineConfig({ mochaFile: 'reports/junit-[hash].xml', }, retries: { - runMode: 2, + runMode: 3, openMode: 0, }, e2e: { diff --git a/cypress/e2e/happypath/recovery_hp_1.cy.js b/cypress/e2e/happypath/recovery_hp_1.cy.js index 30ecce703a..bb036e11fc 100644 --- a/cypress/e2e/happypath/recovery_hp_1.cy.js +++ b/cypress/e2e/happypath/recovery_hp_1.cy.js @@ -4,9 +4,12 @@ import * as owner from '../pages/owners.pages' import * as recovery from '../pages/recovery.pages' import * as tx from '../pages/transactions.page' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let recoverySafes = [] -// +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY + describe('Recovery happy path tests 1', () => { before(async () => { recoverySafes = await getSafes(CATEGORIES.recovery) @@ -20,8 +23,8 @@ describe('Recovery happy path tests 1', () => { // Check that recovery can be setup and removed it('Recovery setup happy path 1', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() - cy.reload() recovery.clearRecoverers() recovery.clickOnSetupRecoveryBtn() recovery.clickOnSetupRecoveryModalBtn() diff --git a/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js b/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js index 330119f9f5..532b72ccea 100644 --- a/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js +++ b/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js @@ -12,12 +12,12 @@ import { createEthersAdapter, createSigners } from '../../support/api/utils_ethe import { createSafes } from '../../support/api/utils_protocolkit' import { contracts, abi_qtrust, abi_nft_pc2 } from '../../support/api/contracts' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' -const safeBalanceEth = 305220000000000000n -const qtrustBanance = 93000000000000000025n const transferAmount = '1' const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY const tokenAmount2 = '0.00001' const netwrok = 'sepolia' @@ -81,6 +81,7 @@ describe('Send funds with connected signer happy path tests', { defaultCommandTi function executeTransactionFlow(fromSafe, toSafe) { return cy.visit(constants.balanceNftsUrl + fromSafe).then(() => { + wallet.connectSigner(signer) nfts.selectNFTs(1) nfts.sendNFT() nfts.typeRecipientAddress(toSafe) @@ -112,6 +113,7 @@ describe('Send funds with connected signer happy path tests', { defaultCommandTi const targetSafe = safesData.SEP_FUNDS_SAFE_12.substring(4) function executeTransactionFlow(fromSafe, toSafe, tokenAmount) { visit(constants.BALANCE_URL + fromSafe) + wallet.connectSigner(signer) assets.clickOnSendBtn(0) loadsafe.inputOwnerAddress(0, toSafe) assets.checkSelectedToken(constants.tokenAbbreviation.sep) @@ -155,7 +157,6 @@ describe('Send funds with connected signer happy path tests', { defaultCommandTi const safeTx = await apiKit.getTransaction(safeTxHashofExistingTx) await protocolKitOwner2_S3.executeTransaction(safeTx) main.verifyNonceChange(network_pref + targetSafe, currentNonce + 1) - main.checkTokenBalance(network_pref + targetSafe, constants.tokenAbbreviation.eth, safeBalanceEth) }) }) @@ -166,6 +167,7 @@ describe('Send funds with connected signer happy path tests', { defaultCommandTi function executeTransactionFlow(fromSafe, toSafe) { visit(constants.BALANCE_URL + fromSafe) + wallet.connectSigner(signer) assets.selectTokenList(assets.tokenListOptions.allTokens) assets.clickOnSendBtn(1) loadsafe.inputOwnerAddress(0, toSafe) @@ -188,7 +190,6 @@ describe('Send funds with connected signer happy path tests', { defaultCommandTi await tx.wait() main.verifyNonceChange(network_pref + originatingSafe, currentNonce + 1) - main.checkTokenBalance(network_pref + originatingSafe, constants.tokenAbbreviation.qtrust, qtrustBanance) }) }) }) diff --git a/cypress/e2e/happypath/sendfunds_queue_1.cy.js b/cypress/e2e/happypath/sendfunds_queue_1.cy.js index dbde035b6f..f0a6d56242 100644 --- a/cypress/e2e/happypath/sendfunds_queue_1.cy.js +++ b/cypress/e2e/happypath/sendfunds_queue_1.cy.js @@ -7,9 +7,11 @@ import SafeApiKit from '@safe-global/api-kit' import { createEthersAdapter, createSigners } from '../../support/api/utils_ether' import { createSafes } from '../../support/api/utils_protocolkit' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) const receiver = walletCredentials.OWNER_2_WALLET_ADDRESS +const signer = walletCredentials.OWNER_4_PRIVATE_KEY const tokenAmount = '0.0001' const netwrok = 'sepolia' @@ -46,6 +48,7 @@ function visit(url) { function executeTransactionFlow(fromSafe) { visit(constants.transactionQueueUrl + fromSafe) + wallet.connectSigner(signer) assets.clickOnConfirmBtn(0) tx.executeFlow_1() cy.wait(5000) @@ -78,41 +81,46 @@ describe('Send funds from queue happy path tests 1', () => { protocolKitOwner2_S3 = safes[3] }) - it('Verify confirmation and execution of native token queued tx by second signer with connected wallet', () => { - cy.wrap(null) - .then(() => { - return main.fetchCurrentNonce(network_pref + existingSafeAddress1) - }) - .then(async (currentNonce) => { - const amount = ethers.parseUnits(tokenAmount, unit_eth).toString() - const safeTransactionData = { - to: receiver, - data: '0x', - value: amount.toString(), - } - - const safeTransaction = await protocolKitOwnerS1.createTransaction({ transactions: [safeTransactionData] }) - const safeTxHash = await protocolKitOwnerS1.getTransactionHash(safeTransaction) - const senderSignature = await protocolKitOwnerS1.signHash(safeTxHash) - const safeAddress = existingSafeAddress1 - - await apiKit.proposeTransaction({ - safeAddress, - safeTransactionData: safeTransaction.data, - safeTxHash, - senderAddress: await owner1Signer.getAddress(), - senderSignature: senderSignature.data, + it( + 'Verify confirmation and execution of native token queued tx by second signer with connected wallet', + { defaultCommandTimeout: 300000 }, + () => { + cy.wrap(null) + .then(() => { + return main.fetchCurrentNonce(network_pref + existingSafeAddress1) }) - - executeTransactionFlow(safeAddress) - cy.wait(5000) - main.verifyNonceChange(network_pref + safeAddress, currentNonce + 1) - }) - }) + .then(async (currentNonce) => { + const amount = ethers.parseUnits(tokenAmount, unit_eth).toString() + const safeTransactionData = { + to: receiver, + data: '0x', + value: amount.toString(), + } + + const safeTransaction = await protocolKitOwnerS1.createTransaction({ transactions: [safeTransactionData] }) + const safeTxHash = await protocolKitOwnerS1.getTransactionHash(safeTransaction) + const senderSignature = await protocolKitOwnerS1.signHash(safeTxHash) + const safeAddress = existingSafeAddress1 + + await apiKit.proposeTransaction({ + safeAddress, + safeTransactionData: safeTransaction.data, + safeTxHash, + senderAddress: await owner1Signer.getAddress(), + senderSignature: senderSignature.data, + }) + + executeTransactionFlow(safeAddress) + cy.wait(5000) + main.verifyNonceChange(network_pref + safeAddress, currentNonce + 1) + }) + }, + ) it.skip('Verify confirmation and execution of native token queued tx by second signer with relayer', () => { function executeTransactionFlow(fromSafe) { visit(constants.transactionQueueUrl + fromSafe) + wallet.connectSigner(signer) assets.clickOnConfirmBtn(0) tx.executeFlow_2() cy.wait(5000) @@ -148,9 +156,10 @@ describe('Send funds from queue happy path tests 1', () => { }) }) - it('Verify 1 signer can execute a tx confirmed by 2 signers', () => { + it('Verify 1 signer can execute a tx confirmed by 2 signers', { defaultCommandTimeout: 300000 }, () => { function executeTransaction(fromSafe) { visit(constants.transactionQueueUrl + fromSafe) + wallet.connectSigner(signer) assets.clickOnExecuteBtn(0) tx.executeFlow_3() cy.wait(5000) diff --git a/cypress/e2e/happypath/sendfunds_relay.cy.js b/cypress/e2e/happypath/sendfunds_relay.cy.js index ae0c129919..1f4a65bb51 100644 --- a/cypress/e2e/happypath/sendfunds_relay.cy.js +++ b/cypress/e2e/happypath/sendfunds_relay.cy.js @@ -12,12 +12,12 @@ import { createEthersAdapter, createSigners } from '../../support/api/utils_ethe import { createSafes } from '../../support/api/utils_protocolkit' import { contracts, abi_qtrust, abi_nft_pc2 } from '../../support/api/contracts' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' -const safeBalanceEth = 405210000000000000n -const qtrustBanance = 59000000000000000000n const transferAmount = '1' const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY const tokenAmount2 = '0.00001' const netwrok = 'sepolia' @@ -48,7 +48,8 @@ function visit(url) { cy.visit(url) } -describe('Send funds with relay happy path tests', { defaultCommandTimeout: 60000 }, () => { +// TODO: Relay only allows 5 txs per hour. +describe('Send funds with relay happy path tests', { defaultCommandTimeout: 300000 }, () => { before(async () => { safesData = await getSafes(CATEGORIES.funds) main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__cookies, ls.cookies.acceptedCookies) @@ -74,11 +75,12 @@ describe('Send funds with relay happy path tests', { defaultCommandTimeout: 6000 protocolKitOwner2_S3 = safes[1] }) - it.skip('Verify tx creation and execution of NFT with relay', () => { + it('Verify tx creation and execution of NFT with relay', () => { cy.wait(2000) const originatingSafe = safesData.SEP_FUNDS_SAFE_9.substring(4) function executeTransactionFlow(fromSafe, toSafe) { return cy.visit(constants.balanceNftsUrl + fromSafe).then(() => { + wallet.connectSigner(signer) nfts.selectNFTs(1) nfts.sendNFT() nfts.typeRecipientAddress(toSafe) @@ -93,24 +95,29 @@ describe('Send funds with relay happy path tests', { defaultCommandTimeout: 6000 return main.fetchCurrentNonce(network_pref + originatingSafe) }) .then(async (currentNonce) => { - executeTransactionFlow(originatingSafe, walletAddress.toString(), transferAmount).then(async () => { - main.checkTokenBalanceIsNull(network_pref + originatingSafe, constants.tokenAbbreviation.tpcc) - const contractWithWallet = nftContract.connect(owner1Signer) - const tx = await contractWithWallet.safeTransferFrom(walletAddress.toString(), originatingSafe, 2, { - gasLimit: 200000, + return main.getRelayRemainingAttempts(originatingSafe).then((remainingAttempts) => { + if (remainingAttempts < 1) { + throw new Error(main.noRelayAttemptsError) + } + executeTransactionFlow(originatingSafe, walletAddress.toString(), transferAmount).then(async () => { + main.checkTokenBalanceIsNull(network_pref + originatingSafe, constants.tokenAbbreviation.tpcc) + const contractWithWallet = nftContract.connect(owner1Signer) + const tx = await contractWithWallet.safeTransferFrom(walletAddress.toString(), originatingSafe, 2, { + gasLimit: 200000, + }) + await tx.wait() + main.verifyNonceChange(network_pref + originatingSafe, currentNonce + 1) }) - - await tx.wait() - main.verifyNonceChange(network_pref + originatingSafe, currentNonce + 1) }) }) }) - it.skip('Verify tx creation and execution of native token with relay', () => { + it('Verify tx creation and execution of native token with relay', () => { cy.wait(2000) const targetSafe = safesData.SEP_FUNDS_SAFE_1.substring(4) function executeTransactionFlow(fromSafe, toSafe, tokenAmount) { visit(constants.BALANCE_URL + fromSafe) + wallet.connectSigner(signer) assets.clickOnSendBtn(0) loadsafe.inputOwnerAddress(0, toSafe) assets.checkSelectedToken(constants.tokenAbbreviation.sep) @@ -124,47 +131,52 @@ describe('Send funds with relay happy path tests', { defaultCommandTimeout: 6000 return main.fetchCurrentNonce(network_pref + targetSafe) }) .then(async (currentNonce) => { - executeTransactionFlow(targetSafe, walletAddress.toString(), tokenAmount2) - const amount = ethers.parseUnits(tokenAmount2, unit_eth).toString() - const safeTransactionData = { - to: targetSafe, - data: '0x', - value: amount.toString(), - } - - const safeTransaction = await protocolKitOwner1_S3.createTransaction({ transactions: [safeTransactionData] }) - const safeTxHash = await protocolKitOwner1_S3.getTransactionHash(safeTransaction) - const senderSignature = await protocolKitOwner1_S3.signHash(safeTxHash) - const safeAddress = outgoingSafeAddress - - await apiKit.proposeTransaction({ - safeAddress, - safeTransactionData: safeTransaction.data, - safeTxHash, - senderAddress: await owner1Signer.getAddress(), - senderSignature: senderSignature.data, - }) + return main.getRelayRemainingAttempts(targetSafe).then(async (remainingAttempts) => { + if (remainingAttempts < 1) { + throw new Error(main.noRelayAttemptsError) + } + executeTransactionFlow(targetSafe, walletAddress.toString(), tokenAmount2) + const amount = ethers.parseUnits(tokenAmount2, unit_eth).toString() + const safeTransactionData = { + to: targetSafe, + data: '0x', + value: amount.toString(), + } + + const safeTransaction = await protocolKitOwner1_S3.createTransaction({ transactions: [safeTransactionData] }) + const safeTxHash = await protocolKitOwner1_S3.getTransactionHash(safeTransaction) + const senderSignature = await protocolKitOwner1_S3.signHash(safeTxHash) + const safeAddress = outgoingSafeAddress + + await apiKit.proposeTransaction({ + safeAddress, + safeTransactionData: safeTransaction.data, + safeTxHash, + senderAddress: await owner1Signer.getAddress(), + senderSignature: senderSignature.data, + }) - const pendingTransactions = await apiKit.getPendingTransactions(safeAddress) - const safeTxHashofExistingTx = pendingTransactions.results[0].safeTxHash + const pendingTransactions = await apiKit.getPendingTransactions(safeAddress) + const safeTxHashofExistingTx = pendingTransactions.results[0].safeTxHash - const signature = await protocolKitOwner2_S3.signHash(safeTxHashofExistingTx) - await apiKit.confirmTransaction(safeTxHashofExistingTx, signature.data) + const signature = await protocolKitOwner2_S3.signHash(safeTxHashofExistingTx) + await apiKit.confirmTransaction(safeTxHashofExistingTx, signature.data) - const safeTx = await apiKit.getTransaction(safeTxHashofExistingTx) - await protocolKitOwner2_S3.executeTransaction(safeTx) - main.verifyNonceChange(network_pref + targetSafe, currentNonce + 1) - main.checkTokenBalance(network_pref + targetSafe, constants.tokenAbbreviation.eth, safeBalanceEth) + const safeTx = await apiKit.getTransaction(safeTxHashofExistingTx) + await protocolKitOwner2_S3.executeTransaction(safeTx) + main.verifyNonceChange(network_pref + targetSafe, currentNonce + 1) + }) }) }) - it.skip('Verify tx creation and execution of non-native token with with relay', () => { + it('Verify tx creation and execution of non-native token with with relay', () => { cy.wait(2000) const originatingSafe = safesData.SEP_FUNDS_SAFE_2.substring(4) const amount = ethers.parseUnits(transferAmount, unit_eth).toString() function executeTransactionFlow(fromSafe, toSafe) { visit(constants.BALANCE_URL + fromSafe) + wallet.connectSigner(signer) assets.selectTokenList(assets.tokenListOptions.allTokens) assets.clickOnSendBtn(1) @@ -180,16 +192,20 @@ describe('Send funds with relay happy path tests', { defaultCommandTimeout: 6000 return main.fetchCurrentNonce(network_pref + originatingSafe) }) .then(async (currentNonce) => { - executeTransactionFlow(originatingSafe, walletAddress.toString(), transferAmount) + return main.getRelayRemainingAttempts(originatingSafe).then(async (remainingAttempts) => { + if (remainingAttempts < 1) { + throw new Error(main.noRelayAttemptsError) + } + executeTransactionFlow(originatingSafe, walletAddress.toString(), transferAmount) + + const contractWithWallet = tokenContract.connect(signers[0]) + const tx = await contractWithWallet.transfer(originatingSafe, amount, { + gasLimit: 200000, + }) - const contractWithWallet = tokenContract.connect(signers[0]) - const tx = await contractWithWallet.transfer(originatingSafe, amount, { - gasLimit: 200000, + await tx.wait() + main.verifyNonceChange(network_pref + originatingSafe, currentNonce + 1) }) - - await tx.wait() - main.verifyNonceChange(network_pref + originatingSafe, currentNonce + 1) - main.checkTokenBalance(network_pref + originatingSafe, constants.tokenAbbreviation.qtrust, qtrustBanance) }) }) }) 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..879878030e --- /dev/null +++ b/cypress/e2e/happypath/tx_history_filter_hp_1.cy.js @@ -0,0 +1,145 @@ +/* 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' + +// TODO: Flaky tests, skiped until solved +describe.skip('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', + { defaultCommandTimeout: 60000 }, + () => { + 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', + { defaultCommandTimeout: 60000 }, + () => { + 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/assets.pages.js b/cypress/e2e/pages/assets.pages.js index 421ac8854d..b92af57eb5 100644 --- a/cypress/e2e/pages/assets.pages.js +++ b/cypress/e2e/pages/assets.pages.js @@ -1,6 +1,7 @@ import * as main from './main.page' import * as addressbook from '../pages/address_book.page' import * as createTx from '../pages/create_tx.pages' +import { tableRow } from '../pages/address_book.page' let etherscanLinkSepolia = 'a[aria-label="View on sepolia.etherscan.io"]' export const balanceSingleRow = '[aria-labelledby="tableTitle"] > tbody tr' @@ -43,14 +44,16 @@ const pageCountString1to25 = '1–25 of' const pageCountString1to10 = '1–10 of' const pageCountString10to20 = '11–20 of' -export const fiatRegex = new RegExp(`([0-9]{1,3},)*[0-9]{1,3}.[0-9]{2}`) +export const fiatRegex = new RegExp(`\\$?(([0-9]{1,3},)*[0-9]{1,3}(\\.[0-9]{2})?|0)`) export const tokenListOptions = { allTokens: 'span[data-track="assets: Show all tokens"]', default: 'span[data-track="assets: Show default tokens"]', } -export const currencyEUR = 'EUR' -export const currencyUSD = 'USD' +export const currencyEUR = '€' +export const currencyOptionEUR = 'EUR' +export const currency$ = '$' +export const currencyCAD = 'CAD' export const currentcySepoliaFormat = '0.09996 ETH' @@ -125,7 +128,7 @@ export function changeCurrency(currency) { } export function clickOnSendBtn(index) { - cy.wait(2000) + cy.wait(4000) cy.get(addressbook.tableRow) .eq(index) .within(() => { @@ -192,10 +195,12 @@ export function clickOnTokenBalanceSortBtn() { export function verifyTokenNamesOrder(option = 'ascending') { const tokens = [] - main.getTextToArray('tr p', tokens) + main.getTextToArray(tableRow, tokens) cy.wrap(tokens).then((arr) => { + cy.log('*** Original array ' + tokens) let sortedNames = [...arr].sort() + cy.log('*** Sorted array ' + sortedNames) if (option == 'descending') sortedNames = [...arr].sort().reverse() expect(arr).to.deep.equal(sortedNames) }) @@ -259,9 +264,9 @@ export function verifyTokenIsPresent(token) { export function selectTokenList(option) { cy.get(tokenListDropdown) - .click() + .click({ force: true }) .then(() => { - cy.get(option).click() + cy.get(option).click({ force: true }) }) } diff --git a/cypress/e2e/pages/create_tx.pages.js b/cypress/e2e/pages/create_tx.pages.js index 553c77861c..2219597165 100644 --- a/cypress/e2e/pages/create_tx.pages.js +++ b/cypress/e2e/pages/create_tx.pages.js @@ -1,5 +1,6 @@ import * as constants from '../../support/constants' import * as main from '../pages/main.page' +import * as wallet from '../pages/create_wallet.pages' export const delegateCallWarning = '[data-testid="delegate-call-warning"]' export const policyChangeWarning = '[data-testid="threshold-warning"]' @@ -31,7 +32,18 @@ const spamTokenWarningIcon = '[data-testid="warning"]' const untrustedTokenWarningModal = '[data-testid="untrusted-token-warning"]' const sendTokensBtn = '[data-testid="send-tokens-btn"]' export const replacementNewSigner = '[data-testid="new-owner"]' -const messageItem = '[data-testid="message-item"]' +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 rejectTxBtn = '[data-testid="reject-btn"]' +const deleteTxModalBtn = '[data-testid="delete-tx-btn"]' const viewTransactionBtn = 'View transaction' const transactionDetailsTitle = 'Transaction details' @@ -52,6 +64,92 @@ 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` +const deleteFromQueueStr = 'Delete from the queue' + +export const filterTypes = { + incoming: 'Incoming', + outgoing: 'Outgoing', + module: 'Module-based', +} + +function clickOnRejectBtn() { + cy.get(rejectTxBtn).click() +} + +export function deleteTx() { + clickOnRejectBtn() + cy.get(wallet.choiceBtn).contains(deleteFromQueueStr).click() + cy.get(deleteTxModalBtn).click() +} + +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 checkApplyBtnEnabled() { + cy.get(filterApplyBtn).should('not.be', 'disabled') +} + +export function clickOnClearBtn() { + cy.get(filterClearBtn).click() +} + +export function fillFilterForm({ address, startDate, endDate, amount, token, nonce, recipient } = {}) { + checkApplyBtnEnabled() + cy.wait(2000) + 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.then(($el) => { + cy.wrap($el).invoke('removeAttr', 'readonly').clear().type(value, { force: true }) + }) + } + }) +} + +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/create_wallet.pages.js b/cypress/e2e/pages/create_wallet.pages.js index ba859cbb02..813ba6d614 100644 --- a/cypress/e2e/pages/create_wallet.pages.js +++ b/cypress/e2e/pages/create_wallet.pages.js @@ -1,4 +1,3 @@ -import * as constants from '../../support/constants' import * as main from '../pages/main.page' import { connectedWalletExecMethod } from '../pages/create_tx.pages' import * as sidebar from '../pages/sidebar.pages' @@ -12,8 +11,7 @@ const thresholdInput = 'input[name="threshold"]' export const removeOwnerBtn = 'button[aria-label="Remove signer"]' const connectingContainer = 'div[class*="connecting-container"]' const createNewSafeBtn = '[data-testid="create-safe-btn"]' -const connectWalletBtn = 'Connect wallet' -const continueWithWalletBtn = 'Continue with E2E Wallet' +const continueWithWalletBtn = 'Continue with Private key' const googleConnectBtn = '[data-testid="google-connect-btn"]' const googleSignedinBtn = '[data-testid="signed-in-account-btn"]' export const accountInfoHeader = '[data-testid="open-account-center"]' @@ -26,9 +24,18 @@ const networkFeeSection = '[data-tetid="network-fee-section"]' const nextBtn = '[data-testid="next-btn"]' const backBtn = '[data-testid="back-btn"]' const cancelBtn = '[data-testid="cancel-btn"]' -const dialogConfirmBtn = '[data-testid="dialog-confirm-btn"]' const safeActivationSection = '[data-testid="activation-section"]' const addressAutocompleteOptions = '[data-testid="address-item"]' +export const qrCode = '[data-testid="qr-code"]' +export const addressInfo = '[data-testid="address-info"]' +export const choiceBtn = '[data-testid="choice-btn"]' +const addFundsBtn = '[data-testid="add-funds-btn"]' +const createTxBtn = '[data-testid="create-tx-btn"]' +const qrCodeSwitch = '[data-testid="qr-code-switch"]' +export const activateAccountBtn = '[data-testid="activate-account-btn"]' +const notificationsSwitch = '[data-testid="notifications-switch"]' +export const addFundsSection = '[data-testid="add-funds-section"]' +export const noTokensAlert = '[data-testid="no-tokens-alert"]' const sponsorStr = 'Your account is sponsored by Goerli' const safeCreationProcessing = 'Transaction is being executed' @@ -38,18 +45,51 @@ const policy1_2 = '1/1 policy' export const walletName = 'test1-sepolia-safe' export const defaultSepoliaPlaceholder = 'Sepolia Safe' const welcomeToSafeStr = 'Welcome to Safe' +const initialSteps = '0 of 2 steps completed' +export const addSignerStr = 'Add signer' +export const accountRecoveryStr = 'Account recovery' +export const sendTokensStr = 'Send tokens' -export function verifyNewSafeDialogModal() { - main.verifyElementsIsVisible([dialogConfirmBtn]) +const connectWalletBtn = '[data-testid="connect-wallet-btn"]' +export function checkNotificationsSwitchIs(status) { + cy.get(notificationsSwitch).find('input').should(`be.${status}`) } -// -export function verifyCFSafeCreated() { - main.verifyElementsIsVisible([sidebar.pendingActivationIcon, safeActivationSection]) + +export function clickOnActivateAccountBtn() { + cy.get(activateAccountBtn).click() +} + +export function clickOnQRCodeSwitch() { + cy.get(qrCodeSwitch).click() +} + +export function checkQRCodeSwitchStatus(state) { + cy.get(qrCodeSwitch).find('input').should(state) +} + +export function checkInitialStepsDisplayed() { + cy.contains(initialSteps).should('be.visible') +} + +export function clickOnAddFundsBtn() { + cy.get(addFundsBtn).click() +} + +export function clickOnCreateTxBtn() { + cy.get(createTxBtn).click() + main.verifyElementsCount(choiceBtn, 6) +} + +export function checkAllTxTypesOrder(expectedOrder) { + main.checkTextOrder(choiceBtn, expectedOrder) } -export function clickOnGotitBtn() { - cy.get(dialogConfirmBtn).click() - main.verifyElementsCount(connectedWalletExecMethod, 0) +export function clickOnTxType(tx) { + cy.get(choiceBtn).contains(tx).click() +} + +export function verifyCFSafeCreated() { + main.verifyElementsIsVisible([sidebar.pendingActivationIcon, safeActivationSection]) } export function selectPayLaterOption() { @@ -87,22 +127,6 @@ export function verifySponsorMessageIsPresent() { cy.get(networkFeeSection).contains(sponsorStr).should('exist') } -export function verifyGoogleConnectBtnIsDisabled() { - cy.get(googleConnectBtn).should('be.disabled') -} - -export function verifyGoogleConnectBtnIsEnabled() { - cy.get(googleConnectBtn).should('not.be.disabled') -} - -export function verifyGoogleSignin() { - return cy.get(googleSignedinBtn).should('exist') -} - -export function verifyGoogleAccountInfoInHeader() { - return cy.get(accountInfoHeader).should('exist') -} - export function verifyPolicy1_1() { cy.contains(policy1_2).should('exist') // TOD: Need data-cy for containers @@ -124,15 +148,6 @@ export function checkNetworkChangeWarningMsg() { cy.get('div').contains(changeNetworkWarningStr).should('exist') } -export function connectWallet() { - cy.get('onboard-v2') - .shadow() - .within(($modal) => { - cy.wrap($modal).contains('div', constants.connectWalletNames.e2e).click() - cy.wrap($modal).get(connectingContainer).should('exist') - }) -} - export function clickOnCreateNewSafeBtn() { cy.get(createNewSafeBtn).click().wait(1000) } @@ -143,7 +158,7 @@ export function clickOnContinueWithWalletBtn() { export function clickOnConnectWalletBtn() { cy.get(welcomeLoginScreen).within(() => { - cy.get('button').contains(connectWalletBtn).should('be.visible').should('be.enabled').click().wait(1000) + cy.get(connectWalletBtn).should('be.visible').should('be.enabled').click().wait(1000) }) } @@ -157,7 +172,7 @@ export function clearWalletName() { export function selectNetwork(network) { cy.wait(1000) - cy.get(expandMoreIcon).eq(1).parents('div').eq(1).click() + cy.get(expandMoreIcon).parents('div').eq(1).click() cy.wait(1000) let regex = new RegExp(`^${network}$`) cy.get('li').contains(regex).click() diff --git a/cypress/e2e/pages/dashboard.pages.js b/cypress/e2e/pages/dashboard.pages.js index 6dc024f54e..746106e154 100644 --- a/cypress/e2e/pages/dashboard.pages.js +++ b/cypress/e2e/pages/dashboard.pages.js @@ -20,7 +20,7 @@ const txBuilder = 'a[href*="tx-builder"]' const safeSpecificLink = 'a[href*="&appUrl=http"]' const copyShareBtn = '[data-testid="copy-btn-icon"]' const exploreAppsBtn = '[data-testid="explore-apps-btn"]' -const viewAllLink = '[data-testid="view-all-link"]' +const viewAllLink = '[data-testid="view-all-link"][href^="/transactions/queue"]' const noTxIcon = '[data-testid="no-tx-icon"]' const noTxText = '[data-testid="no-tx-text"]' const pendingTxWidget = '[data-testid="pending-tx-widget"]' diff --git a/cypress/e2e/pages/main.page.js b/cypress/e2e/pages/main.page.js index 9494cb9a8c..5643720ef9 100644 --- a/cypress/e2e/pages/main.page.js +++ b/cypress/e2e/pages/main.page.js @@ -4,6 +4,7 @@ const acceptSelection = 'Save settings' const executeStr = 'Execute' const connectedOwnerBlock = '[data-testid="open-account-center"]' export const modalDialogCloseBtn = '[data-testid="modal-dialog-close-btn"]' +export const noRelayAttemptsError = 'Not enough relay attempts remaining' export function checkElementBackgroundColor(element, color) { cy.get(element).should('have.css', 'background-color', color) @@ -84,6 +85,23 @@ export function fetchCurrentNonce(safeAddress) { ) } +export const getRelayRemainingAttempts = (safeAddress) => { + const chain = constants.networkKeys.sepolia + + return cy + .request({ + method: 'GET', + url: `${constants.stagingCGWUrlv1}${constants.stagingCGWChains}${chain}${constants.relayPath}${safeAddress}`, + headers: { + accept: 'application/json', + }, + }) + .then((response) => { + console.log('Remaining relay attempts: ', response.body.remaining) + return response.body.remaining + }) +} + export function verifyNonceChange(safeAddress, expectedNonce) { fetchCurrentNonce(safeAddress).then((newNonce) => { expect(newNonce).to.equal(expectedNonce) @@ -98,6 +116,13 @@ export function checkTokenBalance(safeAddress, tokenSymbol, expectedBalance) { }) } +export function getTokenBalance(safeAddress, tokenSymbol) { + getSafeBalance(safeAddress.substring(4), constants.networkKeys.sepolia).then((response) => { + const targetToken = response.body.items.find((token) => token.tokenInfo.symbol === tokenSymbol) + console.log('**** TOKEN BALANCE', targetToken.balance) + }) +} + export function checkNFTBalance(safeAddress, tokenSymbol, expectedBalance) { getSafeNFTs(safeAddress.substring(4), constants.networkKeys.sepolia).then((response) => { const targetToken = response.body.results.find((token) => token.tokenSymbol === tokenSymbol) @@ -273,7 +298,7 @@ export function addToLocalStorage(key, jsonValue) { export function checkTextOrder(selector, expectedTextArray) { cy.get(selector).each((element, index) => { const text = Cypress.$(element).text().trim() - expect(text).to.eq(expectedTextArray[index]) + expect(text).to.include(expectedTextArray[index]) }) } @@ -300,3 +325,11 @@ export function verifyTextVisibility(stringsArray) { cy.contains(string).should('be.visible') }) } + +export function getIframeBody(iframe) { + return cy.get(iframe).its('0.contentDocument.body').should('not.be.empty').then(cy.wrap) +} + +export const checkButtonByTextExists = (buttonText) => { + cy.get('button').contains(buttonText).should('exist') +} diff --git a/cypress/e2e/pages/messages.pages.js b/cypress/e2e/pages/messages.pages.js new file mode 100644 index 0000000000..bc6052b32d --- /dev/null +++ b/cypress/e2e/pages/messages.pages.js @@ -0,0 +1,14 @@ +import { messageItem } from './create_tx.pages' +const onchainMsgInput = 'input[placeholder*="Message"]' + +export function enterOnchainMessage(msg) { + cy.get(onchainMsgInput).type(msg) +} + +export function clickOnMessageSignBtn(index) { + cy.get(messageItem) + .eq(index) + .within(() => { + cy.get('button').contains('Sign').click() + }) +} diff --git a/cypress/e2e/pages/modals.page.js b/cypress/e2e/pages/modals.page.js index 1e8063405a..d2b99a68e2 100644 --- a/cypress/e2e/pages/modals.page.js +++ b/cypress/e2e/pages/modals.page.js @@ -5,6 +5,8 @@ export const modalTitiles = { editEntry: 'Edit entry', deleteEntry: 'Delete entry', dataImport: 'Data import', + confirmTx: 'Confirm transaction', + confirmMsg: 'Confirm message', } export function verifyModalTitle(title) { diff --git a/cypress/e2e/pages/modals/message_confirmation.pages.js b/cypress/e2e/pages/modals/message_confirmation.pages.js new file mode 100644 index 0000000000..99d19342f8 --- /dev/null +++ b/cypress/e2e/pages/modals/message_confirmation.pages.js @@ -0,0 +1,48 @@ +import * as modal from '../modals.page' +import * as checkers from '../../../support/utils/checkers' +import * as main from '../main.page' + +const messageHash = '[data-testid="message-hash"]' +const messageDetails = '[data-testid="message-details"]' +const messageInfobox = '[data-testid="message-infobox"]' + +const messageInfoBoxData = [ + 'Collect all the confirmations', + 'Confirmations (1 of 2)', + 'The signature will be submitted to the requesting app when the message is fully signed', +] + +export function verifyConfirmationWindowTitle(title) { + cy.get(modal.modalTitle).should('contain', title) +} + +export function verifyMessagePresent(msg) { + cy.get('textarea').should('contain', msg) +} + +export function verifySafeAppInPopupWindow(safeApp) { + cy.contains(safeApp) +} + +export function verifyOffchainMessageHash(index) { + cy.get(messageHash) + .eq(index) + .invoke('text') + .then((text) => { + if (!checkers.startsWith0x(text)) { + throw new Error(`Message at index ${index} does not start with '0x': ${text}`) + } + }) +} + +export function checkMessageInfobox() { + cy.get(messageInfobox) + .first() + .within(() => { + main.verifyTextVisibility(messageInfoBoxData) + }) +} + +export function clickOnMessageDetails() { + cy.get(messageDetails).click() +} diff --git a/cypress/e2e/pages/owners.pages.js b/cypress/e2e/pages/owners.pages.js index 9f1a38a52d..afde4caa6b 100644 --- a/cypress/e2e/pages/owners.pages.js +++ b/cypress/e2e/pages/owners.pages.js @@ -5,7 +5,7 @@ import * as navigation from '../pages/navigation.page' import * as addressBook from '../pages/address_book.page' const tooltipLabel = (label) => `span[aria-label="${label}"]` -const removeOwnerBtn = 'span[data-track="settings: Remove owner"] > span > button' +export const removeOwnerBtn = 'span[data-track="settings: Remove owner"] > span > button' const replaceOwnerBtn = 'span[data-track="settings: Replace owner"] > span > button' const tooltip = 'div[role="tooltip"]' const expandMoreIcon = 'svg[data-testid="ExpandMoreIcon"]' @@ -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() + cy.get('body').click(0, 0) } diff --git a/cypress/e2e/pages/safeapps.pages.js b/cypress/e2e/pages/safeapps.pages.js index b7d1238f5d..1810656cb1 100644 --- a/cypress/e2e/pages/safeapps.pages.js +++ b/cypress/e2e/pages/safeapps.pages.js @@ -9,6 +9,7 @@ export const downloadBatchBtn = 'button[title="Download batch"]' export const deleteBatchBtn = 'button[title="Delete Batch"]' const appModal = '[data-testid="app-info-modal"]' export const safeAppsList = '[data-testid="apps-list"]' +const openSafeAppBtn = '[data-testid="open-safe-app-btn"]' const addBtnStr = /add/i const noAppsStr = /no Safe Apps found/i @@ -89,6 +90,8 @@ export const transferStr = 'Transfer' export const successStr = 'Success' export const failedStr = 'Failed' +export const dummyTxStr = 'Trigger dummy tx (safe.txs.send)' +export const signOnchainMsgStr = 'Sign message (on-chain)' export const pinWalletConnectStr = /pin walletconnect/i export const transactionBuilderStr = 'Transaction Builder' export const testAddressValueStr = 'testAddressValue' @@ -126,6 +129,14 @@ export const permissionCheckboxNames = { fullscreen: 'Fullscreen', } +export function triggetOffChainTx() { + cy.contains(dummyTxStr).click() +} + +export function triggetOnChainTx() { + cy.contains(signOnchainMsgStr).click() +} + export function verifyWarningDefaultAppMsgIsDisplayed() { cy.get('p').contains(warningDefaultAppStr).should('be.visible') cy.wait(1000) @@ -145,6 +156,7 @@ export function verifyLinkName(name) { export function clickOnApp(app) { cy.contains(app).click() + cy.wait(2000) } export function verifyNoAppsTextPresent() { @@ -205,8 +217,11 @@ export function verifyAppDescription(descr) { } export function clickOnOpenSafeAppBtn() { - cy.findByRole('link', { name: openSafeAppBtnStr }).click() - cy.wait(500) + cy.get(openSafeAppBtn).click() + cy.wait(2000) +} + +export function verifyDisclaimerIsDisplayed() { verifyDisclaimerIsVisible() cy.wait(500) } diff --git a/cypress/e2e/pages/sidebar.pages.js b/cypress/e2e/pages/sidebar.pages.js index 882f872abe..854394159e 100644 --- a/cypress/e2e/pages/sidebar.pages.js +++ b/cypress/e2e/pages/sidebar.pages.js @@ -33,12 +33,31 @@ const queuedTxInfo = '[data-testid="queued-tx-info"]' const showMoreBtn = '[data-testid="show-more-btn" ]' const importBtn = '[data-testid="import-btn"]' export const pendingActivationIcon = '[data-testid="pending-activation-icon"]' +const safeItemMenuIcon = '[data-testid="MoreVertIcon"]' +export const importBtnStr = 'Import' +export const exportBtnStr = 'Export' export const addedSafesEth = ['0x8675...a19b'] export const addedSafesSepolia = ['0x6d0b...6dC1', '0x5912...fFdb', '0x0637...708e', '0xD157...DE9a'] export const sideBarListItems = ['Home', 'Assets', 'Transactions', 'Address book', 'Apps', 'Settings'] +export const sideBarSafes = { + safe1: '0xBb26E3717172d5000F87DeFd391994f789D80aEB', + safe2: '0x905934aA8758c06B2422F0C90D97d2fbb6677811', + safe1short: '0xBb26...0aEB', + safe2short: '0x9059...7811', + safe3short: '0x86Cb...2C27', +} +export const sideBarSafesPendingActions = { + safe1: '0x5912f6616c84024cD1aff0D5b55bb36F5180fFdb', + safe1short: '0x5912...fFdb', +} export const testSafeHeaderDetails = ['2/2', safes.SEP_STATIC_SAFE_9_SHORT] const receiveAssetsStr = 'Receive assets' +const emptyWatchListStr = 'Watch any Safe Account to keep an eye on its activity' +const emptySafeListStr = "You don't have any Safe Accounts yet" +const myAccountsStr = 'My accounts' +const confirmTxStr = (number) => `${number} to confirm` +export const confirmGenStr = 'to confirm' export function getImportBtn() { return cy.get(importBtn).scrollIntoView().should('be.visible') @@ -141,6 +160,11 @@ export function verifyAddedSafesExist(safes) { main.verifyValuesExist(sideSafeListItem, safes) } +export function verifyAddedSafesExistByIndex(index, safe) { + cy.get(sideSafeListItem).eq(index).should('contain', safe) + cy.get(sideSafeListItem).eq(index).should('contain', 'sep:') +} + export function verifySafesByNetwork(netwrok, safes) { cy.get(sidebarSafeContainer).within(() => { cy.get(chainLogo) @@ -186,7 +210,7 @@ export function renameSafeItem(oldName, newName) { clickOnRenameBtn() typeSafeName(newName) } -// + export function removeSafeItem(name) { clickOnSafeItemOptionsBtn(name) clickOnRemoveBtn() @@ -238,3 +262,34 @@ export function checkCurrencyInHeader(currency) { export function checkSafeAddressInHeader(address) { main.verifyValuesExist(sidebarSafeHeader, address) } + +export function verifyWatchlistIsEmpty() { + main.verifyValuesExist(sidebarSafeContainer, [emptyWatchListStr]) +} + +export function verifySafeListIsEmpty() { + main.verifyValuesExist(sidebarSafeContainer, [emptySafeListStr]) +} + +export function verifySafeGiveNameOptionExists(index) { + cy.get(safeItemMenuIcon).eq(index).click() + clickOnRenameBtn() +} + +export function checkMyAccountCounter(value) { + cy.contains(myAccountsStr).should('contain', value) +} + +export function checkTxToConfirm(numberOfTx) { + const str = confirmTxStr(numberOfTx) + main.verifyValuesExist(sideSafeListItem, [str]) +} + +export function verifyTxToConfirmDoesNotExist() { + main.verifyValuesDoNotExist(sideSafeListItem, [confirmGenStr]) +} + +export function checkBalanceExists() { + const balance = new RegExp(`\\s*\\d*\\.?\\d*\\s*`, 'i') + const element = cy.get(chainLogo).prev().contains(balance) +} diff --git a/cypress/e2e/pages/spending_limits.pages.js b/cypress/e2e/pages/spending_limits.pages.js index 8cf1c1aca8..d6178686b8 100644 --- a/cypress/e2e/pages/spending_limits.pages.js +++ b/cypress/e2e/pages/spending_limits.pages.js @@ -20,7 +20,8 @@ const reviewSpendingLimit = '[data-testid="spending-limit-label"]' const deleteBtn = '[data-testid="delete-btn"]' const resetTimeInfo = '[data-testid="reset-time"]' const spentAmountInfo = '[data-testid="spent-amount"]' -const spendingLimitTxOption = '[data-testid="spending-limit-tx"]' +export const spendingLimitTxOption = '[data-testid="spending-limit-tx"]' +export const standardTx = '[data-testid="standard-tx"]' const tokenBalance = '[data-testid="token-balance"]' const tokenItem = '[data-testid="token-item"]' const maxBtn = '[data-testid="max-btn"]' @@ -121,8 +122,8 @@ export function selectSpendingLimitOption() { main.checkRadioButtonState(input, constants.checkboxStates.checked) } -export function verifySpendingOptionExists() { - cy.get(spendingLimitTxOption).should('exist').and('be.visible') +export function verifyTxOptionExist(options) { + main.verifyElementsIsVisible(options) } export function verifySpendingOptionShowsBalance(balance) { diff --git a/cypress/e2e/pages/swaps.pages.js b/cypress/e2e/pages/swaps.pages.js new file mode 100644 index 0000000000..efb734acaf --- /dev/null +++ b/cypress/e2e/pages/swaps.pages.js @@ -0,0 +1,273 @@ +import * as constants from '../../support/constants.js' +import * as main from '../pages/main.page.js' +import * as create_tx from '../pages/create_tx.pages.js' + +export const inputCurrencyInput = '[id="input-currency-input"]' +export const outputurrencyInput = '[id="output-currency-input"]' +const tokenList = '[id="tokens-list"]' +export const swapBtn = '[id="swap-button"]' +const exceedFeesChkbox = 'input[id="fees-exceed-checkbox"]' +const settingsBtn = 'button[id="open-settings-dialog-button"]' +export const assetsSwapBtn = '[data-testid="swap-btn"]' +export const dashboardSwapBtn = '[data-testid="overview-swap-btn"]' +export const customRecipient = 'div[id="recipient"]' +const recipientToggle = 'button[id="toggle-recipient-mode-button"]' +const orderTypeMenuItem = 'div[class*="MenuItem"]' +const explorerBtn = '[data-testid="explorer-btn"]' +const limitPriceFld = '[data-testid="limit-price"]' +const expiryFld = '[data-testid="expiry"]' +const slippageFld = '[data-testid="slippage"]' +const orderIDFld = '[data-testid="order-id"]' +const widgetFeeFld = '[data-testid="widget-fee"]' +const interactWithFld = '[data-testid="interact-wth"]' +const recipientAlert = '[data-testid="recipient-alert"]' +const confirmSwapStr = 'Confirm Swap' + +const swapBtnStr = /Confirm Swap|Swap|Confirm (Approve COW and Swap)|Confirm/ +const orderSubmittedStr = 'Order Submitted' +const orderIdStr = 'Order ID' +const cowOrdersUrl = 'https://explorer.cow.fi/orders' + +export const blockedAddress = '0x8576acc5c05d6ce88f4e49bf65bdf0c62f91353c' +export const blockedAddressStr = 'Blocked address' + +const swapStr = 'Swap' +const limitStr = 'Limit' + +export const swapTokens = { + cow: 'COW', + dai: 'DAI', + eth: 'ETH', +} + +export const orderTypes = { + swap: 'Swap', + limit: 'Limit', +} + +const swapOrders = '**/api/v1/orders/*' +const surplus = '**/users/*/total_surplus' +const nativePrice = '**/native_price' +const quote = '**/quote/*' + +export const limitOrderSafe = 'sep:0x8f4A19C85b39032A37f7a6dCc65234f966F72551' + +export const swapTxs = { + sell1Action: + '&id=multisig_0x03042B890b99552b60A073F808100517fb148F60_0xd033466000a40227fba7a7deb1a668371c213fec90bac9f2583096be2e0fd959', + buy2actions: + '&id=multisig_0x03042B890b99552b60A073F808100517fb148F60_0x135ff0282653d4c2a62c76cd247764b1abd4c0daa9201a72964feac2acaa7b44', + sellCancelled: + '&id=multisig_0x2a73e61bd15b25B6958b4DA3bfc759ca4db249b9_0xbe159adaa7fb0f7e80ad4bab33a2bb341043818478c96916cfa3877303d22a3d', + sell3Actions: + '&id=multisig_0x140663Cb76e4c4e97621395fc118912fa674150B_0x9f3d2c9c9879fb7eee7005d57b2b5c9006d7c8b98241aa49a0b9e769411c58ef', + sellLimitOrder: + '&id=multisig_0x03042B890b99552b60A073F808100517fb148F60_0xf7093c3e87e3b703a0df4d9360cd38254ed69d0dc4f7ff5399a194bd92e9014c', + sellLimitOrderFilled: + '&id=multisig_0x8f4A19C85b39032A37f7a6dCc65234f966F72551_0xd3d13db9fc438d0674819f81be62fcd9c74a8ed7c101a8249b8895e55ee80d76', + safeAppSwapOrder: + '&id=multisig_0x03042B890b99552b60A073F808100517fb148F60_0x5f08e05edb210a8990791e9df2f287a5311a8137815ec85856a2477a36552f1e', +} + +export function clickOnAssetSwapBtn(index) { + cy.get(assetsSwapBtn).eq(index).as('btn') + cy.get('@btn').click() +} + +export function verifyOrderSubmittedConfirmation() { + cy.get('div').contains(orderSubmittedStr).should('exist') +} + +export function clickOnSettingsBtn() { + cy.get(settingsBtn).click() +} + +export function setExpiry(value) { + cy.get('div').contains('Swap deadline').parent().next().find('input').clear().type(value) +} + +export function enterRecipient(address) { + cy.get(customRecipient).find('input').clear().type(address) +} + +export function setSlippage(value) { + cy.contains('button', 'Auto').next('button').find('input').clear().type(value) +} +export function waitForOrdersCallToComplete() { + cy.intercept('GET', swapOrders).as('Orders') + cy.wait('@Orders') +} + +export function waitForSurplusCallToComplete() { + cy.intercept('GET', surplus).as('Surplus') + cy.wait('@Surplus') +} + +export function waitFornativePriceCallToComplete() { + cy.intercept('GET', nativePrice).as('Price') + cy.wait('@Price') +} + +export function waitForQuoteCallToComplete() { + cy.intercept('GET', quote).as('Quote') + cy.wait('@Quote') +} + +export function clickOnConfirmSwapBtn() { + cy.get('button').contains(confirmSwapStr).click() +} + +export function clickOnExceeFeeChkbox() { + cy.wait(1000) + cy.get(exceedFeesChkbox) + .should(() => {}) + .then(($button) => { + if (!$button.length) { + return + } + cy.wrap($button).click() + }) +} + +export function clickOnSwapBtn() { + cy.get('button').contains(swapBtnStr).as('swapBtn') + + cy.get('@swapBtn').should('exist').click() +} + +export function checkSwapBtnIsVisible() { + cy.get('button').contains(swapBtnStr).should('be.visible') +} + +export const currencyDirectionOptions = { + input: 'input', + output: 'output', +} + +export function acceptLegalDisclaimer() { + cy.get('button').contains('Continue').click() +} + +export function checkTokenBalance(safe, tokenSymbol) { + cy.get(inputCurrencyInput) + .invoke('text') + .then((text) => { + main.getSafeBalance(safe, constants.networkKeys.sepolia).then((response) => { + const targetToken = response.body.items.find((token) => token.tokenInfo.symbol === tokenSymbol) + const tokenBalance = targetToken.balance.toString() + let formattedBalance + + if (tokenBalance.length > 4) { + formattedBalance = `${tokenBalance[0]},${tokenBalance.slice(1, 4)}` + } else { + formattedBalance = tokenBalance + } + + expect(text).to.include(`${formattedBalance} ${tokenSymbol}`) + }) + }) +} + +export function verifySelectedInputCurrancy(option) { + cy.get(inputCurrencyInput).within(() => { + cy.get('span').contains(option).should('be.visible') + }) +} +export function selectInputCurrency(option) { + cy.get(inputCurrencyInput).within(() => { + cy.get('button').eq(0).trigger('mouseover').trigger('click') + }) + cy.get(tokenList).find('span').contains(option).click() +} + +export function selectOutputCurrency(option) { + cy.get(outputurrencyInput).within(() => { + cy.get('button').trigger('mouseover').trigger('click') + }) + cy.get(tokenList).find('span').contains(option).click() +} + +export function setInputValue(value) { + cy.get(inputCurrencyInput).within(() => { + cy.get('input').type(value) + }) +} + +export function setOutputValue(value) { + cy.get(outputurrencyInput).within(() => { + cy.get('input').type(value) + }) +} + +export function enableCustomRecipient(option) { + if (!option) cy.get(recipientToggle).click() +} + +export function disableCustomRecipient(option) { + if (option) cy.get(recipientToggle).click() +} + +export function isInputGreaterZero(inputSelector) { + return cy + .get(inputSelector) + .find('input') + .invoke('val') + .then((val) => { + const n = parseFloat(val) + return n > 0 + }) +} + +export function selectOrderType(type) { + cy.get('a').contains(swapStr).click() + cy.get(orderTypeMenuItem).contains(type).click() +} + +export function createRegex(pattern, placeholder) { + const pattern_ = pattern.replace(placeholder, `\\s*\\d*\\.?\\d*\\s*${placeholder}`) + return new RegExp(pattern_, 'i') +} + +export function getOrderID() { + return new RegExp(`[a-fA-F0-9]{8}`, 'i') +} + +export function getWidgetFee() { + return new RegExp(`\\s*\\d*\\.?\\d+\\s*%\\s*`, 'i') +} + +export function checkTokenOrder(regexPattern, option) { + cy.get(create_tx.txRowTitle) + .filter(`:contains("${option}")`) + .parent('div') + .then(($div) => { + const text = $div.text() + const regex = new RegExp(regexPattern, 'i') + + cy.wrap($div).should(($div) => { + expect(text).to.match(regex) + }) + }) +} + +export function verifyOrderIDUrl() { + cy.get(create_tx.txRowTitle) + .contains(orderIdStr) + .parent() + .within(() => { + cy.get(explorerBtn).should('have.attr', 'href').and('include', cowOrdersUrl) + }) +} + +export function verifyOrderDetails(limitPrice, expiry, slippage, interactWith, oderID, widgetFee) { + cy.get(limitPriceFld).contains(limitPrice) + cy.get(expiryFld).contains(expiry) + cy.get(slippageFld).contains(slippage) + cy.get(orderIDFld).contains(oderID) + cy.get(widgetFeeFld).contains(widgetFee) + cy.get(interactWithFld).contains(interactWith) +} + +export function verifyRecipientAlertIsDisplayed() { + main.verifyElementsIsVisible([recipientAlert]) +} diff --git a/cypress/e2e/pages/transactions.page.js b/cypress/e2e/pages/transactions.page.js index d89457a06d..9efe5bd828 100644 --- a/cypress/e2e/pages/transactions.page.js +++ b/cypress/e2e/pages/transactions.page.js @@ -12,6 +12,10 @@ export function selectExecuteNow() { cy.get(executeNowOption).click() } +export function selectExecuteLater() { + cy.get(executeLaterOption).click() +} + export function selectConnectedWalletOption() { cy.get(connectedWalletExecutionMethod).click() } diff --git a/cypress/e2e/regression/add_owner.cy.js b/cypress/e2e/regression/add_owner.cy.js index 23c1e802e4..6129ab561e 100644 --- a/cypress/e2e/regression/add_owner.cy.js +++ b/cypress/e2e/regression/add_owner.cy.js @@ -3,8 +3,11 @@ import * as main from '../../e2e/pages/main.page' import * as owner from '../pages/owners.pages' import * as addressBook from '../pages/address_book.page' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('Add Owners tests', () => { before(async () => { @@ -18,20 +21,17 @@ describe('Add Owners tests', () => { cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) }) - it('Verify Tooltip displays correct message for disconnected user', () => { - owner.waitForConnectionStatus() - owner.clickOnWalletExpandMoreIcon() - owner.clickOnDisconnectBtn() + it('Verify add owner button is disabled for disconnected user', () => { owner.verifyAddOwnerBtnIsDisabled() }) it('Verify the Add New Owner Form can be opened', () => { - owner.waitForConnectionStatus() + wallet.connectSigner(signer) owner.openAddOwnerWindow() }) it('Verify error message displayed if character limit is exceeded in Name input', () => { - owner.waitForConnectionStatus() + wallet.connectSigner(signer) owner.openAddOwnerWindow() owner.typeOwnerName(main.generateRandomString(51)) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) @@ -45,14 +45,14 @@ describe('Add Owners tests', () => { addressBook.clickOnSaveEntryBtn() addressBook.verifyNewEntryAdded(constants.addresBookContacts.user1.name, constants.addresBookContacts.user1.address) cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_4) - owner.waitForConnectionStatus() + wallet.connectSigner(signer) owner.openAddOwnerWindow() owner.typeOwnerAddress(constants.addresBookContacts.user1.address) owner.verifyNewOwnerName(constants.addresBookContacts.user1.name) }) it('Verify that Name field not mandatory', () => { - owner.waitForConnectionStatus() + wallet.connectSigner(signer) owner.openAddOwnerWindow() owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) owner.clickOnNextBtn() diff --git a/cypress/e2e/regression/address_book.cy.js b/cypress/e2e/regression/address_book.cy.js index aa5f04bcb8..1413b95a09 100644 --- a/cypress/e2e/regression/address_book.cy.js +++ b/cypress/e2e/regression/address_book.cy.js @@ -7,8 +7,11 @@ import * as main from '../../e2e/pages/main.page' import * as ls from '../../support/localstorage_data.js' import * as sidebar from '../pages/sidebar.pages.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY const NAME = 'Owner1' const EDITED_NAME = 'Edited Owner1' @@ -111,6 +114,7 @@ describe('Address book tests', () => { addressBook.clickOnImportFileBtn() addressBook.importCSVFile(addressBook.addedSafesCSVFile) addressBook.clickOnImportBtn() + wallet.connectSigner(signer) sidebar.openSidebar() sidebar.verifyAddedSafesExist([importedSafe]) }) diff --git a/cypress/e2e/regression/balances_pagination.cy.js b/cypress/e2e/regression/balances_pagination.cy.js index a0569de6cc..b110310b9c 100644 --- a/cypress/e2e/regression/balances_pagination.cy.js +++ b/cypress/e2e/regression/balances_pagination.cy.js @@ -7,10 +7,8 @@ const ASSETS_LENGTH = 8 describe('Balance pagination tests', () => { before(() => { cy.clearLocalStorage() - // Open the Safe used for testing cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_6) main.acceptCookies() - assets.selectTokenList(assets.tokenListOptions.allTokens) }) diff --git a/cypress/e2e/regression/batch_tx.cy.js b/cypress/e2e/regression/batch_tx.cy.js index 6986e03460..b9ecb13740 100644 --- a/cypress/e2e/regression/batch_tx.cy.js +++ b/cypress/e2e/regression/batch_tx.cy.js @@ -3,12 +3,15 @@ import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' import * as owner from '../../e2e/pages/owners.pages.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' const currentNonce = 3 const funds_first_tx = '0.001' const funds_second_tx = '0.002' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('Batch transaction tests', () => { before(async () => { @@ -18,6 +21,7 @@ describe('Batch transaction tests', () => { beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_2) + wallet.connectSigner(signer) owner.waitForConnectionStatus() main.acceptCookies() }) diff --git a/cypress/e2e/regression/create_safe_cf.cy.js b/cypress/e2e/regression/create_safe_cf.cy.js new file mode 100644 index 0000000000..4b936701d9 --- /dev/null +++ b/cypress/e2e/regression/create_safe_cf.cy.js @@ -0,0 +1,161 @@ +import * as constants from '../../support/constants' +import * as main from '../pages/main.page' +import * as createwallet from '../pages/create_wallet.pages' +import * as owner from '../pages/owners.pages' +import * as navigation from '../pages/navigation.page.js' +import * as ls from '../../support/localstorage_data.js' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as safeapps from '../pages/safeapps.pages' +import * as wallet from '../../support/utils/wallet.js' + +let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY + +const txOrder = [ + 'Activate Safe now', + 'Add another signer', + 'Set up recovery', + 'Swap tokens', + 'Custom transaction', + 'Send token', +] + +describe('CF Safe regression tests', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.homeUrl + staticSafes.SEP_STATIC_SAFE_14) + main.acceptCookies() + }) + + it('Verify Add native assets and Create tx modals can be opened', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + wallet.connectSigner(signer) + owner.waitForConnectionStatus() + createwallet.clickOnAddFundsBtn() + main.verifyElementsIsVisible([createwallet.qrCode]) + navigation.clickOnModalCloseBtn() + + createwallet.clickOnCreateTxBtn() + navigation.clickOnModalCloseBtn() + }) + + it('Verify "0 out of 2 step completed" is shown in the dashboard', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + createwallet.checkInitialStepsDisplayed() + }) + + it('Verify "Add native assets" button opens a modal with a QR code and the safe address', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + wallet.connectSigner(signer) + owner.waitForConnectionStatus() + createwallet.clickOnAddFundsBtn() + main.verifyElementsIsVisible([createwallet.qrCode, createwallet.addressInfo]) + }) + + it('Verify QR code switch status change works in "Add native assets" modal', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + wallet.connectSigner(signer) + owner.waitForConnectionStatus() + createwallet.clickOnAddFundsBtn() + createwallet.checkQRCodeSwitchStatus(constants.checkboxStates.checked) + createwallet.clickOnQRCodeSwitch() + createwallet.checkQRCodeSwitchStatus(constants.checkboxStates.unchecked) + }) + + it('Verify "Create new transaction" modal contains tx types in sequence', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + wallet.connectSigner(signer) + owner.waitForConnectionStatus() + createwallet.clickOnCreateTxBtn() + createwallet.checkAllTxTypesOrder(txOrder) + }) + + it('Verify "Add safe now" button takes to a tx "Activate account"', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + wallet.connectSigner(signer) + owner.waitForConnectionStatus() + createwallet.clickOnCreateTxBtn() + createwallet.clickOnTxType(txOrder[0]) + main.verifyElementsExist([createwallet.activateAccountBtn]) + }) + + it('Verify "Add another Owner" takes to a tx Add owner', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + wallet.connectSigner(signer) + owner.waitForConnectionStatus() + createwallet.clickOnCreateTxBtn() + createwallet.clickOnTxType(txOrder[1]) + main.verifyTextVisibility([createwallet.addSignerStr]) + }) + + it('Verify "Setup recovery" button takes to the "Account recovery" flow', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + wallet.connectSigner(signer) + owner.waitForConnectionStatus() + createwallet.clickOnCreateTxBtn() + createwallet.clickOnTxType(txOrder[2]) + main.verifyTextVisibility([createwallet.accountRecoveryStr]) + }) + + it('Verify "Send token" takes to the tx form to send tokens', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + wallet.connectSigner(signer) + owner.waitForConnectionStatus() + createwallet.clickOnCreateTxBtn() + createwallet.clickOnTxType(txOrder[5]) + main.verifyTextVisibility([createwallet.sendTokensStr]) + }) + + it('Verify "Custom transaction" takes to the tx builder app ', () => { + const iframeSelector = `iframe[id="iframe-${constants.TX_Builder_url}"]` + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__SafeApps__infoModal, + ls.appPermissions(constants.safeTestAppurl).infoModalAccepted, + ) + cy.reload() + wallet.connectSigner(signer) + owner.waitForConnectionStatus() + createwallet.clickOnCreateTxBtn() + createwallet.clickOnTxType(txOrder[4]) + main.getIframeBody(iframeSelector).within(() => { + cy.contains(safeapps.transactionBuilderStr) + }) + }) + + it('Verify "Notifications" in the settings are disabled', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + cy.visit(constants.notificationsUrl + staticSafes.SEP_STATIC_SAFE_14) + createwallet.checkNotificationsSwitchIs(constants.enabledStates.disabled) + }) + + it('Verify in assets, that a "Add funds" block is present', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_14) + main.verifyElementsIsVisible([createwallet.addFundsSection, createwallet.noTokensAlert]) + }) + + it('Verify clicking on "Activate now" button opens safe activation flow', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_14) + createwallet.clickOnActivateAccountBtn() + main.verifyElementsIsVisible([createwallet.activateAccountBtn]) + }) +}) diff --git a/cypress/e2e/regression/create_safe_google.cy.js b/cypress/e2e/regression/create_safe_google.cy.js deleted file mode 100644 index 4a946f39ea..0000000000 --- a/cypress/e2e/regression/create_safe_google.cy.js +++ /dev/null @@ -1,53 +0,0 @@ -import * as constants from '../../support/constants' -import * as main from '../pages/main.page' -import * as createwallet from '../pages/create_wallet.pages' -import * as owner from '../pages/owners.pages' -import * as navigation from '../pages/navigation.page' - -describe('Safe creation Google tests', () => { - beforeEach(() => { - cy.visit(constants.welcomeUrl + '?chain=sep') - cy.clearLocalStorage() - main.acceptCookies() - // TODO: Need credentials to finish API Google login - // createwallet.loginGoogleAPI() - }) - - // TODO: Clarify requirements - it.skip('Verify that "Connect with Google" option is disabled for the networks without Relay on the Welcome page', () => { - owner.clickOnWalletExpandMoreIcon() - owner.clickOnDisconnectBtn() - createwallet.selectNetwork(constants.networks.polygon) - createwallet.verifyGoogleConnectBtnIsDisabled() - }) - - it.skip('Verify a successful connection with google', () => { - createwallet.verifyGoogleSignin() - }) - - it.skip('Verify Google account info in the header after account connection', () => { - createwallet.verifyGoogleAccountInfoInHeader() - }) - - it.skip('Verify a successful safe creation with a Google account', { defaultCommandTimeout: 90000 }, () => { - createwallet.verifyGoogleSignin().click() - createwallet.clickOnContinueWithWalletBtn() - createwallet.verifyOwnerInfoIsPresent() - createwallet.clickOnReviewStepNextBtn() - createwallet.verifySafeIsBeingCreated() - createwallet.verifySafeCreationIsComplete() - }) - - it.skip('Verify a successful transaction creation with Google account', { defaultCommandTimeout: 90000 }, () => { - createwallet.verifyGoogleSignin().click() - createwallet.clickOnContinueWithWalletBtn() - createwallet.clickOnReviewStepNextBtn() - createwallet.verifySafeCreationIsComplete() - navigation.clickOnSideNavigation(navigation.sideNavSettingsIcon) - owner.openAddOwnerWindow() - owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) - owner.clickOnNextBtn() - main.clickOnExecuteBtn() - owner.verifyOwnerTransactionComplted() - }) -}) diff --git a/cypress/e2e/regression/create_safe_simple.cy.js b/cypress/e2e/regression/create_safe_simple.cy.js index 5755c3c42a..2580029ba8 100644 --- a/cypress/e2e/regression/create_safe_simple.cy.js +++ b/cypress/e2e/regression/create_safe_simple.cy.js @@ -3,16 +3,21 @@ import * as main from '../../e2e/pages/main.page' import * as createwallet from '../pages/create_wallet.pages' import * as owner from '../pages/owners.pages' import * as ls from '../../support/localstorage_data.js' +import * as wallet from '../../support/utils/wallet.js' + +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('Safe creation tests', () => { beforeEach(() => { cy.visit(constants.welcomeUrl + '?chain=sep') cy.clearLocalStorage() main.acceptCookies() + wallet.connectSigner(signer) + owner.waitForConnectionStatus() }) it('Verify Next button is disabled until switching to network is done', () => { - owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.selectNetwork(constants.networks.ethereum) createwallet.clickOnCreateNewSafeBtn() @@ -24,7 +29,6 @@ describe('Safe creation tests', () => { // TODO: Check unit tests it('Verify error message is displayed if wallet name input exceeds 50 characters', () => { - owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.typeWalletName(main.generateRandomString(51)) @@ -35,7 +39,6 @@ describe('Safe creation tests', () => { // TODO: Replace wallet with Safe // TODO: Check unit tests it('Verify there is no error message is displayed if wallet name input contains less than 50 characters', () => { - owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.typeWalletName(main.generateRandomString(50)) @@ -43,7 +46,6 @@ describe('Safe creation tests', () => { }) it('Verify current connected account is shown as default owner', () => { - owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.clickOnNextBtn() @@ -52,7 +54,6 @@ describe('Safe creation tests', () => { // TODO: Check unit tests it('Verify error message is displayed if owner name input exceeds 50 characters', () => { - owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() owner.typeExistingOwnerName(main.generateRandomString(51)) @@ -61,7 +62,6 @@ describe('Safe creation tests', () => { // TODO: Check unit tests it('Verify there is no error message is displayed if owner name input contains less than 50 characters', () => { - owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() owner.typeExistingOwnerName(main.generateRandomString(50)) @@ -70,7 +70,6 @@ describe('Safe creation tests', () => { it('Verify data persistence', () => { const ownerName = 'David' - owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.clickOnNextBtn() @@ -103,7 +102,6 @@ describe('Safe creation tests', () => { }) it('Verify tip is displayed on right side for threshold 1/1', () => { - owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.clickOnNextBtn() @@ -112,7 +110,6 @@ describe('Safe creation tests', () => { // TODO: Check unit tests it('Verify address input validation rules', () => { - owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.clickOnNextBtn() @@ -132,10 +129,12 @@ describe('Safe creation tests', () => { it('Verify duplicated signer error using the autocomplete feature', () => { cy.visit(constants.createNewSafeSepoliaUrl + '?chain=sep') - main - .addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.sameOwnerName) + cy.wrap(null) + .then(() => + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.sameOwnerName), + ) .then(() => { - owner.waitForConnectionStatus() + wallet.connectSigner(signer) createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.clickOnNextBtn() diff --git a/cypress/e2e/regression/create_safe_simple_2.cy.js b/cypress/e2e/regression/create_safe_simple_2.cy.js index ea6f06e50f..f0ad5bf2bd 100644 --- a/cypress/e2e/regression/create_safe_simple_2.cy.js +++ b/cypress/e2e/regression/create_safe_simple_2.cy.js @@ -4,10 +4,13 @@ import * as createwallet from '../pages/create_wallet.pages' import * as owner from '../pages/owners.pages' import * as ls from '../../support/localstorage_data.js' import * as safe from '../pages/load_safe.pages' +import * as wallet from '../../support/utils/wallet.js' const ownerSepolia = ['Automation owner Sepolia'] const ownerName = 'Owner name' const owner1 = 'Owner1' +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('Safe creation tests 2', () => { beforeEach(() => { @@ -17,6 +20,7 @@ describe('Safe creation tests 2', () => { }) it('Cancel button cancels safe creation', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() @@ -27,6 +31,7 @@ describe('Safe creation tests 2', () => { // Owners and confirmation step it('Verify Next button is disabled when address is empty', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() @@ -36,11 +41,14 @@ describe('Safe creation tests 2', () => { }) it('Verify owner names are autocompleted if they are present in the Address book ', () => { - owner.waitForConnectionStatus() - main - .addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.sameOwnerName) + cy.wrap(null) + .then(() => + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.sameOwnerName), + ) .then(() => { cy.visit(constants.welcomeUrl + '?chain=sep') + wallet.connectSigner(signer) + owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() safe.clickOnNextBtn() @@ -49,11 +57,14 @@ describe('Safe creation tests 2', () => { }) it("Verify names don't autofill if they are added to another chain's Address book", () => { - owner.waitForConnectionStatus() - main - .addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.sameOwnerName[1]) + cy.wrap(null) + .then(() => + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.sameOwnerName[1]), + ) .then(() => { cy.visit(constants.welcomeUrl + '?chain=sep') + wallet.connectSigner(signer) + owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() safe.clickOnNextBtn() @@ -62,6 +73,7 @@ describe('Safe creation tests 2', () => { }) it('Verify an valid name for owner can be inputed', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() @@ -71,6 +83,7 @@ describe('Safe creation tests 2', () => { }) it('Verify Threshold matching required confirmations max with amount of owners', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() @@ -80,6 +93,7 @@ describe('Safe creation tests 2', () => { }) it('Verify deleting owner rows updates the currenlty set policies value', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() @@ -91,6 +105,7 @@ describe('Safe creation tests 2', () => { }) it('Verify ENS name in the address and name fields is resolved', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() @@ -101,6 +116,7 @@ describe('Safe creation tests 2', () => { }) it('Verify deleting owner rows is possible', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() @@ -112,11 +128,14 @@ describe('Safe creation tests 2', () => { }) it('Verify existing owner in address book will have their names filled when their address is pasted', () => { - main - .addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.sepoliaAddress1) + cy.wrap(null) + .then(() => + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.sepoliaAddress1), + ) .then(() => { - owner.waitForConnectionStatus() cy.visit(constants.welcomeUrl + '?chain=sep') + wallet.connectSigner(signer) + owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() safe.clickOnNextBtn() diff --git a/cypress/e2e/regression/create_tx.cy.js b/cypress/e2e/regression/create_tx.cy.js index fbb218bed9..9148283023 100644 --- a/cypress/e2e/regression/create_tx.cy.js +++ b/cypress/e2e/regression/create_tx.cy.js @@ -2,11 +2,15 @@ import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' import * as createtx from '../../e2e/pages/create_tx.pages' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] const sendValue = 0.00002 +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY + function happyPathToStepTwo() { createtx.typeRecipientAddress(constants.EOA) createtx.clickOnTokenselectorAndSelectSepoliaEth() @@ -23,6 +27,7 @@ describe('Create transactions tests', () => { cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_6) main.acceptCookies() + wallet.connectSigner(signer) createtx.clickOnNewtransactionBtn() createtx.clickOnSendTokensBtn() }) diff --git a/cypress/e2e/regression/messages_offchain.cy.js b/cypress/e2e/regression/messages_offchain.cy.js deleted file mode 100644 index 62077ced58..0000000000 --- a/cypress/e2e/regression/messages_offchain.cy.js +++ /dev/null @@ -1,74 +0,0 @@ -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 * as msg_data from '../../fixtures/txmessages_data.json' -import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' - -let staticSafes = [] - -const typeMessagesGeneral = msg_data.type.general -const typeMessagesOffchain = msg_data.type.offChain - -describe('Offchain Messages tests', () => { - before(async () => { - staticSafes = await getSafes(CATEGORIES.static) - }) - - beforeEach(() => { - cy.clearLocalStorage() - cy.visit(constants.transactionsMessagesUrl + staticSafes.SEP_STATIC_SAFE_10) - main.acceptCookies() - }) - - it('Verify summary for off-chain unsigned messages', () => { - createTx.verifySummaryByIndex( - 0, - [typeMessagesGeneral.sign, typeMessagesGeneral.oneOftwo, typeMessagesOffchain.walletConnect], - typeMessagesOffchain.altTmage, - ) - createTx.verifySummaryByIndex( - 2, - [typeMessagesGeneral.sign, typeMessagesGeneral.oneOftwo, typeMessagesOffchain.walletConnect], - typeMessagesOffchain.altTmage, - ) - }) - - it('Verify summary for off-chain signed messages', () => { - createTx.verifySummaryByIndex( - 1, - [typeMessagesGeneral.confirmed, typeMessagesGeneral.twoOftwo, typeMessagesOffchain.walletConnect], - typeMessagesOffchain.altTmage, - ) - createTx.verifySummaryByIndex( - 3, - [typeMessagesGeneral.confirmed, typeMessagesGeneral.twoOftwo, typeMessagesOffchain.walletConnect], - typeMessagesOffchain.altTmage, - ) - }) - - it('Verify exapanded details for simple off-chain message', () => { - createTx.clickOnTransactionItemByIndex(2) - cy.contains(typeMessagesOffchain.message2).should('be.visible') - }) - - it('Verify exapanded details for EIP 712 off-chain message', () => { - const jsonString = createTx.messageNestedStr - const values = [ - typeMessagesOffchain.name, - typeMessagesOffchain.testStringNested, - typeMessagesOffchain.EIP712Domain, - typeMessagesOffchain.message3, - ] - - createTx.clickOnTransactionItemByIndex(1) - cy.get(createTx.txRowTitle) - .next() - .then(($section) => { - expect($section.text()).to.include(jsonString) - const count = $section.text().split(jsonString).length - 1 - expect(count).to.eq(3) - }) - - main.verifyTextVisibility(values) - }) -}) diff --git a/cypress/e2e/regression/messages_popup.cy.js b/cypress/e2e/regression/messages_popup.cy.js new file mode 100644 index 0000000000..441debb121 --- /dev/null +++ b/cypress/e2e/regression/messages_popup.cy.js @@ -0,0 +1,74 @@ +import * as constants from '../../support/constants.js' +import * as main from '../pages/main.page.js' +import * as modal from '../pages/modals.page.js' +import * as apps from '../pages/safeapps.pages.js' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as ls from '../../support/localstorage_data.js' +import * as messages from '../pages/messages.pages.js' +import * as msg_confirmation_modal from '../pages/modals/message_confirmation.pages.js' + +let staticSafes = [] +const safeApp = 'Safe Test App' +const onchainMessage = 'Message 1' +let iframeSelector + +describe('Messages popup window tests', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.appsCustomUrl + staticSafes.SEP_STATIC_SAFE_10) + main.acceptCookies() + iframeSelector = `iframe[id="iframe-${constants.safeTestAppurl}"]` + }) + + it('Verify off-chain message popup window can be triggered', () => { + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__customSafeApps_11155111, + ls.customApps(constants.safeTestAppurl).safeTestApp, + ) + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__SafeApps__browserPermissions, + ls.appPermissions(constants.safeTestAppurl).grantedPermissions, + ) + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__SafeApps__infoModal, + ls.appPermissions(constants.safeTestAppurl).infoModalAccepted, + ) + cy.reload() + apps.clickOnApp(safeApp) + apps.clickOnOpenSafeAppBtn() + main.getIframeBody(iframeSelector).within(() => { + apps.triggetOffChainTx() + }) + msg_confirmation_modal.verifyConfirmationWindowTitle(modal.modalTitiles.confirmTx) + msg_confirmation_modal.verifySafeAppInPopupWindow(safeApp) + }) + + it('Verify on-chain message popup window can be triggered', () => { + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__customSafeApps_11155111, + ls.customApps(constants.safeTestAppurl).safeTestApp, + ) + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__SafeApps__browserPermissions, + ls.appPermissions(constants.safeTestAppurl).grantedPermissions, + ) + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__SafeApps__infoModal, + ls.appPermissions(constants.safeTestAppurl).infoModalAccepted, + ) + cy.reload() + apps.clickOnApp(safeApp) + apps.clickOnOpenSafeAppBtn() + main.getIframeBody(iframeSelector).within(() => { + messages.enterOnchainMessage(onchainMessage) + apps.triggetOnChainTx() + }) + msg_confirmation_modal.verifyConfirmationWindowTitle(modal.modalTitiles.confirmMsg) + msg_confirmation_modal.verifySafeAppInPopupWindow(safeApp) + msg_confirmation_modal.verifyMessagePresent(onchainMessage) + }) +}) diff --git a/cypress/e2e/regression/nfts.cy.js b/cypress/e2e/regression/nfts.cy.js index 700161492e..386af62674 100644 --- a/cypress/e2e/regression/nfts.cy.js +++ b/cypress/e2e/regression/nfts.cy.js @@ -4,6 +4,7 @@ import * as nfts from '../pages/nfts.pages' import * as navigation from '../pages/navigation.page' import * as createTx from '../pages/create_tx.pages' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' const singleNFT = ['safeTransferFrom'] const multipleNFT = ['multiSend'] @@ -13,6 +14,9 @@ const NFTSentName = 'GTT #22' let nftsSafes, staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY + describe('NFTs tests', () => { before(() => { getSafes(CATEGORIES.nfts) @@ -29,6 +33,7 @@ describe('NFTs tests', () => { cy.clearLocalStorage() cy.visit(constants.balanceNftsUrl + staticSafes.SEP_STATIC_SAFE_2) main.acceptCookies() + wallet.connectSigner(signer) nfts.waitForNftItems(2) }) @@ -82,6 +87,7 @@ describe('NFTs tests', () => { it('Verify Send NFT transaction has been created', () => { cy.visit(constants.balanceNftsUrl + nftsSafes.SEP_NFT_SAFE_1) + wallet.connectSigner(signer) nfts.verifyInitialNFTData() nfts.selectNFTs(1) nfts.sendNFT() diff --git a/cypress/e2e/regression/pending_actions.cy.js b/cypress/e2e/regression/pending_actions.cy.js deleted file mode 100644 index c7f2399954..0000000000 --- a/cypress/e2e/regression/pending_actions.cy.js +++ /dev/null @@ -1,43 +0,0 @@ -import * as constants from '../../support/constants' -import * as safe from '../pages/load_safe.pages' - -describe('Pending actions tests', () => { - before(() => { - cy.visit(constants.welcomeUrl) - // main.acceptCookies() - }) - - //TODO: Discuss test logic - - beforeEach(() => { - // Uses the previously saved local storage - // to preserve the wallet connection between tests - cy.restoreLocalStorageCache() - }) - - afterEach(() => { - cy.saveLocalStorageCache() - }) - - it.skip('should add the Safe with the pending actions', () => { - safe.openLoadSafeForm() - safe.inputAddress(constants.TEST_SAFE) - safe.clickOnNextBtn() - safe.verifyOwnersModalIsVisible() - safe.clickOnNextBtn() - safe.clickOnAddBtn() - }) - - it.skip('should display the pending actions in the Safe list sidebar', () => { - safe.openSidebar() - safe.verifyAddressInsidebar(constants.SIDEBAR_ADDRESS) - safe.verifySidebarIconNumber(1) - safe.clickOnPendingActions() - //cy.get('img[alt="E2E Wallet logo"]').next().contains('2').should('exist') - }) - - it.skip('should have the right number of queued and signable transactions', () => { - safe.verifyTransactionSectionIsVisible() - safe.verifyNumberOfTransactions(1, 1) - }) -}) diff --git a/cypress/e2e/regression/remove_owner.cy.js b/cypress/e2e/regression/remove_owner.cy.js index 2ea8f5a129..15b0e69632 100644 --- a/cypress/e2e/regression/remove_owner.cy.js +++ b/cypress/e2e/regression/remove_owner.cy.js @@ -4,8 +4,11 @@ import * as owner from '../pages/owners.pages' import * as createwallet from '../pages/create_wallet.pages' import * as createTx from '../pages/create_tx.pages.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('Remove Owners tests', () => { before(async () => { @@ -17,7 +20,6 @@ describe('Remove Owners tests', () => { main.waitForHistoryCallToComplete() cy.clearLocalStorage() main.acceptCookies() - owner.waitForConnectionStatus() cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) }) @@ -25,30 +27,30 @@ describe('Remove Owners tests', () => { owner.verifyRemoveBtnIsEnabled().should('have.length', 2) }) - it('Verify Tooltip displays correct message for Non-Owner', () => { - cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_1) + it('Verify remove button does not exist for Non-Owner when there is only 1 owner in the safe', () => { + cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_3) main.waitForHistoryCallToComplete() - owner.waitForConnectionStatus() - owner.verifyRemoveBtnIsDisabled() + main.verifyElementsCount(owner.removeOwnerBtn, 0) }) - it('Verify Tooltip displays correct message for disconnected user', () => { - owner.clickOnWalletExpandMoreIcon() - owner.clickOnDisconnectBtn() + it('Verify remove owner button is disabled for disconnected user', () => { owner.verifyRemoveBtnIsDisabled() }) it('Verify owner removal form can be opened', () => { + wallet.connectSigner(signer) owner.openRemoveOwnerWindow(1) }) it('Verify threshold input displays the upper limit as the current safe number of owners minus one', () => { + wallet.connectSigner(signer) owner.openRemoveOwnerWindow(1) owner.verifyThresholdLimit(1, 1) owner.getThresholdOptions().should('have.length', 1) }) it('Verify owner deletion transaction has been created', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() owner.openRemoveOwnerWindow(1) cy.wait(3000) diff --git a/cypress/e2e/regression/replace_owner.cy.js b/cypress/e2e/regression/replace_owner.cy.js index ce408ba072..f332faded2 100644 --- a/cypress/e2e/regression/replace_owner.cy.js +++ b/cypress/e2e/regression/replace_owner.cy.js @@ -4,8 +4,11 @@ import * as owner from '../pages/owners.pages' import * as addressBook from '../pages/address_book.page' import * as createTx from '../pages/create_tx.pages.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY const ownerName = 'Replacement Signer Name' @@ -22,14 +25,12 @@ describe('Replace Owners tests', () => { }) it('Verify Tooltip displays correct message for disconnected user', () => { - owner.waitForConnectionStatus() - owner.clickOnWalletExpandMoreIcon() - owner.clickOnDisconnectBtn() owner.verifyReplaceBtnIsDisabled() }) // TODO: Check unit tests it('Verify max characters in name field', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() owner.openReplaceOwnerWindow() owner.typeOwnerName(main.generateRandomString(51)) @@ -45,6 +46,7 @@ describe('Replace Owners tests', () => { addressBook.clickOnSaveEntryBtn() addressBook.verifyNewEntryAdded(constants.addresBookContacts.user1.name, constants.addresBookContacts.user1.address) cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_4) + wallet.connectSigner(signer) owner.waitForConnectionStatus() owner.openReplaceOwnerWindow() owner.typeOwnerAddress(constants.addresBookContacts.user1.address) @@ -52,6 +54,7 @@ describe('Replace Owners tests', () => { }) it('Verify that Name field not mandatory. Verify confirmation for owner replacement is displayed', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() owner.openReplaceOwnerWindow() owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) @@ -60,6 +63,7 @@ describe('Replace Owners tests', () => { }) it('Verify relevant error messages are displayed in Address input', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() owner.openReplaceOwnerWindow() owner.typeOwnerAddress(main.generateRandomString(10)) @@ -80,6 +84,7 @@ describe('Replace Owners tests', () => { it("Verify 'Replace' tx is created", () => { cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_4) + wallet.connectSigner(signer) owner.waitForConnectionStatus() owner.openReplaceOwnerWindow() cy.wait(1000) diff --git a/cypress/e2e/regression/sidebar.cy.js b/cypress/e2e/regression/sidebar.cy.js index 024f278bd7..597b8e3816 100644 --- a/cypress/e2e/regression/sidebar.cy.js +++ b/cypress/e2e/regression/sidebar.cy.js @@ -3,8 +3,11 @@ import * as main from '../pages/main.page' import * as sideBar from '../pages/sidebar.pages' import * as navigation from '../pages/navigation.page' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('Sidebar tests', () => { before(async () => { @@ -39,17 +42,17 @@ describe('Sidebar tests', () => { }) it('Verify New transaction button enabled for owners', () => { + wallet.connectSigner(signer) sideBar.verifyNewTxBtnStatus(constants.enabledStates.enabled) }) it('Verify New transaction button enabled for beneficiaries who are non-owners', () => { cy.visit(constants.homeUrl + staticSafes.SEP_STATIC_SAFE_11) + wallet.connectSigner(signer) sideBar.verifyNewTxBtnStatus(constants.enabledStates.enabled) }) it('Verify New Transaction button disabled for non-owners', () => { - navigation.clickOnWalletExpandMoreIcon() - navigation.clickOnDisconnectBtn() main.verifyElementsCount(navigation.newTxBtn, 0) }) diff --git a/cypress/e2e/regression/sidebar_2.cy.js b/cypress/e2e/regression/sidebar_2.cy.js index 7f82bfcfa2..a24156d4ef 100644 --- a/cypress/e2e/regression/sidebar_2.cy.js +++ b/cypress/e2e/regression/sidebar_2.cy.js @@ -37,16 +37,15 @@ describe('Sidebar added sidebar tests', () => { sideBar.verifySafeNameExists(newSafeName) }) - // TODO: Update to remove from watch list - it.skip('Verify a safe can be removed', () => { + it('Verify a safe can be removed', () => { sideBar.openSidebar() sideBar.removeSafeItem(addedSafe900) sideBar.verifySafeRemoved([addedSafe900]) }) it('Verify Fiat currency changes when edited in the assets tab', () => { - assets.changeCurrency(constants.currencies.cad) - sideBar.checkCurrencyInHeader(constants.currencies.cad) + assets.changeCurrency(assets.currencyCAD) + sideBar.checkCurrencyInHeader(assets.currency$) }) // Waiting for endpoint from CGW diff --git a/cypress/e2e/regression/sidebar_3.cy.js b/cypress/e2e/regression/sidebar_3.cy.js new file mode 100644 index 0000000000..b1ff34d2c2 --- /dev/null +++ b/cypress/e2e/regression/sidebar_3.cy.js @@ -0,0 +1,155 @@ +import * as constants from '../../support/constants.js' +import * as main from '../pages/main.page.js' +import * as sideBar from '../pages/sidebar.pages.js' +import * as ls from '../../support/localstorage_data.js' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' +import * as create_wallet from '../pages/create_wallet.pages.js' +import * as navigation from '../pages/navigation.page.js' +import * as owner from '../pages/owners.pages.js' + +let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY +const signer1 = walletCredentials.OWNER_1_PRIVATE_KEY +const signer2 = walletCredentials.OWNER_3_PRIVATE_KEY + +describe('Sidebar tests 3', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.clearLocalStorage() + }) + + it('Verify that users with no accounts see the empty state in "My accounts" block', () => { + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_9) + main.acceptCookies() + cy.intercept('GET', constants.safeListEndpoint, { 1: [], 100: [], 137: [], 11155111: [] }) + wallet.connectSigner(signer) + sideBar.openSidebar() + sideBar.verifySafeListIsEmpty() + }) + + it('Verify empty state of the Watchlist', () => { + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_9) + main.acceptCookies() + cy.intercept('GET', constants.safeListEndpoint, {}) + wallet.connectSigner(signer) + sideBar.openSidebar() + sideBar.verifyWatchlistIsEmpty() + }) + + it('Verify connected user is redirected from welcome page to accounts page', () => { + cy.visit(constants.welcomeUrl + '?chain=sep') + main.acceptCookies() + wallet.connectSigner(signer) + create_wallet.clickOnContinueWithWalletBtn() + + cy.location().should((loc) => { + expect(loc.pathname).to.eq('/welcome/accounts') + }) + }) + + it('Verify that the user see safes that he owns in the list', () => { + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_9) + main.acceptCookies() + cy.intercept('GET', constants.safeListEndpoint, { + 11155111: [sideBar.sideBarSafes.safe1, sideBar.sideBarSafes.safe2], + }) + + wallet.connectSigner(signer) + sideBar.openSidebar() + sideBar.verifyAddedSafesExistByIndex(0, sideBar.sideBarSafes.safe1short) + sideBar.verifyAddedSafesExistByIndex(1, sideBar.sideBarSafes.safe2short) + }) + + it('Verify there is an option to name an unnamed safe', () => { + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_9) + main.acceptCookies() + cy.intercept('GET', constants.safeListEndpoint, { + 11155111: [sideBar.sideBarSafes.safe1, sideBar.sideBarSafes.safe2], + }) + wallet.connectSigner(signer) + sideBar.openSidebar() + sideBar.verifySafeGiveNameOptionExists(0) + }) + + it('Verify Import/export buttons are present', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.addedSafes) + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_9) + main.acceptCookies() + cy.intercept('GET', constants.safeListEndpoint, { + 11155111: [sideBar.sideBarSafes.safe1, sideBar.sideBarSafes.safe2], + }) + wallet.connectSigner(signer) + sideBar.openSidebar() + main.checkButtonByTextExists(sideBar.importBtnStr) + main.checkButtonByTextExists(sideBar.exportBtnStr) + }) + + it('Verify the "My accounts" counter at the top is counting all safes the user owns', () => { + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_9) + main.acceptCookies() + cy.intercept('GET', constants.safeListEndpoint, { + 11155111: [sideBar.sideBarSafes.safe1, sideBar.sideBarSafes.safe2], + }) + wallet.connectSigner(signer) + sideBar.openSidebar() + sideBar.checkMyAccountCounter(2) + }) + + it('Verify that safes the user do not owns show in the watchlist after adding them', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addedSafes, ls.addedSafes.set4) + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_9) + main.acceptCookies() + wallet.connectSigner(signer1) + sideBar.openSidebar() + sideBar.verifyAddedSafesExist([sideBar.sideBarSafes.safe3short]) + }) + + it('Verify that safes that the user owns do show in the watchlist after adding them', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addedSafes, ls.addedSafes.set4) + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_9) + main.acceptCookies() + wallet.connectSigner(signer1) + sideBar.openSidebar() + sideBar.verifyAddedSafesExist([sideBar.sideBarSafes.safe3short]) + }) + + it('Verify pending signature is displayed in sidebar for unsigned tx', () => { + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_7) + main.acceptCookies() + wallet.connectSigner(signer) + cy.intercept('GET', constants.safeListEndpoint, { + 11155111: [sideBar.sideBarSafesPendingActions.safe1], + }) + sideBar.openSidebar() + sideBar.verifyTxToConfirmDoesNotExist() + + cy.get('body').click() + + owner.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + cy.intercept('GET', constants.safeListEndpoint, { + 11155111: [sideBar.sideBarSafesPendingActions.safe1], + }) + wallet.connectSigner(signer2) + sideBar.openSidebar() + sideBar.verifyAddedSafesExist([sideBar.sideBarSafesPendingActions.safe1short]) + sideBar.checkTxToConfirm(1) + }) + + it('Verify balance exists in a tx in sidebar', () => { + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_7) + main.acceptCookies() + wallet.connectSigner(signer) + cy.intercept('GET', constants.safeListEndpoint, { + 11155111: [sideBar.sideBarSafesPendingActions.safe1], + }) + sideBar.openSidebar() + sideBar.verifyTxToConfirmDoesNotExist() + sideBar.checkBalanceExists() + }) +}) diff --git a/cypress/e2e/regression/sidebar_nonowner.cy.js b/cypress/e2e/regression/sidebar_nonowner.cy.js index 69051fe5a0..629bbbea48 100644 --- a/cypress/e2e/regression/sidebar_nonowner.cy.js +++ b/cypress/e2e/regression/sidebar_nonowner.cy.js @@ -4,8 +4,11 @@ import * as sideBar from '../pages/sidebar.pages.js' import * as navigation from '../pages/navigation.page.js' import * as ls from '../../support/localstorage_data.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY const addedOwner = 'Added owner' const addedNonowner = 'Added non-owner' @@ -25,6 +28,7 @@ describe('Sidebar non-owner tests', () => { }) it('Verify New Transaction button enabled for users with Spending limits allowed', () => { + wallet.connectSigner(signer) navigation.verifyTxBtnStatus(constants.enabledStates.enabled) }) diff --git a/cypress/e2e/regression/spending_limits.cy.js b/cypress/e2e/regression/spending_limits.cy.js index c99b764779..91513b1350 100644 --- a/cypress/e2e/regression/spending_limits.cy.js +++ b/cypress/e2e/regression/spending_limits.cy.js @@ -1,13 +1,15 @@ import * as constants from '../../support/constants' import * as main from '../pages/main.page' import * as spendinglimit from '../pages/spending_limits.pages' -import * as owner from '../pages/owners.pages' import * as navigation from '../pages/navigation.page' import * as tx from '../pages/create_tx.pages' import * as ls from '../../support/localstorage_data.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY const tokenAmount = 0.1 const newTokenAmount = 0.001 @@ -19,15 +21,15 @@ describe('Spending limits tests', () => { }) beforeEach(() => { - cy.visit(constants.securityUrl + staticSafes.SEP_STATIC_SAFE_8) + cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_8) cy.clearLocalStorage() main.acceptCookies() - owner.waitForConnectionStatus() cy.get(spendinglimit.spendingLimitsSection).should('be.visible') }) it('Verify that the Review step shows beneficiary, amount allowed, reset time', () => { //Assume that default reset time is set to One time + wallet.connectSigner(signer) spendinglimit.clickOnNewSpendingLimitBtn() spendinglimit.enterBeneficiaryAddress(staticSafes.SEP_STATIC_SAFE_6) spendinglimit.enterSpendingLimitAmount(0.1) @@ -44,18 +46,28 @@ describe('Spending limits tests', () => { }) it('Verify Spending limit option is available when selecting the corresponding token', () => { + wallet.connectSigner(signer) navigation.clickOnNewTxBtn() tx.clickOnSendTokensBtn() - spendinglimit.verifySpendingOptionExists() + spendinglimit.verifyTxOptionExist([spendinglimit.spendingLimitTxOption]) }) it('Verify spending limit option shows available amount', () => { + wallet.connectSigner(signer) navigation.clickOnNewTxBtn() tx.clickOnSendTokensBtn() spendinglimit.verifySpendingOptionShowsBalance([spendingLimitBalance]) }) + it('Verify when owner is a delegate, standard tx and spending limit tx are present', () => { + wallet.connectSigner(signer) + navigation.clickOnNewTxBtn() + tx.clickOnSendTokensBtn() + spendinglimit.verifyTxOptionExist([spendinglimit.spendingLimitTxOption, spendinglimit.standardTx]) + }) + it('Verify when spending limit is selected the nonce field is removed', () => { + wallet.connectSigner(signer) navigation.clickOnNewTxBtn() tx.clickOnSendTokensBtn() spendinglimit.selectSpendingLimitOption() @@ -63,6 +75,7 @@ describe('Spending limits tests', () => { }) it('Verify "Max" button value set to be no more than the allowed amount', () => { + wallet.connectSigner(signer) navigation.clickOnNewTxBtn() tx.clickOnSendTokensBtn() spendinglimit.clickOnMaxBtn() @@ -70,12 +83,14 @@ describe('Spending limits tests', () => { }) it('Verify selecting a native token from the dropdown in new tx', () => { + wallet.connectSigner(signer) navigation.clickOnNewTxBtn() tx.clickOnSendTokensBtn() spendinglimit.selectToken(constants.tokenNames.sepoliaEther) }) it('Verify that when replacing spending limit for the same owner, previous values are displayed in red', () => { + wallet.connectSigner(signer) spendinglimit.clickOnNewSpendingLimitBtn() spendinglimit.enterBeneficiaryAddress(constants.DEFAULT_OWNER_ADDRESS) spendinglimit.enterSpendingLimitAmount(newTokenAmount) @@ -86,6 +101,7 @@ describe('Spending limits tests', () => { }) it('Verify that when editing spending limit for owner who used some of it, relevant actions are displayed', () => { + wallet.connectSigner(signer) spendinglimit.clickOnNewSpendingLimitBtn() spendinglimit.enterBeneficiaryAddress(constants.SPENDING_LIMIT_ADDRESS_2) spendinglimit.enterSpendingLimitAmount(newTokenAmount) @@ -106,6 +122,7 @@ describe('Spending limits tests', () => { ) .then(() => { cy.reload() + wallet.connectSigner(signer) navigation.clickOnNewTxBtn() tx.clickOnSendTokensBtn() spendinglimit.clickOnTokenDropdown() @@ -123,6 +140,7 @@ describe('Spending limits tests', () => { ) .then(() => { cy.reload() + wallet.connectSigner(signer) spendinglimit.clickOnNewSpendingLimitBtn() spendinglimit.enterBeneficiaryAddress(constants.DEFAULT_OWNER_ADDRESS.substring(30)) spendinglimit.selectRecipient(constants.DEFAULT_OWNER_ADDRESS) diff --git a/cypress/e2e/regression/spending_limits_nonowner.cy.js b/cypress/e2e/regression/spending_limits_nonowner.cy.js index bd2d2dc1cc..e64bda33fb 100644 --- a/cypress/e2e/regression/spending_limits_nonowner.cy.js +++ b/cypress/e2e/regression/spending_limits_nonowner.cy.js @@ -1,7 +1,6 @@ import * as constants from '../../support/constants.js' import * as main from '../pages/main.page.js' import * as spendinglimit from '../pages/spending_limits.pages.js' -import * as owner from '../pages/owners.pages.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' let staticSafes = [] @@ -12,10 +11,9 @@ describe('Spending limits non-owner tests', () => { }) beforeEach(() => { - cy.visit(constants.securityUrl + staticSafes.SEP_STATIC_SAFE_3) + cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_3) cy.clearLocalStorage() main.acceptCookies() - owner.waitForConnectionStatus() cy.get(spendinglimit.spendingLimitsSection).should('be.visible') }) diff --git a/cypress/e2e/regression/swaps.cy.js b/cypress/e2e/regression/swaps.cy.js new file mode 100644 index 0000000000..c8994fdc4a --- /dev/null +++ b/cypress/e2e/regression/swaps.cy.js @@ -0,0 +1,175 @@ +import * as constants from '../../support/constants.js' +import * as main from '../pages/main.page.js' +import * as swaps from '../pages/swaps.pages.js' +import * as tx from '../pages/transactions.page.js' +import * as create_tx from '../pages/create_tx.pages.js' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as owner from '../pages/owners.pages' +import * as wallet from '../../support/utils/wallet.js' +import * as swaps_data from '../../fixtures/swaps_data.json' + +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY +const signer2 = walletCredentials.OWNER_3_WALLET_ADDRESS +let staticSafes = [] + +let iframeSelector + +const swapOrder = swaps_data.type.orderDetails + +describe('Swaps tests', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.swapUrl + staticSafes.SEP_STATIC_SAFE_1) + main.waitForHistoryCallToComplete() + wallet.connectSigner(signer) + main.acceptCookies() + iframeSelector = `iframe[src*="${constants.swapWidget}"]` + }) + + // TODO: Waiting for signer connection issue be resolved + it.skip('Verify an order can be created, signed and appear in tx queue', { defaultCommandTimeout: 30000 }, () => { + swaps.acceptLegalDisclaimer() + cy.wait(4000) + main.getIframeBody(iframeSelector).within(() => { + swaps.clickOnSettingsBtn() + swaps.setSlippage('0.30') + swaps.setExpiry('2') + swaps.clickOnSettingsBtn() + swaps.selectInputCurrency(swaps.swapTokens.cow) + swaps.checkTokenBalance(staticSafes.SEP_STATIC_SAFE_1.substring(4), swaps.swapTokens.cow) + swaps.setInputValue(4) + swaps.selectOutputCurrency(swaps.swapTokens.dai) + swaps.checkSwapBtnIsVisible() + swaps.isInputGreaterZero(swaps.outputurrencyInput).then((isGreaterThanZero) => { + cy.wrap(isGreaterThanZero).should('be.true') + }) + swaps.clickOnExceeFeeChkbox() + swaps.clickOnSwapBtn() + swaps.clickOnSwapBtn() + }) + create_tx.changeNonce(12) + tx.selectExecuteLater() + cy.wait(1000) + + create_tx.clickOnSignTransactionBtn() + main.getIframeBody(iframeSelector).within(() => { + swaps.verifyOrderSubmittedConfirmation() + }) + cy.visit(constants.transactionQueueUrl + staticSafes.SEP_STATIC_SAFE_1) + main.verifyElementsCount(create_tx.transactionItem, 1) + create_tx.verifySummaryByName(swapsQueue.contractName, [swapsQueue.action, swapsQueue.oneOfOne]) + cy.visit(constants.transactionQueueUrl + staticSafes.SEP_STATIC_SAFE_1) + owner.waitForConnectionStatus() + main.acceptCookies() + // main.connectSigner(signer) + create_tx.clickOnTransactionItem(0) + create_tx.deleteTx() + main.verifyElementsCount(create_tx.transactionItem, 0) + }) + + it( + 'Verify entering a blocked address in the custom recipient input blocks the form', + { defaultCommandTimeout: 30000 }, + () => { + let isCustomRecipientFound + swaps.acceptLegalDisclaimer() + cy.wait(4000) + main + .getIframeBody(iframeSelector) + .then(($frame) => { + isCustomRecipientFound = (customRecipient) => { + const element = $frame.find(customRecipient) + return element.length > 0 + } + }) + .within(() => { + swaps.clickOnSettingsBtn() + swaps.enableCustomRecipient(isCustomRecipientFound(swaps.customRecipient)) + swaps.clickOnSettingsBtn() + swaps.enterRecipient(swaps.blockedAddress) + }) + cy.contains(swaps.blockedAddressStr) + }, + ) + + it('Verify enabling custom recipient adds that field to the form', { defaultCommandTimeout: 30000 }, () => { + swaps.acceptLegalDisclaimer() + cy.wait(4000) + + const isCustomRecipientFound = ($frame, customRecipient) => { + const element = $frame.find(customRecipient) + return element.length > 0 + } + + main.getIframeBody(iframeSelector).then(($frame) => { + cy.wrap($frame).within(() => { + swaps.clickOnSettingsBtn() + + if (isCustomRecipientFound($frame, swaps.customRecipient)) { + swaps.disableCustomRecipient(true) + cy.wait(1000) + swaps.enableCustomRecipient(!isCustomRecipientFound($frame, swaps.customRecipient)) + } else { + swaps.enableCustomRecipient(isCustomRecipientFound($frame, swaps.customRecipient)) + cy.wait(1000) + } + + swaps.clickOnSettingsBtn() + swaps.enterRecipient('1') + }) + }) + }) + + it('Verify order deails are displayed in swap confirmation', { defaultCommandTimeout: 30000 }, () => { + const limitPrice = swaps.createRegex(swapOrder.DAIeqCOW, 'COW') + const widgetFee = swaps.getWidgetFee() + const orderID = swaps.getOrderID() + const slippage = swaps.getWidgetFee() + + swaps.acceptLegalDisclaimer() + cy.wait(4000) + main.getIframeBody(iframeSelector).within(() => { + swaps.clickOnSettingsBtn() + swaps.setSlippage('0.30') + swaps.setExpiry('2') + swaps.clickOnSettingsBtn() + swaps.setInputValue(4) + swaps.checkSwapBtnIsVisible() + swaps.isInputGreaterZero(swaps.outputurrencyInput).then((isGreaterThanZero) => { + cy.wrap(isGreaterThanZero).should('be.true') + }) + swaps.clickOnExceeFeeChkbox() + swaps.clickOnSwapBtn() + swaps.clickOnSwapBtn() + }) + + swaps.verifyOrderDetails(limitPrice, swapOrder.expiry2Mins, slippage, swapOrder.interactWith, orderID, widgetFee) + }) + + it( + 'Verify recipient address alert is displayed in order details if the recipient is not owner of the order', + { defaultCommandTimeout: 30000 }, + () => { + const limitPrice = swaps.createRegex(swapOrder.DAIeqCOW, 'COW') + const widgetFee = swaps.getWidgetFee() + const orderID = swaps.getOrderID() + + swaps.acceptLegalDisclaimer() + cy.wait(4000) + main.getIframeBody(iframeSelector).within(() => { + swaps.setInputValue(4) + swaps.checkSwapBtnIsVisible() + swaps.enterRecipient(signer2) + swaps.clickOnExceeFeeChkbox() + swaps.clickOnSwapBtn() + swaps.clickOnSwapBtn() + }) + swaps.verifyRecipientAlertIsDisplayed() + }, + ) +}) diff --git a/cypress/e2e/regression/swaps_history.cy.js b/cypress/e2e/regression/swaps_history.cy.js new file mode 100644 index 0000000000..95d4ffb527 --- /dev/null +++ b/cypress/e2e/regression/swaps_history.cy.js @@ -0,0 +1,41 @@ +import * as constants from '../../support/constants.js' +import * as main from '../pages/main.page.js' +import * as create_tx from '../pages/create_tx.pages.js' +import * as swaps_data from '../../fixtures/swaps_data.json' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' + +let staticSafes = [] + +const swapsHistory = swaps_data.type.history +const limitOrder = + '&id=multisig_0x8f4A19C85b39032A37f7a6dCc65234f966F72551_0x3faf510142c9ade7ac2a701fb697b95f321fd51f5eb9b17e7e534a8abe472b07' +const limitOrderSafe = 'sep:0x8f4A19C85b39032A37f7a6dCc65234f966F72551' + +describe('Swaps history tests', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.transactionsHistoryUrl + staticSafes.SEP_STATIC_SAFE_1) + main.acceptCookies() + }) + + it('Verify swap selling operation with one action', { defaultCommandTimeout: 30000 }, () => { + create_tx.clickOnTransactionItemByName('14') + create_tx.verifyExpandedDetails([ + swapsHistory.sellOrder, + swapsHistory.sell, + swapsHistory.oneCOW, + swapsHistory.forAtLeast, + swapsHistory.dai, + swapsHistory.filled, + ]) + }) + + it('Verify "Partially filled" field is displayed in limit order', () => { + cy.visit(constants.transactionUrl + limitOrderSafe + limitOrder) + create_tx.verifyExpandedDetails([swapsHistory.partiallyFilled]) + }) +}) diff --git a/cypress/e2e/regression/swaps_history_2.cy.js b/cypress/e2e/regression/swaps_history_2.cy.js new file mode 100644 index 0000000000..1f97be155a --- /dev/null +++ b/cypress/e2e/regression/swaps_history_2.cy.js @@ -0,0 +1,155 @@ +import * as constants from '../../support/constants.js' +import * as main from '../pages/main.page.js' +import * as create_tx from '../pages/create_tx.pages.js' +import * as swaps_data from '../../fixtures/swaps_data.json' +import * as swaps from '../pages/swaps.pages.js' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' + +let staticSafes = [] + +const swapsHistory = swaps_data.type.history + +describe('Swaps history tests 2', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.clearLocalStorage() + }) + + it('Verify swap sell order with one action', { defaultCommandTimeout: 30000 }, () => { + cy.visit(constants.transactionUrl + staticSafes.SEP_STATIC_SAFE_1 + swaps.swapTxs.sell1Action) + main.acceptCookies() + + const dai = swaps.createRegex(swapsHistory.forAtLeastFullDai, 'DAI') + const eq = swaps.createRegex(swapsHistory.DAIeqCOW, 'COW') + + create_tx.verifyExpandedDetails([ + swapsHistory.sellFull, + dai, + eq, + swapsHistory.dai, + swapsHistory.filled, + swapsHistory.gGpV2, + ]) + }) + + it('Verify swap buy operation with 2 actions: approve & swap', { defaultCommandTimeout: 30000 }, () => { + cy.visit(constants.transactionUrl + staticSafes.SEP_STATIC_SAFE_1 + swaps.swapTxs.buy2actions) + main.acceptCookies() + + const eq = swaps.createRegex(swapsHistory.oneGNOFull, 'COW') + const atMost = swaps.createRegex(swapsHistory.forAtMostCow, 'COW') + + create_tx.verifyExpandedDetails([ + swapsHistory.buyOrder, + swapsHistory.buy, + eq, + atMost, + swapsHistory.cow, + swapsHistory.expired, + swapsHistory.actionApprove, + swapsHistory.actionPreSignature, + ]) + }) + + it('Verify "Cancelled" status for manually cancelled limit orders', { defaultCommandTimeout: 30000 }, () => { + const safe = '0x2a73e61bd15b25B6958b4DA3bfc759ca4db249b9' + cy.visit(constants.transactionUrl + safe + swaps.swapTxs.sellCancelled) + main.acceptCookies() + + const uni = swaps.createRegex(swapsHistory.forAtLeastFullUni, 'UNI') + const eq = swaps.createRegex(swapsHistory.UNIeqCOW, 'K COW') + + create_tx.verifyExpandedDetails([ + swapsHistory.sellOrder, + swapsHistory.sell, + uni, + eq, + swapsHistory.cow, + swapsHistory.cancelled, + swapsHistory.gGpV2, + ]) + }) + + it('Verify swap operation with 3 actions: wrap & approve & swap', { defaultCommandTimeout: 30000 }, () => { + const safe = '0x140663Cb76e4c4e97621395fc118912fa674150B' + cy.visit(constants.transactionUrl + safe + swaps.swapTxs.sell3Actions) + main.acceptCookies() + + const dai = swaps.createRegex(swapsHistory.forAtLeastFullDai, 'DAI') + const eq = swaps.createRegex(swapsHistory.DAIeqWETH, 'WETH') + + create_tx.verifyExpandedDetails([ + swapsHistory.sellOrder, + swapsHistory.sell, + dai, + eq, + swapsHistory.actionApproveEth, + swapsHistory.actionPreSignature, + swapsHistory.actionDepositEth, + ]) + }) + + it('Verify "Expired" field in the tx details for limit orders', { defaultCommandTimeout: 30000 }, () => { + cy.visit(constants.transactionUrl + staticSafes.SEP_STATIC_SAFE_1 + swaps.swapTxs.sellLimitOrder) + main.acceptCookies() + + const dai = swaps.createRegex(swapsHistory.forAtLeastFullDai, 'DAI') + const eq = swaps.createRegex(swapsHistory.DAIeqCOW, 'COW') + + create_tx.verifyExpandedDetails([swapsHistory.sellOrder, swapsHistory.sell, dai, eq, swapsHistory.expired]) + }) + + it('Verify "Filled" field in the tx details for limit orders', { defaultCommandTimeout: 30000 }, () => { + cy.visit(constants.transactionUrl + swaps.limitOrderSafe + swaps.swapTxs.sellLimitOrderFilled) + main.acceptCookies() + + const usdc = swaps.createRegex(swapsHistory.forAtLeastFullUSDT, 'USDT') + const eq = swaps.createRegex(swapsHistory.USDTeqUSDC, 'USDC') + + create_tx.verifyExpandedDetails([swapsHistory.sellOrder, swapsHistory.sell, usdc, eq, swapsHistory.filled]) + }) + + it( + 'Verify no decoding if tx was created using CowSwap safe-app in the history', + { defaultCommandTimeout: 30000 }, + () => { + cy.visit(constants.transactionUrl + staticSafes.SEP_STATIC_SAFE_1 + swaps.swapTxs.safeAppSwapOrder) + main.acceptCookies() + main.verifyValuesDoNotExist('div', [ + swapsHistory.actionApproveG, + swapsHistory.actionDepositG, + swapsHistory.amount, + swapsHistory.executionPrice, + swapsHistory.surplus, + swapsHistory.expiry, + swapsHistory.oderId, + swapsHistory.status, + swapsHistory.forAtLeast, + swapsHistory.forAtMost, + ]) + main.verifyValuesDoNotExist(create_tx.transactionItem, [swapsHistory.title, swapsHistory.cow, swapsHistory.dai]) + main.verifyValuesExist(create_tx.transactionItem, [swapsHistory.actionPreSignatureG, swapsHistory.safeAppTitile]) + }, + ) + + it('Verify token order in sell and buy operations', { defaultCommandTimeout: 30000 }, () => { + cy.visit(constants.transactionUrl + staticSafes.SEP_STATIC_SAFE_1 + swaps.swapTxs.sell1Action) + main.acceptCookies() + const eq = swaps.createRegex(swapsHistory.DAIeqCOW, 'COW') + swaps.checkTokenOrder(eq, swapsHistory.executionPrice) + + cy.visit(constants.transactionUrl + staticSafes.SEP_STATIC_SAFE_1 + swaps.swapTxs.buy2actions) + main.acceptCookies() + const eq2 = swaps.createRegex(swapsHistory.oneGNOFull, 'COW') + swaps.checkTokenOrder(eq2, swapsHistory.limitPrice) + }) + + it('Verify OrderID url on cowswap explorer', { defaultCommandTimeout: 30000 }, () => { + cy.visit(constants.transactionUrl + staticSafes.SEP_STATIC_SAFE_1 + swaps.swapTxs.sell1Action) + main.acceptCookies() + swaps.verifyOrderIDUrl() + }) +}) diff --git a/cypress/e2e/regression/swaps_tokens.cy.js b/cypress/e2e/regression/swaps_tokens.cy.js new file mode 100644 index 0000000000..b642b8f154 --- /dev/null +++ b/cypress/e2e/regression/swaps_tokens.cy.js @@ -0,0 +1,48 @@ +import * as constants from '../../support/constants.js' +import * as main from '../pages/main.page.js' +import * as swaps from '../pages/swaps.pages.js' +import * as assets from '../pages/assets.pages.js' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' + +let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY + +let iframeSelector = `iframe[src*="${constants.swapWidget}"]` + +describe('[SMOKE] Swaps token tests', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_1) + main.acceptCookies() + }) + + it( + 'Verify that clicking the swap from assets tab, autofills that token automatically in the form', + { defaultCommandTimeout: 30000 }, + () => { + wallet.connectSigner(signer) + assets.selectTokenList(assets.tokenListOptions.allTokens) + + swaps.clickOnAssetSwapBtn(0) + swaps.acceptLegalDisclaimer() + cy.wait(2000) + main.getIframeBody(iframeSelector).within(() => { + swaps.verifySelectedInputCurrancy(swaps.swapTokens.eth) + }) + }, + ) + + it('Verify swap button are displayed in assets table and dashboard', () => { + assets.selectTokenList(assets.tokenListOptions.allTokens) + main.verifyElementsCount(swaps.assetsSwapBtn, 4) + cy.visit(constants.homeUrl + staticSafes.SEP_STATIC_SAFE_1) + main.verifyElementsCount(swaps.assetsSwapBtn, 4) + main.verifyElementsCount(swaps.dashboardSwapBtn, 1) + }) +}) diff --git a/cypress/e2e/regression/tokens.cy.js b/cypress/e2e/regression/tokens.cy.js index 0ffdd593c8..e941adeca3 100644 --- a/cypress/e2e/regression/tokens.cy.js +++ b/cypress/e2e/regression/tokens.cy.js @@ -1,7 +1,6 @@ import * as constants from '../../support/constants' import * as main from '../pages/main.page' import * as assets from '../pages/assets.pages' -import * as owner from '../pages/owners.pages' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' const ASSET_NAME_COLUMN = 0 @@ -93,10 +92,10 @@ describe('Tokens tests', () => { it('Verify the default Fiat currency and the effects after changing it', () => { assets.selectTokenList(assets.tokenListOptions.allTokens) assets.verifyFirstRowDoesNotContainCurrency(assets.currencyEUR, FIAT_AMOUNT_COLUMN) - assets.verifyFirstRowContainsCurrency(assets.currencyUSD, FIAT_AMOUNT_COLUMN) + assets.verifyFirstRowContainsCurrency(assets.currency$, FIAT_AMOUNT_COLUMN) assets.clickOnCurrencyDropdown() - assets.selectCurrency(assets.currencyEUR) - assets.verifyFirstRowDoesNotContainCurrency(assets.currencyUSD, FIAT_AMOUNT_COLUMN) + assets.selectCurrency(assets.currencyOptionEUR) + assets.verifyFirstRowDoesNotContainCurrency(assets.currency$, FIAT_AMOUNT_COLUMN) assets.verifyFirstRowContainsCurrency(assets.currencyEUR, FIAT_AMOUNT_COLUMN) }) @@ -173,10 +172,6 @@ describe('Tokens tests', () => { //Include in smoke. it('Verify that when owner is disconnected, Send button is disabled', () => { - //waits for the user to look connected. Sends a default prefix "sep:" if it is called with no params - main.verifyOwnerConnected() - owner.clickOnWalletExpandMoreIcon() - owner.clickOnDisconnectBtn() assets.selectTokenList(assets.tokenListOptions.allTokens) assets.showSendBtn(0) assets.VerifySendButtonIsDisabled() diff --git a/cypress/e2e/safe-apps/info_modal.cy.js b/cypress/e2e/safe-apps/info_modal.cy.js index f89febd702..3a361ebbf5 100644 --- a/cypress/e2e/safe-apps/info_modal.cy.js +++ b/cypress/e2e/safe-apps/info_modal.cy.js @@ -21,6 +21,7 @@ describe('Info modal tests', () => { it('Verify the disclaimer is displayed when a Safe App is opened', () => { safeapps.clickOnApp(safeapps.transactionBuilderStr) safeapps.clickOnOpenSafeAppBtn() + safeapps.verifyDisclaimerIsDisplayed() }) // Skip tests due to changed logic diff --git a/cypress/e2e/safe-apps/permissions_settings.cy.js b/cypress/e2e/safe-apps/permissions_settings.cy.js index d332f11550..53df8b6ce4 100644 --- a/cypress/e2e/safe-apps/permissions_settings.cy.js +++ b/cypress/e2e/safe-apps/permissions_settings.cy.js @@ -1,6 +1,6 @@ -import * as constants from '../../support/constants' -import * as main from '../pages/main.page' -import * as safeapps from '../pages/safeapps.pages' +import * as constants from '../../support/constants.js' +import * as main from '../pages/main.page.js' +import * as safeapps from '../pages/safeapps.pages.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' let $dapps, @@ -8,7 +8,8 @@ let $dapps, const app1 = 'https://app1.com' const app3 = 'https://app3.com' -describe('Permissions settings tests', () => { +// TODO: Skip until connection error is resolved +describe.skip('Permissions settings tests', () => { before(() => { getSafes(CATEGORIES.static).then((statics) => { staticSafes = statics diff --git a/cypress/e2e/smoke/add_owner.cy.js b/cypress/e2e/smoke/add_owner.cy.js index 8d3fb799e4..4539f4cb49 100644 --- a/cypress/e2e/smoke/add_owner.cy.js +++ b/cypress/e2e/smoke/add_owner.cy.js @@ -2,9 +2,12 @@ import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' import * as owner from '../pages/owners.pages' import * as navigation from '../pages/navigation.page' +import * as wallet from '../../support/utils/wallet.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('[SMOKE] Add Owners tests', () => { before(async () => { @@ -19,19 +22,9 @@ describe('[SMOKE] Add Owners tests', () => { main.verifyElementsExist([navigation.setupSection]) }) - it('[SMOKE] Verify the presence of "Add Owner" button', () => { - owner.verifyAddOwnerBtnIsEnabled() - }) - - it('[SMOKE] Verify “Add new owner” button tooltip displays correct message for Non-Owner', () => { - cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_3) - main.waitForHistoryCallToComplete() - owner.verifyAddOwnerBtnIsDisabled() - }) - // TODO: Check if this test is covered with unit tests it('[SMOKE] Verify relevant error messages are displayed in Address input', () => { - owner.waitForConnectionStatus() + wallet.connectSigner(signer) owner.openAddOwnerWindow() owner.typeOwnerAddress(main.generateRandomString(10)) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidFormat) @@ -49,15 +42,26 @@ describe('[SMOKE] Add Owners tests', () => { owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.alreadyAdded) }) + it('[SMOKE] Verify the presence of "Add Owner" button', () => { + wallet.connectSigner(signer) + owner.verifyAddOwnerBtnIsEnabled() + }) + + it('[SMOKE] Verify “Add new owner” button is disabled for Non-Owner', () => { + cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_3) + main.waitForHistoryCallToComplete() + owner.verifyAddOwnerBtnIsDisabled() + }) + it('[SMOKE] Verify default threshold value. Verify correct threshold calculation', () => { - owner.waitForConnectionStatus() + wallet.connectSigner(signer) owner.openAddOwnerWindow() owner.typeOwnerAddress(constants.DEFAULT_OWNER_ADDRESS) owner.verifyThreshold(1, 2) }) it('[SMOKE] Verify valid Address validation', () => { - owner.waitForConnectionStatus() + wallet.connectSigner(signer) owner.openAddOwnerWindow() owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) owner.clickOnNextBtn() diff --git a/cypress/e2e/smoke/address_book.cy.js b/cypress/e2e/smoke/address_book.cy.js index 8be8923ffa..842369c382 100644 --- a/cypress/e2e/smoke/address_book.cy.js +++ b/cypress/e2e/smoke/address_book.cy.js @@ -2,6 +2,7 @@ import 'cypress-file-upload' import * as constants from '../../support/constants' import * as addressBook from '../../e2e/pages/address_book.page' import * as main from '../../e2e/pages/main.page' +import * as wallet from '../../support/utils/wallet.js' import * as ls from '../../support/localstorage_data.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' @@ -14,6 +15,8 @@ const duplicateEntry = 'test-sepolia-90' const owner1 = 'Automation owner' const recipientData = [owner1, constants.DEFAULT_OWNER_ADDRESS] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('[SMOKE] Address book tests', () => { before(async () => { @@ -104,6 +107,7 @@ describe('[SMOKE] Address book tests', () => { main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.sepoliaAddress2) cy.wait(1000) cy.reload() + wallet.connectSigner(signer) addressBook.clickOnSendBtn() addressBook.verifyRecipientData(recipientData) }) diff --git a/cypress/e2e/smoke/assets.cy.js b/cypress/e2e/smoke/assets.cy.js index fc9562cd4b..368b266f36 100644 --- a/cypress/e2e/smoke/assets.cy.js +++ b/cypress/e2e/smoke/assets.cy.js @@ -2,12 +2,12 @@ import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' import * as assets from '../pages/assets.pages' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] -const ASSET_NAME_COLUMN = 0 -const TOKEN_AMOUNT_COLUMN = 1 -const FIAT_AMOUNT_COLUMN = 2 +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('[SMOKE] Assets tests', () => { const fiatRegex = assets.fiatRegex @@ -22,14 +22,14 @@ describe('[SMOKE] Assets tests', () => { main.acceptCookies() }) - it('[SMOKE] Verify that the token tab is selected by default and the table is visible', () => { - assets.verifyTokensTabIsSelected('true') - }) - it('[SMOKE] Verify that the native token is visible', () => { assets.verifyTokenIsPresent(constants.tokenNames.sepoliaEther) }) + it('[SMOKE] Verify that the token tab is selected by default and the table is visible', () => { + assets.verifyTokensTabIsSelected('true') + }) + it('[SMOKE] Verify that Token list dropdown down options show/hide spam tokens', () => { let spamTokens = [ assets.currencyAave, @@ -53,6 +53,7 @@ describe('[SMOKE] Assets tests', () => { }) it('[SMOKE] Verify that clicking the button with an owner opens the Send funds form', () => { + wallet.connectSigner(signer) assets.selectTokenList(assets.tokenListOptions.allTokens) assets.clickOnSendBtn(0) }) diff --git a/cypress/e2e/smoke/batch_tx.cy.js b/cypress/e2e/smoke/batch_tx.cy.js index 01a5f1a929..93f459c7ec 100644 --- a/cypress/e2e/smoke/batch_tx.cy.js +++ b/cypress/e2e/smoke/batch_tx.cy.js @@ -1,9 +1,9 @@ import * as batch from '../pages/batches.pages' import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' -import * as owner from '../../e2e/pages/owners.pages.js' import * as ls from '../../support/localstorage_data.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] @@ -11,6 +11,9 @@ const currentNonce = 3 const funds_first_tx = '0.001' const funds_second_tx = '0.002' +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY + describe('[SMOKE] Batch transaction tests', () => { before(async () => { staticSafes = await getSafes(CATEGORIES.static) @@ -19,16 +22,17 @@ describe('[SMOKE] Batch transaction tests', () => { beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_2) - owner.waitForConnectionStatus() main.acceptCookies() }) it('[SMOKE] Verify empty batch list can be opened', () => { + wallet.connectSigner(signer) batch.openBatchtransactionsModal() batch.openNewTransactionModal() }) it('[SMOKE] Verify a transaction can be added to the batch', () => { + wallet.connectSigner(signer) batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) cy.contains(batch.transactionAddedToBatchStr).should('be.visible') batch.verifyBatchIconCount(1) @@ -43,6 +47,7 @@ describe('[SMOKE] Batch transaction tests', () => { .then(() => { cy.reload() batch.clickOnBatchCounter() + wallet.connectSigner(signer) batch.clickOnConfirmBatchBtn() batch.verifyBatchTransactionsCount(2) batch.clickOnBatchCounter() @@ -59,6 +64,7 @@ describe('[SMOKE] Batch transaction tests', () => { .then(() => { cy.reload() batch.clickOnBatchCounter() + wallet.connectSigner(signer) cy.contains(batch.batchedTransactionsStr).should('be.visible').parents('aside').find('ul > li').as('BatchList') cy.get('@BatchList').find(batch.deleteTransactionbtn).eq(0).click() cy.get('@BatchList').should('have.length', 1) diff --git a/cypress/e2e/smoke/create_safe_cf.cy.js b/cypress/e2e/smoke/create_safe_cf.cy.js index 4d4d775159..6eba4ed3e8 100644 --- a/cypress/e2e/smoke/create_safe_cf.cy.js +++ b/cypress/e2e/smoke/create_safe_cf.cy.js @@ -2,6 +2,10 @@ import * as constants from '../../support/constants' import * as main from '../pages/main.page' import * as createwallet from '../pages/create_wallet.pages' import * as owner from '../pages/owners.pages' +import * as wallet from '../../support/utils/wallet.js' + +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('[SMOKE] CF Safe creation tests', () => { beforeEach(() => { @@ -10,6 +14,7 @@ describe('[SMOKE] CF Safe creation tests', () => { main.acceptCookies() }) it('[SMOKE] CF creation happy path', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() @@ -17,8 +22,6 @@ describe('[SMOKE] CF Safe creation tests', () => { createwallet.clickOnNextBtn() createwallet.selectPayLaterOption() createwallet.clickOnReviewStepNextBtn() - createwallet.verifyNewSafeDialogModal() - createwallet.clickOnGotitBtn() createwallet.verifyCFSafeCreated() }) }) diff --git a/cypress/e2e/smoke/create_safe_simple.cy.js b/cypress/e2e/smoke/create_safe_simple.cy.js index 2d86511c8d..3b749b1e88 100644 --- a/cypress/e2e/smoke/create_safe_simple.cy.js +++ b/cypress/e2e/smoke/create_safe_simple.cy.js @@ -2,6 +2,10 @@ import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' import * as createwallet from '../pages/create_wallet.pages' import * as owner from '../pages/owners.pages' +import * as wallet from '../../support/utils/wallet.js' + +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('[SMOKE] Safe creation tests', () => { beforeEach(() => { @@ -10,17 +14,18 @@ describe('[SMOKE] Safe creation tests', () => { main.acceptCookies() }) it('[SMOKE] Verify a Wallet can be connected', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() owner.clickOnWalletExpandMoreIcon() owner.clickOnDisconnectBtn() - createwallet.clickOnConnectWalletBtn() - createwallet.connectWallet() + wallet.connectSigner(signer) owner.waitForConnectionStatus() }) it('[SMOKE] Verify that a new Wallet has default name related to the selected network', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() @@ -28,6 +33,7 @@ describe('[SMOKE] Safe creation tests', () => { }) it('[SMOKE] Verify Add and Remove Owner Row works as expected', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() @@ -43,6 +49,7 @@ describe('[SMOKE] Safe creation tests', () => { }) it('[SMOKE] Verify Threshold Setup', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() diff --git a/cypress/e2e/smoke/create_tx.cy.js b/cypress/e2e/smoke/create_tx.cy.js index fda40e71fd..0a7dd1c6f7 100644 --- a/cypress/e2e/smoke/create_tx.cy.js +++ b/cypress/e2e/smoke/create_tx.cy.js @@ -2,18 +2,14 @@ import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' import * as createtx from '../../e2e/pages/create_tx.pages' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] -const sendValue = 0.00002 const currentNonce = 5 -function happyPathToStepTwo() { - createtx.typeRecipientAddress(constants.EOA) - createtx.clickOnTokenselectorAndSelectSepoliaEth() - createtx.setSendValue(sendValue) - createtx.clickOnNextBtn() -} +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('[SMOKE] Create transactions tests', () => { before(async () => { @@ -24,10 +20,16 @@ describe('[SMOKE] Create transactions tests', () => { cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_10) main.acceptCookies() + wallet.connectSigner(signer) createtx.clickOnNewtransactionBtn() createtx.clickOnSendTokensBtn() }) + it('[SMOKE] Verify MaxAmount button', () => { + createtx.setMaxAmount() + createtx.verifyMaxAmount(constants.tokenNames.sepoliaEther, constants.tokenAbbreviation.sep) + }) + it('[SMOKE] Verify error messages for invalid address input', () => { createtx.verifyRandomStringAddress('Lorem Ipsum') createtx.verifyWrongChecksum(constants.WRONGLY_CHECKSUMMED_ADDRESS) @@ -43,11 +45,6 @@ describe('[SMOKE] Create transactions tests', () => { createtx.verifyAmountLargerThanCurrentBalance() }) - it('[SMOKE] Verify MaxAmount button', () => { - createtx.setMaxAmount() - createtx.verifyMaxAmount(constants.tokenNames.sepoliaEther, constants.tokenAbbreviation.sep) - }) - it('[SMOKE] Verify nonce tooltip warning messages', () => { createtx.changeNonce(0) createtx.verifyTooltipMessage(constants.nonceTooltipMsg.lowerThanCurrent + currentNonce.toString()) @@ -56,24 +53,4 @@ describe('[SMOKE] Create transactions tests', () => { createtx.changeNonce(currentNonce + 150) createtx.verifyTooltipMessage(constants.nonceTooltipMsg.muchHigherThanRecommended) }) - - it.skip('[SMOKE] Verify advance parameters gas limit input', () => { - happyPathToStepTwo() - createtx.changeNonce(currentNonce) - createtx.selectCurrentWallet() - createtx.openExecutionParamsModal() - createtx.verifyAndSubmitExecutionParams() - }) - - it.skip('[SMOKE] Verify a transaction shows relayer and addToBatch button', () => { - happyPathToStepTwo() - createtx.verifySubmitBtnIsEnabled() - createtx.verifyNativeTokenTransfer() - createtx.changeNonce(currentNonce) - createtx.verifyConfirmTransactionData() - createtx.verifyRelayerAttemptsAvailable() - createtx.selectCurrentWallet() - createtx.clickOnNoLaterOption() - createtx.verifyAddToBatchBtnIsEnabled() - }) }) diff --git a/cypress/e2e/smoke/create_tx_2.cy.js b/cypress/e2e/smoke/create_tx_2.cy.js new file mode 100644 index 0000000000..7030235f60 --- /dev/null +++ b/cypress/e2e/smoke/create_tx_2.cy.js @@ -0,0 +1,54 @@ +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' +import * as wallet from '../../support/utils/wallet.js' + +let staticSafes = [] + +const sendValue = 0.00002 + +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY + +function happyPathToStepTwo() { + createtx.typeRecipientAddress(constants.EOA) + createtx.clickOnTokenselectorAndSelectSepoliaEth() + createtx.setSendValue(sendValue) + createtx.clickOnNextBtn() +} + +describe('[SMOKE] Create transactions tests 2', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_6) + main.acceptCookies() + wallet.connectSigner(signer) + createtx.clickOnNewtransactionBtn() + createtx.clickOnSendTokensBtn() + }) + + it('[SMOKE] Verify advance parameters gas limit input', () => { + happyPathToStepTwo() + createtx.changeNonce('1') + createtx.selectCurrentWallet() + createtx.openExecutionParamsModal() + createtx.verifyAndSubmitExecutionParams() + }) + + it('[SMOKE] Verify a transaction shows relayer and addToBatch button', () => { + happyPathToStepTwo() + createtx.verifySubmitBtnIsEnabled() + createtx.verifyNativeTokenTransfer() + createtx.changeNonce('1') + createtx.verifyConfirmTransactionData() + createtx.verifyRelayerAttemptsAvailable() + createtx.selectCurrentWallet() + createtx.clickOnNoLaterOption() + createtx.verifyAddToBatchBtnIsEnabled() + }) +}) diff --git a/cypress/e2e/smoke/import_export_data_2.cy.js b/cypress/e2e/smoke/import_export_data_2.cy.js index 675ace9b87..7aa5ee3dca 100644 --- a/cypress/e2e/smoke/import_export_data_2.cy.js +++ b/cypress/e2e/smoke/import_export_data_2.cy.js @@ -4,6 +4,7 @@ import * as main from '../pages/main.page.js' import * as constants from '../../support/constants.js' import * as sidebar from '../pages/sidebar.pages.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] @@ -14,6 +15,9 @@ const invalidJsonPath_3 = 'cypress/fixtures/test-empty-batch.json' const appNames = ['Transaction Builder'] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY + describe('[SMOKE] Import Export Data tests 2', () => { before(async () => { staticSafes = await getSafes(CATEGORIES.static) @@ -26,11 +30,13 @@ describe('[SMOKE] Import Export Data tests 2', () => { }) it('[SMOKE] Verify that the Sidebar Import button opens an import modal', () => { + wallet.connectSigner(signer) sidebar.openSidebar() sidebar.clickOnSidebarImportBtn() }) it('[SMOKE] Verify that correctly formatted json file can be uploaded and shows data', () => { + wallet.connectSigner(signer) sidebar.openSidebar() sidebar.clickOnSidebarImportBtn() file.dragAndDropFile(validJsonPath) @@ -44,6 +50,7 @@ describe('[SMOKE] Import Export Data tests 2', () => { }) it('[SMOKE] Verify that only json files can be imported', () => { + wallet.connectSigner(signer) sidebar.openSidebar() sidebar.clickOnSidebarImportBtn() file.dragAndDropFile(invalidJsonPath) @@ -58,6 +65,7 @@ describe('[SMOKE] Import Export Data tests 2', () => { }) it('[SMOKE] Verify that json files with wrong information are rejected', () => { + wallet.connectSigner(signer) sidebar.openSidebar() sidebar.clickOnSidebarImportBtn() file.dragAndDropFile(invalidJsonPath_3) diff --git a/cypress/e2e/smoke/messages_offchain.cy.js b/cypress/e2e/smoke/messages_offchain.cy.js new file mode 100644 index 0000000000..78839b83b6 --- /dev/null +++ b/cypress/e2e/smoke/messages_offchain.cy.js @@ -0,0 +1,93 @@ +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 * as msg_data from '../../fixtures/txmessages_data.json' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as modal from '../pages/modals.page' +import * as messages from '../pages/messages.pages.js' +import * as msg_confirmation_modal from '../pages/modals/message_confirmation.pages.js' +import * as wallet from '../../support/utils/wallet.js' + +let staticSafes = [] +const offchainMessage = 'Test message 2 off-chain' + +const typeMessagesGeneral = msg_data.type.general +const typeMessagesOffchain = msg_data.type.offChain + +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY + +describe('[SMOKE] Offchain Messages tests', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.transactionsMessagesUrl + staticSafes.SEP_STATIC_SAFE_10) + main.acceptCookies() + }) + + it('[SMOKE] Verify summary for off-chain unsigned messages', () => { + createTx.verifySummaryByIndex(0, [ + typeMessagesGeneral.sign, + typeMessagesGeneral.oneOftwo, + typeMessagesOffchain.testMessage1, + ]) + createTx.verifySummaryByIndex(2, [ + typeMessagesGeneral.sign, + typeMessagesGeneral.oneOftwo, + typeMessagesOffchain.testMessage2, + ]) + }) + + it('[SMOKE] Verify summary for off-chain signed messages', () => { + createTx.verifySummaryByIndex(1, [ + typeMessagesGeneral.confirmed, + typeMessagesGeneral.twoOftwo, + typeMessagesOffchain.name, + ]) + createTx.verifySummaryByIndex(3, [ + typeMessagesGeneral.confirmed, + typeMessagesGeneral.twoOftwo, + typeMessagesOffchain.testMessage3, + ]) + }) + + it('[SMOKE] Verify exapanded details for EIP 191 off-chain message', () => { + createTx.clickOnTransactionItemByIndex(2) + cy.contains(typeMessagesOffchain.message2).should('be.visible') + }) + + it('[SMOKE] Verify exapanded details for EIP 712 off-chain message', () => { + const jsonString = createTx.messageNestedStr + const values = [ + typeMessagesOffchain.name, + typeMessagesOffchain.testStringNested, + typeMessagesOffchain.EIP712Domain, + typeMessagesOffchain.message3, + ] + + createTx.clickOnTransactionItemByIndex(1) + cy.get(createTx.txRowTitle) + .next() + .then(($section) => { + expect($section.text()).to.include(jsonString) + const count = $section.text().split(jsonString).length - 1 + expect(count).to.eq(3) + }) + + main.verifyTextVisibility(values) + }) + + it('[SMOKE] Verify confirmation window is displayed for unsigned message', () => { + wallet.connectSigner(signer) + messages.clickOnMessageSignBtn(2) + msg_confirmation_modal.verifyConfirmationWindowTitle(modal.modalTitiles.confirmMsg) + msg_confirmation_modal.verifyMessagePresent(offchainMessage) + msg_confirmation_modal.clickOnMessageDetails() + msg_confirmation_modal.verifyOffchainMessageHash(0) + msg_confirmation_modal.verifyOffchainMessageHash(1) + msg_confirmation_modal.checkMessageInfobox() + }) +}) diff --git a/cypress/e2e/smoke/replace_owner.cy.js b/cypress/e2e/smoke/replace_owner.cy.js index 60dd4e5b84..ca931b8956 100644 --- a/cypress/e2e/smoke/replace_owner.cy.js +++ b/cypress/e2e/smoke/replace_owner.cy.js @@ -2,8 +2,11 @@ import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' import * as owner from '../pages/owners.pages' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('[SMOKE] Replace Owners tests', () => { before(async () => { @@ -18,17 +21,17 @@ describe('[SMOKE] Replace Owners tests', () => { }) it('[SMOKE] Verify that "Replace" icon is visible', () => { + wallet.connectSigner(signer) owner.verifyReplaceBtnIsEnabled() }) - // TODO: Remove "tooltip" from title - it('[SMOKE] Verify Tooltip displays correct message for Non-Owner', () => { + it('[SMOKE] Verify owner replace button is disabled for Non-Owner', () => { cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_3) - owner.waitForConnectionStatus() owner.verifyReplaceBtnIsDisabled() }) it('[SMOKE] Verify that the owner replacement form is opened', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() owner.openReplaceOwnerWindow() }) diff --git a/cypress/e2e/smoke/spending_limits.cy.js b/cypress/e2e/smoke/spending_limits.cy.js index ad815a8f39..63451967e1 100644 --- a/cypress/e2e/smoke/spending_limits.cy.js +++ b/cypress/e2e/smoke/spending_limits.cy.js @@ -3,8 +3,11 @@ import * as main from '../pages/main.page' import * as spendinglimit from '../pages/spending_limits.pages' import * as owner from '../pages/owners.pages' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('[SMOKE] Spending limits tests', () => { before(async () => { @@ -12,9 +15,10 @@ describe('[SMOKE] Spending limits tests', () => { }) beforeEach(() => { - cy.visit(constants.securityUrl + staticSafes.SEP_STATIC_SAFE_8) + cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_8) cy.clearLocalStorage() main.acceptCookies() + wallet.connectSigner(signer) owner.waitForConnectionStatus() cy.get(spendinglimit.spendingLimitsSection).should('be.visible') spendinglimit.clickOnNewSpendingLimitBtn() 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..44d5b3dd1b --- /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 incoming 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 outgoing date range filter is set to only one day with no transactions, it returns no results', () => { + const params = { + transactionType: txType_outgoing, + startDate: '2024-07-16T23: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/fixtures/safes/static.json b/cypress/fixtures/safes/static.json index 14e88c853e..3a470ab92b 100644 --- a/cypress/fixtures/safes/static.json +++ b/cypress/fixtures/safes/static.json @@ -12,5 +12,6 @@ "SEP_STATIC_SAFE_10": "sep:0xc2F3645bfd395516d1a18CA6ad9298299d328C01", "SEP_STATIC_SAFE_11": "sep:0x10B45a24640E2170B6AA63ea3A289D723a0C9cba", "SEP_STATIC_SAFE_12": "sep:0xFFfaC243A24EecE6553f0Da278322aCF1Fb6CeF1", - "SEP_STATIC_SAFE_13": "sep:0x027bBe128174F0e5e5d22ECe9623698E01cd3970" + "SEP_STATIC_SAFE_13": "sep:0x027bBe128174F0e5e5d22ECe9623698E01cd3970", + "SEP_STATIC_SAFE_14": "sep:0xe41D568F5040FD9adeE8B64200c6B7C363C68c41" } diff --git a/cypress/fixtures/swaps_data.json b/cypress/fixtures/swaps_data.json new file mode 100644 index 0000000000..10418befb0 --- /dev/null +++ b/cypress/fixtures/swaps_data.json @@ -0,0 +1,58 @@ +{ + "type": { + "queue": { + "contractName": "GPv2Settlement", + "action": "setPreSignature", + "oneOfOne": "1 out of 1", + "title": "Swap order" + }, + "orderDetails": { + "expiry2Mins": "in 2 minutes", + "interactWith": "GPv2Settlement", + "DAIeqCOW": "1 DAI = COW" + }, + "history": { + "buyOrder": "Buy order", + "buy": "Buy", + "oneGNO": "1 GNO", + "oneGNOFull": "1 GNO = 8.4747 COW", + "forAtMost": "For at most", + "forAtMostCow": "For at most COW", + "cow": "COW", + "expired": "Expired", + "cancelled": "Cancelled", + "actionApprove": "CoW Protocol Token: approve", + "actionPreSignature": "GPv2Settlement: setPreSignature", + "actionApproveEth": "Wrapped Ether: approve", + "actionDepositEth": "Wrapped Ether: deposit", + "sellOrder": "Sell order", + "actionApproveG": "approve", + "actionPreSignatureG": "setPreSignature", + "actionDepositG": "deposit", + "amount": "Amount", + "executionPrice": "Execution price", + "limitPrice": "Limit price", + "surplus": "Surplus", + "expiry": "Expiry", + "oderId": "Order ID", + "status": "Status", + "sellFull": "Sell 1 COW", + "sell": "Sell", + "oneCOW": "1 COW", + "forAtLeast": "for at least", + "forAtLeastFullDai": "for at least DAI", + "forAtLeastFullUni": "for at least UNI", + "forAtLeastFullUSDT": "for at least USDT", + "DAIeqCOW": "1 DAI = COW", + "UNIeqCOW": "1 UNI = K COW", + "DAIeqWETH": "1 DAI = WETH", + "USDTeqUSDC": "1 USDT = 0.19342 USDC", + "dai": "DAI", + "filled": "Filled", + "partiallyFilled": "Partially filled", + "gGpV2": "GPv2Settlement", + "safeAppTitile": "CowSwap", + "title": "Swap order" + } + } +} diff --git a/cypress/fixtures/txhistory_data_data.json b/cypress/fixtures/txhistory_data_data.json index c0b15960a5..8a276ccf8b 100644 --- a/cypress/fixtures/txhistory_data_data.json +++ b/cypress/fixtures/txhistory_data_data.json @@ -34,11 +34,11 @@ "receive": { "title": "Receive", "summaryTitle": "Received", - "summaryTxInfo": "< 0.00001 ETH", + "summaryTxInfo": "1,000 QTRUST", "summaryTime": "11:00 AM", - "receivedFrom": "Received < 0.00001 ETH from:", + "receivedFrom": "Received 1,000 QTRUST from", "senderAddress": "sep:0x96D4c6fFC338912322813a77655fCC926b9A5aC5", - "transactionHash": "0x4159...3e7d", + "transactionHash": "0xd89d...9136", "transactionHashCopied": "0x415977f4e4912e22a5cabc4116f7e8f8984996e00a641dcccf8cbe1eb3db3e7d", "altImage": "Received", "altToken": "ETH" @@ -47,7 +47,7 @@ "title": "Sent", "summaryTxInfo": "-< 0.00001 ETH", "summaryTime": "11:02 AM", - "sentTo": "Sent < 0.00001 ETH to:", + "sentTo": "Sent 0.000000000001 ETH to:", "recipientAddress": "sep:0x06373d5e45AD31BD354CeBfA8dB4eD2c75B8708e", "transactionHash": "0x6a59...6a98", "altImage": "Sent", @@ -130,11 +130,11 @@ } }, "deleteSpendingLimit": { - "title": "Contract interaction", + "title": "AllowanceModule", "summaryTxInfo": "deleteAllowance", "summaryTime": "11:08 AM", "description": "Delete spending limit", - "altImage": "Contract interaction", + "altImage": "AllowanceModule", "beneficiary": "Beneficiary", "beneficiaryAddress": "sep:0xC16Db0251654C0a72E91B190d81eAD367d2C6fED", "transactionHash": "0xd6e8...de8b", diff --git a/cypress/fixtures/txmessages_data.json b/cypress/fixtures/txmessages_data.json index 807e0065db..fdd609947c 100644 --- a/cypress/fixtures/txmessages_data.json +++ b/cypress/fixtures/txmessages_data.json @@ -8,6 +8,9 @@ }, "offChain": { "walletConnect": "WalletConnect", + "testMessage1": "Test message 1 on-ch…", + "testMessage2": "Test message 2 off-c…", + "testMessage3": "Test message 1 off-c…", "altTmage": "Message type", "sign": "Sign", "oneOftwo": "1 out of 2", diff --git a/cypress/support/commands.js b/cypress/support/commands.js index b8c8e64fee..d30653b333 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -211,3 +211,11 @@ Cypress.Commands.add('enter', (selector, opts) => { 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') +}) diff --git a/cypress/support/constants.js b/cypress/support/constants.js index dc60748283..0bba9e43a4 100644 --- a/cypress/support/constants.js +++ b/cypress/support/constants.js @@ -33,21 +33,25 @@ export const BROWSER_PERMISSIONS_KEY = `${LS_NAMESPACE}SafeApps__browserPermissi export const SAFE_PERMISSIONS_KEY = `${LS_NAMESPACE}SafeApps__safePermissions` export const INFO_MODAL_KEY = `${LS_NAMESPACE}SafeApps__infoModal` -export const goerlyE2EWallet = /E2E Wallet @ G(ö|oe)rli/ export const goerlySafeName = /g(ö|oe)rli-safe/ export const sepoliaSafeName = 'sepolia-safe' export const goerliToken = /G(ö|oe)rli Ether/ +export const swapWidget = 'https://swap.cow.fi/#/11155111/widget/swap/' +export const safeTestAppurl = 'https://safe-apps-test-app.pages.dev' export const TX_Builder_url = 'https://safe-apps.dev.5afe.dev/tx-builder' export const drainAccount_url = 'https://safe-apps.dev.5afe.dev/drain-safe' export const testAppUrl = 'https://safe-test-app.com' +export const swapUrl = '/swap?safe=' export const addressBookUrl = '/address-book?safe=' export const appsUrlGeneral = '/apps?=safe=' +export const appsCustomUrl = 'apps/custom?safe=' export const BALANCE_URL = '/balances?safe=' export const balanceNftsUrl = '/balances/nfts?safe=' export const transactionQueueUrl = '/transactions/queue?safe=' export const transactionsHistoryUrl = '/transactions/history?safe=' export const transactionsMessagesUrl = '/transactions/messages?safe=' +export const transactionUrl = '/transactions/tx?safe=' export const openAppsUrl = '/apps/open?safe=' export const homeUrl = '/home?safe=' export const welcomeUrl = '/welcome' @@ -60,7 +64,8 @@ export const getPermissionsUrl = '/get-permissions' export const appSettingsUrl = '/settings/safe-apps' export const setupUrl = '/settings/setup?safe=' export const dataSettingsUrl = '/settings/data?safe=' -export const securityUrl = '/settings/security-login?safe=' +export const securityUrl = '/settings/security?safe=' +export const notificationsUrl = '/settings/notifications?safe=' export const invalidAppUrl = 'https://my-invalid-custom-app.com/manifest.json' export const validAppUrlJson = 'https://my-valid-custom-app.com/manifest.json' export const validAppUrl = 'https://my-valid-custom-app.com' @@ -69,12 +74,14 @@ export const stagingTxServiceUrl = 'https://safe-transaction-sepolia.staging.5af export const stagingTxServiceSafesUrl = '/safes/' export const stagingTxServiceBalancesUrl = '/balances/' +export const stagingCGWUrl = 'https://safe-client.staging.5afe.dev/' export const stagingCGWUrlv1 = 'https://safe-client.staging.5afe.dev/v1' export const stagingCGWUrlv2 = 'https://safe-client.staging.5afe.dev/v2' export const stagingCGWChains = '/chains/' export const stagingCGWSafes = '/safes/' export const stagingCGWNone = '/nonces/' export const stagingCGWCollectibles = '/collectibles/' +export const relayPath = '/relay/' export const stagingCGWAllTokensBalances = '/balances/USD?trusted=false&exclude_spam=false' export const proposeEndpoint = '/**/propose' @@ -132,11 +139,7 @@ export const tokenAbbreviation = { gtt: 'GTT', qtrust: 'QTRUST', tpcc: 'tpcc', -} - -export const currencies = { - cad: 'CAD', - aud: 'AUD', + cow: 'COW', } export const appNames = { @@ -230,8 +233,8 @@ export const localStorageKeys = { SAFE_v2__safeApps: 'SAFE_v2__safeApps', SAFE_v2__cookies: 'SAFE_v2__cookies', SAFE_v2__tokenlist_onboarding: 'SAFE_v2__tokenlist_onboarding', -} - -export const connectWalletNames = { - e2e: 'E2E Wallet', + SAFE_v2__customSafeApps_11155111: 'SAFE_v2__customSafeApps-11155111', + SAFE_v2__SafeApps__browserPermissions: 'SAFE_v2__SafeApps__browserPermissions', + SAFE_v2__SafeApps__infoModal: 'SAFE_v2__SafeApps__infoModal', + SAFE_v2__undeployedSafes: 'SAFE_v2__undeployedSafes', } diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index bdb6fbbc22..fb98bb6297 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -38,3 +38,7 @@ before(() => { } }) }) + +beforeEach(() => { + cy.setupInterceptors() +}) diff --git a/cypress/support/localstorage_data.js b/cypress/support/localstorage_data.js index 6eb629a971..65add75614 100644 --- a/cypress/support/localstorage_data.js +++ b/cypress/support/localstorage_data.js @@ -1,3 +1,4 @@ +/* eslint-disable */ export const batchData = { entry0: { 11155111: { @@ -625,13 +626,66 @@ export const addedSafes = { }, }, }, + set4: { + 11155111: { + '0x86Cb401afF6A25A335c440C25954A70b3c232C27': { + owners: [ + { + value: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', + }, + { + value: '0x12d0Ad7d21bdbe7E05AB0aDd973C58fB48b52Ae5', + }, + ], + threshold: 1, + }, + }, + }, } export const pinnedApps = { transactionBuilder: { 11155111: { pinned: [24], opened: [] } }, } +export const customApps = (url) => ({ + safeTestApp: [{ url: url }], + grantedPermissions: { + [url]: [ + { feature: 'camera', status: 'granted' }, + { feature: 'microphone', status: 'granted' }, + ], + }, +}) + +export const appPermissions = (url) => ({ + grantedPermissions: { + [url]: [ + { feature: 'camera', status: 'granted' }, + { feature: 'microphone', status: 'granted' }, + ], + }, + infoModalAccepted: { 11155111: { consentsAccepted: true, warningCheckedCustomApps: [] } }, +}) + export const cookies = { acceptedCookies: { necessary: true, updates: true, analytics: true }, acceptedTokenListOnboarding: true, } + +export const undeployedSafe = { + safe1: { + 11155111: { + '0xe41D568F5040FD9adeE8B64200c6B7C363C68c41': { + props: { + safeAccountConfig: { + threshold: 1, + owners: ['0xC16Db0251654C0a72E91B190d81eAD367d2C6fED'], + fallbackHandler: '0x017062a1dE2FE6b99BE3d9d37841FeD19F573804', + }, + safeDeploymentConfig: { saltNonce: '20', safeVersion: '1.3.0' }, + }, + status: { status: 'AWAITING_EXECUTION' }, + }, + }, + }, +} diff --git a/cypress/support/utils/checkers.js b/cypress/support/utils/checkers.js new file mode 100644 index 0000000000..112e11b096 --- /dev/null +++ b/cypress/support/utils/checkers.js @@ -0,0 +1,4 @@ +export function startsWith0x(str) { + const pattern = /^0x/ + return pattern.test(str) +} 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/cypress/support/utils/wallet.js b/cypress/support/utils/wallet.js new file mode 100644 index 0000000000..a9ea82fd88 --- /dev/null +++ b/cypress/support/utils/wallet.js @@ -0,0 +1,47 @@ +const onboardv2 = 'onboard-v2' +const pkInput = '[data-testid="private-key-input"]' +const pkConnectBtn = '[data-testid="pk-connect-btn"]' +const connectWalletBtn = '[data-testid="connect-wallet-btn"]' + +const privateKeyStr = 'Private key' + +export function connectSigner(signer) { + const actions = { + privateKey: () => { + cy.get(onboardv2) + .shadow() + .find('button') + .contains(privateKeyStr) + .click() + .then(() => handlePkConnect()) + }, + retry: () => { + cy.wait(1000).then(enterPrivateKey) + }, + } + + function handlePkConnect() { + cy.get('body').then(($body) => { + if ($body.find(pkConnectBtn).length > 0) { + cy.get(pkInput).find('input').clear().type(signer) + cy.get(pkConnectBtn).click() + } + }) + } + + function enterPrivateKey() { + cy.wait(1000) + cy.get(connectWalletBtn) + .should('be.enabled') + .and('be.visible') + .click() + .then(() => { + cy.get('body').then(($body) => { + const actionKey = $body.find(onboardv2).length > 0 ? 'privateKey' : 'retry' + actions[actionKey]() + }) + }) + } + + enterPrivateKey() +} diff --git a/jest.config.cjs b/jest.config.cjs index aaadccb700..8eb1dc0edf 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -26,5 +26,7 @@ const customJestConfig = { // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async module.exports = async () => ({ ...(await createJestConfig(customJestConfig)()), - transformIgnorePatterns: ['node_modules/(?!(uint8arrays|multiformats|@web3-onboard/common)/)'], + transformIgnorePatterns: [ + 'node_modules/(?!(uint8arrays|multiformats|@web3-onboard/common|@walletconnect/(.*)/uint8arrays)/)', + ], }) diff --git a/package.json b/package.json index 07c57d8c14..2bf1678498 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.4", + "version": "1.39.2", "type": "module", "scripts": { "dev": "next dev", @@ -37,15 +37,13 @@ "node": ">=16" }, "resolutions": { - "@walletconnect/core": "^2.11.2", - "@walletconnect/ethereum-provider": "^2.11.2", "@safe-global/safe-core-sdk-types/**/ethers": "^6.11.1", "@safe-global/protocol-kit/**/ethers": "^6.11.1", "@safe-global/api-kit/**/ethers": "^6.11.1", "@gnosis.pm/zodiac/**/ethers": "^6.11.1" }, "dependencies": { - "@cowprotocol/widget-react": "^0.9.1", + "@cowprotocol/widget-react": "^0.9.3", "@ducanh2912/next-pwa": "^9.7.1", "@emotion/cache": "^11.11.0", "@emotion/react": "^11.11.0", @@ -56,18 +54,16 @@ "@mui/material": "^5.14.20", "@mui/x-date-pickers": "^5.0.20", "@reduxjs/toolkit": "^1.9.5", - "@safe-global/api-kit": "^2.3.0", - "@safe-global/protocol-kit": "^3.1.0", - "@safe-global/safe-apps-sdk": "^9.0.0-next.1", - "@safe-global/safe-deployments": "^1.35.0", - "@safe-global/safe-gateway-typescript-sdk": "3.21.1", + "@safe-global/api-kit": "^2.3.2", + "@safe-global/protocol-kit": "^3.1.1", + "@safe-global/safe-apps-sdk": "^9.1.0", + "@safe-global/safe-deployments": "^1.36.0", + "@safe-global/safe-gateway-typescript-sdk": "3.21.8", "@safe-global/safe-modules-deployments": "^1.2.0", "@sentry/react": "^7.91.0", "@spindl-xyz/attribution-lite": "^1.4.0", - "@tkey-mpc/common-types": "^8.2.2", - "@truffle/hdwallet-provider": "^2.1.4", - "@walletconnect/utils": "^2.11.3", - "@walletconnect/web3wallet": "^1.10.3", + "@walletconnect/utils": "^2.13.1", + "@walletconnect/web3wallet": "^1.12.1", "@web3-onboard/coinbase": "^2.2.6", "@web3-onboard/core": "^2.21.4", "@web3-onboard/injected-wallets": "^2.10.14", @@ -75,20 +71,17 @@ "@web3-onboard/ledger": "2.3.2", "@web3-onboard/trezor": "^2.4.2", "@web3-onboard/walletconnect": "^2.5.4", - "@web3auth/mpc-core-kit": "^1.1.3", "blo": "^1.1.1", - "bn.js": "^5.2.1", "classnames": "^2.3.1", "date-fns": "^2.30.0", "ethers": "^6.11.1", "exponential-backoff": "^3.1.0", "firebase": "^10.3.1", - "framer-motion": "^10.13.1", "fuse.js": "^6.6.2", "idb-keyval": "^6.2.1", "js-cookie": "^3.0.1", "lodash": "^4.17.21", - "next": "^14.1.0", + "next": "^14.1.1", "papaparse": "^5.3.2", "qrcode.react": "^3.1.0", "react": "^18.2.0", @@ -98,7 +91,8 @@ "react-hook-form": "7.41.1", "react-papaparse": "^4.0.2", "react-redux": "^8.0.5", - "semver": "^7.5.2" + "semver": "^7.5.2", + "zodiac-roles-deployments": "^2.2.2" }, "devDependencies": { "@chromatic-com/storybook": "^1.3.1", @@ -106,7 +100,7 @@ "@faker-js/faker": "^8.1.0", "@next/bundle-analyzer": "^13.5.6", "@openzeppelin/contracts": "^4.9.2", - "@safe-global/safe-core-sdk-types": "^4.0.2", + "@safe-global/safe-core-sdk-types": "^4.1.1", "@sentry/types": "^7.74.0", "@storybook/addon-designs": "^8.0.0", "@storybook/addon-essentials": "^8.0.6", @@ -134,7 +128,7 @@ "@types/react-gtm-module": "^2.0.3", "@types/semver": "^7.3.10", "@typescript-eslint/eslint-plugin": "^7.6.0", - "@walletconnect/types": "^2.11.3", + "@walletconnect/types": "^2.13.1", "cross-env": "^7.0.3", "cypress": "^12.15.0", "cypress-file-upload": "^5.0.8", diff --git a/patches/@safe-global+api-kit++@safe-global+protocol-kit+3.0.2.patch b/patches/@safe-global+api-kit++@safe-global+protocol-kit+3.0.2.patch deleted file mode 100644 index 767f945610..0000000000 --- a/patches/@safe-global+api-kit++@safe-global+protocol-kit+3.0.2.patch +++ /dev/null @@ -1,22 +0,0 @@ -diff --git a/node_modules/@safe-global/api-kit/node_modules/@safe-global/protocol-kit/dist/src/utils/eip-3770/config.js b/node_modules/@safe-global/api-kit/node_modules/@safe-global/protocol-kit/dist/src/utils/eip-3770/config.js -index f5142af..fdfd432 100644 ---- a/node_modules/@safe-global/api-kit/node_modules/@safe-global/protocol-kit/dist/src/utils/eip-3770/config.js -+++ b/node_modules/@safe-global/api-kit/node_modules/@safe-global/protocol-kit/dist/src/utils/eip-3770/config.js -@@ -30,7 +30,7 @@ exports.networks = [ - { chainId: 63n, shortName: 'metc' }, - { chainId: 69n, shortName: 'okov' }, - { chainId: 71n, shortName: 'cfxtest' }, -- { chainId: 81n, shortName: 'joc' }, -+ { chainId: 81n, shortName: 'sby' }, - { chainId: 82n, shortName: 'meter' }, - { chainId: 83n, shortName: 'meter-test' }, - { chainId: 88n, shortName: 'tomo' }, -@@ -107,6 +107,8 @@ exports.networks = [ - { chainId: 3636n, shortName: 'BTNX' }, - { chainId: 3737n, shortName: 'csb' }, - { chainId: 3776n, shortName: 'astrzk' }, -+ { chainId: 1261120n, shortName: 'azktn' }, -+ { chainId: 6038361n, shortName: 'azkyt' }, - { chainId: 4002n, shortName: 'tftm' }, - { chainId: 4202n, shortName: 'lisksep' }, - { chainId: 4337n, shortName: 'beam' }, diff --git a/patches/@safe-global+protocol-kit+3.1.0.patch b/patches/@safe-global+protocol-kit+3.1.1.patch similarity index 89% rename from patches/@safe-global+protocol-kit+3.1.0.patch rename to patches/@safe-global+protocol-kit+3.1.1.patch index 8385a15a61..251f35309b 100644 --- a/patches/@safe-global+protocol-kit+3.1.0.patch +++ b/patches/@safe-global+protocol-kit+3.1.1.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@safe-global/protocol-kit/dist/src/utils/eip-3770/config.js b/node_modules/@safe-global/protocol-kit/dist/src/utils/eip-3770/config.js -index a3c670d..15cfd9b 100644 +index c22dc1e..88bfd14 100644 --- a/node_modules/@safe-global/protocol-kit/dist/src/utils/eip-3770/config.js +++ b/node_modules/@safe-global/protocol-kit/dist/src/utils/eip-3770/config.js @@ -30,7 +30,7 @@ exports.networks = [ @@ -11,12 +11,12 @@ index a3c670d..15cfd9b 100644 { chainId: 82n, shortName: 'meter' }, { chainId: 83n, shortName: 'meter-test' }, { chainId: 88n, shortName: 'tomo' }, -@@ -112,6 +112,8 @@ exports.networks = [ +@@ -115,6 +115,8 @@ exports.networks = [ { chainId: 3636n, shortName: 'BTNX' }, { chainId: 3737n, shortName: 'csb' }, { chainId: 3776n, shortName: 'astrzk' }, + { chainId: 1261120n, shortName: 'azktn' }, + { chainId: 6038361n, shortName: 'azkyt' }, { chainId: 4002n, shortName: 'tftm' }, + { chainId: 4078n, shortName: 'muster' }, { chainId: 4157n, shortName: 'crossfi-testnet' }, - { chainId: 4202n, shortName: 'lisksep' }, diff --git a/patches/@safe-global+safe-deployments+1.35.0.patch b/patches/@safe-global+safe-deployments+1.35.0.patch deleted file mode 100644 index 9b46337e69..0000000000 --- a/patches/@safe-global+safe-deployments+1.35.0.patch +++ /dev/null @@ -1,2646 +0,0 @@ -diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/compatibility_fallback_handler.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/compatibility_fallback_handler.json -index ed04d9b..f6d1cf8 100644 ---- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/compatibility_fallback_handler.json -+++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/compatibility_fallback_handler.json -@@ -30,15 +30,15 @@ - "61": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "63": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "69": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -- "71": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -- "81": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -+ "81": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -+ "81111": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "82": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "83": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "97": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "100": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "106": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "108": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -- "109": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -+ "-": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "111": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "122": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "123": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -@@ -72,7 +72,7 @@ - "599": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "686": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "787": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -- "919": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -+ "-": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "943": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "1001": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "1008": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -@@ -97,7 +97,7 @@ - "1890": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "1891": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "1984": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -- "1998": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -+ "-": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "2001": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "2002": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "2008": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -@@ -121,7 +121,7 @@ - "4919": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "5000": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "5001": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -- "5003": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -+ "-": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "5700": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "6001": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "6102": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -@@ -130,8 +130,8 @@ - "7332": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "7341": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "7700": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -- "8192": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -- "8194": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -+ "-": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -+ "-": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "8217": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "8453": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "8822": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -@@ -154,8 +154,8 @@ - "13371": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "13473": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "17000": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -- "17172": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -- "18231": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -+ "-": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -+ "-": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "23294": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "23295": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "28979": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -@@ -180,12 +180,12 @@ - "73799": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "80001": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "80002": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -- "80085": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -+ "-": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "81457": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "84531": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "84532": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "103454": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -- "167008": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -+ "-": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "200101": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "200202": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "200810": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -@@ -195,13 +195,13 @@ - "421614": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "534351": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "534352": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -- "534353": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -- "622277": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -- "713715": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -+ "6038361": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -+ "1261120": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -+ "-": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "6038361": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "7777777": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "11155111": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -- "11155420": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -+ "-": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "94204209": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "123420111": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "168587773": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -@@ -210,7 +210,7 @@ - "245022934": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "333000333": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "666666666": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -- "999999999": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -+ "-": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "1313161554": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "1313161555": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "1666600000": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/create_call.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/create_call.json -index d2a7346..c941b64 100644 ---- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/create_call.json -+++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/create_call.json -@@ -30,15 +30,15 @@ - "61": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "63": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "69": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -- "71": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -- "81": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -+ "81": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -+ "81111": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "82": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "83": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "97": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "100": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "106": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "108": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -- "109": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -+ "-": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "111": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "122": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "123": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -@@ -72,7 +72,7 @@ - "599": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "686": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "787": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -- "919": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -+ "-": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "943": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "1001": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "1008": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -@@ -97,7 +97,7 @@ - "1890": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "1891": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "1984": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -- "1998": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -+ "-": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "2001": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "2002": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "2008": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -@@ -121,7 +121,7 @@ - "4919": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "5000": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "5001": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -- "5003": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -+ "-": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "5700": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "6001": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "6102": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -@@ -130,8 +130,8 @@ - "7332": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "7341": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "7700": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -- "8192": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -- "8194": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -+ "-": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -+ "-": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "8217": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "8453": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "8822": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -@@ -154,8 +154,8 @@ - "13371": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "13473": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "17000": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -- "17172": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -- "18231": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -+ "-": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -+ "-": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "23294": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "23295": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "28979": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -@@ -180,12 +180,12 @@ - "73799": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "80001": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "80002": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -- "80085": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -+ "-": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "81457": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "84531": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "84532": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "103454": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -- "167008": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -+ "-": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "200101": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "200202": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "200810": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -@@ -195,13 +195,13 @@ - "421614": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "534351": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "534352": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -- "534353": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -- "622277": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -- "713715": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -+ "6038361": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -+ "1261120": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -+ "-": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "6038361": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "7777777": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "11155111": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -- "11155420": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -+ "-": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "94204209": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "123420111": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "168587773": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -@@ -210,7 +210,7 @@ - "245022934": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "333000333": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "666666666": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -- "999999999": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -+ "-": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "1313161554": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "1313161555": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "1666600000": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/gnosis_safe.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/gnosis_safe.json -index 42d6fee..4dd9a05 100644 ---- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/gnosis_safe.json -+++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/gnosis_safe.json -@@ -30,15 +30,15 @@ - "61": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "63": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "69": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -- "71": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -- "81": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -+ "81": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -+ "81111": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "82": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "83": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "97": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "100": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "106": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "108": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -- "109": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -+ "-": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "111": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "122": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "123": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -@@ -72,7 +72,7 @@ - "599": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "686": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "787": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -- "919": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -+ "-": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "943": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "1001": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "1008": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -@@ -97,7 +97,7 @@ - "1890": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "1891": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "1984": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -- "1998": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -+ "-": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "2001": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "2002": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "2008": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -@@ -121,7 +121,7 @@ - "4919": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "5000": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "5001": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -- "5003": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -+ "-": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "5700": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "6001": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "6102": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -@@ -130,8 +130,8 @@ - "7332": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "7341": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "7700": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -- "8192": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -- "8194": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -+ "-": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -+ "-": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "8217": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "8453": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "8822": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -@@ -154,8 +154,8 @@ - "13371": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "13473": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "17000": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -- "17172": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -- "18231": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -+ "-": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -+ "-": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "23294": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "23295": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "28979": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -@@ -180,12 +180,12 @@ - "73799": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "80001": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "80002": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -- "80085": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -+ "-": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "81457": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "84531": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "84532": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "103454": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -- "167008": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -+ "-": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "200101": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "200202": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "200810": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -@@ -195,13 +195,13 @@ - "421614": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "534351": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "534352": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -- "534353": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -- "622277": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -- "713715": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -+ "6038361": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -+ "1261120": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -+ "-": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "6038361": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "7777777": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "11155111": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -- "11155420": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -+ "-": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "94204209": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "123420111": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "168587773": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -@@ -210,7 +210,7 @@ - "245022934": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "333000333": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "666666666": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -- "999999999": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -+ "-": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "1313161554": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "1313161555": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "1666600000": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/gnosis_safe_l2.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/gnosis_safe_l2.json -index a1a9be3..dcce244 100644 ---- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/gnosis_safe_l2.json -+++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/gnosis_safe_l2.json -@@ -30,15 +30,15 @@ - "61": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "63": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "69": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -- "71": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -- "81": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -+ "81": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -+ "81111": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "82": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "83": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "97": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "100": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "106": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "108": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -- "109": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -+ "-": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "111": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "122": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "123": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -@@ -72,7 +72,7 @@ - "599": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "686": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "787": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -- "919": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -+ "-": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "943": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "1001": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "1008": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -@@ -97,7 +97,7 @@ - "1890": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "1891": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "1984": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -- "1998": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -+ "-": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "2001": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "2002": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "2008": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -@@ -121,7 +121,7 @@ - "4919": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "5000": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "5001": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -- "5003": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -+ "-": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "5700": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "6001": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "6102": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -@@ -130,8 +130,8 @@ - "7332": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "7341": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "7700": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -- "8192": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -- "8194": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -+ "-": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -+ "-": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "8217": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "8453": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "8822": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -@@ -154,8 +154,8 @@ - "13371": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "13473": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "17000": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -- "17172": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -- "18231": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -+ "-": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -+ "-": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "23294": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "23295": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "28979": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -@@ -180,12 +180,12 @@ - "73799": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "80001": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "80002": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -- "80085": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -+ "-": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "81457": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "84531": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "84532": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "103454": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -- "167008": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -+ "-": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "200101": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "200202": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "200810": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -@@ -195,13 +195,13 @@ - "421614": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "534351": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "534352": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -- "534353": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -- "622277": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -- "713715": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -+ "6038361": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -+ "1261120": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -+ "-": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "6038361": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "7777777": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "11155111": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -- "11155420": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -+ "-": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "94204209": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "123420111": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "168587773": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -@@ -210,7 +210,7 @@ - "245022934": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "333000333": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "666666666": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -- "999999999": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -+ "-": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "1313161554": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "1313161555": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "1666600000": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/multi_send.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/multi_send.json -index 45aa033..0e45e2b 100644 ---- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/multi_send.json -+++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/multi_send.json -@@ -30,15 +30,15 @@ - "61": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "63": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "69": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -- "71": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -- "81": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -+ "81": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -+ "81111": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "82": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "83": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "97": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "100": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "106": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "108": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -- "109": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -+ "-": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "111": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "122": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "123": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -@@ -72,7 +72,7 @@ - "599": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "686": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "787": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -- "919": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -+ "-": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "943": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "1001": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "1008": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -@@ -97,7 +97,7 @@ - "1890": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "1891": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "1984": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -- "1998": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -+ "-": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "2001": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "2002": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "2008": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -@@ -121,7 +121,7 @@ - "4919": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "5000": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "5001": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -- "5003": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -+ "-": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "5700": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "6001": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "6102": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -@@ -130,8 +130,8 @@ - "7332": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "7341": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "7700": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -- "8192": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -- "8194": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -+ "-": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -+ "-": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "8217": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "8453": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "8822": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -@@ -154,8 +154,8 @@ - "13371": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "13473": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "17000": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -- "17172": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -- "18231": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -+ "-": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -+ "-": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "23294": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "23295": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "28979": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -@@ -180,12 +180,12 @@ - "73799": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "80001": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "80002": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -- "80085": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -+ "-": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "81457": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "84531": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "84532": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "103454": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -- "167008": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -+ "-": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "200101": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "200202": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "200810": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -@@ -195,13 +195,13 @@ - "421614": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "534351": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "534352": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -- "534353": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -- "622277": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -- "713715": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -+ "6038361": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -+ "1261120": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -+ "-": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "6038361": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "7777777": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "11155111": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -- "11155420": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -+ "-": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "94204209": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "123420111": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "168587773": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -@@ -210,7 +210,7 @@ - "245022934": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "333000333": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "666666666": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -- "999999999": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -+ "-": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "1313161554": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "1313161555": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "1666600000": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/multi_send_call_only.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/multi_send_call_only.json -index 55e19f2..327e982 100644 ---- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/multi_send_call_only.json -+++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/multi_send_call_only.json -@@ -30,15 +30,15 @@ - "61": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "63": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "69": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -- "71": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -- "81": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -+ "81": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -+ "81111": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "82": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "83": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "97": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "100": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "106": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "108": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -- "109": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -+ "-": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "111": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "122": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "123": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -@@ -72,7 +72,7 @@ - "599": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "686": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "787": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -- "919": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -+ "-": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "943": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "1001": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "1008": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -@@ -97,7 +97,7 @@ - "1890": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "1891": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "1984": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -- "1998": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -+ "-": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "2001": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "2002": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "2008": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -@@ -121,7 +121,7 @@ - "4919": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "5000": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "5001": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -- "5003": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -+ "-": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "5700": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "6001": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "6102": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -@@ -130,8 +130,8 @@ - "7332": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "7341": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "7700": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -- "8192": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -- "8194": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -+ "-": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -+ "-": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "8217": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "8453": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "8822": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -@@ -154,8 +154,8 @@ - "13371": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "13473": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "17000": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -- "17172": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -- "18231": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -+ "-": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -+ "-": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "23294": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "23295": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "28979": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -@@ -180,12 +180,12 @@ - "73799": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "80001": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "80002": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -- "80085": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -+ "-": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "81457": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "84531": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "84532": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "103454": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -- "167008": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -+ "-": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "200101": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "200202": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "200810": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -@@ -195,13 +195,13 @@ - "421614": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "534351": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "534352": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -- "534353": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -- "622277": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -- "713715": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -+ "6038361": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -+ "1261120": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -+ "-": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "6038361": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "7777777": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "11155111": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -- "11155420": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -+ "-": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "94204209": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "123420111": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "168587773": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -@@ -210,7 +210,7 @@ - "245022934": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "333000333": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "666666666": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -- "999999999": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -+ "-": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "1313161554": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "1313161555": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "1666600000": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/proxy_factory.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/proxy_factory.json -index b9ed387..c4dea68 100644 ---- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/proxy_factory.json -+++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/proxy_factory.json -@@ -30,15 +30,15 @@ - "61": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "63": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "69": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -- "71": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -- "81": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -+ "81": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -+ "81111": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "82": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "83": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "97": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "100": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "106": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "108": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -- "109": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -+ "-": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "111": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "122": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "123": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -@@ -72,7 +72,7 @@ - "599": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "686": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "787": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -- "919": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -+ "-": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "943": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "1001": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "1008": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -@@ -97,7 +97,7 @@ - "1890": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "1891": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "1984": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -- "1998": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -+ "-": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "2001": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "2002": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "2008": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -@@ -121,7 +121,7 @@ - "4919": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "5000": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "5001": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -- "5003": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -+ "-": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "5700": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "6001": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "6102": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -@@ -130,8 +130,8 @@ - "7332": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "7341": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "7700": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -- "8192": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -- "8194": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -+ "-": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -+ "-": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "8217": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "8453": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "8822": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -@@ -154,8 +154,8 @@ - "13371": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "13473": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "17000": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -- "17172": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -- "18231": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -+ "-": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -+ "-": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "23294": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "23295": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "28979": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -@@ -180,12 +180,12 @@ - "73799": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "80001": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "80002": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -- "80085": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -+ "-": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "81457": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "84531": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "84532": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "103454": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -- "167008": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -+ "-": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "200101": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "200202": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "200810": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -@@ -195,13 +195,13 @@ - "421614": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "534351": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "534352": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -- "534353": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -- "622277": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -- "713715": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -+ "6038361": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -+ "1261120": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -+ "-": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "6038361": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "7777777": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "11155111": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -- "11155420": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -+ "-": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "94204209": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "123420111": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "168587773": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -@@ -210,7 +210,7 @@ - "245022934": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "333000333": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "666666666": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -- "999999999": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -+ "-": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "1313161554": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "1313161555": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "1666600000": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/sign_message_lib.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/sign_message_lib.json -index 03a1290..5f370d4 100644 ---- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/sign_message_lib.json -+++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/sign_message_lib.json -@@ -30,15 +30,15 @@ - "61": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "63": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "69": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -- "71": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -- "81": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -+ "81": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -+ "81111": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "82": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "83": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "97": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "100": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "106": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "108": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -- "109": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -+ "-": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "111": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "122": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "123": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -@@ -72,7 +72,7 @@ - "599": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "686": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "787": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -- "919": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -+ "-": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "943": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "1001": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "1008": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -@@ -97,7 +97,7 @@ - "1890": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "1891": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "1984": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -- "1998": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -+ "-": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "2001": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "2002": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "2008": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -@@ -121,7 +121,7 @@ - "4919": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "5000": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "5001": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -- "5003": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -+ "-": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "5700": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "6001": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "6102": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -@@ -130,8 +130,8 @@ - "7332": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "7341": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "7700": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -- "8192": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -- "8194": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -+ "-": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -+ "-": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "8217": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "8453": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "8822": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -@@ -154,8 +154,8 @@ - "13371": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "13473": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "17000": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -- "17172": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -- "18231": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -+ "-": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -+ "-": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "23294": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "23295": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "28979": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -@@ -180,12 +180,12 @@ - "73799": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "80001": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "80002": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -- "80085": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -+ "-": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "81457": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "84531": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "84532": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "103454": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -- "167008": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -+ "-": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "200101": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "200202": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "200810": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -@@ -195,13 +195,13 @@ - "421614": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "534351": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "534352": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -- "534353": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -- "622277": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -- "713715": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -+ "6038361": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -+ "1261120": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -+ "-": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "6038361": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "7777777": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "11155111": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -- "11155420": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -+ "-": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "94204209": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "123420111": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "168587773": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -@@ -210,7 +210,7 @@ - "245022934": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "333000333": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "666666666": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -- "999999999": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -+ "-": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "1313161554": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "1313161555": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "1666600000": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/simulate_tx_accessor.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/simulate_tx_accessor.json -index c47a99b..e4e1d37 100644 ---- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/simulate_tx_accessor.json -+++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/simulate_tx_accessor.json -@@ -30,15 +30,15 @@ - "61": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "63": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "69": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -- "71": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -- "81": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -+ "81": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -+ "81111": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "82": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "83": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "97": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "100": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "106": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "108": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -- "109": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -+ "-": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "111": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "122": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "123": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -@@ -72,7 +72,7 @@ - "599": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "686": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "787": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -- "919": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -+ "-": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "943": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "1001": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "1008": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -@@ -97,7 +97,7 @@ - "1890": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "1891": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "1984": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -- "1998": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -+ "-": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "2001": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "2002": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "2008": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -@@ -121,7 +121,7 @@ - "4919": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "5000": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "5001": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -- "5003": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -+ "-": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "5700": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "6001": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "6102": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -@@ -130,8 +130,8 @@ - "7332": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "7341": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "7700": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -- "8192": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -- "8194": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -+ "-": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -+ "-": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "8217": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "8453": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "8822": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -@@ -154,8 +154,8 @@ - "13371": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "13473": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "17000": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -- "17172": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -- "18231": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -+ "-": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -+ "-": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "23294": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "23295": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "28979": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -@@ -180,12 +180,12 @@ - "73799": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "80001": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "80002": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -- "80085": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -+ "-": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "81457": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "84531": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "84532": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "103454": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -- "167008": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -+ "-": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "200101": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "200202": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "200810": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -@@ -195,13 +195,13 @@ - "421614": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "534351": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "534352": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -- "534353": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -- "622277": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -- "713715": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -+ "6038361": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -+ "1261120": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -+ "-": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "6038361": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "7777777": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "11155111": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -- "11155420": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -+ "-": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "94204209": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "123420111": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "168587773": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -@@ -210,7 +210,7 @@ - "245022934": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "333000333": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "666666666": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -- "999999999": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -+ "-": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "1313161554": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "1313161555": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "1666600000": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/compatibility_fallback_handler.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/compatibility_fallback_handler.json -index 90c6727..018dc45 100644 ---- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/compatibility_fallback_handler.json -+++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/compatibility_fallback_handler.json -@@ -8,7 +8,7 @@ - "5": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "10": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "56": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", -- "71": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", -+ "81": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "97": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "100": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "137": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", -@@ -22,8 +22,8 @@ - "4653": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "7171": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "7771": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", -- "8192": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", -- "8194": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", -+ "-": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", -+ "-": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "8453": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "10242": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "10243": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", -@@ -41,7 +41,7 @@ - "205205": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "444444": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "11155111": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", -- "11155420": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", -+ "-": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "666666666": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99" - }, - "abi": [ -diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/create_call.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/create_call.json -index f01834c..f61c0f1 100644 ---- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/create_call.json -+++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/create_call.json -@@ -8,7 +8,7 @@ - "5": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "10": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "56": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", -- "71": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", -+ "81": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "97": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "100": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "137": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", -@@ -22,8 +22,8 @@ - "4653": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "7171": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "7771": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", -- "8192": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", -- "8194": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", -+ "-": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", -+ "-": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "8453": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "10242": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "10243": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", -@@ -41,7 +41,7 @@ - "205205": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "444444": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "11155111": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", -- "11155420": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", -+ "-": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "666666666": "0x9b35Af71d77eaf8d7e40252370304687390A1A52" - }, - "abi": [ -diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/multi_send.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/multi_send.json -index fabbd17..0aded96 100644 ---- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/multi_send.json -+++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/multi_send.json -@@ -8,7 +8,7 @@ - "5": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "10": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "56": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", -- "71": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", -+ "81": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "97": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "100": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "137": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", -@@ -22,8 +22,8 @@ - "4653": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "7171": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "7771": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", -- "8192": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", -- "8194": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", -+ "-": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", -+ "-": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "8453": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "10242": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "10243": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", -@@ -41,7 +41,7 @@ - "205205": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "444444": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "11155111": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", -- "11155420": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", -+ "-": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "666666666": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526" - }, - "abi": [ -diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/multi_send_call_only.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/multi_send_call_only.json -index 990c279..dcc6d46 100644 ---- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/multi_send_call_only.json -+++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/multi_send_call_only.json -@@ -8,7 +8,7 @@ - "5": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "10": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "56": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", -- "71": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", -+ "81": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "97": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "100": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "137": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", -@@ -22,8 +22,8 @@ - "4653": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "7171": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "7771": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", -- "8192": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", -- "8194": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", -+ "-": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", -+ "-": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "8453": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "10242": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "10243": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", -@@ -41,7 +41,7 @@ - "205205": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "444444": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "11155111": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", -- "11155420": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", -+ "-": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "666666666": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2" - }, - "abi": [ -diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe.json -index 67ff1ef..9a41cf9 100644 ---- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe.json -+++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe.json -@@ -8,7 +8,7 @@ - "5": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "10": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "56": "0x41675C099F32341bf84BFc5382aF534df5C7461a", -- "71": "0x41675C099F32341bf84BFc5382aF534df5C7461a", -+ "81": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "97": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "100": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "137": "0x41675C099F32341bf84BFc5382aF534df5C7461a", -@@ -22,8 +22,8 @@ - "4653": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "7171": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "7771": "0x41675C099F32341bf84BFc5382aF534df5C7461a", -- "8192": "0x41675C099F32341bf84BFc5382aF534df5C7461a", -- "8194": "0x41675C099F32341bf84BFc5382aF534df5C7461a", -+ "-": "0x41675C099F32341bf84BFc5382aF534df5C7461a", -+ "-": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "8453": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "10242": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "10243": "0x41675C099F32341bf84BFc5382aF534df5C7461a", -@@ -41,7 +41,7 @@ - "205205": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "444444": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "11155111": "0x41675C099F32341bf84BFc5382aF534df5C7461a", -- "11155420": "0x41675C099F32341bf84BFc5382aF534df5C7461a", -+ "-": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "666666666": "0x41675C099F32341bf84BFc5382aF534df5C7461a" - }, - "abi": [ -diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe_l2.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe_l2.json -index ba9131b..b836782 100644 ---- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe_l2.json -+++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe_l2.json -@@ -8,7 +8,7 @@ - "5": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "10": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "56": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", -- "71": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", -+ "81": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "97": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "100": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "137": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", -@@ -22,8 +22,8 @@ - "4653": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "7171": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "7771": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", -- "8192": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", -- "8194": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", -+ "-": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", -+ "-": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "8453": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "10242": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "10243": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", -@@ -41,7 +41,7 @@ - "205205": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "444444": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "11155111": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", -- "11155420": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", -+ "-": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "666666666": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762" - }, - "abi": [ -diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe_proxy_factory.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe_proxy_factory.json -index 622e502..0ab9216 100644 ---- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe_proxy_factory.json -+++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe_proxy_factory.json -@@ -8,7 +8,7 @@ - "5": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "10": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "56": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", -- "71": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", -+ "81": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "97": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "100": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "137": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", -@@ -22,8 +22,8 @@ - "4653": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "7171": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "7771": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", -- "8192": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", -- "8194": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", -+ "-": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", -+ "-": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "8453": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "10242": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "10243": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", -@@ -41,7 +41,7 @@ - "205205": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "444444": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "11155111": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", -- "11155420": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", -+ "-": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "666666666": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67" - }, - "abi": [ -diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/sign_message_lib.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/sign_message_lib.json -index 1a5b596..86af499 100644 ---- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/sign_message_lib.json -+++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/sign_message_lib.json -@@ -8,7 +8,7 @@ - "5": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "10": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "56": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", -- "71": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", -+ "81": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "97": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "100": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "137": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", -@@ -22,8 +22,8 @@ - "4653": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "7171": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "7771": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", -- "8192": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", -- "8194": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", -+ "-": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", -+ "-": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "8453": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "10242": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "10243": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", -@@ -41,7 +41,7 @@ - "205205": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "444444": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "11155111": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", -- "11155420": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", -+ "-": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "666666666": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9" - }, - "abi": [ -diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/simulate_tx_accessor.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/simulate_tx_accessor.json -index b4e4104..2547557 100644 ---- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/simulate_tx_accessor.json -+++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/simulate_tx_accessor.json -@@ -8,7 +8,7 @@ - "5": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "10": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "56": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", -- "71": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", -+ "81": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "97": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "100": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "137": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", -@@ -22,8 +22,8 @@ - "4653": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "7171": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "7771": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", -- "8192": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", -- "8194": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", -+ "-": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", -+ "-": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "8453": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "10242": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "10243": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", -@@ -41,7 +41,7 @@ - "205205": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "444444": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "11155111": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", -- "11155420": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", -+ "-": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "666666666": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199" - }, - "abi": [ -diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/compatibility_fallback_handler.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/compatibility_fallback_handler.json -index 13a6d78..3a58e91 100644 ---- a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/compatibility_fallback_handler.json -+++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/compatibility_fallback_handler.json -@@ -30,15 +30,15 @@ - "61": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "63": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "69": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -- "71": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -- "81": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -+ "81": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -+ "81111": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "82": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "83": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "97": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "100": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "106": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "108": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -- "109": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -+ "-": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "111": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "122": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "123": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -@@ -72,7 +72,7 @@ - "599": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "686": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "787": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -- "919": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -+ "-": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "943": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "1001": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "1008": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -@@ -97,7 +97,7 @@ - "1890": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "1891": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "1984": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -- "1998": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -+ "-": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "2001": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "2002": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "2008": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -@@ -121,7 +121,7 @@ - "4919": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "5000": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "5001": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -- "5003": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -+ "-": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "5700": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "6001": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "6102": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -@@ -130,8 +130,8 @@ - "7332": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "7341": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "7700": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -- "8192": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -- "8194": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -+ "-": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -+ "-": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "8217": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "8453": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "8822": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -@@ -154,8 +154,8 @@ - "13371": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "13473": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "17000": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -- "17172": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -- "18231": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -+ "-": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -+ "-": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "23294": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "23295": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "28979": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -@@ -180,12 +180,12 @@ - "73799": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "80001": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "80002": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -- "80085": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -+ "-": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "81457": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "84531": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "84532": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "103454": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -- "167008": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -+ "-": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "200101": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "200202": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "200810": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -@@ -195,13 +195,13 @@ - "421614": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "534351": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "534352": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -- "534353": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -- "622277": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -- "713715": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -+ "6038361": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -+ "1261120": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -+ "-": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "6038361": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "7777777": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "11155111": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -- "11155420": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -+ "-": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "94204209": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "123420111": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "168587773": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -@@ -210,7 +210,7 @@ - "245022934": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", - "333000333": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "666666666": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -- "999999999": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", -+ "-": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "1313161554": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "1313161555": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", - "1666600000": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", -diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/create_call.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/create_call.json -index d0c6f62..983ba61 100644 ---- a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/create_call.json -+++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/create_call.json -@@ -30,15 +30,15 @@ - "61": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "63": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "69": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -- "71": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -- "81": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -+ "81": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -+ "81111": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "82": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "83": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "97": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "100": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "106": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "108": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -- "109": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -+ "-": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "111": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "122": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "123": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -@@ -72,7 +72,7 @@ - "599": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "686": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "787": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -- "919": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -+ "-": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "943": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "1001": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "1008": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -@@ -97,7 +97,7 @@ - "1890": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "1891": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "1984": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -- "1998": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -+ "-": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "2001": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "2002": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "2008": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -@@ -121,7 +121,7 @@ - "4919": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "5000": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "5001": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -- "5003": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -+ "-": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "5700": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "6001": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "6102": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -@@ -130,8 +130,8 @@ - "7332": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "7341": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "7700": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -- "8192": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -- "8194": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -+ "-": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -+ "-": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "8217": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "8453": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "8822": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -@@ -154,8 +154,8 @@ - "13371": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "13473": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "17000": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -- "17172": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -- "18231": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -+ "-": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -+ "-": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "23294": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "23295": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "28979": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -@@ -180,12 +180,12 @@ - "73799": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "80001": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "80002": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -- "80085": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -+ "-": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "81457": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "84531": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "84532": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "103454": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -- "167008": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -+ "-": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "200101": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "200202": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "200810": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -@@ -195,13 +195,13 @@ - "421614": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "534351": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "534352": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -- "534353": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -- "622277": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -- "713715": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -+ "6038361": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -+ "1261120": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -+ "-": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "6038361": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "7777777": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "11155111": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -- "11155420": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -+ "-": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "94204209": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "123420111": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "168587773": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -@@ -210,7 +210,7 @@ - "245022934": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", - "333000333": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "666666666": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -- "999999999": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", -+ "-": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "1313161554": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "1313161555": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", - "1666600000": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", -diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/gnosis_safe.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/gnosis_safe.json -index 78d49ca..ba91bb2 100644 ---- a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/gnosis_safe.json -+++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/gnosis_safe.json -@@ -30,15 +30,15 @@ - "61": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "63": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "69": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -- "71": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -- "81": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -+ "81": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -+ "81111": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "82": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "83": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "97": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "100": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "106": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "108": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -- "109": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -+ "-": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "111": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "122": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "123": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -@@ -72,7 +72,7 @@ - "599": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "686": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "787": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -- "919": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -+ "-": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "943": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "1001": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "1008": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -@@ -97,7 +97,7 @@ - "1890": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "1891": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "1984": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -- "1998": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -+ "-": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "2001": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "2002": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "2008": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -@@ -121,7 +121,7 @@ - "4919": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "5000": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "5001": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -- "5003": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -+ "-": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "5700": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "6001": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "6102": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -@@ -130,8 +130,8 @@ - "7332": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "7341": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "7700": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -- "8192": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -- "8194": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -+ "-": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -+ "-": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "8217": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "8453": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "8822": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -@@ -154,8 +154,8 @@ - "13371": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "13473": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "17000": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -- "17172": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -- "18231": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -+ "-": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -+ "-": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "23294": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "23295": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "28979": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -@@ -180,12 +180,12 @@ - "73799": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "80001": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "80002": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -- "80085": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -+ "-": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "81457": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "84531": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "84532": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "103454": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -- "167008": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -+ "-": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "200101": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "200202": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "200810": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -@@ -195,13 +195,13 @@ - "421614": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "534351": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "534352": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -- "534353": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -- "622277": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -- "713715": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -+ "6038361": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -+ "1261120": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -+ "-": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "6038361": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "7777777": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "11155111": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -- "11155420": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -+ "-": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "94204209": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "123420111": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "168587773": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -@@ -210,7 +210,7 @@ - "245022934": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", - "333000333": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "666666666": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -- "999999999": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", -+ "-": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "1313161554": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "1313161555": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", - "1666600000": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", -diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/gnosis_safe_l2.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/gnosis_safe_l2.json -index 30faa53..6dd3664 100644 ---- a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/gnosis_safe_l2.json -+++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/gnosis_safe_l2.json -@@ -30,15 +30,15 @@ - "61": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "63": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "69": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -- "71": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -- "81": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -+ "81": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -+ "81111": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "82": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "83": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "97": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "100": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "106": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "108": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -- "109": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -+ "-": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "111": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "122": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "123": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -@@ -72,7 +72,7 @@ - "599": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "686": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "787": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -- "919": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -+ "-": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "943": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "1001": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "1008": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -@@ -97,7 +97,7 @@ - "1890": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "1891": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "1984": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -- "1998": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -+ "-": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "2001": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "2002": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "2008": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -@@ -121,7 +121,7 @@ - "4919": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "5000": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "5001": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -- "5003": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -+ "-": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "5700": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "6001": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "6102": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -@@ -130,8 +130,8 @@ - "7332": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "7341": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "7700": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -- "8192": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -- "8194": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -+ "-": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -+ "-": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "8217": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "8453": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "8822": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -@@ -154,8 +154,8 @@ - "13371": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "13473": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "17000": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -- "17172": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -- "18231": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -+ "-": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -+ "-": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "23294": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "23295": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "28979": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -@@ -180,12 +180,12 @@ - "73799": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "80001": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "80002": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -- "80085": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -+ "-": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "81457": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "84531": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "84532": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "103454": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -- "167008": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -+ "-": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "200101": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "200202": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "200810": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -@@ -195,13 +195,13 @@ - "421614": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "534351": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "534352": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -- "534353": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -- "622277": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -- "713715": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -+ "6038361": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -+ "1261120": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -+ "-": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "6038361": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "7777777": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "11155111": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -- "11155420": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -+ "-": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "94204209": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "123420111": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "168587773": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -@@ -210,7 +210,7 @@ - "245022934": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", - "333000333": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "666666666": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -- "999999999": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", -+ "-": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "1313161554": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "1313161555": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", - "1666600000": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", -diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/multi_send.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/multi_send.json -index ab59907..a6d8463 100644 ---- a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/multi_send.json -+++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/multi_send.json -@@ -30,15 +30,15 @@ - "61": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "63": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "69": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -- "71": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -- "81": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -+ "81": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -+ "81111": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "82": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "83": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "97": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "100": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "106": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "108": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -- "109": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -+ "-": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "111": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "122": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "123": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -@@ -72,7 +72,7 @@ - "599": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "686": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "787": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -- "919": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -+ "-": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "943": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "1001": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "1008": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -@@ -97,7 +97,7 @@ - "1890": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "1891": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "1984": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -- "1998": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -+ "-": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "2001": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "2002": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "2008": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -@@ -121,7 +121,7 @@ - "4919": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "5000": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "5001": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -- "5003": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -+ "-": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "5700": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "6001": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "6102": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -@@ -130,8 +130,8 @@ - "7332": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "7341": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "7700": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -- "8192": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -- "8194": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -+ "-": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -+ "-": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "8217": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "8453": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "8822": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -@@ -154,8 +154,8 @@ - "13371": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "13473": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "17000": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -- "17172": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -- "18231": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -+ "-": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -+ "-": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "23294": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "23295": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "28979": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -@@ -180,12 +180,12 @@ - "73799": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "80001": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "80002": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -- "80085": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -+ "-": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "81457": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "84531": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "84532": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "103454": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -- "167008": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -+ "-": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "200101": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "200202": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "200810": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -@@ -195,13 +195,13 @@ - "421614": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "534351": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "534352": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -- "534353": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -- "622277": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -- "713715": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -+ "6038361": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -+ "1261120": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -+ "-": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "6038361": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "7777777": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "11155111": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -- "11155420": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -+ "-": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "94204209": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "123420111": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "168587773": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -@@ -210,7 +210,7 @@ - "245022934": "0x998739BFdAAdde7C933B942a68053933098f9EDa", - "333000333": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "666666666": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -- "999999999": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", -+ "-": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "1313161554": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "1313161555": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", - "1666600000": "0x998739BFdAAdde7C933B942a68053933098f9EDa", -diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/multi_send_call_only.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/multi_send_call_only.json -index eeb440d..bcc3929 100644 ---- a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/multi_send_call_only.json -+++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/multi_send_call_only.json -@@ -30,15 +30,15 @@ - "61": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "63": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "69": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -- "71": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -- "81": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -+ "81": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -+ "81111": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "82": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "83": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "97": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "100": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "106": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "108": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -- "109": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -+ "-": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "111": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "122": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "123": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -@@ -72,7 +72,7 @@ - "599": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "686": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "787": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -- "919": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -+ "-": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "943": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "1001": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "1008": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -@@ -97,7 +97,7 @@ - "1890": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "1891": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "1984": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -- "1998": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -+ "-": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "2001": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "2002": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "2008": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -@@ -121,7 +121,7 @@ - "4919": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "5000": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "5001": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -- "5003": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -+ "-": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "5700": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "6001": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "6102": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -@@ -130,8 +130,8 @@ - "7332": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "7341": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "7700": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -- "8192": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -- "8194": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -+ "-": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -+ "-": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "8217": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "8453": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "8822": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -@@ -154,8 +154,8 @@ - "13371": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "13473": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "17000": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -- "17172": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -- "18231": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -+ "-": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -+ "-": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "23294": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "23295": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "28979": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -@@ -180,12 +180,12 @@ - "73799": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "80001": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "80002": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -- "80085": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -+ "-": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "81457": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "84531": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "84532": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "103454": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -- "167008": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -+ "-": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "200101": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "200202": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "200810": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -@@ -195,13 +195,13 @@ - "421614": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "534351": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "534352": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -- "534353": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -- "622277": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -- "713715": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -+ "6038361": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -+ "1261120": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -+ "-": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "6038361": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "7777777": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "11155111": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -- "11155420": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -+ "-": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "94204209": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "123420111": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "168587773": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -@@ -210,7 +210,7 @@ - "245022934": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", - "333000333": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "666666666": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -- "999999999": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", -+ "-": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "1313161554": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "1313161555": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", - "1666600000": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", -diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/proxy_factory.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/proxy_factory.json -index a89ee7a..5c4f214 100644 ---- a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/proxy_factory.json -+++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/proxy_factory.json -@@ -30,15 +30,15 @@ - "61": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "63": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "69": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -- "71": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -- "81": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -+ "81": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -+ "81111": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "82": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "83": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "97": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "100": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "106": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "108": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -- "109": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -+ "-": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "111": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "122": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "123": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -@@ -72,7 +72,7 @@ - "599": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "686": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "787": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -- "919": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -+ "-": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "943": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "1001": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "1008": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -@@ -97,7 +97,7 @@ - "1890": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "1891": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "1984": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -- "1998": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -+ "-": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "2001": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "2002": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "2008": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -@@ -121,7 +121,7 @@ - "4919": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "5000": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "5001": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -- "5003": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -+ "-": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "5700": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "6001": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "6102": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -@@ -130,8 +130,8 @@ - "7332": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "7341": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "7700": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -- "8192": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -- "8194": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -+ "-": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -+ "-": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "8217": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "8453": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "8822": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -@@ -154,8 +154,8 @@ - "13371": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "13473": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "17000": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -- "17172": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -- "18231": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -+ "-": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -+ "-": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "23294": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "23295": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "28979": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -@@ -180,12 +180,12 @@ - "73799": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "80001": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "80002": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -- "80085": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -+ "-": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "81457": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "84531": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "84532": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "103454": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -- "167008": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -+ "-": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "200101": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "200202": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "200810": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -@@ -195,13 +195,13 @@ - "421614": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "534351": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "534352": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -- "534353": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -- "622277": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -- "713715": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -+ "6038361": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -+ "1261120": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -+ "-": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "6038361": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "7777777": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "11155111": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -- "11155420": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -+ "-": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "94204209": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "123420111": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "168587773": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -@@ -210,7 +210,7 @@ - "245022934": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", - "333000333": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "666666666": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -- "999999999": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", -+ "-": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "1313161554": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "1313161555": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", - "1666600000": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", -diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/sign_message_lib.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/sign_message_lib.json -index 2c242d4..028bf21 100644 ---- a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/sign_message_lib.json -+++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/sign_message_lib.json -@@ -30,15 +30,15 @@ - "61": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "63": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "69": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -- "71": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -- "81": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -+ "81": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -+ "81111": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "82": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "83": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "97": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "100": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "106": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "108": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -- "109": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -+ "-": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "111": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "122": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "123": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -@@ -72,7 +72,7 @@ - "599": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "686": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "787": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -- "919": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -+ "-": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "943": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "1001": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "1008": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -@@ -97,7 +97,7 @@ - "1890": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "1891": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "1984": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -- "1998": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -+ "-": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "2001": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "2002": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "2008": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -@@ -121,7 +121,7 @@ - "4919": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "5000": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "5001": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -- "5003": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -+ "-": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "5700": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "6001": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "6102": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -@@ -130,8 +130,8 @@ - "7332": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "7341": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "7700": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -- "8192": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -- "8194": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -+ "-": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -+ "-": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "8217": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "8453": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "8822": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -@@ -154,8 +154,8 @@ - "13371": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "13473": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "17000": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -- "17172": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -- "18231": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -+ "-": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -+ "-": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "23294": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "23295": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "28979": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -@@ -180,12 +180,12 @@ - "73799": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "80001": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "80002": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -- "80085": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -+ "-": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "81457": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "84531": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "84532": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "103454": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -- "167008": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -+ "-": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "200101": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "200202": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "200810": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -@@ -195,13 +195,13 @@ - "421614": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "534351": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "534352": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -- "534353": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -- "622277": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -- "713715": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -+ "6038361": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -+ "1261120": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -+ "-": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "6038361": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "7777777": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "11155111": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -- "11155420": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -+ "-": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "94204209": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "123420111": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "168587773": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -@@ -210,7 +210,7 @@ - "245022934": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", - "333000333": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "666666666": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -- "999999999": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", -+ "-": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "1313161554": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "1313161555": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", - "1666600000": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", -diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/simulate_tx_accessor.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/simulate_tx_accessor.json -index 73fcc9d..3c599e0 100644 ---- a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/simulate_tx_accessor.json -+++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/simulate_tx_accessor.json -@@ -30,15 +30,15 @@ - "61": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "63": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "69": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -- "71": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -- "81": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -+ "81": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -+ "81111": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "82": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "83": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "97": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "100": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "106": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "108": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -- "109": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -+ "-": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "111": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "122": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "123": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -@@ -72,7 +72,7 @@ - "599": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "686": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "787": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -- "919": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -+ "-": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "943": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "1001": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "1008": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -@@ -97,7 +97,7 @@ - "1890": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "1891": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "1984": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -- "1998": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -+ "-": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "2001": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "2002": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "2008": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -@@ -121,7 +121,7 @@ - "4919": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "5000": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "5001": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -- "5003": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -+ "-": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "5700": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "6001": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "6102": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -@@ -130,8 +130,8 @@ - "7332": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "7341": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "7700": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -- "8192": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -- "8194": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -+ "-": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -+ "-": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "8217": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "8453": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "8822": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -@@ -154,8 +154,8 @@ - "13371": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "13473": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "17000": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -- "17172": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -- "18231": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -+ "-": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -+ "-": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "23294": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "23295": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "28979": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -@@ -180,12 +180,12 @@ - "73799": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "80001": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "80002": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -- "80085": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -+ "-": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "81457": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "84531": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "84532": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "103454": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -- "167008": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -+ "-": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "200101": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "200202": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "200810": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -@@ -195,13 +195,13 @@ - "421614": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "534351": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "534352": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -- "534353": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -- "622277": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -- "713715": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -+ "6038361": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -+ "1261120": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -+ "-": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "6038361": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "7777777": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "11155111": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -- "11155420": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -+ "-": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "94204209": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "123420111": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "168587773": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -@@ -210,7 +210,7 @@ - "245022934": "0x727a77a074D1E6c4530e814F89E618a3298FC044", - "333000333": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "666666666": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -- "999999999": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", -+ "-": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "1313161554": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "1313161555": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", - "1666600000": "0x727a77a074D1E6c4530e814F89E618a3298FC044", -diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/compatibility_fallback_handler.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/compatibility_fallback_handler.json -index 415f102..3e87610 100644 ---- a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/compatibility_fallback_handler.json -+++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/compatibility_fallback_handler.json -@@ -8,7 +8,7 @@ - "5": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "10": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "56": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", -- "71": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", -+ "81": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "97": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "100": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "137": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", -@@ -22,8 +22,8 @@ - "4653": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "7171": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "7771": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", -- "8192": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", -- "8194": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", -+ "-": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", -+ "-": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "8453": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "10242": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "10243": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", -@@ -41,7 +41,7 @@ - "205205": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "444444": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "11155111": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", -- "11155420": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", -+ "-": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", - "666666666": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99" - }, - "abi": [ -diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/create_call.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/create_call.json -index ae1e353..591e944 100644 ---- a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/create_call.json -+++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/create_call.json -@@ -8,7 +8,7 @@ - "5": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "10": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "56": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", -- "71": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", -+ "81": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "97": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "100": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "137": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", -@@ -22,8 +22,8 @@ - "4653": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "7171": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "7771": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", -- "8192": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", -- "8194": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", -+ "-": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", -+ "-": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "8453": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "10242": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "10243": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", -@@ -41,7 +41,7 @@ - "205205": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "444444": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "11155111": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", -- "11155420": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", -+ "-": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", - "666666666": "0x9b35Af71d77eaf8d7e40252370304687390A1A52" - }, - "abi": [ -diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/multi_send.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/multi_send.json -index 95a5aa6..4b1ef9b 100644 ---- a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/multi_send.json -+++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/multi_send.json -@@ -8,7 +8,7 @@ - "5": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "10": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "56": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", -- "71": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", -+ "81": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "97": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "100": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "137": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", -@@ -22,8 +22,8 @@ - "4653": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "7171": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "7771": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", -- "8192": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", -- "8194": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", -+ "-": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", -+ "-": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "8453": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "10242": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "10243": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", -@@ -41,7 +41,7 @@ - "205205": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "444444": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "11155111": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", -- "11155420": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", -+ "-": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", - "666666666": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526" - }, - "abi": [ -diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/multi_send_call_only.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/multi_send_call_only.json -index a7ad01d..81ee1df 100644 ---- a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/multi_send_call_only.json -+++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/multi_send_call_only.json -@@ -8,7 +8,7 @@ - "5": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "10": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "56": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", -- "71": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", -+ "81": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "97": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "100": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "137": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", -@@ -22,8 +22,8 @@ - "4653": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "7171": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "7771": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", -- "8192": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", -- "8194": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", -+ "-": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", -+ "-": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "8453": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "10242": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "10243": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", -@@ -41,7 +41,7 @@ - "205205": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "444444": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "11155111": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", -- "11155420": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", -+ "-": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", - "666666666": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2" - }, - "abi": [ -diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe.json -index 0e97d02..e4a9c7b 100644 ---- a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe.json -+++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe.json -@@ -8,7 +8,7 @@ - "5": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "10": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "56": "0x41675C099F32341bf84BFc5382aF534df5C7461a", -- "71": "0x41675C099F32341bf84BFc5382aF534df5C7461a", -+ "81": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "97": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "100": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "137": "0x41675C099F32341bf84BFc5382aF534df5C7461a", -@@ -22,8 +22,8 @@ - "4653": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "7171": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "7771": "0x41675C099F32341bf84BFc5382aF534df5C7461a", -- "8192": "0x41675C099F32341bf84BFc5382aF534df5C7461a", -- "8194": "0x41675C099F32341bf84BFc5382aF534df5C7461a", -+ "-": "0x41675C099F32341bf84BFc5382aF534df5C7461a", -+ "-": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "8453": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "10242": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "10243": "0x41675C099F32341bf84BFc5382aF534df5C7461a", -@@ -41,7 +41,7 @@ - "205205": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "444444": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "11155111": "0x41675C099F32341bf84BFc5382aF534df5C7461a", -- "11155420": "0x41675C099F32341bf84BFc5382aF534df5C7461a", -+ "-": "0x41675C099F32341bf84BFc5382aF534df5C7461a", - "666666666": "0x41675C099F32341bf84BFc5382aF534df5C7461a" - }, - "abi": [ -diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe_l2.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe_l2.json -index c600d01..32e13e8 100644 ---- a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe_l2.json -+++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe_l2.json -@@ -8,7 +8,7 @@ - "5": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "10": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "56": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", -- "71": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", -+ "81": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "97": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "100": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "137": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", -@@ -22,8 +22,8 @@ - "4653": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "7171": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "7771": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", -- "8192": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", -- "8194": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", -+ "-": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", -+ "-": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "8453": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "10242": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "10243": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", -@@ -41,7 +41,7 @@ - "205205": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "444444": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "11155111": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", -- "11155420": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", -+ "-": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", - "666666666": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762" - }, - "abi": [ -diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe_proxy_factory.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe_proxy_factory.json -index 6b01fa3..fb7f00f 100644 ---- a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe_proxy_factory.json -+++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe_proxy_factory.json -@@ -8,7 +8,7 @@ - "5": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "10": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "56": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", -- "71": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", -+ "81": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "97": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "100": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "137": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", -@@ -22,8 +22,8 @@ - "4653": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "7171": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "7771": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", -- "8192": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", -- "8194": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", -+ "-": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", -+ "-": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "8453": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "10242": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "10243": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", -@@ -41,7 +41,7 @@ - "205205": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "444444": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "11155111": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", -- "11155420": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", -+ "-": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - "666666666": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67" - }, - "abi": [ -diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/sign_message_lib.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/sign_message_lib.json -index 87fabca..6515c7f 100644 ---- a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/sign_message_lib.json -+++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/sign_message_lib.json -@@ -8,7 +8,7 @@ - "5": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "10": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "56": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", -- "71": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", -+ "81": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "97": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "100": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "137": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", -@@ -22,8 +22,8 @@ - "4653": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "7171": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "7771": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", -- "8192": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", -- "8194": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", -+ "-": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", -+ "-": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "8453": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "10242": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "10243": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", -@@ -41,7 +41,7 @@ - "205205": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "444444": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "11155111": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", -- "11155420": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", -+ "-": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", - "666666666": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9" - }, - "abi": [ -diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/simulate_tx_accessor.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/simulate_tx_accessor.json -index a537c23..a556f5b 100644 ---- a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/simulate_tx_accessor.json -+++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/simulate_tx_accessor.json -@@ -8,7 +8,7 @@ - "5": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "10": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "56": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", -- "71": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", -+ "81": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "97": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "100": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "137": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", -@@ -22,8 +22,8 @@ - "4653": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "7171": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "7771": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", -- "8192": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", -- "8194": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", -+ "-": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", -+ "-": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "8453": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "10242": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "10243": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", -@@ -41,7 +41,7 @@ - "205205": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "444444": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "11155111": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", -- "11155420": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", -+ "-": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", - "666666666": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199" - }, - "abi": [ diff --git a/patches/@safe-global+safe-deployments+1.36.0.patch b/patches/@safe-global+safe-deployments+1.36.0.patch new file mode 100644 index 0000000000..0979f75bba --- /dev/null +++ b/patches/@safe-global+safe-deployments+1.36.0.patch @@ -0,0 +1,720 @@ +diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/compatibility_fallback_handler.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/compatibility_fallback_handler.json +index 277a537..fa2fef0 100644 +--- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/compatibility_fallback_handler.json ++++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/compatibility_fallback_handler.json +@@ -30,8 +30,8 @@ + "61": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", + "63": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", + "69": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", +- "71": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", +- "81": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", ++ "81": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", ++ "81111": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", + "82": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", + "83": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", + "97": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", +@@ -200,9 +200,9 @@ + "421614": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", + "534351": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", + "534352": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", +- "534353": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", ++ "6038361": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", + "555666": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", +- "622277": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", ++ "1261120": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", + "713715": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", + "6038361": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", + "7225878": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", +diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/create_call.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/create_call.json +index 36ee97c..b43d845 100644 +--- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/create_call.json ++++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/create_call.json +@@ -30,8 +30,8 @@ + "61": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", + "63": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", + "69": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", +- "71": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", +- "81": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", ++ "81": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", ++ "81111": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", + "82": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", + "83": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", + "97": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", +@@ -200,9 +200,9 @@ + "421614": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", + "534351": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", + "534352": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", +- "534353": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", ++ "6038361": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", + "555666": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", +- "622277": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", ++ "1261120": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", + "713715": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", + "6038361": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", + "7225878": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", +diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/gnosis_safe.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/gnosis_safe.json +index a1b80d5..ef1ee0d 100644 +--- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/gnosis_safe.json ++++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/gnosis_safe.json +@@ -30,8 +30,8 @@ + "61": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", + "63": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", + "69": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", +- "71": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", +- "81": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", ++ "81": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", ++ "81111": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", + "82": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", + "83": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", + "97": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", +@@ -200,9 +200,9 @@ + "421614": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", + "534351": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", + "534352": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", +- "534353": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", ++ "6038361": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", + "555666": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", +- "622277": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", ++ "1261120": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", + "713715": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", + "6038361": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", + "7225878": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", +diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/gnosis_safe_l2.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/gnosis_safe_l2.json +index bd5d12b..c0de0a6 100644 +--- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/gnosis_safe_l2.json ++++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/gnosis_safe_l2.json +@@ -30,8 +30,8 @@ + "61": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", + "63": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", + "69": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", +- "71": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", +- "81": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", ++ "81": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", ++ "81111": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", + "82": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", + "83": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", + "97": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", +@@ -200,9 +200,9 @@ + "421614": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", + "534351": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", + "534352": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", +- "534353": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", ++ "6038361": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", + "555666": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", +- "622277": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", ++ "1261120": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", + "713715": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", + "6038361": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", + "7225878": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", +diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/multi_send.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/multi_send.json +index 706ee9a..05c9f02 100644 +--- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/multi_send.json ++++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/multi_send.json +@@ -30,8 +30,8 @@ + "61": "0x998739BFdAAdde7C933B942a68053933098f9EDa", + "63": "0x998739BFdAAdde7C933B942a68053933098f9EDa", + "69": "0x998739BFdAAdde7C933B942a68053933098f9EDa", +- "71": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", +- "81": "0x998739BFdAAdde7C933B942a68053933098f9EDa", ++ "81": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", ++ "81111": "0x998739BFdAAdde7C933B942a68053933098f9EDa", + "82": "0x998739BFdAAdde7C933B942a68053933098f9EDa", + "83": "0x998739BFdAAdde7C933B942a68053933098f9EDa", + "97": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", +@@ -200,9 +200,9 @@ + "421614": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", + "534351": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", + "534352": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", +- "534353": "0x998739BFdAAdde7C933B942a68053933098f9EDa", ++ "6038361": "0x998739BFdAAdde7C933B942a68053933098f9EDa", + "555666": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", +- "622277": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", ++ "1261120": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", + "713715": "0x998739BFdAAdde7C933B942a68053933098f9EDa", + "6038361": "0x998739BFdAAdde7C933B942a68053933098f9EDa", + "7225878": "0x998739BFdAAdde7C933B942a68053933098f9EDa", +diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/multi_send_call_only.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/multi_send_call_only.json +index 6ae2284..fb7e8be 100644 +--- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/multi_send_call_only.json ++++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/multi_send_call_only.json +@@ -30,8 +30,8 @@ + "61": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", + "63": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", + "69": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", +- "71": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", +- "81": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", ++ "81": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", ++ "81111": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", + "82": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", + "83": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", + "97": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", +@@ -200,9 +200,9 @@ + "421614": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", + "534351": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", + "534352": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", +- "534353": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", ++ "6038361": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", + "555666": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", +- "622277": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", ++ "1261120": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", + "713715": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", + "6038361": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", + "7225878": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", +diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/proxy_factory.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/proxy_factory.json +index 709a13a..e651d64 100644 +--- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/proxy_factory.json ++++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/proxy_factory.json +@@ -30,8 +30,8 @@ + "61": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", + "63": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", + "69": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", +- "71": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", +- "81": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", ++ "81": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", ++ "81111": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", + "82": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", + "83": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", + "97": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", +@@ -200,9 +200,9 @@ + "421614": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", + "534351": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", + "534352": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", +- "534353": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", ++ "6038361": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", + "555666": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", +- "622277": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", ++ "1261120": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", + "713715": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", + "6038361": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", + "7225878": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", +diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/sign_message_lib.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/sign_message_lib.json +index ad65b01..9dafb62 100644 +--- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/sign_message_lib.json ++++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/sign_message_lib.json +@@ -30,8 +30,8 @@ + "61": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", + "63": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", + "69": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", +- "71": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", +- "81": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", ++ "81": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", ++ "81111": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", + "82": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", + "83": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", + "97": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", +@@ -200,9 +200,9 @@ + "421614": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", + "534351": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", + "534352": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", +- "534353": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", ++ "6038361": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", + "555666": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", +- "622277": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", ++ "1261120": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", + "713715": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", + "6038361": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", + "7225878": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", +diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/simulate_tx_accessor.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/simulate_tx_accessor.json +index 9785af7..e8bd83e 100644 +--- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/simulate_tx_accessor.json ++++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.3.0/simulate_tx_accessor.json +@@ -30,8 +30,8 @@ + "61": "0x727a77a074D1E6c4530e814F89E618a3298FC044", + "63": "0x727a77a074D1E6c4530e814F89E618a3298FC044", + "69": "0x727a77a074D1E6c4530e814F89E618a3298FC044", +- "71": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", +- "81": "0x727a77a074D1E6c4530e814F89E618a3298FC044", ++ "81": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", ++ "81111": "0x727a77a074D1E6c4530e814F89E618a3298FC044", + "82": "0x727a77a074D1E6c4530e814F89E618a3298FC044", + "83": "0x727a77a074D1E6c4530e814F89E618a3298FC044", + "97": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", +@@ -200,9 +200,9 @@ + "421614": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", + "534351": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", + "534352": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", +- "534353": "0x727a77a074D1E6c4530e814F89E618a3298FC044", ++ "6038361": "0x727a77a074D1E6c4530e814F89E618a3298FC044", + "555666": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", +- "622277": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", ++ "1261120": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", + "713715": "0x727a77a074D1E6c4530e814F89E618a3298FC044", + "6038361": "0x727a77a074D1E6c4530e814F89E618a3298FC044", + "7225878": "0x727a77a074D1E6c4530e814F89E618a3298FC044", +diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/compatibility_fallback_handler.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/compatibility_fallback_handler.json +index 86140dc..6eecfb6 100644 +--- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/compatibility_fallback_handler.json ++++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/compatibility_fallback_handler.json +@@ -8,7 +8,7 @@ + "5": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", + "10": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", + "56": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", +- "71": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", ++ "81": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", + "97": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", + "100": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", + "137": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", +diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/create_call.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/create_call.json +index 6b85f59..728ea8f 100644 +--- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/create_call.json ++++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/create_call.json +@@ -8,7 +8,7 @@ + "5": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", + "10": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", + "56": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", +- "71": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", ++ "81": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", + "97": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", + "100": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", + "137": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", +diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/multi_send.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/multi_send.json +index a2f3fd3..5cccc2a 100644 +--- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/multi_send.json ++++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/multi_send.json +@@ -8,7 +8,7 @@ + "5": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", + "10": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", + "56": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", +- "71": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", ++ "81": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", + "97": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", + "100": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", + "137": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", +diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/multi_send_call_only.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/multi_send_call_only.json +index 796ba9d..ed5cc6a 100644 +--- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/multi_send_call_only.json ++++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/multi_send_call_only.json +@@ -8,7 +8,7 @@ + "5": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", + "10": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", + "56": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", +- "71": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", ++ "81": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", + "97": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", + "100": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", + "137": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", +diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe.json +index bdf59f7..24f3712 100644 +--- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe.json ++++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe.json +@@ -8,7 +8,7 @@ + "5": "0x41675C099F32341bf84BFc5382aF534df5C7461a", + "10": "0x41675C099F32341bf84BFc5382aF534df5C7461a", + "56": "0x41675C099F32341bf84BFc5382aF534df5C7461a", +- "71": "0x41675C099F32341bf84BFc5382aF534df5C7461a", ++ "81": "0x41675C099F32341bf84BFc5382aF534df5C7461a", + "97": "0x41675C099F32341bf84BFc5382aF534df5C7461a", + "100": "0x41675C099F32341bf84BFc5382aF534df5C7461a", + "137": "0x41675C099F32341bf84BFc5382aF534df5C7461a", +diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe_l2.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe_l2.json +index 5354bc6..abcb256 100644 +--- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe_l2.json ++++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe_l2.json +@@ -8,7 +8,7 @@ + "5": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", + "10": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", + "56": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", +- "71": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", ++ "81": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", + "97": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", + "100": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", + "137": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", +diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe_proxy_factory.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe_proxy_factory.json +index a54c849..7250b07 100644 +--- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe_proxy_factory.json ++++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/safe_proxy_factory.json +@@ -8,7 +8,7 @@ + "5": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", + "10": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", + "56": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", +- "71": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", ++ "81": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", + "97": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", + "100": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", + "137": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", +diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/sign_message_lib.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/sign_message_lib.json +index b890c22..5bff3f4 100644 +--- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/sign_message_lib.json ++++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/sign_message_lib.json +@@ -8,7 +8,7 @@ + "5": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", + "10": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", + "56": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", +- "71": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", ++ "81": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", + "97": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", + "100": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", + "137": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", +diff --git a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/simulate_tx_accessor.json b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/simulate_tx_accessor.json +index 6694f7b..d22aca2 100644 +--- a/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/simulate_tx_accessor.json ++++ b/node_modules/@safe-global/safe-deployments/dist/assets/v1.4.1/simulate_tx_accessor.json +@@ -8,7 +8,7 @@ + "5": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", + "10": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", + "56": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", +- "71": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", ++ "81": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", + "97": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", + "100": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", + "137": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", +diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/compatibility_fallback_handler.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/compatibility_fallback_handler.json +index fa77b46..6f3f1df 100644 +--- a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/compatibility_fallback_handler.json ++++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/compatibility_fallback_handler.json +@@ -30,8 +30,8 @@ + "61": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", + "63": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", + "69": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", +- "71": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", +- "81": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", ++ "81": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", ++ "81111": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", + "82": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", + "83": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", + "97": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", +@@ -200,9 +200,9 @@ + "421614": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", + "534351": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", + "534352": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", +- "534353": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", ++ "6038361": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", + "555666": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", +- "622277": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", ++ "1261120": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4", + "713715": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", + "6038361": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", + "7225878": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", +diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/create_call.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/create_call.json +index 8ceb95b..e5413d1 100644 +--- a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/create_call.json ++++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/create_call.json +@@ -30,8 +30,8 @@ + "61": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", + "63": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", + "69": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", +- "71": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", +- "81": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", ++ "81": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", ++ "81111": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", + "82": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", + "83": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", + "97": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", +@@ -200,9 +200,9 @@ + "421614": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", + "534351": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", + "534352": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", +- "534353": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", ++ "6038361": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", + "555666": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", +- "622277": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", ++ "1261120": "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", + "713715": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", + "6038361": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", + "7225878": "0xB19D6FFc2182150F8Eb585b79D4ABcd7C5640A9d", +diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/gnosis_safe.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/gnosis_safe.json +index c5bc83b..3ae2e39 100644 +--- a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/gnosis_safe.json ++++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/gnosis_safe.json +@@ -30,8 +30,8 @@ + "61": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", + "63": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", + "69": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", +- "71": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", +- "81": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", ++ "81": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", ++ "81111": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", + "82": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", + "83": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", + "97": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", +@@ -200,9 +200,9 @@ + "421614": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", + "534351": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", + "534352": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", +- "534353": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", ++ "6038361": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", + "555666": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", +- "622277": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", ++ "1261120": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", + "713715": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", + "6038361": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", + "7225878": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", +diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/gnosis_safe_l2.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/gnosis_safe_l2.json +index a645df8..a7af4d8 100644 +--- a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/gnosis_safe_l2.json ++++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/gnosis_safe_l2.json +@@ -30,8 +30,8 @@ + "61": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", + "63": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", + "69": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", +- "71": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", +- "81": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", ++ "81": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", ++ "81111": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", + "82": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", + "83": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", + "97": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", +@@ -200,9 +200,9 @@ + "421614": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", + "534351": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", + "534352": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", +- "534353": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", ++ "6038361": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", + "555666": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", +- "622277": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", ++ "1261120": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", + "713715": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", + "6038361": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", + "7225878": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", +diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/multi_send.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/multi_send.json +index 3d11813..d40a883 100644 +--- a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/multi_send.json ++++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/multi_send.json +@@ -30,8 +30,8 @@ + "61": "0x998739BFdAAdde7C933B942a68053933098f9EDa", + "63": "0x998739BFdAAdde7C933B942a68053933098f9EDa", + "69": "0x998739BFdAAdde7C933B942a68053933098f9EDa", +- "71": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", +- "81": "0x998739BFdAAdde7C933B942a68053933098f9EDa", ++ "81": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", ++ "81111": "0x998739BFdAAdde7C933B942a68053933098f9EDa", + "82": "0x998739BFdAAdde7C933B942a68053933098f9EDa", + "83": "0x998739BFdAAdde7C933B942a68053933098f9EDa", + "97": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", +@@ -200,9 +200,9 @@ + "421614": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", + "534351": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", + "534352": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", +- "534353": "0x998739BFdAAdde7C933B942a68053933098f9EDa", ++ "6038361": "0x998739BFdAAdde7C933B942a68053933098f9EDa", + "555666": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", +- "622277": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", ++ "1261120": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", + "713715": "0x998739BFdAAdde7C933B942a68053933098f9EDa", + "6038361": "0x998739BFdAAdde7C933B942a68053933098f9EDa", + "7225878": "0x998739BFdAAdde7C933B942a68053933098f9EDa", +diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/multi_send_call_only.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/multi_send_call_only.json +index b61bbc9..bd4fbbc 100644 +--- a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/multi_send_call_only.json ++++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/multi_send_call_only.json +@@ -30,8 +30,8 @@ + "61": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", + "63": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", + "69": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", +- "71": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", +- "81": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", ++ "81": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", ++ "81111": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", + "82": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", + "83": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", + "97": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", +@@ -200,9 +200,9 @@ + "421614": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", + "534351": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", + "534352": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", +- "534353": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", ++ "6038361": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", + "555666": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", +- "622277": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", ++ "1261120": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", + "713715": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", + "6038361": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", + "7225878": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", +diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/proxy_factory.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/proxy_factory.json +index a5bcd34..a8e3763 100644 +--- a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/proxy_factory.json ++++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/proxy_factory.json +@@ -30,8 +30,8 @@ + "61": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", + "63": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", + "69": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", +- "71": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", +- "81": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", ++ "81": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", ++ "81111": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", + "82": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", + "83": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", + "97": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", +@@ -200,9 +200,9 @@ + "421614": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", + "534351": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", + "534352": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", +- "534353": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", ++ "6038361": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", + "555666": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", +- "622277": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", ++ "1261120": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", + "713715": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", + "6038361": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", + "7225878": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", +diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/sign_message_lib.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/sign_message_lib.json +index fc2ea78..7ab5bed 100644 +--- a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/sign_message_lib.json ++++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/sign_message_lib.json +@@ -30,8 +30,8 @@ + "61": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", + "63": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", + "69": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", +- "71": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", +- "81": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", ++ "81": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", ++ "81111": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", + "82": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", + "83": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", + "97": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", +@@ -200,9 +200,9 @@ + "421614": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", + "534351": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", + "534352": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", +- "534353": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", ++ "6038361": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", + "555666": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", +- "622277": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", ++ "1261120": "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", + "713715": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", + "6038361": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", + "7225878": "0x98FFBBF51bb33A056B08ddf711f289936AafF717", +diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/simulate_tx_accessor.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/simulate_tx_accessor.json +index b529ff1..6623b27 100644 +--- a/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/simulate_tx_accessor.json ++++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.3.0/simulate_tx_accessor.json +@@ -30,8 +30,8 @@ + "61": "0x727a77a074D1E6c4530e814F89E618a3298FC044", + "63": "0x727a77a074D1E6c4530e814F89E618a3298FC044", + "69": "0x727a77a074D1E6c4530e814F89E618a3298FC044", +- "71": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", +- "81": "0x727a77a074D1E6c4530e814F89E618a3298FC044", ++ "81": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", ++ "81111": "0x727a77a074D1E6c4530e814F89E618a3298FC044", + "82": "0x727a77a074D1E6c4530e814F89E618a3298FC044", + "83": "0x727a77a074D1E6c4530e814F89E618a3298FC044", + "97": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", +@@ -200,9 +200,9 @@ + "421614": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", + "534351": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", + "534352": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", +- "534353": "0x727a77a074D1E6c4530e814F89E618a3298FC044", ++ "6038361": "0x727a77a074D1E6c4530e814F89E618a3298FC044", + "555666": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", +- "622277": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", ++ "1261120": "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", + "713715": "0x727a77a074D1E6c4530e814F89E618a3298FC044", + "6038361": "0x727a77a074D1E6c4530e814F89E618a3298FC044", + "7225878": "0x727a77a074D1E6c4530e814F89E618a3298FC044", +diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/compatibility_fallback_handler.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/compatibility_fallback_handler.json +index 43d4d3f..898561a 100644 +--- a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/compatibility_fallback_handler.json ++++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/compatibility_fallback_handler.json +@@ -8,7 +8,7 @@ + "5": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", + "10": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", + "56": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", +- "71": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", ++ "81": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", + "97": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", + "100": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", + "137": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", +diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/create_call.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/create_call.json +index 71c2532..7dda80c 100644 +--- a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/create_call.json ++++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/create_call.json +@@ -8,7 +8,7 @@ + "5": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", + "10": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", + "56": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", +- "71": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", ++ "81": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", + "97": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", + "100": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", + "137": "0x9b35Af71d77eaf8d7e40252370304687390A1A52", +diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/multi_send.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/multi_send.json +index 28aa9c3..72f2c4d 100644 +--- a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/multi_send.json ++++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/multi_send.json +@@ -8,7 +8,7 @@ + "5": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", + "10": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", + "56": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", +- "71": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", ++ "81": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", + "97": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", + "100": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", + "137": "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", +diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/multi_send_call_only.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/multi_send_call_only.json +index a3b76ea..67cfdf6 100644 +--- a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/multi_send_call_only.json ++++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/multi_send_call_only.json +@@ -8,7 +8,7 @@ + "5": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", + "10": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", + "56": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", +- "71": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", ++ "81": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", + "97": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", + "100": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", + "137": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2", +diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe.json +index a77251f..87824b5 100644 +--- a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe.json ++++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe.json +@@ -8,7 +8,7 @@ + "5": "0x41675C099F32341bf84BFc5382aF534df5C7461a", + "10": "0x41675C099F32341bf84BFc5382aF534df5C7461a", + "56": "0x41675C099F32341bf84BFc5382aF534df5C7461a", +- "71": "0x41675C099F32341bf84BFc5382aF534df5C7461a", ++ "81": "0x41675C099F32341bf84BFc5382aF534df5C7461a", + "97": "0x41675C099F32341bf84BFc5382aF534df5C7461a", + "100": "0x41675C099F32341bf84BFc5382aF534df5C7461a", + "137": "0x41675C099F32341bf84BFc5382aF534df5C7461a", +diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe_l2.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe_l2.json +index 64b63ca..27bc2c3 100644 +--- a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe_l2.json ++++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe_l2.json +@@ -8,7 +8,7 @@ + "5": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", + "10": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", + "56": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", +- "71": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", ++ "81": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", + "97": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", + "100": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", + "137": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", +diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe_proxy_factory.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe_proxy_factory.json +index 8375315..136caa3 100644 +--- a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe_proxy_factory.json ++++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/safe_proxy_factory.json +@@ -8,7 +8,7 @@ + "5": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", + "10": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", + "56": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", +- "71": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", ++ "81": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", + "97": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", + "100": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", + "137": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", +diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/sign_message_lib.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/sign_message_lib.json +index 9532da6..3294103 100644 +--- a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/sign_message_lib.json ++++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/sign_message_lib.json +@@ -8,7 +8,7 @@ + "5": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", + "10": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", + "56": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", +- "71": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", ++ "81": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", + "97": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", + "100": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", + "137": "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9", +diff --git a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/simulate_tx_accessor.json b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/simulate_tx_accessor.json +index c73fad6..4c92eef 100644 +--- a/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/simulate_tx_accessor.json ++++ b/node_modules/@safe-global/safe-deployments/src/assets/v1.4.1/simulate_tx_accessor.json +@@ -8,7 +8,7 @@ + "5": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", + "10": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", + "56": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", +- "71": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", ++ "81": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", + "97": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", + "100": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", + "137": "0x3d4BA2E0884aa488718476ca2FB8Efc291A46199", diff --git a/public/images/common/ic-swaps.svg b/public/images/common/ic-swaps.svg new file mode 100644 index 0000000000..79377755c9 --- /dev/null +++ b/public/images/common/ic-swaps.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/common/tx-failed.svg b/public/images/common/tx-failed.svg new file mode 100644 index 0000000000..c78cf169da --- /dev/null +++ b/public/images/common/tx-failed.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/images/messages/link.svg b/public/images/messages/link.svg new file mode 100644 index 0000000000..4cb52591e4 --- /dev/null +++ b/public/images/messages/link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/balances/AssetsHeader/index.tsx b/src/components/balances/AssetsHeader/index.tsx index 6709eb94ac..69a563a2a7 100644 --- a/src/components/balances/AssetsHeader/index.tsx +++ b/src/components/balances/AssetsHeader/index.tsx @@ -1,19 +1,24 @@ -import type { ReactElement, ReactNode } from 'react' +import { useMemo, type ReactElement, type ReactNode } from 'react' import NavTabs from '@/components/common/NavTabs' import PageHeader from '@/components/common/PageHeader' import { balancesNavItems } from '@/components/sidebar/SidebarNavigation/config' import css from '@/components/common/PageHeader/styles.module.css' +import { useCurrentChain } from '@/hooks/useChains' +import { isRouteEnabled } from '@/utils/chains' const AssetsHeader = ({ children }: { children?: ReactNode }): ReactElement => { + const chain = useCurrentChain() + const navItems = useMemo(() => balancesNavItems.filter((item) => isRouteEnabled(item.href, chain)), [chain]) + return (
- +
{children &&
{children}
} diff --git a/src/components/balances/AssetsTable/SendButton.tsx b/src/components/balances/AssetsTable/SendButton.tsx new file mode 100644 index 0000000000..f6a1a41b71 --- /dev/null +++ b/src/components/balances/AssetsTable/SendButton.tsx @@ -0,0 +1,41 @@ +import { useContext } from 'react' +import type { TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { Button } from '@mui/material' +import ArrowIconNW from '@/public/images/common/arrow-top-right.svg' +import CheckWallet from '@/components/common/CheckWallet' +import useSpendingLimit from '@/hooks/useSpendingLimit' +import Track from '@/components/common/Track' +import { ASSETS_EVENTS } from '@/services/analytics/events/assets' +import { TokenTransferFlow } from '@/components/tx-flow/flows' +import { TxModalContext } from '@/components/tx-flow' + +const SendButton = ({ tokenInfo, isOutlined }: { tokenInfo: TokenInfo; isOutlined?: boolean }) => { + const spendingLimit = useSpendingLimit(tokenInfo) + const { setTxFlow } = useContext(TxModalContext) + + const onSendClick = () => { + setTxFlow() + } + + return ( + + {(isOk) => ( + + + + )} + + ) +} + +export default SendButton diff --git a/src/components/balances/AssetsTable/index.tsx b/src/components/balances/AssetsTable/index.tsx index 2948b1fc9d..df1f069b5d 100644 --- a/src/components/balances/AssetsTable/index.tsx +++ b/src/components/balances/AssetsTable/index.tsx @@ -1,10 +1,6 @@ import CheckBalance from '@/features/counterfactual/CheckBalance' -import { useHasFeature } from '@/hooks/useChains' -import ArrowIconNW from '@/public/images/common/arrow-top-right.svg' -import { FEATURES } from '@/utils/chains' -import { formatUnits } from 'ethers' -import { type ReactElement, useMemo, useContext } from 'react' -import { Button, Tooltip, Typography, SvgIcon, IconButton, Box, Checkbox, Skeleton } from '@mui/material' +import { type ReactElement } from 'react' +import { Tooltip, Typography, SvgIcon, IconButton, Box, Checkbox, Skeleton } from '@mui/material' import type { TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' import { TokenType } from '@safe-global/safe-gateway-typescript-sdk' import css from './styles.module.css' @@ -19,15 +15,12 @@ import InfoIcon from '@/public/images/notifications/info.svg' import { VisibilityOutlined } from '@mui/icons-material' import TokenMenu from '../TokenMenu' import useBalances from '@/hooks/useBalances' -import useHiddenTokens from '@/hooks/useHiddenTokens' -import { useHideAssets } from './useHideAssets' -import CheckWallet from '@/components/common/CheckWallet' -import useSpendingLimit from '@/hooks/useSpendingLimit' -import { TxModalContext } from '@/components/tx-flow' -import { TokenTransferFlow } from '@/components/tx-flow/flows' +import { useHideAssets, useVisibleAssets } from './useHideAssets' import AddFundsCTA from '@/components/common/AddFunds' import SwapButton from '@/features/swap/components/SwapButton' -import useIsCounterfactualSafe from '@/features/counterfactual/hooks/useIsCounterfactualSafe' +import { SWAP_LABELS } from '@/services/analytics/events/swaps' +import SendButton from './SendButton' +import useIsSwapFeatureEnabled from '@/features/swap/hooks/useIsSwapFeatureEnabled' const skeletonCells: EnhancedTableProps['rows'][0]['cells'] = { asset: { @@ -85,6 +78,7 @@ const headCells = [ id: 'value', label: 'Value', width: '20%', + align: 'right', }, { id: 'actions', @@ -94,36 +88,6 @@ const headCells = [ }, ] -const SendButton = ({ - tokenInfo, - onClick, -}: { - tokenInfo: TokenInfo - onClick: (tokenAddress: string) => void -}): ReactElement => { - const spendingLimit = useSpendingLimit(tokenInfo) - - return ( - - {(isOk) => ( - - - - )} - - ) -} - const AssetsTable = ({ showHiddenAssets, setShowHiddenAssets, @@ -131,32 +95,20 @@ const AssetsTable = ({ showHiddenAssets: boolean setShowHiddenAssets: (hidden: boolean) => void }): ReactElement => { - const hiddenAssets = useHiddenTokens() const { balances, loading } = useBalances() - const { setTxFlow } = useContext(TxModalContext) - const isCounterfactualSafe = useIsCounterfactualSafe() - const isSwapFeatureEnabled = useHasFeature(FEATURES.NATIVE_SWAPS) && !isCounterfactualSafe + const isSwapFeatureEnabled = useIsSwapFeatureEnabled() const { isAssetSelected, toggleAsset, hidingAsset, hideAsset, cancel, deselectAll, saveChanges } = useHideAssets(() => setShowHiddenAssets(false), ) - const visibleAssets = useMemo( - () => - showHiddenAssets - ? balances.items - : balances.items?.filter((item) => !hiddenAssets.includes(item.tokenInfo.address)), - [hiddenAssets, balances.items, showHiddenAssets], - ) + const visible = useVisibleAssets() + const visibleAssets = showHiddenAssets ? balances.items : visible const hasNoAssets = !loading && balances.items.length === 1 && balances.items[0].balance === '0' const selectedAssetCount = visibleAssets?.filter((item) => isAssetSelected(item.tokenInfo.address)).length || 0 - const onSendClick = (tokenAddress: string) => { - setTxFlow() - } - const rows = loading ? skeletonRows : (visibleAssets || []).map((item) => { @@ -197,8 +149,9 @@ const AssetsTable = ({ rawValue: rawFiatValue, collapsed: item.tokenInfo.address === hidingAsset, content: ( - <> + + {rawFiatValue === 0 && ( )} - + ), }, actions: { @@ -226,13 +179,10 @@ const AssetsTable = ({ content: ( <> - onSendClick(item.tokenInfo.address)} /> + {isSwapFeatureEnabled && ( - + )} {showHiddenAssets ? ( diff --git a/src/components/balances/AssetsTable/useHideAssets.ts b/src/components/balances/AssetsTable/useHideAssets.ts index f5b36a2050..f4d3a2fce9 100644 --- a/src/components/balances/AssetsTable/useHideAssets.ts +++ b/src/components/balances/AssetsTable/useHideAssets.ts @@ -1,9 +1,9 @@ +import { useCallback, useMemo, useState } from 'react' import useBalances from '@/hooks/useBalances' import useChainId from '@/hooks/useChainId' import useHiddenTokens from '@/hooks/useHiddenTokens' import { useAppDispatch } from '@/store' import { setHiddenTokensForChain } from '@/store/settingsSlice' -import { useCallback, useState } from 'react' // This is the default for MUI Collapse export const COLLAPSE_TIMEOUT_MS = 300 @@ -91,3 +91,12 @@ export const useHideAssets = (closeDialog: () => void) => { hidingAsset, } } + +export const useVisibleAssets = () => { + const hiddenAssets = useHiddenTokens() + const { balances } = useBalances() + return useMemo( + () => balances.items?.filter((item) => !hiddenAssets.includes(item.tokenInfo.address)), + [hiddenAssets, balances.items], + ) +} diff --git a/src/components/batch/BatchSidebar/BatchReorder.tsx b/src/components/batch/BatchSidebar/BatchReorder.tsx deleted file mode 100644 index 1f7ee41fde..0000000000 --- a/src/components/batch/BatchSidebar/BatchReorder.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Reorder } from 'framer-motion' -import type { DraftBatchItem } from '@/store/batchSlice' -import BatchTxItem from './BatchTxItem' -import { useState } from 'react' - -const BatchReorder = ({ - txItems, - onDelete, - onReorder, -}: { - txItems: DraftBatchItem[] - onDelete?: (id: string) => void - onReorder: (items: DraftBatchItem[]) => void -}) => { - const [dragging, setDragging] = useState(false) - - return ( - - {txItems.map((item, index) => ( - setDragging(true)} - onDragEnd={() => setDragging(false)} - > - - - ))} - - ) -} - -export default BatchReorder diff --git a/src/components/batch/BatchSidebar/BatchTxItem.tsx b/src/components/batch/BatchSidebar/BatchTxItem.tsx index e9161b4def..8ab81c8dc7 100644 --- a/src/components/batch/BatchSidebar/BatchTxItem.tsx +++ b/src/components/batch/BatchSidebar/BatchTxItem.tsx @@ -6,7 +6,6 @@ import { type DraftBatchItem } from '@/store/batchSlice' import TxType from '@/components/transactions/TxType' import TxInfo from '@/components/transactions/TxInfo' import DeleteIcon from '@/public/images/common/delete.svg' -import DragIcon from '@/public/images/common/drag.svg' import TxData from '@/components/transactions/TxDetails/TxData' import { MethodDetails } from '@/components/transactions/TxDetails/TxData/DecodedData/MethodDetails' import { TxDataRow } from '@/components/transactions/TxDetails/Summary/TxDataRow' @@ -17,19 +16,9 @@ type BatchTxItemProps = DraftBatchItem & { id: string count: number onDelete?: (id: string) => void - draggable?: boolean - dragging?: boolean } -const BatchTxItem = ({ - id, - count, - timestamp, - txDetails, - onDelete, - dragging = false, - draggable = false, -}: BatchTxItemProps) => { +const BatchTxItem = ({ id, count, timestamp, txDetails, onDelete }: BatchTxItemProps) => { const txSummary = useMemo( () => ({ timestamp, @@ -37,6 +26,7 @@ const BatchTxItem = ({ txInfo: txDetails.txInfo, txStatus: txDetails.txStatus, safeAppInfo: txDetails.safeAppInfo, + txHash: txDetails.txHash || null, }), [timestamp, txDetails], ) @@ -61,18 +51,8 @@ const BatchTxItem = ({
{count}
- } disabled={dragging} className={css.accordion}> + } className={css.accordion}> - {draggable && ( - e.stopPropagation()} - /> - )} - @@ -95,7 +75,7 @@ const BatchTxItem = ({
- + {timestamp ? dateString(timestamp) : null} diff --git a/src/components/batch/BatchSidebar/index.tsx b/src/components/batch/BatchSidebar/index.tsx index 5c882a2e7f..d1f89ac6d2 100644 --- a/src/components/batch/BatchSidebar/index.tsx +++ b/src/components/batch/BatchSidebar/index.tsx @@ -1,6 +1,5 @@ import { type SyntheticEvent, useEffect } from 'react' import { useCallback, useContext } from 'react' -import dynamic from 'next/dynamic' import { Button, Divider, Drawer, IconButton, SvgIcon, Typography } from '@mui/material' import CloseIcon from '@mui/icons-material/Close' import { useDraftBatch, useUpdateBatch } from '@/hooks/useDraftBatch' @@ -13,13 +12,12 @@ import { BATCH_EVENTS } from '@/services/analytics' import CheckWallet from '@/components/common/CheckWallet' import PlusIcon from '@/public/images/common/plus.svg' import EmptyBatch from './EmptyBatch' - -const BatchReorder = dynamic(() => import('./BatchReorder')) +import BatchTxList from './BatchTxList' const BatchSidebar = ({ isOpen, onToggle }: { isOpen: boolean; onToggle: (open: boolean) => void }) => { const { txFlow, setTxFlow } = useContext(TxModalContext) const batchTxs = useDraftBatch() - const [, deleteTx, onReorder] = useUpdateBatch() + const [, deleteTx] = useUpdateBatch() const closeSidebar = useCallback(() => { onToggle(false) @@ -73,7 +71,7 @@ const BatchSidebar = ({ isOpen, onToggle }: { isOpen: boolean; onToggle: (open: {batchTxs.length ? ( <>
- +
diff --git a/src/components/common/AddFunds/index.tsx b/src/components/common/AddFunds/index.tsx index f7c6d6dd22..b9e549f9df 100644 --- a/src/components/common/AddFunds/index.tsx +++ b/src/components/common/AddFunds/index.tsx @@ -16,7 +16,7 @@ const AddFundsCTA = () => { const qrCode = `${qrPrefix}${safeAddress}` return ( - +
diff --git a/src/components/common/BuyCryptoButton/index.tsx b/src/components/common/BuyCryptoButton/index.tsx index 77d9e8f28c..c843586f39 100644 --- a/src/components/common/BuyCryptoButton/index.tsx +++ b/src/components/common/BuyCryptoButton/index.tsx @@ -21,7 +21,7 @@ const useOnrampAppUrl = (): string | undefined => { const useBuyCryptoHref = (): LinkProps['href'] | undefined => { const query = useSearchParams() - const safe = query.get('safe') + const safe = query?.get('safe') const appUrl = useOnrampAppUrl() return useMemo(() => { diff --git a/src/components/common/Chip/index.tsx b/src/components/common/Chip/index.tsx index 56fb9c3137..81bb623685 100644 --- a/src/components/common/Chip/index.tsx +++ b/src/components/common/Chip/index.tsx @@ -1,18 +1,8 @@ import { Chip as MuiChip } from '@mui/material' import type { ChipProps } from '@mui/material' import type { ReactElement } from 'react' - -import { useDarkMode } from '@/hooks/useDarkMode' +import React from 'react' export function Chip(props: ChipProps): ReactElement { - const isDarkMode = useDarkMode() - return ( - - ) + return } diff --git a/src/components/common/ChoiceButton/index.tsx b/src/components/common/ChoiceButton/index.tsx index 8e5dbec291..9a99c8b178 100644 --- a/src/components/common/ChoiceButton/index.tsx +++ b/src/components/common/ChoiceButton/index.tsx @@ -21,7 +21,7 @@ const ChoiceButton = ({ chip?: string }) => { return ( - + { - const [anchorEl, setAnchorEl] = useState(null) - const open = !!anchorEl - - const handleClick = (event: MouseEvent) => { - setAnchorEl(event.currentTarget) - } - - const handleClose = () => { - setAnchorEl(null) - } - - const ExpandIcon = open ? ExpandLessIcon : ExpandMoreIcon - - if (!isSocialLoginEnabled) { - return ( - - - - ) - } - +const ConnectionCenter = (): ReactElement => { return ( - <> - - - - - Not connected - palette.error.main }}> - Connect wallet - - - - - - - - - - - - + + + ) } -const useIsSocialLoginEnabled = () => useHasFeature(FEATURES.SOCIAL_LOGIN) - -export default madProps(ConnectionCenter, { - isSocialLoginEnabled: useIsSocialLoginEnabled, -}) +export default ConnectionCenter diff --git a/src/components/common/ConnectWallet/WalletDetails.tsx b/src/components/common/ConnectWallet/WalletDetails.tsx index 414a93a4e2..8bab108de3 100644 --- a/src/components/common/ConnectWallet/WalletDetails.tsx +++ b/src/components/common/ConnectWallet/WalletDetails.tsx @@ -1,13 +1,8 @@ -import { Box, Divider, Skeleton, SvgIcon, Typography } from '@mui/material' -import dynamic from 'next/dynamic' +import { Box, Divider, SvgIcon, Typography } from '@mui/material' import type { ReactElement } from 'react' import LockIcon from '@/public/images/common/lock.svg' -const SocialSigner = dynamic(() => import('@/components/common/SocialSigner'), { - loading: () => , -}) - import WalletLogin from '@/components/welcome/WelcomeLogin/WalletLogin' const WalletDetails = ({ onConnect }: { onConnect: () => void }): ReactElement => { @@ -26,8 +21,6 @@ const WalletDetails = ({ onConnect }: { onConnect: () => void }): ReactElement = or - - ) } diff --git a/src/components/common/ConnectWallet/__tests__/ConnectionCenter.test.tsx b/src/components/common/ConnectWallet/__tests__/ConnectionCenter.test.tsx index 5ca16f4512..844eb8a112 100644 --- a/src/components/common/ConnectWallet/__tests__/ConnectionCenter.test.tsx +++ b/src/components/common/ConnectWallet/__tests__/ConnectionCenter.test.tsx @@ -1,16 +1,9 @@ -import { ConnectionCenter } from '@/components/common/ConnectWallet/ConnectionCenter' +import ConnectionCenter from '@/components/common/ConnectWallet/ConnectionCenter' import { render } from '@/tests/test-utils' describe('ConnectionCenter', () => { - it('displays a Connect wallet button if the social login feature is enabled', () => { - const { getByText, queryByText } = render() - - expect(getByText('Connect wallet')).toBeInTheDocument() - expect(queryByText('Connect')).not.toBeInTheDocument() - }) - - it('displays the ConnectWalletButton if the social login feature is disabled', () => { - const { getByText, queryByText } = render() + it('displays the ConnectWalletButton', () => { + const { getByText, queryByText } = render() expect(queryByText('Connect wallet')).not.toBeInTheDocument() expect(getByText('Connect')).toBeInTheDocument() diff --git a/src/components/common/ConnectWallet/styles.module.css b/src/components/common/ConnectWallet/styles.module.css index dcff10dfc8..483c0bf6c5 100644 --- a/src/components/common/ConnectWallet/styles.module.css +++ b/src/components/common/ConnectWallet/styles.module.css @@ -80,10 +80,6 @@ } @media (max-width: 599.95px) { - .socialLoginInfo > div > div { - display: none; - } - .notConnected { display: none; } diff --git a/src/components/common/CookieBanner/index.tsx b/src/components/common/CookieAndTermBanner/index.tsx similarity index 70% rename from src/components/common/CookieBanner/index.tsx rename to src/components/common/CookieAndTermBanner/index.tsx index f05ecf3aea..97216f1c3f 100644 --- a/src/components/common/CookieBanner/index.tsx +++ b/src/components/common/CookieAndTermBanner/index.tsx @@ -6,17 +6,16 @@ import WarningIcon from '@/public/images/notifications/warning.svg' import { useForm } from 'react-hook-form' import { useAppDispatch, useAppSelector } from '@/store' -import { selectCookies, CookieType, saveCookieConsent } from '@/store/cookiesSlice' +import { selectCookies, CookieAndTermType, saveCookieAndTermConsent } from '@/store/cookiesAndTermsSlice' import { selectCookieBanner, openCookieBanner, closeCookieBanner } from '@/store/popupSlice' import css from './styles.module.css' -// import { AppRoutes } from '@/config/routes' -// import ExternalLink from '../ExternalLink' -const COOKIE_WARNING: Record = { - [CookieType.NECESSARY]: '', - [CookieType.UPDATES]: `You attempted to open the "What's new" section but need to accept the "Beamer" cookies first.`, - [CookieType.ANALYTICS]: '', +const COOKIE_AND_TERM_WARNING: Record = { + [CookieAndTermType.TERMS]: '', + [CookieAndTermType.NECESSARY]: '', + [CookieAndTermType.UPDATES]: `You attempted to open the "What's new" section but need to accept the "Beamer" cookies first.`, + [CookieAndTermType.ANALYTICS]: '', } const CookieCheckbox = ({ @@ -29,34 +28,35 @@ const CookieCheckbox = ({ checkboxProps: CheckboxProps }) => } sx={{ mt: '-9px' }} /> -export const CookieBanner = ({ +export const CookieAndTermBanner = ({ warningKey, inverted, }: { - warningKey?: CookieType + warningKey?: CookieAndTermType inverted?: boolean }): ReactElement => { - const warning = warningKey ? COOKIE_WARNING[warningKey] : undefined + const warning = warningKey ? COOKIE_AND_TERM_WARNING[warningKey] : undefined const dispatch = useAppDispatch() const cookies = useAppSelector(selectCookies) const { register, watch, getValues, setValue } = useForm({ defaultValues: { - [CookieType.NECESSARY]: true, - [CookieType.UPDATES]: cookies[CookieType.UPDATES] ?? false, - [CookieType.ANALYTICS]: cookies[CookieType.ANALYTICS] ?? false, + [CookieAndTermType.TERMS]: true, + [CookieAndTermType.NECESSARY]: true, + [CookieAndTermType.UPDATES]: cookies[CookieAndTermType.UPDATES] ?? false, + [CookieAndTermType.ANALYTICS]: cookies[CookieAndTermType.ANALYTICS] ?? false, ...(warningKey ? { [warningKey]: true } : {}), }, }) const handleAccept = () => { - dispatch(saveCookieConsent(getValues())) + dispatch(saveCookieAndTermConsent(getValues())) dispatch(closeCookieBanner()) } const handleAcceptAll = () => { - // setValue(CookieType.UPDATES, true) - // setValue(CookieType.ANALYTICS, true) + setValue(CookieAndTermType.UPDATES, true) + setValue(CookieAndTermType.ANALYTICS, true) setTimeout(handleAccept, 300) } @@ -72,8 +72,9 @@ export const CookieBanner = ({ - By clicking "Accept all" you agree to the use of the tools listed below and their corresponding - cookies. {/* Cookie policy */} + By browsing this page, you accept our Terms & Conditions (last updated July 2024) and the use of necessary + cookies. By clicking "Accept all" you additionally agree to the use of Beamer and Analytics + cookies as listed below. Cookie policy @@ -86,9 +87,9 @@ export const CookieBanner = ({ {/*
New features and product announcements @@ -96,9 +97,9 @@ export const CookieBanner = ({
@@ -136,7 +137,7 @@ const CookieBannerPopup = (): ReactElement | null => { const dispatch = useAppDispatch() // Open the banner if cookie preferences haven't been set - const shouldOpen = cookies[CookieType.NECESSARY] === undefined + const shouldOpen = cookies[CookieAndTermType.NECESSARY] === undefined useEffect(() => { if (shouldOpen) { @@ -148,7 +149,7 @@ const CookieBannerPopup = (): ReactElement | null => { return cookiePopup?.open ? (
- +
) : null } diff --git a/src/components/common/CookieBanner/styles.module.css b/src/components/common/CookieAndTermBanner/styles.module.css similarity index 100% rename from src/components/common/CookieBanner/styles.module.css rename to src/components/common/CookieAndTermBanner/styles.module.css diff --git a/src/components/common/EnhancedTable/index.tsx b/src/components/common/EnhancedTable/index.tsx index 30a5130830..0874f4beb8 100644 --- a/src/components/common/EnhancedTable/index.tsx +++ b/src/components/common/EnhancedTable/index.tsx @@ -34,6 +34,7 @@ type EnhancedHeadCell = { id: string label: ReactNode width?: string + align?: string sticky?: boolean } @@ -75,7 +76,10 @@ function EnhancedTableHead(props: EnhancedTableHeadProps) { align="left" padding="normal" sortDirection={orderBy === headCell.id ? order : false} - sx={headCell.width ? { width: headCell.width } : undefined} + sx={{ + width: headCell.width ? headCell.width : '', + textAlign: headCell.align ? headCell.align : '', + }} className={classNames({ sticky: headCell.sticky })} > {headCell.label && ( diff --git a/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx b/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx index 9decd746ad..1600ac6ba6 100644 --- a/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx +++ b/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx @@ -2,7 +2,8 @@ import classnames from 'classnames' import type { ReactNode, ReactElement, SyntheticEvent } from 'react' import { isAddress } from 'ethers' import { useTheme } from '@mui/material/styles' -import Box from '@mui/material/Box' +import { Box, SvgIcon, Tooltip } from '@mui/material' +import AddressBookIcon from '@/public/images/sidebar/address-book.svg' import useMediaQuery from '@mui/material/useMediaQuery' import Identicon from '../../Identicon' import CopyAddressButton from '../../CopyAddressButton' @@ -29,6 +30,7 @@ export type EthHashInfoProps = { children?: ReactNode trusted?: boolean ExplorerButtonProps?: ExplorerButtonProps + isAddressBookName?: boolean } const stopPropagation = (e: SyntheticEvent) => e.stopPropagation() @@ -50,6 +52,7 @@ const SrcEthHashInfo = ({ ExplorerButtonProps, children, trusted = true, + isAddressBookName = false, }: EthHashInfoProps): ReactElement => { const shouldPrefix = isAddress(address) const theme = useTheme() @@ -81,8 +84,18 @@ const SrcEthHashInfo = ({ {name && ( - - {name} + + + {name} + + + {isAddressBookName && ( + + + + + + )} )} diff --git a/src/components/common/EthHashInfo/index.tsx b/src/components/common/EthHashInfo/index.tsx index 69d9fe0dad..a141d2d71b 100644 --- a/src/components/common/EthHashInfo/index.tsx +++ b/src/components/common/EthHashInfo/index.tsx @@ -26,6 +26,7 @@ const EthHashInfo = ({ copyPrefix={settings.shortName.copy} {...props} name={name} + isAddressBookName={!!addressBookName} customAvatar={props.customAvatar} ExplorerButtonProps={{ title: link?.title || '', href: link?.href || '' }} avatarSize={avatarSize} diff --git a/src/components/common/ExplorerButton/index.tsx b/src/components/common/ExplorerButton/index.tsx index c29133bf79..4bc6db7373 100644 --- a/src/components/common/ExplorerButton/index.tsx +++ b/src/components/common/ExplorerButton/index.tsx @@ -1,6 +1,7 @@ import type { ReactElement, ComponentType, SyntheticEvent } from 'react' -import { IconButton, SvgIcon, Tooltip } from '@mui/material' +import { Box, IconButton, SvgIcon, Tooltip, Typography } from '@mui/material' import LinkIcon from '@/public/images/common/link.svg' +import Link from 'next/link' export type ExplorerButtonProps = { title?: string @@ -8,6 +9,7 @@ export type ExplorerButtonProps = { className?: string icon?: ComponentType onClick?: (e: SyntheticEvent) => void + isCompact?: boolean } const ExplorerButton = ({ @@ -16,21 +18,41 @@ const ExplorerButton = ({ icon = LinkIcon, className, onClick, -}: ExplorerButtonProps): ReactElement => ( - - { + return isCompact ? ( + + + + + + ) : ( + - - - -) + + + View on explorer + + + + + + ) +} export default ExplorerButton diff --git a/src/components/common/FiatValue/index.tsx b/src/components/common/FiatValue/index.tsx index fb51c4dfab..9cc09f1ea4 100644 --- a/src/components/common/FiatValue/index.tsx +++ b/src/components/common/FiatValue/index.tsx @@ -1,17 +1,23 @@ -import type { ReactElement } from 'react' +import type { CSSProperties, ReactElement } from 'react' import { useMemo } from 'react' import { useAppSelector } from '@/store' import { selectCurrency } from '@/store/settingsSlice' import { formatCurrency } from '@/utils/formatNumber' -const FiatValue = ({ value }: { value: string | number }): ReactElement => { +const style = { whiteSpace: 'nowrap' } as CSSProperties + +const FiatValue = ({ value, maxLength }: { value: string | number; maxLength?: number }): ReactElement => { const currency = useAppSelector(selectCurrency) const fiat = useMemo(() => { - return formatCurrency(value, currency) - }, [value, currency]) + return formatCurrency(value, currency, maxLength) + }, [value, currency, maxLength]) - return {fiat} + return ( + + {fiat} + + ) } export default FiatValue diff --git a/src/components/common/GeoblockingProvider/index.tsx b/src/components/common/GeoblockingProvider/index.tsx new file mode 100644 index 0000000000..b6451de004 --- /dev/null +++ b/src/components/common/GeoblockingProvider/index.tsx @@ -0,0 +1,29 @@ +import { AppRoutes } from '@/config/routes' +import { createContext, type ReactElement, type ReactNode, useEffect, useState } from 'react' + +export const GeoblockingContext = createContext(null) + +/** + * Endpoint returns a 403 if the requesting user is from one of the OFAC sanctioned countries + */ +const GeoblockingProvider = ({ children }: { children: ReactNode }): ReactElement => { + const [isBlockedCountry, setIsBlockedCountry] = useState(null) + + useEffect(() => { + const fetchSwaps = async () => { + await fetch(AppRoutes.swap, { method: 'HEAD' }).then((res) => { + if (res.status === 403) { + setIsBlockedCountry(true) + } else { + setIsBlockedCountry(false) + } + }) + } + + fetchSwaps() + }, []) + + return {children} +} + +export default GeoblockingProvider diff --git a/src/components/common/NetworkSelector/index.tsx b/src/components/common/NetworkSelector/index.tsx index 553cd6292f..296dabd33b 100644 --- a/src/components/common/NetworkSelector/index.tsx +++ b/src/components/common/NetworkSelector/index.tsx @@ -16,16 +16,14 @@ import { useCallback } from 'react' import { AppRoutes } from '@/config/routes' import { trackEvent, OVERVIEW_EVENTS } from '@/services/analytics' import useWallet from '@/hooks/wallets/useWallet' -import { isSocialWalletEnabled } from '@/hooks/wallets/wallets' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' const NetworkSelector = (props: { onChainSelect?: () => void }): ReactElement => { - const wallet = useWallet() const isDarkMode = useDarkMode() const theme = useTheme() const { configs } = useChains() const chainId = useChainId() const router = useRouter() + const isWalletConnected = !!useWallet() const [testNets, prodNets] = useMemo(() => partition(configs, (config) => config.isTestnet), [configs]) @@ -34,7 +32,11 @@ const NetworkSelector = (props: { onChainSelect?: () => void }): ReactElement => const shouldKeepPath = !router.query.safe const route = { - pathname: shouldKeepPath ? router.pathname : AppRoutes.index, + pathname: shouldKeepPath + ? router.pathname + : isWalletConnected + ? AppRoutes.welcome.accounts + : AppRoutes.welcome.index, query: { chain: shortName, } as { @@ -49,7 +51,7 @@ const NetworkSelector = (props: { onChainSelect?: () => void }): ReactElement => return route }, - [router], + [router, isWalletConnected], ) const onChange = (event: SelectChangeEvent) => { @@ -64,24 +66,17 @@ const NetworkSelector = (props: { onChainSelect?: () => void }): ReactElement => } } - const isSocialLogin = isSocialLoginWallet(wallet?.label) - const renderMenuItem = useCallback( (value: string, chain: ChainInfo) => { return ( - + ) }, - [getNetworkLink, isSocialLogin, props.onChainSelect], + [getNetworkLink, props.onChainSelect], ) return configs.length ? ( diff --git a/src/components/common/PageLayout/index.tsx b/src/components/common/PageLayout/index.tsx index a3eac5d5a7..e0f00b8e4f 100644 --- a/src/components/common/PageLayout/index.tsx +++ b/src/components/common/PageLayout/index.tsx @@ -9,7 +9,6 @@ import SideDrawer from './SideDrawer' import { useIsSidebarRoute } from '@/hooks/useIsSidebarRoute' import { TxModalContext } from '@/components/tx-flow' import BatchSidebar from '@/components/batch/BatchSidebar' -import SocialLoginDeprecation from '@/components/common/SocialLoginDeprecation' const PageLayout = ({ pathname, children }: { pathname: string; children: ReactElement }): ReactElement => { const [isSidebarRoute, isAnimated] = useIsSidebarRoute(pathname) @@ -36,8 +35,6 @@ const PageLayout = ({ pathname, children }: { pathname: string; children: ReactE })} >
- - {children}
diff --git a/src/components/common/SafeTokenWidget/index.tsx b/src/components/common/SafeTokenWidget/index.tsx index d930a116e7..79c4aa10fb 100644 --- a/src/components/common/SafeTokenWidget/index.tsx +++ b/src/components/common/SafeTokenWidget/index.tsx @@ -54,7 +54,7 @@ const SafeTokenWidget = () => { const url = { pathname: AppRoutes.apps.open, - query: { safe: query.get('safe'), appUrl: GOVERNANCE_APP_URL }, + query: { safe: query?.get('safe'), appUrl: GOVERNANCE_APP_URL }, } const canRedeemSep5 = canRedeemSep5Airdrop(allocationData) diff --git a/src/components/common/SocialLoginDeprecation/index.tsx b/src/components/common/SocialLoginDeprecation/index.tsx deleted file mode 100644 index e484a6b7c0..0000000000 --- a/src/components/common/SocialLoginDeprecation/index.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Alert } from '@mui/material' -import Link from 'next/link' -import useWallet from '@/hooks/wallets/useWallet' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' -import { AppRoutes } from '@/config/routes' -import { useRouter } from 'next/router' - -const SocialLoginDeprecation = () => { - const router = useRouter() - const wallet = useWallet() - const isSocialLogin = isSocialLoginWallet(wallet?.label) - - if (!isSocialLogin) return null - - const ownersPage = { - pathname: AppRoutes.settings.setup, - query: router.query, - } - - const settingsPage = { - pathname: AppRoutes.settings.securityLogin, - query: router.query, - } - - return ( - - The Social Login wallet is deprecated and will be removed on 01.05.2024. -
- Please{' '} - - - swap the signer - - {' '} - to a different wallet, or{' '} - - - export your private key - - {' '} - to avoid losing access to your Safe Account. -
- ) -} - -export default SocialLoginDeprecation diff --git a/src/components/common/SocialLoginInfo/index.tsx b/src/components/common/SocialLoginInfo/index.tsx deleted file mode 100644 index 00423fccba..0000000000 --- a/src/components/common/SocialLoginInfo/index.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import WalletBalance from '@/components/common/WalletBalance' -import { Badge, Box, Typography } from '@mui/material' -import css from './styles.module.css' -import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' -import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' -import CopyAddressButton from '@/components/common/CopyAddressButton' -import ExplorerButton from '@/components/common/ExplorerButton' -import { getBlockExplorerLink } from '@/utils/chains' -import { useAppSelector } from '@/store' -import { selectSettings } from '@/store/settingsSlice' -import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' - -const SocialLoginInfo = ({ - wallet, - chainInfo, - hideActions = false, - size = 28, - balance, - showBalance, -}: { - wallet: ConnectedWallet - chainInfo?: ChainInfo - hideActions?: boolean - size?: number - balance?: string - showBalance?: boolean -}) => { - const socialWalletService = useSocialWallet() - const userInfo = socialWalletService?.getUserInfo() - const prefix = chainInfo?.shortName - const link = chainInfo ? getBlockExplorerLink(chainInfo, wallet.address) : undefined - const settings = useAppSelector(selectSettings) - - if (!userInfo) return <> - - return ( - - - Profile Image - {!socialWalletService?.isMFAEnabled() && } - -
- - {userInfo.name} - - {showBalance ? ( - - - - ) : ( - - {userInfo.email} - - )} -
- {!hideActions && ( -
- - - - -
- )} -
- ) -} - -export default SocialLoginInfo diff --git a/src/components/common/SocialLoginInfo/styles.module.css b/src/components/common/SocialLoginInfo/styles.module.css deleted file mode 100644 index 0acd0bcdcf..0000000000 --- a/src/components/common/SocialLoginInfo/styles.module.css +++ /dev/null @@ -1,33 +0,0 @@ -.profileImg { - border-radius: var(--space-2); - display: block; -} - -.bubble { - content: ''; - position: absolute; - right: 3px; - bottom: 3px; -} - -.bubble span { - outline: 1px solid var(--color-background-paper); -} - -.profileData { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 2px; -} - -.text { - font-size: 12px; - line-height: 14px; -} - -.actionButtons { - display: flex; - justify-self: flex-end; - margin-left: auto; -} diff --git a/src/components/common/SocialSigner/PasswordRecovery.tsx b/src/components/common/SocialSigner/PasswordRecovery.tsx deleted file mode 100644 index d1f09b634d..0000000000 --- a/src/components/common/SocialSigner/PasswordRecovery.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' -import { - Typography, - FormControlLabel, - Checkbox, - Button, - Box, - Divider, - Grid, - LinearProgress, - FormControl, -} from '@mui/material' -import { useState } from 'react' -import Track from '@/components/common/Track' -import { FormProvider, useForm } from 'react-hook-form' -import PasswordInput from '@/components/settings/SecurityLogin/SocialSignerMFA/PasswordInput' -import ErrorMessage from '@/components/tx/ErrorMessage' - -import css from './styles.module.css' - -type PasswordFormData = { - password: string -} - -export const PasswordRecovery = ({ - recoverFactorWithPassword, - onSuccess, -}: { - recoverFactorWithPassword: (password: string, storeDeviceFactor: boolean) => Promise - onSuccess?: (() => void) | undefined -}) => { - const [storeDeviceFactor, setStoreDeviceFactor] = useState(false) - - const formMethods = useForm({ - mode: 'all', - defaultValues: { - password: '', - }, - }) - - const { handleSubmit, formState } = formMethods - - const [error, setError] = useState() - - const onSubmit = async (data: PasswordFormData) => { - setError(undefined) - try { - await recoverFactorWithPassword(data.password, storeDeviceFactor) - - onSuccess?.() - } catch (e) { - setError('Incorrect password') - } - } - - const isDisabled = formState.isSubmitting - - return ( - -
- - - - Verify your account - - - - - - Enter security password - - - This browser is not registered with your Account yet. Please enter your recovery password to restore - access to this Account. - - - - - - - - setStoreDeviceFactor((prev) => !prev)} /> - } - label="Do not ask again on this device" - /> - {error && {error}} - - - - - - - - - - - -
-
- ) -} diff --git a/src/components/common/SocialSigner/__tests__/PasswordRecovery.test.tsx b/src/components/common/SocialSigner/__tests__/PasswordRecovery.test.tsx deleted file mode 100644 index 488a33cccf..0000000000 --- a/src/components/common/SocialSigner/__tests__/PasswordRecovery.test.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { fireEvent, render } from '@/tests/test-utils' -import { PasswordRecovery } from '@/components/common/SocialSigner/PasswordRecovery' -import { act, waitFor } from '@testing-library/react' - -describe('PasswordRecovery', () => { - it('displays an error if password is wrong', async () => { - const mockRecoverWithPassword = jest.fn(() => Promise.reject()) - const mockOnSuccess = jest.fn() - - const { getByText, getByLabelText } = render( - , - ) - - const passwordField = getByLabelText('Recovery password') - const submitButton = getByText('Submit') - - act(() => { - fireEvent.change(passwordField, { target: { value: 'somethingwrong' } }) - submitButton.click() - }) - - await waitFor(() => { - expect(mockOnSuccess).not.toHaveBeenCalled() - expect(getByText('Incorrect password')).toBeInTheDocument() - }) - }) - - it('calls onSuccess if password is correct', async () => { - const mockRecoverWithPassword = jest.fn(() => Promise.resolve()) - const mockOnSuccess = jest.fn() - - const { getByText, getByLabelText } = render( - , - ) - - const passwordField = getByLabelText('Recovery password') - const submitButton = getByText('Submit') - - act(() => { - fireEvent.change(passwordField, { target: { value: 'somethingCorrect' } }) - submitButton.click() - }) - - await waitFor(() => { - expect(mockOnSuccess).toHaveBeenCalled() - }) - }) -}) diff --git a/src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx b/src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx deleted file mode 100644 index 6c3c33a710..0000000000 --- a/src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { act, render, waitFor } from '@/tests/test-utils' - -import { SocialSigner, _getSupportedChains } from '@/components/common/SocialSigner' -import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' -import { COREKIT_STATUS, type UserInfo, type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' -import SocialWalletService from '@/services/mpc/SocialWalletService' -import { TxModalProvider } from '@/components/tx-flow' -import { fireEvent } from '@testing-library/react' -import { type ISocialWalletService } from '@/services/mpc/interfaces' -import { connectedWalletBuilder } from '@/tests/builders/wallet' -import { chainBuilder } from '@/tests/builders/chains' -import PasswordRecoveryModal from '@/services/mpc/PasswordRecoveryModal' - -jest.mock('@/services/mpc/SocialWalletService') - -const mockWallet = connectedWalletBuilder().with({ chainId: '5', label: ONBOARD_MPC_MODULE_LABEL }).build() - -describe('SocialSignerLogin', () => { - let mockSocialWalletService: ISocialWalletService - - beforeEach(() => { - jest.resetAllMocks() - - mockSocialWalletService = new SocialWalletService({} as unknown as Web3AuthMPCCoreKit) - }) - - it('should render continue with connected account when on gnosis chain', async () => { - const mockOnLogin = jest.fn() - - const result = render( - - - - , - ) - - await waitFor(() => { - expect(result.findByText('Continue as Test Testermann')).resolves.toBeDefined() - }) - - // We do not automatically invoke the callback as the user did not actively connect - expect(mockOnLogin).not.toHaveBeenCalled() - - const button = await result.findByRole('button') - button.click() - - expect(mockOnLogin).toHaveBeenCalled() - }) - - it('should render google login button if no wallet is connected on gnosis chain', async () => { - const mockOnLogin = jest.fn() - - const result = render( - - - - , - ) - - await waitFor(async () => { - expect(result.findByText('Continue with Google')).resolves.toBeDefined() - expect(await result.findByRole('button')).toBeEnabled() - }) - }) - - it('should display a Continue as button and call onLogin when clicked', () => { - const mockOnLogin = jest.fn() - mockSocialWalletService.loginAndCreate = jest.fn(() => Promise.resolve(COREKIT_STATUS.LOGGED_IN)) - - const result = render( - - - - , - ) - - expect(result.getByText('Continue as Test Testermann')).toBeInTheDocument() - - const button = result.getByRole('button') - button.click() - - expect(mockOnLogin).toHaveBeenCalled() - }) - - it('should display Password Recovery form and display a Continue as button when login succeeds', async () => { - const mockOnLogin = jest.fn() - mockSocialWalletService.loginAndCreate = jest.fn(() => Promise.resolve(COREKIT_STATUS.REQUIRED_SHARE)) - mockSocialWalletService.getUserInfo = jest.fn().mockReturnValue(undefined) - mockSocialWalletService.recoverAccountWithPassword = jest.fn(() => Promise.resolve(true)) - - const result = render( - - - - , - ) - - await waitFor(() => { - expect(result.findByText('Continue with Google')).resolves.toBeDefined() - }) - - // We do not automatically invoke the callback as the user did not actively connect - expect(mockOnLogin).not.toHaveBeenCalled() - - const button = await result.findByRole('button') - - act(() => { - button.click() - }) - - await waitFor(() => { - expect(result.findByText('Enter security password')).resolves.toBeDefined() - }) - - const passwordField = await result.findByLabelText('Recovery password') - const submitButton = await result.findByText('Submit') - - act(() => { - fireEvent.change(passwordField, { target: { value: 'Test1234!' } }) - submitButton.click() - }) - - mockSocialWalletService.getUserInfo = jest.fn().mockReturnValue({ - email: 'test@testermann.com', - name: 'Test Testermann', - profileImage: 'test.testermann.local/profile.png', - } as unknown as UserInfo) - - result.rerender( - - - - , - ) - - await waitFor(() => { - expect(result.getByText('Continue as Test Testermann')).toBeInTheDocument() - }) - }) - - describe('getSupportedChains', () => { - const mockEthereumChain = chainBuilder() - .with({ - chainId: '1', - chainName: 'Ethereum', - disabledWallets: ['socialSigner'], - }) - .build() - const mockGnosisChain = chainBuilder() - .with({ chainId: '100', chainName: 'Gnosis Chain', disabledWallets: ['Coinbase'] }) - .build() - it('returns chain names where social login is enabled', () => { - const mockGoerliChain = chainBuilder().with({ chainId: '5', chainName: 'Goerli', disabledWallets: [] }).build() - - const mockChains = [mockEthereumChain, mockGnosisChain, mockGoerliChain] - const result = _getSupportedChains(mockChains) - - expect(result).toEqual(['Gnosis Chain', 'Goerli']) - }) - - it('returns an empty array if social login is not enabled on any chain', () => { - const mockGoerliChain = chainBuilder() - .with({ chainId: '5', chainName: 'Goerli', disabledWallets: ['socialSigner'] }) - .build() - - const mockChains = [mockEthereumChain, mockGoerliChain] - const result = _getSupportedChains(mockChains) - - expect(result).toEqual([]) - }) - }) -}) diff --git a/src/components/common/SocialSigner/index.tsx b/src/components/common/SocialSigner/index.tsx deleted file mode 100644 index 2f21e51168..0000000000 --- a/src/components/common/SocialSigner/index.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' -import { type ISocialWalletService } from '@/services/mpc/interfaces' -import { Alert, Box, Button, LinearProgress, SvgIcon, Typography } from '@mui/material' -import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' -import { useState } from 'react' -import GoogleLogo from '@/public/images/welcome/logo-google.svg' - -import css from './styles.module.css' -import useWallet from '@/hooks/wallets/useWallet' -import Track from '@/components/common/Track' -import { CREATE_SAFE_EVENTS } from '@/services/analytics' -import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' -import { CGW_NAMES } from '@/hooks/wallets/consts' -import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' -import madProps from '@/utils/mad-props' -import { asError } from '@/services/exceptions/utils' -import ErrorMessage from '@/components/tx/ErrorMessage' -import { open } from '@/services/mpc/PasswordRecoveryModal' - -export const _getSupportedChains = (chains: ChainInfo[]) => { - return chains - .filter((chain) => CGW_NAMES.SOCIAL_LOGIN && !chain.disabledWallets.includes(CGW_NAMES.SOCIAL_LOGIN)) - .map((chainConfig) => chainConfig.chainName) -} - -type SocialSignerLoginProps = { - socialWalletService: ISocialWalletService | undefined - wallet: ReturnType - onLogin?: () => void - onRequirePassword?: () => void -} - -export const SocialSigner = ({ socialWalletService, wallet, onLogin, onRequirePassword }: SocialSignerLoginProps) => { - const [loginPending, setLoginPending] = useState(false) - const [loginError, setLoginError] = useState(undefined) - const userInfo = socialWalletService?.getUserInfo() - const isDisabled = loginPending - - const isWelcomePage = !!onLogin - - const login = async () => { - if (!socialWalletService) return - - setLoginPending(true) - setLoginError(undefined) - try { - const status = await socialWalletService.loginAndCreate() - - if (status === COREKIT_STATUS.LOGGED_IN) { - setLoginPending(false) - return - } - - if (status === COREKIT_STATUS.REQUIRED_SHARE) { - onRequirePassword?.() - open(() => setLoginPending(false)) - return - } - } catch (err) { - const error = asError(err) - setLoginError(error.message) - } finally { - setLoginPending(false) - onLogin?.() - } - } - - const isSocialLogin = isSocialLoginWallet(wallet?.label) - - return ( - <> - - {isSocialLogin && userInfo ? ( - - - - ) : ( - - - - )} - {loginError && {loginError}} - - - - From 01.05.2024 we will no longer support account creation and login with Google. - - - ) -} - -export default madProps(SocialSigner, { - socialWalletService: useSocialWallet, - wallet: useWallet, -}) diff --git a/src/components/common/SocialSigner/styles.module.css b/src/components/common/SocialSigner/styles.module.css deleted file mode 100644 index 2b81df624a..0000000000 --- a/src/components/common/SocialSigner/styles.module.css +++ /dev/null @@ -1,34 +0,0 @@ -.profileImg { - border-radius: var(--space-2); - width: 32px; - height: 32px; -} - -.profileData { - display: flex; - flex-direction: column; - align-items: flex-start; -} - -.loginProgress { - margin-bottom: -16px; - top: 0; - width: 100%; - height: 2px; - position: absolute; - border-top-left-radius: 6px; - border-top-right-radius: 6px; -} - -.passwordWrapper { - padding: var(--space-4) var(--space-4) var(--space-2) var(--space-4); - display: flex; - flex-direction: column; - align-items: baseline; - gap: var(--space-1); -} - -.loginError { - width: 100%; - margin: 0; -} diff --git a/src/components/common/Table/DataTable.tsx b/src/components/common/Table/DataTable.tsx index 31c8fd450c..8d06bfb3f5 100644 --- a/src/components/common/Table/DataTable.tsx +++ b/src/components/common/Table/DataTable.tsx @@ -3,16 +3,18 @@ import { Stack, Typography } from '@mui/material' import type { DataRow } from '@/components/common/Table/DataRow' type DataTableProps = { - header: string + header?: string rows: ReactElement[] } export const DataTable = ({ header, rows }: DataTableProps): ReactElement | null => { return ( - - {header} - + {header && ( + + {header} + + )} {rows.map((row) => { return row })} diff --git a/src/components/common/Table/EmptyRow.tsx b/src/components/common/Table/EmptyRow.tsx new file mode 100644 index 0000000000..ecd1986097 --- /dev/null +++ b/src/components/common/Table/EmptyRow.tsx @@ -0,0 +1,6 @@ +import type { ReactElement } from 'react' +import css from './styles.module.css' + +export const EmptyRow = (): ReactElement | null => { + return
+} diff --git a/src/components/common/Table/styles.module.css b/src/components/common/Table/styles.module.css index 91bad958c2..ccc68717d1 100644 --- a/src/components/common/Table/styles.module.css +++ b/src/components/common/Table/styles.module.css @@ -1,11 +1,21 @@ .gridRow { display: grid; - grid-template-columns: 35% auto; + grid-template-columns: 25% auto; gap: var(--space-1); justify-content: flex-start; max-width: 900px; } +.gridEmptyRow { + display: grid; + grid-template-columns: 35% auto; + gap: var(--space-1); + justify-content: flex-start; + max-width: 900px; + margin-top: var(--space-1); + margin-bottom: var(--space-1); + border-top: 1px solid var(--color-border-light); +} .title { color: var(--color-primary-light); font-weight: 400; diff --git a/src/components/common/TokenAmount/index.test.tsx b/src/components/common/TokenAmount/index.test.tsx index e99f5ed266..e04b5ba261 100644 --- a/src/components/common/TokenAmount/index.test.tsx +++ b/src/components/common/TokenAmount/index.test.tsx @@ -14,6 +14,6 @@ describe('TokenAmount', () => { it('should format big amount for zero decimals', async () => { const result = render() - await expect(result.findByText('10,000,000')).resolves.not.toBeNull() + await expect(result.findByText('10M')).resolves.not.toBeNull() }) }) diff --git a/src/components/common/TokenAmount/index.tsx b/src/components/common/TokenAmount/index.tsx index aed38b31ad..0e8f874404 100644 --- a/src/components/common/TokenAmount/index.tsx +++ b/src/components/common/TokenAmount/index.tsx @@ -5,6 +5,8 @@ import { formatVisualAmount } from '@/utils/formatters' import TokenIcon from '../TokenIcon' import classNames from 'classnames' +const PRECISION = 20 + const TokenAmount = ({ value, decimals, @@ -12,6 +14,7 @@ const TokenAmount = ({ tokenSymbol, direction, fallbackSrc, + preciseAmount, }: { value: string decimals?: number @@ -19,9 +22,11 @@ const TokenAmount = ({ tokenSymbol?: string direction?: TransferDirection fallbackSrc?: string + preciseAmount?: boolean }): ReactElement => { const sign = direction === TransferDirection.OUTGOING ? '-' : '' - const amount = decimals !== undefined ? formatVisualAmount(value, decimals) : value + const amount = + decimals !== undefined ? formatVisualAmount(value, decimals, preciseAmount ? PRECISION : undefined) : value return ( diff --git a/src/components/common/WalletInfo/index.test.tsx b/src/components/common/WalletInfo/index.test.tsx index 9375bb4ecc..3b435d6dd9 100644 --- a/src/components/common/WalletInfo/index.test.tsx +++ b/src/components/common/WalletInfo/index.test.tsx @@ -1,13 +1,7 @@ import { render } from '@/tests/test-utils' import { WalletInfo } from '@/components/common/WalletInfo/index' import { type EIP1193Provider, type OnboardAPI } from '@web3-onboard/core' -import { type NextRouter } from 'next/router' -import * as mpcModule from '@/services/mpc/SocialLoginModule' -import * as constants from '@/config/constants' -import { type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' import { act } from '@testing-library/react' -import SocialWalletService from '@/services/mpc/SocialWalletService' -import type { ISocialWalletService } from '@/services/mpc/interfaces' const mockWallet = { address: '0x1234567890123456789012345678901234567890', @@ -16,32 +10,21 @@ const mockWallet = { provider: null as unknown as EIP1193Provider, } -const mockRouter = { - query: {}, - pathname: '', -} as NextRouter - const mockOnboard = { connectWallet: jest.fn(), disconnectWallet: jest.fn(), setChain: jest.fn(), } as unknown as OnboardAPI -jest.mock('@/services/mpc/SocialWalletService') - describe('WalletInfo', () => { - let socialWalletService: ISocialWalletService beforeEach(() => { jest.resetAllMocks() - socialWalletService = new SocialWalletService({} as unknown as Web3AuthMPCCoreKit) }) it('should display the wallet address', () => { const { getByText } = render( { const { getByText } = render( { const { getByText } = render( { expect(mockOnboard.disconnectWallet).toHaveBeenCalled() }) - - it('should display a Delete Account button on dev for social login', () => { - jest.spyOn(mpcModule, 'isSocialLoginWallet').mockReturnValue(true) - jest.spyOn(constants, 'IS_PRODUCTION', 'get').mockImplementation(() => false) - - const { getByText } = render( - , - ) - - expect(getByText('Delete account')).toBeInTheDocument() - }) - - it('should not display a Delete Account on prod', () => { - jest.spyOn(mpcModule, 'isSocialLoginWallet').mockReturnValue(true) - jest.spyOn(constants, 'IS_PRODUCTION', 'get').mockImplementation(() => true) - - const { queryByText } = render( - , - ) - - expect(queryByText('Delete account')).not.toBeInTheDocument() - }) - - it('should not display a Delete Account if not social login', () => { - jest.spyOn(mpcModule, 'isSocialLoginWallet').mockReturnValue(false) - jest.spyOn(constants, 'IS_PRODUCTION', 'get').mockImplementation(() => false) - - const { queryByText } = render( - , - ) - - expect(queryByText('Delete account')).not.toBeInTheDocument() - }) - - it('should display an enable mfa button if mfa is not enabled', () => { - jest.spyOn(mpcModule, 'isSocialLoginWallet').mockReturnValue(true) - - const { getByText } = render( - , - ) - - expect(getByText('Add multifactor authentication')).toBeInTheDocument() - }) - - it('should not display an enable mfa button if mfa is already enabled', () => { - jest.spyOn(mpcModule, 'isSocialLoginWallet').mockReturnValue(true) - - // Mock that MFA is enabled - socialWalletService.enableMFA('', '') - - const { queryByText } = render( - , - ) - - expect(queryByText('Add multifactor authentication')).not.toBeInTheDocument() - }) }) diff --git a/src/components/common/WalletInfo/index.tsx b/src/components/common/WalletInfo/index.tsx index f083e57895..23c1af8cbd 100644 --- a/src/components/common/WalletInfo/index.tsx +++ b/src/components/common/WalletInfo/index.tsx @@ -2,21 +2,13 @@ import WalletBalance from '@/components/common/WalletBalance' import { WalletIdenticon } from '@/components/common/WalletOverview' import { Box, Button, Typography } from '@mui/material' import css from './styles.module.css' -import SocialLoginInfo from '@/components/common/SocialLoginInfo' -import Link from 'next/link' -import { AppRoutes } from '@/config/routes' -import LockIcon from '@/public/images/common/lock-small.svg' import EthHashInfo from '@/components/common/EthHashInfo' import ChainSwitcher from '@/components/common/ChainSwitcher' -import { IS_PRODUCTION } from '@/config/constants' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' import useOnboard, { type ConnectedWallet, switchWallet } from '@/hooks/wallets/useOnboard' -import { useRouter } from 'next/router' import useAddressBook from '@/hooks/useAddressBook' import { useAppSelector } from '@/store' import { selectChainById } from '@/store/chainsSlice' import madProps from '@/utils/mad-props' -import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew' import useChainId from '@/hooks/useChainId' @@ -24,23 +16,12 @@ type WalletInfoProps = { wallet: ConnectedWallet balance?: string | bigint currentChainId: ReturnType - socialWalletService: ReturnType - router: ReturnType onboard: ReturnType addressBook: ReturnType handleClose: () => void } -export const WalletInfo = ({ - wallet, - balance, - currentChainId, - socialWalletService, - router, - onboard, - addressBook, - handleClose, -}: WalletInfoProps) => { +export const WalletInfo = ({ wallet, balance, currentChainId, onboard, addressBook, handleClose }: WalletInfoProps) => { const chainInfo = useAppSelector((state) => selectChainById(state, wallet.chainId)) const prefix = chainInfo?.shortName @@ -51,8 +32,6 @@ export const WalletInfo = ({ } } - const resetAccount = () => socialWalletService?.__deleteAccount() - const handleDisconnect = () => { onboard?.disconnectWallet({ label: wallet.label, @@ -61,48 +40,22 @@ export const WalletInfo = ({ handleClose() } - const isSocialLogin = isSocialLoginWallet(wallet.label) - return ( <> - {isSocialLogin ? ( - - - - {socialWalletService && !socialWalletService.isMFAEnabled() && ( - - - - )} - - ) : ( - <> - - - - - - )} + + + + + @@ -148,20 +101,12 @@ export const WalletInfo = ({ > Disconnect - - {!IS_PRODUCTION && isSocialLogin && ( - - )} ) } export default madProps(WalletInfo, { - socialWalletService: useSocialWallet, - router: useRouter, onboard: useOnboard, addressBook: useAddressBook, currentChainId: useChainId, diff --git a/src/components/common/WalletOverview/index.tsx b/src/components/common/WalletOverview/index.tsx index c4339f536e..f60bfc830c 100644 --- a/src/components/common/WalletOverview/index.tsx +++ b/src/components/common/WalletOverview/index.tsx @@ -11,8 +11,6 @@ import { selectChainById } from '@/store/chainsSlice' import WalletBalance from '@/components/common/WalletBalance' import css from './styles.module.css' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' -import SocialLoginInfo from '@/components/common/SocialLoginInfo' export const WalletIdenticon = ({ wallet, size = 32 }: { wallet: ConnectedWallet; size?: number }) => { return ( @@ -39,22 +37,6 @@ const WalletOverview = ({ const walletChain = useAppSelector((state) => selectChainById(state, wallet.chainId)) const prefix = walletChain?.shortName - const isSocialLogin = isSocialLoginWallet(wallet.label) - - if (isSocialLogin) { - return ( -
- -
- ) - } - return ( diff --git a/src/components/common/WalletOverview/styles.module.css b/src/components/common/WalletOverview/styles.module.css index 6cf83c89fd..14dd5f668f 100644 --- a/src/components/common/WalletOverview/styles.module.css +++ b/src/components/common/WalletOverview/styles.module.css @@ -45,8 +45,4 @@ width: 22px; height: auto; } - - .socialLoginInfo > div > div:last-child { - display: none; - } } diff --git a/src/components/dashboard/ActivityRewardsSection/index.tsx b/src/components/dashboard/ActivityRewardsSection/index.tsx index 9cfa0f0b90..6d4d31ca5f 100644 --- a/src/components/dashboard/ActivityRewardsSection/index.tsx +++ b/src/components/dashboard/ActivityRewardsSection/index.tsx @@ -51,7 +51,7 @@ const ActivityRewardsSection = () => { } return ( - + <> { display: { xs: 'none', sm: 'block' }, }} /> - + {
- + - +
- + How it works
- - + +
-
+ ) } diff --git a/src/components/dashboard/ActivityRewardsSection/styles.module.css b/src/components/dashboard/ActivityRewardsSection/styles.module.css index 5d046345ef..92c6ac1639 100644 --- a/src/components/dashboard/ActivityRewardsSection/styles.module.css +++ b/src/components/dashboard/ActivityRewardsSection/styles.module.css @@ -2,7 +2,7 @@ position: relative; border: none; margin: 0; - padding: 40px; + padding: var(--space-4); } .loadErrorCard { @@ -67,7 +67,7 @@ .links { display: flex; - flex-wrap: wrap; + flex-wrap: nowrap; align-items: center; margin-top: var(--space-3); text-wrap: nowrap; diff --git a/src/components/dashboard/Assets/index.tsx b/src/components/dashboard/Assets/index.tsx new file mode 100644 index 0000000000..8df89c1644 --- /dev/null +++ b/src/components/dashboard/Assets/index.tsx @@ -0,0 +1,120 @@ +import { useMemo } from 'react' +import { Box, Skeleton, Typography, Paper } from '@mui/material' +import type { SafeBalanceResponse } from '@safe-global/safe-gateway-typescript-sdk' +import useBalances from '@/hooks/useBalances' +import FiatValue from '@/components/common/FiatValue' +import TokenAmount from '@/components/common/TokenAmount' +import SwapButton from '@/features/swap/components/SwapButton' +import { AppRoutes } from '@/config/routes' +import { WidgetContainer, WidgetBody, ViewAllLink } from '../styled' +import css from '../PendingTxs/styles.module.css' +import { useRouter } from 'next/router' +import { SWAP_LABELS } from '@/services/analytics/events/swaps' +import { useVisibleAssets } from '@/components/balances/AssetsTable/useHideAssets' +import BuyCryptoButton from '@/components/common/BuyCryptoButton' +import SendButton from '@/components/balances/AssetsTable/SendButton' +import useIsSwapFeatureEnabled from '@/features/swap/hooks/useIsSwapFeatureEnabled' + +const MAX_ASSETS = 5 + +const AssetsDummy = () => ( + + + {Array.from({ length: 2 }).map((_, index) => ( + + ))} + + +) + +const NoAssets = () => ( + + + Add funds to get started + + + + Add funds directly from your bank account or copy your address to send tokens from a different account. + + + + + + +) + +const AssetRow = ({ item, showSwap }: { item: SafeBalanceResponse['items'][number]; showSwap: boolean }) => ( + + + + + + + + + + + {showSwap ? ( + + ) : ( + + )} + + +) + +const AssetList = ({ items }: { items: SafeBalanceResponse['items'] }) => { + const isSwapFeatureEnabled = useIsSwapFeatureEnabled() + + return ( + + {items.map((item) => ( + + ))} + + ) +} + +const isNonZeroBalance = (item: SafeBalanceResponse['items'][number]) => item.balance !== '0' + +const AssetsWidget = () => { + const router = useRouter() + const { safe } = router.query + const { loading } = useBalances() + const visibleAssets = useVisibleAssets() + + const items = useMemo(() => { + return visibleAssets.filter(isNonZeroBalance).slice(0, MAX_ASSETS) + }, [visibleAssets]) + + const viewAllUrl = useMemo( + () => ({ + pathname: AppRoutes.balances.index, + query: { safe }, + }), + [safe], + ) + + return ( + +
+ + Top assets + + + {items.length > 0 && } +
+ + + {loading ? : items.length > 0 ? : } + +
+ ) +} + +export default AssetsWidget diff --git a/src/components/dashboard/CreationDialog/index.tsx b/src/components/dashboard/CreationDialog/index.tsx deleted file mode 100644 index c8c2325b36..0000000000 --- a/src/components/dashboard/CreationDialog/index.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import React, { type ElementType } from 'react' -import { Box, Button, Dialog, DialogContent, Grid, SvgIcon, Typography } from '@mui/material' -import { useRouter } from 'next/router' - -import HomeIcon from '@/public/images/sidebar/home.svg' -import TransactionIcon from '@/public/images/sidebar/transactions.svg' -import AppsIcon from '@/public/images/sidebar/apps.svg' -import SettingsIcon from '@/public/images/sidebar/settings.svg' -// import BeamerIcon from '@/public/images/sidebar/whats-new.svg' -import HelpCenterIcon from '@/public/images/sidebar/help-center.svg' -import { useRemoteSafeApps } from '@/hooks/safe-apps/useRemoteSafeApps' -import { useCurrentChain } from '@/hooks/useChains' -import { CREATION_MODAL_QUERY_PARM } from '@/components/new-safe/create/logic' - -const HintItem = ({ Icon, title, description }: { Icon: ElementType; title: string; description: string }) => { - return ( - - - - - {title} - - - - {description} - - ) -} - -const CreationDialog = () => { - const router = useRouter() - const [open, setOpen] = React.useState(true) - const [remoteSafeApps = []] = useRemoteSafeApps() - const chain = useCurrentChain() - - const onClose = () => { - const { [CREATION_MODAL_QUERY_PARM]: _, ...query } = router.query - router.replace({ pathname: router.pathname, query }) - - setOpen(false) - } - - return ( - - - - Welcome to {'Astar Safe'}! - - - Congratulations on your first step to truly unlock ownership. Enjoy the experience and discover our app. - - - - - - - - {/**/} - - - - - - - - - ) -} - -export default CreationDialog diff --git a/src/components/dashboard/FirstSteps/index.tsx b/src/components/dashboard/FirstSteps/index.tsx index cca867fe13..82eb34a5ea 100644 --- a/src/components/dashboard/FirstSteps/index.tsx +++ b/src/components/dashboard/FirstSteps/index.tsx @@ -137,6 +137,7 @@ const AddFundsWidget = ({ completed }: { completed: boolean }) => { {(isOk) => ( - {/* - - {'Astar Safe'} mobile signer key (optional){' '} - - - - - - - Use your mobile phone as an additional signer key - */}
diff --git a/src/components/new-safe/create/steps/ReviewStep/index.test.tsx b/src/components/new-safe/create/steps/ReviewStep/index.test.tsx index 8ebfc9bdaa..9ab659c24f 100644 --- a/src/components/new-safe/create/steps/ReviewStep/index.test.tsx +++ b/src/components/new-safe/create/steps/ReviewStep/index.test.tsx @@ -7,8 +7,7 @@ import { render } from '@/tests/test-utils' import ReviewStep, { NetworkFee } from '@/components/new-safe/create/steps/ReviewStep/index' import * as useWallet from '@/hooks/wallets/useWallet' import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' -import * as socialLogin from '@/services/mpc/SocialLoginModule' -import { act, fireEvent } from '@testing-library/react' +import { act, fireEvent, screen } from '@testing-library/react' const mockChainInfo = { chainId: '100', @@ -20,29 +19,13 @@ const mockChainInfo = { } as ChainInfo describe('NetworkFee', () => { - it('should display the total fee if not social login', () => { + it('should display the total fee', () => { jest.spyOn(useWallet, 'default').mockReturnValue({ label: 'MetaMask' } as unknown as ConnectedWallet) const mockTotalFee = '0.0123' const result = render() expect(result.getByText(`≈ ${mockTotalFee} ${mockChainInfo.nativeCurrency.symbol}`)).toBeInTheDocument() }) - - it('displays a sponsored by message for social login', () => { - jest.spyOn(useWallet, 'default').mockReturnValue({ label: 'Social Login' } as unknown as ConnectedWallet) - const result = render() - - expect(result.getByText(/Your account is sponsored by Gnosis/)).toBeInTheDocument() - }) - - it('displays an error message for social login if there are no relays left', () => { - jest.spyOn(useWallet, 'default').mockReturnValue({ label: 'Social Login' } as unknown as ConnectedWallet) - const result = render() - - expect( - result.getByText(/You have used up your 5 free transactions per hour. Please try again later/), - ).toBeInTheDocument() - }) }) describe('ReviewStep', () => { @@ -66,6 +49,21 @@ describe('ReviewStep', () => { expect(getByText('Pay now')).toBeInTheDocument() }) + it('should display a pay later option as selected by default for counterfactual safe setups', () => { + const mockData: NewSafeFormData = { + name: 'Test', + threshold: 1, + owners: [{ name: '', address: '0x1' }], + saltNonce: 0, + } + jest.spyOn(useChains, 'useHasFeature').mockReturnValue(true) + + render() + + const payLaterOption = screen.getByRole('radio', { name: /Pay later/i }) + expect(payLaterOption).toBeChecked() + }) + it('should not display the network fee for counterfactual safes', () => { const mockData: NewSafeFormData = { name: 'Test', @@ -129,7 +127,6 @@ describe('ReviewStep', () => { } jest.spyOn(useChains, 'useHasFeature').mockReturnValue(true) jest.spyOn(relay, 'hasRemainingRelays').mockReturnValue(true) - jest.spyOn(socialLogin, 'isSocialLoginWallet').mockReturnValue(false) const { getByText } = render( , diff --git a/src/components/new-safe/create/steps/ReviewStep/index.tsx b/src/components/new-safe/create/steps/ReviewStep/index.tsx index 15801600bf..5ed1f11c39 100644 --- a/src/components/new-safe/create/steps/ReviewStep/index.tsx +++ b/src/components/new-safe/create/steps/ReviewStep/index.tsx @@ -1,10 +1,12 @@ import ChainIndicator from '@/components/common/ChainIndicator' import type { NamedAddress } from '@/components/new-safe/create/types' import EthHashInfo from '@/components/common/EthHashInfo' +import { safeCreationDispatch, SafeCreationEvent } from '@/features/counterfactual/services/safeCreationEvents' +import { addUndeployedSafe } from '@/features/counterfactual/store/undeployedSafesSlice' import { getTotalFeeFormatted } from '@/hooks/useGasPrice' import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper' import type { NewSafeFormData } from '@/components/new-safe/create' -import { computeNewSafeAddress } from '@/components/new-safe/create/logic' +import { computeNewSafeAddress, createNewSafe, relaySafeCreation } from '@/components/new-safe/create/logic' import { getAvailableSaltNonce } from '@/components/new-safe/create/logic/utils' import NetworkWarning from '@/components/new-safe/create/NetworkWarning' import css from '@/components/new-safe/create/steps/ReviewStep/styles.module.css' @@ -14,33 +16,32 @@ import useSyncSafeCreationStep from '@/components/new-safe/create/useSyncSafeCre import ReviewRow from '@/components/new-safe/ReviewRow' import ErrorMessage from '@/components/tx/ErrorMessage' import { ExecutionMethod, ExecutionMethodSelector } from '@/components/tx/ExecutionMethodSelector' -import { RELAY_SPONSORS } from '@/components/tx/SponsoredBy' import { LATEST_SAFE_VERSION } from '@/config/constants' import PayNowPayLater, { PayMethod } from '@/features/counterfactual/PayNowPayLater' -import { createCounterfactualSafe } from '@/features/counterfactual/utils' +import { CF_TX_GROUP_KEY, createCounterfactualSafe } from '@/features/counterfactual/utils' import { useCurrentChain, useHasFeature } from '@/hooks/useChains' import useGasPrice from '@/hooks/useGasPrice' import useIsWrongChain from '@/hooks/useIsWrongChain' -import { MAX_HOUR_RELAYS, useLeastRemainingRelays } from '@/hooks/useRemainingRelays' +import { useLeastRemainingRelays } from '@/hooks/useRemainingRelays' import useWalletCanPay from '@/hooks/useWalletCanPay' import useWallet from '@/hooks/wallets/useWallet' import { useWeb3 } from '@/hooks/wallets/web3' import { CREATE_SAFE_CATEGORY, CREATE_SAFE_EVENTS, OVERVIEW_EVENTS, trackEvent } from '@/services/analytics' import { gtmSetSafeAddress } from '@/services/analytics/gtm' import { getReadOnlyFallbackHandlerContract } from '@/services/contracts/safeContracts' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' +import { asError } from '@/services/exceptions/utils' import { useAppDispatch } from '@/store' -import { FEATURES } from '@/utils/chains' +import { FEATURES, hasFeature } from '@/utils/chains' import { hasRemainingRelays } from '@/utils/relaying' +import { isWalletRejection } from '@/utils/wallets' import ArrowBackIcon from '@mui/icons-material/ArrowBack' -import { Alert, Box, Button, CircularProgress, Divider, Grid, Typography } from '@mui/material' +import { Box, Button, CircularProgress, Divider, Grid, Typography } from '@mui/material' import { type DeploySafeProps } from '@safe-global/protocol-kit' +import type { SafeVersion } from '@safe-global/safe-core-sdk-types' import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' import classnames from 'classnames' -import Image from 'next/image' import { useRouter } from 'next/router' import { useMemo, useState } from 'react' -import { usePendingSafe } from '../StatusStep/usePendingSafe' export const NetworkFee = ({ totalFee, @@ -55,45 +56,14 @@ export const NetworkFee = ({ }) => { const wallet = useWallet() - const isSocialLogin = isSocialLoginWallet(wallet?.label) - - if (!isSocialLogin) { - return ( - - - - ≈ {totalFee} {chain?.nativeCurrency.symbol} - - - - ) - } - - if (willRelay) { - const sponsor = RELAY_SPONSORS[chain?.chainId || ''] || RELAY_SPONSORS.default - return ( - <> - Free - - Your account is sponsored by - {sponsor.name}{' '} - {sponsor.name} - - - ) - } - return ( - - You have used up your {MAX_HOUR_RELAYS} free transactions per hour. Please try again later. - + + + + ≈ {totalFee} {chain?.nativeCurrency.symbol} + + + ) } @@ -152,12 +122,12 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps(false) const [submitError, setSubmitError] = useState() const isCounterfactualEnabled = useHasFeature(FEATURES.COUNTERFACTUAL) + const isEIP1559 = chain && hasFeature(chain, FEATURES.EIP1559) const ownerAddresses = useMemo(() => data.owners.map((owner) => owner.address), [data.owners]) const [minRelays] = useLeastRemainingRelays(ownerAddresses) @@ -209,8 +179,8 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps { + dispatch(addUndeployedSafe(undeployedSafe)) + + if (taskId) { + safeCreationDispatch(SafeCreationEvent.RELAYING, { groupKey: CF_TX_GROUP_KEY, taskId, safeAddress }) + } + + if (txHash) { + safeCreationDispatch(SafeCreationEvent.PROCESSING, { + groupKey: CF_TX_GROUP_KEY, + txHash, + safeAddress, + }) + } + + trackEvent(CREATE_SAFE_EVENTS.SUBMIT_CREATE_SAFE) + trackEvent({ ...OVERVIEW_EVENTS.PROCEED_WITH_TX, label: 'deployment', category: CREATE_SAFE_CATEGORY }) - setPendingSafe(pendingSafe) - onSubmit(pendingSafe) + onSubmit(data) + } + + if (willRelay) { + const taskId = await relaySafeCreation( + chain, + props.safeAccountConfig.owners, + props.safeAccountConfig.threshold, + Number(saltNonce), + ) + onSubmitCallback(taskId) + } else { + await createNewSafe(provider, { + safeAccountConfig: props.safeAccountConfig, + saltNonce, + options, + callback: (txHash) => { + onSubmitCallback(undefined, txHash) + }, + }) + } } catch (_err) { - setSubmitError('Error creating the Safe Account. Please try again later.') + const error = asError(_err) + const submitError = isWalletRejection(error) + ? 'User rejected signing.' + : 'Error creating the Safe Account. Please try again later.' + setSubmitError(submitError) + + if (isWalletRejection(error)) { + trackEvent(CREATE_SAFE_EVENTS.REJECT_CREATE_SAFE) + } } setIsCreating(false) } - const isSocialLogin = isSocialLoginWallet(wallet?.label) - const isDisabled = isWrongChain || (isSocialLogin && !willRelay) || isCreating + const isDisabled = isWrongChain || isCreating return ( <> @@ -254,7 +280,7 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps - {canRelay && !isSocialLogin && payMethod === PayMethod.PayNow && ( + {canRelay && payMethod === PayMethod.PayNow && ( - {canRelay && !isSocialLogin && ( + {canRelay && ( - {!willRelay && !isSocialLogin && ( + {!willRelay && ( You will have to confirm a transaction with your connected wallet. diff --git a/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx b/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx index 3705ff4e7c..3776d953f7 100644 --- a/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx +++ b/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx @@ -1,91 +1,79 @@ -import { Box, Typography } from '@mui/material' -import { SafeCreationStatus } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' +import ExternalLink from '@/components/common/ExternalLink' import LoadingSpinner, { SpinnerStatus } from '@/components/new-safe/create/steps/StatusStep/LoadingSpinner' +import { SafeCreationEvent } from '@/features/counterfactual/services/safeCreationEvents' +import type { UndeployedSafe } from '@/features/counterfactual/store/undeployedSafesSlice' +import { useCurrentChain } from '@/hooks/useChains' +import { getBlockExplorerLink } from '@/utils/chains' +import { Box, Typography } from '@mui/material' +import FailedIcon from '@/public/images/common/tx-failed.svg' -const getStep = (status: SafeCreationStatus) => { - const ERROR_TEXT = 'Please cancel the process or retry the transaction.' - +const getStep = (status: SafeCreationEvent) => { switch (status) { - case SafeCreationStatus.AWAITING: - return { - description: 'Waiting for transaction confirmation.', - instruction: 'Please confirm the transaction with your connected wallet.', - } - case SafeCreationStatus.WALLET_REJECTED: - return { - description: 'Transaction was rejected.', - instruction: ERROR_TEXT, - } - case SafeCreationStatus.PROCESSING: - return { - description: 'Transaction is being executed.', - instruction: 'Please do not leave this page.', - } - case SafeCreationStatus.ERROR: + case SafeCreationEvent.PROCESSING: + case SafeCreationEvent.RELAYING: return { - description: 'There was an error.', - instruction: ERROR_TEXT, + description: 'We are activating your account', + instruction: 'It can take some minutes to create your account, but you can check the progress below.', } - case SafeCreationStatus.REVERTED: + case SafeCreationEvent.FAILED: return { - description: 'Transaction was reverted.', - instruction: ERROR_TEXT, + description: "Your account couldn't be created", + instruction: + 'The creation transaction was rejected by the connected wallet. You can retry or create an account from scratch.', } - case SafeCreationStatus.TIMEOUT: + case SafeCreationEvent.REVERTED: return { - description: 'Transaction was not found. Be aware that it might still be processed.', - instruction: ERROR_TEXT, + description: "Your account couldn't be created", + instruction: 'The creation transaction reverted. You can retry or create an account from scratch.', } - case SafeCreationStatus.SUCCESS: + case SafeCreationEvent.SUCCESS: return { description: 'Your Safe Account is being indexed..', instruction: 'The account will be ready for use shortly. Please do not leave this page.', } - case SafeCreationStatus.INDEXED: + case SafeCreationEvent.INDEXED: return { description: 'Your Safe Account was successfully created!', instruction: '', } - case SafeCreationStatus.INDEX_FAILED: - return { - description: 'Your Safe Account is successfully created!', - instruction: - 'You can already open Astar Safe. It might take a moment until it becomes fully usable in the interface.', - } } } -const StatusMessage = ({ status, isError }: { status: SafeCreationStatus; isError: boolean }) => { +const StatusMessage = ({ + status, + isError, + pendingSafe, +}: { + status: SafeCreationEvent + isError: boolean + pendingSafe: UndeployedSafe | undefined +}) => { const stepInfo = getStep(status) + const chain = useCurrentChain() - const color = isError ? 'error' : 'info' - const isSuccess = status >= SafeCreationStatus.SUCCESS - const spinnerStatus = isError ? SpinnerStatus.ERROR : isSuccess ? SpinnerStatus.SUCCESS : SpinnerStatus.PROCESSING + const isSuccess = status === SafeCreationEvent.SUCCESS + const spinnerStatus = isSuccess ? SpinnerStatus.SUCCESS : SpinnerStatus.PROCESSING + const explorerLink = + chain && pendingSafe?.status.txHash ? getBlockExplorerLink(chain, pendingSafe.status.txHash) : undefined return ( <> - - + + {isError ? : } + + {stepInfo.description} - {stepInfo.instruction && ( - ({ - backgroundColor: palette[color].background, - borderColor: palette[color].light, - borderWidth: 1, - borderStyle: 'solid', - borderRadius: '6px', - })} - padding={3} - mt={4} - mb={0} - > - {stepInfo.instruction} - - )} + + {stepInfo.instruction && ( + + {stepInfo.instruction} + + )} + {!isError && explorerLink && Check Status} + ) } diff --git a/src/components/new-safe/create/steps/StatusStep/StatusStepper.tsx b/src/components/new-safe/create/steps/StatusStep/StatusStepper.tsx deleted file mode 100644 index de73ce73b6..0000000000 --- a/src/components/new-safe/create/steps/StatusStep/StatusStepper.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Box, Step, StepConnector, Stepper, Typography } from '@mui/material' -import css from '@/components/new-safe/create/steps/StatusStep/styles.module.css' -import EthHashInfo from '@/components/common/EthHashInfo' -import { SafeCreationStatus } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' -import StatusStep from '@/components/new-safe/create/steps/StatusStep/StatusStep' -import { usePendingSafe } from './usePendingSafe' - -const StatusStepper = ({ status }: { status: SafeCreationStatus }) => { - const [pendingSafe] = usePendingSafe() - if (!pendingSafe?.safeAddress) return null - - return ( - }> - - - - - Your Safe Account address - - - - - - - - - - Validating transaction - - {pendingSafe.txHash && ( - - )} - - - - - - - Indexing - - - - - - - Safe Account is ready - - - - - ) -} - -export default StatusStepper diff --git a/src/components/new-safe/create/steps/StatusStep/__tests__/index.test.tsx b/src/components/new-safe/create/steps/StatusStep/__tests__/index.test.tsx deleted file mode 100644 index 31b5aae5ec..0000000000 --- a/src/components/new-safe/create/steps/StatusStep/__tests__/index.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { SafeCreationStatus } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' -import { render } from '@/tests/test-utils' -import { CreateSafeStatus } from '@/components/new-safe/create/steps/StatusStep' -import { type NewSafeFormData } from '@/components/new-safe/create' -import * as useSafeCreation from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' - -describe('StatusStep', () => { - it('should call useSafeCreation with PROCESSING status if relaying', () => { - const useSafeCreationSpy = jest.spyOn(useSafeCreation, 'useSafeCreation') - - render( - {}} - onBack={() => {}} - setStep={() => {}} - />, - ) - - expect(useSafeCreationSpy).toHaveBeenCalledWith(SafeCreationStatus.PROCESSING, expect.anything(), true) - }) -}) diff --git a/src/components/new-safe/create/steps/StatusStep/__tests__/usePendingSafe.test.ts b/src/components/new-safe/create/steps/StatusStep/__tests__/usePendingSafe.test.ts deleted file mode 100644 index 03cad0f74c..0000000000 --- a/src/components/new-safe/create/steps/StatusStep/__tests__/usePendingSafe.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { renderHook } from '@/tests/test-utils' -import { usePendingSafe } from '../usePendingSafe' - -import { toBeHex } from 'ethers' -import { useCurrentChain } from '@/hooks/useChains' - -// mock useCurrentChain -jest.mock('@/hooks/useChains', () => ({ - useCurrentChain: jest.fn(() => ({ - shortName: 'gor', - chainId: '5', - chainName: 'Goerli', - features: [], - })), -})) - -describe('usePendingSafe()', () => { - const mockPendingSafe1 = { - name: 'joyful-rinkeby-safe', - threshold: 1, - owners: [], - saltNonce: 123, - address: toBeHex('0x10', 20), - } - const mockPendingSafe2 = { - name: 'joyful-rinkeby-safe', - threshold: 1, - owners: [], - saltNonce: 123, - address: toBeHex('0x10', 20), - } - - beforeEach(() => { - window.localStorage.clear() - }) - it('Should initially be undefined', () => { - const { result } = renderHook(() => usePendingSafe()) - expect(result.current[0]).toBeUndefined() - }) - - it('Should set the pendingSafe per ChainId', async () => { - const { result, rerender } = renderHook(() => usePendingSafe()) - - result.current[1](mockPendingSafe1) - - rerender() - - expect(result.current[0]).toEqual(mockPendingSafe1) - ;(useCurrentChain as jest.Mock).mockImplementation(() => ({ - shortName: 'eth', - chainId: '1', - chainName: 'Ethereum', - features: [], - })) - - rerender() - expect(result.current[0]).toEqual(undefined) - - result.current[1](mockPendingSafe2) - rerender() - expect(result.current[0]).toEqual(mockPendingSafe2) - ;(useCurrentChain as jest.Mock).mockImplementation(() => ({ - shortName: 'gor', - chainId: '5', - chainName: 'Goerli', - features: [], - })) - rerender() - expect(result.current[0]).toEqual(mockPendingSafe1) - }) -}) diff --git a/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreation.test.ts b/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreation.test.ts deleted file mode 100644 index 2fc29f51b4..0000000000 --- a/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreation.test.ts +++ /dev/null @@ -1,348 +0,0 @@ -import { renderHook } from '@/tests/test-utils' -import { SafeCreationStatus, useSafeCreation } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' -import * as web3 from '@/hooks/wallets/web3' -import * as chain from '@/hooks/useChains' -import * as wallet from '@/hooks/wallets/useWallet' -import * as logic from '@/components/new-safe/create/logic' -import * as contracts from '@/services/contracts/safeContracts' -import * as txMonitor from '@/services/tx/txMonitor' -import * as usePendingSafe from '@/components/new-safe/create/steps/StatusStep/usePendingSafe' -import { BrowserProvider, zeroPadValue, type JsonRpcProvider } from 'ethers' -import type { ConnectedWallet } from '@/hooks/wallets/useOnboard' -import { chainBuilder } from '@/tests/builders/chains' -import { waitFor } from '@testing-library/react' -import type Safe from '@safe-global/protocol-kit' -import type CompatibilityFallbackHandlerEthersContract from '@safe-global/protocol-kit/dist/src/adapters/ethers/contracts/CompatibilityFallbackHandler/CompatibilityFallbackHandlerEthersContract' -import { FEATURES } from '@safe-global/safe-gateway-typescript-sdk' -import * as gasPrice from '@/hooks/useGasPrice' -import { MockEip1193Provider } from '@/tests/mocks/providers' - -const mockSafeInfo = { - data: '0x', - from: '0x1', - to: '0x2', - nonce: 1, - value: BigInt(0), - startBlock: 1, -} - -jest.mock('@safe-global/protocol-kit', () => { - const originalModule = jest.requireActual('@safe-global/protocol-kit') - - // Mock class - class MockEthersAdapter extends originalModule.EthersAdapter { - getChainId = jest.fn().mockImplementation(() => Promise.resolve(BigInt(4))) - } - - return { - ...originalModule, - EthersAdapter: MockEthersAdapter, - } -}) - -describe('useSafeCreation', () => { - const mockPendingSafe = { - name: 'joyful-rinkeby-safe', - threshold: 1, - owners: [], - saltNonce: 123, - address: '0x10', - } - const mockSetPendingSafe = jest.fn() - const mockStatus = SafeCreationStatus.AWAITING - const mockSetStatus = jest.fn() - const mockProvider: BrowserProvider = new BrowserProvider(MockEip1193Provider) - const mockReadOnlyProvider = { - getCode: jest.fn(), - } as unknown as JsonRpcProvider - - beforeEach(() => { - jest.resetAllMocks() - jest.restoreAllMocks() - - const mockChain = chainBuilder().with({ features: [] }).build() - jest.spyOn(web3, 'useWeb3').mockImplementation(() => mockProvider) - jest.spyOn(web3, 'getWeb3ReadOnly').mockImplementation(() => mockReadOnlyProvider) - jest.spyOn(web3, 'useWeb3ReadOnly').mockImplementation(() => mockReadOnlyProvider) - jest.spyOn(chain, 'useCurrentChain').mockImplementation(() => mockChain) - jest.spyOn(wallet, 'default').mockReturnValue({} as ConnectedWallet) - jest.spyOn(logic, 'getSafeCreationTxInfo').mockReturnValue(Promise.resolve(mockSafeInfo)) - jest.spyOn(logic, 'estimateSafeCreationGas').mockReturnValue(Promise.resolve(BigInt(200000))) - jest.spyOn(contracts, 'getReadOnlyFallbackHandlerContract').mockResolvedValue({ - getAddress: () => zeroPadValue('0x0123', 20), - } as unknown as CompatibilityFallbackHandlerEthersContract) - jest - .spyOn(gasPrice, 'default') - .mockReturnValue([{ maxFeePerGas: BigInt(123), maxPriorityFeePerGas: undefined }, undefined, false]) - }) - - it('should create a safe with gas params if there is no txHash and status is AWAITING', async () => { - const createSafeSpy = jest.spyOn(logic, 'createNewSafe').mockReturnValue(Promise.resolve({} as Safe)) - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([mockPendingSafe, mockSetPendingSafe]) - renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).toHaveBeenCalled() - - const { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = createSafeSpy.mock.calls[0][1].options || {} - - expect(gasPrice).toBe('123') - - expect(maxFeePerGas).toBeUndefined() - expect(maxPriorityFeePerGas).toBeUndefined() - }) - }) - - it('should create a safe with EIP-1559 gas params if there is no txHash and status is AWAITING', async () => { - jest - .spyOn(gasPrice, 'default') - .mockReturnValue([{ maxFeePerGas: BigInt(123), maxPriorityFeePerGas: BigInt(456) }, undefined, false]) - - jest.spyOn(chain, 'useCurrentChain').mockImplementation(() => - chainBuilder() - .with({ features: [FEATURES.EIP1559] }) - .build(), - ) - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([mockPendingSafe, mockSetPendingSafe]) - - const createSafeSpy = jest.spyOn(logic, 'createNewSafe').mockReturnValue(Promise.resolve({} as Safe)) - - renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).toHaveBeenCalled() - - const { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = createSafeSpy.mock.calls[0][1].options || {} - - expect(maxFeePerGas).toBe('123') - expect(maxPriorityFeePerGas).toBe('456') - - expect(gasPrice).toBeUndefined() - }) - }) - - it('should create a safe with no gas params if the gas estimation threw, there is no txHash and status is AWAITING', async () => { - jest.spyOn(gasPrice, 'default').mockReturnValue([undefined, Error('Error for testing'), false]) - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([mockPendingSafe, mockSetPendingSafe]) - - const createSafeSpy = jest.spyOn(logic, 'createNewSafe').mockReturnValue(Promise.resolve({} as Safe)) - - renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).toHaveBeenCalled() - - const { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = createSafeSpy.mock.calls[0][1].options || {} - - expect(gasPrice).toBeUndefined() - expect(maxFeePerGas).toBeUndefined() - expect(maxPriorityFeePerGas).toBeUndefined() - }) - }) - - it('should not create a safe if there is no txHash, status is AWAITING but gas is loading', async () => { - jest.spyOn(gasPrice, 'default').mockReturnValue([undefined, undefined, true]) - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([mockPendingSafe, mockSetPendingSafe]) - - const createSafeSpy = jest.spyOn(logic, 'createNewSafe').mockReturnValue(Promise.resolve({} as Safe)) - - renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).not.toHaveBeenCalled() - }) - }) - - it('should not create a safe if the status is not AWAITING', async () => { - const createSafeSpy = jest.spyOn(logic, 'createNewSafe') - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([mockPendingSafe, mockSetPendingSafe]) - - renderHook(() => useSafeCreation(SafeCreationStatus.WALLET_REJECTED, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).not.toHaveBeenCalled() - }) - - renderHook(() => useSafeCreation(SafeCreationStatus.PROCESSING, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).not.toHaveBeenCalled() - }) - - renderHook(() => useSafeCreation(SafeCreationStatus.ERROR, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).not.toHaveBeenCalled() - }) - - renderHook(() => useSafeCreation(SafeCreationStatus.REVERTED, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).not.toHaveBeenCalled() - }) - - renderHook(() => useSafeCreation(SafeCreationStatus.TIMEOUT, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).not.toHaveBeenCalled() - }) - - renderHook(() => useSafeCreation(SafeCreationStatus.SUCCESS, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).not.toHaveBeenCalled() - }) - - renderHook(() => useSafeCreation(SafeCreationStatus.INDEXED, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).not.toHaveBeenCalled() - }) - - renderHook(() => useSafeCreation(SafeCreationStatus.INDEX_FAILED, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).not.toHaveBeenCalled() - }) - }) - - it('should not create a safe if there is a txHash', async () => { - const createSafeSpy = jest.spyOn(logic, 'createNewSafe') - jest - .spyOn(usePendingSafe, 'usePendingSafe') - .mockReturnValue([{ ...mockPendingSafe, txHash: '0x123' }, mockSetPendingSafe]) - - renderHook(() => useSafeCreation(SafeCreationStatus.AWAITING, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).not.toHaveBeenCalled() - }) - }) - - it('should watch a tx if there is a txHash and a tx object', async () => { - const watchSafeTxSpy = jest.spyOn(logic, 'checkSafeCreationTx') - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([ - { - ...mockPendingSafe, - txHash: '0x123', - tx: { - data: '0x', - from: '0x1234', - nonce: 0, - startBlock: 0, - to: '0x456', - value: BigInt(0), - }, - }, - mockSetPendingSafe, - ]) - renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) - - await waitFor(() => { - expect(watchSafeTxSpy).toHaveBeenCalledTimes(1) - }) - }) - - it('should watch a tx even if no wallet is connected', async () => { - jest.spyOn(wallet, 'default').mockReturnValue(null) - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([ - { - ...mockPendingSafe, - txHash: '0x123', - tx: { - data: '0x', - from: '0x1234', - nonce: 0, - startBlock: 0, - to: '0x456', - value: BigInt(0), - }, - }, - mockSetPendingSafe, - ]) - const watchSafeTxSpy = jest.spyOn(logic, 'checkSafeCreationTx') - - renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) - - await waitFor(() => { - expect(watchSafeTxSpy).toHaveBeenCalledTimes(1) - }) - }) - - it('should not watch a tx if there is no txHash', async () => { - const watchSafeTxSpy = jest.spyOn(logic, 'checkSafeCreationTx') - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([mockPendingSafe, mockSetPendingSafe]) - renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) - - await waitFor(() => { - expect(watchSafeTxSpy).not.toHaveBeenCalled() - }) - }) - - it('should not watch a tx if there is no tx object', async () => { - const watchSafeTxSpy = jest.spyOn(logic, 'checkSafeCreationTx') - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([ - { - ...mockPendingSafe, - tx: { - data: '0x', - from: '0x1234', - nonce: 0, - startBlock: 0, - to: '0x456', - value: BigInt(0), - }, - }, - mockSetPendingSafe, - ]) - renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) - - await waitFor(() => { - expect(watchSafeTxSpy).not.toHaveBeenCalled() - }) - }) - - it('should set a PROCESSING state when watching a tx', async () => { - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([ - { - ...mockPendingSafe, - txHash: '0x123', - tx: { - data: '0x', - from: '0x1234', - nonce: 0, - startBlock: 0, - to: '0x456', - value: BigInt(0), - }, - }, - mockSetPendingSafe, - ]) - - renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) - - await waitFor(() => { - expect(mockSetStatus).toHaveBeenCalledWith(SafeCreationStatus.PROCESSING) - }) - }) - - it('should set a PROCESSING state and monitor relay taskId after successfully tx relay', async () => { - jest.spyOn(logic, 'relaySafeCreation').mockResolvedValue('0x456') - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([ - { - ...mockPendingSafe, - }, - mockSetPendingSafe, - ]) - const txMonitorSpy = jest.spyOn(txMonitor, 'waitForCreateSafeTx').mockImplementation(jest.fn()) - - const initialStatus = SafeCreationStatus.PROCESSING - - renderHook(() => useSafeCreation(initialStatus, mockSetStatus, true)) - - await waitFor(() => { - expect(mockSetStatus).toHaveBeenCalledWith(SafeCreationStatus.PROCESSING) - expect(txMonitorSpy).toHaveBeenCalledWith('0x456', expect.anything()) - }) - }) -}) diff --git a/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreationEffects.test.ts b/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreationEffects.test.ts deleted file mode 100644 index 2c878565a0..0000000000 --- a/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreationEffects.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { renderHook } from '@/tests/test-utils' -import { SafeCreationStatus } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' -import { type SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' -import * as web3 from '@/hooks/wallets/web3' -import * as pendingSafe from '@/components/new-safe/create/logic' -import * as usePendingSafe from '@/components/new-safe/create/steps/StatusStep/usePendingSafe' -import * as addressbook from '@/components/new-safe/create/logic/address-book' -import useSafeCreationEffects from '@/components/new-safe/create/steps/StatusStep/useSafeCreationEffects' -import type { PendingSafeData } from '@/components/new-safe/create/types' -import { toBeHex, BrowserProvider } from 'ethers' -import { MockEip1193Provider } from '@/tests/mocks/providers' - -describe('useSafeCreationEffects', () => { - beforeEach(() => { - jest.resetAllMocks() - jest.spyOn(pendingSafe, 'pollSafeInfo').mockImplementation(jest.fn(() => Promise.resolve({} as SafeInfo))) - jest.spyOn(addressbook, 'updateAddressBook').mockReturnValue(() => {}) - - const mockProvider: BrowserProvider = new BrowserProvider(MockEip1193Provider) - jest.spyOn(web3, 'useWeb3').mockImplementation(() => mockProvider) - }) - - it('should clear the tx hash if it exists on ERROR or REVERTED', () => { - const setStatusSpy = jest.fn() - const setPendingSafeSpy = jest.fn() - jest - .spyOn(usePendingSafe, 'usePendingSafe') - .mockReturnValue([{ txHash: '0x123' } as PendingSafeData, setPendingSafeSpy]) - - renderHook(() => - useSafeCreationEffects({ - status: SafeCreationStatus.ERROR, - setStatus: setStatusSpy, - }), - ) - - expect(setPendingSafeSpy).toHaveBeenCalled() - }) - - it('should not clear the tx hash if it doesnt exist on ERROR or REVERTED', () => { - const setStatusSpy = jest.fn() - const setPendingSafeSpy = jest.fn() - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([{} as PendingSafeData, setPendingSafeSpy]) - renderHook(() => - useSafeCreationEffects({ - status: SafeCreationStatus.ERROR, - setStatus: setStatusSpy, - }), - ) - - expect(setPendingSafeSpy).not.toHaveBeenCalled() - }) - - it('should poll safe info on SUCCESS', () => { - const pollSafeInfoSpy = jest.spyOn(pendingSafe, 'pollSafeInfo') - const setStatusSpy = jest.fn() - const setPendingSafeSpy = jest.fn() - jest - .spyOn(usePendingSafe, 'usePendingSafe') - .mockReturnValue([{ safeAddress: toBeHex('0x123', 20) } as PendingSafeData, setPendingSafeSpy]) - renderHook(() => - useSafeCreationEffects({ - status: SafeCreationStatus.SUCCESS, - setStatus: setStatusSpy, - }), - ) - - expect(pollSafeInfoSpy).toHaveBeenCalled() - }) - - it('should not poll safe info on SUCCESS if there is no safe address', () => { - const pollSafeInfoSpy = jest.spyOn(pendingSafe, 'pollSafeInfo') - const setStatusSpy = jest.fn() - const setPendingSafeSpy = jest.fn() - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([{} as PendingSafeData, setPendingSafeSpy]) - renderHook(() => - useSafeCreationEffects({ - status: SafeCreationStatus.SUCCESS, - setStatus: setStatusSpy, - }), - ) - - expect(pollSafeInfoSpy).not.toHaveBeenCalled() - }) -}) diff --git a/src/components/new-safe/create/steps/StatusStep/index.tsx b/src/components/new-safe/create/steps/StatusStep/index.tsx index 4df5768ef0..2a6b01ac0d 100644 --- a/src/components/new-safe/create/steps/StatusStep/index.tsx +++ b/src/components/new-safe/create/steps/StatusStep/index.tsx @@ -1,75 +1,64 @@ -import { useCallback, useEffect, useState } from 'react' -import { Box, Button, Divider, Paper, Tooltip, Typography } from '@mui/material' -import { useRouter } from 'next/router' - -import Track from '@/components/common/Track' -import { CREATE_SAFE_EVENTS } from '@/services/analytics/events/createLoadSafe' -import StatusMessage from '@/components/new-safe/create/steps/StatusStep/StatusMessage' -import useWallet from '@/hooks/wallets/useWallet' -import useIsWrongChain from '@/hooks/useIsWrongChain' -import type { NewSafeFormData } from '@/components/new-safe/create' +import { useCounter } from '@/components/common/Notifications/useCounter' import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper' -import useSafeCreationEffects from '@/components/new-safe/create/steps/StatusStep/useSafeCreationEffects' -import { SafeCreationStatus, useSafeCreation } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' -import StatusStepper from '@/components/new-safe/create/steps/StatusStep/StatusStepper' -import { OPEN_SAFE_LABELS, OVERVIEW_EVENTS, trackEvent } from '@/services/analytics' +import type { NewSafeFormData } from '@/components/new-safe/create' import { getRedirect } from '@/components/new-safe/create/logic' -import layoutCss from '@/components/new-safe/create/styles.module.css' -import { AppRoutes } from '@/config/routes' +import { updateAddressBook } from '@/components/new-safe/create/logic/address-book' +import StatusMessage from '@/components/new-safe/create/steps/StatusStep/StatusMessage' +import useUndeployedSafe from '@/components/new-safe/create/steps/StatusStep/useUndeployedSafe' import lightPalette from '@/components/theme/lightPalette' +import { AppRoutes } from '@/config/routes' +import { safeCreationPendingStatuses } from '@/features/counterfactual/hooks/usePendingSafeStatuses' +import { SafeCreationEvent, safeCreationSubscribe } from '@/features/counterfactual/services/safeCreationEvents' import { useCurrentChain } from '@/hooks/useChains' -import { usePendingSafe } from './usePendingSafe' +import Rocket from '@/public/images/common/rocket.svg' +import { CREATE_SAFE_EVENTS, trackEvent } from '@/services/analytics' +import { useAppDispatch } from '@/store' +import { Alert, AlertTitle, Box, Button, Paper, Stack, SvgIcon, Typography } from '@mui/material' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' import useSyncSafeCreationStep from '../../useSyncSafeCreationStep' -export const getInitialCreationStatus = (willRelay: boolean): SafeCreationStatus => - willRelay ? SafeCreationStatus.PROCESSING : SafeCreationStatus.AWAITING +const SPEED_UP_THRESHOLD_IN_SECONDS = 15 -export const CreateSafeStatus = ({ data, setProgressColor, setStep }: StepRenderProps) => { +export const CreateSafeStatus = ({ + data, + setProgressColor, + setStep, + setStepData, +}: StepRenderProps) => { + const [status, setStatus] = useState(SafeCreationEvent.PROCESSING) + const [safeAddress, pendingSafe] = useUndeployedSafe() const router = useRouter() - const chainInfo = useCurrentChain() - const chainPrefix = chainInfo?.shortName || '' - const wallet = useWallet() - const isWrongChain = useIsWrongChain() - const isConnected = wallet && !isWrongChain - const [pendingSafe, setPendingSafe] = usePendingSafe() - useSyncSafeCreationStep(setStep) - - // The willRelay flag can come from the previous step or from local storage - const willRelay = !!(data.willRelay || pendingSafe?.willRelay) - const initialStatus = getInitialCreationStatus(willRelay) - const [status, setStatus] = useState(initialStatus) - - const { handleCreateSafe } = useSafeCreation(status, setStatus, willRelay) - - useSafeCreationEffects({ - status, - setStatus, - }) + const chain = useCurrentChain() + const dispatch = useAppDispatch() - const onClose = useCallback(() => { - setPendingSafe(undefined) + const counter = useCounter(pendingSafe?.status.submittedAt) - router.push(AppRoutes.welcome.index) - }, [router, setPendingSafe]) + const isError = status === SafeCreationEvent.FAILED || status === SafeCreationEvent.REVERTED - const handleRetry = useCallback(() => { - setStatus(initialStatus) - void handleCreateSafe() - }, [handleCreateSafe, initialStatus]) + useSyncSafeCreationStep(setStep) - const onFinish = useCallback(() => { - trackEvent(CREATE_SAFE_EVENTS.GET_STARTED) + useEffect(() => { + const unsubFns = Object.entries(safeCreationPendingStatuses).map(([event]) => + safeCreationSubscribe(event as SafeCreationEvent, async () => { + setStatus(event as SafeCreationEvent) + }), + ) + + return () => { + unsubFns.forEach((unsub) => unsub()) + } + }, []) - const { safeAddress } = pendingSafe || {} + useEffect(() => { + if (!chain || !safeAddress) return - if (safeAddress) { - setPendingSafe(undefined) - router.push(getRedirect(chainPrefix, safeAddress, router.query?.safeViewRedirectURL)) + if (status === SafeCreationEvent.SUCCESS) { + dispatch(updateAddressBook(chain.chainId, safeAddress, data.name, data.owners, data.threshold)) + router.push(getRedirect(chain.shortName, safeAddress, router.query?.safeViewRedirectURL)) } - }, [chainPrefix, pendingSafe, router, setPendingSafe]) - - const displaySafeLink = status >= SafeCreationStatus.INDEXED - const isError = status >= SafeCreationStatus.WALLET_REJECTED && status <= SafeCreationStatus.TIMEOUT + }, [dispatch, chain, data.name, data.owners, data.threshold, router, safeAddress, status]) useEffect(() => { if (!setProgressColor) return @@ -81,63 +70,64 @@ export const CreateSafeStatus = ({ data, setProgressColor, setStep }: StepRender } }, [isError, setProgressColor]) + const tryAgain = () => { + trackEvent(CREATE_SAFE_EVENTS.RETRY_CREATE_SAFE) + + if (!pendingSafe) { + setStep(0) + return + } + + setProgressColor?.(lightPalette.secondary.main) + setStep(2) + setStepData?.({ + owners: pendingSafe.props.safeAccountConfig.owners.map((owner) => ({ name: '', address: owner })), + name: '', + threshold: pendingSafe.props.safeAccountConfig.threshold, + saltNonce: Number(pendingSafe.props.safeDeploymentConfig?.saltNonce), + safeAddress, + }) + } + + const onCancel = () => { + trackEvent(CREATE_SAFE_EVENTS.CANCEL_CREATE_SAFE) + } + return ( - - - - - {!isError && pendingSafe && ( - <> - - - - - - )} - - {displaySafeLink && ( - <> - - - - - - - - )} - - {isError && ( - <> - - - - - - - - - - - - - - - - - )} + + + + )} + ) } diff --git a/src/components/new-safe/create/steps/StatusStep/usePendingSafe.ts b/src/components/new-safe/create/steps/StatusStep/usePendingSafe.ts deleted file mode 100644 index 08c3ac543e..0000000000 --- a/src/components/new-safe/create/steps/StatusStep/usePendingSafe.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useCurrentChain } from '@/hooks/useChains' -import useLocalStorage from '@/services/local-storage/useLocalStorage' -import { useCallback } from 'react' -import type { PendingSafeByChain, PendingSafeData } from '../../types' - -const SAFE_PENDING_CREATION_STORAGE_KEY = 'pendingSafe_v2' - -export const usePendingSafe = (): [PendingSafeData | undefined, (safe: PendingSafeData | undefined) => void] => { - const [pendingSafes, setPendingSafes] = useLocalStorage(SAFE_PENDING_CREATION_STORAGE_KEY) - - const chainInfo = useCurrentChain() - - const pendingSafe = chainInfo && pendingSafes?.[chainInfo.chainId] - const setPendingSafe = useCallback( - (safe: PendingSafeData | undefined) => { - if (!chainInfo?.chainId) { - return - } - - // Always copy the object because useLocalStorage does not check for deep equality when writing back to ls - const newPendingSafes = pendingSafes ? { ...pendingSafes } : {} - newPendingSafes[chainInfo.chainId] = safe - setPendingSafes(newPendingSafes) - }, - [chainInfo?.chainId, pendingSafes, setPendingSafes], - ) - - return [pendingSafe, setPendingSafe] -} diff --git a/src/components/new-safe/create/steps/StatusStep/useSafeCreation.ts b/src/components/new-safe/create/steps/StatusStep/useSafeCreation.ts deleted file mode 100644 index 47c7e4ce9d..0000000000 --- a/src/components/new-safe/create/steps/StatusStep/useSafeCreation.ts +++ /dev/null @@ -1,188 +0,0 @@ -import type { Dispatch, SetStateAction } from 'react' -import { useCallback, useEffect, useState } from 'react' -import { useWeb3, useWeb3ReadOnly } from '@/hooks/wallets/web3' -import { useCurrentChain } from '@/hooks/useChains' -import useWallet from '@/hooks/wallets/useWallet' -import type { EthersError } from '@/utils/ethers-utils' -import { getInitialCreationStatus } from '@/components/new-safe/create/steps/StatusStep/index' -import type { PendingSafeTx } from '@/components/new-safe/create/types' -import { - createNewSafe, - getSafeDeployProps, - checkSafeCreationTx, - getSafeCreationTxInfo, - handleSafeCreationError, - SAFE_CREATION_ERROR_KEY, - showSafeCreationError, - relaySafeCreation, - estimateSafeCreationGas, -} from '@/components/new-safe/create/logic' -import { useAppDispatch } from '@/store' -import { closeByGroupKey } from '@/store/notificationsSlice' -import { CREATE_SAFE_EVENTS, trackEvent } from '@/services/analytics' -import { waitForCreateSafeTx } from '@/services/tx/txMonitor' -import useGasPrice from '@/hooks/useGasPrice' -import { hasFeature } from '@/utils/chains' -import { FEATURES } from '@safe-global/safe-gateway-typescript-sdk' -import type { DeploySafeProps } from '@safe-global/protocol-kit' -import { usePendingSafe } from './usePendingSafe' - -export enum SafeCreationStatus { - AWAITING, - PROCESSING, - WALLET_REJECTED, - ERROR, - REVERTED, - TIMEOUT, - SUCCESS, - INDEXED, - INDEX_FAILED, -} - -export const useSafeCreation = ( - status: SafeCreationStatus, - setStatus: Dispatch>, - willRelay: boolean, -) => { - const [isCreating, setIsCreating] = useState(false) - const [isWatching, setIsWatching] = useState(false) - const dispatch = useAppDispatch() - const [pendingSafe, setPendingSafe] = usePendingSafe() - - const wallet = useWallet() - const provider = useWeb3() - const web3ReadOnly = useWeb3ReadOnly() - const chain = useCurrentChain() - const [gasPrice, , gasPriceLoading] = useGasPrice() - - const maxFeePerGas = gasPrice?.maxFeePerGas - const maxPriorityFeePerGas = gasPrice?.maxPriorityFeePerGas - - const isEIP1559 = chain && hasFeature(chain, FEATURES.EIP1559) - - const createSafeCallback = useCallback( - async (txHash: string, tx: PendingSafeTx) => { - setStatus(SafeCreationStatus.PROCESSING) - trackEvent(CREATE_SAFE_EVENTS.SUBMIT_CREATE_SAFE) - setPendingSafe(pendingSafe ? { ...pendingSafe, txHash, tx } : undefined) - }, - [setStatus, setPendingSafe, pendingSafe], - ) - - const handleCreateSafe = useCallback(async () => { - if (!pendingSafe || !provider || !chain || !wallet || isCreating || gasPriceLoading) return - - setIsCreating(true) - dispatch(closeByGroupKey({ groupKey: SAFE_CREATION_ERROR_KEY })) - - const { owners, threshold, saltNonce } = pendingSafe - const ownersAddresses = owners.map((owner) => owner.address) - - try { - if (willRelay) { - const taskId = await relaySafeCreation(chain, ownersAddresses, threshold, saltNonce) - - setPendingSafe(pendingSafe ? { ...pendingSafe, taskId } : undefined) - setStatus(SafeCreationStatus.PROCESSING) - waitForCreateSafeTx(taskId, setStatus) - } else { - const tx = await getSafeCreationTxInfo(provider, owners, threshold, saltNonce, chain, wallet) - - const safeParams = { - threshold, - owners: owners.map((owner) => owner.address), - saltNonce, - } - - const safeDeployProps = await getSafeDeployProps( - safeParams, - (txHash) => createSafeCallback(txHash, tx), - chain.chainId, - ) - - const gasLimit = await estimateSafeCreationGas(chain, provider, tx.from, safeParams) - - const options: DeploySafeProps['options'] = isEIP1559 - ? { - maxFeePerGas: maxFeePerGas?.toString(), - maxPriorityFeePerGas: maxPriorityFeePerGas?.toString(), - gasLimit: gasLimit.toString(), - } - : { gasPrice: maxFeePerGas?.toString(), gasLimit: gasLimit.toString() } - - await createNewSafe(provider, { - ...safeDeployProps, - options, - }) - setStatus(SafeCreationStatus.SUCCESS) - } - } catch (err) { - const _err = err as EthersError - const status = handleSafeCreationError(_err) - - setStatus(status) - - if (status !== SafeCreationStatus.SUCCESS) { - dispatch(showSafeCreationError(_err)) - } - } - - setIsCreating(false) - }, [ - chain, - createSafeCallback, - dispatch, - gasPriceLoading, - isCreating, - isEIP1559, - maxFeePerGas, - maxPriorityFeePerGas, - pendingSafe, - provider, - setPendingSafe, - setStatus, - wallet, - willRelay, - ]) - - const watchSafeTx = useCallback(async () => { - if (!pendingSafe?.tx || !pendingSafe?.txHash || !web3ReadOnly || isWatching) return - - setStatus(SafeCreationStatus.PROCESSING) - setIsWatching(true) - - const txStatus = await checkSafeCreationTx(web3ReadOnly, pendingSafe.tx, pendingSafe.txHash, dispatch) - setStatus(txStatus) - setIsWatching(false) - }, [isWatching, pendingSafe, web3ReadOnly, setStatus, dispatch]) - - // Create or monitor Safe creation - useEffect(() => { - if (status !== getInitialCreationStatus(willRelay)) return - - if (pendingSafe?.txHash && !isCreating) { - void watchSafeTx() - return - } - - if (pendingSafe?.taskId && !isCreating) { - waitForCreateSafeTx(pendingSafe.taskId, setStatus) - return - } - - void handleCreateSafe() - }, [ - handleCreateSafe, - isCreating, - pendingSafe?.taskId, - pendingSafe?.txHash, - setStatus, - status, - watchSafeTx, - willRelay, - ]) - - return { - handleCreateSafe, - } -} diff --git a/src/components/new-safe/create/steps/StatusStep/useSafeCreationEffects.ts b/src/components/new-safe/create/steps/StatusStep/useSafeCreationEffects.ts deleted file mode 100644 index e656fecd49..0000000000 --- a/src/components/new-safe/create/steps/StatusStep/useSafeCreationEffects.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { Dispatch, SetStateAction } from 'react' -import { useEffect } from 'react' -import { pollSafeInfo } from '@/components/new-safe/create/logic' -import { SafeCreationStatus } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' -import { CREATE_SAFE_EVENTS, trackEvent } from '@/services/analytics' -import { updateAddressBook } from '@/components/new-safe/create/logic/address-book' -import { useAppDispatch } from '@/store' -import useChainId from '@/hooks/useChainId' -import { usePendingSafe } from './usePendingSafe' -import { gtmSetSafeAddress } from '@/services/analytics/gtm' - -const useSafeCreationEffects = ({ - status, - setStatus, -}: { - status: SafeCreationStatus - setStatus: Dispatch> -}) => { - const dispatch = useAppDispatch() - const chainId = useChainId() - const [pendingSafe, setPendingSafe] = usePendingSafe() - - // Asynchronously wait for Safe creation - useEffect(() => { - if (status === SafeCreationStatus.SUCCESS && pendingSafe?.safeAddress) { - pollSafeInfo(chainId, pendingSafe.safeAddress) - .then(() => setStatus(SafeCreationStatus.INDEXED)) - .catch(() => setStatus(SafeCreationStatus.INDEX_FAILED)) - } - }, [chainId, pendingSafe?.safeAddress, status, setStatus]) - - // Warn about leaving the page before Safe creation - useEffect(() => { - if (status !== SafeCreationStatus.PROCESSING && status !== SafeCreationStatus.AWAITING) return - - const onBeforeUnload = (event: BeforeUnloadEvent) => { - event.preventDefault() - event.returnValue = 'Are you sure you want to leave before your Safe Account is fully created?' - return event.returnValue - } - - window.addEventListener('beforeunload', onBeforeUnload) - - return () => window.removeEventListener('beforeunload', onBeforeUnload) - }, [status]) - - // Add Safe to Added Safes and add owner and safe names to Address Book - useEffect(() => { - if (status === SafeCreationStatus.SUCCESS && pendingSafe?.safeAddress) { - dispatch( - updateAddressBook( - chainId, - pendingSafe.safeAddress, - pendingSafe.name, - pendingSafe.owners, - pendingSafe.threshold, - ), - ) - } - }, [status, chainId, dispatch, pendingSafe]) - - // Reset pending Safe on error - useEffect(() => { - if ( - status === SafeCreationStatus.WALLET_REJECTED || - status === SafeCreationStatus.ERROR || - status === SafeCreationStatus.REVERTED - ) { - if (pendingSafe?.txHash) { - setPendingSafe(pendingSafe ? { ...pendingSafe, txHash: undefined, tx: undefined } : undefined) - } - } - }, [pendingSafe, setPendingSafe, status]) - - // Tracking - useEffect(() => { - if (status === SafeCreationStatus.SUCCESS) { - pendingSafe?.safeAddress && gtmSetSafeAddress(pendingSafe.safeAddress) - trackEvent({ ...CREATE_SAFE_EVENTS.CREATED_SAFE, label: 'deployment' }) - return - } - - if (status === SafeCreationStatus.WALLET_REJECTED) { - trackEvent(CREATE_SAFE_EVENTS.REJECT_CREATE_SAFE) - return - } - }, [pendingSafe?.safeAddress, status]) -} - -export default useSafeCreationEffects diff --git a/src/components/new-safe/create/steps/StatusStep/useUndeployedSafe.ts b/src/components/new-safe/create/steps/StatusStep/useUndeployedSafe.ts new file mode 100644 index 0000000000..029019d86e --- /dev/null +++ b/src/components/new-safe/create/steps/StatusStep/useUndeployedSafe.ts @@ -0,0 +1,19 @@ +import { PayMethod } from '@/features/counterfactual/PayNowPayLater' +import { selectUndeployedSafes } from '@/features/counterfactual/store/undeployedSafesSlice' +import useChainId from '@/hooks/useChainId' +import { useAppSelector } from '@/store' + +// Returns the undeployed safe for the current network +const useUndeployedSafe = () => { + const chainId = useChainId() + const undeployedSafes = useAppSelector(selectUndeployedSafes) + const undeployedSafe = + undeployedSafes[chainId] && + Object.entries(undeployedSafes[chainId]).find((undeployedSafe) => { + return undeployedSafe[1].status.type === PayMethod.PayNow + }) + + return undeployedSafe || [] +} + +export default useUndeployedSafe diff --git a/src/components/new-safe/create/useSyncSafeCreationStep.ts b/src/components/new-safe/create/useSyncSafeCreationStep.ts index 90a32c0e7f..1e645d6c93 100644 --- a/src/components/new-safe/create/useSyncSafeCreationStep.ts +++ b/src/components/new-safe/create/useSyncSafeCreationStep.ts @@ -1,21 +1,22 @@ +import useUndeployedSafe from '@/components/new-safe/create/steps/StatusStep/useUndeployedSafe' import { useEffect } from 'react' import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper' import type { NewSafeFormData } from '@/components/new-safe/create/index' import useWallet from '@/hooks/wallets/useWallet' -import { usePendingSafe } from './steps/StatusStep/usePendingSafe' import useIsWrongChain from '@/hooks/useIsWrongChain' import { useRouter } from 'next/router' import { AppRoutes } from '@/config/routes' const useSyncSafeCreationStep = (setStep: StepRenderProps['setStep']) => { - const [pendingSafe] = usePendingSafe() + const [safeAddress, pendingSafe] = useUndeployedSafe() + const wallet = useWallet() const isWrongChain = useIsWrongChain() const router = useRouter() useEffect(() => { // Jump to the status screen if there is already a tx submitted - if (pendingSafe) { + if (pendingSafe && pendingSafe.status.status !== 'AWAITING_EXECUTION') { setStep(3) return } diff --git a/src/components/notification-center/NotificationCenter/index.tsx b/src/components/notification-center/NotificationCenter/index.tsx index da69016391..ebce3aee22 100644 --- a/src/components/notification-center/NotificationCenter/index.tsx +++ b/src/components/notification-center/NotificationCenter/index.tsx @@ -20,11 +20,13 @@ import UnreadBadge from '@/components/common/UnreadBadge' import Link from 'next/link' import { useRouter } from 'next/router' import { AppRoutes } from '@/config/routes' -// import SettingsIcon from '@/public/images/sidebar/settings.svg' +import SettingsIcon from '@/public/images/sidebar/settings.svg' import css from './styles.module.css' import { trackEvent, OVERVIEW_EVENTS } from '@/services/analytics' import SvgIcon from '@mui/icons-material/ExpandLess' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' const NOTIFICATION_CENTER_LIMIT = 4 @@ -33,7 +35,7 @@ const NotificationCenter = (): ReactElement => { const [showAll, setShowAll] = useState(false) const [anchorEl, setAnchorEl] = useState(null) const open = Boolean(anchorEl) - + const hasPushNotifications = useHasFeature(FEATURES.PUSH_NOTIFICATIONS) const dispatch = useAppDispatch() const notifications = useAppSelector(selectNotifications) @@ -144,9 +146,11 @@ const NotificationCenter = (): ReactElement => { )}
+
+
{canExpand && ( <> @@ -166,18 +170,21 @@ const NotificationCenter = (): ReactElement => { )} - - - {/* Push notifications settings */} - - + + {hasPushNotifications && ( + + + Push notifications settings + + + )}
diff --git a/src/components/safe-apps/AppFrame/__tests__/AppFrame.test.tsx b/src/components/safe-apps/AppFrame/__tests__/AppFrame.test.tsx index 9ef6c07eb4..584485a9ad 100644 --- a/src/components/safe-apps/AppFrame/__tests__/AppFrame.test.tsx +++ b/src/components/safe-apps/AppFrame/__tests__/AppFrame.test.tsx @@ -61,6 +61,7 @@ describe('AppFrame', () => { }, ], }, + txHash: null, }, conflictType: ConflictType.NONE, }, diff --git a/src/components/safe-apps/AppFrame/useAppCommunicator.ts b/src/components/safe-apps/AppFrame/useAppCommunicator.ts index 90822aa4f6..1342d32857 100644 --- a/src/components/safe-apps/AppFrame/useAppCommunicator.ts +++ b/src/components/safe-apps/AppFrame/useAppCommunicator.ts @@ -15,13 +15,13 @@ import type { GetTxBySafeTxHashParams, RequestId, RPCPayload, - SafeInfo, SendTransactionRequestParams, SendTransactionsParams, SignMessageParams, SignTypedMessageParams, ChainInfo, SafeBalances, + SafeInfoExtended, } from '@safe-global/safe-apps-sdk' import { Methods, RPC_CALLS } from '@safe-global/safe-apps-sdk' import type { Permission, PermissionRequest } from '@safe-global/safe-apps-sdk/dist/types/types/permissions' @@ -56,7 +56,7 @@ export type UseAppCommunicatorHandlers = { onGetTxBySafeTxHash: (transactionId: string) => Promise onGetEnvironmentInfo: () => EnvironmentInfo onGetSafeBalances: (currency: string) => Promise - onGetSafeInfo: () => SafeInfo + onGetSafeInfo: () => SafeInfoExtended onGetChainInfo: () => ChainInfo | undefined onGetPermissions: (origin: string) => Permission[] onSetPermissions: (permissionsRequest?: SafePermissionsRequest) => void diff --git a/src/components/safe-apps/AppFrame/useGetSafeInfo.ts b/src/components/safe-apps/AppFrame/useGetSafeInfo.ts index ad3a4048b3..1dcb0c6c09 100644 --- a/src/components/safe-apps/AppFrame/useGetSafeInfo.ts +++ b/src/components/safe-apps/AppFrame/useGetSafeInfo.ts @@ -19,9 +19,28 @@ const useGetSafeInfo = () => { owners: safe.owners.map((owner) => owner.value), threshold: safe.threshold, isReadOnly: !isOwner, + nonce: safe.nonce, + implementation: safe.implementation.value, + modules: safe.modules ? safe.modules.map((module) => module.value) : null, + fallbackHandler: safe.fallbackHandler ? safe.fallbackHandler?.value : null, + guard: safe.guard?.value || null, + version: safe.version, network: getLegacyChainName(chainName || '', chainId).toUpperCase(), }), - [chainId, chainName, isOwner, safeAddress, safe.owners, safe.threshold], + [ + chainId, + chainName, + isOwner, + safeAddress, + safe.owners, + safe.threshold, + safe.nonce, + safe.implementation, + safe.modules, + safe.fallbackHandler, + safe.guard, + safe.version, + ], ) } diff --git a/src/components/safe-apps/NativeSwapsCard/index.stories.tsx b/src/components/safe-apps/NativeSwapsCard/index.stories.tsx new file mode 100644 index 0000000000..9781c14934 --- /dev/null +++ b/src/components/safe-apps/NativeSwapsCard/index.stories.tsx @@ -0,0 +1,31 @@ +import type { Meta, StoryObj } from '@storybook/react' +import NativeSwapsCard from './index' +import { Box } from '@mui/material' +import { StoreDecorator } from '@/stories/storeDecorator' + +const meta = { + component: NativeSwapsCard, + parameters: { + componentSubtitle: 'Renders a promo card for native swaps', + }, + + decorators: [ + (Story) => { + return ( + + + + + + ) + }, + ], + tags: ['autodocs'], +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: {}, +} diff --git a/src/components/safe-apps/NativeSwapsCard/index.tsx b/src/components/safe-apps/NativeSwapsCard/index.tsx new file mode 100644 index 0000000000..b95e687b72 --- /dev/null +++ b/src/components/safe-apps/NativeSwapsCard/index.tsx @@ -0,0 +1,60 @@ +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' +import Typography from '@mui/material/Typography' +import { Button, Paper, Stack } from '@mui/material' +import SafeAppIconCard from '../SafeAppIconCard' +import css from './styles.module.css' +import { SWAP_EVENTS, SWAP_LABELS } from '@/services/analytics/events/swaps' +import Track from '@/components/common/Track' +import Link from 'next/link' +import { AppRoutes } from '@/config/routes' +import { useRouter } from 'next/router' +import useLocalStorage from '@/services/local-storage/useLocalStorage' +import useIsSwapFeatureEnabled from '@/features/swap/hooks/useIsSwapFeatureEnabled' + +const SWAPS_APP_CARD_STORAGE_KEY = 'showSwapsAppCard' + +const NativeSwapsCard = () => { + const router = useRouter() + const isSwapFeatureEnabled = useIsSwapFeatureEnabled() + const [isSwapsCardVisible = true, setIsSwapsCardVisible] = useLocalStorage(SWAPS_APP_CARD_STORAGE_KEY) + if (!isSwapFeatureEnabled || !isSwapsCardVisible) return null + + return ( + + + +
+ } + /> + + + + Native swaps are here! + + + + Experience seamless trading with better decoding and security in native swaps. + + + + + + + + + + + + + ) +} + +export default NativeSwapsCard diff --git a/src/components/safe-apps/NativeSwapsCard/styles.module.css b/src/components/safe-apps/NativeSwapsCard/styles.module.css new file mode 100644 index 0000000000..68b767d1a5 --- /dev/null +++ b/src/components/safe-apps/NativeSwapsCard/styles.module.css @@ -0,0 +1,50 @@ +.container { + transition: background-color 0.3s ease-in-out, border 0.3s ease-in-out; + border: 1px solid transparent; + height: 100%; +} + +.container:hover { + background-color: var(--color-background-light); + border: 1px solid var(--color-secondary-light); +} + +.header { + padding: var(--space-3) var(--space-2) var(--space-1) var(--space-2); +} + +.content { + padding: var(--space-2); +} + +.iconContainer { + position: relative; + background: var(--color-secondary-light); + border-radius: 50%; + display: flex; + padding: var(--space-1); +} + +.title { + line-height: 175%; + margin: 0; + + flex-grow: 1; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.description { + /* Truncate Safe App Description (3 lines) */ + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.buttons { + padding-top: var(--space-2); + white-space: nowrap; +} diff --git a/src/components/safe-apps/SafeAppList/index.tsx b/src/components/safe-apps/SafeAppList/index.tsx index c9a7daf84e..936129dff6 100644 --- a/src/components/safe-apps/SafeAppList/index.tsx +++ b/src/components/safe-apps/SafeAppList/index.tsx @@ -10,6 +10,7 @@ import useSafeAppPreviewDrawer from '@/hooks/safe-apps/useSafeAppPreviewDrawer' import css from './styles.module.css' import { Skeleton } from '@mui/material' import { useOpenedSafeApps } from '@/hooks/safe-apps/useOpenedSafeApps' +import NativeSwapsCard from '@/components/safe-apps/NativeSwapsCard' type SafeAppListProps = { safeAppsList: SafeAppData[] @@ -20,6 +21,7 @@ type SafeAppListProps = { removeCustomApp?: (safeApp: SafeAppData) => void title: string query?: string + isFiltered?: boolean } const SafeAppList = ({ @@ -31,6 +33,7 @@ const SafeAppList = ({ removeCustomApp, title, query, + isFiltered = false, }: SafeAppListProps) => { const { isPreviewDrawerOpen, previewDrawerApp, openPreviewDrawer, closePreviewDrawer } = useSafeAppPreviewDrawer() const { openedSafeAppIds } = useOpenedSafeApps() @@ -69,6 +72,8 @@ const SafeAppList = ({ ))} + {!isFiltered && !addCustomApp && } + {/* Flat list filtered by search query */} {safeAppsList.map((safeApp) => (
  • diff --git a/src/components/safe-apps/SafeAppPreviewDrawer/index.tsx b/src/components/safe-apps/SafeAppPreviewDrawer/index.tsx index 8f64e2e339..f1e63e4b17 100644 --- a/src/components/safe-apps/SafeAppPreviewDrawer/index.tsx +++ b/src/components/safe-apps/SafeAppPreviewDrawer/index.tsx @@ -91,6 +91,7 @@ const SafeAppPreviewDrawer = ({ isOpen, safeApp, isBookmarked, onClose, onBookma {/* Open Safe App button */} + ) : ( + + + + )} + + + ) +} + +export default MsgShareLink diff --git a/src/components/safe-messages/MsgSummary/index.tsx b/src/components/safe-messages/MsgSummary/index.tsx index 23572a8d13..7c8814263c 100644 --- a/src/components/safe-messages/MsgSummary/index.tsx +++ b/src/components/safe-messages/MsgSummary/index.tsx @@ -11,6 +11,7 @@ import TxConfirmations from '@/components/transactions/TxConfirmations' import css from '@/components/transactions/TxSummary/styles.module.css' import useIsSafeMessagePending from '@/hooks/messages/useIsSafeMessagePending' +import { isEIP712TypedData } from '@/utils/safe-messages' const getStatusColor = (value: SafeMessageStatus, palette: Palette): string => { switch (value) { @@ -28,14 +29,18 @@ const MsgSummary = ({ msg }: { msg: SafeMessage }): ReactElement => { const txStatusLabel = useSafeMessageStatus(msg) const isConfirmed = msg.status === SafeMessageStatus.CONFIRMED const isPending = useIsSafeMessagePending(msg.messageHash) + let type = '' + if (isEIP712TypedData(msg.message)) { + type = (msg.message as unknown as { primaryType: string }).primaryType + } return ( - + - Off-chain signature + {type || 'Signature'} diff --git a/src/components/safe-messages/MsgType/index.tsx b/src/components/safe-messages/MsgType/index.tsx index ce83eac81e..7b0fb197a7 100644 --- a/src/components/safe-messages/MsgType/index.tsx +++ b/src/components/safe-messages/MsgType/index.tsx @@ -1,23 +1,43 @@ -import { Box } from '@mui/material' +import { Box, SvgIcon } from '@mui/material' import type { SafeMessage } from '@safe-global/safe-gateway-typescript-sdk' - +import RequiredIcon from '@/public/images/messages/required.svg' import ImageFallback from '@/components/common/ImageFallback' - import txTypeCss from '@/components/transactions/TxType/styles.module.css' +import { isEIP712TypedData } from '@/utils/safe-messages' const FALLBACK_LOGO_URI = '/images/transactions/custom.svg' +const MAX_TRIMMED_LENGTH = 20 + +const getMessageName = (msg: SafeMessage) => { + if (msg.name != null) return msg.name + + if (isEIP712TypedData(msg.message)) { + return msg.message.domain?.name || '' + } + + const firstLine = msg.message.split('\n')[0] + let trimmed = firstLine.slice(0, MAX_TRIMMED_LENGTH) + if (trimmed.length < firstLine.length) { + trimmed += '…' + } + return trimmed +} const MsgType = ({ msg }: { msg: SafeMessage }) => { return ( - - {msg.name} + {msg.logoUri ? ( + + ) : ( + + )} + {getMessageName(msg)} ) } diff --git a/src/components/safe-messages/SingleMsg/SingleMsg.test.tsx b/src/components/safe-messages/SingleMsg/SingleMsg.test.tsx new file mode 100644 index 0000000000..a7421945e7 --- /dev/null +++ b/src/components/safe-messages/SingleMsg/SingleMsg.test.tsx @@ -0,0 +1,67 @@ +import { extendedSafeInfoBuilder } from '@/tests/builders/safe' +import { fireEvent, render, waitFor } from '@/tests/test-utils' +import * as useSafeInfo from '@/hooks/useSafeInfo' +import * as syncSafeMessageSigner from '@/hooks/messages/useSyncSafeMessageSigner' + +import SingleMsg from '.' +import { safeMsgBuilder } from '@/tests/builders/safeMessage' + +const safeMessage = safeMsgBuilder().build() +const extendedSafeInfo = extendedSafeInfoBuilder().build() + +jest.mock('next/router', () => ({ + useRouter() { + return { + pathname: '/transactions/msg', + query: { + safe: extendedSafeInfo.address.value, + messageHash: safeMessage.messageHash, + }, + } + }, +})) + +jest.spyOn(useSafeInfo, 'default').mockImplementation(() => ({ + safeAddress: extendedSafeInfo.address.value, + safe: { + ...extendedSafeInfo, + chainId: '5', + }, + safeError: undefined, + safeLoading: false, + safeLoaded: true, +})) + +describe('SingleMsg', () => { + beforeAll(() => { + jest.clearAllMocks() + }) + afterEach(() => { + jest.clearAllMocks() + }) + + it('renders ', async () => { + jest + .spyOn(syncSafeMessageSigner, 'fetchSafeMessage') + .mockImplementation(() => Promise.resolve(safeMsgBuilder().build())) + const screen = render() + expect(await screen.findByText('Signature')).toBeInTheDocument() + }) + + it('shows an error when the transaction has failed to load', async () => { + jest + .spyOn(syncSafeMessageSigner, 'fetchSafeMessage') + .mockImplementation(() => Promise.reject(new Error('Server error'))) + + const screen = render() + + await waitFor(() => { + expect(screen.getByText('Failed to load message')).toBeInTheDocument() + }) + + await waitFor(() => { + fireEvent.click(screen.getByText('Details')) + expect(screen.getByText('Server error')).toBeInTheDocument() + }) + }) +}) diff --git a/src/components/safe-messages/SingleMsg/index.tsx b/src/components/safe-messages/SingleMsg/index.tsx new file mode 100644 index 0000000000..dc5c181659 --- /dev/null +++ b/src/components/safe-messages/SingleMsg/index.tsx @@ -0,0 +1,30 @@ +import { useRouter } from 'next/router' +import { TxListGrid } from '@/components/transactions/TxList' +import { TransactionSkeleton } from '@/components/transactions/TxListItem/ExpandableTransactionItem' +import ExpandableMsgItem from '../MsgListItem/ExpandableMsgItem' +import useSafeMessage from '@/hooks/messages/useSafeMessage' +import ErrorMessage from '@/components/tx/ErrorMessage' + +const SingleMsg = () => { + const router = useRouter() + const { messageHash } = router.query + const safeMessageHash = Array.isArray(messageHash) ? messageHash[0] : messageHash + const [safeMessage, _, messageError] = useSafeMessage(safeMessageHash) + + if (safeMessage) { + return ( + + + + ) + } + + if (messageError) { + return Failed to load message + } + + // Loading skeleton + return +} + +export default SingleMsg diff --git a/src/components/settings/ContractVersion/index.tsx b/src/components/settings/ContractVersion/index.tsx index b20683b757..334ca921d7 100644 --- a/src/components/settings/ContractVersion/index.tsx +++ b/src/components/settings/ContractVersion/index.tsx @@ -1,5 +1,5 @@ import { useContext, useMemo } from 'react' -import { Box, SvgIcon, Typography, Alert, AlertTitle, Skeleton, Button } from '@mui/material' +import { SvgIcon, Typography, Alert, AlertTitle, Skeleton, Button } from '@mui/material' import { ImplementationVersionState } from '@safe-global/safe-gateway-typescript-sdk' import { LATEST_SAFE_VERSION } from '@/config/constants' import { sameAddress } from '@/utils/addresses' @@ -25,6 +25,7 @@ export const ContractVersion = () => { const needsUpdate = safe.implementationVersionState === ImplementationVersionState.OUTDATED const showUpdateDialog = safeMasterCopy?.deployer === MasterCopyDeployer.GNOSIS && needsUpdate + const isLatestVersion = safe.version && !showUpdateDialog return ( <> @@ -32,39 +33,43 @@ export const ContractVersion = () => { Contract version - - {safeLoaded ? safe.version ?? 'Unsupported contract' : } + + {safeLoaded ? ( + <> + {safe.version ?? 'Unsupported contract'} + {isLatestVersion && ( + <> + Latest version + + )} + + ) : ( + + )} - - {safeLoaded && safe.version ? ( - showUpdateDialog ? ( - } - > - New version is available: {LATEST_SAFE_VERSION} - - Update now to take advantage of new features and the highest security standards available. You will need - to confirm this update just like any other transaction.{' '} - GitHub - + {safeLoaded && safe.version && showUpdateDialog && ( + } + > + New version is available: {LATEST_SAFE_VERSION} - - {(isOk) => ( - - )} - - - ) : ( - - Latest version - - ) - ) : null} - + + Update now to take advantage of new features and the highest security standards available. You will need to + confirm this update just like any other transaction.{' '} + GitHub + + + + {(isOk) => ( + + )} + + + )} ) } diff --git a/src/components/settings/FallbackHandler/index.tsx b/src/components/settings/FallbackHandler/index.tsx index 5a64b89df0..ff96174d51 100644 --- a/src/components/settings/FallbackHandler/index.tsx +++ b/src/components/settings/FallbackHandler/index.tsx @@ -1,3 +1,4 @@ +import { TWAP_FALLBACK_HANDLER } from '@/features/swap/helpers/utils' import NextLink from 'next/link' import { Typography, Box, Grid, Paper, Link, Alert } from '@mui/material' import semverSatisfies from 'semver/functions/satisfies' @@ -30,6 +31,7 @@ export const FallbackHandler = (): ReactElement | null => { const hasFallbackHandler = !!safe.fallbackHandler const isOfficial = hasFallbackHandler && safe.fallbackHandler?.value === fallbackHandlerDeployment?.networkAddresses[safe.chainId] + const isTWAPFallbackHandler = safe.fallbackHandler?.value === TWAP_FALLBACK_HANDLER const warning = !hasFallbackHandler ? ( <> @@ -45,6 +47,8 @@ export const FallbackHandler = (): ReactElement | null => { )} + ) : isTWAPFallbackHandler ? ( + <>This is CoW's fallback handler. It is needed for this Safe to be able to use the TWAP feature for Swaps. ) : !isOfficial ? ( <> An unofficial fallback handler is currently set. @@ -78,8 +82,16 @@ export const FallbackHandler = (): ReactElement | null => { here - - {warning && {warning}} + + {warning && ( + + {warning} + + )} {safe.fallbackHandler && ( { {(isOk) => ( } label={preferences ? 'On' : 'Off'} disabled={!isOk || isRegistering || !safe.deployed} diff --git a/src/components/settings/RequiredConfirmations/index.tsx b/src/components/settings/RequiredConfirmations/index.tsx index 61db4d9900..2ccc9fc094 100644 --- a/src/components/settings/RequiredConfirmations/index.tsx +++ b/src/components/settings/RequiredConfirmations/index.tsx @@ -19,23 +19,22 @@ export const RequiredConfirmation = ({ threshold, owners }: { threshold: number; - Any transaction requires the confirmation of: - + Any transaction requires the confirmation of: + + {threshold} out of {owners} signers. {owners > 1 && ( - - - {(isOk) => ( - - - - )} - - + + {(isOk) => ( + + + + )} + )} diff --git a/src/components/settings/SafeModules/index.tsx b/src/components/settings/SafeModules/index.tsx index 88c85b9bea..204448ce3e 100644 --- a/src/components/settings/SafeModules/index.tsx +++ b/src/components/settings/SafeModules/index.tsx @@ -65,7 +65,7 @@ const SafeModules = () => { - Safe Account modules + Safe modules diff --git a/src/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal.tsx b/src/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal.tsx deleted file mode 100644 index bacce6a38f..0000000000 --- a/src/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import CopyButton from '@/components/common/CopyButton' -import ModalDialog from '@/components/common/ModalDialog' -import { trackEvent } from '@/services/analytics' -import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' -import { Box, Button, DialogContent, DialogTitle, IconButton, TextField, Typography } from '@mui/material' -import { useState } from 'react' -import { useForm } from 'react-hook-form' -import { Visibility, VisibilityOff, Close } from '@mui/icons-material' -import css from '@/components/settings/SecurityLogin/SocialSignerExport/styles.module.css' -import ErrorCodes from '@/services/exceptions/ErrorCodes' -import { logError } from '@/services/exceptions' -import ErrorMessage from '@/components/tx/ErrorMessage' -import { asError } from '@/services/exceptions/utils' -import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' - -enum ExportFieldNames { - password = 'password', - pk = 'pk', -} - -type ExportFormData = { - [ExportFieldNames.password]: string - [ExportFieldNames.pk]: string | undefined -} - -const ExportMPCAccountModal = ({ onClose, open }: { onClose: () => void; open: boolean }) => { - const socialWalletService = useSocialWallet() - const [error, setError] = useState() - - const [showPassword, setShowPassword] = useState(false) - const formMethods = useForm({ - mode: 'all', - defaultValues: { - [ExportFieldNames.password]: '', - }, - }) - const { register, formState, handleSubmit, setValue, watch, reset } = formMethods - - const exportedKey = watch(ExportFieldNames.pk) - - const onSubmit = async (data: ExportFormData) => { - if (!socialWalletService) { - return - } - try { - setError(undefined) - const pk = await socialWalletService.exportSignerKey(data[ExportFieldNames.password]) - trackEvent(MPC_WALLET_EVENTS.EXPORT_PK_SUCCESS) - setValue(ExportFieldNames.pk, pk) - } catch (err) { - logError(ErrorCodes._305, err) - trackEvent(MPC_WALLET_EVENTS.EXPORT_PK_ERROR) - setError(asError(err).message) - } - } - - const handleClose = () => { - setError(undefined) - reset() - onClose() - } - - const toggleShowPK = () => { - trackEvent(MPC_WALLET_EVENTS.SEE_PK) - setShowPassword((prev) => !prev) - } - - const onCopy = () => { - trackEvent(MPC_WALLET_EVENTS.COPY_PK) - } - - return ( - - Export your account - - - - - -
    - - For security reasons you have to enter your password to reveal your account key. - - {exportedKey ? ( - - - - {showPassword ? : } - - - - ), - }} - {...register(ExportFieldNames.pk)} - /> - - ) : ( - <> - - - )} - {error && {error}} - - - - {exportedKey === undefined && ( - - )} - - -
    -
    -
    - ) -} - -export default ExportMPCAccountModal diff --git a/src/components/settings/SecurityLogin/SocialSignerExport/index.tsx b/src/components/settings/SecurityLogin/SocialSignerExport/index.tsx deleted file mode 100644 index 088e484af2..0000000000 --- a/src/components/settings/SecurityLogin/SocialSignerExport/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import Track from '@/components/common/Track' -import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' -import { Alert, Box, Button, Tooltip, Typography } from '@mui/material' -import { useState } from 'react' -import ExportMPCAccountModal from '@/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal' -import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' - -const SocialSignerExport = () => { - const [isModalOpen, setIsModalOpen] = useState(false) - - const socialWalletService = useSocialWallet() - - const isPasswordSet = socialWalletService?.isRecoveryPasswordSet() ?? false - - return ( - <> - - - Signers created via Google can be exported and imported to any non-custodial wallet outside of Safe. - - - Never disclose your keys or seed phrase to anyone. If someone gains access to them, they have full access over - your social login signer. - - - - - - - - - - - setIsModalOpen(false)} open={isModalOpen} /> - - ) -} - -export default SocialSignerExport diff --git a/src/components/settings/SecurityLogin/SocialSignerExport/styles.module.css b/src/components/settings/SecurityLogin/SocialSignerExport/styles.module.css deleted file mode 100644 index c818925ecd..0000000000 --- a/src/components/settings/SecurityLogin/SocialSignerExport/styles.module.css +++ /dev/null @@ -1,10 +0,0 @@ -.close { - position: absolute; - right: var(--space-1); - top: var(--space-1); -} - -.modalError { - width: 100%; - margin: 0; -} diff --git a/src/components/settings/SecurityLogin/SocialSignerMFA/PasswordInput.tsx b/src/components/settings/SecurityLogin/SocialSignerMFA/PasswordInput.tsx deleted file mode 100644 index 40e78010b2..0000000000 --- a/src/components/settings/SecurityLogin/SocialSignerMFA/PasswordInput.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useState } from 'react' -import { IconButton, TextField, type TextFieldProps } from '@mui/material' -import { Visibility, VisibilityOff } from '@mui/icons-material' -import { useFormContext, type Validate } from 'react-hook-form' - -const PasswordInput = ({ - name, - validate, - required = false, - ...props -}: Omit & { - name: string - validate?: Validate - required?: boolean -}) => { - const [showPassword, setShowPassword] = useState(false) - - const { register, formState } = useFormContext() || {} - - return ( - setShowPassword((prev) => !prev)} - edge="end" - > - {showPassword ? : } - - ), - }} - {...register(name, { - required, - validate, - })} - /> - ) -} - -export default PasswordInput diff --git a/src/components/settings/SecurityLogin/SocialSignerMFA/index.test.tsx b/src/components/settings/SecurityLogin/SocialSignerMFA/index.test.tsx deleted file mode 100644 index 4a2bde113d..0000000000 --- a/src/components/settings/SecurityLogin/SocialSignerMFA/index.test.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { _getPasswordStrength, PasswordStrength } from '@/components/settings/SecurityLogin/SocialSignerMFA/index' - -describe('_getPasswordStrength', () => { - it('should return weak if the value has fewer than 9 characters', () => { - const result = _getPasswordStrength('Testpw1!') - - expect(result).toEqual(PasswordStrength.weak) - }) - - it('should return weak if the value has no uppercase letter', () => { - const result = _getPasswordStrength('testpassword1!') - - expect(result).toEqual(PasswordStrength.weak) - }) - - it('should return weak if the value has no number', () => { - const result = _getPasswordStrength('Testpassword!') - - expect(result).toEqual(PasswordStrength.weak) - }) - - it('should return weak if the value has no special character', () => { - const result = _getPasswordStrength('Testpassword123') - - expect(result).toEqual(PasswordStrength.weak) - }) - - it('should return weak if the value has 12 or more characters but no uppercase letter', () => { - const result = _getPasswordStrength('testpassword123!') - - expect(result).toEqual(PasswordStrength.weak) - }) - - it('should return medium if the value has 9 or more characters, uppercase, number and special character', () => { - const result = _getPasswordStrength('Testpw123!') - - expect(result).toEqual(PasswordStrength.medium) - }) - - it('should return strong if the value has 12 or more characters, uppercase, number and special character', () => { - const result = _getPasswordStrength('Testpassword123!') - - expect(result).toEqual(PasswordStrength.strong) - }) -}) diff --git a/src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx b/src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx deleted file mode 100644 index cfb4ec0464..0000000000 --- a/src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx +++ /dev/null @@ -1,298 +0,0 @@ -import Track from '@/components/common/Track' -import { - Typography, - Button, - Box, - Accordion, - AccordionSummary, - AccordionDetails, - Grid, - FormControl, - SvgIcon, - Divider, - Alert, -} from '@mui/material' -import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' -import { useRouter } from 'next/router' -import { useState, useMemo, type ChangeEvent } from 'react' -import { FormProvider, useForm } from 'react-hook-form' -import CheckIcon from '@/public/images/common/check-filled.svg' -import LockWarningIcon from '@/public/images/common/lock-warning.svg' -import PasswordInput from '@/components/settings/SecurityLogin/SocialSignerMFA/PasswordInput' -import css from '@/components/settings/SecurityLogin/SocialSignerMFA/styles.module.css' -import BarChartIcon from '@/public/images/common/bar-chart.svg' -import ShieldIcon from '@/public/images/common/shield.svg' -import ShieldOffIcon from '@/public/images/common/shield-off.svg' -import ExpandMoreIcon from '@mui/icons-material/ExpandMore' -import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' - -enum PasswordFieldNames { - currentPassword = 'currentPassword', - newPassword = 'newPassword', - confirmPassword = 'confirmPassword', -} - -type PasswordFormData = { - [PasswordFieldNames.currentPassword]: string | undefined - [PasswordFieldNames.newPassword]: string - [PasswordFieldNames.confirmPassword]: string -} - -export enum PasswordStrength { - strong, - medium, - weak, -} - -// At least 12 characters, one lowercase, one uppercase, one number, one symbol -const strongPassword = new RegExp('(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9])(?=.{12,})') -// At least 9 characters, one lowercase, one uppercase, one number, one symbol -const mediumPassword = new RegExp('((?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9])(?=.{9,}))') - -export const _getPasswordStrength = (value: string): PasswordStrength | undefined => { - if (value === '') return undefined - - if (strongPassword.test(value)) { - return PasswordStrength.strong - } - - if (mediumPassword.test(value)) { - return PasswordStrength.medium - } - - return PasswordStrength.weak -} - -const passwordStrengthMap = { - [PasswordStrength.strong]: { - label: 'Strong', - className: 'strongPassword', - }, - [PasswordStrength.medium]: { - label: 'Medium', - className: 'mediumPassword', - }, - [PasswordStrength.weak]: { - label: 'Weak', - className: 'weakPassword', - }, -} as const - -const SocialSignerMFA = () => { - const router = useRouter() - const socialWalletService = useSocialWallet() - const [passwordStrength, setPasswordStrength] = useState() - const [submitError, setSubmitError] = useState() - const [open, setOpen] = useState(false) - - const formMethods = useForm({ - mode: 'all', - defaultValues: { - [PasswordFieldNames.confirmPassword]: '', - [PasswordFieldNames.currentPassword]: undefined, - [PasswordFieldNames.newPassword]: '', - }, - }) - - const { formState, handleSubmit, reset, watch } = formMethods - - const isPasswordSet = useMemo(() => { - if (!socialWalletService) { - return false - } - return socialWalletService.isRecoveryPasswordSet() - }, [socialWalletService]) - - const onSubmit = async (data: PasswordFormData) => { - if (!socialWalletService) return - - try { - await socialWalletService.enableMFA( - data[PasswordFieldNames.currentPassword], - data[PasswordFieldNames.newPassword], - ) - onReset() - setOpen(false) - - // This is a workaround so that the isPasswordSet and isMFAEnabled state update - router.reload() - } catch (e) { - setSubmitError('The password you entered is incorrect. Please try again.') - } - } - - const onReset = () => { - reset() - setPasswordStrength(undefined) - setSubmitError(undefined) - } - - const toggleAccordion = () => { - setOpen((prev) => !prev) - } - - const confirmPassword = watch(PasswordFieldNames.confirmPassword) - const newPassword = watch(PasswordFieldNames.newPassword) - const passwordsEmpty = confirmPassword === '' && newPassword === '' - const passwordsMatch = newPassword === confirmPassword - - const isSubmitDisabled = - !passwordsMatch || - passwordStrength === PasswordStrength.weak || - formState.isSubmitting || - !formMethods.formState.isValid - - return ( - -
    - - - Protect your social login signer with a password. It will be used to restore access in another browser or on - another device. - - - }> - - - Password - - - - - - {isPasswordSet && ( - <> - - - - - - )} - - - ) => { - const value = event.target.value - setPasswordStrength(_getPasswordStrength(value)) - }, - }} - /> - - - {passwordStrength !== undefined - ? `${passwordStrengthMap[passwordStrength].label} password` - : 'Password strength'} - - - Include at least 9 or more characters, a number, an uppercase letter and a symbol - - - - - - - {passwordsEmpty ? ( - <> - Passwords should match - - ) : passwordsMatch ? ( - <> - Passwords match - - ) : ( - <> - Passwords don't match - - )} - - - - {submitError && {submitError}} - - - - - - - - - `1px solid ${theme.palette.border.light}` }} - > - - - - You won't be able to restore this password - -
      - - You will have to input this password if you login with this social login signer in another - browser or on another device. - - - We suggest to use a password manager or to write the password down on a piece of paper and - secure it. - -
    -
    -
    -
    -
    -
    -
    -
    -
    - ) -} - -export default SocialSignerMFA diff --git a/src/components/settings/SecurityLogin/SocialSignerMFA/styles.module.css b/src/components/settings/SecurityLogin/SocialSignerMFA/styles.module.css deleted file mode 100644 index b41b19e58f..0000000000 --- a/src/components/settings/SecurityLogin/SocialSignerMFA/styles.module.css +++ /dev/null @@ -1,76 +0,0 @@ -.list { - list-style: none; - counter-reset: item; - padding: 0; - margin: var(--space-2) 0; -} - -.list li { - counter-increment: item; - margin-bottom: var(--space-2); - display: flex; -} - -.list li:before { - margin-right: var(--space-1); - content: counter(item); - background: var(--color-primary-main); - border-radius: 100%; - color: var(--color-background-paper); - width: 20px; - height: 20px; - line-height: 20px; - font-size: 12px; - text-align: center; - flex-shrink: 0; -} - -.list li:last-child { - margin-bottom: 0; -} - -.defaultPassword, -.passwordsShouldMatch { - color: var(--color-border-main); -} - -.passwordsShouldMatch svg path { - stroke: var(--color-border-main); -} - -.defaultPassword svg path { - stroke: var(--color-border-main); -} - -.weakPassword { - color: var(--color-error-main); -} - -.weakPassword svg path:first-child { - stroke: var(--color-error-main); -} - -.mediumPassword { - color: var(--color-warning-main); -} - -.mediumPassword svg path:first-child, -.mediumPassword svg path:nth-child(2) { - stroke: var(--color-warning-main); -} - -.strongPassword { - color: var(--color-success-main); -} - -.strongPassword svg path { - stroke: var(--color-success-main); -} - -.passwordsMatch { - color: var(--color-success-main); -} - -.passwordsNoMatch { - color: var(--color-error-main); -} diff --git a/src/components/settings/SecurityLogin/index.tsx b/src/components/settings/SecurityLogin/index.tsx index 944df3bac7..d2ba01e986 100644 --- a/src/components/settings/SecurityLogin/index.tsx +++ b/src/components/settings/SecurityLogin/index.tsx @@ -1,55 +1,18 @@ -import { Box, Grid, Paper, Typography } from '@mui/material' -import SocialSignerMFA from './SocialSignerMFA' -import SocialSignerExport from './SocialSignerExport' -import useWallet from '@/hooks/wallets/useWallet' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' -import SpendingLimits from '../SpendingLimits' +import { Box } from '@mui/material' import dynamic from 'next/dynamic' import { useIsRecoverySupported } from '@/features/recovery/hooks/useIsRecoverySupported' import SecuritySettings from '../SecuritySettings' +import { useRouter } from 'next/router' const RecoverySettings = dynamic(() => import('@/features/recovery/components/RecoverySettings')) const SecurityLogin = () => { const isRecoverySupported = useIsRecoverySupported() - const wallet = useWallet() - const isSocialLogin = isSocialLoginWallet(wallet?.label) + const router = useRouter() return ( - {isRecoverySupported && } - - {isSocialLogin && ( - <> - - - - - Multi-factor Authentication - - - - - - - - - - - - - Social login signer export - - - - - - - - - )} - - + {isRecoverySupported && router.query.safe ? : null} diff --git a/src/components/settings/SettingsHeader/index.test.tsx b/src/components/settings/SettingsHeader/index.test.tsx index f50dc917fc..32574c41fb 100644 --- a/src/components/settings/SettingsHeader/index.test.tsx +++ b/src/components/settings/SettingsHeader/index.test.tsx @@ -1,12 +1,11 @@ -import SettingsHeader from '@/components/settings/SettingsHeader/index' +import { SettingsHeader } from '@/components/settings/SettingsHeader/index' +import { CONFIG_SERVICE_CHAINS } from '@/tests/mocks/chains' import * as safeAddress from '@/hooks/useSafeAddress' -import * as feature from '@/hooks/useChains' -import * as wallet from '@/hooks/wallets/useWallet' -import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' -import { connectedWalletBuilder } from '@/tests/builders/wallet' import { render } from '@/tests/test-utils' import { faker } from '@faker-js/faker' +import { FEATURES } from '@/utils/chains' +import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' describe('SettingsHeader', () => { beforeEach(() => { @@ -20,27 +19,24 @@ describe('SettingsHeader', () => { }) it('displays safe specific preferences if on a safe', () => { - const result = render() + const result = render() expect(result.getByText('Setup')).toBeInTheDocument() }) it('displays Notifications if feature is enabled', () => { - jest.spyOn(feature, 'useHasFeature').mockReturnValue(true) - - const result = render() + const result = render( + , + ) expect(result.getByText('Notifications')).toBeInTheDocument() }) - - it('displays Security & Login if connected wallet is a social signer', () => { - const mockWallet = connectedWalletBuilder().with({ label: ONBOARD_MPC_MODULE_LABEL }).build() - jest.spyOn(wallet, 'default').mockReturnValue(mockWallet) - - const result = render() - - expect(result.getByText('Security & Login')).toBeInTheDocument() - }) }) describe('No safe is open', () => { @@ -50,7 +46,7 @@ describe('SettingsHeader', () => { }) it('displays general preferences if no safe is open', () => { - const result = render() + const result = render() expect(result.getByText('Cookies')).toBeInTheDocument() expect(result.getByText('Appearance')).toBeInTheDocument() @@ -59,20 +55,17 @@ describe('SettingsHeader', () => { }) it('displays Notifications if feature is enabled', () => { - jest.spyOn(feature, 'useHasFeature').mockReturnValue(true) - - const result = render() + const result = render( + , + ) expect(result.getByText('Notifications')).toBeInTheDocument() }) - - it('displays Security & Login if connected wallet is a social signer', () => { - const mockWallet = connectedWalletBuilder().with({ label: ONBOARD_MPC_MODULE_LABEL }).build() - jest.spyOn(wallet, 'default').mockReturnValue(mockWallet) - - const result = render() - - expect(result.getByText('Security & Login')).toBeInTheDocument() - }) }) }) diff --git a/src/components/settings/SettingsHeader/index.tsx b/src/components/settings/SettingsHeader/index.tsx index 5bb856f21a..f20def13f1 100644 --- a/src/components/settings/SettingsHeader/index.tsx +++ b/src/components/settings/SettingsHeader/index.tsx @@ -5,11 +5,20 @@ import PageHeader from '@/components/common/PageHeader' import { generalSettingsNavItems, settingsNavItems } from '@/components/sidebar/SidebarNavigation/config' import css from '@/components/common/PageHeader/styles.module.css' import useSafeAddress from '@/hooks/useSafeAddress' +import { useCurrentChain } from '@/hooks/useChains' +import { isRouteEnabled } from '@/utils/chains' +import madProps from '@/utils/mad-props' -const SettingsHeader = (): ReactElement => { - const safeAddress = useSafeAddress() - - const navItems = safeAddress ? settingsNavItems : generalSettingsNavItems +export const SettingsHeader = ({ + safeAddress, + chain, +}: { + safeAddress: ReturnType + chain: ReturnType +}): ReactElement => { + const navItems = safeAddress + ? settingsNavItems.filter((route) => isRouteEnabled(route.href, chain)) + : generalSettingsNavItems return ( { ) } -export default SettingsHeader +export default madProps(SettingsHeader, { + safeAddress: useSafeAddress, + chain: useCurrentChain, +}) diff --git a/src/components/sidebar/SidebarFooter/index.tsx b/src/components/sidebar/SidebarFooter/index.tsx index cdfb75ab72..624c8dba07 100644 --- a/src/components/sidebar/SidebarFooter/index.tsx +++ b/src/components/sidebar/SidebarFooter/index.tsx @@ -9,7 +9,7 @@ import { } from '@/components/sidebar/SidebarList' import { BEAMER_SELECTOR, loadBeamer } from '@/services/beamer' import { useAppDispatch, useAppSelector } from '@/store' -import { selectCookies, CookieType } from '@/store/cookiesSlice' +import { selectCookies, CookieAndTermType } from '@/store/cookiesAndTermsSlice' import { openCookieBanner } from '@/store/popupSlice' // import BeamerIcon from '@/public/images/sidebar/whats-new.svg' import SuggestionIcon from '@/public/images/sidebar/lightbulb_icon.svg' @@ -29,7 +29,7 @@ const SidebarFooter = (): ReactElement => { const cookies = useAppSelector(selectCookies) const chain = useCurrentChain() - const hasBeamerConsent = useCallback(() => cookies[CookieType.UPDATES], [cookies]) + const hasBeamerConsent = useCallback(() => cookies[CookieAndTermType.UPDATES], [cookies]) useEffect(() => { // Initialise Beamer when consent was previously given @@ -40,7 +40,7 @@ const SidebarFooter = (): ReactElement => { const handleBeamer = () => { if (!hasBeamerConsent()) { - dispatch(openCookieBanner({ warningKey: CookieType.UPDATES })) + dispatch(openCookieBanner({ warningKey: CookieAndTermType.UPDATES })) } } diff --git a/src/components/sidebar/SidebarList/index.tsx b/src/components/sidebar/SidebarList/index.tsx index eba0ae05f1..409275b249 100644 --- a/src/components/sidebar/SidebarList/index.tsx +++ b/src/components/sidebar/SidebarList/index.tsx @@ -64,10 +64,34 @@ export const SidebarListItemText = ({ bold = false, ...rest }: ListItemTextProps & { bold?: boolean }): ReactElement => ( - + {children} ) export const SidebarListItemCounter = ({ count }: { count?: string }): ReactElement | null => - count ? : null + count ? ( + + ) : null diff --git a/src/components/sidebar/SidebarNavigation/config.tsx b/src/components/sidebar/SidebarNavigation/config.tsx index d63ea46b39..efa82d35f0 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 '@/components/common/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', @@ -90,8 +93,8 @@ export const settingsNavItems = [ href: AppRoutes.settings.appearance, }, { - label: 'Security & Login', - href: AppRoutes.settings.securityLogin, + label: 'Security', + href: AppRoutes.settings.security, }, // { // label: 'Notifications', @@ -129,8 +132,8 @@ export const generalSettingsNavItems = [ // href: AppRoutes.settings.notifications, // }, { - label: 'Security & Login', - href: AppRoutes.settings.securityLogin, + label: 'Security', + href: AppRoutes.settings.security, }, { label: 'Data', diff --git a/src/components/sidebar/SidebarNavigation/index.tsx b/src/components/sidebar/SidebarNavigation/index.tsx index b247e31b6a..1a5632c803 100644 --- a/src/components/sidebar/SidebarNavigation/index.tsx +++ b/src/components/sidebar/SidebarNavigation/index.tsx @@ -1,7 +1,7 @@ -import React, { useMemo, type ReactElement } from 'react' +import React, { useContext, useMemo, type ReactElement } from 'react' import { useRouter } from 'next/router' import ListItem from '@mui/material/ListItem' -import { type ChainInfo, ImplementationVersionState } from '@safe-global/safe-gateway-typescript-sdk' +import { ImplementationVersionState } from '@safe-global/safe-gateway-typescript-sdk' import { SidebarList, @@ -15,39 +15,32 @@ import useSafeInfo from '@/hooks/useSafeInfo' import { AppRoutes } from '@/config/routes' import { useQueuedTxsLength } from '@/hooks/useTxQueue' import { useCurrentChain } from '@/hooks/useChains' -import { FeatureRoutes, hasFeature } from '@/utils/chains' +import { isRouteEnabled } from '@/utils/chains' import { trackEvent } from '@/services/analytics' import { SWAP_EVENTS, SWAP_LABELS } from '@/services/analytics/events/swaps' -import useIsCounterfactualSafe from '@/features/counterfactual/hooks/useIsCounterfactualSafe' +import { GeoblockingContext } from '@/components/common/GeoblockingProvider' const getSubdirectory = (pathname: string): string => { return pathname.split('/')[1] } -const isRouteEnabled = (route: string, chain?: ChainInfo) => { - if (!chain) return false - - const featureRoute = FeatureRoutes[route] - return !featureRoute || hasFeature(chain, featureRoute) -} - const Navigation = (): ReactElement => { const chain = useCurrentChain() const router = useRouter() const { safe } = useSafeInfo() const currentSubdirectory = getSubdirectory(router.pathname) const queueSize = useQueuedTxsLength() - const isCounterFactualSafe = useIsCounterfactualSafe() + const isBlockedCountry = useContext(GeoblockingContext) const enabledNavItems = useMemo(() => { return navItems.filter((item) => { const enabled = isRouteEnabled(item.href, chain) - if (item.href === AppRoutes.swap && isCounterFactualSafe) { + if (item.href === AppRoutes.swap && isBlockedCountry) { return false } return enabled }) - }, [chain, isCounterFactualSafe]) + }, [chain, isBlockedCountry]) const getBadge = (item: NavItem) => { // Indicate whether the current Safe needs an upgrade @@ -56,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) { @@ -82,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/BulkTxListGroup/index.tsx b/src/components/transactions/BulkTxListGroup/index.tsx new file mode 100644 index 0000000000..b793fef6cc --- /dev/null +++ b/src/components/transactions/BulkTxListGroup/index.tsx @@ -0,0 +1,70 @@ +import type { ReactElement } from 'react' +import { Box, Paper, SvgIcon, Typography } from '@mui/material' +import type { Order, Transaction } from '@safe-global/safe-gateway-typescript-sdk' +import { isMultisigExecutionInfo, isSwapTransferOrderTxInfo } from '@/utils/transaction-guards' +import ExpandableTransactionItem from '@/components/transactions/TxListItem/ExpandableTransactionItem' +import BatchIcon from '@/public/images/common/batch.svg' +import css from './styles.module.css' +import ExplorerButton from '@/components/common/ExplorerButton' +import { getBlockExplorerLink } from '@/utils/chains' +import { useCurrentChain } from '@/hooks/useChains' +import { getOrderClass } from '@/features/swap/helpers/utils' + +const orderClassTitles: Record = { + limit: 'Limit order settlement', + twap: 'TWAP order settlement', + liquidity: 'Liquidity order settlement', + market: 'Swap order settlement', +} + +const getSettlementOrderTitle = (order: Order): string => { + const orderClass = getOrderClass(order) + return orderClassTitles[orderClass] || orderClassTitles['market'] +} + +const GroupedTxListItems = ({ + groupedListItems, + transactionHash, +}: { + groupedListItems: Transaction[] + transactionHash: string +}): ReactElement | null => { + const chain = useCurrentChain() + const explorerLink = chain && getBlockExplorerLink(chain, transactionHash)?.href + if (groupedListItems.length === 0) return null + let title = 'Bulk transactions' + const isSwapTransfer = isSwapTransferOrderTxInfo(groupedListItems[0].transaction.txInfo) + if (isSwapTransfer) { + title = getSettlementOrderTitle(groupedListItems[0].transaction.txInfo as Order) + } + return ( + + + + + + {title} + + {groupedListItems.length} transactions + + + + + + {groupedListItems.map((tx) => { + const nonce = isMultisigExecutionInfo(tx.transaction.executionInfo) ? tx.transaction.executionInfo.nonce : '' + return ( + + + {nonce} + + + + ) + })} + + + ) +} + +export default GroupedTxListItems diff --git a/src/components/transactions/BulkTxListGroup/styles.module.css b/src/components/transactions/BulkTxListGroup/styles.module.css new file mode 100644 index 0000000000..da621dcdbc --- /dev/null +++ b/src/components/transactions/BulkTxListGroup/styles.module.css @@ -0,0 +1,60 @@ +.container { + position: relative; + padding: var(--space-2); + display: grid; + align-items: center; + grid-template-columns: minmax(50px, 0.25fr) minmax(240px, 2fr) minmax(150px, 4fr) minmax(170px, 1fr); + grid-template-areas: + 'icon info action hash' + 'nonce items items items'; +} + +.action { + margin-left: var(--space-2); + grid-area: action; + color: var(--color-text-secondary); +} + +.hash { + grid-area: hash; + display: grid; + justify-content: flex-end; +} + +.nonce { + position: absolute; + left: -24px; + top: var(--space-1); +} + +.txItems { + display: flex; + flex-direction: column; + gap: var(--space-1); + margin-top: var(--space-2); +} + +.txItems :global(.MuiAccordion-root) { + border-color: var(--color-border-light); +} + +@media (max-width: 699px) { + .container { + grid-template-columns: minmax(30px, 0.25fr) minmax(230px, 3fr); + grid-template-areas: + 'icon info ' + 'nonce action' + 'nonce hash ' + 'nonce items'; + } + + .action { + margin: 0; + } + .hash { + justify-content: flex-start; + } + .nonce { + left: -16px; + } +} diff --git a/src/components/transactions/GroupedTxListItems/index.tsx b/src/components/transactions/GroupedTxListItems/index.tsx index 6bd632611d..424bcdcc89 100644 --- a/src/components/transactions/GroupedTxListItems/index.tsx +++ b/src/components/transactions/GroupedTxListItems/index.tsx @@ -45,7 +45,7 @@ const TxGroup = ({ groupedListItems }: { groupedListItems: Transaction[] }): Rea key={tx.transaction.id} className={replacedTxIds.includes(tx.transaction.id) ? css.willBeReplaced : undefined} > - + ))}
    diff --git a/src/components/transactions/ImitationTransactionWarning/index.tsx b/src/components/transactions/ImitationTransactionWarning/index.tsx new file mode 100644 index 0000000000..9ca4a675bb --- /dev/null +++ b/src/components/transactions/ImitationTransactionWarning/index.tsx @@ -0,0 +1,18 @@ +import type { ReactElement } from 'react' +import { Alert, SvgIcon } from '@mui/material' + +import InfoOutlinedIcon from '@/public/images/notifications/info.svg' +import css from './styles.module.css' + +export const ImitationTransactionWarning = (): ReactElement => { + return ( + `3px solid ${palette['error'].main} !important` }} + severity="error" + icon={} + > + This may be a malicious transaction. Check and confirm the address before interacting with it.{' '} + + ) +} diff --git a/src/components/transactions/ImitationTransactionWarning/styles.module.css b/src/components/transactions/ImitationTransactionWarning/styles.module.css new file mode 100644 index 0000000000..4a851f74d5 --- /dev/null +++ b/src/components/transactions/ImitationTransactionWarning/styles.module.css @@ -0,0 +1,3 @@ +.alert { + padding: 0px 10px; +} diff --git a/src/components/transactions/UntrustedTxWarning/index.tsx b/src/components/transactions/MaliciousTxWarning/index.tsx similarity index 53% rename from src/components/transactions/UntrustedTxWarning/index.tsx rename to src/components/transactions/MaliciousTxWarning/index.tsx index e95e15fdf0..adbb973fb7 100644 --- a/src/components/transactions/UntrustedTxWarning/index.tsx +++ b/src/components/transactions/MaliciousTxWarning/index.tsx @@ -1,19 +1,18 @@ import { Tooltip, SvgIcon, Box } from '@mui/material' import WarningIcon from '@/public/images/notifications/warning.svg' -const UntrustedTxWarning = () => { - return ( +const MaliciousTxWarning = ({ withTooltip = true }: { withTooltip?: boolean }) => { + return withTooltip ? ( - + + ) : ( + + + ) } -export default UntrustedTxWarning +export default MaliciousTxWarning diff --git a/src/components/transactions/RejectTxButton/index.tsx b/src/components/transactions/RejectTxButton/index.tsx index d7c6f5a42b..80bac12852 100644 --- a/src/components/transactions/RejectTxButton/index.tsx +++ b/src/components/transactions/RejectTxButton/index.tsx @@ -36,7 +36,13 @@ const RejectTxButton = ({ {(isOk) => ( - diff --git a/src/components/transactions/TrustedToggle/TrustedToggleButton.tsx b/src/components/transactions/TrustedToggle/TrustedToggleButton.tsx index a044c38a78..c5bc393a8d 100644 --- a/src/components/transactions/TrustedToggle/TrustedToggleButton.tsx +++ b/src/components/transactions/TrustedToggle/TrustedToggleButton.tsx @@ -1,6 +1,5 @@ import { type ReactElement } from 'react' -import { Typography, Button, SvgIcon } from '@mui/material' -import GhostIcon from '@/public/images/transactions/ghost.svg' +import { FormControlLabel, Switch } from '@mui/material' import { TX_LIST_EVENTS } from '@/services/analytics' import Track from '@/components/common/Track' @@ -23,22 +22,11 @@ const _TrustedToggleButton = ({ return ( - + control={} + label={<>Hide suspicious} + /> ) } diff --git a/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.test.tsx b/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.test.tsx index 1141df3261..357ca4fc1b 100644 --- a/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.test.tsx +++ b/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.test.tsx @@ -52,7 +52,7 @@ describe('SingleTxDecoded', () => { />, ) - expect(result.queryByText('Unknown contract interaction')).not.toBeNull() + expect(result.queryByText('contract interaction')).not.toBeNull() }) it('should show decoded data ', () => { diff --git a/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx b/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx index 3aa6ea378c..920f350015 100644 --- a/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx +++ b/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx @@ -1,19 +1,25 @@ import { isEmptyHexData } from '@/utils/hex' -import { type InternalTransaction, Operation, type TransactionData } from '@safe-global/safe-gateway-typescript-sdk' +import { + type InternalTransaction, + Operation, + type TransactionData, + TokenType, +} from '@safe-global/safe-gateway-typescript-sdk' import type { AccordionProps } from '@mui/material/Accordion/Accordion' import { useCurrentChain } from '@/hooks/useChains' -import { formatVisualAmount } from '@/utils/formatters' +import { safeFormatUnits } from '@/utils/formatters' import { MethodDetails } from '@/components/transactions/TxDetails/TxData/DecodedData/MethodDetails' import { HexEncodedData } from '@/components/transactions/HexEncodedData' import { isDeleteAllowance, isSetAllowance } from '@/utils/transaction-guards' -import { Accordion, AccordionDetails, AccordionSummary, Typography } from '@mui/material' +import { Accordion, AccordionDetails, AccordionSummary, Stack, Typography } from '@mui/material' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import css from './styles.module.css' import accordionCss from '@/styles/accordion.module.css' import CodeIcon from '@mui/icons-material/Code' import { DelegateCallWarning } from '@/components/transactions/Warning' -import { InfoDetails } from '@/components/transactions/InfoDetails' -import NamedAddressInfo from '@/components/common/NamedAddressInfo' +import SendAmountBlock from '@/components/tx-flow/flows/TokenTransfer/SendAmountBlock' +import { ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants' +import SendToBlock from '@/components/tx/SendToBlock' type SingleTxDecodedProps = { tx: InternalTransaction @@ -36,9 +42,9 @@ export const SingleTxDecoded = ({ }: SingleTxDecodedProps) => { const chain = useCurrentChain() const isNativeTransfer = tx.value !== '0' && (!tx.data || isEmptyHexData(tx.data)) - const method = tx.dataDecoded?.method || (isNativeTransfer ? 'native transfer' : 'Unknown contract interaction') - const { decimals, symbol } = chain?.nativeCurrency || {} - const amount = tx.value ? formatVisualAmount(tx.value, decimals) : 0 + const method = tx.dataDecoded?.method || (isNativeTransfer ? 'native transfer' : 'contract interaction') + const { decimals } = chain?.nativeCurrency || {} + const amount = tx.value ? safeFormatUnits(tx.value, decimals) : 0 let details if (tx.dataDecoded) { @@ -50,9 +56,6 @@ export const SingleTxDecoded = ({ const addressInfo = txData.addressInfoIndex?.[tx.to] const name = addressInfo?.name - const avatarUrl = addressInfo?.logoUri - - const title = `Interact with${Number(amount) !== 0 ? ` (and send ${amount} ${symbol} to)` : ''}:` const isDelegateCall = tx.operation === Operation.DELEGATE && showDelegateCallWarning const isSpendingLimitMethod = isSetAllowance(tx.dataDecoded?.method) || isDeleteAllowance(tx.dataDecoded?.method) @@ -73,16 +76,21 @@ export const SingleTxDecoded = ({ {/* We always warn of nested delegate calls */} {isDelegateCall && } {!isSpendingLimitMethod && ( - - - + + {amount !== '0' && ( + + )} + + )} {details} diff --git a/src/components/transactions/TxDetails/TxData/DecodedData/index.tsx b/src/components/transactions/TxDetails/TxData/DecodedData/index.tsx index cce1e5af95..156d8b1949 100644 --- a/src/components/transactions/TxDetails/TxData/DecodedData/index.tsx +++ b/src/components/transactions/TxDetails/TxData/DecodedData/index.tsx @@ -1,10 +1,15 @@ import type { ReactElement } from 'react' -import type { TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' -import { isCustomTxInfo } from '@/utils/transaction-guards' -import { InfoDetails } from '@/components/transactions/InfoDetails' +import { TokenType, type TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' + import { HexEncodedData } from '@/components/transactions/HexEncodedData' import { MethodDetails } from '@/components/transactions/TxDetails/TxData/DecodedData/MethodDetails' -import NamedAddressInfo from '@/components/common/NamedAddressInfo' +import { useCurrentChain } from '@/hooks/useChains' +import { formatVisualAmount } from '@/utils/formatters' +import SendAmountBlock from '@/components/tx-flow/flows/TokenTransfer/SendAmountBlock' +import { ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants' +import SendToBlock from '@/components/tx/SendToBlock' +import { Stack } from '@mui/material' +import { isCustomTxInfo } from '@/utils/transaction-guards' interface Props { txData: TransactionDetails['txData'] @@ -12,6 +17,7 @@ interface Props { } export const DecodedData = ({ txData, txInfo }: Props): ReactElement | null => { + const chainInfo = useCurrentChain() // nothing to render if (!txData) { return null @@ -25,22 +31,31 @@ export const DecodedData = ({ txData, txInfo }: Props): ReactElement | null => { decodedData = } + const amount = txData.value ? formatVisualAmount(txData.value, chainInfo?.nativeCurrency.decimals) : '0' // we render the decoded data return ( - <> - - + {amount !== '0' && ( + - + )} + {decodedData} - + ) } diff --git a/src/components/transactions/TxDetails/TxData/Transfer/index.test.tsx b/src/components/transactions/TxDetails/TxData/Transfer/index.test.tsx new file mode 100644 index 0000000000..d904372959 --- /dev/null +++ b/src/components/transactions/TxDetails/TxData/Transfer/index.test.tsx @@ -0,0 +1,276 @@ +import { render } from '@/tests/test-utils' +import TransferTxInfo from '.' +import { + TransactionInfoType, + TransactionStatus, + TransactionTokenType, + TransferDirection, +} from '@safe-global/safe-gateway-typescript-sdk' +import { faker } from '@faker-js/faker' +import { parseUnits } from 'ethers' +import { chainBuilder } from '@/tests/builders/chains' + +jest.mock('@/hooks/useChains', () => ({ + __esModule: true, + useChainId: () => '1', + useChain: () => chainBuilder().with({ chainId: '1' }).build(), + useCurrentChain: () => chainBuilder().with({ chainId: '1' }).build(), + default: () => ({ + loading: false, + error: undefined, + configs: [chainBuilder().with({ chainId: '1' }).build()], + }), +})) + +describe('TransferTxInfo', () => { + describe('should render non-malicious', () => { + it('outgoing tx', () => { + const recipient = faker.finance.ethereumAddress() + const sender = faker.finance.ethereumAddress() + const tokenAddress = faker.finance.ethereumAddress() + + const result = render( + , + ) + + expect(result.getByText('1 TST')).toBeInTheDocument() + expect(result.getByText(recipient)).toBeInTheDocument() + expect(result.queryByText('malicious', { exact: false })).toBeNull() + expect(result.queryByLabelText('This token is unfamiliar', { exact: false })).toBeNull() + }) + + it('incoming tx', () => { + const recipient = faker.finance.ethereumAddress() + const sender = faker.finance.ethereumAddress() + const tokenAddress = faker.finance.ethereumAddress() + + const result = render( + , + ) + + expect(result.getByText('12.34 TST')).toBeInTheDocument() + expect(result.getByText(sender)).toBeInTheDocument() + expect(result.queryByText('malicious', { exact: false })).toBeNull() + expect(result.queryByLabelText('This token is unfamiliar', { exact: false })).toBeNull() + }) + }) + + describe('should render untrusted', () => { + it('outgoing tx', () => { + const recipient = faker.finance.ethereumAddress() + const sender = faker.finance.ethereumAddress() + const tokenAddress = faker.finance.ethereumAddress() + + const result = render( + , + ) + + expect(result.getByText('1 TST')).toBeInTheDocument() + expect(result.getByText(recipient)).toBeInTheDocument() + expect(result.queryByText('malicious', { exact: false })).toBeNull() + expect(result.getByLabelText('This token is unfamiliar', { exact: false })).toBeInTheDocument() + }) + + it('incoming tx', () => { + const recipient = faker.finance.ethereumAddress() + const sender = faker.finance.ethereumAddress() + const tokenAddress = faker.finance.ethereumAddress() + + const result = render( + , + ) + + expect(result.getByText('12.34 TST')).toBeInTheDocument() + expect(result.getByText(sender)).toBeInTheDocument() + expect(result.queryByText('malicious', { exact: false })).toBeNull() + expect(result.queryByLabelText('This token is unfamiliar', { exact: false })).toBeInTheDocument() + }) + }) + + describe('should render imitations', () => { + it('outgoing tx', () => { + const recipient = faker.finance.ethereumAddress() + const sender = faker.finance.ethereumAddress() + const tokenAddress = faker.finance.ethereumAddress() + + const result = render( + , + ) + + expect(result.getByText('1 TST')).toBeInTheDocument() + expect(result.getByText(recipient)).toBeInTheDocument() + expect(result.getByText('malicious', { exact: false })).toBeInTheDocument() + expect(result.queryByLabelText('This token is unfamiliar', { exact: false })).toBeNull() + }) + + it('incoming tx', () => { + const recipient = faker.finance.ethereumAddress() + const sender = faker.finance.ethereumAddress() + const tokenAddress = faker.finance.ethereumAddress() + + const result = render( + , + ) + + expect(result.getByText('12.34 TST')).toBeInTheDocument() + expect(result.getByText(sender)).toBeInTheDocument() + expect(result.getByText('malicious', { exact: false })).toBeInTheDocument() + expect(result.queryByLabelText('This token is unfamiliar', { exact: false })).toBeNull() + }) + + it('untrusted and imitation tx', () => { + const recipient = faker.finance.ethereumAddress() + const sender = faker.finance.ethereumAddress() + const tokenAddress = faker.finance.ethereumAddress() + + const result = render( + , + ) + + expect(result.getByText('12.34 TST')).toBeInTheDocument() + expect(result.getByText(sender)).toBeInTheDocument() + expect(result.getByText('malicious', { exact: false })).toBeInTheDocument() + expect(result.queryByLabelText('This token is unfamiliar', { exact: false })).toBeNull() + }) + }) +}) diff --git a/src/components/transactions/TxDetails/TxData/Transfer/index.tsx b/src/components/transactions/TxDetails/TxData/Transfer/index.tsx index 4fde362c28..9971388af1 100644 --- a/src/components/transactions/TxDetails/TxData/Transfer/index.tsx +++ b/src/components/transactions/TxDetails/TxData/Transfer/index.tsx @@ -7,14 +7,17 @@ import { Box, Typography } from '@mui/material' import React from 'react' import TransferActions from '@/components/transactions/TxDetails/TxData/Transfer/TransferActions' -import UntrustedTxWarning from '@/components/transactions/UntrustedTxWarning' +import MaliciousTxWarning from '@/components/transactions/MaliciousTxWarning' +import { ImitationTransactionWarning } from '@/components/transactions/ImitationTransactionWarning' type TransferTxInfoProps = { txInfo: Transfer txStatus: TransactionStatus + trusted: boolean + imitation: boolean } -const TransferTxInfoSummary = ({ txInfo, txStatus, trusted }: TransferTxInfoProps & { trusted: boolean }) => { +const TransferTxInfoMain = ({ txInfo, txStatus, trusted, imitation }: TransferTxInfoProps) => { const { direction } = txInfo return ( @@ -22,21 +25,21 @@ const TransferTxInfoSummary = ({ txInfo, txStatus, trusted }: TransferTxInfoProp {direction === TransferDirection.INCOMING ? 'Received' : isTxQueued(txStatus) ? 'Send' : 'Sent'}{' '} - + {direction === TransferDirection.INCOMING ? ' from:' : ' to:'} - {!trusted && } + {!trusted && !imitation && }
    ) } -const TransferTxInfo = ({ txInfo, txStatus, trusted }: TransferTxInfoProps & { trusted: boolean }) => { +const TransferTxInfo = ({ txInfo, txStatus, trusted, imitation }: TransferTxInfoProps) => { const address = txInfo.direction.toUpperCase() === TransferDirection.INCOMING ? txInfo.sender : txInfo.recipient return ( - + + {imitation && } ) } diff --git a/src/components/transactions/TxDetails/TxData/index.tsx b/src/components/transactions/TxDetails/TxData/index.tsx index 80e10048f5..060e7656a5 100644 --- a/src/components/transactions/TxDetails/TxData/index.tsx +++ b/src/components/transactions/TxDetails/TxData/index.tsx @@ -1,5 +1,4 @@ import SettingsChangeTxInfo from '@/components/transactions/TxDetails/TxData/SettingsChange' -import SwapTxInfo from '@/features/swap/components/SwapTxInfo' import type { SpendingLimitMethods } from '@/utils/transaction-guards' import { isCancellationTxInfo, @@ -9,7 +8,7 @@ import { isSettingsChangeTxInfo, isSpendingLimitMethod, isSupportedSpendingLimitAddress, - isSwapTxInfo, + isSwapOrderTxInfo, isTransferTxInfo, } from '@/utils/transaction-guards' import { SpendingLimits } from '@/components/transactions/TxDetails/TxData/SpendingLimits' @@ -20,13 +19,22 @@ import DecodedData from '@/components/transactions/TxDetails/TxData/DecodedData' import TransferTxInfo from '@/components/transactions/TxDetails/TxData/Transfer' import useChainId from '@/hooks/useChainId' import { MultiSendTxInfo } from '@/components/transactions/TxDetails/TxData/MultiSendTxInfo' +import InteractWith from '@/features/swap/components/SwapTxInfo/interactWith' -const TxData = ({ txDetails, trusted }: { txDetails: TransactionDetails; trusted: boolean }): ReactElement => { +const TxData = ({ + txDetails, + trusted, + imitation, +}: { + txDetails: TransactionDetails + trusted: boolean + imitation: boolean +}): ReactElement => { const chainId = useChainId() const txInfo = txDetails.txInfo if (isTransferTxInfo(txInfo)) { - return + return } if (isSettingsChangeTxInfo(txInfo)) { @@ -46,8 +54,8 @@ const TxData = ({ txDetails, trusted }: { txDetails: TransactionDetails; trusted return } - if (isSwapTxInfo(txInfo)) { - return + if (isSwapOrderTxInfo(txInfo)) { + return } return diff --git a/src/components/transactions/TxDetails/index.tsx b/src/components/transactions/TxDetails/index.tsx index 3305e3af40..c84423d4da 100644 --- a/src/components/transactions/TxDetails/index.tsx +++ b/src/components/transactions/TxDetails/index.tsx @@ -13,13 +13,14 @@ import useChainId from '@/hooks/useChainId' import useAsync from '@/hooks/useAsync' import { isAwaitingExecution, + isOrderTxInfo, isModuleExecutionInfo, isMultiSendTxInfo, isMultisigDetailedExecutionInfo, isMultisigExecutionInfo, - isOpenSwap, - isSwapTxInfo, + isOpenSwapOrder, isTxQueued, + isSwapTransferOrderTxInfo, } from '@/utils/transaction-guards' import { InfoDetails } from '@/components/transactions/InfoDetails' import EthHashInfo from '@/components/common/EthHashInfo' @@ -34,7 +35,7 @@ import { DelegateCallWarning, UnsignedWarning } from '@/components/transactions/ import Multisend from '@/components/transactions/TxDetails/TxData/DecodedData/Multisend' import useSafeInfo from '@/hooks/useSafeInfo' import useIsPending from '@/hooks/useIsPending' -import { isTrustedTx } from '@/utils/transactions' +import { isImitation, isTrustedTx } from '@/utils/transactions' import { useHasFeature } from '@/hooks/useChains' import { FEATURES } from '@/utils/chains' import { SwapOrder } from '@/features/swap/components/SwapOrder' @@ -60,6 +61,7 @@ const TxDetailsBlock = ({ txSummary, txDetails }: TxDetailsProps): ReactElement // If we have no token list we always trust the transfer const isTrustedTransfer = !hasDefaultTokenlist || isTrustedTx(txSummary) + const isImitationTransaction = isImitation(txSummary) let proposer, safeTxHash if (isMultisigDetailedExecutionInfo(txDetails.detailedExecutionInfo)) { @@ -73,7 +75,7 @@ const TxDetailsBlock = ({ txSummary, txDetails }: TxDetailsProps): ReactElement <> {/* /Details */}
    - {isSwapTxInfo(txDetails.txInfo) && ( + {isOrderTxInfo(txDetails.txInfo) && (
    Error parsing data
    }> @@ -87,7 +89,14 @@ const TxDetailsBlock = ({ txSummary, txDetails }: TxDetailsProps): ReactElement
    Error parsing data
    }> - + + {isSwapTransferOrderTxInfo(txDetails.txInfo) && ( +
    + Error parsing data
    }> + + +
    + )} @@ -116,7 +125,7 @@ const TxDetailsBlock = ({ txSummary, txDetails }: TxDetailsProps): ReactElement - {(isMultiSendTxInfo(txDetails.txInfo) || isSwapTxInfo(txDetails.txInfo)) && ( + {(isMultiSendTxInfo(txDetails.txInfo) || isOrderTxInfo(txDetails.txInfo)) && (
    Error parsing data
    }> @@ -159,7 +168,7 @@ const TxDetails = ({ const { safe } = useSafeInfo() const [pollCount] = useIntervalCounter(POLLING_INTERVAL) - const swapPollCount = isOpenSwap(txSummary.txInfo) ? pollCount : 0 + const swapPollCount = isOpenSwapOrder(txSummary.txInfo) ? pollCount : 0 const [txDetailsData, error, loading] = useAsync( async () => { diff --git a/src/components/transactions/TxDetails/styles.module.css b/src/components/transactions/TxDetails/styles.module.css index 5818f7b943..21d7b6ae27 100644 --- a/src/components/transactions/TxDetails/styles.module.css +++ b/src/components/transactions/TxDetails/styles.module.css @@ -17,10 +17,6 @@ top: 16px; } -.details .noSigners { - width: 100%; -} - .loading, .error, .txData, @@ -31,6 +27,14 @@ padding: var(--space-2); } +.swapOrderTransfer { + border-top: 1px solid var(--color-border-light); + margin-top: var(--space-2); + margin-left: calc(var(--space-2) * -1); + margin-right: calc(var(--space-2) * -1); + padding: var(--space-2); + padding-top: var(--space-3) +} .txData, .swapOrder { border-bottom: 1px solid var(--color-border-light); diff --git a/src/components/transactions/TxFilterForm/TxFilterForm.test.tsx b/src/components/transactions/TxFilterForm/TxFilterForm.test.tsx new file mode 100644 index 0000000000..fc9a83e9ae --- /dev/null +++ b/src/components/transactions/TxFilterForm/TxFilterForm.test.tsx @@ -0,0 +1,227 @@ +import React from 'react' +import { screen, fireEvent } from '@testing-library/react' +import { act, render } from '@/tests/test-utils' +import '@testing-library/jest-dom/extend-expect' +import TxFilterForm from './index' +import { useRouter } from 'next/router' + +jest.mock('next/router', () => ({ + useRouter: jest.fn(), +})) + +const mockRouter = { + query: {}, + pathname: '', + push: jest.fn(), +} + +const toggleFilter = jest.fn() + +const fromDate = '20/01/2021' +const toDate = '20/01/2020' +const placeholder = 'dd/mm/yyyy' +const errorMsgFormat = 'Invalid address format' + +describe('TxFilterForm Component Tests', () => { + beforeEach(() => { + ;(useRouter as jest.Mock).mockReturnValue(mockRouter) + }) + + const renderComponent = () => render() + + it('Verify that when an end date is behind a start date, there are validation rules applied', async () => { + renderComponent() + + const errorMsgEndDate = 'Must be after "From" date' + const errorMsgStartDate = 'Must be before "To" date' + + const fromDateInput = screen.getAllByPlaceholderText(placeholder)[0] + const toDateInput = screen.getAllByPlaceholderText(placeholder)[1] + + await act(async () => { + fireEvent.change(fromDateInput, { target: { value: fromDate } }) + fireEvent.change(toDateInput, { target: { value: toDate } }) + }) + + expect(fromDateInput).toHaveValue(fromDate) + expect(toDateInput).toHaveValue(toDate) + + expect(await screen.findByText(errorMsgEndDate, { selector: 'label' })).toBeInTheDocument() + + await act(async () => { + fireEvent.change(fromDateInput, { target: { value: '' } }) + fireEvent.change(toDateInput, { target: { value: '' } }) + fireEvent.change(toDateInput, { target: { value: toDate } }) + fireEvent.change(fromDateInput, { target: { value: fromDate } }) + }) + + expect(toDateInput).toHaveValue(toDate) + expect(fromDateInput).toHaveValue(fromDate) + + expect(await screen.findByText(errorMsgStartDate, { selector: 'label' })).toBeInTheDocument() + }) + + it('Verify there is error when start and end date contain far future dates', async () => { + renderComponent() + + const futureDate = '20/01/2036' + const errorMsgFutureDate = 'Date cannot be in the future' + + const fromDateInput = screen.getAllByPlaceholderText(placeholder)[0] + const toDateInput = screen.getAllByPlaceholderText(placeholder)[1] + + await act(async () => { + fireEvent.change(fromDateInput, { target: { value: fromDate } }) + fireEvent.change(toDateInput, { target: { value: futureDate } }) + }) + + expect(await screen.findByText(errorMsgFutureDate, { selector: 'label' })).toBeInTheDocument() + + await act(async () => { + fireEvent.change(fromDateInput, { target: { value: futureDate } }) + fireEvent.change(toDateInput, { target: { value: toDate } }) + }) + + expect(await screen.findByText(errorMsgFutureDate, { selector: 'label' })).toBeInTheDocument() + }) + + it('Verify that when entering invalid characters in token filed shows an error message', async () => { + renderComponent() + + const token = '694urt5' + + const tokenInput = screen.getByTestId('token-input').querySelector('input') as HTMLInputElement + + expect(tokenInput).toBeInTheDocument() + + await act(async () => { + fireEvent.change(tokenInput, { target: { value: token } }) + }) + + expect(await screen.findByText(errorMsgFormat, { selector: 'label' })).toBeInTheDocument() + }) + + it('Verify there is error when 0 is entered in amount field', async () => { + renderComponent() + + const errorMsgZero = 'The value must be greater than 0' + const amountInput = screen.getByTestId('amount-input').querySelector('input') as HTMLInputElement + + expect(amountInput).toBeInTheDocument() + + await act(async () => { + fireEvent.change(amountInput, { target: { value: '0' } }) + }) + + expect(await screen.findByText(errorMsgZero, { selector: 'label' })).toBeInTheDocument() + }) + + it('Verify that entering negative numbers and a non-numeric value in the amount filter is not allowed', async () => { + renderComponent() + + const amountInput = screen.getByTestId('amount-input').querySelector('input') as HTMLInputElement + + expect(amountInput).toBeInTheDocument() + + await act(async () => { + fireEvent.change(amountInput, { target: { value: '-1' } }) + }) + expect(amountInput).toHaveValue('1') + await act(async () => { + fireEvent.change(amountInput, { target: { value: 'hrtyu' } }) + }) + expect(amountInput).toHaveValue('') + }) + + it('Verify that characters and negative numbers cannot be entered in nonce filed', async () => { + renderComponent() + + const outgoingRadio = screen.getByLabelText('Outgoing') + fireEvent.click(outgoingRadio) + + const nonceInput = screen.getByTestId('nonce-input').querySelector('input') as HTMLInputElement + + expect(nonceInput).toBeInTheDocument() + + await act(async () => { + fireEvent.change(nonceInput, { target: { value: '-1' } }) + }) + expect(nonceInput).toHaveValue('1') + await act(async () => { + fireEvent.change(nonceInput, { target: { value: 'hrtyu' } }) + }) + expect(nonceInput).toHaveValue('') + }) + + it('Verify that entering random characters in module field shows error', async () => { + renderComponent() + + const outgoingRadio = screen.getByLabelText('Module-based') + fireEvent.click(outgoingRadio) + + const addressInput = screen.getByTestId('address-item').querySelector('input') as HTMLInputElement + + expect(addressInput).toBeInTheDocument() + + await act(async () => { + fireEvent.change(addressInput, { target: { value: 'hrtyu' } }) + }) + expect(await screen.findByText(errorMsgFormat, { selector: 'label' })).toBeInTheDocument() + }) + + it('Verify when filter is cleared, the filter modal is still displayed', async () => { + renderComponent() + + const fromDate1 = '20/01/2021' + const toDate1 = '20/01/2022' + + const clearButton = screen.getByTestId('clear-btn') + const modal = screen.getByTestId('filter-modal') + + const fromDateInput = screen.getAllByPlaceholderText(placeholder)[0] + const toDateInput = screen.getAllByPlaceholderText(placeholder)[1] + + await act(async () => { + fireEvent.change(fromDateInput, { target: { value: fromDate1 } }) + fireEvent.change(toDateInput, { target: { value: toDate1 } }) + }) + + expect(fromDateInput).toHaveValue(fromDate1) + expect(toDateInput).toHaveValue(toDate1) + + await act(async () => { + fireEvent.click(clearButton) + }) + + expect(fromDateInput).toHaveValue('') + expect(toDateInput).toHaveValue('') + expect(modal).toBeInTheDocument() + }) + + it('Verify when filter is applied, it disappears from the view', async () => { + renderComponent() + + const fromDate = '20/01/2020' + const toDate = '20/01/2021' + + const applyButton = screen.getByTestId('apply-btn') + + const fromDateInput = screen.getAllByPlaceholderText(placeholder)[0] + const toDateInput = screen.getAllByPlaceholderText(placeholder)[1] + + await act(async () => { + fireEvent.change(fromDateInput, { target: { value: fromDate } }) + fireEvent.change(toDateInput, { target: { value: toDate } }) + }) + + expect(fromDateInput).toHaveValue(fromDate) + expect(toDateInput).toHaveValue(toDate) + + await act(async () => { + fireEvent.click(applyButton) + }) + + // Check that toggleFilter hide filter trigger has been called + expect(toggleFilter).toHaveBeenCalled() + }) +}) diff --git a/src/components/transactions/TxFilterForm/index.tsx b/src/components/transactions/TxFilterForm/index.tsx index dc07cb4c9e..9fb166b661 100644 --- a/src/components/transactions/TxFilterForm/index.tsx +++ b/src/components/transactions/TxFilterForm/index.tsx @@ -120,7 +120,7 @@ const TxFilterForm = ({ toggleFilter }: { toggleFilter: () => void }): ReactElem
    - + palette.primary.light }}>Transaction type @@ -146,7 +146,7 @@ const TxFilterForm = ({ toggleFilter }: { toggleFilter: () => void }): ReactElem {!isModuleFilter && ( <> - + void }): ReactElem }} /> - + void }): ReactElem }} render={({ field, fieldState }) => ( void }): ReactElem {isIncomingFilter && ( void }): ReactElem }} render={({ field, fieldState }) => ( void }): ReactElem - - diff --git a/src/components/transactions/TxInfo/index.tsx b/src/components/transactions/TxInfo/index.tsx index 0cb44b6867..2556365262 100644 --- a/src/components/transactions/TxInfo/index.tsx +++ b/src/components/transactions/TxInfo/index.tsx @@ -1,16 +1,16 @@ import { type ReactElement } from 'react' import type { - Transfer, - Custom, Creation, - TransactionInfo, + Custom, MultiSend, SettingsChange, - SwapOrder, + TransactionInfo, + Transfer, } from '@safe-global/safe-gateway-typescript-sdk' import { SettingsInfoType } from '@safe-global/safe-gateway-typescript-sdk' import TokenAmount from '@/components/common/TokenAmount' import { + isOrderTxInfo, isCreationTxInfo, isCustomTxInfo, isERC20Transfer, @@ -18,22 +18,22 @@ import { isMultiSendTxInfo, isNativeTokenTransfer, isSettingsChangeTxInfo, - isSwapTxInfo, isTransferTxInfo, } from '@/utils/transaction-guards' import { ellipsis, shortenAddress } from '@/utils/formatters' import { useCurrentChain } from '@/hooks/useChains' -import TokenIcon from '@/components/common/TokenIcon' -import { Box, Typography } from '@mui/material' +import { SwapTx } from '@/features/swap/components/SwapTxInfo/SwapTx' export const TransferTx = ({ info, omitSign = false, withLogo = true, + preciseAmount = false, }: { info: Transfer omitSign?: boolean withLogo?: boolean + preciseAmount?: boolean }): ReactElement => { const chainConfig = useCurrentChain() const { nativeCurrency } = chainConfig || {} @@ -48,12 +48,20 @@ export const TransferTx = ({ decimals={nativeCurrency?.decimals} tokenSymbol={nativeCurrency?.symbol} logoUri={withLogo ? nativeCurrency?.logoUri : undefined} + preciseAmount={preciseAmount} /> ) } if (isERC20Transfer(transfer)) { - return + return ( + + ) } if (isERC721Transfer(transfer)) { @@ -91,27 +99,6 @@ const MultiSendTx = ({ info }: { info: MultiSend }): ReactElement => { ) } -const SwapTx = ({ info }: { info: SwapOrder }): ReactElement => { - return ( - - - - - - - {info.sellToken.symbol}  - - to - - - {' '} - - {info.buyToken.symbol} - - - - ) -} const SettingsChangeTx = ({ info }: { info: SettingsChange }): ReactElement => { if ( info.settingsInfo?.type === SettingsInfoType.ENABLE_MODULE || @@ -144,7 +131,7 @@ const TxInfo = ({ info, ...rest }: { info: TransactionInfo; omitSign?: boolean; return } - if (isSwapTxInfo(info)) { + if (isOrderTxInfo(info)) { return } diff --git a/src/components/transactions/TxList/index.tsx b/src/components/transactions/TxList/index.tsx index 1b92a9860b..d7f8b45866 100644 --- a/src/components/transactions/TxList/index.tsx +++ b/src/components/transactions/TxList/index.tsx @@ -1,29 +1,41 @@ +import GroupedTxListItems from '@/components/transactions/GroupedTxListItems' +import { groupTxs } from '@/utils/tx-list' +import { Box } from '@mui/material' +import type { Transaction, TransactionListPage } from '@safe-global/safe-gateway-typescript-sdk' import type { ReactElement, ReactNode } from 'react' import { useMemo } from 'react' -import { Box } from '@mui/material' -import type { TransactionListPage } from '@safe-global/safe-gateway-typescript-sdk' import TxListItem from '../TxListItem' -import GroupedTxListItems from '@/components/transactions/GroupedTxListItems' -import { groupConflictingTxs } from '@/utils/tx-list' import css from './styles.module.css' +import uniq from 'lodash/uniq' +import BulkTxListGroup from '@/components/transactions/BulkTxListGroup' type TxListProps = { items: TransactionListPage['results'] } +const getBulkGroupTxHash = (group: Transaction[]) => { + const hashList = group.map((item) => item.transaction.txHash) + return uniq(hashList).length === 1 ? hashList[0] : undefined +} + export const TxListGrid = ({ children }: { children: ReactNode }): ReactElement => { return {children} } const TxList = ({ items }: TxListProps): ReactElement => { - const groupedItems = useMemo(() => groupConflictingTxs(items), [items]) + const groupedTransactions = useMemo(() => groupTxs(items), [items]) + + const transactions = groupedTransactions.map((item, index) => { + if (!Array.isArray(item)) { + return + } - const transactions = groupedItems.map((item, index) => { - if (Array.isArray(item)) { - return + const bulkTransactionHash = getBulkGroupTxHash(item) + if (bulkTransactionHash) { + return } - return + return }) return {transactions} diff --git a/src/components/transactions/TxListItem/ExpandableTransactionItem.tsx b/src/components/transactions/TxListItem/ExpandableTransactionItem.tsx index 9db293bb7c..f94fb2b358 100644 --- a/src/components/transactions/TxListItem/ExpandableTransactionItem.tsx +++ b/src/components/transactions/TxListItem/ExpandableTransactionItem.tsx @@ -12,13 +12,15 @@ import classNames from 'classnames' import { trackEvent, TX_LIST_EVENTS } from '@/services/analytics' type ExpandableTransactionItemProps = { - isGrouped?: boolean + isConflictGroup?: boolean + isBulkGroup?: boolean item: Transaction txDetails?: TransactionDetails } export const ExpandableTransactionItem = ({ - isGrouped = false, + isConflictGroup = false, + isBulkGroup = false, item, txDetails, testId, @@ -56,7 +58,7 @@ export const ExpandableTransactionItem = ({ }, }} > - + diff --git a/src/components/transactions/TxShareLink/index.tsx b/src/components/transactions/TxShareLink/index.tsx index 4ac7a327cb..10cb9a326c 100644 --- a/src/components/transactions/TxShareLink/index.tsx +++ b/src/components/transactions/TxShareLink/index.tsx @@ -1,5 +1,4 @@ import type { ReactElement } from 'react' -import { useEffect, useState } from 'react' import { IconButton, Link, SvgIcon } from '@mui/material' import ShareIcon from '@/public/images/common/share.svg' import { AppRoutes } from '@/config/routes' @@ -8,17 +7,7 @@ import Track from '@/components/common/Track' import { TX_LIST_EVENTS } from '@/services/analytics/events/txList' import React from 'react' import CopyTooltip from '@/components/common/CopyTooltip' - -const useOrigin = () => { - const [origin, setOrigin] = useState('') - - useEffect(() => { - if (typeof location !== 'undefined') { - setOrigin(location.origin) - } - }, []) - return origin -} +import useOrigin from '@/hooks/useOrigin' const TxShareLink = ({ id }: { id: string }): ReactElement => { const router = useRouter() diff --git a/src/components/transactions/TxStatusLabel/index.tsx b/src/components/transactions/TxStatusLabel/index.tsx index 60122905b8..adb4abbda4 100644 --- a/src/components/transactions/TxStatusLabel/index.tsx +++ b/src/components/transactions/TxStatusLabel/index.tsx @@ -1,11 +1,11 @@ -import { isCancelledSwap } from '@/utils/transaction-guards' +import { isCancelledSwapOrder } from '@/utils/transaction-guards' import { CircularProgress, type Palette, Typography } from '@mui/material' import { TransactionStatus, type TransactionSummary } from '@safe-global/safe-gateway-typescript-sdk' import useIsPending from '@/hooks/useIsPending' import useTransactionStatus from '@/hooks/useTransactionStatus' const getStatusColor = (tx: TransactionSummary, palette: Palette) => { - if (isCancelledSwap(tx.txInfo)) { + if (isCancelledSwapOrder(tx.txInfo)) { return palette.error.main } diff --git a/src/components/transactions/TxSummary/index.test.tsx b/src/components/transactions/TxSummary/index.test.tsx index f14dfa919a..84a1a719ef 100644 --- a/src/components/transactions/TxSummary/index.test.tsx +++ b/src/components/transactions/TxSummary/index.test.tsx @@ -54,64 +54,72 @@ const mockTransactionInHistory = { describe('TxSummary', () => { it('should display a nonce if transaction is not grouped', () => { - const { getByText } = render() + const { getByText } = render() expect(getByText('7')).toBeInTheDocument() }) it('should not display a nonce if transaction is grouped', () => { - const { queryByText } = render() + const { queryByText } = render() expect(queryByText('7')).not.toBeInTheDocument() }) it('should not display a nonce if there is no executionInfo', () => { - const { queryByText } = render() + const { queryByText } = render() + + expect(queryByText('7')).not.toBeInTheDocument() + }) + + it('should not display a nonce for items in bulk execution group', () => { + const { queryByText } = render( + , + ) expect(queryByText('7')).not.toBeInTheDocument() }) it('should display confirmations if transactions is in queue', () => { - const { getByText } = render() + const { getByText } = render() expect(getByText('1 out of 3')).toBeInTheDocument() }) it('should not display confirmations if transactions is already executed', () => { - const { queryByText } = render() + const { queryByText } = render() expect(queryByText('1 out of 3')).not.toBeInTheDocument() }) it('should not display confirmations if there is no executionInfo', () => { - const { queryByText } = render() + const { queryByText } = render() expect(queryByText('1 out of 3')).not.toBeInTheDocument() }) it('should display a Sign button if confirmations are missing', () => { - const { getByText } = render() + const { getByText } = render() expect(getByText('Confirm')).toBeInTheDocument() }) it('should display a status label if transaction is in queue and pending', () => { jest.spyOn(pending, 'default').mockReturnValue(true) - const { getByTestId } = render() + const { getByTestId } = render() expect(getByTestId('tx-status-label')).toBeInTheDocument() }) it('should display a status label if transaction is not in queue', () => { jest.spyOn(pending, 'default').mockReturnValue(true) - const { getByTestId } = render() + const { getByTestId } = render() expect(getByTestId('tx-status-label')).toBeInTheDocument() }) it('should not display a status label if transaction is in queue and not pending', () => { jest.spyOn(pending, 'default').mockReturnValue(false) - const { queryByTestId } = render() + const { queryByTestId } = render() expect(queryByTestId('tx-status-label')).not.toBeInTheDocument() }) diff --git a/src/components/transactions/TxSummary/index.tsx b/src/components/transactions/TxSummary/index.tsx index 72bd3f6fa6..3763d60fb8 100644 --- a/src/components/transactions/TxSummary/index.tsx +++ b/src/components/transactions/TxSummary/index.tsx @@ -10,8 +10,8 @@ import TxInfo from '@/components/transactions/TxInfo' import { isMultisigExecutionInfo, isTxQueued } from '@/utils/transaction-guards' import TxType from '@/components/transactions/TxType' import classNames from 'classnames' -import { isTrustedTx } from '@/utils/transactions' -import UntrustedTxWarning from '../UntrustedTxWarning' +import { isImitation, isTrustedTx } from '@/utils/transactions' +import MaliciousTxWarning from '../MaliciousTxWarning' import QueueActions from './QueueActions' import useIsPending from '@/hooks/useIsPending' import TxConfirmations from '../TxConfirmations' @@ -20,17 +20,19 @@ import { FEATURES } from '@/utils/chains' import TxStatusLabel from '@/components/transactions/TxStatusLabel' type TxSummaryProps = { - isGrouped?: boolean + isConflictGroup?: boolean + isBulkGroup?: boolean item: Transaction } -const TxSummary = ({ item, isGrouped }: TxSummaryProps): ReactElement => { +const TxSummary = ({ item, isConflictGroup, isBulkGroup }: TxSummaryProps): ReactElement => { const hasDefaultTokenlist = useHasFeature(FEATURES.DEFAULT_TOKENLIST) const tx = item.transaction const isQueue = isTxQueued(tx.txStatus) const nonce = isMultisigExecutionInfo(tx.executionInfo) ? tx.executionInfo.nonce : undefined const isTrusted = !hasDefaultTokenlist || isTrustedTx(tx) + const isImitationTransaction = isImitation(tx) const isPending = useIsPending(tx.id) const executionInfo = isMultisigExecutionInfo(tx.executionInfo) ? tx.executionInfo : undefined const expiredSwap = useIsExpiredSwap(tx.txInfo) @@ -40,20 +42,21 @@ const TxSummary = ({ item, isGrouped }: TxSummaryProps): ReactElement => { data-testid="transaction-item" className={classNames(css.gridContainer, { [css.history]: !isQueue, - [css.grouped]: isGrouped, - [css.untrusted]: !isTrusted, + [css.conflictGroup]: isConflictGroup, + [css.bulkGroup]: isBulkGroup, + [css.untrusted]: !isTrusted || isImitationTransaction, })} id={tx.id} > - {nonce !== undefined && !isGrouped && ( + {nonce !== undefined && !isConflictGroup && !isBulkGroup && ( {nonce} )} - {!isTrusted && ( + {(isImitationTransaction || !isTrusted) && ( - + )} @@ -91,7 +94,7 @@ const TxSummary = ({ item, isGrouped }: TxSummaryProps): ReactElement => { )} {isQueue && !expiredSwap && ( - + )} diff --git a/src/components/transactions/TxSummary/styles.module.css b/src/components/transactions/TxSummary/styles.module.css index ef1bfa6ce8..fc8ed69fac 100644 --- a/src/components/transactions/TxSummary/styles.module.css +++ b/src/components/transactions/TxSummary/styles.module.css @@ -16,7 +16,7 @@ grid-template-columns: var(--grid-nonce) var(--grid-type) var(--grid-info) var(--grid-date) var(--grid-confirmations) var(--grid-status) var(--grid-actions); - grid-template-areas: 'nonce type info date confirmations status actions'; + grid-template-areas: 'nonce type info date confirmations status actions'; } .gridContainer > * { @@ -28,14 +28,29 @@ grid-template-areas: 'nonce type info date status'; } -.gridContainer.grouped { +.gridContainer.conflictGroup { grid-template-columns: var(--grid-type) var(--grid-info) var(--grid-date) var(--grid-confirmations) var(--grid-status) var( --grid-actions ); grid-template-areas: 'type info date confirmations status actions'; } -.gridContainer.untrusted { +.gridContainer.bulkGroup { + grid-template-columns: var(--grid-type) var(--grid-info) var(--grid-date) var(--grid-status); + grid-template-areas: 'type info date status'; +} + +.gridContainer.bulkGroup.untrusted { + grid-template-columns: var(--grid-nonce) minmax(200px, 2.4fr) var(--grid-info) var(--grid-date) var(--grid-status); + grid-template-areas: 'nonce type info date status'; +} + +.gridContainer.message { + grid-template-columns: var(--grid-type) var(--grid-info) var(--grid-date) var(--grid-status) var(--grid-confirmations); + grid-template-areas: 'type info date status confirmations'; +} + +.gridContainer.untrusted :not(:first-child):is(div) { opacity: 0.4; } diff --git a/src/components/tx-flow/common/TxButton.tsx b/src/components/tx-flow/common/TxButton.tsx index 214f796660..2288f08571 100644 --- a/src/components/tx-flow/common/TxButton.tsx +++ b/src/components/tx-flow/common/TxButton.tsx @@ -8,6 +8,11 @@ import Track from '@/components/common/Track' import { MODALS_EVENTS } from '@/services/analytics' import { useContext } from 'react' import { TxModalContext } from '..' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' +import SwapIcon from '@/public/images/common/swap.svg' +import AssetsIcon from '@/public/images/sidebar/assets.svg' +import useIsSwapFeatureEnabled from '@/features/swap/hooks/useIsSwapFeatureEnabled' const buttonSx = { height: '58px', @@ -17,7 +22,14 @@ const buttonSx = { export const SendTokensButton = ({ onClick, sx }: { onClick: () => void; sx?: ButtonProps['sx'] }) => { return ( - @@ -27,6 +39,9 @@ export const SendTokensButton = ({ onClick, sx }: { onClick: () => void; sx?: Bu export const SendNFTsButton = () => { const router = useRouter() const { setTxFlow } = useContext(TxModalContext) + const isEnabled = useHasFeature(FEATURES.ERC721) + + if (!isEnabled) return null const isNftPage = router.pathname === AppRoutes.balances.nfts const onClick = isNftPage ? () => setTxFlow(undefined) : undefined @@ -55,10 +70,36 @@ export const TxBuilderButton = () => { return ( - ) } + +export const MakeASwapButton = () => { + const router = useRouter() + const { setTxFlow } = useContext(TxModalContext) + const isSwapFeatureEnabled = useIsSwapFeatureEnabled() + if (!isSwapFeatureEnabled) return null + + const isSwapPage = router.pathname === AppRoutes.swap + const onClick = isSwapPage ? () => setTxFlow(undefined) : undefined + + return ( + + + + + + ) +} diff --git a/src/components/tx-flow/common/TxLayout/index.tsx b/src/components/tx-flow/common/TxLayout/index.tsx index 872f5e6a9c..de243b6d85 100644 --- a/src/components/tx-flow/common/TxLayout/index.tsx +++ b/src/components/tx-flow/common/TxLayout/index.tsx @@ -1,6 +1,7 @@ import useSafeInfo from '@/hooks/useSafeInfo' import { type ComponentType, type ReactElement, type ReactNode, useContext, useEffect, useState } from 'react' import { Box, Container, Grid, Typography, Button, Paper, SvgIcon, IconButton, useMediaQuery } from '@mui/material' +import ArrowBackIcon from '@mui/icons-material/ArrowBack' import { useTheme } from '@mui/material/styles' import type { TransactionSummary } from '@safe-global/safe-gateway-typescript-sdk' import classnames from 'classnames' @@ -146,9 +147,10 @@ const TxLayout = ({ {onBack && step > 0 && ( diff --git a/src/components/tx-flow/common/constants.ts b/src/components/tx-flow/common/constants.ts index ecd6b7a52f..30634cb8ca 100644 --- a/src/components/tx-flow/common/constants.ts +++ b/src/components/tx-flow/common/constants.ts @@ -2,6 +2,6 @@ export const TOOLTIP_TITLES = { THRESHOLD: 'The threshold of a Safe Account specifies how many signers need to confirm a Safe Account transaction before it can be executed.', REVIEW_WINDOW: - 'A period that begins after a recovery submitted on-chain, during which the Safe Account signers can review the proposal and cancel it before it is executable.', + 'A period that begins after a recovery is submitted on-chain, during which the Safe Account signers can review the proposal and cancel it before it is executable.', PROPOSAL_EXPIRY: 'A period after which the recovery proposal will expire and can no longer be executed.', } as const diff --git a/src/components/tx-flow/flows/ConfirmTx/index.tsx b/src/components/tx-flow/flows/ConfirmTx/index.tsx index 00cbf2cfe2..47c91cc764 100644 --- a/src/components/tx-flow/flows/ConfirmTx/index.tsx +++ b/src/components/tx-flow/flows/ConfirmTx/index.tsx @@ -1,24 +1,18 @@ -import { isSwapTxInfo } from '@/utils/transaction-guards' +import { isSwapOrderTxInfo } from '@/utils/transaction-guards' import type { TransactionSummary } from '@safe-global/safe-gateway-typescript-sdk' import TxLayout from '@/components/tx-flow/common/TxLayout' import ConfirmProposedTx from './ConfirmProposedTx' import { useTransactionType } from '@/hooks/useTransactionType' -import TxInfo from '@/components/transactions/TxInfo' import SwapIcon from '@/public/images/common/swap.svg' const ConfirmTxFlow = ({ txSummary }: { txSummary: TransactionSummary }) => { const { text } = useTransactionType(txSummary) - const isSwapOrder = isSwapTxInfo(txSummary.txInfo) + const isSwapOrder = isSwapOrderTxInfo(txSummary.txInfo) return ( - {text}  - {!isSwapOrder && } - - } + subtitle={<>{text} } icon={isSwapOrder && SwapIcon} step={0} txSummary={txSummary} diff --git a/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx b/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx index fb47366b97..5c3d9bb5fb 100644 --- a/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx +++ b/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx @@ -1,6 +1,8 @@ +import useWallet from '@/hooks/wallets/useWallet' +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import { CircularProgress, Typography, Button, CardActions, Divider, Alert } from '@mui/material' import useAsync from '@/hooks/useAsync' -import { FEATURES } from '@safe-global/safe-gateway-typescript-sdk' +import { FEATURES } from '@/utils/chains' import type { TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' import { getReadOnlyMultiSendCallOnlyContract } from '@/services/contracts/safeContracts' import { useCurrentChain } from '@/hooks/useChains' @@ -60,6 +62,7 @@ export const ReviewBatch = ({ params }: { params: ExecuteBatchFlowProps }) => { const canRelay = hasRemainingRelays(relays) const willRelay = canRelay && executionMethod === ExecutionMethod.RELAY const onboard = useOnboard() + const wallet = useWallet() const [txsWithDetails, error, loading] = useAsync(() => { if (!chain?.chainId) return @@ -87,7 +90,8 @@ export const ReviewBatch = ({ params }: { params: ExecuteBatchFlowProps }) => { }, [txsWithDetails, multiSendTxs]) const onExecute = async () => { - if (!userNonce || !onboard || !multiSendTxData || !multiSendContract || !txsWithDetails || !gasPrice) return + if (!userNonce || !onboard || !wallet || !multiSendTxData || !multiSendContract || !txsWithDetails || !gasPrice) + return const overrides: Overrides = isEIP1559 ? { maxFeePerGas: maxFeePerGas?.toString(), maxPriorityFeePerGas: maxPriorityFeePerGas?.toString() } @@ -95,12 +99,14 @@ export const ReviewBatch = ({ params }: { params: ExecuteBatchFlowProps }) => { overrides.nonce = userNonce + await assertWalletChain(onboard, safe.chainId) + await dispatchBatchExecution( txsWithDetails, multiSendContract, multiSendTxData, - onboard, - safe.chainId, + wallet.provider, + wallet.address, safe.address.value, overrides as Overrides & { nonce: number }, ) @@ -141,7 +147,7 @@ export const ReviewBatch = ({ params }: { params: ExecuteBatchFlowProps }) => { 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/components/tx-flow/flows/NewTx/index.tsx b/src/components/tx-flow/flows/NewTx/index.tsx index c50d0f77b2..b480243925 100644 --- a/src/components/tx-flow/flows/NewTx/index.tsx +++ b/src/components/tx-flow/flows/NewTx/index.tsx @@ -1,9 +1,8 @@ import { useCallback, useContext } from 'react' -import { SendNFTsButton, SendTokensButton, TxBuilderButton } from '@/components/tx-flow/common/TxButton' -import { Container, Grid, Paper, SvgIcon, Typography } from '@mui/material' +import { MakeASwapButton, SendTokensButton, TxBuilderButton } from '@/components/tx-flow/common/TxButton' +import { Container, Grid, Paper, Typography } from '@mui/material' import { TxModalContext } from '../../' import TokenTransferFlow from '../TokenTransfer' -import AssetsIcon from '@/public/images/sidebar/assets.svg' import { useTxBuilderApp } from '@/hooks/safe-apps/useTxBuilderApp' import { ProgressBar } from '@/components/common/ProgressBar' import ChainIndicator from '@/components/common/ChainIndicator' @@ -44,19 +43,16 @@ const NewTxFlow = () => { - - Assets + Manage assets - - + {txBuilder?.app && ( <> - {txBuilder.app.name} Contract - interaction + Interact with contracts diff --git a/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowReview.tsx b/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowReview.tsx index 4d78409375..1926c32cc8 100644 --- a/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowReview.tsx +++ b/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowReview.tsx @@ -1,5 +1,6 @@ import { trackEvent } from '@/services/analytics' import { RECOVERY_EVENTS } from '@/services/analytics/events/recovery' +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import { CardActions, Button, Typography, Divider, Box, CircularProgress } from '@mui/material' import { useContext, useEffect, useState } from 'react' import type { ReactElement } from 'react' @@ -70,7 +71,7 @@ export function RecoverAccountFlowReview({ params }: { params: RecoverAccountFlo // On modal submit const onSubmit = async () => { - if (!recovery || !onboard || !safeTx) { + if (!recovery || !onboard || !wallet || !safeTx) { return } @@ -79,11 +80,14 @@ export function RecoverAccountFlowReview({ params }: { params: RecoverAccountFlo setIsRejectedByUser(false) try { + await assertWalletChain(onboard, safe.chainId) + await dispatchRecoveryProposal({ - onboard, + provider: wallet.provider, safe, safeTx, delayModifierAddress: recovery.address, + signerAddress: wallet.address, }) trackEvent({ ...RECOVERY_EVENTS.SUBMIT_RECOVERY_ATTEMPT }) } catch (_err) { diff --git a/src/components/tx-flow/flows/ReplaceTx/DeleteTxModal.tsx b/src/components/tx-flow/flows/ReplaceTx/DeleteTxModal.tsx index 9800ac634b..0bfa985221 100644 --- a/src/components/tx-flow/flows/ReplaceTx/DeleteTxModal.tsx +++ b/src/components/tx-flow/flows/ReplaceTx/DeleteTxModal.tsx @@ -1,3 +1,4 @@ +import useWallet from '@/hooks/wallets/useWallet' import { useState } from 'react' import { Dialog, @@ -14,7 +15,6 @@ import { } from '@mui/material' import { Close } from '@mui/icons-material' import madProps from '@/utils/mad-props' -import useOnboard from '@/hooks/wallets/useOnboard' import useChainId from '@/hooks/useChainId' import useSafeAddress from '@/hooks/useSafeAddress' import { deleteTx } from '@/utils/gateway' @@ -32,12 +32,12 @@ type DeleteTxModalProps = { safeTxHash: string onClose: () => void onSuccess: () => void - onboard: ReturnType + wallet: ReturnType chainId: ReturnType safeAddress: ReturnType } -const _DeleteTxModal = ({ safeTxHash, onSuccess, onClose, onboard, safeAddress, chainId }: DeleteTxModalProps) => { +const _DeleteTxModal = ({ safeTxHash, onSuccess, onClose, wallet, safeAddress, chainId }: DeleteTxModalProps) => { const [error, setError] = useState() const [isLoading, setIsLoading] = useState(false) @@ -46,7 +46,7 @@ const _DeleteTxModal = ({ safeTxHash, onSuccess, onClose, onboard, safeAddress, setIsLoading(true) trackEvent(REJECT_TX_EVENTS.DELETE_CONFIRM) - if (!onboard || !safeAddress || !chainId || !safeTxHash) { + if (!wallet?.provider || !safeAddress || !chainId || !safeTxHash) { setIsLoading(false) setError(new Error('Please connect your wallet first')) trackEvent(REJECT_TX_EVENTS.DELETE_FAIL) @@ -54,7 +54,7 @@ const _DeleteTxModal = ({ safeTxHash, onSuccess, onClose, onboard, safeAddress, } try { - const signer = await getAssertedChainSigner(onboard, chainId) + const signer = await getAssertedChainSigner(wallet.provider) await deleteTx({ safeTxHash, @@ -130,6 +130,7 @@ const _DeleteTxModal = ({ safeTxHash, onSuccess, onClose, onboard, safeAddress, @@ -138,7 +118,4 @@ export const CreateTokenTransfer = ({ ) } -export default madProps(CreateTokenTransfer, { - safeTokenAddress: useSafeTokenAddress, - isSafeTokenPaused: useIsSafeTokenPaused, -}) +export default CreateTokenTransfer diff --git a/src/components/tx-flow/flows/TokenTransfer/ReviewSpendingLimitTx.tsx b/src/components/tx-flow/flows/TokenTransfer/ReviewSpendingLimitTx.tsx index b78d655289..86318c1361 100644 --- a/src/components/tx-flow/flows/TokenTransfer/ReviewSpendingLimitTx.tsx +++ b/src/components/tx-flow/flows/TokenTransfer/ReviewSpendingLimitTx.tsx @@ -1,3 +1,5 @@ +import useWallet from '@/hooks/wallets/useWallet' +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import type { ReactElement, SyntheticEvent } from 'react' import { useContext, useMemo, useState } from 'react' import { type BigNumberish, type BytesLike, parseUnits } from 'ethers' @@ -51,6 +53,7 @@ const ReviewSpendingLimitTx = ({ const { setTxFlow } = useContext(TxModalContext) const currentChain = useCurrentChain() const onboard = useOnboard() + const wallet = useWallet() const { safe, safeAddress } = useSafeInfo() const { balances } = useBalances() const token = balances.items.find((item) => item.tokenInfo.address === params.tokenAddress) @@ -83,7 +86,7 @@ const ReviewSpendingLimitTx = ({ const handleSubmit = async (e: SyntheticEvent) => { e.preventDefault() - if (!onboard) return + if (!onboard || !wallet) return trackEvent(MODALS_EVENTS.USE_SPENDING_LIMIT) @@ -94,7 +97,8 @@ const ReviewSpendingLimitTx = ({ const txOptions = getTxOptions(advancedParams, currentChain) try { - await dispatchSpendingLimitTxExecution(txParams, txOptions, onboard, safe.chainId, safeAddress) + await assertWalletChain(onboard, safe.chainId) + await dispatchSpendingLimitTxExecution(txParams, txOptions, wallet.provider, safe.chainId, safeAddress) onSubmit('', true) setTxFlow(undefined) } catch (_err) { diff --git a/src/components/tx-flow/flows/TokenTransfer/__tests__/CreateTokenTransfer.test.tsx b/src/components/tx-flow/flows/TokenTransfer/__tests__/CreateTokenTransfer.test.tsx index 01c52de3bd..aa5bf407ec 100644 --- a/src/components/tx-flow/flows/TokenTransfer/__tests__/CreateTokenTransfer.test.tsx +++ b/src/components/tx-flow/flows/TokenTransfer/__tests__/CreateTokenTransfer.test.tsx @@ -17,69 +17,29 @@ describe('CreateTokenTransfer', () => { }) it('should display a token amount input', () => { - const { getByText } = render( - , - ) + const { getByText } = render() expect(getByText('Amount')).toBeInTheDocument() }) it('should display a recipient input', () => { - const { getAllByText } = render( - , - ) + const { getAllByText } = render() expect(getAllByText('Recipient address')[0]).toBeInTheDocument() }) - it('should disable the submit button and display a warning if $SAFE token is selected and not transferable', () => { - const { getByText } = render( - , - ) - - const button = getByText('Next') - - expect(getByText('$SAFE is currently non-transferable.')).toBeInTheDocument() - expect(button).toBeDisabled() - }) - - it('should enable the submit button if $SAFE token is selected and transferable', () => { - const { queryByText, getByText } = render( - , - ) - - const button = getByText('Next') - - expect(queryByText('$SAFE is currently non-transferable.')).not.toBeInTheDocument() - expect(button).not.toBeDisabled() - }) - it('should display a type selection if a spending limit token is selected', () => { jest .spyOn(tokenUtils, 'useTokenAmount') .mockReturnValue({ totalAmount: BigInt(1000), spendingLimitAmount: BigInt(500) }) - const { getByText } = render( - , - ) + const { getByText } = render() expect(getByText('Send as')).toBeInTheDocument() }) it('should not display a type selection if there is a txNonce', () => { - const { queryByText } = render( - , - ) + const { queryByText } = render() expect(queryByText('Send as')).not.toBeInTheDocument() }) diff --git a/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowReview.tsx b/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowReview.tsx index 5f498ea19d..169dcfa17e 100644 --- a/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowReview.tsx +++ b/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowReview.tsx @@ -20,6 +20,7 @@ import { UpsertRecoveryFlowFields } from '.' import { TOOLTIP_TITLES } from '../../common/constants' import { useRecoveryPeriods } from './useRecoveryPeriods' import type { UpsertRecoveryFlowProps } from '.' +import { isCustomDelaySelected } from './utils' enum AddressType { EOA = 'EOA', @@ -37,7 +38,11 @@ const getAddressType = async (address: string, chainId: string) => { return AddressType.Other } -const onSubmit = async (isEdit: boolean, params: UpsertRecoveryFlowProps, chainId: string) => { +const onSubmit = async ( + isEdit: boolean, + params: Omit, + chainId: string, +) => { const addressType = await getAddressType(params.recoverer, chainId) const creationEvent = isEdit ? RECOVERY_EVENTS.SUBMIT_RECOVERY_EDIT : RECOVERY_EVENTS.SUBMIT_RECOVERY_CREATE const settings = `delay_${params.delay},expiry_${params.expiry},type_${addressType}` @@ -56,11 +61,15 @@ export function UpsertRecoveryFlowReview({ const web3ReadOnly = useWeb3ReadOnly() const { safe, safeAddress } = useSafeInfo() const { setSafeTx, safeTxError, setSafeTxError } = useContext(SafeTxContext) - const periods = useRecoveryPeriods() - const recoverer = params[UpsertRecoveryFlowFields.recoverer] - const delay = periods.delay.find(({ value }) => value === params[UpsertRecoveryFlowFields.delay])!.label - const expiry = periods.expiration.find(({ value }) => value === params[UpsertRecoveryFlowFields.expiry])!.label + + const { recoverer, expiry, delay, customDelay, selectedDelay } = params + const isCustomDelay = isCustomDelaySelected(selectedDelay) + + const expiryLabel = periods.expiration.find(({ value }) => value === params[UpsertRecoveryFlowFields.expiry])!.label + const delayLabel = isCustomDelay + ? `${customDelay} days` + : periods.delay.find(({ value }) => value === selectedDelay)?.label useEffect(() => { if (!web3ReadOnly) { @@ -90,7 +99,7 @@ export function UpsertRecoveryFlowReview({ const isEdit = !!moduleAddress return ( - onSubmit(isEdit, params, safe.chainId)}> + onSubmit(isEdit, { recoverer, expiry, delay }, safe.chainId)}> This transaction will {moduleAddress ? 'update' : 'enable'} the Account recovery feature once executed. @@ -117,10 +126,10 @@ export function UpsertRecoveryFlowReview({ } > - {delay} + {delayLabel} - {expiry !== '0' && ( + {expiryLabel !== '0' && ( @@ -139,7 +148,7 @@ export function UpsertRecoveryFlowReview({ } > - {expiry} + {expiryLabel} )} diff --git a/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowSettings.tsx b/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowSettings.tsx index 4a991318c7..0aced4627b 100644 --- a/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowSettings.tsx +++ b/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowSettings.tsx @@ -13,17 +13,17 @@ import { FormControlLabel, Tooltip, Alert, + Box, } from '@mui/material' import ExpandLessIcon from '@mui/icons-material/ExpandLess' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import { useForm, FormProvider, Controller } from 'react-hook-form' import { useState } from 'react' -import type { TextFieldProps } from '@mui/material' import type { ReactElement } from 'react' import TxCard from '../../common/TxCard' -import { UpsertRecoveryFlowFields } from '.' import { useRecoveryPeriods } from './useRecoveryPeriods' +import { UpsertRecoveryFlowFields, type UpsertRecoveryFlowProps } from '.' import AddressBookInput from '@/components/common/AddressBookInput' import { sameAddress } from '@/utils/addresses' import useSafeInfo from '@/hooks/useSafeInfo' @@ -33,11 +33,12 @@ import ExternalLink from '@/components/common/ExternalLink' import { HelpCenterArticle, HelperCenterArticleTitles } from '@/config/constants' import { TOOLTIP_TITLES } from '../../common/constants' import Track from '@/components/common/Track' -import type { UpsertRecoveryFlowProps } from '.' import type { RecoveryStateItem } from '@/features/recovery/services/recovery-state' import commonCss from '@/components/tx-flow/common/styles.module.css' import css from './styles.module.css' +import NumberField from '@/components/common/NumberField' +import { getDelay, isCustomDelaySelected } from './utils' export function UpsertRecoveryFlowSettings({ params, @@ -59,8 +60,12 @@ export function UpsertRecoveryFlowSettings({ }) const recoverer = formMethods.watch(UpsertRecoveryFlowFields.recoverer) - const delay = formMethods.watch(UpsertRecoveryFlowFields.delay) const expiry = formMethods.watch(UpsertRecoveryFlowFields.expiry) + const selectedDelay = formMethods.watch(UpsertRecoveryFlowFields.selectedDelay) + const customDelay = formMethods.watch(UpsertRecoveryFlowFields.customDelay) + const customDelayState = formMethods.getFieldState(UpsertRecoveryFlowFields.customDelay) + + const delay = getDelay(customDelay, selectedDelay) // RHF's dirty check is tempermental with our address input dropdown const isDirty = delayModifier @@ -77,17 +82,28 @@ export function UpsertRecoveryFlowSettings({ } } + const validateCustomDelay = (delay: string) => { + if (!delay) return '' + if (delay === '0' || !Number.isInteger(Number(delay))) { + return 'Invalid number' + } + } + const onShowAdvanced = () => { setShowAdvanced((prev) => !prev) trackEvent(RECOVERY_EVENTS.SHOW_ADVANCED) } - const isDisabled = !understandsRisk || !isDirty + const isDisabled = !understandsRisk || !isDirty || !!customDelayState.error + + const handleSubmit = () => { + onSubmit({ expiry, delay, customDelay, selectedDelay, recoverer }) + } return ( <> - + Your Recoverer will be able to reset your Account setup. Only select an address that you trust.{' '} @@ -97,7 +113,6 @@ export function UpsertRecoveryFlowSettings({ -
    Trusted Recoverer @@ -108,7 +123,6 @@ export function UpsertRecoveryFlowSettings({ can initiate the recovery process in the future.
    -
    - You can cancel any recovery proposal when it is not needed or wanted during this period. + The recovery proposal will be available for execution after this period of time. You can cancel any + recovery proposal when it is not needed or wanted during this period.
    - - ( - - {periods.delay.map(({ label, value }, index) => ( - - {label} - - ))} - - )} - /> - + + ( + + {periods.delay.map(({ label, value }, index) => ( + + {label} + + ))} + + )} + /> + + {isCustomDelaySelected(selectedDelay) && ( + <> + ( + + )} + /> + days. + + )} + + Advanced {showAdvanced ? : } -
    @@ -192,13 +229,13 @@ export function UpsertRecoveryFlowSettings({ // Don't reset value if advanced section is collapsed shouldUnregister={false} render={({ field: { ref, ...field } }) => ( - + {periods.expiration.map(({ label, value }, index) => ( {label} ))} - + )} /> @@ -225,26 +262,3 @@ export function UpsertRecoveryFlowSettings({ ) } - -function SelectField(props: TextFieldProps) { - return ( - - ) -} diff --git a/src/components/tx-flow/flows/UpsertRecovery/index.tsx b/src/components/tx-flow/flows/UpsertRecovery/index.tsx index 7810b65e7c..48339019a7 100644 --- a/src/components/tx-flow/flows/UpsertRecovery/index.tsx +++ b/src/components/tx-flow/flows/UpsertRecovery/index.tsx @@ -15,12 +15,16 @@ const Subtitles = ['How does recovery work?', 'Set up recovery settings', 'Set u export enum UpsertRecoveryFlowFields { recoverer = 'recoverer', delay = 'delay', + customDelay = 'customDelay', + selectedDelay = 'selectedDelay', expiry = 'expiry', } export type UpsertRecoveryFlowProps = { [UpsertRecoveryFlowFields.recoverer]: string [UpsertRecoveryFlowFields.delay]: string + [UpsertRecoveryFlowFields.customDelay]: string + [UpsertRecoveryFlowFields.selectedDelay]: string [UpsertRecoveryFlowFields.expiry]: string } @@ -28,7 +32,9 @@ function UpsertRecoveryFlow({ delayModifier }: { delayModifier?: RecoveryState[n const { data, step, nextStep, prevStep } = useTxStepper( { [UpsertRecoveryFlowFields.recoverer]: delayModifier?.recoverers?.[0] ?? '', - [UpsertRecoveryFlowFields.delay]: delayModifier?.delay?.toString() ?? `${DAY_IN_SECONDS * 28}`, // 28 days in seconds + [UpsertRecoveryFlowFields.delay]: '', + [UpsertRecoveryFlowFields.selectedDelay]: delayModifier?.delay?.toString() ?? `${DAY_IN_SECONDS * 28}`, // 28 days in seconds + [UpsertRecoveryFlowFields.customDelay]: '', [UpsertRecoveryFlowFields.expiry]: delayModifier?.expiry?.toString() ?? '0', }, SETUP_RECOVERY_CATEGORY, diff --git a/src/components/tx-flow/flows/UpsertRecovery/useRecoveryPeriods.ts b/src/components/tx-flow/flows/UpsertRecovery/useRecoveryPeriods.ts index b84850c6a1..dd7fcc026f 100644 --- a/src/components/tx-flow/flows/UpsertRecovery/useRecoveryPeriods.ts +++ b/src/components/tx-flow/flows/UpsertRecovery/useRecoveryPeriods.ts @@ -5,7 +5,7 @@ export const DAY_IN_SECONDS = 60 * 60 * 24 type Periods = Array<{ label: string; value: string | number }> -const DefaultRecoveryDelayPeriods: Periods = [ +const ExpirationPeriods: Periods = [ { label: '2 days', value: `${DAY_IN_SECONDS * 2}`, @@ -28,15 +28,7 @@ const DefaultRecoveryDelayPeriods: Periods = [ }, ] -const DefaultRecoveryExpirationPeriods: Periods = [ - { - label: 'Never', - value: '0', - }, - ...DefaultRecoveryDelayPeriods, -] - -const TestRecoveryDelayPeriods: Periods = [ +const TestPeriods: Periods = [ { label: '1 minute', value: '60', @@ -49,7 +41,31 @@ const TestRecoveryDelayPeriods: Periods = [ label: '1 hour', value: `${60 * 60}`, }, - ...DefaultRecoveryDelayPeriods, +] + +const DefaultRecoveryDelayPeriods: Periods = [ + { + label: 'Custom period', + value: '0', + }, + ...ExpirationPeriods, +] + +const DefaultRecoveryExpirationPeriods: Periods = [ + { + label: 'Never', + value: '0', + }, + ...ExpirationPeriods, +] + +const TestRecoveryDelayPeriods: Periods = [ + { + label: 'Custom period', + value: '0', + }, + ...TestPeriods, + ...ExpirationPeriods, ] const TestRecoveryExpirationPeriods: Periods = [ @@ -57,7 +73,8 @@ const TestRecoveryExpirationPeriods: Periods = [ label: 'Never', value: '0', }, - ...TestRecoveryDelayPeriods, + ...TestPeriods, + ...ExpirationPeriods, ] export function useRecoveryPeriods(): { delay: Periods; expiration: Periods } { diff --git a/src/components/tx-flow/flows/UpsertRecovery/utils.ts b/src/components/tx-flow/flows/UpsertRecovery/utils.ts new file mode 100644 index 0000000000..6630da51fe --- /dev/null +++ b/src/components/tx-flow/flows/UpsertRecovery/utils.ts @@ -0,0 +1,11 @@ +import { DAY_IN_SECONDS } from './useRecoveryPeriods' + +export const isCustomDelaySelected = (selectedDelay: string) => { + return !Number(selectedDelay) +} + +export const getDelay = (customDelay: string, selectedDelay: string) => { + const isCustom = isCustomDelaySelected(selectedDelay) + if (!isCustom) return selectedDelay + return customDelay ? `${Number(customDelay) * DAY_IN_SECONDS}` : '' +} diff --git a/src/components/tx-flow/index.tsx b/src/components/tx-flow/index.tsx index 46b98a6d60..5927f3796e 100644 --- a/src/components/tx-flow/index.tsx +++ b/src/components/tx-flow/index.tsx @@ -32,7 +32,7 @@ export const TxModalProvider = ({ children }: { children: ReactNode }): ReactEle const safeId = useChainId() + useSafeAddress() const prevSafeId = useRef(safeId ?? '') const pathname = usePathname() - const prevPathname = useRef(pathname) + const prevPathname = useRef(pathname) const handleModalClose = useCallback(() => { if (shouldWarn.current && !confirmClose()) { diff --git a/src/components/tx/ApprovalEditor/ApprovalEditorForm.tsx b/src/components/tx/ApprovalEditor/ApprovalEditorForm.tsx index 45a8e1b98f..3db81b91d2 100644 --- a/src/components/tx/ApprovalEditor/ApprovalEditorForm.tsx +++ b/src/components/tx/ApprovalEditor/ApprovalEditorForm.tsx @@ -46,7 +46,6 @@ export const ApprovalEditorForm = ({ {Object.entries(groupedApprovals).map(([spender, approvals], spenderIdx) => ( - {approvals.map((tx) => ( ))} + + {spenderIdx !== Object.keys(groupedApprovals).length - 1 && } diff --git a/src/components/tx/ApprovalEditor/EditableApprovalItem.tsx b/src/components/tx/ApprovalEditor/EditableApprovalItem.tsx index 8a9c27995e..8208fcc1a3 100644 --- a/src/components/tx/ApprovalEditor/EditableApprovalItem.tsx +++ b/src/components/tx/ApprovalEditor/EditableApprovalItem.tsx @@ -41,12 +41,20 @@ const EditableApprovalItem = ({ } return ( - + + - + + {readOnly ? ( diff --git a/src/components/tx/ApprovalEditor/index.tsx b/src/components/tx/ApprovalEditor/index.tsx index 719341d5da..3f1a3bcf07 100644 --- a/src/components/tx/ApprovalEditor/index.tsx +++ b/src/components/tx/ApprovalEditor/index.tsx @@ -54,7 +54,7 @@ export const ApprovalEditor = ({ const isReadOnly = (safeTransaction && safeTransaction.signatures.size > 0) || safeMessage !== undefined return ( - + {error ? ( <Alert severity="error">Error while decoding approval transactions.</Alert> diff --git a/src/components/tx/DecodedTx/index.tsx b/src/components/tx/DecodedTx/index.tsx index f8b03f895e..e31efca392 100644 --- a/src/components/tx/DecodedTx/index.tsx +++ b/src/components/tx/DecodedTx/index.tsx @@ -1,8 +1,6 @@ -import { getInteractionTitle } from '@/components/safe-apps/utils' import SendToBlock from '@/components/tx/SendToBlock' -import SwapOrderConfirmationView from '@/features/swap/components/SwapOrderConfirmationView' import { useCurrentChain } from '@/hooks/useChains' -import { isSwapConfirmationViewOrder } from '@/utils/transaction-guards' +import { isConfirmationViewOrder } from '@/utils/transaction-guards' import { type SyntheticEvent, type ReactElement, memo } from 'react' import { Accordion, @@ -10,13 +8,19 @@ import { AccordionSummary, Box, Skeleton, + Stack, SvgIcon, Tooltip, Typography, } from '@mui/material' import { OperationType, type SafeTransaction } from '@safe-global/safe-core-sdk-types' import type { DecodedDataResponse } from '@safe-global/safe-gateway-typescript-sdk' -import { getTransactionDetails, type TransactionDetails, Operation } from '@safe-global/safe-gateway-typescript-sdk' +import { + getTransactionDetails, + type TransactionDetails, + Operation, + TokenType, +} from '@safe-global/safe-gateway-typescript-sdk' import useChainId from '@/hooks/useChainId' import useAsync from '@/hooks/useAsync' import { MethodDetails } from '@/components/transactions/TxDetails/TxData/DecodedData/MethodDetails' @@ -29,6 +33,9 @@ import ExternalLink from '@/components/common/ExternalLink' import { HelpCenterArticle } from '@/config/constants' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import accordionCss from '@/styles/accordion.module.css' +import { formatVisualAmount } from '@/utils/formatters' +import SendAmountBlock from '@/components/tx-flow/flows/TokenTransfer/SendAmountBlock' +import { ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants' type DecodedTxProps = { tx?: SafeTransaction @@ -51,7 +58,7 @@ const DecodedTx = ({ }: DecodedTxProps): ReactElement | null => { const chainId = useChainId() const chain = useCurrentChain() - const isSwapOrder = isSwapConfirmationViewOrder(decodedData) + const isSwapOrder = isConfirmationViewOrder(decodedData) const isMultisend = !!decodedData?.parameters?.[0]?.valueDecoded @@ -68,16 +75,30 @@ const DecodedTx = ({ if (!decodedData) return null + const amount = tx?.data.value ? formatVisualAmount(tx.data.value, chain?.nativeCurrency.decimals) : '0' + return ( - <div> + <Stack spacing={2}> {!isSwapOrder && tx && showToBlock && ( - <SendToBlock address={tx.data.to} title={getInteractionTitle(tx.data.value || '', chain)} /> + <> + {amount !== '0' && ( + <SendAmountBlock + amount={amount} + tokenInfo={{ + type: TokenType.NATIVE_TOKEN, + address: ZERO_ADDRESS, + decimals: chain?.nativeCurrency.decimals ?? 18, + symbol: chain?.nativeCurrency.symbol ?? 'ETH', + logoUri: chain?.nativeCurrency.logoUri, + }} + /> + )} + <SendToBlock address={tx.data.to} title="Interact with" name={addressInfoIndex?.[tx.data.to]?.name} /> + </> )} - {isSwapOrder && tx && <SwapOrderConfirmationView order={decodedData} settlementContract={tx.data.to} />} - {isMultisend && showMultisend && ( - <Box my={2}> + <Box> <Multisend txData={{ dataDecoded: decodedData, @@ -92,7 +113,7 @@ const DecodedTx = ({ </Box> )} - <Box mt={2}> + <Box> <Accordion elevation={0} onChange={onChangeExpand} sx={!tx ? { pointerEvents: 'none' } : undefined}> <AccordionSummary data-testid="decoded-tx-summary" @@ -159,7 +180,7 @@ const DecodedTx = ({ </AccordionDetails> </Accordion> </Box> - </div> + </Stack> ) } diff --git a/src/components/tx/FieldsGrid/index.tsx b/src/components/tx/FieldsGrid/index.tsx index d679b815df..375ed85320 100644 --- a/src/components/tx/FieldsGrid/index.tsx +++ b/src/components/tx/FieldsGrid/index.tsx @@ -4,7 +4,7 @@ import { Grid, Typography } from '@mui/material' const FieldsGrid = ({ title, children }: { title: string; children: ReactNode }) => { return ( <Grid container alignItems="center" gap={1}> - <Grid item xs={1} xl={2} minWidth={90}> + <Grid item xs={2} xl={2} minWidth={90}> <Typography variant="body2" color="text.secondary" noWrap> {title} </Typography> diff --git a/src/components/tx/SendToBlock/index.tsx b/src/components/tx/SendToBlock/index.tsx index 8ae3ef4060..af5423f65c 100644 --- a/src/components/tx/SendToBlock/index.tsx +++ b/src/components/tx/SendToBlock/index.tsx @@ -2,11 +2,28 @@ import { Typography } from '@mui/material' import EthHashInfo from '@/components/common/EthHashInfo' import FieldsGrid from '../FieldsGrid' -const SendToBlock = ({ address, title = 'To' }: { address: string; title?: string }) => { +const SendToBlock = ({ + address, + title = 'To', + avatarSize, + name, +}: { + address: string + name?: string + title?: string + avatarSize?: number +}) => { return ( <FieldsGrid title={title}> <Typography variant="body2" component="div"> - <EthHashInfo address={address} shortAddress={false} hasExplorer showCopyButton /> + <EthHashInfo + address={address} + name={name} + shortAddress={false} + hasExplorer + showCopyButton + avatarSize={avatarSize} + /> </Typography> </FieldsGrid> ) diff --git a/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx b/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx index d0112e3a86..9d4d4f862a 100644 --- a/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx +++ b/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx @@ -41,13 +41,11 @@ export const ExecuteForm = ({ isCreation, isOwner, isExecutionLoop, - relays, txActions, txSecurity, }: SignOrExecuteProps & { isOwner: ReturnType<typeof useIsSafeOwner> isExecutionLoop: ReturnType<typeof useIsExecutionLoop> - relays: ReturnType<typeof useRelaysBySafe> txActions: ReturnType<typeof useTxActions> txSecurity: ReturnType<typeof useTxSecurityContext> safeTx?: SafeTransaction @@ -68,7 +66,7 @@ export const ExecuteForm = ({ // SC wallets can relay fully signed transactions const [walletCanRelay] = useWalletCanRelay(safeTx) - + const relays = useRelaysBySafe(origin) // The transaction can/will be relayed const canRelay = walletCanRelay && hasRemainingRelays(relays[0]) const willRelay = canRelay && executionMethod === ExecutionMethod.RELAY @@ -214,7 +212,6 @@ const useTxSecurityContext = () => useContext(TxSecurityContext) export default madProps(ExecuteForm, { isOwner: useIsSafeOwner, isExecutionLoop: useIsExecutionLoop, - relays: useRelaysBySafe, txActions: useTxActions, txSecurity: useTxSecurityContext, }) diff --git a/src/components/tx/SignOrExecuteForm/PermissionsCheck/__test__/PermissionsCheck.test.tsx b/src/components/tx/SignOrExecuteForm/PermissionsCheck/__test__/PermissionsCheck.test.tsx new file mode 100644 index 0000000000..814c6faca9 --- /dev/null +++ b/src/components/tx/SignOrExecuteForm/PermissionsCheck/__test__/PermissionsCheck.test.tsx @@ -0,0 +1,300 @@ +import { createMockSafeTransaction } from '@/tests/transactions' +import { OperationType } from '@safe-global/safe-core-sdk-types' +import { type ReactElement } from 'react' +import * as zodiacRoles from 'zodiac-roles-deployments' +import { fireEvent, render, waitFor, mockWeb3Provider } from '@/tests/test-utils' + +import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' +import * as useSafeInfoHook from '@/hooks/useSafeInfo' +import * as wallet from '@/hooks/wallets/useWallet' +import * as onboardHooks from '@/hooks/wallets/useOnboard' +import * as txSender from '@/services/tx/tx-sender/dispatch' +import { extendedSafeInfoBuilder } from '@/tests/builders/safe' +import { type OnboardAPI } from '@web3-onboard/core' +import { AbiCoder, ZeroAddress, encodeBytes32String } from 'ethers' +import PermissionsCheck from '..' +import * as hooksModule from '../hooks' +import { FEATURES } from '@/utils/chains' +import { chainBuilder } from '@/tests/builders/chains' +import { useHasFeature } from '@/hooks/useChains' + +// We assume that CheckWallet always returns true +jest.mock('@/components/common/CheckWallet', () => ({ + __esModule: true, + default({ children }: { children: (ok: boolean) => ReactElement }) { + return children(true) + }, +})) + +const mockChain = chainBuilder() + // @ts-expect-error - we are using a local FEATURES enum + .with({ features: [FEATURES.ZODIAC_ROLES, FEATURES.EIP1559] }) + .with({ chainId: '1' }) + .with({ shortName: 'eth' }) + .with({ chainName: 'Ethereum' }) + .with({ transactionService: 'https://tx.service.mock' }) + .build() + +// mock useCurrentChain +jest.mock('@/hooks/useChains', () => ({ + __esModule: true, + ...jest.requireActual('@/hooks/useChains'), + useCurrentChain: jest.fn(() => mockChain), + useHasFeature: jest.fn(), +})) + +jest.mock('@/hooks/useChainId', () => ({ + useChainId: jest.fn().mockReturnValue(() => '1'), +})) + +// mock getModuleTransactionId +jest.mock('@/services/transactions', () => ({ + getModuleTransactionId: jest.fn(() => 'i1234567890'), +})) + +describe('PermissionsCheck', () => { + let executeSpy: jest.SpyInstance + let fetchRolesModMock: jest.SpyInstance + + const mockConnectedWalletAddress = (address: string) => { + // Onboard + jest.spyOn(onboardHooks, 'default').mockReturnValue({ + setChain: jest.fn(), + state: { + get: () => ({ + wallets: [ + { + label: 'MetaMask', + accounts: [{ address }], + connected: true, + chains: [{ id: '1' }], + }, + ], + }), + }, + } as unknown as OnboardAPI) + + // Wallet + jest.spyOn(wallet, 'default').mockReturnValue({ + chainId: '1', + label: 'MetaMask', + address, + } as unknown as ConnectedWallet) + } + + beforeEach(() => { + jest.clearAllMocks() + ;(useHasFeature as jest.Mock).mockImplementation((feature) => mockChain.features.includes(feature)), + // Safe info + jest.spyOn(useSafeInfoHook, 'default').mockImplementation(() => ({ + safe: SAFE_INFO, + safeAddress: SAFE_INFO.address.value, + safeError: undefined, + safeLoading: false, + safeLoaded: true, + })) + + // Roles mod fetching + + // Mock the Roles mod fetching function to return the test roles mod + + fetchRolesModMock = jest.spyOn(zodiacRoles, 'fetchRolesMod').mockReturnValue(Promise.resolve(TEST_ROLES_MOD as any)) + + // Mock signing and dispatching the module transaction + executeSpy = jest + .spyOn(txSender, 'dispatchModuleTxExecution') + .mockReturnValue(Promise.resolve('0xabababababababababababababababababababababababababababababababab')) // tx hash + + // Mock return value of useWeb3ReadOnly + // It's only used for eth_estimateGas requests + mockWeb3Provider([]) + + jest.spyOn(hooksModule, 'pollModuleTransactionId').mockReturnValue(Promise.resolve('i1234567890')) + }) + + it('only fetch roles and show the card if the feature is enabled', async () => { + ;(useHasFeature as jest.Mock).mockImplementation((feature) => feature !== FEATURES.ZODIAC_ROLES) + mockConnectedWalletAddress(SAFE_INFO.owners[0].value) // connect as safe owner (not a role member) + + const safeTx = createMockSafeTransaction({ + to: ZeroAddress, + data: '0xd0e30db0', // deposit() + value: AbiCoder.defaultAbiCoder().encode(['uint256'], [123]), + operation: OperationType.Call, + }) + + const { queryByText } = render(<PermissionsCheck safeTx={safeTx} />) + + // the card is not shown + expect(queryByText('Execute without confirmations')).not.toBeInTheDocument() + + expect(fetchRolesModMock).not.toHaveBeenCalled() + }) + + it('only shows the card when the user is a member of any role', async () => { + mockConnectedWalletAddress(SAFE_INFO.owners[0].value) // connect as safe owner (not a role member) + + const safeTx = createMockSafeTransaction({ + to: ZeroAddress, + data: '0xd0e30db0', // deposit() + value: AbiCoder.defaultAbiCoder().encode(['uint256'], [123]), + operation: OperationType.Call, + }) + + const { queryByText } = render(<PermissionsCheck safeTx={safeTx} />) + + // wait for the Roles mod to be fetched + await waitFor(() => { + expect(fetchRolesModMock).toBeCalled() + }) + + // the card is not shown + expect(queryByText('Execute without confirmations')).not.toBeInTheDocument() + }) + + it('disables the submit button when the call is not allowed and shows the permission check status', async () => { + mockConnectedWalletAddress(MEMBER_ADDRESS) + + const safeTx = createMockSafeTransaction({ + to: ZeroAddress, + data: '0xd0e30db0', // deposit() + value: AbiCoder.defaultAbiCoder().encode(['uint256'], [123]), + operation: OperationType.Call, + }) + + const { findByText, getByText } = render(<PermissionsCheck safeTx={safeTx} />) + expect(await findByText('Execute')).toBeDisabled() + + expect( + getByText( + textContentMatcher('You are a member of the eth_wrapping role but it does not allow this transaction.'), + ), + ).toBeInTheDocument() + + expect(getByText('TargetAddressNotAllowed')).toBeInTheDocument() + }) + + it('execute the tx when the submit button is clicked', async () => { + mockConnectedWalletAddress(MEMBER_ADDRESS) + + const safeTx = createMockSafeTransaction({ + to: WETH_ADDRESS, + data: '0xd0e30db0', // deposit() + value: AbiCoder.defaultAbiCoder().encode(['uint256'], [123]), + operation: OperationType.Call, + }) + + const onSubmit = jest.fn() + + const { findByText } = render(<PermissionsCheck safeTx={safeTx} onSubmit={onSubmit} />) + + fireEvent.click(await findByText('Execute')) + + await waitFor(() => { + expect(executeSpy).toHaveBeenCalledWith( + // call to the Roles mod's execTransactionWithRole function + expect.objectContaining({ + to: TEST_ROLES_MOD.address, + data: '0xc6fe8747000000000000000000000000fff9976782d46cc05630d1f6ebab18b2324d6b14000000000000000000000000000000000000000000000000000000000000007b00000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000006574685f7772617070696e67000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000004d0e30db000000000000000000000000000000000000000000000000000000000', + value: '0', + }), + undefined, + expect.anything(), + ) + }) + + // calls provided onSubmit callback + await waitFor(() => { + expect(onSubmit).toHaveBeenCalled() + }) + }) +}) + +const ROLES_MOD_ADDRESS = '0x1234567890000000000000000000000000000000' +const MEMBER_ADDRESS = '0x1111111110000000000000000000000000000000' +const ROLE_KEY = encodeBytes32String('eth_wrapping') + +const SAFE_INFO = extendedSafeInfoBuilder().build() +SAFE_INFO.modules = [{ value: ROLES_MOD_ADDRESS }] +SAFE_INFO.chainId = '1' + +const lowercaseSafeAddress = SAFE_INFO.address.value.toLowerCase() + +const WETH_ADDRESS = '0xfff9976782d46cc05630d1f6ebab18b2324d6b14' + +const { Clearance, ExecutionOptions } = zodiacRoles + +const TEST_ROLES_MOD = { + address: ROLES_MOD_ADDRESS, + owner: lowercaseSafeAddress, + avatar: lowercaseSafeAddress, + target: lowercaseSafeAddress, + roles: [ + { + key: ROLE_KEY, + members: [MEMBER_ADDRESS], + targets: [ + { + address: '0xc36442b4a4522e871399cd717abdd847ab11fe88', + clearance: Clearance.Function, + executionOptions: ExecutionOptions.None, + functions: [ + { + selector: '0x49404b7c', + wildcarded: false, + executionOptions: ExecutionOptions.None, + }, + ], + }, + { + address: WETH_ADDRESS, // WETH + clearance: Clearance.Function, + executionOptions: ExecutionOptions.None, + functions: [ + { + selector: '0x2e1a7d4d', // withdraw(uint256) + wildcarded: true, + executionOptions: ExecutionOptions.None, + }, + { + selector: '0xd0e30db0', // deposit() + wildcarded: true, + executionOptions: ExecutionOptions.Send, + }, + ], + }, + ], + }, + ], +} + +/** + * Getting the deepest element that contain string / match regex even when it split between multiple elements + * + * @example + * For: + * <div> + * <span>Hello</span><span> World</span> + * </div> + * + * screen.getByText('Hello World') // ❌ Fail + * screen.getByText(textContentMatcher('Hello World')) // ✅ pass + */ +function textContentMatcher(textMatch: string | RegExp) { + const hasText = + typeof textMatch === 'string' + ? (node: Element) => node.textContent === textMatch + : (node: Element) => textMatch.test(node.textContent || '') + + const matcher = (_content: string, node: Element | null) => { + if (!node || !hasText(node)) { + return false + } + + return Array.from(node?.children || []).every((child) => !hasText(child)) + } + + matcher.toString = () => `textContentMatcher(${textMatch})` + + return matcher +} diff --git a/src/components/tx/SignOrExecuteForm/PermissionsCheck/hooks.ts b/src/components/tx/SignOrExecuteForm/PermissionsCheck/hooks.ts new file mode 100644 index 0000000000..6922a96787 --- /dev/null +++ b/src/components/tx/SignOrExecuteForm/PermissionsCheck/hooks.ts @@ -0,0 +1,267 @@ +import useAsync from '@/hooks/useAsync' +import useSafeInfo from '@/hooks/useSafeInfo' +import { useWeb3ReadOnly } from '@/hooks/wallets/web3' +import { Errors, logError } from '@/services/exceptions' +import { getModuleTransactionId } from '@/services/transactions' +import { backOff } from 'exponential-backoff' +import { useEffect, useMemo } from 'react' +import { + type ChainId, + chains, + fetchRolesMod, + Clearance, + type RoleSummary, + ExecutionOptions, + Status, +} from 'zodiac-roles-deployments' +import { OperationType, type Transaction, type MetaTransactionData } from '@safe-global/safe-core-sdk-types' +import { type JsonRpcProvider } from 'ethers' +import { KnownContracts, getModuleInstance } from '@gnosis.pm/zodiac' +import useWallet from '@/hooks/wallets/useWallet' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' + +const ROLES_V2_SUPPORTED_CHAINS = Object.keys(chains) + +/** + * Returns all Zodiac Roles Modifiers v2 instances that are enabled and correctly configured on this Safe + */ +export const useRolesMods = () => { + const { safe } = useSafeInfo() + const isFeatureEnabled = useHasFeature(FEATURES.ZODIAC_ROLES) + + const [data] = useAsync(async () => { + if (!ROLES_V2_SUPPORTED_CHAINS.includes(safe.chainId) || !isFeatureEnabled) return [] + + const safeModules = safe.modules || [] + const rolesMods = await Promise.all( + safeModules.map((address) => + fetchRolesMod({ address: address.value as `0x${string}`, chainId: parseInt(safe.chainId) as ChainId }), + ), + ) + + return rolesMods.filter( + (mod): mod is Exclude<typeof mod, null> => + mod !== null && + mod.target === safe.address.value.toLowerCase() && + mod.avatar === safe.address.value.toLowerCase() && + mod.roles.length > 0, + ) + }, [safe, isFeatureEnabled]) + + return data +} + +/** + * Returns a list of roles mod address + role key assigned to the connected wallet. + * For each role, checks if the role allows the given meta transaction and returns the status. + */ +export const useRoles = (metaTx?: MetaTransactionData) => { + const rolesMods = useRolesMods() + const wallet = useWallet() + const walletAddress = wallet?.address.toLowerCase() as undefined | `0x${string}` + + // find all roles assigned to the connected wallet, statically check if they allow the given meta transaction + const potentialRoles = useMemo(() => { + const result: { + modAddress: `0x${string}` + roleKey: `0x${string}` + status: Status | null + }[] = [] + + if (walletAddress && rolesMods) { + for (const rolesMod of rolesMods) { + for (const role of rolesMod.roles) { + if (role.members.includes(walletAddress)) { + result.push({ + modAddress: rolesMod.address, + roleKey: role.key, + status: metaTx ? checkTransaction(role, metaTx) : null, + }) + } + } + } + } + + return result + }, [rolesMods, walletAddress, metaTx]) + const web3ReadOnly = useWeb3ReadOnly() + + // if the static check is inconclusive (status: null), evaluate the condition through a test call + const [dynamicallyCheckedPotentialRoles] = useAsync( + () => + Promise.all( + potentialRoles.map(async (entry) => { + if (entry.status === null && metaTx && walletAddress && web3ReadOnly) { + entry.status = await checkCondition(entry.modAddress, entry.roleKey, metaTx, walletAddress, web3ReadOnly) + } + return entry + }), + ), + [potentialRoles, metaTx, walletAddress, web3ReadOnly], + ) + + // Return the statically checked roles while the dynamic checks are still pending + return dynamicallyCheckedPotentialRoles || potentialRoles +} + +/** + * Returns the status of the permission check, `null` if it depends on the condition evaluation. + */ +const checkTransaction = (role: RoleSummary, metaTx: MetaTransactionData): Status | null => { + const target = role.targets.find((t) => t.address === metaTx.to.toLowerCase()) + if (!target) return Status.TargetAddressNotAllowed + + if (target.clearance === Clearance.Target) { + // all calls to the target are allowed + return checkExecutionOptions(target.executionOptions, metaTx) + } + + if (target.clearance === Clearance.Function) { + // check if the function is allowed + const selector = metaTx.data.slice(0, 10) as `0x${string}` + const func = target.functions.find((f) => f.selector === selector) + if (func) { + const execOptionsStatus = checkExecutionOptions(func.executionOptions, metaTx) + if (execOptionsStatus !== Status.Ok) return execOptionsStatus + return func.wildcarded ? Status.Ok : null // wildcarded means there's no condition set + } + } + + return Status.FunctionNotAllowed +} + +const checkExecutionOptions = (execOptions: ExecutionOptions, metaTx: MetaTransactionData): Status => { + const isSend = BigInt(metaTx.value || '0') > 0n + const isDelegateCall = metaTx.operation === OperationType.DelegateCall + + if (isSend && execOptions !== ExecutionOptions.Send && execOptions !== ExecutionOptions.Both) { + return Status.SendNotAllowed + } + if (isDelegateCall && execOptions !== ExecutionOptions.DelegateCall && execOptions !== ExecutionOptions.Both) { + return Status.DelegateCallNotAllowed + } + + return Status.Ok +} + +export const useExecuteThroughRole = ({ + modAddress, + roleKey, + metaTx, +}: { + modAddress?: `0x${string}` + roleKey?: `0x${string}` + metaTx?: MetaTransactionData +}) => { + const web3ReadOnly = useWeb3ReadOnly() + const wallet = useWallet() + const walletAddress = wallet?.address.toLowerCase() as undefined | `0x${string}` + + return useMemo( + () => + modAddress && roleKey && metaTx && walletAddress && web3ReadOnly + ? encodeExecuteThroughRole(modAddress, roleKey, metaTx, walletAddress, web3ReadOnly) + : undefined, + [modAddress, roleKey, metaTx, walletAddress, web3ReadOnly], + ) +} + +const encodeExecuteThroughRole = ( + modAddress: `0x${string}`, + roleKey: `0x${string}`, + metaTx: MetaTransactionData, + from: `0x${string}`, + provider: JsonRpcProvider, +): Transaction => { + const rolesModifier = getModuleInstance(KnownContracts.ROLES_V2, modAddress, provider) + const data = rolesModifier.interface.encodeFunctionData('execTransactionWithRole', [ + metaTx.to, + BigInt(metaTx.value), + metaTx.data, + metaTx.operation || 0, + roleKey, + true, + ]) + + return { + to: modAddress, + data, + value: '0', + from, + } +} + +const checkCondition = async ( + modAddress: `0x${string}`, + roleKey: `0x${string}`, + metaTx: MetaTransactionData, + from: `0x${string}`, + provider: JsonRpcProvider, +) => { + const rolesModifier = getModuleInstance(KnownContracts.ROLES_V2, modAddress, provider) + try { + await rolesModifier.execTransactionWithRole.estimateGas( + metaTx.to, + BigInt(metaTx.value), + metaTx.data, + metaTx.operation || 0, + roleKey, + false, + { from }, + ) + + return Status.Ok + } catch (e: any) { + const error = rolesModifier.interface.getError(e.data.slice(0, 10)) + if (error === null || error.name !== 'ConditionViolation') { + console.error('Unexpected error in condition check', error, e.data, e) + return null + } + + // status is a BigInt, convert it to enum + const { status } = rolesModifier.interface.decodeErrorResult(error, e.data) + return Number(status) as Status + } +} + +export const useGasLimit = ( + tx?: Transaction, +): { + gasLimit?: bigint + gasLimitError?: Error + gasLimitLoading: boolean +} => { + const web3ReadOnly = useWeb3ReadOnly() + + const [gasLimit, gasLimitError, gasLimitLoading] = useAsync<bigint | undefined>(async () => { + if (!web3ReadOnly || !tx) return + + return web3ReadOnly.estimateGas(tx) + }, [web3ReadOnly, tx]) + + useEffect(() => { + if (gasLimitError) { + logError(Errors._612, gasLimitError.message) + } + }, [gasLimitError]) + + return { gasLimit, gasLimitError, gasLimitLoading } +} + +export const pollModuleTransactionId = async (props: { + transactionService: string + safeAddress: string + txHash: string +}): Promise<string> => { + // exponential delay between attempts for around 4 min + return backOff(() => getModuleTransactionId(props), { + startingDelay: 750, + maxDelay: 20000, + numOfAttempts: 19, + retry: (e: any) => { + console.info('waiting for transaction-service to index the module transaction', e) + return true + }, + }) +} diff --git a/src/components/tx/SignOrExecuteForm/PermissionsCheck/index.tsx b/src/components/tx/SignOrExecuteForm/PermissionsCheck/index.tsx new file mode 100644 index 0000000000..edbac6d42e --- /dev/null +++ b/src/components/tx/SignOrExecuteForm/PermissionsCheck/index.tsx @@ -0,0 +1,220 @@ +import { useContext, useState } from 'react' +import { Status } from 'zodiac-roles-deployments' +import { type SafeTransaction } from '@safe-global/safe-core-sdk-types' +import { decodeBytes32String } from 'ethers' + +import { Box, Button, CardActions, Chip, CircularProgress, Divider, Typography } from '@mui/material' + +import commonCss from '@/components/tx-flow/common/styles.module.css' +import CheckWallet from '@/components/common/CheckWallet' +import TxCard from '@/components/tx-flow/common/TxCard' +import { getTransactionTrackingType } from '@/services/analytics/tx-tracking' +import { TX_EVENTS } from '@/services/analytics/events/transactions' +import { trackEvent } from '@/services/analytics' +import useSafeInfo from '@/hooks/useSafeInfo' +import WalletRejectionError from '../WalletRejectionError' +import ErrorMessage from '../../ErrorMessage' +import useWallet from '@/hooks/wallets/useWallet' +import { type SubmitCallback } from '..' +import { getTxOptions } from '@/utils/transactions' +import { isWalletRejection } from '@/utils/wallets' +import { Errors, trackError } from '@/services/exceptions' +import { asError } from '@/services/exceptions/utils' +import { SuccessScreenFlow } from '@/components/tx-flow/flows' +import AdvancedParams, { useAdvancedParams } from '../../AdvancedParams' +import { useCurrentChain } from '@/hooks/useChains' +import { dispatchModuleTxExecution } from '@/services/tx/tx-sender' +import useOnboard from '@/hooks/wallets/useOnboard' +import { assertOnboard, assertWallet } from '@/utils/helpers' +import { TxModalContext } from '@/components/tx-flow' +import { pollModuleTransactionId, useExecuteThroughRole, useRoles, useGasLimit } from './hooks' +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' + +const Role = ({ children }: { children: string }) => { + let humanReadableRoleKey = children + try { + humanReadableRoleKey = decodeBytes32String(children) + } catch (e) {} + + return <Chip label={humanReadableRoleKey} /> +} + +const PermissionsCheck: React.FC<{ onSubmit?: SubmitCallback; safeTx: SafeTransaction; safeTxError?: Error }> = ({ + onSubmit, + safeTx, + safeTxError, +}) => { + const currentChain = useCurrentChain() + const onboard = useOnboard() + const wallet = useWallet() + const { safe } = useSafeInfo() + + const chainId = currentChain?.chainId || '1' + + const [isPending, setIsPending] = useState<boolean>(false) + const [isRejectedByUser, setIsRejectedByUser] = useState<boolean>(false) + const [submitError, setSubmitError] = useState<Error | undefined>() + + const { setTxFlow } = useContext(TxModalContext) + + const roles = useRoles(safeTx?.data) + const allowingRole = roles.find((role) => role.status === Status.Ok) + + // If a user has multiple roles, we should prioritize the one that allows the transaction's to address (and function selector) + const mostLikelyRole = + allowingRole || + roles.find((role) => role.status !== Status.TargetAddressNotAllowed && role.status !== Status.FunctionNotAllowed) || + roles.find((role) => role.status !== Status.TargetAddressNotAllowed) || + roles[0] + + // Wrap call routing it through the Roles mod with the allowing role + const txThroughRole = useExecuteThroughRole({ + modAddress: allowingRole?.modAddress, + roleKey: allowingRole?.roleKey, + metaTx: safeTx?.data, + }) + // Estimate gas limit + const { gasLimit, gasLimitError } = useGasLimit(txThroughRole) + const [advancedParams, setAdvancedParams] = useAdvancedParams(gasLimit) + + const handleExecute = async () => { + assertWallet(wallet) + assertOnboard(onboard) + + await assertWalletChain(onboard, chainId) + + setIsRejectedByUser(false) + setIsPending(true) + setSubmitError(undefined) + setIsRejectedByUser(false) + + if (!txThroughRole) { + throw new Error('Execution through role is not possible') + } + + const txOptions = getTxOptions(advancedParams, currentChain) + + let txHash: string + try { + txHash = await dispatchModuleTxExecution({ ...txThroughRole, ...txOptions }, wallet.provider, safe.address.value) + } catch (_err) { + const err = asError(_err) + if (isWalletRejection(err)) { + setIsRejectedByUser(true) + } else { + trackError(Errors._815, err) + setSubmitError(err) + } + setIsPending(false) + return + } + + // On success, forward to the success screen, initially without a txId + setTxFlow(<SuccessScreenFlow txHash={txHash} />, undefined, false) + + // Wait for module tx to be indexed + const transactionService = currentChain?.transactionService + if (!transactionService) { + throw new Error('Transaction service not found') + } + const moduleTxId = await pollModuleTransactionId({ + transactionService, + safeAddress: safe.address.value, + txHash, + }) + + const txId = `module_${safe.address.value}_${moduleTxId}` + + onSubmit?.(txId, true) + + // Track tx event + const txType = await getTransactionTrackingType(chainId, txId) + trackEvent({ ...TX_EVENTS.EXECUTE_THROUGH_ROLE, label: txType }) + + // Update the success screen so it shows a link to the transaction + setTxFlow(<SuccessScreenFlow txId={txId} />, undefined, false) + } + + // Only render the card if the connected wallet is a member of any role + if (roles.length === 0) { + return null + } + + return ( + <TxCard> + <Typography variant="h5">Execute without confirmations</Typography> + + {allowingRole && ( + <> + <Typography component="div"> + As a member of the <Role>{allowingRole.roleKey}</Role> you can execute this transaction immediately without + confirmations from other owners. + </Typography> + <AdvancedParams + willExecute + params={advancedParams} + recommendedGasLimit={gasLimit} + onFormSubmit={setAdvancedParams} + gasLimitError={gasLimitError} + /> + </> + )} + + {!allowingRole && ( + <> + <Typography component="div"> + You are a member of the <Role>{mostLikelyRole.roleKey}</Role> role but it does not allow this transaction. + </Typography> + + {mostLikelyRole.status && ( + <ErrorMessage> + The permission check fails with the following status: + <br /> + <code>{Status[mostLikelyRole.status]}</code> + </ErrorMessage> + )} + </> + )} + + {safeTxError && ( + <ErrorMessage error={safeTxError}> + This transaction will most likely fail. To save gas costs, avoid confirming the transaction. + </ErrorMessage> + )} + + {submitError && ( + <Box mt={1}> + <ErrorMessage error={submitError}>Error submitting the transaction. Please try again.</ErrorMessage> + </Box> + )} + + {isRejectedByUser && ( + <Box mt={1}> + <WalletRejectionError /> + </Box> + )} + + <div> + <Divider className={commonCss.nestedDivider} sx={{ pt: 3 }} /> + + <CardActions> + <CheckWallet allowNonOwner> + {(isOk) => ( + <Button + data-testid="execute-through-role-btn" + variant="contained" + onClick={handleExecute} + disabled={!isOk || !allowingRole || isPending} + sx={{ minWidth: '209px' }} + > + {isPending ? <CircularProgress size={20} /> : 'Execute'} + </Button> + )} + </CheckWallet> + </CardActions> + </div> + </TxCard> + ) +} + +export default PermissionsCheck diff --git a/src/components/tx/SignOrExecuteForm/__tests__/SignOrExecute.test.tsx b/src/components/tx/SignOrExecuteForm/__tests__/SignOrExecute.test.tsx index 0854afcd50..b38d972c67 100644 --- a/src/components/tx/SignOrExecuteForm/__tests__/SignOrExecute.test.tsx +++ b/src/components/tx/SignOrExecuteForm/__tests__/SignOrExecute.test.tsx @@ -102,4 +102,14 @@ describe('SignOrExecute', () => { expect(getByText("You're about to execute this transaction.")).toBeInTheDocument() }) + + it('should not display safeTxError message for valid transactions', () => { + const { queryByText } = render( + <SignOrExecuteForm safeTx={safeTxBuilder().build()} onSubmit={jest.fn()} safeTxError={undefined} chainId="1" />, + ) + + expect( + queryByText('This transaction will most likely fail. To save gas costs, avoid confirming the transaction.'), + ).not.toBeInTheDocument() + }) }) diff --git a/src/components/tx/SignOrExecuteForm/hooks.test.ts b/src/components/tx/SignOrExecuteForm/hooks.test.ts index 735030b3f1..98def65661 100644 --- a/src/components/tx/SignOrExecuteForm/hooks.test.ts +++ b/src/components/tx/SignOrExecuteForm/hooks.test.ts @@ -1,5 +1,5 @@ -import { extendedSafeInfoBuilder } from '@/tests/builders/safe' -import { renderHook } from '@/tests/test-utils' +import { extendedSafeInfoBuilder, safeInfoBuilder } from '@/tests/builders/safe' +import { renderHook, waitFor } from '@/tests/test-utils' import { zeroPadValue } from 'ethers' import { createSafeTx } from '@/tests/builders/safeTx' import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' @@ -10,7 +10,16 @@ import * as pending from '@/hooks/usePendingTxs' import * as txSender from '@/services/tx/tx-sender/dispatch' import * as onboardHooks from '@/hooks/wallets/useOnboard' import { type OnboardAPI } from '@web3-onboard/core' -import { useAlreadySigned, useImmediatelyExecutable, useIsExecutionLoop, useTxActions, useValidateNonce } from './hooks' +import { + useAlreadySigned, + useImmediatelyExecutable, + useIsExecutionLoop, + useRecommendedNonce, + useTxActions, + useValidateNonce, +} from './hooks' +import * as recommendedNonce from '@/services/tx/tx-sender/recommendedNonce' +import { defaultSafeInfo } from '@/store/safeInfoSlice' describe('SignOrExecute hooks', () => { const extendedSafeInfo = extendedSafeInfoBuilder().build() @@ -566,24 +575,151 @@ describe('SignOrExecute hooks', () => { const { result } = renderHook(() => useAlreadySigned(tx)) expect(result.current).toEqual(true) }) + + it('should return false if wallet has not signed a tx yet', () => { + // Wallet + jest.spyOn(wallet, 'default').mockReturnValue({ + chainId: '1', + label: 'MetaMask', + address: '0x1234567890000000000000000000000000000000', + } as unknown as ConnectedWallet) + + const tx = createSafeTx() + tx.addSignature({ + signer: '0x00000000000000000000000000000000000000000', + data: '0x0001', + staticPart: () => '', + dynamicPart: () => '', + isContractSignature: false, + }) + const { result } = renderHook(() => useAlreadySigned(tx)) + expect(result.current).toEqual(false) + }) }) - it('should return false if wallet has not signed a tx yet', () => { - // Wallet - jest.spyOn(wallet, 'default').mockReturnValue({ - chainId: '1', - label: 'MetaMask', - address: '0x1234567890000000000000000000000000000000', - } as unknown as ConnectedWallet) - const tx = createSafeTx() - tx.addSignature({ - signer: '0x00000000000000000000000000000000000000000', - data: '0x0001', - staticPart: () => '', - dynamicPart: () => '', - isContractSignature: false, + describe('useRecommendedNonce', () => { + it('should return undefined without safe info', async () => { + jest.spyOn(useSafeInfoHook, 'default').mockReturnValue({ + safe: { ...defaultSafeInfo, deployed: false }, + safeAddress: '', + safeLoaded: true, + safeLoading: false, + }) + + const { result } = renderHook(useRecommendedNonce) + await waitFor(() => { + expect(result.current).toBeUndefined() + }) + }) + it('should return 0 for counterfactual Safes', async () => { + const mockSafeInfo = safeInfoBuilder().build() + jest.spyOn(useSafeInfoHook, 'default').mockReturnValue({ + safe: { ...mockSafeInfo, deployed: false }, + safeAddress: mockSafeInfo.address.value, + safeLoaded: true, + safeLoading: false, + }) + + const { result } = renderHook(useRecommendedNonce) + await waitFor(() => { + expect(result.current).toEqual(0) + }) + }) + + it('should update if queueTag changes', async () => { + jest.spyOn(recommendedNonce, 'getNonces').mockResolvedValue({ + currentNonce: 1, + recommendedNonce: 1, + }) + const mockSafeInfo = safeInfoBuilder() + .with({ + txQueuedTag: '1', + }) + .build() + jest.spyOn(useSafeInfoHook, 'default').mockReturnValue({ + safe: { ...mockSafeInfo, deployed: true }, + safeAddress: mockSafeInfo.address.value, + safeLoaded: true, + safeLoading: false, + }) + + const { result, rerender } = renderHook(useRecommendedNonce) + await waitFor(() => { + expect(result.current).toEqual(1) + }) + + jest.spyOn(recommendedNonce, 'getNonces').mockResolvedValue({ + currentNonce: 1, + recommendedNonce: 2, + }) + + rerender() + // The hook does not rerender as the queue tag did not change yet + await waitFor(() => { + expect(result.current).toEqual(1) + }) + + jest.spyOn(useSafeInfoHook, 'default').mockReturnValue({ + safe: { ...mockSafeInfo, deployed: true, txQueuedTag: '2' }, + safeAddress: mockSafeInfo.address.value, + safeLoaded: true, + safeLoading: false, + }) + + rerender() + + // Now the queue tag changed from 1 to 2 and the hook should reflect the new recommended Nonce + await waitFor(() => { + expect(result.current).toEqual(2) + }) + }) + + it('should update if historyTag changes', async () => { + jest.spyOn(recommendedNonce, 'getNonces').mockResolvedValue({ + currentNonce: 1, + recommendedNonce: 1, + }) + const mockSafeInfo = safeInfoBuilder() + .with({ + txHistoryTag: '1', + }) + .build() + jest.spyOn(useSafeInfoHook, 'default').mockReturnValue({ + safe: { ...mockSafeInfo, deployed: true }, + safeAddress: mockSafeInfo.address.value, + safeLoaded: true, + safeLoading: false, + }) + + const { result, rerender } = renderHook(useRecommendedNonce) + await waitFor(() => { + expect(result.current).toEqual(1) + }) + + jest.spyOn(recommendedNonce, 'getNonces').mockResolvedValue({ + currentNonce: 2, + recommendedNonce: 2, + }) + + rerender() + // The hook does not rerender as the history tag did not change yet + await waitFor(() => { + expect(result.current).toEqual(1) + }) + + jest.spyOn(useSafeInfoHook, 'default').mockReturnValue({ + safe: { ...mockSafeInfo, deployed: true, txHistoryTag: '2' }, + safeAddress: mockSafeInfo.address.value, + safeLoaded: true, + safeLoading: false, + }) + + rerender() + + // Now the history tag changed from 1 to 2 and the hook should reflect the new recommended Nonce + await waitFor(() => { + expect(result.current).toEqual(2) + }) }) - const { result } = renderHook(() => useAlreadySigned(tx)) - expect(result.current).toEqual(false) }) }) diff --git a/src/components/tx/SignOrExecuteForm/hooks.ts b/src/components/tx/SignOrExecuteForm/hooks.ts index c7a4669ea9..aae69d8b24 100644 --- a/src/components/tx/SignOrExecuteForm/hooks.ts +++ b/src/components/tx/SignOrExecuteForm/hooks.ts @@ -1,3 +1,4 @@ +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import { assertTx, assertWallet, assertOnboard } from '@/utils/helpers' import { useMemo } from 'react' import { type TransactionOptions, type SafeTransaction } from '@safe-global/safe-core-sdk-types' @@ -64,13 +65,12 @@ export const useTxActions = (): TxActions => { const signRelayedTx = async (safeTx: SafeTransaction, txId?: string): Promise<SafeTransaction> => { assertTx(safeTx) assertWallet(wallet) - assertOnboard(onboard) // Smart contracts cannot sign transactions off-chain if (await isSmartContractWallet(wallet.chainId, wallet.address)) { throw new Error('Cannot relay an unsigned transaction from a smart contract wallet') } - return await dispatchTxSigning(safeTx, version, onboard, chainId, txId) + return await dispatchTxSigning(safeTx, version, wallet.provider, txId) } const signTx: TxActions['signTx'] = async (safeTx, txId, origin) => { @@ -78,18 +78,20 @@ export const useTxActions = (): TxActions => { assertWallet(wallet) assertOnboard(onboard) + await assertWalletChain(onboard, chainId) + // Smart contract wallets must sign via an on-chain tx if (await isSmartContractWallet(wallet.chainId, wallet.address)) { // If the first signature is a smart contract wallet, we have to propose w/o signatures // Otherwise the backend won't pick up the tx // The signature will be added once the on-chain signature is indexed const id = txId || (await proposeTx(wallet.address, safeTx, txId, origin)).txId - await dispatchOnChainSigning(safeTx, id, onboard, chainId) + await dispatchOnChainSigning(safeTx, id, wallet.provider, chainId) return id } // Otherwise, sign off-chain - const signedTx = await dispatchTxSigning(safeTx, version, onboard, chainId, txId) + const signedTx = await dispatchTxSigning(safeTx, version, wallet.provider, txId) const tx = await proposeTx(wallet.address, signedTx, txId, origin) return tx.txId } @@ -99,6 +101,8 @@ export const useTxActions = (): TxActions => { assertWallet(wallet) assertOnboard(onboard) + await assertWalletChain(onboard, chainId) + let tx: TransactionDetails | undefined // Relayed transactions must be fully signed, so request a final signature if needed if (isRelayed && safeTx.signatures.size < safe.threshold) { @@ -122,7 +126,7 @@ export const useTxActions = (): TxActions => { if (isRelayed) { await dispatchTxRelay(safeTx, safe, txId, txOptions.gasLimit) } else { - await dispatchTxExecution(safeTx, txOptions, txId, onboard, chainId, safeAddress) + await dispatchTxExecution(safeTx, txOptions, txId, wallet.provider, wallet.address, safeAddress) } return txId @@ -163,7 +167,7 @@ export const useRecommendedNonce = (): number | undefined => { return nonces?.recommendedNonce }, // eslint-disable-next-line react-hooks/exhaustive-deps - [safeAddress, safe.chainId, safe.txQueuedTag], // update when tx queue changes + [safeAddress, safe.chainId, safe.txQueuedTag, safe.txHistoryTag], // update when tx queue or history changes false, // keep old recommended nonce while refreshing to avoid skeleton ) diff --git a/src/components/tx/SignOrExecuteForm/index.tsx b/src/components/tx/SignOrExecuteForm/index.tsx index e85ad42121..84e2397e2e 100644 --- a/src/components/tx/SignOrExecuteForm/index.tsx +++ b/src/components/tx/SignOrExecuteForm/index.tsx @@ -26,6 +26,11 @@ import { getTransactionTrackingType } from '@/services/analytics/tx-tracking' import { TX_EVENTS } from '@/services/analytics/events/transactions' import { trackEvent } from '@/services/analytics' import useChainId from '@/hooks/useChainId' +import PermissionsCheck from './PermissionsCheck' +import { isConfirmationViewOrder } from '@/utils/transaction-guards' +import SwapOrderConfirmationView from '@/features/swap/components/SwapOrderConfirmationView' +import { isSettingTwapFallbackHandler } from '@/features/swap/helpers/utils' +import { TwapFallbackHandlerWarning } from '@/features/swap/components/TwapFallbackHandlerWarning' export type SubmitCallback = (txId: string, isExecuted?: boolean) => void @@ -73,9 +78,11 @@ export const SignOrExecuteForm = ({ const isCorrectNonce = useValidateNonce(safeTx) const [decodedData, decodedDataError, decodedDataLoading] = useDecodeTx(safeTx) const isBatchable = props.isBatchable !== false && safeTx && !isDelegateCall(safeTx) + const isSwapOrder = isConfirmationViewOrder(decodedData) const { safe } = useSafeInfo() const isCounterfactualSafe = !safe.deployed + const isChangingFallbackHandler = isSettingTwapFallbackHandler(decodedData) // If checkbox is checked and the transaction is executable, execute it, otherwise sign it const canExecute = isCorrectNonce && (props.isExecutable || isNewExecutableTx) @@ -96,19 +103,27 @@ export const SignOrExecuteForm = ({ <TxCard> {props.children} + {isChangingFallbackHandler && <TwapFallbackHandlerWarning />} + + {isSwapOrder && ( + <ErrorBoundary fallback={<></>}> + <SwapOrderConfirmationView order={decodedData} settlementContract={safeTx?.data.to ?? ''} /> + </ErrorBoundary> + )} + <ErrorBoundary fallback={<div>Error parsing data</div>}> <ApprovalEditor safeTransaction={safeTx} /> - </ErrorBoundary> - <DecodedTx - tx={safeTx} - txId={props.txId} - decodedData={decodedData} - decodedDataError={decodedDataError} - decodedDataLoading={decodedDataLoading} - showMultisend={!props.isBatch} - showToBlock={props.showToBlock} - /> + <DecodedTx + tx={safeTx} + txId={props.txId} + decodedData={decodedData} + decodedDataError={decodedDataError} + decodedDataLoading={decodedDataLoading} + showMultisend={!props.isBatch} + showToBlock={props.showToBlock} + /> + </ErrorBoundary> {!isCounterfactualSafe && <RedefineBalanceChanges />} </TxCard> @@ -119,6 +134,12 @@ export const SignOrExecuteForm = ({ </TxCard> )} + {!isCounterfactualSafe && safeTx && isCreation && ( + <ErrorBoundary> + <PermissionsCheck onSubmit={onSubmit} safeTx={safeTx} safeTxError={safeTxError} /> + </ErrorBoundary> + )} + <TxCard> <ConfirmationTitle variant={willExecute ? ConfirmationTitleTypes.execute : ConfirmationTitleTypes.sign} diff --git a/src/components/tx/security/redefine/useRedefine.ts b/src/components/tx/security/redefine/useRedefine.ts index dde017cba6..d1f6d3b620 100644 --- a/src/components/tx/security/redefine/useRedefine.ts +++ b/src/components/tx/security/redefine/useRedefine.ts @@ -80,22 +80,33 @@ export const useRedefine = ( const [retryCounter, setRetryCounter] = useState(0) const isFeatureEnabled = useHasFeature(FEATURES.RISK_MITIGATION) + // Memoized JSON data to avoid unnecessary requests + const jsonData = useMemo(() => { + if (!data) return '' + let adjustedData = data + if ('data' in data) { + // We need to set nonce to 0 to avoid repeated requests with an updated nonce + adjustedData = { ...data, data: { ...data.data, nonce: 0 } } + } + return JSON.stringify(adjustedData) + }, [data]) + const [redefinePayload, redefineErrors, redefineLoading] = useAsync<SecurityResponse<RedefineModuleResponse>>( () => { - if (!isFeatureEnabled || !data || !wallet?.address) { + if (!isFeatureEnabled || !jsonData || !wallet?.address) { return } return RedefineModuleInstance.scanTransaction({ chainId: Number(safe.chainId), - data, + data: JSON.parse(jsonData), safeAddress, walletAddress: wallet.address, threshold: safe.threshold, }) }, // eslint-disable-next-line react-hooks/exhaustive-deps - [safe.chainId, safe.threshold, safeAddress, data, wallet?.address, retryCounter, isFeatureEnabled], + [safe.chainId, safe.threshold, safeAddress, jsonData, wallet?.address, retryCounter, isFeatureEnabled], false, ) diff --git a/src/components/welcome/MyAccounts/AccountItem.tsx b/src/components/welcome/MyAccounts/AccountItem.tsx index 56b5cf6e35..683959e872 100644 --- a/src/components/welcome/MyAccounts/AccountItem.tsx +++ b/src/components/welcome/MyAccounts/AccountItem.tsx @@ -2,7 +2,7 @@ import { LoopIcon } from '@/features/counterfactual/CounterfactualStatusButton' import { selectUndeployedSafe } from '@/features/counterfactual/store/undeployedSafesSlice' import type { ChainInfo, SafeOverview } from '@safe-global/safe-gateway-typescript-sdk' import { useCallback, useMemo } from 'react' -import { ListItemButton, Box, Typography, Chip } from '@mui/material' +import { ListItemButton, Box, Typography, Chip, Skeleton } from '@mui/material' import Link from 'next/link' import SafeIcon from '@/components/common/SafeIcon' import Track from '@/components/common/Track' @@ -109,8 +109,8 @@ const AccountItem = ({ onLinkClick, safeItem, safeOverview }: AccountItemProps) )} </Typography> - <Typography variant="body2" fontWeight="bold"> - {safeOverview?.fiatTotal && <FiatValue value={safeOverview.fiatTotal} />} + <Typography variant="body2" fontWeight="bold" textAlign="right" pr={5}> + {safeOverview ? <FiatValue value={safeOverview.fiatTotal} /> : <Skeleton variant="text" />} </Typography> <ChainIndicator chainId={chainId} responsive /> diff --git a/src/components/welcome/MyAccounts/PaginatedSafeList.tsx b/src/components/welcome/MyAccounts/PaginatedSafeList.tsx index a022bae193..da910de584 100644 --- a/src/components/welcome/MyAccounts/PaginatedSafeList.tsx +++ b/src/components/welcome/MyAccounts/PaginatedSafeList.tsx @@ -8,7 +8,7 @@ import { sameAddress } from '@/utils/addresses' import InfiniteScroll from '@/components/common/InfiniteScroll' type PaginatedSafeListProps = { - safes: SafeItem[] + safes?: SafeItem[] title: ReactNode noSafesMessage?: ReactNode action?: ReactElement @@ -26,7 +26,9 @@ const SafeListPage = ({ safes, onLinkClick }: SafeListPageProps) => { const [overviews] = useSafeOverviews(safes) const findOverview = (item: SafeItem) => { - return overviews?.find((overview) => sameAddress(overview.address.value, item.address)) + return overviews?.find( + (overview) => item.chainId === overview.chainId && sameAddress(overview.address.value, item.address), + ) } return ( @@ -77,7 +79,7 @@ const PaginatedSafeList = ({ safes, title, action, noSafesMessage, onLinkClick } <Typography variant="h5" fontWeight={700} mb={2} className={css.listTitle}> {title} - {safes.length > 0 && ( + {safes && safes.length > 0 && ( <Typography component="span" color="var(--color-primary-light)" fontSize="inherit" fontWeight="normal"> {' '} ({safes.length}) @@ -88,11 +90,11 @@ const PaginatedSafeList = ({ safes, title, action, noSafesMessage, onLinkClick } {action} </div> - {safes.length > 0 ? ( + {safes && safes.length > 0 ? ( <AllSafeListPages safes={safes} onLinkClick={onLinkClick} /> ) : ( <Typography variant="body2" color="text.secondary" textAlign="center" py={3} mx="auto" width={250}> - {noSafesMessage} + {safes ? noSafesMessage : 'Loading...'} </Typography> )} </Paper> diff --git a/src/components/welcome/MyAccounts/__tests__/useSafeOverviews.test.ts b/src/components/welcome/MyAccounts/__tests__/useSafeOverviews.test.ts new file mode 100644 index 0000000000..dc20ee92cd --- /dev/null +++ b/src/components/welcome/MyAccounts/__tests__/useSafeOverviews.test.ts @@ -0,0 +1,56 @@ +import useSafeOverviews from '../useSafeOverviews' +import * as balances from '@/hooks/loadables/useLoadBalances' +import * as sdk from '@safe-global/safe-gateway-typescript-sdk' +import * as useWallet from '@/hooks/wallets/useWallet' +import * as store from '@/store' +import type { Eip1193Provider } from 'ethers' +import { renderHook } from '@testing-library/react' +import { act } from 'react-dom/test-utils' + +jest.spyOn(balances, 'useTokenListSetting').mockReturnValue(false) +jest.spyOn(store, 'useAppSelector').mockReturnValue('USD') +jest + .spyOn(useWallet, 'default') + .mockReturnValue({ label: 'MetaMask', chainId: '1', address: '0x1234', provider: null as unknown as Eip1193Provider }) + +describe('useSafeOverviews', () => { + it('should filter out undefined addresses', async () => { + const spy = jest.spyOn(sdk, 'getSafeOverviews').mockResolvedValue([]) + const safes = [ + { address: '0x1234', chainId: '1' }, + { address: undefined as unknown as string, chainId: '2' }, + { address: '0x5678', chainId: '3' }, + ] + + renderHook(() => useSafeOverviews(safes)) + + await act(() => Promise.resolve()) + + expect(spy).toHaveBeenCalledWith(['1:0x1234', '3:0x5678'], { + currency: 'USD', + exclude_spam: false, + trusted: true, + wallet_address: '0x1234', + }) + }) + + it('should filter out undefined chain ids', async () => { + const spy = jest.spyOn(sdk, 'getSafeOverviews').mockResolvedValue([]) + const safes = [ + { address: '0x1234', chainId: '1' }, + { address: '0x5678', chainId: undefined as unknown as string }, + { address: '0x5678', chainId: '3' }, + ] + + renderHook(() => useSafeOverviews(safes)) + + await act(() => Promise.resolve()) + + expect(spy).toHaveBeenCalledWith(['1:0x1234', '3:0x5678'], { + currency: 'USD', + exclude_spam: false, + trusted: true, + wallet_address: '0x1234', + }) + }) +}) diff --git a/src/components/welcome/MyAccounts/index.tsx b/src/components/welcome/MyAccounts/index.tsx index 717de2940a..193d65fac9 100644 --- a/src/components/welcome/MyAccounts/index.tsx +++ b/src/components/welcome/MyAccounts/index.tsx @@ -17,9 +17,10 @@ import { useRouter } from 'next/router' import useTrackSafesCount from './useTrackedSafesCount' const NO_SAFES_MESSAGE = "You don't have any Safe Accounts yet" +const NO_WATCHED_MESSAGE = 'Watch any Safe Account to keep an eye on its activity' type AccountsListProps = { - safes: SafeItems | undefined + safes?: SafeItems | undefined onLinkClick?: () => void } const AccountsList = ({ safes, onLinkClick }: AccountsListProps) => { @@ -28,7 +29,7 @@ const AccountsList = ({ safes, onLinkClick }: AccountsListProps) => { const ownedSafes = useMemo(() => safes?.filter(({ isWatchlist }) => !isWatchlist), [safes]) const watchlistSafes = useMemo(() => safes?.filter(({ isWatchlist }) => isWatchlist), [safes]) - useTrackSafesCount(ownedSafes, watchlistSafes) + useTrackSafesCount(ownedSafes, watchlistSafes, wallet) const isLoginPage = router.pathname === AppRoutes.welcome.accounts const trackingLabel = isLoginPage ? OVERVIEW_LABELS.login_page : OVERVIEW_LABELS.sidebar @@ -47,7 +48,7 @@ const AccountsList = ({ safes, onLinkClick }: AccountsListProps) => { <PaginatedSafeList title="My accounts" - safes={ownedSafes || []} + safes={ownedSafes} onLinkClick={onLinkClick} noSafesMessage={ wallet ? ( @@ -85,7 +86,7 @@ const AccountsList = ({ safes, onLinkClick }: AccountsListProps) => { </Link> </Track> } - noSafesMessage={NO_SAFES_MESSAGE} + noSafesMessage={NO_WATCHED_MESSAGE} onLinkClick={onLinkClick} /> diff --git a/src/components/welcome/MyAccounts/styles.module.css b/src/components/welcome/MyAccounts/styles.module.css index 5aa0edf2b1..60854d02b8 100644 --- a/src/components/welcome/MyAccounts/styles.module.css +++ b/src/components/welcome/MyAccounts/styles.module.css @@ -64,10 +64,18 @@ 'a c d'; } - .safeLink :nth-child(1) { grid-area: a; } - .safeLink :nth-child(2) { grid-area: b; } - .safeLink :nth-child(3) { grid-area: c; } - .safeLink :nth-child(4) { grid-area: d; } + .safeLink :nth-child(1) { + grid-area: a; + } + .safeLink :nth-child(2) { + grid-area: b; + } + .safeLink :nth-child(3) { + grid-area: c; + } + .safeLink :nth-child(4) { + grid-area: d; + } } .safeAddress { diff --git a/src/components/welcome/MyAccounts/useAllOwnedSafes.ts b/src/components/welcome/MyAccounts/useAllOwnedSafes.ts index a14c362eff..b96e7007cb 100644 --- a/src/components/welcome/MyAccounts/useAllOwnedSafes.ts +++ b/src/components/welcome/MyAccounts/useAllOwnedSafes.ts @@ -7,17 +7,32 @@ import { useEffect } from 'react' const CACHE_KEY = 'ownedSafesCache_' +type OwnedSafesPerAddress = { + address: string | undefined + ownedSafes: AllOwnedSafes +} + const useAllOwnedSafes = (address: string): AsyncResult<AllOwnedSafes> => { const [cache, setCache] = useLocalStorage<AllOwnedSafes>(CACHE_KEY + address) - const [data, error, isLoading] = useAsync<AllOwnedSafes>(async () => { - if (!address) return {} - return getAllOwnedSafes(address) + const [data, error, isLoading] = useAsync<OwnedSafesPerAddress>(async () => { + if (!address) + return { + ownedSafes: {}, + address: undefined, + } + const ownedSafes = await getAllOwnedSafes(address) + return { + ownedSafes, + address, + } }, [address]) useEffect(() => { - if (data != undefined) setCache(data) - }, [data, setCache]) + if (data?.ownedSafes != undefined && data.address === address) { + setCache(data.ownedSafes) + } + }, [address, cache, data, setCache]) return [cache, error, isLoading] } diff --git a/src/components/welcome/MyAccounts/useAllSafes.ts b/src/components/welcome/MyAccounts/useAllSafes.ts index c5dc62f6a5..84c3a40020 100644 --- a/src/components/welcome/MyAccounts/useAllSafes.ts +++ b/src/components/welcome/MyAccounts/useAllSafes.ts @@ -8,7 +8,6 @@ import useChains from '@/hooks/useChains' import useWallet from '@/hooks/wallets/useWallet' import { selectUndeployedSafes } from '@/store/slices' import { sameAddress } from '@/utils/addresses' - export type SafeItem = { chainId: string address: string @@ -37,12 +36,15 @@ export const useHasSafes = () => { const useAllSafes = (): SafeItems | undefined => { const { address: walletAddress = '' } = useWallet() || {} - const [allOwned] = useAllOwnedSafes(walletAddress) + const [allOwned, , allOwnedLoading] = useAllOwnedSafes(walletAddress) const allAdded = useAddedSafes() const { configs } = useChains() const undeployedSafes = useAppSelector(selectUndeployedSafes) return useMemo<SafeItems | undefined>(() => { + if (walletAddress && (allOwned === undefined || allOwnedLoading)) { + return undefined + } const chains = uniq(Object.keys(allAdded).concat(Object.keys(allOwned || {}))) return chains.flatMap((chainId) => { @@ -64,7 +66,7 @@ const useAllSafes = (): SafeItems | undefined => { } }) }) - }, [allAdded, allOwned, configs, undeployedSafes, walletAddress]) + }, [allAdded, allOwned, allOwnedLoading, configs, undeployedSafes, walletAddress]) } export default useAllSafes diff --git a/src/components/welcome/MyAccounts/useSafeOverviews.ts b/src/components/welcome/MyAccounts/useSafeOverviews.ts index bfe33c3eb4..7c3b2ff2e1 100644 --- a/src/components/welcome/MyAccounts/useSafeOverviews.ts +++ b/src/components/welcome/MyAccounts/useSafeOverviews.ts @@ -1,26 +1,46 @@ +import { useMemo } from 'react' import { useTokenListSetting } from '@/hooks/loadables/useLoadBalances' -import useAsync from '@/hooks/useAsync' +import useAsync, { type AsyncResult } from '@/hooks/useAsync' import useWallet from '@/hooks/wallets/useWallet' import { useAppSelector } from '@/store' import { selectCurrency } from '@/store/settingsSlice' -import { getSafeOverviews } from '@safe-global/safe-gateway-typescript-sdk' +import { type SafeOverview, getSafeOverviews } from '@safe-global/safe-gateway-typescript-sdk' -function useSafeOverviews(safes: Array<{ address: string; chainId: string }>) { +const _cache: Record<string, SafeOverview[]> = {} + +type SafeParams = { + address: string + chainId: string +} + +// EIP155 address format +const makeSafeId = ({ chainId, address }: SafeParams) => `${chainId}:${address}` as `${number}:0x${string}` + +const validateSafeParams = ({ chainId, address }: SafeParams) => chainId != null && address != null + +function useSafeOverviews(safes: Array<SafeParams>): AsyncResult<SafeOverview[]> { const excludeSpam = useTokenListSetting() || false const currency = useAppSelector(selectCurrency) const wallet = useWallet() const walletAddress = wallet?.address + const safesIds = useMemo(() => safes.filter(validateSafeParams).map(makeSafeId), [safes]) - return useAsync(async () => { - const safesStrings = safes.map((safe) => `${safe.chainId}:${safe.address}` as `${number}:0x${string}`) - - return await getSafeOverviews(safesStrings, { + const [data, error, isLoading] = useAsync(async () => { + return await getSafeOverviews(safesIds, { trusted: true, exclude_spam: excludeSpam, currency, wallet_address: walletAddress, }) - }, [safes, excludeSpam, currency, walletAddress]) + }, [safesIds, excludeSpam, currency, walletAddress]) + + const cacheKey = safesIds.join() + const result = data ?? _cache[cacheKey] + + // Cache until the next page load + _cache[cacheKey] = result + + return useMemo(() => [result, error, isLoading], [result, error, isLoading]) } export default useSafeOverviews diff --git a/src/components/welcome/MyAccounts/useTrackedSafesCount.ts b/src/components/welcome/MyAccounts/useTrackedSafesCount.ts index 69bd25229b..06289dac02 100644 --- a/src/components/welcome/MyAccounts/useTrackedSafesCount.ts +++ b/src/components/welcome/MyAccounts/useTrackedSafesCount.ts @@ -3,20 +3,37 @@ import { OVERVIEW_EVENTS, trackEvent } from '@/services/analytics' import { useRouter } from 'next/router' import { useEffect } from 'react' import type { SafeItems } from './useAllSafes' +import type { ConnectedWallet } from '@/hooks/wallets/useOnboard' -let isTracked = false +let isOwnedSafesTracked = false +let isWatchlistTracked = false -const useTrackSafesCount = (ownedSafes: SafeItems | undefined, watchlistSafes: SafeItems | undefined) => { +const useTrackSafesCount = ( + ownedSafes: SafeItems | undefined, + watchlistSafes: SafeItems | undefined, + wallet: ConnectedWallet | null, +) => { const router = useRouter() const isLoginPage = router.pathname === AppRoutes.welcome.accounts + // Reset tracking for new wallet useEffect(() => { - if (watchlistSafes && ownedSafes && isLoginPage && !isTracked) { + isOwnedSafesTracked = false + }, [wallet?.address]) + + useEffect(() => { + if (wallet && !isOwnedSafesTracked && ownedSafes && ownedSafes.length > 0 && isLoginPage) { trackEvent({ ...OVERVIEW_EVENTS.TOTAL_SAFES_OWNED, label: ownedSafes.length }) + isOwnedSafesTracked = true + } + }, [isLoginPage, ownedSafes, wallet]) + + useEffect(() => { + if (watchlistSafes && isLoginPage && watchlistSafes.length > 0 && !isWatchlistTracked) { trackEvent({ ...OVERVIEW_EVENTS.TOTAL_SAFES_WATCHLIST, label: watchlistSafes.length }) - isTracked = true + isWatchlistTracked = true } - }, [isLoginPage, ownedSafes, watchlistSafes]) + }, [isLoginPage, watchlistSafes]) } export default useTrackSafesCount diff --git a/src/components/welcome/NewSafeSocial.tsx b/src/components/welcome/NewSafeSocial.tsx deleted file mode 100644 index b74e4501a7..0000000000 --- a/src/components/welcome/NewSafeSocial.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React from 'react' -import { Box, Button, Grid, Typography } from '@mui/material' -import css from './styles.module.css' -import Link from 'next/link' - -import ChevronLeftIcon from '@mui/icons-material/ChevronLeft' -import WelcomeLogin from './WelcomeLogin' -import GnosisChainLogo from '@/public/images/common/gnosis-chain-logo.png' -import Image from 'next/image' - -const BulletListItem = ({ text }: { text: string }) => ( - <li> - <Typography color="static.main" fontWeight={700}> - {text} - </Typography> - </li> -) - -const MarqueeItem = () => { - return ( - <li className={css.marqueeItem}> - Free on - <Image src={GnosisChainLogo} alt="Gnosis Chain logo" width={24} height={24} /> - Gnosis Chain - </li> - ) -} - -const NewSafeSocial = () => { - return ( - <> - <Grid container spacing={3} p={3} pb={0} flex={1} direction="row-reverse"> - <Grid item xs={12} lg={6}> - <WelcomeLogin /> - </Grid> - <Grid item xs={12} lg={6} flex={1}> - <div className={css.content}> - <Box pt={5} alignSelf="center" margin="auto"> - <Typography - variant="h1" - fontSize={[44, null, 52]} - lineHeight={1.15} - letterSpacing={-1.5} - color="static.main" - > - Get the most secure web3 account in {'<'}30 seconds - </Typography> - - <ul className={css.numberList}> - <BulletListItem text="Choose your Google Account" /> - <BulletListItem text="Review, wait and you’re done!" /> - <BulletListItem text="Add more signers later to level up your security" /> - </ul> - - <Link href="https://safe.global/campaigns/social-login-gnosis" passHref> - <Button startIcon={<ChevronLeftIcon />} className={css.button}> - Back to landing page - </Button> - </Link> - </Box> - - <div className={css.marquee}> - <ul className={css.marqueeContent}> - <MarqueeItem /> - <MarqueeItem /> - <MarqueeItem /> - </ul> - <ul className={css.marqueeContent} aria-hidden="true"> - <MarqueeItem /> - <MarqueeItem /> - <MarqueeItem /> - </ul> - </div> - </div> - </Grid> - </Grid> - </> - ) -} - -export default NewSafeSocial diff --git a/src/components/welcome/WelcomeLogin/WalletLogin.tsx b/src/components/welcome/WelcomeLogin/WalletLogin.tsx index cec4771cea..97a5d78e01 100644 --- a/src/components/welcome/WelcomeLogin/WalletLogin.tsx +++ b/src/components/welcome/WelcomeLogin/WalletLogin.tsx @@ -1,6 +1,5 @@ import useConnectWallet from '@/components/common/ConnectWallet/useConnectWallet' import useWallet from '@/hooks/wallets/useWallet' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' import { Box, Button, Typography } from '@mui/material' import EthHashInfo from '@/components/common/EthHashInfo' import WalletIcon from '@/components/common/WalletIcon' @@ -14,9 +13,7 @@ const WalletLogin = ({ onLogin }: { onLogin: () => void }) => { onLogin() } - const isSocialLogin = isSocialLoginWallet(wallet?.label) - - if (wallet !== null && !isSocialLogin) { + if (wallet !== null) { return ( <Box sx={{ width: '100%' }}> <Button variant="contained" sx={{ padding: '8px 16px' }} fullWidth onClick={onLogin}> diff --git a/src/components/welcome/WelcomeLogin/index.tsx b/src/components/welcome/WelcomeLogin/index.tsx index 910e573302..7be3ea5c78 100644 --- a/src/components/welcome/WelcomeLogin/index.tsx +++ b/src/components/welcome/WelcomeLogin/index.tsx @@ -1,9 +1,6 @@ import { AppRoutes } from '@/config/routes' -import { useHasFeature } from '@/hooks/useChains' -import { FEATURES } from '@/utils/chains' -import { Paper, SvgIcon, Typography, Divider, Box, Skeleton, Button, Link } from '@mui/material' +import { Paper, SvgIcon, Typography, Divider, Box, Button, Link } from '@mui/material' import SafeLogo from '@/public/images/logo-text.svg' -import dynamic from 'next/dynamic' import css from './styles.module.css' import { useRouter } from 'next/router' import { CREATE_SAFE_EVENTS } from '@/services/analytics/events/createLoadSafe' @@ -14,14 +11,9 @@ import Track from '@/components/common/Track' import { useCallback, useEffect, useState } from 'react' import WalletLogin from './WalletLogin' -const SocialSigner = dynamic(() => import('@/components/common/SocialSigner'), { - loading: () => <Skeleton variant="rounded" height={42} width="100%" />, -}) - const WelcomeLogin = () => { const router = useRouter() const wallet = useWallet() - const isSocialLoginEnabled = useHasFeature(FEATURES.SOCIAL_LOGIN) const { isLoaded, hasSafes } = useHasSafes() const [shouldRedirect, setShouldRedirect] = useState(false) @@ -45,7 +37,11 @@ const WelcomeLogin = () => { return ( <Paper className={css.loginCard} data-testid="welcome-login"> <Box className={css.loginContent}> - <SvgIcon component={SafeLogo} inheritViewBox sx={{ height: '24px', width: '80px', ml: '-8px' }} /> + <SvgIcon + component={SafeLogo} + inheritViewBox + sx={{ height: '24px', width: '80px', ml: '-8px', fill: 'none !important' }} + /> <Typography variant="h6" mt={6} fontWeight={700}> Get started @@ -61,18 +57,6 @@ const WelcomeLogin = () => { <WalletLogin onLogin={onLogin} /> </Track> - {isSocialLoginEnabled && ( - <> - <Divider sx={{ mt: 2, mb: 2, width: '100%' }}> - <Typography color="text.secondary" fontWeight={700} variant="overline"> - or - </Typography> - </Divider> - - <SocialSigner onLogin={onLogin} /> - </> - )} - {!wallet && ( <> <Divider sx={{ mt: 2, mb: 2, width: '100%' }}> diff --git a/src/config/constants.ts b/src/config/constants.ts index 9bdd5463f7..60a9eb1f8f 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -65,15 +65,6 @@ export enum SafeAppsTag { ONRAMP = 'onramp', } -export const WC_APP_PROD = { - id: 5, - url: 'https://apps.safe.protofire.io/wallet-connect', -} -export const WC_APP_DEV = { - id: 5, - url: 'https://dev-apps.safe.protofire.io/wallet-connect', -} - // Help Center export const HELP_CENTER_URL = 'https://help.safe.global' export const HelpCenterArticle = { @@ -94,6 +85,7 @@ export const HelpCenterArticle = { UNEXPECTED_DELEGATE_CALL: `${HELP_CENTER_URL}/en/articles/40794-why-do-i-see-an-unexpected-delegate-call-warning-in-my-transaction`, DELEGATES: `${HELP_CENTER_URL}/en/articles/40799-what-is-a-delegate-key`, PUSH_NOTIFICATIONS: `${HELP_CENTER_URL}/en/articles/99197-how-to-start-receiving-web-push-notifications-in-the-web-wallet`, + SWAP_WIDGET_FEES: `${HELP_CENTER_URL}/en/articles/178530-how-does-the-widget-fee-work-for-native-swaps`, } as const export const HelperCenterArticleTitles = { RECOVERY: 'Learn more about the Account recovery process', diff --git a/src/config/routes.ts b/src/config/routes.ts index 1c2a1ad022..11c78d471d 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -1,13 +1,14 @@ export const AppRoutes = { '404': '/404', + '403': '/403', wc: '/wc', terms: '/terms', + swap: '/swap', privacy: '/privacy', licenses: '/licenses', index: '/', imprint: '/imprint', home: '/home', - swap: '/swap', cookie: '/cookie', addressBook: '/address-book', addOwner: '/addOwner', @@ -28,7 +29,7 @@ export const AppRoutes = { }, settings: { setup: '/settings/setup', - securityLogin: '/settings/security-login', + security: '/settings/security', notifications: '/settings/notifications', modules: '/settings/modules', index: '/settings', @@ -45,13 +46,13 @@ export const AppRoutes = { }, transactions: { tx: '/transactions/tx', + msg: '/transactions/msg', queue: '/transactions/queue', messages: '/transactions/messages', index: '/transactions', history: '/transactions/history', }, welcome: { - socialLogin: '/welcome/social-login', index: '/welcome', accounts: '/welcome/accounts', }, diff --git a/src/features/counterfactual/ActivateAccountButton.tsx b/src/features/counterfactual/ActivateAccountButton.tsx index 5ffcd890e1..0c84bb8d3c 100644 --- a/src/features/counterfactual/ActivateAccountButton.tsx +++ b/src/features/counterfactual/ActivateAccountButton.tsx @@ -24,7 +24,13 @@ const ActivateAccountButton = () => { return ( <Tooltip title={isProcessing ? 'The safe activation is already in process' : undefined}> <span> - <Button variant="contained" size="small" onClick={activateAccount} disabled={isProcessing}> + <Button + data-testid="activate-account-btn" + variant="contained" + size="small" + onClick={activateAccount} + disabled={isProcessing} + > {isProcessing ? ( <> <Typography variant="body2" component="span" mr={1}> diff --git a/src/features/counterfactual/ActivateAccountFlow.tsx b/src/features/counterfactual/ActivateAccountFlow.tsx index 97f55caa0f..4de9d8883a 100644 --- a/src/features/counterfactual/ActivateAccountFlow.tsx +++ b/src/features/counterfactual/ActivateAccountFlow.tsx @@ -24,13 +24,12 @@ import { useWeb3 } from '@/hooks/wallets/web3' import { OVERVIEW_EVENTS, trackEvent, WALLET_EVENTS } from '@/services/analytics' import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' import { asError } from '@/services/exceptions/utils' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' import { useAppSelector } from '@/store' import { hasFeature } from '@/utils/chains' import { hasRemainingRelays } from '@/utils/relaying' import { Box, Button, CircularProgress, Divider, Grid, Typography } from '@mui/material' import type { DeploySafeProps } from '@safe-global/protocol-kit' -import { FEATURES } from '@safe-global/safe-gateway-typescript-sdk' +import { FEATURES } from '@/utils/chains' import React, { useContext, useState } from 'react' const useActivateAccount = () => { @@ -89,7 +88,7 @@ const ActivateAccountFlow = () => { trackEvent(WALLET_EVENTS.ONCHAIN_INTERACTION) if (txHash) { - safeCreationDispatch(SafeCreationEvent.PROCESSING, { groupKey: CF_TX_GROUP_KEY, txHash }) + safeCreationDispatch(SafeCreationEvent.PROCESSING, { groupKey: CF_TX_GROUP_KEY, txHash, safeAddress }) } setTxFlow(undefined) } @@ -105,7 +104,7 @@ const ActivateAccountFlow = () => { try { if (willRelay) { const taskId = await relaySafeCreation(chain, owners, threshold, Number(saltNonce!), safeVersion) - safeCreationDispatch(SafeCreationEvent.RELAYING, { groupKey: CF_TX_GROUP_KEY, taskId }) + safeCreationDispatch(SafeCreationEvent.RELAYING, { groupKey: CF_TX_GROUP_KEY, taskId, safeAddress }) onSubmit() } else { @@ -129,7 +128,6 @@ const ActivateAccountFlow = () => { } const submitDisabled = !isSubmittable - const isSocialLogin = isSocialLoginWallet(wallet?.label) return ( <TxLayout title="Activate account" hideNonce> @@ -145,7 +143,7 @@ const ActivateAccountFlow = () => { <Divider sx={{ mx: -3, mt: 2, mb: 1 }} /> <Box display="flex" flexDirection="column" gap={3}> - {canRelay && !isSocialLogin && ( + {canRelay && ( <Grid container spacing={3}> <ReviewRow name="Execution method" @@ -167,7 +165,7 @@ const ActivateAccountFlow = () => { <> <NetworkFee totalFee={totalFee} willRelay={willRelay} chain={chain} /> - {!willRelay && !isSocialLogin && ( + {!willRelay && ( <Typography variant="body2" color="text.secondary" mt={1}> You will have to confirm a transaction with your connected wallet. </Typography> @@ -195,7 +193,13 @@ const ActivateAccountFlow = () => { <Divider sx={{ mx: -3, mt: 2, mb: 1 }} /> <Box display="flex" flexDirection="row" justifyContent="flex-end" gap={3}> - <Button onClick={createSafe} variant="contained" size="stretched" disabled={submitDisabled}> + <Button + data-testid="activate-account-btn" + onClick={createSafe} + variant="contained" + size="stretched" + disabled={submitDisabled} + > {!isSubmittable ? <CircularProgress size={20} /> : 'Activate'} </Button> </Box> diff --git a/src/features/counterfactual/CheckBalance.tsx b/src/features/counterfactual/CheckBalance.tsx index 1cc5cc9018..6424ea57ee 100644 --- a/src/features/counterfactual/CheckBalance.tsx +++ b/src/features/counterfactual/CheckBalance.tsx @@ -16,7 +16,12 @@ const CheckBalance = () => { const blockExplorerLink = chain ? getBlockExplorerLink(chain, safeAddress) : undefined return ( - <Alert icon={false} severity="info" sx={{ display: 'flex', maxWidth: '600px', mt: 3, px: 3, py: 2, mx: 'auto' }}> + <Alert + data-testid="no-tokens-alert" + icon={false} + severity="info" + sx={{ display: 'flex', maxWidth: '600px', mt: 3, px: 3, py: 2, mx: 'auto' }} + > <Typography fontWeight="bold" mb={1}> Don't see your tokens? </Typography> diff --git a/src/features/counterfactual/CounterfactualForm.tsx b/src/features/counterfactual/CounterfactualForm.tsx index 8f613d0b4e..cf20d005e5 100644 --- a/src/features/counterfactual/CounterfactualForm.tsx +++ b/src/features/counterfactual/CounterfactualForm.tsx @@ -3,11 +3,13 @@ import useDeployGasLimit from '@/features/counterfactual/hooks/useDeployGasLimit import { deploySafeAndExecuteTx } from '@/features/counterfactual/utils' import useChainId from '@/hooks/useChainId' import { getTotalFeeFormatted } from '@/hooks/useGasPrice' +import useSafeInfo from '@/hooks/useSafeInfo' import useWalletCanPay from '@/hooks/useWalletCanPay' import useOnboard from '@/hooks/wallets/useOnboard' import useWallet from '@/hooks/wallets/useWallet' import { OVERVIEW_EVENTS, trackEvent, WALLET_EVENTS } from '@/services/analytics' import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import madProps from '@/utils/mad-props' import React, { type ReactElement, type SyntheticEvent, useContext, useState } from 'react' import { CircularProgress, Box, Button, CardActions, Divider, Alert } from '@mui/material' @@ -49,6 +51,7 @@ export const CounterfactualForm = ({ const onboard = useOnboard() const chain = useCurrentChain() const chainId = useChainId() + const { safeAddress } = useSafeInfo() // Form state const [isSubmittable, setIsSubmittable] = useState<boolean>(true) @@ -81,7 +84,8 @@ export const CounterfactualForm = ({ try { trackEvent({ ...OVERVIEW_EVENTS.PROCEED_WITH_TX, label: TX_TYPES.activate_with_tx }) - await deploySafeAndExecuteTx(txOptions, chainId, wallet, safeTx, onboard) + onboard && (await assertWalletChain(onboard, chainId)) + await deploySafeAndExecuteTx(txOptions, wallet, safeAddress, safeTx, wallet?.provider) trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.activate_with_tx }) trackEvent({ ...TX_EVENTS.EXECUTE, label: TX_TYPES.activate_with_tx }) diff --git a/src/features/counterfactual/CounterfactualHooks.tsx b/src/features/counterfactual/CounterfactualHooks.tsx index 0eae26fa8e..d6be915487 100644 --- a/src/features/counterfactual/CounterfactualHooks.tsx +++ b/src/features/counterfactual/CounterfactualHooks.tsx @@ -1,16 +1,13 @@ import CounterfactualSuccessScreen from '@/features/counterfactual/CounterfactualSuccessScreen' import dynamic from 'next/dynamic' -import useIsCounterfactualSafe from '@/features/counterfactual/hooks/useIsCounterfactualSafe' const LazyCounterfactual = dynamic(() => import('./LazyCounterfactual')) function CounterfactualHooks() { - const isCounterfactualSafe = useIsCounterfactualSafe() - return ( <> <CounterfactualSuccessScreen /> - {isCounterfactualSafe && <LazyCounterfactual />} + <LazyCounterfactual /> </> ) } diff --git a/src/features/counterfactual/CounterfactualStatusButton.tsx b/src/features/counterfactual/CounterfactualStatusButton.tsx index e4440e38e7..1d09f7aa11 100644 --- a/src/features/counterfactual/CounterfactualStatusButton.tsx +++ b/src/features/counterfactual/CounterfactualStatusButton.tsx @@ -1,4 +1,4 @@ -import { PendingSafeStatus, selectUndeployedSafe } from '@/features/counterfactual/store/undeployedSafesSlice' +import { selectUndeployedSafe } from '@/features/counterfactual/store/undeployedSafesSlice' import useSafeInfo from '@/hooks/useSafeInfo' import InfoIcon from '@/public/images/notifications/info.svg' import { useAppSelector } from '@/store' @@ -27,30 +27,28 @@ export const LoopIcon = (props: SvgIconProps) => { ) } -const processingStates = [PendingSafeStatus.PROCESSING, PendingSafeStatus.RELAYING] - const CounterfactualStatusButton = () => { const { safe, safeAddress } = useSafeInfo() const undeployedSafe = useAppSelector((state) => selectUndeployedSafe(state, safe.chainId, safeAddress)) if (safe.deployed) return null - const processing = undeployedSafe && processingStates.includes(undeployedSafe.status.status) + const isActivating = undeployedSafe?.status.status !== 'AWAITING_EXECUTION' return ( <Tooltip placement="right" - title={processing ? 'Safe Account is being activated' : 'Safe Account is not activated'} + title={isActivating ? 'Safe Account is being activated' : 'Safe Account is not activated'} arrow > <IconButton data-testid="pending-activation-icon" - className={classnames(css.statusButton, { [css.processing]: processing })} + className={classnames(css.statusButton, { [css.processing]: isActivating })} size="small" - color={processing ? 'info' : 'warning'} + color={isActivating ? 'info' : 'warning'} disableRipple > - {processing ? <LoopIcon /> : <InfoIcon />} + {isActivating ? <LoopIcon /> : <InfoIcon />} </IconButton> </Tooltip> ) diff --git a/src/features/counterfactual/CounterfactualSuccessScreen.tsx b/src/features/counterfactual/CounterfactualSuccessScreen.tsx index bd5312f0de..f798e5b99e 100644 --- a/src/features/counterfactual/CounterfactualSuccessScreen.tsx +++ b/src/features/counterfactual/CounterfactualSuccessScreen.tsx @@ -1,16 +1,23 @@ +import EthHashInfo from '@/components/common/EthHashInfo' import { safeCreationPendingStatuses } from '@/features/counterfactual/hooks/usePendingSafeStatuses' import { SafeCreationEvent, safeCreationSubscribe } from '@/features/counterfactual/services/safeCreationEvents' +import { useCurrentChain } from '@/hooks/useChains' import { useEffect, useState } from 'react' import { Box, Button, Dialog, DialogContent, Typography } from '@mui/material' import CheckRoundedIcon from '@mui/icons-material/CheckRounded' const CounterfactualSuccessScreen = () => { const [open, setOpen] = useState<boolean>(false) + const [safeAddress, setSafeAddress] = useState<string>() + const chain = useCurrentChain() useEffect(() => { const unsubFns = Object.entries(safeCreationPendingStatuses).map(([event]) => - safeCreationSubscribe(event as SafeCreationEvent, async () => { - if (event === SafeCreationEvent.INDEXED) setOpen(true) + safeCreationSubscribe(event as SafeCreationEvent, async (detail) => { + if (event === SafeCreationEvent.INDEXED) { + setSafeAddress(detail.safeAddress) + setOpen(true) + } }), ) @@ -42,17 +49,23 @@ const CounterfactualSuccessScreen = () => { > <CheckRoundedIcon sx={{ width: 50, height: 50 }} color="success" /> </Box> + <Box textAlign="center"> <Typography variant="h3" fontWeight="bold" mb={1}> - Account is activated! - </Typography> - <Typography> - Your Safe Account was successfully deployed on chain. You can continue making improvements to your account - setup and security. + Your account is all set! </Typography> + <Typography>Start your journey to the smart account security now.</Typography> + <Typography>Use your address to receive funds {chain?.chainName && `on ${chain.chainName}`}.</Typography> </Box> + + {safeAddress && ( + <Box p={2} bgcolor="background.main" borderRadius={1} fontSize={14}> + <EthHashInfo address={safeAddress} shortAddress={false} showCopyButton avatarSize={32} /> + </Box> + )} + <Button variant="contained" onClick={() => setOpen(false)}> - Continue + Let's go </Button> </DialogContent> </Dialog> diff --git a/src/features/counterfactual/FirstTxFlow.tsx b/src/features/counterfactual/FirstTxFlow.tsx index c20fa8498d..a30fa14218 100644 --- a/src/features/counterfactual/FirstTxFlow.tsx +++ b/src/features/counterfactual/FirstTxFlow.tsx @@ -18,9 +18,7 @@ import RecoveryPlus from '@/public/images/common/recovery-plus.svg' import SwapIcon from '@/public/images/common/swap.svg' import SafeLogo from '@/public/images/logo-no-text.svg' import HandymanOutlinedIcon from '@mui/icons-material/HandymanOutlined' -import { useHasFeature } from '@/hooks/useChains' -import { FEATURES } from '@/utils/chains' -import useIsCounterfactualSafe from '@/features/counterfactual/hooks/useIsCounterfactualSafe' +import useIsSwapFeatureEnabled from '../swap/hooks/useIsSwapFeatureEnabled' const FirstTxFlow = ({ open, onClose }: { open: boolean; onClose: () => void }) => { const txBuilder = useTxBuilderApp() @@ -28,8 +26,7 @@ const FirstTxFlow = ({ open, onClose }: { open: boolean; onClose: () => void }) const { setTxFlow } = useContext(TxModalContext) const supportsRecovery = useIsRecoverySupported() const [recovery] = useRecovery() - const isCounterfactualSafe = useIsCounterfactualSafe() - const isSwapFeatureEnabled = useHasFeature(FEATURES.NATIVE_SWAPS) && !isCounterfactualSafe + const isSwapFeatureEnabled = useIsSwapFeatureEnabled() const handleClick = (onClick: () => void) => { onClose() diff --git a/src/features/counterfactual/__tests__/useDeployGasLimit.test.ts b/src/features/counterfactual/__tests__/useDeployGasLimit.test.ts index 6f6c7005d9..0579fc2a6f 100644 --- a/src/features/counterfactual/__tests__/useDeployGasLimit.test.ts +++ b/src/features/counterfactual/__tests__/useDeployGasLimit.test.ts @@ -1,5 +1,7 @@ import useDeployGasLimit from '@/features/counterfactual/hooks/useDeployGasLimit' +import type { ConnectedWallet } from '@/hooks/wallets/useOnboard' import * as onboard from '@/hooks/wallets/useOnboard' +import * as useWallet from '@/hooks/wallets/useWallet' import * as sdk from '@/services/tx/tx-sender/sdk' import { safeTxBuilder } from '@/tests/builders/safeTx' import * as protocolKit from '@safe-global/protocol-kit' @@ -13,6 +15,13 @@ import { faker } from '@faker-js/faker' import type { CompatibilityFallbackHandlerContract, SimulateTxAccessorContract } from '@safe-global/safe-core-sdk-types' describe('useDeployGasLimit hook', () => { + beforeEach(() => { + jest.resetAllMocks() + + jest.spyOn(useWallet, 'default').mockReturnValue({} as ConnectedWallet) + jest.spyOn(sdk, 'assertWalletChain').mockImplementation(jest.fn()) + }) + it('returns undefined in onboard is not initialized', () => { jest.spyOn(onboard, 'default').mockReturnValue(undefined) const { result } = renderHook(() => useDeployGasLimit()) @@ -20,6 +29,13 @@ describe('useDeployGasLimit hook', () => { expect(result.current.gasLimit).toBeUndefined() }) + it('returns undefined in there is no wallet connected', () => { + jest.spyOn(useWallet, 'default').mockReturnValue(null) + const { result } = renderHook(() => useDeployGasLimit()) + + expect(result.current.gasLimit).toBeUndefined() + }) + it('returns safe deployment gas estimation', async () => { const mockGas = '100' const mockOnboard = {} as OnboardAPI diff --git a/src/features/counterfactual/hooks/useDeployGasLimit.ts b/src/features/counterfactual/hooks/useDeployGasLimit.ts index 499adb9232..45e4061008 100644 --- a/src/features/counterfactual/hooks/useDeployGasLimit.ts +++ b/src/features/counterfactual/hooks/useDeployGasLimit.ts @@ -1,7 +1,8 @@ import useAsync from '@/hooks/useAsync' import useChainId from '@/hooks/useChainId' import useOnboard from '@/hooks/wallets/useOnboard' -import { getSafeSDKWithSigner } from '@/services/tx/tx-sender/sdk' +import useWallet from '@/hooks/wallets/useWallet' +import { assertWalletChain, getSafeSDKWithSigner } from '@/services/tx/tx-sender/sdk' import { estimateSafeDeploymentGas, estimateTxBaseGas } from '@safe-global/protocol-kit' import type Safe from '@safe-global/protocol-kit' @@ -21,11 +22,14 @@ type DeployGasLimitProps = { const useDeployGasLimit = (safeTx?: SafeTransaction) => { const onboard = useOnboard() + const wallet = useWallet() const chainId = useChainId() const [gasLimit, gasLimitError, gasLimitLoading] = useAsync<DeployGasLimitProps | undefined>(async () => { - if (!onboard) return - const sdk = await getSafeSDKWithSigner(onboard, chainId) + if (!wallet || !onboard) return + + await assertWalletChain(onboard, chainId) + const sdk = await getSafeSDKWithSigner(wallet.provider) const [baseGas, batchTxGas, safeDeploymentGas] = await Promise.all([ safeTx ? estimateTxBaseGas(sdk, safeTx) : '0', @@ -37,7 +41,7 @@ const useDeployGasLimit = (safeTx?: SafeTransaction) => { const safeTxGas = totalGas - BigInt(safeDeploymentGas) return { safeTxGas, safeDeploymentGas, totalGas } - }, [onboard, chainId, safeTx]) + }, [onboard, wallet, chainId, safeTx]) return { gasLimit, gasLimitError, gasLimitLoading } } diff --git a/src/features/counterfactual/hooks/usePendingSafeStatuses.ts b/src/features/counterfactual/hooks/usePendingSafeStatuses.ts index 783dc89244..d9f6077734 100644 --- a/src/features/counterfactual/hooks/usePendingSafeStatuses.ts +++ b/src/features/counterfactual/hooks/usePendingSafeStatuses.ts @@ -7,7 +7,7 @@ import { import { PendingSafeStatus, removeUndeployedSafe, - selectUndeployedSafe, + selectUndeployedSafes, updateUndeployedSafeStatus, } from '@/features/counterfactual/store/undeployedSafesSlice' import { checkSafeActionViaRelay, checkSafeActivation } from '@/features/counterfactual/utils' @@ -28,9 +28,7 @@ export const safeCreationPendingStatuses: Partial<Record<SafeCreationEvent, Pend } const usePendingSafeMonitor = (): void => { - const chainId = useChainId() - const { safeAddress } = useSafeInfo() - const undeployedSafe = useAppSelector((state) => selectUndeployedSafe(state, chainId, safeAddress)) + const undeployedSafesByChain = useAppSelector(selectUndeployedSafes) const provider = useWeb3ReadOnly() const dispatch = useAppDispatch() @@ -39,43 +37,48 @@ const usePendingSafeMonitor = (): void => { // Monitor pending safe creation mining/validating progress useEffect(() => { - if (undeployedSafe?.status.status === PendingSafeStatus.AWAITING_EXECUTION) { - monitoredSafes.current[safeAddress] = false - } + Object.entries(undeployedSafesByChain).forEach(([chainId, undeployedSafes]) => { + Object.entries(undeployedSafes).forEach(([safeAddress, undeployedSafe]) => { + if (undeployedSafe?.status.status === PendingSafeStatus.AWAITING_EXECUTION) { + monitoredSafes.current[safeAddress] = false + } - if (!provider || !undeployedSafe || undeployedSafe.status.status === PendingSafeStatus.AWAITING_EXECUTION) { - return - } + if (!provider || !undeployedSafe || undeployedSafe.status.status === PendingSafeStatus.AWAITING_EXECUTION) { + return + } - const monitorPendingSafe = async () => { - const { - status: { status, txHash, taskId, startBlock }, - } = undeployedSafe + const monitorPendingSafe = async () => { + const { + status: { status, txHash, taskId, startBlock }, + } = undeployedSafe - const isProcessing = status === PendingSafeStatus.PROCESSING && txHash !== undefined - const isRelaying = status === PendingSafeStatus.RELAYING && taskId !== undefined - const isMonitored = monitoredSafes.current[safeAddress] + const isProcessing = status === PendingSafeStatus.PROCESSING && txHash !== undefined + const isRelaying = status === PendingSafeStatus.RELAYING && taskId !== undefined + const isMonitored = monitoredSafes.current[safeAddress] - if ((!isProcessing && !isRelaying) || isMonitored) return + if ((!isProcessing && !isRelaying) || isMonitored) return - monitoredSafes.current[safeAddress] = true + monitoredSafes.current[safeAddress] = true - if (isProcessing) { - checkSafeActivation(provider, txHash, safeAddress, startBlock) - } + if (isProcessing) { + checkSafeActivation(provider, txHash, safeAddress, startBlock) + } - if (isRelaying) { - checkSafeActionViaRelay(taskId, safeAddress) - } - } + if (isRelaying) { + checkSafeActionViaRelay(taskId, safeAddress) + } + } - monitorPendingSafe() - }, [dispatch, provider, safeAddress, undeployedSafe]) + monitorPendingSafe() + }) + }) + }, [dispatch, provider, undeployedSafesByChain]) } const usePendingSafeStatus = (): void => { const dispatch = useAppDispatch() const { safe, safeAddress } = useSafeInfo() + const chainId = useChainId() const provider = useWeb3ReadOnly() usePendingSafeMonitor() @@ -106,22 +109,30 @@ const usePendingSafeStatus = (): void => { if (event === SafeCreationEvent.SUCCESS) { // TODO: Possible to add a label with_tx, without_tx? trackEvent(CREATE_SAFE_EVENTS.ACTIVATED_SAFE) - pollSafeInfo(safe.chainId, safeAddress).finally(() => { - safeCreationDispatch(SafeCreationEvent.INDEXED, { groupKey: detail.groupKey, safeAddress }) + pollSafeInfo(chainId, detail.safeAddress).finally(() => { + safeCreationDispatch(SafeCreationEvent.INDEXED, { + groupKey: detail.groupKey, + safeAddress: detail.safeAddress, + }) }) return } if (event === SafeCreationEvent.INDEXED) { - dispatch(removeUndeployedSafe({ chainId: safe.chainId, address: safeAddress })) + dispatch(removeUndeployedSafe({ chainId, address: detail.safeAddress })) } if (status === null) { dispatch( updateUndeployedSafeStatus({ - chainId: safe.chainId, - address: safeAddress, - status: { status: PendingSafeStatus.AWAITING_EXECUTION }, + chainId, + address: detail.safeAddress, + status: { + status: PendingSafeStatus.AWAITING_EXECUTION, + startBlock: undefined, + txHash: undefined, + submittedAt: undefined, + }, }), ) return @@ -129,13 +140,14 @@ const usePendingSafeStatus = (): void => { dispatch( updateUndeployedSafeStatus({ - chainId: safe.chainId, - address: safeAddress, + chainId, + address: detail.safeAddress, status: { status, txHash: 'txHash' in detail ? detail.txHash : undefined, taskId: 'taskId' in detail ? detail.taskId : undefined, startBlock: await provider?.getBlockNumber(), + submittedAt: Date.now(), }, }), ) @@ -145,7 +157,7 @@ const usePendingSafeStatus = (): void => { return () => { unsubFns.forEach((unsub) => unsub()) } - }, [safe.chainId, dispatch, safeAddress, provider]) + }, [chainId, dispatch, provider]) } export default usePendingSafeStatus diff --git a/src/features/counterfactual/services/safeCreationEvents.ts b/src/features/counterfactual/services/safeCreationEvents.ts index 29e92c40e1..883f601562 100644 --- a/src/features/counterfactual/services/safeCreationEvents.ts +++ b/src/features/counterfactual/services/safeCreationEvents.ts @@ -13,10 +13,12 @@ export interface SafeCreationEvents { [SafeCreationEvent.PROCESSING]: { groupKey: string txHash: string + safeAddress: string } [SafeCreationEvent.RELAYING]: { groupKey: string taskId: string + safeAddress: string } [SafeCreationEvent.SUCCESS]: { groupKey: string @@ -29,10 +31,12 @@ export interface SafeCreationEvents { [SafeCreationEvent.FAILED]: { groupKey: string error: Error + safeAddress: string } [SafeCreationEvent.REVERTED]: { groupKey: string error: Error + safeAddress: string } } diff --git a/src/features/counterfactual/store/undeployedSafesSlice.ts b/src/features/counterfactual/store/undeployedSafesSlice.ts index 40b1b6bdb1..0196052e95 100644 --- a/src/features/counterfactual/store/undeployedSafesSlice.ts +++ b/src/features/counterfactual/store/undeployedSafesSlice.ts @@ -1,3 +1,4 @@ +import type { PayMethod } from '@/features/counterfactual/PayNowPayLater' import { type RootState } from '@/store' import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit' import type { PredictedSafeProps } from '@safe-global/protocol-kit' @@ -10,9 +11,13 @@ export enum PendingSafeStatus { type UndeployedSafeStatus = { status: PendingSafeStatus + type: PayMethod txHash?: string taskId?: string startBlock?: number + submittedAt?: number + signerAddress?: string + signerNonce?: number | null } export type UndeployedSafe = { @@ -32,9 +37,9 @@ export const undeployedSafesSlice = createSlice({ reducers: { addUndeployedSafe: ( state, - action: PayloadAction<{ chainId: string; address: string; safeProps: PredictedSafeProps }>, + action: PayloadAction<{ chainId: string; address: string; type: PayMethod; safeProps: PredictedSafeProps }>, ) => { - const { chainId, address, safeProps } = action.payload + const { chainId, address, type, safeProps } = action.payload if (!state[chainId]) { state[chainId] = {} @@ -44,13 +49,14 @@ export const undeployedSafesSlice = createSlice({ props: safeProps, status: { status: PendingSafeStatus.AWAITING_EXECUTION, + type, }, } }, updateUndeployedSafeStatus: ( state, - action: PayloadAction<{ chainId: string; address: string; status: UndeployedSafeStatus }>, + action: PayloadAction<{ chainId: string; address: string; status: Omit<UndeployedSafeStatus, 'type'> }>, ) => { const { chainId, address, status } = action.payload @@ -58,7 +64,10 @@ export const undeployedSafesSlice = createSlice({ state[chainId][address] = { props: state[chainId][address].props, - status, + status: { + ...state[chainId][address].status, + ...status, + }, } }, diff --git a/src/features/counterfactual/utils.ts b/src/features/counterfactual/utils.ts index 3260aef775..8b3a16d6d6 100644 --- a/src/features/counterfactual/utils.ts +++ b/src/features/counterfactual/utils.ts @@ -1,21 +1,21 @@ import type { NewSafeFormData } from '@/components/new-safe/create' -import { CREATION_MODAL_QUERY_PARM } from '@/components/new-safe/create/logic' import { LATEST_SAFE_VERSION, POLLING_INTERVAL } from '@/config/constants' import { AppRoutes } from '@/config/routes' +import { PayMethod } from '@/features/counterfactual/PayNowPayLater' import { safeCreationDispatch, SafeCreationEvent } from '@/features/counterfactual/services/safeCreationEvents' import { addUndeployedSafe } from '@/features/counterfactual/store/undeployedSafesSlice' import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' import { createWeb3, getWeb3ReadOnly } from '@/hooks/wallets/web3' import { asError } from '@/services/exceptions/utils' import ExternalStore from '@/services/ExternalStore' -import { assertWalletChain, getUncheckedSafeSDK, tryOffChainTxSigning } from '@/services/tx/tx-sender/sdk' +import { getUncheckedSafeSDK, tryOffChainTxSigning } from '@/services/tx/tx-sender/sdk' import { getRelayTxStatus, TaskState } from '@/services/tx/txMonitor' import type { AppDispatch } from '@/store' import { addOrUpdateSafe } from '@/store/addedSafesSlice' import { upsertAddressBookEntry } from '@/store/addressBookSlice' import { defaultSafeInfo } from '@/store/safeInfoSlice' import { didRevert, type EthersError } from '@/utils/ethers-utils' -import { assertOnboard, assertTx, assertWallet } from '@/utils/helpers' +import { assertProvider, assertTx, assertWallet } from '@/utils/helpers' import type { DeploySafeProps, PredictedSafeProps } from '@safe-global/protocol-kit' import { ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants' import type { SafeTransaction, SafeVersion, TransactionOptions } from '@safe-global/safe-core-sdk-types' @@ -23,11 +23,9 @@ import { type ChainInfo, ImplementationVersionState, type SafeBalanceResponse, - type SafeInfo, TokenType, } from '@safe-global/safe-gateway-typescript-sdk' -import type { OnboardAPI } from '@web3-onboard/core' -import type { BrowserProvider, ContractTransactionResponse, Provider } from 'ethers' +import type { BrowserProvider, ContractTransactionResponse, Eip1193Provider, Provider } from 'ethers' import type { NextRouter } from 'next/router' export const getUndeployedSafeInfo = (undeployedSafe: PredictedSafeProps, address: string, chainId: string) => { @@ -50,19 +48,18 @@ export const CF_TX_GROUP_KEY = 'cf-tx' export const dispatchTxExecutionAndDeploySafe = async ( safeTx: SafeTransaction, txOptions: TransactionOptions, - onboard: OnboardAPI, - chainId: SafeInfo['chainId'], + provider: Eip1193Provider, + safeAddress: string, ) => { - const sdkUnchecked = await getUncheckedSafeSDK(onboard, chainId) + const sdkUnchecked = await getUncheckedSafeSDK(provider) const eventParams = { groupKey: CF_TX_GROUP_KEY } let result: ContractTransactionResponse | undefined try { const signedTx = await tryOffChainTxSigning(safeTx, await sdkUnchecked.getContractVersion(), sdkUnchecked) - const wallet = await assertWalletChain(onboard, chainId) - const provider = createWeb3(wallet.provider) - const signer = await provider.getSigner() + const browserProvider = createWeb3(provider) + const signer = await browserProvider.getSigner() const deploymentTx = await sdkUnchecked.wrapSafeTransactionIntoDeploymentBatch(signedTx, txOptions) @@ -72,27 +69,27 @@ export const dispatchTxExecutionAndDeploySafe = async ( // @ts-ignore TODO: Check why TransactionResponse type doesn't work result = await signer.sendTransaction({ ...deploymentTx, gasLimit: gas }) } catch (error) { - safeCreationDispatch(SafeCreationEvent.FAILED, { ...eventParams, error: asError(error) }) + safeCreationDispatch(SafeCreationEvent.FAILED, { ...eventParams, error: asError(error), safeAddress }) throw error } - safeCreationDispatch(SafeCreationEvent.PROCESSING, { ...eventParams, txHash: result!.hash }) + safeCreationDispatch(SafeCreationEvent.PROCESSING, { ...eventParams, txHash: result!.hash, safeAddress }) return result!.hash } export const deploySafeAndExecuteTx = async ( txOptions: TransactionOptions, - chainId: string, wallet: ConnectedWallet | null, + safeAddress: string, safeTx?: SafeTransaction, - onboard?: OnboardAPI, + provider?: Eip1193Provider, ) => { assertTx(safeTx) assertWallet(wallet) - assertOnboard(onboard) + assertProvider(provider) - return dispatchTxExecutionAndDeploySafe(safeTx, txOptions, onboard, chainId) + return dispatchTxExecutionAndDeploySafe(safeTx, txOptions, provider, safeAddress) } export const { getStore: getNativeBalance, setStore: setNativeBalance } = new ExternalStore<bigint>(0n) @@ -147,6 +144,7 @@ export const createCounterfactualSafe = ( const undeployedSafe = { chainId: chain.chainId, address: safeAddress, + type: PayMethod.PayLater, safeProps: { safeAccountConfig: props.safeAccountConfig, safeDeploymentConfig: { @@ -174,7 +172,7 @@ export const createCounterfactualSafe = ( ) return router.push({ pathname: AppRoutes.home, - query: { safe: `${chain.shortName}:${safeAddress}`, [CREATION_MODAL_QUERY_PARM]: true }, + query: { safe: `${chain.shortName}:${safeAddress}` }, }) } @@ -202,20 +200,17 @@ async function retryGetTransaction(provider: Provider, txHash: string, maxAttemp throw new Error('Transaction not found') } -// TODO: Reuse this for safe creation flow instead of checkSafeCreationTx export const checkSafeActivation = async ( provider: Provider, txHash: string, safeAddress: string, startBlock?: number, ) => { - const TIMEOUT_TIME = 2 * 60 * 1000 // 2 minutes - try { const txResponse = await retryGetTransaction(provider, txHash) const replaceableTx = startBlock ? txResponse.replaceableTransaction(startBlock) : txResponse - const receipt = await replaceableTx?.wait(1, TIMEOUT_TIME) + const receipt = await replaceableTx?.wait(1) /** The receipt should always be non-null as we require 1 confirmation */ if (receipt === null) { @@ -226,6 +221,7 @@ export const checkSafeActivation = async ( safeCreationDispatch(SafeCreationEvent.REVERTED, { groupKey: CF_TX_GROUP_KEY, error: new Error('Transaction reverted'), + safeAddress, }) } @@ -244,14 +240,23 @@ export const checkSafeActivation = async ( return } + if (didRevert(_err.receipt)) { + safeCreationDispatch(SafeCreationEvent.REVERTED, { + groupKey: CF_TX_GROUP_KEY, + error: new Error('Transaction reverted'), + safeAddress, + }) + return + } + safeCreationDispatch(SafeCreationEvent.FAILED, { groupKey: CF_TX_GROUP_KEY, error: _err, + safeAddress, }) } } -// TODO: Reuse this for safe creation flow instead of waitForCreateSafeTx export const checkSafeActionViaRelay = (taskId: string, safeAddress: string) => { const TIMEOUT_TIME = 2 * 60 * 1000 // 2 minutes @@ -278,6 +283,7 @@ export const checkSafeActionViaRelay = (taskId: string, safeAddress: string) => safeCreationDispatch(SafeCreationEvent.FAILED, { groupKey: CF_TX_GROUP_KEY, error: new Error('Transaction failed'), + safeAddress, }) break default: @@ -293,6 +299,7 @@ export const checkSafeActionViaRelay = (taskId: string, safeAddress: string) => safeCreationDispatch(SafeCreationEvent.FAILED, { groupKey: CF_TX_GROUP_KEY, error: new Error('Transaction failed'), + safeAddress, }) clearInterval(intervalId) diff --git a/src/features/recovery/components/CancelRecoveryButton/index.tsx b/src/features/recovery/components/CancelRecoveryButton/index.tsx index 23c683311b..0bb427872c 100644 --- a/src/features/recovery/components/CancelRecoveryButton/index.tsx +++ b/src/features/recovery/components/CancelRecoveryButton/index.tsx @@ -1,5 +1,7 @@ +import useWallet from '@/hooks/wallets/useWallet' import { trackEvent } from '@/services/analytics' import { RECOVERY_EVENTS } from '@/services/analytics/events/recovery' +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import { Button } from '@mui/material' import { useContext } from 'react' import type { SyntheticEvent, ReactElement } from 'react' @@ -29,6 +31,7 @@ export function CancelRecoveryButton({ const { isExpired, isPending } = useRecoveryTxState(recovery) const { setTxFlow } = useContext(TxModalContext) const onboard = useOnboard() + const wallet = useWallet() const { safe } = useSafeInfo() const onClick = async (e: SyntheticEvent) => { @@ -38,13 +41,16 @@ export function CancelRecoveryButton({ trackEvent(RECOVERY_EVENTS.CANCEL_RECOVERY) if (isOwner) { setTxFlow(<CancelRecoveryFlow recovery={recovery} />) - } else if (onboard) { + } else if (onboard && wallet) { try { + await assertWalletChain(onboard, safe.chainId) + await dispatchRecoverySkipExpired({ - onboard, + provider: wallet.provider, chainId: safe.chainId, delayModifierAddress: recovery.address, recoveryTxHash: recovery.args.txHash, + signerAddress: wallet.address, }) } catch (_err) { const err = asError(_err) diff --git a/src/features/recovery/components/ExecuteRecoveryButton/index.tsx b/src/features/recovery/components/ExecuteRecoveryButton/index.tsx index 400b404a39..5b3662d2f4 100644 --- a/src/features/recovery/components/ExecuteRecoveryButton/index.tsx +++ b/src/features/recovery/components/ExecuteRecoveryButton/index.tsx @@ -1,3 +1,5 @@ +import useWallet from '@/hooks/wallets/useWallet' +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import { Button, Tooltip } from '@mui/material' import { useContext } from 'react' import type { SyntheticEvent, ReactElement } from 'react' @@ -22,22 +24,26 @@ export function ExecuteRecoveryButton({ const { setSubmitError } = useContext(RecoveryListItemContext) const { isExecutable, isNext, isPending } = useRecoveryTxState(recovery) const onboard = useOnboard() + const wallet = useWallet() const { safe } = useSafeInfo() const onClick = async (e: SyntheticEvent) => { e.stopPropagation() e.preventDefault() - if (!onboard) { + if (!onboard || !wallet) { return } try { + await assertWalletChain(onboard, safe.chainId) + await dispatchRecoveryExecution({ - onboard, + provider: wallet.provider, chainId: safe.chainId, args: recovery.args, delayModifierAddress: recovery.address, + signerAddress: wallet.address, }) } catch (_err) { const err = asError(_err) @@ -68,6 +74,7 @@ export function ExecuteRecoveryButton({ onClick={onClick} variant="contained" disabled={isDisabled} + sx={{ minWidth: '106.5px', py: compact ? 0.8 : undefined }} size={compact ? 'small' : 'stretched'} > Execute diff --git a/src/features/recovery/components/RecoveryContext/__tests__/useRecoveryState.test.tsx b/src/features/recovery/components/RecoveryContext/__tests__/useRecoveryState.test.tsx index 4296ddbd9a..55099f081d 100644 --- a/src/features/recovery/components/RecoveryContext/__tests__/useRecoveryState.test.tsx +++ b/src/features/recovery/components/RecoveryContext/__tests__/useRecoveryState.test.tsx @@ -25,7 +25,6 @@ jest.mock('@/hooks/useSafeInfo') jest.mock('@/hooks/wallets/web3') jest.mock('@/hooks/useChains') jest.mock('@/hooks/useTxHistory') -jest.mock('@/hooks/useChains') const mockUseSafeInfo = useSafeInfo as jest.MockedFunction<typeof useSafeInfo> const mockUseWeb3ReadOnly = useWeb3ReadOnly as jest.MockedFunction<typeof useWeb3ReadOnly> diff --git a/src/features/recovery/components/RecoverySettings/index.tsx b/src/features/recovery/components/RecoverySettings/index.tsx index fcbeb6a5ea..0aa340ddec 100644 --- a/src/features/recovery/components/RecoverySettings/index.tsx +++ b/src/features/recovery/components/RecoverySettings/index.tsx @@ -4,7 +4,6 @@ import { RECOVERY_EVENTS } from '@/services/analytics/events/recovery' import { Box, Button, Grid, Paper, SvgIcon, Tooltip, Typography } from '@mui/material' import { type ReactElement, useMemo, useState } from 'react' -import { Chip } from '@/components/common/Chip' import ExternalLink from '@/components/common/ExternalLink' import { DelayModifierRow } from './DelayModifierRow' import useRecovery from '@/features/recovery/hooks/useRecovery' @@ -118,8 +117,6 @@ function RecoverySettings(): ReactElement { <Typography variant="h4" fontWeight="bold"> Account recovery </Typography> - - <Chip label="New" /> </Box> </Grid> diff --git a/src/features/recovery/components/RecoverySummary/index.tsx b/src/features/recovery/components/RecoverySummary/index.tsx index bf5373ea8e..e3fdd022c4 100644 --- a/src/features/recovery/components/RecoverySummary/index.tsx +++ b/src/features/recovery/components/RecoverySummary/index.tsx @@ -30,13 +30,15 @@ export function RecoverySummary({ item }: { item: RecoveryQueueItem }): ReactEle <DateTime value={Number(item.timestamp)} /> </Box> - <Box gridArea="status"> - {!isExecutable || isPending ? ( + {!isExecutable || isPending ? ( + <Box gridArea="status"> <RecoveryStatus recovery={item} /> - ) : ( - !isMalicious && wallet && <ExecuteRecoveryButton recovery={item} compact /> - )} - </Box> + </Box> + ) : ( + <Box gridArea="actions" mr={2} display="flex" justifyContent="center"> + {!isMalicious && wallet && <ExecuteRecoveryButton recovery={item} compact />} + </Box> + )} </Box> ) } diff --git a/src/features/recovery/components/RecoveryWidget/index.tsx b/src/features/recovery/components/RecoveryWidget/index.tsx deleted file mode 100644 index 81ab5d6cbc..0000000000 --- a/src/features/recovery/components/RecoveryWidget/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { SetupRecoveryButton } from '@/features/recovery/components/RecoverySettings' -import { Box, Card, Grid, Typography } from '@mui/material' -import type { ReactElement } from 'react' - -import RecoveryLogo from '@/public/images/common/recovery.svg' -import { WidgetBody, WidgetContainer } from '@/components/dashboard/styled' -import { Chip } from '@/components/common/Chip' -import useRecovery from '@/features/recovery/hooks/useRecovery' - -import css from './styles.module.css' - -function RecoveryWidget(): ReactElement { - const [recovery] = useRecovery() - - return ( - <WidgetContainer> - <Typography component="h2" variant="subtitle1" className={css.label}> - New in {'Astar Safe'} - </Typography> - - <WidgetBody> - <Card className={css.card}> - <Grid container className={css.grid}> - <Grid item> - <RecoveryLogo alt="A circular arrow above a lifebuoy" /> - </Grid> - - <Grid item xs> - <Box className={css.wrapper}> - <Typography variant="h4" className={css.title}> - Introducing {`Safe{RecoveryHub}`}{' '} - </Typography> - <Chip label="New" /> - </Box> - - <Typography mt={1}> - Ensure you never lose access to your funds by choosing a recovery option to recover your Safe Account. - </Typography> - - {(!recovery || recovery.length === 0) && <SetupRecoveryButton eventLabel="dashboard" />} - </Grid> - </Grid> - </Card> - </WidgetBody> - </WidgetContainer> - ) -} - -export default RecoveryWidget diff --git a/src/features/recovery/components/RecoveryWidget/styles.module.css b/src/features/recovery/components/RecoveryWidget/styles.module.css deleted file mode 100644 index f8f7fb0562..0000000000 --- a/src/features/recovery/components/RecoveryWidget/styles.module.css +++ /dev/null @@ -1,27 +0,0 @@ -.label { - font-weight: 700; - margin-bottom: var(--space-2); -} - -.card { - padding: var(--space-3) var(--space-4); - height: inherit; -} - -.grid { - display: flex; - align-items: center; - height: inherit; - gap: var(--space-3); -} - -.wrapper { - display: flex; - align-items: center; - gap: var(--space-1); -} - -.title { - font-weight: 700; - display: inline; -} diff --git a/src/features/recovery/services/recovery-sender.ts b/src/features/recovery/services/recovery-sender.ts index 3f2b48503c..8ca388f479 100644 --- a/src/features/recovery/services/recovery-sender.ts +++ b/src/features/recovery/services/recovery-sender.ts @@ -1,38 +1,29 @@ import { getModuleInstance, KnownContracts } from '@gnosis.pm/zodiac' import type { SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' import type { SafeTransaction } from '@safe-global/safe-core-sdk-types' -import type { OnboardAPI } from '@web3-onboard/core' import type { TransactionAddedEvent } from '@gnosis.pm/zodiac/dist/cjs/types/Delay' -import type { TransactionResponse } from 'ethers' +import type { Eip1193Provider, TransactionResponse } from 'ethers' -import { createWeb3 } from '@/hooks/wallets/web3' import { didReprice, didRevert } from '@/utils/ethers-utils' import { recoveryDispatch, RecoveryEvent, RecoveryTxType } from './recoveryEvents' import { asError } from '@/services/exceptions/utils' -import { assertWalletChain } from '../../../services/tx/tx-sender/sdk' +import { getUncheckedSigner } from '../../../services/tx/tx-sender/sdk' import { isSmartContractWallet } from '@/utils/wallets' -import { UncheckedJsonRpcSigner } from '@/utils/providers/UncheckedJsonRpcSigner' async function getDelayModifierContract({ - onboard, + provider, chainId, delayModifierAddress, + signerAddress, }: { - onboard: OnboardAPI + provider: Eip1193Provider chainId: string delayModifierAddress: string + signerAddress: string }) { - // Switch signer to chain of Safe - const wallet = await assertWalletChain(onboard, chainId) + const isSmartContract = await isSmartContractWallet(chainId, signerAddress) - const provider = createWeb3(wallet.provider) - const isSmartContract = await isSmartContractWallet(wallet.chainId, wallet.address) - - const originalSigner = await provider.getSigner() - // Use unchecked signer for smart contract wallets as transactions do not necessarily immediately execute - const signer = isSmartContract - ? new UncheckedJsonRpcSigner(provider, await originalSigner.getAddress()) - : originalSigner + const signer = await getUncheckedSigner(provider) const delayModifier = getModuleInstance(KnownContracts.DELAY, delayModifierAddress, signer).connect(signer) return { @@ -80,20 +71,23 @@ function waitForRecoveryTx({ } export async function dispatchRecoveryProposal({ - onboard, + provider, safe, safeTx, delayModifierAddress, + signerAddress, }: { - onboard: OnboardAPI + provider: Eip1193Provider safe: SafeInfo safeTx: SafeTransaction delayModifierAddress: string + signerAddress: string }) { const { delayModifier, isUnchecked } = await getDelayModifierContract({ - onboard, + provider, chainId: safe.chainId, delayModifierAddress, + signerAddress, }) const txType = RecoveryTxType.PROPOSAL @@ -143,20 +137,23 @@ export async function dispatchRecoveryProposal({ } export async function dispatchRecoveryExecution({ - onboard, + provider, chainId, args, delayModifierAddress, + signerAddress, }: { - onboard: OnboardAPI + provider: Eip1193Provider chainId: string args: TransactionAddedEvent.Log['args'] delayModifierAddress: string + signerAddress: string }) { const { delayModifier, isUnchecked } = await getDelayModifierContract({ - onboard, + provider, chainId, delayModifierAddress, + signerAddress, }) const txType = RecoveryTxType.EXECUTION @@ -192,20 +189,23 @@ export async function dispatchRecoveryExecution({ } export async function dispatchRecoverySkipExpired({ - onboard, + provider, chainId, delayModifierAddress, recoveryTxHash, + signerAddress, }: { - onboard: OnboardAPI + provider: Eip1193Provider chainId: string delayModifierAddress: string recoveryTxHash: string + signerAddress: string }) { const { delayModifier, isUnchecked } = await getDelayModifierContract({ - onboard, + provider, chainId, delayModifierAddress, + signerAddress, }) const txType = RecoveryTxType.SKIP_EXPIRED diff --git a/src/features/speedup/components/SpeedUpModal.tsx b/src/features/speedup/components/SpeedUpModal.tsx index 8705f4bec8..d4a054a93e 100644 --- a/src/features/speedup/components/SpeedUpModal.tsx +++ b/src/features/speedup/components/SpeedUpModal.tsx @@ -1,5 +1,6 @@ import useGasPrice from '@/hooks/useGasPrice' import ModalDialog from '@/components/common/ModalDialog' +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import DialogContent from '@mui/material/DialogContent' import { Box, Button, SvgIcon, Tooltip, Typography } from '@mui/material' import RocketSpeedup from '@/public/images/common/ic-rocket-speedup.svg' @@ -89,12 +90,15 @@ export const SpeedUpModal = ({ try { setWaitingForConfirmation(true) + await assertWalletChain(onboard, chainInfo.chainId) + if (pendingTx.txType === PendingTxType.SAFE_TX) { await dispatchSafeTxSpeedUp( txOptions as Omit<TransactionOptions, 'nonce'> & { nonce: number }, txId, - onboard, + wallet.provider, chainInfo.chainId, + wallet.address, safeAddress, ) const txType = await getTransactionTrackingType(chainInfo.chainId, txId) @@ -105,8 +109,8 @@ export const SpeedUpModal = ({ txId, pendingTx.to, pendingTx.data, - onboard, - chainInfo?.chainId, + wallet.provider, + wallet.address, safeAddress, ) // Currently all custom txs are batch executes diff --git a/src/features/swap/components/HelpIconTooltip/index.tsx b/src/features/swap/components/HelpIconTooltip/index.tsx new file mode 100644 index 0000000000..83ec96c0a4 --- /dev/null +++ b/src/features/swap/components/HelpIconTooltip/index.tsx @@ -0,0 +1,25 @@ +import { SvgIcon, Tooltip } from '@mui/material' +import InfoIcon from '@/public/images/notifications/info.svg' +import type { ReactNode } from 'react' + +type Props = { + title: ReactNode +} +export const HelpIconTooltip = ({ title }: Props) => { + return ( + <Tooltip title={title} arrow placement="top"> + <span> + <SvgIcon + component={InfoIcon} + inheritViewBox + color="border" + fontSize="small" + sx={{ + verticalAlign: 'middle', + ml: 0.5, + }} + /> + </span> + </Tooltip> + ) +} diff --git a/src/features/swap/components/LegalDisclaimer/index.tsx b/src/features/swap/components/LegalDisclaimer/index.tsx new file mode 100644 index 0000000000..a324af6889 --- /dev/null +++ b/src/features/swap/components/LegalDisclaimer/index.tsx @@ -0,0 +1,38 @@ +import ExternalLink from '@/components/common/ExternalLink' +import { AppRoutes } from '@/config/routes' +import { Typography } from '@mui/material' + +import css from './styles.module.css' + +const LegalDisclaimerContent = () => ( + <div className={css.disclaimerContainer}> + <div className={css.disclaimerInner}> + <Typography mb={4} mt={4}> + You are now accessing a third party widget. + </Typography> + + <Typography mb={4}> + Please note that we do not own, control, maintain or audit the CoW Swap Widget. Use of the widget is subject to + third party terms & conditions. We are not liable for any loss you may suffer in connection with interacting + with the widget, which is at your own risk. + </Typography> + + <Typography mb={4}> + Our{' '} + <ExternalLink href={AppRoutes.terms} sx={{ textDecoration: 'none' }}> + terms + </ExternalLink>{' '} + contain more detailed provisions binding on you relating to such third party content. + </Typography> + <Typography> + By clicking "continue" you re-confirm to have read and understood our{' '} + <ExternalLink href={AppRoutes.terms} sx={{ textDecoration: 'none' }}> + terms + </ExternalLink>{' '} + and this message, and agree to them. + </Typography> + </div> + </div> +) + +export default LegalDisclaimerContent diff --git a/src/features/swap/components/LegalDisclaimer/styles.module.css b/src/features/swap/components/LegalDisclaimer/styles.module.css new file mode 100644 index 0000000000..926ac0a491 --- /dev/null +++ b/src/features/swap/components/LegalDisclaimer/styles.module.css @@ -0,0 +1,8 @@ +.disclaimerContainer p, +.disclaimerContainer h3 { + line-height: 24px; +} + +.disclaimerInner p { + text-align: justify; +} diff --git a/src/features/swap/components/SwapButton/index.tsx b/src/features/swap/components/SwapButton/index.tsx index e6ac6979f9..13f4c9fb11 100644 --- a/src/features/swap/components/SwapButton/index.tsx +++ b/src/features/swap/components/SwapButton/index.tsx @@ -2,22 +2,32 @@ import CheckWallet from '@/components/common/CheckWallet' import Track from '@/components/common/Track' import { AppRoutes } from '@/config/routes' import useSpendingLimit from '@/hooks/useSpendingLimit' -import { SWAP_EVENTS, SWAP_LABELS } from '@/services/analytics/events/swaps' +import type { SWAP_LABELS } from '@/services/analytics/events/swaps' +import { SWAP_EVENTS } from '@/services/analytics/events/swaps' import { Button } from '@mui/material' import type { TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' import { useRouter } from 'next/router' import type { ReactElement } from 'react' import SwapIcon from '@/public/images/common/swap.svg' -const SwapButton = ({ tokenInfo, amount }: { tokenInfo: TokenInfo; amount: string }): ReactElement => { +const SwapButton = ({ + tokenInfo, + amount, + trackingLabel, +}: { + tokenInfo: TokenInfo + amount: string + trackingLabel: SWAP_LABELS +}): ReactElement => { const spendingLimit = useSpendingLimit(tokenInfo) const router = useRouter() return ( <CheckWallet allowSpendingLimit={!!spendingLimit}> {(isOk) => ( - <Track {...SWAP_EVENTS.OPEN_SWAPS} label={SWAP_LABELS.asset}> + <Track {...SWAP_EVENTS.OPEN_SWAPS} label={trackingLabel}> <Button + data-testid="swap-btn" variant="outlined" color="primary" size="small" diff --git a/src/features/swap/components/SwapOrder/index.tsx b/src/features/swap/components/SwapOrder/index.tsx index b3166a9acd..8610f97072 100644 --- a/src/features/swap/components/SwapOrder/index.tsx +++ b/src/features/swap/components/SwapOrder/index.tsx @@ -6,10 +6,14 @@ import { capitalize } from '@/hooks/useMnemonicName' import { formatDateTime, formatTimeInWords } from '@/utils/date' import Stack from '@mui/material/Stack' import type { ReactElement } from 'react' -import { type SwapOrder as SwapOrderType, type TransactionData } from '@safe-global/safe-gateway-typescript-sdk' +import type { TwapOrder as SwapTwapOrder } from '@safe-global/safe-gateway-typescript-sdk' +import { + type Order, + type SwapOrder as SwapOrderType, + type TransactionData, +} from '@safe-global/safe-gateway-typescript-sdk' import { DataRow } from '@/components/common/Table/DataRow' import { DataTable } from '@/components/common/Table/DataTable' -import { HexEncodedData } from '@/components/transactions/HexEncodedData' import { compareAsc } from 'date-fns' import css from './styles.module.css' import { Typography } from '@mui/material' @@ -19,126 +23,250 @@ import { getExecutionPrice, getLimitPrice, getOrderClass, + getPartiallyFilledSurplus, getSurplusPrice, isOrderPartiallyFilled, } from '@/features/swap/helpers/utils' import EthHashInfo from '@/components/common/EthHashInfo' import useSafeInfo from '@/hooks/useSafeInfo' +import { isSwapOrderTxInfo, isSwapTransferOrderTxInfo, isTwapOrderTxInfo } from '@/utils/transaction-guards' +import { EmptyRow } from '@/components/common/Table/EmptyRow' +import { PartDuration } from '@/features/swap/components/SwapOrder/rows/PartDuration' +import { PartSellAmount } from '@/features/swap/components/SwapOrder/rows/PartSellAmount' +import { PartBuyAmount } from '@/features/swap/components/SwapOrder/rows/PartBuyAmount' +import { SurplusFee } from '@/features/swap/components/SwapOrder/rows/SurplusFee' type SwapOrderProps = { txData?: TransactionData - txInfo?: SwapOrderType + txInfo?: Order } -export const SellOrder = ({ order }: { order: SwapOrderType }) => { - const { safeAddress } = useSafeInfo() - const { uid, kind, validUntil, status, sellToken, buyToken, sellAmount, buyAmount, explorerUrl, receiver } = order +const AmountRow = ({ order }: { order: Order }) => { + const { sellToken, buyToken, sellAmount, buyAmount, kind } = order + const isSellOrder = kind === 'sell' + return ( + <DataRow key="Amount" title="Amount"> + <Stack flexDirection={isSellOrder ? 'column' : 'column-reverse'}> + <div> + <span className={css.value}> + {isSellOrder ? 'Sell' : 'For at most'}{' '} + {sellToken.logoUri && <TokenIcon logoUri={sellToken.logoUri} size={24} />}{' '} + <Typography component="span" fontWeight="bold"> + {formatVisualAmount(sellAmount, sellToken.decimals)} {sellToken.symbol} + </Typography> + </span> + </div> + <div> + <span className={css.value}> + {isSellOrder ? 'for at least' : 'Buy'}{' '} + {buyToken.logoUri && <TokenIcon logoUri={buyToken.logoUri} size={24} />} + <Typography component="span" fontWeight="bold"> + {formatVisualAmount(buyAmount, buyToken.decimals)} {buyToken.symbol} + </Typography> + </span> + </div> + </Stack> + </DataRow> + ) +} +const PriceRow = ({ order }: { order: Order }) => { + const { status, sellToken, buyToken } = order const executionPrice = getExecutionPrice(order) const limitPrice = getLimitPrice(order) - const surplusPrice = getSurplusPrice(order) - const expires = new Date(validUntil * 1000) + + if (status === 'fulfilled') { + return ( + <DataRow key="Execution price" title="Execution price"> + 1 {buyToken.symbol} = {formatAmount(executionPrice)} {sellToken.symbol} + </DataRow> + ) + } + + return ( + <DataRow key="Limit price" title="Limit price"> + 1 {buyToken.symbol} = {formatAmount(limitPrice)} {sellToken.symbol} + </DataRow> + ) +} + +const ExpiryRow = ({ order }: { order: Order }) => { + const { validUntil, status } = order const now = new Date() + const expires = new Date(validUntil * 1000) + if (status! == 'fulfilled') { + if (compareAsc(now, expires) !== 1) { + return ( + <DataRow key="Expiry" title="Expiry"> + <Typography> + <Typography fontWeight={700} component="span"> + {formatTimeInWords(validUntil * 1000)} + </Typography>{' '} + ({formatDateTime(validUntil * 1000)}) + </Typography> + </DataRow> + ) + } else { + return ( + <DataRow key="Expiry" title="Expiry"> + {formatDateTime(validUntil * 1000)} + </DataRow> + ) + } + } + + return null +} + +const SurplusRow = ({ order }: { order: Order }) => { + const { status, kind } = order + const isPartiallyFilled = isOrderPartiallyFilled(order) + const surplusPrice = isPartiallyFilled ? getPartiallyFilledSurplus(order) : getSurplusPrice(order) + const { sellToken, buyToken } = order + const isSellOrder = kind === 'sell' + if (status === 'fulfilled' || isPartiallyFilled) { + return ( + <DataRow key="Surplus" title="Surplus"> + {formatAmount(surplusPrice)} {isSellOrder ? buyToken.symbol : sellToken.symbol} + </DataRow> + ) + } + + return null +} + +const FilledRow = ({ order }: { order: Order }) => { const orderClass = getOrderClass(order) + if (['limit', 'twap'].includes(orderClass)) { + return ( + <DataRow title="Filled" key="Filled"> + <SwapProgress order={order} /> + </DataRow> + ) + } + + return null +} + +const OrderUidRow = ({ order }: { order: Order }) => { + if (isSwapOrderTxInfo(order) || isSwapTransferOrderTxInfo(order)) { + const { uid, explorerUrl } = order + return ( + <DataRow key="Order ID" title="Order ID"> + <OrderId orderId={uid} href={explorerUrl} /> + </DataRow> + ) + } + return null +} + +const StatusRow = ({ order }: { order: Order }) => { + const { status } = order + const isPartiallyFilled = isOrderPartiallyFilled(order) + return ( + <DataRow key="Status" title="Status"> + <StatusLabel status={isPartiallyFilled ? 'partiallyFilled' : status} /> + </DataRow> + ) +} + +const RecipientRow = ({ order }: { order: Order }) => { + const { safeAddress } = useSafeInfo() + const { receiver } = order + + if (receiver && receiver !== safeAddress) { + return ( + <DataRow key="Recipient" title="Recipient"> + <EthHashInfo address={receiver} showAvatar={false} /> + </DataRow> + ) + } + return null +} + +export const SellOrder = ({ order }: { order: SwapOrderType }) => { + const { kind } = order const orderKindLabel = capitalize(kind) - const isSellOrder = kind === 'sell' + + return ( + <DataTable + header={`${orderKindLabel} order`} + rows={[ + <AmountRow order={order} key="amount-row" />, + <PriceRow order={order} key="price-row" />, + <SurplusRow order={order} key="surplus-row" />, + <ExpiryRow order={order} key="expiry-row" />, + <FilledRow order={order} key="filled-row" />, + <OrderUidRow order={order} key="order-uid-row" />, + <StatusRow order={order} key="status-row" />, + <RecipientRow order={order} key="recipient-row" />, + <SurplusFee order={order} key="fee-row" />, + ]} + /> + ) +} + +export const TwapOrder = ({ order }: { order: SwapTwapOrder }) => { + const { kind, validUntil, status, numberOfParts } = order const isPartiallyFilled = isOrderPartiallyFilled(order) + const expires = new Date(validUntil * 1000) + const now = new Date() + + const orderKindLabel = capitalize(kind) return ( <DataTable header={`${orderKindLabel} order`} rows={[ - <DataRow key="Amount" title="Amount"> - <Stack flexDirection={isSellOrder ? 'column' : 'column-reverse'}> - <div> - <span className={css.value}> - {isSellOrder ? 'Sell' : 'For at most'}{' '} - {sellToken.logoUri && <TokenIcon logoUri={sellToken.logoUri} size={24} />}{' '} - {formatVisualAmount(sellAmount, sellToken.decimals)} {sellToken.symbol} - </span> - </div> - <div> - <span className={css.value}> - {isSellOrder ? 'for at least' : 'Buy'}{' '} - {buyToken.logoUri && <TokenIcon logoUri={buyToken.logoUri} size={24} />} - {formatVisualAmount(buyAmount, buyToken.decimals)} {buyToken.symbol} - </span> - </div> - </Stack> + <AmountRow order={order} key="amount-row" />, + <PriceRow order={order} key="price-row" />, + <SurplusRow order={order} key="surplus-row" />, + <RecipientRow order={order} key="recipient-row" />, + <SurplusFee order={order} key="fee-row" />, + <EmptyRow key="spacer-0" />, + <DataRow title="No of parts" key="n_of_parts"> + {numberOfParts} </DataRow>, - status === 'fulfilled' ? ( - <DataRow key="Execution price" title="Execution price"> - 1 {buyToken.symbol} = {formatAmount(executionPrice)} {sellToken.symbol} + <PartSellAmount order={order} key="part_sell_amount" />, + <PartBuyAmount order={order} key="part_buy_amount" />, + <FilledRow order={order} key="filled-row" />, + <PartDuration order={order} key="part_duration" />, + <EmptyRow key="spacer-1" />, + status !== 'fulfilled' && compareAsc(now, expires) !== 1 ? ( + <DataRow key="Expiry" title="Expiry"> + <Typography> + <Typography fontWeight={700} component="span"> + {formatTimeInWords(validUntil * 1000)} + </Typography>{' '} + ({formatDateTime(validUntil * 1000)}) + </Typography> </DataRow> ) : ( - <DataRow key="Limit price" title="Limit price"> - 1 {buyToken.symbol} = {formatAmount(limitPrice)} {sellToken.symbol} - </DataRow> - ), - status === 'fulfilled' ? ( - <DataRow key="Surplus" title="Surplus"> - {formatAmount(surplusPrice)} {isSellOrder ? buyToken.symbol : sellToken.symbol} - </DataRow> - ) : ( - <></> - ), - status !== 'fulfilled' ? ( - compareAsc(now, expires) !== 1 ? ( - <DataRow key="Expiry" title="Expiry"> - <Typography> - <Typography fontWeight={700} component="span"> - {formatTimeInWords(validUntil * 1000)} - </Typography>{' '} - ({formatDateTime(validUntil * 1000)}) - </Typography> - </DataRow> - ) : ( - <DataRow key="Expiry" title="Expiry"> - {formatDateTime(validUntil * 1000)} - </DataRow> - ) - ) : ( - <></> - ), - orderClass === 'limit' ? ( - <DataRow title="Filled" key="Filled"> - <SwapProgress order={order} /> + <DataRow key="Expired" title="Expired"> + {formatDateTime(validUntil * 1000)} </DataRow> - ) : ( - <></> ), - <DataRow key="Order ID" title="Order ID"> - <OrderId orderId={uid} href={explorerUrl} /> - </DataRow>, <DataRow key="Status" title="Status"> <StatusLabel status={isPartiallyFilled ? 'partiallyFilled' : status} /> </DataRow>, - receiver && receiver !== safeAddress ? ( - <DataRow key="Recipient" title="Recipient"> - <EthHashInfo address={receiver} showAvatar={false} /> - </DataRow> - ) : ( - <></> - ), ]} /> ) } -export const SwapOrder = ({ txData, txInfo }: SwapOrderProps): ReactElement | null => { - if (!txData || !txInfo) return null +export const SwapOrder = ({ txInfo }: SwapOrderProps): ReactElement | null => { + if (!txInfo) return null - // ? when can a multiSend call take no parameters? - if (!txData.dataDecoded?.parameters) { - if (txData.hexData) { - return <HexEncodedData title="Data (hex encoded)" hexData={txData.hexData} /> - } - return null + if (isTwapOrderTxInfo(txInfo)) { + return <TwapOrder order={txInfo} /> } - return <SellOrder order={txInfo} /> + if (isSwapOrderTxInfo(txInfo) || isSwapTransferOrderTxInfo(txInfo)) { + return <SellOrder order={txInfo} /> + } + return null } export default SwapOrder diff --git a/src/features/swap/components/SwapOrder/rows/PartBuyAmount.tsx b/src/features/swap/components/SwapOrder/rows/PartBuyAmount.tsx new file mode 100644 index 0000000000..5076242c14 --- /dev/null +++ b/src/features/swap/components/SwapOrder/rows/PartBuyAmount.tsx @@ -0,0 +1,27 @@ +import { Typography } from '@mui/material' +import { formatVisualAmount } from '@/utils/formatters' +import { type TwapOrder } from '@safe-global/safe-gateway-typescript-sdk' +import { DataRow } from '@/components/common/Table/DataRow' +import { Box } from '@mui/system' + +export const PartBuyAmount = ({ + order, + addonText = '', +}: { + order: Pick<TwapOrder, 'minPartLimit' | 'buyToken'> + addonText?: string +}) => { + const { minPartLimit, buyToken } = order + return ( + <DataRow title="Buy amount" key="buy_amount_part"> + <Box> + <Typography component="span" fontWeight="bold"> + {formatVisualAmount(minPartLimit, buyToken.decimals)} {buyToken.symbol} + </Typography> + <Typography component="span" color="var(--color-primary-light)"> + {` ${addonText}`} + </Typography> + </Box> + </DataRow> + ) +} diff --git a/src/features/swap/components/SwapOrder/rows/PartDuration.tsx b/src/features/swap/components/SwapOrder/rows/PartDuration.tsx new file mode 100644 index 0000000000..1154b513fc --- /dev/null +++ b/src/features/swap/components/SwapOrder/rows/PartDuration.tsx @@ -0,0 +1,12 @@ +import { DataRow } from '@/components/common/Table/DataRow' +import { type TwapOrder } from '@safe-global/safe-gateway-typescript-sdk' +import { getPeriod } from '@/utils/date' + +export const PartDuration = ({ order }: { order: Pick<TwapOrder, 'timeBetweenParts'> }) => { + const { timeBetweenParts } = order + return ( + <DataRow title="Part duration" key="part_duration"> + {getPeriod(+timeBetweenParts)} + </DataRow> + ) +} diff --git a/src/features/swap/components/SwapOrder/rows/PartSellAmount.tsx b/src/features/swap/components/SwapOrder/rows/PartSellAmount.tsx new file mode 100644 index 0000000000..ac8fcf13b6 --- /dev/null +++ b/src/features/swap/components/SwapOrder/rows/PartSellAmount.tsx @@ -0,0 +1,27 @@ +import { Typography } from '@mui/material' +import { formatVisualAmount } from '@/utils/formatters' +import { type TwapOrder } from '@safe-global/safe-gateway-typescript-sdk' +import { DataRow } from '@/components/common/Table/DataRow' +import { Box } from '@mui/system' + +export const PartSellAmount = ({ + order, + addonText = '', +}: { + order: Pick<TwapOrder, 'partSellAmount' | 'sellToken'> + addonText?: string +}) => { + const { partSellAmount, sellToken } = order + return ( + <DataRow title="Sell amount" key="sell_amount_part"> + <Box> + <Typography component="span" fontWeight="bold"> + {formatVisualAmount(partSellAmount, sellToken.decimals)} {sellToken.symbol} + </Typography> + <Typography component="span" color="var(--color-primary-light)"> + {` ${addonText}`} + </Typography> + </Box> + </DataRow> + ) +} diff --git a/src/features/swap/components/SwapOrder/rows/SurplusFee.tsx b/src/features/swap/components/SwapOrder/rows/SurplusFee.tsx new file mode 100644 index 0000000000..91e566f0c1 --- /dev/null +++ b/src/features/swap/components/SwapOrder/rows/SurplusFee.tsx @@ -0,0 +1,40 @@ +import type { Order } from '@safe-global/safe-gateway-typescript-sdk' +import { getOrderFeeBps } from '@/features/swap/helpers/utils' +import { DataRow } from '@/components/common/Table/DataRow' +import { formatVisualAmount } from '@/utils/formatters' +import { HelpIconTooltip } from '@/features/swap/components/HelpIconTooltip' + +export const SurplusFee = ({ + order, +}: { + order: Pick<Order, 'fullAppData' | 'sellToken' | 'buyToken' | 'status' | 'executedSurplusFee' | 'kind'> +}) => { + const bps = getOrderFeeBps(order) + const { executedSurplusFee, sellToken } = order + let token = sellToken + + if (executedSurplusFee === null || typeof executedSurplusFee === 'undefined' || executedSurplusFee === '0') { + return null + } + + return ( + <DataRow + title={ + <> + Total fees + <HelpIconTooltip + title={ + <> + The amount of fees paid for this order. + {bps > 0 && ` This includes a Widget fee of ${bps / 100}% and network fees.`} + </> + } + /> + </> + } + key="widget_fee" + > + {formatVisualAmount(BigInt(executedSurplusFee), token.decimals)} {token.symbol} + </DataRow> + ) +} diff --git a/src/features/swap/components/SwapOrder/index.stories.tsx b/src/features/swap/components/SwapOrder/swap.stories.tsx similarity index 99% rename from src/features/swap/components/SwapOrder/index.stories.tsx rename to src/features/swap/components/SwapOrder/swap.stories.tsx index f122770f23..808702690f 100644 --- a/src/features/swap/components/SwapOrder/index.stories.tsx +++ b/src/features/swap/components/SwapOrder/swap.stories.tsx @@ -39,8 +39,6 @@ const FulfilledSwapOrder = swapOrderBuilder() fullAppData: appDataBuilder('market').build(), }) -console.log(FulfilledSwapOrder) - const NonFulfilledSwapOrder = { ...FulfilledSwapOrder.build(), status: 'open' as OrderStatuses, diff --git a/src/features/swap/components/SwapOrder/twap.stories.tsx b/src/features/swap/components/SwapOrder/twap.stories.tsx new file mode 100644 index 0000000000..efb8f53e35 --- /dev/null +++ b/src/features/swap/components/SwapOrder/twap.stories.tsx @@ -0,0 +1,73 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { TwapOrder as TwapOrderComponent } from './index' +import { Paper } from '@mui/material' +import { appDataBuilder, orderTokenBuilder, twapOrderBuilder } from '@/features/swap/helpers/swapOrderBuilder' +import { StoreDecorator } from '@/stories/storeDecorator' + +const FullfilledTwapOrder = twapOrderBuilder() + .with({ status: 'fulfilled' }) + .with({ kind: 'sell' }) + .with({ orderClass: 'limit' }) + .with({ sellAmount: '10000000000000000' }) + .with({ executedSellAmount: '10000000000000000' }) + .with({ buyAmount: '3388586928324482608' }) + .with({ executedBuyAmount: '3388586928324482608' }) + .with({ validUntil: 1713520008 }) + .with({ + sellToken: { + ...orderTokenBuilder().build(), + logoUri: + 'https://safe-transaction-assets.staging.5afe.dev/tokens/logos/0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14.png', + }, + }) + .with({ + buyToken: { + ...orderTokenBuilder().build(), + logoUri: + 'https://safe-transaction-assets.staging.5afe.dev/tokens/logos/0xbe72E441BF55620febc26715db68d3494213D8Cb.png', + }, + }) + .with({ numberOfParts: '2' }) + + .with({ partSellAmount: '5000000000000000' }) + .with({ minPartLimit: '1694293464162241304' }) + .with({ timeBetweenParts: 1800 }) + .with({ + fullAppData: appDataBuilder('twap').build(), + }) + +const meta = { + component: TwapOrderComponent, + parameters: { + componentSubtitle: 'Renders a Status label with icon and text for a swap order', + }, + + decorators: [ + (Story) => { + return ( + <StoreDecorator initialState={{}}> + <Paper sx={{ padding: 2 }}> + <Story /> + </Paper> + </StoreDecorator> + ) + }, + ], + tags: ['autodocs'], + // excludeStories: ['SwapOrderProps'], +} satisfies Meta<typeof TwapOrderComponent> + +export default meta +type Story = StoryObj<typeof meta> + +export const ExecutedTwap: Story = { + args: { + order: FullfilledTwapOrder.build(), + }, + parameters: { + design: { + type: 'figma', + url: 'https://www.figma.com/design/VyA38zUPbJ2zflzCIYR6Nu/Swap?node-id=6655-24390&t=pg5ZPJArWFJOiEsn-4', + }, + }, +} diff --git a/src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx b/src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx new file mode 100644 index 0000000000..4cc8020a89 --- /dev/null +++ b/src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx @@ -0,0 +1,39 @@ +import type { OrderConfirmationView } from '@safe-global/safe-gateway-typescript-sdk' +import { getOrderFeeBps } from '@/features/swap/helpers/utils' +import { DataRow } from '@/components/common/Table/DataRow' +import { HelpCenterArticle } from '@/config/constants' +import { HelpIconTooltip } from '@/features/swap/components/HelpIconTooltip' +import MUILink from '@mui/material/Link' + +export const OrderFeeConfirmationView = ({ order }: { order: Pick<OrderConfirmationView, 'fullAppData'> }) => { + const bps = getOrderFeeBps(order) + + if (Number(bps) === 0) { + return null + } + + const title = ( + <> + Widget fee{' '} + <HelpIconTooltip + title={ + <> + The tiered widget fee incurred here is charged by CoW Protocol for the operation of this widget. The fee is + automatically calculated into this quote. Part of the fee will contribute to a license fee that supports the + Safe Community. Neither the Safe Ecosystem Foundation nor {`Safe{Wallet}`} operate the CoW Swap Widget + and/or CoW Swap. + <MUILink href={HelpCenterArticle.SWAP_WIDGET_FEES} target="_blank" rel="noopener noreferrer"> + Learn more + </MUILink> + </> + } + /> + </> + ) + + return ( + <DataRow datatestid="widget-fee" title={title} key="widget_fee"> + {Number(bps) / 100} % + </DataRow> + ) +} diff --git a/src/features/swap/components/SwapOrderConfirmationView/index.stories.tsx b/src/features/swap/components/SwapOrderConfirmationView/index.stories.tsx index 71f258d021..a154b0d645 100644 --- a/src/features/swap/components/SwapOrderConfirmationView/index.stories.tsx +++ b/src/features/swap/components/SwapOrderConfirmationView/index.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react' -import SwapOrderConfirmationView from './index' +import CowOrderConfirmationView from './index' import { Paper } from '@mui/material' import type { OrderStatuses } from '@safe-global/safe-gateway-typescript-sdk' import { orderTokenBuilder, swapOrderConfirmationViewBuilder } from '@/features/swap/helpers/swapOrderBuilder' @@ -15,7 +15,7 @@ const Order = swapOrderConfirmationViewBuilder() .with({ status: 'open' as OrderStatuses }) const meta = { - component: SwapOrderConfirmationView, + component: CowOrderConfirmationView, decorators: [ (Story) => { @@ -29,7 +29,7 @@ const meta = { }, ], tags: ['autodocs'], -} satisfies Meta<typeof SwapOrderConfirmationView> +} satisfies Meta<typeof CowOrderConfirmationView> export default meta type Story = StoryObj<typeof meta> diff --git a/src/features/swap/components/SwapOrderConfirmationView/index.tsx b/src/features/swap/components/SwapOrderConfirmationView/index.tsx index ed6dda4717..8c34512b69 100644 --- a/src/features/swap/components/SwapOrderConfirmationView/index.tsx +++ b/src/features/swap/components/SwapOrderConfirmationView/index.tsx @@ -1,6 +1,6 @@ import OrderId from '@/features/swap/components/OrderId' -import { formatDateTime, formatTimeInWords } from '@/utils/date' -import type { ReactElement } from 'react' +import { formatDateTime, formatTimeInWords, getPeriod } from '@/utils/date' +import { Fragment, type ReactElement } from 'react' import { DataRow } from '@/components/common/Table/DataRow' import { DataTable } from '@/components/common/Table/DataTable' import { compareAsc } from 'date-fns' @@ -8,22 +8,28 @@ import { Alert, Typography } from '@mui/material' import { formatAmount } from '@/utils/formatNumber' import { formatVisualAmount } from '@/utils/formatters' import { getLimitPrice, getOrderClass, getSlippageInPercent } from '@/features/swap/helpers/utils' -import type { CowSwapConfirmationView } from '@safe-global/safe-gateway-typescript-sdk' +import type { OrderConfirmationView } from '@safe-global/safe-gateway-typescript-sdk' +import { StartTimeValue } from '@safe-global/safe-gateway-typescript-sdk' +import { ConfirmationViewTypes } from '@safe-global/safe-gateway-typescript-sdk' import SwapTokens from '@/features/swap/components/SwapTokens' import AlertIcon from '@/public/images/common/alert.svg' import EthHashInfo from '@/components/common/EthHashInfo' import css from './styles.module.css' import NamedAddress from '@/components/common/NamedAddressInfo' +import { PartDuration } from '@/features/swap/components/SwapOrder/rows/PartDuration' +import { PartSellAmount } from '@/features/swap/components/SwapOrder/rows/PartSellAmount' +import { PartBuyAmount } from '@/features/swap/components/SwapOrder/rows/PartBuyAmount' +import { OrderFeeConfirmationView } from '@/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView' type SwapOrderProps = { - order: CowSwapConfirmationView + order: OrderConfirmationView settlementContract: string } -export const SwapOrderConfirmationView = ({ order, settlementContract }: SwapOrderProps): ReactElement | null => { - if (!order) return null +export const SwapOrderConfirmationView = ({ order, settlementContract }: SwapOrderProps): ReactElement => { + const { owner, kind, validUntil, sellToken, buyToken, sellAmount, buyAmount, explorerUrl, receiver } = order - const { uid, owner, kind, validUntil, sellToken, buyToken, sellAmount, buyAmount, explorerUrl, receiver } = order + const isTwapOrder = order.type === ConfirmationViewTypes.COW_SWAP_TWAP_ORDER const limitPrice = getLimitPrice(order) const orderClass = getOrderClass(order) @@ -36,9 +42,9 @@ export const SwapOrderConfirmationView = ({ order, settlementContract }: SwapOrd return ( <div className={css.tableWrapper}> <DataTable - header="Order Details" + header="Order details" rows={[ - <div key="amount"> + <div key="amount" className={css.amount}> <SwapTokens first={{ value: formatVisualAmount(sellAmount, sellToken.decimals), @@ -55,12 +61,12 @@ export const SwapOrderConfirmationView = ({ order, settlementContract }: SwapOrd /> </div>, - <DataRow key="Limit price" title="Limit price"> + <DataRow datatestid="limit-price" key="Limit price" title="Limit price"> 1 {buyToken.symbol} = {formatAmount(limitPrice)} {sellToken.symbol} </DataRow>, compareAsc(now, expires) !== 1 ? ( - <DataRow key="Expiry" title="Expiry"> + <DataRow datatestid="expiry" key="Expiry" title="Expiry"> <Typography> <Typography fontWeight={700} component="span"> {formatTimeInWords(validUntil * 1000)} @@ -74,25 +80,30 @@ export const SwapOrderConfirmationView = ({ order, settlementContract }: SwapOrd </DataRow> ), orderClass !== 'limit' ? ( - <DataRow key="Slippage" title="Slippage"> + <DataRow datatestid="slippage" key="Slippage" title="Slippage"> {slippage}% </DataRow> + ) : ( + <Fragment key="none" /> + ), + !isTwapOrder ? ( + <DataRow datatestid="order-id" key="Order ID" title="Order ID"> + <OrderId orderId={order.uid} href={explorerUrl} /> + </DataRow> ) : ( <></> ), - <DataRow key="Order ID" title="Order ID"> - <OrderId orderId={uid} href={explorerUrl} /> - </DataRow>, - <DataRow key="Interact with" title="Interact with"> + <OrderFeeConfirmationView key="SurplusFee" order={order} />, + <DataRow datatestid="interact-wth" key="Interact with" title="Interact with"> <NamedAddress address={settlementContract} onlyName hasExplorer shortAddress={false} avatarSize={24} /> </DataRow>, receiver && owner !== receiver ? ( <> - <DataRow key="recipient-address" title="Recipient"> + <DataRow datatestid="recipient" key="recipient-address" title="Recipient"> <EthHashInfo address={receiver} hasExplorer={true} avatarSize={24} /> </DataRow> <div key="recipient"> - <Alert severity="warning" icon={AlertIcon}> + <Alert data-testid="recipient-alert" severity="warning" icon={AlertIcon}> <Typography variant="body2"> <Typography component="span" sx={{ fontWeight: 'bold' }}> Order recipient address differs from order owner. @@ -107,6 +118,31 @@ export const SwapOrderConfirmationView = ({ order, settlementContract }: SwapOrd ), ]} /> + + {isTwapOrder && ( + <div className={css.partsBlock}> + <DataTable + rows={[ + <Typography key="title" variant="body1" className={css.partsBlockTitle}> + <strong> + Order will be split in{' '} + <span className={css.numberOfPartsLabel}>{order.numberOfParts} equal parts</span> + </strong> + </Typography>, + <PartSellAmount order={order} addonText="per part" key="sell_part" />, + <PartBuyAmount order={order} addonText="per part" key="buy_part" />, + <DataRow title="Start time" key="Start time"> + {order.startTime.startType === StartTimeValue.AT_MINING_TIME && 'Now'} + {order.startTime.startType === StartTimeValue.AT_EPOCH && `At block number: ${order.startTime.epoch}`} + </DataRow>, + <PartDuration order={order} key="part_duration" />, + <DataRow title="Total duration" key="total_duration"> + {getPeriod(+order.timeBetweenParts * +order.numberOfParts)} + </DataRow>, + ]} + /> + </div> + )} </div> ) } diff --git a/src/features/swap/components/SwapOrderConfirmationView/styles.module.css b/src/features/swap/components/SwapOrderConfirmationView/styles.module.css index 3cffed151f..fdabf93ff5 100644 --- a/src/features/swap/components/SwapOrderConfirmationView/styles.module.css +++ b/src/features/swap/components/SwapOrderConfirmationView/styles.module.css @@ -1,3 +1,20 @@ -.tableWrapper > div > * { - margin: 6px 0; +.amount { + margin-bottom: var(--space-1); +} +.partsBlock { + border: 1px solid var(--color-border-light); + border-radius: 4px; + padding: calc(var(--space-1) - 6px) var(--space-2); + margin-top: var(--space-1); +} + +.partsBlockTitle { + padding: var(--space-1) 0; +} + +.numberOfPartsLabel { + display: inline-block; + border-radius: 4px; + padding: 2px 8px; + background-color: var(--color-border-background); } diff --git a/src/features/swap/components/SwapProgress/index.tsx b/src/features/swap/components/SwapProgress/index.tsx index 813413f9db..ca4a1a2b97 100644 --- a/src/features/swap/components/SwapProgress/index.tsx +++ b/src/features/swap/components/SwapProgress/index.tsx @@ -1,9 +1,9 @@ import { getFilledAmount, getFilledPercentage } from '@/features/swap/helpers/utils' import { formatAmount } from '@/utils/formatNumber' import { LinearProgress, Stack, Typography } from '@mui/material' -import type { SwapOrder } from '@safe-global/safe-gateway-typescript-sdk' +import type { Order } from '@safe-global/safe-gateway-typescript-sdk' -const SwapProgress = ({ order }: { order: SwapOrder }) => { +const SwapProgress = ({ order }: { order: Order }) => { const filledPercentage = getFilledPercentage(order) const filledAmount = formatAmount(getFilledAmount(order)) diff --git a/src/features/swap/components/SwapTxInfo/SwapTx.tsx b/src/features/swap/components/SwapTxInfo/SwapTx.tsx new file mode 100644 index 0000000000..08f861a8d0 --- /dev/null +++ b/src/features/swap/components/SwapTxInfo/SwapTx.tsx @@ -0,0 +1,67 @@ +import type { Order } from '@safe-global/safe-gateway-typescript-sdk' +import type { ReactElement } from 'react' +import { Box, Typography } from '@mui/material' +import TokenIcon from '@/components/common/TokenIcon' +import { formatVisualAmount } from '@/utils/formatters' + +export const SwapTx = ({ info }: { info: Order }): ReactElement => { + const { kind, sellToken, sellAmount, buyToken, buyAmount } = info + const isSellOrder = kind === 'sell' + + let from = ( + <> + <Box style={{ paddingRight: 5, display: 'inline-block' }}> + <TokenIcon logoUri={sellToken.logoUri || undefined} tokenSymbol={sellToken.symbol} /> + </Box> + <Typography component="span" fontWeight="bold"> + {formatVisualAmount(sellAmount, sellToken.decimals)} {sellToken.symbol}{' '} + </Typography> + </> + ) + + let to = ( + <> + <Box style={{ paddingLeft: 5, paddingRight: 5, display: 'inline-block' }}> + <TokenIcon logoUri={buyToken.logoUri || undefined} tokenSymbol={buyToken.symbol} /> + </Box>{' '} + <Typography component="span" fontWeight="bold"> + {buyToken.symbol} + </Typography> + </> + ) + + if (!isSellOrder) { + from = ( + <> + <Box style={{ paddingRight: 5, display: 'inline-block' }}> + <TokenIcon logoUri={sellToken.logoUri || undefined} tokenSymbol={sellToken.symbol} /> + </Box> + <Typography component="span" fontWeight="bold"> + {sellToken.symbol} + </Typography> + </> + ) + to = ( + <> + <Box style={{ paddingLeft: 5, paddingRight: 5, display: 'inline-block' }}> + <TokenIcon logoUri={buyToken.logoUri || undefined} tokenSymbol={buyToken.symbol} /> + </Box>{' '} + <Typography component="span" fontWeight="bold"> + {formatVisualAmount(buyAmount, buyToken.decimals)} {buyToken.symbol}{' '} + </Typography> + </> + ) + } + + return ( + <Box display="flex"> + <Typography component="div" display="flex" alignItems="center" fontWeight="bold"> + {from} + <Typography component="span" ml={0.5}> + to + </Typography> + {to} + </Typography> + </Box> + ) +} diff --git a/src/features/swap/components/SwapTxInfo/index.tsx b/src/features/swap/components/SwapTxInfo/interactWith.tsx similarity index 85% rename from src/features/swap/components/SwapTxInfo/index.tsx rename to src/features/swap/components/SwapTxInfo/interactWith.tsx index 4960ef865b..92f3a822d6 100644 --- a/src/features/swap/components/SwapTxInfo/index.tsx +++ b/src/features/swap/components/SwapTxInfo/interactWith.tsx @@ -1,8 +1,8 @@ import type { TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' -import EthHashInfo from '@/components/common/EthHashInfo' import { Box, Typography } from '@mui/material' +import EthHashInfo from '@/components/common/EthHashInfo' -const SwapTxInfo = ({ txData }: { txData: TransactionDetails['txData'] }) => { +const InteractWith = ({ txData }: { txData: TransactionDetails['txData'] }) => { if (!txData) return null return ( @@ -20,5 +20,4 @@ const SwapTxInfo = ({ txData }: { txData: TransactionDetails['txData'] }) => { </Typography> ) } - -export default SwapTxInfo +export default InteractWith diff --git a/src/features/swap/components/SwapWidget/index.tsx b/src/features/swap/components/SwapWidget/index.tsx new file mode 100644 index 0000000000..b0dc22ac75 --- /dev/null +++ b/src/features/swap/components/SwapWidget/index.tsx @@ -0,0 +1,82 @@ +import { Box, Button, Card, Grid, Typography } from '@mui/material' +import type { ReactElement } from 'react' +import { useCallback } from 'react' +import { WidgetBody, WidgetContainer } from '@/components/dashboard/styled' +import { Chip } from '@/components/common/Chip' + +import css from './styles.module.css' +import Link from 'next/link' +import { AppRoutes } from '@/config/routes' +import { useRouter } from 'next/router' +import useLocalStorage from '@/services/local-storage/useLocalStorage' +import { SWAP_EVENTS, SWAP_LABELS } from '@/services/analytics/events/swaps' +import Track from '@/components/common/Track' +import useIsSwapFeatureEnabled from '../../hooks/useIsSwapFeatureEnabled' + +const SWAPS_PROMO_WIDGET_IS_HIDDEN = 'swapsPromoWidgetIsHidden' + +function SwapWidget(): ReactElement | null { + const [isHidden = false, setIsHidden] = useLocalStorage<boolean>(SWAPS_PROMO_WIDGET_IS_HIDDEN) + const isSwapFeatureEnabled = useIsSwapFeatureEnabled() + + const onClick = useCallback(() => { + setIsHidden(true) + }, [setIsHidden]) + + const router = useRouter() + + if (isHidden || !isSwapFeatureEnabled) { + return null + } + + return ( + <WidgetContainer> + <WidgetBody> + <Card className={css.card}> + <Grid container className={css.grid}> + <Grid item xs> + <Box className={css.wrapper} height="100%"> + <Box> + <Typography variant="h4" className={css.title}> + Introducing native swaps + </Typography> + <Chip /> + </Box> + + <Box display="flex" flexDirection="column" height="100%"> + <Typography mt={1}> + Experience our native swaps, powered by CoW Protocol! Trade seamlessly and efficiently with decoded + transactions that are easy to understand. + </Typography> + + <Box flex={1} /> + + <Box className={css.buttonContainer} mt={3}> + <Track {...SWAP_EVENTS.OPEN_SWAPS} label={SWAP_LABELS.promoWidget}> + <Link + href={{ pathname: AppRoutes.swap, query: { safe: router.query.safe } }} + passHref + legacyBehavior + > + <Button variant="contained">Try it now</Button> + </Link> + </Track> + <Button variant="text" onClick={onClick}> + Don't show again + </Button> + </Box> + </Box> + </Box> + </Grid> + + <Grid item xs={6} className={css.imageContainer}> + <img src="/images/common/ic-swaps.svg" alt="Swap" /> + </Grid> + </Grid> + </Card> + </WidgetBody> + </WidgetContainer> + ) +} + +export default SwapWidget diff --git a/src/features/swap/components/SwapWidget/styles.module.css b/src/features/swap/components/SwapWidget/styles.module.css new file mode 100644 index 0000000000..a276f52fba --- /dev/null +++ b/src/features/swap/components/SwapWidget/styles.module.css @@ -0,0 +1,66 @@ +.label { + font-weight: 700; + margin-bottom: var(--space-2); +} + +.card { + height: inherit; + border: none; +} + +.grid { + display: flex; + flex-wrap: nowrap; + height: inherit; + gap: var(--space-3); +} + +.wrapper { + display: flex; + flex-direction: column; + gap: var(--space-1); + padding: var(--space-4); +} + +.title { + font-weight: 700; + display: inline; + margin-right: var(--space-1); +} + +.buttonContainer { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: flex-start; + gap: var(--space-2); + white-space: nowrap; + position: relative; + z-index: 2; +} + +.imageContainer { + position: relative; + z-index: 1; +} + +.imageContainer img { + display: block; + position: absolute; + z-index: 0; + right: 0; + bottom: 0; + max-width: 500px; +} + +@media (max-width: 599.95px) { + .wrapper { + padding: var(--space-3); + } +} + +@media (max-width: 970px) { + .imageContainer { + display: none; + } +} diff --git a/src/features/swap/components/TwapFallbackHandlerWarning/index.tsx b/src/features/swap/components/TwapFallbackHandlerWarning/index.tsx new file mode 100644 index 0000000000..470bb515b4 --- /dev/null +++ b/src/features/swap/components/TwapFallbackHandlerWarning/index.tsx @@ -0,0 +1,15 @@ +import { Alert, Box, SvgIcon } from '@mui/material' +import InfoOutlinedIcon from '@/public/images/notifications/info.svg' + +export const TwapFallbackHandlerWarning = () => { + return ( + <Box mt={1} mb={1}> + <Alert severity="warning" icon={<SvgIcon component={InfoOutlinedIcon} inheritViewBox color="error" />}> + <b>Enable TWAPs and submit order.</b> + {` `} + To enable TWAP orders you need to set a custom fallback handler. This software is developed by CoW Swap and Safe + will not be responsible for any possible issues with it. + </Alert> + </Box> + ) +} diff --git a/src/features/swap/constants.ts b/src/features/swap/constants.ts new file mode 100644 index 0000000000..c6245fac90 --- /dev/null +++ b/src/features/swap/constants.ts @@ -0,0 +1,6 @@ +export const SWAP_TITLE = 'Safe Swap' +export const SWAP_ORDER_TITLE = 'Swap order' +export const LIMIT_ORDER_TITLE = 'Limit order' +export const TWAP_ORDER_TITLE = 'TWAP order' + +export const SWAP_FEE_RECIPIENT = '0x63695Eee2c3141BDE314C5a6f89B98E62808d716' diff --git a/src/features/swap/helpers/__tests__/fee.test.ts b/src/features/swap/helpers/__tests__/fee.test.ts new file mode 100644 index 0000000000..003bc289bf --- /dev/null +++ b/src/features/swap/helpers/__tests__/fee.test.ts @@ -0,0 +1,131 @@ +import { calculateFeePercentageInBps } from '@/features/swap/helpers/fee' +import { type OnTradeParamsPayload } from '@cowprotocol/events' +import { stableCoinAddresses } from '@/features/swap/helpers/data/stablecoins' + +describe('calculateFeePercentageInBps', () => { + it('returns correct fee for non-stablecoin and sell order', () => { + let orderParams: OnTradeParamsPayload = { + sellToken: { address: 'non-stablecoin-address' }, + buyToken: { address: 'non-stablecoin-address' }, + buyTokenFiatAmount: '50000', + sellTokenFiatAmount: '50000', + orderKind: 'sell', + } as OnTradeParamsPayload + + const result = calculateFeePercentageInBps(orderParams) + expect(result).toBe(35) + + orderParams = { + ...orderParams, + buyTokenFiatAmount: '100000', + sellTokenFiatAmount: '100000', + } + + const result2 = calculateFeePercentageInBps(orderParams) + expect(result2).toBe(20) + + orderParams = { + ...orderParams, + buyTokenFiatAmount: '1000000', + sellTokenFiatAmount: '1000000', + } + + const result3 = calculateFeePercentageInBps(orderParams) + expect(result3).toBe(10) + }) + + it('returns correct fee for non-stablecoin and buy order', () => { + let orderParams: OnTradeParamsPayload = { + sellToken: { address: 'non-stablecoin-address' }, + buyToken: { address: 'non-stablecoin-address' }, + buyTokenFiatAmount: '50000', + sellTokenFiatAmount: '50000', + orderKind: 'buy', + } as OnTradeParamsPayload + + const result = calculateFeePercentageInBps(orderParams) + expect(result).toBe(35) + + orderParams = { + ...orderParams, + buyTokenFiatAmount: '100000', + sellTokenFiatAmount: '100000', + } + + const result2 = calculateFeePercentageInBps(orderParams) + expect(result2).toBe(20) + + orderParams = { + ...orderParams, + buyTokenFiatAmount: '1000000', + sellTokenFiatAmount: '1000000', + } + + const result3 = calculateFeePercentageInBps(orderParams) + expect(result3).toBe(10) + }) + + it('returns correct fee for stablecoin and sell order', () => { + const stableCoinAddressesKeys = Object.keys(stableCoinAddresses) + let orderParams: OnTradeParamsPayload = { + sellToken: { address: stableCoinAddressesKeys[0] }, + buyToken: { address: stableCoinAddressesKeys[1] }, + buyTokenFiatAmount: '50000', + sellTokenFiatAmount: '50000', + orderKind: 'sell', + } as OnTradeParamsPayload + + const result = calculateFeePercentageInBps(orderParams) + expect(result).toBe(10) + + orderParams = { + ...orderParams, + buyTokenFiatAmount: '100000', + sellTokenFiatAmount: '100000', + } + + const result2 = calculateFeePercentageInBps(orderParams) + expect(result2).toBe(7) + + orderParams = { + ...orderParams, + buyTokenFiatAmount: '1000000', + sellTokenFiatAmount: '1000000', + } + + const result3 = calculateFeePercentageInBps(orderParams) + expect(result3).toBe(5) + }) + + it('returns correct fee for stablecoin and buy order', () => { + const stableCoinAddressesKeys = Object.keys(stableCoinAddresses) + let orderParams: OnTradeParamsPayload = { + sellToken: { address: stableCoinAddressesKeys[0] }, + buyToken: { address: stableCoinAddressesKeys[1] }, + buyTokenFiatAmount: '50000', + sellTokenFiatAmount: '50000', + orderKind: 'buy', + } as OnTradeParamsPayload + + const result = calculateFeePercentageInBps(orderParams) + expect(result).toBe(10) + + orderParams = { + ...orderParams, + buyTokenFiatAmount: '100000', + sellTokenFiatAmount: '100000', + } + + const result2 = calculateFeePercentageInBps(orderParams) + expect(result2).toBe(7) + + orderParams = { + ...orderParams, + buyTokenFiatAmount: '1000000', + sellTokenFiatAmount: '1000000', + } + + const result3 = calculateFeePercentageInBps(orderParams) + expect(result3).toBe(5) + }) +}) diff --git a/src/features/swap/helpers/__tests__/utils.test.ts b/src/features/swap/helpers/__tests__/utils.test.ts index e04cdfc629..17ebb380f5 100644 --- a/src/features/swap/helpers/__tests__/utils.test.ts +++ b/src/features/swap/helpers/__tests__/utils.test.ts @@ -2,9 +2,13 @@ import { getExecutionPrice, getFilledPercentage, getLimitPrice, + getPartiallyFilledSurplus, getSurplusPrice, isOrderPartiallyFilled, + isSettingTwapFallbackHandler, + TWAP_FALLBACK_HANDLER, } from '../utils' +import type { DecodedDataResponse } from '@safe-global/safe-gateway-typescript-sdk' import { type SwapOrder } from '@safe-global/safe-gateway-typescript-sdk' describe('Swap helpers', () => { @@ -252,4 +256,156 @@ describe('Swap helpers', () => { expect(result1).toBe(false) }) }) + describe('getPartiallyFilledSurplusPrice', () => { + it('returns 0 for partially filled sell order with no surplus', () => { + const mockOrder = { + sellAmount: '100000000000000000000', // 100 tokens + executedSellAmount: '50000000000000000000', // 50 tokens + executedBuyAmount: '50000000000000000000', // 50 tokens + buyAmount: '100000000000000000000', // 100 tokens + kind: 'sell', + buyToken: { decimals: 18 }, + sellToken: { decimals: 18 }, + } as unknown as SwapOrder + + const result = getPartiallyFilledSurplus(mockOrder) + + expect(result).toEqual(0) + }) + it('returns 0 for partially filled buy order with no surplus', () => { + const mockOrder = { + sellAmount: '100000000000000000000', // 100 tokens + executedSellAmount: '50000000000000000000', // 50 tokens + executedBuyAmount: '50000000000000000000', // 50 tokens + buyAmount: '100000000000000000000', // 100 tokens + kind: 'buy', + buyToken: { decimals: 18 }, + sellToken: { decimals: 18 }, + } as unknown as SwapOrder + + const result = getPartiallyFilledSurplus(mockOrder) + + expect(result).toEqual(0) + }) + it('returns surplus for partially filled sell orders', () => { + const mockOrder = { + sellAmount: '100000000000000000000', // 100 tokens + executedSellAmount: '50000000000000000000', // 50 tokens + executedBuyAmount: '55000000000000000000', // 55 tokens + buyAmount: '100000000000000000000', // 100 tokens + kind: 'sell', + buyToken: { decimals: 18 }, + sellToken: { decimals: 18 }, + } as unknown as SwapOrder + + const result = getPartiallyFilledSurplus(mockOrder) + expect(result).toEqual(5) + }) + it('returns surplus for partially filled buy orders', () => { + const mockOrder = { + sellAmount: '100000000000000000000', // 100 tokens + executedSellAmount: '45000000000000000000', // 50 tokens + executedBuyAmount: '50000000000000000000', // 55 tokens + buyAmount: '100000000000000000000', // 100 tokens + kind: 'buy', + buyToken: { decimals: 18 }, + sellToken: { decimals: 18 }, + } as unknown as SwapOrder + + const result = getPartiallyFilledSurplus(mockOrder) + + expect(result).toEqual(5) + }) + }) + + describe('isSettingTwapFallbackHandler', () => { + it('should return true when handler is TWAP_FALLBACK_HANDLER', () => { + const decodedData = { + parameters: [ + { + valueDecoded: [ + { + dataDecoded: { + method: 'setFallbackHandler', + parameters: [{ name: 'handler', value: TWAP_FALLBACK_HANDLER }], + }, + }, + ], + }, + ], + } as unknown as DecodedDataResponse + expect(isSettingTwapFallbackHandler(decodedData)).toBe(true) + }) + + it('should return false when handler is not TWAP_FALLBACK_HANDLER', () => { + const decodedData = { + parameters: [ + { + valueDecoded: [ + { + dataDecoded: { + method: 'setFallbackHandler', + parameters: [{ name: 'handler', value: '0xDifferentHandler' }], + }, + }, + ], + }, + ], + } as unknown as DecodedDataResponse + expect(isSettingTwapFallbackHandler(decodedData)).toBe(false) + }) + + it('should return false when method is not setFallbackHandler', () => { + const decodedData = { + parameters: [ + { + valueDecoded: [ + { + dataDecoded: { + method: 'differentMethod', + parameters: [{ name: 'handler', value: TWAP_FALLBACK_HANDLER }], + }, + }, + ], + }, + ], + } as unknown as DecodedDataResponse + expect(isSettingTwapFallbackHandler(decodedData)).toBe(false) + }) + + it('should return false when decodedData is undefined', () => { + expect(isSettingTwapFallbackHandler(undefined)).toBe(false) + }) + + it('should return false when parameters are missing', () => { + const decodedData = {} as unknown as DecodedDataResponse + expect(isSettingTwapFallbackHandler(decodedData)).toBe(false) + }) + + it('should return false when valueDecoded is missing', () => { + const decodedData = { + parameters: [ + { + valueDecoded: null, + }, + ], + } as unknown as DecodedDataResponse + expect(isSettingTwapFallbackHandler(decodedData)).toBe(false) + }) + + it('should return false when dataDecoded is missing', () => { + const decodedData = { + parameters: [ + { + valueDecoded: [ + { + dataDecoded: null, + }, + ], + }, + ], + } as unknown as DecodedDataResponse + expect(isSettingTwapFallbackHandler(decodedData)).toBe(false) + }) + }) }) diff --git a/src/features/swap/helpers/data/stablecoins.ts b/src/features/swap/helpers/data/stablecoins.ts new file mode 100644 index 0000000000..b818b11d60 --- /dev/null +++ b/src/features/swap/helpers/data/stablecoins.ts @@ -0,0 +1,509 @@ +export const stableCoinAddresses: { + [address: string]: { + name: string + symbol: string + chains: Array<'gnosis' | 'ethereum' | 'arbitrum-one' | 'sepolia'> + } +} = { + '0xdd96b45877d0e8361a4ddb732da741e97f3191ff': { + name: 'BUSD Token from BSC', + symbol: 'BUSD', + chains: ['gnosis'], + }, + '0x44fa8e6f47987339850636f88629646662444217': { + name: 'Dai Stablecoin on Gnosis', + symbol: 'DAI', + chains: ['gnosis'], + }, + '0x1e37e5b504f7773460d6eb0e24d2e7c223b66ec7': { + name: 'HUSD on Gnosis', + symbol: 'HUSD', + chains: ['gnosis'], + }, + '0xb714654e905edad1ca1940b7790a8239ece5a9ff': { + name: 'TrueUSD on Gnosis', + symbol: 'TUSD', + chains: ['gnosis'], + }, + '0xddafbb505ad214d7b80b1f830fccc89b60fb7a83': { + name: 'USD//C on Gnosis', + symbol: 'USDC', + chains: ['gnosis'], + }, + '0x4ecaba5870353805a9f068101a40e0f32ed605c6': { + name: 'Tether on Gnosis', + symbol: 'USDT', + chains: ['gnosis'], + }, + '0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063': { + name: 'Dai Stablecoin', + symbol: 'DAI', + chains: ['gnosis'], + }, + '0x104592a158490a9228070E0A8e5343B499e125D0': { + name: 'Frax', + symbol: 'FRAX', + chains: ['gnosis'], + }, + '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174': { + name: 'USD Coin', + symbol: 'USDC', + chains: ['gnosis'], + }, + '0xc2132D05D31c914a87C6611C10748AEb04B58e8F': { + name: 'Tether USD', + symbol: 'USDT', + chains: ['gnosis'], + }, + '0x3e7676937A7E96CFB7616f255b9AD9FF47363D4b': { + name: 'Dai Stablecoin', + symbol: 'DAI', + chains: ['gnosis'], + }, + '0x0faF6df7054946141266420b43783387A78d82A9': { + name: 'USDC from Ethereum', + symbol: 'USDC', + chains: ['gnosis'], + }, + '0xcB1e72786A6eb3b44C2a2429e317c8a2462CFeb1': { + name: 'Dai Stablecoin', + symbol: 'DAI', + chains: ['gnosis'], + }, + '0x3813e82e6f7098b9583FC0F33a962D02018B6803': { + name: 'Tether USD', + symbol: 'USDT', + chains: ['gnosis'], + }, + + '0xdac17f958d2ee523a2206206994597c13d831ec7': { + name: 'Tether', + symbol: 'usdt', + chains: ['ethereum'], + }, + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': { + name: 'USDC', + symbol: 'usdc', + chains: ['ethereum'], + }, + '0xaf88d065e77c8cc2239327c5edb3a432268e5831': { + name: 'USDC', + symbol: 'usdc', + chains: ['arbitrum-one'], + }, + '0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9': { + name: 'USDT', + symbol: 'usdt', + chains: ['arbitrum-one'], + }, + '0x6b175474e89094c44da98b954eedeac495271d0f': { + name: 'Dai', + symbol: 'dai', + chains: ['ethereum'], + }, + '0xda10009cbd5d07dd0cecc66161fc93d7c9000da1': { + name: 'Dai', + symbol: 'dai', + chains: ['arbitrum-one'], + }, + '0x4c9edd5852cd905f086c759e8383e09bff1e68b3': { + name: 'Ethena USDe', + symbol: 'usde', + chains: ['ethereum'], + }, + '0xc5f0f7b66764f6ec8c8dff7ba683102295e16409': { + name: 'First Digital USD', + symbol: 'fdusd', + chains: ['ethereum'], + }, + '0x0c10bf8fcb7bf5412187a595ab97a3609160b5c6': { + name: 'USDD', + symbol: 'usdd', + chains: ['ethereum'], + }, + '0x680447595e8b7b3aa1b43beb9f6098c79ac2ab3f': { + name: 'USDD', + symbol: 'usdd', + chains: ['arbitrum-one'], + }, + '0x853d955acef822db058eb8505911ed77f175b99e': { + name: 'Frax', + symbol: 'frax', + chains: ['ethereum'], + }, + '0x17fc002b466eec40dae837fc4be5c67993ddbd6f': { + name: 'Frax', + symbol: 'frax', + chains: ['arbitrum-one'], + }, + '0x68749665ff8d2d112fa859aa293f07a622782f38': { + name: 'Tether Gold', + symbol: 'xaut', + chains: ['ethereum'], + }, + '0x0000000000085d4780b73119b644ae5ecd22b376': { + name: 'TrueUSD', + symbol: 'tusd', + chains: ['ethereum'], + }, + '0x45804880de22913dafe09f4980848ece6ecbaf78': { + name: 'PAX Gold', + symbol: 'paxg', + chains: ['ethereum'], + }, + '0x6c3ea9036406852006290770bedfcaba0e23a0e8': { + name: 'PayPal USD', + symbol: 'pyusd', + chains: ['ethereum'], + }, + '0xbc6da0fe9ad5f3b0d58160288917aa56653660e9': { + name: 'Alchemix USD', + symbol: 'alusd', + chains: ['ethereum'], + }, + '0xdb25f211ab05b1c97d595516f45794528a807ad8': { + name: 'STASIS EURO', + symbol: 'eurs', + chains: ['ethereum'], + }, + '0x8e870d67f660d95d5be530380d0ec0bd388289e1': { + name: 'Pax Dollar', + symbol: 'usdp', + chains: ['ethereum'], + }, + '0xf939e0a03fb07f59a73314e73794be0e57ac1b4e': { + name: 'crvUSD', + symbol: 'crvusd', + chains: ['ethereum'], + }, + '0x498bf2b1e120fed3ad3d42ea2165e9b73f99c1e5': { + name: 'crvUSD', + symbol: 'crvusd', + chains: ['arbitrum-one'], + }, + '0x056fd409e1d7a124bd7017459dfea2f387b6d5cd': { + name: 'Gemini Dollar', + symbol: 'gusd', + chains: ['ethereum'], + }, + '0x865377367054516e17014ccded1e7d814edc9ce4': { + name: 'DOLA', + symbol: 'dola', + chains: ['ethereum'], + }, + '0x6a7661795c374c0bfc635934efaddff3a7ee23b6': { + name: 'DOLA', + symbol: 'dola', + chains: ['arbitrum-one'], + }, + '0x40d16fc0246ad3160ccc09b8d0d3a2cd28ae6c2f': { + name: 'GHO', + symbol: 'gho', + chains: ['ethereum'], + }, + '0x5f98805a4e8be255a32880fdec7f6728c6568ba0': { + name: 'Liquity USD', + symbol: 'lusd', + chains: ['ethereum'], + }, + '0x93b346b6bc2548da6a1e7d98e9a421b42541425b': { + name: 'Liquity USD', + symbol: 'lusd', + chains: ['arbitrum-one'], + }, + '0x4fabb145d64652a948d72533023f6e7a623c7c53': { + name: 'BUSD', + symbol: 'busd', + chains: ['ethereum'], + }, + '0x99d8a9c45b2eca8864373a26d1459e3dff1e17f3': { + name: 'Magic Internet Money (Ethereum)', + symbol: 'mim', + chains: ['ethereum'], + }, + '0x59d9356e565ab3a36dd77763fc0d87feaf85508c': { + name: 'Mountain Protocol USD', + symbol: 'usdm', + chains: ['ethereum', 'arbitrum-one'], + }, + '0xbea0000029ad1c77d3d5d23ba2d8893db9d1efab': { + name: 'Bean', + symbol: 'bean', + chains: ['ethereum'], + }, + '0x57ab1ec28d129707052df4df418d58a2d46d5f51': { + name: 'sUSD', + symbol: 'susd', + chains: ['ethereum'], + }, + '0xa970af1a584579b618be4d69ad6f73459d112f95': { + name: 'sUSD', + symbol: 'susd', + chains: ['arbitrum-one'], + }, + '0xc581b735a1688071a1746c968e0798d642ede491': { + name: 'Euro Tether', + symbol: 'eurt', + chains: ['ethereum'], + }, + '0xa774ffb4af6b0a91331c084e1aebae6ad535e6f3': { + name: 'flexUSD', + symbol: 'flexusd', + chains: ['ethereum'], + }, + '0x1a7e4e63778b4f12a199c062f3efdd288afcbce8': { + name: 'EURA', + symbol: 'eura', + chains: ['ethereum'], + }, + '0xfa5ed56a203466cbbc2430a43c66b9d8723528e7': { + name: 'EURA', + symbol: 'eura', + chains: ['arbitrum-one'], + }, + '0xe07f9d810a48ab5c3c914ba3ca53af14e4491e8a': { + name: 'Gyroscope GYD', + symbol: 'gyd', + chains: ['ethereum'], + }, + '0xca5d8f8a8d49439357d3cf46ca2e720702f132b8': { + name: 'Gyroscope GYD', + symbol: 'gyd', + chains: ['arbitrum-one'], + }, + '0x2c537e5624e4af88a7ae4060c022609376c8d0eb': { + name: 'BiLira', + symbol: 'tryb', + chains: ['ethereum'], + }, + '0x0e573ce2736dd9637a0b21058352e1667925c7a8': { + name: 'Verified USD', + symbol: 'usdv', + chains: ['ethereum'], + }, + '0x323665443cef804a3b5206103304bd4872ea4253': { + name: 'Verified USD', + symbol: 'usdv', + chains: ['arbitrum-one'], + }, + '0x956f47f50a910163d8bf957cf5846d573e7f87ca': { + name: 'Fei USD', + symbol: 'fei', + chains: ['ethereum'], + }, + '0x0a5e677a6a24b2f1a2bf4f3bffc443231d2fdec8': { + name: 'dForce USD', + symbol: 'usx', + chains: ['ethereum'], + }, + '0x641441c631e2f909700d2f41fd87f0aa6a6b4edb': { + name: 'dForce USD', + symbol: 'usx', + chains: ['arbitrum-one'], + }, + '0x4591dbff62656e7859afe5e45f6f47d3669fbb28': { + name: 'Prisma mkUSD', + symbol: 'mkusd', + chains: ['ethereum'], + }, + '0xc08512927d12348f6620a698105e1baac6ecd911': { + name: 'GYEN', + symbol: 'gyen', + chains: ['ethereum'], + }, + '0x589d35656641d6ab57a545f08cf473ecd9b6d5f7': { + name: 'GYEN', + symbol: 'gyen', + chains: ['arbitrum-one'], + }, + '0xdf3ac4f479375802a821f7b7b46cd7eb5e4262cc': { + name: 'eUSD', + symbol: 'eusd', + chains: ['ethereum'], + }, + '0x2a8e1e676ec238d8a992307b495b45b3feaa5e86': { + name: 'Origin Dollar', + symbol: 'ousd', + chains: ['ethereum'], + }, + '0x1b3c515f58857e141a966b33182f2f3feecc10e9': { + name: 'USK', + symbol: 'usk', + chains: ['ethereum'], + }, + '0xdf574c24545e5ffecb9a659c229253d4111d87e1': { + name: 'HUSD', + symbol: 'husd', + chains: ['ethereum'], + }, + '0xe2f2a5c287993345a840db3b0845fbc70f5935a5': { + name: 'mStable USD', + symbol: 'musd', + chains: ['ethereum'], + }, + '0x6e109e9dd7fa1a58bc3eff667e8e41fc3cc07aef': { + name: 'CNH Tether', + symbol: 'cnht', + chains: ['ethereum'], + }, + '0x6ba75d640bebfe5da1197bb5a2aff3327789b5d3': { + name: 'VNX EURO', + symbol: 'veur', + chains: ['ethereum'], + }, + '0x70e8de73ce538da2beed35d14187f6959a8eca96': { + name: 'XSGD', + symbol: 'xsgd', + chains: ['ethereum'], + }, + '0x97de57ec338ab5d51557da3434828c5dbfada371': { + name: 'eUSD (OLD)', + symbol: 'eusd', + chains: ['ethereum'], + }, + '0x68037790a0229e9ce6eaa8a99ea92964106c4703': { + name: 'Parallel', + symbol: 'par', + chains: ['ethereum'], + }, + '0x1cfa5641c01406ab8ac350ded7d735ec41298372': { + name: 'Convertible JPY Token', + symbol: 'cjpy', + chains: ['ethereum'], + }, + '0xd74f5255d557944cf7dd0e45ff521520002d5748': { + name: 'Sperax USD', + symbol: 'usds', + chains: ['arbitrum-one'], + }, + '0xd71ecff9342a5ced620049e616c5035f1db98620': { + name: 'sEUR', + symbol: 'seur', + chains: ['ethereum'], + }, + '0x38547d918b9645f2d94336b6b61aeb08053e142c': { + name: 'USC', + symbol: 'usc', + chains: ['ethereum'], + }, + '0x45fdb1b92a649fb6a64ef1511d3ba5bf60044838': { + name: 'SpiceUSD', + symbol: 'usds', + chains: ['ethereum'], + }, + '0xebf2096e01455108badcbaf86ce30b6e5a72aa52': { + name: 'XIDR', + symbol: 'xidr', + chains: ['ethereum'], + }, + '0xb0b195aefa3650a6908f15cdac7d92f8a5791b0b': { + name: 'BOB', + symbol: 'bob', + chains: ['ethereum', 'arbitrum-one'], + }, + '0x86b4dbe5d203e634a12364c0e428fa242a3fba98': { + name: 'poundtoken', + symbol: 'gbpt', + chains: ['ethereum'], + }, + '0xd90e69f67203ebe02c917b5128629e77b4cd92dc': { + name: 'One Cash', + symbol: 'onc', + chains: ['ethereum'], + }, + '0x3449fc1cd036255ba1eb19d65ff4ba2b8903a69a': { + name: 'Basis Cash', + symbol: 'bac', + chains: ['ethereum'], + }, + '0xc285b7e09a4584d027e5bc36571785b515898246': { + name: 'Coin98 Dollar', + symbol: 'cusd', + chains: ['ethereum'], + }, + '0x64343594ab9b56e99087bfa6f2335db24c2d1f17': { + name: 'Vesta Stable', + symbol: 'vst', + chains: ['arbitrum-one'], + }, + '0x2370f9d504c7a6e775bf6e14b3f12846b594cd53': { + name: 'JPY Coin v1', + symbol: 'jpyc', + chains: ['ethereum'], + }, + '0x53dfea0a8cc2a2a2e425e1c174bc162999723ea0': { + name: 'Jarvis Synthetic Swiss Franc', + symbol: 'jchf', + chains: ['ethereum'], + }, + '0x0f17bc9a994b87b5225cfb6a2cd4d667adb4f20b': { + name: 'Jarvis Synthetic Euro', + symbol: 'jeur', + chains: ['ethereum'], + }, + '0x3231cb76718cdef2155fc47b5286d82e6eda273f': { + name: 'Monerium EUR emoney', + symbol: 'eure', + chains: ['ethereum'], + }, + '0x65d72aa8da931f047169112fcf34f52dbaae7d18': { + name: 'f(x) rUSD', + symbol: 'rusd', + chains: ['ethereum'], + }, + '0x085780639cc2cacd35e474e71f4d000e2405d8f6': { + name: 'f(x) Protocol fxUSD', + symbol: 'fxusd', + chains: ['ethereum'], + }, + '0xa663b02cf0a4b149d2ad41910cb81e23e1c41c32': { + name: 'Staked FRAX', + symbol: 'sfrax', + chains: ['ethereum'], + }, + '0xe3b3fe7bca19ca77ad877a5bebab186becfad906': { + name: 'Staked FRAX', + symbol: 'sfrax', + chains: ['arbitrum-one'], + }, + '0xcfc5bd99915aaa815401c5a41a927ab7a38d29cf': { + name: 'Threshold USD', + symbol: 'thusd', + chains: ['ethereum'], + }, + '0xa47c8bf37f92abed4a126bda807a7b7498661acd': { + name: 'Wrapped USTC', + symbol: 'ustc', + chains: ['ethereum'], + }, + '0x3509f19581afedeff07c53592bc0ca84e4855475': { + name: 'xDollar Stablecoin', + symbol: 'xusd', + chains: ['arbitrum-one'], + }, + '0x431d5dff03120afa4bdf332c61a6e1766ef37bdb': { + name: 'JPY Coin', + symbol: 'jpyc', + chains: ['ethereum'], + }, + '0xb6667b04cb61aa16b59617f90ffa068722cf21da': { + name: 'Worldwide USD', + symbol: 'wusd', + chains: ['ethereum'], + }, + '0xB4F1737Af37711e9A5890D9510c9bB60e170CB0D': { + name: 'COW Dai Stablecoin', + symbol: 'DAI', + chains: ['sepolia'], + }, + '0xbe72E441BF55620febc26715db68d3494213D8Cb': { + name: 'COW USD Coin', + symbol: 'USDC', + chains: ['sepolia'], + }, + '0x58eb19ef91e8a6327fed391b51ae1887b833cc91': { + name: 'COW Tether USD', + symbol: 'USDT', + chains: ['sepolia'], + }, +} diff --git a/src/features/swap/helpers/fee.ts b/src/features/swap/helpers/fee.ts new file mode 100644 index 0000000000..0a7cc79d32 --- /dev/null +++ b/src/features/swap/helpers/fee.ts @@ -0,0 +1,52 @@ +import type { OnTradeParamsPayload } from '@cowprotocol/events' +import { stableCoinAddresses } from '@/features/swap/helpers/data/stablecoins' + +const FEE_PERCENTAGE_BPS = { + REGULAR: { + TIER_1: 35, + TIER_2: 20, + TIER_3: 10, + }, + STABLE: { + TIER_1: 10, + TIER_2: 7, + TIER_3: 5, + }, +} + +const FEE_TIERS = { + TIER_1: 100_000, // 0 - 100k + TIER_2: 1_000_000, // 100k - 1m +} + +const getLowerCaseStableCoinAddresses = () => { + const lowerCaseStableCoinAddresses = Object.keys(stableCoinAddresses).reduce((result, key) => { + result[key.toLowerCase()] = stableCoinAddresses[key] + return result + }, {} as typeof stableCoinAddresses) + + return lowerCaseStableCoinAddresses +} +/** + * Function to calculate the fee % in bps to apply for a trade. + * The fee % should be applied based on the fiat value of the buy or sell token. + * + * @param orderParams + */ +export const calculateFeePercentageInBps = (orderParams: OnTradeParamsPayload) => { + const { sellToken, buyToken, buyTokenFiatAmount, sellTokenFiatAmount, orderKind } = orderParams + const stableCoins = getLowerCaseStableCoinAddresses() + const isStableCoin = stableCoins[sellToken?.address?.toLowerCase()] && stableCoins[buyToken?.address.toLowerCase()] + + const fiatAmount = Number(orderKind == 'sell' ? sellTokenFiatAmount : buyTokenFiatAmount) || 0 + + if (fiatAmount < FEE_TIERS.TIER_1) { + return isStableCoin ? FEE_PERCENTAGE_BPS.STABLE.TIER_1 : FEE_PERCENTAGE_BPS.REGULAR.TIER_1 + } + + if (fiatAmount < FEE_TIERS.TIER_2) { + return isStableCoin ? FEE_PERCENTAGE_BPS.STABLE.TIER_2 : FEE_PERCENTAGE_BPS.REGULAR.TIER_2 + } + + return isStableCoin ? FEE_PERCENTAGE_BPS.STABLE.TIER_3 : FEE_PERCENTAGE_BPS.REGULAR.TIER_3 +} diff --git a/src/features/swap/helpers/swapOrderBuilder.ts b/src/features/swap/helpers/swapOrderBuilder.ts index 863e4b5c95..cf5b9d156b 100644 --- a/src/features/swap/helpers/swapOrderBuilder.ts +++ b/src/features/swap/helpers/swapOrderBuilder.ts @@ -1,14 +1,17 @@ import { Builder, type IBuilder } from '@/tests/Builder' import { faker } from '@faker-js/faker' import type { - SwapOrder, OrderToken, + SwapOrder, TransactionInfoType, - CowSwapConfirmationView, + TwapOrder, + SwapOrderConfirmationView, } from '@safe-global/safe-gateway-typescript-sdk' +import { ConfirmationViewTypes } from '@safe-global/safe-gateway-typescript-sdk' +import { DurationType, StartTimeValue } from '@safe-global/safe-gateway-typescript-sdk' export function appDataBuilder( - orderClass: 'limit' | 'market' | 'liquidity' = 'limit', + orderClass: 'limit' | 'market' | 'twap' | 'liquidity' = 'limit', ): IBuilder<Record<string, unknown>> { return Builder.new<Record<string, unknown>>().with({ appCode: 'Safe Wallet Swaps', @@ -65,11 +68,44 @@ export function swapOrderBuilder(): IBuilder<SwapOrder> { }) } +export function twapOrderBuilder(): IBuilder<TwapOrder> { + return Builder.new<TwapOrder>().with({ + type: 'TwapOrder' as TransactionInfoType.TWAP_ORDER, + status: faker.helpers.arrayElement(['presignaturePending', 'open', 'cancelled', 'fulfilled', 'expired']), + kind: faker.helpers.arrayElement(['buy', 'sell']), + orderClass: faker.helpers.arrayElement(['limit', 'market', 'liquidity']), + validUntil: faker.date.future().getTime(), + sellAmount: faker.string.numeric(), + buyAmount: faker.string.numeric(), + executedSellAmount: faker.string.numeric(), + executedBuyAmount: faker.string.numeric(), + sellToken: orderTokenBuilder().build(), + buyToken: orderTokenBuilder().build(), + executedSurplusFee: faker.string.numeric(), + fullAppData: appDataBuilder().build(), + numberOfParts: faker.number.int({ min: 1, max: 10 }).toString(), + /** @description The amount of sellToken to sell in each part */ + partSellAmount: faker.string.numeric(), + /** @description The amount of buyToken that must be bought in each part */ + minPartLimit: faker.string.numeric(), + /** @description The duration of the TWAP interval */ + timeBetweenParts: faker.number.int({ min: 1, max: 10000000 }), + /** @description Whether the TWAP is valid for the entire interval or not */ + durationOfPart: { + durationType: DurationType.AUTO, + }, + /** @description The start time of the TWAP */ + startTime: { + startType: StartTimeValue.AT_MINING_TIME, + }, + }) +} + // create a builder for SwapOrderConfirmationView -export function swapOrderConfirmationViewBuilder(): IBuilder<CowSwapConfirmationView> { +export function swapOrderConfirmationViewBuilder(): IBuilder<SwapOrderConfirmationView> { const ownerAndReceiver = faker.finance.ethereumAddress() - return Builder.new<CowSwapConfirmationView>().with({ - type: 'COW_SWAP_ORDER', + return Builder.new<SwapOrderConfirmationView>().with({ + type: ConfirmationViewTypes.COW_SWAP_ORDER, uid: faker.string.uuid(), kind: faker.helpers.arrayElement(['buy', 'sell']), orderClass: faker.helpers.arrayElement(['limit', 'market', 'liquidity']), diff --git a/src/features/swap/helpers/utils.ts b/src/features/swap/helpers/utils.ts index 495586cd6d..318fad0b79 100644 --- a/src/features/swap/helpers/utils.ts +++ b/src/features/swap/helpers/utils.ts @@ -1,13 +1,15 @@ -import type { SwapOrder } from '@safe-global/safe-gateway-typescript-sdk' +import type { DecodedDataResponse, Order as SwapOrder } from '@safe-global/safe-gateway-typescript-sdk' import { formatUnits } from 'ethers' -import type { AnyAppDataDocVersion, latest } from '@cowprotocol/app-data' +import type { AnyAppDataDocVersion, latest, LatestAppDataDocVersion } from '@cowprotocol/app-data' + +import { TradeType, UiOrderType } from '@/features/swap/types' type Quantity = { amount: string | number | bigint decimals: number } -enum OrderKind { +export enum OrderKind { SELL = 'sell', BUY = 'buy', } @@ -20,6 +22,8 @@ function asDecimal(amount: number | bigint, decimals: number): number { return Number(formatUnits(amount, decimals)) } +export const TWAP_FALLBACK_HANDLER = '0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5' + export const getExecutionPrice = ( order: Pick<SwapOrder, 'executedSellAmount' | 'executedBuyAmount' | 'buyToken' | 'sellToken'>, ): number => { @@ -37,16 +41,13 @@ export const getExecutionPrice = ( } export const getLimitPrice = ( - order: Pick<SwapOrder, 'sellAmount' | 'buyAmount' | 'buyAmount' | 'buyToken' | 'sellToken'>, + order: Pick<SwapOrder, 'sellAmount' | 'buyAmount' | 'buyToken' | 'sellToken'>, ): number => { const { sellAmount, buyAmount, buyToken, sellToken } = order const ratio = calculateRatio( { amount: sellAmount, decimals: sellToken.decimals }, - { - amount: buyAmount, - decimals: buyToken.decimals, - }, + { amount: buyAmount, decimals: buyToken.decimals }, ) return ratio @@ -72,6 +73,49 @@ export const getSurplusPrice = ( } } +export const getPartiallyFilledSurplus = (order: SwapOrder): number => { + if (order.kind === OrderKind.BUY) { + return getPartiallyFilledBuySurplus(order) + } else if (order.kind === OrderKind.SELL) { + return getPartiallyFilledSellSurplus(order) + } else { + return 0 + } +} + +const getPartiallyFilledBuySurplus = ( + order: Pick< + SwapOrder, + 'executedBuyAmount' | 'buyAmount' | 'buyToken' | 'executedSellAmount' | 'sellAmount' | 'sellToken' | 'kind' + >, +): number => { + const { executedSellAmount, sellAmount, sellToken, executedBuyAmount, buyAmount, buyToken } = order + + const limitPrice = calculateRatio( + { amount: sellAmount, decimals: sellToken.decimals }, + { amount: buyAmount, decimals: buyToken.decimals }, + ) + const maximumSellAmount = asDecimal(BigInt(executedBuyAmount), buyToken.decimals) * limitPrice + return maximumSellAmount - asDecimal(BigInt(executedSellAmount), sellToken.decimals) +} + +const getPartiallyFilledSellSurplus = ( + order: Pick< + SwapOrder, + 'executedBuyAmount' | 'buyAmount' | 'buyToken' | 'executedSellAmount' | 'sellAmount' | 'sellToken' | 'kind' + >, +): number => { + const { executedSellAmount, sellAmount, sellToken, executedBuyAmount, buyAmount, buyToken } = order + + const limitPrice = calculateRatio( + { amount: buyAmount, decimals: buyToken.decimals }, + { amount: sellAmount, decimals: sellToken.decimals }, + ) + + const minimumBuyAmount = asDecimal(BigInt(executedSellAmount), sellToken.decimals) * limitPrice + return asDecimal(BigInt(executedBuyAmount), buyToken.decimals) - minimumBuyAmount +} + export const getFilledPercentage = ( order: Pick<SwapOrder, 'executedBuyAmount' | 'kind' | 'buyAmount' | 'executedSellAmount' | 'sellAmount'>, ): string => { @@ -117,6 +161,13 @@ export const getOrderClass = (order: Pick<SwapOrder, 'fullAppData'>): latest.Ord return orderClass || 'market' } +export const getOrderFeeBps = (order: Pick<SwapOrder, 'fullAppData'>): number => { + const fullAppData = order.fullAppData as unknown as LatestAppDataDocVersion + const basisPoints = (fullAppData?.metadata?.partnerFee as latest.PartnerFee)?.bps + + return Number(basisPoints) || 0 +} + export const isOrderPartiallyFilled = ( order: Pick<SwapOrder, 'executedBuyAmount' | 'executedSellAmount' | 'sellAmount' | 'buyAmount' | 'kind'>, ): boolean => { @@ -131,3 +182,30 @@ export const isOrderPartiallyFilled = ( return BigInt(executedSellAmount) !== 0n && executedSellAmount < sellAmount } + +export const UiOrderTypeToOrderType = (orderType: UiOrderType): TradeType => { + switch (orderType) { + case UiOrderType.SWAP: + return TradeType.SWAP + case UiOrderType.LIMIT: + return TradeType.LIMIT + case UiOrderType.TWAP: + return TradeType.ADVANCED + } +} + +export const isSettingTwapFallbackHandler = (decodedData: DecodedDataResponse | undefined) => { + return ( + decodedData?.parameters?.some( + (item) => + Array.isArray(item?.valueDecoded) && + item.valueDecoded.some( + (decoded) => + decoded.dataDecoded?.method === 'setFallbackHandler' && + decoded.dataDecoded.parameters?.some( + (parameter) => parameter.name === 'handler' && parameter.value === TWAP_FALLBACK_HANDLER, + ), + ), + ) || false + ) +} diff --git a/src/features/swap/hooks/useIsExpiredSwap.ts b/src/features/swap/hooks/useIsExpiredSwap.ts index 045760da27..bb563d99a4 100644 --- a/src/features/swap/hooks/useIsExpiredSwap.ts +++ b/src/features/swap/hooks/useIsExpiredSwap.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import type { TransactionInfo } from '@safe-global/safe-gateway-typescript-sdk' -import { isSwapTxInfo } from '@/utils/transaction-guards' +import { isSwapOrderTxInfo } from '@/utils/transaction-guards' import useIntervalCounter from '@/hooks/useIntervalCounter' const INTERVAL_IN_MS = 10_000 @@ -15,7 +15,7 @@ const useIsExpiredSwap = (txInfo: TransactionInfo) => { const [counter] = useIntervalCounter(INTERVAL_IN_MS) useEffect(() => { - if (!isSwapTxInfo(txInfo)) return + if (!isSwapOrderTxInfo(txInfo)) return setIsExpired(Date.now() > txInfo.validUntil * 1000) }, [counter, txInfo]) diff --git a/src/features/swap/hooks/useIsSwapFeatureEnabled.ts b/src/features/swap/hooks/useIsSwapFeatureEnabled.ts new file mode 100644 index 0000000000..d43d02a3fd --- /dev/null +++ b/src/features/swap/hooks/useIsSwapFeatureEnabled.ts @@ -0,0 +1,11 @@ +import { GeoblockingContext } from '@/components/common/GeoblockingProvider' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' +import { useContext } from 'react' + +const useIsSwapFeatureEnabled = () => { + const isBlockedCountry = useContext(GeoblockingContext) + return useHasFeature(FEATURES.NATIVE_SWAPS) && !isBlockedCountry +} + +export default useIsSwapFeatureEnabled diff --git a/src/features/swap/index.tsx b/src/features/swap/index.tsx index 4bf4acc661..295da8ec66 100644 --- a/src/features/swap/index.tsx +++ b/src/features/swap/index.tsx @@ -1,11 +1,9 @@ -import { FEATURES } from '@/utils/chains' import { CowSwapWidget } from '@cowprotocol/widget-react' import { type CowSwapWidgetParams, TradeType } from '@cowprotocol/widget-lib' -import { CowEvents, type CowEventListeners } from '@cowprotocol/events' -import { useState, useEffect, type MutableRefObject, useMemo } from 'react' -import { Container, Grid, useTheme } from '@mui/material' -import { useRef } from 'react' -import { Box } from '@mui/material' +import type { OnTradeParamsPayload } from '@cowprotocol/events' +import { type CowEventListeners, CowEvents } from '@cowprotocol/events' +import { type MutableRefObject, useEffect, useMemo, useRef, useState } from 'react' +import { Box, Container, Grid, useTheme } from '@mui/material' import { SafeAppAccessPolicyTypes, type SafeAppData, @@ -22,14 +20,34 @@ import useWallet from '@/hooks/wallets/useWallet' import BlockedAddress from '@/components/common/BlockedAddress' import useSwapConsent from './useSwapConsent' import Disclaimer from '@/components/common/Disclaimer' -import LegalDisclaimerContent from '@/components/common/LegalDisclaimerContent' +import LegalDisclaimerContent from '@/features/swap/components/LegalDisclaimer' import { isBlockedAddress } from '@/services/ofac' import { selectSwapParams, setSwapParams, type SwapState } from './store/swapParamsSlice' import { setSwapOrder } from '@/store/swapOrderSlice' import useChainId from '@/hooks/useChainId' +import { type BaseTransaction } from '@safe-global/safe-apps-sdk' +import { APPROVAL_SIGNATURE_HASH } from '@/components/tx/ApprovalEditor/utils/approvals' +import { id } from 'ethers' +import useIsSwapFeatureEnabled from './hooks/useIsSwapFeatureEnabled' +import { + LIMIT_ORDER_TITLE, + SWAP_TITLE, + SWAP_ORDER_TITLE, + TWAP_ORDER_TITLE, + SWAP_FEE_RECIPIENT, +} from '@/features/swap/constants' +import { calculateFeePercentageInBps } from '@/features/swap/helpers/fee' +import { UiOrderTypeToOrderType } from '@/features/swap/helpers/utils' +import { FEATURES } from '@/utils/chains' const BASE_URL = typeof window !== 'undefined' && window.location.origin ? window.location.origin : '' +const PRE_SIGN_SIGHASH = id('setPreSignature(bytes,bool)').slice(0, 10) +const WRAP_SIGHASH = id('deposit()').slice(0, 10) +const UNWRAP_SIGHASH = id('withdraw(uint256)').slice(0, 10) +const CREATE_WITH_CONTEXT_SIGHASH = id('createWithContext((address,bytes32,bytes),address,bytes,bool)').slice(0, 10) +const CANCEL_ORDER_SIGHASH = id('invalidateOrder(bytes)').slice(0, 10) + type Params = { sell?: { asset: string @@ -37,10 +55,22 @@ type Params = { } } -export const SWAP_TITLE = 'Safe Swap' +export const getSwapTitle = (tradeType: SwapState['tradeType'], txs: BaseTransaction[] | undefined) => { + const hashToLabel = { + [PRE_SIGN_SIGHASH]: tradeType === 'limit' ? LIMIT_ORDER_TITLE : SWAP_ORDER_TITLE, + [APPROVAL_SIGNATURE_HASH]: 'Approve', + [WRAP_SIGHASH]: 'Wrap', + [UNWRAP_SIGHASH]: 'Unwrap', + [CREATE_WITH_CONTEXT_SIGHASH]: TWAP_ORDER_TITLE, + [CANCEL_ORDER_SIGHASH]: 'Cancel Order', + } + + const swapTitle = txs + ?.map((tx) => hashToLabel[tx.data.slice(0, 10)]) + .filter(Boolean) + .join(' and ') -export const getSwapTitle = (tradeType: SwapState['tradeType']) => { - return tradeType === 'limit' ? 'Limit order' : 'Swap order' + return swapTitle } const SwapWidget = ({ sell }: Params) => { @@ -48,13 +78,68 @@ const SwapWidget = ({ sell }: Params) => { const darkMode = useDarkMode() const chainId = useChainId() const dispatch = useAppDispatch() - const isSwapFeatureEnabled = useHasFeature(FEATURES.NATIVE_SWAPS) + const isSwapFeatureEnabled = useIsSwapFeatureEnabled() const swapParams = useAppSelector(selectSwapParams) - const { tradeType } = swapParams const { safeAddress, safeLoading } = useSafeInfo() const [blockedAddress, setBlockedAddress] = useState('') const wallet = useWallet() const { isConsentAccepted, onAccept } = useSwapConsent() + const feeEnabled = useHasFeature(FEATURES.NATIVE_SWAPS_FEE_ENABLED) + const useStagingCowServer = useHasFeature(FEATURES.NATIVE_SWAPS_USE_COW_STAGING_SERVER) + + const [params, setParams] = useState<CowSwapWidgetParams>({ + appCode: 'Safe Wallet Swaps', // Name of your app (max 50 characters) + width: '100%', // Width in pixels (or 100% to use all available space) + height: '860px', + chainId, + baseUrl: useStagingCowServer ? 'https://staging.swap.cow.fi' : 'https://swap.cow.fi', + standaloneMode: false, + disableToastMessages: true, + disablePostedOrderConfirmationModal: true, + hideLogo: true, + hideNetworkSelector: true, + sounds: { + orderError: null, + orderExecuted: null, + postOrder: null, + }, + tradeType: swapParams.tradeType, + sell: sell || { + asset: '', + amount: '0', + }, + buy: { + asset: '', + amount: '0', + }, + images: { + emptyOrders: darkMode + ? BASE_URL + '/images/common/swap-empty-dark.svg' + : BASE_URL + '/images/common/swap-empty-light.svg', + }, + enabledTradeTypes: [TradeType.SWAP, TradeType.LIMIT, TradeType.ADVANCED], + theme: { + baseTheme: darkMode ? 'dark' : 'light', + primary: palette.primary.main, + background: palette.background.main, + paper: palette.background.paper, + text: palette.text.primary, + danger: palette.error.dark, + info: palette.info.main, + success: palette.success.main, + warning: palette.warning.main, + alert: palette.warning.main, + }, + partnerFee: { + bps: feeEnabled ? 35 : 0, + recipient: SWAP_FEE_RECIPIENT, + }, + content: { + feeLabel: 'Widget Fee', + feeTooltipMarkdown: + 'The [tiered widget fee](https://help.safe.global/en/articles/178530-how-does-the-widget-fee-work-for-native-swaps) incurred here is charged by CoW Protocol for the operation of this widget. The fee is automatically calculated into this quote. Part of the fee will contribute to a license fee that supports the Safe Community. Neither the Safe Ecosystem Foundation nor Safe{Wallet} operate the CoW Swap Widget and/or CoW Swap', + }, + }) useEffect(() => { if (isBlockedAddress(safeAddress)) { @@ -135,48 +220,40 @@ const SwapWidget = ({ sell }: Params) => { }, { event: CowEvents.ON_CHANGE_TRADE_PARAMS, - handler: (newTradeParams) => { - const { orderType: tradeType, recipient } = newTradeParams - dispatch(setSwapParams({ tradeType })) + handler: (newTradeParams: OnTradeParamsPayload) => { + const { orderType: tradeType, recipient, sellToken, buyToken } = newTradeParams + + const newFeeBps = feeEnabled ? calculateFeePercentageInBps(newTradeParams) : 0 + + setParams((params) => ({ + ...params, + tradeType: UiOrderTypeToOrderType(tradeType), + partnerFee: { + recipient: SWAP_FEE_RECIPIENT, + bps: newFeeBps, + }, + sell: { + asset: sellToken?.symbol, + }, + buy: { + asset: buyToken?.symbol, + }, + })) if (recipient && isBlockedAddress(recipient)) { setBlockedAddress(recipient) } + + dispatch(setSwapParams({ tradeType })) }, }, ] - }, [dispatch]) + }, [dispatch, feeEnabled]) - const [params, setParams] = useState<CowSwapWidgetParams | null>(null) useEffect(() => { - setParams({ - appCode: 'Safe Wallet Swaps', // Name of your app (max 50 characters) - width: '100%', // Width in pixels (or 100% to use all available space) - height: '860px', + setParams((params) => ({ + ...params, chainId, - standaloneMode: false, - disableToastMessages: true, - disablePostedOrderConfirmationModal: true, - hideLogo: true, - hideNetworkSelector: true, - sounds: { - orderError: null, - orderExecuted: null, - postOrder: null, - }, - tradeType, // TradeType.SWAP or TradeType.LIMIT - sell: sell - ? sell - : { - asset: '', - amount: '0', - }, - images: { - emptyOrders: darkMode - ? BASE_URL + '/images/common/swap-empty-dark.svg' - : BASE_URL + '/images/common/swap-empty-light.svg', - }, - enabledTradeTypes: [TradeType.SWAP, TradeType.LIMIT], theme: { baseTheme: darkMode ? 'dark' : 'light', primary: palette.primary.main, @@ -189,13 +266,8 @@ const SwapWidget = ({ sell }: Params) => { warning: palette.warning.main, alert: palette.warning.main, }, - content: { - feeLabel: 'No fee for one month', - feeTooltipMarkdown: - 'Any future transaction fee incurred by Cow Protocol here will contribute to a license fee that supports the Safe Community. Neither Safe Ecosystem Foundation nor Core Contributors GmbH operate the CoW Swap Widget and/or Cow Swap.', - }, - }) - }, [sell, palette, darkMode, tradeType, chainId]) + })) + }, [palette, darkMode, chainId]) const chain = useCurrentChain() @@ -210,23 +282,12 @@ const SwapWidget = ({ sell }: Params) => { useCustomAppCommunicator(iframeRef, appData, chain) - if (!params) { - return null - } - if (blockedAddress) { return <BlockedAddress address={blockedAddress} /> } if (!isConsentAccepted) { - return ( - <Disclaimer - title="Legal Disclaimer" - content={<LegalDisclaimerContent withTitle={false} isSafeApps={false} />} - onAccept={onAccept} - buttonText="Continue" - /> - ) + return <Disclaimer title="Note" content={<LegalDisclaimerContent />} onAccept={onAccept} buttonText="Continue" /> } if (!isSwapFeatureEnabled) { diff --git a/src/features/swap/store/swapParamsSlice.ts b/src/features/swap/store/swapParamsSlice.ts index e7a620b5a0..137a8f6abe 100644 --- a/src/features/swap/store/swapParamsSlice.ts +++ b/src/features/swap/store/swapParamsSlice.ts @@ -1,12 +1,8 @@ import type { RootState } from '@/store' import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' - -// Using TradeType from the cow widget library results in lint errors -enum TradeType { - SWAP = 'swap', - LIMIT = 'limit', -} +import { UiOrderTypeToOrderType } from '@/features/swap/helpers/utils' +import { TradeType, type UiOrderType } from '@/features/swap/types' export type SwapState = { tradeType: TradeType @@ -20,9 +16,14 @@ export const swapParamsSlice = createSlice({ name: 'swapParams', initialState, reducers: { - setSwapParams: (_, action: PayloadAction<SwapState>) => { + setSwapParams: ( + _, + action: PayloadAction<{ + tradeType: UiOrderType + }>, + ) => { return { - tradeType: action.payload.tradeType.toLowerCase() as TradeType, + tradeType: UiOrderTypeToOrderType(action.payload.tradeType), } }, }, diff --git a/src/features/swap/types.ts b/src/features/swap/types.ts new file mode 100644 index 0000000000..8df4dfc4e6 --- /dev/null +++ b/src/features/swap/types.ts @@ -0,0 +1,16 @@ +// Jest tests fail if TradeType is imported from widget-lib, so we duplicate them here +export enum TradeType { + SWAP = 'swap', + LIMIT = 'limit', + /** + * Currently it means only TWAP orders. + * But in the future it can be extended to support other order types. + */ + ADVANCED = 'advanced', +} + +export enum UiOrderType { + SWAP = 'SWAP', + LIMIT = 'LIMIT', + TWAP = 'TWAP', +} diff --git a/src/features/swap/useSwapConsent.ts b/src/features/swap/useSwapConsent.ts index 38e85d0fe2..050f1c2041 100644 --- a/src/features/swap/useSwapConsent.ts +++ b/src/features/swap/useSwapConsent.ts @@ -1,7 +1,7 @@ import { useCallback } from 'react' import useLocalStorage from '@/services/local-storage/useLocalStorage' -const SWAPS_CONSENT_STORAGE_KEY = 'swapDisclaimerAccepted' +const SWAPS_CONSENT_STORAGE_KEY = 'swapDisclaimerAcceptedV1' const useSwapConsent = (): { isConsentAccepted: boolean diff --git a/src/features/walletconnect/__tests__/WalletConnectContext.test.tsx b/src/features/walletconnect/__tests__/WalletConnectContext.test.tsx index b014cb8436..b9a504ab8a 100644 --- a/src/features/walletconnect/__tests__/WalletConnectContext.test.tsx +++ b/src/features/walletconnect/__tests__/WalletConnectContext.test.tsx @@ -410,10 +410,9 @@ describe('WalletConnectProvider', () => { 1, { method: 'fake', params: [] }, { - id: 25, name: 'name', description: 'description', - url: 'https://safe-apps.dev.5afe.dev/wallet-connect', + url: 'https://apps-portal.safe.global/wallet-connect', iconUrl: 'iconUrl', }, ) diff --git a/src/features/walletconnect/components/WalletConnectProvider/index.tsx b/src/features/walletconnect/components/WalletConnectProvider/index.tsx index e638c9a233..fd9b0442fb 100644 --- a/src/features/walletconnect/components/WalletConnectProvider/index.tsx +++ b/src/features/walletconnect/components/WalletConnectProvider/index.tsx @@ -5,7 +5,7 @@ import { formatJsonRpcError } from '@walletconnect/jsonrpc-utils' import useSafeInfo from '@/hooks/useSafeInfo' import useSafeWalletProvider from '@/services/safe-wallet-provider/useSafeWalletProvider' import { asError } from '@/services/exceptions/utils' -import { IS_PRODUCTION, WC_APP_DEV, WC_APP_PROD } from '@/config/constants' +import { IS_PRODUCTION } from '@/config/constants' import { getPeerName, stripEip155Prefix } from '@/features/walletconnect/services/utils' import { trackRequest } from '@/features/walletconnect//services/tracking' import { wcPopupStore } from '@/features/walletconnect/components' @@ -23,8 +23,6 @@ export enum WCLoadingState { DISCONNECT = 'Disconnect', } -const WalletConnectSafeApp = IS_PRODUCTION ? WC_APP_PROD : WC_APP_DEV - const walletConnectSingleton = new WalletConnectWallet() const getWrongChainError = (dappName: string): Error => { @@ -90,9 +88,8 @@ export const WalletConnectProvider = ({ children }: { children: ReactNode }) => // Get response from Safe Wallet Provider return safeWalletProvider.request(event.id, event.params.request, { - id: WalletConnectSafeApp.id, - url: WalletConnectSafeApp.url, - name: getPeerName(session.peer) || 'Unknown dApp', + url: session.peer.metadata.url, + name: getPeerName(session.peer) || 'WalletConnect', description: session.peer.metadata.description, iconUrl: session.peer.metadata.icons[0], }) diff --git a/src/features/walletconnect/components/WalletConnectUi/index.tsx b/src/features/walletconnect/components/WalletConnectUi/index.tsx index c78c211514..dbd5bdb678 100644 --- a/src/features/walletconnect/components/WalletConnectUi/index.tsx +++ b/src/features/walletconnect/components/WalletConnectUi/index.tsx @@ -5,7 +5,7 @@ import useWalletConnectSessions from '@/features/walletconnect/hooks/useWalletCo import { WalletConnectContext } from '@/features/walletconnect/WalletConnectContext' import useWcUri from '../../hooks/useWcUri' import WcHeaderWidget from '../WcHeaderWidget' -import WcSessionManager from '../WcSessionMananger' +import WcSessionManager from '../WcSessionManager' import { WalletConnectProvider } from '../WalletConnectProvider' const WalletConnectWidget = () => { diff --git a/src/features/walletconnect/components/WcInput/index.tsx b/src/features/walletconnect/components/WcInput/index.tsx index 2b6018dfb4..ff40714c80 100644 --- a/src/features/walletconnect/components/WcInput/index.tsx +++ b/src/features/walletconnect/components/WcInput/index.tsx @@ -10,6 +10,8 @@ import { getClipboard, isClipboardSupported } from '@/utils/clipboard' import { Button, CircularProgress, InputAdornment, TextField } from '@mui/material' import { useCallback, useContext, useEffect, useState } from 'react' +const PROPOSAL_TIMEOUT = 30_000 + const useTrackErrors = (error?: Error) => { const debouncedErrorMessage = useDebounce(error?.message, 1000) @@ -22,10 +24,10 @@ const useTrackErrors = (error?: Error) => { } const WcInput = ({ uri }: { uri: string }) => { - const { walletConnect, isLoading, setIsLoading } = useContext(WalletConnectContext) + const { walletConnect, isLoading, setIsLoading, setError } = useContext(WalletConnectContext) const [value, setValue] = useState('') - const [error, setError] = useState<Error>() - useTrackErrors(error) + const [inputError, setInputError] = useState<Error>() + useTrackErrors(inputError) const onInput = useCallback( async (val: string) => { @@ -34,11 +36,11 @@ const WcInput = ({ uri }: { uri: string }) => { setValue(val) if (val && !isPairingUri(val)) { - setError(new Error('Invalid pairing code')) + setInputError(new Error('Invalid pairing code')) return } - setError(undefined) + setInputError(undefined) if (!val) return @@ -47,12 +49,17 @@ const WcInput = ({ uri }: { uri: string }) => { try { await walletConnect.connect(val) } catch (e) { - setError(asError(e)) + setInputError(asError(e)) + setIsLoading(undefined) } - - setIsLoading(undefined) + setTimeout(() => { + if (isLoading && isLoading !== WCLoadingState.APPROVE) { + setIsLoading(undefined) + setError(new Error('Connection timed out')) + } + }, PROPOSAL_TIMEOUT) }, - [setIsLoading, walletConnect], + [isLoading, setError, setIsLoading, walletConnect], ) // Insert a pre-filled uri @@ -77,8 +84,8 @@ const WcInput = ({ uri }: { uri: string }) => { autoComplete="off" autoFocus disabled={!!isLoading} - error={!!error} - label={error ? error.message : 'Pairing code'} + error={!!inputError} + label={inputError ? inputError.message : 'Pairing code'} placeholder="wc:" spellCheck={false} InputProps={{ diff --git a/src/features/walletconnect/components/WcSessionMananger/index.tsx b/src/features/walletconnect/components/WcSessionManager/index.tsx similarity index 90% rename from src/features/walletconnect/components/WcSessionMananger/index.tsx rename to src/features/walletconnect/components/WcSessionManager/index.tsx index b5d140c5b1..689c95b11d 100644 --- a/src/features/walletconnect/components/WcSessionMananger/index.tsx +++ b/src/features/walletconnect/components/WcSessionManager/index.tsx @@ -43,7 +43,17 @@ const WcSessionManager = ({ sessions, uri }: WcSessionManagerProps) => { setIsLoading(WCLoadingState.APPROVE) try { - await walletConnect.approveSession(sessionProposal, chainId, safeAddress) + await walletConnect.approveSession(sessionProposal, chainId, safeAddress, { + capabilities: JSON.stringify({ + [safeAddress]: { + [`0x${Number(chainId).toString(16)}`]: { + atomicBatch: { + supported: true, + }, + }, + }, + }), + }) // Auto approve future sessions for non-malicious dApps if ( @@ -87,8 +97,9 @@ const WcSessionManager = ({ sessions, uri }: WcSessionManagerProps) => { } setProposal(proposalData) + setIsLoading(undefined) }) - }, [walletConnect, setError, autoApprove, onApprove, chainId]) + }, [autoApprove, chainId, onApprove, setError, setIsLoading, walletConnect]) // Track errors useEffect(() => { diff --git a/src/features/walletconnect/constants.ts b/src/features/walletconnect/constants.ts index 8ad8c36c22..d2c698c419 100644 --- a/src/features/walletconnect/constants.ts +++ b/src/features/walletconnect/constants.ts @@ -21,9 +21,10 @@ export const SAFE_COMPATIBLE_METHODS = [ 'eth_getLogs', 'eth_gasPrice', 'wallet_switchEthereumChain', - 'wallet_sendFunctionCallBundle', - 'wallet_getBundleStatus', - 'wallet_showBundleStatus', + 'wallet_sendCalls', + 'wallet_getCallsStatus', + 'wallet_showCallsStatus', + 'wallet_getCapabilities', 'safe_setSettings', ] @@ -46,7 +47,6 @@ export const BlockedBridges = [ 'zksync-era.l2scan.co', 'www.portalbridge.com', 'wallet.polygon.technology', - 'app.rhino.fi', // Unsupported chain bridges 'bridge.zora.energy', @@ -67,6 +67,7 @@ export const WarnedBridges = [ 'core.app', 'across.to', // doesn't send their URL in the session proposal 'app.allbridge.io', + 'app.rhino.fi', 'bridge.arbitrum.io', 'bridge.base.org', 'bridge.linea.build', diff --git a/src/features/walletconnect/services/WalletConnectWallet.ts b/src/features/walletconnect/services/WalletConnectWallet.ts index d497e78642..51b5747c6c 100644 --- a/src/features/walletconnect/services/WalletConnectWallet.ts +++ b/src/features/walletconnect/services/WalletConnectWallet.ts @@ -3,7 +3,7 @@ import { Web3Wallet } from '@walletconnect/web3wallet' import { buildApprovedNamespaces, getSdkError } from '@walletconnect/utils' import type Web3WalletType from '@walletconnect/web3wallet' import type { Web3WalletTypes } from '@walletconnect/web3wallet' -import type { SessionTypes } from '@walletconnect/types' +import type { ProposalTypes, SessionTypes } from '@walletconnect/types' import { type JsonRpcResponse } from '@walletconnect/jsonrpc-utils' import uniq from 'lodash/uniq' @@ -82,15 +82,10 @@ class WalletConnectWallet { } private getNamespaces(proposal: Web3WalletTypes.SessionProposal, currentChainId: string, safeAddress: string) { - // Most dApps require mainnet, but we aren't always on mainnet - // As workaround, we pretend include all required and optional chains with the Safe chainId + // As workaround, we pretend to support all the required chains plus the current Safe's chain const requiredChains = proposal.params.requiredNamespaces[EIP155]?.chains || [] - const optionalChains = proposal.params.optionalNamespaces[EIP155]?.chains || [] - const supportedChainIds = [currentChainId].concat( - requiredChains.map(stripEip155Prefix), - optionalChains.map(stripEip155Prefix), - ) + const supportedChainIds = [currentChainId].concat(requiredChains.map(stripEip155Prefix)) const eip155ChainIds = supportedChainIds.map(getEip155ChainId) const eip155Accounts = eip155ChainIds.map((eip155ChainId) => `${eip155ChainId}:${safeAddress}`) @@ -112,7 +107,12 @@ class WalletConnectWallet { }) } - public async approveSession(proposal: Web3WalletTypes.SessionProposal, currentChainId: string, safeAddress: string) { + public async approveSession( + proposal: Web3WalletTypes.SessionProposal, + currentChainId: string, + safeAddress: string, + sessionProperties?: ProposalTypes.SessionProperties, + ) { assertWeb3Wallet(this.web3Wallet) const namespaces = this.getNamespaces(proposal, currentChainId, safeAddress) @@ -121,6 +121,7 @@ class WalletConnectWallet { const session = await this.web3Wallet.approveSession({ id: proposal.id, namespaces, + sessionProperties, }) await this.chainChanged(session.topic, currentChainId) diff --git a/src/features/walletconnect/services/__tests__/WalletConnectWallet.test.ts b/src/features/walletconnect/services/__tests__/WalletConnectWallet.test.ts index ce86f0dbbb..d9a33cdc14 100644 --- a/src/features/walletconnect/services/__tests__/WalletConnectWallet.test.ts +++ b/src/features/walletconnect/services/__tests__/WalletConnectWallet.test.ts @@ -155,21 +155,13 @@ describe('WalletConnectWallet', () => { await wallet.approveSession( proposal, - '69420', // Not in proposal, therefore not supported + '43114', // Not in proposal, therefore not supported toBeHex('0x123', 20), ) const namespaces = { eip155: { - chains: [ - 'eip155:1', - 'eip155:43114', - 'eip155:42161', - 'eip155:8453', - 'eip155:100', - 'eip155:137', - 'eip155:1101', - ], + chains: ['eip155:1', 'eip155:43114'], methods: [ 'eth_sendTransaction', 'personal_sign', @@ -180,15 +172,7 @@ describe('WalletConnectWallet', () => { 'wallet_switchEthereumChain', ], events: ['chainChanged', 'accountsChanged'], - accounts: [ - `eip155:1:${toBeHex('0x123', 20)}`, - `eip155:43114:${toBeHex('0x123', 20)}`, - `eip155:42161:${toBeHex('0x123', 20)}`, - `eip155:8453:${toBeHex('0x123', 20)}`, - `eip155:100:${toBeHex('0x123', 20)}`, - `eip155:137:${toBeHex('0x123', 20)}`, - `eip155:1101:${toBeHex('0x123', 20)}`, - ], + accounts: [`eip155:1:${toBeHex('0x123', 20)}`, `eip155:43114:${toBeHex('0x123', 20)}`], }, } @@ -226,23 +210,16 @@ describe('WalletConnectWallet', () => { await wallet.approveSession( proposal, - '69420', // Not in proposal, therefore not supported + '43114', // Not in proposal, therefore not supported toBeHex('0x123', 20), ) const namespaces = { eip155: { - chains: ['eip155:43114', 'eip155:42161', 'eip155:8453', 'eip155:100', 'eip155:137', 'eip155:1101'], + chains: ['eip155:43114'], methods: ['eth_accounts', 'personal_sign', 'eth_sendTransaction'], events: ['chainChanged', 'accountsChanged'], - accounts: [ - `eip155:43114:${toBeHex('0x123', 20)}`, - `eip155:42161:${toBeHex('0x123', 20)}`, - `eip155:8453:${toBeHex('0x123', 20)}`, - `eip155:100:${toBeHex('0x123', 20)}`, - `eip155:137:${toBeHex('0x123', 20)}`, - `eip155:1101:${toBeHex('0x123', 20)}`, - ], + accounts: [`eip155:43114:${toBeHex('0x123', 20)}`], }, } @@ -272,6 +249,7 @@ describe('WalletConnectWallet', () => { publicKey: '123', metadata: {} as SignClientTypes.Metadata, }, + pairingTopic: '0x3456', requiredNamespaces: {} as ProposalTypes.RequiredNamespaces, optionalNamespaces: {} as ProposalTypes.OptionalNamespaces, expiryTimestamp: 2, @@ -306,6 +284,7 @@ describe('WalletConnectWallet', () => { id: 1, expiry: 1, relays: [], + pairingTopic: '0x3456', proposer: { publicKey: '123', metadata: {} as SignClientTypes.Metadata, diff --git a/src/hooks/Beamer/useBeamer.ts b/src/hooks/Beamer/useBeamer.ts index 96c774fd68..52afabba1e 100644 --- a/src/hooks/Beamer/useBeamer.ts +++ b/src/hooks/Beamer/useBeamer.ts @@ -1,13 +1,13 @@ import { useEffect } from 'react' import { useAppSelector } from '@/store' -import { CookieType, selectCookies } from '@/store/cookiesSlice' +import { CookieAndTermType, selectCookies } from '@/store/cookiesAndTermsSlice' import { loadBeamer, unloadBeamer, updateBeamer } from '@/services/beamer' import { useCurrentChain } from '@/hooks/useChains' const useBeamer = () => { const cookies = useAppSelector(selectCookies) - const isBeamerEnabled = cookies[CookieType.UPDATES] + const isBeamerEnabled = cookies[CookieAndTermType.UPDATES] const chain = useCurrentChain() useEffect(() => { diff --git a/src/hooks/__tests__/usePendingTxs.test.ts b/src/hooks/__tests__/usePendingTxs.test.ts index 4ed501db8f..5765fa1c68 100644 --- a/src/hooks/__tests__/usePendingTxs.test.ts +++ b/src/hooks/__tests__/usePendingTxs.test.ts @@ -6,39 +6,131 @@ import { type Transaction, getTransactionQueue, TransactionListItemType, + DetailedExecutionInfoType, + LabelValue, + type TransactionListPage, + type ExecutionInfo, + type TransactionSummary, + ConflictType, } from '@safe-global/safe-gateway-typescript-sdk' import * as useSafeInfoHook from '@/hooks/useSafeInfo' -import { useHasPendingTxs, usePendingTxsQueue } from '../usePendingTxs' +import { filterUntrustedQueue, useHasPendingTxs, usePendingTxsQueue } from '../usePendingTxs' + +const mockQueue = <TransactionListPage>{ + next: undefined, + previous: undefined, + results: [ + { + type: TransactionListItemType.LABEL, + label: LabelValue.Next, + }, + { + type: 'TRANSACTION', + transaction: { + id: 'multisig_123', + executionInfo: { + confirmationsSubmitted: 0, + type: DetailedExecutionInfoType.MULTISIG, + }, + }, + }, + { + type: 'TRANSACTION', + transaction: { + id: 'multisig_456', + }, + }, + ], +} + +const mockQueueWithConflictHeaders = <TransactionListPage>{ + next: undefined, + previous: undefined, + results: [ + { + type: TransactionListItemType.LABEL, + label: LabelValue.Next, + }, + { + type: TransactionListItemType.CONFLICT_HEADER, + nonce: 2, + }, + { + type: 'TRANSACTION', + transaction: { + id: 'multisig_123', + executionInfo: { + confirmationsSubmitted: 0, + type: DetailedExecutionInfoType.MULTISIG, + }, + }, + }, + { + type: 'TRANSACTION', + transaction: { + id: 'multisig_456', + }, + }, + ], +} // Mock getTransactionQueue jest.mock('@safe-global/safe-gateway-typescript-sdk', () => ({ ...jest.requireActual('@safe-global/safe-gateway-typescript-sdk'), - getTransactionQueue: jest.fn(() => - Promise.resolve({ - next: null, - previous: null, - results: [ - { - type: 'LABEL', - label: 'Next', - }, - { - type: 'TRANSACTION', - transaction: { - id: 'multisig_123', - }, - }, - { - type: 'TRANSACTION', - transaction: { - id: 'multisig_456', - }, - }, - ], - }), - ), + getTransactionQueue: jest.fn(() => Promise.resolve(mockQueue)), })) +describe('filterUntrustedQueue', () => { + it('should remove transactions that are not pending', () => { + const mockPendingIds = ['multisig_123'] + + const result = filterUntrustedQueue(mockQueue, mockPendingIds) + + expect(result?.results.length).toEqual(2) + }) + + it('should rename the first label to Pending', () => { + const mockPendingIds = ['multisig_123'] + + const result = filterUntrustedQueue(mockQueue, mockPendingIds) + + expect(result?.results[0]).toEqual({ type: 'LABEL', label: 'Pending' }) + }) + + it('should remove all conflict headers', () => { + const mockPendingIds = ['multisig_123'] + + const result = filterUntrustedQueue(mockQueueWithConflictHeaders, mockPendingIds) + + expect(result?.results[0]).toEqual({ type: 'LABEL', label: 'Pending' }) + expect(result?.results.length).toEqual(2) + expect(result?.results[1].type).not.toEqual(TransactionListItemType.CONFLICT_HEADER) + }) + + it('should remove all transactions that are signed', () => { + const mockPendingIds = ['multisig_123', 'multisig_789'] + const mockQueueWithSignedTxs = { ...mockQueue } + + mockQueueWithSignedTxs.results.push({ + type: TransactionListItemType.TRANSACTION, + transaction: { + id: 'multisig_789', + executionInfo: { + confirmationsSubmitted: 1, + confirmationsRequired: 1, + type: DetailedExecutionInfoType.MULTISIG, + } as ExecutionInfo, + } as unknown as TransactionSummary, + conflictType: ConflictType.NONE, + }) + + const result = filterUntrustedQueue(mockQueueWithSignedTxs, mockPendingIds) + + expect(result?.results.length).toEqual(2) + expect(result?.results[2]).not.toEqual(mockQueueWithSignedTxs.results[2]) + }) +}) + describe('usePendingTxsQueue', () => { beforeEach(() => { jest.clearAllMocks() @@ -59,7 +151,7 @@ describe('usePendingTxsQueue', () => { })) }) - it('should return the pending txs queue', async () => { + it('should return the pending txs queue for unsigned transactions', async () => { const { result } = renderHook(() => usePendingTxsQueue(), { initialReduxState: { pendingTxs: { @@ -106,35 +198,29 @@ describe('usePendingTxsQueue', () => { expect(result?.current.page).toBeUndefined() }) - it('should remove conflicting header if only one of the conflicting txs is pending', async () => { - ;(getTransactionQueue as jest.Mock).mockImplementation(() => - Promise.resolve({ - next: null, - previous: null, - results: [ - { - type: 'LABEL', - label: 'Next', - }, - { - type: TransactionListItemType.CONFLICT_HEADER, - nonce: 2, - }, - { - type: 'TRANSACTION', - transaction: { - id: 'multisig_123', - }, - }, - { - type: 'TRANSACTION', - transaction: { - id: 'multisig_456', - }, - }, - ], - }), - ) + it('should return undefined if none of the pending txs are unsigned', async () => { + const { result } = renderHook(() => usePendingTxsQueue(), { + initialReduxState: { + pendingTxs: { + multisig_456: { + chainId: '5', + safeAddress: '0x0000000000000000000000000000000000000001', + txHash: 'tx567', + } as PendingTx, + }, + }, + }) + + expect(result?.current.loading).toBe(true) + + await act(() => Promise.resolve(true)) + + expect(result?.current.loading).toBe(false) + expect(result?.current.page).toBeUndefined() + }) + + it('should remove all conflict headers', async () => { + ;(getTransactionQueue as jest.Mock).mockImplementation(() => Promise.resolve(mockQueueWithConflictHeaders)) const { result } = renderHook(() => usePendingTxsQueue(), { initialReduxState: { diff --git a/src/hooks/__tests__/useRemainingRelays.test.ts b/src/hooks/__tests__/useRemainingRelays.test.ts index c77e534cbb..e167618f93 100644 --- a/src/hooks/__tests__/useRemainingRelays.test.ts +++ b/src/hooks/__tests__/useRemainingRelays.test.ts @@ -52,6 +52,46 @@ describe('fetch remaining relays hooks', () => { expect(mockFetch).toHaveBeenCalledTimes(2) }) + + it('should not do a network request if only swaps are supported but the tx is not a swap', () => { + jest.spyOn(useChains, 'useCurrentChain').mockReturnValue( + chainBuilder() + // @ts-expect-error - using local FEATURES enum + .with({ features: [FEATURES.RELAY_NATIVE_SWAPS] }) + .build(), + ) + + const mockFetch = jest.spyOn(gateway, 'getRelayCount') + + renderHook(() => useRelaysBySafe(JSON.stringify({ url: 'https://some.url', name: 'Some app' }))) + expect(mockFetch).toHaveBeenCalledTimes(0) + }) + + it('should do a network request if only swaps are supported and the tx is a swap', async () => { + jest.spyOn(useChains, 'useCurrentChain').mockReturnValue( + chainBuilder() + // @ts-expect-error - using local FEATURES enum + .with({ features: [FEATURES.RELAY_NATIVE_SWAPS] }) + .build(), + ) + + const mockFetch = jest.spyOn(gateway, 'getRelayCount').mockResolvedValue({ + limit: 5, + remaining: 3, + }) + + const { result } = renderHook(() => + useRelaysBySafe(JSON.stringify({ url: 'https://some.url', name: 'Safe Swap' })), + ) + + await waitFor(() => { + expect(mockFetch).toHaveBeenCalledTimes(1) + expect(result.current[0]).toEqual({ + limit: 5, + remaining: 3, + }) + }) + }) }) describe('useLeastRemainingRelays hook', () => { diff --git a/src/hooks/coreSDK/safeCoreSDK.ts b/src/hooks/coreSDK/safeCoreSDK.ts index f5fb90c761..eeaaaf5638 100644 --- a/src/hooks/coreSDK/safeCoreSDK.ts +++ b/src/hooks/coreSDK/safeCoreSDK.ts @@ -1,6 +1,7 @@ import chains from '@/config/chains' import type { UndeployedSafe } from '@/features/counterfactual/store/undeployedSafesSlice' import { getWeb3ReadOnly } from '@/hooks/wallets/web3' +import { UncheckedJsonRpcSigner } from '@/utils/providers/UncheckedJsonRpcSigner' import { getSafeSingletonDeployment, getSafeL2SingletonDeployment } from '@safe-global/safe-deployments' import ExternalStore from '@/services/ExternalStore' import { Gnosis_safe__factory } from '@/types/contracts' @@ -30,7 +31,7 @@ export function assertValidSafeVersion<T extends SafeInfo['version']>(safeVersio } export const createEthersAdapter = async (provider: BrowserProvider) => { - const signer = await provider.getSigner(0) + const signer = new UncheckedJsonRpcSigner(provider, (await provider.getSigner()).address) return new EthersAdapter({ ethers, signerOrProvider: signer, diff --git a/src/hooks/coreSDK/useInitSafeCoreSDK.ts b/src/hooks/coreSDK/useInitSafeCoreSDK.ts index 012502131e..0ca0a8fdd2 100644 --- a/src/hooks/coreSDK/useInitSafeCoreSDK.ts +++ b/src/hooks/coreSDK/useInitSafeCoreSDK.ts @@ -43,7 +43,7 @@ export const useInitSafeCoreSDK = () => { const e = asError(_e) dispatch( showNotification({ - message: 'Please try connecting your wallet again.', + message: 'Error connecting to the blockchain. Please try reloading the page.', groupKey: 'core-sdk-init-error', variant: 'error', detailedMessage: e.message, diff --git a/src/hooks/messages/useSafeMessage.ts b/src/hooks/messages/useSafeMessage.ts index fe5ec4eeb0..e1ae7a4b0c 100644 --- a/src/hooks/messages/useSafeMessage.ts +++ b/src/hooks/messages/useSafeMessage.ts @@ -6,7 +6,7 @@ import useAsync from '../useAsync' import useSafeInfo from '../useSafeInfo' import { fetchSafeMessage } from './useSyncSafeMessageSigner' -const useSafeMessage = (safeMessageHash: string) => { +const useSafeMessage = (safeMessageHash: string | undefined) => { const [safeMessage, setSafeMessage] = useState<SafeMessage | undefined>() const { safe } = useSafeInfo() @@ -17,7 +17,8 @@ const useSafeMessage = (safeMessageHash: string) => { ?.filter(isSafeMessageListItem) .find((msg) => msg.messageHash === safeMessageHash) - const [updatedMessage] = useAsync(async () => { + const [updatedMessage, messageError] = useAsync(async () => { + if (!safeMessageHash) return return fetchSafeMessage(safeMessageHash, safe.chainId) // eslint-disable-next-line react-hooks/exhaustive-deps }, [safeMessageHash, safe.chainId, safe.messagesTag]) @@ -26,7 +27,7 @@ const useSafeMessage = (safeMessageHash: string) => { setSafeMessage(updatedMessage ?? ongoingMessage) }, [ongoingMessage, updatedMessage]) - return [safeMessage, setSafeMessage] as const + return [safeMessage, setSafeMessage, messageError] as const } export default useSafeMessage diff --git a/src/hooks/messages/useSyncSafeMessageSigner.ts b/src/hooks/messages/useSyncSafeMessageSigner.ts index baab16603d..a22a2014b9 100644 --- a/src/hooks/messages/useSyncSafeMessageSigner.ts +++ b/src/hooks/messages/useSyncSafeMessageSigner.ts @@ -1,7 +1,9 @@ +import useWallet from '@/hooks/wallets/useWallet' import { Errors, logError } from '@/services/exceptions' import { asError } from '@/services/exceptions/utils' import { dispatchPreparedSignature } from '@/services/safe-messages/safeMsgNotifications' import { dispatchSafeMsgProposal, dispatchSafeMsgConfirmation } from '@/services/safe-messages/safeMsgSender' +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import { getSafeMessage, SafeMessageListItemType, @@ -38,6 +40,7 @@ const useSyncSafeMessageSigner = ( ) => { const [submitError, setSubmitError] = useState<Error | undefined>() const onboard = useOnboard() + const wallet = useWallet() const { safe } = useSafeInfo() // If the message gets updated in the messageSlice we dispatch it if the signature is complete @@ -51,16 +54,18 @@ const useSyncSafeMessageSigner = ( const onSign = useCallback(async () => { // Error is shown when no wallet is connected, this appeases TypeScript - if (!onboard) { + if (!onboard || !wallet) { return } setSubmitError(undefined) try { + await assertWalletChain(onboard, safe.chainId) + // When collecting the first signature if (!message) { - await dispatchSafeMsgProposal({ onboard, safe, message: decodedMessage, safeAppId }) + await dispatchSafeMsgProposal({ provider: wallet.provider, safe, message: decodedMessage, safeAppId }) // Fetch updated message const updatedMsg = await fetchSafeMessage(safeMessageHash, safe.chainId) @@ -71,7 +76,7 @@ const useSyncSafeMessageSigner = ( } return updatedMsg } else { - await dispatchSafeMsgConfirmation({ onboard, safe, message: decodedMessage }) + await dispatchSafeMsgConfirmation({ provider: wallet.provider, safe, message: decodedMessage }) // No requestID => we are in the confirm message dialog and do not need to leave the window open if (!requestId) { @@ -86,7 +91,7 @@ const useSyncSafeMessageSigner = ( } catch (e) { setSubmitError(asError(e)) } - }, [onboard, requestId, message, safe, decodedMessage, safeAppId, safeMessageHash, onClose]) + }, [onboard, wallet, safe, message, decodedMessage, safeAppId, safeMessageHash, onClose, requestId]) return { submitError, onSign } } diff --git a/src/hooks/useAdjustUrl.ts b/src/hooks/useAdjustUrl.ts index 033eef9015..dacb11a3fb 100644 --- a/src/hooks/useAdjustUrl.ts +++ b/src/hooks/useAdjustUrl.ts @@ -1,16 +1,39 @@ import { useEffect } from 'react' import { useRouter } from 'next/router' +import { AppRoutes } from '@/config/routes' + +const SAFE_ROUTES = [ + AppRoutes.balances.index, + AppRoutes.balances.nfts, + AppRoutes.home, + AppRoutes.settings.modules, + AppRoutes.settings.setup, + AppRoutes.swap, + AppRoutes.transactions.index, + AppRoutes.transactions.history, + AppRoutes.transactions.messages, + AppRoutes.transactions.queue, + AppRoutes.transactions.tx, +] // Replace %3A with : in the ?safe= parameter +// Redirect to index if a required safe parameter is missing const useAdjustUrl = () => { - const { asPath } = useRouter() + const router = useRouter() useEffect(() => { + const { asPath, isReady, query, pathname } = router + const newPath = asPath.replace(/([?&]safe=.+?)%3A(?=0x)/g, '$1:') if (newPath !== asPath) { history.replaceState(history.state, '', newPath) + return + } + + if (isReady && !query.safe && SAFE_ROUTES.includes(pathname)) { + router.replace({ pathname: AppRoutes.index }) } - }, [asPath]) + }, [router]) } export default useAdjustUrl diff --git a/src/hooks/useDecodeTx.ts b/src/hooks/useDecodeTx.ts index 7054a8ae24..f4fbfd0127 100644 --- a/src/hooks/useDecodeTx.ts +++ b/src/hooks/useDecodeTx.ts @@ -2,7 +2,7 @@ import { type SafeTransaction } from '@safe-global/safe-core-sdk-types' import { getConfirmationView, type BaselineConfirmationView, - type CowSwapConfirmationView, + type OrderConfirmationView, type DecodedDataResponse, } from '@safe-global/safe-gateway-typescript-sdk' import { getNativeTransferData } from '@/services/tx/tokenTransferParams' @@ -14,7 +14,7 @@ import useSafeAddress from '@/hooks/useSafeAddress' const useDecodeTx = ( tx?: SafeTransaction, -): AsyncResult<DecodedDataResponse | BaselineConfirmationView | CowSwapConfirmationView> => { +): AsyncResult<DecodedDataResponse | BaselineConfirmationView | OrderConfirmationView> => { const chainId = useChainId() const safeAddress = useSafeAddress() const encodedData = tx?.data.data @@ -22,7 +22,7 @@ const useDecodeTx = ( const isRejection = isEmptyData && tx?.data.value === '0' const [data, error, loading] = useAsync< - DecodedDataResponse | BaselineConfirmationView | CowSwapConfirmationView | undefined + DecodedDataResponse | BaselineConfirmationView | OrderConfirmationView | undefined >(() => { if (!encodedData || isEmptyData) { const nativeTransfer = isEmptyData && !isRejection ? getNativeTransferData(tx?.data) : undefined diff --git a/src/hooks/useDraftBatch.ts b/src/hooks/useDraftBatch.ts index 5dcf1f536e..a4794a24e6 100644 --- a/src/hooks/useDraftBatch.ts +++ b/src/hooks/useDraftBatch.ts @@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from '@/store' import useChainId from './useChainId' import useSafeAddress from './useSafeAddress' import type { DraftBatchItem } from '@/store/batchSlice' -import { selectBatchBySafe, addTx, removeTx, setBatch } from '@/store/batchSlice' +import { selectBatchBySafe, addTx, removeTx } from '@/store/batchSlice' import { type TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' import { BATCH_EVENTS, trackEvent } from '@/services/analytics' import { txDispatch, TxEvent } from '@/services/tx/txEvents' @@ -43,22 +43,7 @@ export const useUpdateBatch = () => { [dispatch, chainId, safeAddress], ) - const onSet = useCallback( - (items: DraftBatchItem[]) => { - dispatch( - setBatch({ - chainId, - safeAddress, - items, - }), - ) - - trackEvent({ ...BATCH_EVENTS.BATCH_REORDER }) - }, - [dispatch, chainId, safeAddress], - ) - - return [onAdd, onDelete, onSet] as const + return [onAdd, onDelete] as const } export const useDraftBatch = (): DraftBatchItem[] => { diff --git a/src/hooks/useIsSafeTokenPaused.ts b/src/hooks/useIsSafeTokenPaused.ts deleted file mode 100644 index 5f6b2b3412..0000000000 --- a/src/hooks/useIsSafeTokenPaused.ts +++ /dev/null @@ -1,34 +0,0 @@ -import useChainId from '@/hooks/useChainId' -import { getSafeTokenAddress } from '@/components/common/SafeTokenWidget' -import { useWeb3ReadOnly } from '@/hooks/wallets/web3' -import useAsync from '@/hooks/useAsync' -import { Contract, Interface } from 'ethers' - -const PAUSED_ABI = 'function paused() public view virtual returns (bool)' - -// TODO: Remove this hook after the safe token has been unpaused -const useIsSafeTokenPaused = () => { - const chainId = useChainId() - const provider = useWeb3ReadOnly() - - const [isSafeTokenPaused] = useAsync<boolean>(async () => { - const safeTokenAddress = getSafeTokenAddress(chainId) - - if (!safeTokenAddress) return false - - const safeTokenContract = new Contract(safeTokenAddress, new Interface([PAUSED_ABI]), provider) - - let isPaused: boolean - try { - isPaused = await safeTokenContract.paused() - } catch (err) { - isPaused = false - } - - return isPaused - }, [chainId, provider]) - - return isSafeTokenPaused -} - -export default useIsSafeTokenPaused diff --git a/src/hooks/useIsSidebarRoute.ts b/src/hooks/useIsSidebarRoute.ts index 1b89b2f606..280eb6fa9b 100644 --- a/src/hooks/useIsSidebarRoute.ts +++ b/src/hooks/useIsSidebarRoute.ts @@ -8,7 +8,6 @@ const NO_SIDEBAR_ROUTES = [ AppRoutes.newSafe.load, AppRoutes.index, AppRoutes.welcome.index, - AppRoutes.welcome.socialLogin, AppRoutes.welcome.accounts, AppRoutes.imprint, AppRoutes.privacy, @@ -26,7 +25,7 @@ const TOGGLE_SIDEBAR_ROUTES = [AppRoutes.apps.open] */ export function useIsSidebarRoute(pathname?: string): [boolean, boolean] { const clientPathname = usePathname() - const route = pathname || clientPathname + const route = pathname || clientPathname || '' const noSidebar = NO_SIDEBAR_ROUTES.includes(route) const toggledSidebar = TOGGLE_SIDEBAR_ROUTES.includes(route) const router = useRouter() diff --git a/src/hooks/useOnceVisible.ts b/src/hooks/useOnceVisible.ts index 3ccd72a247..e7dc3e527f 100644 --- a/src/hooks/useOnceVisible.ts +++ b/src/hooks/useOnceVisible.ts @@ -7,11 +7,13 @@ const useOnceVisible = (element: MutableRefObject<HTMLElement | null>): boolean // Create and memoize an instance of IntersectionObserver const observer = useMemo(() => { + if (typeof IntersectionObserver === 'undefined') return + return new IntersectionObserver((entries) => { const intersectingEntry = entries.find((entry) => entry.isIntersecting) if (intersectingEntry) { setOnceVisible(true) - observer.unobserve(intersectingEntry.target) + observer?.unobserve(intersectingEntry.target) } }) }, []) @@ -19,7 +21,7 @@ const useOnceVisible = (element: MutableRefObject<HTMLElement | null>): boolean // Disconnect the observer on unmount useEffect(() => { return () => { - observer.disconnect() + observer?.disconnect() } }, [observer]) @@ -28,12 +30,12 @@ const useOnceVisible = (element: MutableRefObject<HTMLElement | null>): boolean const target = element.current if (target) { - observer.observe(target) + observer?.observe(target) } return () => { if (target) { - observer.unobserve(target) + observer?.unobserve(target) } } }, [observer, element]) diff --git a/src/hooks/useOrigin.ts b/src/hooks/useOrigin.ts new file mode 100644 index 0000000000..d2710581ea --- /dev/null +++ b/src/hooks/useOrigin.ts @@ -0,0 +1,14 @@ +import { useEffect, useState } from 'react' + +const useOrigin = () => { + const [origin, setOrigin] = useState('') + + useEffect(() => { + if (typeof location !== 'undefined') { + setOrigin(location.origin) + } + }, []) + return origin +} + +export default useOrigin diff --git a/src/hooks/usePendingTxs.ts b/src/hooks/usePendingTxs.ts index f7e95e2683..db7a46c623 100644 --- a/src/hooks/usePendingTxs.ts +++ b/src/hooks/usePendingTxs.ts @@ -8,7 +8,12 @@ import { import { useAppSelector } from '@/store' import { selectPendingTxIdsBySafe } from '@/store/pendingTxsSlice' import useAsync from './useAsync' -import { isConflictHeaderListItem, isLabelListItem, isTransactionListItem } from '@/utils/transaction-guards' +import { + isConflictHeaderListItem, + isLabelListItem, + isMultisigExecutionInfo, + isTransactionListItem, +} from '@/utils/transaction-guards' import useSafeInfo from './useSafeInfo' const usePendingTxIds = (): Array<TransactionSummary['id']> => { @@ -31,6 +36,32 @@ export const useShowUnsignedQueue = (): boolean => { return safe.threshold === 1 && hasPending } +export const filterUntrustedQueue = ( + untrustedQueue: TransactionListPage, + pendingIds: Array<TransactionSummary['id']>, +) => { + // Only keep labels and pending unsigned transactions + const results = untrustedQueue.results + .filter((item) => !isTransactionListItem(item) || pendingIds.includes(item.transaction.id)) + .filter((item) => !isConflictHeaderListItem(item)) + .filter( + (item) => + !isTransactionListItem(item) || + (isTransactionListItem(item) && + isMultisigExecutionInfo(item.transaction.executionInfo) && + item.transaction.executionInfo.confirmationsSubmitted === 0), + ) + + // Adjust the first label ("Next" -> "Pending") + if (results[0] && isLabelListItem(results[0])) { + results[0].label = 'Pending' as LabelValue + } + + const transactions = results.filter((item) => isTransactionListItem(item)) + + return transactions.length ? { results } : undefined +} + export const usePendingTxsQueue = (): { page?: TransactionListPage error?: string @@ -53,29 +84,7 @@ export const usePendingTxsQueue = (): { const pendingTxPage = useMemo(() => { if (!untrustedQueue || !pendingIds.length) return - // Find the pending txs in the "untrusted" queue by id - // Keep labels too - const results = untrustedQueue.results.filter( - (item) => !isTransactionListItem(item) || pendingIds.includes(item.transaction.id), - ) - - // Adjust the first label ("Next" -> "Pending") - if (results[0] && isLabelListItem(results[0])) { - results[0].label = 'Pending' as LabelValue - - if (results.filter((item) => isTransactionListItem(item)).length === 0) { - results.splice(0, 1) - } - } - - if (results[1] && isConflictHeaderListItem(results[1])) { - // Check if we both conflicting txs are still pending - if (results.filter((item) => isTransactionListItem(item)).length <= 1) { - results.splice(1, 1) - } - } - - return results.length ? { results } : undefined + return filterUntrustedQueue(untrustedQueue, pendingIds) }, [untrustedQueue, pendingIds]) return useMemo( diff --git a/src/hooks/useRemainingRelays.ts b/src/hooks/useRemainingRelays.ts index df340b71eb..9903453847 100644 --- a/src/hooks/useRemainingRelays.ts +++ b/src/hooks/useRemainingRelays.ts @@ -6,14 +6,18 @@ import { getRelayCount } from '@safe-global/safe-gateway-typescript-sdk' export const MAX_HOUR_RELAYS = 5 -export const useRelaysBySafe = () => { +export const useRelaysBySafe = (txOrigin?: string) => { const chain = useCurrentChain() const { safe, safeAddress } = useSafeInfo() return useAsync(() => { - if (!safeAddress || !chain || !hasFeature(chain, FEATURES.RELAYING)) return - - return getRelayCount(chain.chainId, safeAddress) + if (!safeAddress || !chain) return + if ( + hasFeature(chain, FEATURES.RELAYING) || + (hasFeature(chain, FEATURES.RELAY_NATIVE_SWAPS) && txOrigin && JSON.parse(txOrigin).name === 'Safe Swap') + ) { + return getRelayCount(chain.chainId, safeAddress) + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [chain, safeAddress, safe.txHistoryTag]) } diff --git a/src/hooks/useTransactionStatus.ts b/src/hooks/useTransactionStatus.ts index 946cf19527..ce79a694ae 100644 --- a/src/hooks/useTransactionStatus.ts +++ b/src/hooks/useTransactionStatus.ts @@ -1,7 +1,7 @@ import { ReplaceTxHoverContext } from '@/components/transactions/GroupedTxListItems/ReplaceTxHoverProvider' import { useAppSelector } from '@/store' import { PendingStatus, selectPendingTxById } from '@/store/pendingTxsSlice' -import { isCancelledSwap, isSignableBy } from '@/utils/transaction-guards' +import { isCancelledSwapOrder, isSignableBy } from '@/utils/transaction-guards' import type { TransactionSummary } from '@safe-global/safe-gateway-typescript-sdk' import { TransactionStatus } from '@safe-global/safe-gateway-typescript-sdk' import { useContext } from 'react' @@ -37,7 +37,7 @@ const useTransactionStatus = (txSummary: TransactionSummary): string => { const wallet = useWallet() const pendingTx = useAppSelector((state) => selectPendingTxById(state, id)) - if (isCancelledSwap(txSummary.txInfo)) { + if (isCancelledSwapOrder(txSummary.txInfo)) { return STATUS_LABELS['CANCELLED'] } diff --git a/src/hooks/useTransactionType.ts b/src/hooks/useTransactionType.ts index b5d9c703ca..a688c1975b 100644 --- a/src/hooks/useTransactionType.ts +++ b/src/hooks/useTransactionType.ts @@ -10,6 +10,7 @@ import { import { isCancellationTxInfo, isModuleExecutionInfo, isOutgoingTransfer, isTxQueued } from '@/utils/transaction-guards' import useAddressBook from './useAddressBook' import type { AddressBook } from '@/store/addressBookSlice' +import { TWAP_ORDER_TITLE } from '@/features/swap/constants' const getTxTo = ({ txInfo }: Pick<TransactionSummary, 'txInfo'>): AddressEx | undefined => { switch (txInfo.type) { @@ -44,6 +45,7 @@ export const getTransactionType = (tx: TransactionSummary, addressBook: AddressB text: 'Safe Account created', } } + case TransactionInfoType.SWAP_TRANSFER: case TransactionInfoType.TRANSFER: { const isSendTx = isOutgoingTransfer(tx.txInfo) @@ -70,6 +72,12 @@ export const getTransactionType = (tx: TransactionSummary, addressBook: AddressB text: orderClass === 'limit' ? 'Limit order' : 'Swap order', } } + case TransactionInfoType.TWAP_ORDER: { + return { + icon: '/images/common/swap.svg', + text: TWAP_ORDER_TITLE, + } + } case TransactionInfoType.CUSTOM: { if (isModuleExecutionInfo(tx.executionInfo)) { return { diff --git a/src/hooks/useTxNotifications.ts b/src/hooks/useTxNotifications.ts index afd3dc5c4a..98967e59e8 100644 --- a/src/hooks/useTxNotifications.ts +++ b/src/hooks/useTxNotifications.ts @@ -1,14 +1,12 @@ import { useEffect, useMemo, useRef } from 'react' import { formatError } from '@/utils/formatters' -import type { LinkProps } from 'next/link' import { selectNotifications, showNotification } from '@/store/notificationsSlice' import { useAppDispatch, useAppSelector } from '@/store' import { TxEvent, txSubscribe } from '@/services/tx/txEvents' -import { AppRoutes } from '@/config/routes' import { useCurrentChain } from './useChains' import useTxQueue from './useTxQueue' import { isSignableBy, isTransactionListItem } from '@/utils/transaction-guards' -import { type ChainInfo, TransactionStatus } from '@safe-global/safe-gateway-typescript-sdk' +import { TransactionStatus } from '@safe-global/safe-gateway-typescript-sdk' import { selectPendingTxs } from '@/store/pendingTxsSlice' import useIsSafeOwner from '@/hooks/useIsSafeOwner' import useWallet from './wallets/useWallet' @@ -16,6 +14,7 @@ import useSafeAddress from './useSafeAddress' import { getExplorerLink } from '@/utils/gateway' import { getTxDetails } from '@/services/transactions' import { isWalletRejection } from '@/utils/wallets' +import { getTxLink } from '@/utils/tx-link' const TxNotifications = { [TxEvent.SIGN_FAILED]: 'Failed to sign. Please try again.', @@ -43,20 +42,6 @@ enum Variant { const successEvents = [TxEvent.PROPOSED, TxEvent.SIGNATURE_PROPOSED, TxEvent.ONCHAIN_SIGNATURE_SUCCESS, TxEvent.SUCCESS] -export const getTxLink = ( - txId: string, - chain: ChainInfo, - safeAddress: string, -): { href: LinkProps['href']; title: string } => { - return { - href: { - pathname: AppRoutes.transactions.tx, - query: { id: txId, safe: `${chain?.shortName}:${safeAddress}` }, - }, - title: 'View transaction', - } -} - const useTxNotifications = (): void => { const dispatch = useAppDispatch() const chain = useCurrentChain() diff --git a/src/hooks/wallets/__tests__/useOnboard.test.ts b/src/hooks/wallets/__tests__/useOnboard.test.ts index d208da552b..7fa1602c35 100644 --- a/src/hooks/wallets/__tests__/useOnboard.test.ts +++ b/src/hooks/wallets/__tests__/useOnboard.test.ts @@ -1,4 +1,3 @@ -import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' import { faker } from '@faker-js/faker' import type { EIP1193Provider, OnboardAPI, WalletState } from '@web3-onboard/core' import { getConnectedWallet, switchWallet } from '../useOnboard' @@ -77,88 +76,7 @@ describe('useOnboard', () => { }) describe('switchWallet', () => { - it('should keep the social signer wallet connected if switching wallets fails', async () => { - const mockOnboard = { - state: { - get: jest.fn().mockReturnValue({ - wallets: [ - { - accounts: [ - { - address: faker.finance.ethereumAddress(), - ens: undefined, - }, - ], - chains: [ - { - id: '5', - }, - ], - label: ONBOARD_MPC_MODULE_LABEL, - }, - ], - }), - }, - connectWallet: jest.fn().mockRejectedValue('Error'), - disconnectWallet: jest.fn(), - } - - await switchWallet(mockOnboard as unknown as OnboardAPI) - - expect(mockOnboard.connectWallet).toHaveBeenCalled() - expect(mockOnboard.disconnectWallet).not.toHaveBeenCalled() - }) - - it('should not disconnect the social signer wallet label if the same label connects', async () => { - const mockNewState = [ - { - accounts: [ - { - address: faker.finance.ethereumAddress(), - ens: undefined, - }, - ], - chains: [ - { - id: '5', - }, - ], - label: ONBOARD_MPC_MODULE_LABEL, - }, - ] - - const mockOnboard = { - state: { - get: jest.fn().mockReturnValue({ - wallets: [ - { - accounts: [ - { - address: faker.finance.ethereumAddress(), - ens: undefined, - }, - ], - chains: [ - { - id: '5', - }, - ], - label: ONBOARD_MPC_MODULE_LABEL, - }, - ], - }), - }, - connectWallet: jest.fn().mockResolvedValue(mockNewState), - disconnectWallet: jest.fn(), - } - - await switchWallet(mockOnboard as unknown as OnboardAPI) - - expect(mockOnboard.connectWallet).toHaveBeenCalled() - expect(mockOnboard.disconnectWallet).not.toHaveBeenCalled() - }) - - it('should not disconnect non social signer wallets if new wallet connects', async () => { + it('should not disconnect the wallet if new wallet connects', async () => { const mockNewState = [ { accounts: [ @@ -206,54 +124,5 @@ describe('useOnboard', () => { expect(mockOnboard.connectWallet).toBeCalled() expect(mockOnboard.disconnectWallet).not.toHaveBeenCalled() }) - - it('should disconnect the social signer wallet label if new wallet connects', async () => { - const mockNewState = [ - { - accounts: [ - { - address: faker.finance.ethereumAddress(), - ens: undefined, - }, - ], - chains: [ - { - id: '5', - }, - ], - label: 'MetaMask', - }, - ] - - const mockOnboard = { - state: { - get: jest.fn().mockReturnValue({ - wallets: [ - { - accounts: [ - { - address: faker.finance.ethereumAddress(), - ens: undefined, - }, - ], - chains: [ - { - id: '5', - }, - ], - label: ONBOARD_MPC_MODULE_LABEL, - }, - ], - }), - }, - connectWallet: jest.fn().mockResolvedValue(mockNewState), - disconnectWallet: jest.fn(), - } - - await switchWallet(mockOnboard as unknown as OnboardAPI) - - expect(mockOnboard.connectWallet).toBeCalled() - expect(mockOnboard.disconnectWallet).toHaveBeenCalledWith({ label: ONBOARD_MPC_MODULE_LABEL }) - }) }) }) diff --git a/src/hooks/wallets/consts.ts b/src/hooks/wallets/consts.ts index 506fac0513..e12c1ccd21 100644 --- a/src/hooks/wallets/consts.ts +++ b/src/hooks/wallets/consts.ts @@ -1,11 +1,11 @@ export const enum WALLET_KEYS { INJECTED = 'INJECTED', WALLETCONNECT_V2 = 'WALLETCONNECT_V2', - SOCIAL = 'SOCIAL_LOGIN', COINBASE = 'COINBASE', LEDGER = 'LEDGER', TREZOR = 'TREZOR', KEYSTONE = 'KEYSTONE', + PK = 'PK', } // TODO: Check if undefined is needed as a return type, possibly couple this with WALLET_MODULES @@ -13,8 +13,8 @@ export const CGW_NAMES: { [key in WALLET_KEYS]: string | undefined } = { [WALLET_KEYS.INJECTED]: 'detectedwallet', [WALLET_KEYS.WALLETCONNECT_V2]: 'walletConnect_v2', [WALLET_KEYS.COINBASE]: 'coinbase', - [WALLET_KEYS.SOCIAL]: 'socialSigner', [WALLET_KEYS.LEDGER]: 'ledger', [WALLET_KEYS.TREZOR]: 'trezor', [WALLET_KEYS.KEYSTONE]: 'keystone', + [WALLET_KEYS.PK]: 'pk', } diff --git a/src/hooks/wallets/mpc/__tests__/useMPC.test.ts b/src/hooks/wallets/mpc/__tests__/useMPC.test.ts deleted file mode 100644 index 6cc2de6dc6..0000000000 --- a/src/hooks/wallets/mpc/__tests__/useMPC.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -import * as useOnboard from '@/hooks/wallets/useOnboard' -import * as socialWalletOptions from '@/services/mpc/config' -import { waitFor } from '@/tests/test-utils' -import { _getMPCCoreKitInstance, initMPC, setMPCCoreKitInstance } from '../useMPC' -import { type ChainInfo, RPC_AUTHENTICATION } from '@safe-global/safe-gateway-typescript-sdk' -import { toBeHex } from 'ethers' -import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' -import { type Web3AuthMPCCoreKit, COREKIT_STATUS } from '@web3auth/mpc-core-kit' -import { type EIP1193Provider, type OnboardAPI } from '@web3-onboard/core' - -jest.mock('@web3auth/mpc-core-kit', () => ({ - ...jest.requireActual('@web3auth/mpc-core-kit'), - Web3AuthMPCCoreKit: jest.fn(), -})) - -jest.mock('@/hooks/wallets/mpc/useSocialWallet') - -type MPCProvider = Web3AuthMPCCoreKit['provider'] - -/** - * Mock for creating and initializing the MPC Core Kit - */ -class MockMPCCoreKit { - provider: MPCProvider | null = null - status = COREKIT_STATUS.NOT_INITIALIZED - private mockState - private mockProvider - - /** - * The parameters are set in the mock MPC Core Kit after init() get's called - * - * @param mockState - * @param mockProvider - */ - constructor(mockState: COREKIT_STATUS, mockProvider: MPCProvider) { - this.mockState = mockState - this.mockProvider = mockProvider - } - - init() { - this.status = this.mockState - this.provider = this.mockProvider - return Promise.resolve() - } -} - -/** - * Small helper class that implements registering RPC event listeners and event emiting. - * Used to test that events onboard relies on are getting called correctly - */ -class EventEmittingMockProvider { - private chainChangedListeners: Function[] = [] - - addListener(event: string, listener: Function) { - if (event === 'chainChanged') { - this.chainChangedListeners.push(listener) - } - } - - emit(event: string, ...args: any[]) { - this.chainChangedListeners.forEach((listener) => listener(...args)) - } -} - -describe('initMPC', () => { - const mockOnboard = { - state: { - get: () => ({ - wallets: [], - walletModules: [], - }), - }, - } as unknown as OnboardAPI - - const mockChain = { - chainId: '5', - chainName: 'Goerli', - blockExplorerUriTemplate: { - address: 'https://goerli.someprovider.io/{address}', - txHash: 'https://goerli.someprovider.io/{txHash}', - api: 'https://goerli.someprovider.io/', - }, - nativeCurrency: { - decimals: 18, - logoUri: 'https://logo.goerli.com', - name: 'Goerli ETH', - symbol: 'ETH', - }, - rpcUri: { - authentication: RPC_AUTHENTICATION.NO_AUTHENTICATION, - value: 'https://goerli.somerpc.io', - }, - } as unknown as ChainInfo - - beforeEach(() => { - jest.resetAllMocks() - jest.spyOn(socialWalletOptions, 'isSocialWalletOptions').mockReturnValue(true) - }) - - it('should set the coreKit if user is not logged in yet', async () => { - jest.spyOn(useOnboard, 'connectWallet').mockImplementation(() => Promise.resolve(undefined)) - jest.spyOn(useOnboard, 'getConnectedWallet').mockReturnValue(null) - - const mockWeb3AuthMpcCoreKit = jest.spyOn(require('@web3auth/mpc-core-kit'), 'Web3AuthMPCCoreKit') - mockWeb3AuthMpcCoreKit.mockImplementation(() => { - return new MockMPCCoreKit(COREKIT_STATUS.INITIALIZED, null) - }) - - await initMPC(mockChain, mockOnboard) - - await waitFor(() => { - expect(_getMPCCoreKitInstance()).toBeDefined() - expect(useOnboard.connectWallet).not.toBeCalled() - }) - }) - - it('should call connectWallet after rehydrating a web3auth session', async () => { - const connectWalletSpy = jest.fn().mockImplementation(() => Promise.resolve()) - jest.spyOn(useOnboard, 'connectWallet').mockImplementation(connectWalletSpy) - jest.spyOn(useOnboard, 'getConnectedWallet').mockReturnValue(null) - - const mockWeb3AuthMpcCoreKit = jest.spyOn(require('@web3auth/mpc-core-kit'), 'Web3AuthMPCCoreKit') - const mockProvider = jest.fn() - mockWeb3AuthMpcCoreKit.mockImplementation(() => { - return new MockMPCCoreKit(COREKIT_STATUS.LOGGED_IN, mockProvider as unknown as MPCProvider) - }) - - await initMPC(mockChain, mockOnboard) - - await waitFor(() => { - expect(connectWalletSpy).toBeCalled() - expect(_getMPCCoreKitInstance()).toBeDefined() - }) - }) - - it('should copy event handlers and emit chainChanged if the current chain is updated', async () => { - jest.spyOn(useOnboard, 'connectWallet').mockImplementation(() => Promise.resolve(undefined)) - jest.spyOn(useOnboard, 'getConnectedWallet').mockReturnValue({ - address: toBeHex('0x1', 20), - label: ONBOARD_MPC_MODULE_LABEL, - chainId: '1', - provider: {} as unknown as EIP1193Provider, - }) - - const mockWeb3AuthMpcCoreKit = jest.spyOn(require('@web3auth/mpc-core-kit'), 'Web3AuthMPCCoreKit') - const mockChainChangedListener = jest.fn() - const mockProviderBefore = { - listeners: (eventName: string) => { - if (eventName === 'chainChanged') { - return [mockChainChangedListener] - } - }, - } - - setMPCCoreKitInstance({ - provider: mockProviderBefore, - } as unknown as Web3AuthMPCCoreKit) - - const mockProvider = new EventEmittingMockProvider() - mockWeb3AuthMpcCoreKit.mockImplementation(() => { - return new MockMPCCoreKit(COREKIT_STATUS.LOGGED_IN, mockProvider as unknown as MPCProvider) - }) - - await initMPC(mockChain, mockOnboard) - - await waitFor(() => { - expect(mockChainChangedListener).toHaveBeenCalledWith('0x5') - expect(_getMPCCoreKitInstance()).toBeDefined() - expect(useOnboard.connectWallet).not.toBeCalled() - }) - }) -}) diff --git a/src/hooks/wallets/mpc/useMPC.ts b/src/hooks/wallets/mpc/useMPC.ts deleted file mode 100644 index c73a8e2fee..0000000000 --- a/src/hooks/wallets/mpc/useMPC.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { IS_PRODUCTION } from '@/config/constants' -import { connectWallet, getConnectedWallet } from '@/hooks/wallets/useOnboard' -import ExternalStore from '@/services/ExternalStore' -import { SOCIAL_WALLET_OPTIONS } from '@/services/mpc/config' -import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' -import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' -import { type OnboardAPI } from '@web3-onboard/core' -import { CHAIN_NAMESPACES } from '@web3auth/base' -import type { Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' -import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' -import { getRpcServiceUrl } from '../web3' - -const { getStore, setStore, useStore } = new ExternalStore<Web3AuthMPCCoreKit>() - -export const initMPC = async (chain: ChainInfo, onboard: OnboardAPI) => { - const chainConfig = { - chainId: `0x${Number(chain.chainId).toString(16)}`, - chainNamespace: CHAIN_NAMESPACES.EIP155, - rpcTarget: getRpcServiceUrl(chain.rpcUri), - displayName: chain.chainName, - blockExplorer: new URL(chain.blockExplorerUriTemplate.address).origin, - ticker: chain.nativeCurrency.symbol, - tickerName: chain.nativeCurrency.name, - } - - const currentInstance = getStore() - let previousChainChangedListeners: Function[] = [] - if (currentInstance?.provider) { - // We are already connected. We copy onboards event listener for the chainChanged event to propagate a potentially new chainId - const oldProvider = currentInstance.provider - previousChainChangedListeners = oldProvider.listeners('chainChanged') - } - - const { Web3AuthMPCCoreKit, WEB3AUTH_NETWORK } = await import('@web3auth/mpc-core-kit') - - const web3AuthCoreKit = new Web3AuthMPCCoreKit({ - web3AuthClientId: SOCIAL_WALLET_OPTIONS.web3AuthClientId, - // Available networks are "sapphire_devnet", "sapphire_mainnet" - web3AuthNetwork: WEB3AUTH_NETWORK.MAINNET, - baseUrl: `${window.location.origin}/`, - uxMode: 'popup', - enableLogging: !IS_PRODUCTION, - //@ts-ignore - chainConfig, - manualSync: true, - hashedFactorNonce: 'safe-global-sfa-nonce', - }) - - return web3AuthCoreKit - .init() - .then(() => { - setStore(web3AuthCoreKit) - // If rehydration was successful, connect to onboard - if (web3AuthCoreKit.status !== COREKIT_STATUS.LOGGED_IN || !web3AuthCoreKit.provider) { - return web3AuthCoreKit - } - - const connectedWallet = getConnectedWallet(onboard.state.get().wallets) - if (!connectedWallet) { - connectWallet(onboard, { - autoSelect: { - label: ONBOARD_MPC_MODULE_LABEL, - disableModals: true, - }, - }).catch((reason) => console.error('Error connecting to MPC module:', reason)) - } else { - const newProvider = web3AuthCoreKit.provider - - // To propagate the changedChain we disconnect and connect - if (previousChainChangedListeners.length > 0 && newProvider) { - previousChainChangedListeners.forEach((previousListener) => - newProvider.addListener('chainChanged', (...args: []) => previousListener(...args)), - ) - newProvider.emit('chainChanged', `0x${Number(chainConfig.chainId).toString(16)}`) - } - } - - return web3AuthCoreKit - }) - .catch((error) => console.error(error)) -} - -export const _getMPCCoreKitInstance = getStore - -export const setMPCCoreKitInstance = setStore - -export default useStore diff --git a/src/hooks/wallets/mpc/useRehydrateSocialWallet.ts b/src/hooks/wallets/mpc/useRehydrateSocialWallet.ts deleted file mode 100644 index 876517b23f..0000000000 --- a/src/hooks/wallets/mpc/useRehydrateSocialWallet.ts +++ /dev/null @@ -1,73 +0,0 @@ -import useAddressBook from '@/hooks/useAddressBook' -import useChainId from '@/hooks/useChainId' -import { useCurrentChain, useHasFeature } from '@/hooks/useChains' -import useOnboard, { connectWallet } from '@/hooks/wallets/useOnboard' -import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' -import { useAppDispatch } from '@/store' -import { upsertAddressBookEntry } from '@/store/addressBookSlice' -import { type WalletState } from '@web3-onboard/core' -import { type UserInfo } from '@web3auth/mpc-core-kit' -import { useCallback, useEffect } from 'react' -import { checksumAddress } from '@/utils/addresses' -import { FEATURES } from '@/utils/chains' -import { isSocialWalletEnabled } from '../wallets' - -const useRehydrateSocialWallet = () => { - const chain = useCurrentChain() - const onboard = useOnboard() - const currentChainId = useChainId() - const addressBook = useAddressBook() - const dispatch = useAppDispatch() - const isSocialLoginEnabled = useHasFeature(FEATURES.SOCIAL_LOGIN) - - const updateAddressBook = useCallback( - (userInfo: UserInfo | undefined, wallets: WalletState[] | undefined | void) => { - if (!userInfo || !wallets || !currentChainId || wallets.length === 0) return - - const address = wallets[0].accounts[0]?.address - if (address) { - const signerAddress = checksumAddress(address) - if (addressBook[signerAddress] === undefined) { - const email = userInfo.email - dispatch(upsertAddressBookEntry({ address: signerAddress, chainId: currentChainId, name: email })) - } - } - }, - [addressBook, currentChainId, dispatch], - ) - - useEffect(() => { - if (!chain || !onboard) return - - const rehydrate = async () => { - const { initMPC } = await import('./useMPC') - const { initSocialWallet } = await import('./useSocialWallet') - const mpcCoreKit = await initMPC(chain, onboard) - - if (!mpcCoreKit) return - - const socialWalletService = await initSocialWallet(mpcCoreKit) - - const onConnect = async () => { - const wallets = await connectWallet(onboard, { - autoSelect: { - label: ONBOARD_MPC_MODULE_LABEL, - disableModals: true, - }, - }).catch((reason) => console.error('Error connecting to MPC module:', reason)) - - // If the signer is not in the address book => add the user's email as name - const userInfo = socialWalletService?.getUserInfo() - updateAddressBook(userInfo, wallets) - } - - socialWalletService.setOnConnect(onConnect) - } - const isOnboardModuleEnabled = isSocialWalletEnabled(chain) - if (isSocialLoginEnabled && isOnboardModuleEnabled) { - void rehydrate() - } - }, [chain, onboard, updateAddressBook, isSocialLoginEnabled]) -} - -export default useRehydrateSocialWallet diff --git a/src/hooks/wallets/mpc/useSocialWallet.ts b/src/hooks/wallets/mpc/useSocialWallet.ts deleted file mode 100644 index 8db21735d6..0000000000 --- a/src/hooks/wallets/mpc/useSocialWallet.ts +++ /dev/null @@ -1,19 +0,0 @@ -import ExternalStore from '@/services/ExternalStore' -import type { ISocialWalletService } from '@/services/mpc/interfaces' -import { type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' - -const { getStore, setStore, useStore } = new ExternalStore<ISocialWalletService>() - -export const initSocialWallet = async (mpcCoreKit: Web3AuthMPCCoreKit) => { - const SocialWalletService = (await import('@/services/mpc/SocialWalletService')).default - const socialWalletService = new SocialWalletService(mpcCoreKit) - setStore(socialWalletService) - - return socialWalletService -} - -export const getSocialWalletService = getStore - -export const __setSocialWalletService = setStore - -export default useStore diff --git a/src/hooks/wallets/useOnboard.ts b/src/hooks/wallets/useOnboard.ts index 500a5629d3..4520682198 100644 --- a/src/hooks/wallets/useOnboard.ts +++ b/src/hooks/wallets/useOnboard.ts @@ -9,8 +9,6 @@ import { logError, Errors } from '@/services/exceptions' import { trackEvent, WALLET_EVENTS } from '@/services/analytics' import { useAppSelector } from '@/store' import { type EnvState, selectRpc } from '@/store/settingsSlice' -import { E2E_WALLET_NAME } from '@/tests/e2e-wallet' -import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' import { formatAmount } from '@/utils/formatNumber' import { localItem } from '@/services/local-storage/local' import { isWalletConnect, isWalletUnlocked } from '@/utils/wallets' @@ -132,18 +130,7 @@ export const connectWallet = async ( } export const switchWallet = async (onboard: OnboardAPI) => { - const oldWalletLabel = getConnectedWallet(onboard.state.get().wallets)?.label - const newWallets = await connectWallet(onboard) - const newWalletLabel = newWallets ? getConnectedWallet(newWallets)?.label : undefined - - // If the wallet actually changed we disconnect the old connected wallet. - if (!newWalletLabel || oldWalletLabel !== ONBOARD_MPC_MODULE_LABEL) { - return - } - - if (newWalletLabel !== oldWalletLabel) { - await onboard.disconnectWallet({ label: oldWalletLabel }) - } + await connectWallet(onboard) } const lastWalletStorage = localItem<string>('lastWallet') @@ -192,7 +179,7 @@ export const useInitOnboard = () => { // e2e wallet if (typeof window !== 'undefined' && window.Cypress) { connectWallet(onboard, { - autoSelect: { label: E2E_WALLET_NAME, disableModals: true }, + autoSelect: { label: 'e2e wallet', disableModals: true }, }) } diff --git a/src/hooks/wallets/wallets.ts b/src/hooks/wallets/wallets.ts index ab81528fab..852b6ccf4f 100644 --- a/src/hooks/wallets/wallets.ts +++ b/src/hooks/wallets/wallets.ts @@ -1,4 +1,4 @@ -import { CYPRESS_MNEMONIC, TREZOR_APP_URL, TREZOR_EMAIL, WC_PROJECT_ID } from '@/config/constants' +import { IS_PRODUCTION, TREZOR_APP_URL, TREZOR_EMAIL, WC_PROJECT_ID } from '@/config/constants' import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' import type { InitOptions } from '@web3-onboard/core' import coinbaseModule from '@web3-onboard/coinbase' @@ -7,11 +7,9 @@ import keystoneModule from '@web3-onboard/keystone/dist/index' import ledgerModule from '@web3-onboard/ledger/dist/index' import trezorModule from '@web3-onboard/trezor' import walletConnect from '@web3-onboard/walletconnect' +import pkModule from '@/services/private-key-module' -import e2eWalletModule from '@/tests/e2e-wallet' import { CGW_NAMES, WALLET_KEYS } from './consts' -import MpcModule from '@/services/mpc/SocialLoginModule' -import { SOCIAL_WALLET_OPTIONS } from '@/services/mpc/config' const prefersDarkMode = (): boolean => { return window?.matchMedia('(prefers-color-scheme: dark)')?.matches @@ -40,16 +38,20 @@ const walletConnectV2 = (chain: ChainInfo) => { }) } -const WALLET_MODULES: { [key in WALLET_KEYS]: (chain: ChainInfo) => WalletInit } = { +const WALLET_MODULES: Partial<{ [key in WALLET_KEYS]: (chain: ChainInfo) => WalletInit }> = { [WALLET_KEYS.INJECTED]: () => injectedWalletModule() as WalletInit, [WALLET_KEYS.WALLETCONNECT_V2]: (chain) => walletConnectV2(chain) as WalletInit, [WALLET_KEYS.COINBASE]: () => coinbaseModule({ darkMode: prefersDarkMode() }) as WalletInit, - [WALLET_KEYS.SOCIAL]: (chain) => MpcModule(chain) as WalletInit, [WALLET_KEYS.LEDGER]: () => ledgerModule() as WalletInit, [WALLET_KEYS.TREZOR]: () => trezorModule({ appUrl: TREZOR_APP_URL, email: TREZOR_EMAIL }) as WalletInit, [WALLET_KEYS.KEYSTONE]: () => keystoneModule() as WalletInit, } +// Testing wallet module +if (!IS_PRODUCTION) { + WALLET_MODULES[WALLET_KEYS.PK] = (chain) => pkModule(chain.chainId, chain.rpcUri) as WalletInit +} + export const getAllWallets = (chain: ChainInfo): WalletInits => { return Object.values(WALLET_MODULES).map((module) => module(chain)) } @@ -60,22 +62,11 @@ export const isWalletSupported = (disabledWallets: string[], walletLabel: string } export const getSupportedWallets = (chain: ChainInfo): WalletInits => { - if (window.Cypress && CYPRESS_MNEMONIC) { - return [e2eWalletModule(chain.rpcUri) as WalletInit] - } const enabledWallets = Object.entries(WALLET_MODULES).filter(([key]) => isWalletSupported(chain.disabledWallets, key)) if (enabledWallets.length === 0) { - return [WALLET_MODULES.INJECTED(chain)] + return [injectedWalletModule()] } return enabledWallets.map(([, module]) => module(chain)) } - -export const isSocialWalletEnabled = (chain: ChainInfo | undefined): boolean => { - if (Object.keys(SOCIAL_WALLET_OPTIONS).length === 0) return false - - if (!chain) return false - - return chain.disabledWallets.every((label) => label !== CGW_NAMES.SOCIAL_LOGIN) -} diff --git a/src/hooks/wallets/web3.ts b/src/hooks/wallets/web3.ts index 2bb3b649d0..ae31d511c3 100644 --- a/src/hooks/wallets/web3.ts +++ b/src/hooks/wallets/web3.ts @@ -29,7 +29,7 @@ export const getRpcServiceUrl = (rpcUri: RpcUri): string => { export const createWeb3ReadOnly = (chain: ChainInfo, customRpc?: string): JsonRpcProvider | undefined => { const url = customRpc || getRpcServiceUrl(chain.rpcUri) if (!url) return - return new JsonRpcProvider(url, undefined, { + return new JsonRpcProvider(url, Number(chain.chainId), { staticNetwork: true, batchMaxCount: BATCH_MAX_COUNT, }) diff --git a/src/pages/403.tsx b/src/pages/403.tsx new file mode 100644 index 0000000000..fd6307a76d --- /dev/null +++ b/src/pages/403.tsx @@ -0,0 +1,24 @@ +import { AppRoutes } from '@/config/routes' +import type { NextPage } from 'next' +import Link from 'next/link' +import MUILink from '@mui/material/Link' + +const Custom403: NextPage = () => { + return ( + <main> + <h1>403 - Access Restricted</h1> + <p> + We regret to inform you that access to this service is currently unavailable in your region. For further + information, you may refer to our{' '} + <Link href={AppRoutes.terms} passHref legacyBehavior> + <MUILink target="_blank" rel="noreferrer"> + terms + </MUILink> + </Link> + . We apologize for any inconvenience this may cause. Thank you for your understanding. + </p> + </main> + ) +} + +export default Custom403 diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index eccc49e4ae..f2c39da99b 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,6 +1,4 @@ import { SentryErrorBoundary } from '@/services/sentry' // needs to be imported first -import useRehydrateSocialWallet from '@/hooks/wallets/mpc/useRehydrateSocialWallet' -import PasswordRecoveryModal from '@/services/mpc/PasswordRecoveryModal' import type { ReactNode } from 'react' import { type ReactElement } from 'react' import { type AppProps } from 'next/app' @@ -25,7 +23,7 @@ import useSafeNotifications from '@/hooks/useSafeNotifications' import useTxPendingStatuses from '@/hooks/useTxPendingStatuses' import { useInitSession } from '@/hooks/useInitSession' import Notifications from '@/components/common/Notifications' -import CookieBanner from '@/components/common/CookieBanner' +import CookieAndTermBanner from 'src/components/common/CookieAndTermBanner' import { useDarkMode } from '@/hooks/useDarkMode' import { cgwDebugStorage } from '@/components/sidebar/DebugToggle' import { useTxTracking } from '@/hooks/useTxTracking' @@ -44,6 +42,8 @@ import { useNotificationTracking } from '@/components/settings/PushNotifications import Recovery from '@/features/recovery/components/Recovery' import WalletProvider from '@/components/common/WalletProvider' import CounterfactualHooks from '@/features/counterfactual/CounterfactualHooks' +import PkModulePopup from '@/services/private-key-module/PkModulePopup' +import GeoblockingProvider from '@/components/common/GeoblockingProvider' const GATEWAY_URL = IS_PRODUCTION || cgwDebugStorage.get() ? GATEWAY_URL_PRODUCTION : GATEWAY_URL_STAGING @@ -68,7 +68,6 @@ const InitApp = (): null => { useTxTracking() useSafeMsgTracking() useBeamer() - useRehydrateSocialWallet() return null } @@ -86,7 +85,9 @@ export const AppProviders = ({ children }: { children: ReactNode | ReactNode[] } <ThemeProvider theme={safeTheme}> <SentryErrorBoundary showDialog fallback={ErrorBoundary}> <WalletProvider> - <TxModalProvider>{children}</TxModalProvider> + <GeoblockingProvider> + <TxModalProvider>{children}</TxModalProvider> + </GeoblockingProvider> </WalletProvider> </SentryErrorBoundary> </ThemeProvider> @@ -124,15 +125,15 @@ const WebCoreApp = ({ <Component {...pageProps} key={safeKey} /> </PageLayout> - <CookieBanner /> + <CookieAndTermBanner /> <Notifications /> - <PasswordRecoveryModal /> - <Recovery /> <CounterfactualHooks /> + + <PkModulePopup /> </AppProviders> </CacheProvider> </Provider> diff --git a/src/pages/apps/index.tsx b/src/pages/apps/index.tsx index 801901fe6a..69ff8eec29 100644 --- a/src/pages/apps/index.tsx +++ b/src/pages/apps/index.tsx @@ -73,6 +73,7 @@ const SafeApps: NextPage = () => { {/* All apps */} <SafeAppList title="All apps" + isFiltered={isFiltered} safeAppsList={isFiltered ? filteredApps : nonPinnedApps} safeAppsListLoading={remoteSafeAppsLoading} bookmarkedSafeAppsId={pinnedSafeAppIds} diff --git a/src/pages/apps/open.tsx b/src/pages/apps/open.tsx index 938804f974..938e690dcb 100644 --- a/src/pages/apps/open.tsx +++ b/src/pages/apps/open.tsx @@ -17,8 +17,6 @@ import { AppRoutes } from '@/config/routes' import { getOrigin } from '@/components/safe-apps/utils' import { useHasFeature } from '@/hooks/useChains' import { FEATURES } from '@/utils/chains' -import { openWalletConnect } from '@/features/walletconnect/components' -import { isWalletConnectSafeApp } from '@/utils/gateway' const SafeApps: NextPage = () => { const chainId = useChainId() @@ -28,7 +26,6 @@ const SafeApps: NextPage = () => { const safeAppData = allSafeApps.find((app) => app.url === appUrl) const { safeApp, isLoading } = useSafeAppFromManifest(appUrl || '', chainId, safeAppData) const isSafeAppsEnabled = useHasFeature(FEATURES.SAFE_APPS) - const isWalletConnectEnabled = useHasFeature(FEATURES.NATIVE_WALLETCONNECT) const { addPermissions, getPermissions, getAllowedFeaturesList } = useBrowserPermissions() const origin = getOrigin(appUrl) @@ -58,9 +55,12 @@ const SafeApps: NextPage = () => { // appUrl is required to be present if (!isSafeAppsEnabled || !appUrl || !router.isReady) return null - if (isWalletConnectEnabled && isWalletConnectSafeApp(appUrl)) { - openWalletConnect() - goToList() + // No `safe` query param, redirect to the share route + if (router.isReady && !router.query.safe) { + router.push({ + pathname: AppRoutes.share.safeApp, + query: { appUrl }, + }) return null } diff --git a/src/pages/licenses.tsx b/src/pages/licenses.tsx index 2c235ff255..ab217f5c2e 100644 --- a/src/pages/licenses.tsx +++ b/src/pages/licenses.tsx @@ -482,14 +482,6 @@ const SafeLicenses = () => ( </ExternalLink> </TableCell> </TableRow> - <TableRow> - <TableCell>@truffle/hdwallet-provider</TableCell> - <TableCell> - <ExternalLink href="https://github.com/trufflesuite/truffle/blob/develop/LICENSE"> - https://github.com/trufflesuite/truffle/blob/develop/LICENSE - </ExternalLink> - </TableCell> - </TableRow> <TableRow> <TableCell>@web3-onboard/coinbase</TableCell> <TableCell> diff --git a/src/pages/privacy.tsx b/src/pages/privacy.tsx index f168d61e83..ced949e2af 100644 --- a/src/pages/privacy.tsx +++ b/src/pages/privacy.tsx @@ -49,7 +49,7 @@ const SafePrivacyPolicy = () => ( `}</style> <h1>Privacy Policy</h1> - <p>Last updated: January 2024.</p> + <p>Last updated: June 2024.</p> <p> Your privacy is important to us. It is our policy to respect your privacy and comply with any applicable law and regulation regarding any personal information we may collect about you, including across our website,{' '} @@ -338,7 +338,7 @@ const SafePrivacyPolicy = () => ( SECTION 2, THIS DATA WILL BECOME PUBLIC AND IT WILL NOT LIKELY BE POSSIBLE TO DELETE OR CHANGE THE DATA AT ANY GIVEN TIME. </p> - <h4>4.2. Tracking</h4> + <h4>4.2. Tracking & Analysis</h4> <p>4.2.1 We will process the following personal data to analyze your behavior:</p> <ol> <li>IP address (will not be stored for EU users), </li> @@ -370,7 +370,24 @@ const SafePrivacyPolicy = () => ( </ol> </p> <p> - 4.2.2 We conduct technical monitoring of your activity on the platform in order to ensure availability, integrity + 4.2.2 For general operational analysis of the {'Safe{Wallet}'} interface, monitoring transaction origins and + measuring transaction failure rates to ensure improved service performance and reliability, we process information + which constitutes the transaction service database, such as: + </p> + <ol> + <li>signatures</li> + <li>signature_type</li> + <li>ethereum_tx_id</li> + <li>message_hash</li> + <li>safe_app_id</li> + <li>safe_message_id</li> + </ol> + <p> + We conduct this analysis in our legitimate interest to continuously improve our product and service and ensure + increased service performance and reliability. + </p> + <p> + 4.2.3 We conduct technical monitoring of your activity on the platform in order to ensure availability, integrity and robustness of the service. For this purpose we process your: </p> <ol> @@ -383,7 +400,7 @@ const SafePrivacyPolicy = () => ( The lawful basis for this processing is our legitimate interest (GDPR Art.6.1f) in ensuring the correctness of the service. </p> - <p>4.2.3. Anonymized tracking</p> + <p>4.2.4 Anonymized tracking</p> <p> We will anonymize the following personal data to gather anonymous user statistics on your browsing behavior on our website: @@ -689,15 +706,7 @@ const SafePrivacyPolicy = () => ( </a> </li> </ul> - <h4>5.13. Web3Auth</h4> - <p> - We use{' '} - <a href="https://web3auth.io/" target="_blank" rel="noreferrer"> - Web3Auth - </a>{' '} - to create a signer wallet/an owner account by using the user's Gmail account or Apple ID information. - </p> - <h4>5.14. MoonPay</h4> + <h4>5.13. MoonPay</h4> <p> We use{' '} <a href="https://www.moonpay.com/" target="_blank" rel="noreferrer"> @@ -706,7 +715,7 @@ const SafePrivacyPolicy = () => ( to offer on-ramp and off-ramp services. For that purpose personal data is required for KYC/AML or other financial regulatory requirements. This data is encrypted by MoonPay. </p> - <h4>5.15. Spindl</h4> + <h4>5.14. Spindl</h4> <p> We use{' '} <a href="https://www.spindl.xyz" target="_blank" rel="noreferrer"> diff --git a/src/pages/settings/cookies.tsx b/src/pages/settings/cookies.tsx index d8bd965854..c3c53f06ef 100644 --- a/src/pages/settings/cookies.tsx +++ b/src/pages/settings/cookies.tsx @@ -1,4 +1,4 @@ -import { CookieBanner } from '@/components/common/CookieBanner' +import { CookieAndTermBanner } from 'src/components/common/CookieAndTermBanner' import SettingsHeader from '@/components/settings/SettingsHeader' import { Grid, Paper, Typography } from '@mui/material' import type { NextPage } from 'next' @@ -23,7 +23,7 @@ const Cookies: NextPage = () => { </Grid> <Grid item container xs> - <CookieBanner /> + <CookieAndTermBanner /> </Grid> </Grid> </Paper> diff --git a/src/pages/settings/security-login.tsx b/src/pages/settings/security.tsx similarity index 69% rename from src/pages/settings/security-login.tsx rename to src/pages/settings/security.tsx index ddbb86eb7a..73e8b27994 100644 --- a/src/pages/settings/security-login.tsx +++ b/src/pages/settings/security.tsx @@ -4,11 +4,11 @@ import Head from 'next/head' import SettingsHeader from '@/components/settings/SettingsHeader' import SecurityLogin from '@/components/settings/SecurityLogin' -const SecurityLoginPage: NextPage = () => { +const SecurityPage: NextPage = () => { return ( <> <Head> - <title>{'Astar Safe – Settings – Security & Login'} + {'Astar Safe – Settings – Security'} @@ -20,4 +20,4 @@ const SecurityLoginPage: NextPage = () => { ) } -export default SecurityLoginPage +export default SecurityPage diff --git a/src/pages/settings/setup.tsx b/src/pages/settings/setup.tsx index 281027a09b..83a8208403 100644 --- a/src/pages/settings/setup.tsx +++ b/src/pages/settings/setup.tsx @@ -8,6 +8,7 @@ import { RequiredConfirmation } from '@/components/settings/RequiredConfirmation import useSafeInfo from '@/hooks/useSafeInfo' import SettingsHeader from '@/components/settings/SettingsHeader' import DelegatesList from '@/components/settings/DelegatesList' +import SpendingLimits from '@/components/settings/SpendingLimits' const Setup: NextPage = () => { const { safe, safeLoaded } = useSafeInfo() @@ -57,12 +58,14 @@ const Setup: NextPage = () => { - + + + diff --git a/src/pages/swap.tsx b/src/pages/swap.tsx index 1d2f918806..297affc64e 100644 --- a/src/pages/swap.tsx +++ b/src/pages/swap.tsx @@ -1,12 +1,21 @@ import type { NextPage } from 'next' import Head from 'next/head' import { useRouter } from 'next/router' -import SwapWidget from '@/features/swap' +import { GeoblockingContext } from '@/components/common/GeoblockingProvider' +import { useContext } from 'react' +import { AppRoutes } from '@/config/routes' +import dynamic from 'next/dynamic' +const SwapWidgetNoSSR = dynamic(() => import('@/features/swap'), { ssr: false }) const Swap: NextPage = () => { const router = useRouter() + const isBlockedCountry = useContext(GeoblockingContext) const { token, amount } = router.query + if (isBlockedCountry) { + router.replace(AppRoutes['403']) + } + let sell = undefined if (token && amount) { sell = { @@ -22,7 +31,7 @@ const Swap: NextPage = () => {
    - +
    ) diff --git a/src/pages/terms.tsx b/src/pages/terms.tsx index 250866b53f..05befd65ea 100644 --- a/src/pages/terms.tsx +++ b/src/pages/terms.tsx @@ -12,7 +12,7 @@ const SafeTerms = () => ( Terms and Conditions -

    Last updated: January 2024.

    +

    Last updated: July 2024.

    1. What is the scope of the Terms?

      @@ -161,8 +161,12 @@ const SafeTerms = () => (
    1. the responsibility to monitor authorized Transactions or to check the correctness or completeness of - Transactions before you are authorizing them. + Transactions before you are authorizing them;
    2. +
    3. notifications about events occurring in or connection with your Safe Account;
    4. +
    5. recovery of your Safe Account;
    6. +
    7. flagging malicious transactions;
    8. +
    9. issuance of the Safe Token and any related functionalities or reward programs.

    5. What do you need to know about Third-Party Services?

    @@ -268,25 +272,118 @@ const SafeTerms = () => (
  • -

    8. Can we terminate or limit your right to use our Services?

    -
      +

      8. Are we responsible for recovering your Safe Account?

      +
        +
      1. We shall not be responsible for recovering your Safe Account.
      2. +
      3. You are solely responsible for securing a back-up of your Safe Account access as you see fit.
      4. +
      5. + Any recovery feature we provide access to within the Safe App is a mechanism controlled by your Safe Account on + the Blockchain, both of which we don't have any influence over once you have set it up. We will never act + as a recoverer ourselves and don't offer recovery services. The Self Custodial Recovery feature allows you + to determine your own recovery setup and nominate anyone including yourself as your recoverer. The recoverer can + start the recovery process at any time. Please note that we are not responsible for notifying you of this + process (see Section 7 above). Furthermore we reserve the right to cease the access to the Self Custodial + Recovery feature via our Safe App taking the user's reasonable interests into account and providing due + notification. +
      6. +
      7. The recovery feature is provided free of charge and liability is limited pursuant to Section 17.4 below.
      8. +
      + +

      9. Are we responsible for notifying you about events occuring in your Safe Account?

      +
        +
      1. + We shall not be responsible for notifying you of any interactions or events occurring in your Safe Account, be + it on the Blockchain, third-party interfaces, within any other infrastructure, or our Services. +
      2. +
      3. You are responsible for monitoring Safe Account as you see fit.
      4. +
      5. + Any notification service we provide or offer for subscription within the Safe App via e-mail or push + notifications or any other means of communication is provided free of charge and liability is limited pursuant + to Section 17.4 below. Furthermore we reserve the right to change the notification feature from time to time or + cease to provide them without notice. +
      6. +
      + +

      10. Are we responsible for flagging malicious transactions?

      +
        +
      1. We shall not be responsible for flagging malicious transactions in our Safe App.
      2. +
      3. + You are solely responsible for checking any transaction, address, Token or other item you interact with via your + Smart Account in our Safe App.{' '} +
      4. - We may terminate the Agreement and refuse access to the Safe Apps at any time giving 30 days’ prior - notice. The right of the parties to terminate the Agreement for cause remains unaffected. In case of our - termination of the Agreement, you may no longer access your Safe Account via our Services. However, you may + Any security flagging or warning service we provide or offer for subscription within the Safe App is provided + free of charge and liability is limited pursuant to Section 17.4 below. Furthermore we reserve the right to + change the feature from time to time or cease to provide them without notice. +
      5. +
      + +

      + 11. Are we responsible for the issuance of the Safe Token and any related functionalities or reward programs? +

      +
        +
      1. + The Safe Token is issued by the Safe Ecosystem Foundation. We are not the issuer or in any way responsible for + the Safe Token. Furthermore, we do not provide any functionalities to the Safe Token or Safe Token reward + programs. +
      2. +
      3. + You are solely responsible for managing your Safe Tokens just like any other Token in your Safe Account and + solely responsible for your eligibility for any reward programs. +
      4. +
      5. + Any interface we provide that allows you to claim or delegate your Safe Tokens or to participate in any third + party program related to Safe Tokens is provided free of charge and we exclude any and all liability for the + correctness, completeness, speed or timeliness of these services. Furthermore we reserve the right to change the + feature from time to time or cease to provide them without notice. +
      6. +
      + +

      12. Are we responsible for third-party content and services?

      +
        +
      1. + You may view, have access to, and use third-party content and services, for example widget integrations, within + the Safe App (“Third-Party Features”). You view, access, or use Third-Party Features at your own election. Your + reliance on Third-Party Features is subject to separate terms and conditions set forth by the applicable third + party content and/or service provider (“Third-Party Terms”). Third-Party Terms may, amongst other things, +
          +
        1. involve separate fees and charges,
        2. +
        3. include disclaimers or risk warnings,
        4. +
        5. apply a different terms and privacy policy.
        6. +
        +
      2. +
      3. + Third Party Features are provided for your convenience only. We do not verify, curate, or control Third Party + Features.{' '} +
      4. +
      5. + If we offer access to Third-Party Features in the Safe App free of charge by us (Third-Parties may charge + separate fees), the liability for providing access to such Third-Party Feature is limited pursuant to Section + 17.1 below. Furthermore we reserve the right to cease to provide access to those Third-Party Features through + the Safe App without notice. +
      6. +
      + +

      13. Can we terminate or limit your right to use our Services?

      +
        +
      1. + We may cease offering our Services and/or terminate the Agreement and refuse access to the Safe Apps at any + time. The right of the parties to terminate the Agreement at any time for cause remains unaffected. In case of + our termination of the Agreement, you may no longer access your Safe Account via our Services. However, you may continue to access your Safe Account and any Tokens via a third-party wallet provider using your Recovery Phrase and Private Keys.
      2. - We reserve the right to limit the use of the Safe Apps to a specified number of Users if necessary to - protect or ensure the stability and integrity of the Services. We will only be able to limit access to the - Services. At no time will we be able to limit or block access to or transfer your funds without your consent. + We reserve the right to limit the use of the Safe Apps to a specified number of Users if necessary to protect or + ensure the stability and integrity of the Services. We will only be able to limit access to the Services. At no + time will we be able to limit or block access to or transfer your funds without your consent.
      -

      9. Can you terminate your Agreement with us?

      +

      14. Can you terminate your Agreement with us?

      You may terminate the Agreement at any time without notice.

      -

      10. What licenses and access do we grant to you?

      + +

      15. What licenses and access do we grant to you?

      1. All intellectual property rights in Safe Accounts and the Services throughout the world belong to us as owner or @@ -300,7 +397,7 @@ const SafeTerms = () => (
      -

      11. What can you expect from the Services and can we make changes to them?

      +

      16. What can you expect from the Services and can we make changes to them?

      1. Without limiting your mandatory warranties, we provide the Services to you “as is” and “as @@ -327,7 +424,7 @@ const SafeTerms = () => (
      -

      12. What do you agree, warrant and represent?

      +

      17. What do you agree, warrant and represent?

      By using our Services you hereby agree, represent and warrant that:

      1. @@ -387,49 +484,179 @@ const SafeTerms = () => (
      2. You are using the Services at your own risk.
      -

      13. What about our liability to you?

      -

      All our liability is excluded, except for the following:

      - +

      18. What about our liability to you?

      1. - In the event of intent and gross negligence on our part, we are liable for damages – regardless of the legal - grounds. -
      2. -
      3. - In the event of negligence on our part, we are liable for damages resulting from injury to life, body or health. -
      4. -
      5. - In the event of simple negligence on our part, we are only liable for damages resulting from the breach of an - essential contractual duty (e.g. a duty, the performance of which enables the proper execution of the contract - in the first place and on the compliance of which the contractual partner regularly relies and may rely), - whereby our liability shall be limited to compensation of the foreseeable, typically occurring damage. Liability - for the violation of a non-essential contractual duty is excluded. -
      6. -
      7. - The liability for simple negligence only applies to the extent that we do not offer the Safe App and the - Services free of charge (please note, in this context, that any service, network and/or transaction fees may be - charged by third parties via the Blockchain and not necessarily by us). Conversely, this means that we are not - liable in cases of simple negligence, when you obtain the Safe App or the service from us free of charge. -
      8. -
      9. - The limitations of liability according to Clauses 13.2 to 13.4. do not apply as far as we have assumed a - guarantee or we have fraudulently concealed a defect in the Services. These limitations of liability also do not - apply to your claims according to the Product Liability Act (”Produkthaftungsgesetz”) and any applicable data - privacy laws. -
      10. -
      11. - If you suffer damages from the loss of data, we are not liable for this, as far as the damages would have been - avoided by your regular and complete backup of all relevant data. -
      12. -
      13. - In the event of disruptions to the technical infrastructure, the internet connection or a relevant Blockchain - that we are not responsible for, we shall be exempt from our obligation to perform. This also applies if we are - prevented from performing due to force majeure or other circumstances, the elimination of which is not possible - or cannot be economically expected of CC. + If the Safe App or Services are provided to the User free of charge (please note, in this context, that any + service, network, and/or transaction fees may be charged by third parties via the Blockchain and not necessarily + by us), CC shall be liable only in cases of intent, gross negligence, or if CC has fraudulently concealed a + possible material or legal defect of the Safe App or Services. If the Safe App or Services are not provided to + the User free of charge, CC shall be liable only (i) in cases pursuant to Clause 17.1 as well as (ii) in cases + of simple negligence for damages resulting from the breach of an essential contractual duty, a duty, the + performance of which enables the proper execution of this Agreement in the first place and on the compliance of + which the User regularly relies and may rely, whereby CC's liability shall be limited to the compensation + of the foreseeable, typically occurring damage. The Parties agree that the typical foreseeable damage equals the + sum of the annual Fees paid or agreed to be paid by the User to CC during the course of the calendar year in + which the event giving rise to the damage claim occurred. Liability in cases of simple negligence for damages + resulting from the breach of a non-essential contractual duty are excluded. The limitations of liability + according to Clause 17.1 and Clause 17.2 do not apply (i) to damages resulting from injury to life, body or + health, (ii) insofar as CC has assumed a guarantee, (iii) to claims of the User according to the Product + Liability Act and (iv) to claims of the User according to the applicable data protection law. The limitation of + liability also applies to the personal liability of the organs, legal representatives, employees and vicarious + agents of CC. If the User suffers damages due to the loss of data, CC is not liable for this, insofar as the + damage would have been avoided by a regular and complete backup of all relevant data by the User. In the event + of disruptions to the technical infrastructure, the internet connection or a relevant Blockchain that we are not + responsible for, we shall be exempt from our obligation to perform. This also applies if we are prevented from + performing due to force majeure or other circumstances, the elimination of which is not possible or cannot be + economically expected of CC. +
      14. +
      15. + If the Safe App or Services are provided to the User free of charge (please note, in this context, that any + service, network and/or transaction fees may be charged by third parties via the Blockchain and not necessarily + by us), CC shall be liable only in cases of intent, gross negligence or if CC has fraudulently concealed a + possible material or legal defect of the Safe App or Services. If the Safe App or Services are not provided to + the User free of charge, CC shall be liable only (i) in cases pursuant to Clause 17.1 as well as (ii) in cases + of simple negligence for damages resulting from the breach of an essential contractual duty, a duty, the + performance of which enables the proper execution of this Agreement in the first place and on the compliance of + which the User regularly relies and may rely, whereby CC's liability shall be limited to the compensation + of the foreseeable, typically occurring damage. The Parties agree that the typical foreseeable damage equals the + sum of the annual Fees paid or agreed to be paid by the User to CC during the course of the calendar year in + which the event giving rise to the damage claim occurred. Liability in cases of simple negligence for damages + resulting from the breach of a non-essential contractual duty are excluded. The limitations of liability + according to Clause 17.1 and Clause 17.2 do not apply (i) to damages resulting from injury to life, body or + health, (ii) insofar as CC has assumed a guarantee, (iii) to claims of the User according to the Product + Liability Act and (iv) to claims of the User according to the applicable data protection law. The limitation of + liability also applies to the personal liability of the organs, legal representatives, employees and vicarious + agents of CC. If the User suffers damages due to the loss of data, CC is not liable for this, insofar as the + damage would have been avoided by a regular and complete backup of all relevant data by the User. In the event + of disruptions to the technical infrastructure, the internet connection or a relevant Blockchain that we are not + responsible for, we shall be exempt from our obligation to perform. This also applies if we are prevented from + performing due to force majeure or other circumstances, the elimination of which is not possible or cannot be + economically expected of CC. +
      16. +
      17. + If the Safe App or Services are provided to the User free of charge (please note, in this context, that any + service, network and/or transaction fees may be charged by third parties via the Blockchain and not necessarily + by us), CC shall be liable only in cases of intent, gross negligence or if CC has fraudulently concealed a + possible material or legal defect of the Safe App or Services. If the Safe App or Services are not provided to + the User free of charge, CC shall be liable only (i) in cases pursuant to Clause 17.1 as well as (ii) in cases + of simple negligence for damages resulting from the breach of an essential contractual duty, a duty, the + performance of which enables the proper execution of this Agreement in the first place and on the compliance of + which the User regularly relies and may rely, whereby CC's liability shall be limited to the compensation + of the foreseeable, typically occurring damage. The Parties agree that the typical foreseeable damage equals the + sum of the annual Fees paid or agreed to be paid by the User to CC during the course of the calendar year in + which the event giving rise to the damage claim occurred. Liability in cases of simple negligence for damages + resulting from the breach of a non-essential contractual duty are excluded. The limitations of liability + according to Clause 17.1 and Clause 17.2 do not apply (i) to damages resulting from injury to life, body or + health, (ii) insofar as CC has assumed a guarantee, (iii) to claims of the User according to the Product + Liability Act and (iv) to claims of the User according to the applicable data protection law. The limitation of + liability also applies to the personal liability of the organs, legal representatives, employees and vicarious + agents of CC. If the User suffers damages due to the loss of data, CC is not liable for this, insofar as the + damage would have been avoided by a regular and complete backup of all relevant data by the User. In the event + of disruptions to the technical infrastructure, the internet connection or a relevant Blockchain that we are not + responsible for, we shall be exempt from our obligation to perform. This also applies if we are prevented from + performing due to force majeure or other circumstances, the elimination of which is not possible or cannot be + economically expected of CC. +
      18. +
      19. + If the Safe App or Services are provided to the User free of charge (please note, in this context, that any + service, network and/or transaction fees may be charged by third parties via the Blockchain and not necessarily + by us), CC shall be liable only in cases of intent, gross negligence or if CC has fraudulently concealed a + possible material or legal defect of the Safe App or Services. If the Safe App or Services are not provided to + the User free of charge, CC shall be liable only (i) in cases pursuant to Clause 17.1 as well as (ii) in cases + of simple negligence for damages resulting from the breach of an essential contractual duty, a duty, the + performance of which enables the proper execution of this Agreement in the first place and on the compliance of + which the User regularly relies and may rely, whereby CC's liability shall be limited to the compensation + of the foreseeable, typically occurring damage. The Parties agree that the typical foreseeable damage equals the + sum of the annual Fees paid or agreed to be paid by the User to CC during the course of the calendar year in + which the event giving rise to the damage claim occurred. Liability in cases of simple negligence for damages + resulting from the breach of a non-essential contractual duty are excluded. The limitations of liability + according to Clause 17.1 and Clause 17.2 do not apply (i) to damages resulting from injury to life, body or + health, (ii) insofar as CC has assumed a guarantee, (iii) to claims of the User according to the Product + Liability Act and (iv) to claims of the User according to the applicable data protection law. The limitation of + liability also applies to the personal liability of the organs, legal representatives, employees and vicarious + agents of CC. If the User suffers damages due to the loss of data, CC is not liable for this, insofar as the + damage would have been avoided by a regular and complete backup of all relevant data by the User. In the event + of disruptions to the technical infrastructure, the internet connection or a relevant Blockchain that we are not + responsible for, we shall be exempt from our obligation to perform. This also applies if we are prevented from + performing due to force majeure or other circumstances, the elimination of which is not possible or cannot be + economically expected of CC. +
      20. +
      21. + If the Safe App or Services are provided to the User free of charge (please note, in this context, that any + service, network and/or transaction fees may be charged by third parties via the Blockchain and not necessarily + by us), CC shall be liable only in cases of intent, gross negligence or if CC has fraudulently concealed a + possible material or legal defect of the Safe App or Services. If the Safe App or Services are not provided to + the User free of charge, CC shall be liable only (i) in cases pursuant to Clause 17.1 as well as (ii) in cases + of simple negligence for damages resulting from the breach of an essential contractual duty, a duty, the + performance of which enables the proper execution of this Agreement in the first place and on the compliance of + which the User regularly relies and may rely, whereby CC's liability shall be limited to the compensation + of the foreseeable, typically occurring damage. The Parties agree that the typical foreseeable damage equals the + sum of the annual Fees paid or agreed to be paid by the User to CC during the course of the calendar year in + which the event giving rise to the damage claim occurred. Liability in cases of simple negligence for damages + resulting from the breach of a non-essential contractual duty are excluded. The limitations of liability + according to Clause 17.1 and Clause 17.2 do not apply (i) to damages resulting from injury to life, body or + health, (ii) insofar as CC has assumed a guarantee, (iii) to claims of the User according to the Product + Liability Act and (iv) to claims of the User according to the applicable data protection law. The limitation of + liability also applies to the personal liability of the organs, legal representatives, employees and vicarious + agents of CC. If the User suffers damages due to the loss of data, CC is not liable for this, insofar as the + damage would have been avoided by a regular and complete backup of all relevant data by the User. In the event + of disruptions to the technical infrastructure, the internet connection or a relevant Blockchain that we are not + responsible for, we shall be exempt from our obligation to perform. This also applies if we are prevented from + performing due to force majeure or other circumstances, the elimination of which is not possible or cannot be + economically expected of CC. +
      22. +
      23. + If the Safe App or Services are provided to the User free of charge (please note, in this context, that any + service, network and/or transaction fees may be charged by third parties via the Blockchain and not necessarily + by us), CC shall be liable only in cases of intent, gross negligence or if CC has fraudulently concealed a + possible material or legal defect of the Safe App or Services. If the Safe App or Services are not provided to + the User free of charge, CC shall be liable only (i) in cases pursuant to Clause 17.1 as well as (ii) in cases + of simple negligence for damages resulting from the breach of an essential contractual duty, a duty, the + performance of which enables the proper execution of this Agreement in the first place and on the compliance of + which the User regularly relies and may rely, whereby CC's liability shall be limited to the compensation + of the foreseeable, typically occurring damage. The Parties agree that the typical foreseeable damage equals the + sum of the annual Fees paid or agreed to be paid by the User to CC during the course of the calendar year in + which the event giving rise to the damage claim occurred. Liability in cases of simple negligence for damages + resulting from the breach of a non-essential contractual duty are excluded. The limitations of liability + according to Clause 17.1 and Clause 17.2 do not apply (i) to damages resulting from injury to life, body or + health, (ii) insofar as CC has assumed a guarantee, (iii) to claims of the User according to the Product + Liability Act and (iv) to claims of the User according to the applicable data protection law. The limitation of + liability also applies to the personal liability of the organs, legal representatives, employees and vicarious + agents of CC. If the User suffers damages due to the loss of data, CC is not liable for this, insofar as the + damage would have been avoided by a regular and complete backup of all relevant data by the User. In the event + of disruptions to the technical infrastructure, the internet connection or a relevant Blockchain that we are not + responsible for, we shall be exempt from our obligation to perform. This also applies if we are prevented from + performing due to force majeure or other circumstances, the elimination of which is not possible or cannot be + economically expected of CC. +
      24. +
      25. + If the Safe App or Services are provided to the User free of charge (please note, in this context, that any + service, network and/or transaction fees may be charged by third parties via the Blockchain and not necessarily + by us), CC shall be liable only in cases of intent, gross negligence or if CC has fraudulently concealed a + possible material or legal defect of the Safe App or Services. If the Safe App or Services are not provided to + the User free of charge, CC shall be liable only (i) in cases pursuant to Clause 17.1 as well as (ii) in cases + of simple negligence for damages resulting from the breach of an essential contractual duty, a duty, the + performance of which enables the proper execution of this Agreement in the first place and on the compliance of + which the User regularly relies and may rely, whereby CC's liability shall be limited to the compensation + of the foreseeable, typically occurring damage. The Parties agree that the typical foreseeable damage equals the + sum of the annual Fees paid or agreed to be paid by the User to CC during the course of the calendar year in + which the event giving rise to the damage claim occurred. Liability in cases of simple negligence for damages + resulting from the breach of a non-essential contractual duty are excluded. The limitations of liability + according to Clause 17.1 and Clause 17.2 do not apply (i) to damages resulting from injury to life, body or + health, (ii) insofar as CC has assumed a guarantee, (iii) to claims of the User according to the Product + Liability Act and (iv) to claims of the User according to the applicable data protection law. The limitation of + liability also applies to the personal liability of the organs, legal representatives, employees and vicarious + agents of CC. If the User suffers damages due to the loss of data, CC is not liable for this, insofar as the + damage would have been avoided by a regular and complete backup of all relevant data by the User. In the event + of disruptions to the technical infrastructure, the internet connection or a relevant Blockchain that we are not + responsible for, we shall be exempt from our obligation to perform. This also applies if we are prevented from + performing due to force majeure or other circumstances, the elimination of which is not possible or cannot be + economically expected of CC.
      -

      14. What about viruses, bugs and security vulnerabilities?

      +

      19. What about viruses, bugs and security vulnerabilities?

      1. We endeavor to provide our Service free from material bugs, security vulnerabilities or viruses.
      2. @@ -443,7 +670,7 @@ const SafeTerms = () => (
      -

      15. What if an event outside our control happens that affects our Services?

      +

      20. What if an event outside our control happens that affects our Services?

      1. We may update and change our Services from time to time. We may suspend or withdraw or restrict the availability @@ -485,7 +712,7 @@ const SafeTerms = () => (
      -

      16. Who is responsible for your tax liabilities?

      +

      21. Who is responsible for your tax liabilities?

      You are solely responsible to determine if your use of the Services have tax implications, in particular income tax and capital gains tax relating to the purchase or sale of Tokens, for you. By using the Services you agree not @@ -493,7 +720,7 @@ const SafeTerms = () => ( action or transaction related thereto.

      -

      17. What if a court disagrees with part of this Agreement?

      +

      22. What if a court disagrees with part of this Agreement?

      Should individual provisions of these Terms be or become invalid or unenforceable in whole or in part, this shall not affect the validity of the remaining provisions. The invalid or unenforceable provision shall be replaced by @@ -502,20 +729,20 @@ const SafeTerms = () => ( valid provision that comes as close as possible to the economic purpose of the invalid or unenforceable provision.

      -

      18. What if we do not enforce certain rights under this Agreement?

      +

      23. What if we do not enforce certain rights under this Agreement?

      Our failure to exercise or enforce any right or remedy provided under this Agreement or by law shall not constitute a waiver of that or any other right or remedy, nor shall it prevent or restrict any further exercise of that or any other right or remedy.

      -

      19. Do third parties have rights?

      +

      24. Do third parties have rights?

      Unless it expressly states otherwise, this Agreement does not give rise to any third-party rights, which may be enforced against us.

      -

      20. Can this Agreement be assigned?

      +

      25. Can this Agreement be assigned?

      1. We are entitled to transfer our rights and obligations under the Agreement in whole or in part to third parties @@ -526,13 +753,13 @@ const SafeTerms = () => (
      -

      21. Which Clauses of this Agreement survive termination?

      +

      26. Which Clauses of this Agreement survive termination?

      All covenants, agreements, representations and warranties made in this Agreement shall survive your acceptance of this Agreement and its termination.

      -

      22. Data Protection

      +

      27. Data Protection

      We inform you about our processing of personal data, including the disclosure to third parties and your rights as an affected party, in the{' '} @@ -542,7 +769,7 @@ const SafeTerms = () => ( .

      -

      23. Which laws apply to the Agreement?

      +

      28. Which laws apply to the Agreement?

      The Agreement including these Terms shall be governed by German law. The application of the UN Convention on Contracts for the International Sale of Goods is excluded. For consumers domiciled in another European country but @@ -551,7 +778,7 @@ const SafeTerms = () => ( German law.

      -

      24. How can you get support for Safe Accounts and tell us about any problems?

      +

      29. How can you get support for Safe Accounts and tell us about any problems?

      If you want to learn more about Safe Accounts or the Service or have any problems using them or have any complaints please get in touch via any of the following channels: @@ -583,14 +810,14 @@ const SafeTerms = () => (

    -

    25. Where is the place of legal proceedings?

    +

    30. Where is the place of legal proceedings?

    For users who are merchants within the meaning of the German Commercial Code (Handelsgesetzbuch), a special fund (Sondervermögen) under public law or a legal person under public law, Berlin shall be the exclusive place of jurisdiction for all disputes arising from the contractual relationship.

    -

    26. Is this all?

    +

    31. Is this all?

    These Terms constitute the entire agreement between you and us in relation to the Agreement’s subject matter. It replaces and extinguishes any and all prior agreements, draft agreements, arrangements, warranties, diff --git a/src/pages/transactions/messages.tsx b/src/pages/transactions/messages.tsx index 656ae97f7e..e12a837fae 100644 --- a/src/pages/transactions/messages.tsx +++ b/src/pages/transactions/messages.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react' import Head from 'next/head' -import { FEATURES } from '@safe-global/safe-gateway-typescript-sdk' +import { FEATURES } from '@/utils/chains' import { useRouter } from 'next/router' import type { NextPage } from 'next' diff --git a/src/pages/transactions/msg.tsx b/src/pages/transactions/msg.tsx new file mode 100644 index 0000000000..2db44e1e61 --- /dev/null +++ b/src/pages/transactions/msg.tsx @@ -0,0 +1,25 @@ +import type { NextPage } from 'next' +import Head from 'next/head' + +import Typography from '@mui/material/Typography' +import SingleMsg from '@/components/safe-messages/SingleMsg' + +const SingleTransaction: NextPage = () => { + return ( + <> + + {'Safe{Wallet} – Message details'} + + +

    + + Message details + + + +
    + + ) +} + +export default SingleTransaction diff --git a/src/pages/welcome/social-login.tsx b/src/pages/welcome/social-login.tsx deleted file mode 100644 index 4232d4ff48..0000000000 --- a/src/pages/welcome/social-login.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { NextPage } from 'next' -import Head from 'next/head' -import NewSafeSocial from '@/components/welcome/NewSafeSocial' - -const SocialLogin: NextPage = () => { - return ( - <> - - {'Astar Safe – Welcome'} - - - - - ) -} - -export default SocialLogin diff --git a/src/service-workers/index.ts b/src/service-workers/index.ts index 70f1e849e8..0ed829a135 100644 --- a/src/service-workers/index.ts +++ b/src/service-workers/index.ts @@ -3,8 +3,5 @@ /// import { firebaseMessagingSw } from './firebase-messaging/firebase-messaging-sw' -import { mpcCoreKitServiceWorker } from './mpc-core-kit-sw' firebaseMessagingSw() - -mpcCoreKitServiceWorker() diff --git a/src/service-workers/mpc-core-kit-sw.ts b/src/service-workers/mpc-core-kit-sw.ts deleted file mode 100644 index 76b47bf211..0000000000 --- a/src/service-workers/mpc-core-kit-sw.ts +++ /dev/null @@ -1,325 +0,0 @@ -/// - -declare const self: ServiceWorkerGlobalScope - -export const mpcCoreKitServiceWorker = () => { - /* eslint-disable */ - function getScope() { - return self.registration.scope - } - - self.addEventListener('message', function (event) { - if (event.data && event.data.type === 'SKIP_WAITING') { - self.skipWaiting() - } - }) - - self.addEventListener('fetch', function (event) { - try { - const url = new URL(event.request.url) - //@ts-expect-error - const redirectURL = self.location.url - if (url.pathname.includes('redirect') && url.href.includes(getScope())) { - event.respondWith( - new Response( - new Blob( - [ - ` - - - - - - - Redirect - - - - -
    -
    -
    -
    -
    -
    -

    You can close this window now

    -
    - - - - - - ${''} - `, - ], - { type: 'text/html' }, - ), - ), - ) - } - } catch (error) { - console.error(error) - } - }) -} diff --git a/src/services/ExternalStore.ts b/src/services/ExternalStore.ts index 34b65821e9..fbd5d71960 100644 --- a/src/services/ExternalStore.ts +++ b/src/services/ExternalStore.ts @@ -23,7 +23,7 @@ class ExternalStore { } } - private readonly subscribe = (listener: Listener): (() => void) => { + public readonly subscribe = (listener: Listener): (() => void) => { this.listeners.add(listener) return () => { this.listeners.delete(listener) diff --git a/src/services/analytics/__tests__/tx-tracking.test.ts b/src/services/analytics/__tests__/tx-tracking.test.ts index 41705985ca..d8c58b3219 100644 --- a/src/services/analytics/__tests__/tx-tracking.test.ts +++ b/src/services/analytics/__tests__/tx-tracking.test.ts @@ -138,14 +138,12 @@ describe('getTransactionTrackingType', () => { expect(txType).toEqual(TX_TYPES.rejection) }) - it('should return walletconnect for walletconnect transactions', async () => { + it('should return walletconnect for transactions w/o safeAppInfo', async () => { const txType = await getMockTxType({ txInfo: { type: TransactionInfoType.CUSTOM, }, - safeAppInfo: { - url: 'https://safe-apps.dev.5afe.dev/wallet-connect', - }, + safeAppInfo: null, } as unknown) expect(txType).toEqual(TX_TYPES.walletconnect) @@ -175,14 +173,4 @@ describe('getTransactionTrackingType', () => { expect(txType).toEqual(TX_TYPES.batch) }) - - it('should return custom for unknown transactions', async () => { - const txType = await getMockTxType({ - txInfo: { - type: TransactionInfoType.CUSTOM, - }, - } as unknown) - - expect(txType).toEqual(TX_TYPES.custom) - }) }) diff --git a/src/services/analytics/events/batching.ts b/src/services/analytics/events/batching.ts index 91c42ebe90..e6f312a390 100644 --- a/src/services/analytics/events/batching.ts +++ b/src/services/analytics/events/batching.ts @@ -16,11 +16,6 @@ export const BATCH_EVENTS = { action: 'Tx added to batch', category, }, - // On reorder of batch items - BATCH_REORDER: { - action: 'Batch reorder', - category, - }, // When batch item details are expanded BATCH_EXPAND_TX: { action: 'Expand batched tx', diff --git a/src/services/analytics/events/createLoadSafe.ts b/src/services/analytics/events/createLoadSafe.ts index 32e860e2a8..991c84d294 100644 --- a/src/services/analytics/events/createLoadSafe.ts +++ b/src/services/analytics/events/createLoadSafe.ts @@ -61,10 +61,6 @@ export const CREATE_SAFE_EVENTS = { action: 'Activated Safe', category: CREATE_SAFE_CATEGORY, }, - GET_STARTED: { - action: 'Load Safe', - category: CREATE_SAFE_CATEGORY, - }, OPEN_HINT: { action: 'Open Hint', category: CREATE_SAFE_CATEGORY, diff --git a/src/services/analytics/events/modals.ts b/src/services/analytics/events/modals.ts index b8fd8df76b..e90fb9d334 100644 --- a/src/services/analytics/events/modals.ts +++ b/src/services/analytics/events/modals.ts @@ -63,6 +63,10 @@ export const MODALS_EVENTS = { category: MODALS_CATEGORY, event: EventType.CLICK, }, + SWAP: { + action: 'Swap', + category: MODALS_CATEGORY, + }, } export enum MODAL_NAVIGATION { diff --git a/src/services/analytics/events/mpcWallet.ts b/src/services/analytics/events/mpcWallet.ts deleted file mode 100644 index 53254ead36..0000000000 --- a/src/services/analytics/events/mpcWallet.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { EventType } from '@/services/analytics/types' - -const MPC_WALLET_CATEGORY = 'mpc-wallet' - -export const MPC_WALLET_EVENTS = { - CONNECT_GOOGLE: { - event: EventType.CLICK, - action: 'Continue with Google button', - category: MPC_WALLET_CATEGORY, - }, - MANUAL_RECOVERY: { - event: EventType.META, - action: 'MFA login started', - category: MPC_WALLET_CATEGORY, - }, - RECOVER_PASSWORD: { - event: EventType.CLICK, - action: 'Recover account using password', - category: MPC_WALLET_CATEGORY, - }, - RECOVERED_SOCIAL_SIGNER: { - event: EventType.META, - action: 'Recovered social signer', - category: MPC_WALLET_CATEGORY, - }, - UPSERT_PASSWORD: { - event: EventType.CLICK, - action: 'Set or change password', - category: MPC_WALLET_CATEGORY, - }, - ENABLE_MFA: { - event: EventType.META, - action: 'Enable MFA for account', - category: MPC_WALLET_CATEGORY, - }, - REVEAL_PRIVATE_KEY: { - event: EventType.CLICK, - action: 'Reveal private key', - category: MPC_WALLET_CATEGORY, - }, - EXPORT_PK_SUCCESS: { - event: EventType.META, - action: 'Export private key successful', - category: MPC_WALLET_CATEGORY, - }, - EXPORT_PK_ERROR: { - event: EventType.META, - action: 'Export private key error', - category: MPC_WALLET_CATEGORY, - }, - SEE_PK: { - event: EventType.CLICK, - action: 'Toggle see private key', - category: MPC_WALLET_CATEGORY, - }, - COPY_PK: { - event: EventType.CLICK, - action: 'Copy private key', - category: MPC_WALLET_CATEGORY, - }, -} diff --git a/src/services/analytics/events/overview.ts b/src/services/analytics/events/overview.ts index f006f126b8..0552c87cfd 100644 --- a/src/services/analytics/events/overview.ts +++ b/src/services/analytics/events/overview.ts @@ -38,10 +38,12 @@ export const OVERVIEW_EVENTS = { TOTAL_SAFES_OWNED: { action: 'Total Safes owned', category: OVERVIEW_CATEGORY, + event: EventType.META, }, TOTAL_SAFES_WATCHLIST: { action: 'Total Safes watchlist', category: OVERVIEW_CATEGORY, + event: EventType.META, }, SIDEBAR: { action: 'Sidebar', diff --git a/src/services/analytics/events/swaps.ts b/src/services/analytics/events/swaps.ts index 6b7fb3bf93..a90da152ac 100644 --- a/src/services/analytics/events/swaps.ts +++ b/src/services/analytics/events/swaps.ts @@ -7,8 +7,11 @@ export const SWAP_EVENTS = { }, } -export const SWAP_LABELS = { - dashboard: 'dashboard', - sidebar: 'sidebar', - asset: 'asset', +export enum SWAP_LABELS { + dashboard = 'dashboard', + sidebar = 'sidebar', + asset = 'asset', + dashboard_assets = 'dashboard_assets', + promoWidget = 'promoWidget', + safeAppsPromoWidget = 'safeAppsPromoWidget', } diff --git a/src/services/analytics/events/transactions.ts b/src/services/analytics/events/transactions.ts index 48a456f7ee..74f83b9c77 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', @@ -52,4 +53,9 @@ export const TX_EVENTS = { action: 'Speed up transaction', category: TX_CATEGORY, }, + EXECUTE_THROUGH_ROLE: { + event: EventType.TX_EXECUTED_THROUGH_ROLE, + action: 'Execute transaction through role', + category: TX_CATEGORY, + }, } diff --git a/src/services/analytics/events/txList.ts b/src/services/analytics/events/txList.ts index 7cdcc687b4..12b0ea586d 100644 --- a/src/services/analytics/events/txList.ts +++ b/src/services/analytics/events/txList.ts @@ -67,4 +67,8 @@ export const MESSAGE_EVENTS = { action: 'Sign message', category: TX_LIST_CATEGORY, }, + COPY_DEEPLINK: { + action: 'Copy message deeplink', + category: TX_LIST_CATEGORY, + }, } diff --git a/src/services/analytics/gtm.ts b/src/services/analytics/gtm.ts index cb004e7de9..3945b2d9ed 100644 --- a/src/services/analytics/gtm.ts +++ b/src/services/analytics/gtm.ts @@ -58,7 +58,7 @@ export const gtmSetDeviceType = (type: DeviceType): void => { } export const gtmSetSafeAddress = (safeAddress: string): void => { - commonEventParams.safeAddress = safeAddress.slice(2) + commonEventParams.safeAddress = safeAddress.slice(2) // Remove 0x prefix } export const gtmInit = (): void => { diff --git a/src/services/analytics/tx-tracking.ts b/src/services/analytics/tx-tracking.ts index 6073c1a489..0f9af3359b 100644 --- a/src/services/analytics/tx-tracking.ts +++ b/src/services/analytics/tx-tracking.ts @@ -1,6 +1,5 @@ import { TX_TYPES } from '@/services/analytics/events/transactions' import { getTxDetails } from '@/services/transactions' -import { isWalletConnectSafeApp } from '@/utils/gateway' import { SettingsInfoType, type TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' import { isERC721Transfer, @@ -9,7 +8,7 @@ import { isTransferTxInfo, isCustomTxInfo, isCancellationTxInfo, - isSwapTxInfo, + isSwapOrderTxInfo, } from '@/utils/transaction-guards' export const getTransactionTrackingType = async (chainId: string, txId: string): Promise => { @@ -30,7 +29,7 @@ export const getTransactionTrackingType = async (chainId: string, txId: string): return TX_TYPES.transfer_token } - if (isSwapTxInfo(txInfo)) { + if (isSwapOrderTxInfo(txInfo)) { return TX_TYPES.native_swap } @@ -63,12 +62,14 @@ export const getTransactionTrackingType = async (chainId: string, txId: string): } if (details.safeAppInfo) { - return isWalletConnectSafeApp(details.safeAppInfo.url) ? TX_TYPES.walletconnect : details.safeAppInfo.url + return details.safeAppInfo.url } if (isMultiSendTxInfo(txInfo)) { return TX_TYPES.batch } + + return TX_TYPES.walletconnect } return TX_TYPES.custom diff --git a/src/services/analytics/types.ts b/src/services/analytics/types.ts index 855b791403..781615ffbf 100644 --- a/src/services/analytics/types.ts +++ b/src/services/analytics/types.ts @@ -13,6 +13,7 @@ export enum EventType { TX_CREATED = 'tx_created', TX_CONFIRMED = 'tx_confirmed', TX_EXECUTED = 'tx_executed', + TX_EXECUTED_THROUGH_ROLE = 'tx_executed_through_role', } export type EventLabel = string | number | boolean | null diff --git a/src/services/analytics/useGtm.ts b/src/services/analytics/useGtm.ts index f5e1906a39..95f546f6d5 100644 --- a/src/services/analytics/useGtm.ts +++ b/src/services/analytics/useGtm.ts @@ -16,7 +16,7 @@ import { } from '@/services/analytics/gtm' import { spindlInit, spindlAttribute } from './spindl' import { useAppSelector } from '@/store' -import { CookieType, selectCookies } from '@/store/cookiesSlice' +import { CookieAndTermType, selectCookies } from '@/store/cookiesAndTermsSlice' import useChainId from '@/hooks/useChainId' import { useRouter } from 'next/router' import { AppRoutes } from '@/config/routes' @@ -30,7 +30,7 @@ import { OVERVIEW_EVENTS } from './events' const useGtm = () => { const chainId = useChainId() const cookies = useAppSelector(selectCookies) - const isAnalyticsEnabled = cookies[CookieType.ANALYTICS] || false + const isAnalyticsEnabled = cookies[CookieAndTermType.ANALYTICS] || false const [, setPrevAnalytics] = useState(isAnalyticsEnabled) const router = useRouter() const theme = useTheme() @@ -96,7 +96,7 @@ const useGtm = () => { useEffect(() => { if (wallet?.address) { - gtmSetUserProperty(AnalyticsUserProperties.WALLET_ADDRESS, wallet.address) + gtmSetUserProperty(AnalyticsUserProperties.WALLET_ADDRESS, wallet.address.slice(2)) // Remove 0x prefix spindlAttribute(wallet.address) } }, [wallet?.address]) diff --git a/src/services/contracts/deployments.ts b/src/services/contracts/deployments.ts index d1bf681416..181b8ff5b6 100644 --- a/src/services/contracts/deployments.ts +++ b/src/services/contracts/deployments.ts @@ -6,6 +6,7 @@ import { getFallbackHandlerDeployment, getProxyFactoryDeployment, getSignMessageLibDeployment, + getCreateCallDeployment, } from '@safe-global/safe-deployments' import type { SingletonDeployment, DeploymentFilter } from '@safe-global/safe-deployments' import type { ChainInfo, SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' @@ -79,3 +80,7 @@ export const getProxyFactoryContractDeployment = (chainId: string, safeVersion: export const getSignMessageLibContractDeployment = (chainId: string, safeVersion: SafeInfo['version']) => { return _tryDeploymentVersions(getSignMessageLibDeployment, chainId, safeVersion) } + +export const getCreateCallContractDeployment = (chainId: string, safeVersion: SafeInfo['version']) => { + return _tryDeploymentVersions(getCreateCallDeployment, chainId, safeVersion) +} diff --git a/src/services/exceptions/ErrorCodes.ts b/src/services/exceptions/ErrorCodes.ts index 95c50e7639..be96e60985 100644 --- a/src/services/exceptions/ErrorCodes.ts +++ b/src/services/exceptions/ErrorCodes.ts @@ -11,7 +11,7 @@ enum ErrorCodes { _101 = '101: Failed to resolve the address', _103 = '103: Error creating a SafeTransaction', _104 = '104: Invalid chain short name in the URL', - _105 = '105: Error initializing the Safe Core SDK', + _105 = '105: Error connecting to the blockchain', _106 = '106: Failed to get connected wallet', _302 = '302: Error connecting to the wallet', @@ -66,6 +66,7 @@ enum ErrorCodes { _812 = '812: Failed to recover', _813 = '813: Failed to cancel recovery', _814 = '814: Failed to speed up transaction', + _815 = '815: Error executing a transaction through a role', _900 = '900: Error loading Safe App', _901 = '901: Error processing Safe Apps SDK request', diff --git a/src/services/local-storage/session.ts b/src/services/local-storage/session.ts index 1d297b5fcd..1a1a45178d 100644 --- a/src/services/local-storage/session.ts +++ b/src/services/local-storage/session.ts @@ -2,4 +2,10 @@ import Storage from './Storage' const session = new Storage(typeof window !== 'undefined' ? window.sessionStorage : undefined) +export const sessionItem = (key: string) => ({ + get: () => session.getItem(key), + set: (value: T) => session.setItem(key, value), + remove: () => session.removeItem(key), +}) + export default session diff --git a/src/services/mpc/PasswordRecoveryModal.tsx b/src/services/mpc/PasswordRecoveryModal.tsx deleted file mode 100644 index 7a7e1d08ab..0000000000 --- a/src/services/mpc/PasswordRecoveryModal.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { PasswordRecovery } from '@/components/common/SocialSigner/PasswordRecovery' -import TxModalDialog from '@/components/common/TxModalDialog' -import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' -import ExternalStore from '@/services/ExternalStore' - -const { useStore: useCloseCallback, setStore: setCloseCallback } = new ExternalStore<() => void>() - -export const open = (cb: () => void) => { - setCloseCallback(() => cb) -} - -export const close = () => { - setCloseCallback(undefined) -} - -const PasswordRecoveryModal = () => { - const socialWalletService = useSocialWallet() - const closeCallback = useCloseCallback() - const open = !!closeCallback - - const handleClose = () => { - closeCallback?.() - setCloseCallback(undefined) - close() - } - - const recoverPassword = async (password: string, storeDeviceFactor: boolean) => { - if (!socialWalletService) return - - await socialWalletService.recoverAccountWithPassword(password, storeDeviceFactor) - } - - if (!open) return null - - return ( - - - - ) -} - -export default PasswordRecoveryModal diff --git a/src/services/mpc/SocialLoginModule.ts b/src/services/mpc/SocialLoginModule.ts deleted file mode 100644 index 84052dfbed..0000000000 --- a/src/services/mpc/SocialLoginModule.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { getWeb3ReadOnly } from '@/hooks/wallets/web3' -import { FEATURES, hasFeature } from '@/utils/chains' -import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' -import { type WalletInit, ProviderRpcError } from '@web3-onboard/common' -import { type EIP1193Provider } from '@web3-onboard/core' -import { type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' - -const assertDefined = (mpcProvider: T | undefined) => { - if (!mpcProvider) { - throw new Error('MPC provider is not ready. Login and initialize it first') - } - return mpcProvider -} - -export const ONBOARD_MPC_MODULE_LABEL = 'Social Login' - -export const isSocialLoginWallet = (walletLabel: string | undefined) => { - return walletLabel === ONBOARD_MPC_MODULE_LABEL -} - -const getConnectedAccounts = async (provider: typeof Web3AuthMPCCoreKit.prototype.provider | undefined) => { - try { - const web3 = assertDefined(provider) - return web3.request({ method: 'eth_accounts' }) - } catch (e) { - throw new ProviderRpcError({ - code: 4001, - message: 'Provider is unavailable', - }) - } -} - -/** - * Module for MPC wallet created by the Web3Auth tKey MPC. - * We gain access to the provider created by tKey MPC after a successful login. - * This module returns a provider which will always get the current MPC Wallet provider from an ExternalStore and delegate all calls to it. - * - * @returns Custom Onboard MpcModule - */ -function MpcModule(chain: ChainInfo): WalletInit { - if (!hasFeature(chain, FEATURES.SOCIAL_LOGIN)) return () => null - - return () => { - return { - label: ONBOARD_MPC_MODULE_LABEL, - getIcon: async () => (await import('./icon')).default, - getInterface: async () => { - const { _getMPCCoreKitInstance } = await import('@/hooks/wallets/mpc/useMPC') - const { getSocialWalletService } = await import('@/hooks/wallets/mpc/useSocialWallet') - const { COREKIT_STATUS } = await import('@web3auth/mpc-core-kit') - const { open } = await import('./PasswordRecoveryModal') - - const getMPCProvider = () => _getMPCCoreKitInstance()?.provider - - const provider: EIP1193Provider = { - on: (event, listener) => { - const web3 = assertDefined(getMPCProvider()) - web3.on(event, listener) - }, - request: (request) => { - return new Promise(async (resolve, reject) => { - try { - /* - * We have to fallback to web3ReadOnly for eth_estimateGas because the provider by Web3Auth does not expose / implement it. - */ - if ('eth_estimateGas' === request.method) { - const web3ReadOnly = assertDefined(getWeb3ReadOnly()) - - web3ReadOnly - ?.send(request.method, request.params ?? []) - .then(resolve) - .catch(reject) - - return - } - - if ('eth_requestAccounts' === request.method) { - try { - // If the provider is defined we already have access to the accounts. - const web3 = assertDefined(getMPCProvider()) - web3.request({ method: 'eth_accounts' }).then(resolve).catch(reject) - } catch (e) { - // Otherwise try to log in the user - const socialWalletService = getSocialWalletService() - if (!socialWalletService) throw Error('Social Login not ready') - - const status = await socialWalletService.loginAndCreate() - - if (status === COREKIT_STATUS.REQUIRED_SHARE) { - open(() => { - getConnectedAccounts(getMPCProvider()).then(resolve).catch(reject) - }) - } else { - getConnectedAccounts(getMPCProvider()).then(resolve).catch(reject) - } - } - return - } - - if ('wallet_switchEthereumChain' === request.method) { - // The MPC provider always uses the current chain as chain. Nothing to do here. - resolve(null) - return - } - - const web3 = assertDefined(getMPCProvider()) - // Default: we call the inner provider - web3.request(request).then(resolve).catch(reject) - return - } catch (error) { - reject( - new ProviderRpcError({ - // TODO: This error code is usually for user rejection. But 4009 for instance has no effect with onboard - code: 4001, - message: 'Provider is unavailable', - }), - ) - } - }) - }, - removeListener: (event, listener) => { - const web3 = assertDefined(getMPCProvider()) - return web3.removeListener(event, listener) - }, - disconnect: () => { - _getMPCCoreKitInstance()?.logout() - }, - } - - return { - provider, - } - }, - } - } -} - -export default MpcModule diff --git a/src/services/mpc/SocialWalletService.ts b/src/services/mpc/SocialWalletService.ts deleted file mode 100644 index 12cad18cdb..0000000000 --- a/src/services/mpc/SocialWalletService.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { COREKIT_STATUS, type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' -import BN from 'bn.js' -import { SecurityQuestionRecovery } from '@/services/mpc/recovery/SecurityQuestionRecovery' -import { trackEvent } from '@/services/analytics' -import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' -import { DeviceShareRecovery } from '@/services/mpc/recovery/DeviceShareRecovery' -import { logError } from '../exceptions' -import ErrorCodes from '../exceptions/ErrorCodes' -import { asError } from '../exceptions/utils' -import { type ISocialWalletService } from './interfaces' -import { isSocialWalletOptions, SOCIAL_WALLET_OPTIONS } from './config' - -const ERROR_MSG_NO_NEW_ACCOUNTS = - 'Social Login is deprecated and will be removed on 01.05.2024. New accounts cannot be created.' - -/** - * Singleton Service for accessing the social login wallet - */ -class SocialWalletService implements ISocialWalletService { - private mpcCoreKit: Web3AuthMPCCoreKit - private onConnect: () => Promise = () => Promise.resolve() - - private deviceShareRecovery: DeviceShareRecovery - private securityQuestionRecovery: SecurityQuestionRecovery - - constructor(mpcCoreKit: Web3AuthMPCCoreKit) { - this.mpcCoreKit = mpcCoreKit - this.deviceShareRecovery = new DeviceShareRecovery(mpcCoreKit) - this.securityQuestionRecovery = new SecurityQuestionRecovery(mpcCoreKit) - } - - isMFAEnabled() { - const { shareDescriptions } = this.mpcCoreKit.getKeyDetails() - return !Object.values(shareDescriptions).some((value) => value[0]?.includes('hashedShare')) - } - - /** - * - * An Account is considered new if - * - It has no MFA setup - * - the hashedShare was created in the past 5 minutes - * - * @returns true if account is new, false otherwise - */ - isAccountNew() { - const { shareDescriptions } = this.mpcCoreKit.getKeyDetails() - const hashedShare = Object.values(shareDescriptions).find((value) => value[0]?.includes('hashedShare')) - - if (!hashedShare || hashedShare.length > 1) { - return false - } - - try { - const firstShare = hashedShare[0] - const data = JSON.parse(firstShare) - if ('dateAdded' in data) { - const dateAdded = Number(data.dateAdded) - const timeDiff = Date.now() - dateAdded - if (timeDiff < 1000 * 5 * 60) { - return true - } - } - } catch {} - return false - } - - isRecoveryPasswordSet() { - return this.securityQuestionRecovery.isEnabled() - } - - async enableMFA(oldPassword: string | undefined, newPassword: string): Promise { - try { - // 1. setup device factor with password recovery - await this.securityQuestionRecovery.upsertPassword(newPassword, oldPassword) - const securityQuestionFactor = await this.securityQuestionRecovery.recoverWithPassword(newPassword) - if (!securityQuestionFactor) { - throw Error('Problem setting up the new password') - } - - if (!this.isMFAEnabled()) { - trackEvent({ ...MPC_WALLET_EVENTS.ENABLE_MFA, label: 'password' }) - // 2. enable MFA in mpcCoreKit - await this.mpcCoreKit.enableMFA({}, false) - } - - await this.mpcCoreKit.commitChanges() - } catch (e) { - const error = asError(e) - logError(ErrorCodes._304, error.message) - throw error - } - } - - setOnConnect(onConnect: () => Promise) { - this.onConnect = onConnect - } - - getUserInfo() { - return this.mpcCoreKit.state.userInfo - } - - async loginAndCreate(): Promise { - const config = SOCIAL_WALLET_OPTIONS - const isConfigured = isSocialWalletOptions(config) - if (!isConfigured) { - throw new Error('The Social signer wallet is not configured correctly') - } - try { - await this.mpcCoreKit.loginWithOauth({ - aggregateVerifierIdentifier: config.web3AuthAggregateVerifierId, - subVerifierDetailsArray: [ - { - clientId: config.googleClientId, - typeOfLogin: 'google', - verifier: config.web3AuthSubverifierId, - }, - ], - aggregateVerifierType: 'single_id_verifier', - }) - - if (this.mpcCoreKit.status === COREKIT_STATUS.REQUIRED_SHARE) { - // Check if we have a device share stored - if (await this.deviceShareRecovery.isEnabled()) { - await this.deviceShareRecovery.recoverWithDeviceFactor() - } else { - // Check password recovery - if (this.securityQuestionRecovery.isEnabled()) { - trackEvent(MPC_WALLET_EVENTS.MANUAL_RECOVERY) - return this.mpcCoreKit.status - } - } - } - - await this.finalizeLogin() - return this.mpcCoreKit.status - } catch (err) { - const error = asError(err) - if (error.message === ERROR_MSG_NO_NEW_ACCOUNTS) { - logError(ErrorCodes._307, error) - throw err - } - logError(ErrorCodes._306, error) - throw new Error('Something went wrong. Please try to login again.') - } - } - - private async finalizeLogin() { - if (this.mpcCoreKit.status === COREKIT_STATUS.LOGGED_IN) { - if (this.isAccountNew()) { - throw new Error(ERROR_MSG_NO_NEW_ACCOUNTS) - } - await this.mpcCoreKit.commitChanges() - await this.mpcCoreKit.provider?.request({ method: 'eth_accounts', params: [] }) - await this.onConnect() - } - } - - async recoverAccountWithPassword(password: string, storeDeviceShare: boolean = false) { - if (this.securityQuestionRecovery.isEnabled()) { - const factorKeyString = await this.securityQuestionRecovery.recoverWithPassword(password) - const factorKey = new BN(factorKeyString, 'hex') - await this.mpcCoreKit.inputFactorKey(factorKey) - - if (storeDeviceShare) { - await this.deviceShareRecovery.createAndStoreDeviceFactor() - } - - await this.finalizeLogin() - } - - if (this.mpcCoreKit.status === COREKIT_STATUS.LOGGED_IN) { - trackEvent({ ...MPC_WALLET_EVENTS.RECOVERED_SOCIAL_SIGNER, label: 'password' }) - } - - return this.mpcCoreKit.status === COREKIT_STATUS.LOGGED_IN - } - - async exportSignerKey(password: string): Promise { - try { - if (this.securityQuestionRecovery.isEnabled()) { - // Only export PK if recovery works - await this.securityQuestionRecovery.recoverWithPassword(password) - } - const exportedPK = await this.mpcCoreKit?._UNSAFE_exportTssKey() - return exportedPK - } catch (err) { - throw new Error('Error exporting account. Make sure the password is correct.') - } - } - - async __deleteAccount() { - // This is a critical function that should only be used for testing purposes - // Resetting your account means clearing all the metadata associated with it from the metadata server - // The key details will be deleted from our server and you will not be able to recover your account - if (!this.mpcCoreKit?.metadataKey) { - throw new Error('MPC Core Kit is not initialized or the user is not logged in') - } - - // In web3auth an account is reset by overwriting the metadata with KEY_NOT_FOUND - await this.mpcCoreKit.tKey.storageLayer.setMetadata({ - privKey: new BN(this.mpcCoreKit.metadataKey, 'hex'), - input: { message: 'KEY_NOT_FOUND' }, - }) - } -} - -export default SocialWalletService diff --git a/src/services/mpc/__mocks__/SocialWalletService.ts b/src/services/mpc/__mocks__/SocialWalletService.ts deleted file mode 100644 index c8d074772f..0000000000 --- a/src/services/mpc/__mocks__/SocialWalletService.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { COREKIT_STATUS, type UserInfo } from '@web3auth/mpc-core-kit' -import { toBeHex } from 'ethers' -import { type ISocialWalletService } from '../interfaces' - -/** - * Manual mock for SocialWalletService - * - * By default it will log in the user after a 1 second timer. - * For password recovery it expects the password to be "Test1234!" - */ -class TestSocialWalletService implements ISocialWalletService { - private fakePassword = 'Test1234!' - private postLoginState = COREKIT_STATUS.LOGGED_IN - private _isMfaEnabled = false - private onConnect: () => Promise = () => Promise.resolve() - private userInfo: UserInfo = { - email: 'test@testermann.com', - name: 'Test Testermann', - profileImage: 'test.testermann.local/profile.png', - } as unknown as UserInfo - - setOnConnect(onConnect: () => Promise): void { - this.onConnect = onConnect - } - - getUserInfo(): UserInfo | undefined { - return this.userInfo - } - isMFAEnabled(): boolean { - return this._isMfaEnabled - } - enableMFA(oldPassword: string, newPassword: string): Promise { - this._isMfaEnabled = true - return Promise.resolve() - } - isRecoveryPasswordSet(): boolean { - throw new Error('Method not implemented.') - } - - /** - * Method for tests to set the expected login state after calling loginAndCreate() - */ - __setPostLoginState(state: COREKIT_STATUS) { - this.postLoginState = state - } - - __setUserInfo(userInfo: UserInfo) { - this.userInfo = userInfo - } - - async loginAndCreate(): Promise { - return new Promise((resolve) => { - this.onConnect().then(() => resolve(this.postLoginState)) - }) - } - - __deleteAccount(): void { - throw new Error('Method not implemented.') - } - async recoverAccountWithPassword(password: string, storeDeviceFactor: boolean): Promise { - if (this.fakePassword === password) { - await this.onConnect() - return true - } - - throw Error('Invalid Password') - } - - exportSignerKey(password: string): Promise { - return Promise.resolve(toBeHex('0x1', 20)) - } -} - -export default TestSocialWalletService diff --git a/src/services/mpc/__tests__/SocialWalletService.test.ts b/src/services/mpc/__tests__/SocialWalletService.test.ts deleted file mode 100644 index 8e124bcaa1..0000000000 --- a/src/services/mpc/__tests__/SocialWalletService.test.ts +++ /dev/null @@ -1,340 +0,0 @@ -import { act, waitFor } from '@/tests/test-utils' -import { - COREKIT_STATUS, - type UserInfo, - type OauthLoginParams, - type Web3AuthMPCCoreKit, - type TssSecurityQuestion, -} from '@web3auth/mpc-core-kit' -import * as mpcCoreKit from '@web3auth/mpc-core-kit' -import * as socialWalletOptions from '@/services/mpc/config' -import { ethers } from 'ethers' -import BN from 'bn.js' -import SocialWalletService from '../SocialWalletService' - -/** time until mock login resolves */ -const MOCK_LOGIN_TIME = 1000 - -/** - * Helper class for mocking MPC Core Kit login flow - */ -class MockMPCCoreKit { - status: COREKIT_STATUS = COREKIT_STATUS.INITIALIZED - state: { - userInfo: UserInfo | undefined - } = { - userInfo: undefined, - } - - private stateAfterLogin: COREKIT_STATUS - private userInfoAfterLogin: UserInfo | undefined - private expectedFactorKey: BN - private expectedKeyDetails: { shareDescriptions: Record } - /** - * - * @param stateAfterLogin State after loginWithOauth resolves - * @param userInfoAfterLogin User info to set in the state after loginWithOauth resolves - * @param expectedFactorKey For MFA login flow the expected factor key. If inputFactorKey gets called with the expected factor key the state switches to logged in - */ - constructor( - stateAfterLogin: COREKIT_STATUS, - userInfoAfterLogin: UserInfo, - expectedFactorKey: BN = new BN(-1), - expectedKeyDetails = { shareDescriptions: {} }, - ) { - this.stateAfterLogin = stateAfterLogin - this.userInfoAfterLogin = userInfoAfterLogin - this.expectedFactorKey = expectedFactorKey - this.expectedKeyDetails = expectedKeyDetails - } - - loginWithOauth(params: OauthLoginParams): Promise { - return new Promise((resolve) => { - // Resolve after 1 sec - setTimeout(() => { - this.status = this.stateAfterLogin - this.state.userInfo = this.userInfoAfterLogin - resolve() - }, MOCK_LOGIN_TIME) - }) - } - - inputFactorKey(factorKey: BN) { - if (factorKey.eq(this.expectedFactorKey)) { - this.status = COREKIT_STATUS.LOGGED_IN - return Promise.resolve() - } else { - Promise.reject() - } - } - - commitChanges = jest.fn().mockImplementation(() => Promise.resolve()) - - getUserInfo() { - return this.state.userInfo - } - - getKeyDetails() { - return this.expectedKeyDetails - } -} - -describe('useMPCWallet', () => { - beforeAll(() => { - jest.useFakeTimers() - }) - beforeEach(() => { - jest.resetAllMocks() - jest.spyOn(socialWalletOptions, 'isSocialWalletOptions').mockReturnValue(true) - }) - afterAll(() => { - jest.useRealTimers() - }) - - describe('triggerLogin', () => { - it('should handle successful log in for SFA account', async () => { - const mockOnConnect = jest.fn() - - const mockCoreKit = new MockMPCCoreKit( - COREKIT_STATUS.LOGGED_IN, - { - email: 'test@test.com', - name: 'Test', - } as unknown as UserInfo, - new BN(-1), - { - shareDescriptions: { - 1: [ - JSON.stringify({ - dateAdded: Date.now() - 30 * 24 * 60 * 60 * 60 * 1000, - module: 'hashedShare', - }), - ], - }, - }, - ) as unknown as Web3AuthMPCCoreKit - - const testService = new SocialWalletService(mockCoreKit) - testService.setOnConnect(mockOnConnect) - - let status: Promise - act(() => { - status = testService.loginAndCreate() - }) - - expect(mockOnConnect).not.toHaveBeenCalled() - - // Resolve mock login - act(() => { - jest.advanceTimersByTime(MOCK_LOGIN_TIME) - }) - - // We should be logged in and onboard should get connected - await waitFor(() => { - expect(status).resolves.toEqual(COREKIT_STATUS.LOGGED_IN) - expect(mockOnConnect).toHaveBeenCalled() - expect(mockCoreKit.commitChanges).toHaveBeenCalled() - }) - }) - - it('should not allow logins to newly created SFA accounts', async () => { - const mockOnConnect = jest.fn() - - const mockCoreKit = new MockMPCCoreKit( - COREKIT_STATUS.LOGGED_IN, - { - email: 'test@test.com', - name: 'Test', - } as unknown as UserInfo, - new BN(-1), - { - shareDescriptions: { - 1: [ - JSON.stringify({ - dateAdded: Date.now() - 5, - module: 'hashedShare', - }), - ], - }, - }, - ) as unknown as Web3AuthMPCCoreKit - - const testService = new SocialWalletService(mockCoreKit) - testService.setOnConnect(mockOnConnect) - - let status: Promise - act(() => { - status = testService.loginAndCreate() - }) - - expect(mockOnConnect).not.toHaveBeenCalled() - - // Resolve mock login - act(() => { - jest.advanceTimersByTime(MOCK_LOGIN_TIME) - }) - - // We should be logged in and onboard should get connected - await waitFor(() => { - expect(status).rejects.toEqual( - new Error('Social Login is deprecated and will be removed on 01.05.2024. New accounts cannot be created.'), - ) - expect(mockOnConnect).not.toHaveBeenCalled() - expect(mockCoreKit.commitChanges).not.toHaveBeenCalled() - }) - }) - - it('should handle successful log in for MFA account with device share', async () => { - const mockOnConnect = jest.fn() - - const mockDeviceFactor = ethers.Wallet.createRandom().privateKey.slice(2) - - const mockCoreKit = new MockMPCCoreKit( - COREKIT_STATUS.REQUIRED_SHARE, - { - email: 'test@test.com', - name: 'Test', - } as unknown as UserInfo, - new BN(mockDeviceFactor, 'hex'), - ) as unknown as Web3AuthMPCCoreKit - - jest.spyOn(mpcCoreKit, 'getWebBrowserFactor').mockReturnValue(Promise.resolve(mockDeviceFactor)) - - const testService = new SocialWalletService(mockCoreKit) - testService.setOnConnect(mockOnConnect) - - let status: Promise - act(() => { - status = testService.loginAndCreate() - }) - - // While the login resolves we are in Authenticating state - expect(mockOnConnect).not.toHaveBeenCalled() - - // Resolve mock login - act(() => { - jest.advanceTimersByTime(MOCK_LOGIN_TIME) - }) - - // We should be logged in and onboard should get connected - await waitFor(() => { - expect(status).resolves.toEqual(COREKIT_STATUS.LOGGED_IN) - expect(mockOnConnect).toHaveBeenCalled() - expect(mockCoreKit.commitChanges).toHaveBeenCalled() - }) - }) - - it('should require manual share for MFA account without device share', async () => { - const mockOnConnect = jest.fn() - const mockCoreKit = new MockMPCCoreKit(COREKIT_STATUS.REQUIRED_SHARE, { - email: 'test@test.com', - name: 'Test', - } as unknown as UserInfo) as unknown as Web3AuthMPCCoreKit - - jest.spyOn(mpcCoreKit, 'getWebBrowserFactor').mockReturnValue(Promise.resolve(undefined)) - jest.spyOn(mpcCoreKit, 'TssSecurityQuestion').mockReturnValue({ - getQuestion: () => 'SOME RANDOM QUESTION', - } as unknown as TssSecurityQuestion) - - const testService = new SocialWalletService(mockCoreKit) - testService.setOnConnect(mockOnConnect) - - let status: Promise - act(() => { - status = testService.loginAndCreate() - }) - - // Resolve mock login - act(() => { - jest.advanceTimersByTime(MOCK_LOGIN_TIME) - }) - - // A missing second factor should result in manual recovery state - await waitFor(() => { - expect(status).resolves.toEqual(COREKIT_STATUS.REQUIRED_SHARE) - expect(mockOnConnect).not.toHaveBeenCalled() - expect(mockCoreKit.commitChanges).not.toHaveBeenCalled() - }) - }) - }) - - describe('resetAccount', () => { - it('should reset an account by overwriting the metadata', async () => { - const mockSetMetadata = jest.fn() - const mockMPCCore = { - metadataKey: ethers.Wallet.createRandom().privateKey.slice(2), - state: { - userInfo: undefined, - }, - tKey: { - storageLayer: { - setMetadata: mockSetMetadata, - }, - }, - } - - const testService = new SocialWalletService(mockMPCCore as unknown as Web3AuthMPCCoreKit) - - await testService.__deleteAccount() - - expect(mockSetMetadata).toHaveBeenCalledWith({ - privKey: new BN(mockMPCCore.metadataKey, 'hex'), - input: { message: 'KEY_NOT_FOUND' }, - }) - }) - }) - - describe('recoverFactorWithPassword', () => { - it('should not recover if wrong password is entered', () => { - const mockOnConnect = jest.fn() - const mockMPCCore = { - state: { - userInfo: undefined, - }, - commitChanges: jest.fn(), - } as unknown as Web3AuthMPCCoreKit - - jest.spyOn(mpcCoreKit, 'TssSecurityQuestion').mockReturnValue({ - getQuestion: () => 'SOME RANDOM QUESTION', - recoverFactor: () => { - throw new Error('Invalid answer') - }, - } as unknown as TssSecurityQuestion) - - const testService = new SocialWalletService(mockMPCCore as unknown as Web3AuthMPCCoreKit) - testService.setOnConnect(mockOnConnect) - - expect(testService.recoverAccountWithPassword('test', false)).rejects.toEqual(new Error('Invalid answer')) - expect(mockOnConnect).not.toHaveBeenCalled() - expect(mockMPCCore.commitChanges).not.toHaveBeenCalled() - }) - - it('should input recovered factor if correct password is entered', async () => { - const mockOnConnect = jest.fn() - const mockSecurityQuestionFactor = ethers.Wallet.createRandom().privateKey.slice(2) - - const mockMPCCore = new MockMPCCoreKit( - COREKIT_STATUS.REQUIRED_SHARE, - { - email: 'test@test.com', - name: 'Test', - } as unknown as UserInfo, - new BN(mockSecurityQuestionFactor, 'hex'), - ) as unknown as Web3AuthMPCCoreKit - - jest.spyOn(mpcCoreKit, 'TssSecurityQuestion').mockReturnValue({ - getQuestion: () => 'SOME RANDOM QUESTION', - recoverFactor: () => Promise.resolve(mockSecurityQuestionFactor), - } as unknown as TssSecurityQuestion) - - const testService = new SocialWalletService(mockMPCCore as unknown as Web3AuthMPCCoreKit) - testService.setOnConnect(mockOnConnect) - - act(() => testService.recoverAccountWithPassword('test', false)) - - await waitFor(() => { - expect(mockOnConnect).toHaveBeenCalled() - }) - }) - }) -}) diff --git a/src/services/mpc/__tests__/module.test.ts b/src/services/mpc/__tests__/module.test.ts deleted file mode 100644 index 548576de78..0000000000 --- a/src/services/mpc/__tests__/module.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { chainBuilder } from '@/tests/builders/chains' -import { FEATURES } from '@/utils/chains' -import MpcModule, { ONBOARD_MPC_MODULE_LABEL } from '../SocialLoginModule' -import { type WalletModule } from '@web3-onboard/common' - -import * as web3 from '@/hooks/wallets/web3' -import * as useMPC from '@/hooks/wallets/mpc/useMPC' -import { toBeHex } from 'ethers' - -const mockChain = chainBuilder() - // @ts-expect-error - we are using a local FEATURES enum - .with({ features: [FEATURES.SOCIAL_LOGIN] }) - .build() - -describe('MPC Onboard module', () => { - it('should return correct metadata', async () => { - const mpcModule = MpcModule(mockChain)({ - device: { - browser: { - name: 'Firefox', - version: '1.0', - }, - os: { - name: 'macOS', - version: '1.0', - }, - type: 'desktop', - }, - }) - expect(Array.isArray(mpcModule)).toBeFalsy() - const walletModule = mpcModule as WalletModule - - expect(walletModule.label).toBe(ONBOARD_MPC_MODULE_LABEL) - expect(walletModule.getIcon()).toBeDefined() - const walletInterface = await walletModule.getInterface({} as any) - expect(walletInterface.instance).toBeUndefined() - expect(walletInterface.provider).toBeDefined() - }) - - it('should call web3readonly for eth_estimateGas', async () => { - const mockReadOnlySend = jest.fn().mockImplementation(() => Promise.resolve('0x5')) - jest.spyOn(web3, 'getWeb3ReadOnly').mockReturnValue({ - send: mockReadOnlySend, - } as any) - - jest.spyOn(useMPC, '_getMPCCoreKitInstance').mockImplementation(() => { - return { - provider: {}, - } as any - }) - - const mpcModule = MpcModule(mockChain)({ - device: { - browser: { - name: 'Firefox', - version: '1.0', - }, - os: { - name: 'macOS', - version: '1.0', - }, - type: 'desktop', - }, - }) - const walletModule = mpcModule as WalletModule - const walletInterface = await walletModule.getInterface({} as any) - - await walletInterface.provider.request({ - method: 'eth_estimateGas', - params: [ - { - to: toBeHex('0x123', 20), - value: '0', - data: '0x', - }, - ], - }) - - expect(mockReadOnlySend).toHaveBeenCalledWith('eth_estimateGas', [ - { - to: toBeHex('0x123', 20), - value: '0', - data: '0x', - }, - ]) - }) - - it('should call eth_accounts when eth_requestAccounts gets called', async () => { - const mockReadOnlySend = jest.fn() - const mockMPCProviderRequest = jest.fn().mockImplementation(() => Promise.resolve(toBeHex('0x456', 20))) - jest.spyOn(web3, 'getWeb3ReadOnly').mockReturnValue({ - send: mockReadOnlySend, - } as any) - - jest.spyOn(useMPC, '_getMPCCoreKitInstance').mockImplementation(() => { - return { - provider: { - request: mockMPCProviderRequest, - }, - } as any - }) - - const mpcModule = MpcModule(mockChain)({ - device: { - browser: { - name: 'Firefox', - version: '1.0', - }, - os: { - name: 'macOS', - version: '1.0', - }, - type: 'desktop', - }, - }) - const walletModule = mpcModule as WalletModule - const walletInterface = await walletModule.getInterface({} as any) - - await walletInterface.provider.request({ - method: 'eth_requestAccounts', - }) - - expect(mockMPCProviderRequest).toHaveBeenCalledWith({ method: 'eth_accounts' }) - }) -}) diff --git a/src/services/mpc/config.ts b/src/services/mpc/config.ts deleted file mode 100644 index db2ce429f4..0000000000 --- a/src/services/mpc/config.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { IS_PRODUCTION } from '@/config/constants' - -enum SocialWalletOptionsKeys { - web3AuthClientId = 'web3AuthClientId', - web3AuthAggregateVerifierId = 'web3AuthAggregateVerifierId', - web3AuthSubverifierId = 'web3AuthSubverifierId', - googleClientId = 'googleClientId', -} - -export type SocialWalletOptions = { - [SocialWalletOptionsKeys.web3AuthClientId]: string - [SocialWalletOptionsKeys.web3AuthAggregateVerifierId]: string - [SocialWalletOptionsKeys.web3AuthSubverifierId]: string - [SocialWalletOptionsKeys.googleClientId]: string -} - -export const isSocialWalletOptions = (options: unknown): options is SocialWalletOptions => { - if (typeof options !== 'object' || options === null) { - return false - } - - const requiredKeys = Object.values(SocialWalletOptionsKeys) - const hasRequiredKeys = requiredKeys.every((key) => key in options) - const hasValues = Object.values(options).every(Boolean) - - return hasRequiredKeys && hasValues -} - -/** env variables */ -export const SOCIAL_WALLET_OPTIONS: any = (() => { - const SOCIAL_WALLET_OPTIONS_PRODUCTION = process.env.NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_PRODUCTION || '{}' - const SOCIAL_WALLET_OPTIONS_STAGING = process.env.NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_STAGING || '{}' - - try { - return JSON.parse(IS_PRODUCTION ? SOCIAL_WALLET_OPTIONS_PRODUCTION : SOCIAL_WALLET_OPTIONS_STAGING) - } catch (error) { - console.error('Error parsing SOCIAL_WALLET_OPTIONS', error) - return {} - } -})() diff --git a/src/services/mpc/icon.ts b/src/services/mpc/icon.ts deleted file mode 100644 index c306bf5353..0000000000 --- a/src/services/mpc/icon.ts +++ /dev/null @@ -1,12 +0,0 @@ -const pairingIcon = ` - - - - - - - - -` - -export default pairingIcon diff --git a/src/services/mpc/interfaces.ts b/src/services/mpc/interfaces.ts deleted file mode 100644 index b76ace0eec..0000000000 --- a/src/services/mpc/interfaces.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { COREKIT_STATUS, UserInfo } from '@web3auth/mpc-core-kit' - -export interface ISocialWalletService { - /** - * Opens a popup with the Google login and creates / restores the mpc wallet. - * - * @returns the follow up status of the mpcCoreKit. - */ - loginAndCreate(): Promise - - /** - * Deletes the currently logged in account. - * This should only be used in dev environments and never in prod! - */ - __deleteAccount(): void - - /** - * Tries to recover a social signer through the Security Questions module - * - * @param password entered recovery password - * @param storeDeviceFactor if true a device factor will be added after successful recovery - */ - recoverAccountWithPassword(password: string, storeDeviceFactor: boolean): Promise - - /** - * Exports the key of the signer - * - * @param password recovery password - */ - exportSignerKey(password: string): Promise - - /** - * Returns true if MFA is enabled - */ - isMFAEnabled(): boolean - - /** - * Enables MFA and stores a device share with 2 factors: - * - one factor encrypted with the password - * - one factor encrypted with a key in the local storage of the browser - * - * @param oldPassword required if MFA is already enabled - * @param newPassword new password to set - */ - enableMFA(oldPassword: string | undefined, newPassword: string): Promise - - isRecoveryPasswordSet(): boolean - - getUserInfo(): UserInfo | undefined - - setOnConnect(onConnect: () => Promise): void -} diff --git a/src/services/mpc/recovery/DeviceShareRecovery.ts b/src/services/mpc/recovery/DeviceShareRecovery.ts deleted file mode 100644 index 7806f91b92..0000000000 --- a/src/services/mpc/recovery/DeviceShareRecovery.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { - BrowserStorage, - getWebBrowserFactor, - storeWebBrowserFactor, - TssShareType, - type Web3AuthMPCCoreKit, -} from '@web3auth/mpc-core-kit' -import BN from 'bn.js' -import { getPubKeyPoint } from '@tkey-mpc/common-types' - -export class DeviceShareRecovery { - private mpcCoreKit: Web3AuthMPCCoreKit - - constructor(mpcCoreKit: Web3AuthMPCCoreKit) { - this.mpcCoreKit = mpcCoreKit - } - - async isEnabled() { - return !!(await getWebBrowserFactor(this.mpcCoreKit)) - } - - async createAndStoreDeviceFactor() { - const userAgent = navigator.userAgent - - const deviceFactorKey = new BN( - await this.mpcCoreKit.createFactor({ shareType: TssShareType.DEVICE, additionalMetadata: { userAgent } }), - 'hex', - ) - await storeWebBrowserFactor(deviceFactorKey, this.mpcCoreKit) - } - - async recoverWithDeviceFactor() { - // Recover from device factor - const deviceFactor = await getWebBrowserFactor(this.mpcCoreKit) - if (!deviceFactor) { - throw Error('Cannot recover from device factor. No device factor found') - } - const deviceFactorKey = new BN(deviceFactor, 'hex') - await this.mpcCoreKit.inputFactorKey(deviceFactorKey) - } - - async removeDeviceFactor() { - const deviceFactor = await getWebBrowserFactor(this.mpcCoreKit) - if (!deviceFactor) { - // No device factor exists. Nothing to do - return - } - // Delete factor - const key = new BN(deviceFactor, 'hex') - const pubKey = getPubKeyPoint(key) - await this.mpcCoreKit.deleteFactor(pubKey) - - // Remove from local storage - const metadata = this.mpcCoreKit.tKey.getMetadata() - const tkeyPubX = metadata.pubKey.x.toString(16, 64) - const currentStorage = BrowserStorage.getInstance('mpc_corekit_store') - currentStorage.remove(tkeyPubX) - } -} diff --git a/src/services/mpc/recovery/SecurityQuestionRecovery.ts b/src/services/mpc/recovery/SecurityQuestionRecovery.ts deleted file mode 100644 index 0d707cb29f..0000000000 --- a/src/services/mpc/recovery/SecurityQuestionRecovery.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { TssSecurityQuestion, TssShareType, type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' - -export class SecurityQuestionRecovery { - /** This is only used internally in the metadata store of tKey. Not in the UI */ - private static readonly DEFAULT_SECURITY_QUESTION = 'ENTER PASSWORD' - - private mpcCoreKit: Web3AuthMPCCoreKit - private securityQuestions = new TssSecurityQuestion() - - constructor(mpcCoreKit: Web3AuthMPCCoreKit) { - this.mpcCoreKit = mpcCoreKit - } - - isEnabled(): boolean { - try { - const question = this.securityQuestions.getQuestion(this.mpcCoreKit) - return !!question - } catch (error) { - // It errors out if recovery is not setup currently - return false - } - } - - async upsertPassword(newPassword: string, oldPassword?: string) { - if (this.isEnabled()) { - if (!oldPassword) { - throw Error('To change the password you need to provide the old password') - } - await this.securityQuestions.changeSecurityQuestion({ - answer: oldPassword, - mpcCoreKit: this.mpcCoreKit, - newAnswer: newPassword, - newQuestion: SecurityQuestionRecovery.DEFAULT_SECURITY_QUESTION, - }) - } else { - await this.securityQuestions.setSecurityQuestion({ - question: SecurityQuestionRecovery.DEFAULT_SECURITY_QUESTION, - answer: newPassword, - mpcCoreKit: this.mpcCoreKit, - shareType: TssShareType.DEVICE, - }) - } - } - - async recoverWithPassword(password: string) { - return this.securityQuestions.recoverFactor(this.mpcCoreKit, password) - } -} diff --git a/src/services/ofac/blockedAddressList.json b/src/services/ofac/blockedAddressList.json index e8df17f077..2a75c8a50b 100644 --- a/src/services/ofac/blockedAddressList.json +++ b/src/services/ofac/blockedAddressList.json @@ -149,5 +149,10 @@ "0x530a64c0ce595026a4a556b703644228179e2d57", "0xfac583c0cf07ea434052c49115a4682172ab6b4f", "0x961c5be54a2ffc17cf4cb021d863c42dacd47fc1", - "0x983a81ca6fb1e441266d2fbcb7d8e530ac2e05a2" + "0x983a81ca6fb1e441266d2fbcb7d8e530ac2e05a2", + "0xf3701f445b6bdafedbca97d1e477357839e4120d", + "0xe950dc316b836e4eefb8308bf32bf7c72a1358ff", + "0x21b8d56bda776bbe68655a16895afd96f5534fed", + "0x175d44451403edf28469df03a9280c1197adb92c", + "0x19f8f2b0915daa12a3f5c9cf01df9e24d53794f7" ] diff --git a/src/services/private-key-module/PkModulePopup.tsx b/src/services/private-key-module/PkModulePopup.tsx new file mode 100644 index 0000000000..5bdbbb2452 --- /dev/null +++ b/src/services/private-key-module/PkModulePopup.tsx @@ -0,0 +1,51 @@ +import type { FormEvent } from 'react' +import { Button, TextField, Typography, Box } from '@mui/material' +import ModalDialog from '@/components/common/ModalDialog' +import pkStore from './pk-popup-store' +const { useStore, setStore } = pkStore + +const PkModulePopup = () => { + const { isOpen, privateKey } = useStore() ?? { isOpen: false, privateKey: '' } + + const onClose = () => { + setStore({ isOpen: false, privateKey }) + } + + const onSubmit = (e: FormEvent) => { + e.preventDefault() + const privateKey = (e.target as unknown as { 'private-key': HTMLInputElement })['private-key'].value + + setStore({ + isOpen: false, + privateKey, + }) + } + + return ( + + + + Enter your signer private key. The key will be saved for the duration of this browser session. + + + + + + + + + + ) +} + +export default PkModulePopup diff --git a/src/services/private-key-module/icon.ts b/src/services/private-key-module/icon.ts new file mode 100644 index 0000000000..69c435dfb8 --- /dev/null +++ b/src/services/private-key-module/icon.ts @@ -0,0 +1,5 @@ +const icon = ` + +` + +export default icon diff --git a/src/services/private-key-module/index.ts b/src/services/private-key-module/index.ts new file mode 100644 index 0000000000..355bd2ded9 --- /dev/null +++ b/src/services/private-key-module/index.ts @@ -0,0 +1,130 @@ +import { JsonRpcProvider, Wallet } from 'ethers' +import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { type WalletInit, createEIP1193Provider } from '@web3-onboard/common' +import { getRpcServiceUrl } from '@/hooks/wallets/web3' +import pkPopupStore from './pk-popup-store' +import { numberToHex } from '@/utils/hex' + +export const PRIVATE_KEY_MODULE_LABEL = 'Private key' + +async function getPrivateKey() { + const savedKey = pkPopupStore.getStore()?.privateKey + if (savedKey) return savedKey + + pkPopupStore.setStore({ + isOpen: true, + privateKey: '', + }) + + return new Promise((resolve) => { + const unsubscribe = pkPopupStore.subscribe(() => { + unsubscribe() + resolve(pkPopupStore.getStore()?.privateKey ?? '') + }) + }) +} + +let currentChainId = '' +let currentRpcUri = '' + +const PrivateKeyModule = (chainId: ChainInfo['chainId'], rpcUri: ChainInfo['rpcUri']): WalletInit => { + currentChainId = chainId + currentRpcUri = getRpcServiceUrl(rpcUri) + + return () => { + return { + label: PRIVATE_KEY_MODULE_LABEL, + getIcon: async () => (await import('./icon')).default, + getInterface: async () => { + const privateKey = await getPrivateKey() + if (!privateKey) { + throw new Error('You rejected the connection') + } + + let provider: JsonRpcProvider + let wallet: Wallet + const chainChangedListeners = new Set<(chainId: string) => void>() + + const updateProvider = () => { + console.log('[Private key signer] Updating provider to chainId', currentChainId, currentRpcUri) + provider?.destroy() + provider = new JsonRpcProvider(currentRpcUri, Number(currentChainId), { staticNetwork: true }) + wallet = new Wallet(privateKey, provider) + + setTimeout(() => { + chainChangedListeners.forEach((listener) => listener(numberToHex(Number(currentChainId)))) + }, 100) + } + + updateProvider() + + return { + provider: createEIP1193Provider( + { + on: (event: string, listener: (...args: any[]) => void) => { + if (event === 'accountsChanged') { + return + } else if (event === 'chainChanged') { + chainChangedListeners.add(listener) + } else { + provider.on(event, listener) + } + }, + + request: async (request: { method: string; params: any[] }) => { + return provider.send(request.method, request.params) + }, + + disconnect: () => { + pkPopupStore.setStore({ + isOpen: false, + privateKey: '', + }) + }, + }, + { + eth_chainId: async () => currentChainId, + + // @ts-ignore + eth_getCode: async ({ params }) => provider.getCode(params[0], params[1]), + + eth_accounts: async () => [wallet.address], + eth_requestAccounts: async () => [wallet.address], + + eth_call: async ({ params }: { params: any }) => wallet.call(params[0]), + + eth_sendTransaction: async ({ params }) => { + const tx = await wallet.sendTransaction(params[0] as any) + return tx.hash // return transaction hash + }, + + personal_sign: async ({ params }) => { + const signedMessage = wallet.signingKey.sign(params[0]) + return signedMessage.serialized + }, + + eth_signTypedData: async ({ params }) => { + const [, json] = params + const typedData = JSON.parse(json) + return await wallet.signTypedData( + typedData.domain, + { [typedData.primaryType]: typedData.types[typedData.primaryType] }, + typedData.message, + ) + }, + + // @ts-ignore + wallet_switchEthereumChain: async ({ params }) => { + console.log('[Private key signer] Switching chain', params) + updateProvider() + }, + }, + ), + } + }, + platforms: ['desktop'], + } + } +} + +export default PrivateKeyModule diff --git a/src/services/private-key-module/pk-popup-store.ts b/src/services/private-key-module/pk-popup-store.ts new file mode 100644 index 0000000000..5779540b50 --- /dev/null +++ b/src/services/private-key-module/pk-popup-store.ts @@ -0,0 +1,23 @@ +import ExternalStore from '@/services/ExternalStore' +import { sessionItem } from '@/services/local-storage/session' + +type PkModulePopupStore = { + isOpen: boolean + privateKey: string +} + +const defaultValue = { + isOpen: false, + privateKey: '', +} + +const STORAGE_KEY = 'privateKeyModulePK' +const pkStorage = sessionItem(STORAGE_KEY) + +const popupStore = new ExternalStore(pkStorage.get() || defaultValue) + +popupStore.subscribe(() => { + pkStorage.set(popupStore.getStore() || defaultValue) +}) + +export default popupStore diff --git a/src/services/safe-messages/__tests__/safeMsgSender.test.ts b/src/services/safe-messages/__tests__/safeMsgSender.test.ts index 744bd3e762..57e3e273ac 100644 --- a/src/services/safe-messages/__tests__/safeMsgSender.test.ts +++ b/src/services/safe-messages/__tests__/safeMsgSender.test.ts @@ -1,3 +1,4 @@ +import { MockEip1193Provider } from '@/tests/mocks/providers' import * as gateway from '@safe-global/safe-gateway-typescript-sdk' import type { JsonRpcSigner } from 'ethers' import { zeroPadBytes } from 'ethers' @@ -7,7 +8,6 @@ import * as utils from '@/utils/safe-messages' import * as events from '@/services/safe-messages/safeMsgEvents' import * as sdk from '@/services/tx/tx-sender/sdk' import { zeroPadValue } from 'ethers' -import type { EIP1193Provider, OnboardAPI, WalletState, AppState } from '@web3-onboard/core' jest.mock('@safe-global/safe-gateway-typescript-sdk', () => ({ ...jest.requireActual('@safe-global/safe-gateway-typescript-sdk'), @@ -15,51 +15,6 @@ jest.mock('@safe-global/safe-gateway-typescript-sdk', () => ({ confirmSafeMessage: jest.fn(), })) -let mockProvider = { - request: jest.fn, -} as unknown as EIP1193Provider - -const mockOnboardState = { - chains: [], - walletModules: [], - wallets: [ - { - label: 'Wallet 1', - icon: '', - provider: mockProvider, - chains: [{ id: '0x5' }], - accounts: [ - { - address: '0x1234567890123456789012345678901234567890', - ens: null, - balance: null, - }, - ], - }, - ] as WalletState[], - accountCenter: { - enabled: true, - }, -} as unknown as AppState - -const mockOnboard = { - connectWallet: jest.fn(), - disconnectWallet: jest.fn(), - setChain: jest.fn(), - state: { - select: (key: keyof AppState) => ({ - subscribe: (next: any) => { - next(mockOnboardState[key]) - - return { - unsubscribe: jest.fn(), - } - }, - }), - get: () => mockOnboardState, - }, -} as unknown as OnboardAPI - const mockValidSignature = `${zeroPadBytes('0x0456', 64)}1c` const mockSignatureWithInvalidV = `${zeroPadBytes('0x0456', 64)}01` describe('safeMsgSender', () => { @@ -90,7 +45,7 @@ describe('safeMsgSender', () => { const message = 'Hello world' const safeAppId = 1 - await dispatchSafeMsgProposal({ onboard: mockOnboard, safe, message, safeAppId }) + await dispatchSafeMsgProposal({ provider: MockEip1193Provider, safe, message, safeAppId }) expect(proposeSafeMessageSpy).toHaveBeenCalledWith('5', zeroPadValue('0x0789', 20), { message, @@ -135,7 +90,7 @@ describe('safeMsgSender', () => { } const safeAppId = 1 - await dispatchSafeMsgProposal({ onboard: mockOnboard, safe, message, safeAppId }) + await dispatchSafeMsgProposal({ provider: MockEip1193Provider, safe, message, safeAppId }) // Normalize message manually message.types['EIP712Domain'] = [ @@ -172,7 +127,7 @@ describe('safeMsgSender', () => { const message = 'Hello world' const safeAppId = 1 - await dispatchSafeMsgProposal({ onboard: mockOnboard, safe, message, safeAppId }) + await dispatchSafeMsgProposal({ provider: MockEip1193Provider, safe, message, safeAppId }) expect(proposeSafeMessageSpy).toHaveBeenCalledWith('5', zeroPadValue('0x0789', 20), { message, @@ -203,7 +158,7 @@ describe('safeMsgSender', () => { const safeAppId = 1 try { - await dispatchSafeMsgProposal({ onboard: mockOnboard, safe, message, safeAppId }) + await dispatchSafeMsgProposal({ provider: MockEip1193Provider, safe, message, safeAppId }) } catch (e) { expect((e as Error).message).toBe('Example error') @@ -237,7 +192,7 @@ describe('safeMsgSender', () => { } as unknown as gateway.SafeInfo const message = 'Hello world' - await dispatchSafeMsgConfirmation({ onboard: mockOnboard, safe, message }) + await dispatchSafeMsgConfirmation({ provider: MockEip1193Provider, safe, message }) expect(confirmSafeMessageSpy).toHaveBeenCalledWith('5', '0x0123', { signature: mockValidSignature, @@ -264,7 +219,7 @@ describe('safeMsgSender', () => { const message = 'Hello world' try { - await dispatchSafeMsgConfirmation({ onboard: mockOnboard, safe, message }) + await dispatchSafeMsgConfirmation({ provider: MockEip1193Provider, safe, message }) } catch (e) { expect((e as Error).message).toBe('Example error') diff --git a/src/services/safe-messages/safeMsgSender.ts b/src/services/safe-messages/safeMsgSender.ts index 8e00d1b9d9..5cedd7a468 100644 --- a/src/services/safe-messages/safeMsgSender.ts +++ b/src/services/safe-messages/safeMsgSender.ts @@ -1,6 +1,6 @@ import { proposeSafeMessage, confirmSafeMessage } from '@safe-global/safe-gateway-typescript-sdk' import type { SafeInfo, SafeMessage } from '@safe-global/safe-gateway-typescript-sdk' -import type { OnboardAPI } from '@web3-onboard/core' +import type { Eip1193Provider } from 'ethers' import { safeMsgDispatch, SafeMsgEvent } from './safeMsgEvents' import { generateSafeMessageHash, isEIP712TypedData, tryOffChainMsgSigning } from '@/utils/safe-messages' @@ -9,12 +9,12 @@ import { getAssertedChainSigner } from '@/services/tx/tx-sender/sdk' import { asError } from '../exceptions/utils' export const dispatchSafeMsgProposal = async ({ - onboard, + provider, safe, message, safeAppId, }: { - onboard: OnboardAPI + provider: Eip1193Provider safe: SafeInfo message: SafeMessage['message'] safeAppId?: number @@ -22,7 +22,7 @@ export const dispatchSafeMsgProposal = async ({ const messageHash = generateSafeMessageHash(safe, message) try { - const signer = await getAssertedChainSigner(onboard, safe.chainId) + const signer = await getAssertedChainSigner(provider) const signature = await tryOffChainMsgSigning(signer, safe, message) let normalizedMessage = message @@ -50,18 +50,18 @@ export const dispatchSafeMsgProposal = async ({ } export const dispatchSafeMsgConfirmation = async ({ - onboard, + provider, safe, message, }: { - onboard: OnboardAPI + provider: Eip1193Provider safe: SafeInfo message: SafeMessage['message'] }): Promise => { const messageHash = generateSafeMessageHash(safe, message) try { - const signer = await getAssertedChainSigner(onboard, safe.chainId) + const signer = await getAssertedChainSigner(provider) const signature = await tryOffChainMsgSigning(signer, safe, message) await confirmSafeMessage(safe.chainId, messageHash, { diff --git a/src/services/safe-wallet-provider/index.test.ts b/src/services/safe-wallet-provider/index.test.ts index ef6f52ed0e..3252f21e23 100644 --- a/src/services/safe-wallet-provider/index.test.ts +++ b/src/services/safe-wallet-provider/index.test.ts @@ -1,8 +1,10 @@ // Unit tests for the SafeWalletProvider class +import { faker } from '@faker-js/faker' import { SafeWalletProvider } from '.' +import { ERC20__factory } from '@/types/contracts' const safe = { - safeAddress: '0x123', + safeAddress: faker.finance.ethereumAddress(), chainId: 1, } @@ -69,7 +71,7 @@ describe('SafeWalletProvider', () => { expect(result).toEqual({ id: 1, jsonrpc: '2.0', - result: ['0x123'], + result: [safe.safeAddress], }) }) }) @@ -136,7 +138,7 @@ describe('SafeWalletProvider', () => { const result = await safeWalletProvider.request( 1, - { method: 'personal_sign', params: ['message', '0x123'] } as any, + { method: 'personal_sign', params: ['message', safe.safeAddress] } as any, {} as any, ) @@ -157,7 +159,7 @@ describe('SafeWalletProvider', () => { const result = await safeWalletProvider.request( 1, - { method: 'eth_sign', params: ['0x123', '0x123'] } as any, + { method: 'eth_sign', params: [safe.safeAddress, '0x345'] } as any, {} as any, ) @@ -214,7 +216,7 @@ describe('SafeWalletProvider', () => { const result = await safeWalletProvider.request( 1, - { method: 'personal_sign', params: ['0x123', '0x123'] } as any, + { method: 'eth_sign', params: [safe.safeAddress, '0x123'] } as any, {} as any, ) @@ -238,7 +240,7 @@ describe('SafeWalletProvider', () => { { method, params: [ - '0x123', + safe.safeAddress, { domain: { chainId: 1, @@ -290,7 +292,7 @@ describe('SafeWalletProvider', () => { { method, params: [ - '0x123', + safe.safeAddress, { domain: { chainId: 1, @@ -320,16 +322,23 @@ describe('SafeWalletProvider', () => { const sdk = { send: jest.fn().mockResolvedValue({ safeTxHash: '0x456' }), } + const toAddress = faker.finance.ethereumAddress() const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) const result = await safeWalletProvider.request( 1, - { method: 'eth_sendTransaction', params: [{ from: '0x123', to: '0x123', value: '0x123', gas: 1000 }] } as any, + { + method: 'eth_sendTransaction', + params: [{ from: safe.safeAddress, to: toAddress, value: '0x01', gas: 1000 }], + } as any, appInfo, ) expect(sdk.send).toHaveBeenCalledWith( - { txs: [{ from: '0x123', to: '0x123', value: '0x123', gas: 1000, data: '0x' }], params: { safeTxGas: 1000 } }, + { + txs: [{ from: safe.safeAddress, to: toAddress, value: '0x01', gas: 1000, data: '0x' }], + params: { safeTxGas: 1000 }, + }, appInfo, ) @@ -426,10 +435,15 @@ describe('SafeWalletProvider', () => { } const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + const toAddress = faker.finance.ethereumAddress() + // Send the transaction await safeWalletProvider.request( 1, - { method: 'eth_sendTransaction', params: [{ from: '0x123', to: '0x123', value: '0x123', gas: 1000 }] } as any, + { + method: 'eth_sendTransaction', + params: [{ from: safe.safeAddress, to: toAddress, value: '0x01', gas: 1000 }], + } as any, appInfo, ) @@ -445,15 +459,15 @@ describe('SafeWalletProvider', () => { result: { blockHash: null, blockNumber: null, - from: '0x123', + from: safe.safeAddress, gas: 0, gasPrice: '0x00', hash: '0x777', input: '0x', nonce: 0, - to: '0x123', + to: toAddress, transactionIndex: null, - value: '0x123', + value: '0x01', }, }) }) @@ -495,7 +509,7 @@ describe('SafeWalletProvider', () => { }) describe('EIP-5792', () => { - describe('wallet_sendFunctionCallBundle', () => { + describe('wallet_sendCalls', () => { it('should send a bundle', async () => { const sdk = { send: jest.fn(), @@ -505,15 +519,16 @@ describe('SafeWalletProvider', () => { const params = [ { chainId: 1, - from: '0x1234', + version: '1.0', + from: faker.finance.ethereumAddress(), calls: [ - { gas: 1000, data: '0x123', to: '0x123', value: '0x123' }, - { gas: 1000, data: '0x456', to: '0x789', value: '0x1' }, + { data: '0x123', to: faker.finance.ethereumAddress(), value: '0x123' }, + { data: '0x456', to: faker.finance.ethereumAddress(), value: '0x1' }, ], }, ] - await safeWalletProvider.request(1, { method: 'wallet_sendFunctionCallBundle', params } as any, appInfo) + await safeWalletProvider.request(1, { method: 'wallet_sendCalls', params } as any, appInfo) expect(sdk.send).toHaveBeenCalledWith( { @@ -529,9 +544,79 @@ describe('SafeWalletProvider', () => { }, ) }) + + it('test contract deployment calls and calls without data / value', async () => { + const fakeCreateCallLib = faker.finance.ethereumAddress() + const sdk = { + send: jest.fn(), + getCreateCallTransaction: jest.fn().mockImplementation((data: string) => { + return { + to: fakeCreateCallLib, + data, + value: '0', + } + }), + } + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + const transferReceiver = faker.finance.ethereumAddress() + const erc20Address = faker.finance.ethereumAddress() + const erc20TransferData = ERC20__factory.createInterface().encodeFunctionData('transfer', [ + transferReceiver, + '100', + ]) + const nativeTransferTo = faker.finance.ethereumAddress() + + const params = [ + { + chainId: 1, + version: '1.0', + from: safe.safeAddress, + calls: [ + { data: '0x1234' }, + { data: '0x', to: nativeTransferTo, value: '0x1' }, + { + to: erc20Address, + data: erc20TransferData, + }, + ], + }, + ] + + await safeWalletProvider.request(1, { method: 'wallet_sendCalls', params } as any, appInfo) + + expect(sdk.send).toHaveBeenCalledWith( + { + txs: [ + { + to: fakeCreateCallLib, + data: '0x1234', + value: '0', + }, + { + to: nativeTransferTo, + data: '0x', + value: '0x1', + }, + { + to: erc20Address, + data: erc20TransferData, + value: '0', + }, + ], + params: { safeTxGas: 0 }, + }, + { + description: 'test', + iconUrl: 'test', + id: 1, + name: 'test', + url: 'test', + }, + ) + }) }) - describe('wallet_getBundleStatus', () => { + describe('wallet_getCallsStatus', () => { it('should look up a tx by txHash', async () => { const sdk = { getBySafeTxHash: jest.fn().mockResolvedValue({ @@ -549,7 +634,7 @@ describe('SafeWalletProvider', () => { const params = ['0x123'] - await safeWalletProvider.request(1, { method: 'wallet_getBundleStatus', params } as any, appInfo) + await safeWalletProvider.request(1, { method: 'wallet_getCallsStatus', params } as any, appInfo) expect(sdk.getBySafeTxHash).toHaveBeenCalledWith(params[0]) expect(sdk.proxy).toHaveBeenCalledWith('eth_getTransactionReceipt', params) @@ -571,14 +656,14 @@ describe('SafeWalletProvider', () => { const params = ['0x123'] - await safeWalletProvider.request(1, { method: 'wallet_getBundleStatus', params } as any, appInfo) + await safeWalletProvider.request(1, { method: 'wallet_getCallsStatus', params } as any, appInfo) expect(sdk.getBySafeTxHash).toHaveBeenCalledWith(params[0]) expect(sdk.proxy).not.toHaveBeenCalled() }) }) - describe('wallet_showBundleStatus', () => { + describe('wallet_showCallsStatus', () => { it('should return the bundle status', async () => { const sdk = { showTxStatus: jest.fn(), @@ -587,10 +672,52 @@ describe('SafeWalletProvider', () => { const params = ['0x123'] - await safeWalletProvider.request(1, { method: 'wallet_showBundleStatus', params } as any, appInfo) + await safeWalletProvider.request(1, { method: 'wallet_showCallsStatus', params } as any, appInfo) expect(sdk.showTxStatus).toHaveBeenCalledWith(params[0]) }) }) + + describe('wallet_getCapabilities', () => { + it('should return atomic batch for the current chain', async () => { + const sdk = { + showTxStatus: jest.fn(), + } + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + + const params = [safe.safeAddress] + + const result = await safeWalletProvider.request(1, { method: 'wallet_getCapabilities', params } as any, appInfo) + + expect(result).toEqual({ + id: 1, + jsonrpc: '2.0', + result: { + ['0x1']: { + atomicBatch: { + supported: true, + }, + }, + }, + }) + }) + + it('should return an empty object if the safe address does not match', async () => { + const sdk = { + showTxStatus: jest.fn(), + } + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + + const params = [faker.finance.ethereumAddress()] + + const result = await safeWalletProvider.request(1, { method: 'wallet_getCapabilities', params } as any, appInfo) + + expect(result).toEqual({ + id: 1, + jsonrpc: '2.0', + result: {}, + }) + }) + }) }) }) diff --git a/src/services/safe-wallet-provider/index.ts b/src/services/safe-wallet-provider/index.ts index ac61be0915..d44109b528 100644 --- a/src/services/safe-wallet-provider/index.ts +++ b/src/services/safe-wallet-provider/index.ts @@ -12,8 +12,10 @@ type SafeSettings = { offChainSigning?: boolean } +type GetCapabilitiesResult = Record<`0x${string}`, Record> + export type AppInfo = { - id: number + id?: number name: string description: string url: string @@ -32,6 +34,11 @@ export type WalletSDK = { switchChain: (chainId: string, appInfo: AppInfo) => Promise setSafeSettings: (safeSettings: SafeSettings) => SafeSettings proxy: (method: string, params?: Array | Record) => Promise + getCreateCallTransaction: (data: string) => { + to: string + data: string + value: '0' + } } interface RpcRequest { @@ -128,28 +135,34 @@ export class SafeWalletProvider { // EIP-5792 // @see https://eips.ethereum.org/EIPS/eip-5792 - case 'wallet_sendFunctionCallBundle': { - return this.wallet_sendFunctionCallBundle( + case 'wallet_sendCalls': { + return this.wallet_sendCalls( ...(params as [ { + version: string chainId: string from: string - calls: Array<{ gas: string; data: string; to?: string; value?: string }> + calls: Array<{ data: string; to?: string; value?: string }> + capabilities?: Record | undefined }, ]), appInfo, ) } - case 'wallet_getBundleStatus': { - return this.wallet_getBundleStatus(...(params as [string])) + case 'wallet_getCallsStatus': { + return this.wallet_getCallsStatus(...(params as [string])) } - case 'wallet_showBundleStatus': { - this.wallet_showBundleStatus(...(params as [string])) + case 'wallet_showCallsStatus': { + this.wallet_showCallsStatus(...(params as [string])) return null } + case 'wallet_getCapabilities': { + return this.wallet_getCapabilities(...(params as [string])) + } + // Safe proprietary methods case 'safe_setSettings': { return this.safe_setSettings(...(params as [SafeSettings])) @@ -314,17 +327,36 @@ export class SafeWalletProvider { // EIP-5792 // @see https://eips.ethereum.org/EIPS/eip-5792 - async wallet_sendFunctionCallBundle( + async wallet_sendCalls( bundle: { chainId: string from: string - calls: Array<{ gas: string; data: string; to?: string; value?: string }> + calls: Array<{ data?: string; to?: string; value?: string }> }, appInfo: AppInfo, ): Promise { + const txs = bundle.calls.map((call) => { + if (!call.to && !call.value && !call.data) { + throw new RpcError(RpcErrorCode.INVALID_PARAMS, 'Invalid call parameters.') + } + if (!call.to && !call.value && call.data) { + // If only data is provided the call is a contract deployment + // We have to use the CreateCall lib + return this.sdk.getCreateCallTransaction(call.data) + } + if (!call.to) { + // For all non-contract deployments we need a to address + throw new RpcError(RpcErrorCode.INVALID_PARAMS, 'Invalid call parameters.') + } + return { + to: call.to, + data: call.data ?? '0x', + value: call.value ?? '0', + } + }) const { safeTxHash } = await this.sdk.send( { - txs: bundle.calls, + txs, params: { safeTxGas: 0 }, }, appInfo, @@ -332,7 +364,7 @@ export class SafeWalletProvider { return safeTxHash } - async wallet_getBundleStatus(safeTxHash: string): Promise<{ + async wallet_getCallsStatus(safeTxHash: string): Promise<{ calls: Array<{ status: BundleStatus receipt: { @@ -381,11 +413,24 @@ export class SafeWalletProvider { calls: calls.map(() => callStatus), } } - async wallet_showBundleStatus(txHash: string): Promise { + async wallet_showCallsStatus(txHash: string): Promise { this.sdk.showTxStatus(txHash) return null } + async wallet_getCapabilities(walletAddress: string): Promise { + if (walletAddress === this.safe.safeAddress) { + return { + [`0x${this.safe.chainId.toString(16)}`]: { + atomicBatch: { + supported: true, + }, + }, + } + } + return {} + } + // Safe proprietary methods async safe_setSettings(settings: SafeSettings): Promise { return this.sdk.setSafeSettings(settings) diff --git a/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx b/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx index e8dba12a22..db9a29c922 100644 --- a/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx +++ b/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx @@ -11,6 +11,9 @@ import useSafeWalletProvider, { _useTxFlowApi } from './useSafeWalletProvider' import { SafeWalletProvider } from '.' import { makeStore } from '@/store' import * as messages from '@/utils/safe-messages' +import { faker } from '@faker-js/faker' +import { Interface } from 'ethers' +import { getCreateCallDeployment } from '@safe-global/safe-deployments' const appInfo = { id: 1, @@ -45,6 +48,7 @@ describe('useSafeWalletProvider', () => { value: '0x1234567890000000000000000000000000000000', }, deployed: true, + version: '1.3.0', } as unknown as ExtendedSafeInfo, }, }, @@ -64,6 +68,7 @@ describe('useSafeWalletProvider', () => { expect(result.current?.getBySafeTxHash).toBeDefined() expect(result.current?.switchChain).toBeDefined() expect(result.current?.proxy).toBeDefined() + expect(result.current?.getCreateCallTransaction).toBeDefined() }) it('should open signing window for off-chain messages', () => { @@ -294,8 +299,7 @@ describe('useSafeWalletProvider', () => { push: mockPush, } as unknown as router.NextRouter) - // @ts-expect-error - auto accept prompt - jest.spyOn(window, 'prompt').mockReturnValue(true) + jest.spyOn(window, 'confirm').mockReturnValue(true) const { result } = renderHook(() => _useTxFlowApi('1', '0x1234567890000000000000000000000000000000'), { initialReduxState: { @@ -354,4 +358,34 @@ describe('useSafeWalletProvider', () => { }, }) }) + + it('should create CreateCall lib transactions', () => { + const createCallDeployment = getCreateCallDeployment({ version: '1.3.0', network: '1' }) + const createCallInterface = new Interface(['function performCreate(uint256,bytes)']) + const safeAddress = faker.finance.ethereumAddress() + const { result } = renderHook(() => _useTxFlowApi('1', safeAddress), { + initialReduxState: { + safeInfo: { + loading: false, + error: undefined, + data: { + chainId: '1', + address: { + value: safeAddress, + }, + deployed: true, + version: '1.3.0', + } as unknown as ExtendedSafeInfo, + }, + }, + }) + + const tx = result.current?.getCreateCallTransaction('0x1234') + + expect(tx).toEqual({ + to: createCallDeployment?.networkAddresses['1'], + value: '0', + data: createCallInterface.encodeFunctionData('performCreate', [0, '0x1234']), + }) + }) }) diff --git a/src/services/safe-wallet-provider/useSafeWalletProvider.tsx b/src/services/safe-wallet-provider/useSafeWalletProvider.tsx index 6833de0a0f..6bb82c0934 100644 --- a/src/services/safe-wallet-provider/useSafeWalletProvider.tsx +++ b/src/services/safe-wallet-provider/useSafeWalletProvider.tsx @@ -14,7 +14,7 @@ import { Methods } from '@safe-global/safe-apps-sdk' import type { EIP712TypedData, SafeSettings } from '@safe-global/safe-apps-sdk' import { useWeb3ReadOnly } from '@/hooks/wallets/web3' import { getTransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' -import { getAddress } from 'ethers' +import { Interface, getAddress } from 'ethers' import { AppRoutes } from '@/config/routes' import useChains, { useCurrentChain } from '@/hooks/useChains' import { NotificationMessages, showNotification } from './notifications' @@ -22,6 +22,7 @@ import { SignMessageOnChainFlow } from '@/components/tx-flow/flows' import { useAppSelector } from '@/store' import { selectOnChainSigning } from '@/store/settingsSlice' import { isOffchainEIP1271Supported } from '@/utils/safe-messages' +import { getCreateCallContractDeployment } from '../contracts/deployments' export const _useTxFlowApi = (chainId: string, safeAddress: string): WalletSDK | undefined => { const { safe } = useSafeInfo() @@ -169,7 +170,7 @@ export const _useTxFlowApi = (chainId: string, safeAddress: string): WalletSDK | throw new Error(`Chain ${chainId} not supported`) } - if (prompt(`${appInfo.name} wants to switch to ${cfg.shortName}. Do you want to continue?`)) { + if (confirm(`${appInfo.name} wants to switch to ${cfg.shortName}. Do you want to continue?`)) { router.push({ pathname: AppRoutes.index, query: { @@ -205,6 +206,23 @@ export const _useTxFlowApi = (chainId: string, safeAddress: string): WalletSDK | async proxy(method, params) { return web3ReadOnly?.send(method, params ?? []) }, + + getCreateCallTransaction(data) { + const createCallDeployment = getCreateCallContractDeployment(safe.chainId, safe.version) + if (!createCallDeployment) { + throw new Error('No CreateCall deployment found for chain and safe version') + } + const createCallAddress = createCallDeployment.networkAddresses[safe.chainId] + + const createCallInterface = new Interface(createCallDeployment.abi) + const callData = createCallInterface.encodeFunctionData('performCreate', ['0', data]) + + return { + to: createCallAddress, + data: callData, + value: '0', + } + }, } }, [chainId, safeAddress, safe, currentChain, onChainSigning, settings, setTxFlow, configs, router, web3ReadOnly]) } diff --git a/src/services/siwe/index.ts b/src/services/siwe/index.ts new file mode 100644 index 0000000000..682e79c1bc --- /dev/null +++ b/src/services/siwe/index.ts @@ -0,0 +1,41 @@ +import { getAuthNonce, verifyAuth } from '@safe-global/safe-gateway-typescript-sdk' +import type { BrowserProvider } from 'ethers' + +/** + * Prompt the user to sign in with their wallet and set an access_token cookie + * @param provider + */ +async function signInWithEthereum(provider: BrowserProvider) { + const { nonce } = await getAuthNonce() + + const [network, signer] = await Promise.all([provider.getNetwork(), provider.getSigner()]) + + const message = { + domain: window.location.host, + address: signer.address as `0x${string}`, + // Results in special signing window in MetaMask + statement: 'Sign in with Ethereum to the app.', + uri: window.location.origin, + version: '1', + chainId: Number(network.chainId), + nonce, + issuedAt: new Date(), + } + + const signableMessage = `${message.domain} wants you to sign in with your Ethereum account: +${message.address} + +${message.statement} + +URI: ${message.uri} +Version: ${message.version} +Chain ID: ${message.chainId} +Nonce: ${message.nonce} +Issued At: ${message.issuedAt.toISOString()}` + + const signature = await signer.signMessage(signableMessage) + + return verifyAuth({ message: signableMessage, signature }) +} + +export default signInWithEthereum diff --git a/src/services/transactions/index.ts b/src/services/transactions/index.ts index d9b2c4da76..01e8fce25f 100644 --- a/src/services/transactions/index.ts +++ b/src/services/transactions/index.ts @@ -1,5 +1,6 @@ import memoize from 'lodash/memoize' import { getTransactionDetails, getTransactionHistory } from '@safe-global/safe-gateway-typescript-sdk' +import { trimTrailingSlash } from '@/utils/url' export const getTimezoneOffset = () => new Date().getTimezoneOffset() * 60 * -1000 @@ -28,3 +29,31 @@ export const getTxHistory = (chainId: string, safeAddress: string, trusted = fal pageUrl, ) } + +/** + * Fetch the module transaction id from the transaction service providing the transaction hash + */ +export const getModuleTransactionId = async ({ + transactionService, + safeAddress, + txHash, +}: { + transactionService: string + safeAddress: string + txHash: string +}) => { + const url = `${trimTrailingSlash( + transactionService, + )}/api/v1/safes/${safeAddress}/module-transactions/?transaction_hash=${txHash}` + const { results } = await fetch(url).then((res) => { + if (res.ok && res.status === 200) { + return res.json() as Promise + } else { + throw new Error('Error fetching Safe module transactions') + } + }) + + if (results.length === 0) throw new Error('module transaction not found') + + return results[0].moduleTransactionId as string +} diff --git a/src/services/tx/__tests__/txMonitor.test.ts b/src/services/tx/__tests__/txMonitor.test.ts index c87a161c58..cc46409c63 100644 --- a/src/services/tx/__tests__/txMonitor.test.ts +++ b/src/services/tx/__tests__/txMonitor.test.ts @@ -3,14 +3,13 @@ import * as txEvents from '@/services/tx/txEvents' import * as txMonitor from '@/services/tx/txMonitor' import { act } from '@testing-library/react' -import { SafeCreationStatus } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' import { toBeHex } from 'ethers' import { MockEip1193Provider } from '@/tests/mocks/providers' import { BrowserProvider, type JsonRpcProvider, type TransactionReceipt } from 'ethers' import { faker } from '@faker-js/faker' import { SimpleTxWatcher } from '@/utils/SimpleTxWatcher' -const { waitForTx, waitForRelayedTx, waitForCreateSafeTx } = txMonitor +const { waitForTx, waitForRelayedTx } = txMonitor const provider = new BrowserProvider(MockEip1193Provider) as unknown as JsonRpcProvider @@ -243,134 +242,6 @@ describe('txMonitor', () => { }) }) }) - - describe('waitForCreateSafeTx', () => { - it("sets the status to SUCCESS if taskStatus 'ExecSuccess'", async () => { - const mockData = { - task: { - taskState: 'ExecSuccess', - }, - } - global.fetch = jest.fn().mockImplementation(setupFetchStub(mockData)) - - const mockFetch = jest.spyOn(global, 'fetch') - const setStatusSpy = jest.fn() - - waitForCreateSafeTx('0x1', setStatusSpy) - - await act(() => { - jest.advanceTimersByTime(15_000 + 1) - }) - - expect(mockFetch).toHaveBeenCalledTimes(1) - expect(setStatusSpy).toHaveBeenCalledWith(SafeCreationStatus.SUCCESS) - }) - - it("sets the status to ERROR if taskStatus 'ExecReverted'", async () => { - const mockData = { - task: { - taskState: 'ExecReverted', - }, - } - global.fetch = jest.fn().mockImplementation(setupFetchStub(mockData)) - - const mockFetch = jest.spyOn(global, 'fetch') - const setStatusSpy = jest.fn() - - waitForCreateSafeTx('0x1', setStatusSpy) - - await act(() => { - jest.advanceTimersByTime(15_000 + 1) - }) - - expect(mockFetch).toHaveBeenCalledTimes(1) - expect(setStatusSpy).toHaveBeenCalledWith(SafeCreationStatus.ERROR) - }) - - it("sets the status to ERROR if taskStatus 'Blacklisted'", async () => { - const mockData = { - task: { - taskState: 'Blacklisted', - }, - } - global.fetch = jest.fn().mockImplementation(setupFetchStub(mockData)) - - const mockFetch = jest.spyOn(global, 'fetch') - const setStatusSpy = jest.fn() - - waitForCreateSafeTx('0x1', setStatusSpy) - - await act(() => { - jest.advanceTimersByTime(15_000 + 1) - }) - - expect(mockFetch).toHaveBeenCalledTimes(1) - expect(setStatusSpy).toHaveBeenCalledWith(SafeCreationStatus.ERROR) - }) - - it("sets the status to ERROR if taskStatus 'Cancelled'", async () => { - const mockData = { - task: { - taskState: 'Cancelled', - }, - } - global.fetch = jest.fn().mockImplementation(setupFetchStub(mockData)) - - const mockFetch = jest.spyOn(global, 'fetch') - const setStatusSpy = jest.fn() - - waitForCreateSafeTx('0x1', setStatusSpy) - - await act(() => { - jest.advanceTimersByTime(15_000 + 1) - }) - - expect(mockFetch).toHaveBeenCalledTimes(1) - expect(setStatusSpy).toHaveBeenCalledWith(SafeCreationStatus.ERROR) - }) - - it("sets the status to ERROR if taskStatus 'NotFound'", async () => { - const mockData = { - task: { - taskState: 'NotFound', - }, - } - global.fetch = jest.fn().mockImplementation(setupFetchStub(mockData)) - - const mockFetch = jest.spyOn(global, 'fetch') - const setStatusSpy = jest.fn() - - waitForCreateSafeTx('0x1', setStatusSpy) - - await act(() => { - jest.advanceTimersByTime(15_000 + 1) - }) - - expect(mockFetch).toHaveBeenCalledTimes(1) - expect(setStatusSpy).toHaveBeenCalledWith(SafeCreationStatus.ERROR) - }) - - it('sets the status to ERROR if the tx relaying timed out', async () => { - const mockData = { - task: { - taskState: 'WaitingForConfirmation', - }, - } - global.fetch = jest.fn().mockImplementation(setupFetchStub(mockData)) - - const mockFetch = jest.spyOn(global, 'fetch') - const setStatusSpy = jest.fn() - - waitForCreateSafeTx('0x1', setStatusSpy) - - await act(() => { - jest.advanceTimersByTime(3 * 60_000 + 1) - }) - - expect(mockFetch).toHaveBeenCalled() - expect(setStatusSpy).toHaveBeenCalledWith(SafeCreationStatus.ERROR) - }) - }) }) describe('getRemainingTimeout', () => { diff --git a/src/services/tx/extractTxInfo.ts b/src/services/tx/extractTxInfo.ts index fc8da6248d..6fc690beb4 100644 --- a/src/services/tx/extractTxInfo.ts +++ b/src/services/tx/extractTxInfo.ts @@ -57,6 +57,8 @@ const extractTxInfo = ( } else { return txDetails.txData?.value ?? '0' } + case 'TwapOrder': + return txDetails.txData?.value ?? '0' case 'SwapOrder': return txDetails.txData?.value ?? '0' case 'Custom': @@ -79,12 +81,12 @@ const extractTxInfo = ( return txDetails.txInfo.transferInfo.tokenAddress } case 'SwapOrder': - const swapOrderTo = txDetails.txData?.to.value - // TODO: Remove assertion after type is corrected - if (!swapOrderTo) { - throw new Error('SwapOrder tx data does not have a `to` field') + case 'TwapOrder': + const orderTo = txDetails.txData?.to.value + if (!orderTo) { + throw new Error('Order tx data does not have a `to` field') } - return swapOrderTo + return orderTo case 'Custom': return txDetails.txInfo.to.value case 'Creation': diff --git a/src/services/tx/tx-sender/__tests__/ts-sender.test.ts b/src/services/tx/tx-sender/__tests__/ts-sender.test.ts index dd5149468e..f961694bd6 100644 --- a/src/services/tx/tx-sender/__tests__/ts-sender.test.ts +++ b/src/services/tx/tx-sender/__tests__/ts-sender.test.ts @@ -33,7 +33,6 @@ const setupFetchStub = (data: any) => (_url: string) => { ok: true, }) } -import type { OnboardAPI, WalletState, AppState } from '@web3-onboard/core' import { toBeHex } from 'ethers' import { generatePreValidatedSignature } from '@safe-global/protocol-kit/dist/src/utils/signatures' import { createMockSafeTransaction } from '@/tests/transactions' @@ -67,47 +66,6 @@ jest.mock('../../proposeTransaction', () => ({ default: jest.fn(() => Promise.resolve({ txId: '123' })), })) -const mockOnboardState = { - chains: [], - walletModules: [], - wallets: [ - { - label: 'Wallet 1', - icon: '', - provider: MockEip1193Provider, - chains: [{ id: '0x5' }], - accounts: [ - { - address: SIGNER_ADDRESS, - ens: null, - balance: null, - }, - ], - }, - ] as WalletState[], - accountCenter: { - enabled: true, - }, -} as unknown as AppState - -const mockOnboard = { - connectWallet: jest.fn(), - disconnectWallet: jest.fn(), - setChain: jest.fn(), - state: { - select: (key: keyof AppState) => ({ - subscribe: (next: any) => { - next(mockOnboardState[key]) - - return { - unsubscribe: jest.fn(), - } - }, - }), - get: () => mockOnboardState, - }, -} as unknown as OnboardAPI - // Mock Safe SDK const mockSafeSDK = { createTransaction: jest.fn(() => ({ @@ -323,7 +281,7 @@ describe('txSender', () => { nonce: 1, }) - const signedTx = await dispatchTxSigning(tx, '1.3.0', mockOnboard, '5', '0x345') + const signedTx = await dispatchTxSigning(tx, '1.3.0', MockEip1193Provider, '0x345') expect(mockSafeSDK.createTransaction).toHaveBeenCalled() @@ -344,7 +302,7 @@ describe('txSender', () => { nonce: 1, }) - const signedTx = await dispatchTxSigning(tx, '1.0.0', mockOnboard, '5', '0x345') + const signedTx = await dispatchTxSigning(tx, '1.0.0', MockEip1193Provider, '0x345') expect(mockSafeSDK.createTransaction).toHaveBeenCalledTimes(1) @@ -365,7 +323,7 @@ describe('txSender', () => { nonce: 1, }) - const signedTx = await dispatchTxSigning(tx, null, mockOnboard, '5', '0x345') + const signedTx = await dispatchTxSigning(tx, null, MockEip1193Provider, '0x345') expect(mockSafeSDK.createTransaction).toHaveBeenCalledTimes(1) @@ -388,7 +346,7 @@ describe('txSender', () => { nonce: 1, }) - const signedTx = await dispatchTxSigning(tx, '1.3.0', mockOnboard, '5', '0x345') + const signedTx = await dispatchTxSigning(tx, '1.3.0', MockEip1193Provider, '0x345') expect(mockSafeSDK.createTransaction).toHaveBeenCalledTimes(1) @@ -414,7 +372,7 @@ describe('txSender', () => { let signedTx try { - signedTx = await dispatchTxSigning(tx, '1.3.0', mockOnboard, '5', '0x345') + signedTx = await dispatchTxSigning(tx, '1.3.0', MockEip1193Provider, '0x345') } catch (error) { expect(mockSafeSDK.createTransaction).toHaveBeenCalledTimes(1) @@ -448,7 +406,7 @@ describe('txSender', () => { let signedTx try { - signedTx = await dispatchTxSigning(tx, '1.3.0', mockOnboard, '5', '0x345') + signedTx = await dispatchTxSigning(tx, '1.3.0', MockEip1193Provider, '0x345') } catch (error) { expect(mockSafeSDK.createTransaction).toHaveBeenCalledTimes(1) @@ -484,7 +442,7 @@ describe('txSender', () => { nonce: 1, }) - await dispatchTxExecution(safeTx, { nonce: 1 }, txId, mockOnboard, '5', safeAddress) + await dispatchTxExecution(safeTx, { nonce: 1 }, txId, MockEip1193Provider, SIGNER_ADDRESS, safeAddress) expect(mockSafeSDK.executeTransaction).toHaveBeenCalled() expect(txEvents.txDispatch).toHaveBeenCalledWith('EXECUTING', { txId }) @@ -512,7 +470,9 @@ describe('txSender', () => { nonce: 1, }) - await expect(dispatchTxExecution(safeTx, {}, txId, mockOnboard, '5', safeAddress)).rejects.toThrow('error') + await expect(dispatchTxExecution(safeTx, {}, txId, MockEip1193Provider, '5', safeAddress)).rejects.toThrow( + 'error', + ) expect(mockSafeSDK.executeTransaction).toHaveBeenCalled() expect(txEvents.txDispatch).toHaveBeenCalledWith('FAILED', { txId, error: new Error('error') }) @@ -531,7 +491,7 @@ describe('txSender', () => { nonce: 1, }) - await dispatchTxExecution(safeTx, { nonce: 1 }, txId, mockOnboard, '5', '0x123') + await dispatchTxExecution(safeTx, { nonce: 1 }, txId, MockEip1193Provider, SIGNER_ADDRESS, '0x123') expect(mockSafeSDK.executeTransaction).toHaveBeenCalled() expect(txEvents.txDispatch).toHaveBeenCalledWith('EXECUTING', { txId }) diff --git a/src/services/tx/tx-sender/dispatch.ts b/src/services/tx/tx-sender/dispatch.ts index 3f9038aa7b..5af9b9f578 100644 --- a/src/services/tx/tx-sender/dispatch.ts +++ b/src/services/tx/tx-sender/dispatch.ts @@ -1,10 +1,15 @@ import { relayTransaction, type SafeInfo, type TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' -import type { SafeTransaction, TransactionOptions, TransactionResult } from '@safe-global/safe-core-sdk-types' +import type { + SafeTransaction, + Transaction, + TransactionOptions, + TransactionResult, +} from '@safe-global/safe-core-sdk-types' import { didRevert } from '@/utils/ethers-utils' import type { MultiSendCallOnlyEthersContract } from '@safe-global/protocol-kit' import { type SpendingLimitTxParams } from '@/components/tx-flow/flows/TokenTransfer/ReviewSpendingLimitTx' import { getSpendingLimitContract } from '@/services/contracts/spendingLimitContracts' -import type { ContractTransactionResponse, Overrides, TransactionResponse } from 'ethers' +import type { ContractTransactionResponse, Eip1193Provider, Overrides, TransactionResponse } from 'ethers' import type { RequestId } from '@safe-global/safe-apps-sdk' import proposeTx from '../proposeTransaction' import { txDispatch, TxEvent } from '../txEvents' @@ -14,11 +19,10 @@ import { getAndValidateSafeSDK, getSafeSDKWithSigner, getUncheckedSafeSDK, - assertWalletChain, tryOffChainTxSigning, + getUncheckedSigner, } from './sdk' import { createWeb3, getUserNonce, getWeb3ReadOnly } from '@/hooks/wallets/web3' -import { type OnboardAPI } from '@web3-onboard/core' import { asError } from '@/services/exceptions/utils' import chains from '@/config/chains' import { LATEST_SAFE_VERSION } from '@/config/constants' @@ -76,11 +80,10 @@ export const dispatchTxProposal = async ({ export const dispatchTxSigning = async ( safeTx: SafeTransaction, safeVersion: SafeInfo['version'], - onboard: OnboardAPI, - chainId: SafeInfo['chainId'], + provider: Eip1193Provider, txId?: string, ): Promise => { - const sdk = await getSafeSDKWithSigner(onboard, chainId) + const sdk = await getSafeSDKWithSigner(provider) let signedTx: SafeTransaction | undefined try { @@ -106,10 +109,10 @@ const ZK_SYNC_ON_CHAIN_SIGNATURE_GAS_LIMIT = 4_500_000 export const dispatchOnChainSigning = async ( safeTx: SafeTransaction, txId: string, - onboard: OnboardAPI, + provider: Eip1193Provider, chainId: SafeInfo['chainId'], ) => { - const sdkUnchecked = await getUncheckedSafeSDK(onboard, chainId) + const sdkUnchecked = await getUncheckedSafeSDK(provider) const safeTxHash = await sdkUnchecked.getTransactionHash(safeTx) const eventParams = { txId } @@ -135,13 +138,13 @@ export const dispatchOnChainSigning = async ( export const dispatchSafeTxSpeedUp = async ( txOptions: Omit & { nonce: number }, txId: string, - onboard: OnboardAPI, + provider: Eip1193Provider, chainId: SafeInfo['chainId'], + signerAddress: string, safeAddress: string, ) => { - const sdkUnchecked = await getUncheckedSafeSDK(onboard, chainId) + const sdkUnchecked = await getUncheckedSafeSDK(provider) const eventParams = { txId } - const wallet = await assertWalletChain(onboard, chainId) const signerNonce = txOptions.nonce // Execute the tx @@ -158,17 +161,17 @@ export const dispatchSafeTxSpeedUp = async ( txDispatch(TxEvent.PROCESSING, { ...eventParams, txHash: result.hash, - signerAddress: wallet.address, + signerAddress, signerNonce, gasLimit: txOptions.gasLimit, txType: 'SafeTx', }) - const provider = getWeb3ReadOnly() + const readOnlyProvider = getWeb3ReadOnly() - if (provider) { + if (readOnlyProvider) { // don't await as we don't want to block - waitForTx(provider, [txId], result.hash, safeAddress, wallet.address, signerNonce) + waitForTx(readOnlyProvider, [txId], result.hash, safeAddress, signerAddress, signerNonce) } return result.hash @@ -179,19 +182,17 @@ export const dispatchCustomTxSpeedUp = async ( txId: string, to: string, data: string, - onboard: OnboardAPI, - chainId: SafeInfo['chainId'], + provider: Eip1193Provider, + signerAddress: string, safeAddress: string, ) => { const eventParams = { txId } - const wallet = await assertWalletChain(onboard, chainId) const signerNonce = txOptions.nonce - const web3Provider = createWeb3(wallet.provider) - const signer = await web3Provider.getSigner() // Execute the tx let result: TransactionResponse | undefined try { + const signer = await getUncheckedSigner(provider) result = await signer.sendTransaction({ to, data, ...txOptions }) txDispatch(TxEvent.EXECUTING, eventParams) } catch (error) { @@ -201,7 +202,7 @@ export const dispatchCustomTxSpeedUp = async ( txDispatch(TxEvent.PROCESSING, { txHash: result.hash, - signerAddress: wallet.address, + signerAddress, signerNonce, data, to, @@ -209,11 +210,11 @@ export const dispatchCustomTxSpeedUp = async ( txType: 'Custom', }) - const provider = getWeb3ReadOnly() + const readOnlyProvider = getWeb3ReadOnly() - if (provider) { + if (readOnlyProvider) { // don't await as we don't want to block - waitForTx(provider, [txId], result.hash, safeAddress, wallet.address, signerNonce) + waitForTx(readOnlyProvider, [txId], result.hash, safeAddress, signerAddress, signerNonce) } return result.hash @@ -226,15 +227,14 @@ export const dispatchTxExecution = async ( safeTx: SafeTransaction, txOptions: TransactionOptions, txId: string, - onboard: OnboardAPI, - chainId: SafeInfo['chainId'], + provider: Eip1193Provider, + signerAddress: string, safeAddress: string, ): Promise => { - const sdkUnchecked = await getUncheckedSafeSDK(onboard, chainId) + const sdkUnchecked = await getUncheckedSafeSDK(provider) const eventParams = { txId } - const wallet = await assertWalletChain(onboard, chainId) - const signerNonce = txOptions.nonce ?? (await getUserNonce(wallet.address)) + const signerNonce = txOptions.nonce ?? (await getUserNonce(signerAddress)) // Execute the tx let result: TransactionResult | undefined @@ -249,18 +249,18 @@ export const dispatchTxExecution = async ( txDispatch(TxEvent.PROCESSING, { ...eventParams, txHash: result.hash, - signerAddress: wallet.address, + signerAddress, signerNonce, gasLimit: txOptions.gasLimit, txType: 'SafeTx', }) - const provider = getWeb3ReadOnly() + const readOnlyProvider = getWeb3ReadOnly() // Asynchronously watch the tx to be mined/validated - if (provider) { + if (readOnlyProvider) { // don't await as we don't want to block - waitForTx(provider, [txId], result.hash, safeAddress, wallet.address, signerNonce) + waitForTx(readOnlyProvider, [txId], result.hash, safeAddress, signerAddress, signerNonce) } return result.hash @@ -270,8 +270,8 @@ export const dispatchBatchExecution = async ( txs: TransactionDetails[], multiSendContract: MultiSendCallOnlyEthersContract, multiSendTxData: string, - onboard: OnboardAPI, - chainId: SafeInfo['chainId'], + provider: Eip1193Provider, + signerAddress: string, safeAddress: string, overrides: Omit & { nonce: number }, ) => { @@ -279,18 +279,15 @@ export const dispatchBatchExecution = async ( let result: ContractTransactionResponse | undefined const txIds = txs.map((tx) => tx.txId) - let signerAddress: string | undefined = undefined let signerNonce = overrides.nonce let txData = multiSendContract.encode('multiSend', [multiSendTxData]) - const wallet = await assertWalletChain(onboard, chainId) try { - signerAddress = wallet.address if (signerNonce === undefined || signerNonce === null) { signerNonce = await getUserNonce(signerAddress) } - const provider = createWeb3(wallet.provider) - result = await multiSendContract.contract.connect(await provider.getSigner()).multiSend(multiSendTxData, overrides) + const signer = await getUncheckedSigner(provider) + result = await multiSendContract.contract.connect(signer).multiSend(multiSendTxData, overrides) txIds.forEach((txId) => { txDispatch(TxEvent.EXECUTING, { txId, groupKey }) @@ -309,27 +306,75 @@ export const dispatchBatchExecution = async ( txHash: result!.hash, groupKey, signerNonce, - signerAddress: wallet.address, + signerAddress, txType: 'Custom', data: txData, to: txTo, }) }) - const provider = getWeb3ReadOnly() + const readOnlyProvider = getWeb3ReadOnly() - if (provider) { + if (readOnlyProvider) { // don't await as we don't want to block - waitForTx(provider, txIds, result.hash, safeAddress, signerAddress, signerNonce) + waitForTx(readOnlyProvider, txIds, result.hash, safeAddress, signerAddress, signerNonce) } return result!.hash } +/** + * Execute a module transaction + */ +export const dispatchModuleTxExecution = async ( + tx: Transaction, + provider: Eip1193Provider, + safeAddress: string, +): Promise => { + const id = JSON.stringify(tx) + + let result: TransactionResponse | undefined + try { + const browserProvider = createWeb3(provider) + const signer = await browserProvider.getSigner() + + txDispatch(TxEvent.EXECUTING, { groupKey: id }) + result = await signer.sendTransaction(tx) + } catch (error) { + txDispatch(TxEvent.FAILED, { groupKey: id, error: asError(error) }) + throw error + } + + txDispatch(TxEvent.PROCESSING_MODULE, { + groupKey: id, + txHash: result.hash, + }) + + result + ?.wait() + .then((receipt) => { + if (receipt === null) { + txDispatch(TxEvent.FAILED, { groupKey: id, error: new Error('No transaction receipt found') }) + } else if (didRevert(receipt)) { + txDispatch(TxEvent.REVERTED, { + groupKey: id, + error: new Error('Transaction reverted by EVM'), + }) + } else { + txDispatch(TxEvent.PROCESSED, { groupKey: id, safeAddress, txHash: result?.hash }) + } + }) + .catch((error) => { + txDispatch(TxEvent.FAILED, { groupKey: id, error: asError(error) }) + }) + + return result?.hash +} + export const dispatchSpendingLimitTxExecution = async ( txParams: SpendingLimitTxParams, txOptions: TransactionOptions, - onboard: OnboardAPI, + provider: Eip1193Provider, chainId: SafeInfo['chainId'], safeAddress: string, ) => { @@ -337,9 +382,8 @@ export const dispatchSpendingLimitTxExecution = async ( let result: ContractTransactionResponse | undefined try { - const wallet = await assertWalletChain(onboard, chainId) - const provider = createWeb3(wallet.provider) - const contract = getSpendingLimitContract(chainId, await provider.getSigner()) + const signer = await getUncheckedSigner(provider) + const contract = getSpendingLimitContract(chainId, signer) result = await contract.executeAllowanceTransfer( txParams.safeAddress, @@ -387,11 +431,10 @@ export const dispatchSpendingLimitTxExecution = async ( export const dispatchSafeAppsTx = async ( safeTx: SafeTransaction, safeAppRequestId: RequestId, - onboard: OnboardAPI, - chainId: SafeInfo['chainId'], + provider: Eip1193Provider, txId?: string, ): Promise => { - const sdk = await getSafeSDKWithSigner(onboard, chainId) + const sdk = await getSafeSDKWithSigner(provider) const safeTxHash = await sdk.getTransactionHash(safeTx) txDispatch(TxEvent.SAFE_APPS_REQUEST, { safeAppRequestId, safeTxHash, txId }) return safeTxHash diff --git a/src/services/tx/tx-sender/sdk.ts b/src/services/tx/tx-sender/sdk.ts index 9379b42b37..3071bd19a2 100644 --- a/src/services/tx/tx-sender/sdk.ts +++ b/src/services/tx/tx-sender/sdk.ts @@ -1,7 +1,7 @@ import { getSafeSDK } from '@/hooks/coreSDK/safeCoreSDK' import type Safe from '@safe-global/protocol-kit' import { EthersAdapter, SigningMethod } from '@safe-global/protocol-kit' -import type { JsonRpcSigner } from 'ethers' +import type { Eip1193Provider, JsonRpcSigner } from 'ethers' import { ethers } from 'ethers' import { isWalletRejection, isHardwareWallet, isWalletConnect } from '@/utils/wallets' import { OperationType, type SafeTransaction } from '@safe-global/safe-core-sdk-types' @@ -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 } } @@ -111,13 +117,14 @@ export const assertWalletChain = async (onboard: OnboardAPI, chainId: string): P return newWallet } -export const getAssertedChainSigner = async ( - onboard: OnboardAPI, - chainId: SafeInfo['chainId'], -): Promise => { - const wallet = await assertWalletChain(onboard, chainId) - const provider = createWeb3(wallet.provider) - return provider.getSigner() +export const getAssertedChainSigner = async (provider: Eip1193Provider): Promise => { + const browserProvider = createWeb3(provider) + return browserProvider.getSigner() +} + +export const getUncheckedSigner = async (provider: Eip1193Provider) => { + const browserProvider = createWeb3(provider) + return new UncheckedJsonRpcSigner(browserProvider, (await browserProvider.getSigner()).address) } /** @@ -126,8 +133,9 @@ export const getAssertedChainSigner = async ( * most of the values of transactionResponse which is needed when * dealing with smart-contract wallet owners */ -export const getUncheckedSafeSDK = async (onboard: OnboardAPI, chainId: SafeInfo['chainId']): Promise => { - const signer = await getAssertedChainSigner(onboard, chainId) +export const getUncheckedSafeSDK = async (provider: Eip1193Provider): Promise => { + const browserProvider = createWeb3(provider) + const signer = await browserProvider.getSigner() const uncheckedJsonRpcSigner = new UncheckedJsonRpcSigner(signer.provider, await signer.getAddress()) const sdk = getAndValidateSafeSDK() @@ -139,8 +147,9 @@ export const getUncheckedSafeSDK = async (onboard: OnboardAPI, chainId: SafeInfo return sdk.connect({ ethAdapter }) } -export const getSafeSDKWithSigner = async (onboard: OnboardAPI, chainId: SafeInfo['chainId']): Promise => { - const signer = await getAssertedChainSigner(onboard, chainId) +export const getSafeSDKWithSigner = async (provider: Eip1193Provider): Promise => { + const browserProvider = createWeb3(provider) + const signer = await browserProvider.getSigner() const sdk = getAndValidateSafeSDK() const ethAdapter = new EthersAdapter({ diff --git a/src/services/tx/txMonitor.ts b/src/services/tx/txMonitor.ts index f0dcc0d5d2..2931808128 100644 --- a/src/services/tx/txMonitor.ts +++ b/src/services/tx/txMonitor.ts @@ -4,7 +4,6 @@ import { txDispatch, TxEvent } from '@/services/tx/txEvents' import { POLLING_INTERVAL } from '@/config/constants' import { Errors, logError } from '@/services/exceptions' -import { SafeCreationStatus } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' import { asError } from '../exceptions/utils' import { type JsonRpcProvider, type TransactionReceipt } from 'ethers' import { SimpleTxWatcher } from '@/utils/SimpleTxWatcher' @@ -205,41 +204,3 @@ export const waitForRelayedTx = (taskId: string, txIds: string[], safeAddress: s clearInterval(intervalId) }, WAIT_FOR_RELAY_TIMEOUT) } - -export const waitForCreateSafeTx = (taskId: string, setStatus: (value: SafeCreationStatus) => void): void => { - let intervalId: NodeJS.Timeout - let failAfterTimeoutId: NodeJS.Timeout - - intervalId = setInterval(async () => { - const status = await getRelayTxStatus(taskId) - - // 404 - if (!status) { - return - } - - switch (status.task.taskState) { - case TaskState.ExecSuccess: - setStatus(SafeCreationStatus.SUCCESS) - break - case TaskState.ExecReverted: - case TaskState.Blacklisted: - case TaskState.Cancelled: - case TaskState.NotFound: - setStatus(SafeCreationStatus.ERROR) - break - default: - // Don't clear interval as we're still waiting for the tx to be relayed - return - } - - clearTimeout(failAfterTimeoutId) - clearInterval(intervalId) - }, POLLING_INTERVAL) - - failAfterTimeoutId = setTimeout(() => { - setStatus(SafeCreationStatus.ERROR) - - clearInterval(intervalId) - }, WAIT_FOR_RELAY_TIMEOUT) -} diff --git a/src/store/batchSlice.ts b/src/store/batchSlice.ts index 6a796b4526..4591aaad26 100644 --- a/src/store/batchSlice.ts +++ b/src/store/batchSlice.ts @@ -20,21 +20,6 @@ export const batchSlice = createSlice({ name: 'batch', initialState, reducers: { - // Set a batch (used for reordering) - setBatch: ( - state, - action: PayloadAction<{ - chainId: string - safeAddress: string - items: DraftBatchItem[] - }>, - ) => { - const { chainId, safeAddress, items } = action.payload - state[chainId] = state[chainId] || {} - // @ts-expect-error - state[chainId][safeAddress] = items - }, - // Add a tx to the batch addTx: ( state, @@ -47,9 +32,11 @@ export const batchSlice = createSlice({ const { chainId, safeAddress, txDetails } = action.payload state[chainId] = state[chainId] || {} state[chainId][safeAddress] = state[chainId][safeAddress] || [] + // @ts-expect-error state[chainId][safeAddress].push({ id: Math.random().toString(36).slice(2), timestamp: Date.now(), + // @ts-expect-error txDetails, }) }, @@ -71,7 +58,7 @@ export const batchSlice = createSlice({ }, }) -export const { setBatch, addTx, removeTx } = batchSlice.actions +export const { addTx, removeTx } = batchSlice.actions const selectAllBatches = (state: RootState): BatchTxsState => { return state[batchSlice.name] || initialState diff --git a/src/store/cookiesAndTermsSlice.ts b/src/store/cookiesAndTermsSlice.ts new file mode 100644 index 0000000000..2c87b0f1f6 --- /dev/null +++ b/src/store/cookiesAndTermsSlice.ts @@ -0,0 +1,31 @@ +import type { PayloadAction } from '@reduxjs/toolkit' +import { createSlice } from '@reduxjs/toolkit' +import type { RootState } from '.' + +export enum CookieAndTermType { + TERMS = 'terms', + NECESSARY = 'necessary', + UPDATES = 'updates', + ANALYTICS = 'analytics', +} + +export type CookiesAndTermsState = Record + +const initialState: CookiesAndTermsState = { + [CookieAndTermType.TERMS]: undefined, + [CookieAndTermType.NECESSARY]: undefined, + [CookieAndTermType.UPDATES]: undefined, + [CookieAndTermType.ANALYTICS]: undefined, +} + +export const cookiesAndTermsSlice = createSlice({ + name: 'cookies_terms_v1', + initialState, + reducers: { + saveCookieAndTermConsent: (_, { payload }: PayloadAction) => payload, + }, +}) + +export const { saveCookieAndTermConsent } = cookiesAndTermsSlice.actions + +export const selectCookies = (state: RootState) => state[cookiesAndTermsSlice.name] diff --git a/src/store/cookiesSlice.ts b/src/store/cookiesSlice.ts deleted file mode 100644 index 70bbc35662..0000000000 --- a/src/store/cookiesSlice.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { PayloadAction } from '@reduxjs/toolkit' -import { createSlice } from '@reduxjs/toolkit' -import type { RootState } from '.' - -export enum CookieType { - NECESSARY = 'necessary', - UPDATES = 'updates', - ANALYTICS = 'analytics', -} - -export type CookiesState = Record - -const initialState: CookiesState = { - [CookieType.NECESSARY]: undefined, - [CookieType.UPDATES]: undefined, - [CookieType.ANALYTICS]: undefined, -} - -export const cookiesSlice = createSlice({ - name: 'cookies', - initialState, - reducers: { - saveCookieConsent: (_, { payload }: PayloadAction) => payload, - }, -}) - -export const { saveCookieConsent } = cookiesSlice.actions - -export const selectCookies = (state: RootState) => state[cookiesSlice.name] diff --git a/src/store/index.ts b/src/store/index.ts index de091020b3..e6b2f8f81c 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -34,7 +34,7 @@ const rootReducer = combineReducers({ [slices.pendingTxsSlice.name]: slices.pendingTxsSlice.reducer, [slices.addedSafesSlice.name]: slices.addedSafesSlice.reducer, [slices.settingsSlice.name]: slices.settingsSlice.reducer, - [slices.cookiesSlice.name]: slices.cookiesSlice.reducer, + [slices.cookiesAndTermsSlice.name]: slices.cookiesAndTermsSlice.reducer, [slices.popupSlice.name]: slices.popupSlice.reducer, [slices.spendingLimitSlice.name]: slices.spendingLimitSlice.reducer, [slices.safeAppsSlice.name]: slices.safeAppsSlice.reducer, @@ -51,7 +51,7 @@ const persistedSlices: (keyof PreloadedState)[] = [ slices.pendingTxsSlice.name, slices.addedSafesSlice.name, slices.settingsSlice.name, - slices.cookiesSlice.name, + slices.cookiesAndTermsSlice.name, slices.safeAppsSlice.name, slices.pendingSafeMessagesSlice.name, slices.batchSlice.name, diff --git a/src/store/popupSlice.ts b/src/store/popupSlice.ts index c077d558a7..831fcc5c90 100644 --- a/src/store/popupSlice.ts +++ b/src/store/popupSlice.ts @@ -1,6 +1,6 @@ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' -import type { CookieType } from './cookiesSlice' +import type { CookieAndTermType } from './cookiesAndTermsSlice' import type { RootState } from '.' export enum PopupType { @@ -10,7 +10,7 @@ export enum PopupType { type PopupState = { [PopupType.COOKIES]: { open: boolean - warningKey?: CookieType + warningKey?: CookieAndTermType } } @@ -24,7 +24,7 @@ export const popupSlice = createSlice({ name: 'popups', initialState, reducers: { - openCookieBanner: (state, { payload }: PayloadAction<{ warningKey?: CookieType }>) => { + openCookieBanner: (state, { payload }: PayloadAction<{ warningKey?: CookieAndTermType }>) => { state[PopupType.COOKIES] = { ...payload, open: true, diff --git a/src/store/slices.ts b/src/store/slices.ts index b186894502..210bd7d7c5 100644 --- a/src/store/slices.ts +++ b/src/store/slices.ts @@ -9,7 +9,7 @@ export * from './notificationsSlice' export * from './pendingTxsSlice' export * from './addedSafesSlice' export * from './settingsSlice' -export * from './cookiesSlice' +export * from './cookiesAndTermsSlice' export * from './popupSlice' export * from './spendingLimitsSlice' export * from './safeAppsSlice' diff --git a/src/store/swapOrderSlice.ts b/src/store/swapOrderSlice.ts index 2fd4afcf2b..c8f884aef6 100644 --- a/src/store/swapOrderSlice.ts +++ b/src/store/swapOrderSlice.ts @@ -2,10 +2,12 @@ import type { listenerMiddlewareInstance } from '@/store' import { createSelector, createSlice } from '@reduxjs/toolkit' import type { OrderStatuses } from '@safe-global/safe-gateway-typescript-sdk' import type { RootState } from '@/store' -import { isSwapTxInfo, isTransactionListItem } from '@/utils/transaction-guards' +import { isSwapOrderTxInfo, isTransactionListItem } from '@/utils/transaction-guards' import { txHistorySlice } from '@/store/txHistorySlice' import { showNotification } from '@/store/notificationsSlice' import { selectSafeInfo } from '@/store/safeInfoSlice' +import { selectChainById } from '@/store/chainsSlice' +import { getTxLink } from '@/utils/tx-link' type AllStatuses = OrderStatuses | 'created' type Order = { @@ -80,11 +82,18 @@ export const swapOrderStatusListener = (listenerMiddleware: typeof listenerMiddl if (oldStatus === newStatus || newStatus === undefined) { return } + const safeInfo = selectSafeInfo(listenerApi.getState()) + + let link = undefined + if (swapOrder.txId && safeInfo.data?.chainId && safeInfo.data?.address) { + const chainInfo = selectChainById(listenerApi.getState(), safeInfo.data?.chainId) + if (chainInfo !== undefined) { + link = getTxLink(swapOrder.txId, chainInfo, safeInfo.data?.address.value) + } + } switch (newStatus) { case 'created': - const safeInfo = selectSafeInfo(listenerApi.getState()) - dispatch( showNotification({ title: 'Order created', @@ -94,6 +103,7 @@ export const swapOrderStatusListener = (listenerMiddleware: typeof listenerMiddl : 'Waiting for confirmation from signers of your Safe', groupKey, variant: 'info', + link, }), ) @@ -105,6 +115,7 @@ export const swapOrderStatusListener = (listenerMiddleware: typeof listenerMiddl message: 'Waiting for confirmation from signers of your Safe', groupKey, variant: 'info', + link, }), ) break @@ -115,6 +126,7 @@ export const swapOrderStatusListener = (listenerMiddleware: typeof listenerMiddl message: 'Waiting for order execution by the CoW Protocol', groupKey, variant: 'info', + link, }), ) break @@ -129,6 +141,7 @@ export const swapOrderStatusListener = (listenerMiddleware: typeof listenerMiddl message: 'Your order has been successful', groupKey, variant: 'success', + link, }), ) break @@ -143,6 +156,7 @@ export const swapOrderStatusListener = (listenerMiddleware: typeof listenerMiddl message: 'Your order has reached the expiry time and has become invalid', groupKey, variant: 'warning', + link, }), ) break @@ -157,6 +171,7 @@ export const swapOrderStatusListener = (listenerMiddleware: typeof listenerMiddl message: 'Your order has been cancelled', groupKey, variant: 'warning', + link, }), ) break @@ -182,7 +197,7 @@ export const swapOrderListener = (listenerMiddleware: typeof listenerMiddlewareI continue } - if (isSwapTxInfo(result.transaction.txInfo)) { + if (isSwapOrderTxInfo(result.transaction.txInfo)) { const swapOrder = result.transaction.txInfo const oldStatus = selectSwapOrderStatus(listenerApi.getOriginalState(), swapOrder.uid) diff --git a/src/tests/builders/safeMessage.ts b/src/tests/builders/safeMessage.ts new file mode 100644 index 0000000000..b3a9da6a34 --- /dev/null +++ b/src/tests/builders/safeMessage.ts @@ -0,0 +1,25 @@ +import { Builder, type IBuilder } from '@/tests/Builder' +import { faker } from '@faker-js/faker' +import { SafeMessageListItemType, SafeMessageStatus, type SafeMessage } from '@safe-global/safe-gateway-typescript-sdk' + +export function safeMsgBuilder(): IBuilder { + return Builder.new().with({ + type: SafeMessageListItemType.MESSAGE, + messageHash: faker.string.hexadecimal(), + status: SafeMessageStatus.NEEDS_CONFIRMATION, + logoUri: null, + name: null, + message: 'Message text', + creationTimestamp: faker.date.past().getTime(), + modifiedTimestamp: faker.date.past().getTime(), + confirmationsSubmitted: 1, + confirmationsRequired: 2, + proposedBy: { value: faker.finance.ethereumAddress() }, + confirmations: [ + { + owner: { value: faker.finance.ethereumAddress() }, + signature: '', + }, + ], + }) +} diff --git a/src/tests/builders/wallet.ts b/src/tests/builders/wallet.ts index f088389e52..fc95ce44f5 100644 --- a/src/tests/builders/wallet.ts +++ b/src/tests/builders/wallet.ts @@ -3,7 +3,7 @@ import { faker } from '@faker-js/faker' import { Builder, type IBuilder } from '../Builder' import { eip1193ProviderBuilder } from './eip1193Provider' -const walletNames = ['MetaMask', 'Wallet Connect', 'Social Login', 'Rainbow'] +const walletNames = ['MetaMask', 'Wallet Connect', 'Rainbow'] export const connectedWalletBuilder = (): IBuilder => { return Builder.new().with({ diff --git a/src/tests/e2e-wallet.ts b/src/tests/e2e-wallet.ts index 58d79323b3..17ce32c32e 100644 --- a/src/tests/e2e-wallet.ts +++ b/src/tests/e2e-wallet.ts @@ -1,30 +1,94 @@ +import { type HDNodeWallet, JsonRpcProvider, Wallet } from 'ethers' import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' -import type { WalletInit } from '@web3-onboard/common' - -import { CYPRESS_MNEMONIC } from '@/config/constants' +import { type WalletInit, createEIP1193Provider } from '@web3-onboard/common' import { getRpcServiceUrl } from '@/hooks/wallets/web3' +import { numberToHex } from '@/utils/hex' +import { CYPRESS_MNEMONIC } from '@/config/constants' export const E2E_WALLET_NAME = 'E2E Wallet' -const e2eWalletModule = (rpcUri: ChainInfo['rpcUri']): WalletInit => { +let currentChainId = '' +let currentRpcUri = '' + +const E2EWalletMoule = (chainId: ChainInfo['chainId'], rpcUri: ChainInfo['rpcUri']): WalletInit => { + currentChainId = chainId + currentRpcUri = getRpcServiceUrl(rpcUri) + return () => { return { label: E2E_WALLET_NAME, getIcon: async () => '', getInterface: async () => { - const { createEIP1193Provider } = await import('@web3-onboard/common') + let provider: JsonRpcProvider + let wallet: HDNodeWallet + const chainChangedListeners = new Set<(chainId: string) => void>() + + const updateProvider = () => { + provider?.destroy() + provider = new JsonRpcProvider(currentRpcUri, Number(currentChainId), { staticNetwork: true }) + wallet = Wallet.fromPhrase(CYPRESS_MNEMONIC, provider) - const { default: HDWalletProvider } = await import('@truffle/hdwallet-provider') + setTimeout(() => { + chainChangedListeners.forEach((listener) => listener(numberToHex(Number(currentChainId)))) + }, 100) + } - const provider = new HDWalletProvider({ - mnemonic: CYPRESS_MNEMONIC, - providerOrUrl: getRpcServiceUrl(rpcUri), - }) + updateProvider() return { - provider: createEIP1193Provider(provider.engine, { - eth_requestAccounts: async () => provider.getAddresses(), - }), + provider: createEIP1193Provider( + { + on: (event: string, listener: (...args: any[]) => void) => { + if (event === 'accountsChanged') { + return + } else if (event === 'chainChanged') { + chainChangedListeners.add(listener) + } else { + provider.on(event, listener) + } + }, + + request: async (request: { method: string; params: any[] }) => { + return provider.send(request.method, request.params) + }, + }, + { + eth_chainId: async () => currentChainId, + + // @ts-ignore + eth_getCode: async ({ params }) => provider.getCode(params[0], params[1]), + + eth_accounts: async () => [wallet.address], + eth_requestAccounts: async () => [wallet.address], + + eth_call: async ({ params }: { params: any }) => wallet.call(params[0]), + + eth_sendTransaction: async ({ params }) => { + const tx = await wallet.sendTransaction(params[0] as any) + return tx.hash // return transaction hash + }, + + personal_sign: async ({ params }) => { + const signedMessage = wallet.signingKey.sign(params[0]) + return signedMessage.serialized + }, + + eth_signTypedData: async ({ params }) => { + const [, json] = params + const typedData = JSON.parse(json) + return await wallet.signTypedData( + typedData.domain, + { [typedData.primaryType]: typedData.types[typedData.primaryType] }, + typedData.message, + ) + }, + + // @ts-ignore + wallet_switchEthereumChain: async ({ params }) => { + updateProvider() + }, + }, + ), } }, platforms: ['desktop'], @@ -32,4 +96,4 @@ const e2eWalletModule = (rpcUri: ChainInfo['rpcUri']): WalletInit => { } } -export default e2eWalletModule +export default E2EWalletMoule diff --git a/src/tests/mocks/chains.ts b/src/tests/mocks/chains.ts index 795f466b90..f21904c274 100644 --- a/src/tests/mocks/chains.ts +++ b/src/tests/mocks/chains.ts @@ -43,7 +43,6 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ ], disabledWallets: ['lattice'], features: [ - FEATURES.CONTRACT_INTERACTION, FEATURES.DOMAIN_LOOKUP, FEATURES.EIP1559, FEATURES.ERC721, @@ -52,6 +51,10 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ FEATURES.SPENDING_LIMIT, FEATURES.TX_SIMULATION, ], + balancesProvider: { + chainName: null, + enabled: false, + }, }, { transactionService: 'https://safe-transaction.xdai.gnosis.io', @@ -94,7 +97,6 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ 'walletLink', ], features: [ - FEATURES.CONTRACT_INTERACTION, FEATURES.EIP1559, FEATURES.ERC721, FEATURES.SAFE_APPS, @@ -102,6 +104,10 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ FEATURES.SPENDING_LIMIT, FEATURES.TX_SIMULATION, ], + balancesProvider: { + chainName: null, + enabled: false, + }, }, { transactionService: 'https://safe-transaction.polygon.gnosis.io', @@ -150,7 +156,6 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ 'walletLink', ], features: [ - FEATURES.CONTRACT_INTERACTION, FEATURES.EIP1559, FEATURES.ERC721, FEATURES.SAFE_APPS, @@ -158,6 +163,10 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ FEATURES.SPENDING_LIMIT, FEATURES.TX_SIMULATION, ], + balancesProvider: { + chainName: null, + enabled: false, + }, }, { transactionService: 'https://safe-transaction.bsc.gnosis.io', @@ -203,13 +212,16 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ 'walletLink', ], features: [ - FEATURES.CONTRACT_INTERACTION, FEATURES.ERC721, FEATURES.SAFE_APPS, FEATURES.SAFE_TX_GAS_OPTIONAL, FEATURES.SPENDING_LIMIT, FEATURES.TX_SIMULATION, ], + balancesProvider: { + chainName: null, + enabled: false, + }, }, { transactionService: 'https://safe-transaction.ewc.gnosis.io', @@ -253,13 +265,16 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ 'walletLink', ], features: [ - FEATURES.CONTRACT_INTERACTION, FEATURES.DOMAIN_LOOKUP, FEATURES.ERC721, FEATURES.SAFE_APPS, FEATURES.SAFE_TX_GAS_OPTIONAL, FEATURES.SPENDING_LIMIT, ], + balancesProvider: { + chainName: null, + enabled: false, + }, }, { transactionService: 'https://safe-transaction.arbitrum.gnosis.io', @@ -301,13 +316,11 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ 'trust', 'walletLink', ], - features: [ - FEATURES.CONTRACT_INTERACTION, - FEATURES.ERC721, - FEATURES.SAFE_APPS, - FEATURES.SAFE_TX_GAS_OPTIONAL, - FEATURES.TX_SIMULATION, - ], + features: [FEATURES.ERC721, FEATURES.SAFE_APPS, FEATURES.SAFE_TX_GAS_OPTIONAL, FEATURES.TX_SIMULATION], + balancesProvider: { + chainName: null, + enabled: false, + }, }, { transactionService: 'https://safe-transaction.aurora.gnosis.io', @@ -351,6 +364,10 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ 'walletLink', ], features: [FEATURES.CONTRACT_INTERACTION, FEATURES.ERC721, FEATURES.SAFE_APPS, FEATURES.SAFE_TX_GAS_OPTIONAL], + balancesProvider: { + chainName: null, + enabled: false, + }, }, { transactionService: 'https://safe-transaction.avalanche.gnosis.io', @@ -398,7 +415,6 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ 'trust', ], features: [ - FEATURES.CONTRACT_INTERACTION, FEATURES.EIP1559, FEATURES.ERC721, FEATURES.SAFE_APPS, @@ -406,6 +422,10 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ FEATURES.SPENDING_LIMIT, FEATURES.TX_SIMULATION, ], + balancesProvider: { + chainName: null, + enabled: false, + }, }, { transactionService: 'https://safe-transaction.optimism.gnosis.io', @@ -447,13 +467,11 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ 'trust', 'walletLink', ], - features: [ - FEATURES.CONTRACT_INTERACTION, - FEATURES.ERC721, - FEATURES.SAFE_APPS, - FEATURES.SAFE_TX_GAS_OPTIONAL, - FEATURES.TX_SIMULATION, - ], + features: [FEATURES.ERC721, FEATURES.SAFE_APPS, FEATURES.SAFE_TX_GAS_OPTIONAL, FEATURES.TX_SIMULATION], + balancesProvider: { + chainName: null, + enabled: false, + }, }, { transactionService: 'https://safe-transaction.goerli.gnosis.io/', @@ -496,7 +514,6 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ 'walletLink', ], features: [ - FEATURES.CONTRACT_INTERACTION, FEATURES.DOMAIN_LOOKUP, FEATURES.EIP1559, FEATURES.ERC721, @@ -505,6 +522,10 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ FEATURES.SPENDING_LIMIT, FEATURES.TX_SIMULATION, ], + balancesProvider: { + chainName: null, + enabled: false, + }, }, { transactionService: 'https://safe-transaction.rinkeby.gnosis.io', @@ -537,7 +558,6 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ gasPrice: [{ type: GAS_PRICE_TYPE.FIXED, weiValue: '24000000000' }], disabledWallets: ['fortmatic', 'lattice', 'tally'], features: [ - FEATURES.CONTRACT_INTERACTION, FEATURES.DOMAIN_LOOKUP, FEATURES.EIP1559, FEATURES.ERC721, @@ -546,6 +566,10 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ FEATURES.SPENDING_LIMIT, FEATURES.TX_SIMULATION, ], + balancesProvider: { + chainName: null, + enabled: false, + }, }, { transactionService: 'https://safe-transaction.volta.gnosis.io', @@ -589,13 +613,16 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ 'walletLink', ], features: [ - FEATURES.CONTRACT_INTERACTION, FEATURES.DOMAIN_LOOKUP, FEATURES.ERC721, FEATURES.SAFE_APPS, FEATURES.SAFE_TX_GAS_OPTIONAL, FEATURES.SPENDING_LIMIT, ], + balancesProvider: { + chainName: null, + enabled: false, + }, }, ] diff --git a/src/tests/mocks/transactions.ts b/src/tests/mocks/transactions.ts index 427c3ad4b5..e70ae51ecd 100644 --- a/src/tests/mocks/transactions.ts +++ b/src/tests/mocks/transactions.ts @@ -23,6 +23,7 @@ const mockTransferInfo: TransferInfo = { tokenAddress: 'string', value: 'string', trusted: true, + imitation: false, } const mockTxInfo: TransactionInfo = { @@ -44,6 +45,7 @@ export const defaultTx: TransactionSummary = { confirmationsRequired: 2, confirmationsSubmitted: 2, }, + txHash: null, } export const getMockTx = ({ nonce }: { nonce?: number }): Transaction => { diff --git a/src/utils/__tests__/chains.test.ts b/src/utils/__tests__/chains.test.ts index 8a12c47cd6..ed8a0cf156 100644 --- a/src/utils/__tests__/chains.test.ts +++ b/src/utils/__tests__/chains.test.ts @@ -4,7 +4,7 @@ import { CONFIG_SERVICE_CHAINS } from '@/tests/mocks/chains' describe('chains', () => { describe('hasFeature', () => { it('returns true for a feature that exists', () => { - expect(hasFeature(CONFIG_SERVICE_CHAINS[0], FEATURES.CONTRACT_INTERACTION)).toBe(true) + expect(hasFeature(CONFIG_SERVICE_CHAINS[0], FEATURES.ERC721)).toBe(true) }) it("returns false for a feature that doesn't exists", () => { diff --git a/src/utils/__tests__/formatNumber.test.ts b/src/utils/__tests__/formatNumber.test.ts index 84f4c000f5..b3890ffc35 100644 --- a/src/utils/__tests__/formatNumber.test.ts +++ b/src/utils/__tests__/formatNumber.test.ts @@ -1,308 +1,61 @@ -import { formatAmount, formatAmountPrecise, formatCurrency } from '@/utils/formatNumber' +import { formatAmountPrecise, formatAmount, formatCurrency } from '@/utils/formatNumber' describe('formatNumber', () => { - describe('formatAmount', () => { - it('should remove trailing zeroes', () => { - expect(formatAmount('0.10000')).toEqual('0.1') - expect(formatAmount('0.100000000000')).toEqual('0.1') - }) - - it('should use maximum of 5 decimals', () => { - expect(formatAmount('0.123456789')).toEqual('0.12346') - }) - - it('should use five decimals for numbers up until 999.99999', () => { - expect(formatAmount('345.123456789')).toEqual('345.12346') // 9 decimals - expect(formatAmount('999.99999')).toEqual('999.99999') // 5 decimals - - // rounds above the specified limit - expect(formatAmount('999.999992')).toEqual('999.99999') // 6 decimals - expect(formatAmount('999.999996')).toEqual('1,000') - }) - - it('should use four decimals for numbers between 1,000.0001 until 9,999.9999', () => { - // rounds down past the specified precision - expect(formatAmount(1_000.00001)).toEqual('1,000') - - expect(formatAmount(1_000.0001234)).toEqual('1,000.0001') - expect(formatAmount(1_234.123456789)).toEqual('1,234.1235') - expect(formatAmount(9_999.9999)).toEqual('9,999.9999') - - // rounds above the specified limit - expect(formatAmount(9_999.99992)).toEqual('9,999.9999') - expect(formatAmount(9_999.99996)).toEqual('10,000') - }) - - it('should use three decimals for numbers between 10,000.001 until 99,999.999', () => { - // rounds down past the specified precision - expect(formatAmount(10_000.00001)).toEqual('10,000') - - expect(formatAmount(10_000.001)).toEqual('10,000.001') - expect(formatAmount(12_345.123456789)).toEqual('12,345.123') - expect(formatAmount(99_999.999)).toEqual('99,999.999') - - // rounds above the specified limit - expect(formatAmount(99_999.9992)).toEqual('99,999.999') - expect(formatAmount(99_999.9996)).toEqual('100,000') - }) - - it('should use two decimals for numbers between 100,000.01 until 999,999.99', () => { - // rounds down past the specified precision - expect(formatAmount(100_000.00001)).toEqual('100,000') - - expect(formatAmount(100_000.01)).toEqual('100,000.01') - expect(formatAmount(123_456.123456789)).toEqual('123,456.12') - expect(formatAmount(999_999.99)).toEqual('999,999.99') - - // rounds above the specified limit - expect(formatAmount(999_999.992)).toEqual('999,999.99') - expect(formatAmount(999_999.996)).toEqual('1,000,000') - }) - - it('should use one decimal for numbers between 1,000,000.1 until 9,999,999.9', () => { - // rounds down past the specified precision - expect(formatAmount(1_000_000.00001)).toEqual('1,000,000') - - expect(formatAmount(1_000_000.1)).toEqual('1,000,000.1') - expect(formatAmount(1_234_567.123456789)).toEqual('1,234,567.1') - expect(formatAmount(9_999_999.9)).toEqual('9,999,999.9') - - // rounds above the specified limit - expect(formatAmount(9_999_999.92)).toEqual('9,999,999.9') - expect(formatAmount(9_999_999.96)).toEqual('10,000,000') - }) - - it('should use no decimals for numbers between 10,000,000 and 99,999,999.5', () => { - // rounds down past the specified precision - expect(formatAmount(10_000_000.00001)).toEqual('10,000,000') - - expect(formatAmount(10_000_000.1)).toEqual('10,000,000') - expect(formatAmount(12_345_678.123456789)).toEqual('12,345,678') - expect(formatAmount(99_999_999)).toEqual('99,999,999') - - // rounds above the specified limit - expect(formatAmount(99_999_999.2)).toEqual('99,999,999') - expect(formatAmount(99_999_999.6)).toEqual('100M') - }) - - it('should use M symbol for numbers between 100,000,000 and 999,999,500', () => { - // rounds down past the specified precision - expect(formatAmount(100_000_000.00001)).toEqual('100M') - expect(formatAmount(100_000_100)).toEqual('100M') - - expect(formatAmount(100_001_000)).toEqual('100.001M') - expect(formatAmount(123_456_789.123456789)).toEqual('123.457M') - expect(formatAmount(999_999_000)).toEqual('999.999M') - - // rounds above the specified limit - expect(formatAmount(999_999_499)).toEqual('999.999M') - expect(formatAmount(999_999_500)).toEqual('1B') + describe('formatAmountPrecise', () => { + it('should format a number with a defined precision', () => { + expect(formatAmountPrecise(1234.5678, 2)).toBe('1,234.57') }) + }) - it('should use B symbol for numbers between 999,999,500 and 999,999,500,000', () => { - // rounds down past the specified precision - expect(formatAmount(1_000_000_000.00001)).toEqual('1B') - expect(formatAmount(1_000_100_000)).toEqual('1B') - - expect(formatAmount(1_100_000_000)).toEqual('1.1B') - expect(formatAmount(1_234_567_898.123456789)).toEqual('1.235B') - expect(formatAmount(100_001_000_500)).toEqual('100.001B') - expect(formatAmount(999_999_000_000)).toEqual('999.999B') - - // rounds above the specified limit - expect(formatAmount(999_999_499_999)).toEqual('999.999B') - expect(formatAmount(999_999_500_000)).toEqual('1T') + describe('formatAmount', () => { + it('should format a number below 0.0001', () => { + expect(formatAmount(0.000000009)).toBe('< 0.00001') }) - it('should use T notation for numbers between 999,999,500,000 and 999,000,000,000', () => { - // rounds down past the specified precision - expect(formatAmount(1_000_000_000_000.00001)).toEqual('1T') - expect(formatAmount(1_000_100_000_000)).toEqual('1T') - - expect(formatAmount(1_100_000_000_000)).toEqual('1.1T') - expect(formatAmount(1_234_567_898_765.123456789)).toEqual('1.235T') - expect(formatAmount(100_001_000_000_000)).toEqual('100.001T') - expect(formatAmount(999_999_000_000_000)).toEqual('> 999T') + it('should format a number below 1', () => { + expect(formatAmount(0.567811)).toBe('0.56781') }) - it('should use > 999T for numbers above 999,000,000,000,000', () => { - expect(formatAmount(999_000_000_000_001)).toEqual('> 999T') - expect(formatAmount(999_000_000_000_000.001)).toEqual('> 999T') + it('should format a number above 1', () => { + expect(formatAmount(285.1257657)).toBe('285.12577') }) - it('should use < 0.00001 for amounts smaller then 0.00001', () => { - expect(formatAmount(0.00001)).toEqual('0.00001') - expect(formatAmount(0.000014)).toEqual('0.00001') - expect(formatAmount(0.000015)).toEqual('0.00002') - expect(formatAmount(0.000001)).toEqual('< 0.00001') - expect(formatAmount(0.000009)).toEqual('< 0.00001') + it('should abbreviate a number with more than 10 digits', () => { + expect(formatAmount(12345678901)).toBe('12.35B') }) - it('should use < -0.00001 or < +0.00001 when the Eucledian distance of the amount is smaller than 0.00001', () => { - // to keep the '+' sign the amount shall be passed as a string - expect(formatAmount('+0.000001')).toEqual('< +0.00001') - expect(formatAmount('+0.000009')).toEqual('< +0.00001') - - // negative numbers will keep the sign either way - expect(formatAmount(-0.000001)).toEqual('< -0.00001') - expect(formatAmount(-0.000009)).toEqual('< -0.00001') - expect(formatAmount('-0.000001')).toEqual('< -0.00001') - expect(formatAmount('-0.000009')).toEqual('< -0.00001') + it('should abbreviate a number with more than a given amount of digits', () => { + expect(formatAmount(1234.12, 2, 4)).toBe('1.23K') }) }) describe('formatCurrency', () => { - it('returns the correct number of decimals', () => { - const amount1 = 0 - - expect(formatCurrency(amount1, 'JPY')).toBe('0 JPY') - expect(formatCurrency(amount1, 'IQD')).toBe('0 IQD') - expect(formatCurrency(amount1, 'USD')).toBe('0.00 USD') - expect(formatCurrency(amount1, 'EUR')).toBe('0.00 EUR') - expect(formatCurrency(amount1, 'GBP')).toBe('0.00 GBP') - expect(formatCurrency(amount1, 'BHD')).toBe('0.000 BHD') - - const amount2 = 1 - - expect(formatCurrency(amount2, 'JPY')).toBe('1 JPY') - expect(formatCurrency(amount2, 'IQD')).toBe('1 IQD') - expect(formatCurrency(amount2, 'USD')).toBe('1.00 USD') - expect(formatCurrency(amount2, 'EUR')).toBe('1.00 EUR') - expect(formatCurrency(amount2, 'GBP')).toBe('1.00 GBP') - expect(formatCurrency(amount2, 'BHD')).toBe('1.000 BHD') - - const amount3 = '1.7777' - - expect(formatCurrency(amount3, 'JPY')).toBe('2 JPY') - expect(formatCurrency(amount3, 'IQD')).toBe('2 IQD') - expect(formatCurrency(amount3, 'USD')).toBe('1.78 USD') - expect(formatCurrency(amount3, 'EUR')).toBe('1.78 EUR') - expect(formatCurrency(amount3, 'GBP')).toBe('1.78 GBP') - expect(formatCurrency(amount3, 'BHD')).toBe('1.778 BHD') - }) - - it('should drop decimals for values above 1k', () => { - // It should stop - expect(formatCurrency(999.99, 'USD')).toBe('999.99 USD') - expect(formatCurrency(1000.1, 'USD')).toBe('1,000 USD') - expect(formatCurrency(1000.99, 'USD')).toBe('1,001 USD') - expect(formatCurrency(32500.5, 'EUR')).toBe('32,501 EUR') - expect(formatCurrency(314285500.1, 'JPY')).toBe('314.286M JPY') + it('should format a 0', () => { + expect(formatCurrency(0, 'USD')).toBe('$ 0') }) - it('should use M symbol for numbers between 100,000,000 and 999,999,500', () => { - const amount1 = 100_000_100 - - expect(formatCurrency(amount1, 'JPY')).toBe('100M JPY') - - const amount2 = 123_456_789.123456789 - - expect(formatCurrency(amount2, 'JPY')).toBe('123.457M JPY') - - const amount3 = 999_999_500 - - expect(formatCurrency(amount3, 'JPY')).toBe('1B JPY') + it('should format a number below 1', () => { + expect(formatCurrency(0.5678, 'USD')).toBe('$ 0.57') }) - it('should use B symbol for numbers between 999,999,500 and 999,999,500,000', () => { - const amount1 = 1_000_000_000 - - expect(formatCurrency(amount1, 'JPY')).toBe('1B JPY') - - const amount2 = 1_234_567_898.123456789 - - expect(formatCurrency(amount2, 'JPY')).toBe('1.235B JPY') - - const amount3 = 999_999_500_000 - - expect(formatCurrency(amount3, 'JPY')).toBe('1T JPY') - }) - - it('should use T notation for numbers between 999,999,500,000 and 999,000,000,000', () => { - const amount1 = 1_000_100_000_000 - - expect(formatCurrency(amount1, 'JPY')).toBe('1T JPY') - - const amount2 = 1_234_567_898_765.123456789 - - expect(formatCurrency(amount2, 'JPY')).toBe('1.235T JPY') - - const amount3 = 999_999_000_000_000 - - expect(formatCurrency(amount3, 'JPY')).toBe('> 999T JPY') + it('should format a number above 1', () => { + expect(formatCurrency(285.1257657, 'EUR')).toBe('€ 285') }) - it('should use > 999T for numbers above 999,000,000,000,000', () => { - const amount1 = 999_000_000_000_001 - - expect(formatCurrency(amount1, 'JPY')).toBe('> 999T JPY') + it('should abbreviate billions', () => { + expect(formatCurrency(12_345_678_901, 'USD')).toBe('$ 12.35B') }) - it('should use < - smallest denomination or < + smallest denomination when amounts are smaller than the smallest denomination', () => { - const amount = 0.000001 - - expect(formatCurrency(amount, 'JPY')).toBe('< 1 JPY') - expect(formatCurrency(amount, 'IQD')).toBe('< 1 IQD') - expect(formatCurrency(amount, 'USD')).toBe('< 0.01 USD') - expect(formatCurrency(amount, 'EUR')).toBe('< 0.01 EUR') - expect(formatCurrency(amount, 'GBP')).toBe('< 0.01 GBP') - expect(formatCurrency(amount, 'BHD')).toBe('< 0.001 BHD') - - // Preserves sign if specified - const amount2 = '+0.000001' - - expect(formatCurrency(amount2, 'JPY')).toBe('< +1 JPY') - expect(formatCurrency(amount2, 'IQD')).toBe('< +1 IQD') - expect(formatCurrency(amount2, 'USD')).toBe('< +0.01 USD') - expect(formatCurrency(amount2, 'EUR')).toBe('< +0.01 EUR') - expect(formatCurrency(amount2, 'GBP')).toBe('< +0.01 GBP') - expect(formatCurrency(amount2, 'BHD')).toBe('< +0.001 BHD') - - const amount3 = -0.000009 - - expect(formatCurrency(amount3, 'JPY')).toBe('< -1 JPY') - expect(formatCurrency(amount3, 'IQD')).toBe('< -1 IQD') - expect(formatCurrency(amount3, 'USD')).toBe('< -0.01 USD') - expect(formatCurrency(amount3, 'EUR')).toBe('< -0.01 EUR') - expect(formatCurrency(amount3, 'GBP')).toBe('< -0.01 GBP') - expect(formatCurrency(amount3, 'BHD')).toBe('< -0.001 BHD') - - const amount4 = '-0.000009' - - expect(formatCurrency(amount4, 'JPY')).toBe('< -1 JPY') - expect(formatCurrency(amount4, 'IQD')).toBe('< -1 IQD') - expect(formatCurrency(amount4, 'USD')).toBe('< -0.01 USD') - expect(formatCurrency(amount4, 'EUR')).toBe('< -0.01 EUR') - expect(formatCurrency(amount4, 'GBP')).toBe('< -0.01 GBP') - expect(formatCurrency(amount4, 'BHD')).toBe('< -0.001 BHD') + it('should abbreviate millions', () => { + expect(formatCurrency(9_589_009.543645, 'EUR')).toBe('€ 9.59M') }) - }) - - describe('formatAmountPrecise', () => { - it('should format amounts without the compact notation', () => { - const tokenDecimals = 18 - - const amount1 = 100_000_000.00001 // 100M - expect(formatAmountPrecise(amount1, tokenDecimals)).toEqual('100,000,000.00001') - const amount2 = 1_000_000_000.00001 // 1B - expect(formatAmountPrecise(amount2, tokenDecimals)).toEqual('1,000,000,000.00001') - - const amount3 = 1_234_567_898.123456789 // 1.235B - expect(formatAmountPrecise(amount3, tokenDecimals)).toEqual('1,234,567,898.1234567') + it('should abbreviate thousands', () => { + expect(formatCurrency(119_589.543645, 'EUR')).toBe('€ 119.59K') }) - it('should preserve the max fraction digits', () => { - const tokenDecimals = 18 - - const amount1 = 0.000001 // < 0.00001 - expect(formatAmountPrecise(amount1, tokenDecimals)).toEqual('0.000001') - - const amount2 = 0.00000123456789 // 14 decimals - expect(formatAmountPrecise(amount2, tokenDecimals)).toEqual('0.00000123456789') // 14 decimals - - const amount3 = 0.00000123456789012345 // 20 decimals - expect(formatAmountPrecise(amount3, tokenDecimals)).toEqual('0.000001234567890123') // 18 decimals + it('should abbreviate a number with more than a given amount of digits', () => { + expect(formatCurrency(1234.12, 'USD', 4)).toBe('$ 1.23K') }) }) }) diff --git a/src/utils/__tests__/tx-history-filter.test.ts b/src/utils/__tests__/tx-history-filter.test.ts index 05115d8deb..956715118d 100644 --- a/src/utils/__tests__/tx-history-filter.test.ts +++ b/src/utils/__tests__/tx-history-filter.test.ts @@ -394,7 +394,7 @@ describe('tx-history-filter', () => { expect(getIncomingTransfers).toHaveBeenCalledWith( '4', '0x123', - { value: '123', executed: undefined, timezone_offset: 3600000, trusted: false }, + { value: '123', executed: undefined, timezone_offset: 3600000, trusted: false, imitation: false }, 'pageUrl1', ) @@ -422,6 +422,7 @@ describe('tx-history-filter', () => { executed: 'true', timezone_offset: 3600000, trusted: false, + imitation: false, }, 'pageUrl2', ) @@ -442,7 +443,7 @@ describe('tx-history-filter', () => { expect(getModuleTransactions).toHaveBeenCalledWith( '1', '0x789', - { to: '0x123', executed: undefined, timezone_offset: 3600000, trusted: false }, + { to: '0x123', executed: undefined, timezone_offset: 3600000, trusted: false, imitation: false }, 'pageUrl3', ) diff --git a/src/utils/__tests__/tx-list.test.ts b/src/utils/__tests__/tx-list.test.ts index 340cea2f7b..db7ca71c0f 100644 --- a/src/utils/__tests__/tx-list.test.ts +++ b/src/utils/__tests__/tx-list.test.ts @@ -2,7 +2,7 @@ import { faker } from '@faker-js/faker' import { TransactionInfoType } from '@safe-global/safe-gateway-typescript-sdk' import type { TransactionListItem } from '@safe-global/safe-gateway-typescript-sdk' -import { groupConflictingTxs, groupRecoveryTransactions, _getRecoveryCancellations } from '@/utils/tx-list' +import { groupTxs, groupRecoveryTransactions, _getRecoveryCancellations } from '@/utils/tx-list' describe('tx-list', () => { describe('groupConflictingTxs', () => { @@ -25,7 +25,7 @@ describe('tx-list', () => { }, ] - const result = groupConflictingTxs(list as TransactionListItem[]) + const result = groupTxs(list as TransactionListItem[]) expect(result).toEqual([ [ { @@ -67,7 +67,7 @@ describe('tx-list', () => { }, ] - const result = groupConflictingTxs(list as TransactionListItem[]) + const result = groupTxs(list as TransactionListItem[]) expect(result).toEqual([ [ { @@ -90,12 +90,13 @@ describe('tx-list', () => { ]) }) - it('should return non-conflicting transaction lists as is', () => { + it('should group transactions with the same txHash (bulk txs)', () => { const list = [ { type: 'TRANSACTION', transaction: { id: 1, + txHash: '0x123', }, conflictType: 'None', }, @@ -103,12 +104,72 @@ describe('tx-list', () => { type: 'TRANSACTION', transaction: { id: 2, + txHash: '0x123', + }, + conflictType: 'None', + }, + { + type: 'TRANSACTION', + transaction: { + id: 3, + txHash: '0x456', + }, + conflictType: 'None', + }, + ] + + const result = groupTxs(list as unknown as TransactionListItem[]) + expect(result).toEqual([ + [ + { + type: 'TRANSACTION', + transaction: { + id: 1, + txHash: '0x123', + }, + conflictType: 'None', + }, + { + type: 'TRANSACTION', + transaction: { + id: 2, + txHash: '0x123', + }, + conflictType: 'None', + }, + ], + { + type: 'TRANSACTION', + transaction: { + id: 3, + txHash: '0x456', + }, + conflictType: 'None', + }, + ]) + }) + + it('should return non-conflicting, and non bulk transaction lists as is', () => { + const list = [ + { + type: 'TRANSACTION', + transaction: { + id: 1, + txHash: '0x123', + }, + conflictType: 'None', + }, + { + type: 'TRANSACTION', + transaction: { + id: 2, + txHash: '0x345', }, conflictType: 'None', }, ] - const result = groupConflictingTxs(list as unknown as TransactionListItem[]) + const result = groupTxs(list as unknown as TransactionListItem[]) expect(result).toEqual(list) }) }) diff --git a/src/utils/chains.ts b/src/utils/chains.ts index b4eeca7477..ae181213cc 100644 --- a/src/utils/chains.ts +++ b/src/utils/chains.ts @@ -5,7 +5,6 @@ import { getExplorerLink } from './gateway' export enum FEATURES { ERC721 = 'ERC721', SAFE_APPS = 'SAFE_APPS', - CONTRACT_INTERACTION = 'CONTRACT_INTERACTION', DOMAIN_LOOKUP = 'DOMAIN_LOOKUP', SPENDING_LIMIT = 'SPENDING_LIMIT', EIP1559 = 'EIP1559', @@ -18,17 +17,22 @@ export enum FEATURES { PUSH_NOTIFICATIONS = 'PUSH_NOTIFICATIONS', NATIVE_WALLETCONNECT = 'NATIVE_WALLETCONNECT', RECOVERY = 'RECOVERY', - SOCIAL_LOGIN = 'SOCIAL_LOGIN', COUNTERFACTUAL = 'COUNTERFACTUAL', DELETE_TX = 'DELETE_TX', SPEED_UP_TX = 'SPEED_UP_TX', SAP_BANNER = 'SAP_BANNER', NATIVE_SWAPS = 'NATIVE_SWAPS', + NATIVE_SWAPS_USE_COW_STAGING_SERVER = 'NATIVE_SWAPS_USE_COW_STAGING_SERVER', + NATIVE_SWAPS_FEE_ENABLED = 'NATIVE_SWAPS_FEE_ENABLED', + RELAY_NATIVE_SWAPS = 'RELAY_NATIVE_SWAPS', + ZODIAC_ROLES = 'ZODIAC_ROLES', } export const FeatureRoutes = { [AppRoutes.apps.index]: FEATURES.SAFE_APPS, [AppRoutes.swap]: FEATURES.NATIVE_SWAPS, + [AppRoutes.balances.nfts]: FEATURES.ERC721, + [AppRoutes.settings.notifications]: FEATURES.PUSH_NOTIFICATIONS, } export const hasFeature = (chain: ChainInfo, feature: FEATURES): boolean => { @@ -43,3 +47,9 @@ export const getBlockExplorerLink = ( return getExplorerLink(address, chain.blockExplorerUriTemplate) } } + +export const isRouteEnabled = (route: string, chain?: ChainInfo) => { + if (!chain) return false + const featureRoute = FeatureRoutes[route] + return !featureRoute || hasFeature(chain, featureRoute) +} diff --git a/src/utils/formatNumber.ts b/src/utils/formatNumber.ts index b0cb100983..f0d0a1b202 100644 --- a/src/utils/formatNumber.ts +++ b/src/utils/formatNumber.ts @@ -1,107 +1,28 @@ -import memoize from 'lodash/memoize' - -// These follow the guideline of "How to format amounts" -// https://github.com/5afe/safe/wiki/How-to-format-amounts - -const LOWER_LIMIT = 0.00001 -const COMPACT_LIMIT = 99_999_999.5 -const UPPER_LIMIT = 999 * 10 ** 12 -const NO_DECIMALS_LIMIT = 1000 +const locale = typeof navigator !== 'undefined' ? navigator.language : undefined /** - * Formatter that restricts the upper and lower limit of numbers that can be formatted + * Intl.NumberFormat number formatter that adheres to our style guide * @param number Number to format - * @param formatter Function to format number - * @param minimum Minimum number to format */ -const format = (number: string | number, formatter: (float: number) => string, minimum = LOWER_LIMIT) => { - const float = Number(number) - - if (float === 0) { - return formatter(float) - } - - if (Math.abs(float) < minimum) { - return `< ${formatter(minimum * Math.sign(float))}` - } - - if (float < UPPER_LIMIT) { - return formatter(float) - } - - return `> ${formatter(UPPER_LIMIT)}` -} - -// Universal amount formatting options - -const getNumberFormatNotation = (number: string | number): Intl.NumberFormatOptions['notation'] => { - return Number(number) >= COMPACT_LIMIT ? 'compact' : undefined -} - -const getNumberFormatSignDisplay = (number: string | number): Intl.NumberFormatOptions['signDisplay'] => { - const shouldDisplaySign = typeof number === 'string' ? number.trim().startsWith('+') : Number(number) < 0 - return shouldDisplaySign ? 'exceptZero' : undefined -} - -// Amount formatting options - -const getAmountFormatterMaxFractionDigits = ( - number: string | number, -): Intl.NumberFormatOptions['maximumFractionDigits'] => { +export const formatAmount = (number: string | number, precision = 5, maxLength = 6): string => { const float = Number(number) + if (float === 0) return '0' + if (float === Math.round(float)) precision = 0 + if (Math.abs(float) < 0.00001) return '< 0.00001' - if (float < 1_000) { - return 5 - } - - if (float < 10_000) { - return 4 - } - - if (float < 100_000) { - return 3 - } - - if (float < 1_000_000) { - return 2 - } - - if (float < 10_000_000) { - return 1 - } - - if (float < COMPACT_LIMIT) { - return 0 - } - - // Represents numbers like 767.343M - if (float < UPPER_LIMIT) { - return 3 - } - - return 0 -} - -const getAmountFormatterOptions = (number: string | number): Intl.NumberFormatOptions => { - return { - maximumFractionDigits: getAmountFormatterMaxFractionDigits(number), - notation: getNumberFormatNotation(number), - signDisplay: getNumberFormatSignDisplay(number), - } -} + const fullNum = new Intl.NumberFormat(locale, { + style: 'decimal', + maximumFractionDigits: precision, + }).format(float) -/** - * Intl.NumberFormat number formatter that adheres to our style guide - * @param number Number to format - */ -export const formatAmount = (number: string | number, precision?: number): string => { - const options = getAmountFormatterOptions(number) - if (precision !== undefined) { - options.maximumFractionDigits = precision - } - const formatter = new Intl.NumberFormat(undefined, options) + // +3 for the decimal point and the two decimal places + if (fullNum.length <= maxLength + 3) return fullNum - return format(number, formatter.format) + return new Intl.NumberFormat(locale, { + style: 'decimal', + notation: 'compact', + maximumFractionDigits: 2, + }).format(float) } /** @@ -109,59 +30,11 @@ export const formatAmount = (number: string | number, precision?: number): strin * @param number Number to format * @param precision Fraction digits to show */ -export const formatAmountPrecise = (number: string | number, precision: number): string => { - const float = Number(number) - - const formatter = new Intl.NumberFormat(undefined, { +export const formatAmountPrecise = (number: string | number, precision?: number): string => { + return new Intl.NumberFormat(locale, { + style: 'decimal', maximumFractionDigits: precision, - }) - - return formatter.format(float) -} - -// Fiat formatting - -const getMinimumCurrencyDenominator = memoize((currency: string): number => { - const BASE_VALUE = 1 - - const formatter = new Intl.NumberFormat(undefined, { - style: 'currency', - currency, - }) - - const fraction = formatter.formatToParts(BASE_VALUE).find(({ type }) => type === 'fraction') - - // Currencies may not have decimals, i.e. JPY - return fraction ? Number(`0.${'1'.padStart(fraction.value.length, '0')}`) : 1 -}) - -const getCurrencyFormatterMaxFractionDigits = ( - number: string | number, - currency: string, -): Intl.NumberFormatOptions['maximumFractionDigits'] => { - const float = Number(number) - - if (float < NO_DECIMALS_LIMIT) { - const [, decimals] = getMinimumCurrencyDenominator(currency).toString().split('.') - return decimals?.length ?? 0 - } - - if (float >= COMPACT_LIMIT) { - return 3 - } - - return 0 -} - -const getCurrencyFormatterOptions = (number: string | number, currency: string): Intl.NumberFormatOptions => { - return { - maximumFractionDigits: getCurrencyFormatterMaxFractionDigits(number, currency), - notation: getNumberFormatNotation(number), - signDisplay: getNumberFormatSignDisplay(number), - style: 'currency', - currency, - currencyDisplay: 'code', - } + }).format(Number(number)) } /** @@ -169,42 +42,26 @@ const getCurrencyFormatterOptions = (number: string | number, currency: string): * @param number Number to format * @param currency ISO 4217 currency code */ -export const formatCurrency = (number: string | number, currency: string): string => { - // Note: we will be able to achieve the following once the `roundingMode` option is supported - // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#parameters - - const minimum = getMinimumCurrencyDenominator(currency) - - const currencyFormatter = (float: number): string => { - const options = getCurrencyFormatterOptions(number, currency) - const formatter = new Intl.NumberFormat(undefined, options) - - const parts = formatter.formatToParts(float) // Returns an array of objects with `type` and `value` properties +export const formatCurrency = (number: string | number, currency: string, maxLength = 6): string => { + let float = Number(number) - const fraction = parts.find(({ type }) => type === 'fraction') - - const amount = parts - .filter(({ type }) => type !== 'currency' && type !== 'literal') // Remove currency code and whitespace - .map((part) => { - if (float >= 0) { - return part - } - - if (fraction && part.type === 'fraction') { - return { ...part, value: '1'.padStart(fraction.value.length, '0') } - } - - if (!fraction && part.type === 'integer') { - return { ...part, value: minimum.toString() } - } - - return part - }) - .reduce((acc, { value }) => acc + value, '') - .trim() - - return `${amount} ${currency.toUpperCase()}` - } - - return format(number, currencyFormatter, minimum) + let result = new Intl.NumberFormat(locale, { + style: 'currency', + currency, + currencyDisplay: 'narrowSymbol', + maximumFractionDigits: Math.abs(float) >= 1 || float === 0 ? 0 : 2, + }).format(float) + + // +1 for the currency symbol + if (result.length > maxLength + 1) { + result = new Intl.NumberFormat(locale, { + style: 'currency', + currency, + currencyDisplay: 'narrowSymbol', + notation: 'compact', + maximumFractionDigits: 2, + }).format(float) + } + + return result.replace(/^(\D+)/, '$1 ') } diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts index a74fab0143..bbff73c66b 100644 --- a/src/utils/formatters.ts +++ b/src/utils/formatters.ts @@ -1,6 +1,6 @@ import type { BigNumberish } from 'ethers' import { formatUnits, parseUnits } from 'ethers' -import { formatAmount } from './formatNumber' +import { formatAmount, formatAmountPrecise } from './formatNumber' const GWEI = 'gwei' @@ -38,7 +38,8 @@ export const formatVisualAmount = ( decimals: number | string = GWEI, precision?: number, ): string => { - return formatAmount(safeFormatUnits(value, decimals), precision) + const amount = safeFormatUnits(value, decimals) + return precision ? formatAmountPrecise(amount, precision) : formatAmount(amount) } export const safeParseUnits = (value: string, decimals: number | string = GWEI): bigint | undefined => { diff --git a/src/utils/gateway.ts b/src/utils/gateway.ts index a6bb8e31c5..12c657f016 100644 --- a/src/utils/gateway.ts +++ b/src/utils/gateway.ts @@ -1,6 +1,5 @@ import type { JsonRpcSigner } from 'ethers' import { type ChainInfo, deleteTransaction } from '@safe-global/safe-gateway-typescript-sdk' -import { WC_APP_PROD, WC_APP_DEV } from '@/config/constants' import { signTypedData } from './web3' export const _replaceTemplate = (uri: string, data: Record): string => { @@ -30,10 +29,6 @@ export const getExplorerLink = ( return { href, title } } -export const isWalletConnectSafeApp = (url: string): boolean => { - return url === WC_APP_PROD.url || url === WC_APP_DEV.url -} - const signTxServiceMessage = async ( chainId: string, safeAddress: string, diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 0c90822e51..a66781819d 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -2,6 +2,7 @@ import type { ConnectedWallet } from '@/hooks/wallets/useOnboard' import type { SafeTransaction } from '@safe-global/safe-core-sdk-types' import type { OnboardAPI } from '@web3-onboard/core' +import type { Eip1193Provider } from 'ethers' export function invariant(condition: T, error: string): asserts condition { if (condition) { @@ -22,3 +23,7 @@ export function assertWallet(wallet: ConnectedWallet | null): asserts wallet { export function assertOnboard(onboard: OnboardAPI | undefined): asserts onboard { return invariant(onboard, 'Onboard not connected') } + +export function assertProvider(provider: Eip1193Provider | undefined): asserts provider { + return invariant(provider, 'Provider not found') +} diff --git a/src/utils/safe-messages.ts b/src/utils/safe-messages.ts index c6e3764e1c..27cd99d585 100644 --- a/src/utils/safe-messages.ts +++ b/src/utils/safe-messages.ts @@ -11,8 +11,8 @@ import { type SafeMessage, type EIP712TypedData, type ChainInfo, - FEATURES, } from '@safe-global/safe-gateway-typescript-sdk' +import { FEATURES } from '@/utils/chains' import { hasFeature } from './chains' import { asError } from '@/services/exceptions/utils' diff --git a/src/utils/transaction-guards.ts b/src/utils/transaction-guards.ts index 5b622126ec..a92fe19a12 100644 --- a/src/utils/transaction-guards.ts +++ b/src/utils/transaction-guards.ts @@ -1,10 +1,12 @@ import type { AddressEx, + BaselineConfirmationView, Cancellation, ConflictHeader, Creation, Custom, DateLabel, + DecodedDataResponse, DetailedExecutionInfo, Erc20Transfer, Erc721Transfer, @@ -16,27 +18,30 @@ import type { MultisigExecutionDetails, MultisigExecutionInfo, NativeCoinTransfer, + Order, + OrderConfirmationView, SafeInfo, SettingsChange, + SwapOrder, + SwapOrderConfirmationView, Transaction, TransactionInfo, TransactionListItem, TransactionSummary, Transfer, TransferInfo, - SwapOrder, - DecodedDataResponse, - BaselineConfirmationView, - CowSwapConfirmationView, + TwapOrder, + TwapOrderConfirmationView, } from '@safe-global/safe-gateway-typescript-sdk' -import { TransferDirection } from '@safe-global/safe-gateway-typescript-sdk' import { + ConfirmationViewTypes, ConflictType, DetailedExecutionInfoType, TransactionInfoType, TransactionListItemType, TransactionStatus, TransactionTokenType, + TransferDirection, } from '@safe-global/safe-gateway-typescript-sdk' import { getSpendingLimitModuleAddress } from '@/services/contracts/spendingLimitContracts' import { sameAddress } from '@/utils/addresses' @@ -73,7 +78,17 @@ export const isModuleDetailedExecutionInfo = (value?: DetailedExecutionInfo): va // TransactionInfo type guards export const isTransferTxInfo = (value: TransactionInfo): value is Transfer => { - return value.type === TransactionInfoType.TRANSFER + return value.type === TransactionInfoType.TRANSFER || isSwapTransferOrderTxInfo(value) +} + +/** + * A fulfillment transaction for swap, limit or twap order is always a SwapOrder + * It cannot be a TWAP order + * + * @param value + */ +export const isSwapTransferOrderTxInfo = (value: TransactionInfo): value is SwapOrder => { + return value.type === TransactionInfoType.SWAP_TRANSFER } export const isSettingsChangeTxInfo = (value: TransactionInfo): value is SettingsChange => { @@ -92,26 +107,50 @@ export const isMultiSendTxInfo = (value: TransactionInfo): value is MultiSend => ) } -export const isSwapTxInfo = (value: TransactionInfo): value is SwapOrder => { +export const isOrderTxInfo = (value: TransactionInfo): value is Order => { + return isSwapOrderTxInfo(value) || isTwapOrderTxInfo(value) +} + +export const isSwapOrderTxInfo = (value: TransactionInfo): value is SwapOrder => { return value.type === TransactionInfoType.SWAP_ORDER } +export const isTwapOrderTxInfo = (value: TransactionInfo): value is TwapOrder => { + return value.type === TransactionInfoType.TWAP_ORDER +} + +export const isConfirmationViewOrder = ( + decodedData: DecodedDataResponse | BaselineConfirmationView | OrderConfirmationView | undefined, +): decodedData is OrderConfirmationView => { + return isSwapConfirmationViewOrder(decodedData) || isTwapConfirmationViewOrder(decodedData) +} + +export const isTwapConfirmationViewOrder = ( + decodedData: DecodedDataResponse | BaselineConfirmationView | OrderConfirmationView | undefined, +): decodedData is TwapOrderConfirmationView => { + if (decodedData && 'type' in decodedData) { + return decodedData.type === ConfirmationViewTypes.COW_SWAP_TWAP_ORDER + } + + return false +} + export const isSwapConfirmationViewOrder = ( - decodedData: DecodedDataResponse | BaselineConfirmationView | CowSwapConfirmationView | undefined, -): decodedData is CowSwapConfirmationView => { + decodedData: DecodedDataResponse | BaselineConfirmationView | OrderConfirmationView | undefined, +): decodedData is SwapOrderConfirmationView => { if (decodedData && 'type' in decodedData) { - return decodedData.type === 'COW_SWAP_ORDER' + return decodedData.type === ConfirmationViewTypes.COW_SWAP_ORDER } return false } -export const isCancelledSwap = (value: TransactionInfo) => { - return isSwapTxInfo(value) && value.status === 'cancelled' +export const isCancelledSwapOrder = (value: TransactionInfo) => { + return isSwapOrderTxInfo(value) && value.status === 'cancelled' } -export const isOpenSwap = (value: TransactionInfo) => { - return isSwapTxInfo(value) && value.status === 'open' +export const isOpenSwapOrder = (value: TransactionInfo) => { + return isSwapOrderTxInfo(value) && value.status === 'open' } export const isCancellationTxInfo = (value: TransactionInfo): value is Cancellation => { diff --git a/src/utils/transactions.ts b/src/utils/transactions.ts index 4ef03485a4..97c2b028b9 100644 --- a/src/utils/transactions.ts +++ b/src/utils/transactions.ts @@ -86,6 +86,7 @@ export const makeTxFromDetails = (txDetails: TransactionDetails): Transaction => txInfo: txDetails.txInfo, executionInfo, safeAppInfo: txDetails?.safeAppInfo, + txHash: txDetails?.txHash || null, }, conflictType: ConflictType.NONE, } @@ -297,3 +298,7 @@ export const isTrustedTx = (tx: TransactionSummary) => { Boolean(tx.txInfo.transferInfo.trusted) ) } + +export const isImitation = ({ txInfo }: TransactionSummary): boolean => { + return isTransferTxInfo(txInfo) && isERC20Transfer(txInfo.transferInfo) && Boolean(txInfo.transferInfo.imitation) +} diff --git a/src/utils/tx-history-filter.ts b/src/utils/tx-history-filter.ts index bad2691c00..bcc4c5ba47 100644 --- a/src/utils/tx-history-filter.ts +++ b/src/utils/tx-history-filter.ts @@ -127,6 +127,7 @@ export const fetchFilteredTxHistory = async ( ...filterData.filter, timezone_offset: getTimezoneOffset(), trusted: onlyTrusted ?? false, + imitation: onlyTrusted ?? false, executed: filterData.type === TxFilterType.MULTISIG ? 'true' : undefined, } diff --git a/src/utils/tx-link.ts b/src/utils/tx-link.ts new file mode 100644 index 0000000000..4b7635efc1 --- /dev/null +++ b/src/utils/tx-link.ts @@ -0,0 +1,17 @@ +import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' +import type { LinkProps } from 'next/link' +import { AppRoutes } from '@/config/routes' + +export const getTxLink = ( + txId: string, + chain: ChainInfo, + safeAddress: string, +): { href: LinkProps['href']; title: string } => { + return { + href: { + pathname: AppRoutes.transactions.tx, + query: { id: txId, safe: `${chain?.shortName}:${safeAddress}` }, + }, + title: 'View transaction', + } +} diff --git a/src/utils/tx-list.ts b/src/utils/tx-list.ts index f9737be4be..f54b89bf0b 100644 --- a/src/utils/tx-list.ts +++ b/src/utils/tx-list.ts @@ -7,6 +7,11 @@ import type { RecoveryQueueItem } from '@/features/recovery/services/recovery-st type GroupedTxs = Array +export const groupTxs = (list: TransactionListItem[]) => { + const groupedByConflicts = groupConflictingTxs(list) + return groupBulkTxs(groupedByConflicts) +} + /** * Group txs by conflict header */ @@ -33,6 +38,31 @@ export const groupConflictingTxs = (list: TransactionListItem[]): GroupedTxs => }) } +/** + * Group txs by tx hash + */ +const groupBulkTxs = (list: GroupedTxs): GroupedTxs => { + return list + .reduce((resultItems, item) => { + if (Array.isArray(item) || !isTransactionListItem(item)) { + return resultItems.concat([item]) + } + const currentTxHash = item.transaction.txHash + + const prevItem = resultItems[resultItems.length - 1] + if (!Array.isArray(prevItem)) return resultItems.concat([[item]]) + const prevTxHash = prevItem[0].transaction.txHash + + if (currentTxHash && currentTxHash === prevTxHash) { + prevItem.push(item) + return resultItems + } + + return resultItems.concat([[item]]) + }, []) + .map((item) => (Array.isArray(item) && item.length === 1 ? item[0] : item)) +} + export function _getRecoveryCancellations(moduleAddress: string, transactions: Array) { const CANCELLATION_TX_METHOD_NAME = 'setTxNonce' diff --git a/src/utils/wallets.ts b/src/utils/wallets.ts index 97493744bb..9a2469e4d9 100644 --- a/src/utils/wallets.ts +++ b/src/utils/wallets.ts @@ -3,7 +3,7 @@ import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' import { getWeb3ReadOnly, isSmartContract } from '@/hooks/wallets/web3' import { WALLET_KEYS } from '@/hooks/wallets/consts' import memoize from 'lodash/memoize' -import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' +import { PRIVATE_KEY_MODULE_LABEL } from '@/services/private-key-module' const WALLETCONNECT = 'WalletConnect' @@ -48,6 +48,8 @@ export const isSmartContractWallet = memoize( /* Check if the wallet is unlocked. */ export const isWalletUnlocked = async (walletName: string): Promise => { + if (walletName === PRIVATE_KEY_MODULE_LABEL) return true + const METAMASK_LIKE = ['MetaMask', 'Rabby Wallet', 'Zerion'] // Only MetaMask exposes a method to check if the wallet is unlocked @@ -60,8 +62,5 @@ export const isWalletUnlocked = async (walletName: string): Promise=12.12.47" @@ -3561,7 +3497,7 @@ dependencies: "@types/mdx" "^2.0.0" -"@metamask/eth-sig-util@4.0.1", "@metamask/eth-sig-util@^4.0.0": +"@metamask/eth-sig-util@^4.0.0": version "4.0.1" resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-4.0.1.tgz#3ad61f6ea9ad73ba5b19db780d40d9aae5157088" integrity sha512-tghyZKLHZjcdlDqCA3gNZmLeR0XvOE9U1qoQO9ohyAZT6Pya+H9vkBPcsyXytmYLNgVoin7CKCmweo/R43V+tQ== @@ -3572,18 +3508,6 @@ tweetnacl "^1.0.3" tweetnacl-util "^0.15.1" -"@metamask/eth-sig-util@^5.0.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-5.1.0.tgz#a47f62800ee1917fef976ba67544a0ccd7d1bd6b" - integrity sha512-mlgziIHYlA9pi/XZerChqg4NocdOgBPB9NmxgXWQO2U2hH8RGOJQrz6j/AIKkYxgCMIE2PY000+joOwXfzeTDQ== - dependencies: - "@ethereumjs/util" "^8.0.6" - bn.js "^4.12.0" - ethereum-cryptography "^2.0.0" - ethjs-util "^0.1.6" - tweetnacl "^1.0.3" - tweetnacl-util "^0.15.1" - "@metamask/obs-store@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@metamask/obs-store/-/obs-store-7.0.0.tgz#6cae5f28306bb3e83a381bc9ae22682316095bd3" @@ -3592,22 +3516,6 @@ "@metamask/safe-event-emitter" "^2.0.0" through2 "^2.0.3" -"@metamask/rpc-errors@^5.1.1": - version "5.1.1" - resolved "https://registry.yarnpkg.com/@metamask/rpc-errors/-/rpc-errors-5.1.1.tgz#f82732ad0952d34d219eca42699c0c74bee95a9e" - integrity sha512-JjZnDi2y2CfvbohhBl+FOQRzmFlJpybcQlIk37zEX8B96eVSPbH/T8S0p7cSF8IE33IWx6JkD8Ycsd+2TXFxCw== - dependencies: - "@metamask/utils" "^5.0.0" - fast-safe-stringify "^2.0.6" - -"@metamask/rpc-errors@^6.0.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@metamask/rpc-errors/-/rpc-errors-6.1.0.tgz#dfdef7cba4b9ad01ca3f99e990b5980575b89b4f" - integrity sha512-JQElKxai26FpDyRKO/yH732wI+BV90i1u6pOuDOpdADSbppB2g1pPh3AGST1zkZqEE9eIKIUw8UdBQ4rp3VTSg== - dependencies: - "@metamask/utils" "^8.1.0" - fast-safe-stringify "^2.0.6" - "@metamask/safe-event-emitter@2.0.0", "@metamask/safe-event-emitter@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz#af577b477c683fad17c619a78208cede06f9605c" @@ -3623,29 +3531,6 @@ semver "^7.3.8" superstruct "^1.0.3" -"@metamask/utils@^5.0.0": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-5.0.2.tgz#140ba5061d90d9dac0280c19cab101bc18c8857c" - integrity sha512-yfmE79bRQtnMzarnKfX7AEJBwFTxvTyw3nBQlu/5rmGXrjAeAMltoGxO62TFurxrQAFMNa/fEjIHNvungZp0+g== - dependencies: - "@ethereumjs/tx" "^4.1.2" - "@types/debug" "^4.1.7" - debug "^4.3.4" - semver "^7.3.8" - superstruct "^1.0.3" - -"@metamask/utils@^8.1.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-8.1.0.tgz#b8e73f5b4696b1b668cf5c1421daad140a3f98ac" - integrity sha512-sFNpzBKRicDgM2ZuU6vrPROlqNGm8/jDsjc5WrU1RzCkAMc4Xr3vUUf8p59uQ6B09etUWNb8d2GTCbISdmH/Ug== - dependencies: - "@ethereumjs/tx" "^4.1.2" - "@noble/hashes" "^1.3.1" - "@types/debug" "^4.1.7" - debug "^4.3.4" - semver "^7.5.4" - superstruct "^1.0.3" - "@motionone/animation@^10.15.1", "@motionone/animation@^10.16.3": version "10.16.3" resolved "https://registry.yarnpkg.com/@motionone/animation/-/animation-10.16.3.tgz#f5b71e27fd8b88b61f983adb0ed6c8e3e89281f9" @@ -3845,10 +3730,10 @@ dependencies: webpack-bundle-analyzer "4.7.0" -"@next/env@14.1.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@next/env/-/env-14.1.0.tgz#43d92ebb53bc0ae43dcc64fb4d418f8f17d7a341" - integrity sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw== +"@next/env@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/env/-/env-14.1.1.tgz#80150a8440eb0022a73ba353c6088d419b908bac" + integrity sha512-7CnQyD5G8shHxQIIg3c7/pSeYFeMhsNbpU/bmvH7ZnDql7mNRgg8O2JZrhrc/soFnfBnKP4/xXNiiSIPn2w8gA== "@next/eslint-plugin-next@14.1.0": version "14.1.0" @@ -3857,50 +3742,50 @@ dependencies: glob "10.3.10" -"@next/swc-darwin-arm64@14.1.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.0.tgz#70a57c87ab1ae5aa963a3ba0f4e59e18f4ecea39" - integrity sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ== - -"@next/swc-darwin-x64@14.1.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.0.tgz#0863a22feae1540e83c249384b539069fef054e9" - integrity sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g== - -"@next/swc-linux-arm64-gnu@14.1.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.0.tgz#893da533d3fce4aec7116fe772d4f9b95232423c" - integrity sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ== - -"@next/swc-linux-arm64-musl@14.1.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.0.tgz#d81ddcf95916310b8b0e4ad32b637406564244c0" - integrity sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g== - -"@next/swc-linux-x64-gnu@14.1.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.0.tgz#18967f100ec19938354332dcb0268393cbacf581" - integrity sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ== - -"@next/swc-linux-x64-musl@14.1.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.0.tgz#77077cd4ba8dda8f349dc7ceb6230e68ee3293cf" - integrity sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg== - -"@next/swc-win32-arm64-msvc@14.1.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.0.tgz#5f0b8cf955644104621e6d7cc923cad3a4c5365a" - integrity sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ== - -"@next/swc-win32-ia32-msvc@14.1.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.0.tgz#21f4de1293ac5e5a168a412b139db5d3420a89d0" - integrity sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw== - -"@next/swc-win32-x64-msvc@14.1.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.0.tgz#e561fb330466d41807123d932b365cf3d33ceba2" - integrity sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg== +"@next/swc-darwin-arm64@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.1.tgz#b74ba7c14af7d05fa2848bdeb8ee87716c939b64" + integrity sha512-yDjSFKQKTIjyT7cFv+DqQfW5jsD+tVxXTckSe1KIouKk75t1qZmj/mV3wzdmFb0XHVGtyRjDMulfVG8uCKemOQ== + +"@next/swc-darwin-x64@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.1.tgz#82c3e67775e40094c66e76845d1a36cc29c9e78b" + integrity sha512-KCQmBL0CmFmN8D64FHIZVD9I4ugQsDBBEJKiblXGgwn7wBCSe8N4Dx47sdzl4JAg39IkSN5NNrr8AniXLMb3aw== + +"@next/swc-linux-arm64-gnu@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.1.tgz#4f4134457b90adc5c3d167d07dfb713c632c0caa" + integrity sha512-YDQfbWyW0JMKhJf/T4eyFr4b3tceTorQ5w2n7I0mNVTFOvu6CGEzfwT3RSAQGTi/FFMTFcuspPec/7dFHuP7Eg== + +"@next/swc-linux-arm64-musl@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.1.tgz#594bedafaeba4a56db23a48ffed2cef7cd09c31a" + integrity sha512-fiuN/OG6sNGRN/bRFxRvV5LyzLB8gaL8cbDH5o3mEiVwfcMzyE5T//ilMmaTrnA8HLMS6hoz4cHOu6Qcp9vxgQ== + +"@next/swc-linux-x64-gnu@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.1.tgz#cb4e75f1ff2b9bcadf2a50684605928ddfc58528" + integrity sha512-rv6AAdEXoezjbdfp3ouMuVqeLjE1Bin0AuE6qxE6V9g3Giz5/R3xpocHoAi7CufRR+lnkuUjRBn05SYJ83oKNQ== + +"@next/swc-linux-x64-musl@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.1.tgz#15f26800df941b94d06327f674819ab64b272e25" + integrity sha512-YAZLGsaNeChSrpz/G7MxO3TIBLaMN8QWMr3X8bt6rCvKovwU7GqQlDu99WdvF33kI8ZahvcdbFsy4jAFzFX7og== + +"@next/swc-win32-arm64-msvc@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.1.tgz#060c134fa7fa843666e3e8574972b2b723773dd9" + integrity sha512-1L4mUYPBMvVDMZg1inUYyPvFSduot0g73hgfD9CODgbr4xiTYe0VOMTZzaRqYJYBA9mana0x4eaAaypmWo1r5A== + +"@next/swc-win32-ia32-msvc@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.1.tgz#5c06889352b1f77e3807834a0d0afd7e2d2d1da2" + integrity sha512-jvIE9tsuj9vpbbXlR5YxrghRfMuG0Qm/nZ/1KDHc+y6FpnZ/apsgh+G6t15vefU0zp3WSpTMIdXRUsNl/7RSuw== + +"@next/swc-win32-x64-msvc@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.1.tgz#d38c63a8f9b7f36c1470872797d3735b4a9c5c52" + integrity sha512-S6K6EHDU5+1KrBDLko7/c1MNy/Ya73pIAmvKeFwsF4RmBFJSO7/7YeD4FnZ4iBdzE69PpQ4sOMU9ORKeNuxe8A== "@ngraveio/bc-ur@^1.0.0", "@ngraveio/bc-ur@^1.1.5": version "1.1.6" @@ -3929,11 +3814,6 @@ dependencies: "@noble/hashes" "1.3.2" -"@noble/hashes@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.2.tgz#e9e035b9b166ca0af657a7848eb2718f0f22f183" - integrity sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA== - "@noble/hashes@1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" @@ -3949,16 +3829,6 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== -"@noble/hashes@~1.1.1": - version "1.1.5" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.5.tgz#1a0377f3b9020efe2fae03290bd2a12140c95c11" - integrity sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ== - -"@noble/secp256k1@1.6.3", "@noble/secp256k1@~1.6.0": - version "1.6.3" - resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.6.3.tgz#7eed12d9f4404b416999d0c87686836c4c5c9b94" - integrity sha512-T04e4iTurVy7I8Sw4+c5OSN9/RkPlo1uKxAomtxQNLq8j1uPAqnsqG1bqvY3Jv7c13gyr6dui0zmh/I3+f/JaQ== - "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -4237,37 +4107,23 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz#2d4260033e199b3032a08b41348ac10de21c47e9" integrity sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA== -"@safe-global/api-kit@^2.3.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@safe-global/api-kit/-/api-kit-2.3.0.tgz#fc7e741a25cb5983ce7e7ccf0f2fd2ef64bbe704" - integrity sha512-Mh9HH9+TK4dz5tiuPLn/GK42cOf1lKGvcmH/JQK8b7gD+YJugnXdrsirs3XahvwdG0rG7xfL9tf4mcW7zWzuXQ== +"@safe-global/api-kit@^2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@safe-global/api-kit/-/api-kit-2.3.2.tgz#37cab964faf79f98eb5f1a86004a56c0fb0eef21" + integrity sha512-pnbP23t3XQOWZv5Y35CoDpBAajSeB0ZY9ahajruNYt6pyPYxleQVuRueCQ+FDZHNDGS0SK7dlUYyUScs/GsTZQ== dependencies: - "@safe-global/protocol-kit" "^3.0.2" - "@safe-global/safe-core-sdk-types" "^4.0.2" + "@safe-global/protocol-kit" "^3.1.1" + "@safe-global/safe-core-sdk-types" "^4.1.1" ethers "^6.7.1" node-fetch "^2.7.0" -"@safe-global/protocol-kit@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@safe-global/protocol-kit/-/protocol-kit-3.0.2.tgz#cfa4c5e890c101aa89e11768d07b2bc5455f72fb" - integrity sha512-Jxvvfu4yqEdWeOuY3VWOOs/q5f27om3tctL2guOCDbAuSx3Vd1peaKRwLiREkvrrqKEW0tfmzUSsqtmlJExfBw== - dependencies: - "@noble/hashes" "^1.3.3" - "@safe-global/safe-deployments" "^1.34.0" - ethereumjs-util "^7.1.5" - ethers "^6.7.1" - semver "^7.5.4" - web3 "^1.10.3" - web3-core "^1.10.3" - web3-utils "^1.10.3" - -"@safe-global/protocol-kit@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@safe-global/protocol-kit/-/protocol-kit-3.1.0.tgz#429f3f11b3b2c60ed31568f853393155f316316b" - integrity sha512-PUZgTohIoQ1KN7RYE2IcQL8lj/LAb4WRgkDwB+1Vv7AoOLTI1U0Whajfe6Ur9w35BrQwr/x2HAoQvVSzH4FZ3Q== +"@safe-global/protocol-kit@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@safe-global/protocol-kit/-/protocol-kit-3.1.1.tgz#eec3cee3432cd1ad8b159e1911527d93ecc502f9" + integrity sha512-ai/N1DI6U53CsC46Do8eOyb6IkgVhjoQFhbFIh5rFSAKiuw3B0hTF7nrVRb0jw4NFlNHCcWDAER/uNH0Qy2Pkg== dependencies: "@noble/hashes" "^1.3.3" - "@safe-global/safe-deployments" "^1.35.0" + "@safe-global/safe-deployments" "^1.36.0" ethereumjs-util "^7.1.5" ethers "^6.7.1" semver "^7.5.4" @@ -4275,66 +4131,55 @@ web3-core "^1.10.3" web3-utils "^1.10.3" -"@safe-global/safe-apps-sdk@^9.0.0-next.1": - version "9.0.0-next.1" - resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-sdk/-/safe-apps-sdk-9.0.0-next.1.tgz#762186e36a3580c93e7efe7f49259c75043c5b7b" - integrity sha512-0XsgPXCOXx0wKhumKX/b65alRgd3umfNhsCq/S0K/hhMD/Uh6BTOzHRNFCosbnGvWQT75Hp3Y11LVS7ZD49t/w== +"@safe-global/safe-apps-sdk@^9.1.0": + version "9.1.0" + resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-sdk/-/safe-apps-sdk-9.1.0.tgz#0e65913e0f202e529ed3c846e0f5a98c2d35aa98" + integrity sha512-N5p/ulfnnA2Pi2M3YeWjULeWbjo7ei22JwU/IXnhoHzKq3pYCN6ynL9mJBOlvDVv892EgLPCWCOwQk/uBT2v0Q== dependencies: "@safe-global/safe-gateway-typescript-sdk" "^3.5.3" - viem "^1.6.0" + viem "^2.1.1" -"@safe-global/safe-core-sdk-types@^4.0.2": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@safe-global/safe-core-sdk-types/-/safe-core-sdk-types-4.0.2.tgz#70a99d8d61b83db458124c263391d86e5d6d5057" - integrity sha512-3I60xV/BLPiBtc3nGp2itgiDL+YbMI9OwaANvnJL2AwSS1kc2kH3/SsCwAW3s4Usr3b0lE08aO7I9ropyxFHhA== +"@safe-global/safe-core-sdk-types@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@safe-global/safe-core-sdk-types/-/safe-core-sdk-types-4.1.1.tgz#b75e15a2e6fa50f131629ca6f6f3a98fd6037d30" + integrity sha512-5NIWG7OjV+C5iquux0yPcu8SHUzg1qJXJ/jAQcPwXGTC7ZVsFawnR43/l2Vg9zEwf0RF0xTm3W8DXkaBYORiEQ== dependencies: - "@safe-global/safe-deployments" "^1.34.0" + "@safe-global/safe-deployments" "^1.36.0" ethers "^6.7.1" web3-core "^1.10.3" web3-utils "^1.10.3" -"@safe-global/safe-deployments@^1.34.0": - version "1.34.0" - resolved "https://registry.yarnpkg.com/@safe-global/safe-deployments/-/safe-deployments-1.34.0.tgz#5eef33012a4af55c4440036b1c0cfdb2245c6e49" - integrity sha512-J55iHhB1tiNoPeVQ5s943zrfeKRYPqBtnz/EM7d878WzUmmDlTGKHN98qPYKBxkRKP1UjEWuQDrZxy80lx1rJw== - dependencies: - semver "^7.3.7" - -"@safe-global/safe-deployments@^1.35.0": - version "1.35.0" - resolved "https://registry.yarnpkg.com/@safe-global/safe-deployments/-/safe-deployments-1.35.0.tgz#6930a86a006526a9791ebd2a11cf8f5d8358563b" - integrity sha512-Of8WQEcvL5Fm+xxnCDjah6Hkw+sNdzcApQnzr+OsPBxYtZL0RRtbmesypj36oOD8BQmyrH54V8DVN+pYjrfJ9g== +"@safe-global/safe-deployments@^1.36.0": + version "1.36.0" + resolved "https://registry.yarnpkg.com/@safe-global/safe-deployments/-/safe-deployments-1.36.0.tgz#7e5cd470cc1042182d47f65b59831a64ed8feff1" + integrity sha512-9MbDJveRR64AbmzjIpuUqmDBDtOZpXpvkyhTUs+5UOPT3WgSO375/ZTO7hZpywP7+EmxnjkGc9EoxjGcC4TAyw== dependencies: semver "^7.6.0" -"@safe-global/safe-gateway-typescript-sdk@3.21.1": - version "3.21.1" - resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.21.1.tgz#984ec2d3d4211caf6a96786ab922b39909093538" - integrity sha512-7nakIjcRSs6781LkizYpIfXh1DYlkUDqyALciqz/BjFU/S97sVjZdL4cuKsG9NEarytE+f6p0Qbq2Bo1aocVUA== +"@safe-global/safe-gateway-typescript-sdk@3.21.8": + version "3.21.8" + resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.21.8.tgz#f90eb668dd620d0c5578f02f7040169610a0eda1" + integrity sha512-n/fYgiqbuzAQuK0bgny6GBYvb585ETxKURa5Kb9hBV3fa47SvJo/dpGq275fJUn0e3Hh1YqETiLGj4HVJjHiTA== "@safe-global/safe-gateway-typescript-sdk@^3.5.3": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.19.0.tgz#18637c205c83bfc0a6be5fddbf202d6bb4927302" - integrity sha512-TRlP05KY6t3wjLJ74FiirWlEt3xTclnUQM2YdYto1jx5G1o0meMnugIUZXhzm7Bs3rDEDNhz/aDf2KMSZtoCFg== + version "3.21.2" + resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.21.2.tgz#2123a7429c2d9713365f51c359bfc055d4c8e913" + integrity sha512-N9Y2CKPBVbc8FbOKzqepy8TJUY2VILX7bmxV4ruByLJvR9PBnGvGfnOhw975cDn6PmSziXL0RaUWHpSW23rsng== "@safe-global/safe-modules-deployments@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@safe-global/safe-modules-deployments/-/safe-modules-deployments-1.2.0.tgz#ca871c3f553cd16cbea1aac8f8be16498329a7d3" integrity sha512-/pjHIPaYwGRM5oOB7lc+yf28fWEq7twNP5dJxpLFgG/9UR4E3F+XfFdYkpP22eIvmOkBwCJXJZfPfESh9WSF2w== -"@scure/base@~1.1.0", "@scure/base@~1.1.2": +"@scure/base@~1.1.0": version "1.1.3" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.3.tgz#8584115565228290a6c6c4961973e0903bb3df2f" integrity sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q== -"@scure/bip32@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.1.0.tgz#dea45875e7fbc720c2b4560325f1cf5d2246d95b" - integrity sha512-ftTW3kKX54YXLCxH6BB7oEEoJfoE2pIgw7MINKAs5PsS6nqKPuKk1haTF/EuHmYqG330t5GSrdmtRuHaY1a62Q== - dependencies: - "@noble/hashes" "~1.1.1" - "@noble/secp256k1" "~1.6.0" - "@scure/base" "~1.1.0" +"@scure/base@~1.1.2": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d" + integrity sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g== "@scure/bip32@1.3.1": version "1.3.1" @@ -4354,14 +4199,6 @@ "@noble/hashes" "~1.3.2" "@scure/base" "~1.1.2" -"@scure/bip39@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.0.tgz#92f11d095bae025f166bef3defcc5bf4945d419a" - integrity sha512-pwrPOS16VeTKg98dYXQyIjJEcWfz7/1YJIwxUEPFfQPtc86Ym/1sVgQ2RLoD43AazMk2l/unK4ITySSpW2+82w== - dependencies: - "@noble/hashes" "~1.1.1" - "@scure/base" "~1.1.0" - "@scure/bip39@1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a" @@ -4481,11 +4318,6 @@ dependencies: "@sinonjs/commons" "^3.0.0" -"@socket.io/component-emitter@~3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" - integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== - "@solana/buffer-layout@^4.0.0": version "4.0.1" resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz#b996235eaec15b1e0b5092a8ed6028df77fa6c15" @@ -4613,7 +4445,7 @@ "@stablelib/constant-time" "^1.0.1" "@stablelib/wipe" "^1.0.1" -"@stablelib/random@^1.0.1", "@stablelib/random@^1.0.2": +"@stablelib/random@1.0.2", "@stablelib/random@^1.0.1", "@stablelib/random@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@stablelib/random/-/random-1.0.2.tgz#2dece393636489bf7e19c51229dd7900eddf742c" integrity sha512-rIsE83Xpb7clHPVRlBj8qNe5L8ISQOzjghYQm/dZ7VaM2KHYwMW5adjQjrzTZCchFnNCNhkwtnOBa9HTMJCI8w== @@ -4644,7 +4476,7 @@ resolved "https://registry.yarnpkg.com/@stablelib/wipe/-/wipe-1.0.1.tgz#d21401f1d59ade56a62e139462a97f104ed19a36" integrity sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg== -"@stablelib/x25519@^1.0.3": +"@stablelib/x25519@1.0.3", "@stablelib/x25519@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@stablelib/x25519/-/x25519-1.0.3.tgz#13c8174f774ea9f3e5e42213cbf9fc68a3c7b7fd" integrity sha512-KnTbKmUhPhHavzobclVJQG5kuivH+qDLpe84iRqX3CLrKp881cF160JvXJ+hjn1aMyCwYOKeIZefIH/P5cJoRw== @@ -5609,438 +5441,18 @@ integrity sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw== dependencies: "@babel/runtime" "^7.12.5" - "@testing-library/dom" "^8.5.0" - "@types/react-dom" "^18.0.0" - -"@testing-library/user-event@^14.4.2", "@testing-library/user-event@^14.5.2": - version "14.5.2" - resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.5.2.tgz#db7257d727c891905947bd1c1a99da20e03c2ebd" - integrity sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ== - -"@tkey-mpc/chrome-storage@^9.0.2": - version "9.0.2" - resolved "https://registry.yarnpkg.com/@tkey-mpc/chrome-storage/-/chrome-storage-9.0.2.tgz#77a0e46df75c844ee9c5f4b07963d22edb44b71b" - integrity sha512-3yeliKVCJP77OhG5J93uj2/0XwASO6H8WqSZgAgbVxXbC6oPazKTv84YVw0rkXmLE7gkHqagcjNiNOQaV29VRw== - dependencies: - "@tkey-mpc/common-types" "^9.0.2" - webextension-polyfill "^0.10.0" - -"@tkey-mpc/common-types@^8.2.2": - version "8.2.2" - resolved "https://registry.yarnpkg.com/@tkey-mpc/common-types/-/common-types-8.2.2.tgz#194324a9ad92027c0f040896fa94fec096e83c5d" - integrity sha512-aN/LUXFh+t+4vf1zzlIIh3xd+M4HKzBgaJq6eVopPLMxaG941ZFyOq9Tbymzliq9lrFhA3ByEyPFI6Lunqzvbg== - dependencies: - "@toruslabs/eccrypto" "^3.0.0" - "@toruslabs/rss-client" "^1.5.0" - bn.js "^5.2.1" - elliptic "^6.5.4" - serialize-error "^8.1.0" - ts-custom-error "^3.3.1" - web3-utils "^1.8.1" - -"@tkey-mpc/common-types@^9.0.2": - version "9.0.2" - resolved "https://registry.yarnpkg.com/@tkey-mpc/common-types/-/common-types-9.0.2.tgz#ad8fc7c905a27caf1c7f6fa724b14768b46e3de2" - integrity sha512-TJrij2Fq/klAcLlpECqxNUnzeLcChNmrjQdwYJXZKMBsyUo3jYmAGTAqA7/6leaHSZfW18DMHcLrsSeiHQw63g== - dependencies: - "@toruslabs/customauth" "^16.0.6" - "@toruslabs/eccrypto" "^4.0.0" - "@toruslabs/rss-client" "^1.5.0" - "@toruslabs/torus.js" "^11.0.6" - bn.js "^5.2.1" - elliptic "^6.5.4" - serialize-error "^8.1.0" - ts-custom-error "^3.3.1" - -"@tkey-mpc/core@^9.0.2": - version "9.0.2" - resolved "https://registry.yarnpkg.com/@tkey-mpc/core/-/core-9.0.2.tgz#ca659d5288e5e818c36dd0618d2903fc6a9e1349" - integrity sha512-sc3w+rjc37OSQwff0bpVlmUDX58hBKY8AOSOsuWJoMfA7l8CFWyQp7iRBiVIxIgOojfeosu0RUcq2Z8HsTKK0g== - dependencies: - "@tkey-mpc/common-types" "^9.0.2" - "@toruslabs/eccrypto" "^4.0.0" - "@toruslabs/http-helpers" "^5.0.0" - "@toruslabs/rss-client" "^1.5.0" - "@toruslabs/torus.js" "^11.0.6" - bn.js "^5.2.1" - elliptic "^6.5.4" - json-stable-stringify "^1.0.2" - -"@tkey-mpc/security-questions@^9.0.2": - version "9.0.2" - resolved "https://registry.yarnpkg.com/@tkey-mpc/security-questions/-/security-questions-9.0.2.tgz#9ed6f944713f08d290bba920738488451db083fe" - integrity sha512-ekxMnmdbwuDRlEuXhZcG4U2W1Y1NFSo10Ctb8Mrw9FNz0XR7lTIAcLn3dlvCQHRmKLT1hlziW8oby4nsMpqsbA== - dependencies: - "@tkey-mpc/common-types" "^9.0.2" - bn.js "^5.2.1" - ethereum-cryptography "^2.1.2" - -"@tkey-mpc/service-provider-base@^9.0.2": - version "9.0.2" - resolved "https://registry.yarnpkg.com/@tkey-mpc/service-provider-base/-/service-provider-base-9.0.2.tgz#03dc78cd3684bd261a1135ab0dfeb08c9b7b74c2" - integrity sha512-Gk3xF1NFV/BqmwMNKDB//DNoN9nff4kGeNxXiVnlxhQKZuX9phzd/sxJljKc6tutOK5YdOcvot7ZuwJg2/vZjA== - dependencies: - "@tkey-mpc/common-types" "^9.0.2" - bn.js "^5.2.1" - elliptic "^6.5.4" - -"@tkey-mpc/service-provider-torus@^9.0.3": - version "9.0.3" - resolved "https://registry.yarnpkg.com/@tkey-mpc/service-provider-torus/-/service-provider-torus-9.0.3.tgz#fdfddf000c55fb22ac92c135db5d46c8d9615e2a" - integrity sha512-9/OjTqjruR5AWMwJPRuR+ZxsZXzt9jQfzfNtde13VHSjtogcLwT12U1QTHJ7tsIZYckBy3ZVDbNpIcTOzz70tQ== - dependencies: - "@tkey-mpc/common-types" "^9.0.2" - "@tkey-mpc/service-provider-base" "^9.0.2" - "@toruslabs/customauth" "^16.0.6" - "@toruslabs/torus.js" "^11.0.6" - bn.js "^5.2.1" - elliptic "^6.5.4" - -"@tkey-mpc/share-serialization@^9.0.2": - version "9.0.2" - resolved "https://registry.yarnpkg.com/@tkey-mpc/share-serialization/-/share-serialization-9.0.2.tgz#a303d5eed1e6d9665a8aede5324193682bf4760e" - integrity sha512-dtQFbWvV9A4y6SGpx72WaRcwSREf/Yld9f4+u7FWe81WY575N655SheqM08gYYEGZlcQ+TiCMN3C2rq9I+NpMw== - dependencies: - "@tkey-mpc/common-types" "^9.0.2" - "@types/create-hash" "1.2.2" - bn.js "^5.2.1" - ethereum-cryptography "^2.1.2" - -"@tkey-mpc/storage-layer-torus@^9.0.2": - version "9.0.2" - resolved "https://registry.yarnpkg.com/@tkey-mpc/storage-layer-torus/-/storage-layer-torus-9.0.2.tgz#a59067379ead3e110359209a004ab69356f7930e" - integrity sha512-qxQ+O9GerdaM66DVh8Qi7oIRjBbcsUcuP+F9P5YdH8J+XEFDMSjhuuBaQpU3xaMFxpTozkSv4Kh/zHWyQE2YeA== - dependencies: - "@tkey-mpc/common-types" "^9.0.2" - "@toruslabs/http-helpers" "^5.0.0" - base64url "3.0.1" - bn.js "^5.2.1" - ethereum-cryptography "^2.1.2" - json-stable-stringify "^1.0.2" - -"@tootallnate/once@2": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" - integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== - -"@toruslabs/base-controllers@^2.2.6": - version "2.9.0" - resolved "https://registry.yarnpkg.com/@toruslabs/base-controllers/-/base-controllers-2.9.0.tgz#e23f4228b5a90bf94ba9b0b27451f3024bd1acc4" - integrity sha512-rKc+bR4QB/wdbH0CxLZC5e2PUZcIgkr9yY7TMd3oIffDklaYBnsuC5ES2/rgK1aRUDRWz+qWbTwLqsY6PlT37Q== - dependencies: - "@ethereumjs/util" "^8.0.6" - "@toruslabs/broadcast-channel" "^6.2.0" - "@toruslabs/http-helpers" "^3.3.0" - "@toruslabs/openlogin-jrpc" "^4.0.0" - async-mutex "^0.4.0" - bignumber.js "^9.1.1" - bowser "^2.11.0" - eth-rpc-errors "^4.0.3" - json-rpc-random-id "^1.0.1" - lodash "^4.17.21" - loglevel "^1.8.1" - -"@toruslabs/base-controllers@^4.2.0": - version "4.5.2" - resolved "https://registry.yarnpkg.com/@toruslabs/base-controllers/-/base-controllers-4.5.2.tgz#ce0a475203990525ec352d3b3283eca5b1b19cbb" - integrity sha512-DEaeKREzY8OJ74k6nuOP1x2a8Z1HeCfV2uQE835QhLKuCDgqRomDwCwRcrCGT3rqxT/AVhWmBj+ykO/XS/4lZw== - dependencies: - "@ethereumjs/util" "^9.0.0" - "@metamask/rpc-errors" "^6.0.0" - "@toruslabs/broadcast-channel" "^9.0.0" - "@toruslabs/http-helpers" "^5.0.0" - "@toruslabs/openlogin-jrpc" "^5.1.0" - async-mutex "^0.4.0" - bignumber.js "^9.1.2" - bowser "^2.11.0" - lodash "^4.17.21" - loglevel "^1.8.1" - -"@toruslabs/base-session-manager@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@toruslabs/base-session-manager/-/base-session-manager-3.0.0.tgz#4302d0747ef71a8278af79e577cf53253c907cd5" - integrity sha512-+EqwizmSFkVEczUtaw+swbAxRIIxC/EaFE040rwfgC5fixaQMNLw2cVYXWN67Ra47wC9A7Om6xwQTuGFR+dy4w== - dependencies: - "@toruslabs/http-helpers" "^5.0.0" - -"@toruslabs/broadcast-channel@^6.2.0": - version "6.3.1" - resolved "https://registry.yarnpkg.com/@toruslabs/broadcast-channel/-/broadcast-channel-6.3.1.tgz#d4b0a08c3a0fa88d42d7f33387ce9be928c2d4b2" - integrity sha512-BEtJQ+9bMfFoGuCsp5NmxyY+C980Ho+3BZIKSiYwRtl5qymJ+jMX5lsoCppoQblcb34dP6FwEjeFw80Y9QC/rw== - dependencies: - "@babel/runtime" "^7.21.0" - "@toruslabs/eccrypto" "^2.1.1" - "@toruslabs/metadata-helpers" "^3.2.0" - bowser "^2.11.0" - loglevel "^1.8.1" - oblivious-set "1.1.1" - socket.io-client "^4.6.1" - unload "^2.4.1" - -"@toruslabs/broadcast-channel@^8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@toruslabs/broadcast-channel/-/broadcast-channel-8.0.0.tgz#904ee44fceeb861d16f282ca211d1e2adf16e2f8" - integrity sha512-qCyWsHVL4Xtx1J6k1+acD7TJKCelJWyUy5Q5zyiWMPxMGFxTv1XdRyqpzV+VgwbcslIqgFN0GewOry2l1jlUQQ== - dependencies: - "@babel/runtime" "^7.22.10" - "@toruslabs/eccrypto" "^4.0.0" - "@toruslabs/metadata-helpers" "^5.0.0" - bowser "^2.11.0" - loglevel "^1.8.1" - oblivious-set "1.1.1" - socket.io-client "^4.7.2" - unload "^2.4.1" - -"@toruslabs/broadcast-channel@^9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@toruslabs/broadcast-channel/-/broadcast-channel-9.0.0.tgz#8f13b829269ee410fd46b7b463dea33b0c116147" - integrity sha512-GA0hh32vt0qu0qZ/QepNHDT5bxKzTsDWu2yp5J1a8CQWNwUGPkYkDe1ycRySQVBo/wa7UyZdXvgLYtYne+/cZw== - dependencies: - "@babel/runtime" "^7.22.10" - "@toruslabs/eccrypto" "^4.0.0" - "@toruslabs/metadata-helpers" "^5.0.0" - bowser "^2.11.0" - loglevel "^1.8.1" - oblivious-set "1.1.1" - socket.io-client "^4.7.2" - unload "^2.4.1" - -"@toruslabs/constants@^13.0.1", "@toruslabs/constants@^13.0.3": - version "13.0.3" - resolved "https://registry.yarnpkg.com/@toruslabs/constants/-/constants-13.0.3.tgz#260a3e5c430d201bd3d92e13ef2044bd89a4432e" - integrity sha512-DiksceNFwzV4XBwcPdKpV6tfrvWIxhmwseTJbNnlzdy6uXgzvtagfapu+98pjrkNrjoRLiM17QBgBi8sNw7oGQ== - -"@toruslabs/customauth@^16.0.6": - version "16.0.6" - resolved "https://registry.yarnpkg.com/@toruslabs/customauth/-/customauth-16.0.6.tgz#cf3f0e614735fb799a9748b992026f81a9ee2ab6" - integrity sha512-4+Cxbxz3fxm750MMaMqMoJS0x8RZdD0y1tHLa/2T+b993cjRoQRixddTa3rWNYdbWsg2rgcmGkpXxywq3aLzwA== - dependencies: - "@chaitanyapotti/register-service-worker" "^1.7.3" - "@toruslabs/broadcast-channel" "^8.0.0" - "@toruslabs/constants" "^13.0.1" - "@toruslabs/eccrypto" "^4.0.0" - "@toruslabs/fetch-node-details" "^13.0.1" - "@toruslabs/http-helpers" "^5.0.0" - "@toruslabs/metadata-helpers" "^5.0.0" - "@toruslabs/torus.js" "^11.0.5" - base64url "^3.0.1" - bowser "^2.11.0" - events "^3.3.0" - jwt-decode "^3.1.2" - lodash.merge "^4.6.2" - loglevel "^1.8.1" - -"@toruslabs/eccrypto@4.0.0", "@toruslabs/eccrypto@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@toruslabs/eccrypto/-/eccrypto-4.0.0.tgz#0b27ed2d1e9483e77f42a7619a2c3c19cb802f44" - integrity sha512-Z3EINkbsgJx1t6jCDVIJjLSUEGUtNIeDjhMWmeDGOWcP/+v/yQ1hEvd1wfxEz4q5WqIHhevacmPiVxiJ4DljGQ== - dependencies: - elliptic "^6.5.4" - -"@toruslabs/eccrypto@^2.1.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@toruslabs/eccrypto/-/eccrypto-2.2.1.tgz#19012cc4e774e8c3df7ceebb2c1a07ecfd784917" - integrity sha512-7sviL0wLYsfA5ogEAOIdb0tu/QAOFXfHc9B8ONYtF04x4Mg3Nr89LL35FhjaEm055q8Ru7cUQhEFSiqJqm9GCw== - dependencies: - elliptic "^6.5.4" - -"@toruslabs/eccrypto@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@toruslabs/eccrypto/-/eccrypto-3.0.0.tgz#96df18e9d7320b230492671cf2942e703cd34ae3" - integrity sha512-+lFjV+0FZ3S4zH5T/Gnc795HoqpzLLDW28fSkibZRlx1Nx8uwbl3pyJSfya0C0bRHH1/+NTeBogUDijaRJ1NCw== - dependencies: - elliptic "^6.5.4" - -"@toruslabs/fetch-node-details@^13.0.1": - version "13.0.3" - resolved "https://registry.yarnpkg.com/@toruslabs/fetch-node-details/-/fetch-node-details-13.0.3.tgz#3cb1d08a61e7ded4eda325b1f52c837553903cd4" - integrity sha512-u8Lhp2rOcK9+0qfz74JzLIisjfDKbMY5H/yh9E601peBydshx9HnfWTuRuj5bYGuHCQlXHC3t7UQbMAiuZ0EuA== - dependencies: - "@toruslabs/constants" "^13.0.3" - "@toruslabs/fnd-base" "^13.0.3" - "@toruslabs/http-helpers" "^5.0.0" - loglevel "^1.8.1" - -"@toruslabs/fnd-base@^13.0.1", "@toruslabs/fnd-base@^13.0.3": - version "13.0.3" - resolved "https://registry.yarnpkg.com/@toruslabs/fnd-base/-/fnd-base-13.0.3.tgz#11a0cc95b085309322a6e20818b5dfb14cf6b004" - integrity sha512-XQyUacMwQ/yb1OgEnZHtnepCIhh9DfeTz+Ki7DkpqiCPnvduXqQYPSYoLwQumBjHOKwn9HxHdPYL9ps3hQpGPg== - dependencies: - "@toruslabs/constants" "^13.0.3" - -"@toruslabs/http-helpers@^3.2.0", "@toruslabs/http-helpers@^3.3.0", "@toruslabs/http-helpers@^3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@toruslabs/http-helpers/-/http-helpers-3.4.0.tgz#6d1da9e6aba094af62e73cf639a69844c82202f3" - integrity sha512-CoeJSL32mpp0gmYjxv48odu6pfjHk/rbJHDwCtYPcMHAl+qUQ/DTpVOOn9U0fGkD+fYZrQmZbRkXFgLhiT0ajQ== - dependencies: - lodash.merge "^4.6.2" - loglevel "^1.8.1" - -"@toruslabs/http-helpers@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@toruslabs/http-helpers/-/http-helpers-5.0.0.tgz#2a309d2a2c5c00d50a725d83ccec8a7475771d62" - integrity sha512-GmezWz9JeF6YyhjLSm+9XDF4YaeICEckY0Jbo43i86SjhfJYgRWqEi63VSiNsaqc/z810Q0FQvEk1TnBRX2tgA== - dependencies: - lodash.merge "^4.6.2" - loglevel "^1.8.1" - -"@toruslabs/metadata-helpers@5.0.0", "@toruslabs/metadata-helpers@^5.0.0", "@toruslabs/metadata-helpers@^5.x": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@toruslabs/metadata-helpers/-/metadata-helpers-5.0.0.tgz#12be5de4e8a5d1af2dd080bdf05f5ad8953aaae7" - integrity sha512-ZUFfOHJVJC53c8wJYHjdF3bIgN2ZvfqehbTZ/zJ7oVFfrrd6O66V3gQ1i1zxBjH3yhOvZKQwc0DaMmh3G0NUXQ== - dependencies: - "@toruslabs/eccrypto" "^4.0.0" - "@toruslabs/http-helpers" "^5.0.0" - elliptic "^6.5.4" - ethereum-cryptography "^2.1.2" - json-stable-stringify "^1.0.2" - -"@toruslabs/metadata-helpers@^3.2.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@toruslabs/metadata-helpers/-/metadata-helpers-3.2.0.tgz#b297933ac37481a9c86a125ac6a4e5c2f109fb78" - integrity sha512-2bCc6PNKd9y+aWfZQ1FXd47QmfyT4NmmqPGfsqk+sQS2o+MlxIyLuh9uh7deMgXo4b4qBDX+RQGbIKM1zVk56w== - dependencies: - "@toruslabs/eccrypto" "^2.1.1" - "@toruslabs/http-helpers" "^3.4.0" - elliptic "^6.5.4" - ethereum-cryptography "^2.0.0" - json-stable-stringify "^1.0.2" - -"@toruslabs/openlogin-jrpc@^2.6.0": - version "2.13.0" - resolved "https://registry.yarnpkg.com/@toruslabs/openlogin-jrpc/-/openlogin-jrpc-2.13.0.tgz#aae71e7c9b0161bc14baf3fc696605d74e0b99f4" - integrity sha512-TEg50/84xSocHLb3MEtw0DaIa+bXU66TJJjjDrqGPjoRo97fn8F8jDW2AcVV+eug39xpfxPIw1FFdCtgunmz7w== - dependencies: - "@toruslabs/openlogin-utils" "^2.13.0" - end-of-stream "^1.4.4" - eth-rpc-errors "^4.0.3" - events "^3.3.0" - fast-safe-stringify "^2.1.1" - once "^1.4.0" - pump "^3.0.0" - readable-stream "^3.6.0" - -"@toruslabs/openlogin-jrpc@^4.0.0": - version "4.7.2" - resolved "https://registry.yarnpkg.com/@toruslabs/openlogin-jrpc/-/openlogin-jrpc-4.7.2.tgz#e04dd6945da92d790f713a58aaa1657c57b330c8" - integrity sha512-9Eb0cPc0lPuS6v2YkQlgzfbRnZ6fLez9Ike5wznoHSFA2/JVu1onwuI56EV1HwswdDrOWPPQEyzI1j9NriZ0ew== - dependencies: - "@metamask/rpc-errors" "^5.1.1" - "@toruslabs/openlogin-utils" "^4.7.0" - end-of-stream "^1.4.4" - events "^3.3.0" - fast-safe-stringify "^2.1.1" - once "^1.4.0" - pump "^3.0.0" - readable-stream "^4.4.2" - -"@toruslabs/openlogin-jrpc@^5.1.0", "@toruslabs/openlogin-jrpc@^5.2.0": - version "5.3.0" - resolved "https://registry.yarnpkg.com/@toruslabs/openlogin-jrpc/-/openlogin-jrpc-5.3.0.tgz#37fefe43d82b0ecfa7a99de56050e4b46bf68a5c" - integrity sha512-tM1XHG5UulvENhZYewo9eVWF5r9SffGru8+09qFED7g/PFupKs/rccaXYwyWRGVenQSMgj6ipgUd72zliuen+A== - dependencies: - "@metamask/rpc-errors" "^6.0.0" - "@toruslabs/openlogin-utils" "^5.3.0" - end-of-stream "^1.4.4" - events "^3.3.0" - fast-safe-stringify "^2.1.1" - once "^1.4.0" - pump "^3.0.0" - readable-stream "^4.4.2" - -"@toruslabs/openlogin-session-manager@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@toruslabs/openlogin-session-manager/-/openlogin-session-manager-3.0.0.tgz#2f13af3ef96ddda48803a0265a798cffc3c9f78c" - integrity sha512-S+nnZQ+Y+XCHvTYaov3ltiV2hAAPpKpwxvB4TmbMvi7KWOZ8BcUJQykSITlIXV4aE5y5BD96rsmjQ3C3MyVtUQ== - dependencies: - "@toruslabs/base-session-manager" "^3.0.0" - "@toruslabs/eccrypto" "^4.0.0" - "@toruslabs/metadata-helpers" "5.0.0" - -"@toruslabs/openlogin-utils@^2.13.0": - version "2.13.0" - resolved "https://registry.yarnpkg.com/@toruslabs/openlogin-utils/-/openlogin-utils-2.13.0.tgz#e339f9d638b1e3a8ecca7b8c973d6060a19afda5" - integrity sha512-g4pj6hIdKcuyetVsUWqiAJmCooTS9hOADL31m7LTqgdXzX9oR437A+c8Dw8gzFVcHmkK16Yt2//GvlKnSsGILg== - dependencies: - base64url "^3.0.1" - keccak "^3.0.3" - randombytes "^2.1.0" - -"@toruslabs/openlogin-utils@^4.7.0": - version "4.7.0" - resolved "https://registry.yarnpkg.com/@toruslabs/openlogin-utils/-/openlogin-utils-4.7.0.tgz#741d6ba1c0754b59a182b1c6dd8d0263695ed980" - integrity sha512-w6XkHs4WKuufsf/zzteBzs4EJuOknrUmJ+iv5FZ8HzIpMQeL/984CP8HYaFSEYkbGCP4ydAnhY4Uh0QAhpDbPg== - dependencies: - base64url "^3.0.1" - -"@toruslabs/openlogin-utils@^5.2.0", "@toruslabs/openlogin-utils@^5.3.0": - version "5.3.0" - resolved "https://registry.yarnpkg.com/@toruslabs/openlogin-utils/-/openlogin-utils-5.3.0.tgz#59906be13fe47c399513360e36b9262410e5a1f2" - integrity sha512-WfwadC7ZqKOTVqfI4rFANu7IzEgI7H4A/0TiIQihxHpFWdFM23yVuCCIm5zCzQ1QtA2mwBbBZYwOJ0YHVrV6HQ== - dependencies: - "@toruslabs/constants" "^13.0.1" - base64url "^3.0.1" - -"@toruslabs/openlogin@^5.2.0": - version "5.3.0" - resolved "https://registry.yarnpkg.com/@toruslabs/openlogin/-/openlogin-5.3.0.tgz#4cb28a62bf30b06b7e56a78bf9d7abe01d640560" - integrity sha512-I6bVBPDlqcnF/11+disyS0kUClJLavYcLi3dBEx2a1mEv18jOy1kuG+FSenDMzF733MCk1Y3Iikox0evPzhpvQ== - dependencies: - "@toruslabs/broadcast-channel" "^8.0.0" - "@toruslabs/eccrypto" "^4.0.0" - "@toruslabs/metadata-helpers" "^5.0.0" - "@toruslabs/openlogin-session-manager" "^3.0.0" - "@toruslabs/openlogin-utils" "^5.3.0" - bowser "^2.11.0" - events "^3.3.0" - loglevel "^1.8.1" - ts-custom-error "^3.3.1" - -"@toruslabs/rss-client@^1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@toruslabs/rss-client/-/rss-client-1.5.0.tgz#82a02736f671152e1a4c9078194320e2bbc4e49e" - integrity sha512-7oaRZjmomNoL0+OZMFe8mv3hSeaYWvrxTjuUtkOHybMitStEcEj64tPjWia646BX7AFeqtUCURNdNaQzqfwDMQ== - dependencies: - "@toruslabs/eccrypto" "^2.1.1" - "@toruslabs/http-helpers" "^3.2.0" - bn.js "^5.2.1" - elliptic "^6.5.4" - fetch "^1.1.0" - loglevel "^1.8.1" - node-fetch "^2.0.0" - web3-eth-contract "^1.8.1" - web3-utils "^1.8.1" - -"@toruslabs/torus.js@^11.0.5", "@toruslabs/torus.js@^11.0.6": - version "11.0.6" - resolved "https://registry.yarnpkg.com/@toruslabs/torus.js/-/torus.js-11.0.6.tgz#982f28555922e66822b38d265c6f8a38bc094db3" - integrity sha512-l+Ba/DX1L2cLngiL08r8zZmRQ/A3MJ4pl20MaTzQuwxS2OfOXSReKld4YOWHwo5NEd36PHZdZ43qbh0NrzrwVQ== - dependencies: - "@toruslabs/constants" "^13.0.1" - "@toruslabs/eccrypto" "^4.0.0" - "@toruslabs/http-helpers" "^5.0.0" - bn.js "^5.2.1" - elliptic "^6.5.4" - ethereum-cryptography "^2.1.2" - json-stable-stringify "^1.0.2" - loglevel "^1.8.1" - -"@toruslabs/tss-client@^1.7.1": - version "1.7.1" - resolved "https://registry.yarnpkg.com/@toruslabs/tss-client/-/tss-client-1.7.1.tgz#018511a75a1ee6094fbe74d2b7be968b92255e97" - integrity sha512-gaVjXy/eJKv59zdWW2lnAhjumw8DMMGlGHXEOipVywCylKqS8VvQVwwo+UyevR2VXUQsNkBr9deD5TGFH0OQHQ== - dependencies: - "@toruslabs/eccrypto" "^4.0.0" - "@toruslabs/tss-lib" "^1.7.1" - bn.js "^5.2.1" - elliptic "^6.5.4" - keccak256 "^1.0.6" - socket.io-client "^4.7.2" + "@testing-library/dom" "^8.5.0" + "@types/react-dom" "^18.0.0" -"@toruslabs/tss-lib@^1.7.1": - version "1.7.1" - resolved "https://registry.yarnpkg.com/@toruslabs/tss-lib/-/tss-lib-1.7.1.tgz#758c62b72b41450de5df90acb42d6c2ba5df0482" - integrity sha512-kdjBO95cPq4i7RaRMkjUJFH0aiSHrMZV/A4I42oUr0FkBd7e/RYyn1e1QH1pAAyidCIKbMkwqTxgPg4nuHEcDg== +"@testing-library/user-event@^14.4.2", "@testing-library/user-event@^14.5.2": + version "14.5.2" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.5.2.tgz#db7257d727c891905947bd1c1a99da20e03c2ebd" + integrity sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ== + +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== "@trezor/analytics@1.0.8": version "1.0.8" @@ -6190,33 +5602,6 @@ varuint-bitcoin "^1.1.2" wif "^2.0.6" -"@truffle/hdwallet-provider@^2.1.4": - version "2.1.15" - resolved "https://registry.yarnpkg.com/@truffle/hdwallet-provider/-/hdwallet-provider-2.1.15.tgz#fbf8e19d112db81b109ebc06ac6d9d42124b512c" - integrity sha512-I5cSS+5LygA3WFzru9aC5+yDXVowEEbLCx0ckl/RqJ2/SCiYXkzYlR5/DjjDJuCtYhivhrn2RP9AheeFlRF+qw== - dependencies: - "@ethereumjs/common" "^2.4.0" - "@ethereumjs/tx" "^3.3.0" - "@metamask/eth-sig-util" "4.0.1" - "@truffle/hdwallet" "^0.1.4" - "@types/ethereum-protocol" "^1.0.0" - "@types/web3" "1.0.20" - "@types/web3-provider-engine" "^14.0.0" - ethereum-cryptography "1.1.2" - ethereum-protocol "^1.0.1" - ethereumjs-util "^7.1.5" - web3 "1.10.0" - web3-provider-engine "16.0.3" - -"@truffle/hdwallet@^0.1.4": - version "0.1.4" - resolved "https://registry.yarnpkg.com/@truffle/hdwallet/-/hdwallet-0.1.4.tgz#eeb21163d9e295692a0ba2fa848cc7b5a29b0ded" - integrity sha512-D3SN0iw3sMWUXjWAedP6RJtopo9qQXYi80inzbtcsoso4VhxFxCwFvCErCl4b27AEJ9pkAtgnxEFRaSKdMmi1Q== - dependencies: - ethereum-cryptography "1.1.2" - keccak "3.0.2" - secp256k1 "4.0.3" - "@trysound/sax@0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" @@ -6278,13 +5663,6 @@ dependencies: "@babel/types" "^7.20.7" -"@types/bn.js@*", "@types/bn.js@^5.1.0", "@types/bn.js@^5.1.1": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.2.tgz#162f5238c46f4bcbac07a98561724eca1fcf0c5e" - integrity sha512-dkpZu0szUtn9UXTmw+e0AJFd4D2XAxDnsCLdc05SfqpqzPEBft8eQr8uaFitfo/dUUOZERaLec2hHMG87A4Dxg== - dependencies: - "@types/node" "*" - "@types/bn.js@5.1.1": version "5.1.1" resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.1.tgz#b51e1b55920a4ca26e9285ff79936bbdec910682" @@ -6299,6 +5677,13 @@ dependencies: "@types/node" "*" +"@types/bn.js@^5.1.0", "@types/bn.js@^5.1.1": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.2.tgz#162f5238c46f4bcbac07a98561724eca1fcf0c5e" + integrity sha512-dkpZu0szUtn9UXTmw+e0AJFd4D2XAxDnsCLdc05SfqpqzPEBft8eQr8uaFitfo/dUUOZERaLec2hHMG87A4Dxg== + dependencies: + "@types/node" "*" + "@types/body-parser@*": version "1.19.5" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" @@ -6324,13 +5709,6 @@ dependencies: "@types/node" "*" -"@types/create-hash@1.2.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@types/create-hash/-/create-hash-1.2.2.tgz#e87247083df8478f6b83655592bde0d709028235" - integrity sha512-Fg8/kfMJObbETFU/Tn+Y0jieYewryLrbKwLCEIwPyklZZVY2qB+64KFjhplGSw+cseZosfFXctXO+PyIYD8iZQ== - dependencies: - "@types/node" "*" - "@types/cross-spawn@^6.0.2": version "6.0.6" resolved "https://registry.yarnpkg.com/@types/cross-spawn/-/cross-spawn-6.0.6.tgz#0163d0b79a6f85409e0decb8dcca17147f81fd22" @@ -6406,13 +5784,6 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== -"@types/ethereum-protocol@*", "@types/ethereum-protocol@^1.0.0": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@types/ethereum-protocol/-/ethereum-protocol-1.0.3.tgz#64a4001b8ef7d3f09e89123feb8c35d04efd00a7" - integrity sha512-peaCYb+wAT3Gnttt8Ep6+b3ciVK+mWX5wyVnJiDtmWXU1c9RXi5qDxEjGyZrjU/9EYdXPd3hMiXXBjDDPu96yQ== - dependencies: - bignumber.js "7.2.1" - "@types/express-serve-static-core@^4.17.33": version "4.19.0" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz#3ae8ab3767d98d0b682cda063c3339e1e86ccfaa" @@ -6782,11 +6153,6 @@ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.4.tgz#2b38784cd16957d3782e8e2b31c03bc1d13b4d65" integrity sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ== -"@types/underscore@*": - version "1.11.11" - resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.11.11.tgz#d687e649dd7f3c4045b71f756cd80892d55d3bb1" - integrity sha512-J/ZgSP9Yv0S+wfUfeRh9ynktcCvycfW4S9NbzkFdiHLBth+Ctdy5nYg3ZAqUKq7v3gcJce6rXo41zJV6IqsXsQ== - "@types/unist@*", "@types/unist@^3.0.0": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.2.tgz#6dd61e43ef60b34086287f83683a5c1b2dc53d20" @@ -6807,21 +6173,6 @@ resolved "https://registry.yarnpkg.com/@types/w3c-web-usb/-/w3c-web-usb-1.0.8.tgz#c593fef468b6e6051209c8aa89d1ead08005e23d" integrity sha512-ouEoUTyB27wFXUUyl0uKIE6VkeCczDtazWTiZGD1M4onceJnp8KnHDf7CzLbpwzek2ZFWXTC5KrNDRc9q/Jf6Q== -"@types/web3-provider-engine@^14.0.0": - version "14.0.2" - resolved "https://registry.yarnpkg.com/@types/web3-provider-engine/-/web3-provider-engine-14.0.2.tgz#ff571f2077abab015616edec3b6437e8dc1f8e40" - integrity sha512-i+vgIh873kDu6MnYZkIqrho4JCan1c8TcPnYY6te2lq1ODD4SPA8JxFCyQjK+vwbLMr5F3N/I37AfK/wxiyuEA== - dependencies: - "@types/ethereum-protocol" "*" - -"@types/web3@1.0.20": - version "1.0.20" - resolved "https://registry.yarnpkg.com/@types/web3/-/web3-1.0.20.tgz#234dd1f976702c0daaff147c80f24a5582e09d0e" - integrity sha512-KTDlFuYjzCUlBDGt35Ir5QRtyV9klF84MMKUsEJK10sTWga/71V+8VYLT7yysjuBjaOx2uFYtIWNGoz3yrNDlg== - dependencies: - "@types/bn.js" "*" - "@types/underscore" "*" - "@types/web@^0.0.100": version "0.0.100" resolved "https://registry.yarnpkg.com/@types/web/-/web-0.0.100.tgz#174f5952c40ab0940b0aa04e76d2f2776005b8c6" @@ -7089,7 +6440,53 @@ events "^3.3.0" isomorphic-unfetch "^3.1.0" -"@walletconnect/core@2.11.2", "@walletconnect/core@2.11.3", "@walletconnect/core@^2.10.1", "@walletconnect/core@^2.11.2": +"@walletconnect/core@2.11.2": + version "2.11.2" + resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.11.2.tgz#35286be92c645fa461fecc0dfe25de9f076fca8f" + integrity sha512-bB4SiXX8hX3/hyBfVPC5gwZCXCl+OPj+/EDVM71iAO3TDsh78KPbrVAbDnnsbHzZVHlsMohtXX3j5XVsheN3+g== + dependencies: + "@walletconnect/heartbeat" "1.2.1" + "@walletconnect/jsonrpc-provider" "1.0.13" + "@walletconnect/jsonrpc-types" "1.0.3" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/jsonrpc-ws-connection" "1.0.14" + "@walletconnect/keyvaluestorage" "^1.1.1" + "@walletconnect/logger" "^2.0.1" + "@walletconnect/relay-api" "^1.0.9" + "@walletconnect/relay-auth" "^1.0.4" + "@walletconnect/safe-json" "^1.0.2" + "@walletconnect/time" "^1.0.2" + "@walletconnect/types" "2.11.2" + "@walletconnect/utils" "2.11.2" + events "^3.3.0" + isomorphic-unfetch "3.1.0" + lodash.isequal "4.5.0" + uint8arrays "^3.1.0" + +"@walletconnect/core@2.13.1": + version "2.13.1" + resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.13.1.tgz#a59646e39a5beaa3f3551d129af43cd404cf4faf" + integrity sha512-h0MSYKJu9i1VEs5koCTT7c5YeQ1Kj0ncTFiMqANbDnB1r3mBulXn+FKtZ2fCmf1j7KDpgluuUzpSs+sQfPcv4Q== + dependencies: + "@walletconnect/heartbeat" "1.2.2" + "@walletconnect/jsonrpc-provider" "1.0.14" + "@walletconnect/jsonrpc-types" "1.0.4" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/jsonrpc-ws-connection" "1.0.14" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/logger" "2.1.2" + "@walletconnect/relay-api" "1.0.10" + "@walletconnect/relay-auth" "1.0.4" + "@walletconnect/safe-json" "1.0.2" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.13.1" + "@walletconnect/utils" "2.13.1" + events "3.3.0" + isomorphic-unfetch "3.1.0" + lodash.isequal "4.5.0" + uint8arrays "3.1.0" + +"@walletconnect/core@^2.10.1": version "2.11.3" resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.11.3.tgz#c81855722cb9afd411f91f5345c7874f48bade0b" integrity sha512-/9m4EqiggFUwkQDv5PDWbcTI+yCVnBd/iYW5iIHEkivg2/mnBr2bQz2r/vtPjp19r/ZK62Dx0+UN3U+BWP8ulQ== @@ -7135,7 +6532,7 @@ "@walletconnect/utils" "2.11.2" events "^3.3.0" -"@walletconnect/events@^1.0.1": +"@walletconnect/events@1.0.1", "@walletconnect/events@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/events/-/events-1.0.1.tgz#2b5f9c7202019e229d7ccae1369a9e86bda7816c" integrity sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ== @@ -7152,6 +6549,15 @@ "@walletconnect/time" "^1.0.2" tslib "1.14.1" +"@walletconnect/heartbeat@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@walletconnect/heartbeat/-/heartbeat-1.2.2.tgz#e8dc5179db7769950c6f9cf59b23516d9b95227d" + integrity sha512-uASiRmC5MwhuRuf05vq4AT48Pq8RMi876zV8rr8cV969uTOzWdB/k+Lj5yI2PBtB1bGQisGen7MM1GcZlQTBXw== + dependencies: + "@walletconnect/events" "^1.0.1" + "@walletconnect/time" "^1.0.2" + events "^3.3.0" + "@walletconnect/jsonrpc-http-connection@^1.0.7": version "1.0.7" resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-http-connection/-/jsonrpc-http-connection-1.0.7.tgz#a6973569b8854c22da707a759d241e4f5c2d5a98" @@ -7171,6 +6577,15 @@ "@walletconnect/safe-json" "^1.0.2" tslib "1.14.1" +"@walletconnect/jsonrpc-provider@1.0.14": + version "1.0.14" + resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-provider/-/jsonrpc-provider-1.0.14.tgz#696f3e3b6d728b361f2e8b853cfc6afbdf2e4e3e" + integrity sha512-rtsNY1XqHvWj0EtITNeuf8PHMvlCLiS3EjQL+WOkxEOA4KPxsohFnBDeyPYiNm4ZvkQdLnece36opYidmtbmow== + dependencies: + "@walletconnect/jsonrpc-utils" "^1.0.8" + "@walletconnect/safe-json" "^1.0.2" + events "^3.3.0" + "@walletconnect/jsonrpc-types@1.0.3", "@walletconnect/jsonrpc-types@^1.0.2", "@walletconnect/jsonrpc-types@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-types/-/jsonrpc-types-1.0.3.tgz#65e3b77046f1a7fa8347ae02bc1b841abe6f290c" @@ -7179,6 +6594,14 @@ keyvaluestorage-interface "^1.0.0" tslib "1.14.1" +"@walletconnect/jsonrpc-types@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-types/-/jsonrpc-types-1.0.4.tgz#ce1a667d79eadf2a2d9d002c152ceb68739c230c" + integrity sha512-P6679fG/M+wuWg9TY8mh6xFSdYnFyFjwFelxyISxMDrlbXokorEVXYOxiqEbrU3x1BmBoCAJJ+vtEaEoMlpCBQ== + dependencies: + events "^3.3.0" + keyvaluestorage-interface "^1.0.0" + "@walletconnect/jsonrpc-utils@1.0.8", "@walletconnect/jsonrpc-utils@^1.0.6", "@walletconnect/jsonrpc-utils@^1.0.7", "@walletconnect/jsonrpc-utils@^1.0.8": version "1.0.8" resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-utils/-/jsonrpc-utils-1.0.8.tgz#82d0cc6a5d6ff0ecc277cb35f71402c91ad48d72" @@ -7198,7 +6621,7 @@ events "^3.3.0" ws "^7.5.1" -"@walletconnect/keyvaluestorage@^1.1.1": +"@walletconnect/keyvaluestorage@1.1.1", "@walletconnect/keyvaluestorage@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz#dd2caddabfbaf80f6b8993a0704d8b83115a1842" integrity sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA== @@ -7207,7 +6630,15 @@ idb-keyval "^6.2.1" unstorage "^1.9.0" -"@walletconnect/logger@2.0.1", "@walletconnect/logger@^2.0.1": +"@walletconnect/logger@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@walletconnect/logger/-/logger-2.1.2.tgz#813c9af61b96323a99f16c10089bfeb525e2a272" + integrity sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw== + dependencies: + "@walletconnect/safe-json" "^1.0.2" + pino "7.11.0" + +"@walletconnect/logger@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/logger/-/logger-2.0.1.tgz#7f489b96e9a1ff6bf3e58f0fbd6d69718bf844a8" integrity sha512-SsTKdsgWm+oDTBeNE/zHxxr5eJfZmE9/5yp/Ku+zJtcTAjELb3DXueWkDXmE9h8uHIbJzIb5wj5lPdzyrjT6hQ== @@ -7240,6 +6671,13 @@ "@walletconnect/modal-core" "2.6.2" "@walletconnect/modal-ui" "2.6.2" +"@walletconnect/relay-api@1.0.10": + version "1.0.10" + resolved "https://registry.yarnpkg.com/@walletconnect/relay-api/-/relay-api-1.0.10.tgz#5aef3cd07c21582b968136179aa75849dcc65499" + integrity sha512-tqrdd4zU9VBNqUaXXQASaexklv6A54yEyQQEXYOCr+Jz8Ket0dmPBDyg19LVSNUN2cipAghQc45/KVmfFJ0cYw== + dependencies: + "@walletconnect/jsonrpc-types" "^1.0.2" + "@walletconnect/relay-api@^1.0.9": version "1.0.9" resolved "https://registry.yarnpkg.com/@walletconnect/relay-api/-/relay-api-1.0.9.tgz#f8c2c3993dddaa9f33ed42197fc9bfebd790ecaf" @@ -7248,7 +6686,7 @@ "@walletconnect/jsonrpc-types" "^1.0.2" tslib "1.14.1" -"@walletconnect/relay-auth@^1.0.4": +"@walletconnect/relay-auth@1.0.4", "@walletconnect/relay-auth@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@walletconnect/relay-auth/-/relay-auth-1.0.4.tgz#0b5c55c9aa3b0ef61f526ce679f3ff8a5c4c2c7c" integrity sha512-kKJcS6+WxYq5kshpPaxGHdwf5y98ZwbfuS4EE/NkQzqrDFm5Cj+dP8LofzWvjrrLkZq7Afy7WrQMXdLy8Sx7HQ== @@ -7260,7 +6698,7 @@ tslib "1.14.1" uint8arrays "^3.0.0" -"@walletconnect/safe-json@^1.0.1", "@walletconnect/safe-json@^1.0.2": +"@walletconnect/safe-json@1.0.2", "@walletconnect/safe-json@^1.0.1", "@walletconnect/safe-json@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@walletconnect/safe-json/-/safe-json-1.0.2.tgz#7237e5ca48046e4476154e503c6d3c914126fa77" integrity sha512-Ogb7I27kZ3LPC3ibn8ldyUr5544t3/STow9+lzz7Sfo808YD7SBWk7SAsdBFlYgP2zDRy2hS3sKRcuSRM0OTmA== @@ -7282,22 +6720,22 @@ "@walletconnect/utils" "2.11.2" events "^3.3.0" -"@walletconnect/sign-client@2.11.3": - version "2.11.3" - resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.11.3.tgz#3ea7b3acf92ee31cc42b45d42e66c44b4720b28b" - integrity sha512-JVjLTxN/3NjMXv5zalSGKuSYLRyU2yX6AWEdq17cInlrwODpbWZr6PS1uxMWdH4r90DXBLhdtwDbEq/pfd0BPg== +"@walletconnect/sign-client@2.13.1": + version "2.13.1" + resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.13.1.tgz#7bdc9226218fd33caf3aef69dff0b4140abc7fa8" + integrity sha512-e+dcqcLsedB4ZjnePFM5Cy8oxu0dyz5iZfhfKH/MOrQV/hyhZ+hJwh4MmkO2QyEu2PERKs9o2Uc6x8RZdi0UAQ== dependencies: - "@walletconnect/core" "2.11.3" - "@walletconnect/events" "^1.0.1" - "@walletconnect/heartbeat" "1.2.1" + "@walletconnect/core" "2.13.1" + "@walletconnect/events" "1.0.1" + "@walletconnect/heartbeat" "1.2.2" "@walletconnect/jsonrpc-utils" "1.0.8" - "@walletconnect/logger" "^2.0.1" - "@walletconnect/time" "^1.0.2" - "@walletconnect/types" "2.11.3" - "@walletconnect/utils" "2.11.3" - events "^3.3.0" + "@walletconnect/logger" "2.1.2" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.13.1" + "@walletconnect/utils" "2.13.1" + events "3.3.0" -"@walletconnect/time@^1.0.2": +"@walletconnect/time@1.0.2", "@walletconnect/time@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@walletconnect/time/-/time-1.0.2.tgz#6c5888b835750ecb4299d28eecc5e72c6d336523" integrity sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g== @@ -7316,7 +6754,7 @@ "@walletconnect/logger" "^2.0.1" events "^3.3.0" -"@walletconnect/types@2.11.3", "@walletconnect/types@^2.11.3": +"@walletconnect/types@2.11.3": version "2.11.3" resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.11.3.tgz#8ce43cb77e8fd9d5269847cdd73bcfa7cce7dd1a" integrity sha512-JY4wA9MVosDW9dcJMTpnwliste0aJGJ1X6Q4ulLsQsgWRSEBRkLila0oUT01TDBW9Yq8uUp7uFOUTaKx6KWVAg== @@ -7328,10 +6766,17 @@ "@walletconnect/logger" "^2.0.1" events "^3.3.0" -"@walletconnect/types@~1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-1.8.0.tgz#3f5e85b2d6b149337f727ab8a71b8471d8d9a195" - integrity sha512-Cn+3I0V0vT9ghMuzh1KzZvCkiAxTq+1TR2eSqw5E5AVWfmCtECFkVZBP6uUJZ8YjwLqXheI+rnjqPy7sVM4Fyg== +"@walletconnect/types@2.13.1", "@walletconnect/types@^2.13.1": + version "2.13.1" + resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.13.1.tgz#393e3bd4d60a755f3a70cbe769b58cf153450310" + integrity sha512-CIrdt66d38xdunGCy5peOOP17EQkCEGKweXc3+Gn/RWeSiRU35I7wjC/Bp4iWcgAQ6iBTZv4jGGST5XyrOp+Pg== + dependencies: + "@walletconnect/events" "1.0.1" + "@walletconnect/heartbeat" "1.2.2" + "@walletconnect/jsonrpc-types" "1.0.4" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/logger" "2.1.2" + events "3.3.0" "@walletconnect/universal-provider@2.11.2": version "2.11.2" @@ -7368,7 +6813,7 @@ query-string "7.1.3" uint8arrays "^3.1.0" -"@walletconnect/utils@2.11.3", "@walletconnect/utils@^2.10.1", "@walletconnect/utils@^2.11.3": +"@walletconnect/utils@2.11.3", "@walletconnect/utils@^2.10.1": version "2.11.3" resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.11.3.tgz#3731809b54902655cf202e0bf0e8f268780e8b54" integrity sha512-jsdNkrl/IcTkzWFn0S2d0urzBXg6RxVJtUYRsUx3qI3wzOGiABP9ui3yiZ3SgZOv9aRe62PaNp1qpbYZ+zPb8Q== @@ -7388,28 +6833,48 @@ query-string "7.1.3" uint8arrays "^3.1.0" -"@walletconnect/web3wallet@^1.10.3": - version "1.10.3" - resolved "https://registry.yarnpkg.com/@walletconnect/web3wallet/-/web3wallet-1.10.3.tgz#8195308757bd298ccc9caa6e3fe9f4ff82b94607" - integrity sha512-1Dr2P8KIDCqEWZ+s4coKGJz/+pj87ogFs+icPDXPu9QpzTgY5Y1WSzuAHaqoY5gTlL7WS58YP49s0E7iacUz4g== +"@walletconnect/utils@2.13.1", "@walletconnect/utils@^2.13.1": + version "2.13.1" + resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.13.1.tgz#f44e81028754c6e056dba588ad9b9fa5ad047645" + integrity sha512-EcooXXlqy5hk9hy/nK2wBF/qxe7HjH0K8ZHzjKkXRkwAE5pCvy0IGXIXWmUR9sw8LFJEqZyd8rZdWLKNUe8hqA== + dependencies: + "@stablelib/chacha20poly1305" "1.0.1" + "@stablelib/hkdf" "1.0.1" + "@stablelib/random" "1.0.2" + "@stablelib/sha256" "1.0.1" + "@stablelib/x25519" "1.0.3" + "@walletconnect/relay-api" "1.0.10" + "@walletconnect/safe-json" "1.0.2" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.13.1" + "@walletconnect/window-getters" "1.0.1" + "@walletconnect/window-metadata" "1.0.1" + detect-browser "5.3.0" + query-string "7.1.3" + uint8arrays "3.1.0" + +"@walletconnect/web3wallet@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@walletconnect/web3wallet/-/web3wallet-1.12.1.tgz#efe7863c6518b2262bca1ea01650222986963cc4" + integrity sha512-34h7UkWjZvZdtCc/t6tZCSBPjDzJbfG1+OPkJ6FiD1KJP+a0wSwuI7l4LliGgvAdsXfrM+sn3ZEWVWy62zeRDA== dependencies: "@walletconnect/auth-client" "2.1.2" - "@walletconnect/core" "2.11.3" - "@walletconnect/jsonrpc-provider" "1.0.13" + "@walletconnect/core" "2.13.1" + "@walletconnect/jsonrpc-provider" "1.0.14" "@walletconnect/jsonrpc-utils" "1.0.8" - "@walletconnect/logger" "2.0.1" - "@walletconnect/sign-client" "2.11.3" - "@walletconnect/types" "2.11.3" - "@walletconnect/utils" "2.11.3" + "@walletconnect/logger" "2.1.2" + "@walletconnect/sign-client" "2.13.1" + "@walletconnect/types" "2.13.1" + "@walletconnect/utils" "2.13.1" -"@walletconnect/window-getters@^1.0.1": +"@walletconnect/window-getters@1.0.1", "@walletconnect/window-getters@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/window-getters/-/window-getters-1.0.1.tgz#f36d1c72558a7f6b87ecc4451fc8bd44f63cbbdc" integrity sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q== dependencies: tslib "1.14.1" -"@walletconnect/window-metadata@^1.0.1": +"@walletconnect/window-metadata@1.0.1", "@walletconnect/window-metadata@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/window-metadata/-/window-metadata-1.0.1.tgz#2124f75447b7e989e4e4e1581d55d25bc75f7be5" integrity sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA== @@ -7525,103 +6990,6 @@ joi "17.9.1" rxjs "^7.5.2" -"@web3auth-mpc/base-provider@^2.1.9": - version "2.1.9" - resolved "https://registry.yarnpkg.com/@web3auth-mpc/base-provider/-/base-provider-2.1.9.tgz#407aca075deb981b5cae80567828526aecf385d7" - integrity sha512-i3NmTDk8ezaZXz+wtR1O931DBq9cDfyCzYI/XHFHA0zS0iealCSHXXKD6iACByoUuFDBNSq7GdgZCJNcoqWOGw== - dependencies: - "@toruslabs/base-controllers" "^2.2.6" - "@toruslabs/openlogin-jrpc" "^2.6.0" - "@web3auth-mpc/base" "^2.1.9" - eth-rpc-errors "^4.0.3" - json-rpc-random-id "^1.0.1" - -"@web3auth-mpc/base@^2.1.9": - version "2.1.9" - resolved "https://registry.yarnpkg.com/@web3auth-mpc/base/-/base-2.1.9.tgz#1d1768f5b7c955c5a7e8e71a6590aeee49352604" - integrity sha512-T91A+1Vu4C3//5Hj94bhMUaerOu3nVwGfELON2FH16yXNPQsNZPwyBuBoROUYdTp4fOZvCG4OtLiw+g8dPncaA== - dependencies: - "@toruslabs/http-helpers" "^3.2.0" - "@toruslabs/openlogin-jrpc" "^2.6.0" - jwt-decode "^3.1.2" - loglevel "^1.8.0" - ts-custom-error "^3.2.2" - -"@web3auth-mpc/ethereum-provider@^2.3.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@web3auth-mpc/ethereum-provider/-/ethereum-provider-2.3.0.tgz#000e83696ee6894879a4a8aaef2fd2df5ae3cc6c" - integrity sha512-2RY8GPd2UJ04P4Gzjt+Uec00j721LIceSJoHlwhOPGYNPx0QpRsAmwOmMRNLBA5Y+beKrnQ8b8rGDF9wBUaqtA== - dependencies: - "@ethereumjs/common" "^3.0.0" - "@ethereumjs/rlp" "^4.0.0" - "@ethereumjs/tx" "^4.0.0" - "@ethereumjs/util" "^8.0.0" - "@metamask/eth-sig-util" "^5.0.0" - "@toruslabs/base-controllers" "^2.2.6" - "@toruslabs/http-helpers" "^3.2.0" - "@toruslabs/openlogin-jrpc" "^2.6.0" - "@walletconnect/types" "~1.8.0" - "@web3auth-mpc/base" "^2.1.9" - "@web3auth-mpc/base-provider" "^2.1.9" - assert "^2.0.0" - bignumber.js "^9.1.0" - bn.js "^5.2.1" - eth-rpc-errors "^4.0.3" - jsonschema "^1.4.1" - -"@web3auth/base-provider@^7.0.1": - version "7.0.4" - resolved "https://registry.yarnpkg.com/@web3auth/base-provider/-/base-provider-7.0.4.tgz#a0152ae15585e3f51a7bf5b6f9c1d20f55a52625" - integrity sha512-DDE/K6AP2UBRQqBJUoFggGt7ylQ96pexVnaps9Yw8JOtx573yzVUPs2tYEjhxmAfRGgmDD1J8juokgvCEeKf4g== - dependencies: - "@metamask/rpc-errors" "^6.0.0" - "@toruslabs/base-controllers" "^4.2.0" - "@toruslabs/openlogin-jrpc" "^5.2.0" - "@web3auth/base" "^7.0.4" - json-rpc-random-id "^1.0.1" - -"@web3auth/base@^7.0.1", "@web3auth/base@^7.0.4": - version "7.0.4" - resolved "https://registry.yarnpkg.com/@web3auth/base/-/base-7.0.4.tgz#93a165486ee991938b87a5fa8fe97da7c58a15b3" - integrity sha512-ufpJ0uqOp7k7uGqDS1DohOeIIrsTLUAdyiExM5D+SQYYIXri3M9wbFgyCeAqHbJbfBO6hI9PDcY82QvOGCMI3w== - dependencies: - "@toruslabs/http-helpers" "^5.0.0" - "@toruslabs/openlogin" "^5.2.0" - "@toruslabs/openlogin-jrpc" "^5.2.0" - "@toruslabs/openlogin-utils" "^5.2.0" - jwt-decode "^3.1.2" - loglevel "^1.8.1" - ts-custom-error "^3.3.1" - -"@web3auth/mpc-core-kit@^1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@web3auth/mpc-core-kit/-/mpc-core-kit-1.1.3.tgz#01c508157f11d5a6685fa578bb873f40678fa403" - integrity sha512-wpbrBDBxZ8vi7oY3zL3BEktKuwfs5miMga4R7qmc12k79gNJa8S6FvZQ38wZkwNJyrRb0iFQ1kOWR0NOteeMyg== - dependencies: - "@tkey-mpc/chrome-storage" "^9.0.2" - "@tkey-mpc/common-types" "^9.0.2" - "@tkey-mpc/core" "^9.0.2" - "@tkey-mpc/security-questions" "^9.0.2" - "@tkey-mpc/service-provider-torus" "^9.0.3" - "@tkey-mpc/share-serialization" "^9.0.2" - "@tkey-mpc/storage-layer-torus" "^9.0.2" - "@toruslabs/constants" "^13.0.1" - "@toruslabs/customauth" "^16.0.6" - "@toruslabs/eccrypto" "4.0.0" - "@toruslabs/fetch-node-details" "^13.0.1" - "@toruslabs/fnd-base" "^13.0.1" - "@toruslabs/metadata-helpers" "^5.x" - "@toruslabs/openlogin-session-manager" "^3.0.0" - "@toruslabs/torus.js" "^11.0.6" - "@toruslabs/tss-client" "^1.7.1" - "@toruslabs/tss-lib" "^1.7.1" - "@web3auth-mpc/ethereum-provider" "^2.3.0" - "@web3auth/base" "^7.0.1" - "@web3auth/base-provider" "^7.0.1" - bn.js "^5.2.1" - bowser "^2.11.0" - elliptic "^6.5.4" - "@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" @@ -7794,10 +7162,10 @@ abab@^2.0.6: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== -abitype@0.9.8: - version "0.9.8" - resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.9.8.tgz#1f120b6b717459deafd213dfbf3a3dd1bf10ae8c" - integrity sha512-puLifILdm+8sjyss4S+fsUN09obiT1g2YW6CtcQF+QDzxR0euzgEB29MZujC6zMk2a6SVmtttq1fc6+YFA7WYQ== +abitype@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.0.tgz#237176dace81d90d018bebf3a45cb42f2a2d9e97" + integrity sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ== abort-controller@^3.0.0: version "3.0.0" @@ -7806,25 +7174,11 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" -abortcontroller-polyfill@^1.7.3, abortcontroller-polyfill@^1.7.5: +abortcontroller-polyfill@^1.7.5: version "1.7.5" resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz#6738495f4e901fbb57b6c0611d0c75f76c485bed" integrity sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ== -abstract-leveldown@~2.6.0: - version "2.6.3" - resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-2.6.3.tgz#1c5e8c6a5ef965ae8c35dfb3a8770c476b82c4b8" - integrity sha512-2++wDf/DYqkPR3o5tbfdhF96EfMApo1GpPfzOsR/ZYXdkSmELlvOOEAl9iKkRsktMPHdGjO4rtkBpf2I7TiTeA== - dependencies: - xtend "~4.0.0" - -abstract-leveldown@~2.7.1: - version "2.7.2" - resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-2.7.2.tgz#87a44d7ebebc341d59665204834c8b7e0932cc93" - integrity sha512-+OVvxH2rHVEhWLdbudP6p0+dNMXu8JA1CbhP19T8paTYAcX7oJ4OVjT+ZUVpv7mITxXHqDMej+GdqXBmXkw09w== - dependencies: - xtend "~4.0.0" - accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -8232,13 +7586,6 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== -async-eventemitter@^0.2.2: - version "0.2.4" - resolved "https://registry.yarnpkg.com/async-eventemitter/-/async-eventemitter-0.2.4.tgz#f5e7c8ca7d3e46aab9ec40a292baf686a0bafaca" - integrity sha512-pd20BwL7Yt1zwDFy+8MX8F1+WCT8aQeKj0kQnTrH9WaeRETlRamVhD0JtRPmrV4GfOJ2F9CvdQkZeZhnh2TuHw== - dependencies: - async "^2.4.0" - async-limiter@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" @@ -8251,25 +7598,6 @@ async-mutex@^0.2.6: dependencies: tslib "^2.0.0" -async-mutex@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.4.0.tgz#ae8048cd4d04ace94347507504b3cf15e631c25f" - integrity sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA== - dependencies: - tslib "^2.4.0" - -async@^1.4.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" - integrity sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w== - -async@^2.0.1, async@^2.1.2, async@^2.4.0, async@^2.5.0: - version "2.6.4" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" - integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== - dependencies: - lodash "^4.17.14" - async@^3.2.0, async@^3.2.3: version "3.2.4" resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" @@ -8458,13 +7786,6 @@ babel-preset-jest@^29.6.3: babel-plugin-jest-hoist "^29.6.3" babel-preset-current-node-syntax "^1.0.0" -backoff@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/backoff/-/backoff-2.5.0.tgz#f616eda9d3e4b66b8ca7fca79f695722c5f8e26f" - integrity sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA== - dependencies: - precond "0.2" - balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -8518,11 +7839,6 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -base64url@3.0.1, base64url@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" - integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== - bchaddrjs@^0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/bchaddrjs/-/bchaddrjs-0.5.2.tgz#1f52b5077329774e7c82d4882964628106bb11a0" @@ -8579,11 +7895,6 @@ bigint-buffer@^1.1.5: dependencies: bindings "^1.3.0" -bignumber.js@7.2.1: - version "7.2.1" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-7.2.1.tgz#80c048759d826800807c4bfd521e50edbba57a5f" - integrity sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ== - bignumber.js@^9.0.0, bignumber.js@^9.0.1, bignumber.js@^9.1.0, bignumber.js@^9.1.1, bignumber.js@^9.1.2: version "9.1.2" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" @@ -8613,13 +7924,6 @@ bip66@^1.1.5: dependencies: safe-buffer "^5.0.1" -biskviit@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/biskviit/-/biskviit-1.0.1.tgz#037a0cd4b71b9e331fd90a1122de17dc49e420a7" - integrity sha512-VGCXdHbdbpEkFgtjkeoBN8vRlbj1ZRX2/mxhE8asCCRalUx2nBzOomLJv8Aw/nRt5+ccDb+tPKidg4XxcfGW4w== - dependencies: - psl "^1.1.7" - bitcoin-ops@^1.3.0, bitcoin-ops@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz#e45de620398e22fd4ca6023de43974ff42240278" @@ -8677,7 +7981,7 @@ bn.js@4.11.6: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215" integrity sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA== -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.0, bn.js@^4.11.6, bn.js@^4.11.8, bn.js@^4.11.9, bn.js@^4.12.0: +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.0, bn.js@^4.11.6, bn.js@^4.11.8, bn.js@^4.11.9: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== @@ -8890,11 +8194,6 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" -btoa@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" - integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== - buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -9130,13 +8429,6 @@ check-more-types@^2.24.0: resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" integrity sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA== -checkpoint-store@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/checkpoint-store/-/checkpoint-store-1.1.0.tgz#04e4cb516b91433893581e6d4601a78e9552ea06" - integrity sha512-J/NdY2WvIx654cc6LWSq/IYFFCUf75fFTgwzFnmbqyORH4MwgiQCgswLLKBGzmsyTI5V7i5bp/So6sMbDWhedg== - dependencies: - functional-red-black-tree "^1.0.1" - "chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3, chokidar@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" @@ -9340,11 +8632,6 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== -clone@^2.0.0, clone@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" - integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== - clsx@^1.1.0, clsx@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" @@ -9689,14 +8976,6 @@ cross-env@^7.0.3: dependencies: cross-spawn "^7.0.1" -cross-fetch@^2.1.0: - version "2.2.6" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-2.2.6.tgz#2ef0bb39a24ac034787965c457368a28730e220a" - integrity sha512-9JZz+vXCmfKUZ68zAptS7k4Nu8e2qcibe7WVZYps7sAgk5R8GYTc+T1WR0v1rlP9HxgARmOX1UTIJZFytajpNA== - dependencies: - node-fetch "^2.6.7" - whatwg-fetch "^2.0.4" - cross-fetch@^3.1.4, cross-fetch@^3.1.5, cross-fetch@^3.1.6: version "3.1.8" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" @@ -9931,7 +9210,7 @@ debug@2.6.9, debug@^2.2.0: dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -10058,13 +9337,6 @@ defer-to-connect@^2.0.0, defer-to-connect@^2.0.1: resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== -deferred-leveldown@~1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/deferred-leveldown/-/deferred-leveldown-1.2.2.tgz#3acd2e0b75d1669924bc0a4b642851131173e1eb" - integrity sha512-uukrWD2bguRtXilKt6cAWKyoXrTSMo5m7crUdLfWQmu8kIm88w3QZoUL+6nhpfKVmhHANER6Re3sKoNoZ3IKMA== - dependencies: - abstract-leveldown "~2.6.0" - define-data-property@^1.0.1, define-data-property@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" @@ -10486,14 +9758,7 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -encoding@0.1.12: - version "0.1.12" - resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" - integrity sha512-bl1LAgiQc4ZWr++pNYUdRe/alecaHFeHxIJ/pNciqGdKXghaTCOwKkbKp6ye7pKZGu/GcaSXFk8PBVhgs+dJdA== - dependencies: - iconv-lite "~0.4.13" - -end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1, end-of-stream@^1.4.4: +end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -10509,22 +9774,6 @@ endent@^2.0.1: fast-json-parse "^1.0.3" objectorarray "^1.0.5" -engine.io-client@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.5.2.tgz#8709e22c291d4297ae80318d3c8baeae71f0e002" - integrity sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg== - dependencies: - "@socket.io/component-emitter" "~3.1.0" - debug "~4.3.1" - engine.io-parser "~5.2.1" - ws "~8.11.0" - xmlhttprequest-ssl "~2.0.0" - -engine.io-parser@~5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.1.tgz#9f213c77512ff1a6cc0c7a86108a7ffceb16fcfb" - integrity sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ== - enhanced-resolve@^5.12.0, enhanced-resolve@^5.16.0, enhanced-resolve@^5.7.0: version "5.16.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz#65ec88778083056cb32487faa9aef82ed0864787" @@ -10561,7 +9810,7 @@ err-code@^3.0.0, err-code@^3.0.1: resolved "https://registry.yarnpkg.com/err-code/-/err-code-3.0.1.tgz#a444c7b992705f2b120ee320b09972eef331c920" integrity sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA== -errno@^0.1.1, errno@~0.1.1: +errno@^0.1.1: version "0.1.8" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== @@ -11160,18 +10409,6 @@ eth-block-tracker@6.1.0: json-rpc-random-id "^1.0.1" pify "^3.0.0" -eth-block-tracker@^4.4.2: - version "4.4.3" - resolved "https://registry.yarnpkg.com/eth-block-tracker/-/eth-block-tracker-4.4.3.tgz#766a0a0eb4a52c867a28328e9ae21353812cf626" - integrity sha512-A8tG4Z4iNg4mw5tP1Vung9N9IjgMNqpiMoJ/FouSFwNCGHv2X0mmOYwtQOJzki6XN7r7Tyo01S29p7b224I4jw== - dependencies: - "@babel/plugin-transform-runtime" "^7.5.5" - "@babel/runtime" "^7.5.5" - eth-query "^2.1.0" - json-rpc-random-id "^1.0.1" - pify "^3.0.0" - safe-event-emitter "^1.0.1" - eth-crypto@^2.1.0: version "2.6.0" resolved "https://registry.yarnpkg.com/eth-crypto/-/eth-crypto-2.6.0.tgz#b777f367ae8c70e5917b3b7d52adab6b34841e29" @@ -11204,45 +10441,6 @@ eth-json-rpc-filters@5.1.0: json-rpc-engine "^6.1.0" pify "^5.0.0" -eth-json-rpc-filters@^4.2.1: - version "4.2.2" - resolved "https://registry.yarnpkg.com/eth-json-rpc-filters/-/eth-json-rpc-filters-4.2.2.tgz#eb35e1dfe9357ace8a8908e7daee80b2cd60a10d" - integrity sha512-DGtqpLU7bBg63wPMWg1sCpkKCf57dJ+hj/k3zF26anXMzkmtSBDExL8IhUu7LUd34f0Zsce3PYNO2vV2GaTzaw== - dependencies: - "@metamask/safe-event-emitter" "^2.0.0" - async-mutex "^0.2.6" - eth-json-rpc-middleware "^6.0.0" - eth-query "^2.1.2" - json-rpc-engine "^6.1.0" - pify "^5.0.0" - -eth-json-rpc-infura@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/eth-json-rpc-infura/-/eth-json-rpc-infura-5.1.0.tgz#e6da7dc47402ce64c54e7018170d89433c4e8fb6" - integrity sha512-THzLye3PHUSGn1EXMhg6WTLW9uim7LQZKeKaeYsS9+wOBcamRiCQVGHa6D2/4P0oS0vSaxsBnU/J6qvn0MPdow== - dependencies: - eth-json-rpc-middleware "^6.0.0" - eth-rpc-errors "^3.0.0" - json-rpc-engine "^5.3.0" - node-fetch "^2.6.0" - -eth-json-rpc-middleware@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/eth-json-rpc-middleware/-/eth-json-rpc-middleware-6.0.0.tgz#4fe16928b34231a2537856f08a5ebbc3d0c31175" - integrity sha512-qqBfLU2Uq1Ou15Wox1s+NX05S9OcAEL4JZ04VZox2NS0U+RtCMjSxzXhLFWekdShUPZ+P8ax3zCO2xcPrp6XJQ== - dependencies: - btoa "^1.2.1" - clone "^2.1.1" - eth-query "^2.1.2" - eth-rpc-errors "^3.0.0" - eth-sig-util "^1.4.2" - ethereumjs-util "^5.1.2" - json-rpc-engine "^5.3.0" - json-stable-stringify "^1.0.1" - node-fetch "^2.6.1" - pify "^3.0.0" - safe-event-emitter "^1.0.1" - eth-lib@0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/eth-lib/-/eth-lib-0.2.8.tgz#b194058bef4b220ad12ea497431d6cb6aa0623c8" @@ -11264,7 +10462,7 @@ eth-lib@^0.1.26: ws "^3.0.0" xhr-request-promise "^0.1.2" -eth-query@^2.1.0, eth-query@^2.1.2: +eth-query@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/eth-query/-/eth-query-2.1.2.tgz#d6741d9000106b51510c72db92d6365456a6da5e" integrity sha512-srES0ZcvwkR/wd5OQBRA1bIJMww1skfGS0s8wlwK3/oNP4+wnds60krvu5R1QbpRQjMmpG5OMIWro5s7gvDPsA== @@ -11279,28 +10477,13 @@ eth-rpc-errors@4.0.2: dependencies: fast-safe-stringify "^2.0.6" -eth-rpc-errors@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eth-rpc-errors/-/eth-rpc-errors-3.0.0.tgz#d7b22653c70dbf9defd4ef490fd08fe70608ca10" - integrity sha512-iPPNHPrLwUlR9xCSYm7HHQjWBasor3+KZfRvwEWxMz3ca0yqnlBeJrnyphkGIXZ4J7AMAaOLmwy4AWhnxOiLxg== - dependencies: - fast-safe-stringify "^2.0.6" - -eth-rpc-errors@^4.0.2, eth-rpc-errors@^4.0.3: +eth-rpc-errors@^4.0.2: version "4.0.3" resolved "https://registry.yarnpkg.com/eth-rpc-errors/-/eth-rpc-errors-4.0.3.tgz#6ddb6190a4bf360afda82790bb7d9d5e724f423a" integrity sha512-Z3ymjopaoft7JDoxZcEb3pwdGh7yiYMhOwm2doUt6ASXlMavpNlK6Cre0+IMl2VSGyEU9rkiperQhp5iRxn5Pg== dependencies: fast-safe-stringify "^2.0.6" -eth-sig-util@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/eth-sig-util/-/eth-sig-util-1.4.2.tgz#8d958202c7edbaae839707fba6f09ff327606210" - integrity sha512-iNZ576iTOGcfllftB73cPB5AN+XUQAT/T8xzsILsghXC1o8gJUqe3RHlcDqagu+biFpYQ61KQrZZJza8eRSYqw== - dependencies: - ethereumjs-abi "git+https://github.com/ethereumjs/ethereumjs-abi.git" - ethereumjs-util "^5.1.1" - ethereum-bloom-filters@^1.0.6: version "1.0.10" resolved "https://registry.yarnpkg.com/ethereum-bloom-filters/-/ethereum-bloom-filters-1.0.10.tgz#3ca07f4aed698e75bd134584850260246a5fed8a" @@ -11308,26 +10491,6 @@ ethereum-bloom-filters@^1.0.6: dependencies: js-sha3 "^0.8.0" -ethereum-common@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/ethereum-common/-/ethereum-common-0.2.0.tgz#13bf966131cce1eeade62a1b434249bb4cb120ca" - integrity sha512-XOnAR/3rntJgbCdGhqdaLIxDLWKLmsZOGhHdBKadEr6gEnJLH52k93Ou+TUdFaPN3hJc3isBZBal3U/XZ15abA== - -ethereum-common@^0.0.18: - version "0.0.18" - resolved "https://registry.yarnpkg.com/ethereum-common/-/ethereum-common-0.0.18.tgz#2fdc3576f232903358976eb39da783213ff9523f" - integrity sha512-EoltVQTRNg2Uy4o84qpa2aXymXDJhxm7eos/ACOg0DG4baAbMjhbdAEsx9GeE8sC3XCxnYvrrzZDH8D8MtA2iQ== - -ethereum-cryptography@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-1.1.2.tgz#74f2ac0f0f5fe79f012c889b3b8446a9a6264e6d" - integrity sha512-XDSJlg4BD+hq9N2FjvotwUET9Tfxpxc3kWGE2AqUG5vcbeunnbImVk3cj6e/xT3phdW21mE8R5IugU4fspQDcQ== - dependencies: - "@noble/hashes" "1.1.2" - "@noble/secp256k1" "1.6.3" - "@scure/bip32" "1.1.0" - "@scure/bip39" "1.1.0" - ethereum-cryptography@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-0.1.3.tgz#8d6143cfc3d74bf79bbd8edecdf29e4ae20dd191" @@ -11351,87 +10514,23 @@ ethereum-cryptography@^0.1.3: ethereum-cryptography@^2.0.0, ethereum-cryptography@^2.1.2: version "2.1.2" - resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-2.1.2.tgz#18fa7108622e56481157a5cb7c01c0c6a672eb67" - integrity sha512-Z5Ba0T0ImZ8fqXrJbpHcbpAvIswRte2wGNR/KePnu8GbbvgJ47lMxT/ZZPG6i9Jaht4azPDop4HaM00J0J59ug== - dependencies: - "@noble/curves" "1.1.0" - "@noble/hashes" "1.3.1" - "@scure/bip32" "1.3.1" - "@scure/bip39" "1.2.1" - -ethereum-protocol@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ethereum-protocol/-/ethereum-protocol-1.0.1.tgz#b7d68142f4105e0ae7b5e178cf42f8d4dc4b93cf" - integrity sha512-3KLX1mHuEsBW0dKG+c6EOJS1NBNqdCICvZW9sInmZTt5aY0oxmHVggYRE0lJu1tcnMD1K+AKHdLi6U43Awm1Vg== - -ethereumjs-abi@^0.6.8: - version "0.6.8" - resolved "https://registry.yarnpkg.com/ethereumjs-abi/-/ethereumjs-abi-0.6.8.tgz#71bc152db099f70e62f108b7cdfca1b362c6fcae" - integrity sha512-Tx0r/iXI6r+lRsdvkFDlut0N08jWMnKRZ6Gkq+Nmw75lZe4e6o3EkSnkaBP5NF6+m5PTGAr9JP43N3LyeoglsA== - dependencies: - bn.js "^4.11.8" - ethereumjs-util "^6.0.0" - -"ethereumjs-abi@git+https://github.com/ethereumjs/ethereumjs-abi.git": - version "0.6.8" - resolved "git+https://github.com/ethereumjs/ethereumjs-abi.git#ee3994657fa7a427238e6ba92a84d0b529bbcde0" - dependencies: - bn.js "^4.11.8" - ethereumjs-util "^6.0.0" - -ethereumjs-account@^2.0.3: - version "2.0.5" - resolved "https://registry.yarnpkg.com/ethereumjs-account/-/ethereumjs-account-2.0.5.tgz#eeafc62de544cb07b0ee44b10f572c9c49e00a84" - integrity sha512-bgDojnXGjhMwo6eXQC0bY6UK2liSFUSMwwylOmQvZbSl/D7NXQ3+vrGO46ZeOgjGfxXmgIeVNDIiHw7fNZM4VA== - dependencies: - ethereumjs-util "^5.0.0" - rlp "^2.0.0" - safe-buffer "^5.1.1" - -ethereumjs-block@^1.2.2: - version "1.7.1" - resolved "https://registry.yarnpkg.com/ethereumjs-block/-/ethereumjs-block-1.7.1.tgz#78b88e6cc56de29a6b4884ee75379b6860333c3f" - integrity sha512-B+sSdtqm78fmKkBq78/QLKJbu/4Ts4P2KFISdgcuZUPDm9x+N7qgBPIIFUGbaakQh8bzuquiRVbdmvPKqbILRg== - dependencies: - async "^2.0.1" - ethereum-common "0.2.0" - ethereumjs-tx "^1.2.2" - ethereumjs-util "^5.0.0" - merkle-patricia-tree "^2.1.2" - -ethereumjs-block@~2.2.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/ethereumjs-block/-/ethereumjs-block-2.2.2.tgz#c7654be7e22df489fda206139ecd63e2e9c04965" - integrity sha512-2p49ifhek3h2zeg/+da6XpdFR3GlqY3BIEiqxGF8j9aSRIgkb7M1Ky+yULBKJOu8PAZxfhsYA+HxUk2aCQp3vg== - dependencies: - async "^2.0.1" - ethereumjs-common "^1.5.0" - ethereumjs-tx "^2.1.1" - ethereumjs-util "^5.0.0" - merkle-patricia-tree "^2.1.2" - -ethereumjs-common@^1.1.0, ethereumjs-common@^1.5.0: - version "1.5.2" - resolved "https://registry.yarnpkg.com/ethereumjs-common/-/ethereumjs-common-1.5.2.tgz#2065dbe9214e850f2e955a80e650cb6999066979" - integrity sha512-hTfZjwGX52GS2jcVO6E2sx4YuFnf0Fhp5ylo4pEPhEffNln7vS59Hr5sLnp3/QCazFLluuBZ+FZ6J5HTp0EqCA== - -ethereumjs-tx@^1.2.2: - version "1.3.7" - resolved "https://registry.yarnpkg.com/ethereumjs-tx/-/ethereumjs-tx-1.3.7.tgz#88323a2d875b10549b8347e09f4862b546f3d89a" - integrity sha512-wvLMxzt1RPhAQ9Yi3/HKZTn0FZYpnsmQdbKYfUUpi4j1SEIcbkd9tndVjcPrufY3V7j2IebOpC00Zp2P/Ay2kA== + resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-2.1.2.tgz#18fa7108622e56481157a5cb7c01c0c6a672eb67" + integrity sha512-Z5Ba0T0ImZ8fqXrJbpHcbpAvIswRte2wGNR/KePnu8GbbvgJ47lMxT/ZZPG6i9Jaht4azPDop4HaM00J0J59ug== dependencies: - ethereum-common "^0.0.18" - ethereumjs-util "^5.0.0" + "@noble/curves" "1.1.0" + "@noble/hashes" "1.3.1" + "@scure/bip32" "1.3.1" + "@scure/bip39" "1.2.1" -ethereumjs-tx@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ethereumjs-tx/-/ethereumjs-tx-2.1.2.tgz#5dfe7688bf177b45c9a23f86cf9104d47ea35fed" - integrity sha512-zZEK1onCeiORb0wyCXUvg94Ve5It/K6GD1K+26KfFKodiBiS6d9lfCXlUKGBBdQ+bv7Day+JK0tj1K+BeNFRAw== +ethereumjs-abi@^0.6.8: + version "0.6.8" + resolved "https://registry.yarnpkg.com/ethereumjs-abi/-/ethereumjs-abi-0.6.8.tgz#71bc152db099f70e62f108b7cdfca1b362c6fcae" + integrity sha512-Tx0r/iXI6r+lRsdvkFDlut0N08jWMnKRZ6Gkq+Nmw75lZe4e6o3EkSnkaBP5NF6+m5PTGAr9JP43N3LyeoglsA== dependencies: - ethereumjs-common "^1.5.0" + bn.js "^4.11.8" ethereumjs-util "^6.0.0" -ethereumjs-util@7.1.5, ethereumjs-util@^7.0.7, ethereumjs-util@^7.0.8, ethereumjs-util@^7.1.0, ethereumjs-util@^7.1.1, ethereumjs-util@^7.1.2, ethereumjs-util@^7.1.3, ethereumjs-util@^7.1.4, ethereumjs-util@^7.1.5: +ethereumjs-util@7.1.5, ethereumjs-util@^7.0.7, ethereumjs-util@^7.0.8, ethereumjs-util@^7.1.3, ethereumjs-util@^7.1.4, ethereumjs-util@^7.1.5: version "7.1.5" resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz#9ecf04861e4fbbeed7465ece5f23317ad1129181" integrity sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg== @@ -11442,19 +10541,6 @@ ethereumjs-util@7.1.5, ethereumjs-util@^7.0.7, ethereumjs-util@^7.0.8, ethereumj ethereum-cryptography "^0.1.3" rlp "^2.2.4" -ethereumjs-util@^5.0.0, ethereumjs-util@^5.1.1, ethereumjs-util@^5.1.2, ethereumjs-util@^5.1.5: - version "5.2.1" - resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz#a833f0e5fca7e5b361384dc76301a721f537bf65" - integrity sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ== - dependencies: - bn.js "^4.11.0" - create-hash "^1.1.2" - elliptic "^6.5.2" - ethereum-cryptography "^0.1.3" - ethjs-util "^0.1.3" - rlp "^2.0.0" - safe-buffer "^5.1.1" - ethereumjs-util@^6.0.0, ethereumjs-util@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-6.2.1.tgz#fcb4e4dd5ceacb9d2305426ab1a5cd93e3163b69" @@ -11468,23 +10554,6 @@ ethereumjs-util@^6.0.0, ethereumjs-util@^6.2.1: ethjs-util "0.1.6" rlp "^2.2.3" -ethereumjs-vm@^2.3.4: - version "2.6.0" - resolved "https://registry.yarnpkg.com/ethereumjs-vm/-/ethereumjs-vm-2.6.0.tgz#76243ed8de031b408793ac33907fb3407fe400c6" - integrity sha512-r/XIUik/ynGbxS3y+mvGnbOKnuLo40V5Mj1J25+HEO63aWYREIqvWeRO/hnROlMBE5WoniQmPmhiaN0ctiHaXw== - dependencies: - async "^2.1.2" - async-eventemitter "^0.2.2" - ethereumjs-account "^2.0.3" - ethereumjs-block "~2.2.0" - ethereumjs-common "^1.1.0" - ethereumjs-util "^6.0.0" - fake-merkle-patricia-tree "^1.0.1" - functional-red-black-tree "^1.0.1" - merkle-patricia-tree "^2.3.2" - rustbn.js "~0.2.0" - safe-buffer "^5.1.1" - ethers@5.5.3: version "5.5.3" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.5.3.tgz#1e361516711c0c3244b6210e7e3ecabf0c75fca0" @@ -11614,7 +10683,7 @@ ethjs-unit@0.1.6: bn.js "4.11.6" number-to-bn "1.7.0" -ethjs-util@0.1.6, ethjs-util@^0.1.3, ethjs-util@^0.1.6: +ethjs-util@0.1.6, ethjs-util@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/ethjs-util/-/ethjs-util-0.1.6.tgz#f308b62f185f9fe6237132fb2a9818866a5cd536" integrity sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w== @@ -11650,7 +10719,7 @@ eventemitter3@^4.0.7: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== -events@^3.0.0, events@^3.2.0, events@^3.3.0: +events@3.3.0, events@^3.2.0, events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -11828,13 +10897,6 @@ fake-indexeddb@^4.0.2: dependencies: realistic-structured-clone "^3.0.0" -fake-merkle-patricia-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fake-merkle-patricia-tree/-/fake-merkle-patricia-tree-1.0.1.tgz#4b8c3acfb520afadf9860b1f14cd8ce3402cddd3" - integrity sha512-Tgq37lkc9pUIgIKw5uitNUKcgcYL3R6JvXtKQbOf/ZSavXbidsksgp/pAY6p//uhw0I4yoMsvTSovvVIsk/qxA== - dependencies: - checkpoint-store "^1.1.0" - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -11881,7 +10943,7 @@ fast-redact@^3.0.0: resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.3.0.tgz#7c83ce3a7be4898241a46560d51de10f653f7634" integrity sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ== -fast-safe-stringify@^2.0.6, fast-safe-stringify@^2.1.1: +fast-safe-stringify@^2.0.6: version "2.1.1" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== @@ -11924,14 +10986,6 @@ fetch-retry@^5.0.2: resolved "https://registry.yarnpkg.com/fetch-retry/-/fetch-retry-5.0.6.tgz#17d0bc90423405b7a88b74355bf364acd2a7fa56" integrity sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ== -fetch@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fetch/-/fetch-1.1.0.tgz#0a8279f06be37f9f0ebb567560a30a480da59a2e" - integrity sha512-5O8TwrGzoNblBG/jtK4NFuZwNCkZX6s5GfRNOaGtm+QGJEuNakSC/i2RW0R93KX6E0jVjNXm6O3CRN4Ql3K+yA== - dependencies: - biskviit "1.0.1" - encoding "0.1.12" - figures@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -12206,15 +11260,6 @@ forwarded@0.2.0: resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== -framer-motion@^10.13.1: - version "10.16.4" - resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-10.16.4.tgz#30279ef5499b8d85db3a298ee25c83429933e9f8" - integrity sha512-p9V9nGomS3m6/CALXqv6nFGMuFOxbWsmaOrdmhyQimMIlLl3LC7h7l86wge/Js/8cRu5ktutS/zlzgR7eBOtFA== - dependencies: - tslib "^2.4.0" - optionalDependencies: - "@emotion/is-prop-valid" "^0.8.2" - fresh@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" @@ -12324,11 +11369,6 @@ function.prototype.name@^1.1.5, function.prototype.name@^1.1.6: es-abstract "^1.22.1" functions-have-names "^1.2.3" -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== - functions-have-names@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" @@ -13033,7 +12073,7 @@ husky@^9.0.11: resolved "https://registry.yarnpkg.com/husky/-/husky-9.0.11.tgz#fc91df4c756050de41b3e478b2158b87c1e79af9" integrity sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw== -iconv-lite@0.4.24, iconv-lite@~0.4.13: +iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -13096,11 +12136,6 @@ image-size@~0.5.0: resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ== -immediate@^3.2.3: - version "3.3.0" - resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.3.0.tgz#1aef225517836bcdf7f2a2de2600c79ff0269266" - integrity sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q== - immer@^9.0.21: version "9.0.21" resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" @@ -13387,11 +12422,6 @@ is-finalizationregistry@^1.0.2: dependencies: call-bind "^1.0.2" -is-fn@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fn/-/is-fn-1.0.0.tgz#9543d5de7bcf5b08a22ec8a20bae6e286d510d8c" - integrity sha512-XoFPJQmsAShb3jEQRfzf2rqXavq7fIqF/jOekp308JlThqrODnMpweVSGilKTCXELfLhltGP2AGgbQGVP8F1dg== - is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -13676,10 +12706,10 @@ isomorphic-ws@^4.0.1: resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== -isows@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.3.tgz#93c1cf0575daf56e7120bab5c8c448b0809d0d74" - integrity sha512-2cKei4vlmg2cxEjm3wVSqn8pcoRF/LX/wpifuuNquFO4SQmPwarClT+SUCA2lt+l581tTeZIPIZuIDo2jWN1fg== +isows@1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.4.tgz#810cd0d90cc4995c26395d2aa4cfa4037ebdf061" + integrity sha512-hEzjY+x9u9hPmBom9IIAqdJCwNLax+xrPb51vEPpERoFlIxgmZcHzsT5jKG06nvInKOBGvReAVz80Umed5CczQ== isstream@~0.1.2: version "0.1.2" @@ -14346,14 +13376,6 @@ json-rpc-engine@6.1.0, json-rpc-engine@^6.1.0: "@metamask/safe-event-emitter" "^2.0.0" eth-rpc-errors "^4.0.2" -json-rpc-engine@^5.3.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-5.4.0.tgz#75758609d849e1dba1e09021ae473f3ab63161e5" - integrity sha512-rAffKbPoNDjuRnXkecTjnsE3xLLrb00rEkdgalINhaYVYIxDwWtvYBr9UFbhTvPB1B2qUOLoFd/cV6f4Q7mh7g== - dependencies: - eth-rpc-errors "^3.0.0" - safe-event-emitter "^1.0.1" - json-rpc-random-id@^1.0.0, json-rpc-random-id@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-rpc-random-id/-/json-rpc-random-id-1.0.1.tgz#ba49d96aded1444dbb8da3d203748acbbcdec8c8" @@ -14379,7 +13401,7 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -json-stable-stringify@^1.0.1, json-stable-stringify@^1.0.2: +json-stable-stringify@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.2.tgz#e06f23128e0bbe342dc996ed5a19e28b57b580e0" integrity sha512-eunSSaEnxV12z+Z73y/j5N37/In40GK4GmsSy+tEHJMxknvqnA7/djeYtAgW0GsWHUfg+847WJjKaEylk2y09g== @@ -14449,11 +13471,6 @@ jsonschema@1.2.2: resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.2.2.tgz#83ab9c63d65bf4d596f91d81195e78772f6452bc" integrity sha512-iX5OFQ6yx9NgbHCwse51ohhKgLuLL7Z5cNOeZOPIlDUtAMrxlruHLzVZxbltdHE5mEDXN+75oFOwq6Gn0MZwsA== -jsonschema@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.4.1.tgz#cc4c3f0077fb4542982973d8a083b6b34f482dab" - integrity sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ== - jsprim@^1.2.2: version "1.4.2" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" @@ -14489,30 +13506,7 @@ jsqr@^1.2.0: object.assign "^4.1.4" object.values "^1.1.6" -jwt-decode@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59" - integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== - -keccak256@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/keccak256/-/keccak256-1.0.6.tgz#dd32fb771558fed51ce4e45a035ae7515573da58" - integrity sha512-8GLiM01PkdJVGUhR1e6M/AvWnSqYS0HaERI+K/QtStGDGlSTx2B1zTqZk4Zlqu5TxHJNTxWAdP9Y+WI50OApUw== - dependencies: - bn.js "^5.2.0" - buffer "^6.0.3" - keccak "^3.0.2" - -keccak@3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.2.tgz#4c2c6e8c54e04f2670ee49fa734eb9da152206e0" - integrity sha512-PyKKjkH53wDMLGrvmRGSNWgmSxZOUqbnXwKL9tmgbFYA1iAYqW21kfR7mZXV0MlESiefxQQE9X9fTa3X+2MPDQ== - dependencies: - node-addon-api "^2.0.0" - node-gyp-build "^4.2.0" - readable-stream "^3.6.0" - -keccak@^3.0.0, keccak@^3.0.1, keccak@^3.0.2, keccak@^3.0.3: +keccak@^3.0.0, keccak@^3.0.1, keccak@^3.0.3: version "3.0.4" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.4.tgz#edc09b89e633c0549da444432ecf062ffadee86d" integrity sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q== @@ -14598,56 +13592,6 @@ less@^4.1.3: needle "^3.1.0" source-map "~0.6.0" -level-codec@~7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/level-codec/-/level-codec-7.0.1.tgz#341f22f907ce0f16763f24bddd681e395a0fb8a7" - integrity sha512-Ua/R9B9r3RasXdRmOtd+t9TCOEIIlts+TN/7XTT2unhDaL6sJn83S3rUyljbr6lVtw49N3/yA0HHjpV6Kzb2aQ== - -level-errors@^1.0.3: - version "1.1.2" - resolved "https://registry.yarnpkg.com/level-errors/-/level-errors-1.1.2.tgz#4399c2f3d3ab87d0625f7e3676e2d807deff404d" - integrity sha512-Sw/IJwWbPKF5Ai4Wz60B52yj0zYeqzObLh8k1Tk88jVmD51cJSKWSYpRyhVIvFzZdvsPqlH5wfhp/yxdsaQH4w== - dependencies: - errno "~0.1.1" - -level-errors@~1.0.3: - version "1.0.5" - resolved "https://registry.yarnpkg.com/level-errors/-/level-errors-1.0.5.tgz#83dbfb12f0b8a2516bdc9a31c4876038e227b859" - integrity sha512-/cLUpQduF6bNrWuAC4pwtUKA5t669pCsCi2XbmojG2tFeOr9j6ShtdDCtFFQO1DRt+EVZhx9gPzP9G2bUaG4ig== - dependencies: - errno "~0.1.1" - -level-iterator-stream@~1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/level-iterator-stream/-/level-iterator-stream-1.3.1.tgz#e43b78b1a8143e6fa97a4f485eb8ea530352f2ed" - integrity sha512-1qua0RHNtr4nrZBgYlpV0qHHeHpcRRWTxEZJ8xsemoHAXNL5tbooh4tPEEqIqsbWCAJBmUmkwYK/sW5OrFjWWw== - dependencies: - inherits "^2.0.1" - level-errors "^1.0.3" - readable-stream "^1.0.33" - xtend "^4.0.0" - -level-ws@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/level-ws/-/level-ws-0.0.0.tgz#372e512177924a00424b0b43aef2bb42496d228b" - integrity sha512-XUTaO/+Db51Uiyp/t7fCMGVFOTdtLS/NIACxE/GHsij15mKzxksZifKVjlXDF41JMUP/oM1Oc4YNGdKnc3dVLw== - dependencies: - readable-stream "~1.0.15" - xtend "~2.1.1" - -levelup@^1.2.1: - version "1.3.9" - resolved "https://registry.yarnpkg.com/levelup/-/levelup-1.3.9.tgz#2dbcae845b2bb2b6bea84df334c475533bbd82ab" - integrity sha512-VVGHfKIlmw8w1XqpGOAGwq6sZm2WwWLmlDcULkKWQXEA5EopA8OBNJ2Ck2v6bdk8HeEZSbCSEgzXadyQFm76sQ== - dependencies: - deferred-leveldown "~1.2.1" - level-codec "~7.0.0" - level-errors "~1.0.3" - level-iterator-stream "~1.3.0" - prr "~1.0.1" - semver "~5.4.1" - xtend "~4.0.0" - leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -14831,7 +13775,7 @@ lodash.uniqby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" integrity sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww== -lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0: +lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -14854,11 +13798,6 @@ log-update@^4.0.0: slice-ansi "^4.0.0" wrap-ansi "^6.2.0" -loglevel@^1.8.0, loglevel@^1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.1.tgz#5c621f83d5b48c54ae93b6156353f555963377b4" - integrity sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg== - long@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" @@ -14936,11 +13875,6 @@ lru-queue@^0.1.0: dependencies: es5-ext "~0.10.2" -ltgt@~2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5" - integrity sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA== - lz-string@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" @@ -15028,18 +13962,6 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== -memdown@^1.0.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/memdown/-/memdown-1.4.1.tgz#b4e4e192174664ffbae41361aa500f3119efe215" - integrity sha512-iVrGHZB8i4OQfM155xx8akvG9FIj+ht14DX5CQkCTG4EHzZ3d3sgckIf/Lm9ivZalEsFuEVnWv2B2WZvbrro2w== - dependencies: - abstract-leveldown "~2.7.1" - functional-red-black-tree "^1.0.1" - immediate "^3.2.3" - inherits "~2.0.1" - ltgt "~2.2.0" - safe-buffer "~5.1.1" - memfs@^3.4.1, memfs@^3.4.12: version "3.6.0" resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" @@ -15108,20 +14030,6 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -merkle-patricia-tree@^2.1.2, merkle-patricia-tree@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/merkle-patricia-tree/-/merkle-patricia-tree-2.3.2.tgz#982ca1b5a0fde00eed2f6aeed1f9152860b8208a" - integrity sha512-81PW5m8oz/pz3GvsAwbauj7Y00rqm81Tzad77tHBwU7pIAtN+TJnMSOJhxBKflSVYhptMMb9RskhqHqrSm1V+g== - dependencies: - async "^1.4.2" - ethereumjs-util "^5.0.0" - level-ws "0.0.0" - levelup "^1.2.1" - memdown "^1.0.0" - readable-stream "^2.0.0" - rlp "^2.0.0" - semaphore ">=1.0.1" - methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -15537,12 +14445,12 @@ next-tick@1, next-tick@^1.1.0: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== -next@^14.1.0: - version "14.1.0" - resolved "https://registry.yarnpkg.com/next/-/next-14.1.0.tgz#b31c0261ff9caa6b4a17c5af019ed77387174b69" - integrity sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q== +next@^14.1.1: + version "14.1.1" + resolved "https://registry.yarnpkg.com/next/-/next-14.1.1.tgz#92bd603996c050422a738e90362dff758459a171" + integrity sha512-McrGJqlGSHeaz2yTRPkEucxQKe5Zq7uPwyeHNmJaZNY4wx9E9QdxmTp310agFRoMuIYgQrCrT3petg13fSVOww== dependencies: - "@next/env" "14.1.0" + "@next/env" "14.1.1" "@swc/helpers" "0.5.2" busboy "1.6.0" caniuse-lite "^1.0.30001579" @@ -15550,15 +14458,15 @@ next@^14.1.0: postcss "8.4.31" styled-jsx "5.1.1" optionalDependencies: - "@next/swc-darwin-arm64" "14.1.0" - "@next/swc-darwin-x64" "14.1.0" - "@next/swc-linux-arm64-gnu" "14.1.0" - "@next/swc-linux-arm64-musl" "14.1.0" - "@next/swc-linux-x64-gnu" "14.1.0" - "@next/swc-linux-x64-musl" "14.1.0" - "@next/swc-win32-arm64-msvc" "14.1.0" - "@next/swc-win32-ia32-msvc" "14.1.0" - "@next/swc-win32-x64-msvc" "14.1.0" + "@next/swc-darwin-arm64" "14.1.1" + "@next/swc-darwin-x64" "14.1.1" + "@next/swc-linux-arm64-gnu" "14.1.1" + "@next/swc-linux-arm64-musl" "14.1.1" + "@next/swc-linux-x64-gnu" "14.1.1" + "@next/swc-linux-x64-musl" "14.1.1" + "@next/swc-win32-arm64-msvc" "14.1.1" + "@next/swc-win32-ia32-msvc" "14.1.1" + "@next/swc-win32-x64-msvc" "14.1.1" no-case@^3.0.4: version "3.0.4" @@ -15624,7 +14532,7 @@ node-fetch@2.6.7: dependencies: whatwg-url "^5.0.0" -node-fetch@^2.0.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7, node-fetch@^2.7.0: +node-fetch@^2.0.0, node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -15851,11 +14759,6 @@ objectorarray@^1.0.5: resolved "https://registry.yarnpkg.com/objectorarray/-/objectorarray-1.0.5.tgz#2c05248bbefabd8f43ad13b41085951aac5e68a5" integrity sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg== -oblivious-set@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.1.1.tgz#d9d38e9491d51f27a5c3ec1681d2ba40aa81e98b" - integrity sha512-Oh+8fK09mgGmAshFdH6hSVco6KZmd1tTwNFWj35OvzdmJTMZtAkbn05zar2iG3v6sDs1JLEtOiBGNb6BHwkb2w== - oboe@2.1.5: version "2.1.5" resolved "https://registry.yarnpkg.com/oboe/-/oboe-2.1.5.tgz#5554284c543a2266d7a38f17e073821fbde393cd" @@ -16498,11 +15401,6 @@ prebuild-install@^7.1.1: tar-fs "^2.0.0" tunnel-agent "^0.6.0" -precond@0.2: - version "0.2.3" - resolved "https://registry.yarnpkg.com/precond/-/precond-0.2.3.tgz#aa9591bcaa24923f1e0f4849d240f47efc1075ac" - integrity sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ== - prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -16576,14 +15474,6 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== -promise-to-callback@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/promise-to-callback/-/promise-to-callback-1.0.0.tgz#5d2a749010bfb67d963598fcd3960746a68feef7" - integrity sha512-uhMIZmKM5ZteDMfLgJnoSq9GCwsNKrYau73Awf1jIy6/eUcuuZ3P+CD9zUv0kJsIUbU+x6uLNIhXhLHDs1pNPA== - dependencies: - is-fn "^1.0.0" - set-immediate-shim "^1.0.1" - prompts@^2.0.1, prompts@^2.4.0: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -16684,7 +15574,7 @@ prr@~1.0.1: resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw== -psl@^1.1.28, psl@^1.1.33, psl@^1.1.7: +psl@^1.1.28, psl@^1.1.33: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== @@ -17141,17 +16031,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@^1.0.33: - version "1.1.14" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" - integrity sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - -readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.2.9, readable-stream@^2.3.8, readable-stream@~2.3.6: +readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.3.8, readable-stream@~2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -17173,7 +16053,7 @@ readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^4.0.0, readable-stream@^4.4.2: +readable-stream@^4.0.0: version "4.5.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== @@ -17184,7 +16064,7 @@ readable-stream@^4.0.0, readable-stream@^4.4.2: process "^0.11.10" string_decoder "^1.3.0" -readable-stream@~1.0.15, readable-stream@~1.0.17, readable-stream@~1.0.27-1: +readable-stream@~1.0.17, readable-stream@~1.0.27-1: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" integrity sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg== @@ -17384,7 +16264,7 @@ request-progress@^3.0.0: dependencies: throttleit "^1.0.0" -request@^2.79.0, request@^2.85.0: +request@^2.79.0: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -17621,7 +16501,7 @@ ripple-lib@^1.10.1: ripple-lib-transactionparser "0.8.2" ws "^7.2.0" -rlp@^2.0.0, rlp@^2.2.3, rlp@^2.2.4: +rlp@^2.2.3, rlp@^2.2.4: version "2.2.7" resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.2.7.tgz#33f31c4afac81124ac4b283e2bd4d9720b30beaf" integrity sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ== @@ -17672,11 +16552,6 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rustbn.js@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/rustbn.js/-/rustbn.js-0.2.0.tgz#8082cb886e707155fd1cb6f23bd591ab8d55d0ca" - integrity sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA== - rxjs@6, rxjs@^6.6.3, rxjs@^6.6.7: version "6.6.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" @@ -17718,13 +16593,6 @@ safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-event-emitter@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/safe-event-emitter/-/safe-event-emitter-1.0.1.tgz#5b692ef22329ed8f69fdce607e50ca734f6f20af" - integrity sha512-e1wFe99A91XYYxoQbcq2ZJUWurxEyP8vfz7A7vuUe1s95q8r5ebraVaA1BukYJcpM6V16ugWoD9vngi8Ccu5fg== - dependencies: - events "^3.0.0" - safe-regex-test@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" @@ -17844,15 +16712,6 @@ secp256k1@3.7.1: nan "^2.14.0" safe-buffer "^5.1.2" -secp256k1@4.0.3, secp256k1@^4.0.0, secp256k1@^4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.3.tgz#c4559ecd1b8d3c1827ed2d1b94190d69ce267303" - integrity sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA== - dependencies: - elliptic "^6.5.4" - node-addon-api "^2.0.0" - node-gyp-build "^4.2.0" - secp256k1@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-5.0.0.tgz#be6f0c8c7722e2481e9773336d351de8cddd12f7" @@ -17862,10 +16721,14 @@ secp256k1@5.0.0: node-addon-api "^5.0.0" node-gyp-build "^4.2.0" -semaphore@>=1.0.1, semaphore@^1.0.3: - version "1.1.0" - resolved "https://registry.yarnpkg.com/semaphore/-/semaphore-1.1.0.tgz#aaad8b86b20fe8e9b32b16dc2ee682a8cd26a8aa" - integrity sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA== +secp256k1@^4.0.0, secp256k1@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.3.tgz#c4559ecd1b8d3c1827ed2d1b94190d69ce267303" + integrity sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA== + dependencies: + elliptic "^6.5.4" + node-addon-api "^2.0.0" + node-gyp-build "^4.2.0" "semver@2 || 3 || 4 || 5", semver@^5.6.0: version "5.7.2" @@ -17891,11 +16754,6 @@ semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2, semve dependencies: lru-cache "^6.0.0" -semver@~5.4.1: - version "5.4.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" - integrity sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg== - send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -17915,13 +16773,6 @@ send@0.18.0: range-parser "~1.2.1" statuses "2.0.1" -serialize-error@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-8.1.0.tgz#3a069970c712f78634942ddd50fbbc0eaebe2f67" - integrity sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ== - dependencies: - type-fest "^0.20.2" - serialize-javascript@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" @@ -17983,11 +16834,6 @@ set-function-name@^2.0.0, set-function-name@^2.0.1: functions-have-names "^1.2.3" has-property-descriptors "^1.0.0" -set-immediate-shim@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" - integrity sha512-Li5AOqrZWCVA2n5kryzEmqai6bKSIvpz5oUJHPVj6+dsbD3X1ixtsY5tEnsaNpH3pFAHmG8eIHUrtEtohrg+UQ== - setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" @@ -18136,24 +16982,6 @@ smart-buffer@^4.2.0: resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== -socket.io-client@^4.6.1, socket.io-client@^4.7.2: - version "4.7.2" - resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.2.tgz#f2f13f68058bd4e40f94f2a1541f275157ff2c08" - integrity sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w== - dependencies: - "@socket.io/component-emitter" "~3.1.0" - debug "~4.3.2" - engine.io-client "~6.5.2" - socket.io-parser "~4.2.4" - -socket.io-parser@~4.2.4: - version "4.2.4" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" - integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== - dependencies: - "@socket.io/component-emitter" "~3.1.0" - debug "~4.3.1" - socks-proxy-agent@6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz#e664e8f1aaf4e1fb3df945f09e3d94f911137f87" @@ -19081,11 +17909,6 @@ ts-command-line-args@^2.2.0: command-line-usage "^6.1.0" string-format "^2.0.0" -ts-custom-error@^3.2.2, ts-custom-error@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/ts-custom-error/-/ts-custom-error-3.3.1.tgz#8bd3c8fc6b8dc8e1cb329267c45200f1e17a65d1" - integrity sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A== - ts-dedent@^2.0.0, ts-dedent@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5" @@ -19411,6 +18234,13 @@ uglify-js@^3.1.4: resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== +uint8arrays@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.1.0.tgz#8186b8eafce68f28bd29bd29d683a311778901e2" + integrity sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog== + dependencies: + multiformats "^9.4.2" + uint8arrays@^2.0.5, uint8arrays@^2.1.2: version "2.1.10" resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-2.1.10.tgz#34d023c843a327c676e48576295ca373c56e286a" @@ -19540,11 +18370,6 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== -unload@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/unload/-/unload-2.4.1.tgz#b0c5b7fb44e17fcbf50dcb8fb53929c59dd226a5" - integrity sha512-IViSAm8Z3sRBYA+9wc0fLQmU9Nrxb16rcDmIiR6Y9LJSZzI7QY5QsDhqPpKOjAn0O9/kfK1TfNEMMAGPTIraPw== - unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -19765,18 +18590,18 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -viem@^1.6.0: - version "1.20.3" - resolved "https://registry.yarnpkg.com/viem/-/viem-1.20.3.tgz#8b8360daee622295f5385949c02c86d943d14e0f" - integrity sha512-7CrmeCb2KYkeCgUmUyb1hsf+IX/PLwi+Np+Vm4YUTPeG82y3HRSgGHSaCOp3d0YtR2kXD3nv9y5kE7LBFE+wWw== +viem@^2.1.1: + version "2.13.8" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.13.8.tgz#d6aaeecc84e5ee5cac1566f103ed4cf0335ef811" + integrity sha512-JX8dOrCJKazNVs7YAahXnX+NANp0nlK16GyYjtQXILnar1daCPsLy4uzKgZDBVBD6DdRP2lsbPfo4X7QX3q5EQ== dependencies: "@adraffy/ens-normalize" "1.10.0" "@noble/curves" "1.2.0" "@noble/hashes" "1.3.2" "@scure/bip32" "1.3.2" "@scure/bip39" "1.2.1" - abitype "0.9.8" - isows "1.0.3" + abitype "1.0.0" + isows "1.0.4" ws "8.13.0" vm-browserify@^1.1.2: @@ -19820,15 +18645,6 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" -web3-bzz@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-bzz/-/web3-bzz-1.10.0.tgz#ac74bc71cdf294c7080a79091079192f05c5baed" - integrity sha512-o9IR59io3pDUsXTsps5pO5hW1D5zBmg46iNc2t4j2DkaYHNdDLwk2IP9ukoM2wg47QILfPEJYzhTfkS/CcX0KA== - dependencies: - "@types/node" "^12.12.6" - got "12.1.0" - swarm-js "^0.1.40" - web3-bzz@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-bzz/-/web3-bzz-1.10.3.tgz#13942b37757eb850f3500a8e08bf605448b67566" @@ -19838,14 +18654,6 @@ web3-bzz@1.10.3: got "12.1.0" swarm-js "^0.1.40" -web3-core-helpers@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-core-helpers/-/web3-core-helpers-1.10.0.tgz#1016534c51a5df77ed4f94d1fcce31de4af37fad" - integrity sha512-pIxAzFDS5vnbXvfvLSpaA1tfRykAe9adw43YCKsEYQwH0gCLL0kMLkaCX3q+Q8EVmAh+e1jWL/nl9U0de1+++g== - dependencies: - web3-eth-iban "1.10.0" - web3-utils "1.10.0" - web3-core-helpers@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-core-helpers/-/web3-core-helpers-1.10.3.tgz#f2db40ea57e888795e46f229b06113b60bcd671c" @@ -19854,17 +18662,6 @@ web3-core-helpers@1.10.3: web3-eth-iban "1.10.3" web3-utils "1.10.3" -web3-core-method@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-core-method/-/web3-core-method-1.10.0.tgz#82668197fa086e8cc8066742e35a9d72535e3412" - integrity sha512-4R700jTLAMKDMhQ+nsVfIXvH6IGJlJzGisIfMKWAIswH31h5AZz7uDUW2YctI+HrYd+5uOAlS4OJeeT9bIpvkA== - dependencies: - "@ethersproject/transactions" "^5.6.2" - web3-core-helpers "1.10.0" - web3-core-promievent "1.10.0" - web3-core-subscriptions "1.10.0" - web3-utils "1.10.0" - web3-core-method@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-core-method/-/web3-core-method-1.10.3.tgz#63f16310ccab4eec8eca0a337d534565c2ba8d33" @@ -19876,13 +18673,6 @@ web3-core-method@1.10.3: web3-core-subscriptions "1.10.3" web3-utils "1.10.3" -web3-core-promievent@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-core-promievent/-/web3-core-promievent-1.10.0.tgz#cbb5b3a76b888df45ed3a8d4d8d4f54ccb66a37b" - integrity sha512-68N7k5LWL5R38xRaKFrTFT2pm2jBNFaM4GioS00YjAKXRQ3KjmhijOMG3TICz6Aa5+6GDWYelDNx21YAeZ4YTg== - dependencies: - eventemitter3 "4.0.4" - web3-core-promievent@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-core-promievent/-/web3-core-promievent-1.10.3.tgz#9765dd42ce6cf2dc0a08eaffee607b855644f290" @@ -19890,17 +18680,6 @@ web3-core-promievent@1.10.3: dependencies: eventemitter3 "4.0.4" -web3-core-requestmanager@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-core-requestmanager/-/web3-core-requestmanager-1.10.0.tgz#4b34f6e05837e67c70ff6f6993652afc0d54c340" - integrity sha512-3z/JKE++Os62APml4dvBM+GAuId4h3L9ckUrj7ebEtS2AR0ixyQPbrBodgL91Sv7j7cQ3Y+hllaluqjguxvSaQ== - dependencies: - util "^0.12.5" - web3-core-helpers "1.10.0" - web3-providers-http "1.10.0" - web3-providers-ipc "1.10.0" - web3-providers-ws "1.10.0" - web3-core-requestmanager@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-core-requestmanager/-/web3-core-requestmanager-1.10.3.tgz#c34ca8e998a18d6ca3fa7f7a11d4391da401c987" @@ -19912,14 +18691,6 @@ web3-core-requestmanager@1.10.3: web3-providers-ipc "1.10.3" web3-providers-ws "1.10.3" -web3-core-subscriptions@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-core-subscriptions/-/web3-core-subscriptions-1.10.0.tgz#b534592ee1611788fc0cb0b95963b9b9b6eacb7c" - integrity sha512-HGm1PbDqsxejI075gxBc5OSkwymilRWZufIy9zEpnWKNmfbuv5FfHgW1/chtJP6aP3Uq2vHkvTDl3smQBb8l+g== - dependencies: - eventemitter3 "4.0.4" - web3-core-helpers "1.10.0" - web3-core-subscriptions@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-core-subscriptions/-/web3-core-subscriptions-1.10.3.tgz#58768cd72a9313252ef05dc52c09536f009a9479" @@ -19928,19 +18699,6 @@ web3-core-subscriptions@1.10.3: eventemitter3 "4.0.4" web3-core-helpers "1.10.3" -web3-core@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-1.10.0.tgz#9aa07c5deb478cf356c5d3b5b35afafa5fa8e633" - integrity sha512-fWySwqy2hn3TL89w5TM8wXF1Z2Q6frQTKHWmP0ppRQorEK8NcHJRfeMiv/mQlSKoTS1F6n/nv2uyZsixFycjYQ== - dependencies: - "@types/bn.js" "^5.1.1" - "@types/node" "^12.12.6" - bignumber.js "^9.0.0" - web3-core-helpers "1.10.0" - web3-core-method "1.10.0" - web3-core-requestmanager "1.10.0" - web3-utils "1.10.0" - web3-core@1.10.3, web3-core@^1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-1.10.3.tgz#4aeb8f4b0cb5775d9fa4edf1127864743f1c3ae3" @@ -19954,14 +18712,6 @@ web3-core@1.10.3, web3-core@^1.10.3: web3-core-requestmanager "1.10.3" web3-utils "1.10.3" -web3-eth-abi@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-eth-abi/-/web3-eth-abi-1.10.0.tgz#53a7a2c95a571e205e27fd9e664df4919483cce1" - integrity sha512-cwS+qRBWpJ43aI9L3JS88QYPfFcSJJ3XapxOQ4j40v6mk7ATpA8CVK1vGTzpihNlOfMVRBkR95oAj7oL6aiDOg== - dependencies: - "@ethersproject/abi" "^5.6.3" - web3-utils "1.10.0" - web3-eth-abi@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-eth-abi/-/web3-eth-abi-1.10.3.tgz#7decfffa8fed26410f32cfefdc32d3e76f717ca2" @@ -19970,22 +18720,6 @@ web3-eth-abi@1.10.3: "@ethersproject/abi" "^5.6.3" web3-utils "1.10.3" -web3-eth-accounts@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-eth-accounts/-/web3-eth-accounts-1.10.0.tgz#2942beca0a4291455f32cf09de10457a19a48117" - integrity sha512-wiq39Uc3mOI8rw24wE2n15hboLE0E9BsQLdlmsL4Zua9diDS6B5abXG0XhFcoNsXIGMWXVZz4TOq3u4EdpXF/Q== - dependencies: - "@ethereumjs/common" "2.5.0" - "@ethereumjs/tx" "3.3.2" - eth-lib "0.2.8" - ethereumjs-util "^7.1.5" - scrypt-js "^3.0.1" - uuid "^9.0.0" - web3-core "1.10.0" - web3-core-helpers "1.10.0" - web3-core-method "1.10.0" - web3-utils "1.10.0" - web3-eth-accounts@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-eth-accounts/-/web3-eth-accounts-1.10.3.tgz#9ecb816b81cd97333988bfcd0afaee5d13bbb198" @@ -20002,21 +18736,7 @@ web3-eth-accounts@1.10.3: web3-core-method "1.10.3" web3-utils "1.10.3" -web3-eth-contract@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-eth-contract/-/web3-eth-contract-1.10.0.tgz#8e68c7654576773ec3c91903f08e49d0242c503a" - integrity sha512-MIC5FOzP/+2evDksQQ/dpcXhSqa/2hFNytdl/x61IeWxhh6vlFeSjq0YVTAyIzdjwnL7nEmZpjfI6y6/Ufhy7w== - dependencies: - "@types/bn.js" "^5.1.1" - web3-core "1.10.0" - web3-core-helpers "1.10.0" - web3-core-method "1.10.0" - web3-core-promievent "1.10.0" - web3-core-subscriptions "1.10.0" - web3-eth-abi "1.10.0" - web3-utils "1.10.0" - -web3-eth-contract@1.10.3, web3-eth-contract@^1.8.1: +web3-eth-contract@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-eth-contract/-/web3-eth-contract-1.10.3.tgz#8880468e2ba7d8a4791cf714f67d5e1ec1591275" integrity sha512-Y2CW61dCCyY4IoUMD4JsEQWrILX4FJWDWC/Txx/pr3K/+fGsBGvS9kWQN5EsVXOp4g7HoFOfVh9Lf7BmVVSRmg== @@ -20030,20 +18750,6 @@ web3-eth-contract@1.10.3, web3-eth-contract@^1.8.1: web3-eth-abi "1.10.3" web3-utils "1.10.3" -web3-eth-ens@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-eth-ens/-/web3-eth-ens-1.10.0.tgz#96a676524e0b580c87913f557a13ed810cf91cd9" - integrity sha512-3hpGgzX3qjgxNAmqdrC2YUQMTfnZbs4GeLEmy8aCWziVwogbuqQZ+Gzdfrym45eOZodk+lmXyLuAdqkNlvkc1g== - dependencies: - content-hash "^2.5.2" - eth-ens-namehash "2.0.8" - web3-core "1.10.0" - web3-core-helpers "1.10.0" - web3-core-promievent "1.10.0" - web3-eth-abi "1.10.0" - web3-eth-contract "1.10.0" - web3-utils "1.10.0" - web3-eth-ens@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-eth-ens/-/web3-eth-ens-1.10.3.tgz#ae5b49bcb9823027e0b28aa6b1de58d726cbaafa" @@ -20058,14 +18764,6 @@ web3-eth-ens@1.10.3: web3-eth-contract "1.10.3" web3-utils "1.10.3" -web3-eth-iban@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-eth-iban/-/web3-eth-iban-1.10.0.tgz#5a46646401965b0f09a4f58e7248c8a8cd22538a" - integrity sha512-0l+SP3IGhInw7Q20LY3IVafYEuufo4Dn75jAHT7c2aDJsIolvf2Lc6ugHkBajlwUneGfbRQs/ccYPQ9JeMUbrg== - dependencies: - bn.js "^5.2.1" - web3-utils "1.10.0" - web3-eth-iban@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-eth-iban/-/web3-eth-iban-1.10.3.tgz#91d458e5400195edc883a0d4383bf1cecd17240d" @@ -20074,18 +18772,6 @@ web3-eth-iban@1.10.3: bn.js "^5.2.1" web3-utils "1.10.3" -web3-eth-personal@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-eth-personal/-/web3-eth-personal-1.10.0.tgz#94d525f7a29050a0c2a12032df150ac5ea633071" - integrity sha512-anseKn98w/d703eWq52uNuZi7GhQeVjTC5/svrBWEKob0WZ5kPdo+EZoFN0sp5a5ubbrk/E0xSl1/M5yORMtpg== - dependencies: - "@types/node" "^12.12.6" - web3-core "1.10.0" - web3-core-helpers "1.10.0" - web3-core-method "1.10.0" - web3-net "1.10.0" - web3-utils "1.10.0" - web3-eth-personal@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-eth-personal/-/web3-eth-personal-1.10.3.tgz#4e72008aa211327ccc3bfa7671c510e623368457" @@ -20098,24 +18784,6 @@ web3-eth-personal@1.10.3: web3-net "1.10.3" web3-utils "1.10.3" -web3-eth@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-eth/-/web3-eth-1.10.0.tgz#38b905e2759697c9624ab080cfcf4e6c60b3a6cf" - integrity sha512-Z5vT6slNMLPKuwRyKGbqeGYC87OAy8bOblaqRTgg94CXcn/mmqU7iPIlG4506YdcdK3x6cfEDG7B6w+jRxypKA== - dependencies: - web3-core "1.10.0" - web3-core-helpers "1.10.0" - web3-core-method "1.10.0" - web3-core-subscriptions "1.10.0" - web3-eth-abi "1.10.0" - web3-eth-accounts "1.10.0" - web3-eth-contract "1.10.0" - web3-eth-ens "1.10.0" - web3-eth-iban "1.10.0" - web3-eth-personal "1.10.0" - web3-net "1.10.0" - web3-utils "1.10.0" - web3-eth@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-eth/-/web3-eth-1.10.3.tgz#b8c6f37f1aac52422583a5a9c29130983a3fb3b1" @@ -20134,15 +18802,6 @@ web3-eth@1.10.3: web3-net "1.10.3" web3-utils "1.10.3" -web3-net@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-net/-/web3-net-1.10.0.tgz#be53e7f5dafd55e7c9013d49c505448b92c9c97b" - integrity sha512-NLH/N3IshYWASpxk4/18Ge6n60GEvWBVeM8inx2dmZJVmRI6SJIlUxbL8jySgiTn3MMZlhbdvrGo8fpUW7a1GA== - dependencies: - web3-core "1.10.0" - web3-core-method "1.10.0" - web3-utils "1.10.0" - web3-net@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-net/-/web3-net-1.10.3.tgz#9486c2fe51452cb958e11915db6f90bd6caa5482" @@ -20152,44 +18811,6 @@ web3-net@1.10.3: web3-core-method "1.10.3" web3-utils "1.10.3" -web3-provider-engine@16.0.3: - version "16.0.3" - resolved "https://registry.yarnpkg.com/web3-provider-engine/-/web3-provider-engine-16.0.3.tgz#8ff93edf3a8da2f70d7f85c5116028c06a0d9f07" - integrity sha512-Q3bKhGqLfMTdLvkd4TtkGYJHcoVQ82D1l8jTIwwuJp/sAp7VHnRYb9YJ14SW/69VMWoOhSpPLZV2tWb9V0WJoA== - dependencies: - "@ethereumjs/tx" "^3.3.0" - async "^2.5.0" - backoff "^2.5.0" - clone "^2.0.0" - cross-fetch "^2.1.0" - eth-block-tracker "^4.4.2" - eth-json-rpc-filters "^4.2.1" - eth-json-rpc-infura "^5.1.0" - eth-json-rpc-middleware "^6.0.0" - eth-rpc-errors "^3.0.0" - eth-sig-util "^1.4.2" - ethereumjs-block "^1.2.2" - ethereumjs-util "^5.1.5" - ethereumjs-vm "^2.3.4" - json-stable-stringify "^1.0.1" - promise-to-callback "^1.0.0" - readable-stream "^2.2.9" - request "^2.85.0" - semaphore "^1.0.3" - ws "^5.1.1" - xhr "^2.2.0" - xtend "^4.0.1" - -web3-providers-http@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-providers-http/-/web3-providers-http-1.10.0.tgz#864fa48675e7918c9a4374e5f664b32c09d0151b" - integrity sha512-eNr965YB8a9mLiNrkjAWNAPXgmQWfpBfkkn7tpEFlghfww0u3I0tktMZiaToJVcL2+Xq+81cxbkpeWJ5XQDwOA== - dependencies: - abortcontroller-polyfill "^1.7.3" - cross-fetch "^3.1.4" - es6-promise "^4.2.8" - web3-core-helpers "1.10.0" - web3-providers-http@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-providers-http/-/web3-providers-http-1.10.3.tgz#d8166ee89db82d37281ea9e15c5882a2d7928755" @@ -20200,14 +18821,6 @@ web3-providers-http@1.10.3: es6-promise "^4.2.8" web3-core-helpers "1.10.3" -web3-providers-ipc@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-providers-ipc/-/web3-providers-ipc-1.10.0.tgz#9747c7a6aee96a51488e32fa7c636c3460b39889" - integrity sha512-OfXG1aWN8L1OUqppshzq8YISkWrYHaATW9H8eh0p89TlWMc1KZOL9vttBuaBEi96D/n0eYDn2trzt22bqHWfXA== - dependencies: - oboe "2.1.5" - web3-core-helpers "1.10.0" - web3-providers-ipc@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-providers-ipc/-/web3-providers-ipc-1.10.3.tgz#a7e015957fc037d8a87bd4b6ae3561c1b1ad1f46" @@ -20216,15 +18829,6 @@ web3-providers-ipc@1.10.3: oboe "2.1.5" web3-core-helpers "1.10.3" -web3-providers-ws@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-providers-ws/-/web3-providers-ws-1.10.0.tgz#cb0b87b94c4df965cdf486af3a8cd26daf3975e5" - integrity sha512-sK0fNcglW36yD5xjnjtSGBnEtf59cbw4vZzJ+CmOWIKGIR96mP5l684g0WD0Eo+f4NQc2anWWXG74lRc9OVMCQ== - dependencies: - eventemitter3 "4.0.4" - web3-core-helpers "1.10.0" - websocket "^1.0.32" - web3-providers-ws@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-providers-ws/-/web3-providers-ws-1.10.3.tgz#03c84958f9da251349cd26fd7a4ae567e3af6caa" @@ -20234,16 +18838,6 @@ web3-providers-ws@1.10.3: web3-core-helpers "1.10.3" websocket "^1.0.32" -web3-shh@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-shh/-/web3-shh-1.10.0.tgz#c2979b87e0f67a7fef2ce9ee853bd7bfbe9b79a8" - integrity sha512-uNUUuNsO2AjX41GJARV9zJibs11eq6HtOe6Wr0FtRUcj8SN6nHeYIzwstAvJ4fXA53gRqFMTxdntHEt9aXVjpg== - dependencies: - web3-core "1.10.0" - web3-core-method "1.10.0" - web3-core-subscriptions "1.10.0" - web3-net "1.10.0" - web3-shh@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-shh/-/web3-shh-1.10.3.tgz#ee44f760598a65a290d611c443838aac854ee858" @@ -20254,20 +18848,7 @@ web3-shh@1.10.3: web3-core-subscriptions "1.10.3" web3-net "1.10.3" -web3-utils@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.10.0.tgz#ca4c1b431a765c14ac7f773e92e0fd9377ccf578" - integrity sha512-kSaCM0uMcZTNUSmn5vMEhlo02RObGNRRCkdX0V9UTAU0+lrvn0HSaudyCo6CQzuXUsnuY2ERJGCGPfeWmv19Rg== - dependencies: - bn.js "^5.2.1" - ethereum-bloom-filters "^1.0.6" - ethereumjs-util "^7.1.0" - ethjs-unit "0.1.6" - number-to-bn "1.7.0" - randombytes "^2.1.0" - utf8 "3.0.0" - -web3-utils@1.10.3, web3-utils@^1.10.3, web3-utils@^1.8.1: +web3-utils@1.10.3, web3-utils@^1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.10.3.tgz#f1db99c82549c7d9f8348f04ffe4e0188b449714" integrity sha512-OqcUrEE16fDBbGoQtZXWdavsPzbGIDc5v3VrRTZ0XrIpefC/viZ1ZU9bGEemazyS0catk/3rkOOxpzTfY+XsyQ== @@ -20281,19 +18862,6 @@ web3-utils@1.10.3, web3-utils@^1.10.3, web3-utils@^1.8.1: randombytes "^2.1.0" utf8 "3.0.0" -web3@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3/-/web3-1.10.0.tgz#2fde0009f59aa756c93e07ea2a7f3ab971091274" - integrity sha512-YfKY9wSkGcM8seO+daR89oVTcbu18NsVfvOngzqMYGUU0pPSQmE57qQDvQzUeoIOHAnXEBNzrhjQJmm8ER0rng== - dependencies: - web3-bzz "1.10.0" - web3-core "1.10.0" - web3-eth "1.10.0" - web3-eth-personal "1.10.0" - web3-net "1.10.0" - web3-shh "1.10.0" - web3-utils "1.10.0" - web3@^1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3/-/web3-1.10.3.tgz#5e80ac532dc432b09fde668d570b0ad4e6710897" @@ -20307,11 +18875,6 @@ web3@^1.10.3: web3-shh "1.10.3" web3-utils "1.10.3" -webextension-polyfill@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz#ccb28101c910ba8cf955f7e6a263e662d744dbb8" - integrity sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g== - webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -20461,11 +19024,6 @@ whatwg-encoding@^2.0.0: dependencies: iconv-lite "0.6.3" -whatwg-fetch@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" - integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== - whatwg-mimetype@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" @@ -20843,23 +19401,11 @@ ws@^3.0.0: safe-buffer "~5.1.0" ultron "~1.1.0" -ws@^5.1.1: - version "5.2.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.3.tgz#05541053414921bc29c63bee14b8b0dd50b07b3d" - integrity sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA== - dependencies: - async-limiter "~1.0.0" - ws@^8.11.0, ws@^8.2.3, ws@^8.5.0: version "8.16.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== -ws@~8.11.0: - version "8.11.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" - integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== - xhr-request-promise@^0.1.2: version "0.1.3" resolved "https://registry.yarnpkg.com/xhr-request-promise/-/xhr-request-promise-0.1.3.tgz#2d5f4b16d8c6c893be97f1a62b0ed4cf3ca5f96c" @@ -20880,7 +19426,7 @@ xhr-request@^1.0.1, xhr-request@^1.1.0: url-set-query "^1.0.0" xhr "^2.0.4" -xhr@^2.0.4, xhr@^2.2.0, xhr@^2.3.3: +xhr@^2.0.4, xhr@^2.3.3: version "2.6.0" resolved "https://registry.yarnpkg.com/xhr/-/xhr-2.6.0.tgz#b69d4395e792b4173d6b7df077f0fc5e4e2b249d" integrity sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA== @@ -20900,12 +19446,7 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xmlhttprequest-ssl@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" - integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== - -xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.0, xtend@~4.0.1: +xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== @@ -21017,3 +19558,8 @@ yocto-queue@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== + +zodiac-roles-deployments@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/zodiac-roles-deployments/-/zodiac-roles-deployments-2.2.2.tgz#feb7e7544398e1572d2f7fa6ff2be033f2c736ce" + integrity sha512-6nG6/AuJh9SIrXR1NieRzSfn1+J6k9p2mb3qns3cRYhx3+i4wWIRh1JcE+jueHSYo+H7yKG/jfwRXMVN1L938A==