Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a91e56ac3 | ||
|
|
4a64d59546 | ||
|
|
3fc43e8a12 | ||
|
|
c24a9229a6 | ||
|
|
90329d44d0 | ||
|
|
9a02bd3f1c | ||
|
|
adba11b462 | ||
|
|
29526b927f | ||
|
|
503e089efa | ||
|
|
c3495f462c | ||
|
|
6da6eeb2a4 | ||
|
|
0cf3e81bd7 | ||
|
|
f8cf1d364b | ||
|
|
6f344c6b5d | ||
|
|
133856afca | ||
|
|
33c543daaf | ||
|
|
c77e727d2b | ||
|
|
b94538711d | ||
|
|
b83479f45c | ||
|
|
001aad3abc | ||
|
|
f8db5e1ba0 | ||
|
|
f8bec0fb3d | ||
|
|
8bf23b7e9c | ||
|
|
3f10e045f4 | ||
|
|
71d1a0321b | ||
|
|
0f16396c8d | ||
|
|
2709d79f59 | ||
|
|
ab0eec5d4f | ||
|
|
4a044da38f | ||
|
|
fbbfbc6580 | ||
|
|
2714c2b084 | ||
|
|
9979bc8a16 | ||
|
|
a958a59ea2 | ||
|
|
2bf9e66da5 | ||
|
|
2980144e40 | ||
|
|
5387fe8632 | ||
|
|
1132ea3bbd | ||
|
|
9188e08a4f | ||
|
|
941ef7f9dd | ||
|
|
b1ffa2e219 | ||
|
|
91ea68dc53 | ||
|
|
cf79528d05 | ||
|
|
ef81f9da57 | ||
|
|
60d59cd211 | ||
|
|
19d38378f3 | ||
|
|
15a64232c0 | ||
|
|
28d3be0370 | ||
|
|
90ff9ccc5a | ||
|
|
33516422e7 | ||
|
|
eb31610a38 | ||
|
|
b0e1655018 | ||
|
|
b4cc5f87ec | ||
|
|
549ae558e7 | ||
|
|
5b06110e2d | ||
|
|
e3da2ee4b1 | ||
|
|
6b38d68b09 | ||
|
|
1dedc93a56 | ||
|
|
1427b55728 | ||
|
|
20521b5b3f | ||
|
|
532df49a06 | ||
|
|
fb65db633a | ||
|
|
279ef6b4f9 | ||
|
|
0def18db07 | ||
|
|
50bd860ced | ||
|
|
818574cedb | ||
|
|
3be0429a60 | ||
|
|
b8e72246b9 | ||
|
|
99e708a984 | ||
|
|
876f620ada | ||
|
|
b9550890da | ||
|
|
3732b955f5 | ||
|
|
253bfc0eff | ||
|
|
d4bcfc0428 | ||
|
|
7dec32df5e | ||
|
|
e09d574d5b | ||
|
|
944fc6dc95 | ||
|
|
91a97a1c68 | ||
|
|
c5f736d805 | ||
|
|
3769f7078c | ||
|
|
edc9fadfee | ||
|
|
5e2d426cd1 | ||
|
|
766209baf1 |
@@ -98,6 +98,11 @@ For the "Build & Development Settings", we recommend the following:
|
||||
- Framework Preset: `Vite`
|
||||
- Build Command (override): `pnpm run build`
|
||||
|
||||
By default, the dev server runs in development mode and the build command runs in production mode. To override the default mode, you can pass in the `--mode` option flag. For example, if you want to build your app for testnet:
|
||||
```
|
||||
pnpm run build --mode testnet
|
||||
```
|
||||
|
||||
If you wish to incorporate analytics via Amplitude and Bugsnag, you can use our scripts:
|
||||
`pnpm run build:inject-amplitude` and `pnpm run build:inject-bugsnag`. You will need to provide your own API keys for these services. In the Environment Variables section, name the variables as `AMPLITUDE_API_KEY` and `BUGSNAG_API_KEY` and provide the respective keys as their values.
|
||||
|
||||
|
||||
@@ -39,14 +39,17 @@
|
||||
"@cosmjs/proto-signing": "^0.31.0",
|
||||
"@cosmjs/stargate": "^0.31.0",
|
||||
"@cosmjs/tendermint-rpc": "^0.31.0",
|
||||
"@dydxprotocol/v4-abacus": "^1.0.19",
|
||||
"@dydxprotocol/v4-client-js": "^1.0.0",
|
||||
"@dydxprotocol/v4-localization": "^1.0.3",
|
||||
"@dydxprotocol/v4-abacus": "^1.1.32",
|
||||
"@dydxprotocol/v4-client-js": "^1.0.11",
|
||||
"@dydxprotocol/v4-localization": "^1.1.6",
|
||||
"@ethersproject/providers": "^5.7.2",
|
||||
"@js-joda/core": "^5.5.3",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-navigation-menu": "^1.1.3",
|
||||
"@radix-ui/react-popover": "^1.0.6",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
@@ -69,6 +72,7 @@
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"@scure/bip32": "^1.3.0",
|
||||
"@scure/bip39": "^1.2.0",
|
||||
"@tanstack/react-query": "^5.17.9",
|
||||
"@types/lodash": "^4.14.195",
|
||||
"@types/styled-components": "^5.1.26",
|
||||
"@visx/axis": "^3.1.0",
|
||||
@@ -96,19 +100,20 @@
|
||||
"long": "^5.2.3",
|
||||
"luxon": "^3.3.0",
|
||||
"qr-code-styling": "1.6.0-rc.1",
|
||||
"query-string": "^8.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-aria": "^3.25.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-number-format": "^5.2.2",
|
||||
"react-query": "^3.39.3",
|
||||
"react-redux": "^8.1.1",
|
||||
"react-router-dom": "^6.14.0",
|
||||
"react-stately": "^3.23.0",
|
||||
"reselect": "^4.1.8",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"styled-components": "^5.3.11",
|
||||
"use-latest": "^1.2.1",
|
||||
"viem": "^1.12.2",
|
||||
"wagmi": "^1.3.7"
|
||||
"viem": "^2.0.0",
|
||||
"wagmi": "^2.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.5",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"chainId": "1",
|
||||
"tokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"name": "Ethereum"
|
||||
},
|
||||
{
|
||||
"chainId": "5",
|
||||
"tokenAddress": "0x07865c6E87B9F70255377e024ace6630C1Eaa37F",
|
||||
"name": "Ethereum Goerli"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"tradingRewardsFAQs": [
|
||||
{
|
||||
"questionLocalizationKey": "APP.TRADING_REWARDS.FAQ_WHO_IS_ELIGIBLE_QUESTION",
|
||||
"answerLocalizationKey": "APP.TRADING_REWARDS.FAQ_WHO_IS_ELIGIBLE_ANSWER"
|
||||
},
|
||||
{
|
||||
"questionLocalizationKey": "APP.TRADING_REWARDS.FAQ_HOW_DO_TRADING_REWARDS_WORK_QUESTION",
|
||||
"answerLocalizationKey": "APP.TRADING_REWARDS.FAQ_HOW_DO_TRADING_REWARDS_WORK_ANSWER"
|
||||
},
|
||||
{
|
||||
"questionLocalizationKey": "APP.TRADING_REWARDS.FAQ_HOW_DO_I_CLAIM_MY_REWARDS_QUESTION",
|
||||
"answerLocalizationKey": "APP.TRADING_REWARDS.FAQ_HOW_DO_I_CLAIM_MY_REWARDS_ANSWER"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -39,10 +39,10 @@
|
||||
"environments": {
|
||||
"dydxprotocol-dev": {
|
||||
"name": "v4 Dev",
|
||||
"ethereumChainId": "5",
|
||||
"ethereumChainId": "11155111",
|
||||
"dydxChainId": "dydxprotocol-testnet",
|
||||
"chainName": "dYdX Chain",
|
||||
"chainLogo": "dydx-chain.png",
|
||||
"chainLogo": "/dydx-chain.png",
|
||||
"squidIntegratorId": "dYdX-api",
|
||||
"isMainNet": false,
|
||||
"tokens": {
|
||||
@@ -71,6 +71,7 @@
|
||||
"https://validator.v4dev.dydx.exchange"
|
||||
],
|
||||
"0xsquid": "https://testnet.api.0xsquid.com",
|
||||
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/",
|
||||
"faucet": "https://faucet.v4dev.dydx.exchange"
|
||||
},
|
||||
"links": {
|
||||
@@ -88,7 +89,7 @@
|
||||
"governanceLearnMore": "https://help.dydx.exchange",
|
||||
"stakingLearnMore": "https://help.dydx.exchange",
|
||||
"keplrDashboard": "https://testnet.keplr.app/",
|
||||
"accountExportLearnMore": "https://help.dydx.exchange",
|
||||
"accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain",
|
||||
"walletLearnMore": "https://www.dydx.academy/video/defi-wallet"
|
||||
},
|
||||
"wallets": {
|
||||
@@ -112,10 +113,10 @@
|
||||
},
|
||||
"dydxprotocol-dev-2": {
|
||||
"name": "v4 Dev 2",
|
||||
"ethereumChainId": "5",
|
||||
"ethereumChainId": "11155111",
|
||||
"dydxChainId": "dydxprotocol-testnet",
|
||||
"chainName": "dYdX Chain",
|
||||
"chainLogo": "dydx-chain.png",
|
||||
"chainLogo": "/dydx-chain.png",
|
||||
"squidIntegratorId": "dYdX-api",
|
||||
"isMainNet": false,
|
||||
"tokens": {
|
||||
@@ -143,7 +144,8 @@
|
||||
"validators": [
|
||||
"http://54.92.118.111"
|
||||
],
|
||||
"0xsquid": "https://testnet.api.0xsquid.com"
|
||||
"0xsquid": "https://testnet.api.0xsquid.com",
|
||||
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/"
|
||||
},
|
||||
"links": {
|
||||
"tos": "https://dydx.exchange/v4-terms",
|
||||
@@ -160,7 +162,7 @@
|
||||
"governanceLearnMore": "https://help.dydx.exchange",
|
||||
"stakingLearnMore": "https://help.dydx.exchange",
|
||||
"keplrDashboard": "https://testnet.keplr.app/",
|
||||
"accountExportLearnMore": "https://help.dydx.exchange",
|
||||
"accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain",
|
||||
"walletLearnMore": "https://www.dydx.academy/video/defi-wallet"
|
||||
},
|
||||
"wallets": {
|
||||
@@ -184,10 +186,10 @@
|
||||
},
|
||||
"dydxprotocol-dev-4": {
|
||||
"name": "v4 Dev 4",
|
||||
"ethereumChainId": "5",
|
||||
"ethereumChainId": "11155111",
|
||||
"dydxChainId": "dydxprotocol-testnet",
|
||||
"chainName": "dYdX Chain",
|
||||
"chainLogo": "dydx-chain.png",
|
||||
"chainLogo": "/dydx-chain.png",
|
||||
"squidIntegratorId": "dYdX-api",
|
||||
"isMainNet": false,
|
||||
"tokens": {
|
||||
@@ -215,7 +217,8 @@
|
||||
"validators": [
|
||||
"http://validator.v4dev4.dydx.exchange"
|
||||
],
|
||||
"0xsquid": "https://testnet.api.0xsquid.com"
|
||||
"0xsquid": "https://testnet.api.0xsquid.com",
|
||||
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/"
|
||||
},
|
||||
"links": {
|
||||
"tos": "https://dydx.exchange/v4-terms",
|
||||
@@ -232,7 +235,7 @@
|
||||
"governanceLearnMore": "https://help.dydx.exchange",
|
||||
"stakingLearnMore": "https://help.dydx.exchange",
|
||||
"keplrDashboard": "https://testnet.keplr.app/",
|
||||
"accountExportLearnMore": "https://help.dydx.exchange",
|
||||
"accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain",
|
||||
"walletLearnMore": "https://www.dydx.academy/video/defi-wallet"
|
||||
},
|
||||
"wallets": {
|
||||
@@ -256,10 +259,10 @@
|
||||
},
|
||||
"dydxprotocol-dev-5": {
|
||||
"name": "v4 Dev 5",
|
||||
"ethereumChainId": "5",
|
||||
"ethereumChainId": "11155111",
|
||||
"dydxChainId": "dydxprotocol-testnet",
|
||||
"chainName": "dYdX Chain",
|
||||
"chainLogo": "dydx-chain.png",
|
||||
"chainLogo": "/dydx-chain.png",
|
||||
"squidIntegratorId": "dYdX-api",
|
||||
"isMainNet": false,
|
||||
"tokens": {
|
||||
@@ -287,7 +290,8 @@
|
||||
"validators": [
|
||||
"http://18.223.78.50"
|
||||
],
|
||||
"0xsquid": "https://testnet.api.0xsquid.com"
|
||||
"0xsquid": "https://testnet.api.0xsquid.com",
|
||||
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/"
|
||||
},
|
||||
"links": {
|
||||
"tos": "https://dydx.exchange/v4-terms",
|
||||
@@ -304,7 +308,7 @@
|
||||
"governanceLearnMore": "https://help.dydx.exchange",
|
||||
"stakingLearnMore": "https://help.dydx.exchange",
|
||||
"keplrDashboard": "https://testnet.keplr.app/",
|
||||
"accountExportLearnMore": "https://help.dydx.exchange",
|
||||
"accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain",
|
||||
"walletLearnMore": "https://www.dydx.academy/video/defi-wallet"
|
||||
},
|
||||
"wallets": {
|
||||
@@ -328,10 +332,10 @@
|
||||
},
|
||||
"dydxprotocol-staging": {
|
||||
"name": "v4 Staging",
|
||||
"ethereumChainId": "5",
|
||||
"ethereumChainId": "11155111",
|
||||
"dydxChainId": "dydxprotocol-testnet",
|
||||
"chainName": "dYdX Chain",
|
||||
"chainLogo": "dydx-chain.png",
|
||||
"chainLogo": "/dydx-chain.png",
|
||||
"squidIntegratorId": "dYdX-api",
|
||||
"isMainNet": false,
|
||||
"tokens": {
|
||||
@@ -360,7 +364,8 @@
|
||||
"validators": [
|
||||
"https://validator.v4staging.dydx.exchange"
|
||||
],
|
||||
"0xsquid": "https://testnet.api.squidrouter.com"
|
||||
"0xsquid": "https://testnet.api.squidrouter.com",
|
||||
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/"
|
||||
},
|
||||
"links": {
|
||||
"tos": "https://dydx.exchange/v4-terms",
|
||||
@@ -378,7 +383,7 @@
|
||||
"governanceLearnMore": "https://help.dydx.exchange",
|
||||
"stakingLearnMore": "https://help.dydx.exchange",
|
||||
"keplrDashboard": "https://testnet.keplr.app/",
|
||||
"accountExportLearnMore": "https://help.dydx.exchange",
|
||||
"accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain",
|
||||
"walletLearnMore": "https://www.dydx.academy/video/defi-wallet"
|
||||
},
|
||||
"wallets": {
|
||||
@@ -402,10 +407,11 @@
|
||||
},
|
||||
"dydxprotocol-staging-forced-update": {
|
||||
"name": "v4 Staging Forced Update",
|
||||
"ethereumChainId": "5",
|
||||
"ethereumChainId": "11155111",
|
||||
"dydxChainId": "dydxprotocol-testnet",
|
||||
"chainName": "dYdX Chain",
|
||||
"chainLogo": "dydx-chain.png",
|
||||
"chainLogo": "/dydx-chain.png",
|
||||
"squidIntegratorId": "dYdX-api",
|
||||
"isMainNet": false,
|
||||
"tokens": {
|
||||
"chain": {
|
||||
@@ -433,12 +439,14 @@
|
||||
"validators": [
|
||||
"https://validator.v4staging.dydx.exchange"
|
||||
],
|
||||
"0xsquid": "https://testnet.api.squidrouter.com"
|
||||
"0xsquid": "https://testnet.api.squidrouter.com",
|
||||
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/"
|
||||
},
|
||||
"links": {
|
||||
"tos": "https://dydx.exchange/v4-terms",
|
||||
"privacy": "https://dydx.exchange/privacy",
|
||||
"mintscan": "https://testnet.mintscan.io/dydx-testnet/txs/{tx_hash}",
|
||||
"mintscanBase": "https://testnet.mintscan.io/dydx-testnet",
|
||||
"documentation": "https://v4-teacher.vercel.app/",
|
||||
"community": "https://discord.com/invite/dydx",
|
||||
"feedback": "https://docs.google.com/forms/d/e/1FAIpQLSezLsWCKvAYDEb7L-2O4wOON1T56xxro9A2Azvl6IxXHP_15Q/viewform",
|
||||
@@ -472,10 +480,10 @@
|
||||
},
|
||||
"dydxprotocol-staging-west": {
|
||||
"name": "v4 Staging West",
|
||||
"ethereumChainId": "5",
|
||||
"ethereumChainId": "11155111",
|
||||
"dydxChainId": "dydxprotocol-testnet",
|
||||
"chainName": "dYdX Chain",
|
||||
"chainLogo": "dydx-chain.png",
|
||||
"chainLogo": "/dydx-chain.png",
|
||||
"squidIntegratorId": "dYdX-api",
|
||||
"isMainNet": false,
|
||||
"tokens": {
|
||||
@@ -504,7 +512,8 @@
|
||||
"validators": [
|
||||
"https://validator-uswest1.v4staging.dydx.exchange"
|
||||
],
|
||||
"0xsquid": "https://testnet.api.squidrouter.com"
|
||||
"0xsquid": "https://testnet.api.squidrouter.com",
|
||||
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/"
|
||||
},
|
||||
"links": {
|
||||
"tos": "https://dydx.exchange/v4-terms",
|
||||
@@ -522,7 +531,7 @@
|
||||
"governanceLearnMore": "https://help.dydx.exchange",
|
||||
"stakingLearnMore": "https://help.dydx.exchange",
|
||||
"keplrDashboard": "https://testnet.keplr.app/",
|
||||
"accountExportLearnMore": "https://help.dydx.exchange",
|
||||
"accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain",
|
||||
"walletLearnMore": "https://www.dydx.academy/video/defi-wallet"
|
||||
},
|
||||
"wallets": {
|
||||
@@ -546,10 +555,10 @@
|
||||
},
|
||||
"dydxprotocol-testnet": {
|
||||
"name": "v4 Public Testnet",
|
||||
"ethereumChainId": "5",
|
||||
"ethereumChainId": "11155111",
|
||||
"dydxChainId": "dydx-testnet-4",
|
||||
"chainName": "dYdX Chain",
|
||||
"chainLogo": "dydx-chain.png",
|
||||
"chainLogo": "/dydx-chain.png",
|
||||
"squidIntegratorId": "dYdX-api",
|
||||
"isMainNet": false,
|
||||
"tokens": {
|
||||
@@ -570,8 +579,8 @@
|
||||
"endpoints": {
|
||||
"indexers": [
|
||||
{
|
||||
"api": "https://dydx-testnet.imperator.co",
|
||||
"socket": "wss://dydx-testnet.imperator.co"
|
||||
"api": "https://indexer.v4testnet.dydx.exchange",
|
||||
"socket": "wss://indexer.v4testnet.dydx.exchange"
|
||||
}
|
||||
],
|
||||
"validators": [
|
||||
@@ -582,6 +591,7 @@
|
||||
"https://dydx-rpc.liquify.com/api=8878132/dydx"
|
||||
],
|
||||
"0xsquid": "https://testnet.api.squidrouter.com",
|
||||
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/",
|
||||
"faucet": "https://faucet.v4testnet.dydx.exchange"
|
||||
},
|
||||
"links": {
|
||||
@@ -600,7 +610,7 @@
|
||||
"governanceLearnMore": "https://help.dydx.exchange",
|
||||
"stakingLearnMore": "https://help.dydx.exchange",
|
||||
"keplrDashboard": "https://testnet.keplr.app/",
|
||||
"accountExportLearnMore": "https://help.dydx.exchange",
|
||||
"accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain",
|
||||
"walletLearnMore": "https://www.dydx.academy/video/defi-wallet"
|
||||
},
|
||||
"wallets": {
|
||||
@@ -624,10 +634,10 @@
|
||||
},
|
||||
"dydxprotocol-testnet-dydx": {
|
||||
"name": "v4 Public Testnet/dYdX",
|
||||
"ethereumChainId": "5",
|
||||
"ethereumChainId": "11155111",
|
||||
"dydxChainId": "dydx-testnet-4",
|
||||
"chainName": "dYdX Chain",
|
||||
"chainLogo": "dydx-chain.png",
|
||||
"chainLogo": "/dydx-chain.png",
|
||||
"squidIntegratorId": "dYdX-api",
|
||||
"isMainNet": false,
|
||||
"tokens": {
|
||||
@@ -656,6 +666,7 @@
|
||||
"https://validator.v4testnet.dydx.exchange"
|
||||
],
|
||||
"0xsquid": "https://testnet.api.squidrouter.com",
|
||||
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/",
|
||||
"faucet": "https://faucet.v4testnet.dydx.exchange"
|
||||
},
|
||||
"links": {
|
||||
@@ -675,7 +686,7 @@
|
||||
"governanceLearnMore": "https://help.dydx.exchange",
|
||||
"stakingLearnMore": "https://help.dydx.exchange",
|
||||
"keplrDashboard": "https://testnet.keplr.app/",
|
||||
"accountExportLearnMore": "https://help.dydx.exchange",
|
||||
"accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain",
|
||||
"walletLearnMore": "https://www.dydx.academy/video/defi-wallet"
|
||||
},
|
||||
"wallets": {
|
||||
@@ -699,10 +710,10 @@
|
||||
},
|
||||
"dydxprotocol-testnet-nodefleet": {
|
||||
"name": "v4 Public Testnet/nodefleet",
|
||||
"ethereumChainId": "5",
|
||||
"ethereumChainId": "11155111",
|
||||
"dydxChainId": "dydx-testnet-4",
|
||||
"chainName": "dYdX Chain",
|
||||
"chainLogo": "dydx-chain.png",
|
||||
"chainLogo": "/dydx-chain.png",
|
||||
"squidIntegratorId": "dYdX-api",
|
||||
"isMainNet": false,
|
||||
"tokens": {
|
||||
@@ -723,14 +734,15 @@
|
||||
"endpoints": {
|
||||
"indexers": [
|
||||
{
|
||||
"api": "https://dydx-testnet.imperator.co",
|
||||
"socket": "wss://dydx-testnet.imperator.co"
|
||||
"api": "https://indexer.v4testnet.dydx.exchange",
|
||||
"socket": "wss://indexer.v4testnet.dydx.exchange"
|
||||
}
|
||||
],
|
||||
"validators": [
|
||||
"https://dydx-testnet.nodefleet.org"
|
||||
],
|
||||
"0xsquid": "https://testnet.api.squidrouter.com",
|
||||
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/",
|
||||
"faucet": "https://faucet.v4testnet.dydx.exchange"
|
||||
},
|
||||
"links": {
|
||||
@@ -750,7 +762,7 @@
|
||||
"governanceLearnMore": "https://help.dydx.exchange",
|
||||
"stakingLearnMore": "https://help.dydx.exchange",
|
||||
"keplrDashboard": "https://testnet.keplr.app/",
|
||||
"accountExportLearnMore": "https://help.dydx.exchange",
|
||||
"accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain",
|
||||
"walletLearnMore": "https://www.dydx.academy/video/defi-wallet"
|
||||
},
|
||||
"wallets": {
|
||||
@@ -774,10 +786,10 @@
|
||||
},
|
||||
"dydxprotocol-testnet-kingnodes": {
|
||||
"name": "v4 Public Testnet/KingNodes",
|
||||
"ethereumChainId": "5",
|
||||
"ethereumChainId": "11155111",
|
||||
"dydxChainId": "dydx-testnet-4",
|
||||
"chainName": "dYdX Chain",
|
||||
"chainLogo": "dydx-chain.png",
|
||||
"chainLogo": "/dydx-chain.png",
|
||||
"squidIntegratorId": "dYdX-api",
|
||||
"isMainNet": false,
|
||||
"tokens": {
|
||||
@@ -798,14 +810,15 @@
|
||||
"endpoints": {
|
||||
"indexers": [
|
||||
{
|
||||
"api": "https://dydx-testnet.imperator.co",
|
||||
"socket": "wss://dydx-testnet.imperator.co"
|
||||
"api": "https://indexer.v4testnet.dydx.exchange",
|
||||
"socket": "wss://indexer.v4testnet.dydx.exchange"
|
||||
}
|
||||
],
|
||||
"validators": [
|
||||
"https://test-dydx.kingnodes.com"
|
||||
],
|
||||
"0xsquid": "https://testnet.api.squidrouter.com",
|
||||
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/",
|
||||
"faucet": "https://faucet.v4testnet.dydx.exchange"
|
||||
},
|
||||
"links": {
|
||||
@@ -825,7 +838,7 @@
|
||||
"governanceLearnMore": "https://help.dydx.exchange",
|
||||
"stakingLearnMore": "https://help.dydx.exchange",
|
||||
"keplrDashboard": "https://testnet.keplr.app/",
|
||||
"accountExportLearnMore": "https://help.dydx.exchange",
|
||||
"accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain",
|
||||
"walletLearnMore": "https://www.dydx.academy/video/defi-wallet"
|
||||
},
|
||||
"wallets": {
|
||||
@@ -849,10 +862,10 @@
|
||||
},
|
||||
"dydxprotocol-testnet-liquify": {
|
||||
"name": "v4 Public Testnet/Liquify",
|
||||
"ethereumChainId": "5",
|
||||
"ethereumChainId": "11155111",
|
||||
"dydxChainId": "dydx-testnet-4",
|
||||
"chainName": "dYdX Chain",
|
||||
"chainLogo": "dydx-chain.png",
|
||||
"chainLogo": "/dydx-chain.png",
|
||||
"squidIntegratorId": "dYdX-api",
|
||||
"isMainNet": false,
|
||||
"tokens": {
|
||||
@@ -873,14 +886,15 @@
|
||||
"endpoints": {
|
||||
"indexers": [
|
||||
{
|
||||
"api": "https://dydx-testnet.imperator.co",
|
||||
"socket": "wss://dydx-testnet.imperator.co"
|
||||
"api": "https://indexer.v4testnet.dydx.exchange",
|
||||
"socket": "wss://indexer.v4testnet.dydx.exchange"
|
||||
}
|
||||
],
|
||||
"validators": [
|
||||
"https://dydx-rpc.liquify.com/api=8878132/dydx"
|
||||
],
|
||||
"0xsquid": "https://testnet.api.squidrouter.com",
|
||||
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/",
|
||||
"faucet": "https://faucet.v4testnet.dydx.exchange"
|
||||
},
|
||||
"links": {
|
||||
@@ -900,7 +914,7 @@
|
||||
"governanceLearnMore": "https://help.dydx.exchange",
|
||||
"stakingLearnMore": "https://help.dydx.exchange",
|
||||
"keplrDashboard": "https://testnet.keplr.app/",
|
||||
"accountExportLearnMore": "https://help.dydx.exchange",
|
||||
"accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain",
|
||||
"walletLearnMore": "https://www.dydx.academy/video/defi-wallet"
|
||||
},
|
||||
"wallets": {
|
||||
@@ -924,10 +938,10 @@
|
||||
},
|
||||
"dydxprotocol-testnet-polkachu": {
|
||||
"name": "v4 Public Testnet/Polkahcu",
|
||||
"ethereumChainId": "5",
|
||||
"ethereumChainId": "11155111",
|
||||
"dydxChainId": "dydx-testnet-4",
|
||||
"chainName": "dYdX Chain",
|
||||
"chainLogo": "dydx-chain.png",
|
||||
"chainLogo": "/dydx-chain.png",
|
||||
"squidIntegratorId": "dYdX-api",
|
||||
"isMainNet": false,
|
||||
"tokens": {
|
||||
@@ -948,14 +962,15 @@
|
||||
"endpoints": {
|
||||
"indexers": [
|
||||
{
|
||||
"api": "https://dydx-testnet.imperator.co",
|
||||
"socket": "wss://dydx-testnet.imperator.co"
|
||||
"api": "https://indexer.v4testnet.dydx.exchange",
|
||||
"socket": "wss://indexer.v4testnet.dydx.exchange"
|
||||
}
|
||||
],
|
||||
"validators": [
|
||||
"https://dydx-testnet-rpc.polkachu.com/"
|
||||
],
|
||||
"0xsquid": "https://testnet.api.squidrouter.com",
|
||||
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/",
|
||||
"faucet": "https://faucet.v4testnet.dydx.exchange"
|
||||
},
|
||||
"links": {
|
||||
@@ -990,10 +1005,10 @@
|
||||
},
|
||||
"dydxprotocol-testnet-bware": {
|
||||
"name": "v4 Public Testnet/BWare",
|
||||
"ethereumChainId": "5",
|
||||
"ethereumChainId": "11155111",
|
||||
"dydxChainId": "dydx-testnet-4",
|
||||
"chainName": "dYdX Chain",
|
||||
"chainLogo": "dydx-chain.png",
|
||||
"chainLogo": "/dydx-chain.png",
|
||||
"squidIntegratorId": "dYdX-api",
|
||||
"isMainNet": false,
|
||||
"tokens": {
|
||||
@@ -1014,14 +1029,15 @@
|
||||
"endpoints": {
|
||||
"indexers": [
|
||||
{
|
||||
"api": "https://dydx-testnet.imperator.co",
|
||||
"socket": "wss://dydx-testnet.imperator.co"
|
||||
"api": "https://indexer.v4testnet.dydx.exchange",
|
||||
"socket": "wss://indexer.v4testnet.dydx.exchange"
|
||||
}
|
||||
],
|
||||
"validators": [
|
||||
"https://dydx-testnet-full-rpc.public.blastapi.io/"
|
||||
],
|
||||
"0xsquid": "https://testnet.api.squidrouter.com",
|
||||
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/",
|
||||
"faucet": "https://faucet.v4testnet.dydx.exchange"
|
||||
},
|
||||
"links": {
|
||||
@@ -1068,7 +1084,7 @@
|
||||
"ethereumChainId": "1",
|
||||
"dydxChainId": "[mainnet chain id]",
|
||||
"chainName": "dYdX Chain",
|
||||
"chainLogo": "dydx-chain.png",
|
||||
"chainLogo": "/dydx-chain.png",
|
||||
"squidIntegratorId": "[mainnet squid integrator id]",
|
||||
"isMainNet": true,
|
||||
"tokens": {
|
||||
@@ -1098,7 +1114,8 @@
|
||||
"[Validator endpoint 1",
|
||||
"[Validator endpoint n]"
|
||||
],
|
||||
"0xsquid": "[0xSquid endpoint for mainnet]"
|
||||
"0xsquid": "[0xSquid endpoint for mainnet]",
|
||||
"nobleValidator": "[noble validator endpoint for mainnet]"
|
||||
},
|
||||
"links": {
|
||||
"tos": "[HTTP link to TOS]",
|
||||
@@ -1109,7 +1126,7 @@
|
||||
"blogs": "[HTTP link to blogs, can be null]",
|
||||
"foundation": "[HTTP link to foundation, can be null]",
|
||||
"initialMarginFractionLearnMore": "[HTTP link to initial margin fraction learn more, can be null]",
|
||||
"reduceOnlyLearnMore": "https://help.dydx.exchange/articles/6345793-reduce-only-orders",
|
||||
"reduceOnlyLearnMore": "[HTTP link to reduce-only learn more, can be null]",
|
||||
"documentation": "[HTTP link to documentation, can be null]",
|
||||
"community": "[HTTP link to community, can be null]",
|
||||
"help": "[HTTP link to help page, can be null]",
|
||||
|
||||
@@ -1,454 +1,360 @@
|
||||
{
|
||||
"1INCH-USD":{
|
||||
"name":"1inch",
|
||||
"tags":[
|
||||
"Defi"
|
||||
],
|
||||
"websiteLink":"https://1inch.io/",
|
||||
"whitepaperLink":"https://github.com/1inch",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/1inch/"
|
||||
},
|
||||
"AAVE-USD":{
|
||||
"name":"Aave",
|
||||
"tags":[
|
||||
"Defi"
|
||||
],
|
||||
"websiteLink":"https://aave.com/",
|
||||
"whitepaperLink":"https://github.com/aave/protocol-v2/blob/master/aave-v2-whitepaper.pdf",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/aave/"
|
||||
},
|
||||
"ADA-USD":{
|
||||
"name":"Cardano",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://cardano.org/",
|
||||
"whitepaperLink":"https://why.cardano.org/en/introduction/motivation/",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/cardano/"
|
||||
},
|
||||
"ALGO-USD":{
|
||||
"name":"Algorand",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://algorand.com/",
|
||||
"whitepaperLink":"https://algorand.com/technology/white-papers",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/algorand/"
|
||||
},
|
||||
"APE-USD":{
|
||||
"name":"ApeCoin",
|
||||
"tags":[
|
||||
|
||||
],
|
||||
"websiteLink":"https://apecoin.com/",
|
||||
"whitepaperLink":"https://apecoin.com/about",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/apecoin-ape/"
|
||||
},
|
||||
"APT-USD":{
|
||||
"name":"Aptos",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://aptoslabs.com/",
|
||||
"whitepaperLink":"https://aptos.dev/aptos-white-paper/",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/aptos/"
|
||||
},
|
||||
"ARB-USD":{
|
||||
"name":"Arbitrum",
|
||||
"tags":[
|
||||
|
||||
],
|
||||
"websiteLink":"https://arbitrum.io/",
|
||||
"whitepaperLink":"https://github.com/OffchainLabs/nitro",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/arbitrum/"
|
||||
},
|
||||
"ATOM-USD":{
|
||||
"name":"Cosmos",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://cosmos.network/",
|
||||
"whitepaperLink":"https://v1.cosmos.network/resources/whitepaper",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/cosmos/"
|
||||
},
|
||||
"AVAX-USD":{
|
||||
"name":"Avalanche",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://www.avalabs.org/",
|
||||
"whitepaperLink":"https://assets.website-files.com/5d80307810123f5ffbb34d6e/6008d7bbf8b10d1eb01e7e16_Avalanche%20Platform%20Whitepaper.pdf",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/avalanche/"
|
||||
},
|
||||
"BCH-USD":{
|
||||
"name":"Bitcoin Cash",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://bitcoincash.org/",
|
||||
"whitepaperLink":"https://bitcoincash.org/",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/bitcoin-cash/"
|
||||
},
|
||||
"BLUR-USD":{
|
||||
"name":"Blur",
|
||||
"tags":[
|
||||
|
||||
],
|
||||
"websiteLink":"https://blur.io/",
|
||||
"whitepaperLink":"https://docs.blur.foundation/",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/blur-token/"
|
||||
},
|
||||
"CELO-USD":{
|
||||
"name":"Celo",
|
||||
"tags":[
|
||||
|
||||
],
|
||||
"websiteLink":"https://celo.org",
|
||||
"whitepaperLink":"https://docs.celo.org",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/celo/"
|
||||
},
|
||||
"BTC-USD":{
|
||||
"name":"Bitcoin",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://bitcoin.org/",
|
||||
"whitepaperLink":"https://bitcoin.org/bitcoin.pdf",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/bitcoin/"
|
||||
},
|
||||
"COMP-USD":{
|
||||
"name":"Compound",
|
||||
"tags":[
|
||||
"Defi"
|
||||
],
|
||||
"websiteLink":"https://compound.finance/",
|
||||
"whitepaperLink":"https://compound.finance/documents/Compound.Whitepaper.pdf",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/compound/"
|
||||
},
|
||||
"CRV-USD":{
|
||||
"name":"Curve",
|
||||
"tags":[
|
||||
"Governance"
|
||||
],
|
||||
"websiteLink":"https://curve.fi/",
|
||||
"whitepaperLink":"https://curve.fi/whitepaper",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/curve-dao-token/"
|
||||
},
|
||||
"DOGE-USD":{
|
||||
"name":"Dogecoin",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://dogecoin.com/",
|
||||
"whitepaperLink":"https://github.com/dogecoin/dogecoin",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/dogecoin/"
|
||||
},
|
||||
"DOT-USD":{
|
||||
"name":"Polkadot",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://polkadot.network/",
|
||||
"whitepaperLink":"https://polkadot.network/PolkaDotPaper.pdf",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/polkadot-new/"
|
||||
},
|
||||
"ENJ-USD":{
|
||||
"name":"Enjin",
|
||||
"tags":[
|
||||
|
||||
],
|
||||
"websiteLink":"https://enjin.io/",
|
||||
"whitepaperLink":"https://cdn.enjin.io/downloads/whitepapers/enjin-coin/en.pdf/",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/enjin-coin/"
|
||||
},
|
||||
"EOS-USD":{
|
||||
"name":"EOS",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://eos.io/",
|
||||
"whitepaperLink":"https://github.com/EOSIO/Documentation/blob/master/TechnicalWhitePaper.md",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/eos/"
|
||||
},
|
||||
"ETC-USD":{
|
||||
"name":"Ethereum Classic",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://ethereumclassic.org/",
|
||||
"whitepaperLink":"https://ethereumclassic.org/why-classic",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/ethereum-classic/"
|
||||
},
|
||||
"ETH-USD":{
|
||||
"name":"Ethereum",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://ethereum.org/",
|
||||
"whitepaperLink":"https://ethereum.org/whitepaper/",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/ethereum/",
|
||||
"displayStepSize":"0.001",
|
||||
"displayTickSize":"0.1"
|
||||
},
|
||||
"FIL-USD":{
|
||||
"name":"Filecoin",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://filecoin.io/",
|
||||
"whitepaperLink":"https://filecoin.io/filecoin.pdf",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/filecoin/"
|
||||
},
|
||||
"HNT-USD":{
|
||||
"name":"Helium",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://www.helium.com",
|
||||
"whitepaperLink":"http://whitepaper.helium.com",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/helium/"
|
||||
},
|
||||
"ICP-USD":{
|
||||
"name":"Internet Computer",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://dfinity.org",
|
||||
"whitepaperLink":"https://dfinity.org/whitepaper.pdf",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/internet-computer/"
|
||||
},
|
||||
"LDO-USD":{
|
||||
"name":"Lido DAO",
|
||||
"tags":[
|
||||
"Defi"
|
||||
],
|
||||
"websiteLink":"https://lido.fi/",
|
||||
"whitepaperLink":"https://lido.fi/static/Lido:Ethereum-Liquid-Staking.pdf",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/lido-dao/"
|
||||
},
|
||||
"LINK-USD":{
|
||||
"name":"Chainlink",
|
||||
"tags":[
|
||||
"Defi"
|
||||
],
|
||||
"websiteLink":"https://chain.link/",
|
||||
"whitepaperLink":"https://link.smartcontract.com/whitepaper",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/chainlink/"
|
||||
},
|
||||
"LTC-USD":{
|
||||
"name":"Litecoin",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://litecoin.org/",
|
||||
"whitepaperLink":"https://litecoin.info/index.php/Main_Page",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/litecoin/"
|
||||
},
|
||||
"MATIC-USD":{
|
||||
"name":"Polygon",
|
||||
"tags":[
|
||||
"Layer 2"
|
||||
],
|
||||
"websiteLink":"https://polygon.technology/",
|
||||
"whitepaperLink":"https://polygon.technology/lightpaper-polygon.pdf",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/polygon/"
|
||||
},
|
||||
"MKR-USD":{
|
||||
"name":"Maker",
|
||||
"tags":[
|
||||
"Governance"
|
||||
],
|
||||
"websiteLink":"https://makerdao.com",
|
||||
"whitepaperLink":"https://makerdao.com/whitepaper",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/maker/"
|
||||
},
|
||||
"NEAR-USD":{
|
||||
"name":"NEAR Protocol",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://near.org",
|
||||
"whitepaperLink":"https://near.org/papers/the-official-near-white-paper/",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/near-protocol/"
|
||||
},
|
||||
"OP-USD":{
|
||||
"name":"Optimism",
|
||||
"tags":[
|
||||
|
||||
],
|
||||
"websiteLink":"https://www.optimism.io/",
|
||||
"whitepaperLink":"https://github.com/ethereum-optimism",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/optimism-ethereum/"
|
||||
},
|
||||
"PEPE-USD":{
|
||||
"name":"Pepe",
|
||||
"tags":[
|
||||
|
||||
],
|
||||
"websiteLink":"https://www.pepe.vip/",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/pepe/"
|
||||
},
|
||||
"RUNE-USD":{
|
||||
"name":"THORChain",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://thorchain.org",
|
||||
"whitepaperLink":"https://whitepaper.io/document/709/thorchain-whitepaper",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/thorchain/"
|
||||
},
|
||||
"SEI-USD":{
|
||||
"name":"Sei",
|
||||
"tags":[
|
||||
"Layer 1",
|
||||
"Defi"
|
||||
],
|
||||
"websiteLink":"https://www.sei.io/",
|
||||
"whitepaperLink":"https://github.com/sei-protocol/sei-chain/blob/3c9576fee3494ce039df684624f918dd8066ba3f/whitepaper/Sei_Whitepaper.pdf",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/sei/"
|
||||
},
|
||||
"SHIB-USD":{
|
||||
"name":"Shiba Inu",
|
||||
"tags":[
|
||||
|
||||
],
|
||||
"websiteLink":"https://shibatoken.com/",
|
||||
"whitepaperLink":"https://docs.shibatoken.com/",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/shiba-inu/"
|
||||
},
|
||||
"SNX-USD":{
|
||||
"name":"Synthetix",
|
||||
"tags":[
|
||||
"Defi"
|
||||
],
|
||||
"websiteLink":"https://synthetix.io/",
|
||||
"whitepaperLink":"https://docs.synthetix.io/litepaper",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/synthetix-network-token/"
|
||||
},
|
||||
"SOL-USD":{
|
||||
"name":"Solana",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://solana.com/",
|
||||
"whitepaperLink":"https://solana.com/solana-whitepaper.pdf",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/solana/"
|
||||
},
|
||||
"SUI-USD":{
|
||||
"name":"Sui",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://sui.io/",
|
||||
"whitepaperLink":"https://github.com/MystenLabs",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/sui/"
|
||||
},
|
||||
"SUSHI-USD":{
|
||||
"name":"Sushi",
|
||||
"tags":[
|
||||
"Defi"
|
||||
],
|
||||
"websiteLink":"https://sushi.com/",
|
||||
"whitepaperLink":"https://docs.sushi.com/",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/sushiswap/"
|
||||
},
|
||||
"TRX-USD":{
|
||||
"name":"TRON",
|
||||
"tags":[
|
||||
"Defi"
|
||||
],
|
||||
"websiteLink":"https://tron.network/",
|
||||
"whitepaperLink":"https://tron.network/static/doc/white_paper_v_2_0.pdf",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/tron/"
|
||||
},
|
||||
"UMA-USD":{
|
||||
"name":"UMA",
|
||||
"tags":[
|
||||
"Defi"
|
||||
],
|
||||
"websiteLink":"https://umaproject.org/",
|
||||
"whitepaperLink":"https://github.com/UMAprotocol/whitepaper",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/uma/"
|
||||
},
|
||||
"UNI-USD":{
|
||||
"name":"Uniswap",
|
||||
"tags":[
|
||||
"Defi"
|
||||
],
|
||||
"websiteLink":"https://uniswap.org/",
|
||||
"whitepaperLink":"https://uniswap.org/whitepaper-v3.pdf",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/uniswap/"
|
||||
},
|
||||
"WLD-USD":{
|
||||
"name":"Worldcoin",
|
||||
"tags":[
|
||||
|
||||
],
|
||||
"websiteLink":"https://worldcoin.org/",
|
||||
"whitepaperLink":"https://whitepaper.worldcoin.org/",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/worldcoin-org/"
|
||||
},
|
||||
"XLM-USD":{
|
||||
"name":"Stellar",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://www.stellar.org/",
|
||||
"whitepaperLink":"https://www.stellar.org/papers/stellar-consensus-protocol",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/stellar/"
|
||||
},
|
||||
"XMR-USD":{
|
||||
"name":"Monero",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://www.getmonero.org/",
|
||||
"whitepaperLink":"https://www.getmonero.org/resources/research-lab/",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/monero/"
|
||||
},
|
||||
"XRP-USD":{
|
||||
"name":"Ripple",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://ripple.com/currency/",
|
||||
"whitepaperLink":"https://github.com/ripple",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/xrp/"
|
||||
},
|
||||
"XTZ-USD":{
|
||||
"name":"Tezos",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://tezos.com",
|
||||
"whitepaperLink":"https://tezos.com/whitepaper.pdf",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/tezos/"
|
||||
},
|
||||
"YFI-USD":{
|
||||
"name":"Yearn",
|
||||
"tags":[
|
||||
"Defi"
|
||||
],
|
||||
"websiteLink":"https://yearn.finance/",
|
||||
"whitepaperLink":"https://docs.yearn.finance/",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/yearn-finance/"
|
||||
},
|
||||
"ZEC-USD":{
|
||||
"name":"Zcash",
|
||||
"tags":[
|
||||
"Layer 1"
|
||||
],
|
||||
"websiteLink":"https://z.cash/",
|
||||
"whitepaperLink":"https://z.cash/technology/",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/zcash/"
|
||||
},
|
||||
"ZRX-USD":{
|
||||
"name":"0x",
|
||||
"tags":[
|
||||
"Defi"
|
||||
],
|
||||
"websiteLink":"https://0x.org/",
|
||||
"whitepaperLink":"https://0x.org/pdfs/0x_white_paper.pdf",
|
||||
"coinMarketCapsLink":"https://coinmarketcap.com/currencies/0x/"
|
||||
}
|
||||
}
|
||||
"1INCH-USD": {
|
||||
"name": "1inch",
|
||||
"tags": ["Defi"],
|
||||
"websiteLink": "https://1inch.io/",
|
||||
"whitepaperLink": "https://github.com/1inch",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/1inch/"
|
||||
},
|
||||
"AAVE-USD": {
|
||||
"name": "Aave",
|
||||
"tags": ["Defi"],
|
||||
"websiteLink": "https://aave.com/",
|
||||
"whitepaperLink": "https://github.com/aave/protocol-v2/blob/master/aave-v2-whitepaper.pdf",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/aave/"
|
||||
},
|
||||
"ADA-USD": {
|
||||
"name": "Cardano",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://cardano.org/",
|
||||
"whitepaperLink": "https://why.cardano.org/en/introduction/motivation/",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/cardano/"
|
||||
},
|
||||
"ALGO-USD": {
|
||||
"name": "Algorand",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://algorand.com/",
|
||||
"whitepaperLink": "https://algorand.com/technology/white-papers",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/algorand/"
|
||||
},
|
||||
"APE-USD": {
|
||||
"name": "ApeCoin",
|
||||
"tags": [],
|
||||
"websiteLink": "https://apecoin.com/",
|
||||
"whitepaperLink": "https://apecoin.com/about",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/apecoin-ape/"
|
||||
},
|
||||
"APT-USD": {
|
||||
"name": "Aptos",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://aptoslabs.com/",
|
||||
"whitepaperLink": "https://aptos.dev/aptos-white-paper/",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/aptos/"
|
||||
},
|
||||
"ARB-USD": {
|
||||
"name": "Arbitrum",
|
||||
"tags": [],
|
||||
"websiteLink": "https://arbitrum.io/",
|
||||
"whitepaperLink": "https://github.com/OffchainLabs/nitro",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/arbitrum/"
|
||||
},
|
||||
"ATOM-USD": {
|
||||
"name": "Cosmos",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://cosmos.network/",
|
||||
"whitepaperLink": "https://v1.cosmos.network/resources/whitepaper",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/cosmos/"
|
||||
},
|
||||
"AVAX-USD": {
|
||||
"name": "Avalanche",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://www.avalabs.org/",
|
||||
"whitepaperLink": "https://assets.website-files.com/5d80307810123f5ffbb34d6e/6008d7bbf8b10d1eb01e7e16_Avalanche%20Platform%20Whitepaper.pdf",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/avalanche/"
|
||||
},
|
||||
"BCH-USD": {
|
||||
"name": "Bitcoin Cash",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://bitcoincash.org/",
|
||||
"whitepaperLink": "https://bitcoincash.org/",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/bitcoin-cash/"
|
||||
},
|
||||
"BLUR-USD": {
|
||||
"name": "Blur",
|
||||
"tags": [],
|
||||
"websiteLink": "https://blur.io/",
|
||||
"whitepaperLink": "https://docs.blur.foundation/",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/blur-token/"
|
||||
},
|
||||
"CELO-USD": {
|
||||
"name": "Celo",
|
||||
"tags": [],
|
||||
"websiteLink": "https://celo.org",
|
||||
"whitepaperLink": "https://docs.celo.org",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/celo/"
|
||||
},
|
||||
"BTC-USD": {
|
||||
"name": "Bitcoin",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://bitcoin.org/",
|
||||
"whitepaperLink": "https://bitcoin.org/bitcoin.pdf",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/bitcoin/"
|
||||
},
|
||||
"COMP-USD": {
|
||||
"name": "Compound",
|
||||
"tags": ["Defi"],
|
||||
"websiteLink": "https://compound.finance/",
|
||||
"whitepaperLink": "https://compound.finance/documents/Compound.Whitepaper.pdf",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/compound/"
|
||||
},
|
||||
"CRV-USD": {
|
||||
"name": "Curve",
|
||||
"tags": ["Governance"],
|
||||
"websiteLink": "https://curve.fi/",
|
||||
"whitepaperLink": "https://curve.fi/whitepaper",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/curve-dao-token/"
|
||||
},
|
||||
"DOGE-USD": {
|
||||
"name": "Dogecoin",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://dogecoin.com/",
|
||||
"whitepaperLink": "https://github.com/dogecoin/dogecoin",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/dogecoin/"
|
||||
},
|
||||
"DOT-USD": {
|
||||
"name": "Polkadot",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://polkadot.network/",
|
||||
"whitepaperLink": "https://polkadot.network/PolkaDotPaper.pdf",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/polkadot-new/"
|
||||
},
|
||||
"ENJ-USD": {
|
||||
"name": "Enjin",
|
||||
"tags": [],
|
||||
"websiteLink": "https://enjin.io/",
|
||||
"whitepaperLink": "https://cdn.enjin.io/downloads/whitepapers/enjin-coin/en.pdf/",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/enjin-coin/"
|
||||
},
|
||||
"EOS-USD": {
|
||||
"name": "EOS",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://eos.io/",
|
||||
"whitepaperLink": "https://github.com/EOSIO/Documentation/blob/master/TechnicalWhitePaper.md",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/eos/"
|
||||
},
|
||||
"ETC-USD": {
|
||||
"name": "Ethereum Classic",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://ethereumclassic.org/",
|
||||
"whitepaperLink": "https://ethereumclassic.org/why-classic",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/ethereum-classic/"
|
||||
},
|
||||
"ETH-USD": {
|
||||
"name": "Ethereum",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://ethereum.org/",
|
||||
"whitepaperLink": "https://ethereum.org/whitepaper/",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/ethereum/",
|
||||
"displayStepSize": "0.001",
|
||||
"displayTickSize": "0.1"
|
||||
},
|
||||
"FIL-USD": {
|
||||
"name": "Filecoin",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://filecoin.io/",
|
||||
"whitepaperLink": "https://filecoin.io/filecoin.pdf",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/filecoin/"
|
||||
},
|
||||
"HNT-USD": {
|
||||
"name": "Helium",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://www.helium.com",
|
||||
"whitepaperLink": "http://whitepaper.helium.com",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/helium/"
|
||||
},
|
||||
"ICP-USD": {
|
||||
"name": "Internet Computer",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://dfinity.org",
|
||||
"whitepaperLink": "https://dfinity.org/whitepaper.pdf",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/internet-computer/"
|
||||
},
|
||||
"LDO-USD": {
|
||||
"name": "Lido DAO",
|
||||
"tags": ["Defi"],
|
||||
"websiteLink": "https://lido.fi/",
|
||||
"whitepaperLink": "https://lido.fi/static/Lido:Ethereum-Liquid-Staking.pdf",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/lido-dao/"
|
||||
},
|
||||
"LINK-USD": {
|
||||
"name": "Chainlink",
|
||||
"tags": ["Defi"],
|
||||
"websiteLink": "https://chain.link/",
|
||||
"whitepaperLink": "https://link.smartcontract.com/whitepaper",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/chainlink/"
|
||||
},
|
||||
"LTC-USD": {
|
||||
"name": "Litecoin",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://litecoin.org/",
|
||||
"whitepaperLink": "https://litecoin.info/index.php/Main_Page",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/litecoin/"
|
||||
},
|
||||
"MATIC-USD": {
|
||||
"name": "Polygon",
|
||||
"tags": ["Layer 2"],
|
||||
"websiteLink": "https://polygon.technology/",
|
||||
"whitepaperLink": "https://polygon.technology/lightpaper-polygon.pdf",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/polygon/"
|
||||
},
|
||||
"MKR-USD": {
|
||||
"name": "Maker",
|
||||
"tags": ["Governance"],
|
||||
"websiteLink": "https://makerdao.com",
|
||||
"whitepaperLink": "https://makerdao.com/whitepaper",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/maker/"
|
||||
},
|
||||
"NEAR-USD": {
|
||||
"name": "NEAR Protocol",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://near.org",
|
||||
"whitepaperLink": "https://near.org/papers/the-official-near-white-paper/",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/near-protocol/"
|
||||
},
|
||||
"OP-USD": {
|
||||
"name": "Optimism",
|
||||
"tags": [],
|
||||
"websiteLink": "https://www.optimism.io/",
|
||||
"whitepaperLink": "https://github.com/ethereum-optimism",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/optimism-ethereum/"
|
||||
},
|
||||
"PEPE-USD": {
|
||||
"name": "Pepe",
|
||||
"tags": [],
|
||||
"websiteLink": "https://www.pepe.vip/",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/pepe/"
|
||||
},
|
||||
"RUNE-USD": {
|
||||
"name": "THORChain",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://thorchain.org",
|
||||
"whitepaperLink": "https://whitepaper.io/document/709/thorchain-whitepaper",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/thorchain/"
|
||||
},
|
||||
"SEI-USD": {
|
||||
"name": "Sei",
|
||||
"tags": ["Layer 1", "Defi"],
|
||||
"websiteLink": "https://www.sei.io/",
|
||||
"whitepaperLink": "https://github.com/sei-protocol/sei-chain/blob/3c9576fee3494ce039df684624f918dd8066ba3f/whitepaper/Sei_Whitepaper.pdf",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/sei/"
|
||||
},
|
||||
"SHIB-USD": {
|
||||
"name": "Shiba Inu",
|
||||
"tags": [],
|
||||
"websiteLink": "https://shibatoken.com/",
|
||||
"whitepaperLink": "https://docs.shibatoken.com/",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/shiba-inu/"
|
||||
},
|
||||
"SNX-USD": {
|
||||
"name": "Synthetix",
|
||||
"tags": ["Defi"],
|
||||
"websiteLink": "https://synthetix.io/",
|
||||
"whitepaperLink": "https://docs.synthetix.io/litepaper",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/synthetix-network-token/"
|
||||
},
|
||||
"SOL-USD": {
|
||||
"name": "Solana",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://solana.com/",
|
||||
"whitepaperLink": "https://solana.com/solana-whitepaper.pdf",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/solana/"
|
||||
},
|
||||
"SUI-USD": {
|
||||
"name": "Sui",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://sui.io/",
|
||||
"whitepaperLink": "https://github.com/MystenLabs",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/sui/"
|
||||
},
|
||||
"SUSHI-USD": {
|
||||
"name": "Sushi",
|
||||
"tags": ["Defi"],
|
||||
"websiteLink": "https://sushi.com/",
|
||||
"whitepaperLink": "https://docs.sushi.com/",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/sushiswap/"
|
||||
},
|
||||
"TIA-USD": {
|
||||
"name": "Celestia",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://celestia.org/",
|
||||
"whitepaperLink": "https://arxiv.org/pdf/1905.09274.pdf",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/celestia/"
|
||||
},
|
||||
"TRX-USD": {
|
||||
"name": "TRON",
|
||||
"tags": ["Defi"],
|
||||
"websiteLink": "https://tron.network/",
|
||||
"whitepaperLink": "https://tron.network/static/doc/white_paper_v_2_0.pdf",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/tron/"
|
||||
},
|
||||
"UMA-USD": {
|
||||
"name": "UMA",
|
||||
"tags": ["Defi"],
|
||||
"websiteLink": "https://umaproject.org/",
|
||||
"whitepaperLink": "https://github.com/UMAprotocol/whitepaper",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/uma/"
|
||||
},
|
||||
"UNI-USD": {
|
||||
"name": "Uniswap",
|
||||
"tags": ["Defi"],
|
||||
"websiteLink": "https://uniswap.org/",
|
||||
"whitepaperLink": "https://uniswap.org/whitepaper-v3.pdf",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/uniswap/"
|
||||
},
|
||||
"WLD-USD": {
|
||||
"name": "Worldcoin",
|
||||
"tags": [],
|
||||
"websiteLink": "https://worldcoin.org/",
|
||||
"whitepaperLink": "https://whitepaper.worldcoin.org/",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/worldcoin-org/"
|
||||
},
|
||||
"XLM-USD": {
|
||||
"name": "Stellar",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://www.stellar.org/",
|
||||
"whitepaperLink": "https://www.stellar.org/papers/stellar-consensus-protocol",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/stellar/"
|
||||
},
|
||||
"XMR-USD": {
|
||||
"name": "Monero",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://www.getmonero.org/",
|
||||
"whitepaperLink": "https://www.getmonero.org/resources/research-lab/",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/monero/"
|
||||
},
|
||||
"XRP-USD": {
|
||||
"name": "Ripple",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://ripple.com/currency/",
|
||||
"whitepaperLink": "https://github.com/ripple",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/xrp/"
|
||||
},
|
||||
"XTZ-USD": {
|
||||
"name": "Tezos",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://tezos.com",
|
||||
"whitepaperLink": "https://tezos.com/whitepaper.pdf",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/tezos/"
|
||||
},
|
||||
"YFI-USD": {
|
||||
"name": "Yearn",
|
||||
"tags": ["Defi"],
|
||||
"websiteLink": "https://yearn.finance/",
|
||||
"whitepaperLink": "https://docs.yearn.finance/",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/yearn-finance/"
|
||||
},
|
||||
"ZEC-USD": {
|
||||
"name": "Zcash",
|
||||
"tags": ["Layer 1"],
|
||||
"websiteLink": "https://z.cash/",
|
||||
"whitepaperLink": "https://z.cash/technology/",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/zcash/"
|
||||
},
|
||||
"ZRX-USD": {
|
||||
"name": "0x",
|
||||
"tags": ["Defi"],
|
||||
"websiteLink": "https://0x.org/",
|
||||
"whitepaperLink": "https://0x.org/pdfs/0x_white_paper.pdf",
|
||||
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/0x/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@
|
||||
},
|
||||
"mobile": {
|
||||
"native": "trust:",
|
||||
"universal": "https://link.trustwallet.com"
|
||||
"universal": ""
|
||||
},
|
||||
"desktop": {
|
||||
"native": "",
|
||||
|
||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 481 KiB |
|
Before Width: | Height: | Size: 817 B After Width: | Height: | Size: 297 B |
@@ -1,8 +1,8 @@
|
||||
import { Suspense } from 'react';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import styled, { AnyStyledComponent, css } from 'styled-components';
|
||||
import { WagmiConfig } from 'wagmi';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { WagmiProvider } from 'wagmi';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { GrazProvider } from 'graz';
|
||||
|
||||
import { AppRoute, DEFAULT_TRADE_ROUTE } from '@/constants/routes';
|
||||
@@ -22,20 +22,9 @@ import { NotificationsProvider } from '@/hooks/useNotifications';
|
||||
import { LocalNotificationsProvider } from '@/hooks/useLocalNotifications';
|
||||
import { RestrictionProvider } from '@/hooks/useRestrictions';
|
||||
import { SubaccountProvider } from '@/hooks/useSubaccount';
|
||||
import { SquidProvider } from '@/hooks/useSquid';
|
||||
|
||||
import { GuardedMobileRoute } from '@/components/GuardedMobileRoute';
|
||||
|
||||
import MarketsPage from '@/pages/markets/Markets';
|
||||
import PortfolioPage from '@/pages/portfolio/Portfolio';
|
||||
import { AlertsPage } from '@/pages/AlertsPage';
|
||||
import ProfilePage from '@/pages/Profile';
|
||||
import { SettingsPage } from '@/pages/settings/Settings';
|
||||
import TradePage from '@/pages/trade/Trade';
|
||||
import { RewardsPage } from '@/pages/rewards/RewardsPage';
|
||||
import { TermsOfUsePage } from '@/pages/TermsOfUsePage';
|
||||
import { PrivacyPolicyPage } from '@/pages/PrivacyPolicyPage';
|
||||
|
||||
import { HeaderDesktop } from '@/layout/Header/HeaderDesktop';
|
||||
import { FooterDesktop } from '@/layout/Footer/FooterDesktop';
|
||||
import { FooterMobile } from '@/layout/Footer/FooterMobile';
|
||||
@@ -47,11 +36,22 @@ import { config } from '@/lib/wagmi';
|
||||
|
||||
import { breakpoints } from '@/styles';
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
import { LoadingSpace } from './components/Loading/LoadingSpinner';
|
||||
|
||||
import '@/styles/constants.css';
|
||||
import '@/styles/fonts.css';
|
||||
import '@/styles/web3modal.css';
|
||||
|
||||
const MarketsPage = lazy(() => import('@/pages/markets/Markets'));
|
||||
const PortfolioPage = lazy(() => import('@/pages/portfolio/Portfolio'));
|
||||
const AlertsPage = lazy(() => import('@/pages/AlertsPage'));
|
||||
const ProfilePage = lazy(() => import('@/pages/Profile'));
|
||||
const SettingsPage = lazy(() => import('@/pages/settings/Settings'));
|
||||
const TradePage = lazy(() => import('@/pages/trade/Trade'));
|
||||
const RewardsPage = lazy(() => import('@/pages/rewards/RewardsPage'));
|
||||
const TermsOfUsePage = lazy(() => import('@/pages/TermsOfUsePage'));
|
||||
const PrivacyPolicyPage = lazy(() => import('@/pages/PrivacyPolicyPage'));
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const Content = () => {
|
||||
@@ -70,7 +70,7 @@ const Content = () => {
|
||||
{isNotTablet && <HeaderDesktop />}
|
||||
|
||||
<Styled.Main>
|
||||
<Suspense fallback={null}>
|
||||
<Suspense fallback={<LoadingSpace id="main" />}>
|
||||
<Routes>
|
||||
<Route path={AppRoute.Trade}>
|
||||
<Route path=":market" element={<TradePage />} />
|
||||
@@ -120,15 +120,14 @@ const wrapProvider = (Component: React.ComponentType<any>, props?: any) => {
|
||||
};
|
||||
|
||||
const providers = [
|
||||
wrapProvider(WagmiProvider, { config }),
|
||||
wrapProvider(QueryClientProvider, { client: queryClient }),
|
||||
wrapProvider(GrazProvider),
|
||||
wrapProvider(WagmiConfig, { config }),
|
||||
wrapProvider(LocaleProvider),
|
||||
wrapProvider(RestrictionProvider),
|
||||
wrapProvider(DydxProvider),
|
||||
wrapProvider(AccountsProvider),
|
||||
wrapProvider(SubaccountProvider),
|
||||
wrapProvider(SquidProvider),
|
||||
wrapProvider(LocalNotificationsProvider),
|
||||
wrapProvider(NotificationsProvider),
|
||||
wrapProvider(DialogAreaProvider),
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
[
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "name",
|
||||
"outputs": [{ "name": "", "type": "string" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [{ "name": "_upgradedAddress", "type": "address" }],
|
||||
"name": "deprecate",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{ "name": "_spender", "type": "address" },
|
||||
{ "name": "_value", "type": "uint256" }
|
||||
],
|
||||
"name": "approve",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "deprecated",
|
||||
"outputs": [{ "name": "", "type": "bool" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [{ "name": "_evilUser", "type": "address" }],
|
||||
"name": "addBlackList",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "totalSupply",
|
||||
"outputs": [{ "name": "", "type": "uint256" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{ "name": "_from", "type": "address" },
|
||||
{ "name": "_to", "type": "address" },
|
||||
{ "name": "_value", "type": "uint256" }
|
||||
],
|
||||
"name": "transferFrom",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "upgradedAddress",
|
||||
"outputs": [{ "name": "", "type": "address" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [{ "name": "", "type": "address" }],
|
||||
"name": "balances",
|
||||
"outputs": [{ "name": "", "type": "uint256" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "decimals",
|
||||
"outputs": [{ "name": "", "type": "uint256" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "maximumFee",
|
||||
"outputs": [{ "name": "", "type": "uint256" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "_totalSupply",
|
||||
"outputs": [{ "name": "", "type": "uint256" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [],
|
||||
"name": "unpause",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [{ "name": "_maker", "type": "address" }],
|
||||
"name": "getBlackListStatus",
|
||||
"outputs": [{ "name": "", "type": "bool" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{ "name": "", "type": "address" },
|
||||
{ "name": "", "type": "address" }
|
||||
],
|
||||
"name": "allowed",
|
||||
"outputs": [{ "name": "", "type": "uint256" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "paused",
|
||||
"outputs": [{ "name": "", "type": "bool" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [{ "name": "who", "type": "address" }],
|
||||
"name": "balanceOf",
|
||||
"outputs": [{ "name": "", "type": "uint256" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [],
|
||||
"name": "pause",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "getOwner",
|
||||
"outputs": [{ "name": "", "type": "address" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "owner",
|
||||
"outputs": [{ "name": "", "type": "address" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "symbol",
|
||||
"outputs": [{ "name": "", "type": "string" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{ "name": "_to", "type": "address" },
|
||||
{ "name": "_value", "type": "uint256" }
|
||||
],
|
||||
"name": "transfer",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{ "name": "newBasisPoints", "type": "uint256" },
|
||||
{ "name": "newMaxFee", "type": "uint256" }
|
||||
],
|
||||
"name": "setParams",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [{ "name": "amount", "type": "uint256" }],
|
||||
"name": "issue",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [{ "name": "amount", "type": "uint256" }],
|
||||
"name": "redeem",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{ "name": "_owner", "type": "address" },
|
||||
{ "name": "_spender", "type": "address" }
|
||||
],
|
||||
"name": "allowance",
|
||||
"outputs": [{ "name": "remaining", "type": "uint256" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "basisPointsRate",
|
||||
"outputs": [{ "name": "", "type": "uint256" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [{ "name": "", "type": "address" }],
|
||||
"name": "isBlackListed",
|
||||
"outputs": [{ "name": "", "type": "bool" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [{ "name": "_clearedUser", "type": "address" }],
|
||||
"name": "removeBlackList",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "MAX_UINT",
|
||||
"outputs": [{ "name": "", "type": "uint256" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [{ "name": "newOwner", "type": "address" }],
|
||||
"name": "transferOwnership",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [{ "name": "_blackListedUser", "type": "address" }],
|
||||
"name": "destroyBlackFunds",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{ "name": "_initialSupply", "type": "uint256" },
|
||||
{ "name": "_name", "type": "string" },
|
||||
{ "name": "_symbol", "type": "string" },
|
||||
{ "name": "_decimals", "type": "uint256" }
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "constructor"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [{ "indexed": false, "name": "amount", "type": "uint256" }],
|
||||
"name": "Issue",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [{ "indexed": false, "name": "amount", "type": "uint256" }],
|
||||
"name": "Redeem",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [{ "indexed": false, "name": "newAddress", "type": "address" }],
|
||||
"name": "Deprecate",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{ "indexed": false, "name": "feeBasisPoints", "type": "uint256" },
|
||||
{ "indexed": false, "name": "maxFee", "type": "uint256" }
|
||||
],
|
||||
"name": "Params",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{ "indexed": false, "name": "_blackListedUser", "type": "address" },
|
||||
{ "indexed": false, "name": "_balance", "type": "uint256" }
|
||||
],
|
||||
"name": "DestroyedBlackFunds",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [{ "indexed": false, "name": "_user", "type": "address" }],
|
||||
"name": "AddedBlackList",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [{ "indexed": false, "name": "_user", "type": "address" }],
|
||||
"name": "RemovedBlackList",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{ "indexed": true, "name": "owner", "type": "address" },
|
||||
{ "indexed": true, "name": "spender", "type": "address" },
|
||||
{ "indexed": false, "name": "value", "type": "uint256" }
|
||||
],
|
||||
"name": "Approval",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{ "indexed": true, "name": "from", "type": "address" },
|
||||
{ "indexed": true, "name": "to", "type": "address" },
|
||||
{ "indexed": false, "name": "value", "type": "uint256" }
|
||||
],
|
||||
"name": "Transfer",
|
||||
"type": "event"
|
||||
},
|
||||
{ "anonymous": false, "inputs": [], "name": "Pause", "type": "event" },
|
||||
{ "anonymous": false, "inputs": [], "name": "Unpause", "type": "event" }
|
||||
]
|
||||
@@ -0,0 +1,119 @@
|
||||
import styled, { keyframes, type AnyStyledComponent } from 'styled-components';
|
||||
|
||||
import { Root, Item, Header, Trigger, Content } from '@radix-ui/react-accordion';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
import { PlusIcon } from '@/icons';
|
||||
|
||||
export type AccordionItem = {
|
||||
header: React.ReactNode;
|
||||
content: React.ReactNode;
|
||||
};
|
||||
|
||||
type AccordionProps = {
|
||||
items: AccordionItem[];
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const Accordion = ({ items, className }: AccordionProps) => (
|
||||
<Styled.Root className={className} type="single" collapsible>
|
||||
{items.map(({ header, content }, idx) => (
|
||||
<Styled.Item key={idx} value={idx.toString()}>
|
||||
<Header>
|
||||
<Styled.Trigger>
|
||||
{header}
|
||||
<Styled.Icon>
|
||||
<PlusIcon />
|
||||
</Styled.Icon>
|
||||
</Styled.Trigger>
|
||||
</Header>
|
||||
<Styled.Content>{content}</Styled.Content>
|
||||
</Styled.Item>
|
||||
))}
|
||||
</Styled.Root>
|
||||
);
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.Root = styled(Root)`
|
||||
> *:not(:last-child) {
|
||||
border-bottom: var(--border-width) solid var(--border-color);
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.Item = styled(Item)``;
|
||||
|
||||
Styled.Icon = styled.div`
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
|
||||
--color-border: var(--color-layer-6);
|
||||
color: var(--color-text-1);
|
||||
background-color: var(--color-layer-5);
|
||||
border: solid var(--border-width) var(--color-border);
|
||||
border-radius: 50%;
|
||||
font: var(--font-small-book);
|
||||
|
||||
svg {
|
||||
height: 1.125em;
|
||||
width: 1.125em;
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.Trigger = styled(Trigger)`
|
||||
${layoutMixins.spacedRow}
|
||||
width: 100%;
|
||||
padding: 1rem 0.75rem;
|
||||
|
||||
color: var(--color-text-1);
|
||||
text-align: start;
|
||||
|
||||
&:hover {
|
||||
${Styled.Icon} {
|
||||
color: var(--color-text-2);
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
color: var(--color-text-0);
|
||||
transition: transform 0.3s var(--ease-out-expo);
|
||||
}
|
||||
|
||||
&[data-state='open'] svg {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.Content = styled(Content)`
|
||||
overflow: hidden;
|
||||
margin: 0 0.75rem 1rem;
|
||||
|
||||
color: var(--color-text-0);
|
||||
|
||||
&[data-state='open'] {
|
||||
animation: ${keyframes`
|
||||
from {
|
||||
height: 0;
|
||||
}
|
||||
to {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
`} 0.3s var(--ease-out-expo);
|
||||
}
|
||||
|
||||
&[data-state='closed'] {
|
||||
animation: ${keyframes`
|
||||
from {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
to {
|
||||
height: 0;
|
||||
}
|
||||
`} 0.1s var(--ease-in-expo);
|
||||
}
|
||||
`;
|
||||
@@ -45,6 +45,7 @@ const assetIcons = {
|
||||
SOL: '/currencies/sol.png',
|
||||
SUI: '/currencies/sui.png',
|
||||
SUSHI: '/currencies/sushi.png',
|
||||
TIA: '/currencies/tia.png',
|
||||
TRX: '/currencies/trx.png',
|
||||
UMA: '/currencies/uma.png',
|
||||
UNI: '/currencies/uni.png',
|
||||
@@ -71,8 +72,13 @@ export const AssetIcon = ({
|
||||
}: {
|
||||
symbol?: Nullable<string>;
|
||||
className?: string;
|
||||
}) =>
|
||||
isAssetSymbol(symbol) ? <Styled.Img src={assetIcons[symbol]} className={className} /> : null;
|
||||
}) => (
|
||||
<Styled.Img
|
||||
src={isAssetSymbol(symbol) ? assetIcons[symbol] : '/currencies/unavailable.png'}
|
||||
className={className}
|
||||
alt={symbol}
|
||||
/>
|
||||
);
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
|
||||
|
||||
return (
|
||||
<StyledBaseButton
|
||||
disabled={state[ButtonState.Disabled]}
|
||||
disabled={state[ButtonState.Disabled] || state[ButtonState.Loading]}
|
||||
{...{ ref, action, size, shape, state, ...otherProps }}
|
||||
>
|
||||
{
|
||||
|
||||
@@ -13,7 +13,9 @@ export const Checkboxes: Story<CheckboxProps> = (args) => {
|
||||
<Checkbox
|
||||
{...args}
|
||||
checked={isChecked}
|
||||
onClick={(e: React.ChangeEvent) => setIsChecked((e.target as HTMLInputElement).checked)}
|
||||
onCheckedChange={setIsChecked}
|
||||
id="story-checkbox"
|
||||
label="example label"
|
||||
/>
|
||||
</StoryWrapper>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import styled, { type AnyStyledComponent } from 'styled-components';
|
||||
import { Root, Indicator } from '@radix-ui/react-checkbox';
|
||||
import { CheckIcon } from '@radix-ui/react-icons';
|
||||
|
||||
import { breakpoints } from '@/styles';
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
type ElementProps = {
|
||||
checked: boolean;
|
||||
onClick: (e: React.ChangeEvent) => void;
|
||||
onCheckedChange: (checked: boolean) => void;
|
||||
id?: string;
|
||||
label?: React.ReactNode;
|
||||
};
|
||||
|
||||
type StyleProps = {
|
||||
@@ -13,85 +17,54 @@ type StyleProps = {
|
||||
|
||||
export type CheckboxProps = ElementProps & StyleProps;
|
||||
|
||||
export const Checkbox: React.FC<CheckboxProps> = ({ checked, className, onClick }) => {
|
||||
return (
|
||||
<Styled.CheckboxWrapper className={className}>
|
||||
<Styled.Checkbox
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={onClick}
|
||||
/>
|
||||
<Styled.CustomCheckbox />
|
||||
</Styled.CheckboxWrapper>
|
||||
);
|
||||
};
|
||||
export const Checkbox: React.FC<CheckboxProps> = ({
|
||||
checked,
|
||||
className,
|
||||
onCheckedChange,
|
||||
id,
|
||||
label,
|
||||
}) => (
|
||||
<Styled.Container>
|
||||
<Styled.Root className={className} checked={checked} onCheckedChange={onCheckedChange} id={id}>
|
||||
<Styled.Indicator>
|
||||
<CheckIcon />
|
||||
</Styled.Indicator>
|
||||
</Styled.Root>
|
||||
{label && <label htmlFor={id}>{label}</label>}
|
||||
</Styled.Container>
|
||||
);
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.CustomCheckbox = styled.span`
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
Styled.Container = styled.div`
|
||||
${layoutMixins.row}
|
||||
gap: 1ch;
|
||||
font: var(--font-small-book);
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.Root = styled(Root)`
|
||||
--checkbox-backgroundColor: var(--color-layer-0);
|
||||
|
||||
min-width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
|
||||
border-radius: 0.375rem;
|
||||
border: var(--border-width) solid var(--color-border);
|
||||
background-color: var(--checkbox-backgroundColor);
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 0.25em;
|
||||
left: 0.4375em;
|
||||
width: 0.3125em;
|
||||
height: 0.5em;
|
||||
border: solid var(--checkbox-checkColor);
|
||||
border-width: 0 0.125em 0.125em 0;
|
||||
border-radius: 0.0625em;
|
||||
opacity: 0;
|
||||
transform: rotate(0deg) scale(0);
|
||||
transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
@media ${breakpoints.tablet} {
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
|
||||
&::after {
|
||||
top: 0.25em;
|
||||
left: 0.4375em;
|
||||
width: 0.4375em;
|
||||
height: 0.625em;
|
||||
}
|
||||
&[data-state='checked'] {
|
||||
--checkbox-backgroundColor: var(--color-accent);
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.CheckboxWrapper = styled.div`
|
||||
--checkbox-backgroundColor: var(--color-layer-1);
|
||||
--checkbox-checkColor: var(--color-text-1);
|
||||
|
||||
Styled.Indicator = styled(Indicator)`
|
||||
display: flex;
|
||||
border-radius: 0.25em;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
> input:checked ~ ${Styled.CustomCheckbox}::after {
|
||||
opacity: 1;
|
||||
transform: rotate(40deg) scale(1);
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.Checkbox = styled.input`
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
|
||||
@media ${breakpoints.tablet} {
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
}
|
||||
color: var(--color-text-2);
|
||||
`;
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import type { Story } from '@ladle/react';
|
||||
|
||||
import { Collapsible } from '@/components/Collapsible';
|
||||
import { Collapsible, type CollapsibleProps } from '@/components/Collapsible';
|
||||
|
||||
import { StoryWrapper } from '.ladle/components';
|
||||
import { IconName } from './Icon';
|
||||
|
||||
export const CollapsibleStory: Story<Parameters<typeof Collapsible>> = (args) => (
|
||||
export const CollapsibleStory: Story<CollapsibleProps> = (args) => (
|
||||
<StoryWrapper>
|
||||
<Collapsible
|
||||
label="Collapsible List of Items"
|
||||
{...args}
|
||||
>
|
||||
<Collapsible {...args}>
|
||||
<ul>
|
||||
<li>Collapsible Item 1</li>
|
||||
<li>Collapsible Item 2</li>
|
||||
@@ -23,6 +20,8 @@ export const CollapsibleStory: Story<Parameters<typeof Collapsible>> = (args) =>
|
||||
|
||||
CollapsibleStory.args = {
|
||||
disabled: false,
|
||||
withTrigger: true,
|
||||
label: 'Collapsible List of Items',
|
||||
};
|
||||
|
||||
CollapsibleStory.argTypes = {
|
||||
|
||||
@@ -14,6 +14,7 @@ type ElementProps = {
|
||||
label: React.ReactNode;
|
||||
triggerIcon?: IconName;
|
||||
children: React.ReactNode;
|
||||
withTrigger?: boolean;
|
||||
};
|
||||
|
||||
type StyleProps = {
|
||||
@@ -23,6 +24,8 @@ type StyleProps = {
|
||||
fullWidth?: boolean;
|
||||
};
|
||||
|
||||
export type CollapsibleProps = ElementProps & StyleProps;
|
||||
|
||||
export const Collapsible = ({
|
||||
defaultOpen = false,
|
||||
disabled,
|
||||
@@ -35,26 +38,29 @@ export const Collapsible = ({
|
||||
triggerIconSide = 'left',
|
||||
fullWidth,
|
||||
className,
|
||||
}: ElementProps & StyleProps) => (
|
||||
withTrigger = true,
|
||||
}: CollapsibleProps) => (
|
||||
<Styled.Root defaultOpen={defaultOpen} open={open} onOpenChange={onOpenChange}>
|
||||
<Styled.Trigger className={className} disabled={disabled}>
|
||||
{triggerIconSide === 'right' && (
|
||||
<>
|
||||
{label}
|
||||
{fullWidth && <HorizontalSeparatorFiller />}
|
||||
</>
|
||||
)}
|
||||
{withTrigger && (
|
||||
<Styled.Trigger className={className} disabled={disabled}>
|
||||
{triggerIconSide === 'right' && (
|
||||
<>
|
||||
{label}
|
||||
{fullWidth && <HorizontalSeparatorFiller />}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Styled.TriggerIcon>
|
||||
<Icon iconName={triggerIcon} />
|
||||
</Styled.TriggerIcon>
|
||||
{triggerIconSide === 'left' && (
|
||||
<>
|
||||
{fullWidth && <HorizontalSeparatorFiller />}
|
||||
{label}
|
||||
</>
|
||||
)}
|
||||
</Styled.Trigger>
|
||||
<Styled.TriggerIcon>
|
||||
<Icon iconName={triggerIcon} />
|
||||
</Styled.TriggerIcon>
|
||||
{triggerIconSide === 'left' && (
|
||||
<>
|
||||
{fullWidth && <HorizontalSeparatorFiller />}
|
||||
{label}
|
||||
</>
|
||||
)}
|
||||
</Styled.Trigger>
|
||||
)}
|
||||
<Styled.Content $transitionDuration={transitionDuration}>{children}</Styled.Content>
|
||||
</Styled.Root>
|
||||
);
|
||||
|
||||
@@ -31,10 +31,12 @@ type ElementProps<TabItemsValue> = {
|
||||
};
|
||||
|
||||
type StyleProps = {
|
||||
fullWidthTabs?: boolean;
|
||||
className?: string;
|
||||
fullWidthTabs?: boolean;
|
||||
};
|
||||
|
||||
export type CollapsibleTabsProps<TabItemsValue> = ElementProps<TabItemsValue> & StyleProps;
|
||||
|
||||
export const CollapsibleTabs = <TabItemsValue extends string>({
|
||||
defaultValue,
|
||||
items,
|
||||
@@ -43,8 +45,9 @@ export const CollapsibleTabs = <TabItemsValue extends string>({
|
||||
onOpenChange,
|
||||
|
||||
fullWidthTabs,
|
||||
|
||||
className,
|
||||
}: ElementProps<TabItemsValue> & StyleProps) => {
|
||||
}: CollapsibleTabsProps<TabItemsValue>) => {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
|
||||
const currentItem = items.find((item) => item.value === value);
|
||||
@@ -86,8 +89,8 @@ export const CollapsibleTabs = <TabItemsValue extends string>({
|
||||
</Styled.Header>
|
||||
|
||||
<Styled.CollapsibleContent>
|
||||
{items.map(({ value, content }) => (
|
||||
<Styled.TabsContent key={value} value={value}>
|
||||
{items.map(({ asChild, value, content }) => (
|
||||
<Styled.TabsContent key={value} asChild={asChild} value={value}>
|
||||
{content}
|
||||
</Styled.TabsContent>
|
||||
))}
|
||||
@@ -219,7 +222,6 @@ Styled.Header = styled.header`
|
||||
|
||||
Styled.CollapsibleContent = styled(CollapsibleContent)`
|
||||
${layoutMixins.stack}
|
||||
${layoutMixins.perspectiveArea}
|
||||
|
||||
box-shadow: none;
|
||||
`;
|
||||
|
||||
@@ -22,7 +22,12 @@ type PickComboxMenuProps<
|
||||
MenuGroupValue extends string | number
|
||||
> = Pick<
|
||||
ComboboxMenuProps<MenuItemValue, MenuGroupValue>,
|
||||
'inputPlaceholder' | 'onItemSelected' | 'slotEmpty' | 'withSearch' | 'withStickyLayout'
|
||||
| 'inputPlaceholder'
|
||||
| 'onItemSelected'
|
||||
| 'slotEmpty'
|
||||
| 'withItemBorders'
|
||||
| 'withSearch'
|
||||
| 'withStickyLayout'
|
||||
>;
|
||||
|
||||
type PickDialogProps = Pick<
|
||||
@@ -53,6 +58,7 @@ export const ComboboxDialogMenu = <
|
||||
onItemSelected,
|
||||
inputPlaceholder,
|
||||
slotEmpty,
|
||||
withItemBorders,
|
||||
withSearch,
|
||||
withStickyLayout = true,
|
||||
children,
|
||||
@@ -83,6 +89,7 @@ export const ComboboxDialogMenu = <
|
||||
title={title}
|
||||
inputPlaceholder={inputPlaceholder}
|
||||
slotEmpty={slotEmpty}
|
||||
withItemBorders={withItemBorders}
|
||||
withSearch={withSearch}
|
||||
withStickyLayout={withStickyLayout}
|
||||
/>
|
||||
@@ -95,6 +102,8 @@ const Styled: Record<string, AnyStyledComponent> = {};
|
||||
Styled.Dialog = styled(Dialog)`
|
||||
/* Params */
|
||||
--comboboxDialogMenu-backgroundColor: var(--color-layer-2);
|
||||
--comboboxDialogMenu-item-gap: 0.5rem;
|
||||
--comboboxDialogMenu-item-padding: 0.5rem 1rem;
|
||||
|
||||
/* Overrides */
|
||||
& {
|
||||
@@ -113,10 +122,12 @@ Styled.Dialog = styled(Dialog)`
|
||||
|
||||
/* Net 0 sticky top inset (let stickyArea1 header stick to top) */
|
||||
--stickyArea0-topGap: calc(-1 * var(--stickyArea0-topHeight));
|
||||
overflow-x: clip;
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.ComboboxMenu = styled(ComboboxMenu)`
|
||||
--comboboxMenu-backgroundColor: var(--comboboxDialogMenu-backgroundColor);
|
||||
--comboboxMenu-item-gap: var(--comboxDialogMenu-item-gap, 0.5rem);
|
||||
--comboboxMenu-item-gap: var(--comboboxDialogMenu-item-gap);
|
||||
--comboboxMenu-item-padding: var(--comboboxDialogMenu-item-padding);
|
||||
`;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Fragment, type ReactNode, useState } from 'react';
|
||||
import styled, { type AnyStyledComponent, css } from 'styled-components';
|
||||
import { Command } from 'cmdk';
|
||||
|
||||
import { type MenuConfig } from '@/constants/menus';
|
||||
import { MenuItem, type MenuConfig } from '@/constants/menus';
|
||||
import { popoverMixins } from '@/styles/popoverMixins';
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
@@ -20,6 +20,7 @@ type ElementProps<MenuItemValue extends string | number, MenuGroupValue extends
|
||||
|
||||
type StyleProps = {
|
||||
className?: string;
|
||||
withItemBorders?: boolean;
|
||||
withStickyLayout?: boolean;
|
||||
};
|
||||
|
||||
@@ -38,17 +39,11 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
|
||||
withSearch = true,
|
||||
|
||||
className,
|
||||
withItemBorders,
|
||||
withStickyLayout,
|
||||
}: ComboboxMenuProps<MenuItemValue, MenuGroupValue>) => {
|
||||
const [highlightedCommand, setHighlightedCommand] = useState<MenuItemValue>();
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
// const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
// console.log({ commandValue: highlightedCommand });
|
||||
|
||||
// useEffect(() => {
|
||||
// inputRef?.current?.focus();
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<Styled.Command
|
||||
@@ -65,11 +60,14 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
|
||||
{withSearch && (
|
||||
<Styled.Header $withStickyLayout={withStickyLayout}>
|
||||
<Styled.Input
|
||||
// ref={inputRef}
|
||||
/**
|
||||
* Mobile Issue: Search Input will always trigger mobile keyboard drawer. There is no fix.
|
||||
* https://github.com/pacocoursey/cmdk/issues/127
|
||||
*/
|
||||
autoFocus
|
||||
type="search"
|
||||
value={searchValue}
|
||||
onValueChange={setSearchValue}
|
||||
autoFocus
|
||||
placeholder={inputPlaceholder}
|
||||
/>
|
||||
</Styled.Header>
|
||||
@@ -80,6 +78,7 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
|
||||
<Styled.Group
|
||||
key={group.group}
|
||||
heading={group.groupLabel}
|
||||
$withItemBorders={withItemBorders}
|
||||
$withStickyLayout={withStickyLayout}
|
||||
>
|
||||
{group.items.map((item) => (
|
||||
@@ -99,22 +98,31 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
|
||||
}
|
||||
}}
|
||||
disabled={item.disabled}
|
||||
$withItemBorders={withItemBorders}
|
||||
>
|
||||
{item.slotBefore}
|
||||
<Styled.ItemLabel>
|
||||
<span>
|
||||
{`${item.label}${item.subitems?.length ? '…' : ''}`}
|
||||
{item.tag && (
|
||||
<>
|
||||
{' '}
|
||||
<Tag>{item.tag}</Tag>
|
||||
</>
|
||||
{
|
||||
<>
|
||||
{item.slotBefore}
|
||||
{item.slotCustomContent ?? (
|
||||
<Styled.ItemLabel>
|
||||
<span>
|
||||
{typeof item.label === 'string'
|
||||
? `${item.label}${item.subitems?.length ? '…' : ''}`
|
||||
: item.label}
|
||||
{item.tag && (
|
||||
<>
|
||||
{' '}
|
||||
<Tag>{item.tag}</Tag>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
{item.description && <span>{item.description}</span>}
|
||||
</Styled.ItemLabel>
|
||||
)}
|
||||
</span>
|
||||
{item.description && <span>{item.description}</span>}
|
||||
</Styled.ItemLabel>
|
||||
{item.slotAfter}
|
||||
{item.subitems && '→'}
|
||||
{item.slotAfter}
|
||||
{item.subitems && '→'}
|
||||
</>
|
||||
}
|
||||
</Styled.Item>
|
||||
|
||||
{searchValue &&
|
||||
@@ -137,6 +145,7 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
|
||||
onItemSelected?.();
|
||||
}}
|
||||
disabled={subitem.disabled}
|
||||
$withItemBorders={withItemBorders}
|
||||
>
|
||||
{subitem.slotBefore}
|
||||
<Styled.ItemLabel>
|
||||
@@ -169,14 +178,17 @@ const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.Command = styled(Command)<{ $withStickyLayout?: boolean }>`
|
||||
--comboboxMenu-backgroundColor: var(--color-layer-2);
|
||||
|
||||
--comboboxMenu-input-backgroundColor: var(--color-layer-3);
|
||||
--comboboxMenu-input-height: 2.5rem;
|
||||
|
||||
--comboboxMenu-item-checked-backgroundColor: ;
|
||||
--comboboxMenu-item-checked-textColor: ;
|
||||
--comboboxMenu-item-highlighted-backgroundColor: var(--color-layer-3);
|
||||
--comboboxMenu-item-highlighted-textColor: var(--color-text-1);
|
||||
--comboboxMenu-item-backgroundColor: ;
|
||||
--comboboxMenu-item-gap: 0.5rem;
|
||||
--comboboxMenu-item-padding: 0.5em 1em;
|
||||
|
||||
display: grid;
|
||||
align-content: start;
|
||||
@@ -225,7 +237,7 @@ Styled.Input = styled(Command.Input)`
|
||||
gap: 0.5rem;
|
||||
`;
|
||||
|
||||
Styled.Group = styled(Command.Group)<{ $withStickyLayout?: boolean }>`
|
||||
Styled.Group = styled(Command.Group)<{ $withItemBorders?: boolean; $withStickyLayout?: boolean }>`
|
||||
color: var(--color-text-0);
|
||||
|
||||
> [cmdk-group-heading] {
|
||||
@@ -242,12 +254,21 @@ Styled.Group = styled(Command.Group)<{ $withStickyLayout?: boolean }>`
|
||||
|
||||
> [cmdk-group-heading] {
|
||||
${layoutMixins.stickyHeader}
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
> [cmdk-group-items] {
|
||||
${layoutMixins.stickyArea3}
|
||||
}
|
||||
`}
|
||||
|
||||
${({ $withItemBorders }) =>
|
||||
$withItemBorders &&
|
||||
css`
|
||||
> [cmdk-group-items] {
|
||||
padding: var(--border-width) 0;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
Styled.List = styled(Command.List)<{ $withStickyLayout?: boolean }>`
|
||||
@@ -274,13 +295,14 @@ Styled.List = styled(Command.List)<{ $withStickyLayout?: boolean }>`
|
||||
`}
|
||||
`;
|
||||
|
||||
Styled.Item = styled(Command.Item)`
|
||||
Styled.Item = styled(Command.Item)<{ $withItemBorders?: boolean }>`
|
||||
${layoutMixins.scrollSnapItem}
|
||||
${popoverMixins.item}
|
||||
--item-checked-backgroundColor: var(--comboboxMenu-item-checked-backgroundColor);
|
||||
--item-checked-textColor: var(--comboboxMenu-item-checked-textColor);
|
||||
--item-highlighted-textColor: var(--comboboxMenu-item-highlighted-textColor);
|
||||
--item-gap: var(--comboboxMenu-item-gap);
|
||||
--item-padding: var(--comboboxMenu-item-padding);
|
||||
|
||||
background-color: var(--comboboxMenu-backgroundColor, inherit);
|
||||
|
||||
@@ -291,6 +313,12 @@ Styled.Item = styled(Command.Item)`
|
||||
opacity: 0.75;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
${({ $withItemBorders }) =>
|
||||
$withItemBorders &&
|
||||
css`
|
||||
${layoutMixins.withOuterBorder}
|
||||
`}
|
||||
`;
|
||||
|
||||
Styled.ItemLabel = styled.div`
|
||||
@@ -309,6 +337,8 @@ Styled.ItemLabel = styled.div`
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
Styled.Empty = styled(Command.Empty)`
|
||||
|
||||
@@ -15,10 +15,10 @@ CopyButtonStory.args = {
|
||||
};
|
||||
|
||||
CopyButtonStory.argTypes = {
|
||||
shownAsText: {
|
||||
options: [true, false],
|
||||
buttonType: {
|
||||
options: ["text", "icon", "default"],
|
||||
control: { type: 'select' },
|
||||
defaultValue: false,
|
||||
defaultValue: "default",
|
||||
},
|
||||
children: {
|
||||
options: ['some text to copy'],
|
||||
|
||||
@@ -9,14 +9,21 @@ import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
import { Button, ButtonProps } from './Button';
|
||||
import { Icon, IconName } from './Icon';
|
||||
import { IconButton } from './IconButton';
|
||||
import { WithTooltip } from './WithTooltip';
|
||||
|
||||
export type CopyButtonProps = {
|
||||
value?: string;
|
||||
shownAsText?: boolean;
|
||||
buttonType?: 'text' | 'icon' | 'default';
|
||||
children?: React.ReactNode;
|
||||
} & ButtonProps;
|
||||
|
||||
export const CopyButton = ({ value, shownAsText, children, ...buttonProps }: CopyButtonProps) => {
|
||||
export const CopyButton = ({
|
||||
value,
|
||||
buttonType = 'default',
|
||||
children,
|
||||
...buttonProps
|
||||
}: CopyButtonProps) => {
|
||||
const stringGetter = useStringGetter();
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
@@ -28,11 +35,23 @@ export const CopyButton = ({ value, shownAsText, children, ...buttonProps }: Cop
|
||||
setTimeout(() => setCopied(false), 500);
|
||||
};
|
||||
|
||||
return shownAsText ? (
|
||||
return buttonType === 'text' ? (
|
||||
<Styled.InlineRow onClick={onCopy} copied={copied}>
|
||||
{children}
|
||||
<Icon iconName={IconName.Copy} />
|
||||
<Styled.Icon copied={copied} iconName={copied ? IconName.Check : IconName.Copy} />
|
||||
</Styled.InlineRow>
|
||||
) : buttonType === 'icon' ? (
|
||||
<WithTooltip
|
||||
tooltipString={stringGetter({ key: copied ? STRING_KEYS.COPIED : STRING_KEYS.COPY })}
|
||||
>
|
||||
<Styled.IconButton
|
||||
{...buttonProps}
|
||||
copied={copied}
|
||||
action={ButtonAction.Base}
|
||||
iconName={copied ? IconName.Check : IconName.Copy}
|
||||
onClick={onCopy}
|
||||
/>
|
||||
</WithTooltip>
|
||||
) : (
|
||||
<Button
|
||||
{...buttonProps}
|
||||
@@ -63,3 +82,21 @@ Styled.InlineRow = styled.div<{ copied: boolean }>`
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
Styled.Icon = styled(Icon)<{ copied: boolean }>`
|
||||
${({ copied }) =>
|
||||
copied &&
|
||||
css`
|
||||
color: var(--color-positive);
|
||||
`}
|
||||
`;
|
||||
|
||||
Styled.IconButton = styled(IconButton)<{ copied: boolean }>`
|
||||
${({ copied }) =>
|
||||
copied &&
|
||||
css`
|
||||
svg {
|
||||
color: var(--color-positive);
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
@@ -38,6 +38,7 @@ type ElementProps = {
|
||||
slotTrigger?: React.ReactNode;
|
||||
slotHeaderInner?: React.ReactNode;
|
||||
slotFooter?: React.ReactNode;
|
||||
withClose?: boolean;
|
||||
};
|
||||
|
||||
type StyleProps = {
|
||||
@@ -46,6 +47,7 @@ type StyleProps = {
|
||||
hasHeaderBorder?: boolean;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
withAnimation?: boolean;
|
||||
};
|
||||
|
||||
export type DialogProps = ElementProps & StyleProps;
|
||||
@@ -79,9 +81,11 @@ export const Dialog = ({
|
||||
slotTrigger,
|
||||
slotHeaderInner,
|
||||
slotFooter,
|
||||
withClose = true,
|
||||
placement = DialogPlacement.Default,
|
||||
portalContainer,
|
||||
hasHeaderBorder = false,
|
||||
withAnimation = false,
|
||||
children,
|
||||
className,
|
||||
}: DialogProps) => {
|
||||
@@ -105,6 +109,7 @@ export const Dialog = ({
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
$withAnimation={withAnimation}
|
||||
>
|
||||
<Styled.Header $withBorder={hasHeaderBorder}>
|
||||
<Styled.HeaderTopRow>
|
||||
@@ -114,7 +119,7 @@ export const Dialog = ({
|
||||
|
||||
{title && <Styled.Title>{title}</Styled.Title>}
|
||||
|
||||
{!preventClose && (
|
||||
{!preventClose && withClose && (
|
||||
<Styled.Close ref={closeButtonRef}>
|
||||
<Icon iconName={IconName.Close} />
|
||||
</Styled.Close>
|
||||
@@ -168,7 +173,7 @@ Styled.Overlay = styled(Overlay)`
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.Container = styled(Content)<{ placement: DialogPlacement }>`
|
||||
Styled.Container = styled(Content)<{ placement: DialogPlacement; $withAnimation?: boolean }>`
|
||||
/* Params */
|
||||
--dialog-inset: 1rem;
|
||||
--dialog-width: 30rem;
|
||||
@@ -224,7 +229,7 @@ Styled.Container = styled(Content)<{ placement: DialogPlacement }>`
|
||||
|
||||
outline: none;
|
||||
|
||||
${({ placement }) =>
|
||||
${({ placement, $withAnimation }) =>
|
||||
({
|
||||
[DialogPlacement.Default]: css`
|
||||
inset: var(--dialog-inset);
|
||||
@@ -260,9 +265,11 @@ Styled.Container = styled(Content)<{ placement: DialogPlacement }>`
|
||||
padding-bottom: var(--dialog-radius); */
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
&[data-state='open'] {
|
||||
animation: ${keyframes`
|
||||
${$withAnimation &&
|
||||
css`
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
&[data-state='open'] {
|
||||
animation: ${keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -270,18 +277,19 @@ Styled.Container = styled(Content)<{ placement: DialogPlacement }>`
|
||||
max-height: 0;
|
||||
}
|
||||
`} 0.15s var(--ease-out-expo);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-state='closed'] {
|
||||
animation: ${keyframes`
|
||||
&[data-state='closed'] {
|
||||
animation: ${keyframes`
|
||||
to {
|
||||
opacity: 0;
|
||||
scale: 0.9;
|
||||
max-height: 0;
|
||||
}
|
||||
`} 0.15s;
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
`,
|
||||
[DialogPlacement.Sidebar]: css`
|
||||
--dialog-width: var(--sidebar-width);
|
||||
@@ -291,50 +299,52 @@ Styled.Container = styled(Content)<{ placement: DialogPlacement }>`
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
&[data-state='open'] {
|
||||
animation: ${keyframes`
|
||||
${$withAnimation &&
|
||||
css`
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
&[data-state='open'] {
|
||||
animation: ${keyframes`
|
||||
from {
|
||||
translate: 100% 0;
|
||||
opacity: 0;
|
||||
}
|
||||
`} 0.15s var(--ease-out-expo);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-state='closed'] {
|
||||
animation: ${keyframes`
|
||||
&[data-state='closed'] {
|
||||
animation: ${keyframes`
|
||||
to {
|
||||
translate: 100% 0;
|
||||
opacity: 0;
|
||||
}
|
||||
`} 0.15s var(--ease-out-expo);
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
`,
|
||||
[DialogPlacement.Inline]: css`
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
&[data-state='open'] {
|
||||
animation: ${keyframes`
|
||||
${$withAnimation &&
|
||||
css`
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
&[data-state='open'] {
|
||||
animation: ${keyframes`
|
||||
from {
|
||||
scale: 0.99;
|
||||
opacity: 0;
|
||||
/* filter: blur(2px); */
|
||||
/* backdrop-filter: none; */
|
||||
}
|
||||
`} 0.15s var(--ease-out-expo);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-state='closed'] {
|
||||
animation: ${keyframes`
|
||||
&[data-state='closed'] {
|
||||
animation: ${keyframes`
|
||||
to {
|
||||
scale: 0.99;
|
||||
opacity: 0;
|
||||
/* filter: blur(2px); */
|
||||
/* backdrop-filter: none; */
|
||||
}
|
||||
`} 0.15s var(--ease-out-expo);
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
`,
|
||||
[DialogPlacement.FullScreen]: css`
|
||||
--dialog-width: 100vw;
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
RadioItem,
|
||||
Portal,
|
||||
} from '@radix-ui/react-dropdown-menu';
|
||||
import { CheckIcon } from '@radix-ui/react-icons';
|
||||
|
||||
import { type MenuItem } from '@/constants/menus';
|
||||
|
||||
@@ -90,7 +91,9 @@ export const DropdownSelectMenu = <MenuItemValue extends string>({
|
||||
|
||||
{slotAfter}
|
||||
|
||||
<Styled.ItemIndicator>✔{/* <CheckIcon /> */}</Styled.ItemIndicator>
|
||||
<Styled.ItemIndicator>
|
||||
<CheckIcon />
|
||||
</Styled.ItemIndicator>
|
||||
</Styled.RadioItem>
|
||||
))}
|
||||
</RadioGroup>
|
||||
|
||||
@@ -1,43 +1,16 @@
|
||||
import styled, { AnyStyledComponent } from 'styled-components';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
import { Icon, IconName } from '@/components/Icon';
|
||||
|
||||
export const GreenCheckCircle = ({ className }: { className?: string }) => (
|
||||
<Styled.GreenCheckCircle className={className}>
|
||||
<Icon iconName={IconName.Check} />
|
||||
</Styled.GreenCheckCircle>
|
||||
<Styled.Icon className={className} iconName={IconName.CheckCircle} />
|
||||
);
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.GreenCheckCircle = styled.div`
|
||||
${layoutMixins.stack}
|
||||
|
||||
Styled.Icon = styled(Icon)`
|
||||
--icon-size: 1.25rem;
|
||||
--icon-border-width: 3px;
|
||||
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
border-radius: 50%;
|
||||
|
||||
align-items: center;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
width: calc(var(--icon-size) - var(--icon-border-width) * 2);
|
||||
height: calc(var(--icon-size) - var(--icon-border-width) * 2);
|
||||
border: solid var(--icon-border-width) var(--color-positive);
|
||||
border-radius: 50%;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: calc(var(--icon-size) / 3);
|
||||
height: calc(var(--icon-size) / 3);
|
||||
|
||||
color: var(--color-positive);
|
||||
justify-self: center;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
CautionCircleIcon,
|
||||
ChatIcon,
|
||||
CheckIcon,
|
||||
CheckCircleIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ClockIcon,
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
HelpCircleIcon,
|
||||
HideIcon,
|
||||
HistoryIcon,
|
||||
LeaderboardIcon,
|
||||
LinkOutIcon,
|
||||
LockIcon,
|
||||
LogoShortIcon,
|
||||
@@ -52,6 +54,8 @@ import {
|
||||
OrderUntriggeredIcon,
|
||||
OverviewIcon,
|
||||
PencilIcon,
|
||||
PlayIcon,
|
||||
PlusIcon,
|
||||
PositionsIcon,
|
||||
PriceChartIcon,
|
||||
PrivacyIcon,
|
||||
@@ -86,6 +90,7 @@ export enum IconName {
|
||||
CautionCircleStroked = 'CautionCircleStroked',
|
||||
Chat = 'Chat',
|
||||
Check = 'Check',
|
||||
CheckCircle = 'CheckCircle',
|
||||
ChevronLeft = 'ChevronLeft',
|
||||
ChevronRight = 'ChevronRight',
|
||||
Clock = 'Clock',
|
||||
@@ -108,6 +113,7 @@ export enum IconName {
|
||||
HelpCircle = 'HelpCircle',
|
||||
Hide = 'Hide',
|
||||
History = 'History',
|
||||
Leaderboard = 'Leaderboard',
|
||||
LinkOut = 'LinkOut',
|
||||
Lock = 'Lock',
|
||||
LogoShort = 'LogoShort',
|
||||
@@ -124,6 +130,8 @@ export enum IconName {
|
||||
OrderUntriggered = 'OrderUntriggered',
|
||||
Overview = 'Overview',
|
||||
Pencil = 'Pencil',
|
||||
Play = 'Play',
|
||||
Plus = 'Plus',
|
||||
Positions = 'Positions',
|
||||
PriceChart = 'PriceChart',
|
||||
Privacy = 'Privacy',
|
||||
@@ -158,6 +166,7 @@ const icons = {
|
||||
[IconName.CautionCircleStroked]: CautionCircleStrokeIcon,
|
||||
[IconName.Chat]: ChatIcon,
|
||||
[IconName.Check]: CheckIcon,
|
||||
[IconName.CheckCircle]: CheckCircleIcon,
|
||||
[IconName.ChevronLeft]: ChevronLeftIcon,
|
||||
[IconName.ChevronRight]: ChevronRightIcon,
|
||||
[IconName.Clock]: ClockIcon,
|
||||
@@ -180,6 +189,7 @@ const icons = {
|
||||
[IconName.HelpCircle]: HelpCircleIcon,
|
||||
[IconName.Hide]: HideIcon,
|
||||
[IconName.History]: HistoryIcon,
|
||||
[IconName.Leaderboard]: LeaderboardIcon,
|
||||
[IconName.LinkOut]: LinkOutIcon,
|
||||
[IconName.Lock]: LockIcon,
|
||||
[IconName.LogoShort]: LogoShortIcon,
|
||||
@@ -195,6 +205,8 @@ const icons = {
|
||||
[IconName.OrderUntriggered]: OrderUntriggeredIcon,
|
||||
[IconName.Overview]: OverviewIcon,
|
||||
[IconName.Pencil]: PencilIcon,
|
||||
[IconName.Play]: PlayIcon,
|
||||
[IconName.Plus]: PlusIcon,
|
||||
[IconName.Positions]: PositionsIcon,
|
||||
[IconName.PriceChart]: PriceChartIcon,
|
||||
[IconName.Privacy]: PrivacyIcon,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
USD_DECIMALS,
|
||||
} from '@/constants/numbers';
|
||||
|
||||
import { BIG_NUMBERS, MustBigNumber } from '@/lib/numbers';
|
||||
import { BIG_NUMBERS } from '@/lib/numbers';
|
||||
import { useLocaleSeparators } from '@/hooks';
|
||||
|
||||
export enum InputType {
|
||||
@@ -29,31 +29,38 @@ type StyleProps = {
|
||||
type ElementProps = {
|
||||
type?: InputType;
|
||||
value?: string | number | null;
|
||||
|
||||
allowNegative?: boolean;
|
||||
decimals?: number;
|
||||
disabled?: boolean;
|
||||
id?: string;
|
||||
max?: number;
|
||||
onBlur?: () => void;
|
||||
onChange?:
|
||||
| Dispatch<SetStateAction<string>>
|
||||
| React.ReactEventHandler<HTMLInputElement>
|
||||
| ((values: NumberFormatValues, e: SourceInfo) => void);
|
||||
onFocus?: () => void;
|
||||
onInput?: ({
|
||||
value,
|
||||
floatValue,
|
||||
formattedValue,
|
||||
}: {
|
||||
value: string;
|
||||
floatValue?: number;
|
||||
formattedValue: string;
|
||||
}) => void;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export type InputProps = ElementProps & StyleProps;
|
||||
type ConditionalProps =
|
||||
| {
|
||||
allowNegative?: boolean;
|
||||
decimals?: number;
|
||||
max?: number;
|
||||
onChange?: (values: NumberFormatValues, e: SourceInfo) => void;
|
||||
onInput?: ({
|
||||
value,
|
||||
floatValue,
|
||||
formattedValue,
|
||||
}: {
|
||||
value: string;
|
||||
floatValue?: number;
|
||||
formattedValue: string;
|
||||
}) => void;
|
||||
}
|
||||
| {
|
||||
allowNegative?: never;
|
||||
decimals?: never;
|
||||
max?: never;
|
||||
onChange?: Dispatch<SetStateAction<string>> | React.ReactEventHandler<HTMLInputElement>;
|
||||
onInput?: (e: SyntheticInputEvent) => void;
|
||||
};
|
||||
|
||||
export type InputProps = ElementProps & StyleProps & ConditionalProps;
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
(
|
||||
@@ -129,6 +136,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
// Other
|
||||
data-1p-ignore // prevent 1Password fill
|
||||
{...otherProps}
|
||||
/>
|
||||
) : (
|
||||
@@ -137,6 +145,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
ref={ref as React.Ref<typeof NumericFormat<unknown>>}
|
||||
id={id}
|
||||
// NumericFormat
|
||||
valueIsNumericString
|
||||
allowNegative={allowNegative}
|
||||
decimalScale={decimals}
|
||||
decimalSeparator={LOCALE_DECIMAL_SEPARATOR}
|
||||
@@ -160,7 +169,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
? undefined
|
||||
: Number(formattedValue.replace(',', '.'));
|
||||
|
||||
onInput?.({ value, floatValue, formattedValue });
|
||||
onInput?.({ value, floatValue, formattedValue, ...e });
|
||||
}}
|
||||
// Native
|
||||
disabled={disabled}
|
||||
@@ -168,6 +177,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
value={formattedValue}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
data-1p-ignore // prevent 1Password fill
|
||||
{...otherProps}
|
||||
/>
|
||||
)}
|
||||
@@ -188,7 +198,6 @@ Styled.InputContainer = styled.div`
|
||||
border-radius: inherit;
|
||||
|
||||
input {
|
||||
user-select: all;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { forwardRef, Ref, useEffect, useRef, useState } from 'react';
|
||||
import { forwardRef, Ref } from 'react';
|
||||
import styled, { type AnyStyledComponent, css, keyframes } from 'styled-components';
|
||||
import { NavLink, matchPath, useLocation } from 'react-router-dom';
|
||||
|
||||
@@ -21,8 +21,6 @@ import {
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
import { isTruthy } from '@/lib/isTruthy';
|
||||
|
||||
import { Tag } from './Tag';
|
||||
import { Icon, IconName } from './Icon';
|
||||
|
||||
@@ -61,7 +59,6 @@ const NavItem = forwardRef(
|
||||
<>
|
||||
{slotBefore}
|
||||
<span>
|
||||
{/* {`${label}${subitems?.length ? ' ' : ''}`} */}
|
||||
{label}
|
||||
{tag && (
|
||||
<>
|
||||
@@ -100,8 +97,6 @@ const NavItem = forwardRef(
|
||||
}
|
||||
);
|
||||
|
||||
type TriggerRef = HTMLAnchorElement | HTMLDivElement | HTMLButtonElement | null;
|
||||
|
||||
export const NavigationMenu = <MenuItemValue extends string, MenuGroupValue extends string>({
|
||||
onSelectItem,
|
||||
onSelectGroup,
|
||||
@@ -112,33 +107,6 @@ export const NavigationMenu = <MenuItemValue extends string, MenuGroupValue exte
|
||||
dir = 'ltr',
|
||||
className,
|
||||
}: ElementProps<MenuItemValue, MenuGroupValue> & StyleProps) => {
|
||||
// Disable click (close) in the first 500ms after hover (open)
|
||||
// https://github.com/radix-ui/primitives/issues/1630#issuecomment-1545995075
|
||||
const [clickIsDisabled, setClickIsDisabled] = useState(false);
|
||||
const triggerRefs = useRef({} as { [value: string]: TriggerRef });
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
for (const mutation of mutationsList) {
|
||||
if (
|
||||
mutation.type === 'attributes' &&
|
||||
mutation.attributeName === 'data-state' &&
|
||||
(mutation.target as unknown as HTMLOrSVGElement).dataset.state === 'open' &&
|
||||
mutation.target !== document.activeElement
|
||||
) {
|
||||
setClickIsDisabled(true);
|
||||
setTimeout(() => setClickIsDisabled(false), 500);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const element of Object.values(triggerRefs.current).filter(isTruthy)) {
|
||||
observer.observe(element, { attributes: true });
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const renderSubitems = ({
|
||||
item,
|
||||
depth = 0,
|
||||
@@ -148,18 +116,18 @@ export const NavigationMenu = <MenuItemValue extends string, MenuGroupValue exte
|
||||
}) => (
|
||||
<>
|
||||
<Styled.SubMenuTrigger
|
||||
asChild={depth > 0}
|
||||
ref={(ref: TriggerRef) => (triggerRefs.current[item.value] = ref)}
|
||||
onClick={(e: MouseEvent) => {
|
||||
if (clickIsDisabled) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
asChild
|
||||
onPointerMove={(e: MouseEvent) => e.preventDefault()}
|
||||
onPointerLeave={(e: MouseEvent) => e.preventDefault()}
|
||||
>
|
||||
<Styled.NavItem onSelect={onSelectGroup} orientation={itemOrientation} {...item} />
|
||||
</Styled.SubMenuTrigger>
|
||||
|
||||
<Styled.Content data-placement={submenuPlacement}>
|
||||
<Styled.Content
|
||||
onPointerEnter={(e: MouseEvent) => e.preventDefault()}
|
||||
onPointerLeave={(e: MouseEvent) => e.preventDefault()}
|
||||
data-placement={submenuPlacement}
|
||||
>
|
||||
<Styled.Sub data-placement={submenuPlacement}>
|
||||
<Styled.List
|
||||
data-orientation={depth > 0 ? 'menu' : orientation === 'vertical' ? 'vertical' : 'menu'}
|
||||
@@ -410,25 +378,12 @@ Styled.List = styled(List)`
|
||||
`;
|
||||
|
||||
Styled.ListItem = styled(Item)`
|
||||
/* display: contents; */
|
||||
display: grid;
|
||||
position: relative;
|
||||
|
||||
${Styled.List}[data-orientation="horizontal"] > & {
|
||||
gap: var(--submenu-side-offset);
|
||||
}
|
||||
|
||||
/* ${Styled.List}[data-orientation="menu"] > & {
|
||||
grid-template-columns: 1fr 0;
|
||||
align-items: start;
|
||||
gap: 2rem;
|
||||
} */
|
||||
|
||||
/* &:has([data-state="open"]) {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
right: 0;
|
||||
} */
|
||||
`;
|
||||
|
||||
Styled.SubMenuTrigger = styled(Trigger)`
|
||||
@@ -436,11 +391,6 @@ Styled.SubMenuTrigger = styled(Trigger)`
|
||||
outline-offset: -2px;
|
||||
|
||||
&[data-state='open'] {
|
||||
div {
|
||||
background-color: var(--navigationMenu-item-checked-backgroundColor);
|
||||
color: var(--navigationMenu-item-checked-textColor);
|
||||
}
|
||||
|
||||
svg {
|
||||
rotate: 0.5turn;
|
||||
}
|
||||
@@ -452,11 +402,10 @@ Styled.NavItem = styled(NavItem)<{ orientation: 'horizontal' | 'vertical' }>`
|
||||
subitems?.length
|
||||
? css`
|
||||
${popoverMixins.trigger}
|
||||
--trigger-backgroundColor: transparent;
|
||||
--trigger-open-backgroundColor: var(--color-layer-3);
|
||||
--trigger-open-backgroundColor: var(--navigationMenu-item-checked-backgroundColor);
|
||||
`
|
||||
: css`
|
||||
&:hover:not(.active) {
|
||||
&:hover:not(:active) {
|
||||
background-color: var(--navigationMenu-item-highlighted-backgroundColor);
|
||||
color: var(--navigationMenu-item-highlighted-textColor);
|
||||
}
|
||||
@@ -470,8 +419,6 @@ Styled.NavItem = styled(NavItem)<{ orientation: 'horizontal' | 'vertical' }>`
|
||||
--item-radius: var(--navigationMenu-item-radius);
|
||||
--item-padding: var(--navigationMenu-item-padding);
|
||||
|
||||
/* ${popoverMixins.backdropOverlay} */
|
||||
|
||||
${layoutMixins.scrollSnapItem}
|
||||
|
||||
min-height: var(--navigationMenu-item-height);
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
import React, { type MouseEvent } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import { ButtonShape, ButtonSize } from '@/constants/buttons';
|
||||
|
||||
import {
|
||||
NotificationStatus,
|
||||
type Notification as NotificationDataType,
|
||||
} from '@/constants/notifications';
|
||||
|
||||
import { useNotifications } from '@/hooks/useNotifications';
|
||||
|
||||
import { popoverMixins } from '@/styles/popoverMixins';
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
import { IconButton } from '@/components/IconButton';
|
||||
import { IconName } from '@/components/Icon';
|
||||
import { Output, OutputType } from '@/components/Output';
|
||||
|
||||
type ElementProps = {
|
||||
notification: NotificationDataType;
|
||||
onClick?: () => void;
|
||||
slotIcon?: React.ReactNode;
|
||||
slotTitle?: React.ReactNode;
|
||||
slotTitleLeft?: React.ReactNode;
|
||||
slotTitleRight?: React.ReactNode;
|
||||
slotDescription?: React.ReactNode;
|
||||
slotCustomContent?: React.ReactNode;
|
||||
slotAction?: React.ReactNode;
|
||||
withClose?: boolean;
|
||||
};
|
||||
|
||||
type StyleProps = {
|
||||
className?: string;
|
||||
isToast?: boolean;
|
||||
};
|
||||
|
||||
export type NotificationProps = ElementProps & StyleProps;
|
||||
|
||||
export const Notification = ({
|
||||
className,
|
||||
isToast,
|
||||
notification,
|
||||
onClick,
|
||||
slotIcon,
|
||||
slotTitle,
|
||||
slotTitleLeft,
|
||||
slotTitleRight,
|
||||
slotDescription,
|
||||
slotCustomContent,
|
||||
slotAction,
|
||||
withClose = !isToast,
|
||||
}: NotificationProps) => {
|
||||
const { markCleared, markSeen } = useNotifications();
|
||||
|
||||
return (
|
||||
<$Container className={className} isToast={isToast} onClick={onClick}>
|
||||
<$Header>
|
||||
{slotTitleLeft ?? (slotIcon && <$Icon>{slotIcon}</$Icon>)}
|
||||
<$Title>{slotTitle}</$Title>
|
||||
{slotTitleRight}
|
||||
{withClose && !isToast && (
|
||||
<$ActionItems>
|
||||
<$Output
|
||||
type={OutputType.RelativeTime}
|
||||
value={notification.timestamps[NotificationStatus.Triggered]}
|
||||
/>
|
||||
|
||||
{notification.status < NotificationStatus.Seen ? <$UnreadIndicator /> : null}
|
||||
|
||||
{notification.status < NotificationStatus.Cleared ? (
|
||||
<$IconButton
|
||||
iconName={IconName.Close}
|
||||
shape={ButtonShape.Square}
|
||||
size={ButtonSize.XSmall}
|
||||
onClick={(e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (notification.status < NotificationStatus.Seen) {
|
||||
markSeen(notification);
|
||||
} else if (notification.status < NotificationStatus.Cleared) {
|
||||
markCleared(notification);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</$ActionItems>
|
||||
)}
|
||||
</$Header>
|
||||
<$Description>{slotCustomContent ?? slotDescription}</$Description>
|
||||
{slotAction && <$Action>{slotAction}</$Action>}
|
||||
</$Container>
|
||||
);
|
||||
};
|
||||
|
||||
const $Container = styled.div<{ isToast?: boolean }>`
|
||||
// Params
|
||||
--toast-icon-size: 1.75em;
|
||||
|
||||
// Rules
|
||||
${popoverMixins.popover}
|
||||
overflow: visible;
|
||||
padding: 1rem 1.25rem;
|
||||
|
||||
${({ isToast }) =>
|
||||
isToast
|
||||
? css`
|
||||
padding: 1rem;
|
||||
box-shadow: 0 0 0 var(--border-width) var(--color-border),
|
||||
// border
|
||||
0 0 0.5rem 0.1rem var(--color-layer-2); // shadow
|
||||
`
|
||||
: css`
|
||||
flex: 1;
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
backdrop-filter: none;
|
||||
`}
|
||||
`;
|
||||
|
||||
const $Header = styled.header`
|
||||
${layoutMixins.row}
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const $Icon = styled.div`
|
||||
${layoutMixins.row}
|
||||
float: left;
|
||||
|
||||
margin-right: 0.5rem;
|
||||
|
||||
line-height: 1;
|
||||
|
||||
> svg,
|
||||
img {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const $Title = styled.div`
|
||||
flex: 1;
|
||||
|
||||
font: var(--font-base-medium);
|
||||
color: var(--color-text-2);
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
const $Description = styled.div`
|
||||
margin-top: 0.5rem;
|
||||
color: var(--color-text-0);
|
||||
font: var(--font-small-book);
|
||||
`;
|
||||
|
||||
const $Action = styled.div`
|
||||
margin-top: 0.5rem;
|
||||
font: var(--font-small-book);
|
||||
`;
|
||||
|
||||
const $ActionItems = styled.div`
|
||||
min-width: fit-content;
|
||||
padding-left: 0.5rem;
|
||||
height: 100%;
|
||||
${layoutMixins.row};
|
||||
justify-content: end;
|
||||
gap: 0.5rem;
|
||||
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
|
||||
background-color: var(--color-layer-2);
|
||||
font: var(--font-mini-book);
|
||||
|
||||
display: none;
|
||||
z-index: 1;
|
||||
|
||||
${$Container}:hover & {
|
||||
display: flex;
|
||||
}
|
||||
`;
|
||||
|
||||
const $UnreadIndicator = styled.div`
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-accent);
|
||||
border: 1px solid var(--color-layer-2);
|
||||
`;
|
||||
|
||||
const $Output = styled(Output)`
|
||||
color: var(--color-text-0);
|
||||
`;
|
||||
|
||||
const $IconButton = styled(IconButton)`
|
||||
--button-border: none;
|
||||
--button-textColor: var(--color-text-0);
|
||||
--button-hover-textColor: var(--color-text-1);
|
||||
`;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import styled, { type AnyStyledComponent, css } from 'styled-components';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
@@ -22,6 +22,11 @@ export const SearchInput = ({
|
||||
}: SearchInputProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [value, setValue] = useState('');
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) inputRef?.current?.focus();
|
||||
}, [inputRef, isOpen]);
|
||||
|
||||
return (
|
||||
<Styled.Search>
|
||||
@@ -41,6 +46,7 @@ export const SearchInput = ({
|
||||
/>
|
||||
<Styled.Input
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
value={value}
|
||||
isOpen={isOpen}
|
||||
type={type}
|
||||
|
||||
@@ -12,6 +12,8 @@ import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
|
||||
import { WithLabel } from '@/components/WithLabel';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
import { formMixins } from '@/styles/formMixins';
|
||||
import breakpoints from '@/styles/breakpoints';
|
||||
|
||||
type ElementProps = {
|
||||
asChild?: boolean;
|
||||
@@ -84,27 +86,30 @@ export const SearchSelectMenu = ({
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.SearchSelectMenu = styled.div`
|
||||
${layoutMixins.column}
|
||||
`;
|
||||
|
||||
Styled.MenuTrigger = styled.div`
|
||||
height: var(--form-input-height);
|
||||
|
||||
${layoutMixins.spacedRow}
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
padding: var(--form-input-paddingY) var(--form-input-paddingX);
|
||||
|
||||
@media ${breakpoints.tablet} {
|
||||
height: var(--form-input-height-mobile);
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.WithLabel = styled(WithLabel)`
|
||||
--label-textColor: var(--color-text-0);
|
||||
gap: 0.25rem;
|
||||
${formMixins.inputLabel}
|
||||
|
||||
label {
|
||||
font: var(--font-mini-book);
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.SearchSelectMenu = styled.div`
|
||||
${layoutMixins.column}
|
||||
`;
|
||||
|
||||
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
|
||||
--withReceipt-backgroundColor: var(--color-layer-2);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import styled, { type AnyStyledComponent } from 'styled-components';
|
||||
import styled, { css, type AnyStyledComponent } from 'styled-components';
|
||||
|
||||
import {
|
||||
Root,
|
||||
@@ -13,11 +13,13 @@ import {
|
||||
Portal,
|
||||
Icon as SelectIcon,
|
||||
} from '@radix-ui/react-select';
|
||||
import { CheckIcon } from '@radix-ui/react-icons';
|
||||
|
||||
import { popoverMixins } from '@/styles/popoverMixins';
|
||||
import { formMixins } from '@/styles/formMixins';
|
||||
|
||||
import { WithLabel } from '@/components/WithLabel';
|
||||
import { Icon, IconName } from './Icon';
|
||||
|
||||
export const SelectMenu = <T extends string>({
|
||||
children,
|
||||
@@ -44,7 +46,9 @@ export const SelectMenu = <T extends string>({
|
||||
) : (
|
||||
<Value />
|
||||
)}
|
||||
{React.Children.toArray(children).length > 1 && <Styled.DropdownIcon />}
|
||||
{React.Children.toArray(children).length > 1 && (
|
||||
<Styled.TriggerIcon iconName={IconName.Triangle} />
|
||||
)}
|
||||
</Styled.Trigger>
|
||||
<Portal>
|
||||
<Styled.Content className={className}>
|
||||
@@ -68,7 +72,9 @@ export const SelectItem = <T extends string>({
|
||||
}) => (
|
||||
<Styled.Item className={className} value={value}>
|
||||
<ItemText>{label}</ItemText>
|
||||
<Styled.ItemIndicator>✔{/* <CheckIcon /> */}</Styled.ItemIndicator>
|
||||
<Styled.ItemIndicator>
|
||||
<CheckIcon />
|
||||
</Styled.ItemIndicator>
|
||||
</Styled.Item>
|
||||
);
|
||||
|
||||
@@ -88,12 +94,6 @@ Styled.Trigger = styled(Trigger)<{ $withBlur?: boolean }>`
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.DropdownIcon = styled(SelectIcon)`
|
||||
font-size: 0.675em;
|
||||
color: var(--color-text-1);
|
||||
margin-left: auto;
|
||||
`;
|
||||
|
||||
Styled.Content = styled(Content)`
|
||||
--select-menu-content-maxWidth: ;
|
||||
max-width: var(--select-menu-content-maxWidth);
|
||||
@@ -116,3 +116,9 @@ Styled.WithLabel = styled(WithLabel)`
|
||||
${formMixins.inputLabel}
|
||||
border-radius: 0;
|
||||
`;
|
||||
|
||||
Styled.TriggerIcon = styled(Icon)`
|
||||
width: 0.625rem;
|
||||
height: 0.375rem;
|
||||
color: var(--color-text-0);
|
||||
`;
|
||||
|
||||
@@ -43,6 +43,7 @@ import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
import { Icon, IconName } from './Icon';
|
||||
import { Tag } from './Tag';
|
||||
import { MustBigNumber } from '@/lib/numbers';
|
||||
|
||||
export { TableCell } from './Table/TableCell';
|
||||
export { TableColumnHeader } from './Table/TableColumnHeader';
|
||||
@@ -161,10 +162,7 @@ export const Table = <TableRowData extends object, TableRowKey extends Key>({
|
||||
? // String
|
||||
collator.compare(first as string, second as string)
|
||||
: // Number
|
||||
Math.sign(
|
||||
(parseInt(first as string) || (first as number)) -
|
||||
(parseInt(second as string) || (second as number))
|
||||
)) *
|
||||
MustBigNumber(first).comparedTo(MustBigNumber(second))) *
|
||||
// Flip the direction if descending order is specified.
|
||||
(sortDirection === 'descending' ? -1 : 1)
|
||||
);
|
||||
@@ -831,6 +829,7 @@ Styled.SortArrow = styled.span<{ sortDirection: 'ascending' | 'descending' }>`
|
||||
Styled.Thead = styled.thead<StyleProps>`
|
||||
${layoutMixins.stickyHeader}
|
||||
scroll-snap-align: none;
|
||||
font: var(--font-mini-book);
|
||||
|
||||
> * {
|
||||
height: var(--stickyArea-topHeight);
|
||||
@@ -839,18 +838,17 @@ Styled.Thead = styled.thead<StyleProps>`
|
||||
color: var(--tableHeader-textColor);
|
||||
background-color: var(--tableHeader-backgroundColor);
|
||||
|
||||
@media ${breakpoints.notTablet} {
|
||||
${({ withInnerBorders, withGradientCardRows }) =>
|
||||
withInnerBorders &&
|
||||
!withGradientCardRows &&
|
||||
css`
|
||||
${layoutMixins.withInnerHorizontalBorders}
|
||||
`}
|
||||
}
|
||||
${({ withInnerBorders, withGradientCardRows }) =>
|
||||
withInnerBorders &&
|
||||
!withGradientCardRows &&
|
||||
css`
|
||||
${layoutMixins.withInnerHorizontalBorders}
|
||||
`}
|
||||
`;
|
||||
|
||||
Styled.Tbody = styled.tbody<StyleProps>`
|
||||
${layoutMixins.stickyArea2}
|
||||
font: var(--font-small-book);
|
||||
|
||||
// If <table> height is fixed with not enough rows to overflow, vertically center the rows
|
||||
&:before,
|
||||
@@ -869,6 +867,10 @@ Styled.Tbody = styled.tbody<StyleProps>`
|
||||
--stickyArea2-paddingBottom: var(--border-width);
|
||||
--stickyArea2-paddingLeft: var(--border-width);
|
||||
--stickyArea2-paddingRight: var(--border-width);
|
||||
|
||||
tr:first-of-type {
|
||||
box-shadow: none;
|
||||
}
|
||||
`}
|
||||
|
||||
${({ withGradientCardRows }) =>
|
||||
|
||||
@@ -21,6 +21,7 @@ export type TabItem<TabItemsValue> = {
|
||||
content?: React.ReactNode;
|
||||
subitems?: TabItem<TabItemsValue>[];
|
||||
customTrigger?: ReactNode;
|
||||
asChild?: boolean;
|
||||
};
|
||||
|
||||
type ElementProps<TabItemsValue> = {
|
||||
@@ -41,6 +42,8 @@ type StyleProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export type TabsProps<TabItemsValue> = ElementProps<TabItemsValue> & StyleProps;
|
||||
|
||||
export const Tabs = <TabItemsValue extends string>({
|
||||
defaultValue,
|
||||
value,
|
||||
@@ -107,9 +110,10 @@ export const Tabs = <TabItemsValue extends string>({
|
||||
sharedContent
|
||||
) : (
|
||||
<Styled.Stack>
|
||||
{items.map(({ value, content, forceMount }) => (
|
||||
{items.map(({ asChild, value, content, forceMount }) => (
|
||||
<Styled.Content
|
||||
key={value}
|
||||
asChild={asChild}
|
||||
value={value}
|
||||
forceMount={forceMount}
|
||||
$hide={forceMount && currentItem?.value !== value}
|
||||
@@ -242,7 +246,6 @@ Styled.Trigger = styled(Trigger)<{ $withBorders?: boolean }>`
|
||||
|
||||
Styled.Stack = styled.div`
|
||||
${layoutMixins.stack}
|
||||
${layoutMixins.perspectiveArea}
|
||||
|
||||
box-shadow: none;
|
||||
`;
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Root, Title, Description, Action, Close } from '@radix-ui/react-toast';
|
||||
import { type MouseEvent, useEffect, useRef, useState } from 'react';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
import { Root, Action, Close } from '@radix-ui/react-toast';
|
||||
|
||||
import { ButtonShape, ButtonSize } from '@/constants/buttons';
|
||||
import { popoverMixins } from '@/styles/popoverMixins';
|
||||
import { breakpoints } from '@/styles';
|
||||
|
||||
import { Notification, type NotificationProps } from '@/components/Notification';
|
||||
|
||||
import { IconButton } from './IconButton';
|
||||
import { CloseIcon } from '@/icons';
|
||||
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
import { popoverMixins } from '@/styles/popoverMixins';
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
import { IconName } from './Icon';
|
||||
|
||||
type ElementProps = {
|
||||
isOpen?: boolean;
|
||||
setIsOpen?: (isOpen: boolean, isClosedFromTimeout?: boolean) => void;
|
||||
slotIcon?: React.ReactNode;
|
||||
slotTitle?: React.ReactNode;
|
||||
slotDescription?: React.ReactNode;
|
||||
slotCustomContent?: React.ReactNode;
|
||||
slotAction?: React.ReactNode;
|
||||
actionDescription?: string;
|
||||
actionAltText?: string;
|
||||
sensitivity?: 'foreground' | 'background';
|
||||
@@ -26,13 +21,22 @@ type ElementProps = {
|
||||
lastUpdated?: number;
|
||||
};
|
||||
|
||||
type StyleProps = {};
|
||||
type StyleProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
type ToastProps = NotificationProps & ElementProps & StyleProps;
|
||||
|
||||
export const Toast = ({
|
||||
className,
|
||||
isOpen = true,
|
||||
notification,
|
||||
onClick,
|
||||
setIsOpen,
|
||||
slotIcon,
|
||||
slotTitle,
|
||||
slotTitleLeft,
|
||||
slotTitleRight,
|
||||
slotDescription,
|
||||
slotCustomContent,
|
||||
slotAction,
|
||||
@@ -41,7 +45,7 @@ export const Toast = ({
|
||||
sensitivity = 'background',
|
||||
duration = Infinity,
|
||||
lastUpdated,
|
||||
}: ElementProps & StyleProps) => {
|
||||
}: ToastProps) => {
|
||||
// Timeout
|
||||
const timeout = useRef<number>();
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
@@ -58,37 +62,45 @@ export const Toast = ({
|
||||
|
||||
return (
|
||||
<$Root
|
||||
className={className}
|
||||
type={sensitivity}
|
||||
duration={Infinity}
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
onPause={() => setIsPaused(true)}
|
||||
onResume={() => setIsPaused(false)}
|
||||
onClick={() => {
|
||||
setIsOpen?.(false, false);
|
||||
onClick?.();
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<$Container>
|
||||
<$Header>
|
||||
{slotIcon && <$Icon>{slotIcon}</$Icon>}
|
||||
<Close asChild>
|
||||
<$CloseButton
|
||||
iconName={IconName.Close}
|
||||
shape={ButtonShape.Circle}
|
||||
size={ButtonSize.XSmall}
|
||||
onClick={(e: MouseEvent) => e.stopPropagation()}
|
||||
/>
|
||||
</Close>
|
||||
|
||||
<Close asChild>
|
||||
<$CloseButton
|
||||
iconComponent={CloseIcon}
|
||||
shape={ButtonShape.Square}
|
||||
size={ButtonSize.XSmall}
|
||||
/>
|
||||
</Close>
|
||||
|
||||
<$Title>{slotTitle}</$Title>
|
||||
</$Header>
|
||||
{!slotCustomContent && <$Description>{slotDescription}</$Description>}
|
||||
{slotCustomContent}
|
||||
{actionDescription && (
|
||||
<$Action asChild altText={actionAltText}>
|
||||
{slotAction}
|
||||
</$Action>
|
||||
)}
|
||||
</$Container>
|
||||
</div>
|
||||
{slotCustomContent ?? (
|
||||
<$Notification
|
||||
isToast
|
||||
notification={notification}
|
||||
slotIcon={slotIcon}
|
||||
slotTitle={slotTitle}
|
||||
slotTitleLeft={slotTitleLeft}
|
||||
slotTitleRight={slotTitleRight}
|
||||
slotDescription={slotDescription}
|
||||
slotAction={
|
||||
actionDescription && (
|
||||
<$Action asChild altText={actionAltText}>
|
||||
{slotAction}
|
||||
</$Action>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</$Root>
|
||||
);
|
||||
};
|
||||
@@ -114,7 +126,7 @@ const $Root = styled(Root)`
|
||||
translate: var(--x) var(--y);
|
||||
will-change: translate, margin-top;
|
||||
|
||||
margin-bottom: var(--toasts-gap);
|
||||
/* margin-bottom: var(--toasts-gap); */
|
||||
|
||||
&[data-swipe-direction='right'] {
|
||||
&[data-state='open'] {
|
||||
@@ -163,6 +175,10 @@ const $Root = styled(Root)`
|
||||
cursor: n-resize;
|
||||
}
|
||||
|
||||
&:focus:not([data-swipe='end']) & {
|
||||
outline: var(--color-accent) 1px solid;
|
||||
}
|
||||
|
||||
&[data-swipe='move'] {
|
||||
transition-property: opacity;
|
||||
opacity: 0.98;
|
||||
@@ -171,6 +187,10 @@ const $Root = styled(Root)`
|
||||
* {
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
& > * {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-state='closed'],
|
||||
@@ -198,69 +218,48 @@ const $Root = styled(Root)`
|
||||
}
|
||||
`;
|
||||
|
||||
const $Container = styled.div`
|
||||
const $Notification = styled(Notification)`
|
||||
// Params
|
||||
--toast-icon-size: 1.75em;
|
||||
|
||||
// Rules
|
||||
${popoverMixins.popover}
|
||||
overflow: visible;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 0 0 var(--border-width) var(--color-border),
|
||||
// border
|
||||
0 0 0.5rem 0.1rem var(--color-layer-2); // shadow
|
||||
|
||||
${$Root}:focus:not([data-swipe='end']) & {
|
||||
outline: var(--color-accent) 1px solid;
|
||||
}
|
||||
|
||||
> * {
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
${$Root}[data-swipe='move'] & > * {
|
||||
opacity: 0.5;
|
||||
}
|
||||
`;
|
||||
|
||||
const $Header = styled.header`
|
||||
display: block;
|
||||
`;
|
||||
|
||||
const $Icon = styled.div`
|
||||
${layoutMixins.row}
|
||||
|
||||
float: left;
|
||||
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
|
||||
margin-right: 0.4em;
|
||||
|
||||
line-height: 1;
|
||||
`;
|
||||
|
||||
const $CloseButton = styled(IconButton)`
|
||||
--button-textColor: var(--color-text-0);
|
||||
--button-border: none;
|
||||
--button-icon-size: 0.85em;
|
||||
display: none;
|
||||
|
||||
float: right;
|
||||
/* float: right; */
|
||||
margin: -0.42rem -0.42rem -0.42rem 0.42rem;
|
||||
`;
|
||||
|
||||
const $Title = styled(Title)`
|
||||
flex: 1;
|
||||
// Absolute
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
font: var(--font-base-medium);
|
||||
color: var(--color-text-2);
|
||||
border: solid var(--border-width) var(--color-border);
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
${$Root}:hover & {
|
||||
display: block;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
const $Description = styled(Description)`
|
||||
margin-top: 0.5rem;
|
||||
color: var(--color-text-0);
|
||||
font: var(--font-small-book);
|
||||
@media ${breakpoints.mobile} {
|
||||
display: block;
|
||||
z-index: 2;
|
||||
}
|
||||
`;
|
||||
|
||||
const $Action = styled(Action)`
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Provider, Viewport } from '@radix-ui/react-toast';
|
||||
|
||||
import styled from 'styled-components';
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
type ElementProps = {
|
||||
@@ -12,7 +13,9 @@ type StyleProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const ToastArea = ({ swipeDirection, children, className }: ElementProps & StyleProps) => (
|
||||
type ToastAreaProps = ElementProps & StyleProps;
|
||||
|
||||
export const ToastArea = ({ swipeDirection, children, className }: ToastAreaProps) => (
|
||||
<$ToastArea className={className}>
|
||||
<Provider swipeDirection={swipeDirection}>
|
||||
{children}
|
||||
@@ -34,6 +37,7 @@ const $ToastArea = styled.aside`
|
||||
> * {
|
||||
display: contents;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
|
||||
> ol {
|
||||
/* display: block; */
|
||||
@@ -43,6 +47,7 @@ const $ToastArea = styled.aside`
|
||||
// Desired display order: latest to earliest (¯\_(ツ)_/¯)
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
position: relative;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
|
||||
@@ -52,15 +52,19 @@ const Styled: Record<string, AnyStyledComponent> = {};
|
||||
Styled.BaseButton = styled(BaseButton)`
|
||||
--button-toggle-off-backgroundColor: var(--color-layer-3);
|
||||
--button-toggle-off-textColor: var(--color-text-0);
|
||||
--button-toggle-off-border: solid var(--border-width) var(--border-color);
|
||||
--button-toggle-on-backgroundColor: var(--color-layer-1);
|
||||
--button-toggle-on-textColor: var(--color-text-2);
|
||||
--button-toggle-on-border: solid var(--border-width) var(--border-color);
|
||||
|
||||
--button-backgroundColor: var(--button-toggle-off-backgroundColor);
|
||||
--button-textColor: var(--button-toggle-off-textColor);
|
||||
--button-border: var(--button-toggle-off-border);
|
||||
|
||||
&[data-state='on'],
|
||||
&[data-state='active'] {
|
||||
--button-backgroundColor: var(--button-toggle-on-backgroundColor);
|
||||
--button-textColor: var(--button-toggle-on-textColor);
|
||||
--button-border: var(--button-toggle-on-border);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -124,6 +124,8 @@ export type SubaccountPosition = Abacus.exchange.dydx.abacus.output.SubaccountPo
|
||||
export type SubaccountOrder = Abacus.exchange.dydx.abacus.output.SubaccountOrder;
|
||||
export type OrderStatus = Abacus.exchange.dydx.abacus.output.input.OrderStatus;
|
||||
export const AbacusOrderStatus = Abacus.exchange.dydx.abacus.output.input.OrderStatus;
|
||||
const abacusOrderStatuses = [...AbacusOrderStatus.values()] as const;
|
||||
export type AbacusOrderStatuses = (typeof abacusOrderStatuses)[number];
|
||||
export type SubaccountFills = Abacus.exchange.dydx.abacus.output.SubaccountFill[];
|
||||
export type SubaccountFill = Abacus.exchange.dydx.abacus.output.SubaccountFill;
|
||||
export type SubaccountFundingPayment = Abacus.exchange.dydx.abacus.output.SubaccountFundingPayment;
|
||||
@@ -209,11 +211,13 @@ type ReadonlyKeysOf<T> = {
|
||||
[K in keyof T]: IfEquals<{ [_ in K]: T[K] }, { readonly [_ in K]: T[K] }, K, never>;
|
||||
}[keyof T];
|
||||
|
||||
type KotlinIrEnumValues<T> = Exclude<ReadonlyKeysOf<T>, 'Companion' | '$serializer'>;
|
||||
export type KotlinIrEnumValues<T> = Exclude<ReadonlyKeysOf<T>, 'Companion' | '$serializer'>;
|
||||
|
||||
export const ORDER_SIDES: Record<KotlinIrEnumValues<typeof AbacusOrderSide>, OrderSide> = {
|
||||
[AbacusOrderSide.buy.name]: OrderSide.BUY,
|
||||
[AbacusOrderSide.sell.name]: OrderSide.SELL,
|
||||
[AbacusOrderSide.buy.rawValue]: OrderSide.BUY,
|
||||
[AbacusOrderSide.sell.rawValue]: OrderSide.SELL,
|
||||
};
|
||||
|
||||
export const POSITION_SIDES: Record<KotlinIrEnumValues<typeof AbacusPositionSide>, PositionSide> = {
|
||||
@@ -234,12 +238,25 @@ export const HISTORICAL_PNL_PERIODS: Record<
|
||||
|
||||
export const ORDER_STATUS_STRINGS: Record<KotlinIrEnumValues<typeof AbacusOrderStatus>, string> = {
|
||||
[AbacusOrderStatus.open.name]: STRING_KEYS.OPEN_STATUS,
|
||||
[AbacusOrderStatus.open.rawValue]: STRING_KEYS.OPEN_STATUS,
|
||||
|
||||
[AbacusOrderStatus.partiallyFilled.name]: STRING_KEYS.PARTIALLY_FILLED,
|
||||
[AbacusOrderStatus.partiallyFilled.rawValue]: STRING_KEYS.PARTIALLY_FILLED,
|
||||
|
||||
[AbacusOrderStatus.filled.name]: STRING_KEYS.ORDER_FILLED,
|
||||
[AbacusOrderStatus.filled.rawValue]: STRING_KEYS.ORDER_FILLED,
|
||||
|
||||
[AbacusOrderStatus.cancelled.name]: STRING_KEYS.CANCELED,
|
||||
[AbacusOrderStatus.cancelled.rawValue]: STRING_KEYS.CANCELED,
|
||||
|
||||
[AbacusOrderStatus.canceling.name]: STRING_KEYS.CANCELING,
|
||||
[AbacusOrderStatus.canceling.rawValue]: STRING_KEYS.CANCELING,
|
||||
|
||||
[AbacusOrderStatus.pending.name]: STRING_KEYS.PENDING,
|
||||
[AbacusOrderStatus.pending.rawValue]: STRING_KEYS.PENDING,
|
||||
|
||||
[AbacusOrderStatus.untriggered.name]: STRING_KEYS.UNTRIGGERED,
|
||||
[AbacusOrderStatus.untriggered.rawValue]: STRING_KEYS.UNTRIGGERED,
|
||||
};
|
||||
|
||||
export const TRADE_TYPES: Record<
|
||||
@@ -247,15 +264,31 @@ export const TRADE_TYPES: Record<
|
||||
Nullable<TradeTypes>
|
||||
> = {
|
||||
[AbacusOrderType.limit.name]: TradeTypes.LIMIT,
|
||||
[AbacusOrderType.limit.rawValue]: TradeTypes.LIMIT,
|
||||
|
||||
[AbacusOrderType.market.name]: TradeTypes.MARKET,
|
||||
[AbacusOrderType.market.rawValue]: TradeTypes.MARKET,
|
||||
|
||||
[AbacusOrderType.stopLimit.name]: TradeTypes.STOP_LIMIT,
|
||||
[AbacusOrderType.stopLimit.rawValue]: TradeTypes.STOP_LIMIT,
|
||||
|
||||
[AbacusOrderType.stopMarket.name]: TradeTypes.STOP_MARKET,
|
||||
[AbacusOrderType.stopMarket.rawValue]: TradeTypes.STOP_MARKET,
|
||||
|
||||
[AbacusOrderType.takeProfitLimit.name]: TradeTypes.TAKE_PROFIT,
|
||||
[AbacusOrderType.takeProfitLimit.rawValue]: TradeTypes.TAKE_PROFIT,
|
||||
|
||||
[AbacusOrderType.takeProfitMarket.name]: TradeTypes.TAKE_PROFIT_MARKET,
|
||||
[AbacusOrderType.takeProfitMarket.rawValue]: TradeTypes.TAKE_PROFIT_MARKET,
|
||||
|
||||
[AbacusOrderType.liquidated.name]: null,
|
||||
[AbacusOrderType.liquidation.name]: null,
|
||||
|
||||
[AbacusOrderType.liquidation.name]: null,
|
||||
[AbacusOrderType.liquidation.rawValue]: null,
|
||||
|
||||
[AbacusOrderType.trailingStop.name]: null,
|
||||
[AbacusOrderType.trailingStop.rawValue]: null,
|
||||
};
|
||||
|
||||
// Custom types involving Abacus
|
||||
|
||||
@@ -3,6 +3,7 @@ export enum DialogTypes {
|
||||
Deposit = 'Deposit',
|
||||
DisconnectWallet = 'DisconnectWallet',
|
||||
ExchangeOffline = 'ExchangeOffline',
|
||||
ExternalLink = 'ExternalLink',
|
||||
FillDetails = 'FillDetails',
|
||||
Help = 'Help',
|
||||
ExternalNavKeplr = 'ExternalNavKeplr',
|
||||
@@ -10,13 +11,14 @@ export enum DialogTypes {
|
||||
MobileSignIn = 'MobileSignIn',
|
||||
Onboarding = 'Onboarding',
|
||||
OrderDetails = 'OrderDetails',
|
||||
Preferences = 'Preferences',
|
||||
RateLimit = 'RateLimit',
|
||||
Receive = 'Receive',
|
||||
RestrictedGeo = 'RestrictedGeo',
|
||||
RestrictedWallet = 'RestrictedWallet',
|
||||
Trade = 'Trade',
|
||||
Transfer = 'Transfer',
|
||||
Withdraw = 'Withdraw',
|
||||
ManageFunds = 'ManageFunds',
|
||||
}
|
||||
|
||||
export enum TradeBoxDialogTypes {
|
||||
|
||||
@@ -13,6 +13,7 @@ export enum LocalStorageKey {
|
||||
PushNotificationsEnabled = 'dydx.PushNotificationsEnabled',
|
||||
PushNotificationsLastUpdated = 'dydx.PushNotificationsLastUpdated',
|
||||
TransferNotifications = 'dydx.TransferNotifications',
|
||||
NotificationPreferences = 'dydx.NotificationPreferences',
|
||||
|
||||
// UI State
|
||||
LastViewedMarket = 'dydx.LastViewedMarket',
|
||||
@@ -21,9 +22,13 @@ export enum LocalStorageKey {
|
||||
SelectedTheme = 'dydx.SelectedTheme',
|
||||
SelectedTradeLayout = 'dydx.SelectedTradeLayout',
|
||||
TradingViewChartConfig = 'dydx.TradingViewChartConfig',
|
||||
HasSeenLaunchIncentives = 'dydx.HasSeenLaunchIncentives',
|
||||
}
|
||||
|
||||
export const LOCAL_STORAGE_VERSIONS = {
|
||||
[LocalStorageKey.EvmDerivedAddresses]: 'v2',
|
||||
[LocalStorageKey.NotificationPreferences]: 'v1',
|
||||
[LocalStorageKey.TransferNotifications]: 'v1',
|
||||
[LocalStorageKey.Notifications]: 'v1',
|
||||
// TODO: version all localStorage keys
|
||||
};
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
LOCALE_DATA,
|
||||
TOOLTIPS,
|
||||
WARNINGS_STRING_KEYS,
|
||||
NOTIFICATIONS_STRING_KEYS,
|
||||
NOTIFICATIONS,
|
||||
} from '@dydxprotocol/v4-localization';
|
||||
|
||||
import { type LinksConfigs } from '@/hooks/useURLConfigs';
|
||||
@@ -27,6 +29,7 @@ export enum SupportedLocales {
|
||||
|
||||
export const EN_LOCALE_DATA = {
|
||||
...LOCALE_DATA[SupportedLocales.EN],
|
||||
...NOTIFICATIONS[SupportedLocales.EN],
|
||||
TOOLTIPS: TOOLTIPS[SupportedLocales.EN],
|
||||
};
|
||||
|
||||
@@ -34,8 +37,13 @@ export const STRING_KEYS = {
|
||||
...APP_STRING_KEYS,
|
||||
...ERRORS_STRING_KEYS,
|
||||
...WARNINGS_STRING_KEYS,
|
||||
...NOTIFICATIONS_STRING_KEYS,
|
||||
};
|
||||
|
||||
export const STRING_KEY_VALUES = Object.fromEntries(
|
||||
Object.values(STRING_KEYS).map((key) => [key, key])
|
||||
);
|
||||
|
||||
export type StringKey = keyof typeof STRING_KEYS;
|
||||
|
||||
export type LocaleData = typeof EN_LOCALE_DATA;
|
||||
|
||||
@@ -26,6 +26,7 @@ export type MenuItem<MenuItemValue, MenuItemTypes = string> = {
|
||||
tag?: React.ReactNode;
|
||||
slotAfter?: React.ReactNode;
|
||||
description?: string;
|
||||
slotCustomContent?: React.ReactNode;
|
||||
|
||||
href?: string;
|
||||
onSelect?: (key: MenuItemValue) => void;
|
||||
|
||||
@@ -2,8 +2,9 @@ import { StatusResponse } from '@0xsquid/sdk';
|
||||
|
||||
/** implemented in useNotificationTypes */
|
||||
export enum NotificationType {
|
||||
OrderStatusChanged = 'OrderStatusChanged',
|
||||
AbacusGenerated = 'AbacusGenerated',
|
||||
SquidTransfer = 'SquidTransfer',
|
||||
ReleaseUpdates = 'ReleaseUpdates',
|
||||
}
|
||||
|
||||
export enum NotificationComponentType {}
|
||||
@@ -59,7 +60,7 @@ export enum NotificationStatus {
|
||||
/** Toast or NotificationsMenu item interacted with or dismissed. "Seen" in NotificationsMenu. */
|
||||
Seen,
|
||||
|
||||
/** Notification marked for deletion. "Archived" in NotificationsMenu. */
|
||||
/** Notification marked for deletion. Hidden in NotificationsMenu. */
|
||||
Cleared,
|
||||
}
|
||||
|
||||
@@ -80,14 +81,20 @@ export type Notifications = Record<NotificationId, Notification<any>>;
|
||||
/** Notification display data derived from app state at runtime. */
|
||||
export type NotificationDisplayData = {
|
||||
icon?: React.ReactNode;
|
||||
title: string; // Title for Toast, Notification, and Push Notification
|
||||
body?: string; // Description body for Toast, Notification, and Push Notification
|
||||
|
||||
title?: string;
|
||||
slotTitleLeft?: React.ReactNode;
|
||||
slotTitleRight?: React.ReactNode;
|
||||
|
||||
description?: React.ReactNode;
|
||||
|
||||
customContent?: React.ReactNode;
|
||||
|
||||
customMenuContent?: React.ReactNode;
|
||||
// Overrides title/body for Notification in NotificationMenu
|
||||
renderCustomBody?: ({
|
||||
isToast,
|
||||
notification,
|
||||
}: {
|
||||
isToast?: boolean;
|
||||
notification: Notification;
|
||||
}) => React.ReactNode; // Custom Notification
|
||||
|
||||
actionDescription?: string;
|
||||
|
||||
@@ -115,12 +122,29 @@ export type NotificationDisplayData = {
|
||||
toastDuration?: number;
|
||||
};
|
||||
|
||||
export enum TransferNotificationTypes {
|
||||
Withdrawal = 'withdrawal',
|
||||
Deposit = 'deposit',
|
||||
}
|
||||
|
||||
// Notification types
|
||||
export type TransferNotifcation = {
|
||||
txHash: string;
|
||||
type?: TransferNotificationTypes;
|
||||
toChainId?: string;
|
||||
fromChainId?: string;
|
||||
toAmount?: number;
|
||||
triggeredAt?: number;
|
||||
isCctp?: boolean;
|
||||
errorCount?: number;
|
||||
status?: StatusResponse;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Struct to store whether a NotificationType should be triggered
|
||||
*/
|
||||
export type NotificationPreferences = {
|
||||
[key in NotificationType]: boolean;
|
||||
} & { version: string };
|
||||
|
||||
export const DEFAULT_TOAST_AUTO_CLOSE_MS = 5000;
|
||||
|
||||
@@ -10,6 +10,7 @@ export const LEVERAGE_DECIMALS = 2;
|
||||
export const TOKEN_DECIMALS = 4;
|
||||
export const LARGE_TOKEN_DECIMALS = 2;
|
||||
export const FEE_DECIMALS = 3;
|
||||
export const FUNDING_DECIMALS = 6;
|
||||
|
||||
export const QUANTUM_MULTIPLIER = 1_000_000;
|
||||
|
||||
@@ -18,3 +19,7 @@ export enum NumberSign {
|
||||
Negative = 'Negative',
|
||||
Neutral = 'Neutral',
|
||||
}
|
||||
|
||||
// Deposit/Withdraw
|
||||
export const MAX_CCTP_TRANSFER_AMOUNT = 1_000_000;
|
||||
export const MAX_PRICE_IMPACT = 0.02; // 2%
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum QueryKeys {
|
||||
ACCOUNT_BALANCE = 'ACCOUNT_BALANCE',
|
||||
LAUNCH_INCENTIVES = 'LAUNCH_INCENTIVES',
|
||||
TRANSACTION_STATUS = 'TRANSACTION_STATUS',
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import { type TooltipStrings, TOOLTIP_STRING_KEYS } from '@/constants/localization';
|
||||
|
||||
export const depositTooltips: TooltipStrings = {
|
||||
'minimum-deposit-amount': ({ stringGetter }) => ({
|
||||
title: stringGetter({ key: TOOLTIP_STRING_KEYS.MINIMUM_DEPOSIT_AMOUNT_TITLE }),
|
||||
body: stringGetter({ key: TOOLTIP_STRING_KEYS.MINIMUM_DEPOSIT_AMOUNT_BODY }),
|
||||
}),
|
||||
swap: ({ stringGetter }) => ({
|
||||
title: stringGetter({ key: TOOLTIP_STRING_KEYS.SWAP_TITLE }),
|
||||
body: stringGetter({ key: TOOLTIP_STRING_KEYS.SWAP_BODY }),
|
||||
|
||||
@@ -5,6 +5,10 @@ export const withdrawTooltips: TooltipStrings = {
|
||||
title: stringGetter({ key: TOOLTIP_STRING_KEYS.FAST_WITHDRAW_FEE_TITLE }),
|
||||
body: stringGetter({ key: TOOLTIP_STRING_KEYS.FAST_WITHDRAW_FEE_BODY }),
|
||||
}),
|
||||
'minimum-amount-received': ({ stringGetter }) => ({
|
||||
title: stringGetter({ key: TOOLTIP_STRING_KEYS.MINIMUM_AMOUNT_RECEIVED_TITLE }),
|
||||
body: stringGetter({ key: TOOLTIP_STRING_KEYS.MINIMUM_AMOUNT_RECEIVED_BODY }),
|
||||
}),
|
||||
'withdraw-types': ({ stringGetter }) => ({
|
||||
title: stringGetter({ key: TOOLTIP_STRING_KEYS.WITHDRAW_TYPES_TITLE }),
|
||||
body: stringGetter({ key: TOOLTIP_STRING_KEYS.WITHDRAW_TYPES_BODY }),
|
||||
|
||||
@@ -116,3 +116,15 @@ export enum MobilePlaceOrderSteps {
|
||||
PlacingOrder = 'PlacingOrder',
|
||||
Confirmation = 'Confirmation',
|
||||
}
|
||||
|
||||
export const CLEARED_TRADE_INPUTS = {
|
||||
limitPriceInput: '',
|
||||
triggerPriceInput: '',
|
||||
trailingPercentInput: '',
|
||||
};
|
||||
|
||||
export const CLEARED_SIZE_INPUTS = {
|
||||
amountInput: '',
|
||||
usdAmountInput: '',
|
||||
leverageInput: '',
|
||||
};
|
||||
|
||||
@@ -52,7 +52,7 @@ type WalletConnectionTypeConfig = {
|
||||
wagmiConnectorId?: string;
|
||||
};
|
||||
|
||||
export const walletConnectionTypes: Record<WalletConnectionType, WalletConnectionTypeConfig> = {
|
||||
export const WALLET_CONNECTION_TYPES: Record<WalletConnectionType, WalletConnectionTypeConfig> = {
|
||||
[WalletConnectionType.CoinbaseWalletSdk]: {
|
||||
name: 'Coinbase Wallet SDK',
|
||||
wagmiConnectorId: 'coinbaseWallet',
|
||||
@@ -110,7 +110,7 @@ export const WALLET_CONNECT_EXPLORER_RECOMMENDED_IDS = Object.values(
|
||||
WALLET_CONNECT_EXPLORER_RECOMMENDED_WALLETS
|
||||
);
|
||||
|
||||
type WalletConfig = {
|
||||
export type WalletConfig = {
|
||||
type: WalletType;
|
||||
stringKey: string;
|
||||
icon: string;
|
||||
@@ -274,6 +274,17 @@ export type WalletConnection = {
|
||||
provider?: ExternalProvider;
|
||||
};
|
||||
|
||||
export type WalletConnectConfig = {
|
||||
client: {
|
||||
name: string;
|
||||
description: string;
|
||||
iconUrl: string;
|
||||
};
|
||||
v2: {
|
||||
projectId: string;
|
||||
};
|
||||
};
|
||||
|
||||
// dYdX Chain wallets
|
||||
export const COSMOS_DERIVATION_PATH = "m/44'/118'/0'/0/0";
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useShouldShowFooter } from './useShouldShowFooter';
|
||||
import { useSelectedNetwork } from './useSelectedNetwork';
|
||||
import { useStringGetter } from './useStringGetter';
|
||||
import { useSubaccount } from './useSubaccount';
|
||||
import { useTradeFormInputs } from './useTradeFormInputs';
|
||||
import { useURLConfigs } from './useURLConfigs';
|
||||
|
||||
export {
|
||||
@@ -49,5 +50,6 @@ export {
|
||||
useSelectedNetwork,
|
||||
useStringGetter,
|
||||
useSubaccount,
|
||||
useTradeFormInputs,
|
||||
useURLConfigs,
|
||||
};
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import { useBalance } from 'wagmi';
|
||||
import { useBalance, useBlockNumber } from 'wagmi';
|
||||
import { StargateClient } from '@cosmjs/stargate';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { formatUnits } from 'viem';
|
||||
|
||||
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
|
||||
import { QueryKeys } from '@/constants/queries';
|
||||
import { EvmAddress } from '@/constants/wallets';
|
||||
|
||||
import { convertBech32Address } from '@/lib/addressUtils';
|
||||
@@ -52,15 +53,27 @@ export const useAccountBalance = ({
|
||||
const evmChainId = Number(ENVIRONMENT_CONFIG_MAP[selectedNetwork].ethereumChainId);
|
||||
const stakingBalances = useSelector(getStakingBalances, shallowEqual);
|
||||
|
||||
const evmQuery = useBalance({
|
||||
enabled: Boolean(!isCosmosChain && addressOrDenom?.startsWith('0x')),
|
||||
const queryClient = useQueryClient();
|
||||
const { data: blockNumber } = useBlockNumber({ watch: true });
|
||||
const {
|
||||
data: evmQuery,
|
||||
status: evmBalanceStatus,
|
||||
fetchStatus: evmBalanceFetchStatus,
|
||||
queryKey,
|
||||
} = useBalance({
|
||||
query: {
|
||||
enabled: Boolean(!isCosmosChain && addressOrDenom?.startsWith('0x')),
|
||||
},
|
||||
address: evmAddress,
|
||||
chainId: typeof chainId === 'number' ? chainId : Number(evmChainId),
|
||||
token:
|
||||
addressOrDenom === CHAIN_DEFAULT_TOKEN_ADDRESS ? undefined : (addressOrDenom as EvmAddress),
|
||||
watch: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
}, [blockNumber]);
|
||||
|
||||
const cosmosQueryFn = useCallback(async () => {
|
||||
if (dydxAddress && bech32AddrPrefix && rpc && addressOrDenom) {
|
||||
const address = convertBech32Address({
|
||||
@@ -78,7 +91,7 @@ export const useAccountBalance = ({
|
||||
|
||||
const cosmosQuery = useQuery({
|
||||
enabled: Boolean(isCosmosChain && dydxAddress && bech32AddrPrefix && rpc && addressOrDenom),
|
||||
queryKey: `accountBalances_${chainId}_${addressOrDenom}`,
|
||||
queryKey: [QueryKeys.ACCOUNT_BALANCE, chainId, addressOrDenom],
|
||||
queryFn: cosmosQueryFn,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
@@ -87,8 +100,11 @@ export const useAccountBalance = ({
|
||||
staleTime: 10_000,
|
||||
});
|
||||
|
||||
const { formatted: evmBalance } = evmQuery.data || {};
|
||||
const balance = isCosmosChain ? cosmosQuery.data : evmBalance;
|
||||
const balance = isCosmosChain
|
||||
? cosmosQuery.data
|
||||
: evmQuery
|
||||
? formatUnits(evmQuery?.value, evmQuery?.decimals)
|
||||
: undefined;
|
||||
|
||||
const nativeTokenCoinBalance = balances?.[chainTokenDenom];
|
||||
const nativeTokenBalance = MustBigNumber(nativeTokenCoinBalance?.amount);
|
||||
@@ -104,7 +120,7 @@ export const useAccountBalance = ({
|
||||
nativeTokenBalance,
|
||||
nativeStakingBalance,
|
||||
usdcBalance,
|
||||
queryStatus: isCosmosChain ? cosmosQuery.status : evmQuery.status,
|
||||
isQueryFetching: isCosmosChain ? cosmosQuery.isFetching : evmQuery.fetchStatus === 'fetching',
|
||||
queryStatus: isCosmosChain ? cosmosQuery.status : evmBalanceStatus,
|
||||
isQueryFetching: isCosmosChain ? cosmosQuery.isFetching : evmBalanceFetchStatus === 'fetching',
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useContext, createContext, useEffect, useState, useMemo }
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { AES, enc } from 'crypto-js';
|
||||
import { LocalWallet, type Subaccount } from '@dydxprotocol/v4-client-js';
|
||||
import { NOBLE_BECH32_PREFIX, LocalWallet, type Subaccount } from '@dydxprotocol/v4-client-js';
|
||||
|
||||
import { OnboardingGuard, OnboardingState, type EvmDerivedAddresses } from '@/constants/account';
|
||||
import { DialogTypes } from '@/constants/dialogs';
|
||||
@@ -39,9 +39,9 @@ const useAccountsContext = () => {
|
||||
walletConnectionType,
|
||||
selectWalletType,
|
||||
selectedWalletType,
|
||||
selectedWalletError,
|
||||
evmAddress,
|
||||
signerWagmi,
|
||||
publicClientWagmi,
|
||||
dydxAddress: connectedDydxAddress,
|
||||
signerGraz,
|
||||
} = useWalletConnection();
|
||||
@@ -135,8 +135,8 @@ const useAccountsContext = () => {
|
||||
getSubaccounts: async ({ dydxAddress }: { dydxAddress: DydxAddress }) => {
|
||||
try {
|
||||
const response = await compositeClient?.indexerClient.account.getSubaccounts(dydxAddress);
|
||||
setDydxSubaccounts(response.subaccounts);
|
||||
return response.subaccounts;
|
||||
setDydxSubaccounts(response?.subaccounts);
|
||||
return response?.subaccounts ?? [];
|
||||
} catch (error) {
|
||||
// 404 is expected if the user has no subaccounts
|
||||
if (error.status === 404) {
|
||||
@@ -218,6 +218,16 @@ const useAccountsContext = () => {
|
||||
else abacusStateManager.attemptDisconnectAccount();
|
||||
}, [localDydxWallet]);
|
||||
|
||||
useEffect(() => {
|
||||
const setNobleWallet = async () => {
|
||||
if (hdKey?.mnemonic) {
|
||||
const nobleWallet = await LocalWallet.fromMnemonic(hdKey.mnemonic, NOBLE_BECH32_PREFIX);
|
||||
abacusStateManager.setNobleWallet(nobleWallet);
|
||||
}
|
||||
};
|
||||
setNobleWallet();
|
||||
}, [hdKey?.mnemonic]);
|
||||
|
||||
// clear subaccounts when no dydxAddress is set
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -289,13 +299,12 @@ const useAccountsContext = () => {
|
||||
walletConnectionType,
|
||||
|
||||
// Wallet selection
|
||||
selectWalletType,
|
||||
selectedWalletType,
|
||||
selectedWalletError,
|
||||
|
||||
// Wallet connection (EVM)
|
||||
evmAddress,
|
||||
signerWagmi,
|
||||
publicClientWagmi,
|
||||
|
||||
// Wallet connection (Cosmos)
|
||||
signerGraz,
|
||||
|
||||
@@ -5,16 +5,15 @@ import { useMatch, useNavigate } from 'react-router-dom';
|
||||
import { LocalStorageKey } from '@/constants/localStorage';
|
||||
import { DEFAULT_MARKETID } from '@/constants/markets';
|
||||
import { AppRoute } from '@/constants/routes';
|
||||
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
||||
|
||||
import { getSelectedNetwork } from '@/state/appSelectors';
|
||||
import { closeDialogInTradeBox } from '@/state/dialogs';
|
||||
import { setCurrentMarketId } from '@/state/perpetuals';
|
||||
import { getMarketIds } from '@/state/perpetualsSelectors';
|
||||
|
||||
import abacusStateManager from '@/lib/abacus';
|
||||
|
||||
import { useLocalStorage } from './useLocalStorage';
|
||||
import { getMarketIds } from '@/state/perpetualsSelectors';
|
||||
|
||||
export const useCurrentMarketId = () => {
|
||||
const navigate = useNavigate();
|
||||
const match = useMatch(`/${AppRoute.Trade}/:marketId`);
|
||||
@@ -22,6 +21,8 @@ export const useCurrentMarketId = () => {
|
||||
const dispatch = useDispatch();
|
||||
const selectedNetwork = useSelector(getSelectedNetwork);
|
||||
const marketIds = useSelector(getMarketIds, shallowEqual);
|
||||
const hasMarketIds = marketIds.length > 0;
|
||||
|
||||
const [lastViewedMarket, setLastViewedMarket] = useLocalStorage({
|
||||
key: LocalStorageKey.LastViewedMarket,
|
||||
defaultValue: DEFAULT_MARKETID,
|
||||
@@ -31,22 +32,40 @@ export const useCurrentMarketId = () => {
|
||||
if (marketIds.length === 0) return marketId ?? lastViewedMarket;
|
||||
if (!marketIds.includes(marketId ?? lastViewedMarket)) return DEFAULT_MARKETID;
|
||||
return marketId ?? lastViewedMarket;
|
||||
}, [marketIds, marketId]);
|
||||
}, [hasMarketIds, marketId]);
|
||||
|
||||
useEffect(() => {
|
||||
setLastViewedMarket(validId);
|
||||
dispatch(setCurrentMarketId(validId));
|
||||
dispatch(closeDialogInTradeBox());
|
||||
// If v4_markets has not been subscribed to yet or marketId is not specified, default to validId
|
||||
if (!hasMarketIds || !marketId) {
|
||||
setLastViewedMarket(validId);
|
||||
dispatch(setCurrentMarketId(validId));
|
||||
dispatch(closeDialogInTradeBox());
|
||||
|
||||
navigate(`${AppRoute.Trade}/${validId}`, {
|
||||
replace: true,
|
||||
});
|
||||
}, [validId]);
|
||||
if (validId !== marketId) {
|
||||
navigate(`${AppRoute.Trade}/${validId}`, {
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// If v4_markets has been subscribed to, check if marketId is valid
|
||||
if (!marketIds.includes(marketId)) {
|
||||
// If marketId is not valid (i.e. final settlement), navigate to markets page
|
||||
navigate(AppRoute.Markets, {
|
||||
replace: true,
|
||||
});
|
||||
} else {
|
||||
// If marketId is valid, set currentMarketId
|
||||
setLastViewedMarket(marketId);
|
||||
dispatch(setCurrentMarketId(marketId));
|
||||
dispatch(closeDialogInTradeBox());
|
||||
}
|
||||
}
|
||||
}, [hasMarketIds, marketId]);
|
||||
|
||||
useEffect(() => {
|
||||
// Check for marketIds otherwise Abacus will silently fail its isMarketValid check
|
||||
if (marketIds) {
|
||||
abacusStateManager.setMarket(marketId ?? DEFAULT_MARKETID);
|
||||
}
|
||||
}, [selectedNetwork, marketIds, marketId]);
|
||||
}, [selectedNetwork, hasMarketIds, marketId]);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { createContext, useContext, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import type { StatusResponse } from '@0xsquid/sdk';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { LocalStorageKey } from '@/constants/localStorage';
|
||||
import { LOCAL_STORAGE_VERSIONS, LocalStorageKey } from '@/constants/localStorage';
|
||||
import { type TransferNotifcation } from '@/constants/notifications';
|
||||
|
||||
import { QueryKeys } from '@/constants/queries';
|
||||
import { useAccounts } from '@/hooks/useAccounts';
|
||||
import { STATUS_ERROR_GRACE_PERIOD, useSquid } from '@/hooks/useSquid';
|
||||
|
||||
import { fetchSquidStatus, STATUS_ERROR_GRACE_PERIOD } from '@/lib/squid';
|
||||
|
||||
import { useLocalStorage } from './useLocalStorage';
|
||||
|
||||
const LocalNotificationsContext = createContext<
|
||||
@@ -22,16 +23,32 @@ export const LocalNotificationsProvider = ({ ...props }) => (
|
||||
export const useLocalNotifications = () => useContext(LocalNotificationsContext)!;
|
||||
|
||||
const TRANSFER_STATUS_FETCH_INTERVAL = 10_000;
|
||||
const ERROR_COUNT_THRESHOLD = 3;
|
||||
|
||||
const useLocalNotificationsContext = () => {
|
||||
// transfer notifications
|
||||
const [allTransferNotifications, setAllTransferNotifications] = useLocalStorage<{
|
||||
[key: `dydx${string}`]: TransferNotifcation[];
|
||||
version: string;
|
||||
}>({
|
||||
key: LocalStorageKey.TransferNotifications,
|
||||
defaultValue: {},
|
||||
defaultValue: {
|
||||
version: LOCAL_STORAGE_VERSIONS[LocalStorageKey.TransferNotifications],
|
||||
},
|
||||
});
|
||||
|
||||
// Ensure version matches, otherwise wipe
|
||||
useEffect(() => {
|
||||
if (
|
||||
allTransferNotifications?.version !==
|
||||
LOCAL_STORAGE_VERSIONS[LocalStorageKey.TransferNotifications]
|
||||
) {
|
||||
setAllTransferNotifications({
|
||||
version: LOCAL_STORAGE_VERSIONS[LocalStorageKey.TransferNotifications],
|
||||
});
|
||||
}
|
||||
}, [allTransferNotifications]);
|
||||
|
||||
const { dydxAddress } = useAccounts();
|
||||
|
||||
const transferNotifications = dydxAddress ? allTransferNotifications[dydxAddress] || [] : [];
|
||||
@@ -52,48 +69,74 @@ const useLocalNotificationsContext = () => {
|
||||
[transferNotifications]
|
||||
);
|
||||
|
||||
const squid = useSquid();
|
||||
|
||||
const { data: transferStatuses } = useQuery({
|
||||
queryKey: ['getTransactionStatus', transferNotifications],
|
||||
const { data: newTransferNotifications } = useQuery({
|
||||
queryKey: [QueryKeys.TRANSACTION_STATUS],
|
||||
queryFn: async () => {
|
||||
const statuses: { [key: string]: StatusResponse } = {};
|
||||
for (const {
|
||||
txHash,
|
||||
toChainId,
|
||||
fromChainId,
|
||||
triggeredAt,
|
||||
status: currentStatus,
|
||||
} of transferNotifications) {
|
||||
try {
|
||||
if (currentStatus && currentStatus?.squidTransactionStatus !== 'ongoing') continue;
|
||||
const processTransferNotifications = async (transferNotifications: TransferNotifcation[]) => {
|
||||
const newTransferNotifications = await Promise.all(
|
||||
transferNotifications.map(async (transferNotification) => {
|
||||
const {
|
||||
txHash,
|
||||
toChainId,
|
||||
fromChainId,
|
||||
triggeredAt,
|
||||
isCctp,
|
||||
errorCount,
|
||||
status: currentStatus,
|
||||
} = transferNotification;
|
||||
|
||||
const status = await squid?.getStatus({ transactionId: txHash, toChainId, fromChainId });
|
||||
if (status) statuses[txHash] = status;
|
||||
} catch (error) {
|
||||
// ignore errors for the first 120s since the status might not be available yet
|
||||
if (!triggeredAt || Date.now() - triggeredAt > STATUS_ERROR_GRACE_PERIOD) {
|
||||
statuses[txHash] = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
return statuses;
|
||||
const hasErrors =
|
||||
// @ts-ignore status.errors is not in the type definition but can be returned
|
||||
// also error can some time come back as an empty object so we need to ignore for that
|
||||
!!currentStatus?.errors ||
|
||||
(currentStatus?.error && Object.keys(currentStatus.error).length !== 0);
|
||||
|
||||
if (
|
||||
!hasErrors &&
|
||||
(!currentStatus?.squidTransactionStatus ||
|
||||
currentStatus?.squidTransactionStatus === 'ongoing')
|
||||
) {
|
||||
try {
|
||||
const status = await fetchSquidStatus(
|
||||
{
|
||||
transactionId: txHash,
|
||||
toChainId,
|
||||
fromChainId,
|
||||
},
|
||||
isCctp
|
||||
);
|
||||
|
||||
if (status) {
|
||||
transferNotification.status = status;
|
||||
}
|
||||
} catch (error) {
|
||||
if (!triggeredAt || Date.now() - triggeredAt > STATUS_ERROR_GRACE_PERIOD) {
|
||||
if (errorCount && errorCount > ERROR_COUNT_THRESHOLD) {
|
||||
transferNotification.status = error;
|
||||
} else {
|
||||
transferNotification.errorCount = errorCount ? errorCount + 1 : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return transferNotification;
|
||||
})
|
||||
);
|
||||
|
||||
return newTransferNotifications;
|
||||
};
|
||||
|
||||
const newTransferNotifications = await processTransferNotifications(transferNotifications);
|
||||
return newTransferNotifications;
|
||||
},
|
||||
refetchInterval: TRANSFER_STATUS_FETCH_INTERVAL,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!transferStatuses) return;
|
||||
const newTransferNotifications = transferNotifications.map((notification) => {
|
||||
const status = transferStatuses[notification.txHash];
|
||||
if (!status) return notification;
|
||||
return {
|
||||
...notification,
|
||||
status,
|
||||
};
|
||||
});
|
||||
if (!newTransferNotifications) return;
|
||||
setTransferNotifications(newTransferNotifications);
|
||||
}, [transferStatuses]);
|
||||
}, [newTransferNotifications]);
|
||||
|
||||
return {
|
||||
transferNotifications,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useNetwork, useSwitchNetwork } from 'wagmi';
|
||||
import { UseSwitchChainReturnType, useAccount, useSwitchChain } from 'wagmi';
|
||||
|
||||
export const useMatchingEvmNetwork = ({
|
||||
chainId,
|
||||
@@ -8,10 +8,10 @@ export const useMatchingEvmNetwork = ({
|
||||
}: {
|
||||
chainId?: string | number;
|
||||
switchAutomatically?: boolean;
|
||||
onError?: (error: Error) => void;
|
||||
onError?: Parameters<UseSwitchChainReturnType['switchChainAsync']>[1]['onError'];
|
||||
}) => {
|
||||
const { chain } = useNetwork();
|
||||
const { isLoading, switchNetworkAsync } = useSwitchNetwork({ onError });
|
||||
const { chain } = useAccount();
|
||||
const { switchChainAsync, isPending } = useSwitchChain();
|
||||
|
||||
// If chainId is not a number, we can assume it is a non EVM compatible chain
|
||||
const isMatchingNetwork = useMemo(
|
||||
@@ -21,7 +21,14 @@ export const useMatchingEvmNetwork = ({
|
||||
|
||||
const matchNetwork = useCallback(async () => {
|
||||
if (!isMatchingNetwork) {
|
||||
await switchNetworkAsync?.(Number(chainId));
|
||||
await switchChainAsync?.(
|
||||
{
|
||||
chainId: Number(chainId),
|
||||
},
|
||||
{
|
||||
onError,
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [chainId, chain]);
|
||||
|
||||
@@ -34,6 +41,6 @@ export const useMatchingEvmNetwork = ({
|
||||
return {
|
||||
isMatchingNetwork,
|
||||
matchNetwork,
|
||||
isSwitchingNetwork: isLoading,
|
||||
isSwitchingNetwork: isPending,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,125 +1,149 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import styled, { type AnyStyledComponent } from 'styled-components';
|
||||
import { useSelector, shallowEqual, useDispatch } from 'react-redux';
|
||||
import { groupBy } from 'lodash';
|
||||
import { type ReactNode, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import { isEqual, groupBy } from 'lodash';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { AlertType } from '@/constants/alerts';
|
||||
import { AbacusOrderStatus, ORDER_SIDES } from '@/constants/abacus';
|
||||
import { DialogTypes } from '@/constants/dialogs';
|
||||
import { STRING_KEYS, StringKey } from '@/constants/localization';
|
||||
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
|
||||
import { type NotificationTypeConfig, NotificationType } from '@/constants/notifications';
|
||||
import { ORDER_SIDE_STRINGS } from '@/constants/trade';
|
||||
import { AppRoute } from '@/constants/routes';
|
||||
import { DydxChainAsset } from '@/constants/wallets';
|
||||
|
||||
import { useSelectedNetwork } from '@/hooks';
|
||||
import {
|
||||
STRING_KEYS,
|
||||
STRING_KEY_VALUES,
|
||||
type StringGetterFunction,
|
||||
type StringKey,
|
||||
} from '@/constants/localization';
|
||||
|
||||
import {
|
||||
type NotificationTypeConfig,
|
||||
NotificationType,
|
||||
DEFAULT_TOAST_AUTO_CLOSE_MS,
|
||||
TransferNotificationTypes,
|
||||
} from '@/constants/notifications';
|
||||
|
||||
import { useSelectedNetwork, useStringGetter } from '@/hooks';
|
||||
import { useLocalNotifications } from '@/hooks/useLocalNotifications';
|
||||
|
||||
import { AssetIcon } from '@/components/AssetIcon';
|
||||
import { Icon, IconName } from '@/components/Icon';
|
||||
import { Output, OutputType } from '@/components/Output';
|
||||
import { TransferStatusToast } from '@/views/TransferStatus';
|
||||
import { TradeNotification } from '@/views/notifications/TradeNotification';
|
||||
import { TransferStatusNotification } from '@/views/notifications/TransferStatusNotification';
|
||||
|
||||
import { getSubaccountFills, getSubaccountOrders } from '@/state/accountSelectors';
|
||||
import { openDialog } from '@/state/dialogs';
|
||||
import { getAbacusNotifications } from '@/state/notificationsSelectors';
|
||||
import { getMarketIds } from '@/state/perpetualsSelectors';
|
||||
|
||||
import { OrderStatusIcon } from '@/views/OrderStatusIcon';
|
||||
import { formatSeconds } from '@/lib/timeUtils';
|
||||
|
||||
import { useStringGetter } from './useStringGetter';
|
||||
import { TransferStatusSteps } from '@/views/TransferStatusSteps';
|
||||
const parseStringParamsForNotification = ({
|
||||
stringGetter,
|
||||
value,
|
||||
}: {
|
||||
stringGetter: StringGetterFunction;
|
||||
value: unknown;
|
||||
}): ReactNode => {
|
||||
if (STRING_KEY_VALUES[value as StringKey]) {
|
||||
return stringGetter({ key: value as StringKey });
|
||||
}
|
||||
|
||||
export const notificationTypes = [
|
||||
return value as ReactNode;
|
||||
};
|
||||
|
||||
export const notificationTypes: NotificationTypeConfig[] = [
|
||||
{
|
||||
type: NotificationType.OrderStatusChanged,
|
||||
|
||||
useTrigger: ({ trigger, lastUpdated }) => {
|
||||
type: NotificationType.AbacusGenerated,
|
||||
useTrigger: ({ trigger }) => {
|
||||
const stringGetter = useStringGetter();
|
||||
|
||||
const orders = useSelector(getSubaccountOrders, shallowEqual) || [];
|
||||
const ordersByOrderId = Object.fromEntries(orders.map((order) => [order.id, order]));
|
||||
|
||||
const fills = useSelector(getSubaccountFills, shallowEqual) || [];
|
||||
const fillsByOrderId = groupBy(fills, (fill) => fill.orderId);
|
||||
|
||||
const orderIds = useMemo(
|
||||
() => [...Object.keys(ordersByOrderId), ...Object.keys(fillsByOrderId)],
|
||||
[orders, fills]
|
||||
);
|
||||
const abacusNotifications = useSelector(getAbacusNotifications, isEqual);
|
||||
|
||||
useEffect(() => {
|
||||
for (const orderId of orderIds) {
|
||||
const fills = fillsByOrderId[orderId];
|
||||
for (const abacusNotif of abacusNotifications) {
|
||||
const [abacusNotificationType = '', id = ''] = abacusNotif.id.split(':');
|
||||
const parsedData = abacusNotif.data ? JSON.parse(abacusNotif.data) : {};
|
||||
|
||||
const order =
|
||||
ordersByOrderId[orderId] ??
|
||||
(fills?.length
|
||||
? {
|
||||
...fills[fills.length - 1],
|
||||
id: orderId,
|
||||
createdAtMilliseconds: Math.max(
|
||||
...fills.map((fill) => fill.createdAtMilliseconds)
|
||||
const params = Object.fromEntries(
|
||||
Object.entries(parsedData).map(([key, value]) => {
|
||||
return [key, parseStringParamsForNotification({ stringGetter, value })];
|
||||
})
|
||||
);
|
||||
|
||||
switch (abacusNotificationType) {
|
||||
case 'order': {
|
||||
trigger(
|
||||
abacusNotif.id,
|
||||
{
|
||||
icon: abacusNotif.image && <$Icon src={abacusNotif.image} alt="" />,
|
||||
title: stringGetter({ key: abacusNotif.title }),
|
||||
body: abacusNotif.text ? stringGetter({ key: abacusNotif.text, params }) : '',
|
||||
toastDuration: DEFAULT_TOAST_AUTO_CLOSE_MS,
|
||||
toastSensitivity: 'foreground',
|
||||
renderCustomBody: ({ isToast, notification }) => (
|
||||
<TradeNotification
|
||||
isToast={isToast}
|
||||
data={parsedData}
|
||||
notification={notification}
|
||||
/>
|
||||
),
|
||||
status: AbacusOrderStatus.filled,
|
||||
}
|
||||
: undefined);
|
||||
|
||||
if (order)
|
||||
trigger(
|
||||
order.id,
|
||||
{
|
||||
icon: (
|
||||
<OrderStatusIcon status={order.status} totalFilled={order.totalFilled ?? 0} />
|
||||
),
|
||||
title: `${stringGetter({
|
||||
key: order.resources.typeStringKey as StringKey,
|
||||
})} ${
|
||||
order.status === AbacusOrderStatus.open && (order?.totalFilled ?? 0) > 0
|
||||
? stringGetter({ key: STRING_KEYS.PARTIALLY_FILLED })
|
||||
: stringGetter({ key: order.resources.statusStringKey as StringKey })
|
||||
}`,
|
||||
description: `${stringGetter({
|
||||
key: ORDER_SIDE_STRINGS[ORDER_SIDES[order.side.name]],
|
||||
})} ${order.size} ${order.marketId} @ $${order.price}`,
|
||||
actionDescription: 'View Order',
|
||||
actionAltText: 'View this order in the Orders tab or the Notifications menu.',
|
||||
toastSensitivity:
|
||||
order.status === AbacusOrderStatus.pending ? 'foreground' : 'background',
|
||||
toastDuration: 5000,
|
||||
},
|
||||
[order.status.name, order.size],
|
||||
!order.createdAtMilliseconds || order.createdAtMilliseconds > lastUpdated
|
||||
);
|
||||
},
|
||||
[abacusNotif.updateTimeInMilliseconds, abacusNotif.data],
|
||||
true
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
trigger(
|
||||
abacusNotif.id,
|
||||
{
|
||||
icon: abacusNotif.image && <$Icon src={abacusNotif.image} alt="" />,
|
||||
title: stringGetter({ key: abacusNotif.title }),
|
||||
body: abacusNotif.text ? stringGetter({ key: abacusNotif.text, params }) : '',
|
||||
toastDuration: DEFAULT_TOAST_AUTO_CLOSE_MS,
|
||||
toastSensitivity: 'foreground',
|
||||
},
|
||||
[abacusNotif.updateTimeInMilliseconds, abacusNotif.data]
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [orderIds]);
|
||||
}, [abacusNotifications, stringGetter]);
|
||||
},
|
||||
useNotificationAction: () => {
|
||||
const dispatch = useDispatch();
|
||||
const orders = useSelector(getSubaccountOrders, shallowEqual) || [];
|
||||
const ordersById = groupBy(orders, 'id');
|
||||
const fills = useSelector(getSubaccountFills, shallowEqual) || [];
|
||||
const fillsById = groupBy(fills, 'id');
|
||||
const marketIds = useSelector(getMarketIds, shallowEqual);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// useNotificationAction: () => {
|
||||
// const dispatch = useDispatch();
|
||||
// const orders = useSelector(getSubaccountOrders, shallowEqual) || [];
|
||||
// const ordersByOrderId = Object.fromEntries(orders.map((order) => [order.id, order]));
|
||||
return (notificationId: string) => {
|
||||
const [abacusNotificationType = '', id = ''] = notificationId.split(':');
|
||||
|
||||
// const fills = useSelector(getSubaccountFills, shallowEqual) || [];
|
||||
// const fillsByOrderId = groupBy(fills, (fill) => fill.orderId);
|
||||
|
||||
// return (id) => {
|
||||
// if (ordersByOrderId[id]) {
|
||||
// dispatch(
|
||||
// openDialog({
|
||||
// type: DialogTypes.OrderDetails,
|
||||
// dialogProps: { orderId: id },
|
||||
// })
|
||||
// );
|
||||
// } else if (fillsByOrderId[id]) {
|
||||
// const fillId = fillsByOrderId[id][0].id;
|
||||
|
||||
// dispatch(
|
||||
// openDialog({
|
||||
// type: DialogTypes.FillDetails,
|
||||
// dialogProps: { fillId },
|
||||
// })
|
||||
// );
|
||||
// }
|
||||
// };
|
||||
// },
|
||||
} as NotificationTypeConfig<string, [string, number]>,
|
||||
if (ordersById[id]) {
|
||||
dispatch(
|
||||
openDialog({
|
||||
type: DialogTypes.OrderDetails,
|
||||
dialogProps: { orderId: id },
|
||||
})
|
||||
);
|
||||
} else if (fillsById[id]) {
|
||||
dispatch(
|
||||
openDialog({
|
||||
type: DialogTypes.FillDetails,
|
||||
dialogProps: { fillId: id },
|
||||
})
|
||||
);
|
||||
} else if (marketIds.includes(id)) {
|
||||
navigate(`${AppRoute.Trade}/${id}`, {
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
type: NotificationType.SquidTransfer,
|
||||
useTrigger: ({ trigger }) => {
|
||||
@@ -127,81 +151,113 @@ export const notificationTypes = [
|
||||
const { transferNotifications } = useLocalNotifications();
|
||||
const { selectedNetwork } = useSelectedNetwork();
|
||||
|
||||
const getTitleStringKey = useCallback((type: 'deposit' | 'withdrawal', finished: boolean) => {
|
||||
if (type === 'deposit' && !finished) return STRING_KEYS.DEPOSIT_IN_PROGRESS;
|
||||
if (type === 'deposit' && finished) return STRING_KEYS.DEPOSIT;
|
||||
if (type === 'withdrawal' && !finished) return STRING_KEYS.WITHDRAW_IN_PROGRESS;
|
||||
return STRING_KEYS.WITHDRAW;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
for (const transfer of transferNotifications) {
|
||||
const { fromChainId, status, txHash, toAmount } = transfer;
|
||||
const finished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing';
|
||||
const type = fromChainId === ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId ? 'withdrawal' : 'deposit';
|
||||
// @ts-ignore status.errors is not in the type definition but can be returned
|
||||
const error = status?.errors?.length ? status?.errors[0] : status?.error;
|
||||
const { fromChainId, status, txHash, toAmount, type } = transfer;
|
||||
const isFinished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing';
|
||||
const icon = <Icon iconName={isFinished ? IconName.Transfer : IconName.Clock} />;
|
||||
|
||||
// TODO: confirm with design what the description should be
|
||||
const description = (
|
||||
<div>
|
||||
<Styled.TransferText>
|
||||
{type === 'deposit' ? 'Deposit of ' : 'Withdraw of '}
|
||||
<Output type={OutputType.Fiat} value={toAmount} />
|
||||
</Styled.TransferText>
|
||||
const transferType =
|
||||
type ?? fromChainId === ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId
|
||||
? TransferNotificationTypes.Withdrawal
|
||||
: TransferNotificationTypes.Deposit;
|
||||
|
||||
{error && (
|
||||
<Styled.ErrorMessage type={AlertType.Error}>
|
||||
{stringGetter({
|
||||
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
|
||||
params: {
|
||||
ERROR_MESSAGE:
|
||||
error.message || stringGetter({ key: STRING_KEYS.UNKNOWN_ERROR }),
|
||||
},
|
||||
})}
|
||||
</Styled.ErrorMessage>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
const title = stringGetter({
|
||||
key: {
|
||||
deposit: isFinished ? STRING_KEYS.DEPOSIT : STRING_KEYS.DEPOSIT_IN_PROGRESS,
|
||||
withdrawal: isFinished ? STRING_KEYS.WITHDRAW : STRING_KEYS.WITHDRAW_IN_PROGRESS,
|
||||
}[transferType],
|
||||
});
|
||||
|
||||
const toChainEta = status?.toChain?.chainData?.estimatedRouteDuration || 0;
|
||||
const estimatedDuration = formatSeconds(Math.max(toChainEta, 0));
|
||||
const body = stringGetter({
|
||||
key: STRING_KEYS.DEPOSIT_STATUS,
|
||||
params: {
|
||||
AMOUNT_USD: `${toAmount} ${DydxChainAsset.USDC.toUpperCase()}`,
|
||||
ESTIMATED_DURATION: estimatedDuration,
|
||||
},
|
||||
});
|
||||
|
||||
trigger(
|
||||
txHash,
|
||||
{
|
||||
icon: <Icon iconName={finished ? IconName.Transfer : IconName.Clock} />,
|
||||
title: stringGetter({ key: getTitleStringKey(type, finished) }),
|
||||
description: description,
|
||||
customContent: (
|
||||
<TransferStatusToast
|
||||
type={type}
|
||||
toAmount={transfer.toAmount}
|
||||
icon,
|
||||
title,
|
||||
body,
|
||||
renderCustomBody: ({ isToast, notification }) => (
|
||||
<TransferStatusNotification
|
||||
isToast={isToast}
|
||||
slotIcon={icon}
|
||||
slotTitle={title}
|
||||
transfer={transfer}
|
||||
type={transferType}
|
||||
triggeredAt={transfer.triggeredAt}
|
||||
status={transfer.status}
|
||||
notification={notification}
|
||||
/>
|
||||
),
|
||||
customMenuContent: !finished && (
|
||||
<div>
|
||||
{description}
|
||||
<TransferStatusSteps status={transfer.status} type={type} />
|
||||
</div>
|
||||
),
|
||||
toastSensitivity: 'foreground',
|
||||
},
|
||||
[]
|
||||
[isFinished]
|
||||
);
|
||||
}
|
||||
}, [transferNotifications]);
|
||||
}, [transferNotifications, stringGetter]);
|
||||
},
|
||||
useNotificationAction: () => {
|
||||
return () => {};
|
||||
},
|
||||
},
|
||||
] satisfies NotificationTypeConfig[];
|
||||
{
|
||||
type: NotificationType.ReleaseUpdates,
|
||||
useTrigger: ({ trigger }) => {
|
||||
const stringGetter = useStringGetter();
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
useEffect(() => {
|
||||
trigger(
|
||||
'rewards-and-full-trading-live',
|
||||
{
|
||||
icon: <AssetIcon symbol="DYDX" />,
|
||||
title: stringGetter({ key: 'NOTIFICATIONS.RELEASE_REWARDS_AND_FULL_TRADING.TITLE' }),
|
||||
body: stringGetter({
|
||||
key: 'NOTIFICATIONS.RELEASE_REWARDS_AND_FULL_TRADING.BODY',
|
||||
params: {
|
||||
DOS_BLOGPOST: (
|
||||
<$Link
|
||||
href="https://www.dydxopsdao.com/blog/deep-dive-full-trading"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.HERE })}
|
||||
</$Link>
|
||||
),
|
||||
TRADING_BLOGPOST: (
|
||||
<$Link
|
||||
href="https://dydx.exchange/blog/v4-full-trading"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.HERE })}
|
||||
</$Link>
|
||||
),
|
||||
},
|
||||
}),
|
||||
toastSensitivity: 'foreground',
|
||||
},
|
||||
[]
|
||||
);
|
||||
}, [stringGetter]);
|
||||
},
|
||||
useNotificationAction: () => {
|
||||
return () => {};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
Styled.TransferText = styled.span`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5ch;
|
||||
const $Icon = styled.img`
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
`;
|
||||
|
||||
Styled.ErrorMessage = styled.div`
|
||||
max-width: 13rem;
|
||||
const $Link = styled.a`
|
||||
--link-color: var(--color-text-2);
|
||||
`;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { LocalStorageKey } from '@/constants/localStorage';
|
||||
import { LOCAL_STORAGE_VERSIONS, LocalStorageKey } from '@/constants/localStorage';
|
||||
import {
|
||||
type Notification,
|
||||
type NotificationDisplayData,
|
||||
type NotificationPreferences,
|
||||
type Notifications,
|
||||
NotificationStatus,
|
||||
NotificationType,
|
||||
} from '@/constants/notifications';
|
||||
|
||||
import { useLocalStorage } from './useLocalStorage';
|
||||
@@ -13,8 +15,10 @@ import { notificationTypes } from './useNotificationTypes';
|
||||
|
||||
import { renderSvgToDataUrl } from '../lib/renderSvgToDataUrl';
|
||||
|
||||
const NotificationsContext = createContext<ReturnType<typeof useNotificationsContext> | undefined>(
|
||||
undefined
|
||||
type NotificationsContextType = ReturnType<typeof useNotificationsContext>;
|
||||
|
||||
const NotificationsContext = createContext<NotificationsContextType>(
|
||||
{} as NotificationsContextType
|
||||
);
|
||||
|
||||
NotificationsContext.displayName = 'Notifications';
|
||||
@@ -27,6 +31,7 @@ export const useNotifications = () => useContext(NotificationsContext)!;
|
||||
|
||||
const useNotificationsContext = () => {
|
||||
// Local storage
|
||||
// const [notifications, setNotifications] = useState<Notifications>({});
|
||||
const [notifications, setNotifications] = useLocalStorage<Notifications>({
|
||||
key: LocalStorageKey.Notifications,
|
||||
defaultValue: {},
|
||||
@@ -37,12 +42,25 @@ const useNotificationsContext = () => {
|
||||
defaultValue: Date.now(),
|
||||
});
|
||||
|
||||
const [notificationPreferences, setNotificationPreferences] =
|
||||
useLocalStorage<NotificationPreferences>({
|
||||
key: LocalStorageKey.NotificationPreferences,
|
||||
defaultValue: {
|
||||
[NotificationType.AbacusGenerated]: true,
|
||||
[NotificationType.SquidTransfer]: true,
|
||||
version: LOCAL_STORAGE_VERSIONS[LocalStorageKey.NotificationPreferences],
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setNotificationsLastUpdated(Date.now());
|
||||
}, [notifications]);
|
||||
|
||||
const getKey = <T extends string | number>(notification: Pick<Notification<T>, 'type' | 'id'>) =>
|
||||
`${notification.type}/${notification.id}`;
|
||||
const getKey = useCallback(
|
||||
<T extends string | number>(notification: Pick<Notification<T>, 'type' | 'id'>) =>
|
||||
`${notification.type}/${notification.id}`,
|
||||
[]
|
||||
);
|
||||
|
||||
// Display data
|
||||
const [notificationsDisplayData, setNotificationsDisplayData] = useState(
|
||||
@@ -54,29 +72,47 @@ const useNotificationsContext = () => {
|
||||
[notificationsDisplayData]
|
||||
);
|
||||
|
||||
// Check for version changes
|
||||
useEffect(() => {
|
||||
if (
|
||||
notificationPreferences.version !==
|
||||
LOCAL_STORAGE_VERSIONS[LocalStorageKey.NotificationPreferences]
|
||||
) {
|
||||
setNotificationPreferences({
|
||||
[NotificationType.AbacusGenerated]: true,
|
||||
[NotificationType.SquidTransfer]: true,
|
||||
[NotificationType.ReleaseUpdates]: true,
|
||||
version: LOCAL_STORAGE_VERSIONS[LocalStorageKey.NotificationPreferences],
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Status changes
|
||||
const updateStatus = useCallback(
|
||||
(notification: Notification, status: NotificationStatus) => {
|
||||
notification.status = status;
|
||||
notification.timestamps[notification.status] = Date.now();
|
||||
setNotifications({ ...notifications });
|
||||
setNotifications({ ...notifications, [getKey(notification)]: notification });
|
||||
},
|
||||
[notifications]
|
||||
[notifications, getKey]
|
||||
);
|
||||
|
||||
const { markUnseen, markSeen, markCleared } = useMemo(
|
||||
() => ({
|
||||
markUnseen: (notification: Notification) => {
|
||||
if (notification.status < NotificationStatus.Unseen)
|
||||
if (notification.status < NotificationStatus.Unseen) {
|
||||
updateStatus(notification, NotificationStatus.Unseen);
|
||||
}
|
||||
},
|
||||
markSeen: (notification: Notification) => {
|
||||
if (notification.status < NotificationStatus.Seen)
|
||||
if (notification.status < NotificationStatus.Seen) {
|
||||
updateStatus(notification, NotificationStatus.Seen);
|
||||
}
|
||||
},
|
||||
markCleared: (notification: Notification) => {
|
||||
if (notification.status < NotificationStatus.Cleared)
|
||||
if (notification.status < NotificationStatus.Cleared) {
|
||||
updateStatus(notification, NotificationStatus.Cleared);
|
||||
}
|
||||
},
|
||||
}),
|
||||
[updateStatus]
|
||||
@@ -97,33 +133,37 @@ const useNotificationsContext = () => {
|
||||
|
||||
const notification = notifications[key];
|
||||
|
||||
// New unique key - create new notification
|
||||
if (!notification) {
|
||||
const notification = (notifications[key] = {
|
||||
id,
|
||||
type,
|
||||
timestamps: {},
|
||||
updateKey,
|
||||
} as Notification);
|
||||
// Filter out notifications that are not enabled
|
||||
if (notificationPreferences[type] !== false) {
|
||||
// New unique key - create new notification
|
||||
if (!notification) {
|
||||
const notification = (notifications[key] = {
|
||||
id,
|
||||
type,
|
||||
timestamps: {},
|
||||
updateKey,
|
||||
} as Notification);
|
||||
updateStatus(
|
||||
notification,
|
||||
isNew ? NotificationStatus.Triggered : NotificationStatus.Cleared
|
||||
);
|
||||
} else if (JSON.stringify(updateKey) !== JSON.stringify(notification.updateKey)) {
|
||||
// updateKey changed - update existing notification
|
||||
|
||||
updateStatus(
|
||||
notification,
|
||||
isNew ? NotificationStatus.Triggered : NotificationStatus.Cleared
|
||||
);
|
||||
}
|
||||
const notification = notifications[key];
|
||||
|
||||
// updateKey changed - update existing notification
|
||||
else if (JSON.stringify(updateKey) !== JSON.stringify(notification.updateKey)) {
|
||||
const notification = notifications[key];
|
||||
|
||||
notification.updateKey = updateKey;
|
||||
updateStatus(notification, NotificationStatus.Updated);
|
||||
notification.updateKey = updateKey;
|
||||
updateStatus(notification, NotificationStatus.Updated);
|
||||
}
|
||||
} else {
|
||||
// Notification is disabled - remove it
|
||||
delete notifications[key];
|
||||
}
|
||||
|
||||
notificationsDisplayData[key] = displayData;
|
||||
setNotificationsDisplayData({ ...notificationsDisplayData });
|
||||
},
|
||||
[notifications, updateStatus]
|
||||
[notifications, updateStatus, notificationPreferences[type]]
|
||||
),
|
||||
|
||||
lastUpdated: notificationsLastUpdated,
|
||||
@@ -136,10 +176,8 @@ const useNotificationsContext = () => {
|
||||
)
|
||||
);
|
||||
|
||||
const onNotificationAction = useCallback(
|
||||
async (notification: Notification) => await actions[notification.type]?.(notification.id),
|
||||
[actions]
|
||||
);
|
||||
const onNotificationAction = async (notification: Notification) =>
|
||||
await actions[notification.type]?.(notification.id);
|
||||
|
||||
// Push notifications
|
||||
const [hasEnabledPush, setHasEnabledPush] = useLocalStorage({
|
||||
@@ -186,7 +224,7 @@ const useNotificationsContext = () => {
|
||||
renotify: true,
|
||||
tag: getKey(notification),
|
||||
data: notification,
|
||||
body: displayData.description,
|
||||
description: displayData.body,
|
||||
icon: iconUrl ?? '/favicon.svg',
|
||||
badge: iconUrl ?? '/favicon.svg',
|
||||
image: iconUrl ?? '/favicon.svg',
|
||||
@@ -208,7 +246,7 @@ const useNotificationsContext = () => {
|
||||
|
||||
setPushNotificationsLastUpdated(Date.now());
|
||||
})();
|
||||
}, [hasEnabledPush, notifications, onNotificationAction, markSeen]);
|
||||
}, [hasEnabledPush, notifications, markSeen]);
|
||||
|
||||
// Menu state
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
@@ -239,5 +277,13 @@ const useNotificationsContext = () => {
|
||||
// Menu state
|
||||
isMenuOpen,
|
||||
setIsMenuOpen,
|
||||
|
||||
// Notification Preferences
|
||||
notificationPreferences,
|
||||
setNotificationPreferences,
|
||||
getNotificationPreferenceForType: useCallback(
|
||||
(type: NotificationType) => notificationPreferences[type],
|
||||
[notificationPreferences]
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Squid } from '@0xsquid/sdk';
|
||||
|
||||
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
|
||||
|
||||
import { getSelectedNetwork } from '@/state/appSelectors';
|
||||
|
||||
export const NATIVE_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
|
||||
|
||||
export const STATUS_ERROR_GRACE_PERIOD = 120_000;
|
||||
|
||||
const useSquidContext = () => {
|
||||
const selectedNetwork = useSelector(getSelectedNetwork);
|
||||
const [_, setInitialized] = useState(false);
|
||||
|
||||
const initializeClient = async () => {
|
||||
setInitialized(false);
|
||||
if (!squid) return;
|
||||
await squid.init();
|
||||
setInitialized(true);
|
||||
};
|
||||
|
||||
const squid = useMemo(
|
||||
() =>
|
||||
new Squid({
|
||||
baseUrl: ENVIRONMENT_CONFIG_MAP[selectedNetwork]?.endpoints['0xsquid'],
|
||||
integratorId: ENVIRONMENT_CONFIG_MAP[selectedNetwork]?.squidIntegratorId,
|
||||
}),
|
||||
[selectedNetwork]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (squid) {
|
||||
initializeClient();
|
||||
}
|
||||
}, [squid]);
|
||||
|
||||
return squid;
|
||||
};
|
||||
|
||||
type SquidContextType = ReturnType<typeof useSquidContext>;
|
||||
const SquidContext = createContext<SquidContextType | undefined>(undefined);
|
||||
SquidContext.displayName = '0xSquid';
|
||||
|
||||
export const SquidProvider = ({ ...props }) => (
|
||||
<SquidContext.Provider value={useSquidContext()} {...props} />
|
||||
);
|
||||
|
||||
export const useSquid = () => useContext(SquidContext);
|
||||
@@ -31,6 +31,7 @@ import { log } from '@/lib/telemetry';
|
||||
import { useAccounts } from './useAccounts';
|
||||
import { useTokenConfigs } from './useTokenConfigs';
|
||||
import { useDydxClient } from './useDydxClient';
|
||||
import { hashFromTx } from '@/lib/hashfromTx';
|
||||
|
||||
type SubaccountContextType = ReturnType<typeof useSubaccountContext>;
|
||||
const SubaccountContext = createContext<SubaccountContextType>({} as SubaccountContextType);
|
||||
@@ -293,12 +294,29 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
|
||||
);
|
||||
|
||||
const sendSquidWithdraw = useCallback(
|
||||
async (amount: number, payload: string) => {
|
||||
async (amount: number, payload: string, isCctp?: boolean) => {
|
||||
|
||||
const cctpWithdraw = () => {
|
||||
return new Promise<string>((resolve, reject) =>
|
||||
abacusStateManager.cctpWithdraw((success, error, data) => {
|
||||
const parsedData = JSON.parse(data);
|
||||
if (success && parsedData?.code == 0) {
|
||||
resolve(parsedData?.transactionHash);
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
if (isCctp) {
|
||||
return await cctpWithdraw();
|
||||
}
|
||||
|
||||
if (!subaccountClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
return await sendSquidWithdrawFromSubaccount({ subaccountClient, amount, payload });
|
||||
const tx = await sendSquidWithdrawFromSubaccount({ subaccountClient, amount, payload });
|
||||
return hashFromTx(tx?.hash);
|
||||
},
|
||||
[subaccountClient, sendSquidWithdrawFromSubaccount]
|
||||
);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { getTradeFormInputs } from '@/state/inputsSelectors';
|
||||
import { useEffect } from 'react';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
|
||||
import { TradeInputField } from '@/constants/abacus';
|
||||
|
||||
import abacusStateManager from '@/lib/abacus';
|
||||
|
||||
export const useTradeFormInputs = () => {
|
||||
const tradeFormInputValues = useSelector(getTradeFormInputs, shallowEqual);
|
||||
const { limitPriceInput, triggerPriceInput, trailingPercentInput } = tradeFormInputValues;
|
||||
|
||||
useEffect(() => {
|
||||
abacusStateManager.setTradeValue({
|
||||
value: triggerPriceInput,
|
||||
field: TradeInputField.triggerPrice,
|
||||
});
|
||||
}, [triggerPriceInput]);
|
||||
|
||||
useEffect(() => {
|
||||
abacusStateManager.setTradeValue({ value: limitPriceInput, field: TradeInputField.limitPrice });
|
||||
}, [limitPriceInput]);
|
||||
|
||||
useEffect(() => {
|
||||
abacusStateManager.setTradeValue({
|
||||
value: trailingPercentInput,
|
||||
field: TradeInputField.trailingPercent,
|
||||
});
|
||||
}, [trailingPercentInput]);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { LocalStorageKey } from '@/constants/localStorage';
|
||||
@@ -90,24 +90,27 @@ export const useWalletConnection = () => {
|
||||
|
||||
const selectedNetwork = useSelector(getSelectedNetwork);
|
||||
const walletConnectConfig = ENVIRONMENT_CONFIG_MAP[selectedNetwork].wallets.walletconnect;
|
||||
|
||||
const { connectAsync: connectWagmi } =
|
||||
walletType && walletConnectionType
|
||||
? useConnectWagmi({
|
||||
connector: resolveWagmiConnector({
|
||||
const wagmiConnector = useMemo(
|
||||
() =>
|
||||
walletType && walletConnectionType
|
||||
? resolveWagmiConnector({
|
||||
walletType,
|
||||
walletConnection: {
|
||||
type: walletConnectionType,
|
||||
},
|
||||
walletConnectConfig,
|
||||
}),
|
||||
})
|
||||
: useConnectWagmi();
|
||||
})
|
||||
: undefined,
|
||||
[walletConnectConfig, walletType, walletConnectionType]
|
||||
);
|
||||
|
||||
const { connectAsync: connectWagmi } = useConnectWagmi();
|
||||
const { suggestAndConnect: connectGraz } = useConnectGraz();
|
||||
|
||||
const connectWallet = useCallback(
|
||||
async ({ walletType }: { walletType: WalletType }) => {
|
||||
const walletConnection = getWalletConnection({ walletType });
|
||||
console.log({ walletConnection });
|
||||
|
||||
try {
|
||||
if (!walletConnection) {
|
||||
@@ -131,13 +134,17 @@ export const useWalletConnection = () => {
|
||||
}
|
||||
} else {
|
||||
if (!isConnectedWagmi) {
|
||||
await connectWagmi({
|
||||
connector: resolveWagmiConnector({
|
||||
walletType,
|
||||
walletConnection,
|
||||
walletConnectConfig,
|
||||
}),
|
||||
const connector = resolveWagmiConnector({
|
||||
walletType,
|
||||
walletConnection,
|
||||
walletConnectConfig,
|
||||
});
|
||||
|
||||
if (connector) {
|
||||
await connectWagmi({
|
||||
connector,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -154,7 +161,7 @@ export const useWalletConnection = () => {
|
||||
walletConnectionType: walletConnection.type,
|
||||
};
|
||||
},
|
||||
[isConnectedGraz, signerGraz, isConnectedWagmi, signerWagmi]
|
||||
[isConnectedGraz, signerGraz, isConnectedWagmi, signerWagmi, walletConnectConfig]
|
||||
);
|
||||
|
||||
const disconnectWallet = useCallback(async () => {
|
||||
@@ -202,10 +209,9 @@ export const useWalletConnection = () => {
|
||||
})();
|
||||
}, [selectedWalletType, signerWagmi, signerGraz]);
|
||||
|
||||
const selectWalletType = async (walletType: WalletType | undefined) => {
|
||||
const selectWalletType = (walletType: WalletType | undefined) => {
|
||||
if (selectedWalletType) {
|
||||
setSelectedWalletType(undefined);
|
||||
await new Promise(requestAnimationFrame);
|
||||
}
|
||||
|
||||
setSelectedWalletType(walletType);
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg width="27" height="28" viewBox="0 0 27 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Frame">
|
||||
<path id="Vector" d="M9.5 15L12.5 18L17.5 11M25.5 14C25.5 15.5759 25.1896 17.1363 24.5866 18.5922C23.9835 20.0481 23.0996 21.371 21.9853 22.4853C20.871 23.5996 19.5481 24.4835 18.0922 25.0866C16.6363 25.6896 15.0759 26 13.5 26C11.9241 26 10.3637 25.6896 8.9078 25.0866C7.45189 24.4835 6.12902 23.5996 5.01472 22.4853C3.90042 21.371 3.0165 20.0481 2.41345 18.5922C1.81039 17.1363 1.5 15.5759 1.5 14C1.5 10.8174 2.76428 7.76516 5.01472 5.51472C7.26516 3.26428 10.3174 2 13.5 2C16.6826 2 19.7348 3.26428 21.9853 5.51472C24.2357 7.76516 25.5 10.8174 25.5 14Z" stroke="#3ED9A4" stroke-opacity="0.5" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_2" d="M9.5 15L12.5 18L17.5 11" stroke="#3ED9A4" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 914 B |
@@ -1,4 +1,4 @@
|
||||
<svg width="13" height="12" viewBox="0 0 13 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 6.39098L5.68182 10L11 2" stroke="currentColor" stroke-width="3" stroke-linecap="round"
|
||||
<path d="M2 6.39098L5.68182 10L11 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 235 B After Width: | Height: | Size: 235 B |
@@ -10,6 +10,7 @@ export { default as CautionCircleIcon } from './caution-circle.svg';
|
||||
export { default as CautionCircleStrokeIcon } from './caution-circle-stroke.svg';
|
||||
export { default as ChatIcon } from './chat-bubble.svg';
|
||||
export { default as CheckIcon } from './check.svg';
|
||||
export { default as CheckCircleIcon } from './check-circle.svg';
|
||||
export { default as ChevronLeftIcon } from './chevron-left.svg';
|
||||
export { default as ChevronRightIcon } from './chevron-right.svg';
|
||||
export { default as ClockIcon } from './clock.svg';
|
||||
@@ -31,6 +32,7 @@ export { default as GiftboxIcon } from './giftbox.svg';
|
||||
export { default as HelpCircleIcon } from './help-circle.svg';
|
||||
export { default as HideIcon } from './hide.svg';
|
||||
export { default as HistoryIcon } from './history.svg';
|
||||
export { default as LeaderboardIcon } from './leaderboard.svg';
|
||||
export { default as LinkOutIcon } from './link-out.svg';
|
||||
export { default as LockIcon } from './lock.svg';
|
||||
export { default as LogoShortIcon } from './logo-short';
|
||||
@@ -42,6 +44,7 @@ export { default as OrderbookIcon } from './orderbook.svg';
|
||||
export { default as OverviewIcon } from './overview.svg';
|
||||
export { default as PencilIcon } from './pencil.svg';
|
||||
export { default as PlayIcon } from './play.svg';
|
||||
export { default as PlusIcon } from './plus.svg';
|
||||
export { default as PortfolioIcon } from './portfolio.svg';
|
||||
export { default as PositionsIcon } from './positions.svg';
|
||||
export { default as PriceChartIcon } from './price-chart.svg';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="13" height="14" viewBox="0 0 13 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.0996094 2.60156C0.0996094 2.07113 0.310323 1.56242 0.685396 1.18735C1.06047 0.812276 1.56918 0.601563 2.09961 0.601562H10.8996C11.43 0.601563 11.9387 0.812276 12.3138 1.18735C12.6889 1.56242 12.8996 2.07113 12.8996 2.60156C12.8996 3.132 12.6889 3.6407 12.3138 4.01578C11.9387 4.39085 11.43 4.60156 10.8996 4.60156H2.09961C1.56918 4.60156 1.06047 4.39085 0.685396 4.01578C0.310323 3.6407 0.0996094 3.132 0.0996094 2.60156ZM0.699609 6.26796C0.540479 6.26796 0.387867 6.33118 0.275345 6.4437C0.162823 6.55622 0.0996094 6.70883 0.0996094 6.86796C0.0996094 7.02709 0.162823 7.1797 0.275345 7.29223C0.387867 7.40475 0.540479 7.46796 0.699609 7.46796H12.2996C12.4587 7.46796 12.6114 7.40475 12.7239 7.29223C12.8364 7.1797 12.8996 7.02709 12.8996 6.86796C12.8996 6.70883 12.8364 6.55622 12.7239 6.4437C12.6114 6.33118 12.4587 6.26796 12.2996 6.26796H0.699609ZM0.699609 9.13196C0.540479 9.13196 0.387867 9.19518 0.275345 9.3077C0.162823 9.42022 0.0996094 9.57283 0.0996094 9.73196C0.0996094 9.89109 0.162823 10.0437 0.275345 10.1562C0.387867 10.2687 0.540479 10.332 0.699609 10.332H12.2996C12.4587 10.332 12.6114 10.2687 12.7239 10.1562C12.8364 10.0437 12.8996 9.89109 12.8996 9.73196C12.8996 9.57283 12.8364 9.42022 12.7239 9.3077C12.6114 9.19518 12.4587 9.13196 12.2996 9.13196H0.699609ZM0.699609 12.0016C0.540479 12.0016 0.387867 12.0648 0.275345 12.1773C0.162823 12.2898 0.0996094 12.4424 0.0996094 12.6016C0.0996094 12.7607 0.162823 12.9133 0.275345 13.0258C0.387867 13.1383 0.540479 13.2016 0.699609 13.2016H12.2996C12.4587 13.2016 12.6114 13.1383 12.7239 13.0258C12.8364 12.9133 12.8996 12.7607 12.8996 12.6016C12.8996 12.4424 12.8364 12.2898 12.7239 12.1773C12.6114 12.0648 12.4587 12.0016 12.2996 12.0016H0.699609Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="none" height="12" viewBox="0 0 12 12" width="12" xmlns="http://www.w3.org/2000/svg"><path d="m6.75.75c0-.198912-.07902-.389678-.21967-.53033-.14065-.1406525-.33142-.21967-.53033-.21967s-.38968.0790175-.53033.21967c-.14065.140652-.21967.331418-.21967.53033v4.5h-4.5c-.198912 0-.389678.07902-.53033.21967-.1406525.14065-.21967.33142-.21967.53033s.0790175.38968.21967.53033c.140652.14065.331418.21967.53033.21967h4.5v4.5c0 .1989.07902.3897.21967.5303.14065.1407.33142.2197.53033.2197s.38968-.079.53033-.2197c.14065-.1406.21967-.3314.21967-.5303v-4.5h4.5c.1989 0 .3897-.07902.5303-.21967.1407-.14065.2197-.33142.2197-.53033s-.079-.38968-.2197-.53033c-.1406-.14065-.3314-.21967-.5303-.21967h-4.5z" fill="currentColor"/></svg>
|
||||
|
After Width: | Height: | Size: 731 B |
@@ -1,5 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<g fill="#FFFFFF">
|
||||
<path d="M461.2 128H80c-8.84 0-16-7.16-16-16s7.16-16 16-16h384c8.84 0 16-7.16 16-16 0-26.51-21.49-48-48-48H64C28.65 32 0 60.65 0 96v320c0 35.35 28.65 64 64 64h397.2c28.02 0 50.8-21.53 50.8-48V176c0-26.47-22.78-48-50.8-48zM416 336c-17.67 0-32-14.33-32-32s14.33-32 32-32 32 14.33 32 32-14.33 32-32 32z"/>
|
||||
</g>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Wallet" clip-path="url(#clip0_14122_34311)">
|
||||
<g id="Group 151">
|
||||
<path id="Exclude" fill-rule="evenodd" clip-rule="evenodd" d="M11.4979 10.9976C11.1764 11.3193 10.7404 11.5 10.2857 11.5L1.71429 11.5C1.25963 11.5 0.823594 11.3193 0.502102 10.9976C0.180612 10.6758 -4.76837e-07 10.2395 -4.76837e-07 9.78455L-4.76837e-07 2.06502C-4.76837e-07 2.51999 0.180612 2.95632 0.502102 3.27803C0.823594 3.59974 1.25963 3.78047 1.71429 3.78047L10.2857 3.78047C10.7404 3.78047 11.1764 3.96121 11.4979 4.28292C11.8194 4.60462 12 5.04096 12 5.49592V9.78455C12 10.2395 11.8194 10.6758 11.4979 10.9976ZM9.64294 8.7126C10.2347 8.7126 10.7144 8.23258 10.7144 7.64044C10.7144 7.0483 10.2347 6.56828 9.64294 6.56828C9.05121 6.56828 8.57151 7.0483 8.57151 7.64044C8.57151 8.23258 9.05121 8.7126 9.64294 8.7126Z" fill="#C8C7D8"/>
|
||||
<path id="Vector" d="M8.57031 2.92186V2.06413C8.57031 1.60917 8.3897 1.17284 8.06821 0.851128C7.74672 0.529419 7.31068 0.348684 6.85603 0.348684L2.14174 0.348684C1.80075 0.348684 1.47372 0.484235 1.2326 0.725517C0.991486 0.966799 0.856028 1.29405 0.856028 1.63527C0.856028 1.9765 0.991486 2.30374 1.2326 2.54503C1.47372 2.78631 1.80075 2.92186 2.14174 2.92186L8.57031 2.92186Z" fill="#C8C7D8"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_14122_34311">
|
||||
<rect width="12" height="12" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 405 B After Width: | Height: | Size: 1.4 KiB |
@@ -11,17 +11,19 @@ import { DepositDialog } from '@/views/dialogs/DepositDialog';
|
||||
import { DisconnectDialog } from '@/views/dialogs/DisconnectDialog';
|
||||
import { ExchangeOfflineDialog } from '@/views/dialogs/ExchangeOfflineDialog';
|
||||
import { HelpDialog } from '@/views/dialogs/HelpDialog';
|
||||
import { ExternalLinkDialog } from '@/views/dialogs/ExternalLinkDialog';
|
||||
import { ExternalNavKeplrDialog } from '@/views/dialogs/ExternalNavKeplrDialog';
|
||||
import { MnemonicExportDialog } from '@/views/dialogs/MnemonicExportDialog';
|
||||
import { MobileSignInDialog } from '@/views/dialogs/MobileSignInDialog';
|
||||
import { OnboardingDialog } from '@/views/dialogs/OnboardingDialog';
|
||||
import { PreferencesDialog } from '@/views/dialogs/PreferencesDialog';
|
||||
import { RateLimitDialog } from '@/views/dialogs/RateLimitDialog';
|
||||
import { ReceiveDialog } from '@/views/dialogs/ReceiveDialog';
|
||||
import { RestrictedGeoDialog } from '@/views/dialogs/RestrictedGeoDialog';
|
||||
import { TradeDialog } from '@/views/dialogs/TradeDialog';
|
||||
import { TransferDialog } from '@/views/dialogs/TransferDialog';
|
||||
import { RestrictedWalletDialog } from '@/views/dialogs/RestrictedWalletDialog';
|
||||
import { WithdrawDialog } from '@/views/dialogs/WithdrawDialog';
|
||||
import { ManageFundsDialog } from '@/views/dialogs/ManageFundsDialog';
|
||||
|
||||
import { OrderDetailsDialog } from '@/views/dialogs/DetailsDialog/OrderDetailsDialog';
|
||||
import { FillDetailsDialog } from '@/views/dialogs/DetailsDialog/FillDetailsDialog';
|
||||
@@ -52,16 +54,18 @@ export const DialogManager = () => {
|
||||
[DialogTypes.FillDetails]: <FillDetailsDialog {...modalProps} />,
|
||||
[DialogTypes.Help]: <HelpDialog {...modalProps} />,
|
||||
[DialogTypes.ExternalNavKeplr]: <ExternalNavKeplrDialog {...modalProps} />,
|
||||
[DialogTypes.ExternalLink]: <ExternalLinkDialog {...modalProps} />,
|
||||
[DialogTypes.MnemonicExport]: <MnemonicExportDialog {...modalProps} />,
|
||||
[DialogTypes.MobileSignIn]: <MobileSignInDialog {...modalProps} />,
|
||||
[DialogTypes.Onboarding]: <OnboardingDialog {...modalProps} />,
|
||||
[DialogTypes.OrderDetails]: <OrderDetailsDialog {...modalProps} />,
|
||||
[DialogTypes.Preferences]: <PreferencesDialog {...modalProps} />,
|
||||
[DialogTypes.RateLimit]: <RateLimitDialog {...modalProps} />,
|
||||
[DialogTypes.Receive]: <ReceiveDialog {...modalProps} />,
|
||||
[DialogTypes.RestrictedGeo]: <RestrictedGeoDialog {...modalProps} />,
|
||||
[DialogTypes.RestrictedWallet]: <RestrictedWalletDialog {...modalProps} />,
|
||||
[DialogTypes.Trade]: <TradeDialog {...modalProps} />,
|
||||
[DialogTypes.Transfer]: <TransferDialog {...modalProps} />,
|
||||
[DialogTypes.Withdraw]: <WithdrawDialog {...modalProps} />,
|
||||
[DialogTypes.ManageFunds]: <ManageFundsDialog {...modalProps} />,
|
||||
}[type];
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import styled, { type AnyStyledComponent, css } from 'styled-components';
|
||||
|
||||
import { AbacusApiStatus } from '@/constants/abacus';
|
||||
import { ButtonSize } from '@/constants/buttons';
|
||||
import { ButtonSize, ButtonType } from '@/constants/buttons';
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
import { ENVIRONMENT_CONFIG_MAP, isDev } from '@/constants/networks';
|
||||
|
||||
@@ -55,10 +55,12 @@ export const FooterDesktop = () => {
|
||||
}
|
||||
>
|
||||
<Styled.FooterButton
|
||||
type={statusPage ? ButtonType.Link : ButtonType.Button}
|
||||
slotLeft={<Styled.StatusDot exchangeStatus={exchangeStatus} />}
|
||||
slotRight={statusPage && <LinkOutIcon />}
|
||||
size={ButtonSize.XSmall}
|
||||
state={{ isDisabled: !statusPage }}
|
||||
href={statusPage}
|
||||
>
|
||||
{label}
|
||||
</Styled.FooterButton>
|
||||
@@ -132,7 +134,7 @@ Styled.StatusDot = styled.div<{ exchangeStatus: ExchangeStatus }>`
|
||||
Styled.FooterButton = styled(Button)`
|
||||
--button-height: 1.5rem;
|
||||
--button-radius: 0.25rem;
|
||||
--button-backgroundColor: var(--color-layer-2);
|
||||
--button-backgroundColor: transparent;
|
||||
--button-border: none;
|
||||
--button-textColor: var(--color-text-0);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import styled, { type AnyStyledComponent } from 'styled-components';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { ButtonAction, ButtonSize } from '@/constants/buttons';
|
||||
import { DialogTypes } from '@/constants/dialogs';
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
import { DEFAULT_MARKETID } from '@/constants/markets';
|
||||
@@ -11,8 +10,8 @@ import { useShouldShowFooter, useStringGetter } from '@/hooks';
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
import { NavigationMenu } from '@/components/NavigationMenu';
|
||||
import { Icon } from '@/components/Icon';
|
||||
import { BellIcon, MarketsIcon, PlayIcon, PortfolioIcon, ProfileIcon, TradeIcon } from '@/icons';
|
||||
import { Icon, IconName } from '@/components/Icon';
|
||||
import { BellIcon, MarketsIcon, PortfolioIcon, ProfileIcon } from '@/icons';
|
||||
import { IconButton } from '@/components/IconButton';
|
||||
|
||||
import { calculateCanAccountTrade } from '@/state/accountCalculators';
|
||||
@@ -42,11 +41,9 @@ export const FooterMobile = () => {
|
||||
value: 'trade',
|
||||
label: stringGetter({ key: STRING_KEYS.TRADE }),
|
||||
slotBefore: (
|
||||
<Styled.IconButton
|
||||
iconComponent={TradeIcon}
|
||||
size={ButtonSize.Large}
|
||||
action={ButtonAction.Primary}
|
||||
/>
|
||||
<Styled.StartIcon>
|
||||
<Icon iconName={IconName.Trade} />
|
||||
</Styled.StartIcon>
|
||||
),
|
||||
href: `${AppRoute.Trade}/${marketId ?? DEFAULT_MARKETID}`,
|
||||
}
|
||||
@@ -54,11 +51,9 @@ export const FooterMobile = () => {
|
||||
value: 'onboarding',
|
||||
label: stringGetter({ key: STRING_KEYS.ONBOARDING }),
|
||||
slotBefore: (
|
||||
<Styled.IconButton
|
||||
iconComponent={PlayIcon}
|
||||
size={ButtonSize.Large}
|
||||
action={ButtonAction.Primary}
|
||||
/>
|
||||
<Styled.StartIcon>
|
||||
<Icon iconName={IconName.Play} />
|
||||
</Styled.StartIcon>
|
||||
),
|
||||
onClick: () => dispatch(openDialog({ type: DialogTypes.Onboarding })),
|
||||
},
|
||||
@@ -168,3 +163,24 @@ Styled.IconButton = styled(IconButton)`
|
||||
Styled.Icon = styled(Icon)`
|
||||
font-size: 1.5rem;
|
||||
`;
|
||||
|
||||
Styled.StartIcon = styled.div`
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: -0.25rem;
|
||||
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
|
||||
color: var(--color-text-2);
|
||||
background-color: var(--color-accent);
|
||||
border: solid var(--border-width) var(--color-border-white);
|
||||
border-radius: 50%;
|
||||
|
||||
svg {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import styled, { type AnyStyledComponent } from 'styled-components';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { ButtonShape } from '@/constants/buttons';
|
||||
import { DialogTypes } from '@/constants/dialogs';
|
||||
@@ -8,6 +8,10 @@ import { STRING_KEYS } from '@/constants/localization';
|
||||
import { AppRoute } from '@/constants/routes';
|
||||
import { LogoShortIcon, BellStrokeIcon } from '@/icons';
|
||||
|
||||
import { headerMixins } from '@/styles/headerMixins';
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
import breakpoints from '@/styles/breakpoints';
|
||||
|
||||
import { useTokenConfigs, useStringGetter, useURLConfigs } from '@/hooks';
|
||||
|
||||
import { Icon, IconName } from '@/components/Icon';
|
||||
@@ -21,10 +25,7 @@ import { NotificationsMenu } from '@/views/menus/NotificationsMenu';
|
||||
import { LanguageSelector } from '@/views/menus/LanguageSelector';
|
||||
|
||||
import { openDialog } from '@/state/dialogs';
|
||||
|
||||
import { headerMixins } from '@/styles/headerMixins';
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
import breakpoints from '@/styles/breakpoints';
|
||||
import { getHasSeenLaunchIncentives } from '@/state/configsSelectors';
|
||||
|
||||
export const HeaderDesktop = () => {
|
||||
const stringGetter = useStringGetter();
|
||||
@@ -32,6 +33,8 @@ export const HeaderDesktop = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { chainTokenLabel } = useTokenConfigs();
|
||||
|
||||
const hasSeenLaunchIncentives = useSelector(getHasSeenLaunchIncentives);
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
group: 'navigation',
|
||||
@@ -55,6 +58,7 @@ export const HeaderDesktop = () => {
|
||||
value: chainTokenLabel,
|
||||
label: chainTokenLabel,
|
||||
href: `/${chainTokenLabel}`,
|
||||
slotAfter: !hasSeenLaunchIncentives && <Styled.UnreadIndicator />,
|
||||
},
|
||||
{
|
||||
value: 'MORE',
|
||||
@@ -238,3 +242,10 @@ Styled.IconButton = styled(IconButton)<{ size?: string }>`
|
||||
--button-icon-size: 1rem;
|
||||
--button-padding: 0 0.5em;
|
||||
`;
|
||||
|
||||
Styled.UnreadIndicator = styled.div`
|
||||
width: 0.4375rem;
|
||||
height: 0.4375rem;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-accent);
|
||||
`;
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
import { NotificationStatus } from '@/constants/notifications';
|
||||
import { ButtonSize } from '@/constants/buttons';
|
||||
import { useMemo, useState } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import { NotificationStatus } from '@/constants/notifications';
|
||||
import { ButtonShape, ButtonSize } from '@/constants/buttons';
|
||||
import { useNotifications } from '@/hooks/useNotifications';
|
||||
import { useBreakpoints } from '@/hooks/useBreakpoints';
|
||||
import { ChevronLeftIcon } from '@/icons';
|
||||
import { breakpoints } from '@/styles';
|
||||
|
||||
import { ToastArea } from '@/components/ToastArea';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import { Button } from '@/components/Button';
|
||||
|
||||
import styled from 'styled-components';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import { ToastArea } from '@/components/ToastArea';
|
||||
import { ToggleButton } from '@/components/ToggleButton';
|
||||
|
||||
type StyleProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const MAX_TOASTS = 10;
|
||||
|
||||
export const NotificationsToastArea = ({ className }: StyleProps) => {
|
||||
const [shouldStackNotifications, setshouldStackNotifications] = useState(true);
|
||||
|
||||
const {
|
||||
notifications,
|
||||
getKey,
|
||||
@@ -26,10 +33,9 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
|
||||
} = useNotifications();
|
||||
|
||||
const { isMobile } = useBreakpoints();
|
||||
|
||||
return (
|
||||
<StyledToastArea swipeDirection={isMobile ? 'up' : 'right'} className={className}>
|
||||
{Object.values(notifications)
|
||||
const notificationMap = useMemo(() => {
|
||||
return (
|
||||
Object.values(notifications)
|
||||
// Sort by time of first trigger
|
||||
.sort(
|
||||
(n1, n2) =>
|
||||
@@ -41,45 +47,129 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
|
||||
key: getKey(notification),
|
||||
displayData: getDisplayData(notification),
|
||||
}))
|
||||
.filter(({ displayData }) => displayData)
|
||||
.map(({ notification, key, displayData }) => (
|
||||
<Toast
|
||||
key={key}
|
||||
isOpen={notification.status < NotificationStatus.Unseen}
|
||||
slotIcon={displayData.icon}
|
||||
slotTitle={displayData.title}
|
||||
slotDescription={displayData.description}
|
||||
slotCustomContent={displayData.customContent}
|
||||
slotAction={
|
||||
<Button size={ButtonSize.Small} onClick={() => onNotificationAction(notification)}>
|
||||
{displayData.actionDescription}
|
||||
</Button>
|
||||
}
|
||||
actionDescription={displayData.actionDescription}
|
||||
actionAltText={displayData.actionAltText}
|
||||
duration={isMenuOpen ? Infinity : displayData.toastDuration}
|
||||
sensitivity={displayData.toastSensitivity}
|
||||
setIsOpen={(isOpen, isClosedFromTimeout) => {
|
||||
if (!isOpen)
|
||||
if (isClosedFromTimeout)
|
||||
// Toast timer expired without user interaction
|
||||
markUnseen(notification);
|
||||
// Toast interacted with or dismissed
|
||||
else markSeen(notification);
|
||||
}}
|
||||
lastUpdated={notification.timestamps[notification.status]}
|
||||
/>
|
||||
))}
|
||||
.filter(
|
||||
({ displayData, notification }) =>
|
||||
displayData && notification.status < NotificationStatus.Unseen
|
||||
)
|
||||
.slice(-MAX_TOASTS)
|
||||
);
|
||||
}, [notifications, getKey, getDisplayData]);
|
||||
|
||||
if (isMenuOpen) return null;
|
||||
|
||||
const hasMultipleToasts = notificationMap.length > 1;
|
||||
|
||||
return (
|
||||
<StyledToastArea swipeDirection={isMobile ? 'up' : 'right'} className={className}>
|
||||
{hasMultipleToasts && (
|
||||
<StyledToggleButton
|
||||
shape={ButtonShape.Pill}
|
||||
size={ButtonSize.XSmall}
|
||||
isPressed={shouldStackNotifications}
|
||||
onPressedChange={() => setshouldStackNotifications(!shouldStackNotifications)}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
</StyledToggleButton>
|
||||
)}
|
||||
|
||||
{notificationMap.map(({ notification, key, displayData }, idx) => (
|
||||
<StyledToast
|
||||
key={key}
|
||||
isStacked={shouldStackNotifications}
|
||||
isOpen={notification.status < NotificationStatus.Unseen}
|
||||
layer={notificationMap.length - 1 - idx}
|
||||
notification={notification}
|
||||
slotIcon={displayData.icon}
|
||||
slotTitle={displayData.title}
|
||||
slotTitleLeft={displayData.slotTitleLeft}
|
||||
slotTitleRight={displayData.slotTitleRight}
|
||||
slotDescription={displayData.body}
|
||||
slotCustomContent={displayData.renderCustomBody?.({ isToast: true, notification })}
|
||||
onClick={() => onNotificationAction(notification)}
|
||||
slotAction={
|
||||
<Button size={ButtonSize.Small} onClick={() => onNotificationAction(notification)}>
|
||||
{displayData.actionDescription}
|
||||
</Button>
|
||||
}
|
||||
actionDescription={displayData.actionDescription}
|
||||
actionAltText={displayData.actionAltText}
|
||||
duration={displayData.toastDuration ?? Infinity}
|
||||
sensitivity={displayData.toastSensitivity}
|
||||
setIsOpen={(isOpen: boolean, isClosedFromTimeout?: boolean) => {
|
||||
if (!isOpen)
|
||||
if (isClosedFromTimeout)
|
||||
// Toast timer expired without user interaction
|
||||
markUnseen(notification);
|
||||
// Toast interacted with or dismissed
|
||||
else markSeen(notification);
|
||||
}}
|
||||
lastUpdated={notification.timestamps[notification.status]}
|
||||
/>
|
||||
))}
|
||||
</StyledToastArea>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledToastArea = styled(ToastArea)`
|
||||
position: absolute;
|
||||
width: min(16rem, 100%);
|
||||
width: min(17.5rem, 100%);
|
||||
inset: 0 0 0 auto;
|
||||
|
||||
padding: 0.75rem 0.75rem 0.75rem 0;
|
||||
|
||||
mask-image: linear-gradient(to left, transparent, white 0.5rem);
|
||||
|
||||
@media ${breakpoints.mobile} {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledToast = styled(Toast)<{ isStacked?: boolean; layer: number }>`
|
||||
// Stacked toast
|
||||
${({ isStacked, layer }) =>
|
||||
isStacked
|
||||
? css`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(${layer} * 2px);
|
||||
right: calc(${layer} * -2px);
|
||||
`
|
||||
: css`
|
||||
margin-bottom: 0.75rem;
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledToggleButton = styled(ToggleButton)<{ isPressed: boolean }>`
|
||||
z-index: 2;
|
||||
pointer-events: auto;
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: calc(50% - 0.75rem);
|
||||
--button-width: 2rem;
|
||||
--button-height: 1rem;
|
||||
|
||||
${StyledToastArea}:hover & {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
> svg {
|
||||
width: 4px;
|
||||
margin-top: 1px;
|
||||
transform: rotate(-90deg);
|
||||
|
||||
${({ isPressed }) =>
|
||||
!isPressed &&
|
||||
css`
|
||||
transform: rotate(90deg);
|
||||
`}
|
||||
}
|
||||
|
||||
@media ${breakpoints.mobile} {
|
||||
display: flex;
|
||||
left: calc(50% - 2rem);
|
||||
--button-width: 4rem;
|
||||
--button-height: 2rem;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import Abacus, { type Nullable } from '@dydxprotocol/v4-abacus';
|
||||
import Long from 'long';
|
||||
import type { IndexedTx } from '@cosmjs/stargate';
|
||||
import { GAS_MULTIPLIER, encodeJson } from '@dydxprotocol/v4-client-js';
|
||||
import { EncodeObject } from '@cosmjs/proto-signing';
|
||||
|
||||
import {
|
||||
CompositeClient,
|
||||
@@ -8,6 +10,7 @@ import {
|
||||
type LocalWallet,
|
||||
Network,
|
||||
NetworkOptimizer,
|
||||
NobleClient,
|
||||
SubaccountClient,
|
||||
ValidatorConfig,
|
||||
OrderType,
|
||||
@@ -39,11 +42,14 @@ import { openDialog } from '@/state/dialogs';
|
||||
import { StatefulOrderError } from '../errors';
|
||||
import { bytesToBigInt } from '../numbers';
|
||||
import { log } from '../telemetry';
|
||||
import { hashFromTx } from '../hashfromTx';
|
||||
|
||||
class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
|
||||
private compositeClient: CompositeClient | undefined;
|
||||
private nobleClient: NobleClient | undefined;
|
||||
private store: RootStore | undefined;
|
||||
private localWallet: LocalWallet | undefined;
|
||||
private nobleWallet: LocalWallet | undefined;
|
||||
|
||||
constructor() {
|
||||
this.compositeClient = undefined;
|
||||
@@ -58,6 +64,17 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
|
||||
this.localWallet = localWallet;
|
||||
}
|
||||
|
||||
setNobleWallet(nobleWallet: LocalWallet) {
|
||||
try {
|
||||
this.nobleWallet = nobleWallet;
|
||||
if (this.nobleClient) {
|
||||
this.nobleClient.connect(nobleWallet);
|
||||
}
|
||||
} catch (e) {
|
||||
log('DydxChainTransactions/setNobleWallet', e);
|
||||
}
|
||||
}
|
||||
|
||||
async connectNetwork(
|
||||
paramsInJson: Nullable<string>,
|
||||
callback: (p0: Nullable<string>) => void
|
||||
@@ -69,6 +86,7 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
|
||||
websocketUrl,
|
||||
validatorUrl,
|
||||
chainId,
|
||||
nobleValidatorUrl,
|
||||
USDC_DENOM,
|
||||
USDC_DECIMALS,
|
||||
USDC_GAS_DENOM,
|
||||
@@ -100,6 +118,15 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
|
||||
|
||||
this.compositeClient = compositeClient;
|
||||
|
||||
try {
|
||||
if (nobleValidatorUrl) {
|
||||
this.nobleClient = new NobleClient(nobleValidatorUrl);
|
||||
if (this.nobleWallet) await this.nobleClient.connect(this.nobleWallet);
|
||||
}
|
||||
} catch (e) {
|
||||
log('DydxChainTransactions/connectNetwork/NobleClient', e);
|
||||
}
|
||||
|
||||
// Dispatch custom event to notify other parts of the app that the network has been connected
|
||||
const customEvent = new CustomEvent('abacus:connectNetwork', {
|
||||
detail: parsedParams,
|
||||
@@ -185,8 +212,8 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
|
||||
timeInForce as OrderTimeInForce,
|
||||
goodTilTimeInSeconds ?? 0,
|
||||
execution as OrderExecution,
|
||||
postOnly,
|
||||
reduceOnly,
|
||||
postOnly ?? undefined,
|
||||
reduceOnly ?? undefined,
|
||||
triggerPrice ?? undefined
|
||||
);
|
||||
|
||||
@@ -195,19 +222,19 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
|
||||
throw new StatefulOrderError('Stateful order has failed to commit.', tx);
|
||||
}
|
||||
|
||||
const parsedTx = this.parseToPrimitives(tx);
|
||||
const hash = parsedTx?.hash;
|
||||
const encodedTx = encodeJson(tx);
|
||||
const parsedTx = JSON.parse(encodedTx);
|
||||
const hash = parsedTx.hash.toUpperCase();
|
||||
|
||||
if (isTestnet) {
|
||||
console.log(
|
||||
`${
|
||||
ENVIRONMENT_CONFIG_MAP[this.compositeClient.network.getString() as DydxNetwork]?.links
|
||||
?.mintscanBase
|
||||
}/txs/${hash}`
|
||||
`${ENVIRONMENT_CONFIG_MAP[
|
||||
this.compositeClient.network.getString() as DydxNetwork
|
||||
]?.links?.mintscan?.replace('{tx_hash}', hash.toString())}`
|
||||
);
|
||||
} else console.log(`txHash: ${hash}`);
|
||||
|
||||
return JSON.stringify(parsedTx);
|
||||
return encodedTx;
|
||||
} catch (error) {
|
||||
if (error?.name !== 'BroadcastError') {
|
||||
log('DydxChainTransactions/placeOrderTransaction', error);
|
||||
@@ -237,9 +264,14 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
|
||||
goodTilBlockTime || undefined
|
||||
);
|
||||
|
||||
const parsedTx = this.parseToPrimitives(tx);
|
||||
const encodedTx = encodeJson(tx);
|
||||
|
||||
return JSON.stringify(parsedTx);
|
||||
if (import.meta.env.MODE === 'development') {
|
||||
const parsedTx = JSON.parse(encodedTx);
|
||||
console.log(parsedTx, parsedTx.hash.toUpperCase());
|
||||
}
|
||||
|
||||
return encodedTx;
|
||||
} catch (error) {
|
||||
log('DydxChainTransactions/cancelOrderTransaction', error);
|
||||
|
||||
@@ -319,6 +351,122 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
async sendNobleIBC(params: { msgTypeUrl: string; msg: any }): Promise<string> {
|
||||
if (!this.nobleClient?.isConnected) {
|
||||
throw new Error('Missing nobleClient or localWallet');
|
||||
}
|
||||
|
||||
try {
|
||||
const ibcMsg = {
|
||||
typeUrl: params.msgTypeUrl, // '/ibc.applications.transfer.v1.MsgTransfer',
|
||||
value: params.msg,
|
||||
};
|
||||
const fee = await this.nobleClient.simulateTransaction([ibcMsg]);
|
||||
|
||||
// take out fee from amount before sweeping
|
||||
const amount =
|
||||
parseInt(ibcMsg.value.token.amount, 10) -
|
||||
Math.floor(parseInt(fee.amount[0].amount, 10) * GAS_MULTIPLIER);
|
||||
|
||||
if (amount <= 0) {
|
||||
throw new Error('noble balance does not cover fees');
|
||||
}
|
||||
|
||||
ibcMsg.value.token.amount = amount.toString();
|
||||
const tx = await this.nobleClient.send([ibcMsg]);
|
||||
|
||||
const parsedTx = this.parseToPrimitives(tx);
|
||||
|
||||
return JSON.stringify(parsedTx);
|
||||
} catch (error) {
|
||||
log('DydxChainTransactions/sendNobleIBC', error);
|
||||
|
||||
return JSON.stringify({
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async withdrawToNobleIBC(params: {
|
||||
subaccountNumber: number;
|
||||
amount: string;
|
||||
ibcPayload: string;
|
||||
}): Promise<string> {
|
||||
if (!this.compositeClient || !this.localWallet) {
|
||||
throw new Error('Missing compositeClient or localWallet');
|
||||
}
|
||||
|
||||
const { subaccountNumber, amount, ibcPayload } = params ?? {};
|
||||
const parsedIbcPayload: {
|
||||
msgTypeUrl: string;
|
||||
msg: any;
|
||||
} = ibcPayload ? JSON.parse(atob(ibcPayload)) : undefined;
|
||||
|
||||
try {
|
||||
const msg = this.compositeClient.withdrawFromSubaccountMessage(
|
||||
new SubaccountClient(this.localWallet, subaccountNumber),
|
||||
parseFloat(amount).toFixed(this.compositeClient.validatorClient.config.denoms.USDC_DECIMALS)
|
||||
);
|
||||
const ibcMsg: EncodeObject = {
|
||||
typeUrl: parsedIbcPayload.msgTypeUrl,
|
||||
value: parsedIbcPayload.msg,
|
||||
};
|
||||
|
||||
const tx = await this.compositeClient.send(
|
||||
this.localWallet,
|
||||
() => Promise.resolve([msg, ibcMsg]),
|
||||
false
|
||||
);
|
||||
|
||||
return JSON.stringify({
|
||||
txHash: hashFromTx(tx?.hash),
|
||||
});
|
||||
} catch (error) {
|
||||
log('DydxChainTransactions/withdrawToNobleIBC', error);
|
||||
|
||||
return JSON.stringify({
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async cctpWithdraw(params: { typeUrl: string; value: any }): Promise<string> {
|
||||
if (!this.nobleClient?.isConnected) {
|
||||
throw new Error('Missing nobleClient or localWallet');
|
||||
}
|
||||
|
||||
try {
|
||||
const ibcMsg = {
|
||||
typeUrl: params.typeUrl, // '/circle.cctp.v1.MsgDepositForBurn',
|
||||
value: params.value,
|
||||
};
|
||||
const fee = await this.nobleClient.simulateTransaction([ibcMsg]);
|
||||
|
||||
// take out fee from amount before sweeping
|
||||
const amount =
|
||||
parseInt(ibcMsg.value.amount, 10) -
|
||||
Math.floor(parseInt(fee.amount[0].amount, 10) * GAS_MULTIPLIER);
|
||||
|
||||
if (amount <= 0) {
|
||||
throw new Error('noble balance does not cover fees');
|
||||
}
|
||||
|
||||
ibcMsg.value.amount = amount.toString();
|
||||
|
||||
const tx = await this.nobleClient.send([ibcMsg]);
|
||||
|
||||
const parsedTx = this.parseToPrimitives(tx);
|
||||
|
||||
return JSON.stringify(parsedTx);
|
||||
} catch (error) {
|
||||
log('DydxChainTransactions/cctpWithdraw', error);
|
||||
|
||||
return JSON.stringify({
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async transaction(
|
||||
type: TransactionTypes,
|
||||
paramsInJson: Abacus.Nullable<string>,
|
||||
@@ -348,6 +496,22 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
|
||||
callback(result);
|
||||
break;
|
||||
}
|
||||
case TransactionType.SendNobleIBC: {
|
||||
const result = await this.sendNobleIBC(params);
|
||||
callback(result);
|
||||
break;
|
||||
}
|
||||
case TransactionType.WithdrawToNobleIBC: {
|
||||
const result = await this.withdrawToNobleIBC(params);
|
||||
callback(result);
|
||||
break;
|
||||
}
|
||||
case TransactionType.CctpWithdraw: {
|
||||
const result = await this.cctpWithdraw(params);
|
||||
callback(result);
|
||||
break;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
@@ -435,8 +599,13 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
|
||||
const parseDelegations = this.parseToPrimitives(delegations);
|
||||
callback(JSON.stringify(parseDelegations));
|
||||
break;
|
||||
// Do not implement Transfers (yet)
|
||||
case QueryType.Transfers:
|
||||
case QueryType.GetNobleBalance:
|
||||
if (this.nobleClient?.isConnected) {
|
||||
const nobleBalance = await this.nobleClient.getAccountBalance('uusdc');
|
||||
const parsedNobleBalance = this.parseToPrimitives(nobleBalance);
|
||||
callback(JSON.stringify(parsedNobleBalance));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -26,12 +26,14 @@ import {
|
||||
} from '@/constants/abacus';
|
||||
|
||||
import { DEFAULT_MARKETID } from '@/constants/markets';
|
||||
import { CURRENT_ABACUS_DEPLOYMENT, type DydxNetwork } from '@/constants/networks';
|
||||
import { CURRENT_ABACUS_DEPLOYMENT, type DydxNetwork, isMainnet } from '@/constants/networks';
|
||||
import { CLEARED_SIZE_INPUTS, CLEARED_TRADE_INPUTS } from '@/constants/trade';
|
||||
|
||||
import type { RootStore } from '@/state/_store';
|
||||
import { setTradeFormInputs } from '@/state/inputs';
|
||||
import { getInputTradeOptions, getTransferInputs } from '@/state/inputsSelectors';
|
||||
|
||||
import { getInputTradeOptions } from '@/state/inputsSelectors';
|
||||
import { getTransferInputs } from '@/state/inputsSelectors';
|
||||
import { testFlags } from '@/lib/testFlags';
|
||||
|
||||
import AbacusRest from './rest';
|
||||
import AbacusAnalytics from './analytics';
|
||||
@@ -43,6 +45,7 @@ import AbacusFormatter from './formatter';
|
||||
import AbacusThreading from './threading';
|
||||
import AbacusFileSystem from './filesystem';
|
||||
import { LocaleSeparators } from '../numbers';
|
||||
|
||||
class AbacusStateManager {
|
||||
private store: RootStore | undefined;
|
||||
private currentMarket: string | undefined;
|
||||
@@ -80,10 +83,13 @@ class AbacusStateManager {
|
||||
this.abacusFormatter
|
||||
);
|
||||
|
||||
const appConfigs = AbacusAppConfig.Companion.forWeb;
|
||||
appConfigs.squidVersion = AbacusAppConfig.SquidVersion.V2;
|
||||
|
||||
this.stateManager = new AsyncAbacusStateManager(
|
||||
'',
|
||||
CURRENT_ABACUS_DEPLOYMENT,
|
||||
AbacusAppConfig.Companion.forWeb,
|
||||
appConfigs,
|
||||
ioImplementations,
|
||||
uiImplementations,
|
||||
// @ts-ignore
|
||||
@@ -132,6 +138,8 @@ class AbacusStateManager {
|
||||
this.setTradeValue({ value: null, field: TradeInputField.limitPrice });
|
||||
}
|
||||
|
||||
this.store?.dispatch(setTradeFormInputs(CLEARED_TRADE_INPUTS));
|
||||
|
||||
if (shouldResetSize) {
|
||||
this.setTradeValue({ value: null, field: TradeInputField.size });
|
||||
this.setTradeValue({ value: null, field: TradeInputField.usdcSize });
|
||||
@@ -139,6 +147,8 @@ class AbacusStateManager {
|
||||
if (needsLeverage) {
|
||||
this.setTradeValue({ value: null, field: TradeInputField.leverage });
|
||||
}
|
||||
|
||||
this.store?.dispatch(setTradeFormInputs(CLEARED_SIZE_INPUTS));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -175,6 +185,12 @@ class AbacusStateManager {
|
||||
}
|
||||
};
|
||||
|
||||
setNobleWallet = (nobleWallet?: LocalWallet) => {
|
||||
if (nobleWallet) {
|
||||
this.chainTransactions.setNobleWallet(nobleWallet);
|
||||
}
|
||||
};
|
||||
|
||||
setTransfersSourceAddress = (evmAddress: string) => {
|
||||
this.stateManager.sourceAddress = evmAddress;
|
||||
};
|
||||
@@ -218,18 +234,6 @@ class AbacusStateManager {
|
||||
this.abacusFormatter.setLocaleSeparators({ group, decimal });
|
||||
};
|
||||
|
||||
setTransferStatus = ({
|
||||
hash,
|
||||
fromChainId,
|
||||
toChainId,
|
||||
}: {
|
||||
hash: string;
|
||||
fromChainId?: string;
|
||||
toChainId?: string;
|
||||
}) => {
|
||||
this.stateManager.transferStatus(hash, fromChainId, toChainId);
|
||||
};
|
||||
|
||||
// ------ Transactions ------ //
|
||||
|
||||
placeOrder = (
|
||||
@@ -257,6 +261,14 @@ class AbacusStateManager {
|
||||
) => void
|
||||
) => this.stateManager.cancelOrder(orderId, callback);
|
||||
|
||||
cctpWithdraw = (
|
||||
callback: (
|
||||
success: boolean,
|
||||
parsingError: Nullable<ParsingError>,
|
||||
data: string,
|
||||
) => void
|
||||
): void => this.stateManager.commitCCTPWithdraw(callback);
|
||||
|
||||
// ------ Utils ------ //
|
||||
getHistoricalPnlPeriod = (): Nullable<HistoricalPnlPeriods> =>
|
||||
this.stateManager.historicalPnlPeriod;
|
||||
|
||||
@@ -56,13 +56,8 @@ class AbacusRest implements AbacusRestProtocol {
|
||||
.then(async (response) => {
|
||||
const data = await response.text();
|
||||
|
||||
if (response.ok) {
|
||||
callback(data, response.status);
|
||||
} else {
|
||||
// response not OK, call callback with null data and the status, this includes 400/500 status codes
|
||||
callback(null, response.status);
|
||||
}
|
||||
|
||||
callback(data, response.status);
|
||||
|
||||
try {
|
||||
lastSuccessfulRestRequestByOrigin[new URL(url).origin] = Date.now();
|
||||
} catch {}
|
||||
|
||||
@@ -35,6 +35,7 @@ import { setApiState } from '@/state/app';
|
||||
import { setAssets } from '@/state/assets';
|
||||
import { setConfigs } from '@/state/configs';
|
||||
import { setInputs } from '@/state/inputs';
|
||||
import { updateNotifications } from '@/state/notifications';
|
||||
import { setHistoricalFundings, setLiveTrades, setMarkets, setOrderbook } from '@/state/perpetuals';
|
||||
import { isTruthy } from '../isTruthy';
|
||||
|
||||
@@ -50,6 +51,7 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol {
|
||||
}
|
||||
|
||||
notificationsChanged(notifications: kollections.List<AbacusNotification>): void {
|
||||
this.store?.dispatch(updateNotifications(notifications.toArray()));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,12 @@ import {
|
||||
PONG_MESSAGE_TYPE,
|
||||
} from '@/constants/websocket';
|
||||
|
||||
import { lastSuccessfulWebsocketRequestByOrigin } from '@/hooks/useAnalytics';
|
||||
import { testFlags } from '@/lib/testFlags';
|
||||
|
||||
import { subscriptionsByChannelId } from '@/lib/tradingView/dydxfeed/cache';
|
||||
import { mapCandle } from '@/lib/tradingView/utils';
|
||||
|
||||
import { lastSuccessfulWebsocketRequestByOrigin } from '@/hooks/useAnalytics';
|
||||
|
||||
import { log } from '../telemetry';
|
||||
|
||||
const RECONNECT_INTERVAL_MS = 10_000;
|
||||
@@ -144,6 +145,23 @@ class AbacusWebsocket implements Omit<AbacusWebsocketProtocol, '__doNotUseOrImpl
|
||||
|
||||
break;
|
||||
}
|
||||
case 'v4_markets': {
|
||||
if (testFlags.displayInitializingMarkets) {
|
||||
shouldProcess = false;
|
||||
const { contents } = parsedMessage;
|
||||
|
||||
Object.keys(contents.markets ?? {}).forEach((market: any) => {
|
||||
const status = contents.markets[market].status;
|
||||
if (status === 'INITIALIZING') {
|
||||
contents.markets[market].status = 'ONLINE';
|
||||
}
|
||||
});
|
||||
|
||||
this.receivedCallback?.(JSON.stringify(parsedMessage));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const hashFromTx = (
|
||||
txHash: string | Uint8Array
|
||||
): string => `0x${Buffer.from(txHash).toString('hex')}`;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { isMainnet } from '@/constants/networks';
|
||||
import { GetStatus, StatusResponse } from '@0xsquid/sdk';
|
||||
|
||||
export const NATIVE_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
|
||||
|
||||
export const STATUS_ERROR_GRACE_PERIOD = 300_000;
|
||||
|
||||
const getSquidStatusUrl = (isV2: boolean) => {
|
||||
if (isV2) {
|
||||
return isMainnet
|
||||
? 'https://v2.api.squidrouter.com/v2/status'
|
||||
: 'https://testnet.v2.api.squidrouter.com/v2/status';
|
||||
}
|
||||
return isMainnet
|
||||
? 'https://api.squidrouter.com/v1/status'
|
||||
: 'https://testnet.api.squidrouter.com/v1/status';
|
||||
};
|
||||
|
||||
export const fetchSquidStatus = async (
|
||||
params: GetStatus,
|
||||
isV2?: boolean,
|
||||
integratorId?: string
|
||||
): Promise<StatusResponse> => {
|
||||
const parsedParams: { [key: string]: string } = {
|
||||
transactionId: params.transactionId,
|
||||
fromChainId: String(params.fromChainId),
|
||||
toChainId: String(params.toChainId),
|
||||
};
|
||||
if (isV2) parsedParams.bridgeType = 'cctp';
|
||||
const url = `${getSquidStatusUrl(!!isV2)}?${new URLSearchParams(parsedParams).toString()}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"x-integrator-id": integratorId || 'dYdX-api'
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const getNobleChainId = () => {
|
||||
return isMainnet ? 'noble-1' : 'grand-1';
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
class TestFlags {
|
||||
public queryParams: { [key: string]: string };
|
||||
|
||||
constructor() {
|
||||
this.queryParams = {};
|
||||
const hash = window.location.hash;
|
||||
const queryIndex = hash.indexOf('?');
|
||||
if (queryIndex === -1) return;
|
||||
|
||||
const queryParamsString = hash.substring(queryIndex + 1);
|
||||
const params = new URLSearchParams(queryParamsString);
|
||||
|
||||
for (const [key, value] of params) {
|
||||
this.queryParams[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
get displayInitializingMarkets() {
|
||||
return !!this.queryParams.displayInitializingMarkets;
|
||||
}
|
||||
|
||||
get withCCTP() {
|
||||
return !!this.queryParams.withCCTP;
|
||||
}
|
||||
}
|
||||
|
||||
export const testFlags = new TestFlags();
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createConfig, configureChains, mainnet, Chain, Connector, usePublicClient } from 'wagmi';
|
||||
import { goerli } from 'wagmi/chains';
|
||||
|
||||
import { createConfig, createConnector, http, usePublicClient } from 'wagmi';
|
||||
import {
|
||||
type Chain,
|
||||
arbitrum,
|
||||
arbitrumGoerli,
|
||||
avalanche,
|
||||
@@ -26,34 +25,33 @@ import {
|
||||
fantomTestnet,
|
||||
celo,
|
||||
celoAlfajores,
|
||||
} from 'viem/chains';
|
||||
mainnet,
|
||||
goerli,
|
||||
sepolia,
|
||||
} from 'wagmi/chains';
|
||||
|
||||
import { alchemyProvider } from 'wagmi/providers/alchemy';
|
||||
import { jsonRpcProvider } from 'wagmi/providers/jsonRpc';
|
||||
import { publicProvider } from 'wagmi/providers/public';
|
||||
|
||||
import { CoinbaseWalletConnector } from 'wagmi/connectors/coinbaseWallet';
|
||||
import { InjectedConnector } from 'wagmi/connectors/injected';
|
||||
import { MetaMaskConnector } from 'wagmi/connectors/metaMask';
|
||||
import { WalletConnectConnector } from 'wagmi/connectors/walletConnect';
|
||||
import { coinbaseWallet, injected, metaMask, walletConnect } from 'wagmi/connectors';
|
||||
import type { ExternalProvider } from '@ethersproject/providers';
|
||||
|
||||
import {
|
||||
type WalletConnectConfig,
|
||||
type WalletConnection,
|
||||
WalletConnectionType,
|
||||
type WalletType,
|
||||
walletConnectionTypes,
|
||||
WalletType,
|
||||
WALLET_CONNECTION_TYPES,
|
||||
wallets,
|
||||
WALLET_CONNECT_EXPLORER_RECOMMENDED_IDS,
|
||||
WalletConfig,
|
||||
} from '@/constants/wallets';
|
||||
|
||||
import { isTruthy } from './isTruthy';
|
||||
import { log } from './telemetry';
|
||||
|
||||
// Config
|
||||
|
||||
export const WAGMI_SUPPORTED_CHAINS: Chain[] = [
|
||||
const WAGMI_SUPPORTED_CHAINS: Parameters<typeof createConfig>[0]['chains'] = [
|
||||
mainnet,
|
||||
goerli,
|
||||
|
||||
sepolia,
|
||||
arbitrum,
|
||||
arbitrumGoerli,
|
||||
avalanche,
|
||||
@@ -80,118 +78,104 @@ export const WAGMI_SUPPORTED_CHAINS: Chain[] = [
|
||||
celoAlfajores,
|
||||
];
|
||||
|
||||
const { chains, publicClient, webSocketPublicClient } = configureChains(
|
||||
WAGMI_SUPPORTED_CHAINS,
|
||||
[
|
||||
import.meta.env.VITE_ALCHEMY_API_KEY &&
|
||||
alchemyProvider({ apiKey: import.meta.env.VITE_ALCHEMY_API_KEY }),
|
||||
jsonRpcProvider({
|
||||
rpc: (chain) => ({ http: chain.rpcUrls.default.http[0] }),
|
||||
}),
|
||||
publicProvider(),
|
||||
].filter(isTruthy)
|
||||
);
|
||||
export const config = createConfig({
|
||||
chains: WAGMI_SUPPORTED_CHAINS,
|
||||
transports: Object.fromEntries(
|
||||
Object.values(WAGMI_SUPPORTED_CHAINS).map(({ id }) => [id, http()])
|
||||
),
|
||||
});
|
||||
|
||||
const injectedConnectorOptions = {
|
||||
chains,
|
||||
options: {
|
||||
name: 'Injected',
|
||||
const createdInjectedConnectorWithProvider = ({ provider }: { provider: ExternalProvider }) => {
|
||||
console.log(provider);
|
||||
return injected({
|
||||
target() {
|
||||
return {
|
||||
id: 'windowProvider',
|
||||
name: 'Injected',
|
||||
provider,
|
||||
};
|
||||
},
|
||||
shimDisconnect: true,
|
||||
shimChainChangedDisconnect: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
type WalletConnectConfig = {
|
||||
client: {
|
||||
name: string;
|
||||
description: string;
|
||||
iconUrl: string;
|
||||
};
|
||||
v2: {
|
||||
projectId: string;
|
||||
};
|
||||
const getConnector = ({
|
||||
walletConnectionType,
|
||||
walletConnectConfig,
|
||||
}: {
|
||||
walletConnectionType: WalletConnectionType;
|
||||
walletConnectConfig: WalletConnectConfig;
|
||||
}) => {
|
||||
console.log(walletConnectionType);
|
||||
switch (walletConnectionType) {
|
||||
case WalletConnectionType.WalletConnect2:
|
||||
return [
|
||||
walletConnect(
|
||||
getConnectorInfoForWc2({
|
||||
walletConnectConfig,
|
||||
})
|
||||
),
|
||||
];
|
||||
case WalletConnectionType.CoinbaseWalletSdk:
|
||||
return [
|
||||
coinbaseWallet({
|
||||
appName: 'dYdX',
|
||||
appLogoUrl: walletConnectConfig.client.iconUrl,
|
||||
reloadOnDisconnect: false,
|
||||
darkMode: true,
|
||||
}),
|
||||
];
|
||||
case WalletConnectionType.InjectedEip1193:
|
||||
return injected({
|
||||
target() {
|
||||
return {
|
||||
id: 'windowProvider',
|
||||
name: 'Injected',
|
||||
provider: window.ethereum,
|
||||
};
|
||||
},
|
||||
shimDisconnect: true,
|
||||
});
|
||||
case WalletConnectionType.CosmosSigner:
|
||||
default: {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getWalletconnect2ConnectorOptions = (
|
||||
config: WalletConnectConfig
|
||||
): ConstructorParameters<typeof WalletConnectConnector>[0] => ({
|
||||
chains,
|
||||
options: {
|
||||
projectId: config.v2.projectId,
|
||||
const getConnectorInfoForWc2 = ({
|
||||
walletConnectId,
|
||||
walletConnectConfig,
|
||||
}: {
|
||||
walletConnectId?: string;
|
||||
walletConnectConfig: WalletConnectConfig;
|
||||
}) => {
|
||||
const explorerRecommendedWalletIds = walletConnectId
|
||||
? [walletConnectId]
|
||||
: WALLET_CONNECT_EXPLORER_RECOMMENDED_IDS;
|
||||
|
||||
return {
|
||||
projectId: walletConnectConfig.v2.projectId,
|
||||
metadata: {
|
||||
name: config.client.name,
|
||||
description: config.client.description,
|
||||
name: walletConnectConfig.client.name,
|
||||
description: walletConnectConfig.client.description,
|
||||
url: import.meta.env.VITE_BASE_URL,
|
||||
icons: [config.client.iconUrl],
|
||||
icons: [walletConnectConfig.client.iconUrl],
|
||||
},
|
||||
showQrModal: true,
|
||||
qrModalOptions: {
|
||||
themeMode: 'dark' as const,
|
||||
themeVariables: {
|
||||
'--wcm-accent-color': '#5973fe',
|
||||
'--wcm-accent-color': 'var(--color-accent)',
|
||||
'--wcm-font-family': 'var(--fontFamily-base)',
|
||||
'--wcm-background-color': 'var(--color-accent)',
|
||||
},
|
||||
explorerRecommendedWalletIds: WALLET_CONNECT_EXPLORER_RECOMMENDED_IDS,
|
||||
explorerRecommendedWalletIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const getConnectors = (walletConnectConfig: WalletConnectConfig) => [
|
||||
new MetaMaskConnector({
|
||||
chains,
|
||||
options: {
|
||||
shimDisconnect: true,
|
||||
},
|
||||
}),
|
||||
new CoinbaseWalletConnector({
|
||||
chains,
|
||||
options: {
|
||||
appName: 'wagmi',
|
||||
reloadOnDisconnect: false,
|
||||
},
|
||||
}),
|
||||
new WalletConnectConnector(getWalletconnect2ConnectorOptions(walletConnectConfig)),
|
||||
new InjectedConnector(injectedConnectorOptions),
|
||||
];
|
||||
|
||||
export const config = createConfig({
|
||||
autoConnect: true,
|
||||
// connectors,
|
||||
publicClient,
|
||||
webSocketPublicClient,
|
||||
});
|
||||
|
||||
// Custom connectors
|
||||
|
||||
import type { ExternalProvider } from '@ethersproject/providers';
|
||||
|
||||
// Create a custom wagmi InjectedConnector using a specific injected EIP-1193 provider (instead of wagmi's default detection logic)
|
||||
const createInjectedConnectorWithProvider = (provider: ExternalProvider) =>
|
||||
new (class extends InjectedConnector {
|
||||
getProvider = async () =>
|
||||
provider as unknown as Awaited<ReturnType<InjectedConnector['getProvider']>>;
|
||||
})(injectedConnectorOptions) as InjectedConnector;
|
||||
|
||||
const createWalletConnect2ConnectorWithId = (
|
||||
walletconnectId: string,
|
||||
walletConnectConfig: WalletConnectConfig
|
||||
) => {
|
||||
const walletconnect2ConnectorOptions = getWalletconnect2ConnectorOptions(walletConnectConfig);
|
||||
return new WalletConnectConnector({
|
||||
...walletconnect2ConnectorOptions,
|
||||
options: {
|
||||
...walletconnect2ConnectorOptions.options,
|
||||
qrModalOptions: {
|
||||
...walletconnect2ConnectorOptions.options.qrModalOptions,
|
||||
explorerRecommendedWalletIds: [walletconnectId],
|
||||
explorerExcludedWalletIds: 'ALL',
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// Custom connector from wallet selection
|
||||
|
||||
export const resolveWagmiConnector = ({
|
||||
walletType,
|
||||
walletConnection,
|
||||
@@ -202,13 +186,23 @@ export const resolveWagmiConnector = ({
|
||||
walletConnectConfig: WalletConnectConfig;
|
||||
}) => {
|
||||
const walletConfig = wallets[walletType];
|
||||
const walletConnectionConfig = walletConnectionTypes[walletConnection.type];
|
||||
|
||||
return walletConnection.type === WalletConnectionType.InjectedEip1193 && walletConnection.provider
|
||||
? createInjectedConnectorWithProvider(walletConnection.provider)
|
||||
: walletConnection.type === WalletConnectionType.WalletConnect2 && walletConfig.walletconnect2Id
|
||||
? createWalletConnect2ConnectorWithId(walletConfig.walletconnect2Id, walletConnectConfig)
|
||||
: getConnectors(walletConnectConfig).find(
|
||||
({ id }: { id: string }) => id === walletConnectionConfig.wagmiConnectorId
|
||||
);
|
||||
if (walletConnection.type === WalletConnectionType.InjectedEip1193 && walletConnection.provider) {
|
||||
if (walletType === WalletType.MetaMask) {
|
||||
return metaMask();
|
||||
}
|
||||
return createdInjectedConnectorWithProvider({ provider: walletConnection.provider });
|
||||
} else if (
|
||||
walletConnection.type === WalletConnectionType.WalletConnect2 &&
|
||||
walletConfig.walletconnect2Id
|
||||
) {
|
||||
return walletConnect(
|
||||
getConnectorInfoForWc2({
|
||||
walletConnectId: walletConfig.walletconnect2Id,
|
||||
walletConnectConfig,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return getConnector({ walletConnectionType: walletConnection.type, walletConnectConfig });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { DialogPlacement } from '@/components/Dialog';
|
||||
import { NotificationsMenu } from '@/views/menus/NotificationsMenu';
|
||||
|
||||
export const AlertsPage = () => <NotificationsMenu placement={DialogPlacement.Inline} />;
|
||||
const AlertsPage = () => <NotificationsMenu placement={DialogPlacement.Inline} />;
|
||||
|
||||
export default AlertsPage;
|
||||
|
||||
@@ -2,7 +2,7 @@ import styled, { AnyStyledComponent } from 'styled-components';
|
||||
|
||||
import { articleMixins } from '@/styles/articleMixins';
|
||||
|
||||
export const PrivacyPolicyPage = () => (
|
||||
const PrivacyPolicyPage = () => (
|
||||
<Styled.Article>
|
||||
<header>
|
||||
<h1>Privacy Policy</h1>
|
||||
@@ -10,6 +10,8 @@ export const PrivacyPolicyPage = () => (
|
||||
</Styled.Article>
|
||||
);
|
||||
|
||||
export default PrivacyPolicyPage;
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.Article = styled.article`
|
||||
|
||||
@@ -2,12 +2,16 @@ import styled, { AnyStyledComponent, css } from 'styled-components';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useEnsName } from 'wagmi';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { ButtonSize } from '@/constants/buttons';
|
||||
import { TransferInputField, TransferType } from '@/constants/abacus';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
import { Details } from '@/components/Details';
|
||||
import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable';
|
||||
import { IconName } from '@/components/Icon';
|
||||
import { Icon, IconName } from '@/components/Icon';
|
||||
import { IconButton, type IconButtonProps } from '@/components/IconButton';
|
||||
import { Panel } from '@/components/Panel';
|
||||
import { Toolbar } from '@/components/Toolbar';
|
||||
@@ -16,22 +20,22 @@ import { OnboardingState } from '@/constants/account';
|
||||
import { DialogTypes } from '@/constants/dialogs';
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
import { AppRoute, PortfolioRoute, HistoryRoute } from '@/constants/routes';
|
||||
import { wallets, WalletType } from '@/constants/wallets';
|
||||
import { wallets } from '@/constants/wallets';
|
||||
import { useAccounts, useStringGetter, useTokenConfigs } from '@/hooks';
|
||||
|
||||
import { getOnboardingState } from '@/state/accountSelectors';
|
||||
import { openDialog } from '@/state/dialogs';
|
||||
|
||||
import abacusStateManager from '@/lib/abacus';
|
||||
import { isTruthy } from '@/lib/isTruthy';
|
||||
import { truncateAddress } from '@/lib/wallet';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
const ENS_CHAIN_ID = 1; // Ethereum
|
||||
|
||||
const Profile = () => {
|
||||
const stringGetter = useStringGetter();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onboardingState = useSelector(getOnboardingState);
|
||||
const isConnected = onboardingState !== OnboardingState.Disconnected;
|
||||
@@ -46,16 +50,50 @@ const Profile = () => {
|
||||
|
||||
const actions = [
|
||||
{
|
||||
key: 'settings',
|
||||
label: stringGetter({ key: STRING_KEYS.SETTINGS }),
|
||||
icon: { iconName: IconName.Gear },
|
||||
href: AppRoute.Settings,
|
||||
key: 'deposit',
|
||||
label: stringGetter({ key: STRING_KEYS.DEPOSIT }),
|
||||
icon: { iconName: IconName.Deposit },
|
||||
onClick: () => {
|
||||
dispatch(
|
||||
openDialog({
|
||||
type: DialogTypes.ManageFunds,
|
||||
dialogProps: {
|
||||
selectedTransferType: TransferType.deposit.rawValue,
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'withdraw',
|
||||
label: stringGetter({ key: STRING_KEYS.WITHDRAW }),
|
||||
icon: { iconName: IconName.Withdraw },
|
||||
onClick: () => {
|
||||
dispatch(
|
||||
openDialog({
|
||||
type: DialogTypes.ManageFunds,
|
||||
dialogProps: {
|
||||
selectedTransferType: TransferType.withdrawal.rawValue,
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'transfer',
|
||||
label: stringGetter({ key: STRING_KEYS.TRANSFER }),
|
||||
icon: { iconName: IconName.Send },
|
||||
onClick: () => {
|
||||
dispatch(
|
||||
openDialog({
|
||||
type: DialogTypes.ManageFunds,
|
||||
dialogProps: {
|
||||
selectedTransferType: TransferType.transferOut.rawValue,
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
// {
|
||||
// key: 'tutorials',
|
||||
// label: stringGetter({ key: STRING_KEYS.TUTORIALS }),
|
||||
// icon: { iconName: IconName.Comment },
|
||||
// },
|
||||
isConnected
|
||||
? {
|
||||
key: 'sign-out',
|
||||
@@ -66,9 +104,9 @@ const Profile = () => {
|
||||
},
|
||||
}
|
||||
: {
|
||||
key: 'wallet',
|
||||
label: stringGetter({ key: STRING_KEYS.WALLET }),
|
||||
icon: { iconComponent: wallets[walletType || WalletType.OtherWallet].icon },
|
||||
key: 'connect',
|
||||
label: stringGetter({ key: STRING_KEYS.CONNECT }),
|
||||
icon: { iconName: IconName.Transfer },
|
||||
onClick: () => {
|
||||
dispatch(openDialog({ type: DialogTypes.Onboarding }));
|
||||
},
|
||||
@@ -104,12 +142,7 @@ const Profile = () => {
|
||||
{actions.map(({ key, label, href, icon, onClick }) => {
|
||||
const action = (
|
||||
<>
|
||||
<Styled.ActionButton
|
||||
{...icon}
|
||||
size={ButtonSize.Large}
|
||||
onClick={onClick}
|
||||
isCloseIcon={icon.iconName === IconName.Close}
|
||||
/>
|
||||
<Styled.ActionButton {...icon} size={ButtonSize.Large} onClick={onClick} />
|
||||
<span>{label}</span>
|
||||
</>
|
||||
);
|
||||
@@ -122,6 +155,26 @@ const Profile = () => {
|
||||
);
|
||||
})}
|
||||
</Styled.Actions>
|
||||
<Styled.EqualGrid>
|
||||
<Styled.PanelButton
|
||||
slotHeader={
|
||||
<Styled.InlineRow>
|
||||
<Icon iconName={IconName.Gear} />
|
||||
{stringGetter({ key: STRING_KEYS.SETTINGS })}
|
||||
</Styled.InlineRow>
|
||||
}
|
||||
onClick={() => navigate(AppRoute.Settings)}
|
||||
/>
|
||||
<Styled.PanelButton
|
||||
slotHeader={
|
||||
<Styled.InlineRow>
|
||||
<Icon iconName={IconName.HelpCircle} />
|
||||
{stringGetter({ key: STRING_KEYS.HELP })}
|
||||
</Styled.InlineRow>
|
||||
}
|
||||
onClick={() => dispatch(openDialog({ type: DialogTypes.Help }))}
|
||||
/>
|
||||
</Styled.EqualGrid>
|
||||
|
||||
<Styled.EqualGrid>
|
||||
<Panel
|
||||
@@ -180,7 +233,7 @@ Styled.MobileProfileLayout = styled.div`
|
||||
${layoutMixins.contentContainerPage}
|
||||
|
||||
gap: 1rem;
|
||||
padding: 1.25rem;
|
||||
padding: 1.25rem 0.9rem;
|
||||
`;
|
||||
|
||||
Styled.Header = styled.header`
|
||||
@@ -245,19 +298,21 @@ Styled.Actions = styled(Toolbar)`
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.ActionButton = styled(IconButton)<{ isCloseIcon?: boolean }>`
|
||||
Styled.ActionButton = styled(IconButton)<{ iconName?: IconName }>`
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
${({ isCloseIcon }) =>
|
||||
isCloseIcon &&
|
||||
css`
|
||||
--button-textColor: var(--color-negative);
|
||||
|
||||
svg {
|
||||
width: 0.875em;
|
||||
height: 0.875em;
|
||||
}
|
||||
`}
|
||||
${({ iconName }) =>
|
||||
iconName === IconName.Close
|
||||
? css`
|
||||
--button-textColor: var(--color-negative);
|
||||
--button-icon-size: 0.75em;
|
||||
`
|
||||
: iconName === IconName.Transfer &&
|
||||
css`
|
||||
--button-icon-size: 1.25em;
|
||||
--button-textColor: var(--color-text-2);
|
||||
--button-backgroundColor: var(--color-accent);
|
||||
`}
|
||||
`;
|
||||
|
||||
Styled.EqualGrid = styled.div`
|
||||
@@ -306,3 +361,14 @@ Styled.TablePanel = styled(Panel)`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.InlineRow = styled.div`
|
||||
${layoutMixins.inlineRow}
|
||||
padding: 1rem;
|
||||
gap: 0.5rem;
|
||||
`;
|
||||
|
||||
Styled.PanelButton = styled(Panel)`
|
||||
--panel-paddingY: 0
|
||||
--panel-paddingX:0;
|
||||
`;
|
||||
|
||||
@@ -304,10 +304,6 @@ Styled.FeeTable = styled(Table)`
|
||||
|
||||
@media ${breakpoints.notTablet} {
|
||||
--tableHeader-backgroundColor: var(--color-layer-1);
|
||||
|
||||
thead tr {
|
||||
--border-width: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,78 +1,97 @@
|
||||
import { useEffect } from 'react';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import styled, { type AnyStyledComponent } from 'styled-components';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { OnboardingState } from '@/constants/account';
|
||||
import { ButtonAction } from '@/constants/buttons';
|
||||
import { DialogTypes } from '@/constants/dialogs';
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
import { HistoryRoute, PortfolioRoute } from '@/constants/routes';
|
||||
|
||||
import { useBreakpoints, useDocumentTitle, useStringGetter } from '@/hooks';
|
||||
import { useAccountBalance, useBreakpoints, useDocumentTitle, useStringGetter } from '@/hooks';
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable';
|
||||
import { FundingPaymentsTable } from '@/views/tables/FundingPaymentsTable';
|
||||
import { TransferHistoryTable } from '@/views/tables/TransferHistoryTable';
|
||||
import { Button } from '@/components/Button';
|
||||
import { Icon, IconName } from '@/components/Icon';
|
||||
import { NavigationMenu } from '@/components/NavigationMenu';
|
||||
import { WithSidebar } from '@/components/WithSidebar';
|
||||
|
||||
import { PortfolioNavMobile } from './PortfolioNavMobile';
|
||||
import { Overview } from './Overview';
|
||||
import { Positions } from './Positions';
|
||||
import { Orders } from './Orders';
|
||||
import { Fees } from './Fees';
|
||||
import { History } from './History';
|
||||
import { getOnboardingState, getSubaccount } from '@/state/accountSelectors';
|
||||
import { openDialog } from '@/state/dialogs';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
import { PortfolioNavMobile } from './PortfolioNavMobile';
|
||||
import { LoadingSpace } from '@/components/Loading/LoadingSpinner';
|
||||
|
||||
const Overview = lazy(() => import('./Overview').then((module) => ({ default: module.Overview })));
|
||||
const Positions = lazy(() =>
|
||||
import('./Positions').then((module) => ({ default: module.Positions }))
|
||||
);
|
||||
const Orders = lazy(() => import('./Orders').then((module) => ({ default: module.Orders })));
|
||||
const Fees = lazy(() => import('./Fees').then((module) => ({ default: module.Fees })));
|
||||
const History = lazy(() => import('./History').then((module) => ({ default: module.History })));
|
||||
|
||||
export default () => {
|
||||
const dispatch = useDispatch();
|
||||
const stringGetter = useStringGetter();
|
||||
const { isTablet, isNotTablet } = useBreakpoints();
|
||||
|
||||
const onboardingState = useSelector(getOnboardingState);
|
||||
const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {};
|
||||
const { nativeTokenBalance } = useAccountBalance();
|
||||
|
||||
const usdcBalance = freeCollateral?.current || 0;
|
||||
|
||||
useDocumentTitle(stringGetter({ key: STRING_KEYS.PORTFOLIO }));
|
||||
|
||||
const routesComponent = (
|
||||
<Routes>
|
||||
<Route path={PortfolioRoute.Overview} element={<Overview />} />
|
||||
<Route path={PortfolioRoute.Positions} element={<Positions />} />
|
||||
<Route path={PortfolioRoute.Orders} element={<Orders />} />
|
||||
<Route path={PortfolioRoute.Fees} element={<Fees />} />
|
||||
<Route path={PortfolioRoute.History} element={<History />}>
|
||||
<Route index path="*" element={<Navigate to={HistoryRoute.Trades} />} />
|
||||
<Route
|
||||
path={HistoryRoute.Trades}
|
||||
element={
|
||||
<FillsTable
|
||||
columnKeys={
|
||||
isTablet
|
||||
? [
|
||||
FillsTableColumnKey.Time,
|
||||
FillsTableColumnKey.TypeAmount,
|
||||
FillsTableColumnKey.PriceFee,
|
||||
]
|
||||
: [
|
||||
FillsTableColumnKey.Time,
|
||||
FillsTableColumnKey.Market,
|
||||
FillsTableColumnKey.Side,
|
||||
FillsTableColumnKey.AmountPrice,
|
||||
FillsTableColumnKey.TotalFee,
|
||||
FillsTableColumnKey.Type,
|
||||
FillsTableColumnKey.Liquidity,
|
||||
]
|
||||
}
|
||||
withOuterBorder={isNotTablet}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={HistoryRoute.Transfers}
|
||||
element={<TransferHistoryTable withOuterBorder={isNotTablet} />}
|
||||
/>
|
||||
<Route
|
||||
path={HistoryRoute.Payments}
|
||||
element={<FundingPaymentsTable withOuterBorder={isNotTablet} />}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to={PortfolioRoute.Overview} replace />} />
|
||||
</Routes>
|
||||
<Suspense fallback={<LoadingSpace id="portfolio" />}>
|
||||
<Routes>
|
||||
<Route path={PortfolioRoute.Overview} element={<Overview />} />
|
||||
<Route path={PortfolioRoute.Positions} element={<Positions />} />
|
||||
<Route path={PortfolioRoute.Orders} element={<Orders />} />
|
||||
<Route path={PortfolioRoute.Fees} element={<Fees />} />
|
||||
<Route path={PortfolioRoute.History} element={<History />}>
|
||||
<Route index path="*" element={<Navigate to={HistoryRoute.Trades} />} />
|
||||
<Route
|
||||
path={HistoryRoute.Trades}
|
||||
element={
|
||||
<FillsTable
|
||||
columnKeys={
|
||||
isTablet
|
||||
? [
|
||||
FillsTableColumnKey.Time,
|
||||
FillsTableColumnKey.TypeAmount,
|
||||
FillsTableColumnKey.PriceFee,
|
||||
]
|
||||
: [
|
||||
FillsTableColumnKey.Time,
|
||||
FillsTableColumnKey.Market,
|
||||
FillsTableColumnKey.Side,
|
||||
FillsTableColumnKey.AmountPrice,
|
||||
FillsTableColumnKey.TotalFee,
|
||||
FillsTableColumnKey.Type,
|
||||
FillsTableColumnKey.Liquidity,
|
||||
]
|
||||
}
|
||||
withOuterBorder={isNotTablet}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={HistoryRoute.Transfers}
|
||||
element={<TransferHistoryTable withOuterBorder={isNotTablet} />}
|
||||
/>
|
||||
<Route
|
||||
path={HistoryRoute.Payments}
|
||||
element={<FundingPaymentsTable withOuterBorder={isNotTablet} />}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to={PortfolioRoute.Overview} replace />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
return isTablet ? (
|
||||
@@ -84,46 +103,74 @@ export default () => {
|
||||
<WithSidebar
|
||||
sidebar={
|
||||
isTablet ? null : (
|
||||
<Styled.NavigationMenu
|
||||
items={[
|
||||
{
|
||||
group: 'views',
|
||||
groupLabel: stringGetter({ key: STRING_KEYS.VIEWS }),
|
||||
items: [
|
||||
{
|
||||
value: PortfolioRoute.Overview,
|
||||
slotBefore: <Styled.Icon iconName={IconName.Overview} />,
|
||||
label: stringGetter({ key: STRING_KEYS.OVERVIEW }),
|
||||
href: PortfolioRoute.Overview,
|
||||
},
|
||||
{
|
||||
value: PortfolioRoute.Positions,
|
||||
slotBefore: <Styled.Icon iconName={IconName.Cube} />,
|
||||
label: stringGetter({ key: STRING_KEYS.POSITIONS }),
|
||||
href: PortfolioRoute.Positions,
|
||||
},
|
||||
{
|
||||
value: PortfolioRoute.Orders,
|
||||
slotBefore: <Styled.Icon iconName={IconName.OrderPending} />,
|
||||
label: stringGetter({ key: STRING_KEYS.ORDERS }),
|
||||
href: PortfolioRoute.Orders,
|
||||
},
|
||||
{
|
||||
value: PortfolioRoute.Fees,
|
||||
slotBefore: <Styled.Icon iconName={IconName.Calculator} />,
|
||||
label: stringGetter({ key: STRING_KEYS.FEES }),
|
||||
href: PortfolioRoute.Fees,
|
||||
},
|
||||
{
|
||||
value: PortfolioRoute.History,
|
||||
slotBefore: <Styled.Icon iconName={IconName.History} />,
|
||||
label: stringGetter({ key: STRING_KEYS.HISTORY }),
|
||||
href: PortfolioRoute.History,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Styled.SideBar>
|
||||
<Styled.NavigationMenu
|
||||
items={[
|
||||
{
|
||||
group: 'views',
|
||||
groupLabel: stringGetter({ key: STRING_KEYS.VIEWS }),
|
||||
items: [
|
||||
{
|
||||
value: PortfolioRoute.Overview,
|
||||
slotBefore: <Styled.Icon iconName={IconName.Overview} />,
|
||||
label: stringGetter({ key: STRING_KEYS.OVERVIEW }),
|
||||
href: PortfolioRoute.Overview,
|
||||
},
|
||||
{
|
||||
value: PortfolioRoute.Positions,
|
||||
slotBefore: <Styled.Icon iconName={IconName.Cube} />,
|
||||
label: stringGetter({ key: STRING_KEYS.POSITIONS }),
|
||||
href: PortfolioRoute.Positions,
|
||||
},
|
||||
{
|
||||
value: PortfolioRoute.Orders,
|
||||
slotBefore: <Styled.Icon iconName={IconName.OrderPending} />,
|
||||
label: stringGetter({ key: STRING_KEYS.ORDERS }),
|
||||
href: PortfolioRoute.Orders,
|
||||
},
|
||||
{
|
||||
value: PortfolioRoute.Fees,
|
||||
slotBefore: <Styled.Icon iconName={IconName.Calculator} />,
|
||||
label: stringGetter({ key: STRING_KEYS.FEES }),
|
||||
href: PortfolioRoute.Fees,
|
||||
},
|
||||
{
|
||||
value: PortfolioRoute.History,
|
||||
slotBefore: <Styled.Icon iconName={IconName.History} />,
|
||||
label: stringGetter({ key: STRING_KEYS.HISTORY }),
|
||||
href: PortfolioRoute.History,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{onboardingState === OnboardingState.AccountConnected && (
|
||||
<Styled.Footer>
|
||||
<Button
|
||||
action={ButtonAction.Primary}
|
||||
onClick={() => dispatch(openDialog({ type: DialogTypes.Deposit }))}
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.DEPOSIT })}
|
||||
</Button>
|
||||
{usdcBalance > 0 && (
|
||||
<Button
|
||||
action={ButtonAction.Base}
|
||||
onClick={() => dispatch(openDialog({ type: DialogTypes.Withdraw }))}
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.WITHDRAW })}
|
||||
</Button>
|
||||
)}
|
||||
{(usdcBalance > 0 || nativeTokenBalance.gt(0)) && (
|
||||
<Button
|
||||
action={ButtonAction.Base}
|
||||
onClick={() => dispatch(openDialog({ type: DialogTypes.Transfer }))}
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.TRANSFER })}
|
||||
</Button>
|
||||
)}
|
||||
</Styled.Footer>
|
||||
)}
|
||||
</Styled.SideBar>
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -139,6 +186,26 @@ Styled.PortfolioMobile = styled.div`
|
||||
${layoutMixins.expandingColumnWithHeader}
|
||||
`;
|
||||
|
||||
Styled.SideBar = styled.div`
|
||||
${layoutMixins.flexColumn}
|
||||
justify-content: space-between;
|
||||
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
Styled.Footer = styled.div`
|
||||
${layoutMixins.row}
|
||||
flex-wrap: wrap;
|
||||
|
||||
padding: 1rem;
|
||||
|
||||
gap: 0.5rem;
|
||||
|
||||
> button {
|
||||
flex-grow: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.NavigationMenu = styled(NavigationMenu)`
|
||||
padding: 0.5rem;
|
||||
padding-top: 0;
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ElementType } from 'react';
|
||||
import styled, { AnyStyledComponent } from 'styled-components';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import breakpoints from '@/styles/breakpoints';
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
import { useAccountBalance, useAccounts, useTokenConfigs, useStringGetter } from '@/hooks';
|
||||
|
||||
@@ -39,29 +40,20 @@ export const DYDXBalancePanel = () => {
|
||||
<AssetIcon symbol={chainTokenLabel} />
|
||||
{chainTokenLabel}
|
||||
</Styled.Title>
|
||||
<Styled.ReceiveAndTransferButtons>
|
||||
<Styled.ActionButtons>
|
||||
{!canAccountTrade ? (
|
||||
<OnboardingTriggerButton size={ButtonSize.Small} />
|
||||
) : (
|
||||
<>
|
||||
<Styled.ReceiveButton
|
||||
slotLeft={<Icon iconName={IconName.Qr} />}
|
||||
size={ButtonSize.Small}
|
||||
onClick={() => dispatch(openDialog({ type: DialogTypes.Receive }))}
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.RECEIVE })}
|
||||
</Styled.ReceiveButton>
|
||||
<Button
|
||||
slotLeft={<Icon iconName={IconName.Send} />}
|
||||
size={ButtonSize.Small}
|
||||
action={ButtonAction.Primary}
|
||||
onClick={() => dispatch(openDialog({ type: DialogTypes.Transfer }))}
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.TRANSFER })}
|
||||
</Button>
|
||||
</>
|
||||
<Button
|
||||
slotLeft={<Icon iconName={IconName.Send} />}
|
||||
size={ButtonSize.Small}
|
||||
action={ButtonAction.Primary}
|
||||
onClick={() => dispatch(openDialog({ type: DialogTypes.Transfer }))}
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.TRANSFER })}
|
||||
</Button>
|
||||
)}
|
||||
</Styled.ReceiveAndTransferButtons>
|
||||
</Styled.ActionButtons>
|
||||
</Styled.Header>
|
||||
}
|
||||
>
|
||||
@@ -125,12 +117,17 @@ const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.Panel = styled(Panel)`
|
||||
--panel-paddingX: 1.5rem;
|
||||
|
||||
@media ${breakpoints.tablet} {
|
||||
--panel-paddingY: 1.5rem;
|
||||
--panel-content-paddingY: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.Header = styled.div`
|
||||
${layoutMixins.spacedRow}
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.5rem 0;
|
||||
padding: var(--panel-paddingY) var(--panel-paddingX) 0;
|
||||
`;
|
||||
|
||||
Styled.Title = styled.h3`
|
||||
@@ -143,7 +140,7 @@ Styled.Title = styled.h3`
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.ReceiveAndTransferButtons = styled(Toolbar)`
|
||||
Styled.ActionButtons = styled(Toolbar)`
|
||||
${layoutMixins.inlineRow}
|
||||
--stickyArea-topHeight: max-content;
|
||||
gap: 0.5rem;
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import styled, { AnyStyledComponent } from 'styled-components';
|
||||
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
import { ButtonAction } from '@/constants/buttons';
|
||||
import { DialogTypes } from '@/constants/dialogs';
|
||||
import { QueryKeys } from '@/constants/queries';
|
||||
|
||||
import breakpoints from '@/styles/breakpoints';
|
||||
import { useAccounts, useBreakpoints, useStringGetter } from '@/hooks';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
import { Panel } from '@/components/Panel';
|
||||
import { Button } from '@/components/Button';
|
||||
|
||||
import { Output, OutputType } from '@/components/Output';
|
||||
import { Icon, IconName } from '@/components/Icon';
|
||||
import { Tag, TagSize } from '@/components/Tag';
|
||||
|
||||
import { markLaunchIncentivesSeen } from '@/state/configs';
|
||||
import { openDialog } from '@/state/dialogs';
|
||||
|
||||
import { log } from '@/lib/telemetry';
|
||||
|
||||
const SEASON_NUMBER = 2;
|
||||
|
||||
export const LaunchIncentivesPanel = () => {
|
||||
const { isNotTablet } = useBreakpoints();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(markLaunchIncentivesSeen());
|
||||
}, []);
|
||||
|
||||
return isNotTablet ? (
|
||||
<Styled.Panel slotHeader={<LaunchIncentivesTitle />} slotRight={<EstimatedRewards />}>
|
||||
<LaunchIncentivesContent />
|
||||
</Styled.Panel>
|
||||
) : (
|
||||
<Styled.Panel>
|
||||
<Styled.Column>
|
||||
<EstimatedRewards />
|
||||
<LaunchIncentivesTitle />
|
||||
<LaunchIncentivesContent />
|
||||
</Styled.Column>
|
||||
</Styled.Panel>
|
||||
);
|
||||
};
|
||||
|
||||
const LaunchIncentivesTitle = () => {
|
||||
const stringGetter = useStringGetter();
|
||||
return (
|
||||
<Styled.Title>
|
||||
{stringGetter({
|
||||
key: STRING_KEYS.LAUNCH_INCENTIVES_TITLE,
|
||||
params: {
|
||||
FOR_V4: <Styled.ForV4>{stringGetter({ key: STRING_KEYS.FOR_V4 })}</Styled.ForV4>,
|
||||
},
|
||||
})}
|
||||
<Styled.NewTag size={TagSize.Medium}>{stringGetter({ key: STRING_KEYS.NEW })}</Styled.NewTag>
|
||||
</Styled.Title>
|
||||
);
|
||||
};
|
||||
|
||||
const EstimatedRewards = () => {
|
||||
const stringGetter = useStringGetter();
|
||||
const { dydxAddress } = useAccounts();
|
||||
|
||||
const { data, isLoading, status, error } = useQuery({
|
||||
enabled: !!dydxAddress,
|
||||
queryKey: [QueryKeys.LAUNCH_INCENTIVES, dydxAddress, SEASON_NUMBER],
|
||||
queryFn: async () => {
|
||||
if (!dydxAddress) return undefined;
|
||||
const resp = await fetch(
|
||||
`https://cloud.chaoslabs.co/query/api/dydx/points/${dydxAddress}?n=${SEASON_NUMBER}`
|
||||
);
|
||||
return (await resp.json())?.incentivePoints;
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'error') {
|
||||
log('LaunchIncentives/fetchPoints', error);
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
return (
|
||||
<Styled.EstimatedRewardsCard>
|
||||
<Styled.EstimatedRewardsCardContent>
|
||||
<div>
|
||||
<span>{stringGetter({ key: STRING_KEYS.ESTIMATED_REWARDS })}</span>
|
||||
<Styled.Season>
|
||||
{stringGetter({
|
||||
key: STRING_KEYS.LAUNCH_INCENTIVES_SEASON_NUM,
|
||||
params: { SEASON_NUMBER },
|
||||
})}
|
||||
</Styled.Season>
|
||||
</div>
|
||||
|
||||
<Styled.Points>
|
||||
<Output type={OutputType.Number} value={data} isLoading={isLoading} fractionDigits={2} />
|
||||
{data !== undefined && stringGetter({ key: STRING_KEYS.POINTS })}
|
||||
</Styled.Points>
|
||||
</Styled.EstimatedRewardsCardContent>
|
||||
|
||||
<Styled.Image src="/rewards-stars.svg" />
|
||||
</Styled.EstimatedRewardsCard>
|
||||
);
|
||||
};
|
||||
|
||||
const LaunchIncentivesContent = () => {
|
||||
const stringGetter = useStringGetter();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return (
|
||||
<Styled.Column>
|
||||
<Styled.Description>
|
||||
{stringGetter({ key: STRING_KEYS.LAUNCH_INCENTIVES_DESCRIPTION })}{' '}
|
||||
</Styled.Description>
|
||||
<Styled.ChaosLabsLogo src="/logos/chaos-labs.svg" />
|
||||
<Styled.ButtonRow>
|
||||
<Styled.AboutButton
|
||||
action={ButtonAction.Base}
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
openDialog({
|
||||
type: DialogTypes.ExternalLink,
|
||||
dialogProps: { link: 'https://dydx.exchange/blog/v4-full-trading' },
|
||||
})
|
||||
);
|
||||
}}
|
||||
slotRight={<Styled.LinkOutIcon iconName={IconName.LinkOut} />}
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.ABOUT })}
|
||||
</Styled.AboutButton>
|
||||
<Styled.Button
|
||||
action={ButtonAction.Primary}
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
openDialog({
|
||||
type: DialogTypes.ExternalLink,
|
||||
dialogProps: { link: 'https://community.chaoslabs.xyz/dydx-v4/risk/leaderboard' },
|
||||
})
|
||||
);
|
||||
}}
|
||||
slotRight={<Styled.LinkOutIcon iconName={IconName.LinkOut} />}
|
||||
slotLeft={<Icon iconName={IconName.Leaderboard} />}
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.LEADERBOARD })}
|
||||
</Styled.Button>
|
||||
</Styled.ButtonRow>
|
||||
</Styled.Column>
|
||||
);
|
||||
};
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.Panel = styled(Panel)`
|
||||
--panel-paddingY: 1rem;
|
||||
--panel-paddingX: 1.5rem;
|
||||
|
||||
background-color: var(--color-layer-4);
|
||||
width: 100%;
|
||||
|
||||
@media ${breakpoints.tablet} {
|
||||
--panel-paddingY: 1.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.ForV4 = styled.span`
|
||||
color: var(--color-text-0);
|
||||
`;
|
||||
|
||||
Styled.Title = styled.h3`
|
||||
${layoutMixins.inlineRow}
|
||||
font: var(--font-medium-book);
|
||||
color: var(--color-text-2);
|
||||
|
||||
@media ${breakpoints.notTablet} {
|
||||
padding: var(--panel-paddingY) var(--panel-paddingX) 0;
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.Description = styled.div`
|
||||
color: var(--color-text-0);
|
||||
--link-color: var(--color-text-1);
|
||||
|
||||
a {
|
||||
display: inline;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.25rem;
|
||||
|
||||
::before {
|
||||
content: ' ';
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.ButtonRow = styled.div`
|
||||
${layoutMixins.inlineRow}
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
a:last-child {
|
||||
--button-width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.Button = styled(Button)`
|
||||
--button-padding: 0 1rem;
|
||||
`;
|
||||
|
||||
Styled.LinkOutIcon = styled(Icon)`
|
||||
color: var(--color-text-1);
|
||||
`;
|
||||
|
||||
Styled.AboutButton = styled(Styled.Button)`
|
||||
--button-textColor: var(--color-text-2);
|
||||
--button-backgroundColor: var(--color-layer-6);
|
||||
--button-border: solid var(--border-width) var(--color-layer-7);
|
||||
`;
|
||||
|
||||
Styled.Column = styled.div`
|
||||
${layoutMixins.flexColumn}
|
||||
gap: 0.5rem;
|
||||
`;
|
||||
|
||||
Styled.EstimatedRewardsCard = styled.div`
|
||||
${layoutMixins.spacedRow}
|
||||
padding: 1rem 1.25rem;
|
||||
min-width: 21.25rem;
|
||||
height: calc(100% - calc(1.5rem * 2));
|
||||
margin: 1.5rem;
|
||||
|
||||
background-color: var(--color-layer-5);
|
||||
background-image: url('/dots-background.svg');
|
||||
background-size: cover;
|
||||
|
||||
border-radius: 0.75rem;
|
||||
border: solid var(--border-width) var(--color-layer-6);
|
||||
color: var(--color-text-1);
|
||||
|
||||
@media ${breakpoints.tablet} {
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.EstimatedRewardsCardContent = styled.div`
|
||||
${layoutMixins.flexColumn}
|
||||
gap: 1rem;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
|
||||
div {
|
||||
${layoutMixins.flexColumn}
|
||||
gap: 0.15rem;
|
||||
font: var(--font-medium-book);
|
||||
|
||||
:first-child {
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.BackgroundDots = styled.img`
|
||||
position: absolute;
|
||||
`;
|
||||
|
||||
Styled.Season = styled.span`
|
||||
font: var(--font-small-book);
|
||||
color: var(--color-text-1);
|
||||
`;
|
||||
|
||||
Styled.Points = styled.span`
|
||||
${layoutMixins.inlineRow}
|
||||
gap: 0.25rem;
|
||||
font: var(--font-large-book);
|
||||
color: var(--color-text-0);
|
||||
|
||||
output {
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.Image = styled.img`
|
||||
position: relative;
|
||||
float: right;
|
||||
|
||||
width: 5.25rem;
|
||||
height: auto;
|
||||
`;
|
||||
|
||||
Styled.ChaosLabsLogo = styled.img`
|
||||
height: 1.25rem;
|
||||
align-self: start;
|
||||
`;
|
||||
|
||||
Styled.NewTag = styled(Tag)`
|
||||
color: var(--color-accent);
|
||||
background-color: var(--color-accent-faded);
|
||||
`;
|
||||
@@ -7,6 +7,8 @@ import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons';
|
||||
|
||||
import { useAccountBalance, useBreakpoints, useStringGetter } from '@/hooks';
|
||||
|
||||
import { breakpoints } from '@/styles';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
import { Details } from '@/components/Details';
|
||||
@@ -134,13 +136,22 @@ const Styled: Record<string, AnyStyledComponent> = {};
|
||||
Styled.MigratePanel = styled(Panel)`
|
||||
--panel-paddingX: 1.5rem;
|
||||
width: 100%;
|
||||
|
||||
background-image: url('/dots-background.svg');
|
||||
background-position: right;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
@media ${breakpoints.tablet} {
|
||||
--panel-paddingY: 1.5rem;
|
||||
--panel-content-paddingY: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.Title = styled.h3`
|
||||
font: var(--font-medium-book);
|
||||
color: var(--color-text-2);
|
||||
|
||||
padding: 1rem 1.5rem 0;
|
||||
padding: var(--panel-paddingY) var(--panel-paddingX) 0;
|
||||
`;
|
||||
|
||||
Styled.MigrateAction = styled.div`
|
||||
@@ -164,6 +175,13 @@ Styled.Token = styled(Output)`
|
||||
Styled.Description = styled.div`
|
||||
color: var(--color-text-0);
|
||||
--link-color: var(--color-text-1);
|
||||
|
||||
a {
|
||||
display: inline;
|
||||
::before {
|
||||
content: ' ';
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.Column = styled.div`
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import styled, { AnyStyledComponent } from 'styled-components';
|
||||
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
import { useStringGetter } from '@/hooks';
|
||||
|
||||
import { Accordion } from '@/components/Accordion';
|
||||
import { Link } from '@/components/Link';
|
||||
import { Panel } from '@/components/Panel';
|
||||
|
||||
const REWARDS_LEARN_MORE_LINK = ''; // to be configured
|
||||
|
||||
export const RewardsHelpPanel = () => {
|
||||
const stringGetter = useStringGetter();
|
||||
|
||||
return (
|
||||
<Styled.HelpCard
|
||||
slotHeader={
|
||||
<Styled.Header>
|
||||
<h3>{stringGetter({ key: STRING_KEYS.HELP })}</h3>
|
||||
{REWARDS_LEARN_MORE_LINK && (
|
||||
<Link withIcon href={REWARDS_LEARN_MORE_LINK}>
|
||||
{stringGetter({ key: STRING_KEYS.LEARN_MORE })}
|
||||
</Link>
|
||||
)}
|
||||
</Styled.Header>
|
||||
}
|
||||
>
|
||||
<Accordion
|
||||
items={[
|
||||
{
|
||||
header: 'Who is eligible for trading rewards?',
|
||||
content: 'All traders are eligible for trading rewards.',
|
||||
},
|
||||
{
|
||||
header: 'How do trading rewards work?',
|
||||
content:
|
||||
'Immediately after each fill, trading rewards are sent directly to the trader’s dYdX Chain address, based on the amount of fees paid by the trader.',
|
||||
},
|
||||
{
|
||||
header: 'How do I claim my rewards?',
|
||||
content:
|
||||
'Each block, trading rewards are automatically sent directly to the trader’s dYdX Chain address.',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Styled.HelpCard>
|
||||
);
|
||||
};
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.HelpCard = styled(Panel)`
|
||||
--panel-content-paddingY: 0;
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
|
||||
text-align: start;
|
||||
`;
|
||||
|
||||
Styled.Header = styled.div`
|
||||
${layoutMixins.spacedRow}
|
||||
gap: 1ch;
|
||||
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-bottom: var(--border-width) solid var(--border-color);
|
||||
|
||||
font: var(--font-small-book);
|
||||
|
||||
h3 {
|
||||
font: var(--font-medium-book);
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.Link = styled(Link)`
|
||||
display: inline-flex;
|
||||
`;
|
||||
@@ -19,8 +19,9 @@ import { openDialog } from '@/state/dialogs';
|
||||
|
||||
import { DYDXBalancePanel } from './DYDXBalancePanel';
|
||||
import { MigratePanel } from './MigratePanel';
|
||||
import { LaunchIncentivesPanel } from './LaunchIncentivesPanel';
|
||||
|
||||
export const RewardsPage = () => {
|
||||
const RewardsPage = () => {
|
||||
const dispatch = useDispatch();
|
||||
const stringGetter = useStringGetter();
|
||||
const { governanceLearnMore, stakingLearnMore } = useURLConfigs();
|
||||
@@ -39,9 +40,20 @@ export const RewardsPage = () => {
|
||||
return (
|
||||
<Styled.Page>
|
||||
{import.meta.env.VITE_V3_TOKEN_ADDRESS && <MigratePanel />}
|
||||
<Styled.PanelRow>
|
||||
{isTablet && <DYDXBalancePanel />}
|
||||
|
||||
{isTablet ? (
|
||||
<>
|
||||
<LaunchIncentivesPanel />
|
||||
<DYDXBalancePanel />
|
||||
</>
|
||||
) : (
|
||||
<Styled.PanelRowIncentivesAndBalance>
|
||||
<LaunchIncentivesPanel />
|
||||
<DYDXBalancePanel />
|
||||
</Styled.PanelRowIncentivesAndBalance>
|
||||
)}
|
||||
|
||||
<Styled.PanelRow>
|
||||
<Styled.Panel
|
||||
slotHeader={<Styled.Title>{stringGetter({ key: STRING_KEYS.GOVERNANCE })}</Styled.Title>}
|
||||
slotRight={panelArrow}
|
||||
@@ -67,13 +79,13 @@ export const RewardsPage = () => {
|
||||
</Link>
|
||||
</Styled.Description>
|
||||
</Styled.Panel>
|
||||
|
||||
{isNotTablet && <DYDXBalancePanel />}
|
||||
</Styled.PanelRow>
|
||||
</Styled.Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default RewardsPage;
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.Page = styled.div`
|
||||
@@ -99,6 +111,11 @@ Styled.Page = styled.div`
|
||||
|
||||
Styled.Panel = styled(Panel)`
|
||||
--panel-paddingX: 1.5rem;
|
||||
|
||||
@media ${breakpoints.tablet} {
|
||||
--panel-paddingY: 1.5rem;
|
||||
--panel-content-paddingY: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.Title = styled.h3`
|
||||
@@ -110,20 +127,29 @@ Styled.Title = styled.h3`
|
||||
Styled.Description = styled.div`
|
||||
color: var(--color-text-0);
|
||||
--link-color: var(--color-text-1);
|
||||
|
||||
a {
|
||||
display: inline;
|
||||
::before {
|
||||
content: ' ';
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.PanelRow = styled.div`
|
||||
${layoutMixins.gridEqualColumns}
|
||||
gap: 1.5rem;
|
||||
|
||||
align-items: flex-start;
|
||||
|
||||
@media ${breakpoints.tablet} {
|
||||
grid-auto-flow: row;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.PanelRowIncentivesAndBalance = styled(Styled.PanelRow)`
|
||||
grid-template-columns: 2fr 1fr;
|
||||
`;
|
||||
|
||||
Styled.IconButton = styled(IconButton)`
|
||||
color: var(--color-text-0);
|
||||
--color-border: var(--color-layer-6);
|
||||
|
||||
@@ -25,7 +25,7 @@ import { useNetworks } from '@/views/menus/useNetworks';
|
||||
import { SettingsHeader } from './SettingsHeader';
|
||||
import { ComingSoonSpace } from '@/components/ComingSoon';
|
||||
|
||||
export const SettingsPage = () => {
|
||||
const SettingsPage = () => {
|
||||
const stringGetter = useStringGetter();
|
||||
const { pathname } = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
@@ -105,3 +105,5 @@ export const SettingsPage = () => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPage;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import styled, { type AnyStyledComponent } from 'styled-components';
|
||||
|
||||
import { AbacusOrderStatus } from '@/constants/abacus';
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
|
||||
import { useBreakpoints, useStringGetter } from '@/hooks';
|
||||
@@ -28,7 +27,6 @@ import {
|
||||
getCurrentMarketTradeInfoNumbers,
|
||||
getHasUnseenFillUpdates,
|
||||
getHasUnseenOrderUpdates,
|
||||
getLatestOrderStatus,
|
||||
getTradeInfoNumbers,
|
||||
} from '@/state/accountSelectors';
|
||||
|
||||
@@ -82,125 +80,141 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => {
|
||||
showCurrentMarket ? numOpenOrders : numTotalOpenOrders
|
||||
);
|
||||
|
||||
const tabItems = [
|
||||
{
|
||||
value: InfoSection.Position,
|
||||
label: stringGetter({
|
||||
key: showCurrentMarket ? STRING_KEYS.POSITION : STRING_KEYS.POSITIONS,
|
||||
}),
|
||||
const tabItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
value: InfoSection.Position,
|
||||
label: stringGetter({
|
||||
key: showCurrentMarket ? STRING_KEYS.POSITION : STRING_KEYS.POSITIONS,
|
||||
}),
|
||||
|
||||
tag: showCurrentMarket ? null : shortenNumberForDisplay(numTotalPositions),
|
||||
tag: showCurrentMarket ? null : shortenNumberForDisplay(numTotalPositions),
|
||||
|
||||
content: showCurrentMarket ? (
|
||||
<PositionInfo showNarrowVariation={isTablet} />
|
||||
) : (
|
||||
<PositionsTable
|
||||
columnKeys={
|
||||
isTablet
|
||||
? [
|
||||
PositionsTableColumnKey.Details,
|
||||
PositionsTableColumnKey.IndexEntry,
|
||||
PositionsTableColumnKey.PnL,
|
||||
]
|
||||
: [
|
||||
PositionsTableColumnKey.Market,
|
||||
PositionsTableColumnKey.Side,
|
||||
PositionsTableColumnKey.Size,
|
||||
PositionsTableColumnKey.Leverage,
|
||||
PositionsTableColumnKey.LiquidationAndOraclePrice,
|
||||
PositionsTableColumnKey.UnrealizedPnl,
|
||||
PositionsTableColumnKey.RealizedPnl,
|
||||
PositionsTableColumnKey.AverageOpenAndClose,
|
||||
]
|
||||
}
|
||||
onNavigate={() => setView(PanelView.CurrentMarket)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: InfoSection.Orders,
|
||||
label: stringGetter({ key: STRING_KEYS.ORDERS }),
|
||||
content: showCurrentMarket ? (
|
||||
<PositionInfo showNarrowVariation={isTablet} />
|
||||
) : (
|
||||
<PositionsTable
|
||||
columnKeys={
|
||||
isTablet
|
||||
? [
|
||||
PositionsTableColumnKey.Details,
|
||||
PositionsTableColumnKey.IndexEntry,
|
||||
PositionsTableColumnKey.PnL,
|
||||
]
|
||||
: [
|
||||
PositionsTableColumnKey.Market,
|
||||
PositionsTableColumnKey.Side,
|
||||
PositionsTableColumnKey.Size,
|
||||
PositionsTableColumnKey.Leverage,
|
||||
PositionsTableColumnKey.LiquidationAndOraclePrice,
|
||||
PositionsTableColumnKey.UnrealizedPnl,
|
||||
PositionsTableColumnKey.RealizedPnl,
|
||||
PositionsTableColumnKey.AverageOpenAndClose,
|
||||
]
|
||||
}
|
||||
onNavigate={() => setView(PanelView.CurrentMarket)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
asChild: true,
|
||||
value: InfoSection.Orders,
|
||||
label: stringGetter({ key: STRING_KEYS.ORDERS }),
|
||||
|
||||
slotRight: isWaitingForOrderToIndex ? (
|
||||
<Styled.LoadingSpinner />
|
||||
) : (
|
||||
ordersTagNumber && (
|
||||
<Tag type={TagType.Number} isHighlighted={hasUnseenOrderUpdates}>
|
||||
{ordersTagNumber}
|
||||
slotRight: isWaitingForOrderToIndex ? (
|
||||
<Styled.LoadingSpinner />
|
||||
) : (
|
||||
ordersTagNumber && (
|
||||
<Tag type={TagType.Number} isHighlighted={hasUnseenOrderUpdates}>
|
||||
{ordersTagNumber}
|
||||
</Tag>
|
||||
)
|
||||
),
|
||||
|
||||
content: (
|
||||
<OrdersTable
|
||||
currentMarket={showCurrentMarket ? currentMarketId : undefined}
|
||||
columnKeys={
|
||||
isTablet
|
||||
? [OrdersTableColumnKey.StatusFill, OrdersTableColumnKey.PriceType]
|
||||
: [
|
||||
!showCurrentMarket && OrdersTableColumnKey.Market,
|
||||
OrdersTableColumnKey.Status,
|
||||
OrdersTableColumnKey.Side,
|
||||
OrdersTableColumnKey.AmountFill,
|
||||
OrdersTableColumnKey.Price,
|
||||
OrdersTableColumnKey.Trigger,
|
||||
OrdersTableColumnKey.GoodTil,
|
||||
!isAccountViewOnly && OrdersTableColumnKey.Actions,
|
||||
].filter(isTruthy)
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
asChild: true,
|
||||
value: InfoSection.Fills,
|
||||
label: stringGetter({ key: STRING_KEYS.FILLS }),
|
||||
|
||||
slotRight: fillsTagNumber && (
|
||||
<Tag type={TagType.Number} isHighlighted={hasUnseenFillUpdates}>
|
||||
{fillsTagNumber}
|
||||
</Tag>
|
||||
)
|
||||
),
|
||||
),
|
||||
|
||||
content: (
|
||||
<OrdersTable
|
||||
currentMarket={showCurrentMarket ? currentMarketId : undefined}
|
||||
columnKeys={
|
||||
isTablet
|
||||
? [OrdersTableColumnKey.StatusFill, OrdersTableColumnKey.PriceType]
|
||||
: [
|
||||
!showCurrentMarket && OrdersTableColumnKey.Market,
|
||||
OrdersTableColumnKey.Status,
|
||||
OrdersTableColumnKey.Side,
|
||||
OrdersTableColumnKey.AmountFill,
|
||||
OrdersTableColumnKey.Price,
|
||||
OrdersTableColumnKey.Trigger,
|
||||
OrdersTableColumnKey.GoodTil,
|
||||
!isAccountViewOnly && OrdersTableColumnKey.Actions,
|
||||
].filter(isTruthy)
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: InfoSection.Fills,
|
||||
label: stringGetter({ key: STRING_KEYS.FILLS }),
|
||||
content: (
|
||||
<FillsTable
|
||||
currentMarket={showCurrentMarket ? currentMarketId : undefined}
|
||||
columnKeys={
|
||||
isTablet
|
||||
? [
|
||||
FillsTableColumnKey.Time,
|
||||
FillsTableColumnKey.TypeAmount,
|
||||
FillsTableColumnKey.PriceFee,
|
||||
]
|
||||
: [
|
||||
!showCurrentMarket && FillsTableColumnKey.Market,
|
||||
FillsTableColumnKey.Time,
|
||||
FillsTableColumnKey.Type,
|
||||
FillsTableColumnKey.Side,
|
||||
FillsTableColumnKey.AmountTag,
|
||||
FillsTableColumnKey.Price,
|
||||
FillsTableColumnKey.TotalFee,
|
||||
FillsTableColumnKey.Liquidity,
|
||||
].filter(isTruthy)
|
||||
}
|
||||
columnWidths={{
|
||||
[FillsTableColumnKey.TypeAmount]: '100%',
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
// TODO - TRCL-1693 - re-enable when funding payments are supported
|
||||
// {
|
||||
// value: InfoSection.Payments,
|
||||
// label: stringGetter({ key: STRING_KEYS.PAYMENTS }),
|
||||
|
||||
slotRight: fillsTagNumber && (
|
||||
<Tag type={TagType.Number} isHighlighted={hasUnseenFillUpdates}>
|
||||
{fillsTagNumber}
|
||||
</Tag>
|
||||
),
|
||||
|
||||
content: (
|
||||
<FillsTable
|
||||
currentMarket={showCurrentMarket ? currentMarketId : undefined}
|
||||
columnKeys={
|
||||
isTablet
|
||||
? [
|
||||
FillsTableColumnKey.Time,
|
||||
FillsTableColumnKey.TypeAmount,
|
||||
FillsTableColumnKey.PriceFee,
|
||||
]
|
||||
: [
|
||||
!showCurrentMarket && FillsTableColumnKey.Market,
|
||||
FillsTableColumnKey.Time,
|
||||
FillsTableColumnKey.Type,
|
||||
FillsTableColumnKey.Side,
|
||||
FillsTableColumnKey.AmountTag,
|
||||
FillsTableColumnKey.Price,
|
||||
FillsTableColumnKey.TotalFee,
|
||||
FillsTableColumnKey.Liquidity,
|
||||
].filter(isTruthy)
|
||||
}
|
||||
columnWidths={{
|
||||
[FillsTableColumnKey.TypeAmount]: '100%',
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
// TODO - TRCL-1693 - re-enable when funding payments are supported
|
||||
// {
|
||||
// value: InfoSection.Payments,
|
||||
// label: stringGetter({ key: STRING_KEYS.PAYMENTS }),
|
||||
|
||||
// tag: shortenNumberForDisplay(
|
||||
// showCurrentMarket ? numFundingPayments : numTotalFundingPayments
|
||||
// ),
|
||||
// content: (
|
||||
// <FundingPaymentsTable currentMarket={showCurrentMarket ? currentMarket?.id : undefined} />
|
||||
// ),
|
||||
// },
|
||||
];
|
||||
// tag: shortenNumberForDisplay(
|
||||
// showCurrentMarket ? numFundingPayments : numTotalFundingPayments
|
||||
// ),
|
||||
// content: (
|
||||
// <FundingPaymentsTable currentMarket={showCurrentMarket ? currentMarket?.id : undefined} />
|
||||
// ),
|
||||
// },
|
||||
],
|
||||
[
|
||||
stringGetter,
|
||||
currentMarketId,
|
||||
showCurrentMarket,
|
||||
isTablet,
|
||||
isWaitingForOrderToIndex,
|
||||
isAccountViewOnly,
|
||||
ordersTagNumber,
|
||||
fillsTagNumber,
|
||||
hasUnseenFillUpdates,
|
||||
hasUnseenOrderUpdates,
|
||||
]
|
||||
);
|
||||
|
||||
return isTablet ? (
|
||||
<MobileTabs defaultValue={InfoSection.Position} items={tabItems} withBorders={false} />
|
||||
|
||||