Merge branch 'development' into issue-1144

This commit is contained in:
nicosampler 2020-08-06 19:28:03 -03:00
commit b119f3e88f
17 changed files with 132 additions and 127 deletions

View File

@ -85,6 +85,10 @@ const isSafeMethod = (methodId: string): boolean => {
} }
export const decodeMethods = (data: string): DataDecoded | null => { export const decodeMethods = (data: string): DataDecoded | null => {
if(!data.length) {
return null
}
const [methodId, params] = [data.slice(0, 10), data.slice(10)] const [methodId, params] = [data.slice(0, 10), data.slice(10)]
if (isSafeMethod(methodId)) { if (isSafeMethod(methodId)) {

View File

@ -3,7 +3,7 @@ import createDecorator from 'final-form-calculate'
import React from 'react' import React from 'react'
import { useField, useFormState } from 'react-final-form' import { useField, useFormState } from 'react-final-form'
import { SafeApp } from 'src/routes/safe/components/Apps/types' import { SafeApp } from 'src/routes/safe/components/Apps/types.d'
import { getAppInfoFromUrl, getIpfsLinkFromEns, uniqueApp } from 'src/routes/safe/components/Apps/utils' import { getAppInfoFromUrl, getIpfsLinkFromEns, uniqueApp } from 'src/routes/safe/components/Apps/utils'
import { composeValidators, required } from 'src/components/forms/validator' import { composeValidators, required } from 'src/components/forms/validator'
import Field from 'src/components/forms/Field' import Field from 'src/components/forms/Field'

View File

@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import { useFormState } from 'react-final-form' import { useFormState } from 'react-final-form'
import { SafeApp } from 'src/routes/safe/components/Apps/types' import { SafeApp } from 'src/routes/safe/components/Apps/types.d'
import { isAppManifestValid } from 'src/routes/safe/components/Apps/utils' import { isAppManifestValid } from 'src/routes/safe/components/Apps/utils'
interface SubmitButtonStatusProps { interface SubmitButtonStatusProps {

View File

@ -6,7 +6,7 @@ import AppAgreement from './AppAgreement'
import AppUrl, { AppInfoUpdater, appUrlResolver } from './AppUrl' import AppUrl, { AppInfoUpdater, appUrlResolver } from './AppUrl'
import SubmitButtonStatus from './SubmitButtonStatus' import SubmitButtonStatus from './SubmitButtonStatus'
import { SafeApp } from 'src/routes/safe/components/Apps/types' import { SafeApp } from 'src/routes/safe/components/Apps/types.d'
import GnoForm from 'src/components/forms/GnoForm' import GnoForm from 'src/components/forms/GnoForm'
import Img from 'src/components/layout/Img' import Img from 'src/components/layout/Img'
import appsIconSvg from 'src/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg' import appsIconSvg from 'src/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg'

View File

@ -4,7 +4,7 @@ import { List } from 'immutable'
import { FIXED } from 'src/components/Table/sorting' import { FIXED } from 'src/components/Table/sorting'
import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount' import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount'
import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers' import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers'
import { TableColumn } from 'src/components/Table/types' import { TableColumn } from 'src/components/Table/types.d'
import { AVAILABLE_CURRENCIES, BalanceCurrencyList } from 'src/logic/currencyValues/store/model/currencyValues' import { AVAILABLE_CURRENCIES, BalanceCurrencyList } from 'src/logic/currencyValues/store/model/currencyValues'
import { Token } from 'src/logic/tokens/store/model/token' import { Token } from 'src/logic/tokens/store/model/token'

View File

@ -1,5 +1,5 @@
import { List } from 'immutable' import { List } from 'immutable'
import { TableColumn } from 'src/components/Table/types' import { TableColumn } from 'src/components/Table/types.d'
import { ModulePair } from 'src/routes/safe/store/models/safe' import { ModulePair } from 'src/routes/safe/store/models/safe'
export const MODULES_TABLE_ADDRESS_ID = 'address' export const MODULES_TABLE_ADDRESS_ID = 'address'

View File

@ -1,5 +1,5 @@
import { List } from 'immutable' import { List } from 'immutable'
import { TableColumn } from 'src/components/Table/types' import { TableColumn } from 'src/components/Table/types.d'
export const OWNERS_TABLE_NAME_ID = 'name' export const OWNERS_TABLE_NAME_ID = 'name'
export const OWNERS_TABLE_ADDRESS_ID = 'address' export const OWNERS_TABLE_ADDRESS_ID = 'address'

View File

@ -22,9 +22,12 @@ import Paragraph from 'src/components/layout/Paragraph'
import LinkWithRef from 'src/components/layout/Link' import LinkWithRef from 'src/components/layout/Link'
import { shortVersionOf } from 'src/logic/wallets/ethAddresses' import { shortVersionOf } from 'src/logic/wallets/ethAddresses'
import { Transaction } from 'src/routes/safe/store/models/types/transaction' import { Transaction } from 'src/routes/safe/store/models/types/transaction'
import { DataDecoded } from 'src/routes/safe/store/models/types/transactions.d'
import DividerLine from 'src/components/DividerLine'
export const TRANSACTIONS_DESC_CUSTOM_VALUE_TEST_ID = 'tx-description-custom-value' export const TRANSACTIONS_DESC_CUSTOM_VALUE_TEST_ID = 'tx-description-custom-value'
export const TRANSACTIONS_DESC_CUSTOM_DATA_TEST_ID = 'tx-description-custom-data' export const TRANSACTIONS_DESC_CUSTOM_DATA_TEST_ID = 'tx-description-custom-data'
export const TRANSACTION_DESC_ACTION_TEST_ID = 'tx-description-action-data'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
@ -45,42 +48,55 @@ const TxInfo = styled.div`
padding: 8px 8px 8px 16px; padding: 8px 8px 8px 16px;
` `
const MultiSendCustomData = ({ tx, order }: { tx: MultiSendDetails; order: number }): React.ReactElement => { const TxInfoDetails = ({ data }: { data: DataDecoded }): React.ReactElement => (
<TxInfo>
<TxDetailsMethodName size="lg" strong>
{data.method}
</TxDetailsMethodName>
{data.parameters.map((param, index) => (
<TxDetailsMethodParam key={`${data.method}_param-${index}`}>
<InlineText size="lg" strong>
{param.name}({param.type}):
</InlineText>
<Value method={data.method} type={param.type} value={param.value} />
</TxDetailsMethodParam>
))}
</TxInfo>
)
const MultiSendCustomDataAction = ({ tx, order }: { tx: MultiSendDetails; order: number }): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
const methodName = tx.data?.method ? ` (${tx.data.method})` : '' const methodName = tx.data?.method ? ` (${tx.data.method})` : ''
return ( return (
<> <Collapse
<Collapse collapseClassName={classes.collapse}
collapseClassName={classes.collapse} headerWrapperClassName={classes.collapseHeaderWrapper}
headerWrapperClassName={classes.collapseHeaderWrapper} title={<IconText iconSize="sm" iconType="code" text={`Action ${order + 1}${methodName}`} textSize="lg" />}
title={<IconText iconSize="sm" iconType="code" text={`Action ${order + 1}${methodName}`} textSize="lg" />} >
> <TxDetailsContent>
<TxDetailsContent> <TxInfo>
<TxInfo> <Bold>Send {humanReadableValue(tx.value)} ETH to:</Bold>
<Bold>Send {humanReadableValue(tx.value)} ETH to:</Bold> <OwnerAddressTableCell address={tx.to} showLinks />
<OwnerAddressTableCell address={tx.to} showLinks /> </TxInfo>
</TxInfo>
{tx.data && ( {!!tx.data && <TxInfoDetails data={tx.data} />}
<TxInfo> </TxDetailsContent>
<TxDetailsMethodName size="lg"> </Collapse>
<strong>{tx.data.method}</strong> )
</TxDetailsMethodName> }
{tx.data?.parameters.map((param, index) => (
<TxDetailsMethodParam key={`${tx.operation}_${tx.to}_${tx.data.method}_param-${index}`}> const MultiSendCustomData = ({ txDetails }: { txDetails: MultiSendDetails[] }): React.ReactElement => {
<InlineText size="lg"> const classes = useStyles()
<strong>
{param.name}({param.type}): return (
</strong> <Block className={classes.multiSendTxData} data-testid={TRANSACTIONS_DESC_CUSTOM_DATA_TEST_ID}>
</InlineText> {txDetails.map((tx, index) => (
<Value method={methodName} type={param.type} value={param.value} /> <MultiSendCustomDataAction key={`${tx.to}-row-${index}`} tx={tx} order={index} />
</TxDetailsMethodParam> ))}
))} </Block>
</TxInfo>
)}
</TxDetailsContent>
</Collapse>
</>
) )
} }
@ -128,13 +144,31 @@ const TxData = ({ data }: { data: string }): React.ReactElement => {
) )
} }
const TxActionData = ({ dataDecoded }: { dataDecoded: DataDecoded }): React.ReactElement => {
const classes = useStyles()
return (
<>
<DividerLine withArrow={false} />
<Block className={classes.txData} data-testid={TRANSACTION_DESC_ACTION_TEST_ID}>
<Bold>Action</Bold>
<TxInfoDetails data={dataDecoded} />
</Block>
<DividerLine withArrow={false} />
</>
)
}
interface GenericCustomDataProps { interface GenericCustomDataProps {
amount?: string amount?: string
data: string data: string
recipient: string recipient: string
storedTx: Transaction
} }
const GenericCustomData = ({ amount = '0', data, recipient }: GenericCustomDataProps): React.ReactElement => { const GenericCustomData = ({ amount = '0', data, recipient, storedTx }: GenericCustomDataProps): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
const recipientName = useSelector((state) => getNameFromAddressBook(state, recipient)) const recipientName = useSelector((state) => getNameFromAddressBook(state, recipient))
@ -148,6 +182,9 @@ const GenericCustomData = ({ amount = '0', data, recipient }: GenericCustomDataP
<EtherscanLink knownAddress={false} type="address" value={recipient} /> <EtherscanLink knownAddress={false} type="address" value={recipient} />
)} )}
</Block> </Block>
{!!storedTx?.dataDecoded && <TxActionData dataDecoded={storedTx.dataDecoded} />}
<Block className={classes.txData} data-testid={TRANSACTIONS_DESC_CUSTOM_DATA_TEST_ID}> <Block className={classes.txData} data-testid={TRANSACTIONS_DESC_CUSTOM_DATA_TEST_ID}>
<Bold>Data (hex encoded):</Bold> <Bold>Data (hex encoded):</Bold>
<TxData data={data} /> <TxData data={data} />
@ -164,16 +201,12 @@ interface CustomDescriptionProps {
} }
const CustomDescription = ({ amount, data, recipient, storedTx }: CustomDescriptionProps): React.ReactElement => { const CustomDescription = ({ amount, data, recipient, storedTx }: CustomDescriptionProps): React.ReactElement => {
const classes = useStyles() const txDetails = (storedTx.multiSendTx && extractMultiSendDecodedData(storedTx).txDetails) ?? undefined
return storedTx.multiSendTx ? ( return txDetails ? (
<Block className={classes.multiSendTxData} data-testid={TRANSACTIONS_DESC_CUSTOM_DATA_TEST_ID}> <MultiSendCustomData txDetails={txDetails} />
{extractMultiSendDecodedData(storedTx).txDetails?.map((tx, index) => (
<MultiSendCustomData key={`${tx.to}-row-${index}`} tx={tx} order={index} />
))}
</Block>
) : ( ) : (
<GenericCustomData amount={amount} data={data} recipient={recipient} /> <GenericCustomData amount={amount} data={data} recipient={recipient} storedTx={storedTx} />
) )
} }

View File

@ -8,7 +8,7 @@ import SettingsTxIcon from './assets/settings.svg'
import CustomIconText from 'src/components/CustomIconText' import CustomIconText from 'src/components/CustomIconText'
import { getAppInfoFromOrigin, getAppInfoFromUrl } from 'src/routes/safe/components/Apps/utils' import { getAppInfoFromOrigin, getAppInfoFromUrl } from 'src/routes/safe/components/Apps/utils'
import { SafeApp } from 'src/routes/safe/components/Apps/types' import { SafeApp } from 'src/routes/safe/components/Apps/types.d'
const typeToIcon = { const typeToIcon = {
outgoing: OutgoingTxIcon, outgoing: OutgoingTxIcon,

View File

@ -8,7 +8,7 @@ import React from 'react'
import TxType from './TxType' import TxType from './TxType'
import { buildOrderFieldFrom } from 'src/components/Table/sorting' import { buildOrderFieldFrom } from 'src/components/Table/sorting'
import { TableColumn } from 'src/components/Table/types' import { TableColumn } from 'src/components/Table/types.d'
import { formatAmount } from 'src/logic/tokens/utils/formatAmount' import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
import { INCOMING_TX_TYPES } from 'src/routes/safe/store/models/incomingTransaction' import { INCOMING_TX_TYPES } from 'src/routes/safe/store/models/incomingTransaction'
import { Transaction } from 'src/routes/safe/store/models/types/transaction' import { Transaction } from 'src/routes/safe/store/models/types/transaction'

View File

@ -13,8 +13,7 @@ describe('TxsTable Columns > getTxTableData', () => {
const txRow = txTableData.first() const txRow = txTableData.first()
// Then // Then
// expect(txRow[TX_TABLE_RAW_CANCEL_TX_ID]).toEqual(mockedCancelTransaction) expect(txRow[TX_TABLE_RAW_CANCEL_TX_ID]).toEqual(mockedCancelTransaction)
expect(txRow[TX_TABLE_RAW_CANCEL_TX_ID]).toBeUndefined()
}) })
it('should not include CancelTx object inside TxTableData', () => { it('should not include CancelTx object inside TxTableData', () => {
// Given // Given

View File

@ -8,7 +8,7 @@ import activateAssetsByBalance from 'src/logic/tokens/store/actions/activateAsse
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 { fetchTokens } from 'src/logic/tokens/store/actions/fetchTokens'
import { COINS_LOCATION_REGEX, COLLECTIBLES_LOCATION_REGEX } from 'src/routes/safe/components/Balances' import { COINS_LOCATION_REGEX, COLLECTIBLES_LOCATION_REGEX } from 'src/routes/safe/components/Balances'
import { Dispatch } from 'src/routes/safe/store/actions/types' import { Dispatch } from 'src/routes/safe/store/actions/types.d'
export const useFetchTokens = (safeAddress: string): void => { export const useFetchTokens = (safeAddress: string): void => {
const dispatch = useDispatch<Dispatch>() const dispatch = useDispatch<Dispatch>()

View File

@ -8,7 +8,7 @@ import fetchLatestMasterContractVersion from 'src/routes/safe/store/actions/fetc
import fetchSafe from 'src/routes/safe/store/actions/fetchSafe' import fetchSafe from 'src/routes/safe/store/actions/fetchSafe'
import fetchTransactions from 'src/routes/safe/store/actions/transactions/fetchTransactions' import fetchTransactions from 'src/routes/safe/store/actions/transactions/fetchTransactions'
import fetchSafeCreationTx from 'src/routes/safe/store/actions/fetchSafeCreationTx' import fetchSafeCreationTx from 'src/routes/safe/store/actions/fetchSafeCreationTx'
import { Dispatch } from 'src/routes/safe/store/actions/types' import { Dispatch } from 'src/routes/safe/store/actions/types.d'
export const useLoadSafe = (safeAddress: string): void => { export const useLoadSafe = (safeAddress: string): void => {
const dispatch = useDispatch<Dispatch>() const dispatch = useDispatch<Dispatch>()

View File

@ -1,59 +1,54 @@
import { getNewTxNonce, shouldExecuteTransaction } from 'src/routes/safe/store/actions/utils' import { getNewTxNonce, shouldExecuteTransaction } from 'src/routes/safe/store/actions/utils'
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
import { TxServiceModel } from 'src/routes/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
describe('Store actions utils > getNewTxNonce', () => { describe('Store actions utils > getNewTxNonce', () => {
it(`should return txNonce if it's a valid value`, async () => { it(`Should return passed predicted transaction nonce if it's a valid value`, async () => {
// Given // Given
const txNonce = '45' const txNonce = '45'
const lastTx = { const lastTx = { nonce: 44 } as TxServiceModel
nonce: 44, const safeInstance = {}
}
const safeInstance = {
nonce: () =>
Promise.resolve({
toString: () => Promise.resolve('45'),
}),
}
// When // When
const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance) const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance as GnosisSafe)
// Then // Then
expect(nonce).toBe('45') expect(nonce).toBe('45')
}) })
it(`should return lastTx.nonce + 1 if txNonce is not valid`, async () => { it(`Should return nonce of a last transaction + 1 if passed nonce is less than last transaction or invalid`, async () => {
// Given // Given
const txNonce = '' const txNonce = ''
const lastTx = { const lastTx = { nonce: 44 } as TxServiceModel
nonce: 44,
}
const safeInstance = { const safeInstance = {
nonce: () => methods: {
Promise.resolve({ nonce: () => ({
toString: () => Promise.resolve('45'), call: () => Promise.resolve('45'),
}), }),
},
} }
// When // When
const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance) const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance as GnosisSafe)
// Then // Then
expect(nonce).toBe('45') expect(nonce).toBe('45')
}) })
it(`should retrieve contract's instance nonce value, if txNonce and lastTx are not valid`, async () => { it(`Should retrieve contract's instance nonce value as a fallback, if txNonce and lastTx are not valid`, async () => {
// Given // Given
const txNonce = '' const txNonce = ''
const lastTx = null const lastTx = null
const safeInstance = { const safeInstance = {
nonce: () => methods: {
Promise.resolve({ nonce: () => ({
toString: () => Promise.resolve('45'), call: () => Promise.resolve('45'),
}), }),
},
} }
// When // When
const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance) const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance as GnosisSafe)
// Then // Then
expect(nonce).toBe('45') expect(nonce).toBe('45')
@ -64,18 +59,17 @@ describe('Store actions utils > shouldExecuteTransaction', () => {
it(`should return false if there's a previous tx pending to be executed`, async () => { it(`should return false if there's a previous tx pending to be executed`, async () => {
// Given // Given
const safeInstance = { const safeInstance = {
getThreshold: () => methods: {
Promise.resolve({ getThreshold: () => ({
toNumber: () => 1, call: () => Promise.resolve('1'),
}), }),
},
} }
const nonce = '1' const nonce = '1'
const lastTx = { const lastTx = { isExecuted: false } as TxServiceModel
isExecuted: false,
}
// When // When
const isExecution = await shouldExecuteTransaction(safeInstance, nonce, lastTx) const isExecution = await shouldExecuteTransaction(safeInstance as GnosisSafe, nonce, lastTx)
// Then // Then
expect(isExecution).toBeFalsy() expect(isExecution).toBeFalsy()
@ -84,18 +78,17 @@ describe('Store actions utils > shouldExecuteTransaction', () => {
it(`should return false if threshold is greater than 1`, async () => { it(`should return false if threshold is greater than 1`, async () => {
// Given // Given
const safeInstance = { const safeInstance = {
getThreshold: () => methods: {
Promise.resolve({ getThreshold: () => ({
toNumber: () => 2, call: () => Promise.resolve('2'),
}), }),
},
} }
const nonce = '1' const nonce = '1'
const lastTx = { const lastTx = { isExecuted: true } as TxServiceModel
isExecuted: true,
}
// When // When
const isExecution = await shouldExecuteTransaction(safeInstance, nonce, lastTx) const isExecution = await shouldExecuteTransaction(safeInstance as GnosisSafe, nonce, lastTx)
// Then // Then
expect(isExecution).toBeFalsy() expect(isExecution).toBeFalsy()
@ -104,18 +97,17 @@ describe('Store actions utils > shouldExecuteTransaction', () => {
it(`should return true is threshold is 1 and previous tx is executed`, async () => { it(`should return true is threshold is 1 and previous tx is executed`, async () => {
// Given // Given
const safeInstance = { const safeInstance = {
getThreshold: () => methods: {
Promise.resolve({ getThreshold: () => ({
toNumber: () => 1, call: () => Promise.resolve('1'),
}), }),
},
} }
const nonce = '1' const nonce = '1'
const lastTx = { const lastTx = { isExecuted: true } as TxServiceModel
isExecuted: true,
}
// When // When
const isExecution = await shouldExecuteTransaction(safeInstance, nonce, lastTx) const isExecution = await shouldExecuteTransaction(safeInstance as GnosisSafe, nonce, lastTx)
// Then // Then
expect(isExecution).toBeTruthy() expect(isExecution).toBeTruthy()

View File

@ -1,6 +1,5 @@
import { List, Map } from 'immutable' import { List, Map } from 'immutable'
import { decodeMethods } from 'src/logic/contracts/methodIds'
import { TOKEN_REDUCER_ID } from 'src/logic/tokens/store/reducer/tokens' import { TOKEN_REDUCER_ID } from 'src/logic/tokens/store/reducer/tokens'
import { import {
getERC20DecimalsAndSymbol, getERC20DecimalsAndSymbol,
@ -318,30 +317,6 @@ export type TxToMock = TxArgs & {
} }
export const mockTransaction = (tx: TxToMock, safeAddress: string, state: AppReduxState): Promise<Transaction> => { export const mockTransaction = (tx: TxToMock, safeAddress: string, state: AppReduxState): Promise<Transaction> => {
const submissionDate = new Date().toISOString()
const transactionStructure: TxServiceModel = {
blockNumber: null,
confirmationsRequired: null,
dataDecoded: decodeMethods(tx.data),
ethGasPrice: null,
executionDate: null,
executor: null,
fee: null,
gasUsed: null,
isExecuted: false,
isSuccessful: null,
modified: submissionDate,
origin: null,
safe: safeAddress,
safeTxHash: null,
signatures: null,
submissionDate,
transactionHash: null,
confirmations: [],
...tx,
}
const knownTokens: Map<string, Token> = state[TOKEN_REDUCER_ID] const knownTokens: Map<string, Token> = state[TOKEN_REDUCER_ID]
const safe: SafeRecord = state[SAFE_REDUCER_ID].getIn(['safes', safeAddress]) const safe: SafeRecord = state[SAFE_REDUCER_ID].getIn(['safes', safeAddress])
const cancellationTxs = state[CANCELLATION_TRANSACTIONS_REDUCER_ID].get(safeAddress) || Map() const cancellationTxs = state[CANCELLATION_TRANSACTIONS_REDUCER_ID].get(safeAddress) || Map()
@ -353,7 +328,7 @@ export const mockTransaction = (tx: TxToMock, safeAddress: string, state: AppRed
knownTokens, knownTokens,
outgoingTxs, outgoingTxs,
safe, safe,
tx: transactionStructure, tx: (tx as unknown) as TxServiceModel,
txCode: EMPTY_DATA, txCode: EMPTY_DATA,
}) })
} }

View File

@ -143,4 +143,4 @@ export default handleActions(
}), }),
) )
export * from './types/safe.d' export * from './types/safe'

View File

@ -11,11 +11,13 @@ export interface SafeReducerState {
latestMasterContractVersion: string latestMasterContractVersion: string
} }
interface SafeReducerStateSerialized extends SafeReducerState { interface SafeReducerStateJSON {
defaultSafe: 'NOT_ASKED' | string | undefined
safes: Record<string, SafeRecordProps> safes: Record<string, SafeRecordProps>
latestMasterContractVersion: string
} }
export interface SafeReducerMap extends Map<string, any> { export interface SafeReducerMap extends Map<string, any> {
toJS(): SafeReducerStateSerialized toJS(): SafeReducerStateJSON
get<K extends keyof SafeReducerState>(key: K): SafeReducerState[K] get<K extends keyof SafeReducerState>(key: K): SafeReducerState[K]
} }