diff --git a/.changeset/quick-crabs-worry.md b/.changeset/quick-crabs-worry.md new file mode 100644 index 000000000..4ad8acf8f --- /dev/null +++ b/.changeset/quick-crabs-worry.md @@ -0,0 +1,6 @@ +--- +"frontend": patch +"@sovryn/ui": patch +--- + +SOV-2470: send flow mock ui diff --git a/apps/frontend/src/app/3_organisms/FastBtcDialog/FastBtcDialog.tsx b/apps/frontend/src/app/3_organisms/FastBtcDialog/FastBtcDialog.tsx index f4581f412..281cc996b 100644 --- a/apps/frontend/src/app/3_organisms/FastBtcDialog/FastBtcDialog.tsx +++ b/apps/frontend/src/app/3_organisms/FastBtcDialog/FastBtcDialog.tsx @@ -16,6 +16,9 @@ import { import { useAccount } from '../../../hooks/useAccount'; import { useIsMobile } from '../../../hooks/useIsMobile'; import { translations } from '../../../locales/i18n'; +import { isMainnet } from '../../../utils/helpers'; +import { BridgeReceiveFlow } from './components/BridgeReceiveFlow'; +import { BridgeSendFlow } from './components/BridgeSendFlow'; import { ReceiveFlow } from './components/ReceiveFlow/ReceiveFlow'; import { SendFlow } from './components/SendFlow/SendFlow'; @@ -43,36 +46,43 @@ export const FastBtcDialog: React.FC = ({ index !== null ? setIndex(index) : setIndex(0); }, []); + const receiveFlow = useMemo( + () => ({ + label: t(translation.tabs.receiveLabel), + infoText: t(translation.tabs.receiveInfoText), + content: isMainnet() ? ( + + ) : ( + + ), + activeClassName: ACTIVE_CLASSNAME, + dataAttribute: 'funding-receive', + }), + [onClose], + ); + + const sendFlow = useMemo( + () => ({ + label: t(translation.tabs.sendLabel), + infoText: t(translation.tabs.sendInfoText), + content: isMainnet() ? ( + + ) : ( + + ), + activeClassName: ACTIVE_CLASSNAME, + dataAttribute: 'funding-send', + }), + [onClose], + ); + const items = useMemo(() => { if (shouldHideSend) { - return [ - { - label: t(translation.tabs.receiveLabel), - infoText: t(translation.tabs.receiveInfoText), - content: , - activeClassName: ACTIVE_CLASSNAME, - dataAttribute: 'funding-receive', - }, - ]; + return [receiveFlow]; } - return [ - { - label: t(translation.tabs.receiveLabel), - infoText: t(translation.tabs.receiveInfoText), - content: , - activeClassName: ACTIVE_CLASSNAME, - dataAttribute: 'funding-receive', - }, - { - label: t(translation.tabs.sendLabel), - infoText: t(translation.tabs.sendInfoText), - content: , - activeClassName: ACTIVE_CLASSNAME, - dataAttribute: 'funding-send', - }, - ]; - }, [onClose, shouldHideSend]); + return [receiveFlow, sendFlow]; + }, [receiveFlow, shouldHideSend, sendFlow]); const dialogSize = useMemo( () => (isMobile ? DialogSize.md : DialogSize.xl2), @@ -88,7 +98,7 @@ export const FastBtcDialog: React.FC = ({ diff --git a/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeReceiveFlow/components/AmountForm.tsx b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeReceiveFlow/components/AmountForm.tsx new file mode 100644 index 000000000..3113057dd --- /dev/null +++ b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeReceiveFlow/components/AmountForm.tsx @@ -0,0 +1,168 @@ +import React, { useCallback, useContext, useMemo, useReducer } from 'react'; + +import { t } from 'i18next'; + +import { + Accordion, + AmountInput, + Button, + ButtonStyle, + FormGroup, + Heading, + HeadingType, + Paragraph, + SimpleTable, + SimpleTableRow, +} from '@sovryn/ui'; + +import { MaxButton } from '../../../../../2_molecules/MaxButton/MaxButton'; +import { useAmountInput } from '../../../../../../hooks/useAmountInput'; +import { useWeiAmountInput } from '../../../../../../hooks/useWeiAmountInput'; +import { translations } from '../../../../../../locales/i18n'; +import { ReceiveContext, ReceiveStep } from '../../../contexts/receive-context'; +import { getNetwork } from '../../../utils/networks'; + +const translation = translations.fastBtc.receive.amountFormScreen; + +// todo: replace with real data +const policies = [ + { + title: t(translation.policies.minimum), + value: '50 BUSD', + }, + { + title: t(translation.policies.conversion), + value: '2 BUSD', + }, + { + title: t(translation.policies.fee), + value: '3 BUSD', + }, +]; + +export const AmountForm = () => { + const { set, originNetwork, asset } = useContext(ReceiveContext); + + const [amount, setAmount] = useWeiAmountInput('0'); + const [slippage, setSlippage] = useAmountInput('0.5'); + + const [slippageOpen, toggleSlippage] = useReducer(v => !v, false); + const [policiesOpen, togglePolicies] = useReducer(v => !v, false); + + // todo: replace with real data + const maxAmount = '12.345'; + const handleMaxButtonClick = useCallback( + () => setAmount(maxAmount), + [setAmount], + ); + + const networkName = useMemo( + () => getNetwork(originNetwork!).label, + [originNetwork], + ); + + const onContinueClick = useCallback( + () => + set(prevState => ({ + ...prevState, + step: ReceiveStep.DETAILS, + })), + [set], + ); + + return ( +
+ + {t(translation.title, { + network: networkName, + asset: asset.toUpperCase(), + })} + + + {t(translation.description__dllr, { asset: asset.toUpperCase() })} + + + +
+ {t(translation.amount.title)} + + +
+ +
+ + + + + + + + + + {policies.map(policy => ( + + ))} + + + + + {t(translation.summary.title)} + + + + + + + +
+ ); +}; diff --git a/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeReceiveFlow/components/AssetList.tsx b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeReceiveFlow/components/AssetList.tsx new file mode 100644 index 000000000..81b297bfa --- /dev/null +++ b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeReceiveFlow/components/AssetList.tsx @@ -0,0 +1,124 @@ +import React, { useCallback, useContext, useMemo, useState } from 'react'; + +import { t } from 'i18next'; + +import { SupportedTokens } from '@sovryn/contracts'; +import { + Align, + Button, + ButtonStyle, + Heading, + HeadingType, + TableBase, + TransactionId, +} from '@sovryn/ui'; + +import { translations } from '../../../../../../locales/i18n'; +import { ReceiveContext, ReceiveStep } from '../../../contexts/receive-context'; +import { getNetwork } from '../../../utils/networks'; + +const translation = translations.fastBtc.receive.assetScreen; + +const columns = [ + { + id: 'asset', + title: t(translation.table.asset), + align: Align.left, + cellRenderer: row => `${row.asset.toUpperCase()}`, + }, + { + id: 'address', + title: t(translation.table.address), + align: Align.center, + cellRenderer: row => ( + + ), + }, + { + id: 'balance', + title: t(translation.table.balance), + align: Align.right, + cellRenderer: row => `${row.balance} ${row.asset.toUpperCase()}`, + }, +]; + +export const AssetList = () => { + const { set, originNetwork } = useContext(ReceiveContext); + + const networkName = useMemo( + () => getNetwork(originNetwork!).label, + [originNetwork], + ); + + //@TODO: Replace with real data + const walletBalance = [ + { + asset: SupportedTokens.bnbs, + address: '0x1234567890123456789012345678901234567890', + balance: 0.2, + }, + { + asset: SupportedTokens.dllr, + address: '0x0p42490ACCbc50F4F9c130b5876521I1q7b3C0p', + balance: 1.234, + }, + { + asset: SupportedTokens.eths, + address: '0x6p42490ACCbc50F4F9c130b5876521I1q7b3C0p', + balance: 4, + }, + { + asset: SupportedTokens.rusdt, + address: '0x6p42490ACCbc50F4F9c130b5876521I1q7b3C0p', + balance: 4, + }, + { + asset: SupportedTokens.rdoc, + address: '0x6p42490ACCbc50F4F9c130b5876521I1q7b3C0p', + balance: 4, + }, + ]; + + const [selectedAsset, setSelectedAsset] = useState(); + + const onRowClick = useCallback((row: any) => setSelectedAsset(row.asset), []); + + const onContinueClick = useCallback( + () => + set(prevState => ({ + ...prevState, + step: ReceiveStep.AMOUNT, + asset: selectedAsset!, + })), + [selectedAsset, set], + ); + + return ( +
+ + {t(translation.title, { network: networkName })} + + + + +
+ ); +}; diff --git a/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeReceiveFlow/components/DetailsScreen.tsx b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeReceiveFlow/components/DetailsScreen.tsx new file mode 100644 index 000000000..18cdcf90f --- /dev/null +++ b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeReceiveFlow/components/DetailsScreen.tsx @@ -0,0 +1,128 @@ +import React, { useContext, useMemo } from 'react'; + +import { t } from 'i18next'; + +import { + Button, + Heading, + HeadingType, + SimpleTable, + SimpleTableRow, +} from '@sovryn/ui'; + +import { TxIdWithNotification } from '../../../../../2_molecules/TxIdWithNotification/TransactionIdWithNotification'; +import { BITCOIN } from '../../../../../../constants/currencies'; +import { translations } from '../../../../../../locales/i18n'; +import { + getBtcExplorerUrl, + getRskExplorerUrl, +} from '../../../../../../utils/helpers'; +import { formatValue } from '../../../../../../utils/math'; +import { ReceiveContext } from '../../../contexts/receive-context'; +import { getNetwork } from '../../../utils/networks'; + +const translation = translations.fastBtc.receive.detailsScreen; + +const rskExplorerUrl = getRskExplorerUrl(); +const btcExplorerUrl = getBtcExplorerUrl(); + +type DetailsScreenProps = { + onConfirm: () => void; +}; + +export const DetailsScreen: React.FC = ({ onConfirm }) => { + const { asset, originNetwork, amount } = useContext(ReceiveContext); + + const items = useMemo( + () => [ + { + label: t(translation.from), + value: ( + + ), + }, + { + label: t(translation.to), + value: ( + + ), + }, + { + label: t(translation.sending), + value: ( + <> + {formatValue(amount, 8)} {asset.toUpperCase()} + + ), + }, + { + label: t(translation.serviceFee), + value: ( + <> + {formatValue(0, 8)} {asset.toUpperCase()} + + ), + }, + { + label: t(translation.expectedToReceive), + value: ( + <> + {formatValue(0, 8)} {BITCOIN} + + ), + }, + { + label: t(translation.minimumReceived), + value: ( + <> + {formatValue(0, 8)} {BITCOIN} + + ), + }, + { + label: t(translation.networkTxId, { + network: getNetwork(originNetwork!).label, + }), + value: ( + + ), + }, + { + label: t(translation.networkTxId, { network: 'Rootstock' }), + value: ( + + ), + }, + ], + [amount, asset, originNetwork], + ); + + return ( + <> + + {t(translation.title)} + + + + {items.map(({ label, value }) => ( + + ))} + + +
+
+ + ); +}; diff --git a/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeReceiveFlow/components/NetworkList.tsx b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeReceiveFlow/components/NetworkList.tsx new file mode 100644 index 000000000..1c421171b --- /dev/null +++ b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeReceiveFlow/components/NetworkList.tsx @@ -0,0 +1,73 @@ +import React, { useCallback, useContext } from 'react'; + +import { t } from 'i18next'; + +import { ChainIds } from '@sovryn/ethers-provider'; +import { + Heading, + HeadingType, + Icon, + IconNames, + WalletContainer, +} from '@sovryn/ui'; + +import { defaultChainId } from '../../../../../../config/chains'; + +import { useNetworkContext } from '../../../../../../contexts/NetworkContext'; +import { translations } from '../../../../../../locales/i18n'; +import { ReceiveContext, ReceiveStep } from '../../../contexts/receive-context'; +import { OriginNetwork } from '../../../types'; +import { getNetwork } from '../../../utils/networks'; + +const translation = translations.fastBtc.receive.networkScreen; + +export const NetworkList = () => { + const { set } = useContext(ReceiveContext); + const { requireChain } = useNetworkContext(); + + const handleNetworkClick = useCallback( + (network: OriginNetwork) => () => { + set(prevState => ({ + ...prevState, + step: + network === OriginNetwork.BITCOIN + ? ReceiveStep.BITCOIN_FLOW + : ReceiveStep.SELECT_ASSET, + originNetwork: network, + })); + + requireChain( + (network === OriginNetwork.BITCOIN + ? defaultChainId + : getNetwork(network).chainId) as ChainIds, + ); + }, + [requireChain, set], + ); + + return ( +
+ + {t(translation.title)} + + } + onClick={handleNetworkClick(OriginNetwork.BITCOIN)} + className="mb-4" + /> + } + onClick={handleNetworkClick(OriginNetwork.BINANCE_SMART_CHAIN)} + className="mb-4" + /> + } + onClick={handleNetworkClick(OriginNetwork.ETHEREUM)} + className="mb-4" + /> +
+ ); +}; diff --git a/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeReceiveFlow/index.tsx b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeReceiveFlow/index.tsx new file mode 100644 index 000000000..37c5b3fcf --- /dev/null +++ b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeReceiveFlow/index.tsx @@ -0,0 +1,92 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; + +import { ChainIds } from '@sovryn/ethers-provider'; + +import { defaultChainId } from '../../../../../config/chains'; + +import { useNetworkContext } from '../../../../../contexts/NetworkContext'; +import { + ReceiveContext, + ReceiveContextStateType, + ReceiveStep, + defaultValue, +} from '../../contexts/receive-context'; +import { GoBackButton } from '../GoBackButton'; +import { MobileCloseButton } from '../MobileCloseButton'; +import { ReceiveFlow } from '../ReceiveFlow/ReceiveFlow'; +import { AmountForm } from './components/AmountForm'; +import { AssetList } from './components/AssetList'; +import { DetailsScreen } from './components/DetailsScreen'; +import { NetworkList } from './components/NetworkList'; + +type ReceiveFlowProps = { + onClose: () => void; +}; + +const allowedStepsToGoBackFrom = [ + ReceiveStep.SELECT_ASSET, + ReceiveStep.AMOUNT, + ReceiveStep.DETAILS, +]; + +const getBackStep = (step: ReceiveStep) => { + switch (step) { + case ReceiveStep.SELECT_ASSET: + return ReceiveStep.MAIN; + case ReceiveStep.AMOUNT: + return ReceiveStep.SELECT_ASSET; + case ReceiveStep.DETAILS: + return ReceiveStep.AMOUNT; + default: + return ReceiveStep.MAIN; + } +}; + +export const BridgeReceiveFlow: React.FC = ({ onClose }) => { + const { requireChain } = useNetworkContext(); + const [state, setState] = useState(defaultValue); + const { step } = state; + + const value = useMemo( + () => ({ + ...state, + set: setState, + }), + [state], + ); + + const onBackClick = useCallback(() => { + value.set(prevState => ({ ...prevState, step: getBackStep(value.step) })); + }, [value]); + + const handleConfirm = useCallback(() => {}, []); + + useEffect(() => { + return () => { + requireChain(defaultChainId as ChainIds); + }; + }, [requireChain]); + + return ( + + {step === ReceiveStep.BITCOIN_FLOW ? ( + + ) : ( + <> + {allowedStepsToGoBackFrom.includes(value.step) && ( + + )} +
+ {step === ReceiveStep.MAIN && } + {step === ReceiveStep.SELECT_ASSET && } + {step === ReceiveStep.AMOUNT && } + {step === ReceiveStep.DETAILS && ( + + )} +
+ + + )} +
+ ); +}; diff --git a/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeSendFlow/components/AddressForm.tsx b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeSendFlow/components/AddressForm.tsx new file mode 100644 index 000000000..3dbc57e62 --- /dev/null +++ b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeSendFlow/components/AddressForm.tsx @@ -0,0 +1,186 @@ +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; + +import { + validate, + getAddressInfo, + AddressType, +} from 'bitcoin-address-validation'; +import { t } from 'i18next'; +import debounce from 'lodash.debounce'; +import { Trans } from 'react-i18next'; + +import { + Button, + ButtonStyle, + Checkbox, + ErrorBadge, + ErrorLevel, + Heading, + HeadingType, + Input, + Link, + Paragraph, + ParagraphSize, +} from '@sovryn/ui'; + +import { WIKI_LINKS } from '../../../../../../constants/links'; +import { useGetProtocolContract } from '../../../../../../hooks/useGetContract'; +import { useMaintenance } from '../../../../../../hooks/useMaintenance'; +import { translations } from '../../../../../../locales/i18n'; +import { currentNetwork } from '../../../../../../utils/helpers'; +import { SendContext, SendStep } from '../../../contexts/send-context'; +import { getNetwork } from '../../../utils/networks'; + +enum AddressValidationState { + NONE, + LOADING, + VALID, + INVALID, +} + +const translation = translations.fastBtc.send.addressForm; + +export const AddressForm: React.FC = () => { + const { address, set, originNetwork } = useContext(SendContext); + const [isConfirmed, setIsConfirmed] = useState(false); + + const networkName = useMemo( + () => getNetwork(originNetwork!).label, + [originNetwork], + ); + + const fastBtcBridgeContract = useGetProtocolContract('fastBtcBridge'); + + const { checkMaintenance, States } = useMaintenance(); + const fastBtcLocked = checkMaintenance(States.FASTBTC_SEND); + + const [addressValidationState, setAddressValidationState] = useState( + AddressValidationState.NONE, + ); + const [value, setValue] = useState(address); + + const invalid = useMemo( + () => addressValidationState === AddressValidationState.INVALID, + [addressValidationState], + ); + + const onContinueClick = useCallback( + () => + set(prevState => ({ + ...prevState, + address: value, + step: SendStep.SENDER_ASSET, + })), + [set, value], + ); + + const validateAddress = useCallback( + async (address: string) => { + setAddressValidationState(AddressValidationState.LOADING); + let result = false; + const isValidBtcAddress = validate(address); + + if (!fastBtcBridgeContract) { + return; + } + const isValid = fastBtcBridgeContract.isValidBtcAddress(address); + + if (isValidBtcAddress && isValid) { + const { network, type } = getAddressInfo(address); + if ( + network.toLowerCase() === currentNetwork.toLowerCase() && + type.toLowerCase() !== AddressType.p2tr + ) { + result = true; + } + } + + setAddressValidationState( + result ? AddressValidationState.VALID : AddressValidationState.INVALID, + ); + }, + [fastBtcBridgeContract], + ); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const delayedOnChange = useCallback( + debounce(addressToValidate => validateAddress(addressToValidate), 300), + [validateAddress], + ); + + useEffect(() => { + if (value) { + setAddressValidationState(AddressValidationState.NONE); + delayedOnChange(value); + } + }, [delayedOnChange, value]); + + const isSubmitDisabled = useMemo( + () => invalid || fastBtcLocked || !value || value === '', + [fastBtcLocked, invalid, value], + ); + + return ( +
+ + {t(translation.title, { + network: networkName, + })} + + +
+ + {t(translation.addressLabel)} + + + + +
+ , + ]} + /> + } + checked={isConfirmed} + onChange={() => setIsConfirmed(!isConfirmed)} + dataAttribute="funding-send-address-confirm-checkbox" + /> +
+
+ +
+ ); +}; diff --git a/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeSendFlow/components/AmountForm.tsx b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeSendFlow/components/AmountForm.tsx new file mode 100644 index 000000000..95aecca59 --- /dev/null +++ b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeSendFlow/components/AmountForm.tsx @@ -0,0 +1,176 @@ +import React, { useCallback, useContext, useMemo, useReducer } from 'react'; + +import { t } from 'i18next'; + +import { + Accordion, + AmountInput, + Button, + ButtonStyle, + FormGroup, + Heading, + HeadingType, + Paragraph, + SimpleTable, + SimpleTableRow, +} from '@sovryn/ui'; + +import { MaxButton } from '../../../../../2_molecules/MaxButton/MaxButton'; +import { useAmountInput } from '../../../../../../hooks/useAmountInput'; +import { useWeiAmountInput } from '../../../../../../hooks/useWeiAmountInput'; +import { translations } from '../../../../../../locales/i18n'; +import { SendContext, SendStep } from '../../../contexts/send-context'; +import { getNetwork } from '../../../utils/networks'; + +const translation = translations.fastBtc.send.amountForm; + +// todo: replace with real data +const policies = [ + { + title: t(translation.policies.minimum), + value: '50 BUSD', + }, + { + title: t(translation.policies.conversion), + value: '2 BUSD', + }, + { + title: t(translation.policies.fee), + value: '3 BUSD', + }, +]; + +export const AmountForm = () => { + const { set, originNetwork, senderAsset, recipientAsset } = + useContext(SendContext); + console.log(senderAsset); + console.log(recipientAsset); + + const [amount, setAmount] = useWeiAmountInput('0'); + const [slippage, setSlippage] = useAmountInput('0.5'); + + const [slippageOpen, toggleSlippage] = useReducer(v => !v, false); + const [policiesOpen, togglePolicies] = useReducer(v => !v, false); + + // todo: replace with real data + const maxAmount = '12.345'; + const handleMaxButtonClick = useCallback( + () => setAmount(maxAmount), + [setAmount], + ); + + const networkName = useMemo( + () => getNetwork(originNetwork!).label, + [originNetwork], + ); + + const onContinueClick = useCallback( + () => + set(prevState => ({ + ...prevState, + step: SendStep.DETAILS, + amount, + })), + [set, amount], + ); + + return ( +
+ + {t(translation.titleBridgeFlow, { + senderAsset: senderAsset!.toUpperCase(), + network: networkName, + })} + + + {t(translation.description, { + senderAsset: senderAsset!.toUpperCase(), + recipientAsset: recipientAsset!.toUpperCase(), + network: networkName, + })} + + + +
+ {t(translation.amount.title)} + + +
+ +
+ + + + + + + + + + {policies.map(policy => ( + + ))} + + + + + {t(translation.summary.title)} + + + + + + + +
+ ); +}; diff --git a/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeSendFlow/components/DetailsScreen.tsx b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeSendFlow/components/DetailsScreen.tsx new file mode 100644 index 000000000..d8f3daaaa --- /dev/null +++ b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeSendFlow/components/DetailsScreen.tsx @@ -0,0 +1,134 @@ +import React, { useContext, useMemo } from 'react'; + +import { t } from 'i18next'; + +import { + Button, + Heading, + HeadingType, + SimpleTable, + SimpleTableRow, +} from '@sovryn/ui'; + +import { TxIdWithNotification } from '../../../../../2_molecules/TxIdWithNotification/TransactionIdWithNotification'; +import { translations } from '../../../../../../locales/i18n'; +import { + getBtcExplorerUrl, + getRskExplorerUrl, +} from '../../../../../../utils/helpers'; +import { formatValue } from '../../../../../../utils/math'; +import { SendContext } from '../../../contexts/send-context'; +import { getNetwork } from '../../../utils/networks'; + +const translation = translations.fastBtc.send.detailsScreen; + +const rskExplorerUrl = getRskExplorerUrl(); +const btcExplorerUrl = getBtcExplorerUrl(); + +type DetailsScreenProps = { + onConfirm: () => void; +}; + +export const DetailsScreen: React.FC = ({ onConfirm }) => { + const { senderAsset, recipientAsset, originNetwork, amount } = + useContext(SendContext); + + const items = useMemo( + () => [ + { + label: t(translation.from), + value: ( + + ), + }, + { + label: t(translation.to), + value: ( + + ), + }, + { + label: t(translation.sending), + value: ( + <> + {formatValue(amount, 4)} {senderAsset!.toUpperCase()} + + ), + }, + { + label: t(translation.conversionFee), + value: ( + <> + {formatValue(0, 4)} {recipientAsset!.toUpperCase()} + + ), + }, + { + label: t(translation.expectedToReceive), + value: ( + <> + {formatValue(0, 4)} {recipientAsset!.toUpperCase()} + + ), + }, + { + label: t(translation.minimumReceived), + value: ( + <> + {formatValue(0, 4)} {recipientAsset!.toUpperCase()} + + ), + }, + { + label: t(translation.networkTxId, { network: 'Rootstock' }), + value: ( + + ), + }, + { + label: t(translation.networkTxId, { + network: getNetwork(originNetwork!).label, + }), + value: ( + + ), + }, + ], + [amount, senderAsset, recipientAsset, originNetwork], + ); + + return ( + <> + + {t(translation.title)} + + + + {items.map(({ label, value }, index) => ( + + ))} + + +
+
+ + ); +}; diff --git a/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeSendFlow/components/NetworkScreen.tsx b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeSendFlow/components/NetworkScreen.tsx new file mode 100644 index 000000000..00297ec75 --- /dev/null +++ b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeSendFlow/components/NetworkScreen.tsx @@ -0,0 +1,170 @@ +import React, { useCallback, useContext, useReducer } from 'react'; + +import { t } from 'i18next'; + +import { ChainIds } from '@sovryn/ethers-provider'; +import { + Accordion, + Heading, + HeadingType, + Icon, + IconNames, + SimpleTable, + SimpleTableRow, + TransactionId, + WalletContainer, +} from '@sovryn/ui'; + +import { defaultChainId } from '../../../../../../config/chains'; + +import { useNetworkContext } from '../../../../../../contexts/NetworkContext'; +import { translations } from '../../../../../../locales/i18n'; +import { SendContext, SendStep } from '../../../contexts/send-context'; +import { OriginNetwork } from '../../../types'; +import { getNetwork } from '../../../utils/networks'; + +const translation = translations.fastBtc.send.networkScreen; + +export const NetworkScreen: React.FC = () => { + const [openBtcNetwork, toggleBtcNetwork] = useReducer(v => !v, false); + const [openEthNetwork, toggleEthNetwork] = useReducer(v => !v, false); + + const { set } = useContext(SendContext); + const { requireChain } = useNetworkContext(); + + const handleNetworkClick = useCallback( + (network: OriginNetwork) => () => { + set(prevState => ({ + ...prevState, + step: + network === OriginNetwork.BITCOIN + ? SendStep.BITCOIN_FLOW + : SendStep.ADDRESS, + originNetwork: network, + })); + + requireChain( + (network === OriginNetwork.BITCOIN + ? defaultChainId + : getNetwork(network).chainId) as ChainIds, + ); + }, + [requireChain, set], + ); + + //@TODO: Replace with real data + const availableLiquidity = [ + { + label: 'BNB', + value: '0.0001 BTC', + transactionId: '0x1234567890123456789012345678901234567890', + }, + { + label: 'BUSD', + value: '123,456,78 BUSD', + transactionId: '0x0987654321098765432109876543210987654321', + }, + { + label: 'ETH', + value: '0.123 ETH', + transactionId: '0xabcdef0123456789abcdef0123456789abcdef01', + }, + { + label: 'BTC', + value: '0.001 BTC', + transactionId: '0x5678901234567890123456789012345678901234', + }, + { + label: 'USDT', + value: '9,876 USDT', + transactionId: '0x3456789012345678901234567890123456789012', + }, + { + label: 'USDC', + value: '123,456 USDC', + transactionId: '0x6789012345678901234567890123456789012345', + }, + ]; + + return ( +
+ + {t(translation.title)} + + + } + onClick={handleNetworkClick(OriginNetwork.BITCOIN)} + className="mb-4" + /> + } + onClick={handleNetworkClick(OriginNetwork.BINANCE_SMART_CHAIN)} + className="mb-4" + /> + + + {availableLiquidity.map((item, index) => ( + + {item.label} + +
+ } + value={item.value} + /> + ))} + + } + dataAttribute="funding-send-available-liquidity-btc-accordion" + className="mb-4" + /> + + } + onClick={handleNetworkClick(OriginNetwork.ETHEREUM)} + className="mb-4" + /> + + {availableLiquidity.map((item, index) => ( + + {item.label} + + + } + value={item.value} + /> + ))} + + } + dataAttribute="funding-send-available-liquidity-eth-accordion" + /> + + ); +}; diff --git a/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeSendFlow/components/RecipientAssetScreen.tsx b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeSendFlow/components/RecipientAssetScreen.tsx new file mode 100644 index 000000000..0e0632b3a --- /dev/null +++ b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeSendFlow/components/RecipientAssetScreen.tsx @@ -0,0 +1,113 @@ +import React, { useCallback, useContext, useMemo, useState } from 'react'; + +import { t } from 'i18next'; + +import { SupportedTokens } from '@sovryn/contracts'; +import { + Align, + Button, + ButtonStyle, + Heading, + HeadingType, + TableBase, + TransactionId, +} from '@sovryn/ui'; + +import { translations } from '../../../../../../locales/i18n'; +import { SendContext, SendStep } from '../../../contexts/send-context'; +import { getNetwork } from '../../../utils/networks'; + +const translation = translations.fastBtc.send.recipientScreen; + +const columns = [ + { + id: 'asset', + title: t(translation.table.asset), + align: Align.left, + cellRenderer: row => `${row.asset.toUpperCase()}`, + }, + { + id: 'address', + title: t(translation.table.address), + align: Align.center, + cellRenderer: row => ( + + ), + }, + { + id: 'availableLiquidity', + title: t(translation.table.availableLiquidity), + align: Align.right, + cellRenderer: row => `${row.balance} ${row.asset.toUpperCase()}`, + }, +]; + +const rows = [ + { + asset: 'BUSD', + address: '0x1234567890123456789012345678901234567890', + balance: 0.2, + }, + { + asset: 'DAI', + address: '0x0p42490ACCbc50F4F9c130b5876521I1q7b3C0p', + balance: 1.234, + }, + { + asset: 'USDC', + address: '0x6p42490ACCbc50F4F9c130b5876521I1q7b3C0p', + balance: 4, + }, +]; + +export const RecipientAssetScreen: React.FC = () => { + const { set, originNetwork } = useContext(SendContext); + const [selectedAsset, setSelectedAsset] = useState(); + + const networkName = useMemo( + () => getNetwork(originNetwork!).label, + [originNetwork], + ); + + const onContinueClick = useCallback( + () => + set(prevState => ({ + ...prevState, + step: SendStep.AMOUNT, + recipientAsset: selectedAsset!, + })), + [selectedAsset, set], + ); + + const onRowClick = useCallback(row => setSelectedAsset(row.asset), []); + + return ( +
+ + {t(translation.title, { network: networkName })} + + + + +
+ ); +}; diff --git a/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeSendFlow/components/SenderAssetScreen.tsx b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeSendFlow/components/SenderAssetScreen.tsx new file mode 100644 index 000000000..bf03f60bc --- /dev/null +++ b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeSendFlow/components/SenderAssetScreen.tsx @@ -0,0 +1,106 @@ +import React, { useCallback, useContext, useState } from 'react'; + +import { t } from 'i18next'; + +import { SupportedTokens } from '@sovryn/contracts'; +import { + Align, + Button, + ButtonStyle, + Heading, + HeadingType, + TableBase, + TransactionId, +} from '@sovryn/ui'; + +import { translations } from '../../../../../../locales/i18n'; +import { SendContext, SendStep } from '../../../contexts/send-context'; + +const translation = translations.fastBtc.send.senderScreen; + +const columns = [ + { + id: 'asset', + title: t(translation.table.asset), + align: Align.left, + cellRenderer: row => `${row.asset.toUpperCase()}`, + }, + { + id: 'address', + title: t(translation.table.address), + align: Align.center, + cellRenderer: row => ( + + ), + }, + { + id: 'balance', + title: t(translation.table.balance), + align: Align.right, + cellRenderer: row => `${row.balance} ${row.asset.toUpperCase()}`, + }, +]; + +const rows = [ + { + asset: 'BNB', + address: '0x1234567890123456789012345678901234567890', + balance: 0.2, + }, + { + asset: 'DLLR', + address: '0x0p42490ACCbc50F4F9c130b5876521I1q7b3C0p', + balance: 1.234, + }, + { + asset: 'ETH', + address: '0x6p42490ACCbc50F4F9c130b5876521I1q7b3C0p', + balance: 4, + }, +]; + +export const SenderAssetScreen: React.FC = () => { + const { set } = useContext(SendContext); + const [selectedAsset, setSelectedAsset] = useState(); + + const onRowClick = useCallback(row => setSelectedAsset(row.asset), []); + + const onContinueClick = useCallback( + () => + set(prevState => ({ + ...prevState, + step: SendStep.RECIPIENT_ASSET, + senderAsset: selectedAsset!, + })), + [selectedAsset, set], + ); + + return ( +
+ + {t(translation.title)} + + + + +
+ ); +}; diff --git a/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeSendFlow/index.tsx b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeSendFlow/index.tsx new file mode 100644 index 000000000..e674df6db --- /dev/null +++ b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/BridgeSendFlow/index.tsx @@ -0,0 +1,107 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; + +import { ChainIds } from '@sovryn/ethers-provider'; + +import { defaultChainId } from '../../../../../config/chains'; + +import { useNetworkContext } from '../../../../../contexts/NetworkContext'; +import { + SendContextStateType, + SendStep, + defaultValue, + SendContext, +} from '../../contexts/send-context'; +import { GoBackButton } from '../GoBackButton'; +import { MobileCloseButton } from '../MobileCloseButton'; +import { SendFlow } from '../SendFlow/SendFlow'; +import { AddressForm } from './components/AddressForm'; +import { AmountForm } from './components/AmountForm'; +import { DetailsScreen } from './components/DetailsScreen'; +import { NetworkScreen } from './components/NetworkScreen'; +import { RecipientAssetScreen } from './components/RecipientAssetScreen'; +import { SenderAssetScreen } from './components/SenderAssetScreen'; + +type SendFlowProps = { + onClose: () => void; +}; + +const allowedStepsToGoBackFrom = [ + SendStep.SENDER_ASSET, + SendStep.RECIPIENT_ASSET, + SendStep.NETWORK, + SendStep.ADDRESS, + SendStep.AMOUNT, + SendStep.DETAILS, +]; + +const getBackStep = (step: SendStep) => { + switch (step) { + case SendStep.NETWORK: + return SendStep.MAIN; + case SendStep.ADDRESS: + return SendStep.NETWORK; + case SendStep.SENDER_ASSET: + return SendStep.ADDRESS; + case SendStep.RECIPIENT_ASSET: + return SendStep.SENDER_ASSET; + case SendStep.AMOUNT: + return SendStep.RECIPIENT_ASSET; + case SendStep.DETAILS: + return SendStep.AMOUNT; + default: + return SendStep.MAIN; + } +}; + +export const BridgeSendFlow: React.FC = ({ onClose }) => { + const { requireChain } = useNetworkContext(); + const [state, setState] = useState(defaultValue); + const { step } = state; + + const value = useMemo( + () => ({ + ...state, + set: setState, + }), + [state], + ); + + const onBackClick = useCallback(() => { + value.set(prevState => ({ ...prevState, step: getBackStep(value.step) })); + }, [value]); + + const handleConfirm = useCallback(() => {}, []); + + useEffect(() => { + return () => { + requireChain(defaultChainId as ChainIds); + }; + }, [requireChain]); + + return ( + + {step === SendStep.BITCOIN_FLOW ? ( + + ) : ( + <> + {allowedStepsToGoBackFrom.includes(value.step) && ( + + )} +
+ {(step === SendStep.MAIN || step === SendStep.NETWORK) && ( + + )} + {step === SendStep.ADDRESS && } + {step === SendStep.SENDER_ASSET && } + {step === SendStep.RECIPIENT_ASSET && } + {step === SendStep.AMOUNT && } + {step === SendStep.DETAILS && ( + + )} +
+ + + )} +
+ ); +}; diff --git a/apps/frontend/src/app/3_organisms/FastBtcDialog/components/ReceiveFlow/ReceiveFlow.tsx b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/ReceiveFlow/ReceiveFlow.tsx index f0e406ee3..d13a8e7a8 100644 --- a/apps/frontend/src/app/3_organisms/FastBtcDialog/components/ReceiveFlow/ReceiveFlow.tsx +++ b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/ReceiveFlow/ReceiveFlow.tsx @@ -22,9 +22,13 @@ import { StatusScreen } from './components/StatusScreen'; type ReceiveFlowProps = { onClose: () => void; + onBack?: () => void; }; -export const ReceiveFlow: React.FC = ({ onClose }) => { +export const ReceiveFlow: React.FC = ({ + onClose, + onBack, +}) => { const { account } = useAccount(); const { value: block } = useBlockNumber(); @@ -145,7 +149,8 @@ export const ReceiveFlow: React.FC = ({ onClose }) => { const onBackClick = useCallback(() => { setState(prevState => ({ ...prevState, step: DepositStep.MAIN })); - }, []); + onBack?.(); + }, [onBack]); const [getDepositRskTransaction] = useGetFastBtcDepositRskTransactionLazyQuery(); @@ -181,7 +186,9 @@ export const ReceiveFlow: React.FC = ({ onClose }) => { return ( - {step === DepositStep.ADDRESS && } + {(step === DepositStep.ADDRESS || onBack) && ( + + )}
{step === DepositStep.MAIN && } diff --git a/apps/frontend/src/app/3_organisms/FastBtcDialog/components/SendFlow/SendFlow.tsx b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/SendFlow/SendFlow.tsx index da8ee3b49..57461efe2 100644 --- a/apps/frontend/src/app/3_organisms/FastBtcDialog/components/SendFlow/SendFlow.tsx +++ b/apps/frontend/src/app/3_organisms/FastBtcDialog/components/SendFlow/SendFlow.tsx @@ -24,25 +24,27 @@ const getBackStep = (step: WithdrawStep) => { case WithdrawStep.REVIEW: return WithdrawStep.ADDRESS; default: - return WithdrawStep.AMOUNT; + return WithdrawStep.MAIN; } }; type SendFlowProps = { onClose: () => void; + onBack?: () => void; }; -export const SendFlow: React.FC = ({ onClose }) => { +export const SendFlow: React.FC = ({ onClose, onBack }) => { const value = useWithdrawBridgeConfig(); const { step, set } = value; const onBackClick = useCallback(() => { set(prevState => ({ ...prevState, step: getBackStep(step) })); - }, [set, step]); + onBack?.(); + }, [set, step, onBack]); return ( - {allowedStepsToGoBackFrom.includes(step) && ( + {(allowedStepsToGoBackFrom.includes(step) || onBack) && ( )} diff --git a/apps/frontend/src/app/3_organisms/FastBtcDialog/contexts/receive-context.ts b/apps/frontend/src/app/3_organisms/FastBtcDialog/contexts/receive-context.ts new file mode 100644 index 000000000..a6fe54369 --- /dev/null +++ b/apps/frontend/src/app/3_organisms/FastBtcDialog/contexts/receive-context.ts @@ -0,0 +1,40 @@ +import { createContext, Dispatch, SetStateAction } from 'react'; + +import { SupportedTokens } from '@sovryn/contracts'; + +import { Nullable } from '../../../../types/global'; +import { OriginNetwork } from '../types'; + +export enum ReceiveStep { + MAIN, + SELECT_ASSET, + BITCOIN_FLOW, + AMOUNT, + DETAILS, +} + +export type ReceiveContextStateType = { + step: ReceiveStep; + originNetwork: Nullable; + asset: SupportedTokens; + amount: string; +}; + +export type ReceiveContextFunctionsType = { + set: Dispatch>; +}; + +export type ReceiveContextType = ReceiveContextStateType & + ReceiveContextFunctionsType; + +export const defaultValue: ReceiveContextType = { + step: ReceiveStep.MAIN, + originNetwork: null, + asset: SupportedTokens.dllr, + amount: '0', + set: () => { + throw new Error('set() has not been defined.'); + }, +}; + +export const ReceiveContext = createContext(defaultValue); diff --git a/apps/frontend/src/app/3_organisms/FastBtcDialog/contexts/send-context.ts b/apps/frontend/src/app/3_organisms/FastBtcDialog/contexts/send-context.ts new file mode 100644 index 000000000..11bb65f4b --- /dev/null +++ b/apps/frontend/src/app/3_organisms/FastBtcDialog/contexts/send-context.ts @@ -0,0 +1,46 @@ +import { createContext, Dispatch, SetStateAction } from 'react'; + +import { SupportedTokens } from '@sovryn/contracts'; + +import { Nullable } from '../../../../types/global'; +import { OriginNetwork } from '../types'; + +export enum SendStep { + BITCOIN_FLOW, + MAIN, + NETWORK, + ADDRESS, + SENDER_ASSET, + RECIPIENT_ASSET, + AMOUNT, + DETAILS, +} + +export type SendContextStateType = { + step: SendStep; + originNetwork: Nullable; + address: string; + senderAsset: Nullable; + recipientAsset: Nullable; + amount: string; +}; + +export type SendContextFunctionsType = { + set: Dispatch>; +}; + +export type SendContextType = SendContextStateType & SendContextFunctionsType; + +export const defaultValue: SendContextType = { + step: SendStep.MAIN, + originNetwork: null, + address: '', + senderAsset: null, + recipientAsset: null, + amount: '0', + set: () => { + throw new Error('set() has not been defined.'); + }, +}; + +export const SendContext = createContext(defaultValue); diff --git a/apps/frontend/src/app/3_organisms/FastBtcDialog/contexts/withdraw-context.ts b/apps/frontend/src/app/3_organisms/FastBtcDialog/contexts/withdraw-context.ts index 34ba563b7..14ec05014 100644 --- a/apps/frontend/src/app/3_organisms/FastBtcDialog/contexts/withdraw-context.ts +++ b/apps/frontend/src/app/3_organisms/FastBtcDialog/contexts/withdraw-context.ts @@ -2,6 +2,9 @@ import { createContext, Dispatch, SetStateAction } from 'react'; export enum WithdrawStep { MAIN, + NETWORK, + SENDER_ASSET, + RECIPIENT_ASSET, AMOUNT, ADDRESS, REVIEW, diff --git a/apps/frontend/src/app/3_organisms/FastBtcDialog/types.ts b/apps/frontend/src/app/3_organisms/FastBtcDialog/types.ts index 971e98618..5bcccbc99 100644 --- a/apps/frontend/src/app/3_organisms/FastBtcDialog/types.ts +++ b/apps/frontend/src/app/3_organisms/FastBtcDialog/types.ts @@ -10,3 +10,9 @@ export enum ReceiveEvents { getDepositAddress = 'getDepositAddress', getDepositHistory = 'getDepositHistory', } + +export enum OriginNetwork { + BITCOIN = 'Bitcoin', + ETHEREUM = 'Ethereum', + BINANCE_SMART_CHAIN = 'BNB Smart Chain', +} diff --git a/apps/frontend/src/app/3_organisms/FastBtcDialog/utils/networks.ts b/apps/frontend/src/app/3_organisms/FastBtcDialog/utils/networks.ts new file mode 100644 index 000000000..7cb73e80f --- /dev/null +++ b/apps/frontend/src/app/3_organisms/FastBtcDialog/utils/networks.ts @@ -0,0 +1,30 @@ +import { t } from 'i18next'; + +import { ChainIds } from '@sovryn/ethers-provider'; + +import { translations } from '../../../../locales/i18n'; +import { isMainnet } from '../../../../utils/helpers'; +import { OriginNetwork } from '../types'; + +type NetworkDetails = { + label: OriginNetwork; + chainId: ChainIds; +}; + +export const networks: Partial> = { + [OriginNetwork.BITCOIN]: { + label: t(translations.common.networks.bitcoin), + chainId: isMainnet() ? ChainIds.RSK_MAINNET : ChainIds.RSK_TESTNET, + }, + [OriginNetwork.ETHEREUM]: { + label: t(translations.common.networks.ethereum), + chainId: isMainnet() ? ChainIds.MAINNET : ChainIds.SEPOLIA, + }, + [OriginNetwork.BINANCE_SMART_CHAIN]: { + label: t(translations.common.networks.bnb), + chainId: isMainnet() ? ChainIds.BSC_MAINNET : ChainIds.BSC_TESTNET, + }, +} as const; + +export const getNetwork = (originNetwork: OriginNetwork) => + networks[originNetwork] as NetworkDetails; diff --git a/apps/frontend/src/app/3_organisms/NetworkProvider/NetworkProvider.tsx b/apps/frontend/src/app/3_organisms/NetworkProvider/NetworkProvider.tsx index ccda67a8a..23265961c 100644 --- a/apps/frontend/src/app/3_organisms/NetworkProvider/NetworkProvider.tsx +++ b/apps/frontend/src/app/3_organisms/NetworkProvider/NetworkProvider.tsx @@ -7,6 +7,7 @@ import { getProvider } from '@sovryn/ethers-provider'; import { chains } from '../../../config/chains'; +import { NetworkContextProvider } from '../../../contexts/NetworkContext'; import { onboard } from '../../../lib/connector'; import { CacheCallOptions, @@ -59,5 +60,5 @@ export const NetworkProvider: React.FC = ({ }; }, []); - return <>{children}; + return {children}; }; diff --git a/apps/frontend/src/config/chains.ts b/apps/frontend/src/config/chains.ts index 5b5a80bd7..46d8dfa86 100644 --- a/apps/frontend/src/config/chains.ts +++ b/apps/frontend/src/config/chains.ts @@ -1,12 +1,20 @@ import setup, { Chain, ChainIds } from '@sovryn/ethers-provider'; -import { RSK_EXPLORER, RSK_RPC } from '../constants/infrastructure'; +import { + BSC_EXPLORER, + BSC_RPC, + ETH_EXPLORER, + ETH_RPC, + RSK_EXPLORER, + RSK_RPC, +} from '../constants/infrastructure'; import { Environments } from '../types/global'; import { isMainnet } from '../utils/helpers'; export enum Chains { RSK = 'rsk', BSC = 'bsc', + ETH = 'eth', } export const defaultChainId = ( @@ -31,6 +39,36 @@ export const chains: Chain[] = [ rpcUrl: RSK_RPC[Environments.Testnet], blockExplorerUrl: RSK_EXPLORER[Environments.Testnet], }, + isMainnet() + ? { + id: ChainIds.MAINNET, + label: 'Ethereum', + token: 'ETH', + rpcUrl: ETH_RPC[Environments.Mainnet], + blockExplorerUrl: ETH_EXPLORER[Environments.Mainnet], + } + : { + id: ChainIds.SEPOLIA, + label: 'Sepolia', + token: 'tETH', + rpcUrl: ETH_RPC[Environments.Testnet], + blockExplorerUrl: ETH_EXPLORER[Environments.Testnet], + }, + isMainnet() + ? { + id: ChainIds.BSC_MAINNET, + label: 'BNB Smart Chain', + token: 'BNB', + rpcUrl: BSC_RPC[Environments.Mainnet], + blockExplorerUrl: BSC_EXPLORER[Environments.Mainnet], + } + : { + id: ChainIds.BSC_TESTNET, + label: 'BNB Smart Chain testnet', + token: 'tBNB', + rpcUrl: BSC_RPC[Environments.Testnet], + blockExplorerUrl: BSC_EXPLORER[Environments.Testnet], + }, ]; setup(chains); diff --git a/apps/frontend/src/constants/infrastructure.ts b/apps/frontend/src/constants/infrastructure.ts index b1a32d838..0564770d2 100644 --- a/apps/frontend/src/constants/infrastructure.ts +++ b/apps/frontend/src/constants/infrastructure.ts @@ -23,12 +23,35 @@ export const RSK_RPC = { ], }; +export const ETH_RPC = { + [Environments.Mainnet]: [ + 'https://eth.llamarpc.com', + 'https://ethereum.publicnode.com', + ], + [Environments.Testnet]: ['https://rpc2.sepolia.org'], +}; + +export const BSC_RPC = { + [Environments.Mainnet]: ['https://bsc.publicnode.com'], + [Environments.Testnet]: ['https://bsc-testnet.publicnode.com'], +}; + //TODO: refactor this into separate dictionary file once we add more chains export const RSK_EXPLORER = { [Environments.Mainnet]: 'https://explorer.rsk.co', [Environments.Testnet]: 'https://explorer.testnet.rsk.co', }; +export const ETH_EXPLORER = { + [Environments.Mainnet]: 'https://etherscan.io', + [Environments.Testnet]: 'https://sepolia.etherscan.io', +}; + +export const BSC_EXPLORER = { + [Environments.Mainnet]: 'https://bscscan.com', + [Environments.Testnet]: 'https://testnet.bscscan.com', +}; + export const BTC_EXPLORER = { [Environments.Mainnet]: 'https://live.blockcypher.com/btc', [Environments.Testnet]: 'https://live.blockcypher.com/btc-testnet', diff --git a/apps/frontend/src/constants/links.ts b/apps/frontend/src/constants/links.ts index 34b4877da..1a276f11e 100644 --- a/apps/frontend/src/constants/links.ts +++ b/apps/frontend/src/constants/links.ts @@ -43,6 +43,8 @@ export const WIKI_LINKS = { SECURITY: 'https://wiki.sovryn.com/technical-documents#security', FEES: 'https://wiki.sovryn.com/sovryn-dapp/fees#zero-borrowing', RISKS: 'https://wiki.sovryn.com/en/sovryn-dapp/subprotocols/zero-zusd#risks', + BRIDGE_ADDRESS: + 'https://wiki.sovryn.app/en/sovryn-dapp/bridge#beware-when-sending-to-exchange-addresses', }; export const HELPDESK_LINK = 'https://help.sovryn.app/'; diff --git a/apps/frontend/src/contexts/NetworkContext.tsx b/apps/frontend/src/contexts/NetworkContext.tsx new file mode 100644 index 000000000..877aea138 --- /dev/null +++ b/apps/frontend/src/contexts/NetworkContext.tsx @@ -0,0 +1,48 @@ +import React, { + createContext, + useCallback, + useContext, + useMemo, + useState, +} from 'react'; + +import { ChainIds } from '@sovryn/ethers-provider'; + +import { defaultChainId } from '../config/chains'; + +interface NotificationContextInterface { + chainId: ChainIds; + requireChain: (chainId: ChainIds) => void; +} + +const NetworkContext = createContext({ + chainId: defaultChainId as ChainIds, +} as NotificationContextInterface); + +export const useNetworkContext = () => + useContext(NetworkContext) as NotificationContextInterface; + +interface NotificationProviderProps { + children?: React.ReactNode; +} + +export const NetworkContextProvider: React.FC = ({ + children, +}) => { + const [chainId, setChainId] = useState(defaultChainId as ChainIds); + const requireChain = useCallback((chainId: ChainIds) => { + setChainId(chainId); + }, []); + + const value = useMemo( + () => ({ + chainId, + requireChain, + }), + [chainId, requireChain], + ); + + return ( + {children} + ); +}; diff --git a/apps/frontend/src/hooks/useWrongNetworkCheck.tsx b/apps/frontend/src/hooks/useWrongNetworkCheck.tsx index bcaa83088..d41cf7066 100644 --- a/apps/frontend/src/hooks/useWrongNetworkCheck.tsx +++ b/apps/frontend/src/hooks/useWrongNetworkCheck.tsx @@ -4,29 +4,34 @@ import { t } from 'i18next'; import { Paragraph, NotificationType, Button, ButtonStyle } from '@sovryn/ui'; -import { chains, defaultChainId } from '../config/chains'; +import { chains } from '../config/chains'; +import { useNetworkContext } from '../contexts/NetworkContext'; import { useNotificationContext } from '../contexts/NotificationContext'; import { useWalletConnect } from './useWalletConnect'; -const defaultChain = chains.find(chain => chain.id === defaultChainId); - const WrongNetworkSwitcherId = 'WrongNetworkSwitcher'; export const useWrongNetworkCheck = () => { + const { chainId } = useNetworkContext(); const { addNotification, removeNotification } = useNotificationContext(); const { wallets, switchNetwork } = useWalletConnect(); const isWrongChain = useMemo(() => { return ( - wallets[0]?.accounts[0]?.address && - wallets[0].chains[0].id !== defaultChainId + wallets[0]?.accounts[0]?.address && wallets[0].chains[0].id !== chainId ); - }, [wallets]); + }, [wallets, chainId]); + + const chain = useMemo( + () => chains.find(chain => chain.id === chainId), + [chainId], + ); - const switchChain = useCallback(() => { - switchNetwork(defaultChainId); - }, [switchNetwork]); + const switchChain = useCallback( + () => switchNetwork(chainId), + [chainId, switchNetwork], + ); useEffect(() => { if (isWrongChain) { @@ -38,7 +43,7 @@ export const useWrongNetworkCheck = () => { <> {t('wrongNetworkSwitcher.description', { - network: defaultChain?.label, + network: chain?.label, })}