Compare commits

...
Author SHA1 Message Date
Bill He 0189fac442 flex-wrap 2023-11-10 16:03:59 -08:00
Bill He 610c583246 Add additional onboarding entry points 2023-11-10 14:25:17 -08:00
Bill d4bcfc0428 Fix status empty bug (#143) 2023-11-10 09:55:54 -08:00
aleka 7dec32df5e fix 24h sort in market dropdown, table styling (#141) 2023-11-09 16:40:27 -05:00
aleka e09d574d5b show deposit form in mainnet onboarding (#140) 2023-11-09 15:33:16 -05:00
Bill 944fc6dc95 Handle squid rout errors (#139)
* Handle squid route errors

* bump abacus

* add dep

* address comments

* fix max

* disable slippage editor

* parse error message in abacus
2023-11-09 10:34:22 -08:00
aleka 91a97a1c68 add network select menu in restriction modals (dev) (#137) 2023-11-09 09:10:12 -05:00
25 changed files with 250 additions and 126 deletions
+1 -1
View File
@@ -39,7 +39,7 @@
"@cosmjs/proto-signing": "^0.31.0", "@cosmjs/proto-signing": "^0.31.0",
"@cosmjs/stargate": "^0.31.0", "@cosmjs/stargate": "^0.31.0",
"@cosmjs/tendermint-rpc": "^0.31.0", "@cosmjs/tendermint-rpc": "^0.31.0",
"@dydxprotocol/v4-abacus": "^1.0.19", "@dydxprotocol/v4-abacus": "^1.0.24",
"@dydxprotocol/v4-client-js": "^1.0.0", "@dydxprotocol/v4-client-js": "^1.0.0",
"@dydxprotocol/v4-localization": "^1.0.5", "@dydxprotocol/v4-localization": "^1.0.5",
"@ethersproject/providers": "^5.7.2", "@ethersproject/providers": "^5.7.2",
+4 -4
View File
@@ -27,8 +27,8 @@ dependencies:
specifier: ^0.31.0 specifier: ^0.31.0
version: 0.31.0 version: 0.31.0
'@dydxprotocol/v4-abacus': '@dydxprotocol/v4-abacus':
specifier: ^1.0.19 specifier: ^1.0.24
version: 1.0.19 version: 1.0.24
'@dydxprotocol/v4-client-js': '@dydxprotocol/v4-client-js':
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0 version: 1.0.0
@@ -982,8 +982,8 @@ packages:
resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==} resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==}
dev: true dev: true
/@dydxprotocol/v4-abacus@1.0.19: /@dydxprotocol/v4-abacus@1.0.24:
resolution: {integrity: sha512-XhvSHGr503gNwHWEiOYP7M2uYeLu+Qm4szpE5w5H6hWFCanSGz6zAsTbPuyPLtt7IE1gj5z8mlL9J3tw2AZROg==} resolution: {integrity: sha512-wDGSjkrc3Se6Ev7UTjPgJV7PiyzZSz2mJwOTZikLwH7W3k1iPYGQmaCd7TudFk8h2aSdlEwNsQBCf0sLoyvHaQ==}
dev: false dev: false
/@dydxprotocol/v4-client-js@1.0.0: /@dydxprotocol/v4-client-js@1.0.0:
+1
View File
@@ -439,6 +439,7 @@
"tos": "https://dydx.exchange/v4-terms", "tos": "https://dydx.exchange/v4-terms",
"privacy": "https://dydx.exchange/privacy", "privacy": "https://dydx.exchange/privacy",
"mintscan": "https://testnet.mintscan.io/dydx-testnet/txs/{tx_hash}", "mintscan": "https://testnet.mintscan.io/dydx-testnet/txs/{tx_hash}",
"mintscanBase": "https://testnet.mintscan.io/dydx-testnet",
"documentation": "https://v4-teacher.vercel.app/", "documentation": "https://v4-teacher.vercel.app/",
"community": "https://discord.com/invite/dydx", "community": "https://discord.com/invite/dydx",
"feedback": "https://docs.google.com/forms/d/e/1FAIpQLSezLsWCKvAYDEb7L-2O4wOON1T56xxro9A2Azvl6IxXHP_15Q/viewform", "feedback": "https://docs.google.com/forms/d/e/1FAIpQLSezLsWCKvAYDEb7L-2O4wOON1T56xxro9A2Azvl6IxXHP_15Q/viewform",
+7 -1
View File
@@ -1,4 +1,4 @@
import { useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import styled, { type AnyStyledComponent, css } from 'styled-components'; import styled, { type AnyStyledComponent, css } from 'styled-components';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
@@ -22,6 +22,11 @@ export const SearchInput = ({
}: SearchInputProps) => { }: SearchInputProps) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (isOpen) inputRef?.current?.focus();
}, [inputRef, isOpen]);
return ( return (
<Styled.Search> <Styled.Search>
@@ -41,6 +46,7 @@ export const SearchInput = ({
/> />
<Styled.Input <Styled.Input
autoFocus autoFocus
ref={inputRef}
value={value} value={value}
isOpen={isOpen} isOpen={isOpen}
type={type} type={type}
+12 -8
View File
@@ -831,6 +831,7 @@ Styled.SortArrow = styled.span<{ sortDirection: 'ascending' | 'descending' }>`
Styled.Thead = styled.thead<StyleProps>` Styled.Thead = styled.thead<StyleProps>`
${layoutMixins.stickyHeader} ${layoutMixins.stickyHeader}
scroll-snap-align: none; scroll-snap-align: none;
font: var(--font-mini-book);
> * { > * {
height: var(--stickyArea-topHeight); height: var(--stickyArea-topHeight);
@@ -839,18 +840,17 @@ Styled.Thead = styled.thead<StyleProps>`
color: var(--tableHeader-textColor); color: var(--tableHeader-textColor);
background-color: var(--tableHeader-backgroundColor); background-color: var(--tableHeader-backgroundColor);
@media ${breakpoints.notTablet} { ${({ withInnerBorders, withGradientCardRows }) =>
${({ withInnerBorders, withGradientCardRows }) => withInnerBorders &&
withInnerBorders && !withGradientCardRows &&
!withGradientCardRows && css`
css` ${layoutMixins.withInnerHorizontalBorders}
${layoutMixins.withInnerHorizontalBorders} `}
`}
}
`; `;
Styled.Tbody = styled.tbody<StyleProps>` Styled.Tbody = styled.tbody<StyleProps>`
${layoutMixins.stickyArea2} ${layoutMixins.stickyArea2}
font: var(--font-small-book);
// If <table> height is fixed with not enough rows to overflow, vertically center the rows // If <table> height is fixed with not enough rows to overflow, vertically center the rows
&:before, &:before,
@@ -869,6 +869,10 @@ Styled.Tbody = styled.tbody<StyleProps>`
--stickyArea2-paddingBottom: var(--border-width); --stickyArea2-paddingBottom: var(--border-width);
--stickyArea2-paddingLeft: var(--border-width); --stickyArea2-paddingLeft: var(--border-width);
--stickyArea2-paddingRight: var(--border-width); --stickyArea2-paddingRight: var(--border-width);
tr:first-of-type {
box-shadow: none;
}
`} `}
${({ withGradientCardRows }) => ${({ withGradientCardRows }) =>
+2 -2
View File
@@ -136,8 +136,8 @@ const useAccountsContext = () => {
getSubaccounts: async ({ dydxAddress }: { dydxAddress: DydxAddress }) => { getSubaccounts: async ({ dydxAddress }: { dydxAddress: DydxAddress }) => {
try { try {
const response = await compositeClient?.indexerClient.account.getSubaccounts(dydxAddress); const response = await compositeClient?.indexerClient.account.getSubaccounts(dydxAddress);
setDydxSubaccounts(response.subaccounts); setDydxSubaccounts(response?.subaccounts);
return response.subaccounts; return response?.subaccounts ?? [];
} catch (error) { } catch (error) {
// 404 is expected if the user has no subaccounts // 404 is expected if the user has no subaccounts
if (error.status === 404) { if (error.status === 404) {
+10 -1
View File
@@ -66,7 +66,16 @@ const useLocalNotificationsContext = () => {
status: currentStatus, status: currentStatus,
} of transferNotifications) { } of transferNotifications) {
try { try {
if (currentStatus && currentStatus?.squidTransactionStatus !== 'ongoing') continue; // skip if error is returned or if the transaction is not ongoing
if (
// @ts-ignore status.errors is not in the type definition but can be returned
currentStatus?.errors ||
currentStatus?.error ||
(currentStatus?.squidTransactionStatus &&
currentStatus?.squidTransactionStatus !== 'ongoing')
) {
continue;
}
const status = await squid?.getStatus({ transactionId: txHash, toChainId, fromChainId }); const status = await squid?.getStatus({ transactionId: txHash, toChainId, fromChainId });
if (status) statuses[txHash] = status; if (status) statuses[txHash] = status;
+1 -1
View File
@@ -8,7 +8,7 @@ import { getSelectedNetwork } from '@/state/appSelectors';
export const NATIVE_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; export const NATIVE_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
export const STATUS_ERROR_GRACE_PERIOD = 120_000; export const STATUS_ERROR_GRACE_PERIOD = 300_000;
const useSquidContext = () => { const useSquidContext = () => {
const selectedNetwork = useSelector(getSelectedNetwork); const selectedNetwork = useSelector(getSelectedNetwork);
+3 -4
View File
@@ -200,10 +200,9 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
if (isTestnet) { if (isTestnet) {
console.log( console.log(
`${ `${ENVIRONMENT_CONFIG_MAP[
ENVIRONMENT_CONFIG_MAP[this.compositeClient.network.getString() as DydxNetwork]?.links this.compositeClient.network.getString() as DydxNetwork
?.mintscanBase ]?.links?.mintscan?.replace('{tx_hash}', hash.toString())}`
}/txs/${hash}`
); );
} else console.log(`txHash: ${hash}`); } else console.log(`txHash: ${hash}`);
+1 -6
View File
@@ -56,12 +56,7 @@ class AbacusRest implements AbacusRestProtocol {
.then(async (response) => { .then(async (response) => {
const data = await response.text(); const data = await response.text();
if (response.ok) { callback(data, response.status);
callback(data, response.status);
} else {
// response not OK, call callback with null data and the status, this includes 400/500 status codes
callback(null, response.status);
}
try { try {
lastSuccessfulRestRequestByOrigin[new URL(url).origin] = Date.now(); lastSuccessfulRestRequestByOrigin[new URL(url).origin] = Date.now();
-4
View File
@@ -304,10 +304,6 @@ Styled.FeeTable = styled(Table)`
@media ${breakpoints.notTablet} { @media ${breakpoints.notTablet} {
--tableHeader-backgroundColor: var(--color-layer-1); --tableHeader-backgroundColor: var(--color-layer-1);
thead tr {
--border-width: 0;
}
} }
`; `;
+104 -42
View File
@@ -1,11 +1,13 @@
import { useEffect } from 'react'; import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent } from 'styled-components'; import styled, { type AnyStyledComponent } from 'styled-components';
import { Navigate, Route, Routes } from 'react-router-dom'; import { Navigate, Route, Routes } from 'react-router-dom';
import { OnboardingState } from '@/constants/account';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { HistoryRoute, PortfolioRoute } from '@/constants/routes'; import { HistoryRoute, PortfolioRoute } from '@/constants/routes';
import { useBreakpoints, useDocumentTitle, useStringGetter } from '@/hooks'; import { useAccountBalance, useBreakpoints, useDocumentTitle, useStringGetter } from '@/hooks';
import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable'; import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable';
import { FundingPaymentsTable } from '@/views/tables/FundingPaymentsTable'; import { FundingPaymentsTable } from '@/views/tables/FundingPaymentsTable';
@@ -14,6 +16,9 @@ import { Icon, IconName } from '@/components/Icon';
import { NavigationMenu } from '@/components/NavigationMenu'; import { NavigationMenu } from '@/components/NavigationMenu';
import { WithSidebar } from '@/components/WithSidebar'; import { WithSidebar } from '@/components/WithSidebar';
import { getOnboardingState, getSubaccount } from '@/state/accountSelectors';
import { openDialog } from '@/state/dialogs';
import { PortfolioNavMobile } from './PortfolioNavMobile'; import { PortfolioNavMobile } from './PortfolioNavMobile';
import { Overview } from './Overview'; import { Overview } from './Overview';
import { Positions } from './Positions'; import { Positions } from './Positions';
@@ -22,11 +27,20 @@ import { Fees } from './Fees';
import { History } from './History'; import { History } from './History';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
import { Button } from '@/components/Button';
import { ButtonAction } from '@/constants/buttons';
export default () => { export default () => {
const dispatch = useDispatch();
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
const { isTablet, isNotTablet } = useBreakpoints(); const { isTablet, isNotTablet } = useBreakpoints();
const onboardingState = useSelector(getOnboardingState);
const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {};
const { nativeTokenBalance } = useAccountBalance();
const usdcBalance = freeCollateral?.current || 0;
useDocumentTitle(stringGetter({ key: STRING_KEYS.PORTFOLIO })); useDocumentTitle(stringGetter({ key: STRING_KEYS.PORTFOLIO }));
const routesComponent = ( const routesComponent = (
@@ -84,46 +98,74 @@ export default () => {
<WithSidebar <WithSidebar
sidebar={ sidebar={
isTablet ? null : ( isTablet ? null : (
<Styled.NavigationMenu <Styled.SideBar>
items={[ <Styled.NavigationMenu
{ items={[
group: 'views', {
groupLabel: stringGetter({ key: STRING_KEYS.VIEWS }), group: 'views',
items: [ groupLabel: stringGetter({ key: STRING_KEYS.VIEWS }),
{ items: [
value: PortfolioRoute.Overview, {
slotBefore: <Styled.Icon iconName={IconName.Overview} />, value: PortfolioRoute.Overview,
label: stringGetter({ key: STRING_KEYS.OVERVIEW }), slotBefore: <Styled.Icon iconName={IconName.Overview} />,
href: PortfolioRoute.Overview, label: stringGetter({ key: STRING_KEYS.OVERVIEW }),
}, href: PortfolioRoute.Overview,
{ },
value: PortfolioRoute.Positions, {
slotBefore: <Styled.Icon iconName={IconName.Cube} />, value: PortfolioRoute.Positions,
label: stringGetter({ key: STRING_KEYS.POSITIONS }), slotBefore: <Styled.Icon iconName={IconName.Cube} />,
href: PortfolioRoute.Positions, label: stringGetter({ key: STRING_KEYS.POSITIONS }),
}, href: PortfolioRoute.Positions,
{ },
value: PortfolioRoute.Orders, {
slotBefore: <Styled.Icon iconName={IconName.OrderPending} />, value: PortfolioRoute.Orders,
label: stringGetter({ key: STRING_KEYS.ORDERS }), slotBefore: <Styled.Icon iconName={IconName.OrderPending} />,
href: PortfolioRoute.Orders, label: stringGetter({ key: STRING_KEYS.ORDERS }),
}, href: PortfolioRoute.Orders,
{ },
value: PortfolioRoute.Fees, {
slotBefore: <Styled.Icon iconName={IconName.Calculator} />, value: PortfolioRoute.Fees,
label: stringGetter({ key: STRING_KEYS.FEES }), slotBefore: <Styled.Icon iconName={IconName.Calculator} />,
href: PortfolioRoute.Fees, label: stringGetter({ key: STRING_KEYS.FEES }),
}, href: PortfolioRoute.Fees,
{ },
value: PortfolioRoute.History, {
slotBefore: <Styled.Icon iconName={IconName.History} />, value: PortfolioRoute.History,
label: stringGetter({ key: STRING_KEYS.HISTORY }), slotBefore: <Styled.Icon iconName={IconName.History} />,
href: PortfolioRoute.History, label: stringGetter({ key: STRING_KEYS.HISTORY }),
}, href: PortfolioRoute.History,
], },
}, ],
]} },
/> ]}
/>
{onboardingState === OnboardingState.AccountConnected && (
<Styled.Footer>
<Button
action={ButtonAction.Primary}
onClick={() => dispatch(openDialog({ type: DialogTypes.Deposit }))}
>
{stringGetter({ key: STRING_KEYS.DEPOSIT })}
</Button>
{usdcBalance > 0 && (
<Button
action={ButtonAction.Base}
onClick={() => dispatch(openDialog({ type: DialogTypes.Withdraw }))}
>
{stringGetter({ key: STRING_KEYS.WITHDRAW })}
</Button>
)}
{(usdcBalance > 0 || nativeTokenBalance.gt(0)) && (
<Button
action={ButtonAction.Base}
onClick={() => dispatch(openDialog({ type: DialogTypes.Transfer }))}
>
{stringGetter({ key: STRING_KEYS.TRANSFER })}
</Button>
)}
</Styled.Footer>
)}
</Styled.SideBar>
) )
} }
> >
@@ -139,6 +181,26 @@ Styled.PortfolioMobile = styled.div`
${layoutMixins.expandingColumnWithHeader} ${layoutMixins.expandingColumnWithHeader}
`; `;
Styled.SideBar = styled.div`
${layoutMixins.flexColumn}
justify-content: space-between;
height: 100%;
`;
Styled.Footer = styled.div`
${layoutMixins.row}
flex-wrap: wrap;
padding: 1rem;
gap: 0.5rem;
> button {
flex-grow: 1;
}
`;
Styled.NavigationMenu = styled(NavigationMenu)` Styled.NavigationMenu = styled(NavigationMenu)`
padding: 0.5rem; padding: 0.5rem;
padding-top: 0; padding-top: 0;
+8 -17
View File
@@ -43,23 +43,14 @@ export const DYDXBalancePanel = () => {
{!canAccountTrade ? ( {!canAccountTrade ? (
<OnboardingTriggerButton size={ButtonSize.Small} /> <OnboardingTriggerButton size={ButtonSize.Small} />
) : ( ) : (
<> <Button
<Styled.ReceiveButton slotLeft={<Icon iconName={IconName.Send} />}
slotLeft={<Icon iconName={IconName.Qr} />} size={ButtonSize.Small}
size={ButtonSize.Small} action={ButtonAction.Primary}
onClick={() => dispatch(openDialog({ type: DialogTypes.Receive }))} onClick={() => dispatch(openDialog({ type: DialogTypes.Transfer }))}
> >
{stringGetter({ key: STRING_KEYS.RECEIVE })} {stringGetter({ key: STRING_KEYS.TRANSFER })}
</Styled.ReceiveButton> </Button>
<Button
slotLeft={<Icon iconName={IconName.Send} />}
size={ButtonSize.Small}
action={ButtonAction.Primary}
onClick={() => dispatch(openDialog({ type: DialogTypes.Transfer }))}
>
{stringGetter({ key: STRING_KEYS.TRANSFER })}
</Button>
</>
)} )}
</Styled.ReceiveAndTransferButtons> </Styled.ReceiveAndTransferButtons>
</Styled.Header> </Styled.Header>
-4
View File
@@ -18,9 +18,5 @@ export const tradeViewMixins: Record<
tbody { tbody {
font: var(--font-small-book); font: var(--font-small-book);
} }
thead tr {
box-shadow: none;
}
`, `,
}; };
+5 -5
View File
@@ -82,18 +82,18 @@ const MarketsDropdownContent = ({ onRowAction }: { onRowAction?: (market: string
), ),
}, },
{ {
columnKey: 'priceChange24H', columnKey: 'priceChange24HPercent',
getCellValue: (row) => row.priceChange24H, getCellValue: (row) => row.priceChange24HPercent,
label: stringGetter({ key: STRING_KEYS._24H }), label: stringGetter({ key: STRING_KEYS._24H }),
renderCell: ({ priceChange24H, priceChange24HPercent }) => ( renderCell: ({ priceChange24HPercent }) => (
<Styled.InlineRow> <Styled.InlineRow>
{!priceChange24H ? ( {!priceChange24HPercent ? (
<Styled.Output type={OutputType.Text} value={null} /> <Styled.Output type={OutputType.Text} value={null} />
) : ( ) : (
<Styled.PriceChangeOutput <Styled.PriceChangeOutput
type={OutputType.Percent} type={OutputType.Percent}
value={priceChange24HPercent} value={priceChange24HPercent}
isNegative={MustBigNumber(priceChange24H).isNegative()} isNegative={MustBigNumber(priceChange24HPercent).isNegative()}
/> />
)} )}
</Styled.InlineRow> </Styled.InlineRow>
+7 -2
View File
@@ -45,7 +45,12 @@ export const TransferStatusSteps = ({ status, type }: ElementProps) => {
type === 'deposit' ? STRING_KEYS.INITIATED_DEPOSIT : STRING_KEYS.INITIATED_WITHDRAWAL, type === 'deposit' ? STRING_KEYS.INITIATED_DEPOSIT : STRING_KEYS.INITIATED_WITHDRAWAL,
}), }),
step: TransferStatusStep.FromChain, step: TransferStatusStep.FromChain,
link: status?.fromChain?.transactionUrl, link:
type === 'deposit'
? status?.fromChain?.transactionUrl
: routeStatus?.[0]?.chainId === dydxChainId && routeStatus[0].txHash
? `${mintscanTxUrl?.replace('{tx_hash}', routeStatus[0].txHash.replace('0x', ''))}`
: undefined,
}, },
{ {
label: stringGetter({ key: STRING_KEYS.BRIDGING_TOKENS }), label: stringGetter({ key: STRING_KEYS.BRIDGING_TOKENS }),
@@ -64,7 +69,7 @@ export const TransferStatusSteps = ({ status, type }: ElementProps) => {
type === 'withdrawal' type === 'withdrawal'
? status?.toChain?.transactionUrl ? status?.toChain?.transactionUrl
: currentStatus?.chainId === dydxChainId && currentStatus?.txHash : currentStatus?.chainId === dydxChainId && currentStatus?.txHash
? `${mintscanTxUrl?.replace('{tx_hash}', currentStatus.txHash)}` ? `${mintscanTxUrl?.replace('{tx_hash}', currentStatus.txHash.replace('0x', ''))}`
: undefined, : undefined,
}, },
]; ];
+18 -11
View File
@@ -3,6 +3,7 @@ import styled, { AnyStyledComponent, css } from 'styled-components';
import { AnalyticsEvent } from '@/constants/analytics'; import { AnalyticsEvent } from '@/constants/analytics';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { isMainnet } from '@/constants/networks';
import { EvmDerivedAccountStatus, OnboardingSteps } from '@/constants/account'; import { EvmDerivedAccountStatus, OnboardingSteps } from '@/constants/account';
import { wallets } from '@/constants/wallets'; import { wallets } from '@/constants/wallets';
@@ -26,6 +27,7 @@ import { track } from '@/lib/analytics';
import { AcknowledgeTerms } from './OnboardingDialog/AcknowledgeTerms'; import { AcknowledgeTerms } from './OnboardingDialog/AcknowledgeTerms';
import { ChooseWallet } from './OnboardingDialog/ChooseWallet'; import { ChooseWallet } from './OnboardingDialog/ChooseWallet';
import { GenerateKeys } from './OnboardingDialog/GenerateKeys'; import { GenerateKeys } from './OnboardingDialog/GenerateKeys';
import { DepositForm } from '../forms/AccountManagementForms/DepositForm';
type ElementProps = { type ElementProps = {
setIsOpen?: (open: boolean) => void; setIsOpen?: (open: boolean) => void;
@@ -50,7 +52,7 @@ export const OnboardingDialog = ({ setIsOpen }: ElementProps) => {
disconnect(); disconnect();
} }
setIsOpen?.(open); setIsOpen?.(open);
} };
return ( return (
<Styled.Dialog <Styled.Dialog
@@ -82,10 +84,7 @@ export const OnboardingDialog = ({ setIsOpen }: ElementProps) => {
description: stringGetter({ key: STRING_KEYS.SIGNATURE_CREATES_COSMOS_WALLET }), description: stringGetter({ key: STRING_KEYS.SIGNATURE_CREATES_COSMOS_WALLET }),
children: ( children: (
<Styled.Content> <Styled.Content>
<GenerateKeys <GenerateKeys status={derivationStatus} setStatus={setDerivationStatus} />
status={derivationStatus}
setStatus={setDerivationStatus}
/>
</Styled.Content> </Styled.Content>
), ),
width: '23rem', width: '23rem',
@@ -101,14 +100,22 @@ export const OnboardingDialog = ({ setIsOpen }: ElementProps) => {
}, },
[OnboardingSteps.DepositFunds]: { [OnboardingSteps.DepositFunds]: {
title: stringGetter({ key: STRING_KEYS.DEPOSIT }), title: stringGetter({ key: STRING_KEYS.DEPOSIT }),
description: 'Test funds will be sent directly to your dYdX account.', description: !isMainnet && 'Test funds will be sent directly to your dYdX account.',
children: ( children: (
<Styled.Content> <Styled.Content>
<TestnetDepositForm {isMainnet ? (
onDeposit={() => { <DepositForm
track(AnalyticsEvent.TransferFaucet); onDeposit={() => {
}} track(AnalyticsEvent.TransferDeposit);
/> }}
/>
) : (
<TestnetDepositForm
onDeposit={() => {
track(AnalyticsEvent.TransferFaucet);
}}
/>
)}
</Styled.Content> </Styled.Content>
), ),
}, },
@@ -1,11 +1,13 @@
import styled, { AnyStyledComponent } from 'styled-components'; import styled, { AnyStyledComponent } from 'styled-components';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { isDev } from '@/constants/networks';
import { useStringGetter } from '@/hooks'; import { useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
import { Dialog } from '@/components/Dialog'; import { Dialog } from '@/components/Dialog';
import { Icon, IconName } from '@/components/Icon'; import { Icon, IconName } from '@/components/Icon';
import { NetworkSelectMenu } from '@/views/menus/NetworkSelectMenu';
type ElementProps = { type ElementProps = {
preventClose?: boolean; preventClose?: boolean;
@@ -25,6 +27,7 @@ export const RestrictedGeoDialog = ({ preventClose, setIsOpen }: ElementProps) =
> >
<Styled.Content> <Styled.Content>
{stringGetter({ key: STRING_KEYS.REGION_NOT_PERMITTED_SUBTITLE })} {stringGetter({ key: STRING_KEYS.REGION_NOT_PERMITTED_SUBTITLE })}
{isDev && <NetworkSelectMenu />}
</Styled.Content> </Styled.Content>
</Dialog> </Dialog>
); );
@@ -1,11 +1,13 @@
import styled, { AnyStyledComponent } from 'styled-components'; import styled, { AnyStyledComponent } from 'styled-components';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { isDev } from '@/constants/networks';
import { useStringGetter } from '@/hooks'; import { useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
import { Dialog } from '@/components/Dialog'; import { Dialog } from '@/components/Dialog';
import { Icon, IconName } from '@/components/Icon'; import { Icon, IconName } from '@/components/Icon';
import { NetworkSelectMenu } from '@/views/menus/NetworkSelectMenu';
type ElementProps = { type ElementProps = {
preventClose?: boolean; preventClose?: boolean;
@@ -25,6 +27,7 @@ export const RestrictedWalletDialog = ({ preventClose, setIsOpen }: ElementProps
> >
<Styled.Content> <Styled.Content>
{stringGetter({ key: STRING_KEYS.REGION_NOT_PERMITTED_SUBTITLE })} {stringGetter({ key: STRING_KEYS.REGION_NOT_PERMITTED_SUBTITLE })}
{isDev && <NetworkSelectMenu />}
</Styled.Content> </Styled.Content>
</Dialog> </Dialog>
); );
@@ -2,7 +2,7 @@ import { type FormEvent, useCallback, useEffect, useMemo, useState } from 'react
import styled, { type AnyStyledComponent } from 'styled-components'; import styled, { type AnyStyledComponent } from 'styled-components';
import { type NumberFormatValues } from 'react-number-format'; import { type NumberFormatValues } from 'react-number-format';
import { shallowEqual, useSelector } from 'react-redux'; import { shallowEqual, useSelector } from 'react-redux';
import { parseUnits } from 'viem' import { parseUnits } from 'viem';
import erc20 from '@/abi/erc20.json'; import erc20 from '@/abi/erc20.json';
import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus'; import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus';
@@ -65,6 +65,8 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
chain: chainIdStr, chain: chainIdStr,
resources, resources,
summary, summary,
errors: routeErrors,
errorMessage: routeErrorMessage,
} = useSelector(getTransferInputs, shallowEqual) || {}; } = useSelector(getTransferInputs, shallowEqual) || {};
const chainId = chainIdStr ? parseInt(chainIdStr) : undefined; const chainId = chainIdStr ? parseInt(chainIdStr) : undefined;
@@ -97,7 +99,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
useEffect(() => { useEffect(() => {
const hasInvalidInput = const hasInvalidInput =
debouncedAmountBN.isNaN() || debouncedAmountBN.lte(0) || debouncedAmountBN.gte(balanceBN); debouncedAmountBN.isNaN() || debouncedAmountBN.lte(0) || debouncedAmountBN.gt(balanceBN);
abacusStateManager.setTransferValue({ abacusStateManager.setTransferValue({
value: hasInvalidInput ? 0 : debouncedAmount, value: hasInvalidInput ? 0 : debouncedAmount,
@@ -171,7 +173,8 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const validateTokenApproval = useCallback(async () => { const validateTokenApproval = useCallback(async () => {
if (!signerWagmi || !publicClientWagmi) throw new Error('Missing signer'); if (!signerWagmi || !publicClientWagmi) throw new Error('Missing signer');
if (!sourceToken?.address || !sourceToken.decimals) throw new Error('Missing source token address'); if (!sourceToken?.address || !sourceToken.decimals)
throw new Error('Missing source token address');
if (!sourceChain?.rpc) throw new Error('Missing source chain rpc'); if (!sourceChain?.rpc) throw new Error('Missing source chain rpc');
if (!requestPayload?.targetAddress) throw new Error('Missing target address'); if (!requestPayload?.targetAddress) throw new Error('Missing target address');
if (!requestPayload?.value) throw new Error('Missing transaction value'); if (!requestPayload?.value) throw new Error('Missing transaction value');
@@ -181,7 +184,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
address: sourceToken.address as EvmAddress, address: sourceToken.address as EvmAddress,
abi: erc20, abi: erc20,
functionName: 'allowance', functionName: 'allowance',
args: [evmAddress as EvmAddress, requestPayload.targetAddress as EvmAddress] args: [evmAddress as EvmAddress, requestPayload.targetAddress as EvmAddress],
}); });
const sourceAmountBN = parseUnits(debouncedAmount, sourceToken.decimals); const sourceAmountBN = parseUnits(debouncedAmount, sourceToken.decimals);
@@ -193,12 +196,12 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
abi: erc20, abi: erc20,
functionName: 'approve', functionName: 'approve',
args: [requestPayload.targetAddress as EvmAddress, sourceAmountBN], args: [requestPayload.targetAddress as EvmAddress, sourceAmountBN],
}) });
const approveTx = await signerWagmi.writeContract(request); const approveTx = await signerWagmi.writeContract(request);
await publicClientWagmi.waitForTransactionReceipt({ await publicClientWagmi.waitForTransactionReceipt({
hash: approveTx, hash: approveTx,
}) });
} }
}, [signerWagmi, sourceToken, sourceChain, requestPayload, publicClientWagmi]); }, [signerWagmi, sourceToken, sourceChain, requestPayload, publicClientWagmi]);
@@ -228,8 +231,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
to: requestPayload.targetAddress as EvmAddress, to: requestPayload.targetAddress as EvmAddress,
data: requestPayload.data as EvmAddress, data: requestPayload.data as EvmAddress,
gasLimit: BigInt(requestPayload.gasLimit), gasLimit: BigInt(requestPayload.gasLimit),
value: value: requestPayload.routeType !== 'SEND' ? BigInt(requestPayload.value) : undefined,
requestPayload.routeType !== 'SEND' ? BigInt(requestPayload.value) : undefined,
}; };
const txHash = await signerWagmi.sendTransaction(tx); const txHash = await signerWagmi.sendTransaction(tx);
@@ -287,6 +289,15 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
return parseWalletError({ error, stringGetter }).message; return parseWalletError({ error, stringGetter }).message;
} }
if (routeErrors) {
return routeErrorMessage
? stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: { ERROR_MESSAGE: routeErrorMessage },
})
: stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG });
}
if (fromAmount) { if (fromAmount) {
if (!chainId) { if (!chainId) {
return stringGetter({ key: STRING_KEYS.MUST_SPECIFY_CHAIN }); return stringGetter({ key: STRING_KEYS.MUST_SPECIFY_CHAIN });
@@ -300,7 +311,16 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
} }
return undefined; return undefined;
}, [error, balance, chainId, fromAmount, sourceToken]); }, [
error,
routeErrors,
routeErrorMessage,
balance,
chainId,
fromAmount,
sourceToken,
stringGetter,
]);
const isDisabled = const isDisabled =
Boolean(errorMessage) || Boolean(errorMessage) ||
@@ -164,6 +164,7 @@ export const DepositButtonAndReceipt = ({
label: <span>{stringGetter({ key: STRING_KEYS.SLIPPAGE })}</span>, label: <span>{stringGetter({ key: STRING_KEYS.SLIPPAGE })}</span>,
value: ( value: (
<SlippageEditor <SlippageEditor
disabled
slippage={slippage} slippage={slippage}
setIsEditing={setIsEditingSlipapge} setIsEditing={setIsEditingSlipapge}
setSlippage={setSlippage} setSlippage={setSlippage}
@@ -24,11 +24,17 @@ type ElementProps = {
slippage: number; slippage: number;
setIsEditing?: Dispatch<SetStateAction<boolean>>; setIsEditing?: Dispatch<SetStateAction<boolean>>;
setSlippage: (slippage: number) => void; setSlippage: (slippage: number) => void;
disabled?: boolean;
}; };
export type SlippageEditorProps = ElementProps; export type SlippageEditorProps = ElementProps;
export const SlippageEditor = ({ slippage, setIsEditing, setSlippage }: SlippageEditorProps) => { export const SlippageEditor = ({
disabled,
slippage,
setIsEditing,
setSlippage,
}: SlippageEditorProps) => {
const percentSlippage = slippage * 100; const percentSlippage = slippage * 100;
const [slippageInputValue, setSlippageInputValue] = useState(percentSlippage.toString()); const [slippageInputValue, setSlippageInputValue] = useState(percentSlippage.toString());
const [editorState, setEditorState] = useState(EditorState.Viewing); const [editorState, setEditorState] = useState(EditorState.Viewing);
@@ -80,6 +86,10 @@ export const SlippageEditor = ({ slippage, setIsEditing, setSlippage }: Slippage
} }
}; };
if (disabled) {
return <Output type={OutputType.Percent} value={slippage} />;
}
return ( return (
<Styled.WithConfirmationPopover <Styled.WithConfirmationPopover
open={editorState !== EditorState.Viewing} open={editorState !== EditorState.Viewing}
@@ -47,7 +47,6 @@ import { MustBigNumber } from '@/lib/numbers';
import { TokenSelectMenu } from './TokenSelectMenu'; import { TokenSelectMenu } from './TokenSelectMenu';
import { WithdrawButtonAndReceipt } from './WithdrawForm/WithdrawButtonAndReceipt'; import { WithdrawButtonAndReceipt } from './WithdrawForm/WithdrawButtonAndReceipt';
import { join } from 'path';
export const WithdrawForm = () => { export const WithdrawForm = () => {
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
@@ -69,6 +68,8 @@ export const WithdrawForm = () => {
chain: chainIdStr, chain: chainIdStr,
address: toAddress, address: toAddress,
resources, resources,
errors: routeErrors,
errorMessage: routeErrorMessage,
} = useSelector(getTransferInputs, shallowEqual) || {}; } = useSelector(getTransferInputs, shallowEqual) || {};
const isValidAddress = toAddress && isAddress(toAddress); const isValidAddress = toAddress && isAddress(toAddress);
@@ -281,6 +282,15 @@ export const WithdrawForm = () => {
}); });
} }
if (routeErrors) {
return routeErrorMessage
? stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: { ERROR_MESSAGE: routeErrorMessage },
})
: stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG });
}
if (!toAddress) return stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_ADDRESS }); if (!toAddress) return stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_ADDRESS });
if (sanctionedAddresses.has(toAddress)) if (sanctionedAddresses.has(toAddress))
@@ -303,12 +313,15 @@ export const WithdrawForm = () => {
return undefined; return undefined;
}, [ }, [
error, error,
routeErrors,
routeErrorMessage,
freeCollateralBN, freeCollateralBN,
chainIdStr, chainIdStr,
debouncedAmountBN, debouncedAmountBN,
toToken, toToken,
toAddress, toAddress,
sanctionedAddresses, sanctionedAddresses,
stringGetter,
]); ]);
const isDisabled = const isDisabled =
@@ -147,6 +147,7 @@ export const WithdrawButtonAndReceipt = ({
label: <span>{stringGetter({ key: STRING_KEYS.SLIPPAGE })}</span>, label: <span>{stringGetter({ key: STRING_KEYS.SLIPPAGE })}</span>,
value: ( value: (
<SlippageEditor <SlippageEditor
disabled
slippage={slippage} slippage={slippage}
setIsEditing={setIsEditingSlipapge} setIsEditing={setIsEditingSlipapge}
setSlippage={setSlippage} setSlippage={setSlippage}
+2
View File
@@ -33,6 +33,8 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.DropdownSelectMenu = styled(DropdownSelectMenu)` Styled.DropdownSelectMenu = styled(DropdownSelectMenu)`
${headerMixins.dropdownTrigger} ${headerMixins.dropdownTrigger}
width: max-content;
& > span:first-of-type { & > span:first-of-type {
${layoutMixins.textOverflow} ${layoutMixins.textOverflow}
max-width: 5.625rem; max-width: 5.625rem;