Merge branch 'development' into AddingTestId

This commit is contained in:
francovenica 2020-07-07 17:13:14 -03:00 committed by GitHub
commit 6b3d7304f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1713 additions and 779 deletions

2
.gitignore vendored
View File

@ -1,5 +1,5 @@
node_modules/
build/
./build
.DS_Store
yarn-error.log
.env*

View File

@ -1,6 +1,6 @@
{
"name": "safe-react",
"version": "2.4.0",
"version": "2.5.2",
"description": "Allowing crypto users manage funds in a safer way",
"website": "https://github.com/gnosis/safe-react#readme",
"bugs": {
@ -151,28 +151,29 @@
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
"@gnosis.pm/safe-react-components": "^0.1.3",
"@gnosis.pm/util-contracts": "2.0.6",
"@ledgerhq/hw-transport-node-hid": "5.16.0",
"@material-ui/core": "4.10.1",
"@ledgerhq/hw-transport-node-hid": "5.19.0",
"@material-ui/core": "4.11.0",
"@material-ui/icons": "4.9.1",
"@material-ui/lab": "4.0.0-alpha.39",
"@openzeppelin/contracts": "3.0.2",
"@openzeppelin/contracts": "3.1.0",
"async-sema": "^3.1.0",
"axios": "0.19.2",
"bignumber.js": "9.0.0",
"bnc-onboard": "1.10.0",
"bnc-onboard": "1.10.2",
"classnames": "^2.2.6",
"concurrently": "^5.2.0",
"connected-react-router": "6.8.0",
"currency-flags": "2.1.2",
"date-fns": "2.14.0",
"electron-is-dev": "^1.1.0",
"electron-log": "4.2.1",
"electron-settings": "^4.0.0",
"electron-log": "4.2.2",
"electron-settings": "4.0.2",
"electron-updater": "4.3.1",
"eth-sig-util": "^2.5.3",
"ethereum-blockies-base64": "^1.0.2",
"exponential-backoff": "^3.0.1",
"express": "^4.17.1",
"final-form": "^4.20.0",
"final-form": "4.20.1",
"final-form-calculate": "^1.3.1",
"history": "4.10.1",
"immortal-db": "^1.0.2",
@ -182,9 +183,9 @@
"material-ui-search-bar": "^1.0.0-beta.13",
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
"open": "^7.0.3",
"polished": "3.6.4",
"polished": "3.6.5",
"qrcode.react": "1.0.0",
"query-string": "6.13.0",
"query-string": "6.13.1",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-final-form": "^6.5.0",
@ -204,41 +205,41 @@
"semver": "7.3.2",
"styled-components": "^5.0.1",
"truffle-contract": "4.0.31",
"web3": "1.2.8"
"web3": "1.2.9"
},
"devDependencies": {
"@testing-library/jest-dom": "5.9.0",
"@testing-library/react": "10.2.1",
"@testing-library/jest-dom": "5.11.0",
"@testing-library/react": "10.4.3",
"@testing-library/user-event": "11.3.1",
"@types/jest": "^25.2.1",
"@types/node": "14.0.12",
"@types/node": "14.0.14",
"@types/react": "^16.9.32",
"@types/react-dom": "^16.9.6",
"@types/styled-components": "^5.1.0",
"@typescript-eslint/eslint-plugin": "3.2.0",
"@typescript-eslint/parser": "3.2.0",
"autoprefixer": "9.8.0",
"@typescript-eslint/eslint-plugin": "3.5.0",
"@typescript-eslint/parser": "3.5.0",
"autoprefixer": "9.8.4",
"cross-env": "^7.0.2",
"dotenv": "^8.2.0",
"dotenv-expand": "^5.1.0",
"electron": "7.1.8",
"electron": "7.2.4",
"electron-builder": "22.7.0",
"electron-notarize": "0.3.0",
"eslint": "6.8.0",
"eslint-config-prettier": "6.11.0",
"eslint-plugin-import": "2.21.1",
"eslint-plugin-import": "2.22.0",
"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.4",
"eslint-plugin-react": "7.20.3",
"eslint-plugin-sort-destructure-keys": "1.3.5",
"ethereumjs-abi": "0.6.8",
"husky": "^4.2.2",
"lint-staged": "10.2.9",
"lint-staged": "10.2.11",
"node-sass": "^4.14.1",
"prettier": "2.0.5",
"react-app-rewired": "^2.1.6",
"truffle": "5.1.29",
"typescript": "^3.9.5",
"truffle": "5.1.33",
"typescript": "3.9.6",
"wait-on": "5.0.1",
"web3-eth-contract": "^1.2.9",
"web3-utils": "^1.2.8"

BIN
public/build/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

@ -18,7 +18,7 @@ const useStyles = makeStyles({
justifyContent: 'center',
margin: '0 auto',
maxWidth: '100%',
padding: `40px ${sm} 20px`,
padding: `20px ${sm} 20px`,
width: `${screenSm}px`,
},
item: {

View File

@ -3,8 +3,7 @@ import styled from 'styled-components'
export const Wrapper = styled.div`
display: grid;
grid-template-columns: 245px auto;
grid-template-rows: 514px;
min-height: 525px;
min-height: 560px;
.background {
box-shadow: 1px 2px 10px 0 rgba(212, 212, 211, 0.59);
background-color: white;

View File

@ -40,6 +40,14 @@ export const greaterThan = (min: number | string) => (value: string) => {
return `Should be greater than ${min}`
}
export const equalOrGreaterThan = (min: number | string) => (value: string): undefined | string => {
if (Number.isNaN(Number(value)) || Number.parseFloat(value) >= Number(min)) {
return undefined
}
return `Should be equal or greater than ${min}`
}
const regexQuery = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i
const url = new RegExp(regexQuery)
export const mustBeUrl = (value: string) => {

View File

@ -9,10 +9,17 @@
@media only screen and (max-width: #{$screenLg}px) {
.page {
padding: 72px $lg 0px $lg;
padding: 72px $lg 0 $lg;
}
}
@media only screen and (min-width: #{$screenLg}px) and (max-width: 1360px) {
.page {
padding: 96px 120px 0 120px;
}
}
.center {
align-self: center;
}

View File

@ -1,5 +1,6 @@
import { checksumAddress } from 'src/utils/checksumAddress';
import { ensureOnce } from 'src/utils/singleton'
import { ETHEREUM_NETWORK, getWeb3 } from 'src/logic/wallets/getWeb3'
import { ETHEREUM_NETWORK } from 'src/logic/wallets/getWeb3'
import {
RELAY_API_URL,
SIGNATURES_VIA_METAMASK,
@ -90,7 +91,7 @@ export const getSafeLastVersion = () => process.env.REACT_APP_LATEST_SAFE_VERSIO
export const buildSafeCreationTxUrl = (safeAddress) => {
const host = getTxServiceHost()
const address = getWeb3().utils.toChecksumAddress(safeAddress)
const address = checksumAddress(safeAddress)
const base = getSafeCreationTxUri(address)
return `${host}${base}`

View File

@ -4,6 +4,58 @@ import { ETHEREUM_NETWORK } from 'src/logic/wallets/getWeb3'
import NFTIcon from 'src/routes/safe/components/Balances/assets/nft_icon.png'
import { OPENSEA_API_KEY } from 'src/utils/constants'
export interface OpenSeaAssetContract {
address: string
name: string
image_url: string
symbol: string
}
export interface OpenSeaCollection {
name: string
slug: string
}
export interface OpenSeaAsset {
asset_contract: OpenSeaAssetContract
background_color: string
collection: OpenSeaCollection
description: string
image_thumbnail_url: string
name: string
token_id: string
}
export type OpenSeaAssets = Array<OpenSeaAsset>
export interface NFTAsset {
address: string
assetContract: OpenSeaAssetContract
collection: OpenSeaCollection
description: string
image: string
name: string
numberOfTokens: number
slug: string
symbol: string
}
export type NFTAssets = Record<string, NFTAsset>
export interface NFTToken {
assetAddress: string
color: string
description: string
image: string
name: string
tokenId: number | string
}
export type NFTTokens = Array<NFTToken>
export interface Collectibles {
nftAssets: NFTAssets
nftTokens: NFTTokens
}
class OpenSea {
_rateLimit = async () => {}
@ -29,7 +81,7 @@ class OpenSea {
this._rateLimit = RateLimit(options.rps, { timeUnit: 60 * 1000, uniformDistribution: true })
}
static extractAssets(assets) {
static extractAssets(assets: OpenSeaAssets): NFTAssets {
const extractNFTAsset = (asset) => ({
address: asset.asset_contract.address,
assetContract: asset.asset_contract,
@ -59,7 +111,7 @@ class OpenSea {
}, {})
}
static extractTokens(assets) {
static extractTokens(assets: OpenSeaAssets): NFTTokens {
return assets.map((asset) => ({
assetAddress: asset.asset_contract.address,
color: asset.background_color,
@ -70,7 +122,7 @@ class OpenSea {
}))
}
static extractCollectiblesInfo(assetResponseJson) {
static extractCollectiblesInfo(assetResponseJson: { assets: OpenSeaAssets }): Collectibles {
return {
nftAssets: OpenSea.extractAssets(assetResponseJson.assets),
nftTokens: OpenSea.extractTokens(assetResponseJson.assets),
@ -82,9 +134,9 @@ class OpenSea {
* for the provided Safe Address in the specified Network
* @param {string} safeAddress
* @param {string} network
* @returns {Promise<{ nftAssets: Map<string, NFTAsset>, nftTokens: Array<NFTToken> }>}
* @returns {Promise<Collectibles>}
*/
async fetchAllUserCollectiblesByCategoryAsync(safeAddress, network) {
async fetchAllUserCollectiblesByCategoryAsync(safeAddress: string, network: string): Promise<Collectibles> {
// eslint-disable-next-line no-underscore-dangle
const metadataSourceUrl = this._endpointsUrls[network]
const url = `${metadataSourceUrl}/assets/?owner=${safeAddress}`

View File

@ -2,9 +2,12 @@ import MockedOpenSea from 'src/logic/collectibles/sources/MockedOpenSea'
import OpenSea from 'src/logic/collectibles/sources/OpenSea'
import { COLLECTIBLES_SOURCE } from 'src/utils/constants'
const sources = {
const SOURCES = {
opensea: new OpenSea({ rps: 4 }),
mockedopensea: new MockedOpenSea({ rps: 4 }),
}
export const getConfiguredSource = () => sources[COLLECTIBLES_SOURCE.toLowerCase()]
type Sources = typeof SOURCES
export const getConfiguredSource = (): Sources['opensea'] | Sources['mockedopensea'] =>
SOURCES[COLLECTIBLES_SOURCE.toLowerCase()]

View File

@ -3,18 +3,21 @@ import { batch } from 'react-redux'
import { getNetwork } from 'src/config'
import { getConfiguredSource } from 'src/logic/collectibles/sources'
import { addNftAssets, addNftTokens } from 'src/logic/collectibles/store/actions/addCollectibles'
import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors'
import { Dispatch } from 'redux'
const fetchCollectibles = () => async (dispatch, getState) => {
const network = getNetwork()
const safeAddress = safeParamAddressFromStateSelector(getState()) || ''
const source = getConfiguredSource()
const collectibles = await source.fetchAllUserCollectiblesByCategoryAsync(safeAddress, network)
const fetchCollectibles = (safeAddress: string) => async (dispatch: Dispatch): Promise<void> => {
try {
const network = getNetwork()
const source = getConfiguredSource()
const collectibles = await source.fetchAllUserCollectiblesByCategoryAsync(safeAddress, network)
batch(() => {
dispatch(addNftAssets(collectibles.nftAssets))
dispatch(addNftTokens(collectibles.nftTokens))
})
batch(() => {
dispatch(addNftAssets(collectibles.nftAssets))
dispatch(addNftTokens(collectibles.nftTokens))
})
} catch (error) {
console.log('Error fetching collectibles:', error)
}
}
export default fetchCollectibles

View File

@ -1,11 +1,13 @@
import { List } from 'immutable'
import { createSelector } from 'reselect'
import { NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/OpenSea'
import { AppReduxState } from 'src/store'
import { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID } from 'src/logic/collectibles/store/reducer/collectibles'
import { safeActiveAssetsSelector } from 'src/routes/safe/store/selectors'
export const nftAssetsSelector = (state) => state[NFT_ASSETS_REDUCER_ID]
export const nftTokensSelector = (state) => state[NFT_TOKENS_REDUCER_ID]
export const nftAssetsSelector = (state: AppReduxState): NFTAssets => state[NFT_ASSETS_REDUCER_ID]
export const nftTokensSelector = (state: AppReduxState): NFTTokens => state[NFT_TOKENS_REDUCER_ID]
export const nftAssetsListSelector = createSelector(nftAssetsSelector, (assets) => {
return assets ? List(Object.entries(assets).map((item) => item[1])) : List([])

View File

@ -11,12 +11,18 @@ import { makeToken } from 'src/logic/tokens/store/model/token'
import { TOKEN_REDUCER_ID } from 'src/logic/tokens/store/reducer/tokens'
import updateSafe from 'src/routes/safe/store/actions/updateSafe'
import { SAFE_REDUCER_ID } from 'src/routes/safe/store/reducer/safe'
import { Dispatch } from 'redux'
import { backOff } from 'exponential-backoff'
import { AppReduxState } from 'src/store'
const humanReadableBalance = (balance, decimals) => new BigNumber(balance).times(`1e-${decimals}`).toFixed()
const noFunc = () => {}
const updateSafeValue = (address) => (valueToUpdate) => updateSafe({ address, ...valueToUpdate })
const fetchSafeTokens = (safeAddress) => async (dispatch, getState) => {
const fetchSafeTokens = (safeAddress: string) => async (
dispatch: Dispatch,
getState: () => AppReduxState,
): Promise<void> => {
try {
const state = getState()
const safe = state[SAFE_REDUCER_ID].getIn([SAFE_REDUCER_ID, safeAddress])
@ -26,7 +32,7 @@ const fetchSafeTokens = (safeAddress) => async (dispatch, getState) => {
return
}
const result = await fetchTokenCurrenciesBalances(safeAddress)
const result = await backOff(() => fetchTokenCurrenciesBalances(safeAddress))
const currentEthBalance = safe.get('ethBalance')
const safeBalances = safe.get('balances')
const alreadyActiveTokens = safe.get('activeTokens')
@ -95,8 +101,6 @@ const fetchSafeTokens = (safeAddress) => async (dispatch, getState) => {
} catch (err) {
console.error('Error fetching active token list', err)
}
return null
}
export default fetchSafeTokens

View File

@ -9,7 +9,7 @@ import saveTokens from './saveTokens'
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
import { fetchTokenList } from 'src/logic/tokens/api'
import { makeToken } from 'src/logic/tokens/store/model/token'
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
import { tokensSelector } from 'src/logic/tokens/store/selectors'
import { getWeb3 } from 'src/logic/wallets/getWeb3'
import { store } from 'src/store'
@ -57,7 +57,7 @@ const getTokenValues = (tokenAddress) =>
methods: ['decimals', 'name', 'symbol'],
})
export const getTokenInfos = async (tokenAddress) => {
export const getTokenInfos = async (tokenAddress: string): Promise<Token> => {
if (!tokenAddress) {
return null
}

View File

@ -10,6 +10,7 @@ import { ALTERNATIVE_TOKEN_ABI } from 'src/logic/tokens/utils/alternativeAbi'
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
import { isEmptyData } from 'src/routes/safe/store/actions/transactions/utils/transactionHelpers'
import { TxServiceModel } from 'src/routes/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
import { Map } from 'immutable'
export const ETH_ADDRESS = '0x000'
export const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '42842e0e'
@ -39,11 +40,15 @@ export const isAddressAToken = async (tokenAddress: string): Promise<boolean> =>
return call !== '0x'
}
export const isTokenTransfer = (tx: any): boolean => {
export const isTokenTransfer = (tx: TxServiceModel): boolean => {
return !isEmptyData(tx.data) && tx.data.substring(0, 10) === '0xa9059cbb' && Number(tx.value) === 0
}
export const isSendERC721Transaction = (tx: any, txCode: string, knownTokens: any) => {
export const isSendERC721Transaction = (
tx: TxServiceModel,
txCode: string,
knownTokens: Map<string, Token>,
): boolean => {
// "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85" - ens token contract, includes safeTransferFrom
// but no proper ERC721 standard implemented
return (
@ -79,9 +84,9 @@ export const getERC20DecimalsAndSymbol = async (
address: tokenAddress,
methods: ['decimals', 'symbol'],
})
return { decimals: Number(tokenDecimals), symbol: tokenSymbol }
}
return { decimals: storedTokenInfo.decimals as number, symbol: storedTokenInfo.symbol }
} catch (err) {
console.error(`Failed to retrieve token info for ERC20 token ${tokenAddress}`)
}
@ -92,7 +97,7 @@ export const getERC20DecimalsAndSymbol = async (
export const isSendERC20Transaction = async (
tx: TxServiceModel,
txCode: string,
knownTokens: any,
knownTokens: Map<string, Token>,
): Promise<boolean> => {
let isSendTokenTx = !isSendERC721Transaction(tx, txCode, knownTokens) && isTokenTransfer(tx)

View File

@ -1,18 +1,19 @@
function transactionDataCheck(): any {
let completed = false
return (stateAndHelpers) => {
const { wallet } = stateAndHelpers
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
import { WALLET_PROVIDER } from 'src/logic/wallets/getWeb3'
if (wallet && wallet.name === 'Ledger' && !completed) {
const USER_ENABLED_LEDGER_TX_DATA = 'USER_ENABLED_LEDGER_TX_DATA'
function transactionDataCheck(): any {
return async (stateAndHelpers) => {
const { wallet } = stateAndHelpers
const isTransactionDataEnabled = await loadFromStorage<boolean>(USER_ENABLED_LEDGER_TX_DATA)
if (wallet && wallet.name === WALLET_PROVIDER.LEDGER && !isTransactionDataEnabled) {
return {
heading: 'Allow Transaction Data', // edit modal heading here
description: 'Please allow transaction data on your Ledger device.', // edit modal description that is displayed here. You can include html strings here and they will be rendered as html elements.
eventCode: 'allowTransactionData',
button: {
text: 'Done',
onclick: () => {
completed = true
},
onclick: async () => await saveToStorage(USER_ENABLED_LEDGER_TX_DATA, true),
},
icon: `
<svg height="14" viewBox="0 0 18 14" width="18" xmlns="http://www.w3.org/2000/svg"><g fill="currentColor"><path d="m10.29375 4.05351563c0-.04921875 0-.09140625 0-.13007813 0-1.0546875 0-2.109375 0-3.1640625 0-.43945312.3480469-.76992188.7804688-.7453125.2003906.01054688.3585937.10546875.4992187.24609375.5800781.58359375 1.1566406 1.16367188 1.7367187 1.74023438 1.4695313 1.46953125 2.9390625 2.93906249 4.4050782 4.40859375.1335937.13359375.2425781.27421875.2707031.46757812.0351562.20742188-.0246094.421875-.1652344.58007813-.0246094.028125-.0492187.05273437-.0738281.08085937-2.0601563 2.06367188-4.1203125 4.1238281-6.1804688 6.1875-.2109375.2109375-.4570312.3023438-.7453125.2179688-.2707031-.0808594-.4464843-.2707032-.5132812-.5484375-.0140625-.0738282-.0175781-.1441407-.0140625-.2179688 0-1.0335937 0-2.0707031 0-3.1042969 0-.0386719 0-.08085935 0-.13359372h-5.06953125c-.49570313 0-.80507813-.309375-.80507813-.80859375 0-1.42382813 0-2.84414063 0-4.26796875 0-.49570313.30585938-.8015625.8015625-.8015625h4.93593748z"/><path d="m5.69882812 13.978125h-4.01132812c-.928125 0-1.6875-.8753906-1.6875-1.9511719v-10.06171872c0-1.07578125.75585938-1.95117188 1.6875-1.95117188h4.01132812c.34101563 0 .61523438.31992188.61523438.71015625 0 .39023438-.27421875.71015625-.61523438.71015625h-4.01132812c-.253125 0-.45703125.23554688-.45703125.52734375v10.06171875c0 .2917969.20390625.5273437.45703125.5273437h4.01132812c.34101563 0 .61523438.3199219.61523438.7101563s-.27773438.7171875-.61523438.7171875z"/></g></svg>

View File

@ -10,7 +10,6 @@ import selector from './selector'
import Page from 'src/components/layout/Page'
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { SAFES_KEY, saveSafes } from 'src/logic/safe/utils'
import { getWeb3 } from 'src/logic/wallets/getWeb3'
import { getNamesFrom, getOwnersFrom } from 'src/routes/open/utils/safeDataExtractor'
import { SAFELIST_ADDRESS } from 'src/routes/routes'
import { buildSafe } from 'src/routes/safe/store/actions/fetchSafe'
@ -19,6 +18,7 @@ import { loadFromStorage } from 'src/utils/storage'
import { Dispatch } from 'redux'
import { SafeOwner } from '../../safe/store/models/safe'
import { List } from 'immutable'
import { checksumAddress } from 'src/utils/checksumAddress'
export const loadSafe = async (
safeName: string,
@ -39,14 +39,15 @@ export const loadSafe = async (
class Load extends React.Component<any> {
onLoadSafeSubmit = async (values) => {
let safeAddress = values[FIELD_LOAD_ADDRESS]
if (safeAddress) {
return
}
try {
const { addSafe } = this.props
const web3 = getWeb3()
const safeName = values[FIELD_LOAD_NAME]
let safeAddress = values[FIELD_LOAD_ADDRESS]
if (safeAddress) {
safeAddress = web3.utils.toChecksumAddress(safeAddress)
}
safeAddress = checksumAddress(safeAddress)
const ownerNames = getNamesFrom(values)
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)

View File

@ -69,12 +69,14 @@ export const createSafe = (values, userAccount) => {
})
.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 }
})
.catch((error) => {
console.error(error)
})
return promiEvent
}

View File

@ -27,7 +27,7 @@ const APPS_STORAGE_KEY = 'APPS_STORAGE_KEY'
const APPS_LEGAL_DISCLAIMER_STORAGE_KEY = 'APPS_LEGAL_DISCLAIMER_STORAGE_KEY'
const StyledIframe = styled.iframe`
padding: 24px;
padding: 15px;
box-sizing: border-box;
width: 100%;
height: 100%;
@ -39,6 +39,10 @@ const Centered = styled.div`
flex-direction: column;
`
const CenteredMT = styled(Centered)`
margin-top: 5px;
`
const IframeWrapper = styled.div`
position: relative;
height: 100%;
@ -409,7 +413,7 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
</Centered>
</Card>
)}
<Centered>
<CenteredMT>
<IconText
color="secondary"
iconSize="sm"
@ -417,7 +421,7 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
text="These are third-party apps, which means they are not owned, controlled, maintained or audited by Gnosis. Interacting with the apps is at your own risk."
textSize="sm"
/>
</Centered>
</CenteredMT>
</>
)
}

