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:
parent
8c50cda0ad
commit
47d20aa645
|
@ -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",
|
||||
|
|
|
@ -50,7 +50,7 @@ const Footer = (): React.ReactElement => {
|
|||
const dispatch = useDispatch()
|
||||
|
||||
const openCookiesHandler = () => {
|
||||
dispatch(openCookieBanner(true))
|
||||
dispatch(openCookieBanner({ cookieBannerOpen: true }))
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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`
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()}`,
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import { Record } from 'immutable'
|
||||
|
||||
export const makeNotification = Record({
|
||||
key: 0,
|
||||
message: '',
|
||||
options: {},
|
||||
dismissed: false,
|
||||
})
|
|
@ -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)
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
}))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
|
@ -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))),
|
||||
),
|
||||
}),
|
||||
|
|
|
@ -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)
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{},
|
||||
)
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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'],
|
||||
)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 []
|
||||
},
|
||||
)
|
|
@ -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())
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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" />,
|
||||
|
|
|
@ -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
|
|
@ -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'
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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}
|
||||
</>
|
||||
)
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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} />
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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} />
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue