Gnosis Safe - Public Release v1.9.0 (#713)

* (Fix) Wrong value for ERC-20 tokens transfers (#679)

The fix attempts to properly differentiate an ERC-721 from an ERC-20 token transaction by identifying if it's a `transfer` transaction looking for a `decimals` method in its code. It the later is not found, then it's considered an ERC-721.

fixes #678

* (Fix) send tx from address book (#677)

* fix: Send funds not working when selecting receipt from addressBook

Also, this commit includes an intent to unify/simplify SendModal component

fixes #632

* Set default value to txData for custom txs

fixes #632

* bump version in package.json (#683)

* Onboardjs - Get wallets by platform, fix ledger/trezor rpcUrls (#665)

* Added desktop support

* Added desktop option and logs for testing

* Get desktop mode from window object

* Add torus support and remove logs

* Update function name

* Upgrade onboardjs version

* fix ledger/trezor rpcUrls

* Bump version to 1.8.3

Co-authored-by: Mikhail Mikheev <mmvsha73@gmail.com>

* Master/dev conflict fix (#685)

* Added desktop support

* Added desktop option and logs for testing

* Get desktop mode from window object

* Add torus support and remove logs

* Update function name

* Upgrade onboardjs version

* v1.8.2 (#682)

* (Fix) Wrong value for ERC-20 tokens transfers (#679)

The fix attempts to properly differentiate an ERC-721 from an ERC-20 token transaction by identifying if it's a `transfer` transaction looking for a `decimals` method in its code. It the later is not found, then it's considered an ERC-721.

fixes #678

* (Fix) send tx from address book (#677)

* fix: Send funds not working when selecting receipt from addressBook

Also, this commit includes an intent to unify/simplify SendModal component

fixes #632

* Set default value to txData for custom txs

fixes #632

* bump version in package.json (#683)

Co-authored-by: Fernando <fernando.greco@gmail.com>

* fix ledger/trezor rpcUrls

* Bump version to 1.8.3

Co-authored-by: Mati Dastugue <matias.dastugue@altoros.com>
Co-authored-by: Fernando <fernando.greco@gmail.com>

* (Fix) #423 - Pending transaction confirmation loop (#637)

* Fixs duplicated notifications

* Implements feedback, now the displayed txHash are stored on localstorage and once the first time we notify the user about it, they won't never appear again

* Uses the last time the user logged in

* Fix safe version null check
Fixs date string comparison
Adds the safe address to the check of last time logged in

* (Feature) Erc721 modal lists (#661)

* Add Assets sections

* (add) collectibles tab

* (add) criptokitty items

* (add) collectible items, definitive edition

* (fix) collectibles were overlapping with bottom banner

* (fix) wording

* (fix) responsive issues

* Install `async-sema` dependency

* Create collectible source classes

- source from mocked data and opensea, it's extendable to import information from other sources

* Update `Collectible` implementation to use new data source

* Create constants file to better handle env variables and default values

* Add description to item's cards

- also added a mocked class with real data

* Fix `saveTxToHistory`, remove hardcoded `CALL`

* Fix after merge development

* Set background color for collectible based on data info

- Changed `withStyles` in favor of a hook-like approach with `makeStyles`

* Enhance collectible card info and group title

* Use current safeAddress to query for collectibles information

- also migrated from `withStyles` to `makeStyles`

* Use proper key values for lists and set more significant names

* update yarn.lock after merge

* Fix linting error

* Move ethAsToken verification outside loop

* Use absolute route for `SendModal` import

* Move Collectibles into redux store

* Update yarn.lock file

* Selectable NFTs

* Divide the `collectible` store into `nftAssets` and `nftTokens`

- Also updated components to retain functionality
- Created a `textShortener` function for better presentation

* Update `yarn.lock`

* Update `yarn.lock`

* Fix item background color

* Clears the tokenID select field when the collectible selected changes

* Open Send modal from the assets section

* Use token name for the token selection dropdown

* Refactor Balances tabs: reduces the amount of props received, exported tokens lists to a component

* Refactor Balances tabs: reduces the amount of props received, exported tokens lists to a component

* Add openZeppelin contracts dependency

* Create ERC721 getter

* Fix types, default values and clean code

* Fix: properly refresh list of collectibles when switching safes

* Add ReviewCollectible step in send NFT

* Displays the assets in the manage list

* Fixs add custom token/asset modal cancel button

* Change items shadow

* Give option to choose what to send by clicking 'Send' button in AddressBook

* Disable [Send] button for Collectibles if not owner

* Set Coins as default option in assets tab

- also fixed styles for `Coins` option

* Use collectible icon in send modal

* Set default message when no assets available

- removed pagination feature

* Create SafeVersionProvider to better handle version-related tasks

Provides:
- current and latest versions,
- a boolean indicating a need for update,
- an upgradeSafe callback to trigger upgrade from any place,
- a list of enabled features, depending on the current version
  - the latter needs a refactor like extract features outside the provider
   and define constants for the features.

* Force build

* Update `yarn.lock`

* Disable Manage list for NFTs

* Implements manage list to add/remove assets

* Implements manage list to add/remove assets

* Merge branch 'feature/#469-ERC721-feature-implementation' of https://github.com/gnosis/safe-react into feature/#469-ERC721-feature-implementation

# Conflicts:
#	src/routes/safe/components/Balances/Collectibles/index.jsx
#	src/routes/safe/components/Balances/index.jsx
#	src/utils/constants.js

* Implements blacklisted assets

* Fix container shadow

- Also fixes tables shadow, thanks to @gabitoesmiapodo

* Enable nested routes for balances (assets) tab

* Default to `/balance` if invalid nested path

* Disable [Send Collectible] button, if not supported by safe

* Change sub-menu buttons to clickable text

* Replace Paragraph with Link

* Fix invalid props errors for Link component

* Fallback to `transferFrom` if `safeTransferFrom` is not implemented

* Use `transfer` as fallback to ERC-721's `safeTransferFrom`

- need to identify ERC721 token using `transfer` and `name` methods

* Display failed transactions

* Use react.lazy for collectibles' modals

* Identify ERC-721 token transaction

* Adds initial components for AddCustomAsset support

* Fix Send Collectibles modal layout/behavior

- disable dropdown list if there's no item to pick
- fix placeholder for tokens list
- fix dropdown list styles

* Set default `isSuccessful` flag to `true`

* Fixs erc721Enabled check

* Adds margin to modal icon
Fix search bar

* Fix hidding buttons in coins table

* Fixs display all available assets by default

* Fixs modal assets

* Fixs blacklistedAssetsAddresses save to storage
Fixs show token button

Co-authored-by: fernandomg <fernando.greco@gmail.com>
Co-authored-by: Gabriel Rodriguez Alsina <gabriel.rodriguez@altoros.com>

* Remove `SafeVersionProvider` (#694)

* (Feature) Safe Deployment #605 #111 #395 #606 #396 (#659)

* Stepper component

* proxyfactory web3 contract

* add styles to body steps

* Steps info

* Open component: moving from class to function

* remove opening route and rendering it in Open component instead

* recover safe creation from txHash in localStorage

* remove commented code

* restore commented code

* creatign TX fix

* fix createSafe then function

* fixing stepper

* remove unused code

* remove opening route and finishin both variants of create

* add loader dots svg

* add error state design and loader dots

* fix error section

* add description to steps

* adding etherscan link

* taking values from variables

* fix heigh in body content

* add success svg

* add check image on last step

* fix margin and heigt to body rows

* remove commented code

* remove commented code

* fix for #396

* Fix empty_code

* set error if getReceipt fails

* fixes

* Fix: remove txHash from pendingInfo on retry

Co-authored-by: Mikhail Mikheev <mmvsha73@gmail.com>
Co-authored-by: Agustín Longoni <agustin.longoni@altoros.com>
Co-authored-by: Fernando <fernando.greco@gmail.com>

* Updated modal description

* Update readme (#696)

* Bump 1.5.0 version of onboardjs

* Feature: #528 optimize network requests (#680)

* Generates a cache to avoid multiples getHumanFriendlyToken() for the same token address

* Adds etags implementation for transactions

* Caches outgoing and incoming safe transactions based on etag value

* Removes cachedSafeTransactions, cachedSafeIncommingTransactions

* Refactors getTokenInstance

* Avoid recreating tokens on fetchTokens() once we have them in redux

* Fixs error on catch

* Batch request tokens balances

* Fixs missing token names
Changes the tokens limit from 300 to 3000

* fix: failed to instantiate non-standard ERC-20 tokens

For the batchRequest of balances, we're just using the `balanceOf` method call. So having a simple ABI with that only method prevents errors with non-standard ERC-20 Tokens.

* Removes unnecessary action updateSafeThreshold
Removes unnecessary action fetchEtherBalance

* Updated comments in code
Replaces constant with directly dispatching action

* BatchRequest done right

* fix: invalid action name `savedToken` -> `saveToken`

* Renames getTokenInstance to getTokenInfos
Fixs first load of transactions are empty

* Move fetchTokenBalances to `Balances` and `SendModal` components

* fix: Incoming transaction type

Backend now changed the type from 'incoming' to one of: `'ERC721_TRANSFER', 'ERC20_TRANSFER', 'ETHER_TRANSFER'`

* fix: tokenInstance `symbol` and `decimal` extraction

* Fix property name `decimals` instead of `tokenDecimals`

* Standardize non-standard ERC20 tokens discovery

* fix: isStandardERC20

* Revert "Move fetchTokenBalances to `Balances` and `SendModal` components"

This reverts commit ed84bd92

* Fixs Typo INCOMING_TX_TYPES
Renames tokenInstance with localToken

* Renames getBatchBalances to getTokenBalances
Returns saved tokens instead of tokenInstance in getTokenInfos

* Remove promise returns

Co-authored-by: fernandomg <fernando.greco@gmail.com>

* Feature: Offchain signatures (#668)

* offchain signatures wip

* offchain signing wip

* offchain signatures wip

* offchain signatures wip

* save signatures to the history service

* fix eth signer & useEfefct hook

* offchain signatures wip

* signature check, mainnet testing wip

* dep update

* disable offchain signing for smart contract wallets

* Refactor EIP712 signer

* bring back .env.example

* Check if save version is >1.1.1

* use canTryoffchainSigning boolean variable, add comment about 4001 error

* move semver selector for safe version/offchain signatures to a constant, make use of empty_data for isContractWallet

* remove TYPE when sending txs to history service

* add eth_signTypedData_v4 signer, dep bump, add missing await

* add comments about version check for canTryOffchainSigning variable

* hide "please sign notification"

* dep bump

* dep bump

* Check if connected is ledger before trying offchain signatures

* minor fixes, temp deployment to test trezor

* add hardwareWallet boolean property to wallet model, disable offchain signatures for hw wallets

* (Fix) Owner replacement transaction details (#688)

* fix: No threshold change for owners replacement

* fix: Extract added owner from addressBook

When replacing an owner, the added owner's name was the same as the removed one

* fix: Add or Update addressBook entry for the newly added owner

* Replace `.then` with `async/await`

* Optimize AddressBook entry `name` update

* Revert "Optimize AddressBook entry `name` update"

This reverts commit 00a75d15

* refactor: AddressBook entry from plain JS object to immutable Record

* fix: merge instead of set for the AddressBookEntry record

* refactor: addOrUpdateAddressBookEntry redux action

changed signature to `addOrUpdateAddressBookEntry(entryAddress, entry)`
Where `entry` is an object with only the required fields to be updated

* Fix safe creation from walletConnect (#703)

* Fixes: Offchain signatures (#706)

* offchain signatures wip

* offchain signing wip

* offchain signatures wip

* offchain signatures wip

* save signatures to the history service

* fix eth signer & useEfefct hook

* offchain signatures wip

* signature check, mainnet testing wip

* dep update

* disable offchain signing for smart contract wallets

* Refactor EIP712 signer

* bring back .env.example

* Check if save version is >1.1.1

* use canTryoffchainSigning boolean variable, add comment about 4001 error

* move semver selector for safe version/offchain signatures to a constant, make use of empty_data for isContractWallet

* remove TYPE when sending txs to history service

* add eth_signTypedData_v4 signer, dep bump, add missing await

* add comments about version check for canTryOffchainSigning variable

* hide "please sign notification"

* dep bump

* dep bump

* Check if connected is ledger before trying offchain signatures

* minor fixes, temp deployment to test trezor

* add hardwareWallet boolean property to wallet model, disable offchain signatures for hw wallets

* add personal signer

* prettier fixes

* offchain signatures fixes

* (Fix) New owners as `UNKNOWN` (#697)

* fix: No threshold change for owners replacement

* fix: Extract added owner from addressBook

When replacing an owner, the added owner's name was the same as the removed one

* fix: Add or Update addressBook entry for the newly added owner

* Replace `.then` with `async/await`

* Optimize AddressBook entry `name` update

* fix: store added owner in the addressBook

* Revert "Optimize AddressBook entry `name` update"

This reverts commit 00a75d15

* refactor: AddressBook entry from plain JS object to immutable Record

* fix: merge instead of set for the AddressBookEntry record

* refactor: addOrUpdateAddressBookEntry redux action

changed signature to `addOrUpdateAddressBookEntry(entryAddress, entry)`
Where `entry` is an object with only the required fields to be updated

* refactor: update `addOrUpdateAddressBookEntry` call, due to signature change

* Bug: Eth balance not updating (#709)

* fix prop name for updating eth balance

* remove unneccessary index in import

* typo fixes in safe opening

* Don't reassign transaction in getAwaitingTransactions, check for another transaction executed with the same nonce and not only cancellation ones (#710)

Co-authored-by: Fernando <fernando.greco@gmail.com>
Co-authored-by: Mati Dastugue <matias.dastugue@altoros.com>
Co-authored-by: Agustin Pane <agustin.pane@gmail.com>
Co-authored-by: Gabriel Rodriguez Alsina <gabriel.rodriguez@altoros.com>
Co-authored-by: nicolas <nicosampler@users.noreply.github.com>
Co-authored-by: Agustín Longoni <agustin.longoni@altoros.com>
Co-authored-by: lukasschor <lukas.schor@gnosis.pm>
This commit is contained in:
Mikhail Mikheev 2020-04-01 18:56:04 +04:00 committed by GitHub
parent 0c5afc0793
commit 8ff6695562
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
161 changed files with 4727 additions and 1833 deletions

View File

@ -23,3 +23,6 @@ REACT_APP_COLLECTIBLES_SOURCE=
REACT_APP_LATEST_SAFE_VERSION=
# Leave it untouched, version will set using dotenv-expand
REACT_APP_APP_VERSION=$npm_package_version
# all environments
REACT_APP_INFURA_TOKEN=

View File

@ -43,19 +43,19 @@
"dependencies": {
"@gnosis.pm/safe-contracts": "1.1.1-dev.1",
"@gnosis.pm/util-contracts": "2.0.6",
"@material-ui/core": "4.9.5",
"@material-ui/core": "4.9.8",
"@material-ui/icons": "4.9.1",
"@material-ui/lab": "4.0.0-alpha.39",
"@openzeppelin/contracts": "^2.5.0",
"@testing-library/jest-dom": "5.1.1",
"@testing-library/jest-dom": "5.3.0",
"@welldone-software/why-did-you-render": "4.0.5",
"async-sema": "^3.1.0",
"axios": "0.19.2",
"bignumber.js": "9.0.0",
"bnc-onboard": "1.4.0",
"connected-react-router": "6.7.0",
"bnc-onboard": "1.5.0",
"connected-react-router": "6.8.0",
"currency-flags": "^2.1.1",
"date-fns": "2.10.0",
"date-fns": "2.11.1",
"dotenv": "^8.2.0",
"ethereum-ens": "0.8.0",
"final-form": "4.18.7",
@ -63,20 +63,20 @@
"immortal-db": "^1.0.2",
"immutable": "^4.0.0-rc.9",
"js-cookie": "^2.2.1",
"lint-staged": "^10.0.7",
"lint-staged": "10.0.10",
"material-ui-search-bar": "^1.0.0-beta.13",
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
"optimize-css-assets-webpack-plugin": "5.0.3",
"polished": "^3.4.2",
"polished": "3.5.1",
"qrcode.react": "1.0.0",
"query-string": "6.11.1",
"react": "16.13.0",
"react-dev-utils": "^10.0.0",
"react-dom": "16.13.0",
"react": "16.13.1",
"react-dev-utils": "10.2.1",
"react-dom": "16.13.1",
"react-final-form": "6.3.5",
"react-final-form-listeners": "^1.0.2",
"react-ga": "^2.7.0",
"react-hot-loader": "4.12.19",
"react-hot-loader": "4.12.20",
"react-qr-reader": "^2.2.1",
"react-redux": "7.2.0",
"react-router-dom": "5.1.2",
@ -92,7 +92,7 @@
},
"devDependencies": {
"@babel/cli": "7.8.4",
"@babel/core": "7.8.7",
"@babel/core": "7.9.0",
"@babel/plugin-proposal-class-properties": "7.8.3",
"@babel/plugin-proposal-decorators": "7.8.3",
"@babel/plugin-proposal-do-expressions": "7.8.3",
@ -104,7 +104,7 @@
"@babel/plugin-proposal-logical-assignment-operators": "7.8.3",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.8.3",
"@babel/plugin-proposal-numeric-separator": "7.8.3",
"@babel/plugin-proposal-optional-chaining": "7.8.3",
"@babel/plugin-proposal-optional-chaining": "7.9.0",
"@babel/plugin-proposal-pipeline-operator": "7.8.3",
"@babel/plugin-proposal-throw-expressions": "7.8.3",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
@ -112,15 +112,15 @@
"@babel/plugin-transform-member-expression-literals": "7.8.3",
"@babel/plugin-transform-property-literals": "7.8.3",
"@babel/polyfill": "7.8.7",
"@babel/preset-env": "7.8.7",
"@babel/preset-flow": "7.8.3",
"@babel/preset-react": "7.8.3",
"@testing-library/react": "9.5.0",
"autoprefixer": "9.7.4",
"@babel/preset-env": "7.9.0",
"@babel/preset-flow": "7.9.0",
"@babel/preset-react": "7.9.4",
"@testing-library/react": "10.0.1",
"autoprefixer": "9.7.5",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "10.1.0",
"babel-jest": "25.1.0",
"babel-loader": "8.0.6",
"babel-jest": "25.2.4",
"babel-loader": "8.1.0",
"babel-plugin-dynamic-import-node": "^2.3.0",
"babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
"babel-plugin-transform-es3-property-literals": "^6.22.0",
@ -130,38 +130,38 @@
"detect-port": "^1.3.0",
"dotenv-expand": "^5.1.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-flowtype": "^4.6.0",
"eslint-plugin-import": "^2.20.1",
"eslint-config-prettier": "6.10.1",
"eslint-plugin-flowtype": "4.7.0",
"eslint-plugin-import": "2.20.2",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-react": "^7.18.3",
"eslint-plugin-sort-destructure-keys": "^1.3.3",
"ethereumjs-abi": "0.6.8",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "5.1.0",
"flow-bin": "0.120.1",
"fs-extra": "8.1.0",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"file-loader": "6.0.0",
"flow-bin": "0.121.0",
"fs-extra": "9.0.0",
"html-loader": "1.0.0",
"html-webpack-plugin": "4.0.3",
"husky": "^4.2.2",
"jest": "25.1.0",
"jest": "25.2.4",
"jest-dom": "4.0.0",
"json-loader": "^0.5.7",
"mini-css-extract-plugin": "0.9.0",
"postcss-loader": "^3.0.0",
"postcss-mixins": "6.2.3",
"postcss-simple-vars": "^5.0.2",
"prettier": "^1.19.1",
"prettier": "2.0.2",
"run-with-testrpc": "0.3.1",
"style-loader": "1.1.3",
"terser-webpack-plugin": "2.3.5",
"truffle": "5.1.16",
"truffle": "5.1.19",
"truffle-contract": "4.0.31",
"truffle-solidity-loader": "0.1.32",
"url-loader": "3.0.0",
"webpack": "4.42.0",
"webpack-bundle-analyzer": "3.6.0",
"url-loader": "4.0.0",
"webpack": "4.42.1",
"webpack-bundle-analyzer": "3.6.1",
"webpack-cli": "3.3.11",
"webpack-dev-server": "3.10.3",
"webpack-manifest-plugin": "2.2.0"

View File

@ -1,6 +1,6 @@
# Gnosis Team Safe
# Gnosis Safe Multisig
The most secure way to manage your crypto funds collectively
The most trusted platform to store digital assets on Ethereum
## Getting Started

View File

@ -9,16 +9,16 @@ const Wrapper = styled.div`
const Icon = styled.img`
max-width: 15px;
max-height: 15px;
margin-right: 5px;
`
const Text = styled.span`
margin-left: 5px;
height: 17px;
`
const IconText = ({ iconUrl, text }: { iconUrl: string, text: string }) => (
const IconText = ({ iconUrl, text }: { iconUrl: string, text?: string }) => (
<Wrapper>
<Icon alt={text} src={iconUrl} />
<Text>{text}</Text>
{text && <Text>{text}</Text>}
</Wrapper>
)

View File

@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="91px" height="91px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<circle cx="84" cy="50" r="0.271746" fill="#d4d5d3">
<animate attributeName="r" repeatCount="indefinite" dur="1.7857142857142856s" calcMode="spline" keyTimes="0;1" values="10;0" keySplines="0 0.5 0.5 1" begin="0s"></animate>
<animate attributeName="fill" repeatCount="indefinite" dur="7.142857142857142s" calcMode="discrete" keyTimes="0;0.25;0.5;0.75;1" values="#d4d5d3;#d4d5d3;#d4d5d3;#d4d5d3;#d4d5d3" begin="0s"></animate>
</circle><circle cx="49.076" cy="50" r="10" fill="#d4d5d3">
<animate attributeName="r" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;10;10;10" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="0s"></animate>
<animate attributeName="cx" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="0s"></animate>
</circle><circle cx="83.076" cy="50" r="10" fill="#d4d5d3">
<animate attributeName="r" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;10;10;10" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-1.7857142857142856s"></animate>
<animate attributeName="cx" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-1.7857142857142856s"></animate>
</circle><circle cx="16" cy="50" r="0" fill="#d4d5d3">
<animate attributeName="r" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;10;10;10" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-3.571428571428571s"></animate>
<animate attributeName="cx" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-3.571428571428571s"></animate>
</circle><circle cx="16" cy="50" r="9.72825" fill="#d4d5d3">
<animate attributeName="r" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;10;10;10" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-5.357142857142857s"></animate>
<animate attributeName="cx" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-5.357142857142857s"></animate>
</circle>
<!-- [ldio] generated by https://loading.io/ --></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -2,6 +2,7 @@
export * from './dataDisplay'
export * from './feedback'
export * from './layouts'
export * from './navigation'
export * from './safeUtils'
export * from './surfaces'
export * from './utils'

View File

@ -37,7 +37,7 @@ type Props = {
const List = ({ activeItem, classes, items, onItemClick }: Props) => {
return (
<Wrapper>
{items.map(i => (
{items.map((i) => (
<Item
className={cn(classes.menuOption, activeItem === i.id && classes.active)}
key={i.id}

View File

@ -0,0 +1,48 @@
// @flow
import React from 'react'
import styled from 'styled-components'
import { IconText } from '~/components-v2'
import CheckIcon from '~/components/layout/PageFrame/assets/check.svg'
import {
background as backgroundColor,
secondaryText as disabledColor,
error as errorColor,
secondary,
} from '~/theme/variables'
const Circle = styled.div`
background-color: ${({ disabled, error }) => {
if (error) {
return errorColor
}
if (disabled) {
return disabledColor
}
return secondary
}};
color: ${backgroundColor};
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 5px;
`
type Props = {
dotIndex: number,
currentIndex: number,
error?: boolean,
}
const DotStep = ({ currentIndex, dotIndex, error }: Props) => {
return (
<Circle disabled={dotIndex > currentIndex} error={error}>
{dotIndex < currentIndex ? <IconText iconUrl={CheckIcon} /> : dotIndex + 1}
</Circle>
)
}
export default DotStep

View File

@ -0,0 +1,69 @@
// @flow
import StepMUI from '@material-ui/core/Step'
import StepLabelMUI from '@material-ui/core/StepLabel'
import StepperMUI from '@material-ui/core/Stepper'
import React from 'react'
import styled from 'styled-components'
import DotStep from './DotStep'
import { secondaryText as disabled, error as errorColor, primary, secondary } from '~/theme/variables'
const StyledStepper = styled(StepperMUI)`
background-color: transparent;
`
const StyledStepLabel = styled.p`
&& {
color: ${({ activeStepIndex, error, index }) => {
if (error) {
return errorColor
}
if (index === activeStepIndex) {
return secondary
}
if (index < activeStepIndex) {
return disabled
}
return primary
}};
}
`
type Props = {
steps: Array<{ id: string | number, label: string }>,
activeStepIndex: number,
error?: boolean,
orientation: 'vertical' | 'horizontal',
}
const Stepper = ({ activeStepIndex, error, orientation, steps }: Props) => {
return (
<StyledStepper activeStep={activeStepIndex} orientation={orientation}>
{steps.map((s, index) => {
return (
<StepMUI key={s.id}>
<StepLabelMUI
icon={
<DotStep currentIndex={activeStepIndex} dotIndex={index} error={index === activeStepIndex && error} />
}
>
<StyledStepLabel
activeStepIndex={activeStepIndex}
error={index === activeStepIndex && error}
index={index}
>
{s.label}
</StyledStepLabel>
</StepLabelMUI>
</StepMUI>
)
})}
</StyledStepper>
)
}
export default Stepper

View File

@ -0,0 +1,2 @@
// @flow
export { default as Stepper } from './Stepper'

View File

@ -23,7 +23,7 @@ export const onboard = new Onboard({
dappId: BLOCKNATIVE_API_KEY,
networkId: getNetworkId(),
subscriptions: {
wallet: wallet => {
wallet: (wallet) => {
if (wallet.provider) {
// this function will intialize web3 and store it somewhere available throughout the dapp and
// can also instantiate your contracts with the web3 instance
@ -31,7 +31,7 @@ export const onboard = new Onboard({
providerName = wallet.name
}
},
address: address => {
address: (address) => {
if (!lastUsedAddress && address) {
lastUsedAddress = address
store.dispatch(fetchProvider(providerName))
@ -45,6 +45,7 @@ export const onboard = new Onboard({
},
},
walletSelect: {
description: 'Please select a wallet to connect to Gnosis Safe Multisig',
wallets,
},
walletCheck: [{ checkName: 'connect' }, transactionDataCheck(), { checkName: 'network' }, { checkName: 'accounts' }],

View File

@ -160,7 +160,7 @@ const CookiesBanner = () => {
disabled
label="Necessary"
name="Necessary"
onChange={() => setLocalNecessary(prev => !prev)}
onChange={() => setLocalNecessary((prev) => !prev)}
value={localNecessary}
/>
</div>
@ -169,7 +169,7 @@ const CookiesBanner = () => {
control={<Checkbox checked={localAnalytics} />}
label="Analytics"
name="Analytics"
onChange={() => setLocalAnalytics(prev => !prev)}
onChange={() => setLocalAnalytics((prev) => !prev)}
value={localAnalytics}
/>
</div>

View File

@ -77,7 +77,7 @@ const Layout = openHoc(({ classes, clickAway, open, providerDetails, providerInf
<NetworkLabel />
<Spacer />
<Provider info={providerInfo} open={open} toggle={toggle}>
{providerRef => (
{(providerRef) => (
<Popper
anchorEl={providerRef.current}
className={classes.popper}

View File

@ -65,6 +65,6 @@ const SafeListHeader = ({ safesCount }: Props) => {
export default connect<Object, Object, ?Function, ?Object>(
// $FlowFixMe
state => ({ safesCount: safesCountSelector(state) }),
(state) => ({ safesCount: safesCountSelector(state) }),
null,
)(SafeListHeader)

View File

@ -1,11 +1,11 @@
/* eslint-disable */
;(function(global, factory) {
;(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined'
? factory(exports)
: typeof define === 'function' && define.amd
? define(['exports'], factory)
: factory((global.blockies = {}))
})(this, function(exports) {
})(this, function (exports) {
'use strict'
/**
@ -40,7 +40,7 @@
return String.fromCharCode(w & 255, (w >> 8) & 255)
}
var PNG = function(width, height, depth) {
var PNG = function (width, height, depth) {
this.width = width
this.height = height
this.depth = depth
@ -115,14 +115,14 @@
}
// compute the index into a png for a given pixel
this.index = function(x, y) {
this.index = function (x, y) {
var i = y * (this.width + 1) + x + 1
var j = this.idat_offs + 8 + 2 + 5 * Math.floor(i / 0xffff + 1) + i
return j
}
// convert a color and build up the palette
this.color = function(red, green, blue, alpha) {
this.color = function (red, green, blue, alpha) {
alpha = alpha >= 0 ? alpha : 255
var color = (((((alpha << 8) | red) << 8) | green) << 8) | blue
@ -142,7 +142,7 @@
}
// output a PNG string, Base64 encoded
this.getBase64 = function() {
this.getBase64 = function () {
var s = this.getDump()
var ch = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
@ -173,7 +173,7 @@
}
// output a PNG string
this.getDump = function() {
this.getDump = function () {
// compute adler32 of output pixels + row filter bytes
var BASE = 65521 /* largest prime smaller than 65536 */
var NMAX = 5552 /* NMAX is the largest n such that 255n(n+1)/2 + (n+1)(BASE-1) <= 2^32-1 */
@ -215,7 +215,7 @@
return '\x89PNG\r\n\x1A\n' + this.buffer.join('')
}
this.fillRect = function(x, y, w, h, color) {
this.fillRect = function (x, y, w, h, color) {
for (var i = 0; i < w; i++) {
for (var j = 0; j < h; j++) {
this.buffer[this.index(x + i, y + j)] = color

View File

@ -45,7 +45,7 @@ class Notifier extends Component<Props> {
componentDidUpdate() {
const { notifications = [], enqueueSnackbar, removeSnackbar } = this.props
notifications.forEach(notification => {
notifications.forEach((notification) => {
// Do nothing if snackbar is already displayed
if (this.displayed.includes(notification.key)) {
return
@ -68,7 +68,7 @@ class Notifier extends Component<Props> {
})
}
storeDisplayed = id => {
storeDisplayed = (id) => {
this.displayed = [...this.displayed, id]
}

View File

@ -72,10 +72,10 @@ const ScanQRModal = ({ classes, isOpen, onClose, onScan }: Props) => {
) : (
<QrReader
legacyMode={!hasWebcam}
onError={err => {
onError={(err) => {
console.error(err)
}}
onScan={data => {
onScan={(data) => {
if (data) onScan(data)
}}
ref={scannerRef}

View File

@ -79,7 +79,7 @@ const SafeList = ({ currentSafe, defaultSafe, onSafeClick, safes, setDefaultSafe
return (
<MuiList className={classes.list}>
{safes.map(safe => (
{safes.map((safe) => (
<React.Fragment key={safe.address}>
<Link
data-testid={SIDEBAR_SAFELIST_ROW_TESTID}
@ -113,7 +113,7 @@ const SafeList = ({ currentSafe, defaultSafe, onSafeClick, safes, setDefaultSafe
) : (
<ButtonLink
className={classes.makeDefaultBtn}
onClick={e => {
onClick={(e) => {
e.preventDefault()
e.stopPropagation()

View File

@ -69,7 +69,7 @@ const Sidebar = ({ children, currentSafe, defaultSafe, safes, setDefaultSafeActi
}
const toggleSidebar = () => {
setIsOpen(prevIsOpen => !prevIsOpen)
setIsOpen((prevIsOpen) => !prevIsOpen)
}
const handleFilterChange = (value: string) => {
@ -142,7 +142,7 @@ const Sidebar = ({ children, currentSafe, defaultSafe, safes, setDefaultSafeActi
export default connect<Object, Object, ?Function, ?Object>(
// $FlowFixMe
state => ({
(state) => ({
safes: sortedSafeListSelector(state),
defaultSafe: defaultSafeSelector(state),
currentSafe: safeParamAddressFromStateSelector(state),

View File

@ -54,7 +54,7 @@ const GnoStepper = (props: Props) => {
const getPageProps = (pages: React.Node): PageProps => React.Children.toArray(pages)[page].props
const updateInitialProps = newInitialProps => {
const updateInitialProps = (newInitialProps) => {
setValues(newInitialProps)
}
@ -107,7 +107,7 @@ const GnoStepper = (props: Props) => {
return next(formValues)
}
const isLastPage = pageNumber => {
const isLastPage = (pageNumber) => {
const { steps } = props
return pageNumber === steps.length - 1
}

View File

@ -39,7 +39,7 @@ export const stableSort = (dataArray: List<any>, cmp: any, fixed: boolean): List
return a[1] - b[1]
})
const sortedElems: List<any> = stabilizedThis.map(el => el[0])
const sortedElems: List<any> = stabilizedThis.map((el) => el[0])
return fixedElems.concat(sortedElems)
}

View File

@ -20,7 +20,7 @@ type Props = {
disabled?: boolean,
}
const isValidEnsName = name => /^([\w-]+\.)+(eth|test|xyz|luxe)$/.test(name)
const isValidEnsName = (name) => /^([\w-]+\.)+(eth|test|xyz|luxe)$/.test(name)
// an idea for second field was taken from here
// https://github.com/final-form/react-final-form-listeners/blob/master/src/OnBlur.js
@ -52,7 +52,7 @@ const AddressInput = ({
validate={composeValidators(required, mustBeEthereumAddress, ...validators)}
/>
<OnChange name={name}>
{async value => {
{async (value) => {
if (isValidEnsName(value)) {
try {
const resolverAddr = await getAddressFromENS(value)

View File

@ -8,10 +8,10 @@ import { getWeb3 } from '~/logic/wallets/getWeb3'
export const simpleMemoize = (fn: Function) => {
let lastArg
let lastResult
return (arg: any) => {
return (arg: any, ...args: any) => {
if (arg !== lastArg) {
lastArg = arg
lastResult = fn(arg)
lastResult = fn(arg, ...args)
}
return lastResult
}
@ -84,7 +84,7 @@ export const ADDRESS_REPEATED_ERROR = 'Address already introduced'
export const uniqueAddress = (addresses: string[] | List<string>) =>
simpleMemoize((value: string) => {
const addressAlreadyExists = addresses.some(address => sameAddress(value, address))
const addressAlreadyExists = addresses.some((address) => sameAddress(value, address))
return addressAlreadyExists ? ADDRESS_REPEATED_ERROR : undefined
})

View File

@ -80,7 +80,7 @@ const PageFrame = ({ children, classes, currentNetwork }: Props) => {
export default withStyles(notificationStyles)(
connect(
state => ({
(state) => ({
currentNetwork: networkSelector(state),
}),
null,

View File

@ -1,5 +1,5 @@
// @flow
import type { RecordOf } from 'immutable'
import { Record, type RecordFactory, type RecordOf } from 'immutable'
export type AddressBookEntry = {
address: string,
@ -11,4 +11,10 @@ export type AddressBookProps = {
addressBook: Map<string, AddressBookEntry>,
}
export const makeAddressBookEntry: RecordFactory<AddressBookEntry> = Record({
address: '',
name: '',
isOwner: false,
})
export type AddressBook = RecordOf<AddressBookProps>

View File

@ -0,0 +1,14 @@
// @flow
import { createAction } from 'redux-actions'
import type { AddressBookEntryType } from '~/logic/addressBook/model/addressBook'
export const ADD_OR_UPDATE_ENTRY = 'ADD_OR_UPDATE_ENTRY'
export const addOrUpdateAddressBookEntry = createAction<string, *, *>(
ADD_OR_UPDATE_ENTRY,
(entryAddress: string, entry: AddressBookEntryType): AddressBookEntryType => ({
entryAddress,
entry,
}),
)

View File

@ -19,7 +19,7 @@ const loadAddressBookFromStorage = () => async (dispatch: ReduxDispatch<GlobalSt
// Fetch all the current safes, in case that we don't have a safe on the adbk, we add it
const safes = safesListSelector(state)
const adbkEntries = addressBook.keySeq().toArray()
safes.forEach(safe => {
safes.forEach((safe) => {
const { address } = safe
const found = adbkEntries.includes(address)
if (!found) {

View File

@ -2,13 +2,14 @@
import type { Dispatch as ReduxDispatch } from 'redux'
import type { AddressBook } from '~/logic/addressBook/model/addressBook'
import { makeAddressBookEntry } from '~/logic/addressBook/model/addressBook'
import { updateAddressBookEntry } from '~/logic/addressBook/store/actions/updateAddressBookEntry'
import { saveAddressBook } from '~/logic/addressBook/utils'
import { type GlobalState } from '~/store/index'
const saveAndUpdateAddressBook = (addressBook: AddressBook) => async (dispatch: ReduxDispatch<GlobalState>) => {
try {
dispatch(updateAddressBookEntry(addressBook))
dispatch(updateAddressBookEntry(makeAddressBookEntry(addressBook)))
await saveAddressBook(addressBook)
} catch (err) {
// eslint-disable-next-line

View File

@ -3,6 +3,7 @@ import type { AnyAction, Store } from 'redux'
import type { AddressBookProps } from '~/logic/addressBook/model/addressBook'
import { ADD_ENTRY } from '~/logic/addressBook/store/actions/addAddressBookEntry'
import { ADD_OR_UPDATE_ENTRY } from '~/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
import { REMOVE_ENTRY } from '~/logic/addressBook/store/actions/removeAddressBookEntry'
import { UPDATE_ENTRY } from '~/logic/addressBook/store/actions/updateAddressBookEntry'
import { addressBookMapSelector } from '~/logic/addressBook/store/selectors'
@ -10,9 +11,9 @@ import { saveAddressBook } from '~/logic/addressBook/utils'
import { enhanceSnackbarForAction, getNotificationsFromTxType } from '~/logic/notifications'
import enqueueSnackbar from '~/logic/notifications/store/actions/enqueueSnackbar'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import { type GlobalState } from '~/store/'
import { type GlobalState } from '~/store'
const watchedActions = [ADD_ENTRY, REMOVE_ENTRY, UPDATE_ENTRY]
const watchedActions = [ADD_ENTRY, REMOVE_ENTRY, UPDATE_ENTRY, ADD_OR_UPDATE_ENTRY]
const addressBookMiddleware = (store: Store<GlobalState>) => (next: Function) => async (action: AnyAction) => {
const handledAction = next(action)

View File

@ -3,8 +3,10 @@ import { List, Map } from 'immutable'
import { type ActionType, handleActions } from 'redux-actions'
import type { AddressBook, AddressBookEntry, AddressBookProps } from '~/logic/addressBook/model/addressBook'
import { makeAddressBookEntry } from '~/logic/addressBook/model/addressBook'
import { ADD_ADDRESS_BOOK } from '~/logic/addressBook/store/actions/addAddressBook'
import { ADD_ENTRY } from '~/logic/addressBook/store/actions/addAddressBookEntry'
import { ADD_OR_UPDATE_ENTRY } from '~/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
import { LOAD_ADDRESS_BOOK } from '~/logic/addressBook/store/actions/loadAddressBook'
import { REMOVE_ENTRY } from '~/logic/addressBook/store/actions/removeAddressBookEntry'
import { UPDATE_ENTRY } from '~/logic/addressBook/store/actions/updateAddressBookEntry'
@ -19,7 +21,8 @@ export const buildAddressBook = (storedAdbk: AddressBook): AddressBookProps => {
let addressBookBuilt = Map([])
Object.entries(storedAdbk).forEach((adbkProps: Array<string, AddressBookEntry[]>) => {
const safeAddress = adbkProps[0]
const adbkSafeEntries = List(adbkProps[1])
const adbkRecords = adbkProps[1].map(makeAddressBookEntry)
const adbkSafeEntries = List(adbkRecords)
addressBookBuilt = addressBookBuilt.set(safeAddress, adbkSafeEntries)
})
return addressBookBuilt
@ -44,11 +47,11 @@ export default handleActions<State, *>(
const { entry } = action.payload
// Adds the entry to all the safes (if it does not already exists)
const newState = state.withMutations(map => {
const newState = state.withMutations((map) => {
const adbkMap = map.get('addressBook')
if (adbkMap) {
adbkMap.keySeq().forEach(safeAddress => {
adbkMap.keySeq().forEach((safeAddress) => {
const safeAddressBook = state.getIn(['addressBook', safeAddress])
if (safeAddressBook) {
@ -68,13 +71,13 @@ export default handleActions<State, *>(
const { entry } = action.payload
// Updates the entry from all the safes
const newState = state.withMutations(map => {
const newState = state.withMutations((map) => {
map
.get('addressBook')
.keySeq()
.forEach(safeAddress => {
.forEach((safeAddress) => {
const entriesList: List<AddressBookEntry> = state.getIn(['addressBook', safeAddress])
const entryIndex = entriesList.findIndex(entryItem => sameAddress(entryItem.address, entry.address))
const entryIndex = entriesList.findIndex((entryItem) => sameAddress(entryItem.address, entry.address))
const updatedEntriesList = entriesList.set(entryIndex, entry)
map.setIn(['addressBook', safeAddress], updatedEntriesList)
})
@ -85,19 +88,41 @@ export default handleActions<State, *>(
[REMOVE_ENTRY]: (state: State, action: ActionType<Function>): State => {
const { entryAddress } = action.payload
// Removes the entry from all the safes
const newState = state.withMutations(map => {
const newState = state.withMutations((map) => {
map
.get('addressBook')
.keySeq()
.forEach(safeAddress => {
.forEach((safeAddress) => {
const entriesList = state.getIn(['addressBook', safeAddress])
const entryIndex = entriesList.findIndex(entry => sameAddress(entry.address, entryAddress))
const entryIndex = entriesList.findIndex((entry) => sameAddress(entry.address, entryAddress))
const updatedEntriesList = entriesList.remove(entryIndex)
map.setIn(['addressBook', safeAddress], updatedEntriesList)
})
})
return newState
},
[ADD_OR_UPDATE_ENTRY]: (state: State, action: ActionType<Function>): State => {
const { entry, entryAddress } = action.payload
// Adds or Updates the entry to all the safes
return state.withMutations((map) => {
const addressBook = map.get('addressBook')
if (addressBook) {
addressBook.keySeq().forEach((safeAddress) => {
const safeAddressBook: List<AddressBookEntry> = state.getIn(['addressBook', safeAddress])
const entryIndex = safeAddressBook.findIndex((entryItem) => sameAddress(entryItem.address, entryAddress))
if (entryIndex !== -1) {
const updatedEntriesList = safeAddressBook.update(entryIndex, (currentEntry) => currentEntry.merge(entry))
map.setIn(['addressBook', safeAddress], updatedEntriesList)
} else {
const updatedSafeAdbkList = safeAddressBook.push(makeAddressBookEntry(entry))
map.setIn(['addressBook', safeAddress], updatedSafeAdbkList)
}
})
}
})
},
},
Map(),
)

View File

@ -41,7 +41,7 @@ export const getNameFromAddressBook = (userAddress: string): string | null => {
return null
}
const addressBook = useSelector(getAddressBook)
const result = addressBook.filter(addressBookItem => addressBookItem.address === userAddress)
const result = addressBook.filter((addressBookItem) => addressBookItem.address === userAddress)
if (result.size > 0) {
return result.get(0).name
}

View File

@ -23,10 +23,10 @@ export const saveAddressBook = async (addressBook: AddressBook) => {
}
export const getAddressesListFromAdbk = (addressBook: AddressBook) =>
Array.from(addressBook).map(entry => entry.address)
Array.from(addressBook).map((entry) => entry.address)
const getNameFromAdbk = (addressBook: AddressBook, userAddress: string): string | null => {
const entry = addressBook.find(addressBookItem => addressBookItem.address === userAddress)
const entry = addressBook.find((addressBookItem) => addressBookItem.address === userAddress)
if (entry) {
return entry.name
}
@ -45,7 +45,7 @@ export const getOwnersWithNameFromAddressBook = (addressBook: AddressBook, owner
if (!ownerList) {
return []
}
const ownersListWithAdbkNames = ownerList.map(owner => {
const ownersListWithAdbkNames = ownerList.map((owner) => {
const ownerName = getNameFromAdbk(addressBook, owner.address)
return {
address: owner.address,

View File

@ -1,6 +1,38 @@
// @flow
import { List, Set } from 'immutable'
import type { Selector } from 'reselect'
import { createSelector } from 'reselect'
import { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID } from '~/logic/collectibles/store/reducer/collectibles'
import type { NFTAssets } from '~/routes/safe/components/Balances/Collectibles/types'
import { safeActiveAssetsSelector } from '~/routes/safe/store/selectors'
import type { GlobalState } from '~/store'
export const nftAssetsSelector = (state: GlobalState) => state[NFT_ASSETS_REDUCER_ID]
export const nftTokensSelector = (state: GlobalState) => state[NFT_TOKENS_REDUCER_ID]
export const nftAssetsListSelector: Selector<GlobalState, NFTAssets, List<NFTAssets>> = createSelector(
nftAssetsSelector,
(assets: NFTAssets) => {
return assets ? List(Object.entries(assets).map((item) => item[1])) : List([])
},
)
export const activeNftAssetsListSelector: Selector<GlobalState, NFTAssets, List<NFTAssets>> = createSelector(
nftAssetsListSelector,
safeActiveAssetsSelector,
(assets: List<NFTAssets>, activeAssetsList: Set<string>) => {
return assets.filter((asset) => activeAssetsList.has(asset.address))
},
)
export const safeActiveSelectorMap: Selector<GlobalState, NFTAssets, List<NFTAssets>> = createSelector(
activeNftAssetsListSelector,
(activeAssets: List<NFTAssets>) => {
let assetsMap = {}
activeAssets.forEach((asset) => {
assetsMap[asset.address] = asset
})
return assetsMap
},
)

View File

@ -5,7 +5,7 @@ import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.
import SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxy.json'
import { ensureOnce } from '~/utils/singleton'
import { simpleMemoize } from '~/components/forms/validator'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { getWeb3, getNetworkIdFrom } from '~/logic/wallets/getWeb3'
import { calculateGasOf, calculateGasPrice } from '~/logic/wallets/ethTransactions'
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
import { isProxyCode } from '~/logic/contracts/historicProxyCode'
@ -27,9 +27,9 @@ const createGnosisSafeContract = (web3: any) => {
return gnosisSafe
}
const createProxyFactoryContract = (web3: any) => {
const proxyFactory = contract(ProxyFactorySol)
proxyFactory.setProvider(web3.currentProvider)
const createProxyFactoryContract = (web3: any, networkId: number) => {
const contractAddress = ProxyFactorySol.networks[networkId].address
const proxyFactory = new web3.eth.Contract(ProxyFactorySol.abi, contractAddress)
return proxyFactory
}
@ -39,10 +39,10 @@ const getCreateProxyFactoryContract = simpleMemoize(createProxyFactoryContract)
const instantiateMasterCopies = async () => {
const web3 = getWeb3()
const networkId = await getNetworkIdFrom(web3)
// Create ProxyFactory Master Copy
const ProxyFactory = getCreateProxyFactoryContract(web3)
proxyFactoryMaster = await ProxyFactory.deployed()
proxyFactoryMaster = getCreateProxyFactoryContract(web3, networkId)
// Initialize Safe master copy
const GnosisSafe = getGnosisSafeContract(web3)
@ -70,22 +70,12 @@ export const getSafeMasterContract = async () => {
return safeMaster
}
export const deploySafeContract = async (safeAccounts: string[], numConfirmations: number, userAccount: string) => {
const gnosisSafeData = await safeMaster.contract.methods
export const getSafeDeploymentTransaction = (safeAccounts: string[], numConfirmations: number, userAccount: string) => {
const gnosisSafeData = safeMaster.contract.methods
.setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS)
.encodeABI()
const proxyFactoryData = proxyFactoryMaster.contract.methods
.createProxy(safeMaster.address, gnosisSafeData)
.encodeABI()
const gas = await calculateGasOf(proxyFactoryData, userAccount, proxyFactoryMaster.address)
const gasPrice = await calculateGasPrice()
.encodeABI()
return proxyFactoryMaster.createProxy(safeMaster.address, gnosisSafeData, {
from: userAccount,
gas,
gasPrice,
value: 0,
})
return proxyFactoryMaster.methods.createProxy(safeMaster.address, gnosisSafeData)
}
export const estimateGasForDeployingSafe = async (
@ -93,10 +83,11 @@ export const estimateGasForDeployingSafe = async (
numConfirmations: number,
userAccount: string,
) => {
console.log(proxyFactoryMaster)
const gnosisSafeData = await safeMaster.contract.methods
.setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS)
.encodeABI()
const proxyFactoryData = proxyFactoryMaster.contract.methods
const proxyFactoryData = proxyFactoryMaster.methods
.createProxy(safeMaster.address, gnosisSafeData)
.encodeABI()
const gas = await calculateGasOf(proxyFactoryData, userAccount, proxyFactoryMaster.address)

View File

@ -18,8 +18,8 @@ export const fetchCurrencyValues = (safeAddress: string) => async (dispatch: Red
// eslint-disable-next-line max-len
const currencyList = List(
tokensFetched.data
.filter(currencyBalance => currencyBalance.balanceUsd)
.map(currencyBalance => {
.filter((currencyBalance) => currencyBalance.balanceUsd)
.map((currencyBalance) => {
const { balanceUsd, tokenAddress } = currencyBalance
return makeBalanceCurrency({
currencyName: balanceUsd ? AVAILABLE_CURRENCIES.USD : null,

View File

@ -16,7 +16,7 @@ export default handleActions<State, *>(
[UPDATE_VIEWED_SAFES]: (state: State, action: ActionType<Function>): State => {
const safeAddress = action.payload
const newState = state.updateIn(['viewedSafes'], prev =>
const newState = state.updateIn(['viewedSafes'], (prev) =>
prev.includes(safeAddress) ? prev : [...prev, safeAddress],
)

View File

@ -231,7 +231,7 @@ export const showSnackbar = (notification: Notification, enqueueSnackbar: Functi
enqueueSnackbar(notification.message, {
...notification.options,
// eslint-disable-next-line react/display-name
action: key => (
action: (key) => (
<IconButton onClick={() => closeSnackbar(key)}>
<IconClose />
</IconButton>

View File

@ -23,10 +23,10 @@ export default handleActions<NotificationReducerState, *>(
const { dismissAll, key } = action.payload
if (key) {
return state.update(key, prev => prev.set('dismissed', true))
return state.update(key, (prev) => prev.set('dismissed', true))
}
if (dismissAll) {
return state.withMutations(map => {
return state.withMutations((map) => {
map.forEach((notification, notificationKey) => {
map.set(notificationKey, notification.set('dismissed', true))
})

View File

@ -23,7 +23,7 @@ export const generateSignaturesFromTxConfirmations = (
let sigs = '0x'
Object.keys(confirmationsMap)
.sort()
.forEach(addr => {
.forEach((addr) => {
const conf = confirmationsMap[addr]
if (conf.signature) {
sigs += conf.signature.slice(2)

View File

@ -12,22 +12,23 @@ export const getAwaitingTransactions = (
return Map({})
}
const allAwaitingTransactions = allTransactions.map(safeTransactions => {
const allAwaitingTransactions = allTransactions.map((safeTransactions) => {
const nonCancelledTransactions = safeTransactions.filter((transaction: Transaction) => {
// If transactions are not executed, but there's a transaction with the same nonce EXECUTED later
// it means that the transaction was cancelled (Replaced) and shouldn't get executed
let isTransactionCancelled = false
if (!transaction.isExecuted) {
if (cancellationTransactionsByNonce.get(transaction.nonce)) {
// eslint-disable-next-line no-param-reassign
transaction = transaction.set('cancelled', true)
isTransactionCancelled = true
}
}
// The transaction is not executed and is not cancelled, so it's still waiting confirmations
if (!transaction.executionTxHash && !transaction.cancelled) {
if (!transaction.executionTxHash && !isTransactionCancelled) {
// Then we check if the waiting confirmations are not from the current user, otherwise, filters this
// transaction
const transactionWaitingUser = transaction.confirmations.filter(
confirmation => confirmation.owner && confirmation.owner.address !== userAccount,
(confirmation) => confirmation.owner && confirmation.owner.address !== userAccount,
)
return transactionWaitingUser.size > 0

View File

@ -5,7 +5,7 @@ import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { getWeb3 } from '~/logic/wallets/getWeb3'
const estimateDataGasCosts = data => {
const estimateDataGasCosts = (data) => {
const reducer = (accumulator, currentValue) => {
if (currentValue === EMPTY_DATA) {
return accumulator + 0

View File

@ -1,6 +1,6 @@
// @flow
export * from './gas'
export * from './send'
export * from './safeTxSignerEIP712'
export * from './offchainSigner'
export * from './txHistory'
export * from './notifiedTransactions'

View File

@ -0,0 +1,100 @@
// @flow
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { getWeb3 } from '~/logic/wallets/getWeb3'
const EIP712_NOT_SUPPORTED_ERROR_MSG = "EIP712 is not supported by user's wallet"
const generateTypedDataFrom = async ({
baseGas,
data,
gasPrice,
gasToken,
nonce,
operation,
refundReceiver,
safeAddress,
safeTxGas,
to,
valueInWei,
}) => {
const typedData = {
types: {
EIP712Domain: [
{
type: 'address',
name: 'verifyingContract',
},
],
SafeTx: [
{ type: 'address', name: 'to' },
{ type: 'uint256', name: 'value' },
{ type: 'bytes', name: 'data' },
{ type: 'uint8', name: 'operation' },
{ type: 'uint256', name: 'safeTxGas' },
{ type: 'uint256', name: 'baseGas' },
{ type: 'uint256', name: 'gasPrice' },
{ type: 'address', name: 'gasToken' },
{ type: 'address', name: 'refundReceiver' },
{ type: 'uint256', name: 'nonce' },
],
},
domain: {
verifyingContract: safeAddress,
},
primaryType: 'SafeTx',
message: {
to,
value: valueInWei,
data,
operation,
safeTxGas,
baseGas,
gasPrice,
gasToken,
refundReceiver,
nonce: Number(nonce),
},
}
return typedData
}
type EIP712RpcCallVersion = 'v3' | 'v4'
export const getEIP712Signer = (version: ?EIP712RpcCallVersion) => async (txArgs) => {
const web3 = getWeb3()
const typedData = await generateTypedDataFrom(txArgs)
let method = 'eth_signTypedData_v3'
if (version === 'v4') {
method = 'eth_signTypedData_v4'
}
if (!version) {
method = 'eth_signTypedData'
}
const jsonTypedData = JSON.stringify(typedData)
const signedTypedData = {
jsonrpc: '2.0',
method,
params: version === 'v3' || version === 'v4' ? [txArgs.sender, jsonTypedData] : [jsonTypedData, txArgs.sender],
from: txArgs.sender,
id: new Date().getTime(),
}
return new Promise((resolve, reject) => {
web3.currentProvider.sendAsync(signedTypedData, (err, signature) => {
if (err) {
reject(err)
return
}
if (signature.result == null) {
reject(new Error(EIP712_NOT_SUPPORTED_ERROR_MSG))
return
}
resolve(signature.result.replace(EMPTY_DATA, ''))
})
})
}

View File

@ -0,0 +1,60 @@
// @flow
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { getWeb3 } from '~/logic/wallets/getWeb3'
const ETH_SIGN_NOT_SUPPORTED_ERROR_MSG = 'ETH_SIGN_NOT_SUPPORTED'
export const getEthSigner = async ({
baseGas,
data,
gasPrice,
gasToken,
nonce,
operation,
refundReceiver,
safeInstance,
safeTxGas,
sender,
to,
valueInWei,
}) => {
const web3 = await getWeb3()
const txHash = await safeInstance.getTransactionHash(
to,
valueInWei,
data,
operation,
safeTxGas,
baseGas,
gasPrice,
gasToken,
refundReceiver,
nonce,
{
from: sender,
},
)
return new Promise(function (resolve, reject) {
web3.currentProvider.sendAsync(
{
jsonrpc: '2.0',
method: 'eth_sign',
params: [sender, txHash],
id: new Date().getTime(),
},
async function (err, signature) {
if (err) {
return reject(err)
}
if (signature.result == null) {
reject(new Error(ETH_SIGN_NOT_SUPPORTED_ERROR_MSG))
return
}
resolve(signature.result.replace(EMPTY_DATA, ''))
},
)
})
}

View File

@ -0,0 +1,31 @@
// @flow
import { getEIP712Signer } from './EIP712Signer'
import { getEthSigner } from './ethSigner'
// 1. we try to sign via EIP-712 if user's wallet supports it
// 2. If not, try to use eth_sign (Safe version has to be >1.1.1)
// If eth_sign, doesn't work continue with the regular flow (on-chain signatures, more in createTransaction.js)
const signingFuncs = [getEIP712Signer('v3'), getEIP712Signer('v4'), getEIP712Signer(), getEthSigner]
export const SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES = '>=1.1.1'
export const tryOffchainSigning = async (txArgs) => {
let signature
for (let signingFunc of signingFuncs) {
try {
signature = await signingFunc(txArgs)
break
} catch (err) {
console.error(err)
// Metamask sign request error code
if (err.code === 4001) {
throw new Error('User denied sign request')
}
}
}
return signature
}

View File

@ -1,98 +0,0 @@
// @flow
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { getWeb3 } from '~/logic/wallets/getWeb3'
const generateTypedDataFrom = async (
safe: any,
safeAddress: string,
to: string,
valueInWei: number,
nonce: number,
data: string,
operation: number,
txGasEstimate: number,
) => {
const txGasToken = 0
// const threshold = await safe.getThreshold()
// estimateDataGas(safe, to, valueInWei, data, operation, txGasEstimate, txGasToken, nonce, threshold)
const dataGasEstimate = 0
const gasPrice = 0
const refundReceiver = 0
const typedData = {
types: {
EIP712Domain: [
{
type: 'address',
name: 'verifyingContract',
},
],
SafeTx: [
{ type: 'address', name: 'to' },
{ type: 'uint256', name: 'value' },
{ type: 'bytes', name: 'data' },
{ type: 'uint8', name: 'operation' },
{ type: 'uint256', name: 'safeTxGas' },
{ type: 'uint256', name: 'dataGas' },
{ type: 'uint256', name: 'gasPrice' },
{ type: 'address', name: 'gasToken' },
{ type: 'address', name: 'refundReceiver' },
{ type: 'uint256', name: 'nonce' },
],
},
domain: {
verifyingContract: safeAddress,
},
primaryType: 'SafeTx',
message: {
to,
value: Number(valueInWei),
data,
operation,
safeTxGas: txGasEstimate,
dataGas: dataGasEstimate,
gasPrice,
gasToken: txGasToken,
refundReceiver,
nonce: Number(nonce),
},
}
return typedData
}
export const generateMetamaskSignature = async (
safe: any,
safeAddress: string,
sender: string,
to: string,
valueInWei: number,
nonce: number,
data: string,
operation: number,
txGasEstimate: number,
) => {
const web3 = getWeb3()
const typedData = await generateTypedDataFrom(
safe,
safeAddress,
to,
valueInWei,
nonce,
data,
operation,
txGasEstimate,
)
const jsonTypedData = JSON.stringify(typedData)
const signedTypedData = {
method: 'eth_signTypedData_v3',
// To change once Metamask fixes their status
// https://github.com/MetaMask/metamask-extension/pull/5368
// https://github.com/MetaMask/metamask-extension/issues/5366
params: [sender, jsonTypedData],
from: sender,
}
const txSignedResponse = await web3.currentProvider.sendAsync(signedTypedData)
return txSignedResponse.result.replace(EMPTY_DATA, '')
}

View File

@ -54,6 +54,7 @@ export const getApprovalTransaction = async ({
try {
const web3 = getWeb3()
const contract = new web3.eth.Contract(GnosisSafeSol.abi, safeInstance.address)
return contract.methods.approveHash(txHash)

View File

@ -19,10 +19,10 @@ const calculateBodyFrom = async (
gasPrice: string | number,
gasToken: string,
refundReceiver: string,
transactionHash: string,
transactionHash: string | null,
sender: string,
confirmationType: TxServiceType,
origin: string | null,
signature: ?string,
) => {
const contractTransactionHash = await safeInstance.getTransactionHash(
to,
@ -51,8 +51,8 @@ const calculateBodyFrom = async (
contractTransactionHash,
transactionHash,
sender: getWeb3().utils.toChecksumAddress(sender),
confirmationType,
origin,
signature,
}
}
@ -75,9 +75,9 @@ export const saveTxToHistory = async ({
safeInstance,
safeTxGas,
sender,
signature,
to,
txHash,
type,
valueInWei,
}: {
safeInstance: any,
@ -91,10 +91,10 @@ export const saveTxToHistory = async ({
gasPrice: string | number,
gasToken: string,
refundReceiver: string,
txHash: string,
txHash: string | null,
sender: string,
type: TxServiceType,
origin: string | null,
signature: ?string,
}) => {
const url = buildTxServiceUrl(safeInstance.address)
const body = await calculateBodyFrom(
@ -109,10 +109,10 @@ export const saveTxToHistory = async ({
gasPrice,
gasToken,
refundReceiver,
txHash,
txHash || null,
sender,
type,
origin || null,
signature,
)
const response = await axios.post(url, body)

View File

@ -22,7 +22,7 @@ export const safeNeedsUpdate = (currentVersion: string, latestVersion: string) =
return latest ? semverLessThan(current, latest) : false
}
export const getCurrentSafeVersion = gnosisSafeInstance => gnosisSafeInstance.VERSION()
export const getCurrentSafeVersion = (gnosisSafeInstance) => gnosisSafeInstance.VERSION()
export const enabledFeatures = (version: string) =>
FEATURES.reduce((acc, feature) => {
@ -57,7 +57,7 @@ export const getCurrentMasterContractLastVersion = async () => {
return safeMasterVersion
}
export const getSafeVersion = async (safeAddress: string) => {
export const getSafeVersionInfo = async (safeAddress: string) => {
try {
const safeMaster = await getGnosisSafeInstanceAt(safeAddress)
const lastSafeVersion = await getCurrentMasterContractLastVersion()

View File

@ -9,7 +9,7 @@ const fetchTokenBalanceList = (safeAddress: string) => {
return axios.get(url, {
params: {
limit: 300,
limit: 3000,
},
})
}

View File

@ -9,7 +9,7 @@ const fetchTokenList = () => {
return axios.get(url, {
params: {
limit: 300,
limit: 3000,
},
})
}

View File

@ -0,0 +1,49 @@
// @flow
import type { Dispatch as ReduxDispatch } from 'redux'
import fetchCollectibles from '~/logic/collectibles/store/actions/fetchCollectibles'
import { nftAssetsSelector } from '~/logic/collectibles/store/selectors'
import updateActiveAssets from '~/routes/safe/store/actions/updateActiveAssets'
import {
safeActiveAssetsSelectorBySafe,
safeBlacklistedAssetsSelectorBySafe,
safesMapSelector,
} from '~/routes/safe/store/selectors'
import { type GetState, type GlobalState } from '~/store'
const activateAssetsByBalance = (safeAddress: string) => async (
dispatch: ReduxDispatch<GlobalState>,
getState: GetState,
) => {
try {
await dispatch(fetchCollectibles())
const state = getState()
const safes = safesMapSelector(state)
const availableAssets = nftAssetsSelector(state)
const alreadyActiveAssets = safeActiveAssetsSelectorBySafe(safeAddress, safes)
const blacklistedAssets = safeBlacklistedAssetsSelectorBySafe(safeAddress, safes)
// active tokens by balance, excluding those already blacklisted and the `null` address
const activeByBalance = Object.entries(availableAssets)
.filter((asset) => {
const { address, numberOfTokens } = asset[1]
return address !== null && !blacklistedAssets.has(address) && numberOfTokens > 0
})
.map((asset) => {
return asset[0]
})
// need to persist those already active assets, despite its balances
const activeAssets = alreadyActiveAssets.union(activeByBalance)
// update list of active tokens
dispatch(updateActiveAssets(safeAddress, activeAssets))
} catch (err) {
console.error('Error fetching active assets list', err)
}
return null
}
export default activateAssetsByBalance

View File

@ -44,7 +44,7 @@ const activateTokensByBalance = (safeAddress: string) => async (
)
// active tokens by balance, excluding those already blacklisted and the `null` address
const activeByBalance = addresses.filter(address => address !== null && !blacklistedTokens.includes(address))
const activeByBalance = addresses.filter((address) => address !== null && !blacklistedTokens.includes(address))
// need to persist those already active tokens, despite its balances
const activeTokens = alreadyActiveTokens.toSet().union(activeByBalance)

View File

@ -10,8 +10,10 @@ import saveTokens from './saveTokens'
import { fetchTokenList } from '~/logic/tokens/api'
import { type TokenProps, makeToken } from '~/logic/tokens/store/model/token'
import { tokensSelector } from '~/logic/tokens/store/selectors'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { type GlobalState } from '~/store'
import { store } from '~/store/index'
import { ensureOnce } from '~/utils/singleton'
const createStandardTokenContract = async () => {
@ -37,12 +39,67 @@ const createERC721TokenContract = async () => {
return erc721Token
}
const OnlyBalanceToken = {
contractName: 'OnlyBalanceToken',
abi: [
{
constant: true,
inputs: [
{
name: 'owner',
type: 'address',
},
],
name: 'balanceOf',
outputs: [
{
name: '',
type: 'uint256',
},
],
payable: false,
stateMutability: 'view',
type: 'function',
},
{
constant: true,
inputs: [
{
name: 'owner',
type: 'address',
},
],
name: 'balances',
outputs: [
{
name: '',
type: 'uint256',
},
],
payable: false,
stateMutability: 'view',
type: 'function',
},
],
}
// For the `batchRequest` of balances, we're just using the `balanceOf` method call.
// So having a simple ABI only with `balanceOf` prevents errors
// when instantiating non-standard ERC-20 Tokens.
const createOnlyBalanceToken = () => {
const web3 = getWeb3()
const contract = new web3.eth.Contract(OnlyBalanceToken.abi)
return contract
}
export const getHumanFriendlyToken = ensureOnce(createHumanFriendlyTokenContract)
export const getStandardTokenContract = ensureOnce(createStandardTokenContract)
export const getERC721TokenContract = ensureOnce(createERC721TokenContract)
export const getOnlyBalanceToken = ensureOnce(createOnlyBalanceToken)
export const containsMethodByHash = async (contractAddress: string, methodHash: string) => {
const web3 = getWeb3()
const byteCode = await web3.eth.getCode(contractAddress)
@ -50,19 +107,54 @@ export const containsMethodByHash = async (contractAddress: string, methodHash:
return byteCode.indexOf(methodHash.replace('0x', '')) !== -1
}
export const fetchTokens = () => async (dispatch: ReduxDispatch<GlobalState>) => {
export const getTokenInfos = async (tokenAddress: string) => {
if (!tokenAddress) {
return null
}
const { tokens } = store.getState()
const localToken = tokens.get(tokenAddress)
// If the token is inside the store we return the store token
if (localToken) {
return localToken
}
// Otherwise we fetch it, save it to the store and return it
const tokenContract = await getHumanFriendlyToken()
const tokenInstance = await tokenContract.at(tokenAddress)
const [tokenSymbol, tokenDecimals, name] = await Promise.all([
tokenInstance.symbol(),
tokenInstance.decimals(),
tokenInstance.name(),
])
const savedToken = makeToken({
address: tokenAddress,
name: name ? name : tokenSymbol,
symbol: tokenSymbol,
decimals: tokenDecimals,
logoUri: '',
})
const newTokens = tokens.set(tokenAddress, savedToken)
store.dispatch(saveTokens(newTokens))
return savedToken
}
export const fetchTokens = () => async (dispatch: ReduxDispatch<GlobalState>, getState: Function) => {
try {
const currentSavedTokens = tokensSelector(getState())
const {
data: { results: tokenList },
} = await fetchTokenList()
if (currentSavedTokens && currentSavedTokens.size === tokenList.length) {
return
}
const tokens = List(tokenList.map((token: TokenProps) => makeToken(token)))
dispatch(saveTokens(tokens))
} catch (err) {
console.error('Error fetching token list', err)
return Promise.resolve()
}
}

View File

@ -16,8 +16,8 @@ export default handleActions<State, *>(
[ADD_TOKENS]: (state: State, action: ActionType<Function>): State => {
const { tokens } = action.payload
const newState = state.withMutations(map => {
tokens.forEach(token => {
const newState = state.withMutations((map) => {
tokens.forEach((token) => {
map.set(token.address, token)
})
})

View File

@ -2,6 +2,7 @@
import { List } from 'immutable'
import logo from '~/assets/icons/icon_etherTokens.svg'
import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens'
import { type Token, makeToken } from '~/logic/tokens/store/model/token'
import { getWeb3 } from '~/logic/wallets/getWeb3'
@ -25,7 +26,7 @@ export const getEthAsToken = (balance: string) => {
}
export const calculateActiveErc20TokensFrom = (tokens: List<Token>) => {
const activeTokens = List().withMutations(list =>
const activeTokens = List().withMutations((list) =>
tokens.forEach((token: Token) => {
const isDeactivated = isEther(token.symbol) || !token.status
if (isDeactivated) {
@ -65,3 +66,15 @@ export const isMultisendTransaction = (data: string, value: number): boolean =>
// 7de7edef - changeMasterCopy (550, 8)
export const isUpgradeTransaction = (data: string) =>
!!data && data.substr(308, 8) === 'f08a0323' && data.substr(550, 8) === '7de7edef'
export const isERC721Contract = async (contractAddress: string): boolean => {
const ERC721Token = await getStandardTokenContract()
let isERC721 = false
try {
isERC721 = true
await ERC721Token.at(contractAddress)
} catch (error) {
console.warn('Asset not found')
}
return isERC721
}

View File

@ -44,7 +44,7 @@ export const removeTokenFromStorage = async (safeAddress: string, token: Token)
export const removeFromActiveTokens = async (safeAddress: string, token: Token) => {
const activeTokens = await getActiveTokens()
const index = activeTokens.findIndex(activeToken => activeToken.name === token.name)
const index = activeTokens.findIndex((activeToken) => activeToken.name === token.name)
if (index !== -1) {
await saveActiveTokens(safeAddress, activeTokens.delete(index))

View File

@ -49,4 +49,4 @@ export const isUserOwner = (safe: Safe, userAccount: string): boolean => {
}
export const isUserOwnerOnAnySafe = (safes: List<Safe>, userAccount: string): boolean =>
safes.some(safe => isUserOwner(safe, userAccount))
safes.some((safe) => isUserOwner(safe, userAccount))

View File

@ -2,6 +2,9 @@
import ENS from 'ethereum-ens'
import Web3 from 'web3'
import { sameAddress } from './ethAddresses'
import { EMPTY_DATA } from './ethTransactions'
import { getNetwork } from '~/config/index'
import type { ProviderProps } from '~/logic/wallets/store/model/provider'
@ -27,6 +30,8 @@ export const WALLET_PROVIDER = {
OPERA: 'OPERA',
DAPPER: 'DAPPER',
AUTHEREUM: 'AUTHEREUM',
LEDGER: 'LEDGER',
TREZOR: 'TREZOR',
}
export const INJECTED_PROVIDERS = [
@ -88,38 +93,44 @@ export const getAccountFrom: Function = async (web3Provider): Promise<string | n
return accounts && accounts.length > 0 ? accounts[0] : null
}
const getNetworkIdFrom = async web3Provider => {
const networkId = await web3Provider.eth.net.getId()
export const getNetworkIdFrom = (web3Provider) => web3Provider.eth.net.getId()
return networkId
const isHardwareWallet = (walletName: $Values<typeof WALLET_PROVIDER>) =>
sameAddress(WALLET_PROVIDER.LEDGER, walletName) || sameAddress(WALLET_PROVIDER.TREZOR, walletName)
const isSmartContractWallet = async (web3Provider, account) => {
const contractCode: string = await web3Provider.eth.getCode(account)
return contractCode.replace(EMPTY_DATA, '').replace(/0/g, '') !== ''
}
export const getProviderInfo: Function = async (
web3Provider,
providerName?: string = 'Wallet',
providerName: string = 'Wallet',
): Promise<ProviderProps> => {
web3 = new Web3(web3Provider)
const name = providerName
const account = await getAccountFrom(web3)
const network = await getNetworkIdFrom(web3)
const smartContractWallet = await isSmartContractWallet(web3, account)
const hardwareWallet = isHardwareWallet(providerName)
const available = account !== null
return {
name,
name: providerName,
available,
loaded: true,
account,
network,
smartContractWallet,
hardwareWallet,
}
}
export const getAddressFromENS = async (name: string) => {
const ens = new ENS(web3)
const address = await ens.resolver(name).addr()
return address
return await ens.resolver(name).addr()
}
export const setWeb3 = (provider: Object) => {

View File

@ -12,15 +12,7 @@ import type { ProviderProps } from '~/logic/wallets/store/model/provider'
import { makeProvider } from '~/logic/wallets/store/model/provider'
export const processProviderResponse = (dispatch: ReduxDispatch<*>, provider: ProviderProps) => {
const { account, available, loaded, name, network } = provider
const walletRecord = makeProvider({
name,
available,
loaded,
account,
network,
})
const walletRecord = makeProvider(provider)
dispatch(addProvider(walletRecord))
}

View File

@ -8,6 +8,8 @@ export type ProviderProps = {
available: boolean,
account: string,
network: number,
smartContractWallet: boolean,
hardwareWallet: boolean,
}
export const makeProvider: RecordFactory<ProviderProps> = Record({
@ -16,6 +18,8 @@ export const makeProvider: RecordFactory<ProviderProps> = Record({
available: false,
account: '',
network: 0,
smartContractWallet: false,
hardwareWallet: false,
})
export type Provider = RecordOf<ProviderProps>

View File

@ -5,7 +5,7 @@ import { ETHEREUM_NETWORK, ETHEREUM_NETWORK_IDS } from '~/logic/wallets/getWeb3'
import type { Provider } from '~/logic/wallets/store/model/provider'
import { PROVIDER_REDUCER_ID } from '~/logic/wallets/store/reducer/provider'
const providerSelector = (state: any): Provider => state[PROVIDER_REDUCER_ID]
export const providerSelector = (state: any): Provider => state[PROVIDER_REDUCER_ID]
export const userAccountSelector = createSelector(providerSelector, (provider: Provider) => {
const account = provider.get('account')

View File

@ -1,7 +1,7 @@
// @flow
function transactionDataCheck() {
let completed = false
return stateAndHelpers => {
return (stateAndHelpers) => {
const { wallet } = stateAndHelpers
if (wallet && wallet.name === 'Ledger' && !completed) {

View File

@ -57,7 +57,7 @@ export const getSupportedWallets = () => {
const { isDesktop } = window
/* eslint-disable no-unused-vars */
if (isDesktop) return wallets.filter(wallet => wallet.desktop).map(({ desktop, ...rest }) => rest)
if (isDesktop) return wallets.filter((wallet) => wallet.desktop).map(({ desktop, ...rest }) => rest)
return wallets.map(({ desktop, ...rest }) => rest)
}

View File

@ -3,14 +3,7 @@ import React, { useEffect, useState } from 'react'
import { connect } from 'react-redux'
import { Redirect, Route, Switch, withRouter } from 'react-router-dom'
import {
LOAD_ADDRESS,
OPENING_ADDRESS,
OPEN_ADDRESS,
SAFELIST_ADDRESS,
SAFE_PARAM_ADDRESS,
WELCOME_ADDRESS,
} from './routes'
import { LOAD_ADDRESS, OPEN_ADDRESS, SAFELIST_ADDRESS, SAFE_PARAM_ADDRESS, WELCOME_ADDRESS } from './routes'
import Welcome from './welcome/container'
import Loader from '~/components/Loader'
@ -21,8 +14,6 @@ const Safe = React.lazy(() => import('./safe/container'))
const Open = React.lazy(() => import('./open/container/Open'))
const Opening = React.lazy(() => import('./opening/container'))
const Load = React.lazy(() => import('./load/container/Load'))
const SAFE_ADDRESS = `${SAFELIST_ADDRESS}/:${SAFE_PARAM_ADDRESS}`
@ -66,7 +57,6 @@ const Routes = ({ defaultSafe, location }: RoutesProps) => {
<Route component={withTracker(Welcome)} exact path={WELCOME_ADDRESS} />
<Route component={withTracker(Open)} exact path={OPEN_ADDRESS} />
<Route component={withTracker(Safe)} path={SAFE_ADDRESS} />
<Route component={withTracker(Opening)} exact path={OPENING_ADDRESS} />
<Route component={withTracker(Load)} exact path={LOAD_ADDRESS} />
<Redirect to="/" />
</Switch>
@ -75,6 +65,6 @@ const Routes = ({ defaultSafe, location }: RoutesProps) => {
// $FlowFixMe
export default connect<Object, Object, ?Function, ?Object>(
state => ({ defaultSafe: defaultSafeSelector(state) }),
(state) => ({ defaultSafe: defaultSafeSelector(state) }),
null,
)(withRouter(Routes))

View File

@ -104,7 +104,7 @@ const Details = ({ classes, errors, form }: Props) => (
<Block className={classes.root} margin="lg">
<AddressInput
component={TextField}
fieldMutator={val => {
fieldMutator={(val) => {
form.mutators.setValue(FIELD_LOAD_ADDRESS, val)
}}
inputAdornment={

View File

@ -50,7 +50,7 @@ class Load extends React.Component<Props> {
await loadSafe(safeName, safeAddress, owners, addSafe)
const url = `${SAFELIST_ADDRESS}/${safeAddress}/balances/`
const url = `${SAFELIST_ADDRESS}/${safeAddress}/balances`
history.push(url)
} catch (error) {
console.error('Error while loading the Safe', error)

View File

@ -98,24 +98,20 @@ const ReviewComponent = ({ classes, userAccount, values }: Props) => {
const numOwners = getNumOwnersFrom(values)
useEffect(() => {
let isCurrent = true
const estimateGas = async () => {
if (!addresses.length || !numOwners || !userAccount) {
return
}
const web3 = getWeb3()
const { fromWei, toBN } = web3.utils
const estimatedGasCosts = await estimateGasForDeployingSafe(addresses, numOwners, userAccount)
const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether')
const formattedGasCosts = formatAmount(gasCostsAsEth)
if (isCurrent) {
setGasCosts(formattedGasCosts)
}
setGasCosts(formattedGasCosts)
}
estimateGas()
return () => {
isCurrent = false
}
}, [])
}, [addresses, numOwners, userAccount])
return (
<>

View File

@ -74,7 +74,7 @@ const SafeOwners = (props: Props) => {
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
const [scanQrForOwnerName, setScanQrForOwnerName] = useState<string | null>(null)
const openQrModal = ownerName => {
const openQrModal = (ownerName) => {
setScanQrForOwnerName(ownerName)
setQrModalOpen(true)
}
@ -94,7 +94,7 @@ const SafeOwners = (props: Props) => {
setNumOwners(numOwners + 1)
}
const handleScan = value => {
const handleScan = (value) => {
let scannedAddress = value
if (scannedAddress.startsWith('ethereum:')) {
@ -144,7 +144,7 @@ const SafeOwners = (props: Props) => {
<Col className={classes.ownerAddress} xs={6}>
<AddressInput
component={TextField}
fieldMutator={val => {
fieldMutator={(val) => {
form.mutators.setValue(addressName, val)
}}
inputAdornment={

View File

@ -10,7 +10,7 @@ export const getOwnerAddressBy = (index: number) => `owner${index}Address`
export const getNumOwnersFrom = (values: Object) => {
const accounts = Object.keys(values)
.sort()
.filter(key => /^owner\d+Address$/.test(key) && !!values[key])
.filter((key) => /^owner\d+Address$/.test(key) && !!values[key])
return accounts.length
}

View File

@ -1,16 +1,18 @@
// @flow
import queryString from 'query-string'
import * as React from 'react'
import React, { useEffect, useState } from 'react'
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import Opening from '../../opening'
import Layout from '../components/Layout'
import actions, { type Actions, type AddSafe } from './actions'
import actions, { type Actions } from './actions'
import selector from './selector'
import { Loader } from '~/components-v2'
import Page from '~/components/layout/Page'
import { deploySafeContract, getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
import { getSafeDeploymentTransaction } from '~/logic/contracts/safeContracts'
import { checkReceiptStatus } from '~/logic/wallets/ethTransactions'
import {
getAccountsFrom,
@ -19,9 +21,12 @@ import {
getSafeNameFrom,
getThresholdFrom,
} from '~/routes/open/utils/safeDataExtractor'
import { OPENING_ADDRESS, SAFELIST_ADDRESS, stillInOpeningView } from '~/routes/routes'
import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from '~/routes/routes'
import { buildSafe } from '~/routes/safe/store/actions/fetchSafe'
import { history } from '~/store'
import { loadFromStorage, removeFromStorage, saveToStorage } from '~/utils/storage'
const SAFE_PENDING_CREATION_STORAGE_KEY = 'SAFE_PENDING_CREATION_STORAGE_KEY'
type Props = Actions & {
provider: string,
@ -62,76 +67,155 @@ const validateQueryParams = (
return true
}
export const createSafe = async (values: Object, userAccount: string, addSafe: AddSafe): Promise<OpenState> => {
const numConfirmations = getThresholdFrom(values)
export const getSafeProps = async (safeAddress, safeName, ownersNames, ownerAddresses) => {
const safeProps = await buildSafe(safeAddress, safeName)
const owners = getOwnersFrom(ownersNames, ownerAddresses)
safeProps.owners = owners
return safeProps
}
export const createSafe = (values: Object, userAccount: string): Promise<OpenState> => {
const confirmations = getThresholdFrom(values)
const name = getSafeNameFrom(values)
const ownersNames = getNamesFrom(values)
const ownerAddresses = getAccountsFrom(values)
const safe = await deploySafeContract(ownerAddresses, numConfirmations, userAccount)
await checkReceiptStatus(safe.tx)
const deploymentTxMethod = getSafeDeploymentTransaction(ownerAddresses, confirmations, userAccount)
const safeAddress = safe.logs[0].args.proxy
const safeContract = await getGnosisSafeInstanceAt(safeAddress)
const safeProps = await buildSafe(safeAddress, name)
const owners = getOwnersFrom(ownersNames, ownerAddresses)
safeProps.owners = owners
const promiEvent = deploymentTxMethod.send({ from: userAccount, value: 0 })
addSafe(safeProps)
if (stillInOpeningView()) {
promiEvent
.once('transactionHash', (txHash) => {
saveToStorage(SAFE_PENDING_CREATION_STORAGE_KEY, { txHash, ...values })
})
.then(async (receipt) => {
await checkReceiptStatus(receipt.transactionHash)
const safeAddress = receipt.events.ProxyCreation.returnValues.proxy
const safeProps = await getSafeProps(safeAddress, name, ownersNames, ownerAddresses)
// returning info for testing purposes, in app is fully async
return { safeAddress: safeProps.address, safeTx: receipt }
})
return promiEvent
}
const Open = ({ addSafe, network, provider, userAccount }: Props) => {
const [loading, setLoading] = useState(false)
const [showProgress, setShowProgress] = useState()
const [creationTxPromise, setCreationTxPromise] = useState()
const [safeCreationPendingInfo, setSafeCreationPendingInfo] = useState()
const [safePropsFromUrl, setSafePropsFromUrl] = useState()
useEffect(() => {
// #122: Allow to migrate an old Multisig by passing the parameters to the URL.
const query: SafePropsType = queryString.parse(location.search, { arrayFormat: 'comma' })
const { name, owneraddresses, ownernames, threshold } = query
if (validateQueryParams(owneraddresses, ownernames, threshold, name)) {
setSafePropsFromUrl({
name,
ownerAddresses: owneraddresses,
ownerNames: ownernames,
threshold,
})
}
})
// check if there is a safe being created
useEffect(() => {
const load = async () => {
const pendingCreation = await loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY)
if (pendingCreation && pendingCreation.txHash) {
setSafeCreationPendingInfo(pendingCreation)
setShowProgress(true)
} else {
setShowProgress(false)
}
setLoading(false)
}
load()
}, [])
const createSafeProxy = async (formValues) => {
let values = formValues
// save form values, used when the user rejects the TX and wants to retry
if (formValues) {
const copy = { ...formValues }
saveToStorage(SAFE_PENDING_CREATION_STORAGE_KEY, copy)
} else {
values = await loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY)
}
const promiEvent = createSafe(values, userAccount, addSafe)
setCreationTxPromise(promiEvent)
setShowProgress(true)
}
const onSafeCreated = async (safeAddress) => {
const pendingCreation = await loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY)
const name = getSafeNameFrom(pendingCreation)
const ownersNames = getNamesFrom(pendingCreation)
const ownerAddresses = getAccountsFrom(pendingCreation)
const safeProps = await getSafeProps(safeAddress, name, ownersNames, ownerAddresses)
addSafe(safeProps)
removeFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY)
const url = {
pathname: `${SAFELIST_ADDRESS}/${safeContract.address}/balances`,
pathname: `${SAFELIST_ADDRESS}/${safeProps.address}/balances`,
state: {
name,
tx: safe.tx,
tx: pendingCreation.txHash,
},
}
history.push(url)
}
// returning info for testing purposes, in app is fully async
return { safeAddress: safeContract.address, safeTx: safe }
}
class Open extends React.Component<Props> {
onCallSafeContractSubmit = async values => {
try {
const { addSafe, userAccount } = this.props
createSafe(values, userAccount, addSafe)
history.push(OPENING_ADDRESS)
} catch (error) {
// eslint-disable-next-line
console.error('Error while creating the Safe: ' + error)
}
const onCancel = () => {
removeFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY)
history.push({
pathname: `${WELCOME_ADDRESS}`,
})
}
render() {
const { location, network, provider, userAccount } = this.props
const query: SafePropsType = queryString.parse(location.search, { arrayFormat: 'comma' })
const { name, owneraddresses, ownernames, threshold } = query
const onRetry = async () => {
const values = await loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY)
delete values.txHash
await saveToStorage(SAFE_PENDING_CREATION_STORAGE_KEY, values)
setSafeCreationPendingInfo(values)
createSafeProxy()
}
let safeProps = null
if (validateQueryParams(owneraddresses, ownernames, threshold, name)) {
safeProps = {
name,
ownerAddresses: owneraddresses,
ownerNames: ownernames,
threshold,
}
}
return (
<Page>
if (loading || showProgress === undefined) {
return <Loader />
}
return (
<Page>
{showProgress ? (
<Opening
creationTxHash={safeCreationPendingInfo ? safeCreationPendingInfo.txHash : undefined}
onCancel={onCancel}
onRetry={onRetry}
onSuccess={onSafeCreated}
provider={provider}
submittedPromise={creationTxPromise}
/>
) : (
<Layout
network={network}
onCallSafeContractSubmit={this.onCallSafeContractSubmit}
onCallSafeContractSubmit={createSafeProxy}
provider={provider}
safeProps={safeProps}
safeProps={safePropsFromUrl}
userAccount={userAccount}
/>
</Page>
)
}
)}
</Page>
)
}
export default connect(selector, actions)(withRouter(Open))

View File

@ -6,17 +6,17 @@ import { type Owner, makeOwner } from '~/routes/safe/store/models/owner'
export const getAccountsFrom = (values: Object): string[] => {
const accounts = Object.keys(values)
.sort()
.filter(key => /^owner\d+Address$/.test(key))
.filter((key) => /^owner\d+Address$/.test(key))
return accounts.map(account => values[account]).slice(0, values.owners)
return accounts.map((account) => values[account]).slice(0, values.owners)
}
export const getNamesFrom = (values: Object): string[] => {
const accounts = Object.keys(values)
.sort()
.filter(key => /^owner\d+Name$/.test(key))
.filter((key) => /^owner\d+Name$/.test(key))
return accounts.map(account => values[account]).slice(0, values.owners)
return accounts.map((account) => values[account]).slice(0, values.owners)
}
export const getOwnersFrom = (names: string[], addresses: string[]): List<Owner> => {

View File

@ -0,0 +1,4 @@
<svg width="89" height="89" viewBox="0 0 89 89" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M44.25 0C53.0018 0 61.5571 2.59522 68.834 7.45747C76.1109 12.3197 81.7825 19.2306 85.1317 27.3163C88.4808 35.4019 89.3571 44.2991 87.6497 52.8827C85.9424 61.4664 81.728 69.351 75.5395 75.5395C69.351 81.728 61.4664 85.9424 52.8827 87.6497C44.2991 89.3571 35.4019 88.4808 27.3163 85.1317C19.2306 81.7825 12.3197 76.1109 7.45747 68.834C2.59522 61.5571 0 53.0018 0 44.25C0.0164019 32.5192 4.68371 21.2736 12.9786 12.9786C21.2736 4.68371 32.5192 0.0164019 44.25 0ZM44.25 4.445C36.3785 4.445 28.6838 6.7791 22.1388 11.1522C15.5939 15.5252 10.4926 21.7408 7.48007 29.013C4.46756 36.2853 3.67909 44.2874 5.21438 52.0078C6.74967 59.7281 10.5398 66.8198 16.1054 72.3861C21.671 77.9524 28.7622 81.7434 36.4823 83.2796C44.2025 84.8159 52.2048 84.0284 59.4773 81.0168C66.7499 78.0052 72.9662 72.9048 77.3401 66.3603C81.7139 59.8159 84.049 52.1215 84.05 44.25C84.0323 33.6998 79.8334 23.5868 72.3733 16.1267C64.9132 8.66661 54.8002 4.46772 44.25 4.45V4.445Z" fill="#D4D5D3"/>
<path d="M66.077 31.405L69.3 34.465L40.146 65.174L19.2 43.111L22.423 40.05L40.146 58.718L66.077 31.405Z" fill="#008C73"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="111" height="91" viewBox="0 0 111 91" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<g fill="#B2B5B2">
<path d="M6.535 81.666h9.8V78.4h-9.8v3.266zm64.131-6.535h-67.4V3.266H94.73l.003 47.754c.203-.004.403-.015.608-.015.898 0 1.785.051 2.659.144V0H0v78.4h3.265v6.535H19.6V78.4h51.202c-.097-.888-.15-1.79-.15-2.705 0-.189.01-.376.014-.564z"/>
<path d="M9.8 68.582L9.816 9.8l78.385.015v4.885h-1.635a1.64 1.64 0 0 0-1.635 1.634c0 .9.736 1.635 1.635 1.635H88.2v1.634h-1.635a1.64 1.64 0 0 0-1.635 1.635c0 .9.736 1.635 1.635 1.635H88.2v29.184c1.06-.32 2.15-.572 3.266-.748V22.866c.899 0 1.634-.736 1.634-1.634 0-.9-.735-1.635-1.634-1.635v-1.635a1.64 1.64 0 0 0 1.634-1.635c0-.9-.735-1.635-1.634-1.635V9.816a3.284 3.284 0 0 0-3.281-3.285H9.816a3.283 3.283 0 0 0-3.281 3.285v58.766a3.283 3.283 0 0 0 3.28 3.284H70.95c.173-1.114.423-2.2.74-3.26L9.8 68.582z"/>
<path d="M60.435 42.466A3.276 3.276 0 0 1 57.17 39.2a3.276 3.276 0 0 1 3.265-3.266 3.277 3.277 0 0 1 3.266 3.266 3.276 3.276 0 0 1-3.266 3.265m0-9.8a6.533 6.533 0 0 0-6.535 6.535 6.534 6.534 0 0 0 6.535 6.535 6.535 6.535 0 0 0 0-13.069"/>
<path d="M75.136 40.832h1.55a16.242 16.242 0 0 1-3.61 8.689l-1.095-1.095a1.633 1.633 0 0 0-2.305 0 1.634 1.634 0 0 0 0 2.305l1.095 1.095a16.303 16.303 0 0 1-8.69 3.61l.004-1.535c0-.9-.735-1.635-1.634-1.635-.9 0-1.635.736-1.635 1.635v1.55a16.249 16.249 0 0 1-8.69-3.61l1.095-1.095a1.631 1.631 0 0 0 0-2.304 1.63 1.63 0 0 0-2.305 0l-1.095 1.095a16.303 16.303 0 0 1-3.61-8.69l1.524.004c.9 0 1.634-.735 1.634-1.635s-.734-1.634-1.634-1.634h-1.55a16.25 16.25 0 0 1 3.61-8.69l1.095 1.094c.325.325.734.475 1.16.475.424 0 .834-.165 1.159-.475a1.63 1.63 0 0 0 0-2.305l-1.095-1.095a16.309 16.309 0 0 1 8.69-3.61l-.003 1.524c0 .9.734 1.635 1.634 1.635.9 0 1.635-.734 1.635-1.635v-1.55a16.254 16.254 0 0 1 8.69 3.61l-1.095 1.095a1.63 1.63 0 0 0 0 2.304c.326.326.735.475 1.16.475a1.67 1.67 0 0 0 1.16-.475l1.095-1.094a16.3 16.3 0 0 1 3.61 8.69l-1.554-.004c-.9 0-1.635.734-1.635 1.634a1.65 1.65 0 0 0 1.635 1.647M74.3 25.347c-.015-.016-.035-.016-.05-.035-3.56-3.525-8.426-5.71-13.816-5.71-5.39 0-10.256 2.189-13.8 5.7-.016.014-.035.014-.05.033-.016.015-.016.034-.035.05-3.53 3.557-5.715 8.426-5.715 13.816 0 5.39 2.19 10.256 5.7 13.8.015.016.015.034.034.05.016.015.035.015.05.034 3.557 3.526 8.426 5.716 13.816 5.716 5.39 0 10.256-2.19 13.8-5.7.016-.016.035-.016.051-.035.015-.015.015-.034.034-.049 3.525-3.56 5.716-8.427 5.716-13.816 0-5.39-2.19-10.26-5.701-13.804-.015-.016-.015-.035-.034-.05M17.966 22.862c-.9 0-1.635.735-1.635 1.634v32.666c0 .9.734 1.635 1.635 1.635.899 0 1.634-.735 1.634-1.635v-1.634c.9 0 1.635-.736 1.635-1.635 0-.9-.735-1.635-1.635-1.635V29.4c.9 0 1.635-.734 1.635-1.634 0-.9-.735-1.635-1.635-1.635v-1.635c0-.898-.735-1.634-1.634-1.634M26.135 22.866c-.9 0-1.635.735-1.635 1.634v1.636c-.9 0-1.635.734-1.635 1.634 0 .9.736 1.634 1.635 1.634V52.27c-.9 0-1.635.735-1.635 1.634 0 .9.736 1.636 1.635 1.636v1.634c0 .9.735 1.635 1.635 1.635s1.634-.735 1.634-1.635L27.765 24.5c0-.899-.734-1.634-1.63-1.634"/>
</g>
<path stroke-width="3" d="M93.756 58.266c-8.466 0-15.361 6.896-15.361 15.362 0 8.466 6.895 15.36 15.361 15.36 8.466 0 15.361-6.894 15.361-15.36s-6.895-15.362-15.361-15.362z" style="stroke: rgb(242, 72, 34);"/>
<path fill-rule="nonzero" d="M93.68 68.471h-3.06v10.313h8.39v-3.06h-5.33z" style="fill: rgb(242, 72, 34);"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -1,79 +0,0 @@
// @flow
import LinearProgress from '@material-ui/core/LinearProgress'
import { withStyles } from '@material-ui/core/styles'
import OpenInNew from '@material-ui/icons/OpenInNew'
import * as React from 'react'
import { type SelectorProps } from '../container/selector'
import Block from '~/components/layout/Block'
import Img from '~/components/layout/Img'
import Page from '~/components/layout/Page'
import Paragraph from '~/components/layout/Paragraph'
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
import { mediumFontSize, secondary, xs } from '~/theme/variables'
type Props = SelectorProps & {
name: string,
tx: string,
classes: Object,
}
const vault = require('../assets/vault.svg')
const styles = {
icon: {
height: mediumFontSize,
color: secondary,
},
follow: {
display: 'flex',
justifyContent: 'center',
marginTop: xs,
},
etherscan: {
color: secondary,
textDecoration: 'underline',
display: 'flex',
alignItems: 'center',
marginLeft: xs,
},
}
const Opening = ({ classes, name = 'Safe creation process', tx }: Props) => (
<Page align="center">
<Paragraph align="center" color="primary" size="xxl" weight="bold">
{name}
</Paragraph>
<Block align="center" margin="lg">
<Img alt="Vault" height={90} src={vault} />
</Block>
<Block margin="lg">
<LinearProgress color="secondary" />
</Block>
<Block margin="md">
<Paragraph align="center" className={classes.page} noMargin size="xl">
Transaction submitted
</Paragraph>
<Paragraph align="center" className={classes.page} noMargin size="xl" weight="bolder">
Deploying your new Safe...
</Paragraph>
</Block>
<Block margin="md">
<Paragraph align="center" noMargin size="md" weight="light">
This process should take a couple of minutes. <br />
</Paragraph>
{tx && (
<Paragraph align="center" className={classes.follow} noMargin size="md" weight="light">
Follow progress on{' '}
<a className={classes.etherscan} href={getEtherScanLink('tx', tx)} rel="noopener noreferrer" target="_blank">
Etherscan.io
<OpenInNew className={classes.icon} />
</a>
</Paragraph>
)}
</Block>
</Page>
)
export default withStyles(styles)(Opening)

View File

@ -1,8 +1,8 @@
// @flow
import { connect } from 'react-redux'
import Layout from '../component'
import selector from './selector'
export default connect(selector)(Layout)
import Opening from './index'
export default connect(selector)(Opening)

View File

@ -0,0 +1,394 @@
// @flow
import React, { useEffect, useState } from 'react'
import styled from 'styled-components'
import { Loader, Stepper } from '~/components-v2'
import LoaderDots from '~/components-v2/feedback/Loader-dots/assets/loader-dots.svg'
import Button from '~/components/layout/Button'
import Heading from '~/components/layout/Heading'
import Img from '~/components/layout/Img'
import Paragraph from '~/components/layout/Paragraph'
import { initContracts } from '~/logic/contracts/safeContracts'
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { getEtherScanLink, getWeb3 } from '~/logic/wallets/getWeb3'
import { background, connected } from '~/theme/variables'
const successSvg = require('./assets/success.svg')
const vaultErrorSvg = require('./assets/vault-error.svg')
const vaultSvg = require('./assets/vault.svg')
const Wrapper = styled.div`
display: grid;
grid-template-columns: 250px auto;
grid-template-rows: 62px auto;
margin-bottom: 30px;
`
const Title = styled(Heading)`
grid-column: 1/3;
grid-row: 1;
`
const Nav = styled.div`
grid-column: 1;
grid-row: 2;
`
const Body = styled.div`
grid-column: 2;
grid-row: 2;
text-align: center;
background-color: #ffffff;
border-radius: 5px;
min-width: 700px;
padding-top: 50px;
box-shadow: 0 0 10px 0 rgba(33, 48, 77, 0.1);
display: grid;
grid-template-rows: 100px 50px 70px 60px 100px;
`
const EtherScanLink = styled.a`
color: ${connected};
`
const CardTitle = styled.div`
font-size: 20px;
`
const FullParagraph = styled(Paragraph)`
background-color: ${background};
padding: 24px;
font-size: 16px;
margin-bottom: 16px;
`
const ButtonMargin = styled(Button)`
margin-right: 16px;
`
const BodyImage = styled.div`
grid-row: 1;
`
const BodyDescription = styled.div`
grid-row: 2;
`
const BodyLoader = styled.div`
grid-row: 3;
display: flex;
justify-content: center;
align-items: center;
`
const BodyInstruction = styled.div`
grid-row: 4;
`
const BodyFooter = styled.div`
grid-row: 5;
padding: 10px 0;
display: flex;
justify-content: center;
align-items: flex-end;
`
type Props = {
provider: string,
creationTxHash: Promise<any>,
submittedPromise: Promise<any>,
onRetry: () => void,
onSuccess: () => void,
onCancel: () => void,
}
const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider, submittedPromise }: Props) => {
const [loading, setLoading] = useState(true)
const [stepIndex, setStepIndex] = useState()
const [safeCreationTxHash, setSafeCreationTxHash] = useState()
const [createdSafeAddress, setCreatedSafeAddress] = useState()
const [error, setError] = useState(false)
const [intervalStarted, setIntervalStarted] = useState(false)
const [waitingSafeDeployed, setWaitingSafeDeployed] = useState(false)
const [continueButtonDisabled, setContinueButtonDisabled] = useState(false)
const genericFooter = (
<span>
<p>This process should take a couple of minutes.</p>
<p>
Follow the progress on{' '}
<EtherScanLink
aria-label="Show details on Etherscan"
href={getEtherScanLink('tx', safeCreationTxHash)}
rel="noopener noreferrer"
target="_blank"
>
Etherscan.io
</EtherScanLink>
.
</p>
</span>
)
const navigateToSafe = () => {
setContinueButtonDisabled(true)
onSuccess(createdSafeAddress)
}
const steps = [
{
id: '1',
label: 'Waiting for transaction confirmation',
description: undefined,
instruction: 'Please confirm the Safe creation in your wallet',
footer: null,
},
{
id: '2',
label: 'Transaction submitted',
description: undefined,
instruction: 'Please do not leave the page',
footer: genericFooter,
},
{
id: '3',
label: 'Validating transaction',
description: undefined,
instruction: 'Please do not leave the page',
footer: genericFooter,
},
{
id: '4',
label: 'Deploying smart contract',
description: undefined,
instruction: 'Please do not leave the page',
footer: genericFooter,
},
{
id: '5',
label: 'Generating your Safe',
description: undefined,
instruction: 'Please do not leave the page',
footer: genericFooter,
},
{
id: '6',
label: 'Success',
description: 'Your Safe was created successfully',
instruction: 'Click below to get started',
footer: (
<Button color="primary" disabled={continueButtonDisabled} onClick={navigateToSafe} variant="contained">
Continue
</Button>
),
},
]
const onError = (error) => {
setIntervalStarted(false)
setWaitingSafeDeployed(false)
setContinueButtonDisabled(false)
setError(true)
console.error(error)
}
// discard click event value
const onRetryTx = () => {
setStepIndex(0)
setError(false)
onRetry()
}
const getImage = () => {
if (error) {
return vaultErrorSvg
}
if (stepIndex <= 4) {
return vaultSvg
}
return successSvg
}
useEffect(() => {
const loadContracts = async () => {
await initContracts()
setLoading(false)
}
if (provider) {
loadContracts()
}
}, [provider])
// creating safe from from submission
useEffect(() => {
if (submittedPromise === undefined) {
return
}
setStepIndex(0)
submittedPromise
.once('transactionHash', (txHash) => {
setSafeCreationTxHash(txHash)
setStepIndex(1)
setIntervalStarted(true)
})
.on('error', onError)
}, [submittedPromise])
// recovering safe creation from txHash
useEffect(() => {
if (creationTxHash === undefined) {
return
}
setSafeCreationTxHash(creationTxHash)
setStepIndex(1)
setIntervalStarted(true)
}, [creationTxHash])
useEffect(() => {
if (!intervalStarted) {
return
}
const isTxMined = async (txHash) => {
const web3 = getWeb3()
const receipt = await web3.eth.getTransactionReceipt(txHash)
if (!receipt.status) {
throw Error('TX status reverted')
}
const txResult = await web3.eth.getTransaction(txHash)
return txResult.blockNumber !== null
}
let interval = setInterval(async () => {
if (stepIndex < 4) {
setStepIndex(stepIndex + 1)
}
// safe created using the form
if (submittedPromise !== undefined) {
submittedPromise.then(() => {
setStepIndex(4)
setWaitingSafeDeployed(true)
setIntervalStarted(false)
})
}
// safe pending creation recovered from storage
if (creationTxHash !== undefined) {
try {
const res = await isTxMined(creationTxHash)
if (res) {
setStepIndex(4)
setWaitingSafeDeployed(true)
setIntervalStarted(false)
}
} catch (error) {
onError(error)
}
}
}, 3000)
return () => {
clearInterval(interval)
}
}, [creationTxHash, submittedPromise, intervalStarted, stepIndex, error])
useEffect(() => {
let interval
const awaitUntilSafeIsDeployed = async () => {
try {
const web3 = getWeb3()
const receipt = await web3.eth.getTransactionReceipt(safeCreationTxHash)
let safeAddress
if (receipt.events) {
safeAddress = receipt.events.ProxyCreation.returnValues.proxy
} else {
// get the address for the just created safe
const events = web3.eth.abi.decodeLog(
[
{
type: 'address',
name: 'ProxyCreation',
},
],
receipt.logs[0].data,
receipt.logs[0].topics,
)
safeAddress = events[0]
}
setCreatedSafeAddress(safeAddress)
interval = setInterval(async () => {
const code = await web3.eth.getCode(safeAddress)
if (code !== EMPTY_DATA) {
setStepIndex(5)
}
}, 1000)
} catch (error) {
onError(error)
}
}
if (!waitingSafeDeployed) {
return
}
awaitUntilSafeIsDeployed()
return () => {
clearInterval(interval)
}
}, [waitingSafeDeployed])
if (loading || stepIndex === undefined) {
return <Loader />
}
return (
<Wrapper>
<Title tag="h2">Safe creation process</Title>
<Nav>
<Stepper activeStepIndex={stepIndex} error={error} orientation="vertical" steps={steps} />
</Nav>
<Body>
<BodyImage>
<Img alt="Vault" height={75} src={getImage()} />
</BodyImage>
<BodyDescription>
<CardTitle>{steps[stepIndex].description || steps[stepIndex].label}</CardTitle>
</BodyDescription>
<BodyLoader>{!error && stepIndex <= 4 && <Img alt="LoaderDots" src={LoaderDots} />}</BodyLoader>
<BodyInstruction>
<FullParagraph color="primary" noMargin size="md">
{error ? 'You can Cancel or Retry the Safe creation process.' : steps[stepIndex].instruction}
</FullParagraph>
</BodyInstruction>
<BodyFooter>
{error ? (
<>
<ButtonMargin onClick={onCancel} variant="contained">
Cancel
</ButtonMargin>
<Button color="primary" onClick={onRetryTx} variant="contained">
Retry
</Button>
</>
) : (
steps[stepIndex].footer
)}
</BodyFooter>
</Body>
</Wrapper>
)
}
export default SafeDeployment

View File

@ -1,14 +1,6 @@
// @flow
import { history } from '~/store'
export const SAFE_PARAM_ADDRESS = 'address'
export const SAFELIST_ADDRESS = '/safes'
export const OPEN_ADDRESS = '/open'
export const LOAD_ADDRESS = '/load'
export const WELCOME_ADDRESS = '/welcome'
export const OPENING_ADDRESS = '/opening'
export const stillInOpeningView = () => {
const path = history.location.pathname
return path === OPENING_ADDRESS
}

View File

@ -43,7 +43,7 @@ const CreateEditEntryModalComponent = ({
newEntryModalHandler,
onClose,
}: Props) => {
const onFormSubmitted = values => {
const onFormSubmitted = (values) => {
if (entryToEdit && !entryToEdit.entry.isNew) {
editEntryModalHandler(values)
} else {

View File

@ -26,7 +26,7 @@ type Props = {
}
const DeleteEntryModalComponent = ({ classes, deleteEntryModalHandler, entryToDelete, isOpen, onClose }: Props) => {
const handleDeleteEntrySubmit = values => {
const handleDeleteEntrySubmit = (values) => {
deleteEntryModalHandler(values, entryToDelete.index)
}

View File

@ -43,7 +43,7 @@ const EllipsisTransactionDetails = ({ address, knownAddress }: EllipsisTransacti
const dispatch = useDispatch()
const currentSafeAddress = useSelector(safeParamAddressFromStateSelector)
const handleClick = event => {
const handleClick = (event) => {
setAnchorEl(event.currentTarget)
}

View File

@ -21,6 +21,7 @@ import Col from '~/components/layout/Col'
import Img from '~/components/layout/Img'
import Row from '~/components/layout/Row'
import type { AddressBookEntry } from '~/logic/addressBook/model/addressBook'
import { makeAddressBookEntry } from '~/logic/addressBook/model/addressBook'
import { addAddressBookEntry } from '~/logic/addressBook/store/actions/addAddressBookEntry'
import { removeAddressBookEntry } from '~/logic/addressBook/store/actions/removeAddressBookEntry'
import { updateAddressBookEntry } from '~/logic/addressBook/store/actions/updateAddressBookEntry'
@ -50,7 +51,7 @@ type Props = {
const AddressBookTable = ({ classes }: Props) => {
const columns = generateColumns()
const autoColumns = columns.filter(c => !c.custom)
const autoColumns = columns.filter((c) => !c.custom)
const dispatch = useDispatch()
const addressBook = useSelector(getAddressBookListSelector)
const [selectedEntry, setSelectedEntry] = useState(null)
@ -67,7 +68,7 @@ const AddressBookTable = ({ classes }: Props) => {
useEffect(() => {
if (entryAddressToEditOrCreateNew) {
const key = addressBook.findKey(entry => entry.address === entryAddressToEditOrCreateNew)
const key = addressBook.findKey((entry) => entry.address === entryAddressToEditOrCreateNew)
if (key >= 0) {
// Edit old entry
const value = addressBook.get(key)
@ -89,13 +90,13 @@ const AddressBookTable = ({ classes }: Props) => {
const newEntryModalHandler = (entry: AddressBookEntry) => {
setEditCreateEntryModalOpen(false)
dispatch(addAddressBookEntry(entry))
dispatch(addAddressBookEntry(makeAddressBookEntry(entry)))
}
const editEntryModalHandler = (entry: AddressBookEntry) => {
setSelectedEntry(null)
setEditCreateEntryModalOpen(false)
dispatch(updateAddressBookEntry(entry))
dispatch(updateAddressBookEntry(makeAddressBookEntry(entry)))
}
const deleteEntryModalHandler = () => {

View File

@ -48,7 +48,7 @@ const appList = [
export default appList
export const getAppInfo = (appId: string) => {
const res = appList.find(app => app.id === appId.toString())
const res = appList.find((app) => app.id === appId.toString())
if (!res) {
return {
id: 0,

View File

@ -13,7 +13,7 @@ import ButtonLink from '~/components/layout/ButtonLink'
const StyledIframe = styled.iframe`
width: 100%;
height: 100%;
display: ${props => (props.shouldDisplay ? 'block' : 'none')};
display: ${(props) => (props.shouldDisplay ? 'block' : 'none')};
`
const operations = {
SEND_TRANSACTIONS: 'sendTransactions',
@ -51,13 +51,13 @@ function Apps({
const [appIsLoading, setAppIsLoading] = useState(true)
const [iframeEl, setframeEl] = useState(null)
const getSelectedApp = () => appsList.find(e => e.id === selectedApp)
const getSelectedApp = () => appsList.find((e) => e.id === selectedApp)
const sendMessageToIframe = (messageId, data) => {
iframeEl.contentWindow.postMessage({ messageId, data }, getSelectedApp().url)
}
const handleIframeMessage = async data => {
const handleIframeMessage = async (data) => {
if (!data || !data.messageId) {
console.warn('iframe: message without messageId')
return
@ -109,7 +109,7 @@ function Apps({
}
}
const iframeRef = useCallback(node => {
const iframeRef = useCallback((node) => {
if (node !== null) {
setframeEl(node)
}
@ -156,7 +156,7 @@ function Apps({
}
}, [iframeEl])
const onSelectApp = appId => {
const onSelectApp = (appId) => {
setAppIsLoading(true)
setSelectedApp(appId)
}

View File

@ -28,7 +28,7 @@ const sendTransactions = (
const encodeMultiSendCalldata = multiSend.methods
.multiSend(
`0x${txs
.map(tx =>
.map((tx) =>
[
web3.eth.abi.encodeParameter('uint8', 0).slice(-2),
web3.eth.abi.encodeParameter('address', tx.to).slice(-40),

View File

@ -0,0 +1,129 @@
// @flow
import TableCell from '@material-ui/core/TableCell'
import TableContainer from '@material-ui/core/TableContainer'
import TableRow from '@material-ui/core/TableRow'
import { makeStyles } from '@material-ui/core/styles'
import CallMade from '@material-ui/icons/CallMade'
import CallReceived from '@material-ui/icons/CallReceived'
import classNames from 'classnames/bind'
import React from 'react'
import { useSelector } from 'react-redux'
import { styles } from './styles'
import Table from '~/components/Table'
import type { Column } from '~/components/Table/TableHead'
import { cellWidth } from '~/components/Table/TableHead'
import Button from '~/components/layout/Button'
import Row from '~/components/layout/Row'
import { currencyValuesListSelector, currentCurrencySelector } from '~/logic/currencyValues/store/selectors'
import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances'
import AssetTableCell from '~/routes/safe/components/Balances/AssetTableCell'
import type { BalanceRow } from '~/routes/safe/components/Balances/dataFetcher'
import {
BALANCE_TABLE_ASSET_ID,
BALANCE_TABLE_BALANCE_ID,
BALANCE_TABLE_VALUE_ID,
generateColumns,
getBalanceData,
} from '~/routes/safe/components/Balances/dataFetcher'
import { extendedSafeTokensSelector, grantedSelector } from '~/routes/safe/container/selector'
const useStyles = makeStyles(styles)
type Props = {
showSendFunds: Function,
showReceiveFunds: Function,
}
const Coins = (props: Props) => {
const { showReceiveFunds, showSendFunds } = props
const classes = useStyles()
const columns = generateColumns()
const autoColumns = columns.filter((c) => !c.custom)
const currencySelected = useSelector(currentCurrencySelector)
const activeTokens = useSelector(extendedSafeTokensSelector)
const currencyValues = useSelector(currencyValuesListSelector)
const granted = useSelector(grantedSelector)
const filteredData = getBalanceData(activeTokens, currencySelected, currencyValues)
return (
<TableContainer>
<Table
columns={columns}
data={filteredData}
defaultFixed
defaultOrderBy={BALANCE_TABLE_ASSET_ID}
defaultRowsPerPage={10}
label="Balances"
size={filteredData.size}
>
{(sortedData: Array<BalanceRow>) =>
sortedData.map((row: any, index: number) => (
<TableRow className={classes.hide} data-testid={BALANCE_ROW_TEST_ID} key={index} tabIndex={-1}>
{autoColumns.map((column: Column) => {
const { align, id, width } = column
let cellItem
switch (id) {
case BALANCE_TABLE_ASSET_ID: {
cellItem = <AssetTableCell asset={row[id]} />
break
}
case BALANCE_TABLE_BALANCE_ID: {
cellItem = <div>{row[id]}</div>
break
}
case BALANCE_TABLE_VALUE_ID: {
cellItem = <div className={classes.currencyValueRow}>{row[id]}</div>
break
}
default: {
cellItem = null
break
}
}
return (
<TableCell align={align} component="td" key={id} style={cellWidth(width)}>
{cellItem}
</TableCell>
)
})}
<TableCell component="td">
<Row align="end" className={classes.actions}>
{granted && (
<Button
className={classes.send}
color="primary"
onClick={() => showSendFunds(row.asset.address)}
size="small"
testId="balance-send-btn"
variant="contained"
>
<CallMade alt="Send Transaction" className={classNames(classes.leftIcon, classes.iconSmall)} />
Send
</Button>
)}
<Button
className={classes.receive}
color="primary"
onClick={showReceiveFunds}
size="small"
variant="contained"
>
<CallReceived
alt="Receive Transaction"
className={classNames(classes.leftIcon, classes.iconSmall)}
/>
Receive
</Button>
</Row>
</TableCell>
</TableRow>
))
}
</Table>
</TableContainer>
)
}
export default Coins

View File

@ -0,0 +1,47 @@
// @flow
import { sm, xs } from '~/theme/variables'
export const styles = () => ({
iconSmall: {
fontSize: 16,
},
hide: {
'&:hover': {
backgroundColor: '#fff3e2',
},
'&:hover $actions': {
visibility: 'initial',
},
'&:focus $actions': {
visibility: 'initial',
},
},
actions: {
justifyContent: 'flex-end',
visibility: 'hidden',
},
receive: {
width: '95px',
minWidth: '95px',
marginLeft: sm,
borderRadius: xs,
'& > span': {
fontSize: '14px',
},
},
send: {
width: '75px',
minWidth: '75px',
borderRadius: xs,
'& > span': {
fontSize: '14px',
},
},
leftIcon: {
marginRight: sm,
},
currencyValueRow: {
maxWidth: '125px',
textAlign: 'right',
},
})

View File

@ -16,7 +16,7 @@ const useStyles = makeStyles({
borderRadius: '8px',
boxShadow: '0 0 10px 0 rgba(33, 48, 77, 0.10)',
boxSizing: 'border-box',
cursor: props => (props.granted ? 'pointer' : 'default'),
cursor: (props) => (props.granted ? 'pointer' : 'default'),
display: 'flex',
flexDirection: 'column',
flexGrow: '1',
@ -54,7 +54,7 @@ const useStyles = makeStyles({
zIndex: '5',
},
image: {
backgroundColor: props => `#${props.backgroundColor}` || '#f0efee',
backgroundColor: (props) => `#${props.backgroundColor}` || '#f0efee',
backgroundPosition: '50% 50%',
backgroundRepeat: 'no-repeat',
backgroundSize: 'contain',

View File

@ -7,9 +7,10 @@ import { useSelector } from 'react-redux'
import Item from './components/Item'
import Paragraph from '~/components/layout/Paragraph'
import type { NFTAssetsState, NFTTokensState } from '~/logic/collectibles/store/reducer/collectibles'
import { nftAssetsSelector, nftTokensSelector } from '~/logic/collectibles/store/selectors'
import type { NFTTokensState } from '~/logic/collectibles/store/reducer/collectibles'
import { activeNftAssetsListSelector, nftTokensSelector } from '~/logic/collectibles/store/selectors'
import SendModal from '~/routes/safe/components/Balances/SendModal'
import { safeSelector } from '~/routes/safe/store/selectors'
import { fontColor, lg, screenSm, screenXs } from '~/theme/variables'
const useStyles = makeStyles({
@ -79,11 +80,11 @@ const Collectibles = () => {
const classes = useStyles()
const [selectedToken, setSelectedToken] = React.useState({})
const [sendNFTsModalOpen, setSendNFTsModalOpen] = React.useState(false)
const nftAssets: NFTAssetsState = useSelector(nftAssetsSelector)
const { address, ethBalance, name } = useSelector(safeSelector)
const nftTokens: NFTTokensState = useSelector(nftTokensSelector)
const nftAssetsKeys = Object.keys(nftAssets)
const activeAssetsList = useSelector(activeNftAssetsListSelector)
const handleItemSend = nftToken => {
const handleItemSend = (nftToken) => {
setSelectedToken(nftToken)
setSendNFTsModalOpen(true)
}
@ -91,10 +92,8 @@ const Collectibles = () => {
return (
<Card className={classes.cardOuter}>
<div className={classes.cardInner}>
{nftAssetsKeys.length ? (
nftAssetsKeys.map(assetAddress => {
const nftAsset = nftAssets[assetAddress]
{activeAssetsList.size ? (
activeAssetsList.map((nftAsset) => {
return (
<React.Fragment key={nftAsset.slug}>
<div className={classes.title}>
@ -105,7 +104,7 @@ const Collectibles = () => {
<div className={classes.gridRow}>
{nftTokens
.filter(({ assetAddress }) => nftAsset.address === assetAddress)
.map(nftToken => (
.map((nftToken) => (
<Item
data={nftToken}
key={`${nftAsset.slug}_${nftToken.tokenId}`}
@ -122,8 +121,11 @@ const Collectibles = () => {
</div>
<SendModal
activeScreenType="sendCollectible"
ethBalance={ethBalance}
isOpen={sendNFTsModalOpen}
onClose={() => setSendNFTsModalOpen(false)}
safeAddress={address}
safeName={name}
selectedToken={selectedToken}
/>
</Card>

View File

@ -77,17 +77,17 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select
const scalableModalSize = activeScreen === 'chooseTxType'
const handleTxCreation = txInfo => {
const handleTxCreation = (txInfo) => {
setActiveScreen('reviewTx')
setTx(txInfo)
}
const handleCustomTxCreation = customTxInfo => {
const handleCustomTxCreation = (customTxInfo) => {
setActiveScreen('reviewCustomTx')
setTx(customTxInfo)
}
const handleSendCollectible = txInfo => {
const handleSendCollectible = (txInfo) => {
setActiveScreen('reviewCollectible')
setTx(txInfo)
}

View File

@ -40,7 +40,7 @@ const textFieldInputStyle = makeStyles(() => ({
},
}))
const filterAddressBookWithContractAddresses = async addressBook => {
const filterAddressBookWithContractAddresses = async (addressBook) => {
const abFlags = await Promise.all(
addressBook.map(async ({ address }) => {
return (await mustBeEthereumContractAddress(address)) === undefined
@ -49,7 +49,7 @@ const filterAddressBookWithContractAddresses = async addressBook => {
return addressBook.filter((adbkEntry, index) => abFlags[index])
}
const isValidEnsName = name => /^([\w-]+\.)+(eth|test|xyz|luxe)$/.test(name)
const isValidEnsName = (name) => /^([\w-]+\.)+(eth|test|xyz|luxe)$/.test(name)
const AddressBookInput = ({
classes,
@ -69,7 +69,7 @@ const AddressBookInput = ({
const [inputAddValue, setInputAddValue] = useState(recipientAddress)
const onAddressInputChanged = async addressValue => {
const onAddressInputChanged = async (addressValue) => {
setInputAddValue(addressValue)
let resolvedAddress = addressValue
let isValidText
@ -92,7 +92,7 @@ const AddressBookInput = ({
// First removes the entries that are not contracts if the operation is custom tx
const adbkToFilter = isCustomTx ? await filterAddressBookWithContractAddresses(addressBook) : addressBook
// Then Filters the entries based on the input of the user
const filteredADBK = adbkToFilter.filter(adbkEntry => {
const filteredADBK = adbkToFilter.filter((adbkEntry) => {
const { address, name } = adbkEntry
return (
name.toLowerCase().includes(addressValue.toLowerCase()) ||
@ -137,7 +137,7 @@ const AddressBookInput = ({
closeIcon={null}
disableOpenOnFocus
filterOptions={(optionsArray, { inputValue }) =>
optionsArray.filter(item => {
optionsArray.filter((item) => {
const inputLowerCase = inputValue.toLowerCase()
const foundName = item.name.toLowerCase().includes(inputLowerCase)
const foundAddress = item.address.toLowerCase().includes(inputLowerCase)
@ -145,7 +145,7 @@ const AddressBookInput = ({
})
}
freeSolo
getOptionLabel={adbkEntry => adbkEntry.address || ''}
getOptionLabel={(adbkEntry) => adbkEntry.address || ''}
id="free-solo-demo"
onChange={(event, value) => {
let address = ''
@ -164,7 +164,7 @@ const AddressBookInput = ({
}}
open={!blurred}
options={adbkList.toArray()}
renderInput={params => (
renderInput={(params) => (
<MuiTextField
{...params}
// eslint-disable-next-line
@ -185,7 +185,7 @@ const AddressBookInput = ({
className: statusClasses,
}}
label={!isValidForm ? validationText : 'Recipient'}
onChange={event => {
onChange={(event) => {
setInputTouched(true)
onAddressInputChanged(event.target.value)
}}
@ -193,7 +193,7 @@ const AddressBookInput = ({
variant="filled"
/>
)}
renderOption={adbkEntry => {
renderOption={(adbkEntry) => {
const { address, name } = adbkEntry
return (
<div className={classes.itemOptionList}>

View File

@ -53,7 +53,7 @@ const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }: Props
const [gasCosts, setGasCosts] = useState<string>('< 0.001')
const [data, setData] = useState('')
const txToken = tokens.find(token => token.address === tx.token)
const txToken = tokens.find((token) => token.address === tx.token)
const isSendingETH = txToken.address === ETH_ADDRESS
const txRecipient = isSendingETH ? tx.recipientAddress : txToken.address

View File

@ -67,10 +67,10 @@ const CollectibleSelectField = ({ initialValue, tokens }: SelectFieldProps) => {
disabled={!tokens.length}
initialValue={initialValue}
name="nftTokenId"
renderValue={nftTokenId => <SelectedCollectible tokenId={nftTokenId} tokens={tokens} />}
renderValue={(nftTokenId) => <SelectedCollectible tokenId={nftTokenId} tokens={tokens} />}
validate={required}
>
{tokens.map(token => (
{tokens.map((token) => (
<MenuItem key={`${token.assetAddress}-${token.tokenId}`} value={token.tokenId}>
<ListItemIcon className={classes.tokenImage}>
<Img alt={token.name} height={28} onError={setImageToPlaceholder} src={token.image} />

View File

@ -69,10 +69,10 @@ const TokenSelectField = ({ assets, initialValue }: SelectFieldProps) => {
disabled={!assetsAddresses.length}
initialValue={initialValue}
name="assetAddress"
renderValue={assetAddress => <SelectedToken assetAddress={assetAddress} assets={assets} />}
renderValue={(assetAddress) => <SelectedToken assetAddress={assetAddress} assets={assets} />}
validate={required}
>
{assetsAddresses.map(assetAddress => {
{assetsAddresses.map((assetAddress) => {
const asset = assets[assetAddress]
return (

View File

@ -24,7 +24,7 @@ import Img from '~/components/layout/Img'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
import type { NFTAssetsState, NFTTokensState } from '~/logic/collectibles/store/reducer/collectibles'
import { nftAssetsSelector, nftTokensSelector } from '~/logic/collectibles/store/selectors'
import { nftTokensSelector, safeActiveSelectorMap } from '~/logic/collectibles/store/selectors'
import type { NFTToken } from '~/routes/safe/components/Balances/Collectibles/types'
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
import AddressBookInput from '~/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
@ -36,7 +36,7 @@ import { sm } from '~/theme/variables'
type Props = {
initialValues: Object,
onClose: () => void,
onNext: any => void,
onNext: (any) => void,
recipientAddress?: string,
selectedToken?: NFTToken | {},
}
@ -58,7 +58,7 @@ const useStyles = makeStyles(styles)
const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, selectedToken = {} }: Props) => {
const classes = useStyles()
const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
const nftAssets: NFTAssetsState = useSelector(nftAssetsSelector)
const nftAssets: NFTAssetsState = useSelector(safeActiveSelectorMap)
const nftTokens: NFTTokensState = useSelector(nftTokensSelector)
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
const [selectedEntry, setSelectedEntry] = useState<Object | null>({
@ -74,7 +74,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
}
}, [selectedEntry, pristine])
const handleSubmit = values => {
const handleSubmit = (values) => {
// If the input wasn't modified, there was no mutation of the recipientAddress
if (!values.recipientAddress) {
values.recipientAddress = selectedEntry.address
@ -110,9 +110,9 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
const formState = args[2]
const mutators = args[3]
const { assetAddress } = formState.values
const selectedNFTTokens = nftTokens.filter(nftToken => nftToken.assetAddress === assetAddress)
const selectedNFTTokens = nftTokens.filter((nftToken) => nftToken.assetAddress === assetAddress)
const handleScan = value => {
const handleScan = (value) => {
let scannedAddress = value
if (scannedAddress.startsWith('ethereum:')) {
@ -144,7 +144,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
</Row>
{selectedEntry && selectedEntry.address ? (
<div
onKeyDown={e => {
onKeyDown={(e) => {
if (e.keyCode !== 9) {
setSelectedEntry(null)
}

View File

@ -36,7 +36,7 @@ import { sm } from '~/theme/variables'
type Props = {
initialValues: Object,
onClose: () => void,
onNext: any => void,
onNext: (any) => void,
recipientAddress: string,
}
@ -103,7 +103,7 @@ const SendCustomTx = ({ initialValues, onClose, onNext, recipientAddress }: Prop
shouldDisableSubmitButton = !selectedEntry.address
}
const handleScan = value => {
const handleScan = (value) => {
let scannedAddress = value
if (scannedAddress.startsWith('ethereum:')) {
@ -128,7 +128,7 @@ const SendCustomTx = ({ initialValues, onClose, onNext, recipientAddress }: Prop
</Row>
{selectedEntry && selectedEntry.address ? (
<div
onKeyDown={e => {
onKeyDown={(e) => {
if (e.keyCode !== 9) {
setSelectedEntry(null)
}

View File

@ -64,10 +64,10 @@ const TokenSelectField = ({ classes, initialValue, isValid, tokens }: SelectFiel
displayEmpty
initialValue={initialValue}
name="token"
renderValue={tokenAddress => <SelectedTokenStyled tokenAddress={tokenAddress} tokens={tokens} />}
renderValue={(tokenAddress) => <SelectedTokenStyled tokenAddress={tokenAddress} tokens={tokens} />}
validate={required}
>
{tokens.map(token => (
{tokens.map((token) => (
<MenuItem key={token.address} value={token.address}>
<ListItemIcon>
<Img alt={token.name} height={28} onError={setImageToPlaceholder} src={token.logoUri} />

View File

@ -39,7 +39,7 @@ import { sm } from '~/theme/variables'
type Props = {
initialValues: Object,
onClose: () => void,
onNext: any => void,
onNext: (any) => void,
recipientAddress?: string,
selectedToken: string,
}
@ -76,7 +76,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
}
}, [selectedEntry, pristine])
const handleSubmit = values => {
const handleSubmit = (values) => {
const submitValues = values
// If the input wasn't modified, there was no mutation of the recipientAddress
if (!values.recipientAddress) {
@ -110,9 +110,9 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
const formState = args[2]
const mutators = args[3]
const { token: tokenAddress } = formState.values
const selectedTokenRecord = tokens.find(token => token.address === tokenAddress)
const selectedTokenRecord = tokens.find((token) => token.address === tokenAddress)
const handleScan = value => {
const handleScan = (value) => {
let scannedAddress = value
if (scannedAddress.startsWith('ethereum:')) {
@ -142,7 +142,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
</Row>
{selectedEntry && selectedEntry.address ? (
<div
onKeyDown={e => {
onKeyDown={(e) => {
if (e.keyCode !== 9) {
setSelectedEntry(null)
}

View File

@ -4,7 +4,7 @@ import { withStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close'
import { List } from 'immutable'
import React, { useState } from 'react'
import { connect } from 'react-redux'
import { connect, useSelector } from 'react-redux'
import actions, { type Actions } from './actions'
import { styles } from './style'
@ -13,11 +13,18 @@ import Hairline from '~/components/layout/Hairline'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
import { type Token } from '~/logic/tokens/store/model/token'
import { orderedTokenListSelector } from '~/logic/tokens/store/selectors'
import AddCustomAssetComponent from '~/routes/safe/components/Balances/Tokens/screens/AddCustomAsset'
import AddCustomToken from '~/routes/safe/components/Balances/Tokens/screens/AddCustomToken'
import AssetsList from '~/routes/safe/components/Balances/Tokens/screens/AssetsList'
import TokenList from '~/routes/safe/components/Balances/Tokens/screens/TokenList'
import { extendedSafeTokensSelector } from '~/routes/safe/container/selector'
import { safeBlacklistedTokensSelector } from '~/routes/safe/store/selectors'
export const MANAGE_TOKENS_MODAL_CLOSE_BUTTON_TEST_ID = 'manage-tokens-close-modal-btn'
type ActiveScreen = 'tokenList' | 'addCustomToken' | 'assetsList' | 'addCustomAsset'
type Props = Actions & {
onClose: () => void,
classes: Object,
@ -25,24 +32,25 @@ type Props = Actions & {
safeAddress: string,
activeTokens: List<Token>,
blacklistedTokens: List<Token>,
modalScreen: ActiveScreen,
}
type ActiveScreen = 'tokenList' | 'addCustomToken'
const Tokens = (props: Props) => {
const [activeScreen, setActiveScreen] = useState<ActiveScreen>('tokenList')
const {
activateTokenForAllSafes,
activeTokens,
addToken,
blacklistedTokens,
classes,
fetchTokens,
modalScreen,
onClose,
safeAddress,
tokens,
updateActiveTokens,
updateBlacklistedTokens,
} = props
const tokens = useSelector(orderedTokenListSelector)
const activeTokens = useSelector(extendedSafeTokensSelector)
const blacklistedTokens = useSelector(safeBlacklistedTokensSelector)
const [activeScreen, setActiveScreen] = useState<ActiveScreen>(modalScreen)
return (
<>
@ -67,18 +75,23 @@ const Tokens = (props: Props) => {
updateBlacklistedTokens={updateBlacklistedTokens}
/>
)}
{activeScreen === 'assetsList' && <AssetsList setActiveScreen={setActiveScreen} />}
{activeScreen === 'addCustomToken' && (
<AddCustomToken
activateTokenForAllSafes={activateTokenForAllSafes}
activeTokens={activeTokens}
addToken={addToken}
onClose={onClose}
parentList={'tokenList'}
safeAddress={safeAddress}
setActiveScreen={setActiveScreen}
tokens={tokens}
updateActiveTokens={updateActiveTokens}
/>
)}
{activeScreen === 'addCustomAsset' && (
<AddCustomAssetComponent onClose={onClose} parentList={'assetsList'} setActiveScreen={setActiveScreen} />
)}
</>
)
}

View File

@ -0,0 +1,190 @@
// @flow
import { withStyles } from '@material-ui/core/styles'
import { List } from 'immutable'
import React, { useState } from 'react'
import { FormSpy } from 'react-final-form'
import { useSelector } from 'react-redux'
import { styles } from './style'
import { getSymbolAndDecimalsFromContract } from './utils'
import Checkbox from '~/components/forms/Checkbox'
import Field from '~/components/forms/Field'
import GnoForm from '~/components/forms/GnoForm'
import TextField from '~/components/forms/TextField'
import { composeValidators, minMaxLength, mustBeEthereumAddress, required } from '~/components/forms/validator'
import Block from '~/components/layout/Block'
import Button from '~/components/layout/Button'
import Col from '~/components/layout/Col'
import Hairline from '~/components/layout/Hairline'
import Img from '~/components/layout/Img'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
import type { NFTAssetsState } from '~/logic/collectibles/store/reducer/collectibles'
import { nftAssetsListSelector } from '~/logic/collectibles/store/selectors'
import { type Token, type TokenProps } from '~/logic/tokens/store/model/token'
import {
addressIsAssetContract,
doesntExistInAssetsList,
} from '~/routes/safe/components/Balances/Tokens/screens/AddCustomAsset/validators'
import TokenPlaceholder from '~/routes/safe/components/Balances/assets/token_placeholder.svg'
export const ADD_CUSTOM_ASSET_ADDRESS_INPUT_TEST_ID = 'add-custom-asset-address-input'
export const ADD_CUSTOM_ASSET_SYMBOLS_INPUT_TEST_ID = 'add-custom-asset-symbols-input'
export const ADD_CUSTOM_ASSET_DECIMALS_INPUT_TEST_ID = 'add-custom-asset-decimals-input'
export const ADD_CUSTOM_ASSET_FORM = 'add-custom-asset-form'
type Props = {
classes: Object,
addToken: Function,
updateActiveTokens: Function,
safeAddress: string,
activeTokens: List<TokenProps>,
tokens: List<Token>,
setActiveScreen: Function,
onClose: Function,
activateTokenForAllSafes: Function,
parentList: 'assetsList' | 'tokenList',
}
const INITIAL_FORM_STATE = {
address: '',
decimals: '',
symbol: '',
logoUri: '',
}
const AddCustomAsset = (props: Props) => {
const { classes, onClose, parentList, setActiveScreen } = props
const nftAssetsList: NFTAssetsState = useSelector(nftAssetsListSelector)
const [formValues, setFormValues] = useState(INITIAL_FORM_STATE)
const handleSubmit = () => {
onClose()
}
const populateFormValuesFromAddress = async (tokenAddress: string) => {
const tokenData = await getSymbolAndDecimalsFromContract(tokenAddress)
if (tokenData.length) {
const [symbol, decimals] = tokenData
setFormValues({
address: tokenAddress,
symbol,
decimals,
name: symbol,
})
}
}
const formSpyOnChangeHandler = async (state) => {
const { dirty, errors, submitSucceeded, validating, values } = state
// for some reason this is called after submitting, we don't need to update the values
// after submit
if (submitSucceeded) {
return
}
if (dirty && !validating && errors.address) {
setFormValues(INITIAL_FORM_STATE)
}
if (!errors.address && !validating && dirty) {
await populateFormValuesFromAddress(values.address)
}
}
const goBack = () => {
setActiveScreen(parentList)
}
return (
<>
<GnoForm initialValues={formValues} onSubmit={handleSubmit} testId={ADD_CUSTOM_ASSET_FORM}>
{() => (
<>
<Block className={classes.formContainer}>
<Paragraph className={classes.title} noMargin size="lg" weight="bolder">
Add custom asset
</Paragraph>
<Field
className={classes.addressInput}
component={TextField}
name="address"
placeholder="Asset contract address*"
testId={ADD_CUSTOM_ASSET_ADDRESS_INPUT_TEST_ID}
text="Token contract address*"
type="text"
validate={composeValidators(
required,
mustBeEthereumAddress,
doesntExistInAssetsList(nftAssetsList),
addressIsAssetContract,
)}
/>
<FormSpy
onChange={formSpyOnChangeHandler}
subscription={{
values: true,
errors: true,
validating: true,
dirty: true,
submitSucceeded: true,
}}
/>
<Row>
<Col layout="column" xs={6}>
<Field
className={classes.addressInput}
component={TextField}
name="symbol"
placeholder="Token symbol*"
testId={ADD_CUSTOM_ASSET_SYMBOLS_INPUT_TEST_ID}
text="Token symbol"
type="text"
validate={composeValidators(required, minMaxLength(2, 12))}
/>
<Field
className={classes.addressInput}
component={TextField}
disabled
name="decimals"
placeholder="Token decimals*"
testId={ADD_CUSTOM_ASSET_DECIMALS_INPUT_TEST_ID}
text="Token decimals*"
type="text"
/>
<Block justify="left">
<Field className={classes.checkbox} component={Checkbox} name="showForAllSafes" type="checkbox" />
<Paragraph className={classes.checkboxLabel} size="md" weight="bolder">
Activate assets for all Safes
</Paragraph>
</Block>
</Col>
<Col align="center" layout="column" xs={6}>
<Paragraph className={classes.tokenImageHeading}>Token Image</Paragraph>
<Img alt="Token image" height={100} src={TokenPlaceholder} />
</Col>
</Row>
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button minHeight={42} minWidth={140} onClick={goBack}>
Cancel
</Button>
<Button color="primary" minHeight={42} minWidth={140} type="submit" variant="contained">
Save
</Button>
</Row>
</>
)}
</GnoForm>
</>
)
}
const AddCustomAssetComponent = withStyles(styles)(AddCustomAsset)
export default AddCustomAssetComponent

Some files were not shown because too many files have changed in this diff Show More