View File

@ -13,13 +13,17 @@ const removeLastTrailingSlash = (url) => {
const gnosisAppsUrl = removeLastTrailingSlash(getGnosisSafeAppsUrl())
export const staticAppsList: Array<{ url: string; disabled: boolean }> = [
// Sablier
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmabPEk7g4zaytFefp6fE4nz8f85QMJoWmRQQZypvJViNG`, disabled: false },
// Aave
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmY1MUZo44UkT8EokYHs7xDvWEziYSn7n3c4ojVB6qo3SM`, disabled: false },
// Compound
{ url: `${gnosisAppsUrl}/compound`, disabled: false },
// Idle
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmUoqmq8jw98VwTSf7aTQeBCfPKicQgcJL5k2Bch9QT8BJ`, disabled: false },
// request
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmQapdJP6zERqpDKKPECNeMDDgwmGUqbKk1PjHpYj8gfDJ`, disabled: false },
// Aave
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmfHFQHCSyaSL8Aq4eWZeB3buy4neiUCPchob2pYdV9gJT`, disabled: false },
{ url: `${gnosisAppsUrl}/compound`, disabled: false },
// Sablier
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmabPEk7g4zaytFefp6fE4nz8f85QMJoWmRQQZypvJViNG`, disabled: false },
// TX-Builder
{ url: `${gnosisAppsUrl}/tx-builder`, disabled: false },
]

