Compare commits

..
Author SHA1 Message Date
Bill He 95f400c9ca fix loading button 2023-12-01 15:10:18 -08:00
Bill He 084772ab33 fix error 2023-12-01 15:10:18 -08:00
Bill He 98140c6878 Bump abacus 2023-12-01 15:10:18 -08:00
Bill He e576beed09 cctp.json 2023-12-01 15:10:18 -08:00
Bill He 476659cc63 bump packages 2023-12-01 15:10:16 -08:00
Bill He 98537b2bd7 update isCctp, and comments 2023-12-01 15:09:45 -08:00
Bill He ffb2a49a94 fix fees 2023-12-01 15:09:45 -08:00
Bill He e8e5e828d6 slippage 2023-12-01 15:09:45 -08:00
Bill He 4b030575a8 Support CCTP and noble auto deposits on testnet 2023-12-01 15:09:45 -08:00
aleka 91ea68dc53 Add chaos labs launch incentives panel (#182)
* add chaos lab incentives panel

* add seen state

* external link dialog

* better disconnected state
2023-12-01 15:27:19 -05:00
aleka cf79528d05 Display more markets 12/01 (#175)
* add more markets 12/01
2023-12-01 09:59:04 -05:00
Bill ef81f9da57 Add nobleValidator to config (#181) 2023-11-30 16:11:55 -08:00
aleka 60d59cd211 add more markets 11/30 (#174) 2023-11-30 10:06:25 -05:00
aleka 19d38378f3 display more markets (#173) 2023-11-29 10:33:02 -05:00
Bill 15a64232c0 Fix USDT approval (#170)
* Fix USDT approval

* address comment
2023-11-28 11:44:21 -08:00
aleka 28d3be0370 Release notification (#169)
* add release notif
2023-11-28 12:36:29 -05:00
Rui 90ff9ccc5a Add token address to cctp.json (#167)
* Add token address to cctp.json

* Update field name to tokenAddress
2023-11-27 17:55:52 -08:00
Jared Vu 33516422e7 Filter markets and add test flag (#168) 2023-11-27 17:05:19 -08:00
Rui eb31610a38 Add CCTP supported chains to cctp.json (#166) 2023-11-27 14:27:42 -08:00
Jared Vu b0e1655018 Bump Abacus@1.0.30 (#165) 2023-11-27 13:01:25 -08:00
Jared Vu b4cc5f87ec Add href prop to button component (#164)
* Add href prop to button component

* Use ButtonType.Link
2023-11-27 12:39:57 -08:00
Bill 549ae558e7 Default Locale to EN if not supported (#163) 2023-11-27 08:57:57 -08:00
Jared Vu 5b06110e2d Update Abacus@1.0.28 (#161) 2023-11-22 14:08:03 -08:00
aleka e3da2ee4b1 update copy and other polish (#157)
* update copy and other polish

* fix mobile nav menu nested buttons

* feedback
2023-11-22 15:06:21 -05:00
Rui 6b38d68b09 Remove universal link for trust wallet since it's no longer valid. (#160) 2023-11-21 19:25:03 -08:00
Jared Vu 1dedc93a56 Mobile friendly Toast: Collapse/Close (#159) 2023-11-21 19:24:19 -08:00
Jared Vu 1427b55728 MarketsTable: Sort OpenInterest by notional, Display 24h volume as fiat value (#158)
* Fix Sort by notional, Display fiat for volume

* OpenInterest notional on top
2023-11-21 16:10:23 -08:00
Jared Vu 20521b5b3f Add button to stack/unstack Toast notifications (#149)
* Add button to stack/unstack

* Button -> Toggle button, add offset for notifs
2023-11-20 15:12:38 -08:00
Jared Vu 532df49a06 Use new redux tradeFormInputs object for TradingForm input display values (#144)
* Redux slice for TradeFormInputs

* Move sideEffects from TradeForm to hook in Trade
2023-11-20 11:56:09 -08:00
aleka fb65db633a move buttons to footer in ack terms (#155) 2023-11-20 13:21:06 -05:00
aleka 279ef6b4f9 bump abacus (#153) 2023-11-13 10:57:40 -05:00
aleka 0def18db07 fix sorting 24h price change in markets table (#152) 2023-11-13 09:36:32 -05:00
Jared Vu 50bd860ced Bump Abacus 1.0.25 (#151) 2023-11-13 09:36:16 -05:00
Jared Vu 818574cedb Change dep in hook to fix input bug (#150) 2023-11-13 09:11:54 -05:00
aleka 3be0429a60 add back border to toggle buttons (#148) 2023-11-11 20:46:04 -08:00
aleka b8e72246b9 add manage funds dialog and transfers form styling (#147)
* add manage funds dialog and transfers form styling

* add sticky footer backdrop

* add testnet deposit toggle
2023-11-10 20:56:27 -08:00
Jared VuandBill He 99e708a984 Notifications Refresh (#135)
* Remove X button from tablet/mobile Notification menu

* Toast styling

* missed in merge

* .env.example

* Update abacus@1.0.9

* Add notif strings, and abaucs notif state

* Update Notification hooks, add Notification Preference controls

* Consolidate Notification into single component

* withLabel -> withTrigger prop

* nits

* OrderStatusChanged -> AbacusGenerated

* Notification OnClick

* Move Notification -> Components

* withAnimation -> $withAnimation

* Notification styling and click handle

* load state, onClick, localStorage

* add error count

* Rez localStorage Notifications

* Update TransferStatus UI

* Use Notification component for other abacusNotifs

* fix uselocalnotif query

* Default to Notification from NotificationMenu

---------

Co-authored-by: Bill He <bill@dydx.exchange>
2023-11-10 20:55:34 -08:00
aleka 876f620ada Account menu and components improvements (#145)
* better copy button

* hide sign into mobile for production and use new copy icon button

* new checkbox
2023-11-10 22:07:29 -05:00
Bill b9550890da Add additional onboarding entry points (#146)
* Add additional onboarding entry points

* flex-wrap
2023-11-10 19:05:46 -08:00
aleka 3732b955f5 add sample terms of use (#138)
* add sample terms of use

* add back article mixin
2023-11-10 14:58:26 -05:00
aleka 253bfc0eff update nav menu dropdown behavior to click instead of hover to open (#142) 2023-11-10 14:58:17 -05:00
Bill d4bcfc0428 Fix status empty bug (#143) 2023-11-10 09:55:54 -08:00
aleka 7dec32df5e fix 24h sort in market dropdown, table styling (#141) 2023-11-09 16:40:27 -05:00
aleka e09d574d5b show deposit form in mainnet onboarding (#140) 2023-11-09 15:33:16 -05:00
Bill 944fc6dc95 Handle squid rout errors (#139)
* Handle squid route errors

* bump abacus

* add dep

* address comments

* fix max

* disable slippage editor

* parse error message in abacus
2023-11-09 10:34:22 -08:00
aleka 91a97a1c68 add network select menu in restriction modals (dev) (#137) 2023-11-09 09:10:12 -05:00
aleka c5f736d805 fix deposit dialog logic (#136) 2023-11-08 14:01:16 -05:00
aleka 3769f7078c update readme re env mode (#133) 2023-11-07 20:28:56 -08:00
Jared Vu edc9fadfee Add TestFlags provider and class (#132)
* Add TestFlags provider and class

* import order nit

* markets is an object and not array

* Use null coalescing op
2023-11-07 20:28:42 -08:00
aleka 5e2d426cd1 Send native tokens inclusive fee (#118)
* send dv4tnt inclusive of fees
2023-11-07 22:42:41 -05:00
Bill 766209baf1 Onboarding fixes (#131)
* Onboarding fixes

* address comments

* rebase
2023-11-07 17:18:16 -08:00
aleka 906faa554d Surface close position input errors (#130)
* show abacus close position validation errors

* bump abacus version
2023-11-07 19:11:08 -05:00
John Huang c339eeccde TRCL-3077 Added an environment for testing. Expected behavior - when "v4 Staging Forced Update" is selected in iOS app, it should switch to the forced update screen (#99) 2023-11-07 13:33:06 -08:00
aleka bd03fb73ca update mintscan tx link on transfer history table (#128)
* update mintscan tx link on transfer history table

* bump localization + fix icon button padding in header
2023-11-07 16:00:22 -05:00
Rui 31825a8a52 Change "iOS" to "ios" for consistency (#129) 2023-11-07 11:01:41 -08:00
John Huang 8d5bf1a721 TRCL-3128 TradePlaceOrder and TradeCancelOrder are now tracked by Abacus (#115) 2023-11-07 10:34:06 -08:00
aleka 7f3d57b46a support multiple modes (#125) 2023-11-07 12:13:59 -05:00
Bill c292c84c2c Fix decimal overflow bug (#127)
* Fix decimal overflow bug

* use decimals:
2023-11-06 17:48:19 -08:00
Bill 2d37ff676b do not use TESTNET_CHAIN_ID (#126) 2023-11-06 17:39:02 -08:00
Rui 68f3552569 Add app configuration to env.json (#121) 2023-11-06 13:02:38 -08:00
aleka 18b12591ca Update LICENSE (#123) 2023-11-06 11:10:31 -08:00
Bill 3f963ffb07 Add Env var descriptions to readme (#124)
* Add Env var descriptions to readme

* update

* remove examples

* grammar
2023-11-06 10:50:45 -08:00
aleka aac024fd5e showing negative buying power as - (#119) 2023-11-06 13:31:41 -05:00
Christopher-Li 4913c690ec Update all dev4 endpoints to use https and wss (#117)
* Update all dev endpoints to use https and wss

* dev4
2023-11-06 09:33:22 -08:00
143 changed files with 5549 additions and 1927 deletions
+4 -1
View File
@@ -1,6 +1,9 @@
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.
Subject to your compliance with applicable law and the v4 Terms of Use, available at dydx.exchange/legal, you are granted the right to use the Program or 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 Program or Licensed Work, all of your rights and licenses to use (including any rights to reproduce, distribute, install or modify) the Program or Licensed Work will automatically and immediately terminate.
The “Program” or “Licensed Work” shall mean any of the following: 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. repository reflecting a copy of, or link to, this license.
The GNU Affero General Public License
+5
View File
@@ -98,6 +98,11 @@ For the "Build & Development Settings", we recommend the following:
- Framework Preset: `Vite`
- Build Command (override): `pnpm run build`
By default, the dev server runs in development mode and the build command runs in production mode. To override the default mode, you can pass in the `--mode` option flag. For example, if you want to build your app for testnet:
```
pnpm run build --mode testnet
```
If you wish to incorporate analytics via Amplitude and Bugsnag, you can use our scripts:
`pnpm run build:inject-amplitude` and `pnpm run build:inject-bugsnag`. You will need to provide your own API keys for these services. In the Environment Variables section, name the variables as `AMPLITUDE_API_KEY` and `BUGSNAG_API_KEY` and provide the respective keys as their values.
+7 -4
View File
@@ -39,14 +39,16 @@
"@cosmjs/proto-signing": "^0.31.0",
"@cosmjs/stargate": "^0.31.0",
"@cosmjs/tendermint-rpc": "^0.31.0",
"@dydxprotocol/v4-abacus": "^1.0.10",
"@dydxprotocol/v4-client-js": "^1.0.0",
"@dydxprotocol/v4-localization": "^1.0.0",
"@dydxprotocol/v4-abacus": "^1.1.4",
"@dydxprotocol/v4-client-js": "^1.0.6",
"@dydxprotocol/v4-localization": "^1.0.17",
"@ethersproject/providers": "^5.7.2",
"@js-joda/core": "^5.5.3",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-navigation-menu": "^1.1.3",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-radio-group": "^1.1.3",
@@ -96,6 +98,7 @@
"long": "^5.2.3",
"luxon": "^3.3.0",
"qr-code-styling": "1.6.0-rc.1",
"query-string": "^8.1.0",
"react": "^18.2.0",
"react-aria": "^3.25.0",
"react-dom": "^18.2.0",
@@ -108,7 +111,7 @@
"styled-components": "^5.3.11",
"use-latest": "^1.2.1",
"viem": "^1.12.2",
"wagmi": "^1.3.7"
"wagmi": "^1.4.5"
},
"devDependencies": {
"@babel/core": "^7.22.5",
+125 -85
View File
@@ -27,20 +27,23 @@ dependencies:
specifier: ^0.31.0
version: 0.31.0
'@dydxprotocol/v4-abacus':
specifier: ^1.0.10
version: 1.0.10
specifier: ^1.1.4
version: 1.1.4
'@dydxprotocol/v4-client-js':
specifier: ^1.0.0
version: 1.0.0
specifier: ^1.0.6
version: 1.0.6
'@dydxprotocol/v4-localization':
specifier: ^1.0.0
version: 1.0.0
specifier: ^1.0.17
version: 1.0.17
'@ethersproject/providers':
specifier: ^5.7.2
version: 5.7.2
'@js-joda/core':
specifier: ^5.5.3
version: 5.5.3
'@radix-ui/react-checkbox':
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-collapsible':
specifier: ^1.0.3
version: 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
@@ -50,6 +53,9 @@ dependencies:
'@radix-ui/react-dropdown-menu':
specifier: ^2.0.5
version: 2.0.5(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-icons':
specifier: ^1.3.0
version: 1.3.0(react@18.2.0)
'@radix-ui/react-navigation-menu':
specifier: ^1.1.3
version: 1.1.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
@@ -197,6 +203,9 @@ dependencies:
qr-code-styling:
specifier: 1.6.0-rc.1
version: 1.6.0-rc.1
query-string:
specifier: ^8.1.0
version: 8.1.0
react:
specifier: ^18.2.0
version: 18.2.0
@@ -234,8 +243,8 @@ dependencies:
specifier: ^1.12.2
version: 1.12.2(typescript@5.1.3)
wagmi:
specifier: ^1.3.7
version: 1.3.7(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2)
specifier: ^1.4.5
version: 1.4.5(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2)
devDependencies:
'@babel/core':
@@ -979,12 +988,12 @@ packages:
resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==}
dev: true
/@dydxprotocol/v4-abacus@1.0.10:
resolution: {integrity: sha512-SdxI/JcyxM1ydtR9ImWujeurea3gbxj37qYnw1wz829BQiZ05uNEtll3xJCI0MjpG+01ARsxVys7SHbpjSxwOg==}
/@dydxprotocol/v4-abacus@1.1.4:
resolution: {integrity: sha512-gml6qheFsPShE9p3FmzFeStYM9ZVhgMq0K0+y12V7pObnMKbumkOIISgCzc47idkah0GMNhiqwi9faD5SjOy/A==}
dev: false
/@dydxprotocol/v4-client-js@1.0.0:
resolution: {integrity: sha512-ehfHO+zQy795TcJRwtiawadFHZyh4HnpJNP26hCGsIjLE5q6LLHweQHpK/1N/sXU1PBOuQwc7iaJnFop6gYauQ==}
/@dydxprotocol/v4-client-js@1.0.6:
resolution: {integrity: sha512-xiWH+kbix+zhI6EsAnd+NDvkjBgxWtGwQmvpd0PjljWNYSFgUtNe5M+piDdRbl2nhy6YWbxAGTwwS3K/ih5qSw==}
dependencies:
'@cosmjs/amino': 0.30.1
'@cosmjs/encoding': 0.31.1
@@ -1011,8 +1020,8 @@ packages:
- utf-8-validate
dev: false
/@dydxprotocol/v4-localization@1.0.0:
resolution: {integrity: sha512-U7QjOlzpfSAVEchkNDB3xYRx7okzdeYi83XsnHwI+EHa2quFm/2Y2dhrBkT5fqpZmhcwxb/vG8AZmFSToOf9hA==}
/@dydxprotocol/v4-localization@1.0.17:
resolution: {integrity: sha512-fybNpi1ono1IdwFQJTszsHbHmGti2BdhTo1nz9KOmKh3tmXdhWObKb64Ve6/6Fjk6h/9+DeZ5ZG4UbYyYMyhCA==}
dev: false
/@dydxprotocol/v4-proto@0.4.1:
@@ -2178,6 +2187,34 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-checkbox@1.0.4(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.22.10
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.14)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.14)(react@18.2.0)
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.14)(react@18.2.0)
'@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.14)(react@18.2.0)
'@radix-ui/react-use-size': 1.0.1(@types/react@18.2.14)(react@18.2.0)
'@types/react': 18.2.14
'@types/react-dom': 18.2.6
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==}
peerDependencies:
@@ -2479,6 +2516,14 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-icons@1.3.0(react@18.2.0):
resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==}
peerDependencies:
react: ^16.x || ^17.x || ^18.x
dependencies:
react: 18.2.0
dev: false
/@radix-ui/react-id@1.0.0(react@18.2.0):
resolution: {integrity: sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==}
peerDependencies:
@@ -5934,26 +5979,12 @@ packages:
pretty-format: 27.5.1
dev: true
/@wagmi/chains@1.4.0(typescript@5.1.3):
resolution: {integrity: sha512-9HwJrhcZ1TxyrCbE10y7s1eSiSiyfGam7AHIOLYExaOX+vpOZ+MNTt4orFEDbEpz1fxwJDPPI38lanAUix1OSA==}
/@wagmi/connectors@3.1.3(@types/react@18.2.14)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2):
resolution: {integrity: sha512-UgwsQKQDFObJVJMf9pDfFoXTv710o4zrTHyhIWKBTMMkLpCMsMxN5+ZaDhBYt/BgoRinfRYQo8uwuwLhxE6Log==}
peerDependencies:
typescript: '>=5.0.4'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
typescript: 5.1.3
dev: false
/@wagmi/connectors@2.6.5(@wagmi/chains@1.4.0)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2):
resolution: {integrity: sha512-klF31togMDd0qQqEcLl5cCGxjMbL0RRXQ8I1vxmEa3KdGzw6Z3ICVzX7/bDfnNEZcOW7BKyAnZDq7rCt5jTOiw==}
peerDependencies:
'@wagmi/chains': '>=1.3.0'
typescript: '>=5.0.4'
viem: '>=0.3.35'
peerDependenciesMeta:
'@wagmi/chains':
optional: true
typescript:
optional: true
dependencies:
@@ -5961,17 +5992,17 @@ packages:
'@ledgerhq/connect-kit-loader': 1.1.2
'@safe-global/safe-apps-provider': 0.17.1(typescript@5.1.3)
'@safe-global/safe-apps-sdk': 8.1.0(typescript@5.1.3)
'@wagmi/chains': 1.4.0(typescript@5.1.3)
'@walletconnect/ethereum-provider': 2.8.6(@walletconnect/modal@2.5.9)
'@walletconnect/ethereum-provider': 2.10.2(@walletconnect/modal@2.6.2)
'@walletconnect/legacy-provider': 2.0.0
'@walletconnect/modal': 2.5.9(react@18.2.0)
'@walletconnect/utils': 2.8.6
'@walletconnect/modal': 2.6.2(@types/react@18.2.14)(react@18.2.0)
'@walletconnect/utils': 2.10.2
abitype: 0.8.7(typescript@5.1.3)
eventemitter3: 4.0.7
typescript: 5.1.3
viem: 1.12.2(typescript@5.1.3)
transitivePeerDependencies:
- '@react-native-async-storage/async-storage'
- '@types/react'
- bufferutil
- encoding
- lokijs
@@ -5981,8 +6012,8 @@ packages:
- zod
dev: false
/@wagmi/core@1.3.6(@types/react@18.2.14)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2):
resolution: {integrity: sha512-TXv9ZlRR5aySfERFuWMuo+lKXC/CoqtxVJZVHPqhK1jY+nldMx3AvrWzzF4CccRaMYcVdvPFepvmxzq2A2VvWg==}
/@wagmi/core@1.4.5(@types/react@18.2.14)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2):
resolution: {integrity: sha512-N9luRb1Uk4tBN9kaYcQSWKE9AsRt/rvZaFt5IZech4JPzNN2sQlfhKd9GEjOXYRDqEPHdDvos7qyBKiDNTz4GA==}
peerDependencies:
typescript: '>=5.0.4'
viem: '>=0.3.35'
@@ -5990,8 +6021,7 @@ packages:
typescript:
optional: true
dependencies:
'@wagmi/chains': 1.4.0(typescript@5.1.3)
'@wagmi/connectors': 2.6.5(@wagmi/chains@1.4.0)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2)
'@wagmi/connectors': 3.1.3(@types/react@18.2.14)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2)
abitype: 0.8.7(typescript@5.1.3)
eventemitter3: 4.0.7
typescript: 5.1.3
@@ -6036,8 +6066,8 @@ packages:
- utf-8-validate
dev: false
/@walletconnect/core@2.8.6:
resolution: {integrity: sha512-rnSqm1KJLcww/v6+UH8JeibQkJ3EKgyUDPfEK0stSEkrIUIcXaFlq3Et8S+vgV8bPhI0MVUhAhFL5OJZ3t2ryg==}
/@walletconnect/core@2.10.2:
resolution: {integrity: sha512-JQz/xp3SLEpTeRQctdck2ugSBVEpMxoSE+lFi2voJkZop1hv6P+uqr6E4PzjFluAjeAnKlT1xvra0aFWjPWVcw==}
dependencies:
'@walletconnect/heartbeat': 1.2.1
'@walletconnect/jsonrpc-provider': 1.0.13
@@ -6050,8 +6080,8 @@ packages:
'@walletconnect/relay-auth': 1.0.4
'@walletconnect/safe-json': 1.0.2
'@walletconnect/time': 1.0.2
'@walletconnect/types': 2.8.6
'@walletconnect/utils': 2.8.6
'@walletconnect/types': 2.10.2
'@walletconnect/utils': 2.10.2
events: 3.3.0
lodash.isequal: 4.5.0
uint8arrays: 3.1.1
@@ -6087,8 +6117,8 @@ packages:
tslib: 1.14.1
dev: false
/@walletconnect/ethereum-provider@2.8.6(@walletconnect/modal@2.5.9):
resolution: {integrity: sha512-wUvJEsXTLmMihrOhQxAs1k9hrWEOT03QBn54P9r9GpJbJ1zEfIjQaXFfi8uup6gldhH+vN38PsbOiLyv/6d3qQ==}
/@walletconnect/ethereum-provider@2.10.2(@walletconnect/modal@2.6.2):
resolution: {integrity: sha512-QMYFZ6+rVq2CJLdIPdKK0j1Qm66UA27oQU5V2SrL8EVwl7wFfm0Bq7fnL+qAWeDpn612dNeNErpk/ROa1zWlWg==}
peerDependencies:
'@walletconnect/modal': '>=2'
peerDependenciesMeta:
@@ -6099,11 +6129,11 @@ packages:
'@walletconnect/jsonrpc-provider': 1.0.13
'@walletconnect/jsonrpc-types': 1.0.3
'@walletconnect/jsonrpc-utils': 1.0.8
'@walletconnect/modal': 2.5.9(react@18.2.0)
'@walletconnect/sign-client': 2.8.6
'@walletconnect/types': 2.8.6
'@walletconnect/universal-provider': 2.8.6
'@walletconnect/utils': 2.8.6
'@walletconnect/modal': 2.6.2(@types/react@18.2.14)(react@18.2.0)
'@walletconnect/sign-client': 2.10.2
'@walletconnect/types': 2.10.2
'@walletconnect/universal-provider': 2.10.2
'@walletconnect/utils': 2.10.2
events: 3.3.0
transitivePeerDependencies:
- '@react-native-async-storage/async-storage'
@@ -6254,32 +6284,34 @@ packages:
tslib: 1.14.1
dev: false
/@walletconnect/modal-core@2.5.9(react@18.2.0):
resolution: {integrity: sha512-isIebwF9hOknGouhS/Ob4YJ9Sa/tqNYG2v6Ua9EkCqIoLimepkG5eC53tslUWW29SLSfQ9qqBNG2+iE7yQXqgw==}
/@walletconnect/modal-core@2.6.2(@types/react@18.2.14)(react@18.2.0):
resolution: {integrity: sha512-cv8ibvdOJQv2B+nyxP9IIFdxvQznMz8OOr/oR/AaUZym4hjXNL/l1a2UlSQBXrVjo3xxbouMxLb3kBsHoYP2CA==}
dependencies:
buffer: 6.0.3
valtio: 1.10.6(react@18.2.0)
valtio: 1.11.2(@types/react@18.2.14)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
- react
dev: false
/@walletconnect/modal-ui@2.5.9(react@18.2.0):
resolution: {integrity: sha512-nfBaAT9Ls7RZTBBgAq+Nt/3AoUcinIJ9bcq5UHXTV3lOPu/qCKmUC/0HY3GvUK8ykabUAsjr0OAGmcqkB91qug==}
/@walletconnect/modal-ui@2.6.2(@types/react@18.2.14)(react@18.2.0):
resolution: {integrity: sha512-rbdstM1HPGvr7jprQkyPggX7rP4XiCG85ZA+zWBEX0dVQg8PpAgRUqpeub4xQKDgY7pY/xLRXSiCVdWGqvG2HA==}
dependencies:
'@walletconnect/modal-core': 2.5.9(react@18.2.0)
lit: 2.7.5
'@walletconnect/modal-core': 2.6.2(@types/react@18.2.14)(react@18.2.0)
lit: 2.8.0
motion: 10.16.2
qrcode: 1.5.3
transitivePeerDependencies:
- '@types/react'
- react
dev: false
/@walletconnect/modal@2.5.9(react@18.2.0):
resolution: {integrity: sha512-Zs2RvPwbBNRdBhb50FuJCxi3FJltt1KSpI7odjU/x9GTpTOcSOkmR66PBCy2JvNA0+ztnS1Xs0LVEr3lu7/Jzw==}
/@walletconnect/modal@2.6.2(@types/react@18.2.14)(react@18.2.0):
resolution: {integrity: sha512-eFopgKi8AjKf/0U4SemvcYw9zlLpx9njVN8sf6DAkowC2Md0gPU/UNEbH1Wwj407pEKnEds98pKWib1NN1ACoA==}
dependencies:
'@walletconnect/modal-core': 2.5.9(react@18.2.0)
'@walletconnect/modal-ui': 2.5.9(react@18.2.0)
'@walletconnect/modal-core': 2.6.2(@types/react@18.2.14)(react@18.2.0)
'@walletconnect/modal-ui': 2.6.2(@types/react@18.2.14)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
- react
dev: false
@@ -6335,17 +6367,17 @@ packages:
- utf-8-validate
dev: false
/@walletconnect/sign-client@2.8.6:
resolution: {integrity: sha512-rOFTKTHP7oJfXgYHX7+SdB8VbcsEE3ZFG/bMdmZboWaBim1mrY3vUyDdKrNr0VgI3AwBiEQezQDfKxBX0pMSQQ==}
/@walletconnect/sign-client@2.10.2:
resolution: {integrity: sha512-vviSLV3f92I0bReX+OLr1HmbH0uIzYEQQFd1MzIfDk9PkfFT/LLAHhUnDaIAMkIdippqDcJia+5QEtT4JihL3Q==}
dependencies:
'@walletconnect/core': 2.8.6
'@walletconnect/core': 2.10.2
'@walletconnect/events': 1.0.1
'@walletconnect/heartbeat': 1.2.1
'@walletconnect/jsonrpc-utils': 1.0.8
'@walletconnect/logger': 2.0.1
'@walletconnect/time': 1.0.2
'@walletconnect/types': 2.8.6
'@walletconnect/utils': 2.8.6
'@walletconnect/types': 2.10.2
'@walletconnect/utils': 2.10.2
events: 3.3.0
transitivePeerDependencies:
- '@react-native-async-storage/async-storage'
@@ -6374,8 +6406,8 @@ packages:
- lokijs
dev: false
/@walletconnect/types@2.8.6:
resolution: {integrity: sha512-Z/PFa3W1XdxeTcCtdR6lUsFgZfU/69wWJBPyclPwn7cu1+eriuCr6XZXQpJjib3flU+HnwHiXeUuqZaheehPxw==}
/@walletconnect/types@2.10.2:
resolution: {integrity: sha512-luNV+07Wdla4STi9AejseCQY31tzWKQ5a7C3zZZaRK/di+rFaAAb7YW04OP4klE7tw/mJRGPTlekZElmHxO8kQ==}
dependencies:
'@walletconnect/events': 1.0.1
'@walletconnect/heartbeat': 1.2.1
@@ -6388,17 +6420,17 @@ packages:
- lokijs
dev: false
/@walletconnect/universal-provider@2.8.6:
resolution: {integrity: sha512-ln1RVv8+oHu9enOJ/oVkjiarneB+4vJCk16znOklIN2JtDHwB8iObDHlQH3UE6ynNTw1iRvaGuPR4g+YdIfB6w==}
/@walletconnect/universal-provider@2.10.2:
resolution: {integrity: sha512-wFgI0LbQ3D56sgaUMsgOHCM5m8WLxiC71BGuCKQfApgsbNMVKugYVy2zWHyUyi8sqTQHI+uSaVpDev4UHq9LEw==}
dependencies:
'@walletconnect/jsonrpc-http-connection': 1.0.7
'@walletconnect/jsonrpc-provider': 1.0.13
'@walletconnect/jsonrpc-types': 1.0.3
'@walletconnect/jsonrpc-utils': 1.0.8
'@walletconnect/logger': 2.0.1
'@walletconnect/sign-client': 2.8.6
'@walletconnect/types': 2.8.6
'@walletconnect/utils': 2.8.6
'@walletconnect/sign-client': 2.10.2
'@walletconnect/types': 2.10.2
'@walletconnect/utils': 2.10.2
events: 3.3.0
transitivePeerDependencies:
- '@react-native-async-storage/async-storage'
@@ -6430,8 +6462,8 @@ packages:
- lokijs
dev: false
/@walletconnect/utils@2.8.6:
resolution: {integrity: sha512-wcy6e5+COYo7tfNnW8YqidnATdJDIW6vDiWWE7A1F78Sl/VflkaevB9cIgyn8eLdxC1SxXgGoeC2oLP90nnHJg==}
/@walletconnect/utils@2.10.2:
resolution: {integrity: sha512-syxXRpc2yhSknMu3IfiBGobxOY7fLfLTJuw+ppKaeO6WUdZpIit3wfuGOcc0Ms3ZPFCrGfyGOoZsCvgdXtptRg==}
dependencies:
'@stablelib/chacha20poly1305': 1.0.1
'@stablelib/hkdf': 1.0.1
@@ -6441,7 +6473,7 @@ packages:
'@walletconnect/relay-api': 1.0.9
'@walletconnect/safe-json': 1.0.2
'@walletconnect/time': 1.0.2
'@walletconnect/types': 2.8.6
'@walletconnect/types': 2.10.2
'@walletconnect/window-getters': 1.0.1
'@walletconnect/window-metadata': 1.0.1
detect-browser: 5.3.0
@@ -7875,7 +7907,6 @@ packages:
/decode-uri-component@0.4.1:
resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==}
engines: {node: '>=14.16'}
dev: true
/deep-eql@4.1.3:
resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==}
@@ -8945,7 +8976,6 @@ packages:
/filter-obj@5.1.0:
resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==}
engines: {node: '>=14.16'}
dev: true
/find-cache-dir@3.3.2:
resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==}
@@ -10372,6 +10402,14 @@ packages:
lit-html: 2.8.0
dev: false
/lit@2.8.0:
resolution: {integrity: sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==}
dependencies:
'@lit/reactive-element': 1.6.3
lit-element: 3.3.3
lit-html: 2.8.0
dev: false
/loader-runner@4.3.0:
resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==}
engines: {node: '>=6.11.5'}
@@ -11876,7 +11914,6 @@ packages:
decode-uri-component: 0.4.1
filter-obj: 5.1.0
split-on-first: 3.0.0
dev: true
/queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@@ -12701,7 +12738,6 @@ packages:
/split-on-first@3.0.0:
resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==}
engines: {node: '>=12'}
dev: true
/split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
@@ -13507,15 +13543,19 @@ packages:
use-sync-external-store: 1.2.0(react@18.2.0)
dev: false
/valtio@1.10.6(react@18.2.0):
resolution: {integrity: sha512-SxN1bHUmdhW6V8qsQTpCgJEwp7uHbntuH0S9cdLQtiohuevwBksbpXjwj5uDMA7bLwg1WKyq9sEpZrx3TIMrkA==}
/valtio@1.11.2(@types/react@18.2.14)(react@18.2.0):
resolution: {integrity: sha512-1XfIxnUXzyswPAPXo1P3Pdx2mq/pIqZICkWN60Hby0d9Iqb+MEIpqgYVlbflvHdrp2YR/q3jyKWRPJJ100yxaw==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=16.8'
react: '>=16.8'
peerDependenciesMeta:
'@types/react':
optional: true
react:
optional: true
dependencies:
'@types/react': 18.2.14
proxy-compare: 2.5.1
react: 18.2.0
use-sync-external-store: 1.2.0(react@18.2.0)
@@ -13738,8 +13778,8 @@ packages:
- supports-color
dev: true
/wagmi@1.3.7(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2):
resolution: {integrity: sha512-n4Qwby0QB8/JSGjc0gGn4MWZ0BT229agEg6saPGS6VswmPjJLdkLq9qxKn1JptXxPREmb/ZvwHxU++3Z+gQJVA==}
/wagmi@1.4.5(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2):
resolution: {integrity: sha512-Ph62E6cO5n2Z8Z5LTyZrkaNprxTsbC4w0qZJT4OJdXrEELziI8z/b4FO6amVFXdu2rDp/wpvF56e4mhKC8/Kdw==}
peerDependencies:
react: '>=17.0.0'
typescript: '>=5.0.4'
@@ -13751,7 +13791,7 @@ packages:
'@tanstack/query-sync-storage-persister': 4.32.6
'@tanstack/react-query': 4.32.6(react-dom@18.2.0)(react@18.2.0)
'@tanstack/react-query-persist-client': 4.32.6(@tanstack/react-query@4.32.6)
'@wagmi/core': 1.3.6(@types/react@18.2.14)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2)
'@wagmi/core': 1.4.5(@types/react@18.2.14)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2)
abitype: 0.8.7(typescript@5.1.3)
react: 18.2.0
typescript: 5.1.3
+61
View File
@@ -0,0 +1,61 @@
[
{
"chainId": "42161",
"tokenAddress": "0xff970a61a04b1ca14834a43f5de4533ebddb5cc8",
"name": "Arbitrum"
},
{
"chainId": "43114",
"tokenAddress": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
"name": "Avalanche"
},
{
"chainId": "8453",
"tokenAddress": "0x66627F389ae46D881773B7131139b2411980E09E",
"name": "Base"
},
{
"chainId": "1",
"tokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"name": "Ethereum"
},
{
"chainId": "10",
"tokenAddress": "0x7F5c764cBc14f9669B88837ca1490cCa17c31607",
"name": "OP Mainnet"
},
{
"chainId": "421613",
"tokenAddress": "0xfd064a18f3bf249cf1f87fc203e90d8f650f2d63",
"name": "Arbitrum Goerli"
},
{
"chainId": "43113",
"tokenAddress": "0x5425890298aed601595a70AB815c96711a31Bc65",
"name": "Avalanche Fuji"
},
{
"chainId": "84531",
"tokenAddress": "0xf175520c52418dfe19c8098071a252da48cd1c19",
"name": "Base Goerli"
},
{
"chainId": "5",
"tokenAddress": "0x07865c6E87B9F70255377e024ace6630C1Eaa37F",
"name": "Ethereum Goerli"
},
{
"chainId": "grand-1",
"tokenAddress": "uusdc",
"name": "Noble Testnet"
},
{
"chainId": "420",
"tokenAddress": "0xe05606174bac4a6364b31bd0eca4bf4dd368f8c6",
"name": "OP Goerli"
},
{
"chainId": "103",
"name": "Solana Devnet"
}
]
+102 -9
View File
@@ -1,4 +1,9 @@
{
"apps": {
"ios": {
"scheme": "dydxv4"
}
},
"deployments": {
"MAINNET": {
"environments": [
@@ -66,6 +71,7 @@
"https://validator.v4dev.dydx.exchange"
],
"0xsquid": "https://testnet.api.0xsquid.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love",
"faucet": "https://faucet.v4dev.dydx.exchange"
},
"links": {
@@ -138,7 +144,8 @@
"validators": [
"http://54.92.118.111"
],
"0xsquid": "https://testnet.api.0xsquid.com"
"0xsquid": "https://testnet.api.0xsquid.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love"
},
"links": {
"tos": "https://dydx.exchange/v4-terms",
@@ -203,14 +210,15 @@
"endpoints": {
"indexers": [
{
"api": "http://indexer.v4dev4.dydx.exchange",
"socket": "ws://indexer.v4dev4.dydx.exchange"
"api": "https://indexer.v4dev4.dydx.exchange",
"socket": "wss://indexer.v4dev4.dydx.exchange"
}
],
"validators": [
"http://validator.v4dev4.dydx.exchange"
],
"0xsquid": "https://testnet.api.0xsquid.com"
"0xsquid": "https://testnet.api.0xsquid.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love"
},
"links": {
"tos": "https://dydx.exchange/v4-terms",
@@ -282,7 +290,8 @@
"validators": [
"http://18.223.78.50"
],
"0xsquid": "https://testnet.api.0xsquid.com"
"0xsquid": "https://testnet.api.0xsquid.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love"
},
"links": {
"tos": "https://dydx.exchange/v4-terms",
@@ -355,7 +364,8 @@
"validators": [
"https://validator.v4staging.dydx.exchange"
],
"0xsquid": "https://testnet.api.squidrouter.com"
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love"
},
"links": {
"tos": "https://dydx.exchange/v4-terms",
@@ -395,6 +405,79 @@
"signTypedDataDomainName": "dYdX Chain"
}
},
"dydxprotocol-staging-forced-update": {
"name": "v4 Staging Forced Update",
"ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
"chain": {
"name": "Dv4TNT",
"denom": "adv4tnt",
"decimals": 18,
"image": "/currencies/dydx.png"
},
"usdc": {
"name": "USDC",
"denom": "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5",
"gasDenom": "uusdc",
"decimals": 6,
"image": "/currencies/usdc.png"
}
},
"endpoints": {
"indexers": [
{
"api": "https://indexer.v4staging.dydx.exchange",
"socket": "wss://indexer.v4staging.dydx.exchange"
}
],
"faucet": "https://faucet.v4staging.dydx.exchange",
"validators": [
"https://validator.v4staging.dydx.exchange"
],
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love"
},
"links": {
"tos": "https://dydx.exchange/v4-terms",
"privacy": "https://dydx.exchange/privacy",
"mintscan": "https://testnet.mintscan.io/dydx-testnet/txs/{tx_hash}",
"mintscanBase": "https://testnet.mintscan.io/dydx-testnet",
"documentation": "https://v4-teacher.vercel.app/",
"community": "https://discord.com/invite/dydx",
"feedback": "https://docs.google.com/forms/d/e/1FAIpQLSezLsWCKvAYDEb7L-2O4wOON1T56xxro9A2Azvl6IxXHP_15Q/viewform",
"blogs": "https://www.dydx.foundation/blog"
},
"wallets": {
"walletconnect": {
"client": {
"name": "dYdX v4",
"description": "dYdX v4 App",
"iconUrl": "/logos/dydx-x.png"
},
"v2": {
"projectId": "47559b2ec96c09aed9ff2cb54a31ab0e"
}
},
"walletSegue": {
"callbackUrl": "/walletsegue"
},
"images": "/wallets/",
"signTypedDataAction": "dYdX Chain Onboarding",
"signTypedDataDomainName": "dYdX Chain"
},
"apps": {
"ios": {
"minimalVersion": "1.0",
"build":40000,
"url": "https://apps.apple.com/app/dydx/id1564787350"
}
}
},
"dydxprotocol-staging-west": {
"name": "v4 Staging West",
"ethereumChainId": "5",
@@ -429,7 +512,8 @@
"validators": [
"https://validator-uswest1.v4staging.dydx.exchange"
],
"0xsquid": "https://testnet.api.squidrouter.com"
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love"
},
"links": {
"tos": "https://dydx.exchange/v4-terms",
@@ -507,6 +591,7 @@
"https://dydx-rpc.liquify.com/api=8878132/dydx"
],
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"links": {
@@ -581,6 +666,7 @@
"https://validator.v4testnet.dydx.exchange"
],
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"links": {
@@ -656,6 +742,7 @@
"https://dydx-testnet.nodefleet.org"
],
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"links": {
@@ -731,6 +818,7 @@
"https://test-dydx.kingnodes.com"
],
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"links": {
@@ -806,6 +894,7 @@
"https://dydx-rpc.liquify.com/api=8878132/dydx"
],
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"links": {
@@ -881,6 +970,7 @@
"https://dydx-testnet-rpc.polkachu.com/"
],
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"links": {
@@ -888,6 +978,7 @@
"privacy": "https://dydx.exchange/privacy",
"statusPage": "https://status.v4testnet.dydx.exchange/",
"mintscan": "https://testnet.mintscan.io/dydx-testnet/txs/{tx_hash}",
"mintscanBase": "https://testnet.mintscan.io/dydx-testnet",
"documentation": "https://docs.dydx.exchange/",
"community": "https://discord.com/invite/dydx",
"feedback": "https://docs.google.com/forms/d/e/1FAIpQLSezLsWCKvAYDEb7L-2O4wOON1T56xxro9A2Azvl6IxXHP_15Q/viewform",
@@ -946,6 +1037,7 @@
"https://dydx-testnet-full-rpc.public.blastapi.io/"
],
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"links": {
@@ -1022,13 +1114,14 @@
"[Validator endpoint 1",
"[Validator endpoint n]"
],
"0xsquid": "[0xSquid endpoint for mainnet]"
"0xsquid": "[0xSquid endpoint for mainnet]",
"nobleValidator": "[noble validator endpoint for mainnet]"
},
"links": {
"tos": "[HTTP link to TOS]",
"privacy": "[HTTP link to Privacy Policy]",
"mintscan": "[HTTP link to Mintscan, with {tx_hash} placeholder]",
"mintscanBase": "[HTTP link to TOS mintsacn base url]",
"mintscanBase": "[HTTP link to TOS mintscan base url]",
"feedback": "[HTTP link to feedback form, can be null]",
"blogs": "[HTTP link to blogs, can be null]",
"foundation": "[HTTP link to foundation, can be null]",
+1 -1
View File
@@ -199,7 +199,7 @@
},
"mobile": {
"native": "trust:",
"universal": "https://link.trustwallet.com"
"universal": ""
},
"desktop": {
"native": "",
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 300 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 817 B

After

Width:  |  Height:  |  Size: 297 B

+2 -2
View File
@@ -22,7 +22,7 @@ import { NotificationsProvider } from '@/hooks/useNotifications';
import { LocalNotificationsProvider } from '@/hooks/useLocalNotifications';
import { RestrictionProvider } from '@/hooks/useRestrictions';
import { SubaccountProvider } from '@/hooks/useSubaccount';
import { SquidProvider } from '@/hooks/useSquid';
import { TestFlagsProvider } from '@/hooks/useTestFlags';
import { GuardedMobileRoute } from '@/components/GuardedMobileRoute';
@@ -123,12 +123,12 @@ const providers = [
wrapProvider(QueryClientProvider, { client: queryClient }),
wrapProvider(GrazProvider),
wrapProvider(WagmiConfig, { config }),
wrapProvider(TestFlagsProvider),
wrapProvider(LocaleProvider),
wrapProvider(RestrictionProvider),
wrapProvider(DydxProvider),
wrapProvider(AccountsProvider),
wrapProvider(SubaccountProvider),
wrapProvider(SquidProvider),
wrapProvider(LocalNotificationsProvider),
wrapProvider(NotificationsProvider),
wrapProvider(DialogAreaProvider),
+390
View File
@@ -0,0 +1,390 @@
[
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [{ "name": "", "type": "string" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "_upgradedAddress", "type": "address" }],
"name": "deprecate",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_spender", "type": "address" },
{ "name": "_value", "type": "uint256" }
],
"name": "approve",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "deprecated",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "_evilUser", "type": "address" }],
"name": "addBlackList",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_from", "type": "address" },
{ "name": "_to", "type": "address" },
{ "name": "_value", "type": "uint256" }
],
"name": "transferFrom",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "upgradedAddress",
"outputs": [{ "name": "", "type": "address" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "", "type": "address" }],
"name": "balances",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "maximumFee",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "_totalSupply",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "unpause",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "_maker", "type": "address" }],
"name": "getBlackListStatus",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "name": "", "type": "address" },
{ "name": "", "type": "address" }
],
"name": "allowed",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "paused",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "who", "type": "address" }],
"name": "balanceOf",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "pause",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getOwner",
"outputs": [{ "name": "", "type": "address" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "owner",
"outputs": [{ "name": "", "type": "address" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [{ "name": "", "type": "string" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_to", "type": "address" },
{ "name": "_value", "type": "uint256" }
],
"name": "transfer",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "newBasisPoints", "type": "uint256" },
{ "name": "newMaxFee", "type": "uint256" }
],
"name": "setParams",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "amount", "type": "uint256" }],
"name": "issue",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "amount", "type": "uint256" }],
"name": "redeem",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "name": "_owner", "type": "address" },
{ "name": "_spender", "type": "address" }
],
"name": "allowance",
"outputs": [{ "name": "remaining", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "basisPointsRate",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "", "type": "address" }],
"name": "isBlackListed",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "_clearedUser", "type": "address" }],
"name": "removeBlackList",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "MAX_UINT",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "newOwner", "type": "address" }],
"name": "transferOwnership",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "_blackListedUser", "type": "address" }],
"name": "destroyBlackFunds",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "name": "_initialSupply", "type": "uint256" },
{ "name": "_name", "type": "string" },
{ "name": "_symbol", "type": "string" },
{ "name": "_decimals", "type": "uint256" }
],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "name": "amount", "type": "uint256" }],
"name": "Issue",
"type": "event"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "name": "amount", "type": "uint256" }],
"name": "Redeem",
"type": "event"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "name": "newAddress", "type": "address" }],
"name": "Deprecate",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": false, "name": "feeBasisPoints", "type": "uint256" },
{ "indexed": false, "name": "maxFee", "type": "uint256" }
],
"name": "Params",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": false, "name": "_blackListedUser", "type": "address" },
{ "indexed": false, "name": "_balance", "type": "uint256" }
],
"name": "DestroyedBlackFunds",
"type": "event"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "name": "_user", "type": "address" }],
"name": "AddedBlackList",
"type": "event"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "name": "_user", "type": "address" }],
"name": "RemovedBlackList",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": true, "name": "owner", "type": "address" },
{ "indexed": true, "name": "spender", "type": "address" },
{ "indexed": false, "name": "value", "type": "uint256" }
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": true, "name": "from", "type": "address" },
{ "indexed": true, "name": "to", "type": "address" },
{ "indexed": false, "name": "value", "type": "uint256" }
],
"name": "Transfer",
"type": "event"
},
{ "anonymous": false, "inputs": [], "name": "Pause", "type": "event" },
{ "anonymous": false, "inputs": [], "name": "Unpause", "type": "event" }
]
+1 -1
View File
@@ -61,7 +61,7 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
return (
<StyledBaseButton
disabled={state[ButtonState.Disabled]}
disabled={state[ButtonState.Disabled] || state[ButtonState.Loading]}
{...{ ref, action, size, shape, state, ...otherProps }}
>
{
+3 -1
View File
@@ -13,7 +13,9 @@ export const Checkboxes: Story<CheckboxProps> = (args) => {
<Checkbox
{...args}
checked={isChecked}
onClick={(e: React.ChangeEvent) => setIsChecked((e.target as HTMLInputElement).checked)}
onCheckedChange={setIsChecked}
id="story-checkbox"
label="example label"
/>
</StoryWrapper>
);
+45 -72
View File
@@ -1,10 +1,14 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { Root, Indicator } from '@radix-ui/react-checkbox';
import { CheckIcon } from '@radix-ui/react-icons';
import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins';
type ElementProps = {
checked: boolean;
onClick: (e: React.ChangeEvent) => void;
onCheckedChange: (checked: boolean) => void;
id?: string;
label?: React.ReactNode;
};
type StyleProps = {
@@ -13,85 +17,54 @@ type StyleProps = {
export type CheckboxProps = ElementProps & StyleProps;
export const Checkbox: React.FC<CheckboxProps> = ({ checked, className, onClick }) => {
return (
<Styled.CheckboxWrapper className={className}>
<Styled.Checkbox
type="checkbox"
checked={checked}
onChange={onClick}
/>
<Styled.CustomCheckbox />
</Styled.CheckboxWrapper>
);
};
export const Checkbox: React.FC<CheckboxProps> = ({
checked,
className,
onCheckedChange,
id,
label,
}) => (
<Styled.Container>
<Styled.Root className={className} checked={checked} onCheckedChange={onCheckedChange} id={id}>
<Styled.Indicator>
<CheckIcon />
</Styled.Indicator>
</Styled.Root>
{label && <label htmlFor={id}>{label}</label>}
</Styled.Container>
);
const Styled: Record<string, AnyStyledComponent> = {};
Styled.CustomCheckbox = styled.span`
width: 1.25em;
height: 1.25em;
Styled.Container = styled.div`
${layoutMixins.row}
gap: 1ch;
font: var(--font-small-book);
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
label {
cursor: pointer;
}
`;
Styled.Root = styled(Root)`
--checkbox-backgroundColor: var(--color-layer-0);
min-width: 1.25rem;
height: 1.25rem;
border-radius: 0.375rem;
border: var(--border-width) solid var(--color-border);
background-color: var(--checkbox-backgroundColor);
&::after {
position: absolute;
content: '';
top: 0.25em;
left: 0.4375em;
width: 0.3125em;
height: 0.5em;
border: solid var(--checkbox-checkColor);
border-width: 0 0.125em 0.125em 0;
border-radius: 0.0625em;
opacity: 0;
transform: rotate(0deg) scale(0);
transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
}
@media ${breakpoints.tablet} {
width: 1.5em;
height: 1.5em;
&::after {
top: 0.25em;
left: 0.4375em;
width: 0.4375em;
height: 0.625em;
}
&[data-state='checked'] {
--checkbox-backgroundColor: var(--color-accent);
}
`;
Styled.CheckboxWrapper = styled.div`
--checkbox-backgroundColor: var(--color-layer-1);
--checkbox-checkColor: var(--color-text-1);
Styled.Indicator = styled(Indicator)`
display: flex;
border-radius: 0.25em;
overflow: hidden;
position: relative;
cursor: pointer;
align-items: center;
justify-content: center;
> input:checked ~ ${Styled.CustomCheckbox}::after {
opacity: 1;
transform: rotate(40deg) scale(1);
}
`;
Styled.Checkbox = styled.input`
width: 1.25em;
height: 1.25em;
z-index: 1;
cursor: pointer;
opacity: 0;
@media ${breakpoints.tablet} {
width: 1.5em;
height: 1.5em;
}
color: var(--color-text-2);
`;
+5 -6
View File
@@ -1,16 +1,13 @@
import type { Story } from '@ladle/react';
import { Collapsible } from '@/components/Collapsible';
import { Collapsible, type CollapsibleProps } from '@/components/Collapsible';
import { StoryWrapper } from '.ladle/components';
import { IconName } from './Icon';
export const CollapsibleStory: Story<Parameters<typeof Collapsible>> = (args) => (
export const CollapsibleStory: Story<CollapsibleProps> = (args) => (
<StoryWrapper>
<Collapsible
label="Collapsible List of Items"
{...args}
>
<Collapsible {...args}>
<ul>
<li>Collapsible Item 1</li>
<li>Collapsible Item 2</li>
@@ -23,6 +20,8 @@ export const CollapsibleStory: Story<Parameters<typeof Collapsible>> = (args) =>
CollapsibleStory.args = {
disabled: false,
withTrigger: true,
label: 'Collapsible List of Items',
};
CollapsibleStory.argTypes = {
+24 -18
View File
@@ -14,6 +14,7 @@ type ElementProps = {
label: React.ReactNode;
triggerIcon?: IconName;
children: React.ReactNode;
withTrigger?: boolean;
};
type StyleProps = {
@@ -23,6 +24,8 @@ type StyleProps = {
fullWidth?: boolean;
};
export type CollapsibleProps = ElementProps & StyleProps;
export const Collapsible = ({
defaultOpen = false,
disabled,
@@ -35,26 +38,29 @@ export const Collapsible = ({
triggerIconSide = 'left',
fullWidth,
className,
}: ElementProps & StyleProps) => (
withTrigger = true,
}: CollapsibleProps) => (
<Styled.Root defaultOpen={defaultOpen} open={open} onOpenChange={onOpenChange}>
<Styled.Trigger className={className} disabled={disabled}>
{triggerIconSide === 'right' && (
<>
{label}
{fullWidth && <HorizontalSeparatorFiller />}
</>
)}
{withTrigger && (
<Styled.Trigger className={className} disabled={disabled}>
{triggerIconSide === 'right' && (
<>
{label}
{fullWidth && <HorizontalSeparatorFiller />}
</>
)}
<Styled.TriggerIcon>
<Icon iconName={triggerIcon} />
</Styled.TriggerIcon>
{triggerIconSide === 'left' && (
<>
{fullWidth && <HorizontalSeparatorFiller />}
{label}
</>
)}
</Styled.Trigger>
<Styled.TriggerIcon>
<Icon iconName={triggerIcon} />
</Styled.TriggerIcon>
{triggerIconSide === 'left' && (
<>
{fullWidth && <HorizontalSeparatorFiller />}
{label}
</>
)}
</Styled.Trigger>
)}
<Styled.Content $transitionDuration={transitionDuration}>{children}</Styled.Content>
</Styled.Root>
);
+13 -2
View File
@@ -22,7 +22,12 @@ type PickComboxMenuProps<
MenuGroupValue extends string | number
> = Pick<
ComboboxMenuProps<MenuItemValue, MenuGroupValue>,
'inputPlaceholder' | 'onItemSelected' | 'slotEmpty' | 'withSearch' | 'withStickyLayout'
| 'inputPlaceholder'
| 'onItemSelected'
| 'slotEmpty'
| 'withItemBorders'
| 'withSearch'
| 'withStickyLayout'
>;
type PickDialogProps = Pick<
@@ -53,6 +58,7 @@ export const ComboboxDialogMenu = <
onItemSelected,
inputPlaceholder,
slotEmpty,
withItemBorders,
withSearch,
withStickyLayout = true,
children,
@@ -83,6 +89,7 @@ export const ComboboxDialogMenu = <
title={title}
inputPlaceholder={inputPlaceholder}
slotEmpty={slotEmpty}
withItemBorders={withItemBorders}
withSearch={withSearch}
withStickyLayout={withStickyLayout}
/>
@@ -95,6 +102,8 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.Dialog = styled(Dialog)`
/* Params */
--comboboxDialogMenu-backgroundColor: var(--color-layer-2);
--comboboxDialogMenu-item-gap: 0.5rem;
--comboboxDialogMenu-item-padding: 0.5rem 1rem;
/* Overrides */
& {
@@ -113,10 +122,12 @@ Styled.Dialog = styled(Dialog)`
/* Net 0 sticky top inset (let stickyArea1 header stick to top) */
--stickyArea0-topGap: calc(-1 * var(--stickyArea0-topHeight));
overflow-x: clip;
}
`;
Styled.ComboboxMenu = styled(ComboboxMenu)`
--comboboxMenu-backgroundColor: var(--comboboxDialogMenu-backgroundColor);
--comboboxMenu-item-gap: var(--comboxDialogMenu-item-gap, 0.5rem);
--comboboxMenu-item-gap: var(--comboboxDialogMenu-item-gap);
--comboboxMenu-item-padding: var(--comboboxDialogMenu-item-padding);
`;
+56 -26
View File
@@ -2,7 +2,7 @@ import { Fragment, type ReactNode, useState } from 'react';
import styled, { type AnyStyledComponent, css } from 'styled-components';
import { Command } from 'cmdk';
import { type MenuConfig } from '@/constants/menus';
import { MenuItem, type MenuConfig } from '@/constants/menus';
import { popoverMixins } from '@/styles/popoverMixins';
import { layoutMixins } from '@/styles/layoutMixins';
@@ -20,6 +20,7 @@ type ElementProps<MenuItemValue extends string | number, MenuGroupValue extends
type StyleProps = {
className?: string;
withItemBorders?: boolean;
withStickyLayout?: boolean;
};
@@ -38,17 +39,11 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
withSearch = true,
className,
withItemBorders,
withStickyLayout,
}: ComboboxMenuProps<MenuItemValue, MenuGroupValue>) => {
const [highlightedCommand, setHighlightedCommand] = useState<MenuItemValue>();
const [searchValue, setSearchValue] = useState('');
// const inputRef = useRef<HTMLInputElement | null>(null);
// console.log({ commandValue: highlightedCommand });
// useEffect(() => {
// inputRef?.current?.focus();
// }, []);
return (
<Styled.Command
@@ -65,11 +60,14 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
{withSearch && (
<Styled.Header $withStickyLayout={withStickyLayout}>
<Styled.Input
// ref={inputRef}
/**
* Mobile Issue: Search Input will always trigger mobile keyboard drawer. There is no fix.
* https://github.com/pacocoursey/cmdk/issues/127
*/
autoFocus
type="search"
value={searchValue}
onValueChange={setSearchValue}
autoFocus
placeholder={inputPlaceholder}
/>
</Styled.Header>
@@ -80,6 +78,7 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
<Styled.Group
key={group.group}
heading={group.groupLabel}
$withItemBorders={withItemBorders}
$withStickyLayout={withStickyLayout}
>
{group.items.map((item) => (
@@ -99,22 +98,31 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
}
}}
disabled={item.disabled}
$withItemBorders={withItemBorders}
>
{item.slotBefore}
<Styled.ItemLabel>
<span>
{`${item.label}${item.subitems?.length ? '…' : ''}`}
{item.tag && (
<>
{' '}
<Tag>{item.tag}</Tag>
</>
{
<>
{item.slotBefore}
{item.slotCustomContent ?? (
<Styled.ItemLabel>
<span>
{typeof item.label === 'string'
? `${item.label}${item.subitems?.length ? '…' : ''}`
: item.label}
{item.tag && (
<>
{' '}
<Tag>{item.tag}</Tag>
</>
)}
</span>
{item.description && <span>{item.description}</span>}
</Styled.ItemLabel>
)}
</span>
{item.description && <span>{item.description}</span>}
</Styled.ItemLabel>
{item.slotAfter}
{item.subitems && '→'}
{item.slotAfter}
{item.subitems && '→'}
</>
}
</Styled.Item>
{searchValue &&
@@ -137,6 +145,7 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
onItemSelected?.();
}}
disabled={subitem.disabled}
$withItemBorders={withItemBorders}
>
{subitem.slotBefore}
<Styled.ItemLabel>
@@ -169,14 +178,17 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.Command = styled(Command)<{ $withStickyLayout?: boolean }>`
--comboboxMenu-backgroundColor: var(--color-layer-2);
--comboboxMenu-input-backgroundColor: var(--color-layer-3);
--comboboxMenu-input-height: 2.5rem;
--comboboxMenu-item-checked-backgroundColor: ;
--comboboxMenu-item-checked-textColor: ;
--comboboxMenu-item-highlighted-backgroundColor: var(--color-layer-3);
--comboboxMenu-item-highlighted-textColor: var(--color-text-1);
--comboboxMenu-item-backgroundColor: ;
--comboboxMenu-item-gap: 0.5rem;
--comboboxMenu-item-padding: 0.5em 1em;
display: grid;
align-content: start;
@@ -225,7 +237,7 @@ Styled.Input = styled(Command.Input)`
gap: 0.5rem;
`;
Styled.Group = styled(Command.Group)<{ $withStickyLayout?: boolean }>`
Styled.Group = styled(Command.Group)<{ $withItemBorders?: boolean; $withStickyLayout?: boolean }>`
color: var(--color-text-0);
> [cmdk-group-heading] {
@@ -242,12 +254,21 @@ Styled.Group = styled(Command.Group)<{ $withStickyLayout?: boolean }>`
> [cmdk-group-heading] {
${layoutMixins.stickyHeader}
z-index: 2;
}
> [cmdk-group-items] {
${layoutMixins.stickyArea3}
}
`}
${({ $withItemBorders }) =>
$withItemBorders &&
css`
> [cmdk-group-items] {
padding: var(--border-width) 0;
}
`}
`;
Styled.List = styled(Command.List)<{ $withStickyLayout?: boolean }>`
@@ -274,13 +295,14 @@ Styled.List = styled(Command.List)<{ $withStickyLayout?: boolean }>`
`}
`;
Styled.Item = styled(Command.Item)`
Styled.Item = styled(Command.Item)<{ $withItemBorders?: boolean }>`
${layoutMixins.scrollSnapItem}
${popoverMixins.item}
--item-checked-backgroundColor: var(--comboboxMenu-item-checked-backgroundColor);
--item-checked-textColor: var(--comboboxMenu-item-checked-textColor);
--item-highlighted-textColor: var(--comboboxMenu-item-highlighted-textColor);
--item-gap: var(--comboboxMenu-item-gap);
--item-padding: var(--comboboxMenu-item-padding);
background-color: var(--comboboxMenu-backgroundColor, inherit);
@@ -291,6 +313,12 @@ Styled.Item = styled(Command.Item)`
opacity: 0.75;
cursor: not-allowed;
}
${({ $withItemBorders }) =>
$withItemBorders &&
css`
${layoutMixins.withOuterBorder}
`}
`;
Styled.ItemLabel = styled.div`
@@ -309,6 +337,8 @@ Styled.ItemLabel = styled.div`
opacity: 0.8;
}
}
min-width: 0;
`;
Styled.Empty = styled(Command.Empty)`
+3 -3
View File
@@ -15,10 +15,10 @@ CopyButtonStory.args = {
};
CopyButtonStory.argTypes = {
shownAsText: {
options: [true, false],
buttonType: {
options: ["text", "icon", "default"],
control: { type: 'select' },
defaultValue: false,
defaultValue: "default",
},
children: {
options: ['some text to copy'],
+41 -4
View File
@@ -9,14 +9,21 @@ import { layoutMixins } from '@/styles/layoutMixins';
import { Button, ButtonProps } from './Button';
import { Icon, IconName } from './Icon';
import { IconButton } from './IconButton';
import { WithTooltip } from './WithTooltip';
export type CopyButtonProps = {
value?: string;
shownAsText?: boolean;
buttonType?: 'text' | 'icon' | 'default';
children?: React.ReactNode;
} & ButtonProps;
export const CopyButton = ({ value, shownAsText, children, ...buttonProps }: CopyButtonProps) => {
export const CopyButton = ({
value,
buttonType = 'default',
children,
...buttonProps
}: CopyButtonProps) => {
const stringGetter = useStringGetter();
const [copied, setCopied] = useState(false);
@@ -28,11 +35,23 @@ export const CopyButton = ({ value, shownAsText, children, ...buttonProps }: Cop
setTimeout(() => setCopied(false), 500);
};
return shownAsText ? (
return buttonType === 'text' ? (
<Styled.InlineRow onClick={onCopy} copied={copied}>
{children}
<Icon iconName={IconName.Copy} />
<Styled.Icon copied={copied} iconName={copied ? IconName.Check : IconName.Copy} />
</Styled.InlineRow>
) : buttonType === 'icon' ? (
<WithTooltip
tooltipString={stringGetter({ key: copied ? STRING_KEYS.COPIED : STRING_KEYS.COPY })}
>
<Styled.IconButton
{...buttonProps}
copied={copied}
action={ButtonAction.Base}
iconName={copied ? IconName.Check : IconName.Copy}
onClick={onCopy}
/>
</WithTooltip>
) : (
<Button
{...buttonProps}
@@ -63,3 +82,21 @@ Styled.InlineRow = styled.div<{ copied: boolean }>`
}
`}
`;
Styled.Icon = styled(Icon)<{ copied: boolean }>`
${({ copied }) =>
copied &&
css`
color: var(--color-positive);
`}
`;
Styled.IconButton = styled(IconButton)<{ copied: boolean }>`
${({ copied }) =>
copied &&
css`
svg {
color: var(--color-positive);
}
`}
`;
+38 -28
View File
@@ -38,6 +38,7 @@ type ElementProps = {
slotTrigger?: React.ReactNode;
slotHeaderInner?: React.ReactNode;
slotFooter?: React.ReactNode;
withClose?: boolean;
};
type StyleProps = {
@@ -46,6 +47,7 @@ type StyleProps = {
hasHeaderBorder?: boolean;
children?: React.ReactNode;
className?: string;
withAnimation?: boolean;
};
export type DialogProps = ElementProps & StyleProps;
@@ -79,9 +81,11 @@ export const Dialog = ({
slotTrigger,
slotHeaderInner,
slotFooter,
withClose = true,
placement = DialogPlacement.Default,
portalContainer,
hasHeaderBorder = false,
withAnimation = false,
children,
className,
}: DialogProps) => {
@@ -105,6 +109,7 @@ export const Dialog = ({
e.preventDefault();
}
}}
$withAnimation={withAnimation}
>
<Styled.Header $withBorder={hasHeaderBorder}>
<Styled.HeaderTopRow>
@@ -114,7 +119,7 @@ export const Dialog = ({
{title && <Styled.Title>{title}</Styled.Title>}
{!preventClose && (
{!preventClose && withClose && (
<Styled.Close ref={closeButtonRef}>
<Icon iconName={IconName.Close} />
</Styled.Close>
@@ -168,7 +173,7 @@ Styled.Overlay = styled(Overlay)`
}
`;
Styled.Container = styled(Content)<{ placement: DialogPlacement }>`
Styled.Container = styled(Content)<{ placement: DialogPlacement; $withAnimation?: boolean }>`
/* Params */
--dialog-inset: 1rem;
--dialog-width: 30rem;
@@ -224,7 +229,7 @@ Styled.Container = styled(Content)<{ placement: DialogPlacement }>`
outline: none;
${({ placement }) =>
${({ placement, $withAnimation }) =>
({
[DialogPlacement.Default]: css`
inset: var(--dialog-inset);
@@ -260,9 +265,11 @@ Styled.Container = styled(Content)<{ placement: DialogPlacement }>`
padding-bottom: var(--dialog-radius); */
}
@media (prefers-reduced-motion: no-preference) {
&[data-state='open'] {
animation: ${keyframes`
${$withAnimation &&
css`
@media (prefers-reduced-motion: no-preference) {
&[data-state='open'] {
animation: ${keyframes`
from {
opacity: 0;
}
@@ -270,18 +277,19 @@ Styled.Container = styled(Content)<{ placement: DialogPlacement }>`
max-height: 0;
}
`} 0.15s var(--ease-out-expo);
}
}
&[data-state='closed'] {
animation: ${keyframes`
&[data-state='closed'] {
animation: ${keyframes`
to {
opacity: 0;
scale: 0.9;
max-height: 0;
}
`} 0.15s;
}
}
}
`}
`,
[DialogPlacement.Sidebar]: css`
--dialog-width: var(--sidebar-width);
@@ -291,50 +299,52 @@ Styled.Container = styled(Content)<{ placement: DialogPlacement }>`
margin-left: auto;
}
@media (prefers-reduced-motion: no-preference) {
&[data-state='open'] {
animation: ${keyframes`
${$withAnimation &&
css`
@media (prefers-reduced-motion: no-preference) {
&[data-state='open'] {
animation: ${keyframes`
from {
translate: 100% 0;
opacity: 0;
}
`} 0.15s var(--ease-out-expo);
}
}
&[data-state='closed'] {
animation: ${keyframes`
&[data-state='closed'] {
animation: ${keyframes`
to {
translate: 100% 0;
opacity: 0;
}
`} 0.15s var(--ease-out-expo);
}
}
}
`}
`,
[DialogPlacement.Inline]: css`
@media (prefers-reduced-motion: no-preference) {
&[data-state='open'] {
animation: ${keyframes`
${$withAnimation &&
css`
@media (prefers-reduced-motion: no-preference) {
&[data-state='open'] {
animation: ${keyframes`
from {
scale: 0.99;
opacity: 0;
/* filter: blur(2px); */
/* backdrop-filter: none; */
}
`} 0.15s var(--ease-out-expo);
}
}
&[data-state='closed'] {
animation: ${keyframes`
&[data-state='closed'] {
animation: ${keyframes`
to {
scale: 0.99;
opacity: 0;
/* filter: blur(2px); */
/* backdrop-filter: none; */
}
`} 0.15s var(--ease-out-expo);
}
}
}
`}
`,
[DialogPlacement.FullScreen]: css`
--dialog-width: 100vw;
+4 -1
View File
@@ -9,6 +9,7 @@ import {
RadioItem,
Portal,
} from '@radix-ui/react-dropdown-menu';
import { CheckIcon } from '@radix-ui/react-icons';
import { type MenuItem } from '@/constants/menus';
@@ -90,7 +91,9 @@ export const DropdownSelectMenu = <MenuItemValue extends string>({
{slotAfter}
<Styled.ItemIndicator>{/* <CheckIcon /> */}</Styled.ItemIndicator>
<Styled.ItemIndicator>
<CheckIcon />
</Styled.ItemIndicator>
</Styled.RadioItem>
))}
</RadioGroup>
+2 -29
View File
@@ -1,43 +1,16 @@
import styled, { AnyStyledComponent } from 'styled-components';
import { layoutMixins } from '@/styles/layoutMixins';
import { Icon, IconName } from '@/components/Icon';
export const GreenCheckCircle = ({ className }: { className?: string }) => (
<Styled.GreenCheckCircle className={className}>
<Icon iconName={IconName.Check} />
</Styled.GreenCheckCircle>
<Styled.Icon className={className} iconName={IconName.CheckCircle} />
);
const Styled: Record<string, AnyStyledComponent> = {};
Styled.GreenCheckCircle = styled.div`
${layoutMixins.stack}
Styled.Icon = styled(Icon)`
--icon-size: 1.25rem;
--icon-border-width: 3px;
width: var(--icon-size);
height: var(--icon-size);
border-radius: 50%;
align-items: center;
&:before {
content: '';
width: calc(var(--icon-size) - var(--icon-border-width) * 2);
height: calc(var(--icon-size) - var(--icon-border-width) * 2);
border: solid var(--icon-border-width) var(--color-positive);
border-radius: 50%;
opacity: 0.6;
}
svg {
width: calc(var(--icon-size) / 3);
height: calc(var(--icon-size) / 3);
color: var(--color-positive);
justify-self: center;
}
`;
+9
View File
@@ -15,6 +15,7 @@ import {
CautionCircleIcon,
ChatIcon,
CheckIcon,
CheckCircleIcon,
ChevronLeftIcon,
ChevronRightIcon,
ClockIcon,
@@ -37,6 +38,7 @@ import {
HelpCircleIcon,
HideIcon,
HistoryIcon,
LeaderboardIcon,
LinkOutIcon,
LockIcon,
LogoShortIcon,
@@ -52,6 +54,7 @@ import {
OrderUntriggeredIcon,
OverviewIcon,
PencilIcon,
PlayIcon,
PositionsIcon,
PriceChartIcon,
PrivacyIcon,
@@ -86,6 +89,7 @@ export enum IconName {
CautionCircleStroked = 'CautionCircleStroked',
Chat = 'Chat',
Check = 'Check',
CheckCircle = 'CheckCircle',
ChevronLeft = 'ChevronLeft',
ChevronRight = 'ChevronRight',
Clock = 'Clock',
@@ -108,6 +112,7 @@ export enum IconName {
HelpCircle = 'HelpCircle',
Hide = 'Hide',
History = 'History',
Leaderboard = 'Leaderboard',
LinkOut = 'LinkOut',
Lock = 'Lock',
LogoShort = 'LogoShort',
@@ -124,6 +129,7 @@ export enum IconName {
OrderUntriggered = 'OrderUntriggered',
Overview = 'Overview',
Pencil = 'Pencil',
Play = 'Play',
Positions = 'Positions',
PriceChart = 'PriceChart',
Privacy = 'Privacy',
@@ -158,6 +164,7 @@ const icons = {
[IconName.CautionCircleStroked]: CautionCircleStrokeIcon,
[IconName.Chat]: ChatIcon,
[IconName.Check]: CheckIcon,
[IconName.CheckCircle]: CheckCircleIcon,
[IconName.ChevronLeft]: ChevronLeftIcon,
[IconName.ChevronRight]: ChevronRightIcon,
[IconName.Clock]: ClockIcon,
@@ -180,6 +187,7 @@ const icons = {
[IconName.HelpCircle]: HelpCircleIcon,
[IconName.Hide]: HideIcon,
[IconName.History]: HistoryIcon,
[IconName.Leaderboard]: LeaderboardIcon,
[IconName.LinkOut]: LinkOutIcon,
[IconName.Lock]: LockIcon,
[IconName.LogoShort]: LogoShortIcon,
@@ -195,6 +203,7 @@ const icons = {
[IconName.OrderUntriggered]: OrderUntriggeredIcon,
[IconName.Overview]: OverviewIcon,
[IconName.Pencil]: PencilIcon,
[IconName.Play]: PlayIcon,
[IconName.Positions]: PositionsIcon,
[IconName.PriceChart]: PriceChartIcon,
[IconName.Privacy]: PrivacyIcon,
+11 -64
View File
@@ -1,4 +1,4 @@
import { forwardRef, Ref, useEffect, useRef, useState } from 'react';
import { forwardRef, Ref } from 'react';
import styled, { type AnyStyledComponent, css, keyframes } from 'styled-components';
import { NavLink, matchPath, useLocation } from 'react-router-dom';
@@ -21,8 +21,6 @@ import {
import { layoutMixins } from '@/styles/layoutMixins';
import { isTruthy } from '@/lib/isTruthy';
import { Tag } from './Tag';
import { Icon, IconName } from './Icon';
@@ -61,7 +59,6 @@ const NavItem = forwardRef(
<>
{slotBefore}
<span>
{/* {`${label}${subitems?.length ? ' ' : ''}`} */}
{label}
{tag && (
<>
@@ -100,8 +97,6 @@ const NavItem = forwardRef(
}
);
type TriggerRef = HTMLAnchorElement | HTMLDivElement | HTMLButtonElement | null;
export const NavigationMenu = <MenuItemValue extends string, MenuGroupValue extends string>({
onSelectItem,
onSelectGroup,
@@ -112,33 +107,6 @@ export const NavigationMenu = <MenuItemValue extends string, MenuGroupValue exte
dir = 'ltr',
className,
}: ElementProps<MenuItemValue, MenuGroupValue> & StyleProps) => {
// Disable click (close) in the first 500ms after hover (open)
// https://github.com/radix-ui/primitives/issues/1630#issuecomment-1545995075
const [clickIsDisabled, setClickIsDisabled] = useState(false);
const triggerRefs = useRef({} as { [value: string]: TriggerRef });
useEffect(() => {
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (
mutation.type === 'attributes' &&
mutation.attributeName === 'data-state' &&
(mutation.target as unknown as HTMLOrSVGElement).dataset.state === 'open' &&
mutation.target !== document.activeElement
) {
setClickIsDisabled(true);
setTimeout(() => setClickIsDisabled(false), 500);
}
}
});
for (const element of Object.values(triggerRefs.current).filter(isTruthy)) {
observer.observe(element, { attributes: true });
}
return () => observer.disconnect();
}, []);
const renderSubitems = ({
item,
depth = 0,
@@ -148,18 +116,18 @@ export const NavigationMenu = <MenuItemValue extends string, MenuGroupValue exte
}) => (
<>
<Styled.SubMenuTrigger
asChild={depth > 0}
ref={(ref: TriggerRef) => (triggerRefs.current[item.value] = ref)}
onClick={(e: MouseEvent) => {
if (clickIsDisabled) {
e.preventDefault();
}
}}
asChild
onPointerMove={(e: MouseEvent) => e.preventDefault()}
onPointerLeave={(e: MouseEvent) => e.preventDefault()}
>
<Styled.NavItem onSelect={onSelectGroup} orientation={itemOrientation} {...item} />
</Styled.SubMenuTrigger>
<Styled.Content data-placement={submenuPlacement}>
<Styled.Content
onPointerEnter={(e: MouseEvent) => e.preventDefault()}
onPointerLeave={(e: MouseEvent) => e.preventDefault()}
data-placement={submenuPlacement}
>
<Styled.Sub data-placement={submenuPlacement}>
<Styled.List
data-orientation={depth > 0 ? 'menu' : orientation === 'vertical' ? 'vertical' : 'menu'}
@@ -410,25 +378,12 @@ Styled.List = styled(List)`
`;
Styled.ListItem = styled(Item)`
/* display: contents; */
display: grid;
position: relative;
${Styled.List}[data-orientation="horizontal"] > & {
gap: var(--submenu-side-offset);
}
/* ${Styled.List}[data-orientation="menu"] > & {
grid-template-columns: 1fr 0;
align-items: start;
gap: 2rem;
} */
/* &:has([data-state="open"]) {
position: sticky;
left: 0;
right: 0;
} */
`;
Styled.SubMenuTrigger = styled(Trigger)`
@@ -436,11 +391,6 @@ Styled.SubMenuTrigger = styled(Trigger)`
outline-offset: -2px;
&[data-state='open'] {
div {
background-color: var(--navigationMenu-item-checked-backgroundColor);
color: var(--navigationMenu-item-checked-textColor);
}
svg {
rotate: 0.5turn;
}
@@ -452,11 +402,10 @@ Styled.NavItem = styled(NavItem)<{ orientation: 'horizontal' | 'vertical' }>`
subitems?.length
? css`
${popoverMixins.trigger}
--trigger-backgroundColor: transparent;
--trigger-open-backgroundColor: var(--color-layer-3);
--trigger-open-backgroundColor: var(--navigationMenu-item-checked-backgroundColor);
`
: css`
&:hover:not(.active) {
&:hover:not(:active) {
background-color: var(--navigationMenu-item-highlighted-backgroundColor);
color: var(--navigationMenu-item-highlighted-textColor);
}
@@ -470,8 +419,6 @@ Styled.NavItem = styled(NavItem)<{ orientation: 'horizontal' | 'vertical' }>`
--item-radius: var(--navigationMenu-item-radius);
--item-padding: var(--navigationMenu-item-padding);
/* ${popoverMixins.backdropOverlay} */
${layoutMixins.scrollSnapItem}
min-height: var(--navigationMenu-item-height);
+201
View File
@@ -0,0 +1,201 @@
import React, { type MouseEvent } from 'react';
import styled, { css } from 'styled-components';
import { ButtonShape, ButtonSize } from '@/constants/buttons';
import {
NotificationStatus,
type Notification as NotificationDataType,
} from '@/constants/notifications';
import { useNotifications } from '@/hooks/useNotifications';
import { popoverMixins } from '@/styles/popoverMixins';
import { layoutMixins } from '@/styles/layoutMixins';
import { IconButton } from '@/components/IconButton';
import { IconName } from '@/components/Icon';
import { Output, OutputType } from '@/components/Output';
type ElementProps = {
notification: NotificationDataType;
onClick?: () => void;
slotIcon?: React.ReactNode;
slotTitle?: React.ReactNode;
slotTitleLeft?: React.ReactNode;
slotTitleRight?: React.ReactNode;
slotDescription?: React.ReactNode;
slotCustomContent?: React.ReactNode;
slotAction?: React.ReactNode;
withClose?: boolean;
};
type StyleProps = {
className?: string;
isToast?: boolean;
};
export type NotificationProps = ElementProps & StyleProps;
export const Notification = ({
className,
isToast,
notification,
onClick,
slotIcon,
slotTitle,
slotTitleLeft,
slotTitleRight,
slotDescription,
slotCustomContent,
slotAction,
withClose = !isToast,
}: NotificationProps) => {
const { markCleared, markSeen } = useNotifications();
return (
<$Container className={className} isToast={isToast} onClick={onClick}>
<$Header>
{slotTitleLeft ?? (slotIcon && <$Icon>{slotIcon}</$Icon>)}
<$Title>{slotTitle}</$Title>
{slotTitleRight}
{withClose && !isToast && (
<$ActionItems>
<$Output
type={OutputType.RelativeTime}
value={notification.timestamps[NotificationStatus.Triggered]}
/>
{notification.status < NotificationStatus.Seen ? <$UnreadIndicator /> : null}
{notification.status < NotificationStatus.Cleared ? (
<$IconButton
iconName={IconName.Close}
shape={ButtonShape.Square}
size={ButtonSize.XSmall}
onClick={(e: MouseEvent) => {
e.stopPropagation();
if (notification.status < NotificationStatus.Seen) {
markSeen(notification);
} else if (notification.status < NotificationStatus.Cleared) {
markCleared(notification);
}
}}
/>
) : null}
</$ActionItems>
)}
</$Header>
<$Description>{slotCustomContent ?? slotDescription}</$Description>
{slotAction && <$Action>{slotAction}</$Action>}
</$Container>
);
};
const $Container = styled.div<{ isToast?: boolean }>`
// Params
--toast-icon-size: 1.75em;
// Rules
${popoverMixins.popover}
overflow: visible;
padding: 1rem 1.25rem;
${({ isToast }) =>
isToast
? css`
padding: 1rem;
box-shadow: 0 0 0 var(--border-width) var(--color-border),
// border
0 0 0.5rem 0.1rem var(--color-layer-2); // shadow
`
: css`
flex: 1;
background-color: transparent;
border-radius: 0;
backdrop-filter: none;
`}
`;
const $Header = styled.header`
${layoutMixins.row}
position: relative;
`;
const $Icon = styled.div`
${layoutMixins.row}
float: left;
margin-right: 0.5rem;
line-height: 1;
> svg,
img {
width: 1.5rem;
height: 1.5rem;
}
`;
const $Title = styled.div`
flex: 1;
font: var(--font-base-medium);
color: var(--color-text-2);
overflow: hidden;
text-overflow: ellipsis;
`;
const $Description = styled.div`
margin-top: 0.5rem;
color: var(--color-text-0);
font: var(--font-small-book);
`;
const $Action = styled.div`
margin-top: 0.5rem;
font: var(--font-small-book);
`;
const $ActionItems = styled.div`
min-width: fit-content;
padding-left: 0.5rem;
height: 100%;
${layoutMixins.row};
justify-content: end;
gap: 0.5rem;
position: absolute;
right: 0;
top: 0;
background-color: var(--color-layer-2);
font: var(--font-mini-book);
display: none;
z-index: 1;
${$Container}:hover & {
display: flex;
}
`;
const $UnreadIndicator = styled.div`
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background-color: var(--color-accent);
border: 1px solid var(--color-layer-2);
`;
const $Output = styled(Output)`
color: var(--color-text-0);
`;
const $IconButton = styled(IconButton)`
--button-border: none;
--button-textColor: var(--color-text-0);
--button-hover-textColor: var(--color-text-1);
`;
+7 -1
View File
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import styled, { type AnyStyledComponent, css } from 'styled-components';
import { layoutMixins } from '@/styles/layoutMixins';
@@ -22,6 +22,11 @@ export const SearchInput = ({
}: SearchInputProps) => {
const [isOpen, setIsOpen] = useState(false);
const [value, setValue] = useState('');
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (isOpen) inputRef?.current?.focus();
}, [inputRef, isOpen]);
return (
<Styled.Search>
@@ -41,6 +46,7 @@ export const SearchInput = ({
/>
<Styled.Input
autoFocus
ref={inputRef}
value={value}
isOpen={isOpen}
type={type}
+12 -7
View File
@@ -12,6 +12,8 @@ import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
import { WithLabel } from '@/components/WithLabel';
import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins';
import breakpoints from '@/styles/breakpoints';
type ElementProps = {
asChild?: boolean;
@@ -84,27 +86,30 @@ export const SearchSelectMenu = ({
const Styled: Record<string, AnyStyledComponent> = {};
Styled.SearchSelectMenu = styled.div`
${layoutMixins.column}
`;
Styled.MenuTrigger = styled.div`
height: var(--form-input-height);
${layoutMixins.spacedRow}
align-items: center;
padding: 0 1rem;
padding: var(--form-input-paddingY) var(--form-input-paddingX);
@media ${breakpoints.tablet} {
height: var(--form-input-height-mobile);
}
`;
Styled.WithLabel = styled(WithLabel)`
--label-textColor: var(--color-text-0);
gap: 0.25rem;
${formMixins.inputLabel}
label {
font: var(--font-mini-book);
}
`;
Styled.SearchSelectMenu = styled.div`
${layoutMixins.column}
`;
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
--withReceipt-backgroundColor: var(--color-layer-2);
+15 -9
View File
@@ -1,5 +1,5 @@
import React from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import styled, { css, type AnyStyledComponent } from 'styled-components';
import {
Root,
@@ -13,11 +13,13 @@ import {
Portal,
Icon as SelectIcon,
} from '@radix-ui/react-select';
import { CheckIcon } from '@radix-ui/react-icons';
import { popoverMixins } from '@/styles/popoverMixins';
import { formMixins } from '@/styles/formMixins';
import { WithLabel } from '@/components/WithLabel';
import { Icon, IconName } from './Icon';
export const SelectMenu = <T extends string>({
children,
@@ -44,7 +46,9 @@ export const SelectMenu = <T extends string>({
) : (
<Value />
)}
{React.Children.toArray(children).length > 1 && <Styled.DropdownIcon />}
{React.Children.toArray(children).length > 1 && (
<Styled.TriggerIcon iconName={IconName.Triangle} />
)}
</Styled.Trigger>
<Portal>
<Styled.Content className={className}>
@@ -68,7 +72,9 @@ export const SelectItem = <T extends string>({
}) => (
<Styled.Item className={className} value={value}>
<ItemText>{label}</ItemText>
<Styled.ItemIndicator>{/* <CheckIcon /> */}</Styled.ItemIndicator>
<Styled.ItemIndicator>
<CheckIcon />
</Styled.ItemIndicator>
</Styled.Item>
);
@@ -88,12 +94,6 @@ Styled.Trigger = styled(Trigger)<{ $withBlur?: boolean }>`
}
`;
Styled.DropdownIcon = styled(SelectIcon)`
font-size: 0.675em;
color: var(--color-text-1);
margin-left: auto;
`;
Styled.Content = styled(Content)`
--select-menu-content-maxWidth: ;
max-width: var(--select-menu-content-maxWidth);
@@ -116,3 +116,9 @@ Styled.WithLabel = styled(WithLabel)`
${formMixins.inputLabel}
border-radius: 0;
`;
Styled.TriggerIcon = styled(Icon)`
width: 0.625rem;
height: 0.375rem;
color: var(--color-text-0);
`;
+12 -8
View File
@@ -831,6 +831,7 @@ Styled.SortArrow = styled.span<{ sortDirection: 'ascending' | 'descending' }>`
Styled.Thead = styled.thead<StyleProps>`
${layoutMixins.stickyHeader}
scroll-snap-align: none;
font: var(--font-mini-book);
> * {
height: var(--stickyArea-topHeight);
@@ -839,18 +840,17 @@ Styled.Thead = styled.thead<StyleProps>`
color: var(--tableHeader-textColor);
background-color: var(--tableHeader-backgroundColor);
@media ${breakpoints.notTablet} {
${({ withInnerBorders, withGradientCardRows }) =>
withInnerBorders &&
!withGradientCardRows &&
css`
${layoutMixins.withInnerHorizontalBorders}
`}
}
${({ withInnerBorders, withGradientCardRows }) =>
withInnerBorders &&
!withGradientCardRows &&
css`
${layoutMixins.withInnerHorizontalBorders}
`}
`;
Styled.Tbody = styled.tbody<StyleProps>`
${layoutMixins.stickyArea2}
font: var(--font-small-book);
// If <table> height is fixed with not enough rows to overflow, vertically center the rows
&:before,
@@ -869,6 +869,10 @@ Styled.Tbody = styled.tbody<StyleProps>`
--stickyArea2-paddingBottom: var(--border-width);
--stickyArea2-paddingLeft: var(--border-width);
--stickyArea2-paddingRight: var(--border-width);
tr:first-of-type {
box-shadow: none;
}
`}
${({ withGradientCardRows }) =>
+76 -77
View File
@@ -1,24 +1,19 @@
import React, { useEffect, useRef, useState } from 'react';
import { Root, Title, Description, Action, Close } from '@radix-ui/react-toast';
import { type MouseEvent, useEffect, useRef, useState } from 'react';
import styled, { keyframes } from 'styled-components';
import { Root, Action, Close } from '@radix-ui/react-toast';
import { ButtonShape, ButtonSize } from '@/constants/buttons';
import { popoverMixins } from '@/styles/popoverMixins';
import { breakpoints } from '@/styles';
import { Notification, type NotificationProps } from '@/components/Notification';
import { IconButton } from './IconButton';
import { CloseIcon } from '@/icons';
import styled, { keyframes } from 'styled-components';
import { popoverMixins } from '@/styles/popoverMixins';
import { layoutMixins } from '@/styles/layoutMixins';
import { IconName } from './Icon';
type ElementProps = {
isOpen?: boolean;
setIsOpen?: (isOpen: boolean, isClosedFromTimeout?: boolean) => void;
slotIcon?: React.ReactNode;
slotTitle?: React.ReactNode;
slotDescription?: React.ReactNode;
slotCustomContent?: React.ReactNode;
slotAction?: React.ReactNode;
actionDescription?: string;
actionAltText?: string;
sensitivity?: 'foreground' | 'background';
@@ -26,13 +21,22 @@ type ElementProps = {
lastUpdated?: number;
};
type StyleProps = {};
type StyleProps = {
className?: string;
};
type ToastProps = NotificationProps & ElementProps & StyleProps;
export const Toast = ({
className,
isOpen = true,
notification,
onClick,
setIsOpen,
slotIcon,
slotTitle,
slotTitleLeft,
slotTitleRight,
slotDescription,
slotCustomContent,
slotAction,
@@ -41,7 +45,7 @@ export const Toast = ({
sensitivity = 'background',
duration = Infinity,
lastUpdated,
}: ElementProps & StyleProps) => {
}: ToastProps) => {
// Timeout
const timeout = useRef<number>();
const [isPaused, setIsPaused] = useState(false);
@@ -58,37 +62,45 @@ export const Toast = ({
return (
<$Root
className={className}
type={sensitivity}
duration={Infinity}
open={isOpen}
onOpenChange={setIsOpen}
onPause={() => setIsPaused(true)}
onResume={() => setIsPaused(false)}
onClick={() => {
setIsOpen?.(false, false);
onClick?.();
}}
>
<div>
<$Container>
<$Header>
{slotIcon && <$Icon>{slotIcon}</$Icon>}
<Close asChild>
<$CloseButton
iconName={IconName.Close}
shape={ButtonShape.Circle}
size={ButtonSize.XSmall}
onClick={(e: MouseEvent) => e.stopPropagation()}
/>
</Close>
<Close asChild>
<$CloseButton
iconComponent={CloseIcon}
shape={ButtonShape.Square}
size={ButtonSize.XSmall}
/>
</Close>
<$Title>{slotTitle}</$Title>
</$Header>
{!slotCustomContent && <$Description>{slotDescription}</$Description>}
{slotCustomContent}
{actionDescription && (
<$Action asChild altText={actionAltText}>
{slotAction}
</$Action>
)}
</$Container>
</div>
{slotCustomContent ?? (
<$Notification
isToast
notification={notification}
slotIcon={slotIcon}
slotTitle={slotTitle}
slotTitleLeft={slotTitleLeft}
slotTitleRight={slotTitleRight}
slotDescription={slotDescription}
slotAction={
actionDescription && (
<$Action asChild altText={actionAltText}>
{slotAction}
</$Action>
)
}
/>
)}
</$Root>
);
};
@@ -114,7 +126,7 @@ const $Root = styled(Root)`
translate: var(--x) var(--y);
will-change: translate, margin-top;
margin-bottom: var(--toasts-gap);
/* margin-bottom: var(--toasts-gap); */
&[data-swipe-direction='right'] {
&[data-state='open'] {
@@ -163,6 +175,10 @@ const $Root = styled(Root)`
cursor: n-resize;
}
&:focus:not([data-swipe='end']) & {
outline: var(--color-accent) 1px solid;
}
&[data-swipe='move'] {
transition-property: opacity;
opacity: 0.98;
@@ -171,6 +187,10 @@ const $Root = styled(Root)`
* {
cursor: inherit;
}
& > * {
opacity: 0.5;
}
}
&[data-state='closed'],
@@ -198,69 +218,48 @@ const $Root = styled(Root)`
}
`;
const $Container = styled.div`
const $Notification = styled(Notification)`
// Params
--toast-icon-size: 1.75em;
// Rules
${popoverMixins.popover}
overflow: visible;
padding: 1rem;
box-shadow: 0 0 0 var(--border-width) var(--color-border),
// border
0 0 0.5rem 0.1rem var(--color-layer-2); // shadow
${$Root}:focus:not([data-swipe='end']) & {
outline: var(--color-accent) 1px solid;
}
> * {
transition: opacity 0.2s;
}
${$Root}[data-swipe='move'] & > * {
opacity: 0.5;
}
`;
const $Header = styled.header`
display: block;
`;
const $Icon = styled.div`
${layoutMixins.row}
float: left;
width: 1em;
height: 1em;
margin-right: 0.4em;
line-height: 1;
`;
const $CloseButton = styled(IconButton)`
--button-textColor: var(--color-text-0);
--button-border: none;
--button-icon-size: 0.85em;
display: none;
float: right;
/* float: right; */
margin: -0.42rem -0.42rem -0.42rem 0.42rem;
`;
const $Title = styled(Title)`
flex: 1;
// Absolute
position: absolute;
top: 0;
right: 0;
font: var(--font-base-medium);
color: var(--color-text-2);
border: solid var(--border-width) var(--color-border);
overflow: hidden;
text-overflow: ellipsis;
`;
${$Root}:hover & {
display: block;
z-index: 2;
}
const $Description = styled(Description)`
margin-top: 0.5rem;
color: var(--color-text-0);
font: var(--font-small-book);
@media ${breakpoints.mobile} {
display: block;
z-index: 2;
}
`;
const $Action = styled(Action)`
+7 -2
View File
@@ -1,6 +1,7 @@
import styled from 'styled-components';
import { Provider, Viewport } from '@radix-ui/react-toast';
import styled from 'styled-components';
import { layoutMixins } from '@/styles/layoutMixins';
type ElementProps = {
@@ -12,7 +13,9 @@ type StyleProps = {
className?: string;
};
export const ToastArea = ({ swipeDirection, children, className }: ElementProps & StyleProps) => (
type ToastAreaProps = ElementProps & StyleProps;
export const ToastArea = ({ swipeDirection, children, className }: ToastAreaProps) => (
<$ToastArea className={className}>
<Provider swipeDirection={swipeDirection}>
{children}
@@ -34,6 +37,7 @@ const $ToastArea = styled.aside`
> * {
display: contents;
pointer-events: none;
position: relative;
> ol {
/* display: block; */
@@ -43,6 +47,7 @@ const $ToastArea = styled.aside`
// Desired display order: latest to earliest (¯\_(ツ)_/¯)
display: flex;
flex-direction: column-reverse;
position: relative;
pointer-events: none;
+4
View File
@@ -52,15 +52,19 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.BaseButton = styled(BaseButton)`
--button-toggle-off-backgroundColor: var(--color-layer-3);
--button-toggle-off-textColor: var(--color-text-0);
--button-toggle-off-border: solid var(--border-width) var(--border-color);
--button-toggle-on-backgroundColor: var(--color-layer-1);
--button-toggle-on-textColor: var(--color-text-2);
--button-toggle-on-border: solid var(--border-width) var(--border-color);
--button-backgroundColor: var(--button-toggle-off-backgroundColor);
--button-textColor: var(--button-toggle-off-textColor);
--button-border: var(--button-toggle-off-border);
&[data-state='on'],
&[data-state='active'] {
--button-backgroundColor: var(--button-toggle-on-backgroundColor);
--button-textColor: var(--button-toggle-on-textColor);
--button-border: var(--button-toggle-on-border);
}
`;
+34 -1
View File
@@ -124,6 +124,8 @@ export type SubaccountPosition = Abacus.exchange.dydx.abacus.output.SubaccountPo
export type SubaccountOrder = Abacus.exchange.dydx.abacus.output.SubaccountOrder;
export type OrderStatus = Abacus.exchange.dydx.abacus.output.input.OrderStatus;
export const AbacusOrderStatus = Abacus.exchange.dydx.abacus.output.input.OrderStatus;
const abacusOrderStatuses = [...AbacusOrderStatus.values()] as const;
export type AbacusOrderStatuses = (typeof abacusOrderStatuses)[number];
export type SubaccountFills = Abacus.exchange.dydx.abacus.output.SubaccountFill[];
export type SubaccountFill = Abacus.exchange.dydx.abacus.output.SubaccountFill;
export type SubaccountFundingPayment = Abacus.exchange.dydx.abacus.output.SubaccountFundingPayment;
@@ -209,11 +211,13 @@ type ReadonlyKeysOf<T> = {
[K in keyof T]: IfEquals<{ [_ in K]: T[K] }, { readonly [_ in K]: T[K] }, K, never>;
}[keyof T];
type KotlinIrEnumValues<T> = Exclude<ReadonlyKeysOf<T>, 'Companion' | '$serializer'>;
export type KotlinIrEnumValues<T> = Exclude<ReadonlyKeysOf<T>, 'Companion' | '$serializer'>;
export const ORDER_SIDES: Record<KotlinIrEnumValues<typeof AbacusOrderSide>, OrderSide> = {
[AbacusOrderSide.buy.name]: OrderSide.BUY,
[AbacusOrderSide.sell.name]: OrderSide.SELL,
[AbacusOrderSide.buy.rawValue]: OrderSide.BUY,
[AbacusOrderSide.sell.rawValue]: OrderSide.SELL,
};
export const POSITION_SIDES: Record<KotlinIrEnumValues<typeof AbacusPositionSide>, PositionSide> = {
@@ -234,12 +238,25 @@ export const HISTORICAL_PNL_PERIODS: Record<
export const ORDER_STATUS_STRINGS: Record<KotlinIrEnumValues<typeof AbacusOrderStatus>, string> = {
[AbacusOrderStatus.open.name]: STRING_KEYS.OPEN_STATUS,
[AbacusOrderStatus.open.rawValue]: STRING_KEYS.OPEN_STATUS,
[AbacusOrderStatus.partiallyFilled.name]: STRING_KEYS.PARTIALLY_FILLED,
[AbacusOrderStatus.partiallyFilled.rawValue]: STRING_KEYS.PARTIALLY_FILLED,
[AbacusOrderStatus.filled.name]: STRING_KEYS.ORDER_FILLED,
[AbacusOrderStatus.filled.rawValue]: STRING_KEYS.ORDER_FILLED,
[AbacusOrderStatus.cancelled.name]: STRING_KEYS.CANCELED,
[AbacusOrderStatus.cancelled.rawValue]: STRING_KEYS.CANCELED,
[AbacusOrderStatus.canceling.name]: STRING_KEYS.CANCELING,
[AbacusOrderStatus.canceling.rawValue]: STRING_KEYS.CANCELING,
[AbacusOrderStatus.pending.name]: STRING_KEYS.PENDING,
[AbacusOrderStatus.pending.rawValue]: STRING_KEYS.PENDING,
[AbacusOrderStatus.untriggered.name]: STRING_KEYS.UNTRIGGERED,
[AbacusOrderStatus.untriggered.rawValue]: STRING_KEYS.UNTRIGGERED,
};
export const TRADE_TYPES: Record<
@@ -247,15 +264,31 @@ export const TRADE_TYPES: Record<
Nullable<TradeTypes>
> = {
[AbacusOrderType.limit.name]: TradeTypes.LIMIT,
[AbacusOrderType.limit.rawValue]: TradeTypes.LIMIT,
[AbacusOrderType.market.name]: TradeTypes.MARKET,
[AbacusOrderType.market.rawValue]: TradeTypes.MARKET,
[AbacusOrderType.stopLimit.name]: TradeTypes.STOP_LIMIT,
[AbacusOrderType.stopLimit.rawValue]: TradeTypes.STOP_LIMIT,
[AbacusOrderType.stopMarket.name]: TradeTypes.STOP_MARKET,
[AbacusOrderType.stopMarket.rawValue]: TradeTypes.STOP_MARKET,
[AbacusOrderType.takeProfitLimit.name]: TradeTypes.TAKE_PROFIT,
[AbacusOrderType.takeProfitLimit.rawValue]: TradeTypes.TAKE_PROFIT,
[AbacusOrderType.takeProfitMarket.name]: TradeTypes.TAKE_PROFIT_MARKET,
[AbacusOrderType.takeProfitMarket.rawValue]: TradeTypes.TAKE_PROFIT_MARKET,
[AbacusOrderType.liquidated.name]: null,
[AbacusOrderType.liquidation.name]: null,
[AbacusOrderType.liquidation.name]: null,
[AbacusOrderType.liquidation.rawValue]: null,
[AbacusOrderType.trailingStop.name]: null,
[AbacusOrderType.trailingStop.rawValue]: null,
};
// Custom types involving Abacus
+3 -1
View File
@@ -3,6 +3,7 @@ export enum DialogTypes {
Deposit = 'Deposit',
DisconnectWallet = 'DisconnectWallet',
ExchangeOffline = 'ExchangeOffline',
ExternalLink = 'ExternalLink',
FillDetails = 'FillDetails',
Help = 'Help',
ExternalNavKeplr = 'ExternalNavKeplr',
@@ -10,13 +11,14 @@ export enum DialogTypes {
MobileSignIn = 'MobileSignIn',
Onboarding = 'Onboarding',
OrderDetails = 'OrderDetails',
Preferences = 'Preferences',
RateLimit = 'RateLimit',
Receive = 'Receive',
RestrictedGeo = 'RestrictedGeo',
RestrictedWallet = 'RestrictedWallet',
Trade = 'Trade',
Transfer = 'Transfer',
Withdraw = 'Withdraw',
ManageFunds = 'ManageFunds',
}
export enum TradeBoxDialogTypes {
+5
View File
@@ -13,6 +13,7 @@ export enum LocalStorageKey {
PushNotificationsEnabled = 'dydx.PushNotificationsEnabled',
PushNotificationsLastUpdated = 'dydx.PushNotificationsLastUpdated',
TransferNotifications = 'dydx.TransferNotifications',
NotificationPreferences = 'dydx.NotificationPreferences',
// UI State
LastViewedMarket = 'dydx.LastViewedMarket',
@@ -21,9 +22,13 @@ export enum LocalStorageKey {
SelectedTheme = 'dydx.SelectedTheme',
SelectedTradeLayout = 'dydx.SelectedTradeLayout',
TradingViewChartConfig = 'dydx.TradingViewChartConfig',
HasSeenLaunchIncentives = 'dydx.HasSeenLaunchIncentives',
}
export const LOCAL_STORAGE_VERSIONS = {
[LocalStorageKey.EvmDerivedAddresses]: 'v2',
[LocalStorageKey.NotificationPreferences]: 'v1',
[LocalStorageKey.TransferNotifications]: 'v1',
[LocalStorageKey.Notifications]: 'v1',
// TODO: version all localStorage keys
};
+8
View File
@@ -6,6 +6,8 @@ import {
LOCALE_DATA,
TOOLTIPS,
WARNINGS_STRING_KEYS,
NOTIFICATIONS_STRING_KEYS,
NOTIFICATIONS,
} from '@dydxprotocol/v4-localization';
import { type LinksConfigs } from '@/hooks/useURLConfigs';
@@ -27,6 +29,7 @@ export enum SupportedLocales {
export const EN_LOCALE_DATA = {
...LOCALE_DATA[SupportedLocales.EN],
...NOTIFICATIONS[SupportedLocales.EN],
TOOLTIPS: TOOLTIPS[SupportedLocales.EN],
};
@@ -34,8 +37,13 @@ export const STRING_KEYS = {
...APP_STRING_KEYS,
...ERRORS_STRING_KEYS,
...WARNINGS_STRING_KEYS,
...NOTIFICATIONS_STRING_KEYS,
};
export const STRING_KEY_VALUES = Object.fromEntries(
Object.values(STRING_KEYS).map((key) => [key, key])
);
export type StringKey = keyof typeof STRING_KEYS;
export type LocaleData = typeof EN_LOCALE_DATA;
+21
View File
@@ -26,3 +26,24 @@ export enum FundingDirection {
ToShort = 'ToShort',
ToLong = 'ToLong',
}
export const MARKETS_TO_DISPLAY = [
'BTC-USD',
'ETH-USD',
'LINK-USD',
'SOL-USD',
'MATIC-USD',
'ATOM-USD',
'AVAX-USD',
'APE-USD',
'XRP-USD',
'UNI-USD',
'ADA-USD',
'TRX-USD',
'OP-USD',
'MKR-USD',
'DOGE-USD',
'SHIB-USD',
'COMP-USD',
'LDO-USD',
];
+1
View File
@@ -26,6 +26,7 @@ export type MenuItem<MenuItemValue, MenuItemTypes = string> = {
tag?: React.ReactNode;
slotAfter?: React.ReactNode;
description?: string;
slotCustomContent?: React.ReactNode;
href?: string;
onSelect?: (key: MenuItemValue) => void;
+12 -6
View File
@@ -1,12 +1,18 @@
import environments from '../../public/configs/env.json';
// TODO: Update for Mainnet deployment
export const AVAILABLE_ENVIRONMENTS =
import.meta.env.MODE === 'production'
? environments.deployments.TESTNET
: environments.deployments.DEV;
const CURRENT_MODE = ({
production: 'MAINNET',
testnet: 'TESTNET',
staging: 'DEV',
development: 'DEV',
}[import.meta.env.MODE] ?? 'MAINNET') as 'MAINNET' | 'TESTNET' | 'DEV';
export const CURRENT_ABACUS_DEPLOYMENT = import.meta.env.MODE === 'production' ? 'TESTNET' : 'DEV';
export const isMainnet = CURRENT_MODE === 'MAINNET';
export const isTestnet = CURRENT_MODE === 'TESTNET';
export const isDev = CURRENT_MODE === 'DEV';
export const AVAILABLE_ENVIRONMENTS = environments.deployments[CURRENT_MODE];
export const CURRENT_ABACUS_DEPLOYMENT = CURRENT_MODE;
export const ENVIRONMENT_CONFIG_MAP = environments.environments;
export type DydxNetwork = keyof typeof ENVIRONMENT_CONFIG_MAP;
export const DEFAULT_APP_ENVIRONMENT = AVAILABLE_ENVIRONMENTS.default as DydxNetwork;
+26 -8
View File
@@ -2,8 +2,9 @@ import { StatusResponse } from '@0xsquid/sdk';
/** implemented in useNotificationTypes */
export enum NotificationType {
OrderStatusChanged = 'OrderStatusChanged',
AbacusGenerated = 'AbacusGenerated',
SquidTransfer = 'SquidTransfer',
ReleaseUpdates = 'ReleaseUpdates',
}
export enum NotificationComponentType {}
@@ -59,7 +60,7 @@ export enum NotificationStatus {
/** Toast or NotificationsMenu item interacted with or dismissed. "Seen" in NotificationsMenu. */
Seen,
/** Notification marked for deletion. "Archived" in NotificationsMenu. */
/** Notification marked for deletion. Hidden in NotificationsMenu. */
Cleared,
}
@@ -80,14 +81,20 @@ export type Notifications = Record<NotificationId, Notification<any>>;
/** Notification display data derived from app state at runtime. */
export type NotificationDisplayData = {
icon?: React.ReactNode;
title: string; // Title for Toast, Notification, and Push Notification
body?: string; // Description body for Toast, Notification, and Push Notification
title?: string;
slotTitleLeft?: React.ReactNode;
slotTitleRight?: React.ReactNode;
description?: React.ReactNode;
customContent?: React.ReactNode;
customMenuContent?: React.ReactNode;
// Overrides title/body for Notification in NotificationMenu
renderCustomBody?: ({
isToast,
notification,
}: {
isToast?: boolean;
notification: Notification;
}) => React.ReactNode; // Custom Notification
actionDescription?: string;
@@ -122,5 +129,16 @@ export type TransferNotifcation = {
fromChainId?: string;
toAmount?: number;
triggeredAt?: number;
isCctp?: boolean;
errorCount?: number;
status?: StatusResponse;
};
/**
* @description Struct to store whether a NotificationType should be triggered
*/
export type NotificationPreferences = {
[key in NotificationType]: boolean;
} & { version: string };
export const DEFAULT_TOAST_AUTO_CLOSE_MS = 5000;
+12
View File
@@ -116,3 +116,15 @@ export enum MobilePlaceOrderSteps {
PlacingOrder = 'PlacingOrder',
Confirmation = 'Confirmation',
}
export const CLEARED_TRADE_INPUTS = {
limitPriceInput: '',
triggerPriceInput: '',
trailingPercentInput: '',
};
export const CLEARED_SIZE_INPUTS = {
amountInput: '',
usdAmountInput: '',
leverageInput: '',
};
+2
View File
@@ -22,6 +22,7 @@ import { useShouldShowFooter } from './useShouldShowFooter';
import { useSelectedNetwork } from './useSelectedNetwork';
import { useStringGetter } from './useStringGetter';
import { useSubaccount } from './useSubaccount';
import { useTradeFormInputs } from './useTradeFormInputs';
import { useURLConfigs } from './useURLConfigs';
export {
@@ -49,5 +50,6 @@ export {
useSelectedNetwork,
useStringGetter,
useSubaccount,
useTradeFormInputs,
useURLConfigs,
};
+15 -3
View File
@@ -2,7 +2,7 @@ import { useCallback, useContext, createContext, useEffect, useState, useMemo }
import { useDispatch } from 'react-redux';
import { AES, enc } from 'crypto-js';
import { LocalWallet, type Subaccount } from '@dydxprotocol/v4-client-js';
import { NOBLE_BECH32_PREFIX, LocalWallet, type Subaccount } from '@dydxprotocol/v4-client-js';
import { OnboardingGuard, OnboardingState, type EvmDerivedAddresses } from '@/constants/account';
import { DialogTypes } from '@/constants/dialogs';
@@ -42,6 +42,7 @@ const useAccountsContext = () => {
selectedWalletError,
evmAddress,
signerWagmi,
publicClientWagmi,
dydxAddress: connectedDydxAddress,
signerGraz,
} = useWalletConnection();
@@ -135,8 +136,8 @@ const useAccountsContext = () => {
getSubaccounts: async ({ dydxAddress }: { dydxAddress: DydxAddress }) => {
try {
const response = await compositeClient?.indexerClient.account.getSubaccounts(dydxAddress);
setDydxSubaccounts(response.subaccounts);
return response.subaccounts;
setDydxSubaccounts(response?.subaccounts);
return response?.subaccounts ?? [];
} catch (error) {
// 404 is expected if the user has no subaccounts
if (error.status === 404) {
@@ -218,6 +219,16 @@ const useAccountsContext = () => {
else abacusStateManager.attemptDisconnectAccount();
}, [localDydxWallet]);
useEffect(() => {
const setNobleWallet = async () => {
if (hdKey?.mnemonic) {
const nobleWallet = await LocalWallet.fromMnemonic(hdKey.mnemonic, NOBLE_BECH32_PREFIX);
abacusStateManager.setNobleWallet(nobleWallet);
}
};
setNobleWallet();
}, [hdKey?.mnemonic]);
// clear subaccounts when no dydxAddress is set
useEffect(() => {
(async () => {
@@ -296,6 +307,7 @@ const useAccountsContext = () => {
// Wallet connection (EVM)
evmAddress,
signerWagmi,
publicClientWagmi,
// Wallet connection (Cosmos)
signerGraz,
+3 -1
View File
@@ -22,6 +22,8 @@ export const useCurrentMarketId = () => {
const dispatch = useDispatch();
const selectedNetwork = useSelector(getSelectedNetwork);
const marketIds = useSelector(getMarketIds, shallowEqual);
const hasMarketIds = marketIds.length > 0;
const [lastViewedMarket, setLastViewedMarket] = useLocalStorage({
key: LocalStorageKey.LastViewedMarket,
defaultValue: DEFAULT_MARKETID,
@@ -48,5 +50,5 @@ export const useCurrentMarketId = () => {
if (marketIds) {
abacusStateManager.setMarket(marketId ?? DEFAULT_MARKETID);
}
}, [selectedNetwork, marketIds, marketId]);
}, [selectedNetwork, hasMarketIds, marketId]);
};
+2 -1
View File
@@ -1,3 +1,4 @@
import { isDev } from '@/constants/networks';
import { WalletType } from '@/constants/wallets';
import { isTruthy } from '@/lib/isTruthy';
@@ -6,7 +7,7 @@ export const useDisplayedWallets = () => {
return [
WalletType.MetaMask,
import.meta.env.MODE !== 'production' && WalletType.Keplr,
isDev && WalletType.Keplr,
WalletType.WalletConnect2,
+74 -42
View File
@@ -1,12 +1,12 @@
import { createContext, useContext, useCallback, useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import type { StatusResponse } from '@0xsquid/sdk';
import { LocalStorageKey } from '@/constants/localStorage';
import { LOCAL_STORAGE_VERSIONS, LocalStorageKey } from '@/constants/localStorage';
import { type TransferNotifcation } from '@/constants/notifications';
import { useAccounts } from '@/hooks/useAccounts';
import { STATUS_ERROR_GRACE_PERIOD, useSquid } from '@/hooks/useSquid';
import { fetchSquidStatus, STATUS_ERROR_GRACE_PERIOD } from '@/lib/squid';
import { useLocalStorage } from './useLocalStorage';
const LocalNotificationsContext = createContext<
@@ -22,16 +22,32 @@ export const LocalNotificationsProvider = ({ ...props }) => (
export const useLocalNotifications = () => useContext(LocalNotificationsContext)!;
const TRANSFER_STATUS_FETCH_INTERVAL = 10_000;
const ERROR_COUNT_THRESHOLD = 3;
const useLocalNotificationsContext = () => {
// transfer notifications
const [allTransferNotifications, setAllTransferNotifications] = useLocalStorage<{
[key: `dydx${string}`]: TransferNotifcation[];
version: string;
}>({
key: LocalStorageKey.TransferNotifications,
defaultValue: {},
defaultValue: {
version: LOCAL_STORAGE_VERSIONS[LocalStorageKey.TransferNotifications],
},
});
// Ensure version matches, otherwise wipe
useEffect(() => {
if (
allTransferNotifications?.version !==
LOCAL_STORAGE_VERSIONS[LocalStorageKey.TransferNotifications]
) {
setAllTransferNotifications({
version: LOCAL_STORAGE_VERSIONS[LocalStorageKey.TransferNotifications],
});
}
}, [allTransferNotifications]);
const { dydxAddress } = useAccounts();
const transferNotifications = dydxAddress ? allTransferNotifications[dydxAddress] || [] : [];
@@ -52,49 +68,65 @@ const useLocalNotificationsContext = () => {
[transferNotifications]
);
const squid = useSquid();
const { data: transferStatuses } = useQuery({
queryKey: ['getTransactionStatus', transferNotifications],
useQuery({
queryKey: 'getTransactionStatus',
queryFn: async () => {
const statuses: { [key: string]: StatusResponse } = {};
for (const {
txHash,
toChainId,
fromChainId,
triggeredAt,
status: currentStatus,
} of transferNotifications) {
try {
if (currentStatus && currentStatus?.squidTransactionStatus !== 'ongoing') continue;
const processTransferNotifications = async (transferNotifications: TransferNotifcation[]) => {
const newTransferNotifications = await Promise.all(
transferNotifications.map(async (transferNotification) => {
const {
txHash,
toChainId,
fromChainId,
triggeredAt,
isCctp,
errorCount,
status: currentStatus,
} = transferNotification;
const status = await squid?.getStatus({ transactionId: txHash, toChainId, fromChainId });
if (status) statuses[txHash] = status;
} catch (error) {
// ignore errors for the first 120s since the status might not be available yet
if (!triggeredAt || Date.now() - triggeredAt > STATUS_ERROR_GRACE_PERIOD) {
statuses[txHash] = error;
}
}
}
return statuses;
// @ts-ignore status.errors is not in the type definition but can be returned
// also error can some time come back as an empty object so we need to ignore for that
const hasErrors = !!currentStatus?.errors ||
(currentStatus?.error && Object.keys(currentStatus.error).length !== 0);
if (
!hasErrors &&
(!currentStatus?.squidTransactionStatus ||
currentStatus?.squidTransactionStatus === 'ongoing')
) {
try {
const status = await fetchSquidStatus({
transactionId: txHash,
toChainId,
fromChainId,
}, isCctp);
if (status) {
transferNotification.status = status;
}
} catch (error) {
if (!triggeredAt || Date.now() - triggeredAt > STATUS_ERROR_GRACE_PERIOD) {
if (errorCount && errorCount > ERROR_COUNT_THRESHOLD) {
transferNotification.status = error;
} else {
transferNotification.errorCount = errorCount ? errorCount + 1 : 1;
}
}
}
}
return transferNotification;
})
);
return newTransferNotifications;
};
const newTransferNotifications = await processTransferNotifications(transferNotifications);
setTransferNotifications(newTransferNotifications);
},
refetchInterval: TRANSFER_STATUS_FETCH_INTERVAL,
});
useEffect(() => {
if (!transferStatuses) return;
const newTransferNotifications = transferNotifications.map((notification) => {
const status = transferStatuses[notification.txHash];
if (!status) return notification;
return {
...notification,
status,
};
});
setTransferNotifications(newTransferNotifications);
}, [transferStatuses]);
return {
transferNotifications,
addTransferNotification,
+11 -2
View File
@@ -1,7 +1,14 @@
import { useMemo } from 'react';
import { useSelector, shallowEqual } from 'react-redux';
import { MarketFilters, MARKET_FILTER_LABELS, type MarketData } from '@/constants/markets';
import {
MarketFilters,
MARKET_FILTER_LABELS,
type MarketData,
MARKETS_TO_DISPLAY,
} from '@/constants/markets';
import { testFlags } from '@/hooks/useTestFlags';
import { getAssets } from '@/state/assetsSelectors';
import { getPerpetualMarkets } from '@/state/perpetualsSelectors';
@@ -42,7 +49,9 @@ export const useMarketsData = (
}, [allPerpetualMarkets, allAssets]);
const filteredMarkets = useMemo(() => {
const filtered = markets.filter(filterFunctions[filter]);
const filtered = markets
.filter(filterFunctions[filter])
.filter(({ id }) => (testFlags.displayAllMarkets ? true : MARKETS_TO_DISPLAY.includes(id)));
if (searchFilter) {
return filtered.filter(
+210 -153
View File
@@ -1,205 +1,262 @@
import { useCallback, useEffect, useMemo } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { useSelector, shallowEqual, useDispatch } from 'react-redux';
import { groupBy } from 'lodash';
import { type ReactNode, useEffect } from 'react';
import styled from 'styled-components';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { isEqual, groupBy } from 'lodash';
import { useNavigate } from 'react-router-dom';
import { AlertType } from '@/constants/alerts';
import { AbacusOrderStatus, ORDER_SIDES } from '@/constants/abacus';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS, StringKey } from '@/constants/localization';
import { type NotificationTypeConfig, NotificationType } from '@/constants/notifications';
import { ORDER_SIDE_STRINGS } from '@/constants/trade';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { AppRoute } from '@/constants/routes';
import { DydxChainAsset } from '@/constants/wallets';
import {
STRING_KEYS,
STRING_KEY_VALUES,
type StringGetterFunction,
type StringKey,
} from '@/constants/localization';
import {
type NotificationTypeConfig,
NotificationType,
DEFAULT_TOAST_AUTO_CLOSE_MS,
} from '@/constants/notifications';
import { useSelectedNetwork, useStringGetter } from '@/hooks';
import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { AssetIcon } from '@/components/AssetIcon';
import { Icon, IconName } from '@/components/Icon';
import { Output, OutputType } from '@/components/Output';
import { TransferStatusToast } from '@/views/TransferStatus';
import { TradeNotification } from '@/views/notifications/TradeNotification';
import { TransferStatusNotification } from '@/views/notifications/TransferStatusNotification';
import { getSubaccountFills, getSubaccountOrders } from '@/state/accountSelectors';
import { openDialog } from '@/state/dialogs';
import { getAbacusNotifications } from '@/state/notificationsSelectors';
import { getMarketIds } from '@/state/perpetualsSelectors';
import { OrderStatusIcon } from '@/views/OrderStatusIcon';
import { formatSeconds } from '@/lib/timeUtils';
import { useStringGetter } from './useStringGetter';
import { TransferStatusSteps } from '@/views/TransferStatusSteps';
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
const parseStringParamsForNotification = ({
stringGetter,
value,
}: {
stringGetter: StringGetterFunction;
value: unknown;
}): ReactNode => {
if (STRING_KEY_VALUES[value as StringKey]) {
return stringGetter({ key: value as StringKey });
}
export const notificationTypes = [
return value as ReactNode;
};
export const notificationTypes: NotificationTypeConfig[] = [
{
type: NotificationType.OrderStatusChanged,
useTrigger: ({ trigger, lastUpdated }) => {
type: NotificationType.AbacusGenerated,
useTrigger: ({ trigger }) => {
const stringGetter = useStringGetter();
const orders = useSelector(getSubaccountOrders, shallowEqual) || [];
const ordersByOrderId = Object.fromEntries(orders.map((order) => [order.id, order]));
const fills = useSelector(getSubaccountFills, shallowEqual) || [];
const fillsByOrderId = groupBy(fills, (fill) => fill.orderId);
const orderIds = useMemo(
() => [...Object.keys(ordersByOrderId), ...Object.keys(fillsByOrderId)],
[orders, fills]
);
const abacusNotifications = useSelector(getAbacusNotifications, isEqual);
useEffect(() => {
for (const orderId of orderIds) {
const fills = fillsByOrderId[orderId];
for (const abacusNotif of abacusNotifications) {
const [abacusNotificationType = '', id = ''] = abacusNotif.id.split(':');
const parsedData = abacusNotif.data ? JSON.parse(abacusNotif.data) : {};
const order =
ordersByOrderId[orderId] ??
(fills?.length
? {
...fills[fills.length - 1],
id: orderId,
createdAtMilliseconds: Math.max(
...fills.map((fill) => fill.createdAtMilliseconds)
const params = Object.fromEntries(
Object.entries(parsedData).map(([key, value]) => {
return [key, parseStringParamsForNotification({ stringGetter, value })];
})
);
switch (abacusNotificationType) {
case 'order': {
trigger(
abacusNotif.id,
{
icon: abacusNotif.image && <$Icon src={abacusNotif.image} alt="" />,
title: stringGetter({ key: abacusNotif.title }),
body: abacusNotif.text ? stringGetter({ key: abacusNotif.text, params }) : '',
toastDuration: DEFAULT_TOAST_AUTO_CLOSE_MS,
toastSensitivity: 'foreground',
renderCustomBody: ({ isToast, notification }) => (
<TradeNotification
isToast={isToast}
data={parsedData}
notification={notification}
/>
),
status: AbacusOrderStatus.filled,
}
: undefined);
if (order)
trigger(
order.id,
{
icon: (
<OrderStatusIcon status={order.status} totalFilled={order.totalFilled ?? 0} />
),
title: `${stringGetter({
key: order.resources.typeStringKey as StringKey,
})} ${
order.status === AbacusOrderStatus.open && (order?.totalFilled ?? 0) > 0
? stringGetter({ key: STRING_KEYS.PARTIALLY_FILLED })
: stringGetter({ key: order.resources.statusStringKey as StringKey })
}`,
description: `${stringGetter({
key: ORDER_SIDE_STRINGS[ORDER_SIDES[order.side.name]],
})} ${order.size} ${order.marketId} @ $${order.price}`,
actionDescription: 'View Order',
actionAltText: 'View this order in the Orders tab or the Notifications menu.',
toastSensitivity:
order.status === AbacusOrderStatus.pending ? 'foreground' : 'background',
toastDuration: 5000,
},
[order.status.name, order.size],
!order.createdAtMilliseconds || order.createdAtMilliseconds > lastUpdated
);
},
[abacusNotif.updateTimeInMilliseconds, abacusNotif.data],
true
);
break;
}
default:
trigger(
abacusNotif.id,
{
icon: abacusNotif.image && <$Icon src={abacusNotif.image} alt="" />,
title: stringGetter({ key: abacusNotif.title }),
body: abacusNotif.text ? stringGetter({ key: abacusNotif.text, params }) : '',
toastDuration: DEFAULT_TOAST_AUTO_CLOSE_MS,
toastSensitivity: 'foreground',
},
[abacusNotif.updateTimeInMilliseconds, abacusNotif.data]
);
break;
}
}
}, [orderIds]);
}, [abacusNotifications, stringGetter]);
},
useNotificationAction: () => {
const dispatch = useDispatch();
const orders = useSelector(getSubaccountOrders, shallowEqual) || [];
const ordersById = groupBy(orders, 'id');
const fills = useSelector(getSubaccountFills, shallowEqual) || [];
const fillsById = groupBy(fills, 'id');
const marketIds = useSelector(getMarketIds, shallowEqual);
const navigate = useNavigate();
// useNotificationAction: () => {
// const dispatch = useDispatch();
// const orders = useSelector(getSubaccountOrders, shallowEqual) || [];
// const ordersByOrderId = Object.fromEntries(orders.map((order) => [order.id, order]));
return (notificationId: string) => {
const [abacusNotificationType = '', id = ''] = notificationId.split(':');
// const fills = useSelector(getSubaccountFills, shallowEqual) || [];
// const fillsByOrderId = groupBy(fills, (fill) => fill.orderId);
// return (id) => {
// if (ordersByOrderId[id]) {
// dispatch(
// openDialog({
// type: DialogTypes.OrderDetails,
// dialogProps: { orderId: id },
// })
// );
// } else if (fillsByOrderId[id]) {
// const fillId = fillsByOrderId[id][0].id;
// dispatch(
// openDialog({
// type: DialogTypes.FillDetails,
// dialogProps: { fillId },
// })
// );
// }
// };
// },
} as NotificationTypeConfig<string, [string, number]>,
if (ordersById[id]) {
dispatch(
openDialog({
type: DialogTypes.OrderDetails,
dialogProps: { orderId: id },
})
);
} else if (fillsById[id]) {
dispatch(
openDialog({
type: DialogTypes.FillDetails,
dialogProps: { fillId: id },
})
);
} else if (marketIds.includes(id)) {
navigate(`${AppRoute.Trade}/${id}`, {
replace: true,
});
}
};
},
},
{
type: NotificationType.SquidTransfer,
useTrigger: ({ trigger }) => {
const stringGetter = useStringGetter();
const { transferNotifications } = useLocalNotifications();
const getTitleStringKey = useCallback((type: 'deposit' | 'withdrawal', finished: boolean) => {
if (type === 'deposit' && !finished) return STRING_KEYS.DEPOSIT_IN_PROGRESS;
if (type === 'deposit' && finished) return STRING_KEYS.DEPOSIT;
if (type === 'withdrawal' && !finished) return STRING_KEYS.WITHDRAW_IN_PROGRESS;
return STRING_KEYS.WITHDRAW;
}, []);
const { selectedNetwork } = useSelectedNetwork();
useEffect(() => {
for (const transfer of transferNotifications) {
const { fromChainId, status, txHash, toAmount } = transfer;
const finished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing';
const type = fromChainId === TESTNET_CHAIN_ID ? 'withdrawal' : 'deposit';
// @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 isFinished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing';
const icon = <Icon iconName={isFinished ? IconName.Transfer : IconName.Clock} />;
// TODO: confirm with design what the description should be
const description = (
<div>
<Styled.TransferText>
{type === 'deposit' ? 'Deposit of ' : 'Withdraw of '}
<Output type={OutputType.Fiat} value={toAmount} />
</Styled.TransferText>
const type =
fromChainId === ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId
? 'withdrawal'
: 'deposit';
{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>
);
const title = stringGetter({
key: {
deposit: isFinished ? STRING_KEYS.DEPOSIT : STRING_KEYS.DEPOSIT_IN_PROGRESS,
withdrawal: isFinished ? STRING_KEYS.WITHDRAW : STRING_KEYS.WITHDRAW_IN_PROGRESS,
}[type],
});
const toChainEta = status?.toChain?.chainData?.estimatedRouteDuration || 0;
const estimatedDuration = formatSeconds(Math.max(toChainEta, 0));
const body = stringGetter({
key: STRING_KEYS.DEPOSIT_STATUS,
params: {
AMOUNT_USD: `${toAmount} ${DydxChainAsset.USDC.toUpperCase()}`,
ESTIMATED_DURATION: estimatedDuration,
},
});
trigger(
txHash,
{
icon: <Icon iconName={finished ? IconName.Transfer : IconName.Clock} />,
title: stringGetter({ key: getTitleStringKey(type, finished) }),
description: description,
customContent: (
<TransferStatusToast
icon,
title,
body,
renderCustomBody: ({ isToast, notification }) => (
<TransferStatusNotification
isToast={isToast}
slotIcon={icon}
slotTitle={title}
transfer={transfer}
type={type}
toAmount={transfer.toAmount}
triggeredAt={transfer.triggeredAt}
status={transfer.status}
notification={notification}
/>
),
customMenuContent: !finished && (
<div>
{description}
<TransferStatusSteps status={transfer.status} type={type} />
</div>
),
toastSensitivity: 'foreground',
},
[]
[isFinished]
);
}
}, [transferNotifications]);
}, [transferNotifications, stringGetter]);
},
useNotificationAction: () => {
return () => {};
},
},
] satisfies NotificationTypeConfig[];
{
type: NotificationType.ReleaseUpdates,
useTrigger: ({ trigger }) => {
const stringGetter = useStringGetter();
const Styled: Record<string, AnyStyledComponent> = {};
useEffect(() => {
trigger(
'rewards-and-full-trading-live',
{
icon: <AssetIcon symbol="DYDX" />,
title: stringGetter({ key: 'NOTIFICATIONS.RELEASE_REWARDS_AND_FULL_TRADING.TITLE' }),
body: stringGetter({
key: 'NOTIFICATIONS.RELEASE_REWARDS_AND_FULL_TRADING.BODY',
params: {
DOS_BLOGPOST: (
<$Link
href="https://www.dydxopsdao.com/blog/deep-dive-full-trading"
target="_blank"
rel="noopener noreferrer"
>
{stringGetter({ key: STRING_KEYS.HERE })}
</$Link>
),
TRADING_BLOGPOST: (
<$Link
href="https://dydx.exchange/blog/v4-full-trading"
target="_blank"
rel="noopener noreferrer"
>
{stringGetter({ key: STRING_KEYS.HERE })}
</$Link>
),
},
}),
toastSensitivity: 'foreground',
},
[]
);
}, [stringGetter]);
},
useNotificationAction: () => {
return () => {};
},
},
];
Styled.TransferText = styled.span`
display: inline-flex;
align-items: center;
gap: 0.5ch;
const $Icon = styled.img`
height: 1.5rem;
width: 1.5rem;
`;
Styled.ErrorMessage = styled.div`
max-width: 13rem;
const $Link = styled.a`
--link-color: var(--color-text-2);
`;
+82 -36
View File
@@ -1,11 +1,13 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { LocalStorageKey } from '@/constants/localStorage';
import { LOCAL_STORAGE_VERSIONS, LocalStorageKey } from '@/constants/localStorage';
import {
type Notification,
type NotificationDisplayData,
type NotificationPreferences,
type Notifications,
NotificationStatus,
NotificationType,
} from '@/constants/notifications';
import { useLocalStorage } from './useLocalStorage';
@@ -13,8 +15,10 @@ import { notificationTypes } from './useNotificationTypes';
import { renderSvgToDataUrl } from '../lib/renderSvgToDataUrl';
const NotificationsContext = createContext<ReturnType<typeof useNotificationsContext> | undefined>(
undefined
type NotificationsContextType = ReturnType<typeof useNotificationsContext>;
const NotificationsContext = createContext<NotificationsContextType>(
{} as NotificationsContextType
);
NotificationsContext.displayName = 'Notifications';
@@ -27,6 +31,7 @@ export const useNotifications = () => useContext(NotificationsContext)!;
const useNotificationsContext = () => {
// Local storage
// const [notifications, setNotifications] = useState<Notifications>({});
const [notifications, setNotifications] = useLocalStorage<Notifications>({
key: LocalStorageKey.Notifications,
defaultValue: {},
@@ -37,12 +42,25 @@ const useNotificationsContext = () => {
defaultValue: Date.now(),
});
const [notificationPreferences, setNotificationPreferences] =
useLocalStorage<NotificationPreferences>({
key: LocalStorageKey.NotificationPreferences,
defaultValue: {
[NotificationType.AbacusGenerated]: true,
[NotificationType.SquidTransfer]: true,
version: LOCAL_STORAGE_VERSIONS[LocalStorageKey.NotificationPreferences],
},
});
useEffect(() => {
setNotificationsLastUpdated(Date.now());
}, [notifications]);
const getKey = <T extends string | number>(notification: Pick<Notification<T>, 'type' | 'id'>) =>
`${notification.type}/${notification.id}`;
const getKey = useCallback(
<T extends string | number>(notification: Pick<Notification<T>, 'type' | 'id'>) =>
`${notification.type}/${notification.id}`,
[]
);
// Display data
const [notificationsDisplayData, setNotificationsDisplayData] = useState(
@@ -54,29 +72,47 @@ const useNotificationsContext = () => {
[notificationsDisplayData]
);
// Check for version changes
useEffect(() => {
if (
notificationPreferences.version !==
LOCAL_STORAGE_VERSIONS[LocalStorageKey.NotificationPreferences]
) {
setNotificationPreferences({
[NotificationType.AbacusGenerated]: true,
[NotificationType.SquidTransfer]: true,
[NotificationType.ReleaseUpdates]: true,
version: LOCAL_STORAGE_VERSIONS[LocalStorageKey.NotificationPreferences],
});
}
}, []);
// Status changes
const updateStatus = useCallback(
(notification: Notification, status: NotificationStatus) => {
notification.status = status;
notification.timestamps[notification.status] = Date.now();
setNotifications({ ...notifications });
setNotifications({ ...notifications, [getKey(notification)]: notification });
},
[notifications]
[notifications, getKey]
);
const { markUnseen, markSeen, markCleared } = useMemo(
() => ({
markUnseen: (notification: Notification) => {
if (notification.status < NotificationStatus.Unseen)
if (notification.status < NotificationStatus.Unseen) {
updateStatus(notification, NotificationStatus.Unseen);
}
},
markSeen: (notification: Notification) => {
if (notification.status < NotificationStatus.Seen)
if (notification.status < NotificationStatus.Seen) {
updateStatus(notification, NotificationStatus.Seen);
}
},
markCleared: (notification: Notification) => {
if (notification.status < NotificationStatus.Cleared)
if (notification.status < NotificationStatus.Cleared) {
updateStatus(notification, NotificationStatus.Cleared);
}
},
}),
[updateStatus]
@@ -97,33 +133,37 @@ const useNotificationsContext = () => {
const notification = notifications[key];
// New unique key - create new notification
if (!notification) {
const notification = (notifications[key] = {
id,
type,
timestamps: {},
updateKey,
} as Notification);
// Filter out notifications that are not enabled
if (notificationPreferences[type] !== false) {
// New unique key - create new notification
if (!notification) {
const notification = (notifications[key] = {
id,
type,
timestamps: {},
updateKey,
} as Notification);
updateStatus(
notification,
isNew ? NotificationStatus.Triggered : NotificationStatus.Cleared
);
} else if (JSON.stringify(updateKey) !== JSON.stringify(notification.updateKey)) {
// updateKey changed - update existing notification
updateStatus(
notification,
isNew ? NotificationStatus.Triggered : NotificationStatus.Cleared
);
}
const notification = notifications[key];
// updateKey changed - update existing notification
else if (JSON.stringify(updateKey) !== JSON.stringify(notification.updateKey)) {
const notification = notifications[key];
notification.updateKey = updateKey;
updateStatus(notification, NotificationStatus.Updated);
notification.updateKey = updateKey;
updateStatus(notification, NotificationStatus.Updated);
}
} else {
// Notification is disabled - remove it
delete notifications[key];
}
notificationsDisplayData[key] = displayData;
setNotificationsDisplayData({ ...notificationsDisplayData });
},
[notifications, updateStatus]
[notifications, updateStatus, notificationPreferences[type]]
),
lastUpdated: notificationsLastUpdated,
@@ -136,10 +176,8 @@ const useNotificationsContext = () => {
)
);
const onNotificationAction = useCallback(
async (notification: Notification) => await actions[notification.type]?.(notification.id),
[actions]
);
const onNotificationAction = async (notification: Notification) =>
await actions[notification.type]?.(notification.id);
// Push notifications
const [hasEnabledPush, setHasEnabledPush] = useLocalStorage({
@@ -186,7 +224,7 @@ const useNotificationsContext = () => {
renotify: true,
tag: getKey(notification),
data: notification,
body: displayData.description,
description: displayData.body,
icon: iconUrl ?? '/favicon.svg',
badge: iconUrl ?? '/favicon.svg',
image: iconUrl ?? '/favicon.svg',
@@ -208,7 +246,7 @@ const useNotificationsContext = () => {
setPushNotificationsLastUpdated(Date.now());
})();
}, [hasEnabledPush, notifications, onNotificationAction, markSeen]);
}, [hasEnabledPush, notifications, markSeen]);
// Menu state
const [isMenuOpen, setIsMenuOpen] = useState(false);
@@ -239,5 +277,13 @@ const useNotificationsContext = () => {
// Menu state
isMenuOpen,
setIsMenuOpen,
// Notification Preferences
notificationPreferences,
setNotificationPreferences,
getNotificationPreferenceForType: useCallback(
(type: NotificationType) => notificationPreferences[type],
[notificationPreferences]
),
};
};
-50
View File
@@ -1,50 +0,0 @@
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { Squid } from '@0xsquid/sdk';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { getSelectedNetwork } from '@/state/appSelectors';
export const NATIVE_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
export const STATUS_ERROR_GRACE_PERIOD = 120_000;
const useSquidContext = () => {
const selectedNetwork = useSelector(getSelectedNetwork);
const [_, setInitialized] = useState(false);
const initializeClient = async () => {
setInitialized(false);
if (!squid) return;
await squid.init();
setInitialized(true);
};
const squid = useMemo(
() =>
new Squid({
baseUrl: ENVIRONMENT_CONFIG_MAP[selectedNetwork]?.endpoints['0xsquid'],
integratorId: ENVIRONMENT_CONFIG_MAP[selectedNetwork]?.squidIntegratorId,
}),
[selectedNetwork]
);
useEffect(() => {
if (squid) {
initializeClient();
}
}, [squid]);
return squid;
};
type SquidContextType = ReturnType<typeof useSquidContext>;
const SquidContext = createContext<SquidContextType | undefined>(undefined);
SquidContext.displayName = '0xSquid';
export const SquidProvider = ({ ...props }) => (
<SquidContext.Provider value={useSquidContext()} {...props} />
);
export const useSquid = () => useContext(SquidContext);
+91 -60
View File
@@ -48,7 +48,7 @@ export const useSubaccount = () => useContext(SubaccountContext);
export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: LocalWallet }) => {
const dispatch = useDispatch();
const { usdcDenom } = useTokenConfigs();
const { usdcDenom, usdcDecimals } = useTokenConfigs();
const { compositeClient, faucetClient } = useDydxClient();
const { getFaucetFunds } = useMemo(
@@ -79,16 +79,34 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
subaccountClient: SubaccountClient;
assetId?: number;
amount: number;
}) => await compositeClient?.depositToSubaccount(subaccountClient, amount.toString()),
}) => {
try {
return await compositeClient?.depositToSubaccount(
subaccountClient,
amount.toFixed(usdcDecimals)
);
} catch (error) {
log('useSubaccount/depositToSubaccount', error);
throw error;
}
},
withdrawFromSubaccount: async ({
subaccountClient,
amount,
}: {
subaccountClient: SubaccountClient;
amount: number;
}) => await compositeClient?.withdrawFromSubaccount(subaccountClient, amount.toString()),
}) => {
try {
return await compositeClient?.withdrawFromSubaccount(
subaccountClient,
amount.toFixed(usdcDecimals)
);
} catch (error) {
log('useSubaccount/withdrawFromSubaccount', error);
throw error;
}
},
transferFromSubaccountToAddress: async ({
subaccountClient,
assetId = 0,
@@ -99,27 +117,33 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
assetId?: number;
amount: number;
recipient: string;
}) =>
await compositeClient?.validatorClient.post.send(
subaccountClient?.wallet,
() =>
new Promise((resolve) => {
const msg =
compositeClient?.validatorClient.post.composer.composeMsgWithdrawFromSubaccount(
subaccountClient.address,
subaccountClient.subaccountNumber,
assetId,
Long.fromNumber(amount * QUANTUM_MULTIPLIER),
recipient
);
}) => {
try {
return await compositeClient?.validatorClient.post.send(
subaccountClient?.wallet,
() =>
new Promise((resolve) => {
const msg =
compositeClient?.validatorClient.post.composer.composeMsgWithdrawFromSubaccount(
subaccountClient.address,
subaccountClient.subaccountNumber,
assetId,
Long.fromNumber(amount * QUANTUM_MULTIPLIER),
recipient
);
resolve([msg]);
}),
false,
undefined,
undefined,
Method.BroadcastTxCommit
),
resolve([msg]);
}),
false,
undefined,
undefined,
Method.BroadcastTxCommit
);
} catch (error) {
log('useSubaccount/transferFromSubaccountToAddress', error);
throw error;
}
},
transferNativeToken: async ({
subaccountClient,
@@ -129,24 +153,30 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
subaccountClient: SubaccountClient;
amount: number;
recipient: string;
}) =>
await compositeClient?.validatorClient.post.send(
subaccountClient.wallet,
() =>
new Promise((resolve) => {
const msg = compositeClient?.sendTokenMessage(
subaccountClient.wallet,
amount.toString(),
recipient,
);
}) => {
try {
return await compositeClient?.validatorClient.post.send(
subaccountClient.wallet,
() =>
new Promise((resolve) => {
const msg = compositeClient?.sendTokenMessage(
subaccountClient.wallet,
amount.toString(),
recipient
);
resolve([msg]);
}),
false,
compositeClient?.validatorClient?.post.defaultDydxGasPrice,
undefined,
Method.BroadcastTxCommit
),
resolve([msg]);
}),
false,
compositeClient?.validatorClient?.post.defaultDydxGasPrice,
undefined,
Method.BroadcastTxCommit
);
} catch (error) {
log('useSubaccount/transferNativeToken', error);
throw error;
}
},
sendSquidWithdrawFromSubaccount: async ({
subaccountClient,
@@ -158,20 +188,27 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
payload: string;
}) => {
if (!compositeClient) throw new Error('client not initialized');
try {
const transaction = JSON.parse(payload);
const transaction = JSON.parse(payload);
const msg = compositeClient.withdrawFromSubaccountMessage(
subaccountClient,
amount.toFixed(usdcDecimals)
);
const ibcMsg: EncodeObject = {
typeUrl: transaction.msgTypeUrl,
value: transaction.msg,
};
const msg = compositeClient.withdrawFromSubaccountMessage(subaccountClient, amount.toString());
const ibcMsg: EncodeObject = {
typeUrl: transaction.msgTypeUrl,
value: transaction.msg,
};
return await compositeClient.send(
subaccountClient.wallet,
() => Promise.resolve([msg, ibcMsg]),
false
);
return await compositeClient.send(
subaccountClient.wallet,
() => Promise.resolve([msg, ibcMsg]),
false
);
} catch (error) {
log('useSubaccount/sendSquidWithdrawFromSubaccount', error);
throw error;
}
},
}),
[compositeClient]
@@ -313,11 +350,6 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
placeOrderParams = abacusStateManager.placeOrder(callback);
}
track(AnalyticsEvent.TradePlaceOrder, {
...placeOrderParams,
isClosePosition,
} as HumanReadablePlaceOrderPayload & { isClosePosition: boolean });
return placeOrderParams;
},
[subaccountClient]
@@ -346,7 +378,6 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
}) => {
const callback = (success: boolean, parsingError?: Nullable<ParsingError>) => {
if (success) {
track(AnalyticsEvent.TradeCancelOrder);
onSuccess?.();
} else {
onError?.({ errorStringKey: parsingError?.stringKey });
+53
View File
@@ -0,0 +1,53 @@
import { createContext, useContext, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import queryString from 'query-string';
class TestFlags {
public queryParams: queryString.ParsedQuery<string>;
constructor() {
this.queryParams = {};
}
setQueryParams(flags: queryString.ParsedQuery<string>) {
this.queryParams = flags;
}
get displayInitializingMarkets() {
return !!this.queryParams.displayInitializingMarkets;
}
/**
* @description We are temporarily displaying a subset of ACTIVE markets, this flag will allow you to see all ACTIVE markets
*/
get displayAllMarkets() {
return !!this.queryParams.displayAllMarkets;
}
}
export const testFlags = new TestFlags();
const useTestFlagsContext = () => {
const [queryParams, setQueryParams] = useState<queryString.ParsedQuery<string>>({});
const location = useLocation();
useEffect(() => {
const parsedQueryParams = queryString.parse(location.search.substring(-1));
testFlags.setQueryParams(parsedQueryParams);
setQueryParams(parsedQueryParams);
}, []);
return {
...queryParams,
};
};
type TestFlagsContextType = ReturnType<typeof useTestFlagsContext>;
const TestFlagsContext = createContext<TestFlagsContextType>({} as TestFlagsContextType);
TestFlagsContext.displayName = 'TestFlags';
export const TestFlagsProvider = ({ ...props }) => (
<TestFlagsContext.Provider value={useTestFlagsContext()} {...props} />
);
export const useTestFlags = () => useContext(TestFlagsContext);
+4
View File
@@ -19,8 +19,10 @@ export const useTokenConfigs = (): {
},
};
usdcDenom: string;
usdcDecimals: number;
usdcLabel: string;
chainTokenDenom: string;
chainTokenDecimals: number;
chainTokenLabel: string;
} => {
const { selectedNetwork } = useSelectedNetwork();
@@ -29,8 +31,10 @@ export const useTokenConfigs = (): {
return {
tokensConfigs,
usdcDenom: tokensConfigs[DydxChainAsset.USDC].denom,
usdcDecimals: tokensConfigs[DydxChainAsset.USDC].decimals,
usdcLabel: tokensConfigs[DydxChainAsset.USDC].name,
chainTokenDenom: tokensConfigs[DydxChainAsset.CHAINTOKEN].denom,
chainTokenDecimals: tokensConfigs[DydxChainAsset.CHAINTOKEN].decimals,
chainTokenLabel: tokensConfigs[DydxChainAsset.CHAINTOKEN].name,
};
};
+33
View File
@@ -0,0 +1,33 @@
import { getTradeFormInputs } from '@/state/inputsSelectors';
import { useEffect } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { TradeInputField } from '@/constants/abacus';
import abacusStateManager from '@/lib/abacus';
export const useTradeFormInputs = () => {
const tradeFormInputValues = useSelector(getTradeFormInputs, shallowEqual);
const { limitPriceInput, triggerPriceInput, trailingPercentInput } = tradeFormInputValues;
useEffect(() => {
const floatValue = parseFloat(triggerPriceInput);
abacusStateManager.setTradeValue({
value: floatValue,
field: TradeInputField.triggerPrice,
});
}, [triggerPriceInput]);
useEffect(() => {
const floatValue = parseFloat(limitPriceInput);
abacusStateManager.setTradeValue({ value: floatValue, field: TradeInputField.limitPrice });
}, [limitPriceInput]);
useEffect(() => {
const floatValue = parseFloat(trailingPercentInput);
abacusStateManager.setTradeValue({
value: floatValue,
field: TradeInputField.trailingPercent,
});
}, [trailingPercentInput]);
};
+11 -9
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useState, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { LocalStorageKey } from '@/constants/localStorage';
@@ -90,19 +90,21 @@ export const useWalletConnection = () => {
const selectedNetwork = useSelector(getSelectedNetwork);
const walletConnectConfig = ENVIRONMENT_CONFIG_MAP[selectedNetwork].wallets.walletconnect;
const { connectAsync: connectWagmi } =
walletType && walletConnectionType
? useConnectWagmi({
connector: resolveWagmiConnector({
const wagmiConnector = useMemo(
() =>
walletType && walletConnectionType
? resolveWagmiConnector({
walletType,
walletConnection: {
type: walletConnectionType,
},
walletConnectConfig,
}),
})
: useConnectWagmi();
})
: undefined,
[walletConnectConfig, walletType, walletConnectionType]
);
const { connectAsync: connectWagmi } = useConnectWagmi({ connector: wagmiConnector })
const { suggestAndConnect: connectGraz } = useConnectGraz();
const connectWallet = useCallback(
+6
View File
@@ -0,0 +1,6 @@
<svg width="27" height="28" viewBox="0 0 27 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame">
<path id="Vector" d="M9.5 15L12.5 18L17.5 11M25.5 14C25.5 15.5759 25.1896 17.1363 24.5866 18.5922C23.9835 20.0481 23.0996 21.371 21.9853 22.4853C20.871 23.5996 19.5481 24.4835 18.0922 25.0866C16.6363 25.6896 15.0759 26 13.5 26C11.9241 26 10.3637 25.6896 8.9078 25.0866C7.45189 24.4835 6.12902 23.5996 5.01472 22.4853C3.90042 21.371 3.0165 20.0481 2.41345 18.5922C1.81039 17.1363 1.5 15.5759 1.5 14C1.5 10.8174 2.76428 7.76516 5.01472 5.51472C7.26516 3.26428 10.3174 2 13.5 2C16.6826 2 19.7348 3.26428 21.9853 5.51472C24.2357 7.76516 25.5 10.8174 25.5 14Z" stroke="#3ED9A4" stroke-opacity="0.5" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M9.5 15L12.5 18L17.5 11" stroke="#3ED9A4" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 914 B

+1 -1
View File
@@ -1,4 +1,4 @@
<svg width="13" height="12" viewBox="0 0 13 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 6.39098L5.68182 10L11 2" stroke="currentColor" stroke-width="3" stroke-linecap="round"
<path d="M2 6.39098L5.68182 10L11 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
</svg>

Before

Width:  |  Height:  |  Size: 235 B

After

Width:  |  Height:  |  Size: 235 B

+2
View File
@@ -10,6 +10,7 @@ export { default as CautionCircleIcon } from './caution-circle.svg';
export { default as CautionCircleStrokeIcon } from './caution-circle-stroke.svg';
export { default as ChatIcon } from './chat-bubble.svg';
export { default as CheckIcon } from './check.svg';
export { default as CheckCircleIcon } from './check-circle.svg';
export { default as ChevronLeftIcon } from './chevron-left.svg';
export { default as ChevronRightIcon } from './chevron-right.svg';
export { default as ClockIcon } from './clock.svg';
@@ -31,6 +32,7 @@ export { default as GiftboxIcon } from './giftbox.svg';
export { default as HelpCircleIcon } from './help-circle.svg';
export { default as HideIcon } from './hide.svg';
export { default as HistoryIcon } from './history.svg';
export { default as LeaderboardIcon } from './leaderboard.svg';
export { default as LinkOutIcon } from './link-out.svg';
export { default as LockIcon } from './lock.svg';
export { default as LogoShortIcon } from './logo-short';
+3
View File
@@ -0,0 +1,3 @@
<svg width="13" height="14" viewBox="0 0 13 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.0996094 2.60156C0.0996094 2.07113 0.310323 1.56242 0.685396 1.18735C1.06047 0.812276 1.56918 0.601563 2.09961 0.601562H10.8996C11.43 0.601563 11.9387 0.812276 12.3138 1.18735C12.6889 1.56242 12.8996 2.07113 12.8996 2.60156C12.8996 3.132 12.6889 3.6407 12.3138 4.01578C11.9387 4.39085 11.43 4.60156 10.8996 4.60156H2.09961C1.56918 4.60156 1.06047 4.39085 0.685396 4.01578C0.310323 3.6407 0.0996094 3.132 0.0996094 2.60156ZM0.699609 6.26796C0.540479 6.26796 0.387867 6.33118 0.275345 6.4437C0.162823 6.55622 0.0996094 6.70883 0.0996094 6.86796C0.0996094 7.02709 0.162823 7.1797 0.275345 7.29223C0.387867 7.40475 0.540479 7.46796 0.699609 7.46796H12.2996C12.4587 7.46796 12.6114 7.40475 12.7239 7.29223C12.8364 7.1797 12.8996 7.02709 12.8996 6.86796C12.8996 6.70883 12.8364 6.55622 12.7239 6.4437C12.6114 6.33118 12.4587 6.26796 12.2996 6.26796H0.699609ZM0.699609 9.13196C0.540479 9.13196 0.387867 9.19518 0.275345 9.3077C0.162823 9.42022 0.0996094 9.57283 0.0996094 9.73196C0.0996094 9.89109 0.162823 10.0437 0.275345 10.1562C0.387867 10.2687 0.540479 10.332 0.699609 10.332H12.2996C12.4587 10.332 12.6114 10.2687 12.7239 10.1562C12.8364 10.0437 12.8996 9.89109 12.8996 9.73196C12.8996 9.57283 12.8364 9.42022 12.7239 9.3077C12.6114 9.19518 12.4587 9.13196 12.2996 9.13196H0.699609ZM0.699609 12.0016C0.540479 12.0016 0.387867 12.0648 0.275345 12.1773C0.162823 12.2898 0.0996094 12.4424 0.0996094 12.6016C0.0996094 12.7607 0.162823 12.9133 0.275345 13.0258C0.387867 13.1383 0.540479 13.2016 0.699609 13.2016H12.2996C12.4587 13.2016 12.6114 13.1383 12.7239 13.0258C12.8364 12.9133 12.8996 12.7607 12.8996 12.6016C12.8996 12.4424 12.8364 12.2898 12.7239 12.1773C12.6114 12.0648 12.4587 12.0016 12.2996 12.0016H0.699609Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+12 -4
View File
@@ -1,5 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<g fill="#FFFFFF">
<path d="M461.2 128H80c-8.84 0-16-7.16-16-16s7.16-16 16-16h384c8.84 0 16-7.16 16-16 0-26.51-21.49-48-48-48H64C28.65 32 0 60.65 0 96v320c0 35.35 28.65 64 64 64h397.2c28.02 0 50.8-21.53 50.8-48V176c0-26.47-22.78-48-50.8-48zM416 336c-17.67 0-32-14.33-32-32s14.33-32 32-32 32 14.33 32 32-14.33 32-32 32z"/>
</g>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Wallet" clip-path="url(#clip0_14122_34311)">
<g id="Group 151">
<path id="Exclude" fill-rule="evenodd" clip-rule="evenodd" d="M11.4979 10.9976C11.1764 11.3193 10.7404 11.5 10.2857 11.5L1.71429 11.5C1.25963 11.5 0.823594 11.3193 0.502102 10.9976C0.180612 10.6758 -4.76837e-07 10.2395 -4.76837e-07 9.78455L-4.76837e-07 2.06502C-4.76837e-07 2.51999 0.180612 2.95632 0.502102 3.27803C0.823594 3.59974 1.25963 3.78047 1.71429 3.78047L10.2857 3.78047C10.7404 3.78047 11.1764 3.96121 11.4979 4.28292C11.8194 4.60462 12 5.04096 12 5.49592V9.78455C12 10.2395 11.8194 10.6758 11.4979 10.9976ZM9.64294 8.7126C10.2347 8.7126 10.7144 8.23258 10.7144 7.64044C10.7144 7.0483 10.2347 6.56828 9.64294 6.56828C9.05121 6.56828 8.57151 7.0483 8.57151 7.64044C8.57151 8.23258 9.05121 8.7126 9.64294 8.7126Z" fill="#C8C7D8"/>
<path id="Vector" d="M8.57031 2.92186V2.06413C8.57031 1.60917 8.3897 1.17284 8.06821 0.851128C7.74672 0.529419 7.31068 0.348684 6.85603 0.348684L2.14174 0.348684C1.80075 0.348684 1.47372 0.484235 1.2326 0.725517C0.991486 0.966799 0.856028 1.29405 0.856028 1.63527C0.856028 1.9765 0.991486 2.30374 1.2326 2.54503C1.47372 2.78631 1.80075 2.92186 2.14174 2.92186L8.57031 2.92186Z" fill="#C8C7D8"/>
</g>
</g>
<defs>
<clipPath id="clip0_14122_34311">
<rect width="12" height="12" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 405 B

After

Width:  |  Height:  |  Size: 1.4 KiB

+6 -2
View File
@@ -11,17 +11,19 @@ import { DepositDialog } from '@/views/dialogs/DepositDialog';
import { DisconnectDialog } from '@/views/dialogs/DisconnectDialog';
import { ExchangeOfflineDialog } from '@/views/dialogs/ExchangeOfflineDialog';
import { HelpDialog } from '@/views/dialogs/HelpDialog';
import { ExternalLinkDialog } from '@/views/dialogs/ExternalLinkDialog';
import { ExternalNavKeplrDialog } from '@/views/dialogs/ExternalNavKeplrDialog';
import { MnemonicExportDialog } from '@/views/dialogs/MnemonicExportDialog';
import { MobileSignInDialog } from '@/views/dialogs/MobileSignInDialog';
import { OnboardingDialog } from '@/views/dialogs/OnboardingDialog';
import { PreferencesDialog } from '@/views/dialogs/PreferencesDialog';
import { RateLimitDialog } from '@/views/dialogs/RateLimitDialog';
import { ReceiveDialog } from '@/views/dialogs/ReceiveDialog';
import { RestrictedGeoDialog } from '@/views/dialogs/RestrictedGeoDialog';
import { TradeDialog } from '@/views/dialogs/TradeDialog';
import { TransferDialog } from '@/views/dialogs/TransferDialog';
import { RestrictedWalletDialog } from '@/views/dialogs/RestrictedWalletDialog';
import { WithdrawDialog } from '@/views/dialogs/WithdrawDialog';
import { ManageFundsDialog } from '@/views/dialogs/ManageFundsDialog';
import { OrderDetailsDialog } from '@/views/dialogs/DetailsDialog/OrderDetailsDialog';
import { FillDetailsDialog } from '@/views/dialogs/DetailsDialog/FillDetailsDialog';
@@ -52,16 +54,18 @@ export const DialogManager = () => {
[DialogTypes.FillDetails]: <FillDetailsDialog {...modalProps} />,
[DialogTypes.Help]: <HelpDialog {...modalProps} />,
[DialogTypes.ExternalNavKeplr]: <ExternalNavKeplrDialog {...modalProps} />,
[DialogTypes.ExternalLink]: <ExternalLinkDialog {...modalProps} />,
[DialogTypes.MnemonicExport]: <MnemonicExportDialog {...modalProps} />,
[DialogTypes.MobileSignIn]: <MobileSignInDialog {...modalProps} />,
[DialogTypes.Onboarding]: <OnboardingDialog {...modalProps} />,
[DialogTypes.OrderDetails]: <OrderDetailsDialog {...modalProps} />,
[DialogTypes.Preferences]: <PreferencesDialog {...modalProps} />,
[DialogTypes.RateLimit]: <RateLimitDialog {...modalProps} />,
[DialogTypes.Receive]: <ReceiveDialog {...modalProps} />,
[DialogTypes.RestrictedGeo]: <RestrictedGeoDialog {...modalProps} />,
[DialogTypes.RestrictedWallet]: <RestrictedWalletDialog {...modalProps} />,
[DialogTypes.Trade]: <TradeDialog {...modalProps} />,
[DialogTypes.Transfer]: <TransferDialog {...modalProps} />,
[DialogTypes.Withdraw]: <WithdrawDialog {...modalProps} />,
[DialogTypes.ManageFunds]: <ManageFundsDialog {...modalProps} />,
}[type];
};
+6 -4
View File
@@ -1,9 +1,9 @@
import styled, { type AnyStyledComponent, css } from 'styled-components';
import { AbacusApiStatus } from '@/constants/abacus';
import { ButtonSize } from '@/constants/buttons';
import { ButtonSize, ButtonType } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { ENVIRONMENT_CONFIG_MAP, isDev } from '@/constants/networks';
import { useApiState, useSelectedNetwork, useStringGetter } from '@/hooks';
import { ChatIcon, LinkOutIcon } from '@/icons';
@@ -55,10 +55,12 @@ export const FooterDesktop = () => {
}
>
<Styled.FooterButton
type={statusPage ? ButtonType.Link : ButtonType.Button}
slotLeft={<Styled.StatusDot exchangeStatus={exchangeStatus} />}
slotRight={statusPage && <LinkOutIcon />}
size={ButtonSize.XSmall}
state={{ isDisabled: !statusPage }}
href={statusPage}
>
{label}
</Styled.FooterButton>
@@ -75,7 +77,7 @@ export const FooterDesktop = () => {
)}
</Styled.Row>
{import.meta.env.MODE !== 'production' && (
{isDev && (
<Styled.Details
withSeparators
items={[
@@ -132,7 +134,7 @@ Styled.StatusDot = styled.div<{ exchangeStatus: ExchangeStatus }>`
Styled.FooterButton = styled(Button)`
--button-height: 1.5rem;
--button-radius: 0.25rem;
--button-backgroundColor: var(--color-layer-2);
--button-backgroundColor: transparent;
--button-border: none;
--button-textColor: var(--color-text-0);
+29 -13
View File
@@ -1,7 +1,6 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { useDispatch, useSelector } from 'react-redux';
import { ButtonAction, ButtonSize } from '@/constants/buttons';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization';
import { DEFAULT_MARKETID } from '@/constants/markets';
@@ -11,8 +10,8 @@ import { useShouldShowFooter, useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins';
import { NavigationMenu } from '@/components/NavigationMenu';
import { Icon } from '@/components/Icon';
import { BellIcon, MarketsIcon, PlayIcon, PortfolioIcon, ProfileIcon, TradeIcon } from '@/icons';
import { Icon, IconName } from '@/components/Icon';
import { BellIcon, MarketsIcon, PortfolioIcon, ProfileIcon } from '@/icons';
import { IconButton } from '@/components/IconButton';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
@@ -42,11 +41,9 @@ export const FooterMobile = () => {
value: 'trade',
label: stringGetter({ key: STRING_KEYS.TRADE }),
slotBefore: (
<Styled.IconButton
iconComponent={TradeIcon}
size={ButtonSize.Large}
action={ButtonAction.Primary}
/>
<Styled.StartIcon>
<Icon iconName={IconName.Trade} />
</Styled.StartIcon>
),
href: `${AppRoute.Trade}/${marketId ?? DEFAULT_MARKETID}`,
}
@@ -54,11 +51,9 @@ export const FooterMobile = () => {
value: 'onboarding',
label: stringGetter({ key: STRING_KEYS.ONBOARDING }),
slotBefore: (
<Styled.IconButton
iconComponent={PlayIcon}
size={ButtonSize.Large}
action={ButtonAction.Primary}
/>
<Styled.StartIcon>
<Icon iconName={IconName.Play} />
</Styled.StartIcon>
),
onClick: () => dispatch(openDialog({ type: DialogTypes.Onboarding })),
},
@@ -168,3 +163,24 @@ Styled.IconButton = styled(IconButton)`
Styled.Icon = styled(Icon)`
font-size: 1.5rem;
`;
Styled.StartIcon = styled.div`
display: inline-flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-top: -0.25rem;
width: 3.5rem;
height: 3.5rem;
color: var(--color-text-2);
background-color: var(--color-accent);
border: solid var(--border-width) var(--color-border-white);
border-radius: 50%;
svg {
width: 1.5rem;
height: 1.5rem;
}
`;
+17 -5
View File
@@ -1,6 +1,6 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { Link } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { ButtonShape } from '@/constants/buttons';
import { DialogTypes } from '@/constants/dialogs';
@@ -8,6 +8,10 @@ import { STRING_KEYS } from '@/constants/localization';
import { AppRoute } from '@/constants/routes';
import { LogoShortIcon, BellStrokeIcon } from '@/icons';
import { headerMixins } from '@/styles/headerMixins';
import { layoutMixins } from '@/styles/layoutMixins';
import breakpoints from '@/styles/breakpoints';
import { useTokenConfigs, useStringGetter, useURLConfigs } from '@/hooks';
import { Icon, IconName } from '@/components/Icon';
@@ -21,10 +25,7 @@ import { NotificationsMenu } from '@/views/menus/NotificationsMenu';
import { LanguageSelector } from '@/views/menus/LanguageSelector';
import { openDialog } from '@/state/dialogs';
import { headerMixins } from '@/styles/headerMixins';
import { layoutMixins } from '@/styles/layoutMixins';
import breakpoints from '@/styles/breakpoints';
import { getHasSeenLaunchIncentives } from '@/state/configsSelectors';
export const HeaderDesktop = () => {
const stringGetter = useStringGetter();
@@ -32,6 +33,8 @@ export const HeaderDesktop = () => {
const dispatch = useDispatch();
const { chainTokenLabel } = useTokenConfigs();
const hasSeenLaunchIncentives = useSelector(getHasSeenLaunchIncentives);
const navItems = [
{
group: 'navigation',
@@ -55,6 +58,7 @@ export const HeaderDesktop = () => {
value: chainTokenLabel,
label: chainTokenLabel,
href: `/${chainTokenLabel}`,
slotAfter: !hasSeenLaunchIncentives && <Styled.UnreadIndicator />,
},
{
value: 'MORE',
@@ -236,4 +240,12 @@ Styled.IconButton = styled(IconButton)<{ size?: string }>`
${headerMixins.button}
--button-border: none;
--button-icon-size: 1rem;
--button-padding: 0 0.5em;
`;
Styled.UnreadIndicator = styled.div`
width: 0.4375rem;
height: 0.4375rem;
border-radius: 50%;
background-color: var(--color-accent);
`;
+130 -40
View File
@@ -1,20 +1,27 @@
import { NotificationStatus } from '@/constants/notifications';
import { ButtonSize } from '@/constants/buttons';
import { useMemo, useState } from 'react';
import styled, { css } from 'styled-components';
import { NotificationStatus } from '@/constants/notifications';
import { ButtonShape, ButtonSize } from '@/constants/buttons';
import { useNotifications } from '@/hooks/useNotifications';
import { useBreakpoints } from '@/hooks/useBreakpoints';
import { ChevronLeftIcon } from '@/icons';
import { breakpoints } from '@/styles';
import { ToastArea } from '@/components/ToastArea';
import { Toast } from '@/components/Toast';
import { Button } from '@/components/Button';
import styled from 'styled-components';
import { Toast } from '@/components/Toast';
import { ToastArea } from '@/components/ToastArea';
import { ToggleButton } from '@/components/ToggleButton';
type StyleProps = {
className?: string;
};
const MAX_TOASTS = 10;
export const NotificationsToastArea = ({ className }: StyleProps) => {
const [shouldStackNotifications, setshouldStackNotifications] = useState(true);
const {
notifications,
getKey,
@@ -26,10 +33,9 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
} = useNotifications();
const { isMobile } = useBreakpoints();
return (
<StyledToastArea swipeDirection={isMobile ? 'up' : 'right'} className={className}>
{Object.values(notifications)
const notificationMap = useMemo(() => {
return (
Object.values(notifications)
// Sort by time of first trigger
.sort(
(n1, n2) =>
@@ -41,45 +47,129 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
key: getKey(notification),
displayData: getDisplayData(notification),
}))
.filter(({ displayData }) => displayData)
.map(({ notification, key, displayData }) => (
<Toast
key={key}
isOpen={notification.status < NotificationStatus.Unseen}
slotIcon={displayData.icon}
slotTitle={displayData.title}
slotDescription={displayData.description}
slotCustomContent={displayData.customContent}
slotAction={
<Button size={ButtonSize.Small} onClick={() => onNotificationAction(notification)}>
{displayData.actionDescription}
</Button>
}
actionDescription={displayData.actionDescription}
actionAltText={displayData.actionAltText}
duration={isMenuOpen ? Infinity : displayData.toastDuration}
sensitivity={displayData.toastSensitivity}
setIsOpen={(isOpen, isClosedFromTimeout) => {
if (!isOpen)
if (isClosedFromTimeout)
// Toast timer expired without user interaction
markUnseen(notification);
// Toast interacted with or dismissed
else markSeen(notification);
}}
lastUpdated={notification.timestamps[notification.status]}
/>
))}
.filter(
({ displayData, notification }) =>
displayData && notification.status < NotificationStatus.Unseen
)
.slice(-MAX_TOASTS)
);
}, [notifications, getKey, getDisplayData]);
if (isMenuOpen) return null;
const hasMultipleToasts = notificationMap.length > 1;
return (
<StyledToastArea swipeDirection={isMobile ? 'up' : 'right'} className={className}>
{hasMultipleToasts && (
<StyledToggleButton
shape={ButtonShape.Pill}
size={ButtonSize.XSmall}
isPressed={shouldStackNotifications}
onPressedChange={() => setshouldStackNotifications(!shouldStackNotifications)}
>
<ChevronLeftIcon />
</StyledToggleButton>
)}
{notificationMap.map(({ notification, key, displayData }, idx) => (
<StyledToast
key={key}
isStacked={shouldStackNotifications}
isOpen={notification.status < NotificationStatus.Unseen}
layer={notificationMap.length - 1 - idx}
notification={notification}
slotIcon={displayData.icon}
slotTitle={displayData.title}
slotTitleLeft={displayData.slotTitleLeft}
slotTitleRight={displayData.slotTitleRight}
slotDescription={displayData.body}
slotCustomContent={displayData.renderCustomBody?.({ isToast: true, notification })}
onClick={() => onNotificationAction(notification)}
slotAction={
<Button size={ButtonSize.Small} onClick={() => onNotificationAction(notification)}>
{displayData.actionDescription}
</Button>
}
actionDescription={displayData.actionDescription}
actionAltText={displayData.actionAltText}
duration={displayData.toastDuration ?? Infinity}
sensitivity={displayData.toastSensitivity}
setIsOpen={(isOpen: boolean, isClosedFromTimeout?: boolean) => {
if (!isOpen)
if (isClosedFromTimeout)
// Toast timer expired without user interaction
markUnseen(notification);
// Toast interacted with or dismissed
else markSeen(notification);
}}
lastUpdated={notification.timestamps[notification.status]}
/>
))}
</StyledToastArea>
);
};
const StyledToastArea = styled(ToastArea)`
position: absolute;
width: min(16rem, 100%);
width: min(17.5rem, 100%);
inset: 0 0 0 auto;
padding: 0.75rem 0.75rem 0.75rem 0;
mask-image: linear-gradient(to left, transparent, white 0.5rem);
@media ${breakpoints.mobile} {
width: 100%;
position: fixed;
}
`;
const StyledToast = styled(Toast)<{ isStacked?: boolean; layer: number }>`
// Stacked toast
${({ isStacked, layer }) =>
isStacked
? css`
position: absolute;
left: 0;
top: calc(${layer} * 2px);
right: calc(${layer} * -2px);
`
: css`
margin-bottom: 0.75rem;
`}
`;
const StyledToggleButton = styled(ToggleButton)<{ isPressed: boolean }>`
z-index: 2;
pointer-events: auto;
display: none;
position: absolute;
top: 4px;
left: calc(50% - 0.75rem);
--button-width: 2rem;
--button-height: 1rem;
${StyledToastArea}:hover & {
display: flex;
}
> svg {
width: 4px;
margin-top: 1px;
transform: rotate(-90deg);
${({ isPressed }) =>
!isPressed &&
css`
transform: rotate(90deg);
`}
}
@media ${breakpoints.mobile} {
display: flex;
left: calc(50% - 2rem);
--button-width: 4rem;
--button-height: 2rem;
}
`;
+85 -18
View File
@@ -1,6 +1,7 @@
import Abacus, { type Nullable } from '@dydxprotocol/v4-abacus';
import Long from 'long';
import type { IndexedTx } from '@cosmjs/stargate';
import { GAS_MULTIPLIER, encodeJson } from '@dydxprotocol/v4-client-js';
import {
CompositeClient,
@@ -8,6 +9,7 @@ import {
type LocalWallet,
Network,
NetworkOptimizer,
NobleClient,
SubaccountClient,
ValidatorConfig,
OrderType,
@@ -30,7 +32,7 @@ import {
import { DialogTypes } from '@/constants/dialogs';
import { UNCOMMITTED_ORDER_TIMEOUT_MS } from '@/constants/trade';
import { QUANTUM_MULTIPLIER } from '@/constants/numbers';
import { ENVIRONMENT_CONFIG_MAP, DydxNetwork, isTestnet } from '@/constants/networks';
import { RootStore } from '@/state/_store';
import { addUncommittedOrderClientId, removeUncommittedOrderClientId } from '@/state/account';
@@ -42,8 +44,10 @@ import { log } from '../telemetry';
class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
private compositeClient: CompositeClient | undefined;
private nobleClient: NobleClient | undefined;
private store: RootStore | undefined;
private localWallet: LocalWallet | undefined;
private nobleWallet: LocalWallet | undefined;
constructor() {
this.compositeClient = undefined;
@@ -58,6 +62,11 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
this.localWallet = localWallet;
}
setNobleWallet(nobleWallet: LocalWallet) {
this.nobleWallet = nobleWallet;
this.nobleClient?.connect(nobleWallet);
}
async connectNetwork(
paramsInJson: Nullable<string>,
callback: (p0: Nullable<string>) => void
@@ -69,6 +78,7 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
websocketUrl,
validatorUrl,
chainId,
nobleValidatorUrl,
USDC_DENOM,
USDC_DECIMALS,
USDC_GAS_DENOM,
@@ -99,7 +109,8 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
);
this.compositeClient = compositeClient;
this.nobleClient = new NobleClient(nobleValidatorUrl);
if (this.nobleWallet) await this.nobleClient.connect(this.nobleWallet);
// Dispatch custom event to notify other parts of the app that the network has been connected
const customEvent = new CustomEvent('abacus:connectNetwork', {
detail: parsedParams,
@@ -195,14 +206,19 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
throw new StatefulOrderError('Stateful order has failed to commit.', tx);
}
const parsedTx = this.parseToPrimitives(tx);
const hash = parsedTx?.hash;
const encodedTx = encodeJson(tx);
const parsedTx = JSON.parse(encodedTx);
const hash = parsedTx.hash.toUpperCase();
if (import.meta.env.MODE === 'production') {
console.log(`https://testnet.mintscan.io/dydx-testnet/txs/${hash}`);
if (isTestnet) {
console.log(
`${ENVIRONMENT_CONFIG_MAP[
this.compositeClient.network.getString() as DydxNetwork
]?.links?.mintscan?.replace('{tx_hash}', hash.toString())}`
);
} else console.log(`txHash: ${hash}`);
return JSON.stringify(parsedTx);
return encodedTx;
} catch (error) {
if (error?.name !== 'BroadcastError') {
log('DydxChainTransactions/placeOrderTransaction', error);
@@ -229,12 +245,17 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
orderFlags,
clobPairId,
goodTilBlock || undefined,
goodTilBlockTime || undefined,
goodTilBlockTime || undefined
);
const parsedTx = this.parseToPrimitives(tx);
const encodedTx = encodeJson(tx);
return JSON.stringify(parsedTx);
if (import.meta.env.MODE === 'development') {
const parsedTx = JSON.parse(encodedTx);
console.log(parsedTx, parsedTx.hash.toUpperCase());
}
return encodedTx;
} catch (error) {
log('DydxChainTransactions/cancelOrderTransaction', error);
@@ -295,15 +316,11 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
if (!this.localWallet) {
throw new Error('Missing compositeClient or localWallet');
}
const msg = compositeClient?.sendTokenMessage(
this.localWallet,
amount,
recipient,
);
const msg = compositeClient?.sendTokenMessage(this.localWallet, amount, recipient);
resolve([msg]);
}),
this.compositeClient?.validatorClient?.post.defaultDydxGasPrice,
this.compositeClient?.validatorClient?.post.defaultDydxGasPrice
);
const parsedTx = this.parseToPrimitives(tx);
@@ -318,6 +335,46 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
}
}
async sendNobleIBC(
params: {
msgTypeUrl: string,
msg: any,
}
): Promise<string> {
if (!this.nobleClient?.isConnected) {
throw new Error('Missing nobleClient or localWallet');
}
try {
const ibcMsg = {
typeUrl: params.msgTypeUrl, // '/ibc.applications.transfer.v1.MsgTransfer',
value: params.msg,
};
const fee = await this.nobleClient.simulateTransaction([ibcMsg]);
// take out fee from amount before sweeping
const amount = parseInt(ibcMsg.value.token.amount, 10) -
Math.floor(parseInt(fee.amount[0].amount, 10) * GAS_MULTIPLIER);
if (amount <= 0) {
throw new Error('noble balance does not cover fees');
}
ibcMsg.value.token.amount = amount.toString();
const tx = await this.nobleClient.send([ibcMsg]);
const parsedTx = this.parseToPrimitives(tx);
return JSON.stringify(parsedTx);
} catch (error) {
log('DydxChainTransactions/sendNobleIBC', error);
return JSON.stringify({
error,
});
}
}
async transaction(
type: TransactionTypes,
paramsInJson: Abacus.Nullable<string>,
@@ -347,6 +404,11 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
callback(result);
break;
}
case TransactionType.SendNobleIBC: {
const result = await this.sendNobleIBC(params);
callback(result);
break;
}
default: {
break;
}
@@ -434,8 +496,13 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
const parseDelegations = this.parseToPrimitives(delegations);
callback(JSON.stringify(parseDelegations));
break;
// Do not implement Transfers (yet)
case QueryType.Transfers:
case QueryType.GetNobleBalance:
if (this.nobleClient?.isConnected) {
const nobleBalance = await this.nobleClient.getAccountBalance('uusdc');
const parsedNobleBalance = this.parseToPrimitives(nobleBalance);
callback(JSON.stringify(parsedNobleBalance));
}
break;
default:
break;
}
-5
View File
@@ -1,10 +1,5 @@
import type { AbacusFileSystemProtocol, FileLocation, Nullable } from '@/constants/abacus';
export const ENDPOINTS_PATH =
import.meta.env.MODE === 'production'
? 'config/prod/endpoints.json'
: 'config/staging/dev_endpoints.json';
class AbacusFileSystem implements AbacusFileSystemProtocol {
readTextFile(location: FileLocation, path: string): Nullable<string> {
return null;
+20 -6
View File
@@ -26,12 +26,12 @@ import {
} from '@/constants/abacus';
import { DEFAULT_MARKETID } from '@/constants/markets';
import { CURRENT_ABACUS_DEPLOYMENT, type DydxNetwork } from '@/constants/networks';
import { CURRENT_ABACUS_DEPLOYMENT, type DydxNetwork, isMainnet } from '@/constants/networks';
import { CLEARED_SIZE_INPUTS, CLEARED_TRADE_INPUTS } from '@/constants/trade';
import type { RootStore } from '@/state/_store';
import { getInputTradeOptions } from '@/state/inputsSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
import { setTradeFormInputs } from '@/state/inputs';
import { getInputTradeOptions, getTransferInputs } from '@/state/inputsSelectors';
import AbacusRest from './rest';
import AbacusAnalytics from './analytics';
@@ -41,8 +41,9 @@ import AbacusStateNotifier from './stateNotification';
import AbacusLocalizer from './localizer';
import AbacusFormatter from './formatter';
import AbacusThreading from './threading';
import AbacusFileSystem, { ENDPOINTS_PATH } from './filesystem';
import AbacusFileSystem from './filesystem';
import { LocaleSeparators } from '../numbers';
class AbacusStateManager {
private store: RootStore | undefined;
private currentMarket: string | undefined;
@@ -80,10 +81,13 @@ class AbacusStateManager {
this.abacusFormatter
);
const appConfigs = AbacusAppConfig.Companion.forWeb;
if (!isMainnet) appConfigs.squidVersion = AbacusAppConfig.SquidVersion.V2DepositOnly;
this.stateManager = new AsyncAbacusStateManager(
'',
CURRENT_ABACUS_DEPLOYMENT,
AbacusAppConfig.Companion.forWeb,
appConfigs,
ioImplementations,
uiImplementations,
// @ts-ignore
@@ -132,6 +136,8 @@ class AbacusStateManager {
this.setTradeValue({ value: null, field: TradeInputField.limitPrice });
}
this.store?.dispatch(setTradeFormInputs(CLEARED_TRADE_INPUTS));
if (shouldResetSize) {
this.setTradeValue({ value: null, field: TradeInputField.size });
this.setTradeValue({ value: null, field: TradeInputField.usdcSize });
@@ -139,6 +145,8 @@ class AbacusStateManager {
if (needsLeverage) {
this.setTradeValue({ value: null, field: TradeInputField.leverage });
}
this.store?.dispatch(setTradeFormInputs(CLEARED_SIZE_INPUTS));
}
};
@@ -175,6 +183,12 @@ class AbacusStateManager {
}
};
setNobleWallet = (nobleWallet?: LocalWallet) => {
if (nobleWallet) {
this.chainTransactions.setNobleWallet(nobleWallet);
}
}
setTransfersSourceAddress = (evmAddress: string) => {
this.stateManager.sourceAddress = evmAddress;
};
+2 -7
View File
@@ -56,13 +56,8 @@ class AbacusRest implements AbacusRestProtocol {
.then(async (response) => {
const data = await response.text();
if (response.ok) {
callback(data, response.status);
} else {
// response not OK, call callback with null data and the status, this includes 400/500 status codes
callback(null, response.status);
}
callback(data, response.status);
try {
lastSuccessfulRestRequestByOrigin[new URL(url).origin] = Date.now();
} catch {}
+2
View File
@@ -35,6 +35,7 @@ import { setApiState } from '@/state/app';
import { setAssets } from '@/state/assets';
import { setConfigs } from '@/state/configs';
import { setInputs } from '@/state/inputs';
import { updateNotifications } from '@/state/notifications';
import { setHistoricalFundings, setLiveTrades, setMarkets, setOrderbook } from '@/state/perpetuals';
import { isTruthy } from '../isTruthy';
@@ -50,6 +51,7 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol {
}
notificationsChanged(notifications: kollections.List<AbacusNotification>): void {
this.store?.dispatch(updateNotifications(notifications.toArray()));
return;
}
+23 -4
View File
@@ -1,5 +1,6 @@
import type { AbacusWebsocketProtocol } from '@/constants/abacus';
import type { TradingViewBar } from '@/constants/candles';
import { isDev } from '@/constants/networks';
import {
PING_INTERVAL_MS,
@@ -8,11 +9,12 @@ import {
PONG_MESSAGE_TYPE,
} from '@/constants/websocket';
import { lastSuccessfulWebsocketRequestByOrigin } from '@/hooks/useAnalytics';
import { testFlags } from '@/hooks/useTestFlags';
import { subscriptionsByChannelId } from '@/lib/tradingView/dydxfeed/cache';
import { mapCandle } from '@/lib/tradingView/utils';
import { lastSuccessfulWebsocketRequestByOrigin } from '@/hooks/useAnalytics';
import { log } from '../telemetry';
const RECONNECT_INTERVAL_MS = 10_000;
@@ -143,6 +145,23 @@ class AbacusWebsocket implements Omit<AbacusWebsocketProtocol, '__doNotUseOrImpl
break;
}
case 'v4_markets': {
if (testFlags.displayInitializingMarkets) {
shouldProcess = false;
const { contents } = parsedMessage;
Object.keys(contents.markets ?? {}).forEach((market: any) => {
const status = contents.markets[market].status;
if (status === 'INITIALIZING') {
contents.markets[market].status = 'ONLINE';
}
});
this.receivedCallback?.(JSON.stringify(parsedMessage));
}
break;
}
default: {
break;
}
@@ -161,13 +180,13 @@ class AbacusWebsocket implements Omit<AbacusWebsocketProtocol, '__doNotUseOrImpl
this.socket.onclose = (e) => {
this.connectedCallback?.(false);
if (import.meta.env.MODE === 'production') return;
if (!isDev) return;
console.warn('AbacusStateManager > WS > close > ', e);
};
this.socket.onerror = (e) => {
this.connectedCallback?.(false);
if (import.meta.env.MODE === 'production') return;
if (!isDev) return;
console.error('AbacusStateManager > WS > error > ', e);
};
};
+48
View File
@@ -0,0 +1,48 @@
import { isMainnet } from '@/constants/networks';
import { GetStatus, StatusResponse } from '@0xsquid/sdk';
export const NATIVE_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
export const STATUS_ERROR_GRACE_PERIOD = 300_000;
const getSquidStatusUrl = (isV2: boolean) => {
if (isV2) {
return isMainnet
? 'https://v2.api.squidrouter.com/v2/status'
: 'https://testnet.v2.api.squidrouter.com/v2/status';
}
return isMainnet
? 'https://api.squidrouter.com/v1/status'
: 'https://testnet.api.squidrouter.com/v1/status';
};
export const fetchSquidStatus = async (
params: GetStatus,
isV2?: boolean,
integratorId?: string
): Promise<StatusResponse> => {
const parsedParams: { [key: string]: string } = {
transactionId: params.transactionId,
fromChainId: String(params.fromChainId),
toChainId: String(params.toChainId),
};
if (isV2) parsedParams.bridgeType = 'cctp';
const url = `${getSquidStatusUrl(!!isV2)}?${new URLSearchParams(parsedParams).toString()}`;
const response = await fetch(url, {
headers: {
"x-integrator-id": integratorId || 'dYdX-api'
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(error);
}
return response.json();
};
export const getNobleChainId = () => {
return isMainnet ? 'noble-1' : 'grand-1';
}
+3 -1
View File
@@ -1,5 +1,7 @@
import { isDev } from "@/constants/networks";
export const log = (location: string, error: Error, metadata?: any) => {
if (import.meta.env.MODE !== 'production') {
if (isDev) {
console.warn('telemetry/log:', { location, error, metadata });
}
+100 -34
View File
@@ -2,12 +2,16 @@ import styled, { AnyStyledComponent, css } from 'styled-components';
import { Link } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { useEnsName } from 'wagmi';
import { useNavigate } from 'react-router-dom';
import { ButtonSize } from '@/constants/buttons';
import { TransferInputField, TransferType } from '@/constants/abacus';
import { layoutMixins } from '@/styles/layoutMixins';
import { Details } from '@/components/Details';
import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable';
import { IconName } from '@/components/Icon';
import { Icon, IconName } from '@/components/Icon';
import { IconButton, type IconButtonProps } from '@/components/IconButton';
import { Panel } from '@/components/Panel';
import { Toolbar } from '@/components/Toolbar';
@@ -16,22 +20,22 @@ import { OnboardingState } from '@/constants/account';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization';
import { AppRoute, PortfolioRoute, HistoryRoute } from '@/constants/routes';
import { wallets, WalletType } from '@/constants/wallets';
import { wallets } from '@/constants/wallets';
import { useAccounts, useStringGetter, useTokenConfigs } from '@/hooks';
import { getOnboardingState } from '@/state/accountSelectors';
import { openDialog } from '@/state/dialogs';
import abacusStateManager from '@/lib/abacus';
import { isTruthy } from '@/lib/isTruthy';
import { truncateAddress } from '@/lib/wallet';
import { layoutMixins } from '@/styles/layoutMixins';
const ENS_CHAIN_ID = 1; // Ethereum
const Profile = () => {
const stringGetter = useStringGetter();
const dispatch = useDispatch();
const navigate = useNavigate();
const onboardingState = useSelector(getOnboardingState);
const isConnected = onboardingState !== OnboardingState.Disconnected;
@@ -46,16 +50,50 @@ const Profile = () => {
const actions = [
{
key: 'settings',
label: stringGetter({ key: STRING_KEYS.SETTINGS }),
icon: { iconName: IconName.Gear },
href: AppRoute.Settings,
key: 'deposit',
label: stringGetter({ key: STRING_KEYS.DEPOSIT }),
icon: { iconName: IconName.Deposit },
onClick: () => {
dispatch(
openDialog({
type: DialogTypes.ManageFunds,
dialogProps: {
selectedTransferType: TransferType.deposit.rawValue,
},
})
);
},
},
{
key: 'withdraw',
label: stringGetter({ key: STRING_KEYS.WITHDRAW }),
icon: { iconName: IconName.Withdraw },
onClick: () => {
dispatch(
openDialog({
type: DialogTypes.ManageFunds,
dialogProps: {
selectedTransferType: TransferType.withdrawal.rawValue,
},
})
);
},
},
{
key: 'transfer',
label: stringGetter({ key: STRING_KEYS.TRANSFER }),
icon: { iconName: IconName.Send },
onClick: () => {
dispatch(
openDialog({
type: DialogTypes.ManageFunds,
dialogProps: {
selectedTransferType: TransferType.transferOut.rawValue,
},
})
);
},
},
// {
// key: 'tutorials',
// label: stringGetter({ key: STRING_KEYS.TUTORIALS }),
// icon: { iconName: IconName.Comment },
// },
isConnected
? {
key: 'sign-out',
@@ -66,9 +104,9 @@ const Profile = () => {
},
}
: {
key: 'wallet',
label: stringGetter({ key: STRING_KEYS.WALLET }),
icon: { iconComponent: wallets[walletType || WalletType.OtherWallet].icon },
key: 'connect',
label: stringGetter({ key: STRING_KEYS.CONNECT }),
icon: { iconName: IconName.Transfer },
onClick: () => {
dispatch(openDialog({ type: DialogTypes.Onboarding }));
},
@@ -104,12 +142,7 @@ const Profile = () => {
{actions.map(({ key, label, href, icon, onClick }) => {
const action = (
<>
<Styled.ActionButton
{...icon}
size={ButtonSize.Large}
onClick={onClick}
isCloseIcon={icon.iconName === IconName.Close}
/>
<Styled.ActionButton {...icon} size={ButtonSize.Large} onClick={onClick} />
<span>{label}</span>
</>
);
@@ -122,6 +155,26 @@ const Profile = () => {
);
})}
</Styled.Actions>
<Styled.EqualGrid>
<Styled.PanelButton
slotHeader={
<Styled.InlineRow>
<Icon iconName={IconName.Gear} />
{stringGetter({ key: STRING_KEYS.SETTINGS })}
</Styled.InlineRow>
}
onClick={() => navigate(AppRoute.Settings)}
/>
<Styled.PanelButton
slotHeader={
<Styled.InlineRow>
<Icon iconName={IconName.HelpCircle} />
{stringGetter({ key: STRING_KEYS.HELP })}
</Styled.InlineRow>
}
onClick={() => dispatch(openDialog({ type: DialogTypes.Help }))}
/>
</Styled.EqualGrid>
<Styled.EqualGrid>
<Panel
@@ -180,7 +233,7 @@ Styled.MobileProfileLayout = styled.div`
${layoutMixins.contentContainerPage}
gap: 1rem;
padding: 1.25rem;
padding: 1.25rem 0.9rem;
`;
Styled.Header = styled.header`
@@ -245,19 +298,21 @@ Styled.Actions = styled(Toolbar)`
}
`;
Styled.ActionButton = styled(IconButton)<{ isCloseIcon?: boolean }>`
Styled.ActionButton = styled(IconButton)<{ iconName?: IconName }>`
margin-bottom: 0.5rem;
${({ isCloseIcon }) =>
isCloseIcon &&
css`
--button-textColor: var(--color-negative);
svg {
width: 0.875em;
height: 0.875em;
}
`}
${({ iconName }) =>
iconName === IconName.Close
? css`
--button-textColor: var(--color-negative);
--button-icon-size: 0.75em;
`
: iconName === IconName.Transfer &&
css`
--button-icon-size: 1.25em;
--button-textColor: var(--color-text-2);
--button-backgroundColor: var(--color-accent);
`}
`;
Styled.EqualGrid = styled.div`
@@ -306,3 +361,14 @@ Styled.TablePanel = styled(Panel)`
}
}
`;
Styled.InlineRow = styled.div`
${layoutMixins.inlineRow}
padding: 1rem;
gap: 0.5rem;
`;
Styled.PanelButton = styled(Panel)`
--panel-paddingY: 0
--panel-paddingX:0;
`;
File diff suppressed because it is too large Load Diff
@@ -131,7 +131,7 @@ export const AccountDetailsAndHistory = () => {
key: 'BuyingPower',
labelKey: STRING_KEYS.BUYING_POWER,
type: OutputType.Fiat,
value: buyingPower?.current,
value: MustBigNumber(buyingPower?.current).lt(0) ? undefined : buyingPower?.current, // show '-' when buying power is negative
},
].filter(isTruthy);
-4
View File
@@ -304,10 +304,6 @@ Styled.FeeTable = styled(Table)`
@media ${breakpoints.notTablet} {
--tableHeader-backgroundColor: var(--color-layer-1);
thead tr {
--border-width: 0;
}
}
`;
+104 -42
View File
@@ -1,11 +1,13 @@
import { useEffect } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent } from 'styled-components';
import { Navigate, Route, Routes } from 'react-router-dom';
import { OnboardingState } from '@/constants/account';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization';
import { HistoryRoute, PortfolioRoute } from '@/constants/routes';
import { useBreakpoints, useDocumentTitle, useStringGetter } from '@/hooks';
import { useAccountBalance, useBreakpoints, useDocumentTitle, useStringGetter } from '@/hooks';
import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable';
import { FundingPaymentsTable } from '@/views/tables/FundingPaymentsTable';
@@ -14,6 +16,9 @@ import { Icon, IconName } from '@/components/Icon';
import { NavigationMenu } from '@/components/NavigationMenu';
import { WithSidebar } from '@/components/WithSidebar';
import { getOnboardingState, getSubaccount } from '@/state/accountSelectors';
import { openDialog } from '@/state/dialogs';
import { PortfolioNavMobile } from './PortfolioNavMobile';
import { Overview } from './Overview';
import { Positions } from './Positions';
@@ -22,11 +27,20 @@ import { Fees } from './Fees';
import { History } from './History';
import { layoutMixins } from '@/styles/layoutMixins';
import { Button } from '@/components/Button';
import { ButtonAction } from '@/constants/buttons';
export default () => {
const dispatch = useDispatch();
const stringGetter = useStringGetter();
const { isTablet, isNotTablet } = useBreakpoints();
const onboardingState = useSelector(getOnboardingState);
const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {};
const { nativeTokenBalance } = useAccountBalance();
const usdcBalance = freeCollateral?.current || 0;
useDocumentTitle(stringGetter({ key: STRING_KEYS.PORTFOLIO }));
const routesComponent = (
@@ -84,46 +98,74 @@ export default () => {
<WithSidebar
sidebar={
isTablet ? null : (
<Styled.NavigationMenu
items={[
{
group: 'views',
groupLabel: stringGetter({ key: STRING_KEYS.VIEWS }),
items: [
{
value: PortfolioRoute.Overview,
slotBefore: <Styled.Icon iconName={IconName.Overview} />,
label: stringGetter({ key: STRING_KEYS.OVERVIEW }),
href: PortfolioRoute.Overview,
},
{
value: PortfolioRoute.Positions,
slotBefore: <Styled.Icon iconName={IconName.Cube} />,
label: stringGetter({ key: STRING_KEYS.POSITIONS }),
href: PortfolioRoute.Positions,
},
{
value: PortfolioRoute.Orders,
slotBefore: <Styled.Icon iconName={IconName.OrderPending} />,
label: stringGetter({ key: STRING_KEYS.ORDERS }),
href: PortfolioRoute.Orders,
},
{
value: PortfolioRoute.Fees,
slotBefore: <Styled.Icon iconName={IconName.Calculator} />,
label: stringGetter({ key: STRING_KEYS.FEES }),
href: PortfolioRoute.Fees,
},
{
value: PortfolioRoute.History,
slotBefore: <Styled.Icon iconName={IconName.History} />,
label: stringGetter({ key: STRING_KEYS.HISTORY }),
href: PortfolioRoute.History,
},
],
},
]}
/>
<Styled.SideBar>
<Styled.NavigationMenu
items={[
{
group: 'views',
groupLabel: stringGetter({ key: STRING_KEYS.VIEWS }),
items: [
{
value: PortfolioRoute.Overview,
slotBefore: <Styled.Icon iconName={IconName.Overview} />,
label: stringGetter({ key: STRING_KEYS.OVERVIEW }),
href: PortfolioRoute.Overview,
},
{
value: PortfolioRoute.Positions,
slotBefore: <Styled.Icon iconName={IconName.Cube} />,
label: stringGetter({ key: STRING_KEYS.POSITIONS }),
href: PortfolioRoute.Positions,
},
{
value: PortfolioRoute.Orders,
slotBefore: <Styled.Icon iconName={IconName.OrderPending} />,
label: stringGetter({ key: STRING_KEYS.ORDERS }),
href: PortfolioRoute.Orders,
},
{
value: PortfolioRoute.Fees,
slotBefore: <Styled.Icon iconName={IconName.Calculator} />,
label: stringGetter({ key: STRING_KEYS.FEES }),
href: PortfolioRoute.Fees,
},
{
value: PortfolioRoute.History,
slotBefore: <Styled.Icon iconName={IconName.History} />,
label: stringGetter({ key: STRING_KEYS.HISTORY }),
href: PortfolioRoute.History,
},
],
},
]}
/>
{onboardingState === OnboardingState.AccountConnected && (
<Styled.Footer>
<Button
action={ButtonAction.Primary}
onClick={() => dispatch(openDialog({ type: DialogTypes.Deposit }))}
>
{stringGetter({ key: STRING_KEYS.DEPOSIT })}
</Button>
{usdcBalance > 0 && (
<Button
action={ButtonAction.Base}
onClick={() => dispatch(openDialog({ type: DialogTypes.Withdraw }))}
>
{stringGetter({ key: STRING_KEYS.WITHDRAW })}
</Button>
)}
{(usdcBalance > 0 || nativeTokenBalance.gt(0)) && (
<Button
action={ButtonAction.Base}
onClick={() => dispatch(openDialog({ type: DialogTypes.Transfer }))}
>
{stringGetter({ key: STRING_KEYS.TRANSFER })}
</Button>
)}
</Styled.Footer>
)}
</Styled.SideBar>
)
}
>
@@ -139,6 +181,26 @@ Styled.PortfolioMobile = styled.div`
${layoutMixins.expandingColumnWithHeader}
`;
Styled.SideBar = styled.div`
${layoutMixins.flexColumn}
justify-content: space-between;
height: 100%;
`;
Styled.Footer = styled.div`
${layoutMixins.row}
flex-wrap: wrap;
padding: 1rem;
gap: 0.5rem;
> button {
flex-grow: 1;
}
`;
Styled.NavigationMenu = styled(NavigationMenu)`
padding: 0.5rem;
padding-top: 0;
+18 -21
View File
@@ -2,6 +2,7 @@ import type { ElementType } from 'react';
import styled, { AnyStyledComponent } from 'styled-components';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import breakpoints from '@/styles/breakpoints';
import { layoutMixins } from '@/styles/layoutMixins';
import { useAccountBalance, useAccounts, useTokenConfigs, useStringGetter } from '@/hooks';
@@ -39,29 +40,20 @@ export const DYDXBalancePanel = () => {
<AssetIcon symbol={chainTokenLabel} />
{chainTokenLabel}
</Styled.Title>
<Styled.ReceiveAndTransferButtons>
<Styled.ActionButtons>
{!canAccountTrade ? (
<OnboardingTriggerButton size={ButtonSize.Small} />
) : (
<>
<Styled.ReceiveButton
slotLeft={<Icon iconName={IconName.Qr} />}
size={ButtonSize.Small}
onClick={() => dispatch(openDialog({ type: DialogTypes.Receive }))}
>
{stringGetter({ key: STRING_KEYS.RECEIVE })}
</Styled.ReceiveButton>
<Button
slotLeft={<Icon iconName={IconName.Send} />}
size={ButtonSize.Small}
action={ButtonAction.Primary}
onClick={() => dispatch(openDialog({ type: DialogTypes.Transfer }))}
>
{stringGetter({ key: STRING_KEYS.TRANSFER })}
</Button>
</>
<Button
slotLeft={<Icon iconName={IconName.Send} />}
size={ButtonSize.Small}
action={ButtonAction.Primary}
onClick={() => dispatch(openDialog({ type: DialogTypes.Transfer }))}
>
{stringGetter({ key: STRING_KEYS.TRANSFER })}
</Button>
)}
</Styled.ReceiveAndTransferButtons>
</Styled.ActionButtons>
</Styled.Header>
}
>
@@ -125,12 +117,17 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.Panel = styled(Panel)`
--panel-paddingX: 1.5rem;
@media ${breakpoints.tablet} {
--panel-paddingY: 1.5rem;
--panel-content-paddingY: 1rem;
}
`;
Styled.Header = styled.div`
${layoutMixins.spacedRow}
gap: 1rem;
padding: 1rem 1.5rem 0;
padding: var(--panel-paddingY) var(--panel-paddingX) 0;
`;
Styled.Title = styled.h3`
@@ -143,7 +140,7 @@ Styled.Title = styled.h3`
}
`;
Styled.ReceiveAndTransferButtons = styled(Toolbar)`
Styled.ActionButtons = styled(Toolbar)`
${layoutMixins.inlineRow}
--stickyArea-topHeight: max-content;
gap: 0.5rem;
+296
View File
@@ -0,0 +1,296 @@
import { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { useQuery } from 'react-query';
import styled, { AnyStyledComponent } from 'styled-components';
import { STRING_KEYS } from '@/constants/localization';
import { ButtonAction } from '@/constants/buttons';
import { DialogTypes } from '@/constants/dialogs';
import breakpoints from '@/styles/breakpoints';
import { useAccounts, useBreakpoints, useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins';
import { Panel } from '@/components/Panel';
import { Button } from '@/components/Button';
import { Output, OutputType } from '@/components/Output';
import { Icon, IconName } from '@/components/Icon';
import { Tag, TagSize } from '@/components/Tag';
import { markLaunchIncentivesSeen } from '@/state/configs';
import { openDialog } from '@/state/dialogs';
import { log } from '@/lib/telemetry';
export const LaunchIncentivesPanel = () => {
const { isNotTablet } = useBreakpoints();
const dispatch = useDispatch();
useEffect(() => {
dispatch(markLaunchIncentivesSeen());
}, []);
return isNotTablet ? (
<Styled.Panel slotHeader={<LaunchIncentivesTitle />} slotRight={<EstimatedRewards />}>
<LaunchIncentivesContent />
</Styled.Panel>
) : (
<Styled.Panel>
<Styled.Column>
<EstimatedRewards />
<LaunchIncentivesTitle />
<LaunchIncentivesContent />
</Styled.Column>
</Styled.Panel>
);
};
const LaunchIncentivesTitle = () => {
const stringGetter = useStringGetter();
return (
<Styled.Title>
{stringGetter({
key: STRING_KEYS.LAUNCH_INCENTIVES_TITLE,
params: {
FOR_V4: <Styled.ForV4>{stringGetter({ key: STRING_KEYS.FOR_V4 })}</Styled.ForV4>,
},
})}
<Styled.NewTag size={TagSize.Medium}>{stringGetter({ key: STRING_KEYS.NEW })}</Styled.NewTag>
</Styled.Title>
);
};
const EstimatedRewards = () => {
const stringGetter = useStringGetter();
const { dydxAddress } = useAccounts();
const { data, isLoading } = useQuery({
enabled: !!dydxAddress,
queryKey: `launch_incentives_rewards_${dydxAddress ?? ''}`,
queryFn: async () => {
if (!dydxAddress) return undefined;
const resp = await fetch(`https://cloud.chaoslabs.co/query/api/dydx/points/${dydxAddress}`);
return (await resp.json())?.incentivePoints;
},
onError: (error: Error) => log('LaunchIncentives/fetchPoints', error),
});
return (
<Styled.EstimatedRewardsCard>
<Styled.EstimatedRewardsCardContent>
<div>
<span>{stringGetter({ key: STRING_KEYS.ESTIMATED_REWARDS })}</span>
<Styled.Season>
{stringGetter({
key: STRING_KEYS.LAUNCH_INCENTIVES_SEASON_NUM,
params: {
SEASON_NUMBER: 1,
},
})}
</Styled.Season>
</div>
<Styled.Points>
<Output type={OutputType.Number} value={data} isLoading={isLoading} fractionDigits={2} />
{data !== undefined && stringGetter({ key: STRING_KEYS.POINTS })}
</Styled.Points>
</Styled.EstimatedRewardsCardContent>
<Styled.Image src="/rewards-stars.svg" />
</Styled.EstimatedRewardsCard>
);
};
const LaunchIncentivesContent = () => {
const stringGetter = useStringGetter();
const dispatch = useDispatch();
return (
<Styled.Column>
<Styled.Description>
{stringGetter({ key: STRING_KEYS.LAUNCH_INCENTIVES_DESCRIPTION })}{' '}
</Styled.Description>
<Styled.ChaosLabsLogo src="/logos/chaos-labs.svg" />
<Styled.ButtonRow>
<Styled.AboutButton
action={ButtonAction.Base}
onClick={() => {
dispatch(
openDialog({
type: DialogTypes.ExternalLink,
dialogProps: { link: 'https://dydx.exchange/blog/v4-full-trading' },
})
);
}}
slotRight={<Styled.LinkOutIcon iconName={IconName.LinkOut} />}
>
{stringGetter({ key: STRING_KEYS.ABOUT })}
</Styled.AboutButton>
<Styled.Button
action={ButtonAction.Primary}
onClick={() => {
dispatch(
openDialog({
type: DialogTypes.ExternalLink,
dialogProps: { link: 'https://community.chaoslabs.xyz/dydx-v4/risk/leaderboard' },
})
);
}}
slotRight={<Styled.LinkOutIcon iconName={IconName.LinkOut} />}
slotLeft={<Icon iconName={IconName.Leaderboard} />}
>
{stringGetter({ key: STRING_KEYS.LEADERBOARD })}
</Styled.Button>
</Styled.ButtonRow>
</Styled.Column>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Panel = styled(Panel)`
--panel-paddingY: 1rem;
--panel-paddingX: 1.5rem;
background-color: var(--color-layer-4);
width: 100%;
@media ${breakpoints.tablet} {
--panel-paddingY: 1.5rem;
}
`;
Styled.ForV4 = styled.span`
color: var(--color-text-0);
`;
Styled.Title = styled.h3`
${layoutMixins.inlineRow}
font: var(--font-medium-book);
color: var(--color-text-2);
@media ${breakpoints.notTablet} {
padding: var(--panel-paddingY) var(--panel-paddingX) 0;
}
`;
Styled.Description = styled.div`
color: var(--color-text-0);
--link-color: var(--color-text-1);
a {
display: inline;
text-decoration: underline;
text-underline-offset: 0.25rem;
::before {
content: ' ';
}
}
`;
Styled.ButtonRow = styled.div`
${layoutMixins.inlineRow}
gap: 0.75rem;
margin-top: 0.5rem;
a:last-child {
--button-width: 100%;
}
`;
Styled.Button = styled(Button)`
--button-padding: 0 1rem;
`;
Styled.LinkOutIcon = styled(Icon)`
color: var(--color-text-1);
`;
Styled.AboutButton = styled(Styled.Button)`
--button-textColor: var(--color-text-2);
--button-backgroundColor: var(--color-layer-6);
--button-border: solid var(--border-width) var(--color-layer-7);
`;
Styled.Column = styled.div`
${layoutMixins.flexColumn}
gap: 0.5rem;
`;
Styled.EstimatedRewardsCard = styled.div`
${layoutMixins.spacedRow}
padding: 1rem 1.25rem;
min-width: 21.25rem;
height: calc(100% - calc(1.5rem * 2));
margin: 1.5rem;
background-color: var(--color-layer-5);
background-image: url('/dots-background.svg');
background-size: cover;
border-radius: 0.75rem;
border: solid var(--border-width) var(--color-layer-6);
color: var(--color-text-1);
@media ${breakpoints.tablet} {
margin: 0 0 0.5rem;
}
`;
Styled.EstimatedRewardsCardContent = styled.div`
${layoutMixins.flexColumn}
gap: 1rem;
height: 100%;
justify-content: space-between;
div {
${layoutMixins.flexColumn}
gap: 0.15rem;
font: var(--font-medium-book);
:first-child {
color: var(--color-text-2);
}
}
`;
Styled.BackgroundDots = styled.img`
position: absolute;
`;
Styled.Season = styled.span`
font: var(--font-small-book);
color: var(--color-text-1);
`;
Styled.Points = styled.span`
${layoutMixins.inlineRow}
gap: 0.25rem;
font: var(--font-large-book);
color: var(--color-text-0);
output {
color: var(--color-text-2);
}
`;
Styled.Image = styled.img`
position: relative;
float: right;
width: 5.25rem;
height: auto;
`;
Styled.ChaosLabsLogo = styled.img`
height: 1.25rem;
align-self: start;
`;
Styled.NewTag = styled(Tag)`
color: var(--color-accent);
background-color: var(--color-accent-faded);
`;
+19 -1
View File
@@ -7,6 +7,8 @@ import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons';
import { useAccountBalance, useBreakpoints, useStringGetter } from '@/hooks';
import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins';
import { Details } from '@/components/Details';
@@ -134,13 +136,22 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.MigratePanel = styled(Panel)`
--panel-paddingX: 1.5rem;
width: 100%;
background-image: url('/dots-background.svg');
background-position: right;
background-repeat: no-repeat;
@media ${breakpoints.tablet} {
--panel-paddingY: 1.5rem;
--panel-content-paddingY: 1rem;
}
`;
Styled.Title = styled.h3`
font: var(--font-medium-book);
color: var(--color-text-2);
padding: 1rem 1.5rem 0;
padding: var(--panel-paddingY) var(--panel-paddingX) 0;
`;
Styled.MigrateAction = styled.div`
@@ -164,6 +175,13 @@ Styled.Token = styled(Output)`
Styled.Description = styled.div`
color: var(--color-text-0);
--link-color: var(--color-text-1);
a {
display: inline;
::before {
content: ' ';
}
}
`;
Styled.Column = styled.div`
+30 -6
View File
@@ -19,6 +19,7 @@ import { openDialog } from '@/state/dialogs';
import { DYDXBalancePanel } from './DYDXBalancePanel';
import { MigratePanel } from './MigratePanel';
import { LaunchIncentivesPanel } from './LaunchIncentivesPanel';
export const RewardsPage = () => {
const dispatch = useDispatch();
@@ -39,9 +40,20 @@ export const RewardsPage = () => {
return (
<Styled.Page>
{import.meta.env.VITE_V3_TOKEN_ADDRESS && <MigratePanel />}
<Styled.PanelRow>
{isTablet && <DYDXBalancePanel />}
{isTablet ? (
<>
<LaunchIncentivesPanel />
<DYDXBalancePanel />
</>
) : (
<Styled.PanelRowIncentivesAndBalance>
<LaunchIncentivesPanel />
<DYDXBalancePanel />
</Styled.PanelRowIncentivesAndBalance>
)}
<Styled.PanelRow>
<Styled.Panel
slotHeader={<Styled.Title>{stringGetter({ key: STRING_KEYS.GOVERNANCE })}</Styled.Title>}
slotRight={panelArrow}
@@ -67,8 +79,6 @@ export const RewardsPage = () => {
</Link>
</Styled.Description>
</Styled.Panel>
{isNotTablet && <DYDXBalancePanel />}
</Styled.PanelRow>
</Styled.Page>
);
@@ -99,6 +109,11 @@ Styled.Page = styled.div`
Styled.Panel = styled(Panel)`
--panel-paddingX: 1.5rem;
@media ${breakpoints.tablet} {
--panel-paddingY: 1.5rem;
--panel-content-paddingY: 1rem;
}
`;
Styled.Title = styled.h3`
@@ -110,20 +125,29 @@ Styled.Title = styled.h3`
Styled.Description = styled.div`
color: var(--color-text-0);
--link-color: var(--color-text-1);
a {
display: inline;
::before {
content: ' ';
}
}
`;
Styled.PanelRow = styled.div`
${layoutMixins.gridEqualColumns}
gap: 1.5rem;
align-items: flex-start;
@media ${breakpoints.tablet} {
grid-auto-flow: row;
grid-template-columns: 1fr;
}
`;
Styled.PanelRowIncentivesAndBalance = styled(Styled.PanelRow)`
grid-template-columns: 2fr 1fr;
`;
Styled.IconButton = styled(IconButton)`
color: var(--color-text-0);
--color-border: var(--color-layer-6);
+7 -1
View File
@@ -7,7 +7,12 @@ import { layoutMixins } from '@/styles/layoutMixins';
import { TradeLayouts } from '@/constants/layout';
import { useBreakpoints, useCurrentMarketId, usePageTitlePriceUpdates } from '@/hooks';
import {
useBreakpoints,
useCurrentMarketId,
usePageTitlePriceUpdates,
useTradeFormInputs,
} from '@/hooks';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { getSelectedTradeLayout } from '@/state/layoutSelectors';
@@ -37,6 +42,7 @@ const TradePage = () => {
const [isHorizontalPanelOpen, setIsHorizontalPanelOpen] = useState(true);
usePageTitlePriceUpdates();
useTradeFormInputs();
return isTablet ? (
<Styled.TradeLayoutMobile>
+3
View File
@@ -1,6 +1,7 @@
import { configureStore } from '@reduxjs/toolkit';
import abacusStateManager from '@/lib/abacus';
import appMiddleware from './appMiddleware';
import localizationMiddleware from './localizationMiddleware';
@@ -13,6 +14,7 @@ import { inputsSlice } from './inputs';
import { layoutSlice } from './layout';
import { localizationSlice } from './localization';
import { perpetualsSlice } from './perpetuals';
import { notificationsSlice } from './notifications';
export const commandMenuSlices = [layoutSlice, localizationSlice];
@@ -26,6 +28,7 @@ export const store = configureStore({
inputs: inputsSlice.reducer,
layout: layoutSlice.reducer,
localization: localizationSlice.reducer,
notifications: notificationsSlice.reducer,
perpetuals: perpetualsSlice.reducer,
},
+2 -1
View File
@@ -2,6 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { AbacusApiStatus } from '@/constants/abacus';
import { DialogTypes } from '@/constants/dialogs';
import { isDev } from '@/constants/networks';
import { setApiState, setSelectedNetwork } from '@/state/app';
import { resetPerpetualsState } from '@/state/perpetuals';
@@ -30,7 +31,7 @@ export default (store: any) => (next: any) => async (action: PayloadAction<any>)
store.dispatch(
openDialog({
type: DialogTypes.ExchangeOffline,
dialogProps: { preventClose: import.meta.env.MODE === 'production' },
dialogProps: { preventClose: !isDev },
})
);
}
+10 -1
View File
@@ -17,6 +17,7 @@ export interface ConfigsState {
feeTiers?: kollections.List<FeeTier>;
feeDiscounts?: FeeDiscount[];
network?: NetworkConfigs;
hasSeenLaunchIncentives: boolean;
}
const DOCUMENT_THEME_MAP = {
@@ -43,6 +44,10 @@ const initialState: ConfigsState = {
feeDiscounts: undefined,
feeTiers: undefined,
network: undefined,
hasSeenLaunchIncentives: getLocalStorage({
key: LocalStorageKey.HasSeenLaunchIncentives,
defaultValue: false,
}),
};
changeTheme(initialState.appTheme);
@@ -60,7 +65,11 @@ export const configsSlice = createSlice({
...state,
...action.payload,
}),
markLaunchIncentivesSeen: (state: ConfigsState) => {
setLocalStorage({ key: LocalStorageKey.HasSeenLaunchIncentives, value: true });
state.hasSeenLaunchIncentives = true;
},
},
});
export const { setAppTheme, setConfigs } = configsSlice.actions;
export const { setAppTheme, setConfigs, markLaunchIncentivesSeen } = configsSlice.actions;
+3
View File
@@ -5,3 +5,6 @@ export const getAppTheme = (state: RootState) => state.configs.appTheme;
export const getFeeTiers = (state: RootState) => state.configs.feeTiers?.toArray();
export const getFeeDiscounts = (state: RootState) => state.configs.feeDiscounts;
export const getHasSeenLaunchIncentives = (state: RootState) =>
state.configs.hasSeenLaunchIncentives;
+19 -2
View File
@@ -1,4 +1,5 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import assign from 'lodash/assign';
import type {
InputError,
@@ -9,9 +10,14 @@ import type {
TransferInputs,
} from '@/constants/abacus';
import { CLEARED_SIZE_INPUTS, CLEARED_TRADE_INPUTS } from '@/constants/trade';
type TradeFormInputs = typeof CLEARED_TRADE_INPUTS & typeof CLEARED_SIZE_INPUTS;
export interface InputsState {
current?: Nullable<string>;
inputErrors?: Nullable<InputError[]>;
tradeFormInputs: TradeFormInputs;
tradeInputs?: Nullable<TradeInputs>;
closePositionInputs?: Nullable<ClosePositionInputs>;
transferInputs?: Nullable<TransferInputs>;
@@ -20,6 +26,10 @@ export interface InputsState {
const initialState: InputsState = {
current: undefined,
inputErrors: undefined,
tradeFormInputs: {
...CLEARED_TRADE_INPUTS,
...CLEARED_SIZE_INPUTS,
},
tradeInputs: undefined,
transferInputs: undefined,
};
@@ -37,10 +47,17 @@ export const inputsSlice = createSlice({
inputErrors: errors?.toArray(),
tradeInputs: trade,
closePositionInputs: closePosition,
transferInputs: transfer,
transferInputs: {
...transfer,
isCctp: !!transfer?.isCctp,
} as Nullable<TransferInputs>,
};
},
setTradeFormInputs: (state, action: PayloadAction<Partial<TradeFormInputs>>) => {
state.tradeFormInputs = assign({}, state.tradeFormInputs, action.payload);
},
},
});
export const { setInputs } = inputsSlice.actions;
export const { setInputs, setTradeFormInputs } = inputsSlice.actions;
+14
View File
@@ -48,6 +48,15 @@ export const getTradeInputErrors = (state: RootState) => {
return currentInput === 'trade' ? getInputErrors(state) : [];
};
/**
* @param state
* @returns input errors for Close position
*/
export const getClosePositionInputErrors = (state: RootState) => {
const currentInput = state.inputs.current;
return currentInput === 'closePosition' ? getInputErrors(state) : [];
};
/**
* @param state
* @returns input errors for Transfer
@@ -111,3 +120,8 @@ export const useTradeFormData = () => {
shallowEqual
);
};
/**
* @returns Tradeform Input states for display. Abacus inputs should track these values.
*/
export const getTradeFormInputs = (state: RootState) => state.inputs.tradeFormInputs;
+9 -4
View File
@@ -1,5 +1,10 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { LOCALE_DATA, SupportedLocale, TOOLTIPS } from '@dydxprotocol/v4-localization';
import {
LOCALE_DATA,
NOTIFICATIONS,
SupportedLocale,
TOOLTIPS,
} from '@dydxprotocol/v4-localization';
import {
type LocaleData,
@@ -26,6 +31,7 @@ const getNewLocaleData = ({
const newLocaleData = {
...LOCALE_DATA[locale],
...NOTIFICATIONS[locale],
TOOLTIPS: TOOLTIPS[locale],
};
@@ -51,9 +57,8 @@ export default (store: any) => (next: any) => async (action: PayloadAction<any>)
} else if (globalThis.navigator?.language) {
const browserLanguageBaseTag = globalThis.navigator.language.split('-')[0].toLowerCase();
const locale = SUPPORTED_BASE_TAGS_LOCALE_MAPPING[
browserLanguageBaseTag
] as SupportedLocales;
const locale = (SUPPORTED_BASE_TAGS_LOCALE_MAPPING[browserLanguageBaseTag] ??
SUPPORTED_BASE_TAGS_LOCALE_MAPPING[SupportedLocales.EN]) as SupportedLocales;
if (locale) {
store.dispatch(setSelectedLocale({ locale, isAutoDetect: true }));
+24
View File
@@ -0,0 +1,24 @@
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import { AbacusNotification } from '@/constants/abacus';
export interface NotificationsState {
abacusNotifications: AbacusNotification[];
}
const initialState: NotificationsState = {
abacusNotifications: [],
};
export const notificationsSlice = createSlice({
name: 'Notifications',
initialState,
reducers: {
updateNotifications: (state, action: PayloadAction<AbacusNotification[]>) => {
state.abacusNotifications = action.payload;
},
},
});
export const { updateNotifications } = notificationsSlice.actions;
+3
View File
@@ -0,0 +1,3 @@
import { RootState } from './_store';
export const getAbacusNotifications = (state: RootState) => state.notifications.abacusNotifications;
+1
View File
@@ -116,6 +116,7 @@
--color-gradient-positive: hsla(158, 49%, 48%, 0.16);
--color-gradient-negative: hsla(0, 100%, 66%, 0.16);
--color-accent-faded: hsla(var(--theme-classic-hue-purple), 100%, 70%, 0.16);
--color-risk-low: var(--theme-classic-color-green);
--color-risk-medium: var(--theme-classic-color-yellow);
+31
View File
@@ -107,12 +107,17 @@ export const formMixins: Record<
`,
inputLabel: css`
--label-textColor: var(--color-text-1);
position: relative;
height: 100%;
width: 100%;
gap: 0;
border-radius: inherit;
@media ${breakpoints.tablet} {
gap: 0.25rem;
}
`,
inputToggleGroup: css`
@@ -130,4 +135,30 @@ export const formMixins: Record<
--button-toggle-off-textColor: var(--color-text-1);
}
`,
footer: css`
${layoutMixins.stickyFooter}
${layoutMixins.withStickyFooterBackdrop}
margin-top: auto;
`,
transfersForm: css`
${() => formMixins.inputsColumn}
--form-input-gap: 1.25rem;
--form-input-height: 3.5rem;
--form-input-height-mobile: 4rem;
--form-input-paddingY: 0.5rem;
--form-input-paddingX: 1rem;
height: 100%;
label {
--label-textColor: var(--color-text-0);
}
@media ${breakpoints.tablet} {
--form-input-gap: 1rem;
}
`,
};
+2 -2
View File
@@ -12,8 +12,8 @@ export const headerMixins = {
--trigger-open-textColor: var(--color-text-2);
&:hover:not(:disabled) {
background-color: var(--trigger-hover-backgroundColor);
color: var(--trigger-hover-textColor);
--trigger-backgroundColor: var(--trigger-hover-backgroundColor);
--trigger-textColor: var(--trigger-hover-textColor);
}
`,
-4
View File
@@ -18,9 +18,5 @@ export const tradeViewMixins: Record<
tbody {
font: var(--font-small-book);
}
thead tr {
box-shadow: none;
}
`,
};
@@ -151,7 +151,10 @@ export const AccountInfoConnectedState = () => {
),
label: stringGetter({ key: STRING_KEYS.BUYING_POWER }),
type: OutputType.Fiat,
value: buyingPower,
value:
MustBigNumber(buyingPower?.current).lt(0) && buyingPower?.postOrder === null
? undefined
: buyingPower,
},
].map(
({

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