Compare commits

..
Author SHA1 Message Date
Bill He 5b71710cef remove cexdeposit testflag 2024-01-30 11:07:37 -08:00
Bill 91f5b89eb6 Coinbase Deposit (#251)
* Coinbase Deposit

* bump packages

* fix lock

* add testflag

* Address feedback

* fix chain select
2024-01-29 15:12:06 -08:00
aleka 41cc531700 downgrade follow-redirects since new version breaks safari (#254)
* downgrade follow-redirects

* explicit override
2024-01-29 11:53:38 -05:00
Brendan Chou 5aeb01f968 allow viewer to use the address test-flag to have a view-only experience of a wallet (#250)
* allow viewer to use  test-flag to specify a view-only experience of a wallet

* respond to CR

* lint
2024-01-26 17:08:51 -05:00
moo-onthelawn c9596de9c3 TRCL-3322 Implement reduce-only checkbox on frontend (#246)
* implemented

* eol lint again

* update with correct abacus version

* update localization to latest

* fix localization v
2024-01-26 17:03:32 -05:00
Bill cc8c724f09 bump v4-client (#252) 2024-01-26 15:38:47 -05:00
aleka ac07abea07 make sure view more is hidden when not num rows not specified (#249) 2024-01-25 15:10:16 -05:00
Rui 54e81cca80 Change iOS app scheme to dydx-t-v4 (#248) 2024-01-25 09:19:49 -08:00
Jared Vu 0ed1f33808 Canvas Orderbook (#227)
* 🚧 Canvas Orderbook

* 🚧 Add orderbookMap to aid in Canvas redraws

* enable mine column

* Merge main into canvas-orderbook-2

* 🧱 fix: Add orderbook constants file, remove console log, nits

* 💄 style: orderbook canvas text 13.5px -> 12px

* Updated constants and typings

* loadingSpace logic was slowing down ref mount
2024-01-24 23:16:33 -08:00
Bill 48e67f5681 Upgrade v4 clients and @cosmjs packages (#243)
* Upgrade v4-client and @cosmjs packages

* add comments
2024-01-24 15:29:43 -08:00
mike-dydx da1ac0d087 Update ios app scheme in env.json (#247) 2024-01-24 15:28:11 -08:00
moo-onthelawn 17fce5417a TRCL-3476 Create colorTokens file + new theme color types (#239)
* introduce tokens file + theme type

* fix lint error

* TRCL-3497 Use color tokens in app + tradingView widget (#240)

* update to use tokens, small fixes

* fix eol lint

* move usage style helper to lib/styles

* move files, fix text colors

* fix logo in light mode

* remove colors.css file

* small lint things

* add transparency to button + destructive borders
2024-01-24 17:03:44 -05:00
aleka b86a346be3 move governance staking panels out of trading rewards to profile on mobile (#242)
* add collapsible table

* update to add a TR inside table instead

* move governance staking panels to mobile profile
2024-01-24 15:33:01 -05:00
aleka 5cecc40a1c fix http->https in validator v4dev4 url (#245)
* fix http->https in validator v4dev4 url

* add faucet url
2024-01-24 15:32:40 -05:00
moo-onthelawn 7134fa541d add icons and update stories (#244) 2024-01-24 15:07:32 -05:00
77 changed files with 2476 additions and 807 deletions
+18 -9
View File
@@ -5,29 +5,35 @@ import styled from 'styled-components';
import { store } from '@/state/_store';
import { GlobalStyle } from '@/styles/globalStyle';
import { SelectMenu, SelectItem } from '@/components/SelectMenu';
import { AppThemeProvider } from '@/hooks/useAppTheme';
import { AppTheme, setAppTheme } from '@/state/configs';
import { setLocaleLoaded } from '@/state/localization';
import '@/index.css';
import './ladle.css';
export const StoryWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [theme, setTheme] = useState('Default theme');
const [theme, setTheme] = useState(AppTheme.Classic);
useEffect(() => {
store.dispatch(setAppTheme(theme));
switch (theme) {
case 'Dark theme': {
case AppTheme.Dark: {
document?.documentElement?.classList.remove('theme-light');
document?.documentElement?.classList.add('theme-dark');
break;
}
case 'Light theme': {
case AppTheme.Light: {
document?.documentElement?.classList.remove('theme-dark');
document?.documentElement?.classList.add('theme-light');
break;
}
default: {
case AppTheme.Classic: {
document?.documentElement?.classList.remove('theme-dark', 'theme-light');
break;
}
@@ -48,15 +54,15 @@ export const StoryWrapper: React.FC<{ children: React.ReactNode }> = ({ children
>
{[
{
value: 'Default theme',
value: AppTheme.Classic,
label: 'Default theme',
},
{
value: 'Dark theme',
value: AppTheme.Dark,
label: 'Dark theme',
},
{
value: 'Light theme',
value: AppTheme.Light,
label: 'Light theme',
},
].map(({ value, label }) => (
@@ -69,8 +75,11 @@ export const StoryWrapper: React.FC<{ children: React.ReactNode }> = ({ children
</SelectMenu>
</StoryHeader>
<hr />
<StoryContent>{children}</StoryContent>
</Provider>
<AppThemeProvider>
<GlobalStyle />
<StoryContent>{children}</StoryContent>
</AppThemeProvider>
</Provider>
);
};
+15 -10
View File
@@ -33,15 +33,15 @@
"packageManager": "pnpm@8.6.6",
"dependencies": {
"@0xsquid/sdk": "^1.10.0",
"@cosmjs/amino": "^0.31.0",
"@cosmjs/crypto": "^0.31.0",
"@cosmjs/encoding": "^0.31.0",
"@cosmjs/proto-signing": "^0.31.0",
"@cosmjs/stargate": "^0.31.0",
"@cosmjs/tendermint-rpc": "^0.31.0",
"@dydxprotocol/v4-abacus": "^1.2.5",
"@dydxprotocol/v4-client-js": "^1.0.11",
"@dydxprotocol/v4-localization": "^1.1.11",
"@cosmjs/amino": "^0.32.1",
"@cosmjs/crypto": "^0.32.1",
"@cosmjs/encoding": "^0.32.1",
"@cosmjs/proto-signing": "^0.32.1",
"@cosmjs/stargate": "^0.32.1",
"@cosmjs/tendermint-rpc": "^0.32.1",
"@dydxprotocol/v4-abacus": "^1.3.2",
"@dydxprotocol/v4-client-js": "^1.0.17",
"@dydxprotocol/v4-localization": "^1.1.17",
"@ethersproject/providers": "^5.7.2",
"@js-joda/core": "^5.5.3",
"@radix-ui/react-accordion": "^1.1.2",
@@ -155,5 +155,10 @@
"vitest": "^0.32.2",
"w3name": "^1.0.8",
"web3.storage": "^4.5.4"
},
"pnpm": {
"overrides": {
"follow-redirects": "1.15.3"
}
}
}
}
+160 -60
View File
@@ -1,36 +1,39 @@
lockfileVersion: '6.0'
overrides:
follow-redirects: 1.15.3
dependencies:
'@0xsquid/sdk':
specifier: ^1.10.0
version: 1.10.0
'@cosmjs/amino':
specifier: ^0.31.0
version: 0.31.0
specifier: ^0.32.1
version: 0.32.2
'@cosmjs/crypto':
specifier: ^0.31.0
version: 0.31.0
specifier: ^0.32.1
version: 0.32.2
'@cosmjs/encoding':
specifier: ^0.31.0
version: 0.31.0
specifier: ^0.32.1
version: 0.32.2
'@cosmjs/proto-signing':
specifier: ^0.31.0
version: 0.31.0
specifier: ^0.32.1
version: 0.32.2
'@cosmjs/stargate':
specifier: ^0.31.0
version: 0.31.0
specifier: ^0.32.1
version: 0.32.2
'@cosmjs/tendermint-rpc':
specifier: ^0.31.0
version: 0.31.0
specifier: ^0.32.1
version: 0.32.2
'@dydxprotocol/v4-abacus':
specifier: ^1.2.5
version: 1.2.5
specifier: ^1.3.2
version: 1.3.2
'@dydxprotocol/v4-client-js':
specifier: ^1.0.11
version: 1.0.11
specifier: ^1.0.17
version: 1.0.17
'@dydxprotocol/v4-localization':
specifier: ^1.1.11
version: 1.1.11
specifier: ^1.1.17
version: 1.1.17
'@ethersproject/providers':
specifier: ^5.7.2
version: 5.7.2
@@ -684,6 +687,15 @@ packages:
'@cosmjs/utils': 0.31.0
dev: false
/@cosmjs/amino@0.32.2:
resolution: {integrity: sha512-lcK5RCVm4OfdAooxKcF2+NwaDVVpghOq6o/A40c2mHXDUzUoRZ33VAHjVJ9Me6vOFxshrw/XEFn1f4KObntjYA==}
dependencies:
'@cosmjs/crypto': 0.32.2
'@cosmjs/encoding': 0.32.2
'@cosmjs/math': 0.32.2
'@cosmjs/utils': 0.32.2
dev: false
/@cosmjs/cosmwasm-stargate@0.30.1:
resolution: {integrity: sha512-W/6SLUCJAJGBN+sJLXouLZikVgmqDd9LCdlMzQaxczcCHTWeJAmRvOiZGSZaSy3shw/JN1qc6g6PKpvTVgj10A==}
dependencies:
@@ -737,7 +749,19 @@ packages:
'@cosmjs/encoding': 0.31.0
'@cosmjs/math': 0.31.0
'@cosmjs/utils': 0.31.0
'@noble/hashes': 1.3.1
'@noble/hashes': 1.3.3
bn.js: 5.2.1
elliptic: 6.5.4
libsodium-wrappers-sumo: 0.7.11
dev: false
/@cosmjs/crypto@0.32.2:
resolution: {integrity: sha512-RuxrYKzhrPF9g6NmU7VEq++Hn1vZJjqqJpZ9Tmw9lOYOV8BUsv+j/0BE86kmWi7xVJ7EwxiuxYsKuM8IR18CIA==}
dependencies:
'@cosmjs/encoding': 0.32.2
'@cosmjs/math': 0.32.2
'@cosmjs/utils': 0.32.2
'@noble/hashes': 1.3.3
bn.js: 5.2.1
elliptic: 6.5.4
libsodium-wrappers-sumo: 0.7.11
@@ -767,8 +791,8 @@ packages:
readonly-date: 1.0.0
dev: false
/@cosmjs/encoding@0.31.1:
resolution: {integrity: sha512-IuxP6ewwX6vg9sUJ8ocJD92pkerI4lyG8J5ynAM3NaX3q+n+uMoPRSQXNeL9bnlrv01FF1kIm8if/f5F7ZPtkA==}
/@cosmjs/encoding@0.32.2:
resolution: {integrity: sha512-WX7m1wLpA9V/zH0zRcz4EmgZdAv1F44g4dbXOgNj1eXZw1PIGR12p58OEkLN51Ha3S4DKRtCv5CkhK1KHEvQtg==}
dependencies:
base64-js: 1.5.1
bech32: 1.1.4
@@ -789,6 +813,13 @@ packages:
xstream: 11.14.0
dev: false
/@cosmjs/json-rpc@0.32.2:
resolution: {integrity: sha512-lan2lOgmz4yVE/HR8eCOSiII/1OudIulk8836koyIDCsPEpt6eKBuctnAD168vABGArKccLAo7Mr2gy9nrKrOQ==}
dependencies:
'@cosmjs/stream': 0.32.2
xstream: 11.14.0
dev: false
/@cosmjs/launchpad@0.27.1:
resolution: {integrity: sha512-DcFwGD/z5PK8CzO2sojDxa+Be9EIEtRZb2YawgVnw2Ht/p5FlNv+OVo8qlishpBdalXEN7FvQ1dVeDFEe9TuJw==}
dependencies:
@@ -821,8 +852,8 @@ packages:
bn.js: 5.2.1
dev: false
/@cosmjs/math@0.31.1:
resolution: {integrity: sha512-kiuHV6m6DSB8/4UV1qpFhlc4ul8SgLXTGRlYkYiIIP4l0YNeJ+OpPYaOlEgx4Unk2mW3/O2FWYj7Jc93+BWXng==}
/@cosmjs/math@0.32.2:
resolution: {integrity: sha512-b8+ruAAY8aKtVKWSft2IvtCVCUH1LigIlf9ALIiY8n9jtM4kMASiaRbQ/27etnSAInV88IaezKK9rQZrtxTjcw==}
dependencies:
bn.js: 5.2.1
dev: false
@@ -851,6 +882,17 @@ packages:
long: 4.0.0
dev: false
/@cosmjs/proto-signing@0.32.2:
resolution: {integrity: sha512-UV4WwkE3W3G3s7wwU9rizNcUEz2g0W8jQZS5J6/3fiN0mRPwtPKQ6EinPN9ASqcAJ7/VQH4/9EPOw7d6XQGnqw==}
dependencies:
'@cosmjs/amino': 0.32.2
'@cosmjs/crypto': 0.32.2
'@cosmjs/encoding': 0.32.2
'@cosmjs/math': 0.32.2
'@cosmjs/utils': 0.32.2
cosmjs-types: 0.9.0
dev: false
/@cosmjs/socket@0.30.1:
resolution: {integrity: sha512-r6MpDL+9N+qOS/D5VaxnPaMJ3flwQ36G+vPvYJsXArj93BjgyFB7BwWwXCQDzZ+23cfChPUfhbINOenr8N2Kow==}
dependencies:
@@ -875,6 +917,18 @@ packages:
- utf-8-validate
dev: false
/@cosmjs/socket@0.32.2:
resolution: {integrity: sha512-Qc8jaw4uSBJm09UwPgkqe3g9TBFx4ZR9HkXpwT6Z9I+6kbLerXPR0Gy3NSJFSUgxIfTpO8O1yqoWAyf0Ay17Mw==}
dependencies:
'@cosmjs/stream': 0.32.2
isomorphic-ws: 4.0.1(ws@7.5.9)
ws: 7.5.9
xstream: 11.14.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
dev: false
/@cosmjs/stargate@0.30.1:
resolution: {integrity: sha512-RdbYKZCGOH8gWebO7r6WvNnQMxHrNXInY/gPHPzMjbQF6UatA6fNM2G2tdgS5j5u7FTqlCI10stNXrknaNdzog==}
dependencies:
@@ -917,6 +971,25 @@ packages:
- utf-8-validate
dev: false
/@cosmjs/stargate@0.32.2:
resolution: {integrity: sha512-AsJa29fT7Jd4xt9Ai+HMqhyj7UQu7fyYKdXj/8+/9PD74xe6lZSYhQPcitUmMLJ1ckKPgXSk5Dd2LbsQT0IhZg==}
dependencies:
'@confio/ics23': 0.6.8
'@cosmjs/amino': 0.32.2
'@cosmjs/encoding': 0.32.2
'@cosmjs/math': 0.32.2
'@cosmjs/proto-signing': 0.32.2
'@cosmjs/stream': 0.32.2
'@cosmjs/tendermint-rpc': 0.32.2
'@cosmjs/utils': 0.32.2
cosmjs-types: 0.9.0
xstream: 11.14.0
transitivePeerDependencies:
- bufferutil
- debug
- utf-8-validate
dev: false
/@cosmjs/stream@0.30.1:
resolution: {integrity: sha512-Fg0pWz1zXQdoxQZpdHRMGvUH5RqS6tPv+j9Eh7Q953UjMlrwZVo0YFLC8OTf/HKVf10E4i0u6aM8D69Q6cNkgQ==}
dependencies:
@@ -929,6 +1002,12 @@ packages:
xstream: 11.14.0
dev: false
/@cosmjs/stream@0.32.2:
resolution: {integrity: sha512-gpCufLfHAD8Zp1ZKge7AHbDf4RA0TZp66wZY6JaQR5bSiEF2Drjtp4mwXZPGejtaUMnaAgff3LrUzPJfKYdQwg==}
dependencies:
xstream: 11.14.0
dev: false
/@cosmjs/tendermint-rpc@0.30.1:
resolution: {integrity: sha512-Z3nCwhXSbPZJ++v85zHObeUggrEHVfm1u18ZRwXxFE9ZMl5mXTybnwYhczuYOl7KRskgwlB+rID0WYACxj4wdQ==}
dependencies:
@@ -967,6 +1046,25 @@ packages:
- utf-8-validate
dev: false
/@cosmjs/tendermint-rpc@0.32.2:
resolution: {integrity: sha512-DXyJHDmcAfCix4H/7/dKR0UMdshP01KxJOXHdHxBCbLIpck94BsWD3B2ZTXwfA6sv98so9wOzhp7qGQa5malxg==}
dependencies:
'@cosmjs/crypto': 0.32.2
'@cosmjs/encoding': 0.32.2
'@cosmjs/json-rpc': 0.32.2
'@cosmjs/math': 0.32.2
'@cosmjs/socket': 0.32.2
'@cosmjs/stream': 0.32.2
'@cosmjs/utils': 0.32.2
axios: 1.6.5
readonly-date: 1.0.0
xstream: 11.14.0
transitivePeerDependencies:
- bufferutil
- debug
- utf-8-validate
dev: false
/@cosmjs/utils@0.27.1:
resolution: {integrity: sha512-VG7QPDiMUzVPxRdJahDV8PXxVdnuAHiIuG56hldV4yPnOz/si/DLNd7VAUUA5923b6jS1Hhev0Hr6AhEkcxBMg==}
dev: false
@@ -979,52 +1077,53 @@ packages:
resolution: {integrity: sha512-nNcycZWUYLNJlrIXgpcgVRqdl6BXjF4YlXdxobQWpW9Tikk61bEGeAFhDYtC0PwHlokCNw0KxWiHGJL4nL7Q5A==}
dev: false
/@cosmjs/utils@0.31.1:
resolution: {integrity: sha512-n4Se1wu4GnKwztQHNFfJvUeWcpvx3o8cWhSbNs9JQShEuB3nv3R5lqFBtDCgHZF/emFQAP+ZjF8bTfCs9UBGhA==}
/@cosmjs/utils@0.32.2:
resolution: {integrity: sha512-Gg5t+eR7vPJMAmhkFt6CZrzPd0EKpAslWwk5rFVYZpJsM8JG5KT9XQ99hgNM3Ov6ScNoIWbXkpX27F6A9cXR4Q==}
dev: false
/@cush/relative@1.0.0:
resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==}
dev: true
/@dydxprotocol/v4-abacus@1.2.5:
resolution: {integrity: sha512-pB3Fv1bzJRUDp29kAN61daZ34gTSGrSTNh+/AP3TUB63/Q/gaXQVBf91xVcso4Hw23xM3FwIcrRK0cvOSWgXKg==}
/@dydxprotocol/v4-abacus@1.3.2:
resolution: {integrity: sha512-zo0IHjGMlJRKOYDgNqNFQ9GtBJKJP4+Y9YY7V0X3Wt61ppKAYzodaYQhc9V/RYchcZTtS/xkicLug444YrvehQ==}
dev: false
/@dydxprotocol/v4-client-js@1.0.11:
resolution: {integrity: sha512-rJNFGTO+HU1GQppXeRLKXNk4HWx/h+DjU2sUCieEv5mnRIMQpLw8dH/L+Uyhxx2oovccY1MaKB4Fr9IngdrF0A==}
/@dydxprotocol/v4-client-js@1.0.17:
resolution: {integrity: sha512-PbTKbzcS7VapuFRofkirUxkF9ThNS2tWYyr0asFOMUfebsQWMvTC1sYC/s1NZIiBeq3cFz9vKdZFbfsKi7Z1bw==}
dependencies:
'@cosmjs/amino': 0.30.1
'@cosmjs/encoding': 0.31.1
'@cosmjs/math': 0.31.1
'@cosmjs/proto-signing': 0.30.1
'@cosmjs/stargate': 0.30.1
'@cosmjs/tendermint-rpc': 0.30.1
'@cosmjs/utils': 0.31.1
'@dydxprotocol/v4-proto': 0.4.1
'@cosmjs/amino': 0.32.2
'@cosmjs/encoding': 0.32.2
'@cosmjs/math': 0.32.2
'@cosmjs/proto-signing': 0.32.2
'@cosmjs/stargate': 0.32.2
'@cosmjs/tendermint-rpc': 0.32.2
'@cosmjs/utils': 0.32.2
'@dydxprotocol/v4-proto': 3.0.0-dev.0
'@osmonauts/lcd': 0.6.0
'@scure/bip32': 1.3.2
'@scure/bip39': 1.2.1
axios: 1.1.3
bech32: 1.1.4
bignumber.js: 9.1.1
cosmjs-types: 0.9.0
ethereum-cryptography: 2.1.2
ethers: 6.6.1
long: 4.0.0
protobufjs: 6.11.4
ws: 8.15.0
ws: 8.15.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)
transitivePeerDependencies:
- bufferutil
- debug
- utf-8-validate
dev: false
/@dydxprotocol/v4-localization@1.1.11:
resolution: {integrity: sha512-ZHnyrWD1bEpvY1tctkcmSV6A5+hQSYBFwyjbiyOqepqDqrv24J5a3hlZU94lDyYf+7Sdk3alOvy9Z7Lpk59nvw==}
/@dydxprotocol/v4-localization@1.1.17:
resolution: {integrity: sha512-kal1LrcihLMEv5YxaA/hd6Zl10Mp3x6jicoXDcXvtyNdrczAl3YapyI2nmeifRAPvfueOaY3W/sKkm2BiSKSsA==}
dev: false
/@dydxprotocol/v4-proto@0.4.1:
resolution: {integrity: sha512-2pBkokufWcxfIpiXE6QuQOBXUe154OTp6ojCIdOPW9TFLxzchoygxXnb5dL6rAQVNj9Qbg+hhXBllutt4m99dw==}
/@dydxprotocol/v4-proto@3.0.0-dev.0:
resolution: {integrity: sha512-hT6F/AgaTqv8Bwo7twpIhmjXvJE/Fx+3mmHHTuIXMeL6OhVtlOpcEQyHvBEAdh1VrNq/S7qWvdQD0fvC/UgKyA==}
dependencies:
protobufjs: 6.11.4
dev: false
@@ -7376,7 +7475,7 @@ packages:
/axios@0.21.4:
resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==}
dependencies:
follow-redirects: 1.15.2
follow-redirects: 1.15.3
transitivePeerDependencies:
- debug
dev: false
@@ -7384,7 +7483,7 @@ packages:
/axios@0.27.2:
resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==}
dependencies:
follow-redirects: 1.15.2
follow-redirects: 1.15.3
form-data: 4.0.0
transitivePeerDependencies:
- debug
@@ -7393,7 +7492,17 @@ packages:
/axios@1.1.3:
resolution: {integrity: sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==}
dependencies:
follow-redirects: 1.15.2
follow-redirects: 1.15.3
form-data: 4.0.0
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
dev: false
/axios@1.6.5:
resolution: {integrity: sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==}
dependencies:
follow-redirects: 1.15.3
form-data: 4.0.0
proxy-from-env: 1.1.0
transitivePeerDependencies:
@@ -8057,6 +8166,10 @@ packages:
protobufjs: 6.11.4
dev: false
/cosmjs-types@0.9.0:
resolution: {integrity: sha512-MN/yUe6mkJwHnCFfsNPeCfXVhyxHYW6c/xDUzrSbBycYzw++XvWDMJArXp2pLdgD6FQ8DW79vkPjeNKVrXaHeQ==}
dev: false
/cosmos-directory-client@0.0.6:
resolution: {integrity: sha512-WIdaQ8uW1vIbYvNnAVunkC6yxTrneJC7VQ5UUQ0kuw8b0C0A39KTIpoQHCfc8tV7o9vF4niwRhdXEdfAgQEsQQ==}
dependencies:
@@ -9412,8 +9525,8 @@ packages:
resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
dev: true
/follow-redirects@1.15.2:
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
/follow-redirects@1.15.3:
resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
@@ -14677,19 +14790,6 @@ packages:
optional: true
dev: false
/ws@8.15.0:
resolution: {integrity: sha512-H/Z3H55mrcrgjFwI+5jKavgXvwQLtfPCUEp6pi35VhoB0pfcHnSoyuTzkBEZpzq49g1193CUEwIvmsjcotenYw==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
dev: false
/ws@8.15.1(bufferutil@4.0.8)(utf-8-validate@5.0.10):
resolution: {integrity: sha512-W5OZiCjXEmk0yZ66ZN82beM5Sz7l7coYxpRkzS+p9PP+ToQry8szKh+61eNktr7EA9DOwvFGhfC605jDHbP6QQ==}
engines: {node: '>=10.0.0'}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"apps": {
"ios": {
"scheme": "dydxv4"
"scheme": "dydx-t-v4"
}
},
"deployments": {
@@ -1157,4 +1157,4 @@
}
}
}
}
}
+8
View File
@@ -0,0 +1,8 @@
[
{
"name": "coinbase",
"label": "Coinbase",
"icon": "/exchanges/coinbase.png",
"depositType": "noble"
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

+40 -34
View File
@@ -16,6 +16,7 @@ import {
} from '@/hooks';
import { DydxProvider } from '@/hooks/useDydxClient';
import { AccountsProvider } from '@/hooks/useAccounts';
import { AppThemeProvider } from '@/hooks/useAppTheme';
import { DialogAreaProvider, useDialogArea } from '@/hooks/useDialogArea';
import { LocaleProvider } from '@/hooks/useLocaleSeparators';
import { NotificationsProvider } from '@/hooks/useNotifications';
@@ -35,6 +36,7 @@ import { GlobalCommandDialog } from '@/views/dialogs/GlobalCommandDialog';
import { config } from '@/lib/wagmi';
import { breakpoints } from '@/styles';
import { GlobalStyle } from '@/styles/globalStyle';
import { layoutMixins } from '@/styles/layoutMixins';
import { LoadingSpace } from './components/Loading/LoadingSpinner';
@@ -66,49 +68,52 @@ const Content = () => {
const { chainTokenLabel } = useTokenConfigs();
return (
<Styled.Content isShowingHeader={isShowingHeader} isShowingFooter={isShowingFooter}>
{isNotTablet && <HeaderDesktop />}
<>
<GlobalStyle />
<Styled.Content isShowingHeader={isShowingHeader} isShowingFooter={isShowingFooter}>
{isNotTablet && <HeaderDesktop />}
<Styled.Main>
<Suspense fallback={<LoadingSpace id="main" />}>
<Routes>
<Route path={AppRoute.Trade}>
<Route path=":market" element={<TradePage />} />
<Route path={AppRoute.Trade} element={<TradePage />} />
</Route>
<Styled.Main>
<Suspense fallback={<LoadingSpace id="main" />}>
<Routes>
<Route path={AppRoute.Trade}>
<Route path=":market" element={<TradePage />} />
<Route path={AppRoute.Trade} element={<TradePage />} />
</Route>
<Route path={AppRoute.Markets} element={<MarketsPage />} />
<Route path={`/${chainTokenLabel}`} element={<RewardsPage />} />
{isTablet && (
<>
<Route path={AppRoute.Alerts} element={<AlertsPage />} />
<Route path={AppRoute.Profile} element={<ProfilePage />} />
<Route path={`${AppRoute.Settings}/*`} element={<SettingsPage />} />
</>
)}
<Route path={AppRoute.Markets} element={<MarketsPage />} />
<Route path={`/${chainTokenLabel}`} element={<RewardsPage />} />
{isTablet && (
<>
<Route path={AppRoute.Alerts} element={<AlertsPage />} />
<Route path={AppRoute.Profile} element={<ProfilePage />} />
<Route path={`${AppRoute.Settings}/*`} element={<SettingsPage />} />
</>
)}
<Route element={<GuardedMobileRoute />}>
<Route path={`${AppRoute.Portfolio}/*`} element={<PortfolioPage />} />
</Route>
<Route element={<GuardedMobileRoute />}>
<Route path={`${AppRoute.Portfolio}/*`} element={<PortfolioPage />} />
</Route>
<Route path={AppRoute.Terms} element={<TermsOfUsePage />} />
<Route path={AppRoute.Privacy} element={<PrivacyPolicyPage />} />
<Route path={AppRoute.Terms} element={<TermsOfUsePage />} />
<Route path={AppRoute.Privacy} element={<PrivacyPolicyPage />} />
<Route path="*" element={<Navigate to={DEFAULT_TRADE_ROUTE} replace />} />
</Routes>
</Suspense>
</Styled.Main>
<Route path="*" element={<Navigate to={DEFAULT_TRADE_ROUTE} replace />} />
</Routes>
</Suspense>
</Styled.Main>
{isTablet ? <FooterMobile /> : <FooterDesktop />}
{isTablet ? <FooterMobile /> : <FooterDesktop />}
<Styled.NotificationsToastArea />
<Styled.NotificationsToastArea />
<Styled.DialogArea ref={setDialogArea}>
<DialogManager />
</Styled.DialogArea>
<Styled.DialogArea ref={setDialogArea}>
<DialogManager />
</Styled.DialogArea>
<GlobalCommandDialog />
</Styled.Content>
<GlobalCommandDialog />
</Styled.Content>
</>
);
};
@@ -131,6 +136,7 @@ const providers = [
wrapProvider(LocalNotificationsProvider),
wrapProvider(NotificationsProvider),
wrapProvider(DialogAreaProvider),
wrapProvider(AppThemeProvider),
];
const App = () => {
+13
View File
@@ -0,0 +1,13 @@
import { forwardRef } from 'react';
type CanvasProps = {
className?: string;
width: number | string;
height: number | string;
};
export const Canvas = forwardRef<HTMLCanvasElement, CanvasProps>(
({ className, width, height }, canvasRef) => (
<canvas ref={canvasRef} className={className} width={width} height={height} />
)
);
+10 -2
View File
@@ -5,7 +5,7 @@ import { Checkbox, CheckboxProps } from '@/components/Checkbox';
import { StoryWrapper } from '.ladle/components';
export const Checkboxes: Story<CheckboxProps> = (args) => {
export const CheckboxStory: Story<CheckboxProps> = (args) => {
const [isChecked, setIsChecked] = useState(false);
return (
@@ -21,4 +21,12 @@ export const Checkboxes: Story<CheckboxProps> = (args) => {
);
};
Checkboxes.args = {};
CheckboxStory.args = {};
CheckboxStory.argTypes = {
disabled: {
options: [true, false],
control: { type: 'select' },
defaultValue: false,
}
}
+20 -4
View File
@@ -1,4 +1,4 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import styled, { css, type AnyStyledComponent } from 'styled-components';
import { Root, Indicator } from '@radix-ui/react-checkbox';
import { CheckIcon } from '@radix-ui/react-icons';
@@ -9,6 +9,7 @@ type ElementProps = {
onCheckedChange: (checked: boolean) => void;
id?: string;
label?: React.ReactNode;
disabled?: boolean;
};
type StyleProps = {
@@ -23,14 +24,15 @@ export const Checkbox: React.FC<CheckboxProps> = ({
onCheckedChange,
id,
label,
disabled
}) => (
<Styled.Container>
<Styled.Root className={className} checked={checked} onCheckedChange={onCheckedChange} id={id}>
<Styled.Root className={className} checked={checked} disabled={disabled} onCheckedChange={onCheckedChange} id={id}>
<Styled.Indicator>
<CheckIcon />
</Styled.Indicator>
</Styled.Root>
{label && <label htmlFor={id}>{label}</label>}
{label && <Styled.label disabled={disabled} htmlFor={id}>{label}</Styled.label>}
</Styled.Container>
);
@@ -48,17 +50,23 @@ Styled.Container = styled.div`
Styled.Root = styled(Root)`
--checkbox-backgroundColor: var(--color-layer-0);
--checkbox-borderColor: var(--color-border);
min-width: 1.25rem;
height: 1.25rem;
border-radius: 0.375rem;
border: var(--border-width) solid var(--color-border);
border: var(--border-width) solid var(--checkbox-borderColor);
background-color: var(--checkbox-backgroundColor);
&[data-state='checked'] {
--checkbox-backgroundColor: var(--color-accent);
}
&[data-disabled] {
cursor: not-allowed;
--checkbox-backgroundColor: var(--color-layer-1);
}
`;
Styled.Indicator = styled(Indicator)`
@@ -68,3 +76,11 @@ Styled.Indicator = styled(Indicator)`
color: var(--color-text-2);
`;
Styled.label = styled.div<{ disabled: boolean; }>`
color: var(--color-text-2);
${({disabled}) => disabled && css`
color: var(--color-text-0);
`}
`;
+1 -1
View File
@@ -14,7 +14,7 @@ export const DiffArrowStory: Story<DiffArrowProps> = (args) => (
DiffArrowStory.argTypes = {
direction: {
options: ['left', 'right'],
options: ['left', 'right', 'up', 'down'],
control: { type: 'select' },
defaultValue: 'right',
},
+7 -1
View File
@@ -10,7 +10,7 @@ type ElementProps = {
type StyleProps = {
className?: string;
direction?: 'right' | 'left';
direction?: 'right' | 'left' | 'up' | 'down';
};
export type DiffArrowProps = ElementProps & StyleProps;
@@ -57,5 +57,11 @@ Styled.DiffArrowContainer = styled.span<DiffArrowProps>`
left: css`
transform: scaleX(-1);
`,
up: css`
transform: rotate(-90deg);
`,
down: css`
transform: rotate(90deg);
`
}[direction || 'right'])}
`;
+6
View File
@@ -44,6 +44,7 @@ import {
MenuIcon,
MigrateIcon,
MintscanIcon,
MoonIcon,
OrderbookIcon,
OrderCanceledIcon,
OrderFilledIcon,
@@ -66,6 +67,7 @@ import {
ShowIcon,
TogglesMenuIcon,
StarIcon,
SunIcon,
TerminalIcon,
TradeIcon,
TransferIcon,
@@ -119,6 +121,7 @@ export enum IconName {
Menu = 'Menu',
Migrate = 'Migrate',
Mintscan = 'Mintscan',
Moon = 'Moon',
Onboarding = 'Onboarding',
Orderbook = 'OrderbookIcon',
OrderCanceled = 'OrderCanceled',
@@ -141,6 +144,7 @@ export enum IconName {
Share = 'Share',
Show = 'Show',
Star = 'Star',
Sun = 'Sun',
Terminal = 'Terminal',
TogglesMenu = 'TogglesMenu',
Trade = 'Trade',
@@ -195,6 +199,7 @@ const icons = {
[IconName.Menu]: MenuIcon,
[IconName.Migrate]: MigrateIcon,
[IconName.Mintscan]: MintscanIcon,
[IconName.Moon]: MoonIcon,
[IconName.Orderbook]: OrderbookIcon,
[IconName.OrderCanceled]: OrderCanceledIcon,
[IconName.OrderFilled]: OrderFilledIcon,
@@ -216,6 +221,7 @@ const icons = {
[IconName.Share]: ShareIcon,
[IconName.Show]: ShowIcon,
[IconName.Star]: StarIcon,
[IconName.Sun]: SunIcon,
[IconName.Terminal]: TerminalIcon,
[IconName.TogglesMenu]: TogglesMenuIcon,
[IconName.Trade]: TradeIcon,
+1 -1
View File
@@ -1,11 +1,11 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { RiskLevels } from '@/constants/abacus';
import { UsageColorFromRiskLevel } from '@/styles/colors';
import { Ring } from '@/components/Ring';
import { abacusHelper } from '@/lib/abacus';
import { UsageColorFromRiskLevel } from '@/lib/styles';
type ElementProps = {
value: number;
+1 -1
View File
@@ -7,7 +7,7 @@ import { layoutMixins } from '@/styles/layoutMixins';
import { breakpoints } from '@/styles';
type PanelProps = {
slotHeaderContent?: string;
slotHeaderContent?: React.ReactNode;
slotHeader?: React.ReactNode;
slotRight?: React.ReactNode;
children?: React.ReactNode;
+3 -2
View File
@@ -11,6 +11,7 @@ type ElementProps = {
};
type StyleProps = {
className?: string;
hasLogo?: boolean;
size?: number;
};
@@ -18,7 +19,7 @@ type StyleProps = {
const DARK_LOGO_MARK_URL = '/logos/logo-mark-dark.svg';
const LIGHT_LOGO_MARK_URL = '/logos/logo-mark-light.svg';
export const QrCode = ({ value, hasLogo, size = 300 }: ElementProps & StyleProps) => {
export const QrCode = ({ className, value, hasLogo, size = 300 }: ElementProps & StyleProps) => {
const ref = useRef<HTMLDivElement>(null);
const appTheme: AppTheme = useSelector(getAppTheme);
@@ -74,7 +75,7 @@ export const QrCode = ({ value, hasLogo, size = 300 }: ElementProps & StyleProps
}
}, [appTheme, hasLogo]);
return <Styled.QrCode ref={ref} />;
return <Styled.QrCode className={className} ref={ref} />;
};
const Styled: Record<string, AnyStyledComponent> = {};
+1 -1
View File
@@ -47,7 +47,7 @@ Styled.Root = styled(Root)`
--switch-thumb-backgroundColor: var(--color-layer-6);
--switch-active-backgroundColor: var(--color-accent);
--switch-active-thumb-backgroundColor: var(--color-white);
--switch-active-thumb-backgroundColor: ${({ theme }) => theme.switchThumbActiveBackground};
position: relative;
width: var(--switch-width);
+4 -3
View File
@@ -125,7 +125,7 @@ export const Table = <TableRowData extends object, TableRowKey extends Key>({
selectionMode = 'single',
selectionBehavior = 'toggle',
slotEmpty,
initialNumRowsToShow = data.length,
initialNumRowsToShow,
// shouldRowRender,
// collection,
@@ -142,6 +142,7 @@ export const Table = <TableRowData extends object, TableRowKey extends Key>({
}: ElementProps<TableRowData, TableRowKey> & StyleProps) => {
const [selectedKeys, setSelectedKeys] = useState(new Set<TableRowKey>());
const [numRowsToShow, setNumRowsToShow] = useState(initialNumRowsToShow);
const enableViewMore = numRowsToShow !== undefined;
const currentBreakpoints = useBreakpoints();
const shownColumns = columns.filter(
@@ -217,7 +218,7 @@ export const Table = <TableRowData extends object, TableRowKey extends Key>({
}
numColumns={shownColumns.length}
onViewMoreClick={
numRowsToShow !== undefined && numRowsToShow < data.length
enableViewMore && numRowsToShow < data.length
? () => setNumRowsToShow(data.length)
: undefined
}
@@ -245,7 +246,7 @@ export const Table = <TableRowData extends object, TableRowKey extends Key>({
)}
</TableHeader>
<TableBody items={list.items.slice(0, numRowsToShow)}>
<TableBody items={enableViewMore ? list.items.slice(0, numRowsToShow) : list.items}>
{(item) => (
<Row key={getRowKey(item)}>
{(columnKey) => (
+4
View File
@@ -8,6 +8,7 @@ import { Button, type ButtonStateConfig, type ButtonProps } from '@/components/B
type ElementProps = {
timeoutInSeconds: number;
slotFinal?: ReactNode;
} & ButtonProps;
export type TimeoutButtonProps = ElementProps;
@@ -15,6 +16,7 @@ export type TimeoutButtonProps = ElementProps;
export const TimeoutButton = ({
children,
timeoutInSeconds,
slotFinal,
...otherProps
}: TimeoutButtonProps) => {
const [timeoutDeadline] = useState(Date.now() + timeoutInSeconds * 1000);
@@ -23,6 +25,8 @@ export const TimeoutButton = ({
const secondsLeft = Math.max(0, (timeoutDeadline - now) / 1000);
if (slotFinal && secondsLeft <= 0) return slotFinal;
return (
<Button
{...otherProps}
+2 -2
View File
@@ -2,9 +2,9 @@ import styled, { type AnyStyledComponent } from 'styled-components';
import { type RiskLevels } from '@/constants/abacus';
import { UsageColorFromRiskLevel } from '@/styles/colors';
import { abacusHelper } from '@/lib/abacus';
import { UsageColorFromRiskLevel } from '@/lib/styles';
type ElementProps = {
value: number;
};
+1 -6
View File
@@ -110,12 +110,7 @@ Styled.Abbr = styled.abbr`
Styled.Content = styled(Content)`
--tooltip-backgroundColor: var(--color-layer-4);
--tooltip-backgroundColor: hsl(
var(--layer-base-hue),
var(--layer-base-saturation),
calc(var(--layer-base-lightness) + 4%),
0.66
);
--tooltip-backgroundColor: ${({ theme }) => theme.tooltipBackground};
${popoverMixins.popover}
--popover-backgroundColor: var(--tooltip-backgroundColor);
+1 -6
View File
@@ -27,12 +27,7 @@ Styled.TooltipContent = styled.aside<{ accentColor?: string }>`
${popoverMixins.popover}
--popover-radius: 0.5rem;
--popover-background-color: hsl(
var(--layer-base-hue),
var(--layer-base-saturation),
calc(var(--layer-base-lightness)),
0.9
);
--popover-background-color: ${({ theme }) => theme.popoverBackground};
--popover-backdrop-filter: saturate(120%) blur(12px);
display: grid;
+16 -1
View File
@@ -282,13 +282,22 @@ export const TRADE_TYPES: Record<
[AbacusOrderType.takeProfitMarket.rawValue]: TradeTypes.TAKE_PROFIT_MARKET,
[AbacusOrderType.liquidated.name]: null,
[AbacusOrderType.liquidation.name]: null,
[AbacusOrderType.liquidated.rawValue]: null,
[AbacusOrderType.liquidation.name]: null,
[AbacusOrderType.liquidation.rawValue]: null,
[AbacusOrderType.trailingStop.name]: null,
[AbacusOrderType.trailingStop.rawValue]: null,
[AbacusOrderType.offsetting.name]: null,
[AbacusOrderType.offsetting.rawValue]: null,
[AbacusOrderType.deleveraged.name]: null,
[AbacusOrderType.deleveraged.rawValue]: null,
[AbacusOrderType.finalSettlement.name]: null,
[AbacusOrderType.finalSettlement.rawValue]: null,
};
// Custom types involving Abacus
@@ -307,3 +316,9 @@ export type NetworkConfig = Partial<{
}>;
export type ConnectNetworkEvent = CustomEvent<Partial<NetworkConfig>>;
export type PerpetualMarketOrderbookLevel = OrderbookLine & {
side?: 'ask' | 'bid';
mine: number;
key: string;
};
+15
View File
@@ -0,0 +1,15 @@
/**
* @description Orderbook display constants
*/
export const ORDERBOOK_MAX_ROWS_PER_SIDE = 30;
export const ORDERBOOK_ANIMATION_DURATION = 400;
/**
* @description Orderbook pixel constants
* @note ORDERBOOK_ROW_HEIGHT should be a divisor of ORDERBOOK_HEIGHT so that we do not have a partial row at the bottom
* @note To change the Orderbook width, --orderbook-trades-width and ORDERBOOK_WIDTH must be changed to the same value
*/
export const ORDERBOOK_HEIGHT = 800;
export const ORDERBOOK_WIDTH = 300;
export const ORDERBOOK_ROW_HEIGHT = 24;
export const ORDERBOOK_ROW_PADDING_RIGHT = 8;
+70
View File
@@ -0,0 +1,70 @@
export enum ColorToken {
Black = '#000000',
White = '#FFFFFF',
LightGray0 = '#FAFAFA',
LightGray1 = '#F7F8FA',
LightGray2 = '#FAFAFD',
LightGray3 = '#F5F5F5',
LightGray4 = '#EDF0F2',
LightGray5 = '#EDEDED',
LightGray6 = '#E9ECEE',
LightGray7 = '#E6E6E6',
LightGray8 = '#DBDBDB',
LightGray9 = '#D5DADF',
LightGray10 = '#D1D9E0',
MediumGray0 = '#CACACE',
MediumGray1 = '#C2CBD3',
DarkGray0 = '#888891',
DarkGray1 = '#737373',
DarkGray2 = '#56565C',
DarkGray3 = '#4B4B4B',
DarkGray4 = '#494950',
DarkGray5 = '#3B3B3F',
DarkGray6 = '#303033',
DarkGray7 = '#313135',
DarkGray8 = '#292929',
DarkGray9 = '#23232F',
DarkGray10 = '#212124',
DarkGray11 = '#18181B',
DarkGray12 = '#181818',
DarkGray13 = '#0C0C0D',
GrayBlue0 = '#464659',
GrayBlue1 = '#38384D',
GrayBlue2 = '#303045',
GrayBlue3 = '#29293D',
GrayBlue4 = '#212131',
GrayBlue5 = '#181825',
GrayBlue6 = '#101018',
GrayBlue7 = '#08080C',
GrayPurple0 = '#DFDADF',
GrayPurple1 = '#C8C7D8',
GrayPurple2 = '#807E98',
Purple0 = '#7774FF',
Purple1 = '#6966FF',
Green0 = '#1AFFB9',
Green1 = '#3ED9A4',
Green2 = '#2CCC98',
Green3 = '#3EB68A',
Yellow0 = '#FFCC48',
Yellow1 = '#FFB647',
Red0 = '#FF5C5C',
Red1 = '#E76565',
Red2 = '#E45555',
}
/** Maps opacity to corresponding hex value */
export enum OpacityToken {
Opacity16 = '29',
Opacity20 = '33',
Opacity66 = 'A8',
Opacity90 = 'E6',
}
+77
View File
@@ -0,0 +1,77 @@
export type ThemeColors = LayerColors &
BorderColors &
TextColors &
GradientColors &
AccentColors &
StatusColors &
DirectionalColors &
RiskColors &
IconColors &
ComponentColors;
type LayerColors = {
layer0: string;
layer1: string;
layer2: string;
layer3: string;
layer4: string;
layer5: string;
layer6: string;
layer7: string;
};
type BorderColors = {
borderDefault: string;
borderDestructive: string;
borderButton: string;
};
type TextColors = {
textPrimary: string;
textSecondary: string;
textTertiary: string;
};
type GradientColors = {
gradientBase0: string;
gradientBase1: string;
};
type AccentColors = {
accent: string;
accentFaded: string;
favorite: string;
};
type StatusColors = {
success: string;
warning: string;
error: string;
};
type DirectionalColors = {
positive: string;
negative: string;
positiveFaded: string;
negativeFaded: string;
};
type RiskColors = {
riskLow: string;
riskMedium: string;
riskHigh: string;
};
type IconColors = {
logoFill: string;
profileYellow: string;
profileRed: string;
};
type ComponentColors = {
inputBackground: string;
popoverBackground: string;
switchThumbActiveBackground: string;
toggleBackground: string;
tooltipBackground: string;
};
+12
View File
@@ -138,6 +138,10 @@ export const tradeTooltips: TooltipStrings = {
title: stringGetter({ key: TOOLTIP_STRING_KEYS.POST_ONLY_TITLE }),
body: stringGetter({ key: TOOLTIP_STRING_KEYS.POST_ONLY_BODY }),
}),
'post-only-timeinforce-gtt': ({ stringGetter }) => ({
title: stringGetter({ key: TOOLTIP_STRING_KEYS.POST_ONLY_TIMEINFORCE_GTT_TITLE }),
body: stringGetter({ key: TOOLTIP_STRING_KEYS.POST_ONLY_TIMEINFORCE_GTT_BODY }),
}),
'price-impact': ({ stringGetter }) => ({
title: stringGetter({ key: TOOLTIP_STRING_KEYS.PRICE_IMPACT_TITLE }),
body: stringGetter({ key: TOOLTIP_STRING_KEYS.PRICE_IMPACT_BODY }),
@@ -151,6 +155,14 @@ export const tradeTooltips: TooltipStrings = {
body: stringGetter({ key: TOOLTIP_STRING_KEYS.REDUCE_ONLY_BODY }),
learnMoreLink: urlConfigs?.reduceOnlyLearnMore,
}),
'reduce-only-execution-ioc-fok': ({ stringGetter }) => ({
title: stringGetter({ key: TOOLTIP_STRING_KEYS.REDUCE_ONLY_EXECUTION_IOC_FOK_TITLE }),
body: stringGetter({ key: TOOLTIP_STRING_KEYS.REDUCE_ONLY_EXECUTION_IOC_FOK_BODY }),
}),
'reduce-only-timeinforce-ioc-fok': ({ stringGetter }) => ({
title: stringGetter({ key: TOOLTIP_STRING_KEYS.REDUCE_ONLY_TIMEINFORCE_IOC_FOK_TITLE }),
body: stringGetter({ key: TOOLTIP_STRING_KEYS.REDUCE_ONLY_TIMEINFORCE_IOC_FOK_BODY }),
}),
spread: () => ({
title: 'Spread',
body: 'The difference in price between the highest bid (the price a buyer is willing to buy for) and lowest ask (the price a seller is willing to sell for) an asset.',
+13 -1
View File
@@ -33,6 +33,7 @@ export enum WalletConnectionType {
CosmosSigner = 'CosmosSigner',
InjectedEip1193 = 'injectedEip1193',
WalletConnect2 = 'walletConnect2',
TestWallet = 'TestWallet',
}
export enum WalletErrorType {
@@ -68,6 +69,9 @@ export const walletConnectionTypes: Record<WalletConnectionType, WalletConnectio
[WalletConnectionType.CosmosSigner]: {
name: 'CosmosSigner',
},
[WalletConnectionType.TestWallet]: {
name: 'TestWallet',
},
};
// Wallets
@@ -88,7 +92,7 @@ export enum WalletType {
TokenPocket = 'TOKEN_POCKET',
TrustWallet = 'TRUST_WALLET',
WalletConnect2 = 'WALLETCONNECT_2',
// TestWallet = 'TEST_WALLET',
TestWallet = 'TEST_WALLET',
OtherWallet = 'OTHER_WALLET',
}
@@ -243,6 +247,12 @@ export const wallets: Record<WalletType, WalletConfig> = {
icon: KeplrIcon,
connectionTypes: [WalletConnectionType.CosmosSigner],
},
[WalletType.TestWallet]: {
type: WalletType.TestWallet,
stringKey: STRING_KEYS.TEST_WALLET,
icon: GenericWalletIcon,
connectionTypes: [WalletConnectionType.TestWallet],
},
};
// Injected EIP-1193 Providers
@@ -354,3 +364,5 @@ export enum DydxChainAsset {
USDC = 'usdc',
CHAINTOKEN = 'chain',
}
export const TEST_WALLET_EVM_ADDRESS: EvmAddress = '0x0000000000000000000000000000000000000000';
+4
View File
@@ -0,0 +1,4 @@
export * from './useCenterOrderbook';
export * from './useSpreadRowScrollListener';
export * from './useDrawOrderbook';
export * from './useOrderbookValues';
+22
View File
@@ -0,0 +1,22 @@
import { useEffect, type RefObject } from 'react';
type Props = {
marketId: string;
orderbookRef: RefObject<HTMLDivElement>;
};
/**
* @description Center Orderbook on load and market change
* Assumed that the two sides are the same height
*/
export const useCenterOrderbook = ({ marketId, orderbookRef }: Props) => {
const orderbookEl = orderbookRef.current;
const { clientHeight, scrollHeight } = orderbookEl ?? {};
const shouldScroll = scrollHeight && clientHeight && scrollHeight > clientHeight;
useEffect(() => {
if (orderbookEl && shouldScroll) {
orderbookEl.scrollTo({ top: (scrollHeight - clientHeight) / 2 });
}
}, [shouldScroll, marketId]);
};
+373
View File
@@ -0,0 +1,373 @@
import { useEffect, useMemo, useRef } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import type { PerpetualMarketOrderbookLevel } from '@/constants/abacus';
import { SMALL_USD_DECIMALS, TOKEN_DECIMALS } from '@/constants/numbers';
import {
ORDERBOOK_ANIMATION_DURATION,
ORDERBOOK_HEIGHT,
ORDERBOOK_ROW_HEIGHT,
ORDERBOOK_ROW_PADDING_RIGHT,
ORDERBOOK_WIDTH,
} from '@/constants/orderbook';
import { getCurrentMarketConfig, getCurrentMarketOrderbookMap } from '@/state/perpetualsSelectors';
import { getAppTheme } from '@/state/configsSelectors';
import { MustBigNumber } from '@/lib/numbers';
import {
getHistogramXValues,
getRektFromIdx,
getXByColumn,
getYForElements,
} from '@/lib/orderbookHelpers';
import { useAppThemeContext } from '../useAppTheme';
type ElementProps = {
data: Array<PerpetualMarketOrderbookLevel | undefined>;
histogramRange: number;
side: PerpetualMarketOrderbookLevel['side'];
};
type StyleProps = {
histogramSide: 'left' | 'right';
};
enum OrderbookRowAnimationType {
REMOVE,
NEW,
NONE,
}
export const useDrawOrderbook = ({
data,
histogramRange,
histogramSide,
side,
}: ElementProps & StyleProps) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const canvas = canvasRef.current;
const currentOrderbookMap = useSelector(getCurrentMarketOrderbookMap, shallowEqual);
const { stepSizeDecimals = TOKEN_DECIMALS, tickSizeDecimals = SMALL_USD_DECIMALS } =
useSelector(getCurrentMarketConfig, shallowEqual) || {};
const prevData = useRef<typeof data>(data);
const theme = useAppThemeContext();
/**
* Scale canvas using device pixel ratio to unblur drawn text
* @url https://stackoverflow.com/questions/15661339/how-do-i-fix-blurry-text-in-my-html5-canvas/65124939#65124939
* @returns adjusted canvas width/height/rowHeight used in coordinates for drawing
**/
const { canvasWidth, canvasHeight, rowHeight } = useMemo(() => {
const ratio = window.devicePixelRatio || 1;
if (!canvas) {
return {
canvasWidth: ORDERBOOK_WIDTH / devicePixelRatio,
canvasHeight: ORDERBOOK_HEIGHT / devicePixelRatio,
rowHeight: ORDERBOOK_ROW_HEIGHT / devicePixelRatio,
};
}
const ctx = canvas.getContext('2d');
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
if (ctx) {
ctx.scale(ratio, ratio);
ctx.font = '12px Satoshi';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.imageSmoothingQuality = 'high';
}
return {
canvasWidth: canvas.width / ratio,
canvasHeight: canvas.height / ratio,
rowHeight: ORDERBOOK_ROW_HEIGHT,
};
}, [canvas]);
const drawBars = ({
barType,
ctx,
depthOrSizeValue,
gradientMultiplier,
histogramAccentColor,
histogramSide,
idx,
}: {
barType: 'depth' | 'size';
ctx: CanvasRenderingContext2D;
depthOrSizeValue: number;
gradientMultiplier: number;
histogramAccentColor: string;
histogramSide: 'left' | 'right';
idx: number;
}) => {
const { x1, x2, y1, y2 } = getRektFromIdx({
idx,
rowHeight,
canvasWidth,
canvasHeight,
side,
});
// X values
const maxHistogramBarWidth = x2 - x1 - (barType === 'size' ? 8 : 2);
const barWidth = depthOrSizeValue
? Math.min((depthOrSizeValue / histogramRange) * maxHistogramBarWidth, maxHistogramBarWidth)
: 0;
const { gradient, bar } = getHistogramXValues({
barWidth,
canvasWidth,
gradientMultiplier,
histogramSide,
});
// Gradient
let linearGradient;
try {
linearGradient = ctx.createLinearGradient(gradient.x1, y1, gradient.x2, y2);
linearGradient.addColorStop(0, histogramAccentColor);
linearGradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.fillStyle = linearGradient;
} catch (err) {
ctx.fillStyle = 'rgba(0, 0, 0, 0)';
}
ctx.beginPath();
// Bar
const { bar: y } = getYForElements({ y: y1, rowHeight });
if (ctx.roundRect) {
ctx.roundRect(
bar.x1,
y,
bar.x2,
rowHeight - 2,
histogramSide === 'right' ? [2, 0, 0, 2] : [0, 2, 2, 0]
);
} else {
ctx.rect(bar.x1, y, bar.x2, rowHeight - 2);
}
ctx.fill();
};
const drawText = ({
animationType = OrderbookRowAnimationType.NONE,
ctx,
idx,
mine,
price,
size,
}: {
animationType?: OrderbookRowAnimationType;
ctx: CanvasRenderingContext2D;
idx: number;
mine?: number;
price?: number;
size?: number;
}) => {
const { y1 } = getRektFromIdx({
idx,
rowHeight,
canvasWidth,
canvasHeight,
side,
});
const { text: y } = getYForElements({ y: y1, rowHeight });
let textColor: string;
let updatedTextColor: string | undefined;
switch (animationType) {
case OrderbookRowAnimationType.REMOVE: {
textColor = theme.textSecondary;
break;
}
case OrderbookRowAnimationType.NEW: {
updatedTextColor = side === 'bid' ? theme.positive : theme.negative;
textColor = theme.textPrimary;
break;
}
default: {
textColor = theme.textPrimary;
break;
}
}
// Size text
if (size) {
ctx.fillStyle = updatedTextColor ?? textColor;
ctx.fillText(
MustBigNumber(size).toFixed(stepSizeDecimals ?? TOKEN_DECIMALS),
getXByColumn({ canvasWidth, colIdx: 0 }) - ORDERBOOK_ROW_PADDING_RIGHT,
y
);
}
// Price text
if (price) {
ctx.fillStyle = textColor;
ctx.fillText(
MustBigNumber(price).toFixed(tickSizeDecimals ?? SMALL_USD_DECIMALS),
getXByColumn({ canvasWidth, colIdx: 1 }) - ORDERBOOK_ROW_PADDING_RIGHT,
y
);
}
// Mine text
if (mine) {
ctx.fillStyle = textColor;
ctx.fillText(
MustBigNumber(mine).toFixed(stepSizeDecimals ?? TOKEN_DECIMALS),
getXByColumn({ canvasWidth, colIdx: 2 }) - ORDERBOOK_ROW_PADDING_RIGHT,
y
);
}
};
const drawOrderbookRow = ({
ctx,
idx,
rowToRender,
animationType = OrderbookRowAnimationType.NONE,
}: {
ctx: CanvasRenderingContext2D;
idx: number;
rowToRender?: PerpetualMarketOrderbookLevel;
animationType?: OrderbookRowAnimationType;
}) => {
if (!rowToRender) return;
const { depth, mine, price, size } = rowToRender;
const histogramAccentColor = side === 'bid' ? theme.positiveFaded : theme.negativeFaded;
// Depth Bar
if (depth) {
drawBars({
barType: 'depth',
ctx,
depthOrSizeValue: depth,
gradientMultiplier: 1.3,
histogramAccentColor,
histogramSide,
idx,
});
}
// Size Bar
drawBars({
barType: 'size',
ctx,
depthOrSizeValue: size,
gradientMultiplier: 5,
histogramAccentColor,
histogramSide,
idx,
});
// Size, Price, Mine
drawText({
animationType,
ctx,
idx,
mine,
price,
size,
});
};
// Update histograms and row contents on data change
useEffect(() => {
const ctx = canvas?.getContext('2d');
if (!canvas || !ctx) return;
// Clear canvas before redraw
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
// Animate row removal and update
const mapOfOrderbookPriceLevels =
side && currentOrderbookMap?.[side === 'ask' ? 'asks' : 'bids'];
const empty: number[] = [];
const removed: number[] = [];
const updated: number[] = [];
prevData.current.forEach((row, idx) => {
if (!row) {
empty.push(idx);
return;
}
if (mapOfOrderbookPriceLevels?.[row.price] === 0) {
removed.push(idx);
drawOrderbookRow({
ctx,
idx,
rowToRender: row,
animationType: OrderbookRowAnimationType.REMOVE,
});
} else if (mapOfOrderbookPriceLevels?.[row.price] === row?.size) {
drawOrderbookRow({
ctx,
idx,
rowToRender: data[idx],
animationType: OrderbookRowAnimationType.NONE,
});
} else {
updated.push(idx);
drawOrderbookRow({
ctx,
idx,
rowToRender: row,
animationType: OrderbookRowAnimationType.NEW,
});
}
});
setTimeout(() => {
[...empty, ...removed, ...updated].forEach((idx) => {
const { x1, y1, x2, y2 } = getRektFromIdx({
idx,
rowHeight,
canvasWidth,
canvasHeight,
side,
});
ctx.clearRect(x1, y1, x2 - x1, y2 - y1);
drawOrderbookRow({
ctx,
idx,
rowToRender: data[idx],
});
});
}, ORDERBOOK_ANIMATION_DURATION);
prevData.current = data;
}, [
canvasHeight,
canvasWidth,
rowHeight,
data,
histogramRange,
stepSizeDecimals,
tickSizeDecimals,
histogramSide,
side,
theme,
currentOrderbookMap,
]);
return { canvasRef };
};
+126
View File
@@ -0,0 +1,126 @@
import { useMemo } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { type PerpetualMarketOrderbookLevel } from '@/constants/abacus';
import { DepthChartSeries, DepthChartDatum } from '@/constants/charts';
import { getCurrentMarketOrderbook } from '@/state/perpetualsSelectors';
import { getSubaccountOpenOrdersBySideAndPrice } from '@/state/accountSelectors';
import { MustBigNumber } from '@/lib/numbers';
export const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number }) => {
const orderbook = useSelector(getCurrentMarketOrderbook, shallowEqual);
const openOrdersBySideAndPrice =
useSelector(getSubaccountOpenOrdersBySideAndPrice, shallowEqual) || {};
return useMemo(() => {
const asks: Array<PerpetualMarketOrderbookLevel | undefined> = (
orderbook?.asks?.toArray() ?? []
)
.map(
(row, idx: number) =>
({
key: `ask-${idx}`,
side: 'ask',
mine: openOrdersBySideAndPrice[OrderSide.SELL]?.[row.price]?.size,
...row,
} as PerpetualMarketOrderbookLevel)
)
.slice(0, maxRowsPerSide);
const bids: Array<PerpetualMarketOrderbookLevel | undefined> = (
orderbook?.bids?.toArray() ?? []
)
.map(
(row, idx: number) =>
({
key: `bid-${idx}`,
side: 'bid',
mine: openOrdersBySideAndPrice[OrderSide.BUY]?.[row.price]?.size,
...row,
} as PerpetualMarketOrderbookLevel)
)
.slice(0, maxRowsPerSide);
// Prevent the bid/ask sides from crossing by using the offsets.
// While the books are crossing...
while (asks[0] && bids[0] && bids[0]!.price >= asks[0].price) {
// Drop the order on the side with the lower offset.
// The offset of the other side is higher and so supercedes.
if (bids[0]!.offset === asks[0].offset) {
// If offsets are the same, give precedence to the larger size. In this case,
// one of the sizes *should* be zero, but we simply check for the larger size.
if (bids[0]!.size > asks[0].size) {
asks.shift();
} else {
bids.pop();
}
} else {
// Offsets are not equal. Give precedence to the larger offset.
if (bids[0]!.offset > asks[0].offset) {
asks.shift();
} else {
bids.pop();
}
}
}
const spread =
asks[0]?.price && bids[0]?.price ? MustBigNumber(asks[0].price).minus(bids[0].price) : null;
const spreadPercent = orderbook?.spreadPercent;
const histogramRange = Math.max(
isNaN(Number(bids[bids.length - 1]?.depth)) ? 0 : Number(bids[bids.length - 1]?.depth),
isNaN(Number(asks[asks.length - 1]?.depth)) ? 0 : Number(asks[asks.length - 1]?.depth)
);
return {
asks,
bids,
spread,
spreadPercent,
histogramRange,
hasOrderbook: !!orderbook,
};
}, [orderbook, openOrdersBySideAndPrice]);
};
export const useOrderbookValuesForDepthChart = () => {
const orderbook = useSelector(getCurrentMarketOrderbook, shallowEqual);
return useMemo(() => {
const bids = (orderbook?.bids?.toArray() ?? [])
.filter(Boolean)
.map((datum) => ({ ...datum, seriesKey: DepthChartSeries.Bids } as DepthChartDatum));
const asks = (orderbook?.asks?.toArray() ?? [])
.filter(Boolean)
.map((datum) => ({ ...datum, seriesKey: DepthChartSeries.Asks } as DepthChartDatum));
const lowestBid = bids[bids.length - 1];
const highestBid = bids[0];
const lowestAsk = asks[0];
const highestAsk = asks[asks.length - 1];
const midMarketPrice = orderbook?.midPrice;
const spread = MustBigNumber(lowestAsk?.price ?? 0).minus(highestBid?.price ?? 0);
const spreadPercent = orderbook?.spreadPercent;
return {
bids,
asks,
lowestBid,
highestBid,
lowestAsk,
highestAsk,
midMarketPrice,
spread,
spreadPercent,
orderbook,
};
}, [orderbook]);
};
@@ -0,0 +1,39 @@
import { type RefObject, useEffect, useState } from 'react';
export const useSpreadRowScrollListener = ({
orderbookRef,
spreadRowRef,
}: {
orderbookRef: RefObject<HTMLDivElement>;
spreadRowRef: RefObject<HTMLDivElement>;
}) => {
const [displaySide, setDisplaySide] = useState<string>();
useEffect(() => {
const onScroll = () => {
if (spreadRowRef.current && orderbookRef.current) {
const { clientHeight } = orderbookRef.current;
const parent = orderbookRef.current.getBoundingClientRect();
const spread = spreadRowRef.current.getBoundingClientRect();
const spreadTop = spread.top - parent.top;
const spreadBottom = spread.bottom - parent.top;
if (spreadBottom > clientHeight) {
setDisplaySide('bottom');
} else if (spreadTop < 0) {
setDisplaySide('top');
} else {
setDisplaySide(undefined);
}
}
};
orderbookRef.current?.addEventListener('scroll', onScroll, false);
return () => {
orderbookRef.current?.removeEventListener('scroll', onScroll, false);
};
}, [orderbookRef.current, spreadRowRef.current]);
return displaySide;
};
+1 -2
View File
@@ -4,7 +4,6 @@ import { useSelector } from 'react-redux';
import type { IChartingLibraryWidget, ThemeName } from 'public/tradingview/charting_library';
import { AppTheme } from '@/state/configs';
import { getAppTheme } from '@/state/configsSelectors';
import { getWidgetOverrides } from '@/lib/tradingView/utils';
@@ -29,7 +28,7 @@ export const useTradingViewTheme = ({
tvWidget: (IChartingLibraryWidget & { _id?: string; _ready?: boolean }) | null;
isWidgetReady?: boolean;
}) => {
const appTheme = useSelector(getAppTheme);
const appTheme: AppTheme = useSelector(getAppTheme);
useEffect(() => {
if (tvWidget && isWidgetReady) {
+37 -2
View File
@@ -6,14 +6,22 @@ import { NOBLE_BECH32_PREFIX, LocalWallet, type Subaccount } from '@dydxprotocol
import { OnboardingGuard, OnboardingState, type EvmDerivedAddresses } from '@/constants/account';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization';
import { LocalStorageKey, LOCAL_STORAGE_VERSIONS } from '@/constants/localStorage';
import { DydxAddress, EvmAddress, PrivateInformation } from '@/constants/wallets';
import {
DydxAddress,
EvmAddress,
PrivateInformation,
TEST_WALLET_EVM_ADDRESS,
WalletType,
} from '@/constants/wallets';
import { setOnboardingState, setOnboardingGuard } from '@/state/account';
import { forceOpenDialog } from '@/state/dialogs';
import abacusStateManager from '@/lib/abacus';
import { log } from '@/lib/telemetry';
import { testFlags } from '@/lib/testFlags';
import { useDydxClient } from './useDydxClient';
import { useLocalStorage } from './useLocalStorage';
@@ -153,6 +161,7 @@ const useAccountsContext = () => {
// dYdX wallet / onboarding state
const [localDydxWallet, setLocalDydxWallet] = useState<LocalWallet>();
const [localNobleWallet, setLocalNobleWallet] = useState<LocalWallet>();
const [hdKey, setHdKey] = useState<PrivateInformation>();
const dydxAccounts = useMemo(() => localDydxWallet?.accounts, [localDydxWallet]);
@@ -162,6 +171,11 @@ const useAccountsContext = () => {
[localDydxWallet]
);
const nobleAddress = useMemo(
() => localNobleWallet?.address,
[localNobleWallet]
);
const setWalletFromEvmSignature = async (signature: string) => {
const { wallet, mnemonic, privateKey, publicKey } = await getWalletFromEvmSignature({
signature,
@@ -178,7 +192,26 @@ const useAccountsContext = () => {
useEffect(() => {
(async () => {
if (connectedDydxAddress && signerGraz) {
if (walletType === WalletType.TestWallet) {
// Get override values. Use the testFlags value if it exists, otherwise use the previously
// saved value where possible. If neither exist, use a default garbage value.
const addressOverride: DydxAddress = testFlags.addressOverride as DydxAddress ||
evmDerivedAddresses?.[TEST_WALLET_EVM_ADDRESS]?.dydxAddress as DydxAddress ||
'dydx1';
dispatch(setOnboardingState(OnboardingState.WalletConnected));
// Set variables.
saveEvmDerivedAccount({
evmAddress: TEST_WALLET_EVM_ADDRESS,
dydxAddress: addressOverride,
});
const wallet = new LocalWallet();
wallet.address = addressOverride;
setLocalDydxWallet(wallet);
dispatch(setOnboardingState(OnboardingState.AccountConnected));
} else if (connectedDydxAddress && signerGraz) {
dispatch(setOnboardingState(OnboardingState.WalletConnected));
try {
setLocalDydxWallet(await LocalWallet.fromOfflineSigner(signerGraz));
@@ -224,6 +257,7 @@ const useAccountsContext = () => {
if (hdKey?.mnemonic) {
const nobleWallet = await LocalWallet.fromMnemonic(hdKey.mnemonic, NOBLE_BECH32_PREFIX);
abacusStateManager.setNobleWallet(nobleWallet);
setLocalNobleWallet(nobleWallet);
}
};
setNobleWallet();
@@ -322,6 +356,7 @@ const useAccountsContext = () => {
localDydxWallet,
dydxAccounts,
dydxAddress,
nobleAddress,
// Onboarding state
saveHasAcknowledgedTerms,
+16
View File
@@ -0,0 +1,16 @@
import { useSelector } from 'react-redux';
import { ThemeProvider } from 'styled-components';
import { AppTheme } from '@/state/configs';
import { getAppTheme } from '@/state/configsSelectors';
import { Themes } from '@/styles/themes';
export const AppThemeProvider = ({ ...props }) => {
return <ThemeProvider theme={useAppThemeContext()} {...props} />
};
export const useAppThemeContext = () => {
const theme: AppTheme = useSelector(getAppTheme);
return Themes[theme];
}
-44
View File
@@ -1,44 +0,0 @@
import { useMemo } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { DepthChartSeries, DepthChartDatum } from '@/constants/charts';
import { getCurrentMarketOrderbook } from '@/state/perpetualsSelectors';
import { MustBigNumber } from '@/lib/numbers';
export const useOrderbookValuesForDepthChart = () => {
const orderbook = useSelector(getCurrentMarketOrderbook, shallowEqual);
return useMemo(() => {
const bids = (orderbook?.bids?.toArray() ?? [])
.filter(Boolean)
.map((datum) => ({ ...datum, seriesKey: DepthChartSeries.Bids } as DepthChartDatum));
const asks = (orderbook?.asks?.toArray() ?? [])
.filter(Boolean)
.map((datum) => ({ ...datum, seriesKey: DepthChartSeries.Asks } as DepthChartDatum));
const lowestBid = bids[bids.length - 1];
const highestBid = bids[0];
const lowestAsk = asks[0];
const highestAsk = asks[asks.length - 1];
const midMarketPrice = orderbook?.midPrice;
const spread = MustBigNumber(lowestAsk?.price ?? 0).minus(highestBid?.price ?? 0);
const spreadPercent = orderbook?.spreadPercent;
return {
bids,
asks,
lowestBid,
highestBid,
lowestAsk,
highestAsk,
midMarketPrice,
spread,
spreadPercent,
orderbook,
};
}, [orderbook]);
};
+7 -1
View File
@@ -198,7 +198,13 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
);
const ibcMsg: EncodeObject = {
typeUrl: transaction.msgTypeUrl,
value: transaction.msg,
value: {
...transaction.msg,
timeoutTimestamp: transaction.msg.timeoutTimestamp
// Squid returns timeoutTimestamp as Long, but the signer expects BigInt
? BigInt(Long.fromValue(transaction.msg.timeoutTimestamp).toString())
: undefined,
},
};
return await compositeClient.send(
+13
View File
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { EvmDerivedAddresses } from '@/constants/account';
import { STRING_KEYS } from '@/constants/localization';
import { LocalStorageKey } from '@/constants/localStorage';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
@@ -38,6 +39,7 @@ import { getWalletConnection, parseWalletError } from '@/lib/wallet';
import { log } from '@/lib/telemetry';
import { useStringGetter } from './useStringGetter';
import { testFlags } from '@/lib/testFlags';
export const useWalletConnection = () => {
const stringGetter = useStringGetter();
@@ -138,6 +140,8 @@ export const useWalletConnection = () => {
walletType: cosmosWalletType,
});
}
} else if (walletConnection.type === WalletConnectionType.TestWallet) {
saveEvmAddress(STRING_KEYS.TEST_WALLET as EvmAddress);
} else {
const isAccountConnected = Boolean(
evmAddress && evmDerivedAddresses[evmAddress]?.encryptedSignature
@@ -224,6 +228,15 @@ export const useWalletConnection = () => {
setSelectedWalletType(walletType);
};
// On page load, if testFlag.address is set, connect to the test wallet.
useEffect(() => {
(async () => {
if (testFlags.addressOverride) {
setSelectedWalletType(WalletType.TestWallet);
}
})();
}, []);
return {
// Wallet connection
walletType,
+2
View File
@@ -39,6 +39,7 @@ export { default as MarketsIcon } from './markets.svg';
export { default as MenuIcon } from './menu.svg';
export { default as MigrateIcon } from './migrate.svg';
export { default as MintscanIcon } from './logos/mintscan.svg';
export { default as MoonIcon } from './moon.svg';
export { default as OrderbookIcon } from './orderbook.svg';
export { default as OverviewIcon } from './overview.svg';
export { default as PencilIcon } from './pencil.svg';
@@ -56,6 +57,7 @@ export { default as SendIcon } from './send.svg';
export { default as ShareIcon } from './share.svg';
export { default as ShowIcon } from './show.svg';
export { default as StarIcon } from './star.svg';
export { default as SunIcon } from './sun.svg';
export { default as TerminalIcon } from './terminal.svg';
export { default as TogglesMenuIcon } from './toggles-menu.svg';
export { default as TradeIcon } from './trade.svg';
+50 -43
View File
@@ -1,45 +1,52 @@
const LogoShortIcon: React.FC<{ id?: string }> = ({ id }: { id?: string }) => (
<svg
width="135"
height="145"
viewBox="0 0 135 145"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M100.986 0L0 144.988H31.0048L132.514 0H100.986Z" fill="white" />
<path
d="M34.2346 0L63.9475 42.7232L48.4451 66.0268L2.58386 0H34.2346Z"
fill={`url(#${id ? `${id}_logo_gradient_0` : 'paint0_linear'})`}
/>
<path
d="M103.995 145L71.0526 97.7455L86.555 75.0893L135 145H103.995Z"
fill={`url(#${id ? `${id}_logo_gradient_1` : 'paint1_linear'})`}
/>
<defs>
<linearGradient
id={id ? `${id}_logo_gradient_0` : 'paint0_linear'}
x1="27.1293"
y1="9.0625"
x2="69.773"
y2="60.4324"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="white" />
<stop offset="1" stopColor="white" stopOpacity="0.55" />
</linearGradient>
<linearGradient
id={id ? `${id}_logo_gradient_1` : 'paint1_linear'}
x1="111.1"
y1="133.996"
x2="58.6959"
y2="63.4999"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#6966FF" />
<stop offset="1" stopColor="#6966FF" stopOpacity="0.36" />
</linearGradient>
</defs>
</svg>
);
import { useAppThemeContext } from '@/hooks/useAppTheme';
const LogoShortIcon: React.FC<{ id?: string }> = ({ id }: { id?: string }) => {
const theme = useAppThemeContext();
const fill = theme.logoFill;
return (
<svg
width="135"
height="145"
viewBox="0 0 135 145"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M100.986 0L0 144.988H31.0048L132.514 0H100.986Z" fill={fill} />
<path
d="M34.2346 0L63.9475 42.7232L48.4451 66.0268L2.58386 0H34.2346Z"
fill={`url(#${id ? `${id}_logo_gradient_0` : 'paint0_linear'})`}
/>
<path
d="M103.995 145L71.0526 97.7455L86.555 75.0893L135 145H103.995Z"
fill={`url(#${id ? `${id}_logo_gradient_1` : 'paint1_linear'})`}
/>
<defs>
<linearGradient
id={id ? `${id}_logo_gradient_0` : 'paint0_linear'}
x1="27.1293"
y1="9.0625"
x2="69.773"
y2="60.4324"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={fill} />
<stop offset="1" stopColor={fill} stopOpacity="0.55" />
</linearGradient>
<linearGradient
id={id ? `${id}_logo_gradient_1` : 'paint1_linear'}
x1="111.1"
y1="133.996"
x2="58.6959"
y2="63.4999"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#6966FF" />
<stop offset="1" stopColor="#6966FF" stopOpacity="0.36" />
</linearGradient>
</defs>
</svg>
);
};
export default LogoShortIcon;
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
</svg>

After

Width:  |  Height:  |  Size: 380 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />
</svg>

After

Width:  |  Height:  |  Size: 393 B

+2 -2
View File
@@ -1,6 +1,5 @@
@import 'styles/fonts.css';
@import 'styles/text.css';
@import 'styles/colors.css';
@import 'styles/animations.css';
:root {
@@ -9,6 +8,7 @@
background-color: var(--color-layer-2);
user-select: none;
overflow: hidden;
--border: var(--default-border-width) solid var(--color-border);
}
body,
@@ -54,7 +54,7 @@ button:disabled {
cursor: pointer;
}
:focus-visible{
:focus-visible {
outline: var(--color-accent) 1px solid;
outline-offset: -1px;
/* outline-offset: -2px; */
+18 -2
View File
@@ -173,6 +173,10 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
return parsedObj as T;
}
if (typeof x === 'bigint') {
return x.toString() as T;
}
throw new Error(`Unsupported data type: ${typeof x}`);
}
@@ -363,7 +367,13 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
try {
const ibcMsg = {
typeUrl: params.msgTypeUrl, // '/ibc.applications.transfer.v1.MsgTransfer',
value: params.msg,
value: {
...params.msg,
timeoutTimestamp: params.msg.timeoutTimestamp
// Squid returns timeoutTimestamp as Long, but the signer expects BigInt
? BigInt(Long.fromValue(params.msg.timeoutTimestamp).toString())
: undefined,
},
};
const fee = await this.nobleClient.simulateTransaction([ibcMsg]);
@@ -413,7 +423,13 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
);
const ibcMsg: EncodeObject = {
typeUrl: parsedIbcPayload.msgTypeUrl,
value: parsedIbcPayload.msg,
value: {
...parsedIbcPayload.msg,
timeoutTimestamp: parsedIbcPayload.msg.timeoutTimestamp
// Squid returns timeoutTimestamp as Long, but the signer expects BigInt
? BigInt(Long.fromValue(parsedIbcPayload.msg.timeoutTimestamp).toString())
: undefined,
},
};
const tx = await this.compositeClient.send(
+133
View File
@@ -0,0 +1,133 @@
// ------ Canvas helper methods ------ //
import type { MarketOrderbook, Nullable, PerpetualMarketOrderbookLevel } from '@/constants/abacus';
/**
* @returns top left x,y and bottom x,y from array idx
*/
export const getRektFromIdx = ({
idx,
canvasWidth,
canvasHeight,
rowHeight,
side = 'bid',
}: {
idx: number;
canvasWidth: number;
canvasHeight: number;
rowHeight: number;
side: PerpetualMarketOrderbookLevel['side'];
}) => {
/**
* Does not change
*/
const x1 = 0;
const x2 = Math.floor(canvasWidth);
/**
* Changes based on side
* Asks: The drawing should starts from the bottom of the orderbook
*/
if (side === 'ask') {
const y1 = Math.floor(canvasHeight - (idx + 1) * rowHeight);
const y2 = Math.floor(y1 + rowHeight);
return { x1, x2, y1, y2 };
}
const y1 = Math.floor(idx * rowHeight);
const y2 = Math.floor(y1 + rowHeight);
return { x1, x2, y1, y2 };
};
/**
* @returns y value for text and bar
*/
export const getYForElements = ({ y, rowHeight }: { y: number; rowHeight: number }) => ({
text: Math.floor(y + rowHeight / 2),
bar: Math.floor(y + 1),
});
/**
* @description X coordinate for text
* @returns Get X value by column, colIdx starts at 0
*/
export const getXByColumn = ({ canvasWidth, colIdx }: { canvasWidth: number; colIdx: number }) => {
return Math.floor(((colIdx + 1) * canvasWidth) / 3);
};
/**
* to === 'right' to === 'left'
* |===================| |===================|
* bar: a b c d
* a = 0
* b = depthBarWidth
* c = canvasWidth - depthBarWidth
* d = canvasWidth
*
*/
export const getHistogramXValues = ({
barWidth,
canvasWidth,
gradientMultiplier,
histogramSide,
}: {
barWidth: number;
canvasWidth: number;
gradientMultiplier: number;
histogramSide: 'left' | 'right';
}) => {
const gradient = {
x1: Math.floor(histogramSide === 'left' ? barWidth : canvasWidth - barWidth),
x2: Math.floor(
histogramSide === 'left'
? 0 - (canvasWidth * gradientMultiplier - canvasWidth)
: canvasWidth * gradientMultiplier
),
};
const bar = {
x1: Math.floor(histogramSide === 'left' ? 0 : canvasWidth - barWidth),
x2: Math.floor(histogramSide === 'left' ? Math.min(barWidth, canvasWidth) : canvasWidth),
};
return {
bar,
gradient,
};
};
// ------ Orderbook helper methods ------ //
export const processOrderbookToCreateMap = ({
orderbookMap,
newOrderbook,
}: {
orderbookMap?: {
asks: Record<string, number>;
bids: Record<string, number>;
};
newOrderbook: Nullable<MarketOrderbook>;
}) => {
// Create Orderbooks Map indexed by Price
const asks = newOrderbook?.asks?.toArray() ?? [];
const bids = newOrderbook?.bids?.toArray() ?? [];
const prevAsks = orderbookMap?.asks ?? {};
const prevBids = orderbookMap?.bids ?? {};
const newAsks = Object.fromEntries(asks.map((ask) => [ask.price, ask.size]) ?? {});
const newBids = Object.fromEntries(bids.map((bid) => [bid.price, bid.size]) ?? {});
Object.keys(prevAsks).forEach((price) => {
if (!newAsks[price] || newAsks[price] === 0) {
newAsks[price] = 0;
}
});
Object.keys(prevBids).forEach((price) => {
if (!newBids[price] || newBids[price] === 0) {
newBids[price] = 0;
}
});
return {
newAsks,
newBids,
};
};
+20
View File
@@ -0,0 +1,20 @@
import { css } from 'styled-components';
import { type RiskLevels } from '@/constants/abacus';
export const UsageColorFromRiskLevel = (riskLevel: RiskLevels) =>
({
low: css`
color: var(--color-risk-low);
`,
medium: css`
color: var(--color-risk-medium);
`,
high: css`
color: var(--color-risk-high);
`,
}[riskLevel.name]);
export const generateFadedColorVariant = (colorHex: string, opacityHex: string) => {
return `${colorHex}${opacityHex}`;
};
+4
View File
@@ -22,6 +22,10 @@ class TestFlags {
get showMobileSignInOption() {
return !!this.queryParams.mobilesignin;
}
get addressOverride():string {
return this.queryParams.address;
}
}
export const testFlags = new TestFlags();
+21 -23
View File
@@ -1,11 +1,9 @@
import Color from 'color';
import isEmpty from 'lodash/isEmpty';
import { Candle, TradingViewBar, TradingViewSymbol } from '@/constants/candles';
import { Colors } from '@/styles/colors';
import { AppTheme } from '@/state/configs';
import { Themes } from '@/styles/themes';
export const mapCandle = ({
startedAt,
open,
@@ -49,42 +47,42 @@ export const getHistorySlice = ({
return bars.filter(({ time }) => time >= fromMs);
};
export const getWidgetOverrides = (theme: AppTheme) => {
const colors = Colors[theme];
export const getWidgetOverrides = (appTheme: AppTheme) => {
const theme = Themes[appTheme];
return {
overrides: {
'paneProperties.background': Color(colors.layer2).hex(),
'paneProperties.horzGridProperties.color': Color(colors.layer3).hex(),
'paneProperties.vertGridProperties.color': Color(colors.layer3).hex(),
'paneProperties.background': theme.layer2,
'paneProperties.horzGridProperties.color': theme.layer3,
'paneProperties.vertGridProperties.color': theme.layer3,
'paneProperties.crossHairProperties.style': 1,
'paneProperties.legendProperties.showBarChange': false,
'paneProperties.backgroundType': 'solid',
'mainSeriesProperties.style': 1,
'mainSeriesProperties.candleStyle.upColor': Color(colors.positive).hex(),
'mainSeriesProperties.candleStyle.borderUpColor': Color(colors.positive).hex(),
'mainSeriesProperties.candleStyle.wickUpColor': Color(colors.positive).hex(),
'mainSeriesProperties.candleStyle.downColor': Color(colors.negative).hex(),
'mainSeriesProperties.candleStyle.borderDownColor': Color(colors.negative).hex(),
'mainSeriesProperties.candleStyle.wickDownColor': Color(colors.negative).hex(),
'mainSeriesProperties.candleStyle.upColor': theme.positive,
'mainSeriesProperties.candleStyle.borderUpColor': theme.positive,
'mainSeriesProperties.candleStyle.wickUpColor': theme.positive,
'mainSeriesProperties.candleStyle.downColor': theme.negative,
'mainSeriesProperties.candleStyle.borderDownColor': theme.negative,
'mainSeriesProperties.candleStyle.wickDownColor': theme.negative,
'mainSeriesProperties.statusViewStyle.symbolTextSource': 'ticker',
'scalesProperties.textColor': Color(colors.text1).hex(),
'scalesProperties.backgroundColor': Color(colors.layer2).hex(),
'scalesProperties.lineColor': Color(colors.layer3).hex(),
'scalesProperties.textColor': theme.textPrimary,
'scalesProperties.backgroundColor': theme.layer2,
'scalesProperties.lineColor': theme.layer3,
},
studies_overrides: {
'volume.volume.color.0': Color(colors.negative).hex(),
'volume.volume.color.1': Color(colors.positive).hex(),
'volume.volume.color.0': theme.negative,
'volume.volume.color.1': theme.positive,
'volume.volume ma.visible': false,
'relative strength index.plot.color': Color(colors.accent).hex(),
'relative strength index.plot.color': theme.accent,
'relative strength index.plot.linewidth': 1.5,
'relative strength index.hlines background.color': '#134A9F',
},
loading_screen: {
backgroundColor: Color(colors.layer2).hex(),
foregroundColor: Color(colors.layer2).hex(),
backgroundColor: theme.layer2,
foregroundColor: theme.layer2,
},
};
};
+6
View File
@@ -62,6 +62,12 @@ export const getWalletConnection = ({
type: WalletConnectionType.CosmosSigner,
};
}
case WalletConnectionType.TestWallet: {
return {
type: WalletConnectionType.TestWallet,
};
}
}
}
};
+11 -6
View File
@@ -5,7 +5,7 @@ import { useEnsName } from 'wagmi';
import { useNavigate } from 'react-router-dom';
import { ButtonSize } from '@/constants/buttons';
import { TransferInputField, TransferType } from '@/constants/abacus';
import { TransferType } from '@/constants/abacus';
import { layoutMixins } from '@/styles/layoutMixins';
@@ -26,12 +26,13 @@ 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 { DYDXBalancePanel } from './rewards/DYDXBalancePanel';
import { MigratePanel } from './rewards/MigratePanel';
import { GovernancePanel } from './rewards/GovernancePanel';
import { StakingPanel } from './rewards/StakingPanel';
const ENS_CHAIN_ID = 1; // Ethereum
@@ -178,7 +179,7 @@ const Profile = () => {
onClick={() => dispatch(openDialog({ type: DialogTypes.Help }))}
/>
</Styled.EqualGrid>
<MigratePanel />
<DYDXBalancePanel />
@@ -230,6 +231,9 @@ const Profile = () => {
withInnerBorders={false}
/>
</Styled.TablePanel>
<GovernancePanel />
<StakingPanel />
</Styled.MobileProfileLayout>
);
};
@@ -243,6 +247,7 @@ Styled.MobileProfileLayout = styled.div`
gap: 1rem;
padding: 1.25rem 0.9rem;
max-width: 100vw;
`;
Styled.Header = styled.header`
@@ -259,8 +264,8 @@ Styled.ProfileIcon = styled.div`
border-radius: 50%;
background: linear-gradient(
135deg,
var(--theme-classic-color-yellow) 0%,
var(--theme-classic-color-red) 100%
${({ theme }) => theme.profileYellow} 0%,
${({ theme }) => theme.profileRed} 100%
);
`;
@@ -337,7 +342,7 @@ Styled.Details = styled(Details)`
Styled.RewardsPanel = styled(Panel)`
align-self: flex-start;
&,
> * {
height: 100%;
+75
View File
@@ -0,0 +1,75 @@
import styled, { AnyStyledComponent } from 'styled-components';
import { useDispatch } from 'react-redux';
import { ButtonAction, ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { DialogTypes } from '@/constants/dialogs';
import { useStringGetter, useURLConfigs } from '@/hooks';
import { Panel } from '@/components/Panel';
import { IconName } from '@/components/Icon';
import { IconButton } from '@/components/IconButton';
import { Link } from '@/components/Link';
import { openDialog } from '@/state/dialogs';
export const GovernancePanel = () => {
const stringGetter = useStringGetter();
const dispatch = useDispatch();
const { governanceLearnMore } = useURLConfigs();
return (
<Panel
slotHeaderContent={
<Styled.Title>{stringGetter({ key: STRING_KEYS.GOVERNANCE })}</Styled.Title>
}
slotRight={
<Styled.Arrow>
<Styled.IconButton
action={ButtonAction.Base}
iconName={IconName.Arrow}
size={ButtonSize.Small}
/>
</Styled.Arrow>
}
onClick={() => dispatch(openDialog({ type: DialogTypes.ExternalNavKeplr }))}
>
<Styled.Description>
{stringGetter({ key: STRING_KEYS.GOVERNANCE_DESCRIPTION })}
<Link href={governanceLearnMore} onClick={(e) => e.stopPropagation()}>
{stringGetter({ key: STRING_KEYS.LEARN_MORE })}
</Link>
</Styled.Description>
</Panel>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Description = styled.div`
color: var(--color-text-0);
--link-color: var(--color-text-1);
a {
display: inline;
::before {
content: ' ';
}
}
`;
Styled.IconButton = styled(IconButton)`
color: var(--color-text-0);
--color-border: var(--color-layer-6);
`;
Styled.Arrow = styled.div`
padding-right: 1.5rem;
`;
Styled.Title = styled.h3`
font: var(--font-medium-book);
color: var(--color-text-2);
margin-bottom: -1rem;
`;
+3 -2
View File
@@ -146,14 +146,15 @@ Styled.Title = styled.h3`
color: var(--color-text-2);
padding: var(--panel-paddingY) var(--panel-paddingX) 0;
margin-bottom: -0.5rem;
`;
Styled.MigrateAction = styled.div`
${layoutMixins.flexEqualColumns}
align-items: center;
margin: 1rem;
gap: 1rem;
padding: 1rem;
margin: 1rem;
width: 100%;
background-color: var(--color-layer-2);
@@ -233,4 +234,4 @@ Styled.InlineRow = styled.div`
${layoutMixins.inlineRow}
color: var(--color-text-0);
--link-color: var(--color-text-1);
`;
`;
+9 -77
View File
@@ -3,45 +3,29 @@ import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { STRING_KEYS } from '@/constants/localization';
import { ButtonAction, ButtonSize } from '@/constants/buttons';
import { DialogTypes } from '@/constants/dialogs';
import { AppRoute } from '@/constants/routes';
import { useBreakpoints, useStringGetter, useURLConfigs } from '@/hooks';
import { useBreakpoints, useStringGetter } from '@/hooks';
import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins';
import { BackButton } from '@/components/BackButton';
import { Panel } from '@/components/Panel';
import { IconName } from '@/components/Icon';
import { IconButton } from '@/components/IconButton';
import { Link } from '@/components/Link';
import { openDialog } from '@/state/dialogs';
import { DYDXBalancePanel } from './DYDXBalancePanel';
import { MigratePanel } from './MigratePanel';
import { LaunchIncentivesPanel } from './LaunchIncentivesPanel';
import { RewardsHelpPanel } from './RewardsHelpPanel';
import { GovernancePanel } from './GovernancePanel';
import { StakingPanel } from './StakingPanel';
const RewardsPage = () => {
const dispatch = useDispatch();
const stringGetter = useStringGetter();
const { governanceLearnMore, stakingLearnMore } = useURLConfigs();
const { isTablet, isNotTablet } = useBreakpoints();
const navigate = useNavigate();
const panelArrow = (
<Styled.Arrow>
<Styled.IconButton
action={ButtonAction.Base}
iconName={IconName.Arrow}
size={ButtonSize.Small}
/>
</Styled.Arrow>
);
return (
<Styled.Page>
{isTablet && (
@@ -61,37 +45,12 @@ const RewardsPage = () => {
</Styled.PanelRowIncentivesAndBalance>
)}
<Styled.PanelRow>
<Styled.Panel
slotHeaderContent={
<Styled.Title>{stringGetter({ key: STRING_KEYS.GOVERNANCE })}</Styled.Title>
}
slotRight={panelArrow}
onClick={() => dispatch(openDialog({ type: DialogTypes.ExternalNavKeplr }))}
>
<Styled.Description>
{stringGetter({ key: STRING_KEYS.GOVERNANCE_DESCRIPTION })}
<Link href={governanceLearnMore} onClick={(e) => e.stopPropagation()}>
{stringGetter({ key: STRING_KEYS.LEARN_MORE })}
</Link>
</Styled.Description>
</Styled.Panel>
<Styled.Panel
slotHeaderContent={
<Styled.Title>{stringGetter({ key: STRING_KEYS.STAKING })}</Styled.Title>
}
slotRight={panelArrow}
onClick={() => dispatch(openDialog({ type: DialogTypes.ExternalNavKeplr }))}
>
<Styled.Description>
{stringGetter({ key: STRING_KEYS.STAKING_DESCRIPTION })}
<Link href={stakingLearnMore} onClick={(e) => e.stopPropagation()}>
{stringGetter({ key: STRING_KEYS.LEARN_MORE })}
</Link>
</Styled.Description>
</Styled.Panel>
</Styled.PanelRow>
{isNotTablet && (
<Styled.PanelRow>
<GovernancePanel />
<StakingPanel />
</Styled.PanelRow>
)}
<RewardsHelpPanel />
</Styled.Page>
@@ -140,24 +99,6 @@ Styled.Panel = styled(Panel)`
height: fit-content;
`;
Styled.Title = styled.h3`
font: var(--font-medium-book);
color: var(--color-text-2);
margin-bottom: -1rem;
`;
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;
@@ -171,12 +112,3 @@ Styled.PanelRow = styled.div`
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);
`;
Styled.Arrow = styled.div`
padding: 1rem;
`;
+73
View File
@@ -0,0 +1,73 @@
import styled, { AnyStyledComponent } from 'styled-components';
import { useDispatch } from 'react-redux';
import { ButtonAction, ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { DialogTypes } from '@/constants/dialogs';
import { useStringGetter, useURLConfigs } from '@/hooks';
import { Panel } from '@/components/Panel';
import { IconName } from '@/components/Icon';
import { IconButton } from '@/components/IconButton';
import { Link } from '@/components/Link';
import { openDialog } from '@/state/dialogs';
export const StakingPanel = () => {
const stringGetter = useStringGetter();
const dispatch = useDispatch();
const { stakingLearnMore } = useURLConfigs();
return (
<Panel
slotHeaderContent={<Styled.Title>{stringGetter({ key: STRING_KEYS.STAKING })}</Styled.Title>}
slotRight={
<Styled.Arrow>
<Styled.IconButton
action={ButtonAction.Base}
iconName={IconName.Arrow}
size={ButtonSize.Small}
/>
</Styled.Arrow>
}
onClick={() => dispatch(openDialog({ type: DialogTypes.ExternalNavKeplr }))}
>
<Styled.Description>
{stringGetter({ key: STRING_KEYS.STAKING_DESCRIPTION })}
<Link href={stakingLearnMore} onClick={(e) => e.stopPropagation()}>
{stringGetter({ key: STRING_KEYS.LEARN_MORE })}
</Link>
</Styled.Description>
</Panel>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Description = styled.div`
color: var(--color-text-0);
--link-color: var(--color-text-1);
a {
display: inline;
::before {
content: ' ';
}
}
`;
Styled.IconButton = styled(IconButton)`
color: var(--color-text-0);
--color-border: var(--color-layer-6);
`;
Styled.Arrow = styled.div`
padding-right: 1.5rem;
`;
Styled.Title = styled.h3`
font: var(--font-medium-book);
color: var(--color-text-2);
margin-bottom: -1rem;
`;
+6 -36
View File
@@ -1,6 +1,4 @@
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import styled, { AnyStyledComponent } from 'styled-components';
import { useState } from 'react';
import { TradeLayouts } from '@/constants/layout';
import { STRING_KEYS } from '@/constants/localization';
@@ -9,11 +7,9 @@ import { useStringGetter } from '@/hooks';
import { Tabs } from '@/components/Tabs';
import { Orderbook, orderbookMixins, OrderbookScrollBehavior } from '@/views/tables/Orderbook';
import { CanvasOrderbook } from '@/views/CanvasOrderbook';
import { LiveTrades } from '@/views/tables/LiveTrades';
import { getCurrentMarketId } from '@/state/perpetualsSelectors';
enum Tab {
Orderbook = 'Orderbook',
Trades = 'Trades',
@@ -27,29 +23,22 @@ const HISTOGRAM_SIDES_BY_LAYOUT = {
export const VerticalPanel = ({ tradeLayout }: { tradeLayout: TradeLayouts }) => {
const stringGetter = useStringGetter();
const [value, setValue] = useState(Tab.Orderbook);
const [scrollBehavior, setScrollBehavior] = useState<OrderbookScrollBehavior>('snapToCenter');
const marketId = useSelector(getCurrentMarketId);
useEffect(() => {
setScrollBehavior('snapToCenter');
}, [marketId]);
return (
<Styled.Tabs
<Tabs
fullWidthTabs
value={value}
onValueChange={(value: Tab) => {
setScrollBehavior('snapToCenter');
setValue(value);
}}
items={[
{
content: <Orderbook histogramSide={HISTOGRAM_SIDES_BY_LAYOUT[tradeLayout]} />,
asChild: true,
content: <CanvasOrderbook histogramSide={HISTOGRAM_SIDES_BY_LAYOUT[tradeLayout]} />,
label: stringGetter({ key: STRING_KEYS.ORDERBOOK_SHORT }),
value: Tab.Orderbook,
forceMount: true,
},
{
content: <LiveTrades histogramSide={HISTOGRAM_SIDES_BY_LAYOUT[tradeLayout]} />,
@@ -57,26 +46,7 @@ export const VerticalPanel = ({ tradeLayout }: { tradeLayout: TradeLayouts }) =>
value: Tab.Trades,
},
]}
onWheel={() => setScrollBehavior('free')}
withTransitions={false}
isShowingOrderbook={value === Tab.Orderbook}
scrollBehavior={scrollBehavior}
// style={{
// scrollSnapType: {
// 'snapToCenter': 'y mandatory',
// 'free': 'none',
// 'snapToCenterUnlessHovered': 'none',
// }[scrollBehavior],
// }}
/>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Tabs = styled(Tabs)<{
isShowingOrderbook: boolean;
scrollBehavior: OrderbookScrollBehavior;
}>`
${orderbookMixins.scrollArea}
`;
+4
View File
@@ -96,6 +96,8 @@ export const useTradeFormData = () => {
needsGoodUntil,
needsPostOnly,
needsReduceOnly,
postOnlyTooltip,
reduceOnlyTooltip,
timeInForceOptions,
} = tradeOptions || {};
@@ -111,6 +113,8 @@ export const useTradeFormData = () => {
needsGoodUntil,
needsPostOnly,
needsReduceOnly,
postOnlyTooltip,
reduceOnlyTooltip,
timeInForceOptions,
tradeErrors,
+26 -5
View File
@@ -14,6 +14,7 @@ import { LocalStorageKey } from '@/constants/localStorage';
import { DEFAULT_MARKETID } from '@/constants/markets';
import { getLocalStorage } from '@/lib/localStorage';
import { processOrderbookToCreateMap } from '@/lib/orderbookHelpers';
interface CandleDataByMarket {
data: Record<string, Candle[]>;
@@ -26,6 +27,13 @@ export interface PerpetualsState {
liveTrades?: Record<string, MarketTrade[]>;
markets?: Record<string, PerpetualMarket>;
orderbooks?: Record<string, MarketOrderbook>;
orderbooksMap?: Record<
string,
{
asks: Record<string, number>;
bids: Record<string, number>;
}
>;
historicalFundings: Record<string, MarketHistoricalFunding[]>;
}
@@ -35,6 +43,7 @@ const initialState: PerpetualsState = {
liveTrades: {},
markets: undefined,
orderbooks: undefined,
orderbooksMap: undefined,
historicalFundings: {},
};
@@ -96,12 +105,24 @@ export const perpetualsSlice = createSlice({
setOrderbook: (
state: PerpetualsState,
action: PayloadAction<{ orderbook?: Nullable<MarketOrderbook>; marketId: string }>
) => ({
...state,
orderbooks: merge({}, state.orderbooks, {
) => {
state.orderbooks = merge({}, state.orderbooks, {
[action.payload.marketId]: action.payload.orderbook,
}),
}),
});
const { newAsks, newBids } = processOrderbookToCreateMap({
orderbookMap: state.orderbooksMap?.[action.payload.marketId],
newOrderbook: action.payload.orderbook,
});
state.orderbooksMap = {
...(state.orderbooksMap ?? {}),
[action.payload.marketId]: {
asks: newAsks,
bids: newBids,
},
};
},
setTvChartResolution: (
state: PerpetualsState,
action: PayloadAction<{ marketId: string; resolution: string }>
+9 -13
View File
@@ -7,7 +7,6 @@ import { mapCandle } from '@/lib/tradingView/utils';
import type { RootState } from './_store';
/**
* @param state
* @returns marketId of the market the user is currently viewing
*/
export const getCurrentMarketId = (state: RootState) => state.perpetuals.currentMarketId;
@@ -21,7 +20,6 @@ export const getCurrentMarketAssetId = (state: RootState) => {
};
/**
* @param state
* @returns Record of PerpetualMarket indexed by MarketId
*/
export const getPerpetualMarkets = (state: RootState) => state.perpetuals.markets;
@@ -34,13 +32,11 @@ export const getMarketData = (marketId: string) => (state: RootState) =>
getPerpetualMarkets(state)?.[marketId];
/**
* @param state
* @returns marketIds of all markets
*/
export const getMarketIds = (state: RootState) => Object.keys(getPerpetualMarkets(state) ?? []);
/**
* @param state
* @returns PerpetualMarket data of the market the user is currently viewing
*/
export const getCurrentMarketData = (state: RootState) => {
@@ -49,7 +45,6 @@ export const getCurrentMarketData = (state: RootState) => {
};
/**
* @param state
* @returns MarketConfig data of the market the user is currently viewing
*/
export const getCurrentMarketConfig = (state: RootState) => getCurrentMarketData(state)?.configs;
@@ -62,13 +57,11 @@ export const getMarketConfig = (marketId: string) => (state: RootState) =>
getMarketData(marketId)(state)?.configs;
/**
* @param state
* @returns Record of list of MarketTrades indexed by marketId
*/
export const getLiveTrades = (state: RootState) => state.perpetuals.liveTrades;
/**
* @param state
* @returns List of MarketTrades of the market the user is currently viewing
*/
export const getCurrentMarketLiveTrades = (state: RootState) => {
@@ -78,13 +71,11 @@ export const getCurrentMarketLiveTrades = (state: RootState) => {
};
/**
* @param state
* @returns Record of subscribed or previously subscribed Orderbook data, indexed by marketId.
*/
export const getOrderbooks = (state: RootState) => state.perpetuals.orderbooks;
/**
* @param state
* @returns Orderbook data for the market the user is currently viewing
*/
export const getCurrentMarketOrderbook = (state: RootState) => {
@@ -94,13 +85,20 @@ export const getCurrentMarketOrderbook = (state: RootState) => {
};
/**
* @param state
* @returns Orderbook data as a Map of price and size for the current market
*/
export const getCurrentMarketOrderbookMap = (state: RootState) => {
const orderbookMap = state.perpetuals.orderbooksMap;
const currentMarketId = getCurrentMarketId(state);
return orderbookMap?.[currentMarketId || ''];
};
/**
* @returns Record of historical funding indexed by marketId.
*/
export const getHistoricalFundings = (state: RootState) => state.perpetuals.historicalFundings;
/**
* @param state
* @returns Historical funding data for the market the user is currently viewing
*/
export const getCurrentMarketHistoricalFundings = createSelector(
@@ -110,7 +108,6 @@ export const getCurrentMarketHistoricalFundings = createSelector(
);
/**
* @param state
* @returns Mid market price for the market the user is currently viewing
*/
export const getCurrentMarketMidMarketPrice = (state: RootState) => {
@@ -150,7 +147,6 @@ export const getSelectedResolutionForMarket = (marketId: string) => (state: Root
state.perpetuals.candles?.[marketId]?.selectedResolution;
/**
* @param state
* @returns Current market's next funding rate
*/
export const getCurrentMarketNextFundingRate = createSelector(
-180
View File
@@ -1,180 +0,0 @@
:root {
/* Parameters */
/*
--layer-base-hue
--layer-base-saturation
--layer-base-lightness
--color-text-0
--color-text-1
--color-text-2
--color-accent
--color-positive
--color-negative
--color-success
--color-warning
--color-error
--color-favorite
*/
/* Computed: Layers */
--color-layer-0: hsl(
var(--layer-base-hue),
var(--layer-base-saturation),
calc(var(--layer-base-lightness) - 12%)
);
--color-layer-1: hsl(
var(--layer-base-hue),
var(--layer-base-saturation),
calc(var(--layer-base-lightness) - 8%)
);
--color-layer-2: hsl(
var(--layer-base-hue),
var(--layer-base-saturation),
calc(var(--layer-base-lightness) - 4%)
);
--color-layer-3: hsl(
var(--layer-base-hue),
var(--layer-base-saturation),
var(--layer-base-lightness)
);
--color-layer-4: hsl(
var(--layer-base-hue),
var(--layer-base-saturation),
calc(var(--layer-base-lightness) + 4%)
);
--color-layer-5: hsl(
var(--layer-base-hue),
calc(var(--layer-base-saturation) - 2%),
calc(var(--layer-base-lightness) + 7%)
);
--color-layer-6: hsl(
var(--layer-base-hue),
calc(var(--layer-base-saturation) - 4%),
calc(var(--layer-base-lightness) + 10%)
);
--color-layer-7: hsla(var(--layer-base-hue), 0%, 100%, 0.07);
--color-gradient-base-0: hsl(
var(--layer-base-hue),
calc(var(--layer-base-saturation) - 5%),
var(--layer-base-lightness)
);
--color-gradient-base-1: hsl(
var(--layer-base-hue),
calc(var(--layer-base-saturation) - 1%),
calc(var(--layer-base-lightness) + 7%)
);
/* Computed: Borders */
--color-border: var(--color-layer-5);
--color-border-white: hsla(0, 0%, 100%, 0.2);
--color-border-red: hsla(360, 73%, 61%, 0.2);
}
/* Theme: Classic (default) */
:root {
/* Constants */
--theme-classic-hue-purple: 240;
--theme-classic-color-green: hsl(159, 67%, 39%);
--theme-classic-color-yellow: hsl(36, 100%, 64%);
--theme-classic-color-red: hsl(360, 73%, 61%);
/* Parameters */
--layer-base-hue: var(--theme-classic-hue-purple);
--layer-base-saturation: 20%;
--layer-base-lightness: 16%;
--color-text-0: hsl(245, 11%, 55%);
--color-text-1: hsl(244, 18%, 81%);
--color-text-2: hsl(240, 43%, 99%);
--color-accent: hsl(var(--theme-classic-hue-purple), 100%, 70%);
--color-positive: var(--theme-classic-color-green);
--color-negative: var(--theme-classic-color-red);
--color-success: var(--theme-classic-color-green);
--color-warning: var(--theme-classic-color-yellow);
--color-error: var(--theme-classic-color-red);
--color-favorite: var(--theme-classic-color-yellow);
--color-white: #fff;
--color-black: #000;
--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);
--color-risk-high: var(--theme-classic-color-red);
}
/* Theme: Light */
:root.theme-light {
/* Constants */
--theme-light-hue-purple: 234;
--theme-light-color-green: hsl(159, 67%, 39%);
--theme-light-color-yellow: hsl(36, 100%, 64%);
--theme-light-color-red: hsl(360, 73%, 61%);
/* Parameters */
--layer-base-hue: var(--theme-light-hue-purple);
--layer-base-saturation: 0%;
--layer-base-lightness: 86%;
--color-text-0: hsl(0, 0%, 55%);
--color-text-1: hsl(0, 0%, 40%);
--color-text-2: hsl(0, 0%, 5%);
--color-accent: hsl(var(--theme-light-hue-purple), 100%, 70%);
--color-positive: var(--theme-light-color-green);
--color-negative: var(--theme-light-color-red);
--color-success: var(--theme-light-color-green);
--color-warning: var(--theme-light-color-yellow);
--color-error: var(--theme-light-color-red);
--color-favorite: var(--theme-light-color-yellow);
}
/* Theme: Dark */
:root.theme-dark {
/* Constants */
--theme-dark-hue-purple: 240;
--theme-dark-color-green: hsl(159, 67%, 39%);
--theme-dark-color-yellow: hsl(36, 100%, 64%);
--theme-dark-color-red: hsl(360, 73%, 61%);
/* Parameters */
--layer-base-hue: 0;
--layer-base-saturation: 0%;
--layer-base-lightness: 16%;
--color-text-0: hsl(0, 0%, 47%);
--color-text-1: hsl(0, 0%, 67%);
--color-text-2: hsl(0, 0%, 100%);
--color-accent: hsl(var(--theme-dark-hue-purple), 100%, 70%);
--color-positive: var(--theme-dark-color-green);
--color-negative: var(--theme-dark-color-red);
--color-success: var(--theme-dark-color-green);
--color-warning: var(--theme-dark-color-yellow);
--color-error: var(--theme-dark-color-red);
--color-favorite: var(--theme-dark-color-yellow);
}
-125
View File
@@ -1,125 +0,0 @@
import { css } from 'styled-components';
import { type RiskLevels } from '@/constants/abacus';
import { AppTheme } from '@/state/configs';
const BaseElements = {
[AppTheme.Classic]: {
hue: 240,
saturation: 20,
lightness: 16,
},
[AppTheme.Dark]: {
hue: 0,
saturation: 0,
lightness: 16,
},
[AppTheme.Light]: {
hue: 234,
saturation: 0,
lightness: 86,
},
};
const LayerColors = ({ theme }: { theme: AppTheme }) => {
const { hue, saturation, lightness } = BaseElements[theme];
return {
layer0: `hsl(${hue}, ${saturation}%, ${lightness - 12}%)`,
layer1: `hsl(${hue}, ${saturation}%, ${lightness - 8}%)`,
layer2: `hsl(${hue}, ${saturation}%, ${lightness - 4}%)`,
layer3: `hsl(${hue}, ${saturation}%, ${lightness}%)`,
layer4: `hsl(${hue}, ${saturation}%, ${lightness + 4}%)`,
layer5: `hsl(${hue}, ${saturation - 2}%, ${lightness + 7}%)`,
layer6: `hsl(${hue}, ${saturation - 4}%, ${lightness + 10}%)`,
};
};
const AccentColor = ({ theme }: { theme: AppTheme }) => {
const purpleHue = {
Classic: 240,
Dark: 240,
Light: 234,
}[theme];
return {
accent: `hsl(${purpleHue}, 100%, 70%)`,
};
};
export const Colors = {
[AppTheme.Classic]: {
...LayerColors({ theme: AppTheme.Classic }),
text0: 'hsl(245, 11%, 55%)',
text1: 'hsl(244, 18%, 81%)',
text2: 'hsl(240, 43%, 99%)',
...AccentColor({ theme: AppTheme.Classic }),
green: 'hsl(159, 67%, 39%)',
yellow: 'hsl(36, 100%, 64%)',
red: 'hsl(360, 73%, 61%)',
positive: 'hsl(159, 67%, 39%)',
negative: 'hsl(360, 73%, 61%)',
success: 'hsl(159, 67%, 39%)',
warning: 'hsl(36, 100%, 64%)',
error: 'hsl(360, 73%, 61%)',
favorite: 'hsl(36, 100%, 64%)',
},
[AppTheme.Dark]: {
...LayerColors({ theme: AppTheme.Dark }),
text0: 'hsl(0, 0%, 47%)',
text1: 'hsl(0, 0%, 67%)',
text2: 'hsl(0, 0%, 100%)',
...AccentColor({ theme: AppTheme.Dark }),
green: 'hsl(159, 67%, 39%)',
yellow: 'hsl(36, 100%, 64%)',
red: 'hsl(360, 73%, 61%)',
positive: 'hsl(159, 67%, 39%)',
negative: 'hsl(360, 73%, 61%)',
success: 'hsl(159, 67%, 39%)',
warning: 'hsl(36, 100%, 64%)',
error: 'hsl(360, 73%, 61%)',
favorite: 'hsl(36, 100%, 64%)',
},
[AppTheme.Light]: {
...LayerColors({ theme: AppTheme.Light }),
text0: 'hsl(0, 0%, 55%)',
text1: 'hsl(0, 0%, 40%)',
text2: 'hsl(0, 0%, 5%)',
...AccentColor({ theme: AppTheme.Light }),
green: 'hsl(159, 67%, 39%)',
yellow: 'hsl(36, 100%, 64%)',
red: 'hsl(360, 73%, 61%)',
positive: 'hsl(159, 67%, 39%)',
negative: 'hsl(360, 73%, 61%)',
success: 'hsl(159, 67%, 39%)',
warning: 'hsl(36, 100%, 64%)',
error: 'hsl(360, 73%, 61%)',
favorite: 'hsl(36, 100%, 64%)',
},
};
export const UsageColorFromRiskLevel = (riskLevel: RiskLevels) =>
({
low: css`
color: var(--color-risk-low);
`,
medium: css`
color: var(--color-risk-medium);
`,
high: css`
color: var(--color-risk-high);
`,
}[riskLevel.name]);
+1 -1
View File
@@ -21,7 +21,7 @@ export const formMixins: Record<
--input-radius: 0.5em;
--input-height: var(--form-input-height);
--input-width: 100%;
--input-backgroundColor: var(--color-layer-4);
--input-backgroundColor: ${({ theme }) => theme.inputBackground};
--input-borderColor: var(--color-layer-6);
${layoutMixins.row}
+42
View File
@@ -0,0 +1,42 @@
import { createGlobalStyle } from 'styled-components';
export const GlobalStyle = createGlobalStyle`
:root {
--color-layer-0: ${({ theme }) => theme.layer0};
--color-layer-1: ${({ theme }) => theme.layer1};
--color-layer-2: ${({ theme }) => theme.layer2};
--color-layer-3: ${({ theme }) => theme.layer3};
--color-layer-4: ${({ theme }) => theme.layer4};
--color-layer-5: ${({ theme }) => theme.layer5};
--color-layer-6: ${({ theme }) => theme.layer6};
--color-layer-7: ${({ theme }) => theme.layer7};
--color-border: ${({ theme }) => theme.borderDefault};
--color-border-white: ${({ theme }) => theme.borderButton};
--color-border-red: ${({ theme }) => theme.borderDestructive};
--color-text-0: ${({ theme }) => theme.textTertiary};
--color-text-1: ${({ theme }) => theme.textSecondary};
--color-text-2: ${({ theme }) => theme.textPrimary};
--color-gradient-base-0: ${({ theme }) => theme.gradientBase0};
--color-gradient-base-1: ${({ theme }) => theme.gradientBase1};
--color-accent: ${({ theme }) => theme.accent};
--color-accent-faded: ${({ theme }) => theme.accentFaded};
--color-favorite: ${({ theme }) => theme.favorite};
--color-success: ${({ theme }) => theme.success};
--color-warning: ${({ theme }) => theme.warning};
--color-error: ${({ theme }) => theme.error};
--color-positive: ${({ theme }) => theme.positive};
--color-negative: ${({ theme }) => theme.negative};
--color-gradient-positive: ${({ theme }) => theme.positiveFaded};
--color-gradient-negative: ${({ theme }) => theme.negativeFaded};
--color-risk-low: ${({ theme }) => theme.riskLow};
--color-risk-medium: ${({ theme }) => theme.riskMedium};
--color-risk-high: ${({ theme }) => theme.riskHigh};
}
`;
+157
View File
@@ -0,0 +1,157 @@
import { AppTheme } from '@/state/configs';
import type { ThemeColors } from '@/constants/styles/colors';
import { ColorToken, OpacityToken } from '@/constants/styles/base';
import { generateFadedColorVariant } from '@/lib/styles';
const ClassicTheme: ThemeColors = {
layer0: ColorToken.GrayBlue7,
layer1: ColorToken.GrayBlue6,
layer2: ColorToken.GrayBlue5,
layer3: ColorToken.GrayBlue4,
layer4: ColorToken.GrayBlue3,
layer5: ColorToken.GrayBlue2,
layer6: ColorToken.GrayBlue1,
layer7: ColorToken.GrayBlue0,
borderDefault: ColorToken.GrayBlue2,
borderDestructive: generateFadedColorVariant(ColorToken.Red2, OpacityToken.Opacity20),
borderButton: generateFadedColorVariant(ColorToken.White, OpacityToken.Opacity20),
textPrimary: ColorToken.LightGray2,
textSecondary: ColorToken.GrayPurple1,
textTertiary: ColorToken.GrayPurple2,
gradientBase0: ColorToken.DarkGray9,
gradientBase1: ColorToken.GrayBlue2,
accent: ColorToken.Purple1,
accentFaded: generateFadedColorVariant(ColorToken.Purple1, OpacityToken.Opacity16),
favorite: ColorToken.Yellow0,
success: ColorToken.Green1,
warning: ColorToken.Yellow0,
error: ColorToken.Red2,
positive: ColorToken.Green1,
negative: ColorToken.Red2,
positiveFaded: generateFadedColorVariant(ColorToken.Green1, OpacityToken.Opacity16),
negativeFaded: generateFadedColorVariant(ColorToken.Red2, OpacityToken.Opacity16),
riskLow: ColorToken.Green1,
riskMedium: ColorToken.Yellow0,
riskHigh: ColorToken.Red2,
logoFill: ColorToken.White,
profileYellow: ColorToken.Yellow1,
profileRed: ColorToken.Red2,
inputBackground: ColorToken.GrayBlue3,
popoverBackground: generateFadedColorVariant(ColorToken.GrayBlue4, OpacityToken.Opacity90),
switchThumbActiveBackground: ColorToken.White,
toggleBackground: ColorToken.GrayBlue3,
tooltipBackground: generateFadedColorVariant(ColorToken.GrayBlue3, OpacityToken.Opacity66),
};
const DarkTheme: ThemeColors = {
layer0: ColorToken.Black,
layer1: ColorToken.DarkGray11,
layer2: ColorToken.DarkGray13,
layer3: ColorToken.DarkGray10,
layer4: ColorToken.DarkGray6,
layer5: ColorToken.DarkGray5,
layer6: ColorToken.DarkGray4,
layer7: ColorToken.DarkGray2,
borderDefault: ColorToken.DarkGray4,
borderDestructive: generateFadedColorVariant(ColorToken.Red0, OpacityToken.Opacity20),
borderButton: generateFadedColorVariant(ColorToken.White, OpacityToken.Opacity20),
textPrimary: ColorToken.LightGray0,
textSecondary: ColorToken.MediumGray0,
textTertiary: ColorToken.DarkGray0,
gradientBase0: ColorToken.DarkGray8,
gradientBase1: ColorToken.DarkGray5,
accent: ColorToken.Purple0,
accentFaded: generateFadedColorVariant(ColorToken.Purple0, OpacityToken.Opacity16),
favorite: ColorToken.Yellow0,
success: ColorToken.Green0,
warning: ColorToken.Yellow0,
error: ColorToken.Red0,
positive: ColorToken.Green0,
negative: ColorToken.Red0,
positiveFaded: generateFadedColorVariant(ColorToken.Green0, OpacityToken.Opacity16),
negativeFaded: generateFadedColorVariant(ColorToken.Red0, OpacityToken.Opacity16),
riskLow: ColorToken.Green0,
riskMedium: ColorToken.Yellow0,
riskHigh: ColorToken.Red0,
logoFill: ColorToken.White,
profileYellow: ColorToken.Yellow1,
profileRed: ColorToken.Red2,
inputBackground: ColorToken.DarkGray6,
popoverBackground: generateFadedColorVariant(ColorToken.DarkGray8, OpacityToken.Opacity90),
switchThumbActiveBackground: ColorToken.White,
toggleBackground: ColorToken.DarkGray6,
tooltipBackground: generateFadedColorVariant(ColorToken.DarkGray6, OpacityToken.Opacity66),
};
const LightTheme: ThemeColors = {
layer0: ColorToken.White,
layer1: ColorToken.LightGray6,
layer2: ColorToken.White,
layer3: ColorToken.LightGray1,
layer4: ColorToken.White,
layer5: ColorToken.LightGray4,
layer6: ColorToken.LightGray9,
layer7: ColorToken.MediumGray1,
borderDefault: ColorToken.LightGray10,
borderDestructive: generateFadedColorVariant(ColorToken.Red1, OpacityToken.Opacity20),
borderButton: generateFadedColorVariant(ColorToken.Black, OpacityToken.Opacity20),
textPrimary: ColorToken.DarkGray12,
textSecondary: ColorToken.DarkGray3,
textTertiary: ColorToken.DarkGray1,
gradientBase0: ColorToken.LightGray8,
gradientBase1: ColorToken.LightGray5,
accent: ColorToken.Purple0,
accentFaded: generateFadedColorVariant(ColorToken.Purple0, OpacityToken.Opacity16),
favorite: ColorToken.Yellow0,
success: ColorToken.Green2,
warning: ColorToken.Yellow0,
error: ColorToken.Red1,
positive: ColorToken.Green2,
negative: ColorToken.Red1,
positiveFaded: generateFadedColorVariant(ColorToken.Green2, OpacityToken.Opacity16),
negativeFaded: generateFadedColorVariant(ColorToken.Red1, OpacityToken.Opacity16),
riskLow: ColorToken.Green2,
riskMedium: ColorToken.Yellow0,
riskHigh: ColorToken.Red1,
logoFill: ColorToken.Black,
profileYellow: ColorToken.Yellow1,
profileRed: ColorToken.Red2,
inputBackground: ColorToken.White,
popoverBackground: generateFadedColorVariant(ColorToken.LightGray8, OpacityToken.Opacity90),
switchThumbActiveBackground: ColorToken.White,
toggleBackground: ColorToken.LightGray4,
tooltipBackground: generateFadedColorVariant(ColorToken.LightGray7, OpacityToken.Opacity66),
};
export const Themes = {
[AppTheme.Classic]: ClassicTheme,
[AppTheme.Dark]: DarkTheme,
[AppTheme.Light]: LightTheme,
};
@@ -0,0 +1,81 @@
import { forwardRef } from 'react';
import styled, { AnyStyledComponent, css } from 'styled-components';
import type { Nullable } from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization';
import { TOKEN_DECIMALS } from '@/constants/numbers';
import { ORDERBOOK_ROW_HEIGHT } from '@/constants/orderbook';
import { useStringGetter } from '@/hooks';
import { Output, OutputType } from '@/components/Output';
import { WithTooltip } from '@/components/WithTooltip';
type StyleProps = {
side?: 'top' | 'bottom';
};
type ElementProps = {
spread?: Nullable<number>;
spreadPercent?: Nullable<number>;
tickSizeDecimals?: Nullable<number>;
};
export const OrderbookRow = styled.div`
display: flex;
flex-shrink: 0;
align-items: center;
height: ${ORDERBOOK_ROW_HEIGHT}px;
min-height: ${ORDERBOOK_ROW_HEIGHT}px;
position: relative;
padding-right: 0.5rem;
font: var(--font-mini-book);
> span {
flex: 1 1 0%;
text-align: right;
}
`;
export const SpreadRow = forwardRef<HTMLDivElement, StyleProps & ElementProps>(
({ side, spread, spreadPercent, tickSizeDecimals = TOKEN_DECIMALS }, ref) => {
const stringGetter = useStringGetter();
return (
<Styled.SpreadRow ref={ref} side={side}>
<span>
<WithTooltip tooltip="spread">
{stringGetter({ key: STRING_KEYS.ORDERBOOK_SPREAD })}
</WithTooltip>
</span>
<span>
<Output type={OutputType.Number} value={spread} fractionDigits={tickSizeDecimals} />
</span>
<span>
<Output type={OutputType.Percent} value={spreadPercent} />
</span>
</Styled.SpreadRow>
);
}
);
const Styled: Record<string, AnyStyledComponent> = {};
Styled.SpreadRow = styled(OrderbookRow)<{ side?: 'top' | 'bottom' }>`
height: 2rem;
border-top: var(--border);
border-bottom: var(--border);
span {
margin-bottom: 2px;
}
${({ side }) =>
side &&
{
top: css`
border-top: none;
`,
bottom: css`
border-bottom: none;
`,
}[side]}
`;
+284
View File
@@ -0,0 +1,284 @@
import { forwardRef, useCallback, useMemo, useRef } from 'react';
import styled, { AnyStyledComponent, css } from 'styled-components';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { Nullable, type PerpetualMarketOrderbookLevel } from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization';
import { ORDERBOOK_HEIGHT, ORDERBOOK_MAX_ROWS_PER_SIDE } from '@/constants/orderbook';
import { useStringGetter } from '@/hooks';
import {
useCalculateOrderbookData,
useCenterOrderbook,
useDrawOrderbook,
useSpreadRowScrollListener,
} from '@/hooks/Orderbook';
import { Canvas } from '@/components/Canvas';
import { LoadingSpace } from '@/components/Loading/LoadingSpinner';
import { Tag } from '@/components/Tag';
import { getCurrentMarketAssetData } from '@/state/assetsSelectors';
import { getCurrentMarketConfig, getCurrentMarketId } from '@/state/perpetualsSelectors';
import { setTradeFormInputs } from '@/state/inputs';
import { getCurrentInput } from '@/state/inputsSelectors';
import { OrderbookRow, SpreadRow } from './OrderbookRow';
type ElementProps = {
maxRowsPerSide?: number;
layout?: 'vertical' | 'horizontal';
};
type StyleProps = {
hideHeader?: boolean;
histogramSide?: 'left' | 'right';
className?: string;
};
export const CanvasOrderbook = forwardRef(
(
{
histogramSide = 'right',
maxRowsPerSide = ORDERBOOK_MAX_ROWS_PER_SIDE,
}: ElementProps & StyleProps,
ref
) => {
const { asks, bids, hasOrderbook, histogramRange, spread, spreadPercent } =
useCalculateOrderbookData({
maxRowsPerSide,
});
const stringGetter = useStringGetter();
const currentMarket = useSelector(getCurrentMarketId) ?? '';
const currentMarketConfig = useSelector(getCurrentMarketConfig, shallowEqual);
const { id = '' } = useSelector(getCurrentMarketAssetData, shallowEqual) ?? {};
const { tickSizeDecimals = 2 } = currentMarketConfig ?? {};
/**
* Slice asks and bids to maxRowsPerSide using empty rows
*/
const { asksSlice, bidsSlice } = useMemo(() => {
let asksSlice: Array<PerpetualMarketOrderbookLevel | undefined> = [];
const emptyAskRows =
asks.length < maxRowsPerSide
? new Array<undefined>(maxRowsPerSide - asks.length).fill(undefined)
: [];
asksSlice = [...emptyAskRows, ...asks.reverse()];
let bidsSlice: Array<PerpetualMarketOrderbookLevel | undefined> = [];
const emptyBidRows =
bids.length < maxRowsPerSide
? new Array<undefined>(maxRowsPerSide - bids.length).fill(undefined)
: [];
bidsSlice = [...bids, ...emptyBidRows];
return {
asksSlice,
bidsSlice,
};
}, [asks, bids]);
const orderbookRef = useRef<HTMLDivElement>(null);
useCenterOrderbook({ orderbookRef, marketId: currentMarket });
/**
* Display top or bottom spreadRow when center spreadRow is off screen
*/
const spreadRowRef = useRef<HTMLDivElement>(null);
const displaySide = useSpreadRowScrollListener({
orderbookRef,
spreadRowRef,
});
/**
* Row action
*/
const currentInput = useSelector(getCurrentInput);
const dispatch = useDispatch();
const onRowAction = useCallback(
(price: Nullable<number>) => {
if (currentInput === 'trade' && price) {
dispatch(setTradeFormInputs({ limitPriceInput: price?.toString() }));
}
},
[currentInput]
);
const { canvasRef: asksCanvasRef } = useDrawOrderbook({
data: [...asksSlice].reverse(),
histogramRange,
histogramSide,
side: 'ask',
});
const { canvasRef: bidsCanvasRef } = useDrawOrderbook({
data: bidsSlice,
histogramRange,
histogramSide,
side: 'bid',
});
return (
<Styled.OrderbookContainer ref={ref}>
<Styled.OrderbookContent $isLoading={!hasOrderbook}>
<Styled.Header>
<span>
{stringGetter({ key: STRING_KEYS.SIZE })} {id && <Tag>{id}</Tag>}
</span>
<span>
{stringGetter({ key: STRING_KEYS.PRICE })} <Tag>USD</Tag>
</span>
<span>{stringGetter({ key: STRING_KEYS.MINE })}</span>
</Styled.Header>
{displaySide === 'top' && (
<Styled.SpreadRow
side="top"
spread={spread}
spreadPercent={spreadPercent}
tickSizeDecimals={tickSizeDecimals}
/>
)}
<Styled.OrderbookWrapper ref={orderbookRef}>
<Styled.OrderbookSideContainer $side="asks">
<Styled.HoverRows $bottom>
{asksSlice.map((row: PerpetualMarketOrderbookLevel | undefined, idx) =>
row ? (
<Styled.Row
key={idx}
title={`${row.price}`}
onClick={() => {
onRowAction(row.price);
}}
/>
) : (
<Styled.Row key={idx} />
)
)}
</Styled.HoverRows>
<Styled.OrderbookCanvas ref={asksCanvasRef} width="100%" height="100%" />
</Styled.OrderbookSideContainer>
<SpreadRow
ref={spreadRowRef}
spread={spread?.toNumber()}
spreadPercent={spreadPercent}
tickSizeDecimals={tickSizeDecimals}
/>
<Styled.OrderbookSideContainer $side="bids">
<Styled.HoverRows>
{bidsSlice.map((row: PerpetualMarketOrderbookLevel | undefined, idx) =>
row ? (
<Styled.Row
key={idx}
title={`${row.price}`}
onClick={
row?.price
? () => {
onRowAction(row.price);
}
: undefined
}
/>
) : (
<Styled.Row key={idx} />
)
)}
</Styled.HoverRows>
<Styled.OrderbookCanvas ref={bidsCanvasRef} width="100%" height="100%" />
</Styled.OrderbookSideContainer>
</Styled.OrderbookWrapper>
{displaySide === 'bottom' && (
<Styled.SpreadRow
side="bottom"
spread={spread}
spreadPercent={spreadPercent}
tickSizeDecimals={tickSizeDecimals}
/>
)}
</Styled.OrderbookContent>
{!hasOrderbook && <LoadingSpace id="canvas-orderbook" />}
</Styled.OrderbookContainer>
);
}
);
const Styled: Record<string, AnyStyledComponent> = {};
Styled.OrderbookContainer = styled.div`
display: flex;
flex: 1 1 0%;
flex-direction: column;
overflow: hidden;
`;
Styled.OrderbookContent = styled.div<{ $isLoading?: boolean }>`
max-height: 100%;
display: flex;
flex-direction: column;
position: relative;
${({ $isLoading }) => $isLoading && 'flex: 1;'}
`;
Styled.Header = styled(OrderbookRow)`
height: 2rem;
border-bottom: var(--border);
color: var(--color-text-0);
`;
Styled.OrderbookWrapper = styled.div`
overflow-y: auto;
display: flex;
flex-direction: column;
flex: 1 1 0%;
`;
Styled.OrderbookSideContainer = styled.div<{ $side: 'bids' | 'asks' }>`
min-height: ${ORDERBOOK_HEIGHT}px;
${({ $side }) => css`
--accent-color: ${$side === 'bids' ? 'var(--color-positive)' : 'var(--color-negative)'};
`}
position: relative;
`;
Styled.OrderbookCanvas = styled(Canvas)`
width: 100%;
height: 100%;
position: absolute;
top: 0;
right: 0;
pointer-events: none;
font-feature-settings: var(--fontFeature-monoNumbers);
`;
Styled.HoverRows = styled.div<{ $bottom?: boolean }>`
position: absolute;
width: 100%;
${({ $bottom }) => $bottom && 'bottom: 0;'}
`;
Styled.Row = styled(OrderbookRow)<{ onClick?: () => void }>`
${({ onClick }) =>
onClick
? css`
cursor: pointer;
&:hover {
background-color: var(--color-layer-4);
filter: darkness(0.1);
}
`
: css`
cursor: default;
`}
`;
Styled.SpreadRow = styled(SpreadRow)`
position: absolute;
`;
+1 -1
View File
@@ -13,7 +13,7 @@ import {
import { STRING_KEYS } from '@/constants/localization';
import { useStringGetter } from '@/hooks';
import { useOrderbookValuesForDepthChart } from '@/hooks/useOrderbookValues';
import { useOrderbookValuesForDepthChart } from '@/hooks/Orderbook/useOrderbookValues';
import { TooltipContent } from '@/components/visx/TooltipContent';
import { Details } from '@/components/Details';
+3 -13
View File
@@ -12,7 +12,7 @@ import {
import { StringGetterFunction } from '@/constants/localization';
import { useBreakpoints } from '@/hooks';
import { useOrderbookValuesForDepthChart } from '@/hooks/useOrderbookValues';
import { useOrderbookValuesForDepthChart } from '@/hooks/Orderbook/useOrderbookValues';
import { getCurrentMarketConfig } from '@/state/perpetualsSelectors';
import { getCurrentMarketAssetData } from '@/state/assetsSelectors';
@@ -83,18 +83,8 @@ export const DepthChart = ({
const { stepSizeDecimals, tickSizeDecimals } =
useSelector(getCurrentMarketConfig, shallowEqual) ?? {};
const {
bids,
asks,
lowestBid,
highestBid,
lowestAsk,
highestAsk,
midMarketPrice,
spread,
spreadPercent,
orderbook,
} = useOrderbookValuesForDepthChart();
const { bids, asks, lowestBid, highestBid, lowestAsk, highestAsk, midMarketPrice, orderbook } =
useOrderbookValuesForDepthChart();
// Chart state
@@ -17,7 +17,6 @@ import type { EvmAddress } from '@/constants/wallets';
import { useAccounts, useDebounce, useStringGetter, useSelectedNetwork } from '@/hooks';
import { useAccountBalance, CHAIN_DEFAULT_TOKEN_ADDRESS } from '@/hooks/useAccountBalance';
import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { useWalletConnection } from '@/hooks/useWalletConnection';
import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins';
@@ -41,10 +40,11 @@ import { getNobleChainId, NATIVE_TOKEN_ADDRESS } from '@/lib/squid';
import { log } from '@/lib/telemetry';
import { parseWalletError } from '@/lib/wallet';
import { ChainSelectMenu } from './ChainSelectMenu';
import { SourceSelectMenu } from './SourceSelectMenu';
import { TokenSelectMenu } from './TokenSelectMenu';
import { DepositButtonAndReceipt } from './DepositForm/DepositButtonAndReceipt';
import { NobleDeposit } from '../NobleDeposit';
type DepositFormProps = {
onDeposit?: () => void;
@@ -57,13 +57,14 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const [isLoading, setIsLoading] = useState(false);
const { selectedNetwork } = useSelectedNetwork();
const { evmAddress, signerWagmi, publicClientWagmi } = useAccounts();
const { evmAddress, signerWagmi, publicClientWagmi, nobleAddress } = useAccounts();
const { addTransferNotification } = useLocalNotifications();
const {
requestPayload,
token,
exchange,
chain: chainIdStr,
resources,
summary,
@@ -129,14 +130,21 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
if (error) onError?.();
}, [error]);
const onSelectChain = useCallback((chain: string) => {
if (chain) {
const onSelectChain = useCallback((name: string, type: 'chain' | 'exchange') => {
if (name) {
abacusStateManager.clearTransferInputValues();
abacusStateManager.setTransferValue({
field: TransferInputField.chain,
value: chain,
});
setFromAmount('');
if (type === 'chain') {
abacusStateManager.setTransferValue({
field: TransferInputField.chain,
value: name,
});
} else {
abacusStateManager.setTransferValue({
field: TransferInputField.exchange,
value: name,
});
}
}
}, []);
@@ -369,39 +377,49 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
return (
<Styled.Form onSubmit={onSubmit}>
<ChainSelectMenu selectedChain={chainIdStr || undefined} onSelectChain={onSelectChain} />
<TokenSelectMenu selectedToken={sourceToken || undefined} onSelectToken={onSelectToken} />
<Styled.WithDetailsReceipt side="bottom" detailItems={amountInputReceipt}>
<FormInput
type={InputType.Number}
onChange={onChangeAmount}
label={stringGetter({ key: STRING_KEYS.AMOUNT })}
value={fromAmount}
slotRight={
<Styled.FormInputButton size={ButtonSize.XSmall} onClick={onClickMax}>
{stringGetter({ key: STRING_KEYS.MAX })}
</Styled.FormInputButton>
}
/>
</Styled.WithDetailsReceipt>
{errorMessage && <AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>}
{requireUserActionInWallet && (
<AlertMessage type={AlertType.Warning}>
{stringGetter({ key: STRING_KEYS.CHECK_WALLET_FOR_REQUEST })}
</AlertMessage>
<SourceSelectMenu
selectedChain={chainIdStr || undefined}
selectedExchange={exchange || undefined}
onSelect={onSelectChain}
/>
{exchange && nobleAddress ? (
<NobleDeposit />
) : (
<>
<TokenSelectMenu selectedToken={sourceToken || undefined} onSelectToken={onSelectToken} />
<Styled.WithDetailsReceipt side="bottom" detailItems={amountInputReceipt}>
<FormInput
type={InputType.Number}
onChange={onChangeAmount}
label={stringGetter({ key: STRING_KEYS.AMOUNT })}
value={fromAmount}
slotRight={
<Styled.FormInputButton size={ButtonSize.XSmall} onClick={onClickMax}>
{stringGetter({ key: STRING_KEYS.MAX })}
</Styled.FormInputButton>
}
/>
</Styled.WithDetailsReceipt>
{errorMessage && <AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>}
{requireUserActionInWallet && (
<AlertMessage type={AlertType.Warning}>
{stringGetter({ key: STRING_KEYS.CHECK_WALLET_FOR_REQUEST })}
</AlertMessage>
)}
<Styled.Footer>
<DepositButtonAndReceipt
isDisabled={isDisabled}
isLoading={isLoading}
chainId={chainId || undefined}
setSlippage={onSetSlippage}
slippage={slippage}
sourceToken={sourceToken || undefined}
setRequireUserActionInWallet={setRequireUserActionInWallet}
setError={setError}
/>
</Styled.Footer>
</>
)}
<Styled.Footer>
<DepositButtonAndReceipt
isDisabled={isDisabled}
isLoading={isLoading}
chainId={chainId || undefined}
setSlippage={onSetSlippage}
slippage={slippage}
sourceToken={sourceToken || undefined}
setRequireUserActionInWallet={setRequireUserActionInWallet}
setError={setError}
/>
</Styled.Footer>
</Styled.Form>
);
};
@@ -3,6 +3,7 @@ import { shallowEqual, useSelector } from 'react-redux';
import { TransferType } from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization';
import { useStringGetter } from '@/hooks';
import { SearchSelectMenu } from '@/components/SearchSelectMenu';
@@ -12,45 +13,77 @@ import { popoverMixins } from '@/styles/popoverMixins';
import { getTransferInputs } from '@/state/inputsSelectors';
import { isTruthy } from '@/lib/isTruthy';
type ElementProps = {
label?: string;
selectedExchange?: string;
selectedChain?: string;
onSelectChain: (chain: string) => void;
onSelect: (name: string, type: 'chain' | 'exchange') => void;
};
export const ChainSelectMenu = ({ label, selectedChain, onSelectChain }: ElementProps) => {
export const SourceSelectMenu = ({
label,
selectedExchange,
selectedChain,
onSelect,
}: ElementProps) => {
const stringGetter = useStringGetter();
const { type, depositOptions, withdrawalOptions, resources } =
useSelector(getTransferInputs, shallowEqual) || {};
const chains =
(type === TransferType.deposit ? depositOptions : withdrawalOptions)?.chains?.toArray() || [];
const exchanges =
(type === TransferType.deposit ? depositOptions : withdrawalOptions)?.exchanges?.toArray() ||
[];
const chainItems = Object.values(chains).map((chain) => ({
value: chain.type,
label: chain.stringKey,
onSelect: () => {
onSelectChain(chain.type);
onSelect(chain.type, 'chain');
},
slotBefore: <Styled.Img src={chain.iconUrl} alt="" />,
}));
const selectedOption = chains.find((item) => item.type === selectedChain);
const exchangeItems = Object.values(exchanges).map((exchange) => ({
value: exchange.type,
label: exchange.string,
onSelect: () => {
onSelect(exchange.type, 'exchange');
},
slotBefore: <Styled.Img src={exchange.iconUrl} alt="" />,
}));
const selectedChainOption = chains.find((item) => item.type === selectedChain);
const selectedExchangeOption = exchanges.find((item) => item.type === selectedExchange);
return (
<SearchSelectMenu
items={[
{
exchangeItems.length > 0 && {
group: 'exchanges',
groupLabel: stringGetter({ key: STRING_KEYS.EXCHANGES }),
items: exchangeItems,
},
chainItems.length > 0 && {
group: 'chains',
groupLabel: stringGetter({ key: STRING_KEYS.CHAINS }),
items: chainItems,
},
]}
].filter(isTruthy)}
label={label || (type === TransferType.deposit ? 'Source' : 'Destination')}
>
<Styled.ChainRow>
{selectedChain ? (
{selectedChainOption ? (
<>
<Styled.Img src={selectedOption?.iconUrl} alt="" /> {selectedOption?.stringKey}
<Styled.Img src={selectedChainOption.iconUrl} alt="" /> {selectedChainOption.stringKey}
</>
) : selectedExchangeOption ? (
<>
<Styled.Img src={selectedExchangeOption.iconUrl} alt="" />{' '}
{selectedExchangeOption.string}
</>
) : (
stringGetter({ key: STRING_KEYS.SELECT_CHAIN })
@@ -38,7 +38,7 @@ import { Tag } from '@/components/Tag';
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
import { Icon, IconName } from '@/components/Icon';
import { ChainSelectMenu } from '@/views/forms/AccountManagementForms/ChainSelectMenu';
import { SourceSelectMenu } from '@/views/forms/AccountManagementForms/SourceSelectMenu';
import { getSubaccount } from '@/state/accountSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
@@ -374,10 +374,10 @@ export const WithdrawForm = () => {
</span>
}
/>
<ChainSelectMenu
<SourceSelectMenu
label={stringGetter({ key: STRING_KEYS.NETWORK })}
selectedChain={chainIdStr || undefined}
onSelectChain={onSelectChain}
onSelect={onSelectChain}
/>
</Styled.DestinationRow>
<TokenSelectMenu selectedToken={toToken || undefined} onSelectToken={onSelectToken} />
+131
View File
@@ -0,0 +1,131 @@
import { useState } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { OpacityToken } from '@/constants/styles/base';
import { STRING_KEYS } from '@/constants/localization';
import { layoutMixins } from '@/styles/layoutMixins';
import { useAccounts, useStringGetter } from '@/hooks';
import { CopyButton } from '@/components/CopyButton';
import { QrCode } from '@/components/QrCode';
import { Checkbox } from '@/components/Checkbox';
import { Icon, IconName } from '@/components/Icon';
import { TimeoutButton } from '@/components/TimeoutButton';
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
import { WithReceipt } from '@/components/WithReceipt';
import { generateFadedColorVariant } from '@/lib/styles';
export const NobleDeposit = () => {
const [hasAcknowledged, setHasAcknowledged] = useState(false);
const stringGetter = useStringGetter();
const { nobleAddress } = useAccounts();
return (
<>
<WithDetailsReceipt
side="bottom"
detailItems={[
{
key: 'nobleAddress',
label: stringGetter({ key: STRING_KEYS.NOBLE_ADDRESS }),
value: nobleAddress,
},
]}
>
<Styled.QrCodeContainer>
<Styled.QrCode size={432} value={nobleAddress || ''} />
</Styled.QrCodeContainer>
</WithDetailsReceipt>
<Styled.WaitingSpan>
<Styled.CautionIconContainer>
<Icon iconName={IconName.CautionCircleStroked} />
</Styled.CautionIconContainer>
<p>{stringGetter({ key: STRING_KEYS.NOBLE_WARNING })}</p>
</Styled.WaitingSpan>
<Styled.WithReceipt
slotReceipt={
<Styled.CheckboxContainer>
<Checkbox
checked={hasAcknowledged}
onCheckedChange={setHasAcknowledged}
id="acknowledge-secret-phase-risk"
label={stringGetter({
key: STRING_KEYS.NOBLE_ACKNOWLEDGEMENT,
})}
/>
</Styled.CheckboxContainer>
}
>
<TimeoutButton
timeoutInSeconds={8}
slotFinal={<CopyButton state={{ isDisabled: !hasAcknowledged }} value={nobleAddress} />}
/>
</Styled.WithReceipt>
</>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.WaitingSpan = styled.span`
${layoutMixins.row}
gap: 1rem;
color: var(--color-text-1);
`;
Styled.WithReceipt = styled(WithReceipt)`
--withReceipt-backgroundColor: var(--color-layer-2);
`;
Styled.QrCodeContainer = styled.div`
display: flex;
justify-content: center;
padding: 0.5rem;
background-color: var(--color-layer-2);
border-radius: 0.5rem;
`;
Styled.QrCode = styled(QrCode)`
max-height: 20rem;
width: fit-content;
svg {
max-height: 20rem;
}
`;
Styled.CheckboxContainer = styled.div`
padding: 1rem;
color: var(--color-text-0);
`;
Styled.CautionIconContainer = styled.div`
${layoutMixins.stack}
min-width: 2.5rem;
height: 2.5rem;
align-items: center;
border-radius: 50%;
overflow: hidden;
color: var(--color-warning);
svg {
width: 1.125em;
height: 1.125em;
justify-self: center;
}
&:before {
content: '';
width: 2.5rem;
height: 2.5rem;
background-color: ${({ theme }) =>
generateFadedColorVariant(theme.warning, OpacityToken.Opacity16)};
}
`;
+3 -2
View File
@@ -10,7 +10,6 @@ import {
type HumanReadablePlaceOrderPayload,
type Nullable,
TradeInputErrorAction,
TradeInputField,
ValidationError,
} from '@/constants/abacus';
@@ -96,6 +95,8 @@ export const TradeForm = ({
needsGoodUntil,
needsPostOnly,
needsReduceOnly,
postOnlyTooltip,
reduceOnlyTooltip,
timeInForceOptions,
tradeErrors,
} = useTradeFormData();
@@ -108,7 +109,7 @@ export const TradeForm = ({
const { limitPriceInput, triggerPriceInput, trailingPercentInput } = tradeFormInputValues;
const needsAdvancedOptions =
needsGoodUntil || timeInForceOptions || executionOptions || needsPostOnly || needsReduceOnly;
needsGoodUntil || timeInForceOptions || executionOptions || (needsPostOnly || postOnlyTooltip) || (needsReduceOnly || reduceOnlyTooltip);
const tradeFormInputs: TradeBoxInputConfig[] = [];
@@ -31,12 +31,15 @@ export const AdvancedTradeOptions = () => {
const { execution, goodTil, postOnly, reduceOnly, timeInForce, type } = inputTradeData || {};
const { executionOptions, needsGoodUntil, needsPostOnly, needsReduceOnly, timeInForceOptions } =
const { executionOptions, needsGoodUntil, needsPostOnly, needsReduceOnly, postOnlyTooltip, reduceOnlyTooltip, timeInForceOptions } =
currentTradeFormConfig || {};
const { duration, unit } = goodTil || {};
const needsExecution = executionOptions || needsPostOnly || needsReduceOnly;
const showPostOnly = (needsPostOnly || postOnlyTooltip);
const showReduceOnly = (needsReduceOnly || reduceOnlyTooltip);
const needsExecution = executionOptions || showPostOnly || showReduceOnly;
const hasTimeInForce = timeInForceOptions?.toArray()?.length;
return (
@@ -129,26 +132,9 @@ export const AdvancedTradeOptions = () => {
))}
</Styled.SelectMenu>
)}
{needsPostOnly && (
<Checkbox
checked={postOnly || false}
onCheckedChange={(checked) =>
abacusStateManager.setTradeValue({
value: checked,
field: TradeInputField.postOnly,
})
}
id="post-only"
label={
<WithTooltip tooltip="post-only" side="right">
{stringGetter({ key: STRING_KEYS.POST_ONLY })}
</WithTooltip>
}
/>
)}
{needsReduceOnly && (
<Checkbox
checked={reduceOnly || false}
{showReduceOnly && <Checkbox
checked={(reduceOnly && !reduceOnlyTooltip) || false}
disabled={!!reduceOnlyTooltip}
onCheckedChange={(checked) =>
abacusStateManager.setTradeValue({
value: checked,
@@ -157,11 +143,29 @@ export const AdvancedTradeOptions = () => {
}
id="reduce-only"
label={
<WithTooltip tooltip="reduce-only" side="right">
<WithTooltip tooltip={needsReduceOnly ? "reduce-only" : reduceOnlyTooltip?.titleStringKey.includes("REDUCE_ONLY_EXECUTION_IOC_FOK") ? "reduce-only-execution-ioc-fok" : "reduce-only-timeinforce-ioc-fok"} side="right">
{stringGetter({ key: STRING_KEYS.REDUCE_ONLY })}
</WithTooltip>
}
/>
}
{showPostOnly && (
<Checkbox
checked={(postOnly && !postOnlyTooltip) || false}
disabled={!!postOnlyTooltip}
onCheckedChange={(checked) =>
abacusStateManager.setTradeValue({
value: checked,
field: TradeInputField.postOnly,
})
}
id="post-only"
label={
<WithTooltip tooltip={needsPostOnly ? "post-only" : "post-only-timeinforce-gtt"} side="right">
{stringGetter({ key: STRING_KEYS.POST_ONLY })}
</WithTooltip>
}
/>
)}
</>
)}
@@ -44,6 +44,7 @@ export const TradeSideToggle = memo(() => {
const ToggleContainer = styled(ToggleGroup)<{ value: OrderSide }>`
--toggle-radius: 0.5em;
--toggle-color: var(--color-negative);
--toggle-background: ${({ theme }) => theme.toggleBackground};
${({ value }) =>
value === OrderSide.BUY &&
@@ -52,7 +53,7 @@ const ToggleContainer = styled(ToggleGroup)<{ value: OrderSide }>`
`}
border-radius: var(--toggle-radius);
background-color: var(--color-layer-4);
background-color: var(--toggle-background);
position: relative;
> button {
+1
View File
@@ -163,6 +163,7 @@ Styled.SizeOutput = styled(Output)<StyleProps>`
Styled.LiveTradesTable = styled(OrderbookTradesTable)<StyleProps>`
tr {
--histogram-bucket-size: 1;
background-color: var(--color-layer-2);
&[data-side=${OrderSide.BUY}] {
--accent-color: var(--color-positive);
+3 -4
View File
@@ -1,10 +1,11 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useMemo } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent, css, keyframes } from 'styled-components';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { type OrderbookLine, TradeInputField } from '@/constants/abacus';
import { type OrderbookLine } from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization';
import { ORDERBOOK_MAX_ROWS_PER_SIDE } from '@/constants/orderbook';
import { useBreakpoints, useStringGetter } from '@/hooks';
@@ -29,8 +30,6 @@ import { OrderbookTradesOutput, OrderbookTradesTable } from './OrderbookTradesTa
import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins';
const ORDERBOOK_MAX_ROWS_PER_SIDE = 35;
type ElementProps = {
maxRowsPerSide?: number;
layout?: 'vertical' | 'horizontal';