View File

@ -14,6 +14,19 @@ import { getAddressBookListSelector } from 'src/logic/addressBook/store/selector
import { getAddressFromENS } from 'src/logic/wallets/getWeb3'
import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
export interface AddressBookProps {
fieldMutator: (address: string) => void
isCustomTx?: boolean
pristine: boolean
recipientAddress?: string
setSelectedEntry: (
entry: { address?: string; name?: string } | React.SetStateAction<{ address: string; name: string }>,
) => void
setIsValidAddress: (valid?: boolean) => void
}
const useStyles = makeStyles(styles)
const textFieldLabelStyle = makeStyles(() => ({
root: {
overflow: 'hidden',
@ -30,34 +43,39 @@ const textFieldInputStyle = makeStyles(() => ({
},
}))
const filterAddressBookWithContractAddresses = async (addressBook) => {
const filterAddressBookWithContractAddresses = async (
addressBook: List<{ address: string }>,
): Promise<List<{ address: string }>> => {
const abFlags = await Promise.all(
addressBook.map(async ({ address }) => {
return (await mustBeEthereumContractAddress(address)) === undefined
}),
addressBook.map(
async ({ address }: { address: string }): Promise<boolean> => {
return (await mustBeEthereumContractAddress(address)) === undefined
},
),
)
return addressBook.filter((adbkEntry, index) => abFlags[index])
return addressBook.filter((_, index) => abFlags[index])
}
const AddressBookInput = ({
classes,
fieldMutator,
isCustomTx,
pristine,
recipientAddress,
setIsValidAddress,
setSelectedEntry,
}: any) => {
}: AddressBookProps) => {
const classes = useStyles()
const addressBook = useSelector(getAddressBookListSelector)
const [isValidForm, setIsValidForm] = useState(true)
const [validationText, setValidationText] = useState<any>(true)
const [validationText, setValidationText] = useState<string>('')
const [inputTouched, setInputTouched] = useState(false)
const [blurred, setBlurred] = useState(pristine)
const [adbkList, setADBKList] = useState(List([]))
const [adbkList, setADBKList] = useState<List<{ address: string }>>(List([]))
const [inputAddValue, setInputAddValue] = useState(recipientAddress)
const onAddressInputChanged = async (addressValue) => {
const onAddressInputChanged = async (addressValue: string): Promise<void> => {
setInputAddValue(addressValue)
let resolvedAddress = addressValue
let isValidText
@ -99,7 +117,7 @@ const AddressBookInput = ({
}
useEffect(() => {
const filterAdbkContractAddresses = async () => {
const filterAdbkContractAddresses = async (): Promise<void> => {
if (!isCustomTx) {
setADBKList(addressBook)
return

View File

@ -1,4 +1,6 @@
export const styles = () => ({
import { createStyles } from '@material-ui/core'
export const styles = createStyles({
itemOptionList: {
display: 'flex',
},

View File

@ -1,7 +1,9 @@
import { makeStyles } from '@material-ui/core/styles'
import React from 'react'
import React, { useState } from 'react'
import { useFormState, useField } from 'react-final-form'
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
import Field from 'src/components/forms/Field'
import TextField from 'src/components/forms/TextField'
import {
@ -16,7 +18,7 @@ import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/Co
const useStyles = makeStyles(styles)
export interface EthAddressProps {
export interface EthAddressInputProps {
isContract?: boolean
isRequired?: boolean
name: string
@ -24,10 +26,24 @@ export interface EthAddressProps {
text: string
}
const EthAddressInput = ({ isContract = true, isRequired = true, name, onScannedValue, text }: EthAddressProps) => {
const EthAddressInput = ({
isContract = true,
isRequired = true,
name,
onScannedValue,
text,
}: EthAddressInputProps) => {
const classes = useStyles()
const validatorsList = [isRequired && required, mustBeEthereumAddress, isContract && mustBeEthereumContractAddress]
const validate = composeValidators(...validatorsList.filter((_) => _))
const { pristine } = useFormState({ subscription: { pristine: true } })
const {
input: { value },
} = useField('contractAddress', { subscription: { value: true } })
const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string } | null>({
address: value,
name: '',
})
const handleScan = (value, closeQrModal) => {
let scannedAddress = value
@ -44,15 +60,25 @@ const EthAddressInput = ({ isContract = true, isRequired = true, name, onScanned
<>
<Row margin="md">
<Col xs={11}>
<Field
component={TextField}
name={name}
placeholder={text}
testId={name}
text={text}
type="text"
validate={validate}
/>
{selectedEntry?.address ? (
<Field
component={TextField}
name={name}
placeholder={text}
testId={name}
text={text}
type="text"
validate={validate}
/>
) : (
<AddressBookInput
setSelectedEntry={setSelectedEntry}
setIsValidAddress={() => {}}
fieldMutator={onScannedValue}
isCustomTx
pristine={pristine}
/>
)}
</Col>
<Col center="xs" className={classes} middle="xs" xs={1}>
<ScanQRWrapper handleScan={handleScan} />

View File

@ -19,7 +19,7 @@ import Field from 'src/components/forms/Field'
import GnoForm from 'src/components/forms/GnoForm'
import TextField from 'src/components/forms/TextField'
import TextareaField from 'src/components/forms/TextareaField'
import { composeValidators, maxValue, mustBeFloat, greaterThan } from 'src/components/forms/validator'
import { composeValidators, maxValue, mustBeFloat, equalOrGreaterThan } from 'src/components/forms/validator'
import Block from 'src/components/layout/Block'
import Button from 'src/components/layout/Button'
import ButtonLink from 'src/components/layout/ButtonLink'
@ -230,7 +230,7 @@ const SendCustomTx: React.FC<Props> = ({ initialValues, onClose, onNext, contrac
placeholder="Value*"
text="Value*"
type="text"
validate={composeValidators(mustBeFloat, maxValue(ethBalance), greaterThan(0))}
validate={composeValidators(mustBeFloat, maxValue(ethBalance), equalOrGreaterThan(0))}
/>
</Col>
</Row>

View File

@ -100,7 +100,6 @@ const ContractInteraction: React.FC<ContractInteractionProps> = ({
>
{(submitting, validating, rest, mutators) => {
setCallResults = mutators.setCallResults
return (
<>
<Block className={classes.formContainer}>

View File

@ -47,7 +47,7 @@ const Balances = (props) => {
const address = useSelector(safeParamAddressFromStateSelector)
const featuresEnabled = useSelector(safeFeaturesEnabledSelector)
useFetchTokens()
useFetchTokens(address)
useEffect(() => {
const erc721Enabled = featuresEnabled && featuresEnabled.includes('ERC721')

View File

@ -4,16 +4,31 @@ import EtherScanLink from 'src/components/EtherscanLink'
import Identicon from 'src/components/Identicon'
import Block from 'src/components/layout/Block'
import Paragraph from 'src/components/layout/Paragraph'
import { useWindowDimensions } from '../../../../container/hooks/useWindowDimensions'
import { useEffect, useState } from 'react'
const OwnerAddressTableCell = (props) => {
const { address, knownAddress, showLinks, userName } = props
const [cut, setCut] = useState(undefined)
const { width } = useWindowDimensions()
useEffect(() => {
if (width <= 900) {
setCut(6)
} else if (width <= 1024) {
setCut(12)
} else {
setCut(undefined)
}
}, [width])
return (
<Block justify="left">
<Identicon address={address} diameter={32} />
{showLinks ? (
<div style={{ marginLeft: 10, flexShrink: 1, minWidth: 0 }}>
{userName}
<EtherScanLink knownAddress={knownAddress} type="address" value={address} />
<EtherScanLink knownAddress={knownAddress} type="address" value={address} cut={cut} />
</div>
) : (
<Paragraph style={{ marginLeft: 10 }}>{address}</Paragraph>

View File

@ -1,31 +0,0 @@
import { useEffect } from 'react'
import { batch, useDispatch, useSelector } from 'react-redux'
import fetchCollectibles from 'src/logic/collectibles/store/actions/fetchCollectibles'
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
import fetchEtherBalance from 'src/routes/safe/store/actions/fetchEtherBalance'
import { checkAndUpdateSafe } from 'src/routes/safe/store/actions/fetchSafe'
import fetchTransactions from 'src/routes/safe/store/actions/transactions/fetchTransactions'
import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors'
import { TIMEOUT } from 'src/utils/constants'
export const useCheckForUpdates = () => {
const dispatch = useDispatch()
const safeAddress = useSelector(safeParamAddressFromStateSelector)
useEffect(() => {
if (safeAddress) {
const collectiblesInterval = setInterval(() => {
batch(() => {
dispatch(fetchEtherBalance(safeAddress))
dispatch(fetchSafeTokens(safeAddress))
dispatch(fetchTransactions(safeAddress))
dispatch(fetchCollectibles)
dispatch(checkAndUpdateSafe(safeAddress))
})
}, TIMEOUT * 3)
return () => {
clearInterval(collectiblesInterval)
}
}
}, [dispatch, safeAddress])
}

View File

@ -1,5 +1,5 @@
import { useMemo } from 'react'
import { batch, useDispatch, useSelector } from 'react-redux'
import { batch, useDispatch } from 'react-redux'
import { useLocation } from 'react-router-dom'
import fetchCollectibles from 'src/logic/collectibles/store/actions/fetchCollectibles'
@ -8,11 +8,9 @@ import activateAssetsByBalance from 'src/logic/tokens/store/actions/activateAsse
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
import { fetchTokens } from 'src/logic/tokens/store/actions/fetchTokens'
import { COINS_LOCATION_REGEX, COLLECTIBLES_LOCATION_REGEX } from 'src/routes/safe/components/Balances'
import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors'
export const useFetchTokens = (): void => {
export const useFetchTokens = (safeAddress: string): void => {
const dispatch = useDispatch()
const address: string | null = useSelector(safeParamAddressFromStateSelector)
const location = useLocation()
useMemo(() => {
@ -20,17 +18,17 @@ export const useFetchTokens = (): void => {
batch(() => {
// fetch tokens there to get symbols for tokens in TXs list
dispatch(fetchTokens())
dispatch(fetchCurrencyValues(address))
dispatch(fetchSafeTokens(address))
dispatch(fetchCurrencyValues(safeAddress))
dispatch(fetchSafeTokens(safeAddress))
})
}
if (COLLECTIBLES_LOCATION_REGEX.test(location.pathname)) {
batch(() => {
dispatch(fetchCollectibles()).then(() => {
dispatch(activateAssetsByBalance(address))
dispatch(fetchCollectibles(safeAddress)).then(() => {
dispatch(activateAssetsByBalance(safeAddress))
})
})
}
}, [address, dispatch, location])
}, [dispatch, location.pathname, safeAddress])
}

View File

@ -0,0 +1,46 @@
import { useEffect, useRef } from 'react'
import { batch, useDispatch } from 'react-redux'
import fetchCollectibles from 'src/logic/collectibles/store/actions/fetchCollectibles'
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
import fetchEtherBalance from 'src/routes/safe/store/actions/fetchEtherBalance'
import { checkAndUpdateSafe } from 'src/routes/safe/store/actions/fetchSafe'
import fetchTransactions from 'src/routes/safe/store/actions/transactions/fetchTransactions'
import { TIMEOUT } from 'src/utils/constants'
export const useSafeScheduledUpdates = (safeAddress: string): void => {
const dispatch = useDispatch()
const timer = useRef<number>(null)
useEffect(() => {
// using this variable to prevent setting a timeout when the component is already unmounted or the effect
// has to run again
let mounted = true
const fetchSafeData = async (address: string): Promise<void> => {
await batch(async () => {
await Promise.all([
dispatch(fetchEtherBalance(address)),
dispatch(fetchSafeTokens(address)),
dispatch(fetchTransactions(address)),
dispatch(fetchCollectibles(address)),
dispatch(checkAndUpdateSafe(address)),
])
})
if (mounted) {
timer.current = setTimeout(() => {
fetchSafeData(safeAddress)
}, TIMEOUT * 3)
}
}
if (safeAddress) {
fetchSafeData(safeAddress)
return () => {
mounted = false
clearTimeout(timer.current)
}
}
}, [dispatch, safeAddress])
}

View File

@ -0,0 +1,24 @@
import { useState, useEffect } from 'react'
function getWindowDimensions() {
const { innerWidth: width, innerHeight: height } = window
return {
width,
height,
}
}
export const useWindowDimensions = (): { width: number; height: number } => {
const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions())
useEffect(() => {
function handleResize() {
setWindowDimensions(getWindowDimensions())
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return windowDimensions
}

View File

@ -7,7 +7,7 @@ import Page from 'src/components/layout/Page'
import Layout from 'src/routes/safe/components/Layout'
import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors'
import { useLoadSafe } from './hooks/useLoadSafe'
import { useCheckForUpdates } from './hooks/useCheckForUpdates'
import { useSafeScheduledUpdates } from './hooks/useSafeScheduledUpdates'
const INITIAL_STATE = {
sendFunds: {
@ -17,12 +17,12 @@ const INITIAL_STATE = {
showReceive: false,
}
const SafeView = () => {
const SafeView = (): JSX.Element => {
const [state, setState] = useState(INITIAL_STATE)
const safeAddress = useSelector(safeParamAddressFromStateSelector)
useLoadSafe(safeAddress)
useCheckForUpdates()
useSafeScheduledUpdates(safeAddress)
const onShow = (action) => () => {
setState((prevState) => ({

View File

@ -117,6 +117,7 @@ const createTransaction = ({
const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, origin)
const beforeExecutionKey = showSnackbar(notificationsQueue.beforeExecution, enqueueSnackbar, closeSnackbar)
let pendingExecutionKey
let txHash
@ -246,6 +247,10 @@ const createTransaction = ({
return receipt.transactionHash
})
} catch (err) {
const errorMsg = err.message
? `${notificationsQueue.afterExecutionError.message} - ${err.message}`
: notificationsQueue.afterExecutionError.message
console.error(err)
closeSnackbar(beforeExecutionKey)
@ -253,7 +258,7 @@ const createTransaction = ({
closeSnackbar(pendingExecutionKey)
}
showSnackbar(notificationsQueue.afterExecutionError, enqueueSnackbar, closeSnackbar)
showSnackbar(errorMsg, enqueueSnackbar, closeSnackbar)
const executeDataUsedSignatures = safeInstance.contract.methods
.execTransaction(to, valueInWei, txData, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs)

View File

@ -1,12 +1,18 @@
import { getBalanceInEtherOf } from 'src/logic/wallets/getWeb3'
import updateSafe from 'src/routes/safe/store/actions/updateSafe'
import { SAFE_REDUCER_ID } from 'src/routes/safe/store/reducer/safe'
import { Dispatch } from 'redux'
import { backOff } from 'exponential-backoff'
import { AppReduxState } from 'src/store'
const fetchEtherBalance = (safeAddress) => async (dispatch, getState) => {
const fetchEtherBalance = (safeAddress: string) => async (
dispatch: Dispatch,
getState: () => AppReduxState,
): Promise<void> => {
try {
const state = getState()
const ethBalance = state[SAFE_REDUCER_ID].getIn([SAFE_REDUCER_ID, safeAddress, 'ethBalance'])
const newEthBalance = await getBalanceInEtherOf(safeAddress)
const newEthBalance = await backOff(() => getBalanceInEtherOf(safeAddress))
if (newEthBalance !== ethBalance) {
dispatch(updateSafe({ address: safeAddress, ethBalance: newEthBalance }))
}

View File

@ -14,6 +14,7 @@ import { makeOwner } from 'src/routes/safe/store/models/owner'
import { checksumAddress } from 'src/utils/checksumAddress'
import { SafeOwner } from '../models/safe'
import { Dispatch } from 'redux'
const buildOwnersFrom = (
safeOwners,
@ -71,7 +72,7 @@ export const buildSafe = async (safeAdd, safeName, latestMasterContractVersion?:
return safe
}
export const checkAndUpdateSafe = (safeAdd) => async (dispatch) => {
export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch): Promise<void> => {
const safeAddress = checksumAddress(safeAdd)
// Check if the owner's safe did change and update them
const safeParams = ['getThreshold', 'nonce', 'getOwners']

View File

@ -199,6 +199,9 @@ const processTransaction = ({
return receipt.transactionHash
})
} catch (err) {
const errorMsg = err.message
? `${notificationsQueue.afterExecutionError.message} - ${err.message}`
: notificationsQueue.afterExecutionError.message
console.error(err)
if (txHash !== undefined) {
@ -208,7 +211,7 @@ const processTransaction = ({
closeSnackbar(pendingExecutionKey)
}
showSnackbar(notificationsQueue.afterExecutionError, enqueueSnackbar, closeSnackbar)
showSnackbar(errorMsg, enqueueSnackbar, closeSnackbar)
const executeData = safeInstance.contract.methods.approveHash(txHash).encodeABI()
const errMsg = await getErrorMessage(safeInstance.address, 0, executeData, from)

View File

@ -7,28 +7,39 @@ import { loadOutgoingTransactions } from './loadOutgoingTransactions'
import { addOrUpdateCancellationTransactions } from 'src/routes/safe/store/actions/transactions/addOrUpdateCancellationTransactions'
import { addOrUpdateTransactions } from 'src/routes/safe/store/actions/transactions/addOrUpdateTransactions'
import { Dispatch } from 'redux'
import { backOff } from 'exponential-backoff'
const noFunc = () => {}
export default (safeAddress: string) => async (dispatch) => {
const transactions = await loadOutgoingTransactions(safeAddress)
export default (safeAddress: string) => async (dispatch: Dispatch): Promise<void> => {
try {
const transactions = await backOff(() => loadOutgoingTransactions(safeAddress))
if (transactions) {
const { cancel, outgoing } = transactions
const updateCancellationTxs = cancel.size
? addOrUpdateCancellationTransactions({ safeAddress, transactions: cancel })
: noFunc
const updateOutgoingTxs = outgoing.size ? addOrUpdateTransactions({ safeAddress, transactions: outgoing }) : noFunc
if (transactions) {
const { cancel, outgoing } = transactions
const updateCancellationTxs = cancel.size
? addOrUpdateCancellationTransactions({ safeAddress, transactions: cancel })
: noFunc
const updateOutgoingTxs = outgoing.size
? addOrUpdateTransactions({
safeAddress,
transactions: outgoing,
})
: noFunc
batch(() => {
dispatch(updateCancellationTxs)
dispatch(updateOutgoingTxs)
})
}
batch(() => {
dispatch(updateCancellationTxs)
dispatch(updateOutgoingTxs)
})
}
const incomingTransactions = await loadIncomingTransactions(safeAddress)
const incomingTransactions = await loadIncomingTransactions(safeAddress)
if (incomingTransactions.get(safeAddress).size) {
dispatch(addIncomingTransactions(incomingTransactions))
if (incomingTransactions.get(safeAddress).size) {
dispatch(addIncomingTransactions(incomingTransactions))
}
} catch (error) {
console.log('Error fetching transactions:', error)
}
}

View File

@ -66,7 +66,7 @@ export type OutgoingTxs = {
export type BatchProcessTxsProps = OutgoingTxs & {
currentUser?: string
knownTokens: Record<string, Token>
knownTokens: Map<string, Token>
safe: SafeRecord
}
@ -79,7 +79,10 @@ export type BatchProcessTxsProps = OutgoingTxs & {
const extractCancelAndOutgoingTxs = (safeAddress: string, outgoingTxs: TxServiceModel[]): OutgoingTxs => {
return outgoingTxs.reduce(
(acc, transaction) => {
if (isCancelTransaction(transaction, safeAddress)) {
if (
isCancelTransaction(transaction, safeAddress) &&
outgoingTxs.find((tx) => tx.nonce === transaction.nonce && !isCancelTransaction(tx, safeAddress))
) {
if (!isNaN(Number(transaction.nonce))) {
acc.cancellationTxs[transaction.nonce] = transaction
}

View File

@ -85,7 +85,7 @@ export const isCustomTransaction = async (
tx: TxServiceModel,
txCode: string,
safeAddress: string,
knownTokens: Record<string, Token>,
knownTokens: Map<string, Token>,
): Promise<boolean> => {
return (
isOutgoingTransaction(tx, safeAddress) &&
@ -340,7 +340,7 @@ export const mockTransaction = (tx: TxToMock, safeAddress: string, state): Promi
...tx,
}
const knownTokens: Record<string, Token> = state[TOKEN_REDUCER_ID]
const knownTokens: Map<string, Token> = state[TOKEN_REDUCER_ID]
const safe: SafeRecord = state[SAFE_REDUCER_ID].getIn([SAFE_REDUCER_ID, safeAddress])
const cancellationTxs = state[CANCELLATION_TRANSACTIONS_REDUCER_ID].get(safeAddress) || Map()
const outgoingTxs = state[TRANSACTIONS_REDUCER_ID].get(safeAddress) || List()

View File

@ -14,6 +14,7 @@ import { UPDATE_SAFE } from 'src/routes/safe/store/actions/updateSafe'
import { makeOwner } from 'src/routes/safe/store/models/owner'
import makeSafe from 'src/routes/safe/store/models/safe'
import { checksumAddress } from 'src/utils/checksumAddress'
import { SafeReducerMap } from './types/safe'
export const SAFE_REDUCER_ID = 'safes'
export const DEFAULT_SAFE_INITIAL_STATE = 'NOT_ASKED'
@ -43,13 +44,17 @@ export const buildSafe = (storedSafe) => {
export default handleActions(
{
[UPDATE_SAFE]: (state, action) => {
[UPDATE_SAFE]: (state: SafeReducerMap, action) => {
const safe = action.payload
const safeAddress = safe.address
return state.updateIn([SAFE_REDUCER_ID, safeAddress], (prevSafe) => prevSafe.merge(safe))
return state.updateIn(
[SAFE_REDUCER_ID, safeAddress],
makeSafe({ name: 'LOADED SAFE', address: safeAddress }),
(prevSafe) => prevSafe.merge(safe),
)
},
[ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state, action) => {
[ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state: SafeReducerMap, action) => {
const tokenAddress = action.payload
return state.withMutations((map) => {
@ -64,7 +69,7 @@ export default handleActions(
})
})
},
[ADD_SAFE]: (state, action) => {
[ADD_SAFE]: (state: SafeReducerMap, action) => {
const { safe } = action.payload
// if you add a new Safe it needs to be set as a record
@ -77,12 +82,12 @@ export default handleActions(
return state.setIn([SAFE_REDUCER_ID, safe.address], makeSafe(safe))
},
[REMOVE_SAFE]: (state, action) => {
[REMOVE_SAFE]: (state: SafeReducerMap, action) => {
const safeAddress = action.payload
return state.deleteIn([SAFE_REDUCER_ID, safeAddress])
},
[ADD_SAFE_OWNER]: (state, action) => {
[ADD_SAFE_OWNER]: (state: SafeReducerMap, action) => {
const { ownerAddress, ownerName, safeAddress } = action.payload
return state.updateIn([SAFE_REDUCER_ID, safeAddress], (prevSafe) =>
@ -91,7 +96,7 @@ export default handleActions(
}),
)
},
[REMOVE_SAFE_OWNER]: (state, action) => {
[REMOVE_SAFE_OWNER]: (state: SafeReducerMap, action) => {
const { ownerAddress, safeAddress } = action.payload
return state.updateIn([SAFE_REDUCER_ID, safeAddress], (prevSafe) =>
@ -100,7 +105,7 @@ export default handleActions(
}),
)
},
[REPLACE_SAFE_OWNER]: (state, action) => {
[REPLACE_SAFE_OWNER]: (state: SafeReducerMap, action) => {
const { oldOwnerAddress, ownerAddress, ownerName, safeAddress } = action.payload
return state.updateIn([SAFE_REDUCER_ID, safeAddress], (prevSafe) =>
@ -111,7 +116,7 @@ export default handleActions(
}),
)
},
[EDIT_SAFE_OWNER]: (state, action) => {
[EDIT_SAFE_OWNER]: (state: SafeReducerMap, action) => {
const { ownerAddress, ownerName, safeAddress } = action.payload
return state.updateIn([SAFE_REDUCER_ID, safeAddress], (prevSafe) => {
@ -131,3 +136,5 @@ export default handleActions(
latestMasterContractVersion: '',
}),
)
export * from './types/safe.d'

View File

@ -0,0 +1,19 @@
import { SafeRecord } from 'src/routes/safe/store/models/safe'
import { Map } from 'immutable'
export type SafesMap = Map<string, SafeRecord>
export interface SafeReducerState {
defaultSafe: 'NOT_ASKED' | string | undefined
safes: SafesMap
latestMasterContractVersion: string
}
interface SafeReducerStateSerialized extends SafeReducerState {
safes: Record<string, SafeRecordProps>
}
export interface SafeReducerMap extends Map<string, any> {
toJS(): SafeReducerStateSerialized
get<K extends keyof SafeReducerState>(key: K): SafeReducerState[K]
}

View File

@ -1,20 +1,20 @@
import { List, Map, Set } from 'immutable'
import { matchPath } from 'react-router-dom'
import { createSelector } from 'reselect'
import { getWeb3 } from 'src/logic/wallets/getWeb3'
import { SAFELIST_ADDRESS, SAFE_PARAM_ADDRESS } from 'src/routes/routes'
import { CANCELLATION_TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/cancellationTransactions'
import { INCOMING_TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/incomingTransactions'
import { SAFE_REDUCER_ID } from 'src/routes/safe/store/reducer/safe'
import { SAFE_REDUCER_ID, SafesMap } from 'src/routes/safe/store/reducer/safe'
import { TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/transactions'
import { AppReduxState } from 'src/store'
import { checksumAddress } from 'src/utils/checksumAddress'
import { SafeRecord } from 'src/routes/safe/store/models/safe'
const safesStateSelector = (state) => state[SAFE_REDUCER_ID]
const safesStateSelector = (state: AppReduxState) => state[SAFE_REDUCER_ID]
export const safesMapSelector = (state) => state[SAFE_REDUCER_ID].get('safes')
export const safesMapSelector = (state: AppReduxState): SafesMap => state[SAFE_REDUCER_ID].get('safes')
export const safesListSelector = createSelector(safesMapSelector, (safes) => safes.toList())
@ -26,18 +26,17 @@ export const latestMasterContractVersionSelector = createSelector(safesStateSele
safeState.get('latestMasterContractVersion'),
)
const transactionsSelector = (state) => state[TRANSACTIONS_REDUCER_ID]
const transactionsSelector = (state: AppReduxState) => state[TRANSACTIONS_REDUCER_ID]
const cancellationTransactionsSelector = (state) => state[CANCELLATION_TRANSACTIONS_REDUCER_ID]
const cancellationTransactionsSelector = (state: AppReduxState) => state[CANCELLATION_TRANSACTIONS_REDUCER_ID]
const incomingTransactionsSelector = (state) => state[INCOMING_TRANSACTIONS_REDUCER_ID]
const incomingTransactionsSelector = (state: AppReduxState) => state[INCOMING_TRANSACTIONS_REDUCER_ID]
export const safeParamAddressFromStateSelector = (state): string | null => {
export const safeParamAddressFromStateSelector = (state: AppReduxState): string | null => {
const match = matchPath(state.router.location.pathname, { path: `${SAFELIST_ADDRESS}/:safeAddress` })
if (match) {
const web3 = getWeb3()
return web3.utils.toChecksumAddress(match.params.safeAddress)
return checksumAddress(match.params.safeAddress)
}
return null
@ -64,7 +63,7 @@ export const safeTransactionsSelector = createSelector(
},
)
export const addressBookQueryParamsSelector = (state) => {
export const addressBookQueryParamsSelector = (state: AppReduxState): string | null => {
const { location } = state.router
let entryAddressToEditOrCreateNew = null
if (location && location.query) {
@ -116,20 +115,26 @@ export const safeSelector = createSelector(safesMapSelector, safeParamAddressFro
return safe
})
export const safeActiveTokensSelector = createSelector(safeSelector, (safe) => {
if (!safe) {
return List()
}
export const safeActiveTokensSelector = createSelector(
safeSelector,
(safe): Set<string> => {
if (!safe) {
return Set()
}
return safe.activeTokens
})
return safe.activeTokens
},
)
export const safeActiveAssetsSelector = createSelector(safeSelector, (safe) => {
if (!safe) {
return List()
}
return safe.activeAssets
})
export const safeActiveAssetsSelector = createSelector(
safeSelector,
(safe): Set<string> => {
if (!safe) {
return Set()
}
return safe.activeAssets
},
)
export const safeActiveAssetsListSelector = createSelector(safeActiveAssetsSelector, (safeList) => {
if (!safeList) {
@ -154,20 +159,21 @@ export const safeBlacklistedAssetsSelector = createSelector(safeSelector, (safe)
return safe.blacklistedAssets
})
export const safeActiveAssetsSelectorBySafe = (safeAddress, safes) => safes.get(safeAddress).get('activeAssets')
export const safeActiveAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap) =>
safes.get(safeAddress).get('activeAssets')
export const safeBlacklistedAssetsSelectorBySafe = (safeAddress, safes) =>
safes.get(safeAddress).get('blacklistedAssets')
export const safeBalancesSelector = createSelector(safeSelector, (safe) => {
if (!safe) {
return List()
return Map()
}
return safe.balances
})
export const safeFieldSelector = (field) => (safe) => safe?.[field]
export const safeFieldSelector = (field: string) => (safe: SafeRecord) => safe?.[field]
export const safeNameSelector = createSelector(safeSelector, safeFieldSelector('name'))

View File

@ -1,6 +1,6 @@
import { connectRouter, routerMiddleware } from 'connected-react-router'
import { connectRouter, routerMiddleware, RouterState } from 'connected-react-router'
import { createHashHistory } from 'history'
import { applyMiddleware, combineReducers, compose, createStore } from 'redux'
import { applyMiddleware, CombinedState, combineReducers, compose, createStore } from 'redux'
import thunk from 'redux-thunk'
import addressBookMiddleware from 'src/logic/addressBook/store/middleware/addressBookMiddleware'
@ -27,8 +27,12 @@ import cancellationTransactions, {
import incomingTransactions, {
INCOMING_TRANSACTIONS_REDUCER_ID,
} from 'src/routes/safe/store/reducer/incomingTransactions'
import safe, { SAFE_REDUCER_ID } from 'src/routes/safe/store/reducer/safe'
import safe, { SAFE_REDUCER_ID, SafeReducerMap } from 'src/routes/safe/store/reducer/safe'
import transactions, { TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/transactions'
import { Map } from 'immutable'
import { NFTAssets, NFTTokens } from '../logic/collectibles/sources/OpenSea'
import { ProviderRecord } from '../logic/wallets/store/model/provider'
import { Token } from 'src/logic/tokens/store/model/token'
export const history = createHashHistory({ hashType: 'slash' })
@ -63,6 +67,23 @@ const reducers = combineReducers({
[CURRENT_SESSION_REDUCER_ID]: currentSession,
})
export type AppReduxState = CombinedState<{
[PROVIDER_REDUCER_ID]?: ProviderRecord
[SAFE_REDUCER_ID]: SafeReducerMap
[NFT_ASSETS_REDUCER_ID]?: NFTAssets
[NFT_TOKENS_REDUCER_ID]?: NFTTokens
[TOKEN_REDUCER_ID]?: Map<string, Token>
[TRANSACTIONS_REDUCER_ID]: Map<string, any>
[CANCELLATION_TRANSACTIONS_REDUCER_ID]: Map<string, any>
[INCOMING_TRANSACTIONS_REDUCER_ID]: Map<string, any>
[NOTIFICATIONS_REDUCER_ID]: Map<string, any>
[CURRENCY_VALUES_KEY]: Map<string, any>
[COOKIES_REDUCER_ID]: Map<string, any>
[ADDRESS_BOOK_REDUCER_ID]: Map<string, any>
[CURRENT_SESSION_REDUCER_ID]: Map<string, any>
router: RouterState
}>
export const store: any = createStore(reducers, finalCreateStore)
export const aNewStore = (localState?: any) => createStore(reducers, localState, finalCreateStore)

View File

@ -1,6 +1,6 @@
import { getWeb3 } from 'src/logic/wallets/getWeb3'
export const checksumAddress = (address) => {
export const checksumAddress = (address: string): string => {
if (!address) return null
return getWeb3().utils.toChecksumAddress(address)
}

1776
yarn.lock

File diff suppressed because it is too large Load Diff