Compare commits

..
Author SHA1 Message Date
Bill He 63f1b26852 add tag 2023-09-25 17:42:25 -04:00
Bill He 9e28657c5f bump localization 2023-09-25 17:38:15 -04:00
Bill He 54db166862 add into reciept 2023-09-25 17:38:15 -04:00
Bill He 0e29a67a9f temp 2023-09-25 17:38:14 -04:00
Bill fe46a4d927 Do not poll evm unless addressOrDenom is provided (#46) 2023-09-25 14:56:20 -04:00
Bill 68e5cb8680 Do not get token balance on withdraw (#45) 2023-09-22 17:12:54 -07:00
Bill dea36da7bf Use abacus for simulate transactions and balance polling (#43)
* Use abacus for simulate transactions and balance polling

* address comment

* use dydx gas token

* address nits
2023-09-22 14:21:42 -07:00
Jared Vu f7d052f52e Abacus Transactions Protocol (#38)
* Merge main

* MarketLinks: add key to map

* Merge stashed changes

* fix callback usage to fulfill success/error cbs

* Add Abacus analytics class

* Bump Abacus 0.4.27, Update cancelOrder to use Abacus

* Cancel order loading states

* Use latestOrderChanged to determine whether an order has been committed

* nits

* restore uncommittedOrderClientIds

* bump abacus@0.5.0

* fix for handling null testMarket
2023-09-21 09:19:01 -07:00
Bill 51906f1096 Onboarding testnet 3 and error handling (#42)
* temp

* Onboarding error handling

* address comments
2023-09-20 10:29:02 -07:00
aleka 9a230401cd enable conditional orders in order type dropdown in prod (#41) 2023-09-18 11:19:56 -04:00
aleka 342bd2d334 remove offline exchange dialog from testnet3 prod (#40) 2023-09-15 17:49:15 -04:00
Bill cf68740725 bump v4-client and abacus (#39) 2023-09-15 10:56:27 -07:00
aleka 1462e37c43 temporarily shut down prod FE (#37)
* shut down fe for testnet3 prod
2023-09-13 17:01:42 -04:00
aleka d99b0356e9 bump v4-localization to use testnet3 (#36) 2023-09-13 09:36:23 -07:00
aleka 5cd9bb0fc5 bump abacus for transfers ws fix (#35) 2023-09-13 12:19:34 -04:00
Jared Vu 986a134d07 handle composite client error (#33) 2023-09-13 09:15:22 -07:00
Jared Vu ab454d5928 eslint (#26)
* eslint - hooks

* eslint - icons

* eslint - styles

* eslint - state
2023-09-12 09:42:38 -07:00
aleka 0f1491ac33 show exchange offline dialog for testnet2 (#32) 2023-09-12 11:29:38 -04:00
Jared Vu 1f73f3a884 v4-abacus (#31) 2023-09-11 16:56:13 -07:00
Jared Vu 419d05a0e1 Satoshi font 2 (#30)
* Use Satoshi in TvChart

* update tradingview.tgz
2023-09-11 15:59:43 -07:00
Jared Vu 0c4b605666 Add base position notional (#29) 2023-09-11 15:15:03 -07:00
James Jia 710e5ac113 Update README (#27) 2023-09-08 16:32:24 -07:00
Bill 8fdac4dc09 bump v4-client (#28) 2023-09-08 16:31:34 -07:00
James Jia - Test 4b86068d8f Initial commit 2023-09-08 13:52:13 -07:00
122 changed files with 2308 additions and 3129 deletions
-12
View File
@@ -2,17 +2,5 @@ VITE_BASE_URL=https://v4.testnet.dydx.exchange
VITE_ALCHEMY_API_KEY=FP275q327Yh7qswtZWenbPXdZZdnAFmC
VITE_FAUCET_URL_DEV=https://faucet.v4dev.dydx.exchange/
VITE_FAUCET_URL_STAGE=https://faucet.v4staging.dydx.exchange/
VITE_FAUCET_URL_TESTNET2=https://faucet.v4testnet2.dydx.exchange/
VITE_VALIDATOR_URL_DEV=http://52.192.187.113:26657/
VITE_VALIDATOR_URL_STAGE=https://validator.v4staging.dydx.exchange/
VITE_VALIDATOR_URL_TESTNET2=https://validator.v4testnet2.dydx.exchange/
VITE_INDEXER_URL_DEV=http://dev-indexer-apne1-lb-public-890774175.ap-northeast-1.elb.amazonaws.com
VITE_INDEXER_URL_STAGE=https://indexer.v4staging.dydx.exchange
VITE_INDEXER_URL_TESTNET2=https://indexer.v4testnet2.dydx.exchange
VITE_WALLETCONNECT1_BRIDGE=wss://api.dydx.exchange/wc/
VITE_WALLETCONNECT2_PROJECT_ID=fbe94eaa691fa8d929561f8567062b32
+3 -12
View File
@@ -1,17 +1,8 @@
VITE_BASE_URL=
VITE_ALCHEMY_API_KEY=
VITE_PK_ENCRYPTION_KEY=
VITE_WALLETCONNECT1_BRIDGE=
VITE_WALLETCONNECT2_PROJECT_ID=
VITE_FAUCET_URL_DEV=
VITE_FAUCET_URL_STAGE=
VITE_FAUCET_URL_TESTNET2=
VITE_VALIDATOR_URL_DEV=
VITE_VALIDATOR_URL_STAGE=
VITE_VALIDATOR_URL_TESTNET2=
VITE_INDEXER_URL_DEV=
VITE_INDEXER_URL_STAGE=
VITE_INDEXER_URL_TESTNET2=
-5
View File
@@ -5,15 +5,10 @@ vite.config.ts
# Temporarily ignore, we will slowly remove each directory as we fix files to follow ESLint rules
/src/components
/src/dialogs
/src/forms
/src/hooks
/src/history.ts
/src/icons
/src/lib
/src/main.tsx
/src/menus
/src/pages
/src/state
/src/styles
/src/views
+4 -6
View File
@@ -33,14 +33,11 @@
"jsx-a11y/anchor-is-valid": "off",
"jsx-a11y/click-events-have-key-events": "off",
"no-continue": "off",
"no-console": [
"error",
{
"devDependencies": ["./scripts/*.js"]
}
],
"no-console": ["error"],
"no-lonely-if": "off",
"no-multi-assign": "off",
"no-nested-ternary": "off",
"no-param-reassign": ["error", { "props": false }],
"no-return-assign": "off",
"no-return-await": "off",
"no-underscore-dangle": "off",
@@ -65,6 +62,7 @@
"react/sort-comp": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/comma-dangle": "off",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/member-delimiter-style": [
"error",
{
+4 -4
View File
@@ -1,4 +1,4 @@
# Contributing to Trader-Fe
# Contributing to V4-web
We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:
- Reporting a bug
@@ -18,10 +18,10 @@ Pull requests are the best way to propose changes to the codebase (we use [Githu
4. Issue that pull request!
## Any contributions you make will be under the same License
When you submit code changes, your submissions are understood to be under the same [License](https://github.com/dydxprotocol/trader-fe/blob/master/LICENSE) that covers the project.
When you submit code changes, your submissions are understood to be under the same [License](https://github.com/dydxprotocol/v4-web/blob/master/LICENSE) that covers the project.
## Report bugs using Github's [issues](https://github.com/dydxprotocol/trader-fe/issues)
Report a bug by [opening a new issue](https://github.com/dydxprotocol/trader-fe/issues/new).
## Report bugs using Github's [issues](https://github.com/dydxprotocol/v4-web/issues)
Report a bug by [opening a new issue](https://github.com/dydxprotocol/v4-web/issues/new).
**Great Bug Reports** tend to have:
+9 -6
View File
@@ -1,16 +1,19 @@
Business Source License 1.1
License text copyright © 2023 MariaDB plc, All Rights Reserved. “Business Source License” is a trademark of MariaDB plc.
Copyright (C) 2023 dYdX Trading Inc.
Subject to your compliance with applicable law, you are granted the right to use the Licensed Work (defined below) under the terms of the below licenses; provided, however, that if you violate any such applicable law in your use of the Licensed Work, all of your rights and licenses to use (including any rights to reproduce, distribute, install or modify) the Licensed Work will automatically and immediately terminate.
Use of this software before the Change Date specified in the LICENSE file is governed by the Business Source License included in the LICENSE file and at https://spdx.org/licenses/BUSL-1.1.html.
On and after the Change Date specified in the LICENSE file, use of this software will be governed by the Change License (GNU Affero GPL v3) as specified in the LICENSE file.
Business Source License 1.1
Business Source License text copyright © 2023 MariaDB plc, All Rights Reserved. “Business Source License” is a trademark of MariaDB plc.
Copyright (C) 2023 dYdX Trading Inc.
Parameters
Licensor: dYdX Trading Inc.
Licensed Work: dydxprotocol/cosmos-sdk, dydxprotocol/cometbft, dydxprotocol/v4-chain, dydxprotocol/v4-clients, dydxprotocol/v4-web, dydxprotocol/v4-abacus, dydxprotocol/v4-localization, dydxprotocol/v4-documentation, and any dYdX or dYdX Trading Inc. github repository reflecting a copy of this license, or link to this license.
Additional Use Grant: None
Change Date: On the later to occur of (i) the release of the Licensed Work labeled “dYdX Version 1.0 GPL”, and which includes a document uploaded to the relevant github repository stating “The software in this repository is Version 1.0, subject to GPL license”, and (ii) September 30, 2023 (approximate projected release of “Version 1.0” of this software).
Change Date: On the later to occur of (i) the release of the Licensed Work labeled “dYdX Version 1.0 Affero GPL”, and which includes a document uploaded to the relevant github repository stating “The software in this repository is Version 1.0, subject to Affero GPL license”, and (ii) September 30, 2023 (approximate projected release of “Version 1.0” of this software).
Change License: GNU Affero GPL License v3
License text copyright © 2023 MariaDB plc, All Rights Reserved. “Business Source License” is a trademark of MariaDB plc.
@@ -39,7 +42,7 @@ OPEN SOURCE LICENSE BELOW - TO BE APPLIED AFTER THE CHANGE DATE.
Copyright (C) 2023 dYdX Trading Inc.
Use of this software is governed by the The GNU Affero General Public License as set forth below.
Subject to your compliance with applicable law, you are granted the right to use the Licensed Work (defined below) under the terms of the GNU Affero General Public License as set forth below; provided, however, that if you violate any such applicable law in your use of the Licensed Work, all of your rights and licenses to use (including any rights to reproduce, distribute, install or modify) the Licensed Work will automatically and immediately terminate.
The GNU Affero General Public License
@@ -835,4 +838,4 @@ copy of the Program in return for a fee.
For more information about this software, see https://dydx.exchange.
Copyright (C) 2023 dYdX Trading Inc.
Copyright (C) 2023 dYdX Trading Inc.
+11 -3
View File
@@ -1,4 +1,12 @@
# v4 Web App
<p align="center"><img src="https://dydx.exchange/icon.svg?" width="256" /></p>
<h1 align="center">v4 Web</h1>
<div align="center">
<a href='https://github.com/dydxprotocol/v4-web/blob/main/LICENSE'>
<img src='https://img.shields.io/badge/License-BSL_1.1-blue' alt='License' />
</a>
</div>
## Prerequisites
@@ -22,8 +30,8 @@ Alternatively, follow the [IPFS Kubo installation guide](https://docs.ipfs.tech
Clone the repository and navigate to its directory:
```bash
git clone https://github.com/dydxprotocol/trader-fe.git
cd trader-fe
git clone https://github.com/dydxprotocol/v4-web.git
cd v4-web
```
### Step 2: Install pnpm and dependencies
+4 -4
View File
@@ -30,16 +30,16 @@
},
"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",
"@cosmjs/tendermint-rpc": "^0.31.0",
"@dydxprotocol/abacus": "^0.4.1",
"@dydxprotocol/v4-client-js": "^0.32.0",
"@dydxprotocol/v4-localization": "^0.0.21",
"@dydxprotocol/v4-abacus": "^0.5.4",
"@dydxprotocol/v4-client-js": "^0.36.1",
"@dydxprotocol/v4-localization": "^0.1.12",
"@ethersproject/providers": "^5.7.2",
"@js-joda/core": "^5.5.3",
"@radix-ui/react-collapsible": "^1.0.3",
+229 -2014
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

+35 -29
View File
@@ -10,11 +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 { DialogAreaProvider, useDialogArea } from './hooks/useDialogArea';
import { LocaleProvider } from './hooks/useLocaleSeparators';
import { NotificationsProvider } from './hooks/useNotifications';
import { SubaccountProvider } from './hooks/useSubaccount';
import { SquidProvider } from '@/hooks/useSquidRouter';
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';
@@ -98,29 +99,32 @@ const Content = () => {
);
};
const App = () => (
<QueryClientProvider client={queryClient}>
<GrazProvider>
<WagmiConfig config={config}>
<LocaleProvider>
<DydxProvider>
<AccountsProvider>
<SubaccountProvider>
<NotificationsProvider>
<DialogAreaProvider>
<SquidProvider>
<Content />
</SquidProvider>
</DialogAreaProvider>
</NotificationsProvider>
</SubaccountProvider>
</AccountsProvider>
</DydxProvider>
</LocaleProvider>
</WagmiConfig>
</GrazProvider>
</QueryClientProvider>
);
const wrapProvider = (Component: React.ComponentType<any>, props?: any) => {
// eslint-disable-next-line react/display-name
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> = {};
@@ -192,7 +196,9 @@ Styled.NotificationsToastArea = styled(NotificationsToastArea)`
`;
Styled.DialogArea = styled.aside`
position: absolute;
position: fixed;
height: 100vh;
z-index: 1;
inset: 0;
overflow: clip;
${layoutMixins.noPointerEvents}
+222
View File
@@ -0,0 +1,222 @@
[
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_spender",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_from",
"type": "address"
},
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "balance",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
},
{
"name": "_spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"payable": true,
"stateMutability": "payable",
"type": "fallback"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "owner",
"type": "address"
},
{
"indexed": true,
"name": "spender",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "from",
"type": "address"
},
{
"indexed": true,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
}
]
+14 -4
View File
@@ -82,7 +82,9 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
<Styled.Item
// value={item.value} // search by both value and textContent
// value={[group.groupLabel, item.label, item.tag].filter(Boolean).join('|')} // exclude item.value from searchable terms (not guaranteed to be unique)
value={[group.groupLabel, item.value, item.description, item.label, item.tag].filter(Boolean).join('|')}
value={[group.groupLabel, item.value, item.description, item.label, item.tag]
.filter(Boolean)
.join('|')}
onSelect={() => {
if (item.subitems) {
// open submenu
@@ -116,7 +118,15 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
<Styled.Item
// value={subitem.value} // search by both value and textContent
// value={[group.groupLabel, item.label, subitem.label, subitem.tag].filter(Boolean).join('|')}
value={[group.groupLabel, item.value, item.description, item.label, item.tag].filter(Boolean).join('|')}
value={[
group.groupLabel,
item.value,
item.description,
item.label,
item.tag,
]
.filter(Boolean)
.join('|')}
onSelect={() => {
subitem.onSelect?.(subitem.value);
onItemSelected?.();
@@ -168,7 +178,7 @@ Styled.Command = styled(Command)<{ $withStickyLayout?: boolean }>`
background-color: var(--comboboxMenu-backgroundColor);
border-radius: inherit;
input:focus {
input:focus-visible {
outline: none;
}
@@ -272,7 +282,7 @@ Styled.Item = styled(Command.Item)`
align-items: center;
gap: 0.5rem;
&[aria-disabled="true"] {
&[aria-disabled='true'] {
opacity: 0.75;
cursor: not-allowed;
}
+2 -5
View File
@@ -90,10 +90,7 @@ export const Dialog = ({
return (
<Root modal={showOverlay} open={isOpen} onOpenChange={setIsOpen}>
{slotTrigger && <Trigger asChild>{slotTrigger}</Trigger>}
<DialogPortal
withPortal={placement !== DialogPlacement.Inline}
container={portalContainer}
>
<DialogPortal withPortal={placement !== DialogPlacement.Inline} container={portalContainer}>
{showOverlay && <Styled.Overlay />}
<Styled.Container
placement={placement}
@@ -425,7 +422,7 @@ Styled.Close = styled(Close)`
}
&:hover,
&:focus {
&:focus-visible {
color: var(--color-text-2);
}
+1 -1
View File
@@ -137,7 +137,7 @@ Styled.ComboboxMenu = styled(ComboboxMenu)`
--stickyArea1-topHeight: var(--form-input-height);
input:focus {
input:focus-visible {
outline: none;
}
+2 -2
View File
@@ -743,7 +743,7 @@ Styled.Tr = styled.tr<{
cursor: pointer;
&:hover,
&:focus,
&:focus-visible,
&:focus-within {
--tableRow-currentBackgroundColor: var(--tableRow-hover-backgroundColor);
filter: brightness(1.1);
@@ -753,7 +753,7 @@ Styled.Tr = styled.tr<{
${({ withFocusStickyRows }) =>
withFocusStickyRows &&
css`
&:focus,
&:focus-visible,
&:focus-within {
${layoutMixins.sticky}
z-index: 1;
+13 -11
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
@@ -150,7 +152,7 @@ const $Root = styled(Root)`
}
}
&:focus {
&:focus-visible {
outline: none;
}
@@ -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']) & {
+1 -1
View File
@@ -91,7 +91,7 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.Content = styled(Content)`
z-index: 1;
&:focus {
&:focus-visible {
outline: none;
}
`;
+10 -1
View File
@@ -1,4 +1,4 @@
import Abacus, { kollections } from '@dydxprotocol/abacus';
import Abacus, { kollections } from '@dydxprotocol/v4-abacus';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { PositionSide, TradeTypes } from './trade';
import { STRING_KEYS } from './localization';
@@ -40,6 +40,10 @@ export type AbacusFileSystemProtocol = Omit<
Abacus.exchange.dydx.abacus.protocols.FileSystemProtocol,
'__doNotUseOrImplementIt'
>;
export type AbacusTrackingProtocol = Omit<
Abacus.exchange.dydx.abacus.protocols.TrackingProtocol,
'__doNotUseOrImplementIt'
>;
export type FileLocation = Abacus.exchange.dydx.abacus.protocols.FileLocation;
export type ThreadingType = Abacus.exchange.dydx.abacus.protocols.ThreadingType;
@@ -118,6 +122,7 @@ export const InputSelectionOption = Abacus.exchange.dydx.abacus.output.input.Sel
// ------ Wallet ------ //
export type Wallet = Abacus.exchange.dydx.abacus.output.Wallet;
export type AccountBalance = Abacus.exchange.dydx.abacus.output.AccountBalance;
export type Subaccount = Abacus.exchange.dydx.abacus.output.Subaccount;
export type SubaccountPosition = Abacus.exchange.dydx.abacus.output.SubaccountPosition;
export type SubaccountOrder = Abacus.exchange.dydx.abacus.output.SubaccountOrder;
@@ -184,6 +189,10 @@ export type HumanReadablePlaceOrderPayload =
Abacus.exchange.dydx.abacus.state.manager.HumanReadablePlaceOrderPayload;
export type HumanReadableCancelOrderPayload =
Abacus.exchange.dydx.abacus.state.manager.HumanReadableCancelOrderPayload;
export type HumanReadableWithdrawPayload =
Abacus.exchange.dydx.abacus.state.manager.HumanReadableWithdrawPayload;
export type HumanReadableTransferPayload =
Abacus.exchange.dydx.abacus.state.manager.HumanReadableTransferPayload;
// ------ Helpers ------ //
export const AbacusHelper = Abacus.exchange.dydx.abacus.utils.AbacusHelper;
+5 -20
View File
@@ -1,18 +1,5 @@
/**
* OnboardingSteps
* 1. Choose between 3 options
* a. Ethereum EOA (current)
* b. Keplr or Other Cosmos (future)
* c. Social (future)
* 2. Key derivation
* a. If wallet has no dYdX Chain transactions and not on whitelist, sign twice (future)
* i. Success
* ii. Signatures don't match error (Wallet is non-deterministic)
* b. Success
* 3. Post Registration items (Only on first onboarding)
* a. Acknowledge Terms
* b. DepositFunds
*/
import type { DydxAddress, EvmAddress } from './wallets';
export enum OnboardingSteps {
ChooseWallet = 'ChooseWallet',
KeyDerivation = 'KeyDerivation',
@@ -49,14 +36,12 @@ export enum EvmDerivedAccountStatus {
Derived,
}
import type { DydxAddress, EthereumAddress } from './wallets';
export type EvmDerivedAddresses = {
version?: string;
[EthereumAddress: EthereumAddress]: {
[EvmAddress: EvmAddress]: {
encryptedSignature?: string;
dydxAddress?: DydxAddress;
};
}
};
export const AMOUNT_RESERVED_FOR_GAS_USDC = 100_000;
export const AMOUNT_RESERVED_FOR_GAS_USDC = 100_000;
+16 -4
View File
@@ -1,7 +1,7 @@
import type { SupportedLocales } from './localization';
import type { DydxNetwork } from './networks';
import type { OnboardingState, OnboardingSteps } from './account';
import type { DydxAddress, WalletType, WalletConnectionType, EthereumAddress } from './wallets';
import type { DydxAddress, WalletType, WalletConnectionType, EvmAddress } from './wallets';
import type { DialogTypes } from './dialogs';
import type { TradeTypes } from './trade';
import type { AbacusApiStatus, HumanReadablePlaceOrderPayload } from './abacus';
@@ -40,7 +40,7 @@ export type AnalyticsUserPropertyValue<T extends AnalyticsUserProperty> =
: T extends AnalyticsUserProperty.WalletConnectionType
? WalletConnectionType | undefined
: T extends AnalyticsUserProperty.WalletAddress
? EthereumAddress | DydxAddress | undefined
? EvmAddress | DydxAddress | undefined
: // Account
T extends AnalyticsUserProperty.DydxAddress
? DydxAddress | undefined
@@ -67,9 +67,11 @@ export enum AnalyticsEvent {
// Onboarding
OnboardingStepChanged = 'OnboardingStepChanged',
OnboardingAccountDerived = 'OnboardingAccountDerived',
OnboardingWalletIsNonDeterministic = 'OnboardingWalletIsNonDeterministic',
// Transfers
TransferFaucet = 'TransferFaucet',
TransferFaucetConfirmed = 'TransferFaucetConfirmed',
TransferDeposit = 'TransferDeposit',
TransferWithdraw = 'TransferWithdraw',
@@ -77,6 +79,7 @@ export enum AnalyticsEvent {
TradeOrderTypeSelected = 'TradeOrderTypeSelected',
TradePlaceOrder = 'TradePlaceOrder',
TradePlaceOrderConfirmed = 'TradePlaceOrderConfirmed',
TradeCancelOrder = 'TradeCancelOrder',
TradeCancelOrderConfirmed = 'TradeCancelOrderConfirmed',
}
@@ -132,6 +135,13 @@ export type AnalyticsEventData<T extends AnalyticsEvent> =
: // Transfers
T extends AnalyticsEvent.TransferFaucet
? {}
: T extends AnalyticsEvent.TransferFaucetConfirmed
? {
/** 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 */
validatorUrl: string;
}
: T extends AnalyticsEvent.TransferDeposit
? {}
: T extends AnalyticsEvent.TransferWithdraw
@@ -150,13 +160,15 @@ export type AnalyticsEventData<T extends AnalyticsEvent> =
/** 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;
validatorUrl: string;
}
: T extends AnalyticsEvent.TradeCancelOrder
? {}
: 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;
validatorUrl: string;
}
: never;
+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',
+2 -16
View File
@@ -34,6 +34,8 @@ export const STRING_KEYS = {
...WARNINGS_STRING_KEYS,
};
export type StringKey = keyof typeof STRING_KEYS;
export type LocaleData = typeof EN_LOCALE_DATA;
export type StringGetterFunction = (a: {
@@ -86,19 +88,3 @@ export type TooltipStrings = {
learnMoreLink?: string;
};
};
export const ORDER_ERROR_CODE_MAP: { [key: number]: string } = {
2000: '2000_FILL_OR_KILL_ORDER_COULD_NOT_BE_FULLY_FILLED',
2001: '2001_REDUCE_ONLY_WOULD_INCREASE_POSITION_SIZE',
2002: '2002_REDUCE_ONLY_WOULD_CHANGE_POSITION_SIDE',
2003: '2003_POST_ONLY_WOULD_CROSS_MAKER_ORDER',
3000: '3000_INVALID_ORDER_FLAGS',
3001: '3001_INVALID_STATEFUL_ORDER_GOOD_TIL_BLOCK_TIME',
3002: '3002_STATEFUL_ORDERS_CANNOT_REQUIRE_IMMEDIATE_EXECUTION',
3003: '3003_TIME_EXCEEDS_GOOD_TIL_BLOCK_TIME',
3004: '3004_GOOD_TIL_BLOCK_TIME_EXCEEDS_STATEFUL_ORDER_TIME_WINDOW',
3005: '3005_STATEFUL_ORDER_ALREADY_EXISTS',
3006: '3006_STATEFUL_ORDER_DOES_NOT_EXIST',
3007: '3007_STATEFUL_ORDER_COLLATERALIZATION_CHECK_FAILED',
3008: '3008_STATEFUL_ORDER_PREVIOUSLY_CANCELLED',
};
-30
View File
@@ -1,36 +1,6 @@
import { Asset, PerpetualMarket } from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization';
export interface Market {
market: string;
status: string;
baseAsset: string;
quoteAsset: string;
stepSize: string;
tickSize: string;
oraclePrice: string;
priceChange24H: string;
nextFundingRate: string;
nextFundingAt: string;
minOrderSize: string;
type: string;
initialMarginFraction: string;
maintenanceMarginFraction: string;
volume24H: string;
trades24H: string;
openInterest: string;
incrementalInitialMarginFraction: string;
incrementalPositionSize: string;
maxPositionSize: string;
baselinePositionSize: string;
assetResolution: string;
syntheticAssetId: string;
}
export type AllMarkets = {
[market: string]: Market;
};
export type MarketData = {
asset: Asset;
tickSizeDecimals: number;
+32 -9
View File
@@ -1,20 +1,26 @@
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<
NotificationIdType 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 */
id: _NotificationId,
id: NotificationIdType,
/** Display data for the triggered notification */
displayData: NotificationDisplayData,
@@ -30,14 +36,14 @@ 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;
}) => void;
/** Callback for notification action (Toast action button click, NotificationsMenu item click, or native push notification interaction) */
useNotificationAction?: () => (id: _NotificationId) => any;
useNotificationAction?: () => (id: NotificationIdType) => any;
};
export enum NotificationStatus {
@@ -58,8 +64,11 @@ export enum NotificationStatus {
}
/** Notification state. Serialized and cached into localStorage. */
export type Notification<_NotificationId extends NotificationId = string, NotificationUpdateKey = any> = {
id: _NotificationId;
export type Notification<
NotificationIdType extends NotificationId = string,
NotificationUpdateKey = any
> = {
id: NotificationIdType;
type: NotificationType;
status: NotificationStatus;
timestamps: Partial<Record<NotificationStatus, number>>;
@@ -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;
};
-2
View File
@@ -1,5 +1,3 @@
import { getSeparator } from '@/lib/numbers';
export const USD_DECIMALS = 2;
export const SMALL_USD_DECIMALS = 4;
+8
View File
@@ -5,6 +5,10 @@ export const tradeTooltips: TooltipStrings = {
title: stringGetter({ key: TOOLTIP_STRING_KEYS.ACCOUNT_LEVERAGE_TITLE }),
body: stringGetter({ key: TOOLTIP_STRING_KEYS.ACCOUNT_LEVERAGE_BODY }),
}),
'base-position-notional': ({ stringGetter }) => ({
title: stringGetter({ key: TOOLTIP_STRING_KEYS.BASE_POSITION_NOTIONAL_TITLE }),
body: stringGetter({ key: TOOLTIP_STRING_KEYS.BASE_POSITION_NOTIONAL_BODY }),
}),
'bracket-sl': ({ stringGetter }) => ({
title: stringGetter({ key: TOOLTIP_STRING_KEYS.BRACKET_ORDER_SL_TITLE }),
body: stringGetter({ key: TOOLTIP_STRING_KEYS.BRACKET_ORDER_SL_BODY }),
@@ -90,6 +94,10 @@ export const tradeTooltips: TooltipStrings = {
title: stringGetter({ key: TOOLTIP_STRING_KEYS.MAINTENANCE_MARGIN_FRACTION_TITLE }),
body: stringGetter({ key: TOOLTIP_STRING_KEYS.MAINTENANCE_MARGIN_FRACTION_BODY }),
}),
'max-reward': ({ stringGetter }) => ({
title: stringGetter({ key: TOOLTIP_STRING_KEYS.MAXIMUM_REWARDS_TITLE }),
body: stringGetter({ key: TOOLTIP_STRING_KEYS.MAXIMUM_REWARDS_BODY }),
}),
'max-withdraw': ({ stringGetter }) => ({
title: stringGetter({ key: TOOLTIP_STRING_KEYS.MAX_WITHDRAW_TITLE }),
body: stringGetter({ key: TOOLTIP_STRING_KEYS.MAX_WITHDRAW_BODY }),
+1 -1
View File
@@ -27,7 +27,7 @@ export enum PositionSide {
Short = 'SHORT',
}
export const UNCOMMITTED_ORDER_TIMEOUT = 10_000;
export const UNCOMMITTED_ORDER_TIMEOUT_MS = 10_000;
export const ORDER_SIDE_STRINGS = {
[OrderSide.BUY]: STRING_KEYS.BUY,
+27 -11
View File
@@ -1,4 +1,6 @@
import type { ExternalProvider } from '@ethersproject/providers';
import { USDC_DENOM, type onboarding, DYDX_DENOM } from '@dydxprotocol/v4-client-js';
import type { suggestChain } from 'graz';
import { STRING_KEYS } from '@/constants/localization';
@@ -32,6 +34,18 @@ export enum WalletConnectionType {
WalletConnect2 = 'walletConnect2',
}
export enum WalletErrorType {
// General
ChainMismatch,
UserCanceled,
// Non-Deterministic
NonDeterministicWallet,
// Misc
Unknown,
}
type WalletConnectionTypeConfig = {
name: string;
wagmiConnectorId?: string;
@@ -301,10 +315,6 @@ export type WalletConnection = {
};
// dYdX Chain wallets
import { USDC_DENOM, type onboarding, DYDX_DENOM } from '@dydxprotocol/v4-client-js';
import type { suggestChain } from 'graz';
export const COSMOS_DERIVATION_PATH = "m/44'/118'/0'/0/0";
/**
@@ -325,16 +335,16 @@ export const SIGN_TYPED_DATA = {
export type PrivateInformation = ReturnType<typeof onboarding.deriveHDKeyFromEthereumSignature>;
export type EthereumAddress = `0x${string}`;
export type EvmAddress = `0x${string}`;
export type DydxAddress = `dydx${string}`;
export const DYDX_CHAIN_INFO: Parameters<typeof suggestChain>[0] = {
rpc: '13.59.4.93:26657',
rest: '13.59.4.93:1317',
chainId: 'dydx-testnet-2',
rpc: 'https://dydx-testnet-archive.allthatnode.com:26657',
rest: 'https://dydx-testnet-archive.allthatnode.com:1317',
chainId: 'dydx-testnet-3',
chainName: 'dYdX Public Testnet',
chainSymbolImageUrl:
'https://raw.githubusercontent.com/chainapsis/keplr-chain-registry/main/images/dydx-testnet-2/chain.png',
'https://raw.githubusercontent.com/chainapsis/keplr-chain-registry/main/images/dydx-testnet-3/chain.png',
bech32Config: {
bech32PrefixAccPub: 'dydxpub',
bech32PrefixValPub: 'dydxvaloperpub',
@@ -373,12 +383,18 @@ export const DYDX_CHAIN_INFO: Parameters<typeof suggestChain>[0] = {
features: [],
};
// TODO: export this type from abacus instead
export enum DydxChainAsset {
USDC = 'USDC',
DYDX = 'Dv4TNT',
USDC = 'usdc',
DYDX = 'dydx',
}
export const DYDX_CHAIN_ASSET_COIN_DENOM: Record<DydxChainAsset, string> = {
[DydxChainAsset.USDC]: USDC_DENOM,
[DydxChainAsset.DYDX]: DYDX_DENOM,
};
export const DYDX_CHAIN_ASSET_TAGS: Record<DydxChainAsset, string> = {
[DydxChainAsset.USDC]: 'USDC',
[DydxChainAsset.DYDX]: 'Dv4TNT',
};
+14 -7
View File
@@ -1,21 +1,23 @@
import { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { shallowEqual, useSelector } from 'react-redux';
import { useBalance } from 'wagmi';
import { StargateClient } from '@cosmjs/stargate';
import { useQuery } from 'react-query';
import { formatUnits } from 'viem';
import { USDC_DENOM, DYDX_DENOM } from '@dydxprotocol/v4-client-js';
import { CLIENT_NETWORK_CONFIGS } from '@/constants/networks';
import { QUANTUM_MULTIPLIER } from '@/constants/numbers';
import { EthereumAddress } from '@/constants/wallets';
import { EvmAddress } from '@/constants/wallets';
import { convertBech32Address } from '@/lib/addressUtils';
import { MustBigNumber } from '@/lib/numbers';
import { getBalances } from '@/state/accountSelectors';
import { getSelectedNetwork } from '@/state/appSelectors';
import { useAccounts } from './useAccounts';
import { usePollNativeTokenBalance } from './usePollNativeTokenBalance';
type UseAccountBalanceProps = {
// Token Items
@@ -35,10 +37,10 @@ type UseAccountBalanceProps = {
* 0xSquid uses this 0x address as the chain's default token.
* @todo We will need to add additional logic here if we 'useAccountBalance' on non-Squid related forms.
*/
const CHAIN_DEFAULT_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
export const CHAIN_DEFAULT_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
export const useAccountBalance = ({
addressOrDenom = CHAIN_DEFAULT_TOKEN_ADDRESS,
addressOrDenom,
assetSymbol,
bech32AddrPrefix,
chainId,
@@ -49,6 +51,7 @@ export const useAccountBalance = ({
const { evmAddress, dydxAddress } = useAccounts();
const selectedNetwork = useSelector(getSelectedNetwork);
const balances = useSelector(getBalances, shallowEqual);
const evmChainId = Number(CLIENT_NETWORK_CONFIGS[selectedNetwork].ethereumChainId);
const evmQuery = useBalance({
@@ -58,7 +61,7 @@ export const useAccountBalance = ({
token:
addressOrDenom === CHAIN_DEFAULT_TOKEN_ADDRESS
? undefined
: (addressOrDenom as EthereumAddress),
: (addressOrDenom as EvmAddress),
watch: true,
});
@@ -91,14 +94,18 @@ export const useAccountBalance = ({
const { formatted: evmBalance } = evmQuery.data || {};
const balance = !assetSymbol ? '0' : isCosmosChain ? cosmosQuery.data : evmBalance;
const nativeTokenCoinBalance = usePollNativeTokenBalance({ dydxAddress });
const nativeTokenCoinBalance = balances?.[DYDX_DENOM];
const nativeTokenBalance = MustBigNumber(nativeTokenCoinBalance?.amount)
.div(QUANTUM_MULTIPLIER)
.toNumber();
const usdcCoinBalance = balances?.[USDC_DENOM];
const usdcBalance = MustBigNumber(usdcCoinBalance?.amount).div(QUANTUM_MULTIPLIER).toNumber();
return {
balance,
nativeTokenBalance,
usdcBalance,
queryStatus: isCosmosChain ? cosmosQuery.status : evmQuery.status,
isQueryFetching: isCosmosChain ? cosmosQuery.isFetching : evmQuery.fetchStatus === 'fetching',
};
+6 -9
View File
@@ -6,12 +6,9 @@ import { LocalWallet, USDC_DENOM, type Subaccount } from '@dydxprotocol/v4-clien
import { OnboardingGuard, OnboardingState, type EvmDerivedAddresses } from '@/constants/account';
import { LocalStorageKey, LOCAL_STORAGE_VERSIONS } from '@/constants/localStorage';
import { DydxAddress, EthereumAddress, PrivateInformation } from '@/constants/wallets';
import { DydxAddress, EvmAddress, PrivateInformation } from '@/constants/wallets';
import {
setOnboardingState,
setOnboardingGuard,
} from '@/state/account';
import { setOnboardingState, setOnboardingGuard } from '@/state/account';
import abacusStateManager from '@/lib/abacus';
import { log } from '@/lib/telemetry';
@@ -82,7 +79,7 @@ const useAccountsContext = () => {
evmAddress,
dydxAddress,
}: {
evmAddress: EthereumAddress;
evmAddress: EvmAddress;
dydxAddress?: DydxAddress;
}) => {
saveEvmDerivedAddresses({
@@ -192,7 +189,7 @@ const useAccountsContext = () => {
} catch (error) {
log('useAccounts/setLocalDydxWallet', error);
}
} else if (evmAddress && signerWagmi) {
} else if (evmAddress) {
if (!localDydxWallet) {
dispatch(setOnboardingState(OnboardingState.WalletConnected));
@@ -222,9 +219,9 @@ const useAccountsContext = () => {
// abacus
// TODO: useAbacus({ dydxAddress })
useEffect(() => {
if (dydxAddress) abacusStateManager.setAccount(dydxAddress);
if (dydxAddress) abacusStateManager.setAccount(localDydxWallet);
else abacusStateManager.attemptDisconnectAccount();
}, [dydxAddress]);
}, [localDydxWallet]);
// clear subaccounts when no dydxAddress is set
useEffect(() => {
+1 -1
View File
@@ -4,7 +4,7 @@ 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 { AppRoute } from '@/constants/routes';
import { getSelectedNetwork } from '@/state/appSelectors';
import { closeDialogInTradeBox } from '@/state/dialogs';
+8 -5
View File
@@ -57,8 +57,8 @@ const useDydxClientContext = () => {
networkConfig?.indexerSocketUrl &&
networkConfig?.validatorUrl
) {
setCompositeClient(
await CompositeClient.connect(
try {
const initializedClient = await CompositeClient.connect(
new Network(
selectedNetwork,
new IndexerConfig(networkConfig.indexerUrl, networkConfig.indexerSocketUrl),
@@ -67,8 +67,11 @@ const useDydxClientContext = () => {
broadcastTimeoutMs: 60_000,
})
)
)
);
);
setCompositeClient(initializedClient);
} catch (error) {
log('useDydxClient/initializeCompositeClient', error);
}
} else {
setCompositeClient(undefined);
}
@@ -111,7 +114,7 @@ const useDydxClientContext = () => {
}): Promise<Candle[]> => {
try {
const { candles } =
(await compositeClient!.indexerClient.markets.getPerpetualMarketCandles(
(await compositeClient?.indexerClient.markets.getPerpetualMarketCandles(
marketId,
RESOLUTION_MAP[resolution],
fromIso,
+101
View File
@@ -0,0 +1,101 @@
import { createContext, useContext, useCallback, useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import type { StatusResponse } from '@0xsquid/sdk';
import { LocalStorageKey } from '@/constants/localStorage';
import { type TransferNotifcation } from '@/constants/notifications';
import { useAccounts } from '@/hooks/useAccounts';
import { SQUID_ERROR_TYPES, useSquid } from '@/hooks/useSquid';
import { useLocalStorage } from './useLocalStorage';
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) {
try {
if (currentStatus && currentStatus?.squidTransactionStatus !== 'ongoing') continue;
const status = await squid?.getStatus({ transactionId: txHash, toChainId, fromChainId });
if (status) statuses[txHash] = status;
} catch (error) {
// ignore not found errors since the route might not be available yet
if (error?.errors?.length && error.errors[0].errorType !== SQUID_ERROR_TYPES.NotFoundError) {
statuses[txHash] = error;
}
}
}
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,
};
};
+1 -1
View File
@@ -29,7 +29,7 @@ export const useMarketsData = (
const markets = useMemo(() => {
return Object.values(allPerpetualMarkets)
.filter(({ assetId }) => allAssets[assetId]) // Filter out markets with no asset information
.filter((market) => allAssets[market?.assetId]) // Filter out markets with no asset information
.map((marketData) => ({
asset: allAssets[marketData.assetId],
tickSizeDecimals: marketData.configs?.tickSizeDecimals,
+93 -1
View File
@@ -1,19 +1,30 @@
import { useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { useSelector, shallowEqual, useDispatch } from 'react-redux';
import { groupBy } from 'lodash';
import { AlertType } from '@/constants/alerts';
import { AbacusOrderStatus, ORDER_SIDES, ORDER_STATUS_STRINGS } from '@/constants/abacus';
import { DialogTypes } from '@/constants/dialogs';
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 { AlertMessage } from '@/components/AlertMessage';
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-js';
export const notificationTypes = [
{
@@ -93,4 +104,85 @@ 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';
// @ts-ignore status.errors is not in the type definition but can be returned
const error = status?.errors?.length ? status?.errors[0] : status?.error;
// TODO: confirm with design what the description should be
const description = (
<div>
<Styled.TransferText>
{type === 'deposit' ? 'Deposit of ' : 'Withdraw of '}
<Output type={OutputType.Fiat} value={toAmount} />
</Styled.TransferText>
{error && (
<Styled.ErrorMessage type={AlertType.Error}>
{stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: {
ERROR_MESSAGE: error.message || stringGetter({ key: STRING_KEYS.UNKNOWN_ERROR }),
},
})}
</Styled.ErrorMessage>
)}
</div>
);
trigger(
txHash,
{
icon: <Icon iconName={finished ? IconName.Transfer : IconName.Clock} />,
title: stringGetter({ key: getTitleStringKey(type, finished) }),
description: description,
customContent: (
<TransferStatusToast
toAmount={transfer.toAmount}
triggeredAt={transfer.triggeredAt}
status={transfer.status}
/>
),
customMenuContent: !finished && (
<div>
{description}
<TransferStatusSteps status={transfer.status} />
</div>
),
toastSensitivity: 'foreground',
},
[]
);
}
}, [transferNotifications]);
},
},
] satisfies NotificationTypeConfig[];
const Styled: Record<string, AnyStyledComponent> = {};
Styled.TransferText = styled.span`
display: inline-flex;
align-items: center;
gap: 0.5ch;
`
Styled.ErrorMessage = styled.div`
max-width: 13rem;
`;
+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,
};
};
// }
+26
View File
@@ -0,0 +1,26 @@
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { getLatestOrderClientId } from '@/state/accountSelectors';
/**
* @description This hook will fire a callback when the latest order has been updated to a non-pending state from the Indexer.
* @param { callback: () => void }
*/
export const useOnLastOrderIndexed = ({ callback }: { callback: () => void }) => {
const [unIndexedClientId, setUnIndexedClientId] = useState<number | undefined>();
const latestOrderClientId = useSelector(getLatestOrderClientId);
useEffect(() => {
if (unIndexedClientId) {
if (unIndexedClientId === latestOrderClientId) {
callback();
setUnIndexedClientId(undefined);
}
}
}, [callback, latestOrderClientId, unIndexedClientId]);
return {
setUnIndexedClientId,
};
};
-31
View File
@@ -1,31 +0,0 @@
import { useQuery } from 'react-query';
import { useAccounts } from '@/hooks';
import { DydxAddress } from '@/constants/wallets';
import { DYDX_DENOM } from '@dydxprotocol/v4-client-js';
const ACCOUNT_BALANCE_POLLING_INTERVAL = 60_000;
export const usePollNativeTokenBalance = ({
dydxAddress,
interval = ACCOUNT_BALANCE_POLLING_INTERVAL,
}: {
dydxAddress?: DydxAddress;
interval?: number;
}) => {
const { getAccountBalance } = useAccounts();
const { data } = useQuery({
enabled: dydxAddress !== undefined,
queryKey: ['usePollNativeTokenBalance', { dydxAddress }],
queryFn: async () => {
if (!dydxAddress) return;
return await getAccountBalance({ dydxAddress, denom: DYDX_DENOM });
},
refetchInterval: interval,
staleTime: interval,
});
return data;
};
-30
View File
@@ -1,30 +0,0 @@
import { useQuery } from 'react-query';
import { useAccounts } from '@/hooks';
import { DydxAddress } from '@/constants/wallets';
const ACCOUNT_BALANCE_POLLING_INTERVAL = 60_000;
export const usePollUSDCBalance = ({
dydxAddress,
interval = ACCOUNT_BALANCE_POLLING_INTERVAL,
}: {
dydxAddress?: DydxAddress;
interval?: number;
}) => {
const { getAccountBalance } = useAccounts();
const { data } = useQuery({
enabled: dydxAddress !== undefined,
queryKey: ['usePollUSDCBalance', { dydxAddress }],
queryFn: async () => {
if (!dydxAddress) return;
return await getAccountBalance({ dydxAddress });
},
refetchInterval: interval,
staleTime: interval,
});
return data;
};
@@ -2,16 +2,15 @@ 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 { CLIENT_NETWORK_CONFIGS, 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',
};
export const NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
export enum SQUID_ERROR_TYPES {
NotFoundError = 'NotFoundError',
}
const useSquidContext = () => {
const selectedNetwork = useSelector(getSelectedNetwork);
@@ -27,7 +26,7 @@ const useSquidContext = () => {
const squid = useMemo(
() =>
isDydxV4Network(selectedNetwork)
? new Squid({ baseUrl: SQUID_BASE_URL[selectedNetwork as DydxV4Network] })
? new Squid({ baseUrl: CLIENT_NETWORK_CONFIGS[selectedNetwork]?.endpoints['0xsquid'] })
: undefined,
[selectedNetwork]
);
+56 -330
View File
@@ -1,53 +1,41 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux';
import type { Nullable } from '@dydxprotocol/abacus';
import { shallowEqual, useSelector, useDispatch } from 'react-redux';
import type { Nullable } from '@dydxprotocol/v4-abacus';
import Long from 'long';
import type { IndexedTx } from '@cosmjs/stargate';
import type { EncodeObject, Coin } from '@cosmjs/proto-signing';
import { Method } from '@cosmjs/tendermint-rpc';
import {
LocalWallet,
OrderExecution,
OrderFlags,
OrderSide,
OrderTimeInForce,
OrderType,
type LocalWallet,
SubaccountClient,
DYDX_DENOM,
USDC_DENOM,
GAS_PRICE_DYDX_DENOM,
} from '@dydxprotocol/v4-client-js';
import type {
HumanReadableCancelOrderPayload,
AccountBalance,
HumanReadablePlaceOrderPayload,
ParsingError,
SubAccountHistoricalPNLs,
} from '@/constants/abacus';
import { AMOUNT_RESERVED_FOR_GAS_USDC } from '@/constants/account';
import { AnalyticsEvent } from '@/constants/analytics';
import { ORDER_ERROR_CODE_MAP } from '@/constants/localization';
import { QUANTUM_MULTIPLIER } from '@/constants/numbers';
import { UNCOMMITTED_ORDER_TIMEOUT } from '@/constants/trade';
import { DydxAddress } from '@/constants/wallets';
import {
addUncommittedOrderClientId,
removeUncommittedOrderClientId,
setSubaccount,
setHistoricalPnl,
} from '@/state/account';
import { setSubaccount, setHistoricalPnl, removeUncommittedOrderClientId } from '@/state/account';
import { getBalances } from '@/state/accountSelectors';
import abacusStateManager from '@/lib/abacus';
import { track } from '@/lib/analytics';
import { StatefulOrderError } from '@/lib/errors';
import { MustBigNumber } from '@/lib/numbers';
import { log } from '@/lib/telemetry';
import { useAccounts } from './useAccounts';
import { useDydxClient } from './useDydxClient';
import { usePollUSDCBalance } from './usePollUSDCBalance';
type SubaccountContextType = ReturnType<typeof useSubaccountContext>;
const SubaccountContext = createContext<SubaccountContextType>({} as SubaccountContextType);
@@ -83,12 +71,8 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
const {
depositToSubaccount,
withdrawFromSubaccount,
simulateWithdrawFromSubaccount,
transferFromSubaccountToAddress,
transferNativeToken,
simulateTransferNativeToken,
placeOrderForSubaccount,
cancelOrderForSubaccount,
sendSquidWithdrawFromSubaccount,
} = useMemo(
() => ({
@@ -110,31 +94,6 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
amount: number;
}) => await compositeClient?.withdrawFromSubaccount(subaccountClient, amount),
simulateWithdrawFromSubaccount: async ({
subaccountClient,
amount,
recipient,
}: {
subaccountClient: SubaccountClient;
amount: number;
recipient?: string;
}) => {
return await compositeClient?.simulate(
subaccountClient?.wallet,
() =>
new Promise((resolve) => {
const msg = compositeClient?.withdrawFromSubaccountMessage(
subaccountClient,
amount,
recipient
);
resolve([msg]);
}),
undefined
);
},
transferFromSubaccountToAddress: async ({
subaccountClient,
assetId = 0,
@@ -185,125 +144,6 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
Method.BroadcastTxCommit
),
simulateTransferNativeToken: async ({
subaccountClient,
amount,
recipient,
}: {
subaccountClient: SubaccountClient;
amount: number;
recipient: string;
}) =>
await compositeClient?.simulate(
subaccountClient?.wallet,
() =>
new Promise((resolve) => {
const msg = compositeClient?.validatorClient.post.composer.composeMsgSendToken(
subaccountClient.address,
recipient,
DYDX_DENOM,
Long.fromNumber(amount * QUANTUM_MULTIPLIER)
);
resolve([msg]);
}),
GAS_PRICE_DYDX_DENOM,
undefined
),
placeOrderForSubaccount: async ({
subaccount,
marketId,
type,
side,
price,
triggerPrice,
size,
clientId,
timeInForce,
goodTilTimeInSeconds,
execution,
postOnly,
reduceOnly,
}: {
subaccount: SubaccountClient;
marketId: string;
type: OrderType;
side: OrderSide;
price: number;
triggerPrice: Nullable<number>;
size: number;
clientId: number;
timeInForce: OrderTimeInForce;
goodTilTimeInSeconds: number;
execution: OrderExecution;
postOnly: boolean;
reduceOnly: boolean;
}) => {
const startTimestamp = performance.now();
const result = await compositeClient?.placeOrder(
subaccount,
marketId,
type,
side,
price,
size,
clientId,
timeInForce,
goodTilTimeInSeconds,
execution,
postOnly,
reduceOnly,
triggerPrice ?? undefined
);
const endTimestamp = performance.now();
track(AnalyticsEvent.TradePlaceOrderConfirmed, {
roundtripMs: endTimestamp - startTimestamp,
validator: compositeClient!.validatorClient.config.restEndpoint,
});
return result;
},
cancelOrderForSubaccount: async ({
subaccount,
clientId,
clobPairId,
orderFlags,
goodTilBlock,
goodTilBlockTime,
}: {
subaccount: SubaccountClient;
clientId: number;
orderFlags: OrderFlags;
clobPairId: number;
goodTilBlock?: number;
goodTilBlockTime?: number;
}) => {
const startTimestamp = performance.now();
const result = await compositeClient?.cancelOrder(
subaccount,
clientId,
orderFlags,
clobPairId,
goodTilBlock,
goodTilBlockTime
);
const endTimestamp = performance.now();
track(AnalyticsEvent.TradeCancelOrderConfirmed, {
roundtripMs: endTimestamp - startTimestamp,
validator: compositeClient!.validatorClient.config.restEndpoint,
});
return result;
},
sendSquidWithdrawFromSubaccount: async ({
subaccountClient,
amount,
@@ -353,10 +193,10 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
// ------ Deposit/Withdraw Methods ------ //
const depositFunds = useCallback(
async (balance?: Coin) => {
async (balance: AccountBalance) => {
if (!localDydxWallet) return;
const amountAfterDust = MustBigNumber(balance?.amount)
const amountAfterDust = MustBigNumber(balance.amount)
.minus(AMOUNT_RESERVED_FOR_GAS_USDC) // keep 0.1 USDC in user's wallet for gas
.toString();
@@ -370,11 +210,14 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
[localDydxWallet, depositToSubaccount]
);
const balance = usePollUSDCBalance({ dydxAddress });
const balances = useSelector(getBalances, shallowEqual);
const usdcCoinBalance = balances?.[USDC_DENOM];
useEffect(() => {
depositFunds(balance);
}, [balance]);
if (usdcCoinBalance) {
depositFunds(usdcCoinBalance);
}
}, [usdcCoinBalance]);
const deposit = useCallback(
async (amount: Long) => {
@@ -413,30 +256,6 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
[subaccountClient, transferFromSubaccountToAddress, transferNativeToken]
);
const simulateTransfer = useCallback(
async (amount: number, recipient: string, coinDenom: string) => {
if (!subaccountClient) {
return;
}
return await (coinDenom === USDC_DENOM
? simulateWithdrawFromSubaccount
: simulateTransferNativeToken)({ subaccountClient, amount, recipient });
},
[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) {
@@ -467,100 +286,41 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
onSuccess,
}: {
isClosePosition?: boolean;
onError?: (onErrorParams?: { errorStringKey?: string }) => void;
onSuccess?: () => void;
onError?: (onErrorParams?: { errorStringKey?: Nullable<string> }) => void;
onSuccess?: (placeOrderPayload: Nullable<HumanReadablePlaceOrderPayload>) => void;
}) => {
let orderParams: Nullable<HumanReadablePlaceOrderPayload>;
const callback = (
success: boolean,
parsingError?: Nullable<ParsingError>,
data?: Nullable<HumanReadablePlaceOrderPayload>
) => {
if (success) {
onSuccess?.(data);
} else {
onError?.({ errorStringKey: parsingError?.stringKey });
if (!subaccountClient) return;
try {
orderParams = isClosePosition
? abacusStateManager.closePositionPayload()
: abacusStateManager.placeOrderPayload();
if (!orderParams) {
throw new Error('Missing order params');
if (data?.clientId !== undefined) {
dispatch(removeUncommittedOrderClientId(data.clientId));
}
}
};
const {
marketId,
type,
side,
price,
triggerPrice,
size,
clientId,
timeInForce,
goodTilTimeInSeconds,
execution,
postOnly,
reduceOnly,
} = orderParams;
let placeOrderParams;
dispatch(addUncommittedOrderClientId(clientId));
// Remove uncommitted order after timeout if it hasn't already been removed
setTimeout(() => {
dispatch(removeUncommittedOrderClientId(clientId));
}, UNCOMMITTED_ORDER_TIMEOUT);
console.log('useSubaccount/placeOrder', {
...orderParams,
});
const response = await placeOrderForSubaccount({
subaccount: subaccountClient,
marketId,
type: type as OrderType,
side: side as OrderSide,
price,
triggerPrice,
size,
clientId,
timeInForce: timeInForce as OrderTimeInForce,
goodTilTimeInSeconds: goodTilTimeInSeconds ?? 0,
execution: execution as OrderExecution,
postOnly,
reduceOnly,
});
// Handle Stateful orders
if ((response as IndexedTx)?.code !== 0) {
throw new StatefulOrderError('Stateful order has failed to commit.', response);
}
if (orderParams?.clientId) {
dispatch(removeUncommittedOrderClientId(orderParams.clientId));
}
if (response?.hash) {
console.log(
isClosePosition
? 'useSubaccount/closePosition'
: 'useSubaccount/placeOrderForSubaccount',
{
txHash: Buffer.from(response.hash).toString('hex').toUpperCase(),
}
);
}
track(AnalyticsEvent.TradePlaceOrder, {
...orderParams,
isClosePosition,
} as HumanReadablePlaceOrderPayload & { isClosePosition: boolean });
onSuccess?.();
} catch (error) {
const errorStringKey = error?.code && ORDER_ERROR_CODE_MAP[error.code as number];
onError?.({ errorStringKey });
log('useSubaccount/placeOrder', error, {
orderParams,
isClosePosition,
});
if (isClosePosition) {
placeOrderParams = abacusStateManager.closePosition(callback);
} else {
placeOrderParams = abacusStateManager.placeOrder(callback);
}
track(AnalyticsEvent.TradePlaceOrder, {
...placeOrderParams,
isClosePosition,
} as HumanReadablePlaceOrderPayload & { isClosePosition: boolean });
return placeOrderParams;
},
[subaccountClient, placeOrderForSubaccount]
[subaccountClient]
);
const closePosition = useCallback(
@@ -568,8 +328,8 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
onError,
onSuccess,
}: {
onError: (onErrorParams?: { errorStringKey?: string }) => void;
onSuccess?: () => void;
onError: (onErrorParams?: { errorStringKey?: Nullable<string> }) => void;
onSuccess?: (placeOrderPayload: Nullable<HumanReadablePlaceOrderPayload>) => void;
}) => await placeOrder({ isClosePosition: true, onError, onSuccess }),
[placeOrder]
);
@@ -581,53 +341,21 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
onSuccess,
}: {
orderId: string;
onError?: ({ errorMsg }?: { errorMsg?: string }) => void;
onError?: ({ errorStringKey }?: { errorStringKey?: Nullable<string> }) => void;
onSuccess?: () => void;
}) => {
let cancelOrderParams: Nullable<HumanReadableCancelOrderPayload>;
if (!subaccountClient) return;
try {
cancelOrderParams = abacusStateManager.cancelOrderPayload(orderId);
if (!cancelOrderParams) {
throw new Error('Missing cancel order params');
const callback = (success: boolean, parsingError?: Nullable<ParsingError>) => {
if (success) {
track(AnalyticsEvent.TradeCancelOrder);
onSuccess?.();
} else {
onError?.({ errorStringKey: parsingError?.stringKey });
}
};
const { clientId, clobPairId, goodTilBlock, goodTilBlockTime, orderFlags } =
cancelOrderParams;
// Keep for debugging
console.log('useSubaccount/cancelOrder', cancelOrderParams);
const response = await cancelOrderForSubaccount({
subaccount: subaccountClient,
clientId,
orderFlags,
clobPairId,
goodTilBlock: goodTilBlock || undefined,
goodTilBlockTime: goodTilBlockTime || undefined,
});
// Handle Stateful orders
if ((response as IndexedTx)?.code !== 0) {
throw new StatefulOrderError('Stateful cancel has failed to commit.', response);
}
if (response?.hash) {
console.log('useSubaccount/cancelOrderForSubaccount', {
txHash: Buffer.from(response.hash).toString('hex').toUpperCase(),
});
}
onSuccess?.();
} catch (error) {
onError?.();
log('useSubaccount/cancelOrder', error, cancelOrderParams);
}
abacusStateManager.cancelOrder(orderId, callback);
},
[subaccountClient, cancelOrderForSubaccount]
[subaccountClient]
);
return {
@@ -637,9 +365,7 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
requestFaucetFunds,
// Transfer Methods
simulateTransfer,
transfer,
simulateWithdraw,
sendSquidWithdraw,
// Trading Methods
+19 -10
View File
@@ -4,17 +4,20 @@ import { LocalStorageKey } from '@/constants/localStorage';
import {
type DydxAddress,
type EthereumAddress,
type EvmAddress,
WalletConnectionType,
WalletType,
wallets,
DYDX_CHAIN_INFO,
} from '@/constants/wallets';
import { useLocalStorage } from '@/hooks/useLocalStorage';
import {
useConnect as useConnectWagmi,
useAccount as useAccountWagmi,
useDisconnect as useDisconnectWagmi,
usePublicClient as usePublicClientWagmi,
useWalletClient as useWalletClientWagmi,
} from 'wagmi';
import {
@@ -26,21 +29,21 @@ import {
} from 'graz';
import { resolveWagmiConnector } from '@/lib/wagmi';
import { getWalletConnection } from '@/lib/wallet';
import { getWalletConnection, parseWalletError } from '@/lib/wallet';
import { log } from '@/lib/telemetry';
import { useLocalStorage } from '@/hooks/useLocalStorage';
import { useStringGetter } from './useStringGetter';
export const useWalletConnection = () => {
const stringGetter = useStringGetter();
// EVM wallet connection
const [evmAddress, saveEvmAddress] = useLocalStorage<EthereumAddress | undefined>({
const [evmAddress, saveEvmAddress] = useLocalStorage<EvmAddress | undefined>({
key: LocalStorageKey.EvmAddress,
defaultValue: undefined,
});
const { address: evmAddressWagmi, isConnected: isConnectedWagmi } = useAccountWagmi();
const publicClientWagmi = usePublicClientWagmi();
const { data: signerWagmi } = useWalletClientWagmi();
const { disconnectAsync: disconnectWagmi } = useDisconnectWagmi();
@@ -129,8 +132,6 @@ export const useWalletConnection = () => {
}
}
} catch (error) {
log('useWalletConnection/connectWallet', error);
throw Object.assign(
new Error([error.message, error.cause?.message].filter(Boolean).join('\n')),
{
@@ -158,7 +159,7 @@ export const useWalletConnection = () => {
// Wallet selection
const [selectedWalletType, setSelectedWalletType] = useState<WalletType | undefined>(walletType);
const [selectedWalletError, setSelectedWalletError] = useState<Error>();
const [selectedWalletError, setSelectedWalletError] = useState<string>();
useEffect(() => {
(async () => {
@@ -173,8 +174,15 @@ export const useWalletConnection = () => {
setWalletType(walletType);
setWalletConnectionType(walletConnectionType);
} catch (error) {
log('useWalletConnection/connectWallet', error);
setSelectedWalletError(error);
const { walletErrorType, message } = parseWalletError({
error,
stringGetter,
});
if (message) {
log('useWalletConnection/connectWallet', error, { walletErrorType });
setSelectedWalletError(message);
}
}
} else {
setWalletType(undefined);
@@ -208,7 +216,8 @@ export const useWalletConnection = () => {
evmAddress,
evmAddressWagmi,
signerWagmi,
publicClientWagmi,
// Wallet connection (Cosmos)
dydxAddress,
dydxAddressGraz,
+1 -1
View File
@@ -1,4 +1,4 @@
const LogoShortIcon: React.FC<{ id?: string }> = ({ id }) => (
const LogoShortIcon: React.FC<{ id?: string }> = ({ id }: { id?: string }) => (
<svg
width="135"
height="145"
+1 -1
View File
@@ -54,7 +54,7 @@ button:disabled {
cursor: pointer;
}
:focus {
:focus-visible{
outline: var(--color-accent) 1px solid;
outline-offset: -1px;
/* outline-offset: -2px; */
+1 -2
View File
@@ -7,10 +7,9 @@ import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization';
import { AppRoute } from '@/constants/routes';
import { useStringGetter } from '@/hooks';
import { LogoShortIcon } from '@/icons';
import { LogoShortIcon, BellIcon } from '@/icons';
import { Icon, IconName } from '@/components/Icon';
import { BellIcon } from '@/icons';
import { IconButton } from '@/components/IconButton';
import { NavigationMenu } from '@/components/NavigationMenu';
import { VerticalSeparator } from '@/components/Separator';
+16 -6
View File
@@ -15,12 +15,20 @@ type StyleProps = {
};
export const NotificationsToastArea = ({ className }: StyleProps) => {
const { notifications, getKey, getDisplayData, markUnseen, markSeen, isMenuOpen, onNotificationAction } = useNotifications();
const {
notifications,
getKey,
getDisplayData,
markUnseen,
markSeen,
isMenuOpen,
onNotificationAction,
} = useNotifications();
const { isMobile } = useBreakpoints();
return (
<$ToastArea swipeDirection={isMobile ? 'up' : 'right'} className={className}>
<StyledToastArea swipeDirection={isMobile ? 'up' : 'right'} className={className}>
{Object.values(notifications)
// Sort by time of first trigger
.sort(
@@ -41,6 +49,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}
@@ -52,19 +61,20 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
sensitivity={displayData.toastSensitivity}
setIsOpen={(isOpen, isClosedFromTimeout) => {
if (!isOpen)
// Toast timer expired without user interaction
if (isClosedFromTimeout) markUnseen(notification);
if (isClosedFromTimeout)
// Toast timer expired without user interaction
markUnseen(notification);
// Toast interacted with or dismissed
else markSeen(notification);
}}
lastUpdated={notification.timestamps[notification.status]}
/>
))}
</$ToastArea>
</StyledToastArea>
);
};
const $ToastArea = styled(ToastArea)`
const StyledToastArea = styled(ToastArea)`
position: absolute;
width: min(16rem, 100%);
inset: 0 0 0 auto;
+18
View File
@@ -0,0 +1,18 @@
import type { AbacusTrackingProtocol, Nullable } from '@/constants/abacus';
import type { AnalyticsEvent } from '@/constants/analytics';
import { track } from '../analytics';
import { log as telemetryLog } from '../telemetry';
class AbacusAnalytics implements AbacusTrackingProtocol {
log(event: string, data: Nullable<string>) {
try {
const parsedData = data ? JSON.parse(data) : {};
track(event as AnalyticsEvent, parsedData);
} catch (error) {
telemetryLog('AbacusAnalytics/log', error);
}
}
}
export default AbacusAnalytics;
+309 -25
View File
@@ -1,28 +1,63 @@
import Abacus, { Nullable } from '@dydxprotocol/abacus';
import Abacus, { type Nullable } from '@dydxprotocol/v4-abacus';
import Long from 'long';
import type { IndexedTx } from '@cosmjs/stargate';
import {
CompositeClient,
IndexerConfig,
type LocalWallet,
Network,
NetworkOptimizer,
SubaccountClient,
ValidatorConfig,
OrderType,
OrderSide,
OrderTimeInForce,
OrderExecution,
DYDX_DENOM,
GAS_PRICE_DYDX_DENOM,
} from '@dydxprotocol/v4-client-js';
import {
type AbacusDYDXChainTransactionsProtocol,
QueryType,
type QueryTypes,
TransactionType,
type TransactionTypes,
type HumanReadablePlaceOrderPayload,
type HumanReadableCancelOrderPayload,
type HumanReadableWithdrawPayload,
type HumanReadableTransferPayload,
} from '@/constants/abacus';
import { DialogTypes } from '@/constants/dialogs';
import { UNCOMMITTED_ORDER_TIMEOUT_MS } from '@/constants/trade';
import { QUANTUM_MULTIPLIER } from '@/constants/numbers';
import { RootStore } from '@/state/_store';
import { addUncommittedOrderClientId, removeUncommittedOrderClientId } from '@/state/account';
import { openDialog } from '@/state/dialogs';
import { StatefulOrderError } from '../errors';
import { bytesToBigInt } from '../numbers';
import { log } from '../telemetry';
class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
private compositeClient: CompositeClient | undefined;
private store: RootStore | undefined;
private localWallet: LocalWallet | undefined;
constructor() {
this.compositeClient = undefined;
this.store = undefined;
}
setStore(store: RootStore): void {
this.store = store;
}
setLocalWallet(localWallet: LocalWallet) {
this.localWallet = localWallet;
}
async connectNetwork(
@@ -33,30 +68,41 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
faucetUrl: Nullable<string> | undefined,
callback: (p0: Nullable<string>) => void
): Promise<void> {
this.compositeClient = await CompositeClient.connect(
new Network(
chainId,
new IndexerConfig(indexerUrl, indexerSocketUrl),
new ValidatorConfig(validatorUrl, chainId, {
broadcastPollIntervalMs: 3_000,
broadcastTimeoutMs: 60_000,
})
)
);
try {
const compositeClient = await CompositeClient.connect(
new Network(
chainId,
new IndexerConfig(indexerUrl, indexerSocketUrl),
new ValidatorConfig(validatorUrl, chainId, {
broadcastPollIntervalMs: 3_000,
broadcastTimeoutMs: 60_000,
})
)
);
// 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 = compositeClient;
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 }));
} catch (error) {
this.store?.dispatch(
openDialog({ type: DialogTypes.ExchangeOffline, dialogProps: { preventClose: true } })
);
log('DydxChainTransactions/connectNetwork', error);
return;
}
}
parseToPrimitives<T>(x: T): T {
@@ -72,6 +118,10 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
return x.toString() as T;
}
if (x instanceof Uint8Array) {
return bytesToBigInt(x).toString() as T;
}
if (typeof x === 'object') {
const parsedObj: { [key: string]: any } = {};
for (const key in x) {
@@ -85,14 +135,218 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
throw new Error(`Unsupported data type: ${typeof x}`);
}
async placeOrderTransaction(params: HumanReadablePlaceOrderPayload): Promise<string> {
if (!this.compositeClient || !this.localWallet)
throw new Error('Missing compositeClient or localWallet');
try {
const {
subaccountNumber,
marketId,
type,
side,
price,
size,
clientId,
timeInForce,
goodTilTimeInSeconds,
execution,
postOnly,
reduceOnly,
triggerPrice,
} = params || {};
// Observe uncommitted order
this.store?.dispatch(addUncommittedOrderClientId(clientId));
setTimeout(() => {
this.store?.dispatch(removeUncommittedOrderClientId(clientId));
}, UNCOMMITTED_ORDER_TIMEOUT_MS);
// Place order
const tx = await this.compositeClient?.placeOrder(
new SubaccountClient(this.localWallet, subaccountNumber),
marketId,
type as OrderType,
side as OrderSide,
price,
size,
clientId,
timeInForce as OrderTimeInForce,
goodTilTimeInSeconds ?? 0,
execution as OrderExecution,
postOnly,
reduceOnly,
triggerPrice ?? undefined
);
// Handle stateful orders
if ((tx as IndexedTx)?.code !== 0) {
throw new StatefulOrderError('Stateful order has failed to commit.', tx);
}
const parsedTx = this.parseToPrimitives(tx);
const hash = parsedTx?.hash;
if (import.meta.env.MODE === 'production') {
console.log(`https://testnet.mintscan.io/dydx-testnet/txs/${hash}`);
} else console.log(`txHash: ${hash}`);
return JSON.stringify(parsedTx);
} catch (error) {
if (error?.name !== 'BroadcastError') {
log('DydxChainTransactions/placeOrderTransaction', error);
}
return JSON.stringify({
error,
});
}
}
async cancelOrderTransaction(params: HumanReadableCancelOrderPayload): Promise<string> {
if (!this.compositeClient || !this.localWallet) {
throw new Error('Missing compositeClient or localWallet');
}
const { subaccountNumber, clientId, orderFlags, clobPairId, goodTilBlock, goodTilBlockTime } =
params ?? {};
try {
const tx = await this.compositeClient?.cancelOrder(
new SubaccountClient(this.localWallet, subaccountNumber),
clientId,
orderFlags,
clobPairId,
goodTilBlock ?? undefined,
goodTilBlockTime ?? undefined
);
const parsedTx = this.parseToPrimitives(tx);
return JSON.stringify(parsedTx);
} catch (error) {
log('DydxChainTransactions/cancelOrderTransaction', error);
return JSON.stringify({
error,
});
}
}
async simulateWithdrawTransaction(params: HumanReadableWithdrawPayload): Promise<string> {
if (!this.compositeClient || !this.localWallet) {
throw new Error('Missing compositeClient or localWallet');
}
const { subaccountNumber, amount } = params ?? {};
const compositeClient = this.compositeClient;
const subaccountClient = new SubaccountClient(this.localWallet, subaccountNumber);
try {
const tx = await compositeClient.simulate(
this.localWallet,
() =>
new Promise((resolve) => {
const msg = compositeClient.withdrawFromSubaccountMessage(subaccountClient, amount);
resolve([msg]);
}),
);
const parsedTx = this.parseToPrimitives(tx);
return JSON.stringify(parsedTx);
} catch (error) {
log('DydxChainTransactions/simulateWithdrawTransaction', error);
return JSON.stringify({
error,
});
}
}
async simulateTransferNativeTokenTransaction(params: HumanReadableTransferPayload): Promise<string> {
if (!this.compositeClient || !this.localWallet) {
throw new Error('Missing compositeClient or localWallet');
}
const { subaccountNumber, amount, recipient } = params ?? {};
const compositeClient = this.compositeClient;
const subaccountClient = new SubaccountClient(this.localWallet, subaccountNumber);
try {
const tx = await compositeClient.simulate(
this.localWallet,
() =>
new Promise((resolve) => {
const msg = compositeClient?.validatorClient.post.composer.composeMsgSendToken(
subaccountClient.address,
recipient,
DYDX_DENOM,
Long.fromNumber(amount * QUANTUM_MULTIPLIER)
);
resolve([msg]);
}),
GAS_PRICE_DYDX_DENOM,
);
const parsedTx = this.parseToPrimitives(tx);
return JSON.stringify(parsedTx);
} catch (error) {
log('DydxChainTransactions/simulateTransferNativeTokenTransaction', error);
return JSON.stringify({
error,
});
}
}
async transaction(
type: TransactionTypes,
paramsInJson: Abacus.Nullable<string>,
callback: (p0: Abacus.Nullable<string>) => void
): Promise<void> {
// To be implemented
return;
try {
const params = paramsInJson ? JSON.parse(paramsInJson) : undefined;
switch (type) {
case TransactionType.PlaceOrder: {
const result = await this.placeOrderTransaction(params);
callback(result);
break;
}
case TransactionType.CancelOrder: {
const result = await this.cancelOrderTransaction(params);
callback(result);
break;
}
case TransactionType.simulateWithdraw: {
const result = await this.simulateWithdrawTransaction(params);
callback(result);
break;
}
case TransactionType.simulateTransferNativeToken: {
const result = await this.simulateTransferNativeTokenTransaction(params);
callback(result);
break;
}
default: {
break;
}
}
} catch (error) {
try {
const serializedError = JSON.stringify(error);
callback(serializedError);
} catch (parseError) {
log('DydxChainTransactions/transaction', parseError);
}
log('DydxChainTransactions/transaction', error);
}
}
async get(
@@ -116,6 +370,12 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
);
callback(JSON.stringify({ url: optimalNode }));
break;
case QueryType.EquityTiers:
const equityTiers =
await this.compositeClient?.validatorClient.get.getEquityTierLimitConfiguration();
const parsedEquityTiers = this.parseToPrimitives(equityTiers);
callback(JSON.stringify(parsedEquityTiers));
break;
case QueryType.FeeTiers:
const feeTiers = await this.compositeClient?.validatorClient.get.getFeeTiers();
const parsedFeeTiers = this.parseToPrimitives(feeTiers);
@@ -135,6 +395,30 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
const parsedUserStats = this.parseToPrimitives(userStats);
callback(JSON.stringify(parsedUserStats));
break;
case QueryType.GetAccountBalances:
if (!this.localWallet?.address) throw new Error('Missing localWallet');
const accountBalances = await this.compositeClient?.validatorClient.get.getAccountBalances(
this.localWallet.address
);
const parsedAccountBalances = this.parseToPrimitives(accountBalances);
callback(JSON.stringify(parsedAccountBalances));
break;
case QueryType.RewardsParams:
const rewardsParams = await this.compositeClient?.validatorClient.get.getRewardsParams();
const parsedRewardsParams = this.parseToPrimitives(rewardsParams);
callback(JSON.stringify(parsedRewardsParams));
break;
case QueryType.GetMarketPrice:
const price = await this.compositeClient?.validatorClient.get.getPrice(params.marketId);
const parsedPrice = this.parseToPrimitives(price);
callback(JSON.stringify(parsedPrice));
break;
case QueryType.GetDelegations:
const delegations =
await this.compositeClient?.validatorClient.get.getDelegatorDelegations(params.address);
const parseDelegations = this.parseToPrimitives(delegations);
callback(JSON.stringify(parseDelegations));
break;
// Do not implement Transfers (yet)
case QueryType.Transfers:
default:
+44 -13
View File
@@ -1,3 +1,5 @@
import type { LocalWallet } from '@dydxprotocol/v4-client-js';
import type {
ClosePositionInputFields,
Nullable,
@@ -6,6 +8,7 @@ import type {
TradeInputFields,
TransferInputFields,
HistoricalPnlPeriods,
ParsingError,
} from '@/constants/abacus';
import {
@@ -30,6 +33,7 @@ import { getInputTradeOptions } from '@/state/inputsSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
import AbacusRest from './rest';
import AbacusAnalytics from './analytics';
import AbacusWebsocket from './websocket';
import AbacusChainTransaction from './dydxChainTransactions';
import AbacusStateNotifier from './stateNotification';
@@ -45,21 +49,25 @@ class AbacusStateManager {
stateManager: InstanceType<typeof AsyncAbacusStateManager>;
websocket: AbacusWebsocket;
stateNotifier: AbacusStateNotifier;
analytics: AbacusAnalytics;
abacusFormatter: AbacusFormatter;
chainTransactions: AbacusChainTransaction;
constructor() {
this.store = undefined;
this.currentMarket = undefined;
this.stateNotifier = new AbacusStateNotifier();
this.analytics = new AbacusAnalytics();
this.websocket = new AbacusWebsocket();
this.abacusFormatter = new AbacusFormatter();
this.chainTransactions = new AbacusChainTransaction();
const ioImplementations = new IOImplementations(
// @ts-ignore
new AbacusRest(),
this.websocket,
new AbacusChainTransaction(),
null,
this.chainTransactions,
this.analytics,
new AbacusThreading(),
new CoroutineTimer(),
new AbacusFileSystem()
@@ -153,11 +161,16 @@ class AbacusStateManager {
// ------ Set Data ------ //
setStore = (store: RootStore) => {
this.store = store;
this.stateNotifier.setStore(store);
this.chainTransactions.setStore(store);
};
setAccount = (walletAddress: string) => {
this.stateManager.accountAddress = walletAddress;
setAccount = (localWallet?: LocalWallet) => {
if (localWallet) {
this.stateManager.accountAddress = localWallet.address;
this.chainTransactions.setLocalWallet(localWallet);
}
};
setTransfersSourceAddress = (evmAddress: string) => {
@@ -215,16 +228,34 @@ class AbacusStateManager {
this.stateManager.transferStatus(hash, fromChainId, toChainId);
};
// ------ Transactions ------ //
placeOrder = (
callback: (
success: boolean,
parsingError: Nullable<ParsingError>,
data: Nullable<HumanReadablePlaceOrderPayload>
) => void
): Nullable<HumanReadablePlaceOrderPayload> => this.stateManager.commitPlaceOrder(callback);
closePosition = (
callback: (
success: boolean,
parsingError: Nullable<ParsingError>,
data: Nullable<HumanReadablePlaceOrderPayload>
) => void
): Nullable<HumanReadablePlaceOrderPayload> => this.stateManager.commitClosePosition(callback);
cancelOrder = (
orderId: string,
callback: (
success: boolean,
parsingError: Nullable<ParsingError>,
data: Nullable<HumanReadableCancelOrderPayload>
) => void
) => this.stateManager.cancelOrder(orderId, callback);
// ------ Utils ------ //
placeOrderPayload = (): Nullable<HumanReadablePlaceOrderPayload> =>
this.stateManager.placeOrderPayload();
closePositionPayload = (): Nullable<HumanReadablePlaceOrderPayload> =>
this.stateManager.closePositionPayload();
cancelOrderPayload = (orderId: string): Nullable<HumanReadableCancelOrderPayload> =>
this.stateManager.cancelOrderPayload(orderId);
getHistoricalPnlPeriod = (): Nullable<HistoricalPnlPeriods> =>
this.stateManager.historicalPnlPeriod;
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Nullable, kollections } from '@dydxprotocol/abacus';
import type { Nullable, kollections } from '@dydxprotocol/v4-abacus';
import type { AbacusRestProtocol } from '@/constants/abacus';
+17 -2
View File
@@ -1,6 +1,7 @@
import { kollections } from '@dydxprotocol/abacus';
import { kollections } from '@dydxprotocol/v4-abacus';
import type {
AccountBalance,
AbacusApiState,
AbacusNotification,
AbacusStateNotificationProtocol,
@@ -18,9 +19,11 @@ import { Changes } from '@/constants/abacus';
import type { RootStore } from '@/state/_store';
import {
setBalances,
setFills,
setFundingPayments,
setHistoricalPnl,
setLatestOrder,
setSubaccount,
setTransfers,
setWallet,
@@ -71,6 +74,16 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol {
);
}
if (changes.has(Changes.accountBalances)) {
if (updatedState.account?.balances) {
const balances: Record<string, AccountBalance> = {}
for (const { k, v } of updatedState.account.balances.toArray()) {
balances[k] = v;
}
dispatch(setBalances(balances));
}
}
if (changes.has(Changes.configs)) {
dispatch(setConfigs(updatedState.configs));
}
@@ -155,7 +168,9 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol {
}
}
lastOrderChanged(order: SubaccountOrder) {}
lastOrderChanged(order: SubaccountOrder) {
this.store?.dispatch(setLatestOrder(order));
}
errorsEmitted(errors: ParsingErrors) {
console.error('parse errors', errors.toArray());
+8
View File
@@ -5,10 +5,15 @@ import type {
AnalyticsEventData,
} from '@/constants/analytics';
const DEBUG_ANALYTICS = false;
export const identify = <T extends AnalyticsUserProperty>(
property: T,
propertyValue: AnalyticsUserPropertyValue<T>
) => {
if (DEBUG_ANALYTICS) {
console.log(`[Analytics:Identify] ${property}`, propertyValue);
}
const customEvent = new CustomEvent('dydx:identify', {
detail: { property, propertyValue },
});
@@ -20,6 +25,9 @@ export const track = <T extends AnalyticsEvent>(
eventType: T,
eventData?: AnalyticsEventData<T>
) => {
if (DEBUG_ANALYTICS) {
console.log(`[Analytics] ${eventType}`, eventData);
}
const customEvent = new CustomEvent('dydx:track', {
detail: { eventType, eventData },
});
+15 -1
View File
@@ -48,7 +48,7 @@ export const isNumber = (value: any): value is number =>
/**
* @description Returns null if input is 0 or null, '99+' if input is greater than 99, otherwise original input number
*/
export const shorternNumberForDisplay = (num?: number) =>
export const shortenNumberForDisplay = (num?: number) =>
MustBigNumber(num).eq(0) ? null : MustBigNumber(num).gt(99) ? '99+' : num;
/**
@@ -66,3 +66,17 @@ export const getSeparator = ({
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;
}
+6
View File
@@ -58,6 +58,12 @@ export const getStatusIconInfo = ({
statusIconColor: `var(--color-negative)`,
};
}
case AbacusOrderStatus.untriggered: {
return {
statusIcon: IconName.OrderUntriggered,
statusIconColor: `var(--color-text-2)`,
};
}
case AbacusOrderStatus.pending:
default: {
return {
+11 -3
View File
@@ -1,5 +1,7 @@
export const log = (location: string, error: Error, metadata?: any) => {
// console.warn(error);
if (import.meta.env.MODE !== 'production') {
console.warn('telemetry/log:', { location, error, metadata });
}
const customEvent = new CustomEvent('dydx:log', {
detail: {
@@ -16,7 +18,10 @@ export const log = (location: string, error: Error, metadata?: any) => {
globalThis.addEventListener('unhandledrejection', (event) => {
event.preventDefault();
log('window/onunhandledrejection', new Error(`Promise rejected and uncaught; reason: ${event.reason}`));
log(
'window/onunhandledrejection',
new Error(`Promise rejected and uncaught; reason: ${event.reason}`)
);
});
// Log rejected Promises that are caught late (handler attached at a later time)
@@ -25,7 +30,10 @@ globalThis.addEventListener(
(event) => {
event.preventDefault();
log('window/onrejectionhandled', new Error(`Promise rejected and caught late; reason: ${event.reason}`));
log(
'window/onrejectionhandled',
new Error(`Promise rejected and caught late; reason: ${event.reason}`)
);
},
false
);
+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)}`;
}
+66
View File
@@ -1,7 +1,10 @@
import { STRING_KEYS, StringGetterFunction } from '@/constants/localization';
import {
type WalletConnection,
wallets,
WalletConnectionType,
WalletErrorType,
WalletType,
} from '@/constants/wallets';
@@ -68,3 +71,66 @@ export const getWalletConnection = ({
}
}
};
export const getWalletErrorType = ({ error }: { error: Error }) => {
const { message } = error;
const messageLower = message.toLowerCase();
// General - Cancelled
if (
messageLower.includes('connection request reset') ||
messageLower.includes('rejected') ||
messageLower.includes('reject') ||
messageLower.includes('cancelled') ||
messageLower.includes('canceled') ||
messageLower.includes('user denied')
) {
return WalletErrorType.UserCanceled;
}
if (messageLower.includes('chain mismatch')) {
return WalletErrorType.ChainMismatch;
}
// ImToken - User canceled
if (messageLower.includes('用户取消了操作')) {
return WalletErrorType.UserCanceled;
}
if (messageLower.includes('does not support deterministic signing')) {
return WalletErrorType.NonDeterministicWallet;
}
return WalletErrorType.Unknown;
};
export const parseWalletError = ({
error,
stringGetter,
}: {
error: Error;
stringGetter: StringGetterFunction;
}) => {
const walletErrorType = getWalletErrorType({ error });
let message;
switch (walletErrorType) {
case WalletErrorType.ChainMismatch:
case WalletErrorType.UserCanceled: {
break;
}
default: {
message = stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: {
ERROR_MESSAGE: error.message || stringGetter({ key: STRING_KEYS.UNKNOWN_ERROR }),
},
});
}
}
return {
walletErrorType,
message,
};
};
+3 -3
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Nullable } from '@dydxprotocol/abacus';
import { Nullable } from '@dydxprotocol/v4-abacus';
import styled, { AnyStyledComponent } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux';
@@ -270,11 +270,11 @@ Styled.FeeTable = styled(Table)`
--table-cell-align: end;
font: var(--font-base-book);
tbody tr {
&[data-yours='true'] {
background-color: var(--color-layer-3);
td:first-child {
border-left: 0.125rem solid var(--color-accent);
}
+1 -1
View File
@@ -49,7 +49,7 @@ export const DYDXBalancePanel = () => {
size={ButtonSize.Small}
onClick={() => dispatch(openDialog({ type: DialogTypes.Receive }))}
>
{stringGetter({ key: STRING_KEYS.EXCHANGE_RECEIVED })}
{stringGetter({ key: STRING_KEYS.RECEIVE })}
</Styled.ReceiveButton>
<Button
slotLeft={<Icon iconName={IconName.Send} />}
+13 -10
View File
@@ -2,6 +2,7 @@ import { useState } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent } from 'styled-components';
import { AbacusOrderStatus } from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization';
import { useBreakpoints, useStringGetter } from '@/hooks';
@@ -18,20 +19,23 @@ import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable';
import { OrdersTable, OrdersTableColumnKey } from '@/views/tables/OrdersTable';
import { PositionsTable, PositionsTableColumnKey } from '@/views/tables/PositionsTable';
import { calculateIsAccountViewOnly } from '@/state/accountCalculators';
import {
calculateHasUncommittedOrders,
calculateIsAccountViewOnly,
} from '@/state/accountCalculators';
import {
getCurrentMarketTradeInfoNumbers,
getHasUnseenFillUpdates,
getHasUnseenOrderUpdates,
getLatestOrderStatus,
getTradeInfoNumbers,
} from '@/state/accountSelectors';
import { getCurrentMarketAssetId, getCurrentMarketId } from '@/state/perpetualsSelectors';
import { isTruthy } from '@/lib/isTruthy';
import { shorternNumberForDisplay } from '@/lib/numbers';
import { shortenNumberForDisplay } from '@/lib/numbers';
import { PositionInfo } from '@/views/PositionInfo';
@@ -69,13 +73,12 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => {
const hasUnseenOrderUpdates = useSelector(getHasUnseenOrderUpdates);
const hasUnseenFillUpdates = useSelector(getHasUnseenFillUpdates);
const isAccountViewOnly = useSelector(calculateIsAccountViewOnly);
const hasUncommittedOrders = useSelector(calculateHasUncommittedOrders);
const isWaitingForOrderToIndex = useSelector(calculateHasUncommittedOrders);
const showCurrentMarket = isTablet || view === PanelView.CurrentMarket;
const fillsTagNumber = shorternNumberForDisplay(showCurrentMarket ? numFills : numTotalFills);
const fillsTagNumber = shortenNumberForDisplay(showCurrentMarket ? numFills : numTotalFills);
const ordersTagNumber = shorternNumberForDisplay(
const ordersTagNumber = shortenNumberForDisplay(
showCurrentMarket ? numOpenOrders : numTotalOpenOrders
);
@@ -86,7 +89,7 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => {
key: showCurrentMarket ? STRING_KEYS.POSITION : STRING_KEYS.POSITIONS,
}),
tag: showCurrentMarket ? null : shorternNumberForDisplay(numTotalPositions),
tag: showCurrentMarket ? null : shortenNumberForDisplay(numTotalPositions),
content: showCurrentMarket ? (
<PositionInfo showNarrowVariation={isTablet} />
@@ -118,7 +121,7 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => {
value: InfoSection.Orders,
label: stringGetter({ key: STRING_KEYS.ORDERS }),
slotRight: hasUncommittedOrders ? (
slotRight: isWaitingForOrderToIndex ? (
<Styled.LoadingSpinner />
) : (
ordersTagNumber && (
@@ -190,7 +193,7 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => {
// value: InfoSection.Payments,
// label: stringGetter({ key: STRING_KEYS.PAYMENTS }),
// tag: shorternNumberForDisplay(
// tag: shortenNumberForDisplay(
// showCurrentMarket ? numFundingPayments : numTotalFundingPayments
// ),
// content: (
+1 -4
View File
@@ -1,9 +1,9 @@
import { configureStore } from '@reduxjs/toolkit';
import abacusStateManager from '@/lib/abacus';
import appMiddleware from './appMiddleware';
import localizationMiddleware from './localizationMiddleware';
import routerMiddleware from './routerMiddleware';
import tradeMiddleware from './tradeMiddleware';
import { accountSlice } from './account';
import { appSlice } from './app';
@@ -16,8 +16,6 @@ import { localizationSlice } from './localization';
import { navigationSlice } from './navigation';
import { perpetualsSlice } from './perpetuals';
import abacusStateManager from '@/lib/abacus';
export const commandMenuSlices = [layoutSlice, localizationSlice];
export const store = configureStore({
@@ -41,7 +39,6 @@ export const store = configureStore({
appMiddleware,
localizationMiddleware,
routerMiddleware,
tradeMiddleware,
],
devTools: process.env.NODE_ENV !== 'production',
+31 -15
View File
@@ -1,7 +1,7 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import _ from 'lodash';
import type {
AccountBalance,
SubaccountFill,
Nullable,
Subaccount,
@@ -25,16 +25,18 @@ export type AccountState = {
fundingPayments?: SubaccountFundingPayments;
transfers?: SubaccountTransfers;
clearedOrderIds?: string[];
uncommittedOrderClientIds?: number[];
hasUnseenFillUpdates: boolean;
hasUnseenOrderUpdates: boolean;
historicalPnl?: SubAccountHistoricalPNLs;
latestOrder?: Nullable<SubaccountOrder>;
onboardingGuards: Record<OnboardingGuard, boolean | undefined>;
onboardingState: OnboardingState;
subaccount?: Nullable<Subaccount>;
uncommittedOrderClientIds: number[];
wallet?: Nullable<Wallet>;
walletType?: WalletType;
historicalPnlPeriod?: HistoricalPnlPeriods;
balances?: Record<string, AccountBalance>;
};
const initialState: AccountState = {
@@ -42,10 +44,10 @@ const initialState: AccountState = {
fundingPayments: undefined,
transfers: undefined,
clearedOrderIds: undefined,
uncommittedOrderClientIds: undefined,
hasUnseenFillUpdates: false,
hasUnseenOrderUpdates: false,
historicalPnl: undefined,
latestOrder: undefined,
onboardingGuards: {
[OnboardingGuard.hasAcknowledgedTerms]: Boolean(
getLocalStorage<boolean>({
@@ -57,6 +59,7 @@ const initialState: AccountState = {
},
onboardingState: OnboardingState.Disconnected,
subaccount: undefined,
uncommittedOrderClientIds: [],
wallet: undefined,
walletType: getLocalStorage<WalletType>({
key: LocalStorageKey.OnboardingSelectedWalletType,
@@ -87,6 +90,16 @@ export const accountSlice = createSlice({
setTransfers: (state, action: PayloadAction<any>) => {
state.transfers = action.payload;
},
setLatestOrder: (state, action: PayloadAction<Nullable<SubaccountOrder>>) => {
const { clientId } = action.payload ?? {};
state.latestOrder = action.payload;
if (clientId) {
state.uncommittedOrderClientIds = state.uncommittedOrderClientIds.filter(
(id) => id !== clientId
);
}
},
clearOrder: (state, action: PayloadAction<string>) => ({
...state,
clearedOrderIds: [...(state.clearedOrderIds || []), action.payload],
@@ -132,22 +145,23 @@ export const accountSlice = createSlice({
...state,
wallet: action.payload,
}),
addUncommittedOrderClientId: (state, action: PayloadAction<number>) => {
state.uncommittedOrderClientIds = state.uncommittedOrderClientIds
? [...state.uncommittedOrderClientIds, action.payload]
: [action.payload];
},
removeUncommittedOrderClientId: (state, action: PayloadAction<number>) => {
state.uncommittedOrderClientIds = state.uncommittedOrderClientIds?.filter(
(clientId) => clientId !== action.payload
);
},
viewedFills: (state) => {
state.hasUnseenFillUpdates = false;
},
viewedOrders: (state) => {
state.hasUnseenOrderUpdates = false;
},
setBalances: (state, action: PayloadAction<Record<string, AccountBalance>>) => {
state.balances = action.payload;
},
addUncommittedOrderClientId: (state, action: PayloadAction<number>) => {
state.uncommittedOrderClientIds.push(action.payload);
},
removeUncommittedOrderClientId: (state, action: PayloadAction<number>) => {
state.uncommittedOrderClientIds = state.uncommittedOrderClientIds.filter(
(id) => id !== action.payload
);
},
},
});
@@ -155,14 +169,16 @@ export const {
setFills,
setFundingPayments,
setTransfers,
setLatestOrder,
clearOrder,
setOnboardingGuard,
setOnboardingState,
setHistoricalPnl,
setSubaccount,
setWallet,
addUncommittedOrderClientId,
removeUncommittedOrderClientId,
viewedFills,
viewedOrders,
setBalances,
addUncommittedOrderClientId,
removeUncommittedOrderClientId,
} = accountSlice.actions;
+6
View File
@@ -9,6 +9,7 @@ import {
getOnboardingGuards,
getOnboardingState,
getSubaccountId,
getUncommittedOrderClientIds,
} from '@/state/accountSelectors';
import { getSelectedNetwork } from '@/state/appSelectors';
@@ -69,6 +70,11 @@ export const calculateHasOpenPositions = createSelector(
(openPositions?: SubaccountPosition[]) => (openPositions?.length || 0) > 0
);
export const calculateHasUncommittedOrders = createSelector(
[getUncommittedOrderClientIds],
(uncommittedOrderClientIds: number[]) => uncommittedOrderClientIds.length > 0
);
/**
* @description calculate whether the client is loading info.
* (account is connected but subaccountId is till missing)
+42 -24
View File
@@ -12,9 +12,8 @@ import {
import { OnboardingState } from '@/constants/account';
import type { RootState } from './_store';
import { getHydratedTradingData } from '@/lib/orders';
import type { RootState } from './_store';
import { getCurrentMarketId, getPerpetualMarkets } from './perpetualsSelectors';
import { getAssets } from './assetsSelectors';
@@ -79,6 +78,12 @@ export const getCurrentMarketPositionData = (state: RootState) => {
export const getSubaccountOrders = (state: RootState) =>
state.account.subaccount?.orders?.toArray();
/**
* @param state
* @returns latestOrder of the currently connected subaccount throughout this session
*/
export const getLatestOrder = (state: RootState) => state.account?.latestOrder;
/**
* @param state
* @returns list of order ids that user has cleared and should be hidden
@@ -106,25 +111,46 @@ export const getSubaccountOpenOrdersBySideAndPrice = createSelector(
[getSubaccountOpenOrders],
(openOrders = []) => {
const ordersBySide: Partial<Record<OrderSide, Record<number, SubaccountOrder>>> = {};
for (const order of openOrders) {
openOrders.forEach((order) => {
const side = ORDER_SIDES[order.side.name];
const byPrice = (ordersBySide[side] ??= {});
byPrice[order.price] = order;
}
});
return ordersBySide;
}
);
/**
* @param state
* @returns the clientId of the latest order
*/
export const getLatestOrderClientId = createSelector([getLatestOrder], (order) => order?.clientId);
/**
* @returns the rawValue status of the latest order
*/
export const getLatestOrderStatus = createSelector(
[getLatestOrder],
(order) => order?.status.rawValue
);
/**
* @returns a list of clientIds belonging to uncommmited orders
*/
export const getUncommittedOrderClientIds = (state: RootState) =>
state.account.uncommittedOrderClientIds;
/**
* @param orderId
* @returns order details with the given orderId
*/
export const getOrderDetails = (orderId: string) =>
createSelector(
[getSubaccountOrders, getAssets, getPerpetualMarkets],
(orders, assets, perpetualMarkets) => {
const order = orders?.find((order) => order.id === orderId);
return order ? getHydratedTradingData({ data: order, assets, perpetualMarkets }) : undefined;
const matchingOrder = orders?.find((order) => order.id === orderId);
return matchingOrder
? getHydratedTradingData({ data: matchingOrder, assets, perpetualMarkets })
: undefined;
}
);
@@ -178,8 +204,10 @@ export const getFillDetails = (fillId: string) =>
createSelector(
[getSubaccountFills, getAssets, getPerpetualMarkets],
(fills, assets, perpetualMarkets) => {
const fill = fills?.find((fill) => fill.id === fillId);
return fill ? getHydratedTradingData({ data: fill, assets, perpetualMarkets }) : undefined;
const matchingFill = fills?.find((fill) => fill.id === fillId);
return matchingFill
? getHydratedTradingData({ data: matchingFill, assets, perpetualMarkets })
: undefined;
}
);
@@ -280,21 +308,6 @@ export const getIsAccountConnected = (state: RootState) =>
*/
export const getOnboardingGuards = (state: RootState) => state.account.onboardingGuards;
/**
* @param state
* @returns a boolean indicating whether the user has uncommitted orders
*/
export const calculateHasUncommittedOrders = (state: RootState) => {
return !!state.account.uncommittedOrderClientIds?.length;
};
/**
* @param state
* @returns List of uncommitted order clientIds
*/
export const getUncommittedOrderClientIds = (state: RootState) =>
state.account.uncommittedOrderClientIds;
/**
* @param state
* @returns Whether there are unseen fill updates
@@ -319,3 +332,8 @@ export const getUserStats = (state: RootState) => ({
makerVolume30D: state.account?.wallet?.user?.makerVolume30D,
takerVolume30D: state.account?.wallet?.user?.takerVolume30D,
});
/**
* @returns user wallet balances
*/
export const getBalances = (state: RootState) => state.account?.balances;
+2 -6
View File
@@ -47,9 +47,5 @@ export const appSlice = createSlice({
},
});
export const {
initializeLocalization,
initializeWebsocket,
setApiState,
setSelectedNetwork,
} = appSlice.actions;
export const { initializeLocalization, initializeWebsocket, setApiState, setSelectedNetwork } =
appSlice.actions;
+1 -1
View File
@@ -1,5 +1,5 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { kollections } from '@dydxprotocol/abacus';
import type { kollections } from '@dydxprotocol/v4-abacus';
import type { Configs, FeeDiscount, FeeTier, NetworkConfigs, Nullable } from '@/constants/abacus';
import { LocalStorageKey } from '@/constants/localStorage';
+1 -1
View File
@@ -2,4 +2,4 @@ import { type RootState } from './_store';
export const getActiveDialog = (state: RootState) => state.dialogs.activeDialog;
export const getActiveTradeBoxDialog = (state: RootState) => state.dialogs.activeTradeBoxDialog;
export const getActiveTradeBoxDialog = (state: RootState) => state.dialogs.activeTradeBoxDialog;
+1 -1
View File
@@ -40,7 +40,7 @@ export const getStringGetterForLocaleData = (localeData: LocaleData) => {
params?: { [key: string]: string | React.ReactNode };
}): string | Array<string | React.ReactNode> => {
// Fallback to english whenever a key doesn't exist for other languages
let formattedString: string = _.get(localeData, key) || _.get(EN_LOCALE_DATA, key) || '';
const formattedString: string = _.get(localeData, key) || _.get(EN_LOCALE_DATA, key) || '';
return formatString(formattedString, params);
};
+2 -3
View File
@@ -1,11 +1,10 @@
import { matchPath } from 'react-router-dom';
import { createSelector } from 'reselect';
import { Candle, TradingViewBar } from '@/constants/candles';
import { TRADE_ROUTE } from '@/constants/routes';
import { mapCandle } from '@/lib/tradingView/utils';
import type { RootState } from './_store';
import { mapCandle } from '@/lib/tradingView/utils';
/**
* @param state
-39
View File
@@ -1,39 +0,0 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import type { SubaccountOrder } from '@/constants/abacus';
import { removeUncommittedOrderClientId, setSubaccount } from '@/state/account';
import { getSubaccountOrders, getUncommittedOrderClientIds } from '@/state/accountSelectors';
export default (store: any) => (next: any) => async (action: PayloadAction<any>) => {
const { type, payload } = action;
const state = store.getState();
next(action);
switch (type) {
case setSubaccount.type: {
const updatedOrders = payload?.orders?.toArray() || [];
const orders = getSubaccountOrders(state);
const uncommittedOrderClientIds = getUncommittedOrderClientIds(state);
if (uncommittedOrderClientIds?.length && updatedOrders?.length !== orders?.length) {
const updatedOrdersClientIds = Object.fromEntries(
updatedOrders.map((order: SubaccountOrder) => [order.clientId, order])
);
const receivedClientId = uncommittedOrderClientIds.find(
(clientId) => updatedOrdersClientIds[clientId]
);
if (receivedClientId) {
store.dispatch(removeUncommittedOrderClientId(receivedClientId));
}
}
break;
}
default: {
break;
}
}
};
+1 -1
View File
@@ -1,4 +1,4 @@
import styled, { css, type FlattenSimpleInterpolation } from 'styled-components';
import { css } from 'styled-components';
import breakpoints from './breakpoints';
+36 -32
View File
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-2
View File
@@ -6,8 +6,6 @@ import {
type ThemeProps,
} from 'styled-components';
import breakpoints from './breakpoints';
export const layoutMixins: Record<
string,
FlattenSimpleInterpolation | FlattenInterpolation<ThemeProps<any>>
+1 -1
View File
@@ -214,7 +214,7 @@ export const popoverMixins = {
/* &:hover, */
/* :not(:has(* > [data-highlighted])) > &:hover, */
&[data-radix-collection-item]:hover, // @radix-ui/react-navigation-menu
/* &:focus, */
/* &:focus-visible, */
&[aria-selected="true"], // cmdk
&[data-highlighted] // @radix-ui
{
-1
View File
@@ -5,7 +5,6 @@ import {
type ThemeProps,
} from 'styled-components';
import breakpoints from './breakpoints';
import { layoutMixins } from './layoutMixins';
export const tableMixins: Record<
+2 -8
View File
@@ -1,22 +1,16 @@
/* Constants */
:root {
--fontFamily-base: "Circular", system-ui, -apple-system, Helvetica, Arial, sans-serif;
--fontFamily-monospace: "CircularMono", Courier, monospace, var(--fontFamily-base);
--fontFamily-base: "Satoshi", system-ui, -apple-system, Helvetica, Arial, sans-serif;
--fontFamily-monospace: Courier, monospace, var(--fontFamily-base);
--fontWeight-base-0: 450;
--fontWeight-base-1: 500;
--fontWeight-base-2: 700;
--fontWeight-monospace: 400;
--letter-spacing-base: -0.02em;
/* New Typography system */
/**
* @size 10px/11px
* @weight 400
*/
--fontSize-tiny: 0.625rem; /* 10px/11px */
--fontSize-mini: 0.75rem; /* 12px/13px */
--fontSize-small: 0.8438rem; /* 13.5px/14.5px */
@@ -62,8 +62,10 @@ export const AccountInfoConnectedState = () => {
const { buyingPower, equity, marginUsage, leverage } = subAccount || {};
const hasDiff =
marginUsage?.postOrder != null &&
!MustBigNumber(marginUsage?.postOrder).eq(MustBigNumber(marginUsage?.current));
(marginUsage?.postOrder !== null &&
!MustBigNumber(marginUsage?.postOrder).eq(MustBigNumber(marginUsage?.current))) ||
(buyingPower?.postOrder !== null &&
!MustBigNumber(buyingPower?.postOrder).eq(MustBigNumber(buyingPower?.current)));
const showHeader = !hasDiff && !isTablet;
+29 -39
View File
@@ -2,7 +2,7 @@ import { shallowEqual, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent } from 'styled-components';
import { ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { STRING_KEYS, type StringKey } from '@/constants/localization';
import { useBreakpoints, useStringGetter } from '@/hooks';
import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins';
@@ -29,15 +29,13 @@ export const MarketDetails: React.FC = () => {
if (!configs) return null;
const {
basePositionNotional,
tickSize,
stepSize,
initialMarginFraction,
maintenanceMarginFraction,
minOrderSize,
maxPositionSize,
baselinePositionSize,
incrementalInitialMarginFraction,
incrementalPositionSize,
stepSizeDecimals,
tickSizeDecimals,
} = configs;
@@ -72,12 +70,28 @@ export const MarketDetails: React.FC = () => {
key: 'step-size',
label: stringGetter({ key: STRING_KEYS.STEP_SIZE }),
tooltip: 'step-size',
value: <Output useGrouping value={stepSize} type={OutputType.Asset} tag={symbol} />,
value: (
<Output
useGrouping
value={stepSize}
type={OutputType.Asset}
tag={symbol}
fractionDigits={stepSizeDecimals}
/>
),
},
{
key: 'min-order-size',
label: stringGetter({ key: STRING_KEYS.MINIMUM_ORDER_SIZE }),
value: <Output useGrouping value={minOrderSize} type={OutputType.Asset} tag={symbol} />,
value: (
<Output
useGrouping
value={minOrderSize}
type={OutputType.Asset}
tag={symbol}
fractionDigits={stepSizeDecimals}
/>
),
},
{
key: 'max-leverage',
@@ -100,40 +114,16 @@ export const MarketDetails: React.FC = () => {
),
},
{
key: 'initial-margin-fraction',
label: stringGetter({ key: STRING_KEYS.INITIAL_MARGIN_FRACTION }),
key: 'base-initial-margin-fraction',
label: stringGetter({ key: STRING_KEYS.BASE_INITIAL_MARGIN_FRACTION }),
tooltip: 'initial-margin-fraction',
value: <Output useGrouping value={initialMarginFraction} type={OutputType.SmallPercent} />,
},
{
key: 'incremental-initial-margin-fraction',
label: stringGetter({ key: STRING_KEYS.INCREMENTAL_INITIAL_MARGIN_FRACTION }),
value: (
<Output
useGrouping
value={incrementalInitialMarginFraction}
type={OutputType.SmallPercent}
/>
),
},
{
key: 'incremental-position-size',
label: stringGetter({ key: STRING_KEYS.INCREMENTAL_POSITION_SIZE }),
value: (
<Output useGrouping value={incrementalPositionSize} type={OutputType.Asset} tag={symbol} />
),
},
{
key: 'baseline-position-size',
label: stringGetter({ key: STRING_KEYS.BASELINE_POSITION_SIZE }),
value: (
<Output useGrouping value={baselinePositionSize} type={OutputType.Asset} tag={symbol} />
),
},
{
key: 'max-position-size',
label: stringGetter({ key: STRING_KEYS.MAXIMUM_POSITION_SIZE }),
value: <Output useGrouping value={maxPositionSize} type={OutputType.Asset} tag={symbol} />,
key: 'base-position-notional',
label: stringGetter({ key: STRING_KEYS.BASE_POSITION_NOTIONAL }),
tooltip: 'base-position-notional',
value: <Output useGrouping value={basePositionNotional} type={OutputType.Number} />,
},
];
@@ -150,10 +140,10 @@ export const MarketDetails: React.FC = () => {
<Styled.MarketDescription>
{primaryDescriptionKey && (
<p>{stringGetter({ key: STRING_KEYS[primaryDescriptionKey] })}</p>
<p>{stringGetter({ key: STRING_KEYS[primaryDescriptionKey as StringKey] })}</p>
)}
{secondaryDescriptionKey && (
<p>{stringGetter({ key: STRING_KEYS[secondaryDescriptionKey] })}</p>
<p>{stringGetter({ key: STRING_KEYS[secondaryDescriptionKey as StringKey] })}</p>
)}
</Styled.MarketDescription>
+5 -2
View File
@@ -15,14 +15,17 @@ export const MarketLinks = () => {
const linkItems = [
{
key: 'coinmarketcap',
href: coinMarketCapsLink,
icon: IconName.CoinMarketCap,
},
{
key: 'whitepaper',
href: whitepaperLink,
icon: IconName.Whitepaper,
},
{
key: 'project-website',
href: websiteLink,
icon: IconName.Website,
},
@@ -31,8 +34,8 @@ export const MarketLinks = () => {
return (
<Styled.MarketLinks>
{linkItems.map(
({ href, icon }) =>
href && <IconButton key={href} href={href} iconName={icon} type={ButtonType.Link} />
({ key, href, icon }) =>
href && <IconButton key={key} href={href} iconName={icon} type={ButtonType.Link} />
)}
</Styled.MarketLinks>
);
+1 -1
View File
@@ -304,7 +304,7 @@ Styled.Popover = styled(Popover)`
}
`} 0.2s;
}
&:focus {
&:focus-visible {
outline: none;
}
`;
+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
}
/>

Some files were not shown because too many files have changed in this diff Show More