Compare commits

..
Author SHA1 Message Date
jaredvu a123976646 notification adjustments 2023-09-06 13:53:38 -07:00
jaredvu 788fed9c26 Merge branch 'main' into abacus-tracking 2023-09-05 15:03:36 -07:00
Bill 564192fa2b Deposit/Withdraw status notifications (#9)
* Deposit/Withdraw status notifications

* prevent log out during switch accounts

* fix merge

* check for token approval

* address comments

* update lock

* wait for approval

* address comments
2023-09-05 13:04:53 -07:00
jaredvu b8526a5568 Merge branch 'main' into abacus-tracking 2023-09-05 13:01:07 -07:00
Rui 102b1702ff Add wallets PNGs to be used on mobile (#21) 2023-09-05 12:58:36 -07:00
Jared Vu 2b888001aa Update Onboarding logs and utilize helper functions (#20)
* Handle wallet signing rejection

* nits

* selectedWalletError -> string
2023-09-05 09:59:27 -07:00
Jared Vu d4c149af12 Conditional limit validation (#17)
* abacus 0.4.3

* Add new strings, bump abacus, fix order validation error

* Use STRING_KEYS as ORDER_ERRORS_MAP valu

* remove unused import
2023-09-05 09:56:12 -07:00
jaredvu b0cd8e71ac add tracking protocol 2023-09-02 14:25:23 -07:00
aleka ffde6ed0fc add transfer insufficient usdc gas warning (#19) 2023-09-01 17:51:02 -04:00
Jared Vu 6ac94e5949 Show outline only on focus-within (#18) 2023-09-01 14:01:15 -07:00
James Jia c067206cd1 update LICENSE (#16) 2023-09-01 15:40:04 -04:00
aleka 30187ef69f add new optimized market icons (#15)
* add new optimized market icons
2023-08-31 17:43:25 -04:00
Jared Vu 7d1b9eb65f Adopt v4-localization STRING_KEYS and translation files (#13)
* Delete old localization files

* nits

* @dydxprotocol/v4-localization: 0.0.21

* TAKE_PROFIT -> TAKE_PROFIT_LIMIT
2023-08-31 10:50:41 -07:00
aleka 1835b42bb0 ChainQueriesProtocol -> ChainTransactionsProtocol (#14) 2023-08-31 13:35:51 -04:00
aleka 37a50def99 AssetIcon .svg -> .png (#11) 2023-08-30 18:28:30 -04:00
Jared Vu a97f296905 Utilize v4-localization for Environment Endpoints (#7)
* add abacusFileSystem protocol

* Use v4-localization endpoints, replace old usage of CLIENT_NETWORK_CONFIGS

* remove squidRouter switchNetwork calls, bump abacus

* use ENDPOINTS_PATH to keep abacus/fileSystem consistent

* use NETWORK_ENDPOINTS const

* @dydxprotocol/v4-client -> @dydxprotocol/v4-client-js

* use v4-client-js

* WithdrawForm: Use v4-client-js

* Fix undefined Symbol

* Bump v4-localization
2023-08-30 15:12:40 -04:00
Rui 683d806f44 Add asset PNGs of 128x128 (#10) 2023-08-30 11:23:01 -07:00
Bill e635778438 Deposit/Withdraw with abacus (#3)
* Deposit/Withdraw with abacus

* revert package

* e2e working deposit/withdraws

* deprecate usesquidrouter

* remove unused types

* fix chain select menu

* fix switch network bug

* working switch network deposits

* fix withdraw chain select

* address comments

* address comments

* address comments

* fix clear bug

* use diffarrow

* address nits
2023-08-29 12:11:20 -07:00
aleka 7a456b33d4 Merge pull request #6 from dydxprotocol/readme-vercel-setup
Add instructions for deploying with vercel in readme
2023-08-29 13:07:21 -04:00
aleka 6d25a072bd Add instructions for deploying with vercel in readme 2023-08-28 17:51:03 -04:00
Jared Vu d6baa6ef3f Merge pull request #5 from dydxprotocol/conditional-orders
Conditional orders
2023-08-28 05:49:27 -10:00
jaredvu 195ddd5af7 Move localeProvider, add label, valid marketId 2023-08-28 08:43:36 -07:00
aleka 2a5373c972 Merge pull request #4 from dydxprotocol/transfers-history-table
[TRCL-1842] Transfers history table
2023-08-25 14:13:53 -04:00
Aleka Cheung 64f663ed5b add CopyButton component 2023-08-25 14:07:03 -04:00
jaredvu 983128fd74 misc fix/cleanup 2023-08-25 10:59:59 -07:00
jaredvu e15f4b919c useCurrentMarketId hook for navigation 2023-08-25 10:59:35 -07:00
jaredvu 71c29524b1 useMarketsData: Filter out test-usd market 2023-08-24 14:23:02 -07:00
jaredvu 0e26433e29 disable conditional orders on testnet until they support them 2023-08-24 14:18:55 -07:00
jaredvu 3afd5ef783 OrdersTable: Minimum stepSizeDecimals = TOKEN_DECIMALS 2023-08-24 13:35:38 -07:00
jaredvu b2cac81235 SearchSelect: DropdownIcon position 2023-08-24 13:27:40 -07:00
jaredvu e48ec4ca73 Fix Orderbook spread when nothing on the books 2023-08-24 12:06:03 -07:00
jaredvu 78f9ddff7a Fix crash on test coin 2023-08-24 12:05:47 -07:00
jaredvu c1dcca43c8 Render TradeBoxOrderView from Abacus typeOptions 2023-08-23 23:32:02 -07:00
jaredvu 029476fa19 fix type for placeOrder analytics 2023-08-23 17:31:14 -07:00
jaredvu 6ecdcd981e enable conditional orders 2023-08-23 17:18:40 -07:00
jaredvu 591166e93d locale/browserTag 2023-08-23 17:18:10 -07:00
Aleka Cheung e30940e7df bump abacus + make addresses copyable 2023-08-23 19:37:19 -04:00
Aleka Cheung 2d0a944097 transfers history table 2023-08-23 19:37:14 -04:00
aleka 0dd2ecb6d2 Merge pull request #1 from dydxprotocol/receive-transfer-entry-in-account-menu
Add receive/transfer dialog entry points in AccountMenu + dv4tnt renames
2023-08-23 18:57:32 -04:00
Aleka Cheung 76819e0839 signed commits 2023-08-23 18:56:12 -04:00
Aleka Cheung 32b375cfa4 address feedback 2023-08-23 18:56:11 -04:00
Aleka Cheung 3dc87e873d show receive button for both assets 2023-08-23 18:56:10 -04:00
Aleka Cheung ed61b3112e rename tokens to dv4tnt, fix header dropdown hover state, add entry points to receive/send dialogs in account menu 2023-08-23 18:56:08 -04:00
Jared Vu 42f4142307 Merge pull request #2 from dydxprotocol/analytics-performance
Analytics: add performance metrics
2023-08-21 10:00:00 -10:00
Darryl Yeo c5e79980d1 Analytics: add performance metrics 2023-08-18 13:02:25 -07:00
James Jia - Test 6e179191d8 Initial commit 2023-08-17 15:52:35 -07:00
90 changed files with 1275 additions and 1074 deletions
+12
View File
@@ -2,5 +2,17 @@ VITE_BASE_URL=https://v4.testnet.dydx.exchange
VITE_ALCHEMY_API_KEY=FP275q327Yh7qswtZWenbPXdZZdnAFmC 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_WALLETCONNECT1_BRIDGE=wss://api.dydx.exchange/wc/
VITE_WALLETCONNECT2_PROJECT_ID=fbe94eaa691fa8d929561f8567062b32 VITE_WALLETCONNECT2_PROJECT_ID=fbe94eaa691fa8d929561f8567062b32
+12 -3
View File
@@ -1,8 +1,17 @@
VITE_BASE_URL= VITE_BASE_URL=
VITE_ALCHEMY_API_KEY= VITE_ALCHEMY_API_KEY=
VITE_PK_ENCRYPTION_KEY= VITE_PK_ENCRYPTION_KEY=
VITE_WALLETCONNECT1_BRIDGE=
VITE_WALLETCONNECT2_PROJECT_ID= 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,10 +5,15 @@ vite.config.ts
# Temporarily ignore, we will slowly remove each directory as we fix files to follow ESLint rules # Temporarily ignore, we will slowly remove each directory as we fix files to follow ESLint rules
/src/components /src/components
/src/dialogs
/src/forms /src/forms
/src/hooks /src/hooks
/src/history.ts
/src/icons
/src/lib /src/lib
/src/main.tsx /src/main.tsx
/src/menus /src/menus
/src/pages /src/pages
/src/state
/src/styles
/src/views /src/views
+6 -4
View File
@@ -33,11 +33,14 @@
"jsx-a11y/anchor-is-valid": "off", "jsx-a11y/anchor-is-valid": "off",
"jsx-a11y/click-events-have-key-events": "off", "jsx-a11y/click-events-have-key-events": "off",
"no-continue": "off", "no-continue": "off",
"no-console": ["error"], "no-console": [
"error",
{
"devDependencies": ["./scripts/*.js"]
}
],
"no-lonely-if": "off", "no-lonely-if": "off",
"no-multi-assign": "off",
"no-nested-ternary": "off", "no-nested-ternary": "off",
"no-param-reassign": ["error", { "props": false }],
"no-return-assign": "off", "no-return-assign": "off",
"no-return-await": "off", "no-return-await": "off",
"no-underscore-dangle": "off", "no-underscore-dangle": "off",
@@ -62,7 +65,6 @@
"react/sort-comp": "off", "react/sort-comp": "off",
"@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/comma-dangle": "off", "@typescript-eslint/comma-dangle": "off",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/member-delimiter-style": [ "@typescript-eslint/member-delimiter-style": [
"error", "error",
{ {
+4 -4
View File
@@ -1,4 +1,4 @@
# Contributing to V4-web # Contributing to Trader-Fe
We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:
- Reporting a bug - 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! 4. Issue that pull request!
## Any contributions you make will be under the same License ## 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/v4-web/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/trader-fe/blob/master/LICENSE) that covers the project.
## Report bugs using Github's [issues](https://github.com/dydxprotocol/v4-web/issues) ## 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/v4-web/issues/new). Report a bug by [opening a new issue](https://github.com/dydxprotocol/trader-fe/issues/new).
**Great Bug Reports** tend to have: **Great Bug Reports** tend to have:
+6 -9
View File
@@ -1,19 +1,16 @@
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. 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.
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. 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. 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 Parameters
Licensor: dYdX Trading Inc. 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. 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 Additional Use Grant: None
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 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 License: GNU Affero GPL License v3 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. License text copyright © 2023 MariaDB plc, All Rights Reserved. “Business Source License” is a trademark of MariaDB plc.
@@ -42,7 +39,7 @@ OPEN SOURCE LICENSE BELOW - TO BE APPLIED AFTER THE CHANGE DATE.
Copyright (C) 2023 dYdX Trading Inc. 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 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. Use of this software is governed by the The GNU Affero General Public License as set forth below.
The GNU Affero General Public License The GNU Affero General Public License
@@ -838,4 +835,4 @@ copy of the Program in return for a fee.
For more information about this software, see https://dydx.exchange. For more information about this software, see https://dydx.exchange.
Copyright (C) 2023 dYdX Trading Inc. Copyright (C) 2023 dYdX Trading Inc.
+3 -11
View File
@@ -1,12 +1,4 @@
<p align="center"><img src="https://dydx.exchange/icon.svg?" width="256" /></p> # v4 Web App
<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 ## Prerequisites
@@ -30,8 +22,8 @@ Alternatively, follow the [IPFS Kubo installation guide](https://docs.ipfs.tech
Clone the repository and navigate to its directory: Clone the repository and navigate to its directory:
```bash ```bash
git clone https://github.com/dydxprotocol/v4-web.git git clone https://github.com/dydxprotocol/trader-fe.git
cd v4-web cd trader-fe
``` ```
### Step 2: Install pnpm and dependencies ### Step 2: Install pnpm and dependencies
+3 -3
View File
@@ -37,9 +37,9 @@
"@cosmjs/proto-signing": "^0.31.0", "@cosmjs/proto-signing": "^0.31.0",
"@cosmjs/stargate": "^0.31.0", "@cosmjs/stargate": "^0.31.0",
"@cosmjs/tendermint-rpc": "^0.31.0", "@cosmjs/tendermint-rpc": "^0.31.0",
"@dydxprotocol/v4-abacus": "^0.5.4", "@dydxprotocol/abacus": "^0.4.8",
"@dydxprotocol/v4-client-js": "^0.36.1", "@dydxprotocol/v4-client-js": "^0.32.0",
"@dydxprotocol/v4-localization": "^0.1.11", "@dydxprotocol/v4-localization": "^0.0.25",
"@ethersproject/providers": "^5.7.2", "@ethersproject/providers": "^5.7.2",
"@js-joda/core": "^5.5.3", "@js-joda/core": "^5.5.3",
"@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-collapsible": "^1.0.3",
+242 -36
View File
@@ -26,15 +26,15 @@ dependencies:
'@cosmjs/tendermint-rpc': '@cosmjs/tendermint-rpc':
specifier: ^0.31.0 specifier: ^0.31.0
version: 0.31.0 version: 0.31.0
'@dydxprotocol/v4-abacus': '@dydxprotocol/abacus':
specifier: ^0.5.4 specifier: ^0.4.8
version: 0.5.4 version: 0.4.8
'@dydxprotocol/v4-client-js': '@dydxprotocol/v4-client-js':
specifier: ^0.36.1 specifier: ^0.32.0
version: 0.36.1 version: 0.32.0
'@dydxprotocol/v4-localization': '@dydxprotocol/v4-localization':
specifier: ^0.1.11 specifier: ^0.0.25
version: 0.1.11 version: 0.0.25
'@ethersproject/providers': '@ethersproject/providers':
specifier: ^5.7.2 specifier: ^5.7.2
version: 5.7.2 version: 5.7.2
@@ -614,6 +614,74 @@ packages:
'@babel/helper-validator-identifier': 7.22.5 '@babel/helper-validator-identifier': 7.22.5
to-fast-properties: 2.0.0 to-fast-properties: 2.0.0
/@bufbuild/buf-darwin-arm64@1.19.0-1:
resolution: {integrity: sha512-HsWPii21wm3QSyuxrNq9+Yf8iAgpnC4rNCy4x3d6P1fd/LmgE1NPzQW0ghEZvl9dgAQKkL/4S5bKhlm7kbUdmQ==}
engines: {node: '>=12'}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@bufbuild/buf-darwin-x64@1.19.0-1:
resolution: {integrity: sha512-2+Ig7ylYpVh4kms/OeJJVY+X0KX4awPA6hYr7L7aZOIcHwZEM8lWtSTO/se5pQc7dc8FXNiC4YUqHC8yfxxX6Q==}
engines: {node: '>=12'}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@bufbuild/buf-linux-aarch64@1.19.0-1:
resolution: {integrity: sha512-g/Vxg3WiBr3nhsxsRr2Q81xXJD+0ktHIO3ZJggTG2Sbbl3dh8kyg1iKM6MjJiMP7su5RKCylLigzoEJzVTShyA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@bufbuild/buf-linux-x64@1.19.0-1:
resolution: {integrity: sha512-anYuGx8k/2kp8GPX3eHNUf3IY/01Zpnyw0HaLPXK1Btqyy6XkapVywrDqg7YUzMd1ySFEp1wD9UqRNdEFNCQ4A==}
engines: {node: '>=12'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@bufbuild/buf-win32-arm64@1.19.0-1:
resolution: {integrity: sha512-xXgF1qYnCfRKbGx1FqvPbpZ6ajh4ddxpXhSxI3VCeb3MsMBuIbiLqX4fQAL3ls/Zwz8tVIITuSwOhYmSEGcpBA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@bufbuild/buf-win32-x64@1.19.0-1:
resolution: {integrity: sha512-futmqgpMQCR1lcAzZJEGjPr7ECw1gYTPIV8crm5SY+iCJ7sOeStOBNt7q5hV4LKmmeWmvm03XIMZPjhQzjH5NQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@bufbuild/buf@1.19.0-1:
resolution: {integrity: sha512-TIsLTTQUntr/Xq/IMSULv3dlC3/ZsVwQtWgxmJ++IzSuOW79TFQfq59vFeTWrPa6+QXFMz5t6jkMyD4ghzO5nw==}
engines: {node: '>=12'}
hasBin: true
requiresBuild: true
optionalDependencies:
'@bufbuild/buf-darwin-arm64': 1.19.0-1
'@bufbuild/buf-darwin-x64': 1.19.0-1
'@bufbuild/buf-linux-aarch64': 1.19.0-1
'@bufbuild/buf-linux-x64': 1.19.0-1
'@bufbuild/buf-win32-arm64': 1.19.0-1
'@bufbuild/buf-win32-x64': 1.19.0-1
dev: false
/@coinbase/wallet-sdk@3.7.1: /@coinbase/wallet-sdk@3.7.1:
resolution: {integrity: sha512-LjyoDCB+7p0waQXfK+fUgcAs3Ezk6S6e+LYaoFjpJ6c9VTop3NyZF40Pi7df4z7QJohCwzuIDjz0Rhtig6Y7Pg==} resolution: {integrity: sha512-LjyoDCB+7p0waQXfK+fUgcAs3Ezk6S6e+LYaoFjpJ6c9VTop3NyZF40Pi7df4z7QJohCwzuIDjz0Rhtig6Y7Pg==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
@@ -658,6 +726,15 @@ packages:
'@cosmjs/utils': 0.27.1 '@cosmjs/utils': 0.27.1
dev: false dev: false
/@cosmjs/amino@0.29.3:
resolution: {integrity: sha512-BFz1++ERerIggiFc7iGHhGe1CeV3rCv8BvkoBQTBN/ZwzHOaKvqQj8smDlRGlQxX3HWlTwgiLN2A+OB5yX4ZRw==}
dependencies:
'@cosmjs/crypto': 0.29.5
'@cosmjs/encoding': 0.29.5
'@cosmjs/math': 0.29.5
'@cosmjs/utils': 0.29.5
dev: false
/@cosmjs/amino@0.30.1: /@cosmjs/amino@0.30.1:
resolution: {integrity: sha512-yNHnzmvAlkETDYIpeCTdVqgvrdt1qgkOXwuRVi8s27UKI5hfqyE9fJ/fuunXE6ZZPnKkjIecDznmuUOMrMvw4w==} resolution: {integrity: sha512-yNHnzmvAlkETDYIpeCTdVqgvrdt1qgkOXwuRVi8s27UKI5hfqyE9fJ/fuunXE6ZZPnKkjIecDznmuUOMrMvw4w==}
dependencies: dependencies:
@@ -711,6 +788,18 @@ packages:
sha.js: 2.4.11 sha.js: 2.4.11
dev: false dev: false
/@cosmjs/crypto@0.29.5:
resolution: {integrity: sha512-2bKkaLGictaNL0UipQCL6C1afaisv6k8Wr/GCLx9FqiyFkh9ZgRHDyetD64ZsjnWV/N/D44s/esI+k6oPREaiQ==}
dependencies:
'@cosmjs/encoding': 0.29.5
'@cosmjs/math': 0.29.5
'@cosmjs/utils': 0.29.5
'@noble/hashes': 1.3.1
bn.js: 5.2.1
elliptic: 6.5.4
libsodium-wrappers: 0.7.11
dev: false
/@cosmjs/crypto@0.30.1: /@cosmjs/crypto@0.30.1:
resolution: {integrity: sha512-rAljUlake3MSXs9xAm87mu34GfBLN0h/1uPPV6jEwClWjNkAMotzjC0ab9MARy5FFAvYHL3lWb57bhkbt2GtzQ==} resolution: {integrity: sha512-rAljUlake3MSXs9xAm87mu34GfBLN0h/1uPPV6jEwClWjNkAMotzjC0ab9MARy5FFAvYHL3lWb57bhkbt2GtzQ==}
dependencies: dependencies:
@@ -743,6 +832,14 @@ packages:
readonly-date: 1.0.0 readonly-date: 1.0.0
dev: false dev: false
/@cosmjs/encoding@0.29.5:
resolution: {integrity: sha512-G4rGl/Jg4dMCw5u6PEZHZcoHnUBlukZODHbm/wcL4Uu91fkn5jVo5cXXZcvs4VCkArVGrEj/52eUgTZCmOBGWQ==}
dependencies:
base64-js: 1.5.1
bech32: 1.1.4
readonly-date: 1.0.0
dev: false
/@cosmjs/encoding@0.30.1: /@cosmjs/encoding@0.30.1:
resolution: {integrity: sha512-rXmrTbgqwihORwJ3xYhIgQFfMSrwLu1s43RIK9I8EBudPx3KmnmyAKzMOVsRDo9edLFNuZ9GIvysUCwQfq3WlQ==} resolution: {integrity: sha512-rXmrTbgqwihORwJ3xYhIgQFfMSrwLu1s43RIK9I8EBudPx3KmnmyAKzMOVsRDo9edLFNuZ9GIvysUCwQfq3WlQ==}
dependencies: dependencies:
@@ -759,12 +856,11 @@ packages:
readonly-date: 1.0.0 readonly-date: 1.0.0
dev: false dev: false
/@cosmjs/encoding@0.31.1: /@cosmjs/json-rpc@0.29.5:
resolution: {integrity: sha512-IuxP6ewwX6vg9sUJ8ocJD92pkerI4lyG8J5ynAM3NaX3q+n+uMoPRSQXNeL9bnlrv01FF1kIm8if/f5F7ZPtkA==} resolution: {integrity: sha512-C78+X06l+r9xwdM1yFWIpGl03LhB9NdM1xvZpQHwgCOl0Ir/WV8pw48y3Ez2awAoUBRfTeejPe4KvrE6NoIi/w==}
dependencies: dependencies:
base64-js: 1.5.1 '@cosmjs/stream': 0.29.5
bech32: 1.1.4 xstream: 11.14.0
readonly-date: 1.0.0
dev: false dev: false
/@cosmjs/json-rpc@0.30.1: /@cosmjs/json-rpc@0.30.1:
@@ -801,6 +897,12 @@ packages:
bn.js: 5.2.1 bn.js: 5.2.1
dev: false dev: false
/@cosmjs/math@0.29.5:
resolution: {integrity: sha512-2GjKcv+A9f86MAWYLUkjhw1/WpRl2R1BTb3m9qPG7lzMA7ioYff9jY5SPCfafKdxM4TIQGxXQlYGewQL16O68Q==}
dependencies:
bn.js: 5.2.1
dev: false
/@cosmjs/math@0.30.1: /@cosmjs/math@0.30.1:
resolution: {integrity: sha512-yaoeI23pin9ZiPHIisa6qqLngfnBR/25tSaWpkTm8Cy10MX70UF5oN4+/t1heLaM6SSmRrhk3psRkV4+7mH51Q==} resolution: {integrity: sha512-yaoeI23pin9ZiPHIisa6qqLngfnBR/25tSaWpkTm8Cy10MX70UF5oN4+/t1heLaM6SSmRrhk3psRkV4+7mH51Q==}
dependencies: dependencies:
@@ -813,10 +915,16 @@ packages:
bn.js: 5.2.1 bn.js: 5.2.1
dev: false dev: false
/@cosmjs/math@0.31.1: /@cosmjs/proto-signing@0.29.3:
resolution: {integrity: sha512-kiuHV6m6DSB8/4UV1qpFhlc4ul8SgLXTGRlYkYiIIP4l0YNeJ+OpPYaOlEgx4Unk2mW3/O2FWYj7Jc93+BWXng==} resolution: {integrity: sha512-Ai3l9THjMOrLJ4Ebn1Dgptwg6W5ZIRJqtnJjijHhGwTVC1WT0WdYU3aMZ7+PwubcA/cA1rH4ZTK7jrfYbra63g==}
dependencies: dependencies:
bn.js: 5.2.1 '@cosmjs/amino': 0.29.3
'@cosmjs/crypto': 0.29.5
'@cosmjs/encoding': 0.29.5
'@cosmjs/math': 0.29.5
'@cosmjs/utils': 0.29.5
cosmjs-types: 0.5.2
long: 4.0.0
dev: false dev: false
/@cosmjs/proto-signing@0.30.1: /@cosmjs/proto-signing@0.30.1:
@@ -843,6 +951,18 @@ packages:
long: 4.0.0 long: 4.0.0
dev: false dev: false
/@cosmjs/socket@0.29.5:
resolution: {integrity: sha512-5VYDupIWbIXq3ftPV1LkS5Ya/T7Ol/AzWVhNxZ79hPe/mBfv1bGau/LqIYOm2zxGlgm9hBHOTmWGqNYDwr9LNQ==}
dependencies:
'@cosmjs/stream': 0.29.5
isomorphic-ws: 4.0.1(ws@7.5.9)
ws: 7.5.9
xstream: 11.14.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
dev: false
/@cosmjs/socket@0.30.1: /@cosmjs/socket@0.30.1:
resolution: {integrity: sha512-r6MpDL+9N+qOS/D5VaxnPaMJ3flwQ36G+vPvYJsXArj93BjgyFB7BwWwXCQDzZ+23cfChPUfhbINOenr8N2Kow==} resolution: {integrity: sha512-r6MpDL+9N+qOS/D5VaxnPaMJ3flwQ36G+vPvYJsXArj93BjgyFB7BwWwXCQDzZ+23cfChPUfhbINOenr8N2Kow==}
dependencies: dependencies:
@@ -867,6 +987,27 @@ packages:
- utf-8-validate - utf-8-validate
dev: false dev: false
/@cosmjs/stargate@0.29.3:
resolution: {integrity: sha512-455TgXStCi6E8KDjnhDAM8wt6aLSjobH4Dixvd7Up1DfCH6UB9NkC/G0fMJANNcNXMaM4wSX14niTXwD1d31BA==}
dependencies:
'@confio/ics23': 0.6.8
'@cosmjs/amino': 0.29.3
'@cosmjs/encoding': 0.29.5
'@cosmjs/math': 0.29.5
'@cosmjs/proto-signing': 0.29.3
'@cosmjs/stream': 0.29.5
'@cosmjs/tendermint-rpc': 0.29.5
'@cosmjs/utils': 0.29.5
cosmjs-types: 0.5.2
long: 4.0.0
protobufjs: 6.11.4
xstream: 11.14.0
transitivePeerDependencies:
- bufferutil
- debug
- utf-8-validate
dev: false
/@cosmjs/stargate@0.30.1: /@cosmjs/stargate@0.30.1:
resolution: {integrity: sha512-RdbYKZCGOH8gWebO7r6WvNnQMxHrNXInY/gPHPzMjbQF6UatA6fNM2G2tdgS5j5u7FTqlCI10stNXrknaNdzog==} resolution: {integrity: sha512-RdbYKZCGOH8gWebO7r6WvNnQMxHrNXInY/gPHPzMjbQF6UatA6fNM2G2tdgS5j5u7FTqlCI10stNXrknaNdzog==}
dependencies: dependencies:
@@ -909,6 +1050,12 @@ packages:
- utf-8-validate - utf-8-validate
dev: false dev: false
/@cosmjs/stream@0.29.5:
resolution: {integrity: sha512-TToTDWyH1p05GBtF0Y8jFw2C+4783ueDCmDyxOMM6EU82IqpmIbfwcdMOCAm0JhnyMh+ocdebbFvnX/sGKzRAA==}
dependencies:
xstream: 11.14.0
dev: false
/@cosmjs/stream@0.30.1: /@cosmjs/stream@0.30.1:
resolution: {integrity: sha512-Fg0pWz1zXQdoxQZpdHRMGvUH5RqS6tPv+j9Eh7Q953UjMlrwZVo0YFLC8OTf/HKVf10E4i0u6aM8D69Q6cNkgQ==} resolution: {integrity: sha512-Fg0pWz1zXQdoxQZpdHRMGvUH5RqS6tPv+j9Eh7Q953UjMlrwZVo0YFLC8OTf/HKVf10E4i0u6aM8D69Q6cNkgQ==}
dependencies: dependencies:
@@ -921,6 +1068,25 @@ packages:
xstream: 11.14.0 xstream: 11.14.0
dev: false dev: false
/@cosmjs/tendermint-rpc@0.29.5:
resolution: {integrity: sha512-ar80twieuAxsy0x2za/aO3kBr2DFPAXDmk2ikDbmkda+qqfXgl35l9CVAAjKRqd9d+cRvbQyb5M4wy6XQpEV6w==}
dependencies:
'@cosmjs/crypto': 0.29.5
'@cosmjs/encoding': 0.29.5
'@cosmjs/json-rpc': 0.29.5
'@cosmjs/math': 0.29.5
'@cosmjs/socket': 0.29.5
'@cosmjs/stream': 0.29.5
'@cosmjs/utils': 0.29.5
axios: 0.21.4
readonly-date: 1.0.0
xstream: 11.14.0
transitivePeerDependencies:
- bufferutil
- debug
- utf-8-validate
dev: false
/@cosmjs/tendermint-rpc@0.30.1: /@cosmjs/tendermint-rpc@0.30.1:
resolution: {integrity: sha512-Z3nCwhXSbPZJ++v85zHObeUggrEHVfm1u18ZRwXxFE9ZMl5mXTybnwYhczuYOl7KRskgwlB+rID0WYACxj4wdQ==} resolution: {integrity: sha512-Z3nCwhXSbPZJ++v85zHObeUggrEHVfm1u18ZRwXxFE9ZMl5mXTybnwYhczuYOl7KRskgwlB+rID0WYACxj4wdQ==}
dependencies: dependencies:
@@ -963,6 +1129,10 @@ packages:
resolution: {integrity: sha512-VG7QPDiMUzVPxRdJahDV8PXxVdnuAHiIuG56hldV4yPnOz/si/DLNd7VAUUA5923b6jS1Hhev0Hr6AhEkcxBMg==} resolution: {integrity: sha512-VG7QPDiMUzVPxRdJahDV8PXxVdnuAHiIuG56hldV4yPnOz/si/DLNd7VAUUA5923b6jS1Hhev0Hr6AhEkcxBMg==}
dev: false dev: false
/@cosmjs/utils@0.29.5:
resolution: {integrity: sha512-m7h+RXDUxOzEOGt4P+3OVPX7PuakZT3GBmaM/Y2u+abN3xZkziykD/NvedYFvvCCdQo714XcGl33bwifS9FZPQ==}
dev: false
/@cosmjs/utils@0.30.1: /@cosmjs/utils@0.30.1:
resolution: {integrity: sha512-KvvX58MGMWh7xA+N+deCfunkA/ZNDvFLw4YbOmX3f/XBIkqrVY7qlotfy2aNb1kgp6h4B6Yc8YawJPDTfvWX7g==} resolution: {integrity: sha512-KvvX58MGMWh7xA+N+deCfunkA/ZNDvFLw4YbOmX3f/XBIkqrVY7qlotfy2aNb1kgp6h4B6Yc8YawJPDTfvWX7g==}
dev: false dev: false
@@ -971,29 +1141,39 @@ packages:
resolution: {integrity: sha512-nNcycZWUYLNJlrIXgpcgVRqdl6BXjF4YlXdxobQWpW9Tikk61bEGeAFhDYtC0PwHlokCNw0KxWiHGJL4nL7Q5A==} resolution: {integrity: sha512-nNcycZWUYLNJlrIXgpcgVRqdl6BXjF4YlXdxobQWpW9Tikk61bEGeAFhDYtC0PwHlokCNw0KxWiHGJL4nL7Q5A==}
dev: false dev: false
/@cosmjs/utils@0.31.1:
resolution: {integrity: sha512-n4Se1wu4GnKwztQHNFfJvUeWcpvx3o8cWhSbNs9JQShEuB3nv3R5lqFBtDCgHZF/emFQAP+ZjF8bTfCs9UBGhA==}
dev: false
/@cush/relative@1.0.0: /@cush/relative@1.0.0:
resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==} resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==}
dev: true dev: true
/@dydxprotocol/v4-abacus@0.5.4: /@dydxprotocol/abacus@0.4.8:
resolution: {integrity: sha512-LMwmyXCih2bBdLX3vO5OwbdGWg3oZ8WSGj+4nqr8tn/bn1qCSKdYdZZ9/U20Yir2rMAcoC0jmznMcBwthlSp/w==} resolution: {integrity: sha512-hX31m+a5Bwbs4dr5raPDtnflwUzEu/9BIt+LOlQgt1YOViuIse8dozgpgCHvqvA8MttqY3QGPalx5iRB8j8vtw==}
dev: false dev: false
/@dydxprotocol/v4-client-js@0.36.1: /@dydxprotocol/dydxjs@0.3.0:
resolution: {integrity: sha512-KJ3MtWI61wmm+xZtXQd7/hYKIoVFG32TRdyyVaXXO8vpW9Oqd0JD8QSM/wuegkciAh5YmP/BrDzYuu77zHO6yg==} resolution: {integrity: sha512-ygNeBs0f3H7sJk1qLUjfNwaXgc5TmjD+qZf7BFDbi7+boHJI9xuOWrGpjfBo7OTUZxS6O5jZG323CgU5XqMSPw==}
dependencies: dependencies:
'@babel/runtime': 7.22.10
'@bufbuild/buf': 1.19.0-1
'@cosmjs/amino': 0.30.1 '@cosmjs/amino': 0.30.1
'@cosmjs/encoding': 0.31.1
'@cosmjs/math': 0.31.1
'@cosmjs/proto-signing': 0.30.1 '@cosmjs/proto-signing': 0.30.1
'@cosmjs/stargate': 0.30.1 '@cosmjs/stargate': 0.30.1
'@cosmjs/tendermint-rpc': 0.30.1 '@cosmjs/tendermint-rpc': 0.30.1
'@cosmjs/utils': 0.31.1 osmojs: 15.5.0
'@dydxprotocol/v4-proto': 0.2.1 protobufjs: 6.11.4
transitivePeerDependencies:
- bufferutil
- debug
- utf-8-validate
dev: false
/@dydxprotocol/v4-client-js@0.32.0:
resolution: {integrity: sha512-KvKActMgWLcsRQ7RQhpi/F8xeQ7MWUp3G5+4IPNZ4wyRwAxDSbcrfYbTetI/IesHyelRPSeYxam9eYq3wXwZ1A==}
dependencies:
'@cosmjs/amino': 0.30.1
'@cosmjs/proto-signing': 0.30.1
'@cosmjs/stargate': 0.30.1
'@cosmjs/tendermint-rpc': 0.30.1
'@dydxprotocol/dydxjs': 0.3.0
'@osmonauts/lcd': 0.6.0 '@osmonauts/lcd': 0.6.0
'@scure/bip32': 1.3.1 '@scure/bip32': 1.3.1
'@scure/bip39': 1.2.1 '@scure/bip39': 1.2.1
@@ -1002,7 +1182,6 @@ packages:
bignumber.js: 9.1.1 bignumber.js: 9.1.1
ethereum-cryptography: 2.1.2 ethereum-cryptography: 2.1.2
long: 4.0.0 long: 4.0.0
protobufjs: 6.11.4
ws: 8.13.0(bufferutil@4.0.7)(utf-8-validate@5.0.10) ws: 8.13.0(bufferutil@4.0.7)(utf-8-validate@5.0.10)
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
@@ -1010,14 +1189,8 @@ packages:
- utf-8-validate - utf-8-validate
dev: false dev: false
/@dydxprotocol/v4-localization@0.1.11: /@dydxprotocol/v4-localization@0.0.25:
resolution: {integrity: sha512-eW88t8KJuDT0fNEgaw615+n3gEYxDZYPeHhdBYquVDXjnkhLNznaH8ftmlLPVPyfu7o5aFeU/cw8ZiqRrf/Stw==} resolution: {integrity: sha512-Rp6VHA8z+nwgvTjjB3fvL8gY7bd4DYDHV+NPoL0MFuRm39vhFxGlBVAGDkXPkQTh6YU5BlZV2yXLkDeSfFqEBQ==}
dev: false
/@dydxprotocol/v4-proto@0.2.1:
resolution: {integrity: sha512-Q5b/g331DoyMjBpO2/jtrxUX3WF5UaIKZp/unK332xuefHNFnkMYtZ05p0V71iSdO35tBdz8yt0VYP2WtUsp+w==}
dependencies:
protobufjs: 6.11.4
dev: false dev: false
/@emotion/is-prop-valid@1.2.1: /@emotion/is-prop-valid@1.2.1:
@@ -2073,6 +2246,15 @@ packages:
fastq: 1.15.0 fastq: 1.15.0
dev: true dev: true
/@osmonauts/lcd@0.10.0:
resolution: {integrity: sha512-PzmXk9x9MHyLn2fUztpAqWqvDmMiEJaQv/JcAoAOE8VdHrD9Hf/KWnE1RZtamuS2ngQRqvQPD0xotCGXW7eTxA==}
dependencies:
'@babel/runtime': 7.22.10
axios: 0.27.2
transitivePeerDependencies:
- debug
dev: false
/@osmonauts/lcd@0.6.0: /@osmonauts/lcd@0.6.0:
resolution: {integrity: sha512-vz9VavXrEfxZoXbSAfNfk90MLpn34XtBYPV3L9YilE+s56AhqYxUh83nne9J5somnTRfGnyR3oeV8C+lHkqiuA==} resolution: {integrity: sha512-vz9VavXrEfxZoXbSAfNfk90MLpn34XtBYPV3L9YilE+s56AhqYxUh83nne9J5somnTRfGnyR3oeV8C+lHkqiuA==}
dependencies: dependencies:
@@ -7605,6 +7787,13 @@ packages:
path-type: 4.0.0 path-type: 4.0.0
dev: true dev: true
/cosmjs-types@0.5.2:
resolution: {integrity: sha512-zxCtIJj8v3Di7s39uN4LNcN3HIE1z0B9Z0SPE8ZNQR0oSzsuSe1ACgxoFkvhkS7WBasCAFcglS11G2hyfd5tPg==}
dependencies:
long: 4.0.0
protobufjs: 6.11.4
dev: false
/cosmjs-types@0.7.2: /cosmjs-types@0.7.2:
resolution: {integrity: sha512-vf2uLyktjr/XVAgEq0DjMxeAWh1yYREe7AMHDKd7EiHVqxBPCaBS+qEEQUkXbR9ndnckqr1sUG8BQhazh4X5lA==} resolution: {integrity: sha512-vf2uLyktjr/XVAgEq0DjMxeAWh1yYREe7AMHDKd7EiHVqxBPCaBS+qEEQUkXbR9ndnckqr1sUG8BQhazh4X5lA==}
dependencies: dependencies:
@@ -11452,6 +11641,23 @@ packages:
type-check: 0.4.0 type-check: 0.4.0
dev: true dev: true
/osmojs@15.5.0:
resolution: {integrity: sha512-7wy3QEYu+R+c8imfLEPS1PliYA8uw0qdFYjc2PToHteYG4hzYt4Noa7X0lM9O59s7nxo9ozT1N9z7uwB/1qHkA==}
dependencies:
'@babel/runtime': 7.22.10
'@cosmjs/amino': 0.29.3
'@cosmjs/proto-signing': 0.29.3
'@cosmjs/stargate': 0.29.3
'@cosmjs/tendermint-rpc': 0.29.5
'@osmonauts/lcd': 0.10.0
long: 5.2.3
protobufjs: 6.11.4
transitivePeerDependencies:
- bufferutil
- debug
- utf-8-validate
dev: false
/p-defer@3.0.0: /p-defer@3.0.0:
resolution: {integrity: sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==} resolution: {integrity: sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==}
engines: {node: '>=8'} engines: {node: '>=8'}
+5 -6
View File
@@ -10,11 +10,11 @@ import { AppRoute, DEFAULT_TRADE_ROUTE } from '@/constants/routes';
import { useBreakpoints, useInitializePage, useShouldShowFooter, useAnalytics } from '@/hooks'; import { useBreakpoints, useInitializePage, useShouldShowFooter, useAnalytics } from '@/hooks';
import { DydxProvider } from '@/hooks/useDydxClient'; import { DydxProvider } from '@/hooks/useDydxClient';
import { AccountsProvider } from '@/hooks/useAccounts'; import { AccountsProvider } from '@/hooks/useAccounts';
import { DialogAreaProvider, useDialogArea } from '@/hooks/useDialogArea'; import { DialogAreaProvider, useDialogArea } from './hooks/useDialogArea';
import { LocaleProvider } from '@/hooks/useLocaleSeparators'; import { LocaleProvider } from './hooks/useLocaleSeparators';
import { NotificationsProvider } from '@/hooks/useNotifications'; import { NotificationsProvider } from './hooks/useNotifications';
import { LocalNotificationsProvider } from '@/hooks/useLocalNotifications'; import { LocalNotificationsProvider } from './hooks/useLocalNotifications';
import { SubaccountProvider } from '@/hooks/useSubaccount'; import { SubaccountProvider } from './hooks/useSubaccount';
import { SquidProvider } from '@/hooks/useSquid'; import { SquidProvider } from '@/hooks/useSquid';
import { GuardedMobileRoute } from '@/components/GuardedMobileRoute'; import { GuardedMobileRoute } from '@/components/GuardedMobileRoute';
@@ -100,7 +100,6 @@ const Content = () => {
}; };
const wrapProvider = (Component: React.ComponentType<any>, props?: any) => { const wrapProvider = (Component: React.ComponentType<any>, props?: any) => {
// eslint-disable-next-line react/display-name
return ({ children }: { children: React.ReactNode }) => ( return ({ children }: { children: React.ReactNode }) => (
<Component {...props}>{children}</Component> <Component {...props}>{children}</Component>
); );
-4
View File
@@ -80,8 +80,6 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
{group.items.map((item) => ( {group.items.map((item) => (
<Fragment key={item.value}> <Fragment key={item.value}>
<Styled.Item <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] value={[group.groupLabel, item.value, item.description, item.label, item.tag]
.filter(Boolean) .filter(Boolean)
.join('|')} .join('|')}
@@ -116,8 +114,6 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
item.subitems?.map((subitem) => ( item.subitems?.map((subitem) => (
<Fragment key={subitem.value}> <Fragment key={subitem.value}>
<Styled.Item <Styled.Item
// value={subitem.value} // search by both value and textContent
// value={[group.groupLabel, item.label, subitem.label, subitem.tag].filter(Boolean).join('|')}
value={[ value={[
group.groupLabel, group.groupLabel,
item.value, item.value,
+1 -7
View File
@@ -1,4 +1,4 @@
import Abacus, { kollections } from '@dydxprotocol/v4-abacus'; import Abacus, { kollections } from '@dydxprotocol/abacus';
import { OrderSide } from '@dydxprotocol/v4-client-js'; import { OrderSide } from '@dydxprotocol/v4-client-js';
import { PositionSide, TradeTypes } from './trade'; import { PositionSide, TradeTypes } from './trade';
import { STRING_KEYS } from './localization'; import { STRING_KEYS } from './localization';
@@ -44,7 +44,6 @@ export type AbacusTrackingProtocol = Omit<
Abacus.exchange.dydx.abacus.protocols.TrackingProtocol, Abacus.exchange.dydx.abacus.protocols.TrackingProtocol,
'__doNotUseOrImplementIt' '__doNotUseOrImplementIt'
>; >;
export type FileLocation = Abacus.exchange.dydx.abacus.protocols.FileLocation; export type FileLocation = Abacus.exchange.dydx.abacus.protocols.FileLocation;
export type ThreadingType = Abacus.exchange.dydx.abacus.protocols.ThreadingType; export type ThreadingType = Abacus.exchange.dydx.abacus.protocols.ThreadingType;
export const CoroutineTimer = Abacus.exchange.dydx.abacus.utils.CoroutineTimer; export const CoroutineTimer = Abacus.exchange.dydx.abacus.utils.CoroutineTimer;
@@ -122,7 +121,6 @@ export const InputSelectionOption = Abacus.exchange.dydx.abacus.output.input.Sel
// ------ Wallet ------ // // ------ Wallet ------ //
export type Wallet = Abacus.exchange.dydx.abacus.output.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 Subaccount = Abacus.exchange.dydx.abacus.output.Subaccount;
export type SubaccountPosition = Abacus.exchange.dydx.abacus.output.SubaccountPosition; export type SubaccountPosition = Abacus.exchange.dydx.abacus.output.SubaccountPosition;
export type SubaccountOrder = Abacus.exchange.dydx.abacus.output.SubaccountOrder; export type SubaccountOrder = Abacus.exchange.dydx.abacus.output.SubaccountOrder;
@@ -189,10 +187,6 @@ export type HumanReadablePlaceOrderPayload =
Abacus.exchange.dydx.abacus.state.manager.HumanReadablePlaceOrderPayload; Abacus.exchange.dydx.abacus.state.manager.HumanReadablePlaceOrderPayload;
export type HumanReadableCancelOrderPayload = export type HumanReadableCancelOrderPayload =
Abacus.exchange.dydx.abacus.state.manager.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 ------ // // ------ Helpers ------ //
export const AbacusHelper = Abacus.exchange.dydx.abacus.utils.AbacusHelper; export const AbacusHelper = Abacus.exchange.dydx.abacus.utils.AbacusHelper;
+19 -4
View File
@@ -1,5 +1,18 @@
import type { DydxAddress, EvmAddress } from './wallets'; /**
* 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
*/
export enum OnboardingSteps { export enum OnboardingSteps {
ChooseWallet = 'ChooseWallet', ChooseWallet = 'ChooseWallet',
KeyDerivation = 'KeyDerivation', KeyDerivation = 'KeyDerivation',
@@ -36,12 +49,14 @@ export enum EvmDerivedAccountStatus {
Derived, Derived,
} }
import type { DydxAddress, EvmAddress } from './wallets';
export type EvmDerivedAddresses = { export type EvmDerivedAddresses = {
version?: string; version?: string;
[EvmAddress: EvmAddress]: { [EvmAddress: EvmAddress]: {
encryptedSignature?: string; encryptedSignature?: string;
dydxAddress?: DydxAddress; dydxAddress?: DydxAddress;
}; };
}; }
export const AMOUNT_RESERVED_FOR_GAS_USDC = 100_000; export const AMOUNT_RESERVED_FOR_GAS_USDC = 100_000;
+2 -13
View File
@@ -71,7 +71,6 @@ export enum AnalyticsEvent {
// Transfers // Transfers
TransferFaucet = 'TransferFaucet', TransferFaucet = 'TransferFaucet',
TransferFaucetConfirmed = 'TransferFaucetConfirmed',
TransferDeposit = 'TransferDeposit', TransferDeposit = 'TransferDeposit',
TransferWithdraw = 'TransferWithdraw', TransferWithdraw = 'TransferWithdraw',
@@ -79,7 +78,6 @@ export enum AnalyticsEvent {
TradeOrderTypeSelected = 'TradeOrderTypeSelected', TradeOrderTypeSelected = 'TradeOrderTypeSelected',
TradePlaceOrder = 'TradePlaceOrder', TradePlaceOrder = 'TradePlaceOrder',
TradePlaceOrderConfirmed = 'TradePlaceOrderConfirmed', TradePlaceOrderConfirmed = 'TradePlaceOrderConfirmed',
TradeCancelOrder = 'TradeCancelOrder',
TradeCancelOrderConfirmed = 'TradeCancelOrderConfirmed', TradeCancelOrderConfirmed = 'TradeCancelOrderConfirmed',
} }
@@ -135,13 +133,6 @@ export type AnalyticsEventData<T extends AnalyticsEvent> =
: // Transfers : // Transfers
T extends AnalyticsEvent.TransferFaucet 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.TransferDeposit
? {} ? {}
: T extends AnalyticsEvent.TransferWithdraw : T extends AnalyticsEvent.TransferWithdraw
@@ -160,15 +151,13 @@ export type AnalyticsEventData<T extends AnalyticsEvent> =
/** roundtrip time between user placing an order and confirmation from indexer (client → validator → indexer → client) */ /** roundtrip time between user placing an order and confirmation from indexer (client → validator → indexer → client) */
roundtripMs: number; roundtripMs: number;
/** URL/IP of node the order was sent to */ /** URL/IP of node the order was sent to */
validatorUrl: string; validator: string;
} }
: T extends AnalyticsEvent.TradeCancelOrder
? {}
: T extends AnalyticsEvent.TradeCancelOrderConfirmed : T extends AnalyticsEvent.TradeCancelOrderConfirmed
? { ? {
/** roundtrip time between user canceling an order and confirmation from indexer (client → validator → indexer → client) */ /** roundtrip time between user canceling an order and confirmation from indexer (client → validator → indexer → client) */
roundtripMs: number; roundtripMs: number;
/** URL/IP of node the order was sent to */ /** URL/IP of node the order was sent to */
validatorUrl: string; validator: string;
} }
: never; : never;
+16
View File
@@ -88,3 +88,19 @@ export type TooltipStrings = {
learnMoreLink?: string; learnMoreLink?: string;
}; };
}; };
export const ORDER_ERROR_CODE_MAP: { [key: number]: string } = {
2000: STRING_KEYS['2000_FILL_OR_KILL_ORDER_COULD_NOT_BE_FULLY_FILLED'],
2001: STRING_KEYS['2001_REDUCE_ONLY_WOULD_INCREASE_POSITION_SIZE'],
2002: STRING_KEYS['2002_REDUCE_ONLY_WOULD_CHANGE_POSITION_SIDE'],
2003: STRING_KEYS['2003_POST_ONLY_WOULD_CROSS_MAKER_ORDER'],
3000: STRING_KEYS['3000_INVALID_ORDER_FLAGS'],
3001: STRING_KEYS['3001_INVALID_STATEFUL_ORDER_GOOD_TIL_BLOCK_TIME'],
3002: STRING_KEYS['3002_STATEFUL_ORDERS_CANNOT_REQUIRE_IMMEDIATE_EXECUTION'],
3003: STRING_KEYS['3003_TIME_EXCEEDS_GOOD_TIL_BLOCK_TIME'],
3004: STRING_KEYS['3004_GOOD_TIL_BLOCK_TIME_EXCEEDS_STATEFUL_ORDER_TIME_WINDOW'],
3005: STRING_KEYS['3005_STATEFUL_ORDER_ALREADY_EXISTS'],
3006: STRING_KEYS['3006_STATEFUL_ORDER_DOES_NOT_EXIST'],
3007: STRING_KEYS['3007_STATEFUL_ORDER_COLLATERALIZATION_CHECK_FAILED'],
3008: STRING_KEYS['3008_STATEFUL_ORDER_PREVIOUSLY_CANCELLED'],
};
+30
View File
@@ -1,6 +1,36 @@
import { Asset, PerpetualMarket } from '@/constants/abacus'; import { Asset, PerpetualMarket } from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization'; 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 = { export type MarketData = {
asset: Asset; asset: Asset;
tickSizeDecimals: number; tickSizeDecimals: number;
+21 -15
View File
@@ -1,4 +1,5 @@
import { StatusResponse } from '@0xsquid/sdk'; import type { ReactNode } from 'react';
import type { StatusResponse } from '@0xsquid/sdk';
/** implemented in useNotificationTypes */ /** implemented in useNotificationTypes */
export enum NotificationType { export enum NotificationType {
@@ -11,39 +12,44 @@ export enum NotificationComponentType {}
export type NotificationId = string | number; export type NotificationId = string | number;
export type NotificationTypeConfig< export type NotificationTypeConfig<
NotificationIdType extends NotificationId = string, _NotificationId extends NotificationId = string,
NotificationUpdateKey = any NotificationUpdateKey = any
> = { > = {
type: NotificationType; type: NotificationType;
/** React hook to trigger notifications based on app state */ /** React hook to trigger notifications based on app state */
useTrigger: (_: { useTrigger: (_: {
trigger: ( trigger: ({
id,
displayData,
updateKey,
isNew,
}: {
/** Unique ID for the triggered notification */ /** Unique ID for the triggered notification */
id: NotificationIdType, id: _NotificationId;
/** Display data for the triggered notification */ /** Display data for the triggered notification */
displayData: NotificationDisplayData, displayData: NotificationDisplayData;
/** /**
* JSON-serializable key. * JSON-serializable key.
* Re-triggers the notification if passed a different value from the last trigger() call (even from a previous browser session). * Re-triggers the notification if passed a different value from the last trigger() call (even from a previous browser session).
* Suggested usage: data dependency array * Suggested usage: data dependency array
*/ */
updateKey?: NotificationUpdateKey, updateKey?: NotificationUpdateKey;
/** /**
* @param true (default): Notification initialized with status NotificationStatus.Triggered * @param true (default): Notification initialized with status NotificationStatus.Triggered
* @param false: Notification initialized with status NotificationStatus.Cleared * @param false: Notification initialized with status NotificationStatus.Cleared
*/ */
isNew?: boolean isNew?: boolean;
) => void; }) => void;
lastUpdated: number; lastUpdated: number;
}) => void; }) => void;
/** Callback for notification action (Toast action button click, NotificationsMenu item click, or native push notification interaction) */ /** Callback for notification action (Toast action button click, NotificationsMenu item click, or native push notification interaction) */
useNotificationAction?: () => (id: NotificationIdType) => any; useNotificationAction?: () => (id: _NotificationId) => any;
}; };
export enum NotificationStatus { export enum NotificationStatus {
@@ -65,10 +71,10 @@ export enum NotificationStatus {
/** Notification state. Serialized and cached into localStorage. */ /** Notification state. Serialized and cached into localStorage. */
export type Notification< export type Notification<
NotificationIdType extends NotificationId = string, _NotificationId extends NotificationId = string,
NotificationUpdateKey = any NotificationUpdateKey = any
> = { > = {
id: NotificationIdType; id: _NotificationId;
type: NotificationType; type: NotificationType;
status: NotificationStatus; status: NotificationStatus;
timestamps: Partial<Record<NotificationStatus, number>>; timestamps: Partial<Record<NotificationStatus, number>>;
@@ -79,15 +85,15 @@ export type Notifications = Record<NotificationId, Notification<any>>;
/** Notification display data derived from app state at runtime. */ /** Notification display data derived from app state at runtime. */
export type NotificationDisplayData = { export type NotificationDisplayData = {
icon?: React.ReactNode; icon?: React.ReactElement<any, 'svg'>;
title?: string; title?: string;
description?: React.ReactNode; description?: ReactNode;
customContent?: React.ReactNode; customContent?: ReactNode;
customMenuContent?: React.ReactNode; customMenuContent?: ReactNode;
actionDescription?: string; actionDescription?: string;
+2
View File
@@ -1,3 +1,5 @@
import { getSeparator } from '@/lib/numbers';
export const USD_DECIMALS = 2; export const USD_DECIMALS = 2;
export const SMALL_USD_DECIMALS = 4; export const SMALL_USD_DECIMALS = 4;
-4
View File
@@ -5,10 +5,6 @@ export const tradeTooltips: TooltipStrings = {
title: stringGetter({ key: TOOLTIP_STRING_KEYS.ACCOUNT_LEVERAGE_TITLE }), title: stringGetter({ key: TOOLTIP_STRING_KEYS.ACCOUNT_LEVERAGE_TITLE }),
body: stringGetter({ key: TOOLTIP_STRING_KEYS.ACCOUNT_LEVERAGE_BODY }), 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 }) => ({ 'bracket-sl': ({ stringGetter }) => ({
title: stringGetter({ key: TOOLTIP_STRING_KEYS.BRACKET_ORDER_SL_TITLE }), title: stringGetter({ key: TOOLTIP_STRING_KEYS.BRACKET_ORDER_SL_TITLE }),
body: stringGetter({ key: TOOLTIP_STRING_KEYS.BRACKET_ORDER_SL_BODY }), body: stringGetter({ key: TOOLTIP_STRING_KEYS.BRACKET_ORDER_SL_BODY }),
+1 -1
View File
@@ -27,7 +27,7 @@ export enum PositionSide {
Short = 'SHORT', Short = 'SHORT',
} }
export const UNCOMMITTED_ORDER_TIMEOUT_MS = 10_000; export const UNCOMMITTED_ORDER_TIMEOUT = 10_000;
export const ORDER_SIDE_STRINGS = { export const ORDER_SIDE_STRINGS = {
[OrderSide.BUY]: STRING_KEYS.BUY, [OrderSide.BUY]: STRING_KEYS.BUY,
+10 -14
View File
@@ -1,6 +1,4 @@
import type { ExternalProvider } from '@ethersproject/providers'; 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'; import { STRING_KEYS } from '@/constants/localization';
@@ -315,6 +313,10 @@ export type WalletConnection = {
}; };
// dYdX Chain wallets // 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"; export const COSMOS_DERIVATION_PATH = "m/44'/118'/0'/0/0";
/** /**
@@ -339,12 +341,12 @@ export type EvmAddress = `0x${string}`;
export type DydxAddress = `dydx${string}`; export type DydxAddress = `dydx${string}`;
export const DYDX_CHAIN_INFO: Parameters<typeof suggestChain>[0] = { export const DYDX_CHAIN_INFO: Parameters<typeof suggestChain>[0] = {
rpc: 'https://dydx-testnet-archive.allthatnode.com:26657', rpc: '13.59.4.93:26657',
rest: 'https://dydx-testnet-archive.allthatnode.com:1317', rest: '13.59.4.93:1317',
chainId: 'dydx-testnet-3', chainId: 'dydx-testnet-2',
chainName: 'dYdX Public Testnet', chainName: 'dYdX Public Testnet',
chainSymbolImageUrl: chainSymbolImageUrl:
'https://raw.githubusercontent.com/chainapsis/keplr-chain-registry/main/images/dydx-testnet-3/chain.png', 'https://raw.githubusercontent.com/chainapsis/keplr-chain-registry/main/images/dydx-testnet-2/chain.png',
bech32Config: { bech32Config: {
bech32PrefixAccPub: 'dydxpub', bech32PrefixAccPub: 'dydxpub',
bech32PrefixValPub: 'dydxvaloperpub', bech32PrefixValPub: 'dydxvaloperpub',
@@ -383,18 +385,12 @@ export const DYDX_CHAIN_INFO: Parameters<typeof suggestChain>[0] = {
features: [], features: [],
}; };
// TODO: export this type from abacus instead
export enum DydxChainAsset { export enum DydxChainAsset {
USDC = 'usdc', USDC = 'USDC',
DYDX = 'dydx', DYDX = 'Dv4TNT',
} }
export const DYDX_CHAIN_ASSET_COIN_DENOM: Record<DydxChainAsset, string> = { export const DYDX_CHAIN_ASSET_COIN_DENOM: Record<DydxChainAsset, string> = {
[DydxChainAsset.USDC]: USDC_DENOM, [DydxChainAsset.USDC]: USDC_DENOM,
[DydxChainAsset.DYDX]: DYDX_DENOM, [DydxChainAsset.DYDX]: DYDX_DENOM,
}; };
export const DYDX_CHAIN_ASSET_TAGS: Record<DydxChainAsset, string> = {
[DydxChainAsset.USDC]: 'USDC',
[DydxChainAsset.DYDX]: 'Dv4TNT',
};
+6 -8
View File
@@ -1,12 +1,10 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { shallowEqual, useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useBalance } from 'wagmi'; import { useBalance } from 'wagmi';
import { StargateClient } from '@cosmjs/stargate'; import { StargateClient } from '@cosmjs/stargate';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { formatUnits } from 'viem'; import { formatUnits } from 'viem';
import { USDC_DENOM, DYDX_DENOM } from '@dydxprotocol/v4-client-js';
import { CLIENT_NETWORK_CONFIGS } from '@/constants/networks'; import { CLIENT_NETWORK_CONFIGS } from '@/constants/networks';
import { QUANTUM_MULTIPLIER } from '@/constants/numbers'; import { QUANTUM_MULTIPLIER } from '@/constants/numbers';
import { EvmAddress } from '@/constants/wallets'; import { EvmAddress } from '@/constants/wallets';
@@ -14,10 +12,11 @@ import { EvmAddress } from '@/constants/wallets';
import { convertBech32Address } from '@/lib/addressUtils'; import { convertBech32Address } from '@/lib/addressUtils';
import { MustBigNumber } from '@/lib/numbers'; import { MustBigNumber } from '@/lib/numbers';
import { getBalances } from '@/state/accountSelectors';
import { getSelectedNetwork } from '@/state/appSelectors'; import { getSelectedNetwork } from '@/state/appSelectors';
import { useAccounts } from './useAccounts'; import { useAccounts } from './useAccounts';
import { usePollNativeTokenBalance } from './usePollNativeTokenBalance';
import { usePollUSDCBalance } from './usePollUSDCBalance';
type UseAccountBalanceProps = { type UseAccountBalanceProps = {
// Token Items // Token Items
@@ -51,7 +50,6 @@ export const useAccountBalance = ({
const { evmAddress, dydxAddress } = useAccounts(); const { evmAddress, dydxAddress } = useAccounts();
const selectedNetwork = useSelector(getSelectedNetwork); const selectedNetwork = useSelector(getSelectedNetwork);
const balances = useSelector(getBalances, shallowEqual);
const evmChainId = Number(CLIENT_NETWORK_CONFIGS[selectedNetwork].ethereumChainId); const evmChainId = Number(CLIENT_NETWORK_CONFIGS[selectedNetwork].ethereumChainId);
const evmQuery = useBalance({ const evmQuery = useBalance({
@@ -94,12 +92,12 @@ export const useAccountBalance = ({
const { formatted: evmBalance } = evmQuery.data || {}; const { formatted: evmBalance } = evmQuery.data || {};
const balance = !assetSymbol ? '0' : isCosmosChain ? cosmosQuery.data : evmBalance; const balance = !assetSymbol ? '0' : isCosmosChain ? cosmosQuery.data : evmBalance;
const nativeTokenCoinBalance = balances?.[DYDX_DENOM]; const nativeTokenCoinBalance = usePollNativeTokenBalance({ dydxAddress });
const nativeTokenBalance = MustBigNumber(nativeTokenCoinBalance?.amount) const nativeTokenBalance = MustBigNumber(nativeTokenCoinBalance?.amount)
.div(QUANTUM_MULTIPLIER) .div(QUANTUM_MULTIPLIER)
.toNumber(); .toNumber();
const usdcCoinBalance = balances?.[USDC_DENOM]; const usdcCoinBalance = usePollUSDCBalance({ dydxAddress });
const usdcBalance = MustBigNumber(usdcCoinBalance?.amount).div(QUANTUM_MULTIPLIER).toNumber(); const usdcBalance = MustBigNumber(usdcCoinBalance?.amount).div(QUANTUM_MULTIPLIER).toNumber();
return { return {
+6 -3
View File
@@ -8,7 +8,10 @@ import { OnboardingGuard, OnboardingState, type EvmDerivedAddresses } from '@/co
import { LocalStorageKey, LOCAL_STORAGE_VERSIONS } from '@/constants/localStorage'; import { LocalStorageKey, LOCAL_STORAGE_VERSIONS } from '@/constants/localStorage';
import { DydxAddress, EvmAddress, 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 abacusStateManager from '@/lib/abacus';
import { log } from '@/lib/telemetry'; import { log } from '@/lib/telemetry';
@@ -219,9 +222,9 @@ const useAccountsContext = () => {
// abacus // abacus
// TODO: useAbacus({ dydxAddress }) // TODO: useAbacus({ dydxAddress })
useEffect(() => { useEffect(() => {
if (dydxAddress) abacusStateManager.setAccount(localDydxWallet); if (dydxAddress) abacusStateManager.setAccount(dydxAddress);
else abacusStateManager.attemptDisconnectAccount(); else abacusStateManager.attemptDisconnectAccount();
}, [localDydxWallet]); }, [dydxAddress]);
// clear subaccounts when no dydxAddress is set // clear subaccounts when no dydxAddress is set
useEffect(() => { useEffect(() => {
+1 -1
View File
@@ -4,7 +4,7 @@ import { useMatch, useNavigate } from 'react-router-dom';
import { LocalStorageKey } from '@/constants/localStorage'; import { LocalStorageKey } from '@/constants/localStorage';
import { DEFAULT_MARKETID } from '@/constants/markets'; import { DEFAULT_MARKETID } from '@/constants/markets';
import { AppRoute } from '@/constants/routes'; import { AppRoute, DEFAULT_TRADE_ROUTE } from '@/constants/routes';
import { getSelectedNetwork } from '@/state/appSelectors'; import { getSelectedNetwork } from '@/state/appSelectors';
import { closeDialogInTradeBox } from '@/state/dialogs'; import { closeDialogInTradeBox } from '@/state/dialogs';
+1 -1
View File
@@ -114,7 +114,7 @@ const useDydxClientContext = () => {
}): Promise<Candle[]> => { }): Promise<Candle[]> => {
try { try {
const { candles } = const { candles } =
(await compositeClient?.indexerClient.markets.getPerpetualMarketCandles( (await compositeClient!.indexerClient.markets.getPerpetualMarketCandles(
marketId, marketId,
RESOLUTION_MAP[resolution], RESOLUTION_MAP[resolution],
fromIso, fromIso,
+3 -6
View File
@@ -6,7 +6,7 @@ import { LocalStorageKey } from '@/constants/localStorage';
import { type TransferNotifcation } from '@/constants/notifications'; import { type TransferNotifcation } from '@/constants/notifications';
import { useAccounts } from '@/hooks/useAccounts'; import { useAccounts } from '@/hooks/useAccounts';
import { SQUID_ERROR_TYPES, useSquid } from '@/hooks/useSquid'; import { useSquid } from '@/hooks/useSquid';
import { useLocalStorage } from './useLocalStorage'; import { useLocalStorage } from './useLocalStorage';
const LocalNotificationsContext = createContext< const LocalNotificationsContext = createContext<
@@ -66,14 +66,11 @@ const useLocalNotificationsContext = () => {
} of transferNotifications) { } of transferNotifications) {
try { try {
if (currentStatus && currentStatus?.squidTransactionStatus !== 'ongoing') continue; if (currentStatus && currentStatus?.squidTransactionStatus !== 'ongoing') continue;
const status = await squid?.getStatus({ transactionId: txHash, toChainId, fromChainId }); const status = await squid?.getStatus({ transactionId: txHash, toChainId, fromChainId });
if (status) statuses[txHash] = status; if (status) statuses[txHash] = status;
} catch (error) { } catch (error) {
// ignore not found errors since the route might not be available yet console.error(error);
if (error?.errors?.length && error.errors[0].errorType !== SQUID_ERROR_TYPES.NotFoundError) {
statuses[txHash] = error;
}
} }
} }
return statuses; return statuses;
+1 -1
View File
@@ -29,7 +29,7 @@ export const useMarketsData = (
const markets = useMemo(() => { const markets = useMemo(() => {
return Object.values(allPerpetualMarkets) return Object.values(allPerpetualMarkets)
.filter((market) => allAssets[market?.assetId]) // Filter out markets with no asset information .filter(({ assetId }) => allAssets[assetId]) // Filter out markets with no asset information
.map((marketData) => ({ .map((marketData) => ({
asset: allAssets[marketData.assetId], asset: allAssets[marketData.assetId],
tickSizeDecimals: marketData.configs?.tickSizeDecimals, tickSizeDecimals: marketData.configs?.tickSizeDecimals,
+46 -84
View File
@@ -1,30 +1,27 @@
import { useCallback, 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 { useSelector, shallowEqual, useDispatch } from 'react-redux';
import { groupBy } from 'lodash'; import { groupBy } from 'lodash';
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
import { AlertType } from '@/constants/alerts';
import { AbacusOrderStatus, ORDER_SIDES, ORDER_STATUS_STRINGS } from '@/constants/abacus'; import { AbacusOrderStatus, ORDER_SIDES, ORDER_STATUS_STRINGS } from '@/constants/abacus';
import { DialogTypes } from '@/constants/dialogs'; import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { type NotificationTypeConfig, NotificationType } from '@/constants/notifications'; import { type NotificationTypeConfig, NotificationType } from '@/constants/notifications';
import { ORDER_SIDE_STRINGS, TRADE_TYPE_STRINGS, TradeTypes } from '@/constants/trade'; import { ORDER_SIDE_STRINGS } from '@/constants/trade';
import { useLocalNotifications } from '@/hooks/useLocalNotifications'; import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { AlertMessage } from '@/components/AlertMessage'; import { AssetIcon } from '@/components/AssetIcon';
import { Icon, IconName } from '@/components/Icon'; import { Icon, IconName } from '@/components/Icon';
import { Output, OutputType } from '@/components/Output'; import { Output, OutputType } from '@/components/Output';
import { OrderStatusIcon } from '@/views/OrderStatusIcon';
import { TransferStatusToast } from '@/views/TransferStatus'; import { TransferStatusToast } from '@/views/TransferStatus';
import { TransferStatusSteps } from '@/views/TransferStatusSteps';
import { getSubaccountFills, getSubaccountOrders } from '@/state/accountSelectors'; import { getSubaccountFills, getSubaccountOrders } from '@/state/accountSelectors';
import { openDialog } from '@/state/dialogs'; import { openDialog } from '@/state/dialogs';
import { OrderStatusIcon } from '@/views/OrderStatusIcon';
import { useStringGetter } from './useStringGetter'; import { useStringGetter } from './useStringGetter';
import { TransferStatusSteps } from '@/views/TransferStatusSteps';
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
export const notificationTypes = [ export const notificationTypes = [
{ {
@@ -61,34 +58,33 @@ export const notificationTypes = [
} }
: undefined); : undefined);
if (order) if (order) console.log(order);
trigger( trigger({
order.id, id: order.id,
{ displayData: {
icon: ( icon: <AssetIcon symbol={order.marketId.split('-')[0]} />, //<OrderStatusIcon status={order.status} totalFilled={order.totalFilled ?? 0} />,
<OrderStatusIcon status={order.status} totalFilled={order.totalFilled ?? 0} /> title: `${stringGetter({
), key: order.resources.typeStringKey ?? '',
title: `${stringGetter({ })} ${
key: TRADE_TYPE_STRINGS[order.type.rawValue as TradeTypes].tradeTypeKey, order.status === AbacusOrderStatus.open && (order?.totalFilled ?? 0) > 0
})} ${ ? stringGetter({ key: STRING_KEYS.PARTIALLY_FILLED })
order.status === AbacusOrderStatus.open && (order?.totalFilled ?? 0) > 0 : stringGetter({ key: ORDER_STATUS_STRINGS[order.status.name] })
? stringGetter({ key: STRING_KEYS.PARTIALLY_FILLED }) }`,
: stringGetter({ key: ORDER_STATUS_STRINGS[order.status.name] })
}`, description: `${stringGetter({
description: `${stringGetter({ key: ORDER_SIDE_STRINGS[ORDER_SIDES[order.side.name]],
key: ORDER_SIDE_STRINGS[ORDER_SIDES[order.side.name]], })} ${order.size} ${order.marketId} @ $${order.price}`,
})} ${order.size} ${order.marketId} @ $${order.price}`, actionDescription: 'View Order',
actionDescription: 'View Order', actionAltText: 'View this order in the Orders tab or the Notifications menu.',
actionAltText: 'View this order in the Orders tab or the Notifications menu.', toastSensitivity:
toastSensitivity: order.status === AbacusOrderStatus.pending ? 'foreground' : 'background',
order.status === AbacusOrderStatus.pending ? 'foreground' : 'background', toastDuration: Infinity,
toastDuration: 5000, },
}, updateKey: [order.status.name, order.size],
[order.status.name, order.size], isNew: !order.createdAtMilliseconds || order.createdAtMilliseconds > lastUpdated,
!order.createdAtMilliseconds || order.createdAtMilliseconds > lastUpdated });
);
} }
}, [orderIds]); }, [orderIds, stringGetter]);
}, },
useNotificationAction: () => { useNotificationAction: () => {
@@ -122,36 +118,19 @@ export const notificationTypes = [
const { toChainId, status, txHash, toAmount } = transfer; const { toChainId, status, txHash, toAmount } = transfer;
const finished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing'; const finished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing';
const type = toChainId === TESTNET_CHAIN_ID ? 'deposit' : 'withdraw'; 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 trigger({
const description = ( id: txHash,
<div> displayData: {
<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} />, icon: <Icon iconName={finished ? IconName.Transfer : IconName.Clock} />,
title: stringGetter({ key: getTitleStringKey(type, finished) }), title: stringGetter({ key: getTitleStringKey(type, finished) }),
description: description, // TODO: confirm with design what the description should be
description: (
<>
<span>{type === 'deposit' ? 'Deposit of ' : 'Withdraw of'}</span>
<Output type={OutputType.Fiat} value={toAmount} />
</>
),
customContent: ( customContent: (
<TransferStatusToast <TransferStatusToast
toAmount={transfer.toAmount} toAmount={transfer.toAmount}
@@ -159,30 +138,13 @@ export const notificationTypes = [
status={transfer.status} status={transfer.status}
/> />
), ),
customMenuContent: !finished && ( customMenuContent: !finished && <TransferStatusSteps status={transfer.status} />,
<div>
{description}
<TransferStatusSteps status={transfer.status} />
</div>
),
toastSensitivity: 'foreground', toastSensitivity: 'foreground',
}, },
[] updateKey: [],
); });
} }
}, [transferNotifications]); }, [transferNotifications, stringGetter]);
}, },
}, } as NotificationTypeConfig<string, []>,
] satisfies NotificationTypeConfig[]; ] 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;
`;
+17 -22
View File
@@ -92,7 +92,7 @@ const useNotificationsContext = () => {
for (const { type, useTrigger } of notificationTypes) for (const { type, useTrigger } of notificationTypes)
useTrigger({ useTrigger({
trigger: useCallback( trigger: useCallback(
(id, displayData, updateKey, isNew = true) => { ({ id, displayData, updateKey, isNew = true }) => {
const key = getKey({ type, id }); const key = getKey({ type, id });
const notification = notifications[key]; const notification = notifications[key];
@@ -182,28 +182,23 @@ const useNotificationsContext = () => {
const iconUrl = const iconUrl =
displayData.icon && (await renderSvgToDataUrl(displayData.icon).catch(() => undefined)); displayData.icon && (await renderSvgToDataUrl(displayData.icon).catch(() => undefined));
const pushNotification = new globalThis.Notification(displayData.title, { if (displayData.title) {
renotify: true, const pushNotification = new globalThis.Notification(displayData.title, {
tag: getKey(notification), renotify: true,
data: notification, tag: getKey(notification),
body: displayData.description, data: notification,
icon: iconUrl ?? '/favicon.svg', body: displayData.description ?? '',
badge: iconUrl ?? '/favicon.svg', icon: (iconUrl as string) ?? '/favicon.svg',
image: iconUrl ?? '/favicon.svg', badge: (iconUrl as string) ?? '/favicon.svg',
vibrate: displayData.toastSensitivity === 'foreground', image: (iconUrl as string) ?? '/favicon.svg',
requireInteraction: displayData.toastDuration === Infinity, requireInteraction: displayData.toastDuration === Infinity,
// actions: [ });
// {
// action: displayData.actionDescription,
// title: displayData.actionDescription,
// }
// ].slice(0, globalThis.Notification.maxActions),
});
pushNotification.addEventListener('click', () => { pushNotification.addEventListener('click', () => {
onNotificationAction(notification); onNotificationAction(notification);
markSeen(notification); markSeen(notification);
}); });
}
} }
setPushNotificationsLastUpdated(Date.now()); setPushNotificationsLastUpdated(Date.now());
-26
View File
@@ -1,26 +0,0 @@
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
@@ -0,0 +1,31 @@
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
@@ -0,0 +1,30 @@
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 -5
View File
@@ -1,17 +1,14 @@
import { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
import { Squid } from '@0xsquid/sdk'; import { Squid } from '@0xsquid/sdk';
import { CLIENT_NETWORK_CONFIGS, isDydxV4Network } from '@/constants/networks'; import { CLIENT_NETWORK_CONFIGS, DydxV4Network, isDydxV4Network } from '@/constants/networks';
import { getSelectedNetwork } from '@/state/appSelectors'; import { getSelectedNetwork } from '@/state/appSelectors';
export const NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; export const NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
export enum SQUID_ERROR_TYPES {
NotFoundError = 'NotFoundError',
}
const useSquidContext = () => { const useSquidContext = () => {
const selectedNetwork = useSelector(getSelectedNetwork); const selectedNetwork = useSelector(getSelectedNetwork);
const [_, setInitialized] = useState(false); const [_, setInitialized] = useState(false);
+332 -57
View File
@@ -1,41 +1,53 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { shallowEqual, useSelector, useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import type { Nullable } from '@dydxprotocol/v4-abacus'; import type { Nullable } from '@dydxprotocol/abacus';
import Long from 'long'; import Long from 'long';
import type { IndexedTx } from '@cosmjs/stargate'; import type { IndexedTx } from '@cosmjs/stargate';
import type { EncodeObject, Coin } from '@cosmjs/proto-signing'; import type { EncodeObject, Coin } from '@cosmjs/proto-signing';
import { Method } from '@cosmjs/tendermint-rpc'; import { Method } from '@cosmjs/tendermint-rpc';
import { import {
type LocalWallet, LocalWallet,
OrderExecution,
OrderFlags,
OrderSide,
OrderTimeInForce,
OrderType,
SubaccountClient, SubaccountClient,
DYDX_DENOM, DYDX_DENOM,
USDC_DENOM, USDC_DENOM,
GAS_PRICE_DYDX_DENOM,
} from '@dydxprotocol/v4-client-js'; } from '@dydxprotocol/v4-client-js';
import type { import type {
AccountBalance, HumanReadableCancelOrderPayload,
HumanReadablePlaceOrderPayload, HumanReadablePlaceOrderPayload,
ParsingError,
SubAccountHistoricalPNLs, SubAccountHistoricalPNLs,
} from '@/constants/abacus'; } from '@/constants/abacus';
import { AMOUNT_RESERVED_FOR_GAS_USDC } from '@/constants/account'; import { AMOUNT_RESERVED_FOR_GAS_USDC } from '@/constants/account';
import { AnalyticsEvent } from '@/constants/analytics'; import { AnalyticsEvent } from '@/constants/analytics';
import { ORDER_ERROR_CODE_MAP } from '@/constants/localization';
import { QUANTUM_MULTIPLIER } from '@/constants/numbers'; import { QUANTUM_MULTIPLIER } from '@/constants/numbers';
import { UNCOMMITTED_ORDER_TIMEOUT } from '@/constants/trade';
import { DydxAddress } from '@/constants/wallets'; import { DydxAddress } from '@/constants/wallets';
import { setSubaccount, setHistoricalPnl, removeUncommittedOrderClientId } from '@/state/account'; import {
import { getBalances } from '@/state/accountSelectors'; addUncommittedOrderClientId,
removeUncommittedOrderClientId,
setSubaccount,
setHistoricalPnl,
} from '@/state/account';
import abacusStateManager from '@/lib/abacus'; import abacusStateManager from '@/lib/abacus';
import { track } from '@/lib/analytics'; import { track } from '@/lib/analytics';
import { StatefulOrderError } from '@/lib/errors';
import { MustBigNumber } from '@/lib/numbers'; import { MustBigNumber } from '@/lib/numbers';
import { log } from '@/lib/telemetry'; import { log } from '@/lib/telemetry';
import { useAccounts } from './useAccounts'; import { useAccounts } from './useAccounts';
import { useDydxClient } from './useDydxClient'; import { useDydxClient } from './useDydxClient';
import { usePollUSDCBalance } from './usePollUSDCBalance';
type SubaccountContextType = ReturnType<typeof useSubaccountContext>; type SubaccountContextType = ReturnType<typeof useSubaccountContext>;
const SubaccountContext = createContext<SubaccountContextType>({} as SubaccountContextType); const SubaccountContext = createContext<SubaccountContextType>({} as SubaccountContextType);
@@ -71,8 +83,12 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
const { const {
depositToSubaccount, depositToSubaccount,
withdrawFromSubaccount, withdrawFromSubaccount,
simulateWithdrawFromSubaccount,
transferFromSubaccountToAddress, transferFromSubaccountToAddress,
transferNativeToken, transferNativeToken,
simulateTransferNativeToken,
placeOrderForSubaccount,
cancelOrderForSubaccount,
sendSquidWithdrawFromSubaccount, sendSquidWithdrawFromSubaccount,
} = useMemo( } = useMemo(
() => ({ () => ({
@@ -94,6 +110,31 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
amount: number; amount: number;
}) => await compositeClient?.withdrawFromSubaccount(subaccountClient, amount), }) => 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 ({ transferFromSubaccountToAddress: async ({
subaccountClient, subaccountClient,
assetId = 0, assetId = 0,
@@ -144,6 +185,125 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
Method.BroadcastTxCommit 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 ({ sendSquidWithdrawFromSubaccount: async ({
subaccountClient, subaccountClient,
amount, amount,
@@ -193,10 +353,10 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
// ------ Deposit/Withdraw Methods ------ // // ------ Deposit/Withdraw Methods ------ //
const depositFunds = useCallback( const depositFunds = useCallback(
async (balance: AccountBalance) => { async (balance?: Coin) => {
if (!localDydxWallet) return; 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 .minus(AMOUNT_RESERVED_FOR_GAS_USDC) // keep 0.1 USDC in user's wallet for gas
.toString(); .toString();
@@ -210,14 +370,11 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
[localDydxWallet, depositToSubaccount] [localDydxWallet, depositToSubaccount]
); );
const balances = useSelector(getBalances, shallowEqual); const balance = usePollUSDCBalance({ dydxAddress });
const usdcCoinBalance = balances?.[USDC_DENOM];
useEffect(() => { useEffect(() => {
if (usdcCoinBalance) { depositFunds(balance);
depositFunds(usdcCoinBalance); }, [balance]);
}
}, [usdcCoinBalance]);
const deposit = useCallback( const deposit = useCallback(
async (amount: Long) => { async (amount: Long) => {
@@ -256,6 +413,30 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
[subaccountClient, transferFromSubaccountToAddress, transferNativeToken] [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( const sendSquidWithdraw = useCallback(
async (amount: number, payload: string) => { async (amount: number, payload: string) => {
if (!subaccountClient) { if (!subaccountClient) {
@@ -286,41 +467,101 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
onSuccess, onSuccess,
}: { }: {
isClosePosition?: boolean; isClosePosition?: boolean;
onError?: (onErrorParams?: { errorStringKey?: Nullable<string> }) => void; onError?: (onErrorParams?: { errorStringKey?: string }) => void;
onSuccess?: (placeOrderPayload: Nullable<HumanReadablePlaceOrderPayload>) => void; onSuccess?: () => void;
}) => { }) => {
const callback = ( let orderParams: Nullable<HumanReadablePlaceOrderPayload>;
success: boolean,
parsingError?: Nullable<ParsingError>,
data?: Nullable<HumanReadablePlaceOrderPayload>
) => {
if (success) {
onSuccess?.(data);
} else {
onError?.({ errorStringKey: parsingError?.stringKey });
if (data?.clientId !== undefined) { if (!subaccountClient) return;
dispatch(removeUncommittedOrderClientId(data.clientId));
} try {
orderParams = isClosePosition
? abacusStateManager.closePositionPayload()
: abacusStateManager.placeOrderPayload();
if (!orderParams) {
throw new Error('Missing order params');
} }
};
let placeOrderParams; const {
marketId,
type,
side,
price,
triggerPrice,
size,
clientId,
timeInForce,
goodTilTimeInSeconds,
execution,
postOnly,
reduceOnly,
} = orderParams;
if (isClosePosition) { dispatch(addUncommittedOrderClientId(clientId));
placeOrderParams = abacusStateManager.closePosition(callback);
} else { // Remove uncommitted order after timeout if it hasn't already been removed
placeOrderParams = abacusStateManager.placeOrder(callback); 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 errorCode: number | undefined = error?.code;
const errorStringKey = errorCode ? ORDER_ERROR_CODE_MAP[errorCode] : undefined;
onError?.({ errorStringKey });
log('useSubaccount/placeOrder', error, {
orderParams,
isClosePosition,
});
} }
track(AnalyticsEvent.TradePlaceOrder, {
...placeOrderParams,
isClosePosition,
} as HumanReadablePlaceOrderPayload & { isClosePosition: boolean });
return placeOrderParams;
}, },
[subaccountClient] [subaccountClient, placeOrderForSubaccount]
); );
const closePosition = useCallback( const closePosition = useCallback(
@@ -328,8 +569,8 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
onError, onError,
onSuccess, onSuccess,
}: { }: {
onError: (onErrorParams?: { errorStringKey?: Nullable<string> }) => void; onError: (onErrorParams?: { errorStringKey?: string }) => void;
onSuccess?: (placeOrderPayload: Nullable<HumanReadablePlaceOrderPayload>) => void; onSuccess?: () => void;
}) => await placeOrder({ isClosePosition: true, onError, onSuccess }), }) => await placeOrder({ isClosePosition: true, onError, onSuccess }),
[placeOrder] [placeOrder]
); );
@@ -341,21 +582,53 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
onSuccess, onSuccess,
}: { }: {
orderId: string; orderId: string;
onError?: ({ errorStringKey }?: { errorStringKey?: Nullable<string> }) => void; onError?: ({ errorMsg }?: { errorMsg?: string }) => void;
onSuccess?: () => void; onSuccess?: () => void;
}) => { }) => {
const callback = (success: boolean, parsingError?: Nullable<ParsingError>) => { let cancelOrderParams: Nullable<HumanReadableCancelOrderPayload>;
if (success) {
track(AnalyticsEvent.TradeCancelOrder);
onSuccess?.();
} else {
onError?.({ errorStringKey: parsingError?.stringKey });
}
};
abacusStateManager.cancelOrder(orderId, callback); if (!subaccountClient) return;
try {
cancelOrderParams = abacusStateManager.cancelOrderPayload(orderId);
if (!cancelOrderParams) {
throw new Error('Missing cancel order params');
}
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);
}
}, },
[subaccountClient] [subaccountClient, cancelOrderForSubaccount]
); );
return { return {
@@ -365,7 +638,9 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
requestFaucetFunds, requestFaucetFunds,
// Transfer Methods // Transfer Methods
simulateTransfer,
transfer, transfer,
simulateWithdraw,
sendSquidWithdraw, sendSquidWithdraw,
// Trading Methods // Trading Methods
+1 -1
View File
@@ -1,4 +1,4 @@
const LogoShortIcon: React.FC<{ id?: string }> = ({ id }: { id?: string }) => ( const LogoShortIcon: React.FC<{ id?: string }> = ({ id }) => (
<svg <svg
width="135" width="135"
height="145" height="145"
+2 -1
View File
@@ -7,9 +7,10 @@ import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { AppRoute } from '@/constants/routes'; import { AppRoute } from '@/constants/routes';
import { useStringGetter } from '@/hooks'; import { useStringGetter } from '@/hooks';
import { LogoShortIcon, BellIcon } from '@/icons'; import { LogoShortIcon } from '@/icons';
import { Icon, IconName } from '@/components/Icon'; import { Icon, IconName } from '@/components/Icon';
import { BellIcon } from '@/icons';
import { IconButton } from '@/components/IconButton'; import { IconButton } from '@/components/IconButton';
import { NavigationMenu } from '@/components/NavigationMenu'; import { NavigationMenu } from '@/components/NavigationMenu';
import { VerticalSeparator } from '@/components/Separator'; import { VerticalSeparator } from '@/components/Separator';
+6 -15
View File
@@ -15,20 +15,12 @@ type StyleProps = {
}; };
export const NotificationsToastArea = ({ className }: StyleProps) => { export const NotificationsToastArea = ({ className }: StyleProps) => {
const { const { notifications, getKey, getDisplayData, markUnseen, markSeen, isMenuOpen, onNotificationAction } = useNotifications();
notifications,
getKey,
getDisplayData,
markUnseen,
markSeen,
isMenuOpen,
onNotificationAction,
} = useNotifications();
const { isMobile } = useBreakpoints(); const { isMobile } = useBreakpoints();
return ( return (
<StyledToastArea swipeDirection={isMobile ? 'up' : 'right'} className={className}> <$ToastArea swipeDirection={isMobile ? 'up' : 'right'} className={className}>
{Object.values(notifications) {Object.values(notifications)
// Sort by time of first trigger // Sort by time of first trigger
.sort( .sort(
@@ -61,20 +53,19 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
sensitivity={displayData.toastSensitivity} sensitivity={displayData.toastSensitivity}
setIsOpen={(isOpen, isClosedFromTimeout) => { setIsOpen={(isOpen, isClosedFromTimeout) => {
if (!isOpen) if (!isOpen)
if (isClosedFromTimeout) // Toast timer expired without user interaction
// Toast timer expired without user interaction if (isClosedFromTimeout) markUnseen(notification);
markUnseen(notification);
// Toast interacted with or dismissed // Toast interacted with or dismissed
else markSeen(notification); else markSeen(notification);
}} }}
lastUpdated={notification.timestamps[notification.status]} lastUpdated={notification.timestamps[notification.status]}
/> />
))} ))}
</StyledToastArea> </$ToastArea>
); );
}; };
const StyledToastArea = styled(ToastArea)` const $ToastArea = styled(ToastArea)`
position: absolute; position: absolute;
width: min(16rem, 100%); width: min(16rem, 100%);
inset: 0 0 0 auto; inset: 0 0 0 auto;
-18
View File
@@ -1,18 +0,0 @@
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;
+25 -293
View File
@@ -1,63 +1,28 @@
import Abacus, { type Nullable } from '@dydxprotocol/v4-abacus'; import Abacus, { Nullable } from '@dydxprotocol/abacus';
import Long from 'long'; import Long from 'long';
import type { IndexedTx } from '@cosmjs/stargate';
import { import {
CompositeClient, CompositeClient,
IndexerConfig, IndexerConfig,
type LocalWallet,
Network, Network,
NetworkOptimizer, NetworkOptimizer,
SubaccountClient,
ValidatorConfig, ValidatorConfig,
OrderType,
OrderSide,
OrderTimeInForce,
OrderExecution,
DYDX_DENOM,
GAS_PRICE_DYDX_DENOM,
} from '@dydxprotocol/v4-client-js'; } from '@dydxprotocol/v4-client-js';
import { import {
type AbacusDYDXChainTransactionsProtocol, type AbacusDYDXChainTransactionsProtocol,
QueryType, QueryType,
type QueryTypes, type QueryTypes,
TransactionType,
type TransactionTypes, type TransactionTypes,
type HumanReadablePlaceOrderPayload,
type HumanReadableCancelOrderPayload,
type HumanReadableWithdrawPayload,
type HumanReadableTransferPayload,
} from '@/constants/abacus'; } 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'; import { log } from '../telemetry';
class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol { class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
private compositeClient: CompositeClient | undefined; private compositeClient: CompositeClient | undefined;
private store: RootStore | undefined;
private localWallet: LocalWallet | undefined;
constructor() { constructor() {
this.compositeClient = undefined; this.compositeClient = undefined;
this.store = undefined;
}
setStore(store: RootStore): void {
this.store = store;
}
setLocalWallet(localWallet: LocalWallet) {
this.localWallet = localWallet;
} }
async connectNetwork( async connectNetwork(
@@ -68,41 +33,30 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
faucetUrl: Nullable<string> | undefined, faucetUrl: Nullable<string> | undefined,
callback: (p0: Nullable<string>) => void callback: (p0: Nullable<string>) => void
): Promise<void> { ): Promise<void> {
try { this.compositeClient = await CompositeClient.connect(
const compositeClient = await CompositeClient.connect( new Network(
new Network( chainId,
chainId, new IndexerConfig(indexerUrl, indexerSocketUrl),
new IndexerConfig(indexerUrl, indexerSocketUrl), new ValidatorConfig(validatorUrl, chainId, {
new ValidatorConfig(validatorUrl, chainId, { broadcastPollIntervalMs: 3_000,
broadcastPollIntervalMs: 3_000, broadcastTimeoutMs: 60_000,
broadcastTimeoutMs: 60_000, })
}) )
) );
);
this.compositeClient = compositeClient; // 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,
},
});
// Dispatch custom event to notify other parts of the app that the network has been connected globalThis.dispatchEvent(customEvent);
const customEvent = new CustomEvent('abacus:connectNetwork', { callback(JSON.stringify({ success: true }));
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 { parseToPrimitives<T>(x: T): T {
@@ -118,10 +72,6 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
return x.toString() as T; return x.toString() as T;
} }
if (x instanceof Uint8Array) {
return bytesToBigInt(x).toString() as T;
}
if (typeof x === 'object') { if (typeof x === 'object') {
const parsedObj: { [key: string]: any } = {}; const parsedObj: { [key: string]: any } = {};
for (const key in x) { for (const key in x) {
@@ -135,218 +85,14 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
throw new Error(`Unsupported data type: ${typeof x}`); 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( async transaction(
type: TransactionTypes, type: TransactionTypes,
paramsInJson: Abacus.Nullable<string>, paramsInJson: Abacus.Nullable<string>,
callback: (p0: Abacus.Nullable<string>) => void callback: (p0: Abacus.Nullable<string>) => void
): Promise<void> { ): Promise<void> {
try { // To be implemented
const params = paramsInJson ? JSON.parse(paramsInJson) : undefined; return;
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( async get(
@@ -370,12 +116,6 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
); );
callback(JSON.stringify({ url: optimalNode })); callback(JSON.stringify({ url: optimalNode }));
break; 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: case QueryType.FeeTiers:
const feeTiers = await this.compositeClient?.validatorClient.get.getFeeTiers(); const feeTiers = await this.compositeClient?.validatorClient.get.getFeeTiers();
const parsedFeeTiers = this.parseToPrimitives(feeTiers); const parsedFeeTiers = this.parseToPrimitives(feeTiers);
@@ -395,14 +135,6 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
const parsedUserStats = this.parseToPrimitives(userStats); const parsedUserStats = this.parseToPrimitives(userStats);
callback(JSON.stringify(parsedUserStats)); callback(JSON.stringify(parsedUserStats));
break; 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;
// Do not implement Transfers (yet) // Do not implement Transfers (yet)
case QueryType.Transfers: case QueryType.Transfers:
default: default:
+14 -43
View File
@@ -1,5 +1,3 @@
import type { LocalWallet } from '@dydxprotocol/v4-client-js';
import type { import type {
ClosePositionInputFields, ClosePositionInputFields,
Nullable, Nullable,
@@ -8,7 +6,6 @@ import type {
TradeInputFields, TradeInputFields,
TransferInputFields, TransferInputFields,
HistoricalPnlPeriods, HistoricalPnlPeriods,
ParsingError,
} from '@/constants/abacus'; } from '@/constants/abacus';
import { import {
@@ -33,13 +30,13 @@ import { getInputTradeOptions } from '@/state/inputsSelectors';
import { getTransferInputs } from '@/state/inputsSelectors'; import { getTransferInputs } from '@/state/inputsSelectors';
import AbacusRest from './rest'; import AbacusRest from './rest';
import AbacusAnalytics from './analytics';
import AbacusWebsocket from './websocket'; import AbacusWebsocket from './websocket';
import AbacusChainTransaction from './dydxChainTransactions'; import AbacusChainTransaction from './dydxChainTransactions';
import AbacusStateNotifier from './stateNotification'; import AbacusStateNotifier from './stateNotification';
import AbacusLocalizer from './localizer'; import AbacusLocalizer from './localizer';
import AbacusFormatter from './formatter'; import AbacusFormatter from './formatter';
import AbacusThreading from './threading'; import AbacusThreading from './threading';
import AbacusTracking from './tracking';
import AbacusFileSystem, { ENDPOINTS_PATH } from './filesystem'; import AbacusFileSystem, { ENDPOINTS_PATH } from './filesystem';
import { LocaleSeparators } from '../numbers'; import { LocaleSeparators } from '../numbers';
class AbacusStateManager { class AbacusStateManager {
@@ -49,25 +46,21 @@ class AbacusStateManager {
stateManager: InstanceType<typeof AsyncAbacusStateManager>; stateManager: InstanceType<typeof AsyncAbacusStateManager>;
websocket: AbacusWebsocket; websocket: AbacusWebsocket;
stateNotifier: AbacusStateNotifier; stateNotifier: AbacusStateNotifier;
analytics: AbacusAnalytics;
abacusFormatter: AbacusFormatter; abacusFormatter: AbacusFormatter;
chainTransactions: AbacusChainTransaction;
constructor() { constructor() {
this.store = undefined; this.store = undefined;
this.currentMarket = undefined; this.currentMarket = undefined;
this.stateNotifier = new AbacusStateNotifier(); this.stateNotifier = new AbacusStateNotifier();
this.analytics = new AbacusAnalytics();
this.websocket = new AbacusWebsocket(); this.websocket = new AbacusWebsocket();
this.abacusFormatter = new AbacusFormatter(); this.abacusFormatter = new AbacusFormatter();
this.chainTransactions = new AbacusChainTransaction();
const ioImplementations = new IOImplementations( const ioImplementations = new IOImplementations(
// @ts-ignore // @ts-ignore
new AbacusRest(), new AbacusRest(),
this.websocket, this.websocket,
this.chainTransactions, new AbacusChainTransaction(),
this.analytics, new AbacusTracking(),
new AbacusThreading(), new AbacusThreading(),
new CoroutineTimer(), new CoroutineTimer(),
new AbacusFileSystem() new AbacusFileSystem()
@@ -163,14 +156,10 @@ class AbacusStateManager {
setStore = (store: RootStore) => { setStore = (store: RootStore) => {
this.store = store; this.store = store;
this.stateNotifier.setStore(store); this.stateNotifier.setStore(store);
this.chainTransactions.setStore(store);
}; };
setAccount = (localWallet?: LocalWallet) => { setAccount = (walletAddress: string) => {
if (localWallet) { this.stateManager.accountAddress = walletAddress;
this.stateManager.accountAddress = localWallet.address;
this.chainTransactions.setLocalWallet(localWallet);
}
}; };
setTransfersSourceAddress = (evmAddress: string) => { setTransfersSourceAddress = (evmAddress: string) => {
@@ -228,34 +217,16 @@ class AbacusStateManager {
this.stateManager.transferStatus(hash, fromChainId, toChainId); 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 ------ // // ------ 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> => getHistoricalPnlPeriod = (): Nullable<HistoricalPnlPeriods> =>
this.stateManager.historicalPnlPeriod; this.stateManager.historicalPnlPeriod;
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Nullable, kollections } from '@dydxprotocol/v4-abacus'; import type { Nullable, kollections } from '@dydxprotocol/abacus';
import type { AbacusRestProtocol } from '@/constants/abacus'; import type { AbacusRestProtocol } from '@/constants/abacus';
+2 -17
View File
@@ -1,7 +1,6 @@
import { kollections } from '@dydxprotocol/v4-abacus'; import { kollections } from '@dydxprotocol/abacus';
import type { import type {
AccountBalance,
AbacusApiState, AbacusApiState,
AbacusNotification, AbacusNotification,
AbacusStateNotificationProtocol, AbacusStateNotificationProtocol,
@@ -19,11 +18,9 @@ import { Changes } from '@/constants/abacus';
import type { RootStore } from '@/state/_store'; import type { RootStore } from '@/state/_store';
import { import {
setBalances,
setFills, setFills,
setFundingPayments, setFundingPayments,
setHistoricalPnl, setHistoricalPnl,
setLatestOrder,
setSubaccount, setSubaccount,
setTransfers, setTransfers,
setWallet, setWallet,
@@ -74,16 +71,6 @@ 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)) { if (changes.has(Changes.configs)) {
dispatch(setConfigs(updatedState.configs)); dispatch(setConfigs(updatedState.configs));
} }
@@ -168,9 +155,7 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol {
} }
} }
lastOrderChanged(order: SubaccountOrder) { lastOrderChanged(order: SubaccountOrder) {}
this.store?.dispatch(setLatestOrder(order));
}
errorsEmitted(errors: ParsingErrors) { errorsEmitted(errors: ParsingErrors) {
console.error('parse errors', errors.toArray()); console.error('parse errors', errors.toArray());
+9
View File
@@ -0,0 +1,9 @@
import type { AbacusTrackingProtocol, Nullable } from '@/constants/abacus';
class AbacusTracking implements AbacusTrackingProtocol {
log(event: string, data: Nullable<string>) {
console.log({ event, data });
}
}
export default AbacusTracking;
-8
View File
@@ -5,15 +5,10 @@ import type {
AnalyticsEventData, AnalyticsEventData,
} from '@/constants/analytics'; } from '@/constants/analytics';
const DEBUG_ANALYTICS = false;
export const identify = <T extends AnalyticsUserProperty>( export const identify = <T extends AnalyticsUserProperty>(
property: T, property: T,
propertyValue: AnalyticsUserPropertyValue<T> propertyValue: AnalyticsUserPropertyValue<T>
) => { ) => {
if (DEBUG_ANALYTICS) {
console.log(`[Analytics:Identify] ${property}`, propertyValue);
}
const customEvent = new CustomEvent('dydx:identify', { const customEvent = new CustomEvent('dydx:identify', {
detail: { property, propertyValue }, detail: { property, propertyValue },
}); });
@@ -25,9 +20,6 @@ export const track = <T extends AnalyticsEvent>(
eventType: T, eventType: T,
eventData?: AnalyticsEventData<T> eventData?: AnalyticsEventData<T>
) => { ) => {
if (DEBUG_ANALYTICS) {
console.log(`[Analytics] ${eventType}`, eventData);
}
const customEvent = new CustomEvent('dydx:track', { const customEvent = new CustomEvent('dydx:track', {
detail: { eventType, eventData }, detail: { eventType, eventData },
}); });
+1 -15
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 * @description Returns null if input is 0 or null, '99+' if input is greater than 99, otherwise original input number
*/ */
export const shortenNumberForDisplay = (num?: number) => export const shorternNumberForDisplay = (num?: number) =>
MustBigNumber(num).eq(0) ? null : MustBigNumber(num).gt(99) ? '99+' : num; MustBigNumber(num).eq(0) ? null : MustBigNumber(num).gt(99) ? '99+' : num;
/** /**
@@ -66,17 +66,3 @@ export const getSeparator = ({
Intl.NumberFormat(browserLanguage) Intl.NumberFormat(browserLanguage)
.formatToParts(1000.1) .formatToParts(1000.1)
.find?.((part) => part.type === separatorType)?.value; .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,12 +58,6 @@ export const getStatusIconInfo = ({
statusIconColor: `var(--color-negative)`, statusIconColor: `var(--color-negative)`,
}; };
} }
case AbacusOrderStatus.untriggered: {
return {
statusIcon: IconName.OrderUntriggered,
statusIconColor: `var(--color-text-2)`,
};
}
case AbacusOrderStatus.pending: case AbacusOrderStatus.pending:
default: { default: {
return { return {
+4 -2
View File
@@ -21,8 +21,10 @@ const applyComputedStyles = (html: string) => {
const toDataUrl = (bytes: string, type = 'image/svg+xml') => const toDataUrl = (bytes: string, type = 'image/svg+xml') =>
new Promise<string | ArrayBuffer | null>((resolve, reject) => { new Promise<string | ArrayBuffer | null>((resolve, reject) => {
Object.assign(new FileReader(), { Object.assign(new FileReader(), {
onload: (e) => resolve(e.target.result), onload: (e: {
onerror: (e) => reject(e.target.error), target: { result: string | ArrayBuffer | PromiseLike<string | ArrayBuffer | null> | null };
}) => resolve(e.target.result),
onerror: (e: { target: { error: Error } }) => reject(e.target.error),
}).readAsDataURL(new File([bytes], '', { type })); }).readAsDataURL(new File([bytes], '', { type }));
}); });
+3 -11
View File
@@ -1,7 +1,5 @@
export const log = (location: string, error: Error, metadata?: any) => { export const log = (location: string, error: Error, metadata?: any) => {
if (import.meta.env.MODE !== 'production') { // console.warn(error);
console.warn('telemetry/log:', { location, error, metadata });
}
const customEvent = new CustomEvent('dydx:log', { const customEvent = new CustomEvent('dydx:log', {
detail: { detail: {
@@ -18,10 +16,7 @@ export const log = (location: string, error: Error, metadata?: any) => {
globalThis.addEventListener('unhandledrejection', (event) => { globalThis.addEventListener('unhandledrejection', (event) => {
event.preventDefault(); event.preventDefault();
log( log('window/onunhandledrejection', new Error(`Promise rejected and uncaught; reason: ${event.reason}`));
'window/onunhandledrejection',
new Error(`Promise rejected and uncaught; reason: ${event.reason}`)
);
}); });
// Log rejected Promises that are caught late (handler attached at a later time) // Log rejected Promises that are caught late (handler attached at a later time)
@@ -30,10 +25,7 @@ globalThis.addEventListener(
(event) => { (event) => {
event.preventDefault(); event.preventDefault();
log( log('window/onrejectionhandled', new Error(`Promise rejected and caught late; reason: ${event.reason}`));
'window/onrejectionhandled',
new Error(`Promise rejected and caught late; reason: ${event.reason}`)
);
}, },
false false
); );
+3 -3
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { Nullable } from '@dydxprotocol/v4-abacus'; import { Nullable } from '@dydxprotocol/abacus';
import styled, { AnyStyledComponent } from 'styled-components'; import styled, { AnyStyledComponent } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux'; import { shallowEqual, useSelector } from 'react-redux';
@@ -270,11 +270,11 @@ Styled.FeeTable = styled(Table)`
--table-cell-align: end; --table-cell-align: end;
font: var(--font-base-book); font: var(--font-base-book);
tbody tr { tbody tr {
&[data-yours='true'] { &[data-yours='true'] {
background-color: var(--color-layer-3); background-color: var(--color-layer-3);
td:first-child { td:first-child {
border-left: 0.125rem solid var(--color-accent); border-left: 0.125rem solid var(--color-accent);
} }
+10 -13
View File
@@ -2,7 +2,6 @@ import { useState } from 'react';
import { shallowEqual, useSelector } from 'react-redux'; import { shallowEqual, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent } from 'styled-components'; import styled, { type AnyStyledComponent } from 'styled-components';
import { AbacusOrderStatus } from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { useBreakpoints, useStringGetter } from '@/hooks'; import { useBreakpoints, useStringGetter } from '@/hooks';
@@ -19,23 +18,20 @@ import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable';
import { OrdersTable, OrdersTableColumnKey } from '@/views/tables/OrdersTable'; import { OrdersTable, OrdersTableColumnKey } from '@/views/tables/OrdersTable';
import { PositionsTable, PositionsTableColumnKey } from '@/views/tables/PositionsTable'; import { PositionsTable, PositionsTableColumnKey } from '@/views/tables/PositionsTable';
import { import { calculateIsAccountViewOnly } from '@/state/accountCalculators';
calculateHasUncommittedOrders,
calculateIsAccountViewOnly,
} from '@/state/accountCalculators';
import { import {
calculateHasUncommittedOrders,
getCurrentMarketTradeInfoNumbers, getCurrentMarketTradeInfoNumbers,
getHasUnseenFillUpdates, getHasUnseenFillUpdates,
getHasUnseenOrderUpdates, getHasUnseenOrderUpdates,
getLatestOrderStatus,
getTradeInfoNumbers, getTradeInfoNumbers,
} from '@/state/accountSelectors'; } from '@/state/accountSelectors';
import { getCurrentMarketAssetId, getCurrentMarketId } from '@/state/perpetualsSelectors'; import { getCurrentMarketAssetId, getCurrentMarketId } from '@/state/perpetualsSelectors';
import { isTruthy } from '@/lib/isTruthy'; import { isTruthy } from '@/lib/isTruthy';
import { shortenNumberForDisplay } from '@/lib/numbers'; import { shorternNumberForDisplay } from '@/lib/numbers';
import { PositionInfo } from '@/views/PositionInfo'; import { PositionInfo } from '@/views/PositionInfo';
@@ -73,12 +69,13 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => {
const hasUnseenOrderUpdates = useSelector(getHasUnseenOrderUpdates); const hasUnseenOrderUpdates = useSelector(getHasUnseenOrderUpdates);
const hasUnseenFillUpdates = useSelector(getHasUnseenFillUpdates); const hasUnseenFillUpdates = useSelector(getHasUnseenFillUpdates);
const isAccountViewOnly = useSelector(calculateIsAccountViewOnly); const isAccountViewOnly = useSelector(calculateIsAccountViewOnly);
const isWaitingForOrderToIndex = useSelector(calculateHasUncommittedOrders); const hasUncommittedOrders = useSelector(calculateHasUncommittedOrders);
const showCurrentMarket = isTablet || view === PanelView.CurrentMarket; const showCurrentMarket = isTablet || view === PanelView.CurrentMarket;
const fillsTagNumber = shortenNumberForDisplay(showCurrentMarket ? numFills : numTotalFills); const fillsTagNumber = shorternNumberForDisplay(showCurrentMarket ? numFills : numTotalFills);
const ordersTagNumber = shortenNumberForDisplay( const ordersTagNumber = shorternNumberForDisplay(
showCurrentMarket ? numOpenOrders : numTotalOpenOrders showCurrentMarket ? numOpenOrders : numTotalOpenOrders
); );
@@ -89,7 +86,7 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => {
key: showCurrentMarket ? STRING_KEYS.POSITION : STRING_KEYS.POSITIONS, key: showCurrentMarket ? STRING_KEYS.POSITION : STRING_KEYS.POSITIONS,
}), }),
tag: showCurrentMarket ? null : shortenNumberForDisplay(numTotalPositions), tag: showCurrentMarket ? null : shorternNumberForDisplay(numTotalPositions),
content: showCurrentMarket ? ( content: showCurrentMarket ? (
<PositionInfo showNarrowVariation={isTablet} /> <PositionInfo showNarrowVariation={isTablet} />
@@ -121,7 +118,7 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => {
value: InfoSection.Orders, value: InfoSection.Orders,
label: stringGetter({ key: STRING_KEYS.ORDERS }), label: stringGetter({ key: STRING_KEYS.ORDERS }),
slotRight: isWaitingForOrderToIndex ? ( slotRight: hasUncommittedOrders ? (
<Styled.LoadingSpinner /> <Styled.LoadingSpinner />
) : ( ) : (
ordersTagNumber && ( ordersTagNumber && (
@@ -193,7 +190,7 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => {
// value: InfoSection.Payments, // value: InfoSection.Payments,
// label: stringGetter({ key: STRING_KEYS.PAYMENTS }), // label: stringGetter({ key: STRING_KEYS.PAYMENTS }),
// tag: shortenNumberForDisplay( // tag: shorternNumberForDisplay(
// showCurrentMarket ? numFundingPayments : numTotalFundingPayments // showCurrentMarket ? numFundingPayments : numTotalFundingPayments
// ), // ),
// content: ( // content: (
+4 -1
View File
@@ -1,9 +1,9 @@
import { configureStore } from '@reduxjs/toolkit'; import { configureStore } from '@reduxjs/toolkit';
import abacusStateManager from '@/lib/abacus';
import appMiddleware from './appMiddleware'; import appMiddleware from './appMiddleware';
import localizationMiddleware from './localizationMiddleware'; import localizationMiddleware from './localizationMiddleware';
import routerMiddleware from './routerMiddleware'; import routerMiddleware from './routerMiddleware';
import tradeMiddleware from './tradeMiddleware';
import { accountSlice } from './account'; import { accountSlice } from './account';
import { appSlice } from './app'; import { appSlice } from './app';
@@ -16,6 +16,8 @@ import { localizationSlice } from './localization';
import { navigationSlice } from './navigation'; import { navigationSlice } from './navigation';
import { perpetualsSlice } from './perpetuals'; import { perpetualsSlice } from './perpetuals';
import abacusStateManager from '@/lib/abacus';
export const commandMenuSlices = [layoutSlice, localizationSlice]; export const commandMenuSlices = [layoutSlice, localizationSlice];
export const store = configureStore({ export const store = configureStore({
@@ -39,6 +41,7 @@ export const store = configureStore({
appMiddleware, appMiddleware,
localizationMiddleware, localizationMiddleware,
routerMiddleware, routerMiddleware,
tradeMiddleware,
], ],
devTools: process.env.NODE_ENV !== 'production', devTools: process.env.NODE_ENV !== 'production',
+15 -31
View File
@@ -1,7 +1,7 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import _ from 'lodash';
import type { import type {
AccountBalance,
SubaccountFill, SubaccountFill,
Nullable, Nullable,
Subaccount, Subaccount,
@@ -25,18 +25,16 @@ export type AccountState = {
fundingPayments?: SubaccountFundingPayments; fundingPayments?: SubaccountFundingPayments;
transfers?: SubaccountTransfers; transfers?: SubaccountTransfers;
clearedOrderIds?: string[]; clearedOrderIds?: string[];
uncommittedOrderClientIds?: number[];
hasUnseenFillUpdates: boolean; hasUnseenFillUpdates: boolean;
hasUnseenOrderUpdates: boolean; hasUnseenOrderUpdates: boolean;
historicalPnl?: SubAccountHistoricalPNLs; historicalPnl?: SubAccountHistoricalPNLs;
latestOrder?: Nullable<SubaccountOrder>;
onboardingGuards: Record<OnboardingGuard, boolean | undefined>; onboardingGuards: Record<OnboardingGuard, boolean | undefined>;
onboardingState: OnboardingState; onboardingState: OnboardingState;
subaccount?: Nullable<Subaccount>; subaccount?: Nullable<Subaccount>;
uncommittedOrderClientIds: number[];
wallet?: Nullable<Wallet>; wallet?: Nullable<Wallet>;
walletType?: WalletType; walletType?: WalletType;
historicalPnlPeriod?: HistoricalPnlPeriods; historicalPnlPeriod?: HistoricalPnlPeriods;
balances?: Record<string, AccountBalance>;
}; };
const initialState: AccountState = { const initialState: AccountState = {
@@ -44,10 +42,10 @@ const initialState: AccountState = {
fundingPayments: undefined, fundingPayments: undefined,
transfers: undefined, transfers: undefined,
clearedOrderIds: undefined, clearedOrderIds: undefined,
uncommittedOrderClientIds: undefined,
hasUnseenFillUpdates: false, hasUnseenFillUpdates: false,
hasUnseenOrderUpdates: false, hasUnseenOrderUpdates: false,
historicalPnl: undefined, historicalPnl: undefined,
latestOrder: undefined,
onboardingGuards: { onboardingGuards: {
[OnboardingGuard.hasAcknowledgedTerms]: Boolean( [OnboardingGuard.hasAcknowledgedTerms]: Boolean(
getLocalStorage<boolean>({ getLocalStorage<boolean>({
@@ -59,7 +57,6 @@ const initialState: AccountState = {
}, },
onboardingState: OnboardingState.Disconnected, onboardingState: OnboardingState.Disconnected,
subaccount: undefined, subaccount: undefined,
uncommittedOrderClientIds: [],
wallet: undefined, wallet: undefined,
walletType: getLocalStorage<WalletType>({ walletType: getLocalStorage<WalletType>({
key: LocalStorageKey.OnboardingSelectedWalletType, key: LocalStorageKey.OnboardingSelectedWalletType,
@@ -90,16 +87,6 @@ export const accountSlice = createSlice({
setTransfers: (state, action: PayloadAction<any>) => { setTransfers: (state, action: PayloadAction<any>) => {
state.transfers = action.payload; 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>) => ({ clearOrder: (state, action: PayloadAction<string>) => ({
...state, ...state,
clearedOrderIds: [...(state.clearedOrderIds || []), action.payload], clearedOrderIds: [...(state.clearedOrderIds || []), action.payload],
@@ -145,23 +132,22 @@ export const accountSlice = createSlice({
...state, ...state,
wallet: action.payload, 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) => { viewedFills: (state) => {
state.hasUnseenFillUpdates = false; state.hasUnseenFillUpdates = false;
}, },
viewedOrders: (state) => { viewedOrders: (state) => {
state.hasUnseenOrderUpdates = false; 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
);
},
}, },
}); });
@@ -169,16 +155,14 @@ export const {
setFills, setFills,
setFundingPayments, setFundingPayments,
setTransfers, setTransfers,
setLatestOrder,
clearOrder, clearOrder,
setOnboardingGuard, setOnboardingGuard,
setOnboardingState, setOnboardingState,
setHistoricalPnl, setHistoricalPnl,
setSubaccount, setSubaccount,
setWallet, setWallet,
viewedFills,
viewedOrders,
setBalances,
addUncommittedOrderClientId, addUncommittedOrderClientId,
removeUncommittedOrderClientId, removeUncommittedOrderClientId,
viewedFills,
viewedOrders,
} = accountSlice.actions; } = accountSlice.actions;
-6
View File
@@ -9,7 +9,6 @@ import {
getOnboardingGuards, getOnboardingGuards,
getOnboardingState, getOnboardingState,
getSubaccountId, getSubaccountId,
getUncommittedOrderClientIds,
} from '@/state/accountSelectors'; } from '@/state/accountSelectors';
import { getSelectedNetwork } from '@/state/appSelectors'; import { getSelectedNetwork } from '@/state/appSelectors';
@@ -70,11 +69,6 @@ export const calculateHasOpenPositions = createSelector(
(openPositions?: SubaccountPosition[]) => (openPositions?.length || 0) > 0 (openPositions?: SubaccountPosition[]) => (openPositions?.length || 0) > 0
); );
export const calculateHasUncommittedOrders = createSelector(
[getUncommittedOrderClientIds],
(uncommittedOrderClientIds: number[]) => uncommittedOrderClientIds.length > 0
);
/** /**
* @description calculate whether the client is loading info. * @description calculate whether the client is loading info.
* (account is connected but subaccountId is till missing) * (account is connected but subaccountId is till missing)
+24 -42
View File
@@ -12,9 +12,10 @@ import {
import { OnboardingState } from '@/constants/account'; import { OnboardingState } from '@/constants/account';
import { getHydratedTradingData } from '@/lib/orders';
import type { RootState } from './_store'; import type { RootState } from './_store';
import { getHydratedTradingData } from '@/lib/orders';
import { getCurrentMarketId, getPerpetualMarkets } from './perpetualsSelectors'; import { getCurrentMarketId, getPerpetualMarkets } from './perpetualsSelectors';
import { getAssets } from './assetsSelectors'; import { getAssets } from './assetsSelectors';
@@ -78,12 +79,6 @@ export const getCurrentMarketPositionData = (state: RootState) => {
export const getSubaccountOrders = (state: RootState) => export const getSubaccountOrders = (state: RootState) =>
state.account.subaccount?.orders?.toArray(); 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 * @param state
* @returns list of order ids that user has cleared and should be hidden * @returns list of order ids that user has cleared and should be hidden
@@ -111,46 +106,25 @@ export const getSubaccountOpenOrdersBySideAndPrice = createSelector(
[getSubaccountOpenOrders], [getSubaccountOpenOrders],
(openOrders = []) => { (openOrders = []) => {
const ordersBySide: Partial<Record<OrderSide, Record<number, SubaccountOrder>>> = {}; const ordersBySide: Partial<Record<OrderSide, Record<number, SubaccountOrder>>> = {};
openOrders.forEach((order) => { for (const order of openOrders) {
const side = ORDER_SIDES[order.side.name]; const side = ORDER_SIDES[order.side.name];
const byPrice = (ordersBySide[side] ??= {}); const byPrice = (ordersBySide[side] ??= {});
byPrice[order.price] = order; byPrice[order.price] = order;
}); }
return ordersBySide; return ordersBySide;
} }
); );
/** /**
* @returns the clientId of the latest order * @param state
*/
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 * @returns order details with the given orderId
*/ */
export const getOrderDetails = (orderId: string) => export const getOrderDetails = (orderId: string) =>
createSelector( createSelector(
[getSubaccountOrders, getAssets, getPerpetualMarkets], [getSubaccountOrders, getAssets, getPerpetualMarkets],
(orders, assets, perpetualMarkets) => { (orders, assets, perpetualMarkets) => {
const matchingOrder = orders?.find((order) => order.id === orderId); const order = orders?.find((order) => order.id === orderId);
return matchingOrder return order ? getHydratedTradingData({ data: order, assets, perpetualMarkets }) : undefined;
? getHydratedTradingData({ data: matchingOrder, assets, perpetualMarkets })
: undefined;
} }
); );
@@ -204,10 +178,8 @@ export const getFillDetails = (fillId: string) =>
createSelector( createSelector(
[getSubaccountFills, getAssets, getPerpetualMarkets], [getSubaccountFills, getAssets, getPerpetualMarkets],
(fills, assets, perpetualMarkets) => { (fills, assets, perpetualMarkets) => {
const matchingFill = fills?.find((fill) => fill.id === fillId); const fill = fills?.find((fill) => fill.id === fillId);
return matchingFill return fill ? getHydratedTradingData({ data: fill, assets, perpetualMarkets }) : undefined;
? getHydratedTradingData({ data: matchingFill, assets, perpetualMarkets })
: undefined;
} }
); );
@@ -308,6 +280,21 @@ export const getIsAccountConnected = (state: RootState) =>
*/ */
export const getOnboardingGuards = (state: RootState) => state.account.onboardingGuards; 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 * @param state
* @returns Whether there are unseen fill updates * @returns Whether there are unseen fill updates
@@ -332,8 +319,3 @@ export const getUserStats = (state: RootState) => ({
makerVolume30D: state.account?.wallet?.user?.makerVolume30D, makerVolume30D: state.account?.wallet?.user?.makerVolume30D,
takerVolume30D: state.account?.wallet?.user?.takerVolume30D, takerVolume30D: state.account?.wallet?.user?.takerVolume30D,
}); });
/**
* @returns user wallet balances
*/
export const getBalances = (state: RootState) => state.account?.balances;
+6 -2
View File
@@ -47,5 +47,9 @@ export const appSlice = createSlice({
}, },
}); });
export const { initializeLocalization, initializeWebsocket, setApiState, setSelectedNetwork } = export const {
appSlice.actions; initializeLocalization,
initializeWebsocket,
setApiState,
setSelectedNetwork,
} = appSlice.actions;
+1 -1
View File
@@ -1,5 +1,5 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { kollections } from '@dydxprotocol/v4-abacus'; import type { kollections } from '@dydxprotocol/abacus';
import type { Configs, FeeDiscount, FeeTier, NetworkConfigs, Nullable } from '@/constants/abacus'; import type { Configs, FeeDiscount, FeeTier, NetworkConfigs, Nullable } from '@/constants/abacus';
import { LocalStorageKey } from '@/constants/localStorage'; 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 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 }; params?: { [key: string]: string | React.ReactNode };
}): string | Array<string | React.ReactNode> => { }): string | Array<string | React.ReactNode> => {
// Fallback to english whenever a key doesn't exist for other languages // Fallback to english whenever a key doesn't exist for other languages
const formattedString: string = _.get(localeData, key) || _.get(EN_LOCALE_DATA, key) || ''; let formattedString: string = _.get(localeData, key) || _.get(EN_LOCALE_DATA, key) || '';
return formatString(formattedString, params); return formatString(formattedString, params);
}; };
+3 -2
View File
@@ -1,10 +1,11 @@
import { matchPath } from 'react-router-dom';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { Candle, TradingViewBar } from '@/constants/candles'; import { Candle, TradingViewBar } from '@/constants/candles';
import { TRADE_ROUTE } from '@/constants/routes';
import { mapCandle } from '@/lib/tradingView/utils';
import type { RootState } from './_store'; import type { RootState } from './_store';
import { mapCandle } from '@/lib/tradingView/utils';
/** /**
* @param state * @param state
+39
View File
@@ -0,0 +1,39 @@
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 { css } from 'styled-components'; import styled, { css, type FlattenSimpleInterpolation } from 'styled-components';
import breakpoints from './breakpoints'; import breakpoints from './breakpoints';
+22 -26
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,6 +6,8 @@ import {
type ThemeProps, type ThemeProps,
} from 'styled-components'; } from 'styled-components';
import breakpoints from './breakpoints';
export const layoutMixins: Record< export const layoutMixins: Record<
string, string,
FlattenSimpleInterpolation | FlattenInterpolation<ThemeProps<any>> FlattenSimpleInterpolation | FlattenInterpolation<ThemeProps<any>>
+1
View File
@@ -5,6 +5,7 @@ import {
type ThemeProps, type ThemeProps,
} from 'styled-components'; } from 'styled-components';
import breakpoints from './breakpoints';
import { layoutMixins } from './layoutMixins'; import { layoutMixins } from './layoutMixins';
export const tableMixins: Record< export const tableMixins: Record<
+8 -2
View File
@@ -1,16 +1,22 @@
/* Constants */ /* Constants */
:root { :root {
--fontFamily-base: "Satoshi", system-ui, -apple-system, Helvetica, Arial, sans-serif; --fontFamily-base: "Circular", system-ui, -apple-system, Helvetica, Arial, sans-serif;
--fontFamily-monospace: Courier, monospace, var(--fontFamily-base); --fontFamily-monospace: "CircularMono", Courier, monospace, var(--fontFamily-base);
--fontWeight-base-0: 450; --fontWeight-base-0: 450;
--fontWeight-base-1: 500; --fontWeight-base-1: 500;
--fontWeight-base-2: 700; --fontWeight-base-2: 700;
--fontWeight-monospace: 400;
--letter-spacing-base: -0.02em; --letter-spacing-base: -0.02em;
/* New Typography system */ /* New Typography system */
/**
* @size 10px/11px
* @weight 400
*/
--fontSize-tiny: 0.625rem; /* 10px/11px */ --fontSize-tiny: 0.625rem; /* 10px/11px */
--fontSize-mini: 0.75rem; /* 12px/13px */ --fontSize-mini: 0.75rem; /* 12px/13px */
--fontSize-small: 0.8438rem; /* 13.5px/14.5px */ --fontSize-small: 0.8438rem; /* 13.5px/14.5px */
@@ -62,10 +62,8 @@ export const AccountInfoConnectedState = () => {
const { buyingPower, equity, marginUsage, leverage } = subAccount || {}; const { buyingPower, equity, marginUsage, leverage } = subAccount || {};
const hasDiff = const hasDiff =
(marginUsage?.postOrder !== null && marginUsage?.postOrder != null &&
!MustBigNumber(marginUsage?.postOrder).eq(MustBigNumber(marginUsage?.current))) || !MustBigNumber(marginUsage?.postOrder).eq(MustBigNumber(marginUsage?.current));
(buyingPower?.postOrder !== null &&
!MustBigNumber(buyingPower?.postOrder).eq(MustBigNumber(buyingPower?.current)));
const showHeader = !hasDiff && !isTablet; const showHeader = !hasDiff && !isTablet;
+39 -29
View File
@@ -2,7 +2,7 @@ import { shallowEqual, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent } from 'styled-components'; import styled, { type AnyStyledComponent } from 'styled-components';
import { ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons'; import { ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons';
import { STRING_KEYS, type StringKey } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { useBreakpoints, useStringGetter } from '@/hooks'; import { useBreakpoints, useStringGetter } from '@/hooks';
import { breakpoints } from '@/styles'; import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
@@ -29,13 +29,15 @@ export const MarketDetails: React.FC = () => {
if (!configs) return null; if (!configs) return null;
const { const {
basePositionNotional,
tickSize, tickSize,
stepSize, stepSize,
initialMarginFraction, initialMarginFraction,
maintenanceMarginFraction, maintenanceMarginFraction,
minOrderSize, minOrderSize,
stepSizeDecimals, maxPositionSize,
baselinePositionSize,
incrementalInitialMarginFraction,
incrementalPositionSize,
tickSizeDecimals, tickSizeDecimals,
} = configs; } = configs;
@@ -70,28 +72,12 @@ export const MarketDetails: React.FC = () => {
key: 'step-size', key: 'step-size',
label: stringGetter({ key: STRING_KEYS.STEP_SIZE }), label: stringGetter({ key: STRING_KEYS.STEP_SIZE }),
tooltip: 'step-size', tooltip: 'step-size',
value: ( value: <Output useGrouping value={stepSize} type={OutputType.Asset} tag={symbol} />,
<Output
useGrouping
value={stepSize}
type={OutputType.Asset}
tag={symbol}
fractionDigits={stepSizeDecimals}
/>
),
}, },
{ {
key: 'min-order-size', key: 'min-order-size',
label: stringGetter({ key: STRING_KEYS.MINIMUM_ORDER_SIZE }), label: stringGetter({ key: STRING_KEYS.MINIMUM_ORDER_SIZE }),
value: ( value: <Output useGrouping value={minOrderSize} type={OutputType.Asset} tag={symbol} />,
<Output
useGrouping
value={minOrderSize}
type={OutputType.Asset}
tag={symbol}
fractionDigits={stepSizeDecimals}
/>
),
}, },
{ {
key: 'max-leverage', key: 'max-leverage',
@@ -114,16 +100,40 @@ export const MarketDetails: React.FC = () => {
), ),
}, },
{ {
key: 'base-initial-margin-fraction', key: 'initial-margin-fraction',
label: stringGetter({ key: STRING_KEYS.BASE_INITIAL_MARGIN_FRACTION }), label: stringGetter({ key: STRING_KEYS.INITIAL_MARGIN_FRACTION }),
tooltip: 'initial-margin-fraction', tooltip: 'initial-margin-fraction',
value: <Output useGrouping value={initialMarginFraction} type={OutputType.SmallPercent} />, value: <Output useGrouping value={initialMarginFraction} type={OutputType.SmallPercent} />,
}, },
{ {
key: 'base-position-notional', key: 'incremental-initial-margin-fraction',
label: stringGetter({ key: STRING_KEYS.BASE_POSITION_NOTIONAL }), label: stringGetter({ key: STRING_KEYS.INCREMENTAL_INITIAL_MARGIN_FRACTION }),
tooltip: 'base-position-notional', value: (
value: <Output useGrouping value={basePositionNotional} type={OutputType.Number} />, <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} />,
}, },
]; ];
@@ -140,10 +150,10 @@ export const MarketDetails: React.FC = () => {
<Styled.MarketDescription> <Styled.MarketDescription>
{primaryDescriptionKey && ( {primaryDescriptionKey && (
<p>{stringGetter({ key: STRING_KEYS[primaryDescriptionKey as StringKey] })}</p> <p>{stringGetter({ key: STRING_KEYS[primaryDescriptionKey] })}</p>
)} )}
{secondaryDescriptionKey && ( {secondaryDescriptionKey && (
<p>{stringGetter({ key: STRING_KEYS[secondaryDescriptionKey as StringKey] })}</p> <p>{stringGetter({ key: STRING_KEYS[secondaryDescriptionKey] })}</p>
)} )}
</Styled.MarketDescription> </Styled.MarketDescription>
+2 -5
View File
@@ -15,17 +15,14 @@ export const MarketLinks = () => {
const linkItems = [ const linkItems = [
{ {
key: 'coinmarketcap',
href: coinMarketCapsLink, href: coinMarketCapsLink,
icon: IconName.CoinMarketCap, icon: IconName.CoinMarketCap,
}, },
{ {
key: 'whitepaper',
href: whitepaperLink, href: whitepaperLink,
icon: IconName.Whitepaper, icon: IconName.Whitepaper,
}, },
{ {
key: 'project-website',
href: websiteLink, href: websiteLink,
icon: IconName.Website, icon: IconName.Website,
}, },
@@ -34,8 +31,8 @@ export const MarketLinks = () => {
return ( return (
<Styled.MarketLinks> <Styled.MarketLinks>
{linkItems.map( {linkItems.map(
({ key, href, icon }) => ({ href, icon }) =>
href && <IconButton key={key} href={href} iconName={icon} type={ButtonType.Link} /> href && <IconButton key={href} href={href} iconName={icon} type={ButtonType.Link} />
)} )}
</Styled.MarketLinks> </Styled.MarketLinks>
); );
+1
View File
@@ -49,6 +49,7 @@ const useTradeTypeOptions = () => {
value && { value && {
value: value as TradeTypes, value: value as TradeTypes,
label, label,
disabled: import.meta.env.MODE === 'production', // TODO: Remove this when stop orders are supported on testnet
} }
) )
.filter(isTruthy), .filter(isTruthy),
-15
View File
@@ -7,11 +7,9 @@ import { StatusResponse } from '@0xsquid/sdk';
import { useInterval, useStringGetter } from '@/hooks'; import { useInterval, useStringGetter } from '@/hooks';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { AlertType } from '@/constants/alerts';
import { formatSeconds } from '@/lib/timeUtils'; import { formatSeconds } from '@/lib/timeUtils';
import { AlertMessage } from '@/components/AlertMessage';
import { Output, OutputType } from '@/components/Output'; import { Output, OutputType } from '@/components/Output';
import { WithReceipt } from '@/components/WithReceipt'; import { WithReceipt } from '@/components/WithReceipt';
import { Icon, IconName } from '@/components/Icon'; import { Icon, IconName } from '@/components/Icon';
@@ -35,9 +33,6 @@ export const TransferStatusToast = ({
const [open, setOpen] = useState<boolean>(false); const [open, setOpen] = useState<boolean>(false);
const [secondsLeft, setSecondsLeft] = useState<number | undefined>(); const [secondsLeft, setSecondsLeft] = useState<number | undefined>();
// @ts-ignore status.errors is not in the type definition but can be returned
const error = status?.errors?.length ? status?.errors[0] : status?.error;
const type = useMemo( const type = useMemo(
() => (status?.toChain?.chainData?.chainId === TESTNET_CHAIN_ID ? 'deposit' : 'withdrawal'), () => (status?.toChain?.chainData?.chainId === TESTNET_CHAIN_ID ? 'deposit' : 'withdrawal'),
[status] [status]
@@ -79,16 +74,6 @@ export const TransferStatusToast = ({
}, },
})} })}
</Styled.Status> </Styled.Status>
{error && (
<AlertMessage type={AlertType.Error}>
{stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: {
ERROR_MESSAGE: error.message || stringGetter({ key: STRING_KEYS.UNKNOWN_ERROR }),
},
})}
</AlertMessage>
)}
<Styled.Trigger> <Styled.Trigger>
<Styled.TriggerIcon> <Styled.TriggerIcon>
<Icon iconName={IconName.Caret} /> <Icon iconName={IconName.Caret} />
+1 -1
View File
@@ -59,7 +59,7 @@ export const TransferStatusSteps = ({ status }: ElementProps) => {
}, },
]; ];
const currentStatus = routeStatus?.[routeStatus?.length - 1]; const currentStatus = routeStatus[routeStatus?.length - 1];
let currentStep = TransferStatusStep.Bridge; let currentStep = TransferStatusStep.Bridge;
@@ -7,7 +7,7 @@ import { layoutMixins } from '@/styles/layoutMixins';
import { AbacusOrderStatus, AbacusOrderTypes, type Nullable } from '@/constants/abacus'; import { AbacusOrderStatus, AbacusOrderTypes, type Nullable } from '@/constants/abacus';
import { ButtonAction } from '@/constants/buttons'; import { ButtonAction } from '@/constants/buttons';
import { STRING_KEYS, type StringKey } from '@/constants/localization'; import { STRING_KEYS, StringKey } from '@/constants/localization';
import { AssetIcon } from '@/components/AssetIcon'; import { AssetIcon } from '@/components/AssetIcon';
import { Button } from '@/components/Button'; import { Button } from '@/components/Button';
@@ -38,6 +38,9 @@ type ElementProps = {
setIsOpen: (open: boolean) => void; setIsOpen: (open: boolean) => void;
}; };
const isStringKey = (key: Nullable<string>): key is StringKey =>
key != null && STRING_KEYS.hasOwnProperty(key);
export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => { export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -109,9 +112,9 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
<Styled.Row> <Styled.Row>
<Styled.StatusIcon iconName={statusIcon} color={statusIconColor} /> <Styled.StatusIcon iconName={statusIcon} color={statusIconColor} />
<Styled.Status> <Styled.Status>
{statusStringKey {isStringKey(statusStringKey)
? stringGetter({ key: statusStringKey }) ? stringGetter({ key: statusStringKey })
: resources.statusStringKey : isStringKey(resources.statusStringKey)
? stringGetter({ key: resources.statusStringKey }) ? stringGetter({ key: resources.statusStringKey })
: undefined} : undefined}
</Styled.Status> </Styled.Status>
@@ -121,8 +124,8 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
{ {
key: 'cancel-reason', key: 'cancel-reason',
label: stringGetter({ key: STRING_KEYS.CANCEL_REASON }), label: stringGetter({ key: STRING_KEYS.CANCEL_REASON }),
value: cancelReason value: isStringKey(cancelReason)
? stringGetter({ key: STRING_KEYS[cancelReason as StringKey] }) ? stringGetter({ key: STRING_KEYS[cancelReason] })
: undefined, : undefined,
}, },
{ {
@@ -184,7 +187,8 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
const onCancelClick = async () => { const onCancelClick = async () => {
setIsPlacingCancel(true); setIsPlacingCancel(true);
await cancelOrder({ orderId, onError: () => setIsPlacingCancel(false) }); await cancelOrder({ orderId });
setIsPlacingCancel(false);
}; };
const onClearClick = () => { const onClearClick = () => {
+10 -5
View File
@@ -1,9 +1,11 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import styled, { AnyStyledComponent } from 'styled-components'; import styled, { AnyStyledComponent } from 'styled-components';
import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { AbacusApiStatus } from '@/constants/abacus'; import { AbacusApiStatus } from '@/constants/abacus';
import { DialogTypes } from '@/constants/dialogs'; import { CLIENT_NETWORK_CONFIGS } from '@/constants/networks';
import { UNICODE } from '@/constants/unicode';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { useApiState, useStringGetter } from '@/hooks'; import { useApiState, useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
@@ -15,7 +17,6 @@ import { NetworkSelectMenu } from '@/views/menus/NetworkSelectMenu';
import { closeDialog } from '@/state/dialogs'; import { closeDialog } from '@/state/dialogs';
import { getSelectedNetwork } from '@/state/appSelectors'; import { getSelectedNetwork } from '@/state/appSelectors';
import { getActiveDialog } from '@/state/dialogsSelectors';
type ElementProps = { type ElementProps = {
preventClose?: boolean; preventClose?: boolean;
@@ -27,10 +28,9 @@ export const ExchangeOfflineDialog = ({ preventClose, setIsOpen }: ElementProps)
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
const { status, statusErrorMessage } = useApiState(); const { status, statusErrorMessage } = useApiState();
const selectedNetwork = useSelector(getSelectedNetwork); const selectedNetwork = useSelector(getSelectedNetwork);
const activeDialog = useSelector(getActiveDialog, shallowEqual);
useEffect(() => { useEffect(() => {
if (activeDialog?.type === DialogTypes.ExchangeOffline && status === AbacusApiStatus.NORMAL) { if (status === AbacusApiStatus.NORMAL) {
dispatch(closeDialog()); dispatch(closeDialog());
} }
}, [status, selectedNetwork]); }, [status, selectedNetwork]);
@@ -44,6 +44,11 @@ export const ExchangeOfflineDialog = ({ preventClose, setIsOpen }: ElementProps)
> >
<Styled.Content> <Styled.Content>
<p>{statusErrorMessage}</p> <p>{statusErrorMessage}</p>
{CLIENT_NETWORK_CONFIGS[selectedNetwork].dydxChainId === 'dydx-testnet-2' && (
<Styled.Link href="https://status.v4testnet.dydx.exchange">
{stringGetter({ key: STRING_KEYS.LEARN_MORE })} {UNICODE.ARROW_RIGHT}
</Styled.Link>
)}
{import.meta.env.MODE !== 'production' && <NetworkSelectMenu />} {import.meta.env.MODE !== 'production' && <NetworkSelectMenu />}
</Styled.Content> </Styled.Content>
</Dialog> </Dialog>
+6 -13
View File
@@ -17,7 +17,7 @@ import { Ring } from '@/components/Ring';
import { ToggleGroup } from '@/components/ToggleGroup'; import { ToggleGroup } from '@/components/ToggleGroup';
import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; import { getCurrentMarketAssetData } from '@/state/assetsSelectors';
import { getInputTradeData, getInputTradeOptions } from '@/state/inputsSelectors'; import { getInputTradeData } from '@/state/inputsSelectors';
import abacusStateManager from '@/lib/abacus'; import abacusStateManager from '@/lib/abacus';
import { getSelectedTradeType } from '@/lib/tradeData'; import { getSelectedTradeType } from '@/lib/tradeData';
@@ -35,15 +35,6 @@ export const TradeDialog = ({ isOpen, setIsOpen, slotTrigger }: ElementProps) =>
const currentTradeData = useSelector(getInputTradeData, shallowEqual); const currentTradeData = useSelector(getInputTradeData, shallowEqual);
const { type } = currentTradeData || {}; const { type } = currentTradeData || {};
const selectedTradeType = getSelectedTradeType(type); const selectedTradeType = getSelectedTradeType(type);
const { typeOptions } = useSelector(getInputTradeOptions, shallowEqual) ?? {};
const allTradeTypeItems = typeOptions?.toArray()?.map(({ type, stringKey }) => ({
value: type,
label: stringGetter({
key: stringKey,
}),
slotBefore: <AssetIcon symbol={symbol} />,
}));
const [currentStep, setCurrentStep] = useState<MobilePlaceOrderSteps>( const [currentStep, setCurrentStep] = useState<MobilePlaceOrderSteps>(
MobilePlaceOrderSteps.EditOrder MobilePlaceOrderSteps.EditOrder
@@ -71,7 +62,11 @@ export const TradeDialog = ({ isOpen, setIsOpen, slotTrigger }: ElementProps) =>
[MobilePlaceOrderSteps.EditOrder]: { [MobilePlaceOrderSteps.EditOrder]: {
title: ( title: (
<Styled.ToggleGroup <Styled.ToggleGroup
items={allTradeTypeItems} items={[TradeTypes.LIMIT, TradeTypes.MARKET].map((tradeType: TradeTypes) => ({
value: tradeType,
label: stringGetter({ key: TRADE_TYPE_STRINGS[tradeType].tradeTypeKey }),
slotBefore: <AssetIcon symbol={symbol} />,
}))}
value={selectedTradeType} value={selectedTradeType}
onValueChange={(tradeType: TradeTypes) => onValueChange={(tradeType: TradeTypes) =>
onTradeTypeChange(tradeType || selectedTradeType) onTradeTypeChange(tradeType || selectedTradeType)
@@ -131,8 +126,6 @@ Styled.Dialog = styled(Dialog)<{ currentStep: MobilePlaceOrderSteps }>`
`; `;
Styled.ToggleGroup = styled(ToggleGroup)` Styled.ToggleGroup = styled(ToggleGroup)`
overflow-x: auto;
button[data-state='off'] { button[data-state='off'] {
gap: 0; gap: 0;
@@ -38,7 +38,6 @@ import { getTransferInputs } from '@/state/inputsSelectors';
import abacusStateManager from '@/lib/abacus'; import abacusStateManager from '@/lib/abacus';
import { MustBigNumber } from '@/lib/numbers'; import { MustBigNumber } from '@/lib/numbers';
import { log } from '@/lib/telemetry'; import { log } from '@/lib/telemetry';
import { parseWalletError } from '@/lib/wallet';
import { ChainSelectMenu } from './ChainSelectMenu'; import { ChainSelectMenu } from './ChainSelectMenu';
import { TokenSelectMenu } from './TokenSelectMenu'; import { TokenSelectMenu } from './TokenSelectMenu';
@@ -287,7 +286,12 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const errorMessage = useMemo(() => { const errorMessage = useMemo(() => {
if (error) { if (error) {
return parseWalletError({ error, stringGetter }).message; return error?.message
? stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: { ERROR_MESSAGE: error.message },
})
: stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG });
} }
if (fromAmount) { if (fromAmount) {
@@ -43,7 +43,7 @@ export const WithdrawForm = () => {
const [error, setError] = useState<Error | null>(null); const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { sendSquidWithdraw } = useSubaccount(); const { simulateWithdraw, sendSquidWithdraw } = useSubaccount();
const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {}; const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {};
// User input // User input
@@ -97,10 +97,16 @@ export const WithdrawForm = () => {
field: TransferInputField.usdcSize, field: TransferInputField.usdcSize,
}); });
} else { } else {
const stdFee = await simulateWithdraw(parseFloat(debouncedAmount));
const amount =
parseFloat(debouncedAmount) -
parseFloat(stdFee?.amount[0]?.amount || '0') / QUANTUM_MULTIPLIER;
abacusStateManager.setTransferValue({ abacusStateManager.setTransferValue({
value: debouncedAmount, value: amount.toString(),
field: TransferInputField.usdcSize, field: TransferInputField.usdcSize,
}); });
setError(null); setError(null);
} }
} catch (error) { } catch (error) {
@@ -49,15 +49,15 @@ export const WithdrawButtonAndReceipt = ({
const [isEditingSlippage, setIsEditingSlipapge] = useState(false); const [isEditingSlippage, setIsEditingSlipapge] = useState(false);
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
// TODO: uncomment when we have a way to get token amount estimate from abacus const { balance, queryStatus } = useAccountBalance({
// const { balance, queryStatus } = useAccountBalance({ addressOrDenom: withdrawToken?.address || undefined,
// addressOrDenom: withdrawToken?.address || undefined, assetSymbol: withdrawToken?.symbol || undefined,
// assetSymbol: withdrawToken?.symbol || undefined, chainId: withdrawChain ? parseInt(withdrawChain) : undefined,
// chainId: withdrawChain ? parseInt(withdrawChain) : undefined, decimals: withdrawToken?.decimals || undefined,
// decimals: withdrawToken?.decimals || undefined, isCosmosChain: false,
// isCosmosChain: false, });
// });
// TODO: uncomment when we have a way to get token amount estimate from abacus
// const balanceBN = MustBigNumber(balance); // const balanceBN = MustBigNumber(balance);
// const newBalance = // const newBalance =
// // toAmountMin && withdrawToken && parseUnits(toAmountMin, withdrawToken.decimals).toString(); // // toAmountMin && withdrawToken && parseUnits(toAmountMin, withdrawToken.decimals).toString();
@@ -158,7 +158,7 @@ export const WithdrawButtonAndReceipt = ({
// }, // },
]; ];
const isFormValid = !isDisabled && !isEditingSlippage; const isFormValid = !isDisabled && !isEditingSlippage && queryStatus !== 'error';
return ( return (
<Styled.WithReceipt <Styled.WithReceipt
+15 -22
View File
@@ -1,19 +1,14 @@
import { type FormEvent, useCallback, useEffect, useState } from 'react'; import { type FormEvent, useEffect, useState } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components'; import styled, { type AnyStyledComponent } from 'styled-components';
import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { import { ClosePositionInputField } from '@/constants/abacus';
ClosePositionInputField,
type HumanReadablePlaceOrderPayload,
type Nullable,
} from '@/constants/abacus';
import { AlertType } from '@/constants/alerts'; import { AlertType } from '@/constants/alerts';
import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons'; import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons';
import { TOKEN_DECIMALS } from '@/constants/numbers'; import { TOKEN_DECIMALS } from '@/constants/numbers';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { MobilePlaceOrderSteps } from '@/constants/trade'; import { MobilePlaceOrderSteps } from '@/constants/trade';
import { useBreakpoints, useIsFirstRender, useStringGetter, useSubaccount } from '@/hooks'; import { useBreakpoints, useIsFirstRender, useStringGetter, useSubaccount } from '@/hooks';
import { useOnLastOrderIndexed } from '@/hooks/useOnLastOrderIndexed';
import { breakpoints } from '@/styles'; import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
@@ -32,7 +27,10 @@ import { Orderbook, orderbookMixins, type OrderbookScrollBehavior } from '@/view
import { PositionPreview } from '@/views/forms/TradeForm/PositionPreview'; import { PositionPreview } from '@/views/forms/TradeForm/PositionPreview';
import { getCurrentMarketPositionData } from '@/state/accountSelectors'; import {
calculateHasUncommittedOrders,
getCurrentMarketPositionData,
} from '@/state/accountSelectors';
import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; import { getCurrentMarketAssetData } from '@/state/assetsSelectors';
import { getInputClosePositionData } from '@/state/inputsSelectors'; import { getInputClosePositionData } from '@/state/inputsSelectors';
@@ -84,7 +82,9 @@ export const ClosePositionForm = ({
const { stepSizeDecimals } = useSelector(getCurrentMarketConfig, shallowEqual) || {}; const { stepSizeDecimals } = useSelector(getCurrentMarketConfig, shallowEqual) || {};
const { size: sizeData, summary } = useSelector(getInputClosePositionData, shallowEqual) || {}; const { size: sizeData, summary } = useSelector(getInputClosePositionData, shallowEqual) || {};
const { size, percent } = sizeData || {}; const { size, percent } = sizeData || {};
const hasUncommittedOrders = useSelector(calculateHasUncommittedOrders);
const currentInput = useSelector(getCurrentInput); const currentInput = useSelector(getCurrentInput);
const currentPositionData = useSelector(getCurrentMarketPositionData, shallowEqual); const currentPositionData = useSelector(getCurrentMarketPositionData, shallowEqual);
const { size: currentPositionSize } = currentPositionData || {}; const { size: currentPositionSize } = currentPositionData || {};
const { current: currentSize } = currentPositionSize || {}; const { current: currentSize } = currentPositionSize || {};
@@ -106,22 +106,17 @@ export const ClosePositionForm = ({
} }
}, [currentInput, market, currentStep]); }, [currentInput, market, currentStep]);
const onLastOrderIndexed = useCallback(() => { useEffect(() => {
if (!isFirstRender) { // close has been placed
if (!isFirstRender && !hasUncommittedOrders) {
abacusStateManager.clearClosePositionInputValues({ shouldFocusOnTradeInput: true }); abacusStateManager.clearClosePositionInputValues({ shouldFocusOnTradeInput: true });
onClosePositionSuccess?.(); onClosePositionSuccess?.();
if (currentStep === MobilePlaceOrderSteps.PlacingOrder) { if (currentStep === MobilePlaceOrderSteps.PlacingOrder) {
setCurrentStep?.(MobilePlaceOrderSteps.Confirmation); setCurrentStep?.(MobilePlaceOrderSteps.Confirmation);
} }
setIsClosingPosition(false);
} }
}, [currentStep, isFirstRender]); }, [hasUncommittedOrders]);
const { setUnIndexedClientId } = useOnLastOrderIndexed({
callback: onLastOrderIndexed,
});
const onAmountInput = ({ floatValue }: { floatValue?: number }) => { const onAmountInput = ({ floatValue }: { floatValue?: number }) => {
if (currentSize == null) return; if (currentSize == null) return;
@@ -170,16 +165,14 @@ export const ClosePositionForm = ({
setIsClosingPosition(true); setIsClosingPosition(true);
await closePosition({ await closePosition({
onError: (errorParams?: { errorStringKey?: Nullable<string> }) => { onError: (errorParams?: { errorStringKey?: string }) => {
setClosePositionError( setClosePositionError(
stringGetter({ key: errorParams?.errorStringKey || STRING_KEYS.SOMETHING_WENT_WRONG }) stringGetter({ key: errorParams?.errorStringKey || STRING_KEYS.SOMETHING_WENT_WRONG })
); );
setIsClosingPosition(false);
},
onSuccess: (placeOrderPayload: Nullable<HumanReadablePlaceOrderPayload>) => {
setUnIndexedClientId(placeOrderPayload?.clientId);
}, },
}); });
setIsClosingPosition(false);
}; };
const alertMessage = closePositionError && ( const alertMessage = closePositionError && (
+11 -15
View File
@@ -1,4 +1,4 @@
import { type FormEvent, useState, Ref, useCallback } from 'react'; import { type FormEvent, useState, useEffect, Ref } from 'react';
import styled, { AnyStyledComponent, css } from 'styled-components'; import styled, { AnyStyledComponent, css } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux'; import { shallowEqual, useSelector } from 'react-redux';
import type { NumberFormatValues, SourceInfo } from 'react-number-format'; import type { NumberFormatValues, SourceInfo } from 'react-number-format';
@@ -7,8 +7,6 @@ import { AlertType } from '@/constants/alerts';
import { import {
ErrorType, ErrorType,
type HumanReadablePlaceOrderPayload,
type Nullable,
TradeInputErrorAction, TradeInputErrorAction,
TradeInputField, TradeInputField,
ValidationError, ValidationError,
@@ -21,7 +19,6 @@ import { InputErrorData, TradeBoxKeys, MobilePlaceOrderSteps } from '@/constants
import { breakpoints } from '@/styles'; import { breakpoints } from '@/styles';
import { useStringGetter, useSubaccount } from '@/hooks'; import { useStringGetter, useSubaccount } from '@/hooks';
import { useOnLastOrderIndexed } from '@/hooks/useOnLastOrderIndexed';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins'; import { formMixins } from '@/styles/formMixins';
@@ -37,6 +34,7 @@ import { WithTooltip } from '@/components/WithTooltip';
import { Orderbook } from '@/views/tables/Orderbook'; import { Orderbook } from '@/views/tables/Orderbook';
import { calculateHasUncommittedOrders } from '@/state/accountSelectors';
import { getCurrentInput, useTradeFormData } from '@/state/inputsSelectors'; import { getCurrentInput, useTradeFormData } from '@/state/inputsSelectors';
import { getCurrentMarketConfig } from '@/state/perpetualsSelectors'; import { getCurrentMarketConfig } from '@/state/perpetualsSelectors';
@@ -99,6 +97,7 @@ export const TradeForm = ({
} = useTradeFormData(); } = useTradeFormData();
const { limitPrice, triggerPrice, trailingPercent } = price || {}; const { limitPrice, triggerPrice, trailingPercent } = price || {};
const hasUncommittedOrders = useSelector(calculateHasUncommittedOrders);
const currentInput = useSelector(getCurrentInput); const currentInput = useSelector(getCurrentInput);
const { tickSizeDecimals, stepSizeDecimals } = const { tickSizeDecimals, stepSizeDecimals } =
useSelector(getCurrentMarketConfig, shallowEqual) || {}; useSelector(getCurrentMarketConfig, shallowEqual) || {};
@@ -155,33 +154,30 @@ export const TradeForm = ({
} }
}; };
const onLastOrderIndexed = useCallback(() => { useEffect(() => {
if (!currentStep || currentStep === MobilePlaceOrderSteps.PlacingOrder) { // order has been placed
if (
(!currentStep || currentStep === MobilePlaceOrderSteps.PlacingOrder) &&
!hasUncommittedOrders
) {
setIsPlacingOrder(false); setIsPlacingOrder(false);
abacusStateManager.clearTradeInputValues({ shouldResetSize: true }); abacusStateManager.clearTradeInputValues({ shouldResetSize: true });
setCurrentStep?.(MobilePlaceOrderSteps.Confirmation); setCurrentStep?.(MobilePlaceOrderSteps.Confirmation);
} }
}, [currentStep]); }, [currentStep, hasUncommittedOrders]);
const { setUnIndexedClientId } = useOnLastOrderIndexed({
callback: onLastOrderIndexed,
});
const onPlaceOrder = async () => { const onPlaceOrder = async () => {
setPlaceOrderError(undefined); setPlaceOrderError(undefined);
setIsPlacingOrder(true); setIsPlacingOrder(true);
await placeOrder({ await placeOrder({
onError: (errorParams?: { errorStringKey?: Nullable<string> }) => { onError: (errorParams?: { errorStringKey?: string }) => {
setPlaceOrderError( setPlaceOrderError(
stringGetter({ key: errorParams?.errorStringKey || STRING_KEYS.SOMETHING_WENT_WRONG }) stringGetter({ key: errorParams?.errorStringKey || STRING_KEYS.SOMETHING_WENT_WRONG })
); );
setIsPlacingOrder(false); setIsPlacingOrder(false);
}, },
onSuccess: (placeOrderPayload?: Nullable<HumanReadablePlaceOrderPayload>) => {
setUnIndexedClientId(placeOrderPayload?.clientId);
},
}); });
}; };
@@ -136,7 +136,7 @@ export const PlaceOrderButtonAndReceipt = ({
const buttonState = currentStep const buttonState = currentStep
? buttonStatesPerStep[currentStep].buttonState ? buttonStatesPerStep[currentStep].buttonState
: { isDisabled: !shouldEnableTrade || isLoading, isLoading }; : { isDisabled: !shouldEnableTrade, isLoading };
return ( return (
<WithDetailsReceipt detailItems={items}> <WithDetailsReceipt detailItems={items}>
+52 -16
View File
@@ -13,7 +13,7 @@ import { ButtonShape, ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { CLIENT_NETWORK_CONFIGS } from '@/constants/networks'; import { CLIENT_NETWORK_CONFIGS } from '@/constants/networks';
import { NumberSign, QUANTUM_MULTIPLIER } from '@/constants/numbers'; import { NumberSign, QUANTUM_MULTIPLIER } from '@/constants/numbers';
import { DYDX_CHAIN_ASSET_COIN_DENOM, DYDX_CHAIN_ASSET_TAGS, DydxChainAsset } from '@/constants/wallets'; import { DYDX_CHAIN_ASSET_COIN_DENOM, DYDX_CHAIN_INFO, DydxChainAsset } from '@/constants/wallets';
import { import {
useAccountBalance, useAccountBalance,
@@ -52,6 +52,28 @@ type TransferFormProps = {
className?: string; className?: string;
}; };
const debouncedEstimateFee = debounce(
async ({ amount, recipientAddress, asset, setFees, simulateTransfer }) => {
if (!amount || !recipientAddress) {
return;
}
try {
const coinDenom = DYDX_CHAIN_ASSET_COIN_DENOM[asset as DydxChainAsset];
const stdFee: StdFee = await simulateTransfer(amount, recipientAddress, coinDenom);
const fee = stdFee?.amount.find((coin) => coin.denom === coinDenom)?.amount;
const feeAmount = MustBigNumber(fee).div(QUANTUM_MULTIPLIER).toNumber();
setFees(feeAmount);
} catch (error) {
console.error('TransferForm > : debouncedEstimateFee > ', error);
}
},
1000,
{ trailing: true }
);
export const TransferForm = ({ export const TransferForm = ({
selectedAsset = DydxChainAsset.DYDX, selectedAsset = DydxChainAsset.DYDX,
onDone, onDone,
@@ -60,8 +82,8 @@ export const TransferForm = ({
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {}; const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {};
const { dydxAddress } = useAccounts(); const { dydxAddress } = useAccounts();
const { address: recipientAddress, size, fee } = useSelector(getTransferInputs, shallowEqual) || {}; const { address: recipientAddress, size } = useSelector(getTransferInputs, shallowEqual) || {};
const { transfer } = useSubaccount(); const { transfer, simulateTransfer } = useSubaccount();
const { nativeTokenBalance, usdcBalance } = useAccountBalance(); const { nativeTokenBalance, usdcBalance } = useAccountBalance();
const { selectedNetwork } = useSelectedNetwork(); const { selectedNetwork } = useSelectedNetwork();
@@ -71,6 +93,7 @@ export const TransferForm = ({
// Form states // Form states
const [error, setError] = useState<Error | undefined>(); const [error, setError] = useState<Error | undefined>();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [fees, setFees] = useState<number>();
const balance = asset === DydxChainAsset.USDC ? freeCollateral?.current : nativeTokenBalance; const balance = asset === DydxChainAsset.USDC ? freeCollateral?.current : nativeTokenBalance;
const newBalance = const newBalance =
@@ -81,7 +104,7 @@ export const TransferForm = ({
.toNumber(); .toNumber();
const amount = asset === DydxChainAsset.USDC ? size?.usdcSize : size?.size; const amount = asset === DydxChainAsset.USDC ? size?.usdcSize : size?.size;
const showNotEnoughGasWarning = fee && asset === DydxChainAsset.USDC && usdcBalance < fee; const showNotEnoughGasWarning = fees && asset === DydxChainAsset.USDC && usdcBalance < fees;
// BN // BN
const amountBN = MustBigNumber(amount); const amountBN = MustBigNumber(amount);
@@ -111,21 +134,34 @@ export const TransferForm = ({
useEffect(() => { useEffect(() => {
setError(undefined); setError(undefined);
abacusStateManager.setTransferValue({ debouncedEstimateFee.cancel();
value: asset,
field: TransferInputField.token, if (isAmountValid && isAddressValid) {
}); debouncedEstimateFee({
amount,
recipientAddress,
asset,
setFees,
simulateTransfer,
});
} else {
setFees(undefined);
}
}, [asset, amount, recipientAddress]);
useEffect(() => {
setError(undefined);
}, [asset]); }, [asset]);
const onTransfer = async () => { const onTransfer = async () => {
if (!isAmountValid || !isAddressValid || !fee) return; if (!isAmountValid || !isAddressValid || !fees) return;
setIsLoading(true); setIsLoading(true);
setError(undefined); setError(undefined);
try { try {
// Subtract fees from amount if sending native tokens // Subtract fees from amount if sending native tokens
const amountToTransfer = ( const amountToTransfer = (
asset === DydxChainAsset.DYDX ? amountBN.minus(fee) : amountBN asset === DydxChainAsset.DYDX ? amountBN.minus(fees) : amountBN
).toNumber(); ).toNumber();
const txResponse = await transfer( const txResponse = await transfer(
@@ -176,7 +212,7 @@ export const TransferForm = ({
value: DydxChainAsset.USDC, value: DydxChainAsset.USDC,
label: ( label: (
<Styled.InlineRow> <Styled.InlineRow>
<AssetIcon symbol="USDC" /> {DYDX_CHAIN_ASSET_TAGS[DydxChainAsset.USDC]} <AssetIcon symbol="USDC" /> USDC
</Styled.InlineRow> </Styled.InlineRow>
), ),
}, },
@@ -185,7 +221,7 @@ export const TransferForm = ({
label: ( label: (
<Styled.InlineRow> <Styled.InlineRow>
{/* <AssetIcon symbol="DYDX" /> */} {/* <AssetIcon symbol="DYDX" /> */}
{DYDX_CHAIN_ASSET_TAGS[DydxChainAsset.DYDX]} Dv4TNT
</Styled.InlineRow> </Styled.InlineRow>
), ),
}, },
@@ -207,7 +243,7 @@ export const TransferForm = ({
key: 'amount', key: 'amount',
label: ( label: (
<span> <span>
{stringGetter({ key: STRING_KEYS.AVAILABLE })} <Tag>{DYDX_CHAIN_ASSET_TAGS[asset]}</Tag> {stringGetter({ key: STRING_KEYS.AVAILABLE })} <Tag>{asset}</Tag>
</span> </span>
), ),
value: ( value: (
@@ -339,9 +375,9 @@ export const TransferForm = ({
<Styled.Footer> <Styled.Footer>
<TransferButtonAndReceipt <TransferButtonAndReceipt
selectedAsset={asset} selectedAsset={asset}
fees={fee || undefined} fees={fees}
isDisabled={!isAmountValid || !isAddressValid || !fee} isDisabled={!isAmountValid || !isAddressValid || !fees}
isLoading={isLoading || Boolean(isAmountValid && isAddressValid && !fee)} isLoading={isLoading || Boolean(isAmountValid && isAddressValid && !fees)}
/> />
</Styled.Footer> </Styled.Footer>
</Styled.Form> </Styled.Form>
@@ -3,7 +3,7 @@ import { shallowEqual, useSelector } from 'react-redux';
import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons'; import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { NumberSign } from '@/constants/numbers'; import { NumberSign } from '@/constants/numbers';
import { DYDX_CHAIN_ASSET_TAGS, DydxChainAsset } from '@/constants/wallets'; import { DydxChainAsset } from '@/constants/wallets';
import { useAccountBalance, useStringGetter } from '@/hooks'; import { useAccountBalance, useStringGetter } from '@/hooks';
@@ -60,7 +60,7 @@ export const TransferButtonAndReceipt = ({
key: 'fees', key: 'fees',
label: ( label: (
<span> <span>
{stringGetter({ key: STRING_KEYS.FEES })} <Tag>{DYDX_CHAIN_ASSET_TAGS[selectedAsset]}</Tag> {stringGetter({ key: STRING_KEYS.FEES })} <Tag>{selectedAsset}</Tag>
</span> </span>
), ),
value: <Output type={OutputType.Asset} value={fees} />, value: <Output type={OutputType.Asset} value={fees} />,
@@ -69,7 +69,7 @@ export const TransferButtonAndReceipt = ({
key: 'balance', key: 'balance',
label: ( label: (
<span> <span>
{stringGetter({ key: STRING_KEYS.BALANCE })} <Tag>{DYDX_CHAIN_ASSET_TAGS[selectedAsset]}</Tag> {stringGetter({ key: STRING_KEYS.BALANCE })} <Tag>{selectedAsset}</Tag>
</span> </span>
), ),
value: ( value: (
+9 -7
View File
@@ -1,10 +1,12 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { groupBy } from 'lodash'; import { groupBy } from 'lodash';
import styled from 'styled-components';
import { STRING_KEYS } from '@/constants/localization';
import { type Notification, NotificationStatus } from '@/constants/notifications'; import { type Notification, NotificationStatus } from '@/constants/notifications';
import { useStringGetter } from '@/hooks';
import { useNotifications } from '@/hooks/useNotifications'; import { useNotifications } from '@/hooks/useNotifications';
import { CloseIcon } from '@/icons';
import { Button } from '@/components/Button'; import { Button } from '@/components/Button';
import { ButtonAction, ButtonSize } from '@/constants/buttons'; import { ButtonAction, ButtonSize } from '@/constants/buttons';
@@ -13,9 +15,7 @@ import { DialogPlacement } from '@/components/Dialog';
import { Output, OutputType } from '@/components/Output'; import { Output, OutputType } from '@/components/Output';
import { IconButton } from '@/components/IconButton'; import { IconButton } from '@/components/IconButton';
import { Toolbar } from '@/components/Toolbar'; import { Toolbar } from '@/components/Toolbar';
import { CloseIcon } from '@/icons';
import styled from 'styled-components';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
type ElementProps = { type ElementProps = {
@@ -27,6 +27,8 @@ export const NotificationsMenu = ({
slotTrigger, slotTrigger,
placement = DialogPlacement.Sidebar, placement = DialogPlacement.Sidebar,
}: ElementProps) => { }: ElementProps) => {
const stringGetter = useStringGetter();
const { const {
notifications, notifications,
getDisplayData, getDisplayData,
@@ -69,7 +71,7 @@ export const NotificationsMenu = ({
.map(([status, notifications]) => ({ .map(([status, notifications]) => ({
group: status, group: status,
groupLabel: { groupLabel: {
[NotificationStatus.Triggered]: 'New', [NotificationStatus.Triggered]: stringGetter({ key: STRING_KEYS.NEW }),
// [NotificationStatus.Updated]: 'Updates', // [NotificationStatus.Updated]: 'Updates',
[NotificationStatus.Seen]: 'Seen', [NotificationStatus.Seen]: 'Seen',
[NotificationStatus.Cleared]: 'Archived', [NotificationStatus.Cleared]: 'Archived',
@@ -135,7 +137,7 @@ export const NotificationsMenu = ({
isOpen={isMenuOpen || placement === DialogPlacement.Inline} isOpen={isMenuOpen || placement === DialogPlacement.Inline}
setIsOpen={setIsMenuOpen} setIsOpen={setIsMenuOpen}
items={items} items={items}
title="Notifications" title={stringGetter({ key: STRING_KEYS.NOTIFICATIONS })}
slotTrigger={ slotTrigger={
<$TriggerContainer> <$TriggerContainer>
{slotTrigger} {slotTrigger}
@@ -168,7 +170,7 @@ export const NotificationsMenu = ({
), ),
}} }}
> >
Clear All {stringGetter({ key: STRING_KEYS.CLEAR })}
</Button> </Button>
</$FooterToolbar> </$FooterToolbar>
} }
+1 -1
View File
@@ -3,12 +3,12 @@ import styled, { type AnyStyledComponent, css } from 'styled-components';
import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { OrderSide } from '@dydxprotocol/v4-client-js'; import { OrderSide } from '@dydxprotocol/v4-client-js';
import type { ColumnSize } from '@react-types/table'; import type { ColumnSize } from '@react-types/table';
import { Nullable } from '@dydxprotocol/v4-abacus';
import { type Asset, type SubaccountFill } from '@/constants/abacus'; import { type Asset, type SubaccountFill } from '@/constants/abacus';
import { DialogTypes } from '@/constants/dialogs'; import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS, StringGetterFunction } from '@/constants/localization'; import { STRING_KEYS, StringGetterFunction } from '@/constants/localization';
import { Nullable } from '@dydxprotocol/abacus';
import { useBreakpoints, useStringGetter } from '@/hooks'; import { useBreakpoints, useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useState } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { useDispatch } from 'react-redux';
import styled, { AnyStyledComponent } from 'styled-components'; import styled, { AnyStyledComponent } from 'styled-components';
import { AbacusOrderStatus, type OrderStatus } from '@/constants/abacus'; import { AbacusOrderStatus, type OrderStatus } from '@/constants/abacus';
@@ -29,7 +29,8 @@ export const OrderActionsCell = ({ orderId, status, isDisabled }: ElementProps)
const onCancel = useCallback(async () => { const onCancel = useCallback(async () => {
setIsCanceling(true); setIsCanceling(true);
await cancelOrder({ orderId, onError: () => setIsCanceling(false) }); await cancelOrder({ orderId });
setIsCanceling(false);
}, []); }, []);
return ( return (
Binary file not shown.