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:
parent
0c5afc0793
commit
8ff6695562
|
@ -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=
|
68
package.json
68
package.json
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
||||
|
|
|
@ -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 |
|
@ -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'
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
// @flow
|
||||
export { default as Stepper } from './Stepper'
|
|
@ -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' }],
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@ const PageFrame = ({ children, classes, currentNetwork }: Props) => {
|
|||
|
||||
export default withStyles(notificationStyles)(
|
||||
connect(
|
||||
state => ({
|
||||
(state) => ({
|
||||
currentNetwork: networkSelector(state),
|
||||
}),
|
||||
null,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
)
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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],
|
||||
)
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
export * from './gas'
|
||||
export * from './send'
|
||||
export * from './safeTxSignerEIP712'
|
||||
export * from './offchainSigner'
|
||||
export * from './txHistory'
|
||||
export * from './notifiedTransactions'
|
||||
|
|
|
@ -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, ''))
|
||||
})
|
||||
})
|
||||
}
|
|
@ -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, ''))
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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, '')
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -9,7 +9,7 @@ const fetchTokenBalanceList = (safeAddress: string) => {
|
|||
|
||||
return axios.get(url, {
|
||||
params: {
|
||||
limit: 300,
|
||||
limit: 3000,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ const fetchTokenList = () => {
|
|||
|
||||
return axios.get(url, {
|
||||
params: {
|
||||
limit: 300,
|
||||
limit: 3000,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
function transactionDataCheck() {
|
||||
let completed = false
|
||||
return stateAndHelpers => {
|
||||
return (stateAndHelpers) => {
|
||||
const { wallet } = stateAndHelpers
|
||||
|
||||
if (wallet && wallet.name === 'Ledger' && !completed) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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> => {
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ const CreateEditEntryModalComponent = ({
|
|||
newEntryModalHandler,
|
||||
onClose,
|
||||
}: Props) => {
|
||||
const onFormSubmitted = values => {
|
||||
const onFormSubmitted = (values) => {
|
||||
if (entryToEdit && !entryToEdit.entry.isNew) {
|
||||
editEntryModalHandler(values)
|
||||
} else {
|
||||
|
|
|
@ -26,7 +26,7 @@ type Props = {
|
|||
}
|
||||
|
||||
const DeleteEntryModalComponent = ({ classes, deleteEntryModalHandler, entryToDelete, isOpen, onClose }: Props) => {
|
||||
const handleDeleteEntrySubmit = values => {
|
||||
const handleDeleteEntrySubmit = (values) => {
|
||||
deleteEntryModalHandler(values, entryToDelete.index)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
|
@ -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',
|
||||
},
|
||||
})
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue