Compare commits

..
Author SHA1 Message Date
Bill He 2d31d46dbd polish 2023-08-29 12:01:43 -07:00
Bill He a4a84bf2a1 polish 2023-08-29 11:36:22 -07:00
Bill He 93c2f6be67 localize 2023-08-29 11:29:06 -07:00
Bill He 048527989c polish 2023-08-29 11:21:37 -07:00
Bill He e9f9b7bd6b misc 2023-08-29 11:10:31 -07:00
Bill He e0bb2046cc remove abacus transfer status 2023-08-29 10:57:59 -07:00
Bill He a8ffd1358f use react context 2023-08-29 10:51:05 -07:00
Bill He cb0bd0f39a move out transfer status hook 2023-08-29 10:11:49 -07:00
Bill He 765f39ec5e Deposit/Withdraw Status Notifications 2023-08-28 23:57:01 -07:00
Bill He e656c219cd temp 2023-08-28 15:32:41 -07:00
Bill He e2596b0654 use diffarrow 2023-08-28 15:22:48 -07:00
Bill He 06734a2f38 fix clear bug 2023-08-28 15:17:18 -07:00
Bill He b1a3fb6dc1 address comments 2023-08-28 11:15:47 -07:00
Bill He de08098409 address comments 2023-08-28 11:15:47 -07:00
Bill He d38d449b58 address comments 2023-08-28 11:15:46 -07:00
Bill He 924c56d4f1 fix withdraw chain select 2023-08-28 11:15:46 -07:00
Bill He 0d8f3b4922 working switch network deposits 2023-08-28 11:15:46 -07:00
Bill He ad9e144d40 fix switch network bug 2023-08-28 11:15:46 -07:00
Bill He 78c0134daa fix chain select menu 2023-08-28 11:15:46 -07:00
Bill He cd977ab955 remove unused types 2023-08-28 11:15:46 -07:00
Bill He 196185aa7e deprecate usesquidrouter 2023-08-28 11:15:45 -07:00
Bill He 5093823b25 e2e working deposit/withdraws 2023-08-28 11:15:43 -07:00
Bill He 7d7adc8da2 revert package 2023-08-28 11:14:20 -07:00
Bill He aee06a2c55 Deposit/Withdraw with abacus 2023-08-28 11:13:18 -07:00
Jared Vu d6baa6ef3f Merge pull request #5 from dydxprotocol/conditional-orders
Conditional orders
2023-08-28 05:49:27 -10:00
jaredvu 195ddd5af7 Move localeProvider, add label, valid marketId 2023-08-28 08:43:36 -07:00
aleka 2a5373c972 Merge pull request #4 from dydxprotocol/transfers-history-table
[TRCL-1842] Transfers history table
2023-08-25 14:13:53 -04:00
Aleka Cheung 64f663ed5b add CopyButton component 2023-08-25 14:07:03 -04:00
jaredvu 983128fd74 misc fix/cleanup 2023-08-25 10:59:59 -07:00
jaredvu e15f4b919c useCurrentMarketId hook for navigation 2023-08-25 10:59:35 -07:00
jaredvu 71c29524b1 useMarketsData: Filter out test-usd market 2023-08-24 14:23:02 -07:00
jaredvu 0e26433e29 disable conditional orders on testnet until they support them 2023-08-24 14:18:55 -07:00
jaredvu 3afd5ef783 OrdersTable: Minimum stepSizeDecimals = TOKEN_DECIMALS 2023-08-24 13:35:38 -07:00
jaredvu b2cac81235 SearchSelect: DropdownIcon position 2023-08-24 13:27:40 -07:00
jaredvu e48ec4ca73 Fix Orderbook spread when nothing on the books 2023-08-24 12:06:03 -07:00
jaredvu 78f9ddff7a Fix crash on test coin 2023-08-24 12:05:47 -07:00
jaredvu c1dcca43c8 Render TradeBoxOrderView from Abacus typeOptions 2023-08-23 23:32:02 -07:00
jaredvu 029476fa19 fix type for placeOrder analytics 2023-08-23 17:31:14 -07:00
jaredvu 6ecdcd981e enable conditional orders 2023-08-23 17:18:40 -07:00
jaredvu 591166e93d locale/browserTag 2023-08-23 17:18:10 -07:00
Aleka Cheung e30940e7df bump abacus + make addresses copyable 2023-08-23 19:37:19 -04:00
Aleka Cheung 2d0a944097 transfers history table 2023-08-23 19:37:14 -04:00
aleka 0dd2ecb6d2 Merge pull request #1 from dydxprotocol/receive-transfer-entry-in-account-menu
Add receive/transfer dialog entry points in AccountMenu + dv4tnt renames
2023-08-23 18:57:32 -04:00
Aleka Cheung 76819e0839 signed commits 2023-08-23 18:56:12 -04:00
Aleka Cheung 32b375cfa4 address feedback 2023-08-23 18:56:11 -04:00
Aleka Cheung 3dc87e873d show receive button for both assets 2023-08-23 18:56:10 -04:00
Aleka Cheung ed61b3112e rename tokens to dv4tnt, fix header dropdown hover state, add entry points to receive/send dialogs in account menu 2023-08-23 18:56:08 -04:00
98 changed files with 3065 additions and 3065 deletions
+2 -2
View File
@@ -29,13 +29,13 @@
},
"packageManager": "pnpm@8.6.6",
"dependencies": {
"@0xsquid/sdk": "^1.7.2",
"@0xsquid/sdk": "^1.10.0",
"@cosmjs/amino": "^0.31.0",
"@cosmjs/crypto": "^0.31.0",
"@cosmjs/encoding": "^0.31.0",
"@cosmjs/proto-signing": "^0.31.0",
"@cosmjs/stargate": "^0.31.0",
"@dydxprotocol/abacus": "file:/Users/jaredvu/Documents/abacus/build/packages/dydxprotocol-abacus-0.2.28.tgz",
"@dydxprotocol/abacus": "^0.2.37",
"@cosmjs/tendermint-rpc": "^0.31.0",
"@dydxprotocol/v4-client": "^0.29.0",
"@ethersproject/providers": "^5.7.2",
+202 -1767
View File
File diff suppressed because it is too large Load Diff
+30 -24
View File
@@ -10,9 +10,12 @@ import { AppRoute, DEFAULT_TRADE_ROUTE } from '@/constants/routes';
import { useBreakpoints, useInitializePage, useShouldShowFooter, useAnalytics } from '@/hooks';
import { DydxProvider } from '@/hooks/useDydxClient';
import { AccountsProvider } from '@/hooks/useAccounts';
import { SubaccountProvider } from './hooks/useSubaccount';
import { DialogAreaProvider, useDialogArea } from './hooks/useDialogArea';
import { LocaleProvider } from './hooks/useLocaleSeparators';
import { NotificationsProvider } from './hooks/useNotifications';
import { LocalNotificationsProvider } from './hooks/useLocalNotifications';
import { SubaccountProvider } from './hooks/useSubaccount';
import { SquidProvider } from '@/hooks/useSquid';
import { GuardedMobileRoute } from '@/components/GuardedMobileRoute';
@@ -61,10 +64,7 @@ const Content = () => {
<Routes>
<Route path={AppRoute.Trade}>
<Route path=":market" element={<TradePage />} />
<Route
path={AppRoute.Trade}
element={<Navigate to={DEFAULT_TRADE_ROUTE} replace />}
/>
<Route path={AppRoute.Trade} element={<TradePage />} />
</Route>
<Route path={AppRoute.Markets} element={<MarketsPage />} />
{import.meta.env.MODE !== 'production' && (
@@ -99,25 +99,31 @@ const Content = () => {
);
};
const App = () => (
<QueryClientProvider client={queryClient}>
<GrazProvider>
<WagmiConfig config={config}>
<DydxProvider>
<AccountsProvider>
<SubaccountProvider>
<NotificationsProvider>
<DialogAreaProvider>
<Content />
</DialogAreaProvider>
</NotificationsProvider>
</SubaccountProvider>
</AccountsProvider>
</DydxProvider>
</WagmiConfig>
</GrazProvider>
</QueryClientProvider>
);
const wrapProvider = (Component: React.ComponentType<any>, props?: any) => {
return ({ children }: { children: React.ReactNode }) => (
<Component {...props}>{children}</Component>
);
};
const providers = [
wrapProvider(QueryClientProvider, { client: queryClient }),
wrapProvider(GrazProvider),
wrapProvider(WagmiConfig, { config }),
wrapProvider(LocaleProvider),
wrapProvider(DydxProvider),
wrapProvider(AccountsProvider),
wrapProvider(SubaccountProvider),
wrapProvider(SquidProvider),
wrapProvider(LocalNotificationsProvider),
wrapProvider(NotificationsProvider),
wrapProvider(DialogAreaProvider),
];
const App = () => {
return [...providers].reverse().reduce((children, Provider) => {
return <Provider>{children}</Provider>;
}, <Content />);
};
const Styled: Record<string, AnyStyledComponent> = {};
+49 -54
View File
@@ -1,68 +1,63 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { Nullable } from '@/constants/abacus';
export type AssetSymbol = keyof typeof assetIcons;
const assetIcons = {
'1INCH': '/currencies/1inch.svg',
'AAVE': '/currencies/aave.svg',
'ADA': '/currencies/ada.svg',
'ALGO': '/currencies/algo.svg',
'ATOM': '/currencies/atom.svg',
'AVAX': '/currencies/avax.svg',
'BCH': '/currencies/bch.svg',
'BTC': '/currencies/btc.svg',
'CELO': '/currencies/celo.svg',
'COMP': '/currencies/comp.svg',
'CRV': '/currencies/crv.svg',
'DAI': '/currencies/dai.svg',
'DOGE': '/currencies/doge.svg',
'DOT': '/currencies/dot.svg',
'DYDX': '/currencies/dydx.svg',
'ENJ': '/currencies/enj.svg',
'EOS': '/currencies/eos.svg',
'ETC': '/currencies/etc.svg',
'ETH': '/currencies/eth.svg',
'FIL': '/currencies/fil.svg',
'ICP': '/currencies/icp.svg',
'LINK': '/currencies/link.svg',
'LTC': '/currencies/ltc.svg',
'LUNA': '/currencies/luna.svg',
'MATIC': '/currencies/matic.svg',
'MKR': '/currencies/mkr.svg',
'NEAR': '/currencies/near.svg',
'RUNE': '/currencies/rune.svg',
'SNX': '/currencies/snx.svg',
'SOL': '/currencies/sol.svg',
'SUSHI': '/currencies/sushi.svg',
'TRX': '/currencies/trx.svg',
'UMA': '/currencies/uma.svg',
'UNI': '/currencies/uni.svg',
'USDC': '/currencies/usdc.svg',
'USDT': '/currencies/usdt.svg',
'WBTC': '/currencies/wbtc.svg',
'WETH': '/currencies/weth.svg',
'XLM': '/currencies/xlm.svg',
'XMR': '/currencies/xmr.svg',
'XTZ': '/currencies/xtz.svg',
'YFI': '/currencies/yfi.svg',
'ZEC': '/currencies/zec.svg',
'ZRX': '/currencies/zrx.svg',
AAVE: '/currencies/aave.svg',
ADA: '/currencies/ada.svg',
ALGO: '/currencies/algo.svg',
ATOM: '/currencies/atom.svg',
AVAX: '/currencies/avax.svg',
BCH: '/currencies/bch.svg',
BTC: '/currencies/btc.svg',
CELO: '/currencies/celo.svg',
COMP: '/currencies/comp.svg',
CRV: '/currencies/crv.svg',
DAI: '/currencies/dai.svg',
DOGE: '/currencies/doge.svg',
DOT: '/currencies/dot.svg',
DYDX: '/currencies/dydx.svg',
ENJ: '/currencies/enj.svg',
EOS: '/currencies/eos.svg',
ETC: '/currencies/etc.svg',
ETH: '/currencies/eth.svg',
FIL: '/currencies/fil.svg',
ICP: '/currencies/icp.svg',
LINK: '/currencies/link.svg',
LTC: '/currencies/ltc.svg',
LUNA: '/currencies/luna.svg',
MATIC: '/currencies/matic.svg',
MKR: '/currencies/mkr.svg',
NEAR: '/currencies/near.svg',
RUNE: '/currencies/rune.svg',
SNX: '/currencies/snx.svg',
SOL: '/currencies/sol.svg',
SUSHI: '/currencies/sushi.svg',
TRX: '/currencies/trx.svg',
UMA: '/currencies/uma.svg',
UNI: '/currencies/uni.svg',
USDC: '/currencies/usdc.svg',
USDT: '/currencies/usdt.svg',
WBTC: '/currencies/wbtc.svg',
WETH: '/currencies/weth.svg',
XLM: '/currencies/xlm.svg',
XMR: '/currencies/xmr.svg',
XTZ: '/currencies/xtz.svg',
YFI: '/currencies/yfi.svg',
ZEC: '/currencies/zec.svg',
ZRX: '/currencies/zrx.svg',
} as const;
export const AssetIcon = ({
symbol,
className
className,
}: {
symbol?: AssetSymbol,
className?: string
}) => (
symbol ? (
<Styled.Img
src={assetIcons[symbol]}
className={className}
/>
) : null
);
symbol?: Nullable<AssetSymbol>;
className?: string;
}) => (symbol ? <Styled.Img src={assetIcons[symbol]} className={className} /> : null);
const Styled: Record<string, AnyStyledComponent> = {};
+28
View File
@@ -0,0 +1,28 @@
import type { Story } from '@ladle/react';
import { CopyButton, type CopyButtonProps } from '@/components/CopyButton';
import { StoryWrapper } from '.ladle/components';
export const CopyButtonStory: Story<CopyButtonProps> = (args) => (
<StoryWrapper>
<CopyButton {...args} />
</StoryWrapper>
);
CopyButtonStory.args = {
value: 'some text to copy',
};
CopyButtonStory.argTypes = {
shownAsText: {
options: [true, false],
control: { type: 'select' },
defaultValue: false,
},
children: {
options: ['some text to copy'],
control: { type: 'select' },
defaultValue: undefined,
}
};
+65
View File
@@ -0,0 +1,65 @@
import { useState } from 'react';
import styled, { css, type AnyStyledComponent } from 'styled-components';
import { ButtonAction } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins';
import { Button, ButtonProps } from './Button';
import { Icon, IconName } from './Icon';
export type CopyButtonProps = {
value?: string;
shownAsText?: boolean;
children?: React.ReactNode;
} & ButtonProps;
export const CopyButton = ({ value, shownAsText, children, ...buttonProps }: CopyButtonProps) => {
const stringGetter = useStringGetter();
const [copied, setCopied] = useState(false);
const onCopy = () => {
if (!value) return;
setCopied(true);
navigator.clipboard.writeText(value);
setTimeout(() => setCopied(false), 500);
};
return shownAsText ? (
<Styled.InlineRow onClick={onCopy} copied={copied}>
{children}
<Icon iconName={IconName.Copy} />
</Styled.InlineRow>
) : (
<Button
{...buttonProps}
action={copied ? ButtonAction.Create : ButtonAction.Primary}
onClick={onCopy}
>
<Icon iconName={IconName.Copy} />
{children ?? stringGetter({ key: copied ? STRING_KEYS.COPIED : STRING_KEYS.COPY })}
</Button>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.InlineRow = styled.div<{ copied: boolean }>`
${layoutMixins.inlineRow}
cursor: pointer;
${({ copied }) =>
copied
? css`
filter: brightness(0.8);
`
: css`
&:hover {
filter: brightness(1.1);
text-decoration: underline;
}
`}
`;
+8 -7
View File
@@ -19,6 +19,7 @@ type ElementProps = {
disabled?: boolean;
label?: string;
items: MenuConfig<string, string>;
slotEmpty?: ReactNode;
withSearch?: boolean;
withReceiptItems?: DetailsItem[];
};
@@ -52,12 +53,10 @@ export const SearchSelectMenu = ({
const Trigger = asChild ? (
children
) : (
<>
<Styled.MenuTrigger>
<Styled.WithLabel label={label}>{children}</Styled.WithLabel>
</Styled.MenuTrigger>
<Styled.MenuTrigger>
{label ? <Styled.WithLabel label={label}>{children}</Styled.WithLabel> : children}
<Styled.TriggerIcon iconName={IconName.Triangle} open={open} />
</>
</Styled.MenuTrigger>
);
return (
@@ -86,7 +85,7 @@ export const SearchSelectMenu = ({
const Styled: Record<string, AnyStyledComponent> = {};
Styled.MenuTrigger = styled.div`
height: 3.5rem;
height: var(--form-input-height);
${layoutMixins.spacedRow}
align-items: center;
@@ -117,7 +116,7 @@ Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
Styled.Popover = styled(Popover)`
max-height: 30vh;
margin-top: 1rem;
border: 1px solid var(--color-layer-6);
border: var(--border-width) solid var(--color-layer-6);
border-radius: 0.5rem;
z-index: 2;
box-shadow: none;
@@ -136,6 +135,8 @@ Styled.ComboboxMenu = styled(ComboboxMenu)`
--comboboxMenu-item-checked-textColor: var(--color-text-2);
--comboboxMenu-item-highlighted-textColor: var(--color-text-2);
--stickyArea1-topHeight: var(--form-input-height);
input:focus {
outline: none;
}
+1
View File
@@ -91,6 +91,7 @@ Styled.Trigger = styled(Trigger)<{ $withBlur?: boolean }>`
Styled.DropdownIcon = styled(SelectIcon)`
font-size: 0.675em;
color: var(--color-text-1);
margin-left: auto;
`;
Styled.Content = styled(Content)`
+12 -10
View File
@@ -17,6 +17,7 @@ type ElementProps = {
slotIcon?: React.ReactNode;
slotTitle?: React.ReactNode;
slotDescription?: React.ReactNode;
slotCustomContent?: React.ReactNode;
slotAction?: React.ReactNode;
actionDescription?: string;
actionAltText?: string;
@@ -33,6 +34,7 @@ export const Toast = ({
slotIcon,
slotTitle,
slotDescription,
slotCustomContent,
slotAction,
actionDescription = '',
actionAltText = actionDescription,
@@ -78,12 +80,13 @@ export const Toast = ({
<$Title>{slotTitle}</$Title>
</$Header>
<$Description>{slotDescription}</$Description>
<$Action asChild altText={actionAltText}>
{slotAction}
</$Action>
{!slotCustomContent && <$Description>{slotDescription}</$Description>}
{slotCustomContent}
{actionDescription && (
<$Action asChild altText={actionAltText}>
{slotAction}
</$Action>
)}
</$Container>
</div>
</$Root>
@@ -118,8 +121,7 @@ const $Root = styled(Root)`
align-items: end;
transform-origin: left bottom;
animation:
${keyframes`
animation: ${keyframes`
from {
/* scale: 0; */
grid-template-rows: 0fr; // height transition
@@ -203,8 +205,8 @@ const $Container = styled.div`
// Rules
${popoverMixins.popover}
padding: 1rem;
box-shadow:
0 0 0 var(--border-width) var(--color-border), // border
box-shadow: 0 0 0 var(--border-width) var(--color-border),
// border
0 0 0.5rem 0.1rem var(--color-layer-2); // shadow
${$Root}:focus:not([data-swipe='end']) & {
+23 -1
View File
@@ -1,6 +1,6 @@
import Abacus, { kollections } from '@dydxprotocol/abacus';
import { OrderSide } from '@dydxprotocol/v4-client';
import { PositionSide } from './trade';
import { PositionSide, TradeTypes } from './trade';
import { STRING_KEYS } from './localization';
export type Nullable<T> = T | null | undefined;
@@ -98,7 +98,11 @@ export type ClosePositionInputs = Abacus.exchange.dydx.abacus.output.input.Close
export type TradeInputSummary = Abacus.exchange.dydx.abacus.output.input.TradeInputSummary;
export type TransferInputs = Abacus.exchange.dydx.abacus.output.input.TransferInput;
export type InputError = Abacus.exchange.dydx.abacus.output.input.ValidationError;
export type TransferInputTokenResource = Abacus.exchange.dydx.abacus.output.input.TransferInputTokenResource;
export type TransferInputChainResource = Abacus.exchange.dydx.abacus.output.input.TransferInputChainResource;
export type SelectionOption = Abacus.exchange.dydx.abacus.output.input.SelectionOption;
export const ErrorType = Abacus.exchange.dydx.abacus.output.input.ErrorType;
export const InputSelectionOption = Abacus.exchange.dydx.abacus.output.input.SelectionOption;
// ------ Wallet ------ //
export type Wallet = Abacus.exchange.dydx.abacus.output.Wallet;
@@ -112,6 +116,8 @@ export type SubaccountFill = Abacus.exchange.dydx.abacus.output.SubaccountFill;
export type SubaccountFundingPayment = Abacus.exchange.dydx.abacus.output.SubaccountFundingPayment;
export type SubaccountFundingPayments =
Abacus.exchange.dydx.abacus.output.SubaccountFundingPayment[];
export type SubaccountTransfer = Abacus.exchange.dydx.abacus.output.SubaccountTransfer;
export type SubaccountTransfers = Abacus.exchange.dydx.abacus.output.SubaccountTransfer[];
// ------ Historical PnL ------ //
export type SubAccountHistoricalPNL = Abacus.exchange.dydx.abacus.output.SubaccountHistoricalPNL;
@@ -215,3 +221,19 @@ export const ORDER_STATUS_STRINGS: Record<KotlinIrEnumValues<typeof AbacusOrderS
[AbacusOrderStatus.pending.name]: STRING_KEYS.PENDING,
[AbacusOrderStatus.untriggered.name]: STRING_KEYS.UNTRIGGERED,
};
export const TRADE_TYPES: Record<
KotlinIrEnumValues<typeof AbacusOrderType>,
Nullable<TradeTypes>
> = {
[AbacusOrderType.limit.name]: TradeTypes.LIMIT,
[AbacusOrderType.market.name]: TradeTypes.MARKET,
[AbacusOrderType.stopLimit.name]: TradeTypes.STOP_LIMIT,
[AbacusOrderType.stopMarket.name]: TradeTypes.STOP_MARKET,
[AbacusOrderType.takeProfitLimit.name]: TradeTypes.TAKE_PROFIT,
[AbacusOrderType.takeProfitMarket.name]: TradeTypes.TAKE_PROFIT_MARKET,
[AbacusOrderType.liquidated.name]: null,
[AbacusOrderType.liquidation.name]: null,
[AbacusOrderType.trailingStop.name]: null,
};
+100 -115
View File
@@ -4,7 +4,7 @@ import type { OnboardingState, OnboardingSteps } from './account';
import type { DydxAddress, WalletType, WalletConnectionType, EthereumAddress } from './wallets';
import type { DialogTypes } from './dialogs';
import type { TradeTypes } from './trade';
import type { AbacusApiStatus, SubaccountPlaceOrderPayload } from './abacus';
import type { AbacusApiStatus, HumanReadablePlaceOrderPayload } from './abacus';
// User properties
export enum AnalyticsUserProperty {
@@ -25,37 +25,28 @@ export enum AnalyticsUserProperty {
SubaccountNumber = 'subaccountNumber',
}
export type AnalyticsUserPropertyValue<T extends AnalyticsUserProperty> =
export type AnalyticsUserPropertyValue<T extends AnalyticsUserProperty> =
// Environment
T extends AnalyticsUserProperty.Breakpoint ?
| 'MOBILE'
| 'TABLET'
| 'DESKTOP_SMALL'
| 'DESKTOP_MEDIUM'
| 'DESKTOP_LARGE'
| 'UNSUPPORTED'
: T extends AnalyticsUserProperty.Locale ?
SupportedLocales
// Network
: T extends AnalyticsUserProperty.Network ?
DydxNetwork
// Wallet
: T extends AnalyticsUserProperty.WalletType ?
WalletType | undefined
: T extends AnalyticsUserProperty.WalletConnectionType ?
WalletConnectionType | undefined
: T extends AnalyticsUserProperty.WalletAddress ?
EthereumAddress | DydxAddress | undefined
// Account
: T extends AnalyticsUserProperty.DydxAddress ?
DydxAddress | undefined
: T extends AnalyticsUserProperty.SubaccountNumber ?
number | undefined
: undefined;
T extends AnalyticsUserProperty.Breakpoint
? 'MOBILE' | 'TABLET' | 'DESKTOP_SMALL' | 'DESKTOP_MEDIUM' | 'DESKTOP_LARGE' | 'UNSUPPORTED'
: T extends AnalyticsUserProperty.Locale
? SupportedLocales
: // Network
T extends AnalyticsUserProperty.Network
? DydxNetwork
: // Wallet
T extends AnalyticsUserProperty.WalletType
? WalletType | undefined
: T extends AnalyticsUserProperty.WalletConnectionType
? WalletConnectionType | undefined
: T extends AnalyticsUserProperty.WalletAddress
? EthereumAddress | DydxAddress | undefined
: // Account
T extends AnalyticsUserProperty.DydxAddress
? DydxAddress | undefined
: T extends AnalyticsUserProperty.SubaccountNumber
? number | undefined
: undefined;
// Events
export enum AnalyticsEvent {
@@ -91,87 +82,81 @@ export enum AnalyticsEvent {
export type AnalyticsEventData<T extends AnalyticsEvent> =
// App
T extends AnalyticsEvent.AppStart ?
{}
: T extends AnalyticsEvent.NetworkStatus ?
{
status: typeof AbacusApiStatus['name'];
/** Last time indexer node was queried successfully */
lastSuccessfulIndexerRpcQuery?: number;
/** Time elapsed since indexer node was queried successfully */
elapsedTime?: number;
blockHeight?: number;
indexerBlockHeight?: number;
}
// Navigation
: T extends AnalyticsEvent.NavigatePage ?
{
path: string;
}
: T extends AnalyticsEvent.NavigateDialog ?
{
type: DialogTypes;
}
: T extends AnalyticsEvent.NavigateDialogClose ?
{
type: DialogTypes;
}
: T extends AnalyticsEvent.NavigateExternal ?
{
link: string;
}
// Wallet
: T extends AnalyticsEvent.ConnectWallet ?
{
walletType: WalletType;
walletConnectionType: WalletConnectionType;
}
: T extends AnalyticsEvent.DisconnectWallet ?
{}
// Onboarding
: T extends AnalyticsEvent.OnboardingStepChanged ?
{
state: OnboardingState;
step?: OnboardingSteps;
}
: T extends AnalyticsEvent.OnboardingAccountDerived ?
{
hasPreviousTransactions: boolean;
}
// Transfers
: T extends AnalyticsEvent.TransferFaucet ?
{}
: T extends AnalyticsEvent.TransferDeposit ?
{}
: T extends AnalyticsEvent.TransferWithdraw ?
{}
// Trading
: T extends AnalyticsEvent.TradeOrderTypeSelected ?
{
type: TradeTypes;
}
: T extends AnalyticsEvent.TradePlaceOrder ?
SubaccountPlaceOrderPayload & {
isClosePosition: boolean;
}
: T extends AnalyticsEvent.TradePlaceOrderConfirmed ?
{
/** roundtrip time between user placing an order and confirmation from indexer (client → validator → indexer → client) */
roundtripMs: number;
/** URL/IP of node the order was sent to */
validator: string;
}
: T extends AnalyticsEvent.TradeCancelOrderConfirmed ?
{
/** roundtrip time between user canceling an order and confirmation from indexer (client → validator → indexer → client) */
roundtripMs: number;
/** URL/IP of node the order was sent to */
validator: string;
}
:
never;
T extends AnalyticsEvent.AppStart
? {}
: T extends AnalyticsEvent.NetworkStatus
? {
status: (typeof AbacusApiStatus)['name'];
/** Last time indexer node was queried successfully */
lastSuccessfulIndexerRpcQuery?: number;
/** Time elapsed since indexer node was queried successfully */
elapsedTime?: number;
blockHeight?: number;
indexerBlockHeight?: number;
}
: // Navigation
T extends AnalyticsEvent.NavigatePage
? {
path: string;
}
: T extends AnalyticsEvent.NavigateDialog
? {
type: DialogTypes;
}
: T extends AnalyticsEvent.NavigateDialogClose
? {
type: DialogTypes;
}
: T extends AnalyticsEvent.NavigateExternal
? {
link: string;
}
: // Wallet
T extends AnalyticsEvent.ConnectWallet
? {
walletType: WalletType;
walletConnectionType: WalletConnectionType;
}
: T extends AnalyticsEvent.DisconnectWallet
? {}
: // Onboarding
T extends AnalyticsEvent.OnboardingStepChanged
? {
state: OnboardingState;
step?: OnboardingSteps;
}
: T extends AnalyticsEvent.OnboardingAccountDerived
? {
hasPreviousTransactions: boolean;
}
: // Transfers
T extends AnalyticsEvent.TransferFaucet
? {}
: T extends AnalyticsEvent.TransferDeposit
? {}
: T extends AnalyticsEvent.TransferWithdraw
? {}
: // Trading
T extends AnalyticsEvent.TradeOrderTypeSelected
? {
type: TradeTypes;
}
: T extends AnalyticsEvent.TradePlaceOrder
? HumanReadablePlaceOrderPayload & {
isClosePosition: boolean;
}
: T extends AnalyticsEvent.TradePlaceOrderConfirmed
? {
/** roundtrip time between user placing an order and confirmation from indexer (client → validator → indexer → client) */
roundtripMs: number;
/** URL/IP of node the order was sent to */
validator: string;
}
: T extends AnalyticsEvent.TradeCancelOrderConfirmed
? {
/** roundtrip time between user canceling an order and confirmation from indexer (client → validator → indexer → client) */
roundtripMs: number;
/** URL/IP of node the order was sent to */
validator: string;
}
: never;
+1
View File
@@ -12,6 +12,7 @@ export enum DialogTypes {
Receive = 'Receive',
Trade = 'Trade',
Transfer = 'Transfer',
Withdraw = 'Withdraw',
}
export enum TradeBoxDialogTypes {
+1
View File
@@ -12,6 +12,7 @@ export enum LocalStorageKey {
NotificationsLastUpdated = 'dydx.NotificationsLastUpdated',
PushNotificationsEnabled = 'dydx.PushNotificationsEnabled',
PushNotificationsLastUpdated = 'dydx.PushNotificationsLastUpdated',
TransferNotifications = 'dydx.TransferNotifications',
// UI State
LastViewedMarket = 'dydx.LastViewedMarket',
+12
View File
@@ -109,6 +109,7 @@ export const APP_STRING_KEYS = {
HEDGIES: 'GENERAL.HEDGIES',
HIDE_SECTION: 'GENERAL.HIDE_SECTION',
HIDE: 'GENERAL.HIDE',
HIDE_DETAILS: 'GENERAL.HIDE_DETAILS',
HISTORY: 'GENERAL.HISTORY',
INCREASED: 'GENERAL.INCREASED',
INCREASING: 'GENERAL.INCREASING',
@@ -177,6 +178,7 @@ export const APP_STRING_KEYS = {
RECEIVE: 'GENERAL.RECEIVE',
RECENT: 'GENERAL.RECENT',
RECENT_TRADES_SHORT: 'GENERAL.RECENT_TRADES_SHORT',
RECIPIENT: 'GENERAL.RECIPIENT',
REFERRAL_CODE: 'GENERAL.REFERRAL_CODE',
REFERRALS: 'GENERAL.REFERRALS',
REFERRER_PERCENT_OFF: 'GENERAL.REFERRER_PERCENT_OFF',
@@ -188,6 +190,7 @@ export const APP_STRING_KEYS = {
SELECT_NETWORK: 'GENERAL.SELECT_NETWORK',
SELL: 'GENERAL.SELL',
SEND: 'GENERAL.SEND',
SENDER: 'GENERAL.SENDER',
SHARE: 'GENERAL.SHARE',
SHORT_POSITION_SHORT: 'GENERAL.SHORT_POSITION_SHORT',
SIDE: 'GENERAL.SIDE',
@@ -381,6 +384,7 @@ export const APP_STRING_KEYS = {
CONNECTING_SUBTITLE: 'ONBOARDING.CONNECTING_SUBTITLE',
COULD_NOT_FIND_AFFILIATE: 'ONBOARDING.COULD_NOT_FIND_AFFILIATE',
CREATE_OR_VERIFY_COSMOS_ADDRESS: 'ONBOARDING.CREATE_OR_VERIFY_COSMOS_ADDRESS',
DEPOSIT_STATUS: 'ONBOARDING.DEPOSIT_STATUS',
DONT_MISS: 'ONBOARDING.DONT_MISS',
ENABLE_API: 'ONBOARDING.ENABLE_API',
ENABLE_TRADING: 'ONBOARDING.ENABLE_TRADING',
@@ -443,6 +447,7 @@ export const APP_STRING_KEYS = {
VERIFY_WALLET_COMPATIBILITY: 'ONBOARDING.VERIFY_WALLET_COMPATIBILITY',
WELCOME: 'ONBOARDING.WELCOME',
WELCOME_DYDX: 'ONBOARDING.WELCOME_DYDX',
WITHDRAW_STATUS: 'ONBOARDING.WITHDRAW_STATUS',
WRONG_NETWORK_EXPLANATION_PRODUCTION: 'ONBOARDING.WRONG_NETWORK_EXPLANATION_PRODUCTION',
WRONG_NETWORK_EXPLANATION_STAGING: 'ONBOARDING.WRONG_NETWORK_EXPLANATION_STAGING',
WRONG_NETWORK: 'ONBOARDING.WRONG_NETWORK',
@@ -472,6 +477,7 @@ export const APP_STRING_KEYS = {
LEAVING_WEBSITE: 'LEGAL.LEAVING_WEBSITE',
// Deposit Modal
BRIDGE_FEE: 'DEPOSIT_MODAL.BRIDGE_FEE',
ENABLE_FOR_THIS_DEPOSIT: 'DEPOSIT_MODAL.ENABLE_FOR_THIS_DEPOSIT',
ENABLE_SYMBOL_ON_DYDX: 'DEPOSIT_MODAL.ENABLE_SYMBOL_ON_DYDX',
ENABLE_SYMBOL: 'DEPOSIT_MODAL.ENABLE_SYMBOL',
@@ -897,6 +903,7 @@ export const APP_STRING_KEYS = {
'66_REDUCTION': 'NOTIFICATIONS.66_REDUCTION',
ADJUSTED_IMR: 'NOTIFICATIONS.ADJUSTED_IMR',
ADJUSTED_IMR_BODY: 'NOTIFICATIONS.ADJUSTED_IMR_BODY',
BRIDGING_TOKENS: "NOTIFICATIONS.BRIDGING_TOKENS",
CLOSE_MARKET_POSITIONS: 'NOTIFICATIONS.CLOSE_MARKET_POSITIONS',
CLOSE_MARKET_POSITIONS_BODY: 'NOTIFICATIONS.CLOSE_MARKET_POSITIONS_BODY',
COMPLIANCE_ALERT: 'NOTIFICATIONS.COMPLIANCE_ALERT',
@@ -904,6 +911,7 @@ export const APP_STRING_KEYS = {
DEPOSIT_IN_PROGRESS: 'NOTIFICATIONS.DEPOSIT_IN_PROGRESS',
DEPOSIT_SUCCESS_DESCRIPTION: 'NOTIFICATIONS.DEPOSIT_SUCCESS_DESCRIPTION',
DEPOSIT_SUCCESS: 'NOTIFICATIONS.DEPOSIT_SUCCESS',
DEPOSIT_TO_CHAIN: "NOTIFICATIONS.DEPOSIT_TO_CHAIN",
DIRECT_TRANSFER_SUCCESS: 'NOTIFICATIONS.DIRECT_TRANSFER_SUCCESS',
DIRECT_TRANSFER_SUCCESS_DESCRIPTION: 'NOTIFICATIONS.DIRECT_TRANSFER_SUCCESS_DESCRIPTION',
EPOCH_REWARDS: 'NOTIFICATIONS.EPOCH_REWARDS',
@@ -912,6 +920,8 @@ export const APP_STRING_KEYS = {
FAST_WITHDRAW_PENDING: 'NOTIFICATIONS.FAST_WITHDRAW_PENDING',
FAST_WITHDRAW_SUCCESS_DESCRIPTION: 'NOTIFICATIONS.FAST_WITHDRAW_SUCCESS_DESCRIPTION',
FAST_WITHDRAW_SUCCESS: 'NOTIFICATIONS.FAST_WITHDRAW_SUCCESS',
INITIATED_DEPOSIT: "NOTIFICATIONS.INITIATED_DEPOSIT",
INITIATED_WITHDRAWAL: "NOTIFICATIONS.INITIATED_WITHDRAWAL",
LEGAL_UPDATES: 'NOTIFICATIONS.LEGAL_UPDATES',
LEGAL_UPDATES_DESCRIPTION: 'NOTIFICATIONS.LEGAL_UPDATES_DESCRIPTION',
MAINTENANCE_MARGIN_ADJUSTMENTS: 'NOTIFICATIONS.MAINTENANCE_MARGIN_ADJUSTMENTS',
@@ -928,6 +938,8 @@ export const APP_STRING_KEYS = {
SLOW_WITHDRAW_PENDING: 'NOTIFICATIONS.SLOW_WITHDRAW_PENDING',
SUSPICIOUS_TRADE: 'NOTIFICATIONS.SUSPICIOUS_TRADE',
SUSPICIOUS_TRADE_BODY: 'NOTIFICATIONS.SUSPICIOUS_TRADE_BODY',
WITHDRAW_IN_PROGRESS: 'NOTIFICATIONS.WITHDRAW_IN_PROGRESS',
WITHDRAW_TO_CHAIN: "NOTIFICATIONS.WITHDRAW_TO_CHAIN",
// Email Notifications
ACCOUNT_DESCRIPTION: 'EMAIL_NOTIFICATIONS.ACCOUNT_DESCRIPTION',
+3
View File
@@ -78,6 +78,9 @@ export const ERROR_STRING_KEYS = {
RESTRICTED_OPEN_POSITIONS_WITHDRAW: 'ERRORS.WITHDRAW_MODAL.RESTRICTED_OPEN_POSITIONS_WITHDRAW',
WITHDRAW_MORE_THAN_FREE_DUE_TO_FEE: 'ERRORS.WITHDRAW_MODAL.WITHDRAW_MORE_THAN_FREE_DUE_TO_FEE',
WITHDRAW_MORE_THAN_FREE: 'ERRORS.WITHDRAW_MODAL.WITHDRAW_MORE_THAN_FREE',
WITHDRAW_MUST_SPECIFY_ADDRESS: 'ERRORS.DEPOSIT_MODAL.WITHDRAW_MUST_SPECIFY_ADDRESS',
WITHDRAW_MUST_SPECIFY_ASSET: 'ERRORS.DEPOSIT_MODAL.WITHDRAW_MUST_SPECIFY_ASSET',
WITHDRAW_MUST_SPECIFY_CHAIN: 'ERRORS.DEPOSIT_MODAL.WITHDRAW_MUST_SPECIFY_CHAIN',
// Transfer modal errors
TRANSFER_INVALID_ETH_ADDRESS: 'ERRORS.TRANSFER_MODAL.TRANSFER_INVALID_ETH_ADDRESS',
+29 -6
View File
@@ -1,16 +1,22 @@
import { StatusResponse } from "@0xsquid/sdk";
/** implemented in useNotificationTypes */
export enum NotificationType {
OrderStatusChanged = 'OrderStatusChanged',
SquidTransfer = 'SquidTransfer',
}
export enum NotificationComponentType {}
export type NotificationId = string | number
export type NotificationId = string | number;
export type NotificationTypeConfig<_NotificationId extends NotificationId = string, NotificationUpdateKey = any> = {
export type NotificationTypeConfig<
_NotificationId extends NotificationId = string,
NotificationUpdateKey = any
> = {
type: NotificationType;
/** React hook to trigger notifications based on app state */
/** React hook to trigger notifications based on app state */
useTrigger: (_: {
trigger: (
/** Unique ID for the triggered notification */
@@ -30,7 +36,7 @@ export type NotificationTypeConfig<_NotificationId extends NotificationId = stri
* @param true (default): Notification initialized with status NotificationStatus.Triggered
* @param false: Notification initialized with status NotificationStatus.Cleared
*/
isNew?: boolean,
isNew?: boolean
) => void;
lastUpdated: number;
@@ -58,7 +64,10 @@ export enum NotificationStatus {
}
/** Notification state. Serialized and cached into localStorage. */
export type Notification<_NotificationId extends NotificationId = string, NotificationUpdateKey = any> = {
export type Notification<
_NotificationId extends NotificationId = string,
NotificationUpdateKey = any
> = {
id: _NotificationId;
type: NotificationType;
status: NotificationStatus;
@@ -74,7 +83,11 @@ export type NotificationDisplayData = {
title?: string;
description?: string;
description?: React.ReactNode;
customContent?: React.ReactNode;
customMenuContent?: React.ReactNode;
actionDescription?: string;
@@ -101,3 +114,13 @@ export type NotificationDisplayData = {
*/
toastDuration?: number;
};
// Notification types
export type TransferNotifcation = {
txHash: string;
toChainId?: string;
fromChainId?: string;
toAmount?: number;
triggeredAt?: number;
status?: StatusResponse;
};
+4 -2
View File
@@ -1,3 +1,5 @@
import { DEFAULT_MARKETID } from './markets';
export enum AppRoute {
Markets = '/markets',
Portfolio = '/portfolio',
@@ -5,7 +7,7 @@ export enum AppRoute {
Profile = '/profile',
Alerts = '/alerts',
Settings = '/settings',
Rewards = '/DYDX',
Rewards = '/DV4TNT',
}
export enum PortfolioRoute {
@@ -31,7 +33,7 @@ export enum MobileSettingsRoute {
export const TRADE_ROUTE = `${AppRoute.Trade}/:market`;
export const PORTFOLIO_ROUTE = `${AppRoute.Portfolio}/:subroute`;
export const HISTORY_ROUTE = `${AppRoute.Portfolio}/${PortfolioRoute.History}/:subroute`;
export const DEFAULT_TRADE_ROUTE = `${AppRoute.Trade}/ETH-USD`;
export const DEFAULT_TRADE_ROUTE = `${AppRoute.Trade}/${DEFAULT_MARKETID}`;
export const SETTINGS_ROUTE = `${AppRoute.Settings}/*`;
export const DEFAULT_DOCUMENT_TITLE = 'dYdX';
+5 -5
View File
@@ -10,7 +10,7 @@ export enum TradeTypes {
LIMIT = 'LIMIT',
STOP_LIMIT = 'STOP_LIMIT',
STOP_MARKET = 'STOP_MARKET',
TAKE_PROFIT_LIMIT = 'TAKE_PROFIT_LIMIT',
TAKE_PROFIT = 'TAKE_PROFIT',
TAKE_PROFIT_MARKET = 'TAKE_PROFIT_MARKET',
TRAILING_STOP = 'TRAILING_STOP',
}
@@ -30,9 +30,9 @@ export enum PositionSide {
export const UNCOMMITTED_ORDER_TIMEOUT = 10_000;
export const ORDER_SIDE_STRINGS = {
[OrderSide.SIDE_BUY]: STRING_KEYS.BUY,
[OrderSide.SIDE_SELL]: STRING_KEYS.SELL,
}
[OrderSide.BUY]: STRING_KEYS.BUY,
[OrderSide.SELL]: STRING_KEYS.SELL,
};
export const POSITION_SIDE_STRINGS: Record<PositionSide, string> = {
[PositionSide.None]: STRING_KEYS.NONE,
@@ -68,7 +68,7 @@ export const TRADE_TYPE_STRINGS: Record<
tradeTypeKey: STRING_KEYS.STOP_MARKET,
descriptionKey: STRING_KEYS.STOP_MARKET_DESCRIPTION,
},
[TradeTypes.TAKE_PROFIT_LIMIT]: {
[TradeTypes.TAKE_PROFIT]: {
tradeTypeKeyShort: STRING_KEYS.TAKE_PROFIT_LIMIT,
tradeTypeKey: STRING_KEYS.TAKE_PROFIT_LIMIT,
descriptionKey: STRING_KEYS.TAKE_PROFIT_LIMIT_DESCRIPTION,
+1 -1
View File
@@ -377,7 +377,7 @@ export const DYDX_CHAIN_INFO: Parameters<typeof suggestChain>[0] = {
export enum DydxChainAsset {
USDC = 'USDC',
DYDX = 'DYDX',
DYDX = 'Dv4TNT',
}
export const DYDX_CHAIN_ASSET_COIN_DENOM: Record<DydxChainAsset, string> = {
+4
View File
@@ -1,6 +1,8 @@
import { useApiState } from './useApiState';
import { useBreakpoints } from './useBreakpoints';
import { useCommandMenu } from './useCommandMenu';
import { useCurrentMarketId } from './useCurrentMarketId';
import { useDebounce } from './useDebounce';
import { useInterval } from './useInterval';
import { useDocumentTitle } from './useDocumentTitle';
import { useDydxClient } from './useDydxClient';
@@ -23,6 +25,8 @@ export {
useApiState,
useBreakpoints,
useCommandMenu,
useCurrentMarketId,
useDebounce,
useDocumentTitle,
useDydxClient,
useAccountBalance,
+6 -17
View File
@@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
import { useBalance } from 'wagmi';
import { StargateClient } from '@cosmjs/stargate';
import { useQuery } from 'react-query';
import { formatUnits } from 'ethers';
import { formatUnits } from 'viem';
import { CLIENT_NETWORK_CONFIGS, type DydxV4Network } from '@/constants/networks';
import { QUANTUM_MULTIPLIER } from '@/constants/numbers';
@@ -43,7 +43,7 @@ export const useAccountBalance = ({
assetSymbol,
bech32AddrPrefix,
chainId,
decimals,
decimals = 0,
rpc,
isCosmosChain,
}: UseAccountBalanceProps = {}) => {
@@ -66,7 +66,7 @@ export const useAccountBalance = ({
});
const cosmosQueryFn = useCallback(async () => {
if (dydxAddress && bech32AddrPrefix && rpc) {
if (dydxAddress && bech32AddrPrefix && rpc && addressOrDenom) {
const address = convertBech32Address({
address: dydxAddress,
bech32Prefix: bech32AddrPrefix,
@@ -76,14 +76,7 @@ export const useAccountBalance = ({
const balanceAsCoin = await client.getBalance(address, addressOrDenom);
await client.disconnect();
/**
* Hardcode decimals because most cosmos coins default to 6 decimals.
* Squids testnet has some coins as having 18 decimals instead of 6.
* @url https://github1s.com/cosmos/cosmjs/blob/HEAD/packages/amino/src/coins.ts#L8-L16
*
* @todo: change '6' to 'decimals' when Squid testnet data is updated.
* */
return formatUnits(balanceAsCoin.amount, decimals);
return formatUnits(BigInt(balanceAsCoin.amount), decimals);
}
}, [addressOrDenom, chainId, rpc]);
@@ -109,11 +102,7 @@ export const useAccountBalance = ({
return {
balance,
nativeTokenBalance,
isBalanceError: isCosmosChain
? cosmosQuery.status === 'error' && !cosmosQuery.isFetching
: evmQuery.status === 'error' && evmQuery.fetchStatus === 'idle',
isBalanceLoading: isCosmosChain
? cosmosQuery.status === 'loading' && cosmosQuery.isFetching
: evmQuery.status === 'loading' && evmQuery.fetchStatus === 'fetching',
queryStatus: isCosmosChain ? cosmosQuery.status : evmQuery.status,
isQueryFetching: isCosmosChain ? cosmosQuery.isFetching : evmQuery.fetchStatus === 'fetching',
};
};
+3 -4
View File
@@ -4,7 +4,6 @@ import { useDispatch } from 'react-redux';
import { AES, enc } from 'crypto-js';
import { LocalWallet, USDC_DENOM, type Subaccount } from '@dydxprotocol/v4-client';
import { SubAccountHistoricalPNLs } from '@/constants/abacus';
import { OnboardingGuard, OnboardingState, type EvmDerivedAddresses } from '@/constants/account';
import { LocalStorageKey, LOCAL_STORAGE_VERSIONS } from '@/constants/localStorage';
import { DydxAddress, EthereumAddress, PrivateInformation } from '@/constants/wallets';
@@ -12,8 +11,6 @@ import { DydxAddress, EthereumAddress, PrivateInformation } from '@/constants/wa
import {
setOnboardingState,
setOnboardingGuard,
setSubaccount,
setHistoricalPnl,
} from '@/state/account';
import abacusStateManager from '@/lib/abacus';
@@ -63,6 +60,8 @@ const useAccountsContext = () => {
forgetEvmSignature(previousEvmAddress);
}
if (evmAddress) abacusStateManager.setTransfersSourceAddress(evmAddress);
setPreviousEvmAddress(evmAddress);
}, [evmAddress]);
@@ -224,7 +223,7 @@ const useAccountsContext = () => {
// TODO: useAbacus({ dydxAddress })
useEffect(() => {
if (dydxAddress) abacusStateManager.setAccount(dydxAddress);
else abacusStateManager.disconnectAccount();
else abacusStateManager.attemptDisconnectAccount();
}, [dydxAddress]);
// clear subaccounts when no dydxAddress is set
+49
View File
@@ -0,0 +1,49 @@
import { useEffect, useMemo } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { useMatch, useNavigate } from 'react-router-dom';
import { LocalStorageKey } from '@/constants/localStorage';
import { DEFAULT_MARKETID } from '@/constants/markets';
import { AppRoute, DEFAULT_TRADE_ROUTE } from '@/constants/routes';
import { getSelectedNetwork } from '@/state/appSelectors';
import { closeDialogInTradeBox } from '@/state/dialogs';
import { setCurrentMarketId } from '@/state/perpetuals';
import abacusStateManager from '@/lib/abacus';
import { useLocalStorage } from './useLocalStorage';
import { getMarketIds } from '@/state/perpetualsSelectors';
export const useCurrentMarketId = () => {
const navigate = useNavigate();
const match = useMatch(`/${AppRoute.Trade}/:marketId`);
const { marketId } = match?.params ?? {};
const dispatch = useDispatch();
const selectedNetwork = useSelector(getSelectedNetwork);
const marketIds = useSelector(getMarketIds, shallowEqual);
const [lastViewedMarket, setLastViewedMarket] = useLocalStorage({
key: LocalStorageKey.LastViewedMarket,
defaultValue: DEFAULT_MARKETID,
});
const validId = useMemo(() => {
if (marketIds.length === 0) return marketId ?? lastViewedMarket;
if (!marketIds.includes(marketId)) return DEFAULT_MARKETID;
return marketId ?? lastViewedMarket;
}, [marketIds, marketId]);
useEffect(() => {
setLastViewedMarket(validId);
dispatch(setCurrentMarketId(validId));
dispatch(closeDialogInTradeBox());
navigate(`${AppRoute.Trade}/${validId}`, {
replace: true,
});
}, [validId]);
useEffect(() => {
abacusStateManager.setMarket(marketId ?? DEFAULT_MARKETID);
}, [selectedNetwork, marketId]);
};
+21
View File
@@ -0,0 +1,21 @@
import { useEffect, useState } from 'react';
/**
* https://usehooks-ts.com/react-hook/use-debounce
* @param value
* @param delay
* @returns the debouncedValue
*/
export function useDebounce<T>(value: T, delay?: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay || 500);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}
-7
View File
@@ -6,7 +6,6 @@ import { LocalStorageKey } from '@/constants/localStorage';
import {
DEFAULT_APP_ENVIRONMENT,
DydxNetwork,
isDydxV4Network,
isValidDydxNetwork,
} from '@/constants/networks';
@@ -16,8 +15,6 @@ import { initializeLocalization } from '@/state/app';
import abacusStateManager from '@/lib/abacus';
import squidRouter from '@/lib/squidRouter';
export const useInitializePage = () => {
const dispatch = useDispatch();
@@ -31,9 +28,5 @@ export const useInitializePage = () => {
useEffect(() => {
dispatch(initializeLocalization());
abacusStateManager.start({ network: localStorageNetwork });
if (isDydxV4Network(localStorageNetwork)) {
squidRouter.switchNetwork(localStorageNetwork);
}
}, []);
};
+95
View File
@@ -0,0 +1,95 @@
import { createContext, useContext, useCallback, useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import { LocalStorageKey } from '@/constants/localStorage';
import { type TransferNotifcation } from '@/constants/notifications';
import { useAccounts } from '@/hooks/useAccounts';
import { useSquid } from '@/hooks/useSquid';
import { useLocalStorage } from './useLocalStorage';
import { StatusResponse } from '@0xsquid/sdk';
const LocalNotificationsContext = createContext<
ReturnType<typeof useLocalNotificationsContext> | undefined
>(undefined);
LocalNotificationsContext.displayName = 'LocalNotifications';
export const LocalNotificationsProvider = ({ ...props }) => (
<LocalNotificationsContext.Provider value={useLocalNotificationsContext()} {...props} />
);
export const useLocalNotifications = () => useContext(LocalNotificationsContext)!;
const TRANSFER_STATUS_FETCH_INTERVAL = 10_000;
const useLocalNotificationsContext = () => {
// transfer notifications
const [allTransferNotifications, setAllTransferNotifications] = useLocalStorage<{
[key: `dydx${string}`]: TransferNotifcation[];
}>({
key: LocalStorageKey.TransferNotifications,
defaultValue: {},
});
const { dydxAddress } = useAccounts();
const transferNotifications = dydxAddress ? allTransferNotifications[dydxAddress] || [] : [];
const setTransferNotifications = useCallback(
(notifications: TransferNotifcation[]) => {
if (!dydxAddress) return;
const updatedNotifications = { ...allTransferNotifications };
updatedNotifications[dydxAddress] = notifications;
setAllTransferNotifications(updatedNotifications);
},
[setAllTransferNotifications, dydxAddress]
);
const addTransferNotification = useCallback(
(notification: TransferNotifcation) =>
setTransferNotifications([...transferNotifications, notification]),
[transferNotifications]
);
const squid = useSquid();
const { data: transferStatuses } = useQuery({
queryKey: ['getTransactionStatus', transferNotifications],
queryFn: async () => {
const statuses: { [key: string]: StatusResponse } = {};
for (const {
txHash,
toChainId,
fromChainId,
status: currentStatus,
} of transferNotifications) {
if (currentStatus && currentStatus?.squidTransactionStatus !== 'ongoing') continue;
const status = await squid?.getStatus({ transactionId: txHash, toChainId, fromChainId });
if (status) statuses[txHash] = status;
}
return statuses;
},
refetchInterval: TRANSFER_STATUS_FETCH_INTERVAL,
});
useEffect(() => {
if (!transferStatuses) return;
const newTransferNotifications = transferNotifications.map((notification) => {
const status = transferStatuses[notification.txHash];
if (!status) return notification;
return {
...notification,
status,
};
});
setTransferNotifications(newTransferNotifications);
}, [transferStatuses]);
return {
transferNotifications,
addTransferNotification,
};
};
-19
View File
@@ -1,19 +0,0 @@
import { useEffect, useMemo, useState } from 'react';
import { getSeparator } from '@/lib/numbers';
export const useLocaleSeparators = () => {
const [locale, setLocale] = useState(navigator.language || 'en-US');
useEffect(() => {
const handler = () => setLocale(navigator.language || 'en-US');
globalThis.addEventListener('languagechange', handler);
return () => globalThis.removeEventListener('languagechange', handler);
}, []);
return useMemo(() => {
return {
group: getSeparator({ locale, separatorType: 'group' }),
decimal: getSeparator({ locale, separatorType: 'decimal' }),
};
}, [locale]);
};
+55
View File
@@ -0,0 +1,55 @@
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { SUPPORTED_BASE_TAGS_LOCALE_MAPPING } from '@/constants/localization';
import { getSelectedLocale } from '@/state/localizationSelectors';
import abacusStateManager from '@/lib/abacus';
import { getSeparator } from '@/lib/numbers';
type LocaleContextType = ReturnType<typeof useLocaleContext>;
const LocaleContext = createContext<LocaleContextType>({} as LocaleContextType);
LocaleContext.displayName = 'Locale';
export const LocaleProvider = ({ ...props }) => (
<LocaleContext.Provider value={useLocaleContext()} {...props} />
);
const useLocaleContext = () => {
const selectedLocale = useSelector(getSelectedLocale);
const [browserLanguage, setBrowserLanguage] = useState(navigator.language || 'en-US');
useEffect(() => {
const handler = () => setBrowserLanguage(navigator.language || 'en-US');
globalThis.addEventListener('languagechange', handler);
return () => globalThis.removeEventListener('languagechange', handler);
}, []);
useEffect(() => {
if (selectedLocale) {
const updatedBrowserLanguage = Object.entries(SUPPORTED_BASE_TAGS_LOCALE_MAPPING).find(
([, value]) => value === selectedLocale
);
if (updatedBrowserLanguage) {
setBrowserLanguage(selectedLocale);
}
}
}, [selectedLocale]);
const separators = useMemo(() => {
return {
group: getSeparator({ browserLanguage, separatorType: 'group' }),
decimal: getSeparator({ browserLanguage, separatorType: 'decimal' }),
};
}, [browserLanguage]);
useEffect(() => {
abacusStateManager.setLocaleSeparators(separators);
}, [separators]);
return separators;
};
export const useLocaleSeparators = () => useContext(LocaleContext);
+2 -2
View File
@@ -29,7 +29,7 @@ export const useMarketsData = (
const markets = useMemo(() => {
return Object.values(allPerpetualMarkets)
.filter(Boolean)
.filter(({ assetId }) => allAssets[assetId]) // Filter out markets with no asset information
.map((marketData) => ({
asset: allAssets[marketData.assetId],
tickSizeDecimals: marketData.configs?.tickSizeDecimals,
@@ -56,7 +56,7 @@ export const useMarketsData = (
() => [
MarketFilters.ALL,
...Object.keys(MARKET_FILTER_LABELS).filter((marketFilter) =>
markets.some((market) => market.asset.tags?.toArray().some((tag) => tag === marketFilter))
markets.some((market) => market.asset?.tags?.toArray().some((tag) => tag === marketFilter))
),
],
[markets]
+56 -1
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useSelector, shallowEqual, useDispatch } from 'react-redux';
import { groupBy } from 'lodash';
@@ -8,12 +8,20 @@ import { STRING_KEYS } from '@/constants/localization';
import { type NotificationTypeConfig, NotificationType } from '@/constants/notifications';
import { ORDER_SIDE_STRINGS, TRADE_TYPE_STRINGS, TradeTypes } from '@/constants/trade';
import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { Icon, IconName } from '@/components/Icon';
import { Output, OutputType } from '@/components/Output';
import { TransferStatusToast } from '@/views/TransferStatus';
import { getSubaccountFills, getSubaccountOrders } from '@/state/accountSelectors';
import { openDialog } from '@/state/dialogs';
import { OrderStatusIcon } from '@/views/OrderStatusIcon';
import { useStringGetter } from './useStringGetter';
import { TransferStatusSteps } from '@/views/TransferStatusSteps';
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client';
export const notificationTypes = [
{
@@ -93,4 +101,51 @@ export const notificationTypes = [
};
},
} as NotificationTypeConfig<string, [string, number]>,
{
type: NotificationType.SquidTransfer,
useTrigger: ({ trigger }) => {
const stringGetter = useStringGetter();
const { transferNotifications } = useLocalNotifications();
const getTitleStringKey = useCallback((type: 'deposit' | 'withdraw', finished: boolean) => {
if (type === 'deposit' && !finished) return STRING_KEYS.DEPOSIT_IN_PROGRESS;
if (type === 'deposit' && finished) return STRING_KEYS.DEPOSIT;
if (type === 'withdraw' && !finished) return STRING_KEYS.WITHDRAW_IN_PROGRESS;
return STRING_KEYS.WITHDRAW;
}, []);
useEffect(() => {
for (const transfer of transferNotifications) {
const { toChainId, status, txHash, toAmount } = transfer;
const finished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing';
const type = toChainId === TESTNET_CHAIN_ID ? 'deposit' : 'withdraw';
trigger(
txHash,
{
icon: <Icon iconName={finished ? IconName.Transfer : IconName.Clock} />,
title: stringGetter({ key: getTitleStringKey(type, finished) }),
// TODO: confirm with design what the description should be
description: (
<>
<span>{type === 'deposit' ? 'Deposit of ' : 'Withdraw of'}</span>
<Output type={OutputType.Fiat} value={toAmount} />
</>
),
customContent: (
<TransferStatusToast
toAmount={transfer.toAmount}
triggeredAt={transfer.triggeredAt}
status={transfer.status}
/>
),
customMenuContent: !finished && <TransferStatusSteps status={transfer.status} />,
toastSensitivity: 'foreground',
},
[]
);
}
}, [transferNotifications]);
},
},
] satisfies NotificationTypeConfig[];
+7 -3
View File
@@ -106,7 +106,10 @@ const useNotificationsContext = () => {
updateKey,
} as Notification);
updateStatus(notification, isNew ? NotificationStatus.Triggered : NotificationStatus.Cleared);
updateStatus(
notification,
isNew ? NotificationStatus.Triggered : NotificationStatus.Cleared
);
}
// updateKey changed - update existing notification
@@ -141,7 +144,9 @@ const useNotificationsContext = () => {
// Push notifications
const [hasEnabledPush, setHasEnabledPush] = useLocalStorage({
key: LocalStorageKey.PushNotificationsEnabled,
defaultValue: Boolean(globalThis.Notification && globalThis.Notification.permission === 'granted'),
defaultValue: Boolean(
globalThis.Notification && globalThis.Notification.permission === 'granted'
),
});
const [isEnablingPush, setIsEnablingPush] = useState(false);
@@ -236,4 +241,3 @@ const useNotificationsContext = () => {
setIsMenuOpen,
};
};
// }
+1 -1
View File
@@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { DEFAULT_DOCUMENT_TITLE } from '@/constants/routes';
+52
View File
@@ -0,0 +1,52 @@
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { Squid } from '@0xsquid/sdk';
import { DydxV4Network, isDydxV4Network } from '@/constants/networks';
import { getSelectedNetwork } from '@/state/appSelectors';
const SQUID_BASE_URL: Record<DydxV4Network, string | undefined> = {
[DydxV4Network.V4Testnet2]: 'https://squid-api-git-feat-cosmos-maintestnet-0xsquid.vercel.app',
[DydxV4Network.V4Staging]: undefined,
[DydxV4Network.V4Local]: undefined,
[DydxV4Network.V4Mainnet]: 'https://api.0xsquid.com',
};
const useSquidContext = () => {
const selectedNetwork = useSelector(getSelectedNetwork);
const [_, setInitialized] = useState(false);
const initializeClient = async () => {
setInitialized(false);
if (!squid) return;
await squid.init();
setInitialized(true);
};
const squid = useMemo(
() =>
isDydxV4Network(selectedNetwork)
? new Squid({ baseUrl: SQUID_BASE_URL[selectedNetwork as DydxV4Network] })
: undefined,
[selectedNetwork]
);
useEffect(() => {
if (squid) {
initializeClient();
}
}, [squid]);
return squid;
};
type SquidContextType = ReturnType<typeof useSquidContext>;
const SquidContext = createContext<SquidContextType>(undefined);
SquidContext.displayName = '0xSquid';
export const SquidProvider = ({ ...props }) => (
<SquidContext.Provider value={useSquidContext()} {...props} />
);
export const useSquid = () => useContext(SquidContext);
+63 -4
View File
@@ -3,7 +3,7 @@ import { useDispatch } from 'react-redux';
import type { Nullable } from '@dydxprotocol/abacus';
import Long from 'long';
import type { IndexedTx } from '@cosmjs/stargate';
import { type Coin } from '@cosmjs/proto-signing';
import type { EncodeObject, Coin } from '@cosmjs/proto-signing';
import { Method } from '@cosmjs/tendermint-rpc';
import {
@@ -89,6 +89,7 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
simulateTransferNativeToken,
placeOrderForSubaccount,
cancelOrderForSubaccount,
sendSquidWithdrawFromSubaccount,
} = useMemo(
() => ({
depositToSubaccount: async ({
@@ -215,6 +216,7 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
type,
side,
price,
triggerPrice,
size,
clientId,
timeInForce,
@@ -228,6 +230,7 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
type: OrderType;
side: OrderSide;
price: number;
triggerPrice: Nullable<number>;
size: number;
clientId: number;
timeInForce: OrderTimeInForce;
@@ -250,7 +253,8 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
goodTilTimeInSeconds,
execution,
postOnly,
reduceOnly
reduceOnly,
triggerPrice ?? undefined
);
const endTimestamp = performance.now();
@@ -287,7 +291,7 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
clobPairId,
goodTilBlock,
goodTilBlockTime
)
);
const endTimestamp = performance.now();
@@ -298,6 +302,32 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
return result;
},
sendSquidWithdrawFromSubaccount: async ({
subaccountClient,
amount,
payload,
}: {
subaccountClient: SubaccountClient;
amount: number;
payload: string;
}) => {
if (!compositeClient) throw new Error('client not initialized');
const transaction = JSON.parse(payload);
const msg = compositeClient.withdrawFromSubaccountMessage(subaccountClient, amount);
const ibcMsg: EncodeObject = {
typeUrl: transaction.msgTypeUrl,
value: transaction.msg,
};
return await compositeClient.send(
subaccountClient.wallet,
() => Promise.resolve([msg, ibcMsg]),
false
);
},
}),
[compositeClient]
);
@@ -395,6 +425,28 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
[subaccountClient, simulateWithdrawFromSubaccount, simulateTransferNativeToken]
);
const simulateWithdraw = useCallback(
async (amount: number) => {
if (!subaccountClient) {
return;
}
return await simulateWithdrawFromSubaccount({ subaccountClient, amount });
},
[subaccountClient, simulateWithdrawFromSubaccount]
);
const sendSquidWithdraw = useCallback(
async (amount: number, payload: string) => {
if (!subaccountClient) {
return;
}
return await sendSquidWithdrawFromSubaccount({ subaccountClient, amount, payload });
},
[subaccountClient, sendSquidWithdrawFromSubaccount]
);
// ------ Faucet Methods ------ //
const requestFaucetFunds = useCallback(async () => {
if (!dydxAddress) return;
@@ -435,6 +487,7 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
type,
side,
price,
triggerPrice,
size,
clientId,
timeInForce,
@@ -461,6 +514,7 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
type: type as OrderType,
side: side as OrderSide,
price,
triggerPrice,
size,
clientId,
timeInForce: timeInForce as OrderTimeInForce,
@@ -490,7 +544,10 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
);
}
track(AnalyticsEvent.TradePlaceOrder, { ...orderParams, isClosePosition });
track(AnalyticsEvent.TradePlaceOrder, {
...orderParams,
isClosePosition,
} as HumanReadablePlaceOrderPayload & { isClosePosition: boolean });
onSuccess?.();
} catch (error) {
const errorStringKey = error?.code && ORDER_ERROR_CODE_MAP[error.code as number];
@@ -581,6 +638,8 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
// Transfer Methods
simulateTransfer,
transfer,
simulateWithdraw,
sendSquidWithdraw,
// Trading Methods
placeOrder,
+2
View File
@@ -17,6 +17,7 @@ import { OnboardingDialog } from '@/views/dialogs/OnboardingDialog';
import { ReceiveDialog } from '@/views/dialogs/ReceiveDialog';
import { TradeDialog } from '@/views/dialogs/TradeDialog';
import { TransferDialog } from '@/views/dialogs/TransferDialog';
import { WithdrawDialog } from '@/views/dialogs/WithdrawDialog';
import { OrderDetailsDialog } from '@/views/dialogs/DetailsDialog/OrderDetailsDialog';
import { FillDetailsDialog } from '@/views/dialogs/DetailsDialog/FillDetailsDialog';
@@ -53,5 +54,6 @@ export const DialogManager = () => {
[DialogTypes.Receive]: <ReceiveDialog {...modalProps} />,
[DialogTypes.Trade]: <TradeDialog {...modalProps} />,
[DialogTypes.Transfer]: <TransferDialog {...modalProps} />,
[DialogTypes.Withdraw]: <WithdrawDialog {...modalProps} />,
}[type];
};
+2 -2
View File
@@ -1,6 +1,7 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { useDispatch, useSelector } from 'react-redux';
import { ButtonAction, ButtonSize } from '@/constants/buttons';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization';
import { DEFAULT_MARKETID } from '@/constants/markets';
@@ -15,9 +16,8 @@ import { BellIcon, MarketsIcon, PlayIcon, PortfolioIcon, ProfileIcon, TradeIcon
import { IconButton } from '@/components/IconButton';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { getCurrentMarketId } from '@/state/perpetualsSelectors';
import { openDialog } from '@/state/dialogs';
import { ButtonAction, ButtonSize } from '@/constants/buttons';
import { getCurrentMarketId } from '@/state/perpetualsSelectors';
export const FooterMobile = () => {
const dispatch = useDispatch();
+1
View File
@@ -41,6 +41,7 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
slotIcon={displayData.icon}
slotTitle={displayData.title}
slotDescription={displayData.description}
slotCustomContent={displayData.customContent}
slotAction={
<Button size={ButtonSize.Small} onClick={() => onNotificationAction(notification)}>
{displayData.actionDescription}
+23 -39
View File
@@ -17,7 +17,6 @@ import {
} from '@/constants/abacus';
import { log } from '../telemetry';
import { bytesToBigInt } from '../numbers';
class DydxChainQueries implements AbacusDydxChainQueriesProtocol {
private compositeClient: CompositeClient | undefined;
@@ -34,34 +33,30 @@ class DydxChainQueries implements AbacusDydxChainQueriesProtocol {
faucetUrl: Nullable<string> | undefined,
callback: (p0: Nullable<string>) => void
): Promise<void> {
try {
this.compositeClient = await CompositeClient.connect(
new Network(
chainId,
new IndexerConfig(indexerUrl, indexerSocketUrl),
new ValidatorConfig(validatorUrl, chainId, {
broadcastPollIntervalMs: 3_000,
broadcastTimeoutMs: 60_000,
})
)
);
} catch (error) {
log('AbacusDydxChainQueries/connectNetwork', error);
} finally {
// Dispatch custom event to notify other parts of the app that the network has been connected
const customEvent = new CustomEvent('abacus:connectNetwork', {
detail: {
indexerUrl,
indexerSocketUrl,
validatorUrl,
chainId,
faucetUrl,
},
});
this.compositeClient = await CompositeClient.connect(
new Network(
chainId,
new IndexerConfig(indexerUrl, indexerSocketUrl),
new ValidatorConfig(validatorUrl, chainId, {
broadcastPollIntervalMs: 3_000,
broadcastTimeoutMs: 60_000,
})
)
);
globalThis.dispatchEvent(customEvent);
callback(JSON.stringify({ success: true }));
}
// Dispatch custom event to notify other parts of the app that the network has been connected
const customEvent = new CustomEvent('abacus:connectNetwork', {
detail: {
indexerUrl,
indexerSocketUrl,
validatorUrl,
chainId,
faucetUrl,
},
});
globalThis.dispatchEvent(customEvent);
callback(JSON.stringify({ success: true }));
}
parseToPrimitives<T>(x: T): T {
@@ -73,10 +68,6 @@ class DydxChainQueries implements AbacusDydxChainQueriesProtocol {
return x.map((item) => this.parseToPrimitives(item)) as T;
}
if (x instanceof Uint8Array) {
return bytesToBigInt(x).toString() as T;
}
if (Long.isLong(x)) {
return x.toString() as T;
}
@@ -115,13 +106,6 @@ class DydxChainQueries implements AbacusDydxChainQueriesProtocol {
);
callback(JSON.stringify({ url: optimalNode }));
break;
case QueryType.EquityTiers:
const equityTiers =
await this.compositeClient?.validatorClient.get.getEquityTierLimitConfiguration();
const parsedEquityTiers = this.parseToPrimitives(equityTiers);
console.log(parsedEquityTiers);
callback(JSON.stringify(parsedEquityTiers));
break;
case QueryType.FeeTiers:
const feeTiers = await this.compositeClient?.validatorClient.get.getFeeTiers();
const parsedFeeTiers = this.parseToPrimitives(feeTiers);
+15 -2
View File
@@ -1,12 +1,25 @@
import type { AbacusFormatterProtocol } from '@/constants/abacus';
import { type LocaleSeparators, MustBigNumber, getFractionDigits } from '../numbers';
class AbacusFormatter implements AbacusFormatterProtocol {
localeSeparators: LocaleSeparators;
constructor() {
this.localeSeparators = { group: ',', decimal: '.' };
}
setLocaleSeparators({ group, decimal }: LocaleSeparators) {
this.localeSeparators = { group, decimal };
}
percent(value: number, digits: number): string {
return value.toString();
return MustBigNumber(value).toFixed(digits);
}
dollar(value: number, tickSize: string): string {
return value.toString();
const tickSizeDecimals = getFractionDigits(tickSize);
return MustBigNumber(value).toFixed(tickSizeDecimals);
}
}
+36 -2
View File
@@ -18,6 +18,7 @@ import {
IOImplementations,
UIImplementations,
CoroutineTimer,
TransferType,
} from '@/constants/abacus';
import { DEFAULT_MARKETID } from '@/constants/markets';
@@ -26,6 +27,7 @@ import { type DydxNetwork } from '@/constants/networks';
import type { RootStore } from '@/state/_store';
import { getInputTradeOptions } from '@/state/inputsSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
import AbacusRest from './rest';
import AbacusWebsocket from './websocket';
@@ -34,6 +36,7 @@ import AbacusStateNotifier from './stateNotification';
import AbacusLocalizer from './localizer';
import AbacusFormatter from './formatter';
import AbacusThreading from './threading';
import { LocaleSeparators } from '../numbers';
class AbacusStateManager {
private store: RootStore | undefined;
@@ -42,12 +45,14 @@ class AbacusStateManager {
stateManager: InstanceType<typeof AsyncAbacusStateManager>;
websocket: AbacusWebsocket;
stateNotifier: AbacusStateNotifier;
abacusFormatter: AbacusFormatter;
constructor() {
this.store = undefined;
this.currentMarket = undefined;
this.stateNotifier = new AbacusStateNotifier();
this.websocket = new AbacusWebsocket();
this.abacusFormatter = new AbacusFormatter();
const ioImplementations = new IOImplementations(
// @ts-ignore
@@ -63,7 +68,7 @@ class AbacusStateManager {
const uiImplementations = new UIImplementations(
// @ts-ignore
new AbacusLocalizer(),
new AbacusFormatter()
this.abacusFormatter
);
this.stateManager = new AsyncAbacusStateManager(
@@ -87,6 +92,15 @@ class AbacusStateManager {
disconnectAccount = () => {
this.stateManager.accountAddress = null;
};
attemptDisconnectAccount = () => {
const state = this.store?.getState();
const { type: transferType }= (state && getTransferInputs(state)) || {};
// we don't want to disconnect the account if we switch network during the deposit form
if (transferType?.rawValue !== TransferType.deposit.rawValue) {
this.disconnectAccount();
}
}
// ------ Input Values ------ //
clearTradeInputValues = ({ shouldResetSize }: { shouldResetSize?: boolean } = {}) => {
@@ -144,6 +158,10 @@ class AbacusStateManager {
this.stateManager.accountAddress = walletAddress;
};
setTransfersSourceAddress = (evmAddress: string) => {
this.stateManager.sourceAddress = evmAddress;
};
setSubaccountNumber = (subaccountNumber: number) =>
(this.stateManager.subaccountNumber = subaccountNumber);
@@ -179,6 +197,22 @@ class AbacusStateManager {
this.stateManager.closePosition(value, field);
};
setLocaleSeparators = ({ group, decimal }: LocaleSeparators) => {
this.abacusFormatter.setLocaleSeparators({ group, decimal });
};
setTransferStatus = ({
hash,
fromChainId,
toChainId,
}: {
hash: string;
fromChainId?: string;
toChainId?: string;
}) => {
this.stateManager.transferStatus(hash, fromChainId, toChainId);
};
// ------ Utils ------ //
placeOrderPayload = (): Nullable<HumanReadablePlaceOrderPayload> =>
this.stateManager.placeOrderPayload();
@@ -203,7 +237,7 @@ class AbacusStateManager {
};
sendSocketRequest = (requestText: string) => {
this.websocket.sendSocketRequest(requestText);
this.websocket.send(requestText);
};
}
+6
View File
@@ -19,6 +19,7 @@ import {
setFundingPayments,
setHistoricalPnl,
setSubaccount,
setTransfers,
setWallet,
} from '@/state/account';
@@ -102,6 +103,11 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol {
dispatch(setFundingPayments(fundingPayments));
}
if (changes.has(Changes.transfers)) {
const transfers = updatedState.subaccountTransfers(subaccountId)?.toArray() || [];
dispatch(setTransfers(transfers));
}
if (changes.has(Changes.historicalPnl)) {
const historicalPnl =
updatedState.subaccountHistoricalPnl(subaccountId)?.toArray() || [];
-4
View File
@@ -46,10 +46,6 @@ class AbacusWebsocket implements Omit<AbacusWebsocketProtocol, '__doNotUseOrImpl
}
}
sendSocketRequest = (requestText: string) => {
this.socket?.send(requestText);
};
handleCandlesSubscription = ({
channelId,
subscribe,
+4 -17
View File
@@ -1,6 +1,7 @@
import { BigNumber } from 'bignumber.js';
export type BigNumberish = BigNumber | string | number;
export type LocaleSeparators = { group?: string; decimal?: string };
export const BIG_NUMBERS = {
ZERO: new BigNumber(0),
@@ -56,26 +57,12 @@ export const shorternNumberForDisplay = (num?: number) =>
* @returns separator for the given locale and separator type
*/
export const getSeparator = ({
locale = navigator.language || 'en-US',
browserLanguage = navigator.language || 'en-US',
separatorType,
}: {
locale?: string;
browserLanguage?: string;
separatorType: Intl.NumberFormatPartTypes;
}) =>
Intl.NumberFormat(locale)
Intl.NumberFormat(browserLanguage)
.formatToParts(1000.1)
.find?.((part) => part.type === separatorType)?.value;
/**
* Converts a byte array (representing an arbitrary-size signed integer) into a bigint.
* @param u Array of bytes represented as a Uint8Array.
*/
export function bytesToBigInt(u: Uint8Array): bigint {
if (u.length <= 1) {
return BigInt(0);
}
const negated: boolean = (u[0] & 1) === 1;
const hex: string = Buffer.from(u.slice(1)).toString('hex');
const abs: bigint = BigInt(`0x${hex}`);
return negated ? -abs : abs;
}
-140
View File
@@ -1,140 +0,0 @@
import { RouteResponse, Squid } from '@0xsquid/sdk';
import type { GetRoute, ChainData, RouteData, TokenData } from '@0xsquid/sdk';
import { DEFAULT_APP_ENVIRONMENT, DydxV4Network } from '@/constants/networks';
import { log } from './telemetry';
/**
* @todo dYdX chain will eventually be added to squidSDK as an option for toChain and
* USDC from Noble chain will also be a possible toToken in the near future. We will
* need to eventually change these constants.
* */
const SQUID_TESTNET_ROUTE_DEFAULTS = {
toChain: 'osmo-test-4', // Osmosis Testnet
toToken: 'uausdc', // aUSDC
enableForecall: true, // instant execution service, defaults to true
quoteOnly: false, // optional, defaults to false
};
// This config is for dev only! Used for testing General Message Passing (route provided by 0xSquid)
const SQUID_TESTNET_AVAX_TO_AXELAR_WITH_GMP = {
toChain: 'axelar-testnet-lisbon-3',
toToken: 'uosmo',
enableForecall: false,
quoteOnly: false,
};
const SQUID_MAINNET_ROUTE_DEFAULTS = {
toChain: 'osmosis-1', // Osmosis Testnet
toToken: 'uusdc', // aUSDC
enableForecall: true, // instant execution service, defaults to true
quoteOnly: false, // optional, defaults to false
};
class SquidFactory {
private client: Squid;
private isInitialized: boolean;
constructor(baseUrl: string) {
this.client = new Squid({ baseUrl });
this.isInitialized = false;
}
async init() {
if (this.isInitialized) return;
try {
await this.client.init();
this.isInitialized = true;
} catch (error) {
log('SquidFactory/init', error);
}
}
getClient = () => this.client;
}
const squidClientTestnet = new SquidFactory('https://testnet.api.0xsquid.com'); // new SquidFactory('https://squid-2gwsw7ij1-0xsquid.vercel.app');
const squidClientMainnet = new SquidFactory('https://api.0xsquid.com');
const SQUID_CLIENT_BY_NETWORK: Record<DydxV4Network, SquidFactory> = {
[DydxV4Network.V4Local]: squidClientTestnet,
[DydxV4Network.V4Mainnet]: squidClientMainnet,
[DydxV4Network.V4Staging]: squidClientTestnet,
[DydxV4Network.V4Testnet2]: squidClientTestnet,
};
class SquidRouter {
private client: Squid;
private network: DydxV4Network;
constructor() {
this.network = DEFAULT_APP_ENVIRONMENT;
this.client = SQUID_CLIENT_BY_NETWORK[this.network].getClient();
}
/**
* @description the default destination chain/token and other hardcoded options
*/
get SQUID_ROUTE_DEFAULTS() {
return {
[DydxV4Network.V4Local]: SQUID_TESTNET_ROUTE_DEFAULTS,
[DydxV4Network.V4Staging]: SQUID_TESTNET_ROUTE_DEFAULTS,
[DydxV4Network.V4Testnet2]: SQUID_TESTNET_ROUTE_DEFAULTS,
[DydxV4Network.V4Mainnet]: SQUID_MAINNET_ROUTE_DEFAULTS,
}[this.network];
}
/**
* @description updates the client to the correct 0xSquid environment based on which dYdX v4 network is selected.
* @param network dYdX v4 network that is currently selected
*/
switchNetwork = (network: DydxV4Network) => {
this.client = SQUID_CLIENT_BY_NETWORK[network].getClient();
};
/**
* @description initializes the Squid client if it was not previously initialized (API request)
*/
initializeClient = () => SQUID_CLIENT_BY_NETWORK[this.network].init();
/**
* @returns list of chains supported by Axelar
*/
getChains = (): ChainData[] => this.client.chains;
/**
* @returns list of tokens supported by Axelar
*/
getTokens = (): TokenData[] => this.client.tokens;
/**
* @param getRouteParams
* @returns 0xSquid RouteData
*/
getRoute = async (getRouteParams: GetRoute): Promise<RouteResponse> => {
const resp = await this.client.getRoute(getRouteParams);
return resp;
};
/**
* @params {
* @route the 0xSquid RouteData
* @signer the EVM/Cosmos Signer used to sign the Axelar bridge
* }
* @returns transaction information after the route has been executed
*/
executeRoute = async ({ route, signer }: { route: RouteData; signer: any }) => {
const tx = await this.client.executeRoute({
signer,
route,
});
return await tx.wait();
};
}
const squidRouter = new SquidRouter();
export default squidRouter;
+6
View File
@@ -72,3 +72,9 @@ export const getTimeTillNextUnit = (unit: 'minute' | 'hour' | 'day') => {
};
export const getTimeString = (time: number) => time.toString().padStart(2, '0');
export const formatSeconds = (seconds: number) => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${getTimeString(minutes)}:${getTimeString(remainingSeconds)}`;
}
+18 -3
View File
@@ -13,7 +13,7 @@ import {
} from '@/constants/abacus';
import { AlertType } from '@/constants/alerts';
import { PERCENT_DECIMALS } from '@/constants/numbers';
import { PERCENT_DECIMALS, USD_DECIMALS } from '@/constants/numbers';
import { TRADE_ROUTE } from '@/constants/routes';
import { PositionSide, TradeTypes } from '@/constants/trade';
@@ -65,9 +65,13 @@ export const hasPositionSideChanged = ({
const formatErrorParam = ({
value,
format,
stepSizeDecimals,
tickSizeDecimals,
}: {
value: Nullable<string>;
format?: Nullable<string>;
stepSizeDecimals: Nullable<number>;
tickSizeDecimals: Nullable<number>;
}) => {
switch (format) {
case 'percent': {
@@ -76,7 +80,11 @@ const formatErrorParam = ({
}
case 'size': {
const sizeBN = MustBigNumber(value);
return sizeBN.toFixed(0);
return sizeBN.toFixed(stepSizeDecimals ?? 0);
}
case 'price': {
const dollarBN = MustBigNumber(value);
return `$${dollarBN.toFixed(tickSizeDecimals ?? USD_DECIMALS)}`;
}
default: {
return value || '';
@@ -90,9 +98,13 @@ const formatErrorParam = ({
export const getTradeInputAlert = ({
abacusInputErrors,
stringGetter,
stepSizeDecimals,
tickSizeDecimals,
}: {
abacusInputErrors: ValidationError[];
stringGetter: StringGetterFunction;
stepSizeDecimals: Nullable<number>;
tickSizeDecimals: Nullable<number>;
}) => {
const inputAlerts = abacusInputErrors.map(({ action: errorAction, resources, type }) => {
const { action, text } = resources || {};
@@ -104,7 +116,10 @@ export const getTradeInputAlert = ({
Object.fromEntries(
stringParams
.toArray()
.map(({ key, value, format }) => [key, formatErrorParam({ value, format })])
.map(({ key, value, format }) => [
key,
formatErrorParam({ value, format, stepSizeDecimals, tickSizeDecimals }),
])
);
return {
+15 -12
View File
@@ -162,7 +162,13 @@ const createWalletConnect2ConnectorWithId = (walletconnect2Id: string) =>
});
// Custom connector from wallet selection
import { type WalletConnection, WalletConnectionType, type WalletType, walletConnectionTypes, wallets } from '@/constants/wallets';
import {
type WalletConnection,
WalletConnectionType,
type WalletType,
walletConnectionTypes,
wallets,
} from '@/constants/wallets';
export const resolveWagmiConnector = ({
walletType,
@@ -174,15 +180,12 @@ export const resolveWagmiConnector = ({
const walletConfig = wallets[walletType];
const walletConnectionConfig = walletConnectionTypes[walletConnection.type];
return (
walletConnection.type === WalletConnectionType.InjectedEip1193 && walletConnection.provider ?
createInjectedConnectorWithProvider(walletConnection.provider)
: walletConnection.type === WalletConnectionType.WalletConnect1 && walletConfig.walletconnect1Name ?
createWalletConnect1ConnectorWithName(walletConfig.walletconnect1Name)
: walletConnection.type === WalletConnectionType.WalletConnect2 && walletConfig.walletconnect2Id ?
createWalletConnect2ConnectorWithId(walletConfig.walletconnect2Id)
: connectors.find(
({ id }: { id: string }) => id === walletConnectionConfig.wagmiConnectorId
)
);
return walletConnection.type === WalletConnectionType.InjectedEip1193 && walletConnection.provider
? createInjectedConnectorWithProvider(walletConnection.provider)
: walletConnection.type === WalletConnectionType.WalletConnect1 &&
walletConfig.walletconnect1Name
? createWalletConnect1ConnectorWithName(walletConfig.walletconnect1Name)
: walletConnection.type === WalletConnectionType.WalletConnect2 && walletConfig.walletconnect2Id
? createWalletConnect2ConnectorWithId(walletConfig.walletconnect2Id)
: connectors.find(({ id }: { id: string }) => id === walletConnectionConfig.wagmiConnectorId);
};
+14 -2
View File
@@ -113,6 +113,7 @@
"HEDGIES": "Hedgies",
"HIDE_SECTION": "Hide section",
"HIDE": "Hide",
"HIDE_DETAILS": "Hide details",
"HISTORY": "History",
"INCREASED": "increased",
"INCREASING": "Increasing",
@@ -182,6 +183,7 @@
"RECEIVE": "Receive",
"RECENT": "Recent",
"RECENT_TRADES_SHORT": "Trades",
"RECIPIENT": "Recipient",
"REFERRAL_CODE": "Referral Code",
"REFERRALS": "Referrals",
"REFERRER_PERCENT_OFF": "{DISCOUNT}% off",
@@ -193,6 +195,7 @@
"SELECT_NETWORK": "Select Network",
"SELL": "Sell",
"SEND": "Send",
"SENDER": "Sender",
"SHARE": "Share",
"SHORT_POSITION_SHORT": "Short",
"SIDE": "Side",
@@ -386,6 +389,7 @@
"CONNECTING_SUBTITLE": "Check your wallet for a connection request. Connecting to dYdX is free and does not affect your funds.",
"COULD_NOT_FIND_AFFILIATE": "We couldn't find this affiliate, please check the link.",
"CREATE_OR_VERIFY_COSMOS_ADDRESS": "Create or verify Cosmos address.",
"DEPOSIT_STATUS": "Your deposit of {AMOUNT_USD} will be available in approximately {ESTIMATED_DURATION}.",
"DONT_MISS": "Don't miss your chance to get a {DISCOUNT} on trading fees!",
"ENABLE_API": "Enable secure access to our API for lightning quick trading",
"ENABLE_TRADING": "Enable trading",
@@ -448,6 +452,7 @@
"VERIFY_WALLET_COMPATIBILITY": "Verify wallet compatibility",
"WELCOME_DYDX": "Welcome to dYdX",
"WELCOME": "Welcome",
"WITHDRAW_STATUS": "Your withdrawal of {AMOUNT_USD} will be available in approximately {ESTIMATED_DURATION}.",
"WRONG_NETWORK_EXPLANATION_PRODUCTION": "To complete dYdX onboarding, set your wallet network to 'Ethereum Mainnet'.",
"WRONG_NETWORK_EXPLANATION_STAGING": "To complete dYdX onboarding, set your wallet network to 'Goerli Test Network'.",
"WRONG_NETWORK": "Wrong network",
@@ -477,6 +482,7 @@
"LEAVING_WEBSITE": "Leaving website"
},
"DEPOSIT_MODAL": {
"BRIDGE_FEE": "Bridge Fee",
"ENABLE_FOR_THIS_DEPOSIT": "Enable for this deposit",
"ENABLE_SYMBOL_ON_DYDX": "Enable {SYMBOL} on dYdX",
"ENABLE_SYMBOL": "Enable {SYMBOL}",
@@ -912,13 +918,15 @@
"66_REDUCTION": "66% reduction",
"ADJUSTED_IMR": "Adjusted Margin",
"ADJUSTED_IMR_BODY": "We have temporarily {ACTION} the Initial Margin Requirements on {MARKET} to {PERCENT} due to volatility. {ADDITIONAL_NOTE}",
"BRIDGING_TOKENS": "Bridging tokens",
"CLOSE_MARKET_POSITIONS": "Close your {MARKET} position",
"CLOSE_MARKET_POSITIONS_BODY": "{MARKET} is set to close only mode and the close price of {MARKET} is now fixed. Please close your open {MARKET} position as soon as you are able. We're here to help via the help chat if you run into issues.",
"COMPLIANCE_ALERT": "Compliance alert",
"DEPOSIT_IN_PROGRESS_DESCRIPTION": "Your deposit of {AMOUNT_ELEMENT} will be available after 14 confirmations.",
"DEPOSIT_IN_PROGRESS": "Deposit in progress",
"DEPOSIT_IN_PROGRESS": "Deposit in progress...",
"DEPOSIT_SUCCESS_DESCRIPTION": "Your deposit of {AMOUNT_ELEMENT} has been confirmed and is now available for trading.",
"DEPOSIT_SUCCESS": "Deposit success!",
"DEPOSIT_TO_CHAIN": "Deposit to {CHAIN}",
"DIRECT_TRANSFER_SUCCESS": "Direct transfer success!",
"DIRECT_TRANSFER_SUCCESS_DESCRIPTION": "Your direct transfer of {AMOUNT_ELEMENT} has been confirmed.",
"EPOCH_REWARDS": "Epoch {EPOCH_NUMBER} Rewards",
@@ -927,6 +935,8 @@
"FAST_WITHDRAW_PENDING": "Fast withdraw pending",
"FAST_WITHDRAW_SUCCESS_DESCRIPTION": "Your fast withdraw of {AMOUNT_ELEMENT} has been confirmed.",
"FAST_WITHDRAW_SUCCESS": "Fast withdraw success!",
"INITIATED_DEPOSIT": "Initiated deposit",
"INITIATED_WITHDRAWAL": "Initiated withdrawal",
"LEGAL_UPDATES": "Legal updates",
"LEGAL_UPDATES_DESCRIPTION": "Please check out our updated {TOU} and {PRIVACY_POLICY}.",
"MAINTENANCE_MARGIN_ADJUSTMENTS": "Maintenance margin requirements {ACTION} at {PERCENT}%.",
@@ -942,7 +952,9 @@
"SLOW_WITHDRAW_PENDING_DESCRIPTION": "You have {AMOUNT_ELEMENT} in pending slow withdraws. You will receive a notification when your funds have been confirmed on Layer 2.",
"SLOW_WITHDRAW_PENDING": "Slow withdraw(s) pending",
"SUSPICIOUS_TRADE": "Suspicious activity",
"SUSPICIOUS_TRADE_BODY": "We have noticed suspicious trading activity related to your account on dYdX, including potential wash trading activity. We will be reviewing your activity on an ongoing basis. If we notice similar activity in the future, then you will be permanently blocked from performing transfers within the protocol and from placing orders other than market orders that reduce your positions."
"SUSPICIOUS_TRADE_BODY": "We have noticed suspicious trading activity related to your account on dYdX, including potential wash trading activity. We will be reviewing your activity on an ongoing basis. If we notice similar activity in the future, then you will be permanently blocked from performing transfers within the protocol and from placing orders other than market orders that reduce your positions.",
"WITHDRAW_IN_PROGRESS": "Withdrawal(s) in progress...",
"WITHDRAW_TO_CHAIN": "Withdraw to {CHAIN}"
},
"EMAIL_NOTIFICATIONS": {
"ACCOUNT_DESCRIPTION": "Deposits, Withdrawals, Account Updates",
+4 -1
View File
@@ -65,7 +65,10 @@
"MIN_SLOW_WITHDRAW_AMOUNT": "Slow withdraw amount must be greater than or equal to 10, or your entire account balance.",
"RESTRICTED_OPEN_POSITIONS_WITHDRAW": "Your account is restricted. You must close all your positions before you can withdraw.",
"WITHDRAW_MORE_THAN_FREE_DUE_TO_FEE": "You cannot withdraw more than your free collateral. Make sure you account for the fast withdraw fee when entering an amount.",
"WITHDRAW_MORE_THAN_FREE": "You cannot withdraw more than your free collateral."
"WITHDRAW_MORE_THAN_FREE": "You cannot withdraw more than your free collateral.",
"WITHDRAW_MUST_SPECIFY_ADDRESS": "Please enter a destination address.",
"WITHDRAW_MUST_SPECIFY_ASSET": "Please select an asset to withdraw to.",
"WITHDRAW_MUST_SPECIFY_CHAIN": "Please select a source chain to withdraw to."
},
"TRANSFER_MODAL": {
"TRANSFER_INVALID_ETH_ADDRESS": "We could not find an account associated with this wallet address.",
+1 -1
View File
@@ -1,5 +1,5 @@
import './polyfills';
import { Fragment, StrictMode } from 'react';
import { StrictMode } from 'react';
import ReactDOM from 'react-dom/client';
import { HashRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
+6 -5
View File
@@ -28,13 +28,14 @@ export const History = () => {
label: <h3>{stringGetter({ key: STRING_KEYS.TRADES })}</h3>,
href: HistoryRoute.Trades,
},
{
value: HistoryRoute.Transfers,
label: <h3>{stringGetter({ key: STRING_KEYS.TRANSFERS })}</h3>,
href: HistoryRoute.Transfers,
tag: 'USDC',
},
// TODO - TRCL-1693 -
// {
// value: HistoryRoute.Transfers,
// label: <h3>{stringGetter({ key: STRING_KEYS.TRANSFERS })}</h3>,
// href: HistoryRoute.Transfers,
// },
// {
// value: HistoryRoute.Payments,
// label: <h3>{stringGetter({ key: STRING_KEYS.PAYMENTS })}</h3>,
// href: HistoryRoute.Payments,
+5 -3
View File
@@ -9,6 +9,7 @@ import { useBreakpoints, useDocumentTitle, useStringGetter } from '@/hooks';
import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable';
import { FundingPaymentsTable } from '@/views/tables/FundingPaymentsTable';
import { TransferHistoryTable } from '@/views/tables/TransferHistoryTable';
import { Icon, IconName } from '@/components/Icon';
import { NavigationMenu } from '@/components/NavigationMenu';
import { WithSidebar } from '@/components/WithSidebar';
@@ -61,7 +62,10 @@ export default () => {
/>
}
/>
<Route path={HistoryRoute.Transfers} element={<div />} />
<Route
path={HistoryRoute.Transfers}
element={<TransferHistoryTable withOuterBorder={isNotTablet} />}
/>
<Route
path={HistoryRoute.Payments}
element={<FundingPaymentsTable withOuterBorder={isNotTablet} />}
@@ -118,8 +122,6 @@ export default () => {
},
],
},
// TODO(aforaleka) Add back subitems when there are clearer designs
// or when transfers and payments are ready
]}
/>
)
+6 -6
View File
@@ -40,12 +40,12 @@ export const PortfolioNavMobile = () => {
label: stringGetter({ key: STRING_KEYS.TRADES }),
description: stringGetter({ key: STRING_KEYS.TRADES_DESCRIPTION }),
},
// TODO: TRCL-1693 - re-enable when Payments and Transfers are ready
// {
// value: `${AppRoute.Portfolio}/${PortfolioRoute.History}/${HistoryRoute.Transfers}`,
// label: stringGetter({ key: STRING_KEYS.TRANSFERS }),
// description: stringGetter({ key: STRING_KEYS.TRANSFERS_DESCRIPTION }),
// },
{
value: `${AppRoute.Portfolio}/${PortfolioRoute.History}/${HistoryRoute.Transfers}`,
label: stringGetter({ key: STRING_KEYS.TRANSFERS }),
description: stringGetter({ key: STRING_KEYS.TRANSFERS_DESCRIPTION }),
},
// TODO: TRCL-1693 - re-enable when Payments are ready
// {
// value: `${AppRoute.Portfolio}/${PortfolioRoute.History}/${HistoryRoute.Payments}`,
// label: stringGetter({ key: STRING_KEYS.PAYMENTS }),
+3 -3
View File
@@ -36,8 +36,8 @@ export const DYDXBalancePanel = () => {
slotHeader={
<Styled.Header>
<Styled.Title>
<AssetIcon symbol="DYDX" />
DYDX
{/* <AssetIcon symbol="DYDX" /> */}
Dv4TNT
</Styled.Title>
<Styled.ReceiveAndTransferButtons>
{!canAccountTrade ? (
@@ -106,7 +106,7 @@ export const DYDXBalancePanel = () => {
{
key: 'totalBalance',
label: 'Total balance',
value: <Output type={OutputType.Asset} value={nativeTokenBalance} tag="DYDX" />,
value: <Output type={OutputType.Asset} value={nativeTokenBalance} tag="Dv4TNT" />,
},
]}
/>
+1 -1
View File
@@ -1,6 +1,6 @@
import { useState } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import styled, { css, type AnyStyledComponent } from 'styled-components';
import styled, { type AnyStyledComponent } from 'styled-components';
import { STRING_KEYS } from '@/constants/localization';
+4 -6
View File
@@ -1,21 +1,19 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux';
import { DEFAULT_MARKETID } from '@/constants/markets';
import { layoutMixins } from '@/styles/layoutMixins';
import { VerticalSeparator } from '@/components/Separator';
import { getCurrentMarketAssetData } from '@/state/assetsSelectors';
import { getCurrentMarketId } from '@/state/perpetualsSelectors';
import { MarketsDropdown } from '@/views/MarketsDropdown';
import { MarketStatsDetails } from '@/views/MarketStatsDetails';
import { getCurrentMarketAssetData } from '@/state/assetsSelectors';
import { getCurrentMarketId } from '@/state/perpetualsSelectors';
export const MarketSelectorAndStats = ({ className }: { className?: string }) => {
const { symbol = '' } = useSelector(getCurrentMarketAssetData, shallowEqual) ?? {};
const currentMarketId = useSelector(getCurrentMarketId) ?? DEFAULT_MARKETID;
const currentMarketId = useSelector(getCurrentMarketId);
return (
<Styled.Container className={className}>
+5 -14
View File
@@ -1,16 +1,13 @@
import { useEffect, useRef, useState } from 'react';
import { useRef, useState } from 'react';
import styled, { AnyStyledComponent, css } from 'styled-components';
import { useLocation } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins';
import { TradeLayouts } from '@/constants/layout';
import { useBreakpoints, usePageTitlePriceUpdates } from '@/hooks';
import { setTradeLocation } from '@/state/navigation';
import { useBreakpoints, useCurrentMarketId, usePageTitlePriceUpdates } from '@/hooks';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { getSelectedTradeLayout } from '@/state/layoutSelectors';
@@ -32,18 +29,12 @@ import { TradeBox } from '@/views/TradeBox';
const TradePage = () => {
const tradePageRef = useRef<HTMLDivElement>(null);
const location = useLocation();
const dispatch = useDispatch();
useCurrentMarketId();
const { isTablet } = useBreakpoints();
const tradeLayout = useSelector(getSelectedTradeLayout);
const [isHorizontalPanelOpen, setIsHorizontalPanelOpen] = useState(true);
const canAccountTrade = useSelector(calculateCanAccountTrade);
useEffect(() => {
dispatch(setTradeLocation(location));
}, [location.pathname]);
const [isHorizontalPanelOpen, setIsHorizontalPanelOpen] = useState(true);
usePageTitlePriceUpdates();
+2 -2
View File
@@ -7,13 +7,13 @@ import { STRING_KEYS } from '@/constants/localization';
import { useStringGetter } from '@/hooks';
import { getCurrentMarketId } from '@/state/perpetualsSelectors';
import { Tabs } from '@/components/Tabs';
import { Orderbook, orderbookMixins, OrderbookScrollBehavior } from '@/views/tables/Orderbook';
import { LiveTrades } from '@/views/tables/LiveTrades';
import { getCurrentMarketId } from '@/state/perpetualsSelectors';
enum Tab {
Orderbook = 'Orderbook',
Trades = 'Trades',
+7
View File
@@ -9,6 +9,7 @@ import type {
SubaccountFundingPayments,
Wallet,
SubaccountOrder,
SubaccountTransfers,
HistoricalPnlPeriods,
SubAccountHistoricalPNLs,
} from '@/constants/abacus';
@@ -22,6 +23,7 @@ import { getLocalStorage } from '@/lib/localStorage';
export type AccountState = {
fills?: SubaccountFills;
fundingPayments?: SubaccountFundingPayments;
transfers?: SubaccountTransfers;
clearedOrderIds?: string[];
uncommittedOrderClientIds?: number[];
hasUnseenFillUpdates: boolean;
@@ -38,6 +40,7 @@ export type AccountState = {
const initialState: AccountState = {
fills: undefined,
fundingPayments: undefined,
transfers: undefined,
clearedOrderIds: undefined,
uncommittedOrderClientIds: undefined,
hasUnseenFillUpdates: false,
@@ -81,6 +84,9 @@ export const accountSlice = createSlice({
setFundingPayments: (state, action: PayloadAction<any>) => {
state.fundingPayments = action.payload;
},
setTransfers: (state, action: PayloadAction<any>) => {
state.transfers = action.payload;
},
clearOrder: (state, action: PayloadAction<string>) => ({
...state,
clearedOrderIds: [...(state.clearedOrderIds || []), action.payload],
@@ -148,6 +154,7 @@ export const accountSlice = createSlice({
export const {
setFills,
setFundingPayments,
setTransfers,
clearOrder,
setOnboardingGuard,
setOnboardingState,
+6
View File
@@ -193,6 +193,12 @@ export const getCurrentMarketFills = createSelector(
!currentMarketId ? [] : marketFills[currentMarketId]
);
/**
* @param state
* @returns list of transfers for the currently connected subaccount
*/
export const getSubaccountTransfers = (state: RootState) => state.account?.transfers;
/**
* @param state
* @returns list of funding payments for the currently connected subaccount
-6
View File
@@ -2,13 +2,11 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { AbacusApiStatus } from '@/constants/abacus';
import { DialogTypes } from '@/constants/dialogs';
import { isDydxV4Network } from '@/constants/networks';
import { setApiState, setSelectedNetwork } from '@/state/app';
import { resetPerpetualsState } from '@/state/perpetuals';
import abacusStateManager from '@/lib/abacus';
import squidRouter from '@/lib/squidRouter';
import { openDialog } from './dialogs';
import { getActiveDialog } from './dialogsSelectors';
@@ -22,10 +20,6 @@ export default (store: any) => (next: any) => async (action: PayloadAction<any>)
case setSelectedNetwork.type: {
store.dispatch(resetPerpetualsState());
abacusStateManager.switchNetwork(payload);
if (isDydxV4Network(payload)) {
squidRouter.switchNetwork(payload);
}
break;
}
case setApiState.type: {
+16 -1
View File
@@ -10,6 +10,10 @@ import type {
} from '@/constants/abacus';
import { Candle, RESOLUTION_MAP } from '@/constants/candles';
import { LocalStorageKey } from '@/constants/localStorage';
import { DEFAULT_MARKETID } from '@/constants/markets';
import { getLocalStorage } from '@/lib/localStorage';
interface CandleDataByMarket {
data: Record<string, Candle[]>;
@@ -17,6 +21,7 @@ interface CandleDataByMarket {
}
export interface PerpetualsState {
currentMarketId?: string;
candles: Record<string, CandleDataByMarket>;
liveTrades?: Record<string, MarketTrade[]>;
markets?: Record<string, PerpetualMarket>;
@@ -25,6 +30,7 @@ export interface PerpetualsState {
}
const initialState: PerpetualsState = {
currentMarketId: undefined,
candles: {},
liveTrades: {},
markets: undefined,
@@ -38,6 +44,9 @@ export const perpetualsSlice = createSlice({
name: 'Perpetuals',
initialState,
reducers: {
setCurrentMarketId: (state: PerpetualsState, action: PayloadAction<string>) => {
state.currentMarketId = action.payload;
},
setCandles: (
state: PerpetualsState,
action: PayloadAction<{ candles: Candle[]; marketId: string; resolution: string }>
@@ -116,11 +125,17 @@ export const perpetualsSlice = createSlice({
) => {
state.historicalFundings[action.payload.marketId] = action.payload.historicalFundings;
},
resetPerpetualsState: () => initialState,
resetPerpetualsState: () =>
({
...initialState,
currentMarketId:
getLocalStorage({ key: LocalStorageKey.LastViewedMarket }) ?? DEFAULT_MARKETID,
} as PerpetualsState),
},
});
export const {
setCurrentMarketId,
setCandles,
setLiveTrades,
setMarkets,
+1 -2
View File
@@ -11,8 +11,7 @@ import { mapCandle } from '@/lib/tradingView/utils';
* @param state
* @returns marketId of the market the user is currently viewing
*/
export const getCurrentMarketId = (state: RootState) =>
matchPath(TRADE_ROUTE, state.navigation.tradeLocation.pathname)?.params.market;
export const getCurrentMarketId = (state: RootState) => state.perpetuals.currentMarketId;
/**
* @returns assetId of the currentMarket
+1 -1
View File
@@ -5,7 +5,7 @@ export const headerMixins = {
--trigger-backgroundColor: transparent;
--trigger-textColor: var(--color-text-0);
--trigger-hover-backgroundColor: var(--color-layer-4);
--trigger-hover-backgroundColor: var(--color-layer-3);
--trigger-hover-textColor: var(--color-text-2);
--trigger-open-backgroundColor: var(--color-layer-1);
+3 -1
View File
@@ -61,7 +61,7 @@ export const popoverMixins = {
${layoutMixins.spacedRow}
grid-template-columns: 1fr auto;
padding-right: 1rem;
gap: 0;
border: 1px solid var(--color-layer-6);
`,
@@ -185,6 +185,7 @@ export const popoverMixins = {
--item-checked-backgroundColor: var(--color-layer-2);
--item-checked-textColor: currentColor;
--item-highlighted-backgroundColor: var(--color-layer-2);
--item-highlighted-textColor: var(--color-text-2);
--item-radius: 0px;
@@ -218,6 +219,7 @@ export const popoverMixins = {
&[data-highlighted] // @radix-ui
{
filter: brightness(1.1);
background-color: var(--item-highlighted-backgroundColor);
color: var(--item-highlighted-textColor, var(--trigger-textColor, inherit)) !important;
outline: none;
}
+4
View File
@@ -18,5 +18,9 @@ export const tradeViewMixins: Record<
tbody {
font: var(--font-small-book);
}
thead tr {
box-shadow: none;
}
`,
};
@@ -72,16 +72,26 @@ export const AccountInfoConnectedState = () => {
{!showHeader ? null : (
<Styled.Header>
<span>{stringGetter({ key: STRING_KEYS.ACCOUNT })}</span>
<Styled.Button
state={{ isDisabled: !dydxAccounts }}
onClick={() => dispatch(openDialog({ type: DialogTypes.Deposit }))}
shape={ButtonShape.Pill}
size={ButtonSize.XSmall}
slotLeft={<Icon iconName={IconName.Transfer} />}
>
{stringGetter({ key: STRING_KEYS.MANAGE_FUNDS })}
</Styled.Button>
<Styled.TransferButtons>
{import.meta.env.MODE !== 'production' && (
<Styled.Button
state={{ isDisabled: !dydxAccounts }}
onClick={() => dispatch(openDialog({ type: DialogTypes.Withdraw }))}
shape={ButtonShape.Pill}
size={ButtonSize.XSmall}
>
{stringGetter({ key: STRING_KEYS.WITHDRAW })}
</Styled.Button>
)}
<Styled.Button
state={{ isDisabled: !dydxAccounts }}
onClick={() => dispatch(openDialog({ type: DialogTypes.Deposit }))}
shape={ButtonShape.Pill}
size={ButtonSize.XSmall}
>
{stringGetter({ key: STRING_KEYS.DEPOSIT })}
</Styled.Button>
</Styled.TransferButtons>
</Styled.Header>
)}
<Styled.Stack>
@@ -273,6 +283,11 @@ Styled.Header = styled.header`
padding: 0 1.25rem;
`;
Styled.TransferButtons = styled.div`
${layoutMixins.inlineRow}
gap: 1rem;
`;
Styled.ConnectedAccountInfoContainer = styled.div<{ $showHeader?: boolean }>`
${layoutMixins.column}
${layoutMixins.withOuterAndInnerBorders}
+2 -2
View File
@@ -15,7 +15,7 @@ import { AssetIcon } from '@/components/AssetIcon';
import { Icon, IconName } from '@/components/Icon';
import { Output, OutputType } from '@/components/Output';
import { Popover, TriggerType } from '@/components/Popover';
import { Tag, TagSize } from '@/components/Tag';
import { Tag } from '@/components/Tag';
import { Toolbar } from '@/components/Toolbar';
import { ColumnDef, Table } from '@/components/Table';
@@ -152,7 +152,7 @@ const MarketsDropdownContent = ({ onRowAction }: { onRowAction?: (market: string
);
};
export const MarketsDropdown: React.FC<{ currentMarketId: string; symbol: string | null }> = memo(
export const MarketsDropdown: React.FC<{ currentMarketId?: string; symbol: string | null }> = memo(
({ currentMarketId, symbol = '' }) => {
const [isOpen, setIsOpen] = useState(false);
const stringGetter = useStringGetter();
+2 -2
View File
@@ -1,7 +1,7 @@
import { useState, useCallback } from 'react';
import { useInterval } from '@/hooks';
import { getTimeTillNextUnit, getTimeString } from '@/lib/timeUtils';
import { getTimeTillNextUnit, formatSeconds } from '@/lib/timeUtils';
import { Output, OutputType } from '@/components/Output';
export const NextFundingTimer = () => {
@@ -18,7 +18,7 @@ export const NextFundingTimer = () => {
type={OutputType.Text}
value={
secondsLeft !== undefined
? `${getTimeString(Math.floor(secondsLeft / 60))}:${getTimeString(secondsLeft % 60)}`
? formatSeconds(secondsLeft)
: undefined
}
/>
+46 -33
View File
@@ -1,58 +1,71 @@
import { useCallback } from 'react';
import styled, { AnyStyledComponent } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { TradeInputField } from '@/constants/abacus';
import { TradeTypes, TRADE_TYPE_STRINGS } from '@/constants/trade';
import { TradeTypes } from '@/constants/trade';
import { STRING_KEYS } from '@/constants/localization';
import { useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins';
import { Tabs } from '@/components/Tabs';
import { getInputTradeData } from '@/state/inputsSelectors';
import { getInputTradeData, getInputTradeOptions } from '@/state/inputsSelectors';
import abacusStateManager from '@/lib/abacus';
import { getSelectedTradeType } from '@/lib/tradeData';
import { isTruthy } from '@/lib/isTruthy';
import { TradeForm } from './forms/TradeForm';
export const TradeBoxOrderView = () => {
const useTradeTypeOptions = () => {
const stringGetter = useStringGetter();
const currentTradeData = useSelector(getInputTradeData, shallowEqual);
const { type } = currentTradeData || {};
const selectedTradeType = getSelectedTradeType(type);
const selectedTradeType = useSelector(
createSelector(
[getInputTradeData],
(currentTradeData) => (currentTradeData?.type?.rawValue as TradeTypes) ?? TradeTypes.LIMIT
)
);
const { typeOptions } = useSelector(getInputTradeOptions, shallowEqual) ?? {};
const allTradeTypeItems = typeOptions?.toArray()?.map(({ type, stringKey }) => ({
value: type,
label: stringGetter({ key: stringKey }),
}));
const onTradeTypeChange = (tradeType?: TradeTypes) => {
return {
selectedTradeType,
tradeTypeItems: allTradeTypeItems
? [
allTradeTypeItems?.shift(), // Limit order is always first
allTradeTypeItems?.shift(), // Market order is always second
// All conditional orders labeled under "Stop Order"
allTradeTypeItems?.length && {
label: stringGetter({ key: STRING_KEYS.STOP_ORDER_SHORT }),
subitems: allTradeTypeItems
?.map(
({ value, label }) =>
value && {
value: value as TradeTypes,
label,
disabled: import.meta.env.MODE === 'production', // TODO: Remove this when stop orders are supported on testnet
}
)
.filter(isTruthy),
},
].filter(isTruthy)
: [],
};
};
export const TradeBoxOrderView = () => {
const onTradeTypeChange = useCallback((tradeType?: TradeTypes) => {
if (tradeType) {
abacusStateManager.clearTradeInputValues();
abacusStateManager.setTradeValue({ value: tradeType, field: TradeInputField.type });
}
};
}, []);
const tradeTypeItems = [
{
value: TradeTypes.LIMIT,
label: stringGetter({ key: TRADE_TYPE_STRINGS[TradeTypes.LIMIT].tradeTypeKeyShort }),
},
{
value: TradeTypes.MARKET,
label: stringGetter({ key: TRADE_TYPE_STRINGS[TradeTypes.MARKET].tradeTypeKeyShort }),
},
{
label: stringGetter({ key: STRING_KEYS.STOP_ORDER_SHORT }),
subitems: [
TradeTypes.STOP_LIMIT,
TradeTypes.STOP_MARKET,
TradeTypes.TRAILING_STOP,
TradeTypes.TAKE_PROFIT_LIMIT,
TradeTypes.TAKE_PROFIT_MARKET,
].map((tradeType) => ({
value: tradeType,
label: stringGetter({ key: TRADE_TYPE_STRINGS[tradeType].tradeTypeKeyShort }),
disabled: true,
})),
},
];
const { selectedTradeType, tradeTypeItems } = useTradeTypeOptions();
return (
<Styled.Tabs
+156
View File
@@ -0,0 +1,156 @@
import { useCallback, useState, useMemo } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client';
import { Root, Trigger, Content } from '@radix-ui/react-collapsible';
import { StatusResponse } from '@0xsquid/sdk';
import { useInterval, useStringGetter } from '@/hooks';
import { STRING_KEYS } from '@/constants/localization';
import { formatSeconds } from '@/lib/timeUtils';
import { Output, OutputType } from '@/components/Output';
import { WithReceipt } from '@/components/WithReceipt';
import { Icon, IconName } from '@/components/Icon';
import { TransferStatusSteps } from '@/views/TransferStatusSteps';
import { LoadingDots } from '@/components/Loading/LoadingDots';
import { layoutMixins } from '@/styles/layoutMixins';
type ElementProps = {
toAmount?: number;
triggeredAt?: number;
status?: StatusResponse;
};
export const TransferStatusToast = ({
toAmount,
triggeredAt = Date.now(),
status,
}: ElementProps) => {
const stringGetter = useStringGetter();
const [open, setOpen] = useState<boolean>(false);
const [secondsLeft, setSecondsLeft] = useState<number | undefined>();
const type = useMemo(
() => (status?.toChain?.chainData.chainId === TESTNET_CHAIN_ID ? 'deposit' : 'withdrawal'),
[status]
);
const updateSecondsLeft = useCallback(() => {
const fromChainEta = (status?.fromChain?.chainData?.estimatedRouteDuration || 0) * 1000;
const toChainEta = (status?.toChain?.chainData?.estimatedRouteDuration || 0) * 1000;
setSecondsLeft(Math.floor((triggeredAt + fromChainEta + toChainEta - Date.now()) / 1000));
}, [status]);
useInterval({ callback: updateSecondsLeft });
if (!status) return <LoadingDots size={3} />;
return (
<Styled.Root open={open} onOpenChange={setOpen}>
<WithReceipt
hideReceipt={!open}
side="bottom"
slotReceipt={
<Styled.Receipt>
<TransferStatusSteps status={status} />
</Styled.Receipt>
}
>
<Styled.BridgingStatus>
<Styled.Status>
{stringGetter({
key: type === 'deposit' ? STRING_KEYS.DEPOSIT_STATUS : STRING_KEYS.WITHDRAW_STATUS,
params: {
AMOUNT_USD: <Styled.InlineOutput type={OutputType.Fiat} value={toAmount} />,
ESTIMATED_DURATION: (
<Styled.InlineOutput
type={OutputType.Text}
value={formatSeconds(Math.max(secondsLeft || 0, 0))}
/>
),
},
})}
</Styled.Status>
<Styled.Trigger>
<Styled.TriggerIcon>
<Icon iconName={IconName.Caret} />
</Styled.TriggerIcon>
{stringGetter({ key: open ? STRING_KEYS.HIDE_DETAILS : STRING_KEYS.VIEW_DETAILS })}
</Styled.Trigger>
</Styled.BridgingStatus>
</WithReceipt>
</Styled.Root>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Root = styled(Root)`
margin: 0.5rem -1rem -1rem -1rem;
color: var(--color-text-0);
font: var(--font-small-book);
`;
Styled.Receipt = styled.div`
padding: 0 1rem;
`;
Styled.BridgingStatus = styled.div`
${layoutMixins.flexColumn};
background-color: var(--color-layer-3);
gap: 0.5rem;
padding: 0 1rem 1rem 1rem;
border-radius: 0.5rem;
`;
Styled.Status = styled.div`
color: var(--color-text-0);
font-size: 0.875rem;
`;
Styled.InlineOutput = styled(Output)`
display: inline-block;
color: var(--color-text-1);
`;
Styled.Content = styled(Content)`
${layoutMixins.flexColumn};
gap: 0.5rem;
padding: 1rem;
`;
Styled.Step = styled.div`
${layoutMixins.row};
gap: 0.5rem;
`;
Styled.Trigger = styled(Trigger)`
display: flex;
align-items: center;
gap: 0.5em;
color: var(--color-accent);
user-select: none;
&:focus {
outline: none;
}
`;
Styled.TriggerIcon = styled.span`
width: 0.75em;
display: inline-flex;
transition: transform 0.3s var(--ease-out-expo);
${Styled.Trigger}[data-state='open'] & {
rotate: -0.5turn;
}
`;
+178
View File
@@ -0,0 +1,178 @@
import { useMemo } from 'react';
import styled, { css, keyframes, type AnyStyledComponent } from 'styled-components';
import { StatusResponse } from '@0xsquid/sdk';
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client';
import { useStringGetter } from '@/hooks';
import { Link } from '@/components/Link';
import { Icon, IconName } from '@/components/Icon';
import { LoadingDots } from '@/components/Loading/LoadingDots';
import { LoadingSpinner } from '@/components/Loading/LoadingSpinner';
import { layoutMixins } from '@/styles/layoutMixins';
import { STRING_KEYS } from '@/constants/localization';
type ElementProps = {
status?: StatusResponse;
};
enum TransferStatusStep {
FromChain,
Bridge,
ToChain,
Complete,
}
export const TransferStatusSteps = ({ status }: ElementProps) => {
const stringGetter = useStringGetter();
const type = useMemo(
() => (status?.toChain?.chainData.chainId === TESTNET_CHAIN_ID ? 'deposit' : 'withdrawal'),
[status]
);
const currentStep = useMemo(() => {
const routeStatus = status?.routeStatus;
const fromChain = status?.fromChain?.chainData.chainId;
const toChain = status?.toChain?.chainData.chainId;
if (!routeStatus?.length) return TransferStatusStep.FromChain;
const currentStatus = routeStatus[routeStatus?.length - 1];
if (currentStatus.chainId === toChain) {
return currentStatus.status !== 'success'
? TransferStatusStep.ToChain
: TransferStatusStep.Complete;
}
if (currentStatus.chainId === fromChain && currentStatus.status !== 'success') {
return TransferStatusStep.FromChain;
}
return TransferStatusStep.Bridge;
}, [status]);
const steps = useMemo(
() => [
{
label: stringGetter({
key: type === 'deposit' ? STRING_KEYS.INITIATED_DEPOSIT : STRING_KEYS.INITIATED_WITHDRAWAL,
}),
step: TransferStatusStep.FromChain,
link: status?.fromChain?.transactionUrl,
},
{
label: stringGetter({ key: STRING_KEYS.BRIDGING_TOKENS }),
step: TransferStatusStep.Bridge,
link: status?.axelarTransactionUrl,
},
{
label: stringGetter({
key: type === 'deposit' ? STRING_KEYS.DEPOSIT_TO_CHAIN : STRING_KEYS.WITHDRAW_TO_CHAIN,
params: {
CHAIN: status?.toChain?.chainData.chainName,
}
}),
step: TransferStatusStep.ToChain,
link: status?.toChain?.transactionUrl,
},
],
[status]
);
if (!status) return <LoadingDots size={3} />;
return (
<Styled.BridgingStatus>
{steps.map((step) => (
<Styled.Step key={step.step}>
<Styled.row>
{step.step === currentStep ? (
<Styled.Icon>
<Styled.Spinner />
</Styled.Icon>
) : step.step < currentStep ? (
<Styled.Icon state="complete">
<Icon iconName={IconName.Check} />
</Styled.Icon>
) : (
<Styled.Icon state="default">{step.step + 1}</Styled.Icon>
)}
{step.link && currentStep >= step.step ? (
<Link href={step.link}>
<Styled.Label highlighted={currentStep >= step.step}>
{step.label}
<Icon iconName={IconName.LinkOut} />
</Styled.Label>
</Link>
) : (
<Styled.Label highlighted={currentStep >= step.step}>
{step.label}
</Styled.Label>
)}
</Styled.row>
</Styled.Step>
))}
</Styled.BridgingStatus>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.BridgingStatus = styled.div`
${layoutMixins.flexColumn};
gap: 1rem;
padding: 1rem 0;
`;
Styled.Step = styled.div`
${layoutMixins.spacedRow};
`;
Styled.row = styled.div`
${layoutMixins.inlineRow};
gap: 0.5rem;
`;
Styled.Icon = styled.div<{ state: 'complete' | 'default' }>`
display: flex;
align-items: center;
justify-content: center;
height: 2rem;
width: 2rem;
border-radius: 50%;
background-color: var(--color-layer-3);
${({ state }) =>
({
['complete']: css`
color: var(--color-positive);
`,
['default']: css`
color: var(--color-text-0);
`,
}[state])}
`;
Styled.Spinner = styled(LoadingSpinner)`
--spinner-width: 1.25rem;
color: var(--color-accent);
`;
Styled.Label = styled(Styled.row)<{ highlighted?: boolean }>`
${({ highlighted }) =>
highlighted
? css`
color: var(--color-text-2);
`
: css`
color: var(--color-text-0);
`}
`;
+4 -4
View File
@@ -6,8 +6,8 @@ import { useBreakpoints, useStringGetter } from '@/hooks';
import { Dialog, DialogPlacement } from '@/components/Dialog';
import { DepositForm } from '@/views/forms/DepositForm';
import { TestnetDepositForm } from '@/views/forms/TestnetDepositForm';
import { DepositForm } from '@/views/forms/AccountManagementForms/DepositForm';
import { TestnetDepositForm } from '@/views/forms/AccountManagementForms/TestnetDepositForm';
import { layoutMixins } from '@/styles/layoutMixins';
@@ -32,12 +32,12 @@ export const DepositDialog = ({ setIsOpen }: ElementProps) => {
>
<Styled.Content>
{showTestDeposit ? <TestnetDepositForm onDeposit={closeDialog} /> : <DepositForm />}
{/* TODO TRCL-1693 - uncomment when deposit form is ready
{/* TODO TRCL-1693 - uncomment when deposit form is ready */}
{import.meta.env.MODE !== 'production' && (
<Styled.TextToggle onClick={() => setShowTestDeposit(!showTestDeposit)}>
{showTestDeposit ? 'Show deposit form (Under Construction)' : 'Show test faucet'}
</Styled.TextToggle>
)} */}
)}
</Styled.Content>
</Dialog>
);
@@ -35,7 +35,7 @@ import {
type ElementProps = {
orderId: string;
setIsOpen?: (open: boolean) => void;
setIsOpen: (open: boolean) => void;
};
export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
+2 -14
View File
@@ -9,7 +9,7 @@ import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins';
import { AlertMessage } from '@/components/AlertMessage';
import { Button } from '@/components/Button';
import { CopyButton } from '@/components/CopyButton';
import { Dialog } from '@/components/Dialog';
import { Checkbox } from '@/components/Checkbox';
import { Icon, IconName } from '@/components/Icon';
@@ -30,21 +30,12 @@ export const MnemonicExportDialog = ({ setIsOpen }: ElementProps) => {
const [hasAcknowledged, setHasAcknowledged] = useState(false);
const [currentStep, setCurrentStep] = useState(MnemonicExportStep.AcknowledgeRisk);
const [isShowing, setIsShowing] = useState(false);
const [copied, setCopied] = useState(false);
const stringGetter = useStringGetter();
const { hdKey } = useAccounts();
const { mnemonic } = hdKey ?? {};
const onCopy = () => {
setCopied(true);
if (mnemonic) {
navigator.clipboard.writeText(mnemonic);
}
setTimeout(() => setCopied(false), 500);
};
const title = {
[MnemonicExportStep.AcknowledgeRisk]: stringGetter({ key: STRING_KEYS.REVEAL_SECRET_PHRASE }),
[MnemonicExportStep.DisplayMnemonic]: stringGetter({ key: STRING_KEYS.EXPORT_SECRET_PHRASE }),
@@ -116,10 +107,7 @@ export const MnemonicExportDialog = ({ setIsOpen }: ElementProps) => {
</Styled.WordList>
}
>
<Button action={copied ? ButtonAction.Create : ButtonAction.Primary} onClick={onCopy}>
<Icon iconName={IconName.Copy} />
{stringGetter({ key: copied ? STRING_KEYS.COPIED : STRING_KEYS.COPY })}
</Button>
<CopyButton value={mnemonic} />
</WithReceipt>
</>
),
+1 -1
View File
@@ -19,7 +19,7 @@ import { GreenCheckCircle } from '@/components/GreenCheckCircle';
import { Icon } from '@/components/Icon';
import { Ring } from '@/components/Ring';
import { TestnetDepositForm } from '@/views/forms/TestnetDepositForm';
import { TestnetDepositForm } from '@/views/forms/AccountManagementForms/TestnetDepositForm';
import { track } from '@/lib/analytics';
+7 -18
View File
@@ -1,7 +1,6 @@
import { useState } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { ButtonAction } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { DydxChainAsset } from '@/constants/wallets';
@@ -10,9 +9,8 @@ import { layoutMixins } from '@/styles/layoutMixins';
import { useAccounts, useStringGetter } from '@/hooks';
import { AssetIcon } from '@/components/AssetIcon';
import { Button } from '@/components/Button';
import { CopyButton } from '@/components/CopyButton';
import { Dialog } from '@/components/Dialog';
import { Icon, IconName } from '@/components/Icon';
import { QrCode } from '@/components/QrCode';
import { SelectItem, SelectMenu } from '@/components/SelectMenu';
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
@@ -22,22 +20,15 @@ import { truncateAddress } from '@/lib/wallet';
import { OnboardingTriggerButton } from './OnboardingTriggerButton';
type ElementProps = {
selectedAsset?: DydxChainAsset;
setIsOpen: (open: boolean) => void;
};
export const ReceiveDialog = ({ setIsOpen }: ElementProps) => {
export const ReceiveDialog = ({ selectedAsset = DydxChainAsset.DYDX, setIsOpen }: ElementProps) => {
const stringGetter = useStringGetter();
const { dydxAddress } = useAccounts();
const [asset, setAsset] = useState(DydxChainAsset.DYDX);
const [copied, setCopied] = useState(false);
const onCopy = () => {
if (!dydxAddress) return;
setCopied(true);
navigator.clipboard.writeText(dydxAddress);
setTimeout(() => setCopied(false), 500);
};
const [asset, setAsset] = useState(selectedAsset);
const assetOptions = [
{
@@ -52,7 +43,8 @@ export const ReceiveDialog = ({ setIsOpen }: ElementProps) => {
value: DydxChainAsset.DYDX,
label: (
<Styled.InlineRow>
<AssetIcon symbol="DYDX" /> DYDX
{/* <AssetIcon symbol="DYDX" /> */}
Dv4TNT
</Styled.InlineRow>
),
},
@@ -87,10 +79,7 @@ export const ReceiveDialog = ({ setIsOpen }: ElementProps) => {
>
<QrCode hasLogo value={dydxAddress!} />
</Styled.WithDetailsReceipt>
<Button action={copied ? ButtonAction.Create : ButtonAction.Primary} onClick={onCopy}>
<Icon iconName={IconName.Copy} />
{stringGetter({ key: copied ? STRING_KEYS.COPIED : STRING_KEYS.COPY_ADDRESS })}
</Button>
<CopyButton value={dydxAddress} />
</>
)}
</Styled.Content>
+4 -2
View File
@@ -1,21 +1,23 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { STRING_KEYS } from '@/constants/localization';
import { DydxChainAsset } from '@/constants/wallets';
import { useStringGetter } from '@/hooks';
import { Dialog } from '@/components/Dialog';
import { TransferForm } from '@/views/forms/TransferForm';
type ElementProps = {
selectedAsset?: DydxChainAsset;
setIsOpen?: (open: boolean) => void;
};
export const TransferDialog = ({ setIsOpen }: ElementProps) => {
export const TransferDialog = ({ selectedAsset, setIsOpen }: ElementProps) => {
const stringGetter = useStringGetter();
return (
<Styled.Dialog isOpen setIsOpen={setIsOpen} title={stringGetter({ key: STRING_KEYS.TRANSFER })}>
<TransferForm onDone={() => setIsOpen?.(false)} />
<TransferForm selectedAsset={selectedAsset} onDone={() => setIsOpen?.(false)} />
</Styled.Dialog>
);
};
+56
View File
@@ -0,0 +1,56 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { STRING_KEYS } from '@/constants/localization';
import { useBreakpoints, useStringGetter } from '@/hooks';
import { Dialog, DialogPlacement } from '@/components/Dialog';
import { WithdrawForm } from '@/views/forms/AccountManagementForms/WithdrawForm';
import { layoutMixins } from '@/styles/layoutMixins';
type ElementProps = {
setIsOpen: (open: boolean) => void;
};
export const WithdrawDialog = ({ setIsOpen }: ElementProps) => {
const stringGetter = useStringGetter();
const { isTablet } = useBreakpoints();
return (
<Dialog
isOpen
setIsOpen={setIsOpen}
title={stringGetter({ key: STRING_KEYS.WITHDRAW })}
placement={isTablet ? DialogPlacement.FullScreen : DialogPlacement.Default}
>
<Styled.Content>
<WithdrawForm />
</Styled.Content>
</Dialog>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.TextToggle = styled.div`
${layoutMixins.stickyFooter}
color: var(--color-accent);
cursor: pointer;
margin-top: auto;
&:hover {
text-decoration: underline;
}
`;
Styled.Content = styled.div`
${layoutMixins.stickyArea0}
--stickyArea0-bottomHeight: 2rem;
--stickyArea0-bottomGap: 1rem;
--stickyArea0-totalInsetBottom: 0.5rem;
${layoutMixins.flexColumn}
gap: 1rem;
`;
@@ -1,31 +1,45 @@
import type { ChainData } from '@0xsquid/sdk';
import styled, { type AnyStyledComponent } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux';
import { TransferType } from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization';
import { useStringGetter } from '@/hooks';
import { WithLabel } from '@/components/WithLabel';
import { SearchSelectMenu } from '@/components/SearchSelectMenu';
import { layoutMixins } from '@/styles/layoutMixins';
import { popoverMixins } from '@/styles/popoverMixins';
import { getTransferInputs } from '@/state/inputsSelectors';
type ElementProps = {
chains?: ChainData[];
currentChain?: ChainData;
setCurrentChain: (chain: ChainData) => void;
label?: string;
selectedChain?: string;
onSelectChain: (chain: string) => void;
};
export const ChainSelectMenu = ({ chains = [], currentChain, setCurrentChain }: ElementProps) => {
export const ChainSelectMenu = ({
label,
selectedChain,
onSelectChain,
}: ElementProps) => {
const stringGetter = useStringGetter();
const { type, depositOptions, withdrawalOptions, resources } =
useSelector(getTransferInputs, shallowEqual) || {};
const chains =
(type === TransferType.deposit ? depositOptions : withdrawalOptions)?.chains?.toArray() || [];
const chainItems = Object.values(chains).map((chain: ChainData) => ({
value: chain.chainId.toString(),
label: chain.chainName,
onSelect: () => setCurrentChain(chain),
slotBefore: <Styled.Img src={chain.chainIconURI} alt="" />,
const chainItems = Object.values(chains).map((chain) => ({
value: chain.type,
label: chain.stringKey,
onSelect: () => {
onSelectChain(chain.type);
},
slotBefore: <Styled.Img src={chain.iconUrl} alt="" />,
}));
const selectedOption = chains.find((item) => item.type === selectedChain);
return (
<SearchSelectMenu
items={[
@@ -35,12 +49,12 @@ export const ChainSelectMenu = ({ chains = [], currentChain, setCurrentChain }:
items: chainItems,
},
]}
label="Source"
label={label || (type === TransferType.deposit ? 'Source' : 'Destination')}
>
<Styled.ChainRow>
{currentChain ? (
{selectedChain ? (
<>
<Styled.Img src={currentChain?.chainIconURI} alt="" /> {currentChain?.chainName}
<Styled.Img src={selectedOption?.iconUrl} alt="" /> {selectedOption?.stringKey}
</>
) : (
stringGetter({ key: STRING_KEYS.SELECT_CHAIN })
@@ -67,4 +81,4 @@ Styled.ChainRow = styled.div`
gap: 0.5rem;
color: var(--color-text-2);
font: var(--font-base-book);
`;
`;
@@ -0,0 +1,335 @@
import { type FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { type NumberFormatValues } from 'react-number-format';
import { shallowEqual, useSelector } from 'react-redux';
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client';
import { ethers } from 'ethers';
import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus';
import { AlertType } from '@/constants/alerts';
import { ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { NumberSign } from '@/constants/numbers';
import { useAccounts, useDebounce, useStringGetter } from '@/hooks';
import { useAccountBalance } from '@/hooks/useAccountBalance';
import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins';
import { AlertMessage } from '@/components/AlertMessage';
import { Button } from '@/components/Button';
import { DiffOutput } from '@/components/DiffOutput';
import { FormInput } from '@/components/FormInput';
import { InputType } from '@/components/Input';
import { Link } from '@/components/Link';
import { LoadingSpace } from '@/components/Loading/LoadingSpinner';
import { OutputType } from '@/components/Output';
import { Tag } from '@/components/Tag';
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
import { getTransferInputs } from '@/state/inputsSelectors';
import abacusStateManager from '@/lib/abacus';
import { MustBigNumber } from '@/lib/numbers';
import { ChainSelectMenu } from './ChainSelectMenu';
import { TokenSelectMenu } from './TokenSelectMenu';
import { DepositButtonAndReceipt } from './DepositForm/DepositButtonAndReceipt';
type DepositFormProps = {
onDeposit?: () => void;
onError?: () => void;
};
export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const stringGetter = useStringGetter();
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(false);
const { signerWagmi } = useAccounts();
const { addTransferNotification } = useLocalNotifications();
const {
requestPayload,
token,
chain: chainIdStr,
resources,
summary,
} = useSelector(getTransferInputs, shallowEqual) || {};
const chainId = chainIdStr ? parseInt(chainIdStr) : undefined;
// User inputs
const sourceToken = useMemo(
() => (token ? resources?.tokenResources?.get(token) : undefined),
[token, resources]
);
const [fromAmount, setFromAmount] = useState('');
const [slippage, setSlippage] = useState(0.01); // 1% slippage
const debouncedAmount = useDebounce<string>(fromAmount, 500);
// Async Data
const { balance, queryStatus, isQueryFetching } = useAccountBalance({
addressOrDenom: sourceToken?.address || undefined,
assetSymbol: sourceToken?.symbol || undefined,
chainId: chainId,
decimals: sourceToken?.decimals || undefined,
isCosmosChain: false,
});
// BN
const debouncedAmountBN = MustBigNumber(debouncedAmount);
const balanceBN = MustBigNumber(balance);
useEffect(() => {
const hasInvalidInput =
debouncedAmountBN.isNaN() || debouncedAmountBN.lte(0) || debouncedAmountBN.gte(balanceBN);
abacusStateManager.setTransferValue({
value: hasInvalidInput ? 0 : debouncedAmount,
field: TransferInputField.size,
});
setError(null);
}, [debouncedAmountBN.toNumber()]);
useEffect(() => {
abacusStateManager.setTransferValue({
field: TransferInputField.type,
value: TransferType.deposit.rawValue,
});
return () => {
abacusStateManager.clearTransferInputValues();
abacusStateManager.setTransferValue({
field: TransferInputField.type,
value: null,
});
};
}, []);
useEffect(() => {
if (error) onError?.();
}, [error]);
const onSelectChain = useCallback((chain: string) => {
if (chain) {
abacusStateManager.clearTransferInputValues();
abacusStateManager.setTransferValue({
field: TransferInputField.chain,
value: chain,
});
setFromAmount('');
}
}, []);
const onSelectToken = useCallback((token: TransferInputTokenResource) => {
if (token) {
abacusStateManager.clearTransferInputValues();
abacusStateManager.setTransferValue({
field: TransferInputField.token,
value: token.address,
});
setFromAmount('');
}
}, []);
const onChangeAmount = useCallback(
({ value }: NumberFormatValues) => {
setFromAmount(value);
},
[setFromAmount]
);
const onSetSlippage = useCallback(
(newSlippage: number) => {
setSlippage(newSlippage);
// TODO: to be implemented via abacus
},
[setSlippage, debouncedAmount]
);
const onClickMax = useCallback(() => {
if (balance) {
setFromAmount(balanceBN.toString());
}
}, [balance, setFromAmount]);
const onSubmit = useCallback(
async (e: FormEvent) => {
try {
e.preventDefault();
if (!signerWagmi) {
throw new Error('Missing signer');
}
if (
!requestPayload?.targetAddress ||
!requestPayload.data ||
!requestPayload.value ||
!requestPayload.gasLimit ||
!requestPayload.gasPrice ||
!requestPayload.routeType
) {
throw new Error('Missing request payload');
}
setIsLoading(true);
let tx = {
to: requestPayload.targetAddress as `0x${string}`,
data: requestPayload.data as `0x${string}`,
gasLimit: ethers.toBigInt(requestPayload.gasLimit),
value:
requestPayload.routeType !== 'SEND' ? ethers.toBigInt(requestPayload.value) : undefined,
};
const txHash = await signerWagmi.sendTransaction(tx);
onDeposit?.();
if (txHash) {
addTransferNotification({
txHash,
toChainId: TESTNET_CHAIN_ID,
fromChainId: chainIdStr || undefined,
toAmount: summary?.usdcSize || undefined,
triggeredAt: Date.now(),
});
abacusStateManager.clearTransferInputValues();
setFromAmount('');
}
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
},
[requestPayload, signerWagmi, chainId]
);
const amountInputReceipt = [
{
key: 'amount',
label: (
<span>
{stringGetter({ key: STRING_KEYS.AVAILABLE })}{' '}
{sourceToken && <Tag>{sourceToken.symbol}</Tag>}
</span>
),
value: (
<DiffOutput
type={OutputType.Asset}
value={balance}
newValue={balanceBN.minus(debouncedAmountBN).toString()}
sign={NumberSign.Negative}
hasInvalidNewValue={balanceBN.minus(debouncedAmountBN).isNegative()}
withDiff={
Boolean(fromAmount && balance) &&
!debouncedAmountBN.isNaN() &&
!debouncedAmountBN.isZero()
}
/>
),
},
];
const errorMessage = useMemo(() => {
if (error) {
return error?.message
? stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: { ERROR_MESSAGE: error.message },
})
: stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG });
}
if (fromAmount) {
if (!chainId) {
return stringGetter({ key: STRING_KEYS.MUST_SPECIFY_CHAIN });
} else if (!sourceToken) {
return stringGetter({ key: STRING_KEYS.MUST_SPECIFY_ASSET });
}
}
if (MustBigNumber(fromAmount).gt(MustBigNumber(balance))) {
return stringGetter({ key: STRING_KEYS.DEPOSIT_MORE_THAN_BALANCE });
}
return undefined;
}, [error, balance, chainId, fromAmount, sourceToken]);
const isDisabled =
Boolean(errorMessage) ||
!sourceToken ||
!chainId ||
debouncedAmountBN.isNaN() ||
debouncedAmountBN.isZero();
if (!resources) {
return <LoadingSpace id="DepositForm" />;
}
return (
<Styled.Form onSubmit={onSubmit}>
<ChainSelectMenu selectedChain={chainIdStr || undefined} onSelectChain={onSelectChain} />
<TokenSelectMenu selectedToken={sourceToken || undefined} onSelectToken={onSelectToken} />
<Styled.WithDetailsReceipt side="bottom" detailItems={amountInputReceipt}>
<FormInput
type={InputType.Number}
onChange={onChangeAmount}
label={stringGetter({ key: STRING_KEYS.AMOUNT })}
value={fromAmount}
slotRight={
<Styled.FormInputButton size={ButtonSize.XSmall} onClick={onClickMax}>
{stringGetter({ key: STRING_KEYS.MAX })}
</Styled.FormInputButton>
}
/>
</Styled.WithDetailsReceipt>
{errorMessage && <AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>}
<DepositButtonAndReceipt
isDisabled={isDisabled}
isLoading={isLoading}
chainId={chainId || undefined}
setSlippage={onSetSlippage}
slippage={slippage}
sourceToken={sourceToken || undefined}
/>
</Styled.Form>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Form = styled.form`
--form-input-height: 3.5rem;
${layoutMixins.flexColumn}
gap: 1.5rem;
${layoutMixins.stickyArea1}
min-height: calc(100% - var(--stickyArea0-bottomHeight));
`;
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
--withReceipt-backgroundColor: var(--color-layer-2);
`;
Styled.Link = styled(Link)`
color: var(--color-accent);
&:visited {
color: var(--color-accent);
}
`;
Styled.TransactionInfo = styled.span`
${layoutMixins.row}
`;
Styled.FormInputButton = styled(Button)`
${formMixins.inputInnerButton}
`;
@@ -1,8 +1,7 @@
import { type Dispatch, type SetStateAction, useState } from 'react';
import styled, { type AnyStyledComponent, css } from 'styled-components';
import { type Dispatch, type SetStateAction, useState, type ReactNode } from 'react';
import styled, { type AnyStyledComponent, } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux';
import type { RouteData, TokenData } from '@0xsquid/sdk';
import BigNumber from 'bignumber.js';
import type { RouteData } from '@0xsquid/sdk';
import {
ButtonAction,
@@ -12,6 +11,7 @@ import {
ButtonType,
} from '@/constants/buttons';
import { TransferInputTokenResource } from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization';
import { NumberSign } from '@/constants/numbers';
@@ -30,11 +30,9 @@ import { ToggleButton } from '@/components/ToggleButton';
import { WithReceipt } from '@/components/WithReceipt';
import { getSubaccountBuyingPower, getSubaccountEquity } from '@/state/accountSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
import { BIG_NUMBERS, MustBigNumber } from '@/lib/numbers';
import squidRouter from '@/lib/squidRouter';
import { SlippageEditor } from './SlippageEditor';
import { SlippageEditor } from '../SlippageEditor';
type ElementProps = {
isDisabled?: boolean;
@@ -43,8 +41,9 @@ type ElementProps = {
chainId?: string | number;
setError?: Dispatch<SetStateAction<Error | undefined>>;
slippage: number;
slotError?: ReactNode;
setSlippage: (slippage: number) => void;
sourceToken?: TokenData;
sourceToken?: TransferInputTokenResource;
squidRoute?: RouteData;
};
@@ -54,10 +53,10 @@ export const DepositButtonAndReceipt = ({
slippage,
setSlippage,
sourceToken,
squidRoute,
isDisabled,
isLoading,
slotError,
}: ElementProps) => {
const [showFeeBreakdown, setShowFeeBreakdown] = useState(false);
const [isEditingSlippage, setIsEditingSlipapge] = useState(false);
@@ -78,35 +77,23 @@ export const DepositButtonAndReceipt = ({
const { current: buyingPower, postOrder: newBuyingPower } =
useSelector(getSubaccountBuyingPower, shallowEqual) || {};
const { gasCosts, feeCosts } = squidRoute?.estimate || {};
const { summary, requestPayload } = useSelector(getTransferInputs, shallowEqual) || {};
const totalGasCosts =
gasCosts?.reduce(
(acc: BigNumber, { amountUSD }: { amountUSD: string }) => acc.plus(MustBigNumber(amountUSD)),
BIG_NUMBERS.ZERO
) || BIG_NUMBERS.ZERO;
const feeSubitems: DetailsItem[] = [];
const totalFeeCosts =
feeCosts?.reduce(
(acc: BigNumber, { amountUSD }: { amountUSD: string }) => acc.plus(MustBigNumber(amountUSD)),
BIG_NUMBERS.ZERO
) || BIG_NUMBERS.ZERO;
const totalFees = totalGasCosts.plus(totalFeeCosts);
const feeSubitems: DetailsItem[] = feeCosts
? feeCosts.map(({ amountUSD, name }: { amountUSD: string; name: string }) => ({
key: name,
label: <span>{name}</span>,
value: <Output type={OutputType.Fiat} value={amountUSD} />,
}))
: [];
if (gasCosts) {
if (typeof summary?.gasFee === 'number') {
feeSubitems.push({
key: 'gas-fees',
label: <span>{stringGetter({ key: STRING_KEYS.GAS_FEE })}</span>,
value: <Output type={OutputType.Fiat} value={totalGasCosts} />,
value: <Output type={OutputType.Fiat} value={summary?.gasFee} />,
});
}
if (typeof summary?.bridgeFee === 'number') {
feeSubitems.push({
key: 'bridge-fees',
label: <span>Bridge Fee</span>,
value: <Output type={OutputType.Fiat} value={summary?.bridgeFee} />,
});
}
@@ -128,9 +115,9 @@ export const DepositButtonAndReceipt = ({
<DiffOutput
type={OutputType.Fiat}
value={equity}
newValue={newEquity}
newValue={newEquity} // using toAmountUSD as a proxy for equity until Abacus supports accounts with no funds.
sign={NumberSign.Positive}
withDiff={Boolean(newEquity) && equity !== newEquity}
withDiff={equity !== newEquity}
/>
),
},
@@ -154,27 +141,20 @@ export const DepositButtonAndReceipt = ({
{
key: 'exchange-rate',
label: <span>{stringGetter({ key: STRING_KEYS.EXCHANGE_RATE })}</span>,
value: sourceToken && squidRoute && (
value: typeof summary?.exchangeRate === 'number' && (
<Styled.ExchangeRate>
<Output
type={OutputType.Asset}
value={1}
fractionDigits={0}
tag={<Tag>{sourceToken?.symbol}</Tag>}
/>
<Output type={OutputType.Asset} value={1} fractionDigits={0} tag={sourceToken?.symbol} />
=
<Output
type={OutputType.Asset}
value={squidRoute?.estimate?.exchangeRate}
tag={<Tag>{squidRouter.SQUID_ROUTE_DEFAULTS.toToken.toUpperCase()}</Tag>}
/>
<Output type={OutputType.Asset} value={summary?.exchangeRate} tag="USDC" />
</Styled.ExchangeRate>
),
},
{
key: 'total-fees',
label: <span>{stringGetter({ key: STRING_KEYS.TOTAL_FEES })}</span>,
value: squidRoute && <Output type={OutputType.Fiat} value={totalFees} />,
value: typeof summary?.bridgeFee === 'number' && typeof summary?.gasFee === 'number' && (
<Output type={OutputType.Fiat} value={summary?.bridgeFee + summary?.gasFee} />
),
subitems: feeSubitems,
},
{
@@ -212,17 +192,25 @@ export const DepositButtonAndReceipt = ({
</Styled.DetailButtons>
</Styled.CollapsibleDetails>
}
slotError={slotError}
>
{!isMatchingNetwork ? (
<Button
action={ButtonAction.Primary}
onClick={switchNetwork}
state={{ isLoading: isSwitchingNetwork || isLoading }}
state={{ isLoading: isSwitchingNetwork }}
>
{stringGetter({ key: STRING_KEYS.SWITCH_NETWORK })}
</Button>
) : (
<Button type={ButtonType.Submit} state={{ isDisabled: !isFormValid, isLoading }}>
<Button
action={ButtonAction.Primary}
type={ButtonType.Submit}
state={{
isDisabled: !isFormValid,
isLoading: (isFormValid && !requestPayload) || isLoading,
}}
>
{stringGetter({ key: STRING_KEYS.DEPOSIT_FUNDS })}
</Button>
)}
@@ -234,7 +222,7 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.ExchangeRate = styled.span`
${layoutMixins.row}
gap: 1ch;
gap: 0.5ch;
`;
Styled.WithReceipt = styled(WithReceipt)`
@@ -12,6 +12,7 @@ import { Icon, IconName } from '@/components/Icon';
import { Input, InputType } from '@/components/Input';
import { Output, OutputType } from '@/components/Output';
import { ToggleGroup } from '@/components/ToggleGroup';
import { FormInput } from '@/components/FormInput';
enum EditorState {
Viewing = 'Viewing',
@@ -115,7 +116,7 @@ export const SlippageEditor = ({ slippage, setIsEditing, setSlippage }: Slippage
</Styled.SlippageInput>
),
[EditorState.Editing]: (
<Styled.Input
<Styled.FormInput
type={InputType.Percent}
value={slippageInputValue}
tabIndex={0}
@@ -151,10 +152,8 @@ Styled.SlippageInput = styled.div`
border-radius: 0.5em;
`;
Styled.Input = styled(Input)`
// TODO(@aforaleka): replace with input container
--input-height: 1.5rem;
--input-backgroundColor: var(--color-layer-5);
Styled.FormInput = styled(FormInput)`
--form-input-height: 1.5rem;
input {
text-align: end;
@@ -0,0 +1,95 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux';
import { STRING_KEYS } from '@/constants/localization';
import { TransferInputTokenResource, TransferType } from '@/constants/abacus';
import { useStringGetter } from '@/hooks';
import { DiffArrow } from '@/components/DiffArrow';
import { Icon, IconName } from '@/components/Icon';
import { SearchSelectMenu } from '@/components/SearchSelectMenu';
import { Tag } from '@/components/Tag';
import { layoutMixins } from '@/styles/layoutMixins';
import { getTransferInputs } from '@/state/inputsSelectors';
type ElementProps = {
selectedToken?: TransferInputTokenResource;
onSelectToken: (token: TransferInputTokenResource) => void;
};
export const TokenSelectMenu = ({ selectedToken, onSelectToken }: ElementProps) => {
const stringGetter = useStringGetter();
const { type, depositOptions, withdrawalOptions, resources } =
useSelector(getTransferInputs, shallowEqual) || {};
const tokens =
(type === TransferType.deposit ? depositOptions : withdrawalOptions)?.assets?.toArray() || [];
const tokenItems = Object.values(tokens).map((token) => ({
value: token.type,
label: token.stringKey,
onSelect: () => {
const selectedToken = resources?.tokenResources?.get(token.type);
selectedToken && onSelectToken(selectedToken);
},
slotBefore: <Styled.Img src={token.iconUrl} alt="" />,
}));
return (
<SearchSelectMenu
items={[
{
group: 'assets',
groupLabel: stringGetter({ key: STRING_KEYS.ASSET }),
items: tokenItems,
},
]}
label={stringGetter({ key: STRING_KEYS.ASSET })}
withReceiptItems={[
{
key: 'swap',
label: stringGetter({ key: STRING_KEYS.SWAP }),
value: selectedToken && (
<>
<Tag>{type === TransferType.deposit ? selectedToken?.symbol : 'USDC'}</Tag>
<DiffArrow />
<Tag>{type === TransferType.deposit ? 'USDC' : selectedToken?.symbol}</Tag>
</>
),
tooltip: 'swap',
},
]}
>
<Styled.AssetRow>
{selectedToken ? (
<>
<Styled.Img src={selectedToken?.iconUrl} alt="" /> {selectedToken?.name}{' '}
<Tag>{selectedToken?.symbol}</Tag>
</>
) : (
stringGetter({ key: STRING_KEYS.SELECT_ASSET })
)}
</Styled.AssetRow>
</SearchSelectMenu>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.AssetRow = styled.div`
${layoutMixins.row}
gap: 0.5rem;
color: var(--color-text-2);
font: var(--font-base-book);
`;
Styled.Img = styled.img`
width: 1.25rem;
height: 1.25rem;
border-radius: 50%;
`;
Styled.Icon = styled(Icon)`
height: 0.5rem;
`;
@@ -0,0 +1,352 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { ChangeEvent, FormEvent } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import type { NumberFormatValues } from 'react-number-format';
import { shallowEqual, useSelector } from 'react-redux';
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client';
import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus';
import { AlertType } from '@/constants/alerts';
import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { NumberSign, QUANTUM_MULTIPLIER } from '@/constants/numbers';
import { useDebounce, useStringGetter, useSubaccount } from '@/hooks';
import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { layoutMixins } from '@/styles/layoutMixins';
import { AlertMessage } from '@/components/AlertMessage';
import { Button } from '@/components/Button';
import { DiffOutput } from '@/components/DiffOutput';
import { FormInput } from '@/components/FormInput';
import { InputType } from '@/components/Input';
import { Link } from '@/components/Link';
import { OutputType } from '@/components/Output';
import { Tag } from '@/components/Tag';
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
import { ChainSelectMenu } from '@/views/forms/AccountManagementForms/ChainSelectMenu';
import { getSubaccount } from '@/state/accountSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
import abacusStateManager from '@/lib/abacus';
import { MustBigNumber } from '@/lib/numbers';
import { TokenSelectMenu } from './TokenSelectMenu';
import { WithdrawButtonAndReceipt } from './WithdrawForm/WithdrawButtonAndReceipt';
export const WithdrawForm = () => {
const stringGetter = useStringGetter();
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(false);
const { simulateWithdraw, sendSquidWithdraw } = useSubaccount();
const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {};
// User input
const [withdrawAmount, setWithdrawAmount] = useState('');
const [slippage, setSlippage] = useState(0.01); // 0.1% slippage
const debouncedAmount = useDebounce<string>(withdrawAmount, 500);
const {
requestPayload,
token,
chain: chainIdStr,
address: toAddress,
resources,
} = useSelector(getTransferInputs, shallowEqual) || {};
const toToken = useMemo(
() => (token ? resources?.tokenResources?.get(token) : undefined),
[token, resources]
);
const { addTransferNotification } = useLocalNotifications();
// Async Data
const debouncedAmountBN = MustBigNumber(debouncedAmount);
const withdrawAmountBN = MustBigNumber(withdrawAmount);
const freeCollateralBN = MustBigNumber(freeCollateral?.current);
useEffect(() => {
abacusStateManager.setTransferValue({
field: TransferInputField.type,
value: TransferType.withdrawal.rawValue,
});
return () => {
abacusStateManager.clearTransferInputValues();
abacusStateManager.setTransferValue({
field: TransferInputField.type,
value: null,
});
};
}, []);
useEffect(() => {
const setTransferValue = async () => {
try {
setIsLoading(true);
const hasInvalidInput = debouncedAmountBN.isNaN() || debouncedAmountBN.lte(0);
if (hasInvalidInput) {
abacusStateManager.setTransferValue({
value: 0,
field: TransferInputField.usdcSize,
});
} else {
const stdFee = await simulateWithdraw(parseFloat(debouncedAmount));
const amount =
parseFloat(debouncedAmount) -
parseFloat(stdFee?.amount[0]?.amount || '0') / QUANTUM_MULTIPLIER;
abacusStateManager.setTransferValue({
value: amount.toString(),
field: TransferInputField.usdcSize,
});
setError(null);
}
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
};
setTransferValue();
}, [debouncedAmountBN.toNumber()]);
const onSubmit = useCallback(
async (e: FormEvent) => {
try {
e.preventDefault();
if (!requestPayload?.data || !debouncedAmountBN.toNumber()) {
throw new Error('Invalid request payload');
}
setIsLoading(true);
const txHash = await sendSquidWithdraw(debouncedAmountBN.toNumber(), requestPayload?.data);
if (txHash?.hash) {
const hash = `0x${Buffer.from(txHash.hash).toString('hex')}`;
setTransactionHash(hash);
addTransferNotification({
txHash: hash,
fromChainId: TESTNET_CHAIN_ID,
toChainId: chainIdStr || undefined,
toAmount: debouncedAmountBN.toNumber(),
triggeredAt: Date.now(),
});
abacusStateManager.clearTransferInputValues();
setWithdrawAmount('');
}
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
},
[setTransactionHash, requestPayload, debouncedAmountBN, chainIdStr]
);
const onChangeAddress = useCallback((e: ChangeEvent<HTMLInputElement>) => {
abacusStateManager.setTransferValue({
field: TransferInputField.address,
value: e.target.value,
});
}, []);
const onChangeAmount = useCallback(
({ value }: NumberFormatValues) => {
setWithdrawAmount(value);
},
[setWithdrawAmount]
);
const onSetSlippage = useCallback(
(newSlippage: number) => {
setSlippage(newSlippage);
// TODO: to be implemented via abacus
// if (MustBigNumber(newSlippage).gt(0) && debouncedAmountBN.gt(0)) {
// fetchRoute({ newAmount: debouncedAmount, newSlippage });
// }
},
[setSlippage, debouncedAmount]
);
const onClickMax = useCallback(() => {
setWithdrawAmount(freeCollateralBN.toString());
}, [freeCollateral?.current, setWithdrawAmount]);
const onSelectChain = useCallback((chain: string) => {
if (chain) {
abacusStateManager.clearTransferInputValues();
abacusStateManager.setTransferValue({
field: TransferInputField.chain,
value: chain,
});
setWithdrawAmount('');
}
}, []);
const onSelectToken = useCallback((token: TransferInputTokenResource) => {
if (token) {
abacusStateManager.clearTransferInputValues();
abacusStateManager.setTransferValue({
field: TransferInputField.token,
value: token.address,
});
setWithdrawAmount('');
}
}, []);
const amountInputReceipt = [
{
key: 'freeCollateral',
label: (
<span>
{stringGetter({ key: STRING_KEYS.FREE_COLLATERAL })} <Tag>USDC</Tag>
</span>
),
value: (
<Styled.DiffOutput
type={OutputType.Fiat}
value={freeCollateral?.current}
newValue={freeCollateral?.postOrder}
sign={NumberSign.Negative}
hasInvalidNewValue={withdrawAmountBN.minus(freeCollateralBN).isNegative()}
withDiff={
Boolean(withdrawAmount) && !debouncedAmountBN.isNaN() && !debouncedAmountBN.isZero()
}
/>
),
},
];
const errorMessage = useMemo(() => {
if (error) {
return error?.message
? stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: { ERROR_MESSAGE: error.message },
})
: stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG });
}
if (!toAddress) return stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_ADDRESS });
if (debouncedAmountBN) {
if (!chainIdStr) {
return stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_CHAIN });
} else if (!toToken) {
return stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_ASSET });
}
}
if (MustBigNumber(debouncedAmountBN).gt(MustBigNumber(freeCollateralBN))) {
return stringGetter({ key: STRING_KEYS.WITHDRAW_MORE_THAN_FREE });
}
return undefined;
}, [error, freeCollateralBN, chainIdStr, debouncedAmountBN, toToken]);
const isDisabled =
!!errorMessage ||
!toToken ||
!chainIdStr ||
!toAddress ||
debouncedAmountBN.isNaN() ||
debouncedAmountBN.isZero();
return (
<Styled.Form onSubmit={onSubmit}>
<Styled.DestinationRow>
<FormInput
type={InputType.Text}
placeholder={stringGetter({ key: STRING_KEYS.ADDRESS })}
onChange={onChangeAddress}
value={toAddress || ''}
label={stringGetter({ key: STRING_KEYS.DESTINATION })}
/>
<ChainSelectMenu
label={stringGetter({ key: STRING_KEYS.NETWORK })}
selectedChain={chainIdStr || undefined}
onSelectChain={onSelectChain}
/>
</Styled.DestinationRow>
<TokenSelectMenu selectedToken={toToken || undefined} onSelectToken={onSelectToken} />
<Styled.WithDetailsReceipt side="bottom" detailItems={amountInputReceipt}>
<FormInput
type={InputType.Number}
onChange={onChangeAmount}
value={withdrawAmount}
label={stringGetter({ key: STRING_KEYS.AMOUNT })}
slotRight={
<Button
type={ButtonType.Button}
action={ButtonAction.Secondary}
size={ButtonSize.XSmall}
onClick={onClickMax}
>
{stringGetter({ key: STRING_KEYS.MAX })}
</Button>
}
/>
</Styled.WithDetailsReceipt>
{errorMessage && <AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>}
<WithdrawButtonAndReceipt
isDisabled={isDisabled}
isLoading={isLoading}
setSlippage={onSetSlippage}
slippage={slippage}
withdrawChain={chainIdStr || undefined}
withdrawToken={toToken || undefined}
/>
</Styled.Form>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.DiffOutput = styled(DiffOutput)`
--diffOutput-valueWithDiff-fontSize: 1em;
`;
Styled.Form = styled.form`
--form-input-height: 3.5rem;
min-height: calc(100% - var(--stickyArea0-bottomHeight));
${layoutMixins.flexColumn}
gap: 1.25rem;
${layoutMixins.stickyArea1}
`;
Styled.DestinationRow = styled.div`
${layoutMixins.spacedRow}
grid-template-columns: 1fr 1fr;
gap: 1rem;
`;
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
--withReceipt-backgroundColor: var(--color-layer-2);
`;
Styled.Link = styled(Link)`
color: var(--color-accent);
&:visited {
color: var(--color-accent);
}
`;
Styled.TransactionInfo = styled.span`
${layoutMixins.row}
`;
@@ -0,0 +1,242 @@
import { useState } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent } from 'styled-components';
import { TransferInputTokenResource } from '@/constants/abacus';
import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { NumberSign } from '@/constants/numbers';
import { useStringGetter } from '@/hooks';
import { useAccountBalance } from '@/hooks/useAccountBalance';
import { layoutMixins } from '@/styles/layoutMixins';
import { Button } from '@/components/Button';
import { Details, DetailsItem } from '@/components/Details';
import { DiffOutput } from '@/components/DiffOutput';
import { Icon, IconName } from '@/components/Icon';
import { Output, OutputType } from '@/components/Output';
import { ToggleButton } from '@/components/ToggleButton';
import { WithReceipt } from '@/components/WithReceipt';
import { SlippageEditor } from '../SlippageEditor';
import { getSubaccount } from '@/state/accountSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
type ElementProps = {
setSlippage: (slippage: number) => void;
slippage: number;
withdrawChain?: string;
withdrawToken?: TransferInputTokenResource;
isDisabled?: boolean;
isLoading?: boolean;
};
export const WithdrawButtonAndReceipt = ({
setSlippage,
slippage,
withdrawChain,
withdrawToken,
isDisabled,
isLoading,
}: ElementProps) => {
const [showFeeBreakdown, setShowFeeBreakdown] = useState(false);
const [isEditingSlippage, setIsEditingSlipapge] = useState(false);
const stringGetter = useStringGetter();
const { balance, queryStatus } = useAccountBalance({
addressOrDenom: withdrawToken?.address || undefined,
assetSymbol: withdrawToken?.symbol || undefined,
chainId: withdrawChain ? parseInt(withdrawChain) : undefined,
decimals: withdrawToken?.decimals || undefined,
isCosmosChain: false,
});
// TODO: uncomment when we have a way to get token amount estimate from abacus
// const balanceBN = MustBigNumber(balance);
// const newBalance =
// // toAmountMin && withdrawToken && parseUnits(toAmountMin, withdrawToken.decimals).toString();
const { leverage } = useSelector(getSubaccount, shallowEqual) || {};
const { summary, requestPayload } = useSelector(getTransferInputs, shallowEqual) || {};
const feeSubitems: DetailsItem[] = [];
if (typeof summary?.gasFee === 'number') {
feeSubitems.push({
key: 'gas-fees',
label: <span>{stringGetter({ key: STRING_KEYS.GAS_FEE })}</span>,
value: <Output type={OutputType.Fiat} value={summary?.gasFee} />,
});
}
if (typeof summary?.bridgeFee === 'number') {
feeSubitems.push({
key: 'bridge-fees',
label: <span>{stringGetter({ key: STRING_KEYS.BRIDGE_FEE })}</span>,
value: <Output type={OutputType.Fiat} value={summary?.bridgeFee} />,
});
}
const hasSubitems = feeSubitems.length > 0;
const showSubitemsToggle = showFeeBreakdown
? stringGetter({ key: STRING_KEYS.HIDE_ALL_DETAILS })
: stringGetter({ key: STRING_KEYS.SHOW_ALL_DETAILS });
const submitButtonReceipt = [
{
key: 'total-fees',
label: <span>{stringGetter({ key: STRING_KEYS.TOTAL_FEES })}</span>,
value: typeof summary?.bridgeFee === 'number' && typeof summary?.gasFee === 'number' && (
<Output type={OutputType.Fiat} value={summary?.bridgeFee + summary?.gasFee} />
),
subitems: feeSubitems,
},
{
key: 'leverage',
label: <span>{stringGetter({ key: STRING_KEYS.LEVERAGE })}</span>,
value: (
<Styled.DiffOutput
type={OutputType.Multiple}
value={leverage?.current}
newValue={leverage?.postOrder}
sign={NumberSign.Negative}
withDiff={Boolean(leverage?.current && leverage.current !== leverage?.postOrder)}
/>
),
},
{
key: 'exchange-rate',
label: <span>{stringGetter({ key: STRING_KEYS.EXCHANGE_RATE })}</span>,
value: withdrawToken && typeof summary?.exchangeRate === 'number' && (
<Styled.ExchangeRate>
<Output type={OutputType.Asset} value={1} fractionDigits={0} tag="USDC" />
=
<Output
type={OutputType.Asset}
value={summary?.exchangeRate}
tag={withdrawToken?.symbol}
/>
</Styled.ExchangeRate>
),
},
{
key: 'slippage',
label: <span>{stringGetter({ key: STRING_KEYS.SLIPPAGE })}</span>,
value: (
<SlippageEditor
slippage={slippage}
setIsEditing={setIsEditingSlipapge}
setSlippage={setSlippage}
/>
),
},
// TODO: uncomment when we have a way to get token amount estimate from abacus
// {
// key: 'wallet',
// label: (
// <span>
// {stringGetter({ key: STRING_KEYS.WALLET })}{' '}
// {withdrawToken && <Tag>{withdrawToken?.symbol}</Tag>}
// </span>
// ),
// value: (
// <Styled.DiffOutput
// type={OutputType.Asset}
// value={balanceBN?.toString()}
// newValue={newBalance}
// sign={NumberSign.Negative}
// withDiff={Boolean(balance !== newBalance)}
// />
// ),
// },
];
const isFormValid = !isDisabled && !isEditingSlippage && queryStatus !== 'error';
return (
<Styled.WithReceipt
slotReceipt={
<Styled.CollapsibleDetails>
<Styled.Details showSubitems={showFeeBreakdown} items={submitButtonReceipt} />
{hasSubitems && (
<Styled.DetailButtons>
<Styled.ToggleButton
shape={ButtonShape.Pill}
size={ButtonSize.XSmall}
isPressed={showFeeBreakdown}
onPressedChange={setShowFeeBreakdown}
slotLeft={<Icon iconName={IconName.Caret} />}
>
{showSubitemsToggle}
</Styled.ToggleButton>
</Styled.DetailButtons>
)}
</Styled.CollapsibleDetails>
}
>
<Button
action={ButtonAction.Primary}
type={ButtonType.Submit}
state={{
isDisabled: !isFormValid,
isLoading: (isFormValid && !requestPayload) || isLoading,
}}
>
{stringGetter({ key: STRING_KEYS.WITHDRAW })}
</Button>
</Styled.WithReceipt>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.DiffOutput = styled(DiffOutput)`
--diffOutput-valueWithDiff-fontSize: 1em;
`;
Styled.ExchangeRate = styled.span`
${layoutMixins.row}
gap: 0.5ch;
`;
Styled.WithReceipt = styled(WithReceipt)`
--withReceipt-backgroundColor: var(--color-layer-2);
`;
Styled.CollapsibleDetails = styled.div`
${layoutMixins.column}
padding: 0.375rem 1rem 0.5rem;
gap: 0.5rem;
`;
Styled.Details = styled(Details)`
font-size: 0.8125em;
`;
Styled.DetailButtons = styled.div`
${layoutMixins.spacedRow}
`;
Styled.ToggleButton = styled(ToggleButton)`
--button-toggle-off-backgroundColor: transparent;
--button-toggle-on-backgroundColor: transparent;
--button-toggle-on-textColor: var(--color-text-0);
svg {
width: 0.875em;
height: 0.875em;
}
&[data-state='on'] {
svg {
transform: rotate(180deg);
}
}
`;
-428
View File
@@ -1,428 +0,0 @@
import { type FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import styled, { type AnyStyledComponent } from 'styled-components';
import type { ChainData, CosmosChain, RouteData, TokenData } from '@0xsquid/sdk';
import { type NumberFormatValues } from 'react-number-format';
import debounce from 'lodash/debounce';
import { parseUnits } from 'ethers';
import { TransferInputField } from '@/constants/abacus';
import { AlertType } from '@/constants/alerts';
import { ButtonSize } from '@/constants/buttons';
import { StringGetterFunction, STRING_KEYS } from '@/constants/localization';
import { NumberSign } from '@/constants/numbers';
import { CLIENT_NETWORK_CONFIGS, type DydxV4Network } from '@/constants/networks';
import { useAccounts, useDydxClient, useStringGetter } from '@/hooks';
import { useAccountBalance } from '@/hooks/useAccountBalance';
import { layoutMixins } from '@/styles/layoutMixins';
import { AlertMessage } from '@/components/AlertMessage';
import { Button } from '@/components/Button';
import { DiffOutput } from '@/components/DiffOutput';
import { FormInput } from '@/components/FormInput';
import { InputType } from '@/components/Input';
import { Link } from '@/components/Link';
import { OutputType } from '@/components/Output';
import { Tag } from '@/components/Tag';
import { WithLabel } from '@/components/WithLabel';
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
import abacusStateManager from '@/lib/abacus';
import { convertBech32Address } from '@/lib/addressUtils';
import { MustBigNumber } from '@/lib/numbers';
import squidRouter from '@/lib/squidRouter';
import { useSquidData } from './DepositForm/useSquidData';
import { ChainSelectMenu } from './DepositForm/ChainSelectMenu';
import { DepositButtonAndReceipt } from './DepositForm/DepositButtonAndReceipt';
import { TokenSelectMenu } from './DepositForm/TokenSelectMenu';
import { log } from '@/lib/telemetry';
import { getSelectedNetwork } from '@/state/appSelectors';
type DepositFormProps = {
onDeposit?: () => void;
onError?: () => void;
};
// debounced function should be outside our functional component
const debouncedGetRoute = debounce(
async ({
amount,
currentChain,
cosmosAddress,
fromAmount,
setAbacusTransferInput,
setError,
setIsLoading,
setSquidRoute,
slippage,
sourceToken,
}) => {
if (!currentChain || !sourceToken || !fromAmount || !cosmosAddress) return;
setIsLoading(true);
setSquidRoute(undefined);
setError(undefined);
const params = {
fromChain: currentChain.chainId,
fromToken: sourceToken.address,
fromAmount: parseUnits(amount, sourceToken.decimals).toString(),
toAddress: convertBech32Address({ address: cosmosAddress, bech32Prefix: 'osmo' }), // the recipient of the trade
slippage: slippage * 100,
...squidRouter.SQUID_ROUTE_DEFAULTS,
};
try {
const { route } = await squidRouter.getRoute(params);
setSquidRoute(route);
setAbacusTransferInput(route.estimate.toAmountUSD);
} catch (error) {
log('DepositForm/getRoute', error);
setError(error);
} finally {
setIsLoading(false);
}
},
1000,
{ trailing: true }
);
const getDepositFormErrorMessage = ({
error,
balance,
currentChain,
fromAmount,
sourceToken,
stringGetter,
}: {
error?: any;
balance?: string;
currentChain?: ChainData;
fromAmount: string;
sourceToken?: TokenData;
stringGetter: StringGetterFunction;
}): string | undefined => {
if (error) {
return error?.message
? stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: { ERROR_MESSAGE: error.message },
})
: stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG });
}
if (fromAmount) {
if (!currentChain) {
return stringGetter({ key: STRING_KEYS.MUST_SPECIFY_CHAIN });
} else if (!sourceToken) {
return stringGetter({ key: STRING_KEYS.MUST_SPECIFY_ASSET });
}
}
if (MustBigNumber(fromAmount).gt(MustBigNumber(balance))) {
return stringGetter({ key: STRING_KEYS.DEPOSIT_MORE_THAN_BALANCE });
}
return undefined;
};
export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const stringGetter = useStringGetter();
const { dydxAddress: cosmosAddress, signerWagmi, signerGraz } = useAccounts();
// const { networkConfig } = useDydxClient();
const selectedNetwork = useSelector(getSelectedNetwork);
const evmChainId = Number(CLIENT_NETWORK_CONFIGS[selectedNetwork as DydxV4Network].ethereumChainId);
const { chains, tokens } = useSquidData(signerGraz ? 'cosmos' : 'evm');
// User inputs
const defaultChain: ChainData | undefined = useMemo(
() => chains?.find(({ chainId }) => chainId === evmChainId) ?? undefined,
[chains]
);
const [currentChain, setCurrentChain] = useState<ChainData | undefined>(defaultChain);
const defaultToken: TokenData | undefined = useMemo(
() => tokens?.filter(({ chainId }) => chainId === currentChain?.chainId)?.[0] ?? undefined,
[currentChain]
);
const [sourceToken, setSourceToken] = useState<TokenData | undefined>(defaultToken);
const [fromAmount, setFromAmount] = useState('');
const [slippage, setSlippage] = useState(0.001); // 0.1% slippage
// Async Data
const [squidRoute, setSquidRoute] = useState<RouteData>();
const [transactionHash, setTransactionHash] = useState<string | undefined>();
// Form states
const [error, setError] = useState<Error | undefined>();
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setCurrentChain(defaultChain);
return () => {
abacusStateManager.clearTransferInputValues();
};
}, [defaultChain]);
useEffect(() => {
setSourceToken(defaultToken);
setSquidRoute(undefined);
setFromAmount('');
}, [currentChain]);
// @ts-ignore ibcDenom exists but is not typed properly on 0xSquid SDK
const addressOrDenom = sourceToken?.ibcDenom || sourceToken?.address;
const bech32AddrPrefix = (currentChain as CosmosChain)?.bech32Config?.bech32PrefixAccAddr;
const { balance, isBalanceError, isBalanceLoading } = useAccountBalance({
addressOrDenom,
assetSymbol: sourceToken?.symbol,
bech32AddrPrefix,
chainId: currentChain?.chainId,
decimals: sourceToken?.decimals,
rpc: currentChain?.rpc,
isCosmosChain: currentChain?.chainType === 'cosmos',
});
// BN
const fromAmountBN = MustBigNumber(fromAmount);
const balanceBN = MustBigNumber(balance);
const requestDeposit = async () => {
if (!squidRoute) {
log('DepositForm/requestDeposit', new Error('No squid route'));
return;
}
try {
setIsLoading(true);
setError(undefined);
setTransactionHash(undefined);
const txReceipt = await squidRouter.executeRoute({
route: squidRoute,
signer: signerGraz || signerWagmi,
});
console.log('DepositForm/executeRoute', { txReceipt });
setFromAmount('');
setTransactionHash(txReceipt?.transactionHash);
} catch (error) {
log('DepositForm/executeRoute', error);
setError(error);
} finally {
setIsLoading(false);
}
};
const setAbacusTransferInput = (value: string | number) => {
abacusStateManager.setTransferValue({ value, field: TransferInputField.usdcSize });
};
const getRoute = useCallback(
({ newAmount, newSlippage }: { newAmount: string; newSlippage?: number }) => {
debouncedGetRoute.cancel();
if (MustBigNumber(newAmount).gt(0) && MustBigNumber(newAmount).lte(balanceBN)) {
debouncedGetRoute({
amount: newAmount,
currentChain,
cosmosAddress,
fromAmount,
setAbacusTransferInput,
setError,
setIsLoading,
setSquidRoute,
slippage: newSlippage || slippage,
sourceToken,
});
} else {
setSquidRoute(undefined);
setAbacusTransferInput(0);
}
},
[currentChain, sourceToken, fromAmount, slippage]
);
const onChangeAmount = ({ value }: NumberFormatValues) => {
setFromAmount(value);
getRoute({ newAmount: value });
};
const onSetSlippage = (newSlippage: number) => {
setSlippage(newSlippage);
if (MustBigNumber(newSlippage).gt(0) && fromAmountBN.gt(0)) {
getRoute({ newAmount: fromAmount, newSlippage });
}
};
const onClickMax = () => {
if (balance) {
setFromAmount(balanceBN.toString());
}
};
const amountInputReceipt = [
{
key: 'amount',
label: (
<span>
{stringGetter({ key: STRING_KEYS.AVAILABLE })}{' '}
{sourceToken && <Tag>{sourceToken.symbol}</Tag>}
</span>
),
value: (
<DiffOutput
type={OutputType.Asset}
value={balance}
newValue={balanceBN.minus(fromAmountBN).toString()}
sign={NumberSign.Negative}
hasInvalidNewValue={balanceBN.minus(fromAmountBN).isNegative()}
withDiff={Boolean(fromAmount && balance) && !fromAmountBN.isNaN()}
/>
),
},
];
const errorMessage = getDepositFormErrorMessage({
error,
balance,
currentChain,
fromAmount,
sourceToken,
stringGetter,
});
const isDisabled =
Boolean(errorMessage) ||
!sourceToken ||
!currentChain ||
fromAmountBN.isNaN() ||
fromAmountBN.isZero() ||
!squidRoute;
const alertMessage = {
type: errorMessage ? AlertType.Error : AlertType.Info,
message:
errorMessage ||
(transactionHash && (
<span>
{stringGetter({ key: STRING_KEYS.VIEW_DETAILS })}
{' on '}
<Styled.Link href={`https://testnet.axelarscan.io/transfer/${transactionHash}`}>
Axelar
</Styled.Link>
.
</span>
)),
};
return (
<Styled.Form
onSubmit={(e: FormEvent) => {
e.preventDefault();
requestDeposit();
}}
>
<ChainSelectMenu
chains={chains}
currentChain={currentChain}
setCurrentChain={setCurrentChain}
/>
<TokenSelectMenu
tokens={tokens}
chain={currentChain}
sourceToken={sourceToken}
setSourceToken={(token: TokenData) => {
setSourceToken(token);
if (fromAmount) {
debouncedGetRoute({
amount: fromAmount,
currentChain,
cosmosAddress,
fromAmount,
setError,
setIsLoading,
setSquidRoute,
slippage,
sourceToken: token,
});
} else {
setSquidRoute(undefined);
}
}}
/>
<WithLabel label={stringGetter({ key: STRING_KEYS.AMOUNT })}>
<Styled.WithDetailsReceipt side="bottom" detailItems={amountInputReceipt}>
<Styled.FormInput
type={InputType.Number}
onChange={onChangeAmount}
value={fromAmount}
slotRight={
<Button size={ButtonSize.XSmall} onClick={onClickMax}>
{stringGetter({ key: STRING_KEYS.MAX })}
</Button>
}
/>
</Styled.WithDetailsReceipt>
</WithLabel>
{alertMessage.message && (
<Styled.AlertMessage type={alertMessage.type}>{alertMessage.message}</Styled.AlertMessage>
)}
<Styled.Footer>
<DepositButtonAndReceipt
isDisabled={isDisabled}
isLoading={isLoading || isBalanceLoading}
chainId={currentChain?.chainId}
setError={setError}
setSlippage={onSetSlippage}
slippage={slippage}
sourceToken={sourceToken}
squidRoute={squidRoute}
/>
</Styled.Footer>
</Styled.Form>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Form = styled.form`
${layoutMixins.flexColumn}
gap: 1.5rem;
${layoutMixins.stickyArea1}
min-height: calc(100% - var(--stickyArea0-bottomHeight));
`;
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
--withReceipt-backgroundColor: var(--color-layer-2);
`;
Styled.FormInput = styled(FormInput)`
--formInput-backgroundColor: var(--color-layer-4);
--formInput-input-height: 3.375rem;
border: 1px solid var(--color-layer-6);
`;
Styled.Link = styled(Link)`
color: var(--color-accent);
`;
Styled.AlertMessage = styled(AlertMessage)`
cursor: normal;
user-select: none;
margin: 0;
`;
Styled.Footer = styled.footer`
${layoutMixins.stickyFooter}
margin-top: auto;
`;
@@ -1,89 +0,0 @@
import type { ChainData, TokenData } from '@0xsquid/sdk';
import styled, { type AnyStyledComponent } from 'styled-components';
import { STRING_KEYS } from '@/constants/localization';
import { useStringGetter } from '@/hooks';
import { SearchSelectMenu } from '@/components/SearchSelectMenu';
import { Tag } from '@/components/Tag';
import { WithLabel } from '@/components/WithLabel';
import { layoutMixins } from '@/styles/layoutMixins';
type ElementProps = {
chain?: ChainData;
tokens?: TokenData[];
sourceToken?: TokenData;
setSourceToken: (token: TokenData) => void;
};
export const TokenSelectMenu = ({
tokens = [],
chain,
sourceToken,
setSourceToken,
}: ElementProps) => {
const stringGetter = useStringGetter();
const tokenItems = Object.values(tokens)
.filter((token: TokenData) => token.chainId === chain?.chainId)
.map((token: TokenData) => ({
value: token.address,
label: token.name,
onSelect: () => setSourceToken(token),
slotBefore: <Styled.Img src={token.logoURI} alt="" />,
}));
return (
<SearchSelectMenu
disabled={!chain}
items={[
{
group: 'assets',
groupLabel: stringGetter({ key: STRING_KEYS.ASSET }),
items: tokenItems,
},
]}
label={stringGetter({ key: STRING_KEYS.ASSET })}
withReceiptItems={[
{
key: 'swap',
label: stringGetter({ key: STRING_KEYS.SWAP }),
value: sourceToken && (
<>
<Tag>{sourceToken?.symbol}</Tag><Tag>USDC</Tag>
</>
),
tooltip: 'swap',
withTooltipIcon: true,
},
]}
>
<Styled.AssetRow>
{sourceToken ? (
<>
<Styled.Img src={sourceToken?.logoURI} alt="" /> {sourceToken?.name}{' '}
<Tag>{sourceToken?.symbol}</Tag>
</>
) : (
stringGetter({ key: STRING_KEYS.SELECT_ASSET })
)}
</Styled.AssetRow>
</SearchSelectMenu>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.AssetRow = styled.div`
${layoutMixins.row}
gap: 0.5rem;
color: var(--color-text-2);
font: var(--font-base-book);
`;
Styled.Img = styled.img`
width: 1.25rem;
height: 1.25rem;
border-radius: 50%;
`;
@@ -1,33 +0,0 @@
import { useEffect, useState } from 'react';
import type { ChainData, TokenData } from '@0xsquid/sdk';
import squidRouter from '@/lib/squidRouter';
import { WAGMI_SUPPORTED_CHAINS } from '@/lib/wagmi';
/**
* @description hook to return chains and tokens supported by squid
* @note Limit to EVM chains | Cosmos chains can only be used as destination on 0xSquid as of 02/21/2023
*/
export const useSquidData = (selectedChainType: 'cosmos' | 'evm') => {
const [, setHasInitialized] = useState(false);
useEffect(() => {
(async () => {
await squidRouter.initializeClient();
setHasInitialized(true);
})();
}, []);
const chainData: ChainData[] = squidRouter.getChains();
const tokenData: TokenData[] = squidRouter.getTokens();
const wagmiChainIds = Object.fromEntries(
WAGMI_SUPPORTED_CHAINS.map((supportedChain) => [supportedChain.id, supportedChain])
);
return {
chains: chainData.filter(({ chainId, chainType }) =>
selectedChainType === 'evm' ? wagmiChainIds[chainId] : chainType === 'cosmos'
),
tokens: tokenData,
};
};
+4 -1
View File
@@ -99,7 +99,8 @@ export const TradeForm = ({
const { limitPrice, triggerPrice, trailingPercent } = price || {};
const hasUncommittedOrders = useSelector(calculateHasUncommittedOrders);
const currentInput = useSelector(getCurrentInput);
const { tickSizeDecimals } = useSelector(getCurrentMarketConfig, shallowEqual) || {};
const { tickSizeDecimals, stepSizeDecimals } =
useSelector(getCurrentMarketConfig, shallowEqual) || {};
const needsAdvancedOptions =
needsGoodUntil || timeInForceOptions || executionOptions || needsPostOnly || needsReduceOnly;
@@ -120,6 +121,8 @@ export const TradeForm = ({
const inputAlert = getTradeInputAlert({
abacusInputErrors: tradeErrors ?? [],
stringGetter,
stepSizeDecimals,
tickSizeDecimals,
});
if (placeOrderError) {
@@ -109,6 +109,7 @@ export const AdvancedTradeOptions = () => {
{executionOptions && (
<Styled.SelectMenu
value={execution}
label={stringGetter({ key: STRING_KEYS.EXECUTION })}
onValueChange={(selectedTimeInForceOption: string) =>
abacusStateManager.setTradeValue({
value: selectedTimeInForceOption,
+9 -3
View File
@@ -48,6 +48,7 @@ import { MustBigNumber } from '@/lib/numbers';
import { log } from '@/lib/telemetry';
type TransferFormProps = {
selectedAsset?: DydxChainAsset;
onDone?: () => void;
className?: string;
};
@@ -74,7 +75,11 @@ const debouncedEstimateFee = debounce(
{ trailing: true }
);
export const TransferForm = ({ onDone, className }: TransferFormProps) => {
export const TransferForm = ({
selectedAsset = DydxChainAsset.DYDX,
onDone,
className,
}: TransferFormProps) => {
const stringGetter = useStringGetter();
const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {};
const { dydxAddress } = useAccounts();
@@ -84,7 +89,7 @@ export const TransferForm = ({ onDone, className }: TransferFormProps) => {
const { selectedNetwork } = useSelectedNetwork();
// User Input
const [asset, setAsset] = useState<DydxChainAsset>(DydxChainAsset.DYDX);
const [asset, setAsset] = useState<DydxChainAsset>(selectedAsset);
// Form states
const [error, setError] = useState<Error | undefined>();
@@ -214,7 +219,8 @@ export const TransferForm = ({ onDone, className }: TransferFormProps) => {
value: DydxChainAsset.DYDX,
label: (
<Styled.InlineRow>
<AssetIcon symbol="DYDX" /> DYDX
{/* <AssetIcon symbol="DYDX" /> */}
Dv4TNT
</Styled.InlineRow>
),
},
+51 -16
View File
@@ -1,11 +1,13 @@
import { memo } from 'react';
import styled, { AnyStyledComponent } from 'styled-components';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import type { Dispatch } from '@reduxjs/toolkit';
import { OnboardingState } from '@/constants/account';
import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS, TOOLTIP_STRING_KEYS } from '@/constants/localization';
import { wallets } from '@/constants/wallets';
import { DydxChainAsset, wallets } from '@/constants/wallets';
import { useAccounts, useBreakpoints, useStringGetter, useAccountBalance } from '@/hooks';
@@ -103,22 +105,28 @@ export const AccountMenu = () => {
</Styled.AddressRow>
<Styled.Balances>
<div>
<Styled.label>
{stringGetter({ key: STRING_KEYS.ASSET_BALANCE, params: { ASSET: 'Dv4TNT' } })}
{/* <AssetIcon symbol="DYDX" /> */}
</Styled.label>
<Styled.BalanceOutput type={OutputType.Asset} value={nativeTokenBalance} />
<div>
<Styled.label>
{stringGetter({ key: STRING_KEYS.ASSET_BALANCE, params: { ASSET: 'Dv4TNT' } })}
{/* <AssetIcon symbol="DYDX" /> */}
</Styled.label>
<Styled.BalanceOutput type={OutputType.Asset} value={nativeTokenBalance} />
</div>
<AssetActions asset={DydxChainAsset.DYDX} dispatch={dispatch} />
</div>
<div>
<Styled.label>
{stringGetter({ key: STRING_KEYS.ASSET_BALANCE, params: { ASSET: 'USDC' } })}
<AssetIcon symbol="USDC" />
</Styled.label>
<Styled.BalanceOutput
type={OutputType.Asset}
value={freeCollateral?.current || 0}
fractionDigits={2}
/>
<div>
<Styled.label>
{stringGetter({ key: STRING_KEYS.ASSET_BALANCE, params: { ASSET: 'USDC' } })}
<AssetIcon symbol="USDC" />
</Styled.label>
<Styled.BalanceOutput
type={OutputType.Asset}
value={freeCollateral?.current || 0}
fractionDigits={2}
/>
</div>
<AssetActions asset={DydxChainAsset.USDC} dispatch={dispatch} />
</div>
</Styled.Balances>
</Styled.AccountInfo>
@@ -191,6 +199,29 @@ export const AccountMenu = () => {
);
};
const AssetActions = memo(({ asset, dispatch }: { asset: DydxChainAsset; dispatch: Dispatch }) => (
<Styled.InlineRow>
{[
// TODO(@rosepuppy): Add withdraw action for USDC
{
dialogType: DialogTypes.Receive,
iconName: IconName.Qr,
},
{ dialogType: DialogTypes.Transfer, iconName: IconName.Send },
].map(({ iconName, dialogType }) => (
<IconButton
key={dialogType}
action={ButtonAction.Base}
shape={ButtonShape.Square}
iconName={iconName}
onClick={() =>
dispatch(openDialog({ type: dialogType, dialogProps: { selectedAsset: asset } }))
}
/>
))}
</Styled.InlineRow>
));
const Styled: Record<string, AnyStyledComponent> = {};
Styled.AccountInfo = styled.div`
@@ -204,6 +235,10 @@ Styled.Column = styled.div`
${layoutMixins.column}
`;
Styled.InlineRow = styled.div`
${layoutMixins.inlineRow}
`;
Styled.AddressRow = styled.div`
${layoutMixins.row}
@@ -256,7 +291,7 @@ Styled.Balances = styled.div`
gap: 2px;
> div {
${layoutMixins.flexColumn}
${layoutMixins.spacedRow}
gap: 0.5rem;
padding: 0.5rem 1rem;
+3 -3
View File
@@ -89,9 +89,9 @@ export const NotificationsMenu = ({
.map(({ notification, key, displayData }) => ({
value: key,
label: displayData.title ?? '',
description: displayData.description,
slotBefore: displayData.icon,
slotAfter: (
description: displayData.customMenuContent || displayData.description,
slotBefore: !displayData.customMenuContent && displayData.icon,
slotAfter: !displayData.customMenuContent && (
<>
<$Output
type={OutputType.RelativeTime}
+4 -3
View File
@@ -44,7 +44,7 @@ type StyleProps = {
type RowData = OrderbookLine & { side: 'bid' | 'ask'; mine?: number };
const calculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number }) => {
const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number }) => {
const orderbook = useSelector(getCurrentMarketOrderbook, shallowEqual);
const openOrdersBySideAndPrice =
@@ -96,7 +96,8 @@ const calculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number })
}
}
const spread = MustBigNumber(asks[0]?.price ?? 0).minus(bids[0]?.price ?? 0);
const spread =
asks[0] && bids[0] ? MustBigNumber(asks[0]?.price ?? 0).minus(bids[0]?.price ?? 0) : null;
const spreadPercent = orderbook?.spreadPercent;
@@ -233,7 +234,7 @@ export const Orderbook = ({
const showMineColumn = useSelector(calculateCanViewAccount) && !isTablet;
const { asks, bids, spread, spreadPercent, histogramRange, hasOrderbook } =
calculateOrderbookData({
useCalculateOrderbookData({
maxRowsPerSide,
});
+10 -2
View File
@@ -153,8 +153,16 @@ const getOrdersTableColumnDef = ({
tag: symbol,
renderCell: ({ size, totalFilled, stepSizeDecimals }) => (
<TableCell stacked>
<Output type={OutputType.Asset} value={size} fractionDigits={stepSizeDecimals} />
<Output type={OutputType.Asset} value={totalFilled} fractionDigits={stepSizeDecimals} />
<Output
type={OutputType.Asset}
value={size}
fractionDigits={stepSizeDecimals < TOKEN_DECIMALS ? TOKEN_DECIMALS : stepSizeDecimals}
/>
<Output
type={OutputType.Asset}
value={totalFilled}
fractionDigits={stepSizeDecimals < TOKEN_DECIMALS ? TOKEN_DECIMALS : stepSizeDecimals}
/>
</TableCell>
),
},
+195
View File
@@ -0,0 +1,195 @@
import styled, { type AnyStyledComponent, css } from 'styled-components';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import type { ColumnSize } from '@react-types/table';
import { type SubaccountTransfer } from '@/constants/abacus';
import { ButtonAction } from '@/constants/buttons';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS, StringGetterFunction } from '@/constants/localization';
import { useBreakpoints, useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins';
import { tradeViewMixins } from '@/styles/tradeViewMixins';
import { Button } from '@/components/Button';
import { CopyButton } from '@/components/CopyButton';
import { Icon } from '@/components/Icon';
import { Link } from '@/components/Link';
import { Output, OutputType } from '@/components/Output';
import { Table, TableCell, TableColumnHeader, type ColumnDef } from '@/components/Table';
import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton';
import { getSubaccountTransfers } from '@/state/accountSelectors';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { openDialog } from '@/state/dialogs';
import { truncateAddress } from '@/lib/wallet';
const MOBILE_TRANSFERS_PER_PAGE = 50;
export enum TransferHistoryTableColumnKey {
Time = 'Time',
Action = 'Action',
SenderRecipient = 'Sender-Recipient',
Amount = 'Amount',
TxHash = 'TxHash',
}
const getTransferHistoryTableColumnDef = ({
key,
stringGetter,
width,
}: {
key: TransferHistoryTableColumnKey;
isTablet?: boolean;
stringGetter: StringGetterFunction;
width?: ColumnSize;
}): ColumnDef<SubaccountTransfer> => ({
width,
...(
{
[TransferHistoryTableColumnKey.Time]: {
columnKey: TransferHistoryTableColumnKey.Time,
getCellValue: (row) => row.updatedAtMilliseconds,
label: stringGetter({ key: STRING_KEYS.TIME }),
renderCell: ({ updatedAtMilliseconds }) => (
<Styled.TimeOutput
type={OutputType.RelativeTime}
relativeTimeFormatOptions={{ format: 'singleCharacter' }}
value={updatedAtMilliseconds}
/>
),
},
[TransferHistoryTableColumnKey.Action]: {
columnKey: TransferHistoryTableColumnKey.Action,
getCellValue: (row) => row.resources.typeStringKey,
label: stringGetter({ key: STRING_KEYS.ACTION }),
renderCell: ({ resources }) =>
resources.typeStringKey && stringGetter({ key: resources.typeStringKey }),
},
[TransferHistoryTableColumnKey.SenderRecipient]: {
columnKey: TransferHistoryTableColumnKey.SenderRecipient,
getCellValue: (row) => `${row.fromAddress}-${row.toAddress}`,
label: (
<TableColumnHeader>
<span>{stringGetter({ key: STRING_KEYS.SENDER })}</span>
<span>{stringGetter({ key: STRING_KEYS.RECIPIENT })}</span>
</TableColumnHeader>
),
renderCell: ({ fromAddress, toAddress }) => (
<TableCell stacked>
<CopyButton shownAsText value={fromAddress ?? undefined}>
{fromAddress ? truncateAddress(fromAddress) : '-'}
</CopyButton>{' '}
<CopyButton shownAsText value={toAddress ?? undefined}>
{toAddress ? truncateAddress(toAddress) : '-'}
</CopyButton>
</TableCell>
),
},
[TransferHistoryTableColumnKey.Amount]: {
columnKey: TransferHistoryTableColumnKey.Amount,
getCellValue: (row) => row.amount,
label: stringGetter({ key: STRING_KEYS.AMOUNT }),
renderCell: ({ amount }) => <Output type={OutputType.Fiat} value={amount} />,
},
[TransferHistoryTableColumnKey.TxHash]: {
columnKey: TransferHistoryTableColumnKey.TxHash,
getCellValue: (row) => row.transactionHash,
label: stringGetter({ key: STRING_KEYS.TRANSACTION }),
renderCell: ({ transactionHash, resources }) =>
transactionHash ? (
<Styled.TxHash withIcon href={resources.blockExplorerUrl}>
{truncateAddress(transactionHash, '')}
</Styled.TxHash>
) : (
'-'
),
},
} as Record<TransferHistoryTableColumnKey, ColumnDef<SubaccountTransfer>>
)[key],
});
type ElementProps = {
columnKeys?: TransferHistoryTableColumnKey[];
columnWidths?: Partial<Record<TransferHistoryTableColumnKey, ColumnSize>>;
};
type StyleProps = {
withOuterBorder?: boolean;
withInnerBorders?: boolean;
};
export const TransferHistoryTable = ({
columnKeys = Object.values(TransferHistoryTableColumnKey),
columnWidths,
withOuterBorder,
withInnerBorders = true,
}: ElementProps & StyleProps) => {
const stringGetter = useStringGetter();
const dispatch = useDispatch();
const { isMobile, isTablet } = useBreakpoints();
const canAccountTrade = useSelector(calculateCanAccountTrade, shallowEqual);
const transfers = useSelector(getSubaccountTransfers, shallowEqual) ?? [];
return (
<Styled.Table
label="Transfers"
data={isMobile ? transfers.slice(0, MOBILE_TRANSFERS_PER_PAGE) : transfers}
getRowKey={(row: SubaccountTransfer) => row.id}
columns={columnKeys.map((key: TransferHistoryTableColumnKey) =>
getTransferHistoryTableColumnDef({
key,
isTablet,
stringGetter,
width: columnWidths?.[key],
})
)}
slotEmpty={
<>
{stringGetter({ key: STRING_KEYS.TRANSFERS_EMPTY_STATE })}
{canAccountTrade ? (
<Button
action={ButtonAction.Primary}
onClick={() => dispatch(openDialog({ type: DialogTypes.Deposit }))}
>
{stringGetter({ key: STRING_KEYS.DEPOSIT_FUNDS })}
</Button>
) : (
<OnboardingTriggerButton />
)}
</>
}
selectionBehavior="replace"
withOuterBorder={withOuterBorder}
withInnerBorders={withInnerBorders}
withScrollSnapColumns
withScrollSnapRows
/>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Table = styled(Table)`
${tradeViewMixins.horizontalTable}
`;
Styled.InlineRow = styled.div`
${layoutMixins.inlineRow}
`;
Styled.Icon = styled(Icon)`
font-size: 3em;
`;
Styled.TimeOutput = styled(Output)`
color: var(--color-text-0);
`;
Styled.TxHash = styled(Link)`
justify-content: flex-end;
`;