Compare commits

..
Author SHA1 Message Date
Bill He 849bda8e8e privy theme 2024-01-18 21:22:44 -08:00
Bill He 57edb711d5 PWA 2024-01-18 19:55:43 -08:00
Bill He 8235c64c93 Noble deposit 2024-01-18 19:10:16 -08:00
Bill He 750fb077f5 remove debugs 2024-01-18 12:36:10 -08:00
Bill He d781cb3199 conditions 2024-01-18 12:28:57 -08:00
Bill He f6bf635b0d debug 2024-01-18 12:21:47 -08:00
Bill He 56187f7a84 ignore ready 2024-01-18 12:18:44 -08:00
Bill He 93c76f1c3f Privy integration [WIP] 2024-01-18 11:58:21 -08:00
aleka 521b6d6135 test flag for sign in option (#236) 2024-01-17 16:22:17 -08:00
aleka 94772bf4dc bump abacus to remove base position notional (#235) 2024-01-17 15:52:02 -08:00
Adam Fraser 149f21e7e0 Add trailing blocks to NetworkStatus analytics event (#230) 2024-01-17 10:34:04 -05:00
aleka 0a3ef3a7ab bump abacus (#233) 2024-01-17 10:33:29 -05:00
Bill fef9935883 Support notification for trading rewards (#224)
* Support notification for trading rewards

* bump abacus

* Stack notifications based on notification type

* address comments
2024-01-16 10:19:48 -08:00
Bill c7a5b706e1 Fix toAmount/toAmountMin on withdraw (#232) 2024-01-15 10:31:23 -08:00
moo-onthelawn f304bb069a TRCL-1979 Show open positions / orders in portfolio sidebar (#228)
* add tags for open orders + positions

* clean up, use new num selector function

* update logic for open orders
2024-01-12 14:44:09 -05:00
Bill ad6aa1af49 Bump abacus, fix onboarding toAmount (#229) 2024-01-11 11:52:02 -08:00
Jared Vu 55b2c3fb4a Update Funding Chart Tooltip (#225)
* 🚧 depth chart

*  Fixed types and shortened FundingRateTooltip

* 💄 update depth chart color scale to use css var

* 💄 use layer-6 instead of text-1

* 🌐 Add localization, fix nits
2024-01-11 10:31:53 -08:00
moo-onthelawn 83bcde00eb TRCL-1623 Update Positions icon in nav sidebar (#226)
* update icon

* remove unused cube icon
2024-01-11 11:02:23 -05:00
Jared Vu e8024b9337 Add Sepolia (#222) 2024-01-09 16:35:19 -08:00
Jared Vu 8280e512bc Remove BasePositionNotional DetailItem (#223) 2024-01-09 14:01:38 -08:00
aleka b2234cb969 reorganize rewards vs. profile, update panel and accordion styling (#221)
* reorganize rewards vs. profile, update panel and accordion styling

* feedback
2024-01-09 16:57:02 -05:00
Bill 4bcb1f64ac Add Kava support for onboarding, and add token symbol to token select (#220)
* Add Kava support for onboarding, and add token symbol to token select

* bump abacus
2024-01-09 11:51:31 -08:00
58 changed files with 4101 additions and 686 deletions
+1
View File
@@ -0,0 +1 @@
if('serviceWorker' in navigator) navigator.serviceWorker.register('/dev-sw.js?dev-sw', { scope: '/', type: 'classic' })
+5 -2
View File
@@ -39,11 +39,13 @@
"@cosmjs/proto-signing": "^0.31.0", "@cosmjs/proto-signing": "^0.31.0",
"@cosmjs/stargate": "^0.31.0", "@cosmjs/stargate": "^0.31.0",
"@cosmjs/tendermint-rpc": "^0.31.0", "@cosmjs/tendermint-rpc": "^0.31.0",
"@dydxprotocol/v4-abacus": "^1.1.33", "@dydxprotocol/v4-abacus": "^1.2.5",
"@dydxprotocol/v4-client-js": "^1.0.11", "@dydxprotocol/v4-client-js": "^1.0.11",
"@dydxprotocol/v4-localization": "^1.1.6", "@dydxprotocol/v4-localization": "^1.1.11",
"@ethersproject/providers": "^5.7.2", "@ethersproject/providers": "^5.7.2",
"@js-joda/core": "^5.5.3", "@js-joda/core": "^5.5.3",
"@privy-io/react-auth": "^1.53.1",
"@privy-io/wagmi-connector": "^0.1.12",
"@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-collapsible": "^1.0.3",
@@ -151,6 +153,7 @@
"url-polyfill": "^1.1.12", "url-polyfill": "^1.1.12",
"util": "^0.12.5", "util": "^0.12.5",
"vite": "^4.3.9", "vite": "^4.3.9",
"vite-plugin-pwa": "^0.17.4",
"vite-plugin-svgr": "^3.2.0", "vite-plugin-svgr": "^3.2.0",
"vitest": "^0.32.2", "vitest": "^0.32.2",
"w3name": "^1.0.8", "w3name": "^1.0.8",
+2712 -50
View File
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -39,7 +39,7 @@
"environments": { "environments": {
"dydxprotocol-dev": { "dydxprotocol-dev": {
"name": "v4 Dev", "name": "v4 Dev",
"ethereumChainId": "5", "ethereumChainId": "11155111",
"dydxChainId": "dydxprotocol-testnet", "dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "/dydx-chain.png", "chainLogo": "/dydx-chain.png",
@@ -113,7 +113,7 @@
}, },
"dydxprotocol-dev-2": { "dydxprotocol-dev-2": {
"name": "v4 Dev 2", "name": "v4 Dev 2",
"ethereumChainId": "5", "ethereumChainId": "11155111",
"dydxChainId": "dydxprotocol-testnet", "dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "/dydx-chain.png", "chainLogo": "/dydx-chain.png",
@@ -186,7 +186,7 @@
}, },
"dydxprotocol-dev-4": { "dydxprotocol-dev-4": {
"name": "v4 Dev 4", "name": "v4 Dev 4",
"ethereumChainId": "5", "ethereumChainId": "11155111",
"dydxChainId": "dydxprotocol-testnet", "dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "/dydx-chain.png", "chainLogo": "/dydx-chain.png",
@@ -259,7 +259,7 @@
}, },
"dydxprotocol-dev-5": { "dydxprotocol-dev-5": {
"name": "v4 Dev 5", "name": "v4 Dev 5",
"ethereumChainId": "5", "ethereumChainId": "11155111",
"dydxChainId": "dydxprotocol-testnet", "dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "/dydx-chain.png", "chainLogo": "/dydx-chain.png",
@@ -332,7 +332,7 @@
}, },
"dydxprotocol-staging": { "dydxprotocol-staging": {
"name": "v4 Staging", "name": "v4 Staging",
"ethereumChainId": "5", "ethereumChainId": "11155111",
"dydxChainId": "dydxprotocol-testnet", "dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "/dydx-chain.png", "chainLogo": "/dydx-chain.png",
@@ -407,7 +407,7 @@
}, },
"dydxprotocol-staging-forced-update": { "dydxprotocol-staging-forced-update": {
"name": "v4 Staging Forced Update", "name": "v4 Staging Forced Update",
"ethereumChainId": "5", "ethereumChainId": "11155111",
"dydxChainId": "dydxprotocol-testnet", "dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "/dydx-chain.png", "chainLogo": "/dydx-chain.png",
@@ -480,7 +480,7 @@
}, },
"dydxprotocol-staging-west": { "dydxprotocol-staging-west": {
"name": "v4 Staging West", "name": "v4 Staging West",
"ethereumChainId": "5", "ethereumChainId": "11155111",
"dydxChainId": "dydxprotocol-testnet", "dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "/dydx-chain.png", "chainLogo": "/dydx-chain.png",
@@ -555,7 +555,7 @@
}, },
"dydxprotocol-testnet": { "dydxprotocol-testnet": {
"name": "v4 Public Testnet", "name": "v4 Public Testnet",
"ethereumChainId": "5", "ethereumChainId": "11155111",
"dydxChainId": "dydx-testnet-4", "dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "/dydx-chain.png", "chainLogo": "/dydx-chain.png",
@@ -634,7 +634,7 @@
}, },
"dydxprotocol-testnet-dydx": { "dydxprotocol-testnet-dydx": {
"name": "v4 Public Testnet/dYdX", "name": "v4 Public Testnet/dYdX",
"ethereumChainId": "5", "ethereumChainId": "11155111",
"dydxChainId": "dydx-testnet-4", "dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "/dydx-chain.png", "chainLogo": "/dydx-chain.png",
@@ -710,7 +710,7 @@
}, },
"dydxprotocol-testnet-nodefleet": { "dydxprotocol-testnet-nodefleet": {
"name": "v4 Public Testnet/nodefleet", "name": "v4 Public Testnet/nodefleet",
"ethereumChainId": "5", "ethereumChainId": "11155111",
"dydxChainId": "dydx-testnet-4", "dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "/dydx-chain.png", "chainLogo": "/dydx-chain.png",
@@ -786,7 +786,7 @@
}, },
"dydxprotocol-testnet-kingnodes": { "dydxprotocol-testnet-kingnodes": {
"name": "v4 Public Testnet/KingNodes", "name": "v4 Public Testnet/KingNodes",
"ethereumChainId": "5", "ethereumChainId": "11155111",
"dydxChainId": "dydx-testnet-4", "dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "/dydx-chain.png", "chainLogo": "/dydx-chain.png",
@@ -862,7 +862,7 @@
}, },
"dydxprotocol-testnet-liquify": { "dydxprotocol-testnet-liquify": {
"name": "v4 Public Testnet/Liquify", "name": "v4 Public Testnet/Liquify",
"ethereumChainId": "5", "ethereumChainId": "11155111",
"dydxChainId": "dydx-testnet-4", "dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "/dydx-chain.png", "chainLogo": "/dydx-chain.png",
@@ -938,7 +938,7 @@
}, },
"dydxprotocol-testnet-polkachu": { "dydxprotocol-testnet-polkachu": {
"name": "v4 Public Testnet/Polkahcu", "name": "v4 Public Testnet/Polkahcu",
"ethereumChainId": "5", "ethereumChainId": "11155111",
"dydxChainId": "dydx-testnet-4", "dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "/dydx-chain.png", "chainLogo": "/dydx-chain.png",
@@ -1005,7 +1005,7 @@
}, },
"dydxprotocol-testnet-bware": { "dydxprotocol-testnet-bware": {
"name": "v4 Public Testnet/BWare", "name": "v4 Public Testnet/BWare",
"ethereumChainId": "5", "ethereumChainId": "11155111",
"dydxChainId": "dydx-testnet-4", "dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain", "chainName": "dYdX Chain",
"chainLogo": "/dydx-chain.png", "chainLogo": "/dydx-chain.png",
+230
View File
@@ -0,0 +1,230 @@
<svg width="254" height="77" viewBox="0 0 254 77" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Group 1258">
<g id="Ellipse 9" opacity="0.4" filter="url(#filter0_f_20300_14972)">
<ellipse cx="86.5425" cy="-3.91219" rx="116.542" ry="20.0878" fill="#7774FF"/>
</g>
<g id="Ellipse 10" opacity="0.3" filter="url(#filter1_f_20300_14972)">
<circle cx="74.5734" cy="4.63007" r="1.00507" fill="#9A9AFF"/>
</g>
<g id="Ellipse 12" opacity="0.3" filter="url(#filter2_f_20300_14972)">
<circle cx="82.1105" cy="7.64179" r="1.00507" fill="#9A9AFF"/>
</g>
<g id="Ellipse 22" opacity="0.3" filter="url(#filter3_f_20300_14972)">
<circle cx="116.785" cy="4.63007" r="1.00507" fill="#9A9AFF"/>
</g>
<g id="Ellipse 11" opacity="0.3" filter="url(#filter4_f_20300_14972)">
<circle cx="64.774" cy="8.90488" r="0.502536" fill="#9A9AFF"/>
</g>
<g id="Ellipse 18" opacity="0.3" filter="url(#filter5_f_20300_14972)">
<circle cx="96.4322" cy="3.87363" r="0.502536" fill="#9A9AFF"/>
</g>
<g id="Ellipse 20" opacity="0.3" filter="url(#filter6_f_20300_14972)">
<circle cx="76.0807" cy="14.9361" r="0.502536" fill="#9A9AFF"/>
</g>
<g id="Ellipse 21" opacity="0.3" filter="url(#filter7_f_20300_14972)">
<circle cx="104.725" cy="4.87754" r="0.502536" fill="#9A9AFF"/>
</g>
<g id="Ellipse 23" opacity="0.3" filter="url(#filter8_f_20300_14972)">
<circle cx="114.021" cy="7.13925" r="0.502536" fill="#9A9AFF"/>
</g>
<g id="Ellipse 24" opacity="0.3" filter="url(#filter9_f_20300_14972)">
<circle cx="109.75" cy="15.4361" r="0.502536" fill="#9A9AFF"/>
</g>
<g id="Ellipse 25" opacity="0.3" filter="url(#filter10_f_20300_14972)">
<circle cx="119.801" cy="12.6705" r="0.502536" fill="#9A9AFF"/>
</g>
<g id="Ellipse 27" opacity="0.3" filter="url(#filter11_f_20300_14972)">
<circle cx="94.0885" cy="11.0182" r="0.502536" fill="#9A9AFF"/>
</g>
<g id="Ellipse 30" opacity="0.3" filter="url(#filter12_f_20300_14972)">
<circle cx="154.475" cy="11.6666" r="0.502536" fill="#9A9AFF"/>
</g>
<g id="Ellipse 29" opacity="0.3" filter="url(#filter13_f_20300_14972)">
<circle cx="146.435" cy="8.90488" r="0.502536" fill="#9A9AFF"/>
</g>
<g id="Ellipse 15" opacity="0.3" filter="url(#filter14_f_20300_14972)">
<circle cx="24.0103" cy="4.44785" r="0.502536" fill="#9A9AFF"/>
</g>
<g id="Ellipse 33" opacity="0.3" filter="url(#filter15_f_20300_14972)">
<circle cx="-1.05899" cy="13.6783" r="0.502536" fill="#9A9AFF"/>
</g>
<g id="Ellipse 16" opacity="0.3" filter="url(#filter16_f_20300_14972)">
<circle cx="17.441" cy="16.4908" r="0.502536" fill="#9A9AFF"/>
</g>
<g id="Ellipse 35" opacity="0.3" filter="url(#filter17_f_20300_14972)">
<circle cx="36.0553" cy="6.63535" r="0.502536" fill="#9A9AFF"/>
</g>
<g id="Ellipse 32" opacity="0.3" filter="url(#filter18_f_20300_14972)">
<circle cx="10.8707" cy="14.2994" r="0.502536" fill="#9A9AFF"/>
</g>
<g id="Ellipse 13" opacity="0.3" filter="url(#filter19_f_20300_14972)">
<circle cx="55.7274" cy="9.65615" r="0.753803" fill="#9A9AFF"/>
</g>
<g id="Ellipse 19" opacity="0.3" filter="url(#filter20_f_20300_14972)">
<circle cx="87.3886" cy="20.7108" r="0.753803" fill="#9A9AFF"/>
</g>
<g id="Ellipse 26" opacity="0.3" filter="url(#filter21_f_20300_14972)">
<circle cx="123.82" cy="15.1874" r="0.753803" fill="#9A9AFF"/>
</g>
<g id="Ellipse 36" opacity="0.3" filter="url(#filter22_f_20300_14972)">
<circle cx="134.876" cy="14.4335" r="0.753803" fill="#9A9AFF"/>
</g>
<g id="Ellipse 28" opacity="0.3" filter="url(#filter23_f_20300_14972)">
<circle cx="129.6" cy="6.38662" r="0.753803" fill="#9A9AFF"/>
</g>
<g id="Ellipse 14" opacity="0.3" filter="url(#filter24_f_20300_14972)">
<circle cx="41.6571" cy="14.9335" r="0.753803" fill="#9A9AFF"/>
</g>
<g id="Ellipse 31" opacity="0.3" filter="url(#filter25_f_20300_14972)">
<circle cx="4.21767" cy="17.9491" r="0.753803" fill="#9A9AFF"/>
</g>
<g id="Ellipse 34" opacity="0.3" filter="url(#filter26_f_20300_14972)">
<circle cx="12.2177" cy="6.88662" r="0.753803" fill="#9A9AFF"/>
</g>
<g id="Ellipse 17" opacity="0.3" filter="url(#filter27_f_20300_14972)">
<circle cx="50.2001" cy="3.6249" r="0.753803" fill="#9A9AFF"/>
</g>
</g>
<defs>
<filter id="filter0_f_20300_14972" x="-94" y="-88" width="361.085" height="168.176" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="32" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter1_f_20300_14972" x="72.5633" y="2.61993" width="4.01991" height="4.02186" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.502536" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter2_f_20300_14972" x="80.6029" y="6.13418" width="3.01484" height="3.01679" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.251268" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter3_f_20300_14972" x="115.278" y="3.12246" width="3.01484" height="3.01679" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.251268" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter4_f_20300_14972" x="63.2664" y="7.39727" width="3.01502" height="3.01405" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.502536" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter5_f_20300_14972" x="94.9246" y="2.36602" width="3.01502" height="3.01405" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.502536" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter6_f_20300_14972" x="74.5731" y="13.4285" width="3.01502" height="3.01405" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.502536" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter7_f_20300_14972" x="103.218" y="3.36993" width="3.01502" height="3.01405" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.502536" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter8_f_20300_14972" x="112.513" y="5.63165" width="3.01502" height="3.01405" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.502536" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter9_f_20300_14972" x="108.242" y="13.9285" width="3.01502" height="3.01405" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.502536" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter10_f_20300_14972" x="118.294" y="11.1629" width="3.01502" height="3.01405" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.502536" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter11_f_20300_14972" x="92.5809" y="9.51055" width="3.01502" height="3.01405" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.502536" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter12_f_20300_14972" x="153.47" y="10.6615" width="2.00995" height="2.00898" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.251268" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter13_f_20300_14972" x="144.928" y="7.39727" width="3.01502" height="3.01405" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.502536" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter14_f_20300_14972" x="22.5027" y="2.94024" width="3.01502" height="3.01405" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.502536" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter15_f_20300_14972" x="-2.56659" y="12.1707" width="3.01502" height="3.01405" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.502536" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter16_f_20300_14972" x="15.9334" y="14.9832" width="3.01502" height="3.01405" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.502536" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter17_f_20300_14972" x="35.0502" y="5.63028" width="2.00995" height="2.00898" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.251268" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter18_f_20300_14972" x="9.86563" y="13.2943" width="2.00995" height="2.00898" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.251268" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter19_f_20300_14972" x="53.9686" y="7.89727" width="3.51795" height="3.51795" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.502536" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter20_f_20300_14972" x="85.6297" y="18.952" width="3.51795" height="3.51795" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.502536" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter21_f_20300_14972" x="122.061" y="13.4285" width="3.51795" height="3.51795" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.502536" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter22_f_20300_14972" x="133.117" y="12.6746" width="3.51795" height="3.51795" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.502536" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter23_f_20300_14972" x="127.842" y="4.62774" width="3.51795" height="3.51795" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.502536" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter24_f_20300_14972" x="39.8982" y="13.1746" width="3.51795" height="3.51795" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.502536" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter25_f_20300_14972" x="2.4588" y="16.1902" width="3.51795" height="3.51795" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.502536" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter26_f_20300_14972" x="10.4588" y="5.12774" width="3.51795" height="3.51795" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.502536" result="effect1_foregroundBlur_20300_14972"/>
</filter>
<filter id="filter27_f_20300_14972" x="48.4412" y="1.86602" width="3.51795" height="3.51795" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.502536" result="effect1_foregroundBlur_20300_14972"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

+8 -1
View File
@@ -2,6 +2,8 @@ import { lazy, Suspense } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom'; import { Navigate, Route, Routes } from 'react-router-dom';
import styled, { AnyStyledComponent, css } from 'styled-components'; import styled, { AnyStyledComponent, css } from 'styled-components';
import { WagmiConfig } from 'wagmi'; import { WagmiConfig } from 'wagmi';
import { PrivyProvider } from '@privy-io/react-auth';
import { PrivyWagmiConnector } from '@privy-io/wagmi-connector';
import { QueryClient, QueryClientProvider } from 'react-query'; import { QueryClient, QueryClientProvider } from 'react-query';
import { GrazProvider } from 'graz'; import { GrazProvider } from 'graz';
@@ -32,7 +34,7 @@ import { NotificationsToastArea } from '@/layout/NotificationsToastArea';
import { DialogManager } from '@/layout/DialogManager'; import { DialogManager } from '@/layout/DialogManager';
import { GlobalCommandDialog } from '@/views/dialogs/GlobalCommandDialog'; import { GlobalCommandDialog } from '@/views/dialogs/GlobalCommandDialog';
import { config } from '@/lib/wagmi'; import { config, privyConfig } from '@/lib/wagmi';
import { breakpoints } from '@/styles'; import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
@@ -120,8 +122,13 @@ const wrapProvider = (Component: React.ComponentType<any>, props?: any) => {
}; };
const providers = [ const providers = [
wrapProvider(PrivyProvider, {
appId: import.meta.env.VITE_PRIVY_APP_ID,
config: privyConfig,
}),
wrapProvider(QueryClientProvider, { client: queryClient }), wrapProvider(QueryClientProvider, { client: queryClient }),
wrapProvider(GrazProvider), wrapProvider(GrazProvider),
wrapProvider(PrivyWagmiConnector, { wagmiChainsConfig: config }),
wrapProvider(WagmiConfig, { config }), wrapProvider(WagmiConfig, { config }),
wrapProvider(LocaleProvider), wrapProvider(LocaleProvider),
wrapProvider(RestrictionProvider), wrapProvider(RestrictionProvider),
+26
View File
@@ -0,0 +1,26 @@
import type { Story } from '@ladle/react';
import { Accordion as AccordionComponent, AccordionProps } from '@/components/Accordion';
import { StoryWrapper } from '.ladle/components';
export const Accordion: Story<AccordionProps> = (args) => {
return (
<StoryWrapper>
<AccordionComponent {...args} />
</StoryWrapper>
);
};
Accordion.args = {
items: [
{
header: 'Question 1?',
content: 'Answer 1.',
},
{
header: 'Question 2?',
content: 'Answer 2.',
},
],
};
+13 -3
View File
@@ -3,6 +3,8 @@ import styled, { keyframes, type AnyStyledComponent } from 'styled-components';
import { Root, Item, Header, Trigger, Content } from '@radix-ui/react-accordion'; import { Root, Item, Header, Trigger, Content } from '@radix-ui/react-accordion';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
import { breakpoints } from '@/styles';
import { PlusIcon } from '@/icons'; import { PlusIcon } from '@/icons';
export type AccordionItem = { export type AccordionItem = {
@@ -10,7 +12,7 @@ export type AccordionItem = {
content: React.ReactNode; content: React.ReactNode;
}; };
type AccordionProps = { export type AccordionProps = {
items: AccordionItem[]; items: AccordionItem[];
className?: string; className?: string;
}; };
@@ -36,6 +38,13 @@ export const Accordion = ({ items, className }: AccordionProps) => (
const Styled: Record<string, AnyStyledComponent> = {}; const Styled: Record<string, AnyStyledComponent> = {};
Styled.Root = styled(Root)` Styled.Root = styled(Root)`
--accordion-paddingY: 1rem;
--accordion-paddingX: 1rem;
@media ${breakpoints.notTablet} {
--accordion-paddingX: 1.5rem;
}
> *:not(:last-child) { > *:not(:last-child) {
border-bottom: var(--border-width) solid var(--border-color); border-bottom: var(--border-width) solid var(--border-color);
} }
@@ -67,7 +76,8 @@ Styled.Icon = styled.div`
Styled.Trigger = styled(Trigger)` Styled.Trigger = styled(Trigger)`
${layoutMixins.spacedRow} ${layoutMixins.spacedRow}
width: 100%; width: 100%;
padding: 1rem 0.75rem; padding: var(--accordion-paddingY) var(--accordion-paddingX);
gap: 0.5rem;
color: var(--color-text-1); color: var(--color-text-1);
text-align: start; text-align: start;
@@ -91,7 +101,7 @@ Styled.Trigger = styled(Trigger)`
Styled.Content = styled(Content)` Styled.Content = styled(Content)`
overflow: hidden; overflow: hidden;
margin: 0 0.75rem 1rem; margin: 0 var(--accordion-paddingX) var(--accordion-paddingY);
color: var(--color-text-0); color: var(--color-text-0);
+3 -3
View File
@@ -24,7 +24,6 @@ import {
CoinsIcon, CoinsIcon,
CommentIcon, CommentIcon,
CopyIcon, CopyIcon,
CubeIcon,
DepositIcon, DepositIcon,
DepthChartIcon, DepthChartIcon,
DiscordIcon, DiscordIcon,
@@ -60,6 +59,7 @@ import {
PriceChartIcon, PriceChartIcon,
PrivacyIcon, PrivacyIcon,
QrIcon, QrIcon,
RewardStarIcon,
SearchIcon, SearchIcon,
SendIcon, SendIcon,
ShareIcon, ShareIcon,
@@ -99,7 +99,6 @@ export enum IconName {
Coins = 'Coins', Coins = 'Coins',
Comment = 'Comment', Comment = 'Comment',
Copy = 'Copy', Copy = 'Copy',
Cube = 'Cube',
Deposit = 'Deposit', Deposit = 'Deposit',
DepthChart = 'DepthChart', DepthChart = 'DepthChart',
Discord = 'Discord', Discord = 'Discord',
@@ -136,6 +135,7 @@ export enum IconName {
PriceChart = 'PriceChart', PriceChart = 'PriceChart',
Privacy = 'Privacy', Privacy = 'Privacy',
Qr = 'Qr', Qr = 'Qr',
RewardStar = 'RewardStar',
Search = 'Search', Search = 'Search',
Send = 'Send', Send = 'Send',
Share = 'Share', Share = 'Share',
@@ -175,7 +175,6 @@ const icons = {
[IconName.Coins]: CoinsIcon, [IconName.Coins]: CoinsIcon,
[IconName.Comment]: CommentIcon, [IconName.Comment]: CommentIcon,
[IconName.Copy]: CopyIcon, [IconName.Copy]: CopyIcon,
[IconName.Cube]: CubeIcon,
[IconName.Deposit]: DepositIcon, [IconName.Deposit]: DepositIcon,
[IconName.DepthChart]: DepthChartIcon, [IconName.DepthChart]: DepthChartIcon,
[IconName.Discord]: DiscordIcon, [IconName.Discord]: DiscordIcon,
@@ -211,6 +210,7 @@ const icons = {
[IconName.PriceChart]: PriceChartIcon, [IconName.PriceChart]: PriceChartIcon,
[IconName.Privacy]: PrivacyIcon, [IconName.Privacy]: PrivacyIcon,
[IconName.Qr]: QrIcon, [IconName.Qr]: QrIcon,
[IconName.RewardStar]: RewardStarIcon,
[IconName.Search]: SearchIcon, [IconName.Search]: SearchIcon,
[IconName.Send]: SendIcon, [IconName.Send]: SendIcon,
[IconName.Share]: ShareIcon, [IconName.Share]: ShareIcon,
+7
View File
@@ -4,6 +4,7 @@ import { Link } from 'react-router-dom';
import { Icon, IconName } from '@/components/Icon'; import { Icon, IconName } from '@/components/Icon';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
import { breakpoints } from '@/styles';
type PanelProps = { type PanelProps = {
slotHeaderContent?: string; slotHeaderContent?: string;
@@ -67,6 +68,12 @@ Styled.Panel = styled.section<{ onClick?: () => void }>`
--panel-content-paddingY: var(--panel-paddingY); --panel-content-paddingY: var(--panel-paddingY);
--panel-content-paddingX: var(--panel-paddingX); --panel-content-paddingX: var(--panel-paddingX);
@media ${breakpoints.notTablet} {
--panel-paddingX: 1.5rem;
--panel-paddingY: 1.25rem;
--panel-content-paddingY: 1rem;
}
${layoutMixins.row} ${layoutMixins.row}
background-color: var(--color-layer-3); background-color: var(--color-layer-3);
+1 -2
View File
@@ -27,9 +27,8 @@ export const ToastArea = ({ swipeDirection, children, className }: ToastAreaProp
const $ToastArea = styled.aside` const $ToastArea = styled.aside`
// Params // Params
--toasts-gap: 0.5rem; --toasts-gap: 0.5rem;
// Rules // Rules
${layoutMixins.scrollArea}
z-index: 1; z-index: 1;
pointer-events: none; pointer-events: none;
@@ -1,26 +1,33 @@
import React, { useContext, useState } from 'react'; import React, { PropsWithChildren, useContext, useState } from 'react';
import { Point } from '@visx/point'; import { Point } from '@visx/point';
import { localPoint } from '@visx/event'; import { localPoint } from '@visx/event';
import { XYChart, DataContext } from '@visx/xychart'; import { XYChart, DataContext, type EventHandlerParams } from '@visx/xychart';
import { getScaleBandwidth } from '@/components/visx/getScaleBandwidth'; import { getScaleBandwidth } from '@/components/visx/getScaleBandwidth';
export const XYChartWithPointerEvents = ({ export const XYChartWithPointerEvents = ({
onPointerMove, onPointerUp, onPointerPressedChange, ...props onPointerMove,
onPointerUp,
onPointerPressedChange,
...props
}: { }: {
onPointerMove?: (point: Point) => void; onPointerMove?: (point: Point | EventHandlerParams<object>) => void;
onPointerUp?: (point: Point) => void; onPointerUp?: (point: Point | EventHandlerParams<object>) => void;
onPointerPressedChange?: (isPointerPressed: boolean) => void; onPointerPressedChange?: (isPointerPressed: boolean) => void;
} & React.PropsWithChildren<Parameters<typeof XYChart>>) => { } & PropsWithChildren<Parameters<typeof XYChart>[0]>) => {
const { xScale, yScale } = useContext(DataContext); const { xScale, yScale } = useContext(DataContext);
const [lastPointerMoveEvent, setLastPointerMoveEvent] = useState<React.PointerEvent>(); const [lastPointerMoveEvent, setLastPointerMoveEvent] = useState<React.PointerEvent>();
const pointerContainerPosition = lastPointerMoveEvent ? localPoint(lastPointerMoveEvent) : null; const pointerContainerPosition = lastPointerMoveEvent ? localPoint(lastPointerMoveEvent) : null;
const pointerChartPosition = xScale && yScale && pointerContainerPosition && const pointerChartPosition =
xScale &&
yScale &&
pointerContainerPosition &&
new Point({ new Point({
// @ts-expect-error invert supposedly doesn't exist on AxisScale
x: xScale.invert(pointerContainerPosition?.x - getScaleBandwidth(xScale) / 2), x: xScale.invert(pointerContainerPosition?.x - getScaleBandwidth(xScale) / 2),
// @ts-expect-error invert supposedly doesn't exist on AxisScale
y: yScale.invert(pointerContainerPosition?.y - getScaleBandwidth(yScale) / 2), y: yScale.invert(pointerContainerPosition?.y - getScaleBandwidth(yScale) / 2),
}); });
@@ -29,15 +36,13 @@ export const XYChartWithPointerEvents = ({
{...props} {...props}
onPointerMove={({ event }) => { onPointerMove={({ event }) => {
setLastPointerMoveEvent(event as React.PointerEvent); setLastPointerMoveEvent(event as React.PointerEvent);
if (pointerChartPosition) if (pointerChartPosition) onPointerMove?.(pointerChartPosition);
onPointerMove?.(pointerChartPosition);
}} }}
onPointerOut={() => setLastPointerMoveEvent(undefined)} onPointerOut={() => setLastPointerMoveEvent(undefined)}
onPointerDown={() => onPointerPressedChange?.(true)} onPointerDown={() => onPointerPressedChange?.(true)}
onPointerUp={() => { onPointerUp={() => {
onPointerPressedChange?.(false); onPointerPressedChange?.(false);
if (pointerChartPosition) if (pointerChartPosition) onPointerUp?.(pointerChartPosition);
onPointerUp?.(pointerChartPosition);
}} }}
> >
{props.children} {props.children}
+3 -3
View File
@@ -142,7 +142,7 @@ const historicalPnlPeriod = [...HistoricalPnlPeriod.values()] as const;
export type HistoricalPnlPeriods = (typeof historicalPnlPeriod)[number]; export type HistoricalPnlPeriods = (typeof historicalPnlPeriod)[number];
// ------ Transfer Items ------ // // ------ Transfer Items ------ //
export const TransferInputField = Abacus.exchange.dydx.abacus.state.modal.TransferInputField; export const TransferInputField = Abacus.exchange.dydx.abacus.state.model.TransferInputField;
const transferInputFields = [...TransferInputField.values()] as const; const transferInputFields = [...TransferInputField.values()] as const;
export type TransferInputFields = (typeof transferInputFields)[number]; export type TransferInputFields = (typeof transferInputFields)[number];
@@ -151,7 +151,7 @@ const transferTypes = [...TransferType.values()] as const;
export type TransferTypes = (typeof transferTypes)[number]; export type TransferTypes = (typeof transferTypes)[number];
// ------ Trade Items ------ // // ------ Trade Items ------ //
export const TradeInputField = Abacus.exchange.dydx.abacus.state.modal.TradeInputField; export const TradeInputField = Abacus.exchange.dydx.abacus.state.model.TradeInputField;
const tradeInputFields = [...TradeInputField.values()] as const; const tradeInputFields = [...TradeInputField.values()] as const;
export type TradeInputFields = (typeof tradeInputFields)[number]; export type TradeInputFields = (typeof tradeInputFields)[number];
@@ -162,7 +162,7 @@ export type TradeState<T> = {
}; };
export const ClosePositionInputField = export const ClosePositionInputField =
Abacus.exchange.dydx.abacus.state.modal.ClosePositionInputField; Abacus.exchange.dydx.abacus.state.model.ClosePositionInputField;
const closePositionInputFields = [...ClosePositionInputField.values()] as const; const closePositionInputFields = [...ClosePositionInputField.values()] as const;
export type ClosePositionInputFields = (typeof closePositionInputFields)[number]; export type ClosePositionInputFields = (typeof closePositionInputFields)[number];
+1
View File
@@ -96,6 +96,7 @@ export type AnalyticsEventData<T extends AnalyticsEvent> =
elapsedTime?: number; elapsedTime?: number;
blockHeight?: number; blockHeight?: number;
indexerBlockHeight?: number; indexerBlockHeight?: number;
trailingBlocks?: number;
} }
: // Navigation : // Navigation
T extends AnalyticsEvent.NavigatePage T extends AnalyticsEvent.NavigatePage
+40
View File
@@ -0,0 +1,40 @@
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { FundingDirection } from './markets';
// ------ Depth Chart ------ //
export enum DepthChartSeries {
Asks = 'Asks',
Bids = 'Bids',
MidMarket = 'MidMarket',
}
export type DepthChartDatum = {
size: number;
price: number;
depth: number;
seriesKey: DepthChartSeries;
};
export type DepthChartPoint = {
side: OrderSide;
price: number;
size: number;
};
export const SERIES_KEY_FOR_ORDER_SIDE = {
[OrderSide.BUY]: DepthChartSeries.Bids,
[OrderSide.SELL]: DepthChartSeries.Asks,
};
// ------ Funding Chart ------ //
export enum FundingRateResolution {
OneHour = 'OneHour',
EightHour = 'EightHour',
Annualized = 'Annualized',
}
export type FundingChartDatum = {
time: number;
fundingRate: number;
direction: FundingDirection;
};
+1
View File
@@ -25,4 +25,5 @@ export const DEFAULT_MARKETID = 'ETH-USD';
export enum FundingDirection { export enum FundingDirection {
ToShort = 'ToShort', ToShort = 'ToShort',
ToLong = 'ToLong', ToLong = 'ToLong',
None = 'None',
} }
+2
View File
@@ -87,6 +87,8 @@ export type NotificationDisplayData = {
slotTitleLeft?: React.ReactNode; slotTitleLeft?: React.ReactNode;
slotTitleRight?: React.ReactNode; slotTitleRight?: React.ReactNode;
groupKey: string; // Grouping key toast notification stacking
// Overrides title/body for Notification in NotificationMenu // Overrides title/body for Notification in NotificationMenu
renderCustomBody?: ({ renderCustomBody?: ({
isToast, isToast,
+11
View File
@@ -31,6 +31,7 @@ import { DydxNetwork, ENVIRONMENT_CONFIG_MAP } from './networks';
export enum WalletConnectionType { export enum WalletConnectionType {
CoinbaseWalletSdk = 'coinbaseWalletSdk', CoinbaseWalletSdk = 'coinbaseWalletSdk',
CosmosSigner = 'CosmosSigner', CosmosSigner = 'CosmosSigner',
Privy = 'Privy',
InjectedEip1193 = 'injectedEip1193', InjectedEip1193 = 'injectedEip1193',
WalletConnect2 = 'walletConnect2', WalletConnect2 = 'walletConnect2',
} }
@@ -68,6 +69,9 @@ export const walletConnectionTypes: Record<WalletConnectionType, WalletConnectio
[WalletConnectionType.CosmosSigner]: { [WalletConnectionType.CosmosSigner]: {
name: 'CosmosSigner', name: 'CosmosSigner',
}, },
[WalletConnectionType.Privy]: {
name: 'Privy',
},
}; };
// Wallets // Wallets
@@ -90,6 +94,7 @@ export enum WalletType {
WalletConnect2 = 'WALLETCONNECT_2', WalletConnect2 = 'WALLETCONNECT_2',
// TestWallet = 'TEST_WALLET', // TestWallet = 'TEST_WALLET',
OtherWallet = 'OTHER_WALLET', OtherWallet = 'OTHER_WALLET',
Privy = 'PRIVY',
} }
const WALLET_CONNECT_EXPLORER_RECOMMENDED_WALLETS = { const WALLET_CONNECT_EXPLORER_RECOMMENDED_WALLETS = {
@@ -243,6 +248,12 @@ export const wallets: Record<WalletType, WalletConfig> = {
icon: KeplrIcon, icon: KeplrIcon,
connectionTypes: [WalletConnectionType.CosmosSigner], connectionTypes: [WalletConnectionType.CosmosSigner],
}, },
[WalletType.Privy]: {
type: WalletType.Privy,
stringKey: STRING_KEYS.KEPLR,
icon: GenericWalletIcon,
connectionTypes: [WalletConnectionType.Privy],
},
}; };
// Injected EIP-1193 Providers // Injected EIP-1193 Providers
+55 -4
View File
@@ -1,13 +1,19 @@
import { useCallback, useContext, createContext, useEffect, useState, useMemo } from 'react'; import { useCallback, useContext, createContext, useEffect, useState, useMemo } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { AES, enc } from 'crypto-js'; import { AES, enc } from 'crypto-js';
import { NOBLE_BECH32_PREFIX, 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 { OnboardingGuard, OnboardingState, type EvmDerivedAddresses } from '@/constants/account';
import { DialogTypes } from '@/constants/dialogs'; import { DialogTypes } from '@/constants/dialogs';
import { LocalStorageKey, LOCAL_STORAGE_VERSIONS } from '@/constants/localStorage'; import { LocalStorageKey, LOCAL_STORAGE_VERSIONS } from '@/constants/localStorage';
import { DydxAddress, EvmAddress, PrivateInformation } from '@/constants/wallets'; import {
DydxAddress,
EvmAddress,
PrivateInformation,
WalletConnectionType,
getSignTypedData,
} from '@/constants/wallets';
import { setOnboardingState, setOnboardingGuard } from '@/state/account'; import { setOnboardingState, setOnboardingGuard } from '@/state/account';
import { forceOpenDialog } from '@/state/dialogs'; import { forceOpenDialog } from '@/state/dialogs';
@@ -20,6 +26,11 @@ import { useLocalStorage } from './useLocalStorage';
import { useRestrictions } from './useRestrictions'; import { useRestrictions } from './useRestrictions';
import { useWalletConnection } from './useWalletConnection'; import { useWalletConnection } from './useWalletConnection';
import { useSignTypedData } from 'wagmi';
import { getSelectedNetwork } from '@/state/appSelectors';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { usePrivy } from '@privy-io/react-auth';
const AccountsContext = createContext<ReturnType<typeof useAccountsContext> | undefined>(undefined); const AccountsContext = createContext<ReturnType<typeof useAccountsContext> | undefined>(undefined);
AccountsContext.displayName = 'Accounts'; AccountsContext.displayName = 'Accounts';
@@ -153,6 +164,7 @@ const useAccountsContext = () => {
// dYdX wallet / onboarding state // dYdX wallet / onboarding state
const [localDydxWallet, setLocalDydxWallet] = useState<LocalWallet>(); const [localDydxWallet, setLocalDydxWallet] = useState<LocalWallet>();
const [localNobleWallet, setLocalNobleWallet] = useState<LocalWallet>();
const [hdKey, setHdKey] = useState<PrivateInformation>(); const [hdKey, setHdKey] = useState<PrivateInformation>();
const dydxAccounts = useMemo(() => localDydxWallet?.accounts, [localDydxWallet]); const dydxAccounts = useMemo(() => localDydxWallet?.accounts, [localDydxWallet]);
@@ -162,6 +174,11 @@ const useAccountsContext = () => {
[localDydxWallet] [localDydxWallet]
); );
const nobleAddress = useMemo(
() => localNobleWallet?.address,
[localNobleWallet]
);
const setWalletFromEvmSignature = async (signature: string) => { const setWalletFromEvmSignature = async (signature: string) => {
const { wallet, mnemonic, privateKey, publicKey } = await getWalletFromEvmSignature({ const { wallet, mnemonic, privateKey, publicKey } = await getWalletFromEvmSignature({
signature, signature,
@@ -176,6 +193,20 @@ const useAccountsContext = () => {
} }
}, [evmAddress, dydxAddress]); }, [evmAddress, dydxAddress]);
const selectedNetwork = useSelector(getSelectedNetwork);
const chainId = Number(ENVIRONMENT_CONFIG_MAP[selectedNetwork].ethereumChainId);
const signTypedData = getSignTypedData(selectedNetwork);
const { signTypedDataAsync } = useSignTypedData({
...signTypedData,
domain: {
...signTypedData.domain,
chainId,
},
});
const { ready, authenticated } = usePrivy();
useEffect(() => { useEffect(() => {
(async () => { (async () => {
if (connectedDydxAddress && signerGraz) { if (connectedDydxAddress && signerGraz) {
@@ -192,7 +223,17 @@ const useAccountsContext = () => {
const evmDerivedAccount = evmDerivedAddresses[evmAddress]; const evmDerivedAccount = evmDerivedAddresses[evmAddress];
if (evmDerivedAccount?.encryptedSignature) { if (walletConnectionType === WalletConnectionType.Privy && authenticated && ready) {
try {
const signature = await signTypedDataAsync();
await setWalletFromEvmSignature(signature);
dispatch(setOnboardingState(OnboardingState.AccountConnected));
} catch (error) {
log('useAccounts/decryptSignature', error);
forgetEvmSignature();
}
} else if (evmDerivedAccount?.encryptedSignature) {
try { try {
const signature = decryptSignature(evmDerivedAccount.encryptedSignature); const signature = decryptSignature(evmDerivedAccount.encryptedSignature);
@@ -211,7 +252,15 @@ const useAccountsContext = () => {
dispatch(setOnboardingState(OnboardingState.Disconnected)); dispatch(setOnboardingState(OnboardingState.Disconnected));
} }
})(); })();
}, [evmAddress, evmDerivedAddresses, signerWagmi, connectedDydxAddress, signerGraz]); }, [
evmAddress,
evmDerivedAddresses,
signerWagmi,
connectedDydxAddress,
signerGraz,
authenticated,
ready,
]);
// abacus // abacus
useEffect(() => { useEffect(() => {
@@ -224,6 +273,7 @@ const useAccountsContext = () => {
if (hdKey?.mnemonic) { if (hdKey?.mnemonic) {
const nobleWallet = await LocalWallet.fromMnemonic(hdKey.mnemonic, NOBLE_BECH32_PREFIX); const nobleWallet = await LocalWallet.fromMnemonic(hdKey.mnemonic, NOBLE_BECH32_PREFIX);
abacusStateManager.setNobleWallet(nobleWallet); abacusStateManager.setNobleWallet(nobleWallet);
setLocalNobleWallet(nobleWallet);
} }
}; };
setNobleWallet(); setNobleWallet();
@@ -322,6 +372,7 @@ const useAccountsContext = () => {
localDydxWallet, localDydxWallet,
dydxAccounts, dydxAccounts,
dydxAddress, dydxAddress,
nobleAddress,
// Onboarding state // Onboarding state
saveHasAcknowledgedTerms, saveHasAcknowledgedTerms,
+2 -1
View File
@@ -97,7 +97,7 @@ export const useAnalytics = () => {
}, []); }, []);
// AnalyticsEvent.NetworkStatus // AnalyticsEvent.NetworkStatus
const { height, indexerHeight, status } = useApiState(); const { height, indexerHeight, status, trailingBlocks} = useApiState();
useEffect(() => { useEffect(() => {
if (status) { if (status) {
@@ -114,6 +114,7 @@ export const useAnalytics = () => {
elapsedTime: lastSuccessfulIndexerRpcQuery && Date.now() - lastSuccessfulIndexerRpcQuery, elapsedTime: lastSuccessfulIndexerRpcQuery && Date.now() - lastSuccessfulIndexerRpcQuery,
blockHeight: height ?? undefined, blockHeight: height ?? undefined,
indexerBlockHeight: indexerHeight ?? undefined, indexerBlockHeight: indexerHeight ?? undefined,
trailingBlocks: trailingBlocks ?? undefined
}); });
} }
}, [status]); }, [status]);
+1 -1
View File
@@ -71,7 +71,7 @@ export const getIndexerHeight = (apiState: Nullable<AbacusApiState>) => {
export const useApiState = () => { export const useApiState = () => {
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
const apiState = useSelector(getApiState, shallowEqual); const apiState = useSelector(getApiState, shallowEqual);
const { haltedBlock, height, status, trailingBlocks } = apiState ?? {}; const { haltedBlock, height, status, trailingBlocks} = apiState ?? {};
const statusErrorMessage = getStatusErrorMessage({ apiState, stringGetter }); const statusErrorMessage = getStatusErrorMessage({ apiState, stringGetter });
const indexerHeight = getIndexerHeight(apiState); const indexerHeight = getIndexerHeight(apiState);
+1
View File
@@ -21,6 +21,7 @@ export const useDisplayedWallets = () => {
// WalletType.BitKeep, // WalletType.BitKeep,
// WalletType.Coin98, // WalletType.Coin98,
WalletType.Privy,
WalletType.OtherWallet, WalletType.OtherWallet,
].filter(isTruthy); ].filter(isTruthy);
}; };
+13 -2
View File
@@ -1,5 +1,9 @@
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { useNetwork, useSwitchNetwork } from 'wagmi'; import { useNetwork, useSwitchNetwork } from 'wagmi';
import { useSwitchNetwork as useSwitchNetworkPrivy } from '@privy-io/wagmi-connector';
import { WalletConnectionType } from '@/constants/wallets';
import { useWalletConnection } from './useWalletConnection';
export const useMatchingEvmNetwork = ({ export const useMatchingEvmNetwork = ({
chainId, chainId,
@@ -11,7 +15,10 @@ export const useMatchingEvmNetwork = ({
onError?: (error: Error) => void; onError?: (error: Error) => void;
}) => { }) => {
const { chain } = useNetwork(); const { chain } = useNetwork();
const { walletConnectionType } = useWalletConnection();
const { isLoading, switchNetworkAsync } = useSwitchNetwork({ onError }); const { isLoading, switchNetworkAsync } = useSwitchNetwork({ onError });
const { isLoading: isLoadingPrivy, switchNetworkAsync: switchNetworkAsyncPrivy } =
useSwitchNetworkPrivy({ onError });
// If chainId is not a number, we can assume it is a non EVM compatible chain // If chainId is not a number, we can assume it is a non EVM compatible chain
const isMatchingNetwork = useMemo( const isMatchingNetwork = useMemo(
@@ -21,7 +28,11 @@ export const useMatchingEvmNetwork = ({
const matchNetwork = useCallback(async () => { const matchNetwork = useCallback(async () => {
if (!isMatchingNetwork) { if (!isMatchingNetwork) {
await switchNetworkAsync?.(Number(chainId)); if (walletConnectionType === WalletConnectionType.Privy) {
await switchNetworkAsyncPrivy?.(Number(chainId));
} else {
await switchNetworkAsync?.(Number(chainId));
}
} }
}, [chainId, chain]); }, [chainId, chain]);
@@ -34,6 +45,6 @@ export const useMatchingEvmNetwork = ({
return { return {
isMatchingNetwork, isMatchingNetwork,
matchNetwork, matchNetwork,
isSwitchingNetwork: isLoading, isSwitchingNetwork: isLoading || isLoadingPrivy,
}; };
}; };
+28
View File
@@ -28,6 +28,7 @@ import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { AssetIcon } from '@/components/AssetIcon'; import { AssetIcon } from '@/components/AssetIcon';
import { Icon, IconName } from '@/components/Icon'; import { Icon, IconName } from '@/components/Icon';
import { BlockRewardNotification } from '@/views/notifications/BlockRewardNotification';
import { TradeNotification } from '@/views/notifications/TradeNotification'; import { TradeNotification } from '@/views/notifications/TradeNotification';
import { TransferStatusNotification } from '@/views/notifications/TransferStatusNotification'; import { TransferStatusNotification } from '@/views/notifications/TransferStatusNotification';
@@ -80,6 +81,7 @@ export const notificationTypes: NotificationTypeConfig[] = [
body: abacusNotif.text ? stringGetter({ key: abacusNotif.text, params }) : '', body: abacusNotif.text ? stringGetter({ key: abacusNotif.text, params }) : '',
toastDuration: DEFAULT_TOAST_AUTO_CLOSE_MS, toastDuration: DEFAULT_TOAST_AUTO_CLOSE_MS,
toastSensitivity: 'foreground', toastSensitivity: 'foreground',
groupKey: abacusNotificationType,
renderCustomBody: ({ isToast, notification }) => ( renderCustomBody: ({ isToast, notification }) => (
<TradeNotification <TradeNotification
isToast={isToast} isToast={isToast}
@@ -93,6 +95,29 @@ export const notificationTypes: NotificationTypeConfig[] = [
); );
break; break;
} }
case 'blockReward': {
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',
groupKey: abacusNotificationType,
renderCustomBody: ({ isToast, notification }) => (
<BlockRewardNotification
isToast={isToast}
data={parsedData}
notification={notification}
/>
),
},
[abacusNotif.updateTimeInMilliseconds, abacusNotif.data],
true
);
break;
}
default: default:
trigger( trigger(
abacusNotif.id, abacusNotif.id,
@@ -102,6 +127,7 @@ export const notificationTypes: NotificationTypeConfig[] = [
body: abacusNotif.text ? stringGetter({ key: abacusNotif.text, params }) : '', body: abacusNotif.text ? stringGetter({ key: abacusNotif.text, params }) : '',
toastDuration: DEFAULT_TOAST_AUTO_CLOSE_MS, toastDuration: DEFAULT_TOAST_AUTO_CLOSE_MS,
toastSensitivity: 'foreground', toastSensitivity: 'foreground',
groupKey: abacusNotificationType,
}, },
[abacusNotif.updateTimeInMilliseconds, abacusNotif.data] [abacusNotif.updateTimeInMilliseconds, abacusNotif.data]
); );
@@ -197,6 +223,7 @@ export const notificationTypes: NotificationTypeConfig[] = [
/> />
), ),
toastSensitivity: 'foreground', toastSensitivity: 'foreground',
groupKey: NotificationType.SquidTransfer,
}, },
[isFinished] [isFinished]
); );
@@ -242,6 +269,7 @@ export const notificationTypes: NotificationTypeConfig[] = [
}, },
}), }),
toastSensitivity: 'foreground', toastSensitivity: 'foreground',
groupKey: NotificationType.ReleaseUpdates,
}, },
[] []
); );
+44
View File
@@ -0,0 +1,44 @@
import { useMemo } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { DepthChartSeries, DepthChartDatum } from '@/constants/charts';
import { getCurrentMarketOrderbook } from '@/state/perpetualsSelectors';
import { MustBigNumber } from '@/lib/numbers';
export const useOrderbookValuesForDepthChart = () => {
const orderbook = useSelector(getCurrentMarketOrderbook, shallowEqual);
return useMemo(() => {
const bids = (orderbook?.bids?.toArray() ?? [])
.filter(Boolean)
.map((datum) => ({ ...datum, seriesKey: DepthChartSeries.Bids } as DepthChartDatum));
const asks = (orderbook?.asks?.toArray() ?? [])
.filter(Boolean)
.map((datum) => ({ ...datum, seriesKey: DepthChartSeries.Asks } as DepthChartDatum));
const lowestBid = bids[bids.length - 1];
const highestBid = bids[0];
const lowestAsk = asks[0];
const highestAsk = asks[asks.length - 1];
const midMarketPrice = orderbook?.midPrice;
const spread = MustBigNumber(lowestAsk?.price ?? 0).minus(highestBid?.price ?? 0);
const spreadPercent = orderbook?.spreadPercent;
return {
bids,
asks,
lowestBid,
highestBid,
lowestAsk,
highestAsk,
midMarketPrice,
spread,
spreadPercent,
orderbook,
};
}, [orderbook]);
};
+11 -2
View File
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useState, useMemo } from 'react'; import { useCallback, useEffect, useState, useMemo } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { usePrivy, useLogout } from '@privy-io/react-auth';
import { LocalStorageKey } from '@/constants/localStorage'; import { LocalStorageKey } from '@/constants/localStorage';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks'; import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
@@ -104,8 +105,10 @@ export const useWalletConnection = () => {
[walletConnectConfig, walletType, walletConnectionType] [walletConnectConfig, walletType, walletConnectionType]
); );
const { connectAsync: connectWagmi } = useConnectWagmi({ connector: wagmiConnector }) const { connectAsync: connectWagmi } = useConnectWagmi({ connector: wagmiConnector });
const { suggestAndConnect: connectGraz } = useConnectGraz(); const { suggestAndConnect: connectGraz } = useConnectGraz();
const { login, ready, authenticated } = usePrivy();
const { logout } = useLogout();
const connectWallet = useCallback( const connectWallet = useCallback(
async ({ walletType }: { walletType: WalletType }) => { async ({ walletType }: { walletType: WalletType }) => {
@@ -114,6 +117,11 @@ export const useWalletConnection = () => {
try { try {
if (!walletConnection) { if (!walletConnection) {
throw new Error('Onboarding: No wallet connection found.'); throw new Error('Onboarding: No wallet connection found.');
} else if (walletConnection.type === WalletConnectionType.Privy) {
if (!isConnectedWagmi && !authenticated && ready) {
login();
}
} else if (walletConnection.type === WalletConnectionType.CosmosSigner) { } else if (walletConnection.type === WalletConnectionType.CosmosSigner) {
const cosmosWalletType = { const cosmosWalletType = {
[WalletType.Keplr as string]: CosmosWalletType.KEPLR, [WalletType.Keplr as string]: CosmosWalletType.KEPLR,
@@ -156,7 +164,7 @@ export const useWalletConnection = () => {
walletConnectionType: walletConnection.type, walletConnectionType: walletConnection.type,
}; };
}, },
[isConnectedGraz, signerGraz, isConnectedWagmi, signerWagmi] [isConnectedGraz, signerGraz, isConnectedWagmi, signerWagmi, login, ready, authenticated]
); );
const disconnectWallet = useCallback(async () => { const disconnectWallet = useCallback(async () => {
@@ -165,6 +173,7 @@ export const useWalletConnection = () => {
if (isConnectedWagmi) await disconnectWagmi(); if (isConnectedWagmi) await disconnectWagmi();
if (isConnectedGraz) await disconnectGraz(); if (isConnectedGraz) await disconnectGraz();
if (authenticated) await logout();
}, [isConnectedGraz, isConnectedWagmi]); }, [isConnectedGraz, isConnectedWagmi]);
// Wallet selection // Wallet selection
+1 -3
View File
@@ -1,3 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg fill="none" height="10" viewBox="0 0 15 10" width="15" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="m.321829 1.09389c.200562-.192959.469529-.298401.747791-.29315.27827.00525.54307.120763.73621.32115l5.194 5.5132 5.19397-5.5132c.0944-.10408.2088-.188152.3363-.247234.1275-.059081.2655-.091968.406-.096713.1404-.004746.2804.018748.4116.069088s.2509.126504.3522.223979c.1012.09747.1818.21427.2371.34347.0552.12921.084.26819.0845.40871.0006.14052-.0271.27972-.0813.40936s-.1339.24707-.2344.34534l-5.94997 6.3c-.09795.10163-.21538.18245-.34527.23766-.1299.05521-.26959.08367-.41073.08367s-.28083-.02846-.41073-.08367c-.12989-.05521-.24732-.13603-.34527-.23766l-5.950001-6.3c-.192962-.20056-.29840394-.46953-.29315364-.74779.00525031-.27827.12076264-.54307.32115364-.73621z" fill="currentColor" fill-rule="evenodd"/></svg>
<path d="M14 6L7.83335 10.5L2 6" stroke="currentColor" stroke-width="2.25" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 205 B

After

Width:  |  Height:  |  Size: 840 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.95513 1.00169C7.89506 1.00645 7.83628 1.02054 7.78124 1.04337L1.31971 3.71042C1.2243 3.74983 1.14329 3.81414 1.08653 3.89553C1.02977 3.97691 0.999705 4.07186 1 4.16883V11.8366C1.00078 11.9327 1.03135 12.0265 1.08805 12.1069C1.14474 12.1873 1.22517 12.2508 1.31971 12.2898L7.78124 14.9568C7.85011 14.9853 7.92463 15 8 15C8.07536 15 8.14989 14.9853 8.21876 14.9568L14.6803 12.2898C14.7748 12.2508 14.8552 12.1873 14.9119 12.1069C14.9686 12.0265 14.9992 11.9327 15 11.8366V4.16883C15.0003 4.07186 14.9702 3.97691 14.9134 3.89553C14.8567 3.81414 14.7756 3.74983 14.6803 3.71042L8.21876 1.04337C8.13605 1.00911 8.04538 0.994778 7.95513 1.00169ZM8 2.04351L13.1378 4.16883L8 6.28894L2.86219 4.16883L8 2.04351ZM2.07693 4.93457L7.46153 7.15886V13.7328L2.07693 11.5084V4.93457ZM13.923 4.93457V11.5084L8.53846 13.7328V7.15886L13.923 4.93457Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 969 B

+1 -1
View File
@@ -19,7 +19,6 @@ export { default as CoinMarketCapIcon } from './logos/coinmarketcap.svg';
export { default as CoinsIcon } from './coins.svg'; export { default as CoinsIcon } from './coins.svg';
export { default as CommentIcon } from './comment.svg'; export { default as CommentIcon } from './comment.svg';
export { default as CopyIcon } from './copy.svg'; export { default as CopyIcon } from './copy.svg';
export { default as CubeIcon } from './cube.svg';
export { default as DepositIcon } from './deposit.svg'; export { default as DepositIcon } from './deposit.svg';
export { default as DepthChartIcon } from './depth-chart.svg'; export { default as DepthChartIcon } from './depth-chart.svg';
export { default as DiscordIcon } from './discord.svg'; export { default as DiscordIcon } from './discord.svg';
@@ -51,6 +50,7 @@ export { default as PriceChartIcon } from './price-chart.svg';
export { default as PrivacyIcon } from './privacy.svg'; export { default as PrivacyIcon } from './privacy.svg';
export { default as ProfileIcon } from './profile.svg'; export { default as ProfileIcon } from './profile.svg';
export { default as QrIcon } from './qr.svg'; export { default as QrIcon } from './qr.svg';
export { default as RewardStarIcon } from './reward-star.svg';
export { default as SearchIcon } from './search.svg'; export { default as SearchIcon } from './search.svg';
export { default as SendIcon } from './send.svg'; export { default as SendIcon } from './send.svg';
export { default as ShareIcon } from './share.svg'; export { default as ShareIcon } from './share.svg';
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 481 KiB

@@ -1,7 +1,11 @@
import { useMemo, useState } from 'react'; import { useState } from 'react';
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import { NotificationStatus } from '@/constants/notifications'; import {
type Notification,
type NotificationDisplayData,
NotificationStatus,
} from '@/constants/notifications';
import { ButtonShape, ButtonSize } from '@/constants/buttons'; import { ButtonShape, ButtonSize } from '@/constants/buttons';
import { useNotifications } from '@/hooks/useNotifications'; import { useNotifications } from '@/hooks/useNotifications';
import { useBreakpoints } from '@/hooks/useBreakpoints'; import { useBreakpoints } from '@/hooks/useBreakpoints';
@@ -13,54 +17,31 @@ import { Toast } from '@/components/Toast';
import { ToastArea } from '@/components/ToastArea'; import { ToastArea } from '@/components/ToastArea';
import { ToggleButton } from '@/components/ToggleButton'; import { ToggleButton } from '@/components/ToggleButton';
type ElementProps = {
notifications: {
notification: Notification<any, any>;
key: string;
displayData: NotificationDisplayData;
}[];
};
type StyleProps = { type StyleProps = {
className?: string; className?: string;
}; };
const MAX_TOASTS = 10; export const NotificationStack = ({ notifications, className }: ElementProps & StyleProps) => {
export const NotificationsToastArea = ({ className }: StyleProps) => {
const [shouldStackNotifications, setshouldStackNotifications] = useState(true); const [shouldStackNotifications, setshouldStackNotifications] = useState(true);
const { const { markUnseen, markSeen, onNotificationAction } = useNotifications();
notifications,
getKey,
getDisplayData,
markUnseen,
markSeen,
isMenuOpen,
onNotificationAction,
} = useNotifications();
const { isMobile } = useBreakpoints(); const { isMobile } = useBreakpoints();
const notificationMap = useMemo(() => { const hasMultipleToasts = notifications.length > 1;
return (
Object.values(notifications)
// Sort by time of first trigger
.sort(
(n1, n2) =>
n1.timestamps[NotificationStatus.Triggered]! -
n2.timestamps[NotificationStatus.Triggered]!
)
.map((notification) => ({
notification,
key: getKey(notification),
displayData: getDisplayData(notification),
}))
.filter(
({ displayData, notification }) =>
displayData && notification.status < NotificationStatus.Unseen
)
.slice(-MAX_TOASTS)
);
}, [notifications, getKey, getDisplayData]);
if (isMenuOpen) return null;
const hasMultipleToasts = notificationMap.length > 1;
return ( return (
<StyledToastArea swipeDirection={isMobile ? 'up' : 'right'} className={className}> <StyledToastArea
swipeDirection={isMobile ? 'up' : 'right'}
className={className}
size={notifications.length}
>
{hasMultipleToasts && ( {hasMultipleToasts && (
<StyledToggleButton <StyledToggleButton
shape={ButtonShape.Pill} shape={ButtonShape.Pill}
@@ -72,12 +53,12 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
</StyledToggleButton> </StyledToggleButton>
)} )}
{notificationMap.map(({ notification, key, displayData }, idx) => ( {notifications.map(({ notification, key, displayData }, idx) => (
<StyledToast <StyledToast
key={key} key={key}
isStacked={shouldStackNotifications} isStacked={idx > 0 && shouldStackNotifications}
isOpen={notification.status < NotificationStatus.Unseen} isOpen={notification.status < NotificationStatus.Unseen}
layer={notificationMap.length - 1 - idx} layer={idx}
notification={notification} notification={notification}
slotIcon={displayData.icon} slotIcon={displayData.icon}
slotTitle={displayData.title} slotTitle={displayData.title}
@@ -110,19 +91,13 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
); );
}; };
const StyledToastArea = styled(ToastArea)` const StyledToastArea = styled(ToastArea)<{ size: number }>`
position: absolute; position: relative;
width: min(17.5rem, 100%); ${({ size }) =>
inset: 0 0 0 auto; size &&
css`
padding: 0.75rem 0.75rem 0.75rem 0; padding-bottom: calc(${size - 1} * 2px);
`}
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 }>` const StyledToast = styled(Toast)<{ isStacked?: boolean; layer: number }>`
@@ -133,7 +108,7 @@ const StyledToast = styled(Toast)<{ isStacked?: boolean; layer: number }>`
position: absolute; position: absolute;
left: 0; left: 0;
top: calc(${layer} * 2px); top: calc(${layer} * 2px);
right: calc(${layer} * -2px); right: 0;
` `
: css` : css`
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
@@ -145,7 +120,7 @@ const StyledToggleButton = styled(ToggleButton)<{ isPressed: boolean }>`
pointer-events: auto; pointer-events: auto;
display: none; display: none;
position: absolute; position: absolute;
top: 4px; top: -0.5rem;
left: calc(50% - 0.75rem); left: calc(50% - 0.75rem);
--button-width: 2rem; --button-width: 2rem;
--button-height: 1rem; --button-height: 1rem;
@@ -0,0 +1,71 @@
import { useMemo } from 'react';
import { groupBy } from 'lodash';
import styled from 'styled-components';
import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins';
import { NotificationStatus } from '@/constants/notifications';
import { useNotifications } from '@/hooks/useNotifications';
import { NotificationStack } from './NotifcationStack';
type StyleProps = {
className?: string;
};
const MAX_TOASTS = 10;
export const NotificationsToastArea = ({ className }: StyleProps) => {
const { notifications, getKey, getDisplayData, isMenuOpen } = useNotifications();
const notificationMapByType = useMemo(() => {
const notificationMap = Object.values(notifications)
// Sort by time of first trigger
.sort(
(n1, n2) =>
n1.timestamps[NotificationStatus.Triggered]! -
n2.timestamps[NotificationStatus.Triggered]!
)
.map((notification) => ({
notification,
key: getKey(notification),
displayData: getDisplayData(notification),
}))
.filter(
({ displayData, notification }) =>
displayData && notification.status < NotificationStatus.Unseen
)
.slice(-MAX_TOASTS);
return groupBy(notificationMap, (notification) => notification.displayData?.groupKey);
}, [notifications, getKey, getDisplayData]);
if (isMenuOpen) return null;
return (
<StyledToastArea className={className}>
{Object.keys(notificationMapByType).map((groupKey) => (
<NotificationStack notifications={notificationMapByType[groupKey]} key={groupKey} />
))}
</StyledToastArea>
);
};
const StyledToastArea = styled.div`
${layoutMixins.scrollArea}
position: absolute;
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);
pointer-events: none;
@media ${breakpoints.mobile} {
width: 100%;
position: fixed;
}
`;
+4 -4
View File
@@ -11,16 +11,16 @@ class TestFlags {
const params = new URLSearchParams(queryParamsString); const params = new URLSearchParams(queryParamsString);
for (const [key, value] of params) { for (const [key, value] of params) {
this.queryParams[key] = value; this.queryParams[key.toLowerCase()] = value;
} }
} }
get displayInitializingMarkets() { get displayInitializingMarkets() {
return !!this.queryParams.displayInitializingMarkets; return !!this.queryParams.displayinitializingmarkets;
} }
get withCCTP() { get showMobileSignInOption() {
return !!this.queryParams.withCCTP; return !!this.queryParams.mobilesignin;
} }
} }
+17 -2
View File
@@ -1,5 +1,6 @@
import { createConfig, configureChains, mainnet, Chain, Connector, usePublicClient } from 'wagmi'; import { createConfig, configureChains, mainnet, Chain } from 'wagmi';
import { goerli } from 'wagmi/chains'; import { goerli } from 'wagmi/chains';
import type { PrivyClientConfig } from '@privy-io/react-auth';
import { import {
arbitrum, arbitrum,
@@ -28,6 +29,7 @@ import {
celoAlfajores, celoAlfajores,
scroll, scroll,
kava, kava,
sepolia,
} from 'viem/chains'; } from 'viem/chains';
import { alchemyProvider } from 'wagmi/providers/alchemy'; import { alchemyProvider } from 'wagmi/providers/alchemy';
@@ -55,7 +57,7 @@ import { isTruthy } from './isTruthy';
export const WAGMI_SUPPORTED_CHAINS: Chain[] = [ export const WAGMI_SUPPORTED_CHAINS: Chain[] = [
mainnet, mainnet,
goerli, goerli,
sepolia,
arbitrum, arbitrum,
arbitrumGoerli, arbitrumGoerli,
avalanche, avalanche,
@@ -84,6 +86,19 @@ export const WAGMI_SUPPORTED_CHAINS: Chain[] = [
kava, kava,
]; ];
export const privyConfig: PrivyClientConfig = {
embeddedWallets: {
createOnLogin: 'users-without-wallets',
requireUserPasswordOnCreate: true,
noPromptOnSignature: true,
},
loginMethods: ['email', 'sms', 'twitter', 'google', 'apple'],
appearance: {
theme: '#28283c',
},
defaultChain: sepolia
};
const { chains, publicClient, webSocketPublicClient } = configureChains( const { chains, publicClient, webSocketPublicClient } = configureChains(
WAGMI_SUPPORTED_CHAINS, WAGMI_SUPPORTED_CHAINS,
[ [
+6
View File
@@ -62,6 +62,12 @@ export const getWalletConnection = ({
type: WalletConnectionType.CosmosSigner, type: WalletConnectionType.CosmosSigner,
}; };
} }
case WalletConnectionType.Privy: {
return {
type: WalletConnectionType.Privy,
};
}
} }
} }
}; };
+29 -13
View File
@@ -30,6 +30,9 @@ import abacusStateManager from '@/lib/abacus';
import { isTruthy } from '@/lib/isTruthy'; import { isTruthy } from '@/lib/isTruthy';
import { truncateAddress } from '@/lib/wallet'; import { truncateAddress } from '@/lib/wallet';
import { DYDXBalancePanel } from './rewards/DYDXBalancePanel';
import { MigratePanel } from './rewards/MigratePanel';
const ENS_CHAIN_ID = 1; // Ethereum const ENS_CHAIN_ID = 1; // Ethereum
const Profile = () => { const Profile = () => {
@@ -175,8 +178,27 @@ const Profile = () => {
onClick={() => dispatch(openDialog({ type: DialogTypes.Help }))} onClick={() => dispatch(openDialog({ type: DialogTypes.Help }))}
/> />
</Styled.EqualGrid> </Styled.EqualGrid>
<MigratePanel />
<DYDXBalancePanel />
<Styled.EqualGrid> <Styled.EqualGrid>
<Styled.RewardsPanel
slotHeaderContent="Trading Rewards"
href={`/${chainTokenLabel}`}
hasSeparator
>
<Styled.Details
items={[
{
key: 'week-rewards',
label: stringGetter({ key: STRING_KEYS.THIS_WEEK }),
value: '-',
},
]}
layout="grid"
/>
</Styled.RewardsPanel>
<Panel <Panel
slotHeaderContent={stringGetter({ key: STRING_KEYS.FEES })} slotHeaderContent={stringGetter({ key: STRING_KEYS.FEES })}
href={`${AppRoute.Portfolio}/${PortfolioRoute.Fees}`} href={`${AppRoute.Portfolio}/${PortfolioRoute.Fees}`}
@@ -191,19 +213,6 @@ const Profile = () => {
layout="grid" layout="grid"
/> />
</Panel> </Panel>
<Styled.RewardsPanel
slotHeaderContent={stringGetter({ key: STRING_KEYS.REWARDS })}
href={`/${chainTokenLabel}`}
hasSeparator
>
<Styled.Details
items={[
{ key: 'maker', label: stringGetter({ key: STRING_KEYS.YOU_WILL_EARN }), value: '-' },
{ key: 'taker', label: stringGetter({ key: STRING_KEYS.EPOCH_ENDS_IN }), value: '-' },
]}
layout="grid"
/>
</Styled.RewardsPanel>
</Styled.EqualGrid> </Styled.EqualGrid>
<Styled.TablePanel <Styled.TablePanel
@@ -327,6 +336,13 @@ Styled.Details = styled(Details)`
`; `;
Styled.RewardsPanel = styled(Panel)` Styled.RewardsPanel = styled(Panel)`
align-self: flex-start;
&,
> * {
height: 100%;
}
dl { dl {
--details-grid-numColumns: 1; --details-grid-numColumns: 1;
} }
+25 -4
View File
@@ -17,11 +17,14 @@ import { TransferHistoryTable } from '@/views/tables/TransferHistoryTable';
import { Button } from '@/components/Button'; import { Button } from '@/components/Button';
import { Icon, IconName } from '@/components/Icon'; import { Icon, IconName } from '@/components/Icon';
import { NavigationMenu } from '@/components/NavigationMenu'; import { NavigationMenu } from '@/components/NavigationMenu';
import { Tag, TagType } from '@/components/Tag';
import { WithSidebar } from '@/components/WithSidebar'; import { WithSidebar } from '@/components/WithSidebar';
import { getOnboardingState, getSubaccount } from '@/state/accountSelectors'; import { getOnboardingState, getSubaccount, getTradeInfoNumbers } from '@/state/accountSelectors';
import { openDialog } from '@/state/dialogs'; import { openDialog } from '@/state/dialogs';
import { shortenNumberForDisplay } from '@/lib/numbers';
import { PortfolioNavMobile } from './PortfolioNavMobile'; import { PortfolioNavMobile } from './PortfolioNavMobile';
import { LoadingSpace } from '@/components/Loading/LoadingSpinner'; import { LoadingSpace } from '@/components/Loading/LoadingSpinner';
@@ -42,6 +45,10 @@ export default () => {
const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {}; const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {};
const { nativeTokenBalance } = useAccountBalance(); const { nativeTokenBalance } = useAccountBalance();
const { numTotalPositions, numTotalOpenOrders } = useSelector(getTradeInfoNumbers, shallowEqual) || {};
const numPositions = shortenNumberForDisplay(numTotalPositions);
const numOrders = shortenNumberForDisplay(numTotalOpenOrders);
const usdcBalance = freeCollateral?.current || 0; const usdcBalance = freeCollateral?.current || 0;
useDocumentTitle(stringGetter({ key: STRING_KEYS.PORTFOLIO })); useDocumentTitle(stringGetter({ key: STRING_KEYS.PORTFOLIO }));
@@ -118,14 +125,28 @@ export default () => {
}, },
{ {
value: PortfolioRoute.Positions, value: PortfolioRoute.Positions,
slotBefore: <Styled.Icon iconName={IconName.Cube} />, slotBefore: <Styled.Icon iconName={IconName.Positions} />,
label: stringGetter({ key: STRING_KEYS.POSITIONS }), label: (
<>
{stringGetter({ key: STRING_KEYS.POSITIONS })}
{numPositions > 0 && (
<Tag type={TagType.Number}> {numPositions} </Tag>
)}
</>
),
href: PortfolioRoute.Positions, href: PortfolioRoute.Positions,
}, },
{ {
value: PortfolioRoute.Orders, value: PortfolioRoute.Orders,
slotBefore: <Styled.Icon iconName={IconName.OrderPending} />, slotBefore: <Styled.Icon iconName={IconName.OrderPending} />,
label: stringGetter({ key: STRING_KEYS.ORDERS }), label: (
<>
{stringGetter({ key: STRING_KEYS.ORDERS })}
{numOrders > 0 && (
<Tag type={TagType.Number}> {numOrders} </Tag>
)}
</>
),
href: PortfolioRoute.Orders, href: PortfolioRoute.Orders,
}, },
{ {
+2 -12
View File
@@ -2,7 +2,6 @@ import type { ElementType } from 'react';
import styled, { AnyStyledComponent } from 'styled-components'; import styled, { AnyStyledComponent } from 'styled-components';
import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import breakpoints from '@/styles/breakpoints';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
import { useAccountBalance, useAccounts, useTokenConfigs, useStringGetter } from '@/hooks'; import { useAccountBalance, useAccounts, useTokenConfigs, useStringGetter } from '@/hooks';
@@ -33,7 +32,7 @@ export const DYDXBalancePanel = () => {
const { chainTokenLabel } = useTokenConfigs(); const { chainTokenLabel } = useTokenConfigs();
return ( return (
<Styled.Panel <Panel
slotHeader={ slotHeader={
<Styled.Header> <Styled.Header>
<Styled.Title> <Styled.Title>
@@ -109,21 +108,12 @@ export const DYDXBalancePanel = () => {
]} ]}
/> />
</Styled.Content> </Styled.Content>
</Styled.Panel> </Panel>
); );
}; };
const Styled: Record<string, AnyStyledComponent> = {}; 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` Styled.Header = styled.div`
${layoutMixins.spacedRow} ${layoutMixins.spacedRow}
gap: 1rem; gap: 1rem;
+5 -10
View File
@@ -70,10 +70,12 @@ const EstimatedRewards = () => {
const { data, isLoading } = useQuery({ const { data, isLoading } = useQuery({
enabled: !!dydxAddress, enabled: !!dydxAddress,
queryKey: `launch_incentives_rewards_${dydxAddress ?? ''}`, queryKey: `launch_incentives_rewards_${dydxAddress ?? ''}_${SEASON_NUMBER}`,
queryFn: async () => { queryFn: async () => {
if (!dydxAddress) return undefined; if (!dydxAddress) return undefined;
const resp = await fetch(`https://cloud.chaoslabs.co/query/api/dydx/points/${dydxAddress}?n=${SEASON_NUMBER}`); const resp = await fetch(
`https://cloud.chaoslabs.co/query/api/dydx/points/${dydxAddress}?n=${SEASON_NUMBER}`
);
return (await resp.json())?.incentivePoints; return (await resp.json())?.incentivePoints;
}, },
onError: (error: Error) => log('LaunchIncentives/fetchPoints', error), onError: (error: Error) => log('LaunchIncentives/fetchPoints', error),
@@ -151,15 +153,8 @@ const LaunchIncentivesContent = () => {
const Styled: Record<string, AnyStyledComponent> = {}; const Styled: Record<string, AnyStyledComponent> = {};
Styled.Panel = styled(Panel)` Styled.Panel = styled(Panel)`
--panel-paddingY: 1rem;
--panel-paddingX: 1.5rem;
background-color: var(--color-layer-4); background-color: var(--color-layer-4);
width: 100%; width: 100%;
@media ${breakpoints.tablet} {
--panel-paddingY: 1.5rem;
}
`; `;
Styled.ForV4 = styled.span` Styled.ForV4 = styled.span`
@@ -223,7 +218,7 @@ Styled.Column = styled.div`
Styled.EstimatedRewardsCard = styled.div` Styled.EstimatedRewardsCard = styled.div`
${layoutMixins.spacedRow} ${layoutMixins.spacedRow}
padding: 1rem 1.25rem; padding: 1rem 1.25rem;
min-width: 21.25rem; min-width: 19rem;
height: calc(100% - calc(1.5rem * 2)); height: calc(100% - calc(1.5rem * 2));
margin: 1.5rem; margin: 1.5rem;
+5 -11
View File
@@ -7,8 +7,6 @@ import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons';
import { useAccountBalance, useBreakpoints, useStringGetter } from '@/hooks'; import { useAccountBalance, useBreakpoints, useStringGetter } from '@/hooks';
import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
import { Details } from '@/components/Details'; import { Details } from '@/components/Details';
@@ -28,7 +26,7 @@ import { MustBigNumber } from '@/lib/numbers';
const TOKEN_MIGRATION_LEARN_MORE_LINK = const TOKEN_MIGRATION_LEARN_MORE_LINK =
'https://www.dydx.foundation/blog/update-on-exploring-the-future-of-dydx'; 'https://www.dydx.foundation/blog/update-on-exploring-the-future-of-dydx';
export const MigratePanel = () => { export const MigratePanel = ({ className }: { className?: string }) => {
const { isNotTablet } = useBreakpoints(); const { isNotTablet } = useBreakpoints();
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
@@ -45,6 +43,7 @@ export const MigratePanel = () => {
return isNotTablet ? ( return isNotTablet ? (
<Styled.MigratePanel <Styled.MigratePanel
className={className}
slotHeader={<Styled.Title>{stringGetter({ key: STRING_KEYS.MIGRATE })}</Styled.Title>} slotHeader={<Styled.Title>{stringGetter({ key: STRING_KEYS.MIGRATE })}</Styled.Title>}
slotRight={ slotRight={
<Styled.MigrateAction> <Styled.MigrateAction>
@@ -74,6 +73,7 @@ export const MigratePanel = () => {
</Styled.MigratePanel> </Styled.MigratePanel>
) : ( ) : (
<Styled.MigratePanel <Styled.MigratePanel
className={className}
slotHeader={ slotHeader={
<Styled.MobileMigrateHeader> <Styled.MobileMigrateHeader>
<h3> <h3>
@@ -134,17 +134,11 @@ export const MigratePanel = () => {
const Styled: Record<string, AnyStyledComponent> = {}; const Styled: Record<string, AnyStyledComponent> = {};
Styled.MigratePanel = styled(Panel)` Styled.MigratePanel = styled(Panel)`
--panel-paddingX: 1.5rem;
width: 100%; width: 100%;
background-image: url('/dots-background.svg'); background-image: url('/dots-background.svg');
background-position: right; background-position: right;
background-repeat: no-repeat; background-repeat: no-repeat;
@media ${breakpoints.tablet} {
--panel-paddingY: 1.5rem;
--panel-content-paddingY: 1rem;
}
`; `;
Styled.Title = styled.h3` Styled.Title = styled.h3`
@@ -157,7 +151,7 @@ Styled.Title = styled.h3`
Styled.MigrateAction = styled.div` Styled.MigrateAction = styled.div`
${layoutMixins.flexEqualColumns} ${layoutMixins.flexEqualColumns}
align-items: center; align-items: center;
margin-right: 1rem; margin: 1rem;
gap: 1rem; gap: 1rem;
padding: 1rem; padding: 1rem;
width: 100%; width: 100%;
@@ -197,7 +191,7 @@ Styled.MobileMigrateHeader = styled.div`
font: var(--font-small-book); font: var(--font-small-book);
color: var(--color-text-0); color: var(--color-text-0);
padding: 1rem 1.5rem 0; padding: 1rem 1rem 0;
h3 { h3 {
${layoutMixins.inlineRow} ${layoutMixins.inlineRow}
+7 -1
View File
@@ -1,5 +1,6 @@
import styled, { AnyStyledComponent } from 'styled-components'; import styled, { AnyStyledComponent } from 'styled-components';
import { breakpoints } from '@/styles';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
import { useStringGetter } from '@/hooks'; import { useStringGetter } from '@/hooks';
@@ -51,6 +52,7 @@ export const RewardsHelpPanel = () => {
const Styled: Record<string, AnyStyledComponent> = {}; const Styled: Record<string, AnyStyledComponent> = {};
Styled.HelpCard = styled(Panel)` Styled.HelpCard = styled(Panel)`
--panel-content-paddingX: 0;
--panel-content-paddingY: 0; --panel-content-paddingY: 0;
width: 100%; width: 100%;
height: max-content; height: max-content;
@@ -64,11 +66,15 @@ Styled.Header = styled.div`
${layoutMixins.spacedRow} ${layoutMixins.spacedRow}
gap: 1ch; gap: 1ch;
padding: 1.25rem 1.5rem; padding: 1rem 1rem;
border-bottom: var(--border-width) solid var(--border-color); border-bottom: var(--border-width) solid var(--border-color);
font: var(--font-small-book); font: var(--font-small-book);
@media ${breakpoints.notTablet} {
padding: 1.5rem 1.25rem;
}
h3 { h3 {
font: var(--font-medium-book); font: var(--font-medium-book);
color: var(--color-text-2); color: var(--color-text-2);
+39 -17
View File
@@ -1,15 +1,18 @@
import styled, { AnyStyledComponent } from 'styled-components'; import styled, { AnyStyledComponent } from 'styled-components';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { ButtonAction, ButtonSize } from '@/constants/buttons'; import { ButtonAction, ButtonSize } from '@/constants/buttons';
import { DialogTypes } from '@/constants/dialogs'; import { DialogTypes } from '@/constants/dialogs';
import { AppRoute } from '@/constants/routes';
import { useBreakpoints, useStringGetter, useURLConfigs } from '@/hooks'; import { useBreakpoints, useStringGetter, useURLConfigs } from '@/hooks';
import { breakpoints } from '@/styles'; import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
import { BackButton } from '@/components/BackButton';
import { Panel } from '@/components/Panel'; import { Panel } from '@/components/Panel';
import { IconName } from '@/components/Icon'; import { IconName } from '@/components/Icon';
import { IconButton } from '@/components/IconButton'; import { IconButton } from '@/components/IconButton';
@@ -20,12 +23,14 @@ import { openDialog } from '@/state/dialogs';
import { DYDXBalancePanel } from './DYDXBalancePanel'; import { DYDXBalancePanel } from './DYDXBalancePanel';
import { MigratePanel } from './MigratePanel'; import { MigratePanel } from './MigratePanel';
import { LaunchIncentivesPanel } from './LaunchIncentivesPanel'; import { LaunchIncentivesPanel } from './LaunchIncentivesPanel';
import { RewardsHelpPanel } from './RewardsHelpPanel';
const RewardsPage = () => { const RewardsPage = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
const { governanceLearnMore, stakingLearnMore } = useURLConfigs(); const { governanceLearnMore, stakingLearnMore } = useURLConfigs();
const { isTablet, isNotTablet } = useBreakpoints(); const { isTablet, isNotTablet } = useBreakpoints();
const navigate = useNavigate();
const panelArrow = ( const panelArrow = (
<Styled.Arrow> <Styled.Arrow>
@@ -39,13 +44,16 @@ const RewardsPage = () => {
return ( return (
<Styled.Page> <Styled.Page>
{import.meta.env.VITE_V3_TOKEN_ADDRESS && <MigratePanel />} {isTablet && (
<Styled.MobileHeader>
<BackButton onClick={() => navigate(AppRoute.Profile)} />
{stringGetter({ key: STRING_KEYS.TRADING_REWARDS })}
</Styled.MobileHeader>
)}
{import.meta.env.VITE_V3_TOKEN_ADDRESS && isNotTablet && <MigratePanel />}
{isTablet ? ( {isTablet ? (
<> <LaunchIncentivesPanel />
<LaunchIncentivesPanel />
<DYDXBalancePanel />
</>
) : ( ) : (
<Styled.PanelRowIncentivesAndBalance> <Styled.PanelRowIncentivesAndBalance>
<LaunchIncentivesPanel /> <LaunchIncentivesPanel />
@@ -55,7 +63,9 @@ const RewardsPage = () => {
<Styled.PanelRow> <Styled.PanelRow>
<Styled.Panel <Styled.Panel
slotHeader={<Styled.Title>{stringGetter({ key: STRING_KEYS.GOVERNANCE })}</Styled.Title>} slotHeaderContent={
<Styled.Title>{stringGetter({ key: STRING_KEYS.GOVERNANCE })}</Styled.Title>
}
slotRight={panelArrow} slotRight={panelArrow}
onClick={() => dispatch(openDialog({ type: DialogTypes.ExternalNavKeplr }))} onClick={() => dispatch(openDialog({ type: DialogTypes.ExternalNavKeplr }))}
> >
@@ -68,7 +78,9 @@ const RewardsPage = () => {
</Styled.Panel> </Styled.Panel>
<Styled.Panel <Styled.Panel
slotHeader={<Styled.Title>{stringGetter({ key: STRING_KEYS.STAKING })}</Styled.Title>} slotHeaderContent={
<Styled.Title>{stringGetter({ key: STRING_KEYS.STAKING })}</Styled.Title>
}
slotRight={panelArrow} slotRight={panelArrow}
onClick={() => dispatch(openDialog({ type: DialogTypes.ExternalNavKeplr }))} onClick={() => dispatch(openDialog({ type: DialogTypes.ExternalNavKeplr }))}
> >
@@ -80,6 +92,8 @@ const RewardsPage = () => {
</Styled.Description> </Styled.Description>
</Styled.Panel> </Styled.Panel>
</Styled.PanelRow> </Styled.PanelRow>
<RewardsHelpPanel />
</Styled.Page> </Styled.Page>
); );
}; };
@@ -95,33 +109,41 @@ Styled.Page = styled.div`
align-items: center; align-items: center;
> * { > * {
--content-max-width: 70rem; --content-max-width: 80rem;
max-width: min(calc(100vw - 4rem), var(--content-max-width)); max-width: min(calc(100vw - 4rem), var(--content-max-width));
} }
@media ${breakpoints.tablet} { @media ${breakpoints.tablet} {
padding: 1.25rem; --stickyArea-topHeight: var(--page-header-height-mobile);
padding: 0 1rem 1rem;
> * { > * {
max-width: calc(100vw - 2.5rem); max-width: calc(100vw - 2rem);
width: 100%; width: 100%;
} }
} }
`; `;
Styled.Panel = styled(Panel)` Styled.MobileHeader = styled.header`
--panel-paddingX: 1.5rem; ${layoutMixins.contentSectionDetachedScrollable}
${layoutMixins.stickyHeader}
z-index: 2;
padding: 1.25rem 0;
margin-bottom: -1.5rem;
@media ${breakpoints.tablet} { font: var(--font-large-medium);
--panel-paddingY: 1.5rem; color: var(--color-text-2);
--panel-content-paddingY: 1rem; background-color: var(--color-layer-2);
} `;
Styled.Panel = styled(Panel)`
height: fit-content;
`; `;
Styled.Title = styled.h3` Styled.Title = styled.h3`
font: var(--font-medium-book); font: var(--font-medium-book);
color: var(--color-text-2); color: var(--color-text-2);
padding: 1rem 1.5rem 0; margin-bottom: -1rem;
`; `;
Styled.Description = styled.div` Styled.Description = styled.div`
+1 -8
View File
@@ -1,10 +1,8 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { SubaccountPosition } from '@/constants/abacus';
import { OnboardingState, OnboardingSteps } from '@/constants/account'; import { OnboardingState, OnboardingSteps } from '@/constants/account';
import { import {
getExistingOpenPositions,
getOnboardingGuards, getOnboardingGuards,
getOnboardingState, getOnboardingState,
getSubaccountId, getSubaccountId,
@@ -60,13 +58,8 @@ export const calculateIsAccountViewOnly = createSelector(
); );
/** /**
* @description calculate whether the subaccount has open positions * @description calculate whether the subaccount has uncommitted positions
*/ */
export const calculateHasOpenPositions = createSelector(
[getExistingOpenPositions],
(openPositions?: SubaccountPosition[]) => (openPositions?.length || 0) > 0
);
export const calculateHasUncommittedOrders = createSelector( export const calculateHasUncommittedOrders = createSelector(
[getUncommittedOrderClientIds], [getUncommittedOrderClientIds],
(uncommittedOrderClientIds: number[]) => uncommittedOrderClientIds.length > 0 (uncommittedOrderClientIds: number[]) => uncommittedOrderClientIds.length > 0
+11 -5
View File
@@ -2,6 +2,7 @@ import { OrderSide } from '@dydxprotocol/v4-client-js';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { import {
type AbacusOrderStatuses,
type SubaccountOrder, type SubaccountOrder,
type SubaccountFill, type SubaccountFill,
type SubaccountFundingPayment, type SubaccountFundingPayment,
@@ -43,7 +44,6 @@ export const getSubaccountBuyingPower = (state: RootState) => state.account.suba
export const getSubaccountEquity = (state: RootState) => state.account.subaccount?.equity; export const getSubaccountEquity = (state: RootState) => state.account.subaccount?.equity;
export const getSubaccountHistoricalPnl = (state: RootState) => state.account?.historicalPnl; export const getSubaccountHistoricalPnl = (state: RootState) => state.account?.historicalPnl;
/** /**
* @param state * @param state
* @returns list of a subaccount's open positions. Each item in the list is an open position in a different market. * @returns list of a subaccount's open positions. Each item in the list is an open position in a different market.
@@ -258,6 +258,14 @@ export const getCurrentMarketFundingPayments = createSelector(
!currentMarketId ? [] : marketFundingPayments[currentMarketId] !currentMarketId ? [] : marketFundingPayments[currentMarketId]
); );
/**
* @param state
* @returns boolean on whether an order status is considered open
*/
const isOpenOrderStatus = (status: AbacusOrderStatuses) => {
return status !== AbacusOrderStatus.filled && status !== AbacusOrderStatus.cancelled;
};
/** /**
* @param state * @param state
* @returns Total numbers of the subaccount's open positions, open orders and fills * @returns Total numbers of the subaccount's open positions, open orders and fills
@@ -266,8 +274,7 @@ export const getTradeInfoNumbers = createSelector(
[getExistingOpenPositions, getSubaccountOrders, getSubaccountFills, getSubaccountFundingPayments], [getExistingOpenPositions, getSubaccountOrders, getSubaccountFills, getSubaccountFundingPayments],
(positions, orders, fills, fundingPayments) => ({ (positions, orders, fills, fundingPayments) => ({
numTotalPositions: positions?.length, numTotalPositions: positions?.length,
numTotalOpenOrders: orders?.filter((order) => order.status !== AbacusOrderStatus.cancelled) numTotalOpenOrders: orders?.filter((order) => isOpenOrderStatus(order.status)).length,
.length,
numTotalFills: fills?.length, numTotalFills: fills?.length,
numTotalFundingPayments: fundingPayments?.length, numTotalFundingPayments: fundingPayments?.length,
}) })
@@ -281,8 +288,7 @@ export const getCurrentMarketTradeInfoNumbers = createSelector(
[getCurrentMarketOrders, getCurrentMarketFills, getCurrentMarketFundingPayments], [getCurrentMarketOrders, getCurrentMarketFills, getCurrentMarketFundingPayments],
(marketOrders, marketFills, marketFundingPayments) => { (marketOrders, marketFills, marketFundingPayments) => {
return { return {
numOpenOrders: marketOrders?.filter((order) => order.status !== AbacusOrderStatus.cancelled) numOpenOrders: marketOrders?.filter((order) => isOpenOrderStatus(order.status)).length,
.length,
numFills: marketFills?.length, numFills: marketFills?.length,
numFundingPayments: marketFundingPayments?.length, numFundingPayments: marketFundingPayments?.length,
}; };
+6 -1
View File
@@ -26,7 +26,12 @@ export const calculateFundingRateHistory = createSelector(
return data.map(({ effectiveAtMilliseconds, rate }) => ({ return data.map(({ effectiveAtMilliseconds, rate }) => ({
time: effectiveAtMilliseconds, time: effectiveAtMilliseconds,
fundingRate: rate, fundingRate: rate,
direction: rate < 0 ? FundingDirection.ToLong : FundingDirection.ToShort, direction:
rate === 0
? FundingDirection.None
: rate < 0
? FundingDirection.ToLong
: FundingDirection.ToShort,
})); }));
} }
); );
-7
View File
@@ -29,7 +29,6 @@ export const MarketDetails: React.FC = () => {
if (!configs) return null; if (!configs) return null;
const { const {
basePositionNotional,
tickSize, tickSize,
stepSize, stepSize,
initialMarginFraction, initialMarginFraction,
@@ -119,12 +118,6 @@ export const MarketDetails: React.FC = () => {
tooltip: 'initial-margin-fraction', tooltip: 'initial-margin-fraction',
value: <Output useGrouping value={initialMarginFraction} type={OutputType.SmallPercent} />, value: <Output useGrouping value={initialMarginFraction} type={OutputType.SmallPercent} />,
}, },
{
key: 'base-position-notional',
label: stringGetter({ key: STRING_KEYS.BASE_POSITION_NOTIONAL }),
tooltip: 'base-position-notional',
value: <Output useGrouping value={basePositionNotional} type={OutputType.Number} />,
},
]; ];
return ( return (
+220
View File
@@ -0,0 +1,220 @@
import { useMemo } from 'react';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import type { RenderTooltipParams } from '@visx/xychart/lib/components/Tooltip';
import { shallowEqual, useSelector } from 'react-redux';
import type { Nullable } from '@/constants/abacus';
import {
DepthChartDatum,
DepthChartPoint,
DepthChartSeries,
SERIES_KEY_FOR_ORDER_SIDE,
} from '@/constants/charts';
import { STRING_KEYS } from '@/constants/localization';
import { useStringGetter } from '@/hooks';
import { useOrderbookValuesForDepthChart } from '@/hooks/useOrderbookValues';
import { TooltipContent } from '@/components/visx/TooltipContent';
import { Details } from '@/components/Details';
import { Output, OutputType } from '@/components/Output';
import { getCurrentMarketAssetData } from '@/state/assetsSelectors';
import { MustBigNumber } from '@/lib/numbers';
type DepthChartTooltipProps = {
chartPointAtPointer: DepthChartPoint;
isEditingOrder?: boolean;
stepSizeDecimals: Nullable<number>;
tickSizeDecimals: Nullable<number>;
} & Pick<RenderTooltipParams<DepthChartDatum>, 'colorScale' | 'tooltipData'>;
export const DepthChartTooltipContent = ({
chartPointAtPointer,
colorScale,
isEditingOrder,
stepSizeDecimals,
tickSizeDecimals,
tooltipData,
}: DepthChartTooltipProps) => {
const { nearestDatum } = tooltipData || {};
const stringGetter = useStringGetter();
const { spread, spreadPercent, midMarketPrice } = useOrderbookValuesForDepthChart();
const { id = '' } = useSelector(getCurrentMarketAssetData, shallowEqual) ?? {};
const priceImpact = useMemo(() => {
if (nearestDatum) {
const depthChartSeries = nearestDatum.key as DepthChartSeries;
return {
[DepthChartSeries.Bids]: MustBigNumber(nearestDatum?.datum.price)
.minus(chartPointAtPointer.price)
.div(nearestDatum?.datum.price),
[DepthChartSeries.Asks]: MustBigNumber(chartPointAtPointer.price)
.minus(nearestDatum?.datum.price)
.div(chartPointAtPointer.price),
[DepthChartSeries.MidMarket]: undefined,
}[depthChartSeries];
}
return undefined;
}, [nearestDatum, chartPointAtPointer.price]);
if (!isEditingOrder && !nearestDatum?.datum) return null;
return (
<TooltipContent
accentColor={
nearestDatum?.key &&
colorScale?.(
isEditingOrder ? SERIES_KEY_FOR_ORDER_SIDE[chartPointAtPointer.side] : nearestDatum.key
)
}
>
<h4>
{isEditingOrder
? stringGetter({ key: STRING_KEYS.RELEASE_TO_EDIT })
: nearestDatum &&
{
[DepthChartSeries.Bids]: stringGetter({ key: STRING_KEYS.BIDS }),
[DepthChartSeries.Asks]: stringGetter({ key: STRING_KEYS.ASKS }),
[DepthChartSeries.MidMarket]: stringGetter({ key: STRING_KEYS.MID_MARKET }),
}[nearestDatum.key]}
</h4>
<Details
layout="column"
items={
isEditingOrder
? [
{
key: 'side',
label: stringGetter({ key: STRING_KEYS.SIDE }),
value: (
<Output
type={OutputType.Text}
value={
{
[OrderSide.BUY]: stringGetter({
key: STRING_KEYS.BUY,
}),
[OrderSide.SELL]: stringGetter({
key: STRING_KEYS.SELL,
}),
}[chartPointAtPointer.side]
}
/>
),
},
{
key: 'limitPrice',
label: stringGetter({ key: STRING_KEYS.LIMIT_PRICE }),
value: (
<Output
type={OutputType.Fiat}
value={chartPointAtPointer.price}
useGrouping={false}
/>
),
},
{
key: 'size',
label: stringGetter({ key: STRING_KEYS.AMOUNT }),
value: (
<Output
type={OutputType.Asset}
value={chartPointAtPointer.size}
fractionDigits={stepSizeDecimals}
tag={id}
useGrouping={false}
/>
),
},
]
: nearestDatum?.key === DepthChartSeries.MidMarket
? [
{
key: 'midMarketPrice',
label: stringGetter({ key: STRING_KEYS.PRICE }),
value: (
<Output type={OutputType.Fiat} value={midMarketPrice} useGrouping={false} />
),
},
{
key: 'spread',
label: stringGetter({ key: STRING_KEYS.ORDERBOOK_SPREAD }),
value: (
<>
<Output
type={OutputType.Fiat}
value={spread}
fractionDigits={tickSizeDecimals}
useGrouping={false}
/>
<Output
type={OutputType.SmallPercent}
value={spreadPercent}
withParentheses
/>
</>
),
},
]
: [
{
key: 'price',
label: stringGetter({ key: STRING_KEYS.PRICE }),
value: (
<>
{nearestDatum &&
{
[DepthChartSeries.Bids]: '≥',
[DepthChartSeries.Asks]: '≤',
}[nearestDatum.key]}
<Output
type={OutputType.Fiat}
value={nearestDatum?.datum.price}
useGrouping={false}
/>
</>
),
},
{
key: 'depth',
label: stringGetter({ key: STRING_KEYS.TOTAL_SIZE }),
value: (
<Output
type={OutputType.Asset}
value={nearestDatum?.datum.depth}
fractionDigits={stepSizeDecimals}
tag={id}
useGrouping={false}
/>
),
},
{
key: 'cost',
label: stringGetter({ key: STRING_KEYS.TOTAL_COST }),
value: (
<Output
useGrouping
type={OutputType.Fiat}
value={
nearestDatum
? nearestDatum?.datum.price * nearestDatum?.datum.depth
: undefined
}
/>
),
},
{
key: 'priceImpact',
label: stringGetter({ key: STRING_KEYS.PRICE_IMPACT }),
value: <Output useGrouping type={OutputType.Percent} value={priceImpact} />,
},
]
}
/>
</TooltipContent>
);
};
@@ -1,14 +1,20 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import styled, { AnyStyledComponent, css, keyframes } from 'styled-components'; import styled, { AnyStyledComponent, css, keyframes } from 'styled-components';
import { useSelector, shallowEqual } from 'react-redux'; import { useSelector, shallowEqual } from 'react-redux';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { StringGetterFunction, STRING_KEYS } from '@/constants/localization'; import {
DepthChartDatum,
DepthChartPoint,
DepthChartSeries,
SERIES_KEY_FOR_ORDER_SIDE,
} from '@/constants/charts';
import { StringGetterFunction } from '@/constants/localization';
import { useBreakpoints } from '@/hooks'; import { useBreakpoints } from '@/hooks';
import { useOrderbookValuesForDepthChart } from '@/hooks/useOrderbookValues';
import { MustBigNumber } from '@/lib/numbers'; import { getCurrentMarketConfig } from '@/state/perpetualsSelectors';
import { getCurrentMarketConfig, getCurrentMarketOrderbook } from '@/state/perpetualsSelectors';
import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; import { getCurrentMarketAssetData } from '@/state/assetsSelectors';
import { XYChartWithPointerEvents } from '@/components/visx/XYChartWithPointerEvents'; import { XYChartWithPointerEvents } from '@/components/visx/XYChartWithPointerEvents';
@@ -21,24 +27,25 @@ import {
darkTheme, darkTheme,
DataProvider, DataProvider,
EventEmitterProvider, EventEmitterProvider,
type EventHandlerParams,
} from '@visx/xychart'; } from '@visx/xychart';
import { LinearGradient } from '@visx/gradient'; import { LinearGradient } from '@visx/gradient';
import { curveStepAfter } from '@visx/curve'; import { curveStepAfter } from '@visx/curve';
import type { Point } from '@visx/point'; import { Point } from '@visx/point';
import Tooltip from '@/components/visx/XYChartTooltipWithBounds'; import Tooltip from '@/components/visx/XYChartTooltipWithBounds';
import { TooltipContent } from '@/components/visx/TooltipContent';
import { AxisLabelOutput } from '@/components/visx/AxisLabelOutput'; import { AxisLabelOutput } from '@/components/visx/AxisLabelOutput';
import { Details } from '@/components/Details';
import { LoadingSpace } from '@/components/Loading/LoadingSpinner'; import { LoadingSpace } from '@/components/Loading/LoadingSpinner';
import { Output, OutputType } from '@/components/Output'; import { OutputType } from '@/components/Output';
import { OrderSide } from '@dydxprotocol/v4-client-js'; import { MustBigNumber } from '@/lib/numbers';
import { DepthChartTooltipContent } from './Tooltip';
// @ts-ignore // @ts-ignore
const theme = buildChartTheme({ const theme = buildChartTheme({
...darkTheme, ...darkTheme,
colors: ['var(--color-positive)', 'var(--color-negative)', 'white'], // categorical colors, mapped to series via `dataKey`s colors: ['var(--color-positive)', 'var(--color-negative)', 'var(--color-layer-6)'], // categorical colors, mapped to series via `dataKey`s
}); });
const clamp = (n: number, min: number, max: number) => Math.max(min, Math.min(max, n)); const clamp = (n: number, min: number, max: number) => Math.max(min, Math.min(max, n));
@@ -58,30 +65,6 @@ const formatNumber = (n: number, selectedLocale: string, isCompact: boolean = n
: formattedNumber; : formattedNumber;
}; };
enum DepthChartSeries {
Asks = 'Asks',
Bids = 'Bids',
MidMarket = 'MidMarket',
}
type DepthChartDatum = {
size: number;
price: number;
depth: number;
seriesKey: DepthChartSeries;
};
const seriesKeyForOrderSide = {
[OrderSide.BUY]: DepthChartSeries.Bids,
[OrderSide.SELL]: DepthChartSeries.Asks,
};
type DepthChartPoint = {
side: OrderSide;
price: number;
size: number;
};
export const DepthChart = ({ export const DepthChart = ({
onChartClick, onChartClick,
stringGetter, stringGetter,
@@ -96,8 +79,6 @@ export const DepthChart = ({
const { isMobile } = useBreakpoints(); const { isMobile } = useBreakpoints();
// Chart data // Chart data
const orderbook = useSelector(getCurrentMarketOrderbook, shallowEqual);
const { id = '' } = useSelector(getCurrentMarketAssetData, shallowEqual) ?? {}; const { id = '' } = useSelector(getCurrentMarketAssetData, shallowEqual) ?? {};
const { stepSizeDecimals, tickSizeDecimals } = const { stepSizeDecimals, tickSizeDecimals } =
useSelector(getCurrentMarketConfig, shallowEqual) ?? {}; useSelector(getCurrentMarketConfig, shallowEqual) ?? {};
@@ -112,43 +93,15 @@ export const DepthChart = ({
midMarketPrice, midMarketPrice,
spread, spread,
spreadPercent, spreadPercent,
} = useMemo(() => { orderbook,
const bids = (orderbook?.bids?.toArray() ?? []) } = useOrderbookValuesForDepthChart();
.filter(Boolean)
.map((datum) => ({ ...datum, seriesKey: DepthChartSeries.Bids } as DepthChartDatum));
const asks = (orderbook?.asks?.toArray() ?? [])
.filter(Boolean)
.map((datum) => ({ ...datum, seriesKey: DepthChartSeries.Asks } as DepthChartDatum));
const lowestBid = bids[bids.length - 1];
const highestBid = bids[0];
const lowestAsk = asks[0];
const highestAsk = asks[asks.length - 1];
const midMarketPrice = orderbook?.midPrice;
const spread = MustBigNumber(lowestAsk?.price ?? 0).minus(highestBid?.price ?? 0);
const spreadPercent = orderbook?.spreadPercent;
return {
bids,
asks,
lowestBid,
highestBid,
lowestAsk,
highestAsk,
midMarketPrice,
spread,
spreadPercent,
};
}, [orderbook]);
// Chart state // Chart state
const [isPointerPressed, setIsPointerPressed] = useState(false); const [isPointerPressed, setIsPointerPressed] = useState(false);
const [chartPointAtPointer, setChartPointAtPointer] = useState<DepthChartPoint>(); const [chartPointAtPointer, setChartPointAtPointer] = useState<DepthChartPoint>();
const isEditingOrder = isPointerPressed && chartPointAtPointer; const isEditingOrder = Boolean(isPointerPressed && chartPointAtPointer);
const [zoomDomain, setZoomDomain] = useState<undefined | number>(); const [zoomDomain, setZoomDomain] = useState<undefined | number>();
@@ -193,12 +146,24 @@ export const DepthChart = ({
}, [orderbook, zoomDomain]); }, [orderbook, zoomDomain]);
const getChartPoint = useCallback( const getChartPoint = useCallback(
({ x: price, y: size }: Point) => (point: Point | EventHandlerParams<object>) => {
({ let price, size;
side: price < midMarketPrice! ? OrderSide.BUY : OrderSide.SELL, if (point instanceof Point) {
const { x, y } = point as Point;
price = x;
size = y;
} else {
const { svgPoint: { x, y } = {} } = point as EventHandlerParams<object>;
price = x;
size = y;
}
return {
side: MustBigNumber(price).lt(midMarketPrice!) ? OrderSide.BUY : OrderSide.SELL,
price, price,
size, size,
} as DepthChartPoint), } as DepthChartPoint;
},
[midMarketPrice] [midMarketPrice]
); );
@@ -256,7 +221,7 @@ export const DepthChart = ({
}} }}
onPointerUp={(point) => point && onChartClick?.(getChartPoint(point))} onPointerUp={(point) => point && onChartClick?.(getChartPoint(point))}
onPointerMove={(point) => point && setChartPointAtPointer(getChartPoint(point))} onPointerMove={(point) => point && setChartPointAtPointer(getChartPoint(point))}
onPointerPressedChange={setIsPointerPressed} onPointerPressedChange={(isPointerPressed) => setIsPointerPressed(isPointerPressed)}
> >
<Axis <Axis
orientation="bottom" orientation="bottom"
@@ -363,7 +328,7 @@ export const DepthChart = ({
<Styled.XAxisLabelOutput <Styled.XAxisLabelOutput
type={OutputType.Fiat} type={OutputType.Fiat}
value={ value={
isEditingOrder isEditingOrder && chartPointAtPointer
? chartPointAtPointer.price ? chartPointAtPointer.price
: tooltipData!.nearestDatum?.datum.price : tooltipData!.nearestDatum?.datum.price
} }
@@ -374,8 +339,8 @@ export const DepthChart = ({
[DepthChartSeries.Bids]: 'var(--color-positive)', [DepthChartSeries.Bids]: 'var(--color-positive)',
[DepthChartSeries.MidMarket]: 'var(--color-layer-6)', [DepthChartSeries.MidMarket]: 'var(--color-layer-6)',
}[ }[
isEditingOrder isEditingOrder && chartPointAtPointer
? seriesKeyForOrderSide[chartPointAtPointer.side] ? SERIES_KEY_FOR_ORDER_SIDE[chartPointAtPointer.side]
: (tooltipData!.nearestDatum?.key as DepthChartSeries) : (tooltipData!.nearestDatum?.key as DepthChartSeries)
] ]
} }
@@ -389,7 +354,7 @@ export const DepthChart = ({
<Styled.YAxisLabelOutput <Styled.YAxisLabelOutput
type={OutputType.Asset} type={OutputType.Asset}
value={ value={
isEditingOrder isEditingOrder && chartPointAtPointer
? chartPointAtPointer.size ? chartPointAtPointer.size
: tooltipData!.nearestDatum?.datum.depth : tooltipData!.nearestDatum?.datum.depth
} }
@@ -400,8 +365,8 @@ export const DepthChart = ({
[DepthChartSeries.Bids]: 'var(--color-positive)', [DepthChartSeries.Bids]: 'var(--color-positive)',
[DepthChartSeries.MidMarket]: 'var(--color-layer-6)', [DepthChartSeries.MidMarket]: 'var(--color-layer-6)',
}[ }[
isEditingOrder isEditingOrder && chartPointAtPointer
? seriesKeyForOrderSide[chartPointAtPointer.side] ? SERIES_KEY_FOR_ORDER_SIDE[chartPointAtPointer.side]
: (tooltipData!.nearestDatum?.key as DepthChartSeries) : (tooltipData!.nearestDatum?.key as DepthChartSeries)
] ]
} }
@@ -410,181 +375,22 @@ export const DepthChart = ({
} }
snapTooltipToDatumX={!isEditingOrder} snapTooltipToDatumX={!isEditingOrder}
snapTooltipToDatumY={isEditingOrder ? false : isMobile} snapTooltipToDatumY={isEditingOrder ? false : isMobile}
renderTooltip={({ tooltipData, colorScale }) => { renderTooltip={({ tooltipData, colorScale }) =>
const { nearestDatum } = tooltipData || {}; chartPointAtPointer && (
<DepthChartTooltipContent
if (!isEditingOrder && !nearestDatum?.datum) return null; {...{
tooltipData,
return ( colorScale,
<TooltipContent isEditingOrder,
accentColor={colorScale?.( chartPointAtPointer,
isEditingOrder stringGetter,
? seriesKeyForOrderSide[chartPointAtPointer.side] selectedLocale,
: nearestDatum.key stepSizeDecimals,
)} tickSizeDecimals,
> }}
<h4> />
{isEditingOrder )
? 'Release mouse to edit order' }
: {
[DepthChartSeries.Bids]: 'Bids',
[DepthChartSeries.Asks]: 'Asks',
[DepthChartSeries.MidMarket]: 'Mid-Market',
}[nearestDatum.key]}
</h4>
<Details
layout="column"
items={
isEditingOrder
? [
{
key: 'side',
label: stringGetter({ key: STRING_KEYS.SIDE }),
value: (
<Output
type={OutputType.Text}
value={
{
[OrderSide.BUY]: stringGetter({
key: STRING_KEYS.BUY,
}),
[OrderSide.SELL]: stringGetter({
key: STRING_KEYS.SELL,
}),
}[chartPointAtPointer.side]
}
/>
),
},
{
key: 'limitPrice',
label: stringGetter({ key: STRING_KEYS.LIMIT_PRICE }),
value: (
<Output
type={OutputType.Fiat}
value={chartPointAtPointer.price}
useGrouping={false}
/>
),
},
{
key: 'size',
label: stringGetter({ key: STRING_KEYS.AMOUNT }),
value: (
<Output
type={OutputType.Asset}
value={chartPointAtPointer.size}
fractionDigits={stepSizeDecimals}
tag={id}
useGrouping={false}
/>
),
},
]
: nearestDatum?.key === DepthChartSeries.MidMarket
? [
{
key: 'midMarketPrice',
// label: stringGetter({ key: STRING_KEYS.ORDERBOOK_MID_MARKET_PRICE }),
label: stringGetter({ key: STRING_KEYS.PRICE }),
value: (
<Output
type={OutputType.Fiat}
value={midMarketPrice}
useGrouping={false}
/>
),
},
{
key: 'spread',
label: stringGetter({ key: STRING_KEYS.ORDERBOOK_SPREAD }),
value: (
<>
<Output
type={OutputType.Fiat}
value={spread}
fractionDigits={tickSizeDecimals}
useGrouping={false}
/>
<Output
type={OutputType.SmallPercent}
value={spreadPercent}
withParentheses
/>
</>
),
},
]
: [
{
key: 'price',
label: stringGetter({ key: STRING_KEYS.PRICE }),
value: (
<>
{nearestDatum &&
{
[DepthChartSeries.Bids]: '≥',
[DepthChartSeries.Asks]: '≤',
}[nearestDatum.key]}
<Output
type={OutputType.Fiat}
value={nearestDatum?.datum.price}
useGrouping={false}
/>
</>
),
},
{
key: 'depth',
label: stringGetter({ key: STRING_KEYS.TOTAL_SIZE }),
value: (
<Output
type={OutputType.Asset}
value={nearestDatum?.datum.depth}
fractionDigits={stepSizeDecimals}
tag={id}
useGrouping={false}
/>
),
},
{
key: 'cost',
label: stringGetter({ key: STRING_KEYS.TOTAL_COST }),
value: (
<Output
useGrouping
type={OutputType.Fiat}
value={nearestDatum?.datum.price * nearestDatum?.datum.depth}
/>
),
},
{
key: 'priceImpact',
label: stringGetter({ key: STRING_KEYS.PRICE_IMPACT }),
value: (
<Output
useGrouping
type={OutputType.Percent}
value={{
[DepthChartSeries.Asks]: () =>
MustBigNumber(nearestDatum.datum.price)
.minus(lowestAsk.price)
.div(nearestDatum.datum.price),
[DepthChartSeries.Bids]: () =>
MustBigNumber(highestBid.price)
.minus(nearestDatum.datum.price)
.div(highestBid.price),
}[nearestDatum.key]()}
/>
),
},
]
}
/>
</TooltipContent>
);
}}
/> />
</XYChartWithPointerEvents> </XYChartWithPointerEvents>
</EventEmitterProvider> </EventEmitterProvider>
+107
View File
@@ -0,0 +1,107 @@
import type { RenderTooltipParams } from '@visx/xychart/lib/components/Tooltip';
import { TooltipContent } from '@/components/visx/TooltipContent';
import { FundingRateResolution, type FundingChartDatum } from '@/constants/charts';
import { STRING_KEYS } from '@/constants/localization';
import { FundingDirection } from '@/constants/markets';
import { useStringGetter } from '@/hooks';
import { Details, DetailsItem } from '@/components/Details';
import { Output, OutputType, ShowSign } from '@/components/Output';
type FundingChartTooltipProps = {
fundingRateView: FundingRateResolution;
latestDatum: FundingChartDatum;
} & Pick<RenderTooltipParams<FundingChartDatum>, 'tooltipData'>;
export const FundingChartTooltipContent = ({
fundingRateView,
latestDatum,
tooltipData,
}: FundingChartTooltipProps) => {
const { nearestDatum } = tooltipData || {};
const stringGetter = useStringGetter();
const tooltipDatum = nearestDatum?.datum ?? latestDatum;
const isShowingCurrentFundingRate = tooltipDatum === latestDatum;
return (
<TooltipContent
accentColor={
{
[FundingDirection.ToLong]: 'var(--color-negative)',
[FundingDirection.ToShort]: 'var(--color-positive)',
[FundingDirection.None]: 'var(--color-layer-6)',
}[tooltipDatum.direction]
}
>
<h4>
{isShowingCurrentFundingRate
? stringGetter({ key: STRING_KEYS.CURRENT_FUNDING_RATE })
: stringGetter({ key: STRING_KEYS.HISTORICAL_FUNDING_RATE })}
</h4>
<Details
layout="column"
items={
[
{
key: 'direction',
label: stringGetter({ key: STRING_KEYS.DIRECTION }),
value: (
<Output
type={OutputType.Text}
value={
{
[FundingDirection.ToLong]: `${stringGetter({
key: STRING_KEYS.SHORT_POSITION_SHORT,
})} → ${stringGetter({
key: STRING_KEYS.LONG_POSITION_SHORT,
})}`,
[FundingDirection.ToShort]: `${stringGetter({
key: STRING_KEYS.LONG_POSITION_SHORT,
})} → ${stringGetter({
key: STRING_KEYS.SHORT_POSITION_SHORT,
})}`,
[FundingDirection.None]: undefined,
}[tooltipDatum.direction]
}
/>
),
},
{
key: 'fundingRate',
label: stringGetter({
key: {
[FundingRateResolution.OneHour]: STRING_KEYS.RATE_1H,
[FundingRateResolution.EightHour]: STRING_KEYS.RATE_8H,
[FundingRateResolution.Annualized]: STRING_KEYS.ANNUALIZED,
}[fundingRateView],
}),
value: (
<Output
type={OutputType.SmallPercent}
value={
{
[FundingRateResolution.OneHour]: tooltipDatum.fundingRate,
[FundingRateResolution.EightHour]: tooltipDatum.fundingRate * 8,
[FundingRateResolution.Annualized]: tooltipDatum.fundingRate * (24 * 365),
}[fundingRateView]
}
showSign={ShowSign.Both}
/>
),
},
{
key: 'time',
label: isShowingCurrentFundingRate
? 'Time Remaining'
: stringGetter({ key: STRING_KEYS.TIME }),
value: <Output type={OutputType.DateTime} value={tooltipDatum.time} />,
},
].filter(Boolean) as Array<DetailsItem>
}
/>
</TooltipContent>
);
};
@@ -3,39 +3,27 @@ import { shallowEqual, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent, css } from 'styled-components'; import styled, { type AnyStyledComponent, css } from 'styled-components';
import { curveMonotoneX, curveStepAfter } from '@visx/curve'; import { curveMonotoneX, curveStepAfter } from '@visx/curve';
import { useBreakpoints, useStringGetter } from '@/hooks';
import { ButtonSize } from '@/constants/buttons'; import { ButtonSize } from '@/constants/buttons';
import { FundingRateResolution, type FundingChartDatum } from '@/constants/charts';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { SMALL_PERCENT_DECIMALS, TINY_PERCENT_DECIMALS } from '@/constants/numbers';
import { FundingDirection } from '@/constants/markets'; import { FundingDirection } from '@/constants/markets';
import { SMALL_PERCENT_DECIMALS, TINY_PERCENT_DECIMALS } from '@/constants/numbers';
import { useBreakpoints, useStringGetter } from '@/hooks';
import { breakpoints } from '@/styles'; import { breakpoints } from '@/styles';
import { Details, DetailsItem } from '@/components/Details'; import { Output, OutputType } from '@/components/Output';
import { Output, OutputType, ShowSign } from '@/components/Output';
import { LoadingSpace } from '@/components/Loading/LoadingSpinner'; import { LoadingSpace } from '@/components/Loading/LoadingSpinner';
import { ToggleGroup } from '@/components/ToggleGroup'; import { ToggleGroup } from '@/components/ToggleGroup';
import { TimeSeriesChart } from '@/components/visx/TimeSeriesChart'; import { TimeSeriesChart } from '@/components/visx/TimeSeriesChart';
import { AxisLabelOutput } from '@/components/visx/AxisLabelOutput'; import { AxisLabelOutput } from '@/components/visx/AxisLabelOutput';
import { TooltipContent } from '@/components/visx/TooltipContent';
import type { TooltipContextType } from '@visx/xychart'; import type { TooltipContextType } from '@visx/xychart';
import { calculateFundingRateHistory } from '@/state/perpetualsCalculators'; import { calculateFundingRateHistory } from '@/state/perpetualsCalculators';
import { MustBigNumber } from '@/lib/numbers'; import { MustBigNumber } from '@/lib/numbers';
import { FundingChartTooltipContent } from './Tooltip';
enum FundingRateResolution {
OneHour = 'OneHour',
EightHour = 'EightHour',
Annualized = 'Annualized',
}
type FundingChartDatum = {
time: number;
fundingRate: number;
direction: FundingDirection;
};
const FUNDING_RATE_TIME_RESOLUTION = 60 * 60 * 1000; // 1 hour const FUNDING_RATE_TIME_RESOLUTION = 60 * 60 * 1000; // 1 hour
@@ -116,114 +104,20 @@ export const FundingChart = ({ selectedLocale }: ElementProps) => {
{ {
[FundingDirection.ToLong]: 'var(--color-negative)', [FundingDirection.ToLong]: 'var(--color-negative)',
[FundingDirection.ToShort]: 'var(--color-positive)', [FundingDirection.ToShort]: 'var(--color-positive)',
[FundingDirection.None]: 'var(--color-layer-6)',
}[tooltipDatum.direction] }[tooltipDatum.direction]
} }
/> />
); );
}} }}
renderTooltip={({ tooltipData }) => { renderTooltip={({ tooltipData }) => (
const { nearestDatum } = tooltipData || {}; <FundingChartTooltipContent
fundingRateView={fundingRateView}
const tooltipDatum = nearestDatum?.datum ?? latestDatum; tooltipData={tooltipData}
const isShowingCurrentFundingRate = tooltipDatum === latestDatum; latestDatum={latestDatum}
/>
return ( )}
<TooltipContent onTooltipContext={(tooltipContext) => setTooltipContext(tooltipContext)}
accentColor={
{
[FundingDirection.ToLong]: 'var(--color-negative)',
[FundingDirection.ToShort]: 'var(--color-positive)',
}[tooltipDatum.direction]
}
>
<h4>
{isShowingCurrentFundingRate
? stringGetter({ key: STRING_KEYS.CURRENT_FUNDING_RATE })
: stringGetter({ key: STRING_KEYS.HISTORICAL_FUNDING_RATE })}
</h4>
<Details
layout="column"
items={
[
{
key: 'direction',
label: stringGetter({ key: STRING_KEYS.DIRECTION }),
value: (
<Output
type={OutputType.Text}
value={
{
[FundingDirection.ToLong]: `${stringGetter({
key: STRING_KEYS.SHORT_POSITION_SHORT,
})} ${stringGetter({
key: STRING_KEYS.LONG_POSITION_SHORT,
})}`,
[FundingDirection.ToShort]: `${stringGetter({
key: STRING_KEYS.LONG_POSITION_SHORT,
})} ${stringGetter({
key: STRING_KEYS.SHORT_POSITION_SHORT,
})}`,
}[tooltipDatum.direction]
}
/>
),
},
{
key: 'fundingRate1h',
label: stringGetter({ key: STRING_KEYS.RATE_1H }),
value: (
<Output
type={OutputType.SmallPercent}
value={tooltipDatum.fundingRate}
showSign={ShowSign.Both}
/>
),
},
{
key: 'fundingRate8h',
label: stringGetter({ key: STRING_KEYS.RATE_8H }),
value: (
<Output
type={OutputType.SmallPercent}
value={tooltipDatum.fundingRate * 8}
// value={
// Math.sign(tooltipDatum.fundingRate) *
// ((Math.abs(tooltipDatum.fundingRate) + 1) ** 8 - 1)
// }
showSign={ShowSign.Both}
/>
),
},
{
key: 'fundingRateAnnualized',
label: stringGetter({ key: STRING_KEYS.ANNUALIZED }),
value: (
<Output
type={OutputType.SmallPercent}
value={tooltipDatum.fundingRate * (24 * 365)}
// value={
// Math.sign(tooltipDatum.fundingRate) *
// ((Math.abs(tooltipDatum.fundingRate) + 1) ** (24 * 365) - 1)
// }
showSign={ShowSign.Both}
/>
),
},
{
key: 'time',
label: isShowingCurrentFundingRate
? 'Time Remaining'
: stringGetter({ key: STRING_KEYS.TIME }),
value: <Output type={OutputType.DateTime} value={tooltipDatum.time} />,
},
].filter(Boolean) as Array<DetailsItem>
}
/>
</TooltipContent>
);
}}
onTooltipContext={setTooltipContext}
minZoomDomain={FUNDING_RATE_TIME_RESOLUTION * 4} minZoomDomain={FUNDING_RATE_TIME_RESOLUTION * 4}
numGridLines={1} numGridLines={1}
slotEmpty={<LoadingSpace id="funding-chart-loading" />} slotEmpty={<LoadingSpace id="funding-chart-loading" />}
@@ -290,6 +184,7 @@ Styled.FundingRateToggle = styled.div`
Styled.CurrentFundingRate = styled.div<{ isShowing?: boolean }>` Styled.CurrentFundingRate = styled.div<{ isShowing?: boolean }>`
place-self: start center; place-self: start center;
padding: clamp(1.5rem, 9rem - 15%, 4rem); padding: clamp(1.5rem, 9rem - 15%, 4rem);
pointer-events: none;
font: var(--font-large-book); font: var(--font-large-book);
@@ -1,6 +1,4 @@
import { useState } from 'react';
import styled, { AnyStyledComponent } from 'styled-components'; import styled, { AnyStyledComponent } from 'styled-components';
import { useDispatch } from 'react-redux';
import { AlertType } from '@/constants/alerts'; import { AlertType } from '@/constants/alerts';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
@@ -13,7 +11,7 @@ import { Button } from '@/components/Button';
import { Icon } from '@/components/Icon'; import { Icon } from '@/components/Icon';
import { Link } from '@/components/Link'; import { Link } from '@/components/Link';
import { useAccounts, useStringGetter, useURLConfigs } from '@/hooks'; import { useAccounts, useBreakpoints, useStringGetter, useURLConfigs } from '@/hooks';
import { useDisplayedWallets } from '@/hooks/useDisplayedWallets'; import { useDisplayedWallets } from '@/hooks/useDisplayedWallets';
import { breakpoints } from '@/styles'; import { breakpoints } from '@/styles';
@@ -27,6 +25,8 @@ export const ChooseWallet = () => {
const { selectWalletType, selectedWalletType, selectedWalletError } = useAccounts(); const { selectWalletType, selectedWalletType, selectedWalletError } = useAccounts();
const { isMobile } = useBreakpoints();
return ( return (
<> <>
{selectedWalletType && selectedWalletError && ( {selectedWalletType && selectedWalletError && (
@@ -56,7 +56,11 @@ export const ChooseWallet = () => {
slotLeft={<Styled.Icon iconComponent={wallets[walletType].icon} />} slotLeft={<Styled.Icon iconComponent={wallets[walletType].icon} />}
size={ButtonSize.Small} size={ButtonSize.Small}
> >
<div>{stringGetter({ key: wallets[walletType].stringKey })}</div> <div>
{walletType !== WalletType.Privy && import.meta.env.VITE_PRIVY_APP_ID
? stringGetter({ key: wallets[walletType].stringKey })
: `Socials ${isMobile ? '' : '(Email, SMS, etc.)'}`}
</div>
</Styled.WalletButton> </Styled.WalletButton>
))} ))}
</Styled.Wallets> </Styled.Wallets>
@@ -34,11 +34,30 @@ export const ChainSelectMenu = ({ label, selectedChain, onSelectChain }: Element
slotBefore: <Styled.Img src={chain.iconUrl} alt="" />, slotBefore: <Styled.Img src={chain.iconUrl} alt="" />,
})); }));
const selectedOption = chains.find((item) => item.type === selectedChain); const selectedOption = selectedChain === 'coinbase' ? {
stringKey: 'Coinbase',
iconUrl: '/wallets/coinbase-wallet.png',
} : chains.find((item) => item.type === selectedChain);
const exchanges = [
{
value: 'coinbase',
label: 'Coinbase',
onSelect: () => {
onSelectChain('coinbase');
},
slotBefore: <Styled.Img src="/wallets/coinbase-wallet.png" alt="" />,
}
];
return ( return (
<SearchSelectMenu <SearchSelectMenu
items={[ items={[
{
group: 'exchanges',
groupLabel: 'Exchanges',
items: exchanges,
},
{ {
group: 'chains', group: 'chains',
groupLabel: stringGetter({ key: STRING_KEYS.CHAINS }), groupLabel: stringGetter({ key: STRING_KEYS.CHAINS }),
@@ -38,12 +38,14 @@ import abacusStateManager from '@/lib/abacus';
import { MustBigNumber } from '@/lib/numbers'; import { MustBigNumber } from '@/lib/numbers';
import { getNobleChainId, NATIVE_TOKEN_ADDRESS } from '@/lib/squid'; import { getNobleChainId, NATIVE_TOKEN_ADDRESS } from '@/lib/squid';
import { log } from '@/lib/telemetry'; import { log } from '@/lib/telemetry';
import { parseWalletError } from '@/lib/wallet'; import { parseWalletError, truncateAddress } from '@/lib/wallet';
import { ChainSelectMenu } from './ChainSelectMenu'; import { ChainSelectMenu } from './ChainSelectMenu';
import { TokenSelectMenu } from './TokenSelectMenu'; import { TokenSelectMenu } from './TokenSelectMenu';
import { DepositButtonAndReceipt } from './DepositForm/DepositButtonAndReceipt'; import { DepositButtonAndReceipt } from './DepositForm/DepositButtonAndReceipt';
import { QrCode } from '@/components/QrCode';
import { CopyButton } from '@/components/CopyButton';
type DepositFormProps = { type DepositFormProps = {
onDeposit?: () => void; onDeposit?: () => void;
@@ -55,8 +57,9 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const [error, setError] = useState<Error | null>(null); const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { selectedNetwork } = useSelectedNetwork(); const { selectedNetwork } = useSelectedNetwork();
const [isCoinbase, setIsCoinbase] = useState(false);
const { evmAddress, signerWagmi, publicClientWagmi } = useAccounts(); const { evmAddress, signerWagmi, publicClientWagmi, nobleAddress } = useAccounts();
const { addTransferNotification } = useLocalNotifications(); const { addTransferNotification } = useLocalNotifications();
@@ -133,7 +136,10 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
}, [error]); }, [error]);
const onSelectChain = useCallback((chain: string) => { const onSelectChain = useCallback((chain: string) => {
if (chain) { if (chain === 'coinbase') {
setIsCoinbase(true);
} else if (chain) {
setIsCoinbase(false);
abacusStateManager.clearTransferInputValues(); abacusStateManager.clearTransferInputValues();
abacusStateManager.setTransferValue({ abacusStateManager.setTransferValue({
field: TransferInputField.chain, field: TransferInputField.chain,
@@ -364,9 +370,43 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
return <LoadingSpace id="DepositForm" />; return <LoadingSpace id="DepositForm" />;
} }
if (isCoinbase) {
return (
<Styled.Form onSubmit={onSubmit}>
<ChainSelectMenu
selectedChain={isCoinbase ? 'coinbase' : chainIdStr || undefined}
onSelectChain={onSelectChain}
/>
{nobleAddress && (
<>
<Styled.ReceiveQr
side="bottom"
detailItems={[
{
key: 'nobleAddress',
label: "Noble Address",
value: nobleAddress,
},
]}
>
<QrCode size={432} value={nobleAddress} />
</Styled.ReceiveQr>
<p>
* This address only supports transfers of USDC to the Noble chain.
</p>
<CopyButton value={nobleAddress} />
</>
)}
</Styled.Form>
);
}
return ( return (
<Styled.Form onSubmit={onSubmit}> <Styled.Form onSubmit={onSubmit}>
<ChainSelectMenu selectedChain={chainIdStr || undefined} onSelectChain={onSelectChain} /> <ChainSelectMenu
selectedChain={isCoinbase ? 'coinbase' : chainIdStr || undefined}
onSelectChain={onSelectChain}
/>
<TokenSelectMenu selectedToken={sourceToken || undefined} onSelectToken={onSelectToken} /> <TokenSelectMenu selectedToken={sourceToken || undefined} onSelectToken={onSelectToken} />
<Styled.WithDetailsReceipt side="bottom" detailItems={amountInputReceipt}> <Styled.WithDetailsReceipt side="bottom" detailItems={amountInputReceipt}>
<FormInput <FormInput
@@ -427,3 +467,13 @@ Styled.TransactionInfo = styled.span`
Styled.FormInputButton = styled(Button)` Styled.FormInputButton = styled(Button)`
${formMixins.inputInnerButton} ${formMixins.inputInnerButton}
`; `;
Styled.Exchange = styled.div`
${layoutMixins.flexColumn}
gap: 1rem;
align-items: center;
`;
Styled.ReceiveQr = styled(WithDetailsReceipt)`
`;
@@ -108,20 +108,6 @@ export const DepositButtonAndReceipt = ({
const totalFees = (summary?.bridgeFee || 0) + (summary?.gasFee || 0); const totalFees = (summary?.bridgeFee || 0) + (summary?.gasFee || 0);
const { toAmount, toAmountMin } = useMemo(() => {
if (isCctp) {
return {
toAmount: summary?.toAmount,
toAmountMin: summary?.toAmountMin,
};
} else {
return {
toAmount: summary?.toAmount && formatUnits(BigInt(summary.toAmount), usdcDecimals),
toAmountMin: summary?.toAmountMin && formatUnits(BigInt(summary.toAmountMin), usdcDecimals),
};
}
}, [isCctp, summary]);
const submitButtonReceipt = [ const submitButtonReceipt = [
{ {
key: 'expected-deposit-amount', key: 'expected-deposit-amount',
@@ -130,7 +116,7 @@ export const DepositButtonAndReceipt = ({
{stringGetter({ key: STRING_KEYS.EXPECTED_DEPOSIT_AMOUNT })} <Tag>{usdcLabel}</Tag> {stringGetter({ key: STRING_KEYS.EXPECTED_DEPOSIT_AMOUNT })} <Tag>{usdcLabel}</Tag>
</span> </span>
), ),
value: <Output type={OutputType.Fiat} fractionDigits={TOKEN_DECIMALS} value={toAmount} />, value: <Output type={OutputType.Fiat} fractionDigits={TOKEN_DECIMALS} value={summary?.toAmount} />,
subitems: [ subitems: [
{ {
key: 'minimum-deposit-amount', key: 'minimum-deposit-amount',
@@ -140,7 +126,7 @@ export const DepositButtonAndReceipt = ({
</span> </span>
), ),
value: ( value: (
<Output type={OutputType.Fiat} fractionDigits={TOKEN_DECIMALS} value={toAmountMin} /> <Output type={OutputType.Fiat} fractionDigits={TOKEN_DECIMALS} value={summary?.toAmountMin} />
), ),
tooltip: 'minimum-deposit-amount', tooltip: 'minimum-deposit-amount',
}, },
@@ -55,7 +55,7 @@ export const WithdrawButtonAndReceipt = ({
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
const { leverage } = useSelector(getSubaccount, shallowEqual) || {}; const { leverage } = useSelector(getSubaccount, shallowEqual) || {};
const { isCctp, summary, requestPayload } = useSelector(getTransferInputs, shallowEqual) || {}; const { summary, requestPayload } = useSelector(getTransferInputs, shallowEqual) || {};
const canAccountTrade = useSelector(calculateCanAccountTrade, shallowEqual); const canAccountTrade = useSelector(calculateCanAccountTrade, shallowEqual);
const { usdcLabel } = useTokenConfigs(); const { usdcLabel } = useTokenConfigs();
@@ -85,26 +85,6 @@ export const WithdrawButtonAndReceipt = ({
const totalFees = (summary?.bridgeFee || 0) + (summary?.gasFee || 0); const totalFees = (summary?.bridgeFee || 0) + (summary?.gasFee || 0);
const { toAmount, toAmountMin } = useMemo(() => {
if (isCctp) {
return {
toAmount: summary?.toAmount,
toAmountMin: summary?.toAmountMin,
};
} else {
return {
toAmount:
summary?.toAmount &&
withdrawToken?.decimals &&
formatUnits(BigInt(summary.toAmount), withdrawToken.decimals),
toAmountMin:
summary?.toAmountMin &&
withdrawToken?.decimals &&
formatUnits(BigInt(summary.toAmountMin), withdrawToken.decimals),
};
}
}, [isCctp, summary, withdrawToken]);
const submitButtonReceipt = [ const submitButtonReceipt = [
{ {
key: 'total-fees', key: 'total-fees',
@@ -153,7 +133,7 @@ export const WithdrawButtonAndReceipt = ({
{withdrawToken && <Tag>{withdrawToken?.symbol}</Tag>} {withdrawToken && <Tag>{withdrawToken?.symbol}</Tag>}
</span> </span>
), ),
value: <Output type={OutputType.Asset} value={toAmount} fractionDigits={TOKEN_DECIMALS} />, value: <Output type={OutputType.Asset} value={summary?.toAmount} fractionDigits={TOKEN_DECIMALS} />,
subitems: [ subitems: [
{ {
key: 'minimum-amount-received', key: 'minimum-amount-received',
@@ -164,7 +144,7 @@ export const WithdrawButtonAndReceipt = ({
</span> </span>
), ),
value: ( value: (
<Output type={OutputType.Asset} value={toAmountMin} fractionDigits={TOKEN_DECIMALS} /> <Output type={OutputType.Asset} value={summary?.toAmountMin} fractionDigits={TOKEN_DECIMALS} />
), ),
tooltip: 'minimum-amount-received', tooltip: 'minimum-amount-received',
}, },
+5 -4
View File
@@ -10,6 +10,9 @@ import { STRING_KEYS, StringGetterFunction, TOOLTIP_STRING_KEYS } from '@/consta
import { isMainnet } from '@/constants/networks'; import { isMainnet } from '@/constants/networks';
import { DydxChainAsset, wallets } from '@/constants/wallets'; import { DydxChainAsset, wallets } from '@/constants/wallets';
import { layoutMixins } from '@/styles/layoutMixins';
import { headerMixins } from '@/styles/headerMixins';
import { import {
useAccounts, useAccounts,
useBreakpoints, useBreakpoints,
@@ -35,10 +38,8 @@ import { getOnboardingState, getSubaccount } from '@/state/accountSelectors';
import { isTruthy } from '@/lib/isTruthy'; import { isTruthy } from '@/lib/isTruthy';
import { truncateAddress } from '@/lib/wallet'; import { truncateAddress } from '@/lib/wallet';
import { layoutMixins } from '@/styles/layoutMixins';
import { headerMixins } from '@/styles/headerMixins';
import { MustBigNumber } from '@/lib/numbers'; import { MustBigNumber } from '@/lib/numbers';
import { testFlags } from '@/lib/testFlags';
export const AccountMenu = () => { export const AccountMenu = () => {
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
@@ -178,7 +179,7 @@ export const AccountMenu = () => {
}, },
...(onboardingState === OnboardingState.AccountConnected && hdKey ...(onboardingState === OnboardingState.AccountConnected && hdKey
? [ ? [
!isMainnet && { (!isMainnet || testFlags.showMobileSignInOption) && {
value: 'MobileQrSignIn', value: 'MobileQrSignIn',
icon: <Icon iconName={IconName.Qr} />, icon: <Icon iconName={IconName.Qr} />,
label: stringGetter({ key: STRING_KEYS.TITLE_SIGN_INTO_MOBILE }), label: stringGetter({ key: STRING_KEYS.TITLE_SIGN_INTO_MOBILE }),
@@ -0,0 +1,78 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { STRING_KEYS } from '@/constants/localization';
import { useStringGetter } from '@/hooks';
import { Details } from '@/components/Details';
import { Notification, NotificationProps } from '@/components/Notification';
import { Output, OutputType } from '@/components/Output';
import { Icon, IconName } from '@/components/Icon';
type ElementProps = {
data: {
BLOCK_REWARD_AMOUNT: string;
BLOCK_REWARD_HEIGHT: string;
BLOCK_REWARD_TIME_MILLISECONDS: string;
TOKEN_NAME: string;
};
};
export type BlockRewardNotificationProps = NotificationProps & ElementProps;
export const BlockRewardNotification = ({
isToast,
data,
notification,
}: BlockRewardNotificationProps) => {
const stringGetter = useStringGetter();
const { BLOCK_REWARD_AMOUNT, TOKEN_NAME } = data;
return (
<Styled.Notification
isToast={isToast}
notification={notification}
slotIcon={<Icon iconName={IconName.RewardStar} />}
slotTitle={stringGetter({ key: STRING_KEYS.TRADING_REWARD_RECEIVED })}
slotCustomContent={
<Styled.Details
items={[
{
key: 'block_reward',
label: stringGetter({ key: STRING_KEYS.BLOCK_REWARD }),
value: (
<Styled.Output
type={OutputType.Asset}
value={BLOCK_REWARD_AMOUNT}
tag={TOKEN_NAME}
/>
),
},
]}
/>
}
/>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Details = styled(Details)`
--details-item-height: 1.5rem;
dd {
color: var(--color-text-2);
}
`;
Styled.Notification = styled(Notification)`
background-image: url('/dots-background-2.svg');
background-size: cover;
`;
Styled.Output = styled(Output)`
&:before {
content: '+';
color: var(--color-positive);
margin-right: 0.5ch;
}
`;
+7
View File
@@ -2,6 +2,7 @@ import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import path from 'path'; import path from 'path';
import svgr from 'vite-plugin-svgr'; import svgr from 'vite-plugin-svgr';
import { VitePWA } from 'vite-plugin-pwa';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({ export default defineConfig(({ mode }) => ({
@@ -50,6 +51,12 @@ export default defineConfig(({ mode }) => ({
svgr({ svgr({
exportAsDefault: true, exportAsDefault: true,
}), }),
VitePWA({
registerType: 'autoUpdate',
devOptions: {
enabled: true,
},
}),
], ],
publicDir: 'public', publicDir: 'public',
})); }));