Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a warning icon if transfer cost is higher than token market value #236

Merged
merged 7 commits into from
Dec 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions apps/drain-safe/src/__tests__/App.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import React from 'react';
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { screen, waitFor, fireEvent } from '@testing-library/react';
import { within } from '@testing-library/dom';
import { useSafeAppsSDK } from '@gnosis.pm/safe-apps-react-sdk';
import useBalances from '../hooks/use-balances';
import { mockTxsRequest, mockInitialBalances, renderWithProviders } from '../utils/test-helpers';
import App from '../components/App';

Expand Down Expand Up @@ -58,14 +56,14 @@ jest.mock('web3', () => {

describe('<App />', () => {
it('should render the tokens in the safe balance', async () => {
const { container } = renderWithProviders(<App />);
renderWithProviders(<App />);

expect(await screen.findByText(/ether/i)).toBeInTheDocument();
expect(await screen.findByText(/0.949938510499549077/)).toBeInTheDocument();
});

it('should drain the safe when submit button is clicked', async () => {
const { container } = renderWithProviders(<App />);
renderWithProviders(<App />);
const { sdk } = useSafeAppsSDK();

await screen.findByText(/chainlink token/i);
Expand All @@ -75,7 +73,7 @@ describe('<App />', () => {
});

it('should drain the safe when submit button is clicked removing the tokens excluded by the user', async () => {
const { container } = renderWithProviders(<App />);
renderWithProviders(<App />);
const { sdk } = useSafeAppsSDK();
const checkboxes = await screen.findAllByRole('checkbox');

Expand All @@ -93,7 +91,7 @@ describe('<App />', () => {
});

it('should show an error if no recipient address is entered', async () => {
const { container } = renderWithProviders(<App />);
renderWithProviders(<App />);

await screen.findByText(/chainLink token/i);
fireEvent.click(screen.getByText(/transfer everything/i));
Expand All @@ -102,7 +100,7 @@ describe('<App />', () => {
});

it('should allow to order token by string prop', async () => {
const { container } = renderWithProviders(<App />);
renderWithProviders(<App />);

await screen.findByText(/chainlink token/i);
const assetColumnHeaderElement = screen.getByText(/asset/i);
Expand All @@ -124,7 +122,7 @@ describe('<App />', () => {
});

it('should allow to order token by numeric prop', async () => {
const { container } = renderWithProviders(<App />);
renderWithProviders(<App />);

await screen.findByText(/chainLink token/i);
const amountColumnHeaderElement = screen.getByText(/amount/i);
Expand All @@ -144,4 +142,25 @@ describe('<App />', () => {
expect(within(tableRows[0]).getByText(/maker/i)).toBeDefined();
});
});

it('Shows a Warning icon when token transfer cost is higher than its current market value ', async () => {
renderWithProviders(<App />);

const warningTooltip =
/Beware that the cost of this token transfer could be higher than its current market value \(Estimated transfer cost: /i;

await waitFor(() => {
const tableRows = document.querySelectorAll('tbody tr');

// warning only should be present in Maker (MKR) row
const makerRow = tableRows[3];
expect(within(makerRow).getByText(/maker/i)).toBeDefined();
expect(within(makerRow).queryByTitle(warningTooltip)).toBeInTheDocument();

// warning should NOT be present in other rows
expect(within(tableRows[0]).queryByTitle(warningTooltip)).not.toBeInTheDocument();
expect(within(tableRows[1]).queryByTitle(warningTooltip)).not.toBeInTheDocument();
expect(within(tableRows[2]).queryByTitle(warningTooltip)).not.toBeInTheDocument();
});
});
});
20 changes: 19 additions & 1 deletion apps/drain-safe/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { Title, Text } from '@gnosis.pm/safe-react-components';
import { useSafeAppsSDK } from '@gnosis.pm/safe-apps-react-sdk';
import web3Utils from 'web3-utils';
import { BigNumber } from 'bignumber.js';

import useBalances, { BalancesType } from '../hooks/use-balances';
import { tokenToTx } from '../utils/sdk-helpers';
import FormContainer from './FormContainer';
Expand All @@ -26,6 +28,7 @@ const App: React.FC = () => {
const [toAddress, setToAddress] = useState<string>('');
const [isFinished, setFinished] = useState<boolean>(false);
const [error, setError] = useState<string>('');
const [gasPrice, setGasPrice] = useState<BigNumber>(new BigNumber(0));
const [networkPrefix, setNetworkPrefix] = useState<string>('');

const onError = (userMsg: string, err: Error) => {
Expand Down Expand Up @@ -117,6 +120,14 @@ const App: React.FC = () => {
}
}, [balancesError]);

useEffect(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about creating a useWeb3 hook? for avoid passing down props to de final CurrencyCell. You can gather the chains.ts info there as well as we are going to remove it later

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it's a good idea to create a useWeb3 hook, but I think that we should keep it in the App.tsx because we only want instantiate web3 and call getGasPrice only once and not in each CurrencyCell.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for a separate hook, I find that it vastly improves readability

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could perhaps be a separate ticket @dasanra ? as TX Builder and Drain Safe can share the same hook and as @DaniSomoza mentioned me offline, we will need a context provider as well for avoid instance recreation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would we want to avoid it? In the drain-safe app, it's only used in one component and then passed down via props. To me, it looks like just moving it to a separate file

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For avoid the prop to only exist to be sent down. In this case in the Balances I think

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but the context is fine isn't it ? I mean, a context for the web3 instance and a hook to instantiate it using our getChainInfo populated with RPC url (task in progress) could be helpful for retrieving it in any level of the component tree

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done! Added useWeb3.js hook

sdk.eth.getGasPrice().then((gasPrice) => {
setGasPrice(new BigNumber(gasPrice));
});
}, [sdk.eth]);

const ethFiatPrice = Number(assets[0]?.fiatConversion || 0);

useEffect(() => {
const getChainInfo = async () => {
try {
Expand All @@ -136,7 +147,14 @@ const App: React.FC = () => {
<Logo />
<Title size="md">Drain Account</Title>
</Flex>
<Balances assets={assets} exclude={excludedTokens} onExcludeChange={handleExcludeChange} />
<Balances
ethFiatPrice={ethFiatPrice}
gasPrice={gasPrice}
web3={web3}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the useWeb3 new hook you don't really need to pass it down right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I decided to keep the const { web3 } = useWeb3(); and setGasPrice useState + useEffect in the App.tsx, But maybe makes sense to declare it directly in the Balances Component

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really in CurrencyCell. In Balances you don't need i think

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but as we talked in the other thread we will need a context provider for avoid instance recreation. And we will address it in a different ticket.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh right 😂 ... so let's improve it later

assets={assets}
exclude={excludedTokens}
onExcludeChange={handleExcludeChange}
/>
{error && <Text size="lg">{error}</Text>}
{isFinished && (
<Text size="lg">
Expand Down
30 changes: 25 additions & 5 deletions apps/drain-safe/src/components/Balances.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { useCallback, useMemo, useState } from 'react';
import { Table, Checkbox, TableSortDirection } from '@gnosis.pm/safe-react-components';
import { formatTokenValue, formatCurrencyValue } from '../utils/formatters';
import Icon from './Icon';
import { TokenBalance, TokenInfo } from '@gnosis.pm/safe-apps-sdk';
import BigNumber from 'bignumber.js';
import Web3 from 'web3';

import { formatTokenValue } from '../utils/formatters';
import Icon from './Icon';
import Flex from './Flex';
import { useCallback, useMemo, useState } from 'react';
import { getComparator } from '../utils/sort-helpers';
import CurrencyCell from './CurrencyCell';

const CURRENCY = 'USD';

Expand All @@ -28,10 +32,16 @@ function Balances({
assets,
exclude,
onExcludeChange,
gasPrice,
ethFiatPrice,
web3,
}: {
assets: TokenBalance[];
exclude: string[];
ethFiatPrice: number;
web3: Web3 | undefined;
onExcludeChange: (address: string, checked: boolean) => void;
gasPrice: BigNumber;
}): JSX.Element {
const [orderBy, setOrderBy] = useState<string | undefined>();
const [order, setOrder] = useState<TableSortDirection>(TableSortDirection.asc);
Expand Down Expand Up @@ -80,7 +90,17 @@ function Balances({
},

{ content: formatTokenValue(item.balance, token.decimals) },
{ content: formatCurrencyValue(item.fiatBalance, CURRENCY) },
{
content: (
<CurrencyCell
web3={web3}
ethFiatPrice={ethFiatPrice}
gasPrice={gasPrice}
item={item}
currency={CURRENCY}
/>
),
},

{
content: (
Expand All @@ -95,7 +115,7 @@ function Balances({
],
};
}),
[assets, exclude, handleExclusion, order, orderBy],
[assets, exclude, handleExclusion, order, orderBy, ethFiatPrice, gasPrice, web3],
);

return (
Expand Down
78 changes: 78 additions & 0 deletions apps/drain-safe/src/components/CurrencyCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { TokenBalance } from '@gnosis.pm/safe-apps-sdk';
import { Icon, Tooltip } from '@gnosis.pm/safe-react-components';
import BigNumber from 'bignumber.js';
import { useEffect, useState } from 'react';
import styled from 'styled-components';
import web3Utils from 'web3-utils';
import Web3 from 'web3';

import { formatCurrencyValue } from '../utils/formatters';
import { tokenToTx } from '../utils/sdk-helpers';
import Flex from './Flex';
import { useSafeAppsSDK } from '@gnosis.pm/safe-apps-react-sdk';

function CurrencyCell({
item,
currency,
gasPrice,
ethFiatPrice,
web3,
}: {
item: TokenBalance;
currency: string;
gasPrice: BigNumber;
ethFiatPrice: number;
web3: Web3 | undefined;
}) {
const label = formatCurrencyValue(item.fiatBalance, currency);
const [transferCostInFiat, setTransferCostInFiat] = useState(new BigNumber(0));

const { safe } = useSafeAppsSDK();

// Transfer cost estimation
useEffect(() => {
const estimateTransferCost = async () => {
try {
const sendTokenTx = tokenToTx(safe.safeAddress, item);

const estimatedTransferGas = await web3?.eth.estimateGas({ ...sendTokenTx, from: safe.safeAddress });

const gasCostInWei = gasPrice.multipliedBy(estimatedTransferGas || 21000);
const gasCostInEther = new BigNumber(web3Utils.fromWei(gasCostInWei.toString(), 'ether'));

const transferCostInFiat = gasCostInEther.multipliedBy(ethFiatPrice);

setTransferCostInFiat(transferCostInFiat);
} catch (e) {
console.log('Error: ', e);
}
};
estimateTransferCost();
}, [gasPrice, ethFiatPrice, item, web3, safe]);

// if transfer cost is higher than token market value, we show a warning icon & tooltip in the cell
const showWarningIcon =
ethFiatPrice > 0 && gasPrice.toNumber() > 0 && transferCostInFiat.toNumber() >= Number(item.fiatBalance);

const warningTooltip = `Beware that the cost of this token transfer could be higher than its current market value (Estimated transfer cost: ${formatCurrencyValue(
transferCostInFiat.toString(),
currency,
)})`;

return showWarningIcon ? (
<Tooltip title={warningTooltip} placement="top">
<Flex>
{label}
<StyledIcon size="md" type="alert" color="warning" />
</Flex>
</Tooltip>
) : (
<Flex>{label}</Flex>
);
}

export default CurrencyCell;

const StyledIcon = styled(Icon)`
margin-left: 4px;
`;
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1627,6 +1627,14 @@
"@gnosis.pm/safe-react-gateway-sdk" "^2.5.6"
ethers "^5.4.7"

"@gnosis.pm/safe-apps-sdk@6.1.1":
version "6.1.1"
resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-apps-sdk/-/safe-apps-sdk-6.1.1.tgz#6a4e19c475967a0d04a8c04559e05703484e2ca8"
integrity sha512-MI2gIdQUlu1NrDt5rF47xey1LSiQoy5Nb+vqn16PM5qVc3FjR+nPNG8+1yGlL96di31z+kyS2J8DzHiea7+TMA==
dependencies:
"@gnosis.pm/safe-react-gateway-sdk" "^2.5.6"
ethers "^5.4.7"

"@gnosis.pm/safe-react-components@^0.5.0":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-react-components/-/safe-react-components-0.5.0.tgz#ce72f3f0674d33bac507bf6409462655fb55f096"
Expand Down