Merge branch 'development' into backmerge-release-3.1.3
This commit is contained in:
commit
f15a91ba7d
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)',
|
||||
},
|
||||
|
|
|
@ -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}`
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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))
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
|
@ -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,
|
||||
}),
|
||||
)
|
|
@ -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,
|
||||
}))
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}`)
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
}
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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> = {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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'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.
|
||||
|
|
|
@ -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'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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
]
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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],
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>(() => {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 />}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
|
@ -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
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
|
@ -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',
|
||||
},
|
||||
})
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue