Merge branch 'development' into backmerge-release-3.1.3

This commit is contained in:
Daniel Sanchez 2021-03-17 20:06:06 +01:00 committed by GitHub
commit f15a91ba7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
113 changed files with 660 additions and 1787 deletions

View File

@ -1,10 +1,13 @@
name: Deploy to EWC network
# Run on pushes to master
# Run on pushes to master or PRs to master
on:
push:
branches:
- master
pull_request:
branches:
- master
# Launches build when release is published
release:
types: [published]

View File

@ -1,7 +1,9 @@
name: Deploy to Mainnet network
# Run on pushes to master
# Run on pushes to master or PRs
on:
# Pull request hook without any config. Launches for every pull request
pull_request:
push:
branches:
- master
@ -84,7 +86,27 @@ jobs:
aws-region: ${{ secrets.AWS_DEFAULT_REGION }}
# Script to deploy Pull Requests
# Mainnet build is never created in Pull Requests
- run: bash ./scripts/github/deploy_pull_request.sh
if: success() && github.event.number
env:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
PR_NUMBER: ${{ github.event.number }}
REVIEW_BUCKET_NAME: ${{ secrets.AWS_REVIEW_BUCKET_NAME }}
REACT_APP_NETWORK: ${{ env.REACT_APP_NETWORK }}
TRAVIS_TAG: ${{ github.event.release.tag_name }}
- name: 'PRaul: Comment PR with app URLs'
uses: mshick/add-pr-comment@v1
with:
message: |
* [Safe Multisig app ${{ env.REACT_APP_NETWORK }}](${{ env.REVIEW_FEATURE_URL }}/${{ env.REACT_APP_NETWORK }}/app/)
repo-token: ${{ secrets.GITHUB_TOKEN }}
repo-token-user-login: 'github-actions[bot]'
allow-repeats: true
if: success() && github.event.number
env:
REVIEW_FEATURE_URL: https://pr${{ github.event.number }}--${{ env.REPO_NAME_ALPHANUMERIC }}.review.gnosisdev.com
# Script to deploy to development environment
# Mainnet build is never created in development branch

View File

@ -7,7 +7,31 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<title>Gnosis Safe Multisig</title>
</head>
<style>
.safe-preloader-animation {
position: absolute;
top: 50%;
left: 50%;
width: 120px;
height: 120px;
margin:-60px 0 0 -60px;
animation: sk-bounce 2.0s infinite ease-in-out;
animation-delay: -1.0s;
}
@keyframes sk-bounce {
0%, 100% {
transform: scale(0.8);
-webkit-transform: scale(0.8);
}
50% {
transform: scale(1.0);
-webkit-transform: scale(1.0);
}
}
</style>
<body>
<div id="root" style="overflow: hidden;"></div>
<div id="root" style="overflow: hidden;"><img class="safe-preloader-animation" src="./resources/safe.png" /></div>
</body>
</html>

View File

