├── .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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
11 | )
12 |
13 | export default icon
14 |
--------------------------------------------------------------------------------
/apps/tx-builder/src/components/FixedIcon/images/plus.tsx:
--------------------------------------------------------------------------------
1 | const icon = (
2 |
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 |
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 |
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 |
16 | ),
17 | md: (
18 |
31 | ),
32 | }
33 |
34 | export default Alert
35 |
--------------------------------------------------------------------------------
/apps/tx-builder/src/components/Icon/images/bookmarkFilled.tsx:
--------------------------------------------------------------------------------
1 | const BookMarkFilled = {
2 | sm: (
3 |
14 | ),
15 | md: (
16 |
27 | ),
28 | }
29 |
30 | export default BookMarkFilled
31 |
--------------------------------------------------------------------------------
/apps/tx-builder/src/components/Icon/images/check.tsx:
--------------------------------------------------------------------------------
1 | const Check = {
2 | sm: (
3 |
12 | ),
13 | md: (
14 |
23 | ),
24 | }
25 |
26 | export default Check
27 |
--------------------------------------------------------------------------------
/apps/tx-builder/src/components/Icon/images/code.tsx:
--------------------------------------------------------------------------------
1 | const Code = {
2 | sm: (
3 |
13 | ),
14 | md: (
15 |
24 | ),
25 | }
26 |
27 | export default Code
28 |
--------------------------------------------------------------------------------
/apps/tx-builder/src/components/Icon/images/copy.tsx:
--------------------------------------------------------------------------------
1 | const Copy = {
2 | sm: (
3 |
13 | ),
14 | md: (
15 |
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 |
14 | ),
15 | md: (
16 |
25 | ),
26 | }
27 |
28 | export default Cross
29 |
--------------------------------------------------------------------------------
/apps/tx-builder/src/components/Icon/images/edit.tsx:
--------------------------------------------------------------------------------
1 | const Edit = {
2 | sm: (
3 |
12 | ),
13 | md: (
14 |
23 | ),
24 | }
25 |
26 | export default Edit
27 |
--------------------------------------------------------------------------------
/apps/tx-builder/src/components/Icon/images/externalLink.tsx:
--------------------------------------------------------------------------------
1 | const ExternalLink = {
2 | sm: (
3 |
17 | ),
18 | md: (
19 |
33 | ),
34 | }
35 |
36 | export default ExternalLink
37 |
--------------------------------------------------------------------------------
/apps/tx-builder/src/components/Icon/images/info.tsx:
--------------------------------------------------------------------------------
1 | const Info = {
2 | sm: (
3 |
19 | ),
20 | md: (
21 |
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 |
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 | }
--------------------------------------------------------------------------------