├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md ├── PULL_REQUEST_TEMPLATE.md ├── actions │ ├── release │ │ └── action.yml │ └── setup-env │ │ └── action.yml └── workflows │ ├── cla.yml │ ├── conventional-commit-check.yml │ ├── deployment.yml │ ├── lint.yml │ ├── safe-apps-check.yml │ └── safe-apps-e2e.yml ├── .gitignore ├── .husky ├── pre-commit └── prepare-commit-msg ├── .nvmrc ├── .prettierrc ├── LICENSE.md ├── README.md ├── apps ├── drain-safe │ ├── CHANGELOG.md │ ├── README.md │ ├── config-overrides.js │ ├── package.json │ ├── project.json │ ├── public │ │ ├── eth.svg │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo.svg │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ ├── question.svg │ │ └── robots.txt │ ├── src │ │ ├── GlobalStyle.ts │ │ ├── __tests__ │ │ │ ├── App.test.js │ │ │ └── sdk-helpers.test.js │ │ ├── abis │ │ │ └── erc20.ts │ │ ├── components │ │ │ ├── AddressInput.tsx │ │ │ ├── App.tsx │ │ │ ├── AppLoader.tsx │ │ │ ├── Balances.tsx │ │ │ ├── CancelButton.tsx │ │ │ ├── CurrencyCell.tsx │ │ │ ├── Flex.tsx │ │ │ ├── FormContainer.tsx │ │ │ ├── Icon.tsx │ │ │ ├── Logo.tsx │ │ │ ├── SubmitButton.tsx │ │ │ └── TimedComponent.tsx │ │ ├── hooks │ │ │ ├── use-balances.ts │ │ │ ├── useTimeout.ts │ │ │ └── useWeb3.ts │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ ├── setupTests.ts │ │ └── utils │ │ │ ├── formatters.ts │ │ │ ├── sdk-helpers.ts │ │ │ └── test-helpers.tsx │ └── tsconfig.json ├── mmi │ ├── .env.sample │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── config-overrides.js │ ├── package.json │ ├── project.json │ ├── public │ │ ├── _headers │ │ ├── index.html │ │ ├── manifest.json │ │ ├── mmi.svg │ │ └── robots.txt │ ├── src │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── components │ │ │ ├── AppBar.tsx │ │ │ └── Help.tsx │ │ ├── index.tsx │ │ ├── lib │ │ │ ├── http.ts │ │ │ ├── mmi.ts │ │ │ └── utils.ts │ │ ├── react-app-env.d.ts │ │ ├── setupProxy.js │ │ └── setupTests.ts │ └── tsconfig.json ├── ramp-network │ ├── .env.example │ ├── CHANGELOG.md │ ├── README.md │ ├── config-overrides.js │ ├── package.json │ ├── project.json │ ├── public │ │ ├── index.html │ │ ├── manifest.json │ │ ├── ramp.svg │ │ └── robots.txt │ ├── src │ │ ├── App.tsx │ │ ├── constants.ts │ │ ├── index.tsx │ │ ├── ramp.ts │ │ ├── react-app-env.d.ts │ │ ├── setupProxy.js │ │ └── utils.ts │ └── tsconfig.json ├── siwe-delegate-manager │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── project.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo.svg │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── GlobalStyles.tsx │ │ ├── assets │ │ │ └── delegateRegistryContractABI.ts │ │ ├── components │ │ │ ├── address-label │ │ │ │ └── AddressLabel.tsx │ │ │ ├── data-table │ │ │ │ └── DataTable.tsx │ │ │ ├── delegate-event-label │ │ │ │ └── DelegateEventLabel.tsx │ │ │ ├── delegate-form │ │ │ │ └── DelegateForm.tsx │ │ │ ├── delegation-history-table │ │ │ │ └── DelegationHistoryTable.tsx │ │ │ ├── delegation-table │ │ │ │ └── DelegationTable.tsx │ │ │ ├── header │ │ │ │ └── Header.tsx │ │ │ ├── loader │ │ │ │ └── Loader.tsx │ │ │ ├── modals │ │ │ │ └── RemoveDelegatorModal.tsx │ │ │ ├── space-label │ │ │ │ └── SpaceLabel.tsx │ │ │ └── transaction-label │ │ │ │ └── TransactionLabel.tsx │ │ ├── hooks │ │ │ ├── useMemoizedAddressLabel.tsx │ │ │ ├── useMemoizedTransactionLabel.tsx │ │ │ └── useModal.tsx │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ ├── setupTests.ts │ │ ├── store │ │ │ ├── delegateRegistryContext.tsx │ │ │ └── safeWalletContext.tsx │ │ └── utils │ │ │ └── siwe.ts │ └── tsconfig.json ├── tx-builder │ ├── .env.example │ ├── CHANGELOG.md │ ├── config-overrides.js │ ├── hardhat.config.js │ ├── package.json │ ├── project.json │ ├── public │ │ ├── index.html │ │ ├── manifest.json │ │ ├── robots.txt │ │ └── tx-builder.png │ ├── src │ │ ├── App.tsx │ │ ├── assets │ │ │ ├── arrowtotheblock.svg │ │ │ ├── empty-library-dark.svg │ │ │ ├── empty-library-light.svg │ │ │ ├── fonts │ │ │ │ ├── DMSans700.woff2 │ │ │ │ └── DMSansRegular.woff2 │ │ │ ├── new-batch-dark.svg │ │ │ ├── new-batch-light.svg │ │ │ └── success-batch.svg │ │ ├── components │ │ │ ├── Accordion │ │ │ │ └── index.tsx │ │ │ ├── Button.tsx │ │ │ ├── Card │ │ │ │ └── index.tsx │ │ │ ├── ChecksumWarning.tsx │ │ │ ├── CreateNewBatchCard.tsx │ │ │ ├── Divider.tsx │ │ │ ├── Dot │ │ │ │ └── index.tsx │ │ │ ├── ETHHashInfo.tsx │ │ │ ├── EditableLabel.tsx │ │ │ ├── EllipsisMenu │ │ │ │ └── index.tsx │ │ │ ├── ErrorAlert.tsx │ │ │ ├── FixedIcon │ │ │ │ ├── images │ │ │ │ │ ├── arrowReceived.tsx │ │ │ │ │ ├── arrowReceivedWhite.tsx │ │ │ │ │ ├── arrowSent.tsx │ │ │ │ │ ├── arrowSentWhite.tsx │ │ │ │ │ ├── arrowSort.tsx │ │ │ │ │ ├── bullit.tsx │ │ │ │ │ ├── chevronDown.tsx │ │ │ │ │ ├── chevronLeft.tsx │ │ │ │ │ ├── chevronRight.tsx │ │ │ │ │ ├── chevronUp.tsx │ │ │ │ │ ├── connectedRinkeby.tsx │ │ │ │ │ ├── connectedWallet.tsx │ │ │ │ │ ├── creatingInProgress.tsx │ │ │ │ │ ├── dropdownArrowSmall.tsx │ │ │ │ │ ├── networkError.tsx │ │ │ │ │ ├── notConnected.tsx │ │ │ │ │ ├── notOwner.tsx │ │ │ │ │ ├── options.tsx │ │ │ │ │ ├── plus.tsx │ │ │ │ │ ├── settingsChange.tsx │ │ │ │ │ └── threeDots.tsx │ │ │ │ └── index.tsx │ │ │ ├── GenericModal.tsx │ │ │ ├── Header.test.tsx │ │ │ ├── Header.tsx │ │ │ ├── Icon │ │ │ │ ├── images │ │ │ │ │ ├── alert.tsx │ │ │ │ │ ├── bookmark.tsx │ │ │ │ │ ├── bookmarkFilled.tsx │ │ │ │ │ ├── check.tsx │ │ │ │ │ ├── code.tsx │ │ │ │ │ ├── copy.tsx │ │ │ │ │ ├── cross.tsx │ │ │ │ │ ├── delete.tsx │ │ │ │ │ ├── edit.tsx │ │ │ │ │ ├── externalLink.tsx │ │ │ │ │ ├── import.tsx │ │ │ │ │ ├── info.tsx │ │ │ │ │ └── termsOfUse.tsx │ │ │ │ └── index.tsx │ │ │ ├── IconText │ │ │ │ └── index.tsx │ │ │ ├── Link │ │ │ │ └── index.tsx │ │ │ ├── Loader │ │ │ │ └── index.tsx │ │ │ ├── QuickTip.tsx │ │ │ ├── ShowMoreText.tsx │ │ │ ├── Switch.tsx │ │ │ ├── Text.tsx │ │ │ ├── Title.tsx │ │ │ ├── Tooltip.tsx │ │ │ ├── TransactionBatchListItem.tsx │ │ │ ├── TransactionDetails.tsx │ │ │ ├── TransactionsBatchList.tsx │ │ │ ├── VirtualizedList.tsx │ │ │ ├── Wrapper │ │ │ │ └── index.tsx │ │ │ ├── buttons │ │ │ │ ├── ButtonLink │ │ │ │ │ └── index.tsx │ │ │ │ ├── CopyToClipboardBtn │ │ │ │ │ ├── copyTextToClipboard.ts │ │ │ │ │ └── index.tsx │ │ │ │ ├── ExplorerButton │ │ │ │ │ └── index.tsx │ │ │ │ └── Identicon │ │ │ │ │ └── index.tsx │ │ │ ├── forms │ │ │ │ ├── AddNewTransactionForm.tsx │ │ │ │ ├── SolidityForm.test.tsx │ │ │ │ ├── SolidityForm.tsx │ │ │ │ ├── fields │ │ │ │ │ ├── AddressContractField.tsx │ │ │ │ │ ├── AddressInput.tsx │ │ │ │ │ ├── Field.tsx │ │ │ │ │ ├── JsonField.tsx │ │ │ │ │ ├── SelectContractField.tsx │ │ │ │ │ ├── TextContractField.tsx │ │ │ │ │ ├── TextFieldInput.tsx │ │ │ │ │ ├── TextareaContractField.tsx │ │ │ │ │ ├── fields.test.ts │ │ │ │ │ ├── fields.ts │ │ │ │ │ └── styles.ts │ │ │ │ └── validations │ │ │ │ │ ├── basicSolidityValidation.ts │ │ │ │ │ ├── validateAddressField.ts │ │ │ │ │ ├── validateAmountField.ts │ │ │ │ │ ├── validateBooleanField.ts │ │ │ │ │ ├── validateField.ts │ │ │ │ │ ├── validateHexEncodedDataField.ts │ │ │ │ │ └── validations.test.ts │ │ │ └── modals │ │ │ │ ├── DeleteBatchFromLibrary.tsx │ │ │ │ ├── DeleteBatchModal.tsx │ │ │ │ ├── DeleteTransactionModal.tsx │ │ │ │ ├── EditTransactionModal.tsx │ │ │ │ ├── ImplementationABIDialog.tsx │ │ │ │ ├── SaveBatchModal.tsx │ │ │ │ ├── SuccessBatchCreationModal.tsx │ │ │ │ └── WrongChainBatchModal.tsx │ │ ├── contracts │ │ │ ├── BasicTypesTestContract.sol │ │ │ └── MatrixTypesTestContract.sol │ │ ├── global.ts │ │ ├── hardhat │ │ │ ├── deploy │ │ │ │ ├── deploy_basic_test_contract.js │ │ │ │ └── deploy_matrix_test_contract.js │ │ │ ├── networks.js │ │ │ └── tasks │ │ │ │ ├── deploy_contracts.js │ │ │ │ └── read_method.js │ │ ├── hooks │ │ │ ├── useAbi.ts │ │ │ ├── useAsync.ts │ │ │ ├── useDebounce.ts │ │ │ ├── useDropZone │ │ │ │ └── index.tsx │ │ │ ├── useElementHeight │ │ │ │ └── useElementHeight.tsx │ │ │ ├── useModal │ │ │ │ └── useModal.tsx │ │ │ ├── useSimulation.ts │ │ │ └── useThrottle.ts │ │ ├── index.tsx │ │ ├── lib │ │ │ ├── analytics.ts │ │ │ ├── batches │ │ │ │ └── index.ts │ │ │ ├── checksum.test.js │ │ │ ├── checksum.ts │ │ │ ├── getAbi.ts │ │ │ ├── interfaceRepository.ts │ │ │ ├── local-storage │ │ │ │ ├── Storage.ts │ │ │ │ └── local.ts │ │ │ ├── simulation │ │ │ │ ├── multisend.ts │ │ │ │ ├── simulation.ts │ │ │ │ └── types.ts │ │ │ └── storage.ts │ │ ├── pages │ │ │ ├── CreateTransactions.tsx │ │ │ ├── Dashboard.tsx │ │ │ ├── EditTransactionLibrary.tsx │ │ │ ├── ReviewAndConfirm.tsx │ │ │ ├── SaveTransactionLibrary.tsx │ │ │ └── TransactionLibrary.tsx │ │ ├── react-app-env.d.ts │ │ ├── routes │ │ │ └── routes.ts │ │ ├── serviceWorker.ts │ │ ├── setupTests.ts │ │ ├── store │ │ │ ├── index.tsx │ │ │ ├── networkContext.tsx │ │ │ ├── transactionLibraryContext.tsx │ │ │ └── transactionsContext.tsx │ │ ├── test-utils.tsx │ │ ├── theme │ │ │ ├── SafeThemeProvider.tsx │ │ │ ├── darkPalette.ts │ │ │ ├── lightPalette.ts │ │ │ ├── safeTheme.ts │ │ │ └── typography.ts │ │ ├── typings │ │ │ ├── custom.d.ts │ │ │ ├── errors.ts │ │ │ ├── fonts.d.ts │ │ │ └── models.ts │ │ ├── utils.test.ts │ │ ├── utils.ts │ │ └── utils │ │ │ ├── address.ts │ │ │ └── strings.ts │ └── tsconfig.json └── wallet-connect │ ├── .env.example │ ├── CHANGELOG.md │ ├── README.md │ ├── config-overrides.js │ ├── package.json │ ├── project.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ ├── robots.txt │ └── wallet-connect.svg │ ├── src │ ├── App.test.tsx │ ├── App.tsx │ ├── assets │ │ ├── cam-permissions.png │ │ └── wallet-connect-logo.svg │ ├── components │ │ ├── AppBar.tsx │ │ ├── Connected.tsx │ │ ├── Connecting.tsx │ │ ├── Disconnected.tsx │ │ ├── Help.tsx │ │ ├── ScanCode.tsx │ │ ├── WalletConnectField.tsx │ │ └── styles.ts │ ├── constants.ts │ ├── global.ts │ ├── hooks │ │ ├── useApps.ts │ │ ├── useQRCode.tsx │ │ ├── useWalletConnect.tsx │ │ ├── useWalletConnectV1.tsx │ │ ├── useWalletConnectV2.tsx │ │ └── useWebcam.tsx │ ├── index.tsx │ ├── mocks │ │ └── mocks.ts │ ├── react-app-env.d.ts │ ├── setupTests.ts │ ├── typings │ │ ├── custom.d.ts │ │ └── fonts.d.ts │ └── utils │ │ ├── analytics.ts │ │ ├── images.ts │ │ └── test-helpers.tsx │ └── tsconfig.json ├── assets └── logo.svg ├── cypress.config.js ├── cypress ├── e2e │ ├── drain-account │ │ └── drain.spec.cy.js │ ├── safe-apps-check.spec.cy.js │ └── tx-builder │ │ └── tx-builder.spec.cy.js ├── fixtures │ ├── balances.json │ ├── test-empty-batch.json │ ├── test-invalid-batch.json │ ├── test-mainnet-batch.json │ ├── test-modified-batch.json │ └── test-working-batch.json ├── lib │ └── slack.js └── support │ ├── commands.js │ ├── e2e.js │ └── iframe.js ├── docs └── release-procedure.md ├── nx.json ├── package.json ├── scripts ├── deploy_pr.sh ├── deploy_to_s3_bucket.sh └── prepare_production_deployment.sh ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | !.eslintrc.js 2 | build 3 | config 4 | contracts 5 | flow-typed 6 | flow-typed/npm 7 | migrations 8 | node_modules 9 | public 10 | scripts 11 | src/assets 12 | src/config 13 | test 14 | *.spec* 15 | *.test* -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: [ 4 | 'react-app', // extends Create React App eslint config 5 | 'plugin:@typescript-eslint/recommended', // Plugin to use typescript with eslint 6 | 'prettier', // Add prettier rules to eslint 7 | ], 8 | parserOptions: { 9 | ecmaVersion: 2018, 10 | sourceType: 'module', 11 | }, 12 | rules: { 13 | '@typescript-eslint/camelcase': 'off', 14 | '@typescript-eslint/no-var-requires': 'off', 15 | '@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }], 16 | }, 17 | globals: { 18 | cy: 'readonly', 19 | Cypress: 'readonly', 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create an issue to fix a bug 4 | --- 5 | 6 | 9 | 10 | ## Description 11 | 12 | ## Environment 13 | - Browser: 14 | - Wallet: MetaMask 15 | - Safe: 16 | - Environment: 17 | - production (rinkeby) 18 | 19 | ## Steps to reproduce 20 | 1. Go to 21 | 22 | ## Expected result 23 | 24 | ## Obtained result 25 | 26 | ## Screenshots 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Create a feature request for Safe Apps 4 | 5 | --- 6 | 7 | ## Overview 8 | 9 | ## Requirements 10 | 11 | ## Designs 12 | 13 | ## Links 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 14 | 15 | ## What it solves 16 | Resolves # 17 | 18 | ## How this PR fixes it 19 | 20 | ## How to test it 21 | 22 | ## Screenshots 23 | -------------------------------------------------------------------------------- /.github/actions/setup-env/action.yml: -------------------------------------------------------------------------------- 1 | name: Prepare environment 2 | 3 | description: Prepare environment in the CI runner and install dependencies 4 | 5 | inputs: 6 | node-version: 7 | description: Node.js version 8 | required: false 9 | default: 18 10 | aws-secret-access-key: 11 | description: AWS secret access key 12 | required: true 13 | aws-access-key-id: 14 | description: AWS access key id 15 | required: true 16 | aws-region: 17 | description: AWS region 18 | required: true 19 | 20 | runs: 21 | using: 'composite' 22 | steps: 23 | - name: Node.js setup 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ inputs.node-version }} 27 | cache: 'yarn' 28 | cache-dependency-path: '**/yarn.lock' 29 | 30 | - name: Env dependencies setup 31 | shell: bash 32 | run: | 33 | sudo apt-get update 34 | sudo apt-get -y install python3-pip python3-dev 35 | python -m venv venv 36 | source venv/bin/activate 37 | pip install awscli --upgrade 38 | - name: Project dependencies setup, node version ${{ inputs.node-version }} 39 | shell: bash 40 | run: yarn install --frozen-lockfile 41 | 42 | - name: Configure AWS credentials 43 | uses: aws-actions/configure-aws-credentials@v1 44 | with: 45 | aws-access-key-id: ${{ inputs.aws-access-key-id }} 46 | aws-secret-access-key: ${{ inputs.aws-secret-access-key }} 47 | aws-region: ${{ inputs.aws-region }} 48 | -------------------------------------------------------------------------------- /.github/workflows/conventional-commit-check.yml: -------------------------------------------------------------------------------- 1 | name: 'Conventional commit check' 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | name: Validate PR title 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: amannn/action-semantic-pull-request@v4 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: 'ESLint check' 2 | on: [pull_request] 3 | 4 | jobs: 5 | eslint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | 10 | - uses: actions/setup-node@v3 11 | with: 12 | node-version: 18 13 | cache: yarn 14 | 15 | - name: Install Dependencies 16 | run: yarn install --frozen-lockfile 17 | 18 | - name: Run eslint 19 | run: yarn lint:check -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | yalc.lock 8 | .yalc 9 | 10 | # testing 11 | **/coverage 12 | .eslintcache 13 | 14 | # production 15 | build 16 | 17 | # misc 18 | .DS_Store 19 | .env 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # vscode folders 30 | .vscode 31 | 32 | # contract artifacts 33 | **/cache 34 | **/artifacts 35 | deployments/ 36 | 37 | # cypress 38 | cypress/reports 39 | cypress/videos 40 | cypress/screenshots 41 | cypress/downloads 42 | 43 | # intellij ide files 44 | .idea 45 | 46 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged --allow-empty 5 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | exec < /dev/tty && npx cz --hook || true 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "arrowParens": "avoid", 4 | "trailingComma": "all", 5 | "singleQuote": true, 6 | "semi": false, 7 | "endOfLine": "auto" 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2022 Safe Ecosystem Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/drain-safe/README.md: -------------------------------------------------------------------------------- 1 | # Drain Safe 2 | 3 | This project is based on the Safe App CRA template. 4 | 5 | Screenshot 2021-04-15 at 15 55 18 6 | 7 | ## Run locally 8 | 9 | Install: 10 | 11 | ``` 12 | yarn 13 | ``` 14 | 15 | Run: 16 | 17 | ``` 18 | yarn start 19 | ``` 20 | -------------------------------------------------------------------------------- /apps/drain-safe/config-overrides.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | 3 | module.exports = { 4 | webpack: function (config, env) { 5 | const fallback = config.resolve.fallback || {} 6 | 7 | // https://github.com/ChainSafe/web3.js#web3-and-create-react-app 8 | Object.assign(fallback, { 9 | crypto: require.resolve('crypto-browserify'), 10 | stream: require.resolve('stream-browserify'), 11 | assert: require.resolve('assert'), 12 | http: require.resolve('stream-http'), 13 | https: require.resolve('https-browserify'), 14 | os: require.resolve('os-browserify'), 15 | url: require.resolve('url'), 16 | // https://stackoverflow.com/questions/68707553/uncaught-referenceerror-buffer-is-not-defined 17 | buffer: require.resolve('buffer'), 18 | }) 19 | 20 | config.resolve.fallback = fallback 21 | 22 | config.plugins = (config.plugins || []).concat([ 23 | new webpack.ProvidePlugin({ 24 | process: 'process/browser', 25 | Buffer: ['buffer', 'Buffer'], 26 | }), 27 | ]) 28 | 29 | // https://github.com/facebook/create-react-app/issues/11924 30 | config.ignoreWarnings = [/to parse source map/i] 31 | 32 | return config 33 | }, 34 | jest: function (config) { 35 | return config 36 | }, 37 | devServer: function (configFunction) { 38 | return function (proxy, allowedHost) { 39 | const config = configFunction(proxy, allowedHost) 40 | 41 | config.headers = { 42 | 'Access-Control-Allow-Origin': '*', 43 | 'Access-Control-Allow-Methods': 'GET', 44 | 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization', 45 | } 46 | 47 | return config 48 | } 49 | }, 50 | paths: function (paths) { 51 | return paths 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /apps/drain-safe/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drain-safe", 3 | "version": "1.5.1", 4 | "private": true, 5 | "dependencies": { 6 | "@gnosis.pm/safe-react-components": "^1.2.0", 7 | "@material-ui/core": "^4.12.4", 8 | "@mui/x-data-grid": "4.0.2", 9 | "@safe-global/safe-apps-provider": "^0.18.0", 10 | "bignumber.js": "^9.1.1", 11 | "web3-eth-abi": "~1.8.1" 12 | }, 13 | "scripts": { 14 | "start": "react-app-rewired start", 15 | "build": "react-app-rewired build", 16 | "test": "react-app-rewired test", 17 | "eject": "react-scripts eject", 18 | "deploy:s3": "bash ../../scripts/deploy_to_s3_bucket.sh", 19 | "deploy:pr": "bash ../../scripts/deploy_pr.sh", 20 | "deploy:prod-hook": "bash ../../scripts/prepare_production_deployment.sh" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "plugin:jsx-a11y/recommended" 26 | ], 27 | "plugins": [ 28 | "jsx-a11y" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | }, 43 | "homepage": "./" 44 | } 45 | -------------------------------------------------------------------------------- /apps/drain-safe/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "apps/drain-safe/", 3 | "sourceRoot": "apps/drain-safe/src/", 4 | "projectType": "application", 5 | "tags": ["scope:applications"], 6 | "targets": { 7 | "version": { 8 | "executor": "@jscutlery/semver:version", 9 | "options": { 10 | "commitMessageFormat": "chore(${projectName}): release version ${version}" 11 | } 12 | }, 13 | "github": { 14 | "executor": "@jscutlery/semver:github", 15 | "options": { 16 | "tag": "${tag}", 17 | "generateNotes": true 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/drain-safe/public/eth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 12 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/drain-safe/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safe-global/safe-react-apps/2bbc198c090c81eb784f4a8a646382520e338056/apps/drain-safe/public/favicon.ico -------------------------------------------------------------------------------- /apps/drain-safe/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safe-global/safe-react-apps/2bbc198c090c81eb784f4a8a646382520e338056/apps/drain-safe/public/logo192.png -------------------------------------------------------------------------------- /apps/drain-safe/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safe-global/safe-react-apps/2bbc198c090c81eb784f4a8a646382520e338056/apps/drain-safe/public/logo512.png -------------------------------------------------------------------------------- /apps/drain-safe/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Drain Account", 3 | "description": "Transfer all your assets in batch", 4 | "iconPath": "logo.svg", 5 | "icons": [ 6 | { 7 | "src": "logo.svg", 8 | "sizes": "any", 9 | "type": "image/svg+xml" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /apps/drain-safe/public/question.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/drain-safe/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /apps/drain-safe/src/GlobalStyle.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components' 2 | 3 | const GlobalStyle = createGlobalStyle` 4 | html { 5 | height: 100% 6 | font-family: 'DM Sans', sans-serif; 7 | } 8 | 9 | body { 10 | height: 100%; 11 | margin: 0px; 12 | padding: 0px; 13 | background-color: #f6f6f6; 14 | font-family: 'DM Sans', sans-serif; 15 | } 16 | 17 | #root { 18 | height: 100%; 19 | padding-right: 0.5rem; 20 | } 21 | 22 | .MuiFormControl-root, 23 | .MuiInputBase-root { 24 | width: 100% !important; 25 | } 26 | 27 | ` 28 | 29 | export default GlobalStyle 30 | -------------------------------------------------------------------------------- /apps/drain-safe/src/abis/erc20.ts: -------------------------------------------------------------------------------- 1 | import { AbiItem } from 'web3-utils' 2 | 3 | const erc20: { [key: string]: AbiItem } = { 4 | transfer: { 5 | constant: false, 6 | inputs: [ 7 | { 8 | name: '_to', 9 | type: 'address', 10 | }, 11 | { 12 | name: '_value', 13 | type: 'uint256', 14 | }, 15 | ], 16 | name: 'transfer', 17 | outputs: [ 18 | { 19 | name: '', 20 | type: 'bool', 21 | }, 22 | ], 23 | payable: false, 24 | stateMutability: 'nonpayable', 25 | type: 'function', 26 | }, 27 | } 28 | 29 | export default erc20 30 | -------------------------------------------------------------------------------- /apps/drain-safe/src/components/AddressInput.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { AddressInput } from '@gnosis.pm/safe-react-components' 3 | 4 | export default styled(AddressInput)` 5 | && { 6 | width: 520px; 7 | margin-bottom: 10px; 8 | 9 | .MuiFormLabel-root { 10 | color: #0000008a; 11 | } 12 | 13 | .MuiFormLabel-root.Mui-focused { 14 | color: #008c73; 15 | } 16 | } 17 | ` 18 | -------------------------------------------------------------------------------- /apps/drain-safe/src/components/AppLoader.tsx: -------------------------------------------------------------------------------- 1 | import { Title, Loader } from '@gnosis.pm/safe-react-components' 2 | import { Grid } from '@material-ui/core' 3 | 4 | const AppLoader = (): React.ReactElement => { 5 | return ( 6 | 13 | Waiting for Safe... 14 | 15 | 16 | ) 17 | } 18 | 19 | export default AppLoader 20 | -------------------------------------------------------------------------------- /apps/drain-safe/src/components/CancelButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Loader } from '@gnosis.pm/safe-react-components' 2 | import Flex from './Flex' 3 | 4 | function CancelButton({ children }: { children: string }): JSX.Element { 5 | return ( 6 | <> 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | ) 17 | } 18 | 19 | export default CancelButton 20 | -------------------------------------------------------------------------------- /apps/drain-safe/src/components/Flex.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components' 2 | 3 | const Flex = styled.div<{ centered?: boolean }>` 4 | display: flex; 5 | align-items: center; 6 | 7 | ${props => 8 | props.centered && 9 | css` 10 | justify-content: center; 11 | `} 12 | ` 13 | 14 | export default Flex 15 | -------------------------------------------------------------------------------- /apps/drain-safe/src/components/FormContainer.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const FormContainer = styled.form` 4 | margin-bottom: 2rem; 5 | width: 100%; 6 | max-width: 800px; 7 | padding: 30px; 8 | 9 | display: grid; 10 | grid-template-columns: 1fr; 11 | grid-column-gap: 1rem; 12 | grid-row-gap: 1rem; 13 | ` 14 | 15 | export default FormContainer 16 | -------------------------------------------------------------------------------- /apps/drain-safe/src/components/Icon.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import styled from 'styled-components' 3 | 4 | interface Props { 5 | logoUri: string | null 6 | symbol: string 7 | } 8 | 9 | const IconImg = styled.img` 10 | margin-right: 10px; 11 | height: 1.5em; 12 | width: auto; 13 | ` 14 | 15 | const defaultIcon = './question.svg' 16 | 17 | function Icon(props: Props): JSX.Element | null { 18 | const [fallbackIcon, setFallbackIcon] = useState('') 19 | const { logoUri, symbol } = props 20 | 21 | const onError = () => { 22 | if (!fallbackIcon) { 23 | setFallbackIcon(defaultIcon) 24 | } 25 | } 26 | 27 | return 28 | } 29 | 30 | export default Icon 31 | -------------------------------------------------------------------------------- /apps/drain-safe/src/components/Logo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | const IconImg = styled.img` 5 | margin-right: 10px; 6 | height: 3em; 7 | width: auto; 8 | ` 9 | 10 | function Logo(): JSX.Element { 11 | return 12 | } 13 | 14 | export default Logo 15 | -------------------------------------------------------------------------------- /apps/drain-safe/src/components/SubmitButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@gnosis.pm/safe-react-components' 2 | import Flex from './Flex' 3 | 4 | function SubmitButton({ 5 | children, 6 | disabled, 7 | }: { 8 | children: string 9 | disabled?: boolean 10 | }): JSX.Element { 11 | return ( 12 | 13 | 16 | 17 | ) 18 | } 19 | 20 | export default SubmitButton 21 | -------------------------------------------------------------------------------- /apps/drain-safe/src/components/TimedComponent.tsx: -------------------------------------------------------------------------------- 1 | import useTimeout from '../hooks/useTimeout' 2 | 3 | type Props = { 4 | onTimeout: () => void 5 | timeout: number 6 | } 7 | 8 | const TimedComponent: React.FC = ({ onTimeout, timeout, children }) => { 9 | useTimeout(onTimeout, timeout) 10 | 11 | return children as React.ReactElement 12 | } 13 | 14 | export default TimedComponent 15 | -------------------------------------------------------------------------------- /apps/drain-safe/src/hooks/use-balances.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from 'react' 2 | import { useSafeAppsSDK } from '@safe-global/safe-apps-react-sdk' 3 | import { TokenBalance } from '@safe-global/safe-apps-sdk' 4 | import { NATIVE_TOKEN } from '../utils/sdk-helpers' 5 | 6 | export type BalancesType = { 7 | assets: TokenBalance[] 8 | error?: Error 9 | loaded: boolean 10 | selectedTokens: string[] 11 | setSelectedTokens: (tokens: string[]) => void 12 | } 13 | 14 | const transferableTokens = (item: TokenBalance) => 15 | item.tokenInfo.type !== NATIVE_TOKEN || 16 | (item.tokenInfo.type === NATIVE_TOKEN && Number(item.fiatBalance) !== 0) 17 | 18 | function useBalances(safeAddress: string, chainId: number): BalancesType { 19 | const { sdk } = useSafeAppsSDK() 20 | const [assets, setAssets] = useState([]) 21 | const [selectedTokens, setSelectedTokens] = useState([]) 22 | const [error, setError] = useState() 23 | const [loaded, setLoaded] = useState(false) 24 | 25 | const loadBalances = useCallback(async () => { 26 | if (!safeAddress || !chainId) { 27 | return 28 | } 29 | 30 | try { 31 | const balances = await sdk.safe.experimental_getBalances({ 32 | currency: 'USD', 33 | }) 34 | const assets = balances.items.filter(transferableTokens) 35 | 36 | setAssets(assets) 37 | setSelectedTokens(assets.map((token: TokenBalance) => token.tokenInfo.address)) 38 | } catch (err) { 39 | setError(err as Error) 40 | } finally { 41 | setLoaded(true) 42 | } 43 | }, [safeAddress, chainId, sdk]) 44 | 45 | useEffect(() => { 46 | loadBalances() 47 | }, [loadBalances]) 48 | 49 | return { assets, error, loaded, selectedTokens, setSelectedTokens } 50 | } 51 | 52 | export default useBalances 53 | -------------------------------------------------------------------------------- /apps/drain-safe/src/hooks/useTimeout.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect, useRef } from 'react' 2 | 3 | function useTimeout(callback: () => void, delay: number | null) { 4 | const savedCallback = useRef(callback) 5 | 6 | useLayoutEffect(() => { 7 | savedCallback.current = callback 8 | }, [callback]) 9 | 10 | useEffect(() => { 11 | if (!delay && delay !== 0) { 12 | return 13 | } 14 | 15 | const id = setTimeout(() => savedCallback.current(), delay) 16 | 17 | return () => clearTimeout(id) 18 | }, [delay]) 19 | } 20 | 21 | export default useTimeout 22 | -------------------------------------------------------------------------------- /apps/drain-safe/src/hooks/useWeb3.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import Web3 from 'web3' 3 | import { useSafeAppsSDK } from '@safe-global/safe-apps-react-sdk' 4 | import { SafeAppProvider } from '@safe-global/safe-apps-provider' 5 | 6 | function useWeb3() { 7 | const [web3, setWeb3] = useState() 8 | const { safe, sdk } = useSafeAppsSDK() 9 | 10 | useEffect(() => { 11 | const safeProvider = new SafeAppProvider(safe, sdk) 12 | // @ts-expect-error Web3 is complaining about some missing properties from websocket provider 13 | const web3Instance = new Web3(safeProvider) 14 | 15 | setWeb3(web3Instance) 16 | }, [safe, sdk]) 17 | 18 | return { 19 | web3, 20 | } 21 | } 22 | 23 | export default useWeb3 24 | -------------------------------------------------------------------------------- /apps/drain-safe/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { ThemeProvider } from 'styled-components' 4 | import { theme } from '@gnosis.pm/safe-react-components' 5 | import SafeProvider from '@safe-global/safe-apps-react-sdk' 6 | 7 | import GlobalStyle from './GlobalStyle' 8 | import App from './components/App' 9 | import AppLoader from './components/AppLoader' 10 | 11 | ReactDOM.render( 12 | 13 | 14 | 15 | }> 16 | 17 | 18 | 19 | , 20 | document.getElementById('root'), 21 | ) 22 | -------------------------------------------------------------------------------- /apps/drain-safe/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/drain-safe/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect' 2 | 3 | // Jest is not able to use this function from node, which is used at viem v1.3.0 4 | // We need to import it manually 5 | import { TextEncoder } from 'util' 6 | 7 | global.TextEncoder = TextEncoder 8 | // END 9 | -------------------------------------------------------------------------------- /apps/drain-safe/src/utils/formatters.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js' 2 | 3 | export const formatTokenValue = (value: number | string, decimals: number): string => { 4 | return new BigNumber(value).times(`1e-${decimals}`).toFixed() 5 | } 6 | 7 | export const formatCurrencyValue = (value: string, currency: string): string => { 8 | return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(parseFloat(value)) 9 | } 10 | -------------------------------------------------------------------------------- /apps/drain-safe/src/utils/sdk-helpers.ts: -------------------------------------------------------------------------------- 1 | import web3Utils, { AbiItem } from 'web3-utils' 2 | import abiCoder, { AbiCoder } from 'web3-eth-abi' 3 | import { BaseTransaction, TokenBalance, TokenType } from '@safe-global/safe-apps-sdk' 4 | import erc20 from '../abis/erc20' 5 | 6 | export const NATIVE_TOKEN = TokenType['NATIVE_TOKEN'] 7 | 8 | export function encodeTxData(method: AbiItem, recipient: string, amount: string): string { 9 | const abi = abiCoder as unknown // a bug in the web3-eth-abi types 10 | return (abi as AbiCoder).encodeFunctionCall(method, [ 11 | web3Utils.toChecksumAddress(recipient), 12 | amount, 13 | ]) 14 | } 15 | 16 | export function tokenToTx(recipient: string, item: TokenBalance): BaseTransaction { 17 | return item.tokenInfo.type === NATIVE_TOKEN 18 | ? { 19 | // Send ETH directly to the recipient address 20 | to: web3Utils.toChecksumAddress(recipient), 21 | value: item.balance, 22 | data: '0x', 23 | } 24 | : { 25 | // For other token types, generate a contract tx 26 | to: web3Utils.toChecksumAddress(item.tokenInfo.address), 27 | value: '0', 28 | data: encodeTxData(erc20.transfer, recipient, item.balance), 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/drain-safe/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "noFallthroughCasesInSwitch": false, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /apps/mmi/.env.sample: -------------------------------------------------------------------------------- 1 | GENERATE_SOURCEMAP=false 2 | REACT_APP_MMI_BACKEND_BASE_URL= 3 | REACT_APP_MMI_ENVIRONMENT= -------------------------------------------------------------------------------- /apps/mmi/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /apps/mmi/README.md: -------------------------------------------------------------------------------- 1 | # Metamask Institutional Safe App 2 | 3 | Integrate Safe with MMI 4 | 5 | ## Config env variables 6 | 7 | - Add `REACT_APP_MMI_BACKEND_BASE_URL` 8 | - Add `REACT_APP_MMI_ENVIRONMENT` (safe-prod, safe-staging) 9 | -------------------------------------------------------------------------------- /apps/mmi/config-overrides.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | 3 | module.exports = { 4 | webpack: function (config, env) { 5 | const fallback = config.resolve.fallback || {} 6 | 7 | // https://github.com/ChainSafe/web3.js#web3-and-create-react-app 8 | Object.assign(fallback, { 9 | crypto: require.resolve('crypto-browserify'), 10 | stream: require.resolve('stream-browserify'), 11 | assert: require.resolve('assert'), 12 | http: require.resolve('stream-http'), 13 | https: require.resolve('https-browserify'), 14 | os: require.resolve('os-browserify'), 15 | url: require.resolve('url'), 16 | // https://stackoverflow.com/questions/68707553/uncaught-referenceerror-buffer-is-not-defined 17 | buffer: require.resolve('buffer'), 18 | }) 19 | 20 | config.resolve.fallback = fallback 21 | 22 | config.plugins = (config.plugins || []).concat([ 23 | new webpack.ProvidePlugin({ 24 | process: 'process/browser', 25 | Buffer: ['buffer', 'Buffer'], 26 | }), 27 | ]) 28 | 29 | // https://github.com/facebook/create-react-app/issues/11924 30 | config.ignoreWarnings = [/to parse source map/i] 31 | 32 | return config 33 | }, 34 | jest: function (config) { 35 | return config 36 | }, 37 | devServer: function (configFunction) { 38 | return function (proxy, allowedHost) { 39 | const config = configFunction(proxy, allowedHost) 40 | 41 | config.headers = { 42 | 'Access-Control-Allow-Origin': '*', 43 | 'Access-Control-Allow-Methods': 'GET', 44 | 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization', 45 | } 46 | 47 | return config 48 | } 49 | }, 50 | paths: function (paths) { 51 | return paths 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /apps/mmi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mmi", 3 | "version": "0.2.0", 4 | "private": true, 5 | "homepage": "/mmi", 6 | "dependencies": { 7 | "@emotion/react": "^11.10.5", 8 | "@emotion/styled": "^11.10.5", 9 | "@mui/icons-material": "^5.10.9", 10 | "@mui/material": "^5.10.12", 11 | "@safe-global/safe-react-components": "^2.0.0", 12 | "ethers": "^5.6.2" 13 | }, 14 | "scripts": { 15 | "start": "dotenv -e .env -- react-app-rewired start", 16 | "build": "dotenv -e .env -- react-app-rewired build", 17 | "test": "react-app-rewired test", 18 | "deploy:s3": "bash ../../scripts/deploy_to_s3_bucket.sh", 19 | "deploy:pr": "bash ../../scripts/deploy_pr.sh", 20 | "deploy:prod-hook": "bash ../../scripts/prepare_production_deployment.sh" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/mmi/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "apps/mmi/", 3 | "sourceRoot": "apps/mmi/src/", 4 | "projectType": "application", 5 | "tags": ["scope:applications"], 6 | "targets": { 7 | "version": { 8 | "executor": "@jscutlery/semver:version", 9 | "options": { 10 | "commitMessageFormat": "chore(${projectName}): release version ${version}" 11 | } 12 | }, 13 | "github": { 14 | "executor": "@jscutlery/semver:github", 15 | "options": { 16 | "tag": "${tag}", 17 | "generateNotes": true 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/mmi/public/_headers: -------------------------------------------------------------------------------- 1 | /* 2 | Access-Control-Allow-Origin: * 3 | Access-Control-Allow-Methods: GET 4 | Access-Control-Allow-Headers: X-Requested-With, content-type, Authorization -------------------------------------------------------------------------------- /apps/mmi/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | MMI Safe App 25 | 26 | 27 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /apps/mmi/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MetaMask Institutional", 3 | "description": "Setup your Safe with MMI and use it inside the Metamask UI", 4 | "icons": [ 5 | { 6 | "src": "mmi.svg", 7 | "sizes": "any", 8 | "type": "image/svg+xml" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /apps/mmi/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /apps/mmi/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import App from './App' 2 | 3 | test('render App', () => { 4 | // render() 5 | }) 6 | -------------------------------------------------------------------------------- /apps/mmi/src/components/AppBar.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@material-ui/core' 2 | import { AppBar as MuiAppBar, Typography, styled } from '@mui/material' 3 | import { EthHashInfo } from '@safe-global/safe-react-components' 4 | 5 | const AppBar = ({ account }: { account: string }) => { 6 | return ( 7 | 8 | 9 | MetaMask Institutional 10 | 11 | {account && ( 12 | 13 | 14 | 15 | )} 16 | 17 | ) 18 | } 19 | 20 | const StyledAppBar = styled(MuiAppBar)` 21 | && { 22 | position: sticky; 23 | top: 0; 24 | background: ${({ theme }) => theme.palette.background.paper}; 25 | height: 70px; 26 | align-items: center; 27 | justify-content: space-between; 28 | flex-direction: row; 29 | border-bottom: 2px solid ${({ theme }) => theme.palette.background.paper}; 30 | box-shadow: none; 31 | } 32 | ` 33 | 34 | export default AppBar 35 | -------------------------------------------------------------------------------- /apps/mmi/src/components/Help.tsx: -------------------------------------------------------------------------------- 1 | import ExpandMoreIcon from '@mui/icons-material/ExpandMore' 2 | import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined' 3 | import { 4 | Box, 5 | Typography, 6 | Accordion, 7 | AccordionSummary, 8 | AccordionDetails, 9 | styled, 10 | } from '@mui/material' 11 | 12 | type HelpProps = { 13 | title: string 14 | steps: string[] 15 | } 16 | 17 | const Help = ({ title, steps }: HelpProps): React.ReactElement => { 18 | return ( 19 | 20 | }> 21 | 22 | 23 | {title} 24 | 25 | 26 | 27 | {steps.map((step, index) => ( 28 | 29 | 30 | {index + 1} 31 | 32 | {step} 33 | 34 | ))} 35 | 36 | 37 | ) 38 | } 39 | 40 | const StyledDot = styled('div')` 41 | display: flex; 42 | align-items: center; 43 | justify-content: center; 44 | border-radius: 50%; 45 | min-width: 20px; 46 | width: 20px; 47 | height: 20px; 48 | margin-right: 16px; 49 | background: ${({ theme }) => theme.palette.background.main}; 50 | color: ${({ theme }) => theme.palette.text.primary}; 51 | ` 52 | 53 | export default Help 54 | -------------------------------------------------------------------------------- /apps/mmi/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import SafeProvider from '@safe-global/safe-apps-react-sdk' 4 | import { CssBaseline, Theme, ThemeProvider } from '@mui/material' 5 | import { SafeThemeProvider } from '@safe-global/safe-react-components' 6 | import App from './App' 7 | 8 | import '@safe-global/safe-react-components/dist/fonts.css' 9 | 10 | ReactDOM.render( 11 | 12 | 13 | {(safeTheme: Theme) => ( 14 | 15 | 16 | Waiting for Safe...}> 17 | 18 | 19 | 20 | )} 21 | 22 | , 23 | document.getElementById('root'), 24 | ) 25 | -------------------------------------------------------------------------------- /apps/mmi/src/lib/http.ts: -------------------------------------------------------------------------------- 1 | export const MMI_BASE_URL = `${process.env.REACT_APP_MMI_BACKEND_BASE_URL}/api/v1` 2 | 3 | export const getRefreshToken = async (address: string, signature: string): Promise => { 4 | try { 5 | const response = await fetch(`${MMI_BASE_URL}/oauth/auth/`, { 6 | method: 'POST', 7 | headers: { 8 | 'Content-Type': 'application/json', 9 | }, 10 | body: JSON.stringify({ 11 | address, 12 | signature, 13 | }), 14 | }) 15 | 16 | const data = await response.json() 17 | 18 | return data.refresh_token 19 | } catch (error) { 20 | throw error 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/mmi/src/lib/mmi.ts: -------------------------------------------------------------------------------- 1 | import { utils } from 'ethers' 2 | export const sign = async (address: string, msg: string): Promise => { 3 | const checksumAddress = utils.getAddress(address) 4 | 5 | try { 6 | const signature = await window.ethereum.request({ 7 | method: 'personal_sign', 8 | params: [checksumAddress, msg], 9 | }) 10 | 11 | return signature 12 | } catch (error) { 13 | throw error 14 | } 15 | } 16 | 17 | export const authenticate = async (refreshToken: string) => { 18 | try { 19 | await window.ethereum.request({ 20 | method: 'metamaskinstitutional_authenticate', 21 | params: { 22 | token: refreshToken, 23 | apiUrl: `${process.env.REACT_APP_MMI_BACKEND_BASE_URL}/api`, 24 | feature: 'custodian', 25 | service: 'JSONRPC', 26 | environment: process.env.REACT_APP_MMI_ENVIRONMENT, 27 | labels: [ 28 | { 29 | key: 'token', 30 | value: 'Token', 31 | }, 32 | ], 33 | }, 34 | }) 35 | } catch (err) { 36 | console.error(err) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /apps/mmi/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | const truncateRegex = /^(0x[a-zA-Z0-9]{4})[a-zA-Z0-9]+([a-zA-Z0-9]{4})$/ 2 | 3 | export const truncateEthAddress = (address: string) => { 4 | if (!address) return 5 | 6 | const match = address.match(truncateRegex) 7 | if (!match) return address 8 | return `${match[1]}…${match[2]}` 9 | } 10 | -------------------------------------------------------------------------------- /apps/mmi/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface Window { 4 | ethereum: any 5 | } 6 | -------------------------------------------------------------------------------- /apps/mmi/src/setupProxy.js: -------------------------------------------------------------------------------- 1 | module.exports = function (app) { 2 | app.use('/manifest.json', function (req, res, next) { 3 | res.set({ 4 | 'Access-Control-Allow-Origin': '*', 5 | 'Access-Control-Allow-Methods': 'GET', 6 | 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization', 7 | }) 8 | 9 | next() 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /apps/mmi/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom' 6 | -------------------------------------------------------------------------------- /apps/mmi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /apps/ramp-network/.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_RAMP_APIKEY= -------------------------------------------------------------------------------- /apps/ramp-network/README.md: -------------------------------------------------------------------------------- 1 | # Ramp Network 2 | 3 | ## Getting Started 4 | 5 | Install dependencies and start a local dev server. 6 | 7 | ``` 8 | yarn install 9 | cp .env.sample .env 10 | yarn start 11 | ``` 12 | 13 | Then: 14 | 15 | - If HTTPS is used (by default enabled) 16 | - Open your Safe app locally (by default via https://localhost:3000/) and accept the SSL error. 17 | - Go to Safe Multisig web interface 18 | - [Mainnet](https://app.safe.global/?chain=eth) 19 | - [Goerli](https://app.safe.global/?chain=gor) 20 | - Create your test safe 21 | - Go to Apps -> Manage Apps -> Add Custom App 22 | - Paste your localhost URL, default is https://localhost:3000/ 23 | - You should see Safe App Starter as a new app 24 | - Develop your app from there 25 | -------------------------------------------------------------------------------- /apps/ramp-network/config-overrides.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | 3 | module.exports = { 4 | webpack: function (config, env) { 5 | const fallback = config.resolve.fallback || {} 6 | 7 | // https://github.com/ChainSafe/web3.js#web3-and-create-react-app 8 | Object.assign(fallback, { 9 | crypto: require.resolve('crypto-browserify'), 10 | stream: require.resolve('stream-browserify'), 11 | assert: require.resolve('assert'), 12 | http: require.resolve('stream-http'), 13 | https: require.resolve('https-browserify'), 14 | os: require.resolve('os-browserify'), 15 | url: require.resolve('url'), 16 | // https://stackoverflow.com/questions/68707553/uncaught-referenceerror-buffer-is-not-defined 17 | buffer: require.resolve('buffer'), 18 | }) 19 | 20 | config.resolve.fallback = fallback 21 | 22 | config.plugins = (config.plugins || []).concat([ 23 | new webpack.ProvidePlugin({ 24 | process: 'process/browser', 25 | Buffer: ['buffer', 'Buffer'], 26 | }), 27 | ]) 28 | 29 | // https://github.com/facebook/create-react-app/issues/11924 30 | config.ignoreWarnings = [/to parse source map/i] 31 | 32 | return config 33 | }, 34 | jest: function (config) { 35 | return config 36 | }, 37 | devServer: function (configFunction) { 38 | return function (proxy, allowedHost) { 39 | const config = configFunction(proxy, allowedHost) 40 | 41 | config.headers = { 42 | 'Access-Control-Allow-Origin': '*', 43 | 'Access-Control-Allow-Methods': 'GET', 44 | 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization', 45 | } 46 | 47 | return config 48 | } 49 | }, 50 | paths: function (paths) { 51 | return paths 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /apps/ramp-network/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ramp-network", 3 | "version": "0.3.0", 4 | "private": true, 5 | "homepage": "./", 6 | "dependencies": { 7 | "@gnosis.pm/safe-react-components": "^0.9.7", 8 | "@material-ui/core": "^4.12.4", 9 | "@ramp-network/ramp-instant-sdk": "^4.0.4" 10 | }, 11 | "scripts": { 12 | "start": "react-app-rewired start", 13 | "build": "react-app-rewired build", 14 | "test": "react-app-rewired test --passWithNoTests", 15 | "deploy:s3": "bash ../../scripts/deploy_to_s3_bucket.sh", 16 | "deploy:pr": "bash ../../scripts/deploy_pr.sh", 17 | "deploy:prod-hook": "bash ../../scripts/prepare_production_deployment.sh" 18 | }, 19 | "eslintConfig": { 20 | "extends": [ 21 | "react-app", 22 | "plugin:jsx-a11y/recommended" 23 | ], 24 | "plugins": [ 25 | "jsx-a11y" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/ramp-network/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "apps/ramp-network/", 3 | "sourceRoot": "apps/ramp-network/src/", 4 | "projectType": "application", 5 | "tags": ["scope:applications"], 6 | "targets": { 7 | "version": { 8 | "executor": "@jscutlery/semver:version", 9 | "options": { 10 | "commitMessageFormat": "chore(${projectName}): release version ${version}" 11 | } 12 | }, 13 | "github": { 14 | "executor": "@jscutlery/semver:github", 15 | "options": { 16 | "tag": "${tag}", 17 | "generateNotes": true 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/ramp-network/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | Ramp Network Safe App 25 | 26 | 27 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /apps/ramp-network/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ramp Network", 3 | "description": "Buy crypto directly from your Safe", 4 | "iconPath": "ramp.svg", 5 | "icons": [ 6 | { 7 | "src": "ramp.svg", 8 | "sizes": "any", 9 | "type": "image/svg+xml" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /apps/ramp-network/public/ramp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/ramp-network/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /apps/ramp-network/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const RAMP_API_KEY = process.env.REACT_APP_RAMP_APIKEY 2 | -------------------------------------------------------------------------------- /apps/ramp-network/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { ThemeProvider } from 'styled-components' 4 | import { Loader, theme, Title } from '@gnosis.pm/safe-react-components' 5 | import SafeProvider from '@safe-global/safe-apps-react-sdk' 6 | 7 | import App from './App' 8 | 9 | const AppLoader = () => ( 10 | <> 11 | Waiting for Safe... 12 | 13 | 14 | ) 15 | 16 | ReactDOM.render( 17 | 18 | 19 | }> 20 | 21 | 22 | 23 | , 24 | document.getElementById('root'), 25 | ) 26 | -------------------------------------------------------------------------------- /apps/ramp-network/src/ramp.ts: -------------------------------------------------------------------------------- 1 | import { RampInstantEvent, RampInstantSDK } from '@ramp-network/ramp-instant-sdk' 2 | import { RAMP_API_KEY } from './constants' 3 | 4 | const WIDGET_CLOSE_EVENT = 'WIDGET_CLOSE' 5 | const PURCHASE_CREATED_EVENT = 'PURCHASE_CREATED' 6 | 7 | export const ASSETS_BY_CHAIN: { [key: string]: string } = { 8 | '1': 'ETH_*', 9 | '10': 'OPTIMISM_*', 10 | '56': 'BSC_*', 11 | '137': 'MATIC_*', 12 | '100': 'XDAI_*', 13 | '43114': 'AVAX_*', 14 | '8453': 'BASE_*', 15 | '324': 'ZKSYNCERA_*', 16 | '1101': 'POLYGONZKEVM_*', 17 | '42161': 'ARBITRUM_*', 18 | '42220': 'CELO_*', 19 | '59144': 'LINEA_*', 20 | } 21 | 22 | type RampWidgetInitializer = { 23 | assets: string 24 | address: string 25 | onClose?: () => void 26 | } 27 | 28 | export const initializeRampWidget = ({ assets, address, onClose }: RampWidgetInitializer) => { 29 | return new RampInstantSDK({ 30 | hostAppName: 'Ramp Network Safe App', 31 | hostLogoUrl: 'https://docs.ramp.network/img/logo-1.svg', 32 | swapAsset: assets, 33 | userAddress: address, 34 | hostApiKey: RAMP_API_KEY, 35 | }) 36 | .on('*', (event: RampInstantEvent) => { 37 | if (event.type === WIDGET_CLOSE_EVENT) { 38 | onClose?.() 39 | } 40 | 41 | if (event.type === PURCHASE_CREATED_EVENT) { 42 | // TODO: Send Analytics when the infra is ready 43 | // https://github.com/gnosis/safe-apps-sdk/issues/255 44 | console.log('PURCHASE_CREATED_EVENT', event) 45 | } 46 | }) 47 | .show() 48 | } 49 | -------------------------------------------------------------------------------- /apps/ramp-network/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/ramp-network/src/setupProxy.js: -------------------------------------------------------------------------------- 1 | // App is an express application, we can add an express middleware that will set headers for manifest.json request 2 | // https://create-react-app.dev/docs/proxying-api-requests-in-development/#configuring-the-proxy-manually 3 | 4 | module.exports = function (app) { 5 | app.use('/manifest.json', function (req, res, next) { 6 | res.set({ 7 | 'Access-Control-Allow-Origin': '*', 8 | 'Access-Control-Allow-Methods': 'GET', 9 | 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization', 10 | }) 11 | 12 | next() 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /apps/ramp-network/src/utils.ts: -------------------------------------------------------------------------------- 1 | export const goBack = () => window.history.back() 2 | -------------------------------------------------------------------------------- /apps/ramp-network/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "noFallthroughCasesInSwitch": false, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). 4 | 5 | ## [0.1.1](https://github.com/safe-global/safe-react-apps/compare/siwe-delegate-manager-0.1.0...siwe-delegate-manager-0.1.1) (2023-02-06) 6 | 7 | 8 | 9 | # 0.1.0 (2022-11-18) 10 | 11 | 12 | ### Features 13 | 14 | * **siwe-delegate-manager:** Sign-In With Ethereum Delegate Manager Safe App ([#499](https://github.com/safe-global/safe-react-apps/issues/499)) ([34c36c5](https://github.com/safe-global/safe-react-apps/commit/34c36c580300672c6366ad2d534de0a3b1534058)) 15 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/README.md: -------------------------------------------------------------------------------- 1 | # Sign-In with Ethereum Delegate Manager 2 | 3 | // TODO: Create Readme -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "siwe-delegate-manager", 3 | "version": "0.1.1", 4 | "private": true, 5 | "homepage": "./", 6 | "scripts": { 7 | "start": "react-scripts start", 8 | "build": "react-scripts build", 9 | "test": "react-scripts test", 10 | "eject": "react-scripts eject", 11 | "deploy:s3": "bash ../../scripts/deploy_to_s3_bucket.sh", 12 | "deploy:pr": "bash ../../scripts/deploy_pr.sh", 13 | "deploy:prod-hook": "bash ../../scripts/prepare_production_deployment.sh" 14 | }, 15 | "eslintConfig": { 16 | "extends": [ 17 | "react-app", 18 | "react-app/jest" 19 | ] 20 | }, 21 | "dependencies": { 22 | "@gnosis.pm/safe-react-components": "^1.2.0", 23 | "@material-ui/core": "^4.12.4", 24 | "@material-ui/icons": "^4.11.3", 25 | "ethers": "^5.6.9" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "apps/siwe-delegate-manager/", 3 | "sourceRoot": "apps/siwe-delegate-manager/src/", 4 | "projectType": "application", 5 | "tags": ["scope:applications"], 6 | "targets": { 7 | "version": { 8 | "executor": "@jscutlery/semver:version", 9 | "options": { 10 | "commitMessageFormat": "chore(${projectName}): release version ${version}" 11 | } 12 | }, 13 | "github": { 14 | "executor": "@jscutlery/semver:github", 15 | "options": { 16 | "tag": "${tag}", 17 | "generateNotes": true 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safe-global/safe-react-apps/2bbc198c090c81eb784f4a8a646382520e338056/apps/siwe-delegate-manager/public/favicon.ico -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Sign-In with Ethereum Delegate Manager 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "SIWE Delegate Manager", 3 | "name": "Sign-In with Ethereum Delegate Manager", 4 | "description": "Manage your Sign-In with Ethereum delegates", 5 | "iconPath": "logo.svg", 6 | "icons": [ 7 | { 8 | "src": "logo.svg", 9 | "type": "image/svg+xml", 10 | "sizes": "48x48 72x72 96x96 128x128 256x256 512x512" 11 | } 12 | ], 13 | "start_url": ".", 14 | "display": "standalone", 15 | "theme_color": "#000000", 16 | "background_color": "#ffffff", 17 | "safe_apps_permissions": ["clipboard-write"] 18 | } 19 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | import App from './App' 4 | 5 | test('renders learn react link', () => { 6 | // render() 7 | // const linkElement = screen.getByText(/Delegate Registry Manager/i) 8 | // expect(linkElement).toBeInTheDocument() 9 | }) 10 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/src/GlobalStyles.tsx: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components' 2 | 3 | const GlobalStyles = createGlobalStyle` 4 | body { 5 | height: 100%; 6 | margin: 0px; 7 | padding: 0px; 8 | } 9 | ` 10 | 11 | export default GlobalStyles 12 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/src/assets/delegateRegistryContractABI.ts: -------------------------------------------------------------------------------- 1 | const delegateRegistryContractABI = [ 2 | { 3 | anonymous: false, 4 | inputs: [ 5 | { indexed: true, internalType: 'address', name: 'delegator', type: 'address' }, 6 | { indexed: true, internalType: 'bytes32', name: 'id', type: 'bytes32' }, 7 | { indexed: true, internalType: 'address', name: 'delegate', type: 'address' }, 8 | ], 9 | name: 'ClearDelegate', 10 | type: 'event', 11 | }, 12 | { 13 | anonymous: false, 14 | inputs: [ 15 | { indexed: true, internalType: 'address', name: 'delegator', type: 'address' }, 16 | { indexed: true, internalType: 'bytes32', name: 'id', type: 'bytes32' }, 17 | { indexed: true, internalType: 'address', name: 'delegate', type: 'address' }, 18 | ], 19 | name: 'SetDelegate', 20 | type: 'event', 21 | }, 22 | { 23 | inputs: [{ internalType: 'bytes32', name: 'id', type: 'bytes32' }], 24 | name: 'clearDelegate', 25 | outputs: [], 26 | stateMutability: 'nonpayable', 27 | type: 'function', 28 | }, 29 | { 30 | inputs: [ 31 | { internalType: 'address', name: '', type: 'address' }, 32 | { internalType: 'bytes32', name: '', type: 'bytes32' }, 33 | ], 34 | name: 'delegation', 35 | outputs: [{ internalType: 'address', name: '', type: 'address' }], 36 | stateMutability: 'view', 37 | type: 'function', 38 | }, 39 | { 40 | inputs: [ 41 | { internalType: 'bytes32', name: 'id', type: 'bytes32' }, 42 | { internalType: 'address', name: 'delegate', type: 'address' }, 43 | ], 44 | name: 'setDelegate', 45 | outputs: [], 46 | stateMutability: 'nonpayable', 47 | type: 'function', 48 | }, 49 | ] 50 | 51 | export default delegateRegistryContractABI 52 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/src/components/delegate-event-label/DelegateEventLabel.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@material-ui/core/Box' 2 | import styled from 'styled-components' 3 | import { orange, red } from '@material-ui/core/colors' 4 | 5 | type DelegateEventLabelProps = { 6 | eventType: string 7 | } 8 | 9 | const DelegateEventLabel = ({ eventType }: DelegateEventLabelProps) => { 10 | return ( 11 | 18 | {eventType === 'SetDelegate' ? ( 19 | Set Delegate 20 | ) : ( 21 | Clear Delegate 22 | )} 23 | 24 | ) 25 | } 26 | 27 | export default DelegateEventLabel 28 | 29 | const SetDelegateLabel = styled.span` 30 | padding: 4px 8px; 31 | border-radius: 4px; 32 | 33 | background-color: ${orange[800]}; 34 | color: white; 35 | white-space: nowrap; 36 | ` 37 | 38 | const ClearDelegateLabel = styled.span` 39 | padding: 4px 8px; 40 | border-radius: 4px; 41 | 42 | background-color: ${red[800]}; 43 | color: white; 44 | white-space: nowrap; 45 | ` 46 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/src/components/header/Header.tsx: -------------------------------------------------------------------------------- 1 | import Toolbar from '@material-ui/core/Toolbar' 2 | import Typography from '@material-ui/core/Typography' 3 | import { useSafeAppsSDK } from '@safe-global/safe-apps-react-sdk' 4 | import styled from 'styled-components' 5 | 6 | import AddressLabel from 'src/components/address-label/AddressLabel' 7 | 8 | const Header = () => { 9 | const { safe } = useSafeAppsSDK() 10 | 11 | return ( 12 | 13 | 14 | 15 | 16 | Sign-In With Ethereum Delegate Manager 17 | 18 | 19 | 20 | 21 | 27 | 28 | 29 | 30 | ) 31 | } 32 | 33 | export default Header 34 | 35 | const TitleWapper = styled.div` 36 | flex-grow: 1; 37 | ` 38 | 39 | const AppBar = styled.header` 40 | position: fixed; 41 | width: 100%; 42 | border-bottom: 1px solid #e2e3e3; 43 | z-index: 10; 44 | background-color: white; 45 | height: 64px; 46 | box-sizing: border-box; 47 | ` 48 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/src/components/loader/Loader.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import { Loader as CircularProgress } from '@gnosis.pm/safe-react-components' 3 | import Box from '@material-ui/core/Box' 4 | import Typography from '@material-ui/core/Typography' 5 | import styled from 'styled-components' 6 | 7 | type LoaderProps = { 8 | isLoading?: boolean 9 | children?: ReactNode 10 | loadingText?: ReactNode 11 | minHeight?: number 12 | } 13 | 14 | const Loader = ({ isLoading, loadingText, minHeight, children }: LoaderProps) => { 15 | return isLoading ? ( 16 | 24 | 25 | {loadingText} 26 | 27 | ) : ( 28 | <>{children} 29 | ) 30 | } 31 | 32 | export default Loader 33 | 34 | const StyledCircularProgress = styled(CircularProgress)` 35 | margin: 18px 0; 36 | ` 37 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/src/components/modals/RemoveDelegatorModal.tsx: -------------------------------------------------------------------------------- 1 | import { GenericModal } from '@gnosis.pm/safe-react-components' 2 | import styled from 'styled-components' 3 | 4 | import DelegateForm from 'src/components/delegate-form/DelegateForm' 5 | import { useDelegateRegistry } from 'src/store/delegateRegistryContext' 6 | 7 | type RemoveDelegatorModalProps = { 8 | delegator: string 9 | onClose: () => void 10 | } 11 | 12 | const RemoveDelegatorModal = ({ delegator, onClose }: RemoveDelegatorModalProps) => { 13 | const { clearDelegate } = useDelegateRegistry() 14 | 15 | const handleSubmit = async (space: string) => { 16 | await clearDelegate(space) 17 | onClose() 18 | } 19 | 20 | return ( 21 | 25 | 31 | 32 | } 33 | onClose={onClose} 34 | /> 35 | ) 36 | } 37 | 38 | export default RemoveDelegatorModal 39 | 40 | const FormContainer = styled.div` 41 | max-width: 500px; 42 | border-radius: 8px; 43 | 44 | background-color: white; 45 | ` 46 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/src/components/space-label/SpaceLabel.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@material-ui/core/Box' 2 | import styled from 'styled-components' 3 | import { green } from '@material-ui/core/colors' 4 | 5 | type SpaceLabelProps = { 6 | space: string 7 | } 8 | 9 | export const ALL_SPACES = '' 10 | 11 | const SpaceLabel = ({ space }: SpaceLabelProps) => { 12 | const isAllSpaces = space === ALL_SPACES 13 | return ( 14 | 21 | {isAllSpaces ? 'All spaces' : space} 22 | 23 | ) 24 | } 25 | 26 | export default SpaceLabel 27 | 28 | const StyledLabel = styled.span` 29 | padding: 4px 8px; 30 | border-radius: 4px; 31 | 32 | background-color: ${green[600]}; 33 | color: white; 34 | white-space: nowrap; 35 | ` 36 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/src/hooks/useMemoizedAddressLabel.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | 3 | const ADDRESS_LENGTH = 42 4 | const CHAR_DISPLAYED = 6 5 | 6 | const useMemoizedAddressLabel = (address: string, showFullAddress: boolean = false) => { 7 | const addressLabel = useMemo(() => { 8 | if (address && !showFullAddress) { 9 | const firstPart = address.slice(0, CHAR_DISPLAYED) 10 | const lastPart = address.slice(ADDRESS_LENGTH - CHAR_DISPLAYED) 11 | 12 | return `${firstPart}...${lastPart}` 13 | } 14 | 15 | return address 16 | }, [address, showFullAddress]) 17 | 18 | return addressLabel 19 | } 20 | 21 | export default useMemoizedAddressLabel 22 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/src/hooks/useMemoizedTransactionLabel.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | 3 | const TRANSACTION_HASH_LENGTH = 66 4 | const CHAR_DISPLAYED = 10 5 | 6 | const useMemoizedTransactionLabel = (address: string, showFullAddress: boolean = false) => { 7 | const addressLabel = useMemo(() => { 8 | if (address && !showFullAddress) { 9 | const firstPart = address.slice(0, CHAR_DISPLAYED) 10 | const lastPart = address.slice(TRANSACTION_HASH_LENGTH - CHAR_DISPLAYED) 11 | 12 | return `${firstPart}...${lastPart}` 13 | } 14 | 15 | return address 16 | }, [address, showFullAddress]) 17 | 18 | return addressLabel 19 | } 20 | 21 | export default useMemoizedTransactionLabel 22 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/src/hooks/useModal.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | 3 | const useModal = (initialValue = false) => { 4 | const [open, setOpen] = useState(initialValue) 5 | 6 | const openModal = useCallback(() => { 7 | setOpen(true) 8 | }, []) 9 | 10 | const closeModal = useCallback(() => { 11 | setOpen(false) 12 | }, []) 13 | 14 | const toggleModal = useCallback(() => { 15 | setOpen(open => !open) 16 | }, []) 17 | 18 | return { 19 | open, 20 | setOpen, 21 | 22 | openModal, 23 | closeModal, 24 | toggleModal, 25 | } 26 | } 27 | 28 | export default useModal 29 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | import { ThemeProvider } from 'styled-components' 5 | import { theme } from '@gnosis.pm/safe-react-components' 6 | import { SafeProvider } from '@safe-global/safe-apps-react-sdk' 7 | 8 | import { SafeWalletProvider } from 'src/store/safeWalletContext' 9 | import { DelegateRegistryProvider } from 'src/store/delegateRegistryContext' 10 | import GlobalStyles from 'src/GlobalStyles' 11 | import App from 'src/App' 12 | 13 | ReactDOM.render( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | , 26 | document.getElementById('root'), 27 | ) 28 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom' 6 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/src/utils/siwe.ts: -------------------------------------------------------------------------------- 1 | import { keccak256 } from 'ethers/lib/utils' 2 | import { toUtf8Bytes } from '@ethersproject/strings' 3 | 4 | const getSiWeSpaceId = (delegateAddress: string): string => 5 | keccak256(toUtf8Bytes(`siwe${delegateAddress}`)) 6 | 7 | export { getSiWeSpaceId } 8 | -------------------------------------------------------------------------------- /apps/siwe-delegate-manager/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "baseUrl": "." 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /apps/tx-builder/.env.example: -------------------------------------------------------------------------------- 1 | HTTPS=false 2 | REACT_APP_TENDERLY_SIMULATE_ENDPOINT_URL= 3 | REACT_APP_TENDERLY_PROJECT_NAME= 4 | REACT_APP_TENDERLY_ORG_NAME= 5 | 6 | # Required only to deploy your own Smart Contract using command line 7 | INFURA_KEY= 8 | PRIVATE_KEY= 9 | ETHERSCAN_API_KEY= 10 | -------------------------------------------------------------------------------- /apps/tx-builder/config-overrides.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | 3 | module.exports = { 4 | webpack: function (config, env) { 5 | const fallback = config.resolve.fallback || {} 6 | 7 | // https://github.com/ChainSafe/web3.js#web3-and-create-react-app 8 | Object.assign(fallback, { 9 | crypto: require.resolve('crypto-browserify'), 10 | stream: require.resolve('stream-browserify'), 11 | assert: require.resolve('assert'), 12 | http: require.resolve('stream-http'), 13 | https: require.resolve('https-browserify'), 14 | os: require.resolve('os-browserify'), 15 | url: require.resolve('url'), 16 | // https://stackoverflow.com/questions/68707553/uncaught-referenceerror-buffer-is-not-defined 17 | buffer: require.resolve('buffer'), 18 | }) 19 | 20 | config.resolve.fallback = fallback 21 | 22 | config.plugins = (config.plugins || []).concat([ 23 | new webpack.ProvidePlugin({ 24 | process: 'process/browser', 25 | Buffer: ['buffer', 'Buffer'], 26 | }), 27 | ]) 28 | 29 | // https://github.com/facebook/create-react-app/issues/11924 30 | config.ignoreWarnings = [/to parse source map/i] 31 | 32 | return config 33 | }, 34 | jest: function (config) { 35 | return config 36 | }, 37 | devServer: function (configFunction) { 38 | return function (proxy, allowedHost) { 39 | const config = configFunction(proxy, allowedHost) 40 | 41 | config.headers = { 42 | 'Access-Control-Allow-Origin': '*', 43 | 'Access-Control-Allow-Methods': 'GET', 44 | 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization', 45 | } 46 | 47 | return config 48 | } 49 | }, 50 | paths: function (paths) { 51 | return paths 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /apps/tx-builder/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | require('hardhat-deploy') 3 | require('@nomiclabs/hardhat-ethers') 4 | require('@nomiclabs/hardhat-etherscan') 5 | 6 | // tasks 7 | require('./src/hardhat/tasks/deploy_contracts') 8 | require('./src/hardhat/tasks/read_method') 9 | 10 | const networks = require('./src/hardhat/networks') 11 | 12 | const { ETHERSCAN_API_KEY } = process.env 13 | 14 | const hardhatConfig = { 15 | solidity: { 16 | version: '0.8.0', 17 | settings: { 18 | optimizer: { 19 | runs: 1, 20 | enabled: true, 21 | }, 22 | }, 23 | }, 24 | 25 | networks, 26 | 27 | etherscan: { 28 | apiKey: ETHERSCAN_API_KEY, 29 | }, 30 | paths: { 31 | sources: './src/contracts', 32 | tests: './src/test', 33 | cache: './src/cache', 34 | artifacts: './src/artifacts', 35 | deploy: './src/hardhat/deploy', 36 | }, 37 | 38 | namedAccounts: { 39 | deployer: 0, 40 | }, 41 | 42 | defaultNetwork: 'rinkeby', 43 | } 44 | 45 | module.exports = hardhatConfig 46 | -------------------------------------------------------------------------------- /apps/tx-builder/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "apps/tx-builder/", 3 | "sourceRoot": "apps/tx-builder/src/", 4 | "projectType": "application", 5 | "tags": ["scope:applications"], 6 | "targets": { 7 | "version": { 8 | "executor": "@jscutlery/semver:version", 9 | "options": { 10 | "commitMessageFormat": "chore(${projectName}): release version ${version}" 11 | } 12 | }, 13 | "github": { 14 | "executor": "@jscutlery/semver:github", 15 | "options": { 16 | "tag": "${tag}", 17 | "generateNotes": true 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/tx-builder/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | Transaction Builder Safe App 25 | 26 | 27 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /apps/tx-builder/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Transaction Builder", 3 | "description": "A Safe app to compose custom transactions", 4 | "iconPath": "tx-builder.png", 5 | "icons": [ 6 | { 7 | "src": "tx-builder.png", 8 | "sizes": "256x256", 9 | "type": "image/png" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /apps/tx-builder/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /apps/tx-builder/public/tx-builder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safe-global/safe-react-apps/2bbc198c090c81eb784f4a8a646382520e338056/apps/tx-builder/public/tx-builder.png -------------------------------------------------------------------------------- /apps/tx-builder/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from 'react-router-dom' 2 | 3 | import Header from './components/Header' 4 | import CreateTransactions from './pages/CreateTransactions' 5 | import Dashboard from './pages/Dashboard' 6 | import EditTransactionLibrary from './pages/EditTransactionLibrary' 7 | import ReviewAndConfirm from './pages/ReviewAndConfirm' 8 | import SaveTransactionLibrary from './pages/SaveTransactionLibrary' 9 | import TransactionLibrary from './pages/TransactionLibrary' 10 | import { 11 | HOME_PATH, 12 | EDIT_BATCH_PATH, 13 | REVIEW_AND_CONFIRM_PATH, 14 | SAVE_BATCH_PATH, 15 | TRANSACTION_LIBRARY_PATH, 16 | } from './routes/routes' 17 | 18 | const App = () => { 19 | return ( 20 | <> 21 | {/* App Header */} 22 |
23 | 24 | 25 | {/* Dashboard Screen (Create transactions) */} 26 | }> 27 | {/* Transactions Batch section */} 28 | } /> 29 | 30 | {/* Save Batch section */} 31 | } /> 32 | 33 | {/* Edit Batch section */} 34 | } /> 35 | 36 | 37 | {/* Review & Confirm Screen */} 38 | } /> 39 | 40 | {/* Transaction Library Screen */} 41 | } /> 42 | 43 | 44 | ) 45 | } 46 | 47 | export default App 48 | -------------------------------------------------------------------------------- /apps/tx-builder/src/assets/fonts/DMSans700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safe-global/safe-react-apps/2bbc198c090c81eb784f4a8a646382520e338056/apps/tx-builder/src/assets/fonts/DMSans700.woff2 -------------------------------------------------------------------------------- /apps/tx-builder/src/assets/fonts/DMSansRegular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safe-global/safe-react-apps/2bbc198c090c81eb784f4a8a646382520e338056/apps/tx-builder/src/assets/fonts/DMSansRegular.woff2 -------------------------------------------------------------------------------- /apps/tx-builder/src/components/Card/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { alpha } from '@material-ui/core/styles' 4 | 5 | const StyledCard = styled.div` 6 | box-shadow: 1px 2px 10px 0 ${alpha('#28363D', 0.18)}; 7 | border-radius: 8px; 8 | padding: 24px; 9 | background-color: ${({ theme }) => theme.palette.common.white}; 10 | position: relative; 11 | ` 12 | 13 | const Disabled = styled.div` 14 | opacity: 0.5; 15 | position: absolute; 16 | height: 100%; 17 | width: 100%; 18 | background-color: ${({ theme }) => theme.palette.common.white}; 19 | z-index: 1; 20 | top: 0; 21 | left: 0; 22 | ` 23 | 24 | type Props = { 25 | className?: string 26 | disabled?: boolean 27 | } & React.HTMLAttributes 28 | 29 | const Card: React.FC = ({ className, children, disabled, ...rest }): React.ReactElement => ( 30 | 31 | {disabled && } 32 | {children} 33 | 34 | ) 35 | 36 | export default Card 37 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/ChecksumWarning.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import MuiAlert from '@material-ui/lab/Alert' 3 | import MuiAlertTitle from '@material-ui/lab/AlertTitle' 4 | import styled from 'styled-components' 5 | import { useTransactionLibrary } from '../store' 6 | 7 | const ChecksumWarning = () => { 8 | const { hasChecksumWarning, setHasChecksumWarning } = useTransactionLibrary() 9 | 10 | if (!hasChecksumWarning) { 11 | return null 12 | } 13 | 14 | return ( 15 | 16 | setHasChecksumWarning(false)}> 17 | 18 | This batch contains some changed properties since you saved or downloaded it 19 | 20 | 21 | 22 | ) 23 | } 24 | 25 | const ChecksumWrapper = styled.div` 26 | position: fixed; 27 | width: 100%; 28 | z-index: 10; 29 | background-color: transparent; 30 | height: 70px; 31 | ` 32 | 33 | export default ChecksumWarning 34 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/Divider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | type Props = { 5 | className?: string 6 | orientation?: 'vertical' | 'horizontal' 7 | } 8 | 9 | const HorizontalDivider = styled.div` 10 | margin: 16px -1.6rem; 11 | border-top: solid 1px ${({ theme }) => theme.palette.border.light}; 12 | width: calc(100% + 3.2rem); 13 | ` 14 | 15 | const VerticalDivider = styled.div` 16 | border-right: 1px solid ${({ theme }) => theme.legacy.colors.separator}; 17 | margin: 0 5px; 18 | height: 100%; 19 | ` 20 | 21 | const Divider = ({ className, orientation }: Props): React.ReactElement => { 22 | return orientation === 'vertical' ? ( 23 | 24 | ) : ( 25 | 26 | ) 27 | } 28 | 29 | export default Divider 30 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/Dot/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { type Theme } from '@material-ui/core/styles' 4 | 5 | type Props = { 6 | className?: string 7 | color: keyof Theme['palette'] 8 | } 9 | 10 | const StyledDot = styled.div` 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | border-radius: 50%; 15 | height: 36px; 16 | width: 36px; 17 | background-color: ${({ theme, color }) => theme.palette[color].main}; 18 | ` 19 | 20 | const Dot: React.FC = ({ children, ...rest }): React.ReactElement => ( 21 | {children} 22 | ) 23 | 24 | export default Dot 25 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/EditableLabel.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export type EditableLabelProps = { 4 | children: React.ReactNode 5 | onEdit: (value: string) => void 6 | } 7 | 8 | const EditableLabel = ({ children, onEdit }: EditableLabelProps) => { 9 | return ( 10 | onEdit(event.target.innerText)} 14 | onKeyPress={(event: any) => 15 | event.key === 'Enter' && event.target.blur() && event.preventDefault() 16 | } 17 | onClick={event => event.stopPropagation()} 18 | > 19 | {children} 20 | 21 | ) 22 | } 23 | 24 | export default EditableLabel 25 | 26 | const EditableComponent = styled.div` 27 | font-family: Averta, 'Roboto', sans-serif; 28 | display: block; 29 | white-space: nowrap; 30 | overflow: hidden; 31 | 32 | padding: 10px; 33 | cursor: text; 34 | border-radius: 8px; 35 | border: 1px solid transparent; 36 | 37 | &:hover { 38 | border-color: #e2e3e3; 39 | } 40 | 41 | &:focus { 42 | outline-color: #008c73; 43 | } 44 | ` 45 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/ErrorAlert.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import MuiAlert from '@material-ui/lab/Alert' 3 | import MuiAlertTitle from '@material-ui/lab/AlertTitle' 4 | import styled from 'styled-components' 5 | import { useTransactionLibrary } from '../store' 6 | 7 | const ErrorAlert = () => { 8 | const { errorMessage, setErrorMessage } = useTransactionLibrary() 9 | 10 | if (!errorMessage) { 11 | return null 12 | } 13 | 14 | return ( 15 | 16 | setErrorMessage('')}> 17 | {errorMessage} 18 | 19 | 20 | ) 21 | } 22 | 23 | const ErrorAlertContainer = styled.div` 24 | position: fixed; 25 | width: 100%; 26 | z-index: 10; 27 | background-color: transparent; 28 | height: 70px; 29 | ` 30 | 31 | export default ErrorAlert 32 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/FixedIcon/images/arrowReceived.tsx: -------------------------------------------------------------------------------- 1 | const icon = ( 2 | 3 | 9 | 10 | ) 11 | 12 | export default icon 13 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/FixedIcon/images/arrowReceivedWhite.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const icon = ( 4 | 5 | 11 | 12 | ) 13 | 14 | export default icon 15 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/FixedIcon/images/arrowSent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const icon = ( 4 | 5 | 11 | 12 | ) 13 | 14 | export default icon 15 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/FixedIcon/images/arrowSentWhite.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const icon = ( 4 | 5 | 11 | 12 | ) 13 | 14 | export default icon 15 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/FixedIcon/images/arrowSort.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const icon = ( 4 | 5 | 11 | 12 | ) 13 | 14 | export default icon 15 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/FixedIcon/images/bullit.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const icon = ( 4 | 5 | 11 | 12 | ) 13 | 14 | export default icon 15 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/FixedIcon/images/chevronDown.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const icon = ( 4 | 5 | 11 | 12 | ) 13 | 14 | export default icon 15 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/FixedIcon/images/chevronLeft.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const icon = ( 4 | 5 | 11 | 12 | ) 13 | 14 | export default icon 15 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/FixedIcon/images/chevronRight.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const icon = ( 4 | 5 | 11 | 12 | ) 13 | 14 | export default icon 15 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/FixedIcon/images/chevronUp.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const icon = ( 4 | 5 | 11 | 12 | ) 13 | 14 | export default icon 15 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/FixedIcon/images/connectedRinkeby.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const icon = ( 4 | 5 | 10 | 11 | ) 12 | 13 | export default icon 14 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/FixedIcon/images/connectedWallet.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const icon = ( 4 | 5 | 10 | 11 | ) 12 | 13 | export default icon 14 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/FixedIcon/images/creatingInProgress.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const icon = ( 4 | 5 | 6 | 7 | 13 | 14 | 15 | ) 16 | 17 | export default icon 18 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/FixedIcon/images/dropdownArrowSmall.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const icon = ( 4 | 5 | 10 | 11 | ) 12 | 13 | export default icon 14 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/FixedIcon/images/networkError.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const icon = ( 4 | 5 | 6 | 7 | 12 | 13 | 22 | 23 | 24 | ) 25 | 26 | export default icon 27 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/FixedIcon/images/notConnected.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const icon = ( 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | ) 17 | 18 | export default icon 19 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/FixedIcon/images/options.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const icon = ( 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ) 12 | 13 | export default icon 14 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/FixedIcon/images/plus.tsx: -------------------------------------------------------------------------------- 1 | const icon = ( 2 | 3 | 10 | 17 | 18 | ) 19 | 20 | export default icon 21 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/FixedIcon/images/settingsChange.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const icon = ( 4 | 5 | 11 | 12 | ) 13 | 14 | export default icon 15 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/FixedIcon/images/threeDots.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const icon = ( 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ) 12 | 13 | export default icon 14 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/Header.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, waitFor } from '@testing-library/react' 2 | 3 | import { render } from '../test-utils' 4 | import Header from './Header' 5 | 6 | // Axios is bundled as ESM module which is not directly compatible with Jest 7 | // https://jestjs.io/docs/ecmascript-modules 8 | jest.mock('axios', () => ({ 9 | get: jest.fn(), 10 | post: jest.fn(), 11 | delete: jest.fn(), 12 | })) 13 | 14 | describe('
', () => { 15 | it('Renders Header component', async () => { 16 | render(
) 17 | 18 | await waitFor(() => { 19 | expect(screen.getByText('Transaction Builder')).toBeInTheDocument() 20 | }) 21 | }) 22 | 23 | it('Shows Link to Transaction Library in Create Batch pathname', async () => { 24 | render(
) 25 | 26 | await waitFor(() => { 27 | expect(screen.getByText('Transaction Builder')).toBeInTheDocument() 28 | expect( 29 | screen.getByText('Your transaction library', { 30 | exact: false, 31 | }), 32 | ).toBeInTheDocument() 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/Icon/images/alert.tsx: -------------------------------------------------------------------------------- 1 | const Alert = { 2 | sm: ( 3 | 4 | 5 | 6 | 10 | 14 | 15 | 16 | ), 17 | md: ( 18 | 19 | 20 | 21 | 25 | 29 | 30 | 31 | ), 32 | } 33 | 34 | export default Alert 35 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/Icon/images/bookmarkFilled.tsx: -------------------------------------------------------------------------------- 1 | const BookMarkFilled = { 2 | sm: ( 3 | 4 | 5 | 12 | 13 | 14 | ), 15 | md: ( 16 | 17 | 18 | 25 | 26 | 27 | ), 28 | } 29 | 30 | export default BookMarkFilled 31 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/Icon/images/check.tsx: -------------------------------------------------------------------------------- 1 | const Check = { 2 | sm: ( 3 | 4 | 5 | 6 | 10 | 11 | 12 | ), 13 | md: ( 14 | 15 | 16 | 17 | 21 | 22 | 23 | ), 24 | } 25 | 26 | export default Check 27 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/Icon/images/code.tsx: -------------------------------------------------------------------------------- 1 | const Code = { 2 | sm: ( 3 | 4 | 5 | 6 | 11 | 12 | 13 | ), 14 | md: ( 15 | 16 | 17 | 18 | 22 | 23 | 24 | ), 25 | } 26 | 27 | export default Code 28 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/Icon/images/copy.tsx: -------------------------------------------------------------------------------- 1 | const Copy = { 2 | sm: ( 3 | 4 | 5 | 6 | 11 | 12 | 13 | ), 14 | md: ( 15 | 16 | 17 | 18 | 23 | 24 | 25 | ), 26 | } 27 | 28 | export default Copy 29 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/Icon/images/cross.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Cross = { 4 | sm: ( 5 | 6 | 7 | 8 | 12 | 13 | 14 | ), 15 | md: ( 16 | 17 | 18 | 19 | 23 | 24 | 25 | ), 26 | } 27 | 28 | export default Cross 29 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/Icon/images/edit.tsx: -------------------------------------------------------------------------------- 1 | const Edit = { 2 | sm: ( 3 | 4 | 11 | 12 | ), 13 | md: ( 14 | 15 | 22 | 23 | ), 24 | } 25 | 26 | export default Edit 27 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/Icon/images/externalLink.tsx: -------------------------------------------------------------------------------- 1 | const ExternalLink = { 2 | sm: ( 3 | 4 | 5 | 6 | 11 | 15 | 16 | 17 | ), 18 | md: ( 19 | 20 | 21 | 22 | 27 | 31 | 32 | 33 | ), 34 | } 35 | 36 | export default ExternalLink 37 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/Icon/images/info.tsx: -------------------------------------------------------------------------------- 1 | const Info = { 2 | sm: ( 3 | 4 | 5 | 6 | 7 | 12 | 17 | 18 | 19 | ), 20 | md: ( 21 | 22 | 23 | 24 | 25 | 30 | 35 | 36 | 37 | ), 38 | } 39 | 40 | export default Info 41 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/IconText/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { type Theme } from '@material-ui/core/styles' 4 | 5 | import { Icon, IconProps, IconType } from '../Icon' 6 | import Text from '../Text' 7 | 8 | const iconTextMargins = { 9 | xxs: '4px', 10 | xs: '6px', 11 | sm: '8px', 12 | md: '12px', 13 | lg: '16px', 14 | xl: '20px', 15 | xxl: '24px', 16 | } 17 | 18 | type IconMargins = keyof typeof iconTextMargins 19 | 20 | type Props = { 21 | iconType: keyof IconType 22 | iconSize: IconProps['size'] 23 | iconColor?: keyof Theme['palette'] 24 | margin?: IconMargins 25 | color?: keyof Theme['palette'] 26 | text: string 27 | className?: string 28 | iconSide?: 'left' | 'right' 29 | } 30 | 31 | const LeftIconText = styled.div<{ margin: IconMargins }>` 32 | display: flex; 33 | align-items: center; 34 | svg { 35 | margin: 0 ${({ margin }) => iconTextMargins[margin]} 0 0; 36 | } 37 | ` 38 | 39 | const RightIconText = styled.div<{ margin: IconMargins }>` 40 | display: flex; 41 | align-items: center; 42 | svg { 43 | margin: 0 0 0 ${({ margin }) => iconTextMargins[margin]}; 44 | } 45 | ` 46 | 47 | /** 48 | * The `IconText` renders an icon next to a text 49 | */ 50 | const IconText = ({ 51 | iconSize, 52 | margin = 'xs', 53 | iconType, 54 | iconColor, 55 | text, 56 | iconSide = 'left', 57 | color, 58 | className, 59 | }: Props): React.ReactElement => { 60 | return iconSide === 'right' ? ( 61 | 62 | {text} 63 | 64 | 65 | ) : ( 66 | 67 | 68 | {text} 69 | 70 | ) 71 | } 72 | 73 | export default IconText 74 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/Link/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { type Theme } from '@material-ui/core/styles' 4 | 5 | export interface Props extends React.AnchorHTMLAttributes { 6 | color?: keyof Theme['palette'] | 'white' 7 | } 8 | 9 | const StyledLink = styled.a` 10 | cursor: pointer; 11 | color: ${({ theme, color = 'primary' }) => 12 | color === 'white' ? theme.palette.common.white : theme.palette[color].dark}; 13 | font-family: ${({ theme }) => theme.typography.fontFamily}; 14 | text-decoration: underline; 15 | ` 16 | 17 | const Link: React.FC = ({ children, ...rest }): React.ReactElement => { 18 | return {children} 19 | } 20 | 21 | export default Link 22 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/Loader/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import CircularProgress from '@material-ui/core/CircularProgress' 4 | import { type Theme } from '@material-ui/core/styles' 5 | 6 | const loaderSizes = { 7 | xxs: '10px', 8 | xs: '16px', 9 | sm: '30px', 10 | md: '50px', 11 | lg: '70px', 12 | } 13 | 14 | type Props = { 15 | size: keyof typeof loaderSizes 16 | color?: keyof Theme['palette'] 17 | className?: string 18 | } 19 | 20 | const StyledCircularProgress = styled( 21 | ({ size, className }: Props): React.ReactElement => ( 22 | 23 | ), 24 | )` 25 | &.MuiCircularProgress-colorPrimary { 26 | color: ${({ theme, color = 'primary' }) => theme.palette[color].main}; 27 | } 28 | ` 29 | 30 | const Loader = ({ className, size, color }: Props): React.ReactElement => ( 31 | 32 | ) 33 | 34 | export default Loader 35 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/QuickTip.tsx: -------------------------------------------------------------------------------- 1 | import MuiAlert from '@material-ui/lab/Alert' 2 | import MuiAlertTitle from '@material-ui/lab/AlertTitle' 3 | import styled from 'styled-components' 4 | import { Icon } from './Icon' 5 | 6 | type QuickTipProps = { 7 | onClose: () => void 8 | } 9 | 10 | const QuickTip = ({ onClose }: QuickTipProps) => { 11 | return ( 12 | 13 | Quick Tip 14 | You can save your batches in your transaction library{' '} 15 | (local 16 | browser storage) or{' '} 17 | download the 18 | .json file to use them later. 19 | 20 | ) 21 | } 22 | 23 | const StyledAlert = styled(MuiAlert)` 24 | && { 25 | font-size: 14px; 26 | padding: 24px; 27 | background: ${({ theme }) => theme.palette.secondary.background}; 28 | color: ${({ theme }) => theme.palette.text.primary}; 29 | border-radius: 8px; 30 | 31 | .MuiAlert-action { 32 | align-items: flex-start; 33 | } 34 | } 35 | ` 36 | 37 | const StyledTitle = styled(MuiAlertTitle)` 38 | && { 39 | font-size: 14px; 40 | font-weight: bold; 41 | } 42 | ` 43 | 44 | const StyledIcon = styled(Icon)` 45 | position: relative; 46 | top: 3px; 47 | ` 48 | 49 | export default QuickTip 50 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/ShowMoreText.tsx: -------------------------------------------------------------------------------- 1 | import { useState, SyntheticEvent } from 'react' 2 | import Link from './Link' 3 | 4 | type ShowMoreTextProps = { 5 | children: string 6 | moreLabel?: string 7 | lessLabel?: string 8 | splitIndex?: number 9 | } 10 | 11 | const SHOW_MORE = 'Show more' 12 | const SHOW_LESS = 'Show less' 13 | 14 | export const ShowMoreText = ({ 15 | children, 16 | moreLabel = SHOW_MORE, 17 | lessLabel = SHOW_LESS, 18 | splitIndex = 50, 19 | }: ShowMoreTextProps) => { 20 | const [expanded, setExpanded] = useState(false) 21 | 22 | const handleToggle = (event: SyntheticEvent) => { 23 | event.preventDefault() 24 | setExpanded(!expanded) 25 | } 26 | 27 | if (children.length < splitIndex) { 28 | return {children} 29 | } 30 | 31 | return ( 32 | <> 33 | {expanded ? `${children} ` : `${children.substr(0, splitIndex)} ... `} 34 | {expanded ? lessLabel : moreLabel} 35 | 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/Switch.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SwitchMui from '@material-ui/core/Switch' 3 | import styled from 'styled-components' 4 | import { alpha } from '@material-ui/core/styles' 5 | 6 | const StyledSwitch = styled(({ ...rest }) => )` 7 | && { 8 | .MuiSwitch-thumb { 9 | background: ${({ theme, checked }) => (checked ? '#12FF80' : theme.palette.common.white)}; 10 | box-shadow: 11 | 1px 1px 2px rgba(0, 0, 0, 0.2), 12 | 0 0 1px rgba(0, 0, 0, 0.5); 13 | } 14 | 15 | .MuiSwitch-track { 16 | background: ${({ theme }) => theme.palette.common.black}; 17 | } 18 | 19 | .MuiIconButton-label, 20 | .MuiSwitch-colorSecondary.Mui-checked { 21 | color: ${({ checked, theme }) => (checked ? theme.palette.secondary.dark : '#B2B5B2')}; 22 | } 23 | 24 | .MuiSwitch-colorSecondary.Mui-checked:hover { 25 | background-color: ${({ theme }) => alpha(theme.palette.secondary.dark, 0.08)}; 26 | } 27 | 28 | .Mui-checked + .MuiSwitch-track { 29 | background-color: ${({ theme }) => theme.palette.secondary.dark}; 30 | } 31 | } 32 | ` 33 | 34 | type Props = { 35 | checked: boolean 36 | onChange: (checked: boolean) => void 37 | } 38 | 39 | const Switch = ({ checked, onChange }: Props): React.ReactElement => { 40 | const onSwitchChange = (_event: any, checked: boolean) => onChange(checked) 41 | 42 | return 43 | } 44 | 45 | export default Switch 46 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/Text.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Tooltip from '@material-ui/core/Tooltip' 3 | import { withStyles, alpha } from '@material-ui/core/styles' 4 | import { type Theme } from '@material-ui/core/styles' 5 | 6 | import { Typography, TypographyProps } from '@material-ui/core' 7 | import styled from 'styled-components' 8 | 9 | type Props = { 10 | children: React.ReactNode 11 | tooltip?: string 12 | color?: keyof Theme['palette'] | 'white' 13 | className?: string 14 | component?: 'span' | 'p' 15 | strong?: boolean 16 | center?: boolean 17 | } 18 | 19 | const StyledTooltip = withStyles(theme => ({ 20 | tooltip: { 21 | backgroundColor: theme.palette.common.white, 22 | color: theme.palette.text.primary, 23 | boxShadow: `0px 0px 10px ${alpha('#28363D', 0.2)}`, 24 | }, 25 | arrow: { 26 | color: theme.palette.common.white, 27 | boxShadow: 'transparent', 28 | }, 29 | }))(Tooltip) 30 | 31 | const StyledTypography = styled(Typography)<{ $color?: keyof Theme['palette'] | 'white' } & Props>` 32 | color: ${({ $color, theme }) => 33 | $color 34 | ? $color === 'white' 35 | ? theme.palette.common.white 36 | : theme.palette[$color].main 37 | : theme.palette.text.primary}; 38 | 39 | ${({ center }) => center && 'text-align: center;'} 40 | 41 | ${({ strong }) => strong && `font-weight: bold;`} 42 | ` 43 | 44 | const Text = ({ 45 | children, 46 | component = 'p', 47 | tooltip, 48 | color, 49 | ...rest 50 | }: Props & Omit): React.ReactElement => { 51 | const TextElement = ( 52 | 53 | {children} 54 | 55 | ) 56 | 57 | return tooltip === undefined ? ( 58 | TextElement 59 | ) : ( 60 | 61 | {TextElement} 62 | 63 | ) 64 | } 65 | 66 | export default Text 67 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/VirtualizedList.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useEffect, useState } from 'react' 2 | import { Virtuoso } from 'react-virtuoso' 3 | import styled from 'styled-components' 4 | 5 | type VirtualizedListProps = { 6 | innerRef: any 7 | items: T[] 8 | renderItem: (item: T, index: number) => React.ReactNode 9 | } 10 | 11 | const VirtualizedList = ({ 12 | innerRef, 13 | items, 14 | renderItem, 15 | }: VirtualizedListProps) => { 16 | return ( 17 | renderItem(item, index)} 22 | components={{ 23 | Item: HeightPreservingItem, 24 | }} 25 | totalCount={items.length} 26 | overscan={100} 27 | /> 28 | ) 29 | } 30 | 31 | const HeightPreservingItem: React.FC = memo(({ children, ...props }: any) => { 32 | const [size, setSize] = useState(0) 33 | const knownSize = props['data-known-size'] 34 | 35 | useEffect(() => { 36 | setSize(prevSize => { 37 | return knownSize === 0 ? prevSize : knownSize 38 | }) 39 | }, [knownSize]) 40 | 41 | return ( 42 | 43 | {children} 44 | 45 | ) 46 | }) 47 | 48 | const HeightPreservingContainer = styled.div<{ size: number }>` 49 | --child-height: ${props => `${props.size}px`}; 50 | &:empty { 51 | min-height: calc(var(--child-height)); 52 | box-sizing: border-box; 53 | } 54 | ` 55 | 56 | export default VirtualizedList 57 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/Wrapper/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | function Wrapper({ children, centered }: { children: React.ReactNode; centered?: boolean }) { 5 | return ( 6 | 7 |
{children}
8 |
9 | ) 10 | } 11 | 12 | const StyledWrapper = styled.main<{ centered?: boolean }>` 13 | width: 100%; 14 | min-height: 100%; 15 | display: flex; 16 | background: ${({ theme }) => theme.palette.background.main}; 17 | color: ${({ theme }) => theme.palette.text.primary}; 18 | 19 | > section { 20 | width: 100%; 21 | padding: 120px 4rem 48px; 22 | box-sizing: border-box; 23 | margin: 0 auto; 24 | max-width: ${({ centered }) => (centered ? '1000px' : '1500px')}; 25 | } 26 | ` 27 | 28 | export default Wrapper 29 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/buttons/ButtonLink/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { Icon, IconProps, IconType } from '../../Icon' 4 | import Text from '../../Text' 5 | import { TypographyProps } from '@material-ui/core' 6 | import { type Theme } from '@material-ui/core/styles' 7 | 8 | export interface Props extends React.ComponentPropsWithoutRef<'button'> { 9 | iconType?: keyof IconType 10 | iconSize?: IconProps['size'] 11 | textSize?: TypographyProps['variant'] 12 | color: keyof Theme['palette'] 13 | children?: React.ReactNode 14 | } 15 | 16 | const StyledButtonLink = styled.button` 17 | background: transparent; 18 | border: none; 19 | text-decoration: none; 20 | cursor: pointer; 21 | color: ${({ theme, color }) => theme.palette[color].main}; 22 | font-family: ${({ theme }) => theme.typography.fontFamily}; 23 | display: flex; 24 | align-items: center; 25 | 26 | :focus { 27 | outline: none; 28 | } 29 | ` 30 | 31 | const StyledText = styled(Text)` 32 | margin: 0 4px; 33 | ` 34 | 35 | const ButtonLink = ({ 36 | iconType, 37 | iconSize = 'md', 38 | children, 39 | textSize = 'body1', 40 | ...rest 41 | }: Props): React.ReactElement => { 42 | return ( 43 | 44 | {iconType && } 45 | 46 | {children} 47 | 48 | 49 | ) 50 | } 51 | 52 | export default ButtonLink 53 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/buttons/CopyToClipboardBtn/copyTextToClipboard.ts: -------------------------------------------------------------------------------- 1 | const copyTextToClipboard = (text: string): void => { 2 | const listener = (e: ClipboardEvent): void => { 3 | e.preventDefault() 4 | if (e.clipboardData) { 5 | e.clipboardData.setData('text/plain', text) 6 | } 7 | } 8 | 9 | const range = document.createRange() 10 | 11 | const documentSelection = document.getSelection() 12 | if (!documentSelection) { 13 | return 14 | } 15 | 16 | range.selectNodeContents(document.body) 17 | documentSelection.addRange(range) 18 | document.addEventListener('copy', listener) 19 | document.execCommand('copy') 20 | document.removeEventListener('copy', listener) 21 | documentSelection.removeAllRanges() 22 | } 23 | 24 | export default copyTextToClipboard 25 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/buttons/ExplorerButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { Icon } from '../../Icon' 4 | import { ExplorerInfo } from '../../ETHHashInfo' 5 | 6 | const StyledLink = styled.a` 7 | background: none; 8 | color: inherit; 9 | border: none; 10 | padding: 0; 11 | font: inherit; 12 | cursor: pointer; 13 | border-radius: 50%; 14 | transition: background-color 0.2s ease-in-out; 15 | outline-color: transparent; 16 | height: 24px; 17 | width: 24px; 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | :hover { 22 | background-color: #f0efee; 23 | } 24 | ` 25 | 26 | type Props = { 27 | className?: string 28 | explorerUrl: ExplorerInfo 29 | } 30 | 31 | const ExplorerButton = ({ className, explorerUrl }: Props): React.ReactElement => { 32 | const { url, alt } = explorerUrl() 33 | const onClick = (event: React.MouseEvent): void => { 34 | event.stopPropagation() 35 | } 36 | 37 | const onKeyDown = (event: React.KeyboardEvent): void => { 38 | // prevents event from bubbling when `Enter` is pressed 39 | if (event.keyCode === 13) { 40 | event.stopPropagation() 41 | } 42 | } 43 | 44 | return ( 45 | 54 | 55 | 56 | ) 57 | } 58 | 59 | export default ExplorerButton 60 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/buttons/Identicon/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import makeBlockie from 'ethereum-blockies-base64' 4 | import styled from 'styled-components' 5 | 6 | export const identiconSizes = { 7 | xs: '10px', 8 | sm: '16px', 9 | md: '32px', 10 | lg: '40px', 11 | xl: '48px', 12 | xxl: '60px', 13 | } 14 | 15 | type Props = { 16 | address: string 17 | size: keyof typeof identiconSizes 18 | } 19 | 20 | const StyledImg = styled.img<{ size: keyof typeof identiconSizes }>` 21 | height: ${({ size }) => identiconSizes[size]}; 22 | width: ${({ size }) => identiconSizes[size]}; 23 | border-radius: 50%; 24 | ` 25 | 26 | const Identicon = ({ size = 'md', address, ...rest }: Props): React.ReactElement => { 27 | const iconSrc = React.useMemo(() => makeBlockie(address), [address]) 28 | 29 | return 30 | } 31 | 32 | export default Identicon 33 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/forms/fields/AddressContractField.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react' 2 | import AddressInput from './AddressInput' 3 | 4 | const AddressContractField = ({ 5 | id, 6 | name, 7 | value, 8 | onChange, 9 | label, 10 | error, 11 | getAddressFromDomain, 12 | networkPrefix, 13 | onBlur, 14 | }: any): ReactElement => { 15 | return ( 16 | 32 | ) 33 | } 34 | 35 | export default AddressContractField 36 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/forms/fields/SelectContractField.tsx: -------------------------------------------------------------------------------- 1 | import Autocomplete from '@mui/material/Autocomplete' 2 | import { SelectItem } from '@gnosis.pm/safe-react-components/dist/inputs/Select' 3 | import { type SyntheticEvent, useCallback, useMemo } from 'react' 4 | import TextFieldInput from './TextFieldInput' 5 | 6 | type SelectContractFieldTypes = { 7 | options: SelectItem[] 8 | onChange: (id: string) => void 9 | value: string 10 | label: string 11 | name: string 12 | id: string 13 | } 14 | 15 | const SelectContractField = ({ 16 | value, 17 | onChange, 18 | options, 19 | label, 20 | name, 21 | id, 22 | }: SelectContractFieldTypes) => { 23 | const selectedValue = useMemo(() => options.find(opt => opt.id === value), [options, value]) 24 | 25 | const onValueChange = useCallback( 26 | (e: SyntheticEvent, value: SelectItem | null) => { 27 | if (value) { 28 | onChange(value.id) 29 | } 30 | }, 31 | [onChange], 32 | ) 33 | 34 | return ( 35 | ( 43 | 52 | )} 53 | /> 54 | ) 55 | } 56 | 57 | export default SelectContractField 58 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/forms/fields/TextContractField.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import TextFieldInput, { TextFieldInputProps } from './TextFieldInput' 3 | 4 | type TextContractFieldTypes = TextFieldInputProps & { 5 | networkPrefix?: undefined | string 6 | getAddressFromDomain?: () => {} 7 | } 8 | 9 | const TextContractField = ({ 10 | networkPrefix, 11 | getAddressFromDomain, 12 | ...props 13 | }: TextContractFieldTypes) => { 14 | return 15 | } 16 | 17 | export default TextContractField 18 | 19 | const StyledTextField = styled(TextFieldInput)` 20 | && { 21 | textarea { 22 | &.MuiInputBase-input { 23 | padding: 0; 24 | } 25 | } 26 | } 27 | ` 28 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/forms/fields/TextFieldInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react' 2 | import TextFieldMui, { TextFieldProps } from '@material-ui/core/TextField' 3 | import styled from 'styled-components' 4 | import { errorStyles, inputLabelStyles, inputStyles } from './styles' 5 | 6 | export type TextFieldInputProps = { 7 | id?: string 8 | name: string 9 | label: string 10 | error?: string 11 | helperText?: string | undefined 12 | hiddenLabel?: boolean | undefined 13 | showErrorsInTheLabel?: boolean | undefined 14 | } & Omit 15 | 16 | function TextFieldInput({ 17 | id, 18 | name, 19 | label, 20 | error = '', 21 | helperText, 22 | value, 23 | hiddenLabel, 24 | showErrorsInTheLabel, 25 | ...rest 26 | }: TextFieldInputProps): ReactElement { 27 | const hasError = !!error 28 | 29 | return ( 30 | 46 | ) 47 | } 48 | 49 | const TextField = styled((props: TextFieldProps) => )` 50 | && { 51 | ${inputLabelStyles} 52 | ${inputStyles} 53 | ${errorStyles} 54 | } 55 | ` 56 | 57 | export default TextFieldInput 58 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/forms/fields/TextareaContractField.tsx: -------------------------------------------------------------------------------- 1 | import TextContractField from './TextContractField' 2 | import { TextFieldInputProps } from './TextFieldInput' 3 | 4 | const DEFAULT_ROWS = 4 5 | 6 | const TextareaContractField = (props: TextFieldInputProps) => { 7 | return 8 | } 9 | 10 | export default TextareaContractField 11 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/forms/validations/basicSolidityValidation.ts: -------------------------------------------------------------------------------- 1 | import { ValidateResult } from 'react-hook-form' 2 | import abiCoder, { AbiCoder } from 'web3-eth-abi' 3 | 4 | import { parseInputValue } from '../../../utils' 5 | import { NON_SOLIDITY_TYPES } from '../fields/fields' 6 | import { isEthersError } from '../../../typings/errors' 7 | 8 | const basicSolidityValidation = (value: string, fieldType: string): ValidateResult => { 9 | const isSolidityFieldType = !NON_SOLIDITY_TYPES.includes(fieldType) 10 | if (isSolidityFieldType) { 11 | try { 12 | const cleanValue = parseInputValue(fieldType, value) 13 | const abi = abiCoder as unknown // a bug in the web3-eth-abi types 14 | ;(abi as AbiCoder).encodeParameter(fieldType, cleanValue) 15 | } catch (error: unknown) { 16 | let errorMessage = error?.toString() 17 | 18 | const errorFromEthers = isEthersError(error) 19 | if (errorFromEthers) { 20 | if (error.reason.toLowerCase().includes('overflow')) { 21 | return 'Overflow error. Please encode all numbers as strings' 22 | } 23 | errorMessage = error.reason 24 | } 25 | 26 | return `format error. details: ${errorMessage}` 27 | } 28 | } 29 | } 30 | 31 | export default basicSolidityValidation 32 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/forms/validations/validateAddressField.ts: -------------------------------------------------------------------------------- 1 | import { ValidateResult } from 'react-hook-form' 2 | 3 | import { isValidAddress } from '../../../utils' 4 | 5 | const validateAddressField = (value: string): ValidateResult => { 6 | if (!isValidAddress(value)) { 7 | return 'Invalid address' 8 | } 9 | } 10 | 11 | export default validateAddressField 12 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/forms/validations/validateAmountField.ts: -------------------------------------------------------------------------------- 1 | import { ValidateResult } from 'react-hook-form' 2 | import { toWei } from 'web3-utils' 3 | 4 | import { isInputValueValid } from '../../../utils' 5 | 6 | const INVALID_AMOUNT_ERROR = 'Invalid amount value' 7 | 8 | const validateAmountField = (value: string): ValidateResult => { 9 | if (!isInputValueValid(value)) { 10 | return INVALID_AMOUNT_ERROR 11 | } 12 | 13 | // should be a valid amount in wei 14 | try { 15 | toWei(value) 16 | } catch (error) { 17 | return INVALID_AMOUNT_ERROR 18 | } 19 | } 20 | 21 | export default validateAmountField 22 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/forms/validations/validateBooleanField.ts: -------------------------------------------------------------------------------- 1 | import { ValidateResult } from 'react-hook-form' 2 | 3 | const validateBooleanField = (value: string): ValidateResult => { 4 | const cleanValue = value?.toLowerCase() 5 | 6 | const isValidBoolean = cleanValue === 'true' || cleanValue === 'false' 7 | 8 | if (!isValidBoolean) { 9 | return 'Invalid boolean value' 10 | } 11 | } 12 | 13 | export default validateBooleanField 14 | -------------------------------------------------------------------------------- /apps/tx-builder/src/components/forms/validations/validateHexEncodedDataField.ts: -------------------------------------------------------------------------------- 1 | import { isHexStrict } from 'web3-utils' 2 | import { ValidateResult } from 'react-hook-form' 3 | 4 | import { getCustomDataError } from '../../../utils' 5 | 6 | const validateHexEncodedDataField = (value: string): ValidateResult => { 7 | if (!isHexStrict(value)) { 8 | return getCustomDataError(value) 9 | } 10 | } 11 | 12 | export default validateHexEncodedDataField 13 | -------------------------------------------------------------------------------- /apps/tx-builder/src/hardhat/deploy/deploy_basic_test_contract.js: -------------------------------------------------------------------------------- 1 | const deploy = async function (hre) { 2 | const { deployments, getNamedAccounts } = hre 3 | const { deployer } = await getNamedAccounts() 4 | const { deploy } = deployments 5 | 6 | await deploy('BasicTypesTestContract', { 7 | from: deployer, 8 | args: [], 9 | log: true, 10 | }) 11 | } 12 | 13 | module.exports = deploy 14 | -------------------------------------------------------------------------------- /apps/tx-builder/src/hardhat/deploy/deploy_matrix_test_contract.js: -------------------------------------------------------------------------------- 1 | const deploy = async function (hre) { 2 | const { deployments, getNamedAccounts } = hre 3 | const { deployer } = await getNamedAccounts() 4 | const { deploy } = deployments 5 | 6 | await deploy('MatrixTypesTestContract', { 7 | from: deployer, 8 | args: [], 9 | log: true, 10 | }) 11 | } 12 | 13 | module.exports = deploy 14 | -------------------------------------------------------------------------------- /apps/tx-builder/src/hardhat/networks.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | 3 | const { INFURA_KEY, PRIVATE_KEY } = process.env 4 | 5 | const sharedConfig = { 6 | accounts: [`0x${PRIVATE_KEY}`], 7 | } 8 | 9 | const MAINNET_CONFIG = { 10 | ...sharedConfig, 11 | url: `https://mainnet.infura.io/v3/${INFURA_KEY}`, 12 | } 13 | 14 | const GNOSIS_CHAIN_CONFIG = { 15 | ...sharedConfig, 16 | url: 'https://xdai.poanetwork.dev', 17 | } 18 | 19 | const POLYGON_CONFIG = { 20 | ...sharedConfig, 21 | url: `https://polygon-mainnet.infura.io/v3/${INFURA_KEY}`, 22 | } 23 | 24 | const BSC_CONFIG = { 25 | ...sharedConfig, 26 | url: 'https://bsc-dataseed.binance.org/', 27 | } 28 | 29 | const ARBITRUM_CONFIG = { 30 | ...sharedConfig, 31 | url: 'https://arb1.arbitrum.io/rpc', 32 | } 33 | 34 | const AVALANCHE_CONFIG = { 35 | ...sharedConfig, 36 | url: 'https://api.avax.network/ext/bc/C/rpc', 37 | } 38 | 39 | const RINKEBY_CONFIG = { 40 | ...sharedConfig, 41 | url: `https://rinkeby.infura.io/v3/${INFURA_KEY}`, 42 | } 43 | 44 | const GOERLI_CONFIG = { 45 | ...sharedConfig, 46 | url: `https://goerli.infura.io/v3/${INFURA_KEY}`, 47 | } 48 | 49 | const VOLTA_CONFIG = { 50 | ...sharedConfig, 51 | url: 'https://volta-rpc.energyweb.org', 52 | } 53 | 54 | const networks = { 55 | hardhat: {}, 56 | localhost: {}, 57 | 58 | mainnet: MAINNET_CONFIG, 59 | xdai: GNOSIS_CHAIN_CONFIG, 60 | polygon: POLYGON_CONFIG, 61 | bsc: BSC_CONFIG, 62 | arbitrum: ARBITRUM_CONFIG, 63 | avalanche: AVALANCHE_CONFIG, 64 | 65 | // testnets 66 | rinkeby: RINKEBY_CONFIG, 67 | goerli: GOERLI_CONFIG, 68 | volta: VOLTA_CONFIG, 69 | } 70 | 71 | module.exports = networks 72 | -------------------------------------------------------------------------------- /apps/tx-builder/src/hardhat/tasks/deploy_contracts.js: -------------------------------------------------------------------------------- 1 | require('hardhat-deploy') 2 | require('@nomiclabs/hardhat-ethers') 3 | const { task } = require('hardhat/config') 4 | 5 | task('deploy-test-contracts', 'Deploys and verifies Test contracts').setAction(async (_, hre) => { 6 | await hre.run('deploy') 7 | await hre.run('sourcify') 8 | await hre.run('etherscan-verify', { forceLicense: true, license: 'LGPL-3.0' }) 9 | }) 10 | 11 | module.exports = {} 12 | -------------------------------------------------------------------------------- /apps/tx-builder/src/hardhat/tasks/read_method.js: -------------------------------------------------------------------------------- 1 | const { task } = require('hardhat/config') 2 | require('@nomiclabs/hardhat-ethers') 3 | 4 | task('read-method', 'Read a method from a test contract') 5 | .addParam('address', "The contract's address") 6 | .addParam('method', "The contract's method") 7 | .setAction(async ({ address, method }, hre) => { 8 | await hre.run('compile') 9 | console.log('\n') 10 | 11 | console.log('contract address: ', address) 12 | console.log('reading method: ', method) 13 | console.log('network: ', hre.network.name, '\n') 14 | 15 | const contracts = await hre.artifacts.getAllFullyQualifiedNames() 16 | 17 | await Promise.all( 18 | contracts.map(async contract => { 19 | const artifact = await hre.artifacts.readArtifact(contract) 20 | 21 | const Contract = await hre.ethers.getContractFactory(artifact.contractName) 22 | 23 | const contractMethod = (await Contract.attach(address))[method] 24 | 25 | if (contractMethod) { 26 | console.log(method, 'read method found on', artifact.contractName) 27 | 28 | const value = await contractMethod() 29 | 30 | console.log('value: ', value, '\n') 31 | } 32 | }), 33 | ) 34 | }) 35 | 36 | module.exports = {} 37 | -------------------------------------------------------------------------------- /apps/tx-builder/src/hooks/useAbi.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { AxiosError } from 'axios' 3 | import { FETCH_STATUS, isValidAddress } from '../utils' 4 | import { useNetwork } from '../store' 5 | 6 | const isAxiosError = (e: unknown): e is AxiosError => 7 | e != null && typeof e === 'object' && 'request' in e 8 | 9 | const useAbi = (address: string) => { 10 | const [abi, setAbi] = useState('') 11 | const { interfaceRepo } = useNetwork() 12 | const [abiStatus, setAbiStatus] = useState(FETCH_STATUS.NOT_ASKED) 13 | 14 | useEffect(() => { 15 | const loadContract = async (address: string) => { 16 | if (!isValidAddress(address) || !interfaceRepo) { 17 | return 18 | } 19 | 20 | setAbi('') 21 | setAbiStatus(FETCH_STATUS.LOADING) 22 | try { 23 | const abiResponse = await interfaceRepo.loadAbi(address) 24 | 25 | if (abiResponse) { 26 | setAbi(abiResponse) 27 | } 28 | setAbiStatus(FETCH_STATUS.SUCCESS) 29 | } catch (e) { 30 | if (isAxiosError(e) && e.request.status === 404) { 31 | // Handle the case where the request is successful but the ABI is not found 32 | setAbiStatus(FETCH_STATUS.SUCCESS) 33 | } else { 34 | setAbiStatus(FETCH_STATUS.ERROR) 35 | console.error(e) 36 | } 37 | } 38 | } 39 | 40 | loadContract(address) 41 | }, [address, interfaceRepo]) 42 | 43 | return { abi, abiStatus, setAbi } 44 | } 45 | 46 | export { useAbi } 47 | -------------------------------------------------------------------------------- /apps/tx-builder/src/hooks/useAsync.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react' 2 | 3 | export type AsyncResult = [result: T | undefined, error: Error | undefined, loading: boolean] 4 | 5 | const useAsync = ( 6 | asyncCall: () => Promise | undefined, 7 | dependencies: unknown[], 8 | clearData = true, 9 | ): AsyncResult => { 10 | const [data, setData] = useState() 11 | const [error, setError] = useState() 12 | const [loading, setLoading] = useState(false) 13 | 14 | // eslint-disable-next-line react-hooks/exhaustive-deps 15 | const callback = useCallback(asyncCall, dependencies) 16 | 17 | useEffect(() => { 18 | setError(undefined) 19 | 20 | const promise = callback() 21 | 22 | // Not a promise, exit early 23 | if (!promise) { 24 | setData(undefined) 25 | setLoading(false) 26 | return 27 | } 28 | 29 | let isCurrent = true 30 | clearData && setData(undefined) 31 | setLoading(true) 32 | 33 | promise 34 | .then((val: T) => { 35 | isCurrent && setData(val) 36 | }) 37 | .catch(err => { 38 | isCurrent && setError(err as Error) 39 | }) 40 | .finally(() => { 41 | isCurrent && setLoading(false) 42 | }) 43 | 44 | return () => { 45 | isCurrent = false 46 | } 47 | }, [callback, clearData]) 48 | 49 | return [data, error, loading] 50 | } 51 | 52 | export default useAsync 53 | -------------------------------------------------------------------------------- /apps/tx-builder/src/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | const useDebounce = (value: T, delay: number): T => { 4 | const [debouncedValue, setDebouncedValue] = useState(value) 5 | 6 | useEffect(() => { 7 | const timer = setTimeout(() => setDebouncedValue(value), delay) 8 | return () => clearTimeout(timer) 9 | }, [value, delay]) 10 | 11 | return debouncedValue 12 | } 13 | 14 | export default useDebounce 15 | -------------------------------------------------------------------------------- /apps/tx-builder/src/hooks/useElementHeight/useElementHeight.tsx: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect, useRef, useState } from 'react' 2 | 3 | type useElementHeightTypes = { 4 | height: number | undefined 5 | elementRef: RefObject 6 | } 7 | 8 | const useElementHeight = (): useElementHeightTypes => { 9 | const elementRef = useRef(null) 10 | 11 | const [height, setHeight] = useState() 12 | 13 | useEffect(() => { 14 | // hack to calculate properly the height of a container 15 | setTimeout(() => { 16 | const height = elementRef?.current?.clientHeight 17 | setHeight(height) 18 | }, 10) 19 | }, [elementRef]) 20 | 21 | return { height, elementRef } 22 | } 23 | 24 | export default useElementHeight 25 | -------------------------------------------------------------------------------- /apps/tx-builder/src/hooks/useModal/useModal.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | 3 | const useModal = (initialValue = false) => { 4 | const [open, setOpen] = useState(initialValue) 5 | 6 | const openModal = useCallback(() => { 7 | setOpen(true) 8 | }, []) 9 | 10 | const closeModal = useCallback(() => { 11 | setOpen(false) 12 | }, []) 13 | 14 | const toggleModal = useCallback(() => { 15 | setOpen(open => !open) 16 | }, []) 17 | 18 | return { 19 | open, 20 | setOpen, 21 | 22 | openModal, 23 | closeModal, 24 | toggleModal, 25 | } 26 | } 27 | 28 | export default useModal 29 | -------------------------------------------------------------------------------- /apps/tx-builder/src/hooks/useThrottle.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useCallback } from 'react' 2 | 3 | const DEFAULT_DELAY = 650 4 | 5 | type ThrottleType = (callback: Function, delay?: number) => void 6 | 7 | const useThrottle: () => ThrottleType = () => { 8 | const timerRefId = useRef | undefined>() 9 | 10 | const throttle = useCallback((callback, delay = DEFAULT_DELAY) => { 11 | // If setTimeout is already scheduled, clearTimeout 12 | if (timerRefId.current) { 13 | clearTimeout(timerRefId.current) 14 | } 15 | 16 | // Schedule the exec after a delay 17 | timerRefId.current = setTimeout(function () { 18 | timerRefId.current = undefined 19 | return callback() 20 | }, delay) 21 | }, []) 22 | 23 | return throttle 24 | } 25 | 26 | export default useThrottle 27 | -------------------------------------------------------------------------------- /apps/tx-builder/src/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom' 2 | import { SafeProvider } from '@safe-global/safe-apps-react-sdk' 3 | import { BrowserRouter } from 'react-router-dom' 4 | 5 | import * as serviceWorker from './serviceWorker' 6 | 7 | import GlobalStyles from './global' 8 | import App from './App' 9 | import StoreProvider from './store' 10 | import SafeThemeProvider from './theme/SafeThemeProvider' 11 | import { ThemeProvider } from 'styled-components' 12 | 13 | ReactDOM.render( 14 | <> 15 | 16 | 17 | {theme => ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | )} 28 | 29 | , 30 | document.getElementById('root'), 31 | ) 32 | 33 | // If you want your app to work offline and load faster, you can change 34 | // unregister() to register() below. Note this comes with some pitfalls. 35 | // Learn more about service workers: https://bit.ly/CRA-PWA 36 | serviceWorker.unregister() 37 | -------------------------------------------------------------------------------- /apps/tx-builder/src/lib/analytics.ts: -------------------------------------------------------------------------------- 1 | const SAFE_APPS_ANALYTICS_CATEGORY = 'safe-apps-analytics' 2 | 3 | export const trackSafeAppEvent = (action: string, label?: string) => { 4 | window.parent.postMessage( 5 | { 6 | category: SAFE_APPS_ANALYTICS_CATEGORY, 7 | action, 8 | label, 9 | safeAppName: 'Transaction Builder', 10 | }, 11 | '*', 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /apps/tx-builder/src/lib/batches/index.ts: -------------------------------------------------------------------------------- 1 | import StorageManager from '../../lib/storage' 2 | 3 | const getExportFileName = () => { 4 | const today = new Date().toISOString().slice(0, 10) 5 | return `tx-builder-batches-${today}.json` 6 | } 7 | 8 | export const exportBatches = async () => { 9 | const batchesRecords = await StorageManager.getBatches() 10 | const data = JSON.stringify({ data: batchesRecords }) 11 | 12 | const blob = new Blob([data], { type: 'application/json' }) 13 | 14 | if ( 15 | navigator.userAgent.includes('Firefox') || 16 | (navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('Chrome')) 17 | ) { 18 | const blobURL = URL.createObjectURL(blob) 19 | 20 | return window.open(blobURL) 21 | } 22 | 23 | const link = document.createElement('a') 24 | 25 | link.download = getExportFileName() 26 | link.href = window.URL.createObjectURL(blob) 27 | link.dataset.downloadurl = ['text/json', link.download, link.href].join(':') 28 | link.dispatchEvent(new MouseEvent('click')) 29 | } 30 | -------------------------------------------------------------------------------- /apps/tx-builder/src/lib/checksum.ts: -------------------------------------------------------------------------------- 1 | import web3 from 'web3' 2 | import { BatchFile } from '../typings/models' 3 | 4 | // JSON spec does not allow undefined so stringify removes the prop 5 | // That's a problem for calculating the checksum back so this function avoid the issue 6 | export const stringifyReplacer = (_: string, value: any) => (value === undefined ? null : value) 7 | 8 | const serializeJSONObject = (json: any): string => { 9 | if (Array.isArray(json)) { 10 | return `[${json.map(el => serializeJSONObject(el)).join(',')}]` 11 | } 12 | 13 | if (typeof json === 'object' && json !== null) { 14 | let acc = '' 15 | const keys = Object.keys(json).sort() 16 | acc += `{${JSON.stringify(keys, stringifyReplacer)}` 17 | 18 | for (let i = 0; i < keys.length; i++) { 19 | acc += `${serializeJSONObject(json[keys[i]])},` 20 | } 21 | 22 | return `${acc}}` 23 | } 24 | 25 | return `${JSON.stringify(json, stringifyReplacer)}` 26 | } 27 | 28 | const calculateChecksum = (batchFile: BatchFile): string | undefined => { 29 | const serialized = serializeJSONObject({ 30 | ...batchFile, 31 | meta: { ...batchFile.meta, name: null }, 32 | }) 33 | const sha = web3.utils.sha3(serialized) 34 | 35 | return sha || undefined 36 | } 37 | 38 | export const addChecksum = (batchFile: BatchFile): BatchFile => { 39 | return { 40 | ...batchFile, 41 | meta: { 42 | ...batchFile.meta, 43 | checksum: calculateChecksum(batchFile), 44 | }, 45 | } 46 | } 47 | 48 | export const validateChecksum = (batchFile: BatchFile): boolean => { 49 | const targetObj = { ...batchFile } 50 | const checksum = targetObj.meta.checksum 51 | delete targetObj.meta.checksum 52 | 53 | return calculateChecksum(targetObj) === checksum 54 | } 55 | -------------------------------------------------------------------------------- /apps/tx-builder/src/lib/local-storage/local.ts: -------------------------------------------------------------------------------- 1 | import Storage from './Storage' 2 | 3 | const local = new Storage(typeof window !== 'undefined' ? window.localStorage : undefined) 4 | 5 | export const localItem = (key: string) => ({ 6 | get: () => local.getItem(key), 7 | set: (value: T) => local.setItem(key, value), 8 | remove: () => local.removeItem(key), 9 | }) 10 | 11 | export default local 12 | -------------------------------------------------------------------------------- /apps/tx-builder/src/lib/simulation/multisend.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | import { BaseTransaction } from '@safe-global/safe-apps-sdk' 3 | import { getMultiSendCallOnlyDeployment } from '@safe-global/safe-deployments' 4 | 5 | const getMultiSendCallOnlyAddress = (chainId: string): string => { 6 | const deployment = getMultiSendCallOnlyDeployment({ network: chainId }) 7 | 8 | if (!deployment) { 9 | throw new Error('MultiSendCallOnly deployment not found') 10 | } 11 | 12 | return deployment.networkAddresses[chainId] 13 | } 14 | 15 | const encodeMultiSendCall = (txs: BaseTransaction[]): string => { 16 | const web3 = new Web3() 17 | 18 | const joinedTxs = txs 19 | .map(tx => 20 | [ 21 | web3.eth.abi.encodeParameter('uint8', 0).slice(-2), 22 | web3.eth.abi.encodeParameter('address', tx.to).slice(-40), 23 | // if you pass wei as number, it will overflow 24 | web3.eth.abi.encodeParameter('uint256', tx.value.toString()).slice(-64), 25 | web3.eth.abi.encodeParameter('uint256', web3.utils.hexToBytes(tx.data).length).slice(-64), 26 | tx.data.replace(/^0x/, ''), 27 | ].join(''), 28 | ) 29 | .join('') 30 | 31 | const encodedMultiSendCallData = web3.eth.abi.encodeFunctionCall( 32 | { 33 | name: 'multiSend', 34 | type: 'function', 35 | inputs: [ 36 | { 37 | type: 'bytes', 38 | name: 'transactions', 39 | }, 40 | ], 41 | }, 42 | [`0x${joinedTxs}`], 43 | ) 44 | 45 | return encodedMultiSendCallData 46 | } 47 | 48 | export { encodeMultiSendCall, getMultiSendCallOnlyAddress } 49 | -------------------------------------------------------------------------------- /apps/tx-builder/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/tx-builder/src/routes/routes.ts: -------------------------------------------------------------------------------- 1 | export const HOME_PATH = '/' 2 | 3 | export const CREATE_BATCH_PATH = HOME_PATH 4 | export const BATCH_PATH = '/batch' 5 | export const SAVE_BATCH_PATH = BATCH_PATH 6 | export const EDIT_BATCH_PATH = `${BATCH_PATH}/:batchId` 7 | 8 | export const REVIEW_AND_CONFIRM_PATH = '/review-and-confirm' 9 | 10 | export const TRANSACTION_LIBRARY_PATH = '/transaction-library' 11 | 12 | export const getEditBatchUrl = (batchId: string | number) => { 13 | return `${BATCH_PATH}/${batchId}` 14 | } 15 | -------------------------------------------------------------------------------- /apps/tx-builder/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect' 6 | import { ChainInfo, SafeInfo } from '@safe-global/safe-apps-sdk' 7 | import { configure } from '@testing-library/react' 8 | 9 | // Jest is not able to use this function from node, which is used at viem v1.3.0 10 | // We need to import it manually 11 | import { TextEncoder } from 'util' 12 | 13 | global.TextEncoder = TextEncoder 14 | // END 15 | 16 | configure({ testIdAttribute: 'id' }) 17 | 18 | const TEST_SAFE_MOCK: SafeInfo = { 19 | safeAddress: '0x57CB13cbef735FbDD65f5f2866638c546464E45F', 20 | chainId: 4, 21 | isReadOnly: false, 22 | owners: ['0x680cde08860141F9D223cE4E620B10Cd6741037E'], 23 | threshold: 2, 24 | } 25 | 26 | const CHAIN_INFO_MOCK: ChainInfo = { 27 | chainId: '4', 28 | chainName: 'Rinkeby', 29 | nativeCurrency: { 30 | decimals: 18, 31 | logoUri: 'https://test/currency_logo.png', 32 | name: 'Ether', 33 | symbol: 'ETH', 34 | }, 35 | blockExplorerUriTemplate: { 36 | address: 'https://rinkeby.etherscan.io/address/{address}', 37 | txHash: 'https://rinkeby.etherscan.io/tx/{transactionHash}', 38 | api: 'https://api.etherscan.io/api', 39 | }, 40 | shortName: 'rin', 41 | } 42 | 43 | const SDK_MOCK = { 44 | txs: { 45 | send: () => {}, 46 | signMessage: () => {}, 47 | }, 48 | safe: { 49 | getChainInfo: () => Promise.resolve(CHAIN_INFO_MOCK), 50 | }, 51 | eth: {}, 52 | } 53 | 54 | jest.mock('@safe-global/safe-apps-react-sdk', () => { 55 | const originalModule = jest.requireActual('@safe-global/safe-apps-react-sdk') 56 | return { 57 | ...originalModule, 58 | useSafeAppsSDK: () => ({ 59 | sdk: SDK_MOCK, 60 | safe: TEST_SAFE_MOCK, 61 | }), 62 | } 63 | }) 64 | -------------------------------------------------------------------------------- /apps/tx-builder/src/store/index.tsx: -------------------------------------------------------------------------------- 1 | import TransactionsProvider from './transactionsContext' 2 | import TransactionLibraryProvider from './transactionLibraryContext' 3 | import React from 'react' 4 | import NetworkProvider from './networkContext' 5 | 6 | const StoreProvider: React.FC = ({ children }) => { 7 | return ( 8 | 9 | 10 | {children} 11 | 12 | 13 | ) 14 | } 15 | 16 | export { useTransactions } from './transactionsContext' 17 | export { useTransactionLibrary } from './transactionLibraryContext' 18 | export { useNetwork } from './networkContext' 19 | 20 | export default StoreProvider 21 | -------------------------------------------------------------------------------- /apps/tx-builder/src/test-utils.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react' 2 | import { ThemeProvider } from 'styled-components' 3 | import { render, RenderResult } from '@testing-library/react' 4 | import { SafeProvider } from '@safe-global/safe-apps-react-sdk' 5 | import { BrowserRouter } from 'react-router-dom' 6 | import StoreProvider from './store' 7 | import SafeThemeProvider from './theme/SafeThemeProvider' 8 | 9 | const renderWithProviders = (Components: ReactElement): RenderResult => { 10 | return render( 11 | 12 | {theme => ( 13 | 14 | 15 | 16 | {Components} 17 | 18 | 19 | 20 | )} 21 | , 22 | ) 23 | } 24 | 25 | export * from '@testing-library/react' 26 | export { renderWithProviders as render } 27 | -------------------------------------------------------------------------------- /apps/tx-builder/src/theme/SafeThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState, type FC } from 'react' 2 | import { type Theme } from '@mui/material' 3 | import { ThemeProvider } from '@material-ui/core' 4 | import createSafeTheme from './safeTheme' 5 | import { getSDKVersion } from '@safe-global/safe-apps-sdk' 6 | 7 | export enum EModes { 8 | DARK = 'dark', 9 | LIGHT = 'light', 10 | } 11 | 12 | type SafeThemeProviderProps = { 13 | children: (theme: Theme) => React.ReactNode 14 | } 15 | 16 | export const ThemeModeContext = React.createContext(EModes.LIGHT) 17 | 18 | const SafeThemeProvider: FC = ({ children }) => { 19 | const [mode, setMode] = useState(EModes.LIGHT) 20 | 21 | const theme = useMemo(() => createSafeTheme(mode), [mode]) 22 | 23 | useEffect(() => { 24 | window.parent.postMessage( 25 | { 26 | id: 'tx-builder', 27 | env: { sdkVersion: getSDKVersion() }, 28 | method: 'getCurrentTheme', 29 | }, 30 | '*', 31 | ) 32 | 33 | window.addEventListener('message', function ({ data: eventData }) { 34 | if (!eventData?.data?.hasOwnProperty('darkMode')) return 35 | 36 | setMode(eventData?.data.darkMode ? EModes.DARK : EModes.LIGHT) 37 | }) 38 | }, []) 39 | 40 | return ( 41 | 42 | {children(theme)} 43 | 44 | ) 45 | } 46 | 47 | export default SafeThemeProvider 48 | -------------------------------------------------------------------------------- /apps/tx-builder/src/theme/darkPalette.ts: -------------------------------------------------------------------------------- 1 | const darkPalette = { 2 | text: { 3 | primary: '#FFFFFF', 4 | secondary: '#636669', 5 | disabled: '#636669', 6 | }, 7 | primary: { 8 | dark: '#0cb259', 9 | main: '#12FF80', 10 | light: '#A1A3A7', 11 | }, 12 | secondary: { 13 | dark: '#636669', 14 | main: '#FFFFFF', 15 | light: '#B0FFC9', 16 | background: '#1B2A22', 17 | }, 18 | border: { 19 | main: '#636669', 20 | light: '#303033', 21 | background: '#121312', 22 | }, 23 | error: { 24 | dark: '#AC2C3B', 25 | main: '#FF5F72', 26 | light: '#FFB4BD', 27 | background: '#2F2527', 28 | }, 29 | success: { 30 | dark: '#028D4C', 31 | main: '#00B460', 32 | light: '#81C784', 33 | background: '#1F2920', 34 | }, 35 | info: { 36 | dark: '#52BFDC', 37 | main: '#5FDDFF', 38 | light: '#B7F0FF', 39 | background: '#19252C', 40 | }, 41 | warning: { 42 | dark: '#C04C32', 43 | main: '#FF8061', 44 | light: '#FFBC9F', 45 | background: '#2F2318', 46 | }, 47 | background: { 48 | default: '#121312', 49 | main: '#121312', 50 | paper: '#1C1C1C', 51 | light: '#1B2A22', 52 | }, 53 | backdrop: { 54 | main: '#636669', 55 | }, 56 | logo: { 57 | main: '#FFFFFF', 58 | background: '#303033', 59 | }, 60 | upload: { 61 | primary: '#fff', 62 | }, 63 | static: { 64 | main: '#121312', 65 | }, 66 | code: { 67 | main: 'transparent', 68 | }, 69 | } 70 | 71 | export default darkPalette 72 | -------------------------------------------------------------------------------- /apps/tx-builder/src/theme/lightPalette.ts: -------------------------------------------------------------------------------- 1 | const lightPalette = { 2 | text: { 3 | primary: '#121312', 4 | secondary: '#A1A3A7', 5 | disabled: '#DDDEE0', 6 | }, 7 | primary: { 8 | dark: '#3c3c3c', 9 | main: '#121312', 10 | light: '#636669', 11 | }, 12 | secondary: { 13 | dark: '#0FDA6D', 14 | main: '#12FF80', 15 | light: '#B0FFC9', 16 | background: '#EFFFF4', 17 | }, 18 | border: { 19 | main: '#A1A3A7', 20 | light: '#DCDEE0', 21 | background: '#F4F4F4', 22 | }, 23 | error: { 24 | dark: '#AC2C3B', 25 | main: '#FF5F72', 26 | light: '#FFB4BD', 27 | background: '#FFE6EA', 28 | }, 29 | success: { 30 | dark: '#028D4C', 31 | main: '#00B460', 32 | light: '#72F5B8', 33 | background: '#EFFAF1', 34 | }, 35 | info: { 36 | dark: '#52BFDC', 37 | main: '#5FDDFF', 38 | light: '#B7F0FF', 39 | background: '#EFFCFF', 40 | }, 41 | warning: { 42 | dark: '#C04C32', 43 | main: '#FF8061', 44 | light: '#FFBC9F', 45 | background: '#FFF1E0', 46 | }, 47 | background: { 48 | default: '#F4F4F4', 49 | main: '#F4F4F4', 50 | paper: '#FFFFFF', 51 | light: '#EFFFF4', 52 | }, 53 | backdrop: { 54 | main: '#636669', 55 | }, 56 | logo: { 57 | main: '#121312', 58 | background: '#EEEFF0', 59 | }, 60 | upload: { 61 | primary: '#12FF80', 62 | }, 63 | static: { 64 | main: '#121312', 65 | }, 66 | code: { 67 | main: '#FFFFFF', 68 | }, 69 | } 70 | 71 | export default lightPalette 72 | -------------------------------------------------------------------------------- /apps/tx-builder/src/theme/typography.ts: -------------------------------------------------------------------------------- 1 | import type { TypographyOptions } from '@mui/material/styles/createTypography' 2 | 3 | const safeFontFamily = 'DM Sans, sans-serif' 4 | 5 | const typography: TypographyOptions = { 6 | fontFamily: safeFontFamily, 7 | h1: { 8 | fontSize: '32px', 9 | lineHeight: '36px', 10 | fontWeight: 700, 11 | }, 12 | h2: { 13 | fontSize: '27px', 14 | lineHeight: '34px', 15 | fontWeight: 700, 16 | }, 17 | h3: { 18 | fontSize: '24px', 19 | lineHeight: '30px', 20 | }, 21 | h4: { 22 | fontSize: '20px', 23 | lineHeight: '26px', 24 | }, 25 | h5: { 26 | fontSize: '16px', 27 | fontWeight: 700, 28 | }, 29 | body1: { 30 | fontSize: '16px', 31 | lineHeight: '22px', 32 | }, 33 | body2: { 34 | fontSize: '14px', 35 | lineHeight: '20px', 36 | }, 37 | caption: { 38 | fontSize: '12px', 39 | lineHeight: '16px', 40 | letterSpacing: '0.4px', 41 | }, 42 | overline: { 43 | fontSize: '11px', 44 | lineHeight: '14px', 45 | textTransform: 'uppercase', 46 | letterSpacing: '1px', 47 | }, 48 | } 49 | 50 | export default typography 51 | -------------------------------------------------------------------------------- /apps/tx-builder/src/typings/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const content: React.FunctionComponent> 3 | export default content 4 | } 5 | -------------------------------------------------------------------------------- /apps/tx-builder/src/typings/errors.ts: -------------------------------------------------------------------------------- 1 | // ethers does not export this type, so we have to define it ourselves 2 | // the type is based on the following code: 3 | // https://github.com/ethers-io/ethers.js/blob/c80fcddf50a9023486e9f9acb1848aba4c19f7b6/packages/logger/src.ts/index.ts#L197 4 | interface EthersError extends Error { 5 | reason: string 6 | code: string 7 | } 8 | 9 | const isEthersError = (error: unknown): error is EthersError => { 10 | return typeof error === 'object' && error !== null && 'reason' in error && 'code' in error 11 | } 12 | 13 | export type { EthersError } 14 | export { isEthersError } 15 | -------------------------------------------------------------------------------- /apps/tx-builder/src/typings/fonts.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.woff' 2 | declare module '*.woff2' 3 | -------------------------------------------------------------------------------- /apps/tx-builder/src/typings/models.ts: -------------------------------------------------------------------------------- 1 | export interface ProposedTransaction { 2 | id: number 3 | contractInterface: ContractInterface | null 4 | description: { 5 | to: string 6 | value: string 7 | customTransactionData?: string 8 | contractMethod?: ContractMethod 9 | contractFieldsValues?: Record 10 | contractMethodIndex?: string 11 | nativeCurrencySymbol?: string 12 | networkPrefix?: string 13 | } 14 | raw: { to: string; value: string; data: string } 15 | } 16 | 17 | export interface ContractInterface { 18 | methods: ContractMethod[] 19 | } 20 | 21 | export interface Batch { 22 | id: number | string 23 | name: string 24 | transactions: ProposedTransaction[] 25 | } 26 | 27 | export interface BatchFile { 28 | version: string 29 | chainId: string 30 | createdAt: number 31 | meta: BatchFileMeta 32 | transactions: BatchTransaction[] 33 | } 34 | 35 | export interface BatchFileMeta { 36 | txBuilderVersion?: string 37 | checksum?: string 38 | createdFromSafeAddress?: string 39 | createdFromOwnerAddress?: string 40 | name: string 41 | description?: string 42 | } 43 | 44 | export interface BatchTransaction { 45 | to: string 46 | value: string 47 | data?: string 48 | contractMethod?: ContractMethod 49 | contractInputsValues?: { [key: string]: string } 50 | } 51 | 52 | export interface ContractMethod { 53 | inputs: ContractInput[] 54 | name: string 55 | payable: boolean 56 | } 57 | 58 | export interface ContractInput { 59 | internalType: string 60 | name: string 61 | type: string 62 | components?: ContractInput[] 63 | } 64 | -------------------------------------------------------------------------------- /apps/tx-builder/src/utils/strings.ts: -------------------------------------------------------------------------------- 1 | export const textShortener = ( 2 | text: string, 3 | charsStart: number, 4 | charsEnd: number, 5 | separator = '...', 6 | ): string => { 7 | const amountOfCharsToKeep = charsEnd + charsStart 8 | 9 | if (amountOfCharsToKeep >= text.length || !amountOfCharsToKeep) { 10 | // no need to shorten 11 | return text 12 | } 13 | 14 | const r = new RegExp(`^(.{${charsStart}}).+(.{${charsEnd}})$`) 15 | const matchResult = r.exec(text) 16 | 17 | if (!matchResult) { 18 | // if for any reason the exec returns null, the text remains untouched 19 | return text 20 | } 21 | 22 | const [, textStart, textEnd] = matchResult 23 | 24 | return `${textStart}${separator}${textEnd}` 25 | } 26 | -------------------------------------------------------------------------------- /apps/tx-builder/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /apps/wallet-connect/.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_WALLETCONNECT_PROJECT_ID= 2 | -------------------------------------------------------------------------------- /apps/wallet-connect/README.md: -------------------------------------------------------------------------------- 1 | # Deprecated! ⚠️ 2 | 3 | This app is deprecated and will no longer be supported. Safe Wallet now has a native WalletConnect implementation. 4 | 5 | ## Walletconnect Safe App 6 | 7 | Safe Wallet integration for version 1 & 2. 8 | 9 | ## Config env variables 10 | 11 | - Add `REACT_APP_WALLETCONNECT_PROJECT_ID` required for walletconnect version 2 integration, see [walletconnect docs](https://docs.walletconnect.com/2.0/javascript/sign/installation#1-obtain-project-id) 12 | 13 | This variable is currently not being passed in the GitHub secrets as the app is deprecated. 14 | -------------------------------------------------------------------------------- /apps/wallet-connect/config-overrides.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | 3 | module.exports = { 4 | webpack: function (config, env) { 5 | const fallback = config.resolve.fallback || {} 6 | 7 | // https://github.com/ChainSafe/web3.js#web3-and-create-react-app 8 | Object.assign(fallback, { 9 | crypto: require.resolve('crypto-browserify'), 10 | stream: require.resolve('stream-browserify'), 11 | assert: require.resolve('assert'), 12 | http: require.resolve('stream-http'), 13 | https: require.resolve('https-browserify'), 14 | os: require.resolve('os-browserify'), 15 | url: require.resolve('url'), 16 | // https://stackoverflow.com/questions/68707553/uncaught-referenceerror-buffer-is-not-defined 17 | buffer: require.resolve('buffer'), 18 | }) 19 | 20 | config.resolve.fallback = fallback 21 | 22 | config.plugins = (config.plugins || []).concat([ 23 | new webpack.ProvidePlugin({ 24 | process: 'process/browser', 25 | Buffer: ['buffer', 'Buffer'], 26 | }), 27 | ]) 28 | 29 | // https://github.com/facebook/create-react-app/issues/11924 30 | config.ignoreWarnings = [/to parse source map/i] 31 | 32 | return config 33 | }, 34 | jest: function (config) { 35 | return config 36 | }, 37 | devServer: function (configFunction) { 38 | return function (proxy, allowedHost) { 39 | const config = configFunction(proxy, allowedHost) 40 | 41 | config.headers = { 42 | 'Access-Control-Allow-Origin': '*', 43 | 'Access-Control-Allow-Methods': 'GET', 44 | 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization', 45 | } 46 | 47 | return config 48 | } 49 | }, 50 | paths: function (paths) { 51 | return paths 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /apps/wallet-connect/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wallet-connect", 3 | "version": "1.13.2", 4 | "private": true, 5 | "homepage": "./", 6 | "dependencies": { 7 | "@gnosis.pm/safe-react-components": "^0.9.7", 8 | "@safe-global/safe-apps-provider": "^0.18.0", 9 | "@safe-global/safe-gateway-typescript-sdk": "^3.7.3", 10 | "@walletconnect/client": "^1.8.0", 11 | "@walletconnect/web3wallet": "^1.8.6", 12 | "date-fns": "^2.30.0", 13 | "ethers": "^5.7.2", 14 | "jsqr": "^1.4.0" 15 | }, 16 | "scripts": { 17 | "start": "react-app-rewired start", 18 | "build": "react-app-rewired build", 19 | "test": "react-app-rewired test --passWithNoTests", 20 | "deploy:s3": "bash ../../scripts/deploy_to_s3_bucket.sh", 21 | "deploy:pr": "bash ../../scripts/deploy_pr.sh", 22 | "deploy:prod-hook": "bash ../../scripts/prepare_production_deployment.sh" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | }, 39 | "devDependencies": { 40 | "@walletconnect/legacy-types": "^2.0.0", 41 | "@walletconnect/types": "^2.9.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /apps/wallet-connect/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "apps/wallet-connect/", 3 | "sourceRoot": "apps/wallet-connect/src/", 4 | "projectType": "application", 5 | "tags": ["scope:applications"], 6 | "targets": { 7 | "version": { 8 | "executor": "@jscutlery/semver:version", 9 | "options": { 10 | "commitMessageFormat": "chore(${projectName}): release version ${version}" 11 | } 12 | }, 13 | "github": { 14 | "executor": "@jscutlery/semver:github", 15 | "options": { 16 | "tag": "${tag}", 17 | "generateNotes": true 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/wallet-connect/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safe-global/safe-react-apps/2bbc198c090c81eb784f4a8a646382520e338056/apps/wallet-connect/public/favicon.ico -------------------------------------------------------------------------------- /apps/wallet-connect/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | WalletConnect Safe App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /apps/wallet-connect/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "WalletConnect", 3 | "description": "Allows your Safe to connect to dapps via WalletConnect.", 4 | "iconPath": "wallet-connect.svg", 5 | "icons": [ 6 | { 7 | "src": "wallet-connect.svg", 8 | "sizes": "any", 9 | "type": "image/svg+xml" 10 | } 11 | ], 12 | "safe_apps_permissions": ["camera"] 13 | } 14 | -------------------------------------------------------------------------------- /apps/wallet-connect/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /apps/wallet-connect/src/assets/cam-permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safe-global/safe-react-apps/2bbc198c090c81eb784f4a8a646382520e338056/apps/wallet-connect/src/assets/cam-permissions.png -------------------------------------------------------------------------------- /apps/wallet-connect/src/components/AppBar.tsx: -------------------------------------------------------------------------------- 1 | import MuiAppBar from '@material-ui/core/AppBar' 2 | import styled from 'styled-components' 3 | import { Icon, Link, Text } from '@gnosis.pm/safe-react-components' 4 | 5 | const WALLET_CONNECT_HELP = 'https://help.safe.global/en/articles/40849-walletconnect-safe-app' 6 | 7 | const AppBar = () => { 8 | return ( 9 | 10 | Wallet Connect 11 | 12 | 13 | 14 | 15 | ) 16 | } 17 | 18 | const StyledAppBar = styled(MuiAppBar)` 19 | && { 20 | background: #fff; 21 | height: 70px; 22 | align-items: center; 23 | justify-content: flex-start; 24 | flex-direction: row; 25 | border-bottom: 2px solid #e8e7e6; 26 | } 27 | ` 28 | 29 | const StyledAppBarText = styled(Text)` 30 | font-size: 20px; 31 | margin-left: 38px; 32 | margin-right: 16px; 33 | ` 34 | 35 | export default AppBar 36 | -------------------------------------------------------------------------------- /apps/wallet-connect/src/components/Disconnected.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Grid from '@material-ui/core/Grid' 3 | import styled from 'styled-components' 4 | import { Text } from '@gnosis.pm/safe-react-components' 5 | import { ReactComponent as WalletConnectLogo } from '../assets/wallet-connect-logo.svg' 6 | 7 | const Disconnected: React.FC = ({ children }) => { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | Connect your Safe to a dApp via the WalletConnect and trigger transactions 16 | 17 | 18 | {children} 19 | 20 | ) 21 | } 22 | 23 | const StyledContainer = styled(Grid)` 24 | padding: 38px 30px 45px 30px; 25 | ` 26 | 27 | const StyledText = styled(Text)` 28 | text-align: center; 29 | margin-bottom: 8px; 30 | ` 31 | 32 | export default Disconnected 33 | -------------------------------------------------------------------------------- /apps/wallet-connect/src/components/styles.ts: -------------------------------------------------------------------------------- 1 | import Grid from '@material-ui/core/Grid' 2 | import styled from 'styled-components' 3 | import { Text } from '@gnosis.pm/safe-react-components' 4 | 5 | export const StyledCardContainer = styled(Grid)` 6 | padding: 16px 22px; 7 | ` 8 | 9 | export const StyledImage = styled.div<{ src: string }>` 10 | background: url('${({ src }) => src}') no-repeat center; 11 | height: 60px; 12 | width: 60px; 13 | background-size: contain; 14 | ` 15 | 16 | export const StyledBoldText = styled(Text)` 17 | font-weight: bold; 18 | ` 19 | -------------------------------------------------------------------------------- /apps/wallet-connect/src/constants.ts: -------------------------------------------------------------------------------- 1 | const { REACT_APP_WALLETCONNECT_PROJECT_ID, NODE_ENV } = process.env 2 | 3 | export const isProduction = NODE_ENV === 'production' 4 | 5 | export const WALLETCONNECT_V2_PROJECT_ID = REACT_APP_WALLETCONNECT_PROJECT_ID 6 | 7 | export const SAFE_WALLET_METADATA = { 8 | name: 'Safe Wallet', 9 | description: 'The most trusted platform to manage digital assets on Ethereum', 10 | url: 'https://app.safe.global', 11 | icons: [ 12 | 'https://app.safe.global/favicons/mstile-150x150.png', 13 | 'https://app.safe.global/favicons/logo_120x120.png', 14 | ], 15 | } 16 | -------------------------------------------------------------------------------- /apps/wallet-connect/src/global.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components' 2 | import avertaFont from '@gnosis.pm/safe-react-components/dist/fonts/averta-normal.woff2' 3 | import avertaBoldFont from '@gnosis.pm/safe-react-components/dist/fonts/averta-bold.woff2' 4 | 5 | const GlobalStyle = createGlobalStyle` 6 | html { 7 | height: 100% 8 | } 9 | 10 | body { 11 | height: 100%; 12 | margin: 0px; 13 | padding: 0px; 14 | } 15 | 16 | #root { 17 | height: 100%; 18 | } 19 | 20 | @font-face { 21 | font-family: 'Averta'; 22 | src: local('Averta'), url(${avertaFont}) format('woff2') 23 | } 24 | 25 | @font-face { 26 | font-family: 'Averta'; 27 | src: local('Averta Bold'), url(${avertaBoldFont}) format('woff'); 28 | font-weight: bold; 29 | } 30 | ` 31 | 32 | export default GlobalStyle 33 | -------------------------------------------------------------------------------- /apps/wallet-connect/src/hooks/useWebcam.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react' 2 | 3 | function useWebcam() { 4 | const videoRef = useRef(null) 5 | 6 | const [isLoadingWebcam, setIsLoadingWebcam] = useState(true) 7 | const [errorConnectingWebcam, setErrorConnectingWebcam] = useState(false) 8 | 9 | useEffect(() => { 10 | let stream: MediaStream | null 11 | async function getUserWebcam() { 12 | setIsLoadingWebcam(true) 13 | try { 14 | // see https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia 15 | stream = await navigator.mediaDevices.getUserMedia({ video: true }) 16 | 17 | if (videoRef.current) { 18 | videoRef.current.srcObject = stream 19 | videoRef.current.setAttribute('playsinline', 'true') // required to tell iOS safari we don't want fullscreen 20 | videoRef.current.play() 21 | setErrorConnectingWebcam(false) 22 | } 23 | } catch (error) { 24 | setErrorConnectingWebcam(true) 25 | console.log('Error connecting the camera: ', error) 26 | } 27 | setIsLoadingWebcam(false) 28 | } 29 | 30 | getUserWebcam() 31 | 32 | // closing webcam connection on unmount 33 | return () => { 34 | stream?.getTracks().forEach((track: MediaStreamTrack) => { 35 | track.stop() 36 | }) 37 | } 38 | }, []) 39 | 40 | return { 41 | videoRef, 42 | isLoadingWebcam, 43 | errorConnectingWebcam, 44 | } 45 | } 46 | 47 | export default useWebcam 48 | -------------------------------------------------------------------------------- /apps/wallet-connect/src/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom' 2 | import App from './App' 3 | import { SafeProvider } from '@safe-global/safe-apps-react-sdk' 4 | import { Loader, theme } from '@gnosis.pm/safe-react-components' 5 | import { ThemeProvider } from 'styled-components' 6 | 7 | import GlobalStyles from './global' 8 | 9 | ReactDOM.render( 10 | <> 11 | 12 | 13 | }> 14 | 15 | 16 | 17 | , 18 | document.getElementById('root'), 19 | ) 20 | -------------------------------------------------------------------------------- /apps/wallet-connect/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/wallet-connect/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect' 2 | 3 | // Jest is not able to use this function from node, which is used at viem v1.3.0 4 | // We need to import it manually 5 | import { TextEncoder } from 'util' 6 | 7 | global.TextEncoder = TextEncoder 8 | // END 9 | 10 | Object.defineProperty(window.navigator, 'mediaDevices', { 11 | writable: true, 12 | value: { 13 | getUserMedia: () => ({ 14 | getTracks: () => [ 15 | // simple MediaStreamTrack stub 16 | { 17 | stop: jest.fn(), 18 | }, 19 | ], 20 | }), 21 | }, 22 | }) 23 | 24 | Object.defineProperty(window.HTMLMediaElement.prototype, 'play', { 25 | writable: true, 26 | value: jest.fn(), 27 | }) 28 | 29 | Object.defineProperty(window.HTMLVideoElement.prototype, 'readyState', { 30 | writable: false, 31 | value: window.HTMLVideoElement.prototype.HAVE_ENOUGH_DATA, 32 | }) 33 | 34 | Object.defineProperty(window.HTMLCanvasElement.prototype, 'getContext', { 35 | writable: false, 36 | value: () => { 37 | return { 38 | drawImage: jest.fn(), 39 | getImageData: jest.fn().mockImplementation(() => { 40 | return { 41 | data: 'image test data', 42 | width: 450, 43 | height: 450, 44 | } 45 | }), 46 | } 47 | }, 48 | }) 49 | -------------------------------------------------------------------------------- /apps/wallet-connect/src/typings/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const content: React.FunctionComponent> 3 | export default content 4 | } 5 | -------------------------------------------------------------------------------- /apps/wallet-connect/src/typings/fonts.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.woff' 2 | declare module '*.woff2' 3 | -------------------------------------------------------------------------------- /apps/wallet-connect/src/utils/analytics.ts: -------------------------------------------------------------------------------- 1 | const SAFE_APPS_ANALYTICS_CATEGORY = 'safe-apps-analytics' 2 | export const NEW_SESSION_ACTION = 'New session' 3 | export const TRANSACTION_CONFIRMED_ACTION = 'Transaction Confirmed' 4 | 5 | export const WALLET_CONNECT_VERSION_1 = 'v1' 6 | export const WALLET_CONNECT_VERSION_2 = 'v2' 7 | 8 | export type WalletConnectVersion = typeof WALLET_CONNECT_VERSION_1 | typeof WALLET_CONNECT_VERSION_2 9 | 10 | export const trackSafeAppEvent = ( 11 | action: string, 12 | version: WalletConnectVersion, 13 | connectedPeer?: string, 14 | ) => { 15 | window.parent.postMessage( 16 | { 17 | category: SAFE_APPS_ANALYTICS_CATEGORY, 18 | action, 19 | label: connectedPeer, 20 | safeAppName: `Walletconnect-${version}`, 21 | }, 22 | '*', 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /apps/wallet-connect/src/utils/images.ts: -------------------------------------------------------------------------------- 1 | const blobToImageData = async (blob: string) => { 2 | return new Promise((resolve, reject) => { 3 | let img = new Image() 4 | img.src = blob 5 | img.onload = () => resolve(img) 6 | img.onerror = err => reject(err) 7 | }).then(img => { 8 | let canvas = document.createElement('canvas') 9 | canvas.width = img.width 10 | canvas.height = img.height 11 | let ctx = canvas.getContext('2d') 12 | 13 | if (!ctx) throw new Error('Could not generate context from canvas') 14 | 15 | ctx.drawImage(img, 0, 0) 16 | return ctx.getImageData(0, 0, img.width, img.height) // some browsers synchronously decode image here 17 | }) 18 | } 19 | 20 | export { blobToImageData } 21 | -------------------------------------------------------------------------------- /apps/wallet-connect/src/utils/test-helpers.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react' 2 | import { ThemeProvider } from 'styled-components' 3 | import { theme } from '@gnosis.pm/safe-react-components' 4 | 5 | import GlobalStyles from '../global' 6 | 7 | function renderWithProviders(ui: JSX.Element) { 8 | return render( 9 | <> 10 | 11 | {ui} 12 | , 13 | ) 14 | } 15 | 16 | export { renderWithProviders } 17 | -------------------------------------------------------------------------------- /apps/wallet-connect/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | 3 | const axios = require('axios') 4 | const { sendSlackMessage } = require('./cypress/lib/slack') 5 | 6 | require('dotenv').config() 7 | 8 | export default defineConfig({ 9 | projectId: 'okn21k', 10 | chromeWebSecurity: false, 11 | modifyObstructiveCode: false, 12 | video: true, 13 | retries: { 14 | runMode: 2, 15 | openMode: 2, 16 | }, 17 | env: { 18 | SAFE_APPS_BASE_URL: process.env.CYPRESS_SAFE_APPS_BASE_URL, 19 | CHAIN_ID: process.env.CYPRESS_CHAIN_ID, 20 | NETWORK_PREFIX: process.env.CYPRESS_NETWORK_PREFIX, 21 | TESTING_SAFE_ADDRESS: process.env.CYPRESS_TESTING_SAFE_ADDRESS, 22 | DRAIN_SAFE_URL: process.env.CYPRESS_DRAIN_SAFE_URL, 23 | TX_BUILDER_URL: process.env.CYPRESS_TX_BUILDER_URL, 24 | }, 25 | e2e: { 26 | baseUrl: process.env.CYPRESS_WEB_BASE_URL, 27 | async setupNodeEvents(on, config) { 28 | on('after:run', sendSlackMessage) 29 | on('task', { 30 | log(message) { 31 | console.log(message) 32 | return null 33 | }, 34 | }) 35 | 36 | let safeAppsList 37 | 38 | try { 39 | safeAppsList = await axios.get( 40 | `${process.env.CYPRESS_CLIENT_GATEWAY_BASE_URL}/v1/chains/${ 41 | process.env.CYPRESS_CHAIN_ID 42 | }/safe-apps?client_url=${encodeURIComponent(process.env.CYPRESS_WEB_BASE_URL)}`, 43 | ) 44 | } catch (e) { 45 | console.log('Unable to fetch the default list: ', e) 46 | } 47 | 48 | config.env.SAFE_APPS_LIST = safeAppsList.data 49 | 50 | return config 51 | }, 52 | }, 53 | }) 54 | -------------------------------------------------------------------------------- /cypress/e2e/safe-apps-check.spec.cy.js: -------------------------------------------------------------------------------- 1 | const safeAppsList = Cypress.env('SAFE_APPS_LIST') || [] 2 | 3 | describe('Safe Apps List', () => { 4 | before(() => { 5 | expect(safeAppsList).to.be.an('array').and.to.have.length.greaterThan(0) 6 | }) 7 | 8 | safeAppsList.forEach(safeApp => { 9 | it(safeApp.name, () => { 10 | cy.visitSafeApp( 11 | `/apps/open?safe=${Cypress.env('NETWORK_PREFIX')}:${Cypress.env( 12 | 'TESTING_SAFE_ADDRESS', 13 | )}&appUrl=${safeApp.url}`, 14 | safeApp.url, 15 | ) 16 | const iframeSelector = `iframe[id="iframe-${safeApp.url}"]` 17 | cy.frameLoaded(iframeSelector) 18 | cy.iframe(iframeSelector).get('#root,#app,.app,main,#__next,app-root,#___gatsby') 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /cypress/fixtures/test-empty-batch.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /cypress/fixtures/test-invalid-batch.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": "I am not a valid batch" 3 | } 4 | -------------------------------------------------------------------------------- /cypress/fixtures/test-mainnet-batch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "chainId": "1", 4 | "createdAt": 1671532788473, 5 | "meta": { 6 | "name": "Transactions Batch", 7 | "description": "", 8 | "txBuilderVersion": "1.13.1", 9 | "createdFromSafeAddress": "0xE96C43C54B08eC528e9e815fC3D02Ea94A320505", 10 | "createdFromOwnerAddress": "", 11 | "checksum": "0x783b24b06f925df195ac0e0103507caf6520cff278555c11e9b8edb43bc2a196" 12 | }, 13 | "transactions": [ 14 | { 15 | "to": "0x51A099ac1BF46D471110AA8974024Bfe518Fd6C4", 16 | "value": "0", 17 | "data": null, 18 | "contractMethod": { 19 | "inputs": [{ "internalType": "bool", "name": "newValue", "type": "bool" }], 20 | "name": "testBooleanValue", 21 | "payable": false 22 | }, 23 | "contractInputsValues": { "newValue": "true" } 24 | }, 25 | { 26 | "to": "0x51A099ac1BF46D471110AA8974024Bfe518Fd6C4", 27 | "value": "0", 28 | "data": null, 29 | "contractMethod": { 30 | "inputs": [{ "internalType": "address", "name": "newValue", "type": "address" }], 31 | "name": "testAddressValue", 32 | "payable": false 33 | }, 34 | "contractInputsValues": { "newValue": "0x51A099ac1BF46D471110AA8974024Bfe518Fd6C4" } 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /cypress/fixtures/test-modified-batch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "chainId": "1", 4 | "createdAt": 1671532788473, 5 | "meta": { 6 | "name": "Transactions Batch", 7 | "description": "", 8 | "txBuilderVersion": "1.13.1", 9 | "createdFromSafeAddress": "0xE96C43C54B08eC528e9e815fC3D02Ea94A320505", 10 | "createdFromOwnerAddress": "", 11 | "checksum": "0x783b24b06f925df195ac0e0103507caf6520cff278555c11e9b8edb43bc2a196" 12 | }, 13 | "transactions": [ 14 | { 15 | "to": "", 16 | "value": "", 17 | "data": null, 18 | "contractMethod": { 19 | "inputs": [{ "internalType": "bool", "name": "newValue", "type": "bool" }], 20 | "name": "testBooleanValue", 21 | "payable": false 22 | }, 23 | "contractInputsValues": { "newValue": "true" } 24 | }, 25 | { 26 | "to": "", 27 | "value": "", 28 | "data": null, 29 | "contractMethod": { 30 | "inputs": [{ "internalType": "address", "name": "newValue", "type": "address" }], 31 | "name": "testAddressValue", 32 | "payable": false 33 | }, 34 | "contractInputsValues": { "newValue": "" } 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /cypress/fixtures/test-working-batch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "chainId": "5", 4 | "createdAt": 1671532788473, 5 | "meta": { 6 | "name": "Transactions Batch", 7 | "description": "", 8 | "txBuilderVersion": "1.13.1", 9 | "createdFromSafeAddress": "0xE96C43C54B08eC528e9e815fC3D02Ea94A320505", 10 | "createdFromOwnerAddress": "", 11 | "checksum": "0x783b24b06f925df195ac0e0103507caf6520cff278555c11e9b8edb43bc2a196" 12 | }, 13 | "transactions": [ 14 | { 15 | "to": "0x51A099ac1BF46D471110AA8974024Bfe518Fd6C4", 16 | "value": "0", 17 | "data": null, 18 | "contractMethod": { 19 | "inputs": [{ "internalType": "bool", "name": "newValue", "type": "bool" }], 20 | "name": "testBooleanValue", 21 | "payable": false 22 | }, 23 | "contractInputsValues": { "newValue": "true" } 24 | }, 25 | { 26 | "to": "0x51A099ac1BF46D471110AA8974024Bfe518Fd6C4", 27 | "value": "0", 28 | "data": null, 29 | "contractMethod": { 30 | "inputs": [{ "internalType": "address", "name": "newValue", "type": "address" }], 31 | "name": "testAddressValue", 32 | "payable": false 33 | }, 34 | "contractInputsValues": { "newValue": "0x51A099ac1BF46D471110AA8974024Bfe518Fd6C4" } 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | import 'cypress-file-upload' 2 | -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/cypress/add-commands' 2 | import './iframe' 3 | import './commands' 4 | 5 | export const INFO_MODAL_KEY = 'SAFE_v2__SafeApps__infoModal' 6 | export const BROWSER_PERMISSIONS_KEY = 'SAFE_v2__SafeApps__browserPermissions' 7 | 8 | const chains = [1, 5, 10, 56, 100, 137, 42161, 43114, 73799, 1313161554] 9 | 10 | let warningCheckedCustomApps = [] 11 | const drainSafeUrl = Cypress.env('DRAIN_SAFE_URL') 12 | 13 | // TODO: Remove this once all the safe apps are deployed on the same domain in each environment 14 | if (drainSafeUrl && drainSafeUrl.includes('safereactapps.review-react-hr.5afe.dev')) { 15 | warningCheckedCustomApps.push(new URL(drainSafeUrl).origin) 16 | } else { 17 | warningCheckedCustomApps = [ 18 | 'https://safe-apps.dev.5afe.dev', 19 | 'https://apps-portal.safe.global', 20 | ] 21 | } 22 | 23 | Cypress.Commands.add('visitSafeApp', (visitUrl, appUrl) => { 24 | if (appUrl) { 25 | cy.intercept('GET', `${appUrl}/manifest.json`, { 26 | name: 'App', 27 | description: 'The App', 28 | iconPath: 'logo.svg', 29 | safe_apps_permissions: [], 30 | }) 31 | } 32 | 33 | cy.on('window:before:load', async window => { 34 | // Avoid to show the disclaimer and unknown apps warning 35 | window.localStorage.setItem( 36 | INFO_MODAL_KEY, 37 | JSON.stringify({ 38 | ...chains.reduce( 39 | (acc, chainId) => ({ 40 | ...acc, 41 | [`${chainId}`]: { 42 | consentsAccepted: true, 43 | warningCheckedCustomApps, 44 | }, 45 | }), 46 | {}, 47 | ), 48 | }), 49 | ) 50 | 51 | window.localStorage.setItem('SAFE_v2__lastWallet', JSON.stringify('E2E Wallet')) 52 | }) 53 | 54 | cy.visit(visitUrl, { failOnStatusCode: false }) 55 | 56 | cy.wait(500) 57 | }) 58 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@nrwl/workspace/presets/npm.json", 3 | "npmScope": "safe-apps", 4 | "tasksRunnerOptions": { 5 | "default": { 6 | "runner": "@nrwl/workspace/tasks-runners/default", 7 | "options": { 8 | "cacheableOperations": ["build", "test"] 9 | } 10 | } 11 | }, 12 | "targetDependencies": { 13 | "build": [ 14 | { 15 | "target": "build", 16 | "projects": "dependencies" 17 | } 18 | ], 19 | "prepare": [ 20 | { 21 | "target": "prepare", 22 | "projects": "dependencies" 23 | } 24 | ], 25 | "package": [ 26 | { 27 | "target": "package", 28 | "projects": "dependencies" 29 | } 30 | ] 31 | }, 32 | "affected": { 33 | "defaultBase": "main" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /scripts/deploy_pr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function deploy_app_pr { 4 | # Pull request number with "pr" prefix 5 | PULL_REQUEST_NUMBER="pr$PR_NUMBER" 6 | REVIEW_FEATURE_FOLDER="$REPO_NAME_ALPHANUMERIC/$PULL_REQUEST_NUMBER" 7 | # When you execute "yarn run deploy:pr", it runs it in the app folder, so we only need the name of the folder with the bundle 8 | BUNDLE_FOLDER="build" 9 | 10 | # Deploy app project 11 | aws s3 sync $BUNDLE_FOLDER s3://${REVIEW_BUCKET_NAME}/${REVIEW_FEATURE_FOLDER}/$1 --delete 12 | } 13 | 14 | # Only: 15 | # - Pull requests 16 | # - Security env variables are available. PR from forks don't have them. 17 | if [ -n "$AWS_SECRET_ACCESS_KEY" ] && [ -n "$REPO_NAME_ALPHANUMERIC" ] && [ -n "$PR_NUMBER" ] && [ -n "$REVIEW_BUCKET_NAME" ] 18 | then 19 | echo "Executing in $(pwd)" 20 | # app name is the name of the current folder 21 | APP_NAME="$(basename $(pwd))" 22 | deploy_app_pr $APP_NAME 23 | else 24 | echo "[ERROR] App could not be deployed because of missing environment variables" 25 | exit 1 26 | fi -------------------------------------------------------------------------------- /scripts/deploy_to_s3_bucket.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function deploy_app { 4 | BUNDLE_FOLDER="build" 5 | 6 | PACKAGE_VERSION=$(sed -nr 's/^\s*\"version": "([0-9]{1,}\.[0-9]{1,}.*)",$/\1/p' package.json) 7 | 8 | if [ -n "$APPEND_TAG" ] 9 | then 10 | aws s3 sync $BUNDLE_FOLDER s3://${BUCKET_NAME}/$1/"$PACKAGE_VERSION" --delete 11 | else 12 | aws s3 sync $BUNDLE_FOLDER s3://${BUCKET_NAME}/$1 --delete 13 | fi 14 | } 15 | 16 | # Only: 17 | # - Releases 18 | # - Security env variables are available. PR from forks don't have them. 19 | if [ -n "$AWS_SECRET_ACCESS_KEY" ] && [ -n "$BUCKET_NAME" ] 20 | then 21 | echo "Executing in $(pwd)" 22 | # app name is the name of the current folder 23 | APP_NAME="$(basename $(pwd))" 24 | deploy_app $APP_NAME 25 | else 26 | echo "[ERROR] App could not be deployed because of missing environment variables" 27 | exit 1 28 | fi -------------------------------------------------------------------------------- /scripts/prepare_production_deployment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ev 4 | 5 | # Only: 6 | # - Security env variables are available. 7 | if [ -n "$PROD_DEPLOYMENT_HOOK_TOKEN" ] && [ -n "$PROD_DEPLOYMENT_HOOK_URL" ] 8 | then 9 | APP_NAME="$(basename $(pwd))" 10 | PACKAGE_VERSION=$(sed -nr 's/^\s*\"version": "([0-9]{1,}\.[0-9]{1,}.*)",$/\1/p' package.json) 11 | curl --silent --output /dev/null --write-out "%{http_code}" -X POST \ 12 | -F token="$PROD_DEPLOYMENT_HOOK_TOKEN" \ 13 | -F ref=master \ 14 | -F "variables[TRIGGER_RELEASE_APP_NAME]=$APP_NAME" \ 15 | -F "variables[TRIGGER_RELEASE_COMMIT_TAG]=$PACKAGE_VERSION" \ 16 | $PROD_DEPLOYMENT_HOOK_URL 17 | else 18 | echo "[ERROR] Production deployment could not be prepared" 19 | fi 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": ["./apps/*/**/src"], 23 | "exclude": [ 24 | "node_modules", "build" 25 | ] 26 | } --------------------------------------------------------------------------------