@ -20,13 +20,17 @@ import { getNetworkId } from 'src/config'
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
import { networkSelector } from 'src/logic/wallets/store/selectors'
import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from 'src/routes/routes'
import { safeNameSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import {
safeFiatBalancesTotalSelector,
safeNameSelector,
safeParamAddressFromStateSelector,
} from 'src/logic/safe/store/selectors'
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
import Modal from 'src/components/Modal'
import SendModal from 'src/routes/safe/components/Balances/SendModal'
import { useLoadSafe } from 'src/logic/safe/hooks/useLoadSafe'
import { useSafeScheduledUpdates } from 'src/logic/safe/hooks/useSafeScheduledUpdates'
import useSafeActions from 'src/logic/safe/hooks/useSafeActions'
import { currentCurrencySelector, safeFiatBalancesTotalSelector } from 'src/logic/currencyValues/store/selectors'
import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount'
import { grantedSelector } from 'src/routes/safe/container/selector'

View File

@ -3,17 +3,18 @@ import * as React from 'react'
import Button from 'src/components/layout/Button'
import Col from 'src/components/layout/Col'
import Row from 'src/components/layout/Row'
import { boldFont, sm } from 'src/theme/variables'
import { boldFont, sm, lg, secondary } from 'src/theme/variables'
const controlStyle = {
backgroundColor: 'white',
padding: sm,
padding: lg,
borderRadius: sm,
}
const firstButtonStyle = {
marginRight: sm,
fontWeight: boldFont,
color: secondary,
}
const secondButtonStyle = {
@ -50,8 +51,8 @@ const Controls = ({
}
return (
<Row align="end" grow style={controlStyle}>
<Col end="xs" xs={12}>
<Row align="center" grow style={controlStyle}>
<Col center="xs" xs={12}>
<Button onClick={onPrevious} size="small" style={firstButtonStyle} type="button">
{back}
</Button>

View File

@ -7,7 +7,7 @@ import { lg } from 'src/theme/variables'
const useStyles = makeStyles({
root: {
margin: '10px',
margin: '10px 0 10px 10px',
maxWidth: '770px',
boxShadow: '0 0 10px 0 rgba(33,48,77,0.10)',
},

View File

@ -10,6 +10,8 @@ import {
uniqueAddress,
differentFrom,
ADDRESS_REPEATED_ERROR,
addressIsNotCurrentSafe,
OWNER_ADDRESS_IS_SAFE_ADDRESS_ERROR,
} from 'src/components/forms/validator'
describe('Forms > Validators', () => {
@ -179,6 +181,22 @@ describe('Forms > Validators', () => {
})
})
describe('addressIsNotSafe validator', () => {
it('Returns undefined if the given `address` it not the given `safeAddress`', async () => {
const address = '0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe'
const safeAddress = '0x2D6F2B448b0F711Eb81f2929566504117d67E44F'
expect(addressIsNotCurrentSafe(safeAddress)(address)).toBeUndefined()
})
it('Returns an error message if the given `address` is the same as the `safeAddress`', async () => {
const address = '0x2D6F2B448b0F711Eb81f2929566504117d67E44F'
const safeAddress = '0x2D6F2B448b0F711Eb81f2929566504117d67E44F'
expect(addressIsNotCurrentSafe(safeAddress)(address)).toEqual(OWNER_ADDRESS_IS_SAFE_ADDRESS_ERROR)
})
})
describe('differentFrom validator', () => {
const getDifferentFromErrMsg = (diffValue: string): string => `Value should be different than ${diffValue}`

View File

@ -92,12 +92,16 @@ export const minMaxLength = (minLen: number, maxLen: number) => (value: string):
value.length >= +minLen && value.length <= +maxLen ? undefined : `Should be ${minLen} to ${maxLen} symbols`
export const ADDRESS_REPEATED_ERROR = 'Address already introduced'
export const OWNER_ADDRESS_IS_SAFE_ADDRESS_ERROR = 'Cannot use Safe itself as owner.'
export const uniqueAddress = (addresses: string[] | List<string> = []) => (address?: string): string | undefined => {
const addressExists = addresses.some((addressFromList) => sameAddress(addressFromList, address))
return addressExists ? ADDRESS_REPEATED_ERROR : undefined
}
export const addressIsNotCurrentSafe = (safeAddress: string) => (address?: string): string | undefined =>
sameAddress(safeAddress, address) ? OWNER_ADDRESS_IS_SAFE_ADDRESS_ERROR : undefined
export const composeValidators = (...validators: Validator[]) => (value: unknown): ValidatorReturnType =>
validators.reduce(
(error: string | undefined, validator: GenericValidatorType): ValidatorReturnType => error || validator(value),

View File

@ -6,7 +6,6 @@ import { Integrations } from '@sentry/tracing'
import Root from 'src/components/Root'
import loadCurrentSessionFromStorage from 'src/logic/currentSession/store/actions/loadCurrentSessionFromStorage'
import loadActiveTokens from 'src/logic/tokens/store/actions/loadActiveTokens'
import loadDefaultSafe from 'src/logic/safe/store/actions/loadDefaultSafe'
import loadSafesFromStorage from 'src/logic/safe/store/actions/loadSafesFromStorage'
import { store } from 'src/store'
@ -17,7 +16,6 @@ disableMMAutoRefreshWarning()
BigNumber.set({ EXPONENTIAL_AT: [-7, 255] })
store.dispatch(loadActiveTokens())
store.dispatch(loadSafesFromStorage())
store.dispatch(loadDefaultSafe())
store.dispatch(loadCurrentSessionFromStorage())

View File

@ -3,8 +3,6 @@ import { NFTAsset, NFTAssets, NFTToken, NFTTokens } from 'src/logic/collectibles
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/logic/safe/store/selectors'
export const nftAssets = (state: AppReduxState): NFTAssets => state[NFT_ASSETS_REDUCER_ID]
export const nftTokens = (state: AppReduxState): NFTTokens => state[NFT_TOKENS_REDUCER_ID]
@ -26,21 +24,8 @@ export const orderedNFTAssets = createSelector(nftTokensSelector, (userNftTokens
export const activeNftAssetsListSelector = createSelector(
nftAssetsListSelector,
safeActiveAssetsSelector,
availableNftAssetsAddresses,
(assets, activeAssetsList, availableNftAssetsAddresses): NFTAsset[] => {
return assets
.filter(({ address }) => activeAssetsList.has(address))
.filter(({ address }) => availableNftAssetsAddresses.includes(address))
},
)
export const safeActiveSelectorMap = createSelector(
activeNftAssetsListSelector,
(activeAssets): NFTAssets => {
return activeAssets.reduce((acc, asset) => {
acc[asset.address] = asset
return acc
}, {})
(assets, availableNftAssetsAddresses): NFTAsset[] => {
return assets.filter(({ address }) => availableNftAssetsAddresses.includes(address))
},
)

View File

@ -0,0 +1,8 @@
import { getClientGatewayUrl } from 'src/config'
import axios from 'axios'
export const fetchAvailableCurrencies = async (): Promise<string[]> => {
const url = `${getClientGatewayUrl()}/balances/supported-fiat-codes`
return axios.get(url).then(({ data }) => data)
}

View File

@ -1,41 +0,0 @@
import axios from 'axios'
import BigNumber from 'bignumber.js'
import { EXCHANGE_RATE_URL } from 'src/utils/constants'
import { fetchTokenCurrenciesBalances } from './fetchTokenCurrenciesBalances'
import { sameString } from 'src/utils/strings'
import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues'
const fetchCurrenciesRates = async (
baseCurrency: string,
targetCurrencyValue: string,
safeAddress: string,
): Promise<number> => {
let rate = 0
if (sameString(targetCurrencyValue, AVAILABLE_CURRENCIES.NETWORK)) {
try {
const tokenCurrenciesBalances = await fetchTokenCurrenciesBalances(safeAddress)
if (tokenCurrenciesBalances.items.length) {
rate = new BigNumber(1).div(tokenCurrenciesBalances.items[0].fiatConversion).toNumber()
}
} catch (error) {
console.error(`Fetching ${AVAILABLE_CURRENCIES.NETWORK} data from the relayer errored`, error)
}
return rate
}
// National currencies
try {
const url = `${EXCHANGE_RATE_URL}?base=${baseCurrency}&symbols=${targetCurrencyValue}`
const result = await axios.get(url)
if (result?.data) {
const { rates } = result.data
rate = rates[targetCurrencyValue] ? rates[targetCurrencyValue] : 0
}
} catch (error) {
console.error('Fetching data from getExchangeRatesUrl errored', error)
}
return rate
}
export default fetchCurrenciesRates

View File

@ -1,27 +0,0 @@
import { Action } from 'redux-actions'
import { ThunkDispatch } from 'redux-thunk'
import fetchCurrenciesRates from 'src/logic/currencyValues/api/fetchCurrenciesRates'
import { setCurrencyRate } from 'src/logic/currencyValues/store/actions/setCurrencyRate'
import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues'
import { CurrencyRatePayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
import { AppReduxState } from 'src/store'
const fetchCurrencyRate = (safeAddress: string, selectedCurrency: string) => async (
dispatch: ThunkDispatch<AppReduxState, undefined, Action<CurrencyRatePayload>>,
): Promise<void> => {
if (AVAILABLE_CURRENCIES.USD === selectedCurrency) {
dispatch(setCurrencyRate(safeAddress, 1))
return
}
const selectedCurrencyRateInBaseCurrency: number = await fetchCurrenciesRates(
AVAILABLE_CURRENCIES.USD,
selectedCurrency,
safeAddress,
)
dispatch(setCurrencyRate(safeAddress, selectedCurrencyRateInBaseCurrency))
}
export default fetchCurrencyRate

View File

@ -2,18 +2,17 @@ import { Action } from 'redux-actions'
import { ThunkDispatch } from 'redux-thunk'
import { setSelectedCurrency } from 'src/logic/currencyValues/store/actions/setSelectedCurrency'
import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues'
import { CurrentCurrencyPayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
import { loadSelectedCurrency } from 'src/logic/currencyValues/store/utils/currencyValuesStorage'
import { AppReduxState } from 'src/store'
export const fetchSelectedCurrency = (safeAddress: string) => async (
dispatch: ThunkDispatch<AppReduxState, undefined, Action<CurrentCurrencyPayload>>,
import { loadSelectedCurrency } from 'src/logic/safe/utils/currencyValuesStorage'
import { AppReduxState } from 'src/store'
import { SelectedCurrencyPayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
export const fetchSelectedCurrency = () => async (
dispatch: ThunkDispatch<AppReduxState, undefined, Action<SelectedCurrencyPayload>>,
): Promise<void> => {
try {
const storedSelectedCurrency = await loadSelectedCurrency()
dispatch(setSelectedCurrency(safeAddress, storedSelectedCurrency || AVAILABLE_CURRENCIES.USD))
dispatch(setSelectedCurrency({ selectedCurrency: storedSelectedCurrency || 'USD' }))
} catch (err) {
console.error('Error fetching currency values', err)
}

View File

@ -0,0 +1,6 @@
import { createAction } from 'redux-actions'
import { AvailableCurrenciesPayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
export const SET_AVAILABLE_CURRENCIES = 'SET_AVAILABLE_CURRENCIES'
export const setAvailableCurrencies = createAction<AvailableCurrenciesPayload>(SET_AVAILABLE_CURRENCIES)

View File

@ -1,12 +0,0 @@
import { createAction } from 'redux-actions'
import { BalanceCurrencyList } from 'src/logic/currencyValues/store/model/currencyValues'
export const SET_CURRENCY_BALANCES = 'SET_CURRENCY_BALANCES'
export const setCurrencyBalances = createAction(
SET_CURRENCY_BALANCES,
(safeAddress: string, currencyBalances: BalanceCurrencyList) => ({
safeAddress,
currencyBalances,
}),
)

View File

@ -1,9 +0,0 @@
import { createAction } from 'redux-actions'
export const SET_CURRENCY_RATE = 'SET_CURRENCY_RATE'
// eslint-disable-next-line max-len
export const setCurrencyRate = createAction(SET_CURRENCY_RATE, (safeAddress: string, currencyRate: number) => ({
safeAddress,
currencyRate,
}))

View File

@ -1,20 +1,6 @@
import { Action, createAction } from 'redux-actions'
import { ThunkDispatch } from 'redux-thunk'
import { CurrencyPayloads } from 'src/logic/currencyValues/store/reducer/currencyValues'
import { AppReduxState } from 'src/store'
import fetchCurrencyRate from 'src/logic/currencyValues/store/actions/fetchCurrencyRate'
import { createAction } from 'redux-actions'
import { SelectedCurrencyPayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
export const SET_CURRENT_CURRENCY = 'SET_CURRENT_CURRENCY'
const setCurrentCurrency = createAction(SET_CURRENT_CURRENCY, (safeAddress: string, selectedCurrency: string) => ({
safeAddress,
selectedCurrency,
}))
export const setSelectedCurrency = (safeAddress: string, selectedCurrency: string) => (
dispatch: ThunkDispatch<AppReduxState, undefined, Action<CurrencyPayloads>>,
): void => {
dispatch(setCurrentCurrency(safeAddress, selectedCurrency))
dispatch(fetchCurrencyRate(safeAddress, selectedCurrency))
}
export const setSelectedCurrency = createAction<SelectedCurrencyPayload>(SET_CURRENT_CURRENCY)

View File

@ -0,0 +1,18 @@
import { Action } from 'redux-actions'
import { ThunkDispatch } from 'redux-thunk'
import { AppReduxState } from 'src/store'
import { AvailableCurrenciesPayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
import { setAvailableCurrencies } from 'src/logic/currencyValues/store/actions/setAvailableCurrencies'
import { fetchAvailableCurrencies } from 'src/logic/currencyValues/api/fetchAvailableCurrencies'
export const updateAvailableCurrencies = () => async (
dispatch: ThunkDispatch<AppReduxState, undefined, Action<AvailableCurrenciesPayload>>,
): Promise<void> => {
try {
const availableCurrencies = await fetchAvailableCurrencies()
dispatch(setAvailableCurrencies({ availableCurrencies }))
} catch (err) {
console.error('Error fetching available currencies', err)
}
return Promise.resolve()
}

View File

@ -1,16 +1,15 @@
import { SET_CURRENT_CURRENCY } from 'src/logic/currencyValues/store/actions/setSelectedCurrency'
import { saveSelectedCurrency } from 'src/logic/currencyValues/store/utils/currencyValuesStorage'
import { saveSelectedCurrency } from 'src/logic/safe/utils/currencyValuesStorage'
const watchedActions = [SET_CURRENT_CURRENCY]
const currencyValuesStorageMiddleware = () => (next) => async (action) => {
export const currencyValuesStorageMiddleware = () => (next) => async (action) => {
const handledAction = next(action)
if (watchedActions.includes(action.type)) {
switch (action.type) {
case SET_CURRENT_CURRENCY: {
const { selectedCurrency } = action.payload
saveSelectedCurrency(selectedCurrency)
await saveSelectedCurrency(selectedCurrency)
break
}
@ -21,5 +20,3 @@ const currencyValuesStorageMiddleware = () => (next) => async (action) => {
return handledAction
}
export default currencyValuesStorageMiddleware

View File

@ -1,66 +0,0 @@
import { List, Record, RecordOf } from 'immutable'
import { getNetworkInfo } from 'src/config'
const { nativeCoin } = getNetworkInfo()
export const AVAILABLE_CURRENCIES = {
NETWORK: nativeCoin.symbol.toLocaleUpperCase(),
USD: 'USD',
EUR: 'EUR',
AUD: 'AUD',
BGN: 'BGN',
BRL: 'BRL',
CAD: 'CAD',
CHF: 'CHF',
CNY: 'CNY',
CZK: 'CZK',
DKK: 'DKK',
GBP: 'GBP',
HKD: 'HKD',
HRK: 'HRK',
HUF: 'HUF',
IDR: 'IDR',
ILS: 'ILS',
INR: 'INR',
ISK: 'ISK',
JPY: 'JPY',
KRW: 'KRW',
MXN: 'MXN',
MYR: 'MYR',
NOK: 'NOK',
NZD: 'NZD',
PHP: 'PHP',
PLN: 'PLN',
RON: 'RON',
RUB: 'RUB',
SEK: 'SEK',
SGD: 'SGD',
THB: 'THB',
TRY: 'TRY',
ZAR: 'ZAR',
} as const
export type BalanceCurrencyRecord = {
currencyName?: string
tokenAddress?: string
balanceInBaseCurrency: string
balanceInSelectedCurrency: string
}
export const makeBalanceCurrency = Record<BalanceCurrencyRecord>({
currencyName: '',
tokenAddress: '',
balanceInBaseCurrency: '',
balanceInSelectedCurrency: '',
})
export type CurrencyRateValueRecord = RecordOf<BalanceCurrencyRecord>
export type BalanceCurrencyList = List<CurrencyRateValueRecord>
export interface CurrencyRateValue {
currencyRate?: number
selectedCurrency?: string
currencyBalances?: BalanceCurrencyList
}

View File

@ -1,44 +1,35 @@
import { Map } from 'immutable'
import { Action, handleActions } from 'redux-actions'
import { SET_CURRENCY_BALANCES } from 'src/logic/currencyValues/store/actions/setCurrencyBalances'
import { SET_CURRENCY_RATE } from 'src/logic/currencyValues/store/actions/setCurrencyRate'
import { SET_CURRENT_CURRENCY } from 'src/logic/currencyValues/store/actions/setSelectedCurrency'
import { BalanceCurrencyList, CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues'
import { AppReduxState } from 'src/store'
import { SET_AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/actions/setAvailableCurrencies'
export const CURRENCY_VALUES_KEY = 'currencyValues'
export interface CurrencyReducerMap extends Map<string, any> {
get<K extends keyof CurrencyRateValue>(key: K, notSetValue?: unknown): CurrencyRateValue[K]
setIn<K extends keyof CurrencyRateValue>(keys: [string, K], value: CurrencyRateValue[K]): this
export type CurrencyValuesState = {
selectedCurrency: string
availableCurrencies: string[]
}
export type CurrencyValuesState = Map<string, CurrencyReducerMap>
export const initialState = {
selectedCurrency: 'USD',
availableCurrencies: ['USD'],
}
type CurrencyBasePayload = { safeAddress: string }
export type CurrencyRatePayload = CurrencyBasePayload & { currencyRate: number }
export type CurrencyBalancesPayload = CurrencyBasePayload & { currencyBalances: BalanceCurrencyList }
export type CurrentCurrencyPayload = CurrencyBasePayload & { selectedCurrency: string }
export type SelectedCurrencyPayload = { selectedCurrency: string }
export type AvailableCurrenciesPayload = { availableCurrencies: string[] }
export type CurrencyPayloads = CurrencyRatePayload | CurrencyBalancesPayload | CurrentCurrencyPayload
export default handleActions<CurrencyReducerMap, CurrencyPayloads>(
export default handleActions<AppReduxState['currencyValues'], CurrencyValuesState>(
{
[SET_CURRENCY_RATE]: (state, action: Action<CurrencyRatePayload>) => {
const { currencyRate, safeAddress } = action.payload
return state.setIn([safeAddress, 'currencyRate'], currencyRate)
[SET_CURRENT_CURRENCY]: (state, action: Action<SelectedCurrencyPayload>) => {
const { selectedCurrency } = action.payload
state.selectedCurrency = selectedCurrency
return state
},
[SET_CURRENCY_BALANCES]: (state, action: Action<CurrencyBalancesPayload>) => {
const { safeAddress, currencyBalances } = action.payload
return state.setIn([safeAddress, 'currencyBalances'], currencyBalances)
},
[SET_CURRENT_CURRENCY]: (state, action: Action<CurrentCurrencyPayload>) => {
const { safeAddress, selectedCurrency } = action.payload
return state.setIn([safeAddress, 'selectedCurrency'], selectedCurrency)
[SET_AVAILABLE_CURRENCIES]: (state, action: Action<AvailableCurrenciesPayload>) => {
const { availableCurrencies } = action.payload
state.availableCurrencies = availableCurrencies
return state
},
},
Map(),
initialState,
)

View File

@ -1,53 +1,12 @@
import { createSelector } from 'reselect'
import {
CURRENCY_VALUES_KEY,
CurrencyReducerMap,
CurrencyValuesState,
} from 'src/logic/currencyValues/store/reducer/currencyValues'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { AppReduxState } from 'src/store'
import { CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues'
import { BigNumber } from 'bignumber.js'
import { CURRENCY_VALUES_KEY, CurrencyValuesState } from 'src/logic/currencyValues/store/reducer/currencyValues'
export const currencyValuesSelector = (state: AppReduxState): CurrencyValuesState => state[CURRENCY_VALUES_KEY]
export const safeFiatBalancesSelector = createSelector(
currencyValuesSelector,
safeParamAddressFromStateSelector,
(currencyValues, safeAddress): CurrencyReducerMap | undefined => {
if (!currencyValues || !safeAddress) return
return currencyValues.get(safeAddress)
},
)
export const currentCurrencySelector = (state: AppReduxState): string => {
return state[CURRENCY_VALUES_KEY].selectedCurrency
}
const currencyValueSelector = <K extends keyof CurrencyRateValue>(key: K) => (
currencyValuesMap?: CurrencyReducerMap,
): CurrencyRateValue[K] => currencyValuesMap?.get(key)
export const safeFiatBalancesListSelector = createSelector(
safeFiatBalancesSelector,
currencyValueSelector('currencyBalances'),
)
export const currentCurrencySelector = createSelector(
safeFiatBalancesSelector,
currencyValueSelector('selectedCurrency'),
)
export const currencyRateSelector = createSelector(safeFiatBalancesSelector, currencyValueSelector('currencyRate'))
export const safeFiatBalancesTotalSelector = createSelector(
safeFiatBalancesListSelector,
currencyRateSelector,
(currencyBalances, currencyRate): string | null => {
if (!currencyBalances) return '0'
if (!currencyRate) return null
const totalInBaseCurrency = currencyBalances.reduce((total, balanceCurrencyRecord) => {
return total.plus(balanceCurrencyRecord.balanceInBaseCurrency)
}, new BigNumber(0))
return totalInBaseCurrency.times(currencyRate).toFixed(2)
},
)
export const availableCurrenciesSelector = (state: AppReduxState): string[] => {
return state[CURRENCY_VALUES_KEY].availableCurrencies
}

View File

@ -1,10 +1,7 @@
import axios from 'axios'
import { getSafeClientGatewayBaseUrl } from 'src/config'
import {
fetchTokenCurrenciesBalances,
BalanceEndpoint,
} from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
import { fetchTokenCurrenciesBalances } from 'src/logic/safe/api/fetchTokenCurrenciesBalances'
import { aNewStore } from 'src/store'
jest.mock('axios')
@ -52,11 +49,15 @@ describe('fetchTokenCurrenciesBalances', () => {
axios.get.mockImplementationOnce(() => Promise.resolve({ data: expectedResult }))
// when
const result = await fetchTokenCurrenciesBalances(safeAddress, excludeSpamTokens)
const result = await fetchTokenCurrenciesBalances({
safeAddress,
excludeSpamTokens,
selectedCurrency: 'USD',
})
// then
expect(result).toStrictEqual(expectedResult)
expect(axios.get).toHaveBeenCalled()
expect(axios.get).toBeCalledWith(`${apiUrl}/balances/usd/?trusted=false&exclude_spam=${excludeSpamTokens}`)
expect(axios.get).toBeCalledWith(`${apiUrl}/balances/USD/?trusted=false&exclude_spam=${excludeSpamTokens}`)
})
})

View File

@ -16,14 +16,22 @@ export type BalanceEndpoint = {
items: TokenBalance[]
}
export const fetchTokenCurrenciesBalances = (
safeAddress: string,
type FetchTokenCurrenciesBalancesProps = {
safeAddress: string
selectedCurrency: string
excludeSpamTokens?: boolean
trustedTokens?: boolean
}
export const fetchTokenCurrenciesBalances = async ({
safeAddress,
selectedCurrency,
excludeSpamTokens = true,
trustedTokens = false,
): Promise<BalanceEndpoint> => {
}: FetchTokenCurrenciesBalancesProps): Promise<BalanceEndpoint> => {
const url = `${getSafeClientGatewayBaseUrl(
checksumAddress(safeAddress),
)}/balances/usd/?trusted=${trustedTokens}&exclude_spam=${excludeSpamTokens}`
)}/balances/${selectedCurrency}/?trusted=${trustedTokens}&exclude_spam=${excludeSpamTokens}`
return axios.get(url).then(({ data }) => data)
}

View File

@ -1,35 +1,32 @@
import { useMemo } from 'react'
import { batch, useDispatch } from 'react-redux'
import { batch, useDispatch, useSelector } from 'react-redux'
import { useLocation } from 'react-router-dom'
import { fetchCollectibles } from 'src/logic/collectibles/store/actions/fetchCollectibles'
import { fetchSelectedCurrency } from 'src/logic/currencyValues/store/actions/fetchSelectedCurrency'
import activateAssetsByBalance from 'src/logic/tokens/store/actions/activateAssetsByBalance'
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
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 { Dispatch } from 'src/logic/safe/store/actions/types.d'
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
export const useFetchTokens = (safeAddress: string): void => {
const dispatch = useDispatch<Dispatch>()
const location = useLocation()
const currentCurrency = useSelector(currentCurrencySelector)
useMemo(() => {
if (COINS_LOCATION_REGEX.test(location.pathname)) {
batch(() => {
// fetch tokens there to get symbols for tokens in TXs list
dispatch(fetchTokens())
dispatch(fetchSelectedCurrency(safeAddress))
dispatch(fetchSafeTokens(safeAddress))
dispatch(fetchSelectedCurrency())
dispatch(fetchSafeTokens(safeAddress, currentCurrency))
})
}
if (COLLECTIBLES_LOCATION_REGEX.test(location.pathname)) {
batch(() => {
dispatch(fetchCollectibles(safeAddress)).then(() => {
dispatch(activateAssetsByBalance(safeAddress))
})
})
dispatch(fetchCollectibles(safeAddress))
}
}, [dispatch, location.pathname, safeAddress])
}, [dispatch, location.pathname, safeAddress, currentCurrency])
}

View File

@ -3,11 +3,12 @@ import { useDispatch } from 'react-redux'
import loadAddressBookFromStorage from 'src/logic/addressBook/store/actions/loadAddressBookFromStorage'
import addViewedSafe from 'src/logic/currentSession/store/actions/addViewedSafe'
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
import { fetchSafeTokens } from 'src/logic/tokens/store/actions/fetchSafeTokens'
import fetchLatestMasterContractVersion from 'src/logic/safe/store/actions/fetchLatestMasterContractVersion'
import fetchSafe from 'src/logic/safe/store/actions/fetchSafe'
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
import { updateAvailableCurrencies } from 'src/logic/currencyValues/store/actions/updateAvailableCurrencies'
export const useLoadSafe = (safeAddress?: string): boolean => {
const dispatch = useDispatch<Dispatch>()
@ -20,6 +21,7 @@ export const useLoadSafe = (safeAddress?: string): boolean => {
await dispatch(fetchSafe(safeAddress))
setIsSafeLoaded(true)
await dispatch(fetchSafeTokens(safeAddress))
dispatch(updateAvailableCurrencies())
dispatch(fetchTransactions(safeAddress))
dispatch(addViewedSafe(safeAddress))
}

View File

@ -2,8 +2,8 @@ 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/logic/safe/store/actions/fetchEtherBalance'
import { fetchSafeTokens } from 'src/logic/tokens/store/actions/fetchSafeTokens'
import { fetchEtherBalance } from 'src/logic/safe/store/actions/fetchEtherBalance'
import { checkAndUpdateSafe } from 'src/logic/safe/store/actions/fetchSafe'
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
import { TIMEOUT } from 'src/utils/constants'

View File

@ -1,7 +0,0 @@
import { createAction } from 'redux-actions'
export const ACTIVATE_TOKEN_FOR_ALL_SAFES = 'ACTIVATE_TOKEN_FOR_ALL_SAFES'
const activateTokenForAllSafes = createAction(ACTIVATE_TOKEN_FOR_ALL_SAFES)
export default activateTokenForAllSafes

View File

@ -2,6 +2,4 @@ import { createAction } from 'redux-actions'
export const ADD_SAFE_OWNER = 'ADD_SAFE_OWNER'
const addSafeOwner = createAction(ADD_SAFE_OWNER)
export default addSafeOwner
export const addSafeOwner = createAction(ADD_SAFE_OWNER)

View File

@ -12,6 +12,7 @@ import {
tryOffchainSigning,
} from 'src/logic/safe/transactions'
import { estimateGasForTransactionCreation } from 'src/logic/safe/transactions/gas'
import * as aboutToExecuteTx from 'src/logic/safe/utils/aboutToExecuteTx'
import { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion'
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
@ -148,6 +149,9 @@ export const createTransaction = (
await saveTxToHistory({ ...txArgs, txHash, origin })
// store the pending transaction's nonce
isExecution && aboutToExecuteTx.setNonce(txArgs.nonce)
dispatch(fetchTransactions(safeAddress))
})
.on('error', (error) => {
@ -156,10 +160,6 @@ export const createTransaction = (
onError?.()
})
.then(async (receipt) => {
if (isExecution) {
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.noMoreConfirmationsNeeded))
}
dispatch(fetchTransactions(safeAddress))
return receipt.transactionHash

View File

@ -5,7 +5,7 @@ import { Dispatch } from 'redux'
import { backOff } from 'exponential-backoff'
import { AppReduxState } from 'src/store'
const fetchEtherBalance = (safeAddress: string) => async (
export const fetchEtherBalance = (safeAddress: string) => async (
dispatch: Dispatch,
getState: () => AppReduxState,
): Promise<void> => {
@ -21,5 +21,3 @@ const fetchEtherBalance = (safeAddress: string) => async (
console.error('Error when fetching Ether balance:', err)
}
}
export default fetchEtherBalance

View File

@ -8,8 +8,8 @@ import { getLocalSafe, getSafeName } from 'src/logic/safe/utils'
import { enabledFeatures, safeNeedsUpdate } from 'src/logic/safe/utils/safeVersion'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { getBalanceInEtherOf } from 'src/logic/wallets/getWeb3'
import addSafeOwner from 'src/logic/safe/store/actions/addSafeOwner'
import removeSafeOwner from 'src/logic/safe/store/actions/removeSafeOwner'
import { addSafeOwner } from 'src/logic/safe/store/actions/addSafeOwner'
import { removeSafeOwner } from 'src/logic/safe/store/actions/removeSafeOwner'
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
import { makeOwner } from 'src/logic/safe/store/models/owner'
import { checksumAddress } from 'src/utils/checksumAddress'
@ -80,6 +80,7 @@ export const buildSafe = async (
threshold,
owners,
ethBalance,
totalFiatBalance: 0,
nonce,
currentVersion: currentVersion ?? '',
needsUpdate,
@ -88,8 +89,6 @@ export const buildSafe = async (
latestIncomingTxBlock: 0,
activeAssets: Set(),
activeTokens: Set(),
blacklistedAssets: Set(),
blacklistedTokens: Set(),
modules,
spendingLimits,
}

View File

@ -11,6 +11,7 @@ import {
} from 'src/logic/safe/safeTxSigner'
import { getApprovalTransaction, getExecutionTransaction, saveTxToHistory } from 'src/logic/safe/transactions'
import { tryOffchainSigning } from 'src/logic/safe/transactions/offchainSigner'
import * as aboutToExecuteTx from 'src/logic/safe/utils/aboutToExecuteTx'
import { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion'
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
import { providerSelector } from 'src/logic/wallets/store/selectors'
@ -117,8 +118,6 @@ export const processTransaction = ({
dispatch(updateTransactionStatus({ txStatus: 'PENDING', safeAddress, nonce: tx.nonce, id: tx.id }))
await saveTxToHistory({ ...txArgs, signature })
// TODO: while we wait for the tx to be stored in the service and later update the tx info
// we should update the tx status in the store to disable owners' action buttons
dispatch(fetchTransactions(safeAddress))
return
@ -154,6 +153,10 @@ export const processTransaction = ({
try {
await saveTxToHistory({ ...txArgs, txHash })
// store the pending transaction's nonce
isExecution && aboutToExecuteTx.setNonce(txArgs.nonce)
dispatch(fetchTransactions(safeAddress))
} catch (e) {
console.error(e)
@ -172,10 +175,6 @@ export const processTransaction = ({
console.error('Processing transaction error: ', error)
})
.then(async (receipt) => {
if (isExecution) {
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.noMoreConfirmationsNeeded))
}
dispatch(fetchTransactions(safeAddress))
if (isExecution) {

View File

@ -2,6 +2,4 @@ import { createAction } from 'redux-actions'
export const REMOVE_SAFE_OWNER = 'REMOVE_SAFE_OWNER'
const removeSafeOwner = createAction(REMOVE_SAFE_OWNER)
export default removeSafeOwner
export const removeSafeOwner = createAction(REMOVE_SAFE_OWNER)

View File

@ -2,6 +2,4 @@ import { createAction } from 'redux-actions'
export const REPLACE_SAFE_OWNER = 'REPLACE_SAFE_OWNER'
const replaceSafeOwner = createAction(REPLACE_SAFE_OWNER)
export default replaceSafeOwner
export const replaceSafeOwner = createAction(REPLACE_SAFE_OWNER)

View File

@ -1,9 +0,0 @@
import { Set } from 'immutable'
import updateAssetsList from './updateAssetsList'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
const updateActiveAssets = (safeAddress: string, activeAssets: Set<string>) => (dispatch: Dispatch): void => {
dispatch(updateAssetsList({ safeAddress, activeAssets }))
}
export default updateActiveAssets

View File

@ -1,19 +0,0 @@
import { Set } from 'immutable'
import updateTokensList from './updateTokensList'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
// the selector uses ownProps argument/router props to get the address of the safe
// so in order to use it I had to recreate the same structure
// const generateMatchProps = (safeAddress: string) => ({
// match: {
// params: {
// [SAFE_PARAM_ADDRESS]: safeAddress,
// },
// },
// })
const updateActiveTokens = (safeAddress: string, activeTokens: Set<string>) => (dispatch: Dispatch): void => {
dispatch(updateTokensList({ safeAddress, activeTokens }))
}
export default updateActiveTokens

View File

@ -1,7 +0,0 @@
import { createAction } from 'redux-actions'
export const UPDATE_ASSETS_LIST = 'UPDATE_ASSETS_LIST'
const updateAssetsList = createAction(UPDATE_ASSETS_LIST)
export default updateAssetsList

View File

@ -1,9 +0,0 @@
import { Set } from 'immutable'
import updateAssetsList from './updateAssetsList'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
const updateBlacklistedAssets = (safeAddress: string, blacklistedAssets: Set<string>) => (dispatch: Dispatch): void => {
dispatch(updateAssetsList({ safeAddress, blacklistedAssets }))
}
export default updateBlacklistedAssets

View File

@ -1,9 +0,0 @@
import { Set } from 'immutable'
import updateTokensList from './updateTokensList'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
const updateBlacklistedTokens = (safeAddress: string, blacklistedTokens: Set<string>) => (dispatch: Dispatch): void => {
dispatch(updateTokensList({ safeAddress, blacklistedTokens }))
}
export default updateBlacklistedTokens

View File

@ -1,7 +0,0 @@
import { createAction } from 'redux-actions'
export const UPDATE_TOKENS_LIST = 'UPDATE_TOKENS_LIST'
const updateTokenList = createAction(UPDATE_TOKENS_LIST)
export default updateTokenList

View File

@ -1,4 +1,5 @@
import { push } from 'connected-react-router'
import { Action } from 'redux-actions'
import { NOTIFICATIONS, enhanceSnackbarForAction } from 'src/logic/notifications'
import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
@ -8,14 +9,19 @@ import { getSafeVersionInfo } from 'src/logic/safe/utils/safeVersion'
import { isUserAnOwner } from 'src/logic/wallets/ethAddresses'
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
import { grantedSelector } from 'src/routes/safe/container/selector'
import { ADD_QUEUED_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/gatewayTransactions'
import {
ADD_QUEUED_TRANSACTIONS,
ADD_HISTORY_TRANSACTIONS,
} from 'src/logic/safe/store/actions/transactions/gatewayTransactions'
import * as aboutToExecuteTx from 'src/logic/safe/utils/aboutToExecuteTx'
import { QueuedPayload } from 'src/logic/safe/store/reducer/gatewayTransactions'
import { safeParamAddressFromStateSelector, safesMapSelector } from 'src/logic/safe/store/selectors'
import { isTransactionSummary } from 'src/logic/safe/store/models/types/gateway.d'
import { isTransactionSummary, TransactionGatewayResult } from 'src/logic/safe/store/models/types/gateway.d'
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
import { ADD_OR_UPDATE_SAFE } from '../actions/addOrUpdateSafe'
const watchedActions = [ADD_OR_UPDATE_SAFE, ADD_QUEUED_TRANSACTIONS]
const watchedActions = [ADD_OR_UPDATE_SAFE, ADD_QUEUED_TRANSACTIONS, ADD_HISTORY_TRANSACTIONS]
const LAST_TIME_USED_LOGGED_IN_ID = 'LAST_TIME_USED_LOGGED_IN_ID'
@ -70,9 +76,21 @@ const notificationsMiddleware = (store) => (next) => async (action) => {
const state = store.getState()
switch (action.type) {
case ADD_HISTORY_TRANSACTIONS: {
const userAddress: string = userAccountSelector(state)
const safes = safesMapSelector(state)
const executedTxNotification = aboutToExecuteTx.getNotification(action.payload, userAddress, safes)
// if we have a notification, dispatch it depending on transaction's status
executedTxNotification && dispatch(enqueueSnackbar(executedTxNotification))
break
}
case ADD_QUEUED_TRANSACTIONS: {
const { safeAddress, values } = action.payload
const transactions = values.filter((tx) => isTransactionSummary(tx)).map((item) => item.transaction)
const { safeAddress, values } = (action as Action<QueuedPayload>).payload
const transactions = values
.filter((tx) => isTransactionSummary(tx))
.map((item: TransactionGatewayResult) => item.transaction)
const userAddress: string = userAccountSelector(state)
const awaitingTransactions = getAwaitingGatewayTransactions(transactions, userAddress)

View File

@ -1,7 +1,4 @@
import { saveDefaultSafe, saveSafes } from 'src/logic/safe/utils'
import { tokensSelector } from 'src/logic/tokens/store/selectors'
import { saveActiveTokens } from 'src/logic/tokens/utils/tokensStorage'
import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes'
import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner'
import { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner'
import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe'
@ -9,9 +6,7 @@ import { REMOVE_SAFE_OWNER } from 'src/logic/safe/store/actions/removeSafeOwner'
import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwner'
import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe'
import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
import { UPDATE_TOKENS_LIST } from 'src/logic/safe/store/actions/updateTokensList'
import { UPDATE_ASSETS_LIST } from 'src/logic/safe/store/actions/updateAssetsList'
import { getActiveTokensAddressesForAllSafes, safesMapSelector } from 'src/logic/safe/store/selectors'
import { safesMapSelector } from 'src/logic/safe/store/selectors'
import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe'
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { checksumAddress } from 'src/utils/checksumAddress'
@ -26,28 +21,10 @@ const watchedActions = [
REMOVE_SAFE_OWNER,
REPLACE_SAFE_OWNER,
EDIT_SAFE_OWNER,
ACTIVATE_TOKEN_FOR_ALL_SAFES,
UPDATE_TOKENS_LIST,
UPDATE_ASSETS_LIST,
SET_DEFAULT_SAFE,
]
const recalculateActiveTokens = (state) => {
const tokens = tokensSelector(state)
const activeTokenAddresses = getActiveTokensAddressesForAllSafes(state)
const activeTokens = tokens.withMutations((map) => {
map.forEach((token) => {
if (!activeTokenAddresses.has(token.address)) {
map.remove(token.address)
}
})
})
saveActiveTokens(activeTokens)
}
const safeStorageMware = (store) => (next) => async (action) => {
export const safeStorageMiddleware = (store) => (next) => async (action) => {
const handledAction = next(action)
if (watchedActions.includes(action.type)) {
@ -57,10 +34,6 @@ const safeStorageMware = (store) => (next) => async (action) => {
await saveSafes(safes.toJSON())
switch (action.type) {
case ACTIVATE_TOKEN_FOR_ALL_SAFES: {
recalculateActiveTokens(state)
break
}
case ADD_OR_UPDATE_SAFE: {
const { safe } = action.payload
safe.owners.forEach((owner) => {
@ -72,10 +45,7 @@ const safeStorageMware = (store) => (next) => async (action) => {
break
}
case UPDATE_SAFE: {
const { activeTokens, name, address } = action.payload
if (activeTokens) {
recalculateActiveTokens(state)
}
const { name, address } = action.payload
if (name) {
dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ name, address })))
}
@ -94,5 +64,3 @@ const safeStorageMware = (store) => (next) => async (action) => {
return handledAction
}
export default safeStorageMware

View File

@ -1,5 +1,6 @@
import { List, Map, Record, RecordOf, Set } from 'immutable'
import { FEATURES } from 'src/config/networks/network.d'
import { BalanceRecord } from 'src/logic/tokens/store/actions/fetchSafeTokens'
export type SafeOwner = {
name: string
@ -28,14 +29,13 @@ export type SafeRecordProps = {
address: string
threshold: number
ethBalance: string
totalFiatBalance: number
owners: List<SafeOwner>
modules?: ModulePair[] | null
spendingLimits?: SpendingLimit[] | null
activeTokens: Set<string>
activeAssets: Set<string>
blacklistedTokens: Set<string>
blacklistedAssets: Set<string>
balances: Map<string, string>
balances: Map<string, BalanceRecord>
nonce: number
latestIncomingTxBlock: number
recurringUser?: boolean
@ -49,13 +49,12 @@ const makeSafe = Record<SafeRecordProps>({
address: '',
threshold: 0,
ethBalance: '0',
totalFiatBalance: 0,
owners: List([]),
modules: [],
spendingLimits: [],
activeTokens: Set(),
activeAssets: Set(),
blacklistedTokens: Set(),
blacklistedAssets: Set(),
balances: Map(),
nonce: 0,
latestIncomingTxBlock: 0,

View File

@ -344,6 +344,7 @@ export const gatewayTransactions = handleActions<AppReduxState['gatewayTransacti
}
case 'queued.queued': {
queued.queued[nonce] = queued.queued[nonce].map((txToUpdate) => {
// TODO: review if is this `PENDING` status required under `queued.queued` list
// prevent setting `PENDING_FAILED` status, if previous status wasn't `PENDING`
if (txStatus === 'PENDING_FAILED' && txToUpdate.txStatus !== 'PENDING') {
return txToUpdate

View File

@ -1,7 +1,6 @@
import { Map, Set, List } from 'immutable'
import { Action, handleActions } from 'redux-actions'
import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes'
import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner'
import { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner'
import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe'
@ -10,8 +9,6 @@ import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwne
import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe'
import { SET_LATEST_MASTER_CONTRACT_VERSION } from 'src/logic/safe/store/actions/setLatestMasterContractVersion'
import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
import { UPDATE_TOKENS_LIST } from 'src/logic/safe/store/actions/updateTokensList'
import { UPDATE_ASSETS_LIST } from 'src/logic/safe/store/actions/updateAssetsList'
import { makeOwner } from 'src/logic/safe/store/models/owner'
import makeSafe, { SafeRecord, SafeRecordProps } from 'src/logic/safe/store/models/safe'
import { AppReduxState } from 'src/store'
@ -29,8 +26,6 @@ export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => {
const owners = buildOwnersFrom(Array.from(names), Array.from(addresses))
const activeTokens = Set(storedSafe.activeTokens)
const activeAssets = Set(storedSafe.activeAssets)
const blacklistedTokens = Set(storedSafe.blacklistedTokens)
const blacklistedAssets = Set(storedSafe.blacklistedAssets)
const balances = Map(storedSafe.balances)
return {
@ -38,9 +33,7 @@ export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => {
owners,
balances,
activeTokens,
blacklistedTokens,
activeAssets,
blacklistedAssets,
latestIncomingTxBlock: 0,
modules: null,
}
@ -102,21 +95,6 @@ export default handleActions<AppReduxState['safes'], Payloads>(
)
: state
},
[ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state, action: Action<SafeRecord>) => {
const tokenAddress = action.payload
return state.withMutations((map) => {
map
.get('safes')
.keySeq()
.forEach((safeAddress) => {
const safeActiveTokens = map.getIn(['safes', safeAddress, 'activeTokens'])
const activeTokens = safeActiveTokens.add(tokenAddress)
map.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.mergeDeep({ activeTokens }))
})
})
},
[ADD_OR_UPDATE_SAFE]: (state, action: Action<SafePayload>) => {
const { safe } = action.payload
const safeAddress = safe.address
@ -195,24 +173,6 @@ export default handleActions<AppReduxState['safes'], Payloads>(
return prevSafe.merge({ owners: updatedOwners })
})
},
[UPDATE_TOKENS_LIST]: (state, action: Action<SafeWithAddressPayload>) => {
// Only activeTokens or blackListedTokens is required
const { safeAddress, activeTokens, blacklistedTokens } = action.payload
const key = activeTokens ? 'activeTokens' : 'blacklistedTokens'
const list = activeTokens ?? blacklistedTokens
return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.set(key, list))
},
[UPDATE_ASSETS_LIST]: (state, action: Action<SafeWithAddressPayload>) => {
// Only activeAssets or blackListedAssets is required
const { safeAddress, activeAssets, blacklistedAssets } = action.payload
const key = activeAssets ? 'activeAssets' : 'blacklistedAssets'
const list = activeAssets ?? blacklistedAssets
return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.set(key, list))
},
[SET_DEFAULT_SAFE]: (state, action: Action<SafeRecord>) => state.set('defaultSafe', action.payload),
[SET_LATEST_MASTER_CONTRACT_VERSION]: (state, action: Action<SafeRecord>) =>
state.set('latestMasterContractVersion', action.payload),

View File

@ -76,51 +76,6 @@ export const safeActiveTokensSelector = createSelector(
},
)
export const safeActiveAssetsSelector = createSelector(
safeSelector,
(safe): Set<string> => {
if (!safe) {
return Set()
}
return safe.activeAssets
},
)
export const safeActiveAssetsListSelector = createSelector(safeActiveAssetsSelector, (safeList) => {
if (!safeList) {
return Set([])
}
return Set(safeList)
})
export const safeBlacklistedTokensSelector = createSelector(
safeSelector,
(safe): Set<string> => {
if (!safe) {
return Set()
}
return safe.blacklistedTokens
},
)
export const safeBlacklistedAssetsSelector = createSelector(
safeSelector,
(safe): Set<string> => {
if (!safe) {
return Set()
}
return safe.blacklistedAssets
},
)
export const safeActiveAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap): Set<string> =>
safes.get(safeAddress)?.get('activeAssets') || Set()
export const safeBlacklistedAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap): Set<string> =>
safes.get(safeAddress)?.get('blacklistedAssets') || Set()
const baseSafe = makeSafe()
export const safeFieldSelector = <K extends keyof SafeRecordProps>(field: K) => (
@ -172,14 +127,6 @@ export const getActiveTokensAddressesForAllSafes = createSelector(safesListSelec
return addresses
})
export const getBlacklistedTokensAddressesForAllSafes = createSelector(safesListSelector, (safes) => {
const addresses = Set().withMutations((set) => {
safes.forEach((safe) => {
safe.blacklistedTokens.forEach((tokenAddress) => {
set.add(tokenAddress)
})
})
})
return addresses
export const safeFiatBalancesTotalSelector = createSelector(safeSelector, (currentSafe) => {
return currentSafe?.totalFiatBalance.toString()
})

View File

@ -1,59 +0,0 @@
import { Set, Map } from 'immutable'
import { aNewStore } from 'src/store'
import updateActiveTokens from 'src/logic/safe/store/actions/updateActiveTokens'
import '@testing-library/jest-dom/extend-expect'
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
import { makeToken } from 'src/logic/tokens/store/model/token'
import { safesMapSelector } from 'src/logic/safe/store/selectors'
describe('Feature > Balances', () => {
let store
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
beforeEach(async () => {
store = aNewStore()
})
it('It should return an updated balance when updates active tokens', async () => {
// given
const tokensAmount = '100'
const token = makeToken({
address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9',
name: 'OmiseGo',
symbol: 'OMG',
decimals: 18,
logoUri:
'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true',
})
const balances = Map({
[token.address]: tokensAmount,
})
const expectedResult = '100'
// when
store.dispatch(updateSafe({ address: safeAddress, balances }))
store.dispatch(updateActiveTokens(safeAddress, Set([token.address])))
const safe = safesMapSelector(store.getState()).get(safeAddress)
const balanceResult = safe?.get('balances').get(token.address)
const activeTokens = safe?.get('activeTokens')
const tokenIsActive = activeTokens?.has(token.address)
// then
expect(balanceResult).toBe(expectedResult)
expect(tokenIsActive).toBe(true)
})
it('The store should have an updated ether balance after updating the value', async () => {
// given
const etherAmount = '1'
const expectedResult = '1'
// when
store.dispatch(updateSafe({ address: safeAddress, ethBalance: etherAmount }))
const safe = safesMapSelector(store.getState()).get(safeAddress)
const balanceResult = safe?.get('ethBalance')
// then
expect(balanceResult).toBe(expectedResult)
})
})

View File

@ -7,9 +7,7 @@ const getMockedOldSafe = ({
needsUpdate,
balances,
recurringUser,
blacklistedAssets,
blacklistedTokens,
activeAssets,
assets,
activeTokens,
owners,
featuresEnabled,
@ -34,8 +32,6 @@ const getMockedOldSafe = ({
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
const mockedActiveAssetsAddress1 = '0x503ab2a6A70c6C6ec8b25a4C87C784e1c8f8e8CD'
const mockedActiveAssetsAddress2 = '0xfdd4E685361CB7E89a4D27e03DCd0001448d731F'
const mockedBlacklistedTokenAddress1 = '0xc7d892dca37a244Fb1A7461e6141e58Ead460282'
const mockedBlacklistedAssetAddress1 = '0x0ac539137c4c99001f16Dd132E282F99A02Ddc3F'
return {
name: name || 'MockedSafe',
@ -46,14 +42,12 @@ const getMockedOldSafe = ({
modules: modules || [],
spendingLimits: spendingLimits || [],
activeTokens: activeTokens || Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2]),
activeAssets: activeAssets || Set([mockedActiveAssetsAddress1, mockedActiveAssetsAddress2]),
blacklistedTokens: blacklistedTokens || Set([mockedBlacklistedTokenAddress1]),
blacklistedAssets: blacklistedAssets || Set([mockedBlacklistedAssetAddress1]),
assets: assets || Set([mockedActiveAssetsAddress1, mockedActiveAssetsAddress2]),
balances:
balances ||
Map({
[mockedActiveTokenAddress1]: '100',
[mockedActiveTokenAddress2]: '10',
[mockedActiveTokenAddress1]: { tokenBalance: '100' },
[mockedActiveTokenAddress2]: { tokenBalance: '10' },
}),
nonce: nonce || 2,
latestIncomingTxBlock: latestIncomingTxBlock || 1,
@ -61,6 +55,7 @@ const getMockedOldSafe = ({
currentVersion: currentVersion || 'v1.1.1',
needsUpdate: needsUpdate || false,
featuresEnabled: featuresEnabled || [],
totalFiatBalance: 110,
}
}
@ -209,43 +204,9 @@ describe('shouldSafeStoreBeUpdated', () => {
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
const oldActiveAssets = Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2])
const newActiveAssets = Set([mockedActiveTokenAddress1])
const oldSafe = getMockedOldSafe({ activeAssets: oldActiveAssets })
const oldSafe = getMockedOldSafe({ assets: oldActiveAssets })
const newSafeProps: Partial<SafeRecordProps> = {
activeAssets: newActiveAssets,
}
// When
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
// Then
expect(expectedResult).toEqual(true)
})
it(`Given an old blacklistedTokens list and a new blacklistedTokens list for the safe, should return true`, () => {
// given
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
const oldBlacklistedTokens = Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2])
const newBlacklistedTokens = Set([mockedActiveTokenAddress1])
const oldSafe = getMockedOldSafe({ blacklistedTokens: oldBlacklistedTokens })
const newSafeProps: Partial<SafeRecordProps> = {
blacklistedTokens: newBlacklistedTokens,
}
// When
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
// Then
expect(expectedResult).toEqual(true)
})
it(`Given an old blacklistedAssets list and a new blacklistedAssets list for the safe, should return true`, () => {
// given
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
const oldBlacklistedAssets = Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2])
const newBlacklistedAssets = Set([mockedActiveTokenAddress1])
const oldSafe = getMockedOldSafe({ blacklistedAssets: oldBlacklistedAssets })
const newSafeProps: Partial<SafeRecordProps> = {
blacklistedAssets: newBlacklistedAssets,
assets: newActiveAssets,
}
// When
@ -259,11 +220,11 @@ describe('shouldSafeStoreBeUpdated', () => {
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
const oldBalances = Map({
[mockedActiveTokenAddress1]: '100',
[mockedActiveTokenAddress2]: '10',
[mockedActiveTokenAddress1]: { tokenBalance: '100' },
[mockedActiveTokenAddress2]: { tokenBalance: '100' },
})
const newBalances = Map({
[mockedActiveTokenAddress1]: '100',
[mockedActiveTokenAddress1]: { tokenBalance: '100' },
})
const oldSafe = getMockedOldSafe({ balances: oldBalances })
const newSafeProps: Partial<SafeRecordProps> = {

View File

@ -0,0 +1,51 @@
import { getNotificationsFromTxType } from 'src/logic/notifications'
import {
isStatusFailed,
isTransactionSummary,
TransactionGatewayResult,
} from 'src/logic/safe/store/models/types/gateway.d'
import { HistoryPayload } from 'src/logic/safe/store/reducer/gatewayTransactions'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import { isUserAnOwner } from 'src/logic/wallets/ethAddresses'
import { SafesMap } from 'src/routes/safe/store/reducer/types/safe'
let nonce: number | undefined
export const setNonce = (newNonce: typeof nonce): void => {
nonce = newNonce
}
export const getNotification = (
{ safeAddress, values }: HistoryPayload,
userAddress: string,
safes: SafesMap,
): undefined => {
const currentSafe = safes.get(safeAddress)
// no notification if not in the current safe or if its not an owner
if (!currentSafe || !isUserAnOwner(currentSafe, userAddress)) {
return
}
// if we have a nonce, then we have a tx that is about to be executed
if (nonce !== undefined) {
const executedTx = values
.filter(isTransactionSummary)
.map((item: TransactionGatewayResult) => item.transaction)
.find((transaction) => transaction.executionInfo?.nonce === nonce)
// transaction that was pending, was moved into history
// that is: it was executed
if (executedTx !== undefined) {
const notificationsQueue = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.STANDARD_TX)
const notification = isStatusFailed(executedTx.txStatus)
? notificationsQueue.afterExecutionError
: notificationsQueue.afterExecution.noMoreConfirmationsNeeded
// reset nonce value
setNonce(undefined)
return notification
}
}
}

View File

@ -1,44 +0,0 @@
import { nftAssetsSelector } from 'src/logic/collectibles/store/selectors'
import updateActiveAssets from 'src/logic/safe/store/actions/updateActiveAssets'
import {
safeActiveAssetsSelectorBySafe,
safeBlacklistedAssetsSelectorBySafe,
safesMapSelector,
} from 'src/logic/safe/store/selectors'
const activateAssetsByBalance = (safeAddress) => async (dispatch, getState) => {
try {
const state = getState()
const safes = safesMapSelector(state)
if (safes.size === 0) {
return
}
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 }: any = asset[1]
return address !== null && !blacklistedAssets.has(address) && numberOfTokens > 0
})
.map((asset) => {
return asset[0]
})
// need to persist those already active assets, despite its balances
const activeAssets = alreadyActiveAssets.union(activeByBalance)
// update list of active tokens
dispatch(updateActiveAssets(safeAddress, activeAssets))
} catch (err) {
console.error('Error fetching active assets list', err)
}
return null
}
export default activateAssetsByBalance

View File

@ -2,8 +2,6 @@ import { createAction } from 'redux-actions'
export const ADD_TOKENS = 'ADD_TOKENS'
const addTokens = createAction(ADD_TOKENS, (tokens) => ({
export const addTokens = createAction(ADD_TOKENS, (tokens) => ({
tokens,
}))
export default addTokens

View File

@ -2,63 +2,56 @@ import { backOff } from 'exponential-backoff'
import { List, Map } from 'immutable'
import { Dispatch } from 'redux'
import { fetchTokenCurrenciesBalances, TokenBalance } from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
import {
AVAILABLE_CURRENCIES,
CurrencyRateValueRecord,
makeBalanceCurrency,
} from 'src/logic/currencyValues/store/model/currencyValues'
import addTokens from 'src/logic/tokens/store/actions/saveTokens'
import { fetchTokenCurrenciesBalances, TokenBalance } from 'src/logic/safe/api/fetchTokenCurrenciesBalances'
import { addTokens } from 'src/logic/tokens/store/actions/addTokens'
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
import { TokenState } from 'src/logic/tokens/store/reducer/tokens'
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
import { AppReduxState } from 'src/store'
import { humanReadableValue } from 'src/logic/tokens/utils/humanReadableValue'
import { safeActiveTokensSelector, safeBlacklistedTokensSelector, safeSelector } from 'src/logic/safe/store/selectors'
import { safeActiveTokensSelector, safeSelector } from 'src/logic/safe/store/selectors'
import { tokensSelector } from 'src/logic/tokens/store/selectors'
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
import { sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { setCurrencyBalances } from 'src/logic/currencyValues/store/actions/setCurrencyBalances'
import { getNetworkInfo } from 'src/config'
import BigNumber from 'bignumber.js'
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
export type BalanceRecord = {
tokenBalance: string
fiatBalance?: string
}
interface ExtractedData {
balances: Map<string, string>
currencyList: List<CurrencyRateValueRecord>
balances: Map<string, BalanceRecord>
ethBalance: string
tokens: List<Token>
}
const { nativeCoin } = getNetworkInfo()
const extractDataFromResult = (currentTokens: TokenState, fiatCode: string) => (
const extractDataFromResult = (currentTokens: TokenState) => (
acc: ExtractedData,
{ balance, fiatBalance, tokenInfo }: TokenBalance,
): ExtractedData => {
const { address: tokenAddress, decimals } = tokenInfo
if (sameAddress(tokenAddress, ZERO_ADDRESS) || sameAddress(tokenAddress, nativeCoin.address)) {
acc.ethBalance = humanReadableValue(balance, 18)
} else {
acc.balances = acc.balances.merge({ [tokenAddress]: humanReadableValue(balance, Number(decimals)) })
if (currentTokens && !currentTokens.get(tokenAddress)) {
acc.tokens = acc.tokens.push(makeToken({ ...tokenInfo }))
}
}
acc.balances = acc.balances.merge({
[tokenAddress]: {
fiatBalance,
tokenBalance: humanReadableValue(balance, Number(decimals)),
},
})
acc.currencyList = acc.currencyList.push(
makeBalanceCurrency({
currencyName: fiatCode,
tokenAddress,
balanceInBaseCurrency: fiatBalance,
balanceInSelectedCurrency: fiatBalance,
}),
)
if (currentTokens && !currentTokens.get(tokenAddress)) {
acc.tokens = acc.tokens.push(makeToken({ ...tokenInfo }))
}
return acc
}
const fetchSafeTokens = (safeAddress: string) => async (
export const fetchSafeTokens = (safeAddress: string, currencySelected?: string) => async (
dispatch: Dispatch,
getState: () => AppReduxState,
): Promise<void> => {
@ -66,38 +59,40 @@ const fetchSafeTokens = (safeAddress: string) => async (
const state = getState()
const safe = safeSelector(state)
const currentTokens = tokensSelector(state)
const currencySelected = currentCurrencySelector(state)
if (!safe) {
return
}
const selectedCurrency = currentCurrencySelector(state)
const tokenCurrenciesBalances = await backOff(() => fetchTokenCurrenciesBalances(safeAddress))
const tokenCurrenciesBalances = await backOff(() =>
fetchTokenCurrenciesBalances({ safeAddress, selectedCurrency: currencySelected ?? selectedCurrency }),
)
const alreadyActiveTokens = safeActiveTokensSelector(state)
const blacklistedTokens = safeBlacklistedTokensSelector(state)
const { balances, currencyList, ethBalance, tokens } = tokenCurrenciesBalances.items.reduce<ExtractedData>(
extractDataFromResult(currentTokens, currencySelected || AVAILABLE_CURRENCIES.USD),
const { balances, ethBalance, tokens } = tokenCurrenciesBalances.items.reduce<ExtractedData>(
extractDataFromResult(currentTokens),
{
balances: Map(),
currencyList: List(),
ethBalance: '0',
tokens: List(),
},
)
// need to persist those already active tokens, despite its balances
const activeTokens = alreadyActiveTokens.union(
// active tokens by balance, excluding those already blacklisted and the `null` address
balances.keySeq().toSet().subtract(blacklistedTokens),
)
const activeTokens = alreadyActiveTokens.union(balances.keySeq().toSet())
dispatch(updateSafe({ address: safeAddress, activeTokens, balances, ethBalance }))
dispatch(setCurrencyBalances(safeAddress, currencyList))
dispatch(
updateSafe({
address: safeAddress,
activeTokens,
balances,
ethBalance,
totalFiatBalance: new BigNumber(tokenCurrenciesBalances.fiatTotal).toFixed(2),
}),
)
dispatch(addTokens(tokens))
} catch (err) {
console.error('Error fetching active token list', err)
}
}
export default fetchSafeTokens

View File

@ -5,9 +5,7 @@ import ERC721 from '@openzeppelin/contracts/build/contracts/ERC721.json'
import { List } from 'immutable'
import contract from '@truffle/contract/index.js'
import { AbiItem } from 'web3-utils'
import saveTokens from './saveTokens'
import { addTokens } from 'src/logic/tokens/store/actions/addTokens'
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
import { fetchErc20AndErc721AssetsList } from 'src/logic/tokens/api'
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
@ -85,7 +83,7 @@ export const getTokenInfos = async (tokenAddress: string): Promise<Token | undef
})
const newTokens = tokens.set(tokenAddress, token)
store.dispatch(saveTokens(newTokens))
store.dispatch(addTokens(newTokens))
return token
}
@ -109,10 +107,8 @@ export const fetchTokens = () => async (
const tokens = List(erc20Tokens.map((token) => makeToken(token)))
dispatch(saveTokens(tokens))
dispatch(addTokens(tokens))
} catch (err) {
console.error('Error fetching token list', err)
}
}
export default fetchTokens

View File

@ -1,25 +0,0 @@
import { List } from 'immutable'
import saveTokens from './saveTokens'
import { makeToken } from 'src/logic/tokens/store/model/token'
import { getActiveTokens } from 'src/logic/tokens/utils/tokensStorage'
const loadActiveTokens = () => async (dispatch) => {
try {
const tokens = (await getActiveTokens()) || {}
// The filter of strings was made because of the issue #751. Please see: https://github.com/gnosis/safe-react/pull/755#issuecomment-612969340
const tokenRecordsList = List(
Object.values(tokens)
.filter((t: any) => typeof t.decimals !== 'string')
.map((token) => makeToken(token)),
)
dispatch(saveTokens(tokenRecordsList))
} catch (err) {
// eslint-disable-next-line
console.error('Error while loading active tokens from storage:', err)
}
}
export default loadActiveTokens

View File

@ -1,5 +1,6 @@
import { Record, RecordOf } from 'immutable'
import { TokenType } from 'src/logic/safe/store/models/types/gateway'
import { BalanceRecord } from 'src/logic/tokens/store/actions/fetchSafeTokens'
export type TokenProps = {
address: string
@ -7,7 +8,7 @@ export type TokenProps = {
symbol: string
decimals: number | string
logoUri: string
balance: number | string
balance: BalanceRecord
type?: TokenType
}
@ -17,7 +18,10 @@ export const makeToken = Record<TokenProps>({
symbol: '',
decimals: 0,
logoUri: '',
balance: 0,
balance: {
fiatBalance: '0',
tokenBalance: '0',
},
})
// balance is only set in extendedSafeTokensSelector when we display user's token balances

View File

@ -2,7 +2,7 @@ import { List, Map } from 'immutable'
import { Action, handleActions } from 'redux-actions'
import { ADD_TOKEN } from 'src/logic/tokens/store/actions/addToken'
import { ADD_TOKENS } from 'src/logic/tokens/store/actions/saveTokens'
import { ADD_TOKENS } from 'src/logic/tokens/store/actions/addTokens'
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
import { AppReduxState } from 'src/store'

View File

@ -15,7 +15,9 @@ export const getEthAsToken = (balance: string | number): Token => {
const { nativeCoin } = getNetworkInfo()
return makeToken({
...nativeCoin,
balance,
balance: {
tokenBalance: balance.toString(),
},
})
}
@ -73,7 +75,7 @@ export type GetTokenByAddress = {
tokens: List<Token>
}
export type TokenFound = {
type TokenFound = {
balance: string | number
decimals: string | number
}
@ -92,7 +94,7 @@ export const getBalanceAndDecimalsFromToken = ({ tokenAddress, tokens }: GetToke
}
return {
balance: token.balance ?? 0,
balance: token.balance.tokenBalance ?? 0,
decimals: token.decimals ?? 0,
}
}

View File

@ -1,25 +0,0 @@
import { Map } from 'immutable'
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
import { TokenProps, Token } from './../store/model/token'
export const ACTIVE_TOKENS_KEY = 'ACTIVE_TOKENS'
export const CUSTOM_TOKENS_KEY = 'CUSTOM_TOKENS'
// Tokens which are active at least in one of used Safes in the app should be saved to localstorage
// to avoid iterating a large amount of data of tokens from the backend
// Custom tokens should be saved too unless they're deleted (marking them as inactive doesn't count)
export const saveActiveTokens = async (tokens: Map<string, Token>): Promise<void> => {
try {
await saveToStorage(ACTIVE_TOKENS_KEY, tokens.toJS() as Record<string, TokenProps>)
} catch (err) {
console.error('Error storing tokens in localstorage', err)
}
}
export const getActiveTokens = async (): Promise<Record<string, TokenProps> | undefined> => {
const data = await loadFromStorage<Record<string, TokenProps>>(ACTIVE_TOKENS_KEY)
return data
}

View File

@ -128,7 +128,7 @@ const ReviewComponent = ({ values, form }: ReviewComponentProps): ReactElement =
</Col>
</Row>
<Row align="center" className={classes.info}>
<Paragraph color="primary" noMargin size="md">
<Paragraph color="primary" noMargin size="lg">
You&apos;re about to create a new Safe and will have to confirm a transaction with your currently connected
wallet. The creation will cost approximately {gasCostFormatted} {nativeCoin.name}. The exact amount will be
determined by your wallet.

View File

@ -1,5 +1,6 @@
import { createStyles, makeStyles } from '@material-ui/core/styles'
import * as React from 'react'
import styled from 'styled-components'
import OpenPaper from 'src/components/Stepper/OpenPaper'
import Field from 'src/components/forms/Field'
@ -28,6 +29,12 @@ const styles = createStyles({
},
})
const StyledField = styled(Field)`
&.MuiTextField-root {
width: 460px;
}
`
const useSafeNameStyles = makeStyles(styles)
const SafeNameForm = ({ safeName }: { safeName: string }): React.ReactElement => {
@ -36,13 +43,13 @@ const SafeNameForm = ({ safeName }: { safeName: string }): React.ReactElement =>
return (
<>
<Block margin="lg">
<Paragraph color="primary" noMargin size="md">
<Paragraph color="primary" noMargin size="lg">
You are about to create a new Gnosis Safe wallet with one or more owners. First, let&apos;s give your new
wallet a name. This name is only stored locally and will never be shared with Gnosis or any third parties.
</Paragraph>
</Block>
<Block className={classes.root} margin="lg">
<Field
<StyledField
component={TextField}
defaultValue={safeName}
name={FIELD_NAME}
@ -54,7 +61,7 @@ const SafeNameForm = ({ safeName }: { safeName: string }): React.ReactElement =>
/>
</Block>
<Block margin="lg">
<Paragraph className={classes.links} color="primary" noMargin size="md">
<Paragraph className={classes.links} color="primary" noMargin size="lg">
By continuing you consent to the{' '}
<a href="https://gnosis-safe.io/terms" rel="noopener noreferrer" target="_blank">
terms of use

View File

@ -5,6 +5,7 @@ import { makeStyles } from '@material-ui/core/styles'
import CheckCircle from '@material-ui/icons/CheckCircle'
import * as React from 'react'
import { styles } from './style'
import styled from 'styled-components'
import QRIcon from 'src/assets/icons/qrcode.svg'
import trash from 'src/assets/icons/trash.svg'
@ -45,6 +46,10 @@ const { useState } = React
export const ADD_OWNER_BUTTON = '+ Add another owner'
const StyledAddressInput = styled(AddressInput)`
width: 460px;
`
/**
* Validates the whole OwnersForm, specially checks for non-repeated addresses
*
@ -152,7 +157,7 @@ const SafeOwnersForm = (props): React.ReactElement => {
return (
<>
<Block className={classes.title}>
<Paragraph color="primary" noMargin size="md" data-testid="create-safe-step-two">
<Paragraph color="primary" noMargin size="lg" data-testid="create-safe-step-two">
Your Safe will have one or more owners. We have prefilled the first owner with your connected wallet details,
but you are free to change this to a different owner.
<br />
@ -167,7 +172,7 @@ const SafeOwnersForm = (props): React.ReactElement => {
rel="noreferrer"
title="Learn about which Safe setup to use"
>
<Text size="lg" as="span" color="primary">
<Text size="xl" as="span" color="primary">
Learn about which Safe setup to use
</Text>
<Icon size="sm" type="externalLink" color="primary" />
@ -176,8 +181,8 @@ const SafeOwnersForm = (props): React.ReactElement => {
</Block>
<Hairline />
<Row className={classes.header}>
<Col xs={4}>NAME</Col>
<Col xs={8}>ADDRESS</Col>
<Col xs={3}>NAME</Col>
<Col xs={7}>ADDRESS</Col>
</Row>
<Hairline />
<Block margin="md" padding="md">
@ -187,7 +192,7 @@ const SafeOwnersForm = (props): React.ReactElement => {
return (
<Row className={classes.owner} key={`owner${index}`} data-testid={`create-safe-owner-row`}>
<Col className={classes.ownerName} xs={4}>
<Col className={classes.ownerName} xs={3}>
<Field
className={classes.name}
component={TextField}
@ -199,8 +204,8 @@ const SafeOwnersForm = (props): React.ReactElement => {
testId={`create-safe-owner-name-field-${index}`}
/>
</Col>
<Col className={classes.ownerAddress} xs={6}>
<AddressInput
<Col className={classes.ownerAddress} xs={7}>
<StyledAddressInput
fieldMutator={(newOwnerAddress) => {
const newOwnerName = getNameFromAddressBook(addressBook, newOwnerAddress, {
filterOnlyValidName: true,
@ -246,7 +251,7 @@ const SafeOwnersForm = (props): React.ReactElement => {
</Block>
<Row align="center" className={classes.add} grow margin="xl">
<Button color="secondary" data-testid="add-owner-btn" onClick={onAddOwner}>
<Paragraph noMargin size="md">
<Paragraph noMargin size="lg">
{ADD_OWNER_BUTTON}
</Paragraph>
</Button>
@ -256,7 +261,7 @@ const SafeOwnersForm = (props): React.ReactElement => {
Any transaction requires the confirmation of:
</Paragraph>
<Row align="center" className={classes.ownersAmount} margin="xl">
<Col className={classes.ownersAmountItem} xs={2}>
<Col className={classes.ownersAmountItem} xs={1}>
<Field
component={SelectField}
data-testid="threshold-select-input"

View File

@ -1,13 +1,16 @@
import { LoadFormValues } from 'src/routes/load/container/Load'
import { CreateSafeValues } from 'src/routes/open/utils/safeDataExtractor'
export const FIELD_NAME = 'name'
export const FIELD_CONFIRMATIONS = 'confirmations'
export const FIELD_OWNERS = 'owners'
export const FIELD_SAFE_NAME = 'safeName'
export const FIELD_CREATION_PROXY_SALT = 'safeCreationSalt'
export const getOwnerNameBy = (index: number): string => `owner${index}Name`
export const getOwnerAddressBy = (index: number): string => `owner${index}Address`
export const getOwnerNameBy = (index: number): string => `owner${index.toString().padStart(4, '0')}Name`
export const getOwnerAddressBy = (index: number): string => `owner${index.toString().padStart(4, '0')}Address`
export const getNumOwnersFrom = (values) => {
export const getNumOwnersFrom = (values: CreateSafeValues | LoadFormValues): number => {
const accounts = Object.keys(values)
.sort()
.filter((key) => {

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -1,18 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="114" height="92" viewBox="0 0 114 92">
<g fill="none" fill-rule="evenodd">
<g>
<g>
<g>
<path fill="#F7F5F5" d="M59.004 0c25.405 0 46 20.595 46 46 0 25.406-20.595 46-46 46s-46-20.594-46-46c0-25.405 20.595-46 46-46" transform="translate(-796 -178) translate(796 178)"/>
<path fill="#008C73" d="M26 30.002H16v-10c0-1.105-.896-2-2-2s-2 .895-2 2v10H2c-1.104 0-2 .896-2 2s.896 2 2 2h10v10c0 1.104.896 2 2 2s2-.896 2-2v-10h10c1.104 0 2-.896 2-2s-.896-2-2-2" transform="translate(-796 -178) translate(796 178)"/>
<path fill="#B2B5B2" d="M109.991 66.798c0 1.22-.992 2.211-2.211 2.211H41.202c-1.218 0-2.211-.992-2.211-2.21v-48.58c0-1.218.993-2.21 2.211-2.21h66.578c1.219 0 2.211.992 2.211 2.21V66.8zm-14 9.2h8V73.01h-8V76zm-50.996.006h8V73.01h-8v2.995zm62.785-63.996H41.202c-3.424 0-6.211 2.787-6.211 6.211V66.8c0 3.353 2.676 6.09 6.004 6.2v5.005c0 1.105.896 2 2 2h12c1.105 0 2-.895 2-2V73.01h34.996V78c0 1.104.896 2 2 2h12c1.104 0 2-.896 2-2v-5c3.327-.114 6-2.847 6-6.2v-48.58c0-3.424-2.786-6.21-6.211-6.21z" transform="translate(-796 -178) translate(796 178)"/>
<path fill="#008C73" d="M84.995 26.006c8.822 0 16 7.178 16 16 0 8.823-7.178 16-16 16s-16-7.177-16-16c0-8.822 7.178-16 16-16zm1.793 4.133c-.001.948-.738 1.732-1.675 1.808l-.15.006c-.956 0-1.74-.736-1.816-1.673l-.006-.131c-1.954.304-3.753 1.081-5.277 2.21l-.032-.03c.54.477.743 1.224.53 1.902l-.053.144c-.273.667-.912 1.104-1.594 1.125l-.146-.001c-.435.007-.855-.143-1.184-.42l-.111-.102c-1.093 1.507-1.844 3.278-2.14 5.197h-.016c.957 0 1.741.736 1.817 1.673l.006.15c0 .956-.737 1.74-1.673 1.817l-.136.006c.296 1.947 1.062 3.742 2.178 5.265.682-.59 1.679-.591 2.357-.029l.124.112c.636.633.712 1.627.179 2.371l-.095.121c1.524 1.127 3.322 1.902 5.275 2.204l.003.054c-.056-.635.223-1.248.727-1.624l.131-.089c.587-.362 1.329-.362 1.916 0 .542.335.866.925.867 1.52l-.006.147c1.962-.295 3.77-1.067 5.302-2.193l-.024-.023c-.658-.688-.672-1.755-.058-2.458l.115-.12c.687-.658 1.754-.671 2.45-.065l.093.09c1.12-1.522 1.89-3.317 2.19-5.264l.036.001c-1.007 0-1.823-.816-1.823-1.823 0-.956.737-1.74 1.674-1.817l.116-.005c-.298-1.96-1.072-3.766-2.2-5.295l-.012.012c-.299.292-.686.472-1.093.515l-.176.01c-.49.01-.963-.182-1.304-.528-.342-.34-.534-.803-.534-1.285 0-.429.152-.841.425-1.166l.12-.129c-1.531-1.124-3.338-1.895-5.297-2.19z" transform="translate(-796 -178) translate(796 178)"/>
<path fill="#008C73" d="M84.995 39.006c-1.654 0-3 1.346-3 3s1.346 3 3 3 3-1.346 3-3-1.346-3-3-3m0 10c-3.86 0-7-3.141-7-7 0-3.86 3.14-7 7-7 3.859 0 7 3.14 7 7 0 3.859-3.141 7-7 7" transform="translate(-796 -178) translate(796 178)"/>
<path fill="#B2B5B2" d="M46.996 61.002c-1.104 0-2-.896-2-2v-33c0-1.105.896-2 2-2s2 .895 2 2v33c0 1.104-.896 2-2 2M55 61.002c-1.104 0-2-.896-2-2v-33c0-1.105.896-2 2-2s2 .895 2 2v33c0 1.104-.896 2-2 2" transform="translate(-796 -178) translate(796 178)"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,4 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,19 +1,36 @@
import React, { SyntheticEvent } from 'react'
import React, { ReactElement, SyntheticEvent } from 'react'
import styled from 'styled-components'
import { Icon, Link, Text } from '@gnosis.pm/safe-react-components'
import Button from 'src/components/layout/Button'
import { connected } from 'src/theme/variables'
import { getExplorerInfo } from 'src/config'
import Hairline from 'src/components/layout/Hairline'
const ExplorerLink = styled.a`
color: ${connected};
const StyledText = styled(Text)`
display: inline-flex;
a {
margin-left: 4px;
}
svg {
position: relative;
top: 4px;
left: 4px;
}
`
const ButtonWithMargin = styled(Button)`
margin-right: 16px;
`
const FooterContainer = styled.div`
width: 100%;
height: 76px;
export const GenericFooter = ({ safeCreationTxHash }: { safeCreationTxHash: string }) => {
button {
margin-top: 24px;
}
`
export const GenericFooter = ({ safeCreationTxHash }: { safeCreationTxHash: string }): ReactElement => {
const explorerInfo = getExplorerInfo(safeCreationTxHash)
const { url, alt } = explorerInfo()
const match = /(http|https):\/\/(\w+\.\w+)\/.*/i.exec(url)
@ -21,20 +38,23 @@ export const GenericFooter = ({ safeCreationTxHash }: { safeCreationTxHash: stri
return (
<span>
<p>This process should take a couple of minutes.</p>
<p>
<Text size="xl">This process should take a couple of minutes.</Text>
<StyledText size="xl">
Follow the progress on{' '}
<ExplorerLink
aria-label={alt}
<Link
href={url}
rel="noopener noreferrer"
aria-label={alt}
target="_blank"
rel="noopener noreferrer"
data-testid="safe-create-explorer-link"
title="More info about this in Etherscan"
>
{explorerDomain}
</ExplorerLink>
.
</p>
<Text size="xl" as="span" color="primary">
{explorerDomain}
</Text>
<Icon size="sm" type="externalLink" color="primary" />
</Link>
</StyledText>
</span>
)
}
@ -45,16 +65,19 @@ export const ContinueFooter = ({
}: {
continueButtonDisabled: boolean
onContinue: (event: SyntheticEvent) => void
}) => (
<Button
color="primary"
disabled={continueButtonDisabled}
onClick={onContinue}
variant="contained"
data-testid="continue-btn"
>
Continue
</Button>
}): ReactElement => (
<FooterContainer>
<Hairline />
<Button
color="primary"
disabled={continueButtonDisabled}
onClick={onContinue}
variant="contained"
data-testid="continue-btn"
>
Get started
</Button>
</FooterContainer>
)
export const ErrorFooter = ({
@ -63,13 +86,14 @@ export const ErrorFooter = ({
}: {
onCancel: (event: SyntheticEvent) => void
onRetry: (event: SyntheticEvent) => void
}) => (
<>
}): ReactElement => (
<FooterContainer>
<Hairline />
<ButtonWithMargin onClick={onCancel} variant="contained">
Cancel
</ButtonWithMargin>
<Button color="primary" onClick={onRetry} variant="contained">
Retry
</Button>
</>
</FooterContainer>
)

View File

@ -12,20 +12,19 @@ import Paragraph from 'src/components/layout/Paragraph'
import { instantiateSafeContracts } from 'src/logic/contracts/safeContracts'
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
import { getWeb3 } from 'src/logic/wallets/getWeb3'
import { background, connected } from 'src/theme/variables'
import { background, connected, fontColor } from 'src/theme/variables'
import { providerNameSelector } from 'src/logic/wallets/store/selectors'
import { useSelector } from 'react-redux'
import LoaderDotsSvg from './assets/loader-dots.svg'
import SuccessSvg from './assets/success.svg'
import SuccessSvg from './assets/safe-created.svg'
import VaultErrorSvg from './assets/vault-error.svg'
import VaultSvg from './assets/vault.svg'
import VaultLoading from './assets/creation-process.gif'
import { PromiEvent, TransactionReceipt } from 'web3-core'
const Wrapper = styled.div`
display: grid;
grid-template-columns: 250px auto;
grid-template-rows: 62px auto;
grid-template-rows: 43px auto;
margin-bottom: 30px;
`
@ -44,29 +43,31 @@ const Body = styled.div`
grid-column: 2;
grid-row: 2;
text-align: center;
background-color: #ffffff;
background-color: ${({ theme }) => theme.colors.white};
border-radius: 5px;
min-width: 700px;
padding-top: 50px;
padding-top: 70px;
box-shadow: 0 0 10px 0 rgba(33, 48, 77, 0.1);
display: grid;
grid-template-rows: 100px 50px 70px 60px 100px;
grid-template-rows: 100px 50px 110px 1fr;
`
const CardTitle = styled.div`
font-size: 20px;
padding-top: 10px;
`
interface FullParagraphProps {
inversecolors: string
stepIndex: number
}
const FullParagraph = styled(Paragraph)<FullParagraphProps>`
background-color: ${(p) => (p.inversecolors ? connected : background)};
color: ${(p) => (p.inversecolors ? background : connected)};
padding: 24px;
font-size: 16px;
background-color: ${({ stepIndex }) => (stepIndex === 0 ? connected : background)};
color: ${({ theme, stepIndex }) => (stepIndex === 0 ? theme.colors.white : fontColor)};
padding: 28px;
font-size: 20px;
margin-bottom: 16px;
transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out;
`
@ -77,17 +78,12 @@ const BodyImage = styled.div`
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;
grid-row: 3;
margin: 27px 0;
`
const BodyFooter = styled.div`
grid-row: 5;
grid-row: 4;
padding: 10px 0;
display: flex;
@ -154,7 +150,7 @@ export const SafeDeployment = ({
}
if (stepIndex <= 4) {
return VaultSvg
return VaultLoading
}
return SuccessSvg
@ -326,20 +322,26 @@ export const SafeDeployment = ({
</Nav>
<Body>
<BodyImage>
<Img alt="Vault" height={75} src={getImage()} />
<Img alt="Vault" height={92} src={getImage()} />
</BodyImage>
<BodyDescription>
<CardTitle>{steps[stepIndex].description || steps[stepIndex].label}</CardTitle>
</BodyDescription>
<BodyLoader>{!error && stepIndex <= 4 && <Img alt="Loader dots" src={LoaderDotsSvg} />}</BodyLoader>
<BodyInstruction>
<FullParagraph color="primary" inversecolors={confirmationStep.toString()} noMargin size="md">
{error ? 'You can Cancel or Retry the Safe creation process.' : steps[stepIndex].instruction}
</FullParagraph>
</BodyInstruction>
{steps[stepIndex].instruction && (
<BodyInstruction>
<FullParagraph
color="primary"
inversecolors={confirmationStep.toString()}
noMargin
size="md"
stepIndex={stepIndex}
>
{error ? 'You can Cancel or Retry the Safe creation process.' : steps[stepIndex].instruction}
</FullParagraph>
</BodyInstruction>
)}
<BodyFooter>
{FooterComponent ? (
@ -354,9 +356,12 @@ export const SafeDeployment = ({
) : null}
</BodyFooter>
</Body>
<BackButton color="primary" minWidth={140} onClick={onCancel} data-testid="safe-creation-back-btn">
Back
</BackButton>
{stepIndex !== 0 && (
<BackButton color="primary" minWidth={140} onClick={onCancel} data-testid="safe-creation-back-btn">
Back
</BackButton>
)}
</Wrapper>
)
}

View File

@ -1,6 +1,6 @@
import { ContinueFooter, GenericFooter } from './components/Footer'
export const isConfirmationStep = (stepIndex?: number) => stepIndex === 0
export const isConfirmationStep = (stepIndex?: number): boolean => stepIndex === 0
export const steps = [
{
@ -42,7 +42,7 @@ export const steps = [
id: '6',
label: 'Success',
description: 'Your Safe was created successfully',
instruction: 'Click below to get started',
instruction: undefined,
footerComponent: ContinueFooter,
},
]

View File

@ -33,7 +33,7 @@ import {
generateColumns,
} from 'src/routes/safe/components/AddressBook/columns'
import SendModal from 'src/routes/safe/components/Balances/SendModal'
import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell'
import { OwnerAddressTableCell } from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell'
import RenameOwnerIcon from 'src/routes/safe/components/Settings/ManageOwners/assets/icons/rename-owner.svg'
import RemoveOwnerIcon from 'src/routes/safe/components/Settings/assets/icons/bin.svg'
import { addressBookQueryParamsSelector, safesListSelector } from 'src/logic/safe/store/selectors'

View File

@ -77,7 +77,7 @@ export const staticAppsList: Array<StaticAppInfo> = [
},
// Mushrooms finance
{
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmQs6CUbMUyKe3Sa3tU3HcnWWzsuCk8oJEk8CZKhRcJfEh`,
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmT96aES2YA9BssByc6DVizQDkofmKRErs8gJyqWipjyS8`,
disabled: false,
networks: [ETHEREUM_NETWORK.MAINNET],
},

View File

@ -14,11 +14,6 @@ import Table from 'src/components/Table'
import { cellWidth } from 'src/components/Table/TableHead'
import Button from 'src/components/layout/Button'
import Row from 'src/components/layout/Row'
import {
currencyRateSelector,
currentCurrencySelector,
safeFiatBalancesListSelector,
} from 'src/logic/currencyValues/store/selectors'
import { BALANCE_ROW_TEST_ID } from 'src/routes/safe/components/Balances'
import AssetTableCell from 'src/routes/safe/components/Balances/AssetTableCell'
import {
@ -33,6 +28,7 @@ import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/con
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
import { makeStyles } from '@material-ui/core/styles'
import { styles } from './styles'
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
const useStyles = makeStyles(styles)
@ -69,9 +65,7 @@ const Coins = (props: Props): React.ReactElement => {
const columns = generateColumns()
const autoColumns = columns.filter((c) => !c.custom)
const selectedCurrency = useSelector(currentCurrencySelector)
const currencyRate = useSelector(currencyRateSelector)
const activeTokens = useSelector(extendedSafeTokensSelector)
const currencyValues = useSelector(safeFiatBalancesListSelector)
const granted = useSelector(grantedSelector)
const { trackEvent } = useAnalytics()
@ -79,10 +73,10 @@ const Coins = (props: Props): React.ReactElement => {
trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Coins' })
}, [trackEvent])
const filteredData: List<BalanceData> = useMemo(
() => getBalanceData(activeTokens, selectedCurrency, currencyValues, currencyRate),
[activeTokens, selectedCurrency, currencyValues, currencyRate],
)
const filteredData: List<BalanceData> = useMemo(() => getBalanceData(activeTokens, selectedCurrency), [
activeTokens,
selectedCurrency,
])
return (
<TableContainer>

View File

@ -18,7 +18,7 @@ import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
import WhenFieldChanges from 'src/components/WhenFieldChanges'
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { getNameFromAddressBook } from 'src/logic/addressBook/utils'
import { nftTokensSelector, safeActiveSelectorMap } from 'src/logic/collectibles/store/selectors'
import { nftAssetsSelector, nftTokensSelector } from 'src/logic/collectibles/store/selectors'
import { Erc721Transfer } from 'src/logic/safe/store/models/types/gateway'
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
import { AddressBookInput } from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
@ -71,7 +71,7 @@ const SendCollectible = ({
selectedToken,
}: SendCollectibleProps): React.ReactElement => {
const classes = useStyles()
const nftAssets = useSelector(safeActiveSelectorMap)
const nftAssets = useSelector(nftAssetsSelector)
const nftTokens = useSelector(nftTokensSelector)
const addressBook = useSelector(addressBookSelector)
const [selectedEntry, setSelectedEntry] = useState<{ address: string; name: string } | null>(() => {

View File

@ -208,7 +208,7 @@ const SendFunds = ({
const setMaxAllowedAmount = () => {
const isSpendingLimit = tokenSpendingLimit && txType === 'spendingLimit'
let maxAmount = selectedToken?.balance ?? 0
let maxAmount = selectedToken?.balance.tokenBalance ?? 0
if (isSpendingLimit) {
const spendingLimitBalance = fromTokenUnit(

View File

@ -1,60 +0,0 @@
import IconButton from '@material-ui/core/IconButton'
import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close'
import React from 'react'
import { useSelector } from 'react-redux'
import { styles } from './style'
import Hairline from 'src/components/layout/Hairline'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { orderedTokenListSelector } from 'src/logic/tokens/store/selectors'
import { AssetsList } from 'src/routes/safe/components/Balances/Tokens/screens/AssetsList'
import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector'
import { safeBlacklistedTokensSelector } from 'src/logic/safe/store/selectors'
import { TokenList } from 'src/routes/safe/components/Balances/Tokens/screens/TokenList'
export const MANAGE_TOKENS_MODAL_CLOSE_BUTTON_TEST_ID = 'manage-tokens-close-modal-btn'
const useStyles = makeStyles(styles)
type Props = {
safeAddress: string
modalScreen: string
onClose: () => void
}
export const Tokens = (props: Props): React.ReactElement => {
const { modalScreen, onClose, safeAddress } = props
const tokens = useSelector(orderedTokenListSelector)
const activeTokens = useSelector(extendedSafeTokensSelector)
const blacklistedTokens = useSelector(safeBlacklistedTokensSelector)
const classes = useStyles()
return (
<>
<Row align="center" className={classes.heading} grow>
<Paragraph noMargin size="xl" weight="bolder">
Manage List
</Paragraph>
<IconButton data-testid={MANAGE_TOKENS_MODAL_CLOSE_BUTTON_TEST_ID} disableRipple onClick={onClose}>
<Close className={classes.close} />
</IconButton>
</Row>
<Hairline />
{modalScreen === 'tokenList' && (
<TokenList
activeTokens={activeTokens}
blacklistedTokens={blacklistedTokens}
safeAddress={safeAddress}
tokens={tokens}
/>
)}
{modalScreen === 'assetsList' && <AssetsList />}
</>
)
}

View File

@ -1,47 +0,0 @@
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
import ListItemText from '@material-ui/core/ListItemText'
import Switch from '@material-ui/core/Switch'
import React, { memo } from 'react'
import { useStyles } from 'src/routes/safe/components/Balances/Tokens/screens/TokenList/style'
import Img from 'src/components/layout/Img'
import { getNetworkInfo } from 'src/config'
import { setCollectibleImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
export const TOGGLE_ASSET_TEST_ID = 'toggle-asset-btn'
const { nativeCoin } = getNetworkInfo()
const AssetRow = memo(({ data, index, style }: any) => {
const classes = useStyles()
const { activeAssetsAddresses, assets, onSwitch } = data
const asset = assets[index]
const { address, image, name, symbol } = asset
const isActive = activeAssetsAddresses.includes(asset.address)
return (
<div style={style}>
<ListItem classes={{ root: classes.tokenRoot }} className={classes.token}>
<ListItemIcon className={classes.tokenIcon}>
<Img alt={name} height={28} onError={setCollectibleImageToPlaceholder} src={image} />
</ListItemIcon>
<ListItemText primary={symbol} secondary={name} />
{address !== nativeCoin.address && (
<ListItemSecondaryAction>
<Switch
checked={isActive}
inputProps={{ 'data-testid': `${symbol}_${TOGGLE_ASSET_TEST_ID}` } as any}
onChange={onSwitch(asset)}
/>
</ListItemSecondaryAction>
)}
</ListItem>
</div>
)
})
AssetRow.displayName = 'AssetRow'
export default AssetRow

View File

@ -1,132 +0,0 @@
import MuiList from '@material-ui/core/List'
import CircularProgress from '@material-ui/core/CircularProgress'
import Search from '@material-ui/icons/Search'
import cn from 'classnames'
import SearchBar from 'material-ui-search-bar'
import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { FixedSizeList } from 'react-window'
import Paragraph from 'src/components/layout/Paragraph'
import { useStyles } from './style'
import Block from 'src/components/layout/Block'
import Hairline from 'src/components/layout/Hairline'
import Row from 'src/components/layout/Row'
import { nftAssetsListSelector } from 'src/logic/collectibles/store/selectors'
import AssetRow from 'src/routes/safe/components/Balances/Tokens/screens/AssetsList/AssetRow'
import updateActiveAssets from 'src/logic/safe/store/actions/updateActiveAssets'
import updateBlacklistedAssets from 'src/logic/safe/store/actions/updateBlacklistedAssets'
import {
safeActiveAssetsListSelector,
safeBlacklistedAssetsSelector,
safeParamAddressFromStateSelector,
} from 'src/logic/safe/store/selectors'
const filterBy = (filter, nfts) =>
nfts.filter(
(asset) =>
!filter ||
asset.description.toLowerCase().includes(filter.toLowerCase()) ||
asset.name.toLowerCase().includes(filter.toLowerCase()) ||
asset.symbol.toLowerCase().includes(filter.toLowerCase()),
)
export const AssetsList = (): React.ReactElement => {
const classes = useStyles()
const searchClasses = {
input: classes.searchInput,
root: classes.searchRoot,
iconButton: classes.searchIcon,
searchContainer: classes.searchContainer,
}
const dispatch = useDispatch()
const activeAssetsList = useSelector(safeActiveAssetsListSelector)
const blacklistedAssets = useSelector(safeBlacklistedAssetsSelector)
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const [filterValue, setFilterValue] = useState('')
const [activeAssetsAddresses, setActiveAssetsAddresses] = useState(activeAssetsList)
const [blacklistedAssetsAddresses, setBlacklistedAssetsAddresses] = useState(blacklistedAssets)
const nftAssetsList = useSelector(nftAssetsListSelector)
const onCancelSearch = () => {
setFilterValue('')
}
const onChangeSearchBar = (value) => {
setFilterValue(value)
}
const getItemKey = (index) => {
return index
}
const onSwitch = (asset) => () => {
let newActiveAssetsAddresses
let newBlacklistedAssetsAddresses
if (activeAssetsAddresses.has(asset.address)) {
newActiveAssetsAddresses = activeAssetsAddresses.delete(asset.address)
newBlacklistedAssetsAddresses = blacklistedAssetsAddresses.add(asset.address)
} else {
newActiveAssetsAddresses = activeAssetsAddresses.add(asset.address)
newBlacklistedAssetsAddresses = blacklistedAssetsAddresses.delete(asset.address)
}
// Set local state
setActiveAssetsAddresses(newActiveAssetsAddresses)
setBlacklistedAssetsAddresses(newBlacklistedAssetsAddresses)
// Dispatch to global state
dispatch(updateActiveAssets(safeAddress, newActiveAssetsAddresses))
dispatch(updateBlacklistedAssets(safeAddress, newBlacklistedAssetsAddresses))
}
const createItemData = (assetsList) => {
return {
assets: assetsList,
activeAssetsAddresses,
onSwitch,
}
}
const nftAssetsFilteredList = filterBy(filterValue, nftAssetsList)
const itemData = createItemData(nftAssetsFilteredList)
return (
<>
<Block className={classes.root}>
<Row align="center" className={cn(classes.padding, classes.actions)}>
<Search className={classes.search} />
<SearchBar
classes={searchClasses}
onCancelSearch={onCancelSearch}
onChange={onChangeSearchBar}
placeholder="Search by name or symbol"
searchIcon={<div />}
value={filterValue}
/>
</Row>
<Hairline />
</Block>
{!nftAssetsList?.length && (
<Block className={classes.progressContainer} justify="center">
{!nftAssetsList ? <CircularProgress /> : <Paragraph>No collectibles available</Paragraph>}
</Block>
)}
{nftAssetsFilteredList.length > 0 && (
<MuiList className={classes.list}>
<FixedSizeList
height={413}
itemCount={nftAssetsFilteredList.length}
itemData={itemData}
itemKey={getItemKey}
itemSize={51}
overscanCount={process.env.NODE_ENV === 'test' ? 100 : 10}
width={500}
>
{AssetRow}
</FixedSizeList>
</MuiList>
)}
</>
)
}

View File

@ -1,79 +0,0 @@
import { createStyles, makeStyles } from '@material-ui/core'
import { md, mediumFontSize, secondaryText, sm, xs } from 'src/theme/variables'
export const useStyles = makeStyles(
createStyles({
root: {
minHeight: '52px',
},
search: {
color: secondaryText,
paddingLeft: sm,
},
padding: {
padding: `0 ${md}`,
},
add: {
fontSize: '11px',
fontWeight: 'normal',
paddingRight: md,
paddingLeft: md,
},
addBtnLabel: {
fontSize: mediumFontSize,
},
actions: {
height: '50px',
},
list: {
overflow: 'hidden',
overflowY: 'scroll',
padding: 0,
height: '100%',
},
tokenIcon: {
marginRight: sm,
height: '28px',
width: '28px',
},
searchInput: {
backgroundColor: 'transparent',
lineHeight: 'initial',
fontSize: '13px',
padding: 0,
'& > input::placeholder': {
letterSpacing: '-0.5px',
fontSize: mediumFontSize,
color: 'black',
},
'& > input': {
letterSpacing: '-0.5px',
},
},
progressContainer: {
width: '100%',
height: '100%',
alignItems: 'center',
},
searchContainer: {
marginLeft: xs,
marginRight: xs,
},
searchRoot: {
letterSpacing: '-0.5px',
fontSize: '13px',
border: 'none',
boxShadow: 'none',
'& > button': {
display: 'none',
},
flex: 1,
},
searchIcon: {
'&:hover': {
backgroundColor: 'transparent !important',
},
},
}),
)

View File

@ -1,54 +0,0 @@
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
import ListItemText from '@material-ui/core/ListItemText'
import Switch from '@material-ui/core/Switch'
import React, { CSSProperties, memo, ReactElement } from 'react'
import { useStyles } from './style'
import Img from 'src/components/layout/Img'
import { getNetworkInfo } from 'src/config'
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
import { ItemData } from 'src/routes/safe/components/Balances/Tokens/screens/TokenList/index'
export const TOGGLE_TOKEN_TEST_ID = 'toggle-token-btn'
interface TokenRowProps {
data: ItemData
index: number
style: CSSProperties
}
const { nativeCoin } = getNetworkInfo()
const TokenRow = memo(({ data, index, style }: TokenRowProps): ReactElement | null => {
const classes = useStyles()
const { activeTokensAddresses, onSwitch, tokens } = data
const token = tokens.get(index)
if (!token) {
return null
}
const isActive = activeTokensAddresses.has(token.address)
return (
<div style={style}>
<ListItem classes={{ root: classes.tokenRoot }} className={classes.token}>
<ListItemIcon className={classes.tokenIcon}>
<Img alt={token.name} height={28} onError={setImageToPlaceholder} src={token.logoUri} />
</ListItemIcon>
<ListItemText primary={token.symbol} secondary={token.name} />
{token.address !== nativeCoin.address && (
<ListItemSecondaryAction data-testid={`${token.symbol}_${TOGGLE_TOKEN_TEST_ID}`}>
<Switch checked={isActive} onChange={onSwitch(token)} />
</ListItemSecondaryAction>
)}
</ListItem>
</div>
)
})
TokenRow.displayName = 'TokenRow'
export default TokenRow

View File

@ -1,136 +0,0 @@
import CircularProgress from '@material-ui/core/CircularProgress'
import MuiList from '@material-ui/core/List'
import Search from '@material-ui/icons/Search'
import cn from 'classnames'
import { List, Set } from 'immutable'
import SearchBar from 'material-ui-search-bar'
import React, { useState } from 'react'
import { FixedSizeList } from 'react-window'
import TokenRow from './TokenRow'
import { useStyles } from './style'
import Block from 'src/components/layout/Block'
import Hairline from 'src/components/layout/Hairline'
import Row from 'src/components/layout/Row'
import { Token } from 'src/logic/tokens/store/model/token'
import { useDispatch } from 'react-redux'
import updateBlacklistedTokens from 'src/logic/safe/store/actions/updateBlacklistedTokens'
import updateActiveTokens from 'src/logic/safe/store/actions/updateActiveTokens'
export const ADD_CUSTOM_TOKEN_BUTTON_TEST_ID = 'add-custom-token-btn'
const filterBy = (filter: string, tokens: List<Token>): List<Token> =>
tokens.filter(
(token) =>
!filter ||
token.symbol.toLowerCase().includes(filter.toLowerCase()) ||
token.name.toLowerCase().includes(filter.toLowerCase()),
)
type Props = {
tokens: List<Token>
activeTokens: List<Token>
blacklistedTokens: Set<string>
safeAddress: string
}
export type ItemData = {
tokens: List<Token>
activeTokensAddresses: Set<string>
onSwitch: (token: Token) => () => void
}
export const TokenList = (props: Props): React.ReactElement => {
const classes = useStyles()
const { tokens, activeTokens, blacklistedTokens, safeAddress } = props
const [activeTokensAddresses, setActiveTokensAddresses] = useState(Set(activeTokens.map(({ address }) => address)))
const [blacklistedTokensAddresses, setBlacklistedTokensAddresses] = useState<Set<string>>(blacklistedTokens)
const [filter, setFilter] = useState('')
const dispatch = useDispatch()
const searchClasses = {
input: classes.searchInput,
root: classes.searchRoot,
iconButton: classes.searchIcon,
searchContainer: classes.searchContainer,
}
const onCancelSearch = () => {
setFilter('')
}
const onChangeSearchBar = (value: string) => {
setFilter(value)
}
const onSwitch = (token: Token) => () => {
let newActiveTokensAddresses
let newBlacklistedTokensAddresses
if (activeTokensAddresses.has(token.address)) {
newActiveTokensAddresses = activeTokensAddresses.delete(token.address)
newBlacklistedTokensAddresses = blacklistedTokensAddresses.add(token.address)
} else {
newActiveTokensAddresses = activeTokensAddresses.add(token.address)
newBlacklistedTokensAddresses = blacklistedTokensAddresses.delete(token.address)
}
// Set local state
setActiveTokensAddresses(newActiveTokensAddresses)
setBlacklistedTokensAddresses(newBlacklistedTokensAddresses)
// Dispatch to global state
dispatch(updateActiveTokens(safeAddress, newActiveTokensAddresses))
dispatch(updateBlacklistedTokens(safeAddress, newBlacklistedTokensAddresses))
}
const createItemData = (tokens: List<Token>, activeTokensAddresses: Set<string>): ItemData => ({
tokens,
activeTokensAddresses,
onSwitch,
})
const getItemKey = (index: number, { tokens }): string => {
return tokens.get(index).address
}
const filteredTokens = filterBy(filter, tokens)
const itemData = createItemData(filteredTokens, activeTokensAddresses)
return (
<>
<Block className={classes.root}>
<Row align="center" className={cn(classes.padding, classes.actions)}>
<Search className={classes.search} />
<SearchBar
classes={searchClasses}
onCancelSearch={onCancelSearch}
onChange={onChangeSearchBar}
placeholder="Search by name or symbol"
searchIcon={<div />}
value={filter}
/>
</Row>
<Hairline />
</Block>
{!tokens.size && (
<Block className={classes.progressContainer} justify="center">
<CircularProgress />
</Block>
)}
{tokens.size > 0 && (
<MuiList className={classes.list}>
<FixedSizeList
height={413}
itemCount={filteredTokens.size}
itemData={itemData}
itemKey={getItemKey}
itemSize={51}
overscanCount={process.env.NODE_ENV === 'test' ? 100 : 10}
width={500}
>
{TokenRow}
</FixedSizeList>
</MuiList>
)}
</>
)
}

View File

@ -1,87 +0,0 @@
import { createStyles, makeStyles } from '@material-ui/core'
import { border, md, mediumFontSize, secondaryText, sm, xs } from 'src/theme/variables'
export const useStyles = makeStyles(
createStyles({
root: {
minHeight: '52px',
},
search: {
color: secondaryText,
paddingLeft: sm,
},
padding: {
padding: `0 ${md}`,
},
add: {
fontSize: '11px',
fontWeight: 'normal',
paddingRight: md,
paddingLeft: md,
},
addBtnLabel: {
fontSize: mediumFontSize,
},
actions: {
height: '50px',
},
list: {
overflow: 'hidden',
overflowY: 'scroll',
padding: 0,
height: '100%',
},
token: {
minHeight: '50px',
borderBottom: `1px solid ${border}`,
},
tokenRoot: {
paddingTop: 0,
paddingBottom: 0,
},
searchInput: {
backgroundColor: 'transparent',
lineHeight: 'initial',
fontSize: '13px',
padding: 0,
'& > input::placeholder': {
letterSpacing: '-0.5px',
fontSize: mediumFontSize,
color: 'black',
},
'& > input': {
letterSpacing: '-0.5px',
},
},
tokenIcon: {
marginRight: md,
height: '28px',
width: '28px',
},
progressContainer: {
width: '100%',
height: '100%',
alignItems: 'center',
},
searchContainer: {
marginLeft: xs,
marginRight: xs,
},
searchRoot: {
letterSpacing: '-0.5px',
fontSize: '13px',
border: 'none',
boxShadow: 'none',
'& > button': {
display: 'none',
},
flex: 1,
},
searchIcon: {
'&:hover': {
backgroundColor: 'transparent !important',
},
},
}),
)

View File

@ -1,15 +0,0 @@
import { lg, md } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const styles = createStyles({
heading: {
padding: `${md} ${lg}`,
justifyContent: 'space-between',
maxHeight: '75px',
boxSizing: 'border-box',
},
close: {
height: '35px',
width: '35px',
},
})

View File

@ -1,32 +1,13 @@
import { BigNumber } from 'bignumber.js'
import { List } from 'immutable'
import { getNetworkInfo } from 'src/config'
import { FIXED } from 'src/components/Table/sorting'
import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount'
import { TableColumn } from 'src/components/Table/types.d'
import { BalanceCurrencyList } from 'src/logic/currencyValues/store/model/currencyValues'
import { Token } from 'src/logic/tokens/store/model/token'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
export const BALANCE_TABLE_ASSET_ID = 'asset'
export const BALANCE_TABLE_BALANCE_ID = 'balance'
export const BALANCE_TABLE_VALUE_ID = 'value'
const { nativeCoin } = getNetworkInfo()
const getTokenValue = (token: Token, currencyValues: BalanceCurrencyList, currencyRate: number): string => {
const currencyValue = currencyValues.find(
({ tokenAddress }) => sameAddress(token.address, tokenAddress) || sameAddress(token.address, nativeCoin.address),
)
if (!currencyValue) {
return ''
}
const { balanceInBaseCurrency } = currencyValue
return new BigNumber(balanceInBaseCurrency).times(currencyRate).toString()
}
const getTokenPriceInCurrency = (balance: string, currencySelected?: string): string => {
if (!currencySelected) {
return Number('').toFixed(2)
@ -44,15 +25,10 @@ export interface BalanceData {
valueOrder: number
}
export const getBalanceData = (
activeTokens: List<Token>,
currencySelected?: string,
currencyValues?: BalanceCurrencyList,
currencyRate?: number,
): List<BalanceData> => {
export const getBalanceData = (activeTokens: List<Token>, currencySelected?: string): List<BalanceData> => {
const { nativeCoin } = getNetworkInfo()
return activeTokens.map((token) => {
const balance = currencyRate && currencyValues ? getTokenValue(token, currencyValues, currencyRate) : '0'
const { tokenBalance, fiatBalance } = token.balance
return {
[BALANCE_TABLE_ASSET_ID]: {
@ -62,11 +38,11 @@ export const getBalanceData = (
symbol: token.symbol,
},
assetOrder: token.name,
[BALANCE_TABLE_BALANCE_ID]: `${formatAmountInUsFormat(token.balance?.toString() || '0')} ${token.symbol}`,
balanceOrder: Number(token.balance),
[BALANCE_TABLE_BALANCE_ID]: `${formatAmountInUsFormat(tokenBalance?.toString() || '0')} ${token.symbol}`,
balanceOrder: Number(tokenBalance),
[FIXED]: token.symbol === nativeCoin.symbol,
[BALANCE_TABLE_VALUE_ID]: getTokenPriceInCurrency(balance, currencySelected),
valueOrder: Number(balance),
[BALANCE_TABLE_VALUE_ID]: getTokenPriceInCurrency(fiatBalance || '0', currencySelected),
valueOrder: Number(tokenBalance),
}
})
}
@ -78,6 +54,7 @@ export const generateColumns = (): List<TableColumn> => {
disablePadding: false,
label: 'Asset',
custom: false,
static: true,
width: 250,
}
@ -88,6 +65,7 @@ export const generateColumns = (): List<TableColumn> => {
disablePadding: false,
label: 'Balance',
custom: false,
static: true,
}
const actions: TableColumn = {
@ -105,6 +83,7 @@ export const generateColumns = (): List<TableColumn> => {
order: true,
label: 'Value',
custom: false,
static: true,
disablePadding: false,
}

View File

@ -3,18 +3,16 @@ import React, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import ReceiveModal from 'src/components/App/ReceiveModal'
import { Tokens } from './Tokens'
import { styles } from './style'
import Modal from 'src/components/Modal'
import ButtonLink from 'src/components/layout/ButtonLink'
import Col from 'src/components/layout/Col'
import Divider from 'src/components/layout/Divider'
import Row from 'src/components/layout/Row'
import { SAFELIST_ADDRESS } from 'src/routes/routes'
import SendModal from 'src/routes/safe/components/Balances/SendModal'
import CurrencyDropdown from 'src/routes/safe/components/CurrencyDropdown'
import { CurrencyDropdown } from 'src/routes/safe/components/CurrencyDropdown'
import {
safeFeaturesEnabledSelector,
safeNameSelector,
@ -35,7 +33,6 @@ export const BALANCE_ROW_TEST_ID = 'balance-row'
const INITIAL_STATE = {
erc721Enabled: false,
showToken: false,
showManageCollectibleModal: false,
sendFunds: {
isOpen: false,
selectedToken: '',
@ -95,17 +92,8 @@ const Balances = (): React.ReactElement => {
}))
}
const {
assetDivider,
assetTab,
assetTabActive,
assetTabs,
controls,
manageTokensButton,
receiveModal,
tokenControls,
} = classes
const { erc721Enabled, sendFunds, showManageCollectibleModal, showReceive, showToken } = state
const { assetDivider, assetTab, assetTabActive, assetTabs, controls, receiveModal, tokenControls } = classes
const { erc721Enabled, sendFunds, showReceive } = state
return (
<>
@ -140,32 +128,7 @@ const Balances = (): React.ReactElement => {
path={`${SAFELIST_ADDRESS}/${address}/balances/collectibles`}
exact
render={() => {
return !erc721Enabled ? (
<Redirect to={`${SAFELIST_ADDRESS}/${address}/balances`} />
) : (
<Col className={tokenControls} end="sm" sm={6} xs={12}>
<ButtonLink
className={manageTokensButton}
onClick={() => onShow('ManageCollectibleModal')}
size="lg"
testId="manage-tokens-btn"
>
Manage List
</ButtonLink>
<Modal
description={'Enable and disable tokens to be listed'}
handleClose={() => onHide('ManageCollectibleModal')}
open={showManageCollectibleModal}
title="Manage List"
>
<Tokens
modalScreen={'assetsList'}
onClose={() => onHide('ManageCollectibleModal')}
safeAddress={address}
/>
</Modal>
</Col>
)
return !erc721Enabled ? <Redirect to={`${SAFELIST_ADDRESS}/${address}/balances`} /> : null
}}
/>
<Route
@ -176,22 +139,6 @@ const Balances = (): React.ReactElement => {
<>
<Col className={tokenControls} end="sm" sm={6} xs={12}>
<CurrencyDropdown />
<ButtonLink
className={manageTokensButton}
onClick={() => onShow('Token')}
size="lg"
testId="manage-tokens-btn"
>
Manage List
</ButtonLink>
<Modal
description={'Enable and disable tokens to be listed'}
handleClose={() => onHide('Token')}
open={showToken}
title="Manage List"
>
<Tokens modalScreen={'tokenList'} onClose={() => onHide('Token')} safeAddress={address} />
</Modal>
</Col>
</>
)

View File

@ -13,26 +13,22 @@ import { useDispatch, useSelector } from 'react-redux'
import CheckIcon from './img/check.svg'
import { setSelectedCurrency } from 'src/logic/currencyValues/store/actions/setSelectedCurrency'
import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues'
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
import { useDropdownStyles } from 'src/routes/safe/components/CurrencyDropdown/style'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { availableCurrenciesSelector, currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
import { DropdownListTheme } from 'src/theme/mui'
import { setImageToPlaceholder } from '../Balances/utils'
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
import Img from 'src/components/layout/Img/index'
import { getNetworkInfo } from 'src/config'
import { sameString } from 'src/utils/strings'
const { nativeCoin } = getNetworkInfo()
const CurrencyDropdown = (): React.ReactElement | null => {
const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
export const CurrencyDropdown = (): React.ReactElement | null => {
const dispatch = useDispatch()
const [anchorEl, setAnchorEl] = useState(null)
const selectedCurrency = useSelector(currentCurrencySelector)
const [searchParams, setSearchParams] = useState('')
const currenciesList = Object.values(AVAILABLE_CURRENCIES)
const currenciesList = useSelector(availableCurrenciesSelector)
const tokenImage = nativeCoin.logoUri
const classes = useDropdownStyles({})
const currenciesListFiltered = currenciesList.filter((currency) =>
@ -47,8 +43,8 @@ const CurrencyDropdown = (): React.ReactElement | null => {
setAnchorEl(null)
}
const onCurrentCurrencyChangedHandler = (newCurrencySelectedName) => {
dispatch(setSelectedCurrency(safeAddress, newCurrencySelectedName))
const onCurrentCurrencyChangedHandler = (newCurrencySelectedName: string) => {
dispatch(setSelectedCurrency({ selectedCurrency: newCurrencySelectedName }))
handleClose()
}
@ -80,6 +76,7 @@ const CurrencyDropdown = (): React.ReactElement | null => {
horizontal: 'center',
vertical: 'top',
}}
TransitionProps={{ mountOnEnter: true, unmountOnExit: true }}
>
<MenuItem className={classes.listItemSearch} key="0">
<div className={classes.search}>
@ -139,5 +136,3 @@ const CurrencyDropdown = (): React.ReactElement | null => {
</MuiThemeProvider>
)
}
export default CurrencyDropdown

View File

@ -6,7 +6,7 @@ import Modal from 'src/components/Modal'
import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import addSafeOwner from 'src/logic/safe/store/actions/addSafeOwner'
import { addSafeOwner } from 'src/logic/safe/store/actions/addSafeOwner'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { checksumAddress } from 'src/utils/checksumAddress'
@ -16,7 +16,7 @@ import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionPara
import { OwnerForm } from './screens/OwnerForm'
import { ReviewAddOwner } from './screens/Review'
import ThresholdForm from './screens/ThresholdForm'
import { ThresholdForm } from './screens/ThresholdForm'
const styles = createStyles({
biggerModalWindow: {
@ -65,7 +65,7 @@ type Props = {
onClose: () => void
}
const AddOwner = ({ isOpen, onClose }: Props): React.ReactElement => {
export const AddOwnerModal = ({ isOpen, onClose }: Props): React.ReactElement => {
const classes = useStyles()
const [activeScreen, setActiveScreen] = useState('selectOwner')
const [values, setValues] = useState<OwnerValues>({ ownerName: '', ownerAddress: '', threshold: '' })
@ -138,5 +138,3 @@ const AddOwner = ({ isOpen, onClose }: Props): React.ReactElement => {
</Modal>
)
}
export default AddOwner

View File

@ -11,14 +11,20 @@ import AddressInput from 'src/components/forms/AddressInput'
import Field from 'src/components/forms/Field'
import GnoForm from 'src/components/forms/GnoForm'
import TextField from 'src/components/forms/TextField'
import { composeValidators, minMaxLength, required, uniqueAddress } from 'src/components/forms/validator'
import {
addressIsNotCurrentSafe,
composeValidators,
minMaxLength,
required,
uniqueAddress,
} from 'src/components/forms/validator'
import Block from 'src/components/layout/Block'
import Button from 'src/components/layout/Button'
import Col from 'src/components/layout/Col'
import Hairline from 'src/components/layout/Hairline'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { safeOwnersAddressesListSelector } from 'src/logic/safe/store/selectors'
import { safeOwnersAddressesListSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
export const ADD_OWNER_NAME_INPUT_TEST_ID = 'add-owner-name-input'
export const ADD_OWNER_ADDRESS_INPUT_TEST_ID = 'add-owner-address-testid'
@ -43,7 +49,9 @@ export const OwnerForm = ({ onClose, onSubmit }: OwnerFormProps): React.ReactEle
onSubmit(values)
}
const owners = useSelector(safeOwnersAddressesListSelector)
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const ownerDoesntExist = uniqueAddress(owners)
const ownerAddressIsNotSafeAddress = addressIsNotCurrentSafe(safeAddress)
return (
<>
@ -98,7 +106,7 @@ export const OwnerForm = ({ onClose, onSubmit }: OwnerFormProps): React.ReactEle
placeholder="Owner address*"
testId={ADD_OWNER_ADDRESS_INPUT_TEST_ID}
text="Owner address*"
validators={[ownerDoesntExist]}
validators={[ownerDoesntExist, ownerAddressIsNotSafeAddress]}
/>
</Col>
<Col center="xs" className={classes} middle="xs" xs={1}>

View File

@ -1,8 +1,8 @@
import IconButton from '@material-ui/core/IconButton'
import MenuItem from '@material-ui/core/MenuItem'
import { withStyles } from '@material-ui/core/styles'
import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close'
import React from 'react'
import React, { ReactElement } from 'react'
import { useSelector } from 'react-redux'
import { styles } from './style'
@ -21,10 +21,24 @@ import { safeOwnersSelector, safeThresholdSelector } from 'src/logic/safe/store/
export const ADD_OWNER_THRESHOLD_NEXT_BTN_TEST_ID = 'add-owner-threshold-next-btn'
const ThresholdForm = ({ classes, onClickBack, onClose, onSubmit }) => {
const useStyles = makeStyles(styles)
type SubmitProps = {
threshold: number
}
type Props = {
onClickBack: () => void
onClose: () => void
onSubmit: (values: SubmitProps) => void
}
export const ThresholdForm = ({ onClickBack, onClose, onSubmit }: Props): ReactElement => {
const classes = useStyles()
const threshold = useSelector(safeThresholdSelector) as number
const owners = useSelector(safeOwnersSelector)
const handleSubmit = (values) => {
const handleSubmit = (values: SubmitProps) => {
onSubmit(values)
}
@ -110,5 +124,3 @@ const ThresholdForm = ({ classes, onClickBack, onClose, onSubmit }) => {
</>
)
}
export default withStyles(styles as any)(ThresholdForm)

View File

@ -1,6 +1,7 @@
import { lg, md, secondaryText, sm } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const styles = () => ({
export const styles = createStyles({
heading: {
padding: `${sm} ${lg}`,
justifyContent: 'flex-start',

View File

@ -1,5 +1,5 @@
import * as React from 'react'
import { useEffect, useState } from 'react'
import { ReactElement, useEffect, useState } from 'react'
import Identicon from 'src/components/Identicon'
import Block from 'src/components/layout/Block'
@ -16,7 +16,7 @@ type OwnerAddressTableCellProps = {
sendModalOpenHandler?: () => void
}
const OwnerAddressTableCell = (props: OwnerAddressTableCellProps): React.ReactElement => {
export const OwnerAddressTableCell = (props: OwnerAddressTableCellProps): ReactElement => {
const { address, knownAddress, showLinks, userName, sendModalOpenHandler } = props
const [cut, setCut] = useState(0)
const { width } = useWindowDimensions()
@ -50,5 +50,3 @@ const OwnerAddressTableCell = (props: OwnerAddressTableCellProps): React.ReactEl
</Block>
)
}
export default OwnerAddressTableCell

View File

@ -4,13 +4,13 @@ import { useDispatch, useSelector } from 'react-redux'
import CheckOwner from './screens/CheckOwner'
import { ReviewRemoveOwnerModal } from './screens/Review'
import ThresholdForm from './screens/ThresholdForm'
import { ThresholdForm } from './screens/ThresholdForm'
import Modal from 'src/components/Modal'
import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import removeSafeOwner from 'src/logic/safe/store/actions/removeSafeOwner'
import { removeSafeOwner } from 'src/logic/safe/store/actions/removeSafeOwner'
import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'

View File

@ -30,7 +30,7 @@ type Props = {
onSubmit: (txParameters: TxParameters) => void
}
const ThresholdForm = ({ onClickBack, onClose, onSubmit }: Props): ReactElement => {
export const ThresholdForm = ({ onClickBack, onClose, onSubmit }: Props): ReactElement => {
const classes = useStyles()
const owners = useSelector(safeOwnersSelector)
const threshold = useSelector(safeThresholdSelector) as number
@ -120,5 +120,3 @@ const ThresholdForm = ({ onClickBack, onClose, onSubmit }: Props): ReactElement
</>
)
}
export default ThresholdForm

View File

@ -7,15 +7,15 @@ import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions
import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import replaceSafeOwner from 'src/logic/safe/store/actions/replaceSafeOwner'
import { replaceSafeOwner } from 'src/logic/safe/store/actions/replaceSafeOwner'
import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors'
import { checksumAddress } from 'src/utils/checksumAddress'
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
import OwnerForm from './screens/OwnerForm'
import { ReviewReplaceOwnerModal } from './screens/Review'
import { OwnerForm } from 'src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm'
import { ReviewReplaceOwnerModal } from 'src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review'
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
const styles = createStyles({

View File

@ -1,8 +1,7 @@
import IconButton from '@material-ui/core/IconButton'
import { withStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close'
import classNames from 'classnames/bind'
import React from 'react'
import React, { ReactElement } from 'react'
import { useSelector } from 'react-redux'
import CopyBtn from 'src/components/CopyBtn'
@ -10,7 +9,13 @@ import AddressInput from 'src/components/forms/AddressInput'
import Field from 'src/components/forms/Field'
import GnoForm from 'src/components/forms/GnoForm'
import TextField from 'src/components/forms/TextField'
import { composeValidators, minMaxLength, required, uniqueAddress } from 'src/components/forms/validator'
import {
addressIsNotCurrentSafe,
composeValidators,
minMaxLength,
required,
uniqueAddress,
} from 'src/components/forms/validator'
import Identicon from 'src/components/Identicon'
import Block from 'src/components/layout/Block'
import Button from 'src/components/layout/Button'
@ -19,11 +24,12 @@ import Hairline from 'src/components/layout/Hairline'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
import { safeOwnersAddressesListSelector } from 'src/logic/safe/store/selectors'
import { safeOwnersAddressesListSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { styles } from './style'
import { getExplorerInfo } from 'src/config'
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
import { makeStyles } from '@material-ui/core'
export const REPLACE_OWNER_NAME_INPUT_TEST_ID = 'replace-owner-name-input'
export const REPLACE_OWNER_ADDRESS_INPUT_TEST_ID = 'replace-owner-address-testid'
@ -35,12 +41,30 @@ const formMutators = {
},
}
const OwnerForm = ({ classes, onClose, onSubmit, ownerAddress, ownerName }) => {
const handleSubmit = (values) => {
const useStyles = makeStyles(styles)
type NewOwnerProps = {
ownerAddress: string
ownerName: string
}
type OwnerFormProps = {
onClose: () => void
onSubmit: (values: NewOwnerProps) => void
ownerAddress: string
ownerName: string
}
export const OwnerForm = ({ onClose, onSubmit, ownerAddress, ownerName }: OwnerFormProps): ReactElement => {
const classes = useStyles()
const handleSubmit = (values: NewOwnerProps) => {
onSubmit(values)
}
const owners = useSelector(safeOwnersAddressesListSelector)
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const ownerDoesntExist = uniqueAddress(owners)
const ownerAddressIsNotSafeAddress = addressIsNotCurrentSafe(safeAddress)
return (
<>
@ -106,7 +130,6 @@ const OwnerForm = ({ classes, onClose, onSubmit, ownerAddress, ownerName }) => {
<Row margin="md">
<Col xs={8}>
<Field
className={classes.addressInput}
component={TextField}
name="ownerName"
placeholder="Owner name*"
@ -120,13 +143,12 @@ const OwnerForm = ({ classes, onClose, onSubmit, ownerAddress, ownerName }) => {
<Row margin="md">
<Col xs={8}>
<AddressInput
className={classes.addressInput}
fieldMutator={mutators.setOwnerAddress}
name="ownerAddress"
placeholder="Owner address*"
testId={REPLACE_OWNER_ADDRESS_INPUT_TEST_ID}
text="Owner address*"
validators={[ownerDoesntExist]}
validators={[ownerDoesntExist, ownerAddressIsNotSafeAddress]}
/>
</Col>
<Col center="xs" className={classes} middle="xs" xs={1}>
@ -136,11 +158,10 @@ const OwnerForm = ({ classes, onClose, onSubmit, ownerAddress, ownerName }) => {
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button className={classes.button} minWidth={140} onClick={onClose}>
<Button minWidth={140} onClick={onClose}>
Cancel
</Button>
<Button
className={classes.button}
color="primary"
minWidth={140}
testId={REPLACE_OWNER_NEXT_BTN_TEST_ID}
@ -157,5 +178,3 @@ const OwnerForm = ({ classes, onClose, onSubmit, ownerAddress, ownerName }) => {
</>
)
}
export default withStyles(styles as any)(OwnerForm)

View File

@ -1,6 +1,7 @@
import { lg, md, secondaryText, sm } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const styles = () => ({
export const styles = createStyles({
heading: {
padding: `${sm} ${lg}`,
justifyContent: 'flex-start',

View File

@ -8,9 +8,9 @@ import { List } from 'immutable'
import RemoveOwnerIcon from '../assets/icons/bin.svg'
import AddOwnerModal from './AddOwnerModal'
import { AddOwnerModal } from './AddOwnerModal'
import { EditOwnerModal } from './EditOwnerModal'
import OwnerAddressTableCell from './OwnerAddressTableCell'
import { OwnerAddressTableCell } from './OwnerAddressTableCell'
import { RemoveOwnerModal } from './RemoveOwnerModal'
import { ReplaceOwnerModal } from './ReplaceOwnerModal'
import RenameOwnerIcon from './assets/icons/rename-owner.svg'

View File

@ -199,8 +199,8 @@ export const ChangeThresholdModal = ({
)}
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} onClick={onClose}>
Back
<Button minWidth={140} onClick={onClose} color="secondary">
Cancel
</Button>
<Button
color="primary"
@ -209,7 +209,7 @@ export const ChangeThresholdModal = ({
variant="contained"
disabled={txEstimationExecutionStatus === EstimationStatus.LOADING}
>
Change
Submit
</Button>
</Row>
</>

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