diff --git a/.storybook/main.js b/.storybook/main.js index 0751908e2..ccbf202e1 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -34,6 +34,20 @@ module.exports = { customRules: custom.module.rules, }) + const rulesWithMarkdownAndImages = replaceRulesForFile({ + filename: 'token_address.png', + baseRules: rulesWithMarkdown, + customRules: [{ + // what storybook uses + test: /\.(svg|ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/, + loader: 'file-loader', + // only change from storybook -- esModule: false -> true + // this way require() and require.context need module.default + // same as in webpack config + options: { name: 'static/media/[name].[hash:8].[ext]', esModule: true } + }] + }) + return { ...config, module: { @@ -41,7 +55,7 @@ module.exports = { // enable rules override sparingly // only if storybook can't transpile something // rules: custom.module.rules, - rules: rulesWithMarkdown, + rules: rulesWithMarkdownAndImages, }, resolve: { ...config.resolve, diff --git a/config-default.yaml b/config-default.yaml index 6b94fe260..a6c653520 100644 --- a/config-default.yaml +++ b/config-default.yaml @@ -53,6 +53,12 @@ dexPriceEstimator: url_production: https://dex-price-estimator.rinkeby.gnosis.io url_develop: https://price-estimate-rinkeby.dev.gnosisdev.com + - networkId: 100 + # TODO: No production URL yet. For now, using DEV also in PRODUCTION + #url_production: https://dex-price-estimator.xdai.gnosis.io + url_production: https://price-estimate-xdai.dev.gnosisdev.com + url_develop: https://price-estimate-xdai.dev.gnosisdev.com + # Subgraph abstraction, used for getting the last price theGraphApi: type: 'the-graph' # choices: the-graph @@ -63,6 +69,9 @@ theGraphApi: - networkId: 4 url: https://api.thegraph.com/subgraphs/name/gnosis/protocol-rinkeby + - networkId: 100 + url: https://api.thegraph.com/subgraphs/name/gnosis/protocol-xdai + # Eth node config defaultProviderConfig: type: 'infura' # Choices: infura | url @@ -84,6 +93,8 @@ exchangeContractConfig: blockNumber: 9340147 - networkId: 4 blockNumber: 5844678 + - networkId: 100 + blockNumber: 11948310 # Wallet Connect walletConnect: @@ -133,6 +144,7 @@ disabledTokens: - address: '0x6Fb0855c404E09c47C3fBCA25f08d4E41f9F062f' - address: '0xFC4B8ED459e00e5400be803A9BB3954234FD50e3' 4: [] # Rinkeby + 100: [] # xDAI # Initial token selection for the sell token and buy token initialTokenSelection: @@ -150,6 +162,7 @@ initialTokenList: addressByNetwork: '1': '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' '4': '0xc778417E063141139Fce010982780140Aa0cD5Ab' + '100': '0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1' - id: 2 name: Tether USD @@ -158,6 +171,7 @@ initialTokenList: addressByNetwork: '1': '0xdAC17F958D2ee523a2206206994597C13D831ec7' '4': '0xa9881E6459CA05d7D7C95374463928369cD7a90C' + '100': '0x4ECaBa5870353805a9F068101A40E0f32ed605C6' - id: 3 name: TrueUSD @@ -165,7 +179,7 @@ initialTokenList: decimals: 18 addressByNetwork: '1': '0x0000000000085d4780B73119b644AE5ecd22b376' - '4': '0x0000000000085d4780B73119b644AE5ecd22b376' + '4': '0x0000000000085d4780B73119b644AE5ecd22b376' - id: 4 name: USD Coin @@ -174,6 +188,7 @@ initialTokenList: addressByNetwork: '1': '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' '4': '0x4DBCdF9B62e891a7cec5A2568C3F4FAF9E8Abe2b' + '100': '0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83' - id: 5 name: Paxos Standard @@ -198,6 +213,7 @@ initialTokenList: addressByNetwork: '1': '0x6B175474E89094C44Da98b954EedeAC495271d0F' '4': '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa' + '100': '0x44fA8E6f47987339850636F88629646662444217' - id: 8 name: Synth sETH @@ -214,6 +230,7 @@ initialTokenList: addressByNetwork: '1': '0x57Ab1E02fEE23774580C119740129eAC7081e9D3' '4': '0x1b642a124cdfa1e5835276a6ddaa6cfc4b35d52c' + '100': '0xB1950Fb2C9C0CbC8553578c67dB52Aa110A93393' - id: 10 name: Synth sBTC @@ -258,6 +275,7 @@ initialTokenList: addressByNetwork: '1': '0x6810e776880C02933D47DB1b9fc05908e5386b96' '4': '0xd0Dab4E640D95E9E8A47545598c33e31bDb53C7c' + '100': '0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb' - id: 19 name: Panvala pan @@ -266,6 +284,22 @@ initialTokenList: addressByNetwork: '1': '0xD56daC73A4d6766464b38ec6D91eB45Ce7457c44' + - id: 20 + name: STAKE + symbol: STAKE + decimals: 18 + addressByNetwork: + '1': '0x0ae055097c6d159879521c384f1d2123d1f195e6' + '100': '0xb7D311E2Eb55F2f68a9440da38e7989210b9A05e' + + - id: 21 + name: Wrapped xDAI + symbol: wxDAI + decimals: 18 + addressByNetwork: + '100': '0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d' + + - id: 0 name: OWL symbol: OWL @@ -273,3 +307,4 @@ initialTokenList: addressByNetwork: '1': '0x1A5F9352Af8aF974bFC03399e3767DF6370d82e4' '4': '0xa7D1C04fAF998F9161fC9F800a99A809b84cfc9D' + '100': '0x0905Ab807F8FD040255F0cF8fa14756c1D824931' diff --git a/src/App.tsx b/src/App.tsx index fb099b41a..cdce60395 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -124,7 +124,7 @@ function getInitialUrl(): string { const { sellToken: initialSellToken, receiveToken: initialReceiveToken } = CONFIG.initialTokenSelection assertNonNull(initialSellToken, 'sellToken is required in the initialTokenSelection config') assertNonNull(initialReceiveToken, 'receiveToken is required in the initialTokenSelection config') - return '/trade/' + encodeSymbol(initialSellToken) + '-' + encodeSymbol(initialReceiveToken) + '?sell=0&price=0' + return '/trade/' + encodeSymbol(initialReceiveToken) + '-' + encodeSymbol(initialSellToken) + '?sell=0&price=0' } const initialUrl = getInitialUrl() diff --git a/src/api/deposit/DepositApi.ts b/src/api/deposit/DepositApi.ts index 38146f001..6b20ce23b 100644 --- a/src/api/deposit/DepositApi.ts +++ b/src/api/deposit/DepositApi.ts @@ -1,7 +1,7 @@ import BN from 'bn.js' -import { assert } from '@gnosis.pm/dex-js' +import { assert, toBN } from '@gnosis.pm/dex-js' -import { logDebug, toBN } from 'utils' +import { logDebug } from 'utils' import { ZERO } from 'const' import { BatchExchangeContract, batchExchangeAbi } from '@gnosis.pm/dex-js' diff --git a/src/api/deposit/batchExchangeAddresses.ts b/src/api/deposit/batchExchangeAddresses.ts index c659877c6..a705ecc58 100644 --- a/src/api/deposit/batchExchangeAddresses.ts +++ b/src/api/deposit/batchExchangeAddresses.ts @@ -9,6 +9,7 @@ function _getAddress(networkId: number): string { const addressesByNetwork = { [Network.Rinkeby]: _getAddress(Network.Rinkeby), [Network.Mainnet]: _getAddress(Network.Mainnet), + [Network.xDAI]: _getAddress(Network.xDAI), } export const getAddressForNetwork = (networkId: Network): string | null => { diff --git a/src/api/erc20/Erc20Api.ts b/src/api/erc20/Erc20Api.ts index 9ea4c80c2..f4130a2d7 100644 --- a/src/api/erc20/Erc20Api.ts +++ b/src/api/erc20/Erc20Api.ts @@ -1,11 +1,11 @@ import BN from 'bn.js' import { AbiItem } from 'web3-utils' -import { Erc20Contract } from '@gnosis.pm/dex-js' +import { Erc20Contract, toBN } from '@gnosis.pm/dex-js' import erc20Abi from '@gnosis.pm/dex-js/build/contracts/abi/Erc20.json' import { TxOptionalParams, Receipt } from 'types' import { ZERO } from 'const' -import { logDebug, toBN } from 'utils' +import { logDebug } from 'utils' import ERC20_DETAILS from 'api/erc20/erc20Details.json' diff --git a/src/api/gasStation.ts b/src/api/gasStation.ts index 602524c6d..d3566c594 100644 --- a/src/api/gasStation.ts +++ b/src/api/gasStation.ts @@ -3,6 +3,10 @@ import { WalletApi } from './wallet/WalletApi' const GAS_STATIONS = { 1: 'https://safe-relay.gnosis.pm/api/v1/gas-station/', 4: 'https://safe-relay.staging.gnosisdev.com/api/v1/gas-station/', + // Pending price estimation + + // TODO: Pending review what makes sense here. How do gas prices are stablished in xDAI network + 100: undefined, } export type GasPriceLevel = Exclude diff --git a/src/api/index.ts b/src/api/index.ts index eb8d55fe3..2499e50b5 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -115,7 +115,7 @@ function createExchangeApi(erc20Api: Erc20Api, injectedDependencies: DepositApiD } function createTokenListApi(): TokenList { - const networkIds = [Network.Mainnet, Network.Rinkeby] + const networkIds = [Network.Mainnet, Network.Rinkeby, Network.xDAI] let tokenListApi: TokenList if (process.env.MOCK_TOKEN_LIST === 'true') { diff --git a/src/api/wallet/WalletApi.ts b/src/api/wallet/WalletApi.ts index 6a6ab9e24..382be51e3 100644 --- a/src/api/wallet/WalletApi.ts +++ b/src/api/wallet/WalletApi.ts @@ -2,6 +2,7 @@ import { Network, Command } from 'types' import BN from 'bn.js' import assert from 'assert' import { getDefaultProvider } from '..' +import { toBN } from '@gnosis.pm/dex-js' import Web3Modal, { getProviderInfo, IProviderOptions, IProviderInfo, isMobile } from 'web3modal' import { IClientMeta } from '@walletconnect/types' @@ -9,7 +10,7 @@ import { IClientMeta } from '@walletconnect/types' import Web3 from 'web3' import { BlockHeader } from 'web3-eth' -import { logDebug, toBN, txDataEncoder, generateWCOptions } from 'utils' +import { logDebug, txDataEncoder, generateWCOptions } from 'utils' import { subscribeToWeb3Event } from './subscriptionHelpers' import { getMatchingScreenSize, subscribeToScreenSizeChange } from 'utils/mediaQueries' diff --git a/src/api/wallet/WalletApiMock.ts b/src/api/wallet/WalletApiMock.ts index 87692f5f0..570cb4709 100644 --- a/src/api/wallet/WalletApiMock.ts +++ b/src/api/wallet/WalletApiMock.ts @@ -1,8 +1,9 @@ import { Network, Command } from 'types' import BN from 'bn.js' import assert from 'assert' +import { toWei } from '@gnosis.pm/dex-js' -import { logDebug, wait, toWei } from 'utils' +import { logDebug, wait } from 'utils' import { USER_1, USER_2 } from '../../../test/data' import { WalletApi, WalletInfo, ProviderInfo } from './WalletApi' diff --git a/src/api/weth/WethApi.ts b/src/api/weth/WethApi.ts index 6e42452ac..1a530cf4f 100644 --- a/src/api/weth/WethApi.ts +++ b/src/api/weth/WethApi.ts @@ -1,7 +1,7 @@ import Web3 from 'web3' import { Network, WithTxOptionalParams, Receipt } from 'types' -import { WETH_ADDRESS_MAINNET, WETH_ADDRESS_RINKEBY } from 'const' +import { WETH_ADDRESS_MAINNET, WETH_ADDRESS_RINKEBY, WXDAI_ADDRESS_XDAI } from 'const' import { wethAbi } from '@gnosis.pm/dex-js' import { logDebug } from 'utils' @@ -31,6 +31,10 @@ function getWethAddressByNetwork(networkId: number): string { return WETH_ADDRESS_MAINNET case Network.Rinkeby: return WETH_ADDRESS_RINKEBY + case Network.xDAI: + // Is not wxDAI is not WETH, but it has the same approve/withdraw methods + // it's just convenient to not rename the API and keep calling it WethApi although it wraps also xDAI + return WXDAI_ADDRESS_XDAI default: throw new Error(`WethApi was not deployed to network ${networkId}`) } diff --git a/src/assets/img/tokens/0xb7D311E2Eb55F2f68a9440da38e7989210b9A05e.png b/src/assets/img/tokens/0xb7D311E2Eb55F2f68a9440da38e7989210b9A05e.png new file mode 100644 index 000000000..31ef3197c Binary files /dev/null and b/src/assets/img/tokens/0xb7D311E2Eb55F2f68a9440da38e7989210b9A05e.png differ diff --git a/src/components/DepositWidget/Row.tsx b/src/components/DepositWidget/Row.tsx index b671c5954..f897ebfea 100644 --- a/src/components/DepositWidget/Row.tsx +++ b/src/components/DepositWidget/Row.tsx @@ -7,8 +7,8 @@ import { faClock, faPlus, faMinus, faBaby } from '@fortawesome/free-solid-svg-ic import { MinusSVG, PlusSVG } from 'assets/img/SVG' // const, utils, types -import { ZERO, MEDIA, WETH_ADDRESS_MAINNET } from 'const' -import { formatSmart, formatAmountFull } from 'utils' +import { ZERO, MEDIA } from 'const' +import { formatSmart, formatAmountFull, getIsWrappable, getNativeTokenName } from 'utils' import { TokenBalanceDetails, Command } from 'types' // Components @@ -27,6 +27,7 @@ import { TokenSymbol } from 'components/TokenSymbol' import { HelpTooltip, HelpTooltipContainer } from 'components/Tooltip' export interface RowProps extends Record { + networkId: number ethBalance: BN | null tokenBalances: TokenBalanceDetails onSubmitDeposit: (amount: BN, onTxHash: (hash: string) => void) => Promise @@ -47,6 +48,7 @@ const ImmatureClaimTooltip: React.FC<{ displayName: string }> = ({ displayName } export const Row: React.FC = (props: RowProps) => { const { + networkId, ethBalance, tokenBalances, onSubmitDeposit, @@ -96,7 +98,9 @@ export const Row: React.FC = (props: RowProps) => { const isDepositFormVisible = visibleForm == 'deposit' const isWithdrawFormVisible = visibleForm == 'withdraw' - const isWeth = addressMainnet === WETH_ADDRESS_MAINNET + + const isWrappable = getIsWrappable(networkId, address) + const { nativeToken, wrappedToken } = getNativeTokenName(networkId) return ( <> @@ -153,20 +157,20 @@ export const Row: React.FC = (props: RowProps) => { - {isWeth ? ( + {isWrappable ? (
  • - {ethBalance ? formatSmart(ethBalance, decimals) : '-'} ETH + {ethBalance ? formatSmart(ethBalance, decimals) : '-'} {nativeToken} {' '}
  • {(claiming || depositing) && spinner} - {formatSmart(walletBalance, decimals)} WETH + {formatSmart(walletBalance, decimals)} {wrappedToken} {' '}
  • diff --git a/src/components/DepositWidget/index.tsx b/src/components/DepositWidget/index.tsx index 98d6e6573..6ea16b38a 100644 --- a/src/components/DepositWidget/index.tsx +++ b/src/components/DepositWidget/index.tsx @@ -31,6 +31,7 @@ import useDataFilter from 'hooks/useDataFilter' // Reducer/Actions import { TokenLocalState } from 'reducers-actions' +import { useWalletConnection } from 'hooks/useWalletConnection' interface WithdrawState { amount: BN @@ -254,6 +255,7 @@ const BalancesDisplay: React.FC = ({ requestWithdrawConfirmation, hasTokensToShow = false, }) => { + const { networkIdOrDefault } = useWalletConnection() const windowSpecs = useWindowSizes() const [{ localTokens }] = useGlobalState() @@ -332,6 +334,7 @@ const BalancesDisplay: React.FC = ({ {displayedBalances && displayedBalances.length > 0 ? ( displayedBalances.map((tokenBalances) => ( > = (props) => {  Pending...
    - {transactionHash && View status} />} + {transactionHash && ( + View status} /> + )} ) } diff --git a/src/components/OrdersWidget/index.tsx b/src/components/OrdersWidget/index.tsx index f3cf0633e..65d28b1e7 100644 --- a/src/components/OrdersWidget/index.tsx +++ b/src/components/OrdersWidget/index.tsx @@ -122,6 +122,11 @@ const OrdersWidget: React.FC = ({ displayOnly }) => { [allTrades, displayOnly], ) + const settledAndNotRevertedTrades = useMemo( + () => trades.filter((trade) => trade && isTradeSettled(trade) && !isTradeReverted(trade)), + [trades], + ) + const tabList = useMemo[]>( () => [ { @@ -130,7 +135,7 @@ const OrdersWidget: React.FC = ({ displayOnly }) => { }, { type: 'trades', - count: trades.length, + count: settledAndNotRevertedTrades.length, }, { type: 'closed', @@ -141,7 +146,7 @@ const OrdersWidget: React.FC = ({ displayOnly }) => { classifiedOrders.active.orders.length, classifiedOrders.active.pendingOrders.length, classifiedOrders.closed.orders.length, - trades.length, + settledAndNotRevertedTrades.length, ], ) @@ -300,11 +305,6 @@ const OrdersWidget: React.FC = ({ displayOnly }) => { [deleteOrders, forceOrdersRefresh, markedForDeletion, selectedTab, setClassifiedOrders], ) - const settledAndNotRevertedTrades = useMemo( - () => trades.filter((trade) => trade && isTradeSettled(trade) && !isTradeReverted(trade)), - [trades], - ) - const { filteredData: filteredTrades, handlers: { handleSearch: handleTradesSearch, clearFilters: clearTradesFilters }, diff --git a/src/components/PoolingWidget/AddFunding.tsx b/src/components/PoolingWidget/AddFunding.tsx index 5da4a3a49..ff960e43f 100644 --- a/src/components/PoolingWidget/AddFunding.tsx +++ b/src/components/PoolingWidget/AddFunding.tsx @@ -8,7 +8,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faSpinner, faCheckCircle } from '@fortawesome/free-solid-svg-icons' // componet -import { EtherscanLink } from 'components/common/EtherscanLink' +import { BlockExplorerLink } from 'components/common/BlockExplorerLink' // PoolingWidget: subcomponent import { HighlightDiv } from 'components/PoolingWidget/PoolingWidget.styled' @@ -26,7 +26,7 @@ const AddFunding: React.FC = ({ txIdentifier, txReceipt }) => ( You can check the transactions status on:{' '} - +

    {txReceipt diff --git a/src/components/TradeWidget/TokenRow.tsx b/src/components/TradeWidget/TokenRow.tsx index 88c77d784..e4f652224 100644 --- a/src/components/TradeWidget/TokenRow.tsx +++ b/src/components/TradeWidget/TokenRow.tsx @@ -5,8 +5,16 @@ import { useFormContext } from 'react-hook-form' // types, const and utils import { TokenDetails, TokenBalanceDetails } from 'types' -import { ZERO, MEDIA, WETH_ADDRESS_MAINNET } from 'const' -import { formatSmart, formatAmountFull, parseAmount, validInputPattern, validatePositiveConstructor } from 'utils' +import { ZERO, MEDIA } from 'const' +import { + formatSmart, + formatAmountFull, + parseAmount, + validInputPattern, + validatePositiveConstructor, + getIsWrappable, + getNativeTokenName, +} from 'utils' // components import TokenSelector from 'components/TokenSelector' @@ -27,6 +35,7 @@ import { faPlus } from '@fortawesome/free-solid-svg-icons' import useNoScroll from 'hooks/useNoScroll' import { useNumberInput } from 'components/TradeWidget/useNumberInput' import { useRowActions } from 'components/DepositWidget/useRowActions' +import { useWalletConnection } from 'hooks/useWalletConnection' const Wrapper = styled.div` display: flex; @@ -172,6 +181,7 @@ const TokenRow: React.FC = ({ userConnected = true, autoFocus, }) => { + const { networkIdOrDefault } = useWalletConnection() const { register, errors, setValue, watch } = useFormContext() const error = errors[inputId] @@ -205,7 +215,8 @@ const TokenRow: React.FC = ({ const editableAndConnected = !readOnly && userConnected const showEnableToken = editableAndConnected && tokenDisabled - const isWeth = balance.addressMainnet === WETH_ADDRESS_MAINNET + const { nativeToken } = getNativeTokenName(networkIdOrDefault) + const isWrappable = getIsWrappable(networkIdOrDefault, balance.address) const errorOrWarning = error?.message ? ( @@ -292,7 +303,7 @@ const TokenRow: React.FC = ({ )} )} - {editableAndConnected && isWeth && } + {editableAndConnected && isWrappable && } Balance: diff --git a/src/components/TradesWidget/TradeRow.tsx b/src/components/TradesWidget/TradeRow.tsx index 592ff99d4..1fc3e2d35 100644 --- a/src/components/TradesWidget/TradeRow.tsx +++ b/src/components/TradesWidget/TradeRow.tsx @@ -6,7 +6,7 @@ import { formatPrice, formatSmart, formatAmountFull, invertPrice, DEFAULT_PRECIS import { Trade, TradeType } from 'api/exchange/ExchangeApi' -import { EtherscanLink } from 'components/common/EtherscanLink' +import { BlockExplorerLink } from 'components/common/BlockExplorerLink' import { EllipsisText } from 'components/common/EllipsisText' import { FoldableRowWrapper } from 'components/layout/SwapLayout/Card' @@ -175,7 +175,7 @@ export const TradeRow: React.FC = (params) => { {orderId} - + ) diff --git a/src/components/TradesWidget/index.tsx b/src/components/TradesWidget/index.tsx index 00619dc0e..90babddbc 100644 --- a/src/components/TradesWidget/index.tsx +++ b/src/components/TradesWidget/index.tsx @@ -4,7 +4,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faFileCsv } from '@fortawesome/free-solid-svg-icons' import styled from 'styled-components' -import { formatPrice, formatAmount, invertPrice, formatAmountFull } from '@gnosis.pm/dex-js' +import { formatPrice, formatAmount, invertPrice, formatAmountFull, getNetworkFromId } from '@gnosis.pm/dex-js' import FilterTools from 'components/FilterTools' import { CardTable } from 'components/layout/SwapLayout/Card' @@ -22,7 +22,7 @@ import { Trade } from 'api/exchange/ExchangeApi' import { toCsv, CsvColumns } from 'utils/csv' import { filterTradesFn } from 'utils/filter' -import { getNetworkFromId, divideBN } from 'utils' +import { divideBN } from 'utils' import { symbolOrAddress } from 'utils/display' const OverflowContainer = styled(BalancesWidget)` diff --git a/src/components/TxNotification.tsx b/src/components/TxNotification.tsx index b42c53b47..64a47b503 100644 --- a/src/components/TxNotification.tsx +++ b/src/components/TxNotification.tsx @@ -1,12 +1,12 @@ import React from 'react' -import { EtherscanLink } from 'components/common/EtherscanLink' +import { BlockExplorerLink } from 'components/common/BlockExplorerLink' interface TxNotificationProps { txHash: string } export const TxNotification: React.FC = ({ txHash }: TxNotificationProps) => { - const link = + const link = if (link) { return

    The transaction has been sent! Check {link} for details
    diff --git a/src/components/UserWallet/UserWallet.styled.ts b/src/components/UserWallet/UserWallet.styled.ts index 8cb550216..f73396b20 100644 --- a/src/components/UserWallet/UserWallet.styled.ts +++ b/src/components/UserWallet/UserWallet.styled.ts @@ -62,7 +62,7 @@ const WalletButton = styled.button` } ` -export const EtherscanButton = styled(WalletButton)` +export const BlockExplorerButton = styled(WalletButton)` background: transparent; color: var(--color-text-primary); > a, diff --git a/src/components/UserWallet/WalletComponent.tsx b/src/components/UserWallet/WalletComponent.tsx index e21841ecf..8f673d326 100644 --- a/src/components/UserWallet/WalletComponent.tsx +++ b/src/components/UserWallet/WalletComponent.tsx @@ -9,7 +9,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faSignOutAlt, faSignInAlt, faCopy, faCheck, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons' // components -import { EtherscanLink } from 'components/common/EtherscanLink' +import { BlockExplorerLink } from 'components/common/BlockExplorerLink' import { UserWalletItem, @@ -24,7 +24,7 @@ import { LogInOutButton, WalletName, WalletImage, - EtherscanButton, + BlockExplorerButton, } from './UserWallet.styled' import { useWalletConnection } from 'hooks/useWalletConnection' @@ -32,11 +32,12 @@ import useSafeState from 'hooks/useSafeState' import { useConnectWallet } from 'hooks/useConnectWallet' import useNoScroll from 'hooks/useNoScroll' -import { abbreviateString, getNetworkFromId } from 'utils' +import { abbreviateString } from 'utils' // TODO: probably not do this import WalletImg from 'assets/img/eth-network.svg' import { Spinner } from 'components/common/Spinner' import { walletApi } from 'api' +import { getNetworkFromId } from '@gnosis.pm/dex-js' interface UserWalletProps extends RouteComponentProps { className: string @@ -182,9 +183,8 @@ const UserWallet: React.FC = (props: UserWalletProps) => { {/* Etherscan Link */} {/* TODO: add network specific */} - - + = (props: UserWalletProps) => { } /> - + {/* Log In/Out Button */} {renderLogInOutButton()} diff --git a/src/components/WrapEtherBtn.tsx b/src/components/WrapEtherBtn.tsx index 843a2c6b0..7d4bd5d55 100644 --- a/src/components/WrapEtherBtn.tsx +++ b/src/components/WrapEtherBtn.tsx @@ -7,7 +7,7 @@ import BN from 'bn.js' import { DEFAULT_PRECISION, formatAmountFull, toWei, parseAmount, ZERO } from '@gnosis.pm/dex-js' // utils -import { validatePositiveConstructor, validInputPattern, logDebug } from 'utils' +import { validatePositiveConstructor, validInputPattern, logDebug, getIsWrappable, getNativeTokenName } from 'utils' // components import { DEFAULT_MODAL_OPTIONS, ModalBodyWrapper } from 'components/Modal' @@ -18,12 +18,12 @@ import { InputBox } from 'components/InputBox' import useSafeState from 'hooks/useSafeState' import { useWrapUnwrapEth } from 'hooks/useWrapUnwrapEth' import { useTokenBalances } from 'hooks/useTokenBalances' -import { WETH_ADDRESS_MAINNET } from 'const' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faSpinner } from '@fortawesome/free-solid-svg-icons' import { composeOptionalParams } from 'utils/transaction' import { toast } from 'toastify' import { useEthBalances } from 'hooks/useEthBalance' +import { useWalletConnection } from 'hooks/useWalletConnection' export const INPUT_ID_AMOUNT = 'wrapAmount' @@ -111,6 +111,8 @@ interface WrapUnwrapInfo { } interface GetModalParams { + nativeToken: string + wrappedToken: string wrap: boolean wethHelpVisible: boolean showWethHelp: React.Dispatch> @@ -121,16 +123,33 @@ interface GetModalParams { } function getModalParams(params: GetModalParams): WrapUnwrapInfo { - const { wrap, wethHelpVisible, showWethHelp, wethBalance, ethBalance, unwrappingWeth, wrappingEth } = params + const { + nativeToken, + wrappedToken, + wrap, + wethHelpVisible, + showWethHelp, + wethBalance, + ethBalance, + unwrappingWeth, + wrappingEth, + } = params const WethHelp = (

    - Gnosis Protocol allows the exchange of any ERC20 token. As ETH is not an ERC20 token, it must first be wrapped. + Gnosis Protocol allows the exchange of any ERC20 token. As {nativeToken} is not an ERC20 token, it must first be + wrapped.

    -

    By wrapping ETH you will be minting your submitted amount as WETH.

    - ETH can be wrapped as WETH anytime. Equally, WETH can be unwrapped back into ETH + By wrapping {nativeToken} you will be minting your submitted amount as {wrappedToken}.

    +

    + {nativeToken} can be wrapped as {wrappedToken} anytime. Equally, {wrappedToken} can be unwrapped{' '} + back into {nativeToken} +

    + {nativeToken === 'xDAI' && ( +

    {wrappedToken} is a wrapped native token, in the same way WETH is to Ether in the Mainnet network.

    + )}

    Learn more about WETH{' '} @@ -144,7 +163,8 @@ function getModalParams(params: GetModalParams): WrapUnwrapInfo { const description = ( <>

    - Wrap ETH into WETH, so it can later be deposited into the exchange. {wethHelpVisible && WethHelp} + Wrap {nativeToken} into {wrappedToken}, so it can later be deposited into the exchange.{' '} + {wethHelpVisible && WethHelp} showWethHelp(!wethHelpVisible)}> {wethHelpVisible ? '[-] Show less...' : '[+] Show more...'} @@ -153,16 +173,16 @@ function getModalParams(params: GetModalParams): WrapUnwrapInfo { ) const tooltipText = (

    - Wrapping converts ETH into WETH, + Wrapping converts {nativeToken} into {wrappedToken},
    - so it can be deposited into the exchange) + so it can be deposited into the exchange
    ) return { - title: 'Wrap ETH', + title: 'Wrap ' + nativeToken, amountLabel: 'Amount to Wrap', - symbolSource: 'ETH', + symbolSource: nativeToken, balance: ethBalance, description, tooltipText, @@ -172,7 +192,7 @@ function getModalParams(params: GetModalParams): WrapUnwrapInfo { const description = ( <>

    - Unwrapping converts WETH back into ETH.{' '} + Unwrapping converts {wrappedToken} back into {nativeToken}.{' '} {!wethHelpVisible && showWethHelp(true)}>Learn more...}

    {wethHelpVisible && WethHelp} @@ -180,12 +200,12 @@ function getModalParams(params: GetModalParams): WrapUnwrapInfo { ) return { - title: 'Unwrap WETH', + title: 'Unwrap ' + wrappedToken, amountLabel: 'Amount to Unwrap', - symbolSource: 'WETH', + symbolSource: wrappedToken, balance: wethBalance || null, description, - tooltipText: 'Unwrapping converts WETH back into ETH', + tooltipText: `Unwrapping converts ${wrappedToken} back into ${nativeToken}`, loading: unwrappingWeth, } } @@ -196,12 +216,15 @@ interface WrapEtherFormData { } const WrapUnwrapEtherBtn: React.FC = (props: WrapUnwrapEtherBtnProps) => { + const { networkIdOrDefault } = useWalletConnection() + const { nativeToken, wrappedToken } = getNativeTokenName(networkIdOrDefault) const { wrap, label, className } = props const [wethHelpVisible, showWethHelp] = useSafeState(false) const { wrapEth, unwrapWeth, wrappingEth, unwrappingWeth } = useWrapUnwrapEth() const { ethBalance } = useEthBalances() const { balances } = useTokenBalances() - const wethBalanceDetails = balances.find((token) => token.addressMainnet === WETH_ADDRESS_MAINNET) + + const wethBalanceDetails = balances.find((token) => getIsWrappable(networkIdOrDefault, token.address)) const wethBalance = wethBalanceDetails?.walletBalance const { register, errors, handleSubmit, setValue, watch } = useForm({ @@ -210,8 +233,29 @@ const WrapUnwrapEtherBtn: React.FC = (props: WrapUnwrap const amountError = errors[INPUT_ID_AMOUNT] const { title, balance, symbolSource, tooltipText, description, amountLabel, loading } = useMemo( - () => getModalParams({ wrap, wethHelpVisible, showWethHelp, wethBalance, ethBalance, wrappingEth, unwrappingWeth }), - [wrap, wethHelpVisible, showWethHelp, wethBalance, ethBalance, wrappingEth, unwrappingWeth], + () => + getModalParams({ + nativeToken, + wrappedToken, + wrap, + wethHelpVisible, + showWethHelp, + wethBalance, + ethBalance, + wrappingEth, + unwrappingWeth, + }), + [ + nativeToken, + wrappedToken, + wrap, + wethHelpVisible, + showWethHelp, + wethBalance, + ethBalance, + wrappingEth, + unwrappingWeth, + ], ) // Show Warning: Check if the user is Wrapping all his balance @@ -306,10 +350,11 @@ const WrapUnwrapEtherBtn: React.FC = (props: WrapUnwrap {amountError &&

    {amountError.message}

    } {wrapAllBalance && (

    - You are wrapping all your ETH balance. This would only make sense if your wallet doesn't need ETH + You are wrapping all your {nativeToken} balance. This would only make sense if your wallet doesn't + need {nativeToken} to pay the gas (as in some wallets that use tokens as payment).

    - Normally you would want to wrap a smaller fraction of your ETH. + Normally you would want to wrap a smaller fraction of your {nativeToken}.

    Are you sure you want to continue? @@ -336,16 +381,16 @@ const WrapUnwrapEtherBtn: React.FC = (props: WrapUnwrap let wrapUnwrapPromise, successMessage: string, errorMessage: string if (wrap) { - logDebug(`[WrapEtherBtn] Wrap ${wrapAmount} ETH`) + logDebug(`[WrapEtherBtn] Wrap ${wrapAmount} ${nativeToken}`) wrapUnwrapPromise = wrapEth(wrapAmount, txOptionalParams) - successMessage = `Successfully wrapped ${wrapAmountEther} ETH` - errorMessage = `Error wrapping ${wrapAmountEther} ETH` + successMessage = `Successfully wrapped ${wrapAmountEther} ${nativeToken}` + errorMessage = `Error wrapping ${wrapAmountEther} ${nativeToken}` } else { - logDebug(`[WrapEtherBtn] Unwrap ${wrapAmount} WETH`) + logDebug(`[WrapEtherBtn] Unwrap ${wrapAmount} ${wrappedToken}`) wrapUnwrapPromise = unwrapWeth(wrapAmount, txOptionalParams) - successMessage = `Successfully unwrapped ${wrapAmountEther} WETH` - errorMessage = `Error unwrapping ${wrapAmountEther} WETH` + successMessage = `Successfully unwrapped ${wrapAmountEther} ${wrappedToken}` + errorMessage = `Error unwrapping ${wrapAmountEther} ${wrappedToken}` } wrapUnwrapPromise diff --git a/src/components/common/EtherscanLink.stories.tsx b/src/components/common/BlockExplorerLink.stories.tsx similarity index 51% rename from src/components/common/EtherscanLink.stories.tsx rename to src/components/common/BlockExplorerLink.stories.tsx index 622cdf17d..346ebce47 100644 --- a/src/components/common/EtherscanLink.stories.tsx +++ b/src/components/common/BlockExplorerLink.stories.tsx @@ -2,24 +2,33 @@ import React from 'react' // also exported from '@storybook/react' if you can deal with breaking changes in 6.1 import { Story, Meta } from '@storybook/react/types-6-0' -import { EtherscanLink, EtherscanLinkProps } from 'components/common/EtherscanLink' +import { BlockExplorerLink, Props } from 'components/common/BlockExplorerLink' import { Network } from 'types' -import { ADDRESS_GNO, ADDRESS_GNOSIS_PROTOCOL, ADDRESS_GNOSIS_PROTOCOL_RINKEBY, TX_EXAMPLE } from 'storybook/data' +import { + ADDRESS_ACCOUNT_XDAI, + ADDRESS_GNOSIS_PROTOCOL_XDAI, + ADDRESS_GNO_XDAI, + ADDRESS_GNO, + ADDRESS_GNOSIS_PROTOCOL, + ADDRESS_GNOSIS_PROTOCOL_RINKEBY, + TX_EXAMPLE, + TX_XDAI, +} from 'storybook/data' const networkIds = Object.values(Network).filter(Number.isInteger) export default { - title: 'Common/EtherscanLink', - component: EtherscanLink, + title: 'Common/BlockExplorerLink', + component: BlockExplorerLink, argTypes: { label: { control: 'text' }, networkId: { control: { type: 'inline-radio', options: networkIds } }, }, } as Meta -const Template: Story = (args) => +const Template: Story = (args) => -const defaultParams: EtherscanLinkProps = { +const defaultParams: Props = { type: 'tx', identifier: TX_EXAMPLE, networkId: Network.Mainnet, @@ -66,3 +75,39 @@ Token.args = { label: 'GNO token', identifier: ADDRESS_GNO, } + +export const TxXdai = Template.bind({}) +TxXdai.storyName = 'Tx on xDAI' +TxXdai.args = { + ...defaultParams, + networkId: Network.xDAI, + type: 'tx', + identifier: TX_XDAI, +} + +export const ContractXDai = Template.bind({}) +ContractXDai.storyName = 'Contract on xDAI' +ContractXDai.args = { + ...defaultParams, + networkId: Network.xDAI, + type: 'contract', + identifier: ADDRESS_GNOSIS_PROTOCOL_XDAI, +} + +export const TokenXDai = Template.bind({}) +TokenXDai.storyName = 'Token on xDAI' +TokenXDai.args = { + ...defaultParams, + networkId: Network.xDAI, + type: 'token', + identifier: ADDRESS_GNO_XDAI, +} + +export const AddressXDai = Template.bind({}) +AddressXDai.storyName = 'Address on xDAI' +AddressXDai.args = { + ...defaultParams, + networkId: Network.xDAI, + type: 'address', + identifier: ADDRESS_ACCOUNT_XDAI, +} diff --git a/src/components/common/BlockExplorerLink.tsx b/src/components/common/BlockExplorerLink.tsx new file mode 100644 index 000000000..f8a5d2a42 --- /dev/null +++ b/src/components/common/BlockExplorerLink.tsx @@ -0,0 +1,118 @@ +import React, { ReactElement } from 'react' +import { useWalletConnection } from 'hooks/useWalletConnection' +import { abbreviateString } from 'utils' +import { Network } from 'types' + +type BlockExplorerLinkType = 'tx' | 'address' | 'contract' | 'token' | 'event' + +export interface Props { + /** + * type of BlockExplorerLink + */ + type: BlockExplorerLinkType + /** + * address or transaction or other hash + */ + identifier: string + /** + * network number | chain id + */ + networkId?: number + /** + * label to replace textContent generated from indentifier + */ + label?: string | ReactElement | void + + /** + * Use the URL as a label + */ + useUrlAsLabel?: boolean + /** + * className to pass on to + */ + className?: string // to allow subclassing styles with styled-components +} + +function getEtherscanUrlPrefix(networkId: Network): string { + return !networkId || networkId === Network.Mainnet ? '' : (Network[networkId] || '').toLowerCase() + '.' +} + +function getEtherscanUrlSuffix(type: BlockExplorerLinkType, identifier: string): string { + switch (type) { + case 'tx': + return `tx/${identifier}` + case 'event': + return `tx/${identifier}#eventlog` + case 'address': + return `address/${identifier}` + case 'contract': + return `address/${identifier}#code` + case 'token': + return `token/${identifier}` + } +} + +function getBlockscoutUrlPrefix(networkId: number): string { + switch (networkId) { + case Network.xDAI: + return 'poa/xdai' + + default: + return '' + } +} + +function getBlockscoutUrlSuffix(type: BlockExplorerLinkType, identifier: string): string { + switch (type) { + case 'tx': + return `tx/${identifier}` + case 'event': + return `tx/${identifier}/logs` + case 'address': + return `address/${identifier}/transactions` + case 'contract': + return `address/${identifier}/contracts` + case 'token': + return `tokens/${identifier}/token-transfers` + } +} + +function getBlockscoutUrl(networkId: number, type: BlockExplorerLinkType, identifier: string): string { + return `https://blockscout.com/${getBlockscoutUrlPrefix(networkId)}/${getBlockscoutUrlSuffix(type, identifier)}` +} + +function getEtherscanUrl(networkId: number, type: BlockExplorerLinkType, identifier: string): string { + return `https://${getEtherscanUrlPrefix(networkId)}etherscan.io/${getEtherscanUrlSuffix(type, identifier)}` +} + +function getExplorerUrl(networkId: number, type: BlockExplorerLinkType, identifier: string): string { + return networkId === Network.xDAI + ? getBlockscoutUrl(networkId, type, identifier) + : getEtherscanUrl(networkId, type, identifier) +} + +export const BlockExplorerLink: React.FC = ({ + type, + identifier, + label: labelProp, + useUrlAsLabel = false, + className, + networkId: networkIdFixed, +}) => { + const { networkIdOrDefault: networkIdWallet } = useWalletConnection() + + const networkId = networkIdFixed || networkIdWallet + + if (!networkId || !identifier) { + return null + } + + const url = getExplorerUrl(networkId, type, identifier) + const label = labelProp || (useUrlAsLabel && url) || abbreviateString(identifier, 6, 4) + + return ( + + {label} + + ) +} diff --git a/src/components/common/EtherscanLink.tsx b/src/components/common/EtherscanLink.tsx deleted file mode 100644 index 854c57215..000000000 --- a/src/components/common/EtherscanLink.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React, { ReactElement } from 'react' -import { useWalletConnection } from 'hooks/useWalletConnection' -import { abbreviateString } from 'utils' -import { Network } from 'types' - -type EtherscanLinkType = 'tx' | 'address' | 'contract' | 'token' | 'event' - -export interface EtherscanLinkProps { - /** - * type of EtherscanLink - */ - type: EtherscanLinkType - /** - * address or transaction or other hash - */ - identifier: string - /** - * network number | chain id - */ - networkId?: number - /** - * label to replace textContent generated from indentifier - */ - label?: string | ReactElement | void - /** - * className to pass on to - */ - className?: string // to allow subclassing styles with styled-components -} - -function getEtherscanDomainPrefix(networkId: Network): string { - return !networkId || networkId === Network.Mainnet ? '' : (Network[networkId] || '').toLowerCase() + '.' -} - -function getEtherscanDomainSuffix(type: EtherscanLinkType, identifier: string): string { - switch (type) { - case 'tx': - return `tx/${identifier}` - case 'event': - return `tx/${identifier}#eventlog` - case 'address': - return `address/${identifier}` - case 'contract': - return `address/${identifier}#code` - case 'token': - return `token/${identifier}` - } -} - -export const EtherscanLink: React.FC = ({ - type, - identifier, - label, - className, - networkId: networkIdFixed, -}) => { - const { networkIdOrDefault: networkIdWallet } = useWalletConnection() - - const networkId = networkIdFixed || networkIdWallet - - if (!networkId || !identifier) { - return null - } - - const href = `https://${getEtherscanDomainPrefix(networkId)}etherscan.io/${getEtherscanDomainSuffix( - type, - identifier, - )}` - return ( - - {label ? label : abbreviateString(identifier, 6, 4)} - - ) -} diff --git a/src/components/common/TokenImg.tsx b/src/components/common/TokenImg.tsx index 657f9810f..d2f8abd25 100644 --- a/src/components/common/TokenImg.tsx +++ b/src/components/common/TokenImg.tsx @@ -34,7 +34,7 @@ export interface WrapperProps { const tokensIconsRequire = process.env.NODE_ENV === 'test' ? RequireContextMock : require.context('assets/img/tokens', false) -const tokensIconsFilesByAddress = tokensIconsRequire.keys().reduce((acc, file) => { +const tokensIconsFilesByAddress: Record = tokensIconsRequire.keys().reduce((acc, file) => { const address = file.match(/0x\w{40}/)?.[0] if (!address) { throw new Error( @@ -54,7 +54,10 @@ export const TokenImg: React.FC = (props) => { iconFile = tokensIconsFilesByAddress[addressMainnet.toLowerCase()] } - const iconFileUrl = iconFile ? tokensIconsRequire(iconFile) : getImageUrl(addressMainnet || address) + const iconFileUrl: string | undefined = iconFile + ? tokensIconsRequire(iconFile).default + : getImageUrl(addressMainnet || address) + // TODO: Simplify safeTokenName signature, it doesn't need the addressMainnet or id! // https://github.com/gnosis/dex-react/issues/1442 const safeName = safeTokenName({ address, symbol, name }) diff --git a/src/components/layout/SwapLayout/Footer.tsx b/src/components/layout/SwapLayout/Footer.tsx index 740997fcb..2d854931e 100644 --- a/src/components/layout/SwapLayout/Footer.tsx +++ b/src/components/layout/SwapLayout/Footer.tsx @@ -11,7 +11,7 @@ import builtOnGP from 'assets/img/builtOnGP.svg' // Components import ThemeToggler, { ThemeTogglerWrapper } from 'components/ThemeToggler' -import { EtherscanLink } from 'components/common/EtherscanLink' +import { BlockExplorerLink } from 'components/common/BlockExplorerLink' // Hooks import { useWalletConnection } from 'hooks/useWalletConnection' @@ -179,7 +179,7 @@ const SideContentWrapper = styled.div` } ` -const LinkWrapper = styled(EtherscanLink)` +const LinkWrapper = styled(BlockExplorerLink)` margin: 0; text-align: center; border: 0.1rem solid #c5d3e0; diff --git a/src/const.ts b/src/const.ts index 314cd0ed5..a0bfb72fa 100644 --- a/src/const.ts +++ b/src/const.ts @@ -149,10 +149,6 @@ if (process.env.ETH_NODE_URL) { export const ETH_NODE_URL = ethNodeUrl export const STORAGE_KEY_LAST_PROVIDER = 'lastProvider' -export const STORAGE_PENDING_ORDER_TX_HASHES = { - 1: 'STORAGE_PENDING_ORDER_TX_HASHES_1', - 4: 'STORAGE_PENDING_ORDER_TX_HASHES_4', -} export const STORAGE_KEY_DISABLED_TOKENS_ADDRESSES = 'disabledTokens' export const TRADES_LOCAL_STORAGE_KEY = 'TRADES_PER_ACCOUNT' @@ -163,6 +159,8 @@ export const INPUT_PRECISION_SIZE = 6 export const WETH_ADDRESS_MAINNET = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' export const WETH_ADDRESS_RINKEBY = '0xc778417E063141139Fce010982780140Aa0cD5Ab' +export const WXDAI_ADDRESS_XDAI = '0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d' + export const ORDER_BOOK_HOPS_DEFAULT = 2 export const ORDER_BOOK_HOPS_MAX = 2 @@ -210,5 +208,6 @@ export const DISABLED_TOKEN_MAPS = Object.keys(disabledTokens).reduce = ({ token, reason, {token.symbol}

    {token.name}
    - = ({ token, reason, {token.symbol}
    {token.name}
    - { return ( {index > 0 && , } - + ) }) diff --git a/src/reducers-actions/pendingOrders.ts b/src/reducers-actions/pendingOrders.ts index 58ebd06cd..9aaf1a97b 100644 --- a/src/reducers-actions/pendingOrders.ts +++ b/src/reducers-actions/pendingOrders.ts @@ -1,6 +1,7 @@ import { PendingTxObj } from 'api/exchange/ExchangeApi' import { Actions } from 'reducers-actions' -import { toBN, setStorageItem } from 'utils' +import { setStorageItem } from 'utils' +import { toBN } from '@gnosis.pm/dex-js' const STORAGE_PENDING_ORDER_KEY = 'STORAGE_PENDING_ORDER_TX_HASHES' @@ -66,11 +67,14 @@ export interface PendingOrdersState { '0x90dcJsdkjb22': [], '0xd9sjsdasnci1': [], }, + 100: { + }, } */ export const EMPTY_PENDING_ORDERS_STATE = { 1: {}, 4: {}, + 100: {}, } export const reducer = (state: PendingOrdersState, action: ReducerType): PendingOrdersState => { diff --git a/src/reducers-actions/trades.ts b/src/reducers-actions/trades.ts index 318e17917..3b08ef73d 100644 --- a/src/reducers-actions/trades.ts +++ b/src/reducers-actions/trades.ts @@ -1,10 +1,11 @@ import BigNumber from 'bignumber.js' +import { toBN } from '@gnosis.pm/dex-js' import { Trade, TradeReversion, EventWithBlockInfo } from 'api/exchange/ExchangeApi' import { Actions } from 'reducers-actions' -import { logDebug, dateToBatchId, toBN, setStorageItem, flattenMapOfLists } from 'utils' +import { logDebug, dateToBatchId, setStorageItem, flattenMapOfLists } from 'utils' import { TRADES_LOCAL_STORAGE_KEY } from 'const' // ******** TYPES/INTERFACES diff --git a/src/storybook/data/addresses.ts b/src/storybook/data/addresses.ts index d8355f1cb..37e63de40 100644 --- a/src/storybook/data/addresses.ts +++ b/src/storybook/data/addresses.ts @@ -1,3 +1,8 @@ +export const ADDRESS_GNOSIS_PROTOCOL_XDAI = '0x25B06305CC4ec6AfCF3E7c0b673da1EF8ae26313' +export const ADDRESS_GNO_XDAI = '0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb' +export const ADDRESS_ACCOUNT_XDAI = '0xE0B3700e0aadcb18ed8d4BFF648Bc99896a18ad1' export const ADDRESS_GNOSIS_PROTOCOL = '0x6f400810b62df8e13fded51be75ff5393eaa841f' export const ADDRESS_GNOSIS_PROTOCOL_RINKEBY = '0xC576eA7bd102F7E476368a5E98FA455d1Ea34dE2' + +export const TX_XDAI = '0xce286241d90b58292f4dbf19bd57c7bdbbec72ed5885c3bd7e5e79a9eb4ef7df' export const TX_EXAMPLE = '0x5f995094ff596cf1aa27e8ec84fab21c4ec6a512981b13c563a58c9172607c3d' diff --git a/src/types/config.ts b/src/types/config.ts index 2ee281a24..6ae99fbca 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -60,6 +60,7 @@ export interface TokenOverride { export interface DisabledTokens { [Network.Mainnet]: TokenOverride[] [Network.Rinkeby]: TokenOverride[] + [Network.xDAI]: TokenOverride[] } export interface Config { diff --git a/src/types/index.ts b/src/types/index.ts index e73bda306..6ade8176a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -15,6 +15,7 @@ export enum Network { Rinkeby = 4, Goerli = 5, Kovan = 42, + xDAI = 100, } export interface TokenDetails extends TokenDex { diff --git a/src/utils/display.tsx b/src/utils/display.tsx index 4652a66bf..c39410604 100644 --- a/src/utils/display.tsx +++ b/src/utils/display.tsx @@ -1,11 +1,11 @@ import React from 'react' import { TokenDex, safeTokenName } from '@gnosis.pm/dex-js' -import { EtherscanLink } from 'components/common/EtherscanLink' +import { BlockExplorerLink } from 'components/common/BlockExplorerLink' export function displayTokenSymbolOrLink(token: TokenDex): React.ReactNode | string { const displayName = safeTokenName(token) if (displayName.startsWith('0x')) { - return + return } return displayName } diff --git a/src/utils/ethereum.ts b/src/utils/ethereum.ts deleted file mode 100644 index fc5e69363..000000000 --- a/src/utils/ethereum.ts +++ /dev/null @@ -1,25 +0,0 @@ -import BN from 'bn.js' - -export { fromWei, toWei, isBN, toBN } from 'web3-utils' - -export function toBnOrNull(value: string | number): BN | null { - if (value === undefined || value === null || value === '') { - return null - } - - try { - return new BN(value) - } catch (error) { - return null - } -} - -const id2Network = { - 1: 'Mainnet', - 3: 'Ropsten', - 4: 'Rinkeby', - 5: 'Goerli', - 42: 'Kovan', -} - -export const getNetworkFromId = (networkId: number): string => id2Network[networkId] || 'Unknown Network' diff --git a/src/utils/index.ts b/src/utils/index.ts index 04ad469bd..8765db01d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,6 +1,5 @@ export * from './time' export * from './format' -export * from './ethereum' export * from './miscellaneous' export * from './autoconnect' export * from './price' @@ -9,3 +8,4 @@ export * from './flagCodes' export * from './deferred' export * from './classifiers' export * from './walletconnectOptions' +export * from './wrap' diff --git a/src/utils/wrap.ts b/src/utils/wrap.ts new file mode 100644 index 000000000..5598c3cf9 --- /dev/null +++ b/src/utils/wrap.ts @@ -0,0 +1,34 @@ +import { WETH_ADDRESS_MAINNET, WETH_ADDRESS_RINKEBY, WXDAI_ADDRESS_XDAI } from 'const' +import { Network } from 'types' + +export interface NativeTokenInfo { + nativeToken: string + wrappedToken: string +} + +export function getIsWrappable(networkId: number, address: string): boolean { + switch (networkId) { + case Network.Mainnet: + return address === WETH_ADDRESS_MAINNET + case Network.Rinkeby: + return address === WETH_ADDRESS_RINKEBY + case Network.xDAI: + return address === WXDAI_ADDRESS_XDAI + default: + return false + } +} + +export function getNativeTokenName(networkId?: number): NativeTokenInfo { + if (networkId === Network.xDAI) { + return { + nativeToken: 'xDAI', + wrappedToken: 'wxDAI', + } + } else { + return { + nativeToken: 'ETH', + wrappedToken: 'WETH', + } + } +} diff --git a/test/components/EtherscanLink.components.test.tsx b/test/components/BlockExplorerLink.components.test.tsx similarity index 50% rename from test/components/EtherscanLink.components.test.tsx rename to test/components/BlockExplorerLink.components.test.tsx index aa38b9133..1a00f0da5 100644 --- a/test/components/EtherscanLink.components.test.tsx +++ b/test/components/BlockExplorerLink.components.test.tsx @@ -1,7 +1,7 @@ import React from 'react' import { render } from 'enzyme' -import { EtherscanLink } from 'components/common/EtherscanLink' +import { BlockExplorerLink } from 'components/common/BlockExplorerLink' import { TX_HASH, USER_1, CONTRACT, TOKEN_1 } from '../data' import { abbreviateString } from 'utils' import { Network } from 'types' @@ -18,7 +18,7 @@ jest.mock('hooks/useWalletConnection', () => { } }) -describe(' general', () => { +describe(' general', () => { beforeEach(() => { isConnected = true }) @@ -26,34 +26,43 @@ describe(' general', () => { it("doesn't change when isConnected == false", () => { isConnected = false networkId = Network.Mainnet - const wrapper = render() + const wrapper = render() expect(wrapper.prop('href')).toMatch(`https://etherscan.io/tx/${TX_HASH}`) }) it('renders for mainnet if networkId is missing', () => { networkId = undefined - const wrapper = render() + const wrapper = render() expect(wrapper.prop('href')).toMatch(`https://etherscan.io/tx/${TX_HASH}`) }) it('renders provided label', () => { networkId = Network.Mainnet const label =
    Custom label
    - const wrapper = render() + const wrapper = render() expect(wrapper.prop('href')).toMatch(`https://etherscan.io/tx/${TX_HASH}`) expect(wrapper.find('div').text()).toEqual('Custom label') }) + + it('renders url as label', () => { + networkId = Network.Mainnet + const wrapper = render() + + const url = `https://etherscan.io/tx/${TX_HASH}` + expect(wrapper.prop('href')).toMatch(url) + expect(wrapper.text()).toEqual(url) + }) }) -describe('', () => { +describe('', () => { beforeEach(() => { isConnected = true }) it('renders the abbreviated tx hash inside a link', () => { networkId = Network.Mainnet - const wrapper = render() + const wrapper = render() const abbreviatedTxHash = abbreviateString(TX_HASH, 6, 4) expect(wrapper.is('a')).toBe(true) @@ -62,49 +71,70 @@ describe('', () => { it('renders link to mainnet', () => { networkId = Network.Mainnet - const wrapper = render() + const wrapper = render() expect(wrapper.prop('href')).toMatch(`https://etherscan.io/tx/${TX_HASH}`) }) it('renders link to rinkeby', () => { networkId = Network.Rinkeby - const wrapper = render() + const wrapper = render() expect(wrapper.prop('href')).toMatch(`https://rinkeby.etherscan.io/tx/${TX_HASH}`) }) + it('renders link to xDai', () => { + networkId = Network.xDAI + const wrapper = render() + expect(wrapper.prop('href')).toMatch(`https://blockscout.com/poa/xdai/tx/${TX_HASH}`) + }) }) -describe('', () => { +describe('', () => { beforeEach(() => { isConnected = true }) it('renders link to mainnet', () => { networkId = Network.Mainnet - const wrapper = render() + const wrapper = render() expect(wrapper.prop('href')).toMatch(`https://etherscan.io/address/${USER_1}`) }) + + it('renders link to xDai', () => { + networkId = Network.xDAI + const wrapper = render() + expect(wrapper.prop('href')).toMatch(`https://blockscout.com/poa/xdai/address/${USER_1}/transactions`) + }) }) -describe('', () => { +describe('', () => { beforeEach(() => { isConnected = true }) it('renders link to mainnet', () => { networkId = Network.Mainnet - const wrapper = render() + const wrapper = render() expect(wrapper.prop('href')).toMatch(`https://etherscan.io/address/${CONTRACT}#code`) }) + it('renders link to xDai', () => { + networkId = Network.xDAI + const wrapper = render() + expect(wrapper.prop('href')).toMatch(`https://blockscout.com/poa/xdai/address/${CONTRACT}/contracts`) + }) }) -describe('', () => { +describe('', () => { beforeEach(() => { isConnected = true }) it('renders link to mainnet', () => { networkId = Network.Mainnet - const wrapper = render() + const wrapper = render() expect(wrapper.prop('href')).toMatch(`https://etherscan.io/token/${TOKEN_1}`) }) + it('renders link to xDai', () => { + networkId = Network.xDAI + const wrapper = render() + expect(wrapper.prop('href')).toMatch(`https://blockscout.com/poa/xdai/tokens/${TOKEN_1}/token-transfers`) + }) }) diff --git a/test/components/DepositWidget.components.test.tsx b/test/components/DepositWidget.components.test.tsx index e6758d5d1..720106f33 100644 --- a/test/components/DepositWidget.components.test.tsx +++ b/test/components/DepositWidget.components.test.tsx @@ -5,7 +5,7 @@ import BN from 'bn.js' import { Row, RowProps } from 'components/DepositWidget/Row' import { ZERO, ONE, TEN } from 'const' -import { TokenBalanceDetails } from 'types' +import { Network, TokenBalanceDetails } from 'types' import { TokenLocalState } from 'reducers-actions' import { createFlux } from '../data' @@ -56,6 +56,7 @@ function _createRow({ const onSubmitWithdraw = jest.fn, [BN, Function]>() return ( ', () => { it('renders with link component', () => { const wrapper = shallow() expect(wrapper.text()).toMatch(/^The transaction has been sent! Check .* for details$/) - expect(wrapper.contains()).toBeTruthy() + expect(wrapper.contains()).toBeTruthy() }) it('does not render component', () => { diff --git a/test/config.spec.ts b/test/config.spec.ts index 7847db100..4b5f5a353 100644 --- a/test/config.spec.ts +++ b/test/config.spec.ts @@ -55,14 +55,25 @@ describe('Test config defaults', () => { config: [ { networkId: 1, + // eslint-disable-next-line @typescript-eslint/camelcase url_production: 'https://dex-price-estimator.gnosis.io', + // eslint-disable-next-line @typescript-eslint/camelcase url_develop: 'https://price-estimate-mainnet.dev.gnosisdev.com', }, { networkId: 4, + // eslint-disable-next-line @typescript-eslint/camelcase url_production: 'https://dex-price-estimator.rinkeby.gnosis.io', + // eslint-disable-next-line @typescript-eslint/camelcase url_develop: 'https://price-estimate-rinkeby.dev.gnosisdev.com', }, + { + networkId: 100, + // eslint-disable-next-line @typescript-eslint/camelcase + url_production: 'https://price-estimate-xdai.dev.gnosisdev.com', + // eslint-disable-next-line @typescript-eslint/camelcase + url_develop: 'https://price-estimate-xdai.dev.gnosisdev.com', + }, ], } expect(CONFIG.dexPriceEstimator).toEqual(expected) @@ -80,6 +91,10 @@ describe('Test config defaults', () => { networkId: 4, url: 'https://api.thegraph.com/subgraphs/name/gnosis/protocol-rinkeby', }, + { + networkId: 100, + url: 'https://api.thegraph.com/subgraphs/name/gnosis/protocol-xdai', + }, ], } expect(CONFIG.theGraphApi).toEqual(expected) @@ -103,6 +118,7 @@ describe('Test config defaults', () => { config: [ { networkId: 1, blockNumber: 9340147 }, { networkId: 4, blockNumber: 5844678 }, + { networkId: 100, blockNumber: 11948310 }, ], } expect(CONFIG.exchangeContractConfig).toEqual(expected) @@ -116,10 +132,15 @@ describe('Test config defaults', () => { }), ]) - const { [Network.Mainnet]: disabledOnMainnet, [Network.Rinkeby]: disabledOnRinkeby } = CONFIG.disabledTokens + const { + [Network.Mainnet]: disabledOnMainnet, + [Network.Rinkeby]: disabledOnRinkeby, + [Network.xDAI]: disabledOnXdai, + } = CONFIG.disabledTokens if (disabledOnMainnet.length) expect(disabledOnMainnet).toEqual(disabledTokensArray) if (disabledOnRinkeby.length) expect(disabledOnRinkeby).toEqual(disabledTokensArray) + if (disabledOnRinkeby.length) expect(disabledOnXdai).toEqual(disabledTokensArray) }) it('initialTokenSelection', () => {