Transaction List v2 (#1781)

* Add types for redux actions (#1737)

* solve errors after rebase

- added `isStoredTransaction` to differentiate tx provided to `isCancelTransaction`

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* Add types + loadGateway transactions cosumer

* add client-gateway endpoints to networks configs

* add client-gateway getters

* WIP: consume gateway-client endpoint

- added the history transactions to the store
- updated types to `/queued` and `/history` endpoints

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* add queued transactions to the store

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* add queued transactions selectors

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* WIP: display history transactions

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* WIP: arrange lists queue/history

* prevent loading data from txs-service

* cherry-pick TokenTransferAmount component

* extract queue transactions logic into a hook `useQueueTransactions`

* Add TxType and TokenTransferAmount components

Co-authored-by: fernandomg <fernando.greco@altoros.com>

* wip: history transactions

* wip: use grid to display list content

* wip: use Accordion

* wip: tx history

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* wip: tx details

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* wip: TxInfo

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* wip: TxSummary

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* wip: TxSettingsInfo

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* Wip: style owners list

Co-authored-by: fernandomg <fernando.greco@altoros.com>

* wip: Owners List

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* wip: TxInfoCreation

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* wip: stop using backOff for client-gateway requests

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* refactor

reorganize files and components

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* refactor

- Accordion implementation
- extract summaryContent to a reusable component

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* Fix prettier issue in src/config/index

* add methods names and descriptions to collapsed row

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* wip: split components to render tx-data depending on the tx type

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* add multiSend tx details

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* refactor TxData

- separate into specified components `HexEncodedData`, `MethodDetails` & `MultiSendDetails`

* remove unused imported type

* wip: infinite-scroll

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* refactor `ADD_HISTORY_TRANSACTIONS` reducer

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* implement infinite scroll pagination

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* avoid defining `page_url` param

* refactor InfiniteScroll implementation

- created a wrapper component to simplify interface
- rearranged code

* add `missingSigners` key to `ExecutionInfo` type

* add `lodash.get`

* update `useTransactionStatus` hook to support queued transactions

* use `lodash.get` to access queued objects

* add votes info to TxCollapsed

* add TxQueueCollapsed

- also update the usage of `useTransactionStatus` hook

* split `TxRow` into `TxHistoryRow` and `TxQueueRow`

* use `txLocation` instead of `title` for `QueueTxList` component

* make `TxDetails` generic

* fix queue list elements arrangement

* export `useTransactionDetails` return type

[skip ci]

* wip: group txs by nonce

[skip ci]

* request tx details on demand

[skip ci]

* display cancelling message in queued transactions only

[skip ci]

* wip: implement tree view for grouped transactions

* styled components

- reorganized
- added comments where necessary
- refactored

* refactor QueueTxList

[skip ci]

* update styled-component

[skip ci]

* refactor

- Accordion implementation
- extract summaryContent to a reusable component

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* update safe-react-components

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* update styled-component

[skip ci]

* fix most-recent list of history transactions update

* make queued transactions list scrollable

* make scrollableTarget a const

* styles fixes

- queued grouped transactions styles
- add styles to scrollbar in scrollable areas

* add safe apps info to tx lists

* wip: add action buttons to tx details

* fix column distribution for transactions rows

* display action count for multiSend transactions

* TxExpandedActions

* add action buttons

- also did a slight refactor around grouped vs. not-grouped transactions

* fix txDetails selector

* adapt button to current SRC specs

* wip: action buttons "action"

- TODO: handle the store update -> screen refresh

* fix execution/confirmation conditions

- fixed modals conditions for execution when last confirmation is able to execute

* fix tree view (no <p> as descendant of <p>)

* wip: handle transactions actions through a context provider

* provide txLocation through context

* fix `react-hooks/exhaustive-deps` warnings

* Sort history list

* Add objects utils

* add `lodash.merge` as a dependency

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* update `ADD_QUEUED_TRANSACTIONS` reducer

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* implement pagination for `queued` transactions

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* prevent rendering action modal if `txDetails` is not available

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* pending status for tx execution

* small style-based behavior fixes

* allow to identify txs to be replaced

* redirect to `gatewayTransaction`

* adjust behavior for grouped vs individual transactions

* add help links

* display execute action only when threshold is reached

* make `setActiveHover` required

* prevent `<p />` as child of `<p />`

* fix cards background colors

* revert staging config

* fix linting errors

* prevent using `no-owner` class in history list

* add `PENDING` status to confirmation transactions

This will mark as _pending_ a transaction by its id, the rest of the txs that share same nonce will remain untouched

* unify action buttons status

- created `useActionButtonsHandler` hook
- extracted `CollapsedActions` into `TxCollapsedActions` component

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* fix wording

* fix pending status

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* fix close Action modal

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* extract `addressInList` as a util function

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* fix action buttons' "disabled" status condition

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* provide proper `to` and `value` for `processTransaction` based on Transfer Type (ERC20, ERC721 or ETHER)

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* update queued transactions pointers if we reached the last page

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* use `as string` for `next` pointer

- also fixed typo

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* add JSDocs

* explicitly discard unused client-gateway headers

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>
Co-authored-by: nicosampler <nf.dominguez.87@gmail.com>

* add loading status to the queue transactions list

* fix tx actions after rebase of v2.19.1

* fix issue with safe data update

* fix types issues

* skip `isCancelTransaction` tests

* fix loading status for queue transactions

* Update notifications for tx-list v2 (#1839)

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* use `sameString` to verify `method` value

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* TxDetails refactor cancelTxDetails condition

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* remove unused TxType component

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* remove unused `isReadyToExecute` function

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* Fix eslint

* Update txs details after `PENDING` status update

* remove log

* Fix send transaction because of removed notification message

* Cleanup pending unwanted notifications

* wip: ellipsis actions

* wip: ellipsis actions - fix tokenAmount

* Refactor to txInfoDetails

* refactor `TxInfoDetails`

* remove old `utils.tsx` file

* support SpendingLimit transactions

* fix `isSpendingLimitMethod`

* Fix styles for tx list v2 (#1859)

Co-authored-by: fernandomg <fernando.greco@gmail.com>
Co-authored-by: Agustín Longoni <agustin.longoni@altoros.com>

* wip: performance enhancement

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>
Co-authored-by: nicosampler <nf.dominguez.87@gmail.com>

* wip: extract data calculation to a hook

* refactor huge ternaries

* fix columns styles for small screens

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* add extra information for `Cancel` transaction identification

* undo custom selector

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>

* undo custom selector

* Pass `action` by prop to TxDetails

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>
Co-authored-by: nicosampler <nf.dominguez.87@gmail.com>

* Unify `processTransaction` / `createTransaction` actions

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>
Co-authored-by: nicosampler <nf.dominguez.87@gmail.com>
Co-authored-by: Daniel Sanchez <daniel.sanchez@gnosis.pm>

* Disable send again when the user is offline

* set pending status for the executed tx only (not the group by nonce)

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>
Co-authored-by: nicosampler <nf.dominguez.87@gmail.com>
Co-authored-by: Daniel Sanchez <daniel.sanchez@gnosis.pm>

* Use gatewayTransactions as default transaction list

* fix styles for TxDetails row

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>
Co-authored-by: nicosampler <nf.dominguez.87@gmail.com>
Co-authored-by: Daniel Sanchez <daniel.sanchez@gnosis.pm>

* Remove old transactions list legacy code
Move gatewayTransactions within transactions folder

* Remove allTransactions legacy code

* Types

* Fix redirect after createTransaction

* fix performance issue for `ApproveTxModal`

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>
Co-authored-by: nicosampler <nf.dominguez.87@gmail.com>
Co-authored-by: Agustin Pane <agustin.pane@gmail.com>

* fix asset icon size

* fix status wording

* add time tooltip

* add _breadcrumb_

* properly identify non existing nonce

* fix open cookie banner types after merge

* add isCancellation flag support

* fix expanded tx styles

Co-authored-by: Mati Dastugue <matiasdastugue@gmail.com>
Co-authored-by: Mati Dastugue <mdastugu@amazon.com>
Co-authored-by: Daniel Sanchez <daniel.sanchez@gnosis.pm>
Co-authored-by: nicosampler <nf.dominguez.87@gmail.com>
Co-authored-by: Mati Dastugue <matias.dastugue@altoros.com>
Co-authored-by: Agustin Pane <agustin.pane@gmail.com>
Co-authored-by: nicolas <nicosampler@users.noreply.github.com>
Co-authored-by: Agustín Longoni <agustin.longoni@altoros.com>
This commit is contained in:
Fernando 2021-02-10 13:43:23 -03:00 committed by GitHub
parent 8c50cda0ad
commit 47d20aa645
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
157 changed files with 5383 additions and 941 deletions

View File

@ -161,7 +161,7 @@
"@gnosis.pm/safe-apps-sdk": "1.0.3",
"@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2",
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#420f595",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#8dea3a6",
"@gnosis.pm/util-contracts": "2.0.6",
"@ledgerhq/hw-transport-node-hid-singleton": "5.41.0",
"@material-ui/core": "^4.11.0",
@ -199,9 +199,13 @@
"immutable": "^4.0.0-rc.12",
"js-cookie": "^2.2.1",
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"lodash.memoize": "^4.1.2",
"lodash.merge": "^4.6.2",
"material-ui-search-bar": "^1.0.0",
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
"object-hash": "^2.1.1",
"qrcode.react": "1.0.1",
"query-string": "6.13.8",
"react": "16.13.1",
@ -211,6 +215,7 @@
"react-final-form-listeners": "^1.0.2",
"react-ga": "3.3.0",
"react-hot-loader": "4.13.0",
"react-infinite-scroll-component": "^5.1.0",
"react-qr-reader": "^2.2.1",
"react-redux": "7.2.2",
"react-router-dom": "5.2.0",
@ -240,12 +245,14 @@
"@typechain/web3-v1": "^2.0.0",
"@types/history": "4.6.2",
"@types/jest": "^26.0.16",
"@types/lodash.get": "^4.4.6",
"@types/lodash.memoize": "^4.1.6",
"@types/node": "^14.14.10",
"@types/react": "^16.9.55",
"@types/react-dom": "^16.9.9",
"@types/react-redux": "^7.1.11",
"@types/react-router-dom": "^5.1.6",
"@types/redux-actions": "^2.6.1",
"@types/styled-components": "^5.1.4",
"@typescript-eslint/eslint-plugin": "^4.14.0",
"@typescript-eslint/parser": "^4.14.0",

View File

@ -50,7 +50,7 @@ const Footer = (): React.ReactElement => {
const dispatch = useDispatch()
const openCookiesHandler = () => {
dispatch(openCookieBanner(true))
dispatch(openCookieBanner({ cookieBannerOpen: true }))
}
return (

View File

@ -110,7 +110,7 @@ const CookiesBanner = (): ReactElement => {
async function fetchCookiesFromStorage() {
const cookiesState = await loadFromCookie(COOKIES_KEY)
if (!cookiesState) {
dispatch(openCookieBanner(true))
dispatch(openCookieBanner({ cookieBannerOpen: true }))
} else {
const { acceptedIntercom, acceptedAnalytics, acceptedNecessary } = cookiesState
if (acceptedIntercom === undefined) {
@ -143,7 +143,7 @@ const CookiesBanner = (): ReactElement => {
await saveCookie(COOKIES_KEY, newState, 365)
setShowAnalytics(!isDesktop)
setShowIntercom(true)
dispatch(openCookieBanner(false))
dispatch(openCookieBanner({ cookieBannerOpen: false }))
}
const closeCookiesBannerHandler = async () => {
@ -159,7 +159,7 @@ const CookiesBanner = (): ReactElement => {
if (!localIntercom && isIntercomLoaded()) {
closeIntercom()
}
dispatch(openCookieBanner(false))
dispatch(openCookieBanner({ cookieBannerOpen: false }))
}
if (showAnalytics && !isDesktop) {
@ -254,7 +254,7 @@ const CookiesBanner = (): ReactElement => {
<img
className={classes.intercomImage}
src={IntercomIcon}
onClick={() => dispatch(openCookieBanner(true, true))}
onClick={() => dispatch(openCookieBanner({ cookieBannerOpen: true, intercomAlertDisplayed: true }))}
/>
)}
{!isDesktop && showBanner?.cookieBannerOpen && (

View File

@ -1,3 +1,4 @@
import { Text } from '@gnosis.pm/safe-react-components'
import React from 'react'
import styled from 'styled-components'
@ -8,16 +9,13 @@ const Wrapper = styled.div`
const Icon = styled.img`
max-width: 15px;
max-height: 15px;
`
const Text = styled.span`
margin-left: 5px;
height: 17px;
margin-right: 9px;
`
const CustomIconText = ({ iconUrl, text }: { iconUrl: string; text?: string }) => (
<Wrapper>
<Icon alt={text} src={iconUrl} />
{text && <Text>{text}</Text>}
{text && <Text size="xl">{text}</Text>}
</Wrapper>
)

View File

@ -0,0 +1,39 @@
import { Loader } from '@gnosis.pm/safe-react-components'
import React, { ReactElement } from 'react'
import { default as ReactInfiniteScroll, Props as ReactInfiniteScrollProps } from 'react-infinite-scroll-component'
import styled from 'styled-components'
import { Overwrite } from 'src/types/helpers'
export const Centered = styled.div<{ padding?: number }>`
width: 100%;
height: 100%;
display: flex;
padding: ${({ padding }) => `${padding}px`};
justify-content: center;
align-items: center;
`
export const SCROLLABLE_TARGET_ID = 'scrollableDiv'
type InfiniteScrollProps = Overwrite<ReactInfiniteScrollProps, { loader?: ReactInfiniteScrollProps['loader'] }>
export const InfiniteScroll = ({ dataLength, next, hasMore, ...props }: InfiniteScrollProps): ReactElement => {
return (
<ReactInfiniteScroll
style={{ overflow: 'hidden' }}
dataLength={dataLength}
next={next}
hasMore={hasMore}
loader={
<Centered>
<Loader size="md" />
</Centered>
}
scrollThreshold="120px"
scrollableTarget={SCROLLABLE_TARGET_ID}
>
{props.children}
</ReactInfiniteScroll>
)
}

View File

@ -68,6 +68,8 @@ const configuration = (): NetworkSpecificConfiguration => {
const getConfig: () => NetworkSpecificConfiguration = ensureOnce(configuration)
export const getClientGatewayUrl = (): string => getConfig().clientGatewayUrl
export const getTxServiceUrl = (): string => getConfig().txServiceUrl
export const getRelayUrl = (): string | undefined => getConfig().relayApiUrl
@ -81,6 +83,11 @@ export const getGasPriceOracle = (): GasPriceOracle | undefined => getConfig()?.
export const getRpcServiceUrl = (): string =>
usesInfuraRPC ? `${getConfig().rpcServiceUrl}/${INFURA_TOKEN}` : getConfig().rpcServiceUrl
export const getSafeClientGatewayBaseUrl = (safeAddress: string) => `${getClientGatewayUrl()}/safes/${safeAddress}`
export const getTxDetailsUrl = (clientGatewayTxId: string) =>
`${getClientGatewayUrl()}/transactions/${clientGatewayTxId}`
export const getSafeServiceBaseUrl = (safeAddress: string) => `${getTxServiceUrl()}/safes/${safeAddress}`
export const getTokensServiceBaseUrl = () => `${getTxServiceUrl()}/tokens`

View File

@ -4,6 +4,7 @@ import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig, WALLETS } from 's
// @todo (agustin) we need to use fixed gasPrice because the oracle is not working right now and it's returning 0
// once the oracle is fixed we need to remove the fixed value
const baseConfig: EnvironmentSettings = {
clientGatewayUrl: 'https://safe-client.ewc.gnosis.io/v1',
txServiceUrl: 'https://safe-transaction.ewc.gnosis.io/api/v1',
safeAppsUrl: 'https://safe-apps-ewc.staging.gnosisdev.com',
gasPriceOracle: {

View File

@ -2,6 +2,7 @@ import EtherLogo from 'src/config/assets/token_eth.svg'
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d'
const baseConfig: EnvironmentSettings = {
clientGatewayUrl: 'http://localhost:8001/v1',
txServiceUrl: 'http://localhost:8000/api/v1',
relayApiUrl: 'https://safe-relay.staging.gnosisdev.com/api/v1',
safeAppsUrl: 'http://localhost:3002',

View File

@ -2,6 +2,7 @@ import EtherLogo from 'src/config/assets/token_eth.svg'
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d'
const baseConfig: EnvironmentSettings = {
clientGatewayUrl: 'https://safe-client.mainnet.staging.gnosisdev.com/v1',
txServiceUrl: 'https://safe-transaction.mainnet.staging.gnosisdev.com/api/v1',
safeAppsUrl: 'https://safe-apps.dev.gnosisdev.com',
gasPriceOracle: {
@ -25,6 +26,7 @@ const mainnet: NetworkConfig = {
},
production: {
...baseConfig,
clientGatewayUrl: 'https://safe-client.mainnet.gnosis.io/v1',
txServiceUrl: 'https://safe-transaction.mainnet.gnosis.io/api/v1',
safeAppsUrl: 'https://apps.gnosis-safe.io',
},

View File

@ -85,8 +85,9 @@ type GasPrice =
}
export type EnvironmentSettings = GasPrice & {
clientGatewayUrl: string
txServiceUrl: string
// Shall we keep a reference to the relay?
// TODO: Shall we keep a reference to the relay?
relayApiUrl?: string
safeAppsUrl: string
rpcServiceUrl: string

View File

@ -2,6 +2,7 @@ import EtherLogo from 'src/config/assets/token_eth.svg'
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d'
const baseConfig: EnvironmentSettings = {
clientGatewayUrl: 'https://safe-client.rinkeby.staging.gnosisdev.com/v1',
txServiceUrl: 'https://safe-transaction.staging.gnosisdev.com/api/v1',
safeAppsUrl: 'https://safe-apps.dev.gnosisdev.com',
gasPriceOracle: {
@ -25,6 +26,7 @@ const rinkeby: NetworkConfig = {
},
production: {
...baseConfig,
clientGatewayUrl: 'https://safe-client.rinkeby.gnosis.io/v1',
txServiceUrl: 'https://safe-transaction.rinkeby.gnosis.io/api/v1',
safeAppsUrl: 'https://apps.gnosis-safe.io',
},

View File

@ -2,6 +2,7 @@ import EwcLogo from 'src/config/assets/token_ewc.svg'
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig, WALLETS } from 'src/config/networks/network.d'
const baseConfig: EnvironmentSettings = {
clientGatewayUrl: 'https://safe-client.volta.gnosis.io/v1',
txServiceUrl: 'https://safe-transaction.volta.gnosis.io/api/v1',
safeAppsUrl: 'https://safe-apps-volta.staging.gnosisdev.com',
gasPriceOracle: {

View File

@ -2,6 +2,7 @@ import xDaiLogo from 'src/config/assets/token_xdai.svg'
import { EnvironmentSettings, ETHEREUM_NETWORK, FEATURES, NetworkConfig, WALLETS } from 'src/config/networks/network.d'
const baseConfig: EnvironmentSettings = {
clientGatewayUrl: 'https://safe-client.xdai.gnosis.io/v1',
txServiceUrl: 'https://safe-transaction.xdai.gnosis.io/api/v1',
safeAppsUrl: 'https://safe-apps-xdai.staging.gnosisdev.com',
gasPrice: 1e9,

View File

@ -9,7 +9,7 @@ type addAddressBookEntryOptions = {
export const addAddressBookEntry = createAction(
ADD_ENTRY,
(entry: AddressBookEntry, options: addAddressBookEntryOptions) => {
(entry: AddressBookEntry, options?: addAddressBookEntryOptions) => {
let notifyEntryUpdate = true
if (options) {
notifyEntryUpdate = options.notifyEntryUpdate

View File

@ -1,11 +1,12 @@
import { handleActions } from 'redux-actions'
import { Action, handleActions } from 'redux-actions'
import { AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { ADD_ENTRY } from 'src/logic/addressBook/store/actions/addAddressBookEntry'
import { ADD_OR_UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
import { LOAD_ADDRESS_BOOK } from 'src/logic/addressBook/store/actions/loadAddressBook'
import { REMOVE_ENTRY } from 'src/logic/addressBook/store/actions/removeAddressBookEntry'
import { UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
import { AppReduxState } from 'src/store'
import { checksumAddress } from 'src/utils/checksumAddress'
import { getValidAddressBookName } from 'src/logic/addressBook/utils'
@ -18,13 +19,19 @@ export const buildAddressBook = (storedAddressBook: AddressBookState): AddressBo
})
}
export default handleActions(
type AddressBookPayload = { addressBook: AddressBookState }
type EntryPayload = { entry: AddressBookEntry }
type RemoveEntryPayload = { entryAddress: string }
type Payloads = AddressBookPayload | EntryPayload | RemoveEntryPayload
export default handleActions<AppReduxState['addressBook'], Payloads>(
{
[LOAD_ADDRESS_BOOK]: (state, action) => {
[LOAD_ADDRESS_BOOK]: (state, action: Action<AddressBookPayload>) => {
const { addressBook } = action.payload
return addressBook
},
[ADD_ENTRY]: (state, action) => {
[ADD_ENTRY]: (state, action: Action<EntryPayload>) => {
const { entry } = action.payload
const entryFound = state.find((oldEntry) => oldEntry.address === entry.address)
@ -34,7 +41,7 @@ export default handleActions(
}
return state
},
[UPDATE_ENTRY]: (state, action) => {
[UPDATE_ENTRY]: (state, action: Action<EntryPayload>) => {
const { entry } = action.payload
const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entry.address)
if (entryIndex >= 0) {
@ -42,13 +49,13 @@ export default handleActions(
}
return state
},
[REMOVE_ENTRY]: (state, action) => {
[REMOVE_ENTRY]: (state, action: Action<RemoveEntryPayload>) => {
const { entryAddress } = action.payload
const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entryAddress)
state.splice(entryIndex, 1)
return state
},
[ADD_OR_UPDATE_ENTRY]: (state, action) => {
[ADD_OR_UPDATE_ENTRY]: (state, action: Action<EntryPayload>) => {
const { entry } = action.payload
// Only updates entries with valid names

View File

@ -1,11 +1,15 @@
import { handleActions } from 'redux-actions'
import { ADD_NFT_ASSETS, ADD_NFT_TOKENS } from 'src/logic/collectibles/store/actions/addCollectibles'
import { NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/collectibles'
import { AppReduxState } from 'src/store'
export const NFT_ASSETS_REDUCER_ID = 'nftAssets'
export const NFT_TOKENS_REDUCER_ID = 'nftTokens'
export const nftAssetReducer = handleActions(
type NFTAssetsPayload = { nftAssets: NFTAssets }
export const nftAssetReducer = handleActions<AppReduxState['nftAssets'], NFTAssetsPayload>(
{
[ADD_NFT_ASSETS]: (state, action) => {
const { nftAssets } = action.payload
@ -16,7 +20,9 @@ export const nftAssetReducer = handleActions(
{},
)
export const nftTokensReducer = handleActions(
type NFTTokensPayload = { nftTokens: NFTTokens }
export const nftTokensReducer = handleActions<AppReduxState['nftTokens'], NFTTokensPayload>(
{
[ADD_NFT_TOKENS]: (state, action) => {
const { nftTokens } = action.payload

View File

@ -1,7 +1,7 @@
import { getNetworkId, getNetworkInfo } from 'src/config'
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
import { nftAssetsListAddressesSelector } from 'src/logic/collectibles/store/selectors'
import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
import { BuildTx, ServiceTx } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { TOKEN_TRANSFER_METHODS_NAMES } from 'src/logic/safe/store/models/types/transactions.d'
import { getERC721TokenContract, getStandardTokenContract } from 'src/logic/tokens/store/actions/fetchTokens'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
@ -31,10 +31,10 @@ export const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '42842e0e'
/**
* Verifies that a tx received by the transaction service is an ERC721 token-related transaction
* @param {TxServiceModel} tx
* @param {BuildTx['tx']} tx
* @returns boolean
*/
export const isSendERC721Transaction = (tx: TxServiceModel): boolean => {
export const isSendERC721Transaction = (tx: BuildTx['tx']): boolean => {
let hasERC721Transfer = false
if (tx.dataDecoded && sameString(tx.dataDecoded.method, TOKEN_TRANSFER_METHODS_NAMES.SAFE_TRANSFER_FROM)) {
@ -44,7 +44,7 @@ export const isSendERC721Transaction = (tx: TxServiceModel): boolean => {
// Note: this is only valid with our current case (client rendering), if we move to server side rendering we need to refactor this
const state = store.getState()
const knownAssets = nftAssetsListAddressesSelector(state)
return knownAssets.includes(tx.to) || hasERC721Transfer
return knownAssets.includes((tx as ServiceTx).to) || hasERC721Transfer
}
/**

View File

@ -25,7 +25,7 @@ const decodeInfo = ({ paramsHash, params }: DecodeInfoProps): DataDecoded['param
}))
}
export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null => {
export const decodeParamsFromSafeMethod = (data: string): DataDecoded | undefined => {
const [methodId, paramsHash] = [data.slice(0, 10), data.slice(10)]
const method = SAFE_METHODS_NAMES[methodId]
@ -99,7 +99,7 @@ export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null =>
}
default:
return null
return
}
}
@ -113,7 +113,7 @@ export const isDeleteAllowanceMethod = (data: string): boolean => {
return sameString(SPENDING_LIMIT_METHOD_ID_TO_NAME[methodId], SPENDING_LIMIT_METHODS_NAMES.DELETE_ALLOWANCE)
}
export const decodeParamsFromSpendingLimit = (data: string): DataDecoded | null => {
export const decodeParamsFromSpendingLimit = (data: string): DataDecoded | undefined => {
const [methodId, paramsHash] = [data.slice(0, 10), data.slice(10)]
const method = SPENDING_LIMIT_METHOD_ID_TO_NAME[methodId]
@ -171,7 +171,7 @@ export const decodeParamsFromSpendingLimit = (data: string): DataDecoded | null
}
default:
return null
return
}
}
@ -183,9 +183,9 @@ const isSpendingLimitMethod = (methodId: string): boolean => {
return !!SPENDING_LIMIT_METHOD_ID_TO_NAME[methodId]
}
export const decodeMethods = (data: string | null): DataDecoded | null => {
export const decodeMethods = (data: string | null): DataDecoded | undefined => {
if (!data?.length) {
return null
return
}
const [methodId, paramsHash] = [data.slice(0, 10), data.slice(10)]
@ -226,6 +226,6 @@ export const decodeMethods = (data: string | null): DataDecoded | null => {
}
default:
return null
return
}
}

View File

@ -1,11 +1,7 @@
import { createAction } from 'redux-actions'
import { OpenCookieBannerPayload } from 'src/logic/cookies/store/reducer/cookies'
export const OPEN_COOKIE_BANNER = 'OPEN_COOKIE_BANNER'
export const openCookieBanner = createAction(
OPEN_COOKIE_BANNER,
(cookieBannerOpen, intercomAlertDisplayed = false) => ({
cookieBannerOpen,
intercomAlertDisplayed,
}),
)
export const openCookieBanner = createAction<OpenCookieBannerPayload>(OPEN_COOKIE_BANNER)

View File

@ -1,12 +1,18 @@
import { Map } from 'immutable'
import { handleActions } from 'redux-actions'
import { OPEN_COOKIE_BANNER } from 'src/logic/cookies/store/actions/openCookieBanner'
import { AppReduxState } from 'src/store'
export const COOKIES_REDUCER_ID = 'cookies'
export default handleActions(
export type OpenCookieBannerPayload = { cookieBannerOpen: boolean; intercomAlertDisplayed?: boolean }
export default handleActions<AppReduxState['cookies'], OpenCookieBannerPayload>(
{
[OPEN_COOKIE_BANNER]: (state, action) => state.set('cookieBannerOpen', action.payload),
[OPEN_COOKIE_BANNER]: (state, action) => {
const { intercomAlertDisplayed = false, cookieBannerOpen } = action.payload
return state.set('cookieBannerOpen', { intercomAlertDisplayed, cookieBannerOpen })
},
},
Map(),
)

View File

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

View File

@ -1,12 +1,14 @@
import { setCurrencyBalances } from 'src/logic/currencyValues/store/actions/setCurrencyBalances'
import { setCurrencyRate } from 'src/logic/currencyValues/store/actions/setCurrencyRate'
import { Action } from 'redux-actions'
import { ThunkDispatch } from 'redux-thunk'
import { setSelectedCurrency } from 'src/logic/currencyValues/store/actions/setSelectedCurrency'
import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues'
import { CurrentCurrencyPayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
import { loadSelectedCurrency } from 'src/logic/currencyValues/store/utils/currencyValuesStorage'
import { Dispatch } from 'redux'
import { AppReduxState } from 'src/store'
export const fetchSelectedCurrency = (safeAddress: string) => async (
dispatch: Dispatch<typeof setCurrencyBalances | typeof setSelectedCurrency | typeof setCurrencyRate>,
dispatch: ThunkDispatch<AppReduxState, undefined, Action<CurrentCurrencyPayload>>,
): Promise<void> => {
try {
const storedSelectedCurrency = await loadSelectedCurrency()

View File

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

View File

@ -1,10 +1,10 @@
import { Map } from 'immutable'
import { handleActions } from 'redux-actions'
import { Action, handleActions } from 'redux-actions'
import { SET_CURRENCY_BALANCES } from 'src/logic/currencyValues/store/actions/setCurrencyBalances'
import { SET_CURRENCY_RATE } from 'src/logic/currencyValues/store/actions/setCurrencyRate'
import { SET_CURRENT_CURRENCY } from 'src/logic/currencyValues/store/actions/setSelectedCurrency'
import { CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues'
import { BalanceCurrencyList, CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues'
export const CURRENCY_VALUES_KEY = 'currencyValues'
@ -15,19 +15,26 @@ export interface CurrencyReducerMap extends Map<string, any> {
export type CurrencyValuesState = Map<string, CurrencyReducerMap>
export default handleActions(
type CurrencyBasePayload = { safeAddress: string }
export type CurrencyRatePayload = CurrencyBasePayload & { currencyRate: number }
export type CurrencyBalancesPayload = CurrencyBasePayload & { currencyBalances: BalanceCurrencyList }
export type CurrentCurrencyPayload = CurrencyBasePayload & { selectedCurrency: string }
export type CurrencyPayloads = CurrencyRatePayload | CurrencyBalancesPayload | CurrentCurrencyPayload
export default handleActions<CurrencyReducerMap, CurrencyPayloads>(
{
[SET_CURRENCY_RATE]: (state: CurrencyReducerMap, action) => {
[SET_CURRENCY_RATE]: (state, action: Action<CurrencyRatePayload>) => {
const { currencyRate, safeAddress } = action.payload
return state.setIn([safeAddress, 'currencyRate'], currencyRate)
},
[SET_CURRENCY_BALANCES]: (state: CurrencyReducerMap, action) => {
const { currencyBalances, safeAddress } = action.payload
[SET_CURRENCY_BALANCES]: (state, action: Action<CurrencyBalancesPayload>) => {
const { safeAddress, currencyBalances } = action.payload
return state.setIn([safeAddress, 'currencyBalances'], currencyBalances)
},
[SET_CURRENT_CURRENCY]: (state: CurrencyReducerMap, action) => {
[SET_CURRENT_CURRENCY]: (state, action: Action<CurrentCurrencyPayload>) => {
const { safeAddress, selectedCurrency } = action.payload
return state.setIn([safeAddress, 'selectedCurrency'], selectedCurrency)

View File

@ -1,8 +1,9 @@
import { handleActions } from 'redux-actions'
import { Action, handleActions } from 'redux-actions'
import { LOAD_CURRENT_SESSION } from 'src/logic/currentSession/store/actions/loadCurrentSession'
import { UPDATE_VIEWED_SAFES } from 'src/logic/currentSession/store/actions/updateViewedSafes'
import { saveCurrentSessionToStorage } from 'src/logic/currentSession/utils'
import { AppReduxState } from 'src/store'
export const CURRENT_SESSION_REDUCER_ID = 'currentSession'
@ -14,13 +15,15 @@ export const initialState = {
viewedSafes: [],
}
export default handleActions(
type CurrentSessionPayloads = CurrentSessionState | string
export default handleActions<AppReduxState['currentSession'], CurrentSessionPayloads>(
{
[LOAD_CURRENT_SESSION]: (state = initialState, action) => ({
[LOAD_CURRENT_SESSION]: (state = initialState, action: Action<CurrentSessionState>) => ({
...state,
...action.payload,
}),
[UPDATE_VIEWED_SAFES]: (state, action) => {
[UPDATE_VIEWED_SAFES]: (state, action: Action<string>) => {
const safeAddress = action.payload
const viewedSafes = state.viewedSafes
const newState = {

View File

@ -155,7 +155,7 @@ type UseEstimateTransactionGasProps = {
manualGasPrice?: string
}
type TransactionGasEstimationResult = {
export type TransactionGasEstimationResult = {
txEstimationExecutionStatus: EstimationStatus
gasEstimation: number // Amount of gas needed for execute or approve the transaction
gasCost: string // Cost of gas in raw format (estimatedGas * gasPrice)

View File

@ -22,18 +22,15 @@ const getStandardTxNotificationsQueue = (
origin: string,
): Record<string, Record<string, Notification> | Notification> => ({
beforeExecution: setNotificationOrigin(NOTIFICATIONS.SIGN_TX_MSG, origin),
pendingExecution: setNotificationOrigin(NOTIFICATIONS.TX_PENDING_MSG, origin),
afterRejection: setNotificationOrigin(NOTIFICATIONS.TX_REJECTED_MSG, origin),
afterExecution: {
noMoreConfirmationsNeeded: setNotificationOrigin(NOTIFICATIONS.TX_EXECUTED_MSG, origin),
moreConfirmationsNeeded: setNotificationOrigin(NOTIFICATIONS.TX_EXECUTED_MORE_CONFIRMATIONS_MSG, origin),
},
afterExecutionError: setNotificationOrigin(NOTIFICATIONS.TX_FAILED_MSG, origin),
})
const waitingTransactionNotificationsQueue = {
beforeExecution: null,
pendingExecution: null,
afterRejection: null,
waitingConfirmation: NOTIFICATIONS.TX_WAITING_MSG,
afterExecution: null,
@ -43,7 +40,6 @@ const waitingTransactionNotificationsQueue = {
const getConfirmationTxNotificationsQueue = (origin: string) => {
return {
beforeExecution: setNotificationOrigin(NOTIFICATIONS.SIGN_TX_MSG, origin),
pendingExecution: setNotificationOrigin(NOTIFICATIONS.TX_CONFIRMATION_PENDING_MSG, origin),
afterRejection: setNotificationOrigin(NOTIFICATIONS.TX_REJECTED_MSG, origin),
afterExecution: {
noMoreConfirmationsNeeded: setNotificationOrigin(NOTIFICATIONS.TX_EXECUTED_MSG, origin),
@ -56,7 +52,6 @@ const getConfirmationTxNotificationsQueue = (origin: string) => {
const getCancellationTxNotificationsQueue = (origin: string) => {
return {
beforeExecution: setNotificationOrigin(NOTIFICATIONS.SIGN_TX_MSG, origin),
pendingExecution: setNotificationOrigin(NOTIFICATIONS.TX_PENDING_MSG, origin),
afterRejection: setNotificationOrigin(NOTIFICATIONS.TX_REJECTED_MSG, origin),
afterExecution: {
noMoreConfirmationsNeeded: setNotificationOrigin(NOTIFICATIONS.TX_EXECUTED_MSG, origin),
@ -68,7 +63,6 @@ const getCancellationTxNotificationsQueue = (origin: string) => {
const safeNameChangeNotificationsQueue = {
beforeExecution: null,
pendingExecution: null,
afterRejection: null,
afterExecution: {
noMoreConfirmationsNeeded: NOTIFICATIONS.SAFE_NAME_CHANGED_MSG,
@ -79,7 +73,6 @@ const safeNameChangeNotificationsQueue = {
const ownerNameChangeNotificationsQueue = {
beforeExecution: null,
pendingExecution: null,
afterRejection: null,
afterExecution: {
noMoreConfirmationsNeeded: NOTIFICATIONS.OWNER_NAME_CHANGE_EXECUTED_MSG,
@ -90,7 +83,6 @@ const ownerNameChangeNotificationsQueue = {
const settingsChangeTxNotificationsQueue = {
beforeExecution: NOTIFICATIONS.SIGN_SETTINGS_CHANGE_MSG,
pendingExecution: NOTIFICATIONS.SETTINGS_CHANGE_PENDING_MSG,
afterRejection: NOTIFICATIONS.SETTINGS_CHANGE_REJECTED_MSG,
afterExecution: {
noMoreConfirmationsNeeded: NOTIFICATIONS.SETTINGS_CHANGE_EXECUTED_MSG,
@ -101,7 +93,6 @@ const settingsChangeTxNotificationsQueue = {
const newSpendingLimitTxNotificationsQueue = {
beforeExecution: NOTIFICATIONS.SIGN_NEW_SPENDING_LIMIT_MSG,
pendingExecution: NOTIFICATIONS.NEW_SPENDING_LIMIT_PENDING_MSG,
afterRejection: NOTIFICATIONS.NEW_SPENDING_LIMIT_REJECTED_MSG,
afterExecution: {
noMoreConfirmationsNeeded: NOTIFICATIONS.NEW_SPENDING_LIMIT_EXECUTED_MSG,
@ -112,7 +103,6 @@ const newSpendingLimitTxNotificationsQueue = {
const removeSpendingLimitTxNotificationsQueue = {
beforeExecution: NOTIFICATIONS.SIGN_REMOVE_SPENDING_LIMIT_MSG,
pendingExecution: NOTIFICATIONS.REMOVE_SPENDING_LIMIT_PENDING_MSG,
afterRejection: NOTIFICATIONS.REMOVE_SPENDING_LIMIT_REJECTED_MSG,
afterExecution: {
noMoreConfirmationsNeeded: NOTIFICATIONS.REMOVE_SPENDING_LIMIT_EXECUTED_MSG,
@ -123,18 +113,15 @@ const removeSpendingLimitTxNotificationsQueue = {
const defaultNotificationsQueue = {
beforeExecution: NOTIFICATIONS.SIGN_TX_MSG,
pendingExecution: NOTIFICATIONS.TX_PENDING_MSG,
afterRejection: NOTIFICATIONS.TX_REJECTED_MSG,
afterExecution: {
noMoreConfirmationsNeeded: NOTIFICATIONS.TX_EXECUTED_MSG,
moreConfirmationsNeeded: NOTIFICATIONS.TX_EXECUTED_MORE_CONFIRMATIONS_MSG,
},
afterExecutionError: NOTIFICATIONS.TX_FAILED_MSG,
}
const addressBookNewEntry = {
beforeExecution: null,
pendingExecution: null,
afterRejection: null,
waitingConfirmation: null,
afterExecution: {
@ -146,7 +133,6 @@ const addressBookNewEntry = {
const addressBookEditEntry = {
beforeExecution: null,
pendingExecution: null,
afterRejection: null,
waitingConfirmation: null,
afterExecution: {
@ -158,7 +144,6 @@ const addressBookEditEntry = {
const addressBookDeleteEntry = {
beforeExecution: null,
pendingExecution: null,
afterRejection: null,
waitingConfirmation: null,
afterExecution: {
@ -231,7 +216,7 @@ export const getNotificationsFromTxType: any = (txType, origin) => {
export const enhanceSnackbarForAction = (
notification: Notification,
key?: number | string,
key?: string,
onClick?: () => void,
): Notification => ({
...notification,

View File

@ -15,45 +15,35 @@ export type NotificationId = keyof typeof NOTIFICATION_IDS
export type Notification = {
message: string
options: OptionsObject
key?: number | string
key?: string
dismissed?: boolean
}
const NOTIFICATION_IDS = {
CONNECT_WALLET_MSG: 'CONNECT_WALLET_MSG',
CONNECT_WALLET_READ_MODE_MSG: 'CONNECT_WALLET_READ_MODE_MSG',
WALLET_CONNECTED_MSG: 'WALLET_CONNECTED_MSG',
WALLET_DISCONNECTED_MSG: 'WALLET_DISCONNECTED_MSG',
UNLOCK_WALLET_MSG: 'UNLOCK_WALLET_MSG',
CONNECT_WALLET_ERROR_MSG: 'CONNECT_WALLET_ERROR_MSG',
SIGN_TX_MSG: 'SIGN_TX_MSG',
TX_PENDING_MSG: 'TX_PENDING_MSG',
TX_REJECTED_MSG: 'TX_REJECTED_MSG',
TX_EXECUTED_MSG: 'TX_EXECUTED_MSG',
TX_CANCELLATION_EXECUTED_MSG: 'TX_CANCELLATION_EXECUTED_MSG',
TX_FAILED_MSG: 'TX_FAILED_MSG',
TX_EXECUTED_MORE_CONFIRMATIONS_MSG: 'TX_EXECUTED_MORE_CONFIRMATIONS_MSG',
TX_WAITING_MSG: 'TX_WAITING_MSG',
TX_INCOMING_MSG: 'TX_INCOMING_MSG',
TX_CONFIRMATION_PENDING_MSG: 'TX_CONFIRMATION_PENDING_MSG',
TX_CONFIRMATION_EXECUTED_MSG: 'TX_CONFIRMATION_EXECUTED_MSG',
TX_CONFIRMATION_FAILED_MSG: 'TX_CONFIRMATION_FAILED_MSG',
SAFE_NAME_CHANGED_MSG: 'SAFE_NAME_CHANGED_MSG',
OWNER_NAME_CHANGE_EXECUTED_MSG: 'OWNER_NAME_CHANGE_EXECUTED_MSG',
SIGN_SETTINGS_CHANGE_MSG: 'SIGN_SETTINGS_CHANGE_MSG',
SETTINGS_CHANGE_PENDING_MSG: 'SETTINGS_CHANGE_PENDING_MSG',
SETTINGS_CHANGE_REJECTED_MSG: 'SETTINGS_CHANGE_REJECTED_MSG',
SETTINGS_CHANGE_EXECUTED_MSG: 'SETTINGS_CHANGE_EXECUTED_MSG',
SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG: 'SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG',
SETTINGS_CHANGE_FAILED_MSG: 'SETTINGS_CHANGE_FAILED_MSG',
TESTNET_VERSION_MSG: 'TESTNET_VERSION_MSG',
SIGN_NEW_SPENDING_LIMIT_MSG: 'SIGN_NEW_SPENDING_LIMIT_MSG',
NEW_SPENDING_LIMIT_PENDING_MSG: 'NEW_SPENDING_LIMIT_PENDING_MSG',
NEW_SPENDING_LIMIT_REJECTED_MSG: 'NEW_SPENDING_LIMIT_REJECTED_MSG',
NEW_SPENDING_LIMIT_EXECUTED_MSG: 'NEW_SPENDING_LIMIT_EXECUTED_MSG',
NEW_SPENDING_LIMIT_EXECUTED_MORE_CONFIRMATIONS_MSG: 'NEW_SPENDING_LIMIT_EXECUTED_MORE_CONFIRMATIONS_MSG',
NEW_SPENDING_LIMIT_FAILED_MSG: 'NEW_SPENDING_LIMIT_FAILED_MSG',
SIGN_REMOVE_SPENDING_LIMIT_MSG: 'SIGN_REMOVE_SPENDING_LIMIT_MSG',
REMOVE_SPENDING_LIMIT_PENDING_MSG: 'REMOVE_SPENDING_LIMIT_PENDING_MSG',
REMOVE_SPENDING_LIMIT_REJECTED_MSG: 'REMOVE_SPENDING_LIMIT_REJECTED_MSG',
REMOVE_SPENDING_LIMIT_EXECUTED_MSG: 'REMOVE_SPENDING_LIMIT_EXECUTED_MSG',
REMOVE_SPENDING_LIMIT_EXECUTED_MORE_CONFIRMATIONS_MSG: 'REMOVE_SPENDING_LIMIT_EXECUTED_MORE_CONFIRMATIONS_MSG',
@ -67,32 +57,6 @@ const NOTIFICATION_IDS = {
export const NOTIFICATIONS: Record<NotificationId, Notification> = {
// Wallet Connection
CONNECT_WALLET_MSG: {
message: 'Please connect wallet to continue',
options: { variant: WARNING, persist: true, preventDuplicate: true },
},
CONNECT_WALLET_READ_MODE_MSG: {
message: 'You are in read-only mode: Please connect wallet',
options: { variant: WARNING, persist: true, preventDuplicate: true },
},
WALLET_CONNECTED_MSG: {
message: 'Wallet connected',
options: {
variant: SUCCESS,
persist: false,
autoHideDuration: shortDuration,
},
},
WALLET_DISCONNECTED_MSG: {
message: 'Wallet disconnected',
key: 'WALLET_DISCONNECTED_MSG',
options: {
variant: SUCCESS,
persist: false,
autoHideDuration: shortDuration,
preventDuplicate: true,
},
},
UNLOCK_WALLET_MSG: {
message: 'Unlock your wallet to connect',
options: { variant: WARNING, persist: true, preventDuplicate: true },
@ -107,62 +71,40 @@ export const NOTIFICATIONS: Record<NotificationId, Notification> = {
message: 'Please sign the transaction',
options: { variant: INFO, persist: true },
},
TX_PENDING_MSG: {
message: 'Transaction pending',
options: { variant: INFO, persist: true },
},
TX_REJECTED_MSG: {
message: 'Transaction rejected',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
options: { variant: ERROR, persist: false, autoHideDuration: shortDuration },
},
TX_EXECUTED_MSG: {
message: 'Transaction successfully executed',
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
options: { variant: SUCCESS, persist: false, autoHideDuration: shortDuration },
},
TX_CANCELLATION_EXECUTED_MSG: {
message: 'Rejection successfully submitted',
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
},
TX_EXECUTED_MORE_CONFIRMATIONS_MSG: {
message: 'Transaction successfully created. More confirmations needed to execute',
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
options: { variant: SUCCESS, persist: false, autoHideDuration: shortDuration },
},
TX_FAILED_MSG: {
message: 'Transaction failed',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
options: { variant: ERROR, persist: false, autoHideDuration: shortDuration },
},
TX_WAITING_MSG: {
message: 'A pending transaction requires your confirmation!',
message: 'A transaction requires your confirmation',
key: 'TX_WAITING_MSG',
options: {
variant: WARNING,
persist: true,
preventDuplicate: true,
},
},
TX_INCOMING_MSG: {
message: 'Incoming transfer: ',
key: 'TX_INCOMING_MSG',
options: {
variant: SUCCESS,
persist: false,
autoHideDuration: longDuration,
autoHideDuration: shortDuration,
preventDuplicate: true,
},
},
// Approval Transactions
TX_CONFIRMATION_PENDING_MSG: {
message: 'Confirmation transaction pending',
options: { variant: INFO, persist: true },
},
TX_CONFIRMATION_EXECUTED_MSG: {
message: 'Confirmation transaction was successful',
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
options: { variant: SUCCESS, persist: false, autoHideDuration: shortDuration },
},
TX_CONFIRMATION_FAILED_MSG: {
message: 'Confirmation transaction failed',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
options: { variant: ERROR, persist: false, autoHideDuration: shortDuration },
},
// Safe Name
@ -182,17 +124,13 @@ export const NOTIFICATIONS: Record<NotificationId, Notification> = {
message: 'Please sign the settings change',
options: { variant: INFO, persist: true },
},
SETTINGS_CHANGE_PENDING_MSG: {
message: 'Settings change pending',
options: { variant: INFO, persist: true },
},
SETTINGS_CHANGE_REJECTED_MSG: {
message: 'Settings change rejected',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
options: { variant: ERROR, persist: false, autoHideDuration: shortDuration },
},
SETTINGS_CHANGE_EXECUTED_MSG: {
message: 'Settings change successfully executed',
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
options: { variant: SUCCESS, persist: false, autoHideDuration: shortDuration },
},
SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG: {
message: 'Settings change successfully created. More confirmations needed to execute',
@ -200,7 +138,7 @@ export const NOTIFICATIONS: Record<NotificationId, Notification> = {
},
SETTINGS_CHANGE_FAILED_MSG: {
message: 'Settings change failed',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
options: { variant: ERROR, persist: false, autoHideDuration: shortDuration },
},
// Spending Limit
@ -208,10 +146,6 @@ export const NOTIFICATIONS: Record<NotificationId, Notification> = {
message: 'Please sign the new Spending Limit',
options: { variant: INFO, persist: true },
},
NEW_SPENDING_LIMIT_PENDING_MSG: {
message: 'New Spending Limit pending',
options: { variant: INFO, persist: true },
},
NEW_SPENDING_LIMIT_REJECTED_MSG: {
message: 'New Spending Limit rejected',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
@ -232,10 +166,6 @@ export const NOTIFICATIONS: Record<NotificationId, Notification> = {
message: 'Please sign the remove Spending Limit',
options: { variant: INFO, persist: true },
},
REMOVE_SPENDING_LIMIT_PENDING_MSG: {
message: 'Remove Spending Limit pending',
options: { variant: INFO, persist: true },
},
REMOVE_SPENDING_LIMIT_REJECTED_MSG: {
message: 'Remove Spending Limit rejected',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
@ -256,7 +186,7 @@ export const NOTIFICATIONS: Record<NotificationId, Notification> = {
// Network
TESTNET_VERSION_MSG: {
message: "Testnet Version: Don't send production assets to this Safe",
options: { variant: WARNING, persist: true, preventDuplicate: true },
options: { variant: WARNING, persist: false, preventDuplicate: true, autoHideDuration: longDuration },
},
WRONG_NETWORK_MSG: {
message: `Wrong network: Please use ${getNetworkName()}`,

View File

@ -1,8 +0,0 @@
import { Record } from 'immutable'
export const makeNotification = Record({
key: 0,
message: '',
options: {},
dismissed: false,
})

View File

@ -1,38 +1,55 @@
import { Map } from 'immutable'
import { handleActions } from 'redux-actions'
import { Action, handleActions } from 'redux-actions'
import { Notification } from 'src/logic/notifications/notificationTypes'
import { AppReduxState } from 'src/store'
import { CLOSE_SNACKBAR } from '../actions/closeSnackbar'
import { ENQUEUE_SNACKBAR } from '../actions/enqueueSnackbar'
import { REMOVE_SNACKBAR } from '../actions/removeSnackbar'
import { makeNotification } from 'src/logic/notifications/store/models/notification'
export const NOTIFICATIONS_REDUCER_ID = 'notifications'
export default handleActions(
type CloseSnackBarPayload = { key: string; dismissAll: boolean }
type Payloads = Notification | CloseSnackBarPayload | string
export default handleActions<AppReduxState['notifications'], Payloads>(
{
[ENQUEUE_SNACKBAR]: (state, action) => {
[ENQUEUE_SNACKBAR]: (state, action: Action<Notification>) => {
const notification = action.payload
return state.set(notification.key, makeNotification(notification))
if (!notification.key) {
return state
}
return state.set(notification.key, notification)
},
[CLOSE_SNACKBAR]: (state, action) => {
[CLOSE_SNACKBAR]: (state, action: Action<CloseSnackBarPayload>) => {
const { dismissAll, key } = action.payload
if (key) {
return state.update(key, (prev) => prev?.set('dismissed', true))
if (key && state.get(key)) {
return state.update(key, (notification) => {
if (notification) {
return {
...notification,
dismissed: true,
}
}
return notification
})
}
if (dismissAll) {
return state.withMutations((map) => {
map.forEach((notification, notificationKey) => {
map.set(notificationKey, notification.set('dismissed', true))
map.set(notificationKey, { ...notification, dismissed: true })
})
})
}
return state
},
[REMOVE_SNACKBAR]: (state, action) => {
[REMOVE_SNACKBAR]: (state, action: Action<string>) => {
const key = action.payload
return state.delete(key)

View File

@ -90,7 +90,7 @@ describe('isInnerTransaction', () => {
})
})
describe('isCancelTransaction', () => {
describe.skip('isCancelTransaction', () => {
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
const mockedETHAccount = '0xd76e0B566e218a80F4c96458FE09a322EBAa9aF2'
it('It should return false if given a inner transaction with empty data', () => {

View File

@ -12,7 +12,6 @@ export const buildOwnersFrom = (names: string[], addresses: string[]): List<Safe
return List(owners)
}
export const addOrUpdateSafe = createAction(ADD_OR_UPDATE_SAFE, (safe: SafeRecordProps, loadedFromStorage = false) => ({
export const addOrUpdateSafe = createAction(ADD_OR_UPDATE_SAFE, (safe: SafeRecordProps) => ({
safe,
loadedFromStorage,
}))

View File

@ -2,7 +2,6 @@ import { push } from 'connected-react-router'
import { ThunkAction } from 'redux-thunk'
import { onboardUser } from 'src/components/ConnectButton'
import { decodeMethods } from 'src/logic/contracts/methodIds'
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { getNotificationsFromTxType } from 'src/logic/notifications'
import {
@ -20,16 +19,7 @@ import { providerSelector } from 'src/logic/wallets/store/selectors'
import { SAFELIST_ADDRESS } from 'src/routes/routes'
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
import {
removeTxFromStore,
storeSignedTx,
storeExecutedTx,
} from 'src/logic/safe/store/actions/transactions/pendingTransactions'
import {
generateSafeTxHash,
mockTransaction,
TxToMock,
} from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { generateSafeTxHash } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils'
import { getErrorMessage } from 'src/test/utils/ethereumErrors'
import fetchTransactions from './transactions/fetchTransactions'
@ -60,7 +50,7 @@ type ConfirmEventHandler = (safeTxHash: string) => void
type ErrorEventHandler = () => void
export const METAMASK_REJECT_CONFIRM_TX_ERROR_CODE = 4001
const createTransaction = (
export const createTransaction = (
{
safeAddress,
to,
@ -105,8 +95,6 @@ const createTransaction = (
const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, origin)
const beforeExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.beforeExecution))
let pendingExecutionKey
let txHash
const txArgs: TxArgs = {
safeInstance,
@ -124,13 +112,13 @@ const createTransaction = (
sigs,
}
const safeTxHash = generateSafeTxHash(safeAddress, txArgs)
try {
if (checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion)) {
const signature = await tryOffchainSigning(safeTxHash, { ...txArgs, safeAddress }, hardwareWallet)
if (signature) {
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded))
dispatch(fetchTransactions(safeAddress))
await saveTxToHistory({ ...txArgs, signature, origin })
@ -148,57 +136,28 @@ const createTransaction = (
nonce: ethParameters?.ethNonce,
}
const txToMock: TxToMock = {
...txArgs,
confirmations: [], // this is used to determine if a tx is pending or not. See `calculateTransactionStatus` helper
value: txArgs.valueInWei,
safeTxHash,
dataDecoded: decodeMethods(txArgs.data),
submissionDate: new Date().toISOString(),
}
const mockedTx = await mockTransaction(txToMock, safeAddress, state)
await tx
.send(sendParams)
.once('transactionHash', async (hash) => {
onUserConfirm?.(safeTxHash)
try {
txHash = hash
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
pendingExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.pendingExecution))
await saveTxToHistory({ ...txArgs, txHash, origin })
await Promise.all([
saveTxToHistory({ ...txArgs, txHash, origin }),
storeSignedTx({ transaction: mockedTx, from, isExecution, safeAddress, dispatch, state }),
])
dispatch(fetchTransactions(safeAddress))
} catch (e) {
removeTxFromStore(mockedTx, safeAddress, dispatch, state)
}
})
.on('error', (error) => {
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
removeTxFromStore(mockedTx, safeAddress, dispatch, state)
console.error('Tx error: ', error)
onError?.()
})
.then(async (receipt) => {
if (pendingExecutionKey) {
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
if (isExecution) {
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.noMoreConfirmationsNeeded))
}
dispatch(
enqueueSnackbar(
isExecution
? notificationsQueue.afterExecution.noMoreConfirmationsNeeded
: notificationsQueue.afterExecution.moreConfirmationsNeeded,
),
)
await storeExecutedTx({ transaction: mockedTx, from, safeAddress, isExecution, receipt, dispatch, state })
dispatch(fetchTransactions(safeAddress))
return receipt.transactionHash
@ -210,10 +169,6 @@ const createTransaction = (
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
if (pendingExecutionKey) {
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
}
dispatch(enqueueSnackbar({ key: err.code, message: errorMsg, options: { persist: true, variant: 'error' } }))
if (err.code !== METAMASK_REJECT_CONFIRM_TX_ERROR_CODE) {
@ -227,5 +182,3 @@ const createTransaction = (
return txHash
}
export default createTransaction

View File

@ -0,0 +1,41 @@
import axios, { AxiosResponse } from 'axios'
import { createAction } from 'redux-actions'
import { getTxDetailsUrl } from 'src/config'
import { Dispatch } from 'src/logic/safe/store/actions/types'
import { ExpandedTxDetails, Transaction, TxLocation } from 'src/logic/safe/store/models/types/gateway.d'
import { TransactionDetailsPayload } from 'src/logic/safe/store/reducer/gatewayTransactions'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { getTransactionDetails } from 'src/logic/safe/store/selectors/gatewayTransactions'
import { AppReduxState } from 'src/store'
export const UPDATE_TRANSACTION_DETAILS = 'UPDATE_TRANSACTION_DETAILS'
const updateTransactionDetails = createAction<TransactionDetailsPayload>(UPDATE_TRANSACTION_DETAILS)
export const fetchTransactionDetails = ({
transactionId,
txLocation,
}: {
transactionId: Transaction['id']
txLocation: TxLocation
}) => async (dispatch: Dispatch, getState: () => AppReduxState): Promise<Transaction['txDetails']> => {
const txDetails = getTransactionDetails(getState())({
attributeValue: transactionId,
attributeName: 'id',
txLocation,
})
const safeAddress = safeParamAddressFromStateSelector(getState())
if (txDetails) {
return
}
try {
const url = getTxDetailsUrl(transactionId)
const { data: transactionDetails } = await axios.get<ExpandedTxDetails, AxiosResponse<ExpandedTxDetails>>(url)
dispatch(updateTransactionDetails({ transactionId, txLocation, safeAddress, value: transactionDetails }))
} catch (error) {
console.error(`Failed to retrieve transaction ${transactionId} details`, error.message)
}
}

View File

@ -13,7 +13,7 @@ const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise<void> =>
if (safes) {
Object.values(safes).forEach((safeProps) => {
dispatch(addOrUpdateSafe(buildSafe(safeProps), true))
dispatch(addOrUpdateSafe(buildSafe(safeProps)))
})
}
} catch (err) {

View File

@ -1,3 +1,4 @@
import { List } from 'immutable'
import { AnyAction } from 'redux'
import { ThunkAction } from 'redux-thunk'
@ -17,22 +18,38 @@ import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackb
import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
import fetchSafe from 'src/logic/safe/store/actions/fetchSafe'
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
import { mockTransaction, TxToMock } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils'
import { AppReduxState } from 'src/store'
import { getErrorMessage } from 'src/test/utils/ethereumErrors'
import { storeExecutedTx, storeSignedTx, storeTx } from 'src/logic/safe/store/actions/transactions/pendingTransactions'
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
import { Dispatch, DispatchReturn } from './types'
import { PayableTx } from 'src/types/contracts/types'
import { updateTransactionStatus } from 'src/logic/safe/store/actions/updateTransactionStatus'
import { Confirmation } from 'src/logic/safe/store/models/types/confirmation'
import { Operation } from 'src/logic/safe/store/models/types/gateway.d'
interface ProcessTransactionArgs {
approveAndExecute: boolean
notifiedTransaction: string
safeAddress: string
tx: Transaction
tx: {
id: string
confirmations: List<Confirmation>
origin: string // json.stringified url, name
to: string
value: string
data: string
operation: Operation
nonce: number
safeTxGas: number
safeTxHash: string
baseGas: number
gasPrice: string
gasToken: string
refundReceiver: string
}
userAddress: string
ethParameters?: Pick<TxParameters, 'ethNonce' | 'ethGasLimit' | 'ethGasPriceInGWei'>
thresholdReached: boolean
@ -71,14 +88,13 @@ export const processTransaction = ({
const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, tx.origin)
const beforeExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.beforeExecution))
let pendingExecutionKey
let txHash
let transaction
const txArgs = {
...tx.toJS(), // merge the previous tx with new data
...tx, // merge the previous tx with new data
safeInstance,
to: tx.recipient,
to: tx.to,
valueInWei: tx.value,
data: tx.data ?? EMPTY_DATA,
operation: tx.operation,
@ -99,10 +115,10 @@ export const processTransaction = ({
if (signature) {
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
dispatch(updateTransactionStatus({ txStatus: 'PENDING', safeAddress, nonce: tx.nonce, id: tx.id }))
await saveTxToHistory({ ...txArgs, signature })
// TODO: while we wait for the tx to be stored in the service and later update the tx info
// we should update the tx status in the store to disable owners' action buttons
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded))
dispatch(fetchTransactions(safeAddress))
return
@ -119,52 +135,47 @@ export const processTransaction = ({
nonce: ethParameters?.ethNonce,
}
const txToMock: TxToMock = {
...txArgs,
value: txArgs.valueInWei,
}
const mockedTx = await mockTransaction(txToMock, safeAddress, state)
await transaction
.send(sendParams)
.once('transactionHash', async (hash: string) => {
txHash = hash
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
pendingExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.pendingExecution))
dispatch(
updateTransactionStatus({
txStatus: 'PENDING',
safeAddress,
nonce: tx.nonce,
// if we provide the tx ID that sole tx will have the _pending_ status.
// if not, all the txs that share the same nonce will have the _pending_ status.
id: tx.id,
}),
)
try {
await Promise.all([
saveTxToHistory({ ...txArgs, txHash }),
storeSignedTx({ transaction: mockedTx, from, isExecution, safeAddress, dispatch, state }),
])
await saveTxToHistory({ ...txArgs, txHash })
dispatch(fetchTransactions(safeAddress))
} catch (e) {
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
await storeTx({ transaction: tx, safeAddress, dispatch, state })
console.error(e)
}
})
.on('error', (error) => {
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
storeTx({ transaction: tx, safeAddress, dispatch, state })
dispatch(
updateTransactionStatus({
txStatus: 'PENDING_FAILED',
safeAddress,
nonce: tx.nonce,
id: tx.id,
}),
)
console.error('Processing transaction error: ', error)
})
.then(async (receipt) => {
if (pendingExecutionKey) {
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
if (isExecution) {
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.noMoreConfirmationsNeeded))
}
dispatch(
enqueueSnackbar(
isExecution
? notificationsQueue.afterExecution.noMoreConfirmationsNeeded
: notificationsQueue.afterExecution.moreConfirmationsNeeded,
),
)
await storeExecutedTx({ transaction: mockedTx, from, safeAddress, isExecution, receipt, dispatch, state })
dispatch(fetchTransactions(safeAddress))
if (isExecution) {
@ -180,10 +191,14 @@ export const processTransaction = ({
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
if (pendingExecutionKey) {
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
}
dispatch(
updateTransactionStatus({
txStatus: 'PENDING_FAILED',
safeAddress,
nonce: tx.nonce,
id: tx.id,
}),
)
dispatch(enqueueSnackbar({ key: err.code, message: errorMsg, options: { persist: true, variant: 'error' } }))
if (txHash) {

View File

@ -1,58 +1,35 @@
import { batch } from 'react-redux'
import { ThunkAction, ThunkDispatch } from 'redux-thunk'
import { ThunkDispatch } from 'redux-thunk'
import { AnyAction } from 'redux'
import { backOff } from 'exponential-backoff'
import { addIncomingTransactions } from 'src/logic/safe/store/actions/addIncomingTransactions'
import { addModuleTransactions } from 'src/logic/safe/store/actions/addModuleTransactions'
import { loadIncomingTransactions } from './loadIncomingTransactions'
import { loadModuleTransactions } from './loadModuleTransactions'
import { loadOutgoingTransactions } from './loadOutgoingTransactions'
import { addOrUpdateCancellationTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateCancellationTransactions'
import { addOrUpdateTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions'
import {
addHistoryTransactions,
addQueuedTransactions,
} from 'src/logic/safe/store/actions/transactions/gatewayTransactions'
import { loadHistoryTransactions, loadQueuedTransactions } from './loadGatewayTransactions'
import { AppReduxState } from 'src/store'
const noFunc = () => {}
export default (safeAddress: string): ThunkAction<Promise<void>, AppReduxState, undefined, AnyAction> => async (
export default (safeAddress: string) => async (
dispatch: ThunkDispatch<AppReduxState, undefined, AnyAction>,
): Promise<void> => {
try {
const transactions = await backOff(() => loadOutgoingTransactions(safeAddress))
const [history, queued] = await Promise.allSettled([
loadHistoryTransactions(safeAddress),
loadQueuedTransactions(safeAddress),
])
if (transactions) {
const { cancel, outgoing } = transactions
const updateCancellationTxs = cancel.size
? addOrUpdateCancellationTransactions({ safeAddress, transactions: cancel })
: noFunc
const updateOutgoingTxs = outgoing.size
? addOrUpdateTransactions({
safeAddress,
transactions: outgoing,
})
: noFunc
if (history.status === 'fulfilled') {
const values = history.value
batch(() => {
dispatch(updateCancellationTxs)
dispatch(updateOutgoingTxs)
})
if (values.length) {
dispatch(addHistoryTransactions({ safeAddress, values }))
}
} else {
console.error('Failed to load history transactions', history.reason)
}
const incomingTransactions = await loadIncomingTransactions(safeAddress)
const safeIncomingTxs = incomingTransactions.get(safeAddress)
if (safeIncomingTxs?.size) {
dispatch(addIncomingTransactions(incomingTransactions))
}
const moduleTransactions = await loadModuleTransactions(safeAddress)
if (moduleTransactions.length) {
dispatch(addModuleTransactions({ modules: moduleTransactions, safeAddress }))
}
} catch (error) {
console.log('Error fetching transactions:', error)
if (queued.status === 'fulfilled') {
const values = queued.value
dispatch(addQueuedTransactions({ safeAddress, values }))
} else {
console.error('Failed to load queued transactions', queued.reason)
}
}

View File

@ -0,0 +1,103 @@
import axios, { AxiosResponse } from 'axios'
import { getSafeClientGatewayBaseUrl } from 'src/config'
import { HistoryGatewayResponse, QueuedGatewayResponse } from 'src/logic/safe/store/models/types/gateway'
import { checksumAddress } from 'src/utils/checksumAddress'
/*************/
/* HISTORY */
/*************/
const getHistoryTransactionsUrl = (safeAddress: string): string => {
const address = checksumAddress(safeAddress)
return `${getSafeClientGatewayBaseUrl(address)}/transactions/history/`
}
const historyPointers: { [safeAddress: string]: { next: string | null; previous: string | null } } = {}
/**
* Fetch next page if there is a next pointer for the safeAddress.
* If the fetch was success, updates the pointers.
* @param {string} safeAddress
*/
export const loadPagedHistoryTransactions = async (
safeAddress: string,
): Promise<{ values: HistoryGatewayResponse['results']; next: string | null } | undefined> => {
// if `historyPointers[safeAddress] is `undefined` it means `loadHistoryTransactions` wasn't called
// if `historyPointers[safeAddress].next is `null`, it means it reached the last page in gateway-client
if (!historyPointers[safeAddress]?.next) {
return
}
const {
data: { results, ...pointers },
} = await axios.get<HistoryGatewayResponse, AxiosResponse<HistoryGatewayResponse>>(
historyPointers[safeAddress].next as string,
)
historyPointers[safeAddress] = pointers
return { values: results, next: historyPointers[safeAddress].next }
}
export const loadHistoryTransactions = async (safeAddress: string): Promise<HistoryGatewayResponse['results']> => {
const historyTransactionsUrl = getHistoryTransactionsUrl(safeAddress)
const {
data: { results, ...pointers },
} = await axios.get<HistoryGatewayResponse, AxiosResponse<HistoryGatewayResponse>>(historyTransactionsUrl)
if (!historyPointers[safeAddress]) {
historyPointers[safeAddress] = pointers
}
return results
}
/************/
/* QUEUED */
/************/
const getQueuedTransactionsUrl = (safeAddress: string): string => {
const address = checksumAddress(safeAddress)
return `${getSafeClientGatewayBaseUrl(address)}/transactions/queued/`
}
const queuedPointers: { [safeAddress: string]: { next: string | null; previous: string | null } } = {}
/**
* Fetch next page if there is a next pointer for the safeAddress.
* If the fetch was success, updates the pointers.
* @param {string} safeAddress
*/
export const loadPagedQueuedTransactions = async (
safeAddress: string,
): Promise<{ values: QueuedGatewayResponse['results']; next: string | null } | undefined> => {
// if `queuedPointers[safeAddress] is `undefined` it means `loadHistoryTransactions` wasn't called
// if `queuedPointers[safeAddress].next is `null`, it means it reached the last page in gateway-client
if (!queuedPointers[safeAddress]?.next) {
return
}
const {
data: { results, ...pointers },
} = await axios.get<QueuedGatewayResponse, AxiosResponse<QueuedGatewayResponse>>(
queuedPointers[safeAddress].next as string,
)
queuedPointers[safeAddress] = pointers
return { values: results, next: queuedPointers[safeAddress].next }
}
export const loadQueuedTransactions = async (safeAddress: string): Promise<QueuedGatewayResponse['results']> => {
const queuedTransactionsUrl = getQueuedTransactionsUrl(safeAddress)
const {
data: { results, ...pointers },
} = await axios.get<QueuedGatewayResponse, AxiosResponse<QueuedGatewayResponse>>(queuedTransactionsUrl)
if (!queuedPointers[safeAddress] || queuedPointers[safeAddress].next === null) {
queuedPointers[safeAddress] = pointers
}
return results
}

View File

@ -1,6 +1,7 @@
import { fromJS, List, Map } from 'immutable'
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
import { CancellationTransactions } from 'src/logic/safe/store/reducer/cancellationTransactions'
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
import { PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer/provider'
import { buildTx, isCancelTransaction } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
@ -57,8 +58,8 @@ export type SafeTransactionsType = {
}
export type OutgoingTxs = {
cancellationTxs: Record<number, TxServiceModel>
outgoingTxs: TxServiceModel[]
cancellationTxs: Record<number, TxServiceModel> | CancellationTransactions
outgoingTxs: TxServiceModel[] | List<Transaction>
}
export type BatchProcessTxsProps = OutgoingTxs & {
@ -94,21 +95,25 @@ const extractCancelAndOutgoingTxs = (safeAddress: string, outgoingTxs: TxService
)
}
type BatchRequestReturnValues = [TxServiceModel, string | undefined]
type BatchRequestReturnValues = [TxServiceModel | Transaction, string | undefined]
/**
* Requests Contract's code for all the Contracts the Safe has interacted with
* @param transactions
* @returns {Promise<[Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>]>}
*/
const batchRequestContractCode = (transactions: TxServiceModel[]): Promise<BatchRequestReturnValues[]> => {
const batchRequestContractCode = (
transactions: (TxServiceModel | Transaction)[],
): Promise<BatchRequestReturnValues[]> => {
if (!transactions || !Array.isArray(transactions)) {
throw new Error('`transactions` must be provided in order to lookup information')
}
const batch = new web3ReadOnly.BatchRequest()
const whenTxsValues = transactions.map((tx) => {
// this will no longer be used when txs-list-v2 feature is finished
// that's why I'm doing this to move forward
const whenTxsValues = (transactions as any[]).map((tx) => {
return generateBatchRequests<BatchRequestReturnValues>({
abi: [],
address: tx.to,
@ -142,8 +147,8 @@ const batchProcessOutgoingTransactions = async ({
outgoing: Transaction[]
}> => {
// cancellation transactions
const cancelTxsValues = Object.values(cancellationTxs)
const cancellationTxsWithData = cancelTxsValues.length ? await batchRequestContractCode(cancelTxsValues) : []
const cancelTxsValues = List(Object.values(cancellationTxs))
const cancellationTxsWithData = cancelTxsValues.size ? await batchRequestContractCode(cancelTxsValues.toArray()) : []
const cancel = {}
for (const [tx] of cancellationTxsWithData) {
@ -157,7 +162,11 @@ const batchProcessOutgoingTransactions = async ({
}
// outgoing transactions
const outgoingTxsWithData = outgoingTxs.length ? await batchRequestContractCode(outgoingTxs) : []
const outgoingTxsList: List<Transaction | TxServiceModel> =
(outgoingTxs as TxServiceModel[]).length !== undefined
? List(outgoingTxs as TxServiceModel[])
: (outgoingTxs as List<Transaction>)
const outgoingTxsWithData = outgoingTxsList.size ? await batchRequestContractCode(outgoingTxsList.toArray()) : []
const outgoing: Transaction[] = []
for (const [tx] of outgoingTxsWithData) {

View File

@ -0,0 +1,9 @@
import { createAction } from 'redux-actions'
import { HistoryPayload, QueuedPayload } from 'src/logic/safe/store/reducer/gatewayTransactions'
export const ADD_HISTORY_TRANSACTIONS = 'ADD_HISTORY_TRANSACTIONS'
export const addHistoryTransactions = createAction<HistoryPayload>(ADD_HISTORY_TRANSACTIONS)
export const ADD_QUEUED_TRANSACTIONS = 'ADD_QUEUED_TRANSACTIONS'
export const addQueuedTransactions = createAction<QueuedPayload>(ADD_QUEUED_TRANSACTIONS)

View File

@ -15,6 +15,7 @@ import {
TransactionTypeValues,
TxArgs,
RefundParams,
isStoredTransaction,
} from 'src/logic/safe/store/models/types/transaction'
import { AppReduxState, store } from 'src/store'
import {
@ -30,14 +31,14 @@ import {
import { TypedDataUtils } from 'eth-sig-util'
import { ProviderRecord } from 'src/logic/wallets/store/model/provider'
import { SafeRecord } from 'src/logic/safe/store/models/safe'
import { DataDecoded, DecodedParams } from 'src/routes/safe/store/models/types/transactions.d'
import { DecodedParams } from 'src/routes/safe/store/models/types/transactions.d'
import { CALL } from 'src/logic/safe/transactions'
export const isEmptyData = (data?: string | null): boolean => {
return !data || data === EMPTY_DATA
}
export const isInnerTransaction = (tx: TxServiceModel | Transaction, safeAddress: string): boolean => {
export const isInnerTransaction = (tx: BuildTx['tx'] | Transaction, safeAddress: string): boolean => {
let isSameAddress = false
if ((tx as TxServiceModel).to !== undefined) {
@ -49,10 +50,16 @@ export const isInnerTransaction = (tx: TxServiceModel | Transaction, safeAddress
return isSameAddress && Number(tx.value) === 0
}
export const isCancelTransaction = (tx: TxServiceModel, safeAddress: string): boolean => {
export const isCancelTransaction = (tx: BuildTx['tx'], safeAddress: string): boolean => {
if (isStoredTransaction(tx)) {
if (!sameAddress(tx.recipient, safeAddress)) {
return false
}
} else {
if (!sameAddress(tx.to, safeAddress)) {
return false
}
}
if (Number(tx.value)) {
return false
@ -89,15 +96,15 @@ export const isPendingTransaction = (tx: Transaction, cancelTx: Transaction): bo
return (!!cancelTx && cancelTx.status === 'pending') || tx.status === 'pending'
}
export const isModifySettingsTransaction = (tx: TxServiceModel, safeAddress: string): boolean => {
export const isModifySettingsTransaction = (tx: BuildTx['tx'], safeAddress: string): boolean => {
return isInnerTransaction(tx, safeAddress) && !isEmptyData(tx.data)
}
export const isMultiSendTransaction = (tx: TxServiceModel): boolean => {
export const isMultiSendTransaction = (tx: BuildTx['tx']): boolean => {
return !isEmptyData(tx.data) && tx.data?.substring(0, 10) === '0x8d80ff0a' && Number(tx.value) === 0
}
export const isUpgradeTransaction = (tx: TxServiceModel): boolean => {
export const isUpgradeTransaction = (tx: BuildTx['tx']): boolean => {
return (
!isEmptyData(tx.data) &&
isMultiSendTransaction(tx) &&
@ -106,11 +113,11 @@ export const isUpgradeTransaction = (tx: TxServiceModel): boolean => {
)
}
export const isOutgoingTransaction = (tx: TxServiceModel, safeAddress: string): boolean => {
return !sameAddress(tx.to, safeAddress) && !isEmptyData(tx.data)
export const isOutgoingTransaction = (tx: BuildTx['tx'], safeAddress?: string): boolean => {
return !sameAddress((tx as ServiceTx).to, safeAddress) && !isEmptyData(tx.data)
}
export const isCustomTransaction = async (tx: TxServiceModel, safeAddress: string): Promise<boolean> => {
export const isCustomTransaction = async (tx: BuildTx['tx'], safeAddress?: string): Promise<boolean> => {
const isOutgoing = isOutgoingTransaction(tx, safeAddress)
const isErc20 = await isSendERC20Transaction(tx)
const isUpgrade = isUpgradeTransaction(tx)
@ -120,7 +127,7 @@ export const isCustomTransaction = async (tx: TxServiceModel, safeAddress: strin
}
export const getRefundParams = async (
tx: TxServiceModel,
tx: BuildTx['tx'],
tokenInfo: (string) => Promise<{ decimals: number; symbol: string } | null>,
): Promise<RefundParams | null> => {
const { nativeCoin } = getNetworkInfo()
@ -155,7 +162,7 @@ export const getRefundParams = async (
return refundParams
}
export const getDecodedParams = (tx: TxServiceModel): DecodedParams | null => {
export const getDecodedParams = (tx: BuildTx['tx']): DecodedParams | null => {
if (tx.dataDecoded) {
return {
[tx.dataDecoded.method]: tx.dataDecoded.parameters.reduce(
@ -170,22 +177,22 @@ export const getDecodedParams = (tx: TxServiceModel): DecodedParams | null => {
return null
}
export const getConfirmations = (tx: TxServiceModel): List<Confirmation> => {
export const getConfirmations = (tx: BuildTx['tx']): List<Confirmation> => {
return List(
tx.confirmations.map((conf) =>
(tx.confirmations as ServiceTx['confirmations'])?.map((conf) =>
makeConfirmation({
owner: conf.owner,
hash: conf.transactionHash,
signature: conf.signature,
}),
),
) ?? [],
)
}
export const isTransactionCancelled = (
tx: TxServiceModel,
outgoingTxs: Array<TxServiceModel>,
cancellationTxs: Record<string, TxServiceModel>,
tx: BuildTx['tx'],
outgoingTxs: BuildTx['outgoingTxs'],
cancellationTxs: BuildTx['cancellationTxs'],
): boolean => {
return (
// not executed
@ -252,8 +259,10 @@ export const calculateTransactionType = (tx: Transaction): TransactionTypeValues
return txType
}
export type ServiceTx = TxServiceModel | TxToMock
export type BuildTx = BatchProcessTxsProps & {
tx: TxServiceModel
tx: ServiceTx | Transaction
}
export const buildTx = async ({
@ -281,19 +290,19 @@ export const buildTx = async ({
let tokenSymbol = nativeCoin.symbol
try {
if (isSendERC20Tx) {
const { decimals, symbol } = await getERC20DecimalsAndSymbol(tx.to)
const { decimals, symbol } = await getERC20DecimalsAndSymbol((tx as ServiceTx).to)
tokenDecimals = decimals
tokenSymbol = symbol
} else if (isSendERC721Tx) {
tokenSymbol = await getERC721Symbol(tx.to)
tokenSymbol = await getERC721Symbol((tx as ServiceTx).to)
}
} catch (err) {
console.log(`Failed to retrieve token data from ${tx.to}`)
console.log(`Failed to retrieve token data from ${(tx as ServiceTx).to}`)
}
const txToStore = makeTransaction({
baseGas: tx.baseGas,
blockNumber: tx.blockNumber,
blockNumber: (tx as ServiceTx).blockNumber,
cancelled: isTxCancelled,
confirmations,
customTx: isCustomTx,
@ -301,23 +310,23 @@ export const buildTx = async ({
dataDecoded: tx.dataDecoded,
decimals: tokenDecimals,
decodedParams,
executionDate: tx.executionDate,
executionTxHash: tx.transactionHash,
executor: tx.executor,
fee: tx.fee,
executionDate: (tx as ServiceTx).executionDate,
executionTxHash: (tx as ServiceTx).transactionHash,
executor: (tx as ServiceTx).executor,
fee: (tx as ServiceTx).fee,
gasPrice: tx.gasPrice,
gasToken: tx.gasToken || ZERO_ADDRESS,
isCancellationTx,
isCollectibleTransfer: isSendERC721Tx,
isExecuted: tx.isExecuted,
isSuccessful: tx.isSuccessful,
isSuccessful: (tx as ServiceTx).isSuccessful,
isTokenTransfer: isSendERC20Tx,
modifySettingsTx: isModifySettingsTx,
multiSendTx: isMultiSendTx,
nonce: tx.nonce,
operation: tx.operation,
origin: tx.origin,
recipient: tx.to,
origin: (tx as ServiceTx).origin,
recipient: (tx as ServiceTx).to,
refundParams,
refundReceiver: tx.refundReceiver || ZERO_ADDRESS,
safeTxGas: tx.safeTxGas,
@ -325,7 +334,7 @@ export const buildTx = async ({
submissionDate: tx.submissionDate,
symbol: tokenSymbol,
upgradeTx: isUpgradeTx,
value: tx.value.toString(),
value: tx.value?.toString(),
})
return txToStore
@ -333,13 +342,7 @@ export const buildTx = async ({
.set('type', calculateTransactionType(txToStore))
}
export type TxToMock = TxArgs & {
confirmations: []
safeTxHash: string
value: string
submissionDate: string
dataDecoded: DataDecoded | null
}
export type TxToMock = TxArgs & Partial<TxServiceModel>
export const mockTransaction = (tx: TxToMock, safeAddress: string, state: AppReduxState): Promise<Transaction> => {
const safe = safeSelector(state)
@ -355,7 +358,7 @@ export const mockTransaction = (tx: TxToMock, safeAddress: string, state: AppRed
currentUser: undefined,
outgoingTxs,
safe,
tx: (tx as unknown) as TxServiceModel,
tx,
})
}
@ -369,7 +372,7 @@ export const updateStoredTransactionsStatus = (dispatch: (any) => void, walletRe
dispatch(
addOrUpdateTransactions({
safeAddress,
transactions: transactions.withMutations((list: any[]) =>
transactions: transactions.withMutations((list) =>
list.map((tx) => tx.set('status', calculateTransactionStatus(tx, safe, walletRecord.account))),
),
}),

View File

@ -0,0 +1,6 @@
import { createAction } from 'redux-actions'
import { TransactionStatusPayload } from 'src/logic/safe/store/reducer/gatewayTransactions'
export const UPDATE_TRANSACTION_STATUS = 'UPDATE_TRANSACTION_STATUS'
export const updateTransactionStatus = createAction<TransactionStatusPayload>(UPDATE_TRANSACTION_STATUS)

View File

@ -3,14 +3,17 @@ import { push } from 'connected-react-router'
import { NOTIFICATIONS, enhanceSnackbarForAction } from 'src/logic/notifications'
import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
import { getAwaitingTransactions } from 'src/logic/safe/transactions/awaitingTransactions'
import {
getAwaitingTransactions,
getAwaitingGatewayTransactions,
} from 'src/logic/safe/transactions/awaitingTransactions'
import { getSafeVersionInfo } from 'src/logic/safe/utils/safeVersion'
import { isUserAnOwner } from 'src/logic/wallets/ethAddresses'
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
import { getIncomingTxAmount } from 'src/routes/safe/components/Transactions/TxsTable/columns'
import { grantedSelector } from 'src/routes/safe/container/selector'
import { ADD_INCOMING_TRANSACTIONS } from 'src/logic/safe/store/actions/addIncomingTransactions'
import { ADD_OR_UPDATE_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions'
import { ADD_QUEUED_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/gatewayTransactions'
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
import {
safeParamAddressFromStateSelector,
@ -18,10 +21,16 @@ import {
safeCancellationTransactionsSelector,
} from 'src/logic/safe/store/selectors'
import { isTransactionSummary } from 'src/logic/safe/store/models/types/gateway.d'
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
import { ADD_OR_UPDATE_SAFE } from '../actions/addOrUpdateSafe'
const watchedActions = [ADD_OR_UPDATE_TRANSACTIONS, ADD_INCOMING_TRANSACTIONS, ADD_OR_UPDATE_SAFE]
const watchedActions = [
ADD_OR_UPDATE_TRANSACTIONS,
ADD_INCOMING_TRANSACTIONS,
ADD_OR_UPDATE_SAFE,
ADD_QUEUED_TRANSACTIONS,
]
const sendAwaitingTransactionNotification = async (
dispatch,
@ -34,7 +43,7 @@ const sendAwaitingTransactionNotification = async (
if (!dispatch || !safeAddress || !awaitingTxsSubmissionDateList || !notificationKey) {
return
}
if (awaitingTxsSubmissionDateList.size === 0) {
if (awaitingTxsSubmissionDateList.length === 0) {
return
}
@ -48,7 +57,7 @@ const sendAwaitingTransactionNotification = async (
return lastTimeUserLoggedIn ? new Date(submissionDate) > new Date(lastTimeUserLoggedIn) : true
})
if (filteredDuplicatedAwaitingTxList.size === 0) {
if (filteredDuplicatedAwaitingTxList.length === 0) {
return
}
dispatch(
@ -101,40 +110,39 @@ const notificationsMiddleware = (store) => (next) => async (action) => {
break
}
case ADD_QUEUED_TRANSACTIONS: {
const { safeAddress, values } = action.payload
const transactions = values.filter((tx) => isTransactionSummary(tx)).map((item) => item.transaction)
const userAddress: string = userAccountSelector(state)
const awaitingTransactions = getAwaitingGatewayTransactions(transactions, userAddress)
const awaitingTxsSubmissionDateList = awaitingTransactions.map((tx) => tx.timestamp)
const safes = safesMapSelector(state)
const currentSafe = safes.get(safeAddress)
if (!currentSafe || !isUserAnOwner(currentSafe, userAddress) || awaitingTransactions.length === 0) {
break
}
const notificationKey = `${safeAddress}-awaiting`
await sendAwaitingTransactionNotification(
dispatch,
safeAddress,
awaitingTxsSubmissionDateList,
notificationKey,
onNotificationClicked(dispatch, notificationKey, safeAddress),
)
break
}
case ADD_INCOMING_TRANSACTIONS: {
action.payload.forEach((incomingTransactions, safeAddress) => {
const { latestIncomingTxBlock } = state.safes.get('safes').get(safeAddress, {})
const viewedSafes = state.currentSession['viewedSafes']
const recurringUser = viewedSafes?.includes(safeAddress)
const newIncomingTransactions = incomingTransactions.filter((tx) => tx.blockNumber > latestIncomingTxBlock)
const { message, ...TX_INCOMING_MSG } = NOTIFICATIONS.TX_INCOMING_MSG
if (recurringUser) {
if (newIncomingTransactions.size > 3) {
dispatch(
enqueueSnackbar(
enhanceSnackbarForAction({
...TX_INCOMING_MSG,
message: 'Multiple incoming transfers',
}),
),
)
} else {
newIncomingTransactions.forEach((tx) => {
dispatch(
enqueueSnackbar(
enhanceSnackbarForAction({
...TX_INCOMING_MSG,
message: `${message}${getIncomingTxAmount(tx)}`,
}),
),
)
})
}
}
dispatch(
updateSafe({
address: safeAddress,

View File

@ -0,0 +1,384 @@
type TransferDirection = 'INCOMING' | 'OUTGOING'
type Erc20Transfer = {
type: 'ERC20'
tokenAddress: string
tokenName: string | null
tokenSymbol: string | null
logoUri: string | null
decimals: number | null
value: string
}
type Erc721Transfer = {
type: 'ERC721'
tokenAddress: string
tokenId: string
tokenName: string | null
tokenSymbol: string | null
logoUri: string | null
decimals: number | null
value: string
}
type NativeTransfer = {
type: 'ETHER'
value: string
tokenSymbol: string | null
decimals: number | null
}
type TransferInfo = Erc20Transfer | Erc721Transfer | NativeTransfer
type Transfer = {
type: 'Transfer'
sender: string
recipient: string
direction?: TransferDirection
transferInfo: TransferInfo // Polymorphic: Erc20, Erc721, Ether
}
export enum Operation {
CALL,
DELEGATE,
}
type InternalTransaction = {
operation: Operation
to: string
value: number | null
data: string | null
dataDecoded: DataDecoded | null
}
type Parameter = {
name: string
type: string
value: string
valueDecoded: InternalTransaction[] | null
}
type DataDecoded = {
method: string
parameters: Parameter[] | null
}
type SetFallbackHandler = {
type: 'SET_FALLBACK_HANDLER'
handler: string
}
type AddOwner = {
type: 'ADD_OWNER'
owner: string
threshold: number
}
type RemoveOwner = {
type: 'REMOVE_OWNER'
owner: string
threshold: number
}
type SwapOwner = {
type: 'SWAP_OWNER'
oldOwner: string
newOwner: string
}
type ChangeThreshold = {
type: 'CHANGE_THRESHOLD'
threshold: number
}
type ChangeImplementation = {
type: 'CHANGE_IMPLEMENTATION'
implementation: string
}
type EnableModule = {
type: 'ENABLE_MODULE'
module: string
}
type DisableModule = {
type: 'DISABLE_MODULE'
module: string
}
type SettingsInfo =
| SetFallbackHandler
| AddOwner
| RemoveOwner
| SwapOwner
| ChangeThreshold
| ChangeImplementation
| EnableModule
| DisableModule
type SettingsChange = {
type: 'SettingsChange'
dataDecoded: DataDecoded
settingsInfo: SettingsInfo | null
}
type AddressInfo = {
name: string
logoUri: string | null
}
type BaseCustom = {
type: 'Custom'
to: string
dataSize: string
value: string
isCancellation: boolean
toInfo: AddressInfo
}
type Custom = BaseCustom & {
methodName: string | null
}
type MultiSend = BaseCustom & {
methodName: 'multiSend'
actionCount: number
}
type Creation = {
type: 'Creation'
creator: string
transactionHash: string
implementation: string | null
factory: string | null
}
type TransactionStatus =
| 'AWAITING_CONFIRMATIONS'
| 'AWAITING_EXECUTION'
| 'CANCELLED'
| 'FAILED'
| 'SUCCESS'
| 'PENDING'
| 'PENDING_FAILED'
| 'WILL_BE_REPLACED'
type TransactionInfo = Transfer | SettingsChange | Custom | MultiSend | Creation
type ExecutionInfo = {
nonce: number
confirmationsRequired: number
confirmationsSubmitted: number
missingSigners?: string[]
}
type SafeAppInfo = {
name: string
url: string
logoUrl: string
}
type TransactionSummary = {
id: string
timestamp: number
txStatus: TransactionStatus
txInfo: TransactionInfo // Polymorphic: Transfer, SettingsChange, Custom, Creation
executionInfo: ExecutionInfo | null
safeAppInfo: SafeAppInfo | null
}
type TransactionData = {
hexData: string | null
dataDecoded: DataDecoded | null
to: string
value: string | null
operation: Operation
}
type ModuleExecutionDetails = {
type: 'MODULE'
address: string
}
type MultiSigConfirmations = {
signer: string
signature: string | null
}
type TokenType = 'ERC721' | 'ERC20' | 'ETHER'
type TokenInfo = {
tokenType: TokenType
address: string
decimals: number | null
symbol: string
name: string
logoUri: string | null
}
type MultiSigExecutionDetails = {
type: 'MULTISIG'
submittedAt: number
nonce: number
safeTxGas: number
baseGas: number
gasPrice: string
gasToken: string
refundReceiver: string
safeTxHash: string
executor: string | null
signers: string[]
confirmationsRequired: number
confirmations: MultiSigConfirmations[]
gasTokenInfo: TokenInfo | null
}
type DetailedExecutionInfo = ModuleExecutionDetails | MultiSigExecutionDetails
type ExpandedTxDetails = {
executedAt: number
txStatus: TransactionStatus
txInfo: TransactionInfo
txData: TransactionData | null
detailedExecutionInfo: DetailedExecutionInfo | null
txHash: string | null
}
type Transaction = TransactionSummary & {
txDetails?: ExpandedTxDetails
}
type StoreStructure = {
queued: {
next: { [nonce: number]: Transaction[] } // 1 Transaction element
queued: { [nonce: number]: Transaction[] } // n Transaction elements
}
history: { [timestamp: number]: Transaction[] } // n Transaction elements
}
type TxQueuedLocation = 'queued.next' | 'queued.queued'
type TxHistoryLocation = 'history'
type TxLocation = TxHistoryLocation | TxQueuedLocation
type Label = {
type: 'LABEL'
label: 'Next' | 'Queued'
}
type DateLabel = {
type: 'DATE_LABEL'
timestamp: number
}
type ConflictHeader = {
type: 'CONFLICT_HEADER'
nonce: number
}
type TransactionGatewayResult = {
type: 'TRANSACTION'
transaction: TransactionSummary
conflictType: 'HasNext' | 'End' | 'None'
}
type GatewayResponse = {
next: string | null
previous: string | null
}
type HistoryGatewayResult = DateLabel | TransactionGatewayResult
type HistoryGatewayResponse = GatewayResponse & {
results: HistoryGatewayResult[]
}
type QueuedGatewayResult = Label | ConflictHeader | TransactionGatewayResult
type QueuedGatewayResponse = GatewayResponse & {
results: QueuedGatewayResult[]
}
export type TransactionDetails = {
count: number
transactions: Array<[nonce: string, transactions: Transaction[]]>
}
/**
* Helper functions
*/
export const isDateLabel = (value: HistoryGatewayResult): value is DateLabel => {
return value.type === 'DATE_LABEL'
}
export const isLabel = (value: QueuedGatewayResult): value is Label => {
return value.type === 'LABEL'
}
export const isConflictHeader = (value: QueuedGatewayResult): value is ConflictHeader => {
return value.type === 'CONFLICT_HEADER'
}
export const isTransactionSummary = (
value: HistoryGatewayResult | QueuedGatewayResult,
): value is TransactionGatewayResult => {
return value.type === 'TRANSACTION'
}
export const isTransferTxInfo = (value: TransactionInfo): value is Transfer => {
return value.type === 'Transfer'
}
export const isSettingsChangeTxInfo = (value: TransactionInfo): value is SettingsChange => {
return value.type === 'SettingsChange'
}
export const isCustomTxInfo = (value: TransactionInfo): value is Custom => {
return value.type === 'Custom'
}
export const isMultiSendTxInfo = (value: TransactionInfo): value is MultiSend => {
return isCustomTxInfo(value) && value.methodName === 'multiSend'
}
export const isCreationTxInfo = (value: TransactionInfo): value is Creation => {
return value.type === 'Creation'
}
export const isStatusSuccess = (value: Transaction['txStatus']): value is 'SUCCESS' => {
return value === 'SUCCESS'
}
export const isStatusFailed = (value: Transaction['txStatus']): value is 'FAILED' => {
return value === 'FAILED'
}
export const isStatusCancelled = (value: Transaction['txStatus']): value is 'CANCELLED' => {
return value === 'CANCELLED'
}
export const isStatusPending = (value: Transaction['txStatus']): value is 'PENDING' => {
return value === 'PENDING'
}
export const isStatusAwaitingConfirmation = (value: Transaction['txStatus']): value is 'AWAITING_CONFIRMATIONS' => {
return value === 'AWAITING_CONFIRMATIONS'
}
export const isStatusWillBeReplaced = (value: Transaction['txStatus']): value is 'WILL_BE_REPLACED' => {
return value === 'WILL_BE_REPLACED'
}
export const isMultiSigExecutionDetails = (
value: ExpandedTxDetails['detailedExecutionInfo'],
): value is MultiSigExecutionDetails => {
return value?.type === 'MULTISIG'
}
export const isModuleExecutionDetails = (
value: ExpandedTxDetails['detailedExecutionInfo'],
): value is ModuleExecutionDetails => {
return value?.type === 'MODULE'
}

View File

@ -6,6 +6,7 @@ import { Confirmation } from './confirmation'
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
import { DataDecoded, Transfer } from './transactions'
import { DecodedParams } from 'src/routes/safe/store/models/types/transactions.d'
import { BuildTx } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
export enum TransactionTypes {
INCOMING = 'incoming',
@ -89,7 +90,11 @@ export type TransactionProps = {
value: string
}
export type Transaction = RecordOf<TransactionProps>
export type Transaction = RecordOf<TransactionProps> & Readonly<TransactionProps>
export const isStoredTransaction = (tx: BuildTx['tx']): tx is Transaction => {
return typeof (tx as Transaction).recipient !== 'undefined'
}
export type TxArgs = {
baseGas: number

View File

@ -1,18 +1,25 @@
import { Map } from 'immutable'
import { handleActions } from 'redux-actions'
import { Action, handleActions } from 'redux-actions'
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
import { ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/addOrUpdateCancellationTransactions'
import { REMOVE_CANCELLATION_TRANSACTION } from 'src/logic/safe/store/actions/transactions/removeCancellationTransaction'
import { AppReduxState } from 'src/store'
export const CANCELLATION_TRANSACTIONS_REDUCER_ID = 'cancellationTransactions'
export type CancellationTransactions = Map<string, Transaction>
export type CancellationTxState = Map<string, CancellationTransactions>
export default handleActions(
type CancellationTransactionsPayload = { safeAddress: string; transactions: CancellationTransactions }
type CancellationTransactionPayload = { safeAddress: string; transaction: Transaction }
export default handleActions<
AppReduxState['cancellationTransactions'],
CancellationTransactionsPayload | CancellationTransactionPayload
>(
{
[ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS]: (state, action) => {
[ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS]: (state, action: Action<CancellationTransactionsPayload>) => {
const { safeAddress, transactions } = action.payload
if (!safeAddress || !transactions || !transactions.size) {
@ -41,7 +48,7 @@ export default handleActions(
}
})
},
[REMOVE_CANCELLATION_TRANSACTION]: (state, action) => {
[REMOVE_CANCELLATION_TRANSACTION]: (state, action: Action<CancellationTransactionPayload>) => {
const { safeAddress, transaction } = action.payload
if (!safeAddress || !transaction) {

View File

@ -0,0 +1,368 @@
import get from 'lodash.get'
import merge from 'lodash.merge'
import { Action, handleActions } from 'redux-actions'
import {
ADD_HISTORY_TRANSACTIONS,
ADD_QUEUED_TRANSACTIONS,
} from 'src/logic/safe/store/actions/transactions/gatewayTransactions'
import { UPDATE_TRANSACTION_STATUS } from 'src/logic/safe/store/actions/updateTransactionStatus'
import {
HistoryGatewayResponse,
isConflictHeader,
isDateLabel,
isLabel,
isTransactionSummary,
QueuedGatewayResponse,
StoreStructure,
Transaction,
TransactionStatus,
TxLocation,
} from 'src/logic/safe/store/models/types/gateway.d'
import { UPDATE_TRANSACTION_DETAILS } from 'src/logic/safe/store/actions/fetchTransactionDetails'
import { AppReduxState } from 'src/store'
import { getUTCStartOfDate } from 'src/utils/date'
import { sameString } from 'src/utils/strings'
import { sortObject } from 'src/utils/objects'
export const GATEWAY_TRANSACTIONS_ID = 'gatewayTransactions'
type BasePayload = { safeAddress: string; isTail?: boolean }
export type HistoryPayload = BasePayload & { values: HistoryGatewayResponse['results'] }
export type QueuedPayload = BasePayload & { values: QueuedGatewayResponse['results'] }
export type TransactionDetailsPayload = {
safeAddress: string
txLocation: TxLocation
transactionId: string
value: Transaction['txDetails']
}
export type TransactionStatusPayload = {
safeAddress: string
nonce: number
id?: string
txStatus: TransactionStatus
}
type Payload = HistoryPayload | QueuedPayload | TransactionDetailsPayload | TransactionStatusPayload
const findTransactionLocation = (
transactionsGroup: { [p: number]: Transaction[] },
transactionId: string,
): { key: string; index: number } => {
let key
let index
let transactions
for ([key, transactions] of Object.entries(transactionsGroup)) {
index = transactions.findIndex(({ id }) => sameString(id, transactionId))
if (index !== -1) {
break
}
}
return { key, index }
}
export const gatewayTransactions = handleActions<AppReduxState['gatewayTransactions'], Payload>(
{
[ADD_HISTORY_TRANSACTIONS]: (state, action: Action<HistoryPayload>) => {
const { safeAddress, values, isTail = false } = action.payload
const history: StoreStructure['history'] = Object.assign({}, state[safeAddress]?.history)
values.forEach((value) => {
if (isDateLabel(value)) {
// DATE_LABEL is discarded as it's not needed for the current implementation
return
}
if (isTransactionSummary(value)) {
const startOfDate = getUTCStartOfDate(value.transaction.timestamp)
if (typeof history[startOfDate] === 'undefined') {
history[startOfDate] = []
}
const txExist = history[startOfDate].some(({ id }) => sameString(id, value.transaction.id))
if (!txExist) {
history[startOfDate].push(value.transaction)
// pushing a newer transaction to the existing list messes the transactions order
// this happens when most recent transactions are added to the existing txs in the store
history[startOfDate] = history[startOfDate].sort((a, b) => b.timestamp - a.timestamp)
}
return
}
})
return {
// all the safes with their respective states
...state,
// current safe
[safeAddress]: {
// keep queued list
...state[safeAddress],
// extend history list
history: isTail ? history : sortObject(history, 'desc'),
},
}
},
[ADD_QUEUED_TRANSACTIONS]: (state, action: Action<QueuedPayload>) => {
// we're assuming that `next` and `queued` labels will be provided in the first page
// as for usage experience there were no more than 5 transactions competing for the same nonce.
// Thus, given the client-gateway page size of 20, we have plenty of "room" to be provided with
// `next` and `queued` transactions in the first page.
const { safeAddress, values } = action.payload
let next = Object.assign({}, state[safeAddress]?.queued?.next)
const queued = Object.assign({}, state[safeAddress]?.queued?.queued)
let label: 'next' | 'queued' | undefined
values.forEach((value) => {
if (isLabel(value)) {
// we're assuming that the first page will always provide `next` and `queued` labels
label = value.label.toLowerCase() as 'next' | 'queued'
return
}
if (isConflictHeader(value)) {
// conflict header is discarded as it's not needed for the current implementation
return
}
if (isTransactionSummary(value)) {
const txNonce = value.transaction.executionInfo?.nonce
if (typeof txNonce === 'undefined') {
console.warn('A transaction without nonce was provided by client-gateway:', JSON.stringify(value))
return
}
if (typeof label === 'undefined') {
label = next[txNonce] ? 'next' : 'queued'
}
switch (label) {
case 'next': {
if (next[txNonce]) {
const txIndex = next[txNonce].findIndex(({ id }) => sameString(id, value.transaction.id))
if (txIndex !== -1) {
const storedTransaction = next[txNonce][txIndex]
const updateFromService =
storedTransaction.executionInfo?.confirmationsSubmitted !==
value.transaction.executionInfo?.confirmationsSubmitted
if (storedTransaction.txStatus === 'PENDING' && !updateFromService) {
// we're waiting for a tx resolution. Thus, we'll prioritize 'PENDING' status
value.transaction.txStatus = 'PENDING'
}
next[txNonce][txIndex] = updateFromService
? // by replacing the current transaction with the one returned by the service
// we remove the `txDetails`, so this will force a re-request of the data
value.transaction
: // we merge, to keep the current unchanged information
merge(storedTransaction, value.transaction)
break
}
// we add the transaction returned by the service to the list of transactions
next[txNonce] = [...next[txNonce], value.transaction]
break
}
// a new tx has arrived to the `next` queue
// we re-create the `next` object with the new transaction
next = { [txNonce]: [value.transaction] }
// we remove the new `next` transaction from the `queue` list, if it exist
queued[txNonce] && delete queued[txNonce]
break
}
case 'queued': {
if (queued[txNonce]) {
const txIndex = queued[txNonce].findIndex(({ id }) => sameString(id, value.transaction.id))
if (txIndex !== -1) {
const storedTransaction = queued[txNonce][txIndex]
const updateFromService =
storedTransaction.executionInfo?.confirmationsSubmitted !==
value.transaction.executionInfo?.confirmationsSubmitted
if (storedTransaction.txStatus === 'PENDING' && !updateFromService) {
// we're waiting for a tx resolution. Thus, we'll prioritize 'PENDING' status
value.transaction.txStatus = 'PENDING'
}
queued[txNonce][txIndex] = updateFromService
? // by replacing the current transaction with the one returned by the service
// we remove the `txDetails`, so this will force a re-request of the data
value.transaction
: // we merge, to keep the current unchanged information
merge(storedTransaction, value.transaction)
break
}
// we add the transaction returned by the service to the list of transactions
queued[txNonce] = [...queued[txNonce], value.transaction]
break
}
queued[txNonce] = [value.transaction]
break
}
}
return
}
})
// no new transactions
if (!values.length) {
// queued list already empty
if (!Object.keys(queued).length) {
// there was an existing next transaction
if (Object.keys(next).length === 1) {
// we cleanup the next queue
next = {}
}
}
}
return {
// all the safes with their respective states
...state,
// current safe
[safeAddress]: {
// keep history list
...state[safeAddress],
// overwrites queued lists
queued: {
next,
queued,
},
},
}
},
[UPDATE_TRANSACTION_DETAILS]: (state, action: Action<TransactionDetailsPayload>) => {
const { safeAddress, transactionId, txLocation, value } = action.payload
const storedTransactions = Object.assign({}, state[safeAddress])
const { queued } = storedTransactions
let { history } = storedTransactions
// get the tx group (it will be `queued.next`, `queued.queued` or `history`)
const txGroup: StoreStructure['queued']['next' | 'queued'] | StoreStructure['history'] = get(
storedTransactions,
txLocation,
)
// find the transaction location
const { key, index } = findTransactionLocation(txGroup, transactionId)
// add details to tx object
txGroup[key][index]['txDetails'] = value
// replace the updated group in its corresponding location
switch (txLocation) {
case 'history':
history = txGroup
break
case 'queued.next':
queued['next'] = txGroup
break
case 'queued.queued':
queued['queued'] = txGroup
break
}
// update state
return {
// all the safes with their respective states
...state,
// current safe
[safeAddress]: {
history,
queued,
},
}
},
[UPDATE_TRANSACTION_STATUS]: (state, action: Action<TransactionStatusPayload>) => {
// if we provide the tx ID that sole tx will have the _pending_ status.
// if not, all the txs that share the same nonce will have the _pending_ status.
const { nonce, id, safeAddress, txStatus } = action.payload
const storedTransactions = Object.assign({}, state[safeAddress])
const { queued } = storedTransactions
const { history } = storedTransactions
let txLocation: TxLocation | undefined
let historyLocation: string | undefined
if (queued.next[nonce]) {
txLocation = 'queued.next'
} else if (queued.queued[nonce]) {
txLocation = 'queued.queued'
} else {
Object.entries(history).forEach(([timestamp, transactions]) => {
const txIndex = transactions.findIndex((transaction) => Number(transaction.executionInfo?.nonce) === nonce)
if (txIndex !== -1) {
txLocation = 'history'
historyLocation = `${timestamp}[${txIndex}]`
}
})
}
if (!txLocation) {
return state
}
switch (txLocation) {
case 'history': {
if (historyLocation) {
const txToUpdate = get(history, historyLocation)
txToUpdate.txStatus = txStatus
}
break
}
case 'queued.next': {
queued.next[nonce] = queued.next[nonce].map((txToUpdate) => {
if (typeof id !== 'undefined') {
if (sameString(txToUpdate.id, id)) {
txToUpdate.txStatus = txStatus
}
} else {
txToUpdate.txStatus = txStatus
}
return txToUpdate
})
break
}
case 'queued.queued': {
queued.queued[nonce] = queued.queued[nonce].map((txToUpdate) => {
if (typeof id !== 'undefined') {
if (sameString(txToUpdate.id, id)) {
txToUpdate.txStatus = txStatus
}
} else {
txToUpdate.txStatus = txStatus
}
return txToUpdate
})
break
}
}
// update state
return {
// all the safes with their respective states
...state,
// current safe
[safeAddress]: {
history,
queued,
},
}
},
},
{},
)

View File

@ -2,10 +2,11 @@ import { Map } from 'immutable'
import { handleActions } from 'redux-actions'
import { ADD_INCOMING_TRANSACTIONS } from 'src/logic/safe/store/actions/addIncomingTransactions'
import { AppReduxState } from 'src/store'
export const INCOMING_TRANSACTIONS_REDUCER_ID = 'incomingTransactions'
export default handleActions(
export default handleActions<AppReduxState['incomingTransactions']>(
{
[ADD_INCOMING_TRANSACTIONS]: (state, action) => action.payload,
},

View File

@ -1,5 +1,5 @@
import { Map, Set, List } from 'immutable'
import { handleActions } from 'redux-actions'
import { Action, handleActions } from 'redux-actions'
import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes'
import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner'
@ -13,9 +13,9 @@ import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
import { UPDATE_TOKENS_LIST } from 'src/logic/safe/store/actions/updateTokensList'
import { UPDATE_ASSETS_LIST } from 'src/logic/safe/store/actions/updateAssetsList'
import { makeOwner } from 'src/logic/safe/store/models/owner'
import makeSafe, { SafeRecordProps } from 'src/logic/safe/store/models/safe'
import makeSafe, { SafeRecord, SafeRecordProps } from 'src/logic/safe/store/models/safe'
import { AppReduxState } from 'src/store'
import { checksumAddress } from 'src/utils/checksumAddress'
import { SafeReducerMap } from 'src/routes/safe/store/reducer/types/safe'
import { ADD_OR_UPDATE_SAFE, buildOwnersFrom } from 'src/logic/safe/store/actions/addOrUpdateSafe'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { shouldSafeStoreBeUpdated } from 'src/logic/safe/utils/shouldSafeStoreBeUpdated'
@ -73,9 +73,22 @@ const updateSafeProps = (prevSafe, safe) => {
})
}
export default handleActions(
export type SafePayload = { safe: SafeRecord }
type SafePayloads = SafeRecord | SafePayload | string
type BaseOwnerPayload = { safeAddress: string; ownerAddress: string }
type FullOwnerPayload = BaseOwnerPayload & { ownerName: string }
type ReplaceOwnerPayload = FullOwnerPayload & { oldOwnerAddress: string }
type OwnerPayloads = BaseOwnerPayload | FullOwnerPayload | ReplaceOwnerPayload
type SafeWithAddressPayload = SafeRecord & { safeAddress: string }
type Payloads = SafePayloads | OwnerPayloads | SafeWithAddressPayload
export default handleActions<AppReduxState['safes'], Payloads>(
{
[UPDATE_SAFE]: (state: SafeReducerMap, action) => {
[UPDATE_SAFE]: (state, action: Action<SafeRecord>) => {
const safe = action.payload
const safeAddress = safe.address
@ -89,7 +102,7 @@ export default handleActions(
)
: state
},
[ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state: SafeReducerMap, action) => {
[ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state, action: Action<SafeRecord>) => {
const tokenAddress = action.payload
return state.withMutations((map) => {
@ -104,8 +117,7 @@ export default handleActions(
})
})
},
[ADD_OR_UPDATE_SAFE]: (state: SafeReducerMap, action) => {
[ADD_OR_UPDATE_SAFE]: (state, action: Action<SafePayload>) => {
const { safe } = action.payload
const safeAddress = safe.address
@ -123,7 +135,7 @@ export default handleActions(
)
: state
},
[REMOVE_SAFE]: (state: SafeReducerMap, action) => {
[REMOVE_SAFE]: (state, action: Action<string>) => {
const safeAddress = action.payload
const currentDefaultSafe = state.get('defaultSafe')
@ -135,7 +147,7 @@ export default handleActions(
return newState
},
[ADD_SAFE_OWNER]: (state: SafeReducerMap, action) => {
[ADD_SAFE_OWNER]: (state, action: Action<FullOwnerPayload>) => {
const { ownerAddress, ownerName, safeAddress } = action.payload
const addressFound = state
@ -152,7 +164,7 @@ export default handleActions(
}),
)
},
[REMOVE_SAFE_OWNER]: (state: SafeReducerMap, action) => {
[REMOVE_SAFE_OWNER]: (state, action: Action<BaseOwnerPayload>) => {
const { ownerAddress, safeAddress } = action.payload
return state.updateIn(['safes', safeAddress], (prevSafe) =>
@ -161,7 +173,7 @@ export default handleActions(
}),
)
},
[REPLACE_SAFE_OWNER]: (state: SafeReducerMap, action) => {
[REPLACE_SAFE_OWNER]: (state, action: Action<ReplaceOwnerPayload>) => {
const { oldOwnerAddress, ownerAddress, ownerName, safeAddress } = action.payload
return state.updateIn(['safes', safeAddress], (prevSafe) =>
@ -172,7 +184,7 @@ export default handleActions(
}),
)
},
[EDIT_SAFE_OWNER]: (state: SafeReducerMap, action) => {
[EDIT_SAFE_OWNER]: (state, action: Action<FullOwnerPayload>) => {
const { ownerAddress, ownerName, safeAddress } = action.payload
return state.updateIn(['safes', safeAddress], (prevSafe) => {
@ -183,7 +195,7 @@ export default handleActions(
return prevSafe.merge({ owners: updatedOwners })
})
},
[UPDATE_TOKENS_LIST]: (state: SafeReducerMap, action) => {
[UPDATE_TOKENS_LIST]: (state, action: Action<SafeWithAddressPayload>) => {
// Only activeTokens or blackListedTokens is required
const { safeAddress, activeTokens, blacklistedTokens } = action.payload
@ -192,7 +204,7 @@ export default handleActions(
return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.set(key, list))
},
[UPDATE_ASSETS_LIST]: (state: SafeReducerMap, action) => {
[UPDATE_ASSETS_LIST]: (state, action: Action<SafeWithAddressPayload>) => {
// Only activeAssets or blackListedAssets is required
const { safeAddress, activeAssets, blacklistedAssets } = action.payload
@ -201,13 +213,13 @@ export default handleActions(
return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.set(key, list))
},
[SET_DEFAULT_SAFE]: (state: SafeReducerMap, action) => state.set('defaultSafe', action.payload),
[SET_LATEST_MASTER_CONTRACT_VERSION]: (state: SafeReducerMap, action) =>
[SET_DEFAULT_SAFE]: (state, action: Action<SafeRecord>) => state.set('defaultSafe', action.payload),
[SET_LATEST_MASTER_CONTRACT_VERSION]: (state, action: Action<SafeRecord>) =>
state.set('latestMasterContractVersion', action.payload),
},
Map({
defaultSafe: DEFAULT_SAFE_INITIAL_STATE,
safes: Map(),
latestMasterContractVersion: '',
}),
}) as AppReduxState['safes'],
)

View File

@ -1,14 +1,22 @@
import { Map } from 'immutable'
import { handleActions } from 'redux-actions'
import { List, Map } from 'immutable'
import { Action, handleActions } from 'redux-actions'
import { ADD_OR_UPDATE_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions'
import { REMOVE_TRANSACTION } from 'src/logic/safe/store/actions/transactions/removeTransaction'
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
import { AppReduxState } from 'src/store'
export const TRANSACTIONS_REDUCER_ID = 'transactions'
export default handleActions(
type TransactionBasePayload = { safeAddress: string }
type TransactionsPayload = TransactionBasePayload & { transactions: List<Transaction> }
type TransactionPayload = TransactionBasePayload & { transaction: Transaction }
type Payload = TransactionsPayload | TransactionPayload
export default handleActions<AppReduxState['transactions'], Payload>(
{
[ADD_OR_UPDATE_TRANSACTIONS]: (state, action) => {
[ADD_OR_UPDATE_TRANSACTIONS]: (state, action: Action<TransactionsPayload>) => {
const { safeAddress, transactions } = action.payload
if (!safeAddress || !transactions || !transactions.size) {
@ -46,7 +54,7 @@ export default handleActions(
}
})
},
[REMOVE_TRANSACTION]: (state, action) => {
[REMOVE_TRANSACTION]: (state, action: Action<TransactionPayload>) => {
const { safeAddress, transaction } = action.payload
if (!safeAddress || !transaction) {

View File

@ -0,0 +1,98 @@
import get from 'lodash.get'
import { createSelector } from 'reselect'
import { StoreStructure, Transaction, TxLocation } from 'src/logic/safe/store/models/types/gateway.d'
import { GATEWAY_TRANSACTIONS_ID } from 'src/logic/safe/store/reducer/gatewayTransactions'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { createHashBasedSelector } from 'src/logic/safe/store/selectors/utils'
import { AppReduxState } from 'src/store'
export const gatewayTransactions = (state: AppReduxState): AppReduxState['gatewayTransactions'] => {
return state[GATEWAY_TRANSACTIONS_ID]
}
export const historyTransactions = createSelector(
gatewayTransactions,
safeParamAddressFromStateSelector,
(gatewayTransactions, safeAddress): StoreStructure['history'] | undefined => {
return gatewayTransactions[safeAddress]?.history
},
)
export const pendingTransactions = createSelector(
gatewayTransactions,
safeParamAddressFromStateSelector,
(gatewayTransactions, safeAddress): StoreStructure['queued'] | undefined => {
return gatewayTransactions[safeAddress]?.queued
},
)
export const nextTransactions = createSelector(pendingTransactions, (pendingTransactions):
| StoreStructure['queued']['next']
| undefined => {
return pendingTransactions?.next
})
export const queuedTransactions = createSelector(pendingTransactions, (pendingTransactions):
| StoreStructure['queued']['queued']
| undefined => {
return pendingTransactions?.queued
})
type TxByLocationAttr = { attributeName: string; attributeValue: string | number; txLocation: TxLocation }
type TxByLocation = {
attributeName: string
attributeValue: string | number
transactions: StoreStructure['history'] | StoreStructure['queued']['queued' | 'next']
}
const getTransactionsByLocation = createHashBasedSelector(
gatewayTransactions,
safeParamAddressFromStateSelector,
(gatewayTransactions, safeAddress) => (rest: TxByLocationAttr): TxByLocation => ({
attributeName: rest.attributeName,
attributeValue: rest.attributeValue,
transactions: get(gatewayTransactions[safeAddress], rest.txLocation),
}),
)
export const getTransactionByAttribute = createSelector(
getTransactionsByLocation,
(fn: (r: TxByLocationAttr) => TxByLocation) => (rest: TxByLocationAttr): Transaction | undefined => {
const { attributeName, attributeValue, transactions } = fn(rest)
if (transactions && attributeValue) {
for (const [, txs] of Object.entries(transactions)) {
const foundTx = txs.find((transaction) => transaction[attributeName] === attributeValue)
if (foundTx) {
return foundTx
}
}
}
},
)
export const getTransactionDetails = createSelector(
getTransactionByAttribute,
(fn: (rest: TxByLocationAttr) => Transaction | undefined) => (
rest: TxByLocationAttr,
): Transaction['txDetails'] | undefined => {
const transaction = fn(rest)
return transaction?.txDetails
},
)
export const getQueuedTransactionsByNonce = createSelector(
getTransactionsByLocation,
(fn: (r: TxByLocationAttr) => TxByLocation) => (rest: TxByLocationAttr): Transaction[] => {
const { attributeValue, attributeName, transactions } = fn(rest)
if (attributeName === 'nonce') {
return transactions?.[attributeValue] ?? []
}
return []
},
)

View File

@ -93,7 +93,7 @@ export const safeCancellationTransactionsSelector = createSelector(
return Map()
}
return cancellationTransactions.get(address, Map({}))
return cancellationTransactions.get(address, Map())
},
)
@ -109,7 +109,7 @@ export const safeIncomingTransactionsSelector = createSelector(
return List([])
}
return incomingTransactions.get(address, List([]))
return incomingTransactions.get(address, List())
},
)

View File

@ -1,14 +1,18 @@
import { List } from 'immutable'
import { createSelector } from 'reselect'
// import { List } from 'immutable'
// import { createSelector } from 'reselect'
//
// import { safeIncomingTransactionsSelector, safeTransactionsSelector } from 'src/logic/safe/store/selectors'
// import { Transaction, SafeModuleTransaction } from 'src/logic/safe/store/models/types/transaction'
// import { safeModuleTransactionsSelector } from 'src/routes/safe/container/selector'
import { safeIncomingTransactionsSelector, safeTransactionsSelector } from 'src/logic/safe/store/selectors'
import { Transaction, SafeModuleTransaction } from 'src/logic/safe/store/models/types/transaction'
import { safeModuleTransactionsSelector } from 'src/routes/safe/container/selector'
// export const extendedTransactionsSelector = createSelector(
// safeTransactionsSelector,
// safeIncomingTransactionsSelector,
// safeModuleTransactionsSelector,
// (transactions, incomingTransactions, moduleTransactions): List<Transaction | SafeModuleTransaction> =>
// List().withMutations((list) => {
// list.concat(transactions).concat(incomingTransactions).concat(moduleTransactions)
// }),
// )
export const extendedTransactionsSelector = createSelector(
safeTransactionsSelector,
safeIncomingTransactionsSelector,
safeModuleTransactionsSelector,
(transactions, incomingTransactions, moduleTransactions): List<Transaction | SafeModuleTransaction> =>
List([...transactions, ...incomingTransactions, ...moduleTransactions]),
)
export {}

View File

@ -0,0 +1,13 @@
import hash from 'object-hash'
import isEqual from 'lodash.isequal'
import memoize from 'lodash.memoize'
import { createSelectorCreator, defaultMemoize } from 'reselect'
import { AppReduxState } from 'src/store'
export const createIsEqualSelector = createSelectorCreator(defaultMemoize, isEqual)
const hashFn = (gatewayTransactions: AppReduxState['gatewayTransactions'], safeAddress: string): string =>
hash(gatewayTransactions[safeAddress])
export const createHashBasedSelector = createSelectorCreator(memoize as any, hashFn)

View File

@ -1,18 +1,22 @@
import { List } from 'immutable'
import { isPendingTransaction } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { isStatusAwaitingConfirmation } from 'src/logic/safe/store/models/types/gateway.d'
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
import { Transaction as GatewayTransaction } from 'src/logic/safe/store/models/types/gateway'
import { addressInList } from 'src/routes/safe/components/Transactions/GatewayTransactions/utils'
import { CancellationTransactions } from 'src/logic/safe/store/reducer/cancellationTransactions'
export const getAwaitingTransactions = (
allTransactions: List<Transaction>,
cancellationTxs,
cancellationTxs: CancellationTransactions,
userAccount: string,
): List<Transaction> => {
return allTransactions.filter((tx) => {
const cancelTx = !!tx.nonce && !isNaN(Number(tx.nonce)) ? cancellationTxs.get(`${tx.nonce}`) : null
// The transaction is not executed and is not cancelled, nor pending, so it's still waiting confirmations
if (!tx.executionTxHash && !tx.cancelled && !isPendingTransaction(tx, cancelTx)) {
if (!tx.executionTxHash && !tx.cancelled && cancelTx && !isPendingTransaction(tx, cancelTx)) {
// Then we check if the waiting confirmations are not from the current user, otherwise, filters this transaction
const transactionWaitingUser = tx.confirmations.filter(({ owner }) => owner !== userAccount)
return transactionWaitingUser.size > 0
@ -21,3 +25,18 @@ export const getAwaitingTransactions = (
return false
})
}
export const getAwaitingGatewayTransactions = (
allTransactions: GatewayTransaction[],
userAccount: string,
): GatewayTransaction[] => {
return allTransactions.filter((tx) => {
// The transaction is not executed and is not cancelled, nor pending, so it's still waiting confirmations
if (isStatusAwaitingConfirmation(tx.txStatus)) {
// Then we check if the waiting confirmations are not from the current user, otherwise, filters this transaction
return addressInList(tx.executionInfo?.missingSigners)(userAccount)
}
return false
})
}

View File

@ -1,4 +1,4 @@
import { Map } from 'immutable'
import isEqual from 'lodash.isequal'
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
@ -6,9 +6,9 @@ import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
const isStateSubset = (superObj, subObj) => {
return Object.keys(subObj).every((key) => {
if (subObj[key] && typeof subObj[key] == 'object') {
if (Map.isMap(subObj[key]) || subObj[key].size >= 0) {
if (typeof subObj[key] === 'object' || subObj[key].length >= 0) {
// If type is Immutable Map, List or Object we use Immutable equals
return superObj[key].equals(subObj[key])
return isEqual(superObj[key], subObj[key])
}
return isStateSubset(superObj[key], subObj[key])
}

View File

@ -15,21 +15,8 @@ import { TokenState } from 'src/logic/tokens/store/reducer/tokens'
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
import { AppReduxState } from 'src/store'
import { humanReadableValue } from 'src/logic/tokens/utils/humanReadableValue'
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
import {
safeActiveTokensSelector,
safeBalancesSelector,
safeBlacklistedTokensSelector,
safeEthBalanceSelector,
safeSelector,
} from 'src/logic/safe/store/selectors'
import { safeActiveTokensSelector, safeBlacklistedTokensSelector, safeSelector } from 'src/logic/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>
@ -78,11 +65,8 @@ const fetchSafeTokens = (safeAddress: string) => async (
}
const tokenCurrenciesBalances = await backOff(() => fetchTokenCurrenciesBalances(safeAddress))
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 } = tokenCurrenciesBalances.reduce<ExtractedData>(
extractDataFromResult(currentTokens),
@ -100,24 +84,10 @@ const fetchSafeTokens = (safeAddress: string) => async (
balances.keySeq().toSet().subtract(blacklistedTokens),
)
const update = updateSafeValue(safeAddress)
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?.get(safeAddress)?.get('currencyBalances')
const updateCurrencies = currencyList.equals(storedCurrencyBalances)
? noFunc
: setCurrencyBalances(safeAddress, currencyList)
const updateTokens = tokens.size === 0 ? noFunc : addTokens(tokens)
batch(() => {
dispatch(updateActiveTokens)
dispatch(updateBalances)
dispatch(updateEthBalance)
dispatch(updateCurrencies)
dispatch(updateTokens)
dispatch(updateSafe({ address: safeAddress, activeTokens, balances, ethBalance }))
dispatch(setCurrencyBalances(safeAddress, currencyList))
dispatch(addTokens(tokens))
})
} catch (err) {
console.error('Error fetching active token list', err)

View File

@ -1,17 +1,22 @@
import { Map } from 'immutable'
import { handleActions } from 'redux-actions'
import { List, Map } from 'immutable'
import { Action, handleActions } from 'redux-actions'
import { ADD_TOKEN } from 'src/logic/tokens/store/actions/addToken'
import { ADD_TOKENS } from 'src/logic/tokens/store/actions/saveTokens'
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
import { AppReduxState } from 'src/store'
export const TOKEN_REDUCER_ID = 'tokens'
export type TokenState = Map<string, Token>
export default handleActions(
type TokensPayload = { tokens: List<Token> }
type TokenPayload = { token: Token }
type Payloads = TokensPayload | TokenPayload
export default handleActions<AppReduxState['tokens'], Payloads>(
{
[ADD_TOKENS]: (state: TokenState, action) => {
[ADD_TOKENS]: (state: TokenState, action: Action<TokensPayload>) => {
const { tokens } = action.payload
return state.withMutations((map) => {
@ -20,7 +25,7 @@ export default handleActions(
})
})
},
[ADD_TOKEN]: (state: TokenState, action) => {
[ADD_TOKEN]: (state: TokenState, action: Action<TokenPayload>) => {
const { token } = action.payload
const { address: tokenAddress } = token

View File

@ -8,8 +8,7 @@ import { isSendERC721Transaction } from 'src/logic/collectibles/utils'
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
import { ALTERNATIVE_TOKEN_ABI } from 'src/logic/tokens/utils/alternativeAbi'
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
import { isEmptyData } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
import { BuildTx, isEmptyData, ServiceTx } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { CALL } from 'src/logic/safe/transactions'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
@ -35,7 +34,7 @@ export const isAddressAToken = async (tokenAddress: string): Promise<boolean> =>
return call !== '0x'
}
export const isTokenTransfer = (tx: TxServiceModel): boolean => {
export const isTokenTransfer = (tx: BuildTx['tx']): boolean => {
return (
!isEmptyData(tx.data) &&
// Check if contains 'transfer' method code
@ -70,11 +69,11 @@ export const getERC20DecimalsAndSymbol = async (
return tokenInfo
}
export const isSendERC20Transaction = async (tx: TxServiceModel): Promise<boolean> => {
export const isSendERC20Transaction = async (tx: BuildTx['tx']): Promise<boolean> => {
let isSendTokenTx = !isSendERC721Transaction(tx) && isTokenTransfer(tx)
if (isSendTokenTx) {
const { decimals, symbol } = await getERC20DecimalsAndSymbol(tx.to)
const { decimals, symbol } = await getERC20DecimalsAndSymbol((tx as ServiceTx).to)
// some contracts may implement the same methods as in ERC20 standard
// we may falsely treat them as tokens, so in case we get any errors when getting token info

View File

@ -44,7 +44,6 @@ const handleProviderNotification = (provider, dispatch) => {
action: 'Connect a wallet',
label: provider.name,
})
dispatch(enqueueSnackbar(enhanceSnackbarForAction(NOTIFICATIONS.WALLET_CONNECTED_MSG)))
} else {
dispatch(enqueueSnackbar(enhanceSnackbarForAction(NOTIFICATIONS.UNLOCK_WALLET_MSG)))
}

View File

@ -2,8 +2,6 @@ import { Dispatch } from 'src/logic/safe/store/actions/types.d'
import { createAction } from 'redux-actions'
import { onboard } from 'src/components/ConnectButton'
import { NOTIFICATIONS, enhanceSnackbarForAction } from 'src/logic/notifications'
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
import { resetWeb3 } from 'src/logic/wallets/getWeb3'
export const REMOVE_PROVIDER = 'REMOVE_PROVIDER'
@ -15,9 +13,4 @@ export default () => (dispatch: Dispatch): void => {
resetWeb3()
dispatch(removeProvider())
dispatch(
enqueueSnackbar(
enhanceSnackbarForAction(NOTIFICATIONS.WALLET_DISCONNECTED_MSG, NOTIFICATIONS.WALLET_DISCONNECTED_MSG.key),
),
)
}

View File

@ -10,6 +10,7 @@ import { useDispatch, useSelector } from 'react-redux'
import { SAFELIST_ADDRESS } from 'src/routes/routes'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { xs } from 'src/theme/variables'
import { grantedSelector } from 'src/routes/safe/container/selector'
const useStyles = makeStyles(
createStyles({
@ -48,6 +49,7 @@ export const EllipsisTransactionDetails = ({
const dispatch = useDispatch()
const currentSafeAddress = useSelector(safeParamAddressFromStateSelector)
const isOwnerConnected = useSelector(grantedSelector)
const handleClick = (event) => setAnchorEl(event.currentTarget)
@ -65,7 +67,7 @@ export const EllipsisTransactionDetails = ({
<Menu anchorEl={anchorEl} id="simple-menu" keepMounted onClose={closeMenuHandler} open={Boolean(anchorEl)}>
{sendModalOpenHandler
? [
<MenuItem key="send-again-button" onClick={sendModalOpenHandler}>
<MenuItem key="send-again-button" onClick={sendModalOpenHandler} disabled={!isOwnerConnected}>
Send Again
</MenuItem>,
<Divider key="divider" />,

View File

@ -1,58 +0,0 @@
import React, { useEffect, useState } from 'react'
import { useTransactions } from 'src/routes/safe/container/hooks/useTransactions'
import { ButtonLink, Loader } from '@gnosis.pm/safe-react-components'
import { Transaction } from 'src/logic/safe/store/models/types/transactions.d'
const Transactions = (): React.ReactElement => {
const [currentPage, setCurrentPage] = useState(0)
const [limit] = useState(50)
const [offset, setOffset] = useState(0)
const [maxPages, setMaxPages] = useState(0)
const { transactions, totalTransactionsCount } = useTransactions({ offset, limit })
const [transactionsByPage, setTransactionsByPage] = useState(transactions)
useEffect(() => {
const currentPage = Math.floor(offset / limit) + 1
const maxPages = Math.ceil(totalTransactionsCount / limit)
setCurrentPage(currentPage)
setMaxPages(maxPages)
const newTransactionsByPage = transactions ? transactions.slice(offset, offset * 2 || limit) : []
setTransactionsByPage(newTransactionsByPage)
}, [offset, limit, totalTransactionsCount, transactions])
// TODO: Remove this once we implement infinite scroll
const nextPageButtonHandler = () => {
setOffset(offset + limit)
}
const previousPageButtonHandler = () => {
setOffset(offset > 0 ? offset - limit : offset)
}
if (!transactionsByPage) return <div>No txs available for safe</div>
if (!transactionsByPage.length) return <Loader size="lg" />
return (
<>
{transactionsByPage.map((tx: Transaction, index) => {
let txHash = ''
if ('transactionHash' in tx) {
txHash = tx.transactionHash as string
}
if ('txHash' in tx) {
txHash = tx.txHash
}
return <div key={txHash || tx.executionDate || index}>Tx hash: {txHash}</div>
})}
<ButtonLink color="primary" onClick={previousPageButtonHandler} disabled={currentPage === 1}>
Previous Page
</ButtonLink>
<ButtonLink color="primary" onClick={nextPageButtonHandler} disabled={currentPage >= maxPages}>
Next Page
</ButtonLink>
</>
)
}
export default Transactions

View File

@ -16,7 +16,7 @@ import Img from 'src/components/layout/Img'
import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
import { SafeApp } from 'src/routes/safe/components/Apps/types.d'
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import { MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts'
import { DELEGATE_CALL, TX_NOTIFICATION_TYPES, CALL } from 'src/logic/safe/transactions'
import { encodeMultiSendCall } from 'src/logic/safe/transactions/multisend'

View File

@ -14,7 +14,7 @@ const { nativeCoin } = getNetworkInfo()
const StyledBlock = styled(Block)`
font-size: 12px;
line-height: 1.08;
letter-spacing: -0.5;
letter-spacing: -0.5px;
background-color: ${border};
width: fit-content;
padding: 5px 10px;

View File

@ -4,6 +4,7 @@ import cn from 'classnames'
import React, { Suspense, useEffect, useState } from 'react'
import Modal from 'src/components/Modal'
import { Erc721Transfer } from 'src/logic/safe/store/models/types/gateway'
import { CollectibleTx } from './screens/ReviewCollectible'
import { CustomTx } from './screens/ContractInteraction/ReviewCustomTx'
import { ContractInteractionTx } from './screens/ContractInteraction'
@ -62,7 +63,7 @@ type Props = {
isOpen: boolean
onClose: () => void
recipientAddress?: string
selectedToken?: string | NFTToken
selectedToken?: string | NFTToken | Erc721Transfer
tokenAmount?: string
}

View File

@ -18,7 +18,7 @@ import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
import { Header } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Header'
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'

View File

@ -16,7 +16,7 @@ import Hairline from 'src/components/layout/Hairline'
import Img from 'src/components/layout/Img'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'

View File

@ -14,7 +14,7 @@ import Img from 'src/components/layout/Img'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { nftTokensSelector } from 'src/logic/collectibles/store/selectors'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'

View File

@ -16,7 +16,7 @@ import Img from 'src/components/layout/Img'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { getSpendingLimitContract } from 'src/logic/contracts/safeContracts'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import { getHumanFriendlyToken } from 'src/logic/tokens/store/actions/fetchTokens'

View File

@ -19,6 +19,7 @@ import WhenFieldChanges from 'src/components/WhenFieldChanges'
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { getNameFromAddressBook } from 'src/logic/addressBook/utils'
import { nftTokensSelector, safeActiveSelectorMap } from 'src/logic/collectibles/store/selectors'
import { Erc721Transfer } from 'src/logic/safe/store/models/types/gateway'
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
import { AddressBookInput } from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
import { NFTToken } from 'src/logic/collectibles/sources/collectibles.d'
@ -52,7 +53,7 @@ type SendCollectibleProps = {
onClose: () => void
onNext: (txInfo: SendCollectibleTxInfo) => void
recipientAddress?: string
selectedToken?: NFTToken
selectedToken?: NFTToken | Erc721Transfer
}
export type SendCollectibleTxInfo = {
@ -243,7 +244,12 @@ const SendCollectible = ({
</Row>
<Row margin="sm">
<Col>
<TokenSelectField assets={nftAssets} initialValue={selectedToken?.assetAddress} />
<TokenSelectField
assets={nftAssets}
initialValue={
(selectedToken as NFTToken)?.assetAddress ?? (selectedToken as Erc721Transfer)?.tokenAddress
}
/>
</Col>
</Row>
<Row margin="xs">

View File

@ -18,7 +18,7 @@ import Row from 'src/components/layout/Row'
import Modal from 'src/components/Modal'
import { getExplorerInfo } from 'src/config'
import { getDisableModuleTxData } from 'src/logic/safe/utils/modules'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import { ModulePair } from 'src/logic/safe/store/models/safe'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'

View File

@ -7,7 +7,7 @@ import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import addSafeOwner from 'src/logic/safe/store/actions/addSafeOwner'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { checksumAddress } from 'src/utils/checksumAddress'
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'

View File

@ -9,7 +9,7 @@ import ThresholdForm from './screens/ThresholdForm'
import Modal from 'src/components/Modal'
import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import removeSafeOwner from 'src/logic/safe/store/actions/removeSafeOwner'
import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'

View File

@ -6,7 +6,7 @@ import Modal from 'src/components/Modal'
import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import replaceSafeOwner from 'src/logic/safe/store/actions/replaceSafeOwner'
import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors'
import { checksumAddress } from 'src/utils/checksumAddress'

View File

@ -9,7 +9,7 @@ interface GenericInfoProps {
const DataDisplay = ({ title, children }: GenericInfoProps): ReactElement => (
<>
{title && (
<Text size="lg" color="secondaryLight">
<Text size="md" color="secondaryLight">
{title}
</Text>
)}

View File

@ -6,7 +6,7 @@ import Block from 'src/components/layout/Block'
import Col from 'src/components/layout/Col'
import Row from 'src/components/layout/Row'
import { getNetworkInfo } from 'src/config'
import createTransaction, { CreateTransactionArgs } from 'src/logic/safe/store/actions/createTransaction'
import { createTransaction, CreateTransactionArgs } from 'src/logic/safe/store/actions/createTransaction'
import { SafeRecordProps, SpendingLimit } from 'src/logic/safe/store/models/safe'
import {
addSpendingLimitBeneficiaryMultiSendTx,

View File

@ -5,7 +5,7 @@ import { useDispatch, useSelector } from 'react-redux'
import Block from 'src/components/layout/Block'
import Col from 'src/components/layout/Col'
import useTokenInfo from 'src/logic/safe/hooks/useTokenInfo'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import { getDeleteAllowanceTxData } from 'src/logic/safe/utils/spendingLimits'

View File

@ -12,7 +12,7 @@ import Row from 'src/components/layout/Row'
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import { grantedSelector } from 'src/routes/safe/container/selector'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import {
safeOwnersSelector,
safeParamAddressFromStateSelector,

View File

@ -12,7 +12,7 @@ import Hairline from 'src/components/layout/Hairline'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { getUpgradeSafeTransactionHash } from 'src/logic/safe/utils/upgradeSafe'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import { makeStyles } from '@material-ui/core'
import { TransactionFees } from 'src/components/TransactionsFees'
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'

View File

@ -0,0 +1,59 @@
import React, { ReactElement, useContext } from 'react'
import { useSelector } from 'react-redux'
import { ExpandedTxDetails, Transaction } from 'src/logic/safe/store/models/types/gateway.d'
import { getTransactionByAttribute } from 'src/logic/safe/store/selectors/gatewayTransactions'
import { useTransactionParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
import { AppReduxState } from 'src/store'
import { ApproveTxModal } from './modals/ApproveTxModal'
import { RejectTxModal } from './modals/RejectTxModal'
import { TransactionActionStateContext } from './TxActionProvider'
import { Overwrite } from 'src/types/helpers'
export const ActionModal = (): ReactElement | null => {
const { selectedAction, selectAction } = useContext(TransactionActionStateContext)
const txParameters = useTransactionParameters()
const transaction = useSelector((state: AppReduxState) =>
getTransactionByAttribute(state)({
attributeValue: selectedAction.transactionId,
attributeName: 'id',
txLocation: selectedAction.txLocation,
}),
)
const onClose = () => selectAction({ actionSelected: 'none', transactionId: '', txLocation: 'history' })
if (!transaction?.txDetails) {
return null
}
switch (selectedAction.actionSelected) {
case 'cancel':
return <RejectTxModal isOpen onClose={onClose} gwTransaction={transaction} />
case 'confirm':
return (
<ApproveTxModal
isOpen
onClose={onClose}
transaction={transaction as Overwrite<Transaction, { txDetails: ExpandedTxDetails }>}
txParameters={txParameters}
/>
)
case 'execute':
return (
<ApproveTxModal
canExecute
isOpen
onClose={onClose}
transaction={transaction as Overwrite<Transaction, { txDetails: ExpandedTxDetails }>}
txParameters={txParameters}
/>
)
case 'none':
return null
}
}

View File

@ -0,0 +1,24 @@
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
import React, { ReactElement } from 'react'
import { useSelector } from 'react-redux'
import { getExplorerInfo } from 'src/config'
import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors'
export const AddressInfo = ({ address }: { address: string }): ReactElement | null => {
const recipientName = useSelector((state) => getNameFromAddressBookSelector(state, address))
if (address === '') {
return null
}
return (
<EthHashInfo
hash={address}
name={recipientName === 'UNKNOWN' ? undefined : recipientName}
showIdenticon
showCopyBtn
explorerUrl={getExplorerInfo(address)}
/>
)
}

View File

@ -0,0 +1,13 @@
import { Text } from '@gnosis.pm/safe-react-components'
import React, { ReactElement } from 'react'
import { TxData as LegacyTxData } from 'src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/CustomDescription'
export const HexEncodedData = ({ hexData }: { hexData: string }): ReactElement => (
<div className="tx-hexData">
<Text size="xl" strong>
Data (hex encoded):
</Text>
<LegacyTxData data={hexData} />
</div>
)

View File

@ -0,0 +1,46 @@
import { Loader } from '@gnosis.pm/safe-react-components'
import { format } from 'date-fns'
import React, { ReactElement } from 'react'
import { InfiniteScroll, SCROLLABLE_TARGET_ID } from 'src/components/InfiniteScroll'
import { usePagedHistoryTransactions } from './hooks/usePagedHistoryTransactions'
import {
SubTitle,
ScrollableTransactionsContainer,
StyledTransactions,
StyledTransactionsGroup,
Centered,
} from './styled'
import { TxHistoryRow } from './TxHistoryRow'
import { TxLocationContext } from './TxLocationProvider'
export const HistoryTxList = (): ReactElement => {
const { count, hasMore, next, transactions } = usePagedHistoryTransactions()
if (count === 0) {
return (
<Centered>
<Loader size="md" />
</Centered>
)
}
return (
<TxLocationContext.Provider value={{ txLocation: 'history' }}>
<ScrollableTransactionsContainer id={SCROLLABLE_TARGET_ID}>
<InfiniteScroll dataLength={transactions.length} next={next} hasMore={hasMore}>
{transactions?.map(([timestamp, txs]) => (
<StyledTransactionsGroup key={timestamp}>
<SubTitle size="lg">{format(Number(timestamp), 'MMM d, yyyy')}</SubTitle>
<StyledTransactions>
{txs.map((transaction) => (
<TxHistoryRow key={transaction.id} transaction={transaction} />
))}
</StyledTransactions>
</StyledTransactionsGroup>
))}
</InfiniteScroll>
</ScrollableTransactionsContainer>
</TxLocationContext.Provider>
)
}

View File

@ -0,0 +1,16 @@
import { Text } from '@gnosis.pm/safe-react-components'
import React, { ReactElement, ReactNode } from 'react'
type InfoDetailsProps = {
children: ReactNode
title: string
}
export const InfoDetails = ({ children, title }: InfoDetailsProps): ReactElement => (
<>
<Text size="xl" strong>
{title}
</Text>
{children}
</>
)

View File

@ -0,0 +1,64 @@
import { Text } from '@gnosis.pm/safe-react-components'
import React from 'react'
import styled from 'styled-components'
import { DataDecoded } from 'src/logic/safe/store/models/types/gateway.d'
import { isArrayParameter } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
import {
DeleteSpendingLimitDetails,
isDeleteAllowance,
isSetAllowance,
ModifySpendingLimitDetails,
} from 'src/routes/safe/components/Transactions/GatewayTransactions/SpendingLimitDetails'
import Value from 'src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/Value'
const TxDetailsMethodName = styled(Text)`
text-indent: 4px;
`
const TxDetailsMethodParam = styled.div<{ isArrayParameter: boolean }>`
padding-left: 24px;
display: ${({ isArrayParameter }) => (isArrayParameter ? 'block' : 'flex')};
align-items: center;
p:first-of-type {
margin-right: ${({ isArrayParameter }) => (isArrayParameter ? '0' : '4px')};
}
`
const TxInfo = styled.div`
padding: 8px 8px 8px 16px;
`
const StyledMethodName = styled(Text)`
white-space: nowrap;
`
export const MethodDetails = ({ data }: { data: DataDecoded }): React.ReactElement => {
// FixMe: this way won't scale well
if (isSetAllowance(data.method)) {
return <ModifySpendingLimitDetails data={data} />
}
// FixMe: this way won't scale well
if (isDeleteAllowance(data.method)) {
return <DeleteSpendingLimitDetails data={data} />
}
return (
<TxInfo>
<TxDetailsMethodName size="xl" strong>
{data.method}
</TxDetailsMethodName>
{data.parameters?.map((param, index) => (
<TxDetailsMethodParam key={`${data.method}_param-${index}`} isArrayParameter={isArrayParameter(param.type)}>
<StyledMethodName size="xl" strong>
{param.name}({param.type}):
</StyledMethodName>
<Value method={data.method} type={param.type} value={param.value} />
</TxDetailsMethodParam>
))}
</TxInfo>
)
}

View File

@ -0,0 +1,82 @@
import { AccordionSummary, IconText } from '@gnosis.pm/safe-react-components'
import React, { ReactElement, ReactNode } from 'react'
import { getNetworkInfo } from 'src/config'
import { DataDecoded, TransactionData } from 'src/logic/safe/store/models/types/gateway.d'
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
import { HexEncodedData } from './HexEncodedData'
import { MethodDetails } from './MethodDetails'
import { isSpendingLimitMethod } from './SpendingLimitDetails'
import { ColumnDisplayAccordionDetails, ActionAccordion } from './styled'
import { TxInfoDetails } from './TxInfoDetails'
type MultiSendTxGroupProps = {
actionTitle: string
children: ReactNode
txDetails: {
title: string
address: string
dataDecoded: DataDecoded | null
}
}
const MultiSendTxGroup = ({ actionTitle, children, txDetails }: MultiSendTxGroupProps): ReactElement => {
return (
<ActionAccordion>
<AccordionSummary>
<IconText iconSize="sm" iconType="code" text={actionTitle} textSize="xl" />
</AccordionSummary>
<ColumnDisplayAccordionDetails>
{!isSpendingLimitMethod(txDetails.dataDecoded?.method) && (
<TxInfoDetails title={txDetails.title} address={txDetails.address} />
)}
{children}
</ColumnDisplayAccordionDetails>
</ActionAccordion>
)
}
const { nativeCoin } = getNetworkInfo()
export const MultiSendDetails = ({ txData }: { txData: TransactionData }): ReactElement | null => {
// no parameters for the `multiSend`
if (!txData.dataDecoded?.parameters) {
// we render the hex encoded data
if (txData.hexData) {
return <HexEncodedData hexData={txData.hexData} />
}
return null
}
// multiSend has one parameter `transactions` therefore `txData.dataDecoded.parameters[0]` is safe to be used here
return (
<>
{txData.dataDecoded.parameters[0].valueDecoded?.map(({ dataDecoded }, index, valuesDecoded) => {
let details
const { data, value, to } = valuesDecoded[index]
const actionTitle = `Action ${index + 1} ${dataDecoded ? `(${dataDecoded.method})` : ''}`
const amount = value ? fromTokenUnit(value, nativeCoin.decimals) : 0
const title = `Send ${amount} ${nativeCoin.name} to:`
if (dataDecoded) {
details = <MethodDetails data={dataDecoded} />
} else {
details = data && <HexEncodedData hexData={data} />
}
return (
details && (
<MultiSendTxGroup
key={`${data ?? to}-${index}`}
actionTitle={actionTitle}
txDetails={{ title, address: to, dataDecoded }}
>
{details}
</MultiSendTxGroup>
)
)
})}
</>
)
}

View File

@ -0,0 +1,22 @@
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
import React, { ReactElement } from 'react'
import { useSelector } from 'react-redux'
import { getExplorerInfo } from 'src/config'
import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors'
export const OwnerRow = ({ ownerAddress }: { ownerAddress: string }): ReactElement => {
const ownerName = useSelector((state) => getNameFromAddressBookSelector(state, ownerAddress))
return (
<EthHashInfo
hash={ownerAddress}
name={ownerName}
showIdenticon
showCopyBtn
explorerUrl={getExplorerInfo(ownerAddress)}
shortenHash={4}
className="owner-info"
/>
)
}

View File

@ -0,0 +1,61 @@
import { Loader, Title } from '@gnosis.pm/safe-react-components'
import React, { ReactElement } from 'react'
import style from 'styled-components'
import { InfiniteScroll, SCROLLABLE_TARGET_ID } from 'src/components/InfiniteScroll'
import Img from 'src/components/layout/Img'
import { usePagedQueuedTransactions } from './hooks/usePagedQueuedTransactions'
import { ActionModal } from './ActionModal'
import { TxActionProvider } from './TxActionProvider'
import { TxLocationContext } from './TxLocationProvider'
import { QueueTxList } from './QueueTxList'
import { ScrollableTransactionsContainer, Centered } from './styled'
import NoTransactionsImage from './assets/no-transactions.svg'
const NoTransactions = style.div`
display: flex;
flex-direction: column;
margin-top: 60px;
`
export const QueueTransactions = (): ReactElement => {
const { count, loading, hasMore, next, transactions } = usePagedQueuedTransactions()
// `loading` is, actually `!transactions`
// added the `transaction` verification to prevent `Object is possibly 'undefined'` error
if (loading || !transactions) {
return (
<Centered>
<Loader size="md" />
</Centered>
)
}
if (count === 0) {
return (
<NoTransactions>
<Img alt="No Transactions yet" src={NoTransactionsImage} />
<Title size="xs">Transactions will appear here </Title>
</NoTransactions>
)
}
return (
<TxActionProvider>
<ScrollableTransactionsContainer id={SCROLLABLE_TARGET_ID}>
<InfiniteScroll dataLength={count} next={next} hasMore={hasMore}>
{/* Next list */}
<TxLocationContext.Provider value={{ txLocation: 'queued.next' }}>
{transactions.next.count !== 0 && <QueueTxList transactions={transactions.next.transactions} />}
</TxLocationContext.Provider>
{/* Queue list */}
<TxLocationContext.Provider value={{ txLocation: 'queued.queued' }}>
{transactions.queue.count !== 0 && <QueueTxList transactions={transactions.queue.transactions} />}
</TxLocationContext.Provider>
</InfiniteScroll>
</ScrollableTransactionsContainer>
<ActionModal />
</TxActionProvider>
)
}

View File

@ -0,0 +1,93 @@
import { Icon, Link, Text } from '@gnosis.pm/safe-react-components'
import React, { Fragment, ReactElement, useContext } from 'react'
import { Transaction, TransactionDetails } from 'src/logic/safe/store/models/types/gateway.d'
import {
DisclaimerContainer,
GroupedTransactions,
GroupedTransactionsCard,
SubTitle,
StyledTransactions,
StyledTransactionsGroup,
AlignItemsWithMargin,
} from './styled'
import { TxHoverProvider } from './TxHoverProvider'
import { TxLocationContext } from './TxLocationProvider'
import { TxQueueRow } from './TxQueueRow'
const TreeView = ({ firstElement }: { firstElement: boolean }): ReactElement => {
return <p className="tree-lines">{firstElement ? <span className="first-node" /> : null}</p>
}
const Disclaimer = ({ nonce }: { nonce: string }): ReactElement => {
return (
<DisclaimerContainer className="disclaimer-container">
<Text size="xl" className="nonce">
{nonce}
</Text>
<AlignItemsWithMargin className="disclaimer">
<Text as="span" size="xl">
These transactions conflict as they use the same nonce. Executing one will automatically replace the other(s).{' '}
</Text>
<Link
href="https://help.gnosis-safe.io/en/articles/4730252-why-are-transactions-with-the-same-nonce-conflicting-with-each-other"
target="_blank"
rel="noreferrer"
title="Why are transactions with the same nonce conflicting with each other?"
>
<AlignItemsWithMargin>
<Text size="xl" as="span" color="primary">
Learn more
</Text>
<Icon size="sm" type="externalLink" color="primary" />
</AlignItemsWithMargin>
</Link>
</AlignItemsWithMargin>
</DisclaimerContainer>
)
}
type QueueTransactionProps = {
nonce: string
transactions: Transaction[]
}
const QueueTransaction = ({ nonce, transactions }: QueueTransactionProps): ReactElement => {
return transactions.length > 1 ? (
<GroupedTransactionsCard>
<TxHoverProvider>
<Disclaimer nonce={nonce} />
<GroupedTransactions>
{transactions.map((transaction, index) => (
<Fragment key={`${nonce}-${transaction.id}`}>
<TreeView firstElement={!index} />
<TxQueueRow isGrouped transaction={transaction} />
</Fragment>
))}
</GroupedTransactions>
</TxHoverProvider>
</GroupedTransactionsCard>
) : (
<TxQueueRow transaction={transactions[0]} />
)
}
type QueueTxListProps = {
transactions: TransactionDetails['transactions']
}
export const QueueTxList = ({ transactions }: QueueTxListProps): ReactElement => {
const { txLocation } = useContext(TxLocationContext)
const title = txLocation === 'queued.next' ? 'NEXT TRANSACTION' : 'QUEUE'
return (
<StyledTransactionsGroup>
<SubTitle size="lg">{title}</SubTitle>
<StyledTransactions>
{transactions.map(([nonce, txs]) => (
<QueueTransaction key={nonce} nonce={nonce} transactions={txs} />
))}
</StyledTransactions>
</StyledTransactionsGroup>
)
}

View File

@ -0,0 +1,75 @@
import { Text } from '@gnosis.pm/safe-react-components'
import React from 'react'
import { sameString } from 'src/utils/strings'
import styled from 'styled-components'
import useTokenInfo from 'src/logic/safe/hooks/useTokenInfo'
import { DataDecoded } from 'src/logic/safe/store/models/types/gateway.d'
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
import { RESET_TIME_OPTIONS } from 'src/routes/safe/components/Settings/SpendingLimit/FormFields/ResetTime'
import { AddressInfo, ResetTimeInfo, TokenInfo } from 'src/routes/safe/components/Settings/SpendingLimit/InfoDisplay'
const SET_ALLOWANCE = 'setAllowance'
const DELETE_ALLOWANCE = 'deleteAllowance'
export const isSetAllowance = (method?: string) => sameString(method, SET_ALLOWANCE)
export const isDeleteAllowance = (method?: string) => sameString(method, DELETE_ALLOWANCE)
export const isSpendingLimitMethod = (method?: string) => isSetAllowance(method) || isDeleteAllowance(method)
const SpendingLimitRow = styled.div`
margin-bottom: 16px;
`
export const ModifySpendingLimitDetails = ({ data }: { data: DataDecoded }): React.ReactElement => {
const [beneficiary, tokenAddress, amount, resetTimeMin] = React.useMemo(
() => data.parameters?.map(({ value }) => value) ?? [],
[data.parameters],
)
const resetTimeLabel = React.useMemo(
() => RESET_TIME_OPTIONS.find(({ value }) => +value === +resetTimeMin / 24 / 60)?.label ?? '',
[resetTimeMin],
)
const tokenInfo = useTokenInfo(tokenAddress)
return (
<>
<SpendingLimitRow>
<Text size="xl" strong>
Modify Spending Limit:
</Text>
</SpendingLimitRow>
<SpendingLimitRow>
<AddressInfo title="Beneficiary" address={beneficiary} cut={0} />
</SpendingLimitRow>
<SpendingLimitRow>
{tokenInfo && <TokenInfo amount={fromTokenUnit(amount, tokenInfo.decimals)} title="Amount" token={tokenInfo} />}
</SpendingLimitRow>
<SpendingLimitRow>
<ResetTimeInfo title="Reset Time" label={resetTimeLabel} />
</SpendingLimitRow>
</>
)
}
export const DeleteSpendingLimitDetails = ({ data }: { data: DataDecoded }): React.ReactElement => {
const [beneficiary, tokenAddress] = React.useMemo(() => data.parameters?.map(({ value }) => value) ?? [], [
data.parameters,
])
const tokenInfo = useTokenInfo(tokenAddress)
return (
<>
<SpendingLimitRow>
<Text size="xl" strong>
Delete Spending Limit:
</Text>
</SpendingLimitRow>
<SpendingLimitRow>
<AddressInfo title="Beneficiary" address={beneficiary} cut={0} />
</SpendingLimitRow>
<SpendingLimitRow>{tokenInfo && <TokenInfo amount="" title="Token" token={tokenInfo} />}</SpendingLimitRow>
</>
)
}

View File

@ -0,0 +1,39 @@
import { Text } from '@gnosis.pm/safe-react-components'
import React, { ReactElement } from 'react'
import styled from 'styled-components'
import Img from 'src/components/layout/Img'
import NFTIcon from 'src/routes/safe/components/Balances/assets/nft_icon.png'
import TokenPlaceholder from 'src/routes/safe/components/Balances/assets/token_placeholder.svg'
import { TokenTransferAsset } from './hooks/useAssetInfo'
const Amount = styled(Text)`
margin-left: 10px;
line-height: 16px;
`
const AmountWrapper = styled.div`
display: flex;
align-items: center;
`
export type TokenTransferAmountProps = {
assetInfo: TokenTransferAsset
}
export const TokenTransferAmount = ({ assetInfo }: TokenTransferAmountProps): ReactElement => {
return (
<AmountWrapper>
<Img
alt={assetInfo.name}
height={26}
onError={(error) => {
error.currentTarget.onerror = null
error.currentTarget.src = assetInfo.tokenType === 'ERC721' ? NFTIcon : TokenPlaceholder
}}
src={assetInfo.logoUri}
/>
<Amount size="xl">{`${assetInfo.directionSign}${assetInfo.amountWithSymbol}`}</Amount>
</AmountWrapper>
)
}

View File

@ -0,0 +1,55 @@
import React, { createContext, ReactElement, ReactNode, useCallback, useRef, useState } from 'react'
import { useDispatch } from 'react-redux'
import { fetchTransactionDetails } from 'src/logic/safe/store/actions/fetchTransactionDetails'
import { TxLocation } from 'src/logic/safe/store/models/types/gateway.d'
export type ActionType = 'cancel' | 'confirm' | 'execute' | 'none'
export type SelectedAction = {
// FixMe: give proper names to the keys
// for instance:
// `action->{ type; forTransactionId; txLocation; }`
// `setAction` as callback
selectedAction: {
actionSelected: ActionType
transactionId: string
txLocation: TxLocation
}
selectAction: (args: SelectedAction['selectedAction']) => Promise<void>
}
export const TransactionActionStateContext = createContext<SelectedAction>({
selectedAction: {
actionSelected: 'none',
transactionId: '',
txLocation: 'history',
},
selectAction: () => Promise.resolve(),
})
export const TxActionProvider = ({ children }: { children: ReactNode }): ReactElement => {
const dispatch = useRef(useDispatch())
const [selectedAction, setSelectedAction] = useState<SelectedAction['selectedAction']>({
actionSelected: 'none',
transactionId: '',
txLocation: 'history',
})
const selectAction = useCallback(
async ({ actionSelected, transactionId, txLocation }: SelectedAction['selectedAction']) => {
if (transactionId) {
await dispatch.current(fetchTransactionDetails({ transactionId, txLocation }))
}
setSelectedAction({ actionSelected, transactionId, txLocation })
},
[],
)
return (
<TransactionActionStateContext.Provider value={{ selectedAction, selectAction }}>
{children}
</TransactionActionStateContext.Provider>
)
}

View File

@ -0,0 +1,216 @@
import { Dot, IconText as IconTextSrc, Text } from '@gnosis.pm/safe-react-components'
import { ThemeColors } from '@gnosis.pm/safe-react-components/dist/theme'
import { Tooltip } from '@material-ui/core'
import CircularProgress from '@material-ui/core/CircularProgress'
import { createStyles, makeStyles } from '@material-ui/core/styles'
import React, { ReactElement, useRef } from 'react'
import CustomIconText from 'src/components/CustomIconText'
import {
isCustomTxInfo,
isMultiSendTxInfo,
isSettingsChangeTxInfo,
Transaction,
} from 'src/logic/safe/store/models/types/gateway.d'
import { TxCollapsedActions } from 'src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsedActions'
import { formatDateTime, formatTime } from 'src/routes/safe/components/Transactions/GatewayTransactions/utils'
import { KNOWN_MODULES } from 'src/utils/constants'
import styled from 'styled-components'
import { AssetInfo, isTokenTransferAsset } from './hooks/useAssetInfo'
import { TransactionActions } from './hooks/useTransactionActions'
import { TransactionStatusProps } from './hooks/useTransactionStatus'
import { TxTypeProps } from './hooks/useTransactionType'
import { StyledGroupedTransactions, StyledTransaction } from './styled'
import { TokenTransferAmount } from './TokenTransferAmount'
import { CalculatedVotes } from './TxQueueCollapsed'
const TxInfo = ({ info }: { info: AssetInfo }) => {
if (isTokenTransferAsset(info)) {
return <TokenTransferAmount assetInfo={info} />
}
if (isSettingsChangeTxInfo(info)) {
const UNKNOWN_MODULE = 'Unknown module'
switch (info.settingsInfo?.type) {
case 'SET_FALLBACK_HANDLER':
case 'ADD_OWNER':
case 'REMOVE_OWNER':
case 'SWAP_OWNER':
case 'CHANGE_THRESHOLD':
case 'CHANGE_IMPLEMENTATION':
break
case 'ENABLE_MODULE':
case 'DISABLE_MODULE':
return (
<Text size="xl" as="span">
{KNOWN_MODULES[info.settingsInfo.module] ?? UNKNOWN_MODULE}
</Text>
)
}
}
if (isCustomTxInfo(info)) {
if (isMultiSendTxInfo(info)) {
return (
<Text size="xl" as="span">
{info.actionCount} {`action${info.actionCount > 1 ? 's' : ''}`}
</Text>
)
}
return (
<Text size="xl" as="span">
{info.methodName}
</Text>
)
}
return null
}
const CircularProgressPainter = styled.div<{ color: ThemeColors }>`
color: ${({ theme, color }) => theme.colors[color]};
`
const SmallDot = styled(Dot)`
height: 8px;
width: 8px;
background-color: ${({ theme, color }) => theme.colors[color]} !important;
`
const IconText = styled(IconTextSrc)`
p {
font-weight: bold;
}
`
const useTooltipStyles = makeStyles(
createStyles(() => ({
arrow: {
color: 'white',
},
tooltip: {
backgroundColor: 'white',
color: 'rgba(0, 0, 0, 0.87)',
boxShadow: '#00000026 0 2px 4px 0',
fontSize: '14px',
lineHeight: '14px',
},
})),
)
const TooltipContent = styled.div`
width: max-content;
`
type TxCollapsedProps = {
transaction?: Transaction
isGrouped?: boolean
nonce?: number
type: TxTypeProps
info?: AssetInfo
time: number
votes?: CalculatedVotes
actions?: TransactionActions
status: TransactionStatusProps
}
export const TxCollapsed = ({
transaction,
isGrouped = false,
nonce,
type,
info,
time,
votes,
actions,
status,
}: TxCollapsedProps): ReactElement => {
const willBeReplaced = transaction?.txStatus === 'WILL_BE_REPLACED' ? ' will-be-replaced' : ''
const txCollapsedNonce = (
<div className={'tx-nonce' + willBeReplaced}>
<Text size="xl">{nonce}</Text>
</div>
)
const txCollapsedType = (
<div className={'tx-type' + willBeReplaced}>
<CustomIconText iconUrl={type.icon} text={type.text} />
</div>
)
const txCollapsedInfo = <div className={'tx-info' + willBeReplaced}>{info && <TxInfo info={info} />}</div>
const tooltipStyles = useTooltipStyles()
const timestamp = useRef<HTMLDivElement | null>(null)
const txCollapsedTime = (
<div className={'tx-time' + willBeReplaced}>
<Tooltip classes={tooltipStyles} title={formatDateTime(time)} arrow>
<TooltipContent ref={timestamp}>
<Text size="xl">{formatTime(time)}</Text>
</TooltipContent>
</Tooltip>
</div>
)
const txCollapsedVotes = (
<div className={'tx-votes' + willBeReplaced}>
{votes && (
<IconText
color={votes.required > votes.submitted ? 'secondaryLight' : 'primary'}
iconType="owners"
iconSize="sm"
text={`${votes.votes}`}
textSize="md"
/>
)}
</div>
)
const txCollapsedActions = (
<div className={'tx-actions' + willBeReplaced}>
{actions?.isUserAnOwner && transaction && <TxCollapsedActions transaction={transaction} />}
</div>
)
const txCollapsedStatus = (
<div className="tx-status">
{transaction?.txStatus === 'PENDING' || transaction?.txStatus === 'PENDING_FAILED' ? (
<CircularProgressPainter color={status.color}>
<CircularProgress size={14} color="inherit" />
</CircularProgressPainter>
) : (
(transaction?.txStatus === 'AWAITING_EXECUTION' || transaction?.txStatus === 'AWAITING_CONFIRMATIONS') && (
<SmallDot color={status.color} />
)
)}
<Text size="md" color={status.color} className="col" strong>
{status.text}
</Text>
</div>
)
return isGrouped ? (
<StyledGroupedTransactions>
{/* no nonce */}
{txCollapsedType}
{txCollapsedInfo}
{txCollapsedTime}
{txCollapsedVotes}
{txCollapsedActions}
{txCollapsedStatus}
</StyledGroupedTransactions>
) : (
<StyledTransaction>
{txCollapsedNonce}
{txCollapsedType}
{txCollapsedInfo}
{txCollapsedTime}
{txCollapsedVotes}
{txCollapsedActions}
{txCollapsedStatus}
</StyledTransaction>
)
}

View File

@ -0,0 +1,58 @@
import { Icon } from '@gnosis.pm/safe-react-components'
import { default as MuiIconButton } from '@material-ui/core/IconButton'
import React, { ReactElement } from 'react'
import styled from 'styled-components'
import { Transaction } from 'src/logic/safe/store/models/types/gateway.d'
import { useActionButtonsHandlers } from './hooks/useActionButtonsHandlers'
const IconButton = styled(MuiIconButton)`
padding: 8px !important;
&.Mui-disabled {
opacity: 0.4;
}
`
type TxCollapsedActionsProps = {
transaction: Transaction
}
export const TxCollapsedActions = ({ transaction }: TxCollapsedActionsProps): ReactElement => {
const {
canCancel,
handleConfirmButtonClick,
handleCancelButtonClick,
handleOnMouseEnter,
handleOnMouseLeave,
isPending,
disabledActions,
} = useActionButtonsHandlers(transaction)
return (
<>
{
<IconButton
size="small"
type="button"
onClick={handleConfirmButtonClick}
disabled={disabledActions}
onMouseEnter={handleOnMouseEnter}
onMouseLeave={handleOnMouseLeave}
>
<Icon
type={transaction.txStatus === 'AWAITING_EXECUTION' ? 'rocket' : 'check'}
color="primary"
size="sm"
tooltip={transaction.txStatus === 'AWAITING_EXECUTION' ? 'Execute' : 'Confirm'}
/>
</IconButton>
}
{canCancel && (
<IconButton size="small" type="button" onClick={handleCancelButtonClick} disabled={isPending}>
<Icon type="circleCross" color="error" size="sm" tooltip="Cancel" />
</IconButton>
)}
</>
)
}

View File

@ -0,0 +1,37 @@
import React, { ReactElement } from 'react'
import { ExpandedTxDetails } from 'src/logic/safe/store/models/types/gateway.d'
import { sameString } from 'src/utils/strings'
import { HexEncodedData } from './HexEncodedData'
import { MethodDetails } from './MethodDetails'
import { MultiSendDetails } from './MultiSendDetails'
type TxDataProps = {
txData: ExpandedTxDetails['txData']
}
export const TxData = ({ txData }: TxDataProps): ReactElement | null => {
// nothing to render
if (!txData) {
return null
}
// unknown tx information
if (!txData.dataDecoded) {
// no hex data, nothing to render
if (!txData.hexData) {
return null
}
// we render the hex encoded data
return <HexEncodedData hexData={txData.hexData} />
}
// known data and particularly `multiSend` data type
if (sameString(txData.dataDecoded.method, 'multiSend')) {
return <MultiSendDetails txData={txData} />
}
// we render the decoded data
return <MethodDetails data={txData.dataDecoded} />
}

View File

@ -0,0 +1,128 @@
import { Icon, Link, Loader, Text } from '@gnosis.pm/safe-react-components'
import cn from 'classnames'
import React, { ReactElement, useContext } from 'react'
import { useSelector } from 'react-redux'
import styled from 'styled-components'
import {
ExpandedTxDetails,
isMultiSendTxInfo,
isSettingsChangeTxInfo,
isTransferTxInfo,
MultiSigExecutionDetails,
Transaction,
} from 'src/logic/safe/store/models/types/gateway.d'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { TransactionActions } from './hooks/useTransactionActions'
import { useTransactionDetails } from './hooks/useTransactionDetails'
import { TxDetailsContainer, Centered, AlignItemsWithMargin } from './styled'
import { TxData } from './TxData'
import { TxExpandedActions } from './TxExpandedActions'
import { TxInfo } from './TxInfo'
import { TxLocationContext } from './TxLocationProvider'
import { TxOwners } from './TxOwners'
import { TxSummary } from './TxSummary'
import { isCancelTxDetails, NOT_AVAILABLE } from './utils'
const NormalBreakingText = styled(Text)`
line-break: normal;
word-break: normal;
`
const TxDataGroup = ({ txDetails }: { txDetails: ExpandedTxDetails }): ReactElement | null => {
const safeAddress = useSelector(safeParamAddressFromStateSelector)
if (isTransferTxInfo(txDetails.txInfo) || isSettingsChangeTxInfo(txDetails.txInfo)) {
return <TxInfo txInfo={txDetails.txInfo} />
}
if (isCancelTxDetails({ executedAt: txDetails.executedAt, txInfo: txDetails.txInfo, safeAddress })) {
return (
<>
<NormalBreakingText size="xl">
{`This is an empty cancelling transaction that doesn't send any funds.
Executing this transaction will replace all currently awaiting transactions with nonce ${
(txDetails.detailedExecutionInfo as MultiSigExecutionDetails).nonce ?? NOT_AVAILABLE
}.`}
</NormalBreakingText>
<Link
href="https://help.gnosis-safe.io/en/articles/4738501-why-do-i-need-to-pay-for-cancelling-a-transaction"
target="_blank"
rel="noreferrer"
title="Why do I need to pay for cancelling a transaction?"
>
<AlignItemsWithMargin>
<Text size="xl" as="span" color="primary">
Why do I need to pay for cancelling a transaction?
</Text>
<Icon size="sm" type="externalLink" color="primary" />
</AlignItemsWithMargin>
</Link>
</>
)
}
if (!txDetails.txData) {
return null
}
return <TxData txData={txDetails.txData} />
}
type TxDetailsProps = {
transaction: Transaction
actions?: TransactionActions
}
export const TxDetails = ({ transaction, actions }: TxDetailsProps): ReactElement => {
const { txLocation } = useContext(TxLocationContext)
const { data, loading } = useTransactionDetails(transaction.id)
if (loading) {
return (
<Centered padding={10}>
<Loader size="sm" />
</Centered>
)
}
if (!data) {
return (
<TxDetailsContainer>
<Text size="xl" strong>
No data available
</Text>
</TxDetailsContainer>
)
}
return (
<TxDetailsContainer>
<div className={cn('tx-summary', { 'will-be-replaced': transaction.txStatus === 'WILL_BE_REPLACED' })}>
<TxSummary txDetails={data} />
</div>
<div
className={cn('tx-details', {
'no-padding': isMultiSendTxInfo(data.txInfo),
'not-executed': !data.executedAt,
'will-be-replaced': transaction.txStatus === 'WILL_BE_REPLACED',
})}
>
<TxDataGroup txDetails={data} />
</div>
<div
className={cn('tx-owners', {
'no-owner': txLocation !== 'history' && !actions?.isUserAnOwner,
'will-be-replaced': transaction.txStatus === 'WILL_BE_REPLACED',
})}
>
<TxOwners detailedExecutionInfo={data.detailedExecutionInfo} />
</div>
{!data.executedAt && txLocation !== 'history' && actions?.isUserAnOwner && (
<div className={cn('tx-details-actions', { 'will-be-replaced': transaction.txStatus === 'WILL_BE_REPLACED' })}>
<TxExpandedActions transaction={transaction} />
</div>
)}
</TxDetailsContainer>
)
}

View File

@ -0,0 +1,49 @@
import { Button } from '@gnosis.pm/safe-react-components'
import React, { ReactElement } from 'react'
import { Transaction } from 'src/logic/safe/store/models/types/gateway.d'
import { useActionButtonsHandlers } from 'src/routes/safe/components/Transactions/GatewayTransactions/hooks/useActionButtonsHandlers'
type TxExpandedActionsProps = {
transaction: Transaction
}
export const TxExpandedActions = ({ transaction }: TxExpandedActionsProps): ReactElement | null => {
const {
canCancel,
handleConfirmButtonClick,
handleCancelButtonClick,
handleOnMouseEnter,
handleOnMouseLeave,
isPending,
disabledActions,
} = useActionButtonsHandlers(transaction)
const onExecuteOrConfirm = (event) => {
handleOnMouseLeave()
handleConfirmButtonClick(event)
}
// There is a problem in chrome that produces onMouseLeave event not being triggered properly.
// https://github.com/facebook/react/issues/4492
return (
<>
<Button
size="md"
color="primary"
disabled={disabledActions}
onClick={onExecuteOrConfirm}
onMouseEnter={handleOnMouseEnter}
onMouseLeave={handleOnMouseLeave}
className="primary"
>
{transaction.txStatus === 'AWAITING_EXECUTION' ? 'Execute' : 'Confirm'}
</Button>
{canCancel && (
<Button size="md" color="error" onClick={handleCancelButtonClick} className="error" disabled={isPending}>
Cancel
</Button>
)}
</>
)
}

View File

@ -0,0 +1,16 @@
import React, { ReactElement } from 'react'
import { Transaction } from 'src/logic/safe/store/models/types/gateway.d'
import { useAssetInfo } from './hooks/useAssetInfo'
import { useTransactionStatus } from './hooks/useTransactionStatus'
import { useTransactionType } from './hooks/useTransactionType'
import { TxCollapsed } from './TxCollapsed'
export const TxHistoryCollapsed = ({ transaction }: { transaction: Transaction }): ReactElement => {
const nonce = transaction.executionInfo?.nonce
const type = useTransactionType(transaction)
const info = useAssetInfo(transaction.txInfo)
const status = useTransactionStatus(transaction)
return <TxCollapsed nonce={nonce} type={type} info={info} time={transaction.timestamp} status={status} />
}

View File

@ -0,0 +1,29 @@
import { AccordionDetails } from '@gnosis.pm/safe-react-components'
import React, { ReactElement } from 'react'
import { isCreationTxInfo, Transaction } from 'src/logic/safe/store/models/types/gateway.d'
import { NoPaddingAccordion, StyledAccordionSummary } from './styled'
import { TxHistoryCollapsed } from './TxHistoryCollapsed'
import { TxDetails } from './TxDetails'
import { TxInfoCreation } from './TxInfoCreation'
export const TxHistoryRow = ({ transaction }: { transaction: Transaction }): ReactElement => (
<NoPaddingAccordion
TransitionProps={{
mountOnEnter: false,
unmountOnExit: true,
appear: true,
}}
>
<StyledAccordionSummary>
<TxHistoryCollapsed transaction={transaction} />
</StyledAccordionSummary>
<AccordionDetails>
{isCreationTxInfo(transaction.txInfo) ? (
<TxInfoCreation transaction={transaction} />
) : (
<TxDetails transaction={transaction} />
)}
</AccordionDetails>
</NoPaddingAccordion>
)

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