Merge branch 'development' into issue-1144
This commit is contained in:
commit
18dd6b7e97
|
@ -0,0 +1,26 @@
|
|||
name: 'CLA Assistant'
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_target:
|
||||
types: [opened, closed, synchronize]
|
||||
|
||||
jobs:
|
||||
CLAssistant:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'CLA Assistant'
|
||||
if: (github.event.comment.body == 'recheckcla' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||
# Alpha Release
|
||||
uses: gnosis/github-action@master
|
||||
# GitHub token, automatically provided to the action
|
||||
# (No need to define this secret in the repo settings)
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
path-to-signatures: 'signatures/version1/cla.json'
|
||||
path-to-cla-document: 'https://github.com/gnosis/safe-react/blob/master/GNOSISCLA.md'
|
||||
branch: 'cla-signatures'
|
||||
allowlist: lukasschor,mikheevm,rmeissner,germartinez,fernandomg,Agupane,nicosampler,matextrem,gabitoesmiapodo,davidalbela,alongoni,Uxio0,dasanra,miguelmota,francovenica,tschubotz,luarx,giacomolicari,gnosis-info,bot*
|
||||
empty-commit-flag: false
|
||||
blockchain-storage-flag: false
|
|
@ -106,7 +106,7 @@ Add additional notes about how to deploy this on a live system
|
|||
* [Truffle React Box](https://github.com/truffle-box/react-box) - The web framework used
|
||||
* [Ganache](https://github.com/trufflesuite/ganache-cli) - Fast Ethereum RPC client
|
||||
* [React](https://reactjs.org/) - A JS library for building user interfaces
|
||||
* [Material UI 1.X](https://material-ui-next.com/) - React components that implement Google's Material Design
|
||||
* [Material UI 4.X](https://material-ui.com/) - React components that implement Google's Material Design
|
||||
* [redux, immutable, reselect, final-form](https://redux.js.org/) - React ecosystem libraries
|
||||
* [Flow](https://flow.org/) - Static Type Checker
|
||||
|
||||
|
|
|
@ -7,14 +7,29 @@ import styled from 'styled-components'
|
|||
|
||||
const Wrapper = styled.div``
|
||||
|
||||
const HeaderWrapper = styled.div``
|
||||
|
||||
const TitleWrapper = styled.div``
|
||||
|
||||
const Header = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const Title = styled.div``
|
||||
interface Collapse {
|
||||
title: React.ReactElement | string
|
||||
description?: React.ReactElement | string
|
||||
collapseClassName?: string
|
||||
headerWrapperClassName?: string
|
||||
}
|
||||
|
||||
const Collapse = ({ children, description, title }: any) => {
|
||||
const Collapse: React.FC<Collapse> = ({
|
||||
children,
|
||||
description = null,
|
||||
title,
|
||||
collapseClassName,
|
||||
headerWrapperClassName,
|
||||
}): React.ReactElement => {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
const handleClick = () => {
|
||||
|
@ -23,15 +38,17 @@ const Collapse = ({ children, description, title }: any) => {
|
|||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Title>{title}</Title>
|
||||
<Header>
|
||||
<IconButton disableRipple onClick={handleClick} size="small">
|
||||
{open ? <ExpandLess /> : <ExpandMore />}
|
||||
</IconButton>
|
||||
{description}
|
||||
</Header>
|
||||
<HeaderWrapper className={headerWrapperClassName} onClick={handleClick}>
|
||||
<TitleWrapper>{title}</TitleWrapper>
|
||||
<Header>
|
||||
<IconButton disableRipple size="small">
|
||||
{open ? <ExpandLess /> : <ExpandMore />}
|
||||
</IconButton>
|
||||
{description}
|
||||
</Header>
|
||||
</HeaderWrapper>
|
||||
|
||||
<CollapseMUI in={open} timeout="auto" unmountOnExit>
|
||||
<CollapseMUI in={open} timeout="auto" unmountOnExit className={collapseClassName}>
|
||||
{children}
|
||||
</CollapseMUI>
|
||||
</Wrapper>
|
||||
|
|
|
@ -27,7 +27,13 @@ const useStyles = makeStyles({
|
|||
},
|
||||
})
|
||||
|
||||
const CopyBtn = ({ className = '', content, increaseZindex = false }) => {
|
||||
interface CopyBtnProps {
|
||||
className?: string
|
||||
content: string
|
||||
increaseZindex?: boolean
|
||||
}
|
||||
|
||||
const CopyBtn = ({ className = '', content, increaseZindex = false }: CopyBtnProps): React.ReactElement => {
|
||||
const [clicked, setClicked] = useState(false)
|
||||
const classes = useStyles()
|
||||
const customClasses = increaseZindex ? { popper: classes.increasedPopperZindex } : {}
|
||||
|
@ -51,7 +57,8 @@ const CopyBtn = ({ className = '', content, increaseZindex = false }) => {
|
|||
<Img
|
||||
alt="Copy to clipboard"
|
||||
height={20}
|
||||
onClick={() => {
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
copyToClipboard(content)
|
||||
setClicked(true)
|
||||
}}
|
||||
|
|
|
@ -27,7 +27,19 @@ const useStyles = makeStyles({
|
|||
},
|
||||
})
|
||||
|
||||
const EtherscanBtn = ({ className = '', increaseZindex = false, type, value }) => {
|
||||
interface EtherscanBtnProps {
|
||||
className?: string
|
||||
increaseZindex?: boolean
|
||||
type: 'tx' | 'address'
|
||||
value: string
|
||||
}
|
||||
|
||||
const EtherscanBtn = ({
|
||||
className = '',
|
||||
increaseZindex = false,
|
||||
type,
|
||||
value,
|
||||
}: EtherscanBtnProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const customClasses = increaseZindex ? { popper: classes.increasedPopperZindex } : {}
|
||||
|
||||
|
@ -36,6 +48,7 @@ const EtherscanBtn = ({ className = '', increaseZindex = false, type, value }) =
|
|||
<a
|
||||
aria-label="Show details on Etherscan"
|
||||
className={cn(classes.container, className)}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
href={getEtherScanLink(type, value)}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import cn from 'classnames'
|
||||
import React from 'react'
|
||||
|
||||
|
@ -11,15 +11,29 @@ import Span from 'src/components/layout/Span'
|
|||
import { shortVersionOf } from 'src/logic/wallets/ethAddresses'
|
||||
import EllipsisTransactionDetails from 'src/routes/safe/components/AddressBook/EllipsisTransactionDetails'
|
||||
|
||||
const EtherscanLink = ({ classes, cut, knownAddress, type, value }: any) => (
|
||||
<Block className={classes.etherscanLink}>
|
||||
<Span className={cn(knownAddress && classes.addressParagraph, classes.address)} size="md">
|
||||
{cut ? shortVersionOf(value, cut) : value}
|
||||
</Span>
|
||||
<CopyBtn className={cn(classes.button, classes.firstButton)} content={value} />
|
||||
<EtherscanBtn className={classes.button} type={type} value={value} />
|
||||
{knownAddress !== undefined ? <EllipsisTransactionDetails address={value} knownAddress={knownAddress} /> : null}
|
||||
</Block>
|
||||
)
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
export default withStyles(styles as any)(EtherscanLink)
|
||||
interface EtherscanLinkProps {
|
||||
className?: string
|
||||
cut?: number
|
||||
knownAddress?: boolean
|
||||
type?: 'tx' | 'address'
|
||||
value: string
|
||||
}
|
||||
|
||||
const EtherscanLink = ({ className, cut, knownAddress, type, value }: EtherscanLinkProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<Block className={cn(classes.etherscanLink, className)}>
|
||||
<Span className={cn(knownAddress && classes.addressParagraph, classes.address)} size="md">
|
||||
{cut ? shortVersionOf(value, cut) : value}
|
||||
</Span>
|
||||
<CopyBtn className={cn(classes.button, classes.firstButton)} content={value} />
|
||||
<EtherscanBtn className={classes.button} type={type} value={value} />
|
||||
{knownAddress !== undefined ? <EllipsisTransactionDetails address={value} knownAddress={knownAddress} /> : null}
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
||||
export default EtherscanLink
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { createStyles } from '@material-ui/core/styles'
|
||||
import { secondaryText } from 'src/theme/variables'
|
||||
|
||||
export const styles = () => ({
|
||||
export const styles = createStyles({
|
||||
etherscanLink: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
@ -11,7 +12,7 @@ export const styles = () => ({
|
|||
},
|
||||
address: {
|
||||
display: 'block',
|
||||
flexShrink: '1',
|
||||
flexShrink: 1,
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
|
|
|
@ -4,7 +4,7 @@ export const FIXED = 'fixed'
|
|||
|
||||
export const buildOrderFieldFrom = (attr: string): string => `${attr}Order`
|
||||
|
||||
const desc = (a, b, orderBy, orderProp) => {
|
||||
const desc = (a: string, b: string, orderBy: string, orderProp: boolean): number => {
|
||||
const order = orderProp ? buildOrderFieldFrom(orderBy) : orderBy
|
||||
|
||||
if (b[order] < a[order]) {
|
||||
|
@ -39,9 +39,8 @@ export const stableSort = (dataArray, cmp, fixed) => {
|
|||
}
|
||||
|
||||
export const getSorting = (
|
||||
order: string,
|
||||
orderBy?: string,
|
||||
orderProp?: boolean,
|
||||
): ((a: unknown, b: unknown) => number) => {
|
||||
return order === 'desc' ? (a, b) => desc(a, b, orderBy, orderProp) : (a, b) => -desc(a, b, orderBy, orderProp)
|
||||
}
|
||||
order: 'desc' | 'asc',
|
||||
orderBy: string,
|
||||
orderProp: boolean,
|
||||
): ((a: string, b: string) => number) =>
|
||||
order === 'desc' ? (a, b) => desc(a, b, orderBy, orderProp) : (a, b) => -desc(a, b, orderBy, orderProp)
|
||||
|
|
|
@ -13,3 +13,5 @@ export const makeAddressBookEntry = Record<AddressBookEntryProps>({
|
|||
name: '',
|
||||
isOwner: false,
|
||||
})
|
||||
|
||||
export type AddressBookEntry = RecordOf<AddressBookEntryProps>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { List, Map } from 'immutable'
|
||||
import { handleActions } from 'redux-actions'
|
||||
|
||||
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { AddressBookEntry, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { ADD_ADDRESS_BOOK } from 'src/logic/addressBook/store/actions/addAddressBook'
|
||||
import { ADD_ENTRY } from 'src/logic/addressBook/store/actions/addAddressBookEntry'
|
||||
import { ADD_OR_UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
|
||||
|
@ -14,6 +14,9 @@ import { checksumAddress } from 'src/utils/checksumAddress'
|
|||
|
||||
export const ADDRESS_BOOK_REDUCER_ID = 'addressBook'
|
||||
|
||||
export type AddressBookCollection = List<AddressBookEntry>
|
||||
export type AddressBookState = Map<string, Map<string, AddressBookCollection>>
|
||||
|
||||
export const buildAddressBook = (storedAdbk) => {
|
||||
let addressBookBuilt = Map([])
|
||||
Object.entries(storedAdbk).forEach((adbkProps: any) => {
|
||||
|
|
|
@ -15,7 +15,7 @@ interface AddressBookReducerStateSerialized extends AddressBookReducerState {
|
|||
|
||||
export interface AddressBookMap extends Map<string> {
|
||||
toJS(): AddressBookMapSerialized
|
||||
get(key: string): List<AddressBookEntryRecord>
|
||||
get(key: string, notSetValue: unknown): List<AddressBookEntryRecord>
|
||||
}
|
||||
|
||||
export interface AddressBookReducerMap extends Map<string, any> {
|
||||
|
|
|
@ -15,7 +15,7 @@ export const getAddressBook = createSelector(
|
|||
(addressBook, safeAddress) => {
|
||||
let result = List([])
|
||||
if (addressBook) {
|
||||
result = addressBook.get(safeAddress)
|
||||
result = addressBook.get(safeAddress, List())
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
|
|
@ -1,84 +1,5 @@
|
|||
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
|
||||
|
||||
// SAFE METHODS TO ITS ID
|
||||
// https://github.com/gnosis/safe-contracts/blob/development/test/safeMethodNaming.js
|
||||
// https://github.com/gnosis/safe-contracts/blob/development/contracts/GnosisSafe.sol
|
||||
// [
|
||||
// { name: "addOwnerWithThreshold", id: "0x0d582f13" },
|
||||
// { name: "DOMAIN_SEPARATOR_TYPEHASH", id: "0x1db61b54" },
|
||||
// { name: "isOwner", id: "0x2f54bf6e" },
|
||||
// { name: "execTransactionFromModule", id: "0x468721a7" },
|
||||
// { name: "signedMessages", id: "0x5ae6bd37" },
|
||||
// { name: "enableModule", id: "0x610b5925" },
|
||||
// { name: "changeThreshold", id: "0x694e80c3" },
|
||||
// { name: "approvedHashes", id: "0x7d832974" },
|
||||
// { name: "changeMasterCopy", id: "0x7de7edef" },
|
||||
// { name: "SENTINEL_MODULES", id: "0x85e332cd" },
|
||||
// { name: "SENTINEL_OWNERS", id: "0x8cff6355" },
|
||||
// { name: "getOwners", id: "0xa0e67e2b" },
|
||||
// { name: "NAME", id: "0xa3f4df7e" },
|
||||
// { name: "nonce", id: "0xaffed0e0" },
|
||||
// { name: "getModules", id: "0xb2494df3" },
|
||||
// { name: "SAFE_MSG_TYPEHASH", id: "0xc0856ffc" },
|
||||
// { name: "SAFE_TX_TYPEHASH", id: "0xccafc387" },
|
||||
// { name: "disableModule", id: "0xe009cfde" },
|
||||
// { name: "swapOwner", id: "0xe318b52b" },
|
||||
// { name: "getThreshold", id: "0xe75235b8" },
|
||||
// { name: "domainSeparator", id: "0xf698da25" },
|
||||
// { name: "removeOwner", id: "0xf8dc5dd9" },
|
||||
// { name: "VERSION", id: "0xffa1ad74" },
|
||||
// { name: "setup", id: "0xa97ab18a" },
|
||||
// { name: "execTransaction", id: "0x6a761202" },
|
||||
// { name: "requiredTxGas", id: "0xc4ca3a9c" },
|
||||
// { name: "approveHash", id: "0xd4d9bdcd" },
|
||||
// { name: "signMessage", id: "0x85a5affe" },
|
||||
// { name: "isValidSignature", id: "0x20c13b0b" },
|
||||
// { name: "getMessageHash", id: "0x0a1028c4" },
|
||||
// { name: "encodeTransactionData", id: "0xe86637db" },
|
||||
// { name: "getTransactionHash", id: "0xd8d11f78" }
|
||||
// ]
|
||||
|
||||
export const SAFE_METHODS_NAMES = {
|
||||
ADD_OWNER_WITH_THRESHOLD: 'addOwnerWithThreshold',
|
||||
CHANGE_THRESHOLD: 'changeThreshold',
|
||||
REMOVE_OWNER: 'removeOwner',
|
||||
SWAP_OWNER: 'swapOwner',
|
||||
ENABLE_MODULE: 'enableModule',
|
||||
DISABLE_MODULE: 'disableModule',
|
||||
}
|
||||
|
||||
const METHOD_TO_ID = {
|
||||
'0xe318b52b': SAFE_METHODS_NAMES.SWAP_OWNER,
|
||||
'0x0d582f13': SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD,
|
||||
'0xf8dc5dd9': SAFE_METHODS_NAMES.REMOVE_OWNER,
|
||||
'0x694e80c3': SAFE_METHODS_NAMES.CHANGE_THRESHOLD,
|
||||
'0x610b5925': SAFE_METHODS_NAMES.ENABLE_MODULE,
|
||||
'0xe009cfde': SAFE_METHODS_NAMES.DISABLE_MODULE,
|
||||
}
|
||||
|
||||
export type SafeMethods = typeof SAFE_METHODS_NAMES[keyof typeof SAFE_METHODS_NAMES]
|
||||
type TokenMethods = 'transfer' | 'transferFrom' | 'safeTransferFrom'
|
||||
|
||||
type DecodedValues = Array<{
|
||||
name: string
|
||||
type?: string
|
||||
value: string
|
||||
}>
|
||||
|
||||
type SafeDecodedParams = {
|
||||
[key in SafeMethods]?: DecodedValues
|
||||
}
|
||||
|
||||
type TokenDecodedParams = {
|
||||
[key in TokenMethods]?: DecodedValues
|
||||
}
|
||||
|
||||
export type DecodedMethods = SafeDecodedParams | TokenDecodedParams | null
|
||||
|
||||
export interface DataDecoded {
|
||||
method: SafeMethods | TokenMethods
|
||||
parameters: DecodedValues
|
||||
}
|
||||
import { DataDecoded, METHOD_TO_ID } from 'src/routes/safe/store/models/types/transactions.d'
|
||||
|
||||
export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null => {
|
||||
const [methodId, params] = [data.slice(0, 10) as keyof typeof METHOD_TO_ID | string, data.slice(10)]
|
||||
|
@ -86,12 +7,12 @@ export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null =>
|
|||
switch (methodId) {
|
||||
// swapOwner
|
||||
case '0xe318b52b': {
|
||||
const decodedParameters = web3.eth.abi.decodeParameters(['uint', 'address', 'address'], params)
|
||||
const decodedParameters = web3.eth.abi.decodeParameters(['uint', 'address', 'address'], params) as string[]
|
||||
return {
|
||||
method: METHOD_TO_ID[methodId],
|
||||
parameters: [
|
||||
{ name: 'oldOwner', value: decodedParameters[1] },
|
||||
{ name: 'newOwner', value: decodedParameters[2] },
|
||||
{ name: 'oldOwner', type: 'address', value: decodedParameters[1] },
|
||||
{ name: 'newOwner', type: 'address', value: decodedParameters[2] },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
@ -102,8 +23,8 @@ export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null =>
|
|||
return {
|
||||
method: METHOD_TO_ID[methodId],
|
||||
parameters: [
|
||||
{ name: 'owner', value: decodedParameters[0] },
|
||||
{ name: '_threshold', value: decodedParameters[1] },
|
||||
{ name: 'owner', type: 'address', value: decodedParameters[0] },
|
||||
{ name: '_threshold', type: 'uint', value: decodedParameters[1] },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
@ -114,8 +35,8 @@ export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null =>
|
|||
return {
|
||||
method: METHOD_TO_ID[methodId],
|
||||
parameters: [
|
||||
{ name: 'oldOwner', value: decodedParameters[1] },
|
||||
{ name: '_threshold', value: decodedParameters[2] },
|
||||
{ name: 'oldOwner', type: 'address', value: decodedParameters[1] },
|
||||
{ name: '_threshold', type: 'uint', value: decodedParameters[2] },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
@ -126,7 +47,7 @@ export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null =>
|
|||
return {
|
||||
method: METHOD_TO_ID[methodId],
|
||||
parameters: [
|
||||
{ name: '_threshold', value: decodedParameters[0] },
|
||||
{ name: '_threshold', type: 'uint', value: decodedParameters[0] },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
@ -137,7 +58,7 @@ export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null =>
|
|||
return {
|
||||
method: METHOD_TO_ID[methodId],
|
||||
parameters: [
|
||||
{ name: 'module', type: '', value: decodedParameters[0] },
|
||||
{ name: 'module', type: 'address', value: decodedParameters[0] },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
@ -148,8 +69,8 @@ export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null =>
|
|||
return {
|
||||
method: METHOD_TO_ID[methodId],
|
||||
parameters: [
|
||||
{ name: 'prevModule', type: '', value: decodedParameters[0] },
|
||||
{ name: 'module', type: '', value: decodedParameters[1] },
|
||||
{ name: 'prevModule', type: 'address', value: decodedParameters[0] },
|
||||
{ name: 'module', type: 'address', value: decodedParameters[1] },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
@ -177,8 +98,8 @@ export const decodeMethods = (data: string): DataDecoded | null => {
|
|||
return {
|
||||
method: 'transfer',
|
||||
parameters: [
|
||||
{ name: 'to', value: decodeParameters[0] },
|
||||
{ name: 'value', value: decodeParameters[1] },
|
||||
{ name: 'to', type: '', value: decodeParameters[0] },
|
||||
{ name: 'value', type: '', value: decodeParameters[1] },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
@ -189,9 +110,9 @@ export const decodeMethods = (data: string): DataDecoded | null => {
|
|||
return {
|
||||
method: 'transferFrom',
|
||||
parameters: [
|
||||
{ name: 'from', value: decodeParameters[0] },
|
||||
{ name: 'to', value: decodeParameters[1] },
|
||||
{ name: 'value', value: decodeParameters[2] },
|
||||
{ name: 'from', type: '', value: decodeParameters[0] },
|
||||
{ name: 'to', type: '', value: decodeParameters[1] },
|
||||
{ name: 'value', type: '', value: decodeParameters[2] },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
@ -202,9 +123,9 @@ export const decodeMethods = (data: string): DataDecoded | null => {
|
|||
return {
|
||||
method: 'safeTransferFrom',
|
||||
parameters: [
|
||||
{ name: 'from', value: decodedParameters[0] },
|
||||
{ name: 'to', value: decodedParameters[1] },
|
||||
{ name: 'value', value: decodedParameters[2] },
|
||||
{ name: 'from', type: '', value: decodedParameters[0] },
|
||||
{ name: 'to', type: '', value: decodedParameters[1] },
|
||||
{ name: 'value', type: '', value: decodedParameters[2] },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import axios, { AxiosResponse } from 'axios'
|
||||
|
||||
import { getTxServiceHost } from 'src/config'
|
||||
import { TokenProps } from '../../tokens/store/model/token'
|
||||
import { TokenProps } from 'src/logic/tokens/store/model/token'
|
||||
|
||||
type BalanceEndpoint = {
|
||||
export type BalanceEndpoint = {
|
||||
balance: string
|
||||
balanceUsd: string
|
||||
tokenAddress?: string
|
||||
|
|
|
@ -44,17 +44,19 @@ export type BalanceCurrencyRecord = {
|
|||
balanceInSelectedCurrency: string
|
||||
}
|
||||
|
||||
export type CurrencyRateValue = {
|
||||
currencyRate?: number
|
||||
selectedCurrency?: AVAILABLE_CURRENCIES
|
||||
currencyBalances?: List<BalanceCurrencyRecord>
|
||||
}
|
||||
|
||||
export type CurrencyRateValueRecord = RecordOf<CurrencyRateValue>
|
||||
|
||||
export const makeBalanceCurrency = Record({
|
||||
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?: AVAILABLE_CURRENCIES
|
||||
currencyBalances?: BalanceCurrencyList
|
||||
}
|
||||
|
|
|
@ -4,22 +4,30 @@ import { 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 { CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues'
|
||||
|
||||
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 = Map<string, CurrencyReducerMap>
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
[SET_CURRENCY_RATE]: (state, action) => {
|
||||
[SET_CURRENCY_RATE]: (state: CurrencyReducerMap, action) => {
|
||||
const { currencyRate, safeAddress } = action.payload
|
||||
|
||||
return state.setIn([safeAddress, 'currencyRate'], currencyRate)
|
||||
},
|
||||
[SET_CURRENCY_BALANCES]: (state, action) => {
|
||||
[SET_CURRENCY_BALANCES]: (state: CurrencyReducerMap, action) => {
|
||||
const { currencyBalances, safeAddress } = action.payload
|
||||
|
||||
return state.setIn([safeAddress, 'currencyBalances'], currencyBalances)
|
||||
},
|
||||
[SET_CURRENT_CURRENCY]: (state, action) => {
|
||||
[SET_CURRENT_CURRENCY]: (state: CurrencyReducerMap, action) => {
|
||||
const { safeAddress, selectedCurrency } = action.payload
|
||||
|
||||
return state.setIn([safeAddress, 'selectedCurrency'], selectedCurrency)
|
||||
|
|
|
@ -1,46 +1,41 @@
|
|||
import { List, Map, RecordOf } from 'immutable'
|
||||
import { createSelector } from 'reselect'
|
||||
|
||||
import { CURRENCY_VALUES_KEY } from 'src/logic/currencyValues/store/reducer/currencyValues'
|
||||
import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors'
|
||||
import { AppReduxState } from 'src/store/index'
|
||||
import {
|
||||
AVAILABLE_CURRENCIES,
|
||||
BalanceCurrencyRecord,
|
||||
CurrencyRateValue,
|
||||
CurrencyRateValueRecord,
|
||||
} from 'src/logic/currencyValues/store/model/currencyValues'
|
||||
CURRENCY_VALUES_KEY,
|
||||
CurrencyReducerMap,
|
||||
CurrencyValuesState,
|
||||
} from 'src/logic/currencyValues/store/reducer/currencyValues'
|
||||
import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors'
|
||||
import { AppReduxState } from 'src/store'
|
||||
import { CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues'
|
||||
import { BigNumber } from 'bignumber.js'
|
||||
|
||||
export const currencyValuesSelector = (state: AppReduxState): Map<string, RecordOf<CurrencyRateValue>> =>
|
||||
state[CURRENCY_VALUES_KEY]
|
||||
export const currencyValuesSelector = (state: AppReduxState): CurrencyValuesState => state[CURRENCY_VALUES_KEY]
|
||||
|
||||
export const safeFiatBalancesSelector = createSelector(
|
||||
currencyValuesSelector,
|
||||
safeParamAddressFromStateSelector,
|
||||
(currencyValues, safeAddress): CurrencyRateValueRecord => {
|
||||
(currencyValues, safeAddress): CurrencyReducerMap | undefined => {
|
||||
if (!currencyValues) return
|
||||
return currencyValues.get(safeAddress)
|
||||
},
|
||||
)
|
||||
|
||||
const currencyValueSelector = <K extends keyof CurrencyRateValue>(key: K) => (
|
||||
currencyValuesMap?: CurrencyReducerMap,
|
||||
): CurrencyRateValue[K] => currencyValuesMap?.get(key)
|
||||
|
||||
export const safeFiatBalancesListSelector = createSelector(
|
||||
safeFiatBalancesSelector,
|
||||
(currencyValuesMap): List<BalanceCurrencyRecord> => {
|
||||
if (!currencyValuesMap) return
|
||||
return currencyValuesMap.get('currencyBalances') ? currencyValuesMap.get('currencyBalances') : List([])
|
||||
},
|
||||
currencyValueSelector('currencyBalances'),
|
||||
)
|
||||
|
||||
export const currentCurrencySelector = createSelector(
|
||||
safeFiatBalancesSelector,
|
||||
(currencyValuesMap): AVAILABLE_CURRENCIES | null =>
|
||||
currencyValuesMap ? currencyValuesMap.get('selectedCurrency') : null,
|
||||
currencyValueSelector('selectedCurrency'),
|
||||
)
|
||||
|
||||
export const currencyRateSelector = createSelector(safeFiatBalancesSelector, (currencyValuesMap): number | null =>
|
||||
currencyValuesMap ? currencyValuesMap.get('currencyRate') : null,
|
||||
)
|
||||
export const currencyRateSelector = createSelector(safeFiatBalancesSelector, currencyValueSelector('currencyRate'))
|
||||
|
||||
export const safeFiatBalancesTotalSelector = createSelector(
|
||||
safeFiatBalancesListSelector,
|
||||
|
|
|
@ -1,23 +1,71 @@
|
|||
import { BigNumber } from 'bignumber.js'
|
||||
import { backOff } from 'exponential-backoff'
|
||||
import { List, Map } from 'immutable'
|
||||
import { batch } from 'react-redux'
|
||||
|
||||
import fetchTokenCurrenciesBalances from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
|
||||
import { setCurrencyBalances } from 'src/logic/currencyValues/store/actions/setCurrencyBalances'
|
||||
import { AVAILABLE_CURRENCIES, makeBalanceCurrency } from 'src/logic/currencyValues/store/model/currencyValues'
|
||||
import { CURRENCY_VALUES_KEY } from 'src/logic/currencyValues/store/reducer/currencyValues'
|
||||
import addTokens from 'src/logic/tokens/store/actions/saveTokens'
|
||||
import { makeToken } from 'src/logic/tokens/store/model/token'
|
||||
import { TOKEN_REDUCER_ID } from 'src/logic/tokens/store/reducer/tokens'
|
||||
import updateSafe from 'src/routes/safe/store/actions/updateSafe'
|
||||
import { SAFE_REDUCER_ID } from 'src/routes/safe/store/reducer/safe'
|
||||
import { Dispatch } from 'redux'
|
||||
import { backOff } from 'exponential-backoff'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
const humanReadableBalance = (balance, decimals) => new BigNumber(balance).times(`1e-${decimals}`).toFixed()
|
||||
const noFunc = () => {}
|
||||
const updateSafeValue = (address) => (valueToUpdate) => updateSafe({ address, ...valueToUpdate })
|
||||
import fetchTokenCurrenciesBalances, {
|
||||
BalanceEndpoint,
|
||||
} from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
|
||||
import { setCurrencyBalances } from 'src/logic/currencyValues/store/actions/setCurrencyBalances'
|
||||
import {
|
||||
AVAILABLE_CURRENCIES,
|
||||
CurrencyRateValueRecord,
|
||||
makeBalanceCurrency,
|
||||
} from 'src/logic/currencyValues/store/model/currencyValues'
|
||||
import addTokens from 'src/logic/tokens/store/actions/saveTokens'
|
||||
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
|
||||
import { TokenState } from 'src/logic/tokens/store/reducer/tokens'
|
||||
import updateSafe from 'src/routes/safe/store/actions/updateSafe'
|
||||
import { AppReduxState } from 'src/store'
|
||||
import { humanReadableValue } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import { SafeRecordProps } from 'src/routes/safe/store/models/safe'
|
||||
import {
|
||||
safeActiveTokensSelector,
|
||||
safeBalancesSelector,
|
||||
safeBlacklistedTokensSelector,
|
||||
safeEthBalanceSelector,
|
||||
safeSelector,
|
||||
} from 'src/routes/safe/store/selectors'
|
||||
import { tokensSelector } from 'src/logic/tokens/store/selectors'
|
||||
import { currencyValuesSelector } from 'src/logic/currencyValues/store/selectors'
|
||||
|
||||
const noFunc = (): void => {}
|
||||
|
||||
const updateSafeValue = (address: string) => (valueToUpdate: Partial<SafeRecordProps>) =>
|
||||
updateSafe({ address, ...valueToUpdate })
|
||||
|
||||
interface ExtractedData {
|
||||
balances: Map<string, string>
|
||||
currencyList: List<CurrencyRateValueRecord>
|
||||
ethBalance: string
|
||||
tokens: List<Token>
|
||||
}
|
||||
|
||||
const extractDataFromResult = (currentTokens: TokenState) => (
|
||||
acc: ExtractedData,
|
||||
{ balance, balanceUsd, token, tokenAddress }: BalanceEndpoint,
|
||||
): ExtractedData => {
|
||||
if (tokenAddress === null) {
|
||||
acc.ethBalance = humanReadableValue(balance, 18)
|
||||
} else {
|
||||
acc.balances = acc.balances.merge({ [tokenAddress]: humanReadableValue(balance, Number(token.decimals)) })
|
||||
|
||||
if (currentTokens && !currentTokens.get(tokenAddress)) {
|
||||
acc.tokens = acc.tokens.push(makeToken({ address: tokenAddress, ...token }))
|
||||
}
|
||||
}
|
||||
|
||||
acc.currencyList = acc.currencyList.push(
|
||||
makeBalanceCurrency({
|
||||
currencyName: balanceUsd ? AVAILABLE_CURRENCIES.USD : null,
|
||||
tokenAddress,
|
||||
balanceInBaseCurrency: balanceUsd,
|
||||
balanceInSelectedCurrency: balanceUsd,
|
||||
}),
|
||||
)
|
||||
|
||||
return acc
|
||||
}
|
||||
|
||||
const fetchSafeTokens = (safeAddress: string) => async (
|
||||
dispatch: Dispatch,
|
||||
|
@ -25,43 +73,22 @@ const fetchSafeTokens = (safeAddress: string) => async (
|
|||
): Promise<void> => {
|
||||
try {
|
||||
const state = getState()
|
||||
const safe = state[SAFE_REDUCER_ID].getIn(['safes', safeAddress])
|
||||
const currentTokens = state[TOKEN_REDUCER_ID]
|
||||
const safe = safeSelector(state)
|
||||
const currentTokens = tokensSelector(state)
|
||||
|
||||
if (!safe) {
|
||||
return
|
||||
}
|
||||
|
||||
const result = await backOff(() => fetchTokenCurrenciesBalances(safeAddress))
|
||||
const currentEthBalance = safe.get('ethBalance')
|
||||
const safeBalances = safe.get('balances')
|
||||
const alreadyActiveTokens = safe.get('activeTokens')
|
||||
const blacklistedTokens = safe.get('blacklistedTokens')
|
||||
const currencyValues = state[CURRENCY_VALUES_KEY]
|
||||
const currentEthBalance = safeEthBalanceSelector(state)
|
||||
const safeBalances = safeBalancesSelector(state)
|
||||
const alreadyActiveTokens = safeActiveTokensSelector(state)
|
||||
const blacklistedTokens = safeBlacklistedTokensSelector(state)
|
||||
const currencyValues = currencyValuesSelector(state)
|
||||
|
||||
const { balances, currencyList, ethBalance, tokens } = result.data.reduce(
|
||||
(acc, { balance, balanceUsd, token, tokenAddress }) => {
|
||||
if (tokenAddress === null) {
|
||||
acc.ethBalance = humanReadableBalance(balance, 18)
|
||||
} else {
|
||||
acc.balances = acc.balances.merge({ [tokenAddress]: humanReadableBalance(balance, token.decimals) })
|
||||
|
||||
if (currentTokens && !currentTokens.get(tokenAddress)) {
|
||||
acc.tokens = acc.tokens.push(makeToken({ address: tokenAddress, ...token }))
|
||||
}
|
||||
}
|
||||
|
||||
acc.currencyList = acc.currencyList.push(
|
||||
makeBalanceCurrency({
|
||||
currencyName: balanceUsd ? AVAILABLE_CURRENCIES.USD : null,
|
||||
tokenAddress,
|
||||
balanceInBaseCurrency: balanceUsd,
|
||||
balanceInSelectedCurrency: balanceUsd,
|
||||
}),
|
||||
)
|
||||
|
||||
return acc
|
||||
},
|
||||
const { balances, currencyList, ethBalance, tokens } = result.data.reduce<ExtractedData>(
|
||||
extractDataFromResult(currentTokens),
|
||||
{
|
||||
balances: Map(),
|
||||
currencyList: List(),
|
||||
|
@ -71,7 +98,7 @@ const fetchSafeTokens = (safeAddress: string) => async (
|
|||
)
|
||||
|
||||
// need to persist those already active tokens, despite its balances
|
||||
const activeTokens = alreadyActiveTokens.toSet().union(
|
||||
const activeTokens = alreadyActiveTokens.union(
|
||||
// active tokens by balance, excluding those already blacklisted and the `null` address
|
||||
balances.keySeq().toSet().subtract(blacklistedTokens),
|
||||
)
|
||||
|
@ -80,10 +107,7 @@ const fetchSafeTokens = (safeAddress: string) => async (
|
|||
const updateActiveTokens = activeTokens.equals(alreadyActiveTokens) ? noFunc : update({ activeTokens })
|
||||
const updateBalances = balances.equals(safeBalances) ? noFunc : update({ balances })
|
||||
const updateEthBalance = ethBalance === currentEthBalance ? noFunc : update({ ethBalance })
|
||||
const storedCurrencyBalances =
|
||||
currencyValues && currencyValues.get(safeAddress)
|
||||
? currencyValues.get(safeAddress).get('currencyBalances')
|
||||
: undefined
|
||||
const storedCurrencyBalances = currencyValues?.get(safeAddress)?.get('currencyBalances')
|
||||
|
||||
const updateCurrencies = currencyList.equals(storedCurrencyBalances)
|
||||
? noFunc
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { BigNumber } from 'bignumber.js'
|
||||
|
||||
export const humanReadableValue = (value: number | string, decimals = 18): string => {
|
||||
return new BigNumber(value).times(`1e-${decimals}`).toFixed()
|
||||
}
|
|
@ -66,7 +66,7 @@ const Load = ({ addSafe, network, provider, userAddress }: LoadProps): React.Rea
|
|||
|
||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const ownerAddresses = await gnosisSafe.methods.getOwners().call()
|
||||
const owners = getOwnersFrom(ownerNames, ownerAddresses.sort())
|
||||
const owners = getOwnersFrom(ownerNames, ownerAddresses.slice().sort())
|
||||
|
||||
await loadSafe(safeName, safeAddress, owners, addSafe)
|
||||
|
||||
|
|
|
@ -132,7 +132,7 @@ const AddressBookTable = ({ classes }) => {
|
|||
defaultRowsPerPage={25}
|
||||
disableLoadingOnEmptyTable
|
||||
label="Owners"
|
||||
size={addressBook.size}
|
||||
size={addressBook?.size || 0}
|
||||
>
|
||||
{(sortedData) =>
|
||||
sortedData.map((row, index) => {
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import React from 'react'
|
||||
import { FixedDialog, Text } from '@gnosis.pm/safe-react-components'
|
||||
|
||||
interface OwnProps {
|
||||
onCancel: () => void
|
||||
onConfirm: () => void
|
||||
}
|
||||
|
||||
const LegalDisclaimer = ({ onCancel, onConfirm }: OwnProps): JSX.Element => (
|
||||
<FixedDialog
|
||||
body={
|
||||
<>
|
||||
<Text size="md">
|
||||
You are now accessing third-party apps, which we do not own, control, maintain or audit. We are not liable for
|
||||
any loss you may suffer in connection with interacting with the apps, which is at your own risk. You must read
|
||||
our Terms, which contain more detailed provisions binding on you relating to the apps.
|
||||
</Text>
|
||||
<br />
|
||||
<Text size="md">
|
||||
I have read and understood the{' '}
|
||||
<a href="https://gnosis-safe.io/terms" rel="noopener noreferrer" target="_blank">
|
||||
Terms
|
||||
</a>{' '}
|
||||
and this Disclaimer, and agree to be bound by them.
|
||||
</Text>
|
||||
</>
|
||||
}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
title="Disclaimer"
|
||||
/>
|
||||
)
|
||||
|
||||
export default LegalDisclaimer
|
|
@ -2,8 +2,8 @@ import { ButtonLink, ManageListModal } from '@gnosis.pm/safe-react-components'
|
|||
import React, { useState } from 'react'
|
||||
|
||||
import appsIconSvg from 'src/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg'
|
||||
import AddAppForm from './AddAppForm'
|
||||
import { SafeApp } from './types'
|
||||
import AddAppForm from '../AddAppForm'
|
||||
import { SafeApp } from '../types'
|
||||
|
||||
const FORM_ID = 'add-apps-form'
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import { Icon, ModalFooterConfirmation, Text, Title } from '@gnosis.pm/safe-react-components'
|
||||
import { BigNumber } from 'bignumber.js'
|
||||
import React, { ReactElement } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
|
@ -13,6 +12,7 @@ import Bold from 'src/components/layout/Bold'
|
|||
import Heading from 'src/components/layout/Heading'
|
||||
import Img from 'src/components/layout/Img'
|
||||
import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
|
||||
import { humanReadableValue } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
|
||||
export type SafeAppTx = {
|
||||
to: string
|
||||
|
@ -28,8 +28,6 @@ type GenericModalProps = {
|
|||
onClose: () => void
|
||||
}
|
||||
|
||||
const humanReadableBalance = (balance, decimals) => new BigNumber(balance).times(`1e-${decimals}`).toFixed()
|
||||
|
||||
const Wrapper = styled.div`
|
||||
margin-bottom: 15px;
|
||||
`
|
||||
|
@ -77,7 +75,7 @@ const confirmTransactions = (
|
|||
ethBalance: string,
|
||||
nameApp: string,
|
||||
iconApp: string,
|
||||
txs: Array<SafeAppTx>,
|
||||
txs: SafeAppTx[],
|
||||
openModal: (modalInfo: GenericModalProps) => void,
|
||||
closeModal: () => void,
|
||||
onConfirm: () => void,
|
||||
|
@ -110,7 +108,7 @@ const confirmTransactions = (
|
|||
<Heading tag="h3">Value</Heading>
|
||||
<div className="value-section">
|
||||
<Img alt="Ether" height={40} src={getEthAsToken('0').logoUri} />
|
||||
<Bold>{humanReadableBalance(tx.value, 18)} ETH</Bold>
|
||||
<Bold>{humanReadableValue(tx.value, 18)} ETH</Bold>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section">
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
||||
import { getAppInfoFromUrl, staticAppsList } from '../utils'
|
||||
import { SafeApp, StoredSafeApp } from '../types'
|
||||
|
||||
const APPS_STORAGE_KEY = 'APPS_STORAGE_KEY'
|
||||
|
||||
type onAppToggleHandler = (appId: string, enabled: boolean) => Promise<void>
|
||||
type onAppAddedHandler = (app: SafeApp) => void
|
||||
|
||||
type UseAppListReturnType = {
|
||||
appList: SafeApp[]
|
||||
loadingAppList: boolean
|
||||
onAppToggle: onAppToggleHandler
|
||||
onAppAdded: onAppAddedHandler
|
||||
}
|
||||
|
||||
const useAppList = (): UseAppListReturnType => {
|
||||
const [appList, setAppList] = useState<SafeApp[]>([])
|
||||
const [loadingAppList, setLoadingAppList] = useState<boolean>(true)
|
||||
|
||||
// Load apps list
|
||||
useEffect(() => {
|
||||
const loadApps = async () => {
|
||||
// recover apps from storage:
|
||||
// * third-party apps added by the user
|
||||
// * disabled status for both static and third-party apps
|
||||
const persistedAppList = (await loadFromStorage<StoredSafeApp[]>(APPS_STORAGE_KEY)) || []
|
||||
const list = [...persistedAppList]
|
||||
|
||||
staticAppsList.forEach((staticApp) => {
|
||||
if (!list.some((persistedApp) => persistedApp.url === staticApp.url)) {
|
||||
list.push(staticApp)
|
||||
}
|
||||
})
|
||||
|
||||
const apps = []
|
||||
// using the appURL to recover app info
|
||||
for (let index = 0; index < list.length; index++) {
|
||||
try {
|
||||
const currentApp = list[index]
|
||||
|
||||
const appInfo: any = await getAppInfoFromUrl(currentApp.url)
|
||||
if (appInfo.error) {
|
||||
throw Error(`There was a problem trying to load app ${currentApp.url}`)
|
||||
}
|
||||
|
||||
appInfo.disabled = currentApp.disabled === undefined ? false : currentApp.disabled
|
||||
|
||||
apps.push(appInfo)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
setAppList(apps)
|
||||
setLoadingAppList(false)
|
||||
}
|
||||
|
||||
loadApps()
|
||||
}, [])
|
||||
|
||||
const onAppToggle: onAppToggleHandler = useCallback(
|
||||
async (appId, enabled) => {
|
||||
// update in-memory list
|
||||
const appListCopy = [...appList]
|
||||
|
||||
const app = appListCopy.find((a) => a.id === appId)
|
||||
if (!app) {
|
||||
return
|
||||
}
|
||||
|
||||
app.disabled = !enabled
|
||||
setAppList(appListCopy)
|
||||
|
||||
// update storage list
|
||||
const listToPersist: StoredSafeApp[] = appListCopy.map(({ url, disabled }) => ({ url, disabled }))
|
||||
saveToStorage(APPS_STORAGE_KEY, listToPersist)
|
||||
},
|
||||
[appList],
|
||||
)
|
||||
|
||||
const onAppAdded: onAppAddedHandler = useCallback(
|
||||
(app) => {
|
||||
const newAppList = [
|
||||
{ url: app.url, disabled: false },
|
||||
...appList.map((a) => ({
|
||||
url: a.url,
|
||||
disabled: a.disabled,
|
||||
})),
|
||||
]
|
||||
saveToStorage(APPS_STORAGE_KEY, newAppList)
|
||||
|
||||
setAppList([...appList, { ...app, disabled: false }])
|
||||
},
|
||||
[appList],
|
||||
)
|
||||
|
||||
return {
|
||||
appList,
|
||||
loadingAppList,
|
||||
onAppToggle,
|
||||
onAppAdded,
|
||||
}
|
||||
}
|
||||
|
||||
export { useAppList }
|
|
@ -0,0 +1,52 @@
|
|||
import { useEffect } from 'react'
|
||||
|
||||
const useIframeMessageHandler = (): void => {
|
||||
useEffect(() => {
|
||||
// const handleIframeMessage = (data) => {
|
||||
// if (!data || !data.messageId) {
|
||||
// console.error('ThirdPartyApp: A message was received without message id.')
|
||||
// return
|
||||
// }
|
||||
// switch (data.messageId) {
|
||||
// case operations.SEND_TRANSACTIONS: {
|
||||
// const onConfirm = async () => {
|
||||
// closeModal()
|
||||
// await sendTransactions(dispatch, safeAddress, data.data, enqueueSnackbar, closeSnackbar, selectedApp.id)
|
||||
// }
|
||||
// confirmTransactions(
|
||||
// safeAddress,
|
||||
// safeName,
|
||||
// ethBalance,
|
||||
// selectedApp.name,
|
||||
// selectedApp.iconUrl,
|
||||
// data.data,
|
||||
// openModal,
|
||||
// closeModal,
|
||||
// onConfirm,
|
||||
// )
|
||||
// break
|
||||
// }
|
||||
// default: {
|
||||
// console.error(`ThirdPartyApp: A message was received with an unknown message id ${data.messageId}.`)
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// const onIframeMessage = async ({ data, origin }) => {
|
||||
// if (origin === window.origin) {
|
||||
// return
|
||||
// }
|
||||
// if (!selectedApp.url.includes(origin)) {
|
||||
// console.error(`ThirdPartyApp: A message was received from an unknown origin ${origin}`)
|
||||
// return
|
||||
// }
|
||||
// handleIframeMessage(data)
|
||||
// }
|
||||
// window.addEventListener('message', onIframeMessage)
|
||||
// return () => {
|
||||
// window.removeEventListener('message', onIframeMessage)
|
||||
// }
|
||||
}, [])
|
||||
}
|
||||
|
||||
export { useIframeMessageHandler }
|
|
@ -0,0 +1,29 @@
|
|||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
||||
|
||||
const APPS_LEGAL_CONSENT_RECEIVED = 'APPS_LEGAL_CONSENT_RECEIVED'
|
||||
|
||||
const useLegalConsent = (): { consentReceived: boolean; onConsentReceipt: () => void } => {
|
||||
const [consentReceived, setConsentReceived] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
const checkLegalDisclaimer = async () => {
|
||||
const storedConsentReceived = await loadFromStorage(APPS_LEGAL_CONSENT_RECEIVED)
|
||||
|
||||
if (storedConsentReceived) {
|
||||
setConsentReceived(true)
|
||||
}
|
||||
}
|
||||
|
||||
checkLegalDisclaimer()
|
||||
}, [])
|
||||
|
||||
const onConsentReceipt = useCallback((): void => {
|
||||
setConsentReceived(true)
|
||||
saveToStorage(APPS_LEGAL_CONSENT_RECEIVED, true)
|
||||
}, [])
|
||||
|
||||
return { consentReceived, onConsentReceipt }
|
||||
}
|
||||
|
||||
export { useLegalConsent }
|
|
@ -1,14 +1,16 @@
|
|||
import { Card, FixedDialog, FixedIcon, IconText, Loader, Menu, Text, Title } from '@gnosis.pm/safe-react-components'
|
||||
import { Card, FixedIcon, IconText, Loader, Menu, Title } from '@gnosis.pm/safe-react-components'
|
||||
import { withSnackbar } from 'notistack'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useState, useMemo } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import ManageApps from './ManageApps'
|
||||
import ManageApps from './components/ManageApps'
|
||||
import confirmTransactions from './confirmTransactions'
|
||||
import sendTransactions from './sendTransactions'
|
||||
import { getAppInfoFromUrl, staticAppsList } from './utils'
|
||||
import LegalDisclaimer from './components/LegalDisclaimer'
|
||||
import { useLegalConsent } from './hooks/useLegalConsent'
|
||||
import { useAppList } from './hooks/useAppList'
|
||||
|
||||
import LCL from 'src/components/ListContentLayout'
|
||||
import { networkSelector } from 'src/logic/wallets/store/selectors'
|
||||
|
@ -19,12 +21,7 @@ import {
|
|||
safeNameSelector,
|
||||
safeParamAddressFromStateSelector,
|
||||
} from 'src/routes/safe/store/selectors'
|
||||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
||||
import { isSameHref } from 'src/utils/url'
|
||||
import { SafeApp, StoredSafeApp } from './types'
|
||||
|
||||
const APPS_STORAGE_KEY = 'APPS_STORAGE_KEY'
|
||||
const APPS_LEGAL_DISCLAIMER_STORAGE_KEY = 'APPS_LEGAL_DISCLAIMER_STORAGE_KEY'
|
||||
|
||||
const StyledCard = styled(Card)`
|
||||
margin-bottom: 24px;
|
||||
|
@ -66,11 +63,10 @@ const operations = {
|
|||
}
|
||||
|
||||
function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
|
||||
const [appList, setAppList] = useState<Array<SafeApp>>([])
|
||||
const [legalDisclaimerAccepted, setLegalDisclaimerAccepted] = useState(false)
|
||||
const [selectedApp, setSelectedApp] = useState<string>()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [appIsLoading, setAppIsLoading] = useState(true)
|
||||
const { appList, loadingAppList, onAppToggle, onAppAdded } = useAppList()
|
||||
|
||||
const [appIsLoading, setAppIsLoading] = useState<boolean>(true)
|
||||
const [selectedAppId, setSelectedAppId] = useState<string>()
|
||||
const [iframeEl, setIframeEl] = useState<HTMLIFrameElement | null>(null)
|
||||
|
||||
const history = useHistory()
|
||||
|
@ -80,8 +76,36 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
|
|||
const network = useSelector(networkSelector)
|
||||
const ethBalance = useSelector(safeEthBalanceSelector)
|
||||
const dispatch = useDispatch()
|
||||
const { consentReceived, onConsentReceipt } = useLegalConsent()
|
||||
|
||||
const getSelectedApp = useCallback(() => appList.find((e) => e.id === selectedApp), [appList, selectedApp])
|
||||
const selectedApp = useMemo(() => appList.find((app) => app.id === selectedAppId), [appList, selectedAppId])
|
||||
|
||||
const onSelectApp = useCallback(
|
||||
(appId) => {
|
||||
if (selectedAppId === appId) {
|
||||
return
|
||||
}
|
||||
|
||||
setAppIsLoading(true)
|
||||
setSelectedAppId(appId)
|
||||
},
|
||||
[selectedAppId],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const selectFirstEnabledApp = () => {
|
||||
const firstEnabledApp = appList.find((a) => !a.disabled)
|
||||
if (firstEnabledApp) {
|
||||
setSelectedAppId(firstEnabledApp.id)
|
||||
}
|
||||
}
|
||||
|
||||
const initialSelect = appList.length && !selectedAppId
|
||||
const currentAppWasDisabled = selectedApp?.disabled
|
||||
if (initialSelect || currentAppWasDisabled) {
|
||||
selectFirstEnabledApp()
|
||||
}
|
||||
}, [appList, selectedApp, selectedAppId])
|
||||
|
||||
const iframeRef = useCallback((node) => {
|
||||
if (node !== null) {
|
||||
|
@ -89,58 +113,15 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
|
|||
}
|
||||
}, [])
|
||||
|
||||
const onSelectApp = useCallback(
|
||||
(appId) => {
|
||||
const selectedApp = getSelectedApp()
|
||||
|
||||
if (selectedApp && selectedApp.id === appId) {
|
||||
return
|
||||
}
|
||||
|
||||
setAppIsLoading(true)
|
||||
setSelectedApp(appId)
|
||||
},
|
||||
[getSelectedApp],
|
||||
)
|
||||
|
||||
const redirectToBalance = () => history.push(`${SAFELIST_ADDRESS}/${safeAddress}/balances`)
|
||||
|
||||
const onAcceptLegalDisclaimer = () => {
|
||||
setLegalDisclaimerAccepted(true)
|
||||
saveToStorage(APPS_LEGAL_DISCLAIMER_STORAGE_KEY, true)
|
||||
}
|
||||
|
||||
const getContent = () => {
|
||||
if (!selectedApp) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!legalDisclaimerAccepted) {
|
||||
return (
|
||||
<FixedDialog
|
||||
body={
|
||||
<>
|
||||
<Text size="md">
|
||||
You are now accessing third-party apps, which we do not own, control, maintain or audit. We are not
|
||||
liable for any loss you may suffer in connection with interacting with the apps, which is at your own
|
||||
risk. You must read our Terms, which contain more detailed provisions binding on you relating to the
|
||||
apps.
|
||||
</Text>
|
||||
<br />
|
||||
<Text size="md">
|
||||
I have read and understood the{' '}
|
||||
<a href="https://gnosis-safe.io/terms" rel="noopener noreferrer" target="_blank">
|
||||
Terms
|
||||
</a>{' '}
|
||||
and this Disclaimer, and agree to be bound by them.
|
||||
</Text>
|
||||
</>
|
||||
}
|
||||
onCancel={redirectToBalance}
|
||||
onConfirm={onAcceptLegalDisclaimer}
|
||||
title="Disclaimer"
|
||||
/>
|
||||
)
|
||||
if (!consentReceived) {
|
||||
return <LegalDisclaimer onCancel={redirectToBalance} onConfirm={onConsentReceipt} />
|
||||
}
|
||||
|
||||
if (network === 'UNKNOWN' || !granted) {
|
||||
|
@ -152,8 +133,6 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
|
|||
)
|
||||
}
|
||||
|
||||
const app = getSelectedApp()
|
||||
|
||||
return (
|
||||
<IframeWrapper>
|
||||
{appIsLoading && (
|
||||
|
@ -161,76 +140,26 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
|
|||
<Loader size="md" />
|
||||
</LoadingContainer>
|
||||
)}
|
||||
<StyledIframe frameBorder="0" id={`iframe-${app.name}`} ref={iframeRef} src={app.url} title={app.name} />
|
||||
<StyledIframe
|
||||
frameBorder="0"
|
||||
id={`iframe-${selectedApp.name}`}
|
||||
ref={iframeRef}
|
||||
src={selectedApp.url}
|
||||
title={selectedApp.name}
|
||||
/>
|
||||
</IframeWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const onAppAdded = (app: SafeApp) => {
|
||||
const newAppList = [
|
||||
{ url: app.url, disabled: false },
|
||||
...appList.map((a) => ({
|
||||
url: a.url,
|
||||
disabled: a.disabled,
|
||||
})),
|
||||
]
|
||||
saveToStorage(APPS_STORAGE_KEY, newAppList)
|
||||
|
||||
setAppList([...appList, { ...app, disabled: false }])
|
||||
}
|
||||
|
||||
const selectFirstApp = useCallback(
|
||||
(apps) => {
|
||||
const firstEnabledApp = apps.find((a) => !a.disabled)
|
||||
if (firstEnabledApp) {
|
||||
onSelectApp(firstEnabledApp.id)
|
||||
}
|
||||
},
|
||||
[onSelectApp],
|
||||
)
|
||||
|
||||
const onAppToggle = async (appId, enabled) => {
|
||||
// update in-memory list
|
||||
const copyAppList = [...appList]
|
||||
|
||||
const app = copyAppList.find((a) => a.id === appId)
|
||||
if (!app) {
|
||||
return
|
||||
}
|
||||
|
||||
app.disabled = !enabled
|
||||
setAppList(copyAppList)
|
||||
|
||||
// update storage list
|
||||
const persistedAppList = (await loadFromStorage<StoredSafeApp[]>(APPS_STORAGE_KEY)) || []
|
||||
let storageApp = persistedAppList.find((a) => a.url === app.url)
|
||||
|
||||
if (!storageApp) {
|
||||
storageApp = { url: app.url }
|
||||
storageApp.disabled = !enabled
|
||||
persistedAppList.push(storageApp)
|
||||
} else {
|
||||
storageApp.disabled = !enabled
|
||||
}
|
||||
|
||||
saveToStorage(APPS_STORAGE_KEY, persistedAppList)
|
||||
|
||||
// select app
|
||||
const selectedApp = getSelectedApp()
|
||||
if (!selectedApp || (selectedApp && selectedApp.id === appId)) {
|
||||
setSelectedApp(undefined)
|
||||
selectFirstApp(copyAppList)
|
||||
}
|
||||
}
|
||||
|
||||
const getEnabledApps = () => appList.filter((a) => !a.disabled)
|
||||
const enabledApps = useMemo(() => appList.filter((a) => !a.disabled), [appList])
|
||||
|
||||
const sendMessageToIframe = useCallback(
|
||||
(messageId, data) => {
|
||||
const app = getSelectedApp()
|
||||
iframeEl?.contentWindow.postMessage({ messageId, data }, app.url)
|
||||
if (iframeEl && selectedApp) {
|
||||
iframeEl.contentWindow.postMessage({ messageId, data }, selectedApp.url)
|
||||
}
|
||||
},
|
||||
[getSelectedApp, iframeEl],
|
||||
[iframeEl, selectedApp],
|
||||
)
|
||||
|
||||
// handle messages from iframe
|
||||
|
@ -245,22 +174,16 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
|
|||
case operations.SEND_TRANSACTIONS: {
|
||||
const onConfirm = async () => {
|
||||
closeModal()
|
||||
await sendTransactions(
|
||||
dispatch,
|
||||
safeAddress,
|
||||
data.data,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
getSelectedApp().id,
|
||||
)
|
||||
|
||||
await sendTransactions(dispatch, safeAddress, data.data, enqueueSnackbar, closeSnackbar, selectedApp.id)
|
||||
}
|
||||
|
||||
confirmTransactions(
|
||||
safeAddress,
|
||||
safeName,
|
||||
ethBalance,
|
||||
getSelectedApp().name,
|
||||
getSelectedApp().iconUrl,
|
||||
selectedApp.name,
|
||||
selectedApp.iconUrl,
|
||||
data.data,
|
||||
openModal,
|
||||
closeModal,
|
||||
|
@ -290,8 +213,7 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
|
|||
return
|
||||
}
|
||||
|
||||
const app = getSelectedApp()
|
||||
if (!app.url.includes(origin)) {
|
||||
if (!selectedApp.url.includes(origin)) {
|
||||
console.error(`ThirdPartyApp: A message was received from an unknown origin ${origin}`)
|
||||
return
|
||||
}
|
||||
|
@ -306,65 +228,11 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
|
|||
}
|
||||
})
|
||||
|
||||
// load legalDisclaimer
|
||||
useEffect(() => {
|
||||
const checkLegalDisclaimer = async () => {
|
||||
const legalDisclaimer = await loadFromStorage(APPS_LEGAL_DISCLAIMER_STORAGE_KEY)
|
||||
|
||||
if (legalDisclaimer) {
|
||||
setLegalDisclaimerAccepted(true)
|
||||
}
|
||||
}
|
||||
|
||||
checkLegalDisclaimer()
|
||||
}, [])
|
||||
|
||||
// Load apps list
|
||||
useEffect(() => {
|
||||
const loadApps = async () => {
|
||||
// recover apps from storage:
|
||||
// * third-party apps added by the user
|
||||
// * disabled status for both static and third-party apps
|
||||
const persistedAppList = (await loadFromStorage<StoredSafeApp[]>(APPS_STORAGE_KEY)) || []
|
||||
const list = [...persistedAppList]
|
||||
|
||||
staticAppsList.forEach((staticApp) => {
|
||||
if (!list.some((persistedApp) => persistedApp.url === staticApp.url)) {
|
||||
list.push(staticApp)
|
||||
}
|
||||
})
|
||||
|
||||
const apps = []
|
||||
// using the appURL to recover app info
|
||||
for (let index = 0; index < list.length; index++) {
|
||||
try {
|
||||
const currentApp = list[index]
|
||||
|
||||
const appInfo: SafeApp = await getAppInfoFromUrl(currentApp.url)
|
||||
if (appInfo.error) {
|
||||
throw Error(`There was a problem trying to load app ${currentApp.url}`)
|
||||
}
|
||||
|
||||
appInfo.disabled = currentApp.disabled === undefined ? false : currentApp.disabled
|
||||
|
||||
apps.push(appInfo)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
setAppList(apps)
|
||||
setLoading(false)
|
||||
selectFirstApp(apps)
|
||||
}
|
||||
|
||||
if (!appList.length) {
|
||||
loadApps()
|
||||
}
|
||||
}, [appList.length, selectFirstApp])
|
||||
|
||||
// on iframe change
|
||||
useEffect(() => {
|
||||
const sendMessageToIframe = (messageId, data) => {
|
||||
iframeEl.contentWindow.postMessage({ messageId, data }, selectedApp.url)
|
||||
}
|
||||
const onIframeLoaded = () => {
|
||||
setAppIsLoading(false)
|
||||
sendMessageToIframe(operations.ON_SAFE_INFO, {
|
||||
|
@ -374,8 +242,7 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
|
|||
})
|
||||
}
|
||||
|
||||
const app = getSelectedApp()
|
||||
if (!iframeEl || !selectedApp || !isSameHref(iframeEl.src, app.url)) {
|
||||
if (!iframeEl || !selectedApp || !isSameHref(iframeEl.src, selectedApp.url)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -384,9 +251,9 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
|
|||
return () => {
|
||||
iframeEl.removeEventListener('load', onIframeLoaded)
|
||||
}
|
||||
}, [ethBalance, getSelectedApp, iframeEl, network, safeAddress, selectedApp, sendMessageToIframe])
|
||||
}, [ethBalance, iframeEl, network, safeAddress, selectedApp])
|
||||
|
||||
if (loading || !appList.length) {
|
||||
if (loadingAppList || !appList.length) {
|
||||
return (
|
||||
<LoadingContainer>
|
||||
<Loader size="md" />
|
||||
|
@ -399,26 +266,12 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
|
|||
<Menu>
|
||||
<ManageApps appList={appList} onAppAdded={onAppAdded} onAppToggle={onAppToggle} />
|
||||
</Menu>
|
||||
{getEnabledApps().length ? (
|
||||
{enabledApps.length ? (
|
||||
<LCL.Wrapper>
|
||||
<LCL.Menu>
|
||||
<LCL.List activeItem={selectedApp} items={getEnabledApps()} onItemClick={onSelectApp} />
|
||||
<LCL.List activeItem={selectedAppId} items={enabledApps} onItemClick={onSelectApp} />
|
||||
</LCL.Menu>
|
||||
<LCL.Content>{getContent()}</LCL.Content>
|
||||
{/* <LCL.Footer>
|
||||
{getSelectedApp() && getSelectedApp().providedBy && (
|
||||
<>
|
||||
<p>This App is provided by </p>
|
||||
<ButtonLink
|
||||
onClick={() => window.open(getSelectedApp().providedBy.url, '_blank')}
|
||||
size="lg"
|
||||
testId="manage-tokens-btn"
|
||||
>
|
||||
{selectedApp && getSelectedApp().providedBy.name}
|
||||
</ButtonLink>
|
||||
</>
|
||||
)}
|
||||
</LCL.Footer> */}
|
||||
</LCL.Wrapper>
|
||||
) : (
|
||||
<StyledCard>
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
BALANCE_TABLE_VALUE_ID,
|
||||
generateColumns,
|
||||
getBalanceData,
|
||||
BalanceData,
|
||||
} from 'src/routes/safe/components/Balances/dataFetcher'
|
||||
import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/container/selector'
|
||||
import { Skeleton } from '@material-ui/lab'
|
||||
|
@ -59,7 +60,7 @@ const Coins = (props: Props): React.ReactElement => {
|
|||
const activeTokens = useSelector(extendedSafeTokensSelector)
|
||||
const currencyValues = useSelector(safeFiatBalancesListSelector)
|
||||
const granted = useSelector(grantedSelector)
|
||||
const [filteredData, setFilteredData] = React.useState<BalanceDataRow>(List())
|
||||
const [filteredData, setFilteredData] = React.useState<List<BalanceData>>(List())
|
||||
|
||||
React.useMemo(() => {
|
||||
setFilteredData(getBalanceData(activeTokens, selectedCurrency, currencyValues, currencyRate))
|
||||
|
|
|
@ -5,9 +5,8 @@ import { FIXED } from 'src/components/Table/sorting'
|
|||
import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount'
|
||||
import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers'
|
||||
import { TableColumn } from 'src/components/Table/types'
|
||||
import { AVAILABLE_CURRENCIES, BalanceCurrencyRecord } 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 { BalanceDataRow } from './Coins'
|
||||
|
||||
export const BALANCE_TABLE_ASSET_ID = 'asset'
|
||||
export const BALANCE_TABLE_BALANCE_ID = 'balance'
|
||||
|
@ -15,15 +14,15 @@ export const BALANCE_TABLE_VALUE_ID = 'value'
|
|||
|
||||
const getTokenPriceInCurrency = (
|
||||
token: Token,
|
||||
currencySelected: AVAILABLE_CURRENCIES,
|
||||
currencyValues: List<BalanceCurrencyRecord>,
|
||||
currencyRate: number | null,
|
||||
currencySelected?: AVAILABLE_CURRENCIES,
|
||||
currencyValues?: BalanceCurrencyList,
|
||||
currencyRate?: number,
|
||||
): string => {
|
||||
if (!currencySelected) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const currencyValue = currencyValues.find(({ tokenAddress }) => {
|
||||
const currencyValue = currencyValues?.find(({ tokenAddress }) => {
|
||||
if (token.address === ETH_ADDRESS && !tokenAddress) {
|
||||
return true
|
||||
}
|
||||
|
@ -31,7 +30,7 @@ const getTokenPriceInCurrency = (
|
|||
return token.address === tokenAddress
|
||||
})
|
||||
|
||||
if (!currencyValue) {
|
||||
if (!currencyValue || !currencyRate) {
|
||||
return ''
|
||||
}
|
||||
|
||||
|
@ -41,13 +40,20 @@ const getTokenPriceInCurrency = (
|
|||
return `${formatAmountInUsFormat(balance)} ${currencySelected}`
|
||||
}
|
||||
|
||||
export interface BalanceData {
|
||||
asset: { name: string; logoUri: string; address: string; symbol: string }
|
||||
balance: string
|
||||
fixed: boolean
|
||||
value: string
|
||||
}
|
||||
|
||||
export const getBalanceData = (
|
||||
activeTokens: List<Token>,
|
||||
currencySelected: AVAILABLE_CURRENCIES,
|
||||
currencyValues: List<BalanceCurrencyRecord>,
|
||||
currencyRate: number,
|
||||
): BalanceDataRow => {
|
||||
return activeTokens.map((token) => ({
|
||||
currencySelected?: AVAILABLE_CURRENCIES,
|
||||
currencyValues?: BalanceCurrencyList,
|
||||
currencyRate?: number,
|
||||
): List<BalanceData> =>
|
||||
activeTokens.map((token) => ({
|
||||
[BALANCE_TABLE_ASSET_ID]: {
|
||||
name: token.name,
|
||||
logoUri: token.logoUri,
|
||||
|
@ -60,7 +66,6 @@ export const getBalanceData = (
|
|||
[FIXED]: token.symbol === 'ETH',
|
||||
[BALANCE_TABLE_VALUE_ID]: getTokenPriceInCurrency(token, currencySelected, currencyValues, currencyRate),
|
||||
}))
|
||||
}
|
||||
|
||||
export const generateColumns = (): List<TableColumn> => {
|
||||
const assetColumn: TableColumn = {
|
||||
|
|
|
@ -4,11 +4,22 @@ import EtherScanLink from 'src/components/EtherscanLink'
|
|||
import Identicon from 'src/components/Identicon'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import { useWindowDimensions } from '../../../../container/hooks/useWindowDimensions'
|
||||
import { useWindowDimensions } from 'src/routes/safe/container/hooks/useWindowDimensions'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
const OwnerAddressTableCell = (props) => {
|
||||
const { address, knownAddress, showLinks, userName } = props
|
||||
interface OwnerAddressTableCellProps {
|
||||
address: string
|
||||
knownAddress?: boolean
|
||||
showLinks: boolean
|
||||
userName?: string
|
||||
}
|
||||
|
||||
const OwnerAddressTableCell = ({
|
||||
address,
|
||||
knownAddress,
|
||||
showLinks,
|
||||
userName,
|
||||
}: OwnerAddressTableCellProps): React.ReactElement => {
|
||||
const [cut, setCut] = useState(undefined)
|
||||
const { width } = useWindowDimensions()
|
||||
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
import { IconText, Text } from '@gnosis.pm/safe-react-components'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { styles } from './styles'
|
||||
import Value from './Value'
|
||||
|
||||
import Block from 'src/components/layout/Block'
|
||||
import {
|
||||
extractMultiSendDecodedData,
|
||||
MultiSendDetails,
|
||||
} from 'src/routes/safe/store/actions/transactions/utils/multiSendDecodedDetails'
|
||||
import Bold from 'src/components/layout/Bold'
|
||||
import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell'
|
||||
import EtherscanLink from 'src/components/EtherscanLink'
|
||||
import { humanReadableValue } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import Collapse from 'src/components/Collapse'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { getNameFromAddressBook } from 'src/logic/addressBook/store/selectors'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import LinkWithRef from 'src/components/layout/Link'
|
||||
import { shortVersionOf } from 'src/logic/wallets/ethAddresses'
|
||||
import { Transaction } from 'src/routes/safe/store/models/types/transaction'
|
||||
|
||||
export const TRANSACTIONS_DESC_CUSTOM_VALUE_TEST_ID = 'tx-description-custom-value'
|
||||
export const TRANSACTIONS_DESC_CUSTOM_DATA_TEST_ID = 'tx-description-custom-data'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const TxDetailsMethodName = styled(Text)`
|
||||
text-indent: 4px;
|
||||
`
|
||||
const TxDetailsMethodParam = styled.div`
|
||||
text-indent: 8px;
|
||||
`
|
||||
const InlineText = styled(Text)`
|
||||
display: inline-flex;
|
||||
`
|
||||
const TxDetailsContent = styled.div`
|
||||
padding: 8px 8px 8px 16px;
|
||||
`
|
||||
|
||||
const TxInfo = styled.div`
|
||||
padding: 8px 8px 8px 16px;
|
||||
`
|
||||
|
||||
const MultiSendCustomData = ({ tx, order }: { tx: MultiSendDetails; order: number }): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const methodName = tx.data?.method ? ` (${tx.data.method})` : ''
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapse
|
||||
collapseClassName={classes.collapse}
|
||||
headerWrapperClassName={classes.collapseHeaderWrapper}
|
||||
title={<IconText iconSize="sm" iconType="code" text={`Action ${order + 1}${methodName}`} textSize="lg" />}
|
||||
>
|
||||
<TxDetailsContent>
|
||||
<TxInfo>
|
||||
<Bold>Send {humanReadableValue(tx.value)} ETH to:</Bold>
|
||||
<OwnerAddressTableCell address={tx.to} showLinks />
|
||||
</TxInfo>
|
||||
{tx.data && (
|
||||
<TxInfo>
|
||||
<TxDetailsMethodName size="lg">
|
||||
<strong>{tx.data.method}</strong>
|
||||
</TxDetailsMethodName>
|
||||
{tx.data?.parameters.map((param, index) => (
|
||||
<TxDetailsMethodParam key={`${tx.operation}_${tx.to}_${tx.data.method}_param-${index}`}>
|
||||
<InlineText size="lg">
|
||||
<strong>
|
||||
{param.name}({param.type}):
|
||||
</strong>
|
||||
</InlineText>
|
||||
<Value method={methodName} type={param.type} value={param.value} />
|
||||
</TxDetailsMethodParam>
|
||||
))}
|
||||
</TxInfo>
|
||||
)}
|
||||
</TxDetailsContent>
|
||||
</Collapse>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const TxData = ({ data }: { data: string }): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [showTxData, setShowTxData] = React.useState(false)
|
||||
const showExpandBtn = data.length > 20
|
||||
|
||||
return (
|
||||
<Paragraph className={classes.txDataParagraph} noMargin size="md">
|
||||
{showExpandBtn ? (
|
||||
<>
|
||||
{showTxData ? (
|
||||
<>
|
||||
{data}{' '}
|
||||
<LinkWithRef
|
||||
aria-label="Hide details of the transaction"
|
||||
className={classes.linkTxData}
|
||||
onClick={() => setShowTxData(false)}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Show Less
|
||||
</LinkWithRef>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{shortVersionOf(data, 20)}{' '}
|
||||
<LinkWithRef
|
||||
aria-label="Show details of the transaction"
|
||||
className={classes.linkTxData}
|
||||
onClick={() => setShowTxData(true)}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Show More
|
||||
</LinkWithRef>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
data
|
||||
)}
|
||||
</Paragraph>
|
||||
)
|
||||
}
|
||||
|
||||
interface GenericCustomDataProps {
|
||||
amount?: string
|
||||
data: string
|
||||
recipient: string
|
||||
}
|
||||
|
||||
const GenericCustomData = ({ amount = '0', data, recipient }: GenericCustomDataProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const recipientName = useSelector((state) => getNameFromAddressBook(state, recipient))
|
||||
|
||||
return (
|
||||
<Block>
|
||||
<Block data-testid={TRANSACTIONS_DESC_CUSTOM_VALUE_TEST_ID}>
|
||||
<Bold>Send {amount} to:</Bold>
|
||||
{recipientName ? (
|
||||
<OwnerAddressTableCell address={recipient} knownAddress showLinks userName={recipientName} />
|
||||
) : (
|
||||
<EtherscanLink knownAddress={false} type="address" value={recipient} />
|
||||
)}
|
||||
</Block>
|
||||
<Block className={classes.txData} data-testid={TRANSACTIONS_DESC_CUSTOM_DATA_TEST_ID}>
|
||||
<Bold>Data (hex encoded):</Bold>
|
||||
<TxData data={data} />
|
||||
</Block>
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
||||
interface CustomDescriptionProps {
|
||||
amount?: string
|
||||
data: string
|
||||
recipient: string
|
||||
storedTx: Transaction
|
||||
}
|
||||
|
||||
const CustomDescription = ({ amount, data, recipient, storedTx }: CustomDescriptionProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
||||
return storedTx.multiSendTx ? (
|
||||
<Block className={classes.multiSendTxData} data-testid={TRANSACTIONS_DESC_CUSTOM_DATA_TEST_ID}>
|
||||
{extractMultiSendDecodedData(storedTx).txDetails?.map((tx, index) => (
|
||||
<MultiSendCustomData key={`${tx.to}-row-${index}`} tx={tx} order={index} />
|
||||
))}
|
||||
</Block>
|
||||
) : (
|
||||
<GenericCustomData amount={amount} data={data} recipient={recipient} />
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomDescription
|
|
@ -0,0 +1,153 @@
|
|||
import { useSelector } from 'react-redux'
|
||||
import React from 'react'
|
||||
|
||||
import { getNameFromAddressBook } from 'src/logic/addressBook/store/selectors'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Bold from 'src/components/layout/Bold'
|
||||
import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell'
|
||||
import EtherscanLink from 'src/components/EtherscanLink'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import { SAFE_METHODS_NAMES, SafeMethods } from 'src/routes/safe/store/models/types/transactions.d'
|
||||
|
||||
export const TRANSACTIONS_DESC_ADD_OWNER_TEST_ID = 'tx-description-add-owner'
|
||||
export const TRANSACTIONS_DESC_REMOVE_OWNER_TEST_ID = 'tx-description-remove-owner'
|
||||
export const TRANSACTIONS_DESC_CHANGE_THRESHOLD_TEST_ID = 'tx-description-change-threshold'
|
||||
export const TRANSACTIONS_DESC_ADD_MODULE_TEST_ID = 'tx-description-add-module'
|
||||
export const TRANSACTIONS_DESC_REMOVE_MODULE_TEST_ID = 'tx-description-remove-module'
|
||||
export const TRANSACTIONS_DESC_NO_DATA = 'tx-description-no-data'
|
||||
|
||||
interface RemovedOwnerProps {
|
||||
removedOwner: string
|
||||
}
|
||||
|
||||
const RemovedOwner = ({ removedOwner }: RemovedOwnerProps): React.ReactElement => {
|
||||
const ownerChangedName = useSelector((state) => getNameFromAddressBook(state, removedOwner))
|
||||
|
||||
return (
|
||||
<Block data-testid={TRANSACTIONS_DESC_REMOVE_OWNER_TEST_ID}>
|
||||
<Bold>Remove owner:</Bold>
|
||||
{ownerChangedName ? (
|
||||
<OwnerAddressTableCell address={removedOwner} knownAddress showLinks userName={ownerChangedName} />
|
||||
) : (
|
||||
<EtherscanLink knownAddress={false} type="address" value={removedOwner} />
|
||||
)}
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
||||
interface AddedOwnerProps {
|
||||
addedOwner: string
|
||||
}
|
||||
|
||||
const AddedOwner = ({ addedOwner }: AddedOwnerProps): React.ReactElement => {
|
||||
const ownerChangedName = useSelector((state) => getNameFromAddressBook(state, addedOwner))
|
||||
|
||||
return (
|
||||
<Block data-testid={TRANSACTIONS_DESC_ADD_OWNER_TEST_ID}>
|
||||
<Bold>Add owner:</Bold>
|
||||
{ownerChangedName ? (
|
||||
<OwnerAddressTableCell address={addedOwner} knownAddress showLinks userName={ownerChangedName} />
|
||||
) : (
|
||||
<EtherscanLink knownAddress={false} type="address" value={addedOwner} />
|
||||
)}
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
||||
interface NewThresholdProps {
|
||||
newThreshold: string
|
||||
}
|
||||
|
||||
const NewThreshold = ({ newThreshold }: NewThresholdProps): React.ReactElement => (
|
||||
<Block data-testid={TRANSACTIONS_DESC_CHANGE_THRESHOLD_TEST_ID}>
|
||||
<Bold>Change required confirmations:</Bold>
|
||||
<Paragraph noMargin size="md">
|
||||
{newThreshold}
|
||||
</Paragraph>
|
||||
</Block>
|
||||
)
|
||||
|
||||
interface AddModuleProps {
|
||||
module: string
|
||||
}
|
||||
|
||||
const AddModule = ({ module }: AddModuleProps): React.ReactElement => (
|
||||
<Block data-testid={TRANSACTIONS_DESC_ADD_MODULE_TEST_ID}>
|
||||
<Bold>Add module:</Bold>
|
||||
<EtherscanLink value={module} knownAddress={false} type="address" />
|
||||
</Block>
|
||||
)
|
||||
|
||||
interface RemoveModuleProps {
|
||||
module: string
|
||||
}
|
||||
|
||||
const RemoveModule = ({ module }: RemoveModuleProps): React.ReactElement => (
|
||||
<Block data-testid={TRANSACTIONS_DESC_REMOVE_MODULE_TEST_ID}>
|
||||
<Bold>Remove module:</Bold>
|
||||
<EtherscanLink value={module} knownAddress={false} type="address" />
|
||||
</Block>
|
||||
)
|
||||
|
||||
interface SettingsDescriptionProps {
|
||||
action: SafeMethods
|
||||
addedOwner?: string
|
||||
newThreshold?: string
|
||||
removedOwner?: string
|
||||
module?: string
|
||||
}
|
||||
|
||||
const SettingsDescription = ({
|
||||
action,
|
||||
addedOwner,
|
||||
newThreshold,
|
||||
removedOwner,
|
||||
module,
|
||||
}: SettingsDescriptionProps): React.ReactElement => {
|
||||
if (action === SAFE_METHODS_NAMES.REMOVE_OWNER && removedOwner && newThreshold) {
|
||||
return (
|
||||
<>
|
||||
<RemovedOwner removedOwner={removedOwner} />
|
||||
<NewThreshold newThreshold={newThreshold} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (action === SAFE_METHODS_NAMES.CHANGE_THRESHOLD && newThreshold) {
|
||||
return <NewThreshold newThreshold={newThreshold} />
|
||||
}
|
||||
|
||||
if (action === SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD && addedOwner && newThreshold) {
|
||||
return (
|
||||
<>
|
||||
<AddedOwner addedOwner={addedOwner} />
|
||||
<NewThreshold newThreshold={newThreshold} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (action === SAFE_METHODS_NAMES.SWAP_OWNER && removedOwner && addedOwner) {
|
||||
return (
|
||||
<>
|
||||
<RemovedOwner removedOwner={removedOwner} />
|
||||
<AddedOwner addedOwner={addedOwner} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (action === SAFE_METHODS_NAMES.ENABLE_MODULE && module) {
|
||||
return <AddModule module={module} />
|
||||
}
|
||||
|
||||
if (action === SAFE_METHODS_NAMES.DISABLE_MODULE && module) {
|
||||
return <RemoveModule module={module} />
|
||||
}
|
||||
|
||||
return (
|
||||
<Block data-testid={TRANSACTIONS_DESC_NO_DATA}>
|
||||
<Bold>No data available for current transaction</Bold>
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsDescription
|
|
@ -0,0 +1,30 @@
|
|||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { TRANSACTIONS_DESC_SEND_TEST_ID } from './index'
|
||||
import { getNameFromAddressBook } from 'src/logic/addressBook/store/selectors'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Bold from 'src/components/layout/Bold'
|
||||
import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell'
|
||||
import EtherscanLink from 'src/components/EtherscanLink'
|
||||
|
||||
interface TransferDescriptionProps {
|
||||
amount: string
|
||||
recipient: string
|
||||
}
|
||||
|
||||
const TransferDescription = ({ amount = '', recipient }: TransferDescriptionProps): React.ReactElement => {
|
||||
const recipientName = useSelector((state) => getNameFromAddressBook(state, recipient))
|
||||
return (
|
||||
<Block data-testid={TRANSACTIONS_DESC_SEND_TEST_ID}>
|
||||
<Bold>Send {amount} to:</Bold>
|
||||
{recipientName ? (
|
||||
<OwnerAddressTableCell address={recipient} knownAddress showLinks userName={recipientName} />
|
||||
) : (
|
||||
<EtherscanLink knownAddress={false} type="address" value={recipient} />
|
||||
)}
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
||||
export default TransferDescription
|
|
@ -0,0 +1,83 @@
|
|||
import { Text } from '@gnosis.pm/safe-react-components'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { styles } from './styles'
|
||||
|
||||
import {
|
||||
isAddress,
|
||||
isArrayParameter,
|
||||
} from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
|
||||
import { useWindowDimensions } from 'src/routes/safe/container/hooks/useWindowDimensions'
|
||||
import SafeEtherscanLink from 'src/components/EtherscanLink'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const InlineText = styled(Text)`
|
||||
display: inline-flex;
|
||||
`
|
||||
|
||||
const NestedWrapper = styled.div`
|
||||
text-indent: 24px;
|
||||
`
|
||||
|
||||
interface RenderValueProps {
|
||||
method: string
|
||||
type: string
|
||||
value: string | string[]
|
||||
}
|
||||
|
||||
const EtherscanLink = ({ method, type, value }: RenderValueProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [cut, setCut] = React.useState(undefined)
|
||||
const { width } = useWindowDimensions()
|
||||
|
||||
React.useEffect(() => {
|
||||
if (width <= 900) {
|
||||
setCut(4)
|
||||
} else if (width <= 1024) {
|
||||
setCut(8)
|
||||
} else {
|
||||
setCut(12)
|
||||
}
|
||||
}, [width])
|
||||
|
||||
if (isArrayParameter(type)) {
|
||||
return (
|
||||
<NestedWrapper>
|
||||
{(value as string[]).map((value, index) => (
|
||||
<SafeEtherscanLink key={`${method}-value-${index}`} cut={cut} value={value} />
|
||||
))}
|
||||
</NestedWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
return <SafeEtherscanLink className={classes.address} cut={cut} value={value as string} />
|
||||
}
|
||||
|
||||
const GenericValue = ({ method, type, value }: RenderValueProps): React.ReactElement => {
|
||||
if (isArrayParameter(type)) {
|
||||
return (
|
||||
<NestedWrapper>
|
||||
{(value as string[]).map((value, index) => (
|
||||
<Text key={`${method}-value-${index}`} size="lg">
|
||||
{value}
|
||||
</Text>
|
||||
))}
|
||||
</NestedWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
return <InlineText size="lg">{value as string}</InlineText>
|
||||
}
|
||||
|
||||
const Value = ({ type, ...props }: RenderValueProps): React.ReactElement => {
|
||||
if (isAddress(type)) {
|
||||
return <EtherscanLink type={type} {...props} />
|
||||
}
|
||||
|
||||
return <GenericValue type={type} {...props} />
|
||||
}
|
||||
|
||||
export default Value
|
|
@ -1,267 +1,22 @@
|
|||
import { withStyles } from '@material-ui/core/styles'
|
||||
import React, { useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
|
||||
import { styles } from './styles'
|
||||
import { getTxData } from './utils'
|
||||
import SettingsDescription from './SettingsDescription'
|
||||
import CustomDescription from './CustomDescription'
|
||||
import TransferDescription from './TransferDescription'
|
||||
|
||||
import EtherscanLink from 'src/components/EtherscanLink'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Bold from 'src/components/layout/Bold'
|
||||
import LinkWithRef from 'src/components/layout/Link'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import { getNameFromAddressBook } from 'src/logic/addressBook/store/selectors'
|
||||
import { SAFE_METHODS_NAMES, SafeMethods } from 'src/logic/contracts/methodIds'
|
||||
import { shortVersionOf } from 'src/logic/wallets/ethAddresses'
|
||||
import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell'
|
||||
import { getTxAmount } from 'src/routes/safe/components/Transactions/TxsTable/columns'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import { Transaction } from 'src/routes/safe/store/models/types/transaction'
|
||||
|
||||
import { lg, md } from 'src/theme/variables'
|
||||
|
||||
export const TRANSACTIONS_DESC_ADD_OWNER_TEST_ID = 'tx-description-add-owner'
|
||||
export const TRANSACTIONS_DESC_REMOVE_OWNER_TEST_ID = 'tx-description-remove-owner'
|
||||
export const TRANSACTIONS_DESC_CHANGE_THRESHOLD_TEST_ID = 'tx-description-change-threshold'
|
||||
export const TRANSACTIONS_DESC_SEND_TEST_ID = 'tx-description-send'
|
||||
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_ADD_MODULE_TEST_ID = 'tx-description-add-module'
|
||||
export const TRANSACTIONS_DESC_REMOVE_MODULE_TEST_ID = 'tx-description-remove-module'
|
||||
export const TRANSACTIONS_DESC_NO_DATA = 'tx-description-no-data'
|
||||
|
||||
export const styles = () => ({
|
||||
txDataContainer: {
|
||||
paddingTop: lg,
|
||||
paddingLeft: md,
|
||||
paddingBottom: md,
|
||||
},
|
||||
txData: {
|
||||
wordBreak: 'break-all',
|
||||
},
|
||||
txDataParagraph: {
|
||||
whiteSpace: 'normal',
|
||||
},
|
||||
linkTxData: {
|
||||
textDecoration: 'underline',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
})
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
interface TransferDescriptionProps {
|
||||
amount: string
|
||||
recipient: string
|
||||
}
|
||||
|
||||
const TransferDescription = ({ amount = '', recipient }: TransferDescriptionProps): React.ReactElement => {
|
||||
const recipientName = useSelector((state) => getNameFromAddressBook(state, recipient))
|
||||
return (
|
||||
<Block data-testid={TRANSACTIONS_DESC_SEND_TEST_ID}>
|
||||
<Bold>Send {amount} to:</Bold>
|
||||
{recipientName ? (
|
||||
<OwnerAddressTableCell address={recipient} knownAddress showLinks userName={recipientName} />
|
||||
) : (
|
||||
<EtherscanLink knownAddress={false} type="address" value={recipient} />
|
||||
)}
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
||||
interface RemovedOwnerProps {
|
||||
removedOwner: string
|
||||
}
|
||||
|
||||
const RemovedOwner = ({ removedOwner }: RemovedOwnerProps): React.ReactElement => {
|
||||
const ownerChangedName = useSelector((state) => getNameFromAddressBook(state, removedOwner))
|
||||
|
||||
return (
|
||||
<Block data-testid={TRANSACTIONS_DESC_REMOVE_OWNER_TEST_ID}>
|
||||
<Bold>Remove owner:</Bold>
|
||||
{ownerChangedName ? (
|
||||
<OwnerAddressTableCell address={removedOwner} knownAddress showLinks userName={ownerChangedName} />
|
||||
) : (
|
||||
<EtherscanLink knownAddress={false} type="address" value={removedOwner} />
|
||||
)}
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
||||
interface AddedOwnerProps {
|
||||
addedOwner: string
|
||||
}
|
||||
|
||||
const AddedOwner = ({ addedOwner }: AddedOwnerProps): React.ReactElement => {
|
||||
const ownerChangedName = useSelector((state) => getNameFromAddressBook(state, addedOwner))
|
||||
|
||||
return (
|
||||
<Block data-testid={TRANSACTIONS_DESC_ADD_OWNER_TEST_ID}>
|
||||
<Bold>Add owner:</Bold>
|
||||
{ownerChangedName ? (
|
||||
<OwnerAddressTableCell address={addedOwner} knownAddress showLinks userName={ownerChangedName} />
|
||||
) : (
|
||||
<EtherscanLink knownAddress={false} type="address" value={addedOwner} />
|
||||
)}
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
||||
interface NewThresholdProps {
|
||||
newThreshold: string
|
||||
}
|
||||
|
||||
const NewThreshold = ({ newThreshold }: NewThresholdProps): React.ReactElement => (
|
||||
<Block data-testid={TRANSACTIONS_DESC_CHANGE_THRESHOLD_TEST_ID}>
|
||||
<Bold>Change required confirmations:</Bold>
|
||||
<Paragraph noMargin size="md">
|
||||
{newThreshold}
|
||||
</Paragraph>
|
||||
</Block>
|
||||
)
|
||||
|
||||
interface AddModuleProps {
|
||||
module: string
|
||||
}
|
||||
|
||||
const AddModule = ({ module }: AddModuleProps): React.ReactElement => (
|
||||
<Block data-testid={TRANSACTIONS_DESC_ADD_MODULE_TEST_ID}>
|
||||
<Bold>Add module:</Bold>
|
||||
<EtherscanLink value={module} knownAddress={false} type="address" />
|
||||
</Block>
|
||||
)
|
||||
|
||||
interface RemoveModuleProps {
|
||||
module: string
|
||||
}
|
||||
|
||||
const RemoveModule = ({ module }: RemoveModuleProps): React.ReactElement => (
|
||||
<Block data-testid={TRANSACTIONS_DESC_REMOVE_MODULE_TEST_ID}>
|
||||
<Bold>Remove module:</Bold>
|
||||
<EtherscanLink value={module} knownAddress={false} type="address" />
|
||||
</Block>
|
||||
)
|
||||
|
||||
interface SettingsDescriptionProps {
|
||||
action: SafeMethods
|
||||
addedOwner?: string
|
||||
newThreshold?: string
|
||||
removedOwner?: string
|
||||
module?: string
|
||||
}
|
||||
|
||||
const SettingsDescription = ({
|
||||
action,
|
||||
addedOwner,
|
||||
newThreshold,
|
||||
removedOwner,
|
||||
module,
|
||||
}: SettingsDescriptionProps): React.ReactElement => {
|
||||
if (action === SAFE_METHODS_NAMES.REMOVE_OWNER && removedOwner && newThreshold) {
|
||||
return (
|
||||
<>
|
||||
<RemovedOwner removedOwner={removedOwner} />
|
||||
<NewThreshold newThreshold={newThreshold} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (action === SAFE_METHODS_NAMES.CHANGE_THRESHOLD && newThreshold) {
|
||||
return <NewThreshold newThreshold={newThreshold} />
|
||||
}
|
||||
|
||||
if (action === SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD && addedOwner && newThreshold) {
|
||||
return (
|
||||
<>
|
||||
<AddedOwner addedOwner={addedOwner} />
|
||||
<NewThreshold newThreshold={newThreshold} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (action === SAFE_METHODS_NAMES.SWAP_OWNER && removedOwner && addedOwner) {
|
||||
return (
|
||||
<>
|
||||
<RemovedOwner removedOwner={removedOwner} />
|
||||
<AddedOwner addedOwner={addedOwner} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (action === SAFE_METHODS_NAMES.ENABLE_MODULE && module) {
|
||||
return <AddModule module={module} />
|
||||
}
|
||||
|
||||
if (action === SAFE_METHODS_NAMES.DISABLE_MODULE && module) {
|
||||
return <RemoveModule module={module} />
|
||||
}
|
||||
|
||||
return (
|
||||
<Block data-testid={TRANSACTIONS_DESC_NO_DATA}>
|
||||
<Bold>No data available for current transaction</Bold>
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
||||
const TxData = (props) => {
|
||||
const { classes, data } = props
|
||||
const [showTxData, setShowTxData] = useState(false)
|
||||
const showExpandBtn = data.length > 20
|
||||
return (
|
||||
<Paragraph className={classes.txDataParagraph} noMargin size="md">
|
||||
{showExpandBtn ? (
|
||||
<>
|
||||
{showTxData ? (
|
||||
<>
|
||||
{data}{' '}
|
||||
<LinkWithRef
|
||||
aria-label="Hide details of the transaction"
|
||||
className={classes.linkTxData}
|
||||
onClick={() => setShowTxData(false)}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Show Less
|
||||
</LinkWithRef>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{shortVersionOf(data, 20)}{' '}
|
||||
<LinkWithRef
|
||||
aria-label="Show details of the transaction"
|
||||
className={classes.linkTxData}
|
||||
onClick={() => setShowTxData(true)}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Show More
|
||||
</LinkWithRef>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
data
|
||||
)}
|
||||
</Paragraph>
|
||||
)
|
||||
}
|
||||
|
||||
const CustomDescription = ({ amount = 0, classes, data, recipient }: any) => {
|
||||
const recipientName = useSelector((state) => getNameFromAddressBook(state, recipient))
|
||||
return (
|
||||
<>
|
||||
<Block data-testid={TRANSACTIONS_DESC_CUSTOM_VALUE_TEST_ID}>
|
||||
<Bold>Send {amount} to:</Bold>
|
||||
{recipientName ? (
|
||||
<OwnerAddressTableCell address={recipient} knownAddress showLinks userName={recipientName} />
|
||||
) : (
|
||||
<EtherscanLink knownAddress={false} type="address" value={recipient} />
|
||||
)}
|
||||
</Block>
|
||||
<Block className={classes.txData} data-testid={TRANSACTIONS_DESC_CUSTOM_DATA_TEST_ID}>
|
||||
<Bold>Data (hex encoded):</Bold>
|
||||
<TxData classes={classes} data={data} />
|
||||
</Block>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const TxDescription = ({ classes, tx }) => {
|
||||
const TxDescription = ({ tx }: { tx: Transaction }): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const {
|
||||
action,
|
||||
addedOwner,
|
||||
|
@ -288,9 +43,7 @@ const TxDescription = ({ classes, tx }) => {
|
|||
module={module}
|
||||
/>
|
||||
)}
|
||||
{!upgradeTx && customTx && (
|
||||
<CustomDescription amount={amount} classes={classes} data={data} recipient={recipient} />
|
||||
)}
|
||||
{!upgradeTx && customTx && <CustomDescription amount={amount} data={data} recipient={recipient} storedTx={tx} />}
|
||||
{upgradeTx && <div>{data}</div>}
|
||||
{!cancellationTx && !modifySettingsTx && !customTx && !creationTx && !upgradeTx && (
|
||||
<TransferDescription amount={amount} recipient={recipient} />
|
||||
|
@ -299,4 +52,4 @@ const TxDescription = ({ classes, tx }) => {
|
|||
)
|
||||
}
|
||||
|
||||
export default withStyles(styles as any)(TxDescription)
|
||||
export default TxDescription
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import { createStyles } from '@material-ui/core/styles'
|
||||
import { lg, md } from 'src/theme/variables'
|
||||
|
||||
export const styles = createStyles({
|
||||
txDataContainer: {
|
||||
paddingTop: lg,
|
||||
paddingLeft: md,
|
||||
paddingBottom: md,
|
||||
},
|
||||
txData: {
|
||||
wordBreak: 'break-all',
|
||||
},
|
||||
txDataParagraph: {
|
||||
whiteSpace: 'normal',
|
||||
},
|
||||
linkTxData: {
|
||||
textDecoration: 'underline',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
multiSendTxData: {
|
||||
marginTop: `-${lg}`,
|
||||
marginLeft: `-${md}`,
|
||||
},
|
||||
collapse: {
|
||||
borderBottom: `2px solid rgb(232, 231, 230)`,
|
||||
},
|
||||
collapseHeaderWrapper: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignContent: 'center',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '8px 8px 8px 16px',
|
||||
borderBottom: '2px solid rgb(232, 231, 230)',
|
||||
|
||||
'&:hover': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
address: {
|
||||
display: 'inline-flex',
|
||||
},
|
||||
})
|
|
@ -1,4 +1,5 @@
|
|||
import { SAFE_METHODS_NAMES } from 'src/logic/contracts/methodIds'
|
||||
import { Transaction } from 'src/routes/safe/store/models/types/transaction'
|
||||
import { SAFE_METHODS_NAMES } from 'src/routes/safe/store/models/types/transactions.d'
|
||||
|
||||
const getSafeVersion = (data) => {
|
||||
const contractAddress = data.substr(340, 40).toLowerCase()
|
||||
|
@ -10,8 +11,26 @@ const getSafeVersion = (data) => {
|
|||
)
|
||||
}
|
||||
|
||||
export const getTxData = (tx) => {
|
||||
const txData: any = {}
|
||||
interface TxData {
|
||||
data?: string
|
||||
recipient?: string
|
||||
module?: string
|
||||
action?: string
|
||||
addedOwner?: string
|
||||
removedOwner?: string
|
||||
newThreshold?: string
|
||||
tokenId?: string
|
||||
isTokenTransfer?: boolean
|
||||
isCollectibleTransfer?: boolean
|
||||
modifySettingsTx?: boolean
|
||||
customTx?: boolean
|
||||
cancellationTx?: boolean
|
||||
creationTx?: boolean
|
||||
upgradeTx?: boolean
|
||||
}
|
||||
|
||||
export const getTxData = (tx: Transaction): TxData => {
|
||||
const txData: TxData = {}
|
||||
|
||||
if (tx.decodedParams) {
|
||||
if (tx.isTokenTransfer) {
|
||||
|
@ -62,6 +81,8 @@ export const getTxData = (tx) => {
|
|||
txData.customTx = true
|
||||
} else {
|
||||
txData.recipient = tx.recipient
|
||||
txData.data = tx.data
|
||||
txData.customTx = true
|
||||
}
|
||||
} else if (tx.customTx) {
|
||||
txData.recipient = tx.recipient
|
||||
|
|
|
@ -8,9 +8,11 @@ import ApproveTxModal from './ApproveTxModal'
|
|||
import OwnersColumn from './OwnersColumn'
|
||||
import RejectTxModal from './RejectTxModal'
|
||||
import TxDescription from './TxDescription'
|
||||
import { IncomingTx } from './IncomingTx'
|
||||
import { CreationTx } from './CreationTx'
|
||||
import { OutgoingTx } from './OutgoingTx'
|
||||
import { styles } from './style'
|
||||
|
||||
import { Transaction } from 'src/routes/safe/store/models/types/transaction'
|
||||
import { getNetwork } from 'src/config'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Bold from 'src/components/layout/Bold'
|
||||
|
@ -21,21 +23,17 @@ import Row from 'src/components/layout/Row'
|
|||
import Span from 'src/components/layout/Span'
|
||||
import IncomingTxDescription from 'src/routes/safe/components/Transactions/TxsTable/ExpandedTx/IncomingTxDescription'
|
||||
import { INCOMING_TX_TYPES } from 'src/routes/safe/store/models/incomingTransaction'
|
||||
|
||||
import { safeNonceSelector, safeThresholdSelector } from 'src/routes/safe/store/selectors'
|
||||
import { IncomingTx } from './IncomingTx'
|
||||
import { CreationTx } from './CreationTx'
|
||||
import { OutgoingTx } from './OutgoingTx'
|
||||
import { TransactionTypes } from 'src/routes/safe/store/models/types/transaction'
|
||||
import { Transaction, TransactionTypes } from 'src/routes/safe/store/models/types/transaction'
|
||||
|
||||
const useStyles = makeStyles(styles as any)
|
||||
|
||||
type Props = {
|
||||
interface ExpandedTxProps {
|
||||
cancelTx: Transaction
|
||||
tx: Transaction
|
||||
}
|
||||
|
||||
const ExpandedTx = ({ cancelTx, tx }: Props): React.ReactElement => {
|
||||
const ExpandedTx = ({ cancelTx, tx }: ExpandedTxProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const nonce = useSelector(safeNonceSelector)
|
||||
const threshold = useSelector(safeThresholdSelector)
|
||||
|
|
|
@ -8,8 +8,11 @@ import React from 'react'
|
|||
import TxType from './TxType'
|
||||
|
||||
import { buildOrderFieldFrom } from 'src/components/Table/sorting'
|
||||
import { TableColumn } from 'src/components/Table/types'
|
||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
||||
import { INCOMING_TX_TYPES } from 'src/routes/safe/store/models/incomingTransaction'
|
||||
import { Transaction } from 'src/routes/safe/store/models/types/transaction'
|
||||
import { CancellationTransactions } from 'src/routes/safe/store/reducer/cancellationTransactions'
|
||||
|
||||
export const TX_TABLE_ID = 'id'
|
||||
export const TX_TABLE_TYPE_ID = 'type'
|
||||
|
@ -20,11 +23,20 @@ export const TX_TABLE_RAW_TX_ID = 'tx'
|
|||
export const TX_TABLE_RAW_CANCEL_TX_ID = 'cancelTx'
|
||||
export const TX_TABLE_EXPAND_ICON = 'expand'
|
||||
|
||||
export const formatDate = (date) => format(parseISO(date), 'MMM d, yyyy - HH:mm:ss')
|
||||
export const formatDate = (date: string): string => format(parseISO(date), 'MMM d, yyyy - HH:mm:ss')
|
||||
|
||||
const NOT_AVAILABLE = 'n/a'
|
||||
|
||||
const getAmountWithSymbol = ({ decimals = 0, symbol = NOT_AVAILABLE, value }, formatted = false) => {
|
||||
interface AmountData {
|
||||
decimals?: number | string
|
||||
symbol?: string
|
||||
value: number | string
|
||||
}
|
||||
|
||||
const getAmountWithSymbol = (
|
||||
{ decimals = 0, symbol = NOT_AVAILABLE, value }: AmountData,
|
||||
formatted = false,
|
||||
): string => {
|
||||
const nonFormattedValue = new BigNumber(value).times(`1e-${decimals}`).toFixed()
|
||||
const finalValue = formatted ? formatAmount(nonFormattedValue).toString() : nonFormattedValue
|
||||
const txAmount = finalValue === 'NaN' ? NOT_AVAILABLE : finalValue
|
||||
|
@ -32,7 +44,7 @@ const getAmountWithSymbol = ({ decimals = 0, symbol = NOT_AVAILABLE, value }, fo
|
|||
return `${txAmount} ${symbol}`
|
||||
}
|
||||
|
||||
export const getIncomingTxAmount = (tx, formatted = true) => {
|
||||
export const getIncomingTxAmount = (tx: Transaction, formatted = true): string => {
|
||||
// simple workaround to avoid displaying unexpected values for incoming NFT transfer
|
||||
if (INCOMING_TX_TYPES[tx.type] === INCOMING_TX_TYPES.ERC721_TRANSFER) {
|
||||
return `1 ${tx.symbol}`
|
||||
|
@ -41,9 +53,9 @@ export const getIncomingTxAmount = (tx, formatted = true) => {
|
|||
return getAmountWithSymbol(tx, formatted)
|
||||
}
|
||||
|
||||
export const getTxAmount = (tx, formatted = true) => {
|
||||
export const getTxAmount = (tx: Transaction, formatted = true): string => {
|
||||
const { decimals = 18, decodedParams, isTokenTransfer, symbol } = tx
|
||||
const { value } = isTokenTransfer && !!decodedParams && !!decodedParams.transfer ? decodedParams.transfer : tx
|
||||
const { value } = isTokenTransfer && !!decodedParams?.transfer ? decodedParams.transfer : tx
|
||||
|
||||
if (tx.isCollectibleTransfer) {
|
||||
return `1 ${tx.symbol}`
|
||||
|
@ -56,8 +68,19 @@ export const getTxAmount = (tx, formatted = true) => {
|
|||
return getAmountWithSymbol({ decimals, symbol, value }, formatted)
|
||||
}
|
||||
|
||||
const getIncomingTxTableData = (tx) => ({
|
||||
[TX_TABLE_ID]: tx.blockNumber,
|
||||
interface TableData {
|
||||
amount: string
|
||||
cancelTx?: Transaction
|
||||
date: string
|
||||
dateOrder?: number
|
||||
id: string
|
||||
status: string
|
||||
tx?: Transaction
|
||||
type: any
|
||||
}
|
||||
|
||||
const getIncomingTxTableData = (tx: Transaction): TableData => ({
|
||||
[TX_TABLE_ID]: tx.blockNumber?.toString() ?? '',
|
||||
[TX_TABLE_TYPE_ID]: <TxType txType="incoming" />,
|
||||
[TX_TABLE_DATE_ID]: formatDate(tx.executionDate),
|
||||
[buildOrderFieldFrom(TX_TABLE_DATE_ID)]: getTime(parseISO(tx.executionDate)),
|
||||
|
@ -66,11 +89,11 @@ const getIncomingTxTableData = (tx) => ({
|
|||
[TX_TABLE_RAW_TX_ID]: tx,
|
||||
})
|
||||
|
||||
const getTransactionTableData = (tx, cancelTx) => {
|
||||
const getTransactionTableData = (tx: Transaction, cancelTx: Transaction): TableData => {
|
||||
const txDate = tx.submissionDate
|
||||
|
||||
return {
|
||||
[TX_TABLE_ID]: tx.blockNumber,
|
||||
[TX_TABLE_ID]: tx.blockNumber?.toString() ?? '',
|
||||
[TX_TABLE_TYPE_ID]: <TxType origin={tx.origin} txType={tx.type} />,
|
||||
[TX_TABLE_DATE_ID]: txDate ? formatDate(txDate) : '',
|
||||
[buildOrderFieldFrom(TX_TABLE_DATE_ID)]: txDate ? getTime(parseISO(txDate)) : null,
|
||||
|
@ -81,7 +104,10 @@ const getTransactionTableData = (tx, cancelTx) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const getTxTableData = (transactions, cancelTxs) => {
|
||||
export const getTxTableData = (
|
||||
transactions: List<Transaction>,
|
||||
cancelTxs: CancellationTransactions,
|
||||
): List<TableData> => {
|
||||
return transactions.map((tx) => {
|
||||
if (INCOMING_TX_TYPES[tx.type] !== undefined) {
|
||||
return getIncomingTxTableData(tx)
|
||||
|
@ -91,7 +117,7 @@ export const getTxTableData = (transactions, cancelTxs) => {
|
|||
})
|
||||
}
|
||||
|
||||
export const generateColumns = () => {
|
||||
export const generateColumns = (): List<TableColumn> => {
|
||||
const nonceColumn = {
|
||||
id: TX_TABLE_ID,
|
||||
disablePadding: false,
|
||||
|
|
|
@ -84,7 +84,7 @@ const TxsTable = ({ classes }) => {
|
|||
onClick={() => handleTxExpand(row.tx.safeTxHash)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{autoColumns.map((column: any) => (
|
||||
{autoColumns.map((column) => (
|
||||
<TableCell
|
||||
align={column.align}
|
||||
className={cn(classes.cell, ['cancelled', 'failed'].includes(row.status) && classes.cancelledRow)}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
//
|
||||
import { List } from 'immutable'
|
||||
import { List, Map } from 'immutable'
|
||||
import { makeTransaction } from 'src/routes/safe/store/models/transaction'
|
||||
import { getTxTableData, TX_TABLE_RAW_CANCEL_TX_ID } from 'src/routes/safe/components/Transactions/TxsTable/columns'
|
||||
|
||||
|
@ -10,7 +9,7 @@ describe('TxsTable Columns > getTxTableData', () => {
|
|||
const mockedCancelTransaction = makeTransaction({ nonce: 1, blockNumber: 123 })
|
||||
|
||||
// When
|
||||
const txTableData = getTxTableData(List([mockedTransaction]), List([mockedCancelTransaction]))
|
||||
const txTableData = getTxTableData(List([mockedTransaction]), Map( { '1': mockedCancelTransaction }))
|
||||
const txRow = txTableData.first()
|
||||
|
||||
// Then
|
||||
|
@ -23,7 +22,7 @@ describe('TxsTable Columns > getTxTableData', () => {
|
|||
const mockedCancelTransaction = makeTransaction({ nonce: 2, blockNumber: 123 })
|
||||
|
||||
// When
|
||||
const txTableData = getTxTableData(List([mockedTransaction]), List([mockedCancelTransaction]))
|
||||
const txTableData = getTxTableData(List([mockedTransaction]), Map( { '2': mockedCancelTransaction }))
|
||||
const txRow = txTableData.first()
|
||||
|
||||
// Then
|
||||
|
|
|
@ -7,11 +7,11 @@ import { PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer/provider'
|
|||
import { buildTx, isCancelTransaction } from 'src/routes/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
import { SAFE_REDUCER_ID } from 'src/routes/safe/store/reducer/safe'
|
||||
import { store } from 'src/store'
|
||||
import { DataDecoded } from 'src/logic/contracts/methodIds'
|
||||
import fetchTransactions from 'src/routes/safe/store/actions/transactions/fetchTransactions/fetchTransactions'
|
||||
import { Transaction, TransactionTypes } from 'src/routes/safe/store/models/types/transaction'
|
||||
import { Token } from 'src/logic/tokens/store/model/token'
|
||||
import { SafeRecord } from 'src/routes/safe/store/models/safe'
|
||||
import { DataDecoded } from 'src/routes/safe/store/models/types/transactions.d'
|
||||
|
||||
export type ConfirmationServiceModel = {
|
||||
confirmationType: string
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import { TransferDetails } from './transferDetails.d'
|
||||
import {
|
||||
DataDecoded,
|
||||
Operation,
|
||||
Parameter,
|
||||
Transfer,
|
||||
TransferType,
|
||||
} from 'src/routes/safe/store/models/types/transactions.d'
|
||||
import { Transaction } from 'src/routes/safe/store/models/types/transaction'
|
||||
import {
|
||||
extractERC20TransferDetails,
|
||||
extractERC721TransferDetails,
|
||||
extractETHTransferDetails,
|
||||
extractUnknownTransferDetails,
|
||||
} from './transferDetails'
|
||||
import { isMultiSendParameter } from './newTransactionHelpers'
|
||||
|
||||
export type MultiSendDetails = {
|
||||
operation: keyof typeof Operation
|
||||
to: string
|
||||
data: DataDecoded | null
|
||||
value: number
|
||||
}
|
||||
|
||||
export type MultiSendDecodedData = {
|
||||
txDetails?: MultiSendDetails[]
|
||||
transfersDetails?: TransferDetails[]
|
||||
}
|
||||
|
||||
export const extractTransferDetails = (transfer: Transfer): TransferDetails => {
|
||||
switch (TransferType[transfer.type]) {
|
||||
case TransferType.ERC20_TRANSFER:
|
||||
return extractERC20TransferDetails(transfer)
|
||||
case TransferType.ERC721_TRANSFER:
|
||||
return extractERC721TransferDetails(transfer)
|
||||
case TransferType.ETHER_TRANSFER:
|
||||
return extractETHTransferDetails(transfer)
|
||||
default:
|
||||
return extractUnknownTransferDetails(transfer)
|
||||
}
|
||||
}
|
||||
|
||||
export const extractMultiSendDetails = (parameter: Parameter): MultiSendDetails[] | undefined => {
|
||||
if (isMultiSendParameter(parameter)) {
|
||||
return parameter.decodedValue.map((decodedValue) => {
|
||||
return {
|
||||
operation: decodedValue.operation,
|
||||
to: decodedValue.to,
|
||||
value: decodedValue.value,
|
||||
data: decodedValue?.decodedData ?? null,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const extractMultiSendDecodedData = (tx: Transaction): MultiSendDecodedData => {
|
||||
const transfersDetails = tx.transfers?.map(extractTransferDetails)
|
||||
const txDetails = extractMultiSendDetails(tx.dataDecoded?.parameters[0])
|
||||
|
||||
return { txDetails, transfersDetails }
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import {
|
||||
EthereumTransaction,
|
||||
ModuleTransaction,
|
||||
MultiSendMethodParameter,
|
||||
MultiSigTransaction,
|
||||
Parameter,
|
||||
Transaction,
|
||||
TxType,
|
||||
} from 'src/routes/safe/store/models/types/transactions.d'
|
||||
|
||||
export const isMultiSigTx = (tx: Transaction): tx is MultiSigTransaction => {
|
||||
return TxType[tx.txType] === TxType.MULTISIG_TRANSACTION
|
||||
}
|
||||
|
||||
export const isModuleTx = (tx: Transaction): tx is ModuleTransaction => {
|
||||
return TxType[tx.txType] === TxType.MODULE_TRANSACTION
|
||||
}
|
||||
|
||||
export const isEthereumTx = (tx: Transaction): tx is EthereumTransaction => {
|
||||
return TxType[tx.txType] === TxType.ETHEREUM_TRANSACTION
|
||||
}
|
||||
|
||||
export const isMultiSendParameter = (parameter: Parameter): parameter is MultiSendMethodParameter => {
|
||||
return !!(parameter as MultiSendMethodParameter)?.decodedValue
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { List, Map } from 'immutable'
|
||||
|
||||
import { DecodedMethods, decodeMethods } from 'src/logic/contracts/methodIds'
|
||||
import { decodeMethods } from 'src/logic/contracts/methodIds'
|
||||
import { TOKEN_REDUCER_ID } from 'src/logic/tokens/store/reducer/tokens'
|
||||
import {
|
||||
getERC20DecimalsAndSymbol,
|
||||
|
@ -24,7 +24,7 @@ import {
|
|||
import { CANCELLATION_TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/cancellationTransactions'
|
||||
import { SAFE_REDUCER_ID } from 'src/routes/safe/store/reducer/safe'
|
||||
import { TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/transactions'
|
||||
import { store } from 'src/store'
|
||||
import { AppReduxState, store } from 'src/store'
|
||||
import { safeSelector, safeTransactionsSelector } from 'src/routes/safe/store/selectors'
|
||||
import { addOrUpdateTransactions } from 'src/routes/safe/store/actions/transactions/addOrUpdateTransactions'
|
||||
import {
|
||||
|
@ -35,6 +35,7 @@ import { TypedDataUtils } from 'eth-sig-util'
|
|||
import { Token } from 'src/logic/tokens/store/model/token'
|
||||
import { ProviderRecord } from 'src/logic/wallets/store/model/provider'
|
||||
import { SafeRecord } from 'src/routes/safe/store/models/safe'
|
||||
import { DecodedParams } from 'src/routes/safe/store/models/types/transactions.d'
|
||||
|
||||
export const isEmptyData = (data?: string | null): boolean => {
|
||||
return !data || data === EMPTY_DATA
|
||||
|
@ -130,7 +131,7 @@ export const getRefundParams = async (
|
|||
return refundParams
|
||||
}
|
||||
|
||||
export const getDecodedParams = (tx: TxServiceModel): DecodedMethods => {
|
||||
export const getDecodedParams = (tx: TxServiceModel): DecodedParams | null => {
|
||||
if (tx.dataDecoded) {
|
||||
return {
|
||||
[tx.dataDecoded.method]: tx.dataDecoded.parameters.reduce(
|
||||
|
@ -276,6 +277,7 @@ export const buildTx = async ({
|
|||
creationTx: tx.creationTx,
|
||||
customTx: isCustomTx,
|
||||
data: tx.data ? tx.data : EMPTY_DATA,
|
||||
dataDecoded: tx.dataDecoded,
|
||||
decimals: tokenDecimals,
|
||||
decodedParams,
|
||||
executionDate: tx.executionDate,
|
||||
|
@ -315,7 +317,7 @@ export type TxToMock = TxArgs & {
|
|||
value: string
|
||||
}
|
||||
|
||||
export const mockTransaction = (tx: TxToMock, safeAddress: string, state): Promise<Transaction> => {
|
||||
export const mockTransaction = (tx: TxToMock, safeAddress: string, state: AppReduxState): Promise<Transaction> => {
|
||||
const submissionDate = new Date().toISOString()
|
||||
|
||||
const transactionStructure: TxServiceModel = {
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
export interface IncomingTransferDetails {
|
||||
from: string
|
||||
}
|
||||
|
||||
export interface OutgoingTransferDetails {
|
||||
to: string
|
||||
}
|
||||
|
||||
export interface CommonERC20TransferDetails {
|
||||
tokenAddress: string
|
||||
value: string
|
||||
name: string
|
||||
txHash: string | null
|
||||
}
|
||||
|
||||
export interface IncomingERC20TransferDetails extends CommonERC20TransferDetails, IncomingTransferDetails {}
|
||||
|
||||
export interface OutgoingERC20TransferDetails extends CommonERC20TransferDetails, OutgoingTransferDetails {}
|
||||
|
||||
export type ERC20TransferDetails = IncomingERC20TransferDetails | OutgoingERC20TransferDetails
|
||||
|
||||
export interface CommonERC721TransferDetails {
|
||||
tokenAddress: string
|
||||
tokenId: string | null
|
||||
txHash: string | null
|
||||
}
|
||||
|
||||
export interface IncomingERC721TransferDetails extends CommonERC721TransferDetails, IncomingTransferDetails {}
|
||||
|
||||
export interface OutgoingERC721TransferDetails extends CommonERC721TransferDetails, OutgoingTransferDetails {}
|
||||
|
||||
export type ERC721TransferDetails = IncomingERC721TransferDetails | OutgoingERC721TransferDetails
|
||||
|
||||
export interface CommonETHTransferDetails {
|
||||
value: string
|
||||
txHash: string | null
|
||||
}
|
||||
|
||||
export interface IncomingETHTransferDetails extends CommonETHTransferDetails, IncomingTransferDetails {}
|
||||
|
||||
export interface OutgoingETHTransferDetails extends CommonETHTransferDetails, OutgoingTransferDetails {}
|
||||
|
||||
export type ETHTransferDetails = IncomingETHTransferDetails | OutgoingETHTransferDetails
|
||||
|
||||
export interface UnknownTransferDetails extends IncomingTransferDetails, OutgoingTransferDetails {
|
||||
value: string
|
||||
txHash: string
|
||||
}
|
||||
|
||||
export type TransferDetails = ERC20TransferDetails | ERC721TransferDetails | ETHTransferDetails | UnknownTransferDetails
|
|
@ -0,0 +1,85 @@
|
|||
import { Transfer, TxConstants } from 'src/routes/safe/store/models/types/transactions.d'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { store } from 'src/store'
|
||||
import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors'
|
||||
import {
|
||||
ERC20TransferDetails,
|
||||
ERC721TransferDetails,
|
||||
ETHTransferDetails,
|
||||
UnknownTransferDetails,
|
||||
} from './transferDetails.d'
|
||||
import { humanReadableValue } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
|
||||
const isIncomingTransfer = (transfer: Transfer): boolean => {
|
||||
// TODO: prevent using `store` here and receive `safeAddress` as a param
|
||||
const state = store.getState()
|
||||
const safeAddress = safeParamAddressFromStateSelector(state)
|
||||
return sameAddress(transfer.to, safeAddress)
|
||||
}
|
||||
|
||||
export const extractERC20TransferDetails = (transfer: Transfer): ERC20TransferDetails => {
|
||||
const erc20TransferDetails = {
|
||||
tokenAddress: transfer.tokenInfo?.address || TxConstants.UNKNOWN,
|
||||
value: humanReadableValue(transfer.value, transfer.tokenInfo?.decimals),
|
||||
name: transfer.tokenInfo?.name || transfer.tokenInfo?.symbol || TxConstants.UNKNOWN,
|
||||
txHash: transfer.transactionHash,
|
||||
}
|
||||
|
||||
if (isIncomingTransfer(transfer)) {
|
||||
return {
|
||||
...erc20TransferDetails,
|
||||
from: transfer.from,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...erc20TransferDetails,
|
||||
to: transfer.to,
|
||||
}
|
||||
}
|
||||
|
||||
export const extractERC721TransferDetails = (transfer: Transfer): ERC721TransferDetails => {
|
||||
const erc721TransferDetails = {
|
||||
tokenAddress: transfer.tokenAddress,
|
||||
tokenId: transfer.tokenId,
|
||||
txHash: transfer.transactionHash,
|
||||
}
|
||||
if (isIncomingTransfer(transfer)) {
|
||||
return {
|
||||
...erc721TransferDetails,
|
||||
from: transfer.from,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...erc721TransferDetails,
|
||||
to: transfer.to,
|
||||
}
|
||||
}
|
||||
|
||||
export const extractETHTransferDetails = (transfer: Transfer): ETHTransferDetails => {
|
||||
const ethTransferDetails = {
|
||||
value: humanReadableValue(transfer.value),
|
||||
txHash: transfer.transactionHash,
|
||||
}
|
||||
if (isIncomingTransfer(transfer)) {
|
||||
return {
|
||||
...ethTransferDetails,
|
||||
from: transfer.from,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...ethTransferDetails,
|
||||
to: transfer.to,
|
||||
}
|
||||
}
|
||||
|
||||
export const extractUnknownTransferDetails = (transfer: Transfer): UnknownTransferDetails => {
|
||||
return {
|
||||
value: transfer?.value || TxConstants.UNKNOWN,
|
||||
txHash: transfer?.transactionHash || TxConstants.UNKNOWN,
|
||||
to: transfer?.to || TxConstants.UNKNOWN,
|
||||
from: transfer?.from || TxConstants.UNKNOWN,
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ export const makeTransaction = Record<TransactionProps>({
|
|||
creationTx: false,
|
||||
customTx: false,
|
||||
data: null,
|
||||
dataDecoded: null,
|
||||
decimals: 18,
|
||||
decodedParams: {},
|
||||
executionDate: '',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { List, Map, RecordOf } from 'immutable'
|
||||
import { DecodedMethods } from 'src/logic/contracts/methodIds'
|
||||
import { Confirmation } from './confirmation'
|
||||
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
|
||||
import { DataDecoded, DecodedParams, Transfer } from './transactions'
|
||||
|
||||
export enum TransactionTypes {
|
||||
INCOMING = 'incoming',
|
||||
|
@ -43,13 +43,14 @@ export type TransactionProps = {
|
|||
creationTx: boolean
|
||||
customTx: boolean
|
||||
data?: string | null
|
||||
dataDecoded: DataDecoded | null
|
||||
decimals?: (number | string) | null
|
||||
decodedParams: DecodedMethods
|
||||
decodedParams: DecodedParams | null
|
||||
executionDate?: string | null
|
||||
executionTxHash?: string | null
|
||||
executor: string
|
||||
factoryAddress: string
|
||||
fee: string | null // It will be replace with the new TXs types.
|
||||
fee?: string // It will be replace with the new TXs types.
|
||||
gasPrice: string
|
||||
gasToken: string
|
||||
isCancellationTx: boolean
|
||||
|
@ -75,6 +76,7 @@ export type TransactionProps = {
|
|||
submissionDate?: string | null
|
||||
symbol?: string | null
|
||||
transactionHash: string | null
|
||||
transfers?: Transfer[]
|
||||
type: TransactionTypes
|
||||
upgradeTx: boolean
|
||||
value: string
|
||||
|
|
|
@ -0,0 +1,254 @@
|
|||
export enum TxConstants {
|
||||
MULTI_SEND = 'multiSend',
|
||||
UNKNOWN = 'UNKNOWN',
|
||||
}
|
||||
|
||||
export enum Operation {
|
||||
CALL = 'CALL',
|
||||
DELEGATE_CALL = 'DELEGATE_CALL',
|
||||
CREATE = 'CREATE',
|
||||
}
|
||||
|
||||
// types comes from: https://github.com/gnosis/safe-client-gateway/blob/752e76b6d1d475791dbd7917b174bb41d2d9d8be/src/utils.rs
|
||||
export enum TransferMethods {
|
||||
TRANSFER = 'transfer',
|
||||
TRANSFER_FROM = 'transferFrom',
|
||||
SAFE_TRANSFER_FROM = 'safeTransferFrom',
|
||||
}
|
||||
|
||||
export enum SettingsChangeMethods {
|
||||
SETUP = 'setup',
|
||||
SET_FALLBACK_HANDLER = 'setFallbackHandler',
|
||||
ADD_OWNER_WITH_THRESHOLD = 'addOwnerWithThreshold',
|
||||
REMOVE_OWNER = 'removeOwner',
|
||||
REMOVE_OWNER_WITH_THRESHOLD = 'removeOwnerWithThreshold',
|
||||
SWAP_OWNER = 'swapOwner',
|
||||
CHANGE_THRESHOLD = 'changeThreshold',
|
||||
CHANGE_MASTER_COPY = 'changeMasterCopy',
|
||||
ENABLE_MODULE = 'enableModule',
|
||||
DISABLE_MODULE = 'disableModule',
|
||||
EXEC_TRANSACTION_FROM_MODULE = 'execTransactionFromModule',
|
||||
APPROVE_HASH = 'approveHash',
|
||||
EXEC_TRANSACTION = 'execTransaction',
|
||||
}
|
||||
|
||||
// note: this extends SAFE_METHODS_NAMES in /logic/contracts/methodIds.ts, we need to figure out which one we are going to use
|
||||
export type DataDecodedMethod = TransferMethods | SettingsChangeMethods | string
|
||||
|
||||
export interface DecodedValue {
|
||||
operation: Operation
|
||||
to: string
|
||||
value: number
|
||||
data: string
|
||||
decodedData: DataDecoded
|
||||
}
|
||||
|
||||
export interface SingleTransactionMethodParameter {
|
||||
name: string
|
||||
type: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface MultiSendMethodParameter extends SingleTransactionMethodParameter {
|
||||
decodedValue: DecodedValue[]
|
||||
}
|
||||
|
||||
export type Parameter = MultiSendMethodParameter | SingleTransactionMethodParameter
|
||||
|
||||
export interface DataDecoded {
|
||||
method: DataDecodedMethod
|
||||
parameters: Parameter[]
|
||||
}
|
||||
|
||||
export enum ConfirmationType {
|
||||
CONFIRMATION = 'CONFIRMATION',
|
||||
EXECUTION = 'EXECUTION',
|
||||
}
|
||||
|
||||
export enum SignatureType {
|
||||
CONTRACT_SIGNATURE = 'CONTRACT_SIGNATURE',
|
||||
APPROVED_HASH = 'APPROVED_HASH',
|
||||
EOA = 'EOA',
|
||||
ETH_SIGN = 'ETH_SIGN',
|
||||
}
|
||||
|
||||
export interface Confirmation {
|
||||
owner: string
|
||||
submissionDate: string
|
||||
transactionHash: string | null
|
||||
confirmationType: ConfirmationType
|
||||
signature: string
|
||||
signatureType: SignatureType
|
||||
}
|
||||
|
||||
export enum TokenType {
|
||||
ERC20 = 'ERC20',
|
||||
ERC721 = 'ERC721',
|
||||
OTHER = 'OTHER',
|
||||
}
|
||||
|
||||
export interface TokenInfo {
|
||||
type: TokenType
|
||||
address: string
|
||||
name: string
|
||||
symbol: string
|
||||
decimals: number
|
||||
logoUri: string
|
||||
}
|
||||
|
||||
export enum TransferType {
|
||||
ETHER_TRANSFER = 'ETHER_TRANSFER',
|
||||
ERC20_TRANSFER = 'ERC20_TRANSFER',
|
||||
ERC721_TRANSFER = 'ERC721_TRANSFER',
|
||||
UNKNOWN = 'UNKNOWN',
|
||||
}
|
||||
|
||||
export interface Transfer {
|
||||
type: TransferType
|
||||
executionDate: string
|
||||
blockNumber: number
|
||||
transactionHash: string | null
|
||||
to: string
|
||||
value: string | null
|
||||
tokenId: string | null
|
||||
tokenAddress: string
|
||||
tokenInfo: TokenInfo | null
|
||||
from: string
|
||||
}
|
||||
|
||||
export enum TxType {
|
||||
MULTISIG_TRANSACTION = 'MULTISIG_TRANSACTION',
|
||||
ETHEREUM_TRANSACTION = 'ETHEREUM_TRANSACTION',
|
||||
MODULE_TRANSACTION = 'MODULE_TRANSACTION',
|
||||
}
|
||||
|
||||
export interface MultiSigTransaction {
|
||||
safe: string
|
||||
to: string
|
||||
value: string
|
||||
data: string | null
|
||||
operation: number
|
||||
gasToken: string
|
||||
safeTxGas: number
|
||||
baseGas: number
|
||||
gasPrice: string
|
||||
refundReceiver: string
|
||||
nonce: number
|
||||
executionDate: string | null
|
||||
submissionDate: string
|
||||
modified: string
|
||||
blockNumber: number | null
|
||||
transactionHash: string | null
|
||||
safeTxHash: string
|
||||
executor: string | null
|
||||
isExecuted: boolean
|
||||
isSuccessful: boolean | null
|
||||
ethGasPrice: string | null
|
||||
gasUsed: number | null
|
||||
fee: string | null
|
||||
origin: string | null
|
||||
dataDecoded: DataDecoded | null
|
||||
confirmationsRequired: number | null
|
||||
confirmations: Confirmation[]
|
||||
signatures: string | null
|
||||
transfers: Transfer[]
|
||||
txType: TxType.MULTISIG_TRANSACTION
|
||||
}
|
||||
|
||||
export interface ModuleTransaction {
|
||||
created: string
|
||||
executionDate: string
|
||||
blockNumber: number
|
||||
transactionHash: string
|
||||
safe: string
|
||||
module: string
|
||||
to: string
|
||||
value: string
|
||||
data: string
|
||||
operation: Operation
|
||||
transfers: Transfer[]
|
||||
txType: TxType.MODULE_TRANSACTION
|
||||
}
|
||||
|
||||
export interface EthereumTransaction {
|
||||
executionDate: string
|
||||
to: string
|
||||
data: string | null
|
||||
txHash: string
|
||||
blockNumber: number
|
||||
transfers: Transfer[]
|
||||
txType: TxType.ETHEREUM_TRANSACTION
|
||||
from: string
|
||||
}
|
||||
|
||||
export type Transaction = MultiSigTransaction | ModuleTransaction | EthereumTransaction
|
||||
|
||||
// SAFE METHODS TO ITS ID
|
||||
// https://github.com/gnosis/safe-contracts/blob/development/test/safeMethodNaming.js
|
||||
// https://github.com/gnosis/safe-contracts/blob/development/contracts/GnosisSafe.sol
|
||||
// [
|
||||
// { name: "addOwnerWithThreshold", id: "0x0d582f13" },
|
||||
// { name: "DOMAIN_SEPARATOR_TYPEHASH", id: "0x1db61b54" },
|
||||
// { name: "isOwner", id: "0x2f54bf6e" },
|
||||
// { name: "execTransactionFromModule", id: "0x468721a7" },
|
||||
// { name: "signedMessages", id: "0x5ae6bd37" },
|
||||
// { name: "enableModule", id: "0x610b5925" },
|
||||
// { name: "changeThreshold", id: "0x694e80c3" },
|
||||
// { name: "approvedHashes", id: "0x7d832974" },
|
||||
// { name: "changeMasterCopy", id: "0x7de7edef" },
|
||||
// { name: "SENTINEL_MODULES", id: "0x85e332cd" },
|
||||
// { name: "SENTINEL_OWNERS", id: "0x8cff6355" },
|
||||
// { name: "getOwners", id: "0xa0e67e2b" },
|
||||
// { name: "NAME", id: "0xa3f4df7e" },
|
||||
// { name: "nonce", id: "0xaffed0e0" },
|
||||
// { name: "getModules", id: "0xb2494df3" },
|
||||
// { name: "SAFE_MSG_TYPEHASH", id: "0xc0856ffc" },
|
||||
// { name: "SAFE_TX_TYPEHASH", id: "0xccafc387" },
|
||||
// { name: "disableModule", id: "0xe009cfde" },
|
||||
// { name: "swapOwner", id: "0xe318b52b" },
|
||||
// { name: "getThreshold", id: "0xe75235b8" },
|
||||
// { name: "domainSeparator", id: "0xf698da25" },
|
||||
// { name: "removeOwner", id: "0xf8dc5dd9" },
|
||||
// { name: "VERSION", id: "0xffa1ad74" },
|
||||
// { name: "setup", id: "0xa97ab18a" },
|
||||
// { name: "execTransaction", id: "0x6a761202" },
|
||||
// { name: "requiredTxGas", id: "0xc4ca3a9c" },
|
||||
// { name: "approveHash", id: "0xd4d9bdcd" },
|
||||
// { name: "signMessage", id: "0x85a5affe" },
|
||||
// { name: "isValidSignature", id: "0x20c13b0b" },
|
||||
// { name: "getMessageHash", id: "0x0a1028c4" },
|
||||
// { name: "encodeTransactionData", id: "0xe86637db" },
|
||||
// { name: "getTransactionHash", id: "0xd8d11f78" }
|
||||
// ]
|
||||
|
||||
export const SAFE_METHODS_NAMES = {
|
||||
ADD_OWNER_WITH_THRESHOLD: 'addOwnerWithThreshold',
|
||||
CHANGE_THRESHOLD: 'changeThreshold',
|
||||
REMOVE_OWNER: 'removeOwner',
|
||||
SWAP_OWNER: 'swapOwner',
|
||||
ENABLE_MODULE: 'enableModule',
|
||||
DISABLE_MODULE: 'disableModule',
|
||||
}
|
||||
|
||||
export const METHOD_TO_ID = {
|
||||
'0xe318b52b': SAFE_METHODS_NAMES.SWAP_OWNER,
|
||||
'0x0d582f13': SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD,
|
||||
'0xf8dc5dd9': SAFE_METHODS_NAMES.REMOVE_OWNER,
|
||||
'0x694e80c3': SAFE_METHODS_NAMES.CHANGE_THRESHOLD,
|
||||
'0x610b5925': SAFE_METHODS_NAMES.ENABLE_MODULE,
|
||||
'0xe009cfde': SAFE_METHODS_NAMES.DISABLE_MODULE,
|
||||
}
|
||||
|
||||
export type SafeMethods = typeof SAFE_METHODS_NAMES[keyof typeof SAFE_METHODS_NAMES]
|
||||
|
||||
type TokenMethods = 'transfer' | 'transferFrom' | 'safeTransferFrom'
|
||||
|
||||
type SafeDecodedParams = {
|
||||
[key in SafeMethods]?: Record<string, string>
|
||||
}
|
||||
|
||||
type TokenDecodedParams = {
|
||||
[key in TokenMethods]?: Record<string, string>
|
||||
}
|
||||
|
||||
export type DecodedParams = SafeDecodedParams | TokenDecodedParams | null
|
|
@ -3,9 +3,13 @@ import { handleActions } from 'redux-actions'
|
|||
|
||||
import { ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS } from 'src/routes/safe/store/actions/transactions/addOrUpdateCancellationTransactions'
|
||||
import { REMOVE_CANCELLATION_TRANSACTION } from 'src/routes/safe/store/actions/transactions/removeCancellationTransaction'
|
||||
import { Transaction } from 'src/routes/safe/store/models/types/transaction'
|
||||
|
||||
export const CANCELLATION_TRANSACTIONS_REDUCER_ID = 'cancellationTransactions'
|
||||
|
||||
export type CancellationTransactions = Map<string, Transaction>
|
||||
export type CancellationTxState = Map<string, CancellationTransactions>
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
[ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS]: (state, action) => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { SafeRecord } from 'src/routes/safe/store/models/safe'
|
||||
import { SafeRecord, SafeRecordProps } from 'src/routes/safe/store/models/safe'
|
||||
import { Map } from 'immutable'
|
||||
|
||||
export type SafesMap = Map<string, SafeRecord>
|
||||
|
|
|
@ -3,7 +3,10 @@ import { matchPath, RouteComponentProps } from 'react-router-dom'
|
|||
import { createSelector } from 'reselect'
|
||||
import { SAFELIST_ADDRESS, SAFE_PARAM_ADDRESS } from 'src/routes/routes'
|
||||
|
||||
import { CANCELLATION_TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/cancellationTransactions'
|
||||
import {
|
||||
CANCELLATION_TRANSACTIONS_REDUCER_ID,
|
||||
CancellationTransactions,
|
||||
} from 'src/routes/safe/store/reducer/cancellationTransactions'
|
||||
import { INCOMING_TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/incomingTransactions'
|
||||
import { SAFE_REDUCER_ID, SafesMap } from 'src/routes/safe/store/reducer/safe'
|
||||
import { TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/transactions'
|
||||
|
@ -81,7 +84,7 @@ export const addressBookQueryParamsSelector = (state: AppReduxState): string | n
|
|||
export const safeCancellationTransactionsSelector = createSelector(
|
||||
cancellationTransactionsSelector,
|
||||
safeParamAddressFromStateSelector,
|
||||
(cancellationTransactions, address) => {
|
||||
(cancellationTransactions, address): CancellationTransactions => {
|
||||
if (!cancellationTransactions) {
|
||||
return Map()
|
||||
}
|
||||
|
@ -118,9 +121,7 @@ export const safeSelector = createSelector(
|
|||
return undefined
|
||||
}
|
||||
const checksumed = checksumAddress(address)
|
||||
const safe = safes.get(checksumed)
|
||||
|
||||
return safe
|
||||
return safes.get(checksumed)
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -152,13 +153,16 @@ export const safeActiveAssetsListSelector = createSelector(safeActiveAssetsSelec
|
|||
return Set(safeList)
|
||||
})
|
||||
|
||||
export const safeBlacklistedTokensSelector = createSelector(safeSelector, (safe) => {
|
||||
if (!safe) {
|
||||
return List()
|
||||
}
|
||||
export const safeBlacklistedTokensSelector = createSelector(
|
||||
safeSelector,
|
||||
(safe): Set<string> => {
|
||||
if (!safe) {
|
||||
return Set()
|
||||
}
|
||||
|
||||
return safe.blacklistedTokens
|
||||
})
|
||||
return safe.blacklistedTokens
|
||||
},
|
||||
)
|
||||
|
||||
export const safeBlacklistedAssetsSelector = createSelector(
|
||||
safeSelector,
|
||||
|
@ -171,23 +175,12 @@ export const safeBlacklistedAssetsSelector = createSelector(
|
|||
},
|
||||
)
|
||||
|
||||
export const safeActiveAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap) =>
|
||||
export const safeActiveAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap): Set<string> =>
|
||||
safes.get(safeAddress).get('activeAssets')
|
||||
|
||||
export const safeBlacklistedAssetsSelectorBySafe = (safeAddress, safes) =>
|
||||
export const safeBlacklistedAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap): Set<string> =>
|
||||
safes.get(safeAddress).get('blacklistedAssets')
|
||||
|
||||
export const safeBalancesSelector = createSelector(
|
||||
safeSelector,
|
||||
(safe): Map<string, string> => {
|
||||
if (!safe) {
|
||||
return Map()
|
||||
}
|
||||
|
||||
return safe.balances
|
||||
},
|
||||
)
|
||||
|
||||
const baseSafe = makeSafe()
|
||||
|
||||
export const safeFieldSelector = <K extends keyof SafeRecordProps>(field: K) => (
|
||||
|
@ -198,6 +191,8 @@ export const safeNameSelector = createSelector(safeSelector, safeFieldSelector('
|
|||
|
||||
export const safeEthBalanceSelector = createSelector(safeSelector, safeFieldSelector('ethBalance'))
|
||||
|
||||
export const safeBalancesSelector = createSelector(safeSelector, safeFieldSelector('balances'))
|
||||
|
||||
export const safeNeedsUpdateSelector = createSelector(safeSelector, safeFieldSelector('needsUpdate'))
|
||||
|
||||
export const safeCurrentVersionSelector = createSelector(safeSelector, safeFieldSelector('currentVersion'))
|
||||
|
|
|
@ -2,9 +2,10 @@ import { List } from 'immutable'
|
|||
import { createSelector } from 'reselect'
|
||||
|
||||
import { safeIncomingTransactionsSelector, safeTransactionsSelector } from 'src/routes/safe/store/selectors'
|
||||
import { Transaction } from 'src/routes/safe/store/models/types/transaction'
|
||||
|
||||
export const extendedTransactionsSelector = createSelector(
|
||||
safeTransactionsSelector,
|
||||
safeIncomingTransactionsSelector,
|
||||
(transactions, incomingTransactions) => List([...transactions, ...incomingTransactions]),
|
||||
(transactions, incomingTransactions): List<Transaction> => List([...transactions, ...incomingTransactions]),
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Map } from 'immutable'
|
||||
import { connectRouter, routerMiddleware, RouterState } from 'connected-react-router'
|
||||
import { createHashHistory } from 'history'
|
||||
import { applyMiddleware, combineReducers, compose, createStore, CombinedState } from 'redux'
|
||||
import { applyMiddleware, combineReducers, compose, createStore, CombinedState, PreloadedState, Store } from 'redux'
|
||||
import thunk from 'redux-thunk'
|
||||
|
||||
import addressBookMiddleware from 'src/logic/addressBook/store/middleware/addressBookMiddleware'
|
||||
|
@ -14,7 +15,10 @@ import {
|
|||
} from 'src/logic/collectibles/store/reducer/collectibles'
|
||||
import cookies, { COOKIES_REDUCER_ID } from 'src/logic/cookies/store/reducer/cookies'
|
||||
import currencyValuesStorageMiddleware from 'src/logic/currencyValues/store/middleware'
|
||||
import currencyValues, { CURRENCY_VALUES_KEY } from 'src/logic/currencyValues/store/reducer/currencyValues'
|
||||
import currencyValues, {
|
||||
CURRENCY_VALUES_KEY,
|
||||
CurrencyReducerMap,
|
||||
} from 'src/logic/currencyValues/store/reducer/currencyValues'
|
||||
import currentSession, { CURRENT_SESSION_REDUCER_ID } from 'src/logic/currentSession/store/reducer/currentSession'
|
||||
import notifications, { NOTIFICATIONS_REDUCER_ID } from 'src/logic/notifications/store/reducer/notifications'
|
||||
import tokens, { TOKEN_REDUCER_ID, TokenState } from 'src/logic/tokens/store/reducer/tokens'
|
||||
|
@ -24,6 +28,7 @@ import notificationsMiddleware from 'src/routes/safe/store/middleware/notificati
|
|||
import safeStorage from 'src/routes/safe/store/middleware/safeStorage'
|
||||
import cancellationTransactions, {
|
||||
CANCELLATION_TRANSACTIONS_REDUCER_ID,
|
||||
CancellationTxState,
|
||||
} from 'src/routes/safe/store/reducer/cancellationTransactions'
|
||||
import incomingTransactions, {
|
||||
INCOMING_TRANSACTIONS_REDUCER_ID,
|
||||
|
@ -31,8 +36,6 @@ import incomingTransactions, {
|
|||
import safe, { SAFE_REDUCER_ID, SafeReducerMap } from 'src/routes/safe/store/reducer/safe'
|
||||
import transactions, { TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/transactions'
|
||||
import { NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/OpenSea'
|
||||
import { Map } from 'immutable'
|
||||
import { CurrencyRateValueRecord } from 'src/logic/currencyValues/store/model/currencyValues'
|
||||
|
||||
export const history = createHashHistory()
|
||||
|
||||
|
@ -74,10 +77,10 @@ export type AppReduxState = CombinedState<{
|
|||
[NFT_TOKENS_REDUCER_ID]: NFTTokens
|
||||
[TOKEN_REDUCER_ID]: TokenState
|
||||
[TRANSACTIONS_REDUCER_ID]: Map<string, any>
|
||||
[CANCELLATION_TRANSACTIONS_REDUCER_ID]: Map<string, any>
|
||||
[CANCELLATION_TRANSACTIONS_REDUCER_ID]: CancellationTxState
|
||||
[INCOMING_TRANSACTIONS_REDUCER_ID]: Map<string, any>
|
||||
[NOTIFICATIONS_REDUCER_ID]: Map<string, any>
|
||||
[CURRENCY_VALUES_KEY]: Map<string, CurrencyRateValueRecord>
|
||||
[CURRENCY_VALUES_KEY]: CurrencyReducerMap
|
||||
[COOKIES_REDUCER_ID]: Map<string, any>
|
||||
[ADDRESS_BOOK_REDUCER_ID]: AddressBookReducerMap
|
||||
[CURRENT_SESSION_REDUCER_ID]: Map<string, any>
|
||||
|
@ -86,4 +89,5 @@ export type AppReduxState = CombinedState<{
|
|||
|
||||
export const store: any = createStore(reducers, finalCreateStore)
|
||||
|
||||
export const aNewStore = (localState?: any) => createStore(reducers, localState, finalCreateStore)
|
||||
export const aNewStore = (localState?: PreloadedState<unknown>): Store =>
|
||||
createStore(reducers, localState, finalCreateStore)
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
//
|
||||
import { fireEvent } from '@testing-library/react'
|
||||
import { sleep } from 'src/utils/timer'
|
||||
import { shortVersionOf } from 'src/logic/wallets/ethAddresses'
|
||||
import { TRANSACTIONS_TAB_BTN_TEST_ID } from 'src/routes/safe/components/Layout/index'
|
||||
import { TRANSACTIONS_TAB_BTN_TEST_ID } from 'src/routes/safe/components/Layout'
|
||||
import { TRANSACTION_ROW_TEST_ID } from 'src/routes/safe/components/Transactions/TxsTable'
|
||||
import {
|
||||
TRANSACTIONS_DESC_SEND_TEST_ID,
|
||||
} from 'src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription'
|
||||
import {
|
||||
TRANSACTIONS_DESC_ADD_OWNER_TEST_ID,
|
||||
TRANSACTIONS_DESC_REMOVE_OWNER_TEST_ID,
|
||||
TRANSACTIONS_DESC_SEND_TEST_ID,
|
||||
} from 'src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription'
|
||||
} from 'src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/SettingsDescription'
|
||||
|
||||
export const getLastTransaction = async (SafeDom) => {
|
||||
// Travel to transactions
|
||||
|
|
|
@ -24,10 +24,7 @@ export const loadFromStorage = async <T = unknown>(key: string): Promise<T | und
|
|||
}
|
||||
}
|
||||
|
||||
export const saveToStorage = async (
|
||||
key: string,
|
||||
value: Record<string, unknown> | boolean | string | number | Array<unknown>,
|
||||
): Promise<void> => {
|
||||
export const saveToStorage = async <T = unknown>(key: string, value: T): Promise<void> => {
|
||||
try {
|
||||
const stringifiedValue = JSON.stringify(value)
|
||||
await storage.set(`${PREFIX}__${key}`, stringifiedValue)
|
||||
|
|
Loading…
Reference in New